vue3 源码学习,实现一个 mini-vue(十三):compiler 编译器 - 编译时核心设计原则

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 从这一章开始我们进入到 compiler 编译器模块的实现。在实现 compiler 编译器模块之前,我们先来了解一下 vue 的编译时核心设计原则

前言

原文来自我的 个人博客

从这一章开始我们进入到 compiler 编译器模块的实现。

在实现 compiler 编译器模块之前,我们先来了解一下 vue 的编译时核心设计原则

1. 初探 compiler 编译器

编译器是一个非常复杂的概念,在很多语言中均有涉及。不同类型的编译器在实现技术上都会有较大的差异。

比如你要实现一个 java 或者 JavaScript 的编译器,那就是一个非常复杂的过程了。

但是对于我们而言,我们并不需要设计这种复杂的语言编辑器,我们只需要有一个 领域特定语言(DSL) 的编辑器即可。

DSL 并不具备很强的普适性,它是仅为某个适用的领域而设计的,但它也足以用于表示这个领域中的问题以及构建对应的解决方案。

我们这里所谓的特定语言指的就是:把 template 模板,编译成 render 函数。这个就是 vue 中 编译器 compiler 的作用。

而这也是我们本章所要研究的内容,"vue 编译器是如何将 template 编译成 render 函数的?"

明确好以上概念后,我们创建以下实例,以此来看一下 vue 中 compiler 的作用:

<script>
 const { compile } = Vue
 const template = `
     <div>hello world</div>
   `
 const renderFn = compile(template)

 console.log(renderFn);

</script>

查看最终的打印结果可以发现,最终 compile 函数把 template 模板字符串转化为了 render 函数

那么我们可以借此来观察一下 compile 这个方法的内部实现。我们可以在源码packages/compiler-dom/src/index.ts 中的 第40行 查看到该方法。

image.png

从代码中可以发现,compile 方法,其实是触发了 baseCompile 方法,那么我们可以进入到该方法。

image.png

该方法的代码比较简单,剔除掉无用的内容之后,我们可以得到上图框框圈出的三块内容

总结这段代码(complie),主要做了三件事情:

  1. 通过 parse 方法进行解析,得到 AST
  2. 通过 transform 方法对 AST 进行转化,得到 JavaScript AST
  3. 通过 generate 方法根据 AST 生成 render 函数

整体的代码解析,虽然比较清晰,但是里面涉及到的一些概念,我们可能并不了解。

比如:什么是 AST

所以接下来我们先花费一些时间,来了解编译器中的一些基础知识,然后再去阅读对应的源码和实现具体的逻辑。

2. 模板编译的核心流程

我们知道,对于 vue 中的 compiler 而言,它的核心作用就是把 template模板 编译成 render 函数 ,那么在这样的一个编译过程中,它的一个具体流程是什么呢?

从上一小节的源码中,我们可以看到 编译器 compiler 本身只是一段程序,它的作用就是:把 A 语言,编译成 B 语言。

在这样的一个场景中 A 语言,我们把它叫做 源代码。而 B 语言,我们把它叫做 目标代码。整个的把源代码变为目标代码的过程,叫做 编译 compiler

一个完整的编译过程,非常复杂,下图大致的描述了完整的编译步骤。

image.png

由图可知,一个完善的编译流程非常复杂。

但是对于 vue 的 compiler 而言,因为他只是一个领域特定语言(DSL)编译器,所以它的一个编译流程会简化很多,如下图所示:

image.png

由上图可知,整个的一个编译流程,被简化为了 4 步。

其中的错误分析就包含了词法分析、语法分析。这个我们不需要过于关注。

我们的关注点只需要放到 parsetransformgenerate 中即可。

3. 抽象语法树 AST

通过上一小节的内容,我们可以知道,利用 parse 方法可以得到一个 AST ,那么这个 AST 是什么东西呢?这一小节我们就来说一下。

抽象语法树(AST) 是一个用来描述模板的 JS 对象,我们以下面的模板为例:

<div v-if="isShow">
  <p class="m-title">hello world</p>  
</div>

生成的 AST 为:

{
  "type": 0,
  "children": [
    {
      "type": 1,
      "ns": 0,
      "tag": "div",
      "tagType": 0,
      "props": [
        {
          "type": 7,
          "name": "if",
          "exp": {
            "type": 4,
            "content": "isShow",
            "isStatic": false,
            "isConstant": false,
            "loc": {
              "start": {
                "column": 12,
                "line": 1,
                "offset": 11
              },
              "end": {
                "column": 18,
                "line": 1,
                "offset": 17
              },
              "source": "isShow"
            }
          },
          "modifiers": [],
          "loc": {
            "start": {
              "column": 6,
              "line": 1,
              "offset": 5
            },
            "end": {
              "column": 19,
              "line": 1,
              "offset": 18
            },
            "source": "v-if=\"isShow\""
          }
        }
      ],
      "isSelfClosing": false,
      "children": [
        {
          "type": 1,
          "ns": 0,
          "tag": "p",
          "tagType": 0,
          "props": [
            {
              "type": 6,
              "name": "class",
              "value": {
                "type": 2,
                "content": "m-title",
                "loc": {
                  "start": {
                    "column": 12,
                    "line": 2,
                    "offset": 31
                  },
                  "end": {
                    "column": 21,
                    "line": 2,
                    "offset": 40
                  },
                  "source": "\"m-title\""
                }
              },
              "loc": {
                "start": {
                  "column": 6,
                  "line": 2,
                  "offset": 25
                },
                "end": {
                  "column": 21,
                  "line": 2,
                  "offset": 40
                },
                "source": "class=\"m-title\""
              }
            }
          ],
          "isSelfClosing": false,
          "children": [
            {
              "type": 2,
              "content": "hello world",
              "loc": {
                "start": {
                  "column": 22,
                  "line": 2,
                  "offset": 41
                },
                "end": {
                  "column": 33,
                  "line": 2,
                  "offset": 52
                },
                "source": "hello world"
              }
            }
          ],
          "loc": {
            "start": {
              "column": 3,
              "line": 2,
              "offset": 22
            },
            "end": {
              "column": 37,
              "line": 2,
              "offset": 56
            },
            "source": "<p class=\"m-title\">hello world</p>"
          }
        }
      ],
      "loc": {
        "start": {
          "column": 1,
          "line": 1,
          "offset": 0
        },
        "end": {
          "column": 7,
          "line": 3,
          "offset": 65
        },
        "source": "<div v-if=\"isShow\">\n  <p class=\"m-title\">hello world</p>  \n</div>"
      }
    }
  ],
  "helpers": [],
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": 0,
  "temps": 0,
  "loc": {
    "start": {
      "column": 1,
      "line": 1,
      "offset": 0
    },
    "end": {
      "column": 1,
      "line": 4,
      "offset": 66
    },
    "source": "<div v-if=\"isShow\">\n  <p class=\"m-title\">hello world</p>  \n</div>\n"
  }
}

对于以上这段 AST 而言,内部包含了一些关键属性,需要我们了解:

image.png

如上图所示:

  1. type:这里的 type 对应一个 enum 类型的数据 NodeTypes,表示 当前节点类型。比如是一个 ELEMENT 还是一个 指令

    1. NodeTypes 可在 packages/compiler-core/src/ast.ts 中进行查看 25 行
  2. children:表示子节点
  3. locloction 内容的位置

    1. start:开始位置
    2. end:结束位置
    3. source:原值
  4. 注意:  不同的 type 类型具有不同的属性值:

    1. NodeTypes.ROOT -- 0 :根节点

      1. 必然包含一个 children 属性,表示对应的子节点
    2. NodeTypes.ELEMENT -- 1DOM 节点

      1. tag:标签名称
      2. tagType:标签类型,对应 ElementTypes
      3. props:标签属性,是一个数组
    3. NodeTypes.DIRECTIVE -- 7指令节点 节点

      1. name:指令名
      2. modifiers:修饰符
      3. exp:表达式

        1. type:表达式的类型,对应 NodeTypes.SIMPLE_EXPRESSION, 共有如下类型:

          1. SIMPLE_EXPRESSION:简单的表达式
          2. COMPOUND_EXPRESSION:复合表达式
          3. JS_CALL_EXPRESSIONJS 调用表达式
          4. JS_OBJECT_EXPRESSIONJS 对象表达式
          5. JS_ARRAY_EXPRESSIONJS 数组表达式
          6. JS_FUNCTION_EXPRESSIONJS 函数表达式
          7. JS_CONDITIONAL_EXPRESSIONJS 条件表达式
          8. JS_CACHE_EXPRESSIONJS 缓存表达式
          9. JS_ASSIGNMENT_EXPRESSIONJS 赋值表达式
          10. JS_SEQUENCE_EXPRESSIONJS 序列表达式
        2. content:表达式的内容
    1. NodeTypes.ATTRIBUTE -- 6:属性节点

      1. `name`:属性名
      2. `value`:属性值
    2. NodeTypes.TEXT -- 2:文本节点

      1.  `content`:文本内容
      

总结:

由以上的 AST 解析可知:

  1. 所谓的 AST 抽象语法树本质上只是一个对象
  2. 不同的属性下,有对应不同的选项,分别代表了不同的内容。
  3. 每一个属性都详细描述了该属性的内容以及存在的位置
  4. 指令的解析也包含在 AST 中

所以我们可以说:AST 描述了一段 template 模板的所有内容 。

4. AST 转化为 JavaScript AST,获取 codegenNode

在上一小节中,我们大致了解了抽象语法树 AST 对应的概念。同时我们也知道,AST 最终会通过 transform 方法转化为 JavaScript AST

那么 JavaScript AST 又是什么样子的呢?

我们知道:compiler 最终的目的是吧 template 转化为 render 函数。而整个过程分为三步:

  1. 生成 AST
  2. 将 AST 转化为 JavaScript AST
  3. 根据 JavaScript AST 生成 render

所以,生成 JavaScript AST 的目的就是为了最终生成渲染函数最准备的。

我们以下面的模板为例:

<div>hello world</div>

vue 的源码中分别打印 AST 和 JavaScript AST,得到如下数据:

1. AST


{
  "type": 0,
  "children": [
    {
      "type": 1,
      "ns": 0,
      "tag": "div",
      "tagType": 0,
      "props": [],
      "isSelfClosing": false,
      "children": [
        {
          "type": 2,
          "content": "hello world",
          "loc": {
            "start": { "column": 6, "line": 1, "offset": 5 },
            "end": { "column": 17, "line": 1, "offset": 16 },
            "source": "hello world"
          }
        }
      ],
      "loc": {
        "start": { "column": 1, "line": 1, "offset": 0 },
        "end": { "column": 23, "line": 1, "offset": 22 },
        "source": "<div>hello world</div>"
      }
    }
  ],
  "helpers": [],
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": 0,
  "temps": 0,
  "loc": {
    "start": { "column": 1, "line": 1, "offset": 0 },
    "end": { "column": 23, "line": 1, "offset": 22 },
    "source": "<div>hello world</div>"
  }
}

2. JavaScript AST

{
  "type": 0,
  "children": [
    {
      "type": 1,
      "ns": 0,
      "tag": "div",
      "tagType": 0,
      "props": [],
      "isSelfClosing": false,
      "children": [
        {
          "type": 2,
          "content": "hello world",
          "loc": {
            "start": { "column": 6, "line": 1, "offset": 5 },
            "end": { "column": 17, "line": 1, "offset": 16 },
            "source": "hello world"
          }
        }
      ],
      "loc": {
        "start": { "column": 1, "line": 1, "offset": 0 },
        "end": { "column": 23, "line": 1, "offset": 22 },
        "source": "<div>hello world</div>"
      },
      "codegenNode": {
        "type": 13,
        "tag": "\"div\"",
        "children": {
          "type": 2,
          "content": "hello world",
          "loc": {
            "start": { "column": 6, "line": 1, "offset": 5 },
            "end": { "column": 17, "line": 1, "offset": 16 },
            "source": "hello world"
          }
        },
        "isBlock": true,
        "disableTracking": false,
        "isComponent": false,
        "loc": {
          "start": { "column": 1, "line": 1, "offset": 0 },
          "end": { "column": 23, "line": 1, "offset": 22 },
          "source": "<div>hello world</div>"
        }
      }
    }
  ],
  "helpers": [xxx, xxx],
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": 0,
  "temps": 0,
  "codegenNode": {
    "type": 13,
    "tag": "\"div\"",
    "children": {
      "type": 2,
      "content": "hello world",
      "loc": {
        "start": { "column": 6, "line": 1, "offset": 5 },
        "end": { "column": 17, "line": 1, "offset": 16 },
        "source": "hello world"
      }
    },
    "isBlock": true,
    "disableTracking": false,
    "isComponent": false,
    "loc": {
      "start": { "column": 1, "line": 1, "offset": 0 },
      "end": { "column": 23, "line": 1, "offset": 22 },
      "source": "<div>hello world</div>"
    }
  },
  "loc": {
    "start": { "column": 1, "line": 1, "offset": 0 },
    "end": { "column": 23, "line": 1, "offset": 22 },
    "source": "<div>hello world</div>"
  }
}

由以上对比可以发现,对于 当前场景下 的 AST 与 JavaScript AST ,相差的就只有 codegenNode 这一个属性。

那么这个 codegenNode 是什么呢?

codegenNode 是 代码生成节点。根据我们之前所说的流程可知:JavaScript AST 的作用就是用来 生成 render 函数

那么生成 render 函数的关键,就是这个 codegenNode 节点。

那么在这一小节我们知道了:

  1. AST 转化为 JavaScript AST 的目的是为了最终生成 render 函数
  2. 而生成 render 函数的核心,就是多出来的 codegenNode 节点
  3. codegenNode 节点描述了如何生成 render 函数的详细内容

5. JavaScript AST 生成 render 函数代码

在上一小节我们已经成功了拿到了对应的 JavaScript AST,那么接下来我们就根据它生成对应的 render 函数。

我们知道利用 render 函数可以完成对应的渲染,根据我们之前了解的规则,render 必须返回一个 vnode

例如,我们想要渲染这样的一个结构:<div>hello world</div>,那么可以构建这样的 render 函数:

render() {
  return h('div', 'hello world')
}

我们可以直接创建如下测试实例,来打印最后生成的 render 函数:

<script>
  const { compile, h, render } = Vue
  // 创建 template
  const template = `<div>hello world</div>`

  // 生成 render 函数
  const renderFn = compile(template)
  
  // 打印 renderFn
  console.log(renderFn.toString());
  
  // 创建组件
  const component = {
    render: renderFn
  }

  // 通过 h 函数,生成 vnode
  const vnode = h(component)

  // 通过 render 函数渲染组件
  render(vnode, document.querySelector('#app'))
</script>

renderFn 的值为

function render(_ctx, _cache) {
  with (_ctx) {
    const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("div", null, "hello world"))
  }
}

对于以上代码,存在一个 with 语法,这个语法是一个 不被推荐 的语法,我们无需太过于关注它,只需要知道它的作用即可:

摘自:《JavaScript 高级程序设计》
with 语句的作用是: 将代码的作用域设置到一个特定的对象中
于大量使用 with语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用 with语句。

我们可以把该代码(render)略作改造,直接应用到 render 的渲染中:

<script>
  const { compile, h, render } = Vue
  // 创建组件
  const component = {
    render: function (_ctx, _cache) {
      with (_ctx) {
        const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue // 把 _Vue 改为 Vue

        return (_openBlock(), _createElementBlock("div", null, "hello world"))
      }
    }
  }

  // 通过 h 函数,生成 vnode
  const vnode = h(component)

  // 通过 render 函数渲染组件
  render(vnode, document.querySelector('#app'))
</script>

发现可以得到与:

render() {
  return h('div', 'hello world')
}

同样的结果。

观察两个 render 可以发现:

  1. compiler 最终生成的 render 函数,与我们自己的写的 render 会略有区别。

    1. 它会直接通过 createElementBlock 来渲染 块级元素 的方法,比 h 函数更加 “精确”
    2. 同时这也意味着,生成的 render 函数会触发更精确的方法,比如:

      1. createTextVNode
      2. createCommentVNode
      3. createElementBlock
  2. 虽然,生成的 render 更加精确,但是本质的逻辑并没有改变,已然是一个:return vnode 进行 render 的过程。

6. 总结

整个 compiler 的过程,就是一个把:源代码(template)转化为目标代码(render 函数)  的过程。

在这个过程中,主要经历了三个大的步骤:

  1. 解析( parse ) template 模板,生成 AST
  2. 转化(transformAST,得到 JavaScript AST
  3. 生成(generaterender 函数

这三步是非常复杂的一个过程,内部的实现涉及到了非常复杂的计算方法,并且会涉及到一些我们现在还没有了解过得概念,比如:自动状态机

这些内容我们都会放到下一章在研究吧~

本章我们只需要知道 compiler 的作用,以及三大步骤即可都在干什么即可。

相关文章
|
20小时前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
102 64
|
20小时前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
25天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
22天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
50 7
|
24天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
41 3
|
22天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
39 1
|
22天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
44 1
|
25天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
25天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
29天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
25 0
下一篇
无影云桌面