目录
前言
前端异步编程通常,是快相当大的内容,异步编程通常能获得更好的性能和更大的灵活性。异步的最大特点是无需一步一步等待,可以多步同时进行。其中 “Promises” 渐渐成为 JavaScript 里很重要的一部分内容,我们目前大部分的功能都是基于 Promise 来实现的,它大大提高了开发效率和交互体验感,是个很好轮子。
本人也就是经常使用 Promise 来实现一些功能,也是在开发一些项目的时候针对存在的问题充分分析后发现了 Promise 设计上的一些缺陷,为了解决缺陷也深入研究了一番!下面将用实际开发遇到的问题进行分析,项目中的服务请求流程如下图所示。客户端与服务端采用 Webrtc 连接,客户端的每次请求可能得到一个回应也可能得不到响应或者有多个回应,客户端请求中断了,服务端还会继续执行,也就是下次连接上了会收到多个重复的回应。
项目中是使用事件总线(EventBus)来实现事件调度的。这个也是发生后面问题的主要原因。
由于存在这个问题发现项目中存在一个 Promise 在长时间的执行中,由于用户操作重新启动了一个一样的 Promise,然后导致等前面的 Promise 在后面收到结果消息的时候,后续步骤就执行了两遍,或者导致执行顺序乱了。
具体现象如图 Promise1 执行过程中接口没有给个结果,或者用户再这个步骤中做了其他操作导致这个步骤没有结果了
为了解决这个问题,那么就产生了三个问题:
1、如何从外部把一个正在等待的 Promise 终止掉,让其结束执行。
2、 如何把一个 Promise 终止掉后,让其后面的其他 Promise 也抛弃掉,后续步骤不执行。
3、重复收到服务端的消息怎么避免多次执行。
一、如何中断和结束 Promise 调用链
我们知道官方并没有提供 promise.abort() 的方法,这个也是 promise 的缺点,一旦建立了就无法取消了,于是我们就有了中断和结束调用链的需求,下面是本人收集的解决方法:
1.中断调用链
想要中断调用链很简单,就是在 then/catch 的最后一行返回一个永远 pending 的 promise 就可以了,这样后续 promise 就一直等待。如图中间 promise 不返回结果就会一直卡在这里。
somePromise
.then(() => {})
.then(() => {
// 终止 Promise 链,让下面的 then、catch 和 finally 都不执行
})
.then(() => console.log('then'))
.catch(() => console.log('catch'))
.finally(() => console.log('finally'))
// 返回一个永远 pending 的 promise
return new Promise((resolve, reject) => {})
2. 结束调用链
结束调用链实现的大致思想就是我们不用等结果,直接 resolve/reject 出来,这样这个 promise 就结束了。例如我们请求接口超时的实现:
function timeoutWrapper(p, timeout = 2000) {
const wait = new Promise((resolve, reject) => {
setTimeout(() => {
reject('请求超时')
}, timeout)
})
return Promise.race([p, wait])
}
我们会发现这个方法不够灵活,由于终止 promise 的缘由可能有不少,例如当用户点击某个按钮或者出现其余事件时手动终止。因此应该写一个包装函数,提供 abort 方法,让使用者本身决定什么时候终止。
function abortWrapper(p1) {
let abort
let p2 = new Promise((resolve, reject) => (abort = reject))
let p = Promise.race([p1, p2])
p.abort = abort
return p
}
二、EventBus 如何避免重复执行
1.EventBus的简单介绍
事件总线,通常作为多个模块间的通信机制,相当于一个事件管理中心,一个模块发送消息,其它模块接受消息,就达到了通信的作用,下面就是一个简单的实现:
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
}
// 发布事件
publish(eventName) {
// 取出当前事件所有的回调函数
const callbackList = this.eventObject[eventName];
if (!callbackList) return console.warn(eventName + " not found!");
// 执行每一个回调函数
for (let callback of callbackList) {
callback();
}
}
// 订阅事件
subscribe(eventName, callback) {
// 初始化这个事件
if (!this.eventObject[eventName]) {
this.eventObject[eventName] = [];
}
// 存储订阅者的回调函数
this.eventObject[eventName].push(callback);
}
}
// 测试
const eventBus = new EventBus();
// 订阅事件eventX
eventBus.subscribe("eventX", () => {
console.log("模块A");
});
eventBus.subscribe("eventX", () => {
console.log("模块B");
});
eventBus.subscribe("eventX", () => {
console.log("模块C");
});
// 发布事件eventX
eventBus.publish("eventX");
// 输出
> 模块A
> 模块B
> 模块C
2.订阅的事件怎么只执行一次
如果一个事件只发生一次,通常也只需要订阅一次,收到消息后就不用再接受消息。还有就是要有取消订阅的逻辑。下面是一个比较完整的事件总线:
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
// 回调函数列表的id
this.callbackId = 0;
}
// 发布事件
publish(eventName, ...args) {
// 取出当前事件所有的回调函数
const callbackObject = this.eventObject[eventName];
if (!callbackObject) return console.warn(eventName + " not found!");
// 执行每一个回调函数
for (let id in callbackObject) {
// 执行时传入参数
callbackObject[id](...args);
// 只订阅一次的回调函数需要删除
if (id[0] === "d") {
delete callbackObject[id];
}
}
}
// 订阅事件
subscribe(eventName, callback) {
// 初始化这个事件
if (!this.eventObject[eventName]) {
// 使用对象存储,注销回调函数的时候提高删除的效率
this.eventObject[eventName] = {};
}
const id = this.callbackId++;
// 存储订阅者的回调函数
// callbackId使用后需要自增,供下一个回调函数使用
this.eventObject[eventName][id] = callback;
// 每一次订阅事件,都生成唯一一个取消订阅的函数
const unSubscribe = () => {
// 清除这个订阅者的回调函数
delete this.eventObject[eventName][id];
// 如果这个事件没有订阅者了,也把整个事件对象清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
// 只订阅一次
subscribeOnce(eventName, callback) {
// 初始化这个事件
if (!this.eventObject[eventName]) {
// 使用对象存储,注销回调函数的时候提高删除的效率
this.eventObject[eventName] = {};
}
// 标示为只订阅一次的回调函数
const id = "d" + this.callbackId++;
// 存储订阅者的回调函数
// callbackId使用后需要自增,供下一个回调函数使用
this.eventObject[eventName][id] = callback;
// 每一次订阅事件,都生成唯一一个取消订阅的函数
const unSubscribe = () => {
// 清除这个订阅者的回调函数
delete this.eventObject[eventName][id];
// 如果这个事件没有订阅者了,也把整个事件对象清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
// 清除事件
clear(eventName) {
// 未提供事件名称,默认清除所有事件
if (!eventName) {
this.eventObject = {};
return;
}
// 清除指定事件
delete this.eventObject[eventName];
}
}
// 测试
const eventBus = new EventBus();
// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模块C", obj, num);
});
// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);
// 清除
eventBus.clear("eventX");
// 再次发布事件eventX,由于已经清除,所有模块都不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);
// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> eventX not found!