咱們在Vue中會使用一些變量,表達式,指令來填充模板,可是這些語法在HTML中是不存在的,那麼Vue是如何對這樣的模板進行編譯的呢?javascript
模板編譯的主要做用是將Vue模板編譯爲渲染函數,首先將模板解析成AST(抽象語法樹),而後使用AST生成渲染函數。html
首先咱們要知道Vue每次渲染,都會生成一份新的vNode與舊的vNode進行對比,在生成渲染函數以前還會遍歷一遍AST,爲全部的靜態節點作一個編輯,在從新渲染時,不會生成新得節點,而是直接克隆已存在的以前的靜態節點。java
因此整體過程是:將模板解析成AST=>遍歷AST標記靜態節點=>使用AST生成渲染函數
。 正則表達式
在這一步驟中,須要通過解析器將模板解析AST,而後還須要通過優化器,遍歷AST找出靜態節點並標記。算法
在解析器內部還分紅了文本解析器,HTML解析器和過濾器解析器。express
其中核心部分是HTML解析器,做用是用來解析字符串模板。變量解析器用於解析帶有模板的文本變量,而不帶用變量的文本節點就是剛纔所說的靜態節點,不須要解析。過濾器解析器用來解析過濾器。解析結果AST是一種以節點爲結構的樹形結構的對象,一個對象表示一個節點,對象的屬性用來保存節點所須要的數據。數組
解析模板例如:函數
<div>
<p>{{name}}</p>
</div>
複製代碼
解析成AST以後:post
//裏面的內容後續會解釋
{
tag: "div"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag: "div", ...},
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
複製代碼
解析器在解析HTML的過程當中會不斷觸發各類鉤子函數。這些鉤子函數包括開始標籤鉤子函數、結束標籤鉤子函數、文本鉤子函數以及註釋鉤子函數。性能
例如:
parseHTML(template, {
start (tag, attrs, unary) {
// 每當解析到標籤的開始位置時,觸發該函數
},
end () {
// 每當解析到標籤的結束位置時,觸發該函數
},
chars (text) {
// 每當解析到文本時,觸發該函數
},
comment (text) {
// 每當解析到註釋時,觸發該函數
}
})
複製代碼
咱們簡單舉一個例子來講明上述方法是如何構建AST節點的:
<div><p>我是一個節點</p></div>
複製代碼
首先,解析器會將html模板做爲一段字符串模板從前向後進行解析,解析到<div>
時,會觸發一個標籤開始的鉤子函數start();而後解析到<p>
時,又觸發一次鉤子函數start();接着解析到我是一個節點
這行文本,此時觸發了文本鉤子函數chars();而後解析到</p>
,觸發了標籤結束的鉤子函數end();接着繼續解析到</div>
,此時又觸發一次標籤結束的鉤子函數end(),解析結束。
start()函數你能夠看做爲HTML解析函數,他的三個參數分別是分別是tag、attrs和unary,分別表明標籤名、標籤的屬性以及是不是自閉合標籤。
而文本節點的解析函數chars和註釋節點的解析函數comment都只有一個參數text。這是由於構建元素節點須要知道標籤名、屬性和是不是自閉合元素,而構建註釋節點和文本節點時只須要知道文本內容便可。 咱們將上面的parseHTML()擴充一下:
//咱們模擬一個建立AST元素類型節點的函數
function createASTElement (tag, attrs, parent) {
// 返回的是一個節點對象
return {
type: 1, // 指定節點類型 1.元素節點
tag, // 指定節點
attrsList: attrs, // 指定節點屬性
parent, // 指定是不是自閉合標籤
children: []
}
}
parseHTML(template, {
start (tag, attrs, unary) {
// 每當解析到標籤的開始位置時,觸發該函數
// 將標籤名、標籤的屬性以及是不是自閉合標籤傳入
let element = createASTElement(tag, attrs, currentParent)
},
end () {
// 每當解析到標籤的結束位置時,觸發該函數
},
chars (text) {
// 每當解析到文本時,觸發該函數
// 返回的是一個文本節點對象
// 文本分兩種類型 2.帶變量的動態文本節點 3.不帶變量的純文本節點
let element = {type: 3, text}
},
comment (text) {
// 每當解析到註釋時,觸發該函數
// 返回的是一個註釋節點對象,註釋文本和文本的區別是打上了isComment標記
let element = {type: 3, text, isComment: true}
}
})
複製代碼
可是使用上述方式建立的節點雖然帶有節點對象信息,可是是扁平的,沒有層級關係,而Vue使用了出入棧的方式來構建一個AST結構對象,爲以前的扁平數據實現層級關係。
每次解析HTML,都會使用一個棧來存儲維護,當觸發start()函數時,將當前構建的節點推入棧中;每當觸發鉤子函數end()時,就從棧中彈出上一個節點。舉個例子:
<div>
<h1>我是h1</h1>
<p>我是文本</p>
</div>
複製代碼
並將模板字符串中的div開始標籤從模板中截取掉
。
將h1添加到div的子節點中(也就是children中)
,而且將h1節點推入棧中,同時從模板中將h1的開始標籤截取掉。
其中對開始標籤,結束標籤,還有標籤屬性的解析基本是使用了大量正則表達式:去解析<div
</div>
:
class=
這樣的字符串,去斷定這是一個什麼標籤該去觸發什麼函數,不作過多描述。
這個過程如何解析HTML中的註釋,條件註釋,DOCTYPE,文本?
HTML中的註釋,判斷<!--
,經過indexOf找到註釋結束位置-->
的下標,而後將結束位置前的字符都截取掉。條件註釋註釋用提早的表達式判斷<
,條件註釋會被直接截取掉。DOCTYPE直接匹配這段字符,根據它的length屬性來決定要截取多長的字符串。文本咱們只須要找到>
與下一個<
在什麼位置,這以前的全部字符都屬於文本。
節點不完整?
<div><p></div>
複製代碼
在上面的代碼中,p標籤沒有結束標籤,那麼當HTML解析器解析到div的結束標籤時,發現棧內元素倒是p標籤。就會從棧頂向棧底遍歷尋找到div標籤,在找到div標籤以前遇到的全部其餘標籤都會標記爲忘記閉合的標籤,在非生產環境下在控制檯打印警告提示。
爲何文本解析器要單獨說,由於文本其實分兩種類型,一種是純文本,另外一種是帶變量的文本。
Hello name
Hello {{name}}
複製代碼
若是是純文本,不須要進行任何處理;但若是是帶變量的文本,那麼須要使用文本解析器進一步解析。由於帶變量的文本在使用虛擬DOM進行渲染時,須要將變量替換成變量中的值。
靜態節點:
<p>我就是一個純文本的靜態節點</p>
複製代碼
優化器則是將解析完的AST進行遍歷,找出靜態節點並標記,在下次更新對比虛擬DOM的vNode時,若是發現這兩個節點是靜態節點,則直接跳過更新節點的流程。達到進一步避免一些無用的DOM操做來提高性能,由於靜態節點在首次渲染後必定不會改變。
代碼生成器是將解析完的AST轉化爲渲染函數須要的內容,這個內容叫代碼字符串,例如:
<div>
<p>{{name}}</p>
</div>
複製代碼
// 解析爲AST
{
tag: "div"
type: 1,
staticRoot: false, // 是否爲根靜態節點(根靜態節點下的所欲節點會認爲是靜態節點)
static: false, // 是否爲根靜態節點
plain: true,
parent: undefined,
attrsList: [], // 元素屬性
attrsMap: {},
children: [
{
tag: "p"
type: 1, //
staticRoot: false,
static: false,
plain: true,
parent: {tag: "div", ...}, // 全部子節點會帶有父節點信息
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
複製代碼
// 解析完的AST生成代碼字符串
`with(this) {return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])}`
複製代碼
以後將這串代碼字符串傳到Vue的渲染函數中,渲染函數根據參數結構,調用相關的建立vNode的方法(生成後的代碼字符串中看到了有幾個函數調用 _c,_v,_s,這是Vue內部的一些渲染函數,_c能夠建立元素類型的vNode,_v能夠建立文本類型的vNode,_e能夠建立註釋類型的vNode)最後組成一份虛擬DOM結構。
咱們拿_c來解釋一下這個字符串的結構:
<p title="biaoti">name</p>
複製代碼
with(this){
return _c(
'p', // 標籤名
{
attrs:{"title":"biaoti"},
}, // 屬性
[_v("name")] // 子節點
)
}
複製代碼
代碼生成器的整體邏輯其實就是遞歸ATS,而後根據ATS結構拼出這樣的_c('div',[_c('p',[_v(_s(name))])])
字符串,再將其傳入渲染函數執行。
至於具體的AST轉換過程就不作深刻解釋,會令文章顯得枯燥。
咱們以上簡單講述了Vue對模板編譯的總體流程:解析器(模板字符串轉換成AST),優化器(標記靜態節點)和代碼生成器(將AST裝換成帶結構的代碼字符串)。
解析器經過使用一個棧來維護節點,每從模板字符串中截取一個節點字符串,就將其推入棧中,同時構建一個AST節點,一直到結束節點在將其推出棧,如此循環最後構建出一套帶有結構的AST對象。
優化器是經過遍歷AST節點,對其中的靜態節點作標記,同時最後標記處根靜態節點,節省部分沒必要要的性能消耗。
代碼生成器也是經過遍歷去拼出一個渲染函數執行的代碼字符串,遍歷的過程根據不一樣的節點類型type調用不一樣的生成字符串方法,最後拼出一個完整的 render 函數須要的代碼字符串。
後續還有兩篇:
《Vue不看源碼懂原理》系列——Vue的diff算法不難懂(直接傳送)
《Vue不看源碼懂原理》系列——Vue的實例函數和指令解密(下週)
之有一篇用心總結的《Javascript垃圾回收原理》沒太有響應,我以爲你們能夠看一看,耐心一下的話比較好理解。
點個贊,我加油
點關注,不迷路,哈哈哈