组件热更新 rerender
function rerender(id: string, newRender?: Function): void {
const record = map.get(id)
// 防御性检查:确保组件记录存在,避免空指针异常
if (!record) {
return
}
// update initial record (for not-yet-rendered component)
// 更新初始记录的 render 方法,确保尚未渲染的组件也能使用新的渲染函数
record.initialDef.render = newRender
// Create a snapshot which avoids the set being mutated during updates
;[...record.instances].forEach(instance => {
if (newRender) {
// 新实例的 render 方法
instance.render = newRender as InternalRenderFunction
// 更新组件类型定义的 render 方法,确保一致性
normalizeClassComponent(instance.type as HMRComponent).render = newRender
}
// 清空实例的渲染缓存
instance.renderCache = []
// this flag forces child components with slot content to update
isHmrUpdating = true
// #13771 don't update if the job is already disposed
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
// 强制重新渲染
instance.update()
}
isHmrUpdating = false
})
}
enum SchedulerJobFlags {
QUEUED = 1 << 0, // 1 标记已经加入队列
PRE = 1 << 1, // 2 标记任务在 DOM 更新前 执行
ALLOW_RECURSE = 1 << 2, // 4 允许自身递归
DISPOSED = 1 << 3, // 已被销毁或已取消
}
setupRenderEffect
setupRenderEffect 函数是组件渲染副作用的核心入口。其核心作用是:为组件实例创建一个响应式渲染副作用(effect),并立即执行一次,完成组件的首次挂载;后续当组件依赖的响应式数据发生变化时,通过调度器重新执行该副作用,触发组件更新。
componentUpdateFn 执行流程
负责在正确的时机调用生命周期钩子、生成虚拟 DOM(VNode)、调用 patch 进行 DOM 操作,并将异步钩子(如 mounted、updated)放入后置渲染队列。
instance.update 是组件的渲染副作用函数(即 effect.run),主要负责执行完整的渲染/更新流程。它在以下时机触发:
- 首次挂载时:在
setupRenderEffect函数创建 effect 后,立即同步执行instance.update(),从而触发组件的首次渲染。 - 响应式数据变化时:当组件依赖的响应式数据(
ref、reactive、computed等)被修改时,会触发渲染副作用的调度器(scheduler),将instance.update推入 Vue 的异步更新队列(微任务队列)。随后在下一个 tick 执行,从而实现批量更新。 - 强制手动更新:可以显式调用
instance.update()强制组件重新渲染(例如在一些特殊场景中使用了非响应式数据)。
renderComponentRoot
renderComponentRoot 负责 执行组件渲染函数并生成根 VNode 的核心函数。
- 从组件实例中提取渲染所需的所有上下文(如
render函数、props、slots、attrs等)。 - 调用
render函数得到组件子树。 - 对返回的 VNode 进行规范化、属性透传、指令继承和过渡钩子绑定等处理。
- 最终返回一个可供
patch阶段使用的完整 VNode。
renderComponentRoot 执行的 render 来源 instance.render
- 模板编译生成的渲染函数。当组件使用
<template>时,无论是构建时,还是运行时,都会将模板编译成render函数,并挂载到组件定义上,最终赋值给instance.render。 - 用户直接定义的 render 选项。在选项式 API 或组合式 API 的
defineComponent中,可以显式提供render函数,它会直接成为instance.render。 - setup 函数返回的渲染函数。当 setup 函数返回一个函数时,该函数被视为组件的渲染函数。
instance.render 是组件的渲染函数,由模板编译或用户直接提供,其唯一职责是返回 VNode 树。它只在 instance.update 执行期间被调用,而且每次 update 执行时都会重新调用 render(无论挂载还是更新)。具体来说:
- 首次挂载:
instance.update执行挂载分支 → 调用renderComponentRoot(instance)→ 内部调用instance.render()生成根 VNode 树(subTree)。 - 响应式更新:
instance.update执行更新分支 → 再次调用renderComponentRoot(instance)→ 内部再次调用instance.render()生成新的 VNode 树。 - 注意:如果
setup返回了一个渲染函数,那么这个渲染函数最终也会被赋值给instance.render,因此执行时机相同。
processComponent
patch 阶段,针对 shapeFlag & ShapeFlags.COMPONENT处理
🍒 mountComponent 组件实例挂载
- 创建或获取组件实例:若兼容模式下已有实例则复用,否则调用
createComponentInstance新建。 - 特殊组件处理:若为
<KeepAlive>组件,注入渲染器内部方法以便管理缓存。 - 初始化组件:通过
setupComponent标准化 props/slots、执行setup函数、合并选项式 API。 - 处理 HMR 更新(开发环境):清空已有 DOM 引用,避免水合错误。
- 区分同步/异步 setup:
- 若存在
asyncDep(异步 setup):将组件注册到父级<Suspense>,并先渲染一个注释节点作为占位符。 - 否则:直接调用
setupRenderEffect创建渲染副作用并立即执行首次挂载。
- 若存在
setupComponent 组件初始化
- 初始化 props 和 slots
- 调用
initProps将 VNode 上的props标准化并挂载到组件实例上。 - 调用
initSlots将 VNode 的children转换为规范的插槽(slots)结构。
- 调用
- 根据组件类型执行
setup- 判断当前组件是否为有状态组件(
isStatefulComponent)。 - 若是,调用
setupStatefulComponent执行setup函数(组合式 API 入口),并返回其结果(同步返回值或异步 Promise)。 - 若是函数式组件,直接返回
undefined。
- 判断当前组件是否为有状态组件(
setupStatefulComponent
- 创建组件的渲染代理(
instance.proxy),使模板能够通过this访问props、data、setupState等上下文; - 执行组件的
setup函数(组合式 API 入口),并根据返回值类型分别处理:- 同步返回值立即合并为
setupState或渲染函数, - 异步返回值(Promise)则存储到
instance.asyncDep中交给<Suspense>或 SSR 等待,同时暂停/恢复响应式追踪以避免副作用;
- 同步返回值立即合并为
- 若没有
setup函数,则回退到finishComponentSetup完成选项式 API 的初始化。
handleSetupResult 执行
负责处理 setup() 函数的返回值,并将其转换为组件实例的内部状态
- 若
setupResult是函数:- 在 SSR 环境下且组件标记了
__ssrInlineRender,将该函数设置为instance.ssrRender; - 否则,将该函数设置为
instance.render,作为组件的渲染函数。
- 在 SSR 环境下且组件标记了
- 若
setupResult是对象:- 如果对象是 VNode(开发环境),会警告应该返回渲染函数而非 VNode。
- 将该对象通过
proxyRefs包装为响应式对象,并保存到instance.setupState。 - 开发环境下,调用
exposeSetupStateOnRenderContext将setupState的属性暴露到渲染上下文中。
- 若
setupResult是其他非undefined值(开发环境):- 发出警告,提示
setup()应该返回对象或函数。
- 发出警告,提示
🍒 updateComponent 更新组件实例
shouldUpdateComponent 通过多层优化策略判断组件是否需要更新:
- 快速路径:HMR、指令、过渡等场景直接返回
true - 优化路径:利用编译时
patchFlag精确检测变化 - 降级路径:手动渲染函数使用更保守的判断逻辑
app.mount
在 Vue 3 项目初始化时,app.mount 先执行,然后内部触发首次渲染(render 和 patch)。
const render: RootRenderFunction = (vnode, container, namespace) => {
let instance
if (vnode == null) {
if (container._vnode) {
// 卸载
unmount(container._vnode, null, null, true)
instance = container._vnode.component
}
} else {
// 渲染/更新场景
patch(
container._vnode || null, // 旧的虚拟节点(如果存在)
vnode, // 新的虚拟节点
container, // 渲染目标容器
null, // 元素命名空间
null, // 子节点
null, // 元素命名空间
namespace,
)
}
container._vnode = vnode // 更新容器引用
if (!isFlushing) {
isFlushing = true
// 执行预刷新回调(如 watch 的 flush: 'pre')
flushPreFlushCbs(instance)
// 执行后刷新回调(如 nextTick、过渡效果)
flushPostFlushCbs()
isFlushing = false
}
}
vue模板可以使用的变量
$attrs, 父组件传入的非 prop 属性(class、style、事件等)$slots, 组件接收的插槽对象$refs, 通过 ref 属性注册的 DOM 元素或子组件实例$el, 组件根 DOM 元素(仅在挂载后可用)$options, 组件选项对象(包含 name, components, data 等)$parent, 父组件实例$root, 根组件实例$data, 组件的整个数据对象$props, 当前组件的 props 对象$nextTick, 等待 DOM 更新后的回调函数$emit, 触发父组件监听的事件(模板中较少直接使用)$forceUpdate, 强制组件重新渲染(不推荐常规使用)
示例 定义的响应式变量 、props、methods、
<template>
<div>
<div>TemplateA</div>
<div>count: {{ count }}</div>
<p>路由名称 {{ $route.name }}</p>
<button @click="($event) => handleClick($event)">click</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
const handleClick = (data: MouseEvent) => {
console.log(data);
};
</script>
示例 父组件透传的 props,子组件未声明的 props
<template>
<div>
<div>TemplateA</div>
<div>count: {{ count }}</div>
<p>{{ $.attrs.message }}</p>
<!-- <button @click="() => handleClick($data, $options, $route)">click</button> -->
<button @click="$router.push('/cloud/group')">click</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
// const handleClick = (data: any, options: any, route: any) => {
// console.log("data", data);
// console.log("options", options);
// console.log("route", route);
// };
</script>
示例 路由 $router、 $route
<template>
<div>
<div>TemplateA</div>
<div>count: {{ count }}</div>
<!-- <p>路由名称 {{ $route.name }}</p> -->
<!-- <button @click="() => handleClick($data, $options, $route)">click</button> -->
<button @click="$router.push('/cloud/group')">click</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
// const handleClick = (data: any, options: any, route: any) => {
// console.log("data", data);
// console.log("options", options);
// console.log("route", route);
// };
</script>
示例 事件 $event
<template>
<div>
<div>TemplateA</div>
<div>count: {{ count }}</div>
<p>路由名称 {{ $route.name }}</p>
<button @click="() => handleClick($data, $options, $route)">click</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleClick = (data: any, options: any, route: any) => {
console.log("data", data);
console.log("options", options);
console.log("route", route);
};
</script>
示例 组件接收的插槽对象$slots
<template>
<div>
<div>TemplateA</div>
<div>count: {{ count }}</div>
<p>{{ $.attrs.message }}</p>
<div>
<component :is="$slots.default" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>