Vue源码解读(入口到构造函数整体流程)__Vue.js
发布于 10 天前 作者 banyungong 157 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

整体流程

在之前的介绍中,我们知道Vue.js内部会根据Web浏览器Weex跨平台和SSR服务端渲染不同的环境寻找不同的入口文件,但其核心代码是在src/core目录下,我们这一篇文章的主要目的是为了搞清楚从入口文件到Vue构造函数执行,这期间的整体流程。

在分析完从入口到构造函数的各个部分的流程后,我们可以得到一份大的流程图:

img

initGlobalAPI流程

我们会在src/core/index.js文件中看到如下精简代码:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)

export default Vue

在以上代码中,我们发现它引入了Vue随后调用了initGlobalAPI()函数,此函数的作用是挂载一些全局API方法。

initGlobalAPI

我们首先能在src/core/global-api文件夹下看到如下目录结构:

|-- global-api        
|   |-- index.js      # 入口文件
|   |-- assets.js     # 挂载filter、component和directive
|   |-- extend.js     # 挂载extend方法
|   |-- mixin.js      # 挂载mixin方法
|   |-- use.js        # 挂载use方法

随后在index.js入口文件中,我们能看到如下精简代码:

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中被定义:

import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'
Vue.compile = compileToFunctions
export default Vue

因此我们根据initGlobalAPI()方法的逻辑,可以得到如下流程图: initGlobalAPI流程图

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流程图

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: Husky-Yellow 原文链接:https://juejin.im/post/6929008264906539022

回到顶部