简易 Vue3 响应式原理

本文最后更新于:2022年4月22日 上午

手写简易 Vue3 响应式原理

我们知道vue 3 的响应式撅弃了 Object.definePropertie()采用了 es6 新增的 proxy

当我们知道上述前提后,想象一下,当我们修改一个对象中的属性时,要如何做到,可以通知到使用方这个属性已经发生变更了呢?

因为我们一个应用程序可能会有非常多个对象,每个对象都有自己的属性,当属性发生变化的时候,需要通知所有用到该属性的地方,那么这个数据结构该怎么设计呢?

先决条件

我们可以如下设计

const targetMap = new WeapMap();

const obj = { name: 'abc', age: 20 }
const depMap = new Map();
depMap.set('name',[fn1, fn2]); // 所有依赖 name 的

targetMap.set(obj, depMap);

解释一下上述的操作是什么意思

创建一个 WeapMap 对象 targetMap,用来存储所有被监听的对象

key 就是 被监听的对象,value 则是 一个 map,这个mapobj 的所有依赖

    这个 mapkey 就是被监听的对象的 key

    value 就是一个个依赖了 obj.name 的方法,如果 name 发生改变后就调用里面的方法以通知更新

    

那当我们需要通知的时候如下操作就可以非常方便的通知了

  1. obj.name 发生改变的时候,通过 obj 取到 obj 所有的 depend

  2. 再从 objdepend中取到 namedepend

  3. 遍历所有依赖了 namefunction,并执行

obj.name = 'js';

const depMap = targetMap(obj);
const dep = depMap('name');

dep.forEach(fn => fn());

通过以上的设计,就可以配合 proxy设计出一个简单的响应式系统了

实现

reactive

function reactive(obj) {
  return new Proxy(obj, {
    get() {
      // 第一次执行时先获取一下依赖并执行
      const depend = getDepend(...arguments);
      depend.depend();
      return Reflect.get(...arguments);
    },
    set() {
      // 当数据发生改变时拿到 这个 depend 调用 notice 来进行通知
      const depend = getDepend(...arguments);
      depend.notice();
      Reflect.set(...arguments);
    },
  });
}

Depend

Depend 是具体某一个属性的依赖对象,里面主要有以下三个属性

  • depends:保存当前所有属性的 set,类似数组结构,使用 set 是为了去重

  • depend():添加依赖

  • notice():通知/广播

class Depend {
  constructor() {
    this.depends = new Set();
  }

  // 通知
  notice() {
    this.depends.forEach((cb) => cb());
  }

  // 收集依赖
  depend() {
    // 此处需要做非空检验,否则,当 notice 通知的时候会再次收集依赖的时候会 拿到 null,且陷入死循环
    currentFunc && this.depends.add(currentFunc);
  }
}

watch

该函数接收一个函数参数,在这里函数里面使用了响应式变量的话,会被自动收集依赖

watch 接收到 func 的时候,会将它先暂存在currentFunc,然后执行 这个函数,此时就可以触发 proxy 中的 get,在 get中我们就可以收集依赖了

let currentFunc = null;
function watch(func) {
  currentFunc = func;
  currentFunc();

  // 调用完成置空
  currentFunc = null;
}

reactive

reactive 会将一个普通对象变成一个响应式对象,如 const obj = reacive({ name: 'abc', age: 18 }),那么这个 obj 就变成了一个响应式对象

reactive 会通过 Proxy 返回一个响应式对象

  • get:会拿到当前属性的 depend,然后收集依赖

  • set:会拿到当前属性的 depend,然后发出通知

function reactive(obj) {
  return new Proxy(obj, {
    get() {
      // 第一次执行时先获取一下依赖并执行
      const depend = getDepend(...arguments);
      depend.depend();
      return Reflect.get(...arguments);
    },
    set() {
      // 当数据发生改变时拿到 这个 depend 调用 notice 来进行通知
      const depend = getDepend(...arguments);
      depend.notice();
      Reflect.set(...arguments);
    },
  });
}

完整示例

let currentFunc = null; // 当前监听的函数

class Depend {
  constructor() {
    this.depends = new Set();
  }

  // 通知
  notice() {
    this.depends.forEach((cb) => cb());
  }

  // 收集依赖
  depend() {
    // 此处需要做非空检验,否则,当 notice 通知的时候会再次收集依赖的时候会 拿到 null,且陷入死循环
    currentFunc && this.depends.add(currentFunc);
  }
}

const targetMap = new WeakMap();

/**
 * 获取 dep
 * @param {*} target
 * @param {*} key
 * @returns
 */
function getDepend(target, key) {
  // 1. 从全局中获取到当前对象的 map,如果没有则设置一个空的 map
  let depMap = targetMap.get(target);

  if (!depMap) {
    depMap = new Map();
    targetMap.set(target, depMap);
  }

  // 2. 从当前的 map 中取到当前 key 的 depend,如果没有的话则在 map 中添加一个新的空 map
  let dep = depMap.get(key);
  if (!dep) {
    dep = new Depend();
    depMap.set(key, dep);
  }

  return dep;
}

function reactive(obj) {
  return new Proxy(obj, {
    get() {
      // 第一次执行时先获取一下依赖并执行
      const depend = getDepend(...arguments);
      depend.depend();
      return Reflect.get(...arguments);
    },
    set() {
      // 当数据发生改变时拿到 这个 depend 调用 notice 来进行通知
      const depend = getDepend(...arguments);
      depend.notice();
      Reflect.set(...arguments);
    },
  });
}

// 将当前需要监听的函数暂存并且首次先执行一次,以添加监听
function watch(func) {
  currentFunc = func;
  currentFunc();

  // 调用完成置空
  currentFunc = null;
}

const obj = {
  name: 'lfm',
  age: 24,
};

const objProxy = reactive(obj);

watch(function () {
  console.log('我监听了name', objProxy.name);
  console.log('我还监听了name', objProxy.age);
});

console.log('^^^^^^^^^^^^^^^^^ 分割线 ^^^^^^^^^^^^^^^^^^^');

objProxy.name = 'aaa';
objProxy.age = 123;

输出

可以看到第一次我们会先执行一次

然后我们修改了 name 的时候触发了一次

修改了 age 的时候也触发了一次

$ node index.js
我监听了name lfm
我还监听了name 24
^^^^^^^^^^^^^^^^^ 分割线 ^^^^^^^^^^^^^^^^^^^
我监听了name lfm
我还监听了name 24
我监听了name aaa
我还监听了name 24

总结

基于以上,我们实现了一个简易的 响应式系统,可以在数据发生改变的时候可以给各个使用到了该属性的地方发出通知