JavaScript 事件循环机制

前端开发的童鞋应该都知道,JavaScript 是一门单线程的脚本语言。这就意味着 JavaScript 代码在执行的时候,只有一个主线程来执行所有的任务,同一个时间只能做同一件事情。

那么为什么 JavaScript 不设计成多线程的语言呢?

这是由其执行的环境是浏览器环境所决定的。试想一下如果 JavaScript 是多线程语言的话,那么当两个线程同时对 Dom 节点进行操作的时候,则可能会出现有歧义的问题,例如一个线程操作的是在一个 Dom 节点中添加内容,另一个线程操作的是删除该 Dom 节点,那么应该以哪个线程为准呢?所以 JavaScript 作为浏览器的脚本语言,其设计只能是单线程的。

需要注意的是,Html5 提出了 Web Worker,允许创建多个在后台运行的子线程来执行 JavaScript 脚本。但是由于子线程完全受主线程控制,而且不能够干扰用户界面(即不能操作 Dom),所以这并没有改变 JavaScript 单线程的本质。

上面讲到,JavaScript 是一门单线程的脚本语言。所谓单线程,就是指所有的任务都需要排队一个个执行,只有前一个任务执行完了才可以执行后一个任务。这就造成了一个问题,如果前一个任务耗时过长,则会阻塞下一个任务的执行,在页面上用户的感知便会是浏览器卡死的现象。

而由于在大部分的情况中,造成任务耗时过长不是任务本身计算量大而导致 CPU 处理不过来,而是因为该任务需要与 IO 设备交互而导致的耗时过长,但这时 CPU 却是处于闲置状态的。所以为了解决这个问题,便有了本章节的 JavaScript(也可以说是浏览器的)事件循环(Event Loop)机制

在 JavaScript 事件循环机制中,使用到了三种数据对象,分别是栈(Stack)、堆(Heap)和队列(Queue)。

  • 栈:一种后进先出(LIFO)的数据结构。可以理解为取乒乓球时的场景,后面放进去的乒乓球反而是最先取出来的。
  • 堆:一种树状的的数据结构。可以理解为在图书馆中取书的场景,可以通过图书索引的方式直接找到需要的书。
  • 队列:一种先进先出(FIFO)的数据结构。即我们平时排队的场景,先排的人总是先出队列。

在 JavaScript 事件循环机制中,使用的栈数据结构便是执行上下文栈,每当有函数被调用时,便会创建相对应的执行上下文并将其入栈;使用到堆数据结构主要是为了表示一个大部分非结构化的内存区域存放对象;使用到的队列数据结构便是任务队列,主要用于存放异步任务。

栈、堆、队列可视化表示
执行上下文栈

在 JavaScript 代码运行过程中,会进入到不同的执行环境中,一开始执行时最先进入到全局环境,此时全局上下文首先被创建并入栈,之后当调用函数时则进入相应的函数环境,此时相应函数上下文被创建并入栈,当处于栈顶的执行上下文代码执行完毕后,则会将其出栈。这里的栈便是执行上下文栈。

举个例子~

function fn2() {
    console.log('fn2')
}
function fn1() {
    console.log('fn1')
    fn2();
}

fn1();

上述代码中的执行上下文栈变化行为如下图

执行上下文栈 ECStack
任务队列

在 JavaScript 事件循环机制中,存在多种任务队列,其分为宏任务(macro-task)微任务(micor-task)两种。

  • 宏任务包括:setTimeoutsetIntervalI/OUI rendering
  • 微任务包括:PromiseObject.observe(已废弃)MutationObserver(html5新特性)

上述所描述的 setTimeout、Promise 等都是指一种任务源,其对应一种任务队列,真正放入任务队列中的,是任务源指定的异步任务。在代码执行过程中,遇到上述任务源时,会将该任务源指定的异步任务放入不同的任务队列中。

不同的任务源对应的任务队列其执行顺序优先级是不同的,上述宏任务和微任务的先后顺序代表了其任务队列执行顺序的优先级。

即在宏任务队列中,各个队列的优先级为
setTimeout > setInterval > I/O
在微任务队列中,各个队列的优先级为
Promise > Object.observe > MutationObserver

对于 UI rendering 来说,浏览器会在每次清空微任务队列会根据实际情况触发,这里不做详细赘述。

事件循环机制流程
  1. 主线程执行 JavaScript 整体代码,形成执行上下文栈,当遇到各种任务源时将其所指定的异步任务挂起,接受到响应结果后将异步任务放入对应的任务队列中,直到执行上下文栈只剩全局上下文;
  2. 将微任务队列中的所有任务队列按优先级、单个任务队列的异步任务按先进先出(FIFO)的方式入栈并执行,直到清空所有的微任务队列;
  3. 将宏任务队列中优先级最高的任务队列中的异步任务按先进先出(FIFO)的方式入栈并执行;
  4. 重复第 2 3 步骤,直到清空所有的宏任务队列和微任务队列,全局上下文出栈。

简单来说,事件循环机制的流程就是,主线程执行 JavaScript 整体代码后将遇到的各个任务源所指定的任务分发到各个任务队列中,然后微任务队列和宏任务队列交替入栈执行直到清空所有的任务队列,全局上下文出栈。

这里要注意的是,任务源所指定的异步任务,并不是立即被放入任务队列中的,而是在接收到响应结果后才会将其放入任务队列中排队。如 setTimeout 中指定延迟事件为 1s,则在 1s 后才会将该任务源所指定的任务队列放入队列中;I/O 交互只有接收到响应结果后才将其异步任务放入队列中排队等待执行。

事件循环机制流程

是不是感觉挺抽象的,举个例子来实际感受一下~

console.log('global');

setTimeout(function() {
    console.log('setTimeout1');
    new Promise(function(resolve) {
        console.log('setTimeout1_promise');
        resolve();
    }).then(function() {
        console.log('setTimeout1_promiseThen')
    })
    process.nextTick(function() {
        console.log('setTimeout1_nextTick');
    })
},0)

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promiseThen1')
})

setImmediate(function() {
    console.log('setImmediate');
})

process.nextTick(function() {
    console.log('nextTick');
})

new Promise(function(resolve) {
    console.log('promise2');
    resolve();
}).then(function() {
    console.log('promiseThen2')
})

setTimeout(function() {
    console.log('setTimeout2');
},0)

在这个例子中,主要分析在事件循环流程中各个任务队列的变化情况,对于执行上下文栈的行为暂不做分析。任务队列图中左边代表队头,右边代表队尾。

为了能够实现该例子中有多个宏任务队列和多个微任务队列的情况,我加入了 node 中的 setImmediate 和 process.nextTick ,node 中的事件循环机制与 JavaScript 类似,只是其实现机制有所不同,这里我们不需要关心。加入 node 两个属性后,其优先级如下

在宏任务队列中,各个队列的优先级为
setTimeout > setInterval > setImmediate > I/O
微任务队列中,各个队列的优先级为
process.nextTick > Promise > Object.observe > MutationObserver

所以上述例子只能够在 node 环境中执行,不能够在浏览器中执行。那么让我们来一步步分析上述代码的执行过程。

一,执行 Javascript 代码,全局上下文入栈,输出 global ,此时遇到第一个 setTimeout 任务源,由于其执行延迟时间为 0,所以能够立即接收到响应结果,将其指定的异步任务放入宏任务队列中;

1

二,遇到第一个 Promise 任务源,此时会执行 Promise 第一个参数中的代码,即输出 promise1,然后将其指定的异步任务(then 中函数)放入微任务队列中;

2

三,遇到 setImmediate 任务源,将其指定的异步任务放入宏任务队列中;

3

四,遇到 nextTick 任务源,将其指定的异步任务放入微任务队列中;

4

五,遇到第二个 Promise 任务源,输出 promise2,将其指定的异步任务放入微任务队列中;

5

六,遇到第二个 setTimeout 任务源,将其指定的异步任务放入宏任务队列中;

6

七,JavaScript 整体代码执行完毕,开始清空微任务队列,将微任务队列中的所有任务队列按优先级、单个任务队列的异步任务按先进先出的方式入栈并执行。此时我们可以看到微任务队列中存在 Promise 和 nextTick 队列,nextTick 队列优先级比较高,取出 nextTick 异步任务入栈执行,输出 nextTick;

7

八,取出 Promise1 异步任务入栈执行,输出 promiseThen1;

8

九,取出 Promise2 异步任务入栈执行,输出 promiseThen2;

9

十,微任务队列清空完毕,执行宏任务队列,将宏任务队列中优先级最高的任务队列中的异步任务按先进先出的方式入栈并执行。此时我们可以看到宏任务队列中存在 setTimeout 和 setImmediate 队列,setTimeout 队列优先级比较高,取出 setTimeout1 异步任务入栈执行,输出 setTimeout1,遇到 Promise 和 nextTick 任务源,输出 setTimeout1_promise,将其指定的异步任务放入微任务队列中;

10

十一,取出 setTimeout2 异步任务入栈执行,输出 setTimeout2;

image.png

十二,至此一个微任务宏任务事件循环完毕,开始下一轮循环。从微任务队列中的 nextTick 队列取出 setTimeout1_nextTick 异步任务入栈执行,输出 setTimeout1_nextTick;

12

十三,从微任务队列中的 Promise 队列取出 setTimeout1_promise 异步任务入栈执行,输出 setTimeout1_promiseThen;

13

十四,从宏任务队列中的 setImmediate 队列取出 setImmediate 异步任务入栈执行,输出 setImmediate;

14

十五,全局上下文出栈,代码执行完毕。最终输出结果为

global
promise1
promise2
nextTick
promiseThen1
promiseThen2
setTimeout1
setTimeout1_promise
setTimeout2
setTimeout1_nextTick
setTimeout1_promiseThen
setImmediate
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容