阅读 369

Vue通过$emit实现父子组件的通讯原理

父子组件通讯的方法之一是使用$emit。 该方法实现步骤:

  • 在父组件内,对子组件的占位符标签上绑定一个自定义事件回调
  • 在子组件内,调用$emit

首先了解Vue.prototype.$emit的定义

它的作用是循环执行当前 vm (组件实例)的 _events 属性内某个 event (事件名)对应的事件回调列表。也就是触发事件。

Vue.prototype.$emit定义在src/core/instance/events.js中。

Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    ...
    // 获取 _events 属性内某个 event (事件名)对应的事件回调列表
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      // 循环执行回调
      for (let i = 0, l = cbs.length; i < l; i++) {
        ...
        cbs[i].apply(vm, args)
        ...
      }
    }
    return vm
  }
复制代码

而 vm 的 _events 属性内的对 event 的回调方法收集全部是通过Vue.prototype.$on方法收集的。即事件监听。

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    ...
    // 对 event 的回调方法进行收集
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    ...
    return vm
  }
复制代码

通过$emit实现父子组件通讯的第一个步骤:在父组件内,对子组件的占位符标签上绑定一个自定义事件回调。这个绑定动作最终将通过子组件的$on方法将回调进行收集。

在父组件内,对子组件的占位符标签上绑定一个自定义事件回调,怎么被子组件收集。

例子:
父组件:

import Child from './Child.js'
export default {
  name: 'Parent',
  template: `<div class="parent-component">
    <Child v-on:custom_event="handleCustomEvent"></Child>
  </div>`,
  methods: {
    handleCustomEvent() {
      console.log('this.$options.name:', this.$options.name)
    }
  },
  components: {
    Child
  }
}
复制代码

子组件

export default {
  name: 'Child',
  template: `<div class="child-component" v-on:click="handleClick">click me!</div>`,
  methods: {
    test() {
      console.log('test')
    }, 
    handleClick() {
      this.$emit('custom_event')
    }
  }
}
复制代码

以下将以上两个组件为例,讲解通过$emit实现父子组件的通讯原理

parse (解析)父组件模版

模版解析过程就是AST (虚拟树)的生成过程、是通过各种正则表达式来匹配到节点的各个部分并处理。

匹配属性

<Child v-on:custom_event="handleCustomEvent"></Child>
复制代码

子节点占位符标签的属性v-on:custom_event="handleCustomEvent"被使用以下正则表达式被匹配到:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
复制代码

正则表达式=前面的作为name值,=后面作为value值,组成一个对象保存在节点的attrs队列中。 匹配结果:

{
    name: 'v-on:custom_event',
    value: 'handleCustomEvent'
}
复制代码

处理属性

节点处理过程中,attrs会被循环遍历,通过不同的正则匹配对属性name进行匹配分类,对不同类别的属性做不同的处理。

例子中占位符子节点的属性会被const onRE = /^@|^v-on:/这个正则被匹配到,属性被处理添加到占位符子节点的events属性内。

{
    tag: 'Child'
    events: {
        'custom_event': {value: 'handleCustomEvent'}
    }
    ...
}
复制代码

父组件渲染函数的生成

组件模版解析成虚拟树后再被生成代码字符串。

节点的events属性会以字符串的形式被添加到一个data的属性on中:

export function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean,
  warn: Function
): string {
  let res = isNative ? 'nativeOn:{' : 'on:{'
  // genHandler会对事件回调做一些处理
  for (const name in events) {
    res += `"${name}":${genHandler(name, events[name])},`
  }
  return res.slice(0, -1) + '}'
}

function genHandler (
  name: string,
  handler: ASTElementHandler | Array<ASTElementHandler>
): string {
  ...
  // 把handler.value组装成执行命令的字符串
    const handlerCode = isMethodPath
      ? handler.value + '($event)'
      : isFunctionExpression
        ? `(${handler.value})($event)`
        : handler.value
    return `function($event){${code}${handlerCode}}`
  ...
}
复制代码

而data会成为创建虚拟节点函数的参数。 例子中父组件模版最后被编译成字符串

"with(this){return _c('div',{staticClass:"parent-component"},[_c('Child',{on:{"custom_event":handleCustomEvent}})],1)}"
复制代码

其中方法_c是 Vnode (虚拟节点)生成方法。 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

生成Vnode虚拟树

createElement方法定义在src/core/vdom/create-element.js。该方法实质上是调用了_createElement。

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode {
...
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    }
...
  if (isDef(vnode)) {
    if (ns) applyNS(vnode, ns)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
复制代码

例子中的_c('Child',{on:{"custom_event":handleCustomEvent}})这段代码最终会执行上面的vnode = createComponent(Ctor, data, context, children, tag) 其中data就是{on:{"custom_event":handleCustomEvent}}

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | void {
  ...
  // data.on的数据将作为实例化一个Vnode的componentOptions的listeners参数
  const listeners = data.on
  ...
  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}
复制代码

实例化子组件

patch过程就是将Vnode转化成真实节点,当转化过程中遇到组件子节点时会递归得实例化子组件,子组件生成Vnode Tree,Vnode Tree经过patch生成正式的节点树,然后返回上一级。

在实例化子组件前,Vnode的数据被重写整合成options,作为实例化子组件的参数。其中listeners成为options._parentListeners。

在实例化子组件时,执行initEvents方法,将所有的options._parentListeners添加到子组件的实例上

function add (event, fn, once) {
// target是当前组件实例
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}
复制代码

终于在父组件模版中的v-on:custom_event="handleCustomEvent"历经千山万水通过子组件实例的$on方法添加到子组件实例的_events中

事件回调中访问父组件

组件在实例化过程中initState(初始化状态),在初始化状态时会执行initMethodsinitMethods这个方法的工作就是通过bind方法,使得 methods 中的方法 this 指向当前 vm (实例)。

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    ...
    // bind方法
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}
复制代码
export function bind (fn: Function, ctx: Object): Function {
  function boundFn (a) {
    const l: number = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }
  // record original fn length
  boundFn._length = fn.length
  return boundFn
}
复制代码

父组件在初始化过程中,已经将handleCustomEventthis绑定父组件, 所以当子组件通过触发custom_event方法时,可以在回调方法中访问到父组件,也就形成父子组件的通讯。