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+规范有三部分,我们先来看看第一部分术语,并实现这第一部分。
- “promise”是一个function或者object,它的行为要符合本规范。
- “thenable”是一个function或者object,它定义了then方法。(啥意思呢,其实就是:thenable是一个可以调用then方法的function或者object,再换句话说,就是我们new Promise().then的这个new Promise()呗,你咋实现我不管,有就行。)
- “value”是任何合法的值(包括thenable、promise或者undefined)。
- “exception”是使用throw语句抛出的值。
- “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 如果resolvePromise
或rejectPromise已经被调用了,那么则忽略它。
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,也就是规范中所说的,允许任意递归调用。
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]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,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结果对象。
附:
- https://promisesaplus.com/
- https://es6.ruanyifeng.com/#docs/promise
- https://github.com/zakingwong/zaking-js-advanced/tree/promise
最后:由于本人能力有限,感觉写的内容并未完全覆盖使用场景,大家见谅,但是还是有参考价值的~额~~然后我第一次尝试加行号,结果博客园的行号会一起复制下来,不太友好,所以特别建议大家手打代码,嘿嘿。
本文来自博客园,作者:Zaking,转载请注明原文链接:https://www.cnblogs.com/zaking/p/16467314.html