Beep, beep, beep
In the previous article, we implemented an Observer. In this section, we will talk about the implementation of dep and watcher. As before, at the end of the article, I wrote a simple beta version of js for you to read the text of this section while testing.
Let's talk about dep first
dep is our dependency collector. You will know from the above observer that observer observes all data in an object array. Therefore, dep also manages multiple dependencies corresponding to all data in the object array observed in observer
To be honest, it's a little windy, so let's start directly
establish
class depNext { subs: Map<string, Array<Watcher>>; constructor() { this.subs = new Map(); } addSub(prop, target) { ... } // Add a dependency depend(prop) { ... } // Notify all dependent updates notify(prop) { ... } }
If we want to manage all the dependencies corresponding to all the data in the object, we must first have an appropriate data structure. Map is very appropriate, for example
//After observer makes obj responsive let obj={ a:1 b:2 c:3 } //The map in dep looks like this. Here is pseudo code let map={ a:[wathcer1,watcher2.....] b:[wathcer3,watcher4.....] c:[wathcer5,watcher6.....] }
So how to build such a Map? We write our addSub logic
Let's first get whether prop:[watcher1.....] has been created in the Map Such a data structure, (hereinafter we call this prop:[watcher1.....], If it is a mapping array), it will be created directly, as follows:
addSub(prop, target) { const sub = this.subs.get(prop); if (!sub) { this.subs.set(prop, [target]); return; } sub.push(target); }
When the data is got, dep.depend () will be called. The logic of depend is also very simple. Check whether the global variable tar get is assigned dependency. If so, ask addSub to add dependency according to prop.
depend(prop) { if (window.target) { this.addSub(prop, window.target); } }
Finally, only notify is left. The logic of notify is to notify all dependencies in the mapping array of prop to complete the update.
notify(prop) { const watchers = this.subs.get(prop); if(!watchers)return; for (let i = 0, l = watchers.length; i < l; i++) { watchers[i].update(); } } }
The dep after construction is as long as this
class depNext { subs: Map<string, Array<Watcher>>; constructor() { this.subs = new Map(); } addSub(prop, target) { const sub = this.subs.get(prop); if (!sub) { this.subs.set(prop, [target]); return; } sub.push(target); } // Add a dependency depend(prop) { if (window.target) { this.addSub(prop, window.target); } } // Notify all dependent updates notify(prop) { const watchers = this.subs.get(prop); if(!watchers)return; for (let i = 0, l = watchers.length; i < l; i++) { watchers[i].update(); } } }
And watcher
The watcher is our "dependency". After the data is updated, the observer will notify dep to update the dependency of relevant data, and the dependency will execute the callback function on it to update the view.
Start with the skeleton:
class Watcher { vm:VM cb:Function; getter:any; value:any; constructor (vm,initVal,expOrFn,cb) { this.vm = vm; //vue instance this.cb = cb; //Callback to execute if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)//Never mind what parsePath is else if(isType(expOrFn,'Function'))this.getter=expOrFn this.value = this.get() //Collection dependency this.value=initVal //Set initial value } get () { //... Collection dependency } update () { //... Dependency update } }
Let's start with core dependency collection
We talked about dep earlier, collecting dependencies in the depend method
depend(prop) { if (window.target) { this.addSub(prop, window.target); } }
Contact the above again. When the data is obtained, the observer will notify dep to execute the dependent collection dependency,
Then the get method of our watcher is ready to come out!
get () { window.target = this; let value = this.getter(this.vm.$data) window.target = undefined; return value }
We first assign the current dependency to the global target, and then obtain the data related to the dependency. At this time, observer will execute the subsequent dependency collection process.
Then, you must have doubts. What is getter?
parsePath method
In constructor
if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn) else if(isType(expOrFn,'Function'))this.getter=expOrFn
If the getter itself is a function to obtain data, it will be assigned directly. If it is a string shaped like "obj.a", then use the parsePath method to turn it into a method to obtain obj:{a:'xxxx'} data in the $data option
To pave the way for the later explanation of the text parser, I will now give a more detailed example,
When the complier parses the text node "Xiao Ming's age is {{obj.a}}", it will generate a watcher. At this time, the expOrFn of the parameter is obj a.
function parsePath(path) { const bailRE = /[^\w.$]/; const segments = path.split("."); return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return; if (bailRE.test(segments[i])) { //this.arr[0] this[arr[0]] const match = segments[i].match(/(\w+)\[(.+)\]/); obj = obj[match[1]]; obj = obj[match[2]]; continue; } obj = obj[segments[i]]; } return obj; };
The parsePath function is simple. It will return a function. This function will take the data from the parameter obj in the order of a.b.c in expOrFn. At this time, observer will execute the subsequent dependency collection process!
Finally, let's talk about dependency update
It's easier to trigger a callback~
update () { const oldValue = this.value this.value = this.getter(this.vm.$data) this.cb.call(this.vm, this.value, oldValue) }
Final results
let target=null; function def(obj, key, val, enumerable = false) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true, }); } class ObserverNext { constructor(key, value, parent) { this.$key = key; this.$value = value; this.$parent = parent; this.dep = new Dep(); def(value, "__ob__", this); this.walk(value); this.detect(value, parent); } walk(obj) { for (const [key, val] of Object.entries(obj)) { if (typeof val == "object") { //Judge arrays and objects at the same time new ObserverNext(key, val, obj); } } } detect(val, parent) { const dep = this.dep; const key = this.$key; const proxy = new Proxy(val, { get(obj, property) { if (!obj.hasOwnProperty(property)) { return; } dep.depend(property); return obj[property]; }, set(obj, property, value) { obj[property] = value; dep.notify(property); if (parent.__ob__) parent.__ob__.dep.notify(key); return true; }, }); parent[key] = proxy; } } class Dep { constructor() { this.subs = new Map(); } addSub(prop, target) { const sub = this.subs.get(prop); if (!sub) { this.subs.set(prop, [target]); return; } sub.push(target); } // Add a dependency depend(prop) { if (target) { this.addSub(prop, target); } } // Notify all dependent updates notify(prop) { const watchers = this.subs.get(prop); if (!watchers) return; for (let i = 0, l = watchers.length; i < l; i++) { watchers[i].update(); } } } class Watcher { constructor (vm,initVal,expOrFn,cb) { this.vm = vm; this.cb = cb; this.getter = parsePath(expOrFn) this.value = this.get() //Collection dependency this.value=initVal } get () { target = this; let value = this.getter(this.vm.data) target = undefined; return value } update () { const oldValue = this.value // this.value = this.get() / / do not trigger the getter when updating, otherwise the dependency will be collected this.value = this.getter(this.vm.data) this.cb.call(this.vm, this.value, oldValue) } } function parsePath(path) { const bailRE = /[^\w.$]/; const segments = path.split("."); return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return; if (bailRE.test(segments[i])) { //this.arr[0] this[arr[0]] const match = segments[i].match(/(\w+)\[(.+)\]/); obj = obj[match[1]]; obj = obj[match[2]]; continue; } obj = obj[segments[i]]; } return obj; }; } const vm = { data: { attr1: { a: 1, b: 2, c: 3, }, array: [1, 2, 3], }, }; new ObserverNext('data',vm.data,vm); new Watcher(vm,'{{attr1,a}}','attr1.a',(val,oldVal)=>{ console.log('Dependency update','@Current value:'+val,'@Old value:'+oldVal); }) vm.data.attr1.a=2
Think again. If we face different nodes and change the callback of the incoming watcher, will it be? This is the reason for the parser to be discussed in the next article
file
#Touch hands to teach you to realize a simple vue (1) responsive principle
#Touch hands to teach you to implement a simple vue (2) start writing observer
#Touch hands to teach you to implement a simple vue (3) start writing dep and watcher