vue中template模板編譯的過程全面剖析
簡述過程
vue template模板編譯的過程經(jīng)過parse()生成ast(抽象語法樹),optimize對靜態(tài)節(jié)點優(yōu)化,generate()生成render字符串
之后調用new Watcher()函數(shù),用來監(jiān)聽數(shù)據(jù)的變化,render 函數(shù)就是數(shù)據(jù)監(jiān)聽的回調所調用的,其結果便是重新生成 vnode。
當這個 render 函數(shù)字符串在第一次 mount、或者綁定的數(shù)據(jù)更新的時候,都會被調用,生成 Vnode。
如果是數(shù)據(jù)的更新,那么 Vnode 會與數(shù)據(jù)改變之前的 Vnode 做 diff,對內容做改動之后,就會更新到 我們真正的 DOM
vue的渲染過程

parse
在了解 parse 的過程之前,我們需要了解 AST,AST 的全稱是 Abstract Syntax Tree,也就是所謂抽象語法樹,用來表示代碼的數(shù)據(jù)結構。
在Vue中我把它理解為嵌套的、攜帶標簽名、屬性和父子關系的 JS 對象,以樹來表現(xiàn) DOM 結構。
vue中的ast類型有以下3種
ASTElement = { // AST標簽元素
type: 1;
tag: string;
attrsList: Array<{ name: string; value: any }>;
attrsMap: { [key: string]: any };
parent: ASTElement | void;
children: Array<ASTNode>
...
}
ASTExpression = { // AST表達式 {{ }}
type: 2;
expression: string;
text: string;
tokens: Array<string | Object>;
static?: boolean;
};
ASTText = { // AST文本
type: 3;
text: string;
static?: boolean;
isComment?: boolean;
};
通過children字段來形成一種層層嵌套的樹狀結構。vue中定義了許多正則(判斷標簽開始、結束、屬性、vue指令、文本),通過對html內容進行遞歸正則匹配,對滿足條件的字符串進行截取。把字符串類型的html轉換位AST結構
parse函數(shù)的作用就是把字符串型的template轉化為AST結構

如,假設我們有一個元素
texttext,在 parse 完之后會變成如下的結構并返回:
ele1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "test"}],
attrsMap: {id: "test"},
parent: undefined,
children: [{
type: 3,
text: 'texttext'
}
],
plain: true,
attrs: [{name: "id", value: "'test'"}]
}
那么它具體是怎么解析、截取的呢?
舉個例子
<div>
<p>我是{{name}}</p>
</div>
他的截取過程,主要如下
// 初始
<div>
<p>我是{{name}}</p>
</div>
// 第一次截取剩余(包括空格)
<p>我是{{name}}</p>
</div>
// 第二次截取
<p>我是{{name}}</p>
</div>
// 第三次截取
我是{{name}}</p>
</div>
// 第四次截取
</p>
</div>
//
</div>
//
</div>
那么,他的截取規(guī)則是什么呢?
vue中截取規(guī)則主要是通過判斷模板中html.indexof(’<’)的值,來確定我們是要截取標簽還是文本.
- 等于 0:這就代表這是注釋、條件注釋、doctype、開始標簽、結束標簽中的某一種
- 大于等于 0:這就說明是文本、表達式
- 小于 0:表示 html 標簽解析完了,可能會剩下一些文本、表達式
若等于0
若等于0,則進行正則匹配看是否為開始標簽、結束標簽、注釋、條件注釋、doctype中的一種。若是開始標簽,則截取對應的開始標簽,并定義ast的基本結構,并且解析標簽上帶的屬性(attrs, tagName)、指令等等。
當然,這里的attrs也是通過正則匹配出來的,具體做法就是通過匹配標簽上對應的屬性,然后把他push到attrs里。
匹配時候的正則表達式如下。
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
- 同時,需要注意的一點是,vue中還需要維護一個stack(可以理解為一個數(shù)組),用來標記DOM的深度
關于stack
stack里的最后一項,永遠是當前正在解析的元素的parentNode。
通過stack解析器會把當前解析的元素和stack里的最后一個元素建立父子關系。即把當前節(jié)點push到stack的最后一個節(jié)點的children里,同時將它自身的parent設為stack的最后一個節(jié)點。
當然,因為我們的標簽中存在一種自閉和的標簽(如input),這種類型的標簽沒有子元素,所以不會push到stack中。
- 若是結束標簽,則需要通過這個結束標簽的tagName從后到前匹配stack中每一項的tagName,將匹配到的那一項之后的所有項全部刪除,表示這一段已經(jīng)解析完成。
- 若不是以上5種中的一種,則表示他是文本
等于0或大于0
若等于0且不滿足以上五種條件或大于0,則表示它是文本或表達式。
- 此時,它會判斷它的剩余部分是否符合標簽的格式,
- 如果不符合,則繼續(xù)再剩余部分判斷’<'的位置,并繼續(xù)1的判斷,直到剩余部分有符合標簽的格式出現(xiàn)。
let textEnd = html.indexOf('<')
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
// 剩余部分的 HTML 不符合標簽的格式那肯定就是文本
// 并且還是以 < 開頭的文本
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
html = html.substring(0, textEnd)
}
關于文本的截取
文本一般分為2種
- 實打實</div>
- 我是{{name}}</div>
如果文本中含有表達式,則需要對文本中的變量進行解析
const expression = parseText(text, delimiters) // 對變量解析 {{name}} => _s(name)
children.push({
type: 2,
expression,
text
})
// 上例中解析過后形成如下的結構
{
expression: "_s(name)",
text: "我是{{name}}",
type: 2
}
現(xiàn)在我們再來看最開始的例子
<div>
? ? <p>我是{{name}}</p>
</div>1.首先第一次判斷<的位置,等于0,且可以匹配上開始標簽,則截取這個標簽。
// 第一次截取后剩余
? ? <p>我是{{name}}</p>
</div>2.繼續(xù)判斷<的位置,大于0(因為有空格),判斷為文本,截取這個文本
// 第二次截取后剩余
<p>我是{{name}}</p>
</div>3.繼續(xù)判斷<位置,等于0,且為開始標簽,截取這一部分,并且維護stack,把當前的解析的元素的parnet置為stack中的最后一項,并且在stack的最后一項的children里push當前解析的元素
// 這里有個判斷,因為非自閉和標簽才會有children,所以非自閉標簽才往stack里push
if (!unary) {
? currentParent = element
? stack.push(element)
}
// 設立父子關系
currentParent.children.push(element)
element.parent = currentParent
// 此時stack
[divAst,pAst]
// ?第三次截取后剩余
我是{{name}}</p>
</div>4.繼續(xù)判斷<的位置,大于0,判斷剩余部分是否屬于標簽的一種,這里剩余部分可以匹配結束標簽,則表明為文本
// 第四次截取后剩余 </p> </div>
5.繼續(xù)判斷<的位置,等于0,且匹配為結束標簽,此時會再stack里尋找滿足tagName和當前標簽名相同的最后一項,把它之后項的全部刪除。
// 此時stack [divAst] // 第五次截取剩余 </div>
6.繼續(xù)通過以上方式截取,直到全部截取完畢。
parse過程總結
簡單來說,template的parse過程,其實就是不斷的截取字符串并解析它們的過程。
在此過程中,如果截取到非閉合標簽就push到stack中,如果截取道結束標簽就把這個標簽pop出來。
optimize優(yōu)化
optimize的作用主要是對生成的AST進行靜態(tài)內容的優(yōu)化,標記靜態(tài)節(jié)點。所謂靜態(tài)內容,指的是和數(shù)據(jù)沒有關系,不需要每次都更新的內容。
標記靜態(tài)節(jié)點的作用的作用是為了之后dom diff時,是否需要patch,diff算法會直接跳過靜態(tài)節(jié)點,從而減少了比較的過程,優(yōu)化了patch的性能。
- 1.如果是表達式AST節(jié)點,直接返回 false
- 2.如果是文本AST節(jié)點,直接返回 true
- 3.如果元素是元素節(jié)點,階段有 v-pre 指令 ||
1.沒有任何指令、數(shù)據(jù)綁定、事件綁定等 &&
2.沒有 v-if 和 v-for &&
3.不是 slot 和 component &&
4.是 HTML 保留標簽 &&
5.不是 template 標簽的直接子元素并且沒有包含在 for 循環(huán)中則返回 true
簡單來說,沒有使用vue獨有的語法的節(jié)點就可以稱為靜態(tài)節(jié)點
判斷一個父級元素是靜態(tài)節(jié)點,則需要判斷它的所有子節(jié)點都是靜態(tài)節(jié)點,否則就不是靜態(tài)節(jié)點
標記靜態(tài)節(jié)點的過程是一個不斷遞歸的過程
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
markStatic方法是用來標記靜態(tài)節(jié)點的方法,它會不斷的循環(huán)children,如果children還有children,則走相同的邏輯。這樣所有的節(jié)點都會被打上標記。
在循環(huán)中會判斷,子節(jié)點是否為靜態(tài)節(jié)點,如果不是則其父節(jié)點不是靜態(tài)節(jié)點。
generate生成render函數(shù)
generate是將AST轉化成render funtion字符串的過程,他遞歸了AST,得到結果是render的字符串。
render函數(shù)的就是返回一個_c(‘tagName’,data,children)的方法
1.第一個參數(shù)是標簽名
2.第二個參數(shù)是他的一些數(shù)據(jù),包括屬性/指令/方法/表達式等等。
3.第三個參數(shù)是當前標簽的子標簽,同樣的,每一個子標簽的格式也是_c(‘tagName’,data,children)。
generate就是通過不斷遞歸形成了這么一種樹形結構。

genElement:用來生成基本的render結構或者叫createElement結構genData: 處理ast結構上的一些屬性,用來生成datagenChildren:處理ast的children,并在內部調用genElement,形成子元素的_c()方法
render字符串內部有幾種方法
幾種內部方法
_c:對應的是 createElement 方法,顧名思義,它的含義是創(chuàng)建一個元素(Vnode)_v:創(chuàng)建一個文本結點。_s:把一個值轉換為字符串。(eg: {{data}})_m:渲染靜態(tài)內容
<template>
<div id="app">
{{val}}
<img src="http://xx.jpg">
</div>
</template>
{
render: with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v("\n" + _s(val) + "\n"),
_c('img', {
attrs: {
"src": ""
}
})
]
)
}
}
那么問題來了,_c(‘tagName’,data,children)如何拼接的,data是如何拼接的,children又是如何拼接的?
// genElement方法用來拼接每一項_c('tagName',data,children)
function genElement (el: ASTElement, state: CodegenState) {
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
let code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
return code
}
線來看data的拼接邏輯
//
function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// ... 類似的還有很多種情況
data = data.replace(/,$/, '') + '}'
return data
}
從上面可以看出來,data的拼接過程就是不斷的判讀ast上一些屬性是否存在,然后拼在data上,最后把這個data返回。
那么children怎么拼出來呢?
function genChildren (
el: ASTElement,
state: CodegenState
): string | void {
const children = el.children
if (children.length) {
return `[${children.map(c => genNode(c, state)).join(',')}]`
}
}
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
最后執(zhí)行render函數(shù)就會形成虛擬DOM.
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Element中Upload組件上傳功能實現(xiàn)(圖片和文件的默認上傳及自定義上傳)
這篇文章主要介紹了Element中Upload組件上傳功能實現(xiàn)包括圖片和文件的默認上傳及自定義上傳,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01
使用Vue父子組件通信實現(xiàn)todolist的功能示例代碼
這篇文章主要給大家介紹了關于如何使用Vue父子組件通信實現(xiàn)todolist的功能的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Vue具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-04-04

