最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - DOM input events vs. setTimeoutsetInterval order - Stack Overflow

programmeradmin8浏览0评论

I have a block of JavaScript code running on my page; let's call it func1. It takes several milliseconds to run. While that code is running, the user may click, move the mouse, enter some keyboard input, etc. I have another block of code, func2, that I want to run after all of those queued-up input events have resolved. That is, I want to ensure the order:

  1. func1
  2. All handlers bound to input events that occurred while func1 was running
  3. func2

My question is: Is calling setTimeout func2, 0 at the end of func1 sufficient to guarantee this ordering, across all modern browsers? What if that line came at the beginning of func1—what order should I expect in that case?

Please back up your answers with either references to the relevant specs, or test cases.

Update: It turns out that no, it's not sufficient. What I failed to realize in my original question was that input events aren't even added to the queue until the current code block has been executed. So if I write

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

then only after that setTimeout is run will any input events (clicks, etc.) that occurred during the time-consuming loop be queued. (To test this, note that if you remove, say, an onclick callback immediately after the time-consuming loop, then clicks that happened during the loop won't trigger that callback.) So func2 is queued first and takes precedence.

Setting a timeout of 1 seemed to work around the issue in Chrome and Safari, but in Firefox, I saw input events resolving after timeouts as high as 80 (!). So a purely time-based approach clearly isn't going to do what I want.

Nor is it sufficient to simply wrap one setTimeout ... 0 inside of another. (I'd hoped that the first timeout would fire after the input events queued, and the second would fire after they resolved. No such luck.) Nor did adding a third, or a fourth, level of nesting suffice (see Update 2 below).

So if anyone has a way of achieving what I described (other than setting a timeout of 90+ milliseconds), I'd be very grateful. Or is this simply impossible with the current JavaScript event model?

Here's my latest JSFiddle testbed: /

Update 2: A partial workaround is to nest func2 inside of two timeouts, removing all input event handlers in the first timeout. However, this has the unfortunate side effect of causing some—or even all—input events that occurred during func1 to fail to resolve. (Head to / and try rapidly clicking the link several times to observe this behavior. How many clicks does the alert tell you that you had?) So this, again, surprises me; I wouldn't think that calling setTimeout func2, 0, where func2 sets onclick to null, could prevent that callback from being run in response to a click that happened a full second ago. I want to ensure that all input events fire, but that my function fires after them.

Update 3: I posted my answer below after playing with this testbed, which is illuminating: /

Move the mouse over the box (triggering a 1-second blocking loop), then click multiple times. After the loop, all the clicks you performed play out: The top box's click handler flips it under the other box, which then receives the next click, and so on. The timeout triggered in the mouseenter callback does not consistently occur after the click events, and the time it takes for the click events to occur varies wildly across browsers even on the same hardware and OS. (Another odd thing this experiment turned up: I sometimes get multiple jQuery mouseenter events even when I move the mouse steadily into the box. Not sure what's going on there.)

I have a block of JavaScript code running on my page; let's call it func1. It takes several milliseconds to run. While that code is running, the user may click, move the mouse, enter some keyboard input, etc. I have another block of code, func2, that I want to run after all of those queued-up input events have resolved. That is, I want to ensure the order:

  1. func1
  2. All handlers bound to input events that occurred while func1 was running
  3. func2

My question is: Is calling setTimeout func2, 0 at the end of func1 sufficient to guarantee this ordering, across all modern browsers? What if that line came at the beginning of func1—what order should I expect in that case?

Please back up your answers with either references to the relevant specs, or test cases.

Update: It turns out that no, it's not sufficient. What I failed to realize in my original question was that input events aren't even added to the queue until the current code block has been executed. So if I write

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

then only after that setTimeout is run will any input events (clicks, etc.) that occurred during the time-consuming loop be queued. (To test this, note that if you remove, say, an onclick callback immediately after the time-consuming loop, then clicks that happened during the loop won't trigger that callback.) So func2 is queued first and takes precedence.

Setting a timeout of 1 seemed to work around the issue in Chrome and Safari, but in Firefox, I saw input events resolving after timeouts as high as 80 (!). So a purely time-based approach clearly isn't going to do what I want.

Nor is it sufficient to simply wrap one setTimeout ... 0 inside of another. (I'd hoped that the first timeout would fire after the input events queued, and the second would fire after they resolved. No such luck.) Nor did adding a third, or a fourth, level of nesting suffice (see Update 2 below).

So if anyone has a way of achieving what I described (other than setting a timeout of 90+ milliseconds), I'd be very grateful. Or is this simply impossible with the current JavaScript event model?

Here's my latest JSFiddle testbed: http://jsfiddle/EJNSu/7/

Update 2: A partial workaround is to nest func2 inside of two timeouts, removing all input event handlers in the first timeout. However, this has the unfortunate side effect of causing some—or even all—input events that occurred during func1 to fail to resolve. (Head to http://jsfiddle/EJNSu/10/ and try rapidly clicking the link several times to observe this behavior. How many clicks does the alert tell you that you had?) So this, again, surprises me; I wouldn't think that calling setTimeout func2, 0, where func2 sets onclick to null, could prevent that callback from being run in response to a click that happened a full second ago. I want to ensure that all input events fire, but that my function fires after them.

Update 3: I posted my answer below after playing with this testbed, which is illuminating: http://jsfiddle/TrevorBurnham/uJxQB/

Move the mouse over the box (triggering a 1-second blocking loop), then click multiple times. After the loop, all the clicks you performed play out: The top box's click handler flips it under the other box, which then receives the next click, and so on. The timeout triggered in the mouseenter callback does not consistently occur after the click events, and the time it takes for the click events to occur varies wildly across browsers even on the same hardware and OS. (Another odd thing this experiment turned up: I sometimes get multiple jQuery mouseenter events even when I move the mouse steadily into the box. Not sure what's going on there.)

Share Improve this question edited Aug 22, 2020 at 10:41 Brian Tompsett - 汤莱恩 5,89372 gold badges61 silver badges133 bronze badges asked Jun 17, 2011 at 20:37 Trevor BurnhamTrevor Burnham 77.4k33 gold badges164 silver badges199 bronze badges 9
  • I think the order is not guaranteed in your case. I have not heard about event handling priorities. I think you should handle an event queue before doing setTimeout if you like situation is obvious. Did you search in google: "javascript event queue"? – sergzach Commented Jun 21, 2011 at 8:27
  • For example, for Opera: dev.opera./articles/view/… – sergzach Commented Jun 21, 2011 at 8:27
  • @sergzach But how would I "handle an event queue before doing setTimeout"? As far as I know, there's no way to ask from an onclick callback or the like, "Are any more DOM events in line to fire after this one?" – Trevor Burnham Commented Jun 21, 2011 at 19:23
  • 1 The Opera article strongly suggests to me that the answer to me question is "yes," but I'd like to see something more authoritative. – Trevor Burnham Commented Jun 21, 2011 at 19:27
  • @jessegavin Here's a test case: gist.github./1038695 – Trevor Burnham Commented Jun 21, 2011 at 19:45
 |  Show 4 more ments

6 Answers 6

Reset to default 4

I think you are on the wrong track with your experiments. One problem is of course that you are fighting different message loop implementations here. The other (the one you didn't recognize it seems) is different double click handling. If you click the link twice you won't get two click events in MSIE - it's rather one click event and a dblclick event (for you that looks like the second click was "swallowed"). All other browsers seem to generate two click events and a dblclick event in this scenario. So you need to handle dblclick events as well.

As message loops go, Firefox should be easiest to handle. From all I know, Firefox adds messages to the queue even when JavaScript code is running. So a simple setTimeout(..., 0) is sufficient to run code after the messages are processed. You should refrain from hiding the link after func1() is done however - at this point clicks aren't processed yet and they won't trigger event handlers on a hidden element. Note that even a zero timeout doesn't get added to the queue immediately, current Firefox versions have 4 milliseconds as the lowest possible timeout value.

MSIE is similar, only that there you need to handle dblclick events as I mentioned before. Opera seems to work like that as well but it doesn't like it if you don't call event.preventDefault() (or return false from the event handler which is essentially the same thing).

Chrome however seems to add the timeout to the queue first and only add ining messages after that. Nesting two timeouts (with zero timeout value) seems to do the job here.

The only browser where I cannot make things work reliably is Safari (version 4.0 on Windows). The scheduling of messages seems random there, looks like timers there execute on a different thread and can push messages into the message queue at random times. In the end you probably have to accept that your code might not get interrupted on the first occasion and the user might have to wait a second longer.

Here is my adaptation of your code: http://jsfiddle/KBFqn/7/

If I'm understanding your question correctly, you have a long-running function but you don't want to block the UI while it is running? After the long-running function is done you then want to run another function?

If so instead of using timeouts or intervals you might want to use Web Workers instead. All modern browsers including IE9 should support Web Workers.

I threw together an example page (couldn't put it on jsfiddle since Web Workers rely on an external .js file that has to be hosted on the same origin).

If you click A, B, C or D a message will be logged on the right. When you press start a Web Worker starts processing for 3 seconds. Any clicks during those 3 seconds will be immediately logged.

The important parts of the code are here:

func1.js The code that runs inside the 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);
}

The code that invokes the 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'});

At this point, I'm prepared to say that, regrettably, there is no solution to this problem that will work under all browsers, in every scenario, every time. In a nutshell: If you run a JavaScript function, there's no way to reliably distinguish between input events that the user triggered during that time and those the user triggered afterward. This has interesting implications for JS developers, especially those working with interactive canvases.

My mental model of how JS input events work was off the mark. I'd thought that it went

  1. The user clicks a DOM element while code is running
  2. If that element has a click event handler, the callback is queued
  3. When all blocking code has executed, the callback is run

However, my experiments, and those contributed by Wladimir Palant (thanks, Wladimir) show that the correct model is

  1. The user clicks a DOM element while code is running
  2. The browser captures the coordinates, etc. of the click
  3. Some time after all blocking code has executed, the browser checks which DOM element is at those coordinates, then runs the callback (if any)

I say "some time after" because different browsers seem to have very different behaviors for this—in Chrome for Mac, I can set a setTimeout func2, 0 at the end of my blocking code and expect func2 to run after the click callbacks (which run only 1-3ms after the blocking code finished); but in Firefox, the timeout always resolves first, and the click callbacks typically happen ~40ms after the blocking code finished executing. This behavior is apparently beyond the purview of any JS or DOM spec. As John Resig put it in his classic How JavaScript Timers Work:

When an asynchronous event occurs (like a mouse click, a timer firing, or an XMLHttpRequest pleting) it gets queued up to be executed later (how this queueing actually occurs surely varies from browser-to-browser, so consider this to be a simplification).

(Emphasis mine.)

So what does this mean from a practical standpoint? This is a non-issue as the execution time of blocking code approaches 0. Which means that this problem is yet another reason to hew to that old advice: Break up your JS operations into small chunks to avoid blocking the thread.

Web workers, as Useless Code suggested, are even better when you can use them—but be aware that you're foregoing patibility with Internet Explorer and all major mobile browsers.

Finally, I hope browser-makers will move forward on standardizing input events in the future. This is one of many quirks in that area. I hope Chrome will lead the way to the future: excellent thread isolation, low event latency, and relatively consistent queueing behavior. A web developer can dream, can't he?

You can use dispatchEvent with a custom event name at the end of your function. This won't work on IE, but is still possible; just use fireEvent instead.

Take a look at this:

http://jsfiddle/minitech/NsY9V/

Click "start the long run", and click on the textbox and type in it. Voilà!

You can make the event handlers check to see if a flag is set by func1; if so queue func2 if not already queued.

This may either be elegant or ugly depending on the specializedness of func2. (Actually it's probably just ugly.) If you choose this approach, you need some way to hook events, or alternatively your own bindEvent(event,handler,...) function which wraps the handler and binds the wrapped handler.

The correctness of this approach depends on all the events during func1 being queued at the same time. If this is not the case, you can either make func2 idempotent, or (depending on the semantics of func2) put an ugly "cannot be called again for N milliseconds" lock on it.

please describe better you scenario.

What you need do

some time ago i need do something how that was so i build an simple javascript's routine across serialize async call in one sync call. maybe you could used that added one variant

for example that let me show how that work

first register all async or sync routines second register end callback third register call's to routines with yours parameters fourth thrown process

in your case it neccesary added one call routine and that routine should be UoW of user actions. Now the main problem is not call to routine in and order of execution if not track changes done by the user

first register all async or sync routines second register end callback third register call's to routines with yours paramter --register your first routine --register BlockUi //maybe for not accept more changes in the view --register UiWriter // UoW of change done by user --register you last routine fourth thrown process

in real code that is one call dummy's function

function Should_Can_Serializer_calls() {
RegisterMethods(model);
model.Queue.BeginUnitProcess(); //clear stack of execution, y others model.Queue.AddEndMethod(SucessfullEnd); // callback to end routine model.AbstractCall("func1",1,"edu",15,""); //set routine how first to execute model.AbstractCall("BlockUi"); //track changes and user's actions model.AbstractCall("UiWork"); //track changes and user's actions model.AbstractCall("func2","VALUE"); //set second routine for execute model.Process(); //throw call }

Now the methods should be async for themselves for this you could use that library http://devedge-temp.mozilla/toolbox/examples/2003/CCallWrapper/index_en.html

so, what do you want do?

发布评论

评论列表(0)

  1. 暂无评论