preface
This column is caused by a question that you can ignore if you already know the answer to.
<! DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <div @click="change">{{a}}</div> </div> </body> <script> var app = new Vue({ el: '#app', data: { a: { b: 1, c: { d: 1, } } }, methods:{ change(){ this.a.c.d = 2; } } }) </script> </html>Copy the code
Page display effect
Click on the display area and the page display will change to
Why does this.a.c.d = 2 refresh the page as shown above? Perhaps you know it from reading this column. During the Vue mount, data this.a collects render subscribers. When this.a.c.d = 2 is executed and data this.a changes, the render subscriber is notified, the render subscriber responds, and DOM updates.
The problem is that only data this.a really collects render subscribers, and when this. A.C.D = 2 is executed, the render subscribers are notified. When you dig into these issues, some processes don’t work. This column will explain them one by one.
Review the process of collecting render subscribers
When the data is read, the getter function for the data is fired, in which the following code is executed to collect subscribers:
if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); }}}Copy the code
Either executing dep.depend(), childob.dep.depend () or dependArray(value) triggers the data publisher to collect subscribers. However, there is a prerequisite for executing this code, dep.target, which is a global object that stores the current subscribers to be collected and also ensures that only one subscriber is collected at collection time. See this column to find out where the dep. target is assigned.
A Watcher class is instantiated during the mount of Vue, and the Watcher instance method get is executed in the Watcher constructor.
Watcher.prototype.get = function get() {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
}
Copy the code
Where dep. target is defined in pushTarget(this), look at the pushTarget function.
Dep.target = null;
var targetStack = [];
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
Copy the code
That is, during the instantiation of the Watcher class, dep. target is assigned a value that is the Watcher instantiation object, and since it is instantiated in the Vue mount, we call it the render subscriber.
If (dep.target) is satisfied, and dep.depend() will now trigger data to collect the render subscriber as soon as the data is read.
So where is this. A read during rendering, or when this. Getter. Call (vm, vm) is called in the get instance method, so what is this. Look for it in the Watcher constructor.
var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
this.value = this.lazy ? undefined : this.get();
}
Copy the code
Note that this. Getter is expOrFn, given that Watcher’s constructor is a expOrFn function. So let’s see how Watcher is instantiated during Vue mount.
var updateComponent; updateComponent = function() { vm._update(vm._render(), hydrating); }; new Watcher(vm, updateComponent, noop, { before: function before() { if (vm._isMounted && ! vm._isDestroyed) { callHook(vm, 'beforeUpdate'); } } }, true /* isRenderWatcher */ ); .Copy the code
_update(vm._render(), hydrating), Vnode = render. Call (vm._renderProxy, vm.$createElement) Where render is the rendering function compiled from the template.
(function anonymous() {
with(this){
return _c('div',
{attrs:{"id":"app"}},
[
_c('div',
{on:{"click":change}},
[
_v(_s(a))
]
)
]
)
}
})
Copy the code
The with statement specifies a default object for one or a group of statements. For example, with(this){a + b} equals this.a + this.b.
_v(_s(a)) is executed first when render is executed.
_v(_s(a)), rather than this._v(this._s(this.a)), reads this.a, fires the getter for this.a, and collects the render subscriber in it.
So here’s the real problem.this.a
Collected render subscribers during executionthis.a.c.d = 2
After, will it really notify render subscribers?
Ii. How to notify subscribers of reviews
When the data is changed, setter functions for the data are triggered, in which dep.notify() is executed to notify the subscriber. For details on the logic, see this column.
At this point, you can conclude that subscribers can only be notified if setter functions for the data are triggered.
So use the following code to simulate if a property in variable data becomes responsive and then execute data.a.c.d = 2 will trigger the setter function for a property.
let data = {
a: {
b: 1,
c: {
d: 1,
}
}
};
let val = data.a;
Object.defineProperty(data, 'a', {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get')
return val
},
set: function reactiveSetter(newVal) {
console.log('set')
}
});
data.a.c.d = 2;
Copy the code
As it turns out, console.log(‘set’) is not executed and the console does not print the set. The render subscriber is not notified when this.a.c.d = 2 is executed. The render subscriber is not notified when this.a.c.d = 2 is executed.
Data this.a.c.d collected the render subscriber, and the render subscriber will be notified when this.a.c.d = 2 is executed. We can simulate it.
let data = {
a: {
b: 1,
c: {
d: 1,
}
}
};
let val = data.a.c.d;
Object.defineProperty(data.a.c, 'd', {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get')
return val
},
set: function reactiveSetter(newVal) {
console.log('set')
}
});
data.a.c.d = 2;
Copy the code
As a result, console.log(‘set’) is executed and the console prints out set. If this.a.c.d collects render subscribers, the render subscribers will be notified only after this.a.c.d = 2.
The new problem is that in Vue, the data is collected only when the data is read and the dep.target is present. So where is the data read this.a.c.D.
In the change method, this.a.c.d is read, but when the change method is called, dep. target is undefined and does not exist, so the data will not be collected.
Where is that? _v(this._s(this.a)); this._s(this.a); this._s(this.a); More specifically, it reads from the this._s method.
This._s is defined by installRenderHelpers(Vue. Prototype) in renderMixin(Vue).
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
Copy the code
_s is the toString function. Let’s look at the toString function.
function toString(val) {
return val == null ? '' :
Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ?
JSON.stringify(val, null, 2) :
String(val)
}
Copy the code
Since this.a is an object, json.stringify (val, null, 2) is executed in toString. Getting closer to the truth.
Deep collection of render subscribers in json. stringify
To find out, look at the implementation of json.stringify. Here’s a rough simulation of json.stringify turning an object into a JSON string, as shown below:
function stringify(data) { let result = ''; let part = ''; if (data === null) { return String(data); } switch (typeof data) { case 'number': return String(data); } switch (Object.prototype.toString.call(data)) { case '[object Object]': result += '{'; for (let key in data) { part = stringify(data[key]); if (part ! == undefined) { result += '"' + key + '":' + part + ','; } } if (result ! == '{') { result = result.slice(0, -1); } result += '}'; return result; }}Copy the code
As you can see, the object is recursively traversed as it is converted to a JSON string, which reads all of the object’s child properties, triggering each of the object’s child properties to collect render subscribers, which perfectly answers the opening question.