# 组件通信方式及实现原理
在Vue中,组件之间的通信方式主要包括一下几种:
- props与$emit
$parents、$children- provide、inject
下面,我们将会通过Vue中的源码,来分别介绍他们的实现原理。
# 1、props与$emit
# props数据传递(从父到子)
父组件通过props向子组件传递属性数据,子组件通过$emit('event-name', data)的方式向父组件传递数据。
子组价初始化时,通过initProps (opens new window)对传入的props进行初始化:
export function initState (vm: Component) {
// ...
const opts = vm.$options
// 初始化props,主要包括: props校验、响应式和代理
if (opts.props) initProps(vm, opts.props)
// ...
}
2
3
4
5
6
7
initProps的作用,主要是:
- 获取从父组件传递的props数据,这些数据存放在
vm.$options.propsData上。 - 对props的数据进行格式校验。
- 定义props的响应式。
- 代理props的访问,即我们可以通过
vm[propKey]直接访问数据,而不需要通过vm._props[propKey]来访问,内部就是通过proxy (opens new window)方法(proxy内部是通过Object.defineProperty实现)实现的。
function initProps(vm: Component, propsOptions: Object) {
// propsData:从父组件传递过来的props数据
const propsData = vm.$options.propsData || {}
// vm._props,存放所有props键值对
const props = vm._props = {}
// 缓存所有的props key
const keys = vm.$options._propKeys = []
for (const key in propsOptions) {
keys.push(key)
// 1) 校验传递的propsData中的值,是否满足propsOptions中定义的规范
const value = validateProp(key, propsOptions, propsData, vm)
// 2) 定义props的响应式, 并将key,value添加到vm._props中,即:vm._props.key = value;
defineReactive(props, key, value)
// 静态props已经在Vue.extend执行期间被代理到component原型上了,在这里,我们仅仅需要代理那些在安装阶段定义的props
if (!(key in vm)) {
// 3) 代理vm.key的访问,即访问vm.key,等同于vm._props.key。所以,可以直接通过vm实例,访问到props的值
// 原理就是通过Object.defineProperty(target, key, sharedPropertyDefinition)重写key的getter、setter
proxy(vm, `_props`, key)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建vnode阶段
propsData是我们通过createComponent (opens new window)方法,在创建组件vnode时,通过extractPropsFromVNodeData (opens new window)方法从data选项中提取的,并且将结果放到了vnode的componentOptions (opens new window)中了。
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 将vnode转换为真实dom
将vnode转为真实dom时,需要调用patch (opens new window)方法。在patch方法内部,我们会调用createElm (opens new window)方法初始化子组件,并将子组件挂载到其父节点上。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ... 子组件初始
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在createComponent内部,调用componentVNodeHooks (opens new window)的init (opens new window)钩子函数
// 创建子组件实例并挂载到父dom节点中
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 初次渲染时,vnode.componentInstance为undefined(vnode.componentInstance赋值是在243行的init钩子中,子组件初始化前执行的)
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// data的hook,是在创建vnode时:
// hook,是在调用createElement- > createComponent -> installComponentHooks中赋值的
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 子组件实例化:src\core\vdom\create-component.js,37行
// componentVNodeHooks.init钩子 --> child = createComponentInstanceForVnode --> child.$mount(undefined, false)
i(vnode, false /* hydrating */)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在init钩子内部,创建vnode的vm实例,并调用$mount挂载子组件,进入子组件的_init初始化过程。
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
if ( // 初次渲染,并且对于keep-alive为true的vnode,执行prepatch钩子,否则,执行子组件child.mount过程
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 通过createComponentInstanceForVnode创建一个Vue子类的实例,并赋值给vnode.componentInstance
// activeInstance:它的作用就是保持当前上下文的 Vue 实例(表示当前vm的实例, 此时在子组件初始化过程中,就可以确定父子关系,即$children, $parent的关系)
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 挂载子组件,即子组件初始化。hydrating为服务端渲染,所以此时为:child.$mount(undefined, hydrating);
// 子组件自己接管$mount挂载过程, 即子组件挂载时,$el为undefined
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在子组件初始化时,在initInternalComponent (opens new window)阶段,又将componentOptions的propsData,放到了opts.propsData (opens new window)上了。这样,我们在初始化子组件并调用initState (opens new window)时,就可以通过vm.$options.propsData拿到从父组件传递的数据了。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
// 以下变量,在createComponentInstanceForVnode中赋值
// 父级vnode
const parentVnode = options._parentVnode
// 父级构造函数
opts.parent = options.parent
opts._parentVnode = parentVnode
// 获取父组件的componentOptions,包括propsData、listeners、children
// <App age={100} title='标题' on={ {test: this.handler} }>
// <div>children</div>
// </App>
// propsData: 传递给子组件的prop属性:age、title(子组件中必须在props中顶替)
// listeners:传递给子组件的事件:{ test: fn }
// children:传递给子组件的children,通常是插槽: [VNode]
const vnodeComponentOptions = parentVnode.componentOptions
// 从vnodeComponentOptions中提取父传给子的props数据, 参考src\core\vdom\create-component.js 178行的extractPropsFromVNodeData方法
opts.propsData = vnodeComponentOptions.propsData
// 父传给子的自定义listeners事件
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
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
整个过程我们以流程图的方式呈现如下,主要包括2个部分:
- 创建vnode阶段,从data中获取props数据,并存放到vnode的componentOptions的propsData中。
- 根据vnode创建dom阶段:子组件初始化并挂载,并在initInternalComponent阶段,提取propsData并存放到vm.$options.propsData上。在initState阶段,对props进行初始化。

# $emit传递数据(从子到父)
我们通过createComponent (opens new window)方法,创建组件vnode的时候,会将data.on中的数据提取出来,从存放到vnode的componentOptions中。data.on就是我们传递的listener。
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
data = data || {};
// 绑定在组件上的监听函数(组件的自定义事件)
const listeners = data.on;
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
而在子组件初始化时,我们通过initEvents (opens new window)方法,对组件的事件进行初始化,这个过程主要做了2件事:
- 创建一个空对象,并赋值给
vm._events对象。 - 调用updateComponentListeners (opens new window),将对应key的handler,存放到
vm._event对象上。
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
// 初始化父组件传递的listeners事件
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
2
3
4
5
6
7
8
9
10
$emit方法(还有$on, $off,$once)等方法,都是在导入Vue时,给Vue原型扩展的方法。详细参考eventsMixin(Vue) (opens new window)。
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
//...
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
// ...
}
Vue.prototype.$emit = function (event: string): Component {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
当我们调用$emit('eventname', data)时,就会从vm._events中寻找对应key的fn数组,并依次执行。这是一个典型的发布订阅模式,后续的eventBus也是通过这种方式实现的。
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
2
3
4
5
6
7
8
9
10
11
12
13
# 2、$parents、$children
在子组件初始化过程中,当我们调用componentVNodeHooks.init方法时,通过createComponentInstanceForVnode (opens new window)创建vnode实例时,传入了当前vm的实例activeInstance,而activeInstance就是当前vnode的父组件实例。
创建完实例后,进入子组件$mount阶段。
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
if ( // 初次渲染,并且对于keep-alive为true的vnode,执行prepatch钩子,否则,执行子组件child.mount过程
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 通过createComponentInstanceForVnode创建一个Vue子类的实例,并赋值给vnode.componentInstance
// activeInstance:它的作用就是保持当前上下文的 Vue 实例(表示当前vm的实例, 此时在子组件初始化过程中,就可以确定父子关系,即$children, $parent的关系)
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 挂载子组件,即子组件初始化。hydrating为服务端渲染,所以此时为:child.$mount(undefined, hydrating);
// 子组件自己接管$mount挂载过程, 即子组件挂载时,$el为undefined
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
我们来看下createComponentInstanceForVnode到底做了什么?
- 创建了一个options选项,存放了parent(父vnode实例),_parentVnode(父vnode),以及一个组件标识符_isComponent(结果为true)。
- new了一个Vue子类Sub的实例,并进入子组件初始化阶段,即_init (opens new window)阶段。
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
// vm._init函数中,对options的判断,参考代码:/src/core/instance/init.js,32行,对options的判断
const options: InternalComponentOptions = {
_isComponent: true, // 是否是一个component(子组件初始化时,为true)
_parentVnode: vnode,
parent // 父组件vm实例
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// vnode.componentOptions.Ctor就是子组件的构造函数Sub
/**
* 因为:在调用_createElement创建vnode时,如果tag是一个component,会调用createComponent(vdom/create-element.js,126行)方法来创建组件vnode
* 这个函数内部,Ctor = baseCtor.extend(Ctor),即Ctor是通过Vue.extend(global-api/extend.js,19行)返回的子类构造函数Sub。
* 然后调用new VNode()生成vnode,传入的componentOptions为:{ Ctor, propsData, listeners, tag, children }
* 所以vnode.componentOptions.Ctor就是Sub构造函数
*/
// 相当于new Sub(options),注意此时options比较特殊(参考组件_init时,options的合并策略)
return new vnode.componentOptions.Ctor(options)
}
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
在子组件初始化过程中,首先调用initInternalComponent (opens new window)方法,将parent、parentVnode赋值给当前的parent及_parentVnode。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// 父级vnode
const parentVnode = options._parentVnode
// 父级构造函数
opts.parent = options.parent
opts._parentVnode = parentVnode
// ...
}
2
3
4
5
6
7
8
9
然后调用initLifecycle (opens new window)初始化父子关系,注意,抽象组件不在父子关系之列(例如keep-alive,transition等)
- $parent为当前vm的父实例;
- $children为当前vm的子实例数组;
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
// 确定父子关系时,跳过abstract组件,例如内置的keep-alive组件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
// $parent,当前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
至此,我们就可以通过$parent、$children分别拿到父子实例对象了。
# $dispatch与$broadcast的实现方式
这两个方法,主要就是基于$children、$parent、$emit的封装,我们直接看下其基本实现。在element-ui中的实现,可参考这里 (opens new window)。
$dispatch:通过$parent向上查找父组件,并调用$emit触发eventName。
function dispatch(componentName, eventName, params) {
// 查找父组件实例
var parent = this.$parent || this.$root;
// 父组件的名称
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
// 如果parent存在,并且name不匹配(或者name为空),则继续向上查找
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
// 如果找到了parent
if (parent) {
// 调用实例的$emti方法,向上传递数据
parent.$emit.apply(parent, [eventName].concat(params));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$broadcast:通过$children向下查找子组件,并调用$emit触发eventName。
// 通过广播的方式,向下查找
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
// 名称匹配,则触发事件
child.$emit.apply(child, [eventName].concat(params));
} else {
// 不匹配,则递归调用broadcast,向下广播
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3、provide、inject
在组件_init阶段,通过initInjections (opens new window)初始化injection,通过initProvide (opens new window)初始化provide。其基本原理是:
- 将provide的数据,存放到vm._provided对象上;
- 使用inject选项时,从当前vm._provided对象上查找对应key的值,找到了则返回。找不到,则继续向上查找父组件实例的_provided对象,直到找到为止。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
callHook(vm, 'beforeCreate')
// provide ,inject核心就是$parent,通过while循环一直向上查找
// 查找父组件的vm._provided上对应的key值,并将vm.key设置为响应式的:defineReactive(vm, key, result[key])
initInjections(vm) // resolve injections before data/props, 在data/props之前,处理injections
initState(vm)
// 将provide的对象,放到vm._provided上,即vm._provided = provideObject || provideFunc(), 这样就可以根据key获取到inject的数据
initProvide(vm) // resolve provide after data/props, 在data/props之后,处理provide
callHook(vm, 'created')
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
initProvide:
从vm.$options.provide中读取provide选项,并将provide的值存放到vm_provided上。
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
initInjections:
- 通过resolveInject,从vm._provided上读取对应key的值,并存放到result对象上,即
reslut = { key: value }。 - 通过defineReactive,设置vm、key、result[key]的响应式。
export function initInjections(vm: Component) {
// 1. 查找父组件的vm._provided上对应的key的值(递归向上查找)
const result = resolveInject(vm.$options.inject, vm)
if (result) {
Object.keys(result).forEach(key => {
// ...
defineReactive(vm, key, result[key])
})
}
}
2
3
4
5
6
7
8
9
10
resolveInject:
从当前vm._provided中查找对应的key,如果存在,返回key对应的值。如果不存在,继续向上查找,直到找到为止。
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)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(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
// 递归查找父组件上的provide对应key,找到则返回
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)
}
}
}
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
# 4、ref
在Vue官方文档中,我们知道,可以通过ref (opens new window)获取组件实例。
<base-input ref="usernameInput"></base-input>
拿到了vm实例,则可以直接调用实例的方法,或者通过$parent、$children的方式,获取对应的父/子组件实例了。 而ref属性,是在子组件挂载后,在执行initComponent (opens new window)方法时,内部执行了registerRef (opens new window)方法,将vnode对应的组件实例(组件vnode),或者普通vnode对应的dom,赋值给ref属性:
export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
const key = vnode.data.ref
if (!isDef(key)) return
const vm = vnode.context
// 取值vnode实例,或者vnode对应的dom元素
const ref = vnode.componentInstance || vnode.elm
const refs = vm.$refs
if (isRemoval) {
if (Array.isArray(refs[key])) {
remove(refs[key], ref)
} else if (refs[key] === ref) {
refs[key] = undefined
}
} else {
if (vnode.data.refInFor) { // 在for循环中使用ref
if (!Array.isArray(refs[key])) {
refs[key] = [ref]
} else if (refs[key].indexOf(ref) < 0) {
// $flow-disable-line
refs[key].push(ref)
}
} else {
// 将ref赋值给refs
refs[key] = ref
}
}
}
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
# 5、EventBus事件总线
eventBus事件总线,利用的就是Vue原型的$on、$off、$emit方法。其基本原理就是发布订阅模式及$emit (opens new window),参考源码eventMixin (opens new window)。
Vue.prototype.$bus = new Vue();
// 订阅事件时:
this.$bus.$on('event-name', handler);
// 发布事件
this.$bus.emit('event-name', data);
// 取消订阅
this.$bus.off('event-name');
2
3
4
5
6
7
8
9
10
我们也可以手动实现一个EventBus:
class EventBus {
constructor() {
this.events = {};
}
on(name, listener) {
if(!this.events) this.events = {};
if(this.events[name]) {
this.events[name].push(listener);
} else {
this.events[name] = [listener];
}
}
emit(name, ...args) {
if(this.events[name]) {
this.events[name].forEach(fn => fn.call(this, ...args));
}
}
once(name, listener) {
const self = this;
function oneTime(...args) {
listener.call(this, ...args);
self.off(name, oneTime);
}
self.on(name, oneTime);
}
off(name, listener) {
if(this.events[name]) {
const index = this.events[name].indexOf(listener);
this.events[name].splice(index, 1);
}
}
}
let bus = new EventBus();
bus.on('say',function(str) {
console.log(str);
});
bus.once('say', function(str) {
console.log('once:' + str)
});
bus.emit('say','test1'); // test1 once:test1
bus.emit('say','tes2'); // test1
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