系列文章:
- Vue2.6x源码解析(二):组件初始化
- Vue2.6x源码解析(三):深入响应式原理
- Vue2.6x源码解析(四):异步更新队列
- Vue2.6x源码解析(五):Vue应用加载流程【多图预警!推荐收藏】
1,源码目录结构
Vue.2.6x
的目录结构如下:
├── scripts # 与构建相关的脚本和配置文件
├── dist # 构建后的文件
├── flow # Flow的类型声明
├── packages # vue-server-renderer和vue-template-compiler,它们作为单独的NPM包发布
├── test # 所有的测试代码
├── src # 源代码
│ ├── compiler # 与模板编译相关的代码
│ ├── core # 通用的、与平台无关的运行时代码 // 核心源码
│ │ ├── observer # 实现变化侦测的代码
│ │ ├── vdom # 实现虚拟DOM的代码
│ │ ├── instance # Vue.js实例的构造函数和原型方法
│ │ ├── global-api # 全局API的代码
│ │ └── components # 通用的抽象组件
│ ├── platforms # 特定平台代码web/weex
│ ├── server # 与服务端渲染相关的代码
│ ├── sfc # 单文件组件(* .vue文件)解析逻辑
│ └── shared # 整个项目的公用工具代码
└── types # TypeScript类型定义
重点: src/core
目录下是Vue.js
的核心源码,这部分逻辑是与平台无关的,是Vue
框架的核心运行时。
2,Vue从哪里来
我们启动一个Vue
项目的时候都知道,项目的入口文件是src/main.js
,在这个文件里我们会引入一个Vue
构造函数,那么这个Vue是从哪里来的呢?
// main.js
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app');
根据项目依赖Vue
源码里面的package.json
文件中,我们可以得到结果:
// package.json
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js"
也就是说我们通过Vue-cli
脚手架搭建的一个Vue
项目,会通过模块化的方式引入dist/vue.runtime.esm.js
这个文件,找到这个文件,我们在最后一行代码可以发现,文件最终其实就是导出了一个Vue
构造函数,而我们在main.js
文件里就是引入了这个构造函数。
// dist/vue.runtime.esm.js
export default Vue;
3,Vue的定义
通过前面我们知道了Import
导入的Vue
构造函数从何而来,下面我们从Vue2.6x
源码中寻找Vue
构造函数。
在src/core/instance/index.js
我们可以找到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'
# 定义Vue构造函数:
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)
}
# 在Vue.prototype原型上挂载方法
initMixin(Vue) # 挂载 _init方法【重点】
stateMixin(Vue) // 挂载数据相关的方法:$watch、$set、$delete...
eventsMixin(Vue) // 挂载事件相关的方法: $on 、$once、$off、$emit...
lifecycleMixin(Vue) // 挂载生命周期相关的实例方法:_update、$forceUpdate、$destroy...
renderMixin(Vue) // 挂载 $nextTick、_render 以及_o/_n/_l等等十几个快捷方法
// 上面的方法挂载完毕之后,导出Vue,继续挂载全局API
export default Vue
这里我们知道每个初始化函数大概挂载了哪些方法就可以了,后面再详细介绍。
这里在定义了一个Vue
构造函数之后,就开始向Vue.prototype
原型对象挂载了很多的方法,我们依次来看挂载了哪些方法。
initMixin
挂载了一个_init
初始化方法:
function initMixin (Vue: Class<Component>) {
# 挂载了一个init初始化方法
Vue.prototype._init = function (options?: Object) {
...
}
}
stateMixin
挂载了一些操作数据的方法:
function stateMixin (Vue: Class<Component>) {
...
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
// $set $delete原理都是observer/index.js里面的set和del方法:set(defineReactive)、del(delete关键字)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function () {}
}
eventsMixin
挂载了一些绑定事件的方法:
function eventsMixin (Vue: Class<Component>) {
// 绑定事件,循环向vm._events属性中添加事件
Vue.prototype.$on = function () {}
// 绑定一次性事件
Vue.prototype.$once = function () {}
// 解绑事件
Vue.prototype.$off = function () {}
// 触发事件
Vue.prototype.$emit = function () {}
}
lifecycleMixin
挂载了一些组件更新卸载的方法:
function lifecycleMixin (Vue: Class<Component>) {
# 组件更新的方法,【重点】
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
// 强制组件更新
Vue.prototype.$forceUpdate = function () {}
// 组件销毁
Vue.prototype.$destroy = function () {}
}
renderMixin
挂载了组件渲染的相关方法:
function renderMixin (Vue: Class<Component>) {
# 挂载运行时的便利属性: vm.prototype._o/_n/_s;为render代码字符串运行时使用【重点】
installRenderHelpers(Vue.prototype)
// 挂载vm.$nextTick方法
Vue.prototype.$nextTick = function (fn: Function) {}
# 挂载组件渲染方法【重点】
Vue.prototype._render = function () {}
}
在Vue.prototype
原型方法挂载完成之后,我们继续查看对Vue
构造函数的处理。
在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'
# 挂载全局API (set、delete、nextTick、use、mixin、extend等多个方法)
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__'
# 导出vue
export default Vue
上面我们主要关注initGlobalAPI(Vue)
这行代码,初始化全局API:
// src/core/golbal-api/index.js
# 挂载全局API
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
// 定义全局配置
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
}
// 挂载全局的set/delete/nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 定义options属性
Vue.options = Object.create(null)
// 定义options对象下components/directives/filters属性, 存储全局组件/指令/过滤器
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// 设置了一个基础构造器; 标识“基本”构造函数以扩展所有普通对象 =====重点;Vue.options._base
Vue.options._base = Vue
// 注册keep-alive全局组件
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
// 挂载全局API Vue.component、Vue.filter、Vue.directive方法
initAssetRegisters(Vue)
}
根据上面的代码我们可以发现,这里主要是对Vue
构造函数自身挂载了set
、delete
、nextTick
、use
、mixin
等全局API。
所以我们new Vue()
初始化之前,Vue构造函数已经挂载了初始化项目所需要的全局API与原型方法,这也是为什么我们可以在调用new Vue()
初始化之前,就可以使用相应的全局API注册一些项目中使用到的资源 (注册插件/ui框架等) 。
import Vue from 'vue'
import App from './App.vue'
// Vue.use(ElementUI);
# 查看Vue结构
console.dir(Vue)
// new Vue({
// render: h => h(App)
// }).$mount('#app')
打印截图:
Vue
构造函数:
Vue.prototype
原型对象:
通过打印结果可以发现,在我们调用new Vue()
初始化之前,Vue构造函数本身及原型对象就已经挂载了相关的全局API和原型方法。
4,new Vue()初始化
通过前面的代码解析,我们已经知道了Vue
构造函数的由来及挂载内容,下面我们开始解析Vue应用的初始化过程:
function Vue (options) {
this._init(options)
}
# Vue应用初始化
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
// this._init()
调用new Vue()
初始化实际上就是内部调用了一个this._init()
方法,要知道具体的初始化过程,我们就得查看_init
方法内容:
// src/core/instance/index.js
# 应用初始化
Vue.prototype._init = function (options?: Object) {
// 获取当前vue实例
const vm: Component = this
// 初始化实例Id
vm._uid = uid++
vm._isVue = true
// 处理options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._renderProxy = vm
# 当new Vue() 执行后,触发的一系列初始化流程都是在 _init方法中启动的
vm._self = vm
initLifecycle(vm) // 初始化实例属性:初始化一些Vue上的实例属性($parent、$root、$refs、_isMounted、
initEvents(vm) // 初始化事件:是指将父组件在模板中使用的v-on 注册的事件添加到子组件的事件系统(Vue.js的事件系统)中。
initRender(vm) // 初始化一些渲染相关属性:(_vnode、$slots、$scopedSlots、_c、$createElement)
# 触发beforeCreate钩子函数
callHook(vm, 'beforeCreate')
initInjections(vm) // 初始化inject
initState(vm) // 初始化props、methods 、data 、computed 和watch
initProvide(vm) // 初始化provide
# 触发created钩子函数
callHook(vm, 'created')
// 如果用户在实例化Vue.js时传递了el选项,则自动开启挂载
// 如果没有传递el选项,则无法挂载,且不会进入下一个生命周期流程 (需要手动调用vm.$mount进行挂载)
# 开始应用加载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
init
方法的内容很多,其实主要就关注这几点:
- 合并
options
。(当前Vue应用实例没有需要合并的) - 初始化
Vue
应用实例的生命周期,event事件监听,状态选项props
、methods
、data
、computed
和watch
等【Vue根实例一般都不会传递这些配置参数】。 - 执行
vm.$mount
进行应用的渲染与挂载。
扩展:我们要明确一点,Vue2的应用实例和组件实例定义是相同的,因为组件的构造函数继承至Vue构造函数,所以这里他们的初始化逻辑是相同的。而在Vue3中,Vue应用实例和组件实例是完全不相同的定义,所以初始化的逻辑也不相同。
这里的vm.$mount
加载方法前面没有提及,实际上这个方法也是挂载到Vue.prototype
原型对象上的:
// platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 加载组件
return mountComponent(this, el, hydrating)
}
这里我们只讲一下大概的过程:
以生产环境为例,所以我们的文件都是已经编译完成的,template
模板都已经编译成了render
代码字符串。
// 编译后的render函数
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ attrs: { id: "app" } },
[
_c("button", { on: { click: _vm.handleButtonClick } }),
_vm._v(" "),
_c("HelloWorld", { on: { customEvent: _vm.handleCustomEvent } })
],
1
)
}
所以执行vm.$mount
大致会经常几个流程:
- 调用
render()
方法。 - 生成
vnode
虚拟dom对象。 - 调用
update()
方法。 - 调用
patch
递归渲染。 - 生成最终的
dom
。 - 完成页面渲染。
5,总结
最后我们再简单总结一下Vue
应用的初始化过程:
- 创建
Vue
构造函数,挂载实例方法与原型方法。 - 注册全局资源(组件/指令/过滤器/插件等等)。
- 调用
new Vue({...})
,创建Vue
应用实例。 - 内部执行
this.Init
方法,开始具体的初始化。 - 执行
$mount
方法,开始应用加载。
对于Vue
应用的初始化,我们掌握它的主要逻辑即可,一些细节过程我们在下节组件初始化中解析。