zhangdizhangdi

渲染器

重点

  • 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

  1. 创建 VNode

h(Component, props, slots) 或模板编译产物(createVNode)。

VNode 上有:

  • type(组件/元素/Fragment)
  • props、key
  • shapeFlag(位掩码,标识“是组件还是元素、是否有插槽/子节点”等)
  1. patch(null, vnode, container)

首次渲染 n1=null → 分发到不同路径:

  • 元素:mountElement
  • 组件:mountComponent
  1. 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 阶段)

更新

触发条件

组件的渲染 effect 在运行时会收集依赖,以下变化会触发它的 scheduler 入队:

  • 本组件 state 改变:reactive/ref 的变更(track/trigger)
  • 父传的 props 改变(并且值确实变化)
  • 插槽内容变化(slot 是函数,依赖父作用域)
  • forceUpdate 场景:比如 shallowRef(obj) 替换 .value

更新过程(patch 的第二次及以后)

  1. 队列里执行 update()
    • 比较 n1 与 n2:若是组件 → updateComponent 判断是否需要更新(props/slots/patchFlag)。
    • 需要更新时:再次调用 render() → 得到 nextTree
    • patch(subTree, nextTree, container):对比新旧子树(Diff)
    • subTree = nextTree,调用 onUpdated(post 阶段)
  2. 元素节点的 Diff(核心在 children)
    • patchProps:只改动“变了的”属性/事件
    • patchChildren:文本/数组切换、无 key / 有 key 的 Diff
    • Keyed Diff:patchKeyedChildren(最常见)。
      • 头尾同步 → 中间对比 → 基于 key 的移动/挂载/卸载
      • 使用 LIS(最长递增子序列) 优化最少移动
  3. 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

  1. 头部同步 (sync from start)
  2. 尾部同步 (sync from end)
  3. 处理新增 (common sequence + mount)
  4. 删除节点 (common sequence + unmount)
  5. 最长递增子序列优化 (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 唯一稳定。

  1. 从头对齐:a[j].key === b[j].key → patch & j++ 直到不等。
  2. 从尾对齐:a[e1].key === b[e2].key → patch & e1–, e2–。
  3. 扫尾增删:
    • 若 j > e1(旧用尽)→ 将 b[j…e2] 挂载。
    • 若 j > e2(新用尽)→ 将 a[j…e1] 卸载。
  4. 处理中段(核心):
    • 建 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