【手写 Vue2.x 源码】第二十六篇 - 数组依赖收集的实现

170 阅读8分钟

一,前言

上篇,主要介绍了数组依赖收集的原理

  • 数组的响应式实现
  • 数组的依赖收集方案介绍
  • 数组依赖收集的入口
  • 添加 dep 后,对象的收益

本篇,数组依赖收集的实现


二,对象依赖收集的总结

{}.dep => watcher

目前,“对象本身”和“对象中的每一个属性”都拥有一个dep属性,用于进行依赖收集,

此时,为对象新增一个不存在的新属性,可以通过对象本身的dep通知到对应的watcher进行视图更新操作;

  • 之前:对象本身没有dep,只有修改对象中已存在的属性,才会触发视图更新;
  • 现在:对象本身就有dep,新增对象属性时,可以通知对象本身dep收集的watcher来触发视图更新;

三,数组依赖收集的位置

对象或数组类型会通过 new Observer 创建 observer 实例,
所以,Observer 中的 value 可能是数组,也可能是对象;

Observer 类中的 value,即 this 指 observer 实例,
为其添加 `__ob__` 属性,这样每个对象本身或数组就拥有了 __ob__ 属性;

因此,可以在此处为 observer 实例添加 dep 属性,
这样,相当于为数组或对象本身都增加了一个 dep 属性;

这样一来,无论对象或数组,都可以通过`value.__ob__.dep`获取到`dep`,
当数组数据变化时,就可以通过`dep`中收集的`watcher`来触发视图更新操作;

// todo:这里与上一篇中“4,数组依赖收集的入口”的描述高度相似,可合并;


四,数组和对象本身做依赖收集

在使用defineReactive定义属性时,value值有可能是数组,

对数组的取值操作,会进入Object.definePropertyget方法,

而在get方法中,会为对象属性、对象和数组本身进行一次依赖收集操作;

// src/observe/index.js

/**
 * 给对象Obj,定义属性key,值为value
 *  使用Object.defineProperty重新定义data对象中的属性
 *  由于Object.defineProperty性能低,所以vue2的性能瓶颈也在这里
 * @param {*} obj 需要定义属性的对象
 * @param {*} key 给对象定义的属性名
 * @param {*} value 给对象定义的属性值
 */
function defineReactive(obj, key, value) {

  // childOb 是数据组进行观测后返回的结果(内部 new Observe 只处理数组或对象类型)
  let childOb = observe(value);
  
  let dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      if(Dep.target){
      
        // 对象属性的依赖收集
        dep.depend();
        
        数组或对象本身也依赖收集
        if(childOb){    // 若 childOb 有值,则说明是数组或对象
            // 在 observe 方法中,会通过 new Observe 为数组或对象本身添加 dep 属性
            childOb.dep.depend();   // 使数组和对象本身的 dep 记住当前 watcher
        }
      }
      return value;
    },
    
    set(newValue) {
      if (newValue === value) return
      observe(newValue);
      value = newValue;
      dep.notify(); // 对象属性的更新
    }
  })
}

默认情况下,会为对象或数组本身添加一个dep属性({}.__ob__.dep[].__ob__.dep),用于依赖收集;

  • 当进行数据观测时,会拿到数组的observer实例(在observer实例中包含__ob__),即返回值childOb;(在defineReactive数据观测方法中,调用observe方法返回new Observer实例,在observer实例中存在dep属性,即childOb.dep)
  • 在页面对数组进行取值时,如{{arr}},就一定会进入get方法,
  • 如果childOb有值,就让当前数组将依赖收集起来childOb.dep.depend()

这样,就完成了数组的依赖收集

功能测试:

image.png

为数组本身添加了dep属性,收集渲染watcher;

五,数组中嵌套对象(对象或数组)的递归处理

在数组中,还有可能会继续嵌套着数组或对象,比如:[{}][[]]或是[[[[]]]]

目前代码版本:

  • 只会对数组外层进行依赖收集;
  • 不会对数组内部嵌套的数组进行依赖收集;

注意:此时,数组中嵌套的对象是能够进行依赖收集的;

1,数组中嵌套对象的依赖收集原理

例如:arr:[{a:1},{b:2}]

  • 当对arr取值时{{arr}},默认会对arr进行JSON.stringify(arr)
  • JSON.stringify操作,将会取出数组内部所有属性进行打印输出,
  • 相当于JSON.stringify会对内部属性进行取值操作,即会进入getter方法,
  • 而在getter方法中,就会为对象本身和对象内部的属性进行依赖收集;

所以,在这种情况下,默认就会进行依赖收集操作;

<body>

  <div id=app>
    {{arr}}
  </div>
  
  <script>
    // 测试数组的依赖收集
    let vm = new Vue({
      el: '#app',
      data() {
        return { arr: [{ a: 1 }, { b: 2 }] }
      }
    });
    
    // 更新数组中对象的属性值
    vm.arr[0].a = 100; 
    console.log("输出当前 vm", vm);
  </script>
</body>

页面输出:[{"a":100},{"b":2}]

分析说明:

  • 对数组arr的取值操作时,内部会对arrJSON.stringify操作,会对对象中所有属性进行取值,这里就会做一次依赖收集;
  • 所以,更新数组中对象的属性值a,实际执行的是对象的更新操作,与外层的数组无关;

通过控制台观察,当前数组中对象的属性是有dep的:

image.png

2,数组中嵌套数组的依赖收集实现

例如:arr:[[1][2]]

  • 当前,对数组arr取值时{{arr}},仅对外层数组本身进行了依赖收集,数组内部的数组并没有进行依赖收集;
  • 所以,当执行arr[0].push()操作时,会直接操作内部的数组,就不会触发视图的更新了;

当数组中存在数组时(如:[[]]),需要对所有数组进行依赖收集:

  • 所以,需要对数组递归做依赖收集,循环数组,让数组中的每一个属性都进行依赖收集;

当数组中存在对象时(如:[{}]),未来可能为对象新增属性:

  • 所以,数组中的对象也需要做依赖收集,为对象本身做依赖收集才能触发视图更新;

综上,不论是对象还是数组,只要外层数组的里面是对象,就将里面的对象或数组都进行依赖收集;

// src/observe/index.js

function defineReactive(obj, key, value) {
  let childOb = observe(value);
  let dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      if(Dep.target){
        dep.depend();
        if(childOb){
            childOb.dep.depend();
            if(Array.isArray(value)){// 如果当前数据是数组类型
              dependArray(value)     // 数组中可能嵌套数组或对象,需要递归处理
            }  
        }
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return
      observe(newValue);
      value = newValue;
      dep.notify();
    }
  })
}

/**
 * 使数组中的引用类型(数组、对象)都进行依赖收集
 * @param {*} value 需要做递归依赖收集的数组
 */
function dependArray(value) {

  // 在数组中如果存在对象:[{}]或[[]],需要做进行依赖收集(后续可能会为对象新增属性)
  for(let i = 0; i < value.length; i++){
    let current = value[i];
    
    // 在每一项 current 上,如果有__ob__,则说明是对象(只有对象上才有 __ob__),使用 dep 收集依赖
    current.__ob__ && current.__ob__.dep.depend();
    
    // 如果数组内部的数组的内部还是数组,比如:[[[]]],需要继续递归处理
    if(Array.isArray(current)){
      dependArray(current)
    }
  }
}

注意:虽然,之前已经对数组进行了递归观测,但用户使用数据时并不是递归使用的;

  • 数据观测时是对arr:[[[[[]]]]]
  • 用户使用是:{{arr}} 只取了最外层
  • 最终显示在页面上是所有的内容 所以,更新数据时修改内部数据依然需要更新;

注意:arr:[{a:1},{b:2}] 这种情况下,{{arr}} 取值会执行JSON.stringify(arr) 当数据更新时arr[0].a = 100可以会触发更新

  • 因为JSON.stringify(arr)对对象的属性进行了取值操作,在取值时就对对象中的属性进行了依赖收集,
  • 但是,外层的对象本身是不会做依赖收集的, 所以,在这里弥补了这个漏洞,通过dependArray,不论数组内部是对象或数组都会对其本身进行一次依赖收集,即数组中的所有引用类型都进行了收集依赖;

功能测试:

<body>
  <div id=app> {{arr}} </div>
  
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return { arr: [[]] }
      }
    });
    
    console.log("输出当前 vm", vm);
  </script>
</body>

页面输出:[[]]

image.png

外层数组本身和内层数组都被添加了dep,收集渲染watcher

3,数组的视图更新

上边,已经完成了数组的依赖收集

但是,目前执行arr.push()操作还不能更新视图,因为此时还没有调用更新方法

所以,当执行arr.push等操作改变原数组时,还要再次触发数组的依赖更新,即通过ob拿到dep并调用notify

// src/observe/array.js

let oldArrayPrototype = Array.prototype;
export let arrayMethods = Object.create(oldArrayPrototype);
let methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'reverse',
  'sort',
  'splice'
]

methods.forEach(method => {
  arrayMethods[method] = function (...args) {
  
    oldArrayPrototype[method].call(this, ...args)
    let inserted = null;
    let ob = this.__ob__; // 获取数组上的 __ob__
    
    switch (method) {
      case 'splice':
        inserted = args.slice(2);
      case 'push':
      case 'unshift':
        inserted = args;
        break;
    }
    
    if(inserted)ob.observeArray(inserted);
    // 通过 ob 拿到 dep,调用 notify 触发 watcher 执行视图更新
    ob.dep.notify();     
  }
});

功能测试:

<body>
  <div id=app> {{arr}} </div>
  
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return { arr: [[]] }
      }
    });
    
    vm.arr[0].push(100);   // 修改数组中的数组
  </script>
</body>

// 页面输出:[[100]]

修改数组内部嵌套的数组,能够触发视图的更新操作;


六,总结

响应式数据原理,分为对象和数组两大类,在Vue的初始化过程中:

  • 1,会对对象的每个属性进行劫持,从而为对象中的所有属性添加一个dep属性(取值时做依赖收集);
  • 2,还会对属性值为对象或数组的本身增加dep属性,进行依赖收集;
  • 3,如果是属性更新,将触发属性对应的dep执行更新操作;
  • 4,如果是数组更新,将触发数组本身的dep执行更新操作;
  • 5,如果取值时是数组,还要让数组中的对象类型(数组中嵌套的对象或数组)也进行依赖收集(递归依赖收集);
  • 6,如果数组中嵌套了对象,由于对象取值会进行JSON.stringify操作,所以,对象中的属性默认就会做依赖收集;

七,结尾

本篇,主要介绍了数组依赖收集的实现

  • 对象依赖收集的总结;
  • 数组依赖收集的位置;
  • 数组和对象本身做依赖收集;
  • 数组中嵌套对象(对象或数组)的递归处理;

下一篇,Vue 生命周期的实现


更新日志

  • 20210629:
    • 添加 5-3、数组的视图更新部分
    • 添加各种情况的测试 Demo、截图、部分文案调整
    • 添加 6、总结部分
  • 20210805:
    • 更新“结尾”部分与文章摘要
  • 20230208:
    • 添加部分代码注释,优化部分内容说明,添加内容中的代码高亮;
  • 20230210:
    • 修改部分可能存在歧义和理解不清晰的内容描述;