preface

Before we begin, we want to let you know that this article is a summary of the asynchronous operations section of the JavaScript language. If you already know the following items, you can skip this section and go directly to the exercises

  • Single thread
  • Synchronization concept
  • Asynchronous concept
  • Mode of asynchronous operation
  • Flow control for asynchronous operations
  • Creating and clearing timers

If you are a little bit forgotten about some parts, 👇🏻 is ready for you!

Summary to summarize

Single thread

Single-threaded means that JavaScript runs on only one thread. That is, JavaScript can only perform one task at a time, and all other tasks must queue up later.

The reason JavaScript is single-threaded, rather than multi-threaded, is a matter of history. JavaScript has been single-threaded since its inception because it didn’t want to make the browser too complex, since multiple threads need to share resources and potentially modify each other’s results, which would be too complex for a web scripting language.

Benefits of single threading:

  • It’s relatively simple to implement
  • The execution environment is relatively simple

Disadvantages of single threading:

  • The disadvantage is that as long as one task takes a long time, subsequent tasks must wait in line, which will delay the execution of the entire program

If the queue is due to a large amount of computation, the CPU is too busy, but many times the CPU is idle because IO operations (input and output) are slow (such as Ajax operations reading data from the network) and have to wait for the results to come out before executing. The designers of the JavaScript language realized that the CPU could simply suspend the pending task and run the next one, regardless of the IO operation. Wait until the IO operation returns the result, then go back and continue the pending task. This mechanism is the “Event Loop” mechanism used in JavaScript.

While single-threading is a major limitation for JavaScript, it gives it advantages that other languages don’t have. When used well, JavaScript programs don’t clog, which is why Node can handle heavy traffic with very few resources.

In order to make use of the computing power of multi-core CPU, HTML5 proposes the Web Worker standard, which allows JavaScript scripts to create multiple threads, but the child threads are completely controlled by the main thread and cannot operate DOM. So, this new standard doesn’t change the single-threaded nature of JavaScript.

synchronous

Synchronous behavior corresponds to sequential execution of processor instructions in memory. Each instruction is executed in the exact order in which it appears, and each instruction is immediately followed by information stored locally in the system (such as registers or system memory). Such an execution process makes it easy to analyze the state of the program as it executes anywhere in the code (such as the value of a variable).

An example of a synchronous operation could be performing a simple mathematical calculation:

let xhs = 3

xhs = xhs + 4
Copy the code

At each step of program execution, the state of the program can be inferred. This is because subsequent instructions are always executed after previous instructions have completed. Once the last specification is executed, the values stored in XHS are immediately available.

First, the operating system allocates space on the stack to store a floating point value, performs a mathematical calculation on the value, and writes the result back into the previously allocated memory. All of these instructions are executed sequentially in a single thread. At the level of low-level instructions, there are sufficient tools to determine system state.

asynchronous

Asynchronous behavior is similar to a system interrupt, where an entity outside the current process can trigger code execution. Asynchronous operations are often necessary because it is usually not feasible to force a process to wait for a long operation (synchronous operations must wait). Long waits can occur if the code accesses resources with high latency, such as sending a request to a remote server and waiting for a response.

An example of an asynchronous operation could be a simple mathematical calculation performed in a timed callback:

let xhs = 3

setTimeout(() = > (xhs = xhs + 4), 1000)
Copy the code

This program ends up doing the same thing the synchronous code does, adding two numbers together, but this time the executing thread doesn’t know when the XHS value will change, because it depends on when the callback is unqueued from the message queue and executed.

Asynchronous code is not easy to infer. Although the low-level code corresponding to this example is ultimately no different from the previous example, the second instruction block (add and assign) is triggered by the system timer, which generates an enqueue interrupt. Exactly when this interrupt will be triggered is a black box to the JavaScript runtime and therefore virtually impossible to predict (although you can guarantee that it will happen after the current thread’s synchronized code executes, otherwise the callback will not have a chance to be executed out of the column). In any case, there is little way to know when the system state is changing after scheduled callbacks.

In order for subsequent code to use XHS, functions that execute asynchronously need to notify other code when the XHS value is updated. If the program does not need this value, then just go ahead and do not wait for the result.

Task queues and event loops

In JavaScript runtime, in addition to a running main thread, the engine also provides a task queue of various asynchronous tasks that need to be handled by the current program. (In fact, there are multiple task queues depending on the type of asynchronous task. For ease of understanding, assume that there is only one queue.

First, the main thread performs all synchronization tasks. When all the synchronous tasks are completed, the asynchronous tasks in the task queue are viewed. If the condition is met, the asynchronous task is re-entered into the main thread to begin execution, and it becomes a synchronous task. The next asynchronous task enters the main thread to start execution. Once the task queue is empty, the program ends execution.

Asynchronous tasks are usually written as callback functions. Once the asynchronous task re-enters the main thread, the corresponding callback function is executed. If an asynchronous task does not have a callback function, it will not enter the task queue, that is, it will not re-enter the main thread, because there is no callback function to specify the next operation.

How does a JavaScript engine know if an asynchronous task has a result, if it can get into the main thread? The answer is that the engine is constantly checking, over and over again, to see if pending asynchronous tasks are ready to enter the main thread once the synchronous task has finished. This Loop checking mechanism is called an Event Loop.

Wikipedia defines it as: An event loop is a programming construct that waits for and dispatches events or messages in a program.

Mode of asynchronous operation

The callback function

Callbacks are the most basic method for asynchronous operations.

Here are the two functions f1 and f2, with the intent that F2 must wait until F1 completes.

function f1() {
  // ...
}

function f2() {
  // ...
}

f1()
f2()
Copy the code

The problem with the above code is that if F1 is an asynchronous operation, F2 will execute immediately and not wait until F1 finishes.

At this point, you can consider rewriting f1, writing f2 as a callback function of F1.

function f1(callback) {
  // ...
  callback()
}

function f2() {
  // ...
}

f1(f2)
Copy the code

The advantages of callback functions are that they are simple, easy to understand, and easy to implement. The disadvantages are that they are not easy to read and maintain. They are also highly coupled between various parts, which makes the program structure confusing and the flow difficult to track (especially when multiple callback functions are nested).

Flow control for asynchronous operations

If there are multiple asynchronous operations, there is a problem of process control: how to determine the order in which the asynchronous operations are executed and how to ensure that the order is followed.

function async(arg, callback) {
  console.log('Parameter is' + arg + ', return result in 1 second ')
  setTimeout(function () {
    callback(arg * 2)},1000)}Copy the code

The async function of the above code is an asynchronous task that takes a very long time to complete and then calls the callback function.

If there are six such asynchronous tasks, all of them need to be completed before the final function can be executed. How should I arrange the operation process?

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)
        })
      })
    })
  })
})
// The argument is 1 and the result will be returned after 1 second
// The argument is 2 and the result will be returned after 1 second
// The parameter is 3 and the result will be returned after 1 second
// The argument is 4 and the result will be returned after 1 second
// The argument is 5 and the result will be returned after 1 second
// The argument is 6 and the result will be returned after 1 second
// Finish: 12
Copy the code

The nesting of six callback functions in the above code is cumbersome to write, error-prone, and difficult to maintain.

The serial execution

We can write a process control function to control asynchronous tasks, and when one task is complete, execute another. This is called serial execution.

var items = [1.2.3.4.5.6]
var results = []

function async(arg, callback) {
  console.log('Parameter is' + arg + ', return result in 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 {
    return final(results[results.length - 1])
  }
}

series(items.shift())
Copy the code

In the above code, the series function is a serial function that executes the asynchronous tasks in sequence, and the final function is executed after all the tasks are completed. The Items array stores the parameters of each asynchronous task, and the Results array stores the results of each asynchronous task.

Note that the above script takes six seconds to complete.

Parallel execution

Process control functions can also be executed in parallel, that is, all asynchronous tasks are executed at the same time, and the final function is not executed until all tasks are complete.

var items = [1.2.3.4.5.6]
var results = []

function async(arg, callback) {
  console.log('Parameter is' + arg + ', return result in 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])}})})Copy the code

In the above code, the forEach method initiates six asynchronous tasks at the same time and waits until all of them are complete before executing the final function.

In comparison, the above method takes only a second to complete the script. That is, parallel execution is more efficient and saves time than serial execution, which can only perform one task at a time. However, the problem is that if there are many parallel tasks, it is easy to exhaust system resources and slow down the operation. Hence the third type of process control.

The combination of parallel and serial

The so-called parallel and serial combination is to set a threshold, a maximum of N asynchronous tasks can be executed in parallel at a time, so as to avoid excessive use of system resources.

var items = [1.2.3.4.5.6]
var results = []
var running = 0
var limit = 2

function async(arg, callback) {
  console.log('Parameter is' + arg + ', return result in 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()
Copy the code

In the above code, you can only run a maximum of two asynchronous tasks simultaneously. The running variable records the number of tasks that are currently running. If it is below the threshold value, a new task will be started. If it is 0, all tasks will be completed.

This code takes three seconds to complete the script, between serial and parallel execution. The optimal balance of efficiency and resources can be achieved by adjusting the limit variable.

Creating and clearing timers

JavaScript executes single-threaded in the browser, but allows you to use timers to specify that the code executes after a certain time or at intervals. SetTimeout () is used to specify that some code will be executed after a certain amount of time, and setInterval() is used to specify that some code will be executed at intervals.

The setTimeout() method usually takes two arguments: the code to execute and the time (milliseconds) to wait before executing the callback function. The first argument can be a string containing JavaScript code (similar to the string passed to eval()) or a function.

// Displays a warning box after 1 second

setTimeout(() = > alert('Hello XHS-Rookies! '), 1000)
Copy the code

The second argument is the number of milliseconds to wait, not the exact time to execute the code. JavaScript is single-threaded, so only one piece of code can be executed at a time. To schedule the execution of different code, JavaScript maintains a task queue. The tasks are executed in the order they were added to the queue. The second argument to setTimeout() simply tells the JavaScript engine to add the task to the queue after the specified number of milliseconds. If the queue is empty, the code is executed immediately. If the queue is not empty, the code must wait for the previous task to complete before it can execute.

When setTimeout() is called, a numeric ID representing the timing of the timeout is returned. This timeout ID is a unique identifier for the code scheduled to execute and can be used to cancel the task. To cancel a scheduled task in a wait, call the clearTimeout() method and pass in the timeout ID, as shown in the following example:

// Set the timeout task
let timeoutId = setTimeout(() = > alert('Hello XHS-Rookies! '), 1000)

// Cancel the timeout task
clearTimeout(timeoutId)
Copy the code

You can cancel the timeout task as long as clearTimeout() is called before the specified time expires. Calling clearTimeout() after the task has executed has no effect.

Note that all time-out code (functions) is run in an anonymous function in the global scope, so the value of this in the function always refers to window in non-strict mode and undefined in strict mode. If setTimeout() is provided with an arrow function, this remains in the lexical scope in which it was defined.

SetInterval () is used in a similar way to setTimeout(), except that the specified task is executed at specified intervals until the loop is untimed or the page is unloaded. SetInterval () can also take two parameters: the code to execute (string or function) and the time to wait (milliseconds) to add the next timed code execution task to the queue. Here’s an example:

setInterval(() = > alert('Hello XHS-Rookies! '), 10000)
Copy the code

Note that the key point here is that the second parameter, the interval, refers to the time to wait before adding a new task to the queue. For example, setInterval() is called at 01:00:00 with an interval of 3000 milliseconds. This means that at 01:00:03, the browser adds the task to the execution queue. The browser doesn’t care when the task is executed or how long it takes to execute. So at 01:00:06, it adds another task to the queue. As you can see, setInterval() is a good choice for short, non-blocking callback functions.

The setInterval() method also returns a cyclic timing ID, which can be used to cancel the cyclic timing at some future point. To cancel cyclic timing, call clearInterval() and pass in the timing ID. The ability to cancel timing is more important to setInterval() than setTimeout(). After all, if left unchecked, the scheduled task will continue to execute until the page is unloaded. Here’s a common example:

let xhsNum = 0,
  intervalId = null
let xhsMax = 10

let xhsIncrementNumber = function () {
  xhsNum++
  // If the maximum value is reached, cancel all unexecuted tasks
  if (xhsNum == xhsMax) {
    clearInterval(xhsIntervalId) // Clear the timer
    alert('Done')
  }
}
xhsIntervalId = setInterval(xhsIncrementNumber, 500)
Copy the code

In this example, the variable num is incremented every half second until the limit is reached. The loop timing is cancelled. This pattern can also be implemented using setTimeout(), for example:

let xhsNum = 0
let xhsMax = 10

let xhsIncrementNumber = function () {
  xhsNum++
  // If the maximum value is not reached, set another timeout task
  if (xhsNum < xhsMax) {
    setTimeout(xhsIncrementNumber, 500)}else {
    alert('Done')}}setTimeout(xhsIncrementNumber, 500)
Copy the code

Note that when using setTimeout(), it is not necessary to record the timeout ID, as it will stop automatically when the condition is met, otherwise another timeout task will be automatically set. This pattern is recommended for setting up cyclic tasks. SetIntervale () is rarely used in a production environment in practice because the time interval between the end of one task and the start of the next is not guaranteed and some recurring timed tasks may be skipped because of this. Using setTimeout(), as in the previous example, ensures that this does not happen. In general, it’s best not to use setInterval().

The title self-test

What is the following code output?

console.log('first')
setTimeOut(() = > {
  console.log('second')},1000)
console.log('third')
Copy the code
Answer
// first
// third
// second
Copy the code

When setTimeOut is executed, the contents are placed in an asynchronous queue. So the contents of setTimeOut are printed only after the following third output is executed.

2. Make a 60s timer.

Answer
function XhsTimer() {
  var xhsTime = 60 // Set the countdown time to 60s
  const xhsTimer = setInterval(() = > {
    // Create a timer
    if (xhsTime > 0) {
      // When greater than 0, subtract again and again
      xhsTime--
      console.log(xhsTime) // Output each second
    } else {
      clearInterval(xhsTimer) // Clear the timer
      xhsTime = 60 // Reset the countdown time to 60s}},1000) // 1000 is the set time, 1000 milliseconds is one second
}
XhsTimer()
Copy the code

JavaScript series of synchronous and asynchronous, we end here, thank you for your support to the author! Your attention and praise will be the strongest motivation for us to move forward! Thank you!