Vue編程三部曲之將template編譯成AST示例詳解
前言
Vue.js 提供了 2 個(gè)版本,一個(gè)是 Runtime + Compiler 版本,一個(gè)是 Runtime only 版本。Runtime + Compiler 版本是包含編譯代碼的,可以把編譯過(guò)程放在運(yùn)行時(shí)做,Runtime only 版本不包含編譯代碼的,需要借助 webpack 的 vue-loader 事先把模板編譯成 render 函數(shù)。
如果你需要在客戶端編譯模板 (比如傳入一個(gè)字符串給 template 選項(xiàng),或掛載到一個(gè)元素上并以其 DOM 內(nèi)部的 HTML 作為模板),就將需要加上編譯器,即完整版:
// 需要編譯器
new Vue({
template: '<div>{{ hi }}</div>'
})
// 不需要編譯器
new Vue({
render (h) {
return h('div', this.hi)
}
})
當(dāng)使用 vue-loader 或 vueify 的時(shí)候,*.vue 文件內(nèi)部的模板會(huì)在構(gòu)建時(shí)預(yù)編譯成 JavaScript。你在最終打好的包里實(shí)際上是不需要編譯器的,所以只用運(yùn)行時(shí)版本即可。因?yàn)檫\(yùn)行時(shí)版本相比完整版體積要小大約 30%,所以應(yīng)該盡可能使用這個(gè)版本。
在 Vue 的整個(gè)編譯過(guò)程中,會(huì)做三件事:
- 解析模板
parse,生成 AST - 優(yōu)化 AST
optimize - 生成代碼
generate
對(duì)編譯過(guò)程的了解會(huì)讓我們對(duì) Vue 的指令、內(nèi)置組件等有更好的理解。不過(guò)由于編譯的過(guò)程是一個(gè)相對(duì)復(fù)雜的過(guò)程,我們只要求理解整體的流程、輸入和輸出即可,對(duì)于細(xì)節(jié)我們不必?fù)柑?xì)。由于篇幅較長(zhǎng),這里會(huì)用三篇文章來(lái)講這三件事。這是第一篇, 模板解析,template -> AST
注:全文源碼來(lái)源,Vue(2.6.11),Runtime + Compiler 的 Vue.js
編譯準(zhǔn)備
這里先做一個(gè)準(zhǔn)備工作,編譯之前有一個(gè)嵌套的函數(shù)調(diào)用,看似非常的復(fù)雜,但是卻有玄機(jī)。有什么玄機(jī)?接著往下看。
源碼編譯鏈?zhǔn)秸{(diào)用

compileToFunctions
在源碼走了一遭,發(fā)現(xiàn)經(jīng)過(guò)一系列的調(diào)用,最后 createCompiler 函數(shù)返回的 compileToFunctions函數(shù) 對(duì)應(yīng)的就是 $mount 函數(shù)調(diào)用的 compileToFunctions 方法,它是調(diào)用 createCompileToFunctionFn 方法的返回值。
// 偽代碼
function createCompilerCreator (baseCompile) {
return function createCompiler (baseOptions) {
function compile (
template,
options
) {
...
return compiled
}
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
function createCompileToFunctionFn (compile) {
var cache = Object.create(null);
return function compileToFunctions (
template,
options,
vm
) {
...
}
}
方法接受三個(gè)參數(shù)。
- 編譯模板 template
- 編譯配置 options
- Vue 的實(shí)例
這個(gè)方法編譯的核心代碼就一行。
// compile var compiled = compile(template, options);
而 compile 方法的核心代碼也就一行。
const compiled = baseCompile(template, finalOptions)
并且 baseCompile方法是在執(zhí)行 createCompilerCreator 方法執(zhí)行的時(shí)候傳入的。
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
baseCompile會(huì)做三件事情。

其實(shí)看到這里你就會(huì)發(fā)現(xiàn),這編譯的準(zhǔn)備工作,做了很多函數(shù)的調(diào)用,但是兜兜轉(zhuǎn)轉(zhuǎn)之后,最后回頭來(lái)還是調(diào)用了最開始createCompilerCreator傳入的函數(shù)。
我理解這樣做的原因是 Vue 本身是支持多平臺(tái)的編譯,在不同平臺(tái)下的編譯會(huì)有所有不同,但是在同一平臺(tái)編譯是相同的,所以在使用createCompiler(baseOptions)時(shí),baseOptions 會(huì)有所有不同。
在 Vue 中利用函數(shù)柯里化的思想,將 baseOptions 的配置參數(shù)進(jìn)行了保存。并且在調(diào)用鏈中,不斷的進(jìn)行函數(shù)調(diào)用并返回函數(shù)。
這其實(shí)也是利用了函數(shù)柯里化的思想把很多基礎(chǔ)的函數(shù)抽離出來(lái), 通過(guò) createCompilerCreator(baseCompile) 的方式把真正編譯的過(guò)程和其它邏輯如對(duì)編譯配置處理、緩存處理等剝離開,這樣的設(shè)計(jì)還是非常巧妙的。
編譯準(zhǔn)備已經(jīng)做完,我們接下來(lái)看看 Vue 是如何做 parse 的。
parse
parse 要做的事情就是對(duì) template 做解析,生成 AST 抽象語(yǔ)法樹。
抽象語(yǔ)法樹(Abstract Syntax Tree,AST),或簡(jiǎn)稱語(yǔ)法樹(Syntax tree),是源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語(yǔ)言的語(yǔ)法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
例如現(xiàn)在有這樣一段代碼:
<body>
<div id="app"></div>
<script>
new Vue({
el: '#app',
template: `
<ul>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
</ul>
`
});
</script>
</body>
經(jīng)過(guò)parse,就變成了一個(gè)嵌套的樹狀結(jié)構(gòu)的對(duì)象。

在 AST 中,每一個(gè)樹節(jié)點(diǎn)都是一個(gè) element,并且維護(hù)了上下文關(guān)系(父子關(guān)系)。
解析 template
parse的過(guò)程核心就是 parseHTML 函數(shù),這個(gè)函數(shù)的作用就是解析 template 模板。下面將解析過(guò)程中一些重要的點(diǎn)進(jìn)行一個(gè)抽象解讀。
function parseHTML (html, options) {
var stack = [];
...
// 遍歷模板字符串
while (html) {
...
}
// 清除所有剩余的標(biāo)簽
parseEndTag();
// 將 html 字符串的指針前移
function advance (n) {
...
}
// 解析開始標(biāo)簽
function parseStartTag () {
...
}
// 處理解析的開始標(biāo)簽的結(jié)果
function handleStartTag (match) {
...
}
// 解析結(jié)束標(biāo)簽
function parseEndTag (tagName, start, end) {
...
}
}
標(biāo)簽匹配相關(guān)的正則
下面也會(huì)講到關(guān)于一些指令匹配相關(guān)的正則。其實(shí)這些正則大家在平時(shí)的項(xiàng)目中有涉及也可以用起來(lái),畢竟這些正則是經(jīng)過(guò)千萬(wàn)人測(cè)試的。
// 識(shí)別合法的xml標(biāo)簽
var ncname = '[a-zA-Z_][\w\-\.]*';
// 復(fù)用拼接,這在我們項(xiàng)目中完成可以學(xué)起來(lái)
var qnameCapture = "((?:" + ncname + "\:)?" + ncname + ")";
// 匹配注釋
var comment =/^<!--/;
// 匹配<!DOCTYPE> 聲明標(biāo)簽
var doctype = /^<!DOCTYPE [^>]+>/i;
// 匹配條件注釋
var conditionalComment =/^<![/;
// 匹配開始標(biāo)簽
var startTagOpen = new RegExp(("^<" + qnameCapture));
// 匹配解說(shuō)標(biāo)簽
var endTag = new RegExp(("^<\/" + qnameCapture + "[^>]*>"));
// 匹配單標(biāo)簽
var startTagClose = /^\s*(/?)>/;
// 匹配屬性,例如 id、class
var attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配動(dòng)態(tài)屬性,例如 v-if、v-else
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
stack
變量 stack ,它定義一個(gè)棧,作用是存儲(chǔ)開始標(biāo)簽。例如我有一個(gè)這樣的簡(jiǎn)單模板:
<div>
<ul>
<li>1</li>
</ul>
</div>
當(dāng)在 while 循環(huán)時(shí),如果遇到一個(gè)非單標(biāo)簽,就會(huì)將開始標(biāo)簽 push 到數(shù)組中,遇到閉合標(biāo)簽就開始元素出棧,這樣可以檢測(cè)我們寫的 template 是否符合嵌套、開閉規(guī)范,這也是檢測(cè) html 字符串中是否缺少閉合標(biāo)簽的原理。

advance
advance 函數(shù)貫穿這個(gè) template 的解析流程。當(dāng)我們?cè)诮馕?template 字符串的時(shí)候,需要對(duì)字符串逐一掃描,直到結(jié)束。advance 函數(shù)的作用就是移動(dòng)指針。例如匹配 <字符,指針移動(dòng) 1,匹配到<!--字符指針移動(dòng) 4。在整個(gè)解析過(guò)程中,貫穿著指針的移動(dòng),因?yàn)橐虢馕鐾瓿删捅仨毎涯0迦烤幾g完。
function advance (n) {
index += n;
html = html.substring(n);
}
while
template 的 while 循環(huán)是解析中最重要的一環(huán),也是這一小節(jié)的重點(diǎn)。
循環(huán)的終止條件是 html 字符串為空,即 html 字符串全部編譯完畢。
循環(huán)時(shí),第一個(gè)判斷是判斷內(nèi)容是否在存純文本標(biāo)簽中。判斷的作用是: 確保我們沒有像腳本/樣式這樣的純文本內(nèi)容元素。

當(dāng)內(nèi)容不在純文本標(biāo)簽,判斷 template 字符串的第一個(gè)<字符位置,來(lái)進(jìn)行不同的操作。
var textEnd = html.indexOf('<');
當(dāng)前 template 第一個(gè)字符是 <
在這種場(chǎng)景下, template 會(huì)出現(xiàn)以下幾種情況,重點(diǎn)是解析開始標(biāo)簽和結(jié)束標(biāo)簽。
<!--開頭的注釋:會(huì)找到注釋的結(jié)尾,將注釋截取出來(lái),移動(dòng)指針,并將注釋當(dāng)做當(dāng)前父節(jié)點(diǎn)的一個(gè)子元素存儲(chǔ)到 children 中。<
⑤,上一步獲取了標(biāo)簽的屬性和動(dòng)態(tài)屬性,但是即使這樣并不能說(shuō)明這是一個(gè)完整的標(biāo)簽,只有當(dāng)匹配到開始標(biāo)記的結(jié)束標(biāo)記時(shí),才能證明這是一個(gè)完整的標(biāo)簽,所以才會(huì)有這一步的判斷。varstartTagClose= /^\s*(/?)>/;并且標(biāo)記 unarySlash屬性。

⑥,假設(shè)正常匹配了,有匹配結(jié)果,也返回了 match (結(jié)構(gòu)如上),就會(huì)走到handleStartTag 這個(gè)函數(shù)的作用就是用來(lái)處理開始標(biāo)簽的解析結(jié)果,所以它接收 parseStartTag 函數(shù)的返回值作為參數(shù)。
handleStartTag 的核心邏輯很簡(jiǎn)單,先判斷開始標(biāo)簽是否是一元標(biāo)簽,類似 <img />、<br/> 這樣,接著對(duì) match.attrs 遍歷并做了一些處理,最后判斷如果非一元標(biāo)簽,則往 stack 里 push 一個(gè)對(duì)象,并且把 tagName 賦值給 lastTag。
function parseStartTag () {
// ①
var start = html.match(startTagOpen);
if (start) {
// ②
var match = {
tagName: start[1],
attrs: [],
start: index
};
// ③
advance(start[0].length);
var end, attr;
// ④
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
...
}
// ⑤
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match
}
}
}
// Start tag:
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
...
}
// ⑥
function handleStartTag (match) {
...
}
解析結(jié)束標(biāo)簽
有解析開始標(biāo)簽就會(huì)解析結(jié)束標(biāo)簽。所以接下來(lái)我們來(lái)看看如何解析結(jié)束標(biāo)簽。
①,正則匹配結(jié)束標(biāo)簽(具體的正則看前面)。
②,匹配到結(jié)束標(biāo)簽,進(jìn)行解析處理,獲取到結(jié)束標(biāo)簽的標(biāo)簽名稱、開始位置和結(jié)束位置,開始進(jìn)行解析操作。
③,查找同一類型的最近打開的標(biāo)記,并記錄位置。
④,如果存在同一類型的標(biāo)記,就將 stack 中匹配的標(biāo)記彈出。
⑤,如果沒有同一類型的標(biāo)記,分別處理 </br>、</p> 標(biāo)簽。這是為了和瀏覽器保持同樣的行為。舉個(gè)例子:在代碼中,分別寫了</br>、</p>的結(jié)束標(biāo)簽,但注意我們并沒有寫起始標(biāo)簽,但是瀏覽器是能夠正常解析他們的,其中 </br> 標(biāo)簽被正常解析為 <br> 標(biāo)簽,而</p>標(biāo)簽被正常解析為 <p></p> 。除了 br 與 p 其他任何標(biāo)簽如果你只寫了結(jié)束標(biāo)簽?zāi)敲礊g覽器都將會(huì)忽略。所以為了與瀏覽器的行為相同,parseEndTag 函數(shù)也需要專門處理 br 與 p 的結(jié)束標(biāo)簽,即:</br> 和</p>。
<div> </br> </p> </div>

// ①
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
// ②
// 獲取到結(jié)束標(biāo)簽的標(biāo)簽名稱、開始位置和結(jié)束位置
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
function parseEndTag (tagName, start, end) {
...
// ③
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
pos = 0;
}
// ④
if (pos >= 0) {
...
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;
// ⑤
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end);
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
到這里結(jié)束標(biāo)簽頁(yè)解析完成,但是在 Vue 中對(duì)開始標(biāo)簽和結(jié)束標(biāo)簽的解析遠(yuǎn)不止這樣,因?yàn)闉榱藶g覽器行為保持一下在解析的過(guò)程中還會(huì)對(duì)一些特殊標(biāo)簽特殊處理,典型的就是 p、br標(biāo)簽,我會(huì)在后面出一篇文章來(lái)詳細(xì)講講 Vue 是如何處理它們的。
當(dāng)前 template 不存在 <
當(dāng)解析到的 template 中不存在 < 時(shí),這認(rèn)為是一個(gè)文本。操作很簡(jiǎn)單就是移動(dòng)指針。
并且這里在源碼中發(fā)現(xiàn)初始化變量的時(shí)候,都是這樣寫的 var text =(void0), rest =(void0), next =(void0);而不是直接 var xx = undefined。原因是這樣操作更加的安全,我在之前的一篇文章中專門解析過(guò),有興趣的可以再去看看。
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length);
}
當(dāng)前 template < 不在第一個(gè)字符串
這里的判斷處理就是為了處理我們?cè)谝恍┘兾谋局幸矔?huì)寫 <標(biāo)記的場(chǎng)景。例如:
<div>1<2</div>
現(xiàn)在有這樣一段模塊,<div>被解析之后,還剩 1<2,這時(shí)解析到存在 <標(biāo)記但是位置不在第一個(gè)。就循環(huán)找出包含<的這一段文本,并將這一段當(dāng)成一個(gè)純文本處理。
if (textEnd >= 0) {
rest = html.slice(textEnd);
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
next = rest.indexOf('<', 1);
if (next < 0) { break }
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}

處理 stack 棧中剩余未處理的標(biāo)簽
當(dāng) while 循環(huán)解析了一遍 template 之后,會(huì)再調(diào)用一次 parseEndTag,這樣做的目的是為了處理 stack 棧中剩余未處理的標(biāo)簽。當(dāng)調(diào)用時(shí),沒有傳遞任何參數(shù),也意味著 tagName, start, end 都是空的,這時(shí) pos 為 0 ,所以 i >= pos 始終成立,這個(gè)時(shí)候 stack 棧中如果有剩余未處理的標(biāo)簽,則會(huì)逐個(gè)警告缺少閉合標(biāo)簽,并調(diào)用 options.end 將其閉合。
// Clean up any remaining tags
parseEndTag();
function parseEndTag (tagName, start, end) {
if (tagName) {
...
} else {
pos = 0;
}
if (pos >= 0) {
for (var i = stack.length - 1; i >= pos; i--) {
if (i > pos || !tagName && options.warn) {
options.warn(
("tag <" + (stack[i].tag) + "> has no matching end tag."),
{ start: stack[i].start, end: stack[i].end }
);
}
}
...
}
}
到這里解析 template 的重點(diǎn)過(guò)程都基本結(jié)束了,整個(gè)過(guò)程就是遍歷 template 字符串,然后通過(guò)正則一點(diǎn)一點(diǎn)的匹配解析字符串,直到整個(gè)字符串被解析完成。
生成 AST
當(dāng)然解析完 template 目的是生成 AST,經(jīng)過(guò)上面的一些列操作,只是解析完 template 字符串,并沒有生成一顆 AST 抽象語(yǔ)法樹。正常的來(lái)說(shuō)抽象語(yǔ)法樹應(yīng)該是如下這樣的,節(jié)點(diǎn)與節(jié)點(diǎn)之間通過(guò) parent 和 children 建立聯(lián)系,每個(gè)節(jié)點(diǎn)的 type 屬性用來(lái)標(biāo)識(shí)該節(jié)點(diǎn)的類別,比如 type 為 1 代表該節(jié)點(diǎn)為元素節(jié)點(diǎn),type 為 3 代表該節(jié)點(diǎn)為文本節(jié)點(diǎn)。

生成 AST 的主要步驟是在解析的過(guò)程中,會(huì)調(diào)用對(duì)應(yīng)的鉤子函數(shù)。解析到開始標(biāo)簽,就調(diào)用開始的鉤子函數(shù),解析到結(jié)束標(biāo)簽就調(diào)用結(jié)束的鉤子函數(shù),解析到文本就會(huì)調(diào)用文本的鉤子,解析到注釋就調(diào)用注釋的鉤子函數(shù)。這些鉤子函數(shù)就會(huì)將所有的節(jié)點(diǎn)串聯(lián)起來(lái),并生成 AST 樹的結(jié)構(gòu)。
start 鉤子函數(shù)
這個(gè)鉤子函數(shù)會(huì)在解析到開始標(biāo)簽的時(shí)候被調(diào)用。為了更加清楚解析過(guò)程,我們引入如下一個(gè)模板,如下:
<div><span></span><p></p></div>
解析 <div>
①,解析到<div>會(huì)調(diào)用 start 鉤子函數(shù)。
②,創(chuàng)建一個(gè)基礎(chǔ)元素對(duì)象。
{
type: 1,
tag:"div",
parent: null,
children: [],
attrsList: []
}
function createASTElement (
tag,
attrs,
parent
) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent: parent,
children: []
}
}
③,接著判斷 root 是否存在,如果不存在則直接將 element 賦值給 root 。root 是一個(gè)記錄值,也就是最后解析返回的整個(gè) AST。
④,如果當(dāng)前標(biāo)簽不是一元標(biāo)簽時(shí),會(huì)將當(dāng)前的 element賦值給 currentParent目的是為建立父子元素的關(guān)系。
⑤,將元素入棧,入棧的目的是為了做回退操作,這里先不講為什么需要做回退,后面在講。此時(shí) stack = [{ tag : "div"... }]。
// parseHTML函數(shù) 解析到開始標(biāo)簽
function handleStartTag (match) {
if (options.start) {
// ①
options.start(tagName, attrs, unary, match.start, match.end);
}
}
// start 鉤子函數(shù)
start: {
// ②
var element = createASTElement(tag, attrs, currentParent);
// element:
// {
// type: 1,
// tag:"div",
// parent: null,
// children: [],
// attrsList: []
// }
// ③
if (!root) {
root = element;
}
// ④
if (!unary) {
currentParent = element;
// currentParent:
// {
// type: 1,
// tag:"div",
// parent: null,
// children: [],
// attrsList: []
// }
// ⑤
stack.push(element);
}
}
解析 <span>
接著解析到 <span>。此時(shí) root 已經(jīng)存在,currentParent 也存在,所以會(huì)將 span 元素的描述對(duì)象添加到 currentParent 的 children 數(shù)組中作為子節(jié)點(diǎn),并將自己的 parent 元素進(jìn)行標(biāo)記。所以最終生成的描述對(duì)象為:
{
type: 1,
tag:"div",
parent: {/*div 元素的描述*/},
attrsList: []
children: [{
type: 1,
tag:"span",
parent: div,
attrsList: [],
children:[]
}],
}
此時(shí) stack = [{ tag : "div"... }, {tag : "span"...}]。
end 鉤子函數(shù)
當(dāng)解析到結(jié)束標(biāo)簽就會(huì)調(diào)用結(jié)束標(biāo)簽的鉤子函數(shù),還是這段模板代碼,解析完<div><span>后遇到了</span>。
<div><span></span><p></p></div>
解析
①,首先就是保存最后一個(gè)元素,將 stack 的最后一個(gè)元素刪除,也就是變成 stack = [{tag: "div" ...}],這就是做了一個(gè)回退操作 。
②,設(shè)置 currentParent 為 stack 的最后一個(gè)元素。
end: function end (tag, start, end$1) {
// ①
var element = stack[stack.length - 1];
stack.length -= 1;
// ②
currentParet = stack[stack.length - 1];
...
},
為什么回退?
解析 <p>
當(dāng)再次解析到開始標(biāo)簽時(shí),就會(huì)再次調(diào)用 start 鉤子函數(shù),這里重點(diǎn)是在解析 p 的開始標(biāo)簽時(shí):stack = [{tag:"div"...},{tag:"p"...}] ,由于在解析到上一個(gè) </span>標(biāo)簽時(shí)做了一個(gè)回退操作, 這就能保證在解析 p 開始標(biāo)簽的時(shí)候,stack 中存儲(chǔ)的是 p 標(biāo)簽父級(jí)元素的描述對(duì)象。
解析 </p>
解析結(jié)束標(biāo)簽,做回退操作。
遇到開始標(biāo)簽就生成元素,勾勒上下文關(guān)系 parent、children 等,每當(dāng)遇到一個(gè)非一元標(biāo)簽的結(jié)束標(biāo)簽時(shí),都會(huì)回退 currentParent 變量的值為之前的值,這樣就修正了當(dāng)前正在解析的元素的父級(jí)元素。
chars 鉤子函數(shù)
當(dāng)然在我們的代碼中肯定不止是開始和結(jié)束標(biāo)簽,還會(huì)有文本。當(dāng)遇到文本時(shí),就會(huì)調(diào)用 chars 鉤子函數(shù)。
①,首先判斷 currentParent(指向的是當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)) 變量是否存在,不存在就說(shuō)明,說(shuō)明 1:只有文本節(jié)點(diǎn)。2:文本在根元素之外。這兩種情況都會(huì)警告 ?? 提醒,接觸后面的操作。
②,第二個(gè)判斷主要是解決 ie textarea 占位符的問(wèn)題。issue
③,判斷當(dāng)前元素未使用 v-pre 指令,text 不為空,使用 parseText 函數(shù)成功解析當(dāng)前文本節(jié)點(diǎn)的內(nèi)容。這里的重點(diǎn)在于 parseText 函數(shù),parseText 函數(shù)的作用就是用來(lái)解析如果我們的文本包含了字面量表達(dá)式。例如:
<div>1111: {{ text }}</div>
這樣的文本就會(huì)解析成如下的一個(gè)描述對(duì)象, 包含 expression 、tokens (包含原始的文本)。

解析完之后會(huì)生成一個(gè) type = 2 的描述對(duì)象:
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
};
④,如果使用了 v-pre || test 為空 || parseText 解析失敗,那么就會(huì)生成一個(gè) type = 3 的存文本描述對(duì)象。
child = {
type: 1,
text: text
};
⑤,最后將解析到描述對(duì)象,添加到當(dāng)前父元素的 children 列表中,注意:這里之前說(shuō)明過(guò)因?yàn)槲覀兊恼麄€(gè) template 是不能是純文本的,必須由根元素,所以如果是文本節(jié)點(diǎn),一點(diǎn)是會(huì)有父元素的。
chars: function chars (text, start, end) {
// ①
if (!currentParent) {
{
if (text === template) {
...警告
} else if ((text = text.trim())) {
...警告
}
}
return
}
// ②
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
var children = currentParent.children;
...
if (text) {
...
// ③
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
};
// ④
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text: text
};
}
// ⑤
if (child) {
...
children.push(child);
}
}
},
到這里文本節(jié)點(diǎn)的解析完成。接下來(lái)看看注釋解析的鉤子函數(shù)。
commit 鉤子函數(shù)
當(dāng)我們配置了 options.comments = true ,也就意味著我們需要保留我們的注釋,這個(gè)配置需要我們手動(dòng)開啟,開啟后就會(huì)在頁(yè)面渲染后保留注釋。
注意:如果開啟了保留注釋匹配后,瀏覽器會(huì)保留注釋。但是可能對(duì)布局產(chǎn)生影響,尤其是對(duì)行內(nèi)元素的影響。為了消除這些影響帶來(lái)的問(wèn)題,好的做法是將它們?nèi)サ簟?/p>

注釋的解析比較簡(jiǎn)單,就是創(chuàng)建注釋節(jié)點(diǎn),然后添加當(dāng)前父元素的子階段列表中。要注意的是純文本節(jié)點(diǎn)和注釋節(jié)點(diǎn)的描述對(duì)象的 type 都是 3,不同的是注釋節(jié)點(diǎn)的元素描述對(duì)象擁有 isComment 屬性,并且該屬性的值為 true,目的就是用來(lái)與普通文本節(jié)點(diǎn)作區(qū)分的。
shouldKeepComment: options.comments,
if (textEnd === 0) {
...
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
}
...
}
comment: function comment (text, start, end) {
if (currentParent) {
var child = {
type: 3,
text: text,
isComment: true
};
...
currentParent.children.push(child);
}
}
到這里在生成 AST 過(guò)程中的 四個(gè)鉤子函數(shù)已經(jīng)全部講完。但是 Vue 本身在對(duì)元素做處理的時(shí)候的時(shí)候肯定不會(huì)是這么簡(jiǎn)單的,因?yàn)檫@處理的過(guò)程中還要處理一元標(biāo)簽、靜態(tài)屬性、動(dòng)態(tài)屬性等。
番外(可跳過(guò))
這一小節(jié)注意是看看在生成 AST 過(guò)程中的一些重要的工具函數(shù)。
createASTElement 函數(shù)
創(chuàng)建元素的描述對(duì)象。
function createASTElement (
tag,
attrs,
parent
) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent: parent,
children: []
}
}
指令解析相關(guān)的正則
前面也講到關(guān)于一些標(biāo)簽匹配相關(guān)的正則。其實(shí)這些正則大家在平時(shí)的項(xiàng)目中有涉及也可以用起來(lái),畢竟這些正則是經(jīng)過(guò)千萬(wàn)人測(cè)試的。
var onRE = /^@|^v-on:/; var dirRE = /^v-|^@|^:|^#/; var forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/; var forIteratorRE = /,([^,}]]*)(?:,([^,}]]*))?$/; var stripParensRE = /^(|)$/g; var dynamicArgRE = /^[.*]$/; var argRE = /:(.*)$/; var bindRE = /^:|^.|^v-bind:/; var modifierRE = /.[^.]]+(?=[^]]*$)/g;
onRE
匹配已字符 @ 或者 v-on開頭的字符串,檢測(cè)標(biāo)簽屬性是否是監(jiān)聽事件的指令。
var onRE = /^@|^v-on:/;
dirRE
匹配 v-、@、:、#開頭的字符串,檢測(cè)屬性名是否是指令。v-開頭的屬性統(tǒng)統(tǒng)都認(rèn)為是指令。@字符是 v-on 的縮寫。: 是 v-bind 的縮寫。 #是 v-slot 的縮寫。
var dirRE = /^v-|^@|^:|^#/;
forAliasRE
匹配 v-for屬性的值,目的是捕獲 in 或者 of 前后的字符串。
var forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
forIteratorRE
這個(gè)也是用來(lái)匹配 v-for屬性的,不同的是,這里是匹配遍歷時(shí)的 value 、 key 、index 。
var forIteratorRE = /,([^,}]]*)(?:,([^,}]]*))?$/;
stripParensRE
匹配以字符 (開頭、)結(jié)尾的字符串。作用是配合上面的正則對(duì)字符進(jìn)行處理(、)。
var stripParensRE = /^(|)$/g;
argRE
匹配指令中的參數(shù)。作用是捕獲指令中的參數(shù)。常見的就是指令中的修飾符。
var argRE = /:(.*)$/;
bindRE
匹配:、.、v-bind:開頭的字符串。作用是檢查屬性是否是綁定。
var bindRE = /^:|^.|^v-bind:/;
modifierRE
匹配修飾符。主要作用是判斷是否有修飾符。
var modifierRE = /.[^.]]+(?=[^]]*$)/g;
parseText 函數(shù)
這個(gè)函數(shù)的作用是解析 text,在上面講 chars 鉤子函數(shù)的時(shí)候也說(shuō)到這個(gè)函數(shù)。函數(shù)有兩個(gè)參數(shù)text、delimiters。delimiters參數(shù)作用就是:改變純文本插入分隔符。例如:
delimiters: ['${', '}'],
// 模板
<div>{{ text }}</div>
模板會(huì)被編譯成這樣。

在 parseText 函數(shù)中,重點(diǎn)邏輯是開啟一個(gè) while 循環(huán),使用 tagRE 正則匹配文本內(nèi)容,并將匹配結(jié)果保存在 match 變量中,直到匹配失敗循環(huán)才會(huì)終止,這時(shí)意味著所有的字面量表達(dá)式都已經(jīng)處理完畢了。
while ((match = tagRE.exec(text))) {
index = match.index;
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index));
tokens.push(JSON.stringify(tokenValue));
}
var exp = parseFilters(match[1].trim());
tokens.push(("_s(" + exp + ")"));
rawTokens.push({ '@binding': exp });
lastIndex = index + match[0].length;
}
例如有一段這樣的 template:
// text: '小白',
// message: '好久不見'
<div>hello, {{ text }},{{ message }}</div>
會(huì)被解析成如下 AST:

closeElement 函數(shù)
這個(gè)函數(shù)會(huì)在解析非一元開始標(biāo)簽和解析結(jié)束標(biāo)簽的時(shí)候調(diào)用,主要作用有兩個(gè):
- 對(duì)數(shù)據(jù)狀態(tài)進(jìn)行還原,
- 調(diào)用后置處理轉(zhuǎn)換鉤子函數(shù)。
整體流程

總結(jié)
Vue 編譯三部曲第一步 parse的整個(gè)流程就已經(jīng)分享完了,雖然源碼看似非常的復(fù)制,但是如果只是抽離主流程的話,還是比較簡(jiǎn)單的。parse 的目的是將開發(fā)者寫的 template 模板字符串轉(zhuǎn)換成抽象語(yǔ)法樹 AST ,AST 就這里來(lái)說(shuō)就是一個(gè)樹狀結(jié)構(gòu)的 JavaScript 對(duì)象,描述了這個(gè)模板,這個(gè)對(duì)象包含了每一個(gè)元素的上下文關(guān)系。那么整個(gè) parse 的過(guò)程是利用很多正則表達(dá)式順序解析模板,當(dāng)解析到開始標(biāo)簽、閉合標(biāo)簽、文本的時(shí)候都會(huì)分別執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù),來(lái)達(dá)到構(gòu)造 AST 樹的目的。
template 編譯成 AST 的過(guò)程就為大家解析到這里,下一篇為大家來(lái)分享編譯中關(guān)于 「模型樹的優(yōu)化」。
參考
https://cn.vuejs.org/v2/guide/installation.html
parseHTML 函數(shù)源碼解析chars、end、comment鉤子函數(shù)
https://github.com/vuejs/vue/issues/4098
更多關(guān)于Vue template編譯成AST的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot+vue+對(duì)接支付寶接口+二維碼掃描支付功能(沙箱環(huán)境)
這篇文章主要介紹了springboot+vue+對(duì)接支付寶接口+二維碼掃描支付(沙箱環(huán)境),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
vue+elementUI中el-radio設(shè)置默認(rèn)值方式
這篇文章主要介紹了vue+elementUI中el-radio設(shè)置默認(rèn)值方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
解決VUE自定義拖拽指令時(shí) onmouseup 與 click事件沖突問(wèn)題
這篇文章主要介紹了解決VUE自定義拖拽指令時(shí) onmouseup 與 click事件沖突問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-07-07
Vue+ElementUI實(shí)現(xiàn)表單動(dòng)態(tài)渲染、可視化配置的方法
這篇文章主要介紹了Vue+ElementUI實(shí)現(xiàn)表單動(dòng)態(tài)渲染、可視化配置的方法,需要的朋友可以參考下2018-03-03
iview-table組件嵌套input?select數(shù)據(jù)無(wú)法雙向綁定解決
這篇文章主要為大家介紹了iview-table組件嵌套input?select數(shù)據(jù)無(wú)法雙向綁定解決示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
iview table render集成switch開關(guān)的實(shí)例
下面小編就為大家分享一篇iview table render集成switch開關(guān)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03

