前端知库

v-once、v-memo、shallowRef 和 markRaw 分别在什么时候用?

v-once 和 v-memo 是模板指令,作用在渲染上:v-once 让一块内容只渲染一次,之后的更新都跳过;v-memo(3.2+)按依赖数组决定是否跳过子树更新,主要用在超过上千项的 v-for 列表。shallowRef 和 markRaw 作用在数据上:shallowRef 只追踪 .value 被整体替换,markRaw 让对象永远不被转成代理,适合第三方库实例。它们都是微优化,依赖写错就会漏掉该有的更新。

参考答案

简答

一个页面顶部有一段只在加载时生成的说明文字,下面是一张上千行、只有“选中”状态会变的表格,旁边还挂着一个 ECharts 实例。这四个 API 正好对应这些场景:说明文字用 v-once,表格行用 v-memo,表格的大数组用 shallowRef,图表实例用 markRaw。

前两个是模板指令,管的是渲染。v-once 让元素和它的子节点只渲染一次,之后重新渲染时都当成静态内容跳过。v-memo 接收一个依赖数组,重新渲染时数组里的值都没变,就跳过整个子树的更新,连虚拟 DOM 节点都不重新创建。它从 Vue 3.2 开始支持。

后两个是响应式 API,管的是数据。shallowRef 只有 .value 本身被换掉才触发更新,里面的对象不会被转成深层响应式,适合大块数据。markRaw 给对象打上标记,让它永远不会被转成代理,适合第三方类实例或者组件对象。

要注意它们都是在特定场景下的微优化。v-memo 依赖写漏了、shallowRef 里改了内部数据,界面就不会更新。所以先用工具确认慢在哪,再有针对性地用。

v-once:只渲染一次

v-once 不需要表达式。下面这段要看 msg 后来变了,页面上会不会跟着变:

vue
<span v-once>这里永远不会变:{{ msg }}</span>
<MyComponent v-once :comment="msg" />

第一次渲染时,msg 的值被正常显示。之后 msg 变了,组件重新渲染,可这两个元素以及它们的子节点都被当成静态内容跳过,所以页面上还是旧值。

这正是它的用途和风险:确定只需要初始值的内容,用它省掉后续的比较;如果哪天这里需要跟着数据变,就要记得把 v-once 去掉。

v-memo:依赖不变就跳过

v-memo 接收一个固定长度的数组。重新渲染时,数组里每个值都和上次一样,整个子树的更新就全部跳过。文档说,最常见的有用场景是超过 1000 项的 v-for 长列表:

vue
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - 选中:{{ item.id === selected }}</p>
  <p>……更多子节点</p>
</div>

selected 变化时,没有 v-memo 的话,每一行都要重新创建虚拟节点再比较,尽管大部分行根本没变。加了 v-memo 以后,只有刚被选中和刚取消选中的两行,依赖值从 true 变成 false 或者反过来,其余行的依赖值没变,就直接复用上次的结果。

那依赖数组可以只写一部分吗?不行。比如行里还显示了 item.name,数组里却没有它,名字改了界面也不会更新。文档反复强调要把依赖写对,否则会跳过应该执行的更新。和 v-for 一起用时,两者要写在同一个元素上,v-memo 不能写在 v-for 内部的子元素上。依赖数组是空的 v-memo="[]",效果和 v-once 一样。

这两个指令和编译器的静态提升是什么关系,见《Vue 模板编译如何减少运行时更新工作?》。

shallowRef 和 markRaw:减少代理

前面两个跳过的是渲染,后面两个跳过的是把数据转成响应式代理的工作。下面这个例子要看三次操作里,哪次会让 effect 重新执行:

JavaScript
import { shallowRef, markRaw, reactive, isReactive, effect } from '@vue/reactivity';

const rows = shallowRef([{ name: '甲' }]);
let renders = 0;
effect(() => {
  rows.value.length;
  renders += 1;
});

rows.value.push({ name: '乙' });
console.log(renders); // 1

rows.value = [...rows.value];
console.log(renders); // 2

const chart = markRaw({ type: '第三方图表实例' });
const state = reactive({ chart });
console.log(isReactive(state.chart)); // false

为什么 push 之后 renders 还是 1?因为 shallowRef 只追踪 .value 这一层,push 改的是数组内部,.value 指向的还是同一个数组,所以不会触发。把 .value 换成新数组,才触发了第二次。这里用 @vue/reactivity 的 effect 代替组件渲染,方便脱离组件单独运行,组件里的表现一样。

所以 shallowRef 适合整块替换的数据,比如每次从接口拿回来一整份新表格。确实需要原地修改时,可以改完之后调用 triggerRef(rows) 手动通知。和 ref、reactive 的对比见《Vue 3 中 ref 和 reactive 有什么区别?》。

markRaw 的对象放进 reactive 里,再读出来也不是代理,isReactive 是 false。文档举的例子是复杂的第三方类实例和 Vue 组件对象,它们本来就不该是响应式的,被代理以后既浪费,还可能因为代理和原对象不是同一个而出错。

markRaw 也有边界。标记只作用在这个对象本身:如果把一个没有标记的嵌套对象放进响应式对象,再读出来得到的还是代理版本。所以文档把它和 shallowReactive 一起列为进阶 API,提醒谨慎使用。

常见追问

v-memo 和 computed 有什么区别?

computed 缓存的是一个计算结果,依赖变了才重新算。v-memo 缓存的是一段模板渲染出来的虚拟 DOM 子树,依赖数组没变就整段跳过更新。一个作用在数据上,一个作用在渲染上。

shallowRef 和 markRaw 怎么选?

数据需要整体替换并触发更新,比如表格数据,用 shallowRef。对象本身永远不需要响应式,比如图表实例、地图实例,用 markRaw,放进任何响应式对象里都不会被代理。

什么时候不该用 v-memo?

列表不长、渲染不慢的时候。文档说它只用于性能关键场景下的微优化,很少需要。依赖数组要手动维护,漏一个依赖就是一个“改了不显示”的 bug,收益小的时候不值得。