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

简介: 从这一章开始我们进入到 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 的作用,以及三大步骤即可都在干什么即可。

相关文章
|
6天前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
106 10
|
4月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
735 5
|
1月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
249 1
|
1月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
145 0
|
2月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
95 0
|
4月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
410 17
|
5月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点
|
5月前
|
存储 JavaScript 前端开发
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
412 6
|
4月前
|
JavaScript 前端开发 API
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
1500 0
|
6月前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
440 2