Earlier, we talked about the data detection mechanism (Observer) in VUE, if implemented to listen for changes in object and array data. However, if we only know the data changes, we can’t update the data to the view in time. Therefore, we need to collect the dependencies, and when the data is updated, we will trigger the collected dependencies once again, so that the changes in the data can be updated to the view.
Collection depends on Dep
For objects, dependencies are collected in getters and triggered in setters. So where are dependencies stored? Vue uses a Dep class to manage dependencies. For each key value of the response data object, there is an array to store dependencies. Let’s start with the Dep class, which helps us collect dependencies, remove dependencies, and trigger dependencies.
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs,sub)
}
depend() {
if(Window.target) {
this.addSub(window.target)
}
}
notify() {
const subs = this.subs.slice();
for(let i=0,len = subs.slice.length; i<1; i++) { subs[i].update() } } }function remove(arr,item) {
if(arr.length) {
const index = arr.indexOf(item);
if(index > - 1) {
return arr.splice(index,1)}}}}Copy the code
With the Dep class, let’s modify the defineReactive function:
function defineReactive(obj,key,value) {
// The value of the recursive object is also monitored if the value is an object
observer(value);
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true.configurable: true,
get() {
// For objects we collect dependency watcher here
dep.depend()
return value
},
set(newValue) {
if(value == newValue) {
return;
}
// Object: This is where the collected dependency is triggered
value = newValue;
dep.notify()
// A key may also be an object that needs to be listened on
observer(newValue);
console.log('View Update'); }})}Copy the code
Now that you’ve collected your object’s dependencies, what exactly are dependencies?
Rely on the watcher
In the above code, we collect dep. target, which is watcher. It is an abstract class that can process the data used by the page or the watch written by the user.
export default class Watcher{
constructor(vm,expOrFn,cb) {
// Vue instance object
this.vm = vm;
// Execute the getter to get the value passed in by the user such as data.a.b.c
this.getter = parsePath(expOrFn);
this.cb = cb;
// Triggers getter collection dependencies
this.value = this.get()
}
get() {
window.target = this;
// Get a new value
let value = this.getter.call(this.vm,this.vm);
window.target = null;
return value;
}
update() {
const oldVaule = this.value;
this.value = this.get();
this.cb.call(this.vm,this.value,oldVaule)
}
}
Copy the code
We now set window.target to this in the get method, which is the current instance of Watcher, and then read the passed property to its initial value (old value), which triggers the getter collection dependent on the Watcher into the deP for the corresponding key. Later, when the key such as data.a.b.c changes, the setter is used to execute Watcher’s update method to get the latest state of the data. The vm.$watch(‘ A.B.C ‘,cb) and the data bound by instructions and interpolation syntax in the template are all based on Watcher. And how parsePath works:
const reg = /[^\w.$]/
export function parsePath(path) {
if (reg.test(path)) {
return;
}
const args = path.split('. ');
// obj in watcher value this.vm
return function (obj) {
for (let i = 0, len = args.length; i < len; i++) {
if(! obj) {return
}
obj = obj[args[i]]
}
}
}
Copy the code
Characters are separated by a ‘. ‘and then evaluated from data layer by layer.
The object problem
At this point, data change detection for type Object and dependency collection triggers are clear. But getter/setter tracing, you can’t listen for new properties anddelete
And dependencies are not notified. But there are two apis officially mentioned: VM.Delete to fill in the hole.
An array of
Arrays have many prototype methods, and the methods we use to alter arrays in VUE are provided by internal interceptors. In order to cooperate with the dependency collection of the array, we modify the observe function.
export class Observer {
constructor(value) {
this.value = value;
if (!Array.isArray(value)) {
/ / object
this.walk(value)
}
}
/* Walk listens for each property of the object */
walk(obj) {
const keys = Object.keys(obj);
for(let i=0; i<keys.length; i++) { defineReactive(obj,keys[i],obj[keys[i]]) } } }function defineReactive(data,key,val) {
if(typeof val == 'object') {
new Observer(val)
}
Object.defineProperty(data,key,{
enumerable: true.configurable: true,
get() {
dep.depend();
return val
},
set(newVal){
if(val == newVal) {
return
}
val = newVal;
dep.nofify()
}
})
}
Copy the code
In transforming the function of the Observer, we quickly under the realization method of data interceptor modify array of commonly used methods of push and pop, shift, unshift, splice, sort, the reverse:
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(null);
['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) {
return original.apply(this,args)
}
})
})
Copy the code
After customizing the original method of manipulating arrays, we can send notifications and trigger dependencies in mutator functions. The prototype is then overwritten with an interceptor
export class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
// Overwrite the prototype
value.__proto__ = arrayMethods;
}else{
/ / object
this.walk(value)
}
}
}
Copy the code
Array collection dependency
In fact, array dependencies are also collected in the getter, because arrays are also accessed through data’s key, such as this.list, which triggers the getter for the list property. So arrays collect dependencies in the getter and fire in the interceptor. The array’s dependencies are then stored in an Observer and accessed by the interceptor:
export class Observer {
constructor(value) {
this.value = value;
// This deP collects all object and array dependencies, as well as the set and DELETE APIS
this.dep = new Dep();
if (Array.isArray(value)) {
// Overwrite the prototype
value.__proto__ = arrayMethods;
}else{
/ / object
this.walk(value)
}
}
}
Copy the code
After storing the DEP on the Observer property, we can collect dependencies on the getter
function defineReactive(data,key,val) {
let childOb = observe(val);
if(typeof val == 'object') {
new Observer(val)
}
Object.defineProperty(data,key,{
enumerable: true.configurable: true,
get() {
if(childOb) {
// Collect dependencies again
childOb.dep.depend()
}
dep.depend();
return val
},
set(newVal){
if(val == newVal) {
return
}
val = newVal;
dep.nofify()
}
})
}
/* returns an Observe instance for value. We can access the DEp in the interceptor. If created successfully, return an Observer instance
export function observe(value,asRootData) {
if(typeOf value ! ='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
In this way, we can collect the dependencies of both objects and arrays in the getter into the DEP of the Observer instance, so that we can notify the dependencies in the interceptor via value.ob.dep. Here I also mark whether the current value has been converted to responsive data by the Observer, so add a line of code to the Observer:
class Observer{
constructor(value) {
...
def(value,'__ob__'.this)... }}function def(obj,key,val,enumerable) {
Object.defineProperty(obj,key,{
enumerable:!!!!! enumerable,writable: true.configurable: true,
val
})
}
Copy the code
Next we need to listen on each item of the array and send a notification in the interceptor:
class Observer{
constructor(value) {
this.value = value
def(value,'__ob__'.this)... if(Array.isArray(value)) {
// Listen for each item in the array
this.observeArray(value)
}else{
this.walk(value)
}
}
observeArray(val){
for(let i=0; i<val.length; i++) { observe(val[i]) } } }Copy the code
The interceptor
const arraryProto = Array.prototype;
// The method on the array prototype
let proto = Object.create(arrayProto);
['push'.'unshift'.'splice'.'reverse'.'sort'.'shift'.'pop'].forEach(method= >{
proto[method] = function (. args) {
const ob = this.__ob__
// Just like Object, we also need to add data to arrays. Push unshift and splice can add data
let inserted; // No new data is inserted by default
switch(method) {
case 'push':
case 'unshift':
inserted = args
break;
// The array splice must pass three arguments to add data to the array
case 'splice':
inserted = args.slice(2)
break;
default:
break;
}
console.log('View Update');
// Check the new data
if(inserted) {
ob.observeArray(inserted)
}
// Send dependencies
ob.dep.depend()
// We still call the prototype array method, but we can send the array change notification here
arrayProto[method].call(this. args) } })Copy the code
List [0] = 1,this.list. Length =0; this.list. I can’t trace it.