首页 > 解决方案 > 如何在最后一步之前异步所有上传的文件?

问题描述

我希望用户上传多个图像文件,应用程序读取所有图像文件并推送到一个数组中,然后全部完成lastStep()

<input type="file" (change) = "fileChangedEvent($event);" multiple/>

fileChangedEvent(event: any): void
{
    for (let f of event.target.files)
    {
        var reader = new FileReader();
        reader.readAsDataURL(f);
        reader.onload= (event) => {this.arrayThumbnail.push(event.target.result);}; 
    } 
    // when all done
    lastStep();  // work on arrayThumbnail[]
}

lastStep()总是在for ... of循环之前运行,导致它在一个空的arrayThumbnail[]. 以下async/await产生相同的结果:

fileChangedEvent(event: any): void
{
    async() => {
    for (let f of event.target.files)
        {
            var reader = new FileReader();
            await reader.readAsDataURL(f);
            await reader.onload= (event) => {this.arrayThumbnail.push(event.target.result);}; 
        } 
        // when all done
        lastStep();  // work on arrayThumbnail[]
    }
}
    

显式Promise也不起作用:

myPromise(fs: any): Promise<number>
{
    var bogus = new Promise<number>((resolve, reject) =>
    {
        for (let f of fs)
        {
            var reader = new FileReader();
            reader.readAsDataURL(f);
            reader.onload= (event) => {this.arrayThumbnail.push(event.target.result);};    
        };
        resolve(1);
    })
    return bogus;
}

fileChangedEvent(event: any): void
{
    this.myPromise(event.target.files).then(
    x=>
    {
        lastStep();
    });
}

标签: typescriptasync-awaitpromise

解决方案


你有正确的想法来使用 Promise,你只需要稍微不同地把事情联系起来。运行下面的代码片段并选择两个小文件进行测试!

// onChange listener
function onChange(event) {
  const promises = []
  for (const file of event.target.files)
    promises.push(readFile(file))
  Promise.all(promises).then(lastStep)
}

// read one file
function readFile(f) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(f)
    reader.onload = event => resolve(event.target.result)
    reader.onerror = reject
  })
}

// example last step
function lastStep(data) {
  console.log("last step")
  console.log(`uploaded ${data.length} files`)
  for (const blob of data) {
    const pre = document.createElement("pre")
    pre.textContent = blob
    document.body.appendChild(pre)
  }
}

document.forms.myapp.files.addEventListener("change", onChange)
<form id="myapp">
  <input type="file" name="files" multiple>
</form>

一旦你明白了这一点,就知道我们可以onChange更容易地重写 -

// onChange listener simplified
function onChange(event) {
  Promise.all(Array.from(event.target.files, readFile)).then(lastStep)
}

现在真的没有理由分开onChangeand lastStep。使用asyncandawait我们可以很容易地将两者合二为一——

// onChange listener
async function onChange(event) {
  const data = await Promise.all(Array.from(event.target.files, readFile))
  console.log(`uploaded ${data.length} files`)
  for (const blob of data) {
    const pre = document.createElement("pre")
    pre.textContent = blob
    document.body.appendChild(pre)
  }
}

// read one file
function readFile(f) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(f)
    reader.onload = event => resolve(event.target.result)
    reader.onerror = reject
  })
}

document.forms.myapp.files.addEventListener("change", onChange)
<form id="myapp">
  <input type="file" name="files" multiple>
</form>

没有承诺

“第二个想法,我们在这里做的是每个文件都有一个Promise,然后等到全部解决。是否可以为所有文件做一个promise?”

Promises 的好处是它们是轻量级且可组合的,这意味着您可以从较小的异步计算中构建更大的异步计算。编写回调更具挑战性,因此更容易犯常见错误。如果我们愿意,我们可以将所有这些包装在一个 Promise 中,或者完全跳过 Promise -

// onChange listener
function onChange(event) {
  const data = []
  const noEror = true

  // for each file, f
  for (const f of event.target.files) {

    // readFile with callback
    readFile(f, (err, result) => {

      // handle errors
      // only call error handler a maximum of one time
      if (err && noError) {
        noError = false
        return handleError(err)
      }
      // add individual result to data
      data.push(result) 
      
      // once data.length is equal to number of input files
      // then proceed to the last step
      if (data.length == event.target.files.length)
        return lastStep(data)
    })
  }
}

// read one file
function readFile(f, callback) {
  const reader = new FileReader()
  reader.readAsDataURL(f)
  reader.onload = event => callback(null, event.target.result)
  reader.onerror = err => callback(err)
}

function handleError (err) { ... }
function lastStep (data) { ... }

正如您所看到的,对于应该是一个简单的过程,要写很多东西。每次我们需要类似的功能时,所有这些工作都会重复。为了解决这个问题,我们可以编写一个通用asyncEach实用程序,当我们需要使用异步操作迭代数组时可以使用它,并在所有操作完成时f指定一个 final -callback

// asyncEach generic utility
function asyncEach(arr, f, callback, data = []) {
  if (arr.length == 0)
    callback(null, data)
  else
    f(arr[0], (err, result) =>
      err
        ? callback(err)
        : asyncEach(arr.slice(1), f, callback, [...data, result]) 
    )
}

// read one file
function readFile(f, callback) {
  const reader = new FileReader()
  reader.readAsDataURL(f)
  reader.onload = event => callback(null, event.target.result)
  reader.onerror = err => callback(err)
}

// onChange listener
function onChange(event) {
  asyncEach(Array.from(event.target.files), readFile, (err, data) => {
    if (err) return console.error(err)
    console.log(`uploaded ${data.length} files`)
    for (const blob of data) {
      const pre = document.createElement("pre")
      pre.textContent = blob
      document.body.appendChild(pre)
    }
  })
}

document.forms.myapp.files.addEventListener("change", onChange)
<form id="myapp">
  <input type="file" name="files" multiple>
</form>

我认为这是一个很棒的练习,但大多数人不会尝试编写自己asyncEach的东西,而对这类东西的共同需求是async等流行库的基础。然而,由于 Promise 和新语法的广泛成功,async这些await旧的和繁琐的回调模式已经很少需要了。


推荐阅读