Vue.js 中不存在“VNode 属性劫持”,VNode 是不可变的只读对象,响应式劫持仅作用于 data、props、computed 等状态源,Proxy 拦截状态读写以触发依赖收集和重渲染,VNode 仅为渲染结果。

Vue.js 中并不存在“VNode 属性劫持”这一机制,VNode(虚拟节点)本身是不可变的普通 JavaScript 对象,不参与响应式系统,也不被 Proxy 或 Object.defineProperty 劫持。真正被劫持的是组件实例的 data、props、computed、setup() 中的响应式状态,这些响应式数据在渲染函数执行时“触发依赖收集”,进而驱动 VNode 的重新生成与比对更新。
响应式数据才是劫持主体,VNode 只是渲染结果
VNode 是 Vue 渲染函数(如 render() 或 template 编译后的函数)的返回值,它描述了当前应渲染的 DOM 结构,本质上是只读快照:
- VNode 对象属性(如 tag、children、props、key 等)不会被 Proxy 拦截,也无法动态响应变化;
- 修改已创建的 VNode 实例(如 vnode.props.id = 'new')不会触发视图更新,也不会影响后续 diff;
- 响应式系统的拦截目标始终是 状态源(ref、reactive、data 选项等),而非 VNode 本身。
Proxy 在渲染阶段的作用:追踪依赖,而非操作 VNode
以 Vue 3 为例,Proxy 包裹响应式对象后,在渲染函数中读取属性时会触发 get 拦截器,完成依赖收集:
- 当 setup() 中的 reactive({ count: 0 }) 被 render 函数访问(如 h('div', `${state.count}`)),Proxy 的 get 触发,将当前渲染 effect 记录为该属性的依赖;
- 后续 state.count 改变时,触发 trigger,通知对应 effect 重新执行 render,产出新的 VNode;
- 新旧 VNode 进入 patch 阶段,由 renderer 执行 diff 和 DOM 更新——整个过程里 Proxy 从未接触或修改 VNode。
为什么容易误解“VNode 被劫持”?
常见混淆点来自以下场景:
审查 Vue 管理后台的变更请求(Vue 2 + Element UI,Vue 3 + Element Plus)。重点关注最小化变更实现、上线风险检查以及回归检查清单生成。
立即学习“前端免费学习笔记(深入)”;
-
JSX 中的响应式变量插值:看似在 VNode 描述中写
{count},实际是渲染函数体内对响应式对象的读取,劫持发生在 count 上,不是 VNode; - 自定义指令或高阶组件透传 props:它们可能基于 VNode 构造新 VNode,但原始 VNode 仍不可变,响应逻辑仍在状态层;
- devtools 或调试时查看 VNode:发现 props 含有 reactive 对象引用,误以为 VNode 本身被代理——其实只是引用传递,Proxy 依然作用于源对象。
协同工作的本质:分层解耦
Proxy 与 VNode 在渲染流程中各司其职,形成清晰分工:
- Proxy 层:拦截状态读写,驱动“何时重渲染”;
- 渲染函数层:用当前状态计算出新的 VNode 树;
- Renderer 层:对比新旧 VNode,最小化更新真实 DOM。
这种设计保证了响应式系统与虚拟 DOM 的正交性——你可以替换渲染器(如迁移到 WebGL),只要保持响应式核心不变,业务逻辑无需改动。

















