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事件循环
相关知识进行了简单梳理。