保姆级手写promise以及promise常用得方法

最近迷上了promise,觉得还挺有意思,看了大佬的一篇知乎文章  面试官:“你能手写一个 Promise 吗” ,写的很详细,超级赞,看完了来着做个笔记,牢固一下理解和印象。

1. 常见 Promise 面试题

这个模块是搬运 面试官:“你能手写一个 Promise 吗” 文章,不想到时候看还得2个文章切换,所以就把这块搬运过来。

  1. Promise 解决了什么问题?
  2. Promise 常用的 API 有哪些?
  3. 能不能手写一个符合 Promise/A+ 规范的 Promise?
  4. 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 })

为了获取回调得结果,就必须得这样一层嵌套一层,使得代码变的更加难以阅读和难以维护,这就是传说中臭名昭著的回调地狱,产生回调地狱的原因:

  1. 嵌套调用,第一个函数的输出往往是第二个函数的输入;
  2. 处理多个异步请求并发,开发时往往需要同步请求最终的结果。

Promise用比较友好的方式解决以上2个问题,用then链式解决嵌套问题,用Promise.all解决异步请求并发的问题。

2. 开始手搓Promise啦

手搓Promise,不是一蹴而就的,慢慢来,先搞简单的版本1,然后再慢慢升级,向Promise靠拢。

Promise核心列一下,不然要实现什么我们都不知道。

  1. 它有3个状态,pending、fulfilled、rejected,一经改变不可逆转
  2. new Promise接受一个executor()执行器,并立即执行
  3. executor接受resolve和reject
  4. then方法,接受2个参数,成功执行参数1,onFulfilled,参数是成功的值value;失败执行参数2, onRejected,参数是失败的值reason;
  5. then链式调用,值的穿透
  6. catch、finally
  7. 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. 关键点1:then返回的是一个promise,
  2. 关键点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的分享就到这里啦,有新的见解可以一起讨论呀,互相学习。

参考

面试官:“你能手写一个 Promise 吗” 

posted @ 2023-02-06 13:47  蛙仔  阅读(897)  评论(0编辑  收藏  举报