VDone Demo VDone Demo
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Nikolay Tuzov

Backend Developer
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • Basics
  • Built-in Objects
  • Object-Oriented Programming
  • Asynchronous Operations
    • 1. Overview of Asynchronous Operations
      • 1.1 Single-Threaded Model
      • 1.2 Synchronous and Asynchronous Tasks
      • 1.3 Task Queue and Event Loop
      • 1.4 Patterns for Asynchronous Operations
      • (1) Callback Functions
      • (2) Event Listeners
      • (3) Publish/Subscribe
      • 1.5 Flow Control of Asynchronous Operations
      • (1) Serial Execution
      • (2) Parallel Execution
      • (3) Combining Parallel and Serial
    • Reference
  • DOM
  • Events
  • Browser Model
  • 《JavaScript教程》笔记
xugaoyi
2020-01-12
Contents

Asynchronous Operations

# Asynchronous Operations

# 1. Overview of Asynchronous Operations

# 1.1 Single-Threaded Model

The single-threaded model means that JavaScript runs on only one thread. In other words, JavaScript can only execute one task at a time, and all other tasks must queue up and wait.

Note that JavaScript running on a single thread does not mean the JavaScript engine has only one thread. In fact, the JavaScript engine has multiple threads, but individual scripts can only run on one thread (called the main thread), while other threads work in the background.

JavaScript uses a single-threaded model (rather than multi-threading) for historical reasons. JavaScript was single-threaded from its inception because the designers didn't want to make browsers too complex, since multi-threading requires shared resources and can modify each other's results. For a scripting language designed for web pages, this would be overly complex. If JavaScript had two threads simultaneously -- one adding content to a DOM node and another deleting that node -- which thread should the browser follow? Would a locking mechanism be needed? So, to avoid complexity, JavaScript was designed as single-threaded from the start, and this has become a core characteristic of the language that will not change.

The advantage of this model is that it is simpler to implement, with a relatively straightforward execution environment; the disadvantage is that if any task takes a long time, all subsequent tasks must wait, slowing down the entire program. Common browser unresponsiveness (freezing) is often caused by a piece of JavaScript code running for too long (such as an infinite loop), blocking the page and preventing other tasks from executing. JavaScript itself is not slow -- what's slow is reading and writing external data, like waiting for an Ajax request to return results. If the remote server is slow to respond or the network is poor, the script will stall for an extended period.

If the queuing were due to heavy computation keeping the CPU busy, that would be understandable. But often the CPU is idle because I/O operations (input/output) are slow (like reading data over the network via Ajax), and it has to wait for results before continuing. JavaScript's designers realized that the CPU could ignore the I/O operation, suspend the waiting task, run subsequent tasks first, and when the I/O operation returns a result, go back and continue the suspended task. This mechanism is the "Event Loop" used internally by JavaScript.

Although the single-threaded model imposes significant limitations on JavaScript, it also gives it advantages that other languages don't have. If used well, JavaScript programs won't experience blocking, which is why Node can handle high traffic with very few resources.

To leverage multi-core CPU computing power, HTML5 introduced the Web Worker standard, allowing JavaScript scripts to create multiple threads. However, child threads are fully controlled by the main thread and cannot manipulate the DOM. So this new standard does not change JavaScript's single-threaded nature.

# 1.2 Synchronous and Asynchronous Tasks

All tasks in a program can be divided into two categories: synchronous tasks and asynchronous tasks.

Synchronous tasks are those that are not suspended by the engine and are queued for execution on the main thread. The previous task must complete before the next one can execute.

Asynchronous tasks are those that the engine sets aside, not entering the main thread but instead placed in a task queue. Only when the engine determines an asynchronous task can be executed (for example, when an Ajax operation receives a result from the server), the task (in the form of a callback function) enters the main thread for execution. Code after an asynchronous task doesn't need to wait for the asynchronous task to finish -- it runs immediately. In other words, asynchronous tasks do not cause "blocking."

For example, an Ajax operation can be handled as either a synchronous or asynchronous task, at the developer's discretion. If synchronous, the main thread waits for the Ajax operation to return a result before continuing; if asynchronous, the main thread proceeds immediately after sending the Ajax request, and when the result arrives, the main thread executes the corresponding callback function.

# 1.3 Task Queue and Event Loop

While JavaScript is running, in addition to the currently executing main thread, the engine also provides a task queue that holds various asynchronous tasks that the current program needs to handle. (In practice, there are multiple task queues depending on the type of asynchronous task. For simplicity, we assume there is only one queue here.)

First, the main thread executes all synchronous tasks. Once all synchronous tasks are complete, it checks the task queue for asynchronous tasks. If conditions are met, the asynchronous task re-enters the main thread and becomes a synchronous task. When it finishes, the next asynchronous task enters the main thread. Once the task queue is empty, the program ends.

Asynchronous tasks are typically written as callback functions. When an asynchronous task re-enters the main thread, its callback function is executed. If an asynchronous task has no callback function, it won't enter the task queue -- meaning it won't re-enter the main thread, since there's no callback to specify the next step.

How does the JavaScript engine know if an asynchronous task has a result and can enter the main thread? The answer is that the engine keeps checking, over and over. As soon as synchronous tasks are done, the engine checks whether suspended asynchronous tasks are ready to enter the main thread. This cyclical checking mechanism is called the Event Loop. Wikipedia (opens new window) defines it as: "An event loop is a programming construct that waits for and dispatches events or messages in a program."

# 1.4 Patterns for Asynchronous Operations

# (1) Callback Functions

Callback functions are the most basic method for asynchronous operations.

Here are two functions f1 and f2, where the intent is that f2 must wait until f1 completes before executing.

function f1() { // This function is asynchronous
  // ...
}

function f2() { // Intended to execute after f1, but f1 is async so f2 runs first
  // ...
}

f1();
f2();
1
2
3
4
5
6
7
8
9
10

The problem is that if f1 is asynchronous, f2 will execute immediately without waiting for f1 to finish.

The solution is to rewrite f1 and pass f2 as a callback function.

function f1(callback) {
  // ...
  callback(); // f2 is passed as a callback, executing after f1 completes
}

function f2() {
  // ...
}

f1(f2);

// Advantages: Simple, easy to understand and implement
// Disadvantages: Not conducive to code readability and maintenance, creates high coupling between parts, makes program structure confusing and flow difficult to trace (especially with multiple nested callbacks), and each task can only specify one callback function.
1
2
3
4
5
6
7
8
9
10
11
12
13

The advantage of callback functions is simplicity and ease of understanding and implementation. The disadvantage is that they are not conducive to code readability and maintenance, create high coupling (opens new window) between parts, make program structure confusing and flow difficult to trace (especially with nested callbacks), and each task can only specify one callback function.

# (2) Event Listeners

Another approach is the event-driven model. The execution of asynchronous tasks depends not on code order, but on whether a certain event occurs.

Using f1 and f2 again, first bind an event to f1 (using jQuery syntax (opens new window) here).

f1.on('done', f2); // When the done event fires, execute f2
1

This means when f1 emits the done event, f2 is executed. Then rewrite f1:

function f1() {
  setTimeout(function () { // Asynchronous task
    // ...
    f1.trigger('done'); // trigger fires the done event
  }, 1000);
}

// Advantages: Fairly easy to understand, can bindmultiple events, each event can specify multiple callbacks, and enables decoupling for modular implementation.
// Disadvantages: The entire program must become event-driven, making the flow unclear. Reading the code, it's hard to see the main flow.
1
2
3
4
5
6
7
8
9

In the above code, f1.trigger('done') means that after f1 completes, it immediately triggers the done event, which starts executing f2.

The advantages of this approach are that it is fairly easy to understand, allows binding multiple events, each event can specify multiple callbacks, and enables "decoupling (opens new window)" for modular implementation. The disadvantage is that the entire program must become event-driven, making the flow unclear. When reading the code, it's hard to see the main flow.

# (3) Publish/Subscribe

Events can be thought of as "signals." If there is a "signal center," when a task completes, it "publishes" a signal to the signal center. Other tasks can "subscribe" to this signal from the center to know when they can start executing. This is called the "Publish/Subscribe Pattern (opens new window)," also known as the "Observer Pattern (opens new window)."

This pattern has multiple implementations (opens new window). The following uses Ben Alman's Tiny Pub/Sub (opens new window), a jQuery plugin.

First, f2 subscribes to the done signal from the signal center jQuery.

jQuery.subscribe('done', f2); // Subscribe to the done signal; execute f2 when received
1

Then rewrite f1:

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done'); // Publish the done signal
  }, 1000);
}
1
2
3
4
5
6

In the above code, jQuery.publish('done') means that after f1 completes, it publishes the done signal to the signal center jQuery, which triggers f2's execution.

After f2 finishes, it can unsubscribe.

jQuery.unsubscribe('done', f2); // Unsubscribe from the done signal
1

This approach is similar in nature to "event listeners," but clearly superior. You can see how many signals exist and how many subscribers each has by looking at the "message center," thereby monitoring the program's execution.

# 1.5 Flow Control of Asynchronous Operations

When there are multiple asynchronous operations, there is a flow control problem: how to determine the execution order and how to enforce that order.

function async(arg, callback) {
  console.log('argument is ' + arg +', returning result after 1 second');
  setTimeout(function () { callback(arg * 2); }, 1000);
}
1
2
3
4

The async function above is an asynchronous task that takes 1 second to complete each time before calling the callback.

If there are six such asynchronous tasks that all need to complete before the final final function can execute, how should the flow be arranged?

function final(value) {
  console.log('Done: ', value);
}

async(1, function (value) {
  async(2, function (value) {
    async(3, function (value) {
      async(4, function (value) {
        async(5, function (value) {
          async(6, final);
        });
      });
    });
  });
});
// argument is 1, returning result after 1 second
// argument is 2, returning result after 1 second
// argument is 3, returning result after 1 second
// argument is 4, returning result after 1 second
// argument is 5, returning result after 1 second
// argument is 6, returning result after 1 second
// Done:  12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Six nested callbacks are not only tedious to write and error-prone, but also hard to maintain.

# (1) Serial Execution

We can write a flow control function to manage asynchronous tasks, executing the next task only after the current one completes. This is called serial execution.

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('argument is ' + arg +', returning result after 1 second');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('Done: ', value);
}

function series(item) {
  if(item) {
    async( item, function(result) {
      results.push(result);
      return series(items.shift());
    });
  } else {
      // results => [2,4,6,8,10,12]
    return final(results[results.length - 1]);
  }
}

series(items.shift());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

The series function above executes asynchronous tasks sequentially, calling final only after all tasks complete. The items array holds the parameters for each asynchronous task, and the results array holds the results.

Note that the above approach takes six seconds to complete the entire script.

# (2) Parallel Execution

The flow control function can also execute all tasks in parallel -- all asynchronous tasks run simultaneously, and final is only called after all of them complete.

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('argument is ' + arg +', returning result after 1 second');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('Done: ', value);
}

items.forEach(function(item) {
  async(item, function(result){
    results.push(result);
    if(results.length === items.length) {
      final(results[results.length - 1]);
    }
  })
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

The forEach method above launches all six asynchronous tasks simultaneously and only calls final after they all complete.

By comparison, the above approach takes only one second to complete the entire script. Parallel execution is more efficient, saving time compared to serial execution where only one task runs at a time. However, if too many tasks run in parallel, system resources can be exhausted, slowing everything down. This leads to the third flow control approach.

# (3) Combining Parallel and Serial

Combining parallel and serial means setting a threshold so that at most n asynchronous tasks run in parallel at any time, avoiding excessive consumption of system resources.

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0; // Tracks the number of currently running tasks
var limit = 2; // Maximum number of concurrent tasks

function async(arg, callback) {
  console.log('argument is ' + arg +', returning result after 1 second');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('Done: ', value);
}

function launcher() {
  while(running < limit && items.length > 0) {
    var item = items.shift();
    async(item, function(result) {
      results.push(result);
      running--;
      if(items.length > 0) {
        launcher();
      } else if(running == 0) {
        final(results);
      }
    });
    running++;
  }
}

launcher();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

In the above code, at most two asynchronous tasks run simultaneously. The running variable tracks the number of currently running tasks. As long as it is below the threshold, a new task is started. When it reaches 0, all tasks are complete and final is called.

This code takes three seconds to complete the entire script, falling between serial and parallel execution. By adjusting the limit variable, you can achieve the optimal balance between efficiency and resource usage.

# Reference

Study reference: https://wangdoc.com/javascript/ (opens new window)

Edit (opens new window)
#JavaScript
Last Updated: 2026/03/21, 12:14:36
Object-Oriented Programming
DOM

← Object-Oriented Programming DOM→

Recent Updates
01
How I Discovered Disposable Email — A True Story
06-12
02
Animations in Grid Layout
09-15
03
Renaming a Git Branch
08-11
More Articles >
Theme by VDone | Copyright © 2026-2026 Nikolay Tuzov | MIT License | Telegram
  • Auto
  • Light Mode
  • Dark Mode
  • Reading Mode