诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

🌧 序

⭐ ⭐ 由于图片是使用 github 做的图床,没有 CDN 加速,且有些图片比较大(可能

⭐ ⭐ 快 1M) 加载挺慢的,每张图片都有对应的七牛(但不一定是最新的)链接,可能会快一点。

⭐ ⭐ 图片已全部更新到 https://www.cheng92.com/img/...

😄 更新日志

  1. <2020-09-07 Mon> 所有图片修改为 github 地址,后续修改图片可以直接使用,而不 需要上传到七牛。

  2. <2020-09-10 Thu> 图床切换到 码云 gitee 自动同步自 github

  3. <2020-09-11 Fri> 更新图片到博客目录 /static/imgs/... ,文内访问路径: /img/vue3/... ,单独访问加上域名就行: https://www.cheng92.com/img/vue3/...

  4. <2020-09-28 Mon> 所有脑图修改为 svg 格式,建议通过新 tab 打开,有些节点可能包含链 接。

🌩 功能特性分析(parser->transform->codegen)

compiler-01 开始都是针对某个示例做的分析,但是随时示例的模板复杂度增加,脑图 的大小将越来越大,不堪重负,阅读回顾起来也很费劲,在完成了 compiler-01 - compiler-05 之后对整个分析过程也有了大概的了解,另起这一章节的目的就是为了能单纯 的针对某一特定功能绘制对应的流程图,比如:

属性是如何解析的,最后在 render 函数中又是什么?

插值? v-if, v-else, v-for, v-once 等指令又是如何处理的?

这一章节的所有脑图,绘制分为以下阶段,如果流程简单多个阶段可能会在同一张脑图上

  1. parser 阶段得到完整的 ast

  2. compiler 阶段解析 ast 生成节点 codegenNode

  3. generate 阶段利用 codegenNode 组装成 render 函数

  4. 待续……

章节预览:

功能简述
div<div></div>
attributes,静态属性<div id="foo"></div>
v-bind<div :class="bar.baz"></div>
interpolation<div>{{ world.burn() }}</div>
v-if<div><div v-if="ok">yes</div></div>
v-once<div><p v-once>test v-once</p></div>

DONE 01 div, 纯标签

<div></div>

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  (function anonymous(
  ) {
    const _Vue = Vue

    return function render(_ctx, _cache) {
      with (_ctx) {
        const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue

        return (_openBlock(), _createBlock("div"))
      }
    }
  })

/img/vue3/compiler-core/pcg/pcg-01-pure-div.svg

  1. parser 阶段, parseElement -> parseTag

    /img/vue3/compiler-core/pcg/pcg-01-1-parser-pure-div.svg

  2. compiler 阶段, transform -> traverseNode -> traverseChildren ,只有 0,ROOT1,ELEMENT 两个类型分支处理。

    /img/vue3/compiler-core/pcg/pcg-01-2-compiler-pure-div.svg

  3. codegen 阶段,只有 div 的 block 处理(_openBlock(), _createBlock("div"))

    /img/vue3/compiler-core/pcg/pcg-01-3-codegen-pure-div.svg

DONE 02 attributes, 静态属性

<div id="foo"></div>

  1. parser 阶段

    pcg-01 相比较,多了左边 parseTag -> parseAttributes -> parseAttribute 解析属 性 id="foo" 的处理。

    /img/vue3/compiler-core/pcg/pcg-02-1-parser-div-with-id.svg

  2. compiler 阶段:

    pcg-01 相比较,多了 transformElement 中 props 属性的处理,因为这个时候 props.length = 1 里面有一个 id="foo" 属性,需要去调用 buildProps 解析,成下面 的解构:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
      {
        properties: [
          {
            key: { type:4, content: "id", ...}, // SIMPLE_EXPRESSION
            value: {type: 4, content: "foo", ...},
            type: 16 // JS_PROPERTY
          }
        ]
        type: 15, // JS_OBJECT_EXPRESSION
      }
    

    /img/vue3/compiler-core/pcg/pcg-02-2-compiler-div-with-id.svg

  3. codegen 阶段:

    genNodeList([tag, props, children, …], ctx) 解析的时候,这里 props 不再是 null,因此会进入 Props 解析过程:

    genNode(props, ctx) -> 15,JS_OBJECT_EXPRESSION -> genObjectExpression(node, ctx) -> 遍历 node.properties -> genExpressionPropertyKey(key,ctx) 生成属性 名 { id: ~ -> ~genNode(value, ctx) 生成属性值 -> 4, SIMPLE_EXPRESSION -> genExpression(value, ctx) 生成属性值 { id: "foo"

    /img/vue3/compiler-core/pcg/pcg-02-3-codegen-div-with-id.svg

DONE 03 v-bind 指令

<div :class="bar.baz"></div>

结果预览:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  (function anonymous(
  ) {
    const _Vue = Vue

    return function render(_ctx, _cache) {
      with (_ctx) {
        const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

        return (_openBlock(), _createBlock("div", { class: bar.baz }, null, 2 /* CLASS */))
      }
    }
  })
  1. parser 阶段:

    /img/vue3/compiler-core/pcg/pcg-03-1-parser-div-with-bind.svg

  2. compiler 阶段:

    /img/vue3/compiler-core/pcg/pcg-03-2-compiler-div-with-bind.svg

  3. codegen 阶段:

    /img/vue3/compiler-core/pcg/pcg-03-3-codegen-div-with-bind.svg

DONE 04 interpolation, 插值

<div>{{ world.burn() }}</div>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  (function anonymous(
  ) {
    const _Vue = Vue

    return function render(_ctx, _cache) {
      with (_ctx) {
        const { toDisplayString : _toDisplayString, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue

        return (_openBlock(), _createBlock("div", null, _toDisplayString(world.burn()), 1 /* TEXT */))
      }
    }
  })
  1. parser 阶段

    /img/vue3/compiler-core/pcg/pcg-04-1-parser-div-with-interpolation.svg

  2. compiler 阶段

    /img/vue3/compiler-core/pcg/pcg-04-2-compiler-div-with-interpolation.svg

  3. codegen 阶段

    /img/vue3/compiler-core/pcg/pcg-04-3-codegen-div-with-interpolation.svg

DONE 05 v-if 指令(git:0a591b6)

<div><div v-if="ok">yes</div></div>

git commit: 0a591b62d6961526b333afeb5f77c532b3992e31

vue.global:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  (function anonymous(
  ) {
    const _Vue = Vue
    const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue

    const _hoisted_1 = { key: 0 }

    return function render(_ctx, _cache) {
      with (_ctx) {
        const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue

        return (_openBlock(), _createBlock("div", null, [
          ok
            ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
            : _createCommentVNode("v-if", true)
        ]))
      }
    }
  })

差异点:

  • 少了全局作用域下的 _Vue 解构

  • key 没有 hoisted

脑图列表:

  1. parser 阶段

    /img/vue3/compiler-core/pcg/pcg-05-1-parser-div-with-if.svg

  2. compiler 阶段

    /img/vue3/compiler-core/pcg/pcg-05-2-compiler-div-with-if.svg

  3. codegen 阶段

    /img/vue3/compiler-core/pcg/pcg-05-3-codegen-div-with-if.svg

    拓展 1:v-if-else 指令

<div><div v-if="ok">yes</div><div v-else>no</div></div>

vue.global:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  (function anonymous(
  ) {
    const _Vue = Vue
    const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue

    const _hoisted_1 = { key: 0 }
    const _hoisted_2 = { key: 1 }

    return function render(_ctx, _cache) {
      with (_ctx) {
        const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue

        return (_openBlock(), _createBlock("div", null, [
          ok
            ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
            : (_openBlock(), _createBlock("div", _hoisted_2, "no"))
        ]))
      }
    }
  })

pcg-05 差异:

1
2
3
  ok
    ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
    : _createCommentVNode("v-if", true) // 这里没有 elseif, else 分支会创建一个注释节点

1
2
3
  ok
    ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
    : (_openBlock(), _createBlock("div", _hoisted_2, "no")) // 分支节点

造成这差一点是在哪处理的呢???

v-if 指令的 codegen 过程有三个重要因素:

  1. test 生成条件表达式

  2. consequent 生成成立条件(ok=true)表达式的

  3. alternate 生成失败条件(ok=false)表达式的

因此该扩展重点在 alternate 处理 🛬…

在 transform 阶段针对 v-else 的处理逻辑:

traverseNode 中的 exitFns 收集阶段,调用 transformIf 取 transform 函数过程中,有 以下几个重要步骤:

  1. 遍历当前 v-else 节点的所有兄弟节点(siblings=parent.children)

  2. 找到当前节点 node 在 siblings 中的位置 i

  3. while i-- 依次往前找兄弟节点(如果是 COMMENT 节点,删除保存待恢复,如果是 9,IF 节点即找到的目标节点 sibling)

  4. 删除当前的 node 同时调用 createIfBranch 创建 10,IF_BRANCH 类型的分支节点结 构,合并到 sibling.branches

  5. 调用 processCodegen 函数即 transformIf 时候执行会得到生成 codegenNode 的那个函数,执行它获取 tranform 函数 exitFn

  6. 手动执行 traverseNode(node, …) 进行递归遍历该 v-else 节点树(因为在 4 中节点 被删除了,因此主递归线上不会出现这个节点,需要手动执行一次 traverse)

  7. 最后执行 exitFn 生成该 v-else 节点树的 codegenNode

    注意点 :这一步 v-else 替换 alternate 过程中有个 while 循环用来递归查找非 19,JS_CONDITIONAL_EXPRESSION 类型的节点的 alternate 再进行替换,这么做的原 因是 v-if-else 指令的在 render 函数中是通过三目运算符(?:)实现的,一般情况下 : 后面的是一个 comment vnode 类型占位用,当实际有 else 分支的时候会进行替换, 此时替换需要考虑到表达式嵌套的情况,所以需要找到最后那个 comment vnode ,详细 步骤直接看脑图吧。

  1. parser 阶段

    /img/vue3/compiler-core/pcg/pcg-05-01-1-parser-div-with-if-else.svg

  2. transform 阶段

    /img/vue3/compiler-core/pcg/pcg-05-01-2-compiler-div-with-if-else.svg

  3. codegen 阶段

    /img/vue3/compiler-core/pcg/pcg-05-01-3-codegen-div-with-if-else.svg

    拓展 2:v-if-elseif-else 指令

  1. parser 阶段

    相比较 拓展1:v-if-else 这里只是多了一个 v-else-if 这在 parser 阶段没什么区别, 直接参考拓展 1 的脑图。

    /img/vue3/compiler-core/pcg/pcg-05-02-1-parser-div-with-if-eif-else.svg

  2. transform 阶段

    /img/vue3/compiler-core/pcg/pcg-05-02-2-compiler-div-with-if-eif-else.svg

    对比前后结果发现: v-if/v-else-if/v-else 指令体系的实现关键在于 codegenNode 中 三个字段:

    • test ?: 表达式的条件

    • consequent ?: 表达式条件为 true 的时候渲染的节点

    • alternate ?: 表达式条件为 false 的时候渲染的节点

    如果有多级嵌套的情况,会在 alternate 中体现出来,这里面要么是一个节点结构, 要么是一个完整的包含({ test, consequent, alternate }) 嵌套的表达式结构。

    v-else-if 渲染流程查看特定的功能脑图

  3. codegen 阶段

    /img/vue3/compiler-core/pcg/pcg-05-02-3-codegen-div-with-if-eif-else.svg

DONE 06 含 v-once 指令模板(git:2d0bab4)

<div><p v-once>test v-once</p></div>

流程图: /img/vue3/compiler-core/pcg/pcg-06-v-once.svg

git commit: 2d0bab4cfbf3408afe93270d7e9dc8ecd511dbe0

  1. parser 阶段没什么不同,最终都是生成指令类型的 ast 树

  2. 重点在 transform 阶段,先 transformText -> transformElement -> transformOnce 处理

    经过 transformOnce 之后 codegenNode结果变化,从 13,VNODE_CALL 类型变成了 20,JS_CACHE_EXPRESSION 类型。

  3. codegen 阶段的处理,生成 Render 函数,对于 v-once 处理原理是利用缓存机制,第 一次创建节点存储到对应的 context.cache[] 中,后面更新节点时候直接取对应缓存。

    实现关键函数:

TODO 07 v-for 指令

脑图: /img/vue3/compiler-core/pcg/pcg-07-v-for.svg

这里将三个阶段合并在一起了, transform 阶段的解析单独放在了 9. transform 阶段如 何转换 v-for 指令? 这部分和 v-if 解析一样比较复杂,且属于特定的指令解析作为关键 功能进行分析。

所以对于 transform 阶段详细实现和脑图请点击上面链接查看内容。

☀ 关键功能

这一章节是针对整个 vue3 源码解构过程中遇到的问题或一些重要或关键的一些功能,进 行提取解读。

DONE 1. buildProps(node, context) 如何构建 props ?

CLOSED: [2020-09-18 Fri 16:07]

  • State "DONE" from "TODO" [2020-09-18 Fri 16:07]

props 在 compile 阶段是如何处理的,是如何从(示例04)

http://qiniu.ii6g.com/img/20200918160246.png

变成下面这样的:

http://qiniu.ii6g.com/img/20200918160311.png

完整流程: /img/vue3/compiler-core/key/key-01-how-build-props.svg

DONE 2. transformIf() 是如何返回 v-if 指令的 transform 的?

参考用例 05

v-if 指令是如何转换的???

这个转换函数又是怎么来的???

得到这个转换函数过程中做了什么 ???

通过在 traverseNode 中, switch node 阶段之前,收集 transform 函数到 exitFns[] 中的时候,如果遇到了 v-if 指令的元素,会执行 transformIf ,这个时候会遍历解析 node.props 拿到这个 v-if 指令属性,调用 processIf 将该节点转换成

1
2
3
4
  {
    branches: [branch],
    type: 9 // IF
  }

并且用这个新生成的节点结构去替换原来的 div v-if 节点结构。

即:在拿到 transform if 函数之前 div v-if 节点结构已经发生了变化,成为了

type = 9 的结构,最后原来的节点成为了 branches 的元素。

并且原节点的 props 会被清空(避免回溯的时候重复处理)。

transformIf:

 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
  const transformIf = createStructuralDirectiveTransform(
    /^(if|else|else-if)$/,
    (node, dir, context) => {
      return processIf(node, dir, context, (ifNode, branch, isRoot) => {
        // Exit callback. Complete the codegenNode when all children have been
        // transformed.
        return () => { // 这个才是真正在回溯过程中调用的 transform if 函数
          if (isRoot) {
            ifNode.codegenNode = createCodegenNodeForBranch(branch, 0, context);
          } else {
            // attach this branch's codegen node to the v-if root.
            let parentCondition = ifNode.codegenNode;
            while (
              parentCondition.alternate.type ===
                19 /* JS_CONDITIONAL_EXPRESSION */
            ) {
              parentCondition = parentCondition.alternate;
            }
            parentCondition.alternate = createCodegenNodeForBranch(
              branch,
              ifNode.branches.length - 1,
              context
            );
          }
        };
      });
    }
  );

流程图: /img/vue3/compiler-core/key/key-02-transform-if.svg

TODO 3. codegen 如何生成属性(_createBLock(tag, props, …))第二个参数?

如:

1
2
3
4
5
6
  // ...

  return (_openBlock(), _createBlock('div', {
    id: "foo",
    class: bar.baz
  }))

id 和 class 是如何生成对象的。

DONE 4. transform 阶段如何对属性静态提升?

CLOSED: [2020-09-28 Mon 10:55]

  • State "DONE" from "TODO" [2020-09-28 Mon 10:55]

没有 hoist 之前:

1
2
3
4
5
  return (_openBlock(), _createBlock("div", null, [
    ok
      ? (_openBlock(), _createBlock("div", { key: 0 }, "yes"))
      : _createCommentVNode("v-if", true)
  ]))

有 hoist 之后:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  (function anonymous(
  ) {
    const _Vue = Vue
    // ... 省略

    // 提升到 render 函数之后
    const _hoisted_1 = { key: 0 }

    return function render(_ctx, _cache) {
      with (_ctx) {
        // ... 省略
        return (_openBlock(), _createBlock("div", null, [
          ok
            ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
            : _createCommentVNode("v-if", true)
        ]))
      }
    }
  })

/img/vue3/compiler-core/key/key-04-how-hoist-props.svg

transform 阶段是在 执行完 traverseNode() 之后调用 hoistStatic(root,context) 通过 walk() 递归遍历 所有的孩子节点来检测满足条件的 hoist 属性或节点。

即:静态提升动作发生在所有节点的 codegenNode 解析完毕之后(且满足: options.hoistStatic = true)。

codegen 阶段是在 genFunctionPreamable(ast, context) 检测 ast.hoists 数组将需要用 到的函数提升到 render 之外,然后调用 genHoists(ast.hoists) 生成需要提升的属性。

最后根据:

1
2
3
4
5
  node:
  content: "_hoisted_1"
  isConstant: true
  isStatic: false
  type: 4 // SIMPLE_EXPRESSION

最后用 _hoisted_1 来替代 { key: 0 } 这个惊天属性。

DONE 5. codegen 如何生成 if-elseif-else 分支节点 ?

CLOSED: [2020-10-04 Sun 12:47]

  • State "DONE" from "TODO" [2020-10-04 Sun 12:47]

生成分支入口函数产生过程traverseNode 中收集 exitFns 过程中执行 transformIf 经过一些列操作之后得到一个函数,该函数会在当前节点树递归结束后调用,生成 codegenNode

返回的分支节点 codegenNode 结构:

 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
  {
    "type":19,
    "test":{ // ok ? ... : ...
      "type":4,
      "content":"ok",
      "isStatic":false,
      "isConstant":false,
      "loc":{
        // ...
        "source":"ok"
      }
    },
    "consequent":{ // cond ? 这里的代码 : ...
      "type":13,
      "tag":""div"",
      "props":{
        "type":15,
        "loc":{ /* ... */ },
        "properties":[
          {
            "type":16,
            "key":{
              "type":4,
              "isConstant":false,
              "content":"key",
              "isStatic":true
            },
            "value":{
              "type":4,
              "isConstant":false,
              "content":"0",
              "isStatic":false
            }
          }
        ]
      },
      "children":{
        "type":2,
        "content":"yes",
        "loc":{
          "source":"yes"
        }
      },
      "isBlock":true,
      "isForBlock":false,
      "loc":{
        "source":"<div v-if="ok">yes</div>"
      }
    },
    "alternate":{ // cond ? ... : 这里的代码
      "type":14,
      "loc":{
        "source":"",
      },
      "arguments":[
        ""v-if"",
        "true"
      ]
    },
    "newline":true,
  }

处理流程图:

/img/vue3/compiler-core/key/key-05-how-gen-if-branches.svg

DONE 6. transform 阶段如何转换 v-else-if 指令?

示例代码:

1
2
3
4
5
  <div>
    <div v-if="ok">yes</div>
    <div v-else-if="nok">nok</div>
    <div v-else>no</div>
  </div>

/img/vue3/compiler-core/key/key-02-transform-if.svg

DONE 7. 什么时候用 createVNode 什么时候用 createBlock ?

到目前为止大部分的实例都是通过 block 解析的,因为孩子节点只有一个。

孩子节点有多个的时候会进入 VNode 流程,这里相当于创建了一个虚拟节点来将多个孩子 包起来去生成 render 函数。

虚拟节点创建有这么几个函数: createVNode, createCommentVNode, createTextVNode 这些函数什么时候使用?和 openBlock, createBlock 区别在哪?

对比两个示例:

vnode 版 v1:

1
2
3
4
<div id="foo" :class="bar.baz">
    {{ world.burn() }}
    <div v-if="ok">yes</div>
</div>

非 vnode 版 v2:

1
2
3
<div id="foo" :class="bar.baz">
    {{ world.burn() }}
</div>

区别:插值 {{world.burn()}} 有一个兄弟节点 <div v-if="ok">yes</div> 此时插值 节点的处理会不一样,先看结果:

  1. v1 结果(这个结果是有问题的,这也是我们要解决的问题):

    问题: _createTextVNode(, 1 /* TEXT */) 这里少了个参数,应该是那个插值表达式。

    解决方法:加上 genNode: COMPOUND_EXPRESSION 分支处理。

    处理之后: _createTextVNode(_toDisplayString(world.burn()) + " ", 1 /* TEXT */)

     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
    
    const _Vue = Vue;
    const {
    createVNode: _createVNode,
    createCommentVNode: _createCommentVNode,
    createTextVNode: _createTextVNode,
    } = _Vue;
    
    const _hoisted_1 = { key: 0 };
    
    return function render(_ctx, _cache) {
        with (_ctx) {
            const {
            toDisplayString: _toDisplayString,
            createVNode: _createVNode,
            openBlock: _openBlock,
            createBlock: _createBlock,
            createCommentVNode: _createCommentVNode,
            createTextVNode: _createTextVNode,
            } = _Vue;
    
            return (
            _openBlock(),
            _createBlock(
                "div",
                {
                id: "foo",
                class: bar.baz,
                },
                [
                _createTextVNode(, 1 /* TEXT */),
                ok
                    ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
                    : _createCommentVNode("v-if", true),
                ],
                2 /* CLASS */
            )
            );
        }
    };
    
  2. v2 结果:

     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
    
    (function anonymous() {
        const _Vue = Vue;
    
        return function render(_ctx, _cache) {
            with (_ctx) {
            const {
                toDisplayString: _toDisplayString,
                createVNode: _createVNode,
                openBlock: _openBlock,
                createBlock: _createBlock,
            } = _Vue;
    
            return (
                _openBlock(),
                _createBlock(
                "div",
                {
                    id: "foo",
                    class: bar.baz,
                },
                _toDisplayString(world.burn()),
                3 /* TEXT, CLASS */
                )
            );
            }
        };
    });
    

脑图:

/img/vue3/compiler-core/key/key-07-diff-block-vnode.svg

DONE 8. transform 阶段如何做静态提升?

静态提升检测在 transform 阶段, traverseNode 遍历完 ast 树之后,会调用 hoistStatic(root, context) 对所有 codegenNode 进行递归,将需要静态提升的节点提取 到 root.hoists 中。

1
2
3
4
5
6
7
8
function transform(root, options) {
    const context = createTransformContext(root, options);
    traverseNode(root, context);
    if (options.hoistStatic) {
      hoistStatic(root, context);
    }
  // ...
}

静态提升条件:

  1. 根节点必须有一个孩子以上节点,且所有子孙节点都必须是静态节点(isStatic(child, resultCache))

  2. 如果节点是动态节点,则检测其所有属性,提取出静态属性将其提升

  3. 提升之后的属性或节点会保存到 context.hoists 里面

源码脑图: /img/vue3/compiler-core/key/key-08-how-hoist-static.svg

DONE 9. transform 阶段如何转换 v-for 指令?

这里和 transform 如何转换 v-else-if 一样复杂,这里将单独进行分析绘出对应脑图,示 例来源于 v-for 指令 且保持同步。

/img/vue3/compiler-core/key/key-09-how-transform-vfor.svg

测试用例:

1
2
3
<ul class="list">
  <li v-for="user in users">{{user.name}}</li>
</ul>`

transform 阶段前后 ast 对比:

  1. transform 之前的 <li> ast:

     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
    
    // ast.children[0]/*<ul>*/.children[0]/*<li>*/
    var before = {
    type: 1,
    tag: 'li',
    props: [
        {
        type: 7,
        name: 'for',
        exp: {
            type: 4,
            content: 'user in users',
            isStatic: false,
            isConstant: false,
        },
        loc: { source: "v-for='user in users'" },
        },
    ],
    isSelfClosing: false,
    children: [
        {
        type: 5,
        content: {
            type: 4,
            isStatic: false,
            isConstant: false,
            content: 'user.name',
        },
        loc: { source: '{{user.name}}' },
        },
    ]
    
  2. tranform 之后的 <li> ast:

     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
    
    // ast.children[0]/*<ul>*/.children[0]/*<li>*/
    var after = {
    type: 11, // FOR
    source: {
        // 源数据
        type: 4,
        isConstant: false,
        content: 'users',
        isStatic: false,
    },
    valueAlias: {
        // 迭代过程中的值
        type: 4,
        isConstant: false,
        content: 'user',
        isStatic: false,
    },
    parseResult: {
        source: '...' /*对应外面的source*/,
        value: '...' /*对应外面的 valueAlias*/,
    },
    children: [
        {
        type: 1,
        tag: 'li',
        props: [],
        children: [
            {
            // {{user.name}}
            type: 5,
            content: {
                type: 4,
                isStatic: false,
                isConstant: false,
                content: 'user.name',
            },
            },
        ],
        codegenNode: {/*...见 li 的 codegenNode */},
        },
    ],
    codegenNode: {/*...*/},
    }
    
    • type: 11, FOR 类型

    • source: 渲染列表的数据来源,这里是 users

    • valueAlias: 渲染列表项需要的数据 user

  3. transform 之后生成的 <li> codegenNode:

     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
    
    node.codegenNode = {
    type: 11, // FOR
    codegenNode: {
        type: 13, // VNODE_CALL
        children: {
        type: 14, // JS_CALL_EXPRESSION
        arguments: [ // 将作为 callee: _renderList 的参数
            { 
            type: 4,
            isConstant: false,
            content: 'users',
            isStatic: false,
            },
            { // 用来生成函数的 (user) => { retrun `解析后的returns` }
            type: 18, // JS_FUNCTION_EXPRESSION
            params: [ // 这个作为 _renderList 第二个函数的参数
                {
                type: 4,
                isConstant: false,
                content: 'user',
                isStatic: false,
                },
            ],
            returns: { // _renderList 第二个参数函数的返回值
                type: 13,
                tag: '"li"',
                children: {
                type: 5, // INTERPOLATION
                content: {
                    type: 4,
                    isStatic: false,
                    isConstant: false,
                    content: 'user.name',
                },
                },
                patchFlag: '1 /* TEXT */',
                isBlock: true,
                disableTracking: false,
            },
            newline: true, // 这个结合 body 决定是否是 (user) => xx 还是 (user) => { return xxx }
            isSlot: false,
            },
        ],
        },
        patchFlag: '256 /* UNKEYED_FRAGMENT */',
        isBlock: true,
        disableTracking: true,
    },
    

    const children = codegenNode.children

    • children: 生成 _renderList( 函数

      _renderList(

    • children.arguments: 生成 _renderList(users, fn) 函数的 两个参数 usersfn

    • children.arguments[0]: 将生成第一个参数 users

      _renderList(users,

    • children.arguments[1]: 将生成 fn 函数

      _renderList(users, fn

    • children.arguments[1].type: 18,表示是 JS_FUNCTION_EXPRESSION 类型,用 来生成函数的

    • children.arguments[1].params: 作为 fn 函数的参数

      _renderList(users, (user) =>

    • children.arguments[1].returns: 作为 fn函数的返回值

      _renderList(users, (user) => { return (_openBlock(), _createBlock("li", null, _toDisplayString(user.name), 1 /*TEXT*/)) })

从结构可以看出, v-for 指令最后会被替换成下面的结构:

{ type:11, valueAlias:/*这里是迭代当前的数据 user */, source: /* 这里是数据源,如:users*/}

生成的 li codegenNode 结构:

{type: 13, children: {/*...*/}}

renderList(users, (user)=> {return xx}) 最终由 children 内数据呈现:

{type: 14, arguments: [{...}, {...}]}

arguments:

[{ type: 4, content: "users" }, { type: 18, params: {...} returns: {...} }}]

第二个参数成员表(生成: _renderList(users, fn))

memebertypevaluedescription
type18,JS_FUNCTION_EXPRESSION18生成函数 fn 的类型
params4,SIMPLE_EXPRESSION{type:4, content: "user"}fn 第一个参数 user, (user) => xxx
returns13,VNODECALL{type:13, tag: "\"li\"", ...}fn 函数的返回值
body--fn 的函数体, () => body, 和 returns 相冲突,二选一,且 returns 优先

☁ compiler-core: parser

vue3.0 的解析器模块,将 html 模板解析成 AST 对象。

带指令的标签解析全过程(v-bind)

代码: baseParse(`<div v-bind:keyup.enter.prevent="ok"></div>`)

  1. parseChildren ➡️ while

  2. parseElement ⬅️ <div ....></div>

  3. parseTag ➡️ node: div ➡️ parseAttributes 解析属性 ⬅️ v-bind:keyup...></div>

  4. parseAttribute ➡️

    1. 先解析 ="ok" 出值

    2. 后解析 v-bind:keyup.enter.prevent

  5. 最后得到 props[0] -> { name: 'bind', arg: { content: 'keyup', ... }, exp: { content: 'ok', ... }, modifiers: ['enter', 'prevent' ]}

    1. name: 指令的名称, v-bind, @ 都会转成 bind 名称

    2. arg: 表示指令绑定的参数名称,这里可以是动态变量,如: v-bind:[dynamicVarName] ,由 arg.isConstant 标识。

    3. exp: 表示表达式的值

流程图: /img/vue3/compiler-core/parser-test-tag-with-directive-v-bind.png

标签解析(<div>hello world</div>)

代码: baseParse(`<div>hello world</div>`)

  1. parseChildren while 开始解析

  2. 遇到 <d 满足 /^[z-a]/i 进入 parseElement 解析标签

  3. parseElement -> parseTag 解析出名为 div 的标签节点, content = `hello world</div>`

  4. parseElement -> parseChildren 解析出 hello world 文本节点作为 div 节点的 children[0], content = `</div>`

  5. 返回到 parseChildren 解析 </div> 发现 ancestors 有内容且找到了 </div> 匹配的 <div> 节点,最后完成匹配。

流程图:

/img/vue3/compiler-core/parser-test-simple-tag-div.png

自闭合标签(<img/>)的解析,也在 parseTag 里面,有一个针对这个的处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

  // 解析到这里的时候 content 应该是这样的:`/>xxx`
  isSelfClosing = startsWith(context.source, '/>')
  if (type === TagType.End && isSelfClosing) {
    // 如果自闭合没有开始标签,是非法的
    emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
  }

  // 这里判断如果是自闭合的,那么该标签的解析就已经结束了
  advanceBy(context, isSelfClosing ? 2 : 1)

空标签的处理,需要在调用解析函数 baseParse 的时候明确告知它哪些是空标签(如: <img>):

1
2
3
  const ast = baseParse('<img>after', {
    isVoidTag: (tag) => tag === 'img'
  })

isVoidTag 会在 parseElement 的时候被调用,在调用 parseTag 解析完 TagType.Start 之后检测,如果是空标签类型,会直接退出解析即完成该标签的解析 过程(因为是空标签,所以后面的内容就不再属于它了,可以结束了):

1
2
3
4
  // 自闭合的到这里就可以结束了
  if (element.isSelfClosing || context.options.isVoidTag?.(element.tag)) {
    return element;
  }

模板标签的解析(<template></template>)

这个解析和普通标签基本一样,只是在 parseTag 里面解析的时候更新下类型就可以了,很 简单的操作:

 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
  function parseTag(
      context: ParserContext,
      type: TagType,
      parent: ElementNode | undefined
  ): ElementNode {

      // ...省略,这些都可以省略了,和普通标签处理一模一样

      let tagType = ElementTypes.ELEMENT
      const options = context.options
      if (!context.inVPre && !options.isCustomElement(tag)) {
          // ...省略,vue 内置组件类型

          if (tag === 'slot') {
              tagType = ElementTypes.SLOT
          } else if (
              // 所以这里才是重点,作为模板标签必须满足一定的条件
              // 1. 必须包含至少一个属性,且类型是指令
              // 2. 并且满足 const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(`if,else,else-if,for,slot`)
              // 即该指令必须是 if, else, else-if, for, slot,也就是说模板必须用作循环或插槽时使用
              tag === 'template' &&
                  props.some(p => {
                      return (
                          p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
                      )
                  })
          ) {
              tagType = ElementTypes.TEMPLATE
          }
      }

      return {
          type: NodeTypes.ELEMENT,
          ns,
          tag,
          tagType,
          props,
          isSelfClosing,
          children: [],
          loc: getSelection(context, start),
          codegenNode: undefined // to be created during transform phase
      }
  }

所以下面这两个用例就能很好的得到解释了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  test("template element with directives", () => {
    const ast = baseParse('<template v-if="ok"></template>');
    const element = ast.children[0];
    expect(element).toMatchObject({
      type: NodeTypes.ELEMENT,
      tagType: ElementTypes.TEMPLATE, // 这里是模板类型,因为有 `v-if' 指令
    });
  }); // template element with directives

  test("template element without directives", () => {
    const ast = baseParse("<template></template>");
    const element = ast.children[0];
    expect(element).toMatchObject({
      type: NodeTypes.ELEMENT,
      tagType: ElementTypes.ELEMENT, // 而这里依旧是元素类型,因为没有任何指令
    });
  });

解析无效的 </div>

代码: baseParse(`</div>`)

经过的函数:

  1. parseChildren 进入解析 while

  2. parseText 解析出有效文本

  3. 回到 parseChildren while 循环解析 </div> 报错

流程图: /img/vue3/compiler-core/parser-test-invalid-end-tag.png

插值解析 some {{ foo + bar }} text

代码: baseParse(`some {{ foo + bar }} text`)

  1. parseChildren ➡️ while: some {{ foo + bar }} text

  2. parseText ➡️ node[0]: `some`

  3. {{ foo + bar }} text ➡️ parseInterpolation ➡️ node[1]: foor + bar

  4. ` text` ➡️ parseText ➡️ node[2]: `text`

  5. nodes -> root.children

解析过程中需要注意的几点:

  1. 插值解析,首先是匹配 `{{` 然后去的 }} 的索引,最后通过 slice(startIdx, endIdx) 取到要解析的表达式。

  2. `some``text` 不会合并到一个 node 中,因为不是相邻的,请注意合并文 本 ndoe 的前提条件:前一个节点也必须是文本节点类型。

流程图: /img/vue3/compiler-core/parser-test-text-with-interpolation.png

解析 simple text

解析纯文本,只会进入 while 循环中的 !node 检测然后进入 parseText 纯文本解 析,会匹配 <, {{, ]]> 作为纯文本的结束标志。

得到纯文本内容后传递给 parseTextData 替换 /&(gt|lt|amp|apos|quot);/g html 语义符号之后返回给 parseText:content 组织文本节点结构返回。

退出 while 循环,将 node 塞到 root.children[] 里面,作为根节点的孩子节点。

代码: baseParse(`simple text`)

流程图: /img/vue3/compiler-core/parser-test-simple-text.png

🌙 compiler-core: compiler

vu3.0 编译器模块,将 parser 解析得到的 AST 对象编译成对应的 render 函数。

该模块主要实现的三大块,因为这三个关联性很强,因此放到一块了。

  1. compile.ts 编译器主模块

  2. transform.ts 即 transforms/ 目录,语法转换模块,入口函数: transform(),比如: v-if 指令,函数,变量等

  3. codegen.ts 入口函数: generate() ,生成代码字符串,用来调用 new Function(code) 生成 render 函数。

流程图: /img/vue3/compiler-core/compiler.png

01-simple text 编译过程

代码:

1
2
3
  compile(`simple text`, {
    filename: `foo.vue`
  })

01-simple-text 测试用例地址

流程图: /img/vue3/compiler-core/compiler-test-simple-text.png

详细过程分析请点击链接。

02-pure interpolation 编译过程

代码:

1
2
3
  compile(`{{ world.burn() }}`, {
    filename: `foo.vue`,
  })

02-pure-interpolation 测试用例地址

流程图:

/img/vue3/compiler-core/compiler-test-pure-interpolation.png

详细过程分析请点击链接。

03-inerpolation in pure div

代码:

1
2
3
  compile(`<div>{{ world.burn() }}</div>`, {
    filename: `foo.vue`,
  })

用例地址

流程图:

/img/vue3/compiler-core/compiler-test-interpolation-in-div.svg

详细过程分析请点击链接。

04-interpolation in div with props

代码:

1
2
3
  compile(`<div id="foo" :class="bar.baz">{{ world.burn() }}</div>`, {
    filename: `foo.vue`,
  })

用例地址

流程图: /img/vue3/compiler-core/compiler-test-interpolation-in-div-with-props.svg

05-interpolation, v-if, props

1
2
3
4
5
  code = `
  <div id="foo" :class="bar.baz">
  {{ world.burn() }}
  <div v-if="ok">yes</div>
  </div>`

如果放到一张图里面,实在太繁琐了,简化,拆分如下:

/img/vue3/compiler-core/compiler-test-05-div-with-vif.svg

  1. 整体流程及导致结果

  2. parse ast 流程

  3. transform ast 流程,这部分会比较繁琐

  4. codegen generate 流程

transform 阶段流程图: /img/vue3/compiler-core/lib/compiler-lib-04-transform.svg

generate 阶段流程图: /img/vue3/compiler-core/lib/compiler-lib-03-generate.svg