【问题标题】:DOM input events vs. setTimeout/setInterval orderDOM 输入事件与 setTimeout/setInterval 顺序
【发布时间】:2023-04-03 18:16:01
【问题描述】:

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

  1. func1
  2. 绑定到func1 运行时发生的输入事件的所有处理程序
  3. func2

我的问题是:func1 的末尾调用setTimeout func2, 0 是否足以保证在所有现代浏览器中的这种排序? 如果该行出现在func1 的开头怎么办?在这种情况下,我应该期待什么顺序?

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

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

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

那么只有在运行setTimeout 之后 才会将在耗时循环期间发生的任何输入事件(点击等)排队。 (为了测试这一点,请注意,如果您在耗时的循环之后立即删除 onclick 回调,那么在循环期间发生的点击不会触发该回调。)所以@ 987654337@ 先排队,优先。

设置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 事件。不确定那里发生了什么。)

【问题讨论】:

  • 我认为您的情况不能保证订单。我还没有听说过事件处理优先级。如果您喜欢的情况很明显,我认为您应该在执行 setTimeout 之前处理一个事件队列。您是否在 google 中搜索过:“javascript 事件队列”?
  • 例如,对于 Opera:dev.opera.com/articles/view/…
  • @sergzach 但是我将如何“在执行setTimeout 之前处理事件队列”?据我所知,没有办法从onclick 回调等中询问“在此之后是否还有其他 DOM 事件要触发?”
  • Opera 文章强烈建议我回答我的问题是“是”,但我希望看到更权威的东西。
  • @jessegavin 这是一个测试用例:gist.github.com/1038695

标签: javascript dom input dom-events


【解决方案1】:

我认为你的实验走错了路。一个问题当然是您在这里与不同的消息循环实现作斗争。另一个(您似乎不认识的那个)是不同的双击处理。如果您单击该链接两次,您将不会在 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/

【讨论】:

  • 在 Firefox 5 for Mac 上,无论我在您的示例中的循环过程中单击多少次,都会出现 0-2 次中断。所以恐怕我不能接受这是一个解决方案。不过,关于双击的要点是好的。也许我们可以通过测试键盘事件来澄清问题?
  • @Trevor Burnham:我明白了,Mac 上的 Firefox 中的消息队列处理肯定是不同的(上面的所有测试都发生在 Windows 上)。太糟糕了。
  • @Trevor Burnham:我在jsfiddle.net/KBFqn/8 中切换到keypress 处理,它似乎在所有浏览器中都能正常工作,即使在Safari 中也是如此。单击链接,然后按几个键。看起来浏览器对待键盘事件和鼠标事件的方式不同。
  • 在 Mac 上:在 Chrome 中,所有keypresses 似乎都在超时之前注册。在 Safari 5 中,结果不一致(我会按 6 个键,然后 2-4 会注册)。在 Firefox 5 中,结果更糟(我将按 6 个键,0-2 将注册)。我很想在 Windows/Linux 上进行测试,因为操作系统处理输入事件的方式完全有可能在这里产生很大的不同。
【解决方案2】:

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

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

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

如果您单击 A、B、C 或 D,则会在右侧记录一条消息。当您按下 start 时,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'});

【讨论】:

  • Web Workers 是一个很好的建议,但 1) 没有得到普遍支持(例如,在 IEcan't be used for drawing on the canvas,这是我使用的用例发布此问题时请注意。
【解决方案3】:

在这一点上,我准备说,很遗憾,这个问题没有解决方案,它可以在所有浏览器下、在任何情况下、任何时候都有效。简而言之:如果您运行 JavaScript 函数,则无法可靠地区分用户期间触发的输入事件和用户之后触发的输入事件。这对 JS 开发人员有有趣的影响,尤其是那些使用交互式画布的开发人员。

我对 JS 输入事件如何工作的心智模型是错误的。我以为它去了

  1. 用户在代码运行时单击 DOM 元素
  2. 如果该元素具有 click 事件处理程序,则回调排队
  3. 当所有阻塞代码都执行完毕后,回调运行

但是,我的实验以及 Wladimir Palant(感谢 Wladimir)贡献的实验表明,正确的模型是

  1. 用户在代码运行时单击 DOM 元素
  2. 浏览器抓取点击的坐标等
  3. 在所有阻塞代码执行后的某个时间,浏览器会检查哪个 DOM 元素位于这些坐标处,然后运行回调(如果有)

我说“一段时间后”是因为不同的浏览器似乎对此有非常不同的行为——在 Mac 版 Chrome 中,我可以在我的阻塞代码末尾设置一个 setTimeout func2, 0 并期望 func2 在点击回调(在阻塞代码完成后仅运行 1-3ms);但在 Firefox 中,超时总是首先解决,并且点击回调通常发生在阻塞代码完成执行后约 40 毫秒。这种行为显然超出了任何 JS 或 DOM 规范的范围。正如 John Resig 在他的经典 How JavaScript Timers Work 中所说:

当异步事件发生时(如鼠标点击、计时器触发或 XMLHttpRequest 完成),它会排队等待稍后执行(这种排队实际发生的方式肯定会因浏览器而异 em>,所以认为这是一种简化)。

(强调我的。)

那么从实际的角度来看,这意味着什么?这不是问题,因为阻塞代码的执行时间接近 0。这意味着这个问题是遵循旧建议的另一个原因:将 JS 操作分成小块以避免阻塞线程。

正如无用代码所建议的那样,当您可以使用 Web Worker 时,它们会变得更好——但请注意,您已经超越了compatibility with Internet Explorer and all major mobile browsers

最后,我希望浏览器制造商在未来将输入事件标准化。这是many quirks in that area 之一。我希望 Chrome 能够引领未来:出色的线程隔离、低事件延迟和相对一致的排队行为。 Web 开发人员可以做梦,不是吗?

【讨论】:

  • 请记住,阻塞的 JavaScript 代码可能会改变事件处理程序或文档的 DOM 结构。此外,文档的不同级别上可能有事件侦听器,其中一些可能会或可能不会到达,具体取决于是否有任何侦听器调用stopPropagation()。所以你简单的心智模型是行不通的。
  • 但是您可以将#2 更改为“点击事件排队等待稍后处理”,将#3 更改为“当所有阻塞代码已执行时,处理排队事件(确定命中目标,触发适当的侦听器) )”。带有备注“如果这些事件在超时之前添加到队列中,则超时进入同一个队列并保证在事件处理后运行”。这与 Firefox 在 Windows 上的行为相匹配,可惜它在 Mac 上明显不同。
  • 我想我们用不同的词说同样的事情。当您说“点击事件排队等待稍后处理”时,这对我来说并不正确,因为当我听到“点击事件”这个词时,我会想到将特定参数传递给特定回调;但众所周知,直到#3 之后才确定参数和回调。 (即使e.timeStamp,在支持它的浏览器中,也匹配第一个回调被调用的时间,而不是用户点击某物的时间。)
  • 这是因为 Firefox(和其他浏览器可能类似)在“本机事件”和“DOM 事件”之间有区别。 DOM 事件是事件侦听器看到的,在事件处理开始之前不会创建它。另一方面,原生事件并不比操作系统提供的更多。
【解决方案4】:

您可以在函数末尾使用带有自定义事件名称的dispatchEvent。这在 IE 上不起作用,但仍然可以;只需改用fireEvent

看看这个:

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

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

【讨论】:

  • 不确定这有什么帮助,minitech。我创建了一个分支here,它显示了input 元素在afterRun 的值;我得到与longRun 循环之前相同的值。你可以让afterRun在处理完用户输入后执行吗?
  • @Trevor:嗯...在那种情况下,究竟是什么情况?用户会打字吗?改变焦点是否可以接受?整体情况如何?
  • 问题出现在我使用画布的工作中。假设渲染一个画布需要 250 毫秒。我希望用户的clickmousemove 事件在此期间反映在这250 毫秒内可见的状态,但我希望所有clickmousemove 事件之后都反映新绘制的状态。所以我希望我的updateState 函数在画布渲染期间发生的所有输入都已解决之后,但在任何进一步的输入发生之前触发。事实证明这是一个挑战。
  • @Trevor:那么您可以使用状态变量。编辑答案。
  • 行不通。 isRunning 在任何输入事件触发之前肯定为 false。
【解决方案5】:

您可以让事件处理程序检查func1 是否设置了标志;如果是,请排队 func2 如果尚未排队。

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

这种方法的正确性取决于func1 期间的所有事件同时排队。如果不是这种情况,您可以使func2 幂等,或者(取决于func2 的语义)在其上设置一个丑陋的“N 毫秒内无法再次调用”锁。

【讨论】:

  • 假设用户在func1期间点击了10次。将if (!h) h = setTimeout(func2, 0) 添加到click 回调中很容易,但我可以确定func2 在其他9 次点击(可能还有其他输入事件)被解决之前不会触发?
  • 啊,当然,在每个输入处理程序中重新排队(clearTimeout 加上setTimeout)确实有效:jsfiddle.net/EJNSu/11 如果没有人有更优雅的解决方案,我会接受这个答案.
  • 嗯,我说得太早了。如果我将alert 代码从任意500ms 超时移到func2 中,上面的示例将失败(在Mac 的Firefox 5 下)——这意味着点击实际上是在func2 之后触发的。见jsfiddle.net/TrevorBurnham/j8kVY
【解决方案6】:

请更好地描述你的场景。

你需要做什么

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

例如,让我展示它是如何工作的

首先注册所有异步或同步例程 第二次注册结束回调 使用您的参数对例程进行第三次注册调用 第四个抛出的进程

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

首先注册所有异步或同步例程 第二次注册结束回调 使用您的参数对例程进行第三次注册调用 --注册你的第一个例程 --register BlockUi //可能不接受视图中的更多更改 --register UiWriter // 用户所做更改的 UoW --注册你上一个例程 第四个抛出的进程

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

函数 Should_Can_Serializer_calls() {
注册方法(模型);
model.Queue.BeginUnitProcess(); //清除执行堆栈,其他 model.Queue.AddEndMethod(成功结束); // 回调结束例程 model.AbstractCall("func1",1,"edu",15,""); //设置例程如何首先执行 model.AbstractCall("BlockUi"); //跟踪更改和用户的操作 model.AbstractCall("UiWork"); //跟踪更改和用户的操作 model.AbstractCall("func2","VALUE"); //设置第二个例程执行 模型.进程(); //抛出调用 }

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

那么,你想做什么?

【讨论】:

    猜你喜欢
    • 2011-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多