ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【JS】强化Promise理解,从零手写属于自己的Promise.all与Promise.race

2022-02-19 18:33:06  阅读:242  来源: 互联网

标签:index resolve const JS race Promise run 执行


壹 ❀ 引

一个思路搞定三道Promise并发编程题,手摸手教你实现一个Promise限制器一文中,我们在文章结尾留了一个疑问,关于第三题的实现能否解决当每次调用时间都不相等的情况(比如第二次调用要早于第一次调用结束),那么最终得到的结果顺序还能与参数顺序保持一致问题?在分享我踩坑过程中其实已经证明是可以满足这种场景的,但为什么呢?

我们可以尝试运行下面代码,你会发现尽管输出顺序不对,但每次indexvalue都是正确的配队关系:

const time = [1, 3, 4, 2, 1];
// 假设请求API为
function request(params) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(params), time[Math.floor(Math.random() * 5)] * 1000);
  });
}

// 最多处理3个请求的调度器
function Scheduler(list = [], limit = 3) {
  let count = 0;
  // 用于统计成功的次数
  let resLength = 0;
  // 浅拷贝一份,原数据的length我们还有用
  const pending = [...list];
  const resList = [];

  // 一定得返回一个promise
  return new Promise((resolve, reject) => {
    const run = () => {
      if (!pending.length || count >= limit) return;
      count++;
      const index = list.length - pending.length;
      const params = pending.shift();

      request(params)
        .then((res) => {
          console.log('当前index为:', index, '当前结果为:', res);
          count--;
          resLength++;
          // 按index来保存结果
          resList[index] = res;
          // 全部成功了吗?没有就继续请求,否则resolve(resList)跳出递归;
          resLength === list.length ? resolve(resList) : run();
        })
        .catch(reject) // 有一个失败就直接失败
    };

    // 遍历,模拟前两次依次调用的动作,然后在run内部控制如何执行
    list.forEach(() => run());
  })
}

Scheduler([1, 2, 3, 4, 5]).then((res) => console.log('最终结果为:', res)); // 1 2 3 4 5

可以毫不夸张的说,上述代码已经算是一个满足了并发限制器功能的Promise.all了,只要我们去除掉限制部分代码,稍加修改就能分别得到Promise.allPromise.race的实现,但在改写之前我们还是先解释为什么尽管执行顺序不同,为什么结果与参数仍是对应关系的问题,而且我觉得也只剩下这一个稍微有点饶的疑惑点了。

贰 ❀ 执行上下文与闭包

还是模拟下上述代码的执行过程,当forEach遍历调用run时,可以确定的是,如下代码绝对是同步执行完成的,且5次都是同步跑完:

// 获得当前的index
const index = list.length - pending.length;
// 获取当前请求需要的参数
const params = pending.shift();

异步的是request(),你什么时候能执行我不关系,反正一开始我已经把你执行需要的参数成对的给你准备好了。有同学的疑问可能就在于,我也知道这些参数一开始是成对的,那Promise执行顺序被打乱之后,后执行的Promise又怎么知道之前的index是多少呢,这是怎么对应上的?

问题又回到了老生常谈的执行上下文与闭包问题。我们知道代码在执行前都要经历执行上下文创建阶段与执行阶段,而一个函数的执行上下文在它创建时就已经决定了,而不是执行时,这也是典型的静态作用域的概念,比如:

const a = 1;
const fn = () => {
  console.log(a);
};
(() => {
  const a = 2;
  fn();// ???
})();

以上代码fn执行时输出1,这是因为fn的执行上下文在创建时决定,而不是执行时,所以不管你在哪调用它,它能访问的永远是同出一个作用域下的const a = 1,这里就当简单复习下静态作用域的概念。

回到上文我们实现的代码,我们知道request().then()这个调用行为是同步的,异步的是requset内部修改状态的代码,以及状态修改完成后才能执行的.then()所注册的回调函数,注意.then()注册回调的行为是同步的,这一点你一定要搞清楚。

也就是说,在五次同步的run()调用过程中,indexparams在不断的同步生成,.then()也在不断的同步注册回调任务。

还记得javascript中什么是闭包吗?所谓闭包,就是能访问外部函数作用域中自由变量的函数,而此时外部函数很明显就是new Promise(fn)fn,内部函数就是.then()注册的回调函数,自由变量自然就是上面同步生成的index了,而闭包的一大特性就是,即便外部上下文已经销毁,它依旧能访问到当时创建它的执行上下文,以及上下文中的那些自由变量(静态作用域的魅力)。

因此即便run()在不断的执行与销毁,.then()在注册callback时这些回调已经自带了它们后续要执行的上下文,这就像人能在地球生活,是因为地球这个上下文提供了空气,水等物质,而宇航员离开了地球依旧能生存,是因为他们自带了氧气等生活物质,即使他们已不在地球这个上下文了。

假设我们断点查看任意一个Promise执行,你会发现每次执行时都有一个closure作用域,这就是闭包的英文单词:

若你对闭包以及执行上下文有一定疑惑,可以阅读博主这两篇文章:

一篇文章看懂JS闭包,都要2020年了,你怎么能还不懂闭包?

一篇文章看懂JS执行上下文

叁 ❀ 改写实现Promise.all

好了,解释完结果与参数的对应关系后,我们直接改写上述代码,得到我们的PromiseAll,它满足2个特性:

  • 只有所有Promise全部resolve时才会resolve,且结果顺序与参数保持一致。
  • 任意一个失败时直接reject
function PromiseAll(promiseList = []) {
  // 用于统计成功的次数
  let resLength = 0;
  // 浅拷贝一份,原数据的length我们还有用
  const pending = [...promiseList];
  const resList = [];

  // 一定得返回一个promise
  return new Promise((resolve, reject) => {
    const run = () => {
      if (!pending.length) return;
      const index = promiseList.length - pending.length;
      const promise = pending.shift();

      promise.then((res) => {
          resLength++;
          // 按index来保存结果
          resList[index] = res;
          // 全部成功了吗?没有就继续请求,否则resolve(resList)跳出递归;
          resLength === promiseList.length ? resolve(resList) : run();
        })
        .catch(reject) // 有一个失败就直接失败
    };

    // 遍历,模拟前两次依次调用的动作,然后在run内部控制如何执行
    promiseList.forEach(() => run());
  })
}

执行如下代码,你会发现结果完全符合预期:

const P1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 3000)
});
const P2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(2), 1000)
});
const P3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(3), 2000)
});
PromiseAll([P1, P2, P3]).then((res) => console.log('最终结果为:', res)); // 1 2 3 4 5

假设你将上述三个Promise中任意一个的状态改为reject,最终Promise也只会得到失败的结果,而上述的改写,我们还真的只是去除了限制器的代码,理解起来也非常简单。

肆 ❀ 改写实现Promise.race

race顾名思义就是赛跑,多个Promise第一个执行完状态是啥就是啥,所以针对上面的代码,我们又只需要删除掉resLength === promiseList.length以及递归的相关逻辑即可,直接上代码:

function PromiseRace(promiseList = []) {
  // 一定得返回一个promise
  return new Promise((resolve, reject) => {
    const run = (p) => {
      p.then((res) => {
          resolve(res);
        })
        .catch(reject) // 有一个失败就直接失败
    };

    // 遍历,模拟前两次依次调用的动作,然后在run内部控制如何执行
    promiseList.forEach((p) => run(p));
  })
}

再运行上面的例子,同样符合预期。

伍 ❀ 总

其实从上篇的文章的题三,到后来的all race的实现,你会发现难度反而是递减的,所以如果你对于这篇文章存在疑虑,我还是建议阅读下前两篇文章:

因两道Promise执行题让我产生自我怀疑,从零手写Promise加深原理理解

一个思路搞定三道Promise并发编程题,手摸手教你实现一个Promise限制器

建议按顺序阅读这三篇文章,我想你对于Promise的理解以及手写,一定会上升一个高度,那么到这里本文结束。

标签:index,resolve,const,JS,race,Promise,run,执行
来源: https://www.cnblogs.com/echolun/p/15913303.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有