70行实现Promise核心源码

前言:

​ 一直以来都是只会调用Promise的API,而且调API还是调用axios封装好的Promise,太丢人了!!!没有真正的去了解过它的原理是如何实现的,自己也看过很多博主实现的Promise,但总觉得用原型链的OOP晦涩难懂。

个人的理解:如果带着观察者模式的想法来理解Promise源码,你就会发现Promise本身其实一种微任务的观察者模式,一个异步任务的完成,res/rej的状态回调hook => 通知所有then()订阅的promise对象。promise只是将观察者模式运用到微任务。让promise对象能够具有很高的优先级。说到底还是一种解藕的设计模式。

promise是诞生的原因?

​ 在了解Promise之前,我觉得有必要去了解一下Promise诞生的原因。 直接就那上面的axios来说吧,以前没有出现axios的时候,大家是怎么去与后台接口做交互的呢? 我当时是用jQuery封装好的AJAX去做的。下面有一个例子

  1. $.ajax({
  2. type: 'POST', //GET or POST
  3. url: "jquery-ajax",
  4. cache: false,
  5. data: {todo:"ajaxexample1"},
  6. success: functionSucceed,
  7. error: functionFailed,
  8. statusCode: {
  9. 404: function() {
  10. alert("page not found");
  11. }
  12. }
  13. });

如果是单独的一个请求还好,但是如果得发送两个相互依赖的请求呢?这时候就会出现回调地狱的问题,不能自拔。以下就是一个简单的例子。

  1. a(function (result1) {
  2. b(result1,function (result2) {
  3. c(result2, function (result3) {
  4. d(result3, function (result4) {
  5. e(result4, function (result5) {
  6. console.log(result5)
  7. })
  8. })
  9. })
  10. })
  11. })

假如说让你去维护一个这样的代码... 害怕的兄弟萌把害怕打在评论区[doge]。上面的代码有什么问题呢?

  • 嵌套调用,下面的任务依赖上个任务的请求结果。如果2层还是容易理顺逻辑,但是一旦出现层数过多,可读性就会变得非常差,就像一坨屎
  • 任务的不确定性。每一个任务会有成功和失败两种状态,就拿上面的代码,假如说有5层的嵌套,就要做5次的成功、失败的判断函数,明显的增加了代码的复杂度,不符合Unix哲学。

用Typescript实现MyPromise

问题出来了,那解决的思路也有了:

  • 消灭嵌套
  • 合并多个错误

设计一个对象实现上面两个功能,使用TypeScript的OOP相比于用原型链来实现会更加的容易理解。在实现Promise源码之前,对于Promise的用法、基本定义一定要有一个全方面的认知,不然去了解Promise也艰深晦涩。可以先去看看MDN对于Promise的本质定义

定义基本的属性和构造函数

Promise有三种状态:pending、 resolve、reject 对应了 等待、成功、失败,表示一个异步任务的状态是怎么样的。

  1. enum States {
  2. PENDING = 'PENDING',
  3. RESOLVED = 'RESOLVED',
  4. REJECTED = 'REJECTED'
  5. }
  6. class MyPromise {
  7. private state: States = States.PENDING;
  8. private handlers: any[] = [];
  9. private value: any;
  10. constructor(executor: (resolve, reject) => void) {
  11. try {
  12. executor(this.resolve, this.reject);
  13. } catch (e) {
  14. this.reject(e);
  15. }
  16. }
  17. }

handlers数组是表示当调用了then()方法时,向handlers添加回调函数。比如以下的情况,handlers中就会有两个回调函数,等待Promise的resolve/reject设置状态之后,调用handlers里的所有回调函数。

  1. let promise1 = new MyPromise(test);
  2. let promise2 = promise1
  3. .then(res => { // <=== 匿名回调函数
  4. console.log(res);
  5. return 2;
  6. });
  7. let promise3 = promise1
  8. .then(res => { // <=== 匿名回调函数
  9. setTimeout(() => {
  10. console.log(res + '***********************');
  11. return 4;
  12. }, 1000);
  13. })

value表示的一个异步函数返回值。

executor是带有 resolvereject 两个参数的函数 。Promise构造函数执行时立即调用executor 函数, resolvereject 两个函数作为参数传递给executor(executor 函数在Promise构造函数返回所建promise实例对象前被调用)

回到主题,我觉得先介绍then()方法是如何实现的比较合适

实现then()

  1. then(onSuccess?, onFail?) {
  2. return new MyPromise((resolve, reject) => {
  3. return this.attachHandler({
  4. onSuccess: result => {
  5. if (!onSuccess) return resolve(result);
  6. try {
  7. return resolve(onSuccess(result));
  8. } catch (e) {
  9. return reject(e);
  10. }
  11. },
  12. onFail: reason => {
  13. if (!onFail) return reject(reason);
  14. try {
  15. return resolve(onFail(reason));
  16. } catch (e) {
  17. return reject(e);
  18. }
  19. }
  20. });
  21. });
  22. }

then方法的工作原理:返回一个新的Promise对象,且向原Promise对象中的handlers添加一个包含回调函数的对象,如果Promise处于Settled状态,那就直接执行回调函数,否则,得等待Promise的状态设置。

  1. private execHandlers = () => {
  2. if (this.state === States.PENDING) return;
  3. this.handlers.forEach(handler => {
  4. if (this.state === States.REJECTED) {
  5. return handler.onFail(this.value);
  6. }
  7. return handler.onSuccess(this.value);
  8. });
  9. this.handlers = [];
  10. };
  11. private attachHandler = (handler: any) => {
  12. this.handlers.push(handler);
  13. this.execHandlers();
  14. };

实现resolve和reject回调函数

按照原生的Promise.then()方法的逻辑来讲,原Promise的状态会直接影响到then方法返回的Promise的状态,因此设置状态的resolvereject函数逻辑如下:

  1. private resolve = value => this.setResult(value, States.RESOLVED);
  2. private reject = value => this.setResult(value, States.REJECTED);
  3. private setResult = (value, state: States) => {
  4. const set = () => {
  5. if (this.state !== States.PENDING) return;
  6. this.value = value;
  7. this.state = state;
  8. return this.execHandlers();
  9. };
  10. setTimeout(set, 0);
  11. };

因为无法实现真正的Promise的微任务,因此只能够通过setTimeout(fn,0),勉强来模拟实现

  1. private resolve = value => this.setResult(value, States.RESOLVED);
  2. private reject = value => this.setResult(value, States.REJECTED);
  3. private setResult = (value, state: States) => {
  4. const set = () => {
  5. if (this.state !== States.PENDING) return;
  6. this.value = value;
  7. this.state = state;
  8. return this.execHandlers();
  9. };
  10. setTimeout(set, 0);
  11. };

离真正的微任务在一些特别的代码上还是有很大差距的,因为setTimeout是宏任务,在execHandlers方法中通过foreach 执行本次Promise的handlers中的回调函数时,处于同一个事件循环,以下的代码就会和真正的Promise有出入。

  1. function test(res, rej) {
  2. console.log('executor');
  3. setTimeout(() => {
  4. console.log('Timer');
  5. res(1);
  6. }, 1000);
  7. }
  8. console.log('start');
  9. let promise1 = new MyPromise(test); // <== 这里替换成原生的Promise,会发现promise2状态不同
  10. let promise2 = promise1.then(res => {
  11. console.log(res);
  12. return 2;
  13. });
  14. let promise3 = promise1.then(res => {
  15. console.log(promise2); //原生的状态是resolve,MyPromise的状态是pending
  16. });
  17. console.log('end');

总结

就拿上面的demo来理解整个Promise帮助我们做了什么吧!

  • 控制台输出'start '
  • 创建MyPromise对象并且执行test函数,引用赋值于promise1=> 输出'executor',向延迟队列添加等待1s的回调函数
  • 调用promise1then方法 => 创建一个新的promise实例赋于给promise2,并且新的promise实例的executor执行promise1的attachHandler,将then函数中的回调函数对象push进promise1的handlers属性中,如果promise1已经是settled状态,直接更加promise1的状态来执行不同函数
  • promise3同promise2一样的道理,这时promise1的handlers数组中有两个持有回调函数的对象,这两个Promise3和promise2都等着promise1的setResult来执行相应的回调,因此promise3和promise此时属于pending状态
  • 控制台输出'end'
  • 等待1秒,控制台输出'Timer' ,调用Promise1的resolve函数,向微任务队列添加setResult状态函数,MyPromise使用settimeout模拟微任务队列
  • setResult状态函数根据res/rej状态执行handlers中的所有then添加的回调,

Promise类的catchfinally都是在then上建立的语法糖,具体大家可以更具MDN的定义来实现,还有Promise类的静态方法,可以参考我自己GitHub的实现。

不断的沉淀下来,总归会理解一个东西存在的意义。理解了promise的原理之后,去理解其他的底层实现有会一个很好的基础,了解了Promise底层之后,深深的感受到设计模式的强大。

如果小伙伴们觉得不错的话,点赞支持一下嗷 铁汁~

以下是用Typescript实现的MyPromise源代码,不过参数并没有用类型,所以称作es6的class语法也不为过。

  1. enum States {
  2. PENDING = 'PENDING',
  3. RESOLVED = 'RESOLVED',
  4. REJECTED = 'REJECTED'
  5. }
  6. export class MyPromise {
  7. private state: States = States.PENDING;
  8. private handlers: any[] = [];
  9. private value: any;
  10. constructor(callback: (resolve, reject) => void) {
  11. try {
  12. callback(this.resolve, this.reject);
  13. } catch (e) {
  14. this.reject(e);
  15. }
  16. }
  17. private resolve = value => this.setResult(value, States.RESOLVED);
  18. private reject = value => this.setResult(value, States.REJECTED);
  19. private setResult = (value, state: States) => {
  20. const set = () => {
  21. if (this.state !== States.PENDING) return;
  22. this.value = value;
  23. this.state = state;
  24. return this.execHandlers();
  25. };
  26. setTimeout(set, 0);
  27. };
  28. private execHandlers = () => {
  29. if (this.state === States.PENDING) return;
  30. this.handlers.forEach(handler => {
  31. if (this.state === States.REJECTED) {
  32. return handler.onFail(this.value);
  33. }
  34. return handler.onSuccess(this.value);
  35. });
  36. this.handlers = [];
  37. };
  38. private attachHandler = (handler: any) => {
  39. this.handlers.push(handler);
  40. this.execHandlers();
  41. };
  42. then(onSuccess?, onFail?) {
  43. return new MyPromise((resolve, reject) => {
  44. return this.attachHandler({
  45. onSuccess: result => {
  46. if (!onSuccess) return resolve(result);
  47. try {
  48. return resolve(onSuccess(result));
  49. } catch (e) {
  50. return reject(e);
  51. }
  52. },
  53. onFail: reason => {
  54. if (!onFail) return reject(reason);
  55. try {
  56. return resolve(onFail(reason));
  57. } catch (e) {
  58. return reject(e);
  59. }
  60. }
  61. });
  62. });
  63. }
  64. }

参考链接

https://www.freecodecamp.org/news/how-to-implement-promises-in-javascript-1ce2680a7f51/

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

 

70行实现Promise核心源码的更多相关文章

  1. Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的 ...

  2. Java内存管理-掌握类加载器的核心源码和设计模式(六)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有 ...

  3. 3 手写Java HashMap核心源码

    手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...

  4. HashMap的结构以及核心源码分析

    摘要 对于Java开发人员来说,能够熟练地掌握java的集合类是必须的,本节想要跟大家共同学习一下JDK1.8中HashMap的底层实现与源码分析.HashMap是开发中使用频率最高的用于映射(键值对 ...

  5. HTTP流量神器Goreplay核心源码详解

    摘要:Goreplay 前称是 Gor,一个简单的 TCP/HTTP 流量录制及重放的工具,主要用 Go 语言编写. 本文分享自华为云社区<流量回放工具之 goreplay 核心源码分析> ...

  6. 并发编程之 SynchronousQueue 核心源码分析

    前言 SynchronousQueue 是一个普通用户不怎么常用的队列,通常在创建无界线程池(Executors.newCachedThreadPool())的时候使用,也就是那个非常危险的线程池 ^ ...

  7. iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects

    Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...

  8. Backbone事件机制核心源码(仅包含Events、Model模块)

    一.应用场景 为了改善酷版139邮箱的代码结构,引入backbone的事件机制,按照MVC的分层思想搭建酷版云邮局的代码框架.力求在保持酷版轻量级的基础上提高代码的可维护性.   二.遗留问题 1.b ...

  9. 6 手写Java LinkedHashMap 核心源码

    概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...

随机推荐

  1. scala_spark实践4

    SparkStreaming中foreachRDD SparkStreaming是流式实时处理数据,就是将数据流按照定义的时间进行分割(就是“批处理”).每一个时间段内处理的都是一个RDD.而Spar ...

  2. Mybatis-项目结构

    源文件:https://github.com/569844962/Mybatis-Learn/blob/master/doc/Mybatis%E6%95%B4%E4%BD%93%E6%9E%B6%E6 ...

  3. 面试 HTTP ,99% 的面试官都爱问这些问题

    HTTP 和 HTTPS 的区别 HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),HTTP 是一个在计算机世界里专门在两点之间传输文字.图片.音频.视频等超 ...

  4. 如何使用python进行自动网上考试

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者: HIS Hacker PS:如有需要Python学习资料的小伙伴可以 ...

  5. window 10 安装paddlepaddle 1.7 GPU版本

    window 10 安装paddlepaddle 1.7 GPU版本 1)更新显卡驱动 2)安装cuda 10 https://developer.nvidia.com/cuda-10.0-downl ...

  6. asp.net core webapi Session 内存缓存

    Startup.cs文件中的ConfigureServices方法配置: #region Session内存缓存 services.Configure<CookiePolicyOptions&g ...

  7. .NET中 kafka消息队列、环境搭建与使用

    前面几篇文章中讲了一些关于消息队列的知识,就每中消息队列中间件,我们并没有做详细的讲解,那么,今天我们就来详细的讲解一下消息队列之一kafka的一些基本的使用与操作. 一.kafka介绍 kafka: ...

  8. 2.react-插件

    PC: antd(蚂蚁金服)https://ant.design/index-cn 移动: mobile-antd(蚂蚁金服)https://mobile.ant.design =========== ...

  9. 两种异常(CPU异常、用户模拟异常)的收集

    Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 两种异常(CPU异常.用户模拟异常)的收集  文章的核心:异常收集 ...

  10. Apache jena SPARQL endpoint及推理

    一.Apache Jena简介 Apache Jena(后文简称Jena),是一个开源的Java语义网框架(open source Semantic Web Framework for Java),用 ...