V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
shaoyaoju
V2EX  ›  程序员

在 Map 遍历中使用 async 函数

  •  
  •   shaoyaoju ·
    juzhiyuan · 2019-10-27 11:36:33 +08:00 · 3695 次点击
    这是一个创建于 1614 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文 https://blog.shaoyaoju.org/javascript-async-with-map/

    有时需要使用 Sleep 函数阻塞代码一段时间,该函数常规实现与调用方式如下:

    // Sleep Function
    const sleep = async ms => new Promise(resolve => setTimeout(resolve, ms))
    
    // Usage
    (async () => {
       await sleep(3000)
    })
    

    但在 Array.prototype.map 中使用时,却有着错误的表现,具体如下:

    // code snippet 1
    [1, 2].map(async num => {
      console.log('Loop Start')
      console.log(num)
      await sleep(3000)
      console.log('Loop End')
    })
    
    // expected output
    //    Loop Start
    //    1
    //        Wait for about 3s
    //    Loop End
    //    Loop Start
    //    2
    //        Wait for about 3s
    //    Loop End
    
    // Actual output
    //    Loop Start
    //    1
    //    Loop Start
    //    2
    //         Wait for about 3s
    //    Loop End
    //    Loop End
    

    我们期望的是,在每一次循环时,暂停约 3s 钟时间后再继续执行;但实际表现是:每当执行

      await sleep(3000)
    

    时,没有等待结果返回便进入到了下一次循环。之所以产生错误的表现,原因是:

    当 async 函数被执行时,将立即返回 pending 状态的 Promise ( Promise 是 Truthy 的)!因此,在 map 循环时,不会等待 await 操作完成,而是直接进入下一次循环,所以应当配合 for 循环使用 async。

    验证一下,我们将 code snippet 1 做一下修改:

    // code snippet 2
    const sleep = ms => new Promise(resolve => {
      console.log('sleep')
      setTimeout(() => {
        console.log('resolve')
        resolve()
      }, ms)
    })
    
    const mapResult = [1, 2].map(async num => {
      console.log('Loop Start')
      console.log(num)
      await sleep(3000)
      console.log('Loop End')
    })
    
    console.log('mapResult', mapResult)
    
    // Actual output
    //    Loop Start
    //    1
    //    sleep
    //    Loop Start
    //    2
    //    sleep
    //    mapResult [ Promise { <pending> }, Promise { <pending> } ]
    //    resolve
    //    Loop End
    //    resolve
    //    Loop End
    

    可以看到,使用了 async 函数后的 map 方法,其返回值为

    // mapResult [ Promise { <pending> }, Promise { <pending> } ]
    

    即包含了多个状态为 pending 的 Promise 的数组!

    另外,如果只是循环而不需要操作 map 返回的数组,那么也应当使用 for 循环。

    对于 forEach 而言,参考 MDN 中它的 Polyfill 可知,若回调函数为异步操作,它将会表现出并发的情况,因为它不支持等待异步操作完成后再进入下一次循环。

    感谢 @杨宁 提供的使用 Array.prototype.reduce 解决的方法:

    // https://codepen.io/juzhiyuan/pen/jONwyeq
    
    const sleep = wait => new Promise(resolve => setTimeout(resolve, wait));
    
    const __main = async function() {
      // 你的需求其实是在一组 task 中,循环执行,每个都 sleep,并返回结果
      const tasks = [1, 2, 3];
    
      let results = await tasks.reduce(async (previousValue, currentValue) => {
        // 这里是关键,需要 await 前一个 task 执行的结果
        // 实际上每个 reduce 每个循环执行都相当于 new Promise
        // 但第二次执行可以拿到上一次执行的结果,也就是上一个 Promise
        // 每次执行只要 await 上一个 Promise,即可实现依次执行
        let results = await previousValue
        console.log(`task ${currentValue} start`)
        await sleep(1000 * currentValue);
        console.log(`${currentValue}`);
        console.log(`task ${currentValue} end`);
        results.push(currentValue)
        return results
      }, []);
    
      console.log(results);
    }
    
    __main()
    
    // Actual output:
    //    task 1 start
    //    1
    //    task 1 end
    //    task 2 start
    //    2
    //    task 2 end
    //    task 3 start
    //    3
    //    task 3 end
    //    [1, 2, 3]
    

    参考

    1. http://devcheater.com/
    2. https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404
    3. https://zellwk.com/blog/async-await-in-loops/

    本篇文章由一文多发平台ArtiPub自动发布

    13 条回复    2019-10-28 11:31:50 +08:00
    popn74
        1
    popn74  
       2019-10-27 13:05:51 +08:00
    添加 promise 数组
    然后 promise.all
    secondwtq
        2
    secondwtq  
       2019-10-27 13:07:13 +08:00 via iPhone
    恭喜你重新实现了 mapM 的某个实例
    momocraft
        3
    momocraft  
       2019-10-27 13:13:00 +08:00
    map 的语义其实就更接近并行,需要等的用 reduce 更自然些
    yyfearth
        4
    yyfearth  
       2019-10-27 14:15:47 +08:00   ❤️ 1
    @shaoyaoju 这么麻烦 要等待用 for of + await 就是了 非要用 forEach/map 或者 reduce 干嘛
    并行用 Promise.all + array.map 就是
    sam014
        5
    sam014  
       2019-10-27 14:40:44 +08:00
    我们期望的是,在每一次循环时,暂停约 3s 钟时间后再继续执行;
    ---------------------------------------------------------------------------------------
    这种情况用 生成器 应该好做一些
    lqzhgood
        6
    lqzhgood  
       2019-10-27 17:58:03 +08:00 via Android
    foreach 是并行 for 是串行
    理论上不是 foreach 更快么…
    我怎么记得 for 更快

    等下起来验证一下

    const a = new Array(10000);
    terax
        7
    terax  
       2019-10-27 18:31:46 +08:00 via iPhone
    据我的理解,js async await 并不是阻塞代码的执行(只是 promise 语法糖,并不会改变 runtime )。可以理解为 await 之后的代码都被包在了 promise.then 里面了。
    dremy
        8
    dremy  
       2019-10-27 21:00:15 +08:00 via iPhone
    直接一个 for 循环+await 就能搞定的事,何必为了用函数式而为难自己呢……
    miniwade514
        9
    miniwade514  
       2019-10-28 08:19:14 +08:00 via iPhone
    首先第一个 sleep 函数实现的就有问题,已经返回 promise 了为啥还要加 async ?感觉没有理解 async 的作用
    ericls
        10
    ericls  
       2019-10-28 08:43:16 +08:00
    weixiangzhe
        12
    weixiangzhe  
       2019-10-28 10:00:41 +08:00 via Android
    还是 for 吧
    shaoyaoju
        13
    shaoyaoju  
    OP
       2019-10-28 11:31:50 +08:00
    @miniwade514 的确是多余了,谢谢提醒!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1529 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 17:07 · PVG 01:07 · LAX 10:07 · JFK 13:07
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.