Skip to content
一、vue3.0 响应式系统的实现原理

vue 2.x 响应式实现原理 利用 Object.defineProperty() 方法作为对象添加 get() 和 set() 方法来侦测对象的变化,缺陷如下:

  1. 性能较差
  2. 在对象上新增属性是无法被侦测的
  3. 改变数组的 length 属性是无法被侦测的

为此 vue 3.0 使用 ES6 的 Proxy 取代 Object.defineProperty()方法,性能更加优异,而且数组和对象一样,可以直接触发 get() 和 set() 方法。 Proxy 称为代理,是一种可以拦截并改变底层 JavaScript 引擎操作的包装器。

调用 new Proxy(target, handler) 可以为一个目标对象创建一个代理,代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱 函数。在调用 Proxy 构造函数时,需要传入两个参数,target 为目标对象,handler 是一个包含陷阱函数的处理器对象。

  • 简单介绍 proxy

    js
    /**
    * 处理对象
    * @type {{}}
    */
    const baseHandler = {
    
    // 陷阱函数,读取属性值时触发
    // 参数 target 是目标对象
    // 参数property 是要获取的属性名
    // 参数 receiver 是 Proxy 对象或继承 Proxy 的对象
    get (target, property, receiver) {
      console.log('获取值')
      return target[property]
    },
    
    // 陷阱函数,写入属性值时触发
    // 参数 value 是新的属性值
    set (target, property, value, receiver) {
      console.log('设置值')
    },
    
    // 陷阱函数,删除属性触发
    deleteProperty(target, property) {
      console.log('删除属性')
    }
    }
    
    // 目标对象
    const target = { name: '张三' }
    // 为目标对象创建代理对象
    const proxy = new Proxy(target, baseHandler);
    // 读取属性的值
    console.log('proxy.name--->', proxy.name)
    // 设置属性值
    proxy.name = '李四'
    // 删除属性
    delete proxy.name

    针对代理对象的相关操作就会触发处理器对象中的对应陷阱函数,在陷阱函数中就可以为目标对象的属性访问添加自定义的业务逻辑。

  • 针对对象监听

    js
    /**
     * 判断某个值是否是对象
     * @param val
     * @return {boolean}
     */
    const isObj = (val) => {
      return val !== null && typeof val === 'object';
    }
    
    /**
     * 响应式核心方法
     * @param target
     * @return {*}
     */
    const reactive = (target) => {
      return createReactiveObject(target)
    }
    
    /**
     * 创建响应式对象的方法
     * @param target
     */
    const createReactiveObject = (target) => {
      // 如果 target 不是对象,则直接返回
      if (!isObj(target)) {
        return target;
      }
    
      const baseHandler = {
        get (target, property, receiver) {
          console.log('获取值');
          const result = Reflect.get(target, property, receiver);
          return result;
        },
        set (target, property, value, receiver) {
          console.log('设置值');
          const result = Reflect.set(target, property, value, receiver)
          return result
        },
        deleteProperty(target, property) {
          console.log('属性删除');
          return Reflect.deleteProperty(target, property);
        }
      }
    
      const observed = new Proxy(target, baseHandler);
      return observed;
    }
    
    const proxy = reactive({ name: '张三' });
    proxy.name = '李四';
    console.log('proxy.name--->', proxy.name)
  • 针对对象的属性值是对象

    js
    /**
     * 判断某个值是否是对象
     * @param val
     * @return {boolean}
     */
    const isObj = (val) => {
      return val !== null && typeof val === 'object';
    }
    
    /**
     * 响应式核心方法
     * @param target
     * @return {*}
     */
    const reactive = (target) => {
    return createReactiveObject(target)
    }
    
    /**
     * 创建响应式对象的方法
     * @param target
     */
    const createReactiveObject = (target) => {
      // 如果 target 不是对象,则直接返回
      if (!isObj(target)) {
        return target;
      }
    
      const baseHandler = {
        get (target, property, receiver) {
          console.log('获取值');
          const result = Reflect.get(target, property, receiver);
          return isObj(result) ? reactive(result) : result;
        },
        set (target, property, value, receiver) {
        console.log('设置值');
          const result = Reflect.set(target, property, value, receiver)
          return result
      },
        deleteProperty(target, property) {
          console.log('属性删除');
          return Reflect.deleteProperty(target, property);
        }
      }
    
      const observed = new Proxy(target, baseHandler);
      return observed;
    }
    
    const proxy = reactive({ name: '张三', address: { city: '北京' }});
    proxy.address.city = '上海';

    访问 proxy.address 会引起 get 陷阱调用,由于 address 属性本身也是一个对象,因此为该属性也创建代码。

    上述代码运行结果为:

    获取值

    设置值

    考虑下面两种情况,第一种情况的代码如下:

    javascript
    const target = { name: '张三' };
    const proxy1 = reactive(target);
    const proxy2 = reactive(target);

    上述代码对同一个目标对象进行了多次代理,如果每次返回一个不同的代理对象,是没有意义的,要解决这个问题,可以在为目标对象初次创建代理后,以目标对象为key,代理为 value, 保存到一个 Map 中,然后在每次创建代理前,对目标对象进行判断,如果已经存在代理对象,则直接返回代理对象,而不再新创建代理对象。

    第二种情况的代码如下:

    javascript
    const target = { name: '张三' };
    const proxy1 = reactive(target);
    const proxy2 = reactive(proxy1);

    上述代码对一个目标对象进行了代理,然后又对该代理对象进行了代理,这也是无意义的,也需要进行区分,解决方法与第一种情况类似,不过是以代理对象为 key,目标对象为 value, 保存到一个 Map 中,然后在每次创建代理前,判断传入的目标对象是否本身就是代理对象,如果是,则直接返回该目标对象(原目标对象的代理对象)。

    定义两个 WeakMap 对象,分别保存目标对象到代理对象的映射,以及代理对象到目标对象的映射,并添加判断逻辑。修改后的代码如下:

    js
    /**
     * 判断某个值是否是对象
     * @param val
     * @return {boolean}
     */
    const isObj = (val) => {
      return val !== null && typeof val === 'object';
    }
    
    /**
     * 响应式核心方法
     * @param target
     * @return {*}
     */
    const reactive = (target) => {
      return createReactiveObject(target)
    }
    
    // 存放目标对象 => 代理对象
    const toProxy = new WeakMap();
    // 存放代理对象 => 目标对象
    const toRow = new WeakMap();
    
    /**
     * 创建响应式对象的方法
     * @param target
     */
    const createReactiveObject = (target) => {
      // 如果 target 不是对象,则直接返回
      if (!isObj(target)) {
        return target;
      }
    
      const proxy = toProxy.get(target);
    
      // 如果目标对象已经有代理对象,则直接返回该代理对象
      if (proxy) {
        return proxy;
      }
    
      // 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回
      if (toRow.has(target)) {
        // 这里的 target 是代理对象
        return target;
      }
    
      const baseHandler = {
        get (target, property, receiver) {
          console.log('获取值');
          const result = Reflect.get(target, property, receiver);
          return isObj(result) ? reactive(result) : result;
        },
        set (target, property, value, receiver) {
          console.log('设置值');
          const result = Reflect.set(target, property, value, receiver)
          return result
        },
        deleteProperty(target, property) {
          console.log('属性删除');
          return Reflect.deleteProperty(target, property);
        }
      }
    
      const observed = new Proxy(target, baseHandler);
      toProxy.set(target, observed);
      toRow.set(observed, target);
      return observed;
    }
    
    const proxy = reactive({ name: '张三', address: { city: '北京' }});
    proxy.address.city = '上海';

    向数组添加元素而导致 set 陷阱响应两次的问题。

    javascript
    const proxy = reactive([1, 2, 3]);
    proxy.push(4)

    上述代码会导致 set 陷阱触发两次,因为 push() 方法向数组中添加元素的同时还会修改数组的长度,因此有两次 set 陷阱的触发;一次是将数组索引为 3 的位置设置为 4 而触发; 一次是修改数组的 length 属性为 4 而触发。假如在 set 陷阱函数中更新视图,那么就会出现更新两次情况。

    为了避免出现上述情况,需要在 set 陷阱函数中区分是新增属性还是修改属性,同时对属性值的修改做一个判断,如果要修改的属性的新值与旧值,则无须进行任何操作。

    js
    /**
     * 判断当前对象是否有指定属性
     * @param target
     * @param property
     */
    const hasOwn = (target, property) => {
      return target.hasOwnProperty(property)
    }
    
    /**
     * 判断某个值是否是对象
     * @param val
     * @return {boolean}
     */
    const isObj = (val) => {
      return val !== null && typeof val === 'object';
    }
    
    /**
     * 响应式核心方法
     * @param target
     * @return {*}
     */
    const reactive = (target) => {
      return createReactiveObject(target)
    }
    
    // 存放目标对象 => 代理对象
    const toProxy = new WeakMap();
    // 存放代理对象 => 目标对象
    const toRow = new WeakMap();
    
    /**
     * 创建响应式对象的方法
     * @param target
     */
    const createReactiveObject = (target) => {
      // 如果 target 不是对象,则直接返回
      if (!isObj(target)) {
        return target;
      }
    
      const proxy = toProxy.get(target);
    
      // 如果目标对象已经有代理对象,则直接返回该代理对象
      if (proxy) {
        return proxy;
      }
    
      // 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回
      if (toRow.has(target)) {
        // 这里的 target 是代理对象
        return target;
      }
    
      const baseHandler = {
        get (target, property, receiver) {
          console.log('获取值');
          const result = Reflect.get(target, property, receiver);
          return isObj(result) ? reactive(result) : result;
        },
        set (target, property, value, receiver) {
          // 判断目标对象是否已经存在该属性
          const hasProperty = hasOwn(target, property);
          const oldValue = Reflect.get(target, property);
          const result = Reflect.set(target, property, value, receiver);
          if (!hasProperty) {
            console.log('新增属性')
          } else if (oldValue !== value) {
            console.log('修改属性')
          }
          return result;
        },
        deleteProperty(target, property) {
          console.log('属性删除');
          return Reflect.deleteProperty(target, property);
        }
      }
    
      const observed = new Proxy(target, baseHandler);
      toProxy.set(target, observed);
      toRow.set(observed, target);
      return observed;
    }
    
    const proxy = reactive({ name: '张三', address: { city: '北京' }});
    proxy.address.city = '上海';

    针对上述使用 push() 方法向数组中添加元素的代码,修改后的响应式代码只会输出"新增属性",原因是添加元素后,数组长度已经是4,当 push() 方法修改数组长度为4时,新增和旧值 相同,因此不进行任何操作。完善后的响应式代码也避免了对属性值进行无意义的修改。

  • vue3.0 的依赖收集,所谓依赖,就是指当数据发生变化时,要通知谁。

    javascript
    ...
    // 保存 effect 的数组,以栈的形式存储
    const effectStack = [];
    
    const effect = (fn) => {
      // 创建响应式 effect
      const effect = createReactiveEffect(fn);
      // 默认先执行一次 effect,本质上调用的是传入的 fn 函数
      effect();
    }
    
    // 创建响应式 effect
    const createReactiveEffect = (fn) => {
      // 响应式 effect
      const effect = () => {
        try {
          // 将 effect 保存到全局数组 effectStack 中,以栈的形式存储
          effectStack.push(effect);
          return fn();
        } finally {
          // 调用完依赖后,弹出 effect
          effectStack.pop();
        }
      }
      return effect;
    }
    ...
    
    const proxy = reactive({ name: '张三' });
    effect( () => {
      console.log(proxy.name);
    });
    proxy.name = '李四';

    上述代码的运行结果为:

    text
    获取值
    张三
    修改属性

    从输出结果中可以看出,除了默认执行一次的 effect 外,当 name属性发生变化时, effect 并没有被执行。为了在对象属性发生变化时,让 effect 再次执行,需要将对象的属性与 effect 进行关联,这可以采用 Map 来存储。考虑到一个属性可能会关联多个依赖,那么存储的映射关系应该是以对象属性为 key,保存所有 effect 的 Set 对象为 value,之所以 选择 Set 而不是数组,是因为 Set 中不能保存重复的元素。

    另外,属性毕竟是对象的属性,不可能脱离对象而单独存在,要跟踪不同对象属性的依赖,还需要一个 WeakMap,将对象本身作为 key,保存所有属性与依赖映射关系的 Map 作为 value, 存储到这个 WeakMap 中。

    定义好数据结构后,编写一个依赖收集函数 track()

    javascript
    // 保存对象与其属性依赖关系的 Map, key是对象,value 是 Map
    const targetMap = new WeakMap();
    // 跟踪收集
    const track = (target, property) => {
      // 获取全局数组 effectStack 中的依赖
      const effect = effectStack[effectStack.length - 1];
    
      // 如果存在依赖
      if (effect) {
        // 取出该对象对应的 Map
        let depsMap = targetMap.get(target);
        // 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中
        if (!depsMap) {
          targetMap.set(target, depsMap = new Map());
        }
        // 从 Map 中取出该属性对应的所有 effect
        let deps = depsMap.get(property)
        // 如果不存在,则以属性为 key,新建的 Set 为 value,保存到 depsMap 中
        if (!deps) {
          depsMap.set(property, deps = new Set());
        }
        // 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中 
        if (!deps.has(effect)) {
          deps.add(effect)
        }
      }
    }

    当属性发生变化时,触发属性关联的所有 effect 执行,为此,在编写一个trigger()函数。

    javascript
    // 执行属性关系的所有 effect
    const trigger = (target, type, property) => {
      const depsMap = targetMap.get(target);
      if (depsMap) {
        let deps = depsMap.get(property);
        // 将当前属性关系的所有 effect 依次执行
        if (deps) {
          deps.forEach(effect => {
            effect();
          });
        }
      }
    }

    依赖收集的函数和触发依赖的函数构建完成,需要在某个地方去收集依赖和触发依赖执行,收集依赖放到触发器对象中的 get 陷阱中,而触发依赖实在属性发生变化时执行依赖,自然是 放到 set 陷阱中。

    修改 createReactiveObject() 函数,添加 track() 和 trigger() 函数的调用。

    js
    /**
     * 判断当前对象是否有指定属性
     * @param target
     * @param property
     */
    const hasOwn = (target, property) => {
      return target.hasOwnProperty(property)
    }
    
    /**
     * 判断某个值是否是对象
     * @param val
     * @return {boolean}
     */
    const isObj = (val) => {
      return val !== null && typeof val === 'object';
    }
    
    /**
     * 响应式核心方法
     * @param target
     * @return {*}
     */
    const reactive = (target) => {
      return createReactiveObject(target)
    }
    
    // 存放目标对象 => 代理对象
    const toProxy = new WeakMap();
    // 存放代理对象 => 目标对象
    const toRow = new WeakMap();
    // 保存对象与其属性依赖关系的 Map, key是对象,value 是 Map
    const targetMap = new WeakMap();
    // 跟踪收集
    const track = (target, property) => {
      // 获取全局数组 effectStack 中的依赖
      const effect = effectStack[effectStack.length - 1];
    
      // 如果存在依赖
      if (effect) {
        // 取出该对象对应的 Map
        let depsMap = targetMap.get(target);
        // 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中
        if (!depsMap) {
          targetMap.set(target, depsMap = new Map());
        }
        // 从 Map 中取出该属性对应的所有 effect
        let deps = depsMap.get(property)
        // 如果不存在,则以属性为 key,新建的 Set 为 value,保存到 depsMap 中
        if (!deps) {
          depsMap.set(property, deps = new Set());
        }
        // 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中
        if (!deps.has(effect)) {
          deps.add(effect)
        }
      }
    }
    // 执行属性关联的所有 effect
    const trigger = (target, type, property) => {
      const depsMap = targetMap.get(target);
      if (depsMap) {
        let deps = depsMap.get(property);
        // 将当前属性关系的所有 effect 依次执行
        if (deps) {
          deps.forEach(effect => {
            effect();
          });
        }
      }
    }
    /**
     * 创建响应式对象的方法
     * @param target
     */
    const createReactiveObject = (target) => {
      // 如果 target 不是对象,则直接返回
      if (!isObj(target)) {
        return target;
      }
    
      const proxy = toProxy.get(target);
    
      // 如果目标对象已经有代理对象,则直接返回该代理对象
      if (proxy) {
        return proxy;
      }
    
      // 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回
      if (toRow.has(target)) {
        // 这里的 target 是代理对象
        return target;
      }
    
      const baseHandler = {
        get (target, property, receiver) {
          console.log('获取值');
          const result = Reflect.get(target, property, receiver);
          // 收集依赖
          track(target, property);
          return isObj(result) ? reactive(result) : result;
        },
        set (target, property, value, receiver) {
          // 判断目标对象是否已经存在该属性
          const hasProperty = hasOwn(target, property);
          const oldValue = Reflect.get(target, property);
          const result = Reflect.set(target, property, value, receiver);
          if (!hasProperty) {
            console.log('新增属性')
            trigger(target, 'add', property)
          } else if (oldValue !== value) {
            trigger(target, 'set', property)
            console.log('修改属性')
          }
          return result;
        },
        deleteProperty(target, property) {
          console.log('属性删除');
          return Reflect.deleteProperty(target, property);
        }
      }
    
      const observed = new Proxy(target, baseHandler);
      toProxy.set(target, observed);
      toRow.set(observed, target);
      return observed;
    }
    
    const proxy = reactive({ name: '张三', address: { city: '北京' }});
    proxy.address.city = '上海';

Released under the MIT License.