前言: 该来的还是来了,经过完成我们前面的四个支线任务
终于到了我们的主线任务 ---《手写 Promise》。对于前端来讲,当你完全理解 Promise 的设计思路,那么你会对你之前所了解的 JS 世界,有一个全新的认知。
不知各位是否有过下面这样的想法:
--“我感觉我现在知识储备差不多了,接下来如何进阶呢?”
其实这个问题谁都无法告诉你准确答案。只有当你在某一天掌握了你之前不知道的知识点,并且完全领悟它的时候。你脑子里就会突然有种 “知识升华” 的感觉。(我也无法准确描述那种感觉,就好像突然悟了一样)脑子里会瞬间把之前所有知识串联起来,各个知识点不再是一个一个的碎片。
这种感觉就是在我某一天读懂 JS 回调函数真正想表达出的思想,并且手写出 Promise 涌现的。当你某天有过一次这样感觉后,你就不会再去问别人关于如何进阶这种问题了,因为那时候的你,其实已经完成了当前知识的进阶,当你回过头看之前的代码时,你会发现不一样的世界,是那种拨云见日的感觉。
⚠️注意:本文的内容需要你对回调函数和宏任务微任务有比较清晰的认识,请不太懂的小伙伴在家长的陪同下认真观看
一. 文件准备
本文需要你准备的文件非常简单,随便在你的目录文件夹下创建一个 myPromise.ts
文件,对 .ts
不太熟悉的小伙伴不需要担心,本篇用到 ts
的相关内容很少。
二. 实现 MyPromise类 的构造器函数
- 首先我们定义一个叫做
MyPromise
的类。在接下来我会顺着原生的 Promise 一步一步帮你去理解 Promise 的实现思想。 - 在上一章内容我们了解了,Promise 类会接收一个叫做
executor
的函数来初始化我们的 Promise 类的实例。
ok,并且我们还特别强调了,executor
就是一个普普通通的函数而已。那么我们就可以像下面这样,在MyPromise
的constructor
函数内定义一个形参,来准备接收在初始化时传递给我们的实参数。
我相信聪明的你一定可以看出来,我们其实在new
一个 Promise 实例的时候,传递给Promise
类的函数就是我们刚刚定义的这个executor
函数的实参。 - OK,我们继续。我们还知道 executor 函数会被传递两个参数。这两个参数分别是一个叫做
resolve
和reject
的函数!注意,它们两个是函数!!所以我们就可以像下面这样写。
好像飘红了?是的,因为在MyPromise
结构体内,还没有这两个函数。那怎么办呢?,这还不简单?没有我们就自己造呗~ - 可以看到我们自己定义了两个函数,好像还是飘红...
这里就需要用到 this 关键词,来告诉executor
我要传递的是类本身的方法。所以正确的方法应该下面这样。
至此,我们的 executor 函数就已经写好了。
三. 实现 resolve,reject 函数
- 这里我们先实现
resolve
,别着急。我们先看原生Promise
是怎么使用的。
我们可以得知,resolve 函数可以被传递一个参数,所以我们可以更进一步得出。
我们先来测试一下我们的思路是不是对的。我们先new
一个实例保存一个数据看看。
嗯...看来有点那味道了。 - 同理
reject
函数也是这种写法。到这一步,我想你的MyPromise
类应该长这个样子。
四.实现 then 方法
- 我们先回忆一下,原生的
Promise
读取数据的时候,是在实例的then
方法上读取的。这里我们就需要提供一个变量去接收resolve
传递的值。并且需要在MyPromise
类中提供一个then
方法,去读取传递过来的数据。 - 这里先停一下,我们想一想。我们的
result
是不希望被实例引用的。什么意思呢?如果我们按照上面的写法,是会引发这样的错误的。
实例竟然可以直接去引用这个 result ,这是我们不希望看到的。
这里需要用到的知识是:假如我们不希望实例调用某个属性,方法也很简单,只需要在前面加上一个#
号即可。 - 接下来是本文的第一个重点。
then
函数该怎么设计?我们先看原生Promise
实例的身上.then
方法是怎么使用的。
由之前的知识我们可以知道,then
方法也是接受两个回调函数作为参数的。并且第一个回调函数的参数会被传递resolve
保存的值,第二个回调函数的参数会被传递reject
保存的值。 - ok,那我们先不考虑那么多,直接先给
then
函数传两个参数,这两个参数也是两个函数。第一个参数我们就起一个叫onFulfilled
的函数 ,它对应着resolve
保存的值。第二个参数我们就叫onRejected
吧,它也应该是一个函数。于是我们就可以补充then
函数的内容,它会被传递我们通过reject
保存的那个结果。
下面的代码应该是你目前写出来的样子。
五. MyPromise 的三种状态
- 等等,我们好像忘了一个很关键的东西,Promise 的三种状态!!!还记得吗?Promise 在初始化的时候是有三个状态的,分别是
pending
,fulfilled
和rejected
。这三种状态分别影响着then
函数中我们取到的值。 - 知道这一点,我们马上就应该想到,我需要再定义一个变量,来存放这三个状态值。
并且还有一点,pending
状态是会在初始化,也就是还没调用resolve
或者reject
函数之前的状态。
所以我们就需要在constructor
构造函数调用executor
之前将state
的状态信息改为pending
,对应就是下面这行代码的意义。 - 并且我们可以推算出,在
resolve
函数内,和reject
函数内需要分别修改状态值为fulfilled
和rejected
,就如同下面的写法一样。 - 由于我们
then
函数会根据状态去做相应传递的参数不同,所以理所当然的需要修改为下面的写法。 - 下面应该是你目前的代码。
六. 创造一个 MyPromise 实例
目前看起来好像我们的逻辑非常严谨对吧,我们别着急往下写,我们自己去调用一下看看是什么样子。我们先自己
new MyPromise
生成一个实例看看。
按照我们的推算,控制台应该会出入一个字符串韩振方
。
我们看一下结果:
什么情况?竟然报错了?别着急,我们分析一下错误。Uncaught TypeError: Cannot set properties of undefined (setting '#result')
这里给出的错误信息是,我们不能给 undefined 设置
#result
的属性。
怎么回事啊?我们怎么在给undefined
设置属性?我们不是在给MyPromise
实例设置属性吗?我还就不信了,我非得打印一下看看,我们在resolve
里面打印一下 this看看到底这个 this 是谁 。
看一下控制台:
emm...好像还真是 undefined。- 到底怎么回事呢? 其关键点在于下面这段代码。
在这里我们看似在引用实例自身去调用resolve
,实际上在这里调用者的其实是window
对象。不过因为我们在 Class 类里面是默认开启严格模式的,如果丢失了 this ,并不会将 this 默认指向全局对象window
。 - 所以我们需要干一件非常非常重要的事情,没错,就是绑定 this。
因为我们 constructor 函数里的this
总是会指向实例本身,所以我们需要在 调用resolve
函数和reject
函数之前,需要在excutor
的参数提前绑定好this
才可以。所以我们现在的代码应该是这个样子: - 让我们再次调用一下我们自己的 MyPromise ,来看一下现在能不能成功读取我们保存的数据。
可以看到,我们已经成功读取了我们通过then
方法读取到了resolve
传递过来的数据。 - 到这里你可能好奇,为什么
then
方法不用绑定this
。因为你别忘了我们的then
是怎么去调用的。
可以看到,我们的then
是通过对象点一个属性名去调用的,那么它的 this 百分百就是 data 实例对象。它是不需要我们考虑 this 指向问题的。
总结
在本文主要引导大家构造好一个 Promise 的骨架,大家可以先消化一下,在下一篇内容我会更深入的讲解其中的细节。