对于 Promise 的定义和根本应用,可参考红宝书和 MDN。
在弄清楚 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
,对于链式调用、值穿透个性等还没有实现。
参考链接
- Javascript 异步编程的 4 种办法
- 为什么 Promise 要引入微工作?
- Promise 对象——阮一峰
- 了解 JavaScript 的 async/await
- JS – Promise 的执行程序