Vue响应式原理(11)-数组遍历

数组的可以通过 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 依赖的情况主要包括以下两种:

  1. 添加新元素,如:arr[1] = 'bar';
  2. 修改数组长度,如: 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 属性、数组索引属性建立了依赖联系。

因此,当我们进行下列两项操作时:

  1. 通过索引设置数组值: arr[1] = 'bar';
  2. 设置数组长度: 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 类型值的依赖收集。

相关推荐
yume_sibai10 小时前
04-Nginx部署实战完全指南(前端部署 + 后端代理 + 监控告警 + 故障排查)
运维·前端·nginx
data analyse 45610 小时前
数据合规的同意记录怎么留存?
前端·人工智能·数据分析
linux_cfan10 小时前
videojs v10 源代码系列解读:35 · HLS 解析器:multivariant 与 media playlist
前端·javascript·音视频
IT_陈寒10 小时前
Redis键过期失效?这个坑我踩得明明白白
前端·人工智能·后端
web打印社区10 小时前
汽修 / 4S 店:维修工单、结算单网页怎么静默出纸
开发语言·前端·javascript·chrome·ecmascript
3Q工具箱 - BraggTroy10 小时前
Vue SPA 站百度收录“全军覆没“?一次预渲染抢救的完整实战(2026 踩坑版)
vue.js·百度·dubbo
liangshanbo121510 小时前
Webpack的分包策略面试题
前端·webpack·node.js
ttwuai10 小时前
Golang Web后台管理框架推荐:Gin、GoFrame与数据面板项目怎么分
前端·golang·gin
广州华水科技10 小时前
北斗GNSS变形监测系统在水库安全监测中的应用与优势
前端