问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

Vue事件原理(从源码角度带你分析)(4)

发布网友 发布时间:2024-09-05 05:37

我来回答

1个回答

热心网友 时间:2024-09-29 17:39

之前我们已经介绍了event的编译过程(点击这里跳转),接下来我们分析在Vue初始化和更新的过程中event的内部是如何生成的。

event生成之自定义事件

Vue中event事件分为原生DOM事件与自定义事件,原生DOM事件的处理(点击这里跳转),我们上一节已经分析过了。这一节我们来分析下自定义事件。

自定义事件是用在组件节点上的,组件节点上定义的事件可以分为两类:一类是原生DOM事件(在vue2.x版本在组件节点上使用原生DOM事件需要添加native修饰符),另一类就是自定义事件。

下面我们来分析自定义事件的流程:

创建组件vnode

创建组建vnode(虚拟节点)的时候会执行createComponent函数,其中有如下逻辑:

exportfunctioncreateComponent(Ctor:Class<Component>|Function|Object|void,data:?VNodeData,context:Component,children:?Array<VNode>,tag?:string):VNode|Array<VNode>|void{......//extractlisteners,sincetheseneedstobetreatedas//childcomponentlistenersinsteadofDOMlisteners//自定义事件赋值给listenersconstlisteners=data.on//replacewithlistenerswith.nativemodifier//soitgetsprocessedduringparentcomponentpatch.//native事件赋值给data.on,这样原生方法直接就上一节相同的逻辑了data.on=data.nativeOn......//returnaplaceholdervnode//创建占位符vnodeconstname=Ctor.options.name||tag//生成虚拟节点的时候,将listeners当参数传入constvnode=newVNode(`vue-component-${Ctor.cid}${name?`-${name}`:''}`,data,undefined,undefined,undefined,context,{Ctor,propsData,listeners,tag,children},asyncFactory)//返回vnodereturnvnode}

创建组件vnode的过程中会将组件节点上的定义的自定义事件赋值给listeners变量,同时将组件节点上定义的原生事件赋值给data.on属性,这样,组件的原生事件就会执行如同上一节生成原生事件相同的逻辑。然后在创建组件vnode的时候,会将listeners(缓存了自定义事件)当做第七个参数(componentOptions)的属性值。

vnode创建完成之后,在初始化组件的时候,会执行initInternalComponent函数:

组件初始化initInternalComponentexportfunctioninitInternalComponent(vm:Component,options:InternalComponentOptions){//子组件构造器的options(配置项)constopts=vm.$options=Object.create(vm.constructor.options)//....//我们之前创建的节点的第七个参数(componentOptions)constvnodeComponentOptions=parentVnode.componentOptions//子组件构造器的_parentListeners属性指向之前定义的listeners(组件自定义事件)opts._parentListeners=vnodeComponentOptions.listeners//...}

执行完这些配置项的生成之后,会初始化子组件事件

exportfunctioninitEvents(vm:Component){vm._events=Object.create(null)vm._hasHookEvent=false//initparentattachedeventsconstlisteners=vm.$options._parentListeners//有listeners,执行updateComponentListenersif(listeners){updateComponentListeners(vm,listeners)}}

listeners非空,执行updateComponentListeners函数:

lettarget:anyexportfunctionupdateComponentListeners(vm:Component,listeners:Object,oldListeners:?Object){//target指向当前实例target=vm//执行updateListenersupdateListeners(listeners,oldListeners||{},add,remove,vm)target=undefined}

这个地方同样执行updateListeners函数,与上一节原生DOM事件的生成相同,但与原生DOM事件的生成有几处不同之处,如下add与remove函数的定义。

functionadd(event,fn,once){if(once){//如果有once属性,执行$once方法target.$once(event,fn)}else{否则执行$on方法target.$on(event,fn)}}functionremove(event,fn){//remove方法是执行$off方法target.$off(event,fn)}

关于$once、$on、$off函数都定义在eventsMixin中:

exportfunctioneventsMixin(Vue:Class<Component>){consthookRE=/^hook:/Vue.prototype.$on=function(event:string|Array<string>,fn:Function):Component{......}Vue.prototype.$once=function(event:string,fn:Function):Component{......}Vue.prototype.$off=function(event?:string|Array<string>,fn?:Function):Component{......}Vue.prototype.$emit=function(event:string):Component{......}}$onVue.prototype.$on=function(event:string|Array<string>,fn:Function):Component{//当前实例就是调用该方法的实例constvm:Component=this//如果event是数组,遍历数组,依次执行$on函数if(Array.isArray(event)){for(leti=0,l=event.length;i<l;i++){this.$on(event[i],fn)}}else{//将当前实例的_events属性初始化为空数组并push当前添加的函数(vm._events[event]||(vm._events[event]=[])).push(fn)//optimizehook:eventcostbyusingabooleanflagmarkedatregistration//insteadofahashlookupif(hookRE.test(event)){vm._hasHookEvent=true}}returnvm}

$on的逻辑就是将当前的方法存入当前实例vm._events属性中。

$onceVue.prototype.$once=function(event:string,fn:Function):Component{//当前实例就是调用该方法的实例constvm:Component=this//定义on函数functionon(){//执行$off销毁当前事件vm.$off(event,on)//执行函数fnfn.apply(vm,arguments)}//on的fn属性指向当前传入的函数on.fn=fn//将on函数存入vm._events中vm.$on(event,on)returnvm}

$once的逻辑就是对传入的fn函数做了一层封装,生成了一个内部函数on,on.fn属性指向传入函数fn,将on函数存入实例的_events属性对象中,这样执行完一次这个函数后,该函数就被销毁了。

$offVue.prototype.$off=function(event?:string|Array<string>,fn?:Function):Component{//当前实例就是调用该方法的实例constvm:Component=this//all//如果没有传参数,将vm._events置为空对象if(!arguments.length){vm._events=Object.create(null)returnvm}//arrayofevents//event如果是数组,遍历该数组,依次调用$off函数if(Array.isArray(event)){for(leti=0,l=event.length;i<l;i++){this.$off(event[i],fn)}//返回returnvm}//specificevent//唯一的eventconstcbs=vm._events[event]//cbs未定义,直接返回if(!cbs){returnvm}//fn未定义(未传入fn的情况下),vm._events[event]赋值为空,直接返回if(!fn){vm._events[event]=nullreturnvm}//fn定义了if(fn){//specifichandlerletcbleti=cbs.length//遍历cbs对象while(i--){cb=cbs[i]//如果查找到有属性与fn相同if(cb===fn||cb.fn===fn){//移除该属性,跳出循环cbs.splice(i,1)break}}}returnvm}

$off的作用就是移除vm._events对象上定义的事件函数。

eventsMixin中还定义了一个函数$emit,在组件通讯的时候经常使用:

$emitVue.prototype.$emit=function(event:string):Component{//当前实例就是调用该方法的实例constvm:Component=thisif(process.env.NODE_ENV!=='production'){constlowerCaseEvent=event.toLowerCase()if(lowerCaseEvent!==event&&vm._events[lowerCaseEvent]){tip(`Event"${lowerCaseEvent}"isemittedincomponent`+`${formatComponentName(vm)}butthehandlerisregisteredfor"${event}".`+`NotethatHTMLattributesarecase-insensitiveandyoucannotuse`+`v-ontolistentocamelCaseeventswhenusingin-DOMtemplates.`+`Youshouldprobablyuse"${hyphenate(event)}"insteadof"${event}".`)}}//拿到vm._events的event事件上的所有函数letcbs=vm._events[event]//存在cbsif(cbs){//cbs转化cbs=cbs.length>1?toArray(cbs):cbs//其他参数转化成数组constargs=toArray(arguments,1)//遍历cbs,依次执行其中的函数for(leti=0,l=cbs.length;i<l;i++){try{cbs[i].apply(vm,args)}catch(e){handleError(e,vm,`eventhandlerfor"${event}"`)}}}returnvm}

从源码上可以看出,在我们平时开发过程中,其实看似通过$emit方法调用父组件上的函数,本质上是调用组件自身实例上定义的函数,而这个函数是在组件生成的过程中传入到子组件的配置项中的。

还有一点值得提一下,组件自定义事件的事件调用,其实就是非常经典的事件中心的实现。而我们在Vue开发过程中常用的eventBus的实现,原理也是同上。

到此为止,关于Vue的event原理已经大致介绍完毕了,欢迎交流探讨。

原文:https://juejin.cn/post/7097405336251793438
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
找专业防水队做完还漏水怎么维权 法院会受理房屋漏水造成的纠纷吗? 巴西龟最长活多久,家养!!! 养胃的药最好的是什么啊 婴儿积食发烧不愿吃药怎么办 板门穴位在哪个部位 手机设置放偷看的方法? 凝结水回收器生产厂家? 个人账户养老金预测公式:现有5万元,缴费20年,能领多少钱? 临沂比较有名的男装品牌 买卖二手摩托车需要什么手续? 摩托车有哪些手续 买摩托车流程及手续 猪猪乐园怎么打开不了 猪猪乐园怎么上不去了? 我家猪猪这是怎么了,还有鼻子上也掉了一块毛 1992年猴是金还是水五行缺什么 我家孩子上五年级,学的是人教版教材,哪些同步辅导书比较好呢?_百度知 ... 梦见死人和你说话 梦见逝去的亲人跟自己说话好不好 油皮适合哪款氨基酸洗面奶? 冰川风对山谷的影响 3kw单相汽油发电机怎么测电机好坏 冰川变化与气候变化的关系 根据决策条件的可控程度,决策可以分为确定决策、()与非确定型决策。_百... 尼可地尔片什么时候服用 尼可地尔片的最佳服用时间 坐长途汽车需要身份证吗 坐长途汽车要身份证吗 8年前在江阴交社保7个月,后来在淮安交了8年,怎样才能转过来? 江阴银行是国企还是私企 一篇文章看懂Vue.js的11种传值通信方式 被人打了一拳在脖子上,喉咙歪了 在单位 我打了同事两拳 一拳打胳膊上了 一拳打脖子上了 他没还手 他报... 城里人能到农村买宅基地吗 城里人买农村宅基地建房如何处理 神秘巨星原版配音 中文字幕 百度云 谢谢! 小磁针的作用 使实验效果更加明显 小磁针在直导线电流的磁场中,能转动多大的角度? 小孩手脚出汗冰凉怎么回事 小孩出虚汗手脚冰凉怎么回事 商业会展与展台设计入行实战图书信息 西安有哪些味道正宗的肉夹馍? 帮我写一篇办公室文员的实践总结论文,一月6号就要交了 谢谢、、字数要... 泰拉瑞亚显示附近有多少敌人 天气冷或心情不好,吃得太饱会拉肚子,食欲不振是什么病?而且肠胃一直都... 下沙在哪坐566公交车?还有566可以直接到杨家墩下车吗? 566公交站公交站路车公交车可以到销品茂吗? 办房产过户手续有什么流程? 价值评价具有主观性吗