如果你学过vue,我想你一定对vue的父子组件通信产生过好奇,为什么子组件中$emit函数中 填写event:事件名,args:传递的参数,在父组件中写上对应的event事件名,和回调函数,就可以在回调函数中接收到子组件传递的args参数呢?我也很好奇,所以我就去百度同时看了vue2的源码,发现里面使用了一种设计模式,叫发布-订阅模式。于是,我就详详细细的去了解了发布-订阅模式是怎么回事。下面我也用我的方式来描述一下,我对发布订阅模式来的理解,以及我遇到的一些问题和如何解决这些问题。
发布-订阅模式,顾名思义,就是发布者发布消息,订阅者就可以用接收到发布者发布的信息,很好理解。转化成代码,要怎么写呢?看下面示例:
我自己打出这简单的几行代码以后,震惊到了,就这么几行代码,就很巧妙的实现了功能,对于数组中存储方法,在特定的时间再去执行该方法有了更加深刻的理解。下面在试试不同的事件,多个相同的事件,不同的处理,会不会收到数据
let eventsMixin = { /* 收集订阅者 */ events: {}, /* 参数:订阅的主题。回调函数, 收集依赖 */ on(event, fn) { (this.events[event] || (this.events[event] = [])).push(fn); }, /* 发布数据 */ emit(event, content) { if (!this.events[event] || this.events[event].length === 0) { console.log('没有人订阅信息--->', event, content); return; } let tempList = this.events[event]; tempList.forEach(fn => { fn(content) }) } } eventsMixin.on('handleTitle', handleTitle); eventsMixin.emit('handleTitle', '标题'); /* 处理事件 */ function handleTitle(title) { console.log('收到消息了--->', title); }
首先定义了一个eventsMixin对象,其中有一个events空对象,两个函数,on函数接收一个事件名,一个回调函数。emit函数接收一个事件名,一个需要发送的消息参数。on函数中把传递进来的fn函数,push到events对象的event属性中,看第六行代码,没有该变量先创建否则会报错(可想而知)。emit函数中去遍历events对象的event属性中的数据,并执行数据中的方法,因为在on函数中的event属性存的是个数组函数,此时emit函数中正好传递进来的数据,那么这个参数也正好可以被此前的fn函数中的参数接收,这样就完成了,订阅者订阅到了发布者发布的消息了。看结果:
![44d4a25fc9cce03aff7c67d7b7741c19.png](https://i-blog.csdnimg.cn/blog_migrate/a8cb97f4ea0174c0597dc8680122e366.png)
eventsMixin.on('handleTitle', handleTitle);eventsMixin.on('content', handleContent);eventsMixin.on('content', handleContent1);eventsMixin.emit('handleTitle', '标题');eventsMixin.emit('content', '内容'); function handleTitle(title) { console.log('收到消息了--->', title);}function handleContent(content) { console.log('收到消息了--->', content);}function handleContent1(content) { console.log('收到消息了1--->', content);}
// 取消订阅off 函数在eventsMixin对象中,简写了off(event, fn) { /* 需要校验参数 */ let _this = this; // let fns = _this.events[event]; /* 没有参数,移除所有的事件监听器 */ if (!arguments.length) { _this.events = {}; return; } /* 只有事件名称,移除该事件所有的监听器 */ if (arguments.length === 1) { delete _this.events[event]; return; } /* 如果同时提供了事件与回调,则只移除这个回调的监听器 */ if (arguments.length === 2) { let fnList = _this.events[event]; if (!fnList) return; // 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可 for (let i = 0; i < fnList.length; i++) { if (fnList[i] === fn) { fnList.splice(i, 1); break } } }}eventsMixin.on('handleTitle', handleTitle);eventsMixin.on('content', handleContent);eventsMixin.on('content', handleContent1);eventsMixin.emit('handleTitle', '标题');eventsMixin.emit('content', '内容');eventsMixin.off('content', handleContent);eventsMixin.emit('content', '新的内容');
看结果:
/* 监听一次 函数在eventsMixin对象中,简写了*/once(event, fn) { let _this = this; // 先绑定,调用后删除 // console.log(fn); function on() { _this.off(event, on); fn.apply(_this, arguments); } on.fn = fn; _this.on(event, on);},
eventsMixin.once('content', handleContent);eventsMixin.emit('content', '内容');eventsMixin.emit('content', '新的内容');
once函数里面还定义了一个on函数(姑且叫内部on函数),内部on函数中执行off方法,和fn函数(姑且叫内部fn函数)。内部fn函数的绑定了once方法中的形参fn。最后执行on函数(外部的on函数)。这里超级绕,有一个办法可以理清楚,那就是打断点调试,就可以看到函数的执行过程了。
最终结果也符合我们的预期。
难道到这里就结束了吗?并没有,下面我想分享的才是我遇到的坑和网上分享不同的地方。拿起小本本仔细听。
当我们再次订阅信息的时候
eventsMixin.once('content', handleContent);eventsMixin.once('content', handleContent1);eventsMixin.emit('content', '内容');
发现只打印出来一条信息了,按道理应该打印出来两条信息才对,因为我分别订阅了两次内容。
我看了很多遍代码后,也没有看出什么毛病,所以我就试着打断点去调试,发现当我们去做删除的时候eventList数组发生了变化,我们再在emit函数中进行forEach循环,那么就得不到数组中第二个函数了,它现在变成第一个了。那要怎么解决,好办,反向遍历即可。
let i = eventList.length;while (i--) { eventList[i](content);}
得到数据了。所以遍历的时候去删除数组中的数据就要注意了,这一点,我在java遍历数组并删除数据遇到的问题也做过相应的笔记。当时使用的是迭代器。es6中也引入了迭代器的概念,那么js的迭代器也可以实现(不过我还没试,你可以试试)。
集合与泛型第三篇
在vue2源码中也能看到这样的处理(我当然是抄袭的了)
Vue.prototype.$off = function (event, fn) { var vm = this; // all if (!arguments.length) { vm._events = Object.create(null); return vm } // array of events if (Array.isArray(event)) { for (var i$1 = 0, l = event.length; i$1 < l; i$1++) { vm.$off(event[i$1], fn); } return vm } // specific event var cbs = vm._events[event]; if (!cbs) { return vm } if (!fn) { vm._events[event] = null; return vm } // specific handler var cb; var i = cbs.length; while (i--) { cb = cbs[i]; if (cb === fn || cb.fn === fn) { cbs.splice(i, 1); break } } return vm };
只不过源码里面是写在off函数里的。原理还是一样倒过来遍历数组
下面是完整的代码
<html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Documenttitle>head><body> <script> /* 定义调度中心 */ let eventsMixin = { /* 收集订阅的主题 */ events: {}, /* 参数:订阅的主题。回调函数, 收集依赖 */ on(event, fn) { (this.events[event] || (this.events[event] = [])).push(fn); }, /* 发布数据 */ emit(event, content) { if (!this.events[event] || this.events[event].length === 0) { console.log('没有人订阅信息--->', event, content); return; } let eventList = this.events[event]; // eventList.forEach(fn => { // fn(content) // }) // for (let i in eventList) { // eventList[i](content); // } // for (let i = 0; i < eventList.length; i++) { // eventList[i](content); // } let i = eventList.length; while (i--) { eventList[i](content); } // for (let item of eventList) { // item(content); // } }, /* 监听一次 */ once(event, fn) { let _this = this; // 先绑定,调用后删除 // console.log(fn); function on() { // console.log(fn); _this.off(event, on); fn.apply(_this, arguments); } on.fn = fn; _this.on(event, on); }, // 取消订阅 off(event, fn) { /* 需要校验参数 */ let _this = this; // let fns = _this.events[event]; /* 没有参数,移除所有的事件监听器 */ if (!arguments.length) { _this.events = {}; return; } /* 只有事件名称,移除该事件所有的监听器 */ if (arguments.length === 1) { delete _this.events[event]; return; } /* 如果同时提供了事件与回调,则只移除这个回调的监听器 */ if (arguments.length === 2) { let fnList = _this.events[event]; if (!fnList) return; // 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可 for (let i = 0; i < fnList.length; i++) { if (fnList[i] === fn || fnList[i].fn === fn) { fnList.splice(i, 1); // console.log(fnList); break } } // let i = fnList.length // while (i--) { // cb = fnList[i]; // if (cb === fn || cb.fn === fn) { // fnList.splice(i, 1); // break // } // } } } } /* 订阅者 到订阅调度中心,调度想要的内容(即主题) */ // eventsMixin.once('handleTitle', handleTitle); eventsMixin.once('content', handleContent); eventsMixin.once('content', handleContent1); // eventsMixin.once('content', handleContent2); // eventsMixin.once('content', handleTitle5); /* 发布者 向调度中心发布内容,(要设置主题,目的,只有订阅了该主题的用户才可以获取到相应的数据) */ // eventsMixin.emit('handleTitle', '标题'); // eventsMixin.emit('content', '内容'); eventsMixin.emit('content', '内容'); // eventsMixin.off('content', handleContent); // eventsMixin.off(); // eventsMixin.emit('content', '新的内容'); // eventsMixin.on('content', handleTitle); // eventsMixin.emit('content', '内容'); // eventsMixin.off(); /* 处理事件 */ function handleTitle(title) { console.log('收到消息了--->', title); } function handleContent(content) { console.log('收到消息了--->', content); } function handleContent1(content) { console.log('收到消息了1--->', content); } function handleContent2(content) { console.log('收到消息了2--->', content); } function handleTitle5(title) { console.log('收到消息了55--->', title); }script>body>html>
总结:发布者与订阅者 通过event事件名来建立关系,订阅者通过回调函数接收发布者发布的消息
长按关注,听wzg扯淡