循环遍历 FileReader 的文件,输出始终包含循环中的最后一个值

IT技术 javascript html filereader dom-events
2021-02-24 13:35:31

我正在使用 FileReader API 在本地读取文件。

<input type="file" id="filesx" name="filesx[]" onchange="readmultifiles(this.files)" multiple="" />

<script>
function readmultifiles(files) {
    var ret = "";
    var ul = document.querySelector("#bag>ul");
    while (ul.hasChildNodes()) {
        ul.removeChild(ul.firstChild);
    }
    for (var i = 0; i < files.length; i++)  //for multiple files
    {
        var f = files[i];
        var name = files[i].name;
        alert(name);
        var reader = new FileReader();  
        reader.onload = function(e) {  
            // get file content  
            var text = e.target.result;
            var li = document.createElement("li");
            li.innerHTML = name + "____" + text;
            ul.appendChild(li);
        }
        reader.readAsText(f,"UTF-8");
    }
}
</script>

如果输入包括 2 个文件:

file1 ---- "content1"
file2 ---- "content2"

我得到这个输出:

file2__content1
file2__content2

如何修复代码以显示:

file1__content1
file2__content2
6个回答

问题是你正在运行的循环,现在,但你设置回调越来越运行(事件触发时)。到它们运行时,循环结束并保持最后一个值。所以它总是会在你的情况下显示“file2”作为名称。

解决方案是将文件名与其余文件名放在一个闭包中。一种方法是创建一个立即调用的函数表达式 (IIFE)并将文件作为参数传递给该函数:

for (var i = 0; i < files.length; i++) { //for multiple files          
    (function(file) {
        var name = file.name;
        var reader = new FileReader();  
        reader.onload = function(e) {  
            // get file content  
            var text = e.target.result; 
            var li = document.createElement("li");
            li.innerHTML = name + "____" + text;
            ul.appendChild(li);
        }
        reader.readAsText(file, "UTF-8");
    })(files[i]);
}

或者,您可以定义一个命名函数并正常调用它:

function setupReader(file) {
    var name = file.name;
    var reader = new FileReader();  
    reader.onload = function(e) {  
        // get file content  
        var text = e.target.result; 
        var li = document.createElement("li");
        li.innerHTML = name + "____" + text;
        ul.appendChild(li);
    }
    reader.readAsText(file, "UTF-8");
}

for (var i = 0; i < files.length; i++) {
    setupReader(files[i]);
}
@srph 它与 javascript 的变量范围有关。它为嵌套函数创建闭包,并且由于 i 的值每次迭代都会改变,因此在调用嵌套函数时,它将获得数组的最后一个值。因此,Ben Lee 修改代码的方式是确保函数在循环期间读取文件,以便维护索引。我希望这是有道理的。(这在Effective Javascript 中有很好的描述
2021-04-21 13:35:31
我渴望了解有关解释的更多详细信息。
2021-05-01 13:35:31
真的很喜欢你写这个答案,伙计。保持良好的工作
2021-05-15 13:35:31

编辑:只需使用let而不是var在循环中。这解决了 OP 的问题(但仅在 2015 年引入)。


旧答案(一个有趣的解决方法):

虽然它并不完全健壮或面向未来,但值得一提的是,这也可以通过FileReader对象添加属性来实现

var reader = new FileReader();
reader._NAME = files[i].name; // create _NAME property that contains filename.

然后通过访问它e的内onload回调函数:

li.innerHTML = e.target._NAME + "____" + text;


为什么这样做:

即使reader在循环中多次替换变量,如inew FileReader对象是唯一的并保留在内存中。它可以reader.onload通过e参数函数内访问通过在reader对象中存储额外的数据,它被保存在内存中并reader.onload通过e.target事件参数访问

这解释了为什么您的输出是:

文件 2 __内容 1 文件 2
__ 内容 2

并不是:

文件1__内容1 文件
2__内容2

内容显示正确,因为它e.target.resultFileReader对象本身的一个属性已经FileReader包含默认的文件名属性,它可能已被使用,这整个烂摊子完全避免。


一句小心的话

这称为扩展主机对象(如果我理解本机对象之间的区别......)。FileReader是在这种情况下被扩展的宿主对象。许多专业开发人员认为这样做是不好的做法和/或邪恶。如果_NAME将来使用,可能会发生冲突此功能没有记录在任何规范中,因此它在未来甚至可能会中断,并且它可能无法在较旧的浏览器中运行。

就我个人而言,通过向宿主对象添加附加属性,我没有遇到任何问题。假设属性名称足够独特,浏览器不会禁用它,并且未来的浏览器不会过多更改这些对象,它应该可以正常工作。

这里有一些文章很好地解释了这一点:

http://kendsnyder.com/extending-host-objects-evil-extending-native-objects-not-evil-but-risky/
http://perfectkills.com/whats-wrong-with-extending-the-dom/

还有一些关于问题本身的文章:

http://tobyho.com/2011/11/02/callbacks-in-loops/

不要使用var而是使用let作为声明的变量,只在一个循环中使用。

for (let i = 0; i < files.length; i++)  //for multiple files
    {
        let f = files[i];
        let name = files[i].name;
        alert(name);
        let reader = new FileReader();  
        reader.onload = function(e) {  
            // get file content  
            let text = e.target.result;
            let li = document.createElement("li");
            li.innerHTML = name + "____" + text;
            ul.appendChild(li);
        }
        reader.readAsText(f,"UTF-8");
    }

您可以为在循环中读取文件做出Promise/回调。

Promise-

  fileBase64(file) {
    return new Promise((resolve, reject) => {
      let reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = function() {
        resolve(reader.result);
      };
      reader.onerror = function(error) {
        reject(error);
      };
    });
  }

我在 onClick 上调用这个函数

  onClick = async () => {
    for (var i = 0; i < this.state.company_bank_statement_array.length; i++) {
      let file = document.getElementById(
        this.state.company_bank_statement_array[i]
      );
      let fileData = await this.fileBase64(file.files[0]);
      this.state.bankStatements.push({
        data: fileData,
        filename: file.files[0].name,
      });
    }
  };

我遇到了同样的问题,通过使用 Array.from 解决了它

let files = e.target.files || e.dataTransfer.files;

Array.from(files).forEach(file => {
 // do whatever
})