本文咱們一塊兒經過學習Vue模板編譯原理(一)-Template生成AST來分析Vue源碼。預計接下來會圍繞Vue源碼來整理一些文章,以下。javascript
這些文章統一放在個人git倉庫:github.com/yzsunlei/ja…。以爲有用記得star收藏。html
模板編譯是Vue中比較核心的一部分。關於Vue編譯原理這塊的總體邏輯主要分三個部分,也能夠說是分三步,先後關係以下:vue
第一步:將模板字符串轉換成element ASTs(解析器)java
第二步:對 AST 進行靜態節點標記,主要用來作虛擬DOM的渲染優化(優化器)git
第三步:使用element ASTs生成render函數代碼字符串(代碼生成器)github
對應的Vue源碼以下,源碼位置在src/compiler/index.js
正則表達式
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1.parse,模板字符串 轉換成 抽象語法樹(AST)
const ast = parse(template.trim(), options)
// 2.optimize,對 AST 進行靜態節點標記
if (options.optimize !== false) {
optimize(ast, options)
}
// 3.generate,抽象語法樹(AST) 生成 render函數代碼字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
複製代碼
這篇文檔主要講第一步將模板字符串轉換成對象語法樹(element ASTs),對應的源碼實現咱們一般稱之爲解析器。算法
在分析解析器的原理前,咱們先舉例看下解析器的具體做用。express
來一個最簡單的實例:segmentfault
<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並非什麼很神奇的東西,不要被它的名字嚇倒。它只是用JS中的對象來描述一個節點,一個對象表明一個節點,對象中的屬性用來保存節點所需的各類數據。
事實上,解析器內部也分了好幾個子解析器,好比HTML解析器、文本解析器以及過濾器解析器,其中最主要的是HTML解析器。顧名思義,HTML解析器的做用是解析HTML,它在解析HTML的過程當中會不斷觸發各類鉤子函數。這些鉤子函數包括開始標籤鉤子函數、結束標籤鉤子函數、文本鉤子函數以及註釋鉤子函數。
咱們先看下解析器總體的代碼結構,源碼位置src/compiler/parser/index.js
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 每當解析到標籤的開始位置時,觸發該函數
start (tag, attrs, unary, start, end) {
//...
},
// 每當解析到標籤的結束位置時,觸發該函數
end (tag, start, end) {
//...
},
// 每當解析到文本時,觸發該函數
chars (text: string, start: number, end: number) {
//...
},
// 每當解析到註釋時,觸發該函數
comment (text: string, start, end) {
//...
}
})
複製代碼
實際上,模板解析的過程就是不斷調用鉤子函數的處理過程。整個過程,讀取template字符串,使用不一樣的正則表達式,匹配到不一樣的內容,而後觸發對應不一樣的鉤子函數處理匹配到的截取片斷,好比開始標籤正則匹配到開始標籤,觸發start鉤子函數,鉤子函數處理匹配到的開始標籤片斷,生成一個標籤節點添加到抽象語法樹上。
還舉上面那個例子來講:
<div>
<p>{{name}}</p>
</div>
複製代碼
整個解析運行過程就是:解析到
時,又觸發一次鉤子函數start,處理匹配片斷,又生成一個標籤節點並做爲上一個節點的子節點添加到AST上;接着解析到{{name}}這行文本,此時觸發了文本鉤子函數chars,處理匹配片斷,生成一個帶變量文本(變量文本下面會講到)標籤節點並做爲上一個節點的子節點添加到AST上;而後解析到
,觸發了標籤結束的鉤子函數end;接着繼續解析到模板解析過程會涉及到許許多多的正則匹配,知道每一個正則有什麼用途,會更加方便以後的分析。
那咱們先來看看這些正則表達式,源碼位置在src/compiler/parser/index.js
export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
? /^v-|^@|^:|^\.|^#/
: /^v-|^@|^:|^#/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const dynamicArgRE = /^\[.*\]$/
const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
const slotRE = /^v-slot(:|$)|^#/
const lineBreakRE = /[\r\n]/
const whitespaceRE = /\s+/g
const invalidAttributeRE = /[\s"'<>\/=]/
複製代碼
上面這些正則相對來講比較簡單,基本上都是用來匹配Vue中自定義的一些語法格式,如onRE匹配 @ 或 v-on 開頭的屬性,forAliasRE匹配v-for中的屬性值,好比item in items、(item, index) of items。
下面這些就是專門針對html的一些正則匹配,源碼位置在src/compiler/parser/html-parser.js
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
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 = /^<!\[/
複製代碼
這些正則表達式相對來講就複雜一些,如attribute用來匹配標籤的屬性,startTagOpen、startTagClose用於匹配標籤的開始、結束部分等。這些正則表達式的寫法就很少說了,有興趣的朋友能夠針對這些正則一個一個的去測試一下。
這裏咱們來看看HTMl解析器。
事實上,解析HTML模板的過程就是循環的過程,簡單來講就是用HTML模板字符串來循環,每輪循環都從HTML模板中截取一小段字符串,而後重複以上過程,直到HTML模板被截成一個空字符串時結束循環,解析完畢。
咱們經過源碼,就能夠看到整個函數邏輯就是被一個while循環包裹着。源碼位置在:src/compiler/parser/html-parser.js
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
while (html) {
//...
}
parseEndTag()
//...
}
複製代碼
下面我用一個簡單的模板,模擬一下HTML解析的過程,以便於更好的理解。
<div>
<p>{{text}}</p>
</div>
複製代碼
最初的HTML模板:
<div>
<p>{{text}}</p>
</div>
複製代碼
第一輪循環時,截取出一段字符串
<p>{{text}}</p>
</div>
複製代碼
第二輪循環時,截取出一段換行空字符串,會觸發鉤子函數chars,截取後的結果爲:
<p>{{text}}</p>
</div>
複製代碼
第三輪循環時,截取出一段字符串
,解析出是p開始標籤而且觸發鉤子函數start,截取後的結果爲:
{{text}}</p>
</div>
複製代碼
第四輪循環時,截取出一段字符串{{name}},解析出是變量字符串而且觸發鉤子函數chars,截取後的結果爲:
</p>
</div>
複製代碼
第五輪循環時,截取出一段字符串
,解析出是p閉合標籤而且觸發鉤子函數end,截取後的結果爲:</div>
複製代碼
第六輪循環時,截取出一段換行空字符串,會觸發鉤子函數chars,截取後的結果爲:
</div>
複製代碼
第七輪循環時,截取出一段字符串
複製代碼
第八輪循環時,發現只有一個空字符串,解析完畢,循環結束。
如今,是否是就對HTML解析過程很清楚了。其實循環過程對每次匹配到的片斷進行分析記錄仍是很複雜的,由於被截取的片斷分不少種類型,好比:
開始標籤,例如
<div>
結束標籤,例如
</div>
HTML註釋,例如
<!-- 註釋 -->
DOCTYPE,例如
<!DOCTYPE html>
條件註釋,例如
<!--[if !IE]>-->註釋<!--<![endif]-->
文本,例如'字符串'
對每一個片斷的具體處理這裏就不說了,有興趣的直接看源碼去。
文本解析器是對HTML解析器解析出來的文本進行二次加工。文本其實分兩種類型,一種是純文本,另外一種是帶變量的文本。以下:
這種就是純文本:
這裏有段文本
複製代碼
這種就是帶變量的文本:
文本內容:{{text}}
複製代碼
上面HTML解析器在解析文本時,並不會區分文本是不是帶變量的文本。若是是純文本,不須要進行任何處理;但若是是帶變量的文本,那麼須要使用文本解析器進一步解析。由於帶變量的文本在使用虛擬DOM進行渲染時,須要將變量替換成變量中的值。
咱們知道,HTML解析器在碰到文本時,會觸發chars鉤子函數,咱們先來看看鉤子函數裏面是怎麼區分普通文本和變量文本的。
源碼位置在:src/compiler/parser/html-parser.js
chars (text: string, start: number, end: number) {
//...
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
//...
children.push(child)
}
複製代碼
咱們重點看res = parseText(text,delimiters)
這一行,經過條件判斷設置不一樣的類型。事實上type=2表示表達式類型,type=3表示普通文本類型。
咱們再來看看parseText函數具體作了什麼
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
// 匹配不到帶變量時直接返回了
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
// 對匹配到的變量循環處理成表達式
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
// 先把 { { 前邊的文本添加到tokens中
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
const exp = parseFilters(match[1].trim())
// 使用_s對變量進行包裝
// 把變量改爲`_s(x)`這樣的形式也添加到數組中
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
// 設置lastIndex來保證下一輪循環時,正則表達式再也不重複匹配已經解析過的文本
lastIndex = index + match[0].length
}
// 當全部變量都處理完畢後,若是最後一個變量右邊還有文本,就將文本添加到數組中
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
複製代碼
實際上這個函數就是處理帶變量的文本,首先若是是純文本,直接return。若是是帶變量的文本,使用正則表達式匹配出文本中的變量,先把變量左邊的文本添加到數組中,而後把變量改爲_s(x)這樣的形式也添加到數組中。若是變量後面還有變量,則重複以上動做,直到全部變量都添加到數組中。若是最後一個變量的後面有文本,就將它添加到數組中。
那麼對於上面示例處理結果以下:
parseText('這裏有段文本')
// undefined
parseText('文本內容:{{text}}')
// '"文本內容:" + _s(text)'
複製代碼
好了,對於文本解析器就這麼多內容。
模板解析是Vue模板編譯的第一步,即經過模板獲得AST(抽象語法樹)。
生成AST的過程核心就是藉助HTML解析器,當HTML解析器經過正則匹配到不一樣的片斷時會觸發對應不一樣的鉤子函數,經過鉤子函數對匹配片斷進行解析咱們能夠構建出不一樣的節點。
文本解析器是對HTML解析器解析出來的文本進行二次加工,主要是爲了處理帶變量的文本。