JavaScript事件循环

事件循环是了解JavaScript的最重要方面之一。这篇文章简单地解释了它

介绍

事件循环是了解JavaScript的最重要方面之一。

我已经使用JavaScript编程了多年,但从未充分地了解事物是如何运作的。完全不了解此概念是完全可以的,但是像往常一样,了解它的工作原理会有所帮助,并且此时您可能会有点好奇。

这篇文章旨在解释JavaScript如何与单个线程一起工作以及如何处理异步函数的内部细节。

您的JavaScript代码运行单线程。一次只发生一件事。

这个限制实际上非常有用,因为它大大简化了您的编程方式,而无需担心并发问题。

您只需要注意如何编写代码,并避免任何可能阻塞线程的事情,例如同步网络调用或无限循环

通常,在大多数浏览器中,每个浏览器选项卡都有一个事件循环,以使每个进程都隔离开,并避免出现无限循环或繁重的处理过程的网页来阻塞您的整个浏览器。

该环境管理多个并发事件循环,以处理例如API调用。网络工作者也可以在自己的事件循环中运行。

您主要需要担心的是您的代码将在单个事件循环上运行,并且在编写代码时要牢记这一点,以避免阻塞它。

阻止事件循环

任何花费很长时间才能将控制权返回到事件循环的JavaScript代码,都将阻止页面中任何JavaScript代码的执行,甚至阻止UI线程,并且用户无法单击浏览,滚动页面等。

JavaScript中几乎所有的I / O原语都是非阻塞的。网络请求,Node.js文件系统操作,等等。被阻塞是一个例外,这就是为什么JavaScript如此多地基于回调,最近才基于诺言异步/等待

调用堆栈

调用堆栈是一个LIFO队列(后进先出)。

事件循环不断检查调用堆栈看看是否有任何需要运行的功能。

这样做时,它将找到的所有函数调用添加到调用堆栈中,并按顺序执行每个函数。

您知道在调试器或浏览器控制台中可能熟悉的错误堆栈跟踪吗?浏览器在调用堆栈中查找函数名称,以通知您哪个函数发起了当前调用:

Exception call stack

一个简单的事件循环说明

让我们举个例子:

我用foobarbaz作为随机名称。输入任何名称以替换它们

const bar = () => console.log('bar')

const baz = () => console.log(‘baz’)

const foo = () => { console.log(‘foo’) bar() baz() }

foo()

此代码打印

foo
bar
baz

as expected.

When this code runs, first foo() is called. Inside foo() we first call bar(), then we call baz().

At this point the call stack looks like this:

Call stack first example

The event loop on every iteration looks if there’s something in the call stack, and executes it:

Execution order first example

until the call stack is empty.

Queuing function execution

The above example looks normal, there’s nothing special about it: JavaScript finds things to execute, runs them in order.

Let’s see how to defer a function until the stack is clear.

The use case of setTimeout(() => {}), 0) is to call a function, but execute it once every other function in the code has executed.

Take this example:

const bar = () => console.log('bar')

const baz = () => console.log(‘baz’)

const foo = () => { console.log(‘foo’) setTimeout(bar, 0) baz() }

foo()

This code prints, maybe surprisingly:

foo
baz
bar

When this code runs, first foo() is called. Inside foo() we first call setTimeout, passing bar as an argument, and we instruct it to run immediately as fast as it can, passing 0 as the timer. Then we call baz().

At this point the call stack looks like this:

Call stack second example

Here is the execution order for all the functions in our program:

Execution order second example

Why is this happening?

The Message Queue

When setTimeout() is called, the Browser or Node.js start the timer. Once the timer expires, in this case immediately as we put 0 as the timeout, the callback function is put in the Message Queue.

The Message Queue is also where user-initiated events like click or keyboard events, or fetch responses are queued before your code has the opportunity to react to them. Or also DOM events like onLoad.

The loop gives priority to the call stack, and it first processes everything it finds in the call stack, and once there’s nothing in there, it goes to pick up things in the message queue.

We don’t have to wait for functions like setTimeout, fetch or other things to do their own work, because they are provided by the browser, and they live on their own threads. For example, if you set the setTimeout timeout to 2 seconds, you don’t have to wait 2 seconds - the wait happens elsewhere.

ES6 Job Queue

ECMAScript 2015 introduced the concept of the Job Queue, which is used by Promises (also introduced in ES6/ES2015). It’s a way to execute the result of an async function as soon as possible, rather than being put at the end of the call stack.

Promises that resolve before the current function ends will be executed right after the current function.

I find nice the analogy of a rollercoaster ride at an amusement park: the message queue puts you at the back of the queue, behind all the other people, where you will have to wait for your turn, while the job queue is the fastpass ticket that lets you take another ride right after you finished the previous one.

Example:

const bar = () => console.log('bar')

const baz = () => console.log(‘baz’)

const foo = () => { console.log(‘foo’) setTimeout(bar, 0) new Promise((resolve, reject) => resolve(‘should be right after baz, before bar’) ).then(resolve => console.log(resolve)) baz() }

foo()

This prints

foo
baz
should be right after baz, before bar
bar

That’s a big difference between Promises (and Async/await, which is built on promises) and plain old asynchronous functions through setTimeout() or other platform APIs.


More js tutorials: