Vue, 現階段受熱指數上升比較塊的前端架構。有必要從源碼的角度,對它的功能的實現原理一窺究竟。看源碼一般主要看兩樣東西, 從宏觀角度是它的設計思想和實現原理; 微觀上則是編程技巧。 這裏我們側重點是實現原理上。
vue 是什麼?
我們在使用vue時,初始化操作都會使用new Vue({…}),不難發現 vue 其實是一個類。 不過ES6普及的今天,vue 的定義任是普通構造函數。 爲什麼不用 ES6的class 呢? 稍後會介紹。 首先來看看vue 被定義的地方:
function Vue(options) {
...
this._init(options)
}
這裏省略掉了,flow的類型檢查及一些邊界情況的源碼及講解。比如省略號這裏邊界情況是使用必須是new Vue() 的形式,否則會報錯。
其實vue 源碼就像一顆樹, 在看之前最好先確定看什麼功能,避開那些分叉邏輯。
我們接下來的目標就是從new Vue()開始,走完一整條從初始化,數據,模板到真實Dom的這整個流程
當執行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 的構造函數傳入到以下方法內(這裏通過注入方式給Vue擴展API):
- initMixin(Vue): 定義 _init 方法。
- stateMixin(Vue): 定義數據相關的方法$set, $delete, $watch 方法。
- eventsMixin(Vue): 定義事件相關的方法$on, $once, $off, $emit。
- lifecycleMixin(Vue): 定義_update, 及生命週期相關的 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-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 這樣一個內部定義的方法。
這裏提一下 vue 的架構設計,它的架構是分層式的。 最頂層是一個ES5 的構造函數, 在上層在原型上會定義一些==_init==, $watch, _render 等這樣的方法, 再上層會在構造函數自定義全局的一些API, 如 set, nextTick, use 等(以上這些事不區分平臺的核心代碼), 接着是跨平臺和服務端渲染及編譯器。 這些屬性方法都定義好了以後,最後導出一個完整的構造函數給到用戶使用。 new Vue 就是開啓的鑰匙。
上面我們從比較微觀的角度近距離的觀察了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 做的離線編譯,運行性能更高。