takeaway
I remember when I first learned the Vue source code, jumping between defineReactive, Observer, Dep, Watcher, and other internal design sources, only to find that I couldn’t get around anymore. Vue development for a long time and a lot of fix and feature increased internal source is more and more huge, too many boundary condition and optimizing the design covers originally simplify the code design, is becoming more and more difficult for beginners to read source code, but the interview, the reactive principle of Vue is almost a Vue company senior front-end technology stack will ask one of the points.
This article through their own implementation of a responsive system, as far as possible to restore and Vue internal source code the same structure, but eliminate and render, optimization and so on related code, to the lowest cost of learning Vue responsive principle.
This article is based on Vue 2.4 version of the principle analysis, the subsequent version may be changed, but the principle is not very different.
preview
Source address: github.com/sl1673495/v…
Source address (js) github.com/sl1673495/v…
Sl1673495. github. IO /vue-reactiv…
reactive
The most common type of Vue is reactive data, defined in Vue
new Vue({
data() {
return {
msg: 'Hello World'}}})Copy the code
When data changes, the view is also updated. In this article, I separate the processing of data into an API: Reactive, and implement this API together.
Effects to be achieved:
const data = reactive({
msg: 'Hello World',})new Watcher((a)= > {
document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code
When data. MSG changes, we need the innerHTML of the app node to be updated synchronously. A new concept called Watcher is added here, which is also a design within the Vue source code. This Watcher is essential for implementing a responsive system.
Before implementing these two apis, we need to clarify the relationship between them. The REACTIVE API defines reactive data, which is known to record who is reading it when a property (such as data.msg in this example) is read. The function that reads it must depend on it. In this case, the following function, because it reads data.msg and displays it on the page, can be said to rely on data.msg.
// Render function
document.getElementById('app').innerHTML = `msg is ${data.msg}`
Copy the code
This explains why we need to use new Watcher to pass in the render function. We can already see that Watcher is the key that helps us record the render function dependency.
MSG has been defined as responsive data. The get function triggered when reading data. MSG has been hijacked by us. In this get function, we record that data. MSG is dependent on this rendering function. Then return the value of data.msg.
This way, the next time data.msg changes, some of the logic Watcher has done inside will tell the rendering function to redo it. That’s how the reactive formula works.
Let’s start implementing the code
import Dep from './dep'
import { isObject } from '.. /utils'
// Define the object as reactive
export default function reactive(data) {
if (isObject(data)) {
Object.keys(data).forEach(key= > {
defineReactive(data, key)
})
}
return data
}
function defineReactive(data, key) {
let val = data[key]
// Collect dependencies
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend()
return val
},
set(newVal) {
val = newVal
dep.notify()
}
})
if (isObject(val)) {
reactive(val)
}
}
Copy the code
The code is simple. It just iterates through data’s keys, hijacking each key with get and set in defineReactive. Dep is a new concept. It is mainly used to do the above mentioned dep.depend() to collect the rendering function currently running and dep.notify() to trigger the rendering function to re-execute.
Dep can be regarded as a dependency collection basket. Whenever a rendering function is run to read a certain key of data, it will throw the rendering function into the key’s own basket. When the key value changes, it will find all the rendering functions in the key’s basket and execute again.
Dep
export default class Dep {
constructor() {
this.deps = new Set()
}
depend() {
if (Dep.target) {
this.deps.add(Dep.target)
}
}
notify() {
this.deps.forEach(watcher= > watcher.update())
}
}
// Running watcher
Dep.target = null
Copy the code
This class uses Set to store data, adds dep. target to the deps Set (Depend), iterates through the deps Set (notify), and triggers updates for each watcher.
Dep. Target is a global variable that hangs on a Dep class. Js is single-threaded, so rendering functions like:
document.getElementById('app').innerHTML = `msg is ${data.msg}`
Copy the code
Before running, set the global dep. target to the watcher that stores the render function:
new Watcher((a)= > {
document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code
In this way, data. MSG can find which render function watcher is currently running through dep. target on the run, so that it can collect its corresponding dependencies.
Here’s the key: Dep.target must be an instance of Watcher.
Because rendering functions can be nested, such as in Vue each component has its own watcher to store rendering functions, in the case of nested components:
// Parent component <template> <div> <Son component /> </div> </template>Copy the code
The watcher run path is: Start -> ParentWatcher -> SonWatcher -> ParentWatcher -> End.
Vue uses the stack data structure to record the trajectory of Watcher.
/ / watcher stack
const targetStack = []
// Push the last watcher onto the stack and update dep. target to the _target variable passed in.
export function pushTarget(_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
// Retrieve the previous watcher as dep. target and pop the previous watcher on the stack.
export function popTarget() {
Dep.target = targetStack.pop()
}
Copy the code
With these auxiliary tools, you can take a look at Watcher in action
import Dep, { pushTarget, popTarget } from './dep'
export default class Watcher {
constructor(getter) {
this.getter = getter
this.get()
}
get() {
pushTarget(this)
this.value = this.getter()
popTarget()
return this.value
}
update() {
this.get()
}
}
Copy the code
Recall the use of Watcher in the initial example.
const data = reactive({
msg: 'Hello World',})new Watcher((a)= > {
document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code
The getter that’s passed in is
() = > {document.getElementById('app').innerHTML = `msg is ${data.msg}`
}
Copy the code
In the constructor, the getter is recorded and get is executed
get() {
pushTarget(this)
this.value = this.getter()
popTarget()
return this.value
}
Copy the code
In this function,this is the watcher instance. Get begins by setting the watcher that stores the render function to the current dep.target, and then this.getter(), which is the render function
The hijacked get in defineReactive is triggered when data. MSG is read on the way to the render function:
Object.defineProperty(data, key, {
get() {
dep.depend()
return val
}
})
Copy the code
Dep. depend
depend() {
if (Dep.target) {
this.deps.add(Dep.target)
}
}
Copy the code
The dep. target collected is what pushTarget(this) collected at the start of the get function
new Watcher((a)= > {
document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code
This is an instance of Watcher.
Suppose we execute an assignment like this:
data.msg = 'ssh'
Copy the code
The set function is hijacked:
Object.defineProperty(data, key, {
set(newVal) {
val = newVal
dep.notify()
}
})
Copy the code
The deP variable is printed in the console, and its internal DEPS property stores an instance of Watcher.
After running dep.notify, the Watcher update method is triggered and the render function is executed again, at which point the view is refreshed.
computed
Once you have implemented the reactive base API, you need to implement the computed API, which works like this:
const data = reactive({
number: 1
})
const numberPlusOne = computed((a)= > data.number + 1)
// Render function watcher
new Watcher((a)= > {
document.getElementById('app2').innerHTML = 'Computed: 1 + number is${numberPlusOne.value}
`
})
Copy the code
Internally, vUE defines the computed property on the VM instance. Here we have no instance, so we use an object to store the returned value of computed, and use.value to get the real value of computed.
In this case, computed is actually a function, and the essence of Watcher is to store a function that needs to be triggered at a particular time. Inside the Vue, each computed property has its own instance of Watcher. It is hereafter called the computedWatcher
First look at the render function:
// Render function watcher
new Watcher((a)= > {
document.getElementById('app2').innerHTML = 'Computed: 1 + number is${numberPlusOne.value}
`
})
Copy the code
When I read the value of numberPlusOne during this render function
The dep. target is first set to the computedWatcher corresponding to numberPlusOne
The computedWatcher is special in this way
- Render Watcher can only be collected as a dependency in other DEP baskets, while
computedWatcher
The instance has its own DEP on it, which can collect other thingswatcher
As their own dependence. - Lazy evaluation, initialization without running the getter.
export default class Watcher {
constructor(getter, options = {}) {
const { computed } = options
this.getter = getter
this.computed = computed
if (computed) {
this.dep = new Dep()
} else {
this.get()
}
}
}
Copy the code
The essence of a computed implementation is that, before a value is read, dep. target must be the watcher of the rendering function that is running.
The watcher of the currently running rendering function is first collected as a dependency into the DEP basket inside the computedWatcher.
Set its own computedWatcher to the global DEP.target and evaluate:
The evaluation function will be running
() => data.number + 1
Copy the code
The Dep. Target is a computedWatcher, and the Dep dependency basket of data.number is thrown into the computedWatcher.
The dependency at this point is that the DeP basket of Data. number holds the computedWatcher, and the DEP basket of the computedWatcher holds the render Watcher.
If data.number is updated, the update will be triggered level by level. ComputedWatcher update is triggered. The render Watcher is installed in the DEP of the computedWatcher, so simply triggering this.dep.notify() triggers the render Watcher update method to update the view.
The path to the update is data.number = 5 -> computedWatcher -> Render Watcher -> Update view
Let’s change the code:
// Watcher
import Dep, { pushTarget, popTarget } from './dep'
export default class Watcher {
constructor(getter, options = {}) {
const { computed } = options
this.getter = getter
this.computed = computed
if (computed) {
this.dep = new Dep()
} else {
this.get()
}
}
get() {
pushTarget(this)
this.value = this.getter()
popTarget()
return this.value
}
// For computed use only
depend() {
this.dep.depend()
}
update() {
if (this.computed) {
this.get()
this.dep.notify()
} else {
this.get()
}
}
}
Copy the code
Computed initialization:
// computed
import Watcher from './watcher'
export default function computed(getter) {
let def = {}
const computedWatcher = new Watcher(getter, { computed: true })
Object.defineProperty(def, 'value', {
get() {
// First let computedWatcher collect render Watcher as its own dependency.
computedWatcher.depend()
// In the function passed in by the execution user, the responsive-type values are collected into the 'computedWatcher' again
return computedWatcher.get()
}
})
return def
}
Copy the code
If data.number is set to be hijacked, you can download the code and debug it step by step. After the set is triggered, you can see what the deP of number is.
watch
The use of watch is as follows:
watch(
(a)= > data.msg,
(newVal, oldVal) => {
console.log('newVal: ', newVal)
console.log('old: ', oldVal)
}
)
Copy the code
The first parameter passed in is a function that needs to read the reactive property to make sure the dependency is collected so that the next time the reactive property changes, the new and old values are printed.
The Watcher for watch is called watchWatcher. The getter function passed in is () => data.msg. WatchWatcher still sets itself to dep. target before executing it, and when it reads data. MSG, it drops the watchWatcher into the dependency basket of data. MSG.
If data. MSG is updated, the watchWatcher update method is triggered
Directly on the code:
// watch
import Watcher from './watcher'
export default function watch(getter, callback) {
new Watcher(getter, { watch: true, callback })
}
Copy the code
{watch: true, callback}}
export default class Watcher {
constructor(getter, options = {}) {
const { computed, watch, callback } = options
this.getter = getter
this.computed = computed
this.watch = watch
this.callback = callback
this.value = undefined
if (computed) {
this.dep = new Dep()
} else {
this.get()
}
}
}
Copy the code
First, the watch option and callback are saved in the constructor, but nothing else is changed.
Then in the update method.
update() {
if (this.computed) {
...
} else if (this.watch) {
const oldValue = this.value
this.get()
this.callback(this.value, oldValue)
} else{... }}Copy the code
Before calling this.get to update the value, save the old value, and pass the new value and the old value to the outside world by calling the callback function.
With just a few lines of code changes, we easily implemented a very important API: Watch.
Summary.
With the clever design of Watcher and Dep, the implementation of the responsive API inside Vue is very simple.