4

从在基于Vue-Cli的项目中,我们在main.js一般是这样使用Vue的

import Vue from 'vue';
import router from './router';
import store from './store';
import App from './App.vue';

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App)
});

我们new的这个Vue倒是是个啥?log看一下
image.png
image.png

所以我们new的是一个Vue对象的实例,它包含了图上那些属性和方法。那么这些实例上的属性和方法又是再哪里加上的呢

我们在new Vue的时候用chrome打个断点,用下面这个step into next function call的工具看看这个new Vue到底调用了什么方法
image.png
image.png

构造函数

我们首先通过全局搜索function Vue,我们找到真正Vue的构造函数,在vue/src/core/instance/index.js

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的构造函数,就做了一件事,执行自己的_init方法。但在执行init之前,我们log个一下这个Vue实例看看:

……
console.log(this);
this._init(options)
……

image.png
怎么就突然冒出来这么多奇奇怪怪的东西?这些在_init之前就存在的属性到底是什么时候加到我们这个Vue的原型上的?

各种mixin

首先我们把怀疑的目光放在下面这些mixin上,毕竟我们的_init既然没有在function Vue这个构造函数中申明,那肯定是从哪里加到原型上的。

initMixin

我们先来看看initMixin执行前后,Vue原型上的变化

console.log(Vue.prototype)
initMixin(Vue)
console.log(Vue.prototype)

image.png
所以,实际上initMinxin就在Vue的原型上挂了一个构造函数需要执行的_init方法。通过initMinxin函数的源码我们也可以印证这一点:

export function initMixin (Vue: Class<Component>) {
  // 对Vue扩展,实现_init实例方法
  Vue.prototype._init = function (options?: Object) {
    ……
  }
}

stateMixin

console.log(Vue.prototype)
stateMixin(Vue)
console.log(Vue.prototype)

image.png
stateMinix里做的都是一些跟响应式相关的勾当,从上图可以看到他们是$data$props两个属性;$set$delete$watch三个方法。源码如下:

export function stateMixin (Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._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)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    ……
  }
}

通过源码可以知道$data$props是只读属性,是通过Object.defineProperty来实现的。举个简单的例子,比如我们要直接暴力去修改$propsVue.prototype.$props = a;这时候就会触发propsDefset方法,会警告说$props is readonly

eventMixin

console.log(Vue.prototype)
stateMixin(Vue)
console.log(Vue.prototype)

image.png
eventMinix里做的都是一些事件相关的东西,从上图可以看到,挂载了$on$once$off$emit四个函数。

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  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.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
  }

  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
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    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
  }
}

从源码上我们可以看到,这几个函数其实都是在对vm实例上的_events这个数组在进行操作。而$on$once的区别也很清晰,$once的原理其实就是对$on执行的函数进行了封装,这个函数执行前会先将自己$off,从而达到只执行一次的目的。

lifecycleMixin

console.log(Vue.prototype)
lifecycleMixin(Vue)
console.log(Vue.prototype)

image.png
从上图可知,lifecycleMinix里并不是我们想象中的那些生命周期的钩子函数,他挂载了_update$forceUpdate$destroy这三个函数。

export function lifecycleMixin (Vue: Class<Component>) {
  // 更新函数
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    ……
  }

  // 强制更新
  Vue.prototype.$forceUpdate = function () {
    ……
  }

  // 销毁
  Vue.prototype.$destroy = function () {
    ……
  }
}

renderMixin

console.log(Vue.prototype)
renderMixin(Vue)
console.log(Vue.prototype)

image.png
从上图可以知道,renderMixin里,做的事情就比较多了,除了$nextTick_render这两个函数,installRenderHelpers方法还挂载了很多在render过程中需要用到的工具函数。

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
    ……
  }
}

我们看一下installRenderHelpers源码,可以看到他加上了都是一些基本的工具函数

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

来自上层的封装

在执行完上面5个minxin方法后,最后log出来的Vue原型上,与我们在this._init()执行之前所看到的Vue实例上的属性和方法还是有少了一些,比如$mount这个函数,这些东西又是啥时候加到原型上的呢?
既然当前这个/src/core/instance/index.js文件没有线索了,而且他最后还把Vue这个构造还是export了出去,所以我们需要看看有没有文件在外面import这个Vue,然后再加上一些骚操作。
我们搜索import Vue from这个关键字,除了test目录下的测试代码外,我们发现还有几个文件在import Vue
image.png

core/index

initGlobalAPI

在core/index里,首先会去初始化化全局API,即执行initGlobalAPI(Vue)这个函数,这个函数会给我们的Vue原型和构造函数里加很多东西。

const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

首先实在构造函数上加上了一个只读属性config,这个config里就是Vue的全局配置项
image.png

  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

然后就是util,里面包含了mergeOptions(来自src/core/util/options.js),defineReactive(来自src/core/observer/index.js),extend,(来自src/shared/util.js)warn(来自src/core/util/debug.js)
image.png

Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick

// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
  observe(obj)
  return obj
}

接下来给Vue构造函数上加入了setdelete方法(来自core/observer/index.js),nextTick方法(来自src/core/util/next-tick.js),而observable实在observe(来自core/observer/index.js)基础上进行了封装。
image.png

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

然后就是在Vue构造函数上加入option对象,里面有componentsdirectivesfilters_base。然后这个extend的作用是将keep-alive这个组件加入到上面的这个Vue.options.components中。

 initUse(Vue)
 initMixin(Vue)
 initExtend(Vue)
 initAssetRegisters(Vue)

最后就是初始化一下useminixextend。其中,在initExtend的时候,会给我们的根组件构造器加上唯一cid=0,以后通过Vue.extend构造组件实例的时候,也会给每个实例的构造器加上这个递增的cid。然后再在用最后的initAssetRegisters做一次代理,把options里面的componentsdirectivesfilters直接挂载到Vue的构造函数下面。
image.png

环境相关

接下来,在core/index里,去定义环境相关的一些属性。在Vue原型上定义了$isServer$ssrContext,看名字都知道是与服务端渲染ssr相关的东西。最后定义在构造函数上一个与函数式组件渲染上下文相关的FunctionalRenderContext

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})


Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})


// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

版本相关

Vue.version = '__VERSION__'
最后会在构造函数上加上我们的版本信息,在webpack打包的时候会替换成当前Vue的版本
const version = process.env.VERSION || require('../package.json').version

platforms/web/runtime/index

Vue.js 最初是为 Web 平台设计的,虽然可以基于 Weex 开发原生应用,但是 Web 开发和原生开发毕竟不同,在功能和开发体验上都有一些差异,这些差异从本质上讲是原生开发平台和 Web 平台之间的差异。因此,在platforms/web/runtime/index这一层,会加入一些与平台相关的属性与操作。

重写config

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

因为现在的运行环境已经变成了web平台,所以一些全局配置就不能再粗暴的直接给个默认值,而是要具体问题具体分析了,比如这isReservedTag,默认值是no的它,就要被重写成根据tag返回一个布尔值:

export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag)
}

安装平台的指令和组件

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

其中,web平台的指令有modelshow两个,组件有TransitionTransitionGroup两个,这些都和dom息息相关。

安装平台补丁函数

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

在Vue原型上的__patch__方法,是由一个工厂函数createPatchFunction返回的,实际上执行的是src/core/vdom/patch.js中第700的那个patch函数。

实现$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)
}

在Vue原型上的$mount方法,实际上执行的就是mountComponent这个方法,只是对el进行了一下处理。

web/entry-runtime-with-compiler.js

这个文件是webpack编译的入口文件,他主要做了两件事,第一是扩展$mount方法,第二个是挂载compile方法

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  ……
  // 最后执行的还是Vue.prototype.$mount
  return mount.call(this, el, hydrating)
}

Vue.compile = compileToFunctions

总结

我们这一次把Vue init之前所有挂载的属性和方法都总结了一遍,目的不是为了搞清楚每个属性的意义,每个方法实现的功能(这工作量太大)。而是为后面的源码工作打下基础,知道原型和构造函数上有哪些属性和方法,又是在哪里定义的,不会看到之后一脸懵逼。然后再通过具体的流程去看每个函数,每个属性究竟有什么用。最后我用一脑图总结了一下init前Vue原型和构造函数上的属性和方法,让我们接下来去看init过程时有的放矢。
image
image


FrankChencc
214 声望9 粉丝