一、$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()的原理和源码分析
-
原理:
采用了发布订阅者设计模式
- 根据传入的事件名从当前实例的
_events属性(事件中心)中,获取该事件名所对应的回调函数cbs - 再获取传入的附加参数
args - 遍历回调函数数组获取回调函数
- 执行回调函数,并将附加参数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.tsexport 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中