Promise 承诺
1. 前言
本节课将会引导大家学习了解:
- JavaScript 为什么需要做流程控制
- Promise 是什么
- Promise 的执行过程
- Promise 与 回调函数 的写法对比
- Promise 类 和 promise 对象 的 API 解析
- Promise 怎么解决 回调函数 的缺点
学习完本节课程后,应该具有:
- 使用 Promise 改造 回调函数 控制执行流程的能力
- 从零开始编写使用 Promise 的异步函数的能力
- 巧妙使用
Promise.all()
、Promise.race()
的能力
2. 常见的流程控制手段
2.1 回调函数
最初,遇到异步问题,常被选用的解决方式叫 回调函数。
回调函数就是把异步操作完成后所要执行的代码分装成方法,在异步操作完成之后调用。
用 回调函数 改写后的代码:
javaScript
/**
* 声明 cb 回调函数。
* 在这里,在异步任务完成后,需要打印 end 到控制台
*/
function cb() {
console.log('end');
}
/* 主函数 */
function main(callback) {
console.log('start');
setTimeout(function() {
console.log('running');
callback();
}, 1000);
}
main(cb);
结果:
shell
start
running
end
- 先声明一个回调函数
- 并在调用别的方法时将该回调函数传入
- 主函数在流程恰当的时候调用传入的回调函数
2.2 Promise
ES6 时,JavaScript 加入了目前常被选用的 Promise。
Promise 的出现比较好的解决了前文提到的 回调函数 的两个劣势 回调地狱 和 异常捕获。
用 Promise 改写后的代码:
javaScript
/**
* 用 promise 将异步流程包裹起来,并返回 promise 对象
* 在适当的时候,更改 promise 状态
*/
function task() {
// 返回 promise 对象
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('running');
// 异步操作完成,更改 promise 对象状态为"已完成"
resolve();
}, 1000);
});
}
这里先做 Promise 控制流程的方案展示,下面用别的例子再作解释。
javaScript
console.log('start');
task()
.then(function() {
console.log('end');
});
开始任务流程
- 在第一行,打印
start
- 在第二行,执行任务,任务将在1秒后打印
running
- 在第三至五行,定义等待任务完成后 ,打印
end
结果:
shell
start
running
end
javascript
那么问题来了:
Promise 究竟是什么?
它是怎么做到控制流程的?
为什么 ES6 将其加入 JavaScript 语法,它有什么优势呢?
3. Promise 是什么
3.1 定义
上面的例子告诉我们,异步执行是 JavaScript 特性,所以在程序流程中,需要控制好异步。
Promise 是 CommonJS 工作组提出的一种规范,成为 ES6 新特性,在 Node.js 4.x 时加入。
其目的是为异步操作提供统一接口,将可读性、可维护性低的代码,统一风格成类同步操作写法。
一个 promise 有:
dart
三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
两种转变:等待 => 已完成、等待 => 已拒绝
3.2 思路
Promise(承诺),他的思路是,程序在执行异步操作时,先向上一层返回一个promise(承诺)
对象。
- 一旦异步操作到某一步或已完成,我们可以切换这个
promise
对象的状态到 已完成 ,通过该promise
的then
方法定义的 回调函数 将会被触发。 - 如果异步操作在执行过程中出现异常,我们可以切换这个
promise
对象的状态到 已拒绝 ,通过该promise
的catch
方法定义的 异常捕获函数 将会被触发。
我们拿一个贴近生活的例子,很多奶茶店点单后给顾客派送的 圆盘形提醒器,顾客不需要继续排队等待。
奶茶制作 / 派送流程变成:
-
客户下单奶茶后,向客户派送提醒器,随后客户即可自由行动。
-
奶茶完成后,店员更改某顾客提醒器模式为 亮绿灯并震动。
-
客户看见提醒器 亮绿灯并震动,则来到取茶窗口取茶。
-
如果由于一些原因,导致不能继续完成奶茶制作(用料沽空等),店员更改某顾客提醒器模式为 亮红灯并震动
-
客户看见提醒器 亮红灯并震动,则去客户服务窗了解情况并退款。
在这个例子中: 派送的提醒器就是向上一层返回的promise对象 亮绿灯并震动就是"已完成"状态 亮红灯并震动就是"已拒绝"状态 去取茶窗口就是触发通过promise的then方法定义的回调函数 去客户服务窗就是触发通过promise的catch方法定义的异常处理函数
Promise 配合上 ES7 的 asnyc/await 语法糖,能减少多层嵌套回调产生的"缩进三角",进一步优化代码结构、提升可读性。
3.3 方法详情
promise 对象提供如下API
API | 含义 | 参数 | 备注 |
---|---|---|---|
then | promise.resolve(n)被调用后触发【状态变成"已完成"后】 | function(n) | |
catch | promise.reject(err)被调用后触发【状态变成"已拒绝"后】 | function(err) | |
finally | promise.resolve(n) / promise.reject(err)被调用后触发【状态改变后】 | function() | ES2018新特性,node8.1.4+ 可用 |
Promise 类提供如下API
| API | 含义 | 参数 |
|---------|-----------------------------------------------------------------------------------------------------|----|---|
| all | 返回一个 promise 对象,并于Array内的所有 promise 完成后状态更改为"已完成",若Array中一个 promise 被拒绝,则所返回的 promise 对象状态更改为"已拒绝"。 | | |
| race | 返回一个 promise 对象,并于Array内的任一 promise 完成后状态更改为"已完成",若Array中任一 promise 被拒绝,则所返回的 promise 对象状态更改为"已拒绝"。 | | |
| resolve | 返回一个以给定值解析后的 promise 对象 | | |
| reject | 返回一个带有拒绝原因的 promise对象 | | |
3.4 常规语法格式
javaScript
new Promise(function(resolve, reject) {
// todo
...
})
// 回调函数
.then(function(result) {
// todo
})
// 异常捕获函数
.catch(function(error) {
// todo
})
3.5 对比优势
Promise 对比 普通回调函数,有更良好的编写体验和可阅读性。
普通回调函数:
javaScript
task1(function() {
// todo
...
task2(function() {
// todo
...
task3(function() {
// todo
...
task4(function() {
// todo
...
})
})
})
})
Promise改写:
javaScript
task1()
.then(function() {
// todo
...
return task2();
})
.then(function() {
// todo
...
return task3();
})
.then(function() {
// todo
...
return task4();
})
我们能明显地看出,使用Promise
改写后,对多层嵌套的回调函数的编写体验和可阅读性明显地提高。
4. 代码例子
javaScript
/**
* generatePromise 方法返回一个promise对象
* 该对象将于2秒后,打印 'promise is running!'并且将状态切换成"已完成"
* 故generatePromise方法调用2秒后,其.then内回调函数将触发
*/
function generatePromise() {
console.log('promise start!');
let promise = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('promise is running!');
resolve('promise resolved');
}, 2000)
});
console.log('promise was defined');
return promise;
}
先将异步代码嵌套到Promise中,在异步操作执行完成后,更改该promise
对象的状态。并且为了方便理解代码流程,在promise
定义前后都进行了打印。
javaScript
console.log('demo run!');
generatePromise()
.then(function(result) {
console.log(result)
})
结果打印:
shell
demo run!
promise start!
promise was defined
promise is running!
promise resolved
不难看出,promise所包裹的setTimeout是异步方法,并不会阻塞'promise was defined'的打印。而2秒后'promise is running!'打印完成后,其.then()方法才被激活,故'promise resolved'在最后打印。
5. 执行过程
- 在第一行,打印
demo run
。 - 在第二行,调用
generatePromise
函数。- 先打印
promise start!
。 - 由于
setTimeout()
是异步方法,在promise
完成定义后,不会立马执行和阻塞,将直接打印promise was defined
。 - 将
promise
返回后generatePromise
函数执行完毕,由于promise
状态未被改变,所以.then()
内方法不会被触发。且由于Node.js的event loop仍有任务待执行(setTimeout
),此时程序将继续等待。 - 2秒后,
setTimeout
内代码被执行:先打印promise is running!
,然后将该promise
的状态改成 已完成 ,并将promise resolved
作为promise
的返回值。
- 先打印
- 在第三至五行,由于
promise
的状态被改变成 已完成 ,其.then()
内方法被触发,打印出返回值promise resolved
。
rust
sequenceDiagram
外部逻辑->>外部逻辑: 打印 "demo run"
外部逻辑->>generatePromise: 调用
generatePromise->>generatePromise: 打印 "promise start!"
generatePromise->>timmer: generatePromise 定义 promise 对象,内部方法生成定时器,造成异步
generatePromise->>generatePromise: 打印 "promise was defined"
generatePromise->>外部逻辑: 返回 promise 对象
外部逻辑->>.then回调函数: 为 promise 对象指定 回调函数
timmer->>timmer: 2秒定时结束,触发定时器代码
timmer->>timmer: 打印 "promise is running!"
timmer->>外部逻辑: 修改 promise 状态为"已完成",同时将 "promise resolved" 作为返回值
外部逻辑->>.then回调函数: 由于 promise 对象状态改变,被触发
.then回调函数->>.then回调函数: 打印返回值 "promise resolved"
6. 应用场景
通常可用于控制异步流程,如等待数据库查询,文件读取,甚至等待接口转发的返回值等。
配合Promise类的race方法,可用于给一些操作配置超时时间。
配合Primise类的all方法,可管理多个异步操作。
7. 示例
示例描述一:写一个被延迟执行的方法,且后续方法能被继续延迟。
不使用promise实现:
javaScript
setTimeout(function() {
console.log('wait 1 second');
setTimeout(function() {
console.log('wait 2 more second');
setTimeout(function() {
console.log('wait 3 more second');
setTimeout(function() {
console.log('wait 4 more second');
}, 4000);
}, 3000);
}, 2000);
}, 1000);
使用 promise 实现:
javaScript
function wait(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms);
})
}
javaScript
wait(1000)
.then(function() {
console.log('wait 1 second');
return wait(2000); // 在本轮.then时先完成等待,再触发下轮.then
})
.then(function() {
console.log('wait 2 more seconds');
return wait(3000);
})
.then(function() {
console.log('wait 3 more seconds');
return wait(4000);
})
.then(function() {
console.log('wait 4 more seconds');
})
流程描述:
- 在第一行,调用
wait
函数(下文称wait1
),等待1秒 - 在第二行,指定在
wait1
等待完毕后,执行方法:- 打印
wait 1 second
- 再次调用
wait
函数(下文称wait2
),等待2秒,并将该promise
返回
- 打印
- 在第六行, 由于上一节
.then
链返回了promise
,所以指定在wait2
等待完毕后,执行方法:- 打印
wait 2 more seconds
- 再次调用
wait
函数(下文称wait3
),等待3秒,并将该promise
返回
- 打印
- 在第十行, 指定在
wait3
等待完毕后,执行方法:- 打印
wait 3 more seconds
- 再次调用
wait
函数(下文称wait4
),等待4秒,并将该promise
返回
- 打印
- 在第十四行, 指定在
wait4
等待完毕后,执行方法:- 打印
wait 4 more seconds
- 打印
运行效果展示:
shell
wait 1 second
wait 2 more seconds
wait 3 more seconds
wait 4 more seconds
如果不封装使用,将会出现多层嵌套,并且如果多次出现这种需求,setTimeout将会出现多次,甚至多个文件出现。
进行封装后,使用.then链,代码结构获得优化。
示例描述二:使用 Promise.all 管理多个异步操作
代码实现如下:
javaScript
function wait(ms) {
return new Promise(function(resolve, reject) {
})
}
javaScript
let wait1 = wait(1000)
.then(function() {
console.log('wait1 complete');
}),
wait2 = wait(2000)
.then(function() {
console.log('wait2 complete');
});
Promise.all([wait1, wait2])
.then(function() {
console.log('complete');
});
流程描述:
- 在第一行,调用异步
wait
函数,等待1秒后打印wait1 complete
,并将该promise
赋值给wait1
- 在第五行,重复上一步操作,等待2秒后打印
wait2 complete
,并将该promise
赋值给wait2
- 在第十行,将
wait1
、wait2
两个promise
组装成数组,推入Promise.all()
方法- 等待
wait1
、wait2
都执行完成后,Promise.all()
返回的promise
状态更改为 已完成 - 触发
Promise.all().then()
,打印complete
- 等待
运行效果展示:
shell
wait1 complete
wait2 complete
complete
效果描述:
-
1秒时,打印出
wait1 complete
。 -
2秒时,打印出
wait2 complete
。 -
2秒时(完成打印
wait2 complete
后),马上打印出complete
Promise.all(Array) 在Array内所有promise完成后再触发.then()。
示例描述三:使用 Promise.race 时限超时时间控制
代码实现如下:
javaScript
// 定时更改状态的 promise
const E_TIMEOUT = new Error('timeout'); // 定义执行超时异常
/**
* 定义 timeoutLimit 超时监控方法,返回一个 promise 对象
* 到定义的超时时间时,将 promise 的状态改变为 "已拒绝"
*/
function timeoutLimit(ms) {
return new Promise(function(resolve, reject) {
setTimeout(reject, ms, E_TIMEOUT);
})
}
/* 需要被控制执行时间的方法 */
function someFunction() {
return new Promise(function(resolve, reject) {
// todo
...
resolve();
})
}
JavaScript
Promise.race([someFunction(), timeoutLimit(2000)])
// 异步操作在超时时间前完成,走.then
.then(function(res) {
console.log('task complete');
})
// 异步操作没能在超时时间前完成,Promise.race返回的promise对象被timeoutLimit拒绝,走.catch
.catch(function(err) {
console.warn(err);
})
流程描述:
- 在第一行,将 被控制时间的方法所返回的 promise 和 超时监控方法返回的 promise 组装成数组,推入
Promise.race()
- 在第三行,如果任务能在超时时间内完成,将状态修改为 已完成 ,那么
Promise.race()
返回的promise
的状态也会被修改为 已完成 ,则触发Promise.race().then()
- 打印
task complete
- 打印
- 在第七行,如果任务没能在超时时间内完成,则超时监控方法返回的
promise
的状态修改为 已拒绝 ,那么Promise.race()
返回的promise
的状态也会被修改为 已拒绝 ,则触发Promise.race().catch()
- 打印
timeout 异常
- 打印
8. 提示、注意
- resolve()、reject() 只用于修改该 promise 状态,后续代码还会继续执行,如需终止,还需要 return。
- resolve()、reject() 不需要都使用上,但考虑到代码的健壮性,在可能出现异常的地方捕获异常还是必须的。
- 多层嵌套的promise(.then),仅需要在最外层添加一层 .catch 即可捕获所有异常:
javaScript
function wait(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms);
})
}
wait(1000)
.then(function() {
console.log('wait 1 second');
return wait(2000); // 在本轮.then时先完成等待,再触发下轮.then
})
.then(function() {
console.log('wait 2 more seconds');
throw new Error('test catch')
return wait(3000);
})
.then(function() {
console.log('wait 3 more seconds');
return wait(4000);
})
.then(function() {
console.log('wait 4 more seconds');
})
.catch(function(error) {
console.log('catch error:');
console.log(error);
})
9. 经验分享
Promise.all(Array)
内 promise 如果出现 / 抛出异常,Promise.all(Array).then()
不会触发,而会触发Promise.all(Array).catch()
。Promise.race(Array)
内 promise 就算不是第一个完成,也会执行完所有代码,所以不适用于相互冲突的操作。- promise 可被逐层返回,即:
javaScript
function wait(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms);
})
}
wait(1000)
.then(function() {
console.log('wait 1 second');
return wait(2000)
.then(function() {
console.log('wait 2 more seconds');
return wait(3000)
.then(function() {
console.log('wait 3 more seconds');
// 允许这么返回,且该段等待完成后才触发最外层的.then。但不推荐,因为这种缩进不利于阅读
return wait(4000);
})
})
})
.then(function() {
console.log('wait 4 more seconds'); // 最后打印
})
10. 知识对比
ES6 Promise 和 ES7 async/await 的关系:
- async/await 更像在 ES7 设计给 Promise 的语法糖,其主要针对的就是 promise 对象,用于继续优化 promise 的代码结构,更加方便阅读、理解。
- 换句话说 没有 promise 的话 async/await 没有意义。
Promise 和 普通回调函数 的区别:
- 回调函数 的可阅读性还是稍逊于链式promise。
- 在实际生产上,如果过多使用 多层嵌套的回调函数 ,很可能坠入"回调地狱",使得开发变慢,并且给阅读代码的人很大的负担。
- Promise类 还有如上文提到的.all()、.race()等方法,还可以相对方便做异常捕获,这是 普通回调函数 不能比的。
11. 小结
本节课程我们主要学习了 什么是 Promise 、Promise 和回调函数的对比 、Promise 提供的 API。
重点如下:
-
重点1
Promise 是 Node.js 做流程控制的工具之一,是开发者面对复杂异步场景理顺编程思路的利器,用好 Promise 是 Node.js 开发者的基础能力体现的地方。
-
重点2
Promise.then()
在该 promise 对象 状态修改为 已完成 时触发。Promise.catch()
在该 promise 对象 状态修改为 已拒绝 时触发。Promise.all()
返回一个新的 promise 对象 ,在数组中所有 子 promise 对象 状态修改为 已完成 时,新产生的 promise 对象 的状态也会被修改为 已完成 ;如果 子 promise 对象 中出现一个 已拒绝 状态,新产生的 promise 对象 的状态也会被修改为 已拒绝。Promise.race()
返回一个新的 promise 对象 ,在数组中其中一个 子 promise 对象 状态修改为 已完成 时,新产生的 promise 对象 的状态也会被修改为 已完成 ;如果 子 promise 对象 中最先出现 已拒绝 状态,新产生的 promise 对象 的状态也会被修改为 已拒绝。 -
重点3
解决 回调地狱 的方法是使用
.then()
链,代码会由上至下执行。 -
重点4
解决 回调函数异常捕获 失败的方法是使用
.catch()
,且.catch()
能在最外层捕获内层 Prmise 抛出的异常。