Hello 大家好!我是壹甲壹!
相信大家无论在前端还是后端开发工作中,都接触并使用过 Promise ,本文将带领大家「step-by-step」实现一个符合 Promises/A+ 规范的 Promise,同时探索 Promise 中的一些方法以及第三方扩展如何实现的。
通过阅读本篇文章你可以学习到:
promises-aplus-tests
进行规范测试Promise.all
,Promise.race
, Promise.resolve
, Promise.reject
等实现原理在正式进入正题之前,为了更好地理解和掌握 Promise ,我们先来介绍一些与 Promise 相关的基础知识。
大家应该都知道,JS 属于单线程语言,所谓单线程,就是一次只能干一件事,其它事情只能在后面乖乖排队等待。
在浏览器中,页面加载过程中存在大量请求,当一个网络请求迟迟没有响应,页面将傻傻等着,不能处理其它事情。
因此,JS 中设计了异步,即发送完网络请求后就可以继续处理其它操作,而网络请求返回的数据,可通过回调函数来接收处理,这样就保证了页面的正常运行。
先看下面一段 Node 代码
var fs = require('fs') fs.readFile('data.json', (err, data) => { console.log(data.toString()) }) 复制代码
fs.readFile
方法的第二个参数是个函数,函数并不会立即执行,而是等到读取的文件结果出来才执行,这是函数就是回调函数,即 callback
处理多个异步请求,并且一个一个嵌套时,就容易产生回调地狱。看下面一段 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 回调函数。
本篇文章对应的项目地址: github.com/Yangjia23..…
首先,Promise 是个类,需要使用 new 来创建实例
new Promise((resolve, reject) => {})
传入的参数是个函数,被称为 executor
执行器,默认会立即执行executor
执行时会传入两个参数 resolve, reject
,分别是执行成功函数、执行失败函数resolve, reject
两个执行函数不属于 Promise 类上的静态属性,也不是实例上的方法,而是一个普通函数class Promise { constructor (executor) { // 成功 const resolve = () => {} // 失败 const reject = () => {} // 立即执行 executor(resolve, reject) } } 复制代码
关于 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) } } 复制代码
由于 executor
执行器是由用户传入的,在执行过程中可能出现错误,此时需要使用 try...catch...
进行异常捕获,当发生错误后,直接调用 reject 抛出错误
class Promise { constructor (executor) { // .... // 异常捕获 try{ // 立即执行 executor(resolve, reject) } catch (e) { reject(e) } } } 复制代码
调用 new Promise()
返回的实例上有个 then
方法,then
方法需要用户提供两个参数,分别是执行成功后对应的成功回调 onFulfilled
和执行失败后对应的失败回调 onRejected
onFulfilled
方法,并传入成功的值 this.valueonRejected
方法,并传入失败的原因 this.reasonclass 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 中的高级特性
对于实例上的 then(onFulfilled, onRejected)
方法,其参数为成功、失败两个回调函数。总结出以下几个使用场景
then
中then
的失败回调中捕获异常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 的值来判断。
resolve
传递给成功回调;reject
传递给失败回调;因为需要使用 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 方法了
resolvePromise 方法主要是用来解析 x 是否是promise, 按照 Promises/A+规范: the-promise-resolution-procedure 规定,分成以下几步
函数参数 resolvePromise(x, promise2, resolve, reject)
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> (循环引用了) 复制代码
resolve
返回then
方法,当存在 then
方法,表明 x 就是一个 promise,此时执行 then
方法then
方法时,有一个成功回调和一个失败回调,执行成功走成功回调,并传入成功结果 y;执行失败走失败回调,并传入失败原因 e, 使用 reject
返回then
的回调函数只能执行一次,要么成功,要么失败(设置标识符 called)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 方法已经基本实现,其中还有以下几点需要说明
因为 resolvePromise 需要兼容其他人写的 promise , 别人的 promise 可能就是一个函数
const then = x.then
为啥需要使用 try...catch...
捕获异常 ?因为可以使用 Object.defineProperties
或 Proxy
改写 x.then 的返回值
then
方法,为啥使用 call
, 而不是直接执行 x.then()
?可以复用上次取出来的then
方法,避免二次调用 x.then()
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+ 中规范的功能,可以对代码进行规范测试了
规范测试,首先需要安装 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
就可以进行测试了,测试结果如下
下面介绍的内容,并不是 Promises/A+ 中的规范,但我们也可以继续探索
实例上的 catch
方法用来捕获执行过程中产生的错误,同时返回值为 promise, 参数为一个失败回调函数,相对于执行 then(null, onRejected)
class Promise{ // ... catch (onErrorCallback) { return this.then(null, onErrorCallback) } } 复制代码
finally
的参数是一个回调函数,无论 promise 是执行成功,还是失败,该回调函数都会执行。
应用场景有:页面异步请求数据,无论数据请求成功还是失败,在 finally 回调函数中都关闭 loading。
同时,finally
方法有以下特点
then
方法中,或者将错误传递到下一个 catch
方法中finally
回调函数返回一个新的 promise, finally
会等待该 promise 执行结束后才处理传值finally
方法将不予理会执行结果,还是将上一个的结果传递到下一个 then
中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}) }) } } 复制代码
静态方法是那通过 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数组,当数组中每一项都执行成功,结果就是成功,反之,有一个失败,结果就是失败。
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 可能还没执行结束,所以使用计数器来判断 复制代码
跟 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 的主要应用场景如下
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 的扩展仅适用于 Node 环境
功能:把 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 方法特点如下
const promisify = fn => { return (...args) => { return new Promise((resolve, reject) => { fn(...args, (err, data) => { if (err) reject(err) resolve(data) }) }) } } 复制代码
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() 特点如下
const promisifyAll = (target) { Reflect.ownKeys(target).forEach(key => { target[`${key}Async`] = promisify(target[key]) }) return target } 复制代码
Reflect 对象是 ES 中内置对象,它提供拦截 JavaScript 操作的方法 Reflect | MDN, 此处,也可使用 Object.keys()
。同时,使用了前面的 promisify
来改写方法
目前,在高版本浏览器中,已经对 api 集成了 promise 的写法,使用如下
const fs = require('fs').promises fs.readFile('./name.txt', 'utf-8').then(data => {}) 复制代码
正因为原生的支持,导致第三方的一些扩展不再流行
本文使用 mdnice 排版