【vue2原理】-第六篇,数组依赖收集

一、介绍

在 Vue 初始化过程中:

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

二、数组依赖收集的原理

1,数组的响应式实现

在响应式实现中,数组数据类型是通过重写能够改变原数组的 7 个方法实现的

收集数组所依赖的渲染 watcher,当数组更新时,触发对应 watcher 更新即可

2,数组的依赖收集方案

对象的依赖收集方案:为对象的每一个属性都增加一个 dep 属性用于做依赖收集

同理,也可以为数组增加 dep 属性用于收集依赖,

当数组更新时,通知数组依赖做视图更新

3,数组依赖收集的入口

js 复制代码
class Observer {
  constructor(value) {
    Object.defineProperty(value, '__ob__', {
      value:this,
      enumerable:false
    });
    if (isArray(value)) {
      value.__proto__ = arrayMethods;
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

对象或数组类型的数据会通过 new Observer 创建 observer 实例,

所以,Observer 中的 value 可能是数组,也可能是对象;

Observer 类中 value 也就是 this,是指 observer 实例,并为其添加__ob__属性

这样,每个对象或数组都有一个 ob 属性,

因此,可在此处为 observer 实例添加 dep 属性

这就相当于为数组或对象本身都增加了一个 dep 属性

这样就可以在对象或数组上,通过value.__ob__.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 值为数组,就进行依赖收集

所以,当取值时,会对数组和对象本身进行一次依赖收集

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 :

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

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

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

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

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

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

当对 arr 取值时{{arr}},默认会对 arr 进行 JSON.stringify(arr),

JSON.stringify 会取出内部所有属性进行打印输出

即 JSON.stringify 会对内部属性进行取值操作,此时会走 getter,

而 getter 中就会为对象本身和内部属性进行依赖收集

所以,这种情况默认就会进行依赖收集

js 复制代码
<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 的:

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

js 复制代码
例如:arr:[[1][2]]
当对 arr 取值时{{arr}},只对外层数组进行依赖收集,内部的数组没有进行依赖收集
所以,arr[0].push直接操作内部数组,是不会触发视图更新的

需要对数组类型做递归依赖收集

css 复制代码
数组中如果有对象[{}],也需要为对象本身做依赖收集,
因为未来有可能会为对象新增属性,对象本身做依赖收集才可以更新视图
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)
    }
  }
}

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

3,数组的视图更新

上边已经将数组的依赖进行了收集,目前 arr.push 还不能更新视图,因为没有调用更新方法

所以,当 arr.push 等操作变更数组时,还需要再触发数组的依赖更新(通过 ob 拿到 dep 调用 notify)

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 做视图更新
  }
});
相关推荐
WindfallSheng1 小时前
从Vibe Coding到Spec Coding:一套可落地的AI-SDD企业级研发实战工程
javascript·vue.js
我有满天星辰2 小时前
Vue 指令完全指南:从 Vue 2 到 Vue 3 的演进与实战
前端·javascript·vue.js
我有满天星辰5 小时前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js
leoZ23116 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄19 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro19 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte21 小时前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅1 天前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七1 天前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒1 天前
前端如何实现在线预览office常见文件功能?
前端·vue.js