【Vue】【选项API - 组件实例】$emit()

78 阅读4分钟

一、$emit()的介绍

1.1 基础知识

  • 简介:生命组件触发的自定义事件 - 父组件自定义事件,在子组件中需要通过$emit()触发

  • 语法

    • this.$emit(eventName, ...args)
    • eventName:String;触发的自定义事件名称
    • args:any[],传参
  • 作用:触发当前实例上的事件,附加参数都会传给监听器回调

  • 使用步骤

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

    export default {
      created() {
        // 仅触发事件
        this.$emit('foo')
        // 带有额外的参数
        this.$emit('bar', 1, 2, 3)
      }
    }
    

1.2 示例

// 父组件
<template>
  <div>
    <ChildComponent
      @customEvent1="customEvent1Handle"
      @customEvent2="customEvent2Handle"
    />
  </div>
</template>
​
export default {
  name: "ParentComponent",
  methods: {
    customEvent1Handle() {
      console.log("customEvent1Handle:::, 没有传参");
    },
    customEvent2Handle(arg1, arg2, arg3) {
      console.log("customEvent2Handle:::,传参分别如下:")
      console.log("arg1::: ", arg1) // arg1::: 1
      console.log("arg2::: ", arg2) // arg2::: [1, 2, 3]
      console.log("arg3::: ", arg3) // arg3::: { name: "zhangsan" }
    },
  },
}
​
// 子组件
export default {
  name: "ChildComponent",
  created() {
    // 仅触发事件
    this.$emit('customEvent1')
    // 带有额外的参数
    this.$emit('customEvent2', 1, [1, 2, 3], { name: "zhangsan" })
  }
}

二、$emit()的原理和源码分析

  • 原理

    采用了发布订阅者设计模式

    1. 根据传入的事件名从当前实例的 _events 属性(事件中心)中,获取该事件名所对应的回调函数cbs
    2. 再获取传入的附加参数 args
    3. 遍历回调函数数组获取回调函数
    4. 执行回调函数,并将附加参数args传回给该回调
  • 源码版本:2.7.14

  • 源码

    $emit() - src\core\instance\events.ts

    // event: 触发的自定义事件名称
    Vue.prototype.$emit = function (event: string): Component {
      const vm: Component = this
      if (__DEV__) {
        const lowerCaseEvent = event.toLowerCase()
        if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
          tip(
            `Event "${lowerCaseEvent}" is emitted in component ` +
              `${formatComponentName(
                vm
              )} but the handler is registered for "${event}". ` +
              `Note that HTML attributes are case-insensitive and you cannot use ` +
              `v-on to listen to camelCase events when using in-DOM templates. ` +
              `You should probably use "${hyphenate(
                event
              )}" instead of "${event}".`
          )
        }
      }
      // _events: 事件中心
      // cbs: 该事件名所对应的回调函数
      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
    }
    

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

    Vue.prototype.$on = function (
      event: string | Array<string>,
      fn: Function
    ): Component {
      const vm: Component = this
      if (isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
          vm.$on(event[i], fn)
        }
      } else {
        // 对 event 的回调方法进行收集
        ;(vm._events[event] || (vm._events[event] = [])).push(fn)
        // optimize hook:event cost by using a boolean flag marked at registration
        // instead of a hash lookup
        if (hookRE.test(event)) {
          vm._hasHookEvent = true
        }
      }
      return vm
    }
    

    invokeWithErrorHandling() - src\core\util\error.ts

    export function invokeWithErrorHandling(
      handler: Function,
      context: any,
      args: null | any[], // 传参
      vm: any,
      info: string
    ) {
      let res
      try {
        // 有传参就用apply;没有传参就用 call
        res = args ? handler.apply(context, args) : handler.call(context)
        if (res && !res._isVue && isPromise(res) && !(res as any)._handled) {
          res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
          // issue #9511
          // avoid catch triggering multiple times when nested calls
          ;(res as any)._handled = true
        }
      } catch (e: any) {
        handleError(e, vm, info)
      }
      return res
    }
    

三、如何实现父子组件的通讯原理

子组件是如何收集父组件内对子组件占位符绑定的自定义事件回调

  • 父组件解析

    模板解析源码src\compiler\parser\html-parser.ts

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

    通过以下正则表达式匹配子组件自定义事件 v-on:customEvent1="customEvent1Handle"

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

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

    {
        name: 'v-on:on:customEvent1',
        value: 'customEvent1Handle'
    }
    

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

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

    {
        tag: 'ChildComponent'
        events: {
            'customEvent1': {value: 'customEvent1Handle'},
            'customEvent2': {value: 'customEvent2Handle'},
        }
        ...
    }
    ​
    
  • 父组件渲染函数的生成:

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

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

src\compiler\codegen\events.ts

export function genHandlers(
  events: ASTElementHandlers,
  isNative: boolean
): string {
  const prefix = isNative ? 'nativeOn:' : 'on:'
  let staticHandlers = ``
  let dynamicHandlers = ``
  for (const name in events) {
    const handlerCode = genHandler(events[name])
    //@ts-expect-error
    if (events[name] && events[name].dynamic) {
      dynamicHandlers += `${name},${handlerCode},`
    } else {
      staticHandlers += `"${name}":${handlerCode},`
    }
  }
  staticHandlers = `{${staticHandlers.slice(0, -1)}}`
  if (dynamicHandlers) {
    return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
  } else {
    return prefix + staticHandlers
  }
}
​
function genHandler(
  handler: ASTElementHandler | Array<ASTElementHandler>
): string {
  if (!handler) {
    return 'function(){}'
  }
​
  if (Array.isArray(handler)) {
    return `[${handler.map(handler => genHandler(handler)).join(',')}]`
  }
​
  const isMethodPath = simplePathRE.test(handler.value)
  const isFunctionExpression = fnExpRE.test(handler.value)
  const isFunctionInvocation = simplePathRE.test(
    handler.value.replace(fnInvokeRE, '')
  )
​
  if (!handler.modifiers) {
    if (isMethodPath || isFunctionExpression) {
      return handler.value
    }
    return `function($event){${
      isFunctionInvocation ? `return ${handler.value}` : handler.value
    }}` // inline statement
  } else {
    let code = ''
    let genModifierCode = ''
    const keys: string[] = []
    for (const key in handler.modifiers) {
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key]
        // left/right
        if (keyCodes[key]) {
          keys.push(key)
        }
      } else if (key === 'exact') {
        const modifiers = handler.modifiers
        genModifierCode += genGuard(
          ['ctrl', 'shift', 'alt', 'meta']
            .filter(keyModifier => !modifiers[keyModifier])
            .map(keyModifier => `$event.${keyModifier}Key`)
            .join('||')
        )
      } else {
        keys.push(key)
      }
    }
    if (keys.length) {
      code += genKeyFilter(keys)
    }
    // Make sure modifiers like prevent and stop get executed after key filtering
    if (genModifierCode) {
      code += genModifierCode
    }
    const handlerCode = isMethodPath
      ? `return ${handler.value}.apply(null, arguments)`
      : isFunctionExpression
      ? `return (${handler.value}).apply(null, arguments)`
      : isFunctionInvocation
      ? `return ${handler.value}`
      : handler.value
    return `function($event){${code}${handlerCode}}`
  }
}
  • 生成VNode虚拟树

    createElement - src\core\vdom\create-element.ts

    // wrapper function for providing a more flexible interface
    // without getting yelled at by flow
    export function createElement(
      context: Component,
      tag: any,
      data: any,
      children: any,
      normalizationType: any,
      alwaysNormalize: boolean
    ): VNode | Array<VNode> {
      if (isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
      }
      if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE
      }
      return _createElement(context, tag, data, children, normalizationType)
    }
    export function _createElement(
      context: Component,
      tag?: string | Component | Function | Object,
      data?: VNodeData,
      children?: any,
      normalizationType?: number
    ): VNode | Array<VNode> {
      if (isDef(data) && isDef((data as any).__ob__)) {
        __DEV__ &&
          warn(
            `Avoid using observed data object as vnode data: ${JSON.stringify(
              data
            )}\n` + 'Always create fresh vnode data objects in each render!',
            context
          )
        return createEmptyVNode()
      }
      // object syntax in v-bind
      if (isDef(data) && isDef(data.is)) {
        tag = data.is
      }
      if (!tag) {
        // in case of component :is set to falsy value
        return createEmptyVNode()
      }
      // warn against non-primitive key
      if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
        warn(
          'Avoid using non-primitive value as key, ' +
            'use string/number value instead.',
          context
        )
      }
      // support single function children as default scoped slot
      if (isArray(children) && isFunction(children[0])) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
      }
      if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children)
      } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
      }
      let vnode, ns
      if (typeof tag === 'string') {
        let Ctor
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
        if (config.isReservedTag(tag)) {
          // platform built-in elements
          if (
            __DEV__ &&
            isDef(data) &&
            isDef(data.nativeOn) &&
            data.tag !== 'component'
          ) {
            warn(
              `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
              context
            )
          }
          vnode = new VNode(
            config.parsePlatformTagName(tag),
            data,
            children,
            undefined,
            undefined,
            context
          )
        } else if (
          (!data || !data.pre) &&
          isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
        ) {
          // component
          vnode = createComponent(Ctor, data, context, children, tag)
        } else {
          // unknown or unlisted namespaced elements
          // check at runtime because it may get assigned a namespace when its
          // parent normalizes children
          vnode = new VNode(tag, data, children, undefined, undefined, context)
        }
      } else {
        // direct component options / constructor
        vnode = createComponent(tag as any, data, context, children)
      }
      if (isArray(vnode)) {
        return vnode
      } else if (isDef(vnode)) {
        if (isDef(ns)) applyNS(vnode, ns)
        if (isDef(data)) registerDeepBindings(data)
        return vnode
      } else {
        return createEmptyVNode()
      }
    }
    
  • 实例化子组件

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

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

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

    initEvents - src\core\instance\events.ts

    export function initEvents(vm: Component) {
      vm._events = Object.create(null)
      vm._hasHookEvent = false
      // init parent attached events
      // 将所有的`options._parentListeners`添加到子组件的实例上
      const listeners = vm.$options._parentListeners
      if (listeners) {
        updateComponentListeners(vm, listeners)
      }
    }
    ​
    export function updateComponentListeners(
      vm: Component,
      listeners: Object,
      oldListeners?: Object | null
    ) {
      target = vm
      updateListeners(
        listeners,
        oldListeners || {},
        add,
        remove,
        createOnceHandler,
        vm
      )
      target = undefined
    }
    ​
    export function updateListeners(
      on: Object,
      oldOn: Object,
      add: Function,
      remove: Function,
      createOnceHandler: Function,
      vm: Component
    ) {
      let name, cur, old, event
      for (name in on) {
        cur = on[name]
        old = oldOn[name]
        event = normalizeEvent(name)
        if (isUndef(cur)) {
          __DEV__ &&
            warn(
              `Invalid handler for event "${event.name}": got ` + String(cur),
              vm
            )
        } else if (isUndef(old)) {
          if (isUndef(cur.fns)) {
            cur = on[name] = createFnInvoker(cur, vm)
          }
          if (isTrue(event.once)) {
            cur = on[name] = createOnceHandler(event.name, cur, event.capture)
          }
          add(event.name, cur, event.capture, event.passive, event.params)
        } else if (cur !== old) {
          old.fns = cur
          on[name] = old
        }
      }
      for (name in oldOn) {
        if (isUndef(on[name])) {
          event = normalizeEvent(name)
          remove(event.name, oldOn[name], event.capture)
        }
      }
    }
    

终于在父组件模版中的@customEvent1="Custom1Eventhandle"通过子组件实例的$on方法添加到子组件实例的_events

参考: