发布订阅作为一种常见的设计模式, 在前端模块化领域可以用来解决模块循环依赖问题.
看一个简单的示例
- // 消息中间件 v1
- var msghub = (function() {
- var listener = [];
- return {
- on: function(type, cb, option) {
- listener[type] = listener[type] || [];
- option = option || {};
- listener[type].push({
- cb: cb,
- priority: option.priority || 0
- });
- },
- fire: function(type, dataObj) {
- if (listener[type]) {
- listener[type].sort((a, b) => a.priority - b.priority).forEach((item) => {
- item.cb.call(null, dataObj);
- });
- }
- }
- }
- })();
以及消息中间件的使用模块
- // a.js
- msghub.on('data', function(data) {
- console.log(data.val + 1); // 3
- })
- // b.js
- msghub.on('data', function(data) {
- console.log(data.val + 2); // 4
- })
- // c.js
- msghub.fire('data', {
- val: 2
- });
当 c 模块触发 data 事件的时候, a 和 b 模块的监听函数都会被执行并输出相应的结果.
订阅函数管道化
上面的例子基本可以满足需求了, 但是有时候希望多个订阅函数之间可以传递执行结果, 类似 linux 管道 a.pipe(b).pipe(c)... 这种, 上一个函数的输出是下一个函数的输入. 针对管道化需求对 msghub 的回调遍历从 forEach 改为 reduce 方式, 如下代码所示
- // 消息中间件 v2 支持执行结果传递
- var msghub = (function() {
- var listener = [];
- option = option || {};
- return {
- on: function(type, cb, option) {
- listener[type] = listener[type] || [];
- listener[type].push({
- cb: cb,
- priority: option.priority || 0
- });
- },
- fire: function(type, dataObj) {
- if (listener[type]) {
- listener[type].sort((a, b) => b.priority - a.priority).reduce((pre, cur) => {
- let result = cur.cb.call(null, pre) || pre; // 如果一个订阅函数没有返回值则传递上上个订阅函数的执行结果, 如果需要完全的管道化的话就把 || pre 去掉即可
- return result;
- }, dataObj);
- }
- }
- }
- })();
测试一下上面的 msghub
- // a.js
- msghub.on('data', function(data) {
- console.log('module a get num:' + data.val); // 3
- return {
- val: ++data.val
- };
- })
- // b.js
- msghub.on('data', function(data) {
- console.log('module b get num:' + data.val)
- return {
- val: data.val + 3
- }
- })
- // d.js
- msghub.on('data', function(data) {
- console.log('module d get num:' + data.val);
- })
- // e.js
- msghub.on('data', function(data) {
- console.log('module e get num:' + data.val);
- })
- // c.js
- msghub.fire('data', {
- val: 2
- });
使用改良后的 msghub 的话
- // a.js
- msghub.on('data', function(data) {
- console.log('module a get num:' + data.val); // 3
- return {
- val: ++data.val
- };
- })
- // b.js
- msghub.on('data', function(data) {
- console.log('module b get num:' + data.val)
- return {
- val: data.val + 3
- }
- })
- // d.js
- msghub.on('data', function(data) {
- console.log('module d get num:' + data.val);
- })
- // e.js
- msghub.on('data', function(data) {
- console.log('module e get num:' + data.val);
- })
- // c.js
- msghub.fire('data', {
- val: 2
- });
最终打印输出如下信息:
module a get num:2
module b get num:3
module d get num:6
module e get num:6
订阅函数支持异步
上面的例子中有一个问题就是订阅函数必须是同步代码, 如果 a.js 包含下述异步代码的话就会出问题
- // a.js
- msghub.on('data', function(data) {
- console.log('module a get num:' + data.val); // 3
- return new Promise(function(resolve, reject) {
- setTimeout(() => {
- resolve({
- val: ++data.val
- })
- }, 1000);
- });
- })
针对可能异步的情况我们需要进一步改良 msghub 来支持, 该请 asyn 和 await 出场了
- // 消息中间件 v3 完美支持同步, 异步管道化
- var msghub = (function() {
- var listener = [];
- return {
- on: function(type, cb, option) {
- listener[type] = listener[type] || [];
- option = option || {};
- listener[type].push({
- cb: cb,
- priority: option.priority || 0
- });
- },
- fire: function(type, dataObj) {
- if (listener[type]) {
- let listenerArr = listener[type].sort((a, b) => b.priority - a.priority);
- (async function iter() {
- let val = dataObj;
- for (const item of listenerArr) {
- val = await item.cb.call(null, val);
- }
- })();
- }
- }
- }
- })();
注意: 上述代码可以在 node 环境做测试, 如果需要在浏览器中运行的话, 需要对 for of 和 async await 进行 babel 编译
来源: https://juejin.im/post/5af97a5d6fb9a07ab37991ac