从零到一打造 Vue3 响应式系统 Day 26 - 数组长度变更处理

在我们构建响应式系统的过程中,虽然对于原生 JavaScript 对象的处理已经相当完善,但数组 (Array) 与普通对象的属性不同,数组的 length 属性与其数值索引之间有紧密的联动关系。

手动变更数组长度

最直接改变数组长度的方式就是手动赋值。虽然这在日常开发中不被鼓励,但一个健壮的响应式系统必须能正确处理这种情况。

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { reactive, effect } from '../dist/reactivity.esm.js'

    const state = reactive(['a', 'b', 'c','d'])

    effect(() => {
      console.log(state.length)
    })

    setTimeout(() => {
      state.length = 2
    }, 1000)
  </script>
</body>

在上述示例中,我们直接修改了数组长度,它会触发更新(通常我们会避免直接更改数组长度的做法)。

像这样直接缩短数组长度,超出新长度的元素会被删除,如下图:

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { reactive, effect } from '../dist/reactivity.esm.js'

    const state = reactive(['a', 'b', 'c','d'])

    effect(() => {
      console.log(state[3])
    })

    setTimeout(() => {
      state.length = 2
    }, 1000)
  </script>
</body>

执行这段代码,控制台会先输出 d,这符合预期。然而,一秒后当 state.length 被修改为 2 时,console.log 并没有再次执行。

然而,问题出在哪里?

我们的 effect 依赖的是 state[3]。当 state.length 被修改为 2 时,索引为 3 的元素实际上已经被删除了。

因此,这个 effect 所依赖的键 ('3') 后续不会再发生任何 set 行为,导致它再也没有机会被重新触发。依赖关系因此丢失。

这个"删除"操作,仅触发了 state 对象 length 属性的 set并未触发 索引 '3'set

所以我们需要做的是,当 length 被缩短时,我们要找出所有依赖"被删除索引"的 effect,并通知它们重新执行:

TypeScript 复制代码
// dep.ts
export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  // 如果 depsMap 不存在,表示没有收集过依赖,直接返回
  if (!depsMap) return

  const targetIsArray = Array.isArray(target)

  // 新增:如果目标是数组且修改的是 length 属性
  if (targetIsArray && key === 'length') {
    // 遍历该数组的所有依赖
    depsMap.forEach((dep, depKey) => {
      // 如果依赖的键是 'length' 或大于等于新 length 值的索引
      // newValue 在 set 处理器中还拿不到,这里假设能拿到
      // 实际上应该在 set 中拿到 newValue 再传给 trigger
      if (depKey >= newValue || depKey === 'length') {
        // 通知访问了这些索引的 effect 以及访问了 length 的 effect 重新执行
        propagate(dep.subs)
      }
    })
  } else {
    // 如果不是数组,或者更新的不是 length,则直接获取依赖
    const dep = depsMap.get(key)
    // 如果依赖不存在,表示这个 key 没有在 effect 中被使用过,直接返回
    if (!dep) return

    // 找到依赖,触发更新
    propagate(dep.subs)
  }
}

(注:上面的代码片段为示意,newValue 需要从 set 处理器传入 trigger

state.length = 2 时,effect 会被重新触发。现在看我们的示例代码,会发现触发更新后,输出结果是 undefined,因为 state[3] 已不存在。

数组方法导致长度变更

除了直接赋值外,一些我们常用的数组方法,如 poppushshift 等,也会隐式地影响到数组长度:

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { reactive, effect } from '../dist/reactivity.esm.js'

    const state = reactive(['a', 'b', 'c','d'])

    effect(() => {
      console.log(state.length)
    })

    setTimeout(() => {
      state.push('e')
    }, 1000)
  </script>
</body>

这里可以看到,我们通过 push 增加了一个新元素,但是依赖 lengtheffect 并没有被触发更新:

那么,当 push 这类方法被调用时,我们如何侦测到 length 的隐式变更呢?

关键在于拦截 set 操作。push('e') 的底层操作,除了在索引 4 上设置新值,也会修改 length 属性。

我们可以在 set 代理中,比较操作前后的数组长度,如果不一致,就主动触发 length 属性的依赖更新:

TypeScript

javascript 复制代码
// baseHandlers.ts

export const mutableHandlers = {
  // ...
  set(target, key, newValue, receiver) {
    const oldValue = target[key]
    const targetIsArray = Array.isArray(target)
    // 如果 target 是数组,记录其旧长度
    const oldLength = targetIsArray ? target.length : 0
    
    const res = Reflect.set(target, key, newValue, receiver)
    
    // ... (ref 相关的处理)

    if (hasChanged(newValue, oldValue)) {
      // 触发当前 key 的更新
      trigger(target, key, newValue) // 把新值传进去
    }
    
    // 如果目标是数组,并且长度发生了变化
    if (targetIsArray && target.length !== oldLength) {
      // 并且被修改的 key 本身不是 'length' (避免重复触发)
      if (key !== 'length') {
        trigger(target, 'length', target.length) // 主动为 'length' 触发一次更新
      }
    }
    return res
  }
}

这段代码的逻辑是: 在 set 操作之后,我们检查目标是否为数组,并比较其新旧长度。

  • 如果长度发生了变化
  • 并且当前修改的 key 不是 length 本身(为了避免在 state.length = 2 这种情况下重复触发)
  • 我们就为 length 属性也主动触发一次更新。

今天我们聚焦于数组 length 属性的特殊性。通过分别在 trigger 函数和 set 代理中增加特殊的处理逻辑:

  • trigger :处理了手动缩短 length 时,对已删除索引的依赖触发。
  • set :处理了数组方法隐式改变 length 时,对 length 属性的依赖触发。

想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

相关推荐
怀柔白敬亭1 天前
js全局函数原来是这样啊
前端
北海道浪子1 天前
多模型Codex、ChatGPT、Claude、DeepSeek等顶级AI在开发中的使用
前端·后端·程序员
zyronon1 天前
用 Vue 3 + tailwindcss 快速开发一个背单词、文章的网页
前端
1024小神1 天前
扫地僧的minielectron使用方法记录
前端
ConardLi1 天前
一个小技巧,帮你显著提高 AI 的回答质量!
前端·人工智能·后端
jason_yang1 天前
vue3中使用auto-import与cdn插件冲突问题
vue.js·vite·cdn
星哥说事1 天前
开发者必备神器:阿里云 Qoder CLI 全面解析与上手指南
前端
Dcc1 天前
构建可维护的 React 应用:系统化思考 State 的分类与管理
前端·react.js
笔尖的记忆1 天前
【前端架和框架】react协调器reconciler工作原理
前端·javascript·面试