通过代码生成的INPUT元素选择文件后,事件onChange不会触发。

9
我正在学习JavaScript,并编写了一个简单的函数来创建一个 INPUT 元素 (type="file") 并模拟点击。
var createAndCallFileSelect = function () {
    var input = document.createElement ("input");
    input.setAttribute ("type", "file");
    input.addEventListener ("change", function () {
        console.log (this.files);
    }, false);
    input.click();
}

它大部分时间都很好用,但有时会在选择文件(或与INPUT上的multiple属性一起使用时选择多个文件)时无法触发onChange事件。我知道如果您重新选择相同的文件,onChange不会触发,但显然这里并非如此。它只有第一次使用此函数时才不触发事件,而且有时只有第一次。如果从对话框中选择了某些内容,则每次单击都可以正常触发onChange事件。我已经尝试在这里和周围搜索这个问题,但似乎所有onChange问题和解决方案都与重新选择相同的文件相关。我发现这在最新版的Opera和Firefox浏览器上发生,从未在其他浏览器上测试过。此外,我尝试等待整个页面加载完成,但结果仍然是相同的——有时不会在第一次调用时触发onChange事件。有人能告诉我为什么会这样吗?我已经有了解决方法,问题不在于此,我只需要解释为什么在创建并以这种方式调用INPUT时会发生这种情况。更新:级联延迟。
var function createAndCallFileSelect = function () {
    var input = document.createElement ("input");
    setTimeout (function () { // set type with 1s delay
        input.setAttribute ("type", "file");
        setTimeout (function () {  // attach event with 1s delay
            input.addEventListener ("change", function () {
                console.log (this.files);
            }, false);
            setTimeout (function () { // simulate click with 1s delay
                input.click();
            }, 1000);
        }, 1000);
    }, 1000);
}

这也不起作用。我试图延迟每行代码的执行时间,以确保一切按正确顺序执行。在调用3秒后,它会打开文件选择对话框,但有时在选择文件后它不会触发 onChange 事件。


这对我绝对不起作用。总是!Debian 8,Mate,FF ESR 45.2.0。但如果假设... https://dev59.com/FHRA5IYBdhLWcg3w4iF_ - Deep
完全无法复制。你说的“有时候”是什么频率?你的系统规格是什么? - cviejo
@cviejo,它大多数情况下都能正常工作。有时候我可以连续几个小时、数千次重新加载而没有任何问题,但有时候在10次重新加载中会有7次无法正常工作(这种情况很少但确实发生过)。没有任何规律可言。系统配置不相关。 - Wh1T3h4Ck5
@Deep 这并没有为我的问题提供详细的解释。请仔细阅读问题,我没有要求解决方案或建议+我从未提到过jQuery(我的代码是纯JavaScript)。 - Wh1T3h4Ck5
1
我已经成功重现了这个问题,而且它确实是“有时候”的。没有明显的模式。 - Fabio Poloni
显示剩余12条评论
5个回答

5
这是一个竞态条件。它取决于堆栈中的内容以及在同步文件浏览器被调用以阻止其余堆栈完成之前需要花费多长时间的某些事情。使用addeventlistener时,它会将回调排队以供稍后使用,并在堆栈清除时由事件循环接收。如果堆栈没有及时清除,则不会及时调用它。不能保证何时运行什么。如果像Pawel建议的那样使用setTimeout(fn,0),则将对click()函数进行排队,以便在放置事件侦听器后调用。
这里有一个很棒的视频,可以将我所说的一切都可视化:https://www.youtube.com/watch?v=8aGhZQkoFbQ 更新: 在进一步研究后,我注意到Chrome上有一个非常有趣的问题......它只允许最多同时创建5个这些元素。我做了这个:
for(var i = 0; i < 20; i += 1) {
    createAndCallFileSelect()
}

在这里有几个不同的数字...每次,任何大于5的数字只会产生5个带有5个回调的输入元素,而5及以下则会产生正确的数量。

我还尝试了递归而不是使用for循环...结果相同。

另外,我选择的文件越大,处理时间就越长,但最终它会在处理完文件后调用回调函数。目前所有这些测试都是在Chrome浏览器中进行的。


听起来很合理,可能是一个答案,但有一件事。使用setTimeout延迟click()并没有帮助。实际上,每次onChange触发失败时,它之前都已经附加到元素上了。我还尝试过进行级联执行,并延迟每行1秒钟调用,但仍然没有帮助。当我将元素附加到文档并延迟click时,同样的情况再次发生。你部分正确,执行堆栈有问题,但这仍然令人困惑。对我来说,它真的看起来像一个bug,但是同样的bug影响更多的浏览器的机会是多少呢? - Wh1T3h4Ck5
嗯,你的更新确实很有趣。我实际上根本不使用Chrome,但我会尝试在其他浏览器中使用它。至少,它可能包含一些隐藏的线索来解决我的谜团。 - Wh1T3h4Ck5

1
您可以像这样做,以触发动态创建的文件输入的更改点击。
var input = document.createElement ("input");
input.setAttribute ("type", "file");

input.addEventListener('change', function(){
    input.addEventListener('click', function(){
      alert("Clicked");
      input.removeEventListener("click", function(){})
    }, false);
    input.click();
}, false); 

JS fiddle(JavaScript演示)

我已经在Chrome,Firefox,Opera和IE中进行了测试。 它可以工作。


如果你仔细阅读问题,你会看到: “有人能向我解释为什么会发生这种情况吗?我已经有解决方法了,那不是问题,只需要解释为什么当以这种方式创建和调用INPUT时会发生这种情况。” - Wh1T3h4Ck5
抱歉之前我没有理解你的问题,你的问题非常有趣,目前我没有一个确定的答案,但是我正在尝试找出来。显然这不是一种竞争条件,在JS中不存在竞争条件,JS执行原子操作并且是单线程的。我得到的唯一提示是这是一种上下文堆栈执行问题。一旦我完全回答了你的问题,我会修改我的答案。感谢提问。 - Siddhartha Chowdhury

0

事件监听器 input.addEventListener ("change" ...

不会立即注册。这就像将代码包装在 setTimeout(fn, 0) 中,使其添加到执行队列的末尾。

但是,input.click(); 立即打开文件选择弹出窗口,暂停 JavaScript(因此事件直到弹出窗口关闭后才会注册)。如果您将 input.click 包装在 setTimeout(function() { input.click(); }, 0) 中,则它肯定会在事件注册后执行,这个理论可能是正确的。

我无法重现您的问题,所以这只是纯理论。


1
事件监听器没有立即注册。不正确! 大多数情况下它确实会立即注册(除了有时和仅在第一次调用时)。输入元素在每次函数调用时都会在本地范围内重新创建,那么为什么它仅在第一次调用时和有时候会失败?这就是我要找的答案。 - Wh1T3h4Ck5

0
这是因为当你关闭“打开文件”对话框窗口时,你的input元素已经不存在了,所以没有目标来触发onchange事件。可能是因为JavaScript的垃圾回收器已经回收了该元素,或者由于其他原因。
要解决这个问题,只需在DOM中保存你的input元素即可:
input.style.visibility='hidden';
document.body.appendChild(input);

同时不要忘记在文件上传完成后保存此元素的链接并从DOM中删除它(我在这里使用Ext.js的this.set()this.get()函数):

// after element initialization:
this.set("inputFileElement", input);
...
// in the "OnFileComplete" event handler or in some similar place:
var inputFileElement = this.get("input");
if(inputFileElement !== null && inputFileElement !== undefined)
{
    inputFileElement.parentNode.removeChild(inputFileElement);
}

0

我知道这个问题不是关于解决方法的,但我来到这里是为了寻找解决方案。这个方法在Chrome上对我有效。我只是将let input移动到函数外面。这个方法有效是因为(如bside所建议的),它防止了input被垃圾回收。我只希望一次只打开一个文件对话框,所以单例模式在这里是可以的。

let input;
var createAndCallFileSelect = function () {
    input = document.createElement ("input");
    input.setAttribute ("type", "file");
    input.addEventListener ("change", function () {
        console.log (this.files);
    }, false);
    input.click();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接