在弄清楚Promise为何物之前,首先要明确它为何存在:
- Promise不是新的语法,而是对回调函数这种异步编程的方式进行的改进。
- Promise将嵌套调用改为链式调用,增加了可阅读性和可维护性;
Promise与回调函数
先说结论:回调函数是JS实现异步编程的方式之一,而Promise是解决回调地狱的方式之一。
在JavaScript的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。
以网络请求为例,如果需要在获取前一个请求的数据之后,再发起下一个请求,那么可能会写成如下形式:
ajax1(url1, () => {
doSomething1()
ajax2(url2, () => {
doSomething2()
ajax3(url3, () => {
doSomething3()
})
})
})
如此下去,如果嵌套更多回调函数,就会形成常说的“回调地狱”。
回调地狱的缺点很明显:
- 代码耦合,阅读性差,不好维护;
- 无法使用try catch,就无法排错。
而Promise可以很好的解决“回调地狱”问题:
ajax1(url1).then(res => {
doSomething1()
return ajax2(url2)
}).then(res => {
doSomething2()
return ajax3(url3)
}).then(res => {
doSomething3()
}).catch(err => {
console.log(err)
})
可以看到Promise的优点有:
- 将回调函数的嵌套调用改为链式调用,代码美观;
- 链式调用过程中如果出错,会进入catch方法,捕获错误;
- Promise还提供了其他强大的功能,比如:race、all等;
用Promise改写回调函数
在使用第三方提供的API时,如果该API是用回调函数写的,可以用Promise进行改写。
比如微信小程序发送请求的API:
wx.request({
url: '', // 请求的路径
method: "", // 请求的方式
data: {}, // 请求的数据
header: {}, // 请求头
success: (res) => {
// res 响应的数据
}
})
下面使用Promise改写,即在成功回调中resolve、在失败回调中reject:
function myrequest(options) {
return new Promise((resolve, reject) => { //创建Promise
wx.request({
url: options.url,
method: options.method || "GET",
data: options.data || {},
header: options.header || {},
success: res => {
resolve(res) //在成功回调中resolve
},
fail: err => {
reject(err) //在失败回调中reject
}
})
})
}
使用该自定义API:
myrequest({
url: 'xxx',
header: {
'content-type': 'json'
}
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
Promise的基本概念
Promise是ES6新增的对象,通过new来实例化,实例化时传入一个执行器函数(executor)作为参数:
// 执行器函数有两个参数:resolve、reject,它们也是函数
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value)
} else {
reject(error)
}
})
Promise的特点有:
- 对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态; - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型);
Promise的三种状态
- Pending:等待;
- Fulfilled:完成,调用resolve;
- Rejected:拒绝,调用reject;
从上图可以看出Promise的生命周期:
- Promise的初始状态的是Pending;
- 在创建Promise时就定义好何时resolve、何时reject;
- then方法接收resolve的结果,而catch接收reject的结果,此时Promise状态为Fulfilled或Rejected;
- then、catch方法又会返回新的Promise,从而实现链式调用;
Promise的链式调用
Promise的链式调用是如何实现的呢?先来看看Promise链式调用的一般写法:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
}).then(res => {
//自行处理
...
res = res + '111'
//交给下一层处理
return res
}).then(res => {
//自行处理
...
res = res + '222'
//交给下一层处理
return res
})
按照上图,then方法应该返回一个Promise对象,才能继续调用then/catch方法,但是这里直接return res
为什么也行?
因为在then方法内部会自动将返回值包装成Promise,所以上述代码等价于:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
}).then(res => {
//自行处理
...
res = res + '111'
//交给下一层处理
return Promise.resolve(res)
}).then(res => {
//自行处理
...
res = res + '222'
//交给下一层处理
return Promise.resolve(res)
})
Promise.resolve(res)
是new Promise(resolve => {resolve(res)})
的语法糖。
Promise与微任务
Promise
中的执行函数是同步进行的,但是里面可能存在着异步操作,在异步操作结束后会调用resolve
方法,或者中途遇到错误调用reject
方法,这两者都是作为微任务进入到事件循环中。那么,Promise
为什么要引入微任务的方式来进行回调操作?
如何处理异步回调,有2种方式:
- 将回调函数放在
宏任务队列
的队尾。 - 将回调函数放到
当前宏任务中
的最后面(即作为微任务)。
- 如果采用第一种方式,那么执行回调(resolve/reject)的时机应该是在前面所有的宏任务完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿。
- 为了解决上述方案的问题,另外也考虑到延迟绑定的需求,Promise 采取第二种方式,引入微任务,即把
resolve/ reject
回调的执行放在当前宏任务的末尾;
Promise的执行顺序
实际上要想搞清楚Promise的执行顺序,就是理解Promise是如何进入事件循环的。
前置知识:
1:每一个当下正在被执行的JS代码是放在JS的主线程中的。同步的代码会按照代码顺序依次放入主栈,然后按照放入的顺序依次执行。
2:异步的代码会被放入微任务/宏任务队列,promise属于微任务。
3:异步的代码一定是要等到同步的代码执行完了才执行。也就是说,直到JS Stack为空,微任务队列里面的代码才会被放入主栈,然后被执行。
4:new Promise()和.then()方法属于同步代码。
5:.then(resolveCallback, rejectCallback)里面的resolveCallback, rejectCallback的执行属于异步代码,会被放入微任务队列。
6: resolve()被调用会起到两点作用:
- Promise由pending状态变为resolved;
- 遍历这个promise上所注册的所有的resolveCallback方法,依次加入微任务队列;
7: .then()只是注册callback方法,并不会把callback方法加入微任务队列(参考上面的第6点)。
来看几个例子:
例子一
new Promise((resolve, reject)=> {
console.log(4)
resolve(1)
Promise.resolve().then(()=>{
console.log(2)
})
}).then((t)=>{console.log(t)})
console.log(3)
//输出为:4 3 2 1
分析:
new Promise
的代码是同步执行的,所以其参数,即执行器函数(resolve, reject)=>{}
是同步执行的,所以打印4是立即执行的;resolve(1)
会把外层pomise状态由pending
变成resolved
,但是由于还没执行到外层then,所以此刻最外层的promise上并没有注册任何的callback方法,也就无法把(t)=>{console.log(t)}
加入微任务队列;Promise.resolve()
的结果已经是resolved
了,所以内部then的回调(打印2)直接加入微任务队列;- 最后才轮到外层then的回调(打印1)加入微任务队列;
此时主栈和微任务队列:
JS Stack: [打印4,打印3] Microtask: [打印2,打印1]
例子二
new Promise((resolve, reject)=>{
Promise.resolve().then(()=>{ // cb1
resolve(1)
Promise.resolve().then(()=>{console.log(2)}) // cb2
})
}).then((value)=>{console.log(value)}) // cb3
console.log(3)
//输出:3 1 2
分析:
第2行then的回调(cb1)立即加入微任务队列;
此时:JS Stack: [打印3] Microtask: [cb1]
宏任务执行完就开始执行微任务(只有一个),先执行
resolve(1)
,此时外层promise变成resolved
,所以可以执行外层then了,将外层then的回调(cb3)加入微任务队列;此时:
JS Stack: [] Microtask: [cb3]
接着执行第4行,直接将
cb2
加入微任务队列;此时:
JS Stack: [cb3] Microtask: [cb2]
例子三
new Promise((resolve, reject)=>{
Promise.resolve().then(()=>{ // cb1
resolve(1);
Promise.resolve().then(()=>{console.log(2)}) // cb2
})
Promise.resolve().then(()=>{console.log(4)}) // cb3
}).then((t)=>{console.log(t)}) // cb4
console.log(3);
//输出:3 4 1 2
分析:
第2行和第6行then的回调(cb1、cb3)立即加入微任务队列;
此时:JS Stack: [打印3] Microtask: [cb1, cb3]
宏任务执行完就开始执行微任务(只有一个),先执行
resolve(1)
,此时外层promise变成resolved
,所以可以执行外层then了,将外层then的回调(cb4)加入微任务队列;此时:
JS Stack: [cb3] Microtask: [cb4]
先执行主栈,打印4。接着执行第4行,直接将
cb2
加入微任务队列;此时:
JS Stack: [] Microtask: [cb2]
Promise和async/await
通过以上分析,Promise的链式调用是对于“回调地狱”的优化,但是如果链式调用太长,也不够美观。所以async/await就是进一步来优化then链的。
如果有三个步骤,每一个步骤都需要之前步骤的结果:
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n)
})
}
function step1(n) {
console.log(`step1 with ${n}`)
return takeLongTime(n)
}
function step2(n) {
console.log(`step2 with ${n}`)
return takeLongTime(n)
}
function step3(n) {
console.log(`step3 with ${n}`)
return takeLongTime(n)
}
Promise链式调用会这么些:
function doIt() {
console.time("doIt")
const time1 = 300
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`)
console.timeEnd("doIt")
})
}
doIt()
如果用 async/await 来实现:
async function doIt() {
console.time("doIt")
const time1 = 300
const time2 = await step1(time1)
const time3 = await step2(time2)
const result = await step3(time3)
console.log(`result is ${result}`)
console.timeEnd("doIt")
}
doIt()
结果和之前的 Promise 实现是一样的,但是代码显得很简洁,看上去跟同步代码一样。
下面来看看对于async/await的理解:
- async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成;
- async 是一个修饰符,async 定义的函数会默认的返回一个Promise对象resolve的值,如果在函数中
return
一个直接量,async 会把这个直接量通过Promise.resolve()
封装成 Promise 对象; - await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定);
如果await等到的是一个 Promise 对象,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
所以,可以将所有Promise的链式调用都转换成async/await的形式。
手写Promise
如果能手写出Promise
,那么对其原理的理解自然就会深刻了。
想要手写一个 Promise,就要遵循Promise/A+ 规范,业界所有Promise
的类库都遵循这个规范。
结合Promise/A+
规范,可以分析出Promise
的基本特征:
- promise 有三个状态:
pending
,fulfilled
,orrejected
;「规范 Promise/A+ 2.1」 new promise
时, 需要传递一个executor()
执行器,执行器立即执行;- executor 接受两个参数,分别是
resolve
和reject
; - promise 的默认状态是
pending
; - promise 有一个
value
保存成功状态的值,可以是undefined/thenable/promise
;「规范 Promise/A+ 1.3」 - promise 有一个
reason
保存失败状态的值;「规范 Promise/A+ 1.5」 - promise 只能从
pending
到rejected
, 或者从pending
到fulfilled
,状态一旦确认,就不会再改变; - promise 必须有一个
then
方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected;「规范 Promise/A+ 2.2」 - 如果调用 then 时,promise 已经成功,则执行
onFulfilled
,参数是promise
的value
; - 如果调用 then 时,promise 已经失败,那么执行
onRejected
, 参数是promise
的reason
; - 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调
onRejected
;
实现Promise如下:
// Promise的三种状态
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
// 自定义MyPromise类
class MyPromise{
constructor(executor){
this.status = PENDING
this.value = undefined
this.reason = undefined
// 存放成功的回调
this.onResolvedCallbacks = []
// 存放失败的回调
this.onRejectedCallbacks = []
let resolve = (value) => {
if(this.status === PENDING){
this.status = FULFILLED
this.value = value
// 依次将对应的函数执行
this.onResolvedCallbacks.forEach(fn=>fn())
}
}
let reject = (reason) => {
if(this.status === PENDING){
this.status = REJECTED
this.reason = reason
// 依次将对应的函数执行
this.onRejectedCallbacks.forEach(fn=>fn())
}
}
try{
executor(resolve, reject)
}catch(err){
reject(err)
}
}
// then方法
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
// 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(()=> {
onRejected(this.reason)
})
}
}
}
使用自定义的MyPromise:
const promise = new MyPromise((resolve, reject) => {
setTimeout(()=>{
resolve('成功');
},1000)
}).then(
(res) => {
console.log('success', res)
},
(err) => {
console.log('faild', err)
}
)
注意,以上只是实现了简易版的Promise
,对于链式调用、值穿透特性等还没有实现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。