该系列文章,均以测试用例通过为基准一步步实现一个 vue3 源码副本(学习)。

文字比较长,如果不想看文字可直接转到这里看脑图

由于 compile 和 transform 关联性比较强这里将放在一起去完成。

准备工作

要完成这一部分,首先要了解它的作用是什么?

parse.ts 文中我们完成了解析器的部分,作用是将模板解析成 AST 对象。

在这里 compile.ts 作用就是将这些 AST 如何翻译成 render 函数。

为了更直观的体验 compile 的作用,在 vue 源码里面有一个打包之后的目录:

/vue-next/packages/vue/dist/vue.global.js

然后我们使用第一个测试用例的模板,去编译下看看结果:

1
2
3
4
5
6
7
8
  const source = `
  <div id="foo" :class="bar.baz">
    {{ world.burn() }}
    <div v-if="ok">yes</div>
    <template v-else>no</template>
    <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>
  </div>
       `.trim(),

进行编译(完整示例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  <script src="./vue.global.js"></script>
  <script>
    console.log(Vue, "00");
    const { compile } = Vue;
    const result = compile(
    `
    <div id="foo" :class="bar.baz">
      {{ world.burn() }}
      <div v-if="ok">yes</div>
      <template v-else>no</template>
      <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>
    </div>
    `.trim(),
    { sourceMap: true, filename: "foo.vue" }
    );
    console.log(result, "xx");
  </script>

运行之后 result 结果:

 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
  (function anonymous() {
    const _Vue = Vue;
    const {
      createVNode: _createVNode,
      createCommentVNode: _createCommentVNode,
      createTextVNode: _createTextVNode,
    } = _Vue;

    const _hoisted_1 = { key: 0 };
    // 这里 v-if ... else 里面的 <template>no</template> ?
    // 创建文本虚拟节点,这里为什么直接在 render 外就执行了???
    // 又是怎么做到的???
    const _hoisted_2 = _createTextVNode("no");

    // 神级函数 >>> render
    return function render(_ctx, _cache) {
      with (_ctx) {
        const {
          toDisplayString: _toDisplayString,
          createVNode: _createVNode,
          openBlock: _openBlock,
          createBlock: _createBlock,
          createCommentVNode: _createCommentVNode,
          createTextVNode: _createTextVNode,
          Fragment: _Fragment,
          renderList: _renderList,
        } = _Vue;

        return (
          _openBlock(),
          _createBlock(
            "div",
            {
              // 解析出来的 div 属性, id 和 class
              // parseAttribute 的结果
              id: "foo", // 注意这里是字符串
              class: bar.baz, // 这里是变量形式存在,因为用到了 :class 属于指令解析
            },
            [
              // 这里是孩子节点们
              // 1. 第一个孩子节点,插值
              _createTextVNode(
                // 插值里面的内容调用转换成文本
                _toDisplayString(world.burn()) + " ",
                1 /* TEXT */
              ),
              // 2. 第二个孩子节点 v-if...v-else
              // v-if 指令,参数是 ok
              // 然后这里又是怎么做到 ok ? ... : ...
              // 指令解析的时候 v-if 的处理又是怎么做的,transform/vIf ???
              // 相邻的下一个节点检测是否是 v-if 指令簇???
              // 到底真相如何 ???

              ok
              // 创建 div
                ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
              // 创建 template
                : (_openBlock(),
                   _createBlock(
                     _Fragment,
                     { key: 1 },
                     // 提前被解析出来的 template -> no 文本节点
                     // 难道是提前遍历???将所有的 template 如果是
                     // 静态的就先全部创建出来???
                     [_hoisted_2],
                     64 /* STABLE_FRAGMENT */
                   )),
              // 3. 第三个孩子节点,div v-for
              (_openBlock(true),
               _createBlock(
                 _Fragment,
                 null,
                 // 渲染列表
                 _renderList(list, (value, index) => {
                   return (
                     _openBlock(),
                     _createBlock("div", null, [
                       _createVNode(
                         "span",
                         null,
                         _toDisplayString(value + index),
                         1 /* TEXT */
                       ),
                     ])
                   );
                 }),
                 256 /* UNKEYED_FRAGMENT */
               )),
            ],
            2 /* CLASS */
          )
        );
      }
    };
  });

诸多的疑问等着去解答!!!

但至少有一点很清晰的知道,compile 就是将 AST 编译成 render 函数用的。

知道了最终目的,接下来就是漫长的探索之路了 🏃 🏃 🏃

构造数据,观察最终生成的 VNode 结构(上面代码执行之后结果返回给 result,其实就 是 render 函数):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  result({
    list: [1,2,3],
    ok: true,
    bar: {
      baz: 'xx'
    },
    world: {
      burn() {}
    }
  })

传递一些参数调用之后结果:

 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
  {_isVNode: true, type: "div", props: {}, key: null, ref: null, }
  anchor: null
  appContext: null
  // 三个孩子节点
  children: Array(3)
  0: {_isVNode: true, type: Symbol(Text), props: null, key: null, ref: null, }
  1: {_isVNode: true, type: "div", props: {}, key: 0, ref: null, }
  2: {_isVNode: true, type: Symbol(Fragment), props: null, key: null, ref: null, }
  length: 3
  component: null
  dirs: null
  // 三个动态孩子节点
  dynamicChildren: Array(3)
  0: {_isVNode: true, type: Symbol(Text), props: null, key: null, ref: null, }
  1: {_isVNode: true, type: "div", props: {}, key: 0, ref: null, }
  2: {_isVNode: true, type: Symbol(Fragment), props: null, key: null, ref: null, }
  length: 3
  dynamicProps: null
  el: null
  key: null
  patchFlag: 2
  // 属性
  props: {id: "foo", class: "xx"}
  ref: null
  scopeId: null
  shapeFlag: 17
  suspense: null
  target: null
  targetAnchor: null
  transition: null
  // 标签
  type: "div"
  // 标识为虚拟节点
  _isVNode: true

compile.spec.ts

由于 compile.spec.ts 原来只有一个用例,相对是比较复杂的,不利于学习。

这里将根据 parse.spec.ts 循序渐进的去实现 compile + transform 的功能。

下面所有的测试用例均以 vue.global.js 打包之后的文件,运行结果为前提:

1
2
3
4
5
6
7
  const test = `simple text`;

  const result = compile(test.trim(), {
    sourceMap: true,
    filename: "foo.vue",
  });
  console.log(result, "xx");

通过修改 test 值来得到真实的 render 函数。

完成了 01-simple text 用例之后发现按照 parse.spec.ts 可能不太理想,毕竟 parse 部 分的用例有点多,如果按照那个来这部分也将会很漫长,思考良久应该还是按照 compile.spec.ts 中的用例进行拆分之后右简入难式去通过该用例。

完整用例:

1
2
3
4
5
6
7
8
  const source = `
  <div id="foo" :class="bar.baz">
    {{ world.burn() }}
    <div v-if="ok">yes</div>
    <template v-else>no</template>
    <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>
  </div>
  `.trim()

01-simple text

compiled:

1
2
3
4
5
6
7
8
9
  (function anonymous(
  ) {

    return function render(_ctx, _cache) {
      with (_ctx) {
        return "simple text"
      }
    }
  })

也就是说 "simple text" 最后转变成的 render 函数如上所示。

我们的第一步就是如何来实现 compile 和 transform 能得到这样的结果,这将是该模块完 成第一步 🆙 🆙 🆙 🆙 🆙 🆙 🆙 🆙 🆙 🆙

parse 之后的 ast:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  {type: 0, children: Array(1), loc: {}, helpers: Array(0), components: Array(0), }
  cached: 0
  children: Array(1)
  0:
  content: "simple text"
  loc: {start: {}, end: {}, source: "simple text"}
  type: 2
  length: 1
  codegenNode: undefined
  components: []
  directives: []
  helpers: []
  hoists: []
  imports: []
  loc: {start: {}, end: {}, source: "simple text"}
  temps: 0
  type: 0

在完成 transformText 之后,发现 result.code 是空的,还以为是这里面实现问题的,其 实是 generate 函数还没实现的原因。

所有需要支持的函数都完成之后:

{ast: {…}, code: "function render(_ctx, _cache) {↵ with (_ctx) {↵  return "simple text"}}", map: ""}
  ast: {type: 0, children: Array(1), loc: {…}, helpers: Array(0), components: Array(0), …}
    code: "function render(_ctx, _cache) {↵ with (_ctx) {↵  return "simple text"}}"
    map: ""

会发现最终的 code 即我们想要的 render 函数,和用 vue.global.js 生成的一致。

如果需要将转成函数,这个需要用到 compileToFunction 这个不在我们这个讨论范围,其 实里面也很简单,直接调用 new Function(code) 就行了,来看下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  import { baseCompile as compile } from "../compile.js";

  const source = `simple text`.trim();

  const result = compile(source, {
    sourceMap: true,
    filename: `foo.vue`,
  });
  const render = new Function(result.code);
  console.log(render, "compiled");

输出:

ƒ anonymous(
) {
function render(_ctx, _cache) {
 with (_ctx) {
  return "simple text"}}
} "compiled"

然后会发现结果好像不太对,首先 render 会被一个匿名函数包起来,这个是没问题的,但 是貌似匿名函数没有结束的 } 这个我想问题肯定处在了 generate 里面。

其实是因为 createCodgenContext 里面的 函数没有实现,另外这样是不对的,因为 new Function(code) 会将 code 用一个匿名函数来包裹起来,因此想要得到 render 函数,必 须是以 return 形式返回,因此这里还有个遗漏的地方: genFunctionPreamble 需要去实 现,这里面最后会 push 一个 return 到 code 开头。

更新后输出:

1
2
3
4
5
6
7
8
9
  ƒ anonymous(
  ) {

    return function render(_ctx, _cache) {
      with (_ctx) {
        return "simple text"
      }
    }
  }

在实现 genFunctionPreamble 之后,至此完成了一个得到 render 函数的完整过程。

下面将使用流程图方式进行回顾,分析整个过程。

/img/vue3/compiler-core/compiler-test-simple-text.png

02-pure interpolation 第一个孩子节点

{{ world.burn() }}

测试:

1
2
3
4
5
6
7
  const test01 = `{{ world.burn() }}`;

  const result = compile(test01.trim(), {
    sourceMap: true,
    filename: "foo.vue",
  });
  console.log(result, "xx");

vue.global 结果:

1
2
3
4
5
6
7
  ƒ render(_ctx, _cache) {
    with (_ctx) {
      const { toDisplayString: _toDisplayString } = _Vue

      return _toDisplayString(world.burn())
    }
  }

01-simple text 阶段代码返回的结果:

1
2
3
4
5
  ƒ render(_ctx, _cache) {
    with (_ctx) {
      return  // 这里没任何东西
    }
  }

通过用例 01 大概的完成了一个比较完整的编译过程,要通过该用例应该只需要在这过程中增 加对插值的处理即可。

处理步骤(通过用例 01 总结出的步骤):

baseCompile -> baseParse -> getBaseTransformPreset 得到 transform 函数 -> transform -> generate

  1. baseParse -> ast

  2. getBaseTransformPreset -> 这里并没有什么 transformInterpolation,插值并没有 对应的 transform 函数,而是直接在 generate 中结合 ast.helpers 处理。

  3. transform -> createTransformContext -> traverseNode -> createRootCodegen -> …

    这一步需要处理的应该只有 traverseNode 需要修改,在 switch 里增加 INTERPOLATION 分支,因为 createRootCodegen 里面 root 如果只有一个孩子的情况 下会和用例 01 一样直接赋值 context.codegenNode = root.children[0]

  4. generate -> createCodegenContext -> genFunctionPreamble 默认是 function 模 式 -> push function render(_ctx, _cache) { -> push with (_ctx) -> … -> genNode(ast.codegenNode, context)

    这里需要修改的点应该只有 genNode 里面,也是增加 INTERPOLATION switch 分支, 处理插值部分的代码。

有了上面的初步分析,这里可以比较明确的知道需要修改的点:

  1. DONE traverseNode 中增加 INTERPOLATION 分支

  2. DONE genNode 中增加 INTERPOLATION 分支

  3. DONE genNode 中增加 SIMPLE_EXPRESSION 分支处理插值内的表达式

修正:事实上并没有 transformInterpolation 🤦🤦🤦🤦

修改完之后报错:

transform.js:184 Uncaught TypeError: Cannot read property 'length' of undefined
    at traverseChildren (transform.js:184)
    at traverseNode (transform.js:119)
    at traverseChildren (transform.js:192)
    at traverseNode (transform.js:119)
    at transform (transform.js:133)
    at baseCompile (compile.js:37)
    at compile.html:12

根据报错定位到,在解析 root.children[0] 的时候经过 traverseChildren 里面时候的 parent.children 值为 undefined。

原因是 traverseNode 里面的 NodeTypes.INTERPOLATION 分支没有加 break 导致的,加上 之后:

1
2
3
4
5
6
7
  ƒ render(_ctx, _cache) {
    with (_ctx) {
      const { toDisplayString : _toDisplayString } = _Vue

      return
    }
  } "compiled"

和正确结果相比少了点东西 return _toDisplayString(world.burn())

  1. with 内的解构来源:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
      function generate() {
        // ...
        if (hasHelpers) {
          // 比如:插值处理时用到 TO_DISPLAY_STRING helper
          // 为了避免命名冲突,这里都需要将他们重命名
    
          // traverseNode 里面 context.help(helper)
          push(
            `const { ${ast.helpers
                .map((s) => `${helperNameMap[s]} : _${helperNameMap[s]}`)
                .join(", ")} } = _Vue`
          );
    
          push("\n");
          newline();
        }
        // ...
      }
    
  2. 缺少的 return _toDisplayString(world.burn())

    • generate 中最后 push `return `

    • 执行 genNode(ast.codgenNode, context) 处理缺少的部分

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
        {type: 5, content: {}, loc: {}}
        content:
        content: "world.burn()"
        isConstant: false
        isStatic: false
        loc: {start: {}, end: {}, source: "world.burn()"}
        type: 4 // SIMPLE_EXPRESSION,第二步
        loc: {start: {}, end: {}, source: "{{ world.burn() }}"}
        type: 5 // INTERPOLATION,第一步
      
      1. node 类型首先是 INTERPOLATION ,进入 genInterpolation(node, context)

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        
          function genInterpolation(node, context) {
            const { push, helper, pure } = context;
        
            if (pure) push(PURE_ANNOTATION);
        
            // 这里从 helpers 里面取出 toDisplayString
            push(`${helper(TO_DISPLAY_STRING)}(`);
        
            // 这里生成 `world.burn()` SIMPLE_EXPRESSION 类型
            genNode(node.content, context);
            push(`)`);
          }
        
      2. 取 node.content 调用 genNode(node.content, context) 生成 `world.burn()` 表达式。

        进入 switch node.type === NodeTypes.SIMPLE_EXPRESSION 分支,调用 genExpression(node, context)

🌻 Perfect: 最后结果:

1
2
3
4
5
6
7
  ƒ render(_ctx, _cache) { // generate
    with (_ctx) { // useWithBlock
      const { toDisplayString : _toDisplayString } = _Vue // ast.helpers

      return _toDisplayString(world.burn()) // genNode -> genInterpolation -> genExpression
    }
  } // "compiled"

完整流程图:

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

03-inerpolation in pure div

test:

1
2
3
4
5
6
  const source = `<div>{{ world.burn() }}</div>`.trim();

  const result = compile(source, {
    sourceMap: true,
    filename: `foo.vue`,
  });

vue.global:

 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
2
3
4
5
  ƒ render(_ctx, _cache) {
    with (_ctx) {
      return
    }
  } "compiled"

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

流程分析:

  1. baseParse(template, options) 解析出 ast

  2. transform(ast, …) 递归遍历处理 root.children 生成各节点的 codegenNode

    1. traverseNode(root, context) 核心函数,结合 traverseChildren 通过遍历+递归 处理所有节点,收集对应的 transform* 函数,在结束递归之后回溯过程中执行这些 transform* 来收集节点对应的 codegenNode

      • 遍历所有的 nodeTrasforms[] 来收集当前节点满足条件的 transform* 函数 到 exitFns[] 中,比如: 这里的 ELEMENT 类型(<div></div>)会收集到 transformElementtransformText

      • NodeTypes.ROOT 进入 traverseChildren(node, context) 继续处理 root.children ,这里同时会记录每个节点的 parent 值,ROOT 类型收集 transformText

      • NodeTypes.ELEMENT 也会进入到 traverseChildren(node, context) 处理 node.children ,赋值 parent, 收集 transformTexttransformElement

      • NodeTypes.INTERPOLATION 对于插值节点,不会进入 traverseChildren 而是在 switch 分支中调用 context.helper() 去更新 context.helpers 用来从 Vue 中 解构出需要的函数。

    2. TODO hoistStatic(root, context) 静态提升用的,针对静态节点提升到函数外面(这里暂 时未深入,没用到)

    3. createRootCodegen(root, context) 生成 root.codegenNode ,有可能是来自第一 个且唯一一个孩子节点,分为两个分支具体细节点击函数链接。

    经过 transform 处理之后的 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
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    
      { // 去掉不重要的部分
        "type":0, // ROOT 类型
        "children":[
          {
            "type":1, // ELEMENT 类型
            "tag":"div",
            "tagType":0, // Start
            "children":[
              {
                "type":5, // INTERPOLATION
                "content":{
                  "type":4, // SIMPLE_EXPRESSION
                  "isStatic":false,
                  "isConstant":false,
                  "content":"world.burn()",
                },
              }
            ],
            "codegenNode":{
              // 这里实际上是 root.children[0] 经过 transformElement 之后的结果
              // 变成了VNODE_CALL 在 codegen-generate 处理部分会用到
              "type":13, // VNODE_CALL
              "tag":""div"",
              "children":{
                "type":5, // INTERPOLATION
                "content":{
                  "type":4, // SIMPLE_EXPRESSION
                  "isStatic":false,
                  "isConstant":false,
                  "content":"world.burn()",
                },
              },
              "patchFlag":"1 /* TEXT */", // 这个目前不知道干啥的
              "isBlock":true, // 决定使用 openBlock/createBlock, 还是 createVNode
              "isForBlock":false,
            }
          }
        ],
        "codegenNode":{ // root 根节点的
          // 在 createRootCodegen 中有个处理是,如果root.children 有且只有一个
          // ELEMENT 类型的节点的时候,root.codegenNode 会被这个节点的 codegenNode
          // 覆盖,即root 使用它唯一的孩子节点的 codegenNode
          "type":13, // VNODE_CALL
          "tag":""div"",
          "children":{
            "type":5,
            "content":{
              "type":4,
              "isStatic":false,
              "isConstant":false,
              "content":"world.burn()",
            },
          },
          "patchFlag":"1 /* TEXT */",
          "isBlock":true,
          "isForBlock":false,
        },
      }
    
  3. generate(ast, …) 生成代码片段 -> new Function(context.code)

    1. genFunctionPreamble(ast, context) 主要使用来检测环境从而导入 Vue 实例 (如:~const _Vue = Vue~),最后 render 函数的 `return ` 也是这里生成的。

    2. genNode(ast.codegenNode, context) 对每个 ast 节点结构进行处理,生成对应的 Render 函数相关部件。

    3. genVNodeCall(node, context) 生成节点的参数,调用函数之类的,如: openBlock() , createBlock(...) 及参数列表 createBlock(tag, props, children, patchFlag, ...) ,指令等部件。

    4. patchFlag 是在 transformElement 里面处理值的。

      经过 generate 之后生成的 render 函数:

       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 */))
            }
          }
        })
      

      render({ world: { burn() { return `burn the world !` }}}) 执行之后得到的 VNode 节点:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      
        {
          "_isVNode":true,
          "type":"div",
          "props":null,
          "key":null,
          "ref":null,
          "scopeId":null,
          "children":"burn the world !",
          "component":null,
          "suspense":null,
          "dirs":null,
          "transition":null,
          "el":null,
          "anchor":null,
          "target":null,
          "targetAnchor":null,
          "shapeFlag":9,
          "patchFlag":1,
          "dynamicProps":null,
          "dynamicChildren":[
      
          ],
          "appContext":null
        }
      

04-interpolation in div with props

code: `<div id="foo" :class="bar.baz">{{ world.burn() }}</div>`

这个用例和 用例3 只有一个属性的差别,所以这里只要参考 test 03 来实现 div 属性的 解析和编译即可,所有流程和流程图可参考 03 来实现。

还是老方法,根据跟踪 vue.global debugger 过程来分析整个过程。 期待结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  (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 */))
      }
    }
  })

createStructuralDirectiveTransform(name, fn) 如果存在属性,都会经过这个函数是因 为 if,else-if,else,for 的 transform 都是通过这个创建的,所以在经过 traverseNode 中的 exitFns 收集过程中会执行到这里。

然后这个用例中并没有 v-if, v-for 类似的分支指令,所以这些 transform* 不会被收集 到。

root.children[0]: div 收集 transformElement ,ELEMENT 类型需要收集来解析出 codegenNode。

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

流程图分析:这里和 03 对比多了两部分处理

  1. transform 阶段的 props 解析

    这一阶段的处理发生在 transformElement 中对 props 属性的检测,一旦检测到有属性 列表,需要经过 buildProps 解析出新的属性对象:

    buildProps 之前的 props

     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
    
      props: Array(2)
      0: // 属性 id
      loc: {..., source: "id="foo""}
      name: "id"
      type: 6 // ATTRIBUTE
      value:
      content: "foo"
      loc: {..., source: ""foo""}
      type: 2 // TEXT
      1: // 属性 :class
      arg:
      content: "class"
      isConstant: true
      isStatic: true
      loc: {..., source: "class"}
      type: 4 // SIMPLE_EXPRESSION
      exp:
      content: "bar.baz"
      isConstant: false
      isStatic: false
      loc: {..., source: "bar.baz"}
      type: 4 // SIMPLE_EXPRESSION
      loc: {..., source: ":class="bar.baz""}
      modifiers: []
      name: "bind"
      type: 7 // DIRECTIVE
    

    buildProps 解析之后:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
      Return value: Object
      directives: []
      dynamicPropNames: []
      patchFlag: 2 // CLASS
      props:
      properties: Array(2)
      0:
      key: {type: 4, isConstant: false, content: "id", isStatic: true}
      type: 16 // JS_ARRAY_EXPRESSION
      value: {type: 4, isConstant: false, content: "foo", isStatic: true}
      1:
      key: {type: 4, content: "class", isStatic: true, isConstant: true}}
      type: 16
      value: {type: 4, content: "bar.baz", isStatic: false, isConstant: false,
              type: 15 // JS_PROPERTY
    

    这里面的处理分为两种类型: 1. ATTRIBUTE<6> 类型, 2. DIRECTIVE<7> 指令类型是 分开处理的,普通属性调用 createObjectProperty(key, value) 构建新的对象,指 令属性通过指令名称从 context.directiveTransforms 对象中取出对应的函数进行处 理,比如 v-bind 对应函数 transformBind(prop, node, context) 处理。

    比如: id="foo" 处理之后的

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
      Return value: Object
      key: // 属性名
      content: "id"
      isConstant: false
      isStatic: true
      loc: {source: "id"}
      type: 4 // SIMPLE_EXPRESSION
      type: 16 // JS_PROPERTY
      value: // 属性值
      content: "foo"
      isConstant: false
      isStatic: true
      loc: {source: ""foo""}
      type: 4 // SIMPLE_EXPRESSION
    
      // 包含 key, type, value 三个属性值
    

    比如: :class="bar.baz" 处理之后的

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
      Return value: Object
      props: Array(1)
      0:
      key:
      content: "class"
      isConstant: true
      isStatic: true
      type: 4 // SIMPLE_EXPRESSION
      type: 16 // JS_ARRAY_EXPRESSION
      value:
      content: "bar.baz"
      isConstant: false
      isStatic: false
      type: 4
    
      // 包含 key, type, value,和普通属性类型不一样
      // 这里是 JS_ARRAY_EXPRESSION
    
  2. generate 阶段的 props 解析

    render 函数生成阶段, genVNodeCall 解析 codegenNode, 其中有一个 genNodeList(nodes, …) 这里的 nodes 是 [tag, props, children, patchFlag, ...] 该用例中相比用例03 这里的 props 不是 null ,所以在 genNodeListi = 1 的时候会进入到 genNode(props, context) 去解析属性列表。

    进入之前 props 值:

    1
    2
    3
    
      loc: {source: "<div id="foo" :class="bar.baz">{{ world.burn() }}</div>"}
      properties: (2) [{}, {}] // 这里是 id, class 两个属性
      type: 15 // JS_OBJECT_EXPRESSION
    

    类型为 JS_OBJECT_EXPRESSION<15>genNode 里面会进入 genObjectExpression(node, context) 分支将属性解析成对象,如: {id: "foo", class: bar.baz}

    genObjectExpression 里面对属性的处理主要分两步,先调用 genExpressionAsPropertyKey(node, context) 去处理属性名 key node,完成之后,再调 用 genNode(value, context) 去处理值 value node(最后进入 genExpression(node, context), 因为类型为 SIMPLE_EXPRESSION<4>)。

    最后得到 {id: "foo", class: bar.baz} 作为 createBlock('div', ...) 的第二个参数。

修改完之后运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  (function anonymous(
  ) {

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

        return (_openBlock(), _createBlock("div", { id: "foo" }, _toDisplayString(world.burn()), 1 /* TEXT */))
      }
    }
  })

结果与正确结果又两点缺陷:

  1. 属性漏掉了 class

  2. patchFlag 那里不对,正确应该是 3 /* TEXT, CLASS */

没有报错能走通说明至少逻辑是通的出现上面两个问题原因,溯源起来也很清晰,因为我们 知道 props 在 transform 阶段是 transformElement 里面,generate 阶段是在 genObjectExpression() 中, 而 patchFlag 也是在 transformElement 处理的。

通过在 genObjectExpression() for 循环中增加打印,显示 properties 中只有一个 id 属性,那么属性解析最后是在 buildProps 里面的, bingo!!! 没有实现 transformBind

那么修改点有二:

  1. 在 compile.ts 的 getBaseTransformPreset 增加指令 transform 函数 transformBind

  2. 实现 transformBind

修改之后:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  (function anonymous(
  ) {

    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 */))
      }
    }
  })

扩展 1:增加 camel 修饰符

code: `<div id="foo" :class="bar.baz" :test-prop.camel="bar.bax">{{ world.burn() }}</div>`

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  (function anonymous(
  ) {

    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,
          testName: bar.bax
        }, _toDisplayString(world.burn()), 3 /* TEXT, CLASS */))
      }
    }
  })

因为 transformBind 中有检测修饰符中是否包含 camel ,如果有则会进行驼峰转换,否 则不会转而是将 test-prop 用引号包起来: "test-prop"

扩展 2:动态属性且有 camel 修饰符

code: `<div id="foo" :class="bar.baz" :[prop_name].camel="bar.bax">{{ world.burn() }}</div>`

这个时候需要实现 transform.js 中 createTransformContextcontext.helperString

1
2
3
  helperString(name) {
    return `_${helperNameMap[context.helper(name)]}`;
  }

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  (function anonymous(
  ) {

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

        return (_openBlock(), _createBlock("div", {
          id: "foo",
          class: bar.baz,
          [_camelize(prop_name)]: bar.bax
        }, _toDisplayString(world.burn()), 1 /* TEXT */))
      }
    }
  })

会发现这里多解构了个 _camelize 函数出来,通过函数调用方式去处理动态属性名。

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>`

增加了 <div v-if="ok"></div>

ast: 在经过 parse.ts 之后应该具备看到模板能够分析出 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
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
  {
    type: 0, // ROOT
    children: [
      { // div#foo
        type: 1, // ELEMENT
        tag: 'div',
        tagType: 0, // Start
        props: [
          { // id
            type: 6, // ATTRIBUTE
            name: 'id',
            value: {
              type: 2, // TEXT
              content: 'foo'
            }
          },
          { // :class
            type: 7, // DIRECTIVE
            name: 'bind',
            arg: {
              type: 4, // SIMPLE_EXPRESSION
              content: 'class',
              isStatic: true, // 静态参数名
              isConstant: true
            }, // 参数名 class
            exp: {
              type: 4, // SIMPLE_EXPRESSION
              content: "bar.baz",
              isStatic: false,
              isConstant: false
            }, // 表达式 bar.baz
            modifiers: [], // 修饰符
          }
        ],
        children: [
          { // world.burn
            type: 5, // INTERPOLATION
            content: {
              content: "world.burn()",
              isStatic: false,
              isConstant: false,
              type: 4, // SIMPLE_EXPRESSION
            },
          },
          { // " " 空
            type: 2, // TEXT
            content: ' '
          },
          { // div v-if
            type: 1, // ELEMENT
            tag: 'div',
            tagType: 0, // Start
            children: [
              { // yes
                type: 2, // TEXT
                content: "yes"
              }
            ],
            props: [
              {
                type: 7, // DIRECTIVE
                name: 'if',
                exp: {
                  type: 4,
                  content: "ok",
                  isStatic: false,
                  isConstant: false
                },
                modifiers: []
              }
            ]
          }
        ]
      }, // div#foo
    ],
    codegenNode: undefined
  }

vue.global 结果:

 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
  (function anonymous() {
    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(
                _toDisplayString(world.burn()) + " ",
                1 /* TEXT */
              ),
              ok
                ? (_openBlock(), _createBlock("div", _hoisted_1, "yes"))
                : _createCommentVNode("v-if", true),
            ],
            2 /* CLASS */
          )
        );
      }
    };
  });

这里有几个不同点:

  1. _createBlock 第三个参数 children 变成了数组,且使用了 _createTextVNode() 创建 虚拟节点

  2. 就是多了个新增的那个 div v-if 节点

  3. patchFlag 的变化

先看下修改之前的结果:

 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() {
    return function render(_ctx, _cache) {
      with (_ctx) {
        const {
          toDisplayString: _toDisplayString,
          createVNode: _createVNode,
          camelize: _camelize,
          createTextVNode: _createTextVNode,
          openBlock: _openBlock,
          createBlock: _createBlock,
        } = _Vue;

        return (
          _openBlock(),
          _createBlock(
            "div",
            {
              id: "foo",
              class: bar.baz,
              [_camelize(prop_name)]: bar.bax,
            },
            [, _createVNode("div", null, "yes")]
          )
        );
      }
    };
  });

差异点:

  1. 没有 render 函数外的解构

  2. 没有 render 函数外的 const _hoisted_1 = { key: 0 };

  3. 没有 _createCommentVNode

  4. children 里面的差值节点丢失了

  5. div v-if 节点处理错误

先解决差值问题(第 4 点),这里插值节点为什么会丢失?

补漏:

  1. 实现 transformIf

    createStructuralDirectiveTransform 创建指令(如:v-if, v-else 等)相关的 transform 函数,注意这里的正则: /^(if|else|else-if)$/

    由于指令是存在 node.props 属性里面的,这里会直接遍历所有的属性,找出满足条件 type:DIRECTIVEprop.name 匹配上面的正则的指令。

    因为这里要将所有的指令转成分支类型的结构。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
      {
        type: 9, // IF
        branches: [{
          children: [{ /* 这里保存了转换之前的 v-if 节点 */}]
          condition: {
            content: 'ok',
            // ...
            type: 4, // SIMPLE_EXPRESSION
          }
          // branch
          type: 10, // IF_BRANCH
        }]
      }
    

    最后处理之后得到的 ifNode 包含所有分支 ifNode.branches , branch 即当前要处 理的分支交给返回的那个 transform 函数待递归完成之后取处理得到该分支节点的 codegenNode

    然后经过递归之后,回溯过程中会执行返回的那个函数(transform if) 进入 createCodegenNodeForBranch -> createChildrenCodegenNode -> createCallExpression 创建分支节点 codegen。

  2. 实现 traverseNode 中的 IF(9)IF_BRANCH(10) 分支

  3. 实现 generate 阶段的 if 节点处理

    修改 genNode 增加 IF, IF_BRANCH, JS_CALL_EXPRESSION, JS_CONDITIONAL_EXPRESSION 分支。

    增加 genCallExpression 函数生成参数。

    增加 genConditionalExpression 函数生成 if 分支(如: ok ? ... : ...)

在完成上述三个步骤之后输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    (function anonymous(
    ) {

      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(_toDisplayString(world.burn()), 1 /* TEXT */),
            ok
              ? (_openBlock(), _createBlock("div", { key: 0 }, "yes"))
              : _createCommentVNode("v-if", true)
          ], 2 /* CLASS */))
        }
      }
    })

扩展一:hoisted 支持

  1. genFunctionPreamble(ast, context) 中增加 genHoists(ast.hoists, context)

  2. transform 阶段对 hoisted 处理(transforms/hoistStatic.tswalk() 函数,遍 历所有孩子节点,找出节点 props 中所有属性名为 keyref 的属性)

  3. transform.js 的 context.hoist() 实现,修改 props 属性

    这里 vue.global.js 和 实际 vue transform.ts 中的代码有细微差别,但不影响整体 流程,不知道为何?

实现之后会发现在返回 render 函数之前多了 const _hoisted_1 = { key: 0 } 和一些 函数的解构。

 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)
        ]))
      }
    }
  })

扩展二:v-else 支持

相关脑图链接 –>

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

v-if 和 v-else 的 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
  [
      { // v-if
          "type":1, // ELEMENT
          "ns":0,
          "tag":"div",
          "tagType":0,
          "props":[
              {
                  "type":7, // DIRECTIVE
                  "name":"if",
                  "exp":{
                      "type":4,
                      "content":"ok",
                      "isStatic":false,
                      "isConstant":false,
                  },
              }
          ],
        "children":[{...}],
      },
      { // v-else
          "type":1, // ELEMENT
          "ns":0,
          "tag":"div",
          "tagType":0,
          "props":[
              {
                  "type":7, // else, DIRECTIVE
                  "name":"else",
              }
          ],
        "children":[{...}],
      }
  ]

从之前的实现结果可知, 单个 v-if 的处理是在后面追加一个注释节点,因为在 Render 函数中这些节点是以三目运算符(?:)链接起来组成表达式的,如:

1
2
3
  ok
    ? (_openBlock(), _createBlock("div", { key: 0 }, "yes"))
    : _createCommentVNode("v-if", true)

按照理解,如果增加了 v-else 分支,那么应该需要将 : 后的注释节点替换成真正的 节点?

猜想

从代码语法角度思考, if/else/else-if 肯定必须是连续的,那么这里的 else 如果想要正 确解析那前提必须要有 if 才行。这一步是如何实现呢?

根据上面 ast 解析结果显示,在 parser 阶段,无论是 if 还是 else 都是同等对待的,即它 们都是个指令(9,DIRECTIVE),然后根据之前 v-if 的 transform 可知,v-else 也是在 这个阶段并且是在同一个 transformIf 中处理的,即下面 else TODO 部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  export const transformIf = createStructuralDirectiveTransform(
    /^(if|else|else-if)$/,
    (node, dir, context) => {
      return processIf(node, dir, context, (ifNode, branch, isRoot) => {
        // 能到这里说明 v-if 下所有的 child 都已经处理完毕,可以返回处理
        // codegenNode 的函数了
        return () => {
          console.log({ dir, isRoot });
          if (isRoot) {
            ifNode.codegenNode = createCodegenNodeForBranch(branch, 0, context);
          } else {
            // TODO
          }
        };
      });
    }
  )

所以实现步骤如下

  1. 实现 transformIf 返回的 transform 函数的 else 部分,这部分承担 了 v-else/v-else-if 指令节点的 codegenNode 生成工作。

  2. 实现 processIf() 的分支部分

  3. createCodegenNodeForBranch(branch, index, context) 返回新的 alternate 替换掉 占位的注释分支。

  4. 因为都是在 v-if 的 branches 里面,在 codegen 阶段和 v-if 的处理是一样的,不需 要修改什么。

修改完之后:

 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"))
        ]))
      }
    }
  })

问题列表

  1. compile.js:37 Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

    原因是:在数组里面使用展开符的时候 [], {} 混用了

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
      transform(ast, {
        // 合并选项
        ...options, // 调用 baseCompile 时候的第二个参数
        prefixIdentifiers, // 还不知道是干啥的???
        // 节点转换器合并,外部转换器优先,即使用者可自定义自己的转换器
        // nodeTransforms: [...nodeTransforms, ...(options.nodeTransforms || {})], // FIX: 这里用法有问题修改前
        nodeTransforms: [...nodeTransforms, ...(options.nodeTransforms || [])], // FIX: 修改后
        // 指令转换器,同上。
        directiveTransforms: {
          ...directiveTransforms,
          ...(options.directiveTransforms || {}),
        },
      });