前言
现代 JavaScript 开发中,异步编程是一个很重要的话题,Promise
的出现为 JavaScript 异步编程提供了优雅的标准解决方案
本篇文章我们将循序渐进理解 Promise
核心知识
同步和异步的概念理解
我们都知道,JavaScript 是单线程的,如果把线程想象成一个快递员(镇上唯一的快递员),那么
同步模式:你必须按顺序挨家挨户的送货,每送到一户,都要等待收件人签收,在当前收件人完成签收前,你无法前往下一户
基于这样的情况,不难看出同步模式的特点在于:
- 阻塞式执行:必须要等待收件人签收,才能送下一单
- 执行顺序是可预测的:某一个快递是否签收?这样的问题可以预测或解答
- 利用率低:如果某个快递消耗时间长(比如开箱检查),快递员必须等待
异步模式:快递员将包裹统统放在驿站,每个包裹都有一个取件码,收件人凭取件码取件,它们之前互不影响
同样的,异步模式的特点在于:
- 非阻塞式执行:快递员把快递存放驿站,取件/签收由收件人自行完成
- 并发:可以处理多个快递的投放状态
记住,同步与异步的本质区别在于:是否会阻塞当前执行流程
JavaScript 中的异步概念,更多指的是单线程 + 任务队列管理异步任务的执行顺序
注意,JavaScript 是单线程的,要避免将异步和多线程混淆,异步是同时处理多个任务,就好比一个人在边吃饭边看电视,而不是两个人吃饭看电视
Promise 之前的回调地狱(Callback Hell)
回调地狱是在异步操作中因多层嵌套的回调函数导致的代码结构问题,它的表现方式就像是俄罗斯套娃,一层一层往下套,特别是在处理多个顺序依赖的异步操作时,也就是下一层的操作,需要基于上一层的数据
js
function double(value,successCallback){
setTimeout(()=>{
successCallback(value * 2)
},1000)
}
// 回调地狱
double(10,(firstResult)=>{
console.log(firstResult);// 20
double(firstResult,(secondResult)=>{
console.log(secondResult); // 40
double(secondResult,(thirdResult)=>{
console.log(thirdResult); // 80
})
})
})
上面这段代码,在一秒后将打印出数字 20,再一秒后打印 40,最后是 80
回调地狱的核心问题在于其代码的可读性、可维护性难以言喻,就如同打了个死结!
Promise 对象
ECMAScript 6 依据社区广泛流行的 Promise/A+ 规范并加以完善支持,在此基础上推出了 Promise 对象,为异步编程模式提供了标准方案
Promise 的基本用法
Promise
对象是一个构造函数,可以通过 new
操作符来创建它的实例,它接收一个执行器函数(executor)作为参数,比如:
js
const p1 = new Promise(()=>{})
console.log(p1);
在控制台打印 p1
实例,你会发现存在一个 [[PromiseState]]
属性,这个属性的值范围可以是三种状态:
pending
(待定) :初始状态,既没有完成,也没有被拒绝fulfilled
(已完成):意味着操作成功完成rejected
(已拒绝):意味着操作失败
这三种状态中,pending
是最初始的状态,也就是没有做任何影响状态的操作
但要注意的是,一个 Promise
对象的状态一旦发生改变,比如从 pending
转为 fulfilled
、pending
转为 rejected
后,就已经落定,是不可逆的
并且,Promise
状态不能通过 JS 检测到,也无法进行修改
通过执行器函数(executor)来切换状态
上面说到,Promise
实例化时接收一个执行器函数,而这个执行器函数又接收两个函数作为参数,一般是 resolve
和 reject
在 TypeScript 中,表现为:
ts
interface PromiseConstructor {
//...
new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
}
resolve
翻译为解决的意思,可以将 Promise
对象的状态从 pending(待定)
转变到 fulfilled(已完成)
,并将结果(value)放在参数中传递出去
js
const p1 = new Promise((resolve,reject)=>{
resolve('成功!')
})
console.log(p1);
相应的,reject
翻译为拒绝的意思,可以将 promise
状态从pending(待定)
转变到 rejected(已拒绝)
,并将理由(reason)放在参数中传递出去
js
const p1 = new Promise((resolve,reject)=>{
reject('失败')
})
console.log(p1);
Promise 的实例方法
Promise
的实例上可以访问三种方法,这些方法是在 Primise.prototype
上定义的
Promise.prototype.then
先聊一聊重要的 then()
方法,它为 Promise
对象的 fulfilled(完成)
状态、rejected(拒绝)
状态提供处理程序,接收两个参数:
onFulfilled
:当状态为fulfilled
时执行此回调函数,接收一个参数,参数值来于resolve(value)
中传递的value
值onRejected
:当状态为rejected
时执行此回调函数,接收一个参数,参数值来于reject(reason)
调用时传递的reason
(原因,理由)
onFulfilled
和onRejected
这两个参数在then()
中是可选的,只写单个参数也可以
它的 TypeScript 类型表现为:
ts
interface Promise<T> {
//...
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
}
理论了解后,来看看下面这个例子:
js
const p1 = new Promise((resolve,reject)=>{
if(/**条件 */){
resolve('成功值') // 触发 then 的 onFulfilled
}else{
reject(new Error('失败')) // 触发 then 的 onRejected
}
})
p1.then(
(res)=>{
console.log(res); // 成功值
},
(err)=>{
console.log(err); // Error 对象
}
)
then()
方法会返回一个新的 Promise
实例 ,这意味着 then()
可以形成链式调用,也就是在 then()
之后再接 then()
并且,上一个 then()
可以通过 return
将值传递给下一个 then()
,但请注意,这里有一些规则:
- 如果没有返回,则下一个
then()
的参数是undefined
- 如果返回的是一个普通值,那么将立即触发后续的
then()
,并将值传递 - 如果返回的是一个
Promise
实例,那么等到此Promise
状态为fulfilled(完成)
或rejected(拒绝)
时将值/理由传递
看下面的例子:
js
const p1 = new Promise((resolve,reject)=>{
resolve('成功值') // 触发 then 的 onFulfilled
})
.then(res=>{
console.log('首次then接收:', res) // 首次then接收: 成功值
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('链式') // 异步改变状态为fulfilled(1秒后)
},1000)
})
})
.then(res=>{
// 前序Promise解决后才执行(等待1秒)
console.log('异步then接收:', res) // 异步then接收: 链式
return '第二个then的值' // 同步
})
.then(res=>{
console.log(res); // 第二个then的值
})
Promise.prototype.catch
catch()
方法用于执行 Promise
状态为 rejected(拒绝)
的回调函数
它的 TypeScript 类型表现为:
ts
interface Promise<T> {
//...
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}
在上文介绍的 then()
方法中,我们说它接收 onFulfilled
和 onRejected
两个参数
但在一般的实践中,我们通常使用 catch()
来充当 onRejected
处理程序的角色,因为 catch()
方法实际上是 then()
第二个参数 onRejected
程序的语法糖
并且,catch()
和 then()
一样,返回一个 Promise
对象
js
const p1 = new Promise((resolve,reject)=>{
reject(new Error('失败值'))
})
p1
.then(res=>{
// 当状态为fulfilled时
console.log(res);
})
.catch(err=>{
// 当状态为rejected时
console.log(err);
})
Promise.prototype.finally
finally()
方法的特点是不管 Promise
状态如何变化,都会执行的操作
它接收一个参数:
onFinally
:在状态为fulfilled(成功)
、rejected(拒绝)
时都会触发的回调函数,但要注意,这个回调函数不接收任何参数值
TypeScript类型表现为:
ts
interface Promise<T> {
//...
finally(onfinally?: (() => void) | undefined | null): Promise<T>;
}
和前面的两个方法一样,finally()
方法也返回一个 Promise
Promise 解决回调地狱
还记得在回调地狱章节的示例代码吗?现在,有了 Promise 后,我们有了更棒的解决方案:
js
const asyncDouble = (value,successCallback)=>{
return new Promise((resolve)=>{
setTimeout(()=>{
resolve(value * 2)
},1000)
})
}
asyncDouble(10)
.then(res => { console.log(res); return asyncDouble(res) }) // 20
.then(res => { console.log(res); return asyncDouble(res) }) // 40
.then(res => { console.log(res); return asyncDouble(res) }) // 80
总结
总结一下本文的内容,在开头我们通过快递员的例子讲解了同步和异步的概念:
- 同步:挨家挨户的送货,在未签收前无法前往下一家
- 异步:快递存放驿站,用户可以互不影响,多任务的签收
然后,我们举例说明了在没有 Promise
之前,异步函数多层嵌套带来的可阅读性、可维护性差问题(回调地狱)
正式介绍 Promise
对象时,我们介绍了其基本语法和三个状态,并可以通过执行器函数切换状态,还介绍了 Promise.prototype
原型对象带给实例的三个方法 then()
、catch()
、finally()
参考资料
- 《JavaScript 高级程序编程》第四版
- 《ECMAScript 6 入门教程》阮一峰
- MDN Promise
JavaScript 内功系列
解析 JavaScript 核心技术,提升编程内功
本文已收录至《JavaScript 内功系列》,全文地址:我的 GitHub 博客 | 掘金专栏
对你有帮助的话,欢迎 Star
交流讨论
对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正