在上文中,我们分析了Vue组件的初始化过程,也就是setupComponent
函数的实现。初始化完成后,组件实例已经创建,但还需要与响应式系统建立联系并进行渲染。这就是setupRenderEffect
函数的职责。
函数定义
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
// 创建组件更新函数
const componentUpdateFn = () => {
// ... 组件的挂载和更新逻辑
}
// 创建响应式effect
instance.scope.on()
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
instance.scope.off()
// 创建更新函数
const update = (instance.update = effect.run.bind(effect))
const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
job.i = instance
job.id = instance.uid
effect.scheduler = () => queueJob(job)
// 允许递归更新
toggleRecurse(instance, true)
// 开发环境下的依赖追踪
if (__DEV__) {
effect.onTrack = instance.rtc
? e => invokeArrayFns(instance.rtc!, e)
: void 0
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
}
// 执行首次更新
update()
}
componentUpdateFn详细分析
componentUpdateFn
是setupRenderEffect中最核心的函数,它负责组件的挂载和更新逻辑。这个函数通过instance.isMounted
来区分是首次挂载还是更新。
首次挂载流程
if (!instance.isMounted) {
// 1. 生命周期钩子:beforeMount
if (bm) {
invokeArrayFns(bm)
}
// 2. 渲染组件
const subTree = (instance.subTree = renderComponentRoot(instance))
这里的renderComponentRoot
函数是组件渲染的核心,让我们详细分析它的实现:
function renderComponentRoot(instance: ComponentInternalInstance): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx,
inheritAttrs
} = instance
// 执行渲染函数,生成vnode
let result = render!.call(proxy, proxy, renderCache, props, setupState, data, ctx)
// 处理返回结果
if (result instanceof VNode) {
// 单个VNode直接返回
return cloneIfMounted(result, instance, true)
} else if (isArray(result)) {
// 数组需要创建Fragment
return createVNode(Fragment, null, result.map(child =>
cloneIfMounted(child, instance, true)
))
}
}
函数返回的VNode结构示例:
// 单个元素
{
type: 'div',
props: { class: 'container' },
children: [/* 子节点 */],
el: null, // 对应的真实DOM,初始为null
key: null,
ref: null
}
// Fragment(多个根节点)
{
type: Symbol(Fragment),
props: null,
children: [
{ type: 'div', props: {}, children: [] },
{ type: 'span', props: {}, children: [] }
]
}
生成的subTree会被传递给patch函数进行实际的DOM操作:
// 3. 挂载子树
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
namespace,
)
// 4. 生命周期钩子:mounted
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// 5. 标记挂载完成
instance.isMounted = true
}
更新流程
else {
// 1. 获取更新相关信息
let { next, bu, u, parent, vnode } = instance
// 2. 生命周期钩子:beforeUpdate
if (bu) {
invokeArrayFns(bu)
}
// 3. 渲染新的子树
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// 4. 更新子树
patch(
prevTree,
nextTree,
hostParentNode(prevTree.el!)!,
getNextHostNode(prevTree),
instance,
parentSuspense,
namespace,
)
// 5. 生命周期钩子:updated
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
}
响应式系统集成
setupRenderEffect是Vue中连接响应式系统和渲染系统的关键函数。它通过以下方式建立这个连接:
// 1. 创建响应式作用域
instance.scope.on()
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
instance.scope.off()
// 2. 设置更新机制
const update = (instance.update = effect.run.bind(effect))
const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
effect.scheduler = () => queueJob(job)
这里涉及两个重要概念:
- 响应式作用域:通过ReactiveEffect包装组件的渲染函数,使其能够响应数据变化。当组件中的响应式数据发生变化时,会触发组件重新渲染。
- 更新队列:Vue采用异步更新队列来优化性能,避免不必要的重复渲染。这个机制确保了即使数据频繁变化,也只会在合适的时机进行一次更新。
关于响应式系统的详细实现和更新队列的优化机制,会在后续的文章中深入分析。
总结
至此,我们分析了setupRenderEffect
函数的实现,这个函数是组件渲染的核心,它通过以下步骤完成渲染:
- 创建响应式作用域,包装渲染函数
- 执行
componentUpdateFn
进行实际的渲染工作 - 调用
patch
函数递归处理虚拟DOM树
当patch
函数被调用后,渲染流程就进入了"递归创建"阶段:
// 递归patch的过程
patch(null, subTree, container, anchor, instance, parentSuspense, namespace)
↓
patch(n1, n2, container, ...) // 根据不同类型节点调用不同处理函数
↓
processElement/processComponent/... // 处理具体类型的节点
↓
mountElement/mountComponent/... // 创建真实DOM节点
↓
patch(child) // 递归处理子节点
这个递归过程会持续进行,直到:
- 所有的虚拟DOM节点都被转换为真实DOM节点
- 所有的DOM节点都被正确插入到文档中
- 完成整个组件树的渲染
在这个过程中:
- 对于普通元素:调用
processElement
创建DOM元素 - 对于组件:调用
processComponent
创建组件实例 - 对于文本节点:直接创建文本节点
- 对于Fragment:处理其子节点
到这里,我们完成了组件层面的渲染流程分析,从组件实例的创建、初始化到渲染的整体过程已经清晰。但这只是第一层,我们还需要继续深入到元素层面。在下一篇文章中,我们将重点分析patch
函数中processElement
的具体实现,看看Vue是如何将虚拟DOM最终转换为真实DOM节点的。