likes
comments
collection
share

mini-vue3实现记录 - reactivity

作者站长头像
站长
· 阅读数 4

vue3的模块组成

mini-vue3实现记录 - reactivity

  • vue:即我们平时导入使用的vue
  • compiler-sfc 依赖于 compiler-domcompiler-coretemplate模板转化成对应的render函数,
  • runtime 模块负责执行render函数

effect & reactive & 依赖收集 & 触发依赖

  • 对于一个响应式对象,其内部存在一个容器存放依赖,通过effect函数收集依赖
  • effect函数接收一个参数函数fn,即为依赖,运行fn会触发封装的代理对象proxygetget 除了完成赋值操作,还有进行track依赖收集
  • track函数的作用就是保存依赖。我们需要两个表,一个根据target取得该target所有key即key对应的deps,一个Set根据key取得对应deps,当收集依赖时,我们将当前传入的fnadd到该set即可? 但是我们的入参只有targetkey,如何取得这个fn呢? 我们可以创建一个全局对象activeEffect,由于我们是先执行的fn,再触发track,这意味着我们可以先保存这个fn,再在track中取得。
export function track(target, key) {

  // 我们需要一个容器,存储响应式对象的属性对应的所有依赖,对于target

  // 那么这个对应关系就是: target -> key -> deps

  // 所以我们需要一个Map,存放所有target, 还需要一个表来存放该`target`对应`key`的所有dep

  // 考虑到一个`key`可能有相同依赖,对于`dep`的收集,我们使用Set数据结构

  
  let depsMap = targetMap.get(target);

  // depsMap: `key`为响应式对象的键`key`, `value`为这个`key`对应的依赖

  if (!depsMap) {

    // init

    depsMap = new Map();

    targetMap.set(target, depsMap);
  }

  
  let dep = depsMap.get(key);

  if (!dep) {
    // init
   dep = new Set();
    depsMap.set(key, dep);
  }

  // 我们需要将fn存入,如何取得?
  // 因为我们是先进行fn的执行,所以我们可以创建一个全局对象,在fn执行时使其指向当前的ReactiveEffect对象,然后在track中即可取得
  dep.add(activeEffect);
}
  • 当我们修改响应式对象时,则需要触发依赖触发依赖的逻辑很简单,取出所有dep循环调用即可。
// 触发依赖,根据`target`和`key`去除dep表,遍历执行即可

export function trigger(target, key) {

  let depsMap = targetMap.get(target);

  let dep = depsMap.get(key);
  
  for (const effect of dep) {
    effect.run();
  }
}

runner

runner用于保存传入effectfn函数,当调用effect时,fn会作为返回值返回。

scheduler

scheduler作为effect函数的第二个参数,可以达到以下效果:

  • effect函数第一次执行时,仍会执行fn函数,进行依赖收集
  • 当改变响应式对象的值时,不会触发fn,而是会执行scheduler
  • 由于runner保存了fn,所以可以通过runner调用fn

stop功能

stop函数可以实现将依赖从依赖表中删除的功能,先从单测入手

mini-vue3实现记录 - reactivity 可以看到, 调用stop(runner)后,改变响应式对象的值不会导致dummy的值变化,这是因为依赖已经被删除,而重新执行runner后,由于依赖被重新收集,改变响应式对象的值可以使dummy同步变化。

实现

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity

  • runner.effect保存了当前执行的依赖对象,调用该对象上的stop方法
  • stop方法主要完成两件事:1. 执行cleanupEffect函数,清除当前依赖 2. 如果scheduler对象中有传入回调函数就执行该函数
  • cleanupEffect函数接收一个对象effect,即当前的依赖对象,要在依赖表中删除该对象,我们首先要在依赖对象上绑定依赖表,activeEffect指向当前的依赖对象,创建属性deps数组来保存dep依赖, 在track收集依赖时保存。
  • 我们还可以进行优化:当用户重复调用stop时,我们只执行一次逻辑,在这里我们创建active属性并初始化为true,当stop被调用,将其赋为false防止其再次执行

修复stop上的bug

还是从单测入手

mini-vue3实现记录 - reactivity 如果我们使用obj.prop++,而不是obj.prop = 3这一简单赋值操作的话(只触发set),我们会同时触发setget,因为obj.prop++ => obj.prop = obj.prop + 1,那么这导致的问题就是,调用stop(runner)删除的依赖重新被收集,即stop函数失效。

实现

mini-vue3实现记录 - reactivity

  • 由于是Track的问题,我们可以考虑在Track前增加是否需要收集依赖的逻辑判断
  • 何时不需要收集依赖?1. 只是单纯的访问响应式对象的属性时,即未执行effect时,activeEffect=== undefined; 2. 当已经调用过stop时,即this.active = false时, 当这两者符合任一种,我们在Track前直接return
  • 因此在run中,当当前处于stop状态时,我们直接执行fn而不进行下面的逻辑, 此时Track通道处于关闭状态,而当处于非stop状态时,我们打开Track通道,由于执行_fn会触发get->Track,所以可以正常收集

ref

为什么我们需要ref? 我们知道,对基本数据类型进行响应式处理的时候,我们都会使用ref而不是reactive,这是reactive底层基于的proxy只能代理对象,那如何处理基本数据类型呢?我们仍需要将其转化为一个对象,这就是ref.value的必要性。

实现

从单测入手

mini-vue3实现记录 - reactivity

  • 可以看到,想要实现ref,实际上就是要实现一个只有value这一个keyreactive对象即可
  • 值得注意的是,当第二次为a.value赋值为2时,不会重新触发依赖,因为其与旧值相同,所以我们需要增加新旧值的判断
  • 据此,我们可以创建一个对象,通过对其的键value进行getset的拦截, 适时的进行相关依赖收集和触发依赖即可
class RefImpl {
  private _value: any;
  public dep;
  private _rawValue: any;

  constructor(value) {
    this._rawValue = value;
    this._value = convert(value);
    this.dep = new Set();
  }

  get value() {
    // 依赖收集
    /**
     * 由于`ref`对象只有`value`一个key
     * 所以我们收集时只需要一个Set存储每次的`activeEffect`即可
     * 如果我们不需要进行依赖收集,就直接return this._value即可,否则会存入`activeEffect = undefined`
     */
    trackRefValue(this);
    return this._value;
  }

  set value(newValue) {

    // 如果set的值与原来的值相同,则无需重复触发依赖

    if (hasChanged(newValue, this._rawValue)) {

      this._rawValue = newValue;

      this._value = convert(newValue);

      // 触发依赖
      triggerEffects(this.dep);
    }
  }
}
function trackRefValue(ref) {
  if (isTracking()) {
    trackEffects(ref.dep);
  }
}
  • 我们需要对其依赖收集和触发依赖进行特殊处理,由于该对象只对一个key上的依赖进行处理,所以我们创建私有属性dep进行保存。 然后使用从reactive抽离出来的逻辑进行依赖处理即可
  • 针对get的拦截,我们需要对是否需要进行依赖收集进行判断,否则可能dep存入undefined
  • 针对set的拦截,我们使用变量rawValue对保存转化前的值(因为如果传入对象需要将其进行reactive处理), 和新值进行对比,如果改变了再进行触发依赖

mini-vue3实现记录 - reactivity

  • 当我们传入对象时,我们也需要将其转化为响应式,这里我们直接将传入的value使用reactive包裹使其成为响应式即可。

computed

computed特点在于其的缓存特性,即只有第一次触发getcomputed``依赖的值发生改变时才会进行触发依赖操作。

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity 从单测可得,我们首先要完成的是,通过.value访问到computed对象的值,且再不访问computed对象的值时,我们传入的getter不会被调用。 其次,当依赖的响应式对象值未被改变时,我们在拦截对computed对象的get时直接返回value即可,无需触发依赖, 即无需调用getter。 最后,当依赖的响应式值被改变时,getter需要重新被触发。

import { ReactiveEffect } from "./effect";

class ComputedRefImpl {
  private _getter: any;
  private _dirty: boolean = true;
  private _value: any;
  private _effect: any;
  constructor(getter) {
    this._getter = getter;
    this._effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) this._dirty = true;
    });
  }

  get value() {
    // computed取值时,通过get对其进行一个拦截
    /**
     * 何时需要调用`getter`?
     * 1. 第一次触发`get` 2. 依赖响应式对象的值改变后
     * 如何知道依赖的响应式值发生改变? 通过引入effect
     * 流程:
     * 1. 第一次进入,通过用effect上的`run`函数实现`getter`的调用,完成赋值操作,并关闭调用`getter`的开关,达到缓存效果
     * 2. 当依赖变化时,由于trigger, 我们传入的scheduler被触发,`getter`触发的通道重新被打开
     * 3. 再次访问computed对象,触发get value()拦截,再次调用`getter`完成赋值操作,并关闭调用`getter`的开关。
     */
    if (this._dirty) {
      this._dirty = false;
      this._value = this._effect.run();
    }
    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}
  • 为了达到控制依赖是否被触发的效果,且我们需要知道依赖响应式的值发生改变,我们需要在computed对象内使用effect,而由于我们还需要进行其他特殊处理,这里我们创建ReactiveEffect对象, 通过调用其上的run和传入scheduler达到该效果。
  • 我们需要一个变量控制是否调用getter函数,这里我们使用dirty变量,初始化为true,当其为true时调用getter函数
  • 当第一次触发getdirtytrue,调用getter,将dirty设为false, 之后当依赖的响应式对象值未改变时,由于dirtyfalse,只会直接返回value
  • 当依赖的响应式值发生改变, computed对象上的ReactiveEffect对象触发trigger, 由于传入scheduler,不会执行getter,而是执行scheduler,然后将dirty设为true,当下次computed对象get被触发时,会再次执行getter

补充一个超级破产版vue3响应式实现, 实现了reactive和依赖收集、触发依赖部分.

    function reactive(raw) {
      return new Proxy(raw, {
        get(target, key) {
          const res = Reflect.get(target, key);
          // TODO: 依赖收集
          track(target, key);
          return res;
        },

        set(target, key, value) {
          const res = Reflect.set(target, key, value);
          //TODO: 收集依赖
          trigger(target, key);
          return res;
        },
      });
    }
    const targetMap = new Map();
    // 当前执行的fn,fn执行 => 触发响应式对象的`get` => 依赖收集track
    let activeFn = null;
    function effect(e) {
      activeFn = e;
      e();
    }

    function track(target, key) {
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }
      let deps = depsMap.get(key);
      if (!deps) {
        deps = new Set();
        depsMap.set(key, deps);
      }
      // 存入deps
      if (activeFn) deps.add(activeFn);
    }

    function trigger(target, key) {
      const depsMap = targetMap.get(target);
      const deps = depsMap.get(key);
      for (const effect of deps) {
        effect();
        // console.log(effect);
      }
    }

    const foo = { obj: 1 };
    const wrapFoo = reactive(foo);
    console.log("wrapFoo.obj: " + wrapFoo.obj);

    let nextObj = null;
    effect(() => {
      nextObj = wrapFoo.obj;
    });
    console.log(nextObj); // 1
    wrapFoo.obj = 2;
    console.log(nextObj); // 2
    wrapFoo.obj++;
    console.log(nextObj); // 3
转载自:https://juejin.cn/post/7211713478178930725
评论
请登录