数组的可以通过 for...of
形式遍历元素,同样数组作为对象也可以用 for...in
形式来遍历 key
值。那么如果我们在副作用函数中对数组进行遍历,该如何进行依赖收集和依赖触发呢?
1. for...in遍历数组
js
const arr = reactive(['foo'])
effect(() => {
for (const key in arr) {
console.log(key) // 0
}
})
数组对象和常规对象的不同仅体现在 [[DefineOwnProperty]] 这个内部方法上,也就是说,使用for...in
循环遍历数组与遍历常规对象并无差异,因此对于 for...in
遍历我们同样可以使用 ownKeys
拦截函数进行拦截。先前我们针对常规对象所写的 ownKeys
拦截函数如下:
js
function createReactive(obj) {
return new Proxy(obj, {
// 省略其他拦截函数
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
}
针对常规对象,我们自己创造了名为 ITERATE_KEY
的 key
值来完成依赖收集,并且当对象的属性新增或者删除时会触发依赖。而对于数组对象则略有不同,其实需要触发 for...in
依赖的情况主要包括以下两种:
- 添加新元素,如:
arr[1] = 'bar'
; - 修改数组长度,如:
arr.length = 0
;
对于第一种情况,根据前文# Vue响应式原理(10)-数组的索引和length中可以得知,当我们设置新元素时会隐式触发对数组 length
属性的 SET
操作;而对于第二种本身就是对数组 length
属性的 SET
操作。因此总结来说我们希望在对数组的 length
进行 SET
操作时,能够获取到 for...in
循环的副作用函数重新执行,自然而然的我们就能想到,在 ownKeys
中以 length
作为 key
值进行依赖收集:
js
function createReactive(obj) {
return new Proxy(obj, {
// 省略其他拦截函数
ownKeys(target) {
track(target, Array.isArray(obj) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
}
在代码中,我们通过 Array.isArray
方法判断当前代理的原始对象是不是数组对象,如果是则用 length
作为 key
值进行依赖收集,否则使用自建的 ITERATE_KEY
作为 key
值进行依赖收集。通过这种形式,无论是为数组添加新元素,还是直接修改 length
属性,都能够正确地触发响应,符合我们的预期。
2. for...of遍历数组
与 for...in
不同,for...of
是用来遍历可迭代对象(iterable object
)的,因此我们需要先搞清楚什么是可迭代对象。ES2015
为 JavaScript
定义了迭代协议(iteration protocol
),它不是新的语法,而是一种协议。具体来说,一个对象能否被迭代,取决于该对象或者该对象的原型是否实现了 @@iterator
方法。这里的 @@[name]
标志在 ECMAScript
规范里用来代指 JavaScript
内建的 symbols
值,例如 @@iterator
指的就是Symbol.iterator
这个值。如果一个对象实现了 Symbol.iterator
方法,那么这个对象就是可以迭代的,如:
js
const obj = {
val: 0,
[Symbol.iterator]() {
return {
next() {
return {
value: obj.val++,
done: obj.val > 10 ? true : false
}
}
}
}
}
obj
实现了 Symbol.iterator
方法,那么它就是一个可迭代对象,就可以用 for...of
循环对它进行遍历操作。
js
for (const value of obj) {
console.log(value) // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
}
数组对象内建了 Symbol.iterator
方法,我们可以通过下列代码来验证:
js
const arr = [1,2,3]
const itr = arr[Symbol.iterator]();
console.log(itr.next()) // {value: 1, done: false}
console.log(itr.next()) // {value: 2, done: false}
console.log(itr.next()) // {value: 3, done: false}
console.log(itr.next()) // {value: undefined, done: true}
可以看到,我们通过将 Symbol.iterator
作为键获取数组迭代器方法。然后手动执行迭代器的 next
函数,可以完成数组对象的遍历。实际上 for...of
就是调用数组迭代器方法完成数组的遍历。
想要实现对数组进行 for...of
遍历操作的拦截,关键点在于找到 for...of
操作依赖的基本语义。在规范的 23.1.5.1 节中定义了数组迭代器的执行流程。从规范中可以得知,数组迭代器的执行会读取数组的 length
属性并且会读取数组的索引。我们可以通过下列代码来简单模拟数组迭代器方法的执行过程:
js
arr[Symbol.iterator] = function() {
const target = this
const len = target.length
let index = 0
return {
next() {
return {
value: index < len ? target[index] : undefined,
done: index++ >= len
}
}
}
}
因此,在我们通过 for...of
循环遍历数组的过程中,会调用数组 Symbol.iterator
属性对应的迭代器方法,而在迭代器方法中会完成对数组 length
和 索引的 GET
操作完成依赖收集,最终副作用函数和 length
属性、数组索引属性建立了依赖联系。
因此,当我们进行下列两项操作时:
- 通过索引设置数组值:
arr[1] = 'bar
'; - 设置数组长度:
arr.length = 0
; 都能触发依赖,重新执行副作用函数,不需要添加新的代码就能实现功能。
实际上,这也符合我们的预期,我们正希望当我们对数组值进行设置时或者当我们对数组长度进行设置时,此时的循环结果已经发生了变化,需要重新执行包含 for...of
循环的副作用函数。
最后,有一个细节需要注意,在进行 for...of
循环时,我们会隐式 arr
的 Symbol.iterator
属性,因此在 get
拦截函数中会获取到 key
值为 "Symbol.iterator"
,进而将当前的副作用函数收集到对应的依赖集合中。但是我们仔细思考下,其实我们并不需要建立这样的依赖关系,因为实际上我们并不会通过 "Symbol.iterator"
作为 key
值触发依赖,也就表明通过这个 key
收集到的副作用函数并不会有被取出执行的机会。为了避免发生意外的错误,以及性能上的考虑,我们需要对 Symbol
类的 key
值进行特殊处理,修改 get
拦截函数:
js
function createReactive(obj) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
// 添加判断,如果 key 的类型是 symbol,则不进行追踪
if (typeof key !== 'symbol') {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
return res
}
})
}
3.总结
本节介绍了数组的两种遍历形式对应的响应式系统设计和简单实现,其中 for...in
循环通过 ownKeys
拦截函数进行拦截,以 length
作为 key
值进行依赖收集;for...of
循环中数组迭代器的执行会读取数组的 length
属性并且会读取数组的索引,因此无需增加额外代码,在通过索引设置数组值或是设置数组 length
时都能正确触发依赖,但需要注意的是要在 get
拦截函数中取消对 symbol
类型值的依赖收集。