You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: 1-js/06-advanced-functions/08-settimeout-setinterval/article.md
+12-171
Original file line number
Diff line number
Diff line change
@@ -236,7 +236,7 @@ For `setInterval` the function stays in memory until `clearInterval` is called.
236
236
There's a side-effect. A function references the outer lexical environment, so, while it lives, outer variables live too. They may take much more memory than the function itself. So when we don't need the scheduled function anymore, it's better to cancel it, even if it's very small.
237
237
````
238
238
239
-
## setTimeout(...,0)
239
+
## Zero delay setTimeout
240
240
241
241
There's a special use case: `setTimeout(func, 0)`, or just `setTimeout(func)`.
242
242
@@ -254,114 +254,12 @@ alert("Hello");
254
254
255
255
The first line "puts the call into calendar after 0ms". But the scheduler will only "check the calendar" after the current code is complete, so `"Hello"` is first, and `"World"` -- after it.
256
256
257
-
### Splitting CPU-hungry tasks
257
+
There are also advanced browser-related use cases of zero-delay timeout, that we'll discuss in the chapter <info:event-loop>.
258
258
259
-
There's a trick to split CPU-hungry tasks using `setTimeout`.
259
+
````smart header="Zero delay is in fact not zero (in a browser)"
260
+
In the browser, there's a limitation of how often nested timers can run. The [HTML5 standard](https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers) says: "after five nested timers, the interval is forced to be at least 4 milliseconds.".
260
261
261
-
For instance, a syntax-highlighting script (used to colorize code examples on this page) is quite CPU-heavy. To highlight the code, it performs the analysis, creates many colored elements, adds them to the document -- for a big text that takes a lot. It may even cause the browser to "hang", which is unacceptable.
262
-
263
-
So we can split the long text into pieces. First 100 lines, then plan another 100 lines using `setTimeout(..., 0)`, and so on.
264
-
265
-
For clarity, let's take a simpler example for consideration. We have a function to count from `1` to `1000000000`.
266
-
267
-
If you run it, the CPU will hang. For server-side JS that's clearly noticeable, and if you are running it in-browser, then try to click other buttons on the page -- you'll see that whole JavaScript actually is paused, no other actions work until it finishes.
268
-
269
-
```js run
270
-
let i =0;
271
-
272
-
let start =Date.now();
273
-
274
-
functioncount() {
275
-
276
-
// do a heavy job
277
-
for (let j =0; j <1e9; j++) {
278
-
i++;
279
-
}
280
-
281
-
alert("Done in "+ (Date.now() - start) +'ms');
282
-
}
283
-
284
-
count();
285
-
```
286
-
287
-
The browser may even show "the script takes too long" warning (but hopefully it won't, because the number is not very big).
288
-
289
-
Let's split the job using the nested `setTimeout`:
290
-
291
-
```js run
292
-
let i =0;
293
-
294
-
let start =Date.now();
295
-
296
-
functioncount() {
297
-
298
-
// do a piece of the heavy job (*)
299
-
do {
300
-
i++;
301
-
} while (i %1e6!=0);
302
-
303
-
if (i ==1e9) {
304
-
alert("Done in "+ (Date.now() - start) +'ms');
305
-
} else {
306
-
setTimeout(count); // schedule the new call (**)
307
-
}
308
-
309
-
}
310
-
311
-
count();
312
-
```
313
-
314
-
Now the browser UI is fully functional during the "counting" process.
315
-
316
-
We do a part of the job `(*)`:
317
-
318
-
1. First run: `i=1...1000000`.
319
-
2. Second run: `i=1000001..2000000`.
320
-
3. ...and so on, the `while` checks if `i` is evenly divided by `1000000`.
321
-
322
-
Then the next call is scheduled in `(**)` if we're not done yet.
323
-
324
-
Pauses between `count` executions provide just enough "breath" for the JavaScript engine to do something else, to react to other user actions.
325
-
326
-
The notable thing is that both variants -- with and without splitting the job by `setTimeout` -- are comparable in speed. There's no much difference in the overall counting time.
327
-
328
-
To make them closer, let's make an improvement.
329
-
330
-
We'll move the scheduling in the beginning of the `count()`:
331
-
332
-
```js run
333
-
let i =0;
334
-
335
-
let start =Date.now();
336
-
337
-
functioncount() {
338
-
339
-
// move the scheduling at the beginning
340
-
if (i <1e9-1e6) {
341
-
setTimeout(count); // schedule the new call
342
-
}
343
-
344
-
do {
345
-
i++;
346
-
} while (i %1e6!=0);
347
-
348
-
if (i ==1e9) {
349
-
alert("Done in "+ (Date.now() - start) +'ms');
350
-
}
351
-
352
-
}
353
-
354
-
count();
355
-
```
356
-
357
-
Now when we start to `count()` and see that we'll need to `count()` more, we schedule that immediately, before doing the job.
358
-
359
-
If you run it, it's easy to notice that it takes significantly less time.
360
-
361
-
````smart header="Minimal delay of nested timers in-browser"
362
-
In the browser, there's a limitation of how often nested timers can run. The [HTML5 standard](https://www.w3.org/TR/html5/webappapis.html#timers) says: "after five nested timers, the interval is forced to be at least four milliseconds.".
363
-
364
-
Let's demonstrate what it means with the example below. The `setTimeout` call in it re-schedules itself after `0ms`. Each call remembers the real time from the previous one in the `times` array. What do the real delays look like? Let's see:
262
+
Let's demonstrate what it means with the example below. The `setTimeout` call in it re-schedules itself with zero delay. Each call remembers the real time from the previous one in the `times` array. What do the real delays look like? Let's see:
First timers run immediately (just as written in the spec), and then the delay comes into play and we see `9, 15, 20, 24...`.
279
+
First timers run immediately (just as written in the spec), and then we see `9, 15, 20, 24...`. The 4+ ms obligatory delay between invocations comes into play.
280
+
281
+
The similar thing happens if we use `setInterval` instead of `setTimeout`: `setInterval(f)` runs `f` few times with zero-delay, and afterwards with 4+ ms delay.
382
282
383
283
That limitation comes from ancient times and many scripts rely on it, so it exists for historical reasons.
384
284
385
-
For server-side JavaScript, that limitation does not exist, and there exist other ways to schedule an immediate asynchronous job, like [process.nextTick](https://nodejs.org/api/process.html) and [setImmediate](https://nodejs.org/api/timers.html) for Node.js. So the notion is browser-specific only.
285
+
For server-side JavaScript, that limitation does not exist, and there exist other ways to schedule an immediate asynchronous job, like [setImmediate](https://nodejs.org/api/timers.html) for Node.js. So this note is browser-specific.
386
286
````
387
287
388
-
### Allowing the browser to render
389
-
390
-
Another benefit of splitting heavy tasks for browser scripts is that we can show a progress bar or something to the user.
391
-
392
-
Usually the browser does all "repainting" after the currently running code is complete. So if we do a single huge function that changes many elements, the changes are not painted out till it finishes.
393
-
394
-
Here's the demo:
395
-
```html run
396
-
<divid="progress"></div>
397
-
398
-
<script>
399
-
let i =0;
400
-
401
-
functioncount() {
402
-
for (let j =0; j <1e6; j++) {
403
-
i++;
404
-
// put the current i into the <div>
405
-
// (we'll talk about innerHTML in the specific chapter, it just writes into element here)
406
-
progress.innerHTML= i;
407
-
}
408
-
}
409
-
410
-
count();
411
-
</script>
412
-
```
413
-
414
-
If you run it, the changes to `i` will show up after the whole count finishes.
415
-
416
-
And if we use `setTimeout` to split it into pieces then changes are applied in-between the runs, so this looks better:
417
-
418
-
```html run
419
-
<divid="progress"></div>
420
-
421
-
<script>
422
-
let i =0;
423
-
424
-
functioncount() {
425
-
426
-
// do a piece of the heavy job (*)
427
-
do {
428
-
i++;
429
-
progress.innerHTML= i;
430
-
} while (i %1e3!=0);
431
-
432
-
if (i <1e9) {
433
-
setTimeout(count);
434
-
}
435
-
436
-
}
437
-
438
-
count();
439
-
</script>
440
-
```
441
-
442
-
Now the `<div>` shows increasing values of `i`.
443
-
444
288
## Summary
445
289
446
290
- Methods `setInterval(func, delay, ...args)` and `setTimeout(func, delay, ...args)` allow to run the `func` regularly/once after `delay` milliseconds.
447
291
- To cancel the execution, we should call `clearInterval/clearTimeout` with the value returned by `setInterval/setTimeout`.
448
292
- Nested `setTimeout` calls is a more flexible alternative to `setInterval`. Also they can guarantee the minimal time *between* the executions.
449
-
- Zero-timeout scheduling `setTimeout(func, 0)` (the same as `setTimeout(func)`) is used to schedule the call "as soon as possible, but after the current code is complete".
450
-
451
-
Some use cases of `setTimeout(func)`:
452
-
- To split CPU-hungry tasks into pieces, so that the script doesn't "hang"
453
-
- To let the browser do something else while the process is going on (paint the progress bar).
293
+
- Zero delay scheduling with `setTimeout(func, 0)` (the same as `setTimeout(func)`) is used to schedule the call "as soon as possible, but after the current code is complete".
294
+
- The browsere ensures that for five or more nested call of `setTimeout`, or for zero-delay `setInterval`, the real delay between calls is at least 4ms. That's for historical reasons.
454
295
455
296
Please note that all scheduling methods do not *guarantee* the exact delay. We should not rely on that in the scheduled code.
456
297
@@ -459,4 +300,4 @@ For example, the in-browser timer may slow down for a lot of reasons:
459
300
- The browser tab is in the background mode.
460
301
- The laptop is on battery.
461
302
462
-
All that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and settings.
303
+
All that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and OS-level performance settings.
Promise handlers `.then`/`.catch`/`.finally` are always asynchronous.
5
5
@@ -52,140 +52,61 @@ Promise.resolve()
52
52
53
53
Now the order is as intended.
54
54
55
-
## Event loop
56
-
57
-
In-browser JavaScript execution flow, as well as Node.js, is based on an *event loop*.
58
-
59
-
"Event loop" is a process when the engine sleeps and waits for events. When they occur - handles them and sleeps again.
60
-
61
-
Events may come either from external sources, like user actions, or just as the end signal of an internal task.
62
-
63
-
Examples of events:
64
-
-`mousemove`, a user moved their mouse.
65
-
-`setTimeout` handler is to be called.
66
-
- an external `<script src="...">` is loaded, ready to be executed.
67
-
- a network operation, e.g. `fetch` is complete.
68
-
- ...etc.
69
-
70
-
Things happen -- the engine handles them -- and waits for more to happen (while sleeping and consuming close to zero CPU).
71
-
72
-

73
-
74
-
As you can see, there's also a queue here. A so-called "macrotask queue" (v8 term).
75
-
76
-
When an event happens, while the engine is busy, its handling is enqueued.
77
-
78
-
For instance, while the engine is busy processing a network `fetch`, a user may move their mouse causing `mousemove`, and `setTimeout` may be due and so on, just as painted on the picture above.
79
-
80
-
Events from the macrotask queue are processed on "first come – first served" basis. When the engine browser finishes with `fetch`, it handles `mousemove` event, then `setTimeout` handler, and so on.
81
-
82
-
So far, quite simple, right? The engine is busy, so other tasks queue up.
83
-
84
-
Now the important stuff.
85
-
86
-
**Microtask queue has a higher priority than the macrotask queue.**
87
-
88
-
In other words, the engine first executes all microtasks, and then takes a macrotask. Promise handling always has the priority.
89
-
90
-
For instance, take a look:
91
-
92
-
```js run
93
-
setTimeout(() =>alert("timeout"));
94
-
95
-
Promise.resolve()
96
-
.then(() =>alert("promise"));
97
-
98
-
alert("code");
99
-
```
100
-
101
-
What's the order?
102
-
103
-
1.`code` shows first, because it's a regular synchronous call.
104
-
2.`promise` shows second, because `.then` passes through the microtask queue, and runs after the current code.
105
-
3.`timeout` shows last, because it's a macrotask.
106
-
107
-
It may happen that while handling a macrotask, new promises are created.
108
-
109
-
Or, vice-versa, a microtask schedules a macrotask (e.g. `setTimeout`).
110
-
111
-
For instance, here `.then` schedules a `setTimeout`:
112
-
113
-
```js run
114
-
Promise.resolve()
115
-
.then(() => {
116
-
setTimeout(() =>alert("timeout"), 0);
117
-
})
118
-
.then(() => {
119
-
alert("promise");
120
-
});
121
-
```
122
-
123
-
Naturally, `promise` shows up first, because `setTimeout` macrotask awaits in the less-priority macrotask queue.
124
-
125
-
As a logical consequence, macrotasks are handled only when promises give the engine a "free time". So if we have a chain of promise handlers that don't wait for anything, execute right one after another, then a `setTimeout` (or a user action handler) can never run in-between them.
126
-
127
55
## Unhandled rejection
128
56
129
57
Remember "unhandled rejection" event from the chapter <info:promise-error-handling>?
130
58
131
-
Now we can describe how JavaScript finds out that a rejection was not handled.
59
+
Now we can see exactly how JavaScript finds out that there was an unhandled rejection
132
60
133
-
**"Unhandled rejection" is when a promise error is not handled at the end of the microtask queue.**
61
+
**"Unhandled rejection" occurs when a promise error is not handled at the end of the microtask queue.**
134
62
135
-
For instance, consider this code:
63
+
Normally, if we expect an error, we add `.catch` to the promise chain to handle it:
136
64
137
65
```js run
138
66
let promise =Promise.reject(newError("Promise Failed!"));
Now the unhandled rejection appears again. Why? Because `unhandledrejection` is generated when the microtask queue is complete. The engine examines promises and, if any of them is in "rejected" state, then the event triggers.
172
-
173
-
In the example, the `.catch` added by `setTimeout` triggers too, of course it does, but later, after `unhandledrejection` has already occurred.
96
+
Now, if you run it, we'll see `Promise Failed!` message first, and then `caught`.
174
97
175
-
## Summary
98
+
If we didn't know about microtasks, we could wonder: "Why did `unhandledrejection` happen? We did catch the error!".
176
99
177
-
- Promise handling is always asynchronous, as all promise actions pass through the internal "promise jobs" queue, also called "microtask queue" (v8 term).
100
+
But now we do know that `unhandledrejection` is generated when the microtask queue is complete: the engine examines promises and, if any of them is in "rejected" state, then the event triggers.
178
101
179
-
**So, `.then/catch/finally` handlers are called after the current code is finished.**
102
+
...By the way, the `.catch` added by `setTimeout` also triggers, of course it does, but later, after `unhandledrejection` has already occurred.
180
103
181
-
If we need to guarantee that a piece of code is executed after `.then/catch/finally`, it's best to add it into a chained `.then` call.
182
-
183
-
- There's also a "macrotask queue" that keeps various events, network operation results, `setTimeout`-scheduled calls, and so on. These are also called "macrotasks" (v8 term).
104
+
## Summary
184
105
185
-
The engine uses the macrotask queue to handle them in the appearance order.
106
+
Promise handling is always asynchronous, as all promise actions pass through the internal "promise jobs" queue, also called "microtask queue" (v8 term).
186
107
187
-
**Macrotasks run after the code is finished*and* after the microtask queue is empty.**
108
+
So, `.then/catch/finally` handlers are always called after the current code is finished.
188
109
189
-
In other words, they have lower priority.
110
+
If we need to guarantee that a piece of code is executed after `.then/catch/finally`, we can add it into a chained `.then` call.
190
111
191
-
So the order is: regular code, then promise handling, then everything else, like events etc.
112
+
In most Javascript engines, including browsers and Node.js, the concept of microtasks is closely tied with "event loop" and "macrotasks". As these have no direct relation to promises, they are covered in another part of the tutorial, in the chapter <info:event-loop>.
0 commit comments