Vue3 编译之美,空白字符的处理

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情


Vue3 为了运行时的性能优化,在编译阶段也是下了不少功夫,在接下来的系列文章中,我们一起去了解 Vue 3 编译过程以及背后的优化思想。由于编译过程平时开发中很难接触到,所以不需要你对每一个细节都了解,你只要对整体有一个理解和掌握即可。

前言

Vue3 整个编译在做三件事情:

  • 模板字符串 template 的解析,将 template 解析成 AST
  • AST 转换
  • 代码生成

第一步 template 解析成 AST 的过程中实现的重点是 parseChildren 函数,这个函数输入的是template输出的即为AST

parseChildren 函数 会将 template 字符串进行逐一解析,将解析出来的node放入到一个数组nodes中。

然后会遍历生成的nodes中每一个节点的信息对空白符进行处理。处理空白符的一个目的是提升编译效率。

在上一篇文章中,我们分析了parseChildren 函数的前半段,template 字符串进行逐一解析生成nodes的过程,那本文将分析parseChildren 函数的后半段,通过对空白字符的处理来提升编译效率。

Vue3 编译之美,抽象语法树的生成?

为什么有空白字符?

我们日常开发时为了代码的美观和可维护,我们会对 template进行格式化,比如添加换行和添加空格,我们将这些换行和空格统称为空白字符。

我相信很少有开发者将template写成没有空格和换行的格式。这样的代码变多之后,既不优雅,也很难维护。

而对于 Vue 的编译器来说,开发者为了美观和可维护写的空白字符在解析的过程中会被当作文本节点解析处理。但这些空白节点对于编译器来说显然是没有什么意义的,所以我们需要移除这些节点,减少后续对这些没用意义的节点的处理,以提高编译效率。

举个例子,如果有如下一段模板代码:

<template>
  <div> 
    <p> 
      1
    </p> 
  </div> 
</template>

在解析时,会将相应的空白字符保留进行到编译流程。

"\n      <div> \n        <p> \n          1\n        </p> \n      </div> \n    "

保留空白字符的 AST

这样一段模板,如果没有不进行空白字符的处理,生成的 AST 是什么样的?

会发现模板中的空白字符都被当做 type 为 2 的空白字符文本节点进行处理。

不保留空白字符的 AST

我们在来看看如果进行空白字符的处理。生成的 AST 是什么样的?

很明显,那些空白字符不见了,也么有type 为 2 的空白字符文本节点

五步处理空白字符

但是其实我们在开发时,有一些空白字符是我们为了代码美观和可维护加上的,还有一些空白字符是模板中本身就需要的。那 Vue 是如何处理的了?

function parseChildren(context, mode, ancestors) {
  ...
  let removedWhitespace = false;
  if (mode !== 2 /* RAWTEXT */ && mode !== 1 /* RCDATA */) {
    ...
    // ①
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      // ②
      if (!context.inPre && node.type === 2 /* TEXT */) {
        // ③
        if (!/[^\t\r\n\f ]/.test(node.content)) {
          const prev = nodes[i - 1];
          const next = nodes[i + 1];
          // 
          if (!prev ||
              !next ||
              (shouldCondense &&
               (prev.type === 3 /* COMMENT */ ||
                next.type === 3 /* COMMENT */ ||
                (prev.type === 1 /* ELEMENT */ &&
                 next.type === 1 /* ELEMENT */ &&
                 /[\r\n]/.test(node.content))))) {
            removedWhitespace = true;
            nodes[i] = null;
          }
          else {
            node.content = ' ';
          }
        }
        else if (shouldCondense) {
          node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ');
        }
      }
      // ④
      else if (node.type === 3 /* COMMENT */ && !context.options.comments) {
        removedWhitespace = true;
        nodes[i] = null;
      }
    }
    ...
  }
  // ⑤  
  return removedWhitespace ? nodes.filter(Boolean) : nodes;
}

第一步

①,removedWhitespace标记是否移除空白字符,遍历整个 nodes(也就是前面生成的 AST 节点数组)。这里需要注意一下,这里的 nodes数组并不是所有节点的集合,而是同一级节点的集合。还是以上面的模板为例:

<template>
  <div> 
    <p> 
      1
    </p> 
  </div> 
</template>

节点解析之后,是存在三个 nodes数组的,而不是一个,原因在于,元素的解析结果本质上是一个递归的过程,原因在于开发者书写的模板是一个嵌套的结构。递归解析才能将嵌套关系全部解析完成。通过不断地递归解析,就可以完整地解析整个模板,并且标签类型的 AST 节点会保持对子节点数组的引用,这样就构成了一个树形的数据结构,所以整个解析过程构造出的 AST 节点数组就能很好地映射整个模板的 DOM 结构。

所以同一层级只需要removedWhitespace变量标记是否移除空白字符。如果为真,同一层级的所有空白字符节点都会被移除。

会发现移除完空白字符节点之后,同层级的节点数量也就减少了,节点数量变少也就减少了后续对无效节点的处理,提升编译效率。

回到源码,接着往下看:

第二步

②,如果是文本节点并且没有在 pre 标签内,走逻辑③,否则走逻辑④。

第三步

③,匹配空白字符(!/[^\t\r\n\f ]/)

如果空白字符,判断空白字符是开始或者结果的节点或者 空白字符与注释相连或者空白字符在两个元素之间并包含换行符 ,标记removedWhitespace为真。并且将当前节点设置为 null 此外,不满足上述三种情况的空白字符都会被压缩成一个空格。

如果不是空白字符,判断 compilerOptions.whitespace的配置。这个配置是做什么的了?

whitespace 的配置默认'condense',也可配置为'preserve'
默认情况下,Vue 会移除/压缩模板元素之间的空格以产生更高效的编译结果:

  1. 元素内的多个开头/结尾空格会被压缩成一个空格
  2. 元素之间的包括折行在内的多个空格会被移除
  3. 文本结点之间可被压缩的空格都会被压缩成为一个空格

将值设置为 'preserve' 可以禁用 (2) 和 (3)。举个例子:

<template>
  <div> 
    <p> 
        1     1
    </p> 
  </div> 
</template>

如果whitespace 的配置是'condense',也就是默认配置。

元素之间的包括折行在内的多个空格会被移除,文本结点之间可被压缩的空格都会被压缩成为一个空格。

如果whitespace 的配置是'preserve'

元素之间的包括折行在内的多个空格不会被移除,文本结点之间可被压缩的空格也都不会被压缩成为一个空格。

第四步

④,如果是注释节点,并且没有将comments配置设置为 true,就会将注释节点移除,设置当前节点为 null。

comments 配置是指compilerOptions.comments。这个配置是指是否移除注释的配置。

默认情况下,Vue 会在生产环境下移除模板内的 HTML 注释。将这个选项设置为 true 可以强制 Vue 在生产环境下保留注释。而在开发环境下注释是始终被保留的。

这个选项一般会用于依赖 HTML 注释的其它库和 Vue 配合使用。

第五步

⑤,会过滤掉这些被标记清除的节点并返回过滤后的 AST 节点数组。

removedWhitespace ? nodes.filter(Boolean) : nodes;

这里在 filter中使用 Boolean() 函数来确定表达式是否为真,也就是在nodes数组中将空元素(null)移除。因为如果是空白字符节点或者是注释节点会被设置为 null 。

总结

结合上一篇文章,Vue3 的整个编译第一步把 template 解析生成 AST 对象就已经全部完结。将开发者编写的template 解析成了 抽象语法树 AST,为后续的流程提供基础对象。

Vue3 编译之美,抽象语法树的生成?


由于编译过程平时开发中很难接触到,所以不需要你对每一个细节都了解,你只要对整体有一个理解和掌握即可。好的,到这里,本篇文章结束。

参考

猜你喜欢

转载自juejin.im/post/7127074001897127943