Vue 2 中的 变化侦测 原理

简介: Vue 2 中的 变化侦测 原理

变化侦测

Object 变化侦测

  • 对象变化追踪

    function defineReactive(data, key, val) {
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          return val;
        },
        set: function(newVal) {
          if (val === newVal) {
            return;
          }
          val = newVal;
        }
      });
    }
  • 如何收集依赖
    在 getter 中收集依赖,在 setter 中触发依赖
  • 依赖收集在哪
    假设依赖是一个函数,保存在 window.target 上,用 dep 数组收集依赖,在 set 被触发时,循环 dep 以触发收集到的依赖

    function defineReactive(data, key, val) {
      let dep = [];
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          dep.push(window.target); // 收集
          return val;
        },
        set: function(newVal) {
          if (val === newVal) {
            return;
          }
          // 触发
          for (let i = 0; i < dep.length; i++) {
            dep[i](newVal, val);
          }
          val = newVal;
        }
      });
    }

    功能解耦

    export default class Dep {
      constructor() {
        this.subs = [];
      }
      addSub(sub) {
        this.subs.push(sub);
      }
      removeSub(sub) {
        remove(this.subs, sub);
      }
      depend() {
        if (window.target) {
          this.addSub(window.target);
        }
      }
      notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update();
        }
      }
    }
    function remove (arr, item) {
      if (arr.length) {
        const index = arr.indexOf(item);
        if (index > -1) {
          return arr.splice(index, 1);
        }
      }
    }
    function defineReactive (data, key, val) {
      let dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          dep.depend();
          return val;
        },
        set: function(newVal) {
          if (val === newVal) {
            return;
          }
          val = newVal;
          dep.notify();
        }
      })
    }
  • 依赖是谁
    收集依赖收集的是数据发生变换时需要通知用到数据的地方,而使用这个数据的地方有很多,且类型不一样,既有可能是模板,也有可能是用户写的一个 watch,需要抽象出一个能集中处理这些情况的类。在收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。接着,它再通知其他地方,将这个类叫 Watcher。

    // keypath 当属性变化时做点什么
    // vm.$watch('a.b.c', function(newVal, oldVal) {
    //  // do something
    // })
    export default class Watcher {
      constructor (vm, expOrFn, cb) {
        this.vm = vm;
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get();
      }
      get() {
        window.target = this;
        let value = this.getter.call(this.vm, this.vm);
        window.target = undefined;
        return value;
      }
      update() {
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
      }
    }
    /**
     * 解析简单路径
     */
    const bailRE = /[^\w.$]/;
    export function parsePath(path) {
      if (bailRE.test(path)) {
        return
      }
      const segments = path.split('.');
      return function(obj) {
        for (let i = 0; i < segments.length; i++) {
          if (!obj) return;
          obj = obj[segments[i]];
        }
        return obj;
      };
    }
  • 递归侦测所有 key

    /**
     * Observer 类会附加到每一个被侦测的 object
     * 一旦被附加上,Observer 会将 object 的所有属性转换为 getter/setter 的形式
     * 来收集属性的依赖,并且当属性变化时会通知这些依赖
     */
    export class Observer {
      constructor (value) {
        this.value = value;
        if (!Array.isArray(value)) {
          this.walk(value);
        }
      }
    
      /**
       * walk 会将每一个属性都转换成 getter/setter 的形式来侦测变化
       * 该方法只有在数据类型是 Object 时被调用
       */
      walk (obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i], obj[keys[i]]);
        }
      }
    }
    
    // 支持递归子属性
    function defineReactive(data, key, val) {
      if (typeof val === 'object') {
        new Observer(val);
      }
      let dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
          dep.depend();
          return val;
        },
        set: function(newVal) {
          if (val === newVal) {
            return
          }
          val = newVal;
          dep.notify();
        }
      })
    }

    将一个 object 传到 Observer 中,该 object 会变成响应式的 object。

  • 关于 Object 的问题
    由于数据的变化是通过 getter/setter 来追踪的,有些语法中数据发生变化,Vue.js 追踪不到,如向 obj 添加或删除属性。
    为了解决该问题,Vue.js 提供了两个 API——vm.$set 与 vm.$delete

Array 变化侦测

在 ES6 之前,JavaScript 没有提供元编程的能力,也就是没有提供可以拦截原型方法的能力。但是可以使用自定义方法覆盖原生的原型方法来实现目的。

  • 拦截器
    Array 原型中可以改变数组自身内容的方法:push, pop, shift, unshift, splice, sort 和 reverse

    const arrayProto = Array.prototype;
    export const arrayMethods = Object.create(arrayProto);
    [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
      .forEach(function (method) {
        // 缓存原始方法
        const original = arrayProto[method];
        Object.defineProperty(arrayMethods, method, {
          value: function mutator (...args) {
            // do something
            return original.apply(this, args);
          },
          enumerable: false,
          writable: true,
          configurable: true,
        });
      });
  • 使用拦截器覆盖 Array 原型
    拦截器支只覆盖那些响应式数组的原型。而 Observer 将数据转换成响应式,所以在 Observer 中使用拦截器覆盖。

    export class Observer {
      constructor (value) {
        this.value = value;
        if (Array.isArray(value)) {
          // __proto__ 是 Object.getPrototypeOf 和 Object.setPrototypeOf 的早期实现
          value.__proto__ = arrayMethods;
        } else {
          this.walk(value);
        }
      }
    }
  • 将拦截器的方法挂载到数组的属性上
    处理不能使用 __proto__ 的情况

    import { arrayMethods } from './array';
    
    // __proto__ 是否可用
    const hasProto = '__proto__' in {};
    const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
    
    export class Observer {
      constructor (value) {
        this.value = value;
        if (Array.isArray(value)) {
          const augment = hasProto ? protoAugment : copyAugment;
          augment(value, arrayMethods, arrayKeys);
        } else {
          this.walk(value);
        }
      }
      // ...
    }
    function protoAugment (target, src, keys) {
      target.__proto__ = src;
    }
    function copyAugment (target, src, keys) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i];
        def(target, key, src[key]);
      }
    }
  • 如何收集依赖
    Array 的依赖和 Object 一样,也在 defineReactive 中收集。
    Array 在 getter 中收集依赖,在拦截器中触发依赖。

    function defineReactive(data, key, val) {
      if (typeof val === 'object') new Observer(val);
      let dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          dep.depend();
          // 收集 Array 的依赖
          return val;
        },
        set: function(newVal) {
          if (val === newVal) {
            return;
          }
          dep.notify();
          val = newVal;
        }
      });
    }
  • 依赖列表存在哪
    Vue.js 把 Array 的依赖存放在 Observer 中。因为数组在 getter 中收集依赖,在 拦截器 中触发依赖,所以依赖保存的位置,必须在 getter 和 拦截器中都可以访问到。

    export class Observer {
      constructor (value) {
        this.value = value;
        this.dep = new Dep();
        if (Array.isArray(value)) {
          const augment = hasProto ? protoAugment : copyAugment;
          augment(value, arrayMethods, arrayKeys);
        } else {
          this.walk(value);
        }
      }
      // ...
    }
  • 收集依赖

    function defineReactive(data, key, val) {
      let childOb = observe(val);
      let dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
          }
          return val;
        },
        set: function(newVal) {
          if (val === newVal) {
            return;
          }
          dep.notify();
          val = newVal;
        }
      });
    }
    /** 
     * 尝试为 value 创建一个 Observer 实例
     * 如果创建成功,直接返回新创建的 Observer 实例
     * 如果 value 已经存在一个 Observer 实例,直接返回它
     */
    export function observe (value, asRootData) {
      if (!isObject(value)) {
        return;
      }
      let ob;
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
      } else {
        ob = new Observer(value);
      }
      return ob;
    }
  • 在拦截器中获取 Observer 实例
    因为 Array 拦截器是对原型的一种封装,所以可以在拦截器中访问到 this (当前正在被操作的数组)。dep 保存在 Observer 中,所以需要在 this 上读到 Observer 的实例。

    // 工具函数
    function def (obj, key, val, enumerable) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true,
      });
    }
    
    export class Observer {
      constructor (value) {
        this.value = value;
        this.dep = new Dep();
        // 通过 __ob__ 属性可以拿到 Observer,进而拿到 dep
        // 同时 __ob__ 还可以用来标记当前的 value 是否被转换成响应式数据
        def(value, '__ob__', this);
    
        if (Array.isArray(value)) {
          const augment = hasProto ? protoAugment : copyAugment;
          augment(value, arrayMethods, arrayKeys);
        } else {
          this.walk(value);
        }
      }
      // ...
    }

    使用 __ob__ , 向数组依赖发送通知

    [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
      .forEach(function (method) {
        const original = arrayProto[method];
        Object.defineProperty(arrayMethods, method, {
          value: function mutator(...args) {
            const result = original.apply(this, args);
            const ob = this.__ob__;
            ob.dep.notify();
            return result;
          },
          enumerable: false,
          writable: true,
          configurable: true,
        });
      });
  • 侦测数组中元素的变化
    响应式数据的子数据也要进行侦测,不管是 Object 还是 Array

    export class Observer {
      constructor (value) {
        this.value = value;
        def(value, '__ob__', this);
        if (Array.isArray(value)) {
          this.observeArray(value);
        } else {
          this.walk(value);
        }
      }
      /**
       * 侦测 Array 中的每一项
       */
      observeArray (items) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i]); // 对每个元素执行一遍 new Observer,递归
        }
      }
      // ...
    }
  • 侦测新增元素的变化

    [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
      .forEach(function (method) {
        const original = arrayProto[method];
        def(arrayMethods, method, function mutator(...args) {
          const result = original.apply(this, args);
          const ob = this.__ob__;
          let inserted;
          switch (method) {
            case 'push':
            case 'unshift':
              inserted = args;
              break;
            case 'splice':
              inserted = args.slice(2);
              break;
          }
          if (inserted) ob.observeArray(inserted);
          ob.dep.notify();
          return result;
        })
      })
  • Array 的问题
    对 Array 的变化侦测是通过拦截原型的方式实现的,因此有些数组操作拦截不到。后续可能会用 Proxy 解决。

    this.list[0] = 2;
    this.list.length = 0;
相关文章
|
12天前
|
JavaScript 前端开发 开发者
响应式原理:Vue 如何跟踪数据变化
【4月更文挑战第22天】Vue 的响应式系统是其核心,通过数据双向绑定实现视图与数据同步。依赖收集和观测数据使Vue能跟踪变化,变化通知组件更新视图。高效的更新策略如批量更新和虚拟DOM提升性能。组件化和可组合性支持有效通信和代码复用,强调数据驱动开发。开发者应合理组织数据、谨慎处理变更并充分利用组件化优势,以提高效率和用户体验。
|
2天前
|
开发框架 JavaScript 算法
了解vue3的基本特性和底层原理
Vue3的底层原理涵盖了响应式系统的Proxy-based实现、组件的模板编译与渲染更新机制、组合式API带来的逻辑组织变革,以及其他关键特性的具体实现。这些原理共同构成了Vue3强大、高效、灵活的现代前端开发框架基础。
11 2
|
5天前
|
JavaScript
Vue3中props的原理与使用
Vue3中props的原理与使用
|
10天前
|
JavaScript 前端开发 开发者
Vue的响应式原理:深入探索Vue的响应式系统与依赖追踪
【4月更文挑战第24天】Vue的响应式原理通过JavaScript getter/setter实现,当数据变化时自动更新视图。它创建Watcher对象收集依赖,并通过依赖追踪机制精确通知更新。当属性改变,setter触发更新相关Watcher,重新执行操作以反映数据最新状态。Vue的响应式系统结合依赖追踪,有效提高性能,简化复杂应用的开发,但对某些复杂数据结构需额外处理。
|
27天前
|
JavaScript 前端开发 API
Vue中v-model的原理
Vue中v-model的原理
|
27天前
|
JavaScript 前端开发 API
vue中的ref/reactive区别及原理
vue中的ref/reactive区别及原理
19 0
|
1月前
|
JavaScript 前端开发 API
vue如何解决跨域?原理?
vue如何解决跨域?原理?
|
2月前
|
JavaScript
vue双向数据绑定的原理?
vue双向数据绑定的原理?
14 1
|
2月前
|
JSON JavaScript 算法
Vue之v-for(包含key内部原理讲解)
Vue之v-for(包含key内部原理讲解)
|
2月前
|
JavaScript 前端开发 API
Vue.js 深度解析:nextTick 原理与应用
Vue.js 深度解析:nextTick 原理与应用