JavaScript 异步原理

763 阅读3分钟

如果你是 JavaScript 新手,那么你一定听说过 JavaScript 是一门异步编程语言,但我要说其实你是错的,JavaScript 不是异步而是同步的语言,那么为什么每个人都在说它是异步的呢?让我们通过下面的文章来了解一下

什么是同步的 JavaScript?

你写了一段代码,代码都是一行接一行地执行,这就是同步代码。每一步操作都要等待前一个步骤执行完成。我们来通过一个例子理解一下。

img

图1. 一个计算两数之和的同步函数

我们来看一下图1里的代码是如何工作的,代码是按照下面的顺序执行的:

  1. 首先会执行的代码是 getSum=add(2,3)。
  2. 这会调用到第一行里的 add 函数。
  3. a 与 b 的和存储在常量 sum 里。
  4. 然后返回 sum 变量并将结果存储在常量 getSum 里。
  5. 最后打印出 getSum。

在上面我们可以看到 console.log 在等着结果返回。 这是因为 JavaScript 是单线程的,在单线程中的单个时间点只能发生一件事,因此它会等待上一步执行,在此之前一切都会被阻塞。

什么是异步的 JavaScript?

你写了一段代码并且特定的步骤不会等待上一步执行完时,那么它就是异步的。 这种情况会出现在调用使用外部设备获取数据的 API 时,那么在数据到来之前阻塞整个后续代码的执行是没有意义的。 让我们通过下面的例子来理解。

img

图2. 一个会在 3s 后打印 sum 值的异步函数

图2中,你认为上面的代码执行后会输出什么?‘The sum of 2 and 3 is 5’是会先打印然后打印 sum 的值呢,还是会它会在 sum 的值之后打印?我们来看一下代码是如何工作的,代码是按照下面的顺序执行的:

  1. 首先会执行的代码是 getSum=add(2,3)。
  2. 这会调用到第一行里的 add 函数。
  3. a 与 b 的和存储在常量 sum 里。
  4. setTimeout 并不会立即执行,它会发送给浏览器来执行,一旦执行完毕,回调函数就会发送到事件队列(稍后会讨论)里去。
  5. 接着返回 sum 变量并将结果存储在常量 getSum 里。
  6. 最后打印出 getSum。
  7. 现在,当调用栈清空后,回调函数会进入主线程,并在 3 秒后打印“2 和 3 之和为 5”

那么上面发生了什么,为什么“The sum of 2 and 3 is 5”会后打印,即使它在“5”之前调用的。这是因为我们有事件队列和事件循环的概念,只有在调用栈为空后才执行异步调用。

什么是事件队列和事件循环?

异步调用会放入到事件队列或回调队列中,它们会在调用栈清空后执行,也就是在主线程处理完所有操作后,以便这些异步调用不会阻止下一行 javascript 代码的运行。

img

图3. 事件循环和回调队列在 JS 引擎中的工作原理

在图 3 中,一旦主线程中的所有代码执行完毕,这些回调队列就会被发送回主线程,然后执行这些队列中的代码。 在事件循环的帮助下,事件队列被发送回主线程。 事件循环持续运行并检查主线程是否为空,一旦主线程为空,队列就会被压进调用栈里。

总结

如我们在上面读到的,它证明了 JS 是一种同步、阻塞和单线程的语言。 是外部 API 使语言表现出了异步特性,为了实现异步,才有了事件队列和事件循环的概念。 这些东西是由 JavaScript 引擎控制的,而不是 Javascript 本身。 浏览器有 JavaScript 引擎,V8就是最常用的引擎之一。