聊聊发布-订阅设计模式

发布于 2021-01-15 23:20

如果你学过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) {          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函数中的参数接收,这样就完成了,者到了发布者发布的消息了。
看结果:

我自己打出这简单的几行代码以后,震惊到了,就这么几行代码,就很巧妙的实现了功能,对于数组中存储方法,在特定的时间再去执行该方法有了更加深刻的理解。
下面在试试不同的事件,多个相同的事件,不同的处理,会不会收到数据
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);}

答案是肯定的。

至此我们对vue的子向传值的原理,和发布-模式应该有了了解了。那么对定的off ,once 方法我们是不是也顺便了解了解呢?
off的话就是,删除所有者,也可以删除对应发布者的所有者,还可以是对应发布者中某个者,对应代码就是删除events对象中特定的数据。
once的话和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.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', '新的内容');
看结果:

'content', handleContent 对应者,被删除了。所以发布者再次发布信息的时候,只有'content', handleContent1对应的者才能收到数据。
看off函数,参数:一个事件名,一个函数。首先,如果没有参数,就删除所有的者,如果只有一个参数,就删除对应的事件的所有者,如果两个参数,就删除对应事件的对应者。

我想上面的代码应该很好理解的。
下面再来介绍once方法,只触发一次。
/* 监听一次  函数在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函数里的。原理还是一样倒过来遍历数组

下面是完整的代码

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></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>



本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。

相关素材