Vue3 模板编译原理(上)

简介: Vue3 模板编译原理

Vue 的编译模块包含 4 个目录:

compiler-core
compiler-dom // 浏览器
compiler-sfc // 单文件组件
compiler-ssr // 服务端渲染

其中 compiler-core 模块是 Vue 编译的核心模块,并且是平台无关的。而剩下的三个都是在 compiler-core 的基础上针对不同的平台作了适配处理。

Vue 的编译分为三个阶段,分别是:parse、transform、codegen。

其中 parse 阶段将模板字符串转化为语法抽象树 AST。transform 阶段则是对 AST 进行了一些转换处理。codegen 阶段根据 AST 生成对应的 render 函数字符串。

Parse

Vue 在解析模板字符串时,可分为两种情况:以 < 开头的字符串和不以 < 开头的字符串。

不以 < 开头的字符串有两种情况:它是文本节点或 {{ exp }} 插值表达式。

而以 < 开头的字符串又分为以下几种情况:

  1. 元素开始标签 <div>
  2. 元素结束标签 </div>
  3. 注释节点 <!-- 123 -->
  4. 文档声明 <!DOCTYPE html>

用伪代码表示,大概过程如下:

while (s.length) {
    if (startsWith(s, '{{')) {
        // 如果以 '{{' 开头
        node = parseInterpolation(context, mode)
    } else if (s[0] === '<') {
        // 以 < 标签开头
        if (s[1] === '!') {
            if (startsWith(s, '<!--')) {
                // 注释
                node = parseComment(context)
            } else if (startsWith(s, '<!DOCTYPE')) {
                // 文档声明,当成注释处理
                node = parseBogusComment(context)
            }
        } else if (s[1] === '/') {
            // 结束标签
            parseTag(context, TagType.End, parent)
        } else if (/[a-z]/i.test(s[1])) {
            // 开始标签
            node = parseElement(context, ancestors)
        }
    } else {
        // 普通文本节点
        node = parseText(context, mode)
    }
}

在源码中对应的几个函数分别是:

  1. parseChildren(),主入口。
  2. parseInterpolation(),解析双花插值表达式。
  3. parseComment(),解析注释。
  4. parseBogusComment(),解析文档声明。
  5. parseTag(),解析标签。
  6. parseElement(),解析元素节点,它会在内部执行 parseTag()
  7. parseText(),解析普通文本。
  8. parseAttribute(),解析属性。

每解析完一个标签、文本、注释等节点时,Vue 就会生成对应的 AST 节点,并且会把已经解析完的字符串给截断

对字符串进行截断使用的是 advanceBy(context, numberOfCharacters) 函数,context 是字符串的上下文对象,numberOfCharacters 是要截断的字符数。

我们用一个简单的例子来模拟一下截断操作:

<div name="test">
  <p></p>
</div>

首先解析 <div,然后执行 advanceBy(context, 4) 进行截断操作(内部执行的是 s = s.slice(4)),变成:

name="test">
  <p></p>
</div>

再解析属性,并截断,变成:

<p></p>
</div>

同理,后面的截断情况为:

></p>
</div>
</div>
<!-- 所有字符串已经解析完 -->

AST 节点

所有的 AST 节点定义都在 compiler-core/ast.ts 文件中,下面是一个元素节点的定义:

export interface BaseElementNode extends Node {
  type: NodeTypes.ELEMENT // 类型
  ns: Namespace // 命名空间 默认为 HTML,即 0
  tag: string // 标签名
  tagType: ElementTypes // 元素类型
  isSelfClosing: boolean // 是否是自闭合标签 例如 <br/> <hr/>
  props: Array<AttributeNode | DirectiveNode> // props 属性,包含 HTML 属性和指令
  children: TemplateChildNode[] // 字节点
}

一些简单的要点已经讲完了,下面我们再从一个比较复杂的例子来详细讲解一下 parse 的处理过程。

<div name="test">
  <!-- 这是注释 -->
  <p>{{ test }}</p>
  一个文本节点
  <div>good job!</div>
</div>

上面的模板字符串假设为 s,第一个字符 s[0] 是 < 开头,那说明它只能是刚才所说的四种情况之一。

这时需要再看一下 s[1] 的字符是什么:

  1. 如果是 !,则调用字符串原生方法 startsWith() 看看是以 '<!--' 开头还是以 '<!DOCTYPE' 开头。虽然这两者对应的处理函数不一样,但它们最终都是解析为注释节点。
  2. 如果是 /,则按结束标签处理。
  3. 如果不是 /,则按开始标签处理。

从我们的示例来看,这是一个 <div> 开始标签。

这里还有一点要提一下,Vue 会用一个栈 stack 来保存解析到的元素标签。当它遇到开始标签时,会将这个标签推入栈,遇到结束标签时,将刚才的标签弹出栈。它的作用是保存当前已经解析了,但还没解析完的元素标签。这个栈还有另一个作用,在解析到某个字节点时,通过 stack[stack.length - 1] 可以获取它的父元素。

从我们的示例来看,它的出入栈顺序是这样的:

1. [div] // div 入栈
2. [div, p] // p 入栈
3. [div] // p 出栈
4. [div, div] // div 入栈
5. [div] // div 出栈
6. [] // 最后一个 div 出栈,模板字符串已解析完,这时栈为空

接着上文继续分析我们的示例,这时已经知道是 div 标签了,接下来会把已经解析完的 <div 字符串截断,然后解析它的属性。

Vue 的属性有两种情况:

  1. HTML 普通属性
  2. Vue 指令

根据属性的不同生成的节点不同,HTML 普通属性节点 type 为 6,Vue 指令节点 type 为 7。

所有的节点类型值如下:

ROOT,  // 根节点 0
ELEMENT, // 元素节点 1
TEXT, // 文本节点 2
COMMENT, // 注释节点 3
SIMPLE_EXPRESSION, // 表达式 4
INTERPOLATION, // 双花插值 {{ }} 5
ATTRIBUTE, // 属性 6
DIRECTIVE, // 指令 7

属性解析完后,div 开始标签也就解析完了,<div name="test"> 这一行字符串已经被截断。现在剩下的字符串如下:

<!-- 这是注释 -->
  <p>{{ test }}</p>
  一个文本节点
  <div>good job!</div>
</div>

注释文本和普通文本节点解析规则都很简单,直接截断,生成节点。注释文本调用 parseComment() 函数处理,文本节点调用 parseText() 处理。

双花插值的字符串处理逻辑稍微复杂点,例如示例中的 {{ test }}

  1. 先将双花括号中的内容提取出来,即 test ,再对它执行 trim(),去除空格。
  2. 然后会生成两个节点,一个节点是 INTERPOLATION,type 为 5,表示它是双花插值。
  3. 第二个节点是它的内容,即 test,它会生成一个 SIMPLE_EXPRESSION 节点,type 为 4。
return {
  type: NodeTypes.INTERPOLATION, // 双花插值类型
  content: {
    type: NodeTypes.SIMPLE_EXPRESSION,
    isStatic: false, // 非静态节点
    isConstant: false,
    content,
    loc: getSelection(context, innerStart, innerEnd)
  },
  loc: getSelection(context, start)
}

剩下的字符串解析逻辑和上文的差不多,就不解释了,最后这个示例解析出来的 AST 如下所示:

从 AST 上,我们还能看到某些节点上有一些别的属性:

  1. ns,命名空间,一般为 HTML,值为 0。
  2. loc,它是一个位置信息,表明这个节点在源 HTML 字符串中的位置,包含行,列,偏移量等信息。
  3. {{ test }} 解析出来的节点会有一个 isStatic 属性,值为 false,表示这是一个动态节点。如果是静态节点,则只会生成一次,并且在后面的阶段一直复用同一个,不用进行 diff 比较。

另外还有一个 tagType 属性,它有 4 个值:

export const enum ElementTypes {
  ELEMENT, // 0 元素节点
  COMPONENT, // 1 组件
  SLOT, // 2 插槽
  TEMPLATE // 3 模板
}

主要用于区分上述四种类型节点。

Transform

在 transform 阶段,Vue 会对 AST 进行一些转换操作,主要是根据不同的 AST 节点添加不同的选项参数,这些参数在 codegen 阶段会用到。下面列举一些比较重要的选项:

cacheHandlers

如果 cacheHandlers 的值为 true,则表示开启事件函数缓存。例如 @click="foo" 默认编译为 { onClick: foo },如果开启了这个选项,则编译为

{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }

hoistStatic

hoistStatic 是一个标识符,表示要不要开启静态节点提升。如果值为 true,静态节点将被提升到 render() 函数外面生成,并被命名为 _hoisted_x 变量。

例如 一个文本节点 生成的代码为 const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")

下面两张图,前者是 hoistStatic = false,后面是 hoistStatic = true。大家可以在网站上自己试一下。

prefixIdentifiers

这个参数的作用是用于代码生成。例如 {{ foo }} 在 module 模式下生成的代码为 _ctx.foo,而在 function 模式下是 with (this) { ... }。因为在 module 模式下,默认为严格模式,不能使用 with 语句。

目录
相关文章
|
2天前
|
JavaScript 前端开发 CDN
vue3速览
vue3速览
11 0
|
2天前
|
设计模式 JavaScript 前端开发
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
|
2天前
|
缓存 JavaScript 前端开发
Vue3 官方文档速通(中)
Vue3 官方文档速通(中)
9 0
|
2天前
|
缓存 JavaScript 前端开发
Vue3 官方文档速通(上)
Vue3 官方文档速通(上)
9 0
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
7 1
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之三:vue-router 的安装和使用
Vue3+Vite+Pinia+Naive后台管理系统搭建之三:vue-router 的安装和使用
6 0
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之二:scss 的安装和使用
Vue3+Vite+Pinia+Naive后台管理系统搭建之二:scss 的安装和使用
6 0
|
2天前
|
JavaScript 前端开发 API
Vue3 系列:从0开始学习vue3.0
Vue3 系列:从0开始学习vue3.0
8 1
|
2天前
|
网络架构
Vue3 系列:vue-router
Vue3 系列:vue-router
8 2
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之一:基础项目构建
Vue3+Vite+Pinia+Naive后台管理系统搭建之一:基础项目构建
7 1