JavaScript的事件循环

对于任何一门编程语言来说,大都是通过自身所规定的语法和词法来表达一定的语义,从而来操作运行时的数据结构执行过程

本文对JavaScript的执行过程进行了简单概述,并对这个过程中所涉及到的事件循环等知识点进行了梳理和索引。

Overview

在继续讨论之前,我们需要对JavaScript代码的执行过程和相关概念有一个整体上的感知。 我们都知道,考虑到与浏览器的交互,JavaScript在设计之初就采用了单线程 + 回调队列的执行方式,因此不存在多线程并发的模型。

当宿主环境(浏览器,Node)加载到一段JS脚本后, 会发起相应的执行任务,将代码交给交给JavaScript Engine执行。

JavaScript Engine

在运行过程中,JavaScript Engine常驻内存中,负责JS代码的解析和执行,主要包括以下两个部分:

  • 执行栈: 执行栈的每一帧用于存放函数的执行上下文环境,每一个栈元素对应一个函数;
  • 内存堆: 用于存放程序运行过程中为对象分配的空间

有关JavaScript执行上下文的相关细节请参见「The Execution Context of Javascript」

在代码执行过程中,除了宿主环境本身可以向JavaScript Engine分配执行任务之外, JavaScript Engine自身在代码运行过程中也可以向JavaScript Engine发起执行任务。

这种由宿主环境发起的执行任务通常被称为宏观任务(MacroTask), 而由JavaScript Engine本身发起的执行任务则被称为微观任务(MicroTask).

那么,宿主环境如何才能合理地向JavaScript Engine分配任务,并保证这些任务能在单线程中同步且非阻塞地执行呢?

Event Loop

对于宿主环境而言,也并非只有在加载到代码片段后才有机会向JavaScript Engine发起宏观任务, 在代码执行过程中,我们也可以通过一些特定的API(例如setTimeOut等)发起新的宏观任务.

setTimeOut方法

设置一个定时器,该定时器在定时器到期后,宿主环境将所指定的回调函数或者代码作为一个宏观任务放入事件队列的末尾,等待执行。

宿主将宏观任务按照发起顺序组织成一个事件队列, 并通过Event Loop机制来对任务的调度进行管理,方式如下: Event Loop监听JavaScript Engine执行栈中的任务执行进度, 如果当前执行栈中所有函数执行完毕,则从事件队列中取出一个待执行的任务分配给JavaScript Engine执行.

在早期的版本中, JavaScript自身无法异步执行代码, 宿主环境分配给JavaScript Engine的任务按顺序执行。

单线程的情况下,如果我们试图同步地执行一些耗时操作,就会导致线程被阻塞。 举个例子,我们在浏览器中同步地发起网络请求,那么在就会导致在网络请求返回结果之前,浏览器无法进行页面渲染,也无法执行其他操作。

随着JavaScript的发展,引入了Promise等异步回调的机制:

Promise是JavaScript的一种标准化的异步管理方式, 当需要执行耗时操作的时候, 不等待执行结果的返回,而是返回一个Promise给调用方, 方便调用方可以选择合适的时机来处理耗时操作的回调。

JavaScript Engine在执行宏观任务的过程中, 通过Promise等语句可以发起异步操作, 异步操作的回调以微观任务的方式来调用执行。 为了保证异步代码务必在同一个宏观任务中完成,每个宏观任务都维护了一个微观任务队列,这些微观任务需要在同一个宏观任务中完成。

在通常认知里,我们会认为Event Loop队列中相关的调度操作属于宿主环境的职责。然而随着Promise机制的引入,也使Event Loop中事件的调度和操作变得更加细致和直接, 因此ES6标准中也规定了Event Loop该如何工作,并把这部分工作划分到JavaScript Engine的职责范围内。

总结

经过上面的简单介绍,我们对JavaScript事件循环相关知识进行了简单梳理。