Vue3 rewrites the responsive system. Compared with Vue2, the bottom layer is implemented by Proxy object. During initialization, there is no need to go through all the attributes and convert them into GET and set through defineProperty. In addition, if there are multiple levels of nested attributes, the next level of attributes will only be recursively processed when an attribute is accessed, so the performance of the responsive system in Vue3 is better than that in Vue2.
Vue3’s responsive system can listen for dynamically added attributes and for attributes to be deleted, as well as changes to array indexes and length attributes. Vue3’s responsive system can also be used as a module alone.
Next we own implementation Vue3 core function of the response system (reactive/ref/toRefs/computed/effect/track/trigger) to study the reactive principle.
First we use Proxy to implement the first function reactive in the reactive formula.
reactive
Reactive receives a parameter and determines whether the parameter is an object. If it is not returned directly, Reactive can only convert the object to a reactive object.
The interceptor object handler is then created, which holds the interceptor methods such as get,set,deleteProperty, and finally creates and returns the Proxy object.
// Check if it is an object
const isObject = val= >val ! = =null && typeof val === 'object'
// Call reactive if it is an object
const convert= target= > isObject(target) ? reactive(target) : target
// Determine whether the object has a key attribute
const haOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) = > haOwnProperty.call(target, key)
export function reactive (target) {
if(! isObject(target)) {// If the object is not returned directly
return target
}
const handler = {
get (target, key, receiver) {
// Collect dependencies
const result = Reflect.get(target, key, receiver)
// If the attribute is an object, recursive processing is required
return convert(result)
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true;
// we need to determine whether the current new value and oldValue are equal. If not, we need to override the oldValue and trigger the update
if(oldValue ! == value) { result =Reflect.set(target, key, value, receiver)
// Trigger the update...
}
The set method needs to return a Boolean value
return result;
},
deleteProperty (target, key) {
// First check whether the current target has its own key attribute
// If the key attribute is present and the deletion triggers the update
const hasKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
// Trigger the update...
}
returnresult; }}return new Proxy(target, handler)
}
Copy the code
The Reactive function is now written, and we can write the dependency collection process.
The dependency collection process creates three collections, targetMap,depsMap, and DEP.
Where targetMap is used to record the target object and dictionary. WeakMap is used. Key is the target object. Since you can call the same Effect multiple times to access the same Effect property, the property will collect multiple dependencies corresponding to multiple Effect functions.
An attribute can correspond to multiple Effect functions. When the update is triggered, the corresponding Effect function can be found through the attribute and executed.
We will implement effect and track respectively.
The effect function takes a function as an argument, and we first define a variable outside to store the callback so that the track function can access the callback.
let activeEffect = null;
export function effect (callback) {
activeEffect = callback;
// Access responsive object properties to collect dependencies
callback();
// Set null at the end of dependency collection
activeEffect = null;
}
Copy the code
The track function takes two parameters, the target object and the property, and internally stores the target in targetMap. You need to define one of these maps first.
let targetMap = new WeakMap(a)export function track (target, key) {
// Check whether activeEffect exists
if(! activeEffect) {return;
}
// depsMap stores the mapping between objects and effects
let depsMap = targetMap.get(target)
// If not, create a map and store it in targetMap
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Find the corresponding DEP object based on the property
let dep = depsMap.get(key)
// deP is a collection that stores the effect function corresponding to the attribute
if(! dep) {// If not, create a new collection to add to depsMap
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
Copy the code
Track is a collection dependent function. You need to call it in the get method of the reactive function.
get (target, key, receiver) {
// Collect dependencies
track(target, key)
const result = Reflect.get(target, key, receiver)
// If the attribute is an object, recursive processing is required
return convert(result)
},
Copy the code
This completes the entire dependency collection. The next step is to implement trigger updates. The corresponding function is trigger, which is the opposite of track.
The trigger function takes two parameters, target and key.
export function trigger (target, key) {
const depsMap = targetMap.get(target)
// If not found, return directly
if(! depsMap) {return;
}
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect= > {
effect()
})
}
}
Copy the code
The trigger function is triggered in the set and deleteProperty of reactive.
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true;
// we need to determine whether the current new value and oldValue are equal. If not, we need to override the oldValue and trigger the update
if(oldValue ! == value) { result =Reflect.set(target, key, value, receiver)
// Trigger the update...
trigger(target, key)
}
The set method needs to return a Boolean value
return result;
},
deleteProperty (target, key) {
// First check whether the current target has its own key attribute
// If the key attribute is present and the deletion triggers the update
const hasKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
// Trigger the update...
trigger(target, key)
}
return result;
}
Copy the code
ref
A REF receives a parameter that can be either a raw value or an object. If the object is passed in and created by a REF, it returns it. If it is a normal object, reactive is called to create a reactive object.
export function ref (raw) {
// Determine whether raw is an object created by ref, if so, return it directly
if (isObject(raw) && raw.__v__isRef) {
return raw
}
// The convert function has been defined previously, and reactive is called if the argument is an object
let value = convert(raw);
const r = {
__v__isRef: true,
get value () {
track(r, 'value')
return value
},
set value (newValue) {
// Determine whether the new value is equal to the old value
if(newValue ! == value) { raw = newValue value = convert(raw)// Trigger the update
trigger(r, 'value')}}}return r
}
Copy the code
toRefs
ToRefs receives reactive objects returned by Reactive functions or returns them directly if they are not. Convert all attributes of the passed object into an object similar to that returned by ref. Mount the quasi-converted attributes onto a new object.
export function toRefs (proxy) {
Create an array of the same length if it is an array, otherwise return an empty object
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
for (const key in proxy) {
ret[key] = toProxyRef(proxy, key)
}
return ret;
}
function toProxyRef (proxy, key) {
const r = {
__v__isRef: true,
get value () { // This is already a reactive object, so there is no need to collect dependencies
return proxy[key]
},
set value (newValue) {
proxy[key] = newValue
}
}
return r
}
Copy the code
What toRefs does is actually make every property in Reactive reactive. The reactive method creates a reactive object, but if you deconstruct the object returned by Reactive, it is no longer reactive. ToRefs supports the deconstruction to remain reactive.
computed
Then simulate the internal implementation of a computed function
For computed, you need to receive as a parameter a function with a return value, which is the value of the calculated property, listen for changes in the responsive data inside the function, and finally return the result of the function’s execution.
export function computed (getter) {
const result = ref()
effect(() = > (result.value = getter()))
return result
}
Copy the code
Computed functions listen for changes in the responsive data in the getter through effect, because properties that access the responsive data when the getter is executed in effect collect dependencies, and when the data changes, the effect function is executed again, storing the result of the getter into Result.