Recently when I was studying Vue, I was always confused about one of its core concepts — responsiveness. By chance, I found a course Vue Advanced Workshop with Evan You. Vue author Yuxi Yu explains Vue himself. The following is a summary of the course. Your reference and comments are welcome.
What is reactive?
Reactive is a core feature of Vue that listens to data bound in a view and updates the view automatically when the data changes.
Responsiveness is defined as automatic updating of dependent parts of the system whenever state changes.
In the case of the Web, changing state reflects changes in the DOM.
The first step in implementing data-driven views in a responsive manner.
Data-driven view
Here’s an example:
Variable A and variable B, the value of variable B is always 10 times that of a.
If you use imperative programming, it can be easily implemented.
let a = 3;
let b = a * 10;
console.log(b) / / 30
Copy the code
But when we set b to 4, b is still equal to 30
let a = 3;
let b = a * 10;
console.log(b) / / 30
a = 4; // Imperative, b does not keep the relationship synchronized
console.log(b) / / 30
Copy the code
So how do you realize that when A changes, B also changes?
There is a magic function called onChanged() that takes a function and automatically executes the code in it when the value of A changes, we put b’s update in it, and the problem is solved.
onChanged (() = > b = a * 10 ) // B changes as A changes
Copy the code
The following code also has a magic function onStateChange, which will automatically run when the state changes, so we can just write the dom manipulation code in the function, and then we can automatically update the DOM.
/ / DOM elements
<span class="cell b1"></span>
// The magic function automatically restarts when the state value changes
onStateChange(() = > {
document.querySelector('.cell.b1').textContent = state.a * 10
})
Copy the code
Let’s take it a step further and replace the DOM manipulation with a rendering engine, but instead of looking at the implementation of the rendering engine, we simply assume that it will automatically parse the template code with the data association, and the code will look like this.
/ / DOM elements
<span class="cell b1">
{{ state.a * 10 }}
</span>
// The magic function automatically restarts when the state value changes
onStateChange(() = > { view = render(state) })
Copy the code
How to implement responsiveness
Getter and setter
Objects in Vue are converted to reactive, overriding getters and setters for all properties using ES5’s defineProperty().
Description of Object.defineProperty on MDN
How to modify object properties using the convert function to modify the getter and setter of the passed object
function convert(obj) {
Object.keys(obj).forEach(key= > {
let internalValue = obj[key] // Closure, which provides the internal value of the store
Object.defineProperty(obj, key, {
get () {
return internalValue
},
set (newValue) {
internalValue = newValue
}
})
})
}
Copy the code
Dependency tracking (subscription publishing mode)
Why rely on collections?
Let’s start with the following code
template:
`<div>
<span>text1:</span> {{text1}}
<div>`,
data: {
text1: 'text1',
text2: 'text2',
}
});
Copy the code
Text3 is not used in the actual template. However, when text3’s data is modified (this.text3 = ‘test’), the setter for Text3 is also triggered to re-render, which is clearly not correct.
So we do dependency collection.
How to do that?
Create a dependency tracing class Dep with two methods: ‘Depend’ and notify’.
‘Depend ‘: Collect this dependency.
‘notify’: Indicates that the dependency has changed. Any expression, function, or calculation previously defined as a dependency will be notified to execute again. Which means we need to find a way for them to connect. We call this computational relationship a dependency. This calculation is also known as the subscriber model. Here is what the Dep class is expected to do. The dep. Depend method is called to collect dependencies. When dep.notify is called, the console prints its updated statement again.
const dep = new Dep()
autorun (() = > {
dep.depend() // Actually add this function to the subscriber list deP and call it from wherever you want
console.log("updated")})// should log: "updated"
dep.notify()
// Should log: "updated" autorun
Copy the code
The autorun function receives an update function or expression, and everything becomes special when you enter the update function. The current code is placed in the response area, and dependencies can be registered through the dep. Depend method.
Code implementation
window.Dep = class Dep {
constructor () {
this.subsctibers = new Set()
}
depend () {
if (activeUpdate) {
// Register this activeUpdate as a subscriber
this.subscribers.add(activeUpdate)
}
},
notify () {
// Notify all subscribers
this.subscribers.forEach(sub= > sub()) // Get the subscription function and execute it}}let activeUpdate / / publisher
function autorun (update) {
function wrappedUpdate () {
activeUpdate = wrappedUpdate // Assigning to wrappedUpdate causes the update function to re-perform dynamic update dependencies when the dependency changes to ensure that the dependency is always up to date
update()
wrappedUpdate = null
}
updateWrapper();
}
autorun(() = > {
dep.depend()
})
Copy the code
Implement mini responsive systems
Rename covert to Observe () by combining the previous two functions convert() autorun(). The observer needs a listener object that listens to their getters and setters. In getters and setters, we can set dependencies. We create an object, we access a property, it collects dependencies, we call dep.depend when we change the property value by assigning, and we call notify to trigger the change.
Expected call effect:
const state = {
count: 0
}
observe(state)
autorun(() = > console.log(state.count))
// should immediately log "count is: 0"
state.count ++
// should log "count is: 1"
Copy the code
The final integration code is as follows:
class Dep {
constructor () {
this.subscribers = new Set()
}
depend () {
if (activeUpdate) {
this.subscribers.add(activeUpdate)
}
}
notify () {
this.subscribers.forEach(sub= > sub())
}
}
function observe (obj) {
Object.keys(obj).forEach(key= > {
let internalValue = obj[key]
const dep = new Dep()
Object.defineProperty(obj, key, {
// Collect dependencies in the getter and re-run when notify is triggered
get () {
dep.depend()
return internalValue
},
// setters are used to call notify
set (newVal) {
if(internalValue ! == newVal) { internalValue = newVal dep.notify() } } }) })return obj
}
let activeUpdate = null
function autorun (update) {
const wrappedUpdate = () = > {
activeUpdate = wrappedUpdate
update()
activeUpdate = null
}
wrappedUpdate()
}
Copy the code
All the above are based on personal understanding, write study notes, welcome to put forward suggestions.