跳到主要内容

4 篇博文 含有标签「promise」

查看所有标签

· 阅读需 4 分钟

Promise.all

  1. 不是原型方法,所以写在 Promise 构造函数中。返回值是一个 promise
Promise.myAll = function (proms) {
return new Promise((resolve, reject) => {
})
}
  1. 处理边界情况。Promise.all 接收的是任意的可迭代对象,不能单纯使用 length 或 size 来判断参数长度。这里利用 for of 来遍历并计数
Promise.myAll = function (proms) {
return new Promise((resolve, reject) => {
+ let i = 0
+ for (const prom of proms) {
+ i++
+ }
+ if (i === 0) { resolve([]) }
})
}
  1. 遍历执行。将参数中每个任务包装为子 Promise,当某个子 Promise 报错时,整个 Promise 置为 reject 状态
Promise.myAll = function (proms) {
return new Promise((resolve, reject) => {
let i = 0
for (const prom of proms) {
i++
+ Promise.resolve(prom)
+ .catch(e => {
+ reject(e)
+ })
}
if (i === 0) { resolve([]) }
})
}
  1. 维护结果数组,在每个子 Promise 返回结果后,将结果放入结果数组的对应项
Promise.myAll = function (proms) {
return new Promise((resolve, reject) => {
let i = 0
const result = []
for (const prom of proms) {
// 缓存 index 位置
+ let index = i
i++
Promise.resolve(prom)
+ .then(r => {
+ result[index] = r
+ })
.catch(e => {
reject(e)
})
}
if (i === 0) { resolve([]) }
})
}
  1. 每当一个任务结束,就执行 i--,直到任务全部处理完,整个 Promise resolve 即可
    • 因为当代码执行到 then 回调时,for 循环已经结束,i 已经完成任务计数,所以不会与 i++ 发生冲突
Promise.myAll = function (proms) {
return new Promise((resolve, reject) => {
let i = 0
const result = []
for (const prom of proms) {
let index = i
i++
Promise.resolve(prom)
.then(r => {
result[index] = r
+ i--
+ if (i === 0) {
+ resolve(result)
+ }
})
.catch(e => {
reject(e)
})
}
if (i === 0) { resolve([]) }
})
}

Promise.race

同样接受一个 promise 可迭代对象,返回最快的那个 promise 的结果或错误即可

Promise.myRace = function (proms) {
// 同样返回 promise
return new Promise((resolve, reject) => {
for (let i = 0; i < proms.length; i++) {
// 遍历,只要某个子 promise 有结果了,就作为整体 Promise 的结果 resolve 或 reject
Promise.resolve(proms[i])
.then(r => {
resolve(r)
}).catch(err => {
reject(err)
})
}
})
}

Promise.allSettled

allSettled 与 all 的区别在于对错误的处理方式

  • 只要有错,all 就直接返回错误,而 allSettled 会将错误也作为结果
  • allSettled 结果的数据结构为 { status: 'fulfilled' | 'rejected', [value | reason]: any }
Promise.myAllSettled = function (proms) {
return new Promise((resolve, reject) => {
let i = 0
const result = []
for (const prom of proms) {
let index = i
i++
Promise.resolve(prom)
.then(r => {
- result[index] = r
+ result[index] = { status: 'fulfilled', value: r }
i--
if (i === 0) {
resolve(result)
}
})
.catch(e => {
- reject(e)
+ result[index] = { status: 'rejected', reason: e }
+ i--
+ if (i === 0) {
+ resolve(result)
+ }
})
}
if (i === 0) { resolve([]) }
})
}

Catch

Promise.prototype.myCatch = function (onRejected) {
return this.then(undefined, onRejected)
}

Promise.resolve

Promise.myResolve = function (value) { 
if (value instanceof Promise) { return value }
if (isPromiseLike(value)) {
return new Promise((res) => {
value.then(res)
})
} else {
return new Promise((res) => res(value))
}
}

Promise.reject

Promise.myReject = function (reason) {
return new Promise((_, reject) => reject(reason))
}

· 阅读需 6 分钟

题目描述

  • 依次顺序执行一系列任务

  • 所有任务全部完成后可以得到每个任务的执行结果

  • 需要返回两个方法,start 用于启动任务,pause 用于暂停任务

  • 每个任务具有原子性,即不可中断,只能在两个任务之间中断

  • 每个任务无参,异步

function processTasks(...task) {}

实现

首先确定返回值为两个方法,start 和 pause

function processTasks(...task) {
return {
start() {}
pause() {}
}
}

先实现 start 的基本功能:遍历 tasks 并取出任务,依次执行,将收集到的结果保存,当全部任务结束后返回最终结果

function processTasks(...tasks) {
// 保存结果
const result = []
return {
// 任务是异步的,所以 start 也是异步的
async start() {
// 取出任务并执行
for (let i = 0; i < tasks.length; i++) {
console.log(`任务${i} 开始...`)
const r = await tasks[i]();
console.log(`任务${i} 完成!`)
result.push(r)
}
// 任务全部完成,返回最终结果
if (result.length === tasks.length) {
return result
}
},
pause() { }
}
}

然后考虑中断的情况,需要缓存当前执行的任务索引,以及维护暂停的状态

pause 方法要做的就只是将暂停状态更改为 true

在 start 中,将暂停状态更改为 false,循环执行每个任务之后,判断当前暂停状态,如果暂停了,就使用 return 终止 start 函数的执行

function processTasks(...tasks) {
const result = []
// 缓存当前执行的位置
let currentIndex = 0
// 当前执行状态
let isPaused = false
return {
async start() {
// 开始执行
console.log('start: 开始执行...')
isPaused = false
for (; currentIndex < tasks.length; currentIndex++) {
console.log(`任务${currentIndex} 开始...`)
const r = await tasks[currentIndex]();
console.log(`任务${currentIndex} 完成!`)
result.push(r)
// 每次任务执行结束后,检查当前状态
if (isPaused) {
console.log('start: 暂停,等待恢复...')
// 如果暂停了,就终止函数执行
return
}
}
if (result.length === tasks.length) {
return result
}
},
pause() {
console.log('pause: 暂停执行!')
isPaused = true
}
}
}

目前存在一个问题,就是暂停后恢复执行时,会重复执行上次的任务

currentIndex 应该在任务完成后就自增,循环可以改为 while

// ...
while(currentIndex < tasks.length) {
console.log(`任务${currentIndex} 开始...`)
const r = await tasks[currentIndex]();
console.log(`任务${currentIndex} 完成!`)
result.push(r)
// 在每次任务执行后就更新任务索引
currentIndex++
if (isPaused) {
console.log('start: 暂停,等待恢复...')
return
}
}
// ...

目前任务执行与中断的需求就完成了

不过唯一美中不足的就是,每次暂停执行后,start 的函数就会 return,导致 start 返回的 Promise 会在每次暂停执行后默认 resolve 一个 undefined,与要求中的「所有任务全部完成后可以得到每个任务的执行结果」不符

  • 如何使 start 方法在所有任务都完成后才会返回最终结果呢?

即 start 方法返回的 Promise 只有在全部任务完成后才 resolve

为了手动显式控制 Promise 的 resolve,所以让 start 方法手动返回一个 Promise;当所有任务完成后,使用 resolve(result) 替换 return result

// start 也不用 async 了
start() {
return new Promise(async (resolve) => {
console.log('start: 开始执行...')
isPaused = false
while(currentIndex < tasks.length) {
console.log(`任务${currentIndex} 开始...`)
const r = await tasks[currentIndex]();
console.log(`任务${currentIndex} 完成!`)
result.push(r)
currentIndex++
if (isPaused) {
console.log('start: 暂停,等待恢复...')
// 这里 return 就不会导致整个 start 的 Promise 返回 undefined 了
// 而是仅仅使得当前的 Promise 进入 pending 状态
return
}
}
if (result.length === tasks.length) {
resolve(result)
}
})
}

这样一来,即使任务中断了,return 不会导致当前 start 返回的 Promise 进入 resolve 状态,而是一直维持 pending 状态

最终代码

function processTasks(...tasks) {
const result = []
let currentIndex = 0
let isPaused = false

return {
start() {
return new Promise(async (resolve) => {
isPaused = false
while(currentIndex < tasks.length) {
const r = await tasks[currentIndex]()
result.push(r)
currentIndex++
if (isPaused) {
return
}
}
if (result.length === tasks.length) {
resolve(result)
}
})
},
pause() {
isPaused = true
}
}
}

测试代码

<body>
<button id="begin">启动任务</button>
<button id="pause">暂停任务</button>
</body>
<script src="./3.js"></script>
<script>
const tasks = []
for (let i = 0; i < 5; i++) {
tasks.push(() => {
return new Promise((resolve) => {
setTimeout(() => resolve(i), 1000)
})
})
}
const processor = processTasks(...tasks)

begin.onclick = async () => {
console.log('点击开始')
const results = await processor.start()
console.log('任务执行完成', results)
}

pause.onclick = () => {
console.log('点击暂停')
processor.pause()
}
</script>

· 阅读需 5 分钟

完成一个并发请求的函数 concurRequest,它允许同时发送多个请求,但限制最大并发请求数

当全部请求全部收到响应结果后,返回一个数组,记录响应数据,同时保证响应顺序与参数的顺序一致

返回的 Promise 没有 reject 状态,只有 resolve,如果失败了只需提供失败原因即可

具体的实现思路如下:

  1. concurRequest 函数接受两个参数 urlsmaxNum,分别是待请求的 URL 数组和最大并发请求数。如果传入的 URL 数组为空,函数会直接返回一个解决(resolved)状态的 Promise 对象。
  2. concurRequest 函数中,定义异步函数 _request,用于发起单个请求,并将结果存储在 result 数组中。
  3. 初始化一些变量:index 表示下一次要请求的 URL 下标,result 数组用于存储所有请求的结果,doneCount 表示已经完成的请求数量。
  4. _request 函数中,我们先缓存下标 i 和当前请求的 URL url,然后通过 await 关键字发送请求并将结果存储在 result 数组中对应的位置。
  5. 无论请求是成功还是失败,我们需要增加 doneCount 的值,并在 finally 代码块中进行判断。如果所有请求都已完成,则使用 resolve 方法解析 Promise,并将 result 数组作为解决值。如果还有未完成的请求,继续调用 _request 函数以发送下一个请求。
  6. 最后,在 concurRequest 函数中,我们使用一个 for 循环来调用 _request 函数,最多同时执行 maxNum 个请求。

通过维护一个请求队列和计数器的方式,实现了并发请求的控制,保证最多同时发送指定数量的请求,并在所有请求完成后返回结果。这种实现方式可以在需要同时发起多个请求但又需要控制最大并发数的场景下使用。

const getUrls = (number = 10) => {
const result = []
for (let i = 1; i <= number; i++) {
result.push(`https://jsonplaceholder.typicode.com/todos/${i}`)
}
return result
}

/**
* 并发请求
* @param {string[]} urls 待请求的 url 数组
* @param {number} maxNum 最大并发数
*/
function concurRequest(urls, maxNum) {
if (urls.length === 0) {
return Promise.resolve([])
}
return new Promise((resolve) => {
let index = 0
const result = []
let doneCount = 0
async function _request() {
const i = index // 将 index 缓存到当前作用域下
const url = urls[index]
index++
try {
const res = await fetch(url)
result[i] = res
} catch (error) {
result[i] = error
} finally {
doneCount++
if (doneCount === urls.length) {
resolve(result)
}
if (index < urls.length)
_request()
}
}
for (let i = 0; i < Math.min(maxNum, urls.length); i++){
_request()
}
})
}

concurRequest(getUrls(10), 3).then(res => {
console.log(res)
})

Promise.all 以及 Promise.allSettled 区别

  1. 最大并发数的限制:concurRequest 函数一开始就限制了最大的并行请求数量 maxNum 。而 Promise.allPromise.allSettled 并不具有这样的限制,它们会立即启动所有传入的 Promise 。
  2. 错误处理:Promise.all 对 Promise 的错误处理是有敏感性的;如果 Promise 数组中任意一个 Promise 失败(rejected),整个 Promise.all 就会立刻失败,并且失败的原因是第一个失败的 Promise 的结果。而 Promise.allSettledconcurRequest 不会因为单个 Promise 失败而导致整体失败,它们会等待所有传入的 Promise 都结束,无论其结果是解决(resolved)还是失败(rejected)。
  3. 结果的返回:concurRequest 函数返回的是一个和输入一致的数组,结果的顺序保证和输入的 Promise 的顺序一致,它们的索引一一对应。而 Promise.allPromise.allSettled 则按照 Promise 完成的顺序确定结果的顺序。

· 阅读需 4 分钟

并发任务控制题

  • 实现 SuperTask 类
// 辅助函数,指定时间后 Promise 完成
function timeout(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
}

const superTask = new SuperTask()

function addTask(time, name) {
superTask
.add(() => timeout(time))
.then(() => {
console.log(`任务 ${name} 完成`)
})
}
  • 要求
addTask(10000, 1) // 10000ms 后输出:任务1完成
addTask(5000, 2) // 5000ms 后输出:任务2完成
addTask(3000, 3) // 8000ms 后输出:任务3完成
addTask(4000, 4) // 12000ms 后输出:任务4完成
addTask(5000, 5) // 15000ms 后输出:任务5完成
  • 分析

SuperTask 实例具有 add 方法,接收一个任务(同步/异步),返回一个 Promise,当 Promise 完成后打印完成日志

任务1和任务2都是在指定时间后输出的,但任务3是在任务2后面输出的,由此推断 SuperTask 默认最大并发数为 2

类比于银行柜台排队办事,一共有两个柜台,最多同时处理两位客户的任务。后续的客户需要排队等待,哪个柜台完成了就去补位执行

  • 实现

需要维护三个属性,分别是最大并发任务数(默认2)、正在执行的任务数以及任务队列

add 方法接受一个新任务,返回一个 Promise,在这个 Promise 中,将新任务加入到任务队列

类比于我们去银行排队办事的「叫号」行为,在每次添加任务后,就需要去触发叫号过程,尝试执行任务

将尝试执行任务的逻辑抽离为私有方法 _run,当当前执行任务数量小于最大并发数量、任务队列存在任务的情况下,循环执行任务

  • 为啥用 while

因为我们不仅要检查当前正在执行的任务数是否小于并行任务数,如果是的话就启动一个新的任务,而且我们还要在每次任务完成时再次进行这个检查。如果还有等待执行的任务并且当前正在执行的任务数仍小于并行任务数,那么我们还需要启动更多的任务。while 循环在当前 context 中直到满足该条件才会停止,这正是我们使用 while 的原因。

  • 要使用 Promise.resolve 包裹 task 返回值

使用 Promise.resolve() 可以确保不论那个 task 的返回值是否为 Promise 对象 ,我们总是在处理一个 Promise 对象。这就确保了我们可以在任务完成或失败后调用 .then.catch 方法。

执行完毕后,无论成功失败,都会(finally)再次调用 _run 方法检查是否还有任务可以执行

class SuperTask {
constructor(parallelCount = 2) {
this.parallelCount = parallelCount
this.tasks = []
this.runningCount = 0
}

add(task) {
return new Promise((resolve, reject) => {
this.tasks.push({
task,
resolve,
reject
})
this._run()
})
}

_run() {
while (
this.runningCount < this.parallelCount &&
this.tasks.length
) {
this.runningCount++
const { task, resolve, reject } = this.tasks.shift()
Promise.resolve(task())
.then(resolve, reject)
.finally(() => {
this.runningCount--
this._run()
})
}
}
}