V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
shaoyaoju
V2EX  ›  程序员

在 Map 遍历中使用 async 函数

  •  
  •   shaoyaoju ·
    juzhiyuan · Oct 27, 2019 · 4583 views
    This topic created in 2383 days ago, the information mentioned may be changed or developed.

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

    等下起来验证一下

    const a = new Array(10000);
    terax
        7
    terax  
       Oct 27, 2019 via iPhone
    据我的理解,js async await 并不是阻塞代码的执行(只是 promise 语法糖,并不会改变 runtime )。可以理解为 await 之后的代码都被包在了 promise.then 里面了。
    dremy
        8
    dremy  
       Oct 27, 2019 via iPhone
    直接一个 for 循环+await 就能搞定的事,何必为了用函数式而为难自己呢……
    miniwade514
        9
    miniwade514  
       Oct 28, 2019 via iPhone
    首先第一个 sleep 函数实现的就有问题,已经返回 promise 了为啥还要加 async ?感觉没有理解 async 的作用
    ericls
        10
    ericls  
       Oct 28, 2019
    weixiangzhe
        12
    weixiangzhe  
       Oct 28, 2019 via Android
    还是 for 吧
    shaoyaoju
        13
    shaoyaoju  
    OP
       Oct 28, 2019
    @miniwade514 的确是多余了,谢谢提醒!
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   1044 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 90ms · UTC 18:41 · PVG 02:41 · LAX 11:41 · JFK 14:41
    ♥ Do have faith in what you're doing.