## VNode 内存优化:从源码角度看清引用关系的真正影响
关于 VNode 内存优化,核心其实很简单——减少冗余引用与闭包捕获。具体来说,就是要精简 props/attrs、避免 render 闭包、合理使用 patchFlag、清理钩子引用,让 VNode 尽可能做到无状态、无残留。

VNode 本质上就是轻量级的 JavaScript 对象,用来描述虚拟 DOM 的结构,并不是真正要渲染的 DOM 节点。但问题在于,如果完全不加约束,当组件频繁更新、嵌套层级过深,或者携带了大量额外数据时,VNode 带来的内存压力依然不容小觑。
所谓的精简属性,说白了就是减少不必要的引用、避免闭包捕获、剥离那些非必要的元信息。这件事不是靠删源码完成的,而是在开发阶段和编译阶段主动去控制生成逻辑。下面一个一个来说。
## 精简 propsData 与 attrs 的冗余合并
Vue 2 里,`v-bind` 和显式传入的 `props` 会合并进 `VNode.data.props` 和 `VNode.data.attrs`;到了 Vue 3,统一成了 `props` 和 `attrs` 两个字段。这里有个容易忽略的细节:父组件传递了未被子组件声明的属性时,这些属性默认会落到 `attrs` 中,而且可能包含事件监听器、自定义属性等。如果这些字段长期持有对函数或大型对象的引用,GC 基本就被堵死了。
具体怎么操作?
- 子组件明确声明所有接收的 prop,避免无用属性进入 `attrs` 兜底
- 用 `inheritAttrs: false` 主动拦截不需要继承的 attrs,防止它们挂载到根节点上顺便保留闭包
- 避免在 `v-bind` 中动态绑定整个对象(`v-bind="obj"` 这种写法),改用显式字段展开,方便静态分析和 Tree-shaking
## 规避 render 函数中闭包捕获导致的 VNode 长期驻留
手写 `render()` 函数时有个坑很容易踩:如果在返回 VNode 的过程中引用了外部作用域的变量——比如组件的 data、methods 或者父级作用域的响应式对象——那么这个 VNode 实例就会持有一套闭包引用链。结果就是,相关对象根本没法被垃圾回收,尤其是在列表项频繁复用、`key` 失控的情况下,问题会特别明显。
- render 函数里只访问 this 上的响应式属性或计算属性,尽量别碰局部变量或外层函数参数
- 需要传递回调时,优先用 `setup()` + `defineComponent` 返回的上下文方法,而不是在 render 中临时定义匿名函数
- 对于列表项的 VNode,确保 `key` 唯一且稳定,别因为 key 误用导致旧闭包一直挂在那里不释放
## 按需启用 patchFlag 与动态子节点标记(Vue 3)
Vue 3 的 VNode 新增了 `patchFlag` 字段,用来标记动态内容的类型——比如文本变化、class 变化还是样式变化。编译器注入这个标志后,runtime 的 diff 算法就能跳过静态子树的比对,效率提升很明显。
但如果模板里到处都是“伪动态”表达式(比如 `{{ item.id || '' }}`),编译器无法做静态判定,就只能打上 `PATCH_NORMAL` 标志,结果整个 vnode 子树都得参与深度 diff,临时 VNode 的创建和比对开销反而增加了。
- 模板中尽量用确定性表达式,避免在插值里混用逻辑运算符或三元判断,这些东西会干扰静态分析
- 纯静态内容(比如图标、固定文案),用 `v-once` 或者提取为 computed 常量,让编译器识别为 `HOISTED`
- 动态 class/style 用对象语法(`:class="{ active: isActive }"`),别用字符串拼接,这样编译器才能提取出正确的 patchFlag
## 避免 VNode.data.hook 中的长生命周期钩子引用
`VNode.data.hook`(Vue 2)或者 `VNode.dirs` / `VNode.component.exposed`(Vue 3)常用来挂载自定义指令或手动挂载逻辑。但如果在 `insert`、`init` 这类钩子中保存了对组件实例、DOM 元素或者大型数据结构的强引用,即便组件已经卸载了,VNode 也可能因为钩子函数没被清理而一直滞留在内存里。
- 指令钩子中避免直接存储 this 或 $refs,改用弱引用容器(比如 WeakMap)来关联 vnode 与状态
- 在 `unbind`(Vue 2)或 `unmounted`(Vue 3 指令)里主动清理闭包内持有的资源
- 慎用 `ref` 绑定到非根元素,尤其是 v-for 列表中——每个 ref 回调都会形成闭包,而且 ref 引用默认不会随 vnode 销毁自动解除
说到底,真正影响内存的不是 VNode 的字段数量本身,而是它携带的引用关系。从源码的角度看,Vue 已经对 VNode 做了高度精简——比如 Vue 3 用 `shapeFlag` 替代了布尔字段集合。开发者要做的,是配合框架的编译策略和响应式设计原则,让 VNode 尽可能“无状态”、“无副作用”、“无残留引用”。