vue template compiler模版解析模块源码解析

Posted 小章鱼哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue template compiler模版解析模块源码解析相关的知识,希望对你有一定的参考价值。

文章目录

前言

尤雨溪在vue官方文档里对于vue这样定义:

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统。

vue.js拥有自身定义的一套模版语法。相信大家一直对于vue的模版解析模块充满好奇。

本文会深入vue template compiler(模版解析器)源码来讲述vue.js的模版解析的过程。

在此之前,本文会带大家温习一下vue的模版语法。

vue模版语法

定义

官方对于vue模版语法的解释如下:

Vue.js 使用了基于 html 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。

在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

如果你熟悉虚拟 DOM 并且偏爱 javascript 的原始力量,你也可以不用模板,直接写渲染 (render) 函数,使用可选的 JSX 语法。

来自: https://cn.vuejs.org/v2/guide/syntax.html

vue的作者对于vue的模版的定义非常准确。

站在模版解析器的立场上,翻译一下这段话,大意如下:

  1. vue的模版语法基于html语法

    • vue拥抱HTML,vue模版语法符合HTML模版规范,是直接可以被浏览器自带的html解析器进行解析的,是可以直接在浏览器中运行的哦
  2. Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

    • vue会对于自身的模版语法进行解析为AST,并且会对AST树进行优化,会区分静态与非静态模版,静态模版片段不会被重新渲染。这是vue的模版解析器所做的性能优化。
  3. 你也可以不用模板,直接写渲染 (render) 函数。

    • 模版解析器支持多种输入:template模版,渲染函数等。

语法

vue的模版语法包含插值指令两种。

插值

使用包含的片段作为插值定义,支持javascript单表达式。

<span>Message:  msg </span>

指令

使用v-开头的属性定义或者@:作为缩写的属性定义作为指令定义。

<p v-if="seen">现在你看到我了</p>
<a v-bind:href="url">...</a>
<a v-on:click="doSomething">...</a>
<a @click="doSomething">...</a>
<a :href="url">...</a>

vue template compiler模版解析器

现状

当前vue2.5.21版本,它的模版解析器都具有哪些能力,同时做了哪些优化呢?相信很多人还不是很清楚。

当前vue的模版解析器,设计者已经将它从vue中剥离出来,也就是说,它是一个独立的包,开发者是可以单独使用的。

我们可以通过

npm install vue-template-compiler

并引入到代码中,

const compiler = require('vue-template-compiler')

体验template模版到js可执行代码的功能。

vue-template-compiler总共提供了五个API。

  • compiler.compile(template, [options]):编译模板字符串并返回已编译的JavaScript代码及其AST树。
  • compiler.compileToFunctions(template):与compile相似,只返回Javascript代码。
  • compiler.ssrCompile(template, [options]):生成SSR渲染代码。
  • compiler.ssrCompileToFunctions(template):同上,只返回Javascript代码
  • compiler.parseComponent(file, [options]):vue-loader内置,只是用来解析SFC(单文件组件)。

vue本身提供了运行时包和完整包。您可以根据项目需要加载自己需要的包。

详情见:https://cn.vuejs.org/v2/guide/installation.html#运行时-编译器-vs-只包含运行时

运行时包就是不带模版编译器的包。vue搞这波操作主要是提供了预编译的能力。vue的模版编译会在打包的时候完成。比如,在webpack生态下使用vue-loader进行vue解析,vue-loader自带vue-template-compiler,会预编译您的template模版,生成javascript可执行函数。运行时不需要模版编译,加快应用运行时间。

vue在模版预编译的时候规避CSP不兼容还做了一波操作。这个一会再聊,我们先来看定义。

定义

上文说过

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析

vue模版就是一个HTML模版。
因此,vue的compiler模块本质上就是一个HTML解释器

哦?这么简单的吗?

是的,就是一个HTML解释器而已。

为什么vue没有定义DSL,而是使用指令的方式去拥抱HTML呢?

作者尤雨溪在官方文档这样回复,您可以看到vue的理念,就是让使用者多快好省:

有些开发者认为模板意味着需要学习额外的 DSL (Domain-Specific Language 领域特定语言) 才能进行开发——我们认为这种区别是比较肤浅的。首先,JSX 并不是没有学习成本的——它是基于 JS 之上的一套额外语法。同时,正如同熟悉 JS 的人学习 JSX 会很容易一样,熟悉 HTML 的人学习 Vue 的模板语法也是很容易的。最后,DSL 的存在使得我们可以让开发者用更少的代码做更多的事,比如 v-on 的各种修饰符,在 JSX 中实现对应的功能会需要多得多的代码。

来自:https://cn.vuejs.org/v2/guide/comparison.html#HTML-amp-CSS

结构

前文说过,vue compiler是可以于vue中剥离出来的独立模块。可以单独拿来使用。

vue compiler目录是独立于核心模块的。结构大致如下:

├── compiler  // 模版编译模块
│   ├── codegen  // 代码生成器
│   ├── index.js
│   ├── optimizer.js  // 优化器
│   ├── parser  // 核心解释器
│   │   ├── html-parser.js
├── core  // 核心模块

index是compiler的入口文件。

源码如下:

// index.js
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult 
  // 步骤一
  const ast = parse(template.trim(), options)
  // 步骤二
  if (options.optimize !== false) 
    optimize(ast, options)
  
  // 步骤三
  const code = generate(ast, options)
  return 
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  
)

vue template compiler包含三个处理步骤,按顺序排列如下:

  • parser:模版解释器,功能为从HTML模版转换为AST
  • optimizer:AST优化,处理静态不参与重复渲染的模版片段
  • codegen:代码生成器。基于AST,生成js函数,延迟到运行时运行,生成纯HTML。

createCompilerCreator 设计为高阶函数,是一个编译器生成器,目标如注释所言,希望可以支持标准的parser/optimizer/generate结构,也可以支持SSR等。

parser

vue的parser模块使用了改良版的HTML模版解析器,
利用HTML解析器暴露出的start,end,chars,comment钩子,处理vue自身的插值指令

parser的功能就是: template模版 -> AST树。

先介绍一下HTML解释器的原理吧~

HTML解释器

vue的HTML解释器孵化于N年前(我还在玩泥巴的年纪)jQuery的作者John Resig写的一款HTML解释器。如果您设计的模版语法是基于HTML模版,您都可以基于此解释器来做解析工作。

源码地址: https://git.xiaojukeji.com/snippets/855

它不是一款严格意义上讲的编译原理课本上基于Lexer和Parser的编译器(代码会无限膨胀)。

它只是一款简单的解释器。

工具: 一个栈,三个标签匹配正则表达式(开始标签,结束标签,标签属性)。

过程: 不停地遍历HTML字符串,匹配<开始标签,</结束标签,<!--注释标签,普通字符串。

对于匹配到开始标签结束标签注释普通字符串时,添加钩子,抛出事件,开发者自己去处理,随意发挥。

使用方式:

HTMLParser(htmlString, 
    // 开始标签钩子
    start: function(tag, attrs, unary) ,
    // 结束标签钩子
    end: function(tag) ,
    // 普通字符串钩子
    chars: function(text) ,
    // 注释钩子
    comment: function(text) 
);

parser 拿到HTML解析器暴露出的start,end,chars,comment钩子,首先会去处理各式各样语法的AST构造。

start钩子下,匹配各式各样的指令。

chars钩子下,匹配插值

// parser/index.js 片段

...

// 工厂流水线式创建ASTNode

  let element: ASTElement = createASTElement(tag, attrs, currentParent)
...
// 根据不同语法修缮ASTNode
  if (!inVPre) 
        // v-pre指令的修缮
        processPre(element)
        if (element.pre) 
          inVPre = true
        
      
      if (platformIsPreTag(element.tag)) 
        inPre = true
      
      if (inVPre) 
        processRawAttrs(element)
       else if (!element.processed) 
        // structural directives
        // v-for v-if v-once指令的修缮
        processFor(element)
        processIf(element)
        processOnce(element)
        // element-scope stuff
        // element 包含ref,slot,component,v-bind,v-on
        processElement(element, options)
    
... 

(…这块不是很好讲,就是实现作者对于各种语法的AST定义,我在上述代码片段里提供了简单注释,琐碎的一坨代码不好在文章里面全都细说,大家自行去看代码吧haha

值得注意的是,插值直接转换成javascript语法。(它在AST树里就是一个text

parser还会维护自己的。用于维护AST树中每个节点父子节点和兄弟节点之间(通过父子节点取)的关系,爆warning等等。

parser还会暴露出三个钩子:preTransformstransformspostTransforms。分别在修缮每个ASTNode之前,处理Element节点时(processElement),和节点出栈时。

vue的作者认为,像v-modelinnerHTMLstyle等不属于template compiler的范畴。它们算自定义指令。在前文提到的createCompilerCreator创建vue模版解析器的时候,传入您需要的钩子callback。在特定时机解析您的自定义指令。vue在weex平台定义了其他指令(可以叫它们平台相关或自定义指令)。就是一种解耦。然后提高了一些使用自由度。

optimizer

optimizer,顾名思义,优化器。

作者在optimizer里的一段注释说明了optimizer的目的。

/**
 * Goal of the optimizer: walk the generated template AST tree
 * and detect sub-trees that are purely static, i.e. parts of
 * the DOM that never needs to change.
 *
 * Once we detect these sub-trees, we can:
 *
 * 1. Hoist them into constants, so that we no longer need to
 *    create fresh nodes for them on each re-render;
 * 2. Completely skip them in the patching process.
 */

optimizer的目的就是虚拟dom对比时的剪枝。主要就是遍历一棵树,为静态子树标记tag。

codegen

codegen是一个代码生成器。

功能是将AST树转化为一个字符串,字符串内容是一段可执行的javascript代码。

举个栗子


// template
<div> a > 0 ? b : c</div>

可以被codegen为

// output
with(this) 
    return _c(
        \\'div\\',
        [
            _v(
                _s(a > 0 ? b : c)
            )
        ]
    )

_c是createElement创建一个节点,其他的函数释义如下:

  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners

with这个修饰符比较厉害了。它可以扩充作用域,在这个地方使用非常巧妙。

但是with这个东西,在javscript严格模式下被禁止(违反CSP)。

我曾经遇到一个问题,曾经在用webpack3配置babel6打包的时候,babel6在javascript转译的时候,会自动帮我们引入一个插件babel-plugin-transform-strict-mode,它会强制对我们的代码加入use strict头,babel会对于不符合CSP原则的代码报错。导致我的代码一直无法打包。查了下就是因为我引入的框架对于with的使用。

所以vue作者对于with问题的考虑是这样:

var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return _c('div', 
    [
        _v(
            _s(_vm.a > 0 ? _vm.b : _vm.c)
        )
    ]

解决了with的问题。

我们的函数何时运行,不在此节范围内啦,下期再讲吧

template vs JSX

vue的template在拥抱HTML,
另一部分人拥护的React为代表的JSX在拥抱Javascript。

JSX比template有更强的表现力,这不言而喻,vue也开始支持JSX。

vue3已经提供class API(和React使用体验一样),会让两种倾向的使用者都满意,敬请期待吧。

参考文献

https://github.com/vuejs/vue

以上是关于vue template compiler模版解析模块源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Vue的模版解析

前端技能树,面试复习第 45 天—— Vue 基础 | 模版编译原理 | mixin | use 原理 | 源码解析

BASH 文本模版的简单实现 micro_template_compile

Vue项目用了脚手架vue-cli3.0,会报错You are using the runtime-only build of Vue where the template compiler is n

vue编译原理之vue-template-compiler

升级到 vue3 找不到模块 'vue-template-compiler/package.json'