深入解析Vue源碼實(shí)例掛載與編譯流程實(shí)現(xiàn)思路詳解
在正文開始之前,先了解vue基于源碼構(gòu)建的兩個(gè)版本,一個(gè)是 runtime only ,另一個(gè)是 runtime加compiler 的版本,兩個(gè)版本的主要區(qū)別在于后者的源碼包括了一個(gè)編譯器。
什么是編譯器,百度百科上面的解釋是
簡單講,編譯器就是將“一種語言(通常為高級(jí)語言)”翻譯為“另一種語言(通常為低級(jí)語言)”的程序。一個(gè)現(xiàn)代編譯器的主要工作流程:源代碼 (source code) → 預(yù)處理器 (preprocessor) → 編譯器 (compiler) → 目標(biāo)代碼 (object code) → 鏈接器 (Linker) → 可執(zhí)行程序 (executables)。
通俗點(diǎn)講,編譯器是一個(gè)提供了將源代碼轉(zhuǎn)化為目標(biāo)代碼的工具。更進(jìn)一步理解,vue內(nèi)置的編譯器實(shí)現(xiàn)了將 .vue 文件轉(zhuǎn)換編譯為可執(zhí)行javascript腳本的功能。
3.1.1 Runtime + Compiler
一個(gè)完整的vue版本是包含編譯器的,我們可以使用 template 進(jìn)行模板編寫。編譯器會(huì)自動(dòng)將模板編譯成 render 函數(shù)。
// 需要編譯器的版本
new Vue({
template: '<div>{{ hi }}</div>'
})
3.1.2 Runtime Only
而對(duì)于一個(gè)不包含編譯器的 runtime-only 版本,需要傳遞一個(gè)編譯好的 render 函數(shù),如下所示:
// 不需要編譯器
new Vue({
render (h) {
return h('div', this.hi)
}
})
很明顯,編譯過程對(duì)性能有一定的損耗,并且由于加入了編譯過程的代碼,vue代碼體積也更加龐大,所以我們可以借助webpack的vue-loader工具進(jìn)行編譯,將編譯階段從vue的構(gòu)建中剝離出來,這樣既優(yōu)化了性能,也縮小了體積。
3.2 掛載的基本思路
vue掛載的流程是比較復(fù)雜的,我們通過流程圖理清基本的實(shí)現(xiàn)思路。

如果用一句話概括掛載的過程,可以描述為掛載組件,將渲染函數(shù)生成虛擬DOM,更新視圖時(shí),將虛擬DOM渲染成為真正的DOM。
詳細(xì)的過程是:首先確定掛載的DOM元素,且必須保證該元素不能為 html,body 這類跟節(jié)點(diǎn)。判斷選項(xiàng)中是否有 render 這個(gè)屬性(如果不在運(yùn)行時(shí)編譯,則在選項(xiàng)初始化時(shí)需要傳遞 render 渲染函數(shù))。當(dāng)有 render 這個(gè)屬性時(shí),默認(rèn)我們使用的是 runtime-only 的版本,從而跳過模板編譯階段,調(diào)用真正的掛載函數(shù) $mount 。另一方面,當(dāng)我們傳遞是 template 模板時(shí)(即在不使用外置編譯器的情況下,我們將使用 runtime+compile 的版本),Vue源碼將首先進(jìn)入編譯階段。該階段的核心是兩步,一個(gè)是把模板解析成抽象的語法樹,也就是我們常聽到的 AST ,第二個(gè)是根據(jù)給定的AST生成目標(biāo)平臺(tái)所需的代碼,在瀏覽器端是前面提到的 render 函數(shù)。完成模板編譯后,同樣會(huì)進(jìn)入 $mount 掛載階段。真正的掛載過程,執(zhí)行的是 mountComponent 方法,該函數(shù)的核心是實(shí)例化一個(gè)渲染 watcher ,具體 watcher 的內(nèi)容,另外放章節(jié)討論。我們只要知道渲染 watcher 的作用,一個(gè)是初始化的時(shí)候會(huì)執(zhí)行回調(diào)函數(shù),另一個(gè)是當(dāng) vm 實(shí)例中監(jiān)測的數(shù)據(jù)發(fā)生變化的時(shí)候執(zhí)行回調(diào)函數(shù)。而這個(gè)回調(diào)函數(shù)就是 updateComponent ,這個(gè)方法會(huì)通過 vm._render 生成虛擬 DOM ,并最終通過 vm._update 將虛擬 DOM 轉(zhuǎn)化為真正的 DOM 。
往下,我們從代碼的角度出發(fā),了解一下掛載的實(shí)現(xiàn)思路,下面只提取mount骨架代碼說明。
// 內(nèi)部真正實(shí)現(xiàn)掛載的方法
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
// 調(diào)用mountComponent方法掛載
return mountComponent(this, el, hydrating)
};
// 緩存了原型上的 $mount 方法
var mount = Vue.prototype.$mount;
// 重新定義$mount,為包含編譯器和不包含編譯器的版本提供不同封裝,最終調(diào)用的是緩存原型上的$mount方法
Vue.prototype.$mount = function (el, hydrating) {
// 獲取掛載元素
el = el && query(el);
// 掛載元素不能為跟節(jié)點(diǎn)
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
var options = this.$options;
// 需要編譯 or 不需要編譯
if (!options.render) {
···
// 使用內(nèi)部編譯器編譯模板
}
// 最終調(diào)用緩存的$mount方法
return mount.call(this, el, hydrating)
}
// mountComponent方法思路
function mountComponent(vm, el, hydrating) {
// 定義updateComponent方法,在watch回調(diào)時(shí)調(diào)用。
updateComponent = function () {
// render函數(shù)渲染成虛擬DOM, 虛擬DOM渲染成真實(shí)的DOM
vm._update(vm._render(), hydrating);
};
// 實(shí)例化渲染watcher
new Watcher(vm, updateComponent, noop, {})
}
3.3 編譯過程 - 模板編譯成 render 函數(shù)
通過文章前半段的學(xué)習(xí),我們對(duì)Vue的掛載流程有了一個(gè)初略的認(rèn)識(shí)。接下來將先從模板編譯的過程展開。閱讀源碼時(shí)發(fā)現(xiàn),模板的編譯過程是相當(dāng)復(fù)雜的,要在短篇幅內(nèi)將整個(gè)編譯的過程講開是不切實(shí)際的,因此這節(jié)剩余內(nèi)容只會(huì)對(duì)實(shí)現(xiàn)思路做簡單的介紹。
3.3.1 template的三種寫法
template模板的編寫有三種方式,分別是:
// 1. 熟悉的字符串模板
var vm = new Vue({
el: '#app',
template: '<div>模板字符串</div>'
})
// 2. 選擇符匹配元素的 innerHTML模板
<div id="app">
<div>test1</div>
<script type="x-template" id="test">
<p>test</p>
</script>
</div>
var vm = new Vue({
el: '#app',
template: '#test'
})
// 3. dom元素匹配元素的innerHTML模板
<div id="app">
<div>test1</div>
<span id="test"><div class="test2">test2</div></span>
</div>
var vm = new Vue({
el: '#app',
template: document.querySelector('#test')
})
三種寫法對(duì)應(yīng)代碼的三個(gè)不同分支。
var template = options.template;
if (template) {
// 針對(duì)字符串模板和選擇符匹配模板
if (typeof template === 'string') {
// 選擇符匹配模板,以'#'為前綴的選擇器
if (template.charAt(0) === '#') {
// 獲取匹配元素的innerHTML
template = idToTemplate(template);
/* istanbul ignore if */
if (!template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
// 針對(duì)dom元素匹配
} else if (template.nodeType) {
// 獲取匹配元素的innerHTML
template = template.innerHTML;
} else {
// 其他類型則判定為非法傳入
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
// 如果沒有傳入template模板,則默認(rèn)以el元素所屬的根節(jié)點(diǎn)作為基礎(chǔ)模板
template = getOuterHTML(el);
}
其中X-Template模板的方式一般用于模板特別大的 demo 或極小型的應(yīng)用,官方不建議在其他情形下使用,因?yàn)檫@會(huì)將模板和組件的其它定義分離開。
3.3.2 流程圖解
vue源碼中編譯流程代碼比較繞,涉及的函數(shù)處理邏輯比較多,實(shí)現(xiàn)流程中巧妙的運(yùn)用了偏函數(shù)的技巧將配置項(xiàng)處理和編譯核心邏輯抽取出來,為了理解這個(gè)設(shè)計(jì)思路,我畫了一個(gè)邏輯圖幫助理解。

3.3.3 邏輯解析
即便有流程圖,編譯邏輯理解起來依然比較晦澀,接下來,結(jié)合代碼分析每個(gè)環(huán)節(jié)的執(zhí)行過程。
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
// 將compileToFunction方法暴露給Vue作為靜態(tài)方法存在
Vue.compile = compileToFunctions;
這是編譯的入口,也是Vue對(duì)外暴露的編譯方法。 compileToFunctions 需要傳遞三個(gè)參數(shù): template 模板,編譯配置選項(xiàng)以及Vue實(shí)例。我們先大致了解一下配置中的幾個(gè)默認(rèn)選項(xiàng)
1. delimiters 該選項(xiàng)可以改變純文本插入分隔符,當(dāng)不傳遞值時(shí),vue默認(rèn)的分隔符為 {{}} ,用戶可通過該選項(xiàng)修改
2. comments 當(dāng)設(shè)為 true 時(shí),將會(huì)保留且渲染模板中的 HTML 注釋。默認(rèn)行為是舍棄它們。
接著一步步尋找compileToFunctions根源
var createCompiler = createCompilerCreator(function baseCompile (template,options) {
//把模板解析成抽象的語法樹
var ast = parse(template.trim(), options);
// 配置中有代碼優(yōu)化選項(xiàng)則會(huì)對(duì)Ast語法樹進(jìn)行優(yōu)化
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
createCompilerCreator 角色定位為創(chuàng)建編譯器的創(chuàng)建者。他傳遞了一個(gè)基礎(chǔ)的編譯器 baseCompile 作為參數(shù), baseCompile 是真正執(zhí)行編譯功能的地方,他傳遞template模板和基礎(chǔ)的配置選項(xiàng)作為參數(shù)。實(shí)現(xiàn)的功能有兩個(gè)
1.把模板解析成抽象的語法樹,簡稱 AST ,代碼中對(duì)應(yīng) parse 部分
2.可選:優(yōu)化 AST 語法樹,執(zhí)行 optimize 方法
3.根據(jù)不同平臺(tái)將 AST 語法樹生成需要的代碼,對(duì)應(yīng)的 generate 函數(shù)
具體看看 createCompilerCreator 的實(shí)現(xiàn)方式。
function createCompilerCreator (baseCompile) {
return function createCompiler (baseOptions) {
// 內(nèi)部定義compile方法
function compile (template, options) {
···
// 將剔除空格后的模板以及合并選項(xiàng)后的配置作為參數(shù)傳遞給baseCompile方法,其中finalOptions為baseOptions和用戶options的合并
var compiled = baseCompile(template.trim(), finalOptions);
{
detectErrors(compiled.ast, warn);
}
compiled.errors = errors;
compiled.tips = tips;
return compiled
}
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
createCompilerCreator 函數(shù)只有一個(gè)作用,利用偏函數(shù)將 baseCompile 基礎(chǔ)編譯方法緩存,并返回一個(gè)編譯器函數(shù),該函數(shù)內(nèi)部定義了真正執(zhí)行編譯的 compile 方法,并最終將 compile 和 compileToFunctons 作為兩個(gè)對(duì)象屬性返回,這也是 compileToFunctions 的來源。而內(nèi)部 compile 的作用,是為了將基礎(chǔ)的配置 baseOptions 和用戶自定義的配置 options 進(jìn)行合并,( baseOptions 是跟外部平臺(tái)相關(guān)的配置),最終返回合并配置后的 baseCompile 編譯方法。
compileToFunctions 來源于 createCompileToFunctionFn 函數(shù)的返回值,該函數(shù)會(huì)將編譯的方法 compile 作為參數(shù)傳入。
function createCompileToFunctionFn (compile) {
var cache = Object.create(null);
return function compileToFunctions (template,options,vm) {
options = extend({}, options);
···
// 緩存的作用:避免重復(fù)編譯同個(gè)模板造成性能的浪費(fèi)
if (cache[key]) {
return cache[key]
}
// 執(zhí)行編譯方法
var compiled = compile(template, options);
···
// turn code into functions
var res = {};
var fnGenErrors = [];
// 編譯出的函數(shù)體字符串作為參數(shù)傳遞給createFunction,返回最終的render函數(shù)
res.render = createFunction(compiled.render, fnGenErrors);
// 渲染優(yōu)化相關(guān)
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
···
return (cache[key] = res)
}
}
最終,我們找到了 compileToFunctions 真正的執(zhí)行過程 var compiled = compile(template, options); ,并將編譯后的函數(shù)體字符串通過 creatFunction 轉(zhuǎn)化為 render 函數(shù)返回。
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err: err, code: code });
return noop
}
}
其中函數(shù)體字符串類似于 "with(this){return _m(0)}" ,最終的render渲染函數(shù)為 function(){with(this){return _m(0)}}
至此,Vue中關(guān)于編譯過程的思路也梳理清楚了,編譯邏輯之所以繞,主要是因?yàn)閂ue在不同平臺(tái)有不同的編譯過程,而每個(gè)編譯過程的 baseOptions 選項(xiàng)會(huì)有所不同,同時(shí)在同一個(gè)平臺(tái)下又不希望每次編譯時(shí)傳入相同的 baseOptions 參數(shù),因此在 createCompilerCreator 初始化編譯器時(shí)便傳入?yún)?shù),并利用偏函數(shù)將配置進(jìn)行緩存。同時(shí)剝離出編譯相關(guān)的合并配置,這些都是Vue在編譯這塊非常巧妙的設(shè)計(jì)。
總結(jié)
以上所述是小編給大家介紹的Vue源碼實(shí)例掛載與編譯流程,希望對(duì)大家有所幫助,如果大家有任何疑問請給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
相關(guān)文章
vue中?根據(jù)判斷條件添加一個(gè)或多個(gè)style及class的寫法小結(jié)
這篇文章主要介紹了vue中?根據(jù)判斷條件添加一個(gè)或多個(gè)style及class的寫法,文中給大家補(bǔ)充介紹了關(guān)于vue里:class的使用結(jié)合自己的實(shí)現(xiàn)給大家講解,需要的朋友可以參考下2023-03-03
在Vue中如何實(shí)現(xiàn)打字機(jī)的效果
這篇文章主要介紹了在Vue中如何實(shí)現(xiàn)打字機(jī)的效果,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
vue-print-nb解決vue打印問題,并且隱藏頁眉頁腳方式
這篇文章主要介紹了vue-print-nb解決vue打印問題,并且隱藏頁眉頁腳方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
詳解Vue全局組件的掛載之實(shí)現(xiàn)彈窗組件
這篇文章主要為大家詳細(xì)介紹了如何通過Vue全局組件的掛載來實(shí)現(xiàn)彈窗組件,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的可以了解一下2022-11-11

