This is the third day of my participation in Gwen Challenge.
preface
Previously we looked at detecting changes in objects, so why is Array a separate topic? Let’s use the following example to illustrate:
this.list.push(1)
Copy the code
Object can detect state through getter/setter, but array push method cannot trigger getter/setter. In this article, we’ll learn how Array implements change detection.
How to track change
Object notifies dependencies on Update via setters. If we can notify the array push, we can achieve the same effect.
We can point the Array prototype to a new object that overrides the Array method on array. prototype.
The interceptor
The name for the object that has both array methods and notification capabilities is an interceptor (which is also an embodiment of the proxy pattern).
How do you implement an interceptor?
Array.prototype has seven methods that can change the contents of an Array: push, POP, Shift, unshift, spice, and reverse.
const arrayProto = array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function (method){
// Cache the method on the prototype
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
enumerable: false.writable: true.configurable: true.value: function mutator(. args){
// ...
// Invoke the method on the prototype
return original.apply(this.args)
}
})
})
Copy the code
Now that we have interceptors, how do we make them work? The violent way is to modify array. prototype directly, but that would pollute the global Array, so we’ll try a different approach — pointing the Array instance’s prototype at the interceptor.
- using
__proto__
- Using the ES6
Object.setPrototypeOf()
For compatibility reasons, we use the first method, which copies the interceptor’s methods directly onto the array instance if __proto__ is not supported.
function protoAugment(target, src, keys) {
target.__proto__ = src;
}
function copyAugment(target, src, keys) {
for (let i = 0, len = keys.length; i < len; i++) {
constkey = keys[i]; def(target, key, src[key]); }}function def(target, key, val, enumerable? : boolean) {
Object.defineProperty(target, key, {
enumerable:!!!!! enumerable,writable: true.configurable: true.value: val,
});
}
Copy the code
Let’s now remake the Observer:
const hasProto = '__proto__' in {};
export default class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value); // Convert all properties of the object to getters/setters}}walk(obj) {
Object.keys(obj).forEach((key) = >{ defineReactive(obj, key, obj[key]); }); }}Copy the code
How to collect dependencies
We implemented the interceptor above, but the interceptor does not yet have notification dependencies. To implement notification dependencies, you must first implement the dependency collection function. So how does an array collect dependencies?
Let’s start by reviewing how Object dependencies are collected. An Object’s dependency collection is collected using a Dep instance in the getter, and each key has a Dep to collect dependencies.
In fact, arrays also collect dependencies in the getter.
{
list: [1.2.3.4]}Copy the code
When a list is read, the getter for the list property fires.
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();
// Collect array dependencies
return val;
},
set: function (newVal) {
if (val === newVal) return; dep.notify(); val = newVal; }}); }Copy the code
Array collects dependencies in the getter and triggers dependencies in the interceptor.
Where is the dependency collection
Vue.js stores Array dependencies in an Observer instance:
export default class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep(); // Array dependencies are collected here
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value); // Convert all properties of the object to getters/setters}}}Copy the code
Collect rely on
After adding a DEP attribute to the Observer instance, we can collect dependencies.
function defineReactive(data, key, val) {
let childOb = observe(val);
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true.configurable: true.get: function () {
dep.depend();
/ / new
if (childOb) {
childOb.dep.depend();
}
return val;
},
set: function (newVal) {
if (val === newVal) return; dep.notify(); val = newVal; }}); }function observe(value, asRootData) {
if (typeofvalue ! = ="object") {
return;
}
let ob;
if (value.hasOwnProperty("__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
Copy the code
Notification dependency update
Now that we’ve done the dependency collection for arrays, we just need to notify dependencies. To notify dependencies, we need to have access to the DEP on the Observer instance, so how do we access the DEP in the interceptor?
Careful students may have noticed that __ob__ appears in the observe function above, and that’s the core.
export default class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, '__ob__'.this); / / new
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value); // Convert all properties of the object to getters/setters}}}Copy the code
We define a new attribute __ob__ on value to point to the Observer instance, and then we can call the Observer instance on value in the interceptor, thus accessing its DEP attribute.
const arrayProto = array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function (method){
constt original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
enumerable: false.writable: true.configurable: true.value: function mutator(. args){
let ob = value.__ob__; / / new
ob.dep.notify() / / new
return original.apply(this.args)
}
})
})
Copy the code
Detects changes to elements in arrays
Above, we implemented dependency collection and dependency notification for arrays. So what if there are objects in the array?
If the properties of an object in the array change, it makes sense to send notifications as well. Also, if you add an object to the array, you need to turn that object into a responsive object. So, we need to go through the array, trying to convert the elements of the array into responsive.
export default class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value); / / new
} else {
this.walk(value); }}observerArray(list) {
for (let i = 0, len = list.length; i < l; i++) {
observe(list[i]); // Try to convert each item into a responsive one}}}Copy the code
New element in detection array
We can try to convert the new element to responsiveness by passing it to the __ob__ observeArray method in the interceptor.
const arrayProto = array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function (method){
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
enumerable: false.writable: true.configurable: true.value: function mutator(. args){
const result = original.apply(this.args)
let ob = value.__ob__;
// add the new element to try to make it responsive
let inserted;
switch(metthod){
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.serveArray(inserted);
ob.dep.notify()
returnresult; }})})Copy the code
The problem of Array
The change is not detectable by modifying elements directly by subscript and emptying the array with list.length = 0.
conclusion
How is Array change detection implemented
- Unlike Object, we point the prototype of the array instance to the interceptor we defined, collect the dependencies in the getter, and notify the dependencies in the interceptor.
- To collect dependencies in the getter, we add an instance attribute, dep, to the Observer
- To enable notification of dependencies, we define
value.__ob__
Property pointing to an observer instance, used in interceptorsvalue.__ob__.dep.notify()
To notify dependencies. - Considering that the elements in the array may be objects, in order to detect the changes of the object elements in the array, we try to convert the elements in the array into responses and add them to the observer
observeArray
Method, which can be used to convert elements of an array into responses at initialization, or called from the array’s interceptorvalue.__ob__.observeArray(inserted)
And try to make the new element responsive.