# 1. vue3源码解读

春节期间在家闲来无事,于是动动小手阅读了 Vue3 的源码,并总结为文章,也是对自己的学习的总结,有任何纰漏欢迎指正~

tip: 本文阅读的 Vue3 源码对于的 commitid 为 4fe4de0

# 1.1. 环境搭建

# 1.1.1. 如何上手

首先需要搭建好调试阅读源码的环境。边阅读本文边对照 vue-next 源码是一个比较有效的阅读方法。

方法一

方法一适合阅读整个 Vue 项目的运行流程,包括 compiler 的编译过程和 runtime 的运行流程。

  1. clone Vue3 的源码
git clone git@github.com:vuejs/vue-next.git
1
  1. 调试 demo

安装完 Vue3 的依赖后,运行yarn dev , 然后在源码中需要的地方打 debugger,使用 vs code 的插件 live server 打开

packages/vue/examples/composition/commits.html ,打开 chrome 的 devtools,刷新后会命中断点。

tips: yarn dev 相当于运行:

npx rollup -wc --environment "COMMIT:4fe4de0,TARGET:vue,FORMATS:global,SOURCE_MAP:true"

因此我们也可以运行:

npx -n inspect rollup -wc --environment "COMMIT:4fe4de0,TARGET:vue,FORMATS:global,SOURCE_MAP:true"
1

来调试 Vue3 构建源码的过程。

其中 4fe4de0 是当前 Vue3 仓的 commitId,我们可以运行上面的 rollup 命令来查看 Vue3 源码的构建过程。

packages/vue/examples/composition/commits.html

<script src="../../dist/vue.global.js"></script>
<!-- 
vue.global.js 是 rollup 以 packages/vue/src/index.ts 为入口文件构建的,若要查看 rollup 的构建过程,可以运行:
npx -n inspect rollup -wc --environment "COMMIT:4fe4de0,TARGET:vue,FORMATS:global,SOURCE_MAP:true"
-->
<div id="demo">
  <h1>Latest Vue.js Commits</h1>
  <template v-for="branch in branches">
    <input type="radio" :id="branch" :value="branch" name="branch" v-model="currentBranch">
    <label :for="branch">{{ branch }}</label>
  </template>
  <p>vuejs/vue@{{ currentBranch }}</p>
  <ul>
    <li v-for="{ html_url, sha, author, commit } in commits">
      <a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
      - <span class="message">{{ truncate(commit.message) }}</span><br>
      by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
      at <span class="date">{{ formatDate(commit.author.date) }}</span>
    </li>
  </ul>
</div>

<script>
  const { createApp, ref, watchEffect } = Vue
  const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=`

  // 截断、删除
  const truncate = v => {
    const newline = v.indexOf('\n')
    return newline > 0 ? v.slice(0, newline) : v
  }

  const formatDate = v => v.replace(/T|Z/g, ' ')

  // createApp 调用的是 runtime-dom/src/index.ts#createApp ,其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp
  const app =
    createApp({
      setup() {
        const currentBranch = ref('master')
        const commits = ref(null)

        watchEffect(() => {
          fetch(`${API_URL}${currentBranch.value}`)
            .then(res => res.json())
            .then(data => {
              console.log(data)
              commits.value = data
            })
        })

        return {
          branches: ['master', 'sync'],
          currentBranch,
          commits,
          truncate,
          formatDate
        }
      }
    });
    debugger

  // app.mount 调用的是 runtime-dom/src/index.ts#mount ,其封装了 packages/runtime-core/src/apiCreateApp.ts#mount
  app.mount('#demo')
</script>

<style>
  #demo {
    font-family: 'Helvetica', Arial, sans-serif;
  }

  a {
    text-decoration: none;
    color: #f66;
  }

  li {
    line-height: 1.5em;
    margin-bottom: 20px;
  }

  .author,
  .date {
    font-weight: bold;
  }
</style>
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

方法二

方法二适合调试阅读项目具体的某个方法或某块代码。

安装 vscode 插件 jest runner,打开 Vue3 的测试文件,然后在相应的位置打断点,点击 debug 字样即可开始调试。

image-20210528154804661

有时候会碰到缺少依赖包导致运行失败的情况,安装相应的 npm 包即可。

# 1.1.2. 项目结构

  1. Vue3 的项目结构:

Vue3 的主要模块位于 packages 目录,我们 cd 到 packages 运行 tree -aI ".git*|.vscode" -C -L 1 列出所有的模块。

packages
├── compiler-core 平台无关的编译器. 它既包含可扩展的基础功能,也包含所有平台无关的插件。
├── compiler-dom 针对浏览器而写的编译器。
├── compiler-sfc
├── compiler-ssr
├── global.d.ts
├── reactivity 数据响应式系统,这是一个单独的系统,可以与任何框架配合使用
├── runtime-core 与平台无关的运行时。其实现的功能有虚拟 DOM 渲染器、Vue 组件和 Vue 的各种API,我们可以利用这个 runtime 实现针对某个具体平台的高阶 runtime,比如自定义渲染器。
├── runtime-dom 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。
├── runtime-test 一个专门为了测试而写的轻量级 runtime。由于这个 rumtime 「渲染」出的 DOM 树其实是一个 JS 对象,所以这个 runtime 可以用在所有 JS 环境里。你可以用它来测试渲染是否正确。它还可以用于序列化 DOM、触发 DOM 事件,以及记录某次更新中的 DOM 操作。
├── server-renderer 用于 SSR
├── sfc-playground
├── shared 没有暴露任何 API,主要包含了一些平台无关的内部帮助方法。
├── size-check
├── template-explorer
└── vue 用于构建「完整构建」版本,引用了上面提到的 runtime 和 compiler。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

其中最核心的模块可以分为两大部分:编译系统(compiler)和运行时(runtime,包括 reactivity),模块间的继承关系如下:

                                    +---------------------+
                                    |                     |
                                    |  @vue/compiler-sfc  |
                                    |                     |
                                    +-----+--------+------+
                                          |        |
                                          v        v
                      +---------------------+    +----------------------+
                      |                     |    |                      |
        +------------>|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
        |             |                     |    |                      |
   +----+----+        +---------------------+    +----------------------+
   |         |
   |   vue   |
   |         |
   +----+----+        +---------------------+    +----------------------+    +-------------------+
        |             |                     |    |                      |    |                   |
        +------------>|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
                      |                     |    |                      |    |                   |
                      +---------------------+    +----------------------+    +-------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. Vue3源码的构建产物

Vue3 源码的构建产物位于packages/vue/dist目录,包含:

├── vue.cjs.js                   适用于 commonjs 方式引入的 vue3 包,包括编译时compile和运行时runtime
├── vue.cjs.js.map
├── vue.cjs.prod.js              生产模式的包
├── vue.cjs.prod.js.map
├── vue.esm-browser.js           适用于 esmodule 方式引入的 vue3 包,适用于浏览器环境,包括编译时和运行时
├── vue.esm-browser.js.map
├── vue.esm-browser.prod.js
├── vue.esm-browser.prod.js.map
├── vue.esm-bundler.js           适用于 esmodule 方式引入的 vue3 包,适用于node环境,包括编译时和运行时
├── vue.esm-bundler.js.map
├── vue.global.js                既适用于浏览器环境又适用于node环境的包
├── vue.global.js.map
├── vue.global.prod.js
├── vue.global.prod.js.map
├── vue.runtime.esm-browser.js   只包含运行时的包
├── vue.runtime.esm-browser.js.map
├── vue.runtime.esm-browser.prod.js
├── vue.runtime.esm-browser.prod.js.map
├── vue.runtime.esm-bundler.js
├── vue.runtime.esm-bundler.js.map
├── vue.runtime.global.js
├── vue.runtime.global.js.map
├── vue.runtime.global.prod.js
└── vue.runtime.global.prod.js.map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 1.1.3. 阅读建议

  1. 如果你只需要了解 Vue3 最核心的特性,可以直接看编译系统或响应式系统的部分。
  2. 如果你想了解一个 Vue 应用从初始化到挂载、diff、卸载的整个过程,可以前往后阅读本文。
  3. 如果你工作中不需要了解 Vue3 运行工程中的某些细节,直接跳过即可,一般不会影响你阅读后面的内容。

本文会按照 Vue3 生命周期的执行顺序来阅读源码

# 1.1.4. 运行时(Runtime)+ 编译器(Compiler) vs 只包含运行时(Runtime-only)

如果你需要动态编译模版(比如:将字符串模版传递给 template 选项,或者通过提供一个挂载元素的方式编写 html 模版),你将需要编译器,因此需要一个完整的构建包。

当你使用 vue-loader 或者 vueify 时,*.vue 文件中的模版在构建时会被编译为 JavaScript 的渲染函数。因此你不需要包含编译器的全量包,只需使用只包含运行时的包即可。

对比 vue.global.jsvue.runtime.global.js 可知,371581 / 579987 = 0.64,只包含运行时的包体积仅为全量包的体积的 64%,因此我们应该尽量使用只包含运行时的包。

各个包的大小, 单位 bit:

vue.cjs.js                              2875              
vue.cjs.js.map                          5196              
vue.cjs.prod.js                         2268              
vue.cjs.prod.js.map                     4027              
vue.esm-browser.js                      530764              
vue.esm-browser.js.map                  1111379              
vue.esm-browser.prod.js                 112250              
vue.esm-browser.prod.js.map             874911              
vue.esm-bundler.js                      2828              
vue.esm-bundler.js.map                  5140              
vue.global.js                           579987              
vue.global.js.map                       1124410              
vue.global.prod.js                      109903              
vue.global.prod.js.map                  868571              
vue.runtime.esm-browser.js              350861              
vue.runtime.esm-browser.js.map          725244              
vue.runtime.esm-browser.prod.js         72336              
vue.runtime.esm-browser.prod.js.map     596109              
vue.runtime.esm-bundler.js              875              
vue.runtime.esm-bundler.js.map          1797              
vue.runtime.global.js                   371581              
vue.runtime.global.js.map               724952              
vue.runtime.global.prod.js              71782              
vue.runtime.global.prod.js.map          589807
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

我们会按照代码的执行顺序,包括 Vue 应用的初始化、挂载、模板编译、patch 等过程,完整的学习一下 Vue3 应用从初始化到渲染完成的整个过程。

参考文献:

  1. https://juejin.cn/post/6949370458793836580
  2. https://juejin.cn/post/6844903957421096967
  3. https://vue3js.cn/global/

# 1.2. 初始化过程

# 1.2.1. 目标

深入理解 Vue3 的初始化过程,即:

const app = createApp({}).mount('#demo')
1

理解上面这步发生了什么。

我们再看下我们在上篇文章中提到的 demo:

packages/vue/examples/composition/commits.html

<script src="../../dist/vue.global.js"></script>
<div id="demo">
  <h1>Latest Vue.js Commits</h1>
  <template v-for="branch in branches">
    <input type="radio" :id="branch" :value="branch" name="branch" v-model="currentBranch">
    <label :for="branch">{{ branch }}</label>
  </template>
  <p>vuejs/vue@{{ currentBranch }}</p>
  <ul>
    <li v-for="{ html_url, sha, author, commit } in commits">
      <a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
      - <span class="message">{{ truncate(commit.message) }}</span><br>
      by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
      at <span class="date">{{ formatDate(commit.author.date) }}</span>
    </li>
  </ul>
</div>

<script>
  const { createApp, ref, watchEffect } = Vue
  const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=`
  // 截断、删除
  const truncate = v => {
    const newline = v.indexOf('\n')
    return newline > 0 ? v.slice(0, newline) : v
  }
  const formatDate = v => v.replace(/T|Z/g, ' ')
  // createApp 调用的是 runtime-dom/src/index.ts#createApp ,
  // 其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp
  const app =
    createApp({
      setup() {
        const currentBranch = ref('master')
        const commits = ref(null)

        watchEffect(() => {
          fetch(`${API_URL}${currentBranch.value}`)
            .then(res => res.json())
            .then(data => {
              console.log(data)
              commits.value = data
            })
        })

        return {
          branches: ['master', 'sync'],
          currentBranch,
          commits,
          truncate,
          formatDate
        }
      }
    });
    debugger
  // app.mount 调用的是 runtime-dom/src/index.ts#mount ,
  // 其封装了 packages/runtime-core/src/apiCreateApp.ts#mount
  app.mount('#demo')
</script>

<style>
		/* 
省略 css 代码
    */
</style>
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

# 1.2.2. createApp

首先看下 createApp 做了什么。

createApp 调用的是 runtime-dom/src/index.ts#createApp ,其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp。我们依次看下这两个方法。

// packages/runtime-dom/src/index.ts
const rendererOptions = {
  patchProp,  // 处理 props 属性 
  ...nodeOps // 处理 DOM 节点操作
}

// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer | HydrationRenderer

let enabledHydration = false

// ensureRenderer 封装了 packages/runtime-core/src/renderer.ts#createRenderer
// renderer.ts#createRenderer 又封装了 baseCreateRenderer
// baseCreateRenderer 我们后面会讲
function ensureRenderer() {
  return renderer || (renderer = createRenderer(rendererOptions))
}

// 封装 createApp
export const createApp = ((...args) => {
  // app 就是上面 createApp 返回的应用对象
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  // 封装 mount
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // normalizeContainer 获取选择器对应的 dom 元素
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    // app._component 就是传给 createApp 的 options
    const component = app._component
    // 传给 createApp 的 options 不包含 render 和 template,则获取 container.innerHTML
    // 用于后续编译成 render
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    // 首先清空我们挂载容器的 innerHTML
    container.innerHTML = ''
    // component.render 不存在的话, mount 阶段会编译 component.template 为 render
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>
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

从上面的分析可以看出,createApp 其实是封装了 baseCreateRenderer 方法返回的 createApp 方法,我们接下来看下这个 baseCreateRenderer 方法。

# 1.2.3. baseCreateRenderer

我们看下上面提到的 baseCreateRenderer,baseCreateRenderer 这个方法足足有差不多 2000 行,baseCreateRenderer 方法里面还定义了很多方法,为了便于阅读进行了删减。

在初始化过程中,我们只需要关注 baseCreateRenderer 方法返回了什么内容即可,方法内定义的 patch 、mountElement 等方法可以以后再回来看解释。你可以直接切换到这块代码的最后查看 baseCreateRenderer 的返回值

// packages/runtime-core/src/renderer.ts
/**
  baseCreateRenderer 中定义了 patch、processElement 等一些列方法,
  用于完成节点的 diff、mount(挂载)、unmount(卸载)等逻辑,我们稍后再看这些方法的具体实现。
  
  从 baseCreateRenderer 的返回值可以看到,ensureRenderer() 的返回值就是 createAppAPI(render, hydrate) 的结果
  我们接下来再看看 createAppAPI 的实现 
 */
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // 代码太多,省略一部分...

  // Note: functions inside this closure should use `const xxx = () => {}`
  // style in order to prevent being inlined by minifiers.
  // Patch 方法中进行节点的挂载、更新、卸载
  const patch: PatchFn = (
    n1, // 旧的 VNode
    n2, // 新的 VNode
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = false
  ) => {
    // 如果有旧VNode,且新旧节点不一样,umount销毁旧节点
    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    // 先通过节点类型 type 来判断选择处理方法
    // 首次挂载时 n1 = null ,n2 是根 vnode ,代表 rootComponent 
    // 所以会走 processComponent 逻辑
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // 首次挂载会走 processComponent 逻辑,将 n2 渲染到 #demo 节点下
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          // 处理其他一些不太常见的情况
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          // 处理其他一些不太常见的情况
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }
    // ...
  }

  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      // 旧的节点 n1 不存在时,就走 mount(挂载) 逻辑
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      // 新旧节点 n2 n1 都存在,则执行更新逻辑
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

  // ...
	// 处理组件挂载与更新的逻辑
  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    // 如果没有旧节点
    if (n1 == null) {
      // 如果是 keep-alive 组件
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // 首次挂载 n1 = null, n2 = 根 vnode
        // 执行 mountComponent
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      // 如果n1 n2 都有则执行更新
      updateComponent(n1, n2, optimized)
    }
  }

  /**
  可见,在mountComponent中主要做了三件事

  createComponentInstance,初始化组件实例,组件实例包括appContext、parent、root、props、attrs、slots、refs等属性
  setupComponent,完善instance,

  调用initProps、initSlots,初始化instance相关属性,
  外还会通过setupStatefulComponent调用传入的setup方法,获取返回值setupResult,根据其数据类型
  finishComponentSetup,

  检测instance.render是否存在,不存在则调用compile(Component.template)编译渲染函数
  在__FEATURE_OPTIONS__配置下调用applyOptions兼容Vue2.x,合并配置项到vue组件实例,初始化watch、computed、methods等配置项,调用相关生命周期钩子等

  setupRenderEffect,主要是实现instance.update方法,该方法等价于effect(function componentEffect(){...}),程序如何渲染和更新视图就在这里,这也是接下来阅读的重点
 */
  const mountComponent: MountComponentFn = (
    initialVNode, // 初始VNode 也就是App组件生成的VNode
    container, // #app Dom容器
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 创建组件实例
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    // inject renderer internals for keepAlive
    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }

    // resolve props and slots for setup context

    // 设置 instance 实例,初始化 props,slots 还有Vue3新增的composition API
    // 如果 instance.render 属性不存在,则编译 template 获得 render 并赋给 instance.render
    setupComponent(instance)
    
    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

      // Give it a placeholder if this is not hydration
      // TODO handle self-defined fallback
      if (!initialVNode.el) {
        const placeholder = (instance.subTree = createVNode(Comment))
        processCommentNode(null, placeholder, container!, anchor)
      }
      return
    }

    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

  }

  /**
  instance 当前 vm 实例
  initialVNode 可以是组件 VNode 或者普通 VNode
  container 挂载的容器,例如 #demo 对应的节点
  anchor, parentSuspense, isSVG 普通情况下都为 null
   */
  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // create reactive effect for rendering
    // 创建响应式的副作用render函数
    instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance

        // beforeMount hook
        // bm 生命周期 及 hook 执行
        if (bm) {
          invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if ((vnodeHook = props && props.onVnodeBeforeMount)) {
          invokeVNodeHook(vnodeHook, parent, initialVNode)
        }

        // render
        // ...
        const subTree = (instance.subTree = renderComponentRoot(instance))
        // ...

        if (el && hydrateNode) {
	        // ...
          // vnode has adopted host node - perform hydration instead of mount.
          hydrateNode(
            initialVNode.el as Node,
            subTree,
            instance,
            parentSuspense,
            null
          )
	        // ...
        } else {
	        // ...
          // 把 subTree 挂载到Dom容器中
          // subTree 是什么? 例如最开始的例子 App 组件为 initialVNode,subTree 就是 App组件模版里的结构生成的VNode,
          // children 属性为 HelloWorld 组件VNode, p 标签VNode。
          // 而App组件 initialVNode 的 chidren 里面,根据 HelloWorld 标签生成的 VNode, 
          // 对于 HelloWorld 组件内部DOM结构来说就是 initialVNode,而其内部DOM结构生成的VNode就是 subTree。
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
	        // ...
          initialVNode.el = subTree.el
        }
        // mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        // onVnodeMounted
        if ((vnodeHook = props && props.onVnodeMounted)) {
          const scopedInitialVNode = initialVNode
          queuePostRenderEffect(() => {
            invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode)
          }, parentSuspense)
        }
        // activated hook for keep-alive roots.
        // #1742 activated hook must be accessed after first render
        // since the hook may be injected by a child keep-alive
        const { a } = instance
        if (
          a &&
          initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        ) {
          queuePostRenderEffect(a, parentSuspense)
        }
        instance.isMounted = true

        // #2458: deference mount-only object parameters to prevent memleaks
        initialVNode = container = anchor = null as any
      } else {
        // 更新组件的逻辑
        // updateComponent
        // This is triggered by mutation of component's own state (next: null)
        // OR parent calling processComponent (next: VNode)
        let { next, bu, u, parent, vnode } = instance
        let originNext = next
        let vnodeHook: VNodeHook | null | undefined

        if (next) {
          next.el = vnode.el
          updateComponentPreRender(instance, next, optimized)
        } else {
          next = vnode
        }

        // beforeUpdate hook
        if (bu) {
          invokeArrayFns(bu)
        }
        // onVnodeBeforeUpdate
        if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
          invokeVNodeHook(vnodeHook, parent, next, vnode)
        }

        // render
        const nextTree = renderComponentRoot(instance)
        const prevTree = instance.subTree
        instance.subTree = nextTree

        patch(
          prevTree,
          nextTree,
          // parent may have changed if it's in a teleport
          hostParentNode(prevTree.el!)!,
          // anchor may have changed if it's in a fragment
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          isSVG
        )

        next.el = nextTree.el
        if (originNext === null) {
          // self-triggered update. In case of HOC, update parent component
          // vnode el. HOC is indicated by parent instance's subTree pointing
          // to child component's vnode
          updateHOCHostEl(instance, nextTree.el)
        }
        // updated hook
        if (u) {
          queuePostRenderEffect(u, parentSuspense)
        }
        // onVnodeUpdated
        if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
          queuePostRenderEffect(() => {
            invokeVNodeHook(vnodeHook!, parent, next!, vnode)
          }, parentSuspense)
        }

      }
      /**
  createDevEffectOptions(instance) 用于后续的派发更新,它会返回一个对象:
  {
      scheduler: queueJob(job) {
                      if (!queue.includes(job)) {
                          queue.push(job);
                          queueFlush();
                      }
                  },
      onTrack: instance.rtc ? e => invokeHooks(instance.rtc, e) : void 0,
      onTrigger: instance.rtg ? e => invokeHooks(instance.rtg, e) : void 0
  }
       */
    }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
  }

  const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean
  ) => {
    nextVNode.component = instance
    const prevProps = instance.vnode.props
    instance.vnode = nextVNode
    instance.next = null
    // 调用updateProps、updateSlots等方法更新相关数据
    updateProps(instance, nextVNode.props, prevProps, optimized)
    updateSlots(instance, nextVNode.children, optimized)

    pauseTracking()
    // props update may have triggered pre-flush watchers.
    // flush them before the render update.
    flushPreFlushCbs(undefined, instance.update)
    resetTracking()
  }

  const removeFragment = (cur: RendererNode, end: RendererNode) => {
    // For fragments, directly remove all contained DOM nodes.
    // (fragment child nodes cannot have transition)
    let next
    while (cur !== end) {
      next = hostNextSibling(cur)!
      hostRemove(cur)
      cur = next
    }
    hostRemove(end)
  }

  // container 就是我们要挂载的那个元素,即 rootContainer
  // vnode 是使用根组件创建的根 vnode
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    // unmount 逻辑
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
      // patch 逻辑,首次挂载会走这里
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

  // ...

  return {
    render,
    hydrate, // 用于SSR渲染
    createApp: createAppAPI(render, hydrate)
  }
}
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485

可以看到,createApp 方法是调用 createAppAPI(render, hydrate) 返回的,render 是在 baseCreateRenderer 中定义的,hydrate 用于SSR渲染,这里先不管。

我们将在下篇文章看一下 createAppAPI。

# 1.3. 挂载根节点

上篇文章中我们提到

const app = createApp({})
1

实际调用的是 packages/runtime-core/src/apiCreateApp.ts#createAppAPI,返回全局实例 app,我们来看一下这个 createApp 方法。

# 1.3.1. createAppAPI

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    /**
    createAppAPI 工厂函数接收 render ,返回真正的 createApp 方法.
    其中 render 就是上面提到的 baseCreateRenderer 中定义的 render 方法

    app 就是实际返回的 app 对象,app 对象上定义的 use、mixin、component 
    等方法大多会返回 app 对象,因此这些方法支持链式调用,我们会在后面的文章中详细介绍这些方法,
    现在我们先继续看 Vue3 的初始化过程
    **/
    const app: App = (context.app = {
      
      // 其他 app 实例方法...

      // rootContainer 就是我们要挂载的那个元素,比如 id 为 demo 的节点
      // app.mount('#demo') 调用的就是这里的 mount 方法,执行挂载逻辑
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        // mount 方法的具体实现,下面有讲
      },

    })

    return app
  }
}
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

从上面的代码中我们可以看出 createApp 最后返回了 app 对象,然后我们调用 app.mount 方法挂载节点。

# 1.3.2. mount

在上篇文章中,我们讲到 packages/runtime-dom/src/index.ts#createApp 方法内对 app.mount 进行了封装

// packages/runtime-dom/src/index.ts
// 省略代码...
  const { mount } = app
  // 封装 mount
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // ...
    const proxy = mount(container, false, container instanceof SVGElement)
		// ...
    return proxy
  }

  return app
}) as CreateAppFunction<Element>
1
2
3
4
5
6
7
8
9
10
11
12
13

app.mount 最终还是会调用 packages/runtime-core/src/apiCreateApp.ts 里面 app 对象上定义的 mount 方法执行挂载逻辑。

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    /**
    createAppAPI 工厂函数接收 render ,返回真正的 createApp 方法.
    其中 render 就是上面提到的 baseCreateRenderer 中定义的 render 方法

    app 就是实际返回的 app 对象,app 对象上定义的 use、mixin、component 
    等方法大多会返回 app 对象,因此这些方法支持链式调用,我们会在后面的文章中详细介绍这些方法,
    现在我们先继续看 Vue3 的初始化过程
    **/
    const app: App = (context.app = {
      
      // 其他 app 实例方法...

      // rootContainer 就是我们要挂载的那个元素,比如 id 为 demo 的节点
      // app.mount('#demo') 调用的就是这里的 mount 方法,执行挂载逻辑
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        //
        if (!isMounted) {
          // rootComponent 是我们传给 createApp 的 option
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          /**
          vnode 的结构为:
          vnode = {
            anchor: null,
            appContext: null,
            children: null,
            component: null,
            dirs: null,
            dynamicChildren: null,
            dynamicProps: null,
            el: null,
            key: null,
            patchFlag: null,
            props: null,
            ref: null,
            scopeId: null,
            shapeFlag: null,
            slotScopeIds: null,
            ssContent: null,
            ssFallback: null,
            staticCount: null,
            suspense: null,
            target: null,
            targetAnchor: null,
            transition: null,
            __v_isVNode: true,
            __v_skip: true,
            type: {
              setup: '...',
              template: '...'
            }
            // type 是合入了 template 的 option
          }
           */

          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

          // HMR root reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, isSVG)
            }
          }

          // 用于 ssr
          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            // 调用 packages/runtime-core/src/renderer.ts#render
            // 这个 render 并不是我们厂听说的 render function
            render(vnode, rootContainer, isSVG)
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsInitApp(app, version)
          }

          return vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          )
        }
      },

    })

    return app
  }
}
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

app.mount 会调用 render(vnode, rootContainer, isSVG) 渲染节点。

# 1.3.3. render

上面文章中我们提到 app.mount 会调用 render(vnode, rootContainer, isSVG) 渲染根节点,这里的 render 是在我们前面讲的 baseCreateRenderer 方法内定义的,我们回顾一下这个 render 方法。

// container 就是我们要挂载的那个元素,即 rootContainer
// vnode 是使用根组件创建的根 vnode
const render: RootRenderFunction = (vnode, container, isSVG) => {
  // unmount 逻辑
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // patch 逻辑,首次挂载会走这里
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以看到 render 主要是调用 unmountpatch 方法执行卸载、挂载、更新的逻辑,我们先看下 patch 方法。

# 1.3.4. patch

const patch: PatchFn = (
  n1, // 旧的 VNode
  n2, // 新的 VNode
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = false
) => {
  // 如果有旧VNode,且新旧节点不一样,umount销毁旧节点
  // patching & not same type, unmount old tree
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }

  if (n2.patchFlag === PatchFlags.BAIL) {
    optimized = false
    n2.dynamicChildren = null
  }

  const { type, ref, shapeFlag } = n2
  // 先通过节点 type 来判断选择处理方法
  // 首次挂载时 n1 = null ,n2 是根 vnode ,代表 rootComponent 
  // 所以会走 processComponent 逻辑
  switch (type) {
    case Text:
      // 根据名字也能猜出下面这些方法分别处理不同的情况
      processText(n1, n2, container, anchor)
      break
    case Comment:
      processCommentNode(n1, n2, container, anchor)
      break
    case Static:
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG)
      } else if (__DEV__) {
        patchStaticNode(n1, n2, container, isSVG)
      }
      break
    case Fragment:
      processFragment(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 首次挂载会走 processComponent 逻辑,将 n2 渲染到 #demo 节点下
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        // ...
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        // ...
      } else if (__DEV__) {
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }

  // set ref
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2)
  }
}
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

我们省略一些不关键的步骤,首次渲染时,processComponent 在挂载 n2 根 vnode 时,经过一系列方法调用(如下调用栈),会进入 finishComponentSetup 方法内部。

image-20210601204643730

我们应该还记得我们怎么创建 app 的:

<script src="../../dist/vue.global.js"></script>
<div id="demo">
	<!-- ... -->
</div>

<script>
// ...
  const app =
    createApp({
      setup() {
        const currentBranch = ref('master')
        const commits = ref(null)

        watchEffect(() => {
          fetch(`${API_URL}${currentBranch.value}`)
            .then(res => res.json())
            .then(data => {
              console.log(data)
              commits.value = data
            })
        })

        return {
          branches: ['master', 'sync'],
          currentBranch,
          commits,
          truncate,
          formatDate,
        }
      }
    });

  app.mount('#demo')
</script>

<style>
/** **/
</style>
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

Component 就是我们传给 createApp 的根选项 options,并且合入了 template 属性,其值是页面模板字符串。

finishComponentSetup 方法内部判断 options 对象上 render 方法是否存在,如果不存在会调用 compile 方法将 template 模板编译为 render function。我们将在下篇文章中讨论将 template 目标编译为 render function 的过程

这里的 render 是由 template 编译而来的 render function,并不是本系列文章第二篇中提到的 render 方法 packages/runtime-core/src/renderer.ts#render

下篇文章中我们会开始介绍 compile 方法,也就是极其重要的 Vue3 的编译系统。

image-20210601203137828

# 1.4. 注册compiler

上篇文章中我们讲到,当我们传给 createApp 方法的根选项 options 没有 render 属性时,Vue 会调用 compile 方法将 template 模板编译为 render code。接下来我们就来学习下 Vue3 的编译过程。

首先,我们看一下编译系统是如何注入到运行时的。

# 1.4.1. finishComponentSetup

// packages/runtime-core/src/component.ts

// 在 packages/vue/src/index.ts 中会调用 registerRuntimeCompiler 将 compile 注册进来,
// 以便在 finishComponentSetup 中需要的时候进行调用
/**
 * For runtime-dom to register the compiler.
 * Note the exported method uses any to avoid d.ts relying on the compiler types.
 */
export function registerRuntimeCompiler(_compile: any) {
  compile = _compile
}

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  // template / render function normalization
  if (__NODE_JS__ && isSSR) {
    // SSR。。。
  } else if (!instance.render) {
    // could be set from setup()
    // 判断 render 方法是否存在,如果不存在会调用 compile 方法将 template 转化为 render。
    if (compile && Component.template && !Component.render) {
      if (__DEV__) {
        startMeasure(instance, `compile`)
      }
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters
      })
      if (__DEV__) {
        endMeasure(instance, `compile`)
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // for runtime-compiled render functions using `with` blocks, the render
    // proxy used needs a different `has` handler which is more performant and
    // also only allows a whitelist of globals to fallthrough.
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }
  // ...
}
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

# 1.4.2. compileToFunction

// packages/vue/src/index.ts
import { compile, CompilerOptions, CompilerError } from '@vue/compiler-dom'
// registerRuntimeCompiler 用于注入 compile 方法
import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom'
// ...

// 封装 compile 方法
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  if (!isString(template)) {
    // 如果 template 不是字符串
    // 则认为是一个 DOM 节点,获取 innerHTML
    if (template.nodeType) {
      template = template.innerHTML
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      return NOOP
    }
  }

  const key = template
  const cached = compileCache[key]
  // 如果缓存中存在,直接从缓存中获取
  if (cached) {
    return cached
  }

  // 如果是 ID 选择器,这获取 DOM 元素后,取 innerHTML
  if (template[0] === '#') {
    const el = document.querySelector(template)
    if (__DEV__ && !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    // __UNSAFE__
    // Reason: potential execution of JS expressions in in-DOM template.
    // The user must make sure the in-DOM template is trusted. If it's rendered
    // by the server, the template should not contain any user data.
    template = el ? el.innerHTML : ``
  }

  // 调用 compile 获取 render code
  let { code } = compile(
    template,
    extend(
      {
        hoistStatic: true,
        onError(err: CompilerError) {
          if (__DEV__) {
            const message = `Template compilation error: ${err.message}`
            const codeFrame =
              err.loc &&
              generateCodeFrame(
                template as string,
                err.loc.start.offset,
                err.loc.end.offset
              )
            warn(codeFrame ? `${message}\n${codeFrame}` : message)
          } else {
            /* istanbul ignore next */
            throw err
          }
        }
      },
      options
    )
  )

  // The wildcard import results in a huge object with every export
  // with keys that cannot be mangled, and can be quite heavy size-wise.
  // In the global build we know `Vue` is available globally so we can avoid
  // the wildcard object.
  const render = (__GLOBAL__
    ? new Function(code)()
    : new Function('Vue', code)(runtimeDom)) as RenderFunction

  // mark the function as runtime compiled
  ;(render as InternalRenderFunction)._rc = true

  // 返回 render 方法的同时,将其放入缓存
  return (compileCache[key] = render)
}

// 向运行时注入 compile
registerRuntimeCompiler(compileToFunction)

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

从上面可以看到, compile 方法是通过 registerRuntimeCompiler(compileToFunction) 注入到运行时的,运行时调用 compile 编译 template 得到 render code (而不是 render function),然后经过:

const render = (__GLOBAL__
    ? new Function(code)()
    : new Function('Vue', code)(runtimeDom)) as RenderFunction
1
2
3

将 render code 转换为 render function,之后会将 render function 挂载到 instance.render 属性上。

前面提到, compile 方法是通过 registerRuntimeCompiler(compileToFunction) 注入到运行时的,而 compileToFunction 其实封装了 @vue/compiler-dom 里面导出的 compile 方法,下一章我们将介绍一下这个 compile 方法,也就是 Vue3 的编译时。

# 1.5. compile-模板编译

上篇文章中我们提到 Vue 调用 compile 方法编译 template 模板得到 render code (而不是 render function),然后 rumtime 执行 render code 得到 render function 的过程。现在让我们具体看一下 Vue3 如何编译 template 得到 render code。

# 1.5.1. template 的编译流程

Vue3 的模板编译的流程和 babel 是类似的,都大致可以分为 parse、transform、generate 三个阶段。

  1. parse: 解析阶段,将模板解析成原始AST,此时的AST只是相当于对模板进行了傻瓜式翻译,并没有实际的可用性
  2. transform: 转换阶段,将原始AST通过各转换插件转换出可用于渲染的目标AST,同时为各节点创建代码生成器,同时注入运行时优化信息
  3. generate: 生成阶段,通过代码生成器生成最终的可运行代码段和其他运行时信息

image-20210601203137828

# 1.5.2. 编译系统的入口

上篇文章中我们提到,runtime 最终调用的是 packages/compiler-dom/src/index.ts#compile 来编译 template。

import {
  baseCompile,
  baseParse,
  CompilerOptions,
  CodegenResult,
  ParserOptions,
  RootNode,
  noopDirectiveTransform,
  NodeTransform,
  DirectiveTransform
} from '@vue/compiler-core'
// ...

export function compile(
  template: string,
  options: CompilerOptions = {}
): CodegenResult {
  return baseCompile(
    template,
    extend({}, parserOptions, options, {
      nodeTransforms: [
        // ignore <script> and <tag>
        // this is not put inside DOMNodeTransforms because that list is used
        // by compiler-ssr to generate vnode fallback branches
        ignoreSideEffectTags,
        // 传入适用于 DOM 环境的 DOMNodeTransforms,用于 node 节点及属性的转换
        ...DOMNodeTransforms,
        ...(options.nodeTransforms || [])
      ],
      directiveTransforms: extend(
        {},
        // 传入适用于 DOM 环境的 DOMDirectiveTransforms,用于指令的编译
        DOMDirectiveTransforms,
        options.directiveTransforms || {}
      ),
      //  __BROWSER__ 判断,这是因为有些优化只能在 node 进行,对于浏览器运行时进行编译是没办法做到的。
      transformHoist: __BROWSER__ ? null : stringifyStatic
    })
  )
}
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

packages/compiler-dom/src/index.ts#compile 又调用了 @vue/compiler-core#baseCompile 并传入适用于相应平台的默认的 transform 方法来进行模板编译。

// ...
// 获取预置的 transformer 插件集
export function getBaseTransformPreset(
  prefixIdentifiers?: boolean
): TransformPreset {
  return [
    // 生成目标节点结构的转换插件
    [
      transformOnce,
      transformIf,
      transformFor,
      ...(!__BROWSER__ && prefixIdentifiers
        ? [
            // order is important
            trackVForSlotScopes,
            transformExpression
          ]
        : __BROWSER__ && __DEV__
          ? [transformExpression]
          : []),
      transformSlotOutlet,
      transformElement,
      trackSlotScopes,
      transformText
    ],
    // 指令转换插件
    {
      on: transformOn,
      bind: transformBind,
      model: transformModel
    }
  ]
}

// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'
  /* istanbul ignore if */
  if (__BROWSER__) {
    if (options.prefixIdentifiers === true) {
      onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
    } else if (isModuleMode) {
      onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
    }
  }

  const prefixIdentifiers =
    !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
  if (!prefixIdentifiers && options.cacheHandlers) {
    onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
  }
  if (options.scopeId && !isModuleMode) {
    onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
  }

  // 解析
  // 如果 template 是 string 类型,解析 template html,转化为 ast
  // 如果 template 是 ast (RootNode),则不需要解析了
  const ast = isString(template) ? baseParse(template, options) : template
  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
    prefixIdentifiers
  )
  
  // 转换
  // 遍历 AST 节点树,对上面生成的 AST 进行指令转换,生成可用节点,同时根据 compiler
  // 传入的配置(如是否做静态节点提升等)对 AST 节点树进行优化处理,为 rootNode 及
  // 下属每个节点挂载 codegenNode
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

  // 生成代码
  // 将 ast 转化为可执行代码
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}
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

我们可以通过 options 传入自定义的 nodeTransforms 和 directiveTransforms,实现对自定义的节点、指令的编译规则。

# 1.6. parse-ast解析

# 1.6.1. 目标

上篇文章中我们提到 compile 编译 template 得到 render code (而不是 render function),然后 rumtime 执行 render code 得到 render function 的过程。现在让我们具体看一下 Vue3 如何编译 template 得到 render code。

编译模板的第一步就是调用 baseParse 解析 template string 为 ast。RootNode 就是一个 dom 树,也即一个 Vnode 树。

// packages/compiler-core/src/parse.ts
// 解析 template html 生成抽象语法树 (AST)
export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  // rootNode:
  // 根节点是一个临时容器,真正在运行时映射成具体内容的是rootNode下的children,
  // 说白了rootNode只是个用来存放实际节点的空壳子,假如parse AST节点时template string中是多根节点,
  // 那么没有一个抽象出来的根节点就无法表述完整的树结构,这也是为什么vue3.0能够允许多根模版的原因所在。

  // 生成解析阶段的执行上下文,用于寄存解析过程中产生的状态
  const context = createParserContext(content, options)
  // 获取模板解析的起始位置
  const start = getCursor(context)
  // 创建出AST根节点,将模板解析成AST节点,并将生成的AST子代节点挂载到根节点上
  return createRoot(
    parseChildren(context, TextModes.DATA, []), // 解析生成子节点
    getSelection(context, start) // 获取成功解析为AST节点的子模板及其起始位置信息
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

我们看下上面调用的 createParserContext


/**
创建解析过程的上下文对象,用于对过程中产生的状态进行集中统一的存储和管控
{
    options: extend({}, defaultParserOptions, options), // parser配置项
    // column、line、offset均是相对template string的全局位置信息
    column: 1, // parser解析到的列数
    line: 1, // 解析到的行数
    offset: 0, // 解析到相对于template string开始的位置
    originalSource: content, // 初始template string,即用户定义的完整模版字符串
    source: content, // parser处理后的最新template string
    inPre: false,
    inVPre: false
}
 * @returns 
 */
function createParserContext(
  content: string,
  rawOptions: ParserOptions
): ParserContext {
  const options = extend({}, defaultParserOptions)
  for (const key in rawOptions) {
    // @ts-ignore
    options[key] = rawOptions[key] || defaultParserOptions[key]
  }
  return {
    options,
    column: 1,
    line: 1,
    offset: 0,
    originalSource: content,
    source: content,
    inPre: false,
    inVPre: false
  }
}

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

可以看到,baseParse 的核心在下面这步:

return createRoot(
  parseChildren(context, TextModes.DATA, []), 
  getSelection(context, start) 
)
1
2
3
4

我们先来具体看下 parseChildren

// 用于解析一段“完整”的模版串
function parseChildren(
  context: ParserContext,
  // 节点的命名空间是,html、xml等情况
  mode: TextModes,
  // 祖先节点,是一个栈结构,用于维护节点嵌套关系,越靠后的节点在dom树中的层级越深
  ancestors: ElementNode[]
): TemplateChildNode[] {
  const parent = last(ancestors)
  const ns = parent ? parent.ns : Namespaces.HTML
  // 存储解析出来的AST子节点
  const nodes: TemplateChildNode[] = []

  // 遇到闭合标签结束解析
  while (!isEnd(context, mode, ancestors)) {
    __TEST__ && assert(context.source.length > 0)
    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined

    if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // '{{'
        // 解析以‘{{’开头的模版,parseInterpolation为核心方法,下面重点讲解
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {
        // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
        if (s.length === 1) {
          emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
        } else if (s[1] === '!') {
          // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
          if (startsWith(s, '<!--')) {
            // 解析注释节点
            node = parseComment(context)
          } else if (startsWith(s, '<!DOCTYPE')) {
            // Ignore DOCTYPE by a limitation.
            node = parseBogusComment(context)
          } else if (startsWith(s, '<![CDATA[')) {
            if (ns !== Namespaces.HTML) {
              node = parseCDATA(context, ancestors)
            } else {
              emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
              node = parseBogusComment(context)
            }
          } else {
            // 错误处理,省略
            emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
            node = parseBogusComment(context)
          }
        } else if (s[1] === '/') {
          // 解析结束标签错误的逻辑,此处省略
          // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
          if (s.length === 2) {
            emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
          } else if (s[2] === '>') {
            emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
            advanceBy(context, 3)
            continue
          } else if (/[a-z]/i.test(s[2])) {
            emitError(context, ErrorCodes.X_INVALID_END_TAG)
            parseTag(context, TagType.End, parent)
            continue
          } else {
            emitError(
              context,
              ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
              2
            )
            node = parseBogusComment(context)
          }
        } else if (/[a-z]/i.test(s[1])) {
          // 解析正常的html开始标签,获得解析到的AST节点
          // parseElement是核心方法,下面重点讲解
          node = parseElement(context, ancestors)
        } else if (s[1] === '?') {
          emitError(
            context,
            ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
            1
          )
          node = parseBogusComment(context)
        } else {
          emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
        }
      }
    }
    if (!node) {
      node = parseText(context, mode)
    }

    if (isArray(node)) {
      for (let i = 0; i < node.length; i++) {
        pushNode(nodes, node[i])
      }
    } else {
      pushNode(nodes, node)
    }
  }

  // Whitespace management for more efficient output
  // (same as v2 whitespace: 'condense')
  let removedWhitespace = false
  if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i]
      if (!context.inPre && node.type === NodeTypes.TEXT) {
        if (!/[^\t\r\n\f ]/.test(node.content)) {
          const prev = nodes[i - 1]
          const next = nodes[i + 1]
          // If:
          // - the whitespace is the first or last node, or:
          // - the whitespace is adjacent to a comment, or:
          // - the whitespace is between two elements AND contains newline
          // Then the whitespace is ignored.
          if (
            !prev ||
            !next ||
            prev.type === NodeTypes.COMMENT ||
            next.type === NodeTypes.COMMENT ||
            (prev.type === NodeTypes.ELEMENT &&
              next.type === NodeTypes.ELEMENT &&
              /[\r\n]/.test(node.content))
          ) {
            removedWhitespace = true
            nodes[i] = null as any
          } else {
            // Otherwise, condensed consecutive whitespace inside the text
            // down to a single space
            node.content = ' '
          }
        } else {
          node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
        }
      }
      // also remove comment nodes in prod by default
      if (
        !__DEV__ &&
        node.type === NodeTypes.COMMENT &&
        !context.options.comments
      ) {
        removedWhitespace = true
        nodes[i] = null as any
      }
    }
    if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
      // remove leading newline per html spec
      // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
      const first = nodes[0]
      if (first && first.type === NodeTypes.TEXT) {
        first.content = first.content.replace(/^\r?\n/, '')
      }
    }
  }

  return removedWhitespace ? nodes.filter(Boolean) : nodes
}
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155

一起看下 parseChildren 用到的核心方法之一 parseElement

// 解析dom节点
function parseElement(
  context: ParserContext,
  ancestors: ElementNode[]
): ElementNode | undefined {
  __TEST__ && assert(/^<[a-z]/i.test(context.source))

  // Start tag.
  const wasInPre = context.inPre
  const wasInVPre = context.inVPre
  const parent = last(ancestors)
  // 解析开始标签生成AST节点
  const element = parseTag(context, TagType.Start, parent)
  const isPreBoundary = context.inPre && !wasInPre
  const isVPreBoundary = context.inVPre && !wasInVPre

  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    return element
  }

  // Children.
  // 根据开始标签解析出的节点入栈,并解析它的子节点,子节点解析完毕后,父节点出栈
  ancestors.push(element)
  const mode = context.options.getTextMode(element, parent)
  // 递归解析子节点,子节点解析过程中遇到父节点的结束标签,即解析完成并返回解析结果
  const children = parseChildren(context, mode, ancestors)
  ancestors.pop()

  // 为当前节点注入children子节点
  element.children = children

  // End tag.
  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End, parent)
  } else {
    emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
    if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
      const first = children[0]
      if (first && startsWith(first.loc.source, '<!--')) {
        emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
      }
    }
  }

  element.loc = getSelection(context, element.loc.start)

  if (isPreBoundary) {
    context.inPre = false
  }
  if (isVPreBoundary) {
    context.inVPre = false
  }
  return element
}
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

TODO:parseTag、parseAttributes

# 1.7. transform

上篇文章中我们介绍了 Vue3 将 template 解析成 ast 的过程,得到 ast 后,我们就可以对 ast 上的节点和属性进行相应的操作,也就是之前我们看过的 transform 的过程。

# 1.7.1. transform

  // 遍历 AST 节点树,对上面生成的 AST 进行指令转换,生成可用节点,同时根据 compiler
  // 传入的配置(如是否做静态节点提升等)对 AST 节点树进行优化处理,为 rootNode 及
  // 下属每个节点挂载 codegenNode
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们来详细看一下 transform 方法。

/**
{
  type: NodeTypes.SIMPLE_EXPRESSION, // 表达式类型标识
  loc, // 位置信息
  isConstant, // 是否是常量
  content, // 表达式内容
  // 是否是静态的,
  // e.g. v-bind:attr="value",value如果是动态变化的变量
  // v-bind:attr="true",true是常量不会变化,因此是静态的
  isStatic 
}
// 比如v-bind:attr="true",true转换为简单表达式对象就是
// { isContant: true, content: 'true', isStatic: true ... }
 */

// transform 函数对静态提升其决定性作用的两件事:
// 1. 将原始 AST 中的静态节点对应的 AST Element 赋值给根 AST 的 hoists 属性。
// 2. 获取原始 AST 需要的 helpers 对应的键名,用于 generate 阶段的生成可执行代码的获取对应函数,
//    例如 createTextVNode、createStaticVNode、renderList 等等。

// 并且,在 traverseNode 函数中会对 AST Element 应用具体的 transform 函数,大致可以分为两类:
// 1. 静态节点 transform 应用,即节点不含有插值、指令、props、动态样式的绑定等。
// 2. 动态节点 transform 应用,即节点含有插值、指令、props、动态样式的绑定等。

// `<div>hi vue3</div>` 会命中 transformElement 和 transformText 两个 plugin 的逻辑。
export function transform(root: RootNode, options: TransformOptions) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  // finalize meta information
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
}

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

其中最核心的步骤是 traverseNode :

// 遍历AST节点树过程中,通过node转换器(nodeTransforms)对当前节点进行node转换,
// 子节点全部遍历完成后执行对应指令的onExit回调退出转换。对v-if、v-for等指令的转换生成对应节点,
// 都是由nodeTransforms中对应的指令转换工具完成的。
// 经nodeTransforms处理过的AST节点会被挂载codeGenNode属性(其实就是调用vnode创建的interface),
// 该属性包含patchFlag等在AST解析阶段无法获得的信息,其作用就是为了在后面的generate阶段生成vnode的创建调用。
// 本质上codegenNode是一个表达式对象。
export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  context.currentNode = node
  // apply transform plugins
  const { nodeTransforms } = context
  const exitFns = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    // 依次执行 nodeTransforms
    // Transform 会返回一个退出函数,在处理完所有的子节点后再执行
    // 其中的包含我们调用 compile 的时候传入的 options.nodeTransforms
    // 转换器:transformElement、transformExpression、transformText、 
    // transformElement负责整个节点层面的转换,
    // transformExpression负责节点中表达式的转化,
    // transformText负责节点中文本的转换,转换后会增加一堆表达式表述对象
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.currentNode) {
      // node was removed
      return
    } else {
      // node may have been replaced
      node = context.currentNode
    }
  }

  switch (node.type) {
    case NodeTypes.COMMENT:
      // 处理注释节点
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // 处理插值表达式节点
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      // 处理 if 表达式节点
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
  }

  // exit transforms
  context.currentNode = node
  let i = exitFns.length
  // 执行所有 Transform 的退出函数
  while (i--) {
    exitFns[i]()
  }
}
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

在上一篇模板编译里面我们曾讲过,默认的 nodeTransforms 包括目标节点结构的转换插件、指令转换插件两大类, 我们可以看一下之前提到的这两类中的 transformIf 插件。

export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    /**
    在 transformIf 方法体内我们可以通过操作 node 来实现一些自定义的节点、属性的转换规则
    nodeTransform 方法的返回值就是退出函数,退出函数会在退出 traverseNode 方法前被依次执行
    while (i--) {
      exitFns[i]()
    }
     */
    // processIf 会在退出 traverseNode 方法前被执行
    // 进行 if 节点的处理,为其标记相应的 codegenNode
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // #1587: We need to dynamically increment the key based on the current
      // node's sibling nodes, since chained v-if/else branches are
      // rendered at the same depth
      const siblings = context.parent!.children
      let i = siblings.indexOf(ifNode)
      let key = 0
      while (i-- >= 0) {
        const sibling = siblings[i]
        if (sibling && sibling.type === NodeTypes.IF) {
          key += sibling.branches.length
        }
      }

      // Exit callback. Complete the codegenNode when all children have been
      // transformed.
      return () => {
        if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            key,
            context
          ) as IfConditionalExpression
        } else {
          // attach this branch's codegen node to the v-if root.
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            key + ifNode.branches.length - 1,
            context
          )
        }
      }
    })
  }
)
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

我们参考 transformIf 方法,就可以实现自定义的 transform 方法,来对 Vnode 和 attrs 进行一些特殊操作了。

参考链接:

https://blog.csdn.net/u014125106/article/details/107130947

# 1.8. generate

# 1.8.1. 目标

generate 阶段的主要过程就是将 transform 转换后的 AST 生成对应的可执行代码,从而在之后 Runtime 的 Render 阶段时,

就可以通过可执行代码生成对应的 VNode Tree,然后最终在页面上映射为真实的 DOM Tree 。

TODO: export function generate

# 1.8.2. Vue3 源码解读

在线客服