点击蓝字 「前端小苑」关注我
本文导读
Promise是工作中非常常用,也是面试经常问到的知识点,本文从以下几方面重学 Promise,对它有更深入的理解,帮助我们更轻松的面对工作和面试。
1. Promise/A+ 规范解读
2. es6的 Promsie 介绍
3. 实现一个符合 Promsie/A+ 规范的 Promsie 和es6中新增的一些重要方法
4. 总结
作者 | 于是乎
编辑 | 桔子酱
01
Promise规范
在 Promise 出现之前,js中的异步是一直依靠回调函数来实现的,当多个异步操作有依赖关系关系的时候,就会出现多层嵌套,这样多个异步的操作形成了强耦合,牵一发而动全身,很难维护,这就是"回调函数地狱"。
Promise 规范最早是由社区提出和实现的,CommonJS组织先后提出了Promise/A、Promise/B、Promise/D 规范,后来一个叫 Promise/A+ 的组织,基于 Promise/A 规范进行修改和补充,制定了 Promise/A+ 规范。ES6中 Promise 的实现就是基于这个规范来实现的。
Promise/A+ 规范分为术语和要求两部分,下面我们看一下核心的规范内容(文章结尾参考文章中,给出了 Promise/A+ 原文地址,有兴趣可以参考)
术语
promise 一个包含了兼容规范的then方法的对象或函数。
thenable 一个包含了then方法的对象或函数。
value 一个任何Javascript值。
exception是由throw表达式抛出来的值。
reason 是一个用于描述Promise被拒绝原因的值。
要求
Promise 状态
Promise 必须是下面3个状态之一:pending, fulfilled 或 rejected。
状态只能由 pending 转到 fulfilled 或 rejected 状态,且状态不能再改变。
状态由 pending 转到 fulfilled ,必须有一个值,由pending转到rejected,必须有一个reason(原因)。
then方法
Promise 必须提供一个then方法来获取其值或原因。then方法接受两个参数:
promise.then(onFulfilled, onRejected)
1. onFulfilled 和 onRejected 都是可选的:如果 onFulfilled 或 onRejected 不是函数,直接忽略
2. 如果 onFulfilled 是函数, 它必须在状态变为 fulfilled 后调用,且不能调用多次,promise 的 value 为其第一个参数。
如果 onRejected 是函数, 它必须在状态变为 rejected 后调用,且不能调用多次,promise 的 reason 为其第一个参数。
3. 对于一个 promise,它的 then 方法可以调用多次。
当状态 fulfilled 后,所有 onFulfilled 都必须按照其注册顺序执行。当状态 rejected 后,所有 OnRejected 都必须按照其注册顺序执行。
02
es6 的 Promise
下面我们参照 promise 规范,一起看下es6对promise的实现。
ES6 中,Promise 对象是一个构造函数,用来生产 Promise 实例。Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。
resolve:状态从 pending 变为resolved时调用,并将异步操作的结果作为参数;
reject: 状态从从 pending 变为 rejected 时调用,并将异步操作报出的错误作为参数。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
then方法
then 方法定义在 Promise.prototype 上,Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数,其中 rejected 状态的回调函数(第二个参数)是可选的。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then 方法会返回的是一个新的 Promise 实例,因此可以采用链式写法。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 1);
});
}
timeout(5000)
.then((value) => {
console.log(value);
return 2
})
.then((value) => {
console.log(value)
})
.then((value) => {
console.log(value)
})
// 1
// 2
// undefined
上面代码采用了then的链式写法,此时会按照次序依次调用回调函数。第一个回调函数完成以后,会将返回值2作为参数,传入第二个回调函数,所以第二个回调函数会打印2,由于第2个函数没有返回值,所以第3个回调会打印 undefined。
catch方法
catch 方法也定义在 Promise.prototype 上。是.then(null, rejection)或.then(undefined, rejection)的别名,用来指定发生错误时的回调函数。
$.post("/path1")
.then(() => {
return $.post("/path2")
})
.then(() => {
return $.post("/path3")
})
.catch((err) => {
console.log(err)
})
上面的代码中,如果异步操作发生错误,状态变为 rejected 时,就会触发 catch 方法中的回调函数,另外,如果代码运行出现错误也会执行 catch 方法。
Promise 对象的错误总是会被下一个 catch 语句捕获,也就是说上面代码中的3个 Promise 中的任何一个出现错误,都能够被 catch 捕获。
catch 方法并不是 Promise/A+ 规范中规定的,下面我们要说的一些方法也都不是 Promise/A+ 中规定的,而是es6中新增的。
在使用时,使用 catch 方法,而不是then方法的第二个参数,来处理异常情况是更好的选择。第一,catch 可以捕获上面所有then方法中的执行错误。第二,使用 catch 方法更接近于同步的写法。
resolve()
将现有对象转为promise 对象
reject()
Promise.reject(reason)方法也会返回一个新的 promise 实例,该实例的状态为 rejected。
finally方法
ES2018 标准引入,用于指定不管 Promise 对象最后状态如何,都会执行的操作。
finally方法不依赖于 Promise 的执行结果,其回调函数不接受任何参数。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码,不管 promise 的状态怎么变化,最终都会执行 finally 方法中的回调。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
Promise.all
Promise.all() 方法可以将多个 Promise 实例包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中p1,p2,p3都为promise实例。包装后的promise实例的状态由p1,p2,p3决定,有两种情况。
只有p1、p2、p3的状态全都变成 fulfilled,p的状态才会变成 fulfilled,此时传递给p回调函数的参数将是p1、p2、p3的返回值组成一个数组。
只要p1、p2、p3之中任意一个被 rejected,p的状态就变成 rejected,此时传递给p的回调函数的参数是第一个 reject 的实例的返回值。
Promise.race
Promise.race()方法也是将多个 Promise 实例包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3中任一个实例状态发生变化,p的状态就会改变。第一状态变化的 Promise 实例的返回值,就传递给p的回调函数的参数。
Promise.allSettled
ES2020 标准引入,同样接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。
const p = Promise.allSettled([p1, p2, p3]);
上面代码中,只有等到所有p1,p2,p3实例状态都发生改变,无论状态是fulfilled还是rejected,实例p的状态才会发生变化。
Promise.any
该方法目前是一个第三阶段的提案。该方法同样接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。
const p = Promise.any([p1, p2, p3]);
只要参数p1,p2,p3中任意一个变成 fulfilled 状态,包装后的实例p就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。
03
手写Promise
下面一步步自己实现一个 Promsie,目的并不是完全实现一个和es6一样功能的 Promise,而是通过手写一个 Promise,更深入的了解 Promise 的原理。
下面我们先实现一个符合 Promise/A+ 规范的 Promsie。
工具函数定义
首先定义 promise 的状态常量和和一些工具函数
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
// 工具函数,判断是否为函数
const isFunction = fn => Object.prototype.toString.call(fn) === '[object Function]'
// 工具函数,判断状态是否为pending
const isPending = status => status === PENDING
// 工具函数,判断是否是promise实例
const isPromise = target => target instanceof Promise
经过上面我们知道,Promise 类接收一个函数作为参数,这个函数又有两个参数 resolve,reject,这两个参数也是方法。
Promise类创建
// 创建Promise类
class Promise {
constractor(executor){
// 要求Promise的参数必须为函数,如果不是函数就抛出错误
if(!isFunction(executor)){
throw new Error('Promise must accept a function as a parameter')
}
this._status = PENDING // 初始化状态值为pending
this._value = undefined // 给value设置默认值
this._reason = undefined // 给reason设置默认值
// 执行executor方法,把参数resole,reject传递进来
try {
executor(this._resolve.bind(this), this._reject.bind(this))
}
catch (err) { // executor执行出现异常调用rejected方法
this._reject(err)
}
},
// 定义executor的参数resolve的执行函数
_resolve(value){
const {_status: status} = this
if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
this._value = value // 将resove方法传递进来的参数保存下来
this._status = FULFILLED // 状态更新为fufilled
}
},
// 定义executor的参数reject的执行函数
_reject(reason){
const {_status: status} = this
if(isPending(status)){ // 状态只能由pending变成其他两种状态,且只能改变一次
this._reason = reason
this._status = REJECTED
}
}
}
then方法实现
在实现 then 方法之前,建议读者再仔细阅读上面 Promise/A+ 规范的 then 部分,有助于更好的理解这部分代码。
then 方法接受两个参数,onFulfilled 和 onRejected。并且 then 方法必须返回一个 promise 对象,可以被同一个 promise 对象调用多次,当 promise 成功状态时,所有 onFulfilled 需按照其注册顺序依次回调 当 promise 失败状态时,所有 onRejected 需按照其注册顺序依次回调。
首先在 constractor 增加两个数组,fulfilledQueues,rejectedQueues,用来维护每次 then 注册时的回调函数。
this._fulfilledQueues = [] // 添加成功回调函数队列
this._rejectedQueues = [] // 添加失败回调函数队列
// then 方法实现
then(onFulfilled, onRejected) {
// 定义then方法返回的promise
return new Promise((resolve, reject) => {
// 成功时执行的函数
let fulfilledHandle = (value) => {
try {
// 如果onFulfilled不是函数,忽略掉
if (isFunction(onFulfilled)) {
const result = onFulfilled(value)
// 如果当前回调函数返回值为promise实例,需要等待其状态变化后再执行下一个回调
if (isPromise(result)) {
result.then(resolve, reject)
} else {
// 如果当前回调函数返回值不是promise实例,直接执行下一个then的回调,并将结果作为参数
resolve(result)
}
}
} catch (err) {
// 如果新的promise执行出错,新的promise状态直接变为rejected,调用reject函数,并将error作为参数
reject(err)
}
}
// 失败时执行的函数,实现上同成功时执行的函数
let rejectedHandle = (reason) => {
try {
// 如果onFulfilled不是函数,忽略掉
if (isFunction(onRejected)) {
const result = onRejected(reason)
if (isPromise(result)) {
result.then(resolve, reject)
} else {
reject(reason)
}
}
} catch (err) {
reject(err)
}
}
const { _value: value, _status: status, _reason: reason, _fulfilledQueues: fulfilledQueues, _rejectedQueues: rejectedQueues } = this
switch (status) {
// 状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
fulfilledQueues.push(fulfilledHandle)
rejectedQueues.push(rejectedHandle)
break
// 状态为fulfilled时,直接执行onFulfilled方法
case FULFILLED:
fulfilledHandle(value)
break
// 状态为rejected时,直接执行onRejected方法
case REJECTED:
rejectedHandle(reason)
break
}
})
}
实现then的链式调用
为了实现 then 方法的链式调用,我们需要修改 constractor 下的 _resolve 和 _reject 方法,让他们依次执行成功/失败任务队列中的函数。
_resolve(value) {
const { _status: status } = this
const resolveFun = () => {
if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
this._value = value // 将resove方法传递进来的参数保存下来
this._status = FULFILLED // 状态更新为fufilled
let cb
while (cb = this._fulfilledQueues.shift()) {
cb(value)
}
}
}
// 通过setTimeout 0 模拟同步的promise情况
setTimeout(resolveFun, 0)
}
_reject(reason) {
const { _status: status } = this
const rejectFun = () => {
if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
this._reason = reason
this._status = REJECTED
let cb
while (cb = this._rejectedQueues.shift()) {
cb(reason)
}
}
}
setTimeout(rejectFun, 0)
}
这样我们就实现了一个符合 promsie/A+ 规范的 Promise。
下面再来实现es6的 Promise 的一些方法
finally方法
finally(callback){
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
}
Promise.resolve和Promise.reject
static resolve (value) {
if (value instanceof Promise) return value
return new Promise(resolve => resolve(value))
}
static reject (value) {
return new Promise((resolve ,reject) => reject(value))
}
Promise.all
Promise.all 是 Promise 类的静态方法。
static all(list){
return new Promise((resolve, reject) => {
let count = 0 // promise状态结束的个数,初始化为0
let resValues = [] // 状态转为fufilled的value值
const listLength = list.length
for (let i = 0; i < list.length; i++) {
// 如果不是不是Promise,调用Promise.resolve方法
const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
p.then((res)=>{
resValues.push(res)
count++
// 所有状态都变为fufilled,直接resolve,返回resValues
if(count === listLength){
resolve(resValues)
}
}, (e)=> {
// 如果有一个reject,直接reject
reject(e)
})
}
})
}
Promsie.race
static race(list){
return new Promise((resolve, reject) => {
for(let i = 0; i < list.length; i++){
const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
// 只要有一个状态转变,新的promsie状态就转变
p.then((res) => {
resolve(res)
}, (e) => {
reject(e)
})
}
})
}
下面是刚刚实现的 promsie 的完成代码:
// 定义promise的状态常量
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
// 工具函数,判断是否为函数
const isFunction = fn => Object.prototype.toString.call(fn) === '[object Function]'
// 工具函数,判断状态是否为pending
const isPending = status => status === PENDING
// 工具函数,判断是否是promise实例
const isPromise = target => target instanceof Promise
// 已经知道,promise 类接收一个函数作为参数,这个函数又有两个参数resolve,reject,这两个参数也是方法。
class Promise {
constructor(executor) {
// 要求Promise的参数必须为函数,如果不是函数就抛出错误
if (!isFunction(executor)) {
throw new Error('Promise must accept a function as a parameter')
}
this._status = PENDING // 初始化状态值为pending
this._value = undefined // 给value设置默认值
this._reason = undefined // 给reason设置默认值
this._fulfilledQueues = [] // 添加成功回调函数队列
this._rejectedQueues = [] // 添加失败回调函数队列
// 执行executor方法,把参数resole,reject传递进来
try {
executor(this._resolve.bind(this), this._reject.bind(this))
} catch (err) { // executor执行出现异常调用rejected方法
this._reject(err)
}
}
// 为了实现 then 方法的链式调用,我们需要修改constractor下的 _resolve 和 _reject 方法,让他们依次执行成功/失败任务队列中的函数。
_resolve(value) {
const { _status: status } = this
const resolveFun = () => {
if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
this._value = value // 将resove方法传递进来的参数保存下来
this._status = FULFILLED // 状态更新为fufilled
let cb
while (cb = this._fulfilledQueues.shift()) {
cb(value)
}
}
}
// 通过setTimeout 0 模拟同步的promise情况
setTimeout(resolveFun, 0)
}
_reject(reason) {
const { _status: status } = this
const rejectFun = () => {
if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
this._reason = reason
this._status = REJECTED
let cb
while (cb = this._rejectedQueues.shift()) {
cb(reason)
}
}
}
setTimeout(rejectFun, 0)
}
// then 方法实现
then(onFulfilled, onRejected) {
// 定义then方法返回的promise
return new Promise((resolve, reject) => {
// 成功时执行的函数
let fulfilledHandle = (value) => {
try {
// 如果onFulfilled不是函数,直接忽略
if (isFunction(onFulfilled)) {
const result = onFulfilled(value)
// 如果当前回调函数返回值为promise实例,需要等待其状态变化后再执行下一个回调
if (isPromise(result)) {
result.then(resolve, reject)
} else {
// 如果当前回调函数返回值不是promise实例,直接执行下一个then的回调,并将结果作为参数
resolve(result)
}
}
} catch (err) {
// 如果新的promise执行出错,新的promise状态直接变为rejected,调用reject函数,并将error作为参数
reject(err)
}
}
// 失败时执行的函数,实现上同成功时执行的函数
let rejectedHandle = (reason) => {
try {
// 如果onRejected不是函数,直接忽略
if (isFunction(onFulfilled)) {
const result = onRejected(reason)
if (isPromise(result)) {
result.then(resolve, reject)
} else {
reject(reason)
}
}
} catch (err) {
reject(err)
}
}
const { _value: value, _status: status, _reason: reason, _fulfilledQueues: fulfilledQueues, _rejectedQueues: rejectedQueues } = this
switch (status) {
// 状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
fulfilledQueues.push(fulfilledHandle)
rejectedQueues.push(rejectedHandle)
break
// 状态为fulfilled时,直接执行onFulfilled方法
case FULFILLED:
fulfilledHandle(value)
break
// 状态为rejected时,直接执行onRejected方法
case REJECTED:
rejectedHandle(reason)
break
}
})
}
finially(callback){
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
}
static resolve (value) {
if (value instanceof Promise) return value
return new Promise(resolve => resolve(value))
}
static reject (value) {
return new Promise((resolve ,reject) => reject(value))
}
static all(list){
return new Promise((resolve, reject) => {
let count = 0 // promise状态结束的个数,初始化为0
let resValues = [] // 状态转为fufilled的value值
const listLength = list.length
for (let i = 0; i < list.length; i++) {
// 如果不是不是Promise,调用Promise.resolve方法
const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
p.then((res)=>{
resValues.push(res)
count++
// 所有状态都变为fufilled,直接resolve,返回resValues
if(count === listLength){
resolve(resValues)
}
}, (e)=> {
// 如果有一个reject,直接reject
reject(e)
})
}
})
}
static race(list){
return new Promise((resolve, reject) => {
for(let i = 0; i < list.length; i++){
const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
// 只要有一个状态转变,新的promsie状态就转变
p.then((res) => {
resolve(res)
}, (e) => {
reject(e)
})
}
})
}
}
04
总结
对比回调函数而言,Promise 可以通过链式写法,将异步操作以同步操作的流程表达出来。
Promise 的一个更大的特点是,它为所有的异步操作提供了一个统一的 API,这让我们可以更统一的处理异步操作。
然而 Promsie 的本质还是回调函数,只是在写法上进行了改进,在 then 中执行,使其看起来更清楚而已。带来的坏处也很明显,原有的代码都要使用promsie包装一层,一眼看去都是then,使原有的语义看起来不是十分清楚。
那么除了 Promise 有什么更好的写法吗?优缺点又是什么呢,欢迎关注重学js异步系列文章。
参考
https://segmentfault.com/a/1190000004325757
https://promisesaplus.com/
https://segmentfault.com/a/1190000002452115
https://es6.ruanyifeng.com/#docs/promise
更多文章请点击“阅读原文”
喜欢本文点个“在看”哟!