template 與 render
在使用vue的時(shí)候,我們有兩種方式來(lái)創(chuàng)建我們的HTML頁(yè)面,第一種情況,也是大多情況下,我們會(huì)使用模板template的方式,因?yàn)檫@更易讀易懂也是官方推薦的方法;第二種情況是使用render函數(shù)來(lái)生成HTML,它比template更接近編譯器,這也對(duì)我們的JavaScript的編程能力要求更高。
實(shí)際上使用render函數(shù)的方式會(huì)比使用template的效率更高,因?yàn)関ue會(huì)先將template編譯成render函數(shù),然后再走之后的流程,也就是說(shuō)使用template會(huì)比render多走編譯成render這一步,而這一步就是由我們的compiler來(lái)實(shí)現(xiàn)的啦,本節(jié)講述的就是vue源碼中的編譯器compiler,它是如何一步步將template最后轉(zhuǎn)換為render。
$mount小插曲
由于在vue實(shí)例的那一篇我漏掉了$mount
的具體實(shí)現(xiàn),而理解$mount
的流程也會(huì)幫助我們更好地引入compiler
,那我們就快速地看看$mount
吧(假如覺(jué)得只想看編譯部分的同學(xué)可以跳過(guò)這一段哦 )
entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// 沒(méi)有render時(shí)將template轉(zhuǎn)化為render
if (!options.render) {
let template = options.template
// 有template
if (template) {
// 判斷template類型(#id、模板字符串、dom元素)
// template是字符串
if (typeof template === 'string') {
// template是#id
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
// template是dom元素
} else if (template.nodeType) {
template = template.innerHTML
} else {
// 無(wú)效template
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
// 無(wú)template
} else if (el) {
// 如果 render 函數(shù)和 template 屬性都不存在,掛載 DOM 元素的 HTML 會(huì)被提取出來(lái)用作模板
template = getOuterHTML(el)
}
// 執(zhí)行template => compileToFunctions()
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 有render
return mount.call(this, el, hydrating)
}
整體流程:
- 用一個(gè)變量
mount
把原來(lái)的$mount
方法存起來(lái),再重寫$mount
方法 - 然后對(duì)
el
進(jìn)行處理,el
可以是dom節(jié)點(diǎn)或者是節(jié)點(diǎn)的選擇器字符串,若是后者的話在通過(guò)query(el)
進(jìn)行轉(zhuǎn)換 -
el
不能是html
或者body
元素(也就是說(shuō)不能直接將vue綁定在html
或者body
標(biāo)簽上) - 若沒(méi)有
render
函數(shù)- 若有
template
,判斷template
類型(#id、模板字符串、dom元素) - 若
render
函數(shù)和template
都不存在,掛載DOM元素的HTML會(huì)被提取出來(lái)用作template
- 執(zhí)行
template => compileToFunctions()
,將template
轉(zhuǎn)換為render
- 若有
- 若有
render
函數(shù)- 走原來(lái)的
$mount
方法
- 走原來(lái)的
這里就證明了使用template
的話還是會(huì)先轉(zhuǎn)換為render
再進(jìn)行下一步的操作,我們接著看下一步發(fā)生了什么吧~
runtime/index.js
上一個(gè)文件中的vue是來(lái)自這里的,我們?cè)谶@里可以看到
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
可以看出這個(gè)$mount
方法返回的是mountComponent
這個(gè)方法,我們又繼續(xù)找找
instance/lifecycle.js
原來(lái)mountComponent
是在lifecycle.js
中,兜兜轉(zhuǎn)轉(zhuǎn)我們又回到了實(shí)例的這一塊來(lái)~
export function mountComponent () {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
這里的操作就是在調(diào)用beforeMount
鉤子前檢查選項(xiàng)里有沒(méi)有render
函數(shù),沒(méi)有的話我們就給它建個(gè)空的,然后我們執(zhí)行vm._update(vm._render(), hydrating)
,再用watcher
進(jìn)行數(shù)據(jù)綁定,然后調(diào)用mounted
鉤子。關(guān)于_update
和_render
的實(shí)現(xiàn)我們先賣個(gè)關(guān)子~ 等我們學(xué)到虛擬dom實(shí)現(xiàn)的時(shí)候再看。
compiler 整體流程
前面搞了這么多前戲,終于開始講compiler了~ 還記得剛提到重寫的$mount
方法嗎,里面將template
轉(zhuǎn)換為render
是通過(guò)compileToFunctions
方法實(shí)現(xiàn)的,我們看看他的來(lái)頭,之后的邏輯會(huì)有點(diǎn)繞但是不難理解,提醒~~~~ 對(duì)于繞來(lái)繞去的源碼有一個(gè)好的方法就是寫demo + 打斷點(diǎn)!根據(jù)你的需求去打斷點(diǎn)看一下輸出的內(nèi)容是否符合你的預(yù)期,這會(huì)對(duì)你理解源碼很有幫助哦,在后面的學(xué)習(xí)中我們也會(huì)用例子去分析~~~ 跟隨著compileToFunctions
的源頭,我們走起!~
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){
return function createCompiler (baseOptions) {
function compile (template, options) {
const finalOptions = Object.create(baseOptions)
// merge
if (options) {
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
// finalOptions 合并 baseOptions 和 options
const compiled = baseCompile(template, finalOptions)
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
呼~一下子貼這么多代碼,好怕被打,我們可以先把路找著,具體代碼再慢慢看。
createCompilerCreator
是個(gè)高階函數(shù),接受一個(gè)函數(shù)baseCompile
,返回了一個(gè)函數(shù)createCompiler
,createCompiler
函數(shù)里又有一個(gè)compile
函數(shù),里面調(diào)用了baseCompile
和最初傳入的baseOptions
,最后返回compile
函數(shù)和compileToFunctions
函數(shù)。emmm...有點(diǎn)亂呵,我畫個(gè)圖給你們將就著看吧。。。
我們先看create-compiler.js
中的createCompilerCreator
函數(shù)中的createCompiler
函數(shù)中的compile
函數(shù)中(好累。。。):
先是將參數(shù)baseOptions
和傳入的options
進(jìn)行合并得到finalOptions
,再進(jìn)行最關(guān)鍵一步(終于!):const compiled = baseCompile(template, finalOptions)
。
而baseCompile
函數(shù)就是最外層createCompilerCreator
函數(shù)的一個(gè)參數(shù),這個(gè)關(guān)鍵的流程我們等下就看,我們先繼續(xù),由baseCompile
得到了我們想要的結(jié)果compiled
,再返回給上一個(gè)函數(shù)createCompiler
,在return
中有我們要的一個(gè)函數(shù),就是我們最開始調(diào)用的compileToFunctions
,原來(lái)他就是通過(guò)一個(gè)函數(shù)將我們的compile
結(jié)果轉(zhuǎn)換為compileToFunctions
。
我們?nèi)タ纯催@個(gè)轉(zhuǎn)換函數(shù)createCompileToFunctionFn
,然后對(duì)比一下轉(zhuǎn)換前后兩者的差別。在src/compiler/to-function.js
文件中,我就不貼代碼了,你們自己對(duì)著源碼看吧,我說(shuō)一下里面主要完成的操作就是執(zhí)行了compile
函數(shù)得到原來(lái)的值再進(jìn)行轉(zhuǎn)化,再將其存進(jìn)緩存中。
而原compile
返回的結(jié)構(gòu)是:
{
ast,
render,
staticRenderFns
}
經(jīng)過(guò)轉(zhuǎn)化后沒(méi)有了ast
,而且將render
和staticRenderFns
轉(zhuǎn)換為函數(shù)的形式:
{
render,
staticRenderFns
}
看完了整體流程,我們看回很關(guān)鍵的函數(shù)baseCompile
baseCompile
function baseCompile (template, options) {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
其實(shí)這個(gè)函數(shù)很短但是闡述了我們編譯的全過(guò)程
parse -> optimize -> generate
step 1 :先對(duì)template
進(jìn)行parse
得到抽象語(yǔ)法樹AST
step 2 :將AST
進(jìn)行靜態(tài)優(yōu)化
step 3 :由AST
生成render
返回的格式就是
{
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
最后我放上來(lái)自web平臺(tái)中的baseOptions的配置含義,方便你們以后看源碼可以查詢
{
expectHTML: true, // 是否期望HTML,不知道是啥反正web中的是true
modules, // klass和style,對(duì)模板中類和樣式的解析
directives, // v-model、v-html、v-text
isPreTag, // v-pre標(biāo)簽
isUnaryTag, // 單標(biāo)簽,比如img、input、iframe
mustUseProp, // 需要使用props綁定的屬性,比如value、selected等
canBeLeftOpenTag, // 可以不閉合的標(biāo)簽,比如tr、td等
isReservedTag, // 是否是保留標(biāo)簽,html標(biāo)簽和SVG標(biāo)簽
getTagNamespace, // 命名空間,svg和math
staticKeys: genStaticKeys(modules) // staticClass,staticStyle。
}
這三個(gè)步驟在接下來(lái)的文章里我們會(huì)進(jìn)行更詳細(xì)的分析~ 對(duì)于compiler的概念和整體的流程都基本講完啦,謝謝你們的支持,如有分析錯(cuò)誤之處可以隨意提出來(lái),我們一起探討探討~