本文我們一起通過學習Vue模板編譯原理(一)-Template生成AST來分析Vue原始碼,預計接下來會圍繞Vue原始碼來整理一些文章,如下,
- 一起來學Vue雙向系結原理-資料劫持和發布訂閱
- 一起來學Vue模板編譯原理(一)-Template生成AST
- 一起來學Vue模板編譯原理(二)-AST生成Render字串
- 一起來學Vue虛擬DOM決議-Virtual Dom實作和Dom-diff演算法
這些文章統一放在我的git倉庫:https://github.com/yzsunlei/javascript-series-code-analyzing,覺得有用記得star收藏,
編譯程序
模板編譯是Vue中比較核心的一部分,關于Vue編譯原理這塊的整體邏輯主要分三個部分,也可以說是分三步,前后關系如下:
第一步:將模板字串轉換成element ASTs(決議器)
第二步:對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器)
第三步:使用element ASTs生成render函式代碼字串(代碼生成器)
對應的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),對應的原始碼實作我們通常稱之為決議器,
決議器運行程序
在分析決議器的原理前,我們先舉例看下決議器的具體作用,
來一個最簡單的實體:
<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上;然后決議到時,又觸發一次鉤子函式start,處理匹配片段,又生成一個標簽節點并作為上一個節點的子節點添加到AST上;接著決議到{{name}}這行文本,此時觸發了文本鉤子函式chars,處理匹配片段,生成一個帶變數文本(變數文本下面會講到)標簽節點并作為上一個節點的子節點添加到AST上;然后決議到
,觸發了標簽結束的鉤子函式end;接著繼續決議到,此時又觸發一次標簽結束的鉤子函式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模板中截取一小段字串,然后重復以上程序,直到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>
第一輪回圈時,截取出一段字串
,決議出是div開始標簽并且觸發鉤子函式start,截取后的結果為:
<p>{{text}}</p>
</div>
第二輪回圈時,截取出一段換行空字串,會觸發鉤子函式chars,截取后的結果為:
<p>{{text}}</p>
</div>
第三輪回圈時,截取出一段字串
,決議出是p開始標簽并且觸發鉤子函式start,截取后的結果為:
{{text}}</p>
</div>
第四輪回圈時,截取出一段字串{{name}},決議出是變數字串并且觸發鉤子函式chars,截取后的結果為:
</p>
</div>
第五輪回圈時,截取出一段字串
,決議出是p閉合標簽并且觸發鉤子函式end,截取后的結果為:
</div>
第六輪回圈時,截取出一段換行空字串,會觸發鉤子函式chars,截取后的結果為:
</div>
第七輪回圈時,截取出一段字串
,決議出是div閉合標簽并且觸發鉤子函式end,截取后的結果為:
第八輪回圈時,發現只有一個空字串,決議完畢,回圈結束,
現在,是不是就對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 = https://www.cnblogs.com/yzsunlei/p/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 = https://www.cnblogs.com/yzsunlei/p/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決議器決議出來的文本進行二次加工,主要是為了處理帶變數的文本,
相關
- https://juejin.im/post/5ca44160518825440a4b9fab
- https://segmentfault.com/a/1190000012922342
- https://www.jianshu.com/p/743166a8968c
- https://segmentfault.com/a/1190000013763590
- https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90ast.md
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/158911.html
標籤:JavaScript
上一篇:Typescript 最佳實踐
下一篇:來自程式員的浪漫
