VNode劫持是在渲染阶段拦截并创建新VNode包裹Loading,而非修改原VNode;Vue2用render高阶组件实现,Vue3推荐useLoading Hook或AsyncWrapper组件,需透传props/children/key等字段并避免ref失效、key冲突等问题。

Render 函数劫持 VNode 的核心,是在组件渲染过程中拦截并修改虚拟节点(VNode)的结构,从而在不侵入业务逻辑的前提下,自动包裹 Loading 状态。这在 Vue 2 中通过 render 函数 + h(即 createElement)可自然实现;Vue 3 中则需结合 setup 和 h 或 render 选项,但更推荐使用自定义 Hook 或高阶组件(HOC)模式。
理解 VNode 劫持的本质
VNode 是 Vue 的虚拟 DOM 节点对象,描述了真实 DOM 应该长什么样。所谓“劫持”,并不是魔改 Vue 内部机制,而是:在组件生成最终 VNode 前,用一个中间层函数接管原始 render,对返回的 VNode 做递归遍历或条件包裹——比如检测异步状态,就用 h(Loading) 包一层,再把原 VNode 当插槽传进去。
关键点:
- 必须在
render函数内部操作,不能等挂载后操作真实 DOM - 不能直接修改 VNode(它是只读的),而要创建新 VNode(用
h或cloneVNode) - 需保留原始 VNode 的 props、children、key、ref 等关键字段,否则响应式和生命周期会异常
Vue 2 实现:基于 render 函数的高阶组件
写一个 withLoading 工厂函数,接收目标组件和 loading 判定逻辑:
function withLoading(WrappedComponent, isLoading) {
return {
name: `WithLoading(${WrappedComponent.name || 'Anonymous'})`,
props: WrappedComponent.props,
data() {
return { loading: false };
},
async created() {
this.loading = true;
try {
await this.$nextTick(); // 确保子组件 created 完成(可选)
const res = await Promise.resolve(isLoading.call(this));
this.loading = !res;
} catch (e) {
this.loading = false;
}
},
render(h) {
if (this.loading) {
return h('div', { class: 'loading-wrap' }, [
h('div', { class: 'loading-spinner' }, '加载中...'),
h(WrappedComponent, {
props: this.$props,
attrs: this.$attrs,
on: this.$listeners,
scopedSlots: this.$scopedSlots,
key: this._uid
})
]);
}
return h(WrappedComponent, {
props: this.$props,
attrs: this.$attrs,
on: this.$listeners,
scopedSlots: this.$scopedSlots,
key: this._uid
});
}
};
}
用法示例:
export default withLoading(MyTable, function() {
return this.fetching; // 依赖 data 中的 fetching 布尔值
});
Vue 3 实现:组合式 API + render 函数(Options API 兼容写法)
Vue 3 推荐用 setup,但 render 函数仍可作为选项存在。为保持一致性,建议封装为一个可复用的 useLoading Hook,配合自定义 render:
import { h, ref, onBeforeMount, getCurrentInstance } from 'vue';
export function useLoading(loadingRef, delay = 0) {
const isLoading = ref(false);
onBeforeMount(() => {
const timer = setTimeout(() => {
isLoading.value = unref(loadingRef);
}, delay);
getCurrentInstance().proxy?.$on('hook:updated', () => {
clearTimeout(timer);
isLoading.value = unref(loadingRef);
});
});
return isLoading;
}
// 在组件中:
export default {
setup(props, { slots }) {
const loading = ref(true);
const isLoading = useLoading(loading);
// 模拟异步加载
setTimeout(() => loading.value = false, 1000);
return () => {
if (isLoading.value) {
return h('div', { class: 'loading' }, [
h('span', '加载中...'),
h('div', { class: 'spinner' })
]);
}
return slots.default?.();
};
}
};
更工程化的做法是抽离为一个 AsyncWrapper 组件,接收 promise 或 loading prop,并支持 error、empty 插槽。
注意事项与避坑指南
实际落地时容易踩的坑:
-
key 冲突:被包裹组件若用了
v-for,外层加 div 可能导致 key 丢失,务必透传或重设 key -
ref 失效:原组件的
ref会指向外层容器而非目标组件,需用ref="wrappedRef"+this.$refs.wrappedRef.$refs.target间接访问 -
样式穿透:Loading 层可能遮挡原组件事件(如点击),需加
pointer-events: none或精准控制 z-index -
服务端渲染(SSR):Loading 状态在 SSR 中不可见,需配合
v-show或服务端预取数据避免闪烁
真正健壮的 Loading 组件,应支持延迟显示(防闪动)、取消请求、错误重试、骨架屏 fallback —— 这些都建立在干净劫持 VNode 的基础上。











