Home > Article > Web Front-end > An in-depth analysis of JavaScript asynchronous event polling
This article brings you an in-depth analysis of JavsScript asynchronous event polling. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
JavsScript is a single-threaded programming language, which means that it can only process one thing at a time. That is to say, the JavaScript engine can only process one thing in one thread at a time. statement.
While single-threading simplifies programming code because you don't have to worry too much about problems caused by concurrency, it also means that you will be performing long-term operations, such as network requests, while blocking the main thread.
Imagine requesting some data from an API. Depending on the circumstances, the server will take some time to process the request, while blocking the main thread, leaving the webpage in an unresponsive state for a long time.
This is why asynchronous JavaScript was introduced. Using asynchronous JavaScript (such as callback functions, promises, async/await), you can perform network requests for a long time without blocking the main thread:)
Maybe you know how asynchronous JavsScript works, it doesn’t matter. But knowing how it works, a deeper understanding of JavaScript async is helpful.
So without further ado, let’s get started :)
Before we delve into asynchronous JavaScript
, let's first understand how synchronous JavaScript
code is executed in the JavaScript
engine. For 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 must understand the concepts of execution context and call stack (also called execution stack).
Function code is executed in the function execution context, and global code is executed in the global execution context. Each function has its own execution context.
The call stack, as the name suggests, is a stack with a LIFO (last in, first out) structure that is used to store all execution contexts created during code execution.
JavaScript has only one call stack because it is a single-threaded programming language. The call stack has a LIFO structure, which means items can only be added or removed from the top of the stack.
Let’s go back to the above code snippet and try to understand how the code is executed in the JavaScript engine.
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
When this code is executed, a global execution context is created (by main() represents) and pushes it to the top of the call stack. When a call to first() is encountered, it is pushed to the top of the stack.
Next, console.log('Hi there!') is pushed to the top of the stack, and when it completes, it is popped off the stack. After that, we call second(), so the second() function is pushed to the top of the stack.
console.log('Hello there!') is pushed to the top of the stack and popped off the stack when completed. second() function ends, so it's popped off the stack.
console.log("the End") is pushed to the top of the stack and deleted when completed. After that, the first() function completes, so it is removed from the stack.
The program has completed its execution at this point, so the global execution context (main()) is popped off the stack.
Now that we have a basic understanding of the call stack and how synchronous JavaScript works, let’s get back to asynchronous JavaScript.
Let us assume that we are doing image processing or network requests in a synchronous manner. For example:
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();
Doing image processing and network requests takes time, and when the processImage() function is called, it will take some time depending on the size of the image.
After the processImage() function completes, it will be removed from the stack. Then the networkRequest() function is called and pushed onto the stack. Likewise, it also takes some time to complete execution.
Finally, when the networkRequest() function completes, the greeting() function is called because it only contains a console. Log statements and console. Log statements are usually fast, so the greeting() function executes and returns immediately.
Therefore, we must wait for the function (such as processImage() or networkRequest()) to complete. This means these functions block the call stack or the main thread. Therefore, while the above code is executing, we cannot perform any other operations, which is not ideal.
The simplest solution is an asynchronous callback. We use asynchronous callbacks to make the code non-blocking. For example:
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest();
Here I use the setTimeout method to simulate network requests. Remember setTimeout is not part of the JavaScript engine, it is part of the web api (in browsers) and the C/c api (in node.js).
In order to understand how this code is executed, we must understand more concepts, such as event polling and callback queue (or message queue).
事件轮询、web api和消息队列不是JavaScript引擎的一部分,而是浏览器的JavaScript运行时环境或Nodejs JavaScript运行时环境的一部分(对于Nodejs)。在Nodejs中,web api被c/c++ api所替代。
现在让我们回到上面的代码,看看它是如何异步执行的。
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End');
当上述代码在浏览器中加载时,console.log(' Hello World ') 被推送到堆栈中,并在完成后弹出堆栈。接下来,将遇到对 networkRequest() 的调用,因此将它推到堆栈的顶部。
下一个 setTimeout() 函数被调用,因此它被推到堆栈的顶部。setTimeout()有两个参数:
1) 回调和
2) 以毫秒(ms)为单位的时间。
setTimeout() 方法在web api环境中启动一个2s的计时器。此时,setTimeout()已经完成,并从堆栈中弹出。cosole.log(“the end”) 被推送到堆栈中,在完成后执行并从堆栈中删除。
同时,计时器已经过期,现在回调被推送到消息队列。但是回调不会立即执行,这就是事件轮询开始的地方。
事件轮询的工作是监听调用堆栈,并确定调用堆栈是否为空。如果调用堆栈是空的,它将检查消息队列,看看是否有任何挂起的回调等待执行。
在这种情况下,消息队列包含一个回调,此时调用堆栈为空。因此,事件轮询将回调推到堆栈的顶部。
然后是 console.log(“Async Code”) 被推送到堆栈顶部,执行并从堆栈中弹出。此时,回调已经完成,因此从堆栈中删除它,程序最终完成。
消息队列还包含来自DOM事件(如单击事件和键盘事件)的回调。例如:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); });
对于DOM事件,事件侦听器位于web api环境中,等待某个事件(在本例中单击event)发生,当该事件发生时,回调函数被放置在等待执行的消息队列中。
同样,事件轮询检查调用堆栈是否为空,并在调用堆栈为空并执行回调时将事件回调推送到堆栈。
我们还可以使用setTimeout
来延迟函数的执行,直到堆栈清空为止。例如
const bar = () => { console.log('bar'); } const baz = () => { console.log('baz'); } const foo = () => { console.log('foo'); setTimeout(bar, 0); baz(); } foo();
打印结果:
foo baz bar
当这段代码运行时,第一个函数foo()被调用,在foo内部我们调用console.log('foo'),然后setTimeout()被调用,bar()作为回调函数和时0秒计时器。
现在,如果我们没有使用 setTimeout, bar() 函数将立即执行,但是使用 setTimeout 和0秒计时器,将bar的执行延迟到堆栈为空的时候。
0秒后,bar()回调被放入等待执行的消息队列中。但是它只会在堆栈完全空的时候执行,也就是在baz和foo函数完成之后。
我们已经了解了异步回调和DOM事件是如何执行的,它们使用消息队列存储等待执行所有回调。
ES6引入了任务队列的概念,任务队列是 JavaScript 中的 promise 所使用的。消息队列和任务队列的区别在于,任务队列的优先级高于消息队列,这意味着任务队列中的promise 作业将在消息队列中的回调之前执行,例如:
const bar = () => { console.log('bar'); }; const baz = () => { console.log('baz'); }; const foo = () => { console.log('foo'); setTimeout(bar, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); baz(); }; foo();
打印结果:
foo baz Promised resolved bar
我们可以看到 promise 在 setTimeout 之前执行,因为 promise 响应存储在任务队列中,任务队列的优先级高于消息队列。
因此,我们了解了异步 JavaScript 是如何工作的,以及调用堆栈、事件循环、消息队列和任务队列等概念,这些概念共同构成了 JavaScript 运行时环境。虽然成为一名出色的JavaScript开发人员并不需要学习所有这些概念,但是了解这些概念是有帮助的:)
The above is the detailed content of An in-depth analysis of JavaScript asynchronous event polling. For more information, please follow other related articles on the PHP Chinese website!