链式Promise
最近项目有个需求,处理过程中需要执行完一个异步任务之后执行另外一个异步任务,并且任务数量也是不确定的,所以就想到用链式Promise
来处理. 使用过程中还是遇到一些问题的,这里记录分享一下.
概念
Promise
的链式调用主要依赖于Promise.prototype.then(onFullfilled, onRejected)
, 在onFullfilled
中我们可以继续返回Promise
对象来实现链式调用, 链式调用过程中某个异步任务出现reject
的情况,我们可以在最后的catch
中处理.
一个Promise
的链式调用大概是下面这个样子
promise1.then((res) => {
...
return promise2;
}).then((res) => {
...
return promise3;
}).catch(err) {
...
}
问题描述
实际项目中遇到的问题一般会复杂一些,在这里我们简化一下.
问题: 已知一个指定数组list
,要求按顺序每隔1s
逐个打印每一项.
对于这个问题除了使用链式Promise
之外还有其他处理方式,比如下面的代码
function logInOrder(list) {
let i = 0
const interval = setInterval(() => {
if (i >= list.length) {
console.log('log over.')
clearInterval(interval)
return
}
console.log(list[i])
i++
}, 1000 * 1)
}
如果我不知道Promise
, 上面的代码是我能想到最直接的办法, 但是它很局限,拓展性会比较差.
我们稍微把问题复杂一点,现在我们按顺序每隔1s
逐个打印每一项,变更一下需求:
1. list
中每一项作为请求参数按顺序依次调用接口
2. 必须在上一个请求成功响应之后才能进行下一个请求
3. 如果某一次请求时出错了,不再执行后续的请求,并对用户进行提示
使用定时器的方案已经很难处理了, 对于list
中的每一项要做的任务就不是简单的打印了,而是一个异步请求任务. 甚至我们再叠加需求, *后面请求的请求参数都依赖于上一个请求中的响应结果. *
这篇文章我不会去解决这个连续进行接口请求的问题,我们还是先使用Promise
链式调用来解决按顺序依次延迟打印数组每一项的问题, 因为不管是再复杂的异步任务抽象完了都相当于一个延迟的打印.
实现
先看下面的实现
function generateLogTask(n) {
return new Promise(resolve => {
setTimeout(() => {
console.log(n)
resolve(n)
}, 1000 * 1)
})
}
function logInOrder(list) {
const tasks = list.map(item => generateLogTask(item))
let promise = Promise.resolve()
while (tasks.length) {
const task = tasks.shift()
promise = promise.then(() => task)
}
promise.then(() => console.log('log over'))
}
logInOrder([1,2,3,4,5,6])
上面的代码中我们使用generateLogTask
来针对list
中的每一个item
生成一个打印任务,随后通过循环按顺序添加到Promise
链中. 看起来都挺美好的, 但是执行这段代码时候出问题了, 在等待了一秒之后数字不是每隔一秒打印一个而是一下子全部打印了出来.
这是因为Promise
实例在新建之后就会立即执行, 所以在我们通过list.map
生成打印任务的时候每个异步都会马上开始执行,也就导致了我们会直接看到1,2,3,4,5,6
一下子打印了出来,你可以尝试下面的例子来验证这个问题
function foo() {
console.log("foo start")
const promise = new Promise(resolve => {
console.log("task start")
resolve()
})
promise.then(() => console.log("task fullfilled"))
console.log("foo end")
}
foo()
执行完之后控制台会按下面的顺序依次打印
1. foo start
2. task start
3. foo end
4. task fullfilled
解决一次性打印的bug
方法一
上面我们已经知道了Promise
实例在创建的时候是立即执行的, 那么我们只需在需要执行的时候再创建就行了,我们对logInOrder
的中的代码进行修改
function logInOrder(list) {
let promise = Promise.resolve()
while (list.length) {
const item = list.shift()
promise = promise.then(() => generateTask(item))
}
promise.then(() => console.log("log over"))
}
再次执行就能得到我们想要的结果了.
方法二
函数柯里化的一个应用场景就是延迟计算, 那么我们可以在创建打印任务的时候不立即创建promise实例
,我们来修改generateLogTask
,让它返回一个创建Promise
实例的函数,而不是直接返回Promise
实例,接着我们还需要修改一下logInOrder
,因为每个tasks
中的每一项不再是Promise
实例,而是创建Promise的函数.
function generateLogTask(n) {
return function() {
return new Promise(resolve => {
setTimeout(() => {
console.log(n)
resolve(n)
}, 1000 * 1)
});
}
}
function logInOrder(list) {
const tasks = list.map(item => generateLogTask(item))
let promise = Promise.resolve()
while (tasks.length) {
const task = tasks.shift()
promise = promise.then(() => task())
}
promise.then(() => console.log('log over'))
}
总结
其实两种方法1和方法2对最初代码的修复没什么本质区别,都是为了在合适的时机再创建Promise
实力,因为一旦创建它就会立即执行.
现在更复杂的场景也可以做了,比如前面提到的依次发送请求, 就不在这里详细写了.