实现promise.all方法

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现promise.all方法相关的知识,希望对你有一定的参考价值。

参考技术A

1- Promise.all 的用法

逆向的去实现功能,最关键的前提是准确了解API,输入、输出、和注意事项。

这里直接引用MDN:

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。

MDN后面也给出了详细说明:

此方法在集合多个promise的返回结果时很有用。

完成(Fulfillment):

    如果传入的可迭代对象为空,Promise.all会同步地返回一个已完成(resolved)状态的promise。

    如果所有传入的promise都变为完成状态,或者传入的可迭代对象内没有promise,Promise.all返回的promise异步地变为完成。

    在任何情况下,Promise.all返回的promise的完成状态的结果都是一个数组,它包含所有的传入迭代参数对象的值(也包括非promise值)。

    失败/拒绝(Rejection):

    如果传入的promise中有一个失败(rejected),Promise.all异步地将失败的那个结果给失败状态的回调函数,而不管其它promise是否完成。

    个人感觉MDN解释的比较清楚了,还是云里雾里的话,可以反复细品一下上面的说明。或者结合下面的代码去理解。

    2 - 手动实现Promise.all

    面试美团的时候,面试官看我写不出来,就说“既然你知道了输入和输出是什么,应该能写出来了....”。

    面试官其实不是在鄙视我“我不行”,而是在试图引导我的思路,只是当时自己编程思路太差,最后还是没写出来。

    但是面试官的提示,确实是一个很好的思考思路。 先不管完整的Promise.all代码是什么样子,甚至包括优化啥的。先想想"Promise.all(iterable) 方法返回一个 Promise实例",就这么简单的一句话怎么写呢?

    function myPromiseAll(arr)  // 参数是一个iterable对象,一般是数组
      // 返回一个Promise实例
       return new Promise((resolve, reject) =>
           resolve("面试官让我写一个Promise.all");
           // 或者
           // reject("我太笨了,写不出来");
           
       );



    let pResult = myPromiseAll([]);  // 先不要去想数组有没有元素
    pResult.then(value=>
       console.log(value);  // 输出: 面试官让我写一个Promise.all
    , err=>
       console.log(err);
    )

    好了,如过看懂了,那么最重要的一步就完成了。是不是很简单。

    接下来,只要根据MDN的说明,一步步完善内部函数的功能就行了。

    我们先从“完成”情况下手:
    完成(Fulfillment):

    A. 如果传入的可迭代对象为空,Promise.all会同步地返回一个已完成(resolved)状态的promise。

    B. 如果所有传入的promise都变为完成状态,或者传入的可迭代对象内没有promise,Promise.all返回的promise异步地变为完成。

    C. 在任何情况下,Promise.all返回的promise的完成状态的结果都是一个数组,它包含所有的传入迭代参数对象的值(也包括非promise值)。

    请先看C,在完成情况下,会始终返回一个数组.

    function myPromiseAll(arr)
       // 定义一个数组
       let result = [];

       return new Promise((resolve, reject) =>

           // 现在只考虑 “在完成情况下” ,会返回一个数组
           resolve(result);
         
           
       );


    let pResult = myPromiseAll([]);
    pResult.then(value=>
       console.log(pResult);  // 输出 Promise <state>: "fulfilled", <value>: []
       console.log(value); // 输出:[]
    )

    那么下面来实现B,B里有分两种情况:

    元素是Promise实例

    元素不是Promise实例

    那先考虑元素不是Promise实例,从简单的开始

    function myPromiseAll(arr)
       let result = [];

       return new Promise((resolve, reject) =>

           for(let i = 0; i < arr.length; i++)
               result.push(arr[i]);
           

           resolve(result);
         
           
       );



    let pResult = myPromiseAll([1,2,3]);  // 元素不是Promise实例
    pResult.then(value=>
       console.log(pResult); // 输出:  Promise <state>: "fulfilled", <value>: (3) […]
       console.log(value); // 输出: Array(3) [ 1, 2, 3 ]
    )

    最难的来了,元素都是Promise实例呢?
    别慌,先写顶层设计,再想细节(自上向下编程)

    function myPromiseAll(arr)
       let result = [];

       return new Promise((resolve, reject) =>


           for(let i = 0; i < arr.length; i++)
               
               if(/*如果是Promise实例*/)
                   
                else
                   result.push(arr[i]);
               


           


           // 先想想,resolve放在这里,对不对?
           resolve(result);
         
           
       );

    继续完善

    function myPromiseAll(arr)
       let result = [];

       return new Promise((resolve, reject) =>

           // 数组为空,直接resolve了
           if(arr.length == 0)
               resolve(result);
           


           for(let i = 0; i < arr.length; i++)
               
               if(arr[i].then) // 若元素是Promise实例,则会有then函数,这里只是简单的作为判断标准
                   
                   // 元素是Promise
                   arr[i].then(value =>
                       console.log(value);
                       result.push(value);

                       // 想一想什么时候resolve呢?--- 所有Promise实例都完成了
                       if(result.length == arr.length)
                          console.log("所有都完成了")
                           resolve(result);
                       

                   )
                   
                else
                   result.push(arr[i]);

                   // 这段代码跟上面重复,想想,能不能提取放到外面,会出现什么情况呢?
                   if(result.length == arr.length)
                       resolve(result);
                   

               
           

       );


    let p1 = new Promise((resolve, reject)=>
       setTimeout(resolve, 2000, "P1 resolved");
    )

    let p2 = new Promise((resolve, reject)=>
       setTimeout(resolve, 3000, "P2 resolved");
    )

    let p3 = new Promise((resolve, reject)=>
       setTimeout(resolve, 4000, "P3 resolved");
    )


    let pResult = myPromiseAll([p1,p2,p3]);
    pResult.then(value=>
       console.log(pResult);
       console.log(value);
    )

    // 输出
    // P1 resolved
    // P2 resolved
    // P3 resolved
    // 所有都完成了
    // Promise <state>: "fulfilled", <value>: (3) […]
    // Array(3) [ "P1 resolved", "P2 resolved", "P3 resolved" ]

    完成情况写完了,还剩失败情况:

    如果传入的 promise 中有一个失败(rejected),Promise.all 异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成。

    function myPromiseAll(arr)
       let result = [];

       return new Promise((resolve, reject) =>

           // 如果数组为空,直接返回空数组
           if(arr.length == 0)
               resolve(result);
           


           for(let i = 0; i < arr.length; i++)
               
               if(arr[i].then) // 若元素是Promise实例,则会有then函数,这里只是简单的作为判断标准
                   
                   // 元素是Promise
                   arr[i].then(value =>
                       console.log(value);
                       result.push(value);

                       // 想一想什么时候resolve呢?
                       if(result.length == arr.length)
                           console.log("所有都成功了")
                           resolve(result);
                       

                   , err =>
                       console.log("很不幸,其中一个失败了");
                       // 注意到没, 这里没有像上面的判断 result.length == arr.length, 为什么?
                       // 只要碰到 resolve 或 reject ,就结束了
                       reject(err);
                   )
                   
                else
                   result.push(arr[i]);

                   // 这段代码跟上面重复,想想,能不能提取放到外面,会出现什么情况呢?
                   if(result.length == arr.length)
                       resolve(result);
                   

               
           

       );


    let p1 = new Promise((resolve, reject)=>
       setTimeout(reject, 2000, "P1 rejected");
    )

    let p2 = new Promise((resolve, reject)=>
       setTimeout(resolve, 3000, "P2 resolved");
    )

    let p3 = new Promise((resolve, reject)=>
       setTimeout(resolve, 4000, "P3 resolved");
    )


    let pResult = myPromiseAll([p1,p2,p3]);
    pResult.then(value=>
       console.log(pResult);  // 是输出成功
       console.log(value);
    , err =>
       console.log(pResult);   // 还是输出失败呢?
       console.log(err);
    )

    // 输出
    // 很不幸,其中一个失败了
    // Promise <state>: "rejected"
    // P1 rejected
    // P2 resolved
    // P3 resolved

    为什么最后还是输出了 P2 和 P3 的结果呢? 这是因为,尽管遇到了P1就reject了,然而 P2 和 P3 仍在执行。注意MDN说的是“不管其他Promise是否完成”,而不是“其他Promise被stop”。

    let p2 = new Promise((resolve, reject)=>
       setTimeout(resolve, 3000, "P2 resolved");
    )

    let p3 = new Promise((resolve, reject)=>
       setTimeout(resolve, 4000, "P3 resolved");
    )


    let pResult = myPromiseAll([p2,55,p3]);
    pResult.then(value=>
       console.log(pResult);
       console.log(value); // 输出 [55, 'P2 resolved', 'P3 resolved']
    , err =>
       console.log(pResult);
       console.log(err);

使用 Promise.all() 在 Promise 实现时执行操作

【中文标题】使用 Promise.all() 在 Promise 实现时执行操作【英文标题】:Perform actions as promises get fulfilled using Promise.all() 【发布时间】:2017-02-05 07:54:12 【问题描述】:

我可以用Promise.all(array) 异步解决一堆promise。然而.then() 只会在所有这些承诺都得到解决后运行。当承诺得到解决时,我如何执行操作?

例如,我想使用 Promise.all() 异步加载文章中的所有段落 - 这样网络请求会立即全部触发。如果第 1 段完成加载,我希望它呈现到页面 - 但只有在第 2 段之前完成加载,然后我希望第 2 段加载。如果第 3 段已完成加载而第 2 段未加载,我希望 3 在渲染到页面之前等待 2。以此类推。

我尝试了类似的方法,但我不知道下一步该做什么:

var getStuff = function(number, time)
  return new Promise(function(resolve, reject)
    window.setTimeout(function()resolve(`$number - Done.`), time);
  );
;

Promise.all([ getStuff(1, 200),
              getStuff(2, 100),
              getStuff(3, 250),
              getStuff(4, 200),
              getStuff(5, 300),
              getStuff(6, 250),
              getStuff(7, 5000)])
.then(function(data)
  console.log(data);
);

我怎样才能让数据的控制台日志一个接一个地发生 - 在发出下一个请求之前,不用then() 解决每个承诺?有没有更好的方法来做到这一点?

【问题讨论】:

一些 Promise 库有 progress 回调。 没有办法使用原生 es6 获得这种行为?不向我的项目添加另一个库? 为什么不为每个承诺只渲染getStuff(...)then 我不认为它是本地可用的。不过,您仍然可以拥有一系列承诺,并且每个承诺都有一个单独的 then() @nem035 哦,我错过了那部分。 【参考方案1】:

您无法使用Promise.all 实现此顺序,因为从Promise.all 返回的promise 会等待所提供数组中的所有promise 在它自己解析之前同时(而不是顺序)解析。

相反,您可以单独创建它们的请求的承诺和触发:

// create promises and make concurrent requests
const s1 = getStuff(1, 200);
const s2 = getStuff(2, 100);
const s3 = getStuff(3, 250);
// ...

然后创建一个关于如何处理它们的反应链(stuff1 在 stuff2 之前,stuff2 在 stuff3 之前,等等)

// create a chain of reaction order to the results of parallel promises
s1
  .then(console.log) // s1 resolved: log result
  .then(() => s2)    // chain s2
  .then(console.log) // s2 resolved: log result
  .then(() => s3)    // chain s3
  // ...
  .then(() =>       // chain another function at at the end for when all promises resolved
    // all promises resolved (all data was logged)
  

要按照创建 Promise 的顺序对 Promise 结果做出反应,您可以更改 getStuff 函数以使用 Array.prototype.reduce 动态链接反应:

var times = [200, 100, 250, 200, 300, 250, 5000];

var getStuff = function(time, index)  // swap the order of arguments so number is the index passed in from Array.map
  return new Promise((resolve, reject) => 
    window.setTimeout(() => 
      resolve(`$index + 1 - Done.`); // use index + 1 because indexes start at 0
    , time);
  );
;

times
  // map each time to a promise (and number to the index of that time + 1) and fire of a request
  .map(getStuff)
  // dynamically build a reaction chain for the results of promises
  .reduce((chain, promise) => 
    return chain
      .then(() => promise)
      .then(console.log);
  , Promise.resolve())
  .then(() => 
    // all promises resolved (all data was logged in order)
  );

【讨论】:

啊,这很有道理!基本上,一旦我调用 getStuff,他们就会发出网络请求,如果在之后执行 then 链,我将得到我想要的行为,因为每个 promise 都将在链中调用 .then 之前解决? 一旦你创建了一个 Promise,它就会运行你的请求。在那之后,您可以在任何时候构建链,无论该承诺是否已解决。如果承诺在您创建链之前解决,它将保持其价值,直到您创建链。如果你先做链,链会等待 Promise 接收值。这就是诺言的美妙之处。它们总是异步解析,你总是可以假装在用它们编码时已经有了这些值。【参考方案2】:

nem035 的回答很到位。我想指出,在这种情况下,您通常希望在请求发生时采取相同的操作,并在请求全部完成时采取另一个行动。

您可以使用.all.map

Promise.all([ getStuff(1, 200),
            getStuff(2, 100),
            getStuff(3, 250),
            getStuff(4, 200),
            getStuff(5, 300),
            getStuff(6, 250),
            getStuff(7, 5000)]
.map(request => request.then(v => 
   console.log("Request done! Got," v); // or some action per request
   return v;
)).then(data => console.log(data));

您可以通过 .map 进一步控制这一点,因为您对每个请求都使用相同的功能:

Promise.all([[1, 200],
            [2, 100],
            [3, 250],
            [4, 200],
            [5, 300],
            [6, 250],
            [7, 5000]])
.map((a, b) => getStuff(a, b))
.map(request => request.then(v => 
   console.log("Request done! Got," v); // or some action per request
   return v;
)).then(data => console.log(data));

还有:

Promise.all([200, 100, 250, 200, 300, 250, 5000])
.map((a, i) => getStuff(a, i + 1))
.map(request => request.then(v => 
   console.log("Request done! Got," v); // or some action per request
   return v;
)).then(data => console.log(data));

或与蓝鸟:

const sideEffect = v => console.log("Got partial result", v));
const data = [200, 100, 250, 200, 300, 250, 5000];
Promise.map(data, (a, i) => getStuff(a, i + 1).tap(sideEffect))
       .then(data => console.log(data));

当然 - 你应该只修复你的后端,要求客户端对数据的不同部分发出 7 次请求是完全不合理的 - 让后端取值范围。

【讨论】:

【参考方案3】:

我知道它不是原生的,但是对于 bluebird,您可以使用 Promise.some(在实现 count 承诺后完成)或 Promise.mapSeries(在系列中完成承诺)以某种方式实现您期望的流程。

Bluebird API

【讨论】:

【参考方案4】:

正常操作:您可以安全使用Promise.all()。 Promise 执行器将被并行触发,结果将按照您将 Promise 插入 Promise 数组的顺序返回。然后,您可以按照自己喜欢的方式对它们进行排序。例如在下面的 sn-p 中,我们有五个 Promise,每个 Promise 都会在 5 秒内随机解决。无论他们的解决时间如何,您都会在最新解决时得到结果;

var promises = [ new Promise( v => setTimeout(_ => v("1st paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("2nd paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("3rd paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("4th paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("5th paragraph text"),~~(Math.random()*5000))),
               ];
Promise.all(promises)
       .then(result => console.log(result.reduce((p,c) => p + "\n" + c)));

你想要什么:但是你不想等到最后一个完成,而是想按顺序处理它们,尽快解决它们。那么Array.prototype.reduce() 是你这里最好的朋友。比如

var promises = [ new Promise( v => setTimeout(_ => v("1st paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("2nd paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("3rd paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("4th paragraph text"),~~(Math.random()*5000))),
                 new Promise( v => setTimeout(_ => v("5th paragraph text"),~~(Math.random()*5000)))
               ];
promises.reduce((p,c) => p.then(result => (console.log(result + "\n"),c)))
        .then(result => (console.log(result + "\n")));

请多次运行代码以查看代码的行为方式。文本将在承诺解决后尽快更新,但前提是轮到它。因此,如果第 1 次在第 2 次之后解决,我们将看到第 1 次和第 2 次按顺序同时出现,但他们不会等待第 3 次解决以此类推...

【讨论】:

以上是关于实现promise.all方法的主要内容,如果未能解决你的问题,请参考以下文章

假设把官方的promise.all去掉,实现自己的promise.all方法

Promise静态方法实现(all race finally resolve reject)

实现Promise的first等各种变体

因为实现不了Promise.all,一场面试凉凉了

使用 Promise.all() 在 Promise 实现时执行操作

Promise.all()使用方法