# 组件通信方式及实现原理

在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)
  // ...
}
1
2
3
4
5
6
7

initProps的作用,主要是:

  1. 获取从父组件传递的props数据,这些数据存放在vm.$options.propsData上。
  2. 对props的数据进行格式校验。
  3. 定义props的响应式。
  4. 代理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)
    }
  }
}
1
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
}
1
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
    }
  }
1
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 */)
      }
  }
1
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)
    }
  }
}
1
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
  }
}
1
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个部分:

  1. 创建vnode阶段,从data中获取props数据,并存放到vnode的componentOptions的propsData中。
  2. 根据vnode创建dom阶段:子组件初始化并挂载,并在initInternalComponent阶段,提取propsData并存放到vm.$options.propsData上。在initState阶段,对props进行初始化。

Vue组件通信方式详解.png

# $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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

而在子组件初始化时,我们通过initEvents (opens new window)方法,对组件的事件进行初始化,这个过程主要做了2件事:

  1. 创建一个空对象,并赋值给vm._events对象。
  2. 调用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)
  }
}
1
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 {
  	// ...
  }
}
1
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
  }
1
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)
    }
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

我们来看下createComponentInstanceForVnode到底做了什么?

  1. 创建了一个options选项,存放了parent(父vnode实例),_parentVnode(父vnode),以及一个组件标识符_isComponent(结果为true)。
  2. 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)
}
1
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
	// ...
}
1
2
3
4
5
6
7
8
9

然后调用initLifecycle (opens new window)初始化父子关系,注意,抽象组件不在父子关系之列(例如keep-alive,transition等)

  1. $parent为当前vm的父实例;
  2. $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
}
1
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));
  }
}
1
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]));
    }
  });
}
1
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。其基本原理是:

  1. 将provide的数据,存放到vm._provided对象上;
  2. 使用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')
    // ...
  }
1
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
  }
}
1
2
3
4
5
6
7
8

initInjections:

  1. 通过resolveInject,从vm._provided上读取对应key的值,并存放到result对象上,即reslut = { key: value }。
  2. 通过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])
    })
  }
}
1
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
  }
}
1
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>
1

拿到了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
    }
  }
}
1
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');
1
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
1
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