深入了解Vue3模板編譯原理
Vue 的編譯模塊包含 4 個(gè)目錄:
compiler-core compiler-dom // 瀏覽器 compiler-sfc // 單文件組件 compiler-ssr // 服務(wù)端渲染
其中 compiler-core 模塊是 Vue 編譯的核心模塊,并且是平臺(tái)無(wú)關(guān)的。而剩下的三個(gè)都是在 compiler-core 的基礎(chǔ)上針對(duì)不同的平臺(tái)作了適配處理。
Vue 的編譯分為三個(gè)階段,分別是:parse、transform、codegen。
其中 parse 階段將模板字符串轉(zhuǎn)化為語(yǔ)法抽象樹(shù) AST。transform 階段則是對(duì) AST 進(jìn)行了一些轉(zhuǎn)換處理。codegen 階段根據(jù) AST 生成對(duì)應(yīng)的 render 函數(shù)字符串。
Parse
Vue 在解析模板字符串時(shí),可分為兩種情況:以 < 開(kāi)頭的字符串和不以 < 開(kāi)頭的字符串。
不以 < 開(kāi)頭的字符串有兩種情況:它是文本節(jié)點(diǎn)或 {{ exp }} 插值表達(dá)式。
而以 < 開(kāi)頭的字符串又分為以下幾種情況:
- 元素開(kāi)始標(biāo)簽 <div>
- 元素結(jié)束標(biāo)簽 </div>
- 注釋節(jié)點(diǎn) <!-- 123 -->
- 文檔聲明 <!DOCTYPE html>
用偽代碼表示,大概過(guò)程如下:
e (s.length) {
if (startsWith(s, '{{')) {
// 如果以 '{{' 開(kāi)頭
node = parseInterpolation(context, mode)
} else if (s[0] === '<') {
// 以 < 標(biāo)簽開(kāi)頭
if (s[1] === '!') {
if (startsWith(s, '<!--')) {
// 注釋
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// 文檔聲明,當(dāng)成注釋處理
node = parseBogusComment(context)
}
} else if (s[1] === '/') {
// 結(jié)束標(biāo)簽
parseTag(context, TagType.End, parent)
} else if (/[a-z]/i.test(s[1])) {
// 開(kāi)始標(biāo)簽
node = parseElement(context, ancestors)
}
} else {
// 普通文本節(jié)點(diǎn)
node = parseText(context, mode)
}
}
在源碼中對(duì)應(yīng)的幾個(gè)函數(shù)分別是:
parseChildren(),主入口。-
parseInterpolation(),解析雙花插值表達(dá)式。 parseComment(),解析注釋。parseBogusComment(),解析文檔聲明。parseTag(),解析標(biāo)簽。parseElement(),解析元素節(jié)點(diǎn),它會(huì)在內(nèi)部執(zhí)行parseTag()。parseText(),解析普通文本。parseAttribute(),解析屬性。
每解析完一個(gè)標(biāo)簽、文本、注釋等節(jié)點(diǎn)時(shí),Vue 就會(huì)生成對(duì)應(yīng)的 AST 節(jié)點(diǎn),并且 會(huì)把已經(jīng)解析完的字符串給截?cái)?/strong> 。
對(duì)字符串進(jìn)行截?cái)嗍褂玫氖?advanceBy(context, numberOfCharacters) 函數(shù),context 是字符串的上下文對(duì)象,numberOfCharacters 是要截?cái)嗟淖址麛?shù)。
我們用一個(gè)簡(jiǎn)單的例子來(lái)模擬一下截?cái)嗖僮鳎?/p>
<div name="test"> <p></p> </div>
首先解析 <div ,然后執(zhí)行 advanceBy(context, 4) 進(jìn)行截?cái)嗖僮鳎▋?nèi)部執(zhí)行的是 s = s.slice(4) ),變成:
name="test"> <p></p> </div>
再解析屬性,并截?cái)?,變成?/p>
<p></p> </div>
同理,后面的截?cái)嗲闆r為:
></p> </div>
</div>
<!-- 所有字符串已經(jīng)解析完 -->
AST 節(jié)點(diǎn)
所有的 AST 節(jié)點(diǎn)定義都在 compiler-core/ast.ts 文件中,下面是一個(gè)元素節(jié)點(diǎn)的定義:
rt interface BaseElementNode extends Node {
type: NodeTypes.ELEMENT // 類型
ns: Namespace // 命名空間 默認(rèn)為 HTML,即 0
tag: string // 標(biāo)簽名
tagType: ElementTypes // 元素類型
isSelfClosing: boolean // 是否是自閉合標(biāo)簽 例如 <br/> <hr/>
props: Array<AttributeNode | DirectiveNode> // props 屬性,包含 HTML 屬性和指令
children: TemplateChildNode[] // 字節(jié)點(diǎn)
}
一些簡(jiǎn)單的要點(diǎn)已經(jīng)講完了,下面我們?cè)購(gòu)囊粋€(gè)比較復(fù)雜的例子來(lái)詳細(xì)講解一下 parse 的處理過(guò)程。
<div name="test">
<!-- 這是注釋 -->
<p>{{ test }}</p>
一個(gè)文本節(jié)點(diǎn)
<div>good job!</div>
</div>
上面的模板字符串假設(shè)為 s,第一個(gè)字符 s[0] 是 < 開(kāi)頭,那說(shuō)明它只能是剛才所說(shuō)的四種情況之一。 這時(shí)需要再看一下 s[1] 的字符是什么:
- 如果是
!,則調(diào)用字符串原生方法startsWith()看看是以'<!--'開(kāi)頭還是以'<!DOCTYPE'開(kāi)頭。雖然這兩者對(duì)應(yīng)的處理函數(shù)不一樣,但它們最終都是解析為注釋節(jié)點(diǎn)。 - 如果是
/,則按結(jié)束標(biāo)簽處理。 - 如果不是
/,則按開(kāi)始標(biāo)簽處理。
從我們的示例來(lái)看,這是一個(gè) <div> 開(kāi)始標(biāo)簽。
這里還有一點(diǎn)要提一下,Vue 會(huì)用一個(gè)棧 stack 來(lái)保存解析到的元素標(biāo)簽。當(dāng)它遇到開(kāi)始標(biāo)簽時(shí),會(huì)將這個(gè)標(biāo)簽推入棧,遇到結(jié)束標(biāo)簽時(shí),將剛才的標(biāo)簽彈出棧。它的作用是保存當(dāng)前已經(jīng)解析了,但還沒(méi)解析完的元素標(biāo)簽。這個(gè)棧還有另一個(gè)作用,在解析到某個(gè)字節(jié)點(diǎn)時(shí),通過(guò) stack[stack.length - 1] 可以獲取它的父元素。
從我們的示例來(lái)看,它的出入棧順序是這樣的:
1. [div] // div 入棧 2. [div, p] // p 入棧 3. [div] // p 出棧 4. [div, div] // div 入棧 5. [div] // div 出棧 6. [] // 最后一個(gè) div 出棧,模板字符串已解析完,這時(shí)棧為空
接著上文繼續(xù)分析我們的示例,這時(shí)已經(jīng)知道是 div 標(biāo)簽了,接下來(lái)會(huì)把已經(jīng)解析完的 <div 字符串截?cái)啵缓蠼馕鏊膶傩浴?/p>
Vue 的屬性有兩種情況:
- HTML 普通屬性
- Vue 指令
根據(jù)屬性的不同生成的節(jié)點(diǎn)不同,HTML 普通屬性節(jié)點(diǎn) type 為 6,Vue 指令節(jié)點(diǎn) type 為 7。
所有的節(jié)點(diǎn)類型值如下:
ROOT, // 根節(jié)點(diǎn) 0
ELEMENT, // 元素節(jié)點(diǎn) 1
TEXT, // 文本節(jié)點(diǎn) 2
COMMENT, // 注釋節(jié)點(diǎn) 3
SIMPLE_EXPRESSION, // 表達(dá)式 4
INTERPOLATION, // 雙花插值 {{ }} 5
ATTRIBUTE, // 屬性 6
DIRECTIVE, // 指令 7
屬性解析完后, div 開(kāi)始標(biāo)簽也就解析完了, <div name="test"> 這一行字符串已經(jīng)被截?cái)唷,F(xiàn)在剩下的字符串如下:
<!-- 這是注釋 -->
<p>{{ test }}</p>
一個(gè)文本節(jié)點(diǎn)
<div>good job!</div>
</div>
注釋文本和普通文本節(jié)點(diǎn)解析規(guī)則都很簡(jiǎn)單,直接截?cái)?,生成?jié)點(diǎn)。注釋文本調(diào)用 parseComment() 函數(shù)處理,文本節(jié)點(diǎn)調(diào)用 parseText() 處理。
雙花插值的字符串處理邏輯稍微復(fù)雜點(diǎn),例如示例中的 {{ test }} :
- 先將雙花括號(hào)中的內(nèi)容提取出來(lái),即
test,再對(duì)它執(zhí)行trim(),去除空格。 - 然后會(huì)生成兩個(gè)節(jié)點(diǎn),一個(gè)節(jié)點(diǎn)是
INTERPOLATION,type 為 5,表示它是雙花插值。 - 第二個(gè)節(jié)點(diǎn)是它的內(nèi)容,即
test,它會(huì)生成一個(gè)SIMPLE_EXPRESSION節(jié)點(diǎn),type 為 4。
turn {
type: NodeTypes.INTERPOLATION, // 雙花插值類型
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false, // 非靜態(tài)節(jié)點(diǎn)
isConstant: false,
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
剩下的字符串解析邏輯和上文的差不多,就不解釋了,最后這個(gè)示例解析出來(lái)的 AST 如下所示:

從 AST 上,我們還能看到某些節(jié)點(diǎn)上有一些別的屬性:
- ns,命名空間,一般為 HTML,值為 0。
- loc,它是一個(gè)位置信息,表明這個(gè)節(jié)點(diǎn)在源 HTML 字符串中的位置,包含行,列,偏移量等信息。
- {{ test }} 解析出來(lái)的節(jié)點(diǎn)會(huì)有一個(gè) isStatic 屬性,值為 false,表示這是一個(gè)動(dòng)態(tài)節(jié)點(diǎn)。如果是靜態(tài)節(jié)點(diǎn),則只會(huì)生成一次,并且在后面的階段一直復(fù)用同一個(gè),不用進(jìn)行 diff 比較
另外還有一個(gè) tagType 屬性,它有 4 個(gè)值:
t const enum ElementTypes {
ELEMENT, // 0 元素節(jié)點(diǎn)
COMPONENT, // 1 組件
SLOT, // 2 插槽
TEMPLATE // 3 模板
}
主要用于區(qū)分上述四種類型節(jié)點(diǎn)。
Transform
在 transform 階段,Vue 會(huì)對(duì) AST 進(jìn)行一些轉(zhuǎn)換操作,主要是根據(jù)不同的 AST 節(jié)點(diǎn)添加不同的選項(xiàng)參數(shù),這些參數(shù)在 codegen 階段會(huì)用到。下面列舉一些比較重要的選項(xiàng):
cacheHandlers
如果 cacheHandlers 的值為 true,則表示開(kāi)啟事件函數(shù)緩存。例如 @click="foo" 默認(rèn)編譯為 { onClick: foo } ,如果開(kāi)啟了這個(gè)選項(xiàng),則編譯為
{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }
hoistStatic
hoistStatic 是一個(gè)標(biāo)識(shí)符,表示要不要開(kāi)啟靜態(tài)節(jié)點(diǎn)提升。如果值為 true,靜態(tài)節(jié)點(diǎn)將被提升到 render() 函數(shù)外面生成,并被命名為 _hoisted_x 變量。
例如 一個(gè)文本節(jié)點(diǎn) 生成的代碼為 const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一個(gè)文本節(jié)點(diǎn) ") 。
下面兩張圖,前者是 hoistStatic = false ,后面是 hoistStatic = true 。大家可以在 網(wǎng)站 上自己試一下。


prefixIdentifiers
這個(gè)參數(shù)的作用是用于代碼生成。例如 {{ foo }} 在 module 模式下生成的代碼為 _ctx.foo ,而在 function 模式下是 with (this) { ... } 。因?yàn)樵?module 模式下,默認(rèn)為嚴(yán)格模式,不能使用 with 語(yǔ)句。
PatchFlags
transform 在對(duì) AST 節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換時(shí),會(huì)打上 patchflag 參數(shù),這個(gè)參數(shù)主要用于 diff 比較過(guò)程。當(dāng) DOM 節(jié)點(diǎn)有這個(gè)標(biāo)志并且大于 0,就代表要更新,沒(méi)有就跳過(guò)。
我們來(lái)看一下 patchflag 的取值范圍:
enum PatchFlags {
// 動(dòng)態(tài)文本節(jié)點(diǎn)
TEXT = 1,
// 動(dòng)態(tài) class
CLASS = 1 << 1, // 2
// 動(dòng)態(tài) style
STYLE = 1 << 2, // 4
// 動(dòng)態(tài)屬性,但不包含類名和樣式
// 如果是組件,則可以包含類名和樣式
PROPS = 1 << 3, // 8
// 具有動(dòng)態(tài) key 屬性,當(dāng) key 改變時(shí),需要進(jìn)行完整的 diff 比較。
FULL_PROPS = 1 << 4, // 16
// 帶有監(jiān)聽(tīng)事件的節(jié)點(diǎn)
HYDRATE_EVENTS = 1 << 5, // 32
// 一個(gè)不會(huì)改變子節(jié)點(diǎn)順序的 fragment
STABLE_FRAGMENT = 1 << 6, // 64
// 帶有 key 屬性的 fragment 或部分子字節(jié)有 key
KEYED_FRAGMENT = 1 << 7, // 128
// 子節(jié)點(diǎn)沒(méi)有 key 的 fragment
UNKEYED_FRAGMENT = 1 << 8, // 256
// 一個(gè)節(jié)點(diǎn)只會(huì)進(jìn)行非 props 比較
NEED_PATCH = 1 << 9, // 512
// 動(dòng)態(tài) slot
DYNAMIC_SLOTS = 1 << 10, // 1024
// 靜態(tài)節(jié)點(diǎn)
HOISTED = -1,
// 指示在 diff 過(guò)程應(yīng)該要退出優(yōu)化模式
BAIL = -2
}
從上述代碼可以看出 patchflag 使用一個(gè) 11 位的位圖來(lái)表示不同的值,每個(gè)值都有不同的含義。Vue 在 diff 過(guò)程會(huì)根據(jù)不同的 patchflag 使用不同的 patch 方法。
下圖是經(jīng)過(guò) transform 后的 AST:

可以看到 codegenNode、helpers 和 hoists 已經(jīng)被填充上了相應(yīng)的值。codegenNode 是生成代碼要用到的數(shù)據(jù),hoists 存儲(chǔ)的是靜態(tài)節(jié)點(diǎn),helpers 存儲(chǔ)的是創(chuàng)建 VNode 的函數(shù)名稱(其實(shí)是 Symbol)。
在正式開(kāi)始 transform 前,需要?jiǎng)?chuàng)建一個(gè) transformContext,即 transform 上下文。和這三個(gè)屬性有關(guān)的數(shù)據(jù)和方法如下:
helpers: new Set(),
hoists: [],
// methods
helper(name) {
context.helpers.add(name)
return name
},
helperString(name) {
return `_${helperNameMap[context.helper(name)]}`
},
hoist(exp) {
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc,
true
)
identifier.hoisted = exp
return identifier
},
我們來(lái)看一下具體的 transform 過(guò)程是怎樣的,用 <p>{{ test }}</p> 來(lái)做示例。
這個(gè)節(jié)點(diǎn)對(duì)應(yīng)的是 transformElement() 轉(zhuǎn)換函數(shù),由于 p 沒(méi)有綁定動(dòng)態(tài)屬性,沒(méi)有綁定指令,所以重點(diǎn)不在它,而是在 {{ test }} 上。 {{ test }} 是一個(gè)雙花插值表達(dá)式,所以將它的 patchFlag 設(shè)為 1(動(dòng)態(tài)文本節(jié)點(diǎn)),對(duì)應(yīng)的執(zhí)行代碼是 patchFlag |= 1 。然后再執(zhí)行 createVNodeCall() 函數(shù),它的返回值就是這個(gè)節(jié)點(diǎn)的 codegenNode 值。
node.codegenNode = createVNodeCall( context, vnodeTag, vnodeProps, vnodeChildren, vnodePatchFlag, vnodeDynamicProps, vnodeDirectives, !!shouldUseBlock, false /* disableTracking */, node.loc )
createVNodeCall() 根據(jù)這個(gè)節(jié)點(diǎn)添加了一個(gè) createVNode Symbol 符號(hào),它放在 helpers 里。其實(shí)就是要在代碼生成階段引入的幫助函數(shù)。
// createVNodeCall() 內(nèi)部執(zhí)行過(guò)程,已刪除多余的代碼
context.helper(CREATE_VNODE)
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
loc
}
hoists
一個(gè)節(jié)點(diǎn)是否添加到 hoists 中,主要看它是不是靜態(tài)節(jié)點(diǎn),并且需要將 hoistStatic 設(shè)為 true。
<div name="test"> // 屬性靜態(tài)節(jié)點(diǎn)
<!-- 這是注釋 -->
<p>{{ test }}</p>
一個(gè)文本節(jié)點(diǎn) // 靜態(tài)節(jié)點(diǎn)
<div>good job!</div> // 靜態(tài)節(jié)點(diǎn)
</div>
可以看到,上面有三個(gè)靜態(tài)節(jié)點(diǎn),所以 hoists 數(shù)組有 3 個(gè)值。并且無(wú)論靜態(tài)節(jié)點(diǎn)嵌套有多深,都會(huì)被提升到 hoists 中。
type 變化

從上圖可以看到,最外層的 div 的 type 原來(lái)為 1,經(jīng)過(guò) transform 生成的 codegenNode 中的 type 變成了 13。 這個(gè) 13 是代碼生成對(duì)應(yīng)的類型 VNODE_CALL 。另外還有:
// codegen VNODE_CALL, // 13 JS_CALL_EXPRESSION, // 14 JS_OBJECT_EXPRESSION, // 15 JS_PROPERTY, // 16 JS_ARRAY_EXPRESSION, // 17 JS_FUNCTION_EXPRESSION, // 18 JS_CONDITIONAL_EXPRESSION, // 19 JS_CACHE_EXPRESSION, // 20
剛才提到的例子 {{ test }} ,它的 codegenNode 就是通過(guò)調(diào)用 createVNodeCall() 生成的:
{
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
loc
}
可以從上述代碼看到,type 被設(shè)置為 NodeTypes.VNODE_CALL,即 13。
每個(gè)不同的節(jié)點(diǎn)都由不同的 transform 函數(shù)來(lái)處理,由于篇幅有限,具體代碼請(qǐng)自行查閱。
Codegen
代碼生成階段最后生成了一個(gè)字符串,我們把字符串的雙引號(hào)去掉,看一下具體的內(nèi)容是什么:
nst _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue
const _hoisted_1 = { name: "test" }
const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一個(gè)文本節(jié)點(diǎn) ")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock("div", _hoisted_1, [
_createCommentVNode(" 這是注釋 "),
_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */),
_hoisted_2,
_hoisted_3
]))
}
}
代碼生成模式
可以看到上述代碼最后返回一個(gè) render() 函數(shù),作用是生成對(duì)應(yīng)的 VNode。
其實(shí)代碼生成有兩種模式:module 和 function,由標(biāo)識(shí)符 prefixIdentifiers 決定使用哪種模式。
function 模式的特點(diǎn)是:使用 const { helpers... } = Vue 的方式來(lái)引入幫助函數(shù),也就是是 createVode() createCommentVNode() 這些函數(shù)。向外導(dǎo)出使用 return 返回整個(gè) render() 函數(shù)。
module 模式的特點(diǎn)是:使用 es6 模塊來(lái)導(dǎo)入導(dǎo)出函數(shù),也就是使用 import 和 export。
靜態(tài)節(jié)點(diǎn)
另外還有三個(gè)變量是用 _hoisted_ 命名的,后面跟著數(shù)字,代表這是第幾個(gè)靜態(tài)變量。 再看一下 parse 階段的 HTML 模板字符串:
<div name="test">
<!-- 這是注釋 -->
<p>{{ test }}</p>
一個(gè)文本節(jié)點(diǎn)
<div>good job!</div>
</div>
這個(gè)示例只有一個(gè)動(dòng)態(tài)節(jié)點(diǎn),即 {{ test }} ,剩下的全是靜態(tài)節(jié)點(diǎn)。從生成的代碼中也可以看出,生成的節(jié)點(diǎn)和模板中的代碼是一一對(duì)應(yīng)的。靜態(tài)節(jié)點(diǎn)的作用就是只生成一次,以后直接復(fù)用。
細(xì)心的網(wǎng)友可能發(fā)現(xiàn)了 _hoisted_2 和 _hoisted_3 變量中都有一個(gè) /*#__PURE__*/ 注釋。
這個(gè)注釋的作用是表示這個(gè)函數(shù)是純函數(shù),沒(méi)有副作用,主要用于 tree-shaking。壓縮工具在打包時(shí)會(huì)將未被使用的代碼直接刪除(shaking 搖掉)。
再來(lái)看一下生成動(dòng)態(tài)節(jié)點(diǎn) {{ test }} 的代碼: _createVNode("p", null, _toDisplayString(test), 1 /* TEXT */) 。
其中 _toDisplayString(test) 的內(nèi)部實(shí)現(xiàn)是:
n val == null ? '' : isObject(val) ? JSON.stringify(val, replacer, 2) : String(val)
代碼很簡(jiǎn)單,就是轉(zhuǎn)成字符串輸出。
而 _createVNode("p", null, _toDisplayString(test), 1 /* TEXT */) 最后一個(gè)參數(shù) 1 就是 transform 添加的 patchflag 了。
幫助函數(shù) helpers
在 transform、codegen 這兩個(gè)階段,我們都能看到 helpers 的影子,到底 helpers 是干什么用的?
// Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime!
// Using `any` here because TS doesn't allow symbols as index type.
export const helperNameMap: any = {
[FRAGMENT]: `Fragment`,
[TELEPORT]: `Teleport`,
[SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`,
[BASE_TRANSITION]: `BaseTransition`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`,
[CREATE_COMMENT]: `createCommentVNode`,
[CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[WITH_DIRECTIVES]: `withDirectives`,
[RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`,
[CREATE_SLOTS]: `createSlots`,
[TO_DISPLAY_STRING]: `toDisplayString`,
[MERGE_PROPS]: `mergeProps`,
[TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`,
[CAPITALIZE]: `capitalize`,
[SET_BLOCK_TRACKING]: `setBlockTracking`,
[PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`,
[WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`
}
export function registerRuntimeHelpers(helpers: any) {
Object.getOwnPropertySymbols(helpers).forEach(s => {
helperNameMap[s] = helpers[s]
})
}
其實(shí)幫助函數(shù)就是在代碼生成時(shí)從 Vue 引入的一些函數(shù),以便讓程序正常執(zhí)行,從上面生成的代碼中就可以看出來(lái)。而 helperNameMap 是默認(rèn)的映射表名稱,這些名稱就是要從 Vue 引入的函數(shù)名稱。
另外,我們還能看到一個(gè)注冊(cè)函數(shù) registerRuntimeHelpers(helpers: any() ,它是干什么用的呢?
我們知道編譯模塊 compiler-core 是平臺(tái)無(wú)關(guān)的,而 compiler-dom 是瀏覽器相關(guān)的編譯模塊。為了能在瀏覽器正常運(yùn)行 Vue 程序,就得把瀏覽器相關(guān)的 Vue 數(shù)據(jù)和函數(shù)導(dǎo)入進(jìn)來(lái)。 registerRuntimeHelpers(helpers: any() 正是用來(lái)做這件事的,從 compiler-dom 的 runtimeHelpers.ts 文件就能看出來(lái):
registerRuntimeHelpers({
[V_MODEL_RADIO]: `vModelRadio`,
[V_MODEL_CHECKBOX]: `vModelCheckbox`,
[V_MODEL_TEXT]: `vModelText`,
[V_MODEL_SELECT]: `vModelSelect`,
[V_MODEL_DYNAMIC]: `vModelDynamic`,
[V_ON_WITH_MODIFIERS]: `withModifiers`,
[V_ON_WITH_KEYS]: `withKeys`,
[V_SHOW]: `vShow`,
[TRANSITION]: `Transition`,
[TRANSITION_GROUP]: `TransitionGroup`
})
它運(yùn)行 registerRuntimeHelpers(helpers: any() ,往映射表注入了瀏覽器相關(guān)的部分函數(shù)。
helpers 是怎么使用的呢?
在 parse 階段,解析到不同節(jié)點(diǎn)時(shí)會(huì)生成對(duì)應(yīng)的 type。
在 transform 階段,會(huì)生成一個(gè) helpers,它是一個(gè) set 數(shù)據(jù)結(jié)構(gòu)。每當(dāng)它轉(zhuǎn)換 AST 時(shí),都會(huì)根據(jù) AST 節(jié)點(diǎn)的 type 添加不同的 helper 函數(shù)。
例如,假設(shè)它現(xiàn)在正在轉(zhuǎn)換的是一個(gè)注釋節(jié)點(diǎn),它會(huì)執(zhí)行 context.helper(CREATE_COMMENT) ,內(nèi)部實(shí)現(xiàn)相當(dāng)于 helpers.add('createCommentVNode') 。然后在 codegen 階段,遍歷 helpers,將程序需要的函數(shù)從 Vue 里導(dǎo)入,代碼實(shí)現(xiàn)如下:
// 這是 module 模式
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
如何生成代碼?
從 codegen.ts 文件中,可以看到很多代碼生成函數(shù):
generate() // 代碼生成入口文件 genFunctionExpression() // 生成函數(shù)表達(dá)式 genNode() // 生成 Vnode 節(jié)點(diǎn) ...
生成代碼則是根據(jù)不同的 AST 節(jié)點(diǎn)調(diào)用不同的代碼生成函數(shù),最終將代碼字符串拼在一起,輸出一個(gè)完整的代碼字符串。
老規(guī)矩,還是看一個(gè)例子:
t _hoisted_1 = { name: "test" }
const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一個(gè)文本節(jié)點(diǎn) ")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
看一下這段代碼是怎么生成的,首先執(zhí)行 genHoists(ast.hoists, context) ,將 transform 生成的靜態(tài)節(jié)點(diǎn)數(shù)組 hoists 作為第一個(gè)參數(shù)。 genHoists() 內(nèi)部實(shí)現(xiàn):
hoists.forEach((exp, i) => {
if (exp) {
push(`const _hoisted_${i + 1} = `);
genNode(exp, context);
newline();
}
})
從上述代碼可以看到,遍歷 hoists 數(shù)組,調(diào)用 genNode(exp, context) 。 genNode() 根據(jù)不同的 type 執(zhí)行不同的函數(shù)。
st _hoisted_1 = { name: "test" }
這一行代碼中的 const _hoisted_1 = 由 genHoists() 生成, { name: "test" } 由 genObjectExpression() 生成。 同理,剩下的兩行代碼生成過(guò)程也是如此,只是最終調(diào)用的函數(shù)不同。
到此這篇關(guān)于深入了解Vue3模板編譯原理的文章就介紹到這了,更多相關(guān)Vue3模板編譯內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用vue.js實(shí)現(xiàn)checkbox的全選和多個(gè)的刪除功能
這篇文章主要介紹了使用vue.js實(shí)現(xiàn)checkbox的全選和多個(gè)的刪除功能,需要的朋友可以參考下2017-02-02
Vue3中使用echarts的簡(jiǎn)單七個(gè)步驟(易懂,附緊急避坑)
近期在做一個(gè)vue3的項(xiàng)目,里面有個(gè)圖表需求,因公司之前使用第三方封裝的圖表缺少文檔,就去看了echars的官網(wǎng)文檔,引入原生echars來(lái)實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于Vue3中使用echarts的簡(jiǎn)單七個(gè)步驟,需要的朋友可以參考下2023-01-01
axios解決高并發(fā)的方法:axios.all()與axios.spread()的操作
這篇文章主要介紹了axios解決高并發(fā)的方法:axios.all()與axios.spread()的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11
vue中注冊(cè)組件的兩種方式詳解(全局注冊(cè)&& 局部注冊(cè))
vue 是一個(gè)完全支持組件化開(kāi)發(fā)的框架, 組件之間可以進(jìn)行相互的引用,這篇文章主要介紹了vue中注冊(cè)組件的兩種方式詳解(全局注冊(cè)&& 局部注冊(cè)),需要的朋友可以參考下2023-06-06
vue項(xiàng)目實(shí)戰(zhàn)之圓柱狀水波效果實(shí)現(xiàn)
最近工作中實(shí)現(xiàn)的一個(gè)效果不錯(cuò),分享給大家,下面這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目實(shí)戰(zhàn)之圓柱狀水波效果實(shí)現(xiàn)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12

