在 Vue 里,模板編譯也是非常重要的一部分,里面也非常復雜,這次探究不會深入探究每一個細節,而是走一個全景概要,來吧,大家和我一起去一探究竟。
初體驗
我們看了 Vue 的初始化函數就會知道,在最后一步,它進行了 vm.$mount(el)
的操作,而這個 $mount 在兩個地方定義過,分別是在 entry-runtime-with-compiler.js
(簡稱:eMount) 和 runtime/index.js(簡稱:rMount) 這兩個文件里,那么這兩個有什么區別呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
// 這個 $mount 其實就是 rMount
Vue.prototype.$mount =
function
(
el?: string | Element,
hydrating?: boolean
): Component {
const options =
this
.$options
if
(!options.render) {
...
if
(template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
options.render = render
options.staticRenderFns = staticRenderFns
}
...
}
return
mount.call(
this
, el, hydrating)
}
|
其實 eMount 最后還是去調用的 rMount,只不過在 eMount 做了一定的操作,如果你提供了 render 函數,那么它會直接去調用 rMount,如果沒有,它就會去找你有沒有提供 template,如果你沒有提供 template,它就會用 el 去查詢 dom 生成 template,最后通過編譯返回了一個 render 函數,再去調用 eMount。
從上面可以看出,最重要的一部分就是 compileToFunctions 這個函數,它最后返回了 render 函數,關於這個函數,它有點復雜,我畫了一張圖來看一看它的關系,可能會有誤差,希望大俠們可以指出。
編譯三步走
看一下這個編譯的整體過程,我們其實可以發現,最核心的部分就是在這里傳進去的 baseCompile 做的工作:
- parse: 第一步,我們需要將 template 轉換成抽象語法樹(AST)。
- optimizer: 第二步,我們對這個抽象語法樹進行靜態節點的標記,這樣就可以優化渲染過程。
- generateCode: 第三步,根據 AST 生成一個 render 函數字符串。
好了,我們接下來就一個一個慢慢看。
解析器
在解析器中有一個非常重要的概念 AST,大家可以去自行了解一下。
在 Vue 中,ASTNode 分幾種不同類型,關於 ASTNode 的定義在 flow/compile.js 里面,請看下圖:
我們用一個簡單的例子來說明一下:
1
2
3
4
|
<div id=
"demo"
>
<h1>Latest Vue.js Commits</h1>
<p>{{1 + 1}}</p>
</div>
|
我們想一想這段代碼會生成什么樣的 AST 呢?
我們這個例子最后生成的大概就是這么一棵樹,那么 Vue 是如何去做這樣一些解析的呢?我們繼續看。
在 parse 函數中,我們先是定義了非常多的全局屬性以及函數,然后調用了 parseHTML 這么一個函數,這也是 parse 最核心的函數,這個函數會不斷的解析模板,填充 root,最后把 root(AST) 返回回去。
parseHTML
在這個函數中,最重要的是 while 循環中的代碼,而在解析過程中發揮重要作用的有這么幾個正則表達式。
1
2
3
4
5
6
7
8
9
|
const attribute = /^\s*([^\s
"'<>\/=]+)(?:\s*(=)\s*(?:"
([^
"]*)"
+|'([^
']*)'
+|([^\s"
'=<>`]+)))?/
const ncname = '
[a-zA-Z_][\\w\\-\\.]*'
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 = /^<!\[/
|
Vue 通過上面幾個正則表達式去匹配開始結束標簽、標簽名、屬性等等。
關於 while 的詳細注解我放在我倉庫里了,有興趣的可以去看看。
在 while 里,其實就是不斷的去用 html.indexOf('<')
去匹配,然后根據返回的索引的不同去做不同的解析處理:
- __等於 0:__這就代表這是注釋、條件注釋、doctype、開始標簽、結束標簽中的某一種
- __大於等於 0:__這就說明是文本、表達式
- __小於 0:__表示 html 標簽解析完了,可能會剩下一些文本、表達式
parse 函數就是不斷的重復這個工作,然后將 template 轉換成 AST,在解析過程中,其實對於標簽與標簽之間的空格,Vue 也做了優化處理,有些元素之間的空格是沒用的。
compile 其實要說要說非常多的篇幅,但是這里只能簡單的理一下思路,具體代碼還需要各位下去深扣。
優化器
從代碼中的注釋我們可以看出,優化器的目的就是去找出 AST 中純靜態的子樹:
把純靜態子樹提升為常量,每次重新渲染的時候就不需要創建新的節點了
在 patch 的時候就可以跳過它們
optimize 的代碼量沒有 parse 那么多,我們來看看:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
export
function
optimize (root: ?ASTElement, options: CompilerOptions) {
// 判斷 root 是否存在
if
(!root)
return
// 判斷是否是靜態的屬性
// 'type,tag,attrsList,attrsMap,plain,parent,children,attrs'
isStaticKey = genStaticKeysCached(options.staticKeys ||
''
)
// 判斷是否是平台保留的標簽,html 或者 svg 的
isPlatformReservedTag = options.isReservedTag || no
// 第一遍遍歷: 給所有靜態節點打上是否是靜態節點的標記
markStatic(root)
// 第二遍遍歷:標記所有靜態根節點
markStaticRoots(root,
false
)
}
|
下面兩段代碼我都剪切了一部分,因為有點多,這里就不貼太多代碼了,詳情請參考我的倉庫。
第一遍遍歷
1
2
3
4
5
6
|
function
markStatic (node: ASTNode) {
node.static = isStatic(node)
if
(node.type === 1) {
...
}
}
|
其實 markStatic 就是一個遞歸的過程,不斷地去檢查 AST 上的節點,然后打上標記。
剛剛我們說過,AST 節點分三種,在 isStatic 這個函數中我們對不同類型的節點做了判斷:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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)
))
}
|
可以看到 Vue 對下面幾種情況做了處理:
當這個節點的 type 為 2,也就是表達式節點的時候,很明顯它不是一個靜態節點,所以返回 false
當 type 為 3 的時候,也就是文本節點,那它就是一個靜態節點,返回 true
如果你在元素節點中使用了 v-pre 或者使用了 <pre>
標簽,就會在這個節點上加上 pre 為 true,那么這就是個靜態節點
如果它是靜態節點,那么需要它不能有動態的綁定、不能有 v-if、v-for、v-else 這些指令,不能是 slot 或者 component 標簽、不是我們自定義的標簽、沒有父節點或者元素的父節點不能是帶 v-for 的 template、 這個節點的屬性都在 type,tag,attrsList,attrsMap,plain,parent,children,attrs 里面,滿足這些條件,就認為它是靜態的節點。
接下來,就開始對 AST 進行遞歸操作,標記靜態的節點,至於里面做了哪些操作,可以到上面那個倉庫里去看,這里就不展開了。
第二遍遍歷
第二遍遍歷的過程是標記靜態根節點,那么我們對靜態根節點的定義是什么,首先根節點的意思就是他不能是葉子節點,起碼要有子節點,並且它是靜態的。在這里 Vue 做了一個說明,如果一個靜態節點它只擁有一個子節點並且這個子節點是文本節點,那么就不做靜態處理,它的成本大於收益,不如直接渲染。
同樣的,我們在函數中不斷的遞歸進行標記,最后在所有靜態根節點上加上 staticRoot 的標記,關於這段代碼也可以去上面的倉庫看一看。
代碼生成器
在這個函數中,我們將 AST 轉換成為 render 函數字符串,代碼量還是挺多的,我們可以來看一看。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
export
function
generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 這就是編譯的一些參數
const state =
new
CodegenState(options)
// 生成 render 字符串
const code = ast ? genElement(ast, state) :
'_c("div")'
return
{
render: `
with
(
this
){
return
$[code]}`,
staticRenderFns: state.staticRenderFns
}
}
|
可以看到在最后代碼生成階段,最重要的函數就是 genElement 這個函數,針對不同的指令、屬性,我們會選擇不同的代碼生成函數。最后我們按照 AST 生成拼接成一個字符串,如下所示:
1
|
with
(
this
){
return
_c(
'div'
,{attrs:{
"id"
:
"demo"
}},[(1>0)?_c(
'h1'
,[_v(
"Latest Vue.js Commits"
)]):_e(),...}
|
在 render 這個函數字符串中,我們會看到一些函數,那么這些函數是在什么地方定義的呢?我們可以在 core/instance/index.js 這個文件中找到這些函數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// v-once
target._o = markOnce
// 轉換
target._n = toNumber
target._s = toString
// v-for
target._l = renderList
// slot
target._t = renderSlot
// 是否相等
target._q = looseEqual
// 檢測數組里是否有相等的值
target._i = looseIndexOf
// 渲染靜態樹
target._m = renderStatic
// 過濾器處理
target._f = resolveFilter
// 檢查關鍵字
target._k = checkKeyCodes
// v-bind
target._b = bindObjectProps
// 創建文本節點
target._v = createTextVNode
// 創建空節點
target._e = createEmptyVNode
// 處理 scopeslot
target._u = resolveScopedSlots
// 處理事件綁定
target._g = bindObjectListeners
// 創建 VNode 節點
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d,
false
)
|
在編譯結束后,我們根據不同的指令、屬性等等去選擇需要調用哪一個處理函數,最后拼接成一個函數字符串。
我們可以很清楚的看到,最后生成了一個 render 渲染字符串,那么我們要如何去使用它呢?其實在后面進行渲染的時候,我們進行了 new Function(render)
的操作,然后我們就能夠正常的使用 render 函數了。
總結
大流程走完之后,我相信大家會對編譯過程有一個比較清晰的認識,然后再去挖細節相信也會容易的多了,讀源碼,其實並不是一個為了讀而讀的過程,我們可以在源碼中學到很多我們可能在日常開發中沒有了解到的知識。
至於最后代碼生成器中的那一大段代碼,我還沒有把它注釋好,后面應該會將源碼注釋放到倉庫里,不過我也相信大家也能夠順利的去讀懂源碼。
還有一點要提的是在 render 函數中,Vue 使用了 with 函數,我們平時肯定沒見過,因為官方不推薦我們去使用 with,我抱着這樣的想法去找了找原因,最后我在知乎上找到了尤大大的回答,這是鏈接,大家可以去了解下。
以上所述是小編給大家介紹的Vue 中的compile,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網站的支持!