vue中template模板編譯的過程全面剖析
簡述過程
vue template模板編譯的過程經(jīng)過parse()生成ast(抽象語法樹),optimize對靜態(tài)節(jié)點優(yōu)化,generate()生成render字符串
之后調(diào)用new Watcher()函數(shù),用來監(jiān)聽數(shù)據(jù)的變化,render 函數(shù)就是數(shù)據(jù)監(jiān)聽的回調(diào)所調(diào)用的,其結(jié)果便是重新生成 vnode。
當(dāng)這個 render 函數(shù)字符串在第一次 mount、或者綁定的數(shù)據(jù)更新的時候,都會被調(diào)用,生成 Vnode。
如果是數(shù)據(jù)的更新,那么 Vnode 會與數(shù)據(jù)改變之前的 Vnode 做 diff,對內(nèi)容做改動之后,就會更新到 我們真正的 DOM
vue的渲染過程
parse
在了解 parse 的過程之前,我們需要了解 AST,AST 的全稱是 Abstract Syntax Tree,也就是所謂抽象語法樹,用來表示代碼的數(shù)據(jù)結(jié)構(gòu)。
在Vue中我把它理解為嵌套的、攜帶標(biāo)簽名、屬性和父子關(guān)系的 JS 對象,以樹來表現(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字段來形成一種層層嵌套的樹狀結(jié)構(gòu)。vue中定義了許多正則(判斷標(biāo)簽開始、結(jié)束、屬性、vue指令、文本),通過對html內(nèi)容進(jìn)行遞歸正則匹配,對滿足條件的字符串進(jìn)行截取。把字符串類型的html轉(zhuǎn)換位AST結(jié)構(gòu)
parse函數(shù)的作用就是把字符串型的template轉(zhuǎn)化為AST結(jié)構(gòu)
如,假設(shè)我們有一個元素
texttext,在 parse 完之后會變成如下的結(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'"}] }
那么它具體是怎么解析、截取的呢?
舉個例子
<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(’<’)的值,來確定我們是要截取標(biāo)簽還是文本.
- 等于 0:這就代表這是注釋、條件注釋、doctype、開始標(biāo)簽、結(jié)束標(biāo)簽中的某一種
- 大于等于 0:這就說明是文本、表達(dá)式
- 小于 0:表示 html 標(biāo)簽解析完了,可能會剩下一些文本、表達(dá)式
若等于0
若等于0,則進(jìn)行正則匹配看是否為開始標(biāo)簽、結(jié)束標(biāo)簽、注釋、條件注釋、doctype中的一種。若是開始標(biāo)簽,則截取對應(yīng)的開始標(biāo)簽,并定義ast的基本結(jié)構(gòu),并且解析標(biāo)簽上帶的屬性(attrs, tagName)、指令等等。
當(dāng)然,這里的attrs也是通過正則匹配出來的,具體做法就是通過匹配標(biāo)簽上對應(yīng)的屬性,然后把他push到attrs里。
匹配時候的正則表達(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 = /^<!\[/
- 同時,需要注意的一點是,vue中還需要維護(hù)一個stack(可以理解為一個數(shù)組),用來標(biāo)記DOM的深度
關(guān)于stack
stack里的最后一項,永遠(yuǎn)是當(dāng)前正在解析的元素的parentNode。
通過stack解析器會把當(dāng)前解析的元素和stack里的最后一個元素建立父子關(guān)系。即把當(dāng)前節(jié)點push到stack的最后一個節(jié)點的children里,同時將它自身的parent設(shè)為stack的最后一個節(jié)點。
當(dāng)然,因為我們的標(biāo)簽中存在一種自閉和的標(biāo)簽(如input),這種類型的標(biāo)簽沒有子元素,所以不會push到stack中。
- 若是結(jié)束標(biāo)簽,則需要通過這個結(jié)束標(biāo)簽的tagName從后到前匹配stack中每一項的tagName,將匹配到的那一項之后的所有項全部刪除,表示這一段已經(jīng)解析完成。
- 若不是以上5種中的一種,則表示他是文本
等于0或大于0
若等于0且不滿足以上五種條件或大于0,則表示它是文本或表達(dá)式。
- 此時,它會判斷它的剩余部分是否符合標(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種
- 實打?qū)?lt;/div>
- 我是{{name}}</div>
如果文本中含有表達(dá)式,則需要對文本中的變量進(jìn)行解析
const expression = parseText(text, delimiters) // 對變量解析 {{name}} => _s(name) children.push({ type: 2, expression, text }) // 上例中解析過后形成如下的結(jié)構(gòu) { expression: "_s(name)", text: "我是{{name}}", type: 2 }
現(xiàn)在我們再來看最開始的例子
<div> ? ? <p>我是{{name}}</p> </div>
1.首先第一次判斷<的位置,等于0,且可以匹配上開始標(biāo)簽,則截取這個標(biāo)簽。
// 第一次截取后剩余 ? ? <p>我是{{name}}</p> </div>
2.繼續(xù)判斷<的位置,大于0(因為有空格),判斷為文本,截取這個文本
// 第二次截取后剩余 <p>我是{{name}}</p> </div>
3.繼續(xù)判斷<位置,等于0,且為開始標(biāo)簽,截取這一部分,并且維護(hù)stack,把當(dāng)前的解析的元素的parnet置為stack中的最后一項,并且在stack的最后一項的children里push當(dāng)前解析的元素
// 這里有個判斷,因為非自閉和標(biāo)簽才會有children,所以非自閉標(biāo)簽才往stack里push if (!unary) { ? currentParent = element ? stack.push(element) } // 設(shè)立父子關(guān)系 currentParent.children.push(element) element.parent = currentParent // 此時stack [divAst,pAst] // ?第三次截取后剩余 我是{{name}}</p> </div>
4.繼續(xù)判斷<的位置,大于0,判斷剩余部分是否屬于標(biāo)簽的一種,這里剩余部分可以匹配結(jié)束標(biāo)簽,則表明為文本
// 第四次截取后剩余 </p> </div>
5.繼續(xù)判斷<的位置,等于0,且匹配為結(jié)束標(biāo)簽,此時會再stack里尋找滿足tagName和當(dāng)前標(biāo)簽名相同的最后一項,把它之后項的全部刪除。
// 此時stack [divAst] // 第五次截取剩余 </div>
6.繼續(xù)通過以上方式截取,直到全部截取完畢。
parse過程總結(jié)
簡單來說,template的parse過程,其實就是不斷的截取字符串并解析它們的過程。
在此過程中,如果截取到非閉合標(biāo)簽就push到stack中,如果截取道結(jié)束標(biāo)簽就把這個標(biāo)簽pop出來。
optimize優(yōu)化
optimize的作用主要是對生成的AST進(jìn)行靜態(tài)內(nèi)容的優(yōu)化,標(biāo)記靜態(tài)節(jié)點。所謂靜態(tài)內(nèi)容,指的是和數(shù)據(jù)沒有關(guān)系,不需要每次都更新的內(nèi)容。
標(biāo)記靜態(tài)節(jié)點的作用的作用是為了之后dom diff時,是否需要patch,diff算法會直接跳過靜態(tài)節(jié)點,從而減少了比較的過程,優(yōu)化了patch的性能。
- 1.如果是表達(dá)式AST節(jié)點,直接返回 false
- 2.如果是文本AST節(jié)點,直接返回 true
- 3.如果元素是元素節(jié)點,階段有 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
簡單來說,沒有使用vue獨有的語法的節(jié)點就可以稱為靜態(tài)節(jié)點
判斷一個父級元素是靜態(tài)節(jié)點,則需要判斷它的所有子節(jié)點都是靜態(tài)節(jié)點,否則就不是靜態(tài)節(jié)點
標(biāo)記靜態(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方法是用來標(biāo)記靜態(tài)節(jié)點的方法,它會不斷的循環(huán)children,如果children還有children,則走相同的邏輯。這樣所有的節(jié)點都會被打上標(biāo)記。
在循環(huán)中會判斷,子節(jié)點是否為靜態(tài)節(jié)點,如果不是則其父節(jié)點不是靜態(tài)節(jié)點。
generate生成render函數(shù)
generate是將AST轉(zhuǎn)化成render funtion字符串的過程,他遞歸了AST,得到結(jié)果是render的字符串。
render函數(shù)的就是返回一個_c(‘tagName’,data,children)的方法
1.第一個參數(shù)是標(biāo)簽名
2.第二個參數(shù)是他的一些數(shù)據(jù),包括屬性/指令/方法/表達(dá)式等等。
3.第三個參數(shù)是當(dāng)前標(biāo)簽的子標(biāo)簽,同樣的,每一個子標(biāo)簽的格式也是_c(‘tagName’,data,children)。
generate就是通過不斷遞歸形成了這么一種樹形結(jié)構(gòu)。
genElement
:用來生成基本的render結(jié)構(gòu)或者叫createElement結(jié)構(gòu)genData
: 處理ast結(jié)構(gòu)上的一些屬性,用來生成datagenChildren
:處理ast的children,并在內(nèi)部調(diào)用genElement,形成子元素的_c()方法
render字符串內(nèi)部有幾種方法
幾種內(nèi)部方法
_c
:對應(yīng)的是 createElement 方法,顧名思義,它的含義是創(chuàng)建一個元素(Vnode)_v
:創(chuàng)建一個文本結(jié)點。_s
:把一個值轉(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": "" } }) ] ) } }
那么問題來了,_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)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue限制輸入數(shù)字或者保留兩位小數(shù)實現(xiàn)
這篇文章主要為大家介紹了vue限制輸入數(shù)字或者保留兩位小數(shù)實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07vue?中使用?this?更新數(shù)據(jù)的一次問題記錄
這篇文章主要介紹了vue?中使用?this?更新數(shù)據(jù)的一次大坑?,我在 vue 實例中聲明了一個數(shù)組屬性如?books: [],在異步請求的回調(diào)函數(shù)中使用?this.books = res.data.data;?進(jìn)行數(shù)據(jù)更新,感興趣的朋友跟隨小編一起看看吧2022-11-11Vue中transition單個節(jié)點過渡與transition-group列表過渡全過程
這篇文章主要介紹了Vue中transition單個節(jié)點過渡與transition-group列表過渡全過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04VUE識別訪問設(shè)備是pc端還是移動端的實現(xiàn)步驟
經(jīng)常在項目中會有支持pc與手機(jī)端需求,并且pc與手機(jī)端是兩個不一樣的頁面,這時就要求判斷設(shè)置,下面這篇文章主要給大家介紹了關(guān)于VUE識別訪問設(shè)備是pc端還是移動端的相關(guān)資料,需要的朋友可以參考下2023-05-05vue中keep-alive、activated的探討和使用詳解
這篇文章主要介紹了vue中keep-alive、activated的探討和使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07