从在基于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看一下

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

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

构造函数

我们首先通过全局搜索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)……


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

各种mixin

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

initMixin

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

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


所以,实际上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)


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)


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)


从上图可知,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)


从上图可以知道,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

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的全局配置项

  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)

Vue.set = setVue.delete = delVue.nextTick = nextTick// 2.6 explicit observable APIVue.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)基础上进行了封装。

  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的构造函数下面。

环境相关

接下来,在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 installationObject.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 utilsVue.config.mustUseProp = mustUsePropVue.config.isReservedTag = isReservedTagVue.config.isReservedAttr = isReservedAttrVue.config.getTagNamespace = getTagNamespaceVue.config.isUnknownElement = isUnknownElement

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

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

安装平台的指令和组件

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

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

安装平台补丁函数

// install platform patch functionVue.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 methodVue.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.$mountVue.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过程时有的放矢。