DOM输入事件与setTimeout / setInterval顺序

时间:2011-06-17 20:37:16

标签: javascript dom javascript-events input

我的页面上运行了一段JavaScript代码;我们称之为func1。运行需要几毫秒。当该代码正在运行时,用户可以单击,移动鼠标,输入一些键盘输入等。我有另一个代码块func2,我希望在所有这些排队的输入事件之后运行解决。也就是说,我想确保订单:

  1. func1
  2. 所有处理程序绑定到func1正在运行时发生的输入事件
  3. func2
  4. 我的问题是:setTimeout func2, 0结束时调用func1是否足以保证所有现代浏览器的排序?如果该行在func1 - 在这种情况下,我应该期待什么顺序?

    请通过参考相关规范或测试用例来备份您的答案。

    更新:事实证明不,这还不够。我在原始问题中未能意识到的是,在执行当前代码块之前,输入事件甚至不会添加到队列中。所以,如果我写

    // time-consuming loop...
    setTimeout func2, 0
    

    然后只有在运行setTimeout的之后的将在耗时的循环中排队的任何输入事件(点击等)排队。 (要对此进行测试,请注意,如果在耗时循环后立即删除onclick回调,那么在循环期间发生的点击将不会触发该回调。)因此func2排在第一位,优先排序。

    设置1的超时似乎解决了Chrome和Safari中的问题,但在Firefox中,我看到输出事件在超时后解析为高80(!)。所以纯粹基于时间的方法显然不会做我想要的。

    简单地将一个setTimeout ... 0包裹在另一个内部也不够。 (我希望第一次超时会在输入事件排队后触发,第二次超时会在解决之后触发。没有这样的运气。)也没有添加第三或第四级嵌套就足够了(参见下面的更新2) )。

    所以,如果有人有办法实现我所描述的(除了设置90+毫秒的超时),我将非常感激。或者使用当前的JavaScript事件模型这是不可能的?

    这是我最新的JSFiddle测试平台:http://jsfiddle.net/EJNSu/7/

    更新2:部分解决方法是将func2嵌套在两个超时内,在第一个超时中删除所有输入事件处理程序。但是,这会产生令人遗憾的副作用,导致func1期间发生的某些甚至所有输入事件无法解决。 (前往http://jsfiddle.net/EJNSu/10/并尝试快速点击链接几次以观察此行为。警报会告诉您有多少次点击?)所以这再一次让我感到惊讶;我不认为调用setTimeout func2, 0,其中func2onclick设置为null,可能会阻止该回调运行以响应整整一秒前发生的点击。我想确保所有输入事件都会触发,但我的函数会在它们之后触发。

    更新3:在使用此测试平台后,我在下面发布了我的答案,这很有启发性:http://jsfiddle.net/TrevorBurnham/uJxQB/

    将鼠标移到框上(触发1秒阻塞循环),然后单击多次。在循环之后,您执行的所有点击都会显示出来:顶部框的click处理程序将其翻转到另一个框下,然后接收下一个click,依此类推。在单击事件之后,mouseenter回调中触发的超时不会始终发生,并且即使在相同的硬件和操作系统上,单击事件发生的时间也会因浏览器的不同而异。 (这个实验出现了另一个奇怪的事情:即使我将鼠标稳定地移动到盒子中,我有时会得到多个jQuery mouseenter事件。不确定那里发生了什么。)

6 个答案:

答案 0 :(得分:4)

我认为你的实验走错了路。一个问题当然是你在这里打击不同的消息循环实现。另一个(你似乎不认识的那个)是不同的双击处理。如果您点击该链接两次,则不会在MSIE中收到两个click事件 - 它只是一个click事件和一个dblclick事件(对于您来说,看起来像第二次点击是“吞下” “)。在此方案中,所有其他浏览器似乎都会生成两个click事件和一个dblclick事件。所以你也需要处理dblclick事件。

随着消息循环的进行,Firefox应该是最容易处理的。据我所知,即使JavaScript代码正在运行,Firefox也会向队列添加消息。因此,简单的setTimeout(..., 0)足以在处理消息后运行代码。但是,在完成func1()之后,您应该避免隐藏链接 - 此时,尚未处理点击,并且它们不会触发隐藏元素上的事件处理程序。请注意,即使零超时也不会立即添加到队列中,当前的Firefox版本具有4毫秒作为最低可能的超时值。

MSIE类似,只是你需要处理dblclick事件,正如我之前提到的那样。 Opera似乎也是这样的,但如果你不调用event.preventDefault()(或从事件处理程序返回false,它本质上是相同的东西)它不喜欢它。

然而,Chrome似乎首先将超时添加到队列中,之后只添加传入消息。嵌套两个超时(零超时值)似乎在这里完成了工作。

唯一无法使其可靠运行的浏览器是Safari(Windows版本4.0)。消息的调度在那里似乎是随机的,看起来像在不同线程上执行的定时器并且可以在随机时间将消息推送到消息队列中。最后,你可能不得不承认你的代码可能不会在第一次被中断,用户可能需要等待一段时间。

以下是我对您的代码的修改:http://jsfiddle.net/KBFqn/7/

答案 1 :(得分:3)

如果我正确理解你的问题,你有一个长时间运行的功能,但你不想在它运行时阻止它?在长时间运行的功能完成后,您想要运行另一个功能吗?

如果是,而不是使用超时或间隔,则可能需要使用Web Workers。包括IE9在内的所有现代浏览器都应该支持Web Workers。

我把example page放在一起(由于Web Workers依赖于必须托管在同一来源的外部.js文件,因此无法将其放在jsfiddle上。)

如果单击A,B,C或D,则会在右侧记录消息。按下启动时,Web Worker将开始处理3秒钟。将立即记录这3秒内的任何点击。

代码的重要部分在这里:

func1.js 在Web Worker

中运行的代码
onmessage = function (e) {
    var result,
    data = e.data, // get the data passed in when this worker was called
                   // data now contains the JS literal {theData: 'to be processed by func1'}
    startTime;
    // wait for a second
    startTime = (new Date).getTime();
    while ((new Date).getTime() - startTime < 1000) {
        continue;
    }
    result = 42;
    // return our result
    postMessage(result);
}

调用Web Worker的代码:

var worker = new Worker("func1.js");
// this is the callback which will fire when "func1.js" is done executing
worker.onmessage = function(event) {
    log('Func1 finished');
    func2();
};

worker.onerror = function(error) {
    throw error;
};

// send some data to be processed
log('Firing Func1');
worker.postMessage({theData: 'to be processed by func1'});

答案 2 :(得分:2)

此时,我很遗憾地说,很遗憾,没有解决此问题的方法,它可以在所有浏览器中,在每种情况下,每次都可以使用。简而言之:如果您运行JavaScript函数,则无法可靠地区分用户在期间触发的输入事件与用户之后触发 的输入事件。这对JS开发人员有很大的影响,特别是那些使用交互式画布的人。

我对JS输入事件如何工作的心理模型不合时宜。我以为它去了

  1. 用户在代码运行时单击DOM元素
  2. 如果该元素具有click事件处理程序,则回调排队
  3. 执行所有阻止代码后,将运行回调
  4. 然而,我的实验,以及由弗拉基米尔·帕兰德(谢谢,弗拉迪米尔)提供的实验表明,正确的模型是

    1. 用户在代码运行时单击DOM元素
    2. 浏览器捕获点击的坐标等
    3. 在所有阻止代码执行完毕后,浏览器会检查哪些DOM元素位于这些坐标处,然后运行回调(如果有的话)
    4. 我说“之后的某个时间”,因为不同的浏览器似乎有相同的行为 - 在Chrome for Mac中,我可以在阻止代码的末尾设置setTimeout func2, 0并期望func2单击回调后运行(在阻塞代码完成后仅运行1-3ms);但在Firefox中,超时总是首先解析,并且单击回调通常在阻塞代码执行完毕后约40ms发生。这种行为显然超出了任何JS或DOM规范的范围。正如John Resig所说的经典How JavaScript Timers Work

        

      当发生异步事件时(如鼠标点击,计时器触发或XMLHttpRequest完成),它会排队等待以后执行(这种排队实际上是如何发生在浏览器到浏览器之间的确有所不同,所以认为这是一个简化。)

      (强调我的。)

      那么从实际的角度来看,这意味着什么呢?这是一个非问题,因为阻塞代码的执行时间接近0.这意味着这个问题是另一个理由来提出这个旧建议:将你的JS操作分解成小块以避免阻塞线程。 /强>

      无用代码建议的网络工作者在使用它们时会更好 - 但要注意你已经提到compatibility with Internet Explorer and all major mobile browsers

      最后,我希望浏览器制造商能够在未来标准化输入事件方面取得进展。这是many quirks in that area之一。我希望Chrome能够引领未来:优秀的线程隔离,低事件延迟和相对一致的排队行为。网络开发者可以做梦,不是吗?

答案 3 :(得分:1)

您可以在功能结束时将dispatchEvent与自定义事件名称一起使用。这不适用于IE,但仍有可能;只需使用fireEvent

看看这个:

http://jsfiddle.net/minitech/NsY9V/

点击“开始长跑”,然后点击文本框并输入。瞧!

答案 4 :(得分:0)

您可以检查事件处理程序以查看func1是否设置了标记;如果是,那么队列func2如果尚未排队。

这可能是优雅的,也可能是丑陋的,具体取决于func2的专业性。 (实际上它可能只是丑陋。)如果你选择这种方法,你需要一些方法来挂钩事件,或者你自己的bindEvent(event,handler,...)函数,它包装了处理程序并绑定了包装的处理程序。

此方法的正确性取决于func1同时排队期间的所有事件。如果不是这种情况,你可以使func2幂等,或者(取决于func2的语义)把一个丑陋的“不能再调用N毫秒”锁定它。

答案 5 :(得分:0)

请更好地描述你的情景。

你需要做什么

前段时间我需要做点什么,所以我在一个同步调用中跨序列化异步调用构建了一个简单的javascript例程。也许你可以使用增加的一个变体

例如,让我展示一下这项工作的方式

首先注册所有异步或同步例程 第二个寄存器结束回调 第三个寄存器调用与您的参数的例程 第四个抛出过程

在你的情况下,它需要添加一个调用例程,该例程应该是用户操作的UoW。 现在主要问题是如果没有跟踪用户做出的更改,则不会调用例程和执行顺序

首先注册所有异步或同步例程 第二个寄存器结束回调 第三次注册调用与你的参数的例行程序    - 注册你的第一个例程   --register BlockUi //可能因为不接受视图中的更多更改   --register UiWriter //用户完成的大部分更改    - 注册你最后的例程 第四个抛出过程

在实际代码中,这是一个调用虚拟函数

函数Should_Can_Serializer_calls() {
  RegisterMethods(模型);
  model.Queue.BeginUnitProcess(); //明确执行堆栈,其他人   model.Queue.AddEndMethod(SucessfullEnd); //回调结束例程   model.AbstractCall( “func1的”,1, “edu的”,15 “”); //设置例程如何首先执行   model.AbstractCall( “BlockUi”); //跟踪更改和用户的操作   model.AbstractCall( “UiWork”); //跟踪更改和用户的操作   model.AbstractCall( “FUNC2”, “值”); //为执行设置第二个例程   model.Process(); //扔电话 }

现在这些方法应该是异步的,你可以使用该库http://devedge-temp.mozilla.org/toolbox/examples/2003/CCallWrapper/index_en.html

那么,你想做什么?