保姆级手写promise以及promise常用得方法
最近迷上了promise,觉得还挺有意思,看了大佬的一篇知乎文章 面试官:“你能手写一个 Promise 吗” ,写的很详细,超级赞,看完了来着做个笔记,牢固一下理解和印象。
1. 常见 Promise 面试题
这个模块是搬运 面试官:“你能手写一个 Promise 吗” 文章,不想到时候看还得2个文章切换,所以就把这块搬运过来。
- Promise 解决了什么问题?
- Promise 常用的 API 有哪些?
- 能不能手写一个符合 Promise/A+ 规范的 Promise?
- Promise 有什么缺陷,可以如何解决?
1.1 Promise 出现的原因 & 业界实现
在 Promise 出现以前,在我们处理多个异步请求嵌套时,代码往往是这样的。。。
1 let fs = require('fs') 2 3 fs.readFile('./name.txt','utf8',function(err,data){ 4 fs.readFile(data, 'utf8',function(err,data){ 5 fs.readFile(data,'utf8',function(err,data){ 6 console.log(data); 7 }) 8 }) 9 })
为了获取回调得结果,就必须得这样一层嵌套一层,使得代码变的更加难以阅读和难以维护,这就是传说中臭名昭著的回调地狱,产生回调地狱的原因:
- 嵌套调用,第一个函数的输出往往是第二个函数的输入;
- 处理多个异步请求并发,开发时往往需要同步请求最终的结果。
Promise用比较友好的方式解决以上2个问题,用then链式解决嵌套问题,用Promise.all解决异步请求并发的问题。
2. 开始手搓Promise啦
手搓Promise,不是一蹴而就的,慢慢来,先搞简单的版本1,然后再慢慢升级,向Promise靠拢。
Promise核心列一下,不然要实现什么我们都不知道。
- 它有3个状态,pending、fulfilled、rejected,一经改变不可逆转
- new Promise接受一个executor()执行器,并立即执行
-
executor接受resolve和reject
-
then方法,接受2个参数,成功执行参数1,onFulfilled,参数是成功的值value;失败执行参数2, onRejected,参数是失败的值reason;
- then链式调用,值的穿透
- catch、finally
- all,race,any,allsettled常用方法
2.1 手搓PromiseV1.0.0
第一个版本,我们就希望能让以上示例跑起来,打开对应的注释,就会打印对应的内容。开敲!
我们首先根据上面分析的promise核心和我们想要实现的版本1示例,把大概的架子搭出来了。
然后我们继续往下走,resolve就是成功的,reject就是失败的,所以resolve和reject这2个方法,要做的事情就是改变promise的状态,并把对应状态的值给赋上去。
好的,resolve和reject该做的事情也都做了,接下来是then,then接受2个方法参数,成功的就执行方法1,并把成功的值value作为参数;失败的就执行方法2,并把失败的值reason作为参数。
接下来我们执行一下,看看版本1的示例能不能跑起来。
恭喜恭喜!喜提V1.0.0的手搓promise,代码如下:
1 /* V1.0.0 promise*/ 2 /* promise的3个状态 */ 3 const PENDING = 'PENDING'; 4 const FULFILLED = 'FULFILLED'; 5 const REJECTED = 'REJECTED'; 6 7 class MyPromise { 8 constructor(executor) { 9 this.status = PENDING; // 初始的状态 10 this.value = undefined; // 成功的值 11 this.reason = undefined; // 失败的值 12 let resolve = value => { 13 /* 防止重复执行,表示状态不可逆 */ 14 if (this.status === PENDING) { 15 this.value = value; 16 this.status = FULFILLED; 17 } 18 }; 19 let reject = reason => { 20 /* 防止重复执行,表示状态不可逆 */ 21 if (this.status === PENDING) { 22 this.reason = reason; 23 this.status = REJECTED; 24 } 25 }; 26 /* 立即执行 executor*/ 27 try { 28 executor(resolve, reject); 29 } catch (e) { 30 reject(e); 31 } 32 } 33 then(onFulfilled, onRejected) { 34 if (this.status === FULFILLED) { 35 onFulfilled(this.value); 36 } 37 if (this.status === REJECTED) { 38 onRejected(this.reason); 39 } 40 } 41 } 42 43 new MyPromise((resolve, reject) => { 44 // resolve('成功啦!'); 45 reject('失败啦'); 46 }).then( 47 res => { 48 console.log('成功:', res); 49 }, 50 err => { 51 console.log('失败:', err); 52 } 53 );
2.2 手搓PromiseV1.1.0
我们已经实现了一个基础版的 Promise,但是还不要高兴的太早噢,这里我们只处理了同步操作的 promise。如果在 executor()
中传入一个异步操作的话呢,我们试一下:
我们发现,传一个异步操作时候,没任何打印,为什么?因为我们在执行then的时候,这个promise的状态还是pending,pending时候不会执行then的2个方法参数的任何一个,所以就没得反应。换句话说,promise只有执行了resolve或reject,状态改变了,才会执行then。所以如果当调用 then 方法时,当前状态是 pending,我们需要先将成功和失败的回调分别存放起来,在executor()
的异步任务被执行时,触发 resolve 或 reject,依次调用成功或失败的回调。升级了朋友们!
恭喜恭喜!解决了传入异步的问题,喜提V1.1.0的手搓promise,代码如下:
1 /* promise的3个状态 */ 2 const PENDING = 'PENDING'; 3 const FULFILLED = 'FULFILLED'; 4 const REJECTED = 'REJECTED'; 5 6 class MyPromise { 7 constructor(executor) { 8 this.status = PENDING; // 初始的状态 9 this.value = undefined; // 成功的值 10 this.reason = undefined; // 失败的值 11 this.onFulfilledCallbacks = []; // 存放成功的回调 12 this.onRejectedCallbacks = []; // 存放失败的回调 13 let resolve = value => { 14 /* 防止重复执行,表示状态不可逆 */ 15 if (this.status === PENDING) { 16 this.value = value; 17 this.status = FULFILLED; 18 this.onFulfilledCallbacks.forEach(fn => fn()); 19 } 20 }; 21 let reject = reason => { 22 /* 防止重复执行,表示状态不可逆 */ 23 if (this.status === PENDING) { 24 this.reason = reason; 25 this.status = REJECTED; 26 this.onRejectedCallbacks.forEach(fn => fn()); 27 } 28 }; 29 /* 立即执行 executor*/ 30 try { 31 executor(resolve, reject); 32 } catch (e) { 33 reject(e); 34 } 35 } 36 then(onFulfilled, onRejected) { 37 if (this.status === FULFILLED) { 38 onFulfilled(this.value); 39 } 40 if (this.status === REJECTED) { 41 onRejected(this.reason); 42 } 43 if (this.status === PENDING) { 44 this.onFulfilledCallbacks.push(() => onFulfilled(this.value)); 45 this.onRejectedCallbacks.push(() => onRejected(this.reason)); 46 } 47 } 48 } 49 50 new MyPromise((resolve, reject) => { 51 setTimeout(() => { 52 resolve('异步成功啦!'); 53 // reject('失败啦'); 54 }, 1000); 55 }).then( 56 res => { 57 console.log('成功:', res); 58 }, 59 err => { 60 console.log('失败:', err); 61 } 62 );
2.3 手搓PromiseV2.0.0
我们开始要接触核心区了朋友们!promise最关键的还是它的then链式调用和值的穿透。
我们先来看看then链式调用,它可以一直.then,为什么?因为.then返回的也是一个promise呀,promise里面有then方法,当然可以继续往下点咯。所以
- 关键点1:then返回的是一个promise,
- 关键点2:上一次返回的值可以作为下一个then方法执行的参数
【promise2】里面该写啥呢?首先,上面版本做的事情,肯定要做进去的:执行【onFulfilled】或者【onRejected】,pending时候要存一下回调方法,其次,我们上面有说过,想要执行then,一定得执行resolve或reject,把状态改变了,才能执行then,所以我们手搓的then方法中,promise2一定要执行resolve或者reject才行,而且resolve或reject的参数,是下一次then【onFulfilled】或者【onRejected】返回的值。
then链式回调算是做了一个粗糙的版本了,但是那个resolvePromise方法里面的【resolve(x)】太粗糙啦,如果x里面有then,我们是要继续执行的,then里面返回的值才是下个then的方法参数,我们看看MDN里面的promise。
再看看把这个示例,用我们手写的promise打印出来的值
所以我们的resolvePromise需要更加精细一点,再打磨打磨一下。
then链式了,还有值穿透哦,就是then没做任何处理的时候,值会往下传。
以上,手写promise算是完成,下面就是扩展一下,在原型上增加catch、finally、all、race等等。
1 /* promise的3个状态 */ 2 const PENDING = 'PENDING'; 3 const FULFILLED = 'FULFILLED'; 4 const REJECTED = 'REJECTED'; 5 const resolvePromise = (promise2, x, resolve, reject) => { 6 // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1 7 if (promise2 === x) { 8 return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); 9 } 10 // Promise/A+ 2.3.3.3.3 只能调用一次 11 let called; 12 if ((typeof x === 'object' && x !== null) || typeof x === 'function') { 13 try { 14 let then = x.then; 15 if (typeof then === 'function') { 16 then.call( 17 x, 18 y => { 19 if (called) return; 20 called = true; 21 resolvePromise(promise2, y, resolve, reject); 22 }, 23 r => { 24 if (called) return; 25 called = true; 26 reject(r); 27 } 28 ); 29 } else { 30 resolve(x); 31 } 32 } catch (e) { 33 if (called) return; 34 called = true; 35 reject(e); 36 } 37 } else { 38 resolve(x); 39 } 40 }; 41 class MyPromise { 42 constructor(executor) { 43 this.status = PENDING; // 初始的状态 44 this.value = undefined; // 成功的值 45 this.reason = undefined; // 失败的值 46 this.onFulfilledCallbacks = []; // 存放成功的回调 47 this.onRejectedCallbacks = []; // 存放失败的回调 48 let resolve = value => { 49 /* 防止重复执行,表示状态不可逆 */ 50 if (this.status === PENDING) { 51 this.value = value; 52 this.status = FULFILLED; 53 this.onFulfilledCallbacks.forEach(fn => fn()); 54 } 55 }; 56 let reject = reason => { 57 /* 防止重复执行,表示状态不可逆 */ 58 if (this.status === PENDING) { 59 this.reason = reason; 60 this.status = REJECTED; 61 this.onRejectedCallbacks.forEach(fn => fn()); 62 } 63 }; 64 /* 立即执行 executor*/ 65 try { 66 executor(resolve, reject); 67 } catch (e) { 68 reject(e); 69 } 70 } 71 then(onFulfilled, onRejected) { 72 /* 穿透就是没有方法,则给一个默认的方法,将值传下去 */ 73 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v; 74 /* !!!注意啊,reject的方法空时不可以写 v=>v,不然永远都不会走catch,应该v=>{throw v}*/ 75 onRejected = 76 typeof onRejected === 'function' 77 ? onRejected 78 : err => { 79 throw err; 80 }; 81 let promise2 = new MyPromise((resolve, reject) => { 82 let commonDo = status => { 83 let x; 84 setTimeout(() => { 85 try { 86 if (status === FULFILLED) { 87 x = onFulfilled(this.value); 88 } 89 if (status === REJECTED) { 90 x = onRejected(this.reason); 91 } 92 resolvePromise(promise2, x, resolve, reject); 93 } catch (e) { 94 reject(e); 95 } 96 }, 0); 97 }; 98 if (this.status === FULFILLED) { 99 commonDo(this.status); 100 } 101 if (this.status === REJECTED) { 102 commonDo(this.status); 103 } 104 if (this.status === PENDING) { 105 this.onFulfilledCallbacks.push(() => commonDo(FULFILLED)); 106 this.onRejectedCallbacks.push(() => commonDo(REJECTED)); 107 } 108 }); 109 return promise2; 110 } 111 } 112 113 new MyPromise(resolve => { 114 resolve(1); 115 }) 116 .then(res => { 117 console.log(res); 118 return { 119 then: resolve => { 120 return resolve(2); 121 }, 122 }; 123 }) 124 .then(res => { 125 console.log(res); 126 return new Promise((resolve, reject) => { 127 reject('3错误'); 128 }); 129 }) 130 .then( 131 res => { 132 console.log(res); 133 }, 134 err => { 135 console.log('错误:', err); 136 } 137 );
3. 扩展Promise的api
3.1 Promise.prototype.resolve
1 static resolve(value) { 2 return new MyPromise((resolve, reject) => { 3 resolve(value); 4 }); 5 }
resolve 要稍微调整一下,因为promise.resolve 是具备等待功能的。
1 let resolve = value => { 2 if (value instanceof MyPromise) { 3 return value.then(resolve, reject); 4 } 5 /* 防止重复执行,表示状态不可逆 */ 6 if (this.status === PENDING) { 7 this.value = value; 8 this.status = FULFILLED; 9 this.onFulfilledCallbacks.forEach(fn => fn()); 10 } 11 };
3.2 Promise.prototype.reject
1 static reject(reason) { 2 return new MyPromise((resolve, reject) => { 3 reject(reason); 4 }); 5 }
3.3 Promise.prototype.catch
catch就是特殊的then,第一个参数传空就行。
1 catch(onRejected) { 2 this.then(null, onRejected); 3 }
3.4 Promise.prototype.finally
finally,不论promise的状态是fulfilled还是rejected,都会执行finally,而且finally上面返回的结果,会是下面执行的参数。
1 finally(callback) { 2 return this.then( 3 value => { 4 return MyPromise.resolve(callback()).then(() => value); 5 }, 6 reason => { 7 return MyPromise.resolve(callback()).then(() => { 8 throw reason; 9 }); 10 } 11 ); 12 }
之前有写过一篇 Promise常用的方法,对以下常用方法给出了一些说明。
3.5 Promise.prototype.all
接受promise数组,都成功时,按照顺序返回,有一个失败就算失败。
1 MyPromise.all = function (promises) { 2 return new MyPromise((resolve, reject) => { 3 let arr = []; 4 promises.forEach((promise, index) => { 5 MyPromise.resolve(promise) 6 .then(res => { 7 arr[index] = res; 8 // 只有都请求成功了,才算成功 9 if (Object.keys(arr).length === promises.length) { 10 resolve(arr); 11 } 12 }) 13 .catch(err => { 14 // 一个失败就算失败 15 reject(err); 16 }); 17 }); 18 }); 19 };
3.6 Promise.prototype.any
接受一个promise数组,有一个成功,则立马resolve执行then,只有全部失败才执行catch,并返回一个返回一个失败的 promise
和AggregateError
类型的实例。
3.7 Promise.prototype.race
接受一个数组,返回给出回应最快的那个,不论这个回应是resolve还是reject。
1 MyPromise.race = function (promises) { 2 return new MyPromise((resolve, reject) => { 3 promises.forEach(promise => { 4 MyPromise.resolve(promise) 5 .then(res => { 6 // 成功的立马返回 7 resolve(res); 8 }) 9 .catch(err => { 10 // 失败的立马返回 11 reject(err); 12 }); 13 }); 14 }); 15 };
3.8 Promise.prototype.allSettled
接受一个数组,但它不会执行.catch,只会执行.then,也是等所有请求结束后,返回一个数组,数组里的每项与参数的数组每项一一对应,返回的每项包含字段:
status:状态(rejected/fulfilled),value:成功返回的值,reason:失败返回的值。
我一般用它的场景是页面中有多个表单需要校验,这样多个表单的validate方法就是一个数组,allSettled方法的参数,然后我可以找到第几个表单校验失败。
1 MyPromise.allSettled = function (promises) { 2 return new MyPromise((resolve, reject) => { 3 let arr = []; 4 promises.forEach((promise, index) => { 5 MyPromise.resolve(promise) 6 .then(res => { 7 // 成功的 8 arr[index] = { 9 status: 'fulfilled', 10 value: res, 11 }; 12 }) 13 .catch(err => { 14 // 失败的 15 arr[index] = { 16 status: 'rejected', 17 reason: err, 18 }; 19 }) 20 .finally(() => { 21 // 所有请求都完成,那么resolve 22 if (Object.keys(arr).length === promises.length) { 23 resolve(arr); 24 } 25 }); 26 }); 27 }); 28 };
4. Promise 有什么缺陷,可以如何解决?
Promise 是没有中断方法的,xhr.abort()、ajax 有自己的中断方法,一种是设置超时时间让ajax自动断开,一种是手动去停止ajax请求,其核心是调用XMLHttpRequest对象上的abort方法,axios 是基于 ajax 实现的;fetch 基于 promise,所以他的请求是无法中断的。
我们可以利用promise的race,来自己封装一个中断的方法:
1 function wrap(promise) { 2 let abort; 3 let newPromise = new MyPromise((resolve, reject) => { 4 abort = reject; 5 // 假设我们设置的超时时间是2000ms,超过就超时失败 6 setTimeout(() => { 7 reject('超时了'); 8 }, 2000); 9 }); 10 let p = MyPromise.race([promise, newPromise]); 11 p.abort = abort; 12 return p; 13 }
好啦,promise的分享就到这里啦,有新的见解可以一起讨论呀,互相学习。