前言
Hello 大家好!我是壹甲壹!
相信大家无论在前端还是后端开发工作中,都接触并使用过 Promise ,本文将带领大家「step-by-step」实现一个符合 Promises/A+ 规范的 Promise,同时探索 Promise 中的一些方法以及第三方扩展如何实现的。
通过阅读本篇文章你可以学习到:
- 手写实现符合规范的 Promise
- 使用
promises-aplus-tests
进行规范测试 - 掌握
Promise.all
,Promise.race
,Promise.resolve
,Promise.reject
等实现原理 - 掌握 Node 中对 Promise 的一些扩展
在正式进入正题之前,为了更好地理解和掌握 Promise ,我们先来介绍一些与 Promise 相关的基础知识。
一、什么是异步
1.1 JS 中为什么存在异步
大家应该都知道,JS 属于单线程语言,所谓单线程,就是一次只能干一件事,其它事情只能在后面乖乖排队等待。
在浏览器中,页面加载过程中存在大量请求,当一个网络请求迟迟没有响应,页面将傻傻等着,不能处理其它事情。
因此,JS 中设计了异步,即发送完网络请求后就可以继续处理其它操作,而网络请求返回的数据,可通过回调函数来接收处理,这样就保证了页面的正常运行。
1.2 异步解决方案
先看下面一段 Node 代码
var fs = require('fs')
fs.readFile('data.json', (err, data) => {
console.log(data.toString())
})
fs.readFile
方法的第二个参数是个函数,函数并不会立即执行,而是等到读取的文件结果出来才执行,这是函数就是回调函数,即 callback
1.3 回调地狱
处理多个异步请求,并且一个一个嵌套时,就容易产生回调地狱。看下面一段 Node 代码
const fs = require('fs')
fs.readFile('data1.json', (err, data1) => {
fs.readFile('data2.json', (err, data2) => {
fs.readFile('data3.json', (err, data3) => {
fs.readFile('data4.json', (err, data4) => {
console.log(data4.toString())
})
})
})
})
使用 Promise 改写
const fs = require('fs')
const readFilePromise = (file) => {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) {
reject(err)
}
resolve(data)
})
})
}
readFilePromise('data1.json')
.then(data1 => {
return readFilePromise('data2.json')
}).then(data2 => {
return readFilePromise('data3.json')
}).then(data3 => {
return readFilePromise('data4.json')
}).then(data4 => {
console.log(data4.toString())
}).catch(err => {
console.log(err)
})
“「思考题」:Promise 真的取代 callback 了嘛?
Promise 只是对于异步操作代码的可读性的一种变化,没有改变 JS 中异步执行的本质,也无法取代 callback 在 JS 中的存在。同时,在 Promise 中,也存在着 callback 的使用,实例的 then() 的参数分别是执行成功、失败的函数,也就是 callback 回调函数。
二、Promise 的实现
本篇文章对应的项目地址: github.com/Yangjia23..…
2.1 基本实现
2.1.1 executor 执行器
首先,Promise 是个类,需要使用 new 来创建实例
new Promise((resolve, reject) => {})
传入的参数是个函数,被称为executor
执行器,默认会立即执行executor
执行时会传入两个参数resolve, reject
,分别是执行成功函数、执行失败函数resolve, reject
两个执行函数不属于 Promise 类上的静态属性,也不是实例上的方法,而是一个普通函数
class Promise {
constructor (executor) {
// 成功
const resolve = () => {}
// 失败
const reject = () => {}
// 立即执行
executor(resolve, reject)
}
}
2.1.2 三种状态
关于 Promise 状态
promise 有三种状态:等待 (pending)、已成功 (fulfilled)、已失败(rejected),默认状态为
pending
promise 的状态只能从
pending
转换成fulfilled
或rejected
两种状态变化
了解promise状态更多内容,请查看Promises/A+规范: promise-states
以 readFilePromise 为例
- 读取文件成功时,会调用
resolve
函数,传入读取的内容,表示执行成功,此时的状态应是fulfilled
成功态 - 读取文件失败,会调用
reject
函数,传入失败的原因,表示执行失败,此时的状态应是fulfilled
失败态 - 读取的文件内容或失败的原因需要保存,分别使用
value
和reason
存储
const ENUM = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
class Promise {
constructor (executor) {
this.status = ENUM.PENDING // 默认状态
this.value = undefined // 保存执行成功的值
this.reason = undefined // 保存执行失败的原因
// 成功
const resolve = (value) => {
if (this.status === ENUM.PENDING) {
this.status = ENUM.FULFILLED
this.value = value
}
}
// 失败
const reject = (reason) => {
if (this.status === ENUM.PENDING) {
this.status = ENUM.REJECTED
this.reason = reason
}
}
// 立即执行
executor(resolve, reject)
}
}
2.1.3 异常捕获
由于 executor
执行器是由用户传入的,在执行过程中可能出现错误,此时需要使用 try...catch...
进行异常捕获,当发生错误后,直接调用 reject 抛出错误
class Promise {
constructor (executor) {
// ....
// 异常捕获
try{
// 立即执行
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
}
2.1.4 实现 then 方法
调用 new Promise()
返回的实例上有个 then
方法,then
方法需要用户提供两个参数,分别是执行成功后对应的成功回调 onFulfilled
和执行失败后对应的失败回调 onRejected
- 当状态变成 fulfilled,会调用
onFulfilled
方法,并传入成功的值 this.value - 当状态变成 rejected,会调用
onRejected
方法,并传入失败的原因 this.reason
class Promise {
constructor(executor) {
// ...
}
then(onFulfilled, onRejected) {
if (this.status == ENUM.FULFILLED) {
onFulfilled(this.value)
}
if (this.status == ENUM.REJECTED) {
onRejected(this.reason)
}
}
}
当 executor
中执行的是异步操作时,执行 then
方法时状态还是 pending
“异步操作例如
setTimeout
属于宏任务,而promise.then
属于微任务, 微任务先于宏任务执行,所以then
方法执行时,promise
的状态还是pending
同时实例promise可以多次调用 then 方法,所以,需要将所有 then
方法中的回调函数搜集保存好,当异步操作完成后,再执行保存的回调函数(基于发布订阅模式)
const promise = new Promise((resolve, reject) => {
setTimeout(() => {}, 2000)
})
promise.then(data => {//...}, err => {})
promise.then(data => {//...}, err => {})
所以,接下来需要实现的是
- 创建两个队列
onResolvedCallbacks
和onRejectedCallbacks
,分别存放 then 方法中对应的成功回调和失败回调 - 当异步操作成功时,调用
resolve
函数时,执行onResolvedCallbacks
队列中每个成功回调 - 当异步操作失败时,调用
reject
函数时,执行onRejectedCallbacks
队列中每个失败回调
class Promise {
constructor(executor) {
this.status = ENUM.PENDING
this.value = undefined
this.reason = undefined
this.onResolvedCallbacks = [] // 成功队列
this.onRejectedCallbacks = [] // 失败队列
// 成功回调
const resolve = (value) => {
if (this.status === ENUM.PENDING) {
this.status = ENUM.FULFILLED
this.value = value
this.onResolvedCallbacks.forEach(cb => cb()) // 相对于发布
}
}
// 失败回调
const reject = (reason) => {
if (this.status === ENUM.PENDING) {
this.status = ENUM.REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(cb => cb())
}
}
// 立即执行
executor(resolve, reject)
}
then(onFulfilled, onRejected) {
// ...
if (this.status === ENUM.PENDING) {
// 相对于订阅
this.onResolvedCallbacks.push(() => {
// todo...
onFulfilled(this.value)
});
this.onRejectedCallbacks.push(() => {
// todo...
onRejected(this.reason);
})
}
}
}
注意:在 then
方法中,并没有往队列中直接插入回调函数, 而是使用函数包装后再 push
,是为了方便后续扩展 ( eg:获取并处理 onFulfilled()
的返回值)
到现在为止,实现了基础版 Promise , 但看着和之前的 callback 只是写法上不同,并没有体现出 Promise 的优势,接下来,继续探索 Promise 中的高级特性
2.2 高级特性
2.2.1 实现 then 链式调用
对于实例上的 then(onFulfilled, onRejected)
方法,其参数为成功、失败两个回调函数。总结出以下几个使用场景
- 如果两个方法执行返回值是普通值,则会被传递到外层的下一个
then
中 - 如果两个方法执行过程中抛出异常,则会在下一个
then
的失败回调中捕获异常 - 当两个方法执行返回值是 promise, 那么会用该 promise 的状态作为结果 ( promise 的状态是“成功”,则会调用下一个
then
的成功回调;状态为“失败”则会调用下一个then
的失败回调) - 错误处理,当发生错误时(
then
中抛错或返回一个失败的 promise ),该错误会被最近的一个失败回调捕获,当该失败回调执行后,可以继续调用then
方法
在 Promise 中,promise.then 链式调用的实现原理是通过返回一个新的 promise 来实现的
“「思考题」为什么返回新的 promise, 而不是使用原来的 promise?
因为 promise 的状态一旦"成功"或"失败"了,就不能再改变了,所以只能返回新的 promise,这样才可以继续调用下一个then
中的成功/失败回调
接下来,需要实现以下几点
- 调用
then
方法,创建一个新的 promise, 最后将这个新 promise 返回 - 需要获取
then
方法中onFulfilled
、onRejected
回调函数的返回值,通过新的promise
传递到下一个then
方法中
class Promise {
//....
then(onFulfilled, onRejected) {
// 新的 promise
let promise2 = new Promise((resolve, reject) => {})
if (this.status == ENUM.FULFILLED) {
let x = onFulfilled(this.value)
}
if (this.status == ENUM.REJECTED) {
let x = onRejected(this.reason)
}
if (this.status === ENUM.PENDING) {
this.onResolvedCallbacks.push(() => {
let x = onFulfilled(this.value)
});
this.onRejectedCallbacks.push(() => {
let x = onRejected(this.reason);
})
}
return promise2
}
}
现在,需要将回调函数执行的返回值 x 传递到下一个 then
方法中,是传递到下一个 then
方法中的成功回调,还是失败回调?需要根据 x 的值来判断。
- 若 x 是普通值,将通过 promise2 中的
resolve
传递给成功回调; - 若 x 是个 Error,则通过 promise2 中的
reject
传递给失败回调; - 当然 x 也又有可能是个 promise 实例,所以都需要考虑到。
因为需要使用 promise2 中的 resolve
, reject
传递 x (两个方法在外部无法获取到), 同时new Promise(executor)
时,executor
是立即执行,所以,将整个 then
方法中的逻辑放到 executor
函数中执行,就可以访问到 resolve
, reject
方法了
class Promise {
//....
then(onFulfilled, onRejected) {
// 新的 promise
let promise2 = new Promise((resolve, reject) => {
if (this.status == ENUM.FULFILLED) {
// onFulfilled 执行可能报错,使用 try...catch...捕获
try{
let x = onFulfilled(this.value)
resolve(x)
} catch (e){
reject(e)
}
}
// ...
})
return promise2
}
}
因为返回值 x 存在多种情况, 所以将判断逻辑抽离到外部函数 resolvePromise 中
class Promise {
//....
then(onFulfilled, onRejected) {
// 新的 promise
let promise2 = new Promise((resolve, reject) => {
if (this.status == ENUM.FULFILLED) {
try{
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (e){
reject(e)
}
}
// ...
})
return promise2
}
}
const resolvePromise = (x, promise2, resolve, reject) => {
}
相信仔细的小伙伴已经发现,在 new Promise
还没结束就访问 promise2 肯定会报错。只需将 resolvePromise
变成异步代码执行就可以访问到 promise2
//...
if (this.status == ENUM.FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(x, promise2, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
接下来,需要实现 resolvePromise 方法了
2.2.2 resolvePromise 方法
resolvePromise 方法主要是用来解析 x 是否是promise, 按照 Promises/A+规范: the-promise-resolution-procedure 规定,分成以下几步
函数参数 resolvePromise(x, promise2, resolve, reject)
- (1) 若 x 和 promise2 引用的是同一个对象,则直接报错。(示例代码如下)
let promise = new Promise((resolve, reject) => {})
let promise2 = promise.then(() => {
return promise2 // x 代表了then中函数的返回值,也就是 promise2
})
promise2.then(() => {}, err=> {
console.log('err:', err)
})
// err: TypeError: Chaining cycle detected for promise #<Promise> (循环引用了)
- (2) 若 x 是一个普通值,直接通过
resolve
返回 - (3) 若 x 是一个对象或者函数,判断 x 是否存在
then
方法,当存在then
方法,表明 x 就是一个 promise,此时执行then
方法 - (4) 执行
then
方法时,有一个成功回调和一个失败回调,执行成功走成功回调,并传入成功结果 y;执行失败走失败回调,并传入失败原因 e, 使用reject
返回 - (5) 执行成功返回值 y 可能还是个 promise, 继续递归解析 y 的值
- (6)
then
的回调函数只能执行一次,要么成功,要么失败(设置标识符 called) - (7) 当 x 不存在
then
方法时,表明 x 是普通的对象,直接通过resolve
返回
const resolvePromise = (x, promise2, resolve, reject) => {
// (1)
if (x === promise2) {
reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise>`))
}
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let called = false // (6)
try {
const then = x.then
// (3)
if (typeof then === 'function') {
// (4)
then.call(x, y => {
// (5) y 可能是个 promise
if (called) return
called = true
resolvePromise(y, promise2, resolve, reject)
}, e => {
if (called) return
called = true
reject(e)
})
} else {
// (7)
resolve(x)
}
} catch (e) {
// then 执行过程出错,也不能继续向下执行
if (called) return
called = true
reject(e)
}
} else {
// (2)
resolve(x)
}
}
现在 resolvePromise 方法已经基本实现,其中还有以下几点需要说明
- 为啥需要判断 x 为函数?
因为 resolvePromise 需要兼容其他人写的 promise , 别人的 promise 可能就是一个函数
- 执行
const then = x.then
为啥需要使用try...catch...
捕获异常 ?
因为可以使用 Object.defineProperties
或 Proxy
改写 x.then 的返回值
- 执行
then
方法,为啥使用call
, 而不是直接执行x.then()
?
可以复用上次取出来的then
方法,避免二次调用 x.then()
2.2.3 值穿透
new Promise((resolve, reject) => {
resolve(123)
}).then().then().then(data => {
console.log('success:', data)
})
// success: 123
上面代码中的 123 是如何直接穿透到最后一个 then
方法中的呢?
Promises/A+规范: onFulfilled, onRejected are optional arguments , 规定 then
方法中的 onFulfilled
, onRejected
是可选参数,所以我们需要提供一个默认值
class Promise {
// ...
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected: e => {throw e}
// ...
}
}
通过给 onFulfilled
, onRejected
设置默认值就可以实现值穿透。至此,已经实现 Promises/A+ 中规范的功能,可以对代码进行规范测试了
2.3 规范测试
规范测试,首先需要安装 promises-aplus-tests npm 包,同时需要在导出 Promise
前增加下面测试代码
class Promise {
// ...
}
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = Promise;
安装依赖
npm install promises-aplus-tests -D
同时在 package.json 增加
"scripts": {
"test": "promises-aplus-tests ./index.js"
},
最后,运行 npm run test
就可以进行测试了,测试结果如下
2.4 其它方法和属性
下面介绍的内容,并不是 Promises/A+ 中的规范,但我们也可以继续探索
2.4.1 catch 方法
实例上的 catch
方法用来捕获执行过程中产生的错误,同时返回值为 promise, 参数为一个失败回调函数,相对于执行 then(null, onRejected)
class Promise{
// ...
catch (onErrorCallback) {
return this.then(null, onErrorCallback)
}
}
2.4.2 finally 方法
finally
的参数是一个回调函数,无论 promise 是执行成功,还是失败,该回调函数都会执行。
应用场景有:页面异步请求数据,无论数据请求成功还是失败,在 finally 回调函数中都关闭 loading。
同时,finally
方法有以下特点
- 值穿透。可以将前面 promise 的值传递到下一个
then
方法中,或者将错误传递到下一个catch
方法中 - 等待执行。当
finally
回调函数返回一个新的 promise,finally
会等待该 promise 执行结束后才处理传值 - 若该 promise 执行成功,
finally
方法将不予理会执行结果,还是将上一个的结果传递到下一个then
中 - 若新的 promise 执行失败报错,
finally
方法会将错误原因传递到下一个catch
方法
下面是具体代码演示
// (1) 值穿透, 请注意 finally 的回调函数是不存在参数的
Promise.resolve(100).finally((data) => {
console.log('finally: ', data)
}).then(data => {
console.log('success: ', data)
}).catch(err => {
console.log('error', err)
})
// finally: undefined
// success: 100
// (2) 等待执行
// 返回一个执行成功的 promise, 但向下传递但还是上一次执行结果
Promise.resolve(100).finally(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(200)
}, 1000)
})
}).then(data => {
console.log('success: ', data) // success: 100
}).catch(err => {
console.log('error', err)
})
// 当 promise 执行失败,则将该 promise 执行结果向下传递
Promise.reject(100).finally(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(200)
}, 1000)
})
}).then(data => {
console.log('success: ', data)
}).catch(err => {
console.log('error', err) // error 200
})
在掌握了 finally
的用法后,继续探索如何实现它?
class Promise{
finally (callback) {
return this.then(value => {
return Promise.resolve(callback()).then(() => value)
}, err => {
return Promise.resolve(callback()).then(() => {throw err})
})
}
}
2.4.3 静态方法
静态方法是那通过 Promise 来调用,而不是通过实例 promise 来调用的方法
- Promise.resolve()、Promise.reject() 返回值:一个成功状态的 promise 、一个失败状态的 promise
class Promise{
// ...
// 成功状态
static resolve(value){
return new Promise((resolve, reject) => {
resolve(value)
})
}
// 失败状态
static reject(reason){
return new Promise((resolve, reject) => {
reject(reason)
})
}
}
假设执行成功返回值 value
是个 promise,Promise.resolve() 会对该 value 递归解析,直到该 promise 执行结束才会向下执行
class Promise{
constructor() {
//...
const resolve = (value) => {
if (value instanceof Promise) {
// 递归解析, 直到 value 为普通值
value.then(resolve, reject)
}
// ...
}
const reject = (err) => {
// ...
}
//...
}
}
现在,执行下面代码,就可以正常获取数据了
Promise.resolve(new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello')
}, 2000)
})).then(data => {
console.log(data) // hello
})
- Promise.all()
解决并发问题,多个异步并发并获取最终的结果。
参数是一个 promise数组,当数组中每一项都执行成功,结果就是成功,反之,有一个失败,结果就是失败。
class Promise {
static all(arrList) {
if (!Array.isArray(arrList)) {
const type = typeof arrList;
return new TypeError(`TypeError: ${type} ${arrList} is not iterable`)
}
return new Promise((resolve, reject) => {
const backArr = []
const count = 0
const processResultByKey = (value, index) => {
backArr[index] = value
if (++count === arrList.length) {
resolve(backArr)
}
}
for (let i = 0; i < arrList.length; i++) {
const item = arrList[i];
if (item && item.then === 'function') {
item.then((value) => {
processResultByKey(value, i)
}, reject)
} else {
processResultByKey(item, i)
}
}
})
}
}
⚠️注意:在 all
方法中,是通过 ++count === arrList.length
(count 为计数器) 来判断是否全部执行完成,而不是使用 index === arrlist.length - 1
来判断,具体原因如下
// p1 为 promise 实例
Promise.all([1,2, p1, 4]).then(data => {})
// 当执行数组最后一项时,index === arrlist.length - 1 表达式成立,
// 就会执行 resolve 返回执行结果,
// 但此时的 p1 可能还没执行结束,所以使用计数器来判断
- Promise.race()
跟 all
方法不同的是,Promise.race 采用最先成功或最先失败的作为执行结果
class Promise {
static race(arrList) {
return new Promise((resolve, reject) => {
for (let i = 0; i < arrList.length; i++) {
const value = arrList[i];
if (value && value.then === 'function') {
value.then(resolve, reject)
} else {
resolve(value)
}
}
})
}
}
Promise.race 的主要应用场景如下
- (基础)多个请求采取最快的 (eg: 小飞机的多个代理线路,哪条线路的响应速度最快,就使用哪条)
- (高级)封装中断方法,中断
promise
的执行 (异步请求设置超时时间,当超时后,异步请求就会被迫失败)
原生的 promise 上并没有 abort
(停止、中断) 方法,假设使用场景如下
const p1 = new Promise((resolve, reject) => {
setTimeout(() => { // 模拟异步请求,5s 后返回
resolve('hello')
}, 5000)
})
const newP = wrap(p1)
setTimeout(() => { // 设置超时时间,超时后,调用 newP.abort
newP.abort('请求超时了')
}, 4000)
newP.then(data => {}).catch(err => {})
newP1 是一个具有 abort
方法的 promise, 超时后就调用 newP.abort()
。
现在需要实现 wrap
封装方法,传入一个普通 promise 实例,返回一个具有 abort
方法的 promise 实例
const wrap = (promise) => {
let abort
let newPromise = new Promise((resolve, reject) => {
abort = reject
})
let p = Promise.race([promise, newPromise])
p.abort = abort
return p
}
wrap
方法就是利用 Promise.race 采用最快的作为执行结果这一特性,来看 promise, newPromise
哪个最先执行,而 newPromise
的执行,是通过外部调用 abort 来实现的
三、Promise 的扩展
“⚠️注意:以下对 Promise 的扩展仅适用于 Node 环境
3.1 promisify
功能:把 node 中的一个 api 转换成promise的写法, 以 fs.readFile
读取文件为例
常规写法
const fs = require('fs)
fs.readFile('./name.json', (err, data) => {})
缺点:回调地狱嵌套
改成 promisify 链式调用写法
const util = require('util')
const read = util.promisify(fs.readFile)
read('./name.json').then(data => console.log(data))
特点:promisify 方法特点如下
- 返回一个函数,函数执行后才返回 promise
const promisify = fn => {
return (...args) => {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
}
- 在 promisify 函数中,执行 fn 函数时,可以手动添加了回调函数是因为 node 中大部分的方法的回调都是这种格式
3.2 bluebird
promisify
方法每次只能修改一个方法,而第三方的库 bluebird 中实现了 promisifyAll
方法,可以将某个对象下所有的方法转换成 promise 写法
const fs = require('fs')
const bluebird = require('bluebird'); // 第三方库,需提前安装
const newFs = bluebird.promisifyAll(fs);
newFs.readFileAsync('./name.txt', 'utf-8').then(data => {}).catch(err => {})
promisifyAll() 特点如下
- 函数参数为对象,会将对象上所有的方法,增加一个 Async** **后缀,变成 promise 写法
- 并没有覆盖原方法,只是扩展
const promisifyAll = (target) {
Reflect.ownKeys(target).forEach(key => {
target[`${key}Async`] = promisify(target[key])
})
return target
}
Reflect 对象是 ES 中内置对象,它提供拦截 JavaScript 操作的方法 Reflect | MDN, 此处,也可使用 Object.keys()
。同时,使用了前面的 promisify
来改写方法
3.3 原生 Node 支持
目前,在高版本浏览器中,已经对 api 集成了 promise 的写法,使用如下
const fs = require('fs').promises
fs.readFile('./name.txt', 'utf-8').then(data => {})
正因为原生的支持,导致第三方的一些扩展不再流行
四、参考资源
五、后语
本文使用 mdnice 排版