【问题标题】:Javascript Promises with FileReader()带有 FileReader() 的 Javascript 承诺
【发布时间】:2016-04-02 10:29:27
【问题描述】:

我有以下 HTML 代码:

<input type='file' multiple>

这是我的 JS 代码:

var inputFiles = document.getElementsByTagName("input")[0];
inputFiles.onchange = function(){
    var fr = new FileReader();
    for(var i = 0; i < inputFiles.files.length; i++){
        fr.onload = function(){
            console.log(i) // Prints "0, 3, 2, 1" in case of 4 chosen files
        }
    }
    fr.readAsDataURL(inputFiles.files[i]);
}

所以我的问题是,我怎样才能使这个循环同步?那就是首先等待文件完成加载,然后继续下一个文件。有人告诉我使用 JS Promises。但我无法让它工作。这是我正在尝试的:

var inputFiles = document.getElementsByTagName("input")[0];
inputFiles.onchange = function(){
    for(var i = 0; i < inputFiles.files.length; i++){
        var fr = new FileReader();
        var test = new Promise(function(resolve, reject){
            console.log(i) // Prints 0, 1, 2, 3 just as expected
            resolve(fr.readAsDataURL(inputFiles.files[i]));
        });
        test.then(function(){
            fr.onload = function(){
                console.log(i); // Prints only 3
            }
        });
    };
}

提前谢谢...

【问题讨论】:

  • Promise 用于异步操作。
  • 那我如何让它同步呢?我在网上研究过,他们都说它使代码同步
  • @ZahidSaeed:不,promise 不会使代码同步。你能指出“所有人”都说他们这样做的地方之一吗?
  • MDN 说:“Promise 代表一个值的代理,在创建 Promise 时不一定知道。它允许您将处理程序与异步操作的最终成功值或失败原因相关联。这允许异步方法返回值,如 SYNCHRONOUS METHODS":
  • 是的。如果同步意味着您可以控制文件读取代码的不同部分的执行顺序,则可以使其以同步方式运行。但它仍然会异步运行,例如鼠标处理程序或 JavaScript 引擎本身。 “像同步方法”与“是同步方法”不同。

标签: javascript promise filereader synchronous


【解决方案1】:

我们修改了midos answer 以使其在以下情况下工作:

function readFile(file){
  return new Promise((resolve, reject) => {
    var fr = new FileReader();  
    fr.onload = () => {
      resolve(fr.result )
    };
    fr.onerror = reject;
    fr.readAsText(file.blob);
  });
}

【讨论】:

  • 我尝试了很多不同的解决方案,这是唯一一个对我从 Blazor 调用 API 有效的解决方案。谢谢!!!
  • 我在 RxJS 管道中调整了答案:switchMap( file =&gt; new Promise( (resolve, reject) =&gt; Object.assign( new FileReader(), { onload: e =&gt; resolve( e.currentTarget.result ), onerror: reject }).readAsDataURL(file) )),
【解决方案2】:

如果您想使用 Promises 顺序(而不是同步)执行此操作,您可以执行以下操作:

var inputFiles = document.getElementsByTagName("input")[0];
inputFiles.onchange = function(){
  var promise = Promise.resolve();
  inputFiles.files.map( file => promise.then(()=> pFileReader(file)));
  promise.then(() => console.log('all done...'));
}

function pFileReader(file){
  return new Promise((resolve, reject) => {
    var fr = new FileReader();  
    fr.onload = resolve;  // CHANGE to whatever function you want which would eventually call resolve
    fr.onerror = reject;
    fr.readAsDataURL(file);
  });
}

【讨论】:

  • 你就是那个男人!第二部分(新的 Promise)正是我需要让我的 FileReader 根据需要作为 Promise 返回的部分。谢谢!
  • 我必须做new Promise&lt;Event&gt;((resolve, reject) =&gt; { 才能让它工作......(需要&lt;Event&gt; 通用。)
  • 老兄。您能否仅通过如何执行 pFileReader(file) 并从变量中的 promise 而不是 inputfiles.files.map 获取结果来更新您的答案,因为我对此并不陌生,并且正在为解决方案苦苦挣扎。但你的溶胶看起来很棒。我想做的是调用一个方法将图像转换为base64并将其返回给调用者,然后我将数据保存到一个变量中。请帮助
【解决方案3】:

FileReader 的本质是不能使其操作同步。

我怀疑您并不真正需要或不希望它是同步的,只是您想正确获取生成的 URL。建议使用 Promise 的人可能是对的,但不是因为 Promise 使流程同步(它们不会),而是因为它们为处理异步操作(无论是并行还是串行)提供了标准化的语义:

使用 Promise,您将从 readAsDataURL 的 Promise 包装器开始(我在这里使用 ES2015+,但您可以使用 Promise 库将其转换为 ES5):

function readAsDataURL(file) {
    return new Promise((resolve, reject) => {
        const fr = new FileReader();
        fr.onerror = reject;
        fr.onload = () => {
            resolve(fr.result);
        }
        fr.readAsDataURL(file);
    });
}

然后您将使用我描述的基于 Promise 的操作 in this answer 来并行读取这些操作:

Promise.all(Array.prototype.map.call(inputFiles.files, readAsDataURL))
.then(urls => {
    // ...use `urls` (an array) here...
})
.catch(error => {
    // ...handle/report error...
});

...或串联:

let p = Promise.resolve();
for (const file of inputFiles.files) {
    p = p.then(() => readAsDataURL(file).then(url => {
        // ...use `url` here...
    }));
}
p.catch(error => {
    // ...handle/report error...
});

在 ES2017 async 函数中,您可以使用 await。它对并行版本没有多大作用:

// Inside an `async` function
try {
    const urls = await Promise.all(Array.prototype.map.call(inputFiles.files, readAsDataURL));
} catch (error) {
    // ...handle/report error...
}

...但它使系列版本更简单明了:

// Inside an `async` function
try {
    for (const file of inputFiles.files) {
        const url = await readAsDataURL(file);
        // ...use `url` here...
    }
} catch (error) {
    // ...handle/report error...
}

如果没有承诺,您可以通过跟踪您有多少未完成的操作来做到这一点,以便知道何时完成:

const inputFiles = document.getElementsByTagName("input")[0];
inputFiles.onchange = () => {
    const data = [];    // The results
    let pending = 0;    // How many outstanding operations we have

    // Schedule reading all the files (this finishes before the first onload
    // callback is allowed to be executed). Note that the use of `let` in the
    // `for` loop is important, `var` would not work correctly.
    for (let index = 0; index < inputFiles.files.length; ++index) {
        const file = inputFiles.files[index];
        // Read this file, remember it in `data` using the same index
        // as the file entry
        const fr = new FileReader();
        fr.onload = () => {
            data[index] = fr.result;
            --pending;
            if (pending == 0) {
                // All requests are complete, you're done
            }
        }
        fr.readAsDataURL(file);
        ++pending;
    });
};

或者如果您出于某种原因想要按顺序读取文件(但仍然是异步的),您可以通过仅在前一个调用完成时安排下一个调用来做到这一点:

// Note: This assumes there is at least one file, if that
// assumption isn't valid, you'll need to add an up-front check
var inputFiles = document.getElementsByTagName("input")[0];
inputFiles.onchange = () => {
    let index = 0;

    readNext();

    function readNext() {
        const file = inputFiles.files[index++];
        const fr = new FileReader();
        fr.onload = () => {
            // use fr.result here
            if (index < inputFiles.files.length) {
                // More to do, start loading the next one
                readNext();
            }
        }
        fr.readAsDataURL(file);
    }
};

【讨论】:

  • 非常清晰,谢谢!
【解决方案4】:

我通过添加工作示例升级 Jens Lincke answer 并引入 async/await 语法

function readFile(file) {
  return new Promise((resolve, reject) => {
    let fr = new FileReader();
    fr.onload = x=> resolve(fr.result);
    fr.onerrror = reject;
    fr.readAsDataURL(file) // or readAsText(file) to get raw content
})}

function readFile(file) {
  return new Promise((resolve, reject) => {
    let fr = new FileReader();
    fr.onload = x=> resolve(fr.result);
    fr.readAsDataURL(file) // or readAsText(file) to get raw content
})}

async function load(e) {
  for(let [i,f] of [...e.target.files].entries() ){
    msg.innerHTML += `<h1>File ${i}: ${f.name}</h1>`;
    let p = document.createElement("pre");
    p.innerText += await readFile(f);
    msg.appendChild(p);
  }
}
<input type="file" onchange="load(event)" multiple />
<div id="msg"></div>

【讨论】:

    【解决方案5】:

    承诺的文件阅读器

    /**
     * Promisified FileReader
     * More info https://developer.mozilla.org/en-US/docs/Web/API/FileReader
     * @param {*} file
     * @param {*} method: readAsArrayBuffer, readAsBinaryString, readAsDataURL, readAsText
     */
    export const readFile = (file = {}, method = 'readAsText') => {
      const reader = new FileReader()
      return new Promise((resolve, reject) => {
        reader[method](file)
        reader.onload = () => {
          resolve(reader)
        }
        reader.onerror = (error) => reject(error)
      })
    }
    

    用法

    const file =  new File(["foo"], "foo.txt", {
      type: "text/plain",
    });
    
    // Text
    const resp1 = await readFile(file)
    console.log(resp1.result)
    
    // DataURL
    const resp2 = await readFile(file, 'readAsDataURL')
    console.log(resp2.result)
    

    【讨论】:

      【解决方案6】:

      使用 Promise 可以使它更加优雅,

      // opens file dialog waits till user selects file and return dataurl of uploaded file
      
      async function pick() {
        var filepicker = document.createElement("input");
        filepicker.setAttribute("type","file");
        filepicker.click();
        return new Promise((resolve,reject) => {
          filepicker.addEventListener("change", e => {
            var reader = new FileReader();
            reader.addEventListener('load', file => resolve(file.target.result));
            reader.addEventListener('error', reject);
            reader.readAsDataURL(e.target.files[0]);
          });
        });
      }
      
      // Only call this function on a user event
      
      window.onclick = async function() {
        var file = await pick();
        console.log(file);
      }
      

      【讨论】:

        【解决方案7】:

        这是对 Jens 答案的另一个修改(借鉴 Mido 的答案),以另外检查文件大小:

        function readFileBase64(file, max_size){
                max_size_bytes = max_size * 1048576;
                return new Promise((resolve, reject) => {
                    if (file.size > max_size_bytes) {
                        console.log("file is too big at " + (file.size / 1048576) + "MB");
                        reject("file exceeds max size of " + max_size + "MB");
                    }
                    else {
                    var fr = new FileReader();  
                    fr.onload = () => {
                        data = fr.result;
                        resolve(data)
                    };
                    fr.readAsDataURL(file);
                    }
                });
            }
        

        【讨论】:

        • 这个答案正确地使用了onloadend 而不是onload,这可能会给出部分结果。
        • ??不,我是说onloadend 是正确的使用。其他所有答案都使用 wrong api (onload)
        • onloadend 在文件读取成功或不成功时触发,而onload 仅在读取成功时触发@nathan-m
        猜你喜欢
        • 1970-01-01
        • 2017-10-04
        • 1970-01-01
        • 2015-12-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多