Vue
,現(xiàn)在前端的當(dāng)紅炸子雞,隨著熱度指數(shù)上升,實(shí)在是有必要從源碼的角度,對(duì)它功能的實(shí)現(xiàn)原理一窺究竟。個(gè)人覺(jué)得看源碼主要是看兩樣?xùn)|西,從宏觀上來(lái)說(shuō)是它的設(shè)計(jì)思想和實(shí)現(xiàn)原理;微觀上來(lái)說(shuō)就是編程技巧,也就是俗稱的騷操作。我們這次的側(cè)重點(diǎn)是它的實(shí)現(xiàn)原理。好吧,讓我們推開(kāi)它那神秘的大門(mén),進(jìn)入Vue
的世界~
vue是什么?
vue
究竟是什么?為什么就能實(shí)現(xiàn)這么多酷炫的功能,不知道大家有沒(méi)有思考過(guò)這個(gè)問(wèn)題。其實(shí)在每次初始化vue
,使用new Vue({...})
時(shí),不難發(fā)現(xiàn)vue
其實(shí)是一個(gè)類。不過(guò)即使在ES6
已經(jīng)如此普及的今天,vue
的定義卻是普通構(gòu)造函數(shù)定義的,為什么沒(méi)有采用ES6
的class
呢?這個(gè)我們稍后回答,通過(guò)層層追蹤終于找到了vue
被定義的地方:
function Vue(options) {
...
this._init(options)
}
因?yàn)槭窃斫馕觯?code>flow的類型檢測(cè)及一些邊界情況,如使用方式不對(duì)或參數(shù)不對(duì)或不是主要邏輯的代碼我們就省略掉吧。比如省略號(hào)這里邊界情況是使用時(shí)必須是new Vue()
的形式,否則會(huì)報(bào)錯(cuò)。
其實(shí)vue
源碼就像一棵樹(shù),我們看之前最好要確定看什么功能,然后避開(kāi)那些分叉邏輯,我們接下來(lái)的目標(biāo)就是以new Vue()
開(kāi)始,走完一整條從初始化、數(shù)據(jù)、模板到真實(shí)Dom
的這整個(gè)流程。
這就是vue
最初始被定義的地方,你沒(méi)看錯(cuò),就是這么簡(jiǎn)單。當(dāng)執(zhí)行new Vue
時(shí),內(nèi)部會(huì)執(zhí)行一個(gè)方法 this._init(options)
,將初始化的參數(shù)傳入。
這里需要說(shuō)明一點(diǎn),在vue
的內(nèi)部,_
符號(hào)開(kāi)頭定義的變量是供內(nèi)部私有使用的,而$
符號(hào)定義的變量是供用戶使用的,而且用戶自定義的變量不能以_
或$
開(kāi)頭,以防止內(nèi)部沖突。我們接著看:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
function Vue(options) {
...
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
現(xiàn)在可以回答之前的問(wèn)題了,為什么不采用ES6
的class
來(lái)定義,因?yàn)檫@樣可以方便的把vue
的功能拆分到不同的目錄中去維護(hù),將vue
的構(gòu)造函數(shù)傳入到以下方法內(nèi):
- initMixin(Vue):定義
_init
方法。 - stateMixin(Vue):定義數(shù)據(jù)相關(guān)的方法
$set
,$delete
,$watch
方法。 - eventsMixin(Vue):定義事件相關(guān)的方法
$on
,$once
,$off
,$emit
。 - lifecycleMixin(Vue):定義
_update
,及生命周期相關(guān)的$forceUpdate
和$destroy
。 - renderMixin(Vue):定義
$nextTick
,_render
將render函數(shù)轉(zhuǎn)為vnode
。
這些方法都是在各自的文件內(nèi)維護(hù)的,從而讓代碼結(jié)構(gòu)更加清晰易懂可維護(hù)。如this._init
方法被定義在:
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
...當(dāng)執(zhí)行new Vue時(shí),進(jìn)行一系列初始化并掛載
}
}
再這些xxxMixin
完成后,接著會(huì)定義一些全局的API
:
export function initGlobalAPI(Vue) {
Vue.set方法
Vue.delete方法
Vue.nextTick方法
...
內(nèi)置組件:
keep-alive
transition
transition-group
...
initUse(Vue):Vue.use方法
initMixin(Vue):Vue.mixin方法
initExtend(Vue):Vue.extend方法
initAssetRegisters(Vue):Vue.component,Vue.directive,Vue.filter方法
}
這里有部分API
和xxxMixin
定義的原型方法功能是類似或相同的,如this.$set
和Vue.set
他們都是使用set
這樣一個(gè)內(nèi)部定義的方法。
這里需要提一下vue
的架構(gòu)設(shè)計(jì),它的架構(gòu)是分層式的。最底層是一個(gè)ES5
的構(gòu)造函數(shù),再上層在原型上會(huì)定義一些_init
、$watch
、_render
等這樣的方法,再上層會(huì)在構(gòu)造函數(shù)自身定義全局的一些API
,如set
、nextTick
、use
等(以上這些是不區(qū)分平臺(tái)的核心代碼),接著是跨平臺(tái)和服務(wù)端渲染(這些暫時(shí)不在討論范圍)及編譯器。將這些屬性方法都定義好了之后,最后會(huì)導(dǎo)出一個(gè)完整的構(gòu)造函數(shù)給到用戶使用,而new Vue
就是啟動(dòng)的鑰匙。這就是我們陌生且又熟悉的vue
,至于Vue.prototype._init
內(nèi)部做了啥?我們下章節(jié)再說(shuō)吧,因?yàn)檫€有很多其他的要補(bǔ)充。
目錄結(jié)構(gòu)
剛才是從比較微觀的角度近距離的觀察了vue
,現(xiàn)在我們從宏觀角度來(lái)了解它內(nèi)部的代碼結(jié)構(gòu)是如何組建起來(lái)的。
目錄如下:
|-- dist 打包后的vue版本
|-- flow 類型檢測(cè),3.0換了typeScript
|-- script 構(gòu)建不同版本vue的相關(guān)配置
|-- src 源碼
|-- compiler 編譯器
|-- core 不區(qū)分平臺(tái)的核心代碼
|-- components 通用的抽象組件
|-- global-api 全局API
|-- instance 實(shí)例的構(gòu)造函數(shù)和原型方法
|-- observer 數(shù)據(jù)響應(yīng)式
|-- util 常用的工具方法
|-- vdom 虛擬dom相關(guān)
|-- platforms 不同平臺(tái)不同實(shí)現(xiàn)
|-- server 服務(wù)端渲染
|-- sfc .vue單文件組件解析
|-- shared 全局通用工具方法
|-- test 測(cè)試
flow:
javaScript
是弱類型語(yǔ)言,使用flow
以定義類型和檢測(cè)類型,增加代碼的健壯性。src/compiler:將
template
模板編譯為render
函數(shù)。src/core:與平臺(tái)無(wú)關(guān)通用的邏輯,可以運(yùn)行在任何
javaScript
環(huán)境下,如web
、Node.js
、weex
嵌入原生應(yīng)用中。src/platforms:針對(duì)
web
平臺(tái)和weex
平臺(tái)分別的實(shí)現(xiàn),并提供統(tǒng)一的API
供調(diào)用。src/observer:
vue
檢測(cè)數(shù)據(jù)數(shù)據(jù)變化改變視圖的代碼實(shí)現(xiàn)。src/vdom:將
render
函數(shù)轉(zhuǎn)為vnode
從而patch
為真實(shí)dom
以及diff
算法的代碼實(shí)現(xiàn)。dist:存放著針對(duì)不同使用方式的不同的
vue
版本。
vue版本
vue
使用的是rollup
構(gòu)建的,具體怎么構(gòu)建的不重要,總之會(huì)構(gòu)建出很多不同版本的vue
。按照使用方式的不同,可以分為以下三類:
- UMD:通過(guò)
<script>
標(biāo)簽直接在瀏覽器中使用。 - CommonJS:使用比較舊的打包工具使用,如
webpack1
。 - ES Module:配合現(xiàn)代打包工具使用,如
webpack2
及以上。
而每個(gè)使用方式內(nèi)又分為了完整版和運(yùn)行時(shí)版本,這里主要以ES Module
為例,有了官方腳手架其他兩類應(yīng)該沒(méi)多少人用了。再說(shuō)明這兩個(gè)版本的區(qū)別之前,抱歉我又要補(bǔ)充點(diǎn)其他的。在vue
的內(nèi)部是只認(rèn)render
函數(shù)的,我們來(lái)自己定義一個(gè)render
函數(shù),也就是這么個(gè)東西:
new Vue({
data: {
msg: 'hello Vue!'
},
render(h) {
return h('span', this.msg);
}
}).$mount('#app');
可能有人會(huì)納悶了,既然只認(rèn)render
函數(shù),同時(shí)我們開(kāi)發(fā)好像從來(lái)并沒(méi)有寫(xiě)過(guò)render
函數(shù),而是使用的template
模板。這是因?yàn)橛?code>vue-loader,它會(huì)將我們?cè)?code>template內(nèi)定義的內(nèi)容編譯為render
函數(shù),而這個(gè)編譯就是區(qū)分完整版和運(yùn)行時(shí)版本的關(guān)鍵所在,完整版就自帶這個(gè)編譯器,而運(yùn)行時(shí)版本就沒(méi)有,如下面這段代碼如果是在運(yùn)行時(shí)版本環(huán)境下就會(huì)報(bào)錯(cuò)了:
new Vue({
data: {
msg: 'hello Vue!'
},
template: `<div>{{msg}}</div>`
})
vue-cli
默認(rèn)是使用運(yùn)行時(shí)版本的,更改或覆蓋腳手架內(nèi)的默認(rèn)配置,將其更改為完整版即可通過(guò)編譯:'vue$': 'vue/dist/vue.esm.js'
,推薦還是使用運(yùn)行時(shí)版本。好吧,具體區(qū)別最后我們以一個(gè)面試時(shí)經(jīng)常會(huì)被問(wèn)到的問(wèn)題作為本章節(jié)的結(jié)束。
面試官微笑而又不失禮貌的問(wèn)到:
- 請(qǐng)問(wèn)
runtime
和runtime-only
這兩個(gè)版本的區(qū)別?
懟回去:
- 主要是兩點(diǎn)不同:
- 最明顯的就是大小的區(qū)別,帶編譯器會(huì)比不帶的版本大
6kb
。 - 編譯的時(shí)機(jī)不同,編譯器是運(yùn)行時(shí)編譯,性能會(huì)有一定的損耗;運(yùn)行時(shí)版本是借助
loader
做的離線編譯,運(yùn)行性能更高。
順手點(diǎn)個(gè)贊或關(guān)注唄,找起來(lái)也方便~
分享一個(gè)筆者自己寫(xiě)的組件庫(kù),哪天可能會(huì)用的上了 ~ ↓