[TOC]
- Vue 學(xué)習(xí)筆記
- Vue 源碼解析 - 主線流程
- Vue 源碼解析 - 模板編譯
- Vue 源碼解析 - 組件掛載
- Vue 源碼解析 - 數(shù)據(jù)驅(qū)動(dòng)與響應(yīng)式原理
模板編譯
前文在對(duì) Vue 源碼解析 - 主線流程 進(jìn)行分析時(shí),我們已經(jīng)知道對(duì)于 Runtime + Compiler 的編譯版本來(lái)說(shuō)日矫,Vue 在實(shí)例化前總共會(huì)經(jīng)歷兩輪mount
過(guò)程梧躺,分別為:
定義于
src\platforms\web\runtime\index.js
的$mount
函數(shù),主要負(fù)責(zé)組件掛載功能。定義于
src\platforms\web\entry-runtime-with-compiler.js
的$mount
函數(shù),主要負(fù)責(zé)模板編譯 + 組件掛載(其會(huì)緩存src\platforms\web\runtime\index.js
中定義的$mount
函數(shù),最后的組件掛載轉(zhuǎn)交給該函數(shù)進(jìn)行處理)功能盐捷。
以下我們對(duì)src\platforms\web\entry-runtime-with-compiler.js
的$mount
函數(shù)進(jìn)行解析,主要分析 模板編譯 部分內(nèi)容:
// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean,
): Component {
// 獲取 el 元素對(duì)象默勾,找不到則返回一個(gè) div
el = el && query(el);
...
const options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template;
if (template) {
// Vue.$options.template 為字符串
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 由 id 取得對(duì)應(yīng)的 DOM 元素的 innerHTML
template = idToTemplate(template);
...
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this);
}
return this;
}
} else if (el) { // 沒(méi)有 template
template = getOuterHTML(el);
}
if (template) {
...
// 對(duì)模板進(jìn)行編譯
const {render, staticRenderFns} = compileToFunctions(
template,
{
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this,
);
options.render = render;
options.staticRenderFns = staticRenderFns;
...
}
}
return mount.call(this, el, hydrating);
};
function getOuterHTML(el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// src/shared/util.js
export function cached<F: Function>(fn: F): F {
const cache = Object.create(null)
return (function cachedFn(str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
從源碼中可以看到碉渡,只有在Options
沒(méi)有定義render
函數(shù)時(shí),才會(huì)進(jìn)行模板編譯母剥。
模板編譯步驟共分兩步:
-
獲取模板字符串:模板字符串的獲取包含以下幾種情況:
如果沒(méi)有定義
template
滞诺,則直接獲取el
元素的outerHTML
,即把el
元素作為template
环疼。-
如果
template
為字符串习霹,并且以#
開(kāi)頭,則表明template
是以id
進(jìn)行指定炫隶,則通過(guò)該id
獲取對(duì)應(yīng)元素的innerHTML
淋叶。注:
cached
函數(shù)參數(shù)為一個(gè)函數(shù),返回為一個(gè)參數(shù)為string
的函數(shù)伪阶,在該返回函數(shù)內(nèi)部會(huì)調(diào)用cached
函數(shù)的函數(shù)參數(shù)煞檩,并做一個(gè)緩存處理。
對(duì)應(yīng)于我們編譯這部分栅贴,即會(huì)緩存以id
進(jìn)行聲明的template
的innerHTML
斟湃。 如果
template
為字符串,并且不以#
開(kāi)頭檐薯,則表明template
是一個(gè)完整的模板字符串凝赛,直接返回本身即可。如果
template
為nodeType
類型坛缕,直接返回其innerHTML
墓猎。如果定義了
template
,但格式無(wú)法識(shí)別(即不是字符串祷膳,也不是nodeType
類型)陶衅,則給出警告,并退出編譯流程直晨。
將模板字符串編譯為
render
函數(shù):該功能主要由函數(shù)compileToFunctions
進(jìn)行實(shí)現(xiàn)搀军,其源碼如下所示:
// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (...): CompiledResult {...})
// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
compileToFunctions
是由createCompiler(baseOptions)
返回的,而createCompiler
為createCompilerCreator(function baseCompile (...){...})
勇皇,這里其實(shí)使用了 函數(shù)柯里化 的思想罩句,將接收多個(gè)參數(shù)的函數(shù)轉(zhuǎn)化為接收單一參數(shù)的函數(shù),這樣做的原因是 編譯 這個(gè)流程和平臺(tái)或構(gòu)建方式相關(guān)敛摘,采用 函數(shù)柯里化门烂,將與平臺(tái)無(wú)關(guān)的東西固定化,只留出平臺(tái)相關(guān)的內(nèi)容作為參數(shù),簡(jiǎn)化調(diào)用屯远。比如蔓姚,這里固定化參數(shù)為baseCompile
,其主要負(fù)責(zé)模板的解析慨丐,優(yōu)化并最終生成模板代碼的字符串(具體詳情見(jiàn)后文)坡脐,該操作是平臺(tái)無(wú)關(guān)操作,而與平臺(tái)相關(guān)的參數(shù)為baseOptions
房揭,不同的平臺(tái)該參數(shù)不同备闲。
簡(jiǎn)而言之,compileToFunctions
會(huì)經(jīng)由createCompilerCreator(function baseCompile (...){...}) --> createCompiler(baseOptions)
而得到捅暴。
因此恬砂,我們先來(lái)看下createCompilerCreator(function baseCompile (...){...})
的源碼實(shí)現(xiàn):
// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (...){...})
// src\compiler\create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (...): CompiledResult {
...
const compiled = baseCompile(template.trim(), finalOptions)
...
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
所以createCompilerCreator
就是固定了參數(shù)baseCompile
,并返回一個(gè)函數(shù)createCompiler
蓬痒,該函數(shù)內(nèi)部又會(huì)返回一個(gè)包含兩個(gè)函數(shù)的實(shí)例泻骤,這其中就有一個(gè)我們需要分析的函數(shù)compileToFunctions
(這個(gè)就是$mount
函數(shù)內(nèi)部使用的createCompileToFunctionFn
),其指向?yàn)楹瘮?shù)createCompileToFunctionFn(compile)
的執(zhí)行結(jié)果乳幸,我們先對(duì)函數(shù)createCompileToFunctionFn
源碼進(jìn)行查看:
// src/compiler/to-function.js
export function createCompileToFunctionFn(compile: Function): Function {
...
return function compileToFunctions(...): CompiledFunctionResult {...}
}
可以看到又是一個(gè) 函數(shù)柯里化 的操作瞪讼,固定了平臺(tái)無(wú)關(guān)參數(shù)compile
,并返回了我們最終需要的compileToFunctions
函數(shù)粹断。
注:compileToFunctions
函數(shù)獲取這部分的代碼由于采用了多個(gè) 函數(shù)柯里化 操作符欠,導(dǎo)致代碼邏輯比較混亂,下面是該部分代碼的整個(gè)調(diào)用鏈:
// src/platforms/web/entry-runtime-with-compiler.js
const {render, staticRenderFns} = compileToFunctions(template, {...}, this)
// src/platforms/web/compiler/index.js
const {compile, compileToFunctions} = createCompiler(baseOptions)
// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile(...) {...})
// src/compiler/create-compiler.js
export function createCompilerCreator(baseCompile: Function): Function {
return function createCompiler(baseOptions: CompilerOptions) {
function compile(...): CompiledResult {...}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
// src/compiler/to-function.js
export function createCompileToFunctionFn(compile: Function): Function {
return function compileToFunctions(...): CompiledFunctionResult {
...
const compiled = compile(template, options)
...
}
}
可以看到瓶埋,compileToFunctions
的獲取調(diào)用鏈為:createCompilerCreator --> createCompiler --> createCompileToFunctionFn --> compileToFunctions
希柿。
到這里我們才理清了compileToFunctions
函數(shù)的定義出處,現(xiàn)在回到主線流程养筒,看下compileToFunctions
是怎樣具體編譯出render
函數(shù):
// src/compiler/to-function.js
export function createCompileToFunctionFn(compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions(
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
...
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
const compiled = compile(template, options)
...
// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors) // 生成渲染函數(shù)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
...
return (cache[key] = res)
}
}
function createFunction(code, errors) {
try {
return new Function(code) // 將字符串 code 渲染成函數(shù)
} catch (err) {
errors.push({err, code})
return noop
}
}
所以當(dāng)我們調(diào)用compileToFunctions
時(shí)曾撤,其會(huì)做如下三件事:
模板編譯:通過(guò)函數(shù)
compile
進(jìn)行編譯。生成渲染函數(shù):通過(guò)函數(shù)
createFunction
將編譯完成的模板生成相應(yīng)的渲染函數(shù)(其實(shí)就是使用Function
構(gòu)造函數(shù)將編譯完成的模板代碼字符串轉(zhuǎn)換成函數(shù))晕粪。緩存渲染函數(shù):依據(jù)模板字符串內(nèi)容作為鍵值挤悉,緩存其編譯結(jié)果。
這里面最核心的就是 模板編譯 步驟巫湘,目的就是編譯出模板對(duì)應(yīng)的渲染函數(shù)字符串装悲。
我們著重對(duì)這步進(jìn)行分析,對(duì)compile
函數(shù)進(jìn)行源碼查看:
// src/compiler/create-compiler.js
export function createCompilerCreator(baseCompile: Function): Function {
return function createCompiler(baseOptions: CompilerOptions) {
function compile(
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
...
const compiled = baseCompile(template.trim(), finalOptions)
...
return compiled
}
...
}
}
compile
內(nèi)部會(huì)將編譯過(guò)程交由參數(shù)baseCompile
進(jìn)行實(shí)際處理尚氛,而根據(jù)我們前面的分析诀诊,baseCompile
就是函數(shù)createCompilerCreator
采用 函數(shù)柯里化 固定的平臺(tái)無(wú)關(guān)的參數(shù),其源碼如下所示:
// src/compiler/index.js
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
}
}
因此阅嘶,$mount
函數(shù)內(nèi)部的compileToFunctions
函數(shù)最終調(diào)用的就是baseCompile
函數(shù)進(jìn)行模板編譯流程属瓣。
從源碼中可以看到载迄,baseCompile
函數(shù)內(nèi)部主要做了三件事:
模板編譯:由函數(shù)
parse
進(jìn)行模板編譯,并生成抽象語(yǔ)法樹(shù) AST抡蛙。優(yōu)化 AST:由函數(shù)
optimize
負(fù)責(zé)护昧。生成代碼:由函數(shù)
generate
負(fù)責(zé)根據(jù) AST 和編譯選項(xiàng)生成相應(yīng)代碼(字符串形式)。
我們下面針對(duì)這三個(gè)過(guò)程繼續(xù)進(jìn)行分析:
-
模板編譯:編譯過(guò)程的第一步就是解析模板字符串溜畅,生成抽象語(yǔ)法樹(shù) AST捏卓。
我們進(jìn)入parse
函數(shù)极祸,查看其源碼:
// src/compiler/parser/index.js
/**
* Convert HTML string to AST.
*/
export function parse(
template: string,
options: CompilerOptions
): ASTElement | void {
...
let root
...
parseHTML(template, {
...
start(tag, attrs, unary, start, end) {
...
let element: ASTElement = createASTElement(tag, attrs, currentParent)
...
if (!root) {
root = element
...
}
...
},
end(tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
...
closeElement(element)
},
chars(text: string, start: number, end: number) {
...
parseText(text, delimiters)
...
children.push(child)
...
},
comment(text: string, start, end) {
if (currentParent) {
...
currentParent.children.push(child)
}
}
})
return root
}
parse
函數(shù)最終通過(guò)調(diào)用函數(shù)parseHTML
對(duì)模板進(jìn)行解析慈格,查看parseHTML
源碼:
// src/compiler/parser/html-parser.js
// Regular Expressions for parsing tags and attributes
...
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
...
export function parseHTML(html, options) {
...
let index = 0
let last, lastTag
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
...
advance(commentEnd + 3)
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
...
advance(conditionalEnd + 2)
continue
}
}
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 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) {
...
advance(1)
}
continue
}
}
...
if (textEnd >= 0) {
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
...
text = html.substring(0, textEnd)
}
...
advance(text.length)
...
} else {
...
parseEndTag(stackedTag, index - endTagLength, index)
}
...
}
// Clean up any remaining tags
parseEndTag()
function advance(n) {
index += n
html = html.substring(n)
}
...
}
簡(jiǎn)單來(lái)說(shuō),parseHTML
函數(shù)采用正則表達(dá)式來(lái)解析模板template
遥金,其解析步驟大概如下所示:
-
首先獲取模板字符
<
的索引位置浴捆,如果索引為0
,則表明當(dāng)前模板以<
開(kāi)頭稿械,則使用正則依次判斷template
是否匹配Comment
选泻,conditionalComment
,Doctype
美莫,End Tag
還是Start Tag
页眯,匹配完成后,依據(jù)不同的匹配標(biāo)簽進(jìn)行各自的解析厢呵,比如窝撵,對(duì)于Comment
標(biāo)簽,則會(huì)進(jìn)行如下解析:// Comment: if (comment.test(html)) { const commentEnd = html.indexOf('-->') if (commentEnd >= 0) { ... options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3) ... advance(commentEnd + 3) continue } }
即如果
template
匹配Comment
標(biāo)簽襟铭,則找到-->
的索引位置碌奉,即找到注釋標(biāo)簽的末尾位置,然后取出注釋內(nèi)容html.substring(4,commentEnd)
赐劣,交由options.comment
函數(shù)進(jìn)行處理(options.comment
其實(shí)是parse
函數(shù)內(nèi)調(diào)用parseHTML
傳遞進(jìn)行的options.comment
,因此魁兼,這里其實(shí)起一個(gè)回調(diào)作用漠嵌,parse
函數(shù)內(nèi)就可以通過(guò)回調(diào)獲取當(dāng)前模板解析得到的注釋節(jié)點(diǎn)的內(nèi)容,從而可以進(jìn)行處理或保存)碉考,解析完成后會(huì)通過(guò)advance
函數(shù)將當(dāng)前template
的字符串進(jìn)行截取挺身,只保留還未進(jìn)行解析的內(nèi)容。其他節(jié)點(diǎn)解析處理與上述操作邏輯類似,均是根據(jù)節(jié)點(diǎn)特點(diǎn)進(jìn)行解析墙贱,然后通過(guò)
advance
函數(shù)去除已解析的內(nèi)容热芹,只保留未解析的模板字符串,繼續(xù)新一輪的解析惨撇。舉個(gè)栗子:比如對(duì)于如下模板:
template: `<h2 style="color:red">Hi, {{message}}</h2>`
其解析流程如下圖所示:
parseHTML
每次解析完成一個(gè)節(jié)點(diǎn)時(shí)伊脓,就會(huì)將結(jié)果回調(diào)給parse
函數(shù),parse
函數(shù)就可以根據(jù)這些結(jié)果進(jìn)行抽象語(yǔ)法樹(shù)(AST)的構(gòu)建魁衙,其實(shí)質(zhì)就是構(gòu)建一個(gè)javascript
對(duì)象报腔,比如,上述模板構(gòu)建得到的 AST 如下所示:
{
"type": 1,
"tag": "h2",
"attrsList": [],
"attrsMap": {
"style": "color:red"
},
"rawAttrsMap": {
"style": {
"name": "style",
"value": "color:red",
"start": 4,
"end": 21
}
},
"children": [
{
"type": 2,
"expression": "\"Hi, \"+_s(message)",
"tokens": [
"Hi, ",
{
"@binding": "message"
}
],
"text": "Hi, {{message}}",
"start": 22,
"end": 37
}
],
"start": 0,
"end": 42,
"plain": false,
"staticStyle": "{\"color\":\"red\"}"
}
到這里剖淀,我們就大概了解了模板字符串template
解析成抽象語(yǔ)法樹(shù)(AST)的整個(gè)過(guò)程纯蛾。
-
優(yōu)化 AST:當(dāng)完成 AST 的構(gòu)建后,就可以對(duì) AST 進(jìn)行一些優(yōu)化纵隔。
注:Vue 之所以有 優(yōu)化 AST 這個(gè)過(guò)程翻诉,主要是因?yàn)?Vue 的特性之一是 數(shù)據(jù)驅(qū)動(dòng),并且數(shù)據(jù)具備響應(yīng)式功能捌刮,因此碰煌,當(dāng)更改數(shù)據(jù)的時(shí)候,模板會(huì)重新進(jìn)行渲染绅作,顯示最新的數(shù)據(jù)芦圾。但是,模板中并不是所有的節(jié)點(diǎn)都需要進(jìn)行重新渲染棚蓄,對(duì)于不包含響應(yīng)式數(shù)據(jù)的節(jié)點(diǎn)堕扶,從始至終只需一次渲染即可。
我們進(jìn)入
optimize
函數(shù)梭依,查看其源碼:// src/compiler/optimizer.js /** * Goal of the optimizer: walk the generated template AST tree * and detect sub-trees that are purely static, i.e. parts of * the DOM that never needs to change. * * Once we detect these sub-trees, we can: * * 1. Hoist them into constants, so that we no longer need to * create fresh nodes for them on each re-render; * 2. Completely skip them in the patching process. */ export function optimize(root: ?ASTElement, options: CompilerOptions) { ... // first pass: mark all non-static nodes. markStatic(root) // second pass: mark static roots. markStaticRoots(root, false) }
optimize
主要就是做了兩件事:-
markStatic
:標(biāo)記靜態(tài)節(jié)點(diǎn)稍算。其源碼如下:
// src/compiler/optimizer.js function markStatic(node: ASTNode) { node.static = isStatic(node) if (node.type === 1) { ... for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) if (!child.static) { node.static = false } } if (node.ifConditions) { for (let i = 1, l = node.ifConditions.length; i < l; i++) { const block = node.ifConditions[i].block markStatic(block) if (!block.static) { node.static = false } } } } }
注:
parse
函數(shù)解析生成的抽象語(yǔ)法樹(shù) AST 中,其元素節(jié)點(diǎn)總有三種類型役拴,如下所示:type
類型 1
普通元素 2
表達(dá)式(expression) 3
文本(text) 從源碼中可以看到糊探,
markStatic
函數(shù)會(huì)對(duì)當(dāng)前節(jié)點(diǎn)科平,以及當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)和v-if
的子節(jié)點(diǎn)進(jìn)行靜態(tài)節(jié)點(diǎn)標(biāo)記瞪慧,靜態(tài)節(jié)點(diǎn)的判定標(biāo)準(zhǔn)由函數(shù)isStatic
判定:// src/compiler/optimizer.js function isStatic(node: ASTNode): boolean { if (node.type === 2) { // expression return false } if (node.type === 3) { // text return true } return !!(node.pre || ( !node.hasBindings && // no dynamic bindings !node.if && !node.for && // not v-if or v-for or v-else !isBuiltInTag(node.tag) && // not a built-in isPlatformReservedTag(node.tag) && // not a component !isDirectChildOfTemplateFor(node) && Object.keys(node).every(isStaticKey) )) }
可以看到氨菇,靜態(tài)節(jié)點(diǎn)的判定標(biāo)準(zhǔn)為:純文本類型 或者 沒(méi)有動(dòng)態(tài)綁定且沒(méi)有
v-if
且沒(méi)有v-for
指令且不是內(nèi)置slot
/component
且是平臺(tái)保留標(biāo)簽且不是帶有v-for
指令的template
標(biāo)簽的直接子節(jié)點(diǎn)且節(jié)點(diǎn)的所有屬性都是靜態(tài)key
。
當(dāng)滿足靜態(tài)節(jié)點(diǎn)的判定時(shí)豌研,就會(huì)為該節(jié)點(diǎn)打上static=true
屬性鹃共,作為標(biāo)記及汉。-
markStaticRoots
:標(biāo)記靜態(tài)根節(jié)點(diǎn)。其源碼如下:
// src/compiler/optimizer.js function markStaticRoots(node: ASTNode, isInFor: boolean) { if (node.type === 1) { ... // For a node to qualify as a static root, it should have children that // are not just static text. Otherwise the cost of hoisting out will // outweigh the benefits and it's better off to just always render it fresh. if (node.static && node.children.length && !( node.children.length === 1 && node.children[0].type === 3 )) { node.staticRoot = true return } else { node.staticRoot = false } if (node.children) { for (let i = 0, l = node.children.length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !!node.for) } } if (node.ifConditions) { for (let i = 1, l = node.ifConditions.length; i < l; i++) { markStaticRoots(node.ifConditions[i].block, isInFor) } } } }
根節(jié)點(diǎn)即為普通元素節(jié)點(diǎn),從源碼中可以看到翁狐,
markStaticRoots
函數(shù)會(huì)對(duì)當(dāng)前節(jié)點(diǎn)露懒,以及當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)和帶有v-if
的子節(jié)點(diǎn)進(jìn)行靜態(tài)根節(jié)點(diǎn)標(biāo)記懈词。
靜態(tài)根節(jié)點(diǎn)的判定標(biāo)準(zhǔn)為:節(jié)點(diǎn)為靜態(tài)節(jié)點(diǎn)坎弯,且其有子節(jié)點(diǎn)抠忘,并且子節(jié)點(diǎn)不能只是一個(gè)文本節(jié)點(diǎn)。
當(dāng)滿足靜態(tài)根節(jié)點(diǎn)的判定時(shí)囚灼,就會(huì)為該節(jié)點(diǎn)打上staticRoot=true
屬性,作為標(biāo)記谭网。因此,Vue 對(duì)模板解析生成的 AST 的優(yōu)化就是對(duì) AST 元素節(jié)點(diǎn)進(jìn)行靜態(tài)節(jié)點(diǎn)和靜態(tài)根節(jié)點(diǎn)的標(biāo)記锥涕,以避免重新渲染靜態(tài)節(jié)點(diǎn)元素层坠。
-
-
生成代碼:當(dāng)對(duì) AST 進(jìn)行優(yōu)化后破花,編譯的最后一步就是將優(yōu)化過(guò)后的 AST 樹(shù)轉(zhuǎn)換成可執(zhí)行代碼座每。
我們進(jìn)入
generate
函數(shù),查看其源碼:// src/compiler/codegen/index.js 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 } }
generate
函數(shù)內(nèi)部主要就是調(diào)用了函數(shù)genElement
對(duì) AST 樹(shù)進(jìn)行解析并生成對(duì)應(yīng)可執(zhí)行代碼葱椭,最后返回一個(gè)對(duì)象,該對(duì)象含有兩個(gè)函數(shù)render
和state.staticRenderFns
窃祝,其中大磺,render
函數(shù)會(huì)封裝一下genElement
函數(shù)生成的代碼杠愧。我們下面對(duì)
genElement
函數(shù)進(jìn)行分析流济,看下其代碼生成邏輯:// src/compiler/codegen/index.js export function genElement(el: ASTElement, state: CodegenState): string { ... 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) ... } ... return code } }
可以看到雕憔,
genElement
函數(shù)內(nèi)部會(huì)依據(jù) AST 樹(shù)的節(jié)點(diǎn)類別分別調(diào)用不同的函數(shù)生成對(duì)應(yīng)的代碼斤彼,比如:- 對(duì)于靜態(tài)根節(jié)點(diǎn)琉苇,使用
genStatic
函數(shù)進(jìn)行代碼生成。 - 對(duì)于帶有
v-once
指令的節(jié)點(diǎn)穷蛹,使用genOnce
函數(shù)進(jìn)行代碼生成俩莽。 - 對(duì)于帶有
v-for
指令的節(jié)點(diǎn),使用genFor
函數(shù)進(jìn)行代碼生成。 - 對(duì)于帶有
v-if
指令的節(jié)點(diǎn)坯辩,使用genIf
函數(shù)進(jìn)行代碼生成漆魔。 - 對(duì)于不帶
slot
指令的template
標(biāo)簽,會(huì)使用genChildren
函數(shù)遍歷其子節(jié)點(diǎn)并進(jìn)行代碼生成阿纤。 - 對(duì)于
slot
標(biāo)簽胰锌,使用genSlot
函數(shù)進(jìn)行代碼生成资昧。 - 對(duì)于組件或元素格带,使用
genComponent
等函數(shù)進(jìn)行代碼生成。
...
我們這里就隨便找一個(gè)函數(shù)簡(jiǎn)單分析下代碼生成的具體邏輯嘶卧,比如:
genStatic
侦铜,其源碼如下所示:// src/compiler/codegen/index.js function genStatic(el: ASTElement, state: CodegenState): string { ... state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) ... return `_m(${ state.staticRenderFns.length - 1 }${ el.staticInFor ? ',true' : '' })` }
其實(shí)最終就是使用字符串拼接將對(duì)應(yīng)元素的內(nèi)容轉(zhuǎn)換為字符串代碼。
舉個(gè)栗子:比如我們構(gòu)造一個(gè)靜態(tài)根節(jié)點(diǎn)的模板贡未,如下所示:
template: ` <div> <h2 style="color:red">Hi</h2> </div> `
最后俊卤,經(jīng)過(guò)
genStatic
后,最終生成的代碼為:_m(0)
狠怨。
所以上述模板最終經(jīng)過(guò)generate
函數(shù)后,生成的代碼如下:{ "render": "with(this){return _m(0)}", "staticRenderFns": [ "with(this){return _c('div',[_c('h2',{staticStyle:{\"color\":\"red\"}},[_v(\"Hi\")])])}" ] }
注:
_m
函數(shù)其實(shí)是renderStatic
函數(shù)茵汰,Vue 中還設(shè)置了_o
蹂午,_l
栏豺,_v
等函數(shù),如下所示:// src/core/instance/render-helpers/index.js export function installRenderHelpers (target: any) { target._o = markOnce target._n = toNumber target._s = toString target._l = renderList target._t = renderSlot target._q = looseEqual target._i = looseIndexOf target._m = renderStatic target._f = resolveFilter target._k = checkKeyCodes target._b = bindObjectProps target._v = createTextVNode target._e = createEmptyVNode target._u = resolveScopedSlots target._g = bindObjectListeners target._d = bindDynamicKeys target._p = prependModifier }
其余的代碼生成函數(shù)就不進(jìn)行分析了豆胸,大概的原理就是根據(jù)不同節(jié)點(diǎn)特征奥洼,在 AST 樹(shù)中獲取需要的數(shù)據(jù),拼接成可執(zhí)行代碼的字符串代碼晚胡。
- 對(duì)于靜態(tài)根節(jié)點(diǎn)琉苇,使用
到這里灵奖,模板編譯的一個(gè)大概完整過(guò)程便完成了。