ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Javascript之我也来手写一下Promise

2022-07-19 14:01:34  阅读:158  来源: 互联网

标签:resolve Javascript value onFulfilled promise reject 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结果对象。

 

  附:

 

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

标签:resolve,Javascript,value,onFulfilled,promise,reject,Promise,手写
来源: https://www.cnblogs.com/zaking/p/16467314.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有