One, foreword
An overview of the
Data models are just plain old JavaScript objects. And when you modify them, the view is updated.
With Vue, we only need to change the data (state) and the view can be updated accordingly. This is a responsive system. To implement a responsive system of our own, we first need to understand what to do:
- Data hijacking: There are certain things we can do when data changes
- Dependency collection: we need to know the contents of those view layers (
DOM
) depends on what data (state
) - Distribute updates: How do I notify those dependent on data when it changes
DOM
Next, we will implement a toy responsive system of our own step by step
Front knowledge
1. Vue monitors all levels of data in data
- How do I monitor data in objects?
Monitoring is implemented through setters and the data to be monitored is passed in at new Vue
- Object, Vue does not respond by default
- To respond to attributes added later, use the following API
Vue.set(target, propertyName/index, value)
vm.$set(target, propertyName/index, value)
Copy the code
- How do I detect data in an array?
By wrapping the array update element, we essentially do two things:
- Call the method corresponding to the native array to update the array
- Reparse the template to update the page
To modify an element in a Vue array, do the following:
API: Push () pop() shift() unshift() splice() sort() reverse() vue.set () or vm.$setCopy the code
Note: Vue.set() and vm.$set() cannot add attributes to a VM or its root data object
2. Object.defineProperty()
Juejin. Cn/post / 699508…
- with
getter
andsetter
Method can listen on data, and access and Settings are captured by listeners - Getters fire when data is read, setters fire when data is modified
Data hijacking
Almost all articles and tutorials on Vue responsive systems start with the following: Vue uses Object.defineProperty for data hijacking. So, let’s also start with data hijacking. You might be a little confused about the concept of hijacking. That’s ok.
The usage of object.defineProperty is not explained here, you can check it on MDN if you don’t understand. Next, we define an A property for obj
const obj = {}
let val = 1
Object.defineProperty(obj, a, {
get() { This method is referred to as the getter for the rest of the article
console.log('get property a')
return val
},
set(newVal) { This method is collectively referred to as setter in the following sections
if (val === newVal) return
console.log(`set property a -> ${newVal}`)
val = newVal
}
})
Copy the code
Thus, when we access obj. A, print get Property a and return 1, and when obj. A = 2 sets a new value, print Set Property a -> 2. This is equivalent to customizing the behavior of obj. A values and assignments, using custom getters and setters to override the original behavior, which is what data hijacking is all about.
Many of you might be wondering, why do we have one hereval
Instead of directly in get and set functionsreturn obj.a
andobj.a = val
?
If we return obj. A directly from get, obJ. A will also call get once, which will result in an infinite loop. The same is true for set, so we use a third-party variable val to prevent an infinite loop.
But if we need to delegate more properties, it is not possible to define a third party variable for each property, which can be solved with closures
usedefineReactive
function
So we define a function to encapsulate defineProperty to implement data hijacking. Using defineReactive, we don’t need to set temporary variables, but use closures
// value uses the default value of the parameter
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, "a".1)
Copy the code
Reactive handling of objects and arrays
- An Observer is an object
Object or array
Where responsive processing takes place- DefineReactive: Intercept every one on the object
key
theGet and set functions
The place where- Observe: Entry to responsive processing
The flow goes something like this: observe-> Observer -> defineReactive -> observe-> Observer -> defineReactive Recursively
const { arrayMethods } = require('./array')
const obj = {
a: 1.b: {
c: 2
}
}
observe(obj)
function observe(value) {
// If an object or array is passed in, it is processed responsively
if (Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value)) {
return new Observer(value)
}
}
// Observer objects, it is easier to build them using es6 classes -- to convert a normal object into an object whose properties at each level are responsive (detectable)
class Observer {
constructor(value) {
// Set an __ob__ object to the value object or array passed in
// The __ob__ object is very useful. If the __ob__ object is attached to the value, the value has been processed in response
Object.defineProperty(value, '__ob__', {
value: this.// The value is this, which is an Observer instance of new
enumerable: false.// Cannot be enumerated
writable: true.// We can rewrite __ob__ with the assignment operator
configurable: true // Can be overwritten and deleted
})
// Determine whether value is a function or an object
if(Array.isArray(value)) {
// Modify the array prototype if it is an array
value.__proto__ = arrayMethods
// If the array contains an array, you need to recurse
this.observeArray(value)
} else {
// If it is an object, the walk function is executed to process the object responsively
this.walk(value)
}
}
walk(data) {
// Get all the keys of the data object
let keys = Object.keys(data)
// The value of each key is processed responsively
for(let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = data[key]
// Pass in the data object, key, and value
defineReactive(data, key, value)
}
}
observeArray(items) {
// Iterate over the array passed in, if the array still contains the array, then recursive processing
for(let i = 0; i < items.length; i++) {
observe(items[i])
}
}
}
function defineReactive(data, key, value) {
// Recursive important steps
// Because there may be objects or arrays in the object, recursion is required
observe(value)
/ / core
// Intercept the get and set attributes of each key in the object
// This implements the underlying principle that both read and write can be captured and responsive
Object.defineProperty(data, key, {
get() {
console.log('Get value')
return value
},
set(newVal) {
if (newVal === value) return
console.log('Set value')
value = newVal
}
})
}
Copy the code
Because interception of array subscripts is too wasteful, it adds to the array judgment of the data arguments passed to the Observer constructor
// src/obserber/index.js
class Observer {
/ / observations
constructor(value) {
Object.defineProperty(value, "__ob__", {
// The value refers to an instance of an Observer
value: this.// Not enumerable
enumerable: false.writable: true.configurable: true}); }}Copy the code
Before we rewrite the array prototype, we need to understand this code. What this code means is that we add an unenumerable __ob__ attribute to each responsive data and point to an Observer instance. Therefore, we can use this attribute to prevent the data that has already been observed by the responsive data from being observed again and again Reactive data can use __ob__ to get methods related to Observer instances, which is critical for arrays
// src/obserber/array.js
// Keep the array prototype
const arrayProto = Array.prototype;
// arrayMethods is then inherited from the array prototype
// Here is the idea of Slice-oriented programming (AOP) -- dynamically extending functionality without breaking encapsulation
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
"push"."pop"."shift"."unshift"."splice"."reverse"."sort",]; methodsToPatch.forEach((method) = > {
arrayMethods[method] = function (. args) {
// The execution result of the prototype method is preserved here
const result = arrayProto[method].apply(this, args);
// This sentence is the key
// this represents the data itself. For example, if the data is {a:[1,2,3]}, then we use a.paush (4). This is a ob, which is a.__ob__
const ob = this.__ob__;
// Add a new operation to the array
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
[// If there are new elements inserted is an array] Call the observeArray of the Observer instance to observe each item in the array
if (inserted) ob.observeArray(inserted);
// We can also detect that the array has changed and trigger the view update operation -- source code will be revealed later
return result;
};
});
Copy the code
Why objects and arrays are handled separately:
object
Attributes are usually few, hijacking every one of themSet and get
, does not consume much performanceAn array of
There could be thousands of elements, if each one were hijackedSet and get
It definitely consumes too much performance- so
object
throughdefineProperty
A normal hijackingSet and get
An array of
byModify part of the method on the array prototype
To achieveModify array trigger response
For this part, you may be a little dizzy, so let’s comb it out:
Observe (obj) ├─ New Observer(obj) And This.walk () DefineReactive () ├── defineReactive(OBj, a) ├─ ObJ. ├─ defineReactive(obj, a) Remainder of code ├─ defineReactive(obj, B) ├─ Observe (obj. B) ├─ New Observer(obJ. B).ObJ. B DefineReactive () ├─ Perform defineReactive(obJ. B, c) ├─ Perform ObJ. B.c Go straight back to ├─ Perform Remainder of ├─ perform Define Active (obj, b) code endCopy the code
As can be seen, the above three functions are called as follows:
The three functions call each other to form recursion, which is different from normal recursion. Some of you might be thinking, well, if I just call a render function in the setter to rerender the page, wouldn’t that be enough to update the page when the data changes? It can, but the price of doing this is that any data change will cause the page to be rerendered, which is too high a price. What we want to do is update only the DOM structures associated with the data as it changes, which brings us to dependencies
2. Collect dependencies and send updates
Rely on
Before getting into dependency collection, let’s look at what a dependency is. Take a life example: Taobao shopping. Now there is a video card (air) in a taobao shop in the pre-sale stage, if we want to buy, we can click the pre-sale reminder, when the video card is sold, Taobao push a message for us, we can see the message, we can start to buy.
To abstract this example, it’s a publish-subscribe model: when a buyer clicks on a pre-sale alert, he or she registers his or her information on Taobao (subscription), which taobao stores in a data structure (such as an array). When the graphics card is officially open for purchase, Taobao will notify all buyers: the graphics card is sold (released), and buyers will carry out some actions according to this information (such as buying back mining).
In a Vue responsive system, the graphics card corresponds to the data, so what does the buyer in this example correspond to? It’s an abstract class: Watcher. You don’t have to think about what the name means, just what it does: Each Watcher instance subscribs to one or more pieces of data, also known as Wacther dependencies (goods are the buyer’s dependencies); When a dependency changes, the Watcher instance receives a message that the data has changed and then executes a callback function to perform some function, such as updating the page (the buyer does some action).
So the Watcher class can be implemented as follows
class Watcher { constructor(data, expression, cb) { // data: Data object, such as obj // expression: expression, such as b.c, can obtain the data that Watcher depends on according to data and expression // cb: This.data = data this.expression = expression this.cb = cb // Subscribe to data when initialingWatcher instance this.value = this.get()} Get () {const value = parsePath(this.data, this.expression) return value} Cb update() {this.value = parsePath(this.data, this.expression) cb()}} function parsePath(obj, expression) { const segments = expression.split('.') for (let key of segments) { if (! Obj) return obj = obj[key]} return obj} Copy codeCopy the code
If you have any questions about when this Watcher class is instantiated, that’s fine, we’ll get to that in a second, okay
In fact, there is one point we haven’t mentioned in the previous example: as mentioned in the video card example, Taobao will store buyer information in an array, so our responsive system should also have an array to store buyer information, namely watcher.
To summarize the features we need to implement:
- There’s an array to store it
watcher
watcher
Instances need to subscribe to (dependency) data, that is, get dependencies or collect dependencieswatcher
Triggered when a dependency changeswatcher
To send out updates.
Each data should maintain its own array to hold its own dependent watcher. We can define an array DEP in defineReactive so that each property has its own DEP through closures
Function defineReactive(data, key, value = data[key]) {const dep = [] // Add observe(value) object.defineProperty (data, key, { get: function reactiveGetter() { return value }, set: function reactiveSetter(newValue) { if (newValue === value) return value = newValue observe(newValue) dep.notify() } }) } Duplicate codeCopy the code
At this point, we have implemented the first feature, and then we implement the process of collecting dependencies.
Depend on the collection
Now let’s focus on the first rendering of the page (ignoring rendering functions, virtual DOM, etc.) : The rendering engine parses the template, and if the engine encounters an interpolation, what happens if we instantiate a Watcher? The get method gets the data that we depend on, and we rewrite the data access behavior, and we define a getter for each data, so the getter will execute, If we add the current watcher to the DEP array in the getter, we can complete the dependency collection!!
Note: The get method of new Watcher() is not finished when the getter is executed.
New Watcher () constructor, call the instance of the get method, the instance of the get method will read the value of the data, triggering the data of the getter, complete after getter, instances of the get method is performed, and the return value, the constructor is done, The instantiation is complete.
Watcher is the deP of data collection. Watcher is the deP of data collection. Watcher is the deP of data collection. Those of you who have this question can revisit the previous taobao example (where Taobao records user information) or take a closer look at the publish-subscribe model.
From the above analysis, we only need to make a few changes to the getter:
Get: function reactiveGetter() {dep.push(watcher) // add return valueCopy the code
So again, where does watcher come from? We’re instantiating Watcher in a template-compiled function, and we can’t get that instance in the getter. The solution is simply to place the watcher instance globally, such as window.target. Therefore, Watcher’s get method is modified as follows
Get () {window.target = this // Add const value = parsePath(this.data, this.expression) return value} Copy codeCopy the code
So, change dep.push(watcher) in the get method to dep.push(window.target).
Note that window.target = new Watcher() cannot be written like this. Window.target is undefined because the instantiation of watcher is not complete when the getter is executed
Dependency collection process: When rendering a page, it encounters interpolation expressions, and v-bind or other places that need data will instantiate a Watcher. The instantiated Watcher will evaluate the dependent data and trigger the getter, and the getter function of the data will add its own dependent Watcher to complete the dependency collection. We can think of watcher as collecting dependencies, and the code is implemented by storing the dependent Watcher in the data
Careful readers may notice that with this approach, a new Watcher is created for each interpolation encountered, so that each node has a watcher. This is actually what Vu1.x does, with fine-grained updates on a per-node basis. In ve2. X, there is a Watcher for each component. When instantiating the Watcher, it is no longer an expression, but a rendering function, which is converted from the template of the component. It’s a medium grained approach. There are a lot of other things involved in implementing a responsive system for VUe2. X, such as componentization, virtual DOM, etc. This series of articles will only focus on the principles of data responsiveness, so it cannot be implemented for VUe2.
Distributed update
After implementing dependency collection, the last thing we want to do is distribute updates, which trigger watcher’s callback when a dependency changes. We know from the dependency collection section that what data is fetched, that is, what data getter is triggered, tells watcher what data it depends on. How do we notify Watcher when data changes? As many of you have already guessed, send updates in the setter.
set: function reactiveSetter(newValue) { if (newValue === value) return value = newValue observe(newValue) dep.forEach(d => D.update ()) // Add the update method see Watcher class} copy codeCopy the code
3. Optimize code
1. The Dep
We can abstract the DEP array as a class:
class Dep { constructor() { this.subs = [] } depend() { this.addSub(Dep.target) } notify() { const subs = [...this.subs] Subs.foreach ((s) => s.update())} addSub(sub) {this.subs.push(sub)}} Copy codeCopy the code
The defineReactive function only needs to be modified accordingly
function defineReactive(data, key, Value = data[key]) {const dep = new dep () // Modify observe(value) object.defineProperty (data, key, {get: Function reactitter () {dev.depend ()}, set: Function reactiveSetter(newValue) {if (newValue === value) return value = newValue observe(newValue) dep.notify() // Modify }})} copy the codeCopy the code
2. window.target
In watcher’s get method
Get () {window.target = this // Sets window.target const value = parsePath(this.data, this.expression) return value} copy codeCopy the code
You may have noticed that we didn’t reset window.target. Some students might think this is fine, but consider the following scenario: we have an object obj: {a: 1, b: 2} and we instantiate watcher1, which depends on obj. Then we visit OBJ.B. What happens? Accessing obj. B triggers obJ. B’s getter, which calls DEP.depend (), and then OBj. B’s DEP collects window.target, which is watcher1, which causes Watcher1 to rely on obj. B, which is not the case. To solve this problem, we make the following modifications:
// Get () {window.target = this const value = parsePath(this.data, This. Expression) window.target = null // Dep depend() {if (dep.target) {// add this.addSub(dep.target)}}Copy the code
As you can see from the above analysis, window.target is the watcher instance in the current execution context. Because of the single-threaded nature of JS, only one Watcher code is executing at a time, so window.target is the watcher currently being instantiated
3. The update method
The update method we implemented earlier looks like this:
Update () {this.value = parsePath(this.data, this.expression) this.cb()Copy the code
Recalling the vm.$watch method, we can access this in a defined callback that receives both the old and new values of the listening data, so make the following changes
update() { const oldValue = this.value this.value = parsePath(this.data, this.expression) this.cb.call(this.data, This. value, oldValue)} copies the codeCopy the code
4. Learn about Vue source code
In the Vue source code — line 56, we see a variable called targetStack, which looks like it’s related to our window.target, and yes, it is. Imagine a scenario where we have two nested parent components. When the parent component is rendered, a new watcher of the parent component is created. When a child component is found during rendering, the child component is rendered, and a new watcher of the child component is created. In our implementation, when the parent component watcher is created, window.target will point to the parent component watcher, then create a child component watcher, window.target will overwrite the quilt component Watcher, the child component is rendered, and when the parent component Watcher is returned, Window.target becomes null, which is a problem, so we use a stack structure to hold watcher.
const targetStack = [] function pushTarget(_target) { targetStack.push(window.target) window.target = _target } function PopTarget () {window.target = targetstack.pop ()} copies the codeCopy the code
The Watcher get method is modified as follows
Get () {pushTarget(this) // Modify const value = parsePath(this.data, this.expression) popTarget() // modify return value} copy codeCopy the code
In addition, the use of dep. target instead of window.target to hold the current watcher in Vue doesn’t matter much, as long as there is a globally unique variable to hold the current watcher
5. Summarize the code
The code is summarized as follows:
Function observe(data) {if (typeof data! == 'object') return new Observer(data) } class Observer { constructor(value) { this.value = value this.walk() } walk() { Object.keys(this.value).foreach ((key) => defineReactive(this.value, key))}} key, value = data[key]) { const dep = new Dep() observe(value) Object.defineProperty(data, key, { get: function reactiveGetter() { dep.depend() return value }, set: function reactiveSetter(newValue) { if (newValue === value) return value = newValue observe(newValue) dep.notify() } }) Constructor () {this.subs = []} Depend () {if (dep.target) {this.addSub(dep.target)}} notify() { const subs = [...this.subs] subs.forEach((s) => s.update()) } addSub(sub) { this.subs.push(sub) } } Dep.target = null const TargetStack = [] function pushTarget(_target) { TargetStack.push(Dep.target) Dep.target = _target } function popTarget() { Dep.target = TargetStack.pop() } // watcher class Watcher { constructor(data, expression, cb) { this.data = data this.expression = expression this.cb = cb this.value = this.get() } get() { pushTarget(this) const value = parsePath(this.data, this.expression) popTarget() return value } update() { const oldValue = this.value this.value = parsePath(this.data, This. Expression) this.cb.call(this.data, this.value, oldValue)}} function parsePath(obj, expression) { const segments = expression.split('.') for (let key of segments) { if (! obj) return obj = obj[key] } return obj } // for test let obj = { a: 1, b: { m: { n: 4 } } } observe(obj) let w1 = new Watcher(obj, 'a', (val, OldVal) => {console.log(' obj.a changed from ${oldVal}(oldVal) to ${val}(newVal) ')}) copies the codeCopy the code
4. Precautions
1. The closure
This is possible thanks to closures: closures are formed in defineReactive so that each attribute of each object can hold its own value and dependent object DEP.
2. Will dependencies be collected as soon as the getter is triggered
The answer is no. In Dep’s Depend method, we see that dependencies are added only if dep. target is true. For example, watcher’s update method is triggered when an update is sent. This method also triggers parsePath, but dep. target is null and no dependency is added. PushTarget (this) is called to dep. target only when watcher is instantiated. Otherwise, dep. target is null and get is called only when watcher is instantiated. A Watcher dependency is defined at instantiation time, and any subsequent reading of the value does not increase the dependency.
3. Rely on nested object properties
Let’s combine the above code to consider the following question:
let w2 = new Watcher(obj, 'b.m.n', (val, OldVal) => {console.log(' obj.b.m.n from ${oldVal}(oldVal) to ${val}(newVal) ')}) copies the codeCopy the code
We know that w2 will depend on obj.b.m.n, but will w2 depend on obj.b, obj.b.m? Or, obj.b, and obj.b.m, do they have W2 in the DEP stored in their closure? The answer is yes. Without looking at the code, imagine that if we set obj.b = null, then it’s obvious that w2’s callback should be triggered, which means that W2 relies on the object properties in the middle.
When new Watcher() is called, the get method of Watcher will be called. Set dep. target to w2, and the get method will call parsePath.
Function parsePath(obj, expression) {const segments = expression.split('.') // Segments :['b', 'm', 'n'] // loop value for (let key of segments) {if (! Obj) return obj = obj[key]} return obj} Copy codeCopy the code
The above code flow is as follows:
- A local variable
obj
For the objectobj
Read,obj.b
The value of triggergetter
To triggerdep.depend()
(thedep
isobj.b
In the closure ofdep
),Dep.target
If yes, add a dependency - A local variable
obj
forobj.b
Read,obj.b.m
The value of triggergetter
To triggerdep.depend()
(thedep
isobj.b.m
In the closure ofdep
),Dep.target
If yes, add a dependency - A local variable
obj
For the objectobj.b.m
Read,obj.b.m.n
The value of triggergetter
To triggerdep.depend()
(thedep
isobj.b.m.n
In the closure ofdep
),Dep.target
If yes, add a dependency
As you can see from the code above, it is logical for W2 to rely on each item associated with the target attribute.
5. To summarize
To sum up:
- call
observe(obj)
That will beobj
Set to a responsive object,Observe, observe, defineReactive
All three call each other, recursivelyobj
Set as a responsive object - Instantiate when rendering the page
watcher
The process completes by reading the values of the dependent dataGet the dependency in the getter
- Triggered when a dependency changes
setter
To issue updates, perform callbacks, and completeDistribute updates in the setter
Of a hole
Strictly speaking, the responsive system we have now completed cannot be used to render pages, because the actual watcher for rendering pages does not require callback functions. We call it rendering Watcher. In addition, the render Watcher can accept a render function instead of an expression as an argument, and automatically re-render when the dependency changes, which creates the problem of duplicate dependencies. Another important thing we haven’t covered yet is the handling of arrays.
It doesn’t matter if you can’t understand the problems mentioned above. Later articles in this series will solve these problems step by step. I hope you can continue to pay attention to them.