第28章 虚拟 DOM 原理

第二十八章 虚拟 DOM 原理

虚拟 DOM(vDOM)是 Vue 和 React 等现代前端框架的核心概念之一。它是一种用 JavaScript 对象描述真实 DOM 的技术,可以让框架在内存中高效地计算和更新 DOM。有人说虚拟 DOM 慢,真的是这样吗?本章我们就来揭开它的神秘面纱。

28.1 虚拟 DOM 概述

28.1.1 什么是虚拟 DOM

虚拟 DOM(Virtual Document Object Model)是用普通的 JavaScript 对象来表示 DOM 结构的一种技术:

flowchart LR
    subgraph RealDOM["真实 DOM"]
        A["<div class='container'>"]
        A --> B["<h1>标题</h1>"]
        A --> C["<p>内容</p>"]
    end
    
    subgraph VirtualDOM["虚拟 DOM (vnode)"]
        A2["{ type: 'div', props: { class: 'container' } }"]
        A2 --> B2["{ type: 'h1', children: '标题' }"]
        A2 --> C2["{ type: 'p', children: '内容' }"]
    end
    
    RealDOM <-->|"映射"| VirtualDOM
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 虚拟 DOM 节点(vnode)示例
const vnode = {
  type: 'div',
  props: {
    class: 'container',
    id: 'app',
    onClick: () => console.log('clicked'),
  },
  children: [
    {
      type: 'h1',
      props: {},
      children: 'Hello Virtual DOM',
    },
    {
      type: 'button',
      props: { class: 'btn' },
      children: 'Click me',
    },
  ],
}

28.1.2 为什么需要虚拟 DOM

直接操作真实 DOM 不是更快吗?让我们分析一下:

flowchart TD
    A[数据变化] --> B[直接操作 DOM]
    A --> C[虚拟 DOM]
    
    B --> B1[更新 DOM 1]
    B --> B2[更新 DOM 2]
    B --> B3[更新 DOM 3]
    
    C --> C1[生成新 vnode]
    C1 --> C2[Diff 对比]
    C2 --> C3[最小化 DOM 操作]
    
    B1 --> B4[多次 reflow/repaint]
    B2 --> B4
    B3 --> B4
    
    C3 --> C4[批量更新,更少的 reflow/repaint]
操作方式优点缺点
直接操作 DOM简单直接容易触发多次 reflow/repaint
虚拟 DOM批量更新,最小化操作有额外的 JavaScript 计算开销

什么是 reflow 和 repaint? 这是浏览器渲染引擎的两次重要计算:

  • reflow(回流):当元素的位置或尺寸发生变化时,浏览器需要重新计算整个页面布局。比如把一个 div 的宽度从 100px 改成 200px,浏览器要重新算所有元素的位置——这是一个非常"贵"的操作。
  • repaint(重绘):当元素的外观变化但不影响布局时(比如改了颜色、背景图),浏览器只需要重新绘制这些像素,比较便宜。

为什么直接操作 DOM 容易触发多次 reflow?因为你可能连续改了 3 次元素的样式,每次改都会触发一次 reflow。如果用虚拟 DOM,Vue 会把这 3 次改动"攒起来",算出最小的变更方案,只触发一次 reflow。

28.1.3 Vue 3 虚拟 DOM 的优势

Vue 3 对虚拟 DOM 做了大量优化:

  • 编译时优化:静态节点提升,减少比对
  • PatchFlag:标记动态内容,快速定位
  • 缓存事件处理器:避免不必要的更新
  • Block:更智能的收集动态节点

28.2 vnode 结构解析

28.2.1 vnode 的类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// packages/runtime-core/src/vnode.ts

// vnode 类型
export const enum VNodeTypes {
  ELEMENT,     // 普通 HTML 元素
  TEXT,        // 文本节点
  COMMENT,     // 注释节点
  FRAGMENT,    // 片段(多个根节点)
  COMPONENT,   // 组件
  TELEPORT,   // Teleport 组件
  SUSPENSE,   // Suspense 组件
}

// 插槽类型
export const enum SlotsTypes {
  DEFAULT,    // 默认插槽
  NAMED,       // 具名插槽
  CONDITIONAL, // 条件插槽
}

// vnode 的完整结构
interface VNode<
  HostNode = any,
  HostElement = any,
  ExtraProps = any
> {
  // 节点类型
  type: VNodeTypes | string | Component
  
  // DOM 相关
  el: HostNode | null          // 指向真实 DOM 节点
  anchor: HostNode | null      // 用于 Fragment 的定位
  
  // key,用于 diff 算法
  key: string | number | symbol | null
  
  // 节点名称(用于调试)
  name?: string
  
  // 属性
  props: VNodeProps & ExtraProps
  
  // 子节点
  children: VNodeNormalizedChildren
  
  // 组件实例
  component: ComponentInternalInstance | null
  
  // DOM 元素
  target: HostElement | null
  
  // 特效类型
  teleport: TeleportVNode | null
  
  // suspense
  suspense: SuspenseBoundary | null
  
  // 其他
  shapeFlag: number             // 形状标记
  patchFlag: number             // patch 标记
  dirs: VNodeDirs | null        // 指令
  scopeId: string | null
}

28.2.2 shapeFlag 详解

shapeFlag 是 vnode 的形状标记,用于快速判断 vnode 的特征:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// packages/shared/src/shapeFlags.ts

export const enum ShapeFlags {
  // 元素类型
  ELEMENT = 1,                  // 1 << 0 = 1
  TEXT = 1 << 1,               // 2
  COMMENT = 1 << 2,            // 4
  FRAGMENT = 1 << 3,           // 8
  
  // 组件类型
  COMPONENT = 1 << 4,          // 16
  COMPONENT_KEPT_ALIVE = 1 << 5, // 32
  COMPONENT_ON_VISIBLE = 1 << 6, // 64
  
  // 状态
  TELEPORT = 1 << 7,           // 128
  SUSPENSE = 1 << 8,           // 256
  KEEP_ALIVE = 1 << 9,         // 512
  
  // children 类型
  ARRAY_CHILDREN = 1 << 10,    // 1024
  TEXT_CHILDREN = 1 << 11,      // 2048
  SLOTS_CHILDREN = 1 << 12,     // 4096
  TELEPORT_CHILDREN = 1 << 13,  // 8192
  SUSPENSE_CHILDREN = 1 << 14,  // 16384
}

// 使用位运算快速判断
function render(vnode: VNode) {
  // 判断是否是组件
  if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
    // 处理组件
  }
  
  // 判断是否有数组类型的 children
  if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 处理数组 children
  }
  
  // 判断是否是元素且有文本 children
  if (
    (vnode.shapeFlag & ShapeFlags.ELEMENT) &&
    (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN)
  ) {
    // 处理文本 children
  }
}

28.2.3 创建一个 vnode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 创建一个 div 元素的 vnode
const vnode = h('div', {
  class: 'container',
  id: 'app',
  onClick: handleClick,
}, [
  h('h1', { class: 'title' }, 'Hello Vue'),
  h('p', 'This is a paragraph'),
])

// h 函数的类型签名
// h 函数是 Vue 中创建 vnode 的核心函数
function h(
  type: string | Component,      // 元素类型或组件
  props?: VNodeProps | null,      // 属性
  children?: VNodeNormalizedChildren // 子节点
): VNode

// 特殊用法
h('div', {
  class: 'wrapper',
  style: { color: 'red' },
  'data-id': '1',                // HTML 属性
  attrs: { title: 'tooltip' },  // attributes
  on: { click: handler },       // 事件
  props: { value: 'test' },     // DOM properties
  // ...
})

// children 的多种写法
h('ul', [
  h('li', 'item 1'),
  h('li', 'item 2'),
  h('li', 'item 3'),
])

// 或者字符串(自动包装为文本节点)
h('p', '这是一段文本')

// 或者数字
h('span', 42)

// 或者混合
h('div', [
  '文字',
  h('strong', '加粗'),
  null, // null 不渲染
  undefined, // undefined 也不渲染
])

28.3 h 函数详解

28.3.1 h 函数的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// packages/runtime-core/src/h.ts

// 简化版的 h 函数实现
function h(type: any, propsAndChildren: any): VNode {
  // 从参数中分离 props 和 children
  const l = arguments.length
  
  if (l === 2) {
    // h('div', { class: 'container' })
    // h('div', children)
    // h('div', null)
    if (propsAndChildren != null && !isArray(propsAndChildren)) {
      // 如果第二个参数不是数组,它是 props 或 children
      if (isObject(propsAndChildren)) {
        // props
        return createVNode(type, propsAndChildren)
      } else {
        // children(文本或其他原始值)
        return createVNode(type, null, propsAndChildren)
      }
    } else {
      // children 是数组
      return createVNode(type, null, propsAndChildren)
    }
  } else if (l > 2) {
    // h('div', props, child1, child2, ...)
    const props = arguments[1]
    const children = Array.prototype.slice.call(arguments, 2)
    return createVNode(type, props, children)
  }
}

// createVNode 的简化实现
function createVNode(
  type: string | Component,
  props: any,
  children: any
): VNode {
  // 处理 class 和 style(可能传入字符串或数组)
  if (props) {
    // normalizeClass: 统一 class 格式
    // normalizeStyle: 统一 style 格式
  }
  
  // 创建 vnode 对象
  const vnode: VNode = {
    type,
    props,
    children,
    key: props?.key || null,
    // ... 其他属性
  }
  
  // 根据 children 类型设置 shapeFlag
  if (isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
  } else if (isString(children) || isNumber(children)) {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
    children = String(children)
  }
  
  return vnode
}

28.3.2 动态子节点的处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!-- Vue 3 模板中动态子节点的处理 -->
<template>
  <!-- 静态子节点提升到渲染函数外部 -->
  <div class="static">
    <h1>静态标题</h1>
    <p>静态内容</p>
  </div>
  
  <!-- 动态子节点 -->
  <div class="dynamic">
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </div>
  
  <!-- 条件渲染 -->
  <div v-if="show">
    <p>条件内容</p>
  </div>
  
  <!-- 列表渲染 -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>

<!-- 编译后的渲染函数 -->
function render(ctx) {
  // 静态部分在每次渲染时复用
  const _hoisted_1 = h('h1', { class: 'static' }, '静态标题')
  const _hoisted_2 = h('p', '静态内容')
  
  return h('div', [
    // 静态提升
    h('div', { class: 'static' }, [_hoisted_1, _hoisted_2]),
    
    // 动态部分
    h('div', { class: 'dynamic' }, [
      ctx.title,  // 动态插值
      ctx.content,
    ]),
    
    // 条件
    ctx.show
      ? h('div', [h('p', '条件内容')])
      : createCommentVNode('v-if', true),
    
    // 列表
    ctx.items.map(item => 
      h('li', { key: item.id }, item.name)
    ),
  ])
}

28.4 patch 算法详解

28.4.1 patch 的基本流程

flowchart TD
    A[new vnode] --> B[sameVnode?]
    B -->|是| C[patchProps]
    B -->|否| D[卸载旧节点]
    D --> E[挂载新节点]
    C --> F[patch children]
    E --> G[完成]
    F --> B
    
    subgraph sameVnode["sameVnode 判断"]
    B1[比较 type] --> B2[type 不同?]
    B2 -->|是| B3[返回 false]
    B2 -->|否| B4[比较 key]
    B4 --> B5[key 不同?]
    B5 -->|是| B3
    B5 -->|否| B6[返回 true]
    end

28.4.2 sameVnode 的判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// packages/runtime-core/src/vnode.ts

function sameVnode(a: VNode, b: VNode): boolean {
  return (
    a.key === b.key &&            // key 必须相同
    a.type === b.type &&          // type 必须相同
    a.shapeFlag === b.shapeFlag   // shapeFlag 应该相同
  )
}

// 简化版
function sameVnode(oldNode, newNode) {
  return oldNode.key === newNode.key &&
         oldNode.type === newNode.type
}

// 对于元素节点,key 和 type 相同就认为是同一节点
// 对于组件节点,还需要比较组件实例

28.4.3 children diff 算法

Vue 3 采用了更高效的 diff 算法,主要优化点:

flowchart LR
    subgraph Old["旧 children"]
        O1["A"]
        O2["B"]
        O3["C"]
        O4["D"]
    end
    
    subgraph New["新 children"]
        N1["B"]
        N2["A"]
        N3["D"]
        N4["C"]
        N5["E"]
    end
    
    Old -->|最优复用策略| New
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// packages/runtime-core/src/renderer.ts

// patch children 的核心逻辑
function patchChildren(
  oldNode: VNode,
  newNode: VNode,
  container: HostElement
) {
  const { children: oldChildren } = oldNode
  const { children: newChildren } = newNode
  
  // 几种情况
  // 1. 新 children 是文本
  if (isText(newChildren)) {
    if (!isText(oldChildren)) {
      // 旧 children 不是文本,直接替换
      hostSetElementText(container, newChildren)
    } else {
      // 都是文本,只更新文本
      if (oldChildren !== newChildren) {
        hostSetElementText(container, newChildren)
      }
    }
  } else {
    // 2. 新 children 是数组
    if (isArray(oldChildren)) {
      // 都是数组,使用双端 diff
      patchKeyedChildren(oldChildren, newChildren, container)
    } else {
      // 3. 旧 children 是文本,新 children 是数组
      // 先清空文本,再挂载新 children
      hostSetElementText(container, '')
      mountChildren(newChildren, container)
    }
  }
}

// 双端 diff 算法
function patchKeyedChildren(
  oldChildren: VNode[],
  newChildren: VNode[],
  container: HostElement
) {
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]
  
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (!oldStartVNode) {
      // 被移动的元素(key 为 undefined 时可能发生)
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (!oldEndVNode) {
      oldEndVNode = oldChildren[--oldEndIdx]
    } else if (sameVnode(oldStartVNode, newStartVNode)) {
      // 头头相同,patch
      patch(oldStartVNode, newStartVNode)
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (sameVnode(oldEndVNode, newEndVNode)) {
      // 尾尾相同,patch
      patch(oldEndVNode, newEndVNode)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (sameVnode(oldStartVNode, newEndVNode)) {
      // 头尾相同,说明 oldStart 移动到了 newEnd 位置
      // 需要移动 DOM 节点
      patch(oldStartVNode, newEndVNode)
      hostInsert(
        oldStartVNode.el!,
        container,
        hostNextSibling(oldEndVNode.el!)!
      )
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (sameVnode(oldEndVNode, newStartVNode)) {
      // 尾头相同,说明 oldEnd 移动到了 newStart 位置
      patch(oldEndVNode, newStartVNode)
      hostInsert(
        oldEndVNode.el!,
        container,
        oldStartVNode.el!
      )
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else {
      // 没有匹配,使用 Map 查找
      const keyToOldIndex = new Map()
      for (let i = oldStartIdx; i <= oldEndIdx; i++) {
        const key = oldChildren[i].key
        if (key != null) {
          keyToOldIndex.set(key, i)
        }
      }
      
      // 在旧 children 中找新 children[0]
      const index = keyToOldIndex.get(newStartVNode.key)
      if (index != null) {
        // 找到了,移动到正确位置
        const toMoveNode = oldChildren[index]
        patch(toMoveNode, newStartVNode)
        hostInsert(toMoveNode.el!, container, oldStartVNode.el!)
        oldChildren[index] = undefined as any
      } else {
        // 没找到,是新增节点
        mount(newStartVNode, container, oldStartVNode.el!)
      }
      
      newStartVNode = newChildren[++newStartIdx]
    }
  }
  
  // 处理剩余情况
  if (oldStartIdx > oldEndIdx) {
    // 旧 children 已遍历完,新 children 还有剩余
    // -> 新增
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      mount(newChildren[i], container, oldStartVNode.el)
    }
  } else if (newStartIdx > newEndIdx) {
    // 新 children 已遍历完,旧 children 还有剩余
    // -> 删除
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      unmount(oldChildren[i])
    }
  }
}

28.5 编译优化详解

28.5.1 静态节点提升

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- 模板 -->
<template>
  <div>
    <!-- 这个 div 及其子节点是静态的只创建一次 -->
    <div class="static-content">
      <h1>静态标题</h1>
      <p>静态内容 {{ count }} </p>
    </div>
    
    <!-- 动态部分 -->
    <div>{{ dynamicContent }}</div>
  </div>
</template>

<!-- 编译后的 render 函数 -->
function render(ctx) {
  // 静态内容提升到函数外部
  const _hoisted_1 = h('div', { class: 'static-content' }, [
    h('h1', '静态标题'),
    h('p', ['静态内容 ', ctx.count, ' 次']),
  ])
  
  return h('div', [_hoisted_1, ctx.dynamicContent])
}

// _hoisted_1 在组件实例创建时生成一次
// 之后每次渲染都复用这个 vnode
// 只需要 patch 内部可能变化的部分

28.5.2 PatchFlag(虚拟 DOM 标记)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!-- 模板 -->
<div class="container" id="app">
  <span>{{ name }}</span>
  <p>{{ age }}</p>
  <div>{{ address }}</div>
  <span>{{ nickname }}</span>
</div>

<!-- 编译后 -->
const vnode = h('div', {
  class: 'container',
  id: 'app',
}, [
  // PatchFlag 1 = TEXT,表示只有文本内容是动态的
  h('span', { patchFlag: 1 /* TEXT */ }, name),
  h('p', { patchFlag: 1 /* TEXT */ }, age),
  h('div', { patchFlag: 1 /* TEXT */ }, address),
  h('span', { patchFlag: 1 /* TEXT */ }, nickname),
])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// packages/shared/src/patchFlags.ts

export const enum PatchFlags {
  // 文本内容变化
  TEXT = 1,              // 1 << 0
  
  // class 变化
  CLASS = 1 << 1,        // 2
  
  // style 变化
  STYLE = 1 << 2,       // 4
  
  // props 变化(不包括 class、style、事件)
  PROPS = 1 << 3,        // 8
  
  // 完整 props(包括 class、style)
  FULL_PROPS = 1 << 4,  // 16
  
  // 事件变化
  EVENT = 1 << 5,       // 32
  
  // 需要止血(止血是什么?后面解释)
  NEED_HYDRATION = 1 << 6, // 64
  
  // 顺序变化(stable sequence 或 key)
  KEYED = 1 << 7,        // 128
  
  // 无 key 的顺序变化
  UNKEYED = 1 << 8,      // 256
  
  // 需要创建新的 vnode
  CREATE = 1 << 9,       // 512
  
  // 动态节点(标记为一个 block 的所有动态节点)
  DYNAMIC = 1 << 10,     // 1024
}

// patch 时的优化
function patchElement(oldNode, newNode) {
  const el = oldNode.el
  
  // 只更新动态属性
  if (newNode.patchFlag > 0) {
    if (newNode.patchFlag & PatchFlags.TEXT) {
      // 只更新文本
      el.textContent = newNode.children
    }
    if (newNode.patchFlag & PatchFlags.CLASS) {
      // 只更新 class
      el.className = newNode.props.class
    }
    // ... 其他
  } else {
    // 没有 patchFlag,更新所有属性
    patchProps(oldNode.props, newNode.props, el)
  }
}

28.5.3 hoistStatic(静态提升)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!-- 多个相邻的静态元素 -->
<div>
  <span>静态1</span>
  <span>静态2</span>
  <span>静态3</span>
</div>

<!-- 编译后会合并提升 -->
const _hoisted = [
  h('span', '静态1'),
  h('span', '静态2'),
  h('span', '静态3'),
]

return h('div', _hoisted)

28.5.4 cacheEventHandler(事件缓存)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!-- 多次使用同一个事件处理函数 -->
<template>
  <button @click="handler">按钮1</button>
  <button @click="handler">按钮2</button>
  <button @click="handler">按钮3</button>
</template>

<!-- 编译后 -->
function render(ctx) {
  // 事件处理函数被缓存,不会每次渲染都创建新函数
  const _cache = ctx._cache || (ctx._cache = {})
  
  return h('div', [
    h('button', {
      onClick: _cache[0] || (ctx.handler)
    }, '按钮1'),
    // ...
  ])
}

28.6 nextTick 原理

28.6.1 为什么需要 nextTick

JavaScript 是单线程执行的,DOM 更新是同步的。但是 Vue 为了提高性能,会将多个状态变更合并为一次更新:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 连续的多次修改,只触发一次更新
state.count = 1   // 不立即更新
state.count = 2   // 不立即更新
state.count = 3   // 不立即更新
// DOM 更新的时机?

// 实际执行顺序
state.count = 1
state.count = 2
state.count = 3
// 这里 DOM 才真正更新

28.6.2 nextTick 的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// packages/runtime-core/src/scheduler.ts

// 任务队列
const queue: Function[] = []
// 用于去重
const seen = new Set()

// 调度器核心
let isFlushing = false
const p = Promise.resolve()

function nextTick<T>(this: any, fn?: () => T): Promise<T> {
  return fn ? p.then(fn) : p
}

function queueJob(job: () => void) {
  // 去重
  if (!seen.has(job)) {
    seen.add(job)
    queue.push(job)
  }
  
  // 如果当前没有在执行队列,开始执行
  if (!isFlushing) {
    isFlushing = true
    nextTick(flushJobs)
  }
}

function flushJobs() {
  // 清空队列前先排序(保证父组件先于子组件更新)
  queue.sort((a, b) => getId(a) - getId(b))
  
  for (const job of queue) {
    job()
    seen.delete(job)
  }
  
  queue.length = 0
  isFlushing = false
}

// 使用微任务
// Promise.resolve() 创建一个微任务
// 微任务在当前宏任务执行完毕后,下一个宏任务开始前执行
// 这保证了 DOM 在同一批次内所有同步代码执行完后才更新

28.6.3 nextTick 的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)
const listRef = ref(null)

async function updateList() {
  // 连续修改
  for (let i = 0; i < 100; i++) {
    items.value.push({ id: i, text: `item ${i}` })
  }
  
  // 此时 DOM 还没有更新
  // 如果直接访问 DOM 尺寸,会得到旧值
  
  // 等待 DOM 更新
  await nextTick()
  
  // 现在可以访问更新后的 DOM
  const height = listRef.value.$el.offsetHeight
  console.log(`列表高度: ${height}`)
}
</script>

28.7 组件实例与生命周期

28.7.1 组件实例结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// packages/runtime-core/src/component.ts

interface ComponentInternalInstance {
  // 唯一 ID
  uid: number
  
  // 组件类型
  type: Component
  
  // 父组件
  parent: ComponentInternalInstance | null
  
  // 根组件
  root: ComponentInternalInstance
  
  // props
  props: Record<string, any>
  
  // slots
  slots: Record<string, Function>
  
  // 上下文
  ctx: ComponentRenderProxy
  
  // data
  data: ReactiveScope
  
  // setup 返回值
  setupState: any
  
  // 渲染函数
  render: Function | null
  
  // vnode
  vnode: VNode
  
  // 更新函数
  update: SchedulerJob
  
  // 是否正在卸载
  isUnmounted: boolean
  
  // 是否已卸载
  isMounted: boolean
  
  // 副作用
  effects: ReactiveEffect[]
  
  // provide / inject
  provides: Record<string, any>
  parentProvides: Record<string, any>
  
  // 访问器
  accessCache: Record<string, any>
  renderCache: VNode[]
}

28.7.2 组件生命周期

flowchart TD
    subgraph Mount["挂载阶段"]
        M1[创建组件实例]
        M2[执行 setup]
        M3[编译 render 函数]
        M4[执行 beforeMount]
        M5[创建真实 DOM]
        M6[patch vnode]
        M7[执行 mounted]
    end
    
    subgraph Update["更新阶段"]
        U1[数据变化触发更新]
        U2[执行 beforeUpdate]
        U3[重新执行 render]
        U4[patch vnode]
        U5[执行 updated]
    end
    
    subgraph Unmount["卸载阶段"]
        U1a[调用 unmount]
        U2a[执行 beforeUnmount]
        U3a[卸载子组件和 DOM]
        U4a[执行 unmounted]
    end
    
    Mount --> Update
    Update --> Unmount
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 生命周期钩子的执行顺序
// beforeCreate -> setup -> created -> beforeMount -> mounted
// 数据变化: beforeUpdate -> updated
// unmount: beforeUnmount -> unmounted -> unmount

// Vue 3 组合式 API 中的生命周期
import { 
  onMounted, 
  onUpdated, 
  onUnmounted,
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount,
} from 'vue'

export default {
  setup() {
    // 相当于 beforeCreate 和 created
    console.log('setup 执行,此时组件实例已创建')
    
    onBeforeMount(() => {
      console.log('即将挂载,DOM 还未创建')
    })
    
    onMounted(() => {
      console.log('挂载完成,DOM 已创建')
    })
    
    onBeforeUpdate(() => {
      console.log('即将更新,DOM 还未 patch')
    })
    
    onUpdated(() => {
      console.log('更新完成,DOM 已 patch')
    })
    
    onBeforeUnmount(() => {
      console.log('即将卸载')
    })
    
    onUnmounted(() => {
      console.log('卸载完成')
    })
  }
}

28.7.3 setup 执行流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// packages runtime-core/src/component.ts

function setupComponent(instance: ComponentInternalInstance) {
  // 1. 创建实例
  const instance: ComponentInternalInstance = {
    uid: uid++,
    type,
    props: {},
    slots: {},
    // ...
  }
  
  // 2. 处理 props(如果组件有 props 选项)
  if (type.props) {
    instance.props = type.props
  }
  
  // 3. 处理 slots
  if (type.slots) {
    instance.slots = type.slots
  }
  
  // 4. 调用 setup
  const setupResult = callWithErrorHandling(
    type.setup,
    instance,
    ErrorCodes.SETUP_FUNCTION,
    [createPropsProxy(instance.props), instance.slots]
  )
  
  // 5. 处理 setup 返回值
  handleSetupResult(instance, setupResult)
}

function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    // setup 返回渲染函数
    instance.render = setupResult
  } else if (isObject(setupResult)) {
    // setup 返回响应式状态
    instance.setupState = proxyRefs(setupResult)
  }
  
  // 6. 编译模板或使用 render 函数
  if (!instance.render) {
    instance.render = compile(template, instance)
  }
}

28.8 本章小结

本章我们深入探讨了 Vue 3 虚拟 DOM 的实现原理:

  1. 虚拟 DOM 概述:了解了什么是虚拟 DOM,以及为什么需要它
  2. vnode 结构:学习了 vnode 的类型、shapeFlag 和创建方式
  3. h 函数:掌握了如何创建虚拟节点
  4. patch 算法:深入理解了 Vue 3 的 diff 算法优化
  5. 编译优化:学习了静态提升、PatchFlag、事件缓存等优化手段
  6. nextTick 原理:理解了异步更新和微任务队列
  7. 组件系统:了解了组件实例结构和生命周期

虚拟 DOM 是 Vue 性能优秀的关键之一。通过理解它的工作原理,我们可以更好地编写高效的 Vue 应用,也能理解为什么某些写法会触发不必要的更新。

下一章,我们将介绍 Vue 3 的生态系统,包括 Nuxt 3、Electron+Vue、跨平台方案等。🌟