本文是关于数据响应式原理的学习笔记,目的在于更好的理解 Vue 的底层原理,篇幅较长,故而拆分为几篇,今后会陆续更新~
在上一篇《数据响应式原理 - 01》中,我们实现将一个对象变为响应式对象的处理,本篇则对如何处理数组进行介绍。
数组的响应式处理
目前为止,当 obj 的某个属性的值为数组时,我们只可以侦测到数组的查看,无法侦测到数组是否被修改,比如现在给 obj 添加 c 属性为数组如下:
let obj = {
a: {
m: {
n: 1
}
},
b: 2,
c: [1, 2, { x: 3 }]
}
observe(obj)
obj.c.push(4)
console.log(obj)
得到的结果如下图,虽然 obj.c 是增加了一项,但并不能侦测到被修改这件事情。
如何侦测到数组被修改
思路分析
就是改写 obj.c.push 时的这个 push 方法,添加一个可以侦测的入口。借助的是原型链,新建一个 arrayMethods 对象 ,让 arrayMethods 隐式原型(__proto__
) 指向 Array.prototype,在 arrayMethods 上重写数组方法,再让 obj.c(是个数组) 的 __proto__
指向 arrayMethods ,那么 obj.c.push 时调用的 push 就是我们重写的 push,我们可以在重写的函数里进行侦测。
需要用到的方法:
- Object.create()
- Object.setPrototypeOf()
开始手写
新建 array.js 文件
// array.js
import { def } from './utils.js'
// 获取 Array.prototype,因为数组的方法都定义在这上面
const arrayPrototype = Array.prototype
// 让 arrayMethods 的隐式原型(__proto__)指向 Array.prototype
const arrayMethods = Object.create(arrayPrototype)
// 7 种可以改变数组自身的方法
const methodsCouldChange = [
'push',
'pop',
'unshift',
'shift',
'splice',
'reserve',
'sort'
]
// 改写上面 7 种数组方法
methodsCouldChange.forEach(item => {
// 保留数组方法的功能
const original = arrayPrototype[item]
def(arrayMethods, item, function() { // 注意这里不能用箭头函数,因为 arguments 和 this 指向的原因
// 能 console 就说明能侦测到改变了
console.log('数组被改变了')
// 给改写的方法添加上功能
const result = original.apply(this, arguments)
return result
}, false)
})
export default arrayMethods
在 Observer.js 引入,现在文件如下:
// Observer.js
// ...同之前重复部分省略
import arrayMethods from './array.js'
export default class Observer {
constructor(value) {
if (Array.isArray(value)) { // 判断传入的对象是否是数组
// 将数组 value 的隐式原型指向 arrayMethods
Object.setPrototypeOf(value, arrayMethods)
} else {
this.walk(value)
}
}
}
至此,在 index.js 中 obj.c.push(4) 时,打印台的输出变为下图,可以看出已经能侦测数组的改变,并且 obj.c 也成功添加了 4 这一项:
给数组的每一项添加侦测
数组的某一项的值也可能为对象或数组,我们需要给它们也添加上侦测(添加 observeArray 方法) 完善 Observer.js 文件:
// Observer.js
// ...省略重复
import observe from './observe.js'
export default class Observer {
constructor(value) {
// ...
if (Array.isArray(value)) {
...
this.observeArray(value)
} else {
// ...
}
}
...
// 处理数组,让数组的每一项变为响应式
observeArray(arr) {
arr.forEach(item => {
observe(item)
})
}
}
对于数组新增项的侦测
在 7 种被改写的数组方法中,push,unshift,splice 这 3 种是可以增加数组的项的,新增的项也需要被侦测,所以需要完善 array.js 文件,增加如下代码:
// array.js
// ...省略重复代码
methodsCouldChange.forEach(item => {
def(arrayMethods, item, function() {
// 如果是 push、unshift、splice,则要判断有没有新增项
let inserted = []
switch (item){
// 不同的 case 使用相同的代码
case 'push':
case 'unshift':
inserted = [...arguments]
break
case 'splice':
inserted = [...arguments].slice(2) // arguments 为类数组对象,不能直接调用数组方法
break
default:
break
}
// 获取到 Observe 实例 ob,以便调用实例方法 observeArray
const ob = this.__ob__
if (inserted.length) {
ob.observeArray(inserted)
}
return result
}, false)
})
注意,这里是可以保证调用这些数组方法的数组身上是有 ob 属性的,因为 Observer.js 里的 def(value, '__ob__', this, false)
。
至此,我们已经完成了对数组的响应式处理,接下来就是对于依赖的收集和 Watcher 类的介绍,将在下篇继续分享~
One More Thing
类数组对象
上面的代码里有用到 arguments 对象,它是类数组对象(伪数组),对于一个普通的对象来说,如果它的所有属性名均为正整数,同时也有相应的 length 属性,那么虽然该对象并不是由 Array 构造函数所创建的,它依然呈现出数组的行为,在这种情况下,这些对象被称为“类数组对象”。
比如打印 arguments:
类数组对象的方法属性
- 可以使用for循环
- 类数组对象没有继承 Array.prototype,因此不能直接调用数组方。可以间接使用 Function.call 方法调用,如:
Array.prototype.slice.call(类数组)
这样就可以调用数组方法了(用[].slice.call(类数组)
也一样)