vue2 - vue3响应式原理代码的实现

995 阅读5分钟

一、什么是响应式?

  • 有一个初始化的值,有一段代码使用了这个值;
  • 在有一个新的值时,这段代码可以自动重新执行;
    let initial = 10;
    
    //使用了这个值
    console.log(10);
    //有一个新的值
    console.log(initial * 2);
    
    //可以自动重新执行
    initial = 20;
    
  • 这样一种可以自动响应数据变量的代码机制,我们就称之为是响应式的;

1.1. 对象的响应式

  • 现在有一个响应式对象;
    const obj = {
      name: 'yzh',
      age: 18
    };
    
  • 当对象里的数据发生变更时,需要执行上百行代码:
    const newName = obj.name;
    console.log("zheshiyiduanmiaoshu");
    console.log("Hello World");
    console.log(obj.name) // 100行
    
    obj.name = "luffy";
    

二、响应式函数设计

  • 执行的代码中可能不止一行代码,所以可以将这些代码放到一个函数中;
  • 当数据发生变化时,自动去执行某一个函数;

2.1. 响应式函数的实现

  • 1)封装一个新的函数watchFn;
  • 2)传入到watchFn的函数,就是需要响应式的;
  • 3)默认定义的函数都是不需要响应式的;
    const reactiveFns = [];
    
    //收集需要响应的函数
    function watchFn(fn) {
      reactiveFns.push(fn)
    };
    
    const obj = {
      name: "yzh",
      age: 18
    };
    
    //依赖响应式对象的函数
    watchFn(function() {
      const newName = obj.name;
      console.log("zheshiyiduanmiaoshu");
      console.log("Hello World");
      console.log("Hello World1");
      console.log("Hello World2");
      console.log("Hello World3");
      console.log("Hello World4");
      console.log(obj.name) // 100行
    });
    
    //依赖响应式对象的函数
    watchFn(function() {
      console.log('watchFn: ', obj.name);
    });
    
    function foo() {
      console.log('普通函数/默认定义的函数');
    };
    
    obj.name = 'ace';
    reactiveFns.forEach(fn => {
      fn()
    });
    

三、响应式依赖的收集

  • 目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
    • 在实际开发中需要监听很多对象的响应式;
    • 这些对象需要监听的不只是一个属性。它们很多属性的变化,都会有对应的响应式函数;
    • 所以不可能在全局维护一大堆的数组来保存这些响应函数;
  • 设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数;
  • 相当于替代了原来的简单 reactiveFns 的数组;
    class Depend {
      constructor() {
        this.reactiveFns= []
      }
    
      addDepend(reactiveFn) {
        this.reactiveFns.push(reactiveFn)
      }
    
      notify() {
        this.reactiveFns.forEach(fn => {
          fn()
        })
      }
    }
    
    const depend = new Depend();
    
    function watchFn(fn) {
      depend.addDepend(fn)
    }
    
    const obj = {
      name: "yzh",
      age: 18
    };
    
    watchFn(function() {
      const newName = obj.name;
      console.log("zheshiyiduanmiaoshu");
      console.log('需要响应的代码块1');
      console.log(obj.name) // 100行
    });
    
    watchFn(function() {
      console.log('watchFn: ', obj.name);
    });
    
    function foo() {
      console.log('普通函数/默认定义的函数');
    };
    
    obj.name = 'ace';
    depend.notify();
    

四、监听对象的变化

  • 两种方式:
  • 这里使用Proxy的方式来自动监听对象变化:
    //原来
    //obj.name = 'ace';
    //depend.notify();
    
    //更改为:
    const objProxy = new Proxy(obj, {
      get(target, key, receiver) {
        return Reflect.get(target, key, receiver);
      },
    
      set(target, key, newVal, receiver) {
        Reflect.set(target, key, newVal, receiver);
    
        depend.notify();
      }
    });
    
    objProxy.name = 'ace';
    

五、对象的依赖管理

  • 目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数;
  • 通过WeakMap来管理不同对象的不同依赖关系;

5.1. 对象依赖管理的实现

  • 监听对象的变化代码重构
    const targetMap = new WeakMap();
    
    const objProxy = new Proxy(obj, {
      get(target, key, receiver) {
        return Reflect.get(target, key, receiver);
      },
    
      set(target, key, newVal, receiver) {
        Reflect.set(target, key, newVal, receiver);
    
        let map = targetMap.get(target);
        if (!map) {
          map = new Map();
          targetMap.set(target, map);
        };
    
        let depend = map.get(key);
        if (!depend) {
          depend = new Depend();
          map.set(key, depend);
        };
    
        depend.notify();
      }
    });
    

5.2. 正确的依赖收集

  • 收集依赖watchFn代码重构;
  • 如果一个函数中使用了某个对象的key,那么它应该被收集依赖;
    //原来:
    //function watchFn(fn) {
    //  depend.addDepend(fn);
    //};
    
    //更改为:
    let activefn = null;
    function watchFn(fn) {
      activefn = fn;
      //7.调用函数会触发get
      fn();
      activefn = null;
    };
    
    // 封装一个获取depend的函数
    const targetMap = new WeakMap();
    function getDepend(target, key) {
      let map = targetMap.get(target);
      if (!map) {
        map = new Map();
        targetMap.set(target, map);
      };
    
      let depend = map.get(key);
      if (!depend) {
        depend = new Depend();
        map.set(key, depend);
      };
    
      return depend;
    };
    
    const objProxy = new Proxy(obj, {
      get(target, key, receiver) {
        const depend = getDepend(target, key);
        depend.addDepend(activefn);
    
        return Reflect.get(target, key, receiver);
      },
      set(target, key, newVal, receiver) {
        Reflect.set(target, key, newVal, receiver);
    
        const depend = getDepend(target, key);
        depend.notify();
      }
    });
    
    watchFn(function() {
      console.log(objProxy.name, 'name需要响应的代码块');
    });
    
    watchFn(function() {
      console.log(objProxy.age, 'age需要响应的代码块');
    });
    
    objProxy.name = 'ace';
    /*
    yzh name需要响应的代码块
    18 age需要响应的代码块
    ace name需要响应的代码块
    */
    

六、Depend重构

  • 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
    • 解决:不使用数组,而是使用Set;
  • 问题二:我们并不希望将添加reactiveFn放到get中,因为它是属于Dep的行为;
    • 解决:addDepend方法优化;
    let activefn = null;
    
    class Depend {
      constructor() {
        // this.reactiveFns = [];
        this.reactiveFns = new Set();
      }
    
      // addDepend(reactiveFn) {
      //   this.reactiveFns.push(reactiveFn)
      // }
      addDepend() {
        if (activefn) {
          this.reactiveFns.add(activefn);
        }
      }
    
      notify() {
        this.reactiveFns.forEach(fn => {
          fn();
        })
      }
    }
    
    const objProxy = new Proxy(obj, {
      get(target, key, receiver) {
        const depend = getDepend(target, key);
        // depend.addDepend(activefn);
        depend.addDepend();
    
        return Reflect.get(target, key, receiver);
      },
      set(target, key, newVal, receiver) {
        Reflect.set(target, key, newVal, receiver);
    
        const depend = getDepend(target, key);
        depend.notify();
      }
    })
    

七、创建响应式对象

  • 目前的响应式是针对于obj一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象;
  • 完整代码:
    let activefn = null;
    
    // 3.
    class Depend {
      constructor() {
        this.reactiveFns = new Set();
      }
    
      addDepend() {
        if (activefn) {
          this.reactiveFns.add(activefn);
        }
      }
    
      notify() {
        this.reactiveFns.forEach(fn => {
          fn();
        })
      }
    };
    
    // 1.
    function watchFn(fn) {
      activefn = fn;
      //7.调用函数会触发get
      fn();
      activefn = null;
    };
    
    // 5.封装一个获取depend函数
    const targetMap = new WeakMap();
    function getDepend(target, key) {
      // 7.1.根据target对象获取map,第一次肯定是没有所以设置进去
      let map = targetMap.get(target);
      if (!map) {
        map = new Map();
        targetMap.set(target, map);
      };
    
      // 根据key获取depend对象,第一次肯定是没有所以设置进去
      let depend = map.get(key);
      if (!depend) {
        depend = new Depend();
        map.set(key, depend);
      };
    
      return depend;
    };
    
    function reactive(obj) {
      return new Proxy(obj, {
        get(target, key, receiver) {
          //根据target key获取对应的depend
          const depend = getDepend(target, key);
          depend.addDepend();
    
          return Reflect.get(target, key, receiver);
        },
        set(target, key, newVal, receiver) {
          Reflect.set(target, key, newVal, receiver);
    
          // 6.拿到属于变化的depend调用notify
          const depend = getDepend(target, key);
          depend.notify();
        }
      });
    };
    
    const info = reactive({
      name: 'luffy'
    });
    
    watchFn(() => {
      console.log(info.name);
    });
    
    info.name = 'yzh';
    

八、Vue2响应式原理

  • 可以将reactive函数进行如下的重构:
    • 在传入对象时,我们可以遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改;
    • 在setter和getter方法中的逻辑和前面的Proxy是一致的;
      function reactive(obj) {
        Object.keys(obj).forEach(key => {
          let value = obj[key];
          Object.defineProperty(obj, key, {
            get() {
              const depend = getDepend(obj, key);
              depend.addDepend();
      
              return value;
            },
      
            set(newVal) {
              value = newVal;
              const depend = getDepend(obj, key);
      
              depend.notify();
            }
          })
        });
      
        return obj;
      };
      

V3为什么不用 Object.defineProperty ???