手动构建 VNode 属性对象需严格区分响应式数据与静态属性:class/style 必须解包为纯 JS 值;事件处理器须用稳定函数引用;directives 和 slots 需单独传参;props 对象本身不可响应式。

手动构建 VNode 属性对象,核心是理解 Vue 3 中 VNodeProps 的结构规范,并严格区分“响应式数据”与“静态属性”的注入时机。关键不是堆砌字段,而是按 Vue 内部解析逻辑组织键名和值类型——尤其要避开把响应式对象直接赋给 class、style 或事件处理器等需特殊处理的字段。
class 和 style 必须用规范化格式,不能传响应式对象
class 和 style 在 VNode props 中虽是字符串键,但 Vue 会做深度归一化:它期望接收的是数组、对象或字符串,而非一个 ref 或 reactive 包裹的值。若你传入 class: reactive({ active: isActived }),Vue 不会自动追踪其变化,渲染时取到的仍是初始快照。
- ✅ 正确做法:在构建 props 对象前,先解包响应式值,生成纯 JS 值
- 例如:
const cls = [baseClass, { 'is-active': isActived.value }],再传入{ class: cls } - ✅ style 同理:用
{ color: textColor.value, fontSize: `${size.value}px` },而不是style: reactive({ ... })
事件监听器必须是函数引用,不可包裹或延迟求值
Vue 在 patch 阶段比对事件时,依赖函数引用相等性(===)判断是否更新。若每次构建 props 都写 onClick: () => handleClick() 或 onClick: handleClick.bind(null, id),会导致每次生成新函数,强制重绑事件,丢失 DOM 事件缓存,还可能引发内存泄漏。
- ✅ 正确做法:提前定义稳定函数引用,必要时用闭包捕获参数
- 例如:
const handleClickItem = (id) => { /* ... */ }; const props = { onClick: () => handleClickItem(itemId) }; - ⚠️ 注意:不要在 render 函数里动态创建箭头函数作为事件处理器
指令(directives)和插槽(slots)不能混进 data 对象
Vue 3 的 VNode 构造函数(如 createVNode)明确将 props、directives、slots 分离为不同参数。若把 v-loading 指令写成 { 'v-loading': true } 放进 props,它不会生效;同理,插槽函数也不能塞进 props.children。
- ✅ 指令应单独传入第 4 个参数:
createVNode(Component, props, slots, { directives: [{ name: 'loading', value: isLoading }] }) - ✅ 插槽必须作为独立参数传入,且需符合函数签名:
slots: { default: () => [/* VNodes */] } - ⚠️
key、ref、class、style等属于 props;onXXX事件也属 props,但v-xxx不是
避免用响应式对象直接覆盖整个 props 对象
有些开发者会写 const props = reactive({ class: ..., onClick: ... }); createVNode(..., props),这看似简洁,实则危险:Vue 不会对整个 props 对象做响应式代理监听,只读取构建瞬间的值;后续 props.class 变更不会触发 VNode 更新。
- ✅ 正确策略:props 对象本身保持普通(non-reactive),仅内部字段值可来自响应式源,且需手动同步
- ✅ 更稳妥方式:在
render函数中每次重新计算 props,确保所有字段都是最新纯值 - 例如:
return () => createVNode('div', { class: computeClass(), onClick: handleClick })









