vue2.x源碼解析系列二:Vue組件初始化過程概要

NO IMAGE

寫在前面的話:關於作者言川

筆名言川, 前端工程師,精通 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 的源碼,如果你想一遍看一遍參閱源碼,請務必記得切換到此版本,不然可能存在微小的差異。

vue2.x源碼解析系列二:Vue組件初始化過程概要

大家都知道,我們的應用是一個由Vue組件構成的一棵樹,其中每一個節點都是一個 Vue 組件。我們的每一個Vue組件是如何被創建出來的,創建的過程經歷了哪些步驟呢?把這些都搞清楚,那麼我們對Vue的整個原理將會有很深入的理解。

從入口函數開始,有比較複雜的引用關係,為了方便大家理解,我畫了一張圖可以直觀地看出他們之間的關係:

vue2.x源碼解析系列二:Vue組件初始化過程概要

創建Vue實例的兩步

我們創建一個Vue實例,只需要兩行代碼:

import Vue from ‘vue'
new Vue(options)

而這兩步分別經歷了一個比較複雜的構建過程:

  1. 創建類:創建一個 Vue 構造函數,以及他的一系列原型方法和類方法
  2. 創建實例:創建一個 Vue 實例,初始化他的數據,事件,模板等
    下面我們分別解析這兩個階段,其中每個階段 又分為好多個 步驟

第一階段:創建Vue類

第一階段是要創建一個Vue類,因為我們這裡用的是原型而不是ES6中的class聲明,所以拆成了三步來實現:

  1. 創建一個構造函數 Vue
  2. Vue.prototype 上創建一系列實例屬性方法,比如 this.$data
  3. 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 上添加了一些方法,這裡暫時先不一個個貼代碼,總結一下如下:

  1. core/instance/state.js,主要是添加了 $data,$props,$watch,$set,$delete 幾個屬性和方法
  2. core/instance/events.js,主要是添加了 $on,$off,$once,$emit 三個方法
  3. core/instance/lifecycle.js,主要添加了 _update, $forceUpdate, $destroy 三個方法
  4. 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 階段都很複雜,所以後面會分幾個章節來分別詳細講解。下一篇,讓我們從最神祕的數據響應化說起。

下一篇:Vue2.x源碼解析系列三:Options配置的處理

相關文章

體驗WebAssembly

Vue2.x源碼解析系列七:深入Compiler理解render函數的生成過程

Vue2.x源碼解析系列五:數據響應之Watcher

Vue2.x源碼解析系列四:數據響應之Observer