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_KEYkey 值来完成依赖收集,并且当对象的属性新增或者删除时会触发依赖。而对于数组对象则略有不同,其实需要触发 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)的,因此我们需要先搞清楚什么是可迭代对象。ES2015JavaScript 定义了迭代协议(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 循环时,我们会隐式 arrSymbol.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 类型值的依赖收集。

相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   7 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   7 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web8 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery