4
头图
"Code tailor" provides technical-related information and a series of basic articles for front-end developers. Follow the "Novices of Xiaoheshan" public account on WeChat to get the latest articles in time.

Preface

Before starting to learn, what we want to tell you is that this article is a summary of the asynchronous operation JavaScript language knowledge. If you have mastered the following knowledge items, you can skip this section and go directly to the topic exercise

  • Single thread
  • Synchronization concept
  • Asynchronous concept
  • Asynchronous operation mode
  • Process control of asynchronous operation
  • Timer creation and clearing

If you have forgotten some parts, 👇🏻 is ready for you!

Summary

Single thread

Single thread means that JavaScript only one thread. In other words, JavaScript can only perform one task at the same time, and other tasks must be queued up later.

JavaScript reason why 060b5a9c8bfbc8 uses single-threaded instead of multi-threaded is related to history. JavaScript has been single-threaded since its inception. The reason is that you don't want to make the browser too complicated, because multiple threads need to share resources and may modify each other's running results. This is too complicated for a web scripting language.

single thread benefits :

  • Relatively simple to implement
  • The execution environment is relatively simple

single-threaded disadvantages :

  • The disadvantage is that as long as there is a task that takes a long time, the following tasks must be queued and waited, which will delay the execution of the entire program.

If the queue is due to a large amount of calculations, CPU busy to be too busy, but that is not the case, but most of the time CPU is idle because the IO operation (input and output) is very slow (for example, the Ajax operation reads data from the network) and has to wait After the results come out, execute it again. JavaScript language realized that at this time, CPU can completely ignore the IO operation, suspend the waiting tasks, and run the tasks that are ranked later. Wait until the IO operation returns the result, then go back and continue the execution of the suspended task. This mechanism is JavaScript the internal use of "event loop" mechanism ( Event Loop ).

Although the single thread JavaScript , it also has advantages that other languages do not have. If used well, the JavaScript program will not be blocked, which is why Node can use very few resources to cope with high-traffic access.

In order to take advantage of the computing power CPU HTML5 proposed the Web Worker standard, allowing the JavaScript script to create multiple threads, but the sub-threads are completely controlled by the main thread and cannot operate DOM . Therefore, this new standard does not change the nature of single-threaded JavaScript

Synchronize

Synchronous behavior corresponds to processor instructions executed sequentially in memory. Each instruction will be executed strictly in the order in which they appear, and the information stored in the system local (such as registers or system memory) can be obtained immediately after each instruction is executed. Such an execution flow is easy to analyze the state of the program when it is executed to any position in the code (such as the value of a variable).

An example of a synchronous operation can be to perform a simple mathematical calculation:

let xhs = 3

xhs = xhs + 4

At each step of the program execution, the state of the program can be inferred. This is because the following instructions are always executed after the previous instructions are completed. When the last designation is completed, xhs can be used immediately.

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

asynchronous

Asynchronous behavior is similar to system interrupts, that is, entities outside the current process can trigger code execution. Asynchronous operations are often necessary, because forcing the process to wait for a long operation is usually not feasible (synchronous operations must wait). If the code wants to access some high-latency resources, such as sending a request to a remote server and waiting for a response, there will be a long wait.

An example of asynchronous operation can be to perform a simple mathematical calculation in a timed callback:

let xhs = 3

setTimeout(() => (xhs = xhs + 4), 1000)

This program is ultimately the same as the task executed by the synchronization code. It is to add two numbers together, but this time the thread of execution does not know xhs value of 060b5a9c8bfdc8 will change, because it depends on when the callback is dequeued from the message queue. carried out.

Asynchronous code is not easy to infer. Although the low-level code corresponding to this example is ultimately the same as the previous example, the second instruction block (add operation and assignment operation) is triggered by the system timer, which generates an interrupt for enqueue execution. When exactly will this interrupt be triggered, this JavaScript , so it is actually unpredictable (although it can be guaranteed that this will happen after the synchronization code of the current thread is executed, otherwise the callback will not have a chance to be executed out of queue) . In any case, there is basically no way to know when the system state changes after the callback is scheduled.

In order for subsequent codes to use xhs , the asynchronously executed function needs to notify other codes after xhs If the program does not need this value, then just continue execution without waiting for the result.

Task queue and event loop

When JavaScript is running, in addition to a running main thread, the engine also provides a task queue ( task queue ), which contains various asynchronous tasks that need to be processed by the current program. (Actually, there are multiple task queues depending on the type of asynchronous tasks. For ease of understanding, it is assumed that there is only one queue.)

First, the main thread will perform all synchronization tasks. When all the synchronous tasks are executed, they will look at the asynchronous tasks in the task queue. If the conditions are met, the asynchronous task will re-enter the main thread to start execution, and then it will become a synchronous task. After the execution is complete, the next asynchronous task enters the main thread to start execution. Once the task queue is emptied, 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 will be 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 the callback function is not used to specify the next operation.

JavaScript engine know whether the asynchronous task has a result, and can it enter the main thread? The answer is that the engine is constantly checking, over and over again, as long as the synchronization tasks are executed, the engine will check whether the suspended asynchronous tasks can enter the main thread. This kind of loop checking mechanism is called event loop ( Event Loop ).

Wikipedia is defined as: "The event loop is a program structure used to wait and send messages and events (a programming construct that waits for and dispatches events or messages in a program)".

Asynchronous operation mode

Callback

Callback function is the most basic method of asynchronous operation.

The following are two functions f1 and f2 . The programming intention is that f2 must wait until f1 executed before it can be executed.

function f1() {
  // ...
}

function f2() {
  // ...
}

f1()
f2()

The problem with the above code is that if f1 is an asynchronous operation, f2 will be executed immediately, and will not wait until f1 over.

At this time, you can consider rewriting f1 , and write f2 as the callback function of f1

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

function f2() {
  // ...
}

f1(f2)

The advantage of the callback function is that it is simple, easy to understand and implement. The disadvantage is that it is not conducive to the reading and maintenance of the code. The height between the various parts is coupled with ( coupling ), which makes the program structure chaotic and the process difficult to trace (especially multiple callback function embedded Set the situation), and each task can only specify a callback function.

Process control of asynchronous operation

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

function async(arg, callback) {
  console.log('参数为 ' + arg + ' , 1秒后返回结果')
  setTimeout(function () {
    callback(arg * 2)
  }, 1000)
}

async function of the above code is an asynchronous task, which is very time-consuming. Each execution takes 1 seconds to complete, and then the callback function is called.

If there are six such asynchronous tasks, they need to be completed before the final final function can be executed. How should the operation process be arranged?

function final(value) {
  console.log('完成: ', value)
}

async(1, function (value) {
  async(2, function (value) {
    async(3, function (value) {
      async(4, function (value) {
        async(5, function (value) {
          async(6, final)
        })
      })
    })
  })
})
// 参数为 1 , 1秒后返回结果
// 参数为 2 , 1秒后返回结果
// 参数为 3 , 1秒后返回结果
// 参数为 4 , 1秒后返回结果
// 参数为 5 , 1秒后返回结果
// 参数为 6 , 1秒后返回结果
// 完成:  12

In the above code, the nesting of the six callback functions is not only troublesome to write, error-prone, and difficult to maintain.

Serial execution

We can write a flow control function, let it control asynchronous tasks, after one task is completed, then execute another. This is called serial execution.

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

function async(arg, callback) {
  console.log('参数为 ' + arg + ' , 1秒后返回结果')
  setTimeout(function () {
    callback(arg * 2)
  }, 1000)
}

function final(value) {
  console.log('完成: ', 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())

In the above code, the function series is a serial function. It will execute asynchronous tasks in sequence. After all tasks are completed, the final function will be executed. items array holds the parameters of each asynchronous task, and the results array holds the running result of each asynchronous task.

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

Parallel execution

The flow control function can also be executed in parallel, that is, all asynchronous tasks are executed at the same time, and the final function is executed after all the tasks are completed.

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

function async(arg, callback) {
  console.log('参数为 ' + arg + ' , 1秒后返回结果')
  setTimeout(function () {
    callback(arg * 2)
  }, 1000)
}

function final(value) {
  console.log('完成: ', value)
}

items.forEach(function (item) {
  async(item, function (result) {
    results.push(result)
    if (results.length === items.length) {
      final(results[results.length - 1])
    }
  })
})

In the above code, the forEach method will initiate six asynchronous tasks at the same time. final

In comparison, the above writing takes only one second to complete the entire script. This means that parallel execution is more efficient and saves time compared to serial execution which can only execute one task at a time. But the problem is that if there are many parallel tasks, it is easy to exhaust system resources and slow down the running speed. So there is a third method of process control.

Combination of parallel and serial

The so-called combination of parallel and serial is to set a threshold, at most n asynchronous tasks can be executed in parallel each time, so as to avoid excessive occupation 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('参数为 ' + arg + ' , 1秒后返回结果')
  setTimeout(function () {
    callback(arg * 2)
  }, 1000)
}

function final(value) {
  console.log('完成: ', 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()

In the above code, at most two asynchronous tasks can be run at the same time. The variable running records the number of tasks currently running. As long as it is below the threshold, a new task will be started. If it is equal to 0 , it means that all tasks have been executed. At this time, the final function will be executed.

This code takes three seconds to complete the entire script, between serial execution and parallel execution. By adjusting the limit , the best balance of efficiency and resources can be achieved.

Timer creation and clearing

JavaScript is executed in a single thread in the browser, but allows the use of timers to specify that the corresponding code will be executed after a certain time or at regular intervals. setTimeout() used to specify the execution of certain codes after a certain period of time, and setInterval() used to specify the execution of certain codes at regular intervals.

setTimeout() method usually receives two parameters: the code to be executed and the time (in milliseconds) to wait before executing the callback function. The first parameter can be JavaScript (similar to eval() ) or a function.

// 在 1 秒后显示警告框
setTimeout(() => alert('Hello XHS-Rookies!'), 1000)

The second parameter 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. In order to schedule the execution of different codes, JavaScript maintains a task queue. The tasks will be executed in the order in which they are added to the queue. setTimeout() the second parameter just tell JavaScript engine after a specified number of milliseconds to add tasks to this queue. If the queue is empty, the code will be executed immediately. If the queue is not empty, the code must wait for the execution of the previous task to be executed.

When calling setTimeout() , it will return a value ID that represents the timeout schedule. This timeout ID is a unique identifier of the code scheduled for execution and can be used to cancel the task. To cancel the waiting scheduled task, you can call the clearTimeout() method and pass in the timeout ID , as shown in the following example:

// 设置超时任务
let timeoutId = setTimeout(() => alert('Hello XHS-Rookies!'), 1000)// 取消超时任务clearTimeout(timeoutId)

clearTimeout() is called before the specified time arrives, the timeout task can be cancelled. clearTimeout() after the task is executed has no effect.

Note that all code execution timeout (function) runs in an anonymous function in the global scope, so the function of this value in non-strict mode always point window , and is in strict mode undefined . If an arrow function is provided setTimeout() this will remain in the vocabulary scope in which it was defined.

setInterval() of setTimeout() is similar to 060b5a9c8c026d, except that the specified task will be executed every specified time until the cycle timing is cancelled or the page is unloaded. setInterval() can also receive two parameters: the code to be executed (string or function), and the time (in milliseconds) to wait for the next task of executing the timing code to be added to the queue. Below is an example:

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

Note that the key point here is that the second parameter, the interval time, refers to the time to wait before adding a new task to the queue. For example, the time to setInterval() 01:00:00 , and the interval time is 3000 milliseconds. This means that 01:00:03 , the browser will add the task to the execution queue. The browser does not care when this task is executed or how long it takes to execute. Therefore, at 01:00:06 , it will add another task to the queue. It can be seen that the callback function with short execution time and non-blocking is more suitable for setInterval() .

setInterval() method will also return a loop timing ID , which can be used to cancel the loop timing at a certain point in the future. To cancel the loop timing, you can call clearInterval() and pass in the timing ID . Compared to setTimeout() , the ability to cancel timing is more important setInterval() After all, if you leave it alone, the timed task will be executed until the page is unloaded. Here is a common example:

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

let xhsIncrementNumber = function () {
  xhsNum++
  // 如果达到最大值,则取消所有未执行的任务
  if (xhsNum == xhsMax) {
    clearInterval(xhsIntervalId) // 清除定时器
    alert('Done')
  }
}
xhsIntervalId = setInterval(xhsIncrementNumber, 500)

In this example, the variable num will increase every half a second until it reaches the maximum limit. At this time, the cycle timing will be cancelled. This mode can also be setTimeout() , such as:

let xhsNum = 0
let xhsMax = 10

let xhsIncrementNumber = function () {
  xhsNum++
  // 如果还没有达到最大值,再设置一个超时任务
  if (xhsNum < xhsMax) {
    setTimeout(xhsIncrementNumber, 500)
  } else {
    alert('Done')
  }
}
setTimeout(xhsIncrementNumber, 500)
Note that when using setTimeout() , it is not necessary to record the timeout ID , because it will automatically stop when the conditions are met, otherwise it will automatically set another timeout task. This mode is the recommended way to set up recurring tasks. setIntervale() is rarely used in a production environment in practice, because the time interval between the end of a task and the beginning of the next task cannot be guaranteed, and some cyclic timing tasks may be skipped as a result. setTimeout() as in the previous example ensures that this will not happen. Generally speaking, it is best not to use setInterval() .

Self-test

1. What is the output of the following code?

console.log('first')
setTimeOut(() => {
  console.log('second')
}, 1000)
console.log('third')

2. Make a 60s timer.

Problem analysis

One,

// first
// third
// second

setTimeOut causes the contents to enter the asynchronous queue when executed. So will first perform the following third after the output, only the output setTimeOut content.

two,

function XhsTimer() {
  var xhsTime = 60 // 设置倒计时时间 60s
  const xhsTimer = setInterval(() => {
    // 创建定时器
    if (xhsTime > 0) {
      // 大于 0 时,一次次减
      xhsTime--
      console.log(xhsTime) // 输出每一秒
    } else {
      clearInterval(xhsTimer) // 清除定时器
      xhsTime = 60 // 重新设置倒计时时间 60s
    }
  }, 1000) // 1000 为设置的时间,1000毫秒 也就是一秒
}
XhsTimer()

小和山的菜鸟们
377 声望2.1k 粉丝

每日进步的菜鸟,分享前端学习手册,和有心学习前端技术的小伙伴们互相探讨,一同成长。