引子
在《Vue2 源码构建》一文中,一步一步地讲解 Vue2 是如何打包构建的,最终构建出不同用途的 Vue 版本。当使用 vue-cli4+ 初始化项目时,默认使用的是 Runtime Only Vue 版本;如果项目需要使用 Runtime + Compiler 版本,则可在 vue.config.js 配置文件中设置属性:runtimeCompile 为 true 即可。
那么当在 main.js 引入 Vue,即 import Vue from 'vue' 时,项目是如何找到 Vue 呢?
这得归功于 Webpack,使用命令 vue inspect 导出配置,可以看到:
resolve: {
alias: {
vue$: 'vue/dist/vue.runtime.esm.js'
}
},
其实,Vue 只是一个别名,最终引入的是位于 vue/dist 目录下文件;那么如果是 Runtime + Compiler Vue 版本,其别名设置如下:
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
}
},
对于 Vue2 源码的分析,是基于 Runtime + Compiler 版本来分析。因此,对于 vue/dist/vue.esm.js 产物,其实是由文件 src/platforms/web/entry-runtime-with-compiler.js 打包而生成的;也就是说,它就是引入 Vue 的入口,下面将详细分析该入口的具体实现。
对于入口文件,将其逻辑实现整理成一张流程图,如下:
下面将根据流程图一步一步地分析入口文件的具体实现,整体上以主线为主,对于细节稍微展开,更详细的后续文章会讲到。
逻辑分析
从流程图上可看出,实现 Vue 涉及到的主要文件有 4 个,先看入口文件:src/platforms/web/entry-runtime-with-compiler.js ,前几行代码是引入各种依赖,其中一行比较重要的代码:
import Vue from './runtime/index'
跳转到文件 src/platforms/web/runtime/index ,第一行代码:
import Vue from 'core/index'
也是导入 Vue,继续跳转到文件 src/core/index,第一行代码也是导入 Vue:
import Vue from './instance/index'
跳转到文件 src/core/instance/index ,具体逻辑实现如下:
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 的庐山真面目。实际上,Vue 是用 Function 实现的类,我们只能使用 new Vue 去实例化它。首先,定义函数 Vue,基于函数在 JavaScript 也是一种对象,在 Vue 的 prototype 上进行扩展。扩展的功能如下:
initMixin
在 Vue 原型上,即 Vue.prototype 定义方法:_init,在函数 Vue 的实现中会调用到。
stateMixin
在 Vue 原型上定义属性:$data,$props,$set,$delete、$watch。
eventsMixin
在 Vue 原型上定义属性:$on、$off、$once、$emit。
lifecycleMixin
在 Vue 原型上定义属性:_update、$forceUpdate、$destroy。
renderMixin
在 Vue 原型上定义属性:$nextTick、_render。
最终将 Vue 导出,那么该文件就分析完了,回到文件:src/core/index,所实现的功能:
- 初始化全局 API(
initGlobalAPI(Vue)) - 在 Vue 原型上定义属性:
$isServer、$ssrContext - 在 Vue 上定义属性:
FunctionalRenderContext、version - 导出 Vue
来看下 initGlobalAPI(Vue) 具体实现:
export function initGlobalAPI (Vue: GlobalAPI) {
// 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 = <T>(obj: T): T => {
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)
}
主要在 Vue 定义全局属性,比如:config、util(官方不考虑将其作为公共 API 的一部分,不推荐使用)、set、delete、nextTick、observable、options ;同时还定义几个方法来定义全局属性,如下:
initUse
定义全局属性:use。
initMixin
定义全局属性:mixin。
initExtend
定义全局属性:extend。
initAssetRegisters
定义全局属性:component、directive、filter。
最终也导出 Vue,至此,主要逻辑分析完了,回到文件:src/platforms/web/runtime/index
基于上一步在 Vue 定义全局属性:config 基础上,config 其数据类型是 Object,在其定义属性:
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
然后初始化 Runtime Only 环境下 directives & components,具体代码如下:
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
接着在 Vue 原型上定义:__patch__、$mount,最后将 Vue 导出。
最后,回到入口文件:`src/platforms/web/entry-runtime-with-compiler.js ,其实现逻辑也很简单,将上一步在 Vue 原型定义的属性 $mount 保存到变量 mount,重新在 Vue 原型上定义属性 $mount,其值是一个函数,与 mount 的区别是具有编译功能,将 template 编译成 render 函数;最终,其内部在最后也是通过调用 mount保存的 $mount 来实现挂载的。
至此,Vue 初始化入口也就分析完了,我们也清晰地知道 import Vue from 'vue' 这行代码具体是怎么回事了。