Vue源碼解讀(入口到構造函數總體流程)

總體流程

在以前的介紹中,咱們知道Vue.js內部會根據Web瀏覽器Weex跨平臺和SSR服務端渲染不一樣的環境尋找不一樣的入口文件,但其核心代碼是在src/core目錄下,咱們這一篇文章的主要目的是爲了搞清楚從入口文件到Vue構造函數執行,這期間的總體流程。vue

在分析完從入口到構造函數的各個部分的流程後,咱們能夠獲得一份大的流程圖:git

img

initGlobalAPI流程

咱們會在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

initGlobalAPI

咱們首先能在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函數,其中setdeletenextTickobservable直接賦值爲一個函數,而其餘幾種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()方法的邏輯,能夠獲得以下流程圖: initGlobalAPI流程圖app

initMixin流程

在上一目錄咱們講到了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:數據狀態相關,例如:dataprops以及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文件很是直觀的代碼邏輯流程圖:

instance流程

接下來咱們的首要任務是弄清楚_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的總體流程圖。

initMixin流程圖

stateMixin流程

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$deletesetdelete這兩個方法被定義在跟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以下流程圖:

stateMinxin流程圖

eventsMixin流程

在使用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的實現

$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
}
複製代碼

代碼分析:

  1. 咱們發如今Vue源碼中,$on方法還接受一個數組event,這實際上是在Vue2.2.0版本之後纔有的,當傳遞一個event數組時,會經過遍歷數組的形式遞歸調用$on方法。
  2. 咱們還發現,全部$on的事件所有綁定在_events私有屬性上,這個屬性實際上是在咱們上面已經提到過的initEvents()方法中被定義的。
export function initEvents (vm) {
  vm._events = Object.create(null)
}
複製代碼

$emit的實現

咱們先來實現一個簡單的$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
}
複製代碼

代碼分析:

  1. 從總體上看,$emit實現方法很是簡單,第一步從_events對象中取出對應的cbs,接着一個個遍歷cbs數組、調用並傳參。
  2. invokeWithErrorHandling代碼中會使用try/catch把咱們函數調用並執行的地方包裹起來,當函數調用出錯時,會執行VuehandleError()方法,這種作法不只更加友好,並且對錯誤處理也很是有用。

$off的實現

$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的實現

關於$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以下流程圖:

eventMinxin流程圖

lifecycleMixin流程

和以上其它幾種方法同樣,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()方法的代碼不是不少,咱們也能很容易的獲得以下流程圖:

lifecycleMinxin流程圖

renderMixin流程

相比於以上幾種方法,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 = toNumberthis._s = toStringthis._v = createTextVNodethis._e = createEmptyVNode
  • _render()_render()方法會把模板編譯成VNode,咱們會在其後的編譯章節詳細介紹。
  • nextTick:就像咱們以前介紹過的,nextTick會在Vue構造函數上掛載一個全局的nextTick()方法,而此處爲實例方法,本質上引用的是同一個nextTick

在以上代碼分析完畢後,咱們能夠獲得renderMixin以下流程圖:

renderMinxin流程圖

相關文章
相關標籤/搜索