Javascript之我也来手写一下Promise

  Promise太重要了,可以说是改变了JavaScript开发体验重要内容之一。而Promise也可以说是现代Javascript中极为重要的核心概念,所以理解Promise/A+规范,理解Promise的实现,手写Promise就显得格外重要。如果要聊Promise就要从回调函数聊到回调地狱,再聊到同步异步,最终聊到Promise、async await。但是我们这篇文章,目的是手写Promise,这些前置知识如果大家不了解的话,希望可以去补充一下。那你可能会说了,我他妈不懂你在说啥,我就是想手写Promise,不行么?大佬~~那肯定是没问题的。好了,废话不多说,咱们开始吧。

一、实现Promise的基本结构

  最开始的部分,我们不得不去看看Promise/A+规范,因为我们所实现的、手写的代码,其实都是根据规范,一步一步来实现的。规范的地址,附在文末。

  整个Promise/A+规范有三个部分:术语、要求、说明。术语部分介绍了核心字段的关键性解释,要求部分基本上就是对整个Promise实现的要求,说明部分,则对Promise的实现提供了一些特殊场景的补充说明。

  首先,我们需要简单了解下Promise的基本概念。Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。与Promise交互的主要方式是通过它的then方法,该方法会注册一个回调函数,用来接收Promise的最终结果,这个结果可能是Promise的最终值,也可能是一个失败的原因。也就是Promise的resolve和reject呗~

  Promise/A+规范详细说明了该then方法的行为,所有符合Promise/A+规范所实现的Promise都可以基于此来提供一个可以互相操作的基础。因此,该规范应该是十分稳定的。也就是说,该Promise/A+规范并不会随意迭代,提供了一个长期稳定不变的规范版本。当然,也可能会有小的修改,但是一定是经过深思熟虑的。额~这些都是废话。重点就是最开始那一句:详细说明了then方法的行为。记住这句话!记住这句话!记住这句话!因为我们后面所做的一切,除了基本结构和参数,所有的一切都是围绕着then展开的。

  最后,核心的Promise/A+规范并不会去管你怎么实现,而是选择专注于提供可互操作的then方法。换句话说,我不管你写怎么实现Promise,我只关注你应该怎么实现then方法的可操作性。

  好了,基本的背景,我们了解了,我们之前说了,整个Promise/A+规范有三部分,我们先来看看第一部分术语,并实现这第一部分。

  1. “promise”是一个function或者object,它的行为要符合本规范。
  2. “thenable”是一个function或者object,它定义了then方法。(啥意思呢,其实就是:thenable是一个可以调用then方法的function或者object,再换句话说,就是我们new Promise().then的这个new Promise()呗,你咋实现我不管,有就行。)
  3. “value”是任何合法的值(包括thenable、promise或者undefined)。
  4. “exception”是使用throw语句抛出的值。
  5. “reason”是一个用来表示Promise为什么被reject的原因,的值。

  就这些,但是我稍微解释下,promise是指Promise这个整体,它可以是一个构造函数,也可以是个对象。如果Promise是个构造函数的话,那么thenable,其实就是Promise这个构造函数的实例,这个实例要有一个then方法。继续,exception就是异常处理,通过try catch语句来抛出错误。那么最后,value和reason分别对应了resolve和reject的参数。

  巴巴了这么多,我们先来写一点代码吧,不然长篇大论确实有点枯燥。

  前面总结了一下Promise基本背景,还有基本的术语,我们这一小节就来实现基于此的代码,但是我先总结一下哈,我们要实现的到底包括哪些内容。

  首先,我们要实现一个Promise类,这个类会提供一个实例方法也就是then方法。然后,我们还要有两个值:value和reason。最后,我们还要处理抛出的异常。没啦~一句话概括,精炼!

 1 class Promise {
 2   constructor(excutor) {
 3     this.value = undefined;
 4     this.reason = undefined;
 5     const resolve = (value) => {};
 6     const reject = (reason) => {};
 7     try {
 8       excutor(resolve, reject);
 9     } catch (err) {
10       reject(err);
11     }
12   }
13   then(onFulfilled, onRejected) {}
14 }

  简单不,其实啥也没有,就是按照之前我们描述的规范,来一步一步实现的。我简单再总结下,首先我们声明了一个Promise类,然后这个类有一个实例方法then,这个then方法接收两个回调函数(后面我们会完善的),然后整个类的构造函数部分,声明了一个value和reason,分别就是该Promise构造函数对应结果状态的值。最后,我们通过try…catch语句执行传递进来的回调excutor,并把resolve和reject方法作为excutor的回调。这里稍微有点绕,要捋一下噢。

  那么,我们再根据使用时的场景,来巩固一下上面的代码,通常,我们在使用Promise的时候,是这样的:

1 const p1 = new Promise((resolve, reject) => {});

  我们看,使用的时候,我们会给Promise这个类传过去一个方法,OK,这个方法就是我们Promise类中excutor。而这个executor又传回去了两个函数参数,可以让我们在new Promise时传递的executor的内部去调用:

1 const p1 = new Promise((resolve, reject) => {
2   if(true){
3     resolve("success")
4   } else {
5     reject("fail")
6   }
7 });

  这样我们的value和reason就传递给了constructor中的resolve和reject。最后,还有个实例方法then:

1 p1.then(
2   (value) => {
3     console.log("成功", value);
4   },
5   (reason) => {
6     console.log("失败", reason);
7   }
8 );

  这个then方法有两个参数,分别是成功的回调函数和失败的回调函数,也就是分别代表了Promise类中的实例的then方法的onFulfilled, onRejected。那么基本的结构我们就写完了,但是现在我们写的这个Promise肯定是没法用的。所以,我们还得继续往下,看看规范怎么要求的,我们按照要求再来完善。

二、完成基本的Promise

  我们前面完成了基本的结构,但是那些代码还缺了一些内容,这一小节,我们就来根据后面的Promise/A+规范,也就是要求部分,来实现、完善后面的代码。首先,规范就对Promise的状态,进行了详细的描述。

  promise必须处于以下三种状态之一:pending, fulfilled, or rejected。也就是待处理、已完成或已拒绝。

    1. 当处于pending状态时:

    • 可以转换到已完成(fulfilled)或者已拒绝(rejected)状态。

    2. 当处于fulfilled状态时:

    • 不可以改变状态。
    • 必须有一个不能改变的value。

    3.  当处于rejected状态时:

    • 不可以改变状态。
    • 必须有一个不能改变的reason。

  OK,这就是关于Promise的状态的描述。其实上面的说明很好理解,就是我们要给Promise这个类维护一个status字段,这个status有三个状态常量。一旦确定了status的结果,也就是status如果不是pending的话,就不能再修改status的状态了。

  我们继续,针对then方法的详细描述,当然,我们这个阶段,只需要其中的一部分。首先,Promise必须提供一个then方法来让使用者可以访问当前或最终状态的value或reason。其次,Promise的then方法接受两个参数:

promise.then(onFulfilled, onRejected)

  诶嘿!就是我们上面写的噢!继续~注意,以下的列表标记,完全抄袭自Promise/A+规范,方便大家查找。

  2.2.1 onFulfilled和onRejected都是可选参数

    2.2.1.1  如果onFulFilled不是函数,则忽略。

    2.2.1.2  如果onRejected不是函数,则忽略。

  2.2.2  如果onFulfilled是一个函数

    2.2.2.1  onFulfilled必须在promise已经是fulfilled的状态后调用,并且把promise的value作为它的第一个参数。

    2.2.2.2  在promise已经是fulfilled状态之前该方法不能被调用。

    2.2.2.3  该方法只能被调用一次。

  2.2.3  如果onRejected是一个函数

    2.2.3.1  onRejected必须在promise已经是rejected的状态后调用,并且把promise的reason作为它的第一个参数。

    2.2.3.2  在promise已经是rejected状态之前该方法不能被调用。

    2.2.3.3  该方法只能被调用一次。

  2.2.4  onFulfilled或者onRejected必须在执行上下文栈只有平台代码的时候才可以被调用。

  2.2.5  onFulfilled或者onRejected必须作为函数被调用(即没有this值)。

  OK,这一段规范翻译,其中尤其要关注的是2.2.4和2.2.5。

  首先,2.2.4是什么意思呢?平台代码,就是指环境、引擎、或者promise实现的代码,也就是说在onFulfilled或者onRejected被调用的时候,只能是在执行上下文栈中的最底层,它的前面要么是全局、要么是另一个符合规范的promise。在实践中,这样可以确保在调用事件环之后异步执行onFulfilled或者onRejected回调。这可以通过宏任务(比如 setTimeout 或者setImmediate)或微任务(MutationObserver或者process.nextTick)机制来实现。由于promise的实现会被当作平台代码,所以它本身可能包含一个任务调度队列或者一个调用处理程序的“蹦床”。这里的蹦床可以理解成一种转换函数,比如递归可能会导致栈溢出,那么可以通过蹦床函数把递归转换成循环,绕过JS的栈溢出检测机制。当然你也可能会写成死循环,那就是你代码的问题了。

  其次,2.2.5比较容易理解,就是指你实现的onFulfilled或者onRejected只能是一个函数,并且它内部的this如果在严格模式下是undefined,在非严格模式,则是全局对象。换句话说,也就是你的onFulfilled或者onRejected不会被任何对象或者函数调用,不归属于任何对象或者函数。

  理解了这段规范了吧?那么我们开始手写代码吧。

  首先,根据规范,我们要加入三个状态常量,并且Promise的初始状态肯定是pending:

 1 const PEDNING = "PENDING";
 2 const FULFILLED = "FULFILLED";
 3 const REJECTED = "REJECTED";
 4 class Promise {
 5   constructor(excutor) {
 6     this.status = PENDING;
 7     this.value = undefined;
 8     this.reason = undefined;
 9     const resolve = (value) => {};
10     const reject = (reason) => {};
11     try {
12       excutor(resolve, reject);
13     } catch (err) {
14       reject(err);
15     }
16   }
17   then(onFulfilled, onRejected) {}
18 }

  额外的,我们还要维护一个队列,这个队列用来当我们调用resolve或者reject的时候,触发我们传给then方法的函数参数。我们还要完善resolve和reject方法,以及then方法:

 1 const PENDING = "PEDNING";
 2 const FULFILLED = "FULFILLED";
 3 const REJECTED = "REJECTED";
 4 class Promise {
 5   constructor(excutor) {
 6     this.status = PENDING;
 7     this.value = undefined;
 8     this.reason = undefined;
 9     this.onResolvedCallbacks = [];
10     this.onRejectedCallbacks = [];
11     const resolve = (value) => {
12       if (this.status === PENDING) {
13         this.value = value;
14         this.status = FULFILLED;
15         this.onResolvedCallbacks.forEach((cb) => cb(this.value));
16       }
17     };
18     const reject = (reason) => {
19       if (this.status === PENDING) {
20         this.reason = reason;
21         this.status = REJECTED;
22         this.onRejectedCallbacks.forEach((cb) => cb(this.reason));
23       }
24     };
25     try {
26       excutor(resolve, reject);
27     } catch (err) {
28       reject(err);
29     }
30   }
31   then(onFulfilled, onRejected) {
32     if (this.status === FULFILLED) {
33       onFulfilled(this.value);
34     }
35     if (this.status === REJECTED) {
36       onRejected(this.reason);
37     }
38     if (this.status === PENDING) {
39       this.onResolvedCallbacks.push(onFulfilled);
40       this.onRejectedCallbacks.push(onRejected);
41     }
42   }
43 }

  我们来看下上面的代码,第9、10行,我们分别维护了一个数组,当然我们在promise仍旧是pending状态的时候调用then方法,就会把这两个状态的回调函数缓存起来。等到我们调用resolve或者reject确认了promise的状态时,就会去调用对应的缓存队列执行回调。

  resolve和reject方法十分简单,绑定value或者reason,改变状态,然后执行对应缓存的回调函数。而then方法的处理目前也并不复杂,根据状态去调用对应回调或者缓存回调。

  注意,之前我们翻译规范的时候还说过要确保在调用事件环之后异步执行onFulfilled或者onRejected回调。这个我们后面再实现,所以现在还都是同步代码。我们来测试一下我们的代码执行效果吧:

 1 const p1 = new Promise((resolve, reject) => {
 2   resolve("success");
 3 });
 4 
 5 p1.then(
 6   (value) => {
 7     console.log("成功", value);
 8   },
 9   (reason) => {
10     console.log("失败", reason);
11   }
12 );
13 
14 const p2 = new Promise((resolve, reject) => {
15   reject("fail");
16 });
17 
18 p2.then(
19   (value) => {
20     console.log("成功", value);
21   },
22   (reason) => {
23     console.log("失败", reason);
24   }
25 );

  我们来分析下这段代码是如何执行的。首先,当我们new Promise的时候就会执行Promise这个类的constructor,此时生成了部分实例字段和resolve、reject方法。并且执行了excutor方法,那么由于我们在方法内部调用resolve方法,也就相当于constructor是这样执行的:

1 try {
2   function excutor(resolve,reject){
3     resolve("success")
4   }
5   excutor(resolve, reject);
6 } catch (err) {
7   reject(err);
8 }

  那么此时,我们已经先走了resolve方法,也就是先确定了promised状态,再强调一下,现在都是同步的噢,然后,当我们后面再去调用then方法的时候,promise的状态已经是确定的了,所以不会去走缓存,直接走了

if (this.status === FULFILLED) {
  onFulfilled(this.value);
}

  这段代码,于是就执行了传给then的onFulfilled状态的回调。p2的例子也是一样的。

  那么我们继续再来看个例子:

 1 const p1 = new Promise((resolve, reject) => {
 2   setTimeout(() => {
 3     resolve(1);
 4   }, 1000);
 5 });
 6 
 7 p1.then(
 8   (value) => {
 9     console.log(value);
10   },
11   (reason) => {
12     console.log(reason);
13   }
14 );

  这个例子的执行结果是什么?我先不说答案,来捋一下,首先我们执行了excutor,但是此时我们并没有调用resolve,因为是异步的,所以要在一秒后才去调用resolve,那么此时executor就执行完了,然后我们去执行then方法,此时状态还没确定,所以走then的时候就走了这段代码:

1 if (this.status === PENDING) {
2   this.onResolvedCallbacks.push(onFulfilled);
3   this.onRejectedCallbacks.push(onRejected);
4 }

  缓存了起来,那么等到一秒后,调用了resolve,由于此时还是pending状态,所以resolve就走了其内部的逻辑:

1 const resolve = (value) => {
2   if (this.status === PENDING) {
3     this.value = value;
4     this.status = FULFILLED;
5     this.onResolvedCallbacks.forEach((cb) => cb(this.value));
6   }
7 };

  所以一秒后才会打印出1。

三、完善Promise的then方法

  那么继续,我们要进入下一个阶段了,之前的阶段我们解读(就是把规范复制过来翻译翻译)的规范还欠了点债,就是异步处理。那么这一小节,我们继续翻译剩下的规范,并且把欠大家的债还上。

  2.2.6  then方法可以被同一个promise调用多次

    2.2.6.1  当promise是fulfilled状态的时候,所有相关的onFulfilled回调函数必须按照then方法调用的顺序执行。

    2.2.6.2  当promise是rejected状态的时候,所有相关的onRejected回调函数必须按照then方法调用的顺序执行。

  2.2.7  then方法必须返回一个promise

promise2 = promise1.then(onFulfilled, onRejected);

    2.2.7.1  如果onFulfilled或者onRejected的其中一个返回了一个值,x。那么要运行:Promise Resolution Procedure [[Resolve]](promise2, x)。

    2.2.7.2  如果onFulfilled或者onRejected其中一个抛出了一个错误,e。promise2也必须变成使用e作为reason的拒绝状态。

    2.2.7.3  如果onFulfilled不是一个函数,并且promise1已经是fulfilled状态,promise2也必须变成fulfilled状态并且使用和promise1一样的value作为值。

    2.2.7.4  如果onRejected不是一个函数,并且promise1已经是rejected状态,promise2也必须变成rejected状态并且使用和promise1一样的reason作为错误信息。

  OK,这就是我们这个阶段要实现的规范。其中有些内容还是要解释下的。首先就是让人疑惑的Promise Resolution Procedure [[Resolve]](promise2, x)这个东西我特意没有翻译,因为你需要把它看作一个整体,现在这个阶段,你可以把它理解成一段要处理特定逻辑的代码块

  那么然后就是2.2.7.2到2.2.7.4,实际上只说了一句话,then方法必须返回一个promise,并且一旦该promise的状态已经确定,后续的promise也一定是同样的状态不得更改,且要把源promise的value或reason依次向后传递。也就是所谓的值穿透,听起来高大上,实际上没啥复杂的概念。

  我们接下来写代码吧。哦对,2.2.6其实我们上一小节已经写完了,就是那个数组,每次调用then如果promise是pending状态,就会往两个数组中依照then调用的顺序依次往里添加回调。所以,我们这一小节,实际上需要处理的核心内容,就是then方法,或者,我可以在这里确切的告诉大家,Promise的核心就是这个then方法,Promise中核心的核心是resolvePromise方法,接下来你就知道我为什么这么说了。

  constructor的代码暂时没有变化,这里就不再复制了,我们仅特别重要的关注then的变化,那么第一件要做的就是then方法会返回一个promise:

 1 then(onFulfilled, onRejected) {
 2   let p = new Promise((resolve, reject) => {
 3     if (this.status === FULFILLED) {
 4       onFulfilled(this.value);
 5     }
 6     if (this.status === REJECTED) {
 7       onRejected(this.reason);
 8     }
 9     if (this.status === PENDING) {
10       this.onResolvedCallbacks.push(onFulfilled);
11       this.onRejectedCallbacks.push(onRejected);
12     }
13   })
14   return p;
15 }

  啥也没干哈,就是返回了个新的promise,其实这也就是promise的链式调用。那问题来了,为啥我要把then的内容用一个新的promise包裹起来呢?既然要返回个promise,那我直接在代码的最后面,比如这样:

 1 then(onFulfilled, onRejected) {
 2   if (this.status === FULFILLED) {
 3     onFulfilled(this.value);
 4   }
 5   if (this.status === REJECTED) {
 6     onRejected(this.reason);
 7   }
 8   if (this.status === PENDING) {
 9     this.onResolvedCallbacks.push(onFulfilled);
10     this.onRejectedCallbacks.push(onRejected);
11   }
12   let p = new Promise((resolve, reject) => {
13   })
14   return p;
15 }

  那我这样不就可以了么?别忘了,我们还要把promise1的结果,传递给promise2,所以我们通过再生成一个新的promise2的内部去执行我们的回调。

  那么我们要如何把promise1的结果传给下一层呢?我们回头看下2.2.7.1,结果叫做x:

 1 then(onFulfilled, onRejected) {
 2   let p = new Promise((resolve, reject) => {
 3     if (this.status === FULFILLED) {
 4       setTimeout(() => {
 5         try {
 6           let x = onFulfilled(this.value);
 7           resolvePromise(p, x, resolve, reject);
 8         } catch (error) {
 9           reject(error);
10         }
11       }, 0);
12     }
13     if (this.status === REJECTED) {
14       setTimeout(() => {
15         try {
16           let x = onRejected(this.reason);
17           resolvePromise(p, x, resolve, reject);
18         } catch (error) {
19           reject(error);
20         }
21       }, 0);
22     }
23     if (this.status === PENDING) {
24       this.onResolvedCallbacks.push(() => {
25         setTimeout(() => {
26           try {
27             let x = onFulfilled(this.value);
28             resolvePromise(p, x, resolve, reject);
29           } catch (error) {
30             reject(error);
31           }
32         }, 0);
33       });
34       this.onRejectedCallbacks.push(() => {
35         setTimeout(() => {
36           try {
37             let x = onRejected(this.reason);
38             resolvePromise(p, x, resolve, reject);
39           } catch (error) {
40             reject(error);
41           }
42         }, 0);
43       });
44     }
45   });
46   return p;
47 }

  OK,这就是这一小节完整的then方法的部分了,甚至是整个promise实现的then方法也基本上跟这个差不多了,我们先来看当调用then方法的时候已经是fulfilled状态了的话,我们会获取到我们传给promise1的onFulfilled回调的结果作为x,这就是我们之前规范里所说的x,然后它走了一个resolvePromise,传了四个参数,并且把我们外面的那个p传了进去,那么假设,我没写外面的setTimeout,我能把p传给resolvePromise方法么?不能!!!因为我们想要在p还未生成之前就传给了内部的resolvePromise方法,这时候还没有p呢,所以我们利用事件循环机制,包了一层setTimeout,这样等到下一个tik整个p生成,我们就可以传给resolvePromise了。

  那么我们注意,无论在哪一个状态中,两种结果状态也就是fulfilled或rejected时,只要拿到了结果,就会走resolvePromise方法,只有try……catch报错了,才会直接抛出异常。那么这里的resolvePromise,其实就是我们前面没有翻译的那一段逻辑:Promise Resolution Procedure [[Resolve]](promise2, x),就这个。换句话说promise1的任何确定的状态结果,对于promise2来说都是要去resolve的,除非执行promise2的时候报错了。

  简单总结下:

  • then方法要返回个promise2。
  • then方法的成功或失败的回调函数的结果都是x。
  • x会传给resolvePromise做后续的处理。
  • 通过setTimeout的异步来使得resolvePromise可以获取到then方法内新生成的promise2。
  • 无论是失败还是成功,结果都是x,都会传给resolvePromise去做处理,同样的try……catch的报错会走promise1的reject。

四、实现完整的Promise

  那么这一小节,我们要实现完整的Promise,其实就是完善resolvePromise方法啦。在写代码之前,我们还是要来解读一下规范,我们就是照着规范写代码啦。

  Promise Resolution Procedure是一个会接收promise和x作为参数的一个抽象操作,我们把它标记为:[[Resolve]](promise, x)。如果x的表现形式是thenable的,也就是说如果x的结果是一个promise的话,那么假设x的行为与promise相似,则会试图使promise采用x的状态。

  这种thenable的处理允许promise之间的互相操作,只要这些promise的实现符合本规范的要求。它同样允许符合本规范的实现使用合理的then方法“同化”不符合的实现。

  [[Resolve]](promise, x)程序需要遵循以下步骤:

  2.3.1  如果promise和x引用自同一个对象,那么则抛出一个TypeError作为reason的reject状态。

  2.3.2  如果x是一个promise,则采用它的状态。通常,只有当x的实现是符合本规范的要求时,才会知道它是不是一个真正的承诺。本条规则允许使用特定于实现的方法来采用已知符合promise的状态。

    2.3.2.1  如果x是pending的状态,那么promise也必须保持pending状态,直到x的状态已变更为fulfilled或者rejected。

    2.3.2.2  如果x是fulfilled状态,那么就把promise的状态变更为fulfilled并使用与x一样的value。

    2.3.2.3  如果x是rejected状态,那么就把promise的状态变更为rejected并使用与x一样的reason。

  2.3.3  如果x是一个对象或者函数

    2.3.3.1  声明一个then变量存储x.then方法。这样做,其实就是为了缓存一下x.then的引用,那么当我们后续测试该引用,调用该引用的时候,都避免了多次调用x.then的访问。这些预防措施对于确保访问器属性的一致性非常重要,因为访问器属性的值可能在两次检索之间发生变化。

    2.3.3.2  如果我们在执行x.then后抛出了一个异常e,那么promise状态应修改为以e作为参数的rejected状态。

    2.3.3.3  如果then是一个函数,那么则调用then.call,把x作为call的第一个参数(也就是作为then的this),resolvePromise作为第二个参数,rejectPromise作为第三个参数,其中:

      2.3.3.3.1  如果resolvePromise被调用,并且使用y作为value,就执行[[Resolve]](promise, y)。

      2.3.3.3.2  如果rejectPromise被调用,并且使用r作为reason,就让promise的状态变为rejected。

      2.3.3.3.3  如果同时调用resolvePromise和rejectPromise或多次调用同一个,则第一个调用优先,并且任何进一步的调用都将被忽略。

      2.3.3.3.4  如果调用then方法抛出了一个错误e。

        2.3.3.3.4.1  如果resolvePromiserejectPromise已经被调用了,那么则忽略它。

        2.3.3.3.4.2  否则,把promise的状态变更为rejected,e作为reason。

    2.3.3.4  如果then不是一个一个函数,那么则把promise的状态变更为fulfilled,把x作为value。

  2.3.4  如果x不是一个函数或者对象,那么则把promise的状态变更为fulfilled,把x作为value。

  如果一个promise被一个参与循环的thenable链中的thenable所resolved,这样[[Resolve]](promise,thenable)的递归性质将最终导致再次调用[[Resolve]](promise,thenable),遵循上诉算法将导致无限递归。本规范鼓励但是并不要求一定要去检测这种无限递归,如果实现的话,那么此时promise的状态应该变成rejected并提供有效的错误信息作为reason。

  从实现上来说,不应该对可调用链的深度做任何限制,并假设超出该限制,递归将是无限的。只有确定的循环调用才会抛出TypeError。如果遇到无限的不同thenable链,则无限递归是正确的。

  好吧,这规范巴巴了好多。我建议要看一遍!我们继续上一小节的内容,去完善resolvePromise方法。

  首先2.3.1说了如果promise和x引用自同一个对象,那么抛出错误,这个简单,就这样被:

1 function resolvePromise(promise, x, resolve, reject) {
2   if (promise == x) {
3     return reject(
4       new TypeError("Chaining cycle detected for promise #<Promise>")
5     );
6   }
7 }

  那这样的场景什么时候才会出现呢?为什么x和promise不能相等呢?我们先来看个例子,首先我们在判断条件中添加一个打印,确定走到这个逻辑了:

1   if (promise == x) {
2     console.log("进来了");
3     return reject(
4       new TypeError("Chaining cycle detected for promise #<Promise>")
5     );
6   }

  然后例子是这样的:

 1 const p1 = new Promise((resolve, reject) => {
 2   resolve("success");
 3 });
 4 
 5 let p2 = p1.then((res) => {
 6   return p2;
 7 });
 8 p2.then(
 9   (value) => {
10     console.log("成功", value);
11   },
12   (reason) => {
13     console.log("失败", reason);
14   }
15 );

  执行这段代码,打印结果如下:

进来了
失败 TypeError: Chaining cycle detected for promise #<Promise>

  说明我们的代码没问题,那么我们得来分析下这段代码是如何执行的。在声明p1的时候,我们直接执行了excutor的resolve,所以此时的promise就已经是fulfilled的状态了。当我们再去调用then方法的时候,then方法会返回个promise,此时由于已经是fulfilled状态了,所以会命中if (this.status === FULFILLED)条件,那么此时的x就是调用onFulfilled方法的结果,这个x最终的结果就是p2,所以此时resolvePromise(p, x, resolve, reject)方法中的p和x是同一个,那么在Promise内部,我们要等待p2的执行结果,那么此时p2就即是执行的过程,又是等待的结果,所以p2永远都不会处于结果状态,于是我们要特殊处理这样无法得到最终结果的情况,reject出去。理解了吧?

  我们继续,再往后是2.3.2的解释,这块的解释我们在写代码的时候可以不去考虑,因为在写2.3.3的时候,可以覆盖到2.3.2的逻辑。那么:

 1 function resolvePromise(promise, x, resolve, reject) {
 2   if (promise == x) {
 3     console.log("进来了");
 4     return reject(
 5       new TypeError("Chaining cycle detected for promise #<Promise>")
 6     );
 7   }
 8   if ((typeof x === "object" && x !== null) || typeof x === "function") {
 9   } else {
10     resolve(x);
11   }
12 }

  着三行代码,就是2.3.3和2.3.4的逻辑框架,如果x是一个对象且不是null或者x是一个函数,那么要走一段逻辑,如果不符合这个条件的话说明是一个普通值,直接resolve就好了。这块很好理解,跟规范描述的一模一样。

  继续,2.3.3.1和2.3.3.2,当我们去取x.then的时候,可能会有报错,所以我们要try……catch处理一下:

1 if ((typeof x === "object" && x !== null) || typeof x === "function") {
2   try {
3     let then = x.then
4   } catch (error) {
5     reject(error)
6   }
7 } else {
8   resolve(x);
9 }

  就是这样。继续2.3.3.3:

 1 if ((typeof x === "object" && x !== null) || typeof x === "function") {
 2   try {
 3     let then = x.then;
 4     if (typeof then === "function") {
 5       then.call(
 6         x,
 7         (y) => {
 8           resolve(y)
 9         },
10         (r) => {
11           reject(r);
12         }
13       );
14     } else {
15       resolve(x);
16     }
17   } catch (error) {
18     reject(error);
19   }
20 } else {
21   resolve(x);
22 }

  我们看这段代码,跟2.3.3.3所说是一模一样的。我们调用then方法,把x作为this并且传了onFulfilled, onRejected两个函数作为参数,那么基本的核心逻辑我们就处理完了,但是还要补全一些细节,比如2.3.3.3.3所说,如果多次调用,只取第一次的,后面的调用都应该被忽略,那这个逻辑我们要怎么处理呢?再比如2.3.3.3.1所说,then在call的时候,如果resolvePromise被调用,并且使用y作为value,就执行[[Resolve]](promise, y),而不是像我们上面那样,直接resolve就完事了。所以我们要把resolve(y)修改成resolvePromise(promise, y, resolve, reject)。因为我们可能再返回一个promise,直到最后不是一个promise,也就是规范中所说的,允许任意递归调用。

  那么我们继续处理2.3.3.3.3这种情况:
 1 if ((typeof x === "object" && x !== null) || typeof x === "function") {
 2   let called = false;
 3   try {
 4     let then = x.then;
 5     if (typeof then === "function") {
 6       then.call(
 7         x,
 8         (y) => {
 9           if (called) return;
10           called = true;
11           resolvePromise(promise, y, resolve, reject);
12         },
13         (r) => {
14           if (called) return;
15           called = true;
16           reject(r);
17         }
18       );
19     } else {
20       resolve(x);
21     }
22   } catch (error) {
23     if (called) return;
24     called = true;
25     reject(error);
26   }
27 } else {
28   resolve(x);
29 }

  我们看代码,其实处理起来也并不复杂,我们在执行的时候,添加了一个flag,只要走到了某一个会修改promise状态的逻辑中,就会修改flag的状态,后续再调用就不会再去走逻辑代码了。也就是一旦状态确定,后续无论怎么调用,都是之前确定的状态,无法更改。

  那么其实到现在,核心的代码基本上就都完事了,其实你看,核心的代码其实就是resolvePromise这个方法,剩下的,我们还需要处理一点细节,比如2.2.1,onFulfilled, onRejected都是可选参数。我们稍微来处理一下then方法:

 1 then(onFulfilled, onRejected) {
 2   onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
 3   onRejected =
 4     typeof onRejected === "function"
 5       ? onRejected
 6       : (e) => {
 7           throw e;
 8         };
 9   // 后面的略了  
10 }

  看到没,代码很简单,如果没传,我们就自己造一个就好了。最后我们就写完了这个Promise的实现,完整代码可在:https://github.com/zakingwong/zaking-js-advanced/tree/promise这里查看。

  那么最后,我们还可以使用社区的工具库,来测试下我们所写的代码是否符合规范。这个我就不多说了,具体可以参考gitHub上的代码。

  既然代码我们都写完了,来玩两个例子吧。

 1 const zp1 = new ZakingPromise((resolve, reject) => {
 2   resolve("ok");
 3 }).then((data) => {
 4   return new Promise((resolve, reject) => {
 5     setTimeout(() => {
 6       resolve(data);
 7     }, 1000);
 8   });
 9 });
10 
11 zp1.then(
12   (data) => {
13     console.log(data, "zp1-resolve");
14   },
15   (err) => {
16     console.log(data, "zp1-reject");
17   }
18 );

  这里的ZakingPromise是我们自己手写的Promise,Promise就是ES6的Promise。我们来分析下这段代码,其实并不难噢。首先,ZakingPromise直接resolve出去了一个ok字符串,那么当我们调用then的时候,状态已经是fulfilled了,此时then里面执行的代码是这样的:

 1 let p = new Promise((resolve, reject) => {
 2   if (this.status === FULFILLED) {
 3     setTimeout(() => {
 4       try {
 5         let x = onFulfilled(this.value);
 6         resolvePromise(p, x, resolve, reject);
 7       } catch (error) {
 8         reject(error);
 9       }
10     }, 0);
11   }
12 }
13 return p

  pending和rejected都跟我没关系。就执行了上面这点代码,那么这里的onFulfilled就是我们传进来的这段代码:

1 (data) => {
2   return new Promise((resolve, reject) => {
3     setTimeout(() => {
4       resolve(data);
5     }, 1000);
6   });
7 }

  它返回了一个new Promise,所以此时的x可以理解为:

let x = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('ok');
  }, 1000);
});

  OK,那么我们继续,要去走resolvePromise这段代码了。那么resolvePromise判断逻辑就不走了哈,它肯定是个函数然后获取x.then,并且x.then肯定也是函数。那么里面的逻辑就相当于是这样执行的:

 1 new Promise((resolve, reject) => {
 2   setTimeout(() => {
 3     resolve("ok");
 4   }, 1000);
 5 }).then(
 6   (y) => {
 7     if (called) return;
 8     called = true;
 9     resolvePromise(promise, y, resolve, reject);
10   },
11   (r) => {
12     if (called) return;
13     called = true;
14     reject(r);
15   }
16 );

  这样是不是就很好理解了,那么我们就得去分析下上面的代码,还没调用then是如何执行的,就很简单了对吧,在Promise内部由于调用then的时候还没resolve,之前咱们分析过,所以then方法内存了两个缓存数组,当1秒后调用了resolve("ok")的时候,then里的onFulfilled回调就执行了,此时的y就是“ok”。注意,这里我省略了那个调用call时候的x,也就是此时then方法内部的this指向,那这个x、也就是this就是指这个new Promise他自己。

  于是我们再走这段代码:

1 zp1.then(
2   (data) => {
3     console.log(data, "zp1-resolve");
4   },
5   (err) => {
6     console.log(data, "zp1-reject");
7   }
8 );

  就走了zp1的onFulfilled回调,于是就打印了“ok zp1-resolve”。行吧,例子就这一个了吧暂时,大家要去多练练例子啊,把复杂的情况都梳理梳理。

五、实现Promise的其他方法

   诶?我记得Promise还有catch方法、all方法、还有race、还有finally,这些方法你咋没写呢?嗯,首先前四章所有翻译的内容就是Promise规范的所有内容,不信的话我在文末贴出了Promise/A+规范的地址,也就是说规范中压根就没有这些方法,这些方法都是ES6额外实现的,那么接下来我们就来实现下这些方法。

1、Promise.resolve、Promise.reject和Promise.catch方法的实现

  我们先来看下Promise.resolve和Promise.reject,其实实现起来很简单哈,我们先写个例子:

1 Promise.resolve("ok").then((data) => {
2   console.log(data, "resolve");
3 });

  我们看,调用Promise.resolve直接就返回了个promise的fulfilled状态,再去调用then的成功的回调,所以实现起来就是这样的:

1 static resolve(value) {
2   return new Promise((resolve, reject) => {
3     resolve(value);
4   });
5 }

  前面的那些代码我就不写了哈,但是这样还没完,我们再看个例子:

1 Promise.resolve(
2   new Promise((resolve, reject) => {
3     setTimeout(() => {
4       resolve("Zaking");
5     }, 1000);
6   })
7 ).then((data) => {
8   console.log(data, "resolve----");
9 });

  你猜,按照我们之前实现的代码,这个打印的结果是什么。

Promise {
  status: 'PENDING',
  value: undefined,
  reason: undefined,
  onResolvedCallbacks: [],
  onRejectedCallbacks: []
} resolve----

  为什么会这样呢?按道理不应该是Zaking这个字符串么?ES6实现的是这样的,但是咱们之前的代码并没有做这个的处理,其实这里的resolve方法,不就是包裹了一层Promise后执行了resolve嘛,所以当我们调用Promise.resolve的时候,上面例子的代码中,传给Promise.resolve的那个新的promise被当作value返回了,所以这里我们要添加点代码处理下:

 1 class Promise {
 2   constructor(excutor) {
 3     // ...
 4     const resolve = (value) => {
 5       if (value instanceof Promise) {
 6         return value.then(resolve, reject);
 7       }
 8       if (this.status === PENDING) {
 9         this.value = value;
10         this.status = FULFILLED;
11         this.onResolvedCallbacks.forEach((cb) => cb(this.value));
12       }
13     };
14     // ...
15   }
16   // ...
17 }

  我们判断一下传入的value是不是一个Promise的实例,如果是的话,那么就再调用一下then,这样就可以把结果传递到下一层了。这样,我们打印的结果就符合ES6的实现了,注意,这个跟规范无关了噢。

Zaking resolve----

  你猜Promise.reject这个方法怎么实现?我不写了噢,你自己试试!要注意的是,resolve一个Promise会等待解析后的结果,但是reject一个Promise会直接走向失败。

  那么接下来我们来实现一个更简单的方法,Promise.catct:

1 catch(errCb) {
2   return this.then(null, errCb);
3 }

  就这么简单,其实catch方法本质上就是then中的onRejected这个回调,那么我们直接调用就好了,不传onFulfilled只传onRejected。

2、Promise.all的实现

  all方法其实很好理解,就是所有的回调都成功了,才算是成功,我们看个例子:

 1 Promise.all([
 2   new Promise((resolve, reject) => {
 3     resolve("1");
 4   }),
 5   new Promise((resolve, reject) => {
 6     resolve("2");
 7   }),
 8 ]).then((data) => {
 9   console.log(data, "all");
10 });

  就是这样,all方法会接收一个都是Promise的数组,然后内部会去处理这个数组,我们就来看看是怎么处理的吧:

 1 static all(promises) {
 2   let result = [];
 3   let times = 0;
 4   return new Promise((resolve, reject) => {
 5     function processResult(data, index) {
 6       result[index] = data;
 7       if (++times === promises.length) {
 8         resolve(result);
 9       }
10     }
11     for (let i = 0; i < promises.length; i++) {
12       const promise = promises[i];
13       Promise.resolve(promise).then((data) => {
14         processResult(data, i);
15       }, reject);
16     }
17   });
18 }

  其实你看代码并不多的,我们来分析下吧。首先我们声明了两个变量,一个存储结果的数组,这个结果是指resolve成功后的onFulfilled回调的value,一个是计数器。然后我们去循环整个传入的promises,让每一个promise传递给Promise.resolve去执行,然后我们用一个processResult去处理返回的data和当前的下标i,然后我们还要处理一下reject,那么这里要注意哈,对于all方法来说,只要有一个出错了,那么整个all执行的结果就是rejected的,所以reject不用特殊处理,直接reject就好了,不需要添加进数组这个那个的。

  那么继续,processResult每当我们执行一次的时候,就会根据传入的下标的位置去存储结果,这样处理其实就是为了按照传入promise的先后顺序去存储结果,然后先++times再去和promises的length长度去做比较,换句话说就是计数嘛,如果相等,就直接resolve最终的result即可。并不是很复杂噢。

3、Promise.race的实现

  这个race是什么意思呢,我们先看个例子:

const p = Promise.race([p1, p2, p3]);

  上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。换句话说就是,只要有一个结果就行了,不管这个结果是啥。谁跑得快我就算谁是第一,这就是race的意思。那既然是谁快我选谁,那我们代码要怎么写?循环一下全部执行一遍呗,谁有结果了就完事了呗:

1 static race(promises) {
2   return new Promise((resolve, reject) => {
3     for (let i = 0; i < promises.length; i++) {
4       let promise = promises[i];
5       Promise.resolve(promise).then(resolve, reject);
6     }
7   });
8 }

  嗯……就这么简单,其实就是all的那部分删除了一些~~

4、Promise.allSettled的实现

  这个方法是啥意思呢,就是不管成功或失败,会返回所有异步的结果。诶?那不是跟all方法很类似,只要修改一下all方法不就可以了,没错,你可真是个小聪明:

 1 static allSettled(promises) {
 2   let result = [];
 3   let times = 0;
 4   return new Promise((resolve, reject) => {
 5     function processResult(data, index, status) {
 6       result[index] = { status, value: data };
 7       if (++times === promises.length) {
 8         resolve(result);
 9       }
10     }
11     for (let i = 0; i < promises.length; i++) {
12       const promise = promises[i];
13       Promise.resolve(promise).then(
14         (data) => {
15           processResult(data, i, "fulfilled");
16         },
17         (err) => {
18           processResult(err, i, "rejected");
19         }
20       );
21     }
22   });
23 }

  你看跟all方法有啥区别?无非就是额外处理了onRejected,直接onRejected就直接失败了,现在回去走processResult方法也存到result中,当然,result存的内容也不太一样,多了个状态,会告诉你是失败的结果还是成功的结果。不复杂吧,其实就是all方法。

5、Promise.finally的实现

  finally方法用于指定不管Promise对象最后的状态如何,都会执行的操作,就是我不管你Promise最后是fulfilled还是rejected,都会执行finally的回调。那我们来看是咋实现的吧:

 1 finally(finallyCallback) {
 2   let p = this.constructor;
 3   return this.then(
 4     (data) => {
 5       return p.resolve(finallyCallback()).then(() => data);
 6     },
 7     (err) => {
 8       return p.resolve(finallyCallback()).then(() => {
 9         throw err;
10       });
11     }
12   );
13 }

  其实代码不难,但是这里面还是有点东西的。为什么我在this.then中又调用了p.resolve并且传了finally的finallyCallback回调?我直接这样写不行么?

 1 finally(finallyCallback) {
 2   return this.then(
 3     (data) => {
 4       finallyCallback()
 5       return data;
 6     },
 7     (err) => {
 8       finallyCallback()
 9       throw err;
10     }
11   );
12 }

  调用finallyCallback,返回结果。好像挺完美的。但是,不太行,因为按照上面的finally实现,你这样去写Promise的用法:

 1 Promise.resolve("zaking-finally")
 2   .finally(() => {
 3     console.log("finally~~~");
 4   })
 5   .then((data) => {
 6     console.log(data, "finally-resolved");
 7   })
 8   .catch((err) => {
 9     console.log(err, "finally-rejected");
10   });

  或者这样:

 1 Promise.resolve("zaking-finally")
 2   .finally(() => {
 3     console.log("finally~~~");
 4     return 1;
 5   })
 6   .then((data) => {
 7     console.log(data, "finally-resolved");
 8   })
 9   .catch((err) => {
10     console.log(err, "finally-rejected");
11   });

  都没问题,但是因为finallyCallback有可能是异步的,所以我们需要额外的包裹一层,再去执行最终的onFulfilled或者onRejected。比如这样:

 1 Promise.resolve("zaking-finally")
 2   .finally(() => {
 3     return new Promise((resolve, reject) => {
 4       setTimeout(() => {
 5         resolve();
 6         console.log("finally");
 7       }, 1000);
 8     });
 9   })
10   .then((data) => {
11     console.log(data, "finally-resolved");
12   })
13   .catch((err) => {
14     console.log(err, "finally-rejected");
15   });

  那么本章所有的内容就都完事了,当然我们并没有实现所有的方法,如果你真的理解了,完全可以自己去实现了,比如any方法和try方法,甚至于你可以自己去创造某些方法,因为Promise/A+规范范围内的所有内容,其实就那么点,其他的都是实现罢了,你看我讲了五个方法,实际上只讲了all和finally这两个方法。

6、promisify

  最后我们来聊一聊promisify,也是这篇文章最后的一点内容,promisify其实是Node.js提供的可以把普通带回调的函数,转换成promise对象的工具方法。我们先来看个例子,怎么用promisify:

 1 function testA(a, b, cb) {
 2   cb(null, a + b);
 3 }
 4 
 5 let A = promisify(testA);
 6 A(1, 2).then(
 7   (data) => {
 8     console.log(data, "data-----");
 9   },
10   (err) => {
11     console.log(err);
12   }
13 );

  上面的代码,首先testA是一个带有回调函数作为参数的方法,当然这是一个同步回调,然后要注意的是,cb在testA中必须是最后一个参数,而执行cb的时候,第一个参数必须是错误处理的回调函数,这里我们直接用null代替了,然后,我们通过promisify方法,包裹了一下testA,生成了一个A方法,那么此时的A就是一个Promise对象了噢,然后调用Promise的then方法,传入onFulfilled和onRejected两个回调,其实这里的onFulfilled就可以理解成是cb,而onRejected就是那个null。那么我们来看下实现,代码很少:

 1 function promisify(fn) {
 2   return function (...args) {
 3     return new Promise((resolve, reject) => {
 4       fn(...args, function (err, data) {
 5         if (err) return reject(err);
 6         resolve(data);
 7       });
 8     });
 9   };
10 }

  好嘞,那么我们依照实现和例子代码,我们来分析下这个promisify是如何运行的。首先,promisify返回了一个函数,那么这个返回的函数,就是我们例子中的A方法,A传入的参数1,2也同样在promisify返回的function中通过rest参数来处理的,从而获取到我们传入的参数。然后,我们在返回的函数内部又返回了一个Promise。我们先不管这个Promise,咱们删除点东西再看一下:

1 function promisify(fn) {
2   return function (...args) {
3     return new Promise((resolve, reject) => {
4       fn();
5     });
6   };
7 }

  我们看,其实就是在返回的Promise内部执行了一下我们传入的testA方法,只不过他回调前面的参数都用rest参数的方式获取到了,那么我们testA的第三个参数就是这里的:

1 function (err, data) {
2     if (err) return reject(err);
3     resolve(data);
4 }

  这一部分,就是testA内部执行的cb了呗。err就是我们传的null,data就是那个a + b。简单吧?其实一点都不复杂。

  那你可能会问为啥要这样固定的去写testA方法呢,回调必须是最后一个参数,错误的回调还必须是回调函数的第一个参数,嗯。。是因为node的实现是这样的~~

  最后,再补充一点,这个东西理解了promisify后就很简单了,不多说了,也就是promisifyAll方法,接收一个对象作为参数:

1 function promisifyAll(obj) {
2   let result = {};
3   for (let key in obj) {
4     result[key] =
5       typeof obj[key] === "function" ? promisify(obj[key]) : obj[key];
6   }
7   return result;
8 }

  很简单,就是生成一个新的对象result,如果传入的obj中的某个key是函数,就走一下promisify方法转换,最终返回result结果对象。

 

  附:

 

  最后:由于本人能力有限,感觉写的内容并未完全覆盖使用场景,大家见谅,但是还是有参考价值的~额~~然后我第一次尝试加行号,结果博客园的行号会一起复制下来,不太友好,所以特别建议大家手打代码,嘿嘿。

posted @ 2022-07-19 13:49  Zaking  阅读(873)  评论(2编辑  收藏  举报