Javascript Promises with FileReader()



我有以下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承诺。但我无法工作。这是我正在尝试的:

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
            }
        });
    };
}

提前感谢...

我们修改了 midos 答案以使其工作如下:

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

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

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);
  });
}

前言:这个答案最初写于 2015 年,显示了将FileReader包裹在一个承诺中。这仍然是执行问题所问readAsDataURL操作的完全有效的方法,但是如果您要使用readAsTextreadAsArrayBuffer(通常,新代码不应使用较旧的readAsBinaryString(,则需要使用File对象的内置基于promise的方法textarrayBuffer代替(如果您想在数据流经时对数据进行内联处理,则可能stream(, 所有这些都是从Blob继承而来的。

<小时 />

FileReader的本质是您无法使其操作同步。

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

使用

promises,您可以从 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.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);
    }
};

我通过添加工作示例并引入异步/等待语法来升级 Jens Lincke 答案

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>

Promisized FileReader

/**
 * 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)

使用承诺可以使它更加优雅,

// 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);
}

这是对 Jen 答案的另一个修改(捎带 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);
            }
        });
    }

我们可以使用回调函数来获取 reader.result

function myDisplay(some) {
    document.getElementById('demo').innerHTML = some;
}

function read(file, callback) {
  const reader = new FileReader();
  reader.onload = () => {
    callback(reader.result);
  }
  reader.readAsText(file);
}
// When you pass a function as an argument, remember not to use parenthesis.
read(this.files[0], myDisplay);

最新更新