Finally, rendering Watcher. After reading this article, you should be able to implement a responsive system that can be represented on the page.
Source code address: gitee
Series of articles:
1. Fundamentals
2. Array processing
4. The final chapter
Vue project summary series of articles:
- The infrastructure
- Login and permission control
Continuously updated…
What is rendering Watcher
There are many types of Watcher in vUE, and the watcher we implemented earlier is similar to vue.$watch, which executes a callback function when the dependency changes. Rendering Watcher does not require a callback function. Rendering Watcher receives a rendering function instead of a dependency expression, and automatically executes the rendering function when the dependency changes
new Watcher(app, renderFn)
Copy the code
So how do we re-execute the rendering function when the dependency changes? We need to make some changes to Watcher’s constructor first
constructor(data, expOrFn, cb) {
this.data = data
/ / modify
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.cb = cb
this.value = this.get()
}
// parsePath's transformation returns a function
function parsePath(path) {
const segments = path.split('. ')
return function (obj) {
for (let key of segments) {
if(! obj)return
obj = obj[key]
}
return obj
}
}
Copy the code
In this case, this.getter is a value function, and get changes
get() {
pushTarget(this)
const data = this.data
const value = this.getter.call(data, data) / / modify
popTarget()
return value
}
Copy the code
To re-execute the render function depending on the change, an update needs to be made during the distribution update phase, so the update method also needs to be modified:
update() {
// re-execute the get method
const value = this.get()
// Render watcher's value is undefined because the render function returns no value
// Therefore value and this.value are both undefined and will not enter if
// If the dependency is an object, an update is triggered
if(value ! = =this.value || isObject(value)) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
function isObject(target) {
return typeof target === 'object'&& target ! = =null
}
Copy the code
You may wonder why you can’t just use this.getter.call(this.data) to re-execute the render function, which involves re-collecting dependencies as discussed below. But before we do that, we need to address a problem: duplicate collection of dependencies
Repeated dependency
Look at an example like this
<div>
{{ name }} -- {{ name }}
</div>
Copy the code
If we render the template, then render Watcher will rely on name twice. Because name is read twice and getter is fired twice, dep. target is the current watcher. In the Depend method,
depend() {
if (Dep.target) {
dep.addSub(Dep.target)
}
}
Copy the code
Dependencies are collected twice, and two rerenders are triggered when the name changes. So VUE takes the following approach
Start by adding an ID for each DEP
let uid = 0
constructor() {
this.subs = []
this.id = uid++ / / add
}
Copy the code
Watcher added four attributes: deps, depIds, newDeps, and newDepIds
this.deps = [] // store the deP from the last evaluation
this.depIds = new Set(a)// Store the id of the deP that was stored when it was evaluated last time
this.newDeps = [] // store your own deP when storing this evaluation
this.newDepIds = new Set(a)// store the id of the deP when storing this evaluation
Copy the code
After each value, deP and newDep are swapped and newDep is emptied, as described below
The idea is that when a Watcher needs to be collected, it’s up to the Watcher to decide if it needs to be collected by the DEP. In the example above, we assume that when name is evaluated, Watcher is collected by DEP1, and when name is evaluated a second time, Watcher finds that it has already been collected by DEP1 and does not collect again
// dep.depend
depend() {
if (Dep.target) {
Dep.target.addDep(this) // Let Watcher decide if he is collected by deP}}// watcher.addDep
addDep(dep) {
const id = dep.id
// If you have not been collected by deP during the evaluation process, enter if
if (!this.newDepIds.has(id)) {
// Record in watcher to collect your own DP
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)}}}Copy the code
Now to explain the last if, consider the case of rerendering: Watcher depends on name, name has changed, so the get method on watcher is going to reevaluate name, and when it goes into addDep, newDepIds is empty, so it’s going to go into if, it’s going to go to the last if, because the first time it evaluates, Dep has already collected the Watcher, so it should not be added again. This if is what it does.
Vue Tech Insider summed it up well:
NewDeps and newDepIds are used to avoid repeated dependencies during re-evaluation, such as {{name}} — {{name}}
Deps and depIds are used to avoid repeated dependencies during re-rendering of values
The get method clears newDeps and newDepIds
cleanUpDeps() {
// Swap depIds and newDepIds
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
/ / empty newDepIds
this.newDepIds.clear()
// Swap deps and newDeps
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
/ / empty newDeps
this.newDeps.length = 0
}
Copy the code
Dependency re-collection
Dependency recollection as I understand it consists of two parts: collecting new dependencies and removing invalid dependencies. The Dep. Target method is present only during the execution of the get method. The Dep. Target method is present only during the execution of the get method. Instead of get.call (), we used the get method in the update method to refire the render function. It is also necessary to re-collect the dependencies, as in the case of V-IF, so the current reactive system is an improvement over the previous version of fixed dependencies.
To remove invalid dependencies, you can add the following code to cleanUpDeps
cleanUpDeps() {
/ / add
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}let tmp = this.depIds
// ...
}
Copy the code
At the end of evaluation (that is, the end of dependency collection), if it is found that some DEPs collected themselves in the last evaluation but did not collect themselves in the evaluation this time, it indicates that the data does not need to collect themselves, and it can be deleted from the DEP
// Dep.js
removeSub(sub) {
remove(this.subs, sub)
}
function remove(arr, item) {
if(! arr.length)return
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)}}Copy the code
In this way, our responsive system is more complete
conclusion
The rendering Watcher is not much different from other Watcher systems, except that it automatically executes the rendering function when a dependency changes.
The next article will make a simple template compiler to integrate our responsive system with page rendering, and will implement v-Model bi-directional binding, please pay attention.
If you feel the article is ok, please click a “like”!!