上节课我们介绍了Promise
中的一些关键问题,也说了这些问题是作为后续学习Promise
封装的基础支撑的,那么接下来我们就开始学习一下关于Promise
的自定义封装。所谓自定义封装其实就是手写Promise
。用纯原生js
代码来实现Promise
。
初始结构搭建
我们想要手写一个Promise
底层实现可想而知是一件比较困难的事情,所以我们把整个Promise
来拆分开逐一介绍,首先第一步我们来从搭建初始结构开始。
let p = new Promise( (resolve, reject) => { resolve("OK"); } ); p.then( value => console.log(value), reason => console.log(reason) );
我们来看一下上面这段代码是正常情况下我们来创建的一个Promise
对象,这一段代码在控制台中执行肯定是输出OK
,我们来看一下:
但是现在我们要自定义的话那肯定要把内置的Promise
覆盖掉,那么我们来自定义一个js
,并在我们的html
文件中引入
// promise.js function Promise() { }
首先,Promise
是什么?我们在之前的内容中介绍过,Promise
是一个构造函数,我们可以通过这个构造函数来实例化一个对象。那么我们现在就定义一个函数。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Promise Encapsulation</title> <script src="./js/promise.js"></script> </head> <body> <script> let p = new Promise( (resolve, reject) => { resolve("OK"); } ); p.then( value => console.log(value), reason => console.log(reason) ); </script> </body> </html>
然后我们在html
文件中引入这个js
文件,我们来看一下能不能成功覆盖内置的Promise
构造函数:
控制台里面报错了,说p.then
不是一个函数,这是为什么呢?这说明我们已经成功用我们自己定义的Promise
函数覆盖了内置的Promise
函数了,而我们自己定义的Promise
函数就只是一个空的函数,也没有为我们自己定义的Promise
函数添加then
方法。所以说这里报错了,那么我们就给Promise
函数添加一个then
方法:
function Promise(excutor) { } Promise.prototype.then = (onResolved, onRejected) => { };
我们现在再来看代码,首先我们给Promise
函数添加了一个形参,为什么要添加这个形参?因为我们在实例化Promise
对象的时候要传入一个执行器函数来执行异步任务或者同步任务。其次我们给Promise
的原型对象上添加了一个then
方法,then
方法是用来指定回调的,要接收两个函数类型的参数,所以我们也加了两个形参,现在我们再来看一下效果:
这次没有报错,但是控制台中也没有输出任何东西,因为我们虽然定义了then
方法,但是我们并没有做任何操作,所以只是使语法上完成了初始结构的搭建。
以上便是一个Promise
的最初始的一个基本结构。
resolve
与reject
函数初始结构搭建
我们现在已经有了一个Promise
的基本解构了,但是感觉好像还是还差点意思,我们在实例化Promise
对象的时候是不是要传进去一个执行器函数(excutor
)啊?而且我们之前在介绍的时候也说过我们的执行器函数在实例化Promise
对象的时候会立即被同步调用,那么就简单了:
function Promise(excutor) { excutor(); }
我们直接这样同步调用一下不就行了嘛,当然这样的思路是对的,但是大家还记不记得我们传入的执行器函数都接收了两个形参?而且这两个形参都是函数,一个是resolve
函数,另一个是reject
函数。那么好,我们这样改一下代码:
function Promise(excutor) { excutor(resolve, reject); }
但是这样能行吗?
直接报错了,说resolve
没有被定义,那么我们是不是就要来声明一下这两个函数呢?我们来修改一下代码:
function Promise(excutor) { const resolve = data => { }; const reject = data => { }; excutor(resolve, reject); }
大家来看这段代码,我们声明了两个箭头函数,分别是resolve
和reject
,而且这两个函数在声明的时候都分别接收了一个形参,因为我们在执行器函数中开启同步任务或者异步任务的时候都是要传入我们异步任务成功或者失败时候的值的,所以需要预留一个形参的位置。那么我们现在再看一下:
现在我们再看控制台里不再报错了,这样的话我们就完成了resolve
函数和reject
函数的结构搭建
resolve
函数与reject
函数的实现
我们现在已经实现了resolve
和reject
的基本机构了,但是我们这两个函数存在的目的是什么呢?当然是用来修改Promise
对象的状态和Promise
对象的结果值,但是我们现在就一个空函数那么肯定是没有办法修改Promise
对象的状态和结果值的。
那么接下来我们就来实现这两个函数:
function Promise(excutor) { this.PromiseState = "pending"; this.PromiseResult = null; const resolve = data => { this.PromiseState = "fulfilled"; this.PromiseResult = data; }; const reject = data => { this.PromiseState = "rejected"; this.PromiseResult = data; }; excutor(resolve, reject); }
我们来看这段代码,第一步我们先用this
关键字给Promise
添加了两个属性,分别就是Promise
的状态和结果,为什么要添加这一步呢?因为我们Promise
对象在第一步实例化的时候是不是有默认状态和结果的?默认是pending
状态,结果是null
,所以说我们要在第一步来添加默认状态和结果。
然后再来看resolve
函数和reject
函数,这两个函数接收一个形参,根据我们之前的学习都知道,这个形参就是Promise
对象的结果值,而这两个函数对状态的修改是固定的,不需要参数传入,那么我们直接再通过this
关键字将对应的属性做一下修改就可以完成了。
let p1 = new Promise( (resolve, reject) => { resolve("OK"); } ); let p2 = new Promise( (resolve, reject) => { reject("error"); } ); console.log("p1: ", p1); console.log("p2: ", p2);
但是我们现在还没有去实现then
方法的内部代码,那么现在我们来打印一下看看我们的resolve
函数和reject
函数能不能正常工作:
我们可以看到控制台中正常输出了我们实例化的Promise
对象。那么这样我们便成功实现了resolve
函数和reject
函数的内部代码逻辑。
throw
异常处理
我们在之前的内容中说过,修改Promise
对象的状态以及结果的方法有三种,分别是resolve
函数,reject
函数,以及throw
关键字,现在我们已经实现了前面两个方法,那么接下来我们来实现第三个throw
异常的方式来修改Promise
对象的状态和结果。
let p = new Promise( (resolve, reject) => { throw "error"; } ); console.log(p);
我先来看一下当前状态下如果我们throw
一个异常出来会是什么情况:
现在是直接报错的,连后面的console.log
都没有执行,首先这肯定是不对的,我们不能让throw
在这里中断程序,那么如何才能让一段程序执行过程中报了错但是不会中止程序的运行呢?那么我们下意识是不是就会想到try/catch
?那么这个try/catch
应该写在哪呢?
我们来思考一个问题,这个异常是在哪里抛出的?是不是在执行器函数里面?那么执行器函数是在哪里被执行的呢?是不是在Promise
构造函数中?那么我们这么改一下代码:
function Promise(excutor) { this.PromiseState = "pending"; this.PromiseResult = null; const resolve = data => {...}; const reject = data => {...}; try { excutor(resolve, reject); } catch (e) { reject(e); } }
我们拉看一下这段代码,我们在try
部分来执行excutor
函数,当excutor
函数抛出异常之后,就会被catch
捕获,而我们throw
出来的异常字符串就会直接被catch
的形参e
接收到,那么我们怎么样来修改Promise
对象的状态和结果呢?是不是直接调用一下reject
函数就可以实现了?我们来看一下:
控制台输出的正是一个失败的Promise
对象,而且结果值就是我们抛出的异常字符串。
以上我们便实现了所有修改Promise
对象的状态和结果的三种方法。
实现Promise
状态只能修改一次
我们在之前的学习中提到过,任何一个Promise
对象的状态只能修改一次,只能单方向地从pending
变为resolved
或者rejected
,而且不允许resolved
和reject
之间相互转换,但是我们来看一下我们现在的情况下是什么情况:
let p = new Promise( (resolve, reject) => { resolve("OK"); reject("error"); } ); console.log(p);
我们在这个执行器函数中既调用了resolve
函数又调用了reject
函数,按照Promise
的设计哲学,我们这里输出的应该是一个成功的Promise
对象才对,那么我们来看一下结果是什么样子:
我们发现控制台中输出了一个失败的Promise
对象,这是不符合我们的预期的,我们来回想一下Promise
的设计哲学,只允许修改pending
状态的Promise
对象的状态,且不可逆,不可逆我们已经做到了,我们没有实现将其他状态改为pending
状态的方法。那么我们就要做一件事,禁止修改状态不是pending
的Promise
对象的状态,那么简单,只要加一个判断就行了:
function Promise(excutor) { this.PromiseState = "pending"; this.PromiseResult = null; const resolve = data => { if (this.PromiseState !== "pending") return; this.PromiseState = "fulfilled"; this.PromiseResult = data; }; const reject = data => { if (this.PromiseState !== "pending") return; this.PromiseState = "rejected"; this.PromiseResult = data; }; try { excutor(resolve, reject); } catch (e) { reject(e); } }
我们来看一下代码,现在我们在resolve
函数和reject
函数中都加了一行判断,在修改状态之前做一下判断,只要状态不是pending
那么我们直接中止函数执行。那么我们现在再来看一下刚才代码的结果:
现在正如我们预期一样输出的是一个成功的Promise
对象。到这一步我们便实现了Promise
的状态只能修改一次的规则。
then
方法调用回调函数
我们目前已经实现了很多属性和规则了,但是现在我们还有一个核心的功能并没有实现,就是then
方法,我们虽然实现了then
方法的基本结构,已经可以指定回调了,但是我们的回调还并没有被执行。那么接下来我们就来实现以下。
首先我们要弄清楚我们的回调是在哪里被执行的,首先如果Promise
执行器函数中如果执行的是同步任务,那么是先修改状态,然后再指定回调,紧接着马上执行回调,那么我们先来实现同步任务的回调执行,这个时候回调是在哪里执行的呢?是不是在then
方法中执行的?那么我们来修改一下代码:
Promise.prototype.then = (onResolved, onRejected) => { if (this.PromiseState === "fulfilled") { onResolved(this.PromiseResult); } if (this.PromiseState === "rejected") { onRejected(this.PromiseResult); } };
我们来看一下这段代码,我们用两个形参来接收回调函数,分别是成功时的回调和失败时的回调,但是我们能直接就在函数体中调用这两个函数吗?是不是不能?因为我们要根据状态来调用对应的回调。所以说我们根据状态来来做一个判断。那我们来看一下效果:
控制台中好像还是什么都没有输出,这是为什么呢?我们这里then
方法是不是一个箭头函数?箭头函数又自己的this
吗?是不是没有?如果箭头函数中要用this
关键字,会去箭头函数的上一层作用于来找this
,这个箭头函数的外层中的this
是什么呢?这里的this
其实是window
,所以说我们这里根本没有取到this.PromiseState
和this.PromiseResult
,那么我们修改一下代码:
Promise.prototype.then = function (onResolved, onRejected) { if (this.PromiseState === "fulfilled") { onResolved(this.PromiseResult); } if (this.PromiseState === "rejected") { onRejected(this.PromiseResult); } };
这次我们不用箭头函数,而是直接用匿名函数再来看一下结果:
这一次我们成功输出了我们的回调执行结果,那么我们让我们的Promise
对象是一个失败的Promise
再看一下结果:
也成功调用了我们指定的回调,那么这次我们再来抛出异常试一下:
这次我们也可以成功输出我们的回调执行结果。以上便是同步任务时then
方法执行回调的实现。
但是有一个问题,为什么我们用匿名函数可以呢?因为匿名函数是有自己的this
的,而我们then
方法是不是通过实例对象来调用的?所以说用匿名函数的this
就是实例对象自身,而PromiseState
和PromiseResult
属性就在实例对象上,那么就可以通过this
关键字取到。
异步任务回调执行
既然讲完了同步任务的回调执行,那么接下来肯定就是大家都好奇的异步任务回调的执行了,因为Promise
是解决异步任务的一个新方案,那么肯定用Promise
封装异步任务的使用场景更多。我们先来看当前状态:
let p = new Promise( (resolve, reject) => { setTimeout( () => resolve("OK"), 1000 ) } ); p.then( value => console.log(value), reason => console.warn(reason) );
我们还是用一个定时器来模拟一个异步任务,让Promise
对象的状态延迟 1 秒再改变,那么我们来看一下结果:
控制台什么都没有输出,这是为什么呢?按照同步任务的情况来说,应该会输出OK
才对啊。我们来分析一下,当我们实例化完Promise
对象的时候,状态改变了没有?是不是还没有,因为这是一个异步任务,要等 1 秒钟才会修改状态,但是p.then
方法会等异步任务执行完再调用吗?肯定不会的啊,那么这个时候就会直接指定回调函数,并且调用,可是这个时候Promise
的状态还没有发生改变,依然还是pending
,而我们在then
方法中只对fulfilled
和rejected
做了判断,所以导致then
方法并没有执行任何一个回调。
那么我们来思考一个问题,从Promise
的设计哲学来说,我们不论Promise
中开的是同步任务还是异步任务,我们执行回调的前提条件是不是Promise
的状态已经发生改变了?那么异步任务会在什么时候修改状态呢?是不是当异步任务执行完成并执行resolve
或者reject
函数的时候才会修改状态?
我们在讲同步任务的时候提到过,首先我们要弄清楚我们的回调是在哪里被执行的,异步任务完成后调用resolve
或者reject
函数修改状态然后调用回调,但是这个时候then
方法已经执行完了,所以说有没有一种可能异步任务的回调是在resolve
或者reject
函数中被执行的呢?比如这样:
function Promise(excutor) { this.PromiseState = "pending"; this.PromiseResult = null; const resolve = data => { if (this.PromiseState !== "pending") return; this.PromiseState = "fulfilled"; this.PromiseResult = data; onResolved(this.PromiseResult); }; ... }
但是,有一个问题,这个onResolved
和onRejected
都是传给then
方法的,我们的这个Promise
函数中的resolve
和reject
函数是不是根本就拿不到啊?那么我们是不是应该在then
方法上做一个手脚,当我们异步任务还没有完成,Promise
状态还没有改变的时候,then
方法已经被调用了,但是then
方法中并没有做对pending
状态的判断,那么我们就把这个判断加上,但是这个判断里面执行什么呢?我们先来看代码看大家能不能明白:
function Promise(excutor) { ... this.callback = {}; const resolve = data => { ... if (this.callback.onResolved) { this.callback.onResolved(this.PromiseResult); } }; const reject = data => { ... if (this.callback.onRejected) { this.callback.onRejected(this.PromiseResult); } }; ... } Promise.prototype.then = function (onResolved, onRejected) { ... if (this.PromiseState === "pending") { this.callback = { onResolved, onRejected }; } };
我们看见我们在Promise
函数中添加了一个属性叫callback
,这个属性是一个对象,然后我们在then
方法中判断如果是pending
状态的话就把我们传给then
方法的两个回调函数都保存到Promise
对象的callback
属性里,这样的我们在resolve
以及reject
函数中来判断callback
属性中的onResolved
与onRejected
是否为空,如果不为空就调用对应的回调函数,我们现在来看一下结果:
现在我们异步任务的then
以及回调函数的执行也完成了。
指定多个回调的实现
我们现在实现了同步任务和异步任务回调的执行,那么还有一个问题,我们如果要指定多个回调函数的话该怎么实现呢?首先我们现在肯定是不行的我们来看一下:
let p = new Promise( (resolve, reject) => { setTimeout( () => resolve("OK"), 1000 ) } ); p.then( value => console.log(value), reason => console.warn(reason) ); p.then( value => console.log(value, value), reason => console.warn(reason, reason) );
我们现在是这个异步任务,然后我们每个状态都指定了两个回调,那么我们看一下结果是什么:
首先回调执行了,但是好像只执行了第二次指定的回调,这是为什么呢?因为我们每次调用then
方法,如果状态为pending
,就都把回调都放在一个对象中并且存储到Promise
对象的callback
属性中,当我们第二个回调指定的时候,Promise
的状态还没有改变,所以第二次存储的时候覆盖了第一次存储的属性。那么这么看来我们then
方法方法就不太合理了。那么我们这么修改一下:
function Promise(excutor) { ... this.callbacks = []; const resolve = data => { ... this.callbacks.forEach(item => item.onResolved(this.PromiseResult)); }; const reject = data => { ... this.callbacks.forEach(item => item.onRejected(this.PromiseResult)); }; ... } Promise.prototype.then = function (onResolved, onRejected) { ... if (this.PromiseState === "pending") { this.callbacks.push({ onResolved, onRejected }); } };
我们来看一下代码我们把callback
属性改成了callbacks
,并且改成了数组,那么我们在then
方法中就不用赋值了,我们直接给push
到数组中就好了,那么我们在resolve
和reject
函数中就不能像原来那样调用回调了,我们要遍历数组来执行每一个回调。那么我们来看一下结果:
以上便实现了给异步任务指定多个回调。
可能有人要问了,那么同步任务不用再处理then
方法吗?我们来分析一下,同步任务是不是先改变状态然后再指定回调啊?那么根本不需要把回调存到对象属性中了啊。指定完直接就执行掉了。
本节课的内容暂时就先到这,其他的封装方面的内容我们在下节课再继续介绍。
Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.
Link of this article:https://work.lynchow.com/article/encapsulation_of_promise/