Promise 学习系列:下篇

前言

我们用了前面两个文章的篇幅给出了 Promise 及其相关 API 的完整实现。在实际的业务场景(当然包括大家很关心的面试环节)中有很多是基于 Promise 的实现方式,本篇将给出一部分常见问题的代码解答。

1、常见场景

1.1、前菜 timeout,给爱加一个期限

正所谓 “一万年太久只争朝夕”,Promise 生来就无法打断,假如你正好碰上了一个非常耗时的任务,这个时候你需要给他一期限。

这个场景里,一般是需要你为你的承诺——Promise加上一个期限(即,添加原型方法 setTimeout)。

那么简单分析,setTimeout 方法需要满足两个条件:

  • 它需要返回一个新的 promise 方法,以便继续链式调用
  • 拿到此前的承诺状态,此前承诺如果在期限内兑现(能够达到终态,在一起/分手)就传递状态,超期则强制达到终态(分手)

Ok,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
MyPromise.prototype.timeout = function(ms) {
return new MyPromise((resolve, reject) => {
this.then(res => {
resolve(res)
})
.catch(err => {
reject(err)
})
setTimeout(() => {
reject(new Error('timeout'))
}, ms)
})
}

1.2、限流调度器

限流调度器的应用场景很多,常见比如:网络请求中进行并发控制。

1.2.1、场景1

考虑这么一个场景,有一个调度器 Scheduler 类。它通过一个 add 函数可以不断的往里面添加异步任务(实现上可以是一个返回 Promise 对象的函数)。并且控制并发度(限流)为 m,即同时可执行 m 个异步任务。

初始代码框架

1
2
3
4
5
6
7
8
9
class Scheduler {
constructor(){
}
async add(promiseFunc){
return new Promise((resolve, reject) => {
})
}
}

该场景下,添加任务的时候判断下是否达到最大并发度,然后再决定是进行任务执行还是加入等待队列。当执行中的任务执行完毕后唤起等待任务执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Scheduler {
constructor(){
this.waitTasks = [] // 等待中的任务
this.runningCount = 0 // 执行中的任务数
this.taskMax = 2 // 同时执行最大任务量
}
async add(promiseFunc){
return new Promise((resolve, reject) => {
const task = this.getTask(promiseFunc, resolve, reject)
if(this.runningCount < this.taskMax) {
task()
}else{
this.waitTasks.push(task)
}
})
}
getTask(fn, resolve, reject) {
return () => {
this.runningCount++
fn()
.then(resolve, reject)
.finally(()=>{
this.runningCount--
if(this.waitTasks.length > 0) {
const task = this.waitTasks.shift()
if(task) task()
}
})
}
}
}

1.2.2、场景2

该场景下,会议开始接受一组任务,然后设定并发度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function batch(tasks, concurrency) {
let finishCount = 0
const len = tasks.length
function sealingTask(fn, resolve, idx, ans){
return ()=>{
fn()
.then(res=>{
ans[idx] = {
status:'fulfilled',
value: res
}
}, err=>{
ans[idx] = {
status:'rejected',
reason: err
}
})
.finally(()=>{
finishCount++
if(finishCount === len){
resolve(ans)
return
}
const task = tasks.shift()
if(task){
sealingTask(task, resolve, len - tasks.length - 1, ans)()
}
})
}
}
return new Promise((resolve, reject)=>{
if(len) {
const ans = new Array(len)
for (let i = 0; i < len && runningCount < concurrency; i++) {
const task = tasks.shift()
sealingTask(task, resolve, i, ans)()
}
}else{
resolve([])
}
})
}

代码层面并不复杂,一上来直接填满任务进行执行。每个任务执行的时候要注意执行结果在 ans 中的顺序。任务执行完要进行下一个任务唤起,并检查是否已经完成所有任务结果并修改 promise 状态。

1.3、控制器

代码实现一个控制器,使得调用后在控制台打印这样的结果

1
2
3
4
5
6
7
8
9
10
11
12
function SingleDog(name){
//TODO
}
SingleDog('Hank').sleep(2).eat('dinner').sleep(1)
控制台:
Hi! This is Hank!
Wake up after 2
Eat dinner~
Wake up after 1

仔细看这个题,首先,要实现一个链式调用。SingleDog 应该返回一个对象,这个对象有 sleep、eat 方法。其次要实现定时控制。那么我们首先有一个 Executor 函数,这个函数有两个原型方法,这两个方法返回 this 对象以实现链式调用。并且在 SingleDog 中返回一个 Executor 的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SingleDog(name){
console.log(`Hi! This is ${name}!`)
return new Executor()
}
function Executor(){
//todo
}
Executor.prototype.sleep = function(seconds){
//todo
return this
}
Executor.prototype.eat = function(food){
//todo
return this
}

Ok,代码实现到这里,有两个思路,一个是通过 sleep、eat 方法收集任务,然后把收集到的任务按类型进行处理执行。但是在我们这里肯定要用 Promise 来实现。

看一下需求,eat比较简单,那么就变成了怎么来实现 sleep 进行定时控制。

我们考虑下一般的 Promise 实现链式调用代码:

1
2
3
4
5
6
7
8
9
10
11
new Promise(resolve=>{
resolve(1)
})
.then(rs=>{//then1
//
console.log('then1')
})
.then(rs=>{//then2
//
console.log('then2')
})

假如,我想把 then1 的打印做一个延迟打印可以这么修改

1
2
3
4
5
6
7
8
9
10
11
new Promise(resolve=>{
resolve(1)
})
.then(rs=>{//then1
//
setTimeout(()=>console.log('then1'), 1000)
})
.then(rs=>{//then2
//
console.log('then2')
})

那 then2 的打印放在 then1 后面呢?我们回顾下之前的内容,then 方法返回的是一个新的 promise 对象,这个对象的终态会被 then2 执行掉。如果 then1 的 onfulfilled 方法返回的是一个 promise A 呢?那么 then2 的 onfulfilled 方法将在 promise A 达到终态后执行。再次修改代码后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise(resolve=>{ // 初始化 promise 对象
resolve(1)
})
.then(rs=>{//then1
//
return new Promise(resolve=>{
setTimeout(()=>{
console.log('then1')
resolve()
}, 1000)
})
})
.then(rs=>{//then2
//
console.log('then2')
})

Ok,现在我们来拆解下。最开始 Executor 中只有初始化 promise 对象,then1 部分就是 sleep 函数,而 then2 部分就是 eat 函数。接下来把代码填装一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Executor(){
this.promise = Promise.resolve()
}
Executor.prototype.sleep = function(seconds){
this.promise = this.promise.then(()=>{
return new Promise(resolve=>{
setTimeout(()=>{
console.log(`Wake up after ${seconds}`)
resolve()
}, seconds * 1000)
})//.then()
})
return this
}
Executor.prototype.eat = function(food){
this.promise = this.promise.then(()=>{
console.log(`Eat ${food}~`)
})
return this
}

2、结语

Ok,Promise 系列至此已经结束。希望通过这三篇的学习能够让大家对 Promise 有更加深入的认识。第三篇的目的并不是让大家去有目的的记一些题,而是在对 Promise 的实现机制有更加深刻的理解,能够在实际的业务场景中有所发挥。此外,鉴于本人水平有限,代码或表述中有问题可以联系我进行交流。

附录

1.3 小节中控制器的非 Promise 实现参数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function Task(type, arg, executor) {
this.executor = executor
this.type = type
this.arg = arg
}
Task.prototype.run = function(){
if(this.type === 'eat'){
console.log(`Eat ${this.arg}~`)
this.executor.refresh()
this.executor.next()
}else{
setTimeout(() => {
console.log(`Wake up after ${this.arg}`)
this.executor.refresh()
this.executor.next()
}, this.arg * 1000)
}
}
function Executor(){
this.queue = []
this.running = false
}
Executor.prototype.sleep = function(delay){
this.queue.push(new Task('sleep', delay, this))
this.next()
return this
}
Executor.prototype.eat = function(food){
this.queue.push(new Task('eat', food, this))
this.next()
return this
}
Executor.prototype.next = function(){
if(this.running) return
let task = this.queue.shift()
if(task){
this.running = true
task.run()
}
}
Executor.prototype.refresh = function(){
this.running = false
}