解析 Vue 3 中 trigger
函数是如何触发 DOM 更新的整个流程。
1. trigger 函数的实现
typescript:packages/reactivity/src/effect.ts
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 获取对象的依赖映射
const depsMap = targetMap.get(target)
if (!depsMap) return
// 收集需要触发的 effects
const effects = new Set<ReactiveEffect>()
// 添加 effects 到集合中
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
// 避免自触发
if (effect !== activeEffect) {
effects.add(effect)
}
})
}
}
// 1. 处理特定属性的依赖
if (key !== void 0) {
add(depsMap.get(key))
}
// 2. 处理数组长度变化
if (type === TriggerOpTypes.ADD && isArray(target)) {
add(depsMap.get('length'))
}
// 3. 执行收集到的 effects
effects.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
})
}
2. DOM 更新的完整流程
第一阶段:触发响应式更新
- 数据变更触发 trigger
typescript
// 当响应式数据发生变化时
state.count = 2
- 收集相关 effects
typescript
const effects = new Set<ReactiveEffect>()
// 从 targetMap 中找到相关的依赖并添加到 effects 中
add(depsMap.get(key))
第二阶段:调度更新
- 调度器介入
typescript
effects.forEach(effect => {
if (effect.scheduler) {
// 组件更新会走调度器
effect.scheduler()
} else {
// 普通 effect 直接运行
effect.run()
}
})
- 进入调度队列
typescript:packages/runtime-core/src/scheduler.ts
function queueJob(job: SchedulerJob) {
if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
// 将更新任务加入队列
queue.push(job)
queueFlush()
}
}
第三阶段:组件更新
- 组件渲染 effect 执行
typescript:packages/runtime-core/src/renderer.ts
const setupRenderEffect = (
instance: ComponentInternalInstance,
initialVNode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentSuspense: null | SuspenseBoundary,
namespace: ElementNamespace,
optimized: boolean
) => {
// 创建渲染 effect
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 首次挂载
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(null, subTree, container, anchor, instance, parentSuspense, namespace)
instance.isMounted = true
} else {
// 更新
let { next, vnode } = instance
// 更新组件实例
if (next) {
updateComponentPreRender(instance, next, optimized)
}
// 重新渲染
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// 执行 patch 更新 DOM
patch(
prevTree,
nextTree,
hostParentNode(prevTree.el!)!,
getNextHostNode(prevTree),
instance,
parentSuspense,
namespace
)
}
}
}
第四阶段:DOM 更新
- patch 过程
typescript:packages/runtime-core/src/renderer.ts
const patch = (
n1: VNode | null, // 旧 vnode
n2: VNode, // 新 vnode
container: RendererElement,
anchor: RendererNode | null = null,
parentComponent: ComponentInternalInstance | null = null,
parentSuspense: SuspenseBoundary | null = null,
namespace: ElementNamespace = SVGNamespace,
optimized = false
) => {
// 1. 处理不同类型的节点
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, optimized)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, optimized)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, optimized)
}
}
}
- 实际 DOM 操作
typescript
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
optimized: boolean
) => {
if (n1 === null) {
// 挂载新元素
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
optimized
)
} else {
// 更新已有元素
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
namespace,
optimized
)
}
}
总结:完整的更新链路
-
触发阶段
- 响应式数据变更
- trigger 函数被调用
- 收集相关的副作用函数
-
调度阶段
- 更新任务进入调度队列
- 通过 Promise.then 等待下一个微任务
- 批量处理更新任务
-
渲染阶段
- 执行组件的更新函数
- 生成新的虚拟 DOM 树
- 对比新旧虚拟 DOM
-
提交阶段
- patch 过程对比差异
- 执行实际的 DOM 操作
- 更新完成
这个过程是 Vue 3 响应式系统和渲染系统协同工作的结果,通过精心设计的调度系统,确保了更新的高效性和可控性。