Vue构造函数

902 阅读3分钟

Vue应用从new Vue开始,构造函数接收一个包含组件选项的对象为参数,其中有一个render方法用于将根组件渲染为vnode;再调用$mount函数,该函数接受一个DOM节点,这样就成功的创建了一个Vue应用了。

import Vue from 'vue';
import App from './App';
new Vue({
    render: h => h(App)
}).$mount('#root');

new Vue时构造函数内部做了什么?App又是一个什么?先看下Vue构造函数是什么。

Vue构造函数

由于Vue是一个支持多平台的JavaScript框架,不同平台有着不同的渲染机制,所以Vue在源码的目录结构设计上分为多个模块,首先将核心逻辑放在src/core目录下维护,而web平台的逻辑维护在src/platforms/web下,模板编译的核心逻辑维护在src/compiler,web相关的编译逻辑维护在src/platforms/web/compiler内。所以web应用程序入口文件导入的Vue是从platforms/web导出的并且对web端进行了特有的功能扩展。

web平台

web平台的代码维护在src/platforms/web目录下,该目录有多个入口文件。entry-runtime-with-compiler.js是包含compilerruntime的入口,而entry-runtime.js是仅包含runtime的入口,引用这个文件导出的Vue构造函数时,需使用vue-loader先将template转换成render函数(本篇从runtime的Vue说起)。

web.png

上图是源码目录的截图,entry-runtime仅作为统一出口,web端的逻辑还是在platforms/web/runtime/index.js中实现。先看下platforms/web/runtime/index.js源码

// platforms/web/runtime/index.js
import Vue from 'core/index'
import { patch } from './patch'
import { mountComponent } from 'core/instance/lifecycle'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
Vue.prototype.__patch__ = inBrowser ? patch : noop

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

以上代码做了以下三件事:

  1. 将web端的指令(v-model, v-show)和内置组件transition,transition-group混合到Vue.options.directivesVue.options.components对象内
  2. 在浏览器环境中定义了渲染dom的patch方法
  3. 定义了渲染到dom的入口$mount方法

$mount方法接受一个DOM字符串或DOM节点,调用mountComponent并返回,该参数接受三个参数

  • this Vue根实例对象
  • el Vue应用程序挂载点
  • hydrating ssr相关

core

从web端的runtime/index.js中并没有看到Vue构造函数的实现而是从src/core/index.js中引入,src/core/index.js代码如下(但此处任不是Vue构造函数定义的地方)

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

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__'

export default Vue

核心逻辑入口对Vue构造函数也进行了一次扩展,如原型对象、Vue构造函数对象(函数也是对象)的扩展。

原型对象扩展

  • 添加$isServer属性,用于获取当前是否是在服务端环境
  • 添加$ssrContext属性,用于获取ssrContext上下文

Vue静态属性扩展

initGlobalAPI函数接受Vue构造函数作为入参,并在函数内部对Vue构造函数对象做扩展,主要是给Vue构造函数添加一下静态方法和属性。

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

export function initGlobalAPI (Vue) {
  // config
  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)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

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

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

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

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

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

以上代码可以看到initGlobalAPI内部将一些常用的全局API添加到Vue对象上

  • Vue.set 向响应式对象中添加一个属性,并确保这个属性也同样是响应式的
  • Vue.delete 响应式对象被删除属性时,确保能触发更新视图(由于Vue响应式系统不能检测到属性被删除)
  • Vue.observable 手动将一个非响应式对象转换成响应式对象
  • Vue.use 安装一个Vue插件
  • Vue.mixin 全局注册一个混入,混入会被合并到vm.$options内,所以会影响所有创建的Vue实例
  • Vue.extend 通过基础Vue构造器,创建子构造函数VueComponent,参数是一个包含组件选项的对象
  • Vue.component 创建或获取一个全局子构造函数(组件),创建时被添加到vm.options.components对象中
  • Vue.directive 创建或获取一个全局指令,创建时,被添加到vm.options.directives对象中
  • Vue.filter 创建或获取一个全局过滤器,创建时,被添加到vm.options.filters对象中

同时创建了Vue.options空对象,并在Vue.options中创建了components,directives,filters,_base属性,这些options最终会以$options对象挂载到实例上。

_base在构造子构造函数时被使用;Vue.options.components内添加了一个内置组件keep-alive,这些组件都是全局注册的组件,可以在子组件中直接使用而无需引入和局部注册。以下代码就是构造Vue.options对象的实现。

// ASSET_TYPES = ['component', 'directive', 'filter']
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)

core/runtime

src/core/index文件中可以看到Vue是从./instance/index.js文件中引入,所以真正定义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构造函数仅仅是一个接受options的普通函数,内部调用了this._init方法。之前看到的Vue构造函数的扩展都没有_init实例方法,所以_init应该是在core/runtime中实现的。接下来看Vue构造函数下方的几个函数调用。

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
  • initMixin定义了Vue.prototype._init方法,该方法既时创建实例时调用的函数,初始化实例的一些属性与方法
  • stateMixin定义了Vue.prototype.$setVue.prototype.$deleteVue.prototype.$watch方法和Vue.prototype.$dataVue.prototype.$props属性,都是与Vue实例的状态相关
  • eventMixin定义了Vue.prototype.$onVue.prototype.$emitVue.prototype.$onceVue.prototype.$off方法,实现了Vue自定义事件系统
  • lifecycleMixin定义了Vue.prototype._updateVue.prototype.$forceUpdateVue.prototype.$destory方法,实现了Vue实例的更新与卸载
  • renderMixin定义了Vue.prototype._renderVue.prototype.$nextTick方法,用于创建vnode

所以在我们web应用程序中导入的Vue时,它是一个扩展了很多原型属性和方法的构造函数:

Vue-constructor.png 原型对象方法属性见下图

vue-prototype.png

回答开头的问题:new Vue时构造函数内部做了什么?App又是一个什么?

1、new Vue时创建了一个Vue实例,Vue构造函数内部调用了Vue.prototype._init方法,该方法完成了实例属性与方法的初始化,若提供了el属性,会调用$mount方法完成整个渲染流程。

2、App是一个组件选项的对象,h(App)内部会将App构造成一个VueComponent子类构造函数,后续流程会将其创建为一个组件实例对象。

总结

Vue构造函数利用了原型链继承,将初始化实例方法、实例状态操作方法、自定义事件方法、vnode渲染与更新操作方法挂载在原型上这样所有实例都继承这些方法。同时将一些参数配置与全局方法添加到Vue对象上。