大家好,我是六六。今天分析关于Vue实例事件相关的方法。与事件相关的实例方法有四个,vm.$on,vm.$emit,vm.$once,vm.$off。这四个方法都挂载到Vue的prototype属性上,接下里我们详细的讲一讲:
vm.$on( event, callback )
监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
示例:
vm.$on('test', function (msg) { console.log(msg) }) vm.$emit('test', 'hi') // => "hi" 复制代码
其实通过用法我们就知道,可以先把事件的回调函数先收集起来,等到事件触发时,我们从收集的回调函数取出来执行就好了。所以,我们就需要一个对象来存储这些回调函数。事实上,在执行new Vue()时,Vue会执行this._init进行一系列初始化工作,其中就会在实例上创建一个_events属性,来存储事件以及相应的回调函数。
vm._events=Object.create(null) 复制代码
知道了基本原理,我们手动实现一下:
Vue.prototype.$on=function(event,fn){ const vm=this if(Array.isArray(fn)){ for(let i=0,j=event.length;i<j;i++){ this.$on(event,fn[i]) } }else{ (vm._events[event]||(vm.events[event]=[])).push(fn) } return vm } 复制代码
解析:首先我们会先判断event参数是否为数组,然后遍历数组,再继续递归调用,之后将event作为属性名传入vm._events并且属性值为一个数组,接着就将回调函数push进去。等待被触发。
vm.$off( [event, callback] )
移除自定义事件监听器。
如果没有提供参数,则移除所有的事件监听器;
如果只提供了事件,则移除该事件所有的监听器;
如果同时提供了事件与回调,则只移除这个回调的监听器。
通过用法我们知道vm.$off是用于移除事件的,并且对于参数的不同有三种情况要分析。
Vue.prototype.$off=function(event,fn){ const vm=this if(arguments.length===0){ vm._events=Object.create(null) return vm } return vm } 复制代码
只要满足了arguments.length===0德情况,vm._events属性就会被重置,等于null.之前绑定的所有回调和事件都清楚了。
Vue.prototype.$off=function(event,fn){ const vm=this // 无参数 if(!arguments){ vm._events=Object.create(null) return vm } // 新增 // 传递事件名和回调函数 else if(fn && typeof event==='string'){ const cbs=vm._events[event] let j=cbs.length for(let i=0;i<j;i++){ // if(fn===cbs[i]||cbs[i].fn===fn){ cbs.splice(i,1) return } } } return vm } 复制代码
首先判断参数evnet为字符串和有回调函数,通过循环该事件的数组,找到该回调函数并使用splice方法移除。
Vue.prototype.$off=function(event,fn){ const vm=this // 无参数 if(!arguments){ vm._events=Object.create(null) return vm } // 新增 // 一个参数 else if(arguments.length===1){ const e=arguments[0] //判断是否为数组 if(Array.isArray(e)){ for(let i=0,j=e.length;i<j;i++){ this.$off(e[i]) } }else{ vm._events[event]=null return vm } } // 传递事件名和回调函数 else if(fn && typeof event==='string'){ const cbs=vm._events[event] let j=cbs.length for(let i=0;i<j;i++){ // if(fn===cbs[i]||cbs[i].fn===fn){ cbs.splice(i,1) return } } } return vm } 复制代码
因为event参数可以为数组或者字符串,所以需要分情况讨论,是数组的话循环加递归就,找到该事件,指向null就移除了。
vm.$emit( eventName, […args] )
触发当前实例上的事件。附加参数都会传给监听器回调。
我们知道所有事件监听的回调函数都存储在vm._events里面,那么我们只需要根据事件名找到他们,依次执行列表中的函调函数即可,并将参数传入回调函数。
Vue.prototype.$emit=function(event){ const vm=this let cbs=vm._events[event] if(cbs){ const args=toArray(arguments,1) for(let i=0,j=cbs.length;i<j;i++){ try{ cbs[i].apply(vm,args) }catch(e){ console.log(e) } } } return vm } 复制代码
其实也是很简单,toArray的作用就是将类似于数组的数据转换成真正的数组,第二个参数作为起始位置。
vm.$once( event, callback )
监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
vm.$once和 vm.$on的区别就是前者只能实现一次,所以我们就可以当自定义触发了执行拦截器,删除这个监听器并执行一次。
Vue.prototype.$once=function(event,fn){ const vm=this function on (){ vm.$off(event,on) fn.apply(vm,arguments) } on.fn=fn vm.$on(event,on) return vm } 复制代码
我们在vm.$once中使用vm.$on来监听事件。首先我们是将on函数注册到事件中的(这点很重要,并不是fn).当自定义事件被触发时,我们会执行on函数,在这个函数里首先将自己移除掉,在执行fn.所以这个自定义事件就只执行一次。
我们仔细看一下代码,我们注册的是on函数,但是用户如果通过vm.$off移除的不是on函数啊,而是fn函数。但是fn函数我们没有注册啊,注册的是on函数(用户不知道on函数的存在),所以此时我们可以给on添加一个属性fn为fn这个函数on.fn=fn
,在进行删除时,我们可以判断次函数的fn属性即可:
fn===cbs[i]||cbs[i].fn===fn 复制代码
on函数属性fn正是用户手写的函数,所以可以完美的解决了函数不相同的问题了。
不想只做api的搬运工,想要更深入原理的去理解。如果本文有什么问题请大家及时提出来哦。