Better reading experience
This article will give you a comprehensive understanding of vUE’s rendering Watcher, computed and User Watcher. In fact, computed and user Watcher are based on Watcher. Let everyone fully understand the realization principle and the core idea. So this article will implement the following function points:
- Implement data responsiveness
- Based on the rendering
wather
Achieve the first data rendering to the interface - Data depends on collection and updating
- Implement data update trigger rendering
watcher
Execute to update the UI interface - Based on the
watcher
implementationcomputed
- Based on the
watcher
implementationuser watcher
Without further ado, let’s look at the final example below.
And then we’ll go straight to work.
The preparatory work
First we prepare an index. HTML file and a vue.js file. Let’s look at the code for index. HTML
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fully understand VUE's rendering watcher, computed, and User atcher</title>
</head>
<body>
<div id="root"></div>
<script src="./vue.js"></script>
<script>
const root = document.querySelector('#root')
var vue = new Vue({
data() {
return {
name: 'Joe'.age: 10}},render() {
root.innerHTML = `The ${this.name}----The ${this.age}`}})</script>
</body>
</html>
Copy the code
Index.html has a div node with the id root, which is the parent node, and then in the script tag, we introduce vue.js, which provides the vue constructor, and then instantiate vue, which takes an object with the data and render functions, respectively. Then let’s look at the vue.js code:
function Vue (options) {
this._init(options) / / initialization
this.$mount() // Execute the render function
}
Vue.prototype._init = function (options) {
const vm = this
vm.$options = options // Mount options to this
if (options.data) {
initState(vm) // Data responsive
}
if (options.computed) {
initComputed(vm) // Initialize the calculated properties
}
if (options.watch) {
initWatch(vm) // Initialize watch}}Copy the code
This._init() and this.$mount(). The this._init method is to initialize the configuration we passed in, This includes data initialization initState(VM), computed attributes initialization initComputed(VM), and customized watch initialization initWatch(VM). The this.$mount method renders the render function to the page. We’ll write about these methods later to give you an idea of the structure of the code. Now let’s formally fill in the methods we wrote above.
Implement data responsiveness
To implement these Watcher methods, the first step is to implement data responsiveness, which is to implement initState(VM) above. I’m sure you’re all familiar with reactive code, so I’ll just post it up.
function initState(vm) {
let data = vm.$options.data; // Get the configured data attribute value
// Determine whether data is a function or another type
data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {};
const keys = Object.keys(data);
let i = keys.length;
while(i--) {
// All data read from this is intercepted into this._data
// For example, this.name equals this._data.name
proxy(vm, '_data', keys[i]);
}
observe(data); // Data observation
}
// Data observation function
function observe(data) {
if(! (data ! = =null && typeof data === 'object')) {
return;
}
return new Observer(data)
}
// All data read from this is intercepted into this._data
// For example, this.name equals this._data.name
function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key] // this.name is the same as this._data.name
},
set(newValue) {
return vm[source][key] = newValue
}
})
}
class Observer{
constructor(value) {
this.walk(value) // Set get set for each attribute
}
walk(data) {
let keys = Object.keys(data);
for (let i = 0, len = keys.length; i < len; i++) {
let key = keys[i]
let value = data[key]
defineReactive(data, key, value) // Set get set for the object}}}function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
if (newValue == value) return
observe(newValue) // Set the response to the new value
value = newValue
}
})
observe(value); // Recursively set get set for data
}
Copy the code
All the important points are in the comments. The main core is to set get and set for the data recursed to data, and then set the data proxy so that this.name equals this._data.name. After setting up the data observation, we can see the data as shown below.
console.log(vue.name) / / zhang SAN
console.log(vue.age) / / 10
Copy the code
Ps: Array data observation we go to improve ha, here focuses on the implementation of Watcher.
For the first time to render
With the data observation done, we can render the render function into our interface. Prototype.$mount() : $this.$mount();
// Mount method
Vue.prototype.$mount = function () {
const vm = this
new Watcher(vm, vm.$options.render, () = > {}, true)}Copy the code
Wather is a function that executes the render function on Watcher and inserts data into the root node. Let’s look at the simplest implementation of Watcher
let wid = 0
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm // Mount the VM to the current this
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn // Mount exprOrFn to the current this, where exprOrFn equals vm.$options.render
}
this.cb = cb // Attach cb to this
this.options = options // Mount options to the current this
this.id = wid++
this.value = this.get() $options.render()
}
get() {
const vm = this.vm
let value = this.getter.call(vm, vm) // Point this to the VM
return value
}
}
Copy the code
We can now get data from this. Name in render and insert it into root.innerhtml. The phased work is done. As shown below, the completed first render ✌️
Data depends on collection and updating
First data collection, we need to have a place to collect, which is our Dep class, so let’s see how we can implement this Dep.
// Rely on collection
let dId = 0
class Dep{
constructor() {
this.id = dId++ // Each instantiation generates an ID
this.subs = [] // Let the DEP instance collect the watcher
}
depend() {
// dep. target is the current watcher
if (Dep.target) {
Dep.target.addDep(this) // Select deP from watcher and deP from watcher}}notify() {
// Trigger the update
this.subs.forEach(watcher= > watcher.update())
}
addSub(watcher) {
this.subs.push(watcher)
}
}
let stack = []
// Push the current watcher to stack and record the current watcer
function pushTarget(watcher) {
Dep.target = watcher
stack.push(watcher)
}
// Clear the current watcher after running
function popTarget() {
stack.pop()
Dep.target = stack[stack.length - 1]}Copy the code
The classes collected by Dep are implemented, but how we collect them is to instantiate Dep in our data observation get and let Dep collect the current Watcher. Here we go step by step:
- 1. On top
this.$mount()
In the code, we runnew Watcher(vm, vm.$options.render, () => {}, true)
At this time we can atWatcher
Perform insidethis.get()
And then executepushTarget(this)
“, you can execute this sentenceDep.target = watcher
, put the currentwatcher
mountDep.target
On. So let’s see how we can do that.
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
this.cb = cb
this.options = options
this.id = wid++
this.id = wId++
this.deps = []
this.depsId = new Set(a)// DeP has already collected the same watcher
this.value = this.get()
}
get() {
const vm = this.vm
pushTarget(this)
let value = this.getter.call(vm, vm) // Execute the function
popTarget()
return value
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this); }}update(){
this.get()
}
}
Copy the code
- 2, know
Dep.target
What comes after, and then the above code runsthis.get()
, which is equivalent to runningvm.$options.render
In therender
Inner loop executionthis.name
, which triggersObject. DefineProperty · the get
Method, in which we can do some dependency collection (dep.depend), the following code
function defineReactive(data, key, value) {
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
if (Dep.target) { // If the value is watcher
dep.depend() // Let watcher save deP, and let DEP save watcher, two-way save
}
return value
},
set(newValue) {
if (newValue == value) return
observe(newValue) // Set the response to the new value
value = newValue
dep.notify() // Notify render Watcher to update}})// Recursively set get set for data
observe(value);
}
Copy the code
- 3, called
dep.depend()
It’s actually calledDep.target.addDep(this)
At this time,Dep.target
Equal to the currentwatcher
And then it will be executed
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep) // The current Watcher collects deP
dep.addSub(this); // The current DEP collects the current watcer}}Copy the code
It’s a little bit convoluted in both directions, so you can understand it a little bit better. Let’s take a look at what the collected DES looks like.
- 4, data update, call
This. name = 'li si'
When back to triggerObject.defineProperty.set
Method, directly called insidedep.notify()
And then loop through all of themwatcer.update
Method to update allwatcher
For example, this is a re-executionvm.$options.render
Methods.
With the dependency to collect data updates, we also added a timed method to the index. HTML to modify the data attribute:
// index.html
<button onClick="changeData()"> change name and age</button>// -----
/ /... Omit code
function changeData() {
vue.name = 'bill'
vue.age = 20
}
Copy the code
The operation effect is shown below
At this point we’re rendering Watcher and it’s all done.
To realize the computed
First, we configure a computed script tag in index.html that looks like this:
const root = document.querySelector('#root')
var vue = new Vue({
data() {
return {
name: 'Joe'.age: 10}},computed: {
info() {
return this.name + this.age
}
},
render() {
root.innerHTML = `The ${this.name}----The ${this.age}----The ${this.info}`}})function changeData() {
vue.name = 'bill'
vue.age = 20
}
Copy the code
In the code above, note that computed is used in Render.
In vue.js, I wrote the following line of code earlier.
if (options.computed) {
// Initialize the calculated properties
initComputed(vm)
}
Copy the code
Let’s now implement this initComputed, with the following code
// Initialize computed
function initComputed(vm) {
const computed = vm.$options.computed // Get computed configuration
const watchers = vm._computedWatchers = Object.create(null) // Mount the _computedWatchers attribute to the current VM
// cycle computed for each attribute
for (const key in computed) {
const userDef = computed[key]
// Determine whether it is a function or an object
const getter = typeof userDef === 'function' ? userDef : userDef.get
// Create a computed watcher for each computed object. Note {lazy: true}
// Then mount to the vm._computedWatchers object
watchers[key] = new Watcher(vm, getter, () = > {}, { lazy: true })
if(! (keyin vm)) {
defineComputed(vm, key, userDef)
}
}
}
Copy the code
You know that computed is cached, so when you create a watcher, you pass a configuration {lazy: true}, and you can differentiate it from a computed watcher, and then you receive the object in watcer
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
if (options) {
this.lazy = !! options.lazy// Designed for computed
} else {
this.lazy = false
}
this.dirty = this.lazy
this.cb = cb
this.options = options
this.id = wId++
this.deps = []
this.depsId = new Set(a)this.value = this.lazy ? undefined : this.get()
}
// Omit a lot of code
}
Copy the code
Value = this.lazy? Use undefined: this.get() as you can see, computed watcher creation does not point to this.get. Only executed in the render function.
Now we can’t get a value in the render function through this.info because we haven’t mounted it to the VM yet, and defineComputed(VM, key, userDef) does this to mount computed to the VM. So let’s implement that.
// Set the comoputed set
function defineComputed(vm, key, userDef) {
let getter = null
// Determine whether it is a function or an object
if (typeof userDef === 'function') {
getter = createComputedGetter(key)
} else {
getter = userDef.get
}
Object.defineProperty(vm, key, {
enumerable: true.configurable: true.get: getter,
set: function() {} // Set = set ()})}// Create computed functions
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {// Add subscription Watchers to computed attributes
watcher.evaluate()
}
// Add the render watcher to the attribute's subscription, which is critical
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
Copy the code
Use watcher to evaluate() and watcher.depend(). Use watcher to evaluate() and watcher.depend().
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
if (options) {
this.lazy = !! options.lazy// Designed for computed
} else {
this.lazy = false
}
this.dirty = this.lazy
this.cb = cb
this.options = options
this.id = wId++
this.deps = []
this.depsId = new Set(a)// DeP has already collected the same watcher
this.value = this.lazy ? undefined : this.get()
}
get() {
const vm = this.vm
pushTarget(this)
// Execute the function
let value = this.getter.call(vm, vm)
popTarget()
return value
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this); }}update(){
if (this.lazy) {
this.dirty = true
} else {
this.get()
}
}
// Get and this.dirty = false
evaluate() {
this.value = this.get()
this.dirty = false
}
// All attributes collect the current watcer
depend() {
let i = this.deps.length
while(i--) {
this.deps[i].depend()
}
}
}
Copy the code
So once we’ve done that, let’s talk about the process,
- 1, first in
render
The function reads itthis.info
, this will triggercreateComputedGetter(key)
In thecomputedGetter(key)
; - 2. Then judge
watcher.dirty
, the implementation ofwatcher.evaluate()
; - 3, into the
watcher.evaluate()
I really want to executethis.get
Method, which is executedpushTarget(this)
The currentcomputed watcher
Push it into the stack and push itDep.target is set to current
The computed watcher `; - 4. Then run
this.getter.call(vm, vm)
Equivalent to runningcomputed
theinfo: function() { return this.name + this.age }
, this method; - 5,
info
The function will read itthis.name
, which triggers data responsivenessObject.defineProperty.get
Student: Herename
Will do the dependency collection, thewatcer
Collect the correspondingdep
The above; And returnsName = 'zhang SAN'
The value of theage
Collect the same; - 6. Execute dependencies after collection is complete
popTarget()
, put the currentcomputed watcher
Clears from the stack, returns the calculated value (‘ three +10’), andthis.dirty = false
; - 7,
watcher.evaluate()
When it’s done, it’s judgedDep.target
Isn’t ittrue
If there is, there isRender the watcher
, they performwatcher.depend()
And then letwatcher
The inside of thedeps
collectRender the watcher
This is the advantage of two-way saving. - 8, at this time
name
All collectedcomputed watcher
和Render the watcher
. Then set thename
Will go to the update implementationwatcher.update()
- 9. If so
computed watcher
It’s not going to be executed again, it’s just going to bethis.dirty
Set totrue
, if the data changeswatcher.evaluate()
forinfo
Update, no change in wordsthis.dirty
isfalse
, will not be implementedinfo
Methods. This is the computed caching mechanism.
After that, let’s look at the implementation:
Here conputed object set configuration is not implemented, you can see the source code
Watch implementation
First configure watch in the script tag and configure the following code:
const root = document.querySelector('#root')
var vue = new Vue({
data() {
return {
name: 'Joe'.age: 10}},computed: {
info() {
return this.name + this.age
}
},
watch: {
name(oldValue, newValue) {
console.log(oldValue, newValue)
}
},
render() {
root.innerHTML = `The ${this.name}----The ${this.age}----The ${this.info}`}})function changeData() {
vue.name = 'bill'
vue.age = 20
}
Copy the code
Now that you know the computed implementation, it’s easy to customize the watch implementation, so let’s implement initWatch directly
function initWatch(vm) {
let watch = vm.$options.watch
for (let key in watch) {
const handler = watch[key]
new Watcher(vm, key, handler, { user: true}}})Copy the code
Then modify Watcher to look directly at Wacher’s complete code.
let wId = 0
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
} else {
this.getter = parsePath(exprOrFn) // user watcher
}
if (options) {
this.lazy = !! options.lazy// Designed for computed
this.user = !! options.user// For User Wather
} else {
this.user = this.lazy = false
}
this.dirty = this.lazy
this.cb = cb
this.options = options
this.id = wId++
this.deps = []
this.depsId = new Set(a)// DeP has already collected the same watcher
this.value = this.lazy ? undefined : this.get()
}
get() {
const vm = this.vm
pushTarget(this)
// Execute the function
let value = this.getter.call(vm, vm)
popTarget()
return value
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this); }}update(){
if (this.lazy) {
this.dirty = true
} else {
this.run()
}
}
// Get and this.dirty = false
evaluate() {
this.value = this.get()
this.dirty = false
}
// All attributes collect the current watcer
depend() {
let i = this.deps.length
while(i--) {
this.deps[i].depend()
}
}
run () {
const value = this.get()
const oldValue = this.value
this.value = value
/ cb/execution
if (this.user) {
try{
this.cb.call(this.vm, value, oldValue)
} catch(error) {
console.error(error)
}
} else {
this.cb && this.cb.call(this.vm, oldValue, value)
}
}
}
function parsePath (path) {
const segments = path.split('. ')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
}
Copy the code
And finally look at the effect
Of course, many configurations are not implemented, such as options.immediate or options.deep. It’s too long. Oneself also lazy ~ ~ ~ end scatter flower
Detailed code: github.com/naihe138/wr…