Vue 大集合响应式:让依赖图跟用户可见范围一致

Vue 大集合响应式:让依赖图跟用户可见范围一致原创封面

先用性能轨迹找到真正的依赖扇出

大列表卡顿不能只根据“数组有几万条”归因,因为实际成本可能来自深层代理创建、反复排序过滤、大量组件渲染,或一次局部更新触发的广泛依赖。在生产模式使用浏览器性能轨迹和 Vue 调试工具,记录用户输入、快照替换、筛选和滚动时的长任务,并检查哪些组件更新及哪个计算属性重新求值。没有这些证据就盲目使用缓存,往往只是把陈旧问题加到原有性能问题上。

为一次可重现操作分解主线程时间:服务端数据转为响应式对象的时间,纯函数排序过滤的时间,虚拟 DOM 生成与对比的时间,以及浏览器布局和绘制的时间。如果纯计算只占少量,把排序换成更复杂缓存不会改善大量 DOM 节点的布局。反之,如果每个键入都为整个数组构建深层代理,即使只渲染几十行,数据入口也会占用显著时间。

建立固定规模的基准数据,包含常见行、深层嵌套行、缺失字段和极端长文本,分别测量首次加载、单行编辑、批量替换、排序和快速滚动。使用中位与高分位而不是单次最快数值,同时记录组件更新数和内存增长。优化前后在相同浏览器、硬件限速与生产构建下比较,避免开发调试开销或数据随机性让结论失真。

服务端快照用浅层引用保持边界

对一次从服务端返回、应用中不做原地深层编辑的大集合,递归转为深层响应式代理通常没有价值。使用浅层引用保留顶层替换通知,将内部行当作只读快照。批量更新时构建新数组并替换引用,而不逐行把每个字段写回深层对象。这使依赖图与真正的版本边界一致:新快照到达才通知消费者,一行中一个未被业务观察的嵌套属性不会创建巨大跟踪网。

浅层响应不是对任意对象的一键优化。如果业务仍在各个组件中直接修改行对象的嵌套字段,浅层引用不会感知这种变化,界面可能停留在旧值。因此要把“快照不可变”变成模块契约:对象类型使用只读表达,编辑操作进入独立草稿,保存成功后用新行或新快照替换。开发环境可以冻结测试数据捕获误修改,但不必在生产中递归冻结几万条记录再创造新成本。

选择浅层引用后,还要检查组合式函数是否在内部把数组再包装为深层响应对象,或通过展开语法频繁克隆整份快照。数据边界要从获取层一直传递到视图层,只在真正需要响应式编辑的小对象上创建深层状态。用快照版本号作为昂贵派生计算的输入,比监听整个深层结构更容易理解与测试。

编辑状态以稳定标识与服务端快照分离

用户选中行、展开详情、输入未保存值和显示校验错误,都是客户端交互状态,不应被写进服务端大快照。用稳定行标识保存选中集合、展开集合和小型编辑草稿映射,快照刷新后按标识保留仍存在的交互状态,删除不再存在的键。这样一次选中不会让整个行对象变成新响应式嵌套,服务端返回的新对象引用也不会令选中状态意外丢失。

行标识必须在数据生命周期中稳定且唯一,不能使用当前数组下标。排序、筛选或在顶部插入新行会改变下标,如果下标同时是模板键和选中键,输入框、焦点与局部组件状态会转移到另一条数据。来自数据库的不可变主键是常见选择,如果数据没有稳定身份,应在进入前端前建模,而不用内容序列化结果作为巨大且不稳定的键。

保存编辑时,用行标识与服务端版本做条件写入,成功后以返回的新行替换快照中相同标识项并清除该草稿。冲突时保留用户草稿与服务端新值,让用户比较,不在背景中盲目覆盖。只替换一行可以减少不必要的快照抖动,但应使用新对象和新数组保持不可变契约,不直接改写浅层快照中的旧行。

昂贵排序与过滤先变成可测的纯函数

将排序、筛选、分组和搜索从计算属性内部抽出成纯函数,使输入只包含快照、查询和明确规则,输出为新的标识列表或行列表。先用基准证明这段计算是真正热点,再决定是在客户端优化、移到服务端查询,还是将大数据处理放入工作线程。计算属性的缓存只在响应式依赖不变时有效;如果每次渲染都构造新查询对象或新数组,就会主动使缓存失效。

排序函数不应原地修改服务端快照,先复制标识或浅层数组再排序,并明确处理空值、未定义、数字字符串、大小写和语言排序规则。比较器必须稳定且自洽,对相等值用不可变主键作次级排序,否则页面刷新时行会在等价项之间抖动。对多次使用的文本规范化值可以在数据入口预计算,但要把规则版本与快照绑定,避免分散组件对同一行得到不同正规化结果。

对每次输入都必须扫描整个集合的模糊搜索,延迟执行只能减少扫描次数,无法降低单次复杂度。数据规模超过客户端可稳定处理范围时,将搜索与排序移到有索引的服务端,返回分页结果。若离线场景要求全量本地数据,可建专用索引或在工作线程处理,但工作线程不能接收 Vue 代理,需要传递可结构化克隆的紧凑纯数据,也要计入克隆成本。

虚拟列表只渲染用户可见窗口

当浏览器性能轨迹显示主要成本是数千个行组件的创建、布局和绘制时,响应式层的微调不会解决根本问题。虚拟列表只为视口及适度过扫范围创建 DOM,用一个总高度占位保持滚动条。窗口计算使用排序后的稳定标识列表,行组件键仍为真实标识,不能因为 DOM 被复用就改用窗口下标,否则输入状态和无障碍关系会在滚动中错位。

固定行高的窗口计算简单且稳定,动态行高则需要实测尺寸缓存、预估高度和锚点修正。图片加载、文本换行、字体到达和详情展开都可能改变行高,测量后更新偏移时要保持用户当前锚点不突跳。如果内容高度高度不可预测,分页或渐进加载可能比复杂虚拟化更稳妥。选择方案时同时考虑搜索页内文本、打印和辅助技术能否访问未挂载行。

虚拟化不应破坏键盘导航。当焦点行即将离开窗口时,应根据交互模式扩大过扫、将焦点移至稳定容器,或先滚动再聚焦目标标识,不能让已聚焦 DOM 被直接回收。列表要向辅助技术提供总行数与当前行位置,并在排序、筛选后同步更新。用大字号、不同缩放和键盘持续移动做回归,只看鼠标滚动的帧率不足以证明列表可用。

过度记忆化会把计算成本变成内存成本

对每个筛选、排序和页面组合保存派生数组,可以让重复查询更快,但如果键空间没有上限,用户输入每个不同字符串都会留下一份大数组和其引用的行对象。缓存键必须包含快照版本,并在新快照到达时丢弃旧版派生结果。对查询键设置容量与最近使用逃汰,或只缓存真正重复的固定视图,不为一次性搜索保留几万项结果。

缓存中保存行标识列表往往比保存完整行副本更紧凑,渲染时通过当前快照的标识映射取回对象。但映射也要随快照一次构建而不是在每个行组件中线性查找。删除或权限变化后,派生标识中可能存在当前映射没有的项,渲染层要安全忽略并触发缓存失效,不使用旧行副本回退,否则会把性能缓存变成越过当前授权的数据源。

内存回归测试连续替换多个大快照、输入数百个不同查询、打开与关闭编辑草稿,然后触发垃圾回收并观察保留对象。如果内存持续随历史快照增长,查找是否有闭包、全局映射、未停止的侦听器或缓存仍引用旧行。不要只在页面初始加载后拍一张堆快照,真正泄漏往往只在多次查询与路由往返后出现。

从局部编辑到滚动的验证矩阵

正确性矩阵覆盖快照全量替换、单行成功保存、版本冲突、行被删除、排序中插入和筛选后恢复。断言选中、展开、草稿和焦点按稳定标识跟随正确记录,不因对象引用替换或数组下标变化转移。对浅层快照尝试原地修改嵌套字段,确保开发阶段能够发现违反不可变契约,正常编辑则只通过草稿和替换提交。

性能矩阵按一千、一万和数万行分层,测量快照设置、单行输入、组合筛选、多列排序和快速滚动的高分位时间。对比深层响应、浅层快照、只虚拟化、只服务端分页和组合方案,同时记录 DOM 节点、组件更新数、堆内存和主线程长任务。不为了得到漂亮平均值而删除长文本、动态高度和低端设备样本,因为这些才是用户最容易遇到的失败边界。

可用性矩阵在两百分比缩放、小视口、键盘导航和读屏环境中操作虚拟列表,检查焦点不被回收,行位置与总数被正确宣告,展开动态高度不会让当前内容跳离视口。再连续导航离开与返回页面,确认侦听器、工作线程和缓存在卸载时释放。只有主线程延迟、内存和交互正确性同时达标,大集合优化才算真正完成。

实现片段

const rows = shallowRef<ReadonlyArray<Row>>([])

一手参考资料

评论 · 0

还没有评论留下第一句经过思考的话。

游客评论需审核。注册后可直接公开,无需审核。

SHARE / 分享

分享这篇文章

WECHAT / 微信

用微信扫一扫

在手机微信中打开文章后,再从微信右上角分享给朋友或朋友圈。