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的模版的定义非常准确。
站在模版解析器的立场上,翻译一下这段话,大意如下:
-
vue的模版语法基于html语法。
- vue拥抱HTML,vue模版语法符合HTML模版规范,是直接可以被浏览器自带的html解析器进行解析的,是可以直接在浏览器中运行的哦。
-
Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。
- vue会对于自身的模版语法进行解析为AST,并且会对AST树进行优化,会区分静态与非静态模版,静态模版片段不会被重新渲染。这是vue的模版解析器所做的性能优化。
-
你也可以不用模板,直接写渲染 (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还会暴露出三个钩子:preTransforms,transforms,postTransforms。分别在修缮每个ASTNode之前,处理Element节点时(processElement),和节点出栈时。
vue的作者认为,像v-model
,innerHTML
,style
等不属于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使用体验一样),会让两种倾向的使用者都满意,敬请期待吧。
参考文献
以上是关于vue template compiler模版解析模块源码解析的主要内容,如果未能解决你的问题,请参考以下文章
前端技能树,面试复习第 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