寫在前面的話:關於作者言川
筆名言川, 前端工程師,精通 Vue/Webpack/Git
等,熟悉Node/React
等,涉獵廣泛,對算法/後端/人工智能/linux等都有一定研究。開源愛好者,github上目前總計5000+ Star。
- 我的github主頁:https://github.com/lihongxun945
- 我的博客地址:https://github.com/lihongxun945/myblog
- 我的掘金主頁:https://juejin.im/user/1398234518660551/posts
- 我的知乎專欄:https://zhuanlan.zhihu.com/c_1007281871281090560
此博客原地址:https://github.com/lihongxun945/myblog/issues/23
這裡分析的是當前(2018/07/25)最新版 V2.5.16
的源碼,如果你想一遍看一遍參閱源碼,請務必記得切換到此版本,不然可能存在微小的差異。

大家都知道,我們的應用是一個由Vue組件構成的一棵樹,其中每一個節點都是一個 Vue 組件。我們的每一個Vue組件是如何被創建出來的,創建的過程經歷了哪些步驟呢?把這些都搞清楚,那麼我們對Vue的整個原理將會有很深入的理解。
從入口函數開始,有比較複雜的引用關係,為了方便大家理解,我畫了一張圖可以直觀地看出他們之間的關係:
創建Vue實例的兩步
我們創建一個Vue實例,只需要兩行代碼:
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
import Vue from './runtime/index'
export default Vue
可以看到,這裡不是 Vue 構造函數的定義地方,而是返回了從下面一步得到的Vue構造函數,但是做了一些平臺相關的操作,比如內置 directives 註冊等。這裡就會有人問了,為什麼不直接定義一個構造函數,而是這樣不停的傳遞呢?因為 vue 有不同的運行環境,而每一個環境又有帶不帶 compiler
等不同版本,所以環境的不同以及版本的不同都會導致 Vue
類會有一些差異,那麼這裡會通過不同的步驟來處理這些差異,而所有的環境版本都要用到的核心代碼是相同的,因此這些相同的代碼就統一到 core/
中了。
完整代碼和我加的註釋如下:
platforms/web/runtime/index.js
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
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 完整代碼如下:
// 省略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
下面我們分成兩段來講解這些代碼分別幹了什麼。
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初始化必定有很多工作要做,比如數據的響應化、事件的綁定等,在第二階段我們會詳細講解這個函數到底做了什麼。這裡我們暫且跳過它。
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
上面這五個函數其實都是在Vue.prototype
上添加了一些屬性方法,讓我們先找一個看看具體的代碼,比如initMixin
就是添加 _init
函數,沒錯正是我們構造函數中調用的那個 this._init(options)
哦,它裡面主要是調用其他的幾個初始化方法,因為比較簡單,我們直接看代碼:
core/instance/init.js
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
來添加的,那麼我們直接看看這個函數的樣子:
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類都包含了哪些內容:
//構造函數
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 中會進行真正的初始化操作,讓我們詳細看一下這個函數具體都做了些什麼。
先看看它的完整代碼:
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)
}
}
我們來一段一段看看上面的代碼分別作了什麼。
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本身的運行原理無關,我們先跳過。
// 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
等
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
這段代碼可能看起來比較奇怪,這個 renderProxy
是幹嘛的呢,其實就是定義了在 render
函數渲染模板的時候,訪問屬性的時候的一個代理,可以看到生產環境下就是自己。
開發環境下作了一個什麼操作呢?暫時不用關心,反正知道渲染模板的時候上下文就是 vm
也就是 this
就行了。如果有興趣可以看看非生產環境,作了一些友好的報錯提醒等。
這裡只需要記住,在生產環境下,模板渲染的上下文就是 vm
就行了。
// 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的整個工作原理,而我們接下來的幾篇文章,其實都是從這幾個函數中的某一個開始的。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
開始mount,注意這裡如果是我們的options
中指定了 el
才會在這裡進行 $mount
,而一般情況下,我們是不設置 el
而是通過直接調用 $mount("#app")
來觸發的。比如一般我們都是這樣的:
new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount('#app')
以上就是Vue實例的初始化過程。因為在 create
階段和 $mount
階段都很複雜,所以後面會分幾個章節來分別詳細講解。下一篇,讓我們從最神祕的數據響應化說起。