preface
As the saying goes, know the person and know why, Vue3 has come out for a long time, in the skilled use of Vue framework brick at the same time, we are bound to understand its underlying implementation principle, which is one of the core concepts of Vue, today summarize the Vue responsive implementation principle.
What is reactive?
Reactive is a declarative programming paradigm for data streaming and change propagation. This means that static or dynamic data flows can be easily expressed in a programming language, and the associated computational model automatically propagates the changing values through the data flows. In plain English the data changes and so does the view.
You probably know more or less how Vue’s responsive implementation works:
- The response formula of Vue2 is based on
Object.defineProperty
Implementation of the - Vue3’s responsiveness is based on ES6
Proxy
To implement the
There are fundamental differences between VUe3 and VUe2 due to the different implementation of responsive apis, and this is where the two versions are good and bad.
Vue2
The responsivity of Vue2 is based on Object.defineProperty. Here is a simple example of object.defineProperty:
const createReactive = (target, prop, value) = > {
return Object.defineProperty(target, prop, {
get() {
console.log(Visited `${prop}Attribute `)
return value
},
set(newValue) {
console.log(` will${prop}By the - >${value}-> Set to ->${newValue}`)
value = newValue
}
})
}
const data = createReactive({}, 'name'.'lisi')
console.log('data.name :>> ', data.name);
// Access the name attribute
// data.name :>> lisi
data.name = 'zhangsan'
// Access the name attribute
// data.name :>> lisi
// Set name from ->lisi-> to ->zhangsan
Copy the code
The above example is a simple application of Object.defineProperty, and object.defineProperty also has some drawbacks that made it obsolete in Vue3. See the following example:
// Add the following code
data.age = 18
console.log('data.age :>> ', data.age); // data.age :>> 18
data.age = 28
console.log('data.age :>> ', data.age); // data.age :>> 28
Copy the code
Object.defineproperty does not trigger get or set when accessing or setting a new age attribute to data, so object.defineProperty only listens for attributes in the original Object. This is why Vue2 uses vue. $set to modify new attributes of objects.
Vue3
The Proxy method of ES6 naturally supports new attributes of Proxy objects. When accessing and setting values, get/ SET methods are triggered. So it makes up for the fact that Vue2 uses object.defineProperty to implement responsiveness, as shown below:
const createReactive = (target) = > {
return new Proxy(target, {
get(target, prop) {
console.log(Visited `${prop}Attribute `)
return Reflect.get(target, prop)
},
set(target, prop, value) {
console.log(` will${prop}By the - >${target[prop]}-> Set to ->${value}`)
Reflect.set(target, prop, value)
}
})
}
const data = createReactive({
name: 'lisi',})console.log('data.name :>> ', data.name);
// Access the name attribute
// data.name :>> lisi
data.name = 'zhangsan' // Set name from ->lisi-> to ->zhangsan
Copy the code
DefineProperty is the same as object.defineProperty. The only difference is that Proxy can trigger get/set methods on new attributes of objects.
// Add the above code
data.age = 18 // Change age from ->undefined-> to ->18
console.log('data.age :>> ', data.age);
// The age attribute is accessed
// data.age :>> 18
data.age = 28 // Change age from ->18-> to ->28
console.log('data.age :>> ', data.age);
// The age attribute is accessed
// data.age :>> 28
Copy the code
Object.defineproperty does not natively listen for new attributes in a responsive manner, but it does provide the corresponding Vue.$set method. While Proxy is convenient and powerful, its biggest problem is that it is not compatible with Internet Explorer 11. If the project also considers Internet Explorer, it can only use the Vue2 version. You can use Caniuse to check compatibility.
Let’s start with a simple function
Let’s start with the following code:
let x;
let y;
let f = n= > n * 100 + 100
x = 1
y = f(x)
console.log(y) / / 200
x = 2
y = f(x)
console.log(y) / / 300
x = 3
y = f(x)
console.log(y) / / 400
Copy the code
In the code above, to make y change with x, it’s a little bit easier to just run the code again.
watchEffect
As mentioned above, every time x changes, you have to execute the same code again to update y, which is not elegant. We can encapsulate a watchEffect function.
let x;
let y;
let f = n= > n * 100 + 100
let watchEffect = () = > {
y = f(x)
console.log(y)
}
x = 1 / / 200
x = 2 / / 300
x = 3 / / 400
Copy the code
The vue2 Object.defineProperty implementation will be used first, and the watchEffect function will be re-executed each time x changes. Object.defineproperty is an operation on an Object, so you need to implement a ref function to change the x variable from a base type to a reference data type.
Realize the ref
So for that, we need to define a ref function.
const ref = initValue= > {
let value = initValue
return Object.defineProperty({}, 'value', {
get() {
return value
},
set(newValue) {
value = newValue
activeEffect()
}
})
}
Copy the code
In the above code we can encapsulate a createReactive function that can be reused by other apis.
const createReactive = (target, prop, value) = > {
return Object.defineProperty(target, prop, {
get() {
return value
},
set(newValue) {
value = newValue
}
})
}
const ref = (initValue) = > createReactive({}, 'value', initValue)
Copy the code
In order for y to change with x, we also need a global activeEffect variable to hold the current watchEffect function.
let x;
let y;
let f = n= > n * 100 + 100
let activeEffect;
let watchEffect = cb= > {
activeEffect = cb
activeEffect()
}
const createReactive = (target, prop, value) = > {
return Object.defineProperty(target, prop, {
get() {
return value
},
set(newValue) {
value = newValue
activeEffect()
}
})
}
const ref = (initValue) = > createReactive({}, 'value', initValue)
x = ref(1) / / 200
watchEffect(() = > {
y = f(x.value)
console.log(y)
})
x.value = 2 / / 300
x.value = 3 / / 400
Copy the code
The above implementation also implements a simple watchEffect function for a single reactive variable. In a project where there are many reactive variables and multiple watchEffect functions, one activeEffect variable is not enough and you need to rely on the collection class.
Dep
As mentioned above, with multiple watchEffect functions, you need a class to manage them. Dep is defined as follows:
// Add the above code
class Dep {
deps = new Set(a)depend() {
if (activeEffect) {
this.deps.add(activeEffect)
}
}
notify() {
this.deps.forEach(dep= > dep())
}
}
Copy the code
The above DEP uses Set for collection because Set can be automatically de-duplicated. The addition of dependencies occurs when properties are accessed, and the collection of dependencies occurs when property values change. The code is as follows:
// Add the above code
let watchEffect = cb= > {
activeEffect = cb
activeEffect()
activeEffect = null
}
const createReactive = (target, prop, value) = > {
let dep = new Dep()
return Object.defineProperty(target, prop, {
get() {
dep.depend()
return value
},
set(newValue) {
value = newValue
dep.notify()
}
})
}
Copy the code
The above code implements dependency collection and dependency notification for multiple variables, but there is a flaw in the above code that should not be difficult to be careful. The watchEffect function executes each successive assignment to the x variable. If there are 100 assignments, 100 function operations are performed. So we need to optimize our code for asynchronous queues.
Implement nextTick
As mentioned above, we need a queue to optimize our code. The code is as follows:
// Add the above code
let nextTick = (cb) = > Promise.resolve().then(cb)
let queue = []
let queueJob = job= > {
if(! queue.includes(job)) { queue.push(job) nextTick(flushJobs) } }let flushJobs = () = > {
let job;
while((job = queue.shift()) ! = =undefined) {
job()
}
}
class Dep {
deps = new Set(a)depend() {
if (activeEffect) {
this.deps.add(activeEffect)
}
}
notify() {
this.deps.forEach(dep= > queueJob(dep))
}
}
...
x.value = 2
x.value = 3 / / 400
Copy the code
Vue.nexttick serves to defer the callback until after the next DOM update cycle. Use it immediately after you have changed some data to wait for DOM updates. The simple asynchronous queue constructed above with Promise allows for performance optimization. If you are not familiar with asynchrony, check out the Js Event Loop mechanism.
Implement reactive
Reactive function reactive function reactive function reactive function reactive function reactive
const reactive = (obj) = > {
Object.keys(obj).forEach(key= > createReactive(obj, key, obj[key]))
return obj
}
Copy the code
Take a look at the results:
.const data = reactive({
count: 0
})
watchEffect(() = > {
console.log(data.count)
})
data.count = 2 / / 2
Copy the code
To realize the computed
Let’s start with a simple computed function:
let computed = (fn) = > {
let value;
return {
get value() {
value = fn()
return value
}
}
}
Copy the code
A quick look at the results:
.let computed = (fn) = > {
let value;
return {
get value() {
value = fn()
return value
}
}
}
let count = ref(0)
watchEffect(() = > {
console.log(computedValue.value)
})
const computedValue = computed(() = > count.value + 3)
count.value++ / / 4
Copy the code
The above code has basically implemented the functions of computed function, but it is well known that computed function has the characteristics of cache. To achieve the characteristics of cache, we can define a dirty variable to control. And when the dirty variable is reset back to its original value: when the response variable in the FN function changes above. The watchEffect function will be triggered by a responsive variable change, so we need to wrap and process the watchEffect function. Here we can extract the contents of the watchEffect function and define an effect function. Why wrap the Effect function separately? We don’t want to add options to fn to contaminate the original function.
let effect = (fn, options = {}) = > {
let effect = (. args) = > {
try {
activeEffect = effect
returnfn(... args) }finally {
activeEffect = null
}
}
effect.options = options
return effect
}
let watchEffect = (cb) = > {
let runner = effect(cb)
runner()
}
Copy the code
The Vue3 source code defines a Schedular hook function as a way to fire after a dependency response occurs. With the Schedular hook function executed when the notify dependency is fired, we can set dirty to true when the dependency changes.
let computed = (fn) = > {
let value;
let dirty = true
let runner = effect(fn, {
schedular: () = > {
if(! dirty) { dirty =true}}})return {
get value() {
if (dirty) {
value = runner()
dirty = false
}
return value
}
}
}
class Dep {
deps = new Set(a)depend() {
if (activeEffect) {
this.deps.add(activeEffect)
}
}
notify() {
this.deps.forEach(dep= > queueJob(dep))
this.deps.forEach(dep= > {
dep.options && dep.options.schedular && dep.options.schedular()
})
}
}
Copy the code
Complete computations have been achieved.
Realize the watch
The watch function needs to receive three parameters, listening object, callback function and optional parameter configuration. The simple code is as follows:
let watch = (source, cb, options = {}) = > {
let getter = () = > {
return source()
}
const { immediate } = options
let oldValue;
let runner = effect(getter, {
schedular: () = > applyCb()
})
const applyCb = () = > {
let newValue = runner()
if(newValue ! == oldValue) { cb(newValue, oldValue) oldValue = newValue } }if (immediate) {
applyCb()
} else {
oldValue = runner()
}
}
Copy the code
In the source code, the arrow value function and array are judged, and only the arrow function is processed here. The optional parameter only implements immediate. Let’s look at the simple result:
let watch = (source, cb, options = {}) = > {
let getter = () = > {
return source()
}
const { immediate } = options
let oldValue;
let runner = effect(getter, {
schedular: () = > applyCb()
})
const applyCb = () = > {
let newValue = runner()
if(newValue ! == oldValue) { cb(newValue, oldValue) oldValue = newValue } }if (immediate) {
applyCb()
} else {
oldValue = runner()
}
}
let x = ref(0)
watch(() = > x.value, (newValue, oldValue) = > {
console.log('newValue, oldValue :>> ', newValue, oldValue);
}, {immediate: true})
Copy the code
Implement watchEffect
The watchEffect function returns a cleanup function, which we now implement briefly. First we need to define the clearUpEffect clear function in the watchEffect function. The code is as follows:
let watchEffect = cb= > {
let runner = effect(cb)
runner()
return () = > {
clearUpEffect(runner)
}
}
let clearUpEffect = (effect) = > {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
}
}
Copy the code
In the above code we need to get the dependency from the effect function, so we need to add the dependency object to Effect when the dependency is collected.
let effect = (fn, options = {}) = > {
let effect = (. args) = > {
try {
activeEffect = effect
returnfn(... args) }finally {
activeEffect = null
}
}
effect.options = options
effect.deps = []
return effect
}
class Dep {
deps = new Set(a)depend() {
if (activeEffect) {
this.deps.add(activeEffect)
activeEffect.deps.push(this.deps)
}
}
notify() {
this.deps.forEach(dep= > queueJob(dep))
this.deps.forEach(dep= > {
dep.options && dep.options.schedular && dep.options.schedular()
})
}
}
Copy the code
The above dependency collection function also adds the dependency object to the effetc definition deps property.
Implement array methods
Methods that operate on arrays in Object.defineProperty do not trigger responsiveness, whereas vue2 overrides array methods so they can trigger view changes. The implementation code is as follows:
let push = Array.prototype.push
Array.prototype.push = function(. args) {
push.apply(this, [...args])
this._dep && this._dep.notify()
}
const createReactive = (target, prop, value) = > {
target._dep = new Dep()
return Object.defineProperty(target, prop, {
get() {
target._dep.depend()
return value
},
set(newValue) {
value = newValue
target._dep.notify()
}
})
}
Copy the code
The above code only implements the push method, which simply mounts the dependency on the object and notifies the dependency update when the object calls the array method.
To achieve the set
Set function implementation is relatively simple, directly look at the code:
const set = (target, prop, initValue) = > createReactive(target, prop, initValue)
Copy the code
Vue3
DefineProperty in vue2 and Proxy in Vue3. We just need to replace the Object. DefineProperty in createReactive with Proxy. Basically nothing else needs to change.
const createReactive = (target) = > {
let deps = new Dep()
return new Proxy(target, {
get(target, prop) {
deps.depend()
return Reflect.get(target, prop)
},
set(target, prop, value) {
Reflect.set(target, prop, value)
deps.notify()
}
} )
}
const ref = initValue= > createReactive({value: initValue})
const reactive = (obj) = > {
Object.keys(obj).forEach(key= > createReactive(obj))
return obj
}
Copy the code
The demo source code
- Vue2 demo source
- Vue3 demo source
Refer to the article
- Lin Sanxin drew 8 diagrams, the most understandable Vue3 response core principle analysis