JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。
简单地说,对于 JS 运行中的任务,JS 有一套处理收集,排队,执行的特殊机制,我们把这套处理机制称为事件循环(Event Loop)。
为了更深刻的理解事件循环,我们先了解几个相关概念
单线程我们都知道 JS 是单线程的,什么意思呢?
JS 单线程指的是 javascript 引擎(如V8)在同一时刻只能处理一个任务。
有人或许会问,异步任务 ajax 难道不是可以和 JS 代码同时执行么?
答案是可以的,但是这和 JS 单线程并不冲突,前面说过 javascript 引擎(如V8)在同一时刻只能处理一个任务。但这并不是说浏览器在同一个时刻只能处理一件事情,实际上 ajax 等异步任务不是在 JS 引擎上运行的,ajax 在浏览器处理网络的模块中执行,此时不会影响到 JS 引擎的任务处理。
需要强调的是,同一时刻只能处理一个任务,并不表示此时处理的只有一个函数,我们可以有多个正在处理的函数,同时拥有多个执行环境,后面会有分析。
执行环境关于执行环境可以参考我之前的博客浅谈JS执行环境及作用域。
执行环境是 JS 代码语句执行的环境,包括全局执行环境和函数执行环境。
全局执行环境:全局环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样,在web中,全局执行环境被认为是window对象。
函数执行环境:每个函数都有自己的执行环境。
当一个任务执行时,相应的会对应一个动态变化的执行环境栈,这个执行环境栈包括了不同的执行环境,是一个后进先出的结构。
以下面代码为例,我们看看执行环境栈的动态变化
function Fn1() { var a = 1; function Fn2() { var b = 2; } Fn2(); // 当程序执行到此时 } Fn1(); 变量对象关于变量对象可以参考我之前的博客浅谈JS执行环境及作用域
每个执行环境都有一个变量对象与之关联(一一对应),变量对象包含了执行环境中定义的所有变量及函数。(在此处可以思考下为什么我们提倡尽量少创建全局变量,答案就是因为全局环境对应的变量对象一直会存在内存中。)
事件循环机制我们先看看 MDN 上的一张图片
上面这张图很好地展示了 JS 中的事件循环机制,我们可以看到图中主要包括三个部分,Stack,Heap,Queue,下面逐个分析。
Stack 表示计算机的栈结构,此处 Stack 区域表示的是当前 JS 线程正在处理的任务(一个任务)。结合执行环境部分,我们其实可以把这些 Frame 的组合当作当前的执行环境栈。一个 Frame 表示一个执行环境。这里也解释了一个任务下其实可以包含多个相关函数。
Heap 一般用来表示计算机内存,此处 Heap 表示当前任务下相关的数据,结合上面变量对象的概念,我们可以把其中的 Object 标签当作是执行环境对应的变量对象。一个执行环境推入执行环境栈时,创建一个变量对象放入 Heap 区域,当执行环境栈推出这个执行环境时,其相对应的变量对象在 Heap 移除并销毁。如果再深入点,我们可以发现,里面 Object 的集合其实就是我们的作用域链的变量对象集合。
Queue 在计算机中表示队列,是一种先进先出的数据结构。此处 Queue 区表示了当前正在排队的任务集合,我们称之为任务队列。一个 Message 表示一个待执行任务,它们是按顺序排队的。
分析完图片的不同区域,我们就可以很轻松地分析出这张图中阐释的事件环境机制了
JS 线程在同一时间只执行一个任务,期间可能创建多个函数执行环境,对应 Frame。
在执行任务的时候,随时执行环境栈的动态变化,相对应的变量对象不断创建销毁,对应 Object。
异步任务 ajax I/O 等得到结果时,会将其回调作为一个任务添加到任务队列,排队等待执行。
当 JS 线程中的任务执行完毕,会读取任务队列 Queue,并将队列中的第一个任务添加到 JS 线程中并执行。
循环 3 4 步,异步任务完成后不断地往任务队列中添加任务,线程空闲时从任务列表读取任务并执行。
事件循环下的宏任务与微任务通常我们把异步任务分为宏任务与微任务,它们的区分在于: