new Vue({
render: h => h(App)
})
這個(gè)大家都熟悉,調(diào)用 render 就會(huì)得到傳入的模板(.vue文件)對(duì)應(yīng)的虛擬 DOM,那么這個(gè) render 是哪來(lái)的呢?它是怎么把 .vue 文件轉(zhuǎn)成瀏覽器可識(shí)別的代碼的呢?
render 函數(shù)是怎么來(lái)的有兩種方式
- 第一種就是經(jīng)過(guò)模板編譯生成 render 函數(shù)
- 第二種是我們自己在組件里定義了 render 函數(shù),這種會(huì)跳過(guò)模板編譯的過(guò)程
本文將為大家分別介紹這兩種,以及詳細(xì)的編譯過(guò)程原理
認(rèn)識(shí)模板編譯
我們知道 <template></template> 這個(gè)是模板,不是真實(shí)的 HTML,瀏覽器是不認(rèn)識(shí)模板的,所以我們需要把它編譯成瀏覽器認(rèn)識(shí)的原生的 HTML
這一塊的主要流程就是
- 提取出模板中的原生 HTML 和非原生 HTML,比如綁定的屬性、事件、指令等等
- 經(jīng)過(guò)一些處理生成 render 函數(shù)
- render 函數(shù)再將模板內(nèi)容生成對(duì)應(yīng)的 vnode
- 再經(jīng)過(guò) patch 過(guò)程( Diff )得到要渲染到視圖中的 vnode
- 最后根據(jù) vnode 創(chuàng)建真實(shí)的 DOM 節(jié)點(diǎn),也就是原生 HTML 插入到視圖中,完成渲染
上面的 1、2、3 條就是模板編譯的過(guò)程了
那它是怎么編譯,最終生成 render 函數(shù)的呢?
模板編譯詳解——源碼
baseCompile()
這就是模板編譯的入口函數(shù),它接收兩個(gè)參數(shù)
-
template:就是要轉(zhuǎn)換的模板字符串 -
options:就是轉(zhuǎn)換時(shí)需要的參數(shù)
編譯的流程,主要有三步:
- 模板解析:通過(guò)正則等方式提取出
<template></template>模板里的標(biāo)簽元素、屬性、變量等信息,并解析成抽象語(yǔ)法樹(shù)AST - 優(yōu)化:遍歷
AST找出其中的靜態(tài)節(jié)點(diǎn)和靜態(tài)根節(jié)點(diǎn),并添加標(biāo)記 - 代碼生成:根據(jù)
AST生成渲染函數(shù)render
這三步分別對(duì)應(yīng)三個(gè)函數(shù),后面會(huì)一一下介紹,先看一下 baseCompile 源碼中是在哪里調(diào)用的
源碼地址:src/complier/index.js - 11行
export const createCompiler = createCompilerCreator(function baseCompile (
template: string, // 就是要轉(zhuǎn)換的模板字符串
options: CompilerOptions //就是轉(zhuǎn)換時(shí)需要的參數(shù)
): CompiledResult {
// 1. 進(jìn)行模板解析,并將結(jié)果保存為 AST
const ast = parse(template.trim(), options)
// 沒(méi)有禁用靜態(tài)優(yōu)化的話
if (options.optimize !== false) {
// 2. 就遍歷 AST,并找出靜態(tài)節(jié)點(diǎn)并標(biāo)記
optimize(ast, options)
}
// 3. 生成渲染函數(shù)
const code = generate(ast, options)
return {
ast,
render: code.render, // 返回渲染函數(shù) render
staticRenderFns: code.staticRenderFns
}
})
就這么幾行代碼,三步,調(diào)用了三個(gè)方法很清晰
我們先看一下最后 return 出去的是個(gè)啥,再來(lái)深入上面這三步分別調(diào)用的方法源碼,也好更清楚的知道這三步分別是要做哪些處理
編譯結(jié)果
比如有這樣的模板
<template>
<div id="app">{{name}}</div>
</template>
打印一下編譯后的結(jié)果,也就是上面源碼 return 出去的結(jié)果,看看是啥
{
ast: {
type: 1,
tag: 'div',
attrsList: [ { name: 'id', value: 'app' } ],
attrsMap: { id: 'app' },
rawAttrsMap: {},
parent: undefined,
children: [
{
type: 2,
expression: '_s(name)',
tokens: [ { '@binding': 'name' } ],
text: '{{name}}',
static: false
}
],
plain: false,
attrs: [ { name: 'id', value: '"app"', dynamic: undefined } ],
static: false,
staticRoot: false
},
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`,
staticRenderFns: [],
errors: [],
tips: []
}
看不明白也沒(méi)有關(guān)系,注意看上面提到的三步都干了啥
-
ast字段,就是第一步生成的 -
static字段,就是標(biāo)記,是在第二步中根據(jù)ast里的type加上去的 -
render字段,就是第三步生成的
有個(gè)大概的印象了,然后再來(lái)看源碼
1. parse()
源碼地址:src/complier/parser/index.js - 79行
就是這個(gè)方法就是解析器的主函數(shù),就是它通過(guò)正則等方法提取出 <template></template> 模板字符串里所有的 tag、props、children 信息,生成一個(gè)對(duì)應(yīng)結(jié)構(gòu)的 ast 對(duì)象
parse 接收兩個(gè)參數(shù)
-
template:就是要轉(zhuǎn)換的模板字符串 -
options:就是轉(zhuǎn)換時(shí)需要的參數(shù)。它包含有四個(gè)鉤子函數(shù),就是用來(lái)把parseHTML解析出來(lái)的字符串提取出來(lái),并生成對(duì)應(yīng)的AST
核心步驟是這樣的:
調(diào)用 parseHTML 函數(shù)對(duì)模板字符串進(jìn)行解析
- 解析到開(kāi)始標(biāo)簽、結(jié)束標(biāo)簽、文本、注釋分別進(jìn)行不同的處理
- 解析過(guò)程中遇到文本信息就調(diào)用文本解析器
parseText函數(shù)進(jìn)行文本解析 - 解析過(guò)程中遇到包含過(guò)濾器,就調(diào)用過(guò)濾器解析器
parseFilters函數(shù)進(jìn)行解析
每一步解析的結(jié)果都合并到一個(gè)對(duì)象上(就是最后的 AST)
這個(gè)地方的源碼實(shí)在是太長(zhǎng)了,有大幾百行代碼,我就只貼個(gè)大概吧,有興趣的自己去看一下
export function parse (
template: string, // 要轉(zhuǎn)換的模板字符串
options: CompilerOptions // 轉(zhuǎn)換時(shí)需要的參數(shù)
): ASTElement | void {
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 解析到開(kāi)始標(biāo)簽時(shí)調(diào)用,如 <div>
start (tag, attrs, unary, start, end) {
// unary 是否是自閉合標(biāo)簽,如 <img />
...
},
// 解析到結(jié)束標(biāo)簽時(shí)調(diào)用,如 </div>
end (tag, start, end) {
...
},
// 解析到文本時(shí)調(diào)用
chars (text: string, start: number, end: number) {
// 這里會(huì)判斷判斷很多東西,來(lái)看它是不是帶變量的動(dòng)態(tài)文本
// 然后創(chuàng)建動(dòng)態(tài)文本或靜態(tài)文本對(duì)應(yīng)的 AST 節(jié)點(diǎn)
...
},
// 解析到注釋時(shí)調(diào)用
comment (text: string, start, end) {
// 注釋是這么找的
const comment = /^<!\--/
if (comment.test(html)) {
// 如果是注釋?zhuān)屠^續(xù)找 '-->'
const commentEnd = html.indexOf('-->')
...
}
})
// 返回的這個(gè)就是 AST
return root
}
上面解析文本時(shí)調(diào)用的 chars() 會(huì)根據(jù)不同類(lèi)型節(jié)點(diǎn)加上不同 type,來(lái)標(biāo)記 AST 節(jié)點(diǎn)類(lèi)型,這個(gè)屬性在下一步標(biāo)記的時(shí)候會(huì)用到
| type | AST 節(jié)點(diǎn)類(lèi)型 |
|---|---|
| 1 | 元素節(jié)點(diǎn) |
| 2 | 包含變量的動(dòng)態(tài)文本節(jié)點(diǎn) |
| 3 | 沒(méi)有變量的純文本節(jié)點(diǎn) |
2. optimize()
這個(gè)函數(shù)就是在 AST 里找出靜態(tài)節(jié)點(diǎn)和靜態(tài)根節(jié)點(diǎn),并添加標(biāo)記,為了后面 patch 過(guò)程中就會(huì)跳過(guò)靜態(tài)節(jié)點(diǎn)的對(duì)比,直接克隆一份過(guò)去,從而優(yōu)化了 patch 的性能
函數(shù)里面調(diào)用的外部函數(shù)就不貼代碼了,大致過(guò)程是這樣的
-
標(biāo)記靜態(tài)節(jié)點(diǎn)(markStatic)。就是判斷 type,上面介紹了值為 1、2、3的三種類(lèi)型
- type 值為1:就是包含子元素的節(jié)點(diǎn),設(shè)置 static 為 false 并遞歸標(biāo)記子節(jié)點(diǎn),直到標(biāo)記完所有子節(jié)點(diǎn)
- type 值為 2:設(shè)置 static 為 false
- type 值為 3:就是不包含子節(jié)點(diǎn)和動(dòng)態(tài)屬性的純文本節(jié)點(diǎn),把它的 static = true,patch 的時(shí)候就會(huì)跳過(guò)這個(gè),直接克隆一份去
-
標(biāo)記靜態(tài)根節(jié)點(diǎn)(markStaticRoots),這里的原理和標(biāo)記靜態(tài)節(jié)點(diǎn)基本相同,只是需要滿足下面條件的節(jié)點(diǎn)才能算作是靜態(tài)根節(jié)點(diǎn)
- 節(jié)點(diǎn)本身必須是靜態(tài)節(jié)點(diǎn)
- 必須有子節(jié)點(diǎn)
- 子節(jié)點(diǎn)不能只有一個(gè)文本節(jié)點(diǎn)
源碼地址:src/complier/optimizer.js - 21行
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// 標(biāo)記靜態(tài)節(jié)點(diǎn)
markStatic(root)
// 標(biāo)記靜態(tài)根節(jié)點(diǎn)
markStaticRoots(root, false)
}
3. generate()
這個(gè)就是生成 render 的函數(shù),就是說(shuō)最終會(huì)返回下面這樣的東西
// 比如有這么個(gè)模板
<template>
<div id="app">{{name}}</div>
</template>
// 上面模板編譯后返回的 render 字段 就是這樣的
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`
// 把內(nèi)容格式化一下,容易理解一點(diǎn)
with(this){
return _c(
'div',
{ attrs:{"id":"app"} },
[ _v(_s(name)) ]
)
}
這個(gè)結(jié)構(gòu)是不是有點(diǎn)熟悉?
了解虛擬 DOM 就可以看出來(lái),上面的 render 正是虛擬 DOM 的結(jié)構(gòu),就是把一個(gè)標(biāo)簽分為 tag、props、children,沒(méi)有錯(cuò)
在看 generate 源碼之前,我們要先了解一下上面這最后返回的 render 字段是什么意思,再來(lái)看 generate 源碼,就會(huì)輕松得多,不然連函數(shù)返回的東西是干嘛的都不知道怎么可能看得懂這個(gè)函數(shù)呢
render
我們來(lái)翻譯一下上面編譯出來(lái)的 render
這個(gè) with 在 《你不知道的JavaScript》上卷里介紹的是,用來(lái)欺騙詞法作用域的關(guān)鍵字,它可以讓我們更快的引用一個(gè)對(duì)象上的多個(gè)屬性
看個(gè)例子
const name = '掘金'
const obj = { name:'沐華', age: 18 }
with(obj){
console.log(name) // 沐華 不需要寫(xiě) obj.name 了
console.log(age) // 18 不需要寫(xiě) obj.age 了
}
上面的 with(this){} 里的 this 就是當(dāng)前組件實(shí)例。因?yàn)橥ㄟ^(guò) with 改變了詞法作用域中屬性的指向,所以標(biāo)簽里使用 name 直接用就是了,而不需要 this.name 這樣
那 _c、 _v 和 _s 是什么呢?
在源碼里是這樣定義的,格式是:_c(縮寫(xiě)) = createElement(函數(shù)名)
源碼地址:src/core/instance/render-helpers/index.js - 15行
// 其實(shí)不止這幾個(gè),由于本文例子中沒(méi)有用到就沒(méi)都復(fù)制過(guò)來(lái)占位了
export function installRenderHelpers (target: any) {
target._s = toString // 轉(zhuǎn)字符串函數(shù)
target._l = renderList // 生成列表函數(shù)
target._v = createTextVNode // 創(chuàng)建文本節(jié)點(diǎn)函數(shù)
target._e = createEmptyVNode // 創(chuàng)建空節(jié)點(diǎn)函數(shù)
}
// 補(bǔ)充
_c = createElement // 創(chuàng)建虛擬節(jié)點(diǎn)函數(shù)
再來(lái)看是不是就清楚多了呢
with(this){ // 欺騙詞法作用域,將該作用域里所有屬姓和方法都指向當(dāng)前組件
return _c( // 創(chuàng)建一個(gè)虛擬節(jié)點(diǎn)
'div', // 標(biāo)簽為 div
{ attrs:{"id":"app"} }, // 有一個(gè)屬性 id 為 'app'
[ _v(_s(name)) ] // 是一個(gè)文本節(jié)點(diǎn),所以把獲取到的動(dòng)態(tài)屬性 name 轉(zhuǎn)成字符串
)
}
接下來(lái)我們?cè)賮?lái)看 generate() 源碼
generate
源碼地址:src/complier/codegen/index.js - 43行
這個(gè)流程很簡(jiǎn)單,只有幾行代碼,就是先判斷 AST 是不是為空,不為空就根據(jù) AST 創(chuàng)建 vnode,否則就創(chuàng)建一個(gè)空div 的 vnode
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 就是先判斷 AST 是不是為空,不為空就根據(jù) AST 創(chuàng)建 vnode,否則就創(chuàng)建一個(gè)空div的 vnode
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
可以看出這里面主要就是通過(guò) genElement() 方法來(lái)創(chuàng)建 vnode 的,所以我們來(lái)看一下它的源碼,看是怎么創(chuàng)建的
genElement()
源碼地址:src/complier/codegen/index.js - 56行
這里的邏輯還是很清晰的,就是一堆 if/else 判斷傳進(jìn)來(lái)的 AST 元素節(jié)點(diǎn)的屬性來(lái)執(zhí)行不同的生成函數(shù)
這里還可以發(fā)現(xiàn)另一個(gè)知識(shí)點(diǎn) v-for 的優(yōu)先級(jí)要高于 v-if,因?yàn)橄扰袛?for 的
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) { // v-once
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if
return genIf(el, state)
// template 節(jié)點(diǎn) && 沒(méi)有插槽 && 沒(méi)有 pre 標(biāo)簽
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // v-slot
return genSlot(el, state)
} else {
// component or element
let code
// 如果有子組件
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
// 獲取元素屬性 props
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
// 獲取元素子節(jié)點(diǎn)
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)
}
// 返回上面作為 with 作用域執(zhí)行的內(nèi)容
return code
}
}
每一種類(lèi)型調(diào)用的生成函數(shù)就不一一列舉了,總的來(lái)說(shuō)最后創(chuàng)建出來(lái)的 vnode 節(jié)點(diǎn)類(lèi)型無(wú)非就三種,元素節(jié)點(diǎn)、文本節(jié)點(diǎn)、注釋節(jié)點(diǎn)
自定義的 render
先舉個(gè)例子吧,三種情況如下
// 1. test.vue
<template>
<h1>我是沐華</h1>
</template>
<script>
export default {}
</script>
// 2. test.vue
<script>
export default {
render(h){
return h('h1',{},'我是沐華')
}
}
</script>
// 3. test.js
export default {
render(h){
return h('h1',{},'我是沐華')
}
}
上面三種,最后渲染的出來(lái)的就是完全一模一樣的,因?yàn)檫@個(gè) h 就是上面模板編譯后的那個(gè) _c
這時(shí)有人可能就會(huì)問(wèn),為什么要自己寫(xiě)呢,不是有模板編譯自動(dòng)生成嗎?
這個(gè)問(wèn)題問(wèn)得好!自己寫(xiě)肯定是有好處的
- 自己把 vnode 給寫(xiě)了,就會(huì)直接跳過(guò)了模板編譯,不用去解析模板里的動(dòng)態(tài)屬性、事件、指令等等了,所以性能上會(huì)有那么一丟丟提升。這一點(diǎn)在下面的渲染的優(yōu)先級(jí)上就有體現(xiàn)
- 還有一些情況,能讓我們代碼寫(xiě)法的更加靈活,更加方便簡(jiǎn)潔,不會(huì)冗余
比如 Element-UI 里面的組件源碼里就有大量直接寫(xiě) render 函數(shù)
接下來(lái)分別看下這兩點(diǎn)是如何體現(xiàn)的
1. 渲染優(yōu)先級(jí)
先看一下在官網(wǎng)的生命周期里,關(guān)于模板編譯的部分
如圖可以知道,如果有 template,就不會(huì)管 el 了,所以 template 比 el 的優(yōu)先級(jí)更高,比如
那我們自己寫(xiě)了 render 呢?
<div id='app'>
<p>{{ name }}</p>
</div>
<script>
new Vue({
el:'#app',
data:{ name:'沐華' },
template:'<div>掘金</div>',
render(h){
return h('div', {}, '好好學(xué)習(xí),天天向上')
}
})
</script>
這個(gè)代碼執(zhí)行后頁(yè)面渲染出來(lái)只有 <div>好好學(xué)習(xí),天天向上</div>
可以得出 render 函數(shù)的優(yōu)先級(jí)更高
因?yàn)椴还苁?el 掛載的,還是 emplate 最后都會(huì)被編譯成 render 函數(shù),而如果已經(jīng)有了 render 函數(shù)了,就跳過(guò)前面的編譯了
這一點(diǎn)在源碼里也有體現(xiàn)
在源碼中找到答案:dist/vue.js - 11927行
Vue.prototype.$mount = function ( el, hydrating ) {
el = el && query(el);
var options = this.$options;
// 如果沒(méi)有 render
if (!options.render) {
var template = options.template;
// 再判斷,如果有 template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
return this
}
// 再判斷,如果有 el
} else if (el) {
template = getOuterHTML(el);
}
}
return mount.call(this, el, hydrating)
};
2. 更靈活的寫(xiě)法
比如說(shuō)我們需要寫(xiě)很多 if 判斷的時(shí)候
<template>
<h1 v-if="level === 1">
<a href="xxx">
<slot></slot>
</a>
</h1>
<h2 v-else-if="level === 2">
<a href="xxx">
<slot></slot>
</a>
</h2>
<h3 v-else-if="level === 3">
<a href="xxx">
<slot></slot>
</a>
</h3>
</template>
<script>
export default {
props:['level']
}
</script>
不知道你有沒(méi)有寫(xiě)過(guò)類(lèi)似上面這樣的代碼呢?
我們換一種方式來(lái)寫(xiě)出和上面一模一樣的代碼看看,直接寫(xiě) render
<script>
export default {
props:['level'],
render(h){
return h('h' + this.level, this.$slots.default())
}
}
</script>
搞定!就這!就這?
沒(méi)錯(cuò),就這!
或者下面這樣,多次調(diào)用的時(shí)候就很方便
<script>
export default {
props:['level'],
render(h){
const tag = 'h' + this.level
return (<tag>{this.$slots.default()}</tag>)
}
}
</script>
補(bǔ)充
如果想知道更多格式的模板編譯出來(lái)是什么樣的,可以這樣
Vue2 的模板編譯可以安裝 vue-template-compiler
Vue3 的模板編譯可以點(diǎn)這里
然后自行測(cè)試
另外在 Vue3 里模板編譯部分有一些修改,可以點(diǎn)擊下面鏈接,看下 深入淺出虛擬 DOM 和 Diff 算法,里面有介紹
往期精彩
- 深入淺出虛擬 DOM 和 Diff 算法,及 Vue2 與 Vue3 中的區(qū)別
- Vue3的7種和Vue2的12種組件通信,值得收藏
- 最新的 Vue3.2 都更新了些什么了解一下
- JavaScript進(jìn)階知識(shí)點(diǎn)
- 前端異常監(jiān)控和容災(zāi)
- 20分鐘助你拿下HTTP和HTTPS,鞏固你的HTTP知識(shí)體系
結(jié)語(yǔ)
如果本文對(duì)你有一丁點(diǎn)幫助,點(diǎn)個(gè)贊支持一下吧,感謝感謝