1. List some of the things that are important to the implementation of Reactive
- Reactive: This API can transform data into responsive data and ensure that the interface can be updated after the data is used after reative transformation.
- Basic understanding and use of Proxy API, if not familiar with it, recommend Ruan Yifeng ES6 introduction
- Track relies on the collection function to collect the dependencies needed to access data and is executed in proxy GET
- Trigger relies on the trigger function to notify the execution of the collected effect function, which is executed in the proxy’s set
- TargetMap, a WeakMap data structure that stores data dependencies, is equivalent to DEP in VUe2
- Effect function is equivalent to Watcher in VUe2, component rendering, computed property, watch function, and watchEffect function all depend on effect function
- ActiveEffect, which represents the effect currently being executed and is used to collect dependencies
- EffectStack, which is used to simulate the stack structure to store the effect array, to solve the effect nested execution scenario in the execution process, and to ensure the correct collection of dependent functions
The sampleCopy the code
const { reactive, effect } = vue;
const effectDom = document.querySelector(".effect");
const state = reactive([1.2.3]);
effect(function () {
effectDom.innerHTML = state
})
setTimeout(() = > {
state[2] = 2;
}, 1000)
Copy the code
After rendering the array to the interface for the first time, modify the state data 1 second later, and the interface automatically updatesCopy the code
2. The reactive function
Reactive function implementationCopy the code
/ / reactive function
export function reactive (target) {
return createReactiveObject(target, baseHandler)
}
// createReactiveObject Creates a proxy because there are many proxy modes, which can be read-only or read-only
// readOnlyRactive, readOnlyRactive, readOnlyRactive, readOnlyRactive, readOnlyRactive, readOnlyRactive, readOnlyRactive, readOnlyRactive
Reactive
function createReactiveObject (target, baseHandler) {
// First check if target is an object type, if not not allowed to cast to response
if(! isObject(target)) {return;
}
// Do the proxy
let proxy = new Proxy(target, baseHandler);
return proxy;
}
// baseHandler
export const baseHandler = {
get (target, key, recevier) {
// Get the key value
const res = Reflect.get(target, key);
// Collect dependencies
track(target, key);
return res;
},
set (target, key, val, recevier) {
// get the old value first
const oldVal = Reflect.get(target, key);
// You need to decide whether to add or change the attribute value
Arr [10] = arR [10] = arR [10
// Determine the length and size of the key
const hadKey = Array.isArray(target) && isIntegerKey(key) ? Number(key) < target.length :
hasOwn(target, key);
// Set the new value
const res = Reflect.set(target, key, val);
if(! hadKey) {// New attribute notifications depend on updates
trigger(target, "ADD", key, val, oldVal);
} else {
// Change property notification depends on update
trigger(target, "SET", key, val, oldVal);
}
returnres; }}Copy the code
CreateReactiveObject (createReactiveObject); reactive data and readOnly data are available in the source code, and different types of data are created by createReactiveObject. We only implement rective data here.
-
CreateReactiveObject implementation. In 2.1 createReactiveObject, the first step is to determine the data type. If the data type is basic, the reactive conversion is not performed. 2.2 Use proxy to proxy the incoming data, use baseHandler as the proxy configuration item, and pass in the second parameter of proxy. 2.3 Returning proxy data for users.
-
When a user accesses or sets the data returned by the Proxy, get and SET in the baseHandle are triggered.
-
Get implements the logic in baseHandler.
4.1 Get the value with reflect.get.
4.2 Dependencies required to collect data by track (main logic).
4.3 Returning Data to the User.
-
Set implementation logic in baseHandler.
5.1 Use reflect. get first to get the old oldValue.
5.2. If it is an array and the key is a string number, the value of the current key is greater than the length of the array. If it is an object, the hasOwnProperty is used to determine whether the key exists in the object. Determine whether to add or modify.
5.3 Set the new value with reflect. set
5.4 Trigger notification relies on the hadKey to tell the trigger whether to add or change properties. Pass both newValue and oldValue into the function
3. Track function implementation
// This is used to store effects that are being executed, and then collect dependencies
let activeEffect;
// Rely on storage data destruct
const targetMap = new WeakMap(a);// Used to collect dependencies
export function track (target, key) {
// Get the deps object of the object store
let depsMap = targetMap.get(target);
// If the deps object does not exist, it is the first time for this object to collect dependencies
if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
}
// Get the deP-dependent data stored in the key of the object
let deps = depsMap.get(key);
// If dePS does not exist, it is the first time to collect dependencies
if(! deps) { depsMap.set(key, (deps =new Set()));
}
// The currently executing activeEffect is the dependent function we need to collect for the current value
// Check if the current side effect function has already been collected
if(! deps.has(activeEffect)) {// Add the dependency
deps.add(activeEffect);
// Side effect functions also need to collect DEPs, forming a two-way recording processactiveEffect.deps.push(deps); }}Copy the code
- Obtain the dependent map object depsMap based on target. If depsMap does not exist, create a map and store it in the targetMap.
Select * from depsMap; select * from depsMap; select * from depsMap;
- The final step is to set up a two-way data store, where DEPS stores the Effect side effect function for the next data change triggered execution, and Effect stores its DEPS for subsequent cancellation data responses.
4 Trigger function implementation
// This is used to store effects that are being executed, and then collect dependencies
let activeEffect;
// Rely on storage data destruct
const targetMap = new WeakMap(a);// Used to collect dependencies
export function track (target, key) {
// Get the deps object of the object store
let depsMap = targetMap.get(target);
// If the deps object does not exist, it is the first time for this object to collect dependencies
if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
}
// Get the deP-dependent data stored in the key of the object
let deps = depsMap.get(key);
// If dePS does not exist, it is the first time to collect dependencies
if(! deps) { depsMap.set(key, (deps =new Set()));
}
// The currently executing activeEffect is the dependent function we need to collect for the current value
// Check if the current side effect function has already been collected
if(! deps.has(activeEffect)) {// Add the dependency
deps.add(activeEffect);
// Side effect functions also need to collect DEPs, forming a two-way recording processactiveEffect.deps.push(deps); }}Copy the code
-
If the depMap does not exist, the current data does not depend on the depMap. If the depMap does not exist, the current data does not depend on the depMap
-
Getting the key from depMap depends on the set set, defining an Effects set data structure to store effects to be executed, and defining the add function method to add effects
-
Get the corresponding dependent function that needs to be updated through data changes and add it to Effects with Add
3.1 The first step is to deal with the special case of the array operation length changing the array.
1– If the value of length is smaller than the subscript of the array used in the template, the data larger than the current length is not displayed in the page, so we need to loop depsMap to find all subscript dependencies larger than length and perform the update interface. If it is not a value of a single array, the template will access the length array when rendering, so the length property will collect the corresponding array dependencies. When we change the length value and the array data changes, we can get the length property dependency and add it to the effects to execute the dependency function. Update interface 3.2 After dealing with the special case of array, the following is to determine whether to ADD the attribute ADD operation or modify the attribute set operation according to hadKey in the set function
1– ADD ADD property if it’s an array, check if key is a numeric string, if it’s an array that’s been modified by subscript, here we get the set of length dependency functions that we ADD to effects if it’s an object’s new property, get all of the object’s data dependencies, Add to Effects 2– Set to modify the data, directly obtain the corresponding key value set to add to Effects
-
Loop through the Effects data structure, using the run function to execute the effect side effect function to perform the update operation
Effect function implementation
// this is javascript
// Side effect function, equivalent to Watcher in VUe2
export function effect (fn) {
// Because the side effect function is executing, we need to store the currently executing side effect function, which is used to get the value
// And you need to add some attributes, so use the higher-order function to create effect
let effect = createReactiveEffect(fn);
if(! effect.options.lazy) {// If it is not a lazy function (that is, computed), use it to collect dependencies for the first time
effect();
}
return effect;
}
// Add a unique tag to each side effect function
let uid = 0;
// Actually create the side effects function and do some property definitions
function createReactiveEffect (fn, options = {}) {
const effect = function () {
// Set activeEffect to null to prevent dependencies on values that are not in the effect function
/ /! Effectstack.includes (effect) Determines the disposal of dead objects
// effect(function () {
// state.a++;
// })
// When effect is state. A ++, dependent updates are told to fire frequently because a increases every time,
Effect is executed because effect is in the effectStack
// And we are already executing the side effects function, so there is no need to trigger again
if(! effectStack.includes(effect)) {try {
// Put effect on the stack first to solve the effect nesting problem
effectStack.push(effect);
activeEffect = effect;
return fn();
} finally {
// Set activeEffect to the previous effect in the stack
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
}
effect.active = true;
// Unique tag
effect.uid = uid++;
// Used to store which value to depend on
effect.deps = [];
// Some other properties of effect, such as lazy, shelduler
effect.options = options;
return effect
}
Copy the code
- If the value of lazy is true, it indicates that it is a computed or watch function and does not need to be created. If it is not true, the effect function needs to be executed immediately
2. Implement the createReactiveEffect function
2.1 Create effect in createReactiveEffect and add some properties for effect, such as DEPS to store dependent DEP, options configuration item, uid to identify the uniqueness of effect, and active to identify whether an effect is active.
In fact, the internal essence is to execute the fn function passed in. Fn may be defined by the user by calculating the property, watch or watchEffect, or it may be a rendering function.
2.3 Effect should be stored in the effectStack before it is executed, and then assigned to activeEffect (activeEffect= effect) and then executed fn. Fn may access the data used by proxy. Accessing data triggers the Get method of the baseHandler in the Proxy, The track in get stores the activeEffect function into the Set structure that accesses the data. This completes the collection of dependencies. ActiveEffect = effectStack[effectStack.length-1];
Here’s what the stack structure does
-
Prevent dead loops
effect(function () { state.a++; }) Copy the code
When effect is executed, it changes the value of state.a, triggering the trigger method and notifying effect to execute. Effectstack.includes (effect) Determines whether the current effect is running or not. If the current effect is in the stack, it will not be executed and an infinite loop will be avoided
-
The nesting effect function case
const counter = reactive({
num: 0.num2: 0
})
function logCount() {
console.log('num2:', counter.num2)
}
effect(() = > {
effect(logCount)
console.log('num:', counter.num)
} )
Copy the code
When an outer effect is executed, activeEffect is set to the outer effect and when an inner effect is executed, activeEffect is set to the outer effect
-
If you don’t use the effectStack, you need to set activeEffect to NULL at the end of the effect function. ActiveEffect is null when console.log(‘num:’, counter. Num) is executed, num cannot collect dependent functions
-
When using the stack effectStack, activeEffect is set to the outer effect and pushed into the array, then inner effect is pushed into the array, and activeEffect is the inner effect, Console. log(‘num2:’, counter.num2), num2 successfully collects the internal effect function as a dependent, after collecting the effectStack, delete the internal effect, and assign the last item of the array to the activeEffect. Log (‘num:’, counter.num), num successfully collects the external effect function as a dependency, and removes the external effect function from the effectStack. Because the array is now empty, activeEffect is set to NULL
Git address
The code links to the miniVue3 folder for a simple implementation of vue3 code