5.7.2 遍历数组
数组的
for...in要看length,for...of依赖内建迭代器读取length与索引,两者都能自动响应,但要避免用 symbol 键建立依赖。
一、for...in 遍历数组
- 遍历数组与遍历常规对象没有区别,同样由
ownKeys拦截; - 但普通对象只有添加/删除属性才会影响
for...in的结果,数组则不同:
影响数组 for...in 结果的操作 | 本质 |
|---|---|
添加新元素 arr[100] = 'bar' | 修改了 length |
修改长度 arr.length = 0 | 修改了 length |
本质上都是修改了数组的
length,因此数组的遍历依赖应该与'length'建立联系,而不是与ITERATE_KEY。
ownKeys(target) {
// 数组用 'length' 作为 key,普通对象仍用 ITERATE_KEY
track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
const arr = reactive(['foo'])
effect(() => {
for (const key in arr) console.log(key) // 0
})
arr[1] = 'bar' // 触发(length 变化)
arr.length = 0 // 触发(length 变化)
二、什么是可迭代对象
ES2015 定义了迭代协议(iteration protocol):一个对象能否被迭代,取决于该对象或其原型是否实现了 @@iterator 方法(@@[name] 是规范里代指内建 symbol 值的写法,@@iterator 即 Symbol.iterator)。
// 实现了 Symbol.iterator 方法,该对象就可迭代
const obj = {
val: 0,
[Symbol.iterator]() {
return {
next() {
return { value: obj.val++, done: obj.val > 10 }
}
}
}
}
for (const value of obj) console.log(value) // 0, 1, 2, ... 9
三、for...of 遍历数组
数组内建了 Symbol.iterator,规范 23.1.5.1 节定义的数组迭代器执行流程会:
- 读取数组的
length属性; - 迭代元素值时读取数组的索引。
一个模拟实现可以看清这一点:
const arr = [1, 2, 3, 4, 5]
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
}
}
}
}
结论:只要数组的 length 与索引建立了响应联系,for...of 无需额外代码即可响应——因为迭代器就是这样读数据的。
另外,Array.prototype.values === Array.prototype[Symbol.iterator] 为 true,所以 values() 等方法同样自动工作。
四、避免用 symbol 键建立响应联系
无论 for...of 还是调用 values(),都会读取数组的 Symbol.iterator 属性;它是一个 symbol 值:
- 为
Symbol.iterator这类 symbol 值建立响应联系没有意义,还可能引发意外错误、造成性能浪费; - 因此修改
get拦截函数,跳过 symbol 类型的 key(raw属性已单独处理):
get(target, key, receiver) {
if (key === 'raw') return target
// 非只读并且 key 不是 symbol 时才建立响应联系
if (!isReadonly && typeof key !== 'symbol') {
track(target, key)
}
// ...
}
相关
- 5.7.1 数组的索引与 length:索引与 length 的联动触发
- 5.7.3 数组的查找方法:
includes/indexOf在代理数组下的坑