ECMAScript6中promise是划时代的API,他的出现解决了一直困扰前端开发者的异步问题,从此面对异步回调,我们有了更好的武器
前言
面对天天都能见面的promise,不知道你是否有以下的一些问题
- 我们new Promise((resolve,reject) =>{}),resolve,reject都是哪来的?
- 为什么resolve之后才会执行then或者catch?
- 为什么可以链式.then,并且还都会按同步进行?
- 为什么执行promise.resolve(),后面的函数就支持promise了?
- promise.all是如何实现的?
- 是否被面试题中的promise题目迷惑的头晕目眩?
让我们了解Promise的实现原理,所有问题答案自然浮出水面~
promise在潜移默化之间帮助我们简化了复杂的异步代码,降低逻辑难度,说promise是划时代的异步解决方案也不为过,他很好的提现了开放封闭原则,解决耦合性过高的问题
说一个小知识,es6发布之前类似prmise的异步方案已经存在,在jquery的ajax中已经应用了类似的技术方案的jQuery.deferred(),感兴趣的同学可以去了解一下
1 2 3
| $.ajax("test.html") .done(function(){ alert("哈哈,成功了!"); }) .fail(function(){ alert("出错啦!"); });
|
简化版Primise
基础版本的实现虽然简单,但是解释了很多问题
建议将代码复制到本地,通过断点的方式查看代码的执行流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| const PEDDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected'
class APromise { constructor(executor) { this.status = PEDDING this.value = undefined this.reason = undefined this.onFulfilledCallbacks = [] this.onRejectCallbacks = []
const resolve = (data) => { if (this.status == PEDDING) { this.status = FULFILLED this.value = data } this.onFulfilledCallbacks.map((e) => e()) }
const reject = (err) => { if (this.status == PEDDING) { this.status = REJECTED this.reason = err } this.onRejectCallbacks.map((e) => e()) } try { executor(resolve, reject) } catch (error) { rejected(error) } }
then(onFulfilled, onRejected) { if (this.status == FULFILLED) { onFulfilled(this.value) } if (this.status == REJECTED) { onRejected(this.reason) } if (this.status == PEDDING) { this.onFulfilledCallbacks.push(() => { onFulfilled(this.value) }) this.onRejectCallbacks.push(() => { onRejected(this.reason) }) } } }
new APromise((resolve, reject) => { console.log('开始回调') setTimeout(() => { console.log('执行回调') resolve(11111) }, 1000) }).then( (value) => { console.log('成功回调', value) }, (err) => { console.log('失败回调', err) } )
|
代码运行流程
- 初始化APromise,开始执行class中的constructor
- 在constructor中初始化当前promise的一些状态值以及resolve,reject函数
- 最后将resolve函数与reject函数以参数的形式给promise的回调函数,同时执行函数,打印开始回调
- 运行setTimeout,并且开始解析then函数
- 如果是成功,或者失败状态,直接执行回调,如果是pedding状态,则存储成功与失败回调函数
- 1s之后,setTimeout执行完毕,resolve执行触发constructor中的resolve
- resolve函数中执行之前初始化.then时候存储的回调函数,打印 成功回调,11111或者失败回调
逻辑流程图
基础版本的实现,不支持链式调用,不支持then穿透,不支持catch,只实现了最基础的逻辑
我们在这里解答一下前言中提出的问题
我们new Promise((resolve,reject) =>{}),resolve,reject都是哪来的?
答:new的时候执行Promise中的constructor,声明了resolve与reject,并且在执行Promise回调函数的时候将参数传入到函数中
为什么resolve之后才会执行then或者catch?
答:因为在初始化阶段,pedding状态下,我们存储了当前Promise的成功与失败回调,当执行resolve的时候,当前Promise的状态发生变化,开始执行之前存储的回调函数,如果不是padding,则立即执行回调函数
后面的问题我们暂时还无法解释,但是随着我们进一步的实现,答案都会浮出水面
正式版(链式回调,then值穿透,.catch 等)
链式回调
我们一般写promise都会写多个.then,在多个.then中我们将异步代码变成同步代码块,但是我们基础版本的promise中无法显示链式调用,因为执行.then之后函数没有任何返回值,自然不会存在.then方法,在这个思路上,我们对promise的.then解析过程进行改写,尝试让其支持链式调用
- 每次.then中都需要返回一个promise来触发下一个.then
- 对then回调函数的各种情况需要进行判断,例如。then中返回的是一个string还是返回了一个promise,如果是则需要增加链式回调触发父级的resolve
- then函数执行需要通过settimeout进行包裹,让其加入宏任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
|
const resolvePromise = (promise, x, resolve, reject) => { if (promise === x) { return reject(new TypeError('检测到promise的循环调用')) } let called = false if ((typeof x === 'object' && x !== null) || typeof x === 'function') { try { const then = x.then if (typeof then === 'function') { then.call( x, (y) => { if (called) return called = true resolvePromise(promise, y, resolve, reject) }, (r) => { if (called) return called = true reject(r) } ) } else { resolve(x) } } catch (err) { if (called) return called = true reject(err) } } else { resolve(x) } }
then(onFulfilled, onRejected) { let apromise = new APromise((resolve, reject) => { if (this.status === FULFILLED) { setTimeout(() => { try { const x = onFulfilled(this.value) resolvePromise(apromise, x, resolve, reject) } catch (err) { reject(err) } }, 0) } if (this.status === REJECTED) { setTimeout(() => { try { const x = onRejected(this.reason) resolvePromise(apromise, x, resolve, reject) } catch (err) { reject(err) } }, 0) } if (this.status === PEDDING) { this.onFulfilledCallbacks.push(() => { setTimeout(() => { try { const x = onFulfilled(this.value) resolvePromise(apromise, x, resolve, reject) } catch (err) { reject(err) } }, 0) }) this.onRejectCallbacks.push(() => { setTimeout(() => { try { const x = onRejected(this.reason) resolvePromise(apromise, x, resolve, reject) } catch (err) { reject(err) } }, 0) }) } }) return apromise }
|
经过上面的内部promise处理,函数的运行逻辑发生了很大的变化
我们直观看到的逻辑是
实际的运行逻辑是每次.then中都会再次创建一个Promise,以便于下次进行调用,并且对.then的回调函数进行处理,区分.then中返回了Promise对象还是普通对象,这样的思路实现了.then链式调用
当then中存在return promise的情况,逻辑会发生一些变化,这些主要体现在resolvePromise函数中
then值穿透
首先查看一种场景
1 2 3 4 5 6 7 8 9 10
| new APromise((resolve, reject) => { resolve(11111); }) .then() .then() .then(data => { console.log('成功回调', data); }, err => { console.log('失败回调', err); })
|
这里我们就会发现,then的回调函数都不存在,自然无法将resolve的值传递到最下面的.then中,所以这里我们需要对这种情况做一些处理
1 2 3 4 5 6 7 8 9
| then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : (data) => data onRejected = typeof onRejected == 'function' ? onRejected : (err) => { throw err } let apromise = new APromise((resolve, reject) => { }) return apromise }
|
当我们对then值中的回调函数进行处理后,实际运行的函数变成
1 2 3 4 5 6 7 8 9 10
| new APromise((resolve, reject) => { resolve(11111); }) .then((data) => data) .then((data) => data) .then(data => { console.log('成功回调', data); }, err => { console.log('失败回调', err); })
|
这样便实现了then穿透问题
.catch
目前我们错误回调在.then的第二个参数中,并不支持.catch的写法,我们可以在原型链上面增加catch方法
catch其实也是对.then方法的封装,只不过不存在成功回调,只有失败回调
1 2 3
| APromise.prototype.catch = function (errCallback) { return this.then(null, errCallback) }
|
.finally
由于finally无法预知promise的最终状态,所以finally的回调函数中不接受任何参数,他仅用于无论最终结果都要执行的情况
需要注意的一点是如果finally中存在Promise,这需要等待promise执行完毕
1 2 3 4 5 6 7 8 9 10 11 12
| APromise.prototype.finally = function (callBack) { return this.then( (data) => { return APromise.resolve(callBack()).then(() => data) }, (err) => { return APromise.reject(callBack()).then(() => { throw err }) } ) }
|
关于finally的小知识
1 2 3
| Promise.resolve(2).then(() => {}, () => {})
Promise.resolve(2).finally(() => {}, () => {})
|
Promise.resolve()
调用Promise.resolve()就会返回一个真实的promise,并且直接返回成功回调
1 2 3 4 5
| APromise.resolve = function (data) { return new APromise((resolve, reject) => { resolve(data) }) }
|
Promise.reject()
调用Promise.resolve()就会返回一个真实的promise,并且直接返回失败回调
1 2 3 4 5
| APromise.reject = function (data) { return new APromise((resolve, reject) => { reject(data) }) }
|
Promise.race()
当调用race方法的时候,必须传入一个数组,数组中可以存在不同类型以及函数类型,在初始化过程中会再次创建一个promise,当数组中的某个promise对象最先执行的时候,触发自身的.then在回调函数中触发了race本身的resolve,后面执行完毕之后,因为race的状态已经发生了变化,自然无法再执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
APromise.race = function (promiseList) { if (!Array.isArray(promiseList)) { throw new TypeError('必须传递数组') } return new APromise((resolve, reject) => { promiseList.forEach((item) => { if (item && typeof item.then == 'function') { item.then(resolve, reject) } else { resolve(item) } }) }) }
let p1 = new APromise((resolve, reject) => { setTimeout(() => { resolve('ok1') }, 3000) })
let p2 = new APromise((resolve, reject) => { setTimeout(() => { reject('ok2') }, 2000) })
APromise.race([1, p1, p2]).then( (data) => { console.log('success1', data) }, (err) => { console.log('error1', err) } )
|
Promise.all()
all的实现逻辑非常简单,all的时候创建一个promise,内部记录当前传入的列表状态成功的单个数据,当所有的then数据都成功,调用自己的resolve,当有一个失败的时候,调用自己的reject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
APromise.all = function (promiseList) { if (!Array.isArray(promiseList)) { throw new TypeError('必须是数组') } return new APromise((resolve, reject) => { const resulteArr = [] const len = promiseList.length let currentIndex = 0 const getResult = (key, val) => { resulteArr[key] = val if (++currentIndex == len) { resolve(resulteArr) } } for (let i = 0; i < len; i++) { const val = promiseList[i] if (val && typeof val.then === 'function') { val.then((data) => { getResult(i, data) }, reject) } else { getResult(i, val) } } }) }
let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('ok1'); }, 1000); })
let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('ok2'); }, 2000); })
Promise.all([1,2,3,p1,p2]).then(data => { console.log('success', data); }, err => { console.log('error', err); })
|
Promise.any()
实现方法与all非常相似,是all完全相反的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
APromise.any = function (promiseList) { if (!Array.isArray(promiseList)) { throw new TypeError('必须是数组') } return new APromise((resolve, reject) => { const resultArr = [] const len = promiseList.length let currentIndex = 0 const getResult = (index, err) => { resultArr[index] = err if (++currentIndex == len) { reject(resultArr) } } promiseList.map((res, index) => { if (res && typeof res.then == 'function') { res.then(resolve, (err) => { getResult(index, err) }) } else { resolve(res) } }) }) }
let p3 = new APromise((resolve, reject) => { setTimeout(() => { reject('err3') }, 1000) })
let p4 = new APromise((resolve, reject) => { setTimeout(() => { reject('err4') }, 2000) })
APromise.any([p3, p4]).then( (data) => { console.log('success', data) }, (err) => { console.log('error', err) } )
|
Promise.allSettled()
allSettled是ES2020加入的工具方法,一句话总结:他是永远都不会失败处理的promise.all
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
APromise.allSettled = function (promiseList) { if (!Array.isArray(promiseList)) { throw new TypeError('必须是数组') } return new APromise((resolve, reject) => { const resultArr = [] const len = promiseList.length let currentIndex = 0 const getResult = (index, data, status) => { if (status == FULFILLED) { resultArr.push({ status: status, value: data, }) } if (status == REJECTED) { resultArr.push({ status: status, reason: data, }) } if (++currentIndex == len) { resolve(resultArr) } } promiseList.map((res, index) => { if (res && typeof res.then == 'function') { res.then( (data) => { getResult(index, data, FULFILLED) }, (err) => { getResult(index, err, REJECTED) } ) } else { getResult(index, res, FULFILLED) } }) }) }
let p1 = new APromise((resolve, reject) => { setTimeout(() => { resolve('ok1') }, 3000) }) let p2 = new APromise((resolve, reject) => { setTimeout(() => { resolve('ok2') }, 2000) })
let p3 = new APromise((resolve, reject) => { setTimeout(() => { reject('err3') }, 1000) })
let p4 = new APromise((resolve, reject) => { setTimeout(() => { reject('err4') }, 2000) })
APromise.allSettled([1, 2, 3, p1, p2, p3, p4]).then((res) => { console.log('success', res) })
|
测试函数
首先需要安装测试脚本 npm install -g promises-aplus-tests
测试命令 promises-aplus-tests xxxx.js
测试文件末尾需要加入如下代码
不存在错误则为符合promiseA+标准
1 2 3 4 5 6 7 8 9 10
| APromise.defer = APromise.deferred = function () { let dfd = {} dfd.promise = new APromise((resolve, reject) => { dfd.resolve = resolve dfd.reject = reject }) return dfd }
module.exports = APromise
|
源码地址
github-promise
可以通过chrome DevTool或者Vscode Debug的方式,加上断点,查看代码运行流程,便于理解promise运行逻辑
参考链接
重学Promise,基于A+规范实现它,感谢掘金@关er的promise解读文章,大大降低了深入promise的门槛
PromiseA+规范
MDN-Promise
45道Promise面试题