写点什么

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

用户头像
Brave
关注
发布于: 30 分钟前
【Vue2.x 源码学习】第二十六篇 - 数组依赖收集的实现

一,前言


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


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


二,对象依赖收集的总结


{}.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 :


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

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


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


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

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 的:


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:


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 生命周期的实现

用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
【Vue2.x 源码学习】第二十六篇 - 数组依赖收集的实现