其他
技术干货 | Vue模板编译原理
小编推荐:本文来源于普惠泛前端问卷平台技术小组的源码阅读分享活动。团队组织了Vue2.0源码阅读和分享,旨在提升成员的技术水平和代码阅读能力,在分享之余沉淀和输出了技术博客数篇。该文章主要针对Vue2.0的模板编译部分源码进行分析和梳理。希望能够对Vue源码阅读感兴趣的同学提供一些思路和启发。
编译器初探
const { render, staticRenderFns } = compileToFunctions(template, someOptions, this)
模板解析:将 template 模板字符串转换成 AST 树:由 parse 函数完成,其担当了词法分析和句法分析的责任 代码优化:遍历 AST 树去寻找并标记所有纯静态节点,为虚拟 DOM 的渲染优化提供支持:由 optimize 函数完成。 代码生成:将 AST 树转化为目标平台的渲染函数 render:由 generate 函数完成,同时会基于 optimize 的标记结果来生成 staticRenderFns。
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
/**
* 调用parse将模板解析成AST
*/
const AST = parse(template.trim(), options)
optimize(AST, options)
// 根据给定的AST生成目标平台的代码
const code = generate(AST, options)
return {
AST,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
模板解析:parse
parseHTML(template, {
start (tag, attrs, unary, start, end) {},
end (tag, start, end) {},
chars (text: string, start: number, end: number) {},
comment (text: string, start, end) {}
}
export function parseHTML (html, options) {
const stack = []
let index = 0
let lAST, lASTTag
while (html) {
lAST = html
if (!lASTTag || !isPlainTextElement(lASTTag)) {
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
}
}
}
}
// Clean up any remaining tags
parseEndTag()
function advance (n){}
function parseStartTag () {}
function handleStartTag (match) {}
function parseEndTag (tagName, start, end) {}
}
stack:存储未解析完的非一元标签的节点的栈,主要目的是为了给未正常闭合的节点进行闭合处理。 index:记录当前字符流的读入位置(读取的 html 的位置)。 lAST:存储剩余的尚未解析的模板内容(html 剩余部分)。 lASTTag:存储stack栈顶节点的标签。
const endTagMatch = html.match(endTag)
const startTagMatch = parseStartTag()
将非一元标签推入 stack 栈内。 调用 start 钩子。
<div class="myroot" :desc="desc">
<p>内容1<div></p>
<inner-con
v-for="f in data"
:key="f.key"
@click="onClick(f)"/>
</div>
第五次循环
从 stack 末尾开始遍历寻找第一个相同标签 如果没找到,那么这段 html 会被自动忽略。对于 br 标签和 p 标签的解析会做特殊处理,以保持与浏览器的行为一致。类似此例中,会做一个补足工作,对 p 先调用 start 钩子,再调用 end 钩子。 如果找到了,然而不是 stack 的尾元素,意味着中间有一些未闭合的标签。parseEndTag 会做一个自动闭合工作。类似<div><h1><h2></div>,当我们处理这个结束标签的时候,stack=['div','h1','h2']。对div闭合处理前,会先将 h1,h2 进行闭合。 因此,stack 依旧剩余两个 div 标签。
经过 handleStartTag 和 parseEndTag 做的一些闭合和补足工作,实际上其最终的效果应当是等同于下面这段 html:
<div class="myroot" :desc="desc">
<p>内容1</p>
<div>
<p></p>
<inner-con
v-for="f in data"
:key="f.key"
@click="onClick(f)"/>
</div>
</div>
▌句法分析
template: 我们需要解析的模板 。 html options: 编译器的一些平台化选项参数,即不同平台(web、weex)上的vue编译渲染的个性化的一些配置参数。
let transforms, preTransforms, postTransforms
function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
transforms = pluckModuleFunction(options.modules, 'transformNode')// 根据编译器的选项参数对平台化的变量进行了初始化以及一些其他变量的定义
const stack = []
let root
let currentParent
let inVPre = false
let inPre = false
parseHTML(template, {
start (tag, attrs, unary, start, end) {},
end (tag, start, end) {},
chars (text, start, end) {},
comment (text, start, end) {}
}
}
stack: 存储处理过的未结束的节点; root: 定义根节点,作为最终的 AST 树返回; currentParent: 当前处理节点的父节点; inVPre 和 inPre: 这两个变量主要用来标记节点是否在有v-pre属性的标签内或者在pre标签内,这决定最终将如何对节点的属性进行处理。工具函数介绍。
process 函数:对当前元素描述对象做进一步处理(在元素描述对象上添加各种各样的具有标识作用的属性)。 我们看 parse 方法的大纲就可以发现其内包含大量 process 函数,从命名上也可以看出这是针对元素各个属性和指令进行了区分处理(v-for、v-if、v-pre等)。
transform 函数:指的是存储在 transforms, preTransforms,postTransforms 这三个变量中的函数。它们来源于对应平台下的 options.modules 配置。以 web 平台为例,如下图。这三个变量其实是根据调用时机进行区分命名的, 是不同调用时机下的函数。 和 process 函数的功能相同,唯一的区别就是平台化的区分:process 函数是不区分平台执行的,而 transform 函数是处理对应平台下的相关逻辑的。
根据 parseHTML 的结果(tag 和 attrs)以及当前的父节点CurrentParent 创建 AST 元素 进行 AST 元素的处理:包括最早执行的 preTransforms,inVPre 和 inPre 赋值以及一些 process 操作 根元素的赋值 若元素为一元标签(自闭合),对元素做关闭处理(closeElement);反之,将当前元素赋值给 CurrentParent,并将其推入 stack 内,为下一个元素的任务1做准备。
start(tag, attrs, unary){
let element = createASTElement(tag, attrs, currentParent)
preTransforms[i](element) // 对当前节点 element 遍历执行 preTransforms
// inVPre 和 inPre 赋值
process*(element) //一些 process 操作
if (!root) {
root = element
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
}
end (tag) {
const element = stack[stack.length - 1]
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
}
补充:AST元素的主要组成结构
{
"type": 1, //
"tag": "div", // 标签
"attrsList": [], // 原始属性数组,parseHTML 解析结果
"attrsMap": {}, // 原始属性数组对应属性字典
"children": [], // 子元素数组
"staticClass": "myroot",
"hasBindings": false, // 是否包含动态属性
}
▌process 和 transform 是如何对 AST 元素进行处理的
通过 getAndRemoveAttr 方法判断当前元素是否有相应属性并提取出相应属性值。 对属性值进行解析,并通过 extend 方法将解析结果挂载到 el 上。
// v-for="(obj,ind) in list"
export function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
// exp = "(obj,ind) in list"
const res = parseFor(exp)
// res = { for: 'list', alias: 'obj', iterator1: 'ind' }
if (res) {
extend(el, res)
}
}
}
父子结构的维系:将当前节点存储到其父节点的 children 数组中。 元素剩余属性的处理 processElement:processElement 其实是其它剩余的一系列 process 函数的集合,包括 processKey、processRef、processAttrs 等等。通过这些函数对节点的剩余属性进行处理,在这个过程中,还会调用 transforms 方法。web 平台上的 transforms 主要是对 class 和 style 做动态绑定值和静态值的处理。 pre 状态的重置:将 inVPre 和 inPre 这两个属性回退为 false,避免影响后续节点属性的处理。 调用 postTransforms 方法组对节点做最后的处理。
function closeElement (element) {
currentParent.children.push(element)
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
{
"tag": "div",
"children": [
{
"tag": "p",
"children": [
{
"text": "内容1"
}
]
},
{
"tag": "div",
"children": [
{
"tag": "p",
},
{
"tag": "inner-con",
"for": "data",
"alias": "f",
"key": "f.key",
"hasBindings": true,
"events": {
"click": {
"value": "onClick(f)",
"dynamic": false
}
}
}
],
}
],
"staticClass": "myroot",
"hasBindings": true
}
表达式文本节点一定是非静态节点。表达式文本节点如其命名,节点内含有类似我是{{name}},今年{{age}}岁 这样的字面量表达式。 普通文本节点或者注释节点一定是静态节点。 自身或其祖先节点是 pre 标签或者有 v-pre 属性的节点属于静态节点。 符合以下任意一个条件的节点属于非静态节点:1) 有动态绑定属性,e.g. <div :class="testClass"> ; 2) 自身或其祖先节点含有 v-for、 v-if 结构化属性 ;3)自定义组件; 子节点为非静态,则其父节点也为非静态节点。
▌代码生成:generate
generate 方法的核心是:根据 AST 树生成渲染函数。注意,generate 方法最终产出的有两部分:
render 渲染函数体 所有纯静态节点的渲染函数体数组;
{
"tag": "inner-con",
"for": "data",
"alias": "f",
"key": "f.key",
}
export function genFor ( el: any, state: CodegenState): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
with(this){
return _c(
'div',
{staticClass:"myroot",attrs:{"desc":desc}},
[ _c('p',[_v("内容1")]),
_c('div',
[ _c('p'),
_l((data),function(f){
return _c('inner-con',
{key:f.key,attrs:{"data":f},on:{"click":function($event){return onClick(f)}}})
})],2)])}
总结
葛佳丽