JavaScript 事件循环

前言

事件循环是每个 Javascript 开发人员都必须面对的,起初理解起来会有些混乱。事件循环的唯一任务是将队列与调用栈连接起来~。如果调用栈为空,也就是先前调用的函数都返回了值并从调用栈中调出,这时队列中的第一项便可以添加到调用栈中。



# 一. 事件循环

首先,事件循环是什么,为什么我们需要关注它?

我们都知道 Javascript 是单线程的:一次只能运行一个任务。通常,这没什么大问题,但是假设你现在正在运行一个耗时需要 30s 的任务。由于是单线程处理,我们需要等待 30s 才能去做其它事情(默认情况下,Javascript 在浏览器的主线程上运行,因此整个界面将卡住)。我想,现在没有人想访问这样一个反应迟钝的网站了。

幸运的是,浏览器为我们提供了一个 Javascript 引擎本身没有的功能:Web API它包含了 DOM API、setTimeout、HTTP 请求 等等。这可帮助我们创建一些异步的,非阻塞的行为。

当我们调用一个函数时,函数会被添加到调用栈中。调用栈是 JS 引擎的一部分,而不是与游览器相关的。它是一个栈,即满足 FIFO。当一个函数返回一个值时,会从栈中弹出。

respond 函数返回一个 setTimeout 函数。 setTimeout函数是由 Web API 提供的:它让我们可以延迟任务而不会阻塞主线程。我们传递给 setTimeout 函数的回调函数,即箭头函数 () => { return 'Hey'}会被添加到 Web API。同时, setTimeout 函数和 respond 函数从栈中弹出,它们都返回了自己的值。

在 Web API 中,计时器运行了 1000ms (我们传递的第二个参数)。回调不会立即添加到调用栈中,而是会传递到队列中。

在 Web API 中,计时器运行了 1000ms (我们传递的第二个参数)。回调不会立即添加到调用栈中,而是会传递到队列中。

这可能是一个令人困惑的地方: 这并不意味着在 1000ms 后将回调函数添加到调用栈中(从而返回一个值)。而只是简单地在 1000ms 后添加到队列中。而在队列中,函数必须等待,直到轮到它!

现在到了我们要讨论的重点:

事件循环: 事件循环的唯一任务是将队列与调用栈连接起来。如果调用栈为空,也就是先前调用的函数都返回了值并从调用栈中调出,这时队列中的第一项便可以添加到调用栈中。

回调被添加到调用栈中,被调用,并返回一个值,然后从调用栈中弹出。


# 二. 举例

我们再举个例子。运行下面的命令,看看会输出什么内容:

const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");

bar();
foo();
baz();

看出来了么?让我们看看在浏览器中运行这段代码会发生什么。

执行

  • 首先调用 bar , bar 返回一个 setTimeout 函数。
  • 我们传递给 setTimeout 的回调被添加到 Web API, setTimeout 函数从调用栈中弹出
  • 运行计时器,同时调用 foo 并打印 First 。 foo 返回,调用 baz ,回调函数被添加到队列。
  • baz 打印出 Third ,事件循环发现在 baz 返回后,调用栈为空,所以将回调添加到调用栈中。
  • 回调打印出 Second