通过effect透析vue3响应式基本原理

396 阅读6分钟

前言

在vue3官网中我们能搜到各种各样的xxxEffect的api,但却没看到关于effect这个api的介绍,这很合理,毕竟effect是底层api,不只是那些xxxEffect是基于effect去实现的,computed也是基于effect去实现的,effect是非常强大的api,它的使用跟watchEffect很像,都是自动去收集依赖的。由于effect是底层核心api,所以揭开它的面纱,那么vue3的响应式就懂得差不多了,那么现在我们结合上文(# 关于vue3中的Reactive)关于reactive的介绍,一步一步揭开effect的面纱,搞懂vue3的响应式机制。

effect

认识effect

effect的使用非常的简单,和watchEffect基本是一样的。

<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, effect } from '../../../node_modules/vue/dist/vue.runtime.esm-browser.js';
    const person = reactive({ name: 'rippi', age: 18 });

    effect(() => {
      app.innerHTML = person.name;
    });
    setTimeout(() => {
      person.name = 'RippiCin';
    }, 2000)
  </script>
</body>

我们随便起一个html文件写下这段代码。

动画.gif

效果如图,在2s后,视图变更。

代码中我们没有像使用watch一样手动写下依赖,而视图也会因为的依赖的改变而改变,那么可以推断出effect是自动收集依赖的,然后依赖变更后执行effect内的函数,那么这个自动收集依赖以及检测依赖变化后自动执行是如何实现的呢?

依赖收集

依赖收集我们需要依赖上文写下的代码,这里就贴一版精简版,去掉无关的判断只留核心代码。

const mutableHandlers = {
  get(target, key, receiver) {
    // 使用proxy的时候要搭配reflect,用来解决this问题
    const res = Reflect.get(target, key, receiver);
    return res;
  },
  set(target, key, value, receiver) {
    const r = Reflect.set(target, key, value, receiver);
    return r;
  }
};

function reactive(target) {
  const proxy = new Proxy(target, mutableHandlers);
  return proxy;
}

ok,上面这点reactive的代码就足够我们去实现effect了。

我们回到上面effect的使用的例子里,effect接受一个执行函数,首次肯定是会调用这个执行函数的,然后页面上呈现了内容,接着2s后改变了person.name这个依赖项,最后页面也做出了变更。

在这么一过程中,各位试想一下,什么时候收集依赖最为合适,我们通过何手段去收集依赖更为合适?

结合reactive的代码,不难猜想到在get方法中做劫持然后收集依赖,毕竟函数执行的时候,必定会执行一个读取变量的过程也就是会读取person.name,只要有读取操作必然后触发get,我们在此执行收集依赖最为合适了。

基于这些,我们在get中增加一个收集依赖的动作。

 get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver);
  // 收集依赖
  track(target, key);
  return res;
},

这里我们推断出了何时收集依赖了,那么我们收集的依赖的数据格式应该如何设计呢?

在我们日常使用vue3的过程,我们会有很多副作用(也就是effect),而这些副作用很可能有着相同的依赖,由此可得依赖数据格式如下:

依赖: [];

又因为每一个副作用都是不同的函数,又得出依赖数据格式如下:

依赖: new Set();

依赖的数据格式右边我们知道了,现在我们来想想左边如何设计,从上面的例子来看,依赖是精确到某一个响应式数据的某一个属性的。如:

  • 例子中person这个响应数据的name。
  • const state = ref(0); 这个个代码中的state的value。
  • 等等...

也就是说我们需要记录依赖的具体属性名。

属性名: new Set();

这样的话还不够,响应式数据那么多,大家很可能都存在同名的属性。所以我们需要知道是哪个响应式数据下的属性才可以,因此我们需要加多一层。

响应式数据: { 属性名: new Set() };

为了更方便的查询以及判断是否存在,Map的has和get是非常好用的,所以我们这里都使用Map格式。

let activeEffect = 需要执行依赖收集的effect;
const targetMap = new WeakMap();
function track(target, key) {
  // 从记录中获取depsMap,看是否存在,不存在就new Map,存在就直接往Map里面加数据
  let depsMap = targetMap.get(target);

  // 不存在,new Map,同时将新Map赋值给depsMap
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  // 看看是否已经记录过这个属性值了(也就是例子中的name属性)
  let dep = depsMap.get(key);

  // 同理,不存在就new Set()
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }

  // 检测下看看是否已经收集过依赖,收集过了就不管了
  const shouldTrack = !dep.has(activeEffect);
  if (shouldTrack) {
    // 添加依赖
    dep.add(activeEffect);
  }
}

这样依赖收集的大体样貌就基本实现了。

上面代码的activeEffect是一个全局的变量,它永远指向当前正在执行依赖收集的那个effect,也就是当effect开始执行的时候,就要将自己赋值给activeEffect。

ReactiveEffect

effect即是ReactiveEffect的实例,这里做个简单介绍看看它都有何属性。这一部分其实和本文关系不太大,如果没兴趣的,只需要知道effect里有个run属性,这个run就是effect接受的那个函数,执行的时候跑的就是这个run,且在run完后会将activeEffect置为undefined。

export class ReactiveEffect {
  // fn就是那个传入的函数,scheduler就是大家在写watch的时候的option
  constructor(private fn, public scheduler?) {}

  // 为了解决嵌套effect而存在的属性
  parent = undefined;

  // 状态,为false的时候是不会收集依赖的
  active = true;

  // 依赖的effect集合,主要用于清理依赖
  deps = [];

  // effect接受的函数在此执行,只不过会处理一些依赖相关的逻辑
  run() {
    try {
      return this.fn();
    } finally {
      activeEffect = undefined;
    }
  }

  // stop,就是watch返回的那个stop函数
  stop() {
  }
}

正如上面说的,这部分其实与本文关系不大,这里面的很多属性都是为了优化和处理一些特殊场景所设,这里还是将这部分写出来是为了表示effect相关的内容还有很多需要处理的,希望感兴趣的朋友可以自行尝试去解决或者调试源码解决。

依赖改变自动执行effect

依赖有所变更的时候,那么就会触发到set,那么我们只需要在set中去执行收集到的effect即可。

set(target, key, value, receiver) {
  const oldValue = target[key];
  const r = Reflect.set(target, key, value, receiver);
  // 相同的值就不必要触发了那些effect了
  if (oldValue !== value) {
    // 触发收集到的effect
    trigger(target, key);
  }
  return r;
}

trigger方法很简单,就是在targetMap里面找到那些effect,然后执行。

function trigger(target, key, newValue, oldValue) {
  // 通过对象找到对应的属性让这个属性对应的effect重新执行
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const dep = depsMap.get(key);
  dep.forEach((effect) => {
    if (effect !== activeEffect) effect.run();
  });
}

在执行effect之前做了个判断,判断当前要执行的effect是否是正在收集依赖的effect,是的话就不执行了,免得死循环了。

结尾

以上便是本文的所有内容了,本文我们以effect这个vue3的底层核心api入手到一步一步地揭开响应式原理(也可称双向绑定)。

最后的最后,希望本文能帮助到各位,另外,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹