在以前的介紹中,咱們知道Vue.js
內部會根據Web瀏覽器
、Weex
跨平臺和SSR服務端渲染
不一樣的環境尋找不一樣的入口文件,但其核心代碼是在src/core
目錄下,咱們這一篇文章的主要目的是爲了搞清楚從入口文件到Vue
構造函數執行,這期間的總體流程。vue
在分析完從入口到構造函數的各個部分的流程後,咱們能夠獲得一份大的流程圖:git
咱們會在src/core/index.js
文件中看到以下精簡代碼:github
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue
複製代碼
在以上代碼中,咱們發現它引入了Vue
隨後調用了initGlobalAPI()
函數,此函數的做用是掛載一些全局API
方法。web
咱們首先能在src/core/global-api
文件夾下看到以下目錄結構:express
|-- global-api
| |-- index.js # 入口文件
| |-- assets.js # 掛載filter、component和directive
| |-- extend.js # 掛載extend方法
| |-- mixin.js # 掛載mixin方法
| |-- use.js # 掛載use方法
複製代碼
隨後在index.js
入口文件中,咱們能看到以下精簡代碼:api
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { observe } from 'core/observer/index'
import { extend, nextTick } from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
Vue.observable = (obj) => {
observe(obj)
return obj
}
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
複製代碼
咱們能從以上代碼很清晰的看到在index.js
入口文件中,會在Vue
構造函數上掛載各類全局API
函數,其中set
、delete
、nextTick
和observable
直接賦值爲一個函數,而其餘幾種API
則是調用了一個以init
開頭的方法,咱們以initAssetRegisters()
方法爲例,它的精簡代碼以下:數組
// ['component','directive', 'filter']
import { ASSET_TYPES } from 'shared/constants'
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function () {
// 省略了函數的參數和函數實現代碼
}
})
}
複製代碼
其中ASSET_TYPES
是一個定義在src/shared/constants.js
中的一個數組,而後在initAssetRegisters()
方法中遍歷這個數組,依次在Vue
構造函數上掛載Vue.component()
、Vue.directive()
和Vue.filter()
方法,另外三種init
開頭的方法調用掛載對應的全局API
是同樣的道理:瀏覽器
// initUse
export function initUse(Vue) {
Vue.use = function () {}
}
// initMixin
export function initMixin(Vue) {
Vue.mixin = function () {}
}
// initExtend
export function initExtend(Vue) {
Vue.extend = function () {}
}
複製代碼
最後,咱們發現還差一個Vue.compile()
方法,它實際上是在runtime+compile
版本纔會有的一個全局方法,所以它在src/platforms/web/entry-runtime-with-compile.js
中被定義:markdown
import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'
Vue.compile = compileToFunctions
export default Vue
複製代碼
所以咱們根據initGlobalAPI()
方法的邏輯,能夠獲得以下流程圖: app
在上一目錄咱們講到了initGlobalAPI
的總體流程,這一,咱們來介紹initMixin
的總體流程。首選,咱們把目光回到src/core/index.js
文件中:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue
複製代碼
咱們發現,它從別的模塊中引入了大Vue
,那麼接下來咱們的首要任務就是揭開Vue
構造函數的神祕面紗。
在看src/core/instance/index.js
代碼以前,咱們發現instance
目錄結構以下:
|-- instance
| |-- render-helpers # render渲染相關的工具函數目錄
| |-- events.js # 事件處理相關
| |-- init.js # _init等方法相關
| |-- inject.js # inject和provide相關
| |-- lifecycle.js # 生命週期相關
| |-- proxy.js # 代理相關
| |-- render.js # 渲染相關
| |-- state.js # 數據狀態相關
| |-- index.js # 入口文件
複製代碼
能夠看到,目錄結構文件有不少,並且包含的面也很是雜,但咱們如今只須要對咱們最關心的幾個部分作介紹:
events.js
:處理事件相關,例如:$on
,$off
,$emit
以及$once
等方法的實現。init.js
:此部分代碼邏輯包含了Vue
從建立實例到實例掛載階段的全部主要邏輯。lifecycle.js
:生命週期相關,例如:$destroy
、$activated
和$deactivated
。state.js
:數據狀態相關,例如:data
、props
以及computed
等。render.js
:渲染相關,其中最值得關注的是Vue.prototype._render
渲染函數的定義。在介紹了instance
目錄結構的及其各自的做用之後,咱們再來看入口文件,其實入口文件這裏纔是Vue
構造函數廬山真面目:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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
複製代碼
代碼分析:
Vue
構造函數其實就是一個普通的函數,咱們只能經過new
操做符進行訪問,既new Vue()
的形式,Vue
函數內部也使用了instanceof
操做符來判斷實例的父類是否爲Vue
構造函數,不是的話則在開發環境下輸出一個警告信息。Vue
構造函數,這部分的代碼也調用了幾種mixin
方法,其中每種mixin
方法各司其職,處理不一樣的內容。從以上代碼中,咱們能獲得src/core/instance/index.js
文件很是直觀的代碼邏輯流程圖:
接下來咱們的首要任務是弄清楚_init()
函數的代碼邏輯以及initMixin
的總體流程。咱們從上面的代碼發現,在構造函數內部會調用this._init()
方法,也就是說:
// 實例化時,會調用this._init()方法。
new Vue({
data: {
msg: 'Hello, Vue.js'
}
})
複製代碼
而後,咱們在init.js
中來看initMixin()
方法是如何被定義的:
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
// 省略代碼
}
}
複製代碼
咱們能夠發現,initMixin()
方法的主要做用就是在Vue.prototype
上定義一個_init()
實例方法,接下來咱們來看一下_init()
函數的具體實現邏輯:
Vue.prototype._init = function (options) {
const vm = this
// 1. 合併配置
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 2.render代理
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 3.初始化生命週期、初始化事件中心、初始化inject,
// 初始化state、初始化provide、調用生命週期
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// 4.掛載
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製代碼
由於咱們是要分析initMixin
總體流程,對於其中某些方法的具體實現邏輯會在後續進行詳細的說明,所以咱們能夠從以上代碼獲得initMixin
的總體流程圖。
stateMixin
主要是處理跟實例相關的屬性和方法,它會在Vue.prototype
上定義實例會使用到的屬性或者方法,這一節咱們主要任務是弄清楚stateMixin
的主要流程。在src/core/instance/state.js
代碼中,它精簡後以下所示:
import { set, del } from '../observer/index'
export function stateMixin (Vue) {
// 定義$data, $props
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
// 定義$set, $delete, $watch
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function() {}
}
複製代碼
咱們能夠從上面代碼中發現,stateMixin()
方法中在Vue.prototype
上定義的幾個屬性或者方法,所有都是和響應式相關的,咱們來簡要分析一下以上代碼:
$data和$props
:根據以上代碼,咱們發現$data
和$props
分別是_data
和_props
的訪問代理,從命名中咱們能夠推測,如下劃線開頭的變量,咱們通常認爲是私有變量,而後經過$data
和$props
來提供一個對外的訪問接口,雖然能夠經過屬性的get()
方法去取,但對於這兩個私有變量來講是並不能隨意set
,對於data
來講不能替換根實例,而對於props
來講它是隻讀的。所以在原版源碼中,還劫持了set()
方法,當設置$data
或者$props
時會報錯:if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
複製代碼
$set
和$delete
:set
和delete
這兩個方法被定義在跟instance
目錄平級的observer
目錄下,在stateMixin()
中,它們分別賦值給了$set
和$delete
方法,而在initGlobalAPI
中,也一樣使用到了這兩個方法,只不過一個是全局方法,一個是實例方法。$watch
:在stateMixin()
方法中,詳細實現了$watch()
方法,此方法實現的核心是經過一個watcher
實例來監聽。當取消監聽時,一樣是使用watcher
實例相關的方法,關於watcher
咱們會在後續響應式章節詳細介紹。Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function () {
watcher.teardownunwatchFn()
}
}
複製代碼
在以上代碼分析完畢後,咱們能夠獲得stateMixin
以下流程圖:
在使用Vue
作開發的時候,咱們必定常用到$emit
、$on
、$off
和$once
等幾個實例方法,eventsMixin
主要作的就是在Vue.prototype
上定義這四個實例方法:
export function eventsMixin (Vue) {
// 定義$on
Vue.prototype.$on = function (event, fn) {}
// 定義$once
Vue.prototype.$once = function (event, fn) {}
// 定義$off
Vue.prototype.$off = function (event, fn) {}
// 定義$emit
Vue.prototype.$emit = function (event) {}
}
複製代碼
經過以上代碼,咱們發現eventsMixin()
所作的事情就是使用發佈-訂閱模式來處理事件,接下來讓咱們先使用發佈-訂閱實現本身的事件中心,隨後再來回顧源碼。
$on
方法的實現比較簡單,咱們先來實現一個基礎版本的:
function Vue () {
this._events = Object.create(null)
}
Vue.prototype.$on = function (event, fn) {
if (!this._events[event]) {
this._events[event] = []
}
this._events[event].push(fn)
return this
}
複製代碼
接下來對比一下Vue
源碼中,關於$on
的實現:
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
複製代碼
代碼分析:
Vue
源碼中,$on
方法還接受一個數組event
,這實際上是在Vue2.2.0
版本之後纔有的,當傳遞一個event
數組時,會經過遍歷數組的形式遞歸調用$on
方法。$on
的事件所有綁定在_events
私有屬性上,這個屬性實際上是在咱們上面已經提到過的initEvents()
方法中被定義的。export function initEvents (vm) {
vm._events = Object.create(null)
}
複製代碼
咱們先來實現一個簡單的$emit
方法:
Vue.prototype.$emit = function (event) {
const cbs = this._events[event]
if (cbs) {
const args = Array.prototype.slice.call(arguments, 1)
for (let i = 0; i < cbs.length; i++) {
const cb = cbs[i]
cb && cb.apply(this, args)
}
}
return this
}
複製代碼
接下來,咱們使用$emit
和$on
來配合測試事件的監聽和觸發:
const app = new Vue()
app.$on('eat', (food) => {
console.log(`eating ${food}!`)
})
app.$emit('eat', 'orange')
// eating orange!
複製代碼
最後咱們來看Vue
源碼中關於$emit
的實現:
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
// ...省略處理邊界代碼
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
複製代碼
代碼分析:
$emit
實現方法很是簡單,第一步從_events
對象中取出對應的cbs
,接着一個個遍歷cbs
數組、調用並傳參。invokeWithErrorHandling
代碼中會使用try/catch
把咱們函數調用並執行的地方包裹起來,當函數調用出錯時,會執行Vue
的handleError()
方法,這種作法不只更加友好,並且對錯誤處理也很是有用。$off
方法的實現,相對來講比較複雜一點,由於它須要根據不一樣的傳參作不一樣的事情:
event
參數時,只移除此event
對應的監聽器。event
參數和fn
回調,則只移除此event
對應的fn
這個監聽器。在瞭解了以上功能點後,咱們來實現一個簡單的$off
方法:
Vue.prototype.$off = function (event, fn) {
// 沒有傳遞任何參數
if (!arguments.length) {
this._events = Object.create(null)
return this
}
// 傳遞了未監聽的event
const cbs = this._events[event]
if (!cbs) {
return this
}
// 沒有傳遞fn
if (!fn) {
this._events[event] = null
return this
}
// event和fn都傳遞了
let i = cbs.length
let cb
while (i--) {
cb = cbs[i]
if (cb === fn) {
cbs.splice(i, 1)
break
}
}
return this
}
複製代碼
接下來,咱們撰寫測試代碼:
const app = new Vue()
function eatFood (food) {
console.log(`eating ${food}!`)
}
app.$on('eat', eatFood)
app.$emit('eat', 'orange')
app.$off('eat', eatFood)
// 不執行回調
app.$emit('eat', 'orange')
複製代碼
最後咱們來看Vue
源碼中關於$off
的實現:
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
複製代碼
關於$once
方法的實現比較簡單,能夠簡單的理解爲在回調以後立馬調用$off
,所以咱們來實現一個簡單的$once
方法:
Vue.prototype.$once = function (event, fn) {
function onFn () {
this.$off(event, onFn)
fn.apply(this, arguments)
}
this.$on(event, onFn)
return this
}
複製代碼
接着咱們對比一下Vue
源碼中的$once
方法:
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
複製代碼
注意:在源碼中$once
的實現是在回調函數中使用fn
綁定了原回調函數的引用,在上面已經提到過的$off
方法中也一樣進行了cb.fn === fn
的判斷。
在實現完以上幾種方法後,咱們能夠獲得eventsMixin
以下流程圖:
和以上其它幾種方法同樣,lifecycleMixin
主要是定義實例方法和生命週期,例如:$forceUpdate()
、$destroy
,另外它還定義一個_update
的私有方法,其中$forceUpdate()
方法會調用它,所以lifecycleMixin
精簡代碼以下:
export function lifecycleMixin (Vue) {
// 私有方法
Vue.prototype._update = function () {}
// 實例方法
Vue.prototype.$forceUpdate = function () {
if (this._watcher) {
this._watcher.update()
}
}
Vue.prototype.$destroy = function () {}
}
複製代碼
代碼分析:
_update()
會在組件渲染的時候調用,其具體的實現咱們會在組件章節詳細介紹$forceUpdate()
爲一個強制Vue
實例從新渲染的方法,它的內部調用了_update
,也就是強制組件重選編譯掛載。$destroy()
爲組件銷燬方法,在其具體的實現中,會處理父子組件的關係,事件監聽,觸發生命週期等操做。lifecycleMixin()
方法的代碼不是不少,咱們也能很容易的獲得以下流程圖:
相比於以上幾種方法,renderMixin
是最簡單的,它主要在Vue.prototype
上定義各類私有方法和一個很是重要的實例方法:$nextTick
,其精簡代碼以下:
export function renderMixin (Vue) {
// 掛載各類私有方法,例如this._c,this._v等
installRenderHelpers(Vue.prototype)
Vue.prototype._render = function () {}
// 實例方法
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
}
}
複製代碼
代碼分析:
installRenderHelpers
:它會在Vue.prototype
上掛載各類私有方法,例如this._n = toNumber
、this._s = toString
、this._v = createTextVNode
和this._e = createEmptyVNode
。_render()
:_render()
方法會把模板編譯成VNode
,咱們會在其後的編譯章節詳細介紹。nextTick
:就像咱們以前介紹過的,nextTick
會在Vue
構造函數上掛載一個全局的nextTick()
方法,而此處爲實例方法,本質上引用的是同一個nextTick
。在以上代碼分析完畢後,咱們能夠獲得renderMixin
以下流程圖: