在使用Vue进行实际开发的过程当中,大多数时候使用模板来建立HTML,模板功能强大且简洁直观,最终模板会编译成渲染函数,本文主要介绍模板编译的具体过程。
html
Vue从可否处理 template 选项的角度分为两个版本:运行时+编译器、只包含运行时。运行时+编译器版本也被称为完整版。只包含运行时比完整版体积小30%左右,使用只包含运行时版本须要借助 vue-loader 或 vueify 等工具编译模板。
本文从 web 平台的编译入口开始探究 Vue 完整版的模板编译过程。在 src/platforms/web/entry-runtime-with-compiler.js 文件下的 $mount 方法中经过 compileToFunctions 方法将模板编译成渲染函数。编译方法的生成过程以下如所示:
前端
function baseCompile (template, options){
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模板编译的核心,由以上代码能够看出,编译的第一步是将模板经过 parse 函数解析成 AST(抽象语法树),第二步优化AST,第三步根据优化后的抽象语法树生成包含渲染函数字符串的对象。
其次,向createCompiler() 函数传入基本配置对象 baseOptions,返回包含函数属性 compile 与 compileToFunctions 的对象。
compile 函数接收两个参数:模板字符串以及编译选项。另外还经过闭包引用了前面传入的基础编译函数 baseCompile 与基本编译配置对象 baseOptions。该函数的功能主要有三点:
vue
一、合并基础配置选项与传入的编译选项,生成 finalOptions。
二、收集编译过程当中的错误。
三、调用基础编译函数 baseCompile。
node
compileToFunctions 函数是将 compile 函数做为参数传入 createCompileToFunctionFn() 函数生成的返回值。createCompileToFunctionFn 函数定义一个缓存变量 cache,而后返回函数 compileToFunctions。模板字符串的编译比较费时,使用缓存变量 cache 是为了防止重复编译,从而提高性能。
compileToFunctions 函数接受三个参数:模板字符串、编译选项、Vue实例。该函数的主要做用有如下五点:
web
一、缓存编译结果,防止重复编译。
二、检测内容安全策略,保证 new Function() 可以使用。
三、调用 compile 函数将模板字符串转成渲染函数字符串
四、调用 createFunction 函数将渲染函数字符串转成真正的渲染函数
五、打印编译错误。
正则表达式
最后,将要编译的模板字符串、编译选项与 Vue 的实例对象传入 compileToFunctions 函数,返回包含 render 与 staticRenderFns 属性的对象。
render 为最终生成的渲染函数字符串,staticRenderFns 为存储静态根节点渲染函数字符串。这些函数字符串会经过 new Function() 来生成最终的渲染函数。
Vue利用函数柯里化的技巧生成编译模板的方法,在初读代码的时候让人感受十分繁琐,实际倒是设计的十分巧妙。
这样设计的缘由是 Vue 可以在不一样平台运行,好比在服务器端作SSR,也能够在weex下使用。不一样平台都会有编译过程,所依赖的基本编译选项 baseOptions 会有所不一样。Vue 将基础的编译过程抽离出来,而且能够在多处添加编译器选项,而后将添加的编译器选项和基本编译选项合并起来,最终灵活实如今不一样平台下的编译。
express
关于 AST 的概念参照以下维基百科的描述:
编程
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。之因此说语法是“抽象”的,是由于这里的语法并不会表示出真实语法中出现的每一个细节。
在源代码的翻译和编译过程当中,语法分析器建立出分析树,而后从分析树生成AST。一旦AST被建立出来,在后续的处理过程当中,好比语义分析阶段,会添加一些信息。
数组
Vue 编译过程的核心的第一步是调用 parse 方法将模板字符串解析为 AST 。
浏览器
const ast = parse(template.trim(), options)
复制代码
生成AST的过程分为两步:词法分析、句法分析。parse 函数中实现的功能主要是句法分析,词法分析功能由 parse 内部调用的 parseHTML 函数来完成。咱们首先分析模板字符串作词法分析的过程。
parseHTML 函数的省略具体细节的代码以下所示:
export function parseHTML (html, options) {
const stack = []
let last, lastTag
/*省略。。。*/
while (html) {
last = html
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {/*省略具体实现*/}
let text, rest, next
if (textEnd >= 0) {/*省略具体实现*/}
if (textEnd < 0) { text = html }
if (text) { advance(text.length) }
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
} else {
/*省略具体实现*/
}
if (html === last) {/*省略具体实现*/}
}
parseEndTag()
function advance (n) {
index += n
html = html.substring(n)
}
function parseStartTag () {/*省略具体实现*/}
function handleStartTag (match) {/*省略具体实现*/}
function parseEndTag (tagName, start, end) {/*省略具体实现*/}
}
复制代码
parseHTML 函数的具体功能以下图所示:
<div><span></div>
复制代码
parseHTML 函数利用栈的数据结构来实现的:解析到开始标签时,将开始标签推入到数组 stack 中,变量 lastTag 始终指向栈顶元素。当解析到结束标签时,会与栈顶的开始元素相匹配,若是是一对非一元标签,则将栈顶开始标签推出栈,同时继续向前解析。若是匹配失败或者解析完毕后栈中仍有开始标签,则表示非一元标签未闭合。
如上例所示,先将 <div> 推入数组 stack 中,继续解析后将 <span> 也推入栈中,此时栈顶标签为 <span>,解析到结束标签 </div> 时会与栈顶标签对比,<span> 与 </div> 不是一对非一元便签,则说明模板字符串缺乏 <span> 的结束标签。
parseHTML 函数首先判断将要解析的字符串是否是在纯文本标签里的内容,纯文本标签是指 <script>、<style>、<textarea> ,若是为纯文本标签的内容,则抽取纯文本标签里的内容,直接使用传入的 chars() 进行处理。
若是不是在纯文本标签里的内容,则根据字符 '<' 的位置来判断要解析的字符串开头是标签仍是文本。若是是文本,则使用传入的 chars() 进行处理。
若是是标签,则有五种可能性:
一、如果注释标签 <!---->,则使用传入的 comment() 方法处理注释内容。
二、如果条件注释标签<!--[]>,则不作任何处理,直接跳过。
三、如果文档类型声明<!DOCTYPE>,则不作任何处理,直接跳过。
四、如果结束标签,则调用 parseEndTag() 函数处理。
五、如果开始标签,则调用 parseStartTag() 与 handleStartTag() 函数进行处理。
总之,parseHTML 函数解析到文本调用 chars() 方法处理,解析到注释标签调用 comment() 方法处理,解析到条件注释标签与文档类型声明跳过不作处理, chars() 与 comment() 做为传入的方法将会在讲解 parse() 方法时加以讲解。
对开始标签与结束标签的处理相对麻烦一些,在调用传入的处理开始标签与结束标签的函数以前,parseHTML 函数会先对其作一些处理。
解析开始标签是会首先调用 parseStartTag() 函数,而后将函数返回值做为参数传入 handleStartTag() 函数进行处理。
parseStartTag() 函数利用正则表达式来解析开始标签,各项解析结果做为 match 对象的属性。
match = {
tagName: '', // 开始标签的标签名
attrs: [], // 标签中各属性的信息数组
start: startIndex, // 标签开始下标
unarySlash: undefined || '/', // 判断标签是否为一元标签
end: endIndex // 标签结束下标
}
复制代码
handleStartTag() 函数接收 match 对象做为参数。主要有如下五个功能:
一、stack 栈顶标签为 <p>,且当前解析的开始标签为段落式内容模型时,调用 parseEndTag() 方法闭合 <p>。
二、当前解析标签能够省略结束标签,且与栈顶标签相同,则调用 parseEndTag() 方法关闭当前解析标签而后给出警告。
三、格式化 match.attrs 存储属性数组,格式化后 attrs 为对象数组,每一个对象有两个属性:name(属性名)、value(解码后的属性值)。
四、将当前解析标签的信息推入到 stack 中,并将变量 lastTag 的值改为栈顶标签名称。
五、调用传入的 start 函数,参数为当前解析标签的信息。
解析结束标签是会调用 parseEndTag() 函数。该函数主要有如下四个功能:
一、检测是否缺乏闭合标签。
二、处理 stack 栈中剩余的标签。
三、处理 </br> 与 </p> 标签。
四、调用传入的 end() 方法处理结束标签。
在 handleStartTag() 函数中有讲到遇到 <p> 调用 parseEndTag() 函数的状况。如下是 <p> 标签MDN的介绍:
起始标签是必需的,结束标签在如下情形中能够省略。
<p>元素后紧跟<address>, <article>, <aside>, <blockquote>,
<div>, <dl>, <fieldset>, <footer>, <form>, <h1>, <h2>, <h3>,
<h4>, <h5>, <h6>, <header>, <hr>, <menu>, <nav>, <ol>, <pre>,
<section>, <table>, <ul>或另外一个<p>元素;
或者父元素中没有其余内容了,并且父元素不是<a>元素。
复制代码
若是 <p> 后面跟以上元素,parseEndTag() 函数会模拟浏览器的行为,自动补全 <p> 标签。以下所示:
<p><h5></h5></p>
复制代码
上述html代码会被解析成以下代码:
<p></p><h5></h5><p></p>
复制代码
在 handleStartTag() 函数中讲到:当前解析标签能够省略结束标签,且与栈顶标签相同,则调用 parseEndTag() 方法。 parseEndTag() 会闭合第二个标签,并因第一个标签未闭合而发出警告。
<li>123<li>456
复制代码
上述html代码会被解析成以下代码,并警告第一个标签未闭合。
<li>123<li></li>456
复制代码
另外,仅仅写下闭合标签 </p> 与 </br> 时,浏览器会将 </p> 转化成 <p></p>,将 </br> 转化成 <br> 。Vue在转换模板字符串的时候与浏览器保持一致,在 handleStartTag() 函数中将这两个闭合标签进行转换处理。
句法分析函数 parse 的代码在 /src/compiler/parser/index.js 中。省略具体内容的 parse 函数代码以下所示:
export function parse (template,options){
const stack = []
let root
let currentParent
/*省略。。。*/
parseHTML(template, {
// 省略一些参数
start (tag, attrs, unary, start, end) {/*省略具体实现*/},
end (tag, start, end) {/*省略具体实现*/},
chars (text, start, end) {/*省略具体实现*/},
comment (text, start, end) {/*省略具体实现*/}
})
return root
}
复制代码
变量 root 为 parseHTML 函数的返回值,即最终生成的AST。Vue将模板中节点分为四种:标签节点、包含字面量表达式的文本节点、普通文本节点、注释节点,其中普通文本节点与注释节点都是纯文本节点,算做同一类型。
AST中的节点描述对象有三种类型:标签节点描述对象、表达式文本节点描述对象、纯文本节点描述对象。不一样类型节点描述对象的基本属性以下所示:
// 标签节点类型描述对象基本属性
element = {
type: 1, // 标签节点类型标识
tag: '', // 标签名称
attrsList: [], // 对象数组,对象存储着标签属性的名和值
attrsMap: {}, // 标签属性对象,以键值对的形式存储标签属性
rawAttrsMap: {} // 将attrsList转化为对象,其属性为标签属性名
parent: {}, // 父标签节点
children: [], // 子节点数组
start: Number, // 开始标签第一个字符在html字符串的位置
end: Number // 结束标签最后一个字符在html字符串的位置
}
// 表达式文本节点描述对象基本属性
expression = {
type: 2, // 表达式文本节点类型标识
expression: '', // 表达式文本字符串,变量被 _s() 包裹
tokens: [] // 存储文本的token,有文本和表达式两种类型
text: '', // 文本字符串
start: Number, // 表达式文本第一个字符在html字符串的位置
end: Number // 表达式文本最后一个字符在html字符串的位置
}
// 纯文本节点描述对象基本属性
text = {
type: 3, // 纯文本节点类型标识
text: '', // 文本字符串
start: Number, // 纯文本第一个字符在html字符串的位置
end: Number // 纯文本最后一个字符在html字符串的位置
}
复制代码
AST是树状结构的对象,经过标签节点描述对象的 parent 与 children 来实现。parent 属性指向父节点元素描述对象,children 属性存储着该节点全部子节点的元素描述对象。根节点的 parent 属性值为 undefined 。
变量 stack 与 currentParent 配合使用来完成将子节点正确添加到父节点 children 属性中的任务。stack 是栈的数据结构,用来存储当前解析的节点的父节点以及祖先节点。currentParent 指向当前解析内容的父节点。
在词法分析的过程当中,解析节点时会调用对应的函数进行处理,下面分别加以介绍。
在 start() 函数中,首先会调用 createASTElement() 函数,将标签名、标签属性以及标签的父节点做为参数传入,生成一个标签节点类型描述对象。
let element = createASTElement(tag, attrs, currentParent)
复制代码
此时标签节点对象以下所示:
element = {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
复制代码
若是开始标签是 svg 或者 math,则额外添加 ns 属性,属性值与标签名相同。接着向 element 对象添加 start、end 属性,使用 attrsList 属性格式化 rawAttrsMap 属性。
而后调用 preTransforms 函数数组中的每个函数来处理 element 对象,以及以 process 开头的一系列函数。在 parse 函数所在的文件中声明了不少 process* 函数,好比 processFor、processIf、processOnce等。这些函数和 preTransforms 函数数组中的函数做用都是同样的,都是用来对当前元素描述对象作进一步处理。这是出于平台化的考虑,将这一系列的函数放在不一样的文件夹里。process 系列函数是通用的,而 preTransforms 函数数组根据平台不一样而不一样。
这些根据不一样属性对 element 进行不一样处理的过程至关繁杂,本文的主旨是讲述模板字符串到渲染函数的编译过程,这些具体的属性处理会在后续文章讲述相应指令时详细阐述。
最后,判断开始标签是否为一元标签,若是是则调用 closeElement 方法进行处理,closeElement 方法的具体内容将在下一节介绍;若是不是则将 element 对象赋值给变量 currentParent,做为后续解析的父节点存在,并将 element 对象推入 stack 栈中。
结束标签处理函数 end 逻辑相对简单,代码以下所示:
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
}
复制代码
首先将栈顶节点取出赋值给 element 变量,而后删除 stack 中栈顶节点,并将 currentParent 变量指向栈顶节点。这样作由于当前节点做为父节点的状况已经处理完毕,要将做用域还给上层节点。
接着将 end 方法添加在结束标签所在的节点上,最后将 element 变量传入 closeElement 函数。
closeElement 函数除了调用 postTransforms 数组中的函数处理节点以外,还根据不一样状况调用对应的 process* 对节点进行进一步处理。该函数的另外一主要功能是将当前节点推入到父节点 children 属性中,并添加 parent 节点指向父节点。
currentParent.children.push(element)
element.parent = currentParent
复制代码
函数 chars 的核心代码以下所示:
let res
let child
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].t!== ' ') {
child = {
type: 3,
text
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
复制代码
函数 chars 会调用 parseText 函数处理文本字符串,parseText 主要解析包含字面量表达式的文本,若是文本中没有字面量表达式则返回空值,不然返回包含 expression 与 tokens 属性的对象。
若文本包含字面量表达式,则生成 type 值为2的节点描述对象,若为纯文本,则生成 type 值为3的节点描述对象。而后将字符串开始字符的位置 start 与 结束字符的位置 end 添加到节点对象上,最后将节点描述对象推入到父节点的 children 数组属性中。
举个例子,其中 title 为变量数据:
<div>标题:{{title}}。</div>
<div>456<div>
复制代码
第一个<div> 标签下的包含的包含字面量表达式的文本被 parseText 解析后返回以下对象:
{
expression: "标题:"+_s(title)+"。",
tokens: [ "标题:", { @binding: "title" }, "。" ]
}
复制代码
第一个<div> 标签下文本最终生成的节点描述对象为:
{
type: 2,
expression: "标题:"+_s(title)+"。",
tokens: [ "标题:", { @binding: "title" }, "。" ],
text: "标题:{{title}}。",
start: Number,
end: Number
}
复制代码
第二个<div> 标签下文本最终生成的节点描述对象为:
{
type: 3,
text: "456",
start: Number,
end: Number
}
复制代码
注释文本处理的逻辑跟 chars 函数中处理不含字面量表达式的文本很像,只是生成的 type 值为3的节点描述对象多了一个属性:isComment,其值为 true,是注释文本描述节点的标识。处理函数 comment 代码以下所示:
comment (text, start, end) {
if (currentParent) {
const child = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
}
复制代码
AST 的优化途径主要是检测出不须要更改的DOM的纯静态子树,这样作有两个好处:
一、将纯静态节点描述对象提高为常量,在从新渲染时不用从新生成。
二、在 Virtual DOM patching 的过程跳过这部分。
AST的优化是经过 optimize 函数来完成的,函数代码以下:
export function optimize (root, options) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
markStatic(root)
markStaticRoots(root, false)
}
复制代码
AST优化逻辑相对比较简单,分为两步:
一、使用 markStatic 函数标记静态节点。
二、使用 markStaticRoots 方法标记静态根节点。
标记静态节点函数 markStatic 代码以下所示:
function markStatic (node) {
node.static = isStatic(node)
if (node.type === 1) {
/* 省略一些代码 */
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
/* 省略处理 if else 等指令的状况,具体讲解指令时补充 */
}
}
复制代码
markStatic 函数首先调用 isStatic 函数判断是否为静态节点,在节点描述对象上添加布尔变量 static 标识是否为静态节点。
若是是元素节点且有子节点则递归调用 markStatic 函数处理每一个子节点,若是子节点中有一个不是静态节点的,该元素节点就不是静态节点,即 static 属性值为 false。
判断节点是否为静态的函数 markStatic 代码以下:
function isStatic (node) {
if (node.type === 2) { return false }
if (node.type === 3) { return true }
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
复制代码
判断节点是否为静态的规则有如下四条:
一、含有字面表达式的文本节点为非静态节点。
二、纯文本节点为静态节点。
三、节点描述对象拥有 pre 属性(即标签有 v-pre 属性)为静态节点。
四、若是一个标签节点同时知足如下条件即为静态节点:没有使用 v-if、v-for、没有使用除 v-once 外的其它指令、非平台保留的标签、不是组件、不是带有 v-for 的 template 标签的直接子节点、节点的全部属性的 key 都知足静态 key。
标记静态根节点的函数 markStaticRoots 代码以下所示:
function markStaticRoots (node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
/* 省略处理 if else 等指令的状况,具体讲解指令时补充 */
}
}
复制代码
属性 staticRoot 是用来标记节点是否为静态根节点的,只有标签节点才有多是静态根节点,判断静态根节点的标准为同时知足一下三点:
一、节点 static 为 true,即为静态节点。
二、标签节点拥有子节点。
三、标签节点不是只拥有一个纯文本节点。
之因此要求标签节点不是只拥有一个纯文本节点,是将一个这样的节点标记为静态根节点收益比较小,最好是让其老是保持新鲜。
判断当前节点是否为静态根节点以后,会递归调用 markStaticRoots 函数处理该节点的每个子节点。
总之,通过AST优化函数 optimize 处理以后,每一个节点的描述对象上增长了布尔类型的属性 static 用来标识是否为静态节点。type 属性为1的标签节点描述对象上增长了布尔类型的属性 staticRoot 用来标识是否为静态根节点。
将优化后的 AST 转化成渲染函数字符串是在 generate 函数中完成的,代码以下所示:
export function generate (ast, options){
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
复制代码
generate 函数代码看似简单,其中包含的逻辑却比较复杂,由于要对各类各样的状况进行处理。本文经过一个简单的例子来大体阐述AST生成渲染函数字符串的过程,对示例以外的其它指令例如:v-for、v-if 等存在时的状况在后续的具体文章中再加以介绍。
<div id="app" class="home" @click="showTitle">
<div class="title">标题:{{title}}。</div>
<div class="content">
<span>456</span>
</div>
</div>
复制代码
以上模板字符串通过 parse 函数解析成AST,而后通过 optimize 函数优化以后的AST以下所示:
ast = {
tag: "div",
type: 1,
attrs: [{dynamic: undefined,end: 13,name: "id",start: 5,value: ""app""}],
attrsList: [
{ end: 13,name: "id",start: 5,value: "app" },
{ end: 45,name: "@click",start: 27,value: "showTitle" }
],
attrsMap: {id: "app", class: "home", @click: "showTitle"},
end: 178,
events: {click: {dynamic: false,end: 45,start: 27,value: "showTitle"}},
hasBindings: true,
parent: undefined,
plain: false,
rawAttrsMap: {
@click: {end: 45,name: "@click",start: 27,value: "showTitle"},
class: {end: 26,name: "class",start: 14,value: "home"},
id: {end: 13,name: "id",start: 5,value: "app"}
},
start: 0,
static: false,
staticClass: ""home"",
staticRoot: false,
children: [
{
tag: "div",
type: 1,
attrsList: [],
attrsMap: {class: "title"},
children: [{
text: "标题:{{title}}。",
type: 2,
end: 87,
expression: ""标题:"+_s(title)+"。"",
start: 74,
static: false,
tokens: (3) ["标题:", {@binding: "title"}, "。"]
}],
end: 93,
parent: {/*对父节点描述对象的引入*/},
plain: false,
rawAttrsMap: {
class: {end: 73,name: "class",start: 60,value: "title"}
},
start: 55,
static: false,
staticClass: ""title"",
staticRoot: false
},
{
text: " ",
type: 3,
end: 102,
start: 93,
static: true
},
{
tag: "div",
type: 1,
attrsList: [],
attrsMap: {class: "content"},
children: [
{
tag: "span",
type: 1,
attrsList: [],
attrsMap: {},
children: [{text: "456",type: 3,end: 145,start: 142,static: true}],
end: 152,
parent: {/*对父节点描述对象的引入*/},
plain: true,
rawAttrsMap: {},
start: 136,
static: true
}
],
end: 167,
parent: {/*对父节点描述对象的引入*/},
plain: false,
rawAttrsMap: {class: {end: 122,name: "class",start: 107,value: "content"}},
start: 102,
static: true,
staticClass: ""content"",
staticInFor: false,
staticRoot: true
}
]
}
复制代码
id 为 app 的 <div> 节点描述对象 children 属性数组中有三个对象,这是由于其两个 <div> 子节点中间有空格,算做一个纯文本节点。
generate 函数首先根据传入的配置参数对象 options 实例化 CodegenState 对象。类 CodegenState 的代码以下所示:
export class CodegenState {
constructor (options) {
this.options = options
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
this.directives = extend(extend({}, baseDirectives), options.directives)
const isReservedTag = options.isReservedTag || no
this.maybeComponent = (el) => !!el.component || !isReservedTag(el.tag)
this.onceId = 0
this.staticRenderFns = []
this.pre = false
}
}
复制代码
在这里咱们重点关注该对象上的 dataGenFns 与 staticRenderFns 属性。staticRenderFns 属性是一个数组,存储着静态根节点的渲染函数字符串,是 generate 函数的返回对象属性之一。dataGenFns 数组中存储着选项 modules 中的 genData 函数,分别处理标签描述对象的class 与 :class属性、style 与 :style属性。
dataGenFns = [
function genData (el) {
var data = '';
if (el.staticClass) {
data += "staticClass:" + (el.staticClass) + ",";
}
if (el.classBinding) {
data += "class:" + (el.classBinding) + ",";
}
return data
},
function genData(el) {
var data = '';
if (el.staticStyle) {
data += "staticStyle:" + (el.staticStyle) + ",";
}
if (el.styleBinding) {
data += "style:(" + (el.styleBinding) + "),";
}
return data
}
]
复制代码
在生成 CodegenState 的实例化对象 state 以后,generate 函数将 AST 和 state 传入 genElement 函数,最终生成渲染函数字符串。genElement 函数跟示例html有关的代码以下:
export function genElement (el, state) {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
}
/* 省略一些判断条件 */
else {
let code
/* 省略为标签组件的状况 */
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
/* 省略一些代码 */
return code
}
}
复制代码
根据示例生成的 ast 状况,在genElement 函数中会首先调用:
data = genData(el, state)
复制代码
genData 函数主要是处理标签中的属性,将其转化成字符串返回。在标签拥有 class 或者 style 属性时会循环调用前面讲过的 state.dataGenFns 数组中的函数加以处理。当前标签描述对象的属性通过 genData 函数处理后 data 值为:
"{staticClass:"home",attrs:{"id":"app"},on:{"click":showTitle}}"
复制代码
而后调用 genChildren 函数处理当前标签描述对象 children 属性数组中的对象,即处理其子节点描述对象。
const children = genChildren(el, state, true)
复制代码
genChildren 函数代码以下所示:
function genChildren (el,state,checkSkip,altGenElement,altGenNode) {
const children = el.children
if (children.length) {
const el = children[0]
/* 省略一些代码 */
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }`
}
}
复制代码
函数的主要逻辑是使用 genNode 函数分别处理对象的 children 属性中的各个节点描述对象。
function genNode (node, state) {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
复制代码
genNode 函数根据节点类型的不一样分别调用不一样的函数进行处理,使用 genElement 函数处理标签节点、使用 genComment 函数处理注释节点、使用 genText 函数处理文本节点。
注释节点的处理方式比较简单,直接用 _e() 函数的字符串形式包装注释节点的 text 属性。
function genComment(comment) {
return `_e(${JSON.stringify(comment.text)})`
}
复制代码
文本节点的处理函数 genText 使用 _v() 函数的字符串形式包装文本内容,纯文本节点内容为节点描述对象 text 属性的值,含字面量表达式的文本内容为节点描述对象 expression 属性的值。
function genText (text) {
return `_v(${text.type === 2 ? text.expression // no need for () because already wrapped in _s() : transformSpecialNewlines(JSON.stringify(text.text)) })`
}
复制代码
接着讲 genElement 函数,在拿到子节点的函数字符串后,使用逗号拼接标签名、标签属性字符串、子节点函数字符串,最后使用 _v() 函数的字符串形式加以包装。使用 new Function 处理后变成以下代码:
_c(tag,data,children)
复制代码
class 为 content 的 <div> 是静态根节点,在 genElement 中会调用 genStatic 函数处理。
function genStatic (el, state) {
/* 省略一些代码 */
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
/* ··· */
return `_m(${ state.staticRenderFns.length - 1 }${ el.staticInFor ? ',true' : '' })`
}
复制代码
处理后的函数字符串会被推入到 state.staticRenderFns 数组中,静态根节点函数字符串以下:
"with(this){return _c('div',{staticClass:"content"},[_c('span',[_v("456")])])}"
复制代码
总之,函数 generate 的返回值为:
{
render: "with(this){return _c('div',{staticClass:"home",attrs:{"id":"app"},on:{"click":showTitle}},[_c('div',{staticClass:"title"},[_v("标题:"+_s(title)+"。")]),_v(" "),_m(0)])}",
staticRenderFns: ["with(this){return _c('div',{staticClass:"content"},[_c('span',[_v("456")])])}"]
}
复制代码
编译实例代码生成的函数字符串以及静态根节点函数字符串通过 new Function 处理以后以下所示:
render = function() {
with(this){
return _c(
'div',
{
staticClass:"home",
attrs:{"id":"app"},
on:{"click":showTitle}
},
[
_c(
'div',
{staticClass:"title"},
[_v("标题:"+_s(title)+"。")]
),
_v(" "),
_m(0)
]
)
}
}
复制代码
_c 函数定义在 src/core/instance/render.js 中,用来建立 VNode。其它的编译渲染的内部函数定义在 src/core/instance/render-helpers/index.js 的 installRenderHelpers 函数中。
_v 函数用来建立文本类型的 VNode;_s 函数用来处理字面量表达式返回结果字符串;_m 函数处理静态根节点。这些根据渲染函数生成 VNode 的过程会在后续讲解 Virtual DOM 时详细阐述。
Vue 使用函数柯里化的技巧来实现不一样平台下的编译函数,核心编译过程分为三步:根据模板字符串生成AST、优化AST、根据AST生成渲染函数。
生成AST的过程分为两步:词法分析、语法分析。在词法分析的过程当中,逐个字符的解析html字符串。首先判断待解析的字符串开头是元素标签仍是文本,标签又分为:开始标签、结束标签、注释标签、文档类型声明标签和条件注释标签,而后根据待解析字符串的类型作相应的处理。句法分析函数 parse 根据词法解析的结果生成三种节点描述对象:标签节点描述对象、字面量表达式文本节点描述对象、纯文本节点描述对象。AST依靠标签节点的指向父节点的 parent 属性与包含子节点的 children 属性构建树状结构。
AST的优化分为两步:标记静态节点、标记静态根节点。优化的主要途径是标记出不须要重复编译且DOM不会发生改变的静态根节点,在作相关处理时忽略掉该类节点。
渲染函数字符串的生成主要是根据AST将各类节点拼接成包裹在不一样函数中的字符串,最后经过new Function 将函数字符串转化成真正的渲染函数。
欢迎关注公众号:前端桃花源,互相交流学习!