从零到一打造 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」,一起跟日安当同学。

相关推荐
华玥作者2 小时前
[特殊字符] VitePress 对接 Algolia AI 问答(DocSearch + AI Search)完整实战(下)
前端·人工智能·ai
Mr Xu_3 小时前
告别冗长 switch-case:Vue 项目中基于映射表的优雅路由数据匹配方案
前端·javascript·vue.js
前端摸鱼匠3 小时前
Vue 3 的toRefs保持响应性:讲解toRefs在解构响应式对象时的作用
前端·javascript·vue.js·前端框架·ecmascript
sleeppingfrog3 小时前
zebra通过zpl语言实现中文打印(二)
javascript
lang201509283 小时前
JSR-340 :高性能Web开发新标准
java·前端·servlet
好家伙VCC4 小时前
### WebRTC技术:实时通信的革新与实现####webRTC(Web Real-TimeComm
java·前端·python·webrtc
未来之窗软件服务5 小时前
未来之窗昭和仙君(六十五)Vue与跨地区多部门开发—东方仙盟练气
前端·javascript·vue.js·仙盟创梦ide·东方仙盟·昭和仙君
baidu_247438615 小时前
Android ViewModel定时任务
android·开发语言·javascript
嘿起屁儿整5 小时前
面试点(网络层面)
前端·网络
VT.馒头5 小时前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript