本文首发于 vivo互联网技术 微信公众号 
链接: https://mp.weixin.qq.com/s/Xz2bGaLxVL4xw1M2hb2nJQ
作者:Morrain

很多同学在学习 Promise 时,知其然却不知其所以然,对其中的用法理解不了。本系列文章由浅入深逐步实现 Promise,并结合流程图、实例以及动画进行演示,达到深刻理解 Promise 用法的目的。

本系列文章有如下几个章节组成:

  1. 图解 Promise 实现原理(一)—— 基础实现
  2. 图解 Promise 实现原理(二)—— Promise 链式调用
  3. 图解 Promise 实现原理(三)—— Promise 原型方法实现
  4. 图解 Promise 实现原理(四)—— Promise 静态方法实现

一、前言

上一节中,实现了 Promise 的基础版本:

  1. //极简的实现+链式调用+延迟机制+状态
  2. class Promise {
  3. callbacks = [];
  4. state = 'pending';//增加状态
  5. value = null;//保存结果
  6. constructor(fn) {
  7. fn(this._resolve.bind(this));
  8. }
  9. then(onFulfilled) {
  10. if (this.state === 'pending') {//在resolve之前,跟之前逻辑一样,添加到callbacks中
  11. this.callbacks.push(onFulfilled);
  12. } else {//在resolve之后,直接执行回调,返回结果了
  13. onFulfilled(this.value);
  14. }
  15. return this;
  16. }
  17. _resolve(value) {
  18. this.state = 'fulfilled';//改变状态
  19. this.value = value;//保存结果
  20. this.callbacks.forEach(fn => fn(value));
  21. }
  22. }

但链式调用,只是在 then 方法中 return 了 this,使得 Promise 实例可以多次调用 then 方法,但因为是同一个实例,调用再多次 then 也只能返回相同的一个结果,通常我们希望的链式调用是这样的:

  1. //使用Promise
  2. function getUserId(url) {
  3. return new Promise(function (resolve) {
  4. //异步请求
  5. http.get(url, function (id) {
  6. resolve(id)
  7. })
  8. })
  9. }
  10. getUserId('some_url').then(function (id) {
  11. //do something
  12. return getNameById(id);
  13. }).then(function (name) {
  14. //do something
  15. return getCourseByName(name);
  16. }).then(function (course) {
  17. //do something
  18. return getCourseDetailByCourse(course);
  19. }).then(function (courseDetail) {
  20. //do something
  21. });

每个 then 注册的 onFulfilled 都返回了不同的结果,层层递进,很明显在 then 方法中 return this 不能达到这个效果。引入真正的链式调用,then 返回的一定是一个新的Promise实例。

真正的链式 Promise 是指在当前 Promise 达到 fulfilled 状态后,即开始进行下一个 Promise(后邻 Promise)。那么我们如何衔接当前 Promise 和后邻 Promise 呢?(这是理解 Promise 的难点,我们会通过动画演示这个过程)。

二、链式调用的实现

先看下实现源码:

  1. //完整的实现
  2. class Promise {
  3. callbacks = [];
  4. state = 'pending';//增加状态
  5. value = null;//保存结果
  6. constructor(fn) {
  7. fn(this._resolve.bind(this));
  8. }
  9. then(onFulfilled) {
  10. return new Promise(resolve => {
  11. this._handle({
  12. onFulfilled: onFulfilled || null,
  13. resolve: resolve
  14. });
  15. });
  16. }
  17. _handle(callback) {
  18. if (this.state === 'pending') {
  19. this.callbacks.push(callback);
  20. return;
  21. }
  22. //如果then中没有传递任何东西
  23. if (!callback.onFulfilled) {
  24. callback.resolve(this.value);
  25. return;
  26. }
  27. var ret = callback.onFulfilled(this.value);
  28. callback.resolve(ret);
  29. }
  30. _resolve(value) {
  31. this.state = 'fulfilled';//改变状态
  32. this.value = value;//保存结果
  33. this.callbacks.forEach(callback => this._handle(callback));
  34. }
  35. }

由上面的实现,我们可以看到:

  • then 方法中,创建并返回了新的 Promise 实例,这是串行Promise的基础,是实现真正链式调用的根本。
  • then 方法传入的形参 onFulfilled 以及创建新 Promise 实例时传入的 resolve 放在一起,被push到当前 Promise 的 callbacks 队列中,这是衔接当前 Promise 和后邻 Promise 的关键所在。
  • 根据规范,onFulfilled 是可以为空的,为空时不调用 onFulfilled。

看下动画演示:

(Promise 链式调用演示动画)

当第一个 Promise 成功时,resolve 方法将其状态置为 fulfilled ,并保存 resolve 带过来的value。然后取出 callbacks 中的对象,执行当前 Promise的 onFulfilled,返回值通过调用第二个 Promise 的 resolve 方法,传递给第二个 Promise。动画演示如下:

(Promise 链式调用 fulfilled)

为了真实的看到链式调用的过程,我写一个mockAjax函数,用来模拟异步请求:

  1. /**
  2. * 模拟异步请求
  3. * @param {*} url 请求的URL
  4. * @param {*} s 指定该请求的耗时,即多久之后请求会返回。单位秒
  5. * @param {*} callback 请求返回后的回调函数
  6. */
  7. const mockAjax = (url, s, callback) => {
  8. setTimeout(() => {
  9. callback(url + '异步请求耗时' + s + '秒');
  10. }, 1000 * s)
  11. }

除此之外,我给 Promise 的源码加上了日志输出并增加了构造顺序标识,可以清楚的看到构造以及执行过程:

  1. //Demo1
  2. new Promise(resolve => {
  3. mockAjax('getUserId', 1, function (result) {
  4. resolve(result);
  5. })
  6. }).then(result => {
  7. console.log(result);
  8. })

Demo1 的源码

执行结果如下:

  1. [Promse-1]:constructor
  2. [Promse-1]:then
  3. [Promse-2]:constructor
  4. [Promse-1]:_handle state= pending
  5. [Promse-1]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
  6. => Promise { callbacks: [], name: 'Promse-2', state: 'pending', value: null }
  7. [Promse-1]:_resolve
  8. [Promse-1]:_resolve value= getUserId异步请求耗时1
  9. [Promse-1]:_handle state= fulfilled
  10. getUserId异步请求耗时1
  11. [Promse-2]:_resolve
  12. [Promse-2]:_resolve value= undefined

通过打印出来的日志,可以看到:

  1. 构造 Promise-1 实例,立即执行 mackAjax('getUserId',callback);

  2. 调用 Promise-1 的 then 方法,注册 Promise-1 的 onFulfilled 函数。

  3. then 函数内部构造了一个新的 Promise实例:Promise-2。立即执行 Promise-1 的 _handle方法。

  4. 此时 Promise-1 还是pending的状态。

  5. Promise-1._handle 中就把注册在 Promise-1 的 onFulfilled 和 Promise-2 的 resolve 保存在 Promise-1 内部的 callbacks。

  6. 至此当前线程执行结束。返回的是 Promise-2 的 Promise实例。

  7. 1s后,异步请求返回,要改变 Promise-1 的状态和结果,执行 resolve(result)。

  8. Promise-1 的值被改变,内容为异步请求返回的结果:"getUserId异步请求耗时1s"。

  9. Promise-1 的状态变成 fulfilled。

  10. Promise-1 的 onFulfilled 被执行,打印出了"getUserId异步请求耗时1秒"。

  11. 然后再调用 Promise-2.resolve。

  12. 改变 Promise-2 的值和状态,因为 Promise-1 的 onFulfilled 没有返回值,所以 Promise-2的值为undefined。

上例中,如果把异步的请求改成同步会是什么的效果?

  1. new Promise(resolve => {
  2. resolve('getUserId同步请求');
  3. }).then(result => {
  4. console.log(result);
  5. });
  6. //打印日志
  7. [Promse-1]:constructor
  8. [Promse-1]:_resolve
  9. [Promse-1]:_resolve value= getUserId同步请求
  10. [Promse-1]:then
  11. [Promse-2]:constructor
  12. [Promse-1]:_handle state= fulfilled
  13. getUserId同步请求
  14. [Promse-2]:_resolve
  15. [Promse-2]:_resolve value= undefined
  16. => Promise {
  17. callbacks: [],
  18. name: 'Promse-2',
  19. state: 'fulfilled',
  20. value: undefined }

感兴趣的可以自己去分析一下。

三、链式调用真正的意义

执行当前 Promise 的 onFulfilled 时,返回值通过调用第二个 Promise 的 resolve 方法,传递给第二个 Promise,作为第二个 Promise 的值。于是我们考虑如下Demo:

  1. //Demo2
  2. new Promise(resolve => {
  3. mockAjax('getUserId', 1, function (result) {
  4. resolve(result);
  5. })
  6. }).then(result => {
  7. console.log(result);
  8. //对result进行第一层加工
  9. let exResult = '前缀:' + result;
  10. return exResult;
  11. }).then(exResult => {
  12. console.log(exResult);
  13. });

Demo2 的源码

我们加了一层 then,来看下执行的结果:

  1. [Promse-1]:constructor
  2. [Promse-1]:then
  3. [Promse-2]:constructor
  4. [Promse-1]:_handle state= pending
  5. [Promse-1]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
  6. [Promse-2]:then
  7. [Promse-3]:constructor
  8. [Promse-2]:_handle state= pending
  9. [Promse-2]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
  10. => Promise { callbacks: [], name: 'Promse-3', state: 'pending', value: null }
  11. [Promse-1]:_resolve
  12. [Promse-1]:_resolve value= getUserId异步请求耗时1
  13. [Promse-1]:_handle state= fulfilled
  14. getUserId异步请求耗时1
  15. [Promse-2]:_resolve
  16. [Promse-2]:_resolve value= 前缀:getUserId异步请求耗时1
  17. [Promse-2]:_handle state= fulfilled
  18. 前缀:getUserId异步请求耗时1
  19. [Promse-3]:_resolve
  20. [Promse-3]:_resolve value= undefined

链式调用可以无限的写下去,上一级 onFulfilled return 的值,会变成下一级 onFulfilled 的结果。可以参考Demo3:

Demo3 的源码

我们很容易发现,上述 Demo3 中只有第一个是异步请求,后面都是同步的,我们完全没有必要这么链式的实现。如下一样能得到我们想要的三个结果: 分别打印出来的值。

  1. //等价于 Demo3
  2. new Promise(resolve => {
  3. mockAjax('getUserId', 1, function (result) {
  4. resolve(result);
  5. })
  6. }).then(result => {
  7. console.log(result);
  8. //对result进行第一层加工
  9. let exResult = '前缀:' + result;
  10. console.log(exResult);
  11. let finalResult = exResult + ':后缀';
  12. console.log(finalResult);
  13. });

那链式调用真正的意义在哪里呢?

刚才演示的都是 onFulfilled 返回值是 value 的情况,如果是一个 Promise 呢?是不是就可以通过 onFulfilled,由使用 Promise 的开发者决定后续 Promise 的状态。

于是在 _resolve 中增加对前一个 Promise onFulfilled 返回值的判断:

  1. _resolve(value) {
  2. if (value && (typeof value === 'object' || typeof value === 'function')) {
  3. var then = value.then;
  4. if (typeof then === 'function') {
  5. then.call(value, this._resolve.bind(this));
  6. return;
  7. }
  8. }
  9. this.state = 'fulfilled';//改变状态
  10. this.value = value;//保存结果
  11. this.callbacks.forEach(callback => this._handle(callback));
  12. }

从代码上看,它是对 resolve 中的值作了一个特殊的判断,判断 resolve 的值是否为 Promise实例,如果是 Promise 实例,那么就把当前 Promise 实例的状态改变接口重新注册到 resolve 的值对应的 Promise 的 onFulfilled 中,也就是说当前 Promise 实例的状态要依赖 resolve 的值的 Promise 实例的状态。

  1. //Demo4
  2. const pUserId = new Promise(resolve => {
  3. mockAjax('getUserId', 1, function (result) {
  4. resolve(result);
  5. })
  6. })
  7. const pUserName = new Promise(resolve => {
  8. mockAjax('getUserName', 2, function (result) {
  9. resolve(result);
  10. })
  11. })
  12. pUserId.then(id => {
  13. console.log(id)
  14. return pUserName
  15. }).then(name => {
  16. console.log(name)
  17. })

Demo 4 的源码

执行的结果如下:

  1. [Promse-1]:constructor
  2. [Promse-2]:constructor
  3. [Promse-1]:then
  4. [Promse-3]:constructor
  5. [Promse-1]:_handle state= pending
  6. [Promse-1]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
  7. [Promse-3]:then
  8. [Promse-4]:constructor
  9. [Promse-3]:_handle state= pending
  10. [Promse-3]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
  11. => Promise { callbacks: [], name: 'Promse-4', state: 'pending', value: null }
  12. [Promse-1]:_resolve
  13. [Promse-1]:_resolve value= getUserId异步请求耗时1
  14. [Promse-1]:_handle state= fulfilled
  15. getUserId异步请求耗时1
  16. [Promse-3]:_resolve
  17. [Promse-3]:_resolve value= Promise { callbacks: [], name: 'Promse-2', state: 'pending', value: null }
  18. [Promse-2]:then
  19. [Promse-5]:constructor
  20. [Promse-2]:_handle state= pending
  21. [Promse-2]:_handle callbacks= [ { onFulfilled: [Function], resolve: [Function] } ]
  22. [Promse-2]:_resolve
  23. [Promse-2]:_resolve value= getUserName异步请求耗时2
  24. [Promse-2]:_handle state= fulfilled
  25. [Promse-3]:_resolve
  26. [Promse-3]:_resolve value= getUserName异步请求耗时2
  27. [Promse-3]:_handle state= fulfilled
  28. getUserName异步请求耗时2
  29. [Promse-4]:_resolve
  30. [Promse-4]:_resolve value= undefined
  31. [Promse-5]:_resolve
  32. [Promse-5]:_resolve value= undefined

一样的,我做了一个演示动画,还原了这个过程:

(Promise 真正的链式调用)

至此,就实现了 Promise 链式调用的全部内容。链式调用是 Promise 难点,更是重点。一定要通过实例还有动画,深刻体会。下一节介绍 Promise 其它原型方法的实现。

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:Labs2020 联系。

图解 Promise 实现原理(二)—— Promise 链式调用的更多相关文章

  1. 学了ES6,还不会Promise的链式调用?🧐

    前言 本文主要讲解promise的链式调用的方法及其最终方案 应用场景 假如开发有个需求是先要请求到第一个数据,然后根据第一个数据再去请求第二个数据,再根据第二个数据去请求第三个数据...一直到最后得 ...

  2. Promise.then链式调用

    let a = new Promise((resolve,reject)=>{ resolve(1) }).then((r)=>{console.log(r)}).then(()=> ...

  3. 史上最简单的手写Promise,仅17行代码即可实现Promise链式调用

    Promise的使用相比大家已经孰能生巧了,我这里就不赘述了 先说说我写的Promise的问题吧,无法实现宏任务和微任务里的正确执行(也就是在Promise里面写setTimeout,setInter ...

  4. ES6 Promise 的链式调用

    1.什么是Promise Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息. 2.对象的状态不受外界影响.Promise 对象代表一个异步操作,有三种状态: pending: 初始 ...

  5. JavaScript中的链式调用

    链模式 链模式是一种链式调用的方式,准确来说不属于通常定义的设计模式范畴,但链式调用是一种非常有用的代码构建技巧. 描述 链式调用在JavaScript语言中很常见,如jQuery.Promise等, ...

  6. js简单实现链式调用

    链式调用实现原理:对象中的方法执行后返回对象自身即可以实现链式操作.说白了就是每一次调用方法返回的是同一个对象才可以链式调用. js简单实现链式调用demo Object.prototype.show ...

  7. 玩一把JS的链式调用

    链式调用我们平常用到很多,比如jQuery中的$(ele).show().find(child).hide(),再比如angularjs中的$http.get(url).success(fn_s).e ...

  8. 《javascript设计模式》笔记之第六章:方法的链式调用

    这一章要实现的就是jQuery的那种链式调用,例子: $(this).setStyle('color', 'green').show(); 一:调用链的结构: 首先我们来看一下最简单的$()函数的实现 ...

  9. 如何写 JS 的链式调用 ---》JS 设计模式《----方法的链式调用

    1.以$ 函数为例.通常返回一个HTML元素或一个元素集合. 代码如下: function $(){ var elements = []; ;i<arguments.length;i++){ v ...

  10. 【Java】子类的链式调用

    记录最近在项目设计中遇到的一个小问题. 前提:有这样两个POJO类,它们都可以通过链式调用的方式来设置其属性值,其中一个类继承了另一个类. 问题:通过链式调用,子类对象访问父类方法后,如何使返回对象仍 ...

随机推荐

  1. 【python】【报错:pip中第三库下载成功,但是pycharm却没有显示】一步解决

    解决方案: 直接在这个目录下安装第三方库

  2. PHP异步通信

    目录 PHP swoole websocket服务器端 websocket 客户端 直播平台 基于宝塔nginx安装Nginx-rtmp-module搭建流媒体服务器 web H5端拉流 其他 PHP ...

  3. [AGC038E] Gachapon

    Problem Statement Snuke found a random number generator. It generates an integer between $0$ and $N- ...

  4. Mybatis-Flex之QueryWrapper

    1.完整DQL语句 /** * 使用QueryWrapper构建超复杂SQL语句 */ @Test public void testQueryWrapper1() { QueryWrapper wra ...

  5. ElasticSearch之查看集群的参数

    参考Cluster get settings API. 命令样例,不指定参数,如下: curl -X GET "https://localhost:9200/_cluster/setting ...

  6. 探索 Linux Namespace:Docker 隔离的神奇背后

    在 深入理解 Docker 核心原理:Namespace.Cgroups 和 Rootfs 一文中我们分析了 Docker 是由三大核心技术实现的. 今天就一起分析 Docker 三大核心技术之一的 ...

  7. Java 设置Excel页面背景

    本文介绍通过Java 程序在Excel表格中设置页面背景的方法,可设置颜色背景(即指定单一颜色作为背景色).图片背景(即加载图片设置成页面背景).程序中需要使用免费版Excel类库工具 Free Sp ...

  8. 大数据实践解析(上):聊一聊spark的文件组织方式

    摘要: 在大数据/数据库领域,数据的存储格式直接影响着系统的读写性能.Spark针对不同的用户/开发者,支持了多种数据文件存储方式.本文的内容主要来自于Spark AI Summit 2019中的一个 ...

  9. 华为云PB级数据库GaussDB(for Redis)介绍第四期:高斯 Geo的介绍与应用

    摘要:高斯Redis的大规模地理位置信息存储的解决方案. 1.背景 LBS(Location Based Service,基于位置的服务)有非常广泛的应用场景,最常见的应用就是POI(Point of ...

  10. 搞AI开发,你不得不会的PyCharm技术

    摘要:PyCharm在AI项目开发提供了优秀的代码编辑.调试.远程连接和同步能力,在开发者中广受欢迎. 使用PyCharm插件配合ModelArts: 一键帮助用户配置远程ModelArts Note ...