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