Vue实例化过程
# 前言
上文讲述了 Vue 项目的文件结构以及调试相关的知识,本章将开始正式进入源码解析的环节。
我们从 Vue 实例化开始分析,即:当我们 new Vue(xx)
的时候,内部都做了什么
# 寻找 Vue 构造方法
要分析 Vue 的实例化,得先分析 Vue 在实例化前都做了什么,我们需要先找到构造函数
上文提到,进行 rollup 打包的时候,以 src/platforms/web/entry-runtime-with-compiler.js
为入口,我们从该文件入手,发现导入了 Vue
// src/platforms/web/entry-runtime-with-compiler.js
import Vue from './runtime/index'
2
3
点进去
// src/platforms/web/runtime/index.js
import Vue from 'core/index'
2
3
在 core/index
中对 Vue 方法加了一些属性,继续深入
// src/core/index.js
import Vue from './instance/index'
2
3
继续进入,找到了 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
和我们前一篇结构目录分析的一样, core/instance 包含 Vue 构造函数的代码
当我们 new Vue()
的时候,实际调用的就是这个构造函数
# Vue 的初始化
当我们进行 import Vue from 'vue'
的时候,发生了什么?
在回答这个问题之前,我们需要知道的是,vue 目前有 web 和 weex 两个平台,每个平台包括 compiler
, runtime
和 server
三个部分,每个部分会注入一些特定于平台的模块/实用程序到 vue 的核心实现中。
先分析核心部分,不同平台和版本最终都是引入核心实现 src/core/index.js
导出的 vue
// src/core/index.js
// 构造函数和原型定义,稍后分析
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:给 Vue 挂载一些属性和方法,如
* Vue.config
* Vue.nextTick 等等
* 注意,Vue.component 等一些方法没有显示定义,在 initAssetRegisters 进行定义,
* 其效果为 this.options[type + 's'][id] = definition
* 因此 Vue.options.components 上会新增一个全局组件
*
* 可以在 https://cn.vuejs.org/v2/api/?#%E5%85%A8%E5%B1%80-API 查看详细
*/
initGlobalAPI(Vue)
// ssr 相关的属性和方法,不具体分析
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
一开始执行的 import Vue from './instance/index'
,其中定义了 Vue 构造函数,并对 Vue 进行了一些 Mixin 操作
从以下入手
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
2
3
4
5
# initMixin
// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// 略
}
}
2
3
4
5
6
7
作用:为 Vue 原型设置 _init 方法,该方法会在实例化的时候调用,该方法的具体操作 实例化 的时候会细讲
# stateMixin
// src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = ...
}
2
3
4
5
6
7
8
9
作用:如上所示,挂载了一些数据响应相关的属性和方法到 Vue.prototype ,等用到的时候再具体分析
# eventsMixin
// src/core/instance/event.js
export function eventsMixin (Vue: Class<Component>) {
Vue.prototype.$on = ...
Vue.prototype.$once = ...
Vue.prototype.$off = ...
Vue.prototype.$emit = ...
}
2
3
4
5
6
7
8
作用:如上所示,挂载了一些事件监听触发的方法到 Vue.prototype
# lifecycleMixin
// src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
// 更新 vdom
Vue.prototype._update = ...
// 调用监听器的更新方法
Vue.prototype.$forceUpdate = ...
Vue.prototype.$destroy = ...
}
2
3
4
5
6
7
8
9
作用:如上所示,挂载了一些生命周期的方法到 Vue.prototype
# renderMixin
// src/core/instance/render.js
export function renderMixin (Vue: Class<Component>) {
// 添加工具方法到原型中,如 Vue.prototype._n = toNumber
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
// 生成 vnode
Vue.prototype._render = ...
}
2
3
4
5
6
7
8
9
10
11
作用:如上所示,挂载了一些方法到 Vue.prototype
接着分析平台特定版本差异。
server 和 compiler 版本最后单独打包,这里我们就不分析了。
我们主要分析 web 的 runtime 和 runtime-with-compiler(完整版) 的差异
使用上的不同可以参看官方文档 (opens new window),简单介绍下:
完整版:同时包含编译器和运行时的版本。
编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。
运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。
进行源码分析,两者都是
// src\platforms\web\entry-runtime.js
// src\platforms\web\entry-runtime-with-compiler.js
import Vue from './runtime/index'
2
3
4
在 ./runtime/index
进行的操作如下:
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// web 平台相关配置
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 安装 web 平台 runtime 版本的指令:model show
extend(Vue.options.directives, platformDirectives)
// 安装 web 平台 runtime 版本的组件:Transition 相关
extend(Vue.options.components, platformComponents)
// 安装 vdom patch(修改 vdom 更新视图) 方法
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 挂载 mount 方法
Vue.prototype.$mount = function (
// 挂载的元素
el?: string | Element,
// 服务端渲染相关参数
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// 省略 devtools 工具提示部分
export default Vue
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$mount 主要调用的是 src\core\instance\lifecycle.js
的 mountComponent 方法,粗略分析如下:
// src\core\instance\lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 没有 render 属性则赋值一个 创建空 VNode 的方法
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// ... 提示相关
}
// 调用 beforeMount 钩子
callHook(vm, 'beforeMount')
let updateComponent
// ... 插件相关
// vm._render() 创建一个 vnode
// _update 进行 vdom 更新
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 实例化 Watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
// 调用 mounted 钩子
callHook(vm, 'mounted')
}
return vm
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
对于 rumtime 版本,直接将上述的 Vue 导出,而完整版增加了如下处理:
// src\platforms\web\entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
// 挂载的元素
el?: string | Element,
// 服务端渲染相关参数
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// 解析 template/el 并将其转化为 render 函数
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
// template 为 html 片段
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 设置 render 方法
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 执行原来的 $mount
return mount.call(this, el, hydrating)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
相比 runtime 版本,完整版增加了将 template/el 转化为 render 函数等手段
注意:如果完整版提供了 render 函数,那就不会去解析 template/el
至此,Vue 的初始化告一段落。
一般我们写的项目,都是用单文件组件 .vue
,其中的 template 会被 vue-loader
解析成 render 函数.
一开始 Vue 实例化的时候应该避免传入 template 参数,如
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
2
3
4
5
6
7
为了解析 template ,导致需要用完整版的 vue , 改成如下写法,只需要用 runtime 版本的 vue
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
// 或
new Vue({
el: '#app',
router,
store,
render: h => h(App)
});
2
3
4
5
6
7
8
9
10
11
12
# Vue 实例化
当我们执行 new Vue()
的时候,会发生什么?
再贴下 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)
}
2
3
4
5
6
7
8
可以看到,new Vue(options)
的时候,会将传入的配置作为参数调用 _init 原型方法
上文已经说了,_init 方法是在 initMixin 时挂载的,我们进入查看
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
// 合并 options
if (options && options._isComponent) {
// 优化内部组件实例
// 因为合并动态选项非常慢,并且内部组件选项都不需要特殊处理。
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
// 挂载 $parent, $root, $children, $refs, 父组件 $children.push(vm)
initLifecycle(vm)
// 初始化父组件在 vm 上添加的事件
initEvents(vm)
// 实例上挂载 $slots, $scopedSlots, $attrs, $listeners, $createElement
initRender(vm)
// 调用 beforeCreate 钩子
callHook(vm, 'beforeCreate')
// 在 data/props 前处理 injections
initInjections(vm)
// 挂载 props/methods/data/computed/watcher
initState(vm)
// 在 data/props 后处理 provide
initProvide(vm)
// 调用 created 钩子
callHook(vm, 'created')
// ...
// 提供 el 参数的话,执行 $mount 方法挂载组件,上文已经分析了 $mount 方法了
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
需要注意的几点:
- injections 在 data/props 前处理,因此 data/props 可以使用 injections 的值初始化
- provide 在 data/props 后处理,因此 provide 中可以使用 data/props 的值
- initState 上属性的初始化顺序为: props -> methods -> data -> computed -> watch, 只有前面的初始化完了才能进行访问,比如 data 可以访问 methods 而 props 不能
- methods 中定义的方法绑定了 vm 上下文:
bind(methods[key], vm)
- prop 的 validator 直接调用 validator(value) 不绑定 vm, 因此其中访问不到 injections;validator 验证失败时仅进行 warn 提示
最后上一段 data 进行响应式处理的代码, 抛个开头,后面文章会讲响应式
function initData (vm: Component) {
let data = vm.$options.data
// 获取 data 方法返回值
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// Object.prototype.toString.call(data) ==== '[object Object]'
// data 不是严格意义上的对象,进行警告
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// ...属性冲突检测
// data 属性代理 vm.a 等价于访问 vm._data.a
proxy(vm, `_data`, key)
// 观察 data
observe(data, true /* asRootData */)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
注意,实例化 Vue 的时候,data 是可以传对象的,而组件中,data 传 返回对象的方法
那么,组件中的 data 解析是在哪做的呢?
以 Vue.component 全局注册为例
Vue.component('select2', {
template: '#select2-template',
data: {},
})
2
3
4
前面分析过了, Vue.component 在 initAssetRegisters 中进行定义
// src\core\global-api\assets.js
// definition 为传入的第二个参数
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 断点进入该方法
definition = this.options._base.extend(definition)
}
2
3
4
5
6
7
8
在 extend 中,处理如下(省略部分代码):
// src\core\global-api\extend.js
Vue.extend = function (extendOptions: Object): Function {
const Super = this
// VueComponent 继承 Vue
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 合并参数
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// 一些优化,可以跳过
// 对于 props 和 computed 属性,在拓展的原型上定义 proxy getters
// 避免为每个创建的实例调用 Object.defineProperty
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// Sub 静态方法复用 Vue 的
Sub.extend = Super.extend
// ...
return Sub
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
在 mergeOptions 的时候出错,因为在 mergeField("data")
会调用 strats.data 方法,其中对 data 参数进行 function 的判断
至此,分析结束。平时写的 .vue 单文件组件,会解析成一个对象,当其进行局部注册的时候和上面类似
后面会专门写一篇关于Vue组件实例化的