前端-Promise详解与实例
Promise.all和Promise.race传入空数组
关于将 Promise.all和 Promise.race传入空数组的两段代码的输出结果说法正确的是:
Promise.all([]).then(res => {
console.log('all', res); // 输出all []
})
Promise.race([]).then(res => {
console.log('race', res); // 没有任何输出
})
Promise.all([])会返回一个成功状态的promise
Promise.race([])会返回一个pending状态的promise
promise的超时控制
Promise原生是不支持设置超时时间的,也不支持中断请求。如果我们想要实现超时控制或者取消重复请求这样的需求,只能寻求其它思路,另外这两个需求本质上都是中断请求,并且我们可以随
控制什么时候中断。
总体思路是:创建一个新的newPromise,和原来用于发送请求的originPromise作为参数传入Promise.race方法中,接下来使用这个race方法返回的promise,上边添加then和catch方法。如果
要中断原来的promise,只需要将newPromise变成失败状态即可。
需求1:自定义实现超时控制
请求是无法设置超时时间的,因此我们需要自己去模拟一个超时控制。
方法一:使用promise.race
// 封装一个延时失败的promise
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('timeout')
}, time);
})
}
// 第一个参数是正常的promise,第二个参数设置超时时间
function timeoutPromise(promise, time) {
return Promise.race([promise, sleep(time)]);
}
// 测试
function createPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done');
}, time);
})
}
// 超时。设置正常的promise在2s后成功,超时时间是1s
timeoutPromise(createPromise(2000), 1000).then(res => {
console.log(res);
}).catch(err => {
console.log(err); // timeout
});
// 不超时
timeoutPromise(createPromise(2000), 3000).then(res => {
console.log(res); // done
}).catch(err => {
console.log(err);
});
方法二:不使用promise.race。但是本质上相当于自定义实现了promise.race,手动创建一个promise的数组,让每一个promise都开始执行,谁的状态先改变就以谁的状态为准
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('timeout')
}, time);
});
}
function timeoutPromise(promise, time) {
const promiseArr = [promise, sleep(time)];
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
p.then(res => {
resolve(res);
}).catch(err => {
reject(err);
})
})
})
}
// 测试
function createPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done');
}, time);
})
}
// 超时
timeoutPromise(createPromise(2000), 1000).then(res => {
console.log(res);
}).catch(err => {
console.log(err); // timeout
});
// 不超时
timeoutPromise(createPromise(2000), 3000).then(res => {
console.log(res); // done
}).catch(err => {
console.log(err);
});
需求2:转盘问题
转盘问题,一个抽奖转盘动画效果有5秒,但是一般来说向后端请求转盘结果只需要不到一秒,因此请求结果至少得等5秒才能展现给用户。
需要考虑两种情况。
- 转盘动画还未完成,请求结果已经拿到了,此时要等到动画完成再展示结果给用户。
- 转盘动画完成了,请求结果还未拿到,此时需要等待结果返回(可以设置请求超时时间)。
所以,转盘问题更适合用Promise.all()来解决。
// 转盘动画在time秒后完成
function plate(time) {
return new Promise(resolve => {
setTimeout(() => {
resolve('我转完了~');
}, time);
})
}
// 第一个参数是包裹着请求的promise,第二个参数是转盘动画时间
function wrapPromise(promise, time) {
return Promise.all([promise, plate(time)]);
}
// 测试
function createPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done');
}, time);
})
}
wrapPromise(createPromise(2000), 3000).then(res => {
console.log(res); // ["done", "我转完了~"]
});
wrapPromise(createPromise(2000), 1000).then(res => {
console.log(res); // ["done", "我转完了~"]
});
取消重复请求
同一类请求是有序发出的(根据按钮点击的次序),但是响应顺序却是无法预测的,我们通常只希望渲染最后一次发出请求响应的数据,而其他数据则丢弃。因此,我们需要丢弃(或不处理)除最
一次请求外的其他请求的响应数据。
function CancelablePromise() {
this.pendingPromise = null;
}
// 参数是一个正常的包裹着请求的promise
CancelablePromise.prototype.request = function (promise) {
if (this.pendingPromise) {
this.cancel('取消重复请求');
}
const _promise = new Promise((resolve, reject) => {
this.reject = reject;
});
const newPromise = Promise.race([promise, _promise]);
this.pendingPromise = newPromise;
return newPromise;
}
CancelablePromise.prototype.cancel = function (reason) {
this.reject(new Error(reason));
this.pendingPromise = null;
}
// 测试
function createRequest(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('请求完成')
}, delay);
})
}
const cancelablePromise = new CancelablePromise();
for (let i = 0; i < 3; i++) {
cancelablePromise.request(createRequest(1000)).then(res => {
console.log(res);
}).catch(err => {
console.error(err);
})
}
限制并发请求数
Promise.allSettled不适合应对这样的场景,它能控制的粒度还是太粗了。它必须等待所有Promise都resolve或reject后才能继续。比如现在总共有10个请求,并发请求数限制为4个,当其中
个请求完成时,应该就可以进行下一个请求了,为了最高效率,要始终保证并发请求数量被最大程度使用。但是如果使用Promise.allSettled,它必须等待4个请求都完成了才能进行接下来的4个
求,这显然是不合理的。
function limitRequest(requestFnArr, limit) {
function request(requestFn) {
requestFn().finally(() => {
// 如果前边的请求进行完成了,就可以处理新的请求
if (_requestFnArr.length > 0) {
request(_requestFnArr.shift());
}
})
}
// 限制并发量
const _requestFnArr = [...requestFnArr];
for (let i = 0; i < limit && _requestFnArr.length > 0; i++) {
request(_requestFnArr.shift());
}
}
// 测试
function createRequest(delay) {
return function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done')
}, delay);
}).then(res =>
console.log(res);
})
}
}
const requestFns = [];
for (let i = 0; i < 10; i++) {
requestFns.push(createRequest(1000));
}
limitRequest(requestFns, 4);
实现有并行限制的Promise调度器
class Scheduler {
constructor(limit) {
this.limit = limit;
this.count = 0;
this.taskList = [];
}
run(promiseCreator) {
promiseCreator().finally(() => {
this.count--;
if (this.taskList.length > 0) {
this.run(this.taskList.shift());
}
})
}
add(promiseCreator) {
this.taskList.push(promiseCreator);
if (this.count < this.limit) {
this.run(this.taskList.shift());
this.count++;
}
}
}
// 测试
const scheduler = new Scheduler(3);
const timeout = time => new Promise(resolve => {
setTimeout(resolve, time);
});
const addTask = (time, value) => {
scheduler.add(() => {
return timeout(time).then(() => {
console.log(value);
})
})
};
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
自定义实现allSettled
function allSettled(promises) {
let result = [];
let count = 0;
return new Promise(resolve => {
for (let i = 0; i < promises.length; i++) {
let promise = Promise.resolve(promises[i]);
let obj = {};
promise.then(res => {
obj = {
status: 'fulfilled',
value: res
}
}).catch(err => {
obj = {
status: 'rejected',
reason: err
}
}).finally(() => {
// 注意这里需要根据下标来存值,否则无法保证返回值数组与参数顺序一致
result[i] = obj;
if (++count === promises.length) {
resolve(result);
}
})
}
})
}
自定义实现Promise.all和Promise.race
Promise.all:
function promiseAll(promises) {
// 传入的参数不一定是数组对象,可以是"遍历器"
// 如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。
promises = Array.from(promises);
let count = 0;
let newValues = [];
return new Promise(((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i])
.then(res => {
// 保存这个promise实例的value
// 注意这里需要根据下标来存值,否则无法保证返回值数组与参数顺序一致
result[i] = res;
// 通过计数器,标记是否所有实例均 fulfilled
if (++count === promises.length) {
resolve(newValues);
}
})
.catch(err => {
reject(err);
})
}
}))
}function promiseAll(promises) {
// 传入的参数不一定是数组对象,可以是"遍历器"
// 如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。
promises = Array.from(promises);
let count = 0;
let newValues = [];
return new Promise(((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i])
.then(res => {
// 保存这个promise实例的value
// 注意这里需要根据下标来存值,否则无法保证返回值数组与参数顺序一致
result[i] = res;
// 通过计数器,标记是否所有实例均 fulfilled
if (++count === promises.length) {
resolve(newValues);
}
})
.catch(err => {
reject(err);
})
}
}))
}
Promise.race:
function race(promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises[i]; i++) {
Promise.resolve(promises[i]).then(res => {
resolve(res);
}).catch(err => {
reject(err);
})
}
})
}
问:上边代码中能否直接将resolve传递给then方法,或者能否直接将reject传递给catch方法?
是可以的。当原来的promise变为成功状态时,会去调用then的函数参数,并将原来调用resolve时传递的参数作为then的回调函数的参数。所以可以写的更简洁:
function race(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p)
.then(resolve)
.catch(reject);
});
});
}
全局捕获promise异常
业务场景:团队开发中可能有同学忘记处理promise的异常,此时全局中应该对此异常进行捕获,做一个兜底。
new Promise((resolve, reject) => {
throw new Error("出错了");
}).then((res) => {
console.log(res);
});
window.addEventListener('unhandledrejection', event => {
const {
error, // 错误对象
promise, // 出现异常的promise对象
} = event
console.log(error, promise)
})
对比Promise.race方法,实现一个last方法
描述:race方式是接收一个数组,只要其中有一个promise实例的状态率先改变,最终promise的实例就会改变。现在要对比着自定义实现一个last方法,不管前边的promise状态怎么变,我们只
据最后一个发生状态改变的promise来决定最终promise实例的状态。
new Promise((resolve, reject) => {
throw new Error("出错了");
}).then((res) => {
console.log(res);
});
window.addEventListener('unhandledrejection', event => {
const {
error, // 错误对象
promise, // 出现异常的promise对象
} = event
console.log(error, promise)
})
实现并行发送请求的函数
要求实现一个send(list, n, callback)函数,该函数接收三个参数,第一个参数是存储url的数组,第一个参数是限制的并发请求数,第三个参数是回调函数。要求在发送请求的过程中收集数据
存入数组中,数据和请求发送的顺序要一一对应。最后所有请求完成时,调用callback([data1, data2, data3, ...])。本题假设所有请求都会被正常处理,有返回结果。
// 请求函数
function myFetch(url) {
return new Promise(resolve => {
const timeout = parseInt(Math.random() * 3 + 1);
setTimeout(() => {
resolve(url);
}, timeout * 1000);
});
}
// 实现的send函数
function send(list, n, callback) {
let num = 0;
let count = 0;
const data = [];
const fn = (url, index) => {
num++;
myFetch(url).then(res => {
data[index] = res;
if (num < list.length) {
fn(list[num], num);
}
if (++count === list.length) {
callback(data);
}
});
};
for (let i = 0; i < n && i < list.length; i++) {
fn(list[i], i);
}
}
// 测试
const list = [1, 2, 3, 4, 5, 6, 7, 8, 9];
function callback(data) {
console.log(data);
}
send(list, 5, callback);
send(list, 15, callback);