vue源码分析(十七) 初始化
# 1. 概述
上一章节我们已经分析了 vm.$options
这个属性了,它才是用来做一系列初始化工作的最终选项,那么接下来我们就继续看 _init
方法中的代码,继续了解 Vue
的初始化工作。
在分析之前,我们还是以上一章的案例进行讲解,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>深入响应式原理-响应式对象</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const child = {
template: `<div v-on:click="getName"> {{ name }} </div>`,
data() {
return { name: 'robin' }
},
methods: {
getName() {
console.log(this.name)
}
}
}
new Vue({
el: '#app',
components: {child},
template: `<child/>`,
data: {
msg: 'message'
}
})
</script>
</body>
</html>
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
# 2. initProxy
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
/*
* istanbul ignore else
* 参考:https://segmentfault.com/a/1190000014824359
*/
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
2
3
4
5
6
7
8
9
这段代码是一个 if...else
语句块,如果是开发环境的话则执行 initProxy(vm)
函数,如果在生产环境则直接在实例上添加 _renderProxy
实例属性,该属性的值就是当前实例。
接下来我们看看在开发环境中,initProxy
的内容,如下:
源码目录:`scr/core/instance/proxy.js`
let initProxy
if (process.env.NODE_ENV !== 'production') {
const allowedGlobals = function(){}
const warnNonPresent = function(){}
const warnReservedPrefix = function(){}
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
const hasHandler = function(){}
const getHandler = function(){}
initProxy = function initProxy (vm) {}
}
export { initProxy }
2
3
4
5
6
7
8
9
10
11
12
13
上面是我们把 proxy.js
中的代码,用伪代码的方式梳理了一下,从上面代码可以知道在一开始声明了 initProxy
但没有赋值,所以为 undefined
,如果当前环境为开发环境,才会执行 if
语句进行初始化。即在生产环境 initProxy
为 undefined
,在开发环境 initProxy
才是一个函数。
我们继续看 if
语句中的代码,如下:
源码目录:scr/core/instance/proxy.js
if (process.env.NODE_ENV !== 'production') {
const allowedGlobals = makeMap(
'Infinity,undefined,NaN,isFinite,isNaN,' +
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
'require' // for Webpack/Browserify
)
// 省略...
}
2
3
4
5
6
7
8
9
从上面代码我们可以看到 allowedGlobals
实际上是通过 makeMap
生成的函数,所以 allowedGlobals
函数的作用是判断给定的 key
是否出现在上面字符串中定义的关键字中的。这些关键字都是在 js
中可以全局访问的。
我们继续看往下看,源码如下:
源码目录:scr/core/instance/proxy.js
if (process.env.NODE_ENV !== 'production') {
// 省略...
const warnNonPresent = (target, key) => {
warn(
`Property or method "${key}" is not defined on the instance but ` +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
)
}
const warnReservedPrefix = (target, key) => {
warn(
`Property "${key}" must be accessed with "$data.${key}" because ` +
'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
'prevent conflicts with Vue internals. ' +
'See: https://vuejs.org/v2/api/#data',
target
)
}
// 省略...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
warnNonPresent
这个函数就是通过 warn
打印一段警告信息,警告信息提示你 “在渲染的时候引用了 key
,但是在实例对象上并没有定义 key
这个属性或方法”。
warnReservedPrefix
这个函数就是通过 warn
打印一段警告信息,作用是提示开发者以 _
或 $
开头的 property
不会被 Vue
实例代理,因为它们可能和 Vue
内置的 property
、API
方法冲突。
我们继续看往下看,源码如下:
源码目录:scr/core/instance/proxy.js
if (process.env.NODE_ENV !== 'production') {
// 省略...
const hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy)
// 省略...
}
2
3
4
5
6
上面代码的作用是判断当前宿主环境是否支持原生 Proxy
。
我们继续看往下看,源码如下:
源码目录:scr/core/instance/proxy.js
if (process.env.NODE_ENV !== 'production') {
// 省略...
if (hasProxy) {
const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
config.keyCodes = new Proxy(config.keyCodes, {
set (target, key, value) {
if (isBuiltInModifier(key)) {
warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
return false
} else {
target[key] = value
return true
}
}
})
}
// 省略...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上面的代码首先检测宿主环境是否支持 Proxy
,如果支持的话才会执行里面的代码,内部的代码首先使用 makeMap
函数生成一个 isBuiltInModifier
函数,该函数用来检测给定的值是否是内置的事件修饰符。
然后为 config.keyCodes
设置了 set
代理,其目的是防止开发者在自定义键位别名的时候,覆盖了内置的修饰符。
说明:关于 事件修饰符 (opens new window) 、 按键修饰符 (opens new window) 、系统修饰符 (opens new window) 可以参考 Vue
官网。
我们继续看往下看,源码如下:
源码目录:scr/core/instance/proxy.js
if (process.env.NODE_ENV !== 'production') {
// 省略...
const hasHandler = {
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = 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
上面这段代码我们先看看 initProxy
的实现, initProxy
的作用实际上就是对实例对象 vm
的代理,通过原生的 Proxy
实现。
initProxy
接收一个参数为 Vue
实例,然后是一个 if...else
语句块,如果当前环境支持 Proxy
则执行 if
语句块,如果当前环境不支持 Proxy
则执行else
语句块,直接在 vm
对象上添加了 _renderProxy
属性,值为当前实例。
当前环境支持 Proxy
的情况下,首先将 vm.$options
的引用赋值给 options
,然后判断 options.render
和 options.render._withStripped
是否都为真时,handlers
的值为 getHandler
,否则都为 hasHandler
,在们当前的案例中 options.render._withStripped
一般情况下这个条件都会为假,也就是使用 hasHandler
作为代理配置。
我们再来看看 hasHandler
,hasHandler
方法的应用场景在于查看 vm
实例是否拥有某个属性,首先使用 in
操作符判断该属性是否在 vm
实例上存在,接下来通过判断 key
在 allowedGlobals
之内即访问了全局属性也是不允许的,或者 key
是以下划线 _
开头的字符串并且不在 target.$data
对象中的字符串,则为真。接下来如果 has
和 isAllowed
都为假时,报一个警告。
报警告也是分两种类型,如上面的代码,首先判断 key
是否在 target.$data
对象中,如果存在说明存在以 _
或 $
开头的 property
,如果不存在说明属性名不存在。
我们再来看看 getHandler
,getHandler
的作用是设置的 get
拦截,其最终实现的效果无非就是检测到访问的属性不存在就给你一个警告。警告的类型和 hasHandler
中一样,这里就不重复说明了。
最后我们回到 initProxy
看看这句 vm._renderProxy = new Proxy(vm, handlers)
,当前环境支持 Proxy
的情况下,那么将会使用 Proxy
对 vm
做一层代理,代理对象赋值给 vm._renderProxy
,所以今后对 vm._renderProxy
的访问,如果有代理那么就会被拦截。
至此我们已经分析完了 initProxy
,其作用就是设置渲染函数的作用域代理,其目的是为我们提供更好的提示信息。
# 3. initLifecycle
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
vm._self = vm
initLifecycle(vm)
2
首先执行了 vm._self = vm
语句,这句话在 Vue
实例对象 vm
上添加了 _self
属性,指向 Vue
实例对象本身。然后是执行 initLifecycle(vm)
,作用是初始化生命周期,接下来我们来看看 initLifecycle
的定义,如下:
源码目录:`scr/core/instance/lifecycle.js`
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
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
接下来我们将 initLifecycle
函数中的代码拆开来分析一下,首先定义 options
常量,它是 vm.$options
的引用,如下:
const options = vm.$options
我们继续往下看,代码如下:
// locate first non-abstract parent
let parent = options.parent
2
这句代码是的作用是获取父实例,即获取 options.parent
,前面我们分析过 options
是 vm.$options
的引用,所以 options.parent
等于 vm.$options.parent
。
这里涉及到了父子关系,逻辑比较绕,我们用一个案例来说明一下,如下:
const child = {
template: `<div> {{ name }} </div>`,
data() {
return { name: 'robin' }
}
}
var vm = new Vue({
el: '#app',
components: { child },
template: `<child/>`,
data: {
msg: 'this is message'
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
在我们这个案例中,父实例为 options.parent = vm.$options.parent
等于 new Vue
的实例化对象,子实例为 Vue.extend
实例化的对象。
我们继续往下看,代码如下:
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
2
3
4
5
6
这段代码是一个 if
语句,判断条件如果父实例存在,且当前实例不是抽象的,则执行 if
语句。
这里我们需要解释一点的是关于抽象组件,如果一个组件的 $options.abstract
,选项为 true
,则该组件是抽象的,那么通过该组件创建的实例也都是抽象的。
抽象组件一个最显著的特点就是它们一般不渲染真实 DOM
, Vue
内置了一些全局组件比如 keep-alive
或者 transition
,我们知道这两个组件它是不会渲染 DOM
至页面的,但他们依然给我提供了很有用的功能。抽象组件还有一个特点,就是它们不会出现在父子关系的路径上
我们在回到 if
语句,如果 !options.abstract
为真,说明当前实例是抽象组件,不会执行 if
语句,直接设置 vm.$parent
和 vm.$root
的值,跳过 if
语句块的结果将导致该抽象实例不会被添加到父实例的 $children
中。
如果 !options.abstract
为不真,说明说明当前实例不是抽象组件,则执行 if
语句,接下来是一个 while
循环目的就是沿着父实例链逐层向上寻找到第一个不抽象的实例作为 parent
(父级),并且在找到父级之后将当前实例添加到父实例的 $children
属性中,这样最终的目的就达成了。
我们继续往下看,代码如下:
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
这段代码的作用是在当前实例上添加一系列属性。其中 $children
和 $refs
都是我们熟悉的实例属性,他们都在 initLifecycle
函数中被初始化,其中 $children
被初始化为一个数组,$refs
被初始化为一个空 json
对象,除此之外,还定义了一些以 _
开始的属性供内部使用,需要注意的是这些属性是在 initLifecycle
函数中定义的,那么属性会与生命周期有关。这样 initLifecycle
函数我们就分析完毕了。
说明:关于抽象组件我们会在组件化章节具体分析,不了解的同学可以先参考 这里 (opens new window)
# 4. initEvents
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
initEvents(vm)
然后是执行 initEvents(vm)
,作用是初始化事件,接下来我们来看看 initEvents
的定义,如下:
源码目录:`scr/core/instance/events.js`
export function initEvents (vm: Component) {
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
首先在 vm
实例对象上添加两个实例属性 _events
和 _hasHookEvent
,其中 _events
被初始化为一个空对象,_hasHookEvent
的初始值为 false
。
接下来是定义常量 listeners
,它是 vm.$options._parentListeners
的引用。 _parentListeners
是在函数 createComponentInstanceForVnode
中添加到 options
上的,它在 core/vdom/create-component.js
文件中,也就是说在创建子组件实例的时候才会有这个参数选项,由于在我们当前的案例中获取到的 _parentListeners
为 undefined
,所以现在我们不做深入讨论,在组件化章节再来分析。
说明:事件初始化参考事件初始化 (opens new window) 、事件系统 (opens new window)。
# 5. initRender
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
initRender(vm)
然后是执行 initRender(vm)
,作用是初始化渲染,接下来我们来看看 initRender
的定义,如下:
源码目录:`scr/core/instance/events.js`
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => 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
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
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
由于这部分代码比较多,接下来我们将 initRender
函数中的代码拆开来分析一下,首先看一下如下代码:
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
2
首先在 Vue
实例对象上添加两个实例属性,即 _vnode
和 _staticTrees
,并初始化为 null
。
我们继续往下看,代码如下:
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
2
3
4
5
这段代码是在 Vue
实例对象上添加三个实例属性,即 $vnode
、 $slots
和 $scopedSlots
。
说明:关于上面代码的作用我们在后面插槽的章节再来分析。
关于$slots
请到 这里 (opens new window) 学习。
关于$scopedSlots
请到 这里 (opens new window) 学习。
我们继续往下看,代码如下:
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
2
3
4
5
6
7
8
这段代码在 Vue
实例对象上添加了两个方法:vm._c
和 vm.$createElement
,这两个方法实际上是对内部函数 createElement
的包装。
对于以上两个属性的理解我们举个例子来看看,如下代码:
render: function (createElement) {
return createElement('h2', 'Title')
}
2
3
我们知道,渲染函数的第一个参数是 createElement
函数,该函数用来创建虚拟节点,实际上你也完全可以这么做:
render: function () {
return this.$createElement('h2', 'Title')
}
2
3
上面两段代码是完全等价的。而对于 vm._c
方法,则用于编译器根据模板字符串生成的渲染函数的。vm._c
和 vm.$createElement
的不同之处就在于调用 createElement
函数时传递的第六个参数不同,至于这么做的原因,我们放到后面章节讲解。有一点需要注意,即 $createElement
看上去像对外暴露的接口,但其实文档上并没有体现。
我们继续往下看,代码如下:
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
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
这段代码的主要作用就是在 Vue
实例对象上定义两个属性:vm.$attrs
以及 vm.$listeners
。在开发环境中调用 defineReactive
函数时传递的第四个参数是一个函数,实际上这个函数是一个自定义的 setter
,这个 setter
会在你设置 $attrs
或 $listeners
属性时触发并执行。
自定义的 setter
中,当 !isUpdatingChildComponent
成立时,会提示你 $attrs
或 $listeners
是只读属性,你不应该手动设置它的值。
说明:关于$attrs
请到 这里 (opens new window) 学习。
关于$listeners
请到 这里 (opens new window) 学习。
# 6. initInjections
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
initInjections(vm)
我们继续往下看,代码如下:
源码目录 src/core/instance/inject.js
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
// 省略...
}
2
3
4
initInjections
函数接收组件实例对象作为参数,在 initInjections
函数内部首先定义了 result
常量,并且我们能够注意到接下来的 if
条件语句的判断条件就是 result
常量,只有 result
为真的情况下才会执行 if
语句块内的代码。我们首先来看一下 result
常量的值是什么,可以看到它是 resolveInject
函数的返回值。我们知道子组件中通过 inject
选项注入的数据其实是存放在其父代组件实例的 vm._provided
属性中,实际上 resolveInject
函数的作用就是根据当前组件的 inject
选项去父代组件中寻找注入的数据,并将最终的数据返回。
我们接下来先看看 resolveInject
函数,代码如下:
源码目录 src/core/instance/inject.js
export function resolveInject (inject: any, vm: Component): ?Object {
// 省略...
}
2
3
resolveInject
函数接收两个参数,分别是 inject
选项以及组件实例对象。我们可以看到在 initInjections
函数中调用 resolveInject
函数时所传递的参数分别是 vm.$options.inject
以及 vm
。
我们继续往下看,代码如下:
源码目录 src/core/instance/inject.js
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
// 省略...
return result
}
}
2
3
4
5
6
7
8
我们能够看到 if
语句块内首先定义一个常量,值为一个空对象,的最后一句代码将 result
返回,该 result
就是最终寻找到的注入的数据。如果 inject
选项不存在则返回 undefined
。
我们继续往下看,代码如下:
源码目录 src/core/instance/inject.js
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// 省略...
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
// 省略...
}
}
2
3
4
5
6
7
8
9
接着定义了 keys
常量,它的值是一个数组,即由 inject
选项对象所有键名组成的数组,我们在前面章节规范化中分析过 inject
选项被规范化后将会是一个对象,并且该对象必然会包含 from
属性。例如 inject
选项是一个字符串数组:
inject: ['data1', 'data2']
// 规范化后
{
'data1': { from: 'data1' },
'data2': { from: 'data2' }
}
2
3
4
5
6
7
如果 inject
选项是一个对象,那么这个对象你可以有好几种写法:
inject: {
// 第一种写法
data1: 'd1',
// 第二种写法
data2: {
someProperty: 'someValue'
}
}
// 规范化后
inject: {
'data1': { from: 'd1' },
'data2': { from: 'data2', someProperty: 'someValue' }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看到被规范化后的每个 inject
选项值也都是一个对象,并且都包含 from
属性。同时我们注意到 someProperty
属性被保留了,所以你完全可以把 someProperty
属性替换成 default
属性:
inject: {
data1: {
default: 'defaultValue'
}
}
2
3
4
5
这就是 Vue
文档中提到的可以使用 default
属性为注入的值指定默认值。
现在我们知道 keys
常量中保存 inject
选项对象的每一个键名,但我们注意到这里有一个对 hasSymbol
的判断,其目的是保证 Symbol
类型与 Reflect.ownKeys
可用且为宿主环境原生提供,如果 hasSymbol
为真,则说明可用,此时会使用 Reflect.ownKeys
获取 inject
对象中所有可枚举的键名,否则使用 Object.keys
作为降级处理。实际上 Reflect.ownKeys
配合可枚举过滤等价于 Object.keys
与 Object.getOwnPropertySymbols
配合可枚举过滤之和,其好处是支持 Symbol
类型作为键名,当然了这一切都建立在宿主环境的支持之上,所以 Vue
官网中提到了**inject
选项对象的属性可以使用 ES2015 Symbols
作为 key
,但是只在原生支持 Symbol
和 Reflect.ownKeys
的环境下可工作**。
我们继续往下看,代码如下:
源码目录 src/core/instance/inject.js
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// 省略...
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const 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)
}
}
}
// 省略...
}
}
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
接下来的代码使用 for
循环,用来遍历刚刚获取到的 keys
数组,在循环内部首先定义了两个常量以及一个变量,其中 key
常量就是 keys
数组中的每一个值,即 inject
选项的每一个键值,provideKey
常量保存的是每一个 inject
选项内所定义的注入对象的 from
属性的值,我们知道 from
属性的值代表着 vm._provided
数据中的每个数据的键名,所以 provideKey
常量将用来查找所注入的数据。最后定义了 source
变量,它的初始值是当前组件实例对象。
接下来将开启一个 while
循环,用来查找注入数据的工作,我们知道 source
是当前组件实例对象,在循环内部有一个 if
条件语句,该条件检测了 source._provided
属性是否存在,并且 source._provided
对象自身是否拥有 provideKey
键,如果有则说明找到了注入的数据:source._provided[provideKey]
,并将它赋值给 result
对象的同名属性。有的同学会问:“source
变量的初始值为当前组件实例对象,那么如果在当前对象下找到了通过 provide
选项提供的值,那岂不是自身给自身注入数据?”。大家不要忘了 inject
选项的初始化是在 provide
选项初始化之前的,也就是说即使该组件通过 provide
选项提供的数据中的确存在 inject
选项注入的数据,也不会有任何影响,因为在 inject
选项查找数据时 provide
提供的数据还没有被初始化,所以当一个组件使用 provide
提供数据时,该数据只有子代组件可用。
那么如果 if
判断条件为假,重新赋值 source
变量,使其引用父组件,以及类推就完成了向父代组件查找数据的需求,直到找到数据为止。但是如果一直找到了根组件,但依然没有找到数据,将会执行下面代码,如下:
我们继续往下看,代码如下:
源码目录 src/core/instance/inject.js
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// 省略...
for (let i = 0; i < keys.length; i++) {
// 省略...
if (!source) {
if ('default' in inject[key]) {
const 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)
}
}
}
// 省略...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们知道根组件实例对象的 vm.$parent
属性为 null
,所以如上 if
条件语句的判断条件如果成立,说明一直寻找到根组件也没有找到要的数据,此时需要查看 inject[key]
对象中是否定义了 default
选项,如果定义了 default
选项则使用 default
选项提供的数据作为注入的数据,否则在非生产环境下会提示开发者未找到注入的数据。另外我们可以看到 default
选项可以是一个函数,此时会通过执行该函数来获取注入的数据。
最后如果查询到了数据,resolveInject
函数会将 result
作为返回值返回,并且 result
对象的键就是注入数据的名字,result
对象每个键的值就是注入的数据。
我们回到 initInjections
函数继续往下看,代码如下:
源码目录 src/core/instance/inject.js
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
initInjections
函数通过 resolveInject
函数取得了注入的数据,并赋值给 result
常量,我们知道 result
常量的值有可能是不存在的,所以需要一个 if
条件语句对 result
进行判断,当条件为真时说明成功取得注入的数据,此时会执行 if
语句块内的代码。
在 if
语句块内,通过遍历 result
常量并调用 defineReactive
函数在当前组件实例对象 vm
上定义与注入名称相同的变量,并赋予取得的值。这里有一个对环境的判断,在非生产环境下调用 defineReactive
函数时会多传递一个参数,即 customSetter
,当你尝试设置注入的数据时会提示你不要这么做。
另外大家也注意到了在使用 defineReactive
函数为组件实例对象定义属性之前,调用了 toggleObserving(false)
函数关闭了响应式定义的开关,之后又将开关开启:toggleObserving(true)
。关于toggleObserving我们将在初始化props中详细分析,这么做将会导致使用 defineReactive
定义属性时不会将该属性的值转换为响应式的,所以 Vue
文档中提到了:
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
当然啦,如果父代组件提供的数据本身就是响应式的,即使 defineReactive
不转,那么最终这个数据也还是响应式的。
说明:更多内容查看 provide/inject (opens new window) 。
# 7. initState
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
initState(vm)
接下来执行 initState(vm)
,作用是初始化 props
属性、data
属性、methods
属性、computed
属性、watch
属性,接下来我们来看看 initState
的定义,如下:
源码目录:`scr/core/instance/state.js`
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)
// Firefox has a "watch" function on Object.prototype
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
首先在 Vue
实例对象添加一个属性 vm._watchers = []
,其初始值是一个数组,这个数组将用来存储所有该组件实例的 watcher
对象。随后定义了常量 opts
,它是 vm.$options
的引用。
如果 opts.props
存在,即选项中有 props
,那么就调用 initProps
初始化 props
选项。
如果 opts.methods
存在,则调用 initMethods
初始化 methods
选项。
接下来判断 data
选项是否存在,如果存在则调用 initData
初始化 data
选项,如果不存在则直接调用 observe
函数观测一个空对象:{}
。
如果 opts.computed
存在,即选项中有 computed
,那么就调用 initComputed
初始化 computed
选项。
最后判断 opts.watch
是否存在,并且 opts.watch
不是原生的 watch
对象,即选项中有 watch
,那么就调用 initWatch
初始化 watch
选项。
说明:前面的章节中我们提到过,这是因为在 Firefox
中原生提供了 Object.prototype.watch
函数,所以即使没有 opts.watch
选项,如果在火狐浏览器中依然能够通过原型链访问到原生的 Object.prototype.watch
。但这其实不是我们想要的结果,所以这里加了一层判断避免把原生 watch
函数误认为是我们预期的 opts.watch
选项。
# 8. initProvide
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
initProvide(vm)
可以发现 initInjections
函数在 initProvide
函数之前被调用,这说明对于任何一个组件来讲,总是要优先初始化 inject
选项,再初始化 provide
选项。但是我们知道 inject
选项的数据需要从父代组件中的 provide
获取,所以我们优先来了解 provide
选项的实现,然后再查看 inject
选项的实现。
源码目录 src/core/instance/inject.js
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
2
3
4
5
6
7
8
如上是 initProvide
函数的全部代码,它接收组件实例对象作为参数。在 initProvide
函数内部首先定义了 provide
常量,它的值是 vm.$options.provide
选项的引用,接着是一个 if
条件语句,只有在 provide
选项存在的情况下才会执行 if
语句块内的代码,我们知道 provide
选项可以是对象,也可以是一个返回对象的函数。所以在 if
语句块内使用 typeof
操作符检测 provide
常量的类型,如果是函数则执行该函数获取数据,否则直接将 provide
本身作为数据。最后将数据复制给组件实例对象的 vm._provided
属性,后面我们可以看到当组件初始化 inject
选项时,其注入的数据就是从父代组件实例的 vm._provided
属性中获取的。
以上就是 provide
选项的初始化及实现,它本质上就是在组件实例对象上添加了 vm._provided
属性,并保存了用于子代组件的数据。
说明:更多内容查看 provide/inject (opens new window) 。
# 9. callHook
我们回到 _init
函数中,继续往下看,代码如下:
源码目录:src/core/instance/init.js
callHook(vm, 'beforeCreate')
// 初始化Injections、State、Provide...
callHook(vm, 'created')
2
3
callHook
函数的作用是调用生命周期钩子函数。接下来我们来看看 callHook
的定义,如下:
源码目录:`scr/core/instance/lifecycle.js`
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
pushTarget()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
首先我们看一下 callHook
函数的参数列表,callHook
接收两个参数:实例对象和要调用的生命周期钩子的名称。
接下来我们再来分析 callHook
内部的实现逻辑,首先 pushTarget()
和 pushTarget()
的作用是为了避免在某些生命周期钩子中使用 props
数据导致收集冗余的依赖,我们这里不展开讲解中,在后面章节中我们会详细介绍。
接下来是获取要调用的生命周期钩子,我们在 vue源码分析(十六) 选项合并之合并策略 (opens new window) 中分析知道生命周期钩子选项最终会被合并处理成一个数组,所以这里通过 vm.$options[hook]
获取的也是一个数组即 handlers
是一个数组。
接下来是定义一个常量,值为当前要调用的生命周期的名称和 字符串 hook
组成的一个新的字符串。然后判断 handlers
是否在实例选项中写了生命周期钩子,如果写了则执行 if
语句块。if
语句块中通过 for
循环遍历获取到的生命周期钩子的数组,调用 invokeWithErrorHandling
函数处理钩子函数。
我们再来看看 invokeWithErrorHandling
定义,如下:
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
invokeWithErrorHandling
函数中使用 try...catch
语句块,为了捕获可能出现的错误, 并在 catch
语句块内使用 handleError
处理错误信息。 通过判断参数是否存在来确定使用 .apply(context, args)
还是使用 .call(context)
,如果存在则使用 .apply(context, args)
执行钩子函数,如果不存在则使用 .call(context)
执行钩子函数,需要注意的是为了保证生命周期钩子函数内可以通过 this
访问实例对象,此处的 context
为 vm
实例。
我们再回到 callHook
函数中,代码分析到这里还有一个 if
语句没有分析,如下:
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
2
3
其中 vm._hasHookEvent
是在 initEvents
函数中定义的,它的作用是判断是否存在生命周期钩子的事件侦听器,初始化值为 false
代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将 vm._hasHookEvent
设置为 true
。那这个逻辑什么时候执行呢?我们举个例子来说明,如下:
<child
@hook:beforeCreate="handleChildBeforeCreate"
@hook:created="handleChildCreated"
@hook:mounted="handleChildMounted"
@hook:生命周期钩子
/>
2
3
4
5
6
如上代码可以使用 hook:
加 生命周期钩子名称
的方式来监听组件相应的生命周期事件。这是 Vue
官方文档上没有体现的,但你确实可以这么用,不过除非你对 Vue
非常了解,否则不建议使用。
通过上面分析我们知道其中 initInjections
、initState
、initProvide
在 callHook(vm, 'beforeCreate')
、 callHook(vm, 'created')
两个钩子函数之间,其中 initState
包括了:initProps
、initMethods
、initData
、initComputed
以及 initWatch
。所以当 beforeCreate
钩子被调用时,所有与 props
、methods
、data
、computed
以及 watch
相关的内容都不能使用,当然了 inject/provide
也是不可用的。而在 created
中这些数据都可以使用。