vue?parseHTML?函數(shù)源碼解析AST基本形成
AST(抽象語法樹)?
vue parseHTML函數(shù)解析器遇到結(jié)束標(biāo)簽
在上篇文章中我們已經(jīng)把整個詞法分析的解析過程分析完畢了。
例如有html(template)字符串:
<div id="app">
<p>{{ message }}</p>
</div>產(chǎn)出如下:
{
attrs: [" id="app"", "id", "=", "app", undefined, undefined]
end: 14
start: 0
tagName: "div"
unarySlash: ""
}
{
attrs: []
end: 21
start: 18
tagName: "p"
unarySlash: ""
}
看到這不禁就有疑問? 這難道就是AST(抽象語法樹)??
非常明確的告訴你答案:No 這不是我們想要的AST,parse 階段最終生成的這棵樹應(yīng)該是與如上html(template)字符串的結(jié)構(gòu)一一對應(yīng)的:
├── div │ ├── p │ │ ├── 文本
如果每一個節(jié)點我們都用一個 javascript 對象來表示的話,那么 div 標(biāo)簽可以表示為如下對象:
{
type: 1,
tag: "div"
}
子節(jié)點
由于每個節(jié)點都存在一個父節(jié)點和若干子節(jié)點,所以我們?yōu)槿缟蠈ο筇砑觾蓚€屬性:parent 和 children ,分別用來表示當(dāng)前節(jié)點的父節(jié)點和它所包含的子節(jié)點:
{
type: 1,
tag:"div",
parent: null,
children: []
}
同時每個元素節(jié)點還可能包含很多屬性 (attributes),所以我們可以為每個節(jié)點添加attrsList屬性,用來存儲當(dāng)前節(jié)點所擁有的屬性:
{
type: 1,
tag:"div",
parent: null,
children: [],
attrsList: []
}
按照以上思路去描述之前定義的 html 字符串,那么這棵抽象語法樹應(yīng)該長成如下這個樣子:
{
type: 1,
tag: "div",
parent: null,
attrsList: [],
children: [{
type: 1,
tag: "p",
parent: div,
attrsList: [],
children:[
{
type: 3,
tag:"",
parent: p,
attrsList: [],
text:"{{ message }}"
}
]
}],
}
實際上構(gòu)建抽象語法樹的工作就是創(chuàng)建一個類似如上所示的一個能夠描述節(jié)點關(guān)系的對象樹,節(jié)點與節(jié)點之間通過 parent 和 children 建立聯(lián)系,每個節(jié)點的 type 屬性用來標(biāo)識該節(jié)點的類別,比如 type 為 1 代表該節(jié)點為元素節(jié)點,type 為 3 代表該節(jié)點為文本節(jié)點。
這里可參考NodeType:https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
回顧我們所學(xué)的 parseHTML 函數(shù)可以看出,他只是在生成 AST 中的一個重要環(huán)節(jié)并不是全部。 那在Vue中是如何把html(template)字符串編譯解析成AST的呢?
Vue中是如何把html(template)字符串編譯解析成AST
在源碼中:
function parse (html) {
var root;
parseHTML(html, {
start: function (tag, attrs, unary) {
// 省略...
},
end: function (){
// 省略...
}
})
return root
}
可以看到Vue在進(jìn)行模板編譯詞法分析階段調(diào)用了parse函數(shù),parse函數(shù)返回root,其中root 所代表的就是整個模板解析過后的 AST,這中間還有兩個非常重要的鉤子函數(shù),之前我們沒有講到的,options.start 、options.end。
接下來重點就來看看他們做了什么。
解析html
假設(shè)解析的html字符串如下:
<div></div>
這是一個沒有任何子節(jié)點的div 標(biāo)簽。如果要解析它,我們來簡單寫下代碼。
function parse (html) {
var root;
parseHTML(html, {
start: function (tag, attrs, unary) {
var element = {
type: 1,
tag: tag,
parent: null,
attrsList: attrs,
children: []
}
if (!root) root = element
},
end: function (){
// 省略...
}
})
return root
}
如上: 在start 鉤子函數(shù)中首先定義了 element 變量,它就是元素節(jié)點的描述對象,接著判斷root 是否存在,如果不存在則直接將 element 賦值給 root 。當(dāng)解析這段 html 字符串時首先會遇到 div 元素的開始標(biāo)簽,此時 start 鉤子函數(shù)將被調(diào)用,最終 root 變量將被設(shè)置為:
{
type: 1,
tag:"div",
parent: null,
children: [],
attrsList: []
}
html 字符串復(fù)雜度升級: 比之前的 div 標(biāo)簽多了一個子節(jié)點,span 標(biāo)簽。
<div> <span></span> </div>
代碼重新改造
此時需要把代碼重新改造。
function parse (html) {
var root;
var currentParent;
parseHTML(html, {
start: function (tag, attrs, unary) {
var element = {
type: 1,
tag: tag,
parent: null,
attrsList: attrs,
children: []
}
if (!root){
root = element;
}else if(currentParent){
currentParent.children.push(element)
}
if (!unary) currentParent = element
},
end: function (){
// 省略...
}
})
return root
}
我們知道當(dāng)解析如上 html 字符串時首先會遇到 div 元素的開始標(biāo)簽,此時 start 鉤子函數(shù)被調(diào)用,root變量被設(shè)置為:
{
type: 1,
tag:"div",
parent: null,
children: [],
attrsList: []
}還沒完可以看到在 start 鉤子函數(shù)的末尾有一個 if 條件語句,當(dāng)一個元素為非一元標(biāo)簽時,會設(shè)置 currentParent 為該元素的描述對象,所以此時currentParent也是:
{
type: 1,
tag:"div",
parent: null,
children: [],
attrsList: []
}接著解析 html (template)字符串
接著解析 html (template)字符串,會遇到 span 元素的開始標(biāo)簽,此時root已經(jīng)存在,currentParent 也存在,所以會將 span 元素的描述對象添加到 currentParent 的 children 數(shù)組中作為子節(jié)點,所以最終生成的 root 描述對象為:
{
type: 1,
tag:"div",
parent: null,
attrsList: []
children: [{
type: 1,
tag:"span",
parent: div,
attrsList: [],
children:[]
}],
}
到目前為止好像沒有問題,但是當(dāng)html(template)字符串復(fù)雜度在升級,問題就體現(xiàn)出來了。
<div> <span></span> <p></p> </div>
在之前的基礎(chǔ)上 div 元素的子節(jié)點多了一個 p 標(biāo)簽,到解析span標(biāo)簽的邏輯都是一樣的,但是解析 p 標(biāo)簽時候就有問題了。
注意這個代碼:
if (!unary) currentParent = element
在解析 p 元素的開始標(biāo)簽時,由于 currentParent 變量引用的是 span 元素的描述對象,所以p 元素的描述對象將被添加到 span 元素描述對象的 children 數(shù)組中,被誤認(rèn)為是 span 元素的子節(jié)點。而事實上 p 標(biāo)簽是 div 元素的子節(jié)點,這就是問題所在。
為了解決這個問題,就需要我們額外設(shè)計一個回退的操作,這個回退的操作就在end鉤子函數(shù)里面實現(xiàn)。
解析div
這是一個什么思路呢?舉個例子在解析div 的開始標(biāo)簽時:
stack = [{tag:"div"...}]
在解析span 的開始標(biāo)簽時:
stack = [{tag:"div"...},{tag:"span"...}]
在解析span 的結(jié)束標(biāo)簽時:
stack = [{tag:"div"...}]
在解析p 的開始標(biāo)簽時:
stack = [{tag:"div"...},{tag:"p"...}]
在解析p 的標(biāo)簽時:
這樣的一個回退操作看懂了嗎? 這就能保證在解析p開始標(biāo)簽的時候,stack中存儲的是p標(biāo)簽父級元素的描述對象。
接下來繼續(xù)改造我們的代碼。
function parse (html) {
var root;
var currentParent;
var stack = [];
parseHTML(html, {
start: function (tag, attrs, unary) {
var element = {
type: 1,
tag: tag,
parent: null,
attrsList: attrs,
children: []
}
if (!root){
root = element;
}else if(currentParent){
currentParent.children.push(element)
}
if (!unary){
currentParent = element;
stack.push(currentParent);
}
},
end: function (){
stack.pop();
currentParent = stack[stack.length - 1]
}
})
return root
}
通過上述代碼,每當(dāng)遇到一個非一元標(biāo)簽的結(jié)束標(biāo)簽時,都會回退 currentParent 變量的值為之前的值,這樣我們就修正了當(dāng)前正在解析的元素的父級元素。
以上就是根據(jù) parseHTML 函數(shù)生成 AST 的基本方式,但實際上還不完美在Vue中還會去處理一元標(biāo)簽,文本節(jié)點和注釋節(jié)點等等。
接下來你是否迫不及待要進(jìn)入到源碼部分去看看了? 但Vue這塊代碼稍微復(fù)雜點,我們還需要有一些前期的預(yù)備知識。
parseHTML 函數(shù)源碼解析 AST 預(yù)備知識
以上就是vue parseHTML 函數(shù)源碼解析AST基本形成的詳細(xì)內(nèi)容,更多關(guān)于vue parseHTML 函數(shù)AST的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
element-ui實現(xiàn)表格邊框的動態(tài)切換并防抖
這篇文章主要介紹了element-ui實現(xiàn)表格邊框的動態(tài)切換并防抖方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
ant design vue 清空upload組件圖片緩存的問題
這篇文章主要介紹了ant design vue 清空upload組件圖片緩存的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10

