Promise静态四兄弟实现示例详解


Posted in Javascript onJuly 07, 2022

前言

恰逢 Promise 也有四个很像的静态三兄弟(Promise.allPromise.allSettledPromise.racePromise.any),它们接受的参数类型相同,但各自逻辑处理不同,它们具体会有什么区别那?别急,下面等小包慢慢道来。

在文章的开始,小包先给大家提出几个问题:

  • Promise.allPromise.allSettled 有啥区别啊?
  • Promise.race 的运行机制? Promise.any 呐,两者有啥区别?
  • 四兄弟只能接受数组作为参数吗?
  • 四兄弟方法我们应该如何优雅完美的实现?

Promise.all

Promise.all 在目前手写题中热度频度应该是 top5 级别的,所以我们要深刻掌握 Promise.all 方法。下面首先来简单回顾一下 all 方法。

基础学习

Promise.all 方法类似于一群兄弟们并肩前行,参数可以类比为一群兄弟,只有当兄弟全部快乐,all 老大才会收获快乐;只要有一个兄弟不快乐,老大就不会快乐。

Promise.all() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

Promise.all 方法接受一个数组做参数,p1、p2、p3 都是 Promise 实例。如果不是 Promise 实例,则会先调用 Promise.resolve 方法将参数先转化为 Promise 实例,之后进行下一步处理。

返回值 p 的状态由 p1、p2、p3 决定,可以分成两种情况:

  • 只有 p1、p2、p3 的状态都变成 fulfilledp 的状态才会变成 fulfilled ,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。

  • 只要 p1、p2、p3 之中有一个被 rejectedp 的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

// 模拟异步的promise
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});
// 普通promise
const p2 = Promise.resolve(2);
// 常数值
const p3 = 3;
// 失败的promise
const p4 = Promise.reject("error");
// 异步失败的promise
const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("TypeError");
  }, 1000);
});
// 1. promise全部成功
Promise.all([p1, p2, p3])
  .then((data) => console.log(data)) // [1, 2, 3]
  .catch((error) => console.log(error));
// 2. 存在失败的promise
Promise.all([p1, p2, p3, p4])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 3. 存在多个失败的promise
Promise.all([p1, p2, p3, p4, p5])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error

从上面案例的输出中,我们可以得出下列结论:

  • p 状态由参数执行结果决定,全部成功则返回成功,存有一个失败则失败
  • 参数为非 Promise 实例,会通过 Promise.resolve 转化成 Promise 实例
  • 成功后返回一个数组,数组内数据按照参数顺序排列
  • 短路效应: 只会返回第一个失败信息

Iterator 接口参数

《ES6 入门教程》还指出: Promise.all 方法可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例

说实话,加粗部分小包是没能完全理解的,难道 Promise.all 使用 Iterator 类型时,要求迭代项都是 Promise 实例吗?我们以 String 类型为例,看 Promise.all 是否可以支持迭代项为非 Promise 实例。

//  ['x', 'i', 'a', 'o', 'b', 'a', 'o']
Promise.all("xiaobao").then((data) => console.log(data));

可见 PromiseIterator 类型的处理与数组相同,如果参数不是 Promise 实例,会先调用 Promise.all 转化为 Promise 实例。

思路分析

  • Promise.all 会返回一个新 Promise 对象
Promise.all = function (promises) {
  return new Promise((resolve, reject) => {});
};
  • (亮点) all 方法参数可以是数组,同样也可以是 Iterator 类型,因此应该使用 for of 循环进行遍历。
Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    for (let p of promises) {
    }
  });
};
  • 某些参数有可能未必是 Promise 类型,因此参数使用前先通过 Promise.resolve 转换
Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    for (let p of promises) {
      // 保证所有的参数为 promise 实例,然后执行后续操作
      Promise.resolve(p).then((data) => {
        //...
      });
    }
  });
};
  • Iterator 类型我们是无法得知迭代深度,因此我们要维护一个 count 用来记录 promise 总数,同时维护 fulfilledCount 代表完成的 promise 数,当 count === fulfilledCount ,代表所有传入的 Promise 执行成功,返回数据。
Promise.all = function (promises) {
  let count = 0; // promise总数
  let fulfilledCount = 0; // 完成的promise数
  return new Promise((resolve, reject) => {
    for (let p of promises) {
      count++; // promise总数 + 1
      Promise.resolve(p).then((data) => {
        fulfilledCount++; // 完成的promise数量+1
        if (count === fulfilledCount) {
          // 代表最后一个promise完成了
          resolve();
        }
      });
    }
  });
};

有可能有的读者会好奇,为啥 count === fulfilledCount 可以判断所有的 promise 都完成了呐?

Promise.then 方法是 microTasks(微任务),当同步任务执行完毕后,Event Loop 才会去执行 microTaskscount++ 位于同步代码部分,因此在执行 promise.then 方法之前,已经成功的计算出 promise 的总数。

然后依次执行 promise.then 方法,fulfilledCount 增加,当 count === fulfilledCount 说明所有的 promise 都已经成功完成了。

  • 返回数据的顺序应该是 all 方法中比较难处理的部分。
  • 创建一个数组 result 存储所有 promise 成功的数据
  • for of 循环中,使用 let 变量定义 i,其值等于当前的遍历索引
  • let 定义的变量不会发生变量提升,因此我们直接令 result[i]promise 成功数据,这样就可以实现按参数输入顺序输出结果
Promise.all = function (promises) {
  const result = []; // 存储promise成功数据
  let count = 0;
  let fulfilledCount = 0;
  return new Promise((resolve, reject) => {
    for (let p of promises) {
      // i为遍历的第几个promise
      // 使用let避免形成闭包问题
      let i = count;
      count++;
      // 保证所有的参数为 promise 实例,然后执行后续操作
      Promise.resolve(p).then((data) => {
        fulfilledCount++;
        // 将第i个promise成功数据赋值给对应位置
        result[i] = data;
        if (count === fulfilledCount) {
          // 代表最后一个promise完成了
          // 返回result数组
          resolve(result);
        }
      });
    }
  });
};
  • 处理一下边界情况
    • 某个 promise 失败——直接调用 reject 即可
    • 传入 promise 数量为 0 ——返回空数组(规范规定)
    • 代码执行过程抛出异常 —— 返回错误信息
// 多余代码省略
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        // 3.捕获代码执行中的异常
        try{
            for (let p of promises) {
                Promise.resolve(p).then(data => {}
                                .catch(reject);  // 1.直接调用reject函数返回失败原因
                })
            }
            // 2.传入promise数量为0
            if (count === 0) {
                resolve(result)
            }
        } catch(error) {
            reject(error)
        }
    })
}

源码实现

我们把上面的代码汇总一下,加上详细的注释,同时测试一下手写 Promise.all 是否成功。

Promise.all = function (promises) {
  const result = []; // 存储promise成功数据
  let count = 0; // promise总数
  let fulfilledCount = 0; //完成promise数量
  return new Promise((resolve, reject) => {
    // 捕获代码执行中的异常
    try {
      for (let p of promises) {
        // i为遍历的第几个promise
        // 使用let避免形成闭包问题
        let i = count;
        count++; // promise总数 + 1
        Promise.resolve(p)
          .then((data) => {
            fulfilledCount++; // 完成的promise数量+1
            // 将第i个promise成功数据赋值给对应位置
            result[i] = data;
            if (count === fulfilledCount) {
              // 代表最后一个promise完成了
              // 返回result数组
              resolve(result);
            }
          })
          .catch(reject);
        // 传入promise数量为0
        if (count === 0) {
          resolve(result); // 返回空数组
        }
      }
    } catch (error) {
      reject(error);
    }
  });
};

测试代码(使用案例中的测试代码,附加 Iterator 类型 Stirng):

// 1. promise全部成功
Promise.all([p1, p2, p3])
  .then((data) => console.log(data)) // [1, 2, 3]
  .catch((error) => console.log(error));
// 2. 存在失败的promise
Promise.all([p1, p2, p3, p4])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 3. 存在多个失败的promise
Promise.all([p1, p2, p3, p4, p5])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 4. String 类型
Promise.all("zcxiaobao").then((data) => console.log(data));
// ['z', 'c', 'x', 'i', 'a', 'o', 'b', 'a', 'o']

Promise.allSettled

基础学习

不是每群兄弟们都会碰到好老大(all 方法),allSettled 方法他并不管兄弟们的死活,他只管兄弟们是否做了,而他的任务就是把所有兄弟的结果返回。

Promise.allSettled() 方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是 fulfilled 还是 rejected),返回的 Promise 对象才会发生状态变更。

还是以上面的例子为例,我们来看一下与 Promise.all 方法有啥不同。

// 1. promise 全部成功
Promise.allSettled([p1, p2, p3])
  .then((data) => console.log(data)) // [1, 2, 3]
  .catch((error) => console.log(error));
// 2. 存在失败的 promise
Promise.allSettled([p1, p2, p3, p4])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 3. 存在多个失败的 promise
Promise.allSettled([p1, p2, p3, p4, p5])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 4. 传入 String 类型
Promise.allSettled("zc").then((data) => console.log(data));

Promise静态四兄弟实现示例详解

从输出结果我们可以发现:

  • allSettled 方法只会成功,不会失败
  • 返回结果每个成员为对象,对象的格式固定
    • 如果 promise 成功,对象属性值 status: fulfilledvalue 记录成功值
    • 如果 promise 失败,对象属性值 status: rejectedreason 记录失败原因。
  • allSettled 方法也可以接受 Iterator 类型参数

思路分析

allSettled 方法与 all 方法最大的区别在于两点:

  • allSettled 方法没有失败情况
  • allSettled 方法返回有固定格式

我们可以围绕这两点改造 all 方法。

all 方法我们是通过计算成功数量来判断是否终结,allSettled 方法不计较成功失败,因此我们需要计算成功/失败总数量即可。

在累加完成总数量的过程中,分情况构造 allSettled 所需要的数据格式: 成功时压入成功格式,失败时压入失败格式

源码实现

由于有了 all 方法手写的基础,上面就不一步一步啰嗦的实现了。

Promise.allSettled = function (promises) {
  const result = [];
  let count = 0;
  let totalCount = 0; //完成promise数量
  return new Promise((resolve, reject) => {
    try {
      for (let p of promises) {
        let i = count;
        count++; // promise总数 + 1
        Promise.resolve(p)
          .then((res) => {
            totalCount++;
            // 成功时返回成功格式数据
            result[i] = {
              status: "fulfilled",
              value: res,
            };
            // 执行完成
            if (count === totalCount) {
              resolve(result);
            }
          })
          .catch((error) => {
            totalCount++;
            // 失败时返回失败格式数据
            result[i] = {
              status: "rejected",
              reason: error,
            };
            // 执行完成
            if (count === totalCount) {
              resolve(result);
            }
          });
        if (count === 0) {
          resolve(result);
        }
      }
    } catch (error) {
      reject(error);
    }
  });
};

Promise.race

基础学习

race 方法形象化来讲就是赛跑机制,只认第一名,不管是成功的第一还是失败的第一。

Promise.race() 方法同样是接收多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面案例中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

const p1 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        resolve(1)
    },1000)
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        reject(2)
    },2000)
})
const p3 = 3;
// 成功在先,失败在后
Promise.race([p1, p2]).then(res => {console.log(res)}) // 1
// 同步在先,异步在后
Promise.race([p1, p3]).then(res => console.log(res)) // 3
// String
Promise.race('zc').then(res => console.log(res)) // z

思路分析

race 方法就没有那么多弯弯绕绕了,只要某个 promise 改变状态就返回其对应结果。

因此我们只需监听每个 promisethencatch 方法,当发生状态改变,直接调用 resolvereject 方法即可。

源码实现

Promise.race(promises) {
    return new Promise((resolve, reject) => {
        for (let p of promises) {
            // Promise.resolve将p进行转化,防止传入非Promise实例
            // race执行机制为那个实例发生状态改变,则返回其对应结果
            // 因此监听
            Promise.resolve(p).then(resolve).catch(reject);
        }
    })
}

Promise.any

基础学习

any 方法形象化来说是天选唯一,只要第一个成功者。如果全部失败了,就返回失败情况。

ES2021 引入了 Promise.any() 方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。

any 方法与 race 方法很像,也存在短路特性,只要有一个实例变成 fulfilled 状态,就会返回成功的结果;如果全部失败,则返回失败情况。

// 成功的promise
const p1 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        resolve(1)
    },1000)
})
// 失败的promise
const p2 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        reject(2)
    },2000)
})
//失败的promise
const p3 = new Promise((resolve, reject) => {
    reject(3)
})
// 存在一个成功的promise
Promise.any([p1,p2]).then(res => console.log(res))// 1
// 全部失败的promise
Promise.any([p2,p3]).then(res => console.log(res))
                    .catch(error => console.log(error)) // AggregateError: All promises were rejected
// String类型
Promise.any('zc').then(res => console.log(res)) // z

通过上述输出结果我们可以发现:

  • any 方法也可以接受 Iterator 格式参数
  • 当一个 promise 实例转变为 fulfilled 时,any 返回成功的 promise ,值为最早成功的 promise值。
  • promise 全部失败时,any 返回失败的 promise ,值固定为 AggregateError: All promises were rejected

思路分析

上面我们分析了 any 方法的机制:

  • 某个实例转化为 fulfilledany 随之返回成功的 promise。因此这里我们就可以类似使用 race 的方法,监测每个 promise 的成功。
  • 全部实例转化为 rejectedany 返回 AggregateError: All promises were rejected。这里我们可以参考 all 方法的全部成功,才返回成功,因此我们需要累计失败数量,当 rejectCount === count 时,返回失败值。

源码实现

Promise.any = function(promises) {
    return new Promise((resolve,reject) => {
        let count = 0;
        let rejectCount = 0;
        let errors = [];
        let i = 0;
        for (let p of promises) {
            i = count;
            count ++;
            Promise.resolve(p).then(res => {
                resolve(res)
            }).catch(error => {
                errors[i] = error;
                rejectCount ++;
                if (rejectCount === count) {
                    return reject(new AggregateError(errors))
                }
            })
        }
        if(count === 0) return reject(new AggregateError('All promises were rejected'))        
    })
}

以上就是Promise静态四兄弟实现示例详解的详细内容,更多关于Promise静态实现的资料请关注三水点靠木其它相关文章!


Tags in this post...

Javascript 相关文章推荐
JavaScript 函数调用规则
Sep 14 Javascript
jQuery中多个元素的Hover事件解决方案
Jun 12 Javascript
JavaScript定时器和优化的取消定时器方法
Jul 03 Javascript
jQuery中的基本选择器用法学习教程
Apr 14 Javascript
实用jquery操作表单元素的简单代码
Jul 04 Javascript
Bootstrap导航条可点击和鼠标悬停显示下拉菜单
Nov 25 Javascript
JS匿名函数实例分析
Nov 26 Javascript
AngularJS页面传参的5种方式
Apr 01 Javascript
JS组件系列之MVVM组件构建自己的Vue组件
Apr 28 Javascript
JS 组件系列之Bootstrap Table的冻结列功能彻底解决高度问题
Jun 30 Javascript
JS计算距当前时间的时间差实例
Dec 29 Javascript
Vue服务端渲染实践之Web应用首屏耗时最优化方案
Mar 22 Javascript
Three.js实现雪糕地球的使用示例详解
二维码条形码生成的JavaScript脚本库
Jul 07 #Javascript
JS实现简单的九宫格抽奖
JS实现九宫格拼图游戏
JavaScript实现九宫格拖拽效果
JS实现简单九宫格抽奖
Jun 28 #Javascript
Vite + React从零开始搭建一个开源组件库
Jun 25 #Javascript
You might like
2019年中国咖啡业现状与发展趋势
2021/03/04 咖啡文化
php GD绘制24小时柱状图
2008/06/28 PHP
PHP isset()与empty()的使用区别详解
2010/08/29 PHP
PHP 基于文件头的文件类型验证类函数
2012/05/01 PHP
WordPress迁移时一些常见问题的解决方法整理
2015/11/24 PHP
ThinkPHP实现静态缓存和动态缓存示例代码
2017/05/02 PHP
基于Laravel 5.2 regex验证的正确写法
2019/09/29 PHP
javascript模拟select,jselect的方法实现
2012/11/08 Javascript
jQuery获取Select选择的Text和Value(详细汇总)
2013/01/25 Javascript
js日期相关函数总结分享
2013/10/15 Javascript
JS 实现BASE64_ENCODE和BASE64_DECODE(实例代码)
2013/11/13 Javascript
JS实现一个列表中包含上移下移删除等功能
2014/09/24 Javascript
JS实现的5级联动Select下拉选择框实例
2015/08/17 Javascript
妙用缓存调用链实现JS方法的重载
2018/04/30 Javascript
js实现导航跟随效果
2018/11/17 Javascript
详解JavaScript中关于this指向的4种情况
2019/04/18 Javascript
JS工厂模式开发实践案例分析
2019/10/17 Javascript
[07:20]2014DOTA2西雅图国际邀请赛 选手讲解积分赛第二天
2014/07/11 DOTA
python处理二进制数据的方法
2015/06/03 Python
Python编程实现数学运算求一元二次方程的实根算法示例
2017/04/02 Python
python数据抓取分析的示例代码(python + mongodb)
2017/12/25 Python
用Python实现读写锁的示例代码
2018/11/05 Python
15行Python代码实现网易云热门歌单实例教程
2019/03/10 Python
详解Python locals()的陷阱
2019/03/26 Python
Python3实现计算两个数组的交集算法示例
2019/04/03 Python
Python实现疫情通定时自动填写功能(附代码)
2020/05/27 Python
keras训练曲线,混淆矩阵,CNN层输出可视化实例
2020/06/15 Python
Fashion Eyewear美国:英国线上设计师眼镜和太阳镜的零售商
2016/08/15 全球购物
Sunglasses Shop荷兰站:英国最大的太阳镜独立在线零售商和供应商
2017/01/08 全球购物
EMU Australia澳大利亚官网:澳大利亚本土雪地靴品牌
2019/07/24 全球购物
党校学习自我鉴定
2014/02/24 职场文书
企业委托书范本
2014/09/13 职场文书
2014幼儿园卫生保健工作总结
2014/12/05 职场文书
小学一年级班主任工作经验交流材料
2015/11/02 职场文书
php中pcntl_fork详解
2021/04/01 PHP
MySQL sql_mode的使用详解
2021/05/08 MySQL