最近迷上了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 #
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 吗”