在需要回调(例如 Array.map)的循环中使用 async/await 时,到底是什么影响了 fs.writeFile 的行为?

What exactly is affecting how fs.writeFile behaves when using async/await in a loop requiring a callback such as Array.map?

这个问题似乎与字符串长度有关,但我发现在代码的某些地方包含 console.log() 时会发生此问题。需要明确的是,只有在需要回调的循环中使用 fs.writeFile 时才会出现此问题,例如 Array.map.

当我使用 for循环,没有问题。预期的结果是,到循环结束时,写入的文件仅包含数组最后一个元素的数据。一切都很好。您可以在 here on repl.it 上查看。这是代码:

const fs = require('fs')

const writeFile = (uri, data, options) => new Promise((resolve, reject) => {
  fs.writeFile(uri, data, (err) => {
    if (err) {
      return reject(`Error writing file: ${uri} --> ${err}`)
    }
    resolve(`Successfully wrote file: ${uri}  --> with data: ${data}`)
  })
})

const asyncWriteFile = (uri, data) => {
  return writeFile(uri, data).then( res => console.log(res)).catch(e => console.log(e))
}
const asyncWriteFileLoop = async (uri, seed) => {
  for (let i = 0; i < dataArr.length; i++) {
    await asyncWriteFile(uri, seed[i])
  }
}

const uri = 'test-write-dump.txt'
const dataArr = [
  'this is string long enough to cause an issue when using Array.map but not in a for loop',[1,2,3,4],1600,'foobar',['foo','bar','baz']
]

// works as expected, output in file is foo,bar,baz
asyncWriteFileLoop(uri, dataArr)  

问题来了:当我尝试使用 Promise.allfor 循环替换为 Array.map 时。该文件是附加的,而不是被覆盖的,并且以一种看似奇怪且不可预测的顺序。我意识到这是连续调用 fs.writeFile 过快的本质。然而,我期待一系列的承诺能够一个接一个地解决,从而绕过这个问题,但事实似乎并非如此。您可以在 here on repl.it 上查看。这是有问题的代码:

const fs = require('fs')

const writeFile = (uri, data, options) => new Promise((resolve, reject) => {
  fs.writeFile(uri, data, (err) => {
    if (err) {
      return reject(`Error writing file: ${uri} --> ${err}`)
    }
    resolve(`Successfully wrote file: ${uri}  --> with data: ${data}`)
  })
})

const appendFile = (uri, data, options) => new Promise((resolve, reject) => {
  fs.appendFile(uri, data, (err) => {
    if (err) {
      return reject(`Error appending file: ${uri} --> ${err}`)
    }
    resolve(`Successfully appended file: ${uri}  --> with data: ${data}`)
  })
})

asyncWriteFileMap = async (uri, seed) => {
  const promises = seed.map(async data => {
    await writeFile(uri, data)
      .then(res => console.log(res))
      .catch(e => console.log(e))
  })
  const result = await Promise.all(promises)
}

const uri = 'test-write-dump.txt'
const dataArr = [
  'This is a string long enough to cause an issue, change this to a single character and the expected result almost always occurs',[1,2,3,4],1600,'foobar',['foo','bar','baz']
]

// Seems like it doesnt work with long strings, writes a junk file
asyncWriteFileMap(uri, dataArr) 

我真的很想知道确切地 是什么导致了这种行为,为什么 for 循环工作而 Array.map 不工作。

.map() 不是 promise-aware。它不会注意你的回调 returns 的承诺。相反,它只是将 promise 放在结果数组中并继续其循环。

因此,发生的情况是 .map() 立即启动所有 writeFileAsync() 调用,然后在稍后的某个时间每个承诺完成。您在回调中使用的 await 不会暂停 .map() 迭代 - 事实上,它根本没有帮助您。

另一方面,for 循环是 promise-aware 并将暂停 await.

的循环

因此,for 循环被 await 暂停,.map() 循环没有。


如果您没有意识到,当 async 函数(例如您的 .map() 回调)遇到它的第一个 await 时,async 函数会立即 returns一个承诺。它暂停进一步的函数执行,直到 await 看到已解决的承诺,但函数本身 returns 调用代码的承诺和执行继续。这就是 .map() 继续其其余迭代而不等待 await.

的原因

此外,请记住 Promise.all() 不会“运行”任何东西。异步操作已经 运行ning 并且 Promise.all() 只是给出了一组承诺来监视和收集结果。所以,说 Promise.all() 运行 是并行的,而不是特定的顺序是不正确的。在你的例子中是 .map() 这样做的。

Promise.all() 通常用于多个 promise-based 异步操作并行 运行 的情况,但这些操作是由其他东西启动的。 Promise.all()只是监控完成和收集结果。