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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一前言

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

本篇数组依赖收集的实现


二对象依赖收集的总结

{}.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 触发视图更新操作

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

在使用 defineReactive 定义属性时此时value值有可能是数组
对数组的取值会走 Object.defineProperty 的 get方法

而 get 方法中就会进行依赖收集如果当前value值为数组就进行依赖收集
所以当取值时会对数组和对象本身进行一次依赖收集
// 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();  // 为每个属性添加一个 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 属性
当进行观测时会拿到数组的 observer 实例即 childOb`childOb.dep` 就是 dep

在页面对数组进行取值时如{{arr}} 一定会走 get 方法
如果 childOb 有值就让当前数组把依赖收集起来`childOb.dep.depend()`
这样就完成了数组的依赖收集

数组本身添加了 dep

image.png

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

数组中有可能嵌套数组或对象如[{}]或[[]]

当前只会对数组的外层进行依赖收集数组中嵌套的数组不会进行依赖收集

注意此时数组中嵌套的对象是可以进行依赖收集的

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 src="./vue.js"></script>
  <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 取值内部会进行 JSON.stringify就会为对象中的 a 属性做依赖收集
所以数组中的对象中的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__说明是对象就让 dep 收集依赖只有对象上才有 __ob__
    current.__ob__ && current.__ob__.dep.depend();
    // 如果内部还是数组继续递归处理
    if(Array.isArray(current)){
      dependArray(current)
    }
  }
}

测试

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

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

外层数组本身和内层数组都添加了 dep

image.png

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();  // 通过 ob 拿到 dep调用 notify 触发 watcher 做视图更新
  }
});

测试修改数组内部嵌套的数组

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

// 页面输出[[100]]

六总结

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

  • 通过对象属性劫持会为所有属性添加 dep
  • 还会为属性值进行依赖收集为对象本身和数组也添加 dep
  • 如果是属性变化将触发属性对应的 dep 去做更新
  • 如果是数组更新将触发数组本身的 dep 去做更新
  • 如果取值时属性值为数组数组中的对象类型数组中嵌套的对象或数组递归进行依赖收集
  • 如果数组中嵌套了对象由于对象取值会进行 JSON.stringify所以对象中的属性默认就会做依赖收集

七结尾

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

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

下一篇Vue 生命周期的实现

维护日志

20210629:

  • 添加 5-3、数组的视图更新部分
  • 添加各种情况的测试 Demo、截图、部分文案调整
  • 添加 6、总结部分

20210805:

  • 更新“结尾”部分与文章摘要
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue