catalogue
3 use interceptors to cover the Array prototype
4 mount the interceptor method on the attribute of the array
6 Where does the dependency list exist
8 get the Observer instance in the interceptor
9 send notification to array dependencies
10 detect the changes of elements in the array
11 detect changes in new elements
1 how to track changes
Before es6, js did not provide the ability of meta programming, and pages did not provide the ability to intercept prototype methods, but you can use custom methods to cover the native prototype methods.
You can override the Array with an interceptor prototype. After that, whenever the Array is operated by the method on the Array prototype, the method provided in the interceptor is actually executed, such as the push method. Then, use the prototype method of the native Array to operate the Array in the interceptor.
2 interceptor
The interceptor is actually an array Objects with the same prototype contain exactly the same attributes, but some methods in this Object that can change the contents of the array itself are handled by us.
There are seven methods in the Array prototype that can change the contents of the Array itself, namely push, pop, shift, unshift, splice, sort, reverse;
const arrayProto = Array.prototype; // Use arrayMethods to overwrite Array.prototype export const arrayMethods = Object.create(arrayProto); [("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach( (method) => { // Cache original method const original = arrayProto[method]; /** * Use the Object.defineProperty method on arrayMethods to change the array itself * Method of content encapsulation */ Object.defineProperty(arrayMethods, method, { value: function mutator(...args) { /** * When using the push method, it actually uses arrayMethods.push, and arrayMethods.push is the function mutator, * Actually, the mutator function is executed. * Execute original in mutator to do what it should do, such as push function. */ return original.apply(this, args); }, enumerable: false, writable: true, configurable: true, }); } );
3 use interceptors to cover the Array prototype
After having an interceptor, if you want it to take effect, you need to use it to overwrite the Array prototype. However, it cannot be directly covered, which will pollute the global Array. We just want the interceptor to cover only the prototype of the responsive Array.
export class Observer { constructor(value) { this.value = value; if (Array.isArray(value)) { /** * Its function is to assign the interceptor (arraymethods with interception function after processing) to value__ proto__, * Adopted__ proto__ It can skillfully realize the function of covering the value prototype */ value.__proto__ = arrayMethods; // newly added } else { this.walk(value); } } }
4 mount the interceptor method on the attribute of the array
If it cannot be used__ proto__, Directly set these methods on arrayMethods to the detected array;
import { arrayMethods } from "./array"; // __ proto__ Available or not const hasProto = "__proto__" in {}; const arrayKeys = Object.getOwnPropertyNames(arrayMethods); export class Observer { constructor(value) { this.value = value; if (Array.isArray(value)) { // modify const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); } else { this.walk(value); } // .... } } function protoAugment(target, src, keys) { // Overlay prototype target.__proto__ = src; } function copyAugment(target, src, keys) { // Add the prototype method that has processed the interception operation directly to the attribute of value for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i]; def(target, key, src[key]); } }
5 how to collect dependencies
Object dependencies are collected using Dep in the getter in defineReactive, and each key will have a corresponding Dep list to store dependencies.
Array dependencies, like objects, are also collected in defineReactive:
function defineReactive(data, key, val) { if (typeof val === "object") new Observer(val); let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend(); // Here we collect the dependencies of Array return val; }, set: function () { if (val === newVal) { return; } dep.notify(); val = newVal; }, }); }
Therefore, Array collects dependencies in getter s and triggers dependencies in interceptors.
6 Where does the dependency list exist
vue.js stores Array dependencies in Observer:
export class Observer { constructor(value) { this.value = value; this.dep = new Dep(); // Add dep if (Array.isArray(value)) { // .... } // ... } }
Why should the dep (dependency) of the array be saved on the Observer instance?
As mentioned earlier, arrays collect dependencies in getters and trigger dependencies in interceptors, so the location of saving dependencies is critical, and it must be accessible in both getters and interceptors.
The reason why we save the dependency on the Observer instance is that the Observer instance can be accessed in the getter and the Observer instance can also be accessed in the Array interceptor.
7 collection dependency
After saving the Dep instance on the attribute of Observer, we can access and collect dependencies in getter as follows:
function defineReactive(data, key, val) { let childOb = observe(val); // modify let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend(); // newly added if (childOb) { childOb.dep.depend(); } return val; }, set: function () { if (val === newVal) { return; } dep.notify(); val = newVal; }, }); } /** * Try to create an Observer instance for value, * If the creation is successful, the newly created Observer instance is returned directly, * If value already has an Observer instance, it will be returned directly */ export function observe(value, asRootData) { // observe is called in the defineReactive function, which passes val as a parameter and gets a return value, that is, the Observer instance if (!isObject(value)) { return; } let ob; // If value is already responsive data, there is no need to create an Observer instance again, // Just return the created Observer instance directly, avoiding the problem of repeatedly detecting value changes if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) { ob = value.__ob__; } else { ob = new Observer(value); } return ob; }
The Observer instance (childOb) of the array is obtained through observe. Finally, the dependency is collected through childOb's dep execution of the depend method.
8 get the Observer instance in the interceptor
Because the Array interceptor is an encapsulation of the prototype, you can access this (the Array currently being operated on) in the interceptor.
function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true, }); } export class Observer { constructor(value) { this.value = value; this.dep = new Dep(); // Add a non enumerable attribute on value__ ob__, The value of this attribute is the instance of the current Observer def(value, "__ob__", this); //newly added if (Array.isArray(value)) { // .... } // .... } }
In this way, we can use the__ ob__ Get the attribute to the Observer instance, and then you can get it__ ob__ dep on.
Of course__ ob__ The function of is not only to access the Observer instance in the interceptor, but also to identify whether the current value has been converted into responsive data by the Observer.
In other words, there will be one on all the data that has been detected__ ob__ Property to indicate that they are responsive.
When value is marked__ ob__ After that, you can pass value__ ob__ To access the Observer instance. If it is an Array interceptor, because the interceptor is a prototype method, you can directly use this__ ob__ To access the Observer instance. The specific implementation is as follows:
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach( (method) => { const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args) { const ob = this.__ob__; // newly added return original.apply(this, args); }, enumerable: false, writable: true, configurable: true, }); } );
9 send notification to array dependencies
Previously, we introduced how to access the Observer instance in the interceptor, so here you only need to get the dep attribute in the Observer instance to send the notification directly:
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach( (method) => { const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args) { const ob = this.__ob__; ob.dep.notify(); // Send message to dependency return original.apply(this, args); }, enumerable: false, writable: true, configurable: true, }); } );
10 detect the changes of elements in the array
Some elements are stored in the Array, and their changes also need to be detected. In other words, all group data of responsive data should be detected, whether it is data in Object or data in Array (each item in the Array needs to be converted into responsive).
All Observer classes handle not only Object types, but also Array types.
export class Observer { constructor(value) { this.value = value; this.dep = new Dep(); def(value, "__ob__", this); // newly added if (Array.isArray(value)) { this.observeArray(value); } else { this.walk(value); } // .... } // Detect each item in the Array and execute the observe function to detect changes observeArray(items) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]); } } // ... }
11 detect changes in new elements
If you add array elements such as push, unshift, and splice, you also need to convert the contents into responsive ones. The interceptor will judge the type of array methods, and then use obverse to detect them.
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach( (method) => { const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args) { const ob = this.__ob__; // newly added let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); break; } if (inserted) ob.observeArray(inserted); //newly added ob.dep.notify(); return original.apply(this, args); }, enumerable: false, writable: true, configurable: true, }); } );
12 questions about Array
this.list[0] = 2 or this.list Length = 0 cannot be monitored, and re render or watch will not be triggered.
13 summary
Array tracking changes are tracked by creating interceptors to cover the array prototype.
In order not to pollute the global array Prototype is only used for arrays that need to detect changes in Observer__ proto__ To cover the prototype method.
Array and Object collect dependencies in the same way. They are both collected in getter s. However, due to the different use of dependency locations, the array sends messages to the dependency in the interceptor, so the dependency cannot be saved in defineReactive like Object, but on the Observer instance.
In the Observer, every data detected changes is marked with__ ob__, And save this (Observer instance) in__ ob__ Up. There are two main functions, one is to mark whether the data has been detected changes, and the other is to facilitate access through the data__ ob__, So as to get the dependencies saved on the Observer instance.
In addition to detecting the changes of the array itself, the changes of the elements in the array should also be detected. Judge in the Observer that if the currently detected data is an array, call the observeArray method to convert each item in the array into a responsive one and detect the change.
In addition to detecting existing data, when users use push and other methods to add new data, the new data should also be detected for changes. In the interceptor method, judge whether it is push, unshift or splice methods, extract the new data from the parameters, and then use observeArray to detect the changes of the new data.
Note: This article is from the notes after reading vue.js (people's post and Telecommunications Press)