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;
相关文章
|
23天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
34 9
|
2月前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
1月前
|
JavaScript UED
Vue双向数据绑定的原理
【10月更文挑战第7天】
|
26天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
24 0
|
2月前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
|
2月前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
2月前
|
缓存 JavaScript 容器
vue动态组件化原理
【9月更文挑战第2天】vue动态组件化原理
47 2
|
3月前
|
缓存 JavaScript 前端开发
[译] Vue.js 内部原理浅析
[译] Vue.js 内部原理浅析
|
3月前
|
JavaScript 前端开发 开发者
Vue学习之--------深入理解Vuex、原理详解、实战应用(2022/9/1)
这篇文章详细介绍了Vuex的基本概念、使用场景、安装配置、基本用法、实际应用案例以及注意事项,通过一个数字累加器的实战示例,帮助开发者深入理解Vuex的原理和应用。
|
3月前
|
JavaScript API
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
这篇博客文章讲解了Vue中列表排序的方法,使用`filter`、`sort`和`indexOf`等数组方法进行数据的过滤和排序,并探讨了Vue检测数据变化的原理,包括Vue如何通过setter和数组方法来实现数据的响应式更新。
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
下一篇
无影云桌面