本页目录
渲染器
- 渲染机制 - 官网中文
重点
- Renderer 渲染器的架构(平台无关)
- mount / patch / unmount 流程
- 虚拟 DOM(VNode)的创建与更新(createVNode、patchElement)
- Diff 算法实现(核心:patchKeyedChildren)
- 组件实例的创建(createComponentInstance)与生命周期调用
- 依赖响应式更新到视图的过程
VNode
shapeFlags
- shared/src/shapeFlags.ts
- 存在于 vnode.shapeFlag
- 类型:位运算标记(bitwise flags)
- 用来区分 VNode 属于哪种类型(元素?组件?fragment?)以及它的 children 是什么类型(文本?数组?slots?)
js
export enum ShapeFlags {
ELEMENT = 1, // 0001 1 普通元素
FUNCTIONAL_COMPONENT = 1 << 1, // 0010 2 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 0100 4 有状态组件
TEXT_CHILDREN = 1 << 3, // 1000 8 文本子节点
ARRAY_CHILDREN = 1 << 4, // 10000 16 数组子节点
SLOTS_CHILDREN = 1 << 5, // 100000 32 插槽子节点
TELEPORT = 1 << 6, // 1000000 64 teleport
SUSPENSE = 1 << 7, // 10000000 128 suspense
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 100000000 256 组件需要被缓存
COMPONENT_KEPT_ALIVE = 1 << 9, // 1000000000 512 组件已经被缓存
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
}
示例:
ts
// <div>hello</div>
vnode.shapeFlag = ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
// <MyComp><p>123</p></MyComp>
vnode.shapeFlag = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.ARRAY_CHILDREN
patchFlags
- shared/src/patchFlags.ts
- 存在于 VNode.patchFlag
- 编译器生成 render 时
- 标识该节点哪些部分需要 diff,哪些部分可以跳过;运行时 patchElement 时会参考它来做 静态提升 / 快速更新
js
export enum PatchFlags {
TEXT = 1, // 动态文本节点 0001 1
CLASS = 1 << 1, // 动态 class 0010 2
STYLE = 1 << 2, // 动态 style 0100 4
PROPS = 1 << 3, // 动态 props (非 class/style) 1000 8
FULL_PROPS = 1 << 4, // 有 key,需完整 diff props 10000 16
NEED_HYDRATION = 1 << 5, // 100000 32
STABLE_FRAGMENT = 1 << 6, // 子节点顺序稳定 1000000 64
KEYED_FRAGMENT = 1 << 7, // 子节点带 key 10000000 128
UNKEYED_FRAGMENT = 1 << 8, // 子节点无 key 100000000 256
NEED_PATCH = 1 << 9, // 需要强制触发更新 1000000000 512
DYNAMIC_SLOTS = 1 << 10, // 动态插槽 10000000000 1024
DEV_ROOT_FRAGMENT = 1 << 11, // 开发环境根节点 100000000000 2048
CACHED = -1, // 静态提升,不需要 diff
BAIL = -2 // 退出优化,走完整 diff
}
js
// TEXT = 1
;<div>{{ msg }}</div>
// 编译后:
createElementVNode('div', null, toDisplayString(_ctx.msg), 1 /* TEXT */)
js
// CLASS = 1 << 1 = 2
<div :class="cls">Hi</div>
// 编译后:
createElementVNode("div", { class: normalizeClass(_ctx.cls) }, "Hi", 2 /* CLASS */)
js
// STYLE = 1 << 2 = 4
<div :style="{ color: color }"></div>
// 编译后:
createElementVNode("div", { style: normalizeStyle({ color: _ctx.color }) }, null, 4 /* STYLE */))
js
// PROPS = 1 << 3 = 8
<div :id="id" :title="title"></div>
// 编译后:
const _hoisted_1 = ["id", "title"]
createElementVNode("div", { id: _ctx.id, title: _ctx.title }, null, 8 /* PROPS */, _hoisted_1)
js
// FULL_PROPS = 1 << 4 = 16
;<div v-bind="obj"></div>
// 因为 obj 可能新增或删除属性,必须完整比对。
createElementBlock(
'div',
normalizeProps(_guardReactiveProps(_ctx.obj)),
null,
16 /* FULL_PROPS */,
)
js
// ⌛️ NEED_HYDRATION = 1 << 5 = 32
js
// STABLE_FRAGMENT = 1 << 6 = 64
;<template v-for="i in 3">
<span>{{ i }}</span>
</template>
// 编译后:
_createElementBlock(
_Fragment,
null,
_renderList(3, i => {
return _createElementVNode('span', null, _toDisplayString(i), 1 /* TEXT */)
}),
64 /* STABLE_FRAGMENT */,
)
js
// KEYED_FRAGMENT = 1 << 7 = 128
<ul>
<li v-for="item in list" :key="item.id">{{ item.text }}</li>
</ul>
// 编译后:
_createElementBlock("ul", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item) => {
return (_openBlock(), _createElementBlock("li", {
key: item.id
}, _toDisplayString(item.text), 1 /* TEXT */))
}), 128 /* KEYED_FRAGMENT */)
js
// UNKEYED_FRAGMENT = 1 << 8 = 256
<ul>
<li v-for="item in list">{{ item.text }}</li>
</ul>
// 编译后:
_createElementBlock("ul", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item) => {
return (_openBlock(), _createElementBlock("li", null, _toDisplayString(item.text), 1 /* TEXT */))
}), 256 /* UNKEYED_FRAGMENT */))
])
js
// NEED_PATCH = 1 << 9 = 512
// VNode 必须被标记,以保证这些副作用执行。
;<div ref="el"></div>
// 编译后:
const _hoisted_1 = { ref: 'el' }
_createElementBlock('div', _hoisted_1, null, 512 /* NEED_PATCH */)
js
// DYNAMIC_SLOTS = 1 << 10 = 1024
<MyComp>
<template v-slot:[name]>Hello</template>
</MyComp>
// 编译后:
const _component_MyComp = _resolveComponent("MyComp")
return (_openBlock(), _createBlock(_component_MyComp, null, {
[_ctx.name]: _withCtx(() => [...(_cache[0] || (_cache[0] = [
_createTextVNode("Hello", -1 /* CACHED */)
]))]),
_: 2 /* DYNAMIC */
}, 1024 /* DYNAMIC_SLOTS */))
首次渲染
App VNode → mountComponent → setup → render() 得到 subTree → patch 子树 → DOM
- 创建 VNode
h(Component, props, slots) 或模板编译产物(createVNode)。
VNode 上有:
- type(组件/元素/Fragment)
- props、key
- shapeFlag(位掩码,标识“是组件还是元素、是否有插槽/子节点”等)
- patch(null, vnode, container)
首次渲染 n1=null → 分发到不同路径:
- 元素:mountElement
- 组件:mountComponent
- mountComponent(最重要)
- createComponentInstance(vnode, parent):建实例(instance),挂 props/slots/ctx/emit 等。
- setupComponent(instance):解析 props/slots,执行 setup()(得到 setupState 和生命周期注册)。
- finishComponentSetup:保证有 render(来自 setup 或编译的模板)。
- setupRenderEffect(instance, initialVNode, container, anchor):创建一个 ReactiveEffect,其 scheduler = queueJob(update)。
- 首次运行 update():
- subTree = render.call(proxy, proxy) 产生子树 VNode
- patch(null, subTree, container) 把子树挂到 DOM
- 调用 mounted/onMounted(在 post 阶段)
- 首次运行 update():
更新
触发条件
组件的渲染 effect 在运行时会收集依赖,以下变化会触发它的 scheduler 入队:
- 本组件 state 改变:reactive/ref 的变更(track/trigger)
- 父传的 props 改变(并且值确实变化)
- 插槽内容变化(slot 是函数,依赖父作用域)
- forceUpdate 场景:比如 shallowRef(obj) 替换 .value
更新过程(patch 的第二次及以后)
- 队列里执行 update()
- 比较 n1 与 n2:若是组件 → updateComponent 判断是否需要更新(props/slots/patchFlag)。
- 需要更新时:再次调用 render() → 得到 nextTree
- patch(subTree, nextTree, container):对比新旧子树(Diff)
- subTree = nextTree,调用 onUpdated(post 阶段)
- 元素节点的 Diff(核心在 children)
- patchProps:只改动“变了的”属性/事件
- patchChildren:文本/数组切换、无 key / 有 key 的 Diff
- Keyed Diff:patchKeyedChildren(最常见)。
- 头尾同步 → 中间对比 → 基于 key 的移动/挂载/卸载
- 使用 LIS(最长递增子序列) 优化最少移动
- PatchFlag(编译期优化)
- 编译器在模板里为“动态节点”打标(如 TEXT/CLASS/PROPS/KEYED_FRAGMENT),运行时只检查这些点,跳过静态区。
diff 算法
js
processFragment{}
patchElement{
patchBlockChildren
patchChildren{
patchKeyedChildren...
patchUnkeyedChildren...
}
}
元素更新走 patchElement → patchChildren。当 新旧都是数组子节点 时进入“列表 diff”。
- Unkeyed:按索引一对一补丁 → 多余卸载 / 不足挂载,不做移动。
- Keyed:四步法(头对齐→尾对齐→扫尾增删→中段最少移动(LIS))。
- Block & PatchFlags:编译器把“动态片段”打标,只对有标记的地方进行 diff,跳过静态区;同时给出“用 keyed / unkeyed 的强提示”。
UnkeyedChildren
KeyedChildren
- 头部同步 (sync from start)
- 尾部同步 (sync from end)
- 处理新增 (common sequence + mount)
- 删除节点 (common sequence + unmount)
- 最长递增子序列优化 (unknown sequence)
js
1. sync from start
// (a b) c
// (a b) d e
2. sync from end
// a (b c)
// d e (b c)
3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
设旧列表 a[j…e1]、新列表 b[j…e2],key 唯一稳定。
- 从头对齐:a[j].key === b[j].key → patch & j++ 直到不等。
- 从尾对齐:a[e1].key === b[e2].key → patch & e1–, e2–。
- 扫尾增删:
- 若 j > e1(旧用尽)→ 将 b[j…e2] 挂载。
- 若 j > e2(新用尽)→ 将 a[j…e1] 卸载。
- 处理中段(核心):
- 建 newKeyIndexMap 映射(新列表 key → 新索引)。
- 遍历旧中段:
- 新中段找不到 → 卸载;
- 找到 → patch 并记录“旧索引对应的新索引”。
- 对“新索引序列”做 LIS(最长递增子序列),LIS 内的保持相对顺序,其余执行移动(从后往前插入,避免锚点干扰)。
Block / PatchFlags(编译期优化)
- 编译器在模板里给动态节点或片段打 patchFlag,并把这些动态子节点收集进“block”的 dynamicChildren。
- 运行时 patch 时:
- 若有 dynamicChildren:只遍历这些做补丁(如 TEXT、CLASS、PROPS)。
- fragment 上的 flag 决定 children diff 策略:
- KEYED_FRAGMENT → 走 keyed diff
- UNKEYED_FRAGMENT → 走 unkeyed diff
- STABLE_FRAGMENT → 结构稳定,仅微小变动
最长递增子序列
js
function getSequence(arr) {
const p = arr.slice() // 复制一份,用来存储前驱索引
const result = [0] // 保存 LIS 的下标序列,初始只有第0个元素
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
console.log('♻️ -------')
console.log('值 arrI:', arrI)
if (arrI !== 0) {
j = result[result.length - 1] // 当前 LIS 的最后一个索引
console.log('索引 j:', j)
console.log('值 arr[j]:', arr[j])
if (arr[j] < arrI) {
// arrI 比当前 LIS 的最后一个值大,直接接在后面
p[i] = j
result.push(i)
console.log('🍒')
console.log('result[]:', result, result.toString())
console.log('p[]:', p, p.toString())
continue
}
// 否则,用二分查找找到它应该替换的位置
u = 0
v = result.length - 1
console.log('二分查找,u:', u, 'v:', v)
while (u < v) {
c = (u + v) >> 1
console.log('u:', u, 'v:', v, 'c:', c)
console.log('result[c]:', result[c], ' arr[result[c]]:', arr[result[c]])
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
console.log('二分查找后,u:', u, 'v:', v)
console.log('result[u]:', result[u], ' arr[result[u]]:', arr[result[u]])
// 找到比 arrI 大的第一个位置,替换成当前索引 i
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
console.log('🍒')
console.log('result[]:', result, result.toString())
console.log('p[]:', p, p.toString())
}
}
console.log('🌹 -------')
console.log('result[]:', result, result.toString())
console.log('p[]:', p, p.toString())
console.log('🌹 -------')
u = result.length
v = result[u - 1]
console.log('u:', u, 'v', v)
while (u-- > 0) {
console.log('while,u:', u, 'v', v)
result[u] = v
v = p[v]
console.log('white, result[]:', result, result.toString())
}
return result
}
// const res = getSequence([5, 3, 4, 0])
const res = getSequence([3, 5, 6, 2, 5, 4, 19, 5])
console.log(res)
执行结果
♻️ -------
值 arrI: 3
索引 j: 0
值 arr[j]: 3
二分查找,u: 0 v: 0
二分查找后,u: 0 v: 0
result[u]: 0 arr[result[u]]: 3
🍒
result[]: [ 0 ] 0
p[]: [
3, 5, 6, 2,
5, 4, 19, 5
] 3,5,6,2,5,4,19,5
♻️ -------
值 arrI: 5
索引 j: 0
值 arr[j]: 3
🍒
result[]: [ 0, 1 ] 0,1
p[]: [
3, 0, 6, 2,
5, 4, 19, 5
] 3,0,6,2,5,4,19,5
♻️ -------
值 arrI: 6
索引 j: 1
值 arr[j]: 5
🍒
result[]: [ 0, 1, 2 ] 0,1,2
p[]: [
3, 0, 1, 2,
5, 4, 19, 5
] 3,0,1,2,5,4,19,5
♻️ -------
值 arrI: 2
索引 j: 2
值 arr[j]: 6
二分查找,u: 0 v: 2
u: 0 v: 2 c: 1
result[c]: 1 arr[result[c]]: 5
u: 0 v: 1 c: 0
result[c]: 0 arr[result[c]]: 3
二分查找后,u: 0 v: 0
result[u]: 0 arr[result[u]]: 3
🍒
result[]: [ 3, 1, 2 ] 3,1,2
p[]: [
3, 0, 1, 2,
5, 4, 19, 5
] 3,0,1,2,5,4,19,5
♻️ -------
值 arrI: 5
索引 j: 2
值 arr[j]: 6
二分查找,u: 0 v: 2
u: 0 v: 2 c: 1
result[c]: 1 arr[result[c]]: 5
u: 0 v: 1 c: 0
result[c]: 3 arr[result[c]]: 2
二分查找后,u: 1 v: 1
result[u]: 1 arr[result[u]]: 5
🍒
result[]: [ 3, 1, 2 ] 3,1,2
p[]: [
3, 0, 1, 2,
5, 4, 19, 5
] 3,0,1,2,5,4,19,5
♻️ -------
值 arrI: 4
索引 j: 2
值 arr[j]: 6
二分查找,u: 0 v: 2
u: 0 v: 2 c: 1
result[c]: 1 arr[result[c]]: 5
u: 0 v: 1 c: 0
result[c]: 3 arr[result[c]]: 2
二分查找后,u: 1 v: 1
result[u]: 1 arr[result[u]]: 5
🍒
result[]: [ 3, 5, 2 ] 3,5,2
p[]: [
3, 0, 1, 2,
5, 3, 19, 5
] 3,0,1,2,5,3,19,5
♻️ -------
值 arrI: 19
索引 j: 2
值 arr[j]: 6
🍒
result[]: [ 3, 5, 2, 6 ] 3,5,2,6
p[]: [
3, 0, 1, 2,
5, 3, 2, 5
] 3,0,1,2,5,3,2,5
♻️ -------
值 arrI: 5
索引 j: 6
值 arr[j]: 19
二分查找,u: 0 v: 3
u: 0 v: 3 c: 1
result[c]: 5 arr[result[c]]: 4
u: 2 v: 3 c: 2
result[c]: 2 arr[result[c]]: 6
二分查找后,u: 2 v: 2
result[u]: 2 arr[result[u]]: 6
🍒
result[]: [ 3, 5, 7, 6 ] 3,5,7,6
p[]: [
3, 0, 1, 2,
5, 3, 2, 5
] 3,0,1,2,5,3,2,5
🌹 -------
result[]: [ 3, 5, 7, 6 ] 3,5,7,6
p[]: [
3, 0, 1, 2,
5, 3, 2, 5
] 3,0,1,2,5,3,2,5
🌹 -------
u: 4 v 6
while,u: 3 v 6
white, result[]: [ 3, 5, 7, 6 ] 3,5,7,6
while,u: 2 v 2
white, result[]: [ 3, 5, 2, 6 ] 3,5,2,6
while,u: 1 v 1
white, result[]: [ 3, 1, 2, 6 ] 3,1,2,6
while,u: 0 v 0
white, result[]: [ 0, 1, 2, 6 ] 0,1,2,6
[ 0, 1, 2, 6 ]
i | arr[i] | p[i] | result(索引) | result对应值 |
---|---|---|---|---|
0 | 3 | 不变 | [0] | [3] |
1 | 5 | 0 | [0, 1] | [3, 5] |
2 | 6 | 1 | [0, 1, 2] | [3, 5, 6] |
3 | 2 | 不变 | [3, 1 ,2] | [2, 5, 6] |
4 | 5 | 不变 | [3, 1, 2] | [2, 5, 6] |
5 | 4 | 3 | [3, 5, 2] | [2, 4, 6] |
6 | 19 | 2 | [3, 5, 2, 6] | [2, 4, 6, 19] |
7 | 5 | 5 | [3, 5, 7, 6] | [2, 4, 5, 19] |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
arr | 3 | 5 | 6 | 2 | 5 | 4 | 19 | 5 |
p | 3 | 0 |
1 |
2 | 5 | 3 |
2 |
5 |