Responsive – Data driven
Data-driven means that developers only care about data changes, and data changes will automatically trigger corresponding view updates, which greatly reduces the points that developers need to pay attention to, and helps to form a unified development mode and improve efficiency. Js provides us with a tool: Object.defineProperty
How to listen for property changes
Our requirement is that when we change the value of an Object property, we can listen for the change to do something. The object.defineProperty mentioned above can do this for us:
var obj = { name: 'jack' } function defineReactive(target, key, val) { Object.defineProperty(target, key, { enumerable: // Can be enumerated without any additional control system: True, get() {console.log(' ${key} property was read ') return val}, // getter set(newVal) {console.log(' ${key} property was reset ') val = newVal}, // setter})} defineReactive(obj, 'name', Obj.name) obj.name // Print: 'obj.name property is read 'obj.name = 'Tom' // print: 'obj.name property is reset 'Copy the code
You can run the above example in your browser’s Console
The code above gives us a rough idea of what we need to do: listen for reads/changes to object properties to do something.
You can implement the following code to listen for all properties of an object:
function def(target, key, val, enumerable = true) { Object.defineProperty(target, key, { value: val, enumerable: enumerable, writable: true, configurable: true }) } class Observer { constructor(value) { this.value = value def(value, '__ob__', This) // Add a '__ob__' attribute to value to avoid repeating Observer, Walk (value)} walk(value) {const keys = object.keys (value) for (let I = 0; i < keys.length; I ++) {defineReactive(value, keys[I], value[keys[I]])}} 'male'} var ob = new Observer(obj)Copy the code
Now we implemented to monitor all attributes of the object, speaking, reading and writing, while there are some defects, if the property is a reference type, the current way also can’t cover, the back to improve, then consider: if a property from the object changes, we also listen to, but who to do the corresponding action to inform? Update the view? Or… ? The problem is that at present we do not know who is the dependent of a certain attribute x of the object. When the attribute X changes, we inform who to update the status, thus introducing the concept of dependency collection
Depend on the collection
Object.defineproperty can not only listen to the write of attributes, but also listen to the read of objects. If an Object (not a JS Object) reads an attribute X of the Object, we have reason to think that the Object depends on attribute X. Once the attribute X is updated in the future, we need to inform the Object to update its status accordingly. The following code can be implemented
window.Deptarget = null function remove(target, list) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, Constructor () {this.id = constructor ++ // this.subs = [] // list of subscribers } addSub(sub) {// Add subscriber this.subs.push(sub)} removeSub(sub) {// Remove subscriber remove(sub, This.subs)} notify() {} function defineReactive(target, key, Var) {const dep = new dep () Object. DefineProperty (target, key, {enumerable: true, signals: true, If (window.deptarget) {depsub (window.deptarget) // add the current subscriber to the dep.subs list} return val }, // getter set(newVal) {console.log(' ${key} property reset ') val = newVal}, // setter})}Copy the code
The above code extends the defineReactive method to introduce class Dep. In fact, each instance DEP corresponds to the object property one by one. You can think of the instance DEP as the proxy of the object property, which stores a lot of content related to the property. Depending on the collection process:
- Each property of the object passes through
defineReactive
The getter/setter is set - The subscriber reads the properties of the object
- The getter for the property is fired, and the DEP for the property adds the subscriber to the dep.subs list
Consider: Why Dep? Because we want to record the subscribers of attributes, we need to have an abstract object to hold these, and this object implements the methods of adding subscribers, removing subscribers, notifying subscribers, etc
Change notification
Now we’ve done:
- Iterate over each property of an object to make it reactive
- Complete dependency collection
Next implement object property changes to notify subscribers of updates:
Constructor () {this.id = UI ++ // this.subs = [] // subscriber list, } addSub(sub) {// Add subscriber this.subs.push(sub)} removeSub(sub) {// Remove subscriber remove(sub, This.subs)} notify() {for (let I = 0; i< this.subs.length; i++) { this.subs[i].update() } } } function defineReactive(target, key, Var) {const dep = new dep () Object. DefineProperty (target, key, {enumerable: true, signals: If (Deptarget) {Deptarget. AddSub (Deptarget) // Add the current subscriber to dep.subs} return val}, // getter set(newVal) { if (newVal === val) return; Val = newVal dep.notify() // notify all subscribers in dep.subs}, // setter})}Copy the code
The above code implements notify, the instance method of Dep, extending the setter. The setter is triggered to execute dep.notify() to notify the subscriber of the update when the object property is reassigned
Note: The subscriber object here is updated with the update method
The subscriber
The previous sections have pretty much cleared out the flow from object responsiveness to dependency collection to change notification, but there is one important role missing: the subscriber. What should a subscriber look like in code?
let uid = 0 class Watcher { constructor(getter) { this.id = uid++ this.getter = getter this.value = this.get() } get() { Deptarget = this; // Assign the current Watcher to the global variable, This return this.getter() window.depTarget = null} update() {// Implement the update method this.run()} run() {// This.value = this.get()}}Copy the code
To tidy up the full code:
let uid = 0 function remove(target, list) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } function def(target, key, val, enumerable = true) { Object.defineProperty(target, key, { value: val, enumerable: enumerable, writable: true, configurable: true }) } function defineReactive(target, key, val) { const dep = new Dep() Object.defineProperty(target, key, {enumerable: true, // Enumerable can be different: If (Deptarget) {Deptarget. AddSub (Deptarget) // Add the current subscriber to dep.subs} return val}, // getter set(newVal) { if (newVal === val) return; Val = newVal dep.notify() // Notify all subscribers in dep.subs}, Constructor () {constructor() {this.id = id++ // this.subs = [] // subscriber list, } addSub(sub) {// Add subscriber this.subs.push(sub)} removeSub(sub) {// Remove subscriber remove(sub, This.subs)} notify() {for (let I = 0; i< this.subs.length; i++) { this.subs[i].update() } } } class Observer { constructor(value) { this.value = value def(value, '__ob__', This) // Add a '__ob__' attribute to value to avoid repeating Observer, Walk (value)} walk(value) {const keys = object.keys (value) for (let I = 0; i < keys.length; i++) { defineReactive(value, keys[i], value[keys[i]]) } } } class Watcher { constructor(getter) { this.id = uid++ this.getter = getter this.value = this.get() } get() {window.deptarget = this // assign the current Watcher to the global variable, Value = this.getter() window.depTarget = null} update() {// Implement the update method required by deP This.run ()} run() {this.value = this.get()}}Copy the code
Now let’s test it out
var obj = {name: 'jack'} function getter() {console.log(obj)} var ob = new Observer(obj) // Subscriber execute new Watcher(getter) // Update obj.name obj.name = 'Tom'Copy the code
Unfortunately, the above code will always print ‘Tom’, meaning that the getter is always executed. Let’s find the problem:
- First, Observer(OBJ) responds to OBJ
- Pass the getter into Watcher to create the subscriber instance
- The getter passed in will be executed during Watcher instantiation
- A reference to obj.name in the getter fires the corresponding property getter
- The getter executes depsub (window.depTarget) to add Watcher to the dep.subs list
- The reassignment of obj.name triggers the corresponding setter for the property, executing dep.notify() to notify the subscriber of update
- The execution of watcher.update, which executes the getter(), triggers dependency collection, which is step 3
The problem was in step 7, which triggered the subscriber update after updating the object obj and again triggered the dependency collection to repeatedly add the current observer to the dep.subs list, while steps 1-7 were performed synchronously, so the dep.subs kept growing and could never be traversed
Once you get to the root of the problem, the next step is to fix it
Address the pitfalls of the previous section of reactive reliance on collecting change notifications
After the analysis of the previous little section, the problem is located in the dependency collection, specifically in the second dependency collection of the subscriber update after the object properties are updated. You should wait until the getter has actually executed before you actually add the subscriber to the DEP.subs. In the previous code, we simply added the subscriber to the dep.subs list. Imagine if the object update triggered the subscriber update and the subscriber no longer depended on this attribute.
Constructor () {this.id = UI ++ // this.subs = [] // subscriber list, } addSub(sub) {// Add subscriber this.subs.push(sub)} removeSub(sub) {// Remove subscriber remove(sub, This.subs)} notify() {for (let I = 0; i< this.subs.length; I ++) {this.subs[I].update()}} // Add depend method for dependency collection Depend () {if (window.deptarget) {window.deptarget.adddep (this) Pass the current DEP to the subscriber, Function defineReactive(target, key, val) {const dep = new dep () object.defineProperty (target, Key, {enumerable: true, // Can be enumerable, different: true, Get () {if (window.deptarget) {dep.depend()} return val})} Class Watcher {constructor(getter) {this.id = UI ++ this.getter = getter // Extend several variables used to store dep this.newDeps = [] // Dep this.depids = new Set() this.value = this.get()} get() Deptarget = this // Assigns the current Watcher to the global variable, Value = this.getter() window.depTarget = null // After dependency collection is complete, null this.cleanupdeps ()} // CleanUpDeps () {cleanUpDeps() {// cleanUpDeps() {newDeps (); Empty newDeps const I = this.deps. Length while(I --) {if (! this.newDepIds.has(this.deps[i].id)) { this.deps[i].removeSub(this) } } let temp = this.depIds this.depIds = this.newDepIds this.newDepIds = temp this.newDepIds.clear() temp = this.deps this.deps = this.newDeps this.newDeps = Temp this.newdeps. length = 0} addDep(dep) {// If the dependency list does not contain dep, add it to the new DEP list. Newdepids.has (dep.id)) {this.newdepids.add (dep.id) this.newdepids.push (dep) // If the old deP list also does not contain dep, Immediately add the current subscriber to the dep.subs list if (! this.depIds.has(dep.id)) { dep.addSub(this) } } } ... }Copy the code
The complete code
let uid = 0 function remove(target, list) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } function def(target, key, val, enumerable = true) { Object.defineProperty(target, key, { value: val, enumerable: enumerable, writable: true, configurable: true }) } function defineReactive(target, key, val) { const dep = new Dep() Object.defineProperty(target, key, {enumerable: true, // Enumerable can be different: If (Deptarget) {dep.depend() // Add the current subscriber to dep.subs} return val}, // getter set(newVal) { if (newVal === val) return; Val = newVal dep.notify() // Notify all subscribers in dep.subs}, Constructor () {constructor() {this.id = id++ // this.subs = [] // subscriber list, } addSub(sub) {// Add subscriber this.subs.push(sub)} removeSub(sub) {// Remove subscriber remove(sub, This.subs)} notify() {for (let I = 0; i< this.subs.length; I ++) {this.subs[I].update()}} Depend () {if (window.deptarget) {window.deptarget.adddep (this) Class Observer {constructor(value) {this.value = value def(value, '__ob__', this) this.walk(value) } walk(value) { const keys = Object.keys(value) for (let i = 0; i < keys.length; i++) { defineReactive(value, keys[i], value[keys[i]]) } } } class Watcher { constructor(getter) { this.id = uid++ this.getter = getter this.newDeps = [] // Dep this.depids = new Set() this.value = this.get()} get() { window.Deptarget = this this.value = this.getter() window.Deptarget = null this.cleanUpDeps() } update() { this.run() } run() {this.get()} cleanUpDeps() {// cleanUpDeps that are no longer dependent, assign newDeps to deps, NewDeps let I = this.deps.length while(I --) {if (! this.newDepIds.has(this.deps[i].id)) { this.deps[i].removeSub(this) } } let temp = this.depIds this.depIds = this.newDepIds this.newDepIds = temp this.newDepIds.clear() temp = this.deps this.deps = this.newDeps this.newDeps = Temp this.newdeps. length = 0} addDep(dep) {// If the dependency list does not contain dep, add it to the new DEP list. This.newdepids. has(dep.id)) {this.newdepids. add(dep.id) this.newdepids. push(dep) // If the old deP list does not contain dep, Immediately add the current subscriber to the dep.subs list if (! this.depIds.has(dep.id)) { dep.addSub(this) } } } }Copy the code
Test it out:
var obj = {name: 'jack'} function getter() {console.log(' my name is ${obj.name} ')} var ob = new Observer(obj) // Subscriber execute new Watcher(getter) // Update obj.name obj.name = 'Tom' // will output 'my name is Tom 'Copy the code
conclusion
Vue implements data-driven processes:
- First, Observer(OBJ) responds to OBJ
- Pass the getter into Watcher to create the subscriber instance
- The getter passed in will be executed during Watcher instantiation
- A reference to obj.name in the getter fires the corresponding property getter
- The getter property performs dep.depend to inform subscribers in the global Deptarget of dependency collection
- The subscriber implements addDep method, which identifies the old generation dependency list and the new generation dependency list with deps and newDeps attributes respectively. First, deP is added to the new generation dependency list. If the old generation dependency list does not contain the current DEP, the dep.addSub() notification is immediately executed to add the subscriber to dep.subs
- When the getter is executed and a round of dependency collection is completed, the watcher.cleanupdeps () method is executed to remove the old generation dependencies that do not exist in the new generation dependency list, and assign the new generation dependencies to the old generation dependency list to empty the new generation dependency list and wait for the next round of dependency collection
- The reassignment of obj.name triggers the corresponding setter for the property, executing dep.notify() to notify the subscriber of update
- The execution of watcher.update, which executes the getter(), triggers dependency collection, which is step 3
legacy
Think about:
- How do I respond to nested objects
- How to solve the problem of the subscriber updating multiple times in a round of changes where multiple attributes of the subscriber’s dependency are changed or one attribute of the dependency is changed multiple times
- What would the dependency collection process look like if you nested another subscriber in the getter function of one subscriber