面试官:能不能手写一个 Promise?

大家好,我是若川。最近组织了源码共读活动,感兴趣的可以点此加我微信ruochuan12 进群参与,每周大家一起学习200行左右的源码,共同进步。已进行4个月了,很多小伙伴表示收获颇丰。

d2cb0a1998fab92ad2a1655fc49a4002.png以下问题你是不是在哪里听过?

  1. 你知道什么是 Promise 吗?它是干什么用的呢?

  2. 那你知道 Promise 有哪些方法吗?如何使用呢?

  3. Promise 的 then 方法(或者 catch 方法)是怎么实现的呢?

  4. 能手写一个 Promise 吗?

  5. Promise 和 async/await 的区别是什么?

  6. Promise 中有异步任务(例如 setTimeout 等)的执行顺序是什么样的呢?

为什么面试过程中 Promise 出现的频率这么高呢?

异步编程是 JavaScript 中的一个核心概念,与其他脚本编程语言相比,异步编程是一项让 JavaScript 速度更快的特性。JavaScript 是单线程的,这意味着它逐行执行程序。它也是异步的,这意味着如果我们的程序执行到达一个必须等待结果的代码块,它将继续经过这个正在等待的代码块,因此程序不会冻结执行,并且一旦该异步任务完成,我们的代码将通过使用回调来处理它正在等待的结果。如果回调太多,嵌套太深,Promise 确实可以解决这一痛点。其实上面的问题如果动手写过一次源码,基本就是都清楚了

接下来就根据 Promise 的特性来实现一下

大体结构如下:

8afea7c0769f32aa3c31ca380bf17e9d.png
Promise1

第一步是需要根据使用实现构造函数;

第二步是实现原型方法 then,then 是核心逻辑,其他的方法都是对 then 方法的使用和完善;

下面我们就来一步步看看这个 Promise 的实现。

一、介绍 Promise

Promise 是 ES6 中进行异步编程的新解决方案(相对于单纯使用回调函数),具有三种状态:pending、rejected、resolved,状态的修改只能是 pending 到 rejected 或者 pending 到 resolved,且状态是不可逆的。它的使用这里就不多说啦,大致结构如下:

const p = new Promise((resolve, reject) => {resolve("success");
});
p.then((value) => {console.log("成功", value);},(reason) => {console.log("失败", reason);}
).catch((error) => {console.log("错误", error);
});

then 方法中有成功和失败的回调,catch 是捕获整个过程中产生的错误。

在这里需要注意一个问题,如果resolve("success"); 是在一个异步中,例如定时器,then 方法并不是在定时器结束才绑定,而是直接绑定的,只不过成功和失败的回调是在状态修改以后才调用的,这个很重要,封装 then 方法的时候需要实现这一逻辑。

它的方法分为原型方法和构造函数方法,then 和 catch 为原型上的方法,即实例上可调用的方法,其它为构造函数的方法。现有的方法和解释给大家都列出来啦!

  • Promise.prototype.then 方法: (onResolved, onRejected) => {} (1) onResolved 函数: 成功的回调函数 (value) => {} (2) onRejected 函数: 失败的回调函数 (reason) => {} 说明: 指定用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调 返回一个新的 promise 对象

  • Promise.prototype.catch 方法: (onRejected) => {} (1) onRejected 函数: 失败的回调函数 (reason) => {} 说明: then()的语法糖, 相当于: then(undefined, onRejected)

  • Promise.resolve 方法: (value) => {} (1) value: 成功的数据或 promise 对象 说明: 返回一个成功/失败的 promise 对象

  • Promise.reject 方法: (reason) => {} (1) reason: 失败的原因 说明: 返回一个失败的 promise 对象

  • Promise.all 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明: 返回一个新的 promise, 接收一个 Promise 对象的集合,只有所有的 promise 都成功才成功, 只要有一个失败了就 直接失败

  • Promise.race 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明: 返回一个新的 promise, 接收一个 Promise 对象的集合,第一个完成的 promise 的结果状态就是最终的结果状态

  • Promise.any 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明: 返回一个新的 promise, 接收一个 Promise 对象的集合,当其中的一个 promise 成功,就返回那个成功的 promise 的值,如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和 AggregateError 类型的实例。

  • Promise.allSettled 方法: (promises) => {} (1) promises: 包含 n 个 promise 的数组 说明:方法返回一个在所有给定的 promise 都已经fulfilledrejected后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。该方法为 ES2020 新增的特性,它能够返回所有任务的结果。

二、封装 Promise

根据 Promise 的使用可以确定需要封装的整体结构如下:

// 构造函数
function Promise(executor) {function resolve(data) {}function reject(data) {}executor(resolve, reject);
}//then方法
Promise.prototype.then = function (onResolved, onRejected) {};//catch方法
Promise.prototype.catch = function () {};Promise.resolve = function () {};
Promise.reject = function () {};
Promise.race = function () {};
Promise.any = function () {};
Promise.all = function () {};
Promise.allSettled = function () {};

new Promise()有一个回调函数需要实现,且回调函数需要有两个参数,所以构造函数需要有一个参数executor

Promise 构造方法的实现如下:

// 构造函数
function Promise(executor) {this.promiseState = "pending";this.primiseResult = null;// 保存then的回调函数,使用数组主要是为了链式调用的场景,多个then方法的回调this.callbacks = [];const self = this;/*** 改变状态的三种方式* 1、resolve* 2、reject* 3、throw*/function resolve(data) {// 保证状态只能修改一次if (self.promiseState !== "pending") return;// 修改对象状态self.promiseState = "fulfilled";// 设置对象结果值self.primiseResult = data;// then方法的回调函数异步执行setTimeout(() => {// 状态改变触发回调函数的执行self.callbacks.forEach((item) => {item.onResolved(data);});});}function reject(data) {// 保证状态只能修改一次if (self.promiseState !== "pending") return;// 修改对象状态self.promiseState = "rejected";// 设置对象结果值self.primiseResult = data;// then方法的回调函数异步执行setTimeout(() => {// 状态改变触发回调函数的执行self.callbacks.forEach((item) => {item.onRejected(data);});});}// throw要改变状态 通过try...catch...try {executor(resolve, reject);} catch (e) {//catch方法的实现reject(e);}
}

改变 Promise 状态的三种方式:

  1. resolve()

  2. reject()

  3. throw() 通过 try...catch...实现

上面的代码中兼容了对上面三种方法的处理,Promise 状态只能修改一次且不可逆,如果调用了 resolve(),然后再调用 reject(),只会执行前者,后者不执行;那么如何实现状态的不可逆修改呢?通过判断状态if(self.promiseState !== 'pending') return; 即保证每次都是从 pending 修改状态到失败或者成功。

new 完以后需要通过实例方法调用 then 和 catch 方法,所以下面是这两个方法的实现:

//then方法
Promise.prototype.then = function (onResolved, onRejected) {const self = this;// 【异常穿透】如果没有写失败的回调,这里需要补充上,并抛出一个错误if (typeof onRejected !== "function") {onRejected = (reason) => {throw reason;};}// 【值传递】if (typeof onResolved !== "function") {onResolved = (value) => value;}return new Promise((resolve, reject) => {function callback(type) {// 获取then回调函数的执行结果try {const result = type(self.primiseResult);if (result instanceof Promise) {// 返回结果是Promiseresult.then((v) => {resolve(v);},(r) => {reject(r);});} else {resolve(result);}} catch (e) {reject(e);}}if (this.promiseState === "fulfilled") {// then方法的回调函数异步执行setTimeout(() => {callback(onResolved);});}if (this.promiseState === "rejected") {// then方法的回调函数异步执行setTimeout(() => {callback(onRejected);});}// 异步处理,状态没有变更if (this.promiseState === "pending") {this.callbacks.push({onResolved: function () {callback(onResolved);},onRejected: function () {callback(onRejected);},});}});
};

then 方法中需要判断 pending 的情况,主要是因为状态变更有异步的可能,需要先存储 then 的回调函数,方便状态修改以后调用,将所有的异步回调存储到callbacks,由于会有多个 then 方法链式调用,所以 callbacks 是数组,用于保存多个回调,且 then 方法的回调函数不是同步执行的,所以需要通过 setTimeout 放入另一个队列;

链式调用,涉及到 then 方法的返回,返回值必须是个 Promise 才能实现链式调用;成功的回调函数返回的结果也可能是 Promise;成功的回调函数返回的结果 考虑到 throw 的情况,还是要使用 try...catch...;中断 promise,返回一个 pending 状态的 promise;

// catch方法
// 需要处理异常穿透
Promise.prototype.catch = function (onRejected) {return this.then(undefined, onRejected);
};

catch 方法及异常穿透 catch 方法的功能 then 已经实现了,直接使用就可以,只是没有成功的处理函数;then 方法中没有写失败的回调函数,会默认添加一个失败的回调函数并抛出异常,最后统一由 catch 处理异常。

值传递:第一个回调函数不传也可以,我们会在 then 方法处理这种情况,如果检测到没有这个方法,就自动添加这个方法。

接下来是对构造函数的实现,之所以在 then 方法后面现实是因为下面这些方法的实现是基于上面的实现。resolve 方法快速创建 promise 对象的实现,所以可以直接调用封装好的 Promise,以下的方法基本都是对上面方法的使用

// resolve方法 作用: 快速创建promise对象
Promise.resolve = function (value) {return new Promise((resolve, reject) => {if (value instanceof Promise) {value.then((v) => {resolve(v);},(r) => {reject(r);});} else {resolve(value);}});
};

接下来是 reject 方法的实现,传入什么都是返回失败,也是调用现有的方法,直接返回将状态修改为失败:

Promise.reject = function (value) {return new Promise((resolve, reject) => {reject(value);});
};

race 方法无论成功失败,只要最先返回的结果,只要有结果就返回:

Promise.race = function (promises) {return new Promise((resolve, reject) => {for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {// 最先返回的改变状态resolve(v);},(r) => {reject(r);});}});
};

all 方法的实现:其中一个 Promise 成功的时候不可以改变状态,只有全部成功才能改变状态;实现是使用一个计数器,当数量和promises数量相同,且都成功了,就返回所有结果,失败直接改变状态结

// all方法
Promise.all = function (promises) {return new Promise((resolve, reject) => {let count = 0;let arr = [];for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {// 根据all的定义,不可以直接改变状态count++;//不用push是为了保证输出的顺序正常一一对应arr[i] = v;if (count === promises.length) {resolve(arr);}},(r) => {reject(r);});}});
};

any 方法实现:其中的一个 promise 成功,就返回那个成功的 promise 的值,失败返回一个AggregateError类型的错误new AggregateError('AggregateError: All promises were rejected')

// any方法 其中的一个 promise 成功,就返回那个成功的promise的值,失败返回一个AggregateError类型的错误
Promise.any = function (promises) {let count = 0;return new Promise((resolve, reject) => {for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {resolve(v);},(r) => {count++;if (count === promises.length) {reject(new AggregateError("AggregateError: All promises were rejected"));}});}});
};

allSettled 方法是比较少知道的方法,有时候会在面试者被问到你如何将所有的成功失败结构都返回,下面就是答案:

// allSettled方法 所有结果都返回后显示每个结果的返回值
Promise.allSettled = function (promises) {return new Promise((resolve, reject) => {let arr = [];for (let i = 0; i < promises.length; i++) {promises[i].then((v) => {arr[i] = { status: "fulfilled", value: v };if (arr.length === promises.length) {resolve(arr);}},(r) => {arr[i] = { status: "rejected", reason: r };if (arr.length === promises.length) {resolve(arr);}});}});
};

实现了上面的构造方法以后,可以发现,提供的方法如果在你的逻辑中不适用,也可以类比上面的方法实现自己想要的方法。

三、总结 Promise

一步一步实现下来,发现逻辑都是环环相扣的

由于需要状态的管理并且不可逆,所以需要有个变量来保存状态;

由于构造函数的参数(回调函数)可以改变状态,所以需要添加对应的方法来处理状态的修改;

又由于状态的可能是异步修改的,所以需要添加一个变量来保存 then 方法的回调函数;

由于 then 可以存在多个,所以保存回调函数的变量得是一个数组;

由于 then 可以链式调用,所以 then 方法必须返回一个 promise 对象;

其他方法也可以调用 then 方法,所以也需要返回一个 promise 对象;

由于 throw 也可以改变状态,所以处理需要使用 try...catch...实现状态的改变;

由于可能会存在 then 方法没有失败回调函数的情况,所以异常需要统一由 catch 方法收口;

由于 catch 方法可以再多个 then 方法之后,所以需要考虑异常穿透,将失败回调函数补充上并抛出异常;

其他方法的实现主要是在上面的基础上保证在特定的时期改变返回的 promise 的状态,有的是在第一次成功的时候返回成功(比如 any 方法);有的是在所有都成功的时候返回成功(比如 all 方法);有的是在第一结果返回的时候就返回,无论成功失败(比如 race 方法);有的是在所有结果都返回了以后就返回结果,无论成功失败(比如 allSettled 方法)。

四、扩展

async/await 也是异步编程的一种解决方案,他遵循的是 Generator 函数的语法糖,他拥有内置执行器,不需要额外的调用直接会自动执行并输出结果,它返回的是一个 Promise 对象。在涉及到比较复杂的业务场景,then 方法的调用会显得不太美观,但是 async/await 看起来就好很多,这一句代码执行完,才会执行下一句。

以上就是我对 promise 的学习和理解,如果有什么问题请大家指正。


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

推荐阅读

整整4个月了,尽全力组织了源码共读活动~
我历时3年才写了10余篇源码文章,但收获了100w+阅读

老姚浅谈:怎么学JavaScript?

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

8ad3f5d7a92b07ef1d622da1731fd836.gif

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

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》10余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助1000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

b5320ecb88f6a72ad769aab9538966a0.png

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

今日话题

略。分享、收藏、点赞、在看我的文章就是对我最大的支持~

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

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

相关文章

设计图像素和开发像素_游戏开发的像素艺术设计

设计图像素和开发像素Pixel art is a large part of the legacy of game development. Every Pokemon game up until their X/Y series was rendered entirely with pixel art, Ragnarok Online (2000) was one of the first commercial works to feature 3D rendering along s…

深入浅出Nintex——更新PeopleandGroup类型的Field

转载于:https://www.cnblogs.com/mingle/archive/2011/11/25/2263199.html

从 vue-cli 源码中,我发现了27行读取 json 文件有趣的 npm 包

1. 前言大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。已进行四个月了&#xff0c;很多小伙伴表示收获颇丰。想学源码&#xff0c;极力推荐订阅我写…

自定义view示例_自定义404页的10个示例(从最佳到最差)

自定义view示例自定义404页面 (Custom 404 pages) To customize or not to customize your 404 page? I hope by now you know the answer is that, yes, under essentially all circumstances you should customize your 404 page. 404 errors occur when someone attempts t…

BTF:实践指南

本文地址&#xff1a;BTF&#xff1a;实践指南 | 深入浅出 eBPF 1. BPF 的常见限制 1.1 调试限制1.2 可移植性2. BTF 是什么&#xff1f;3. BTF 快速入门 3.1 BPF 快速入门3.1 BTF 和 CO-RE4. 结论 BPF 是 Linux 内核中基于寄存器的虚拟机&#xff0c;可安全、高效和事件驱动…

python 混入类MixIn

写在前面 能把一件事情说的那么清楚明白&#xff0c;感谢廖雪峰的官方网站。 1.为什么要用混入类&#xff1f;&#xff08;小白入门&#xff09; 继承是面向对象编程的一个重要的方式&#xff0c;因为通过继承&#xff0c;子类就可以扩展父类的功能。 step1: 回忆一下Animal类层…

估计很多前端都没学过单元测试~

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。已进行四个月了&#xff0c;很多小伙伴表示收获颇丰。想学源码&#xff0c;极力推荐订阅我写的《学习…

xd可以用ui动效效果吗_通过动画使UI设计栩栩如生:Adobe XD和After Effects

xd可以用ui动效效果吗Note — If you don’t fancy splashing out on an Adobe license, you can trial their products for 14 days each. That should give you more than enough time to play, check it out.注意—如果您不愿意花钱购买Adobe许可证&#xff0c;则可以分别试…

第十二周编程总结

这个作业属于那个课程C语言程序设计II这个作业要求在哪里https://pintia.cn/problem-sets/1127748174659035136/problems/1127749414029729792我在这个课程的目标是更好的学习函数这个作业在那个具体方面帮助我实现目锻炼了我的编程能力参考文献c语言程序设计26-1 计算最长的字…

可能是全网首个前端源码共读活动,诚邀加入学习

大家好&#xff0c;我是若川。从8月份到现在11月结束了。每周一期&#xff0c;一起读200行左右的源码&#xff0c;撰写辅助文章&#xff0c;截止到现在整整4个月了。由写有《学习源码整体架构系列》20余篇的若川【若川视野公众号号主】倾力组织&#xff0c;召集了各大厂对于源码…

现代游戏中的UX趋势

ux设计中的各种地图游戏UX (GAMES UX) Even though websites and games have matured side-by-side over the past few decades, games have a long and detailed history of user experience. Sure, it was scrappy and fairly rudimentary initially, but the only way you c…

你提交代码前没有校验?巧用gitHooks解决

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。已进行四个月了&#xff0c;很多小伙伴表示收获颇丰。想学源码&#xff0c;极力推荐订阅我写的《学习…

Linux下自动化测试环境的搭建

1.安装Linux虚拟机&#xff0c;详情参考 https://blog.csdn.net/qq_22770715/article/details/78558374 https://www.cnblogs.com/Q277227/p/8176564.html 1.1 需要确定IP &#xff0c;使用 ifconfig 1.2 linux的用户名跟密码&#xff1b; 1.3 确定可以远程ssh登录&…

code craft_以Craft.io为先—关于我们行业的实践职业道路的系列

code craft重点 (Top highlight)For the past two decades, digital product design / UX has been shifting to become a more strategic discipline within organizations. Partially because business leaders have started to pay attention to how design-driven companie…

Nginx+httpd反代实现动静分离

什么是动静分离为了提高网站的响应速度&#xff0c;减轻程序服务器&#xff08;apachephp&#xff0c;nginxphp等&#xff09;的负载&#xff0c;对于静态资源比如图片&#xff0c;js&#xff0c;css&#xff0c;html等静态文件&#xff0c;我们可以在反向代理服务器中设置&…

(建议收藏)前端面试必问的十六条HTTP网络知识体系

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。已进行四个月了&#xff0c;很多小伙伴表示收获颇丰。想学源码&#xff0c;极力推荐订阅我写的《学习…

多边形的时针方向与法线方向

从相反的法线方向观察&#xff0c;顺时针还是逆时针是相反的。 多边形的时针方向与法线方向的关系呈右手法则关系。 GoogleEarth中的面具有时针方向&#xff0c;法线方向为正向&#xff0c;反之为负向 GoogleEarth的垂面在法线方向为亮色&#xff0c;反向为暗色 GoogleEarth的水…

裂墙推荐!再也不用求后端给接口了...

大家好&#xff0c;我是若川。今天咱们来介绍一款强大的云服务平台&#xff01;MemFire Cloud注册即享5GB存储空间、每月100万读额度和每月10万写额度。平台入口&#xff1a;https://memfiredb.com/今天&#xff08;12月10号&#xff09;还有限时的送书活动&#xff01;感兴趣的…

1.今日标签:视频价值一千字

I love the App Store. It looks and works better than ever. But also, I love tricky design challenges. How do you improve something that already works great?我喜欢App Store。 它的外观和工作比以往更好。 但是我也很棘手 设计挑战。 您如何改善已经很好的工作&a…

蚂蚁金服疯了吗?大动作,非裁员,年底全员涨薪又涨假期!!!

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。以下分享一篇水文&#…