Vue源碼解析之Template轉(zhuǎn)化為AST的實(shí)現(xiàn)方法
什么是AST
在Vue的mount過(guò)程中,template會(huì)被編譯成AST語(yǔ)法樹(shù),AST是指抽象語(yǔ)法樹(shù)(abstract syntax tree或者縮寫(xiě)為AST),或者語(yǔ)法樹(shù)(syntax tree),是源代碼的抽象語(yǔ)法結(jié)構(gòu)的樹(shù)狀表現(xiàn)形式。
Virtual Dom
Vue的一個(gè)厲害之處就是利用Virtual DOM模擬DOM對(duì)象樹(shù)來(lái)優(yōu)化DOM操作的一種技術(shù)或思路。
Vue源碼中虛擬DOM構(gòu)建經(jīng)歷 template編譯成AST語(yǔ)法樹(shù) -> 再轉(zhuǎn)換為render函數(shù) 最終返回一個(gè)VNode(VNode就是Vue的虛擬DOM節(jié)點(diǎn))
本文通過(guò)對(duì)源碼中AST轉(zhuǎn)化部分進(jìn)行簡(jiǎn)單提取,因?yàn)樵创a中轉(zhuǎn)化過(guò)程還需要進(jìn)行各種兼容判斷,非常復(fù)雜,所以筆者對(duì)主要功能代碼進(jìn)行提取,用了300-400行代碼完成對(duì)template轉(zhuǎn)化為AST這個(gè)功能。下面用具體代碼進(jìn)行分析。
function parse(template) {
var currentParent; //當(dāng)前父節(jié)點(diǎn)
var root; //最終返回出去的AST樹(shù)根節(jié)點(diǎn)
var stack = [];
parseHTML(template, {
start: function start(tag, attrs, unary) {
......
},
end: function end() {
......
},
chars: function chars(text) {
......
}
})
return root
}
第一步就是調(diào)用parse這個(gè)方法,把template傳進(jìn)來(lái),這里假設(shè)template為 <div id="app"><span>{{message}}</span></div>
然后聲明3個(gè)變量
currentParent -> 存放當(dāng)前父元素,root -> 最終返回出去的AST樹(shù)根節(jié)點(diǎn),stack -> 一個(gè)棧用來(lái)輔助樹(shù)的建立
接著調(diào)用parseHTML函數(shù)進(jìn)行轉(zhuǎn)化,傳入template和options(包含3個(gè)方法 start,end,chars 等下用到這3個(gè)函數(shù)再進(jìn)行解釋?zhuān)┙酉聛?lái)先看parseHTML這個(gè)方法
function parseHTML(html, options) {
var stack = []; //這里和上面的parse函數(shù)一樣用到stack這個(gè)數(shù)組 不過(guò)這里的stack只是為了簡(jiǎn)單存放標(biāo)簽名 為了和結(jié)束標(biāo)簽進(jìn)行匹配的作用
var isUnaryTag$$1 = isUnaryTag; //判斷是否為自閉合標(biāo)簽
var index = 0;
var last;
while (html) {
// 第一次進(jìn)入while循環(huán)時(shí),由于字符串以<開(kāi)頭,所以進(jìn)入startTag條件,并進(jìn)行AST轉(zhuǎn)換,最后將對(duì)象彈入stack數(shù)組中
last = html;
var textEnd = html.indexOf('<');
if (textEnd === 0) { // 此時(shí)字符串是不是以<開(kāi)頭
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
// Start tag: // 匹配起始標(biāo)簽
var startTagMatch = parseStartTag(); //處理后得到match
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
}
// 初始化為undefined 這樣安全且字符數(shù)少一點(diǎn)
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) { // 截取<字符索引 => </div> 這里截取到閉合的<
rest = html.slice(textEnd); //截取閉合標(biāo)簽
// 處理文本中的<字符
// 獲取中間的字符串 => {{message}}
text = html.substring(0, textEnd); //截取到閉合標(biāo)簽前面部分
advance(textEnd); //切除閉合標(biāo)簽前面部分
}
// 當(dāng)字符串沒(méi)有<時(shí)
if (textEnd < 0) {
text = html;
html = '';
}
// // 處理文本
if (options.chars && text) {
options.chars(text);
}
}
}
函數(shù)進(jìn)入while循環(huán)對(duì)html進(jìn)行獲取<標(biāo)簽索引 var textEnd = html.indexOf('<');如果textEnd === 0 說(shuō)明當(dāng)前是標(biāo)簽<xxx>或者</xxx> 再用正則匹配是否當(dāng)前是結(jié)束標(biāo)簽</xxx>。var endTagMatch = html.match(endTag); 匹配不到那么就是開(kāi)始標(biāo)簽,調(diào)用parseStartTag()函數(shù)解析。
function parseStartTag() { //返回匹配對(duì)象
var start = html.match(startTagOpen); // 正則匹配
if (start) {
var match = {
tagName: start[1], // 標(biāo)簽名(div)
attrs: [], // 屬性
start: index // 游標(biāo)索引(初始為0)
};
advance(start[0].length);
var end, attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length);
match.attrs.push(attr);
}
if (end) {
advance(end[0].length); // 標(biāo)記結(jié)束位置
match.end = index; //這里的index 是在 parseHTML就定義 在advance里面相加
return match // 返回匹配對(duì)象 起始位置 結(jié)束位置 tagName attrs
}
}
}
該函數(shù)主要是為了構(gòu)建一個(gè)match對(duì)象,對(duì)象里面包含tagName(標(biāo)簽名),attrs(標(biāo)簽的屬性),start(<左開(kāi)始標(biāo)簽在template中的位置),end(>右開(kāi)始標(biāo)簽在template中的位置) 如template = <div id="app"><div><span>{{message}}</span></div></div> 程序第一次進(jìn)入該函數(shù) 匹配的是div標(biāo)簽 所以tagName就是div
start:0 end:14 如圖:

接著把match返回出去 作為調(diào)用handleStartTag的參數(shù)
var startTagMatch = parseStartTag(); //處理后得到match
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
接下來(lái)看handleStartTag這個(gè)函數(shù):
function handleStartTag(match) {
var tagName = match.tagName;
var unary = isUnaryTag$$1(tagName) //判斷是否為閉合標(biāo)簽
var l = match.attrs.length;
var attrs = new Array(l);
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
value: value
};
}
if (!unary) {
stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs});
lastTag = tagName;
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
}
函數(shù)中分為3部分 第一部分是for循環(huán)是對(duì)attrs進(jìn)行轉(zhuǎn)化,我們從上一步的parseStartTag()得到的match對(duì)象中的attrs屬性如圖

當(dāng)時(shí)attrs是上面圖這樣子滴 我們通過(guò)這個(gè)循環(huán)把它轉(zhuǎn)化為只帶name 和 value這2個(gè)屬性的對(duì)象 如圖:

接著判斷如果不是自閉合標(biāo)簽,把標(biāo)簽名和屬性推入棧中(注意 這里的stack這個(gè)變量在parseHTML中定義,作用是為了存放標(biāo)簽名 為了和結(jié)束標(biāo)簽進(jìn)行匹配的作用。)接著調(diào)用最后一步 options.start 這里的options就是我們?cè)趐arse函數(shù)中 調(diào)用parseHTML是傳進(jìn)來(lái)第二個(gè)參數(shù)的那個(gè)對(duì)象(包含start end chars 3個(gè)方法函數(shù)) 這里開(kāi)始看options.start這個(gè)函數(shù)的作用:
start: function start(tag, attrs, unary) {
var element = {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent: currentParent,
children: []
};
processAttrs(element);
if (!root) {
root = element;
}
if(currentParent){
currentParent.children.push(element);
element.parent = currentParent;
}
if (!unary) {
currentParent = element;
stack.push(element);
}
}
這個(gè)函數(shù)中 生成element對(duì)象 再連接元素的parent 和 children節(jié)點(diǎn) 最終push到棧中
此時(shí)棧中第一個(gè)元素生成 如圖:

完成了while循環(huán)的第一次執(zhí)行,進(jìn)入第二次循環(huán)執(zhí)行,這個(gè)時(shí)候html變成<span>{{message}}</span></div> 接著截取到<span> 處理過(guò)程和第一次一致 經(jīng)過(guò)這次循環(huán)stack中元素如圖:


接著繼續(xù)執(zhí)行第三個(gè)循環(huán) 這個(gè)時(shí)候是處理文本節(jié)點(diǎn)了 {{message}}
// 初始化為undefined 這樣安全且字符數(shù)少一點(diǎn)
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) { // 截取<字符索引 => </div> 這里截取到閉合的<
rest = html.slice(textEnd); //截取閉合標(biāo)簽
// 處理文本中的<字符
// 獲取中間的字符串 => {{message}}
text = html.substring(0, textEnd); //截取到閉合標(biāo)簽前面部分
advance(textEnd); //切除閉合標(biāo)簽前面部分
}
// 當(dāng)字符串沒(méi)有<時(shí)
if (textEnd < 0) {
text = html;
html = '';
}
// 另外一個(gè)函數(shù)
if (options.chars && text) {
options.chars(text);
}
這里的作用就是把文本提取出來(lái) 調(diào)用options.chars這個(gè)函數(shù) 接下來(lái)看options.chars
chars: function chars(text) {
if (!currentParent) { //如果沒(méi)有父元素 只是文本
return
}
var children = currentParent.children; //取出children
// text => {{message}}
if (text) {
var expression;
if (text !== ' ' && (expression = parseText(text))) {
// 將解析后的text存進(jìn)children數(shù)組
children.push({
type: 2,
expression: expression,
text: text
});
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text: text
});
}
}
}
})
這里的主要功能是判斷文本是{{xxx}}還是簡(jiǎn)單的文本xxx,如果是簡(jiǎn)單的文本 push進(jìn)父元素的children里面,type設(shè)置為3,如果是字符模板{{xxx}},調(diào)用parseText轉(zhuǎn)化。如這里的{{message}}轉(zhuǎn)化為 _s(message)(加上_s是為了AST的下一步轉(zhuǎn)為render函數(shù),本文中暫時(shí)不會(huì)用到。) 再把轉(zhuǎn)化后的內(nèi)容push進(jìn)children。

又走完一個(gè)循環(huán)了,這個(gè)時(shí)候html = </span></div> 剩下2個(gè)結(jié)束標(biāo)簽進(jìn)行匹配了
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
接下來(lái)看parseEndTag這個(gè)函數(shù) 傳進(jìn)來(lái)了標(biāo)簽名 開(kāi)始索引和結(jié)束索引
function parseEndTag(tagName, start, end) {
var pos, lowerCasedTagName;
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
}
// Find the closest opened tag of the same type
if (tagName) { // 獲取最近的匹配標(biāo)簽
for (pos = stack.length - 1; pos >= 0; pos--) {
// 提示沒(méi)有匹配的標(biāo)簽
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0;
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (options.end) {
options.end(stack[i].tag, start, end);
}
}
// Remove the open elements from the stack
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;
}
這里首先找到棧中對(duì)應(yīng)的開(kāi)始標(biāo)簽的索引pos,再?gòu)脑撍饕_(kāi)始到棧頂?shù)乃栽卣{(diào)用options.end這個(gè)函數(shù)
end: function end() {
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
},
把棧頂元素出棧,因?yàn)檫@個(gè)元素已經(jīng)匹配到結(jié)束標(biāo)簽了,再把當(dāng)前父元素更改。終于走完了,把html的內(nèi)容循環(huán)完,最終return root 這個(gè)root就是我們所要得到的AST

這只是Vue的冰山一角,文中有什么不對(duì)的地方請(qǐng)大家?guī)兔χ刚?,本人最近也一直在學(xué)習(xí)Vue的源碼,希望能夠拿出來(lái)與大家一起分享經(jīng)驗(yàn),接下來(lái)會(huì)繼續(xù)更新后續(xù)的源碼,如果覺(jué)得有幫忙請(qǐng)給個(gè)Star哈
github地址為:https://github.com/zwStar/vue-ast 歡迎各位star或issues
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
從零開(kāi)始用webpack構(gòu)建一個(gè)vue3.0項(xiàng)目工程的實(shí)現(xiàn)
這篇文章主要介紹了從零開(kāi)始用webpack構(gòu)建一個(gè)vue3.0項(xiàng)目工程的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
vue+element實(shí)現(xiàn)錨點(diǎn)鏈接方式
這篇文章主要介紹了vue+element實(shí)現(xiàn)錨點(diǎn)鏈接方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
Vue報(bào)錯(cuò)ERR_OSSL_EVP_UNSUPPORTED解決方法
Vue項(xiàng)目啟動(dòng)時(shí)報(bào)錯(cuò)ERR_OSSL_EVP_UNSUPPORTED,本文主要介紹了Vue報(bào)錯(cuò)ERR_OSSL_EVP_UNSUPPORTED解決方法,具有一定的參考價(jià)值,感興趣的可以了解一下2024-08-08
Vue不能檢測(cè)到數(shù)據(jù)變化的幾種情況說(shuō)明
這篇文章主要介紹了Vue不能檢測(cè)到數(shù)據(jù)變化的幾種情況說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
vue項(xiàng)目使用axios發(fā)送請(qǐng)求讓ajax請(qǐng)求頭部攜帶cookie的方法
今天小編就為大家分享一篇vue項(xiàng)目使用axios發(fā)送請(qǐng)求讓ajax請(qǐng)求頭部攜帶cookie的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
Vue?el-menu?左側(cè)菜單導(dǎo)航功能的實(shí)現(xiàn)
這篇文章主要介紹了Vue?el-menu?左側(cè)菜單導(dǎo)航功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08

