You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// emit方法绑定组件实例// packages/runtime-core/src/component.ts #484instance.emit=emit.bind(null,instance)// packages/runtime-core/src/componentEmits.ts #46exportfunctionemit(instance: ComponentInternalInstance,event: string,
...rawArgs: any[]){constprops=instance.vnode.props||EMPTY_OBJ// 开发环境,如果触发的事件没有在emit或props选项中声明时,警告开发者if(__DEV__){const{
emitsOptions,propsOptions: [propsOptions]}=instanceif(emitsOptions){if(!(eventinemitsOptions)){if(!propsOptions||!(toHandlerKey(event)inpropsOptions)){warn(`Component emitted event "${event}" but it is neither declared in `+`the emits option nor as an "${toHandlerKey(event)}" prop.`)}}else{constvalidator=emitsOptions[event]if(isFunction(validator)){constisValid=validator(...rawArgs)if(!isValid){warn(`Invalid event arguments: event validation failed for event "${event}".`)}}}}}letargs=rawArgs// 判断是否为双向绑定事件constisModelListener=event.startsWith('update:')// for v-model update:xxx events, apply modifiers on argsconstmodelArg=isModelListener&&event.slice(7)if(modelArg&&modelArginprops){// 修饰符名称constmodifiersKey=`${modelArg==='modelValue' ? 'model' : modelArg}Modifiers`const{ number, trim }=props[modifiersKey]||EMPTY_OBJif(trim){args=rawArgs.map(a=>a.trim())}elseif(number){args=rawArgs.map(toNumber)}}// convert handler name to camelCase. See issue #2249// 转换事件名为 `onXxx`lethandlerName=toHandlerKey(camelize(event))// 匹配事件回调lethandler=props[handlerName]// for v-model update:xxx events, also trigger kebab-case equivalent// for props passed via kebab-caseif(!handler&&isModelListener){handlerName=toHandlerKey(hyphenate(event))handler=props[handlerName]}// 执行事件回调if(handler){callWithAsyncErrorHandling(handler,instance,ErrorCodes.COMPONENT_EVENT_HANDLER,args)}// 执行一次性回调constonceHandler=props[handlerName+`Once`]if(onceHandler){if(!instance.emitted){;(instance.emitted={}asRecord<string,boolean>)[handlerName]=true}elseif(instance.emitted[handlerName]){return}callWithAsyncErrorHandling(onceHandler,instance,ErrorCodes.COMPONENT_EVENT_HANDLER,args)}}
背景
我们都知道
Vue 2
内部实现了事件的发布订阅,不仅在Vue
内部机制中使用,开发人员经常把它当做事件总线来使用,主要Api
如下:但是在
Vue 3
中,只剩下了$emit
,其余Api
全部移除了,因为Vue 3
内部不再需要这套事件发布订阅机制,所以没有必要实现,这样也能减小Vue
的体积,倘若开发人员需要使用事件发布订阅模式,完全可以自己实现或者使用其他现成的类库。那么vue 3脱离了事件发布订阅机制,怎么实现事件系统呢?
VNode 扁平化
研究事件之前我们来看一下
Vue 3
中props
的变化,Vue 3
中VNode
现在是一个扁平的prop
结构,包括用户自定义的属性和事件回调,这个改变使prop
的结构变得简单,也有利于其他功能的实现,比如事件系统,对比一下2.x
和3.x
的props
结构:组件事件
既然
$emit
还存在,那么先了解一下其具体实现:如果需要触发一个用户的事件,只需要在组件内调用
emit
,并传入事件名,根据事件名解析prop
中的回调并执行,这样就达到了事件绑定、事件触发的效果了,确实不需要一个完整的发布订阅的实现。其中,
VNode
中事件回调的key
必须是已on
开头的驼峰式命名,内部会把这种命名的prop
解析到事件对象中,当然如果使用template
我们不用关心命名问题,跟Vue 2
中的语法一样,在template
编译后,事件prop
会被编译成onXxx
的形式。注意:上述
emit
的过程是组件自定义事件的触发流程,Vue 3
也增加了emits
选项给与特定的限制和功能的支持。原生DOM事件
原生事件是由外部的输入设备交互所触发的,所以不需要关心事件的触发问题,关键是在于事件是在什么时候被注册的。
下面我们来追溯一下原生元素的创建以及事件的注册。
原生
dom
的prop
格式也遵循上面的新规范,所以事件回调传递的方式是一样的,原生事件最终肯定是需要注册在dom
上,那么我们就来分析一下mountElement
中关于props
的处理逻辑,废话少说,咱么直接定位到源码位置这里我们发现,对于
dom
的处理,Vue 采用依赖注入的方式,这样可以更好的提供跨平台能力,接下来我们来分析基于 web dom 的patchProp
,代码定位:到了这里我们定位到了原生元素事件注册的地方,当原生元素创建后,紧接着会进行
props
处理,其中包括事件的注册。延伸
我们都知道,在vue的template中事件是支持修饰符的,如下:
.stop
- 调用event.stopPropagation()
。.prevent
- 调用event.preventDefault()
。.capture
- 添加事件侦听器时使用 capture 模式。.self
- 只当事件是从侦听器绑定的元素本身触发时才触发回调。.{keyAlias}
- 仅当事件是从特定键触发时才触发回调。.once
- 只触发一次回调。.left
- 只当点击鼠标左键时触发。.right
- 只当点击鼠标右键时触发。.middle
- 只当点击鼠标中键时触发。.passive
-{ passive: true }
模式添加侦听器其中
**/(?:Once|Passive|Capture)$/**
这三个修饰符是属于原生监听的选项(第三个参数),所以在事件绑定(上面代码中)时已经处理,那么其他的修饰符在什么时候处理的呢、怎样处理的呢?经过一顿操作,终于定位到了,当然,过程比较坎坷
修饰符这里也是基于不同平台而实现的,上层包为下层包提供具体
Api
实现,底层包实现函数名注入(编译层)我们通过一个例子来看一下编译的结果:
编译结果:
我们看到了
_withModifiers
方法,这就是实际调用的地方,当有修饰符时,才会编译出这样的代码。总结
Vue 3
中删除了Vue 2
中发布订阅机制的实现,改为更简单直接的实现方式,一方面减少了源码的体积,另一方面更加专注于框架本身,删除了一些没有必要的概念和实现方式,避免Api
的滥用造成过高的维护成本,大道至简!The text was updated successfully, but these errors were encountered: