数组的索引与 length
拿下面这个例子来说,当通过数组索引访问元素的值时,已经能够建立响应联系了:
1 | const arr = reactive(['foo']) |
但是通过索引设置数组的元素值与设置对象的属性值仍然存在根本上的不同,这是因为数组对象部署的内部方法[[DefineOwnProperty]]
不同于常规对象。实际上,当我们通过索引设置数组元素的值时,会执行数组对象所部署的内部方法[[Set]]
,这一步与设置常规对象的属性值一样。根据规范可知,内部方法[[Set]]
其实依赖于[[DefineOwnProperty]]
,到了这里就体现出了差异。数组对象所部署的内部方法[[DefineOwnProperty]]
的逻辑定义在规范的 10.4.2.1 节。
根据规范可知,如果设置的索引值大于数组当前的长度,那么要更新数组的length
属性。所以当通过索引设置元素值时,可能会隐式地修改 length
的属性值。因此在触发响应时,也应该触发与 length
属性相关联的副作用函数重新执行,如下面代码所示:
1 | const arr = reactive(['foo']) |
在这段代码中,数组的原长度为 1,并且在副作用函数中访问了 length
属性。然后设置索引为 1 的元素值,这会导致数组的长度变为 2.因此应该触发副作用函数重新执行。但目前的实现还做不到这一点,为了实现目标,我们需要修改 set
拦截函数,如下面的代码所示:
1 | function createReactive(obj, isShallow = false, isReadonly = false) { |
我们在判断操作类型时,新增了对数组类型的判断。如果代理的目标对象是数组,那么对于操作类型的判断会有所区别。即被设置的索引值小于数组长度,就被视为 SET
操作,因为它不会改变数组长度;如果设置的索引值大于了数组长度,则被视为 ADD
操作,因为这会隐式地改变数组的 length
属性值。有了这些信息,我们就可以在 trigger
函数中正确地触发与数组对象的 length
属性相关联的副作用函数重新执行了:
1 | function trigger(target, key, type) { |
但是反过来思考,其实修改数组的 length
属性也会隐式地影响数组元素,例如:
1 | const arr = reactive(['foo']) |
如上面代码所示,在副作用函数中访问了数组的第 0 个元素,接着将数组的 length
属性设置为 0.我们知道这会隐式地影响数组元素,即所有元素都会被删除,所以应该触发副作用函数重新执行。然而并非所有对 length
属性的修改都会影响数组中的已有元素,拿上面的例子来说,如果我们将 length
属性设置为 100,这并不会影响第 0 个元素,所以也就不需要触发副作用函数重新执行。这让我们意识到,当修改 length
属性值时,只有那些索引值大于或者等于新的 length
属性值的元素才需要触发响应。但无论如何,目前的实现还做不到这一点,为了实现目标,我们需要修改 set
拦截函数。在调用 trigger
函数触发响应时,应该把新的属性值传递过去:
1 | function createReactive(obj, isShallow = false, isReadonly = false) { |
接着,我们还需要修改trigger
函数:
1 | function trigger(target, key, type, newVal) { |
如上面的代码所示,为trigger
函数增加了第四个参数,即触发响应时的新值。在本例中,新值指的是新的length
属性值,它代表新的数组长度。接着,我们判断操作的目标是否是数组,如果是,则需要找到所有索引值大于或等于新的length
值的元素,然后把它们相关联的副作用函数取出并执行。