Understanding of asynchronous Javascript

Foreword

2925 word article, reading takes about 10 minutes.

In summary: This paper reviews the difference between asynchronous and synchronous tags execute, the concept Javascript event loop, micro task queue task queues.

I am afraid that people never fail never succeeded.

Javascript is single-threaded programming language, a single thread that is at the same time can only do one thing. Put it on the programming language, that is Javascript engine (execute Javascript code in a virtual machine) can only be performed at the same time a statement.

The benefits of single-threaded language is that you just write without worrying about concurrency issues. But it also means you can not go on for a long time to perform some operations, such as in the case of network requests without blocking the main thread.

Under imagine if we request some data from an interface, then the server needs some time to return the data, then it will block the main thread page in an unresponsive state.

This is asynchronous Javascript comes in, we can perform long network requests without blocking the main thread through asynchronous operations (such as callback, promise and async / await).

Although understand all these concepts do not necessarily make you immediately become an excellent Javascript developers, but it will be helpful to understand asynchronous.

Man of few words said, the body start :)

How synchronization code is executed

Before delving into asynchronous Javascript study, we first look at how the synchronization code is executed in the Javascript engine. Look at an example:

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

To understand how the above code is executed in the Javascript engine, we have to go to understand the context of the implementation and execution stack of Javascript .

Execution Context

The so-called execution context is an abstract concept Javascript code execution environment. Any Javascript code is executed in the execution context.

Internal function code will execute in the context of the implementation of the function, the global code will execute in the global execution context, each function has its own execution context.

Execution stack

As the name implies the execution stack is a last in first out (LIFO) stack structure, which is used to create all the stored execution context of the executing code.

Single-threaded reasons, Javascript is only one execution stack, because it is so only add stack-based structure from the top of the stack or delete execution context.

Let us return to the above code, try to understand how the Javascript engine to perform them.

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

Code execution stack above

Code execution stack above

So what happened here then?

When the code is executed, first a global execution context (used here main()shown) is created and then pressed to the top of the execution stack. When the implementation of first()this line of code, its execution context is pressed onto the top of the execution stack.

Then, console.log('Hi there!');the function execution context of the execution stack is pressed onto the top, after the execution of the execution context is popped from the execution stack. Then calls second()the function, the function execution context is pressed into the top of the execution stack.

Then perform console.log('Hello there!');the corresponding function execution context is pushed on the execution stack is popped execution ends, and second()the end of the function execution, execution context is ejected.

console.log(‘The End’)Execution, the function execution context is pushed on the execution stack, execution ends is ejected, when first()the function execution ends, the corresponding execution context is ejected.

Throughout the program ended, the global execution context (main ()) is ejected.

How asynchronous code is executed

Now that we have a basic understanding of the implementation of the synchronization code, let's look at how asynchronous code is executed:

Clog

Suppose we use a synchronous manner to initiate a request or a general picture of network requests, examples are as follows:

const processImage = (image) => {
  /**
  * doing some operations on image
  **/
  console.log('Image processed');
}
const networkRequest = (url) => {
  /**
  * requesting network resource
  **/
  return someData;
}
const greeting = () => {
  console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();

Picture request or request a network takes time, so when we call processImage()time, the time it takes depends on the size of the picture.

When the processImage()end of the function are performed in response to execution context is popped from the stack, and then calls networkRequest()the function, the corresponding execution context is pushed on the execution stack, the same function may take some time to end.

networkRequest()函数执行结束,调用greeting(),然后里面只有一行console.log('Hello World'),``console.log()函数通常执行会很快,因此greeting()`会很快执行完然后返回结果。

可以发现,我们必须等函数(比如processImage,networkRequest函数)执行结束才能调用下一个函数。这意味着这些函数调用的时候会阻塞主线程,造成主线程不能执行其他代码,这是我们所不希望的。

所以怎么解决这个问题呢?

最简单的解决办法就是使用异步的回调函数,有了异步的回调函数就不会阻塞主线程,看例子:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();

这里我们使用了setTimeout方法去模拟网络请求函数。

请注意setTimeout不是Javascript引擎提供的,而是web API(浏览器中)和C/C++ API(nodejs中)的一部分。

Javascript Runtime Environment Overview

Javascript运行环境概述

事件循环Web API消息队列/任务队列并不是Javascript引擎的一部分而是浏览器的Javascript运行环境或是Nodejs的Javascript运行环境的一部分,在Nodejs中,Web API被C/C++ API替代。

回到上面的代码,看看异步的代码是如何执行的:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');

Event Loop

事件循环

代码开始执行,console.log(‘Hello World’)函数的执行上下文首先被压入执行栈,执行结束后被弹出,然后调用networkRequest(),对应的函数执行上下文被压入执行栈。

紧接着 setTimeout() 函数被调用,对应的函数执行上下文被压入执行栈。

setTimeout有两个参数:1. 回调函数;2. 时间(以毫秒ms为单位);3. 附加参数(会被传到回调函数里面)

setTimeout() 函数会在web API运行环境中进行一个2s的倒计时,这个时候 setTimeout() 函数就已经执行完了,执行上下文从执行栈中弹出。再然后console.log('The End')函数被执行,进入执行栈,结束后弹出执行栈。

这时候倒计时到期,setTimeout()的回调函数被推到消息队列中,但回调函数不会立即执行,这是事件循环开始的地方。

事件循环

事件循环的工作就是去查看执行栈,确定执行栈是否为空,如果执行栈为空,那么就去检查消息队列,看看消息队列中是否有待执行的回调函数。它按照类似如下的方式来被实现:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

在这里,执行栈已经为空,消息队列包含一个setTimeout函数的回调函数,因此事件循环把回调函数的执行上下文压入执行栈的顶端。

然后console.log(‘Async Code’)函数的执行上下文被压入执行栈,结束后从执行栈弹出。这时候回调函数执行结束,对应的执行上下文也从执行栈中弹出。

DOM事件

**消息队列(也叫任务队列)**中也会包含来自DOM事件(比如点击事件,键盘事件等),看例子:

document.querySelector('.btn').addEventListener('click',(event) => {
  console.log('Button Clicked');
});

对于DOM事件来说,web API中会有一个事件侦听器坚挺某个事件被触发(在这里是click事件),当某个事件被触发时,就会把相应的回调函数放入消息队列中执行。

事件循环再次检查执行栈,如果执行栈为空,就把事件的回调函数推入执行栈。

我们已经了解了异步回调和事件回调是如何执行的,这些回调函数被存储在消息队列中等待被执行。

ES6任务队列和微任务队列

ES6中为promise函数引入了微任务队列(也叫作业队列)的概念。微任务队列消息队列的区别就是优先级上的区别,微任务队列的优先级要高于消息队列。也就是说在微任务队列promise回调函数会比在消息队列中的回调函数更先执行。

比如:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
  resolve('Promise resolved');
}).then(res => console.log(res))
  .catch(err => console.log(err));
console.log('Script End');

输出:

Script start
Script End
Promise resolved
setTimeout

可以看到promise是在setTimeout之前执行的,因为promise的response被存储在微任务队列中,有比消息队列更高的优先级。

再看另一个例子,有两个promise函数,两个setTimeout函数:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout 1');
}, 0);
setTimeout(() => {
  console.log('setTimeout 2');
}, 0);
new Promise((resolve, reject) => {
  resolve('Promise 1 resolved');
}).then(res => console.log(res))
  .catch(err => console.log(err));
new Promise((resolve, reject) => {
  resolve('Promise 2 resolved');
}).then(res => console.log(res))
  .catch(err => console.log(err));
console.log('Script End');

输出:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2

可以看到两个promise的回调函数都在setTimeout的回调函数之前运行,因为相比消息队列事件循环会优先处理微任务队列中的回调函数。

当事件循环处理微任务队列中的回调函数的时候另一个promise被resolved了,然后这个promise的回调函数会被添加到微任务队列中。并且它会被优先执行,无论消息队列中的回调函数的执行会花费多长时间,都要排队。

比如:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
  resolve('Promise 1 resolved');
}).then(res => console.log(res));

new Promise((resolve, reject) => {
  resolve('Promise 2 resolved');
}).then(res => {
  console.log(res);
  return new Promise((resolve, reject) => {
    resolve('Promise 3 resolved');
  })
}).then(res => console.log(res));
console.log('Script End');

打印:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout

Thus all the micro-task queue callback function will be in the message queue to be executed before the callback function. In other words, the event loop will first clear the micro-task queue callback function to execute the will of the message queue callback function.

in conclusion

We understand the Javascript code that is how synchronous and asynchronous execution, as well as some other concepts (including the execution stack, the event loop, micro task queues, message queues etc.).

the above.


Limited capacity, the level of general, welcomed the errata, be grateful.

Subscribe more articles may be concerned about the public number "front-end Advanced Learning ', replies," 666 ", get a package of front-end technology books

Advanced front-end learning

Published 43 original articles · won praise 84 · views 90000 +

Guess you like

Origin blog.csdn.net/Jizhen_Tan/article/details/104441793