In this paper, the corresponding source location vue – next/packages/reactivity/SRC/effect. The ts line 53
Front knowledge
Vue3 source code uses WeakMap to store the corresponding relationship between responsive objects and dependencies, while using WeakMap is completely out of performance consideration, so Map can be used instead. WeakMap’s key value must be an object, which I think directly leads to Reactive being used in everyday use only to encapsulate object data, unlike REF (later implemented).
Write to see
You can look at the results first, implement the prototype, and then refine the details as shown below
const reactive = target= > {
return new Proxy(target, {
get(target, key, receiver) {
track();
const res = Reflect.get(target, key, receiver);
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger();
returnres; }}); };const effect = () = > {
/ /???????
};
const track = () = > {
/ /???????
};
const trigger = () = > {
/ /???????
};
// Run and see
const a = reactive({
count: 0}); effect(() = > {
console.log(`effect called...... a.count:${a.count}`); // effect called...... A. mount: 0 uses effect once by itself
});
a.count = 10; // effect called...... a.count: 10
a.count = 20; // effect called...... a.count: 20
Copy the code
After analyzing the output results, the following conclusions can be preliminarily drawn
effect
The function receives a callback function, temporarily calledeffectFn
- call
effect
And then it will do it itself first - Each update or value operation on a reactive object is called again
effectFn
So it’s pretty clear what Effect is basically trying to do
const effect = effectFn= > {
effectFn();
};
Copy the code
Yeah, it’s just a simple one, with a little bit of detail
track
Next to track, dependency collection essentially just saves the dependency relationship of the target object in an appropriate data structure (named targetMap below). In simple terms, it saves the mapping relationship between different target objects and effectFn, while WeakMap is used to save it in the source code. In line with the principle of running, Map is used to achieve this. In targetMap, you need to store three types of data: the target object, the key value, and the corresponding effectFn. Therefore, you need to nest a Map. The effectFn cannot be repeated, and there may be many different effectFn
{
[targetMap]: {
[key]: [effectFn] // [effectFn] is a Set}}Copy the code
So all we have to do is store the effectFn and the target and the key step by step.
Once the data structure is resolved, a new problem is how to expose effectFn in effect. Sharing data between functions at the same level is not so much a fancy operation. We will define a global variable activeEffect to hold the effectFn currently being executed, assuming that activeEffect is already defined
After the analysis, the concrete implementation of track is as follows
const targetMap = new Map(a)const track = (target, key) = > {
if(! activeEffect) {return;
}
let depsMap = targetMap.get(target);
if(! depsMap) { depsMap =new Map(a); targetMap.set(target, depsMap); }let deps = depsMap.get(key);
if(! deps) { deps =new Set(a); depsMap.set(key, deps); }// Save the effect function that is currently executing
deps.add(activeEffect);
};
Copy the code
trigger
The rest of the trigger is very simple. You just need to extract the corresponding DEPS and execute the effectFn
const trigger = (target, key) = > {
const depsMap = targetMap.get(target);
if(! depsMap) {return;
}
const deps = depsMap.get(key);
if(! deps) {return;
}
// Execute all effectFn's previously stored
deps.forEach(effectFn= > effectFn());
};
Copy the code
effect
Returning to the effect method, effect requires:
- Execute the function passed in
fn
- Expose what is currently being executed
effectFn
The above process is encapsulated in an internal function, as follows
let activeEffect;
const effect = fn= > {
const effectFn = () = > {
activeEffect = fn;
return fn();
};
// Execute once
effectFn();
return effectFn;
};
Copy the code
Ran to see
The above has basically completed the preparation of effect. Pull the reactive I wrote last time to do a small test
const a = reactive({
count: 0}); effect(() = > {
console.log(`effect called...... a.count:${a.count}`); // effect called...... A. mount: 0 calls effect once by itself
});
a.count = 10; // effect called...... a.count: 10
a.count = 20; // effect called...... a.count: 20
Copy the code
Small refinements
This has roughly completed the effect section, but there is still a problem. Let’s take a look at what the problem looks like
const a = reactive({ num1: 10.num2: 20 });
effect(() = > {
effect(() = > {
console.log(`a.num2: ${a.num2}`);
});
console.log(`a.num1: ${a.num1}`);
});
a.num1 = 100;
a.num1 = 200;
a.num1 = 300;
Copy the code
Here are the results
This is obviously not normal. The first and second are the first implementations of the initialization, which is fine, but when I assign a.num1, I print a.num2
Why did it go wrong
In fact, it is not hard to imagine that we use an activeEffect to record the effectFn currently executed. When an effect is nested within an effect, the inner effect overwrites the outer effect. Therefore, an outer effect is triggered, but an inner effect is executed. How to solve this problem
Since the coverage will be, you only need to use something to the record and control the execution order, here are likely to conjure up function execution stack, such is the execution stack, when recursive produce pressed new function stack, after playing stack, in order to control the order of the function, so we can use a stack to auxiliary effectFn, Control it to execute the inner effect first, and then execute the outer effect
change
The above analysis of the problem and the solution, it is very simple to implement, the following directly gives the modified code
const effectStack = [];
let activeEffect;
const effect = fn= > {
const effectFn = () = > {
try {
effectStack.push(effectFn);
// The assignment must be done once, because return is required below
activeEffect = effectFn;
return fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1]; }}; effectFn();return effectFn;
};
Copy the code
The idea is to use a stack to control activeEffect. Each time effectFn is executed, it is pushed onto the stack. ActiveEffect is always the top element of the stack, and when it is executed, it pops onto the stack. ActiveEffect overrides are addressed
conclusion
The core part of effect is that effect exposes the effectFn currently being executed, and track maintains a targetMap to store the mapping between target and effectFn for trigger consumption
Q&A
Q: Is it that simple? A: Of course not. In the source code, effect is implemented through a ReactiveEffect class, which internally implements the member methods run and stop. However, it is not very critical (in fact, it is very critical, but I write it this way also can barely achieve the purpose), so only the simplest implementation, to see the source code can be more specific. The path and file name are at the beginning of the article, down to the line number
Q: Is it a problem for you to write so simple? A: I don’t know. There will be some problems if many special cases are not handled. But these are the most important ones
Q: Who can understand your text in such a mess? A: Here’s the complete code
// effect
const effectStack = [];
let activeEffect;
const effect = fn= > {
const effectFn = () = > {
try {
effectStack.push(effectFn);
activeEffect = effectFn;
return fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1]; }}; effectFn();return effectFn;
};
const targetMap = new WeakMap(a);// track
function track(target, key) {
if(! activeEffect)return;
let depsMap = targetMap.get(target);
if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
}
let deps = depsMap.get(key);
if(! deps) { depsMap.set(key, (deps =new Set()));
}
deps.add(activeEffect);
}
// trigger
function trigger(target, key) {
const depsMap = targetMap.get(target);
if(! depsMap)return;
const deps = depsMap.get(key);
if(! deps)return;
deps.forEach(effectFn= > effectFn());
}
Copy the code