Vue 实例化过程
# 实例化场景
这里说的实例化,并非单单指 Vue 根实例的实例化,其中也包括组件的实例化过程。
总的来说,其实可以分为两种场景,两种场景下的实例化过程存在着差异,这两种场景大体如下:
# 手动调用构造器
这种场景下,其实又可以分两种情况:
new Vue()
这是最常用的一种实例化方式,就是调用 Vue 构造器创建根 Vue 实例的过程
import App from './app'
new Vue({
data: {
name: 'main.js',
},
created() {
console.log('this is main.js')
},
// 渲染函数
render(h) {
return h(App)
},
}).$mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.extend
这种方式其实跟调用 Vue 构造器的方式差不多,不同之处在于构造器不同,通过 Vue.extend 方法创建的构造器继承自 Vue 构造器。
let Message = Vue.extend({
name: 'Message',
template: '<p>{{msg}}</p>',
data() {
return {
type: 'success',
msg: 'this is Message',
}
},
created() {
console.log(this.type + ':' + this.msg)
},
})
new Message().$mount('#message')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自动调用构造器创建
这其实是 vue 组件的构造场景,其实也可以分为两种情况
# 使用 Vue.component
的方式注册的全局组件
Vue.component('Message', {
data() {
return {
msg: 'this is message',
}
},
})
2
3
4
5
6
7
其实 Vue.component 方法的本质也是在内部调用了 Vue.extend 方法
Vue['component'] = function (id, definition) {
// 只传递组件名称,则是查找组件
if (!definition) {
return this.options['components'][id]
} else {
validateComponentName(id)
if (isPlainObject(definition)) {
definition.name = definition.name || id
// 其实就是 definition = Vue.extend(definition)
definition = this.options._base.extend(definition)
}
// 全局注册的组件,其定义是一个 组件构造器 函数
this.options['components'][id] = definition
return definition
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
值得注意的是,使用 Vue.component()
方法注册的全局组件,其定义是一个组件构造器函数,而下面要说的,在组件内部通过 components
选项注册的组件则是一个组件对象
# 在组件内部通过 components
选项注册的局部组件
export default {
name: 'MessageBox',
components: {
Message,
},
render(h) {
return h(Message)
},
}
2
3
4
5
6
7
8
9
其实这种方式的本质,同样是需要调用 Vue.extend 方法来创建组件的构造器
var baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
2
3
4
5
6
- 这里的 baseCtor 其实就是 Vue
- Ctor 则是当前组件对象
关于组件的实例化过程,后面在组件相关的部分再说,这里先记录下 Vue 根实例的构造过程。
# 示例
首先从一个示例开始
import App from './app'
new Vue({
el: '#app',
data: {
name: 'main.js',
},
created() {
console.log('this is main.js')
},
// 渲染函数
render(h) {
return h(App)
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Vue
// Vue 构造器
function Vue(options) {
this._init(options)
}
2
3
4
当调用 new Vue(options)
的时候,构造器内部就会调用 Vue.prototype._init
方法,并将传入的组件选项对象 options
传入进去,也就是这里的
{
el: '#app',
data: {
name: 'main.js',
},
// 渲染函数
render(h) {
return h(App)
},
}
2
3
4
5
6
7
8
9
10
更多的选项配置参见选项列表 (opens new window)
# Vue.prototype._init
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 每一个组件实例都会有一个唯一的uid
vm._uid = uid++
// 防止被转换成 响应式对象 的一个标记
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
// 配置合并:将子组件上的配置与根组件上的配置合并到一起,挂在 当前组件实例的 $options 上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._renderProxy = vm
// expose real self
vm._self = vm
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 配置合并
首先这里有一个判断,就是当 options._isComponent
这个属性存在的时候,就会走到 initInternalComponent
这个方法中去,
不存在的话,就会走到 else
逻辑,进行配置合并操作。
_isComponent
这个属性是一个内部属性,也就是在 Vue 内部使用的。其实在 Vue 中有这样一个规范,
- 以
$
开头的属性,是提供给用户使用的 - 以
_
开头的属性,则是在 Vue 内部使用的
那么这个属性,
- 是什么时候被添加到
options
上的呢? - 这个属性的作用又是什么呢?
这里可以先说结论,这部分的处理流程其实与上面提到的自动调用构造器也就是组件的构造场景相关,后面再细说
- 这个属性是在源码
core/vdom/create-component.js
这个文件中的createComponentInstanceForVnode
这个方法中添加的
这个方法会在组件 __patch__
过程中被调用
export function createComponentInstanceForVnode(vnode, parent): Component {
// 就是这里
const options = {
_isComponent: true,
_parentVnode: vnode,
parent,
}
// ...省略
return new vnode.componentOptions.Ctor(options)
}
2
3
4
5
6
7
8
9
10
11
12
BTW:这里的vnode.componentOptions.Ctor
其实就是通过 Vue.extend 方法继承来的子组件构造器
const Sub = function VueComponent(options) {
this._init(options)
}
// 寄生式组合继承方式
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(Super.options, extendOptions)
Sub['super'] = Super
2
3
4
5
6
7
8
9
- 这个属性的作用其实就是用来标记当前的 vue 实例,是一个 组件实例,即通过
VueComponent
构造的实例,而不是根 vue 实例,即通过 Vue 构造的实例
# 初始化
再往后面会进行一系列初始化,并在合适的时机调用 beforeCreate
& created
钩子函数
最后,通过判断 options.el
选项是否传入,来决定下一步是不是要进行组件的挂载操作,也就是 vm.$mount
Vue.prototype._init = function (options?: Object) {
// ...
// 生命周期初始化
initLifecycle(vm)
// 事件中心初始化
initEvents(vm)
// 初始化渲染器
initRender(vm)
// 调用 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 初始化inject
initInjections(vm)
// 初始化props/methods/data/computed/watcher
initState(vm)
// 初始化provide
initProvide(vm)
// 调用 created 钩子函数
callHook(vm, 'created')
// 如果提供了 el 选项,就直接进行挂载操作
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
- 初始化生命周期
- 初始化事件中心
- 初始化渲染函数
- 调用
beforeCreate
钩子 - 初始化 inject
- 初始化 state(包括 props/methods/data/computed/watch) 等等
- 初始化 provide
- 调用
created
钩子
这里有一个需要注意的地方,就是在 beforeCreate
钩子里面是没有办法获取到 state
的,原因就是 state 根本还没有开始初始化
# 生命周期初始化
这里涉及到一个抽象组件的概念,像内置的 keep-alive
就是一个抽象组件,抽象组件本身不渲染任何 DOM。
可以看到这里在构建父子组件关系的时候,是不会将抽象组件添加到这种关系中去的
function initLifecycle(vm: Component) {
const options = vm.$options
// 找到第一个非抽象组件
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
// 找到第一个非抽象的父组件 并将自身添加到父组件的 $children 中
parent.$children.push(vm)
}
// 子组件会把父实例挂载到自身的 $parent 属性上
// 根 vue 实例的 $parent 属性为 undefined
vm.$parent = parent
// 根 vue 实例的 $root 属性为 实例自身
vm.$root = parent ? parent.$root : vm
// 初始化实例上的一些属性
vm.$children = []
vm.$refs = {}
// 初始化内部属性
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
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
# 事件中心初始化
这里没什么好说的,
- 初始化了一个
_events
属性,值是一个空对象,用来保存组件中定义的事件处理函数 - 初始化了一个
_hasHookEvent
属性,用来标记组件是否包含 生命周期有关的事件钩子定义 ·
function initEvents(vm: Component) {
// 初始化内部 _events 属性
vm._events = Object.create(null)
vm._hasHookEvent = false
// 下述流程不涉及
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
2
3
4
5
6
7
8
9
10
11
# 初始化渲染函数
vm._c
这个方法用于将模板编译成 render 函数的时候使用vm.$createElement
也就是 h 函数,这个是用户手写 render 的时候调用
二者的区别仅在最后一个参数 alwaysNormalize
,即是否总是需要对传入的第三个参数,即 children
VNode 进行规范化处理。
其中,通过模板编译成的 render 函数,这个参数的值是 false,而用户手写 render 函数的情况,就是 true。
function initRender(vm) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
var options = vm.$options
var parentVnode = (vm.$vnode = options._parentVnode) // the placeholder node in parent tree
var renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 参数顺序: tag, data, children, normalizationType, alwaysNormalize
// 供内部组件使用
vm._c = function (a, b, c, d) {
return createElement(vm, a, b, c, d, false)
}
// normalization is always applied for the public version, used in
// 给用户写的render方法使用
vm.$createElement = function (a, b, c, d) {
return createElement(vm, a, b, c, d, true)
}
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
var parentData = parentVnode && parentVnode.data
// 添加 $attrs & $listeners 响应式对象属性
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
null,
true
)
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
null,
true
)
}
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
# 调用 beforeCreate 钩子
这里有两种情况,根据定义的方式不同,触发的方式也有所不同
- 一个是在选项对象中定义的
beforeCreate
钩子 - 一个是通过
@hook:beforeCreate
的方式注册的事件处理钩子
function callHook(vm, hook) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
// 从 $options 上获取到 beforeCreate 钩子的定义
var handlers = vm.$options[hook]
var info = hook + ' hook'
// 如果组件中有定义这个钩子,就会被调用
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
// 这里就是第二种定义的触发方式
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 初始化 inject
inject 跟 provide 是配对一起使用的,文档中是这样说的:
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在上下游关系成立的时间里始终生效
而且还有一点要注意:
provide 和 inject 绑定并不是响应式的。如果传入了一个可监听的对象,那么其对象的属性还是可响应的
function initInjections(vm) {
var result = resolveInject(vm.$options.inject, vm)
if (result) {
// 控制 defineReactive 方法是否将属性定义为响应式的,false 为非响应式
toggleObserving(false)
Object.keys(result).forEach(function (key) {
defineReactive(vm, key, result[key])
})
toggleObserving(true)
}
}
function resolveInject(inject, vm) {
if (inject) {
var result = Object.create(null)
var keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject)
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
// 已经是响应式的 则跳过
if (key === '__ob__') {
continue
}
var provideKey = inject[key].from
var source = vm
while (source) {
// 获取定义在 vm 上的 _provided 属性的值
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
// 这里保证了 祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
var provideDefault = inject[key].default
result[key] =
typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn('Injection "' + key + '" not found', vm)
}
}
}
return result
}
}
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
# 初始化 state
这里需要关注一下,state 的处理顺序:
props
->methods
->data
->computed
->watch
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
以 initData 为例,
- data 选项必须是对象或返回对象的函数
- data 中定义的 key 如果与 methods 以及 props 中的 key 重复,开发环境下会报警告
function initData(vm) {
var data = vm.$options.data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
// 如果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
)
}
// proxy data on instance
var keys = Object.keys(data)
var props = vm.$options.props
var methods = vm.$options.methods
var i = keys.length
while (i--) {
var key = keys[i]
// 如果data中的key 和 methods中的key重复,开发环境报警告
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
'Method "' + key + '" has already been defined as a data property.',
vm
)
}
}
// 如果data中的key 和 props 中的key重复,开发环境报警告
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' &&
warn(
'The data property "' +
key +
'" is already declared as a prop. ' +
'Use prop default value instead.',
vm
)
} else if (!isReserved(key)) {
// 将 vm[key] 代理到 vm._data[key] 上来
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- 为什么可以通过 this[key] 这种方式访问定义在 data 中的属性,原理就是通过 proxy 代理过去的
// proxy
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
}
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 初始化 provide
通过获取定义在 $options 上的 provide 的值,然后定义到 vm 的 _provided
属性上,这个值的使用是在上面提到的 初始化 inject
部分
function initProvide(vm) {
var provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function' ? provide.call(vm) : provide
}
}
2
3
4
5
6
思考个问题: 为什么是先处理的 inject 然后才是 provide,这样的话在初始化 reject 的时候,vm._provided
都还没有值啊?
其实看 inject 初始化的时候,它是从当前组件逐层向上不断查找他的父组件的 provide 的一个过程,而在 vue 组件的创建过程中,都是父组件先完成 created 这个生命周期,所以这就保证了父组件的 provide 已经被求值了。
# 调用 created 钩子
最后就是在完成一些列配置合并以及初始化操作后,调用 created 生命周期钩子函数,完成初始化过程,钩子的调用和 beforeCreate 钩子类似,不再赘述。
还有一点要注意的就是,在 created 这个钩子函数里,现在已经可以获取到定义在组件对象的 data/props/methods 等等这些数据了。
# $mount
最后就是挂载操作了,可以说挂载操作是整个实例化过程中最复杂的了,其中涉及到有关组件 VNode 的创建以及组件的 patch 过程,这个后面会分开来说。
下一篇先说一下
- 配置合并的过程
- 针对不同的选项配置合并的策略都有哪些