不变的承诺 Promise
背景
Promise 是异步编程的一种解决方案,可以解决前端的回调地狱。以下根据 ES6 入门 简单过一下 Promise 的基础及个人的一些见解补充,同时尝试实现一个符合 PromiseA+ 规范的 Promise。
Promise 基础
Promise 的状态
Promise 只有三种状态:pending(进行中),fulfilled(已成功)和 rejected(已失败)。
Promise 的状态改变只有两种情况:
pending 到 fulfilled 以及 pending 到 rejected。当状态变成 fulfilled 或 rejected时,我们就称这个 Promise resolved。
⚠️ 我们通常说的 resolved 仅指代 fulfilled,不包括 rejected。
构造一个 Promise
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then
Promise 的实例拥有 then 方法:
promise.then(function(value) {
// success
}, function(error) {
// failure
});
⚠️ Promise 新建后立即执行,而 then 方法则属于微任务的回调,执行时机为宏任务执行之后,具体的事件循环机制后面开新坑总结。
resolve
构造 Promise 时,第一个参数 resolve 函数所接收的参数,可以是一个普通的值,也可以是一个 promise。如果接收的是一个 promise 实例,那么 promise 的状态取决于 resolve 接收的 promise 的状态:
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// 3s 后输出:Error: fail
这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。
resolve 与 return
调用resolve或reject并不会终结 Promise 的参数函数的执行:
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return:
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
.catch
Promise.prototype.catch()方法是.then(null | undefined, rejection),用于指定发生错误时的回调函数。Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,即错误总是会被下一个
catch语句捕获。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()与catch()方法。promise 内部的错误不会抛到外层,因此
try/catch无法捕获:try { const p2 = new Promise((resolve, reject) => { throw 'err' }) console.log('out') } catch(e) { console.log('catch you', e) } // out // 未捕获的异常:Uncaught (in promise) err因此最好 promise 后都用 catch 来捕获。
.finally()
- ES2018 引入,即不管 Promise 对象最后状态如何,都会执行的操作。
finally方法的回调函数不接受任何参数
手写一个简易的 Promise
手写一个符合 PromiseA+ 规范的 Promise 并不简单,可以先简单根据上述 promise 的基本功能实现一个简易版本:
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
class MyPromise {
constructor(fn) {
this.state = PENDING
// 收集 .then 中的 resolve 的回调函数
this.resolveCallbacks = []
// 收集 .then 中的 reject 的回调函数
this.rejectCallbacks = []
// resolve(result)
this.result = null
// reject(reason)
this.reason = null
// 由于 fn 有可能异常,需要捕获
try {
// Promise 一实例化就执行传入的 fn
// 注意此处将函数当作参数传入,需要 bind 绑定上下文
// 或者 resolve / reject 用箭头函数实现
fn(this.resolve.bind(this), this.reject.bind(this))
} catch (error) {
this.reject(error)
}
}
resolve(result) {
if (this.state === PENDING) {
// 修改状态
this.state = RESOLVED
this.result = result
// 执行 then 中的 resolve 回调函数
this.resolveCallbacks.forEach((cb) => cb(result))
}
}
reject(reason) {
if (this.state === PENDING) {
// 修改状态
this.state = REJECTED
this.reason = reason
// 执行 then 中的 reject 回调函数
this.rejectCallbacks.forEach((cb) => cb(reason))
}
}
then(onFulfilled, onRejected) {
// 调用 .then 时如果还在 pending 则将回调先存起来,等待 resolve 后执行
if (this.state === PENDING) {
// 收集回调
this.resolveCallbacks.push(onFulfilled)
this.rejectCallbacks.push(onRejected)
}
// 如果调用 .then 时已经 resolve / reject, 那么直接执行对应的回调
if (this.state === RESOLVED) {
onFulfilled(this.result)
}
if (this.state === REJECTED) {
onRejected(this.reason)
}
}
}
测试代码:
const myPromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('resolved')
}, 3000)
})
myPromise.then(
(result) => {
console.log(result)
},
(reason) => {
console.log(reason)
}
)
Promise 的设计思路实际上就是观察者模式:收集回调函数,触发通知,依次执行回调。
上述版本还未能达到 Promise / A+ 的规范,比如 链式调用 等。
符合 Promise / A+ 的 Promise
参考 Promise / A+ ,以下还需要补充下面几点:
- then 方法的参数可选
onFulfilled和onRejected是异步执行的,可以使用如setTimeout或setImmediate之类的“宏任务”机制,或者使用诸如MutationObserver或process.nextTick之类的“微任务”机制来实现。- 链式调用 与 promise 解决步骤
值穿透
针对 then 的参数可选这一点,如果 onFulfilled 和 onRejectd 不是函数,则会被忽略,并且将值传递给下一个 then。即所谓的值穿透,对 then 进行改造:
then(onFulfilled, onRejected) {
// 如果不是函数,传递 result
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (result) => result
// 如果不是函数,抛出 reason
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
// ... if (this.state === PENDING) ...
}
当然当前的简易版 promise 还不支持链式调用,值穿透也就无从谈起。
异步执行
在实现链式调用之前,我们完善代码,使得onFulfilled 和 onRejected是异步执行的。
当前执行以下代码:
var myPromise = new MyPromise((resolve, reject) => {
resolve(1)
})
myPromise.then(
(val) => {
console.log(val)
},
(val) => {
console.log(val)
}
)
console.log(2)
现输出 1 再输出 2,显然不是异步的。根据 Promise/ A+ 的规范,这里我们可以通过 setTimeout 来改造 resolve 和 reject :
// ...
resolve(result) {
// 通过 setTimeout 模拟异步
setTimeout(() => {
if (this.state === PENDING) {
// ...
}
})
}
reject(reason) {
// 通过 setTimeout 模拟异步
setTimeout(() => {
if (this.state === PENDING) {
// ...
}
})
}
// ...
至此,输出顺序将是 2, 1。 符合预期
链式调用
现在来实现链式调用:
then需要返回一个 Promisethen拿到上一次 then 的返回值- 如果 then 返回的是一个 Promise 则需要等待该 promise 状态发生变化后再执行下一个 then
promise2: 我们将 then 返回的 promise 命名为 promise2, 即 promise2 = promise1.then(onFulfilled, onRejected);
x: 将 onFulfilled 或者 onRejected 的返回值命名为 x,这个 x 可以是一个 thenable。
resolutionProcedure: promise 解决步骤。如果 x 具有 thenable 特性,我们就假设 x 的行为至少有点像 promise,它将试图使 promise 接收 x 的状态。否则,它使用值 x 执行 promise。之所以是 thenable 是为了可以兼容别的 promise(例如 fetch)。实际上这个方法在 Promise / A+ 中有很多情况分叉,我们先简化成以下:
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更, 否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
现在的 Promise 完整代码如下,接近完全体版本:
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
class MyPromise {
constructor(fn) {
this.state = PENDING
// 收集 .then 中的 resolve 的回调函数
this.resolveCallbacks = []
// 收集 .then 中的 reject 的回调函数
this.rejectCallbacks = []
// resolve(result)
this.result = null
// reject(reason)
this.reason = null
// 由于 fn 有可能异常,需要捕获
try {
// Promise 一实例化就执行传入的 fn
// 注意此处将函数当作参数传入,需要 bind 绑定上下文
// 或者 resolve / reject 用箭头函数实现
fn(this.resolve.bind(this), this.reject.bind(this))
} catch (error) {
this.reject(error)
}
}
resolve(result) {
setTimeout(() => {
if (this.state === PENDING) {
// 修改状态
this.state = RESOLVED
this.result = result
// 执行 then 中的 resolve 回调函数
this.resolveCallbacks.forEach((cb) => cb(result))
}
})
}
reject(reason) {
setTimeout(() => {
if (this.state === PENDING) {
// 修改状态
this.state = REJECTED
this.reason = reason
// 执行 then 中的 reject 回调函数
this.rejectCallbacks.forEach((cb) => cb(reason))
}
})
}
then(onFulfilled, onRejected) {
// 如果不是函数,传递 result
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (result) => result
// 如果不是函数,抛出 reason
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
// 调用 .then 时如果还在 pending 则将回调先存起来,等待 resolve 后执行
if (this.state === PENDING) {
const promise2 = new MyPromise((resolve, reject) => {
const resolvedCallBack = () => {
try {
const x = onFulfilled(this.result)
// 简化逻辑的 resolutionProcedure
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
// resolutionProcedure(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}
const rejectCallBack = () => {
try {
const x = onRejected(this.result)
// 简化逻辑的 resolutionProcedure
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
// resolutionProcedure(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}
// 收集回调
this.resolveCallbacks.push(resolvedCallBack)
this.rejectCallbacks.push(onRejected)
})
return promise2
}
// 如果调用 .then 时已经 resolve / reject, 那么直接执行对应的回调
if (this.state === RESOLVED) {
onFulfilled(this.result)
}
if (this.state === REJECTED) {
onRejected(this.reason)
}
}
}
resolutionProcedure
最后根据 Promise / A+ 的规范来完善 resolutionProcedure,实际上就是一系列的判断:
const resolutionProcedure = (promise2, x, resolve, reject) => {
if (promise2 === x) {
return reject(new TypeError('Error'))
}
if (x instanceof MyPromise) {
x.then(function(value) {
resolutionProcedure(promise2, value, resolve, reject)
}, reject)
}
let called = false
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return
called = true
resolutionProcedure(promise2, y, resolve, reject)
},
e => {
if (called) return
called = true
reject(e)
}
)
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}
符合 Promise / A+ 的 Promise
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
const resolutionProcedure = (promise2, x, resolve, reject) => {
if (promise2 === x) {
return reject(new TypeError('Error'))
}
if (x instanceof MyPromise) {
x.then(function (value) {
resolutionProcedure(promise2, value, resolve, reject)
}, reject)
}
let called = false
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called) return
called = true
resolutionProcedure(promise2, y, resolve, reject)
},
(e) => {
if (called) return
called = true
reject(e)
}
)
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}
class MyPromise {
constructor(fn) {
this.state = PENDING
// 收集 .then 中的 resolve 的回调函数
this.resolveCallbacks = []
// 收集 .then 中的 reject 的回调函数
this.rejectCallbacks = []
// resolve(result)
this.result = null
// reject(reason)
this.reason = null
// 由于 fn 有可能异常,需要捕获
try {
// Promise 一实例化就执行传入的 fn
// 注意此处将函数当作参数传入,需要 bind 绑定上下文
// 或者 resolve / reject 用箭头函数实现
fn(this.resolve.bind(this), this.reject.bind(this))
} catch (error) {
this.reject(error)
}
}
resolve(result) {
setTimeout(() => {
if (this.state === PENDING) {
// 修改状态
this.state = RESOLVED
this.result = result
// 执行 then 中的 resolve 回调函数
this.resolveCallbacks.forEach((cb) => cb(result))
}
})
}
reject(reason) {
setTimeout(() => {
if (this.state === PENDING) {
// 修改状态
this.state = REJECTED
this.reason = reason
// 执行 then 中的 reject 回调函数
this.rejectCallbacks.forEach((cb) => cb(reason))
}
})
}
then(onFulfilled, onRejected) {
// 如果不是函数,传递 result
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (result) => result
// 如果不是函数,抛出 reason
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
// 调用 .then 时如果还在 pending 则将回调先存起来,等待 resolve 后执行
if (this.state === PENDING) {
const promise2 = new MyPromise((resolve, reject) => {
const resolvedCallBack = () => {
try {
const x = onFulfilled(this.result)
resolutionProcedure(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}
const rejectCallBack = () => {
try {
const x = onRejected(this.result)
resolutionProcedure(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}
// 收集回调
this.resolveCallbacks.push(resolvedCallBack)
this.rejectCallbacks.push(onRejected)
})
return promise2
}
// 如果调用 .then 时已经 resolve / reject, 那么直接执行对应的回调
if (this.state === RESOLVED) {
onFulfilled(this.result)
}
if (this.state === REJECTED) {
onRejected(this.reason)
}
}
}
至此我们完成了一个 符合 Promise / A+ 的 Promise,实际上 Promise 还会提供一些如 .all, .race 之类的 api,这里也实现一下。
Promise.all
Promise.all 接收多个 Promise 实例,返回一个新的 Promise 实例。例如:
const p = Promise.all([p1, p2, p3])
上述的 p1, p2, p3 都是 Promise 实例,如果不是则会用 Promise.resolve 转成 promise。此外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
实现如下:
class MyPromise {
// ...
all(arr) {
// 用于存储返回值
let list = []
// 记录 resolve 了多少个 promise
let length = 0
// 执行 promise 的过程中是否有异常
let hasError = false
// 返回一个新的 Promise
return new MyPromise((resolve, reject) => {
for (let i = 0; i < arr.length; i++) {
// 判断元素是否为 MyPromise, 否则 resolve
const p =
arr[i] instanceof MyPromise ? arr[i] : MyPromise.resolve(arr[i])
p.then((data) => {
list[i] = data
length++
if (length === arr.length) {
// 如果全部 promise 都 resolved
resolve(list)
}
}).catch((error) => {
if (!hasError) {
// 第一个被 reject 的实例的返回值,会传递给 p 的回调函数
reject(error)
}
hasErr = true
})
}
})
}
// ...
}
Promise.race
Promise.race()`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
class MyPromise {
// ...
race(arr) {
// 是否有结果
let hasResult = false
// 返回一个新的 Promise
return new MyPromise((resolve, reject) => {
for (let i = 0; i < arr.length; i++) {
// 判断元素是否为 MyPromise, 否则 resolve
const p =
arr[i] instanceof MyPromise ? arr[i] : MyPromise.resolve(arr[i])
p.then((data) => {
if (!hasResult) {
resolve(data)
}
hasResult = true
}).catch((error) => {
if (!hasResult) {
reject(error)
}
hasResult = true
})
}
})
}
// ...
}