发布订阅作为一种常见的设计模式,在前端模块化领域可以用来解决模块循环依赖问题。
看一个简单的示例
// 消息中间件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.jsmsghub.on('data', function(data) { console.log(data.val + 1); // 3})// b.jsmsghub.on('data', function(data) { console.log(data.val + 2); // 4})// c.jsmsghub.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.jsmsghub.on('data', function(data) { console.log('module a get num:' + data.val); // 3 return { val: ++data.val };})// b.jsmsghub.on('data', function(data) { console.log('module b get num:' + data.val) return { val: data.val + 3 }})// d.jsmsghub.on('data', function(data) { console.log('module d get num:' + data.val);})// e.jsmsghub.on('data', function(data) { console.log('module e get num:' + data.val);})// c.jsmsghub.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:2module b get num:3module d get num:6module e get num:6
订阅函数支持异步
上面的例子中有一个问题就是订阅函数必须是同步代码,如果a.js包含下述异步代码的话就会出问题
// a.jsmsghub.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编译