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

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

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

genElement:用來(lái)生成基本的render結(jié)構(gòu)或者叫createElement結(jié)構(gòu)genData: 處理ast結(jié)構(gòu)上的一些屬性,用來(lái)生成datagenChildren:處理ast的children,并在內(nèi)部調(diào)用genElement,形成子元素的_c()方法
render字符串內(nèi)部有幾種方法
幾種內(nèi)部方法
_c:對(duì)應(yīng)的是 createElement 方法,顧名思義,它的含義是創(chuàng)建一個(gè)元素(Vnode)_v:創(chuàng)建一個(gè)文本結(jié)點(diǎn)。_s:把一個(gè)值轉(zhuǎn)換為字符串。(eg: {{data}})_m:渲染靜態(tài)內(nè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": ""
}
})
]
)
}
}
那么問題來(lái)了,_c(‘tagName’,data,children)如何拼接的,data是如何拼接的,children又是如何拼接的?
// genElement方法用來(lái)拼接每一項(xiàng)_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
}
線來(lái)看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
}
從上面可以看出來(lái),data的拼接過程就是不斷的判讀ast上一些屬性是否存在,然后拼在data上,最后把這個(gè)data返回。
那么children怎么拼出來(lái)呢?
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ù)就會(huì)形成虛擬DOM.
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Element中Upload組件上傳功能實(shí)現(xiàn)(圖片和文件的默認(rèn)上傳及自定義上傳)
這篇文章主要介紹了Element中Upload組件上傳功能實(shí)現(xiàn)包括圖片和文件的默認(rèn)上傳及自定義上傳,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
使用Vue父子組件通信實(shí)現(xiàn)todolist的功能示例代碼
這篇文章主要給大家介紹了關(guān)于如何使用Vue父子組件通信實(shí)現(xiàn)todolist的功能的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Vue具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
vue 獲取視頻時(shí)長(zhǎng)的實(shí)例代碼
這篇文章主要介紹了vue 獲取視頻時(shí)長(zhǎng)的實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08
Vue實(shí)現(xiàn)彈出框點(diǎn)擊空白頁(yè)彈框消失效果
這篇文章主要介紹了VUE實(shí)現(xiàn)彈出框點(diǎn)擊空白頁(yè)彈框消失,實(shí)現(xiàn)方法可以在Vue中實(shí)現(xiàn)彈出框然后通過點(diǎn)擊空白頁(yè)面來(lái)讓彈窗隱藏,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12
vue3中reactive和ref函數(shù)及對(duì)比分析
這篇文章主要介紹了vue3中reactive和ref函數(shù)及對(duì)比,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01

