這裡分析的是當前(2018/07/25)最新版 V2.5.16
的原始碼,如果你想一遍看一遍參閱原始碼,請務必記得切換到此版本,不然可能存在微小的差異。
大家都知道,我們的應用是一個由Vue元件構成的一棵樹,其中每一個節點都是一個 Vue 元件。我們的每一個Vue元件是如何被建立出來的,建立的過程經歷了哪些步驟呢?把這些都搞清楚,那麼我們對Vue的整個原理將會有很深入的理解。
從入口函式開始,有比較複雜的引用關係,為了方便大家理解,我畫了一張圖可以直觀地看出他們之間的關係:
建立Vue例項的兩步
我們建立一個Vue例項,只需要兩行程式碼:
1 2 |
import Vue from ‘vue' new Vue(options) |
而這兩步分別經歷了一個比較複雜的構建過程:
- 建立類:建立一個
Vue
建構函式,以及他的一系列原型方法和類方法 - 建立例項:建立一個
Vue
例項,初始化他的資料,事件,模板等
下面我們分別解析這兩個階段,其中每個階段
又分為好多個 步驟
第一階段:建立Vue類
第一階段是要建立一個Vue類,因為我們這裡用的是原型而不是ES6中的class宣告,所以拆成了三步來實現:
- 建立一個建構函式
Vue
- 在
Vue.prototype
上建立一系列例項屬性方法,比如this.$data
等 - 在
Vue
上建立一些全域性方法,比如Vue.use
可以註冊外掛
我們匯入 Vue 建構函式 import Vue from ‘vue’
的時候(new Vue(options)
之前),會生成一個Vue的建構函式,這個建構函式本身很簡單,但是他上面會新增一系列的例項方法和一些全域性方法,讓我們跟著程式碼來依次看看如何一步步構造一個 Vue 類的,我們要明白每一步大致是做什麼的,但是這裡先不深究,因為我們會在接下來幾章具體講解每一步都做了什麼,這裡我們先有一個大致的概念即可。
我們看程式碼先從入口開始,這是我們在瀏覽器環境最常用的一個入口,也就是我們 import Vue
的時候直接匯入的,它很簡單,直接返回了 從 platforms/web/runtime/index/js
中得到的 Vue
建構函式,具體程式碼如下:
platforms/web/entry-runtime.js
1 2 |
import Vue from './runtime/index' export default Vue |
可以看到,這裡不是 Vue 建構函式的定義地方,而是返回了從下面一步得到的Vue建構函式,但是做了一些平臺相關的操作,比如內建 directives 註冊等。這裡就會有人問了,為什麼不直接定義一個建構函式,而是這樣不停的傳遞呢?因為 vue 有不同的執行環境,而每一個環境又有帶不帶 compiler
等不同版本,所以環境的不同以及版本的不同都會導致 Vue
類會有一些差異,那麼這裡會通過不同的步驟來處理這些差異,而所有的環境版本都要用到的核心程式碼是相同的,因此這些相同的程式碼就統一到 core/
中了。
完整程式碼和我加的註釋如下:
platforms/web/runtime/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import Vue from 'core/index' import config from 'core/config' // 省略 import platformDirectives from './directives/index' import platformComponents from './components/index' //這裡都是web平臺相關的一些配置 // install platform specific utils Vue.config.mustUseProp = mustUseProp // 省略 // 註冊指令和元件,這裡的 directives 和 components 也是web平臺上的,是內建的指令和元件,其實很少 // install platform runtime directives & components extend(Vue.options.directives, platformDirectives) // 內建的directives只有兩個,`v-show` 和 `v-model` extend(Vue.options.components, platformComponents) // 內建的元件也很少,只有`keepAlive`, `transition`和 `transitionGroup` // 如果不是瀏覽器,就不進行 `patch` 操作了 // install platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop // 如果有 `el` 且在瀏覽器中,則進行 `mount` 操作 // public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) } // 省略devtool相關程式碼 export default Vue |
上面的程式碼終於把平臺和配置相關的邏輯都處理完了,我們可以進入到了 core
目錄,這裡是Vue元件的核心程式碼,我們首先進入 core/index檔案,發現 Vue
建構函式也不是在這裡定義的。不過這裡有一點值得注意的就是,這裡呼叫了一個 initGlobalAPI
函式,這個函式是新增一些全域性屬性方法到 Vue
上,也就是類方法,而不是例項方法。具體他是做什麼的我們後面再講
core/index.js
1 2 3 4 5 6 7 8 9 10 11 |
import Vue from './instance/index' import { initGlobalAPI } from './global-api/index' initGlobalAPI(Vue) // 這個函式新增了一些類方法屬性 // 省略一些ssr相關的內容 // 省略 Vue.version = '__VERSION__' export default Vue |
到 core/instance/index.js
這裡才是真正的建立了 Vue
建構函式的地方,雖然程式碼也很簡單,就是建立了一個建構函式,然後通過mixin把一堆例項方法新增上去。
core/instance/index.js 完整程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 省略import語句 function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue |
下面我們分成兩段來講解這些程式碼分別幹了什麼。
1 2 3 4 5 6 7 8 |
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) // 建構函式有用的只有這一行程式碼,是不是很簡單,至於這一行程式碼具體做了什麼,在第二階段我們詳細講解。 } |
這裡才是真正的Vue建構函式,注意其實很簡單,忽略在開發模式下的警告外,只執行了一行程式碼 this._init(options)
。可想而知,Vue初始化必定有很多工作要做,比如資料的響應化、事件的繫結等,在第二階段我們會詳細講解這個函式到底做了什麼。這裡我們暫且跳過它。
1 2 3 4 5 |
initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) |
上面這五個函式其實都是在Vue.prototype
上新增了一些屬性方法,讓我們先找一個看看具體的程式碼,比如initMixin
就是新增 _init
函式,沒錯正是我們建構函式中呼叫的那個 this._init(options)
哦,它裡面主要是呼叫其他的幾個初始化方法,因為比較簡單,我們直接看程式碼:
core/instance/init.js
1 2 3 4 5 6 |
export function initMixin (Vue: Class<Component>) { // 就是這裡,新增了一個方法 Vue.prototype._init = function (options?: Object) { // 省略,這部分我們會在第二階段講解 } } |
另外的幾個同樣都是在 Vue.prototype
上新增了一些方法,這裡暫時先不一個個貼程式碼,總結一下如下:
- core/instance/state.js,主要是新增了
$data
,$props
,$watch
,$set
,$delete
幾個屬性和方法 - core/instance/events.js,主要是新增了
$on
,$off
,$once
,$emit
三個方法 - core/instance/lifecycle.js,主要新增了
_update
,$forceUpdate
,$destroy
三個方法 - core/instance/renderMixin.js,主要新增了
$nextTick
和_render
兩個方法以及一大堆renderHelpers
還記得我們跳過的在core/index.js中 新增 globalAPI
的程式碼嗎,前面的程式碼都是在 Vue.prototype
上新增例項屬性,讓我們回到 core/index 檔案,這一步需要在 Vue
上新增一些全域性屬性方法。前面講到過,是通過 initGlobalAPI
來新增的,那麼我們直接看看這個函式的樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
export function initGlobalAPI (Vue: GlobalAPI) { // config const configDef = {} configDef.get = () => config // 省略 // 這裡新增了一個`Vue.config` 物件,至於在哪裡會用到,後面會講 Object.defineProperty(Vue, 'config', configDef) // exposed util methods. // NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk. Vue.util = { warn, extend, mergeOptions, defineReactive } //一般我們用例項方法而不是這三個類方法 Vue.set = set Vue.delete = del Vue.nextTick = nextTick // 注意這裡,迴圈出來的結果其實是三個 `components`,`directives`, `filters`,這裡先建立了空物件作為容器,後面如果有對應的外掛就會放進來。 Vue.options = Object.create(null) ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) // this is used to identify the "base" constructor to extend all plain-object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue // 內建元件只有一個,就是 `keepAlive` extend(Vue.options.components, builtInComponents) initUse(Vue) // 新增了 Vue.use 方法,可以註冊外掛 initMixin(Vue) //新增了Vue.mixin 方法 initExtend(Vue) // 新增了 Vue.extend 方法 // 這一步是註冊了 `Vue.component` ,`Vue.directive` 和 `Vue.filter` 三個方法,上面不是有 `Vue.options.components` 等空物件嗎,這三個方法的作用就是把註冊的元件放入對應的容器中。 initAssetRegisters(Vue) } |
至此,我們就構建出了一個 Vue
類,這個類上的方法都已經新增完畢。這裡再次強調一遍,這個階段只是新增方法而不是執行他們,具體執行他們是要到第二階段的。總結一下,我們建立的Vue類都包含了哪些內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
//建構函式 function Vue () { this._init() } //全域性config物件,我們幾乎不會用到 Vue.config = { keyCodes, _lifecycleHooks: ['beforeCreate', 'created', ...] } // 預設的options配置,我們每個元件都會繼承這個配置。 Vue.options = { beforeCreate, // 比如 vue-router 就會註冊這個回撥,因此會每一個元件繼承 components, // 前面提到了,預設元件有三個 `KeepAlive`,`transition`, `transitionGroup`,這裡註冊的元件就是全域性元件,因為任何一個元件中不用宣告就能用了。所以全域性元件的原理就是這麼簡單 directives, // 預設只有 `v-show` 和 `v-model` filters // 不推薦使用了 } //一些全域性方法 Vue.use // 註冊外掛 Vue.component // 註冊元件 Vue.directive // 註冊指令 Vue.nextTick //下一個tick執行函式 Vue.set/delete // 資料的修改操作 Vue.mixin // 混入mixin用的 //Vue.prototype 上有幾種不同作用的方法 //由initMixin 新增的 `_init` 方法,是Vue例項初始化的入口方法,會呼叫其他的功能初始話函式 Vue.prototype._init // 由 initState 新增的三個用來進行資料操作的方法 Vue.prototype.$data Vue.prototype.$props Vue.prototype.$watch // 由initEvents新增的事件方法 Vue.prototype.$on Vue.prototype.$off Vue.prototype.$one Vue.prototype.$emit // 由 lifecycle新增的生命週期相關的方法 Vue.prototype._update Vue.prototype.$forceUpdate Vue.prototype.$destroy //在 platform 中新增的生命週期方法 Vue.prototype.$mount // 由renderMixin新增的`$nextTick` 和 `_render` 以及一堆renderHelper Vue.prototype.$nextTick Vue.prototype._render Vue.prototype._b Vue.prototype._e //... |
上述就是我們的 Vue
類的全部了,有一些特別細小的點暫時沒有列出來,如果你在後面看程式碼的時候,發現有哪個函式不知道在哪定義的,可以參考這裡。那麼讓我們進入第二個階段:建立例項階段
第二階段:建立 Vue 例項
我們通過 new Vue(options)
來建立一個例項,例項的建立,肯定是從建構函式開始的,然後會進行一系列的初始化操作,我們依次看一下建立過程都進行了什麼初始化操作:
core/instance/index.js, 建構函式本身只進行了一個操作,就是呼叫 this._init(options)
進行初始化,這個在前面也提到過,這裡就不貼程式碼了。
core/instance/init.js 中會進行真正的初始化操作,讓我們詳細看一下這個函式具體都做了些什麼。
先看看它的完整程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } } |
我們來一段一段看看上面的程式碼分別作了什麼。
1 2 3 4 5 6 7 8 9 10 11 |
const vm: Component = this // vm 就是this的一個別名而已 // a uid vm._uid = uid++ // 唯一自增ID let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } |
這段程式碼首先生成了一個全域性唯一的id。然後如果是非生產環境並且開啟了 performance
,那麼會呼叫 mark
進行performance標記,這段程式碼就是開發模式下收集效能資料的,因為和Vue本身的執行原理無關,我們先跳過。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// a flag to avoid this being observed vm._isVue = true // merge options // // TODO if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { // mergeOptions 本身比較簡單,就是做了一個合併操作 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } |
上面這段程式碼,暫時先不用管_isComponent
,暫時只需要知道我們自己開發的時候使用的元件,都不是 _isComponent
,所以我們會進入到 else
語句中。這裡主要是進行了 options
的合併,最終生成了一個 $options
屬性。下一章我們會詳細講解 options
合併的時候都做了什麼,這裡我們只需要暫時知道,他是把建構函式上的options和我們建立元件時傳入的配置 options
進行了一個合併就可以了。正是由於合併了這個全域性的 options
所以我們在可以直接在元件中使用全域性的 directives
等
1 2 3 4 5 6 |
/* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } |
這段程式碼可能看起來比較奇怪,這個 renderProxy
是幹嘛的呢,其實就是定義了在 render
函式渲染模板的時候,訪問屬性的時候的一個代理,可以看到生產環境下就是自己。
開發環境下作了一個什麼操作呢?暫時不用關心,反正知道渲染模板的時候上下文就是 vm
也就是 this
就行了。如果有興趣可以看看非生產環境,作了一些友好的報錯提醒等。
這裡只需要記住,在生產環境下,模板渲染的上下文就是 vm
就行了。
1 2 3 4 5 6 7 8 9 10 11 |
// expose real self vm._self = vm initLifecycle(vm) // 做了一些生命週期的初始化工作,初始化了很多變數,最主要是設定了父子元件的引用關係,也就是設定了 `$parent` 和 `$children`的值 initEvents(vm) // 註冊事件,注意這裡註冊的不是自己的,而是父元件的。因為很明顯父元件的監聽器才會註冊到孩子身上。 initRender(vm) // 做一些 render 的準備工作,比如處理父子繼承關係等,並沒有真的開始 render callHook(vm, 'beforeCreate') // 準備工作完成,接下來進入 `create` 階段 initInjections(vm) // resolve injections before data/props initState(vm) // `data`, `props`, `computed` 等都是在這裡初始化的,常見的面試考點比如`Vue是如何實現資料響應化的` 答案就在這個函式中尋找 initProvide(vm) // resolve provide after data/props callHook(vm, 'created') // 至此 `create` 階段完成 |
這一段程式碼承擔了元件初始化的大部分工作。我直接把每一步的作用寫在註釋裡面了。 把這幾個函式都弄懂,那麼我們也就差不多弄懂了Vue的整個工作原理,而我們接下來的幾篇文章,其實都是從這幾個函式中的某一個開始的。
1 2 3 4 5 |
if (vm.$options.el) { vm.$mount(vm.$options.el) } } } |
開始mount,注意這裡如果是我們的options
中指定了 el
才會在這裡進行 $mount
,而一般情況下,我們是不設定 el
而是通過直接呼叫 $mount("#app")
來觸發的。比如一般我們都是這樣的:
1 2 3 4 5 6 |
new Vue({ router, store, i18n, render: h => h(App) }).$mount('#app') |
以上就是Vue例項的初始化過程。因為在 create
階段和 $mount
階段都很複雜,所以後面會分幾個章節來分別詳細講解。下一篇,讓我們從最神祕的資料響應化說起。