面试官问:来实现一个Promise

大家好,我是若川。最近组织了源码共读活动《1个月,200+人,一起读了4周源码》,已经有超50+人提交了笔记,群里已经有超1500人,感兴趣的可以点此链接扫码加我微信 ruochuan12 参与,一起学习,共同进步。

前言

Promise 作为异步编程的一种解决方案,在 ES6 中被标准化,提供了 Promise 对象和一系列的 API。在事件循环、链式调用、调度器实现等面试场景中均有涉及。在本文中笔者将从零实现一个符合 Promise/A+ 标准的 Promise 主体代码逻辑,并在后续系列文章中给出其他方法的实现以及常见的实际使用场景中的解法。

1、准备

本文按 Promise/A+[1] (中文版【翻译】Promises/A+规范[2]) 的标准实现,不熟悉的读者可以先看一遍,了解一些术语做些准备知识。

2、Promise 实现

本节,我们将进入正题,从零实现一个我们自己的 Promise。PS:在下文中,本文约定大写 Promise 指代我们实现的 MyPromise 函数对象,小写 promise 指代一个实例对象。

现在,我们实现的 MyPromise 函数第一版定义如下

function MyPromise(executor) {// TODO}

2.1、状态定义

Promise 只有三种状态:pending、fulfilled 和 rejected, 其中后两种是终态。

因此,我们可以先定义一个状态集合:

const PRO_STATUS = {PENDING: 'pending',FULFILLED: 'fulfilled',REJECTED: 'rejected'
}

2.2、状态转换及方式

promise 对象内部就像一个状态机,但是这个状态机有一点自己的限制条件,即, 它的状态变换路径只有两种:

pending-》fulfilled 
或者
pending -》rejected

并且,转换之后是固定的。Ok,在谈完状态转换的路径后,我们来看一下状态转换的方式。

在初始化 promise 对象时需要向构造函数提供一个 executor 函数,该函数有两个入参(函数类型):

•1、resolve,该函数接受一个参数,更改 promise 内部状态 pending-》fullfilled•2、reject,该函数接受一个 Error 类型参数,更改 promise 内部状态 pending -》rejected

至此,总结一下我们的 MyPromise 里应该有几个东西:

当前状态
fulfilled 状态下的 value 值
rejected 状态下的 reason 值
resolve 函数
reject 函数

那么,MyPromise 第二版现在是如下的样子:

let count = 0
function MyPromise(executor) {const self = thisself.status = PRO_STATUS.PENDINGself.count = ++countself.fulfilledValue = undefinedself.rejectedReason = undefinedtry {executor(resolve, reject)} catch (error) {reject(error)}function resolve(rs) {//TODO}function reject(err){// TODO}
}

这里,我们为每个实例追加了一个 count 计数,读者可以忽略。

实例化函数后,我们直接执行了 executor 函数,并传入了两个函数类型的参数。在 executor 函数内部,用户可以通过 resolve 或者 reject 修改 promise 对象进入终态,并且只能进入一次,举个例子:

new MyPromise((resolve, reject)=>{//balabala.......resolve(1) reject(new Error('error'))resolve(2)
})

这里写了三行修改 promise 状态的代码,但是最后 promise 的状态是 fulfilled,并且 fulfilledValue 是 1。这个我们在后面 resolve 和 reject 实现中说。

2.3、then 和 catch 方法

我们知道,Promise 对象实现了链式调用来解决回调地狱的问题。类似这样:

new Promise(()=>{....
})
.then(rs=>{...
})
.then(rs=>{})
.catch(err=>{})

也就是说,我们可以在 then 或 catch 中拿到 promise 对象的终态数据并通过生成新的 promise 对象向下传递。

首先,我们来看看 then 方法。

then 方法接受两个函数类型的参数:onfulfilled 和 onrejected。onfulfilled 接受 promise 的 fulfilledValue 作为入参并在 promise 为 fulfilled 状态时被调用, onrejected 接受 promise 的 rejectedReason 作为入参并在 promise 为 rejected 状态时被调用。

const onfulfilled = value =>{...}
const onrejected = reason =>{...}
promiseA.then(onfulfilled, onrejected)

此外,then 方法将返回一个新的 Promise 类型对象。

相比 then方法,catch 方法仅接受一个 onrejected 函数类型的参数。和 then 方法一样将返回一个新的 Promise 类型对象。

const onrejected = reason =>{...}
promiseA.catch(onrejected)

实际上,then 和 catch 方法有几个作用:

•为 promise 对象收集 onfulfilled 和 onrejected 回调函数,在终态后(resolve 和 reject 函数触发)进行回调的调用•触发 onfulfilled 和 onrejected 回调函数

其实第一个比较好理解,第二个可以用下面一个代码去解释。

let promise = new Promise((resolve)=>{setTimeout(()=>{resolve()promise.then(rs=>{console.log(2)}) // then2})
}).then(rs=>{console.log(1)}) // then1

rs=>{console.log(1)} 回调通过 then1 收集,在 resolve 调用后被触发。此时 promise 对象进入终态, rs=>{console.log(2)} 回调通过 then2 收集并触发执行。

并且,这些回调函数只会被调用一次。

综上,我们可以总结如下:

•MyPromise 内部 resolve、reject 函数以及 then、catch 都可能会触发回调函数执行,那么他们可能在代码链路上交汇在某个执行点,也就是说他们调用了同一个处理函数,我们定义为 _handle 函数。•此外,Promise 函数内部有一个数据结构维护当前的回调函数,这里我们需要一个队列。•最后,如果我们有 promise A 对象,promise A 对象的 then 和 catch 方法都会返回一个新的 promise B 实例,A 内部状态是 fullfilled,它只调用 onfulfilled 方法。此外,promise A 进入终态才会使得 promise B 进入终态,关键点在于 A 持有 B 的 resolve、reject,A 进入终态后调用 B 的 resolve/reject,具体调用 resolve 还是 reject 以及入参要分情况区别。

Ok,有了上面的结论,我们继续修改已有代码:

const TYPES = {THEN: 'then',CATCH: 'catch',FINALLY: 'finally'
}
...
function MyPromise(executor) {const self = this...self.cbQueue = [] // 保存回调等数据...function resolve(rs) {self._handle()}function reject(err){self._handle()}
}MyPromise.prototype._handle = function(cb){// TODO
}/*** * @param {*} onfulfilled * @param {*} onrejected * @returns */
MyPromise.prototype.then = function(onfulfilled, onrejected) {return new Promise((resolve, reject) => {this._handle({type: TYPES.THEN,resolve,reject,onfulfilled,onrejected})})
}/*** * @param {*} onrejected * @returns */
MyPromise.prototype.catch = function(onrejected) {return new Promise((resolve, reject) => {this._handle({type: TYPES.CATCH,resolve,reject,onrejected})})
}

上面的代码里,我们还定义了一个 TYPES 来指定回调函数是通过 then、catch还是 finally 方法收集的,以此来辅助我们在 _handle 函数中的处理。

现在关键来到了 _handle 函数。我们根据 Promise/A+ 的标准和实际代码使用中,对于细节进行了归结。

•1、A.resolve() 情况下,B 不管是通过 then 还是 catch 产生, 都要调用 B.resolve(),入参要看是否提供了 onfulfilled,具体如下:

    1)如果 B 是 A.then 生成,则 B.resolve(onfulfilled(A.fulfilledValue))

    2)如果 B 是 A.catch 生成,则 B.resolve(A.fulfilledValue),不会调用 A.catch 提供的 onrejected 方法 如果不提供 onfulfilled 则 B.resolve(A.fulfilledValue)

•2、注意,A.reject() 的情况下,如果有 onrejected 函数处理则状态发生转换,并且入参要看是否提供了 onrejected 函数进行包装,具体如下:

    1)A 调用 reject,B 是 A.then 生成,则 B.reject(A.rejectedReason) 或者 B.resolve(onrejected(A.rejectedReason))

    2)A 调用 reject,B 是 A.catch 生成,则 B.resolve(onrejected(A.rejectedReason)) 如果不提供 onrejected 则 B.reject(A.rejectedReason)

    •    3、如果 A.fulfilledValue 是一个 Promise 类型,则要把 A.then() 这些收集到的回调给 A.fulfilledValue

针对 3,可以看如下示例代码:

function delay(){new Promise((resolve, reject)=>{// promise 0// console.log('0')// resolve('resolve')reject(new Error('reject'))}).then(rs=>{return new Promise(resolve=>{setTimeout(()=>{console.log('1')resolve('inner rs')}, 2000)})}).catch(err=>{ // promise 1return new Promise(resolve=>{ // promise 2setTimeout(()=>{console.log('1')resolve('inner err')}, 2000)})}).then(rs=>{console.log('2')return 'then2'})
}
最后打印:
(注:先延迟2s)
1 
2

最后的 then 会被 promise1 收集到,因为 promise1 的 fulfilledValue 是一个 Promise 类型对象,即 promise2。要实现延迟 2s 打印 1 后再打印 2,需要把 promise1 收集到的回调赋给 promise2。

Ok,了解了处理逻辑,我们就可以直接上代码了。

MyPromise.prototype._handle = function(cb){if(cb) {// then、catch、finally 方法处理this.cbQueue.push(cb)}else{// resolve、reject 处理}if(this.status === PRO_STATUS.PENDING){// nothing to do} else {for (let i = 0; i < this.cbQueue.length; i++) {const cb = this.cbQueue[i];const { type, resolve, reject, onfulfilled, onrejected, onfinally } = cb// finallyif(type === TYPES.FINALLY){onfinally()resolve()continue}if(this.status === PRO_STATUS.FULFILLED){//if(typeof resolve === 'function'){let fulfilledValue = this.fulfilledValuelet ans = fulfilledValueif(fulfilledValue instanceof MyPromise){// 收集的回调赋给 fulfilledValuefulfilledValue.cbQueue = this.cbQueuethis.cbQueue = []continue}if(typeof onfulfilled === 'function'){// 这里要处理一下数据ans = onfulfilled(fulfilledValue)if(ans instanceof MyPromise && ans.status !== PRO_STATUS.PENDING){if(ans.status === PRO_STATUS.FULFILLED){resolve(ans.fulfilledValue)}else{reject(ans.rejectedReason)}}else{resolve(ans)}}else{resolve(ans)}}}else{if(typeof resolve === 'function'){let ans = this.fulfilledValueif(typeof onrejected === 'function'){/*** 这个地方要注意下,上面 setTimeout模拟异步的地方,修改状态的部分要放在 setTimeout 外面。* 否则到这里, status 还是 pending*/ans = onrejected(this.rejectedReason)if(ans instanceof MyPromise && ans.status !== PRO_STATUS.PENDING){if(ans.status === PRO_STATUS.FULFILLED){resolve(ans.fulfilledValue)}else{reject(ans.rejectedReason)}}else{resolve(ans)}}else{reject(this.rejectedReason)}}}// })}this.cbQueue = []}
}

_handle 函数先对进来的参数进行判断,有的话就入队列。然后看当前的状态,是终态就处理上面的逻辑,最终清空队列。否则就直接退出。PS:这里缺少了 finally 方法的处理代码,我们在后面补上。此外还有就是关于异常的抛出问题,当 promise A 对象进入 rejected 状态,此时,如果 promise.then 未提供 onrejected,则会抛出 error; 如果提供 onrejected,则不会,也就是有的资料中提到的 error 被“吃掉”了,这部分功能并未实现。

2.4、resolve 和 reject 实现

好了,到了这里基本上完成了大部分的工作了,但是还缺少了 resolve 和 reject 部分的代码实现。无论是 resolve 还是 reject 函数,他们的功能都是两个部分:

•修改状态•触发 onfulfilled/onrejected 回调(如果有的话)

我们都知道,Promise 属于异步任务里的微任务,在构造函数里的代码和 onfulfilled/onrejected 里的代码都运行在主线程中。因此,我们需要模拟一个异步的过程,并且在定义多个 Promise 对象实例时保证一个时序,这里我们用 setTimeout,并在 setTimeout 中调用 _handle 函数。好的,我们来看 resolve 函数的具体实现。

function resolve(rs) {if(self.status === PRO_STATUS.PENDING) {/*** 在主线程修改状态*/self.status = PRO_STATUS.FULFILLEDself.fulfilledValue = rssetTimeout(() => { /*** 模拟异步,但是这里有一个bug,setTimeout 并不准确,* 在 Promise.race 中有问题,主要是 setTimeout 执行间隔*/self._handle()})}
}

可以看到,首先判断了当前状态,确保只能第一个 resolve/reject 方法里面的代码被执行去把 pending 状态修改为终态,并且是在主线程中修改了状态。另外注释里标明了一个问题,我们使用了 setTimeout 去模拟异步,但是因为它本身延迟执行的特性,会带来一些问题,比如下面的测试代码:

const Promise = MyPromise
function race(){Promise.race([new Promise(resolve=>{setTimeout(()=>{resolve(1)}, 200)// 这里有一个bug?超时改成20看看}),new Promise((resolve, reject)=>{setTimeout(()=>{// resolve(2)reject(new Error('timeout'))}, 10)})]).then(res=>{log2('race res', {res})},err=>{log2('race err', {err})})
}

在注释处修改为 20 会产生意外的效果。比照 resolve 函数的实现,我们可以很容易给出 reject 函数的代码实现:

function reject(err){if(self.status === PRO_STATUS.PENDING) {self.status = PRO_STATUS.REJECTEDself.rejectedReason = errsetTimeout(() => {self._handle()})}
}

3、结语

通过本文的介绍,我们得到了一个 Promise 实现。下一节中,我们介绍一些 API 的实现。

References

[1] Promise/A+: https://promisesaplus.com/
[2] 【翻译】Promises/A+规范: https://www.ituring.com.cn/article/66566

最近组建了一个江西人的前端交流群,如果你是江西人可以加我微信 ruochuan12 私信 江西 拉你进群。


推荐阅读

1个月,200+人,一起读了4周源码
我历时3年才写了10余篇源码文章,但收获了100w+阅读

老姚浅谈:怎么学JavaScript?

我在阿里招前端,该怎么帮你(可进面试群)

fcce66a6d72f4df0ab5ffe4a9179fd5f.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动

ca26810f7a6572b0c20305fd065415d2.png

识别方二维码加我微信、拉你进源码共读

今日话题

略。欢迎分享、收藏、点赞、在看我的公众号文章~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/275339.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

奇迹暖暖服务器不稳定,闪耀暖暖用土豆当服务器?开服仅半小时就崩溃,无数玩家疯狂吐槽...

大家好&#xff0c;这里是正惊游戏&#xff0c;我是你们的正惊小弟。继奇迹暖暖之后&#xff0c;叠纸游戏的3D换装类游戏《闪耀暖暖》于昨天正式开启了全平台公测。就在大家想要上游戏给女儿买好看的衣服时&#xff0c;发现游戏的服务器崩了&#xff0c;谁都登录不上去&#xf…

nda协议_如何将NDA项目添加到您的投资组合

nda协议Being on the job hunt meant I needed to update my portfolio again. I had a new project to add, but it was under an NDA and I couldn’t say too much about it. Since I’ve never had to figure out how to display an NDA project on my portfolio before, I…

程序员一定会有35岁危机吗?

大家好&#xff0c;我是若川。最近组织了源码共读活动《1个月&#xff0c;200人&#xff0c;一起读了4周源码》&#xff0c;已经有超50人提交了笔记&#xff0c;群里已经有超1500人&#xff0c;感兴趣的可以点此链接扫码加我微信 ruochuan12你好&#xff0c;我是黄老师。最近经…

hdu 2141 Can you find it? hdu1597 find the nth digit

hdu2141 唉&#xff0c;是我 想多了&#xff0c;用普通方法拼命剪枝&#xff0c;还是TLE 直接将前俩个数组的和求出来并保存&#xff0c;之后就是一个二分查找的过程了 二分的俩种写法 第一种 #include<iostream>#include<algorithm>#include<string>using …

网页开发环境的重要性_少即是多:极简方法在网页设计中的重要性

网页开发环境的重要性Written by Alan Smith由艾伦史密斯 ( Alan Smith)撰写 Minimalism has been an increasingly popular trend in the web design world. Designers may be tempted by bolder, feature-rich design because it might seem like the best way to engage us…

聊聊前端八股文?

大家好&#xff0c;我是若川&#xff0c;点此加我微信进源码群&#xff0c;一起学习源码。同时可以进群免费看Vue专场直播&#xff0c;有尤雨溪分享「Vue3 生态现状以及展望」前些天&#xff0c;我看到《剑指前端offer》一系列文章&#xff0c;被前言部分图示和文章内容惊艳到。…

服务器内存型号与频率,一张图看懂如何选择DDR4内存的频率和容量

Intel发布了代号为Skylake的第六代酷睿处理器&#xff0c;与此同时各大主板厂商也迅速推出基于100系列芯片组的各型号主板以迎接Skylake处理器&#xff0c;分别有Z170、H170及B150三个不同级别的芯片组。那针对着不同芯片组主板&#xff0c;如何选择DDR4内存的频率和容量&#…

Promise 到底是什么?看这个小故事

大家好&#xff0c;我是若川&#xff0c;点此加我微信进源码群&#xff0c;一起学习源码。还可以进《剑指前端offer》交流群。另外&#xff0c;可以进群免费看下周六Vue专场直播&#xff0c;有尤雨溪分享「Vue3 生态现状以及展望」如果你还是一个 JavaScript 初学者&#xff0c…

Vue 团队公开快如闪电的全新脚手架工具,未来将替代 Vue-CLI,才300余行代码,学它!...

1. 前言大家好&#xff0c;我是若川。欢迎关注我的公众号若川视野源码共读活动ruochuan12想学源码&#xff0c;极力推荐之前我写的《学习源码整体架构系列》jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue-next-release…

斑马无线打印服务器,如何设置斑马打印机无线WiFi

安装Zebra Setup Utilities.exe&#xff0c;打开软件(没有该软件的可以向客服索要)界面如果是英文请选择options(选项)&#xff0c;选择应用程序语言Simplified Chinese(简体中文)点击确定&#xff0c;关闭软件&#xff0c;重新打开&#xff0c;界面就会显示中文。点击相应的打…

Python自然语言处理学习笔记(19):3.3 使用Unicode进行文字处理

3.3 Text Processing with Unicode 使用Unicode进行文字处理 Our programs will often need to deal with different languages, and different character sets. The concept of “plain text” is a fiction&#xff08;虚构&#xff09;. If you live in the English-speakin…

小程序卡片叠层切换卡片_现在,卡片和清单在哪里?

小程序卡片叠层切换卡片重点 (Top highlight)介绍 (Intro) I was recently tasked to redesign the results of the following filters:我最近受命重新设计以下过滤器的结果&#xff1a; Filtered results for users (creatives) 用户的筛选结果(创意) 2. Filtered results fo…

效率神器!UI 稿智能转换成前端代码

做前端&#xff0c;不搬砖大家好&#xff0c;我是若川。从事前端五年之久&#xff0c;也算见证了前端数次变革&#xff0c;从到DW&#xff08;Dreamweaver&#xff09;到H5C3、从JQuery到MVC框架&#xff0c;无数前端大佬在为打造前端完整生态做出努力&#xff0c;由于他们的努…

$.when.apply_When2Meet vs.LettuceMeet:UI和美学方面的案例研究

$.when.apply并非所有计划应用程序都是一样创建的。 (Not all scheduling apps are created equal.) As any college student will tell you, we use When2Meet almost religiously. Between classes, extracurriculars, work, and simply living, When2Meet is the scheduling…

前端不容你亵渎

大家好&#xff0c;我是若川&#xff0c;点此加我微信进源码群&#xff0c;一起学习源码。同时可以进群免费看Vue专场直播&#xff0c;有尤雨溪分享「Vue3 生态现状以及展望」背景最近我在公众号的后台收到一条留言&#xff1a;言语里充满了对前端的不屑和鄙夷&#xff0c;但仔…

利益相关者软件工程_如何向利益相关者解释用户体验的重要性

利益相关者软件工程With the ever increasing popularity of user experience (UX) design there is a growing need for good designers. However, there’s a problem for designers here as well. How can you show the importance of UX to your stakeholders and convince…

云栖大会上,阿里巴巴重磅发布前端知识图谱!

大家好&#xff0c;我是若川&#xff0c;点此加我微信进源码群&#xff0c;一起学习源码。同时可以进群免费看Vue专场直播&#xff0c;有尤雨溪分享「Vue3 生态现状以及展望」阿里巴巴前端知识图谱&#xff0c;由大阿里众多前端技术专家团历经1年时间精心整理&#xff0c;从 初…

Linux下“/”和“~”的区别

在linux中&#xff0c;”/“代表根目录&#xff0c;”~“是代表目录。Linux存储是以挂载的方式&#xff0c;相当于是树状的&#xff0c;源头就是”/“&#xff0c;也就是根目录。 而每个用户都有”家“目录&#xff0c;也就是用户的个人目录&#xff0c;比如root用户的”家“目…

在当今移动互联网时代_谁在提供当今最好的电子邮件体验?

在当今移动互联网时代Hey, a new email service from the makers of Basecamp was recently launched. The Verge calls it a “genuinely original take on messaging”, and it indeed features some refreshing ideas for the sometimes painful exercise we call inbox man…

React 全新文档上线!

大家好&#xff0c;我是若川&#xff0c;点此加我微信进源码群&#xff0c;一起学习源码。同时可以进群免费看明天的Vue专场直播&#xff0c;有尤雨溪分享「Vue3 生态现状以及展望」&#xff0c;还可以领取50场录播视频和PPT。React 官方文档改版耗时 1 年&#xff0c;今天已完…