创建一个完善的vue3响应式系统

998 阅读5分钟

vue重要的特性就是响应式,这次使用ES6proxy特性,手动实现一个响应式,深入理解vue3响应式原理

副作用函数

  • 副作用函数: 是指会产生副作用的函数,直接或间接的影响外部参数产生变化
var num = 1
function fns() {
    num = 2     // 修改全局变量,产生副作用
}
fns()
console.log(num)    // 2

实现一个简单的响应式功能

     const obj = { text: "hello world" };
    function effect() {
      document.body.innerHTML = obj.text
    }
    effect()
    obj.text = 'hello vue3'
  • 如上述代码,当副作用函数执行后,页面显示hello world,如果再次给obj.data重新赋值,不能触发副作用函数直接修改,没有响应式的执行。

如果想要设置成响应式,就要监听它的读取和设置操作

1.当读取时,会把当前的副作用函数存储在一个'桶'中 Snipaste_2023-05-27_10-42-29.png 2.在重新赋值操作中,会将存储的副作用函数取出再重新执行 Snipaste_2023-05-27_14-42-10.pngvue2中,实现响应式的是ES5的Object.defineProperty而在vue3中,实现响应式的是ES6的代理对象proxy来实现

基本思路:

  // 存储副作用函数的桶
    const bucket = new Set();
    // 原始数据
    const data = { text: "hello world" };
    // 对原始数据的代理
    const obj = new Proxy(data, {
      // 拦截读取操作
      get(target, key) {
        // 将副作用函数 effect 添加到存储副作用函数的桶中
        bucket.add(effect);
        // 返回属性值
        return target[key];
      },
      // 拦截设置操作
      set(target, key, newVal) {
        // 设置属性值
        target[key] = newVal;
        // 把副作用函数从桶里取出并执行
        bucket.forEach((fn) => fn());
        // 返回 true 代表设置操作成功
        return true;
      },
    });
  • 使用一个定时器可以验证当重新改变参数时,会再次触发副作用函数执行
   function effect() {
      document.body.innerHTML = obj.text
    }
    effect()
    setTimeout(() => {
      obj.text = 'hello vue3'
    },1000)

一个完善的响应式系统

现在一个基本响应式系统创建好了,但是还有一些问题:

  • 当前的副作用函数写死了,如果副作用函数不是'effect'时,就不能正确实现响应式
    • 解决方法:提供一个用来注册副作用函数的机制
      // 用一个全局变量存储被注册的副作用函数
      let activeEffect;
      // effect 函数用于注册副作用函数
      function effect(fn) {
        activeEffect = fn;
        // 执行副作用函数
        fn();
      }
      
       //   参数是一个匿名的副作用函数
      effect(() => {
        document.body.innerHTML = obj.text;
      });

修改代理对象中的get的存储方式

   get(target, key) {
    // bucket.add(effect);    之前的
    // 将 activeEffect 中存储的副作用函数收集到“桶”中
    if (activeEffect) { // 新增
    bucket.add(activeEffect) // 新增
    } // 新增
    return target[key]
    }
  • 如果定时器中添加一个不存在的参数,并且没有与副作用函数建立联系,但还是会触发了响应式监听
    • 根本原因:没有在副作用函数与被操作的字段之间建立明确的关系,无论是读取的哪一个属性,都会当前桶中存储的副作用函数取出并执行
 effect(() => {
        console.log('执行副作用函数');        // 测试可以会执行两次
        document.body.innerHTML = obj.text;
      });

      setTimeout(() => {
        // obj.text = "heeeeeeeeee";
        obj.noText = '不存在'
      }, 1000);
  • 解决方法:副作用函数与被被操作的字段之间建立联系,将桶改成Map,将参数和副作用函数挂钩起来

基本思路:

// 创建一个新桶来存储副作用函数,包含key和value
      const bucket = new WeakMap();
      const obj = new Proxy(data, {
        get(target, key) {    // target:当前对象,key:触发监听的key
          // 没有正在执行的副作用函数 直接返回
          if (!activeEffect) return target[key];
          // 从这个桶中取出一个Map类型(key -> value)
          let depsMap = bucket.get(target);
          // 不存在,则创建一个Map与target关联
          if (!depsMap) {
            bucket.set(target, (depsMap = new Map()));
          }
          // 根据key判断每个key上是否存在对应的副作用函数
          let deps = depsMap.get(key);
          // 不存在,则新建一个new Set,并与key关联
          if (!deps) {
            depsMap.set(key, (deps = new Set()));
          }
          // 最后将当前激活的副作用函数添加到桶中
          deps.add(activeEffect);
          return target[key];

          //  将 activeEffect 中存储的副作用函数收集到“桶”中
          //  if(activeEffect) {
          //     bucket.add(activeEffect)
          //  }
        },

        set(target, key, newVal) {
          target[key] = newVal;
          // 根据target从桶中取得depsMap,它是key --> effects
          const depsMap = bucket.get(target)
          if(!depsMap) return
          // 根据key取得当前对应的副作用函数
          const effects = depsMap.get(key)
          // 执行副作用函数
          effects && effects.forEach(fn => fn())
          
          // // 把副作用函数从桶里取出并执行
          // bucket.forEach((fn) => fn());
          // // 返回 true 代表设置操作成功
          // return true;
        },
      });

weakMapMapSet的关系图

Snipaste_2023-05-28_14-44-34.png 完整代码:可以将get和set中的逻辑封装在track和trigger函数中,极大的提高灵活性

  const data = { text: "hello world" };
      // 用一个全局变量存储被注册的副作用函数
      let activeEffect;
      // 创建一个新桶来存储副作用函数,包含key和value
      const bucket = new WeakMap();
      const obj = new Proxy(data, {
        get(target, key) {    // target:当前对象,key:触发监听的key
          track(target, key)
          return target[key];
        },

        set(target, key, newVal) {
          target[key] = newVal;
          trigger(target, key)
        },
      });
           // effect 函数用于注册副作用函数
      function effect(fn) {
        activeEffect = fn;
        // 执行副作用函数
        fn();
      }
      //   参数是一个匿名的副作用函数
      effect(() => {
        console.log("执行副作用函数");
        document.body.innerHTML = obj.text;
      });

      setTimeout(() => {
        obj.text = 'hello vue3'
        // obj.noText = "不存在";
      }, 1000);
      
      // track函数
      function track(target, key) {
         // 没有正在执行的副作用函数 直接返回
         if (!activeEffect) return target[key];
          // 从这个桶中取出一个Map类型(key -> value)
          let depsMap = bucket.get(target);
          // 不存在,则创建一个Map与target关联
          if (!depsMap) {
            bucket.set(target, (depsMap = new Map()));
          }
          // 根据key判断每个key上是否存在对应的副作用函数
          let deps = depsMap.get(key);
          // 不存在,则新建一个new Set,并与key关联
          if (!deps) {
            depsMap.set(key, (deps = new Set()));
          }
          // 最后将当前激活的副作用函数添加到桶中
          deps.add(activeEffect);
      }

      // trigger函数
      function trigger(target, key) {
       
          // 根据target从桶中取得depsMap,它是key --> effects
          const depsMap = bucket.get(target)
          if(!depsMap) return
          // 根据key取得当前对应的副作用函数
          const effects = depsMap.get(key)
          // 执行副作用函数
          effects && effects.forEach(fn => fn())
      }