Summary of the previous article: Design and Implementation of 3.0 Responsive System
1. Set a reasonable effect side effect function
As mentioned above, if we directly use the simple effect
function as a side effect function, if a side effect function is not called effect
, wouldn’t it be impossible to find it. The solution is also very simple, we set a global variable to register the side effect function, the code is as follows:
let activeEffect = null function effect(fn) {<!-- --> // When the effect function is called to register the side effect function, assign the side effect function fn to activeEffect activeEffect = fn fn() }
At this time, the effect
function is only used for registration. It receives a parameter fn
, which is the side effect function to be registered. You can use effect< in the following way /code> function:
effect( () => {<!-- -->document.body.innerText = obj.text} )
At this time, an anonymous function is passed to effect
as a side effect function, and this anonymous function will be assigned to activeEffect
, so you only need to pass activeEffect
each time Just put it in the bucket. like:
const bucket = new Set() const obj = new Proxy(data, {<!-- --> get(target, key) {<!-- --> // If the object has a side effect function, put it in the bucket if(activeEffect) bucket.set(target, activeEffect) return target[key] }, set(target, key, newVal) {<!-- --> // The side effect function execution of each assignment to take out the object bucket. forEach(fn => fn()) target[key] = newVal return true } })
The execution logic in the above code can be clearly seen, since the side effect function is stored in activeEffect
, you can directly use activeEffect
when get
Functions are put into buckets so that there is no need to depend on the name of the side effect function.
2. More detailed binding of side effect functions
All the side-effect functions we mentioned above are actually directly bound to the object, because we only set a bucket for the object, so every time you change any value in the object, all the side-effect functions in the bucket will be executed.
What problems will this cause? The most obvious one is performance waste. If a large object has many side-effect functions for each element, then we will execute all side-effect functions when we change an unrelated element.
At this time, we can change the way of bucketing, use a WeakMap as the bucket, each key in the WeakMap is a responsive object, and its value is a Map, and the key in the Map is the attribute name in the object. The value of the key is Set, and at this time, the Set is loaded with the side effect function of the object element. as the picture shows:
The code is implemented as follows:
// WeakMap description see below const bucket = new WeakMap() const obj = new Proxy(data, {<!-- --> get(target, key) {<!-- --> // No side effect function returns directly if(!activeEffect) return target[key] // Get the Map of all elements in the current object let depsMap = bucket. get(target) // Create a new one when the current object does not have a Map if(!depsMap) {<!-- --> bucket.set(target, (depsMap = new Map())) } // Get the Set of all side-effect functions of the current element let deps = depsMap. get(key) // If not, create and add a side effect function if(!deps) depsMap.set(key, (deps = new Set())) deps. add(activeEffect) return target[key] }, set(target, key, newVal) {<!-- --> target[key] = newVal const depsMap = bucket. get(target) if(!depsMap) return const deps = depsMap. get(key) deps & amp; & amp; deps.forEach(fn => fn()) return true } }) }
In fact, the code looks very complicated. In fact, there are only three containers, WeakMap, Map, and Set. Here, WeakMap is used to put all objects as key values (keys), and the map is a Map. This Map is based on the object's All element attribute names are used as keys, and its value is a Set, which stores all side effects of the current key.
This way each side effect function is finely bound to an element of an object.
3. About WeakMap
Here we also need to talk about the difference between WeakMap and Map. In fact, the mapping relationship between the two is the same. The difference is that WeakMap is a weak reference. What is a weak reference? Suppose we have an object that is only referenced by a Map. At this time, the object has no other references. At this time, the object will not be reclaimed by the garbage collector. Because the Map exists, the reference to the object will always exist, but if this The object is only referenced by a WeakMap, and will be reclaimed by the garbage collector when there are no other references. That is, WeakMap will not affect the garbage collector, as the following code can explain well:
const weakMap = new WeakMap() const map = new Map() (function () {<!-- --> const foo = {<!-- -->foo: 1} const bar = {<!-- -->bar: 1} map.set(foo, 1) weakMap.set(bar, 1) })()
In the immediate execution function, the two objects foo are referenced by map, and bar is referenced by weakMap. After execution, bar will be recycled, but foo still exists.
Another point of WeakMap is that there is no iterator, and it cannot iterate through values directly like Map, so generally WeakMap is often used for mapping that does not affect the object itself, or for marking.
It is also well understood to use WeakMap as the outermost bucket in Vue. This will not cause the object to be referenced by the bucket for a long time and cannot be recycled. The bucket will not affect the execution of the program itself.
4. Branch switching and cleanup
Before introducing branch switching, you can encapsulate the above code first, encapsulate the collection of side effects in the get
interception function into a track
function, and set
The trigger of the side effect function in the code> function is divided into the trigger
function, as follows:
// Use buckets to put all objects containing side-effect functions into const bucket = new WeakMap() const obj = new Proxy(data, {<!-- --> get(target, key) {<!-- --> // trace function track(target, key) return target[key] }, set(target, key, newVal) {<!-- --> target[key] = newVal // trigger function trigger(target, key) return true } }) function track(target, key){<!-- --> // No side effect function returns directly if(!activeEffect) return target[key] // Get the Map of all elements in the current object let depsMap = bucket. get(target) // Create a new one when the current object does not have a Map if(!depsMap) {<!-- --> bucket.set(target, (depsMap = new Map())) } // Get the Set of all side-effect functions of the current element let deps = depsMap. get(key) // If not, create and add a side effect function if(!deps) depsMap.set(key, (deps = new Set())) deps. add(activeEffect) } function trigger(target, key) {<!-- --> const depsMap = bucket. get(target) if(!depsMap) return const deps = depsMap. get(key) deps & amp; & amp; deps.forEach(fn => fn()) } }
In this way, it is much simpler and clearer in the proxy about Proxy.
Let's look at a piece of code below:
const data = {<!-- -->ok: true, text: 'hello world'} const obj = new Proxy(data, {<!-- -->/* omitted */}) effect(() => {<!-- --> // If obj.ok is true, read the value of obj.text document.body.innerText = obj.ok? obj.text: 'not' })
In the responsive system we wrote, this side effect function will be associated with obj.ok and obj.text at the same time, and this side effect function will be triggered whether obj.ok or obj.text is modified. But if you think about it carefully, is it really necessary? In fact, it is not necessary.
In this code, if obj.ok is true, then this side effect function is associated with obj.text, otherwise it is not associated, because the text content of body is always not.
So how to optimize this point? In fact, we said in the previous article that a responsive data execution process is:
- Modify the value of obj.content, which will trigger the execution of the effect function.
- Trigger the effect function, which gets the value of obj.content.
I mentioned the second point here before, whether triggering the effect function will definitely get the value of obj.content, the answer is yes, if you don’t get the value of obj.content here, you don’t need to create responsive data.
Based on this, we delete the side effect function from all elements before executing the side effect function, and then execute the side effect function. If the side effect function needs to read the element value of the current object during the execution of the side effect function, this will recreate the side effect function.
In this way, our code needs to add three parts, the first part is cleanup
used to delete the side effect function from all elements, and the second part is to add an array to the side effect function for recording and this side effect function Map of all related elements, the third part is to record the elements associated with the side effect during track
.
function cleanup(effectFn) {<!-- --> // traverse the collection containing the side effect function effctFn for(let i=0;i<effectFn.deps.length;i ++ ) {<!-- --> const deps = effectFn.deps[i] // Delete the effctFn side effect function in the collection, and it will be recreated when it is executed deps.delete(effectFn) } // The collection containing this side effect function is currently 0 effectFn.deps.length = 0 } function effect(fn) {<!-- --> const effectFn = () => {<!-- --> activeEffect = effectFn //Clear first and then execute, naturally forming a branch switch cleanup(effectFn) fn() } effectFn. deps = [] effectFn() } function track(target, key){<!-- --> // No side effect function returns directly if(!activeEffect) return target[key] // Get the Map of all elements in the current object let depsMap = bucket. get(target) // Create a new one when the current object does not have a Map if(!depsMap) {<!-- --> bucket.set(target, (depsMap = new Map())) } // Get the Set of all side-effect functions of the current element let deps = depsMap. get(key) // If not, create and add a side effect function if(!deps) depsMap.set(key, (deps = new Set())) deps.add(key, activeEffect) // Here, the Map related to the side effect function is recorded activeEffect.deps.push(deps) }
In fact, the idea of branch switching is very simple. It is to delete all the current side-effect functions from the associated elements before the side-effect function is executed, and then execute the side-effect function. If the element is read during execution, the side-effect function will be re-associated. Naturally formed a branch switch.
In this way, our responsive system has improved a lot. In fact, there are still many problems that have not yet been resolved, such as how to execute nested effects, how to schedule side-effect functions, and whether there will be infinite recursion. Explain later