Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


基礎編譯器 baseCompile

看下編譯器工廠中,baseCompile 基礎編譯器的方法代碼:

<code>//source-code\\vue\\src\\compiler\\index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})/<code>

其中分為 parse generate 兩個大部分拆解來看。

AST 抽象語法樹的生成

大體過程

通過 parse 方法,解析 template 模板代碼:

<code>//src\\compiler\\index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
// ...
})/<code>


parse 內部調用 parseHTML 方法,AST 相關的解析也就在此方法中:

<code>//src\\compiler\\parser\\index.js
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// ...
parseHTML(template, {
/** 一堆參數*/,
start(){},
end(){},
chars(){},
comment(){}
})
return root
}/<code>
<code>//src\\compiler\\parser\\html-parser.js
export function parseHTML (html, options) {
\twhile(html){
//...
}
}/<code>

主要過程都在 parseHTML 方法中,下面逐步對 parseHTML

來分析。


不停解析什麼模板?

一個非常簡單的 helloworld 示例:

<code>

{{ message }}

/<code>


看到進入此方法時,html 到底是什麼內容?

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


然後通過 while 不停解析 html,當 html 全部解析完畢後,將結束 while 循環:

<code>export function parseHTML (html, options) {
const stack = []
//...
let index = 0
let last, lastTag
while (html) {
last = html
if (!lastTag || !isPlainTextElement(lastTag)) {
}else{
}
if (html === last) {
}
}
parseEndTag()
}/<code>


跳過一些不相干代碼

由於我們才開始解析 html,一個標準的 html 必然以 < 開頭,那麼 textEnd=0

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


接下來就會對幾種常見的"< 起始的 html"特定用法做解析:

<code>if (textEnd === 0) {
// 解析註釋
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
\tadvance(xx)
\tcontinue
}
}

// 解析條件註釋
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}

// 解析
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}

// 解析 結束和開始標籤 tag
}/<code>

我們知道以上這些 html 語法對代碼邏輯都不具備什麼大作用,所以就偏移 advance 對應長度,通過

continue 繼續回到 while 再解析。


偏移 advance

上面反覆出現 advance 方法,這是個工具方法,其具體作用如下:

  • 根據解析到的長度 n,累加到 index 索引變量中
  • 並且拋棄 n 長度之前的 html 模板,從而截取新的 html
<code>function advance (n) {
index += n
html = html.substring(n)
}/<code>


解析 startTag 標籤

結束註釋之類 html 的判斷,接下來就是對 startTag 和 endTag 標籤的解析:

<code>if (textEnd === 0) {
// ...
// 解析 結束和開始標籤 tag
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {

const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}

// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}/<code>


簡單理解下正則

這兩個解析判斷中,涉及兩個比較複雜的正則:

<code>const ncname = `[a-zA-Z_][\\\\-\\\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\\\:)?${ncname})`
const startTagOpen = new RegExp(`^const startTagClose = /^\\s*(\\/?)>/
const endTag = new RegExp(`^]*>`)/<code>


好吧,我看到這些是頭暈的。下面簡化下,便於知道結束標籤 endTag 是怎麼工作的:

<code>const ncname=`[a-z]*`
const qnameCapture = `((?:${ncname})?${ncname})`

// 簡化後的 endTag:/^]*>/
const endTag = new RegExp(`^]*>`)/<code>


Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


如果標籤正好是封閉標籤,將被 endTagMatch 將會匹配到結果。那麼就知道源碼中到底是在幹什麼了:

<code>// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}/<code>

同理,也很好理解 startTagMatch 的處理邏輯:

<code>// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
//...
continue
}/<code>


開始解析 startTag 標籤

對於我們目前的 html 模板代碼來說,是先被 startTagMatch 匹配到,然後再到 endTagMatch 的。下面逐步來看:

先是 parseStartTag 方法的處理:

<code>function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index
advance(attr[0].length)
attr.end = index
match.attrs.push(attr)
}
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}/<code>


可能有些複雜,簡單來說幹麼那麼些事:

  • 正則匹配到結果
  • 對結果,進行對象封裝,形成 match 對象(有:tagName、attrs、start 字段)
  • 通過 advance 對 html 繼續往後解析
  • 解析標籤內部的 attr 屬性,放到 match.attrs 對象中
  • 記錄 startTag 標籤的 > 位置


截了幾個圖:

我們 html 經過 advance 後,將去除"

"這些內容(被 startTagOpen 正則匹配的),然後在此方法中的 while 內進行第一部分的判斷(是否 startTag 標籤結束了):
Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


然後根據 dynamicArgAttribute 正則解析,提取出 attr 內容(對於正則邏輯,這裡跳過),大概長這樣子:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


每次執行 while 時,將得到對應的 end,通過

advance 的處理後,當 while 不再進入後,我們的 html 就成了這樣子(startTag 前半段解析完畢):

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

最後,我們的 match 對象結果如下:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


此方法結束後,接下來會進到 handleStartTag 方法:

<code>function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash

//...

const unary = isUnaryTag(tagName) || !!unarySlash

const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
//...
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}

if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
lastTag = tagName
}

if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}/<code>

註釋掉一些特殊情況的代碼,能看到此方法會對我們 match 對象中的 attrs 屬性進行加工,添加些新字段:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

然後提取部分字段(標籤名稱 tagName、屬性 attrs 、開始結束索引值)塞到 stack 堆棧中,這個堆棧的結構像是這樣:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

最後調用 parseHTML 中的 options.start 方法:

<code>if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}/<code>


options.start

這又是個複雜的方法:

<code>start (tag, attrs, unary, start, end) {
//...
let element: ASTElement = createASTElement(tag, attrs, currentParent)
//...
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}

if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
}

if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}

if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
}/<code>

通過 createASTElement 方法,來創建 element 元素對象。先看下其中 element 到時是什麼結構:

<code>export function createASTElement (
tag: string,
attrs: Array<astattr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}/<astattr>/<code>


Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


然後會判斷是否是 root 節點,最後更新 currentParent 當前父節點字段,並往 stack 塞入 element。


舉例 process 處理

為了能走到對應的 process 判斷,這裡將我們的 demo 添加個 v-if 判斷邏輯:

<code>

{{ message }}
/<code>

這樣能在 option.start 方法中,進入對應的 processXX 方法:

<code>start (tag, attrs, unary, start, end) {
\t//...
if(){}
else if (!element.processed) {
// structural directives
processIf(element)
//...
}
//...
}/<code>

大致看下,這個

processIf 會怎麼更新我們的 element:

<code>function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}/<code>

先看下執行 getAndRemoveAttr 之前 element 長什麼樣:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

processIf 處理完後,會對 attrsList 進行元素的剔除,另外會增加對應條件相關字段:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

就這樣,最後把處理後的 element 被放入 stack 隊列。


text 內容解析

經歷 startTag 的邏輯判斷,我們當前 html 變成了如下形式:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


由於第二次,進入 while 判斷時,會再次取得 < 開始標籤的起始位置,則進入不到 if (textEnd === 0) 裡了,就會開始一段新的邏輯

<code>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(' if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}/<code>

當然這段邏輯不必費太多心思,因為 text 這個變量名稱基本猜到它是幹什麼的了。由於我們 textEnd 重新取值,當前判斷會進入 textEnd>=0 條件中,那麼最終會執行到如下代碼,就獲得了標籤內部文本內({{message}}):

<code>text = html.substring(0, textEnd)/<code>
Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


當然能注意到在他之前還有一個 while 判斷,它會幫我們排除一些特殊情況(雖然 textEnd 以 < 開頭,但標籤內部可能是註釋、條件註釋、結尾標籤、內嵌開始標籤),然後通過不停遍歷,直至找到真正的 text 內容。

<code>chars (text: string, start: number, end: number) {
//...
const children = currentParent.children
//...
if (text) {
//...
let res
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
}
}
if (child) {
children.push(child)
}
}
}/<code>


options.chars

通過

options.chars 方法,在繼前面 options.start 方法中賦值的 currentParent 中添加 children 對象:

<code>chars (text: string, start: number, end: number) {
//...
const children = currentParent.children
//...
if (text) {
//...
let res
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
}
}
if (child) {
children.push(child)
}
}
}/<code>

將解析到的 text 內容賦值給 child 對象,最後塞到 children 中:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

解析 endTag 標籤

以上步驟走完後,下面就會解析我們的閉合標籤:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

<code>// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}/<code>

能看到 parseEndTag 中會處理 p、br 等一些 warn 邏輯,當然這裡圖方便也一併跳過:

<code>function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index

// Find the closest opened tag of the same type
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0
}

if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
// warn ...
if (options.end) {
options.end(stack[i].tag, start, end)
}
}

// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
// ...
} else if (lowerCasedTagName === 'p') {
// ...
}
}/<code>

能看到上面方法是為了判斷標籤開始和結束做匹配用的。最終會進入到 options.end 收尾:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析


options.end

options.end 中就是一個對 stack 的出棧操作,並且更新 currentParent 對象:

<code>end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
//...
closeElement(element)
}/<code>

最後通過 closeElement 做一些其他的判斷。

最後

所有 html 模板都解析完後,將返回 root 對象(其就是一個封裝了 AST 的 element 結構),供給模板編譯調用。

<code>export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// ...
parseHTML(template, {
/** 一堆參數*/,
start(){},
end(){},
chars(){},
comment(){}
})
return root
}/<code>

生成可運行 Code

<code>//src\\compiler\\index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// ...
\tconst code = generate(ast, options)
})/<code>

generate

generate 方法返回的 render

staticRenderFns 結果都是通過 CodegenState 創建後得到:

<code>export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}/<code>

CodegenState 只是個包含構造函數的對象,最後將創建後的實例賦值給 state 變量:

<code>export class CodegenState {
options: CompilerOptions;
warn: Function;
transforms: Array<transformfunction>;
dataGenFns: Array<datagenfunction>;
directives: { [key: string]: DirectiveFunction };
maybeComponent: (el: ASTElement) => boolean;
onceId: number;
staticRenderFns: Array<string>;
pre: boolean;

constructor (options: CompilerOptions) {
this.options = options
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
this.directives = extend(extend({}, baseDirectives), options.directives)
const isReservedTag = options.isReservedTag || no
this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
this.onceId = 0
this.staticRenderFns = []
this.pre = false
}
}/<string>/<datagenfunction>/<transformfunction>/<code>

genElement

接下來看對應 code 變量的實現,通過 genElement 來生成對應元素:

<code>export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}

if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}

const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)

}
return code
}
}/<code>

我們前面在 AST 中為了測試 processIf 的功能,添加了 v-if 屬性,所以在這裡會先進入到 genIf 的邏輯:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

接下來就會走到 genIfConditions 方法:

<code>export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}/<code>

genIfConditions 方法內部會根據我們的條件表達式 condition.exp 結果,通過 genTernaryExp 來渲染對應的 condition.block 元素模塊;反之,繼續根據剩餘的 condition 表達式再次進入 genIfConditions 方法。

<code>// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}

const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code/<code>

可以把中心放在 genTernaryExp

方法中 return 的三元表達式。

目前又會執行到 genElement 方法:

<code>// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}

const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code/<code>


genData

getData 會構造一個對象字面量,裡面 key/value 將是我們 AST 處理過程中解析後的一些關鍵詞(比如:attrs、key、ref、pre、props、events 等)

目前我們的 data 結果將是模板中設置的 id 屬性:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

genChildren

getChildren 會從 AST element 的 children 中拿到子元素們,根據不同節點類型判斷 gen 方式。當然目前 children 的解析結果如下:

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

所以最後的完整的 genTernaryExp 的結果將是如下這個樣子

Vue 源碼淺析:模板渲染-編譯器 baseCompile 裡的 AST 解析

最後

通過 generate 和 內部的 genElement 使得 $mount 中的 vue.options 得到了 render staticRenderFns 兩個參數:

<code>export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {

const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}/<code>
<code>options.render = render
options.staticRenderFns = staticRenderFns/<code>
"


分享到:


相關文章: