Home  >  Article  >  Web Front-end  >  How to handwrite Vue3 responsive system

How to handwrite Vue3 responsive system

WBOY
WBOYforward
2023-05-14 09:40:05945browse

Responsiveness

First of all, what is responsiveness?

Responsiveness means doing a series of linkage processing when the observed data changes. Just like a hot social event, when there is an update, all media will follow up and make relevant reports. Here social hot events are the targets to be observed. So in the front-end framework, what is the observed target? Obviously, it's the state. There are generally multiple states, which are organized by objects. Therefore, we can observe the changes of each key of the state object and perform a series of processes in conjunction.

We need to maintain such a data structure:

How to handwrite Vue3 responsive system

Each key of the state object has an associated series of effect side effects functions , that is, the logic of linkage execution when changes are organized through Set.

Each key is associated with a series of effect functions, and multiple keys can be maintained in a Map.

This Map exists when the object exists. When the object is destroyed, it will also be destroyed. (Because the objects are gone, there is no need to maintain the effects associated with each key)

And WeakMap has just such a feature. The key of WeakMap must be an object, and the value can be any data. The key When the object is destroyed, the value will also be destroyed.

So, the responsive Map will be saved using WeakMap, and the key is the original object.

This data structure is the core data structure of responsiveness.

For example, such a status object:

const obj = {
    a: 1,
    b: 2
}

Its responsive data structure is like this:

const depsMap = new Map();
const aDeps = new Set();
depsMap.set('a', aDeps);
const bDeps = new Set();
depsMap.set('b', bDeps);
const reactiveMap = new WeakMap()
reactiveMap.set(obj, depsMap);

The created data structure is the one in the picture:

How to handwrite Vue3 responsive system

How to handwrite Vue3 responsive system

##Then add deps dependency, for example, a function depends on a , then it must be added to the deps collection of a:

effect(() => {
    console.log(obj.a);
});

That is:

const depsMap = reactiveMap.get(obj);
const aDeps = depsMap.get('a');
aDeps.add(该函数);

There is no problem in maintaining the deps function in this way, but do we want users to Do you need to add deps manually? Not only will that invade the business code, but it's also easy to miss.

So we will definitely not let users maintain deps manually, but will do automatic dependency collection. So how to automatically collect dependencies? When reading the status value, a dependency relationship with the status is established, so it is easy to think of the get method that can be used to proxy the status. Either through Object.defineProperty or Proxy:

const data = {
    a: 1,
    b: 2
}
let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
    get(targetObj, key) {
        let depsMap = reactiveMap.get(targetObj);
        
        if (!depsMap) {
          reactiveMap.set(targetObj, (depsMap = new Map()))
        }
        
        let deps = depsMap.get(key)
        
        if (!deps) {
          depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)

        return targetObj[key]
   }
})

effect will execute the incoming callback function fn. When you read obj.a in fn, get will be triggered and the response of the object will be obtained. Formula Map, take the deps set corresponding to a from it, and add the current effect function to it.

This completes a dependency collection.

When you modify obj.a, you need to notify all deps, so you also need to proxy set:

set(targetObj, key, newVal) {
    targetObj[key] = newVal
    const depsMap = reactiveMap.get(targetObj)
    if (!depsMap) return
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => fn())
}

The basic responsiveness is completed. Let’s test it:

How to handwrite Vue3 responsive system

is printed twice, the first time is 1 and the second time is 3. The effect will first execute the incoming callback function and trigger get to collect the dependencies. At this time, the printed obj.a is 1. Then when obj.a is assigned a value of 3, set will be triggered to execute the collected dependencies. At this time, obj. a is 3

The dependencies are also collected correctly:

How to handwrite Vue3 responsive system

The result is correct, we have completed the basic responsiveness! Of course, responsiveness doesn’t just have this little code. Our current implementation is not perfect and there are still some problems. For example,

If there is a branch switch in the code, the last execution will depend on obj.b, but the next execution will not depend on it. Is there an invalid dependency at this time?

Such a piece of code:

const obj = {
    a: 1,
    b: 2
}
effect(() => {
    console.log(obj.a ? obj.b : 'nothing');
});
obj.a = undefined;
obj.b = 3;

The first time the effect function is executed, obj.a is 1. At this time, it will go to the first branch, and then Depends on obj.b. Modify obj.a to undefined, trigger set, and execute all dependent functions. At this time, we go to branch two and no longer rely on obj.b.

Change obj.b to 3. It stands to reason that there is no function that depends on b at this time. Let’s try it out:

How to handwrite Vue3 responsive system

It is correct to print 2 for the first time, that is, it has reached the first branch, and print obj.b

It is also correct to print nothing for the second time, and it has reached the second branch at this time. But printing nothing for the third time is wrong, because at this time obj.b no longer has dependent functions, but it is still printed.

Print and look at the deps, you will find that the deps of obj.b have not been cleared

How to handwrite Vue3 responsive system

所以解决方案就是每次添加依赖前清空下上次的 deps。怎么清空某个函数关联的所有 deps 呢?记录下就好了。

我们改造下现有的 effect 函数:

let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}

记录下这个 effect 函数被放到了哪些 deps 集合里。也就是:

let activeEffect
function effect(fn) {
  const effectFn = () => {
      activeEffect = effectFn
      fn()
  }
  effectFn.deps = []
  effectFn()
}

对之前的 fn 包一层,在函数上添加个 deps 数组来记录被添加到哪些依赖集合里。

get 收集依赖的时候,也记录一份到这里:

How to handwrite Vue3 responsive system

这样下次再执行这个 effect 函数的时候,就可以把这个 effect 函数从上次添加到的依赖集合里删掉:

How to handwrite Vue3 responsive system

cleanup 实现如下:

function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}

effectFn.deps 数组记录了被添加到的 deps 集合,从中删掉自己。全删完之后就把上次记录的 deps 数组置空。

我们再来测试下:

How to handwrite Vue3 responsive system

无限循环打印了,什么鬼?

问题出现在这里:

How to handwrite Vue3 responsive system

set 的时候会执行所有的当前 key 的 deps 集合里的 effect 函数。

而我们执行 effect 函数之前会把它从之前的 deps 集合中清掉:

How to handwrite Vue3 responsive system

执行的时候又被添加到了 deps 集合。这样 delete 又 add,delete 又 add,所以就无限循环了。

解决的方式就是创建第二个 Set,只用于遍历:

How to handwrite Vue3 responsive system

这样就不会无限循环了。

再测试一次:

How to handwrite Vue3 responsive system

现在当 obj.a 赋值为 undefined 之后,再次执行 effect 函数,obj.b 的 deps 集合就被清空了,所以需改 obj.b 也不会打印啥。

看下现在的响应式数据结构:

How to handwrite Vue3 responsive system

确实,b 的 deps 集合被清空了。那现在的响应式实现是完善的了么?也不是,还有一个问题:

如果 effect 嵌套了,那依赖还能正确的收集么?

首先讲下为什么要支持 effect 嵌套,因为组件是可以嵌套的,而且组件里会写 effect,那也就是 effect 嵌套了,所以必须支持嵌套。

我们嵌套下试试:

effect(() => {
    console.log(&#39;effect1&#39;);
    effect(() => {
        console.log(&#39;effect2&#39;);
        obj.b;
    });
    obj.a;
});
obj.a = 3;

按理说会打印一次 effect1、一次 effect2,这是最开始的那次执行。然后 obj.a 修改为 3 后,会触发一次 effect1 的打印,执行内层 effect,又触发一次 effect2 的打印。也就是会打印 effect1、effect2、effect1、effect2。

我们测试下:

How to handwrite Vue3 responsive system

打印了 effect1、effet2 这是对的,但第三次打印的是 effect2,这说明 obj.a 修改后并没有执行外层函数,而是执行的内层函数。为什么呢?

看下这段代码:

How to handwrite Vue3 responsive system

我们执行 effect 的时候,会把它赋值给一个全局变量 activeEffect,然后后面收集依赖就用的这个。

当嵌套 effect 的时候,内层函数执行后会修改 activeEffect 这样收集到的依赖就不对了。

怎么办呢?嵌套的话加一个栈来记录 effect 不就行了?

也就是这样:

How to handwrite Vue3 responsive system

执行 effect 函数前把当前 effectFn 入栈,执行完以后出栈,修改 activeEffect 为栈顶的 effectFn。

这样就保证了收集到的依赖是正确的。

这种思想的应用还是很多的,需要保存和恢复上下文的时候,都是这样加一个栈。

我们再测试一下:

How to handwrite Vue3 responsive system

现在的打印就对了。至此,我们的响应式系统就算比较完善了。

全部代码如下:

const data = {
    a: 1,
    b: 2
}
let activeEffect
const effectStack = [];
function effect(fn) {
  const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      effectStack.push(effectFn);
      fn()
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
  }
  effectFn.deps = []
  effectFn()
}
function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
    get(targetObj, key) {
        let depsMap = reactiveMap.get(targetObj)
        
        if (!depsMap) {
          reactiveMap.set(targetObj, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
          depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)
        activeEffect.deps.push(deps);
        return targetObj[key]
   },
   set(targetObj, key, newVal) {
        targetObj[key] = newVal
        const depsMap = reactiveMap.get(targetObj)
        if (!depsMap) return
        const effects = depsMap.get(key)
        // effects && effects.forEach(fn => fn())
        const effectsToRun = new Set(effects);
        effectsToRun.forEach(effectFn => effectFn());
    }
})

总结

响应式就是数据变化的时候做一系列联动的处理。

核心是这样一个数据结构:

How to handwrite Vue3 responsive system

最外层是 WeakMap,key 为对象,value 为响应式的 Map。这样当对象销毁时,Map 也会销毁。Map 里保存了每个 key 的依赖集合,用 Set 组织。

我们通过 Proxy 来完成自动的依赖收集,也就是添加 effect 到对应 key 的 deps 的集合里。 set 的时候触发所有的 effect 函数执行。

这就是基本的响应式系统。

但是还不够完善,每次执行 effect 前要从上次添加到的 deps 集合中删掉它,然后重新收集依赖。这样可以避免因为分支切换产生的无效依赖。并且执行 deps 中的 effect 前要创建一个新的 Set 来执行,避免 add、delete 循环起来。此外,为了支持嵌套 effect,需要在执行 effect 之前把它推到栈里,然后执行完出栈。解决了这几个问题之后,就是一个完善的 Vue 响应式系统了。当然,现在虽然功能是完善的,但是没有实现 computed、watch 等功能,之后再实现。

最后,再来看一下这个数据结构,理解了它就理解了 vue 响应式的核心:

How to handwrite Vue3 responsive system

The above is the detailed content of How to handwrite Vue3 responsive system. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete