Vue3响应式系统

285 阅读5分钟

前言

Vue 最独特的特性之一,是其非侵入性的响应式系统,这篇文章将由浅入深的一步步探索一下 Vue 的响应式系统到底是如何实现的。

思考

假如有一个数据状态,被其他模块(例如一个函数)所引用,当数据状态发生了改变,我们也希望引入该数据的模块里面也能得到响应,根据数据状态的变化进行重新计算,例如下面这个例子:

cosnt data = {
    counter: 100
}

function double() {
    const res = data.counter * 2
    console.log(res)
}

// 数据发生变化,重新执行函数
data.counter++
double()

上面的例子中,double函数里面使用了data.counter这个数据,当data.counter发生变化之后,我们想要double函数重新计算,那我们就必须要重新执行一遍double函数。

但是这样有个很大的问题,如果有很多的模块都应用了这个数据,那么当数据发生变更,所有的模块都要做更新操作,如上的例子就是,所有的函数都需要重新执行一遍。

那有什么办法能够更高效的去完成这个操作呢?

优化一:统一通知更新

我们希望当有很多模块依赖于某个数据状态,当该数据发生变化的时候,我们只需要做一次通知操作,所有的模块就都能自动的完成更新操作,代码如下:

class Dep {
    watchers: Set<Function>;

    constructor() {
      this.watchers = new Set();
    }

    addEffect(effect: Function) {
      this.watchers.add(effect);
    }

    notify() {
      this.watchers.forEach((effect) => {
        effect && effect();
      });
    }
  }

  const dep = new Dep();

  const data = {
    counter: 100,
  };

  const double = () => {
    const double = data.counter * 2;
    console.log(double);
  };

  const plus10 = () => {
    const plus10 = data.counter + 10;
    console.log(plus10);
  };

  dep.addEffect(double);
  dep.addEffect(plus10);

  data.counter++;
  dep.notify();

如上,doubleplus10两个副作用函数都依赖data.counter数据,当data.counter发生变化的时候,调用dep.notify()就可以进行统一通知更新了。

以上代码虽然实现了统一更新,但是多个模块引用数据,我们仍然需要一个一个的将这些模块手动的添加到副作用列表,我们继续优化。

优化二:自动收集依赖

let activeEffect: Function = null
const watchEffect = (effect: Function) => {
    activeEffect = effect
    dep.depend()
    effect()
    activeEffect = null
}

Dep类中新增depend方法添加副作用函数


...
depend(activeEffect) {
    this.addEffect(activeEffect)
}
...

这样我们就不用再去手动的一个个的添加副作用函数,而是将副作用函数通过watchEffect函数进行包裹,完整代码如下:

  class Dep {
    watchers: Set<Function>;

    constructor() {
      this.watchers = new Set();
    }

    addEffect(effect: Function) {
      this.watchers.add(effect);
    }

    depend() {
      this.addEffect(activeEffect);
    }

    notify() {
      this.watchers.forEach((effect) => {
        effect();
      });
    }
  }

  let activeEffect: Function = null;
  const watchEffect = (effect: Function) => {
    // 通过watchEffect包裹的函数赋值给activeEffect
    activeEffect = effect;
    // 然后将该函数添加到副作用列表
    dep.depend();
    // 被包裹的函数默认会执行一次
    effect();
    // 重置
    activeEffect = null;
  };

  const dep = new Dep();

  const data = {
    counter: 1,
  };

  watchEffect(function () {
    const double = data.counter * 2;
    console.log(double);
  });

  watchEffect(function () {
    const plus10 = data.counter + 10;
    console.log(plus10);
  });

  data.counter++;

  dep.notify();

到这里,我们再看还有哪些问题,我们设想,当 data 中还有其他属性,比如data.name = 'zhangsan',部分模块依赖于data.counter,部分模块依赖data.name,按照当前已经实现的代码,如果只有data.counter发生了变化,所有的副作用函数都会重新执行。造成这个问题的关键在于,目前只定义了一个dep实例,所有的数据共用同一个实例,当调用dep.notify()的时候,会通知到所有的副作用函数,继续优化。

优化三:依赖项相互独立

先上完整代码(部分关键说明见代码注释):


  interface Target {
    [key: string]: any;
  }

  class Dep {
    watchers: Set<Function>;

    constructor() {
      this.watchers = new Set();
    }

    addEffect(effect: Function) {
      this.watchers.add(effect);
    }

    depend() {
      this.addEffect(activeEffect);
    }

    notify() {
      this.watchers.forEach((effect) => {
        effect && effect();
      });
    }
  }

  let activeEffect: Function = null;
  const watchEffect = (effect: Function) => {
    activeEffect = effect;
    // effect执行,effect函数体代码获取数据,就会触发属性get操作
    effect();
    activeEffect = null;
  };

  const targetMap = new WeakMap();
  function getDep(target: Object, key: string | symbol): Dep {
    // 1 根据对象(target)取出对应的Map对象
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }

    // 2 取出对应的dep对象
    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Dep();
      depsMap.set(key, dep);
    }

    return dep;
  }

  // 对数据进行劫持
  function reactive<T extends object>(target: T): T;
  function reactive(raw: Target) {
    return new Proxy(raw, {
      get(target: Target, key: string) {
        // 当获取数据的时候就会自动添加依赖
        const dep = getDep(target, key);
        dep.depend();
        return target[key];
      },
      set(target: Target, key: string, newValue: unknown) {
        // 当对数据进行修改的时候,获取数据对应的dep,然后进行针对性的通知
        const dep = getDep(target, key);
        target[key] = newValue;
        dep.notify();
        return true;
      },
    });
  }

  // 测试
  const data1 = reactive({ counter: 1, name: "zhangsan" });
  const data2 = reactive({ age: 18 });

  // watchEffect1
  watchEffect(function () {
    console.log("watchEffect1-------", data1.counter * 2, data1.name);
  });

  // watchEffect2
  watchEffect(function () {
    console.log("watchEffect2-------", data1.counter * data1.counter);
  });

  // watchEffect3
  watchEffect(function () {
    console.log("watchEffect3-------", data1.counter + 1, data1.name);
  });

  // watchEffect4
  watchEffect(function () {
    console.log("watchEffect4-------", data2.age);
  });

  data2.age = 20;

以上代码主要实现了两个函数的封装,getDep函数和reactive函数,对于独立的dep实例的存储,,我们使用WeakMap数据结构,一个原因是WeakMap数据结构的键是一个对象,另一个就是WeakMap数据结构对对象的引用是弱引用,对于内存回收友好。这样我们就能够通过对象及相应的键名方便的找到对应的dep实例。

getDep函数做的事情主要就是根据目标对象及键名,获取对应的dep实例,实例没有创建就创建,已经创建了就直接获取。

reactive函数做的事情主要是对数据进行劫持,获取数据时自动添加依赖,修改数据时自动的进行通知。

到这里就实现了对于数据依赖进行精准的自动收集,当数据发生变化时,自动的进行更新,不过还只是实现了最简单的对象,如果有嵌套的情况,还需要通过递归实现深层次监听。