JS异步编程(2)-异步核心Event loop

Event loop 是 JavaScript 异步编程的核心,通过事件循环机制,让单线程的 JavaScript 具备异步处理任务的能力

异步任务队列

异步任务队列分为两类

  • 宏任务队列
  • 微任务队列
    都用于存放异步任务

为什么异步队列要分宏微任务?

其实在学习了 Event loop 很久之后,才突然反应过来,反问自己这个最初的问题
异步队列有一个就行了,已经能够满足异步操作的需求,为什么还需要分两种队列呢?

答案是:为了插队!

宏微任务的执行逻辑,本质上就是为了满足异步操作的插队需求,让某个后插入的异步操作尽量早的执行

宏任务(macrotasks)

API Web Node
DOM API
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

有些地方会把 UI Rendering 也列为宏任务
但是在 HTML 规范文档中,发现这其实是和微任务平行的一个操作步骤

  • UI Rendering 代表的不是一个单一的任务,而是一个任务队列(queue)
  • 具备一些不同于普通宏任务和微任务的特性
    • 触发的时机在当前微任务队列和下一个宏任务之间
    • UI Rendering 执行中触发的新的 requestAnimationFrame,不会推进当前正在执行的 UI Rendering 队列。而是进入下一次的 UI Rendering 队列

微任务(microtasks)

API Web Node
process.nextTick
MutationObserver
Promise.then catch finally

process.nextTick 和 web 端的 UI Rendering 类似

  • process.nextTick 也有一个自己的任务队列 nextTick queue
  • 具备一些不同于普通微任务的特性
    • 触发的时机在当前宏任务和当前微任务队列之间

执行机制

Web 中的执行机制

浏览器环境下的 Event loop 是由HTML5规范明确定义,由各大浏览器厂商各自实现
这里主要涉及到下面几个浏览器线程:

  • JS引擎线程:主要处理主执行栈任务(同步任务)
  • 异步http请求线程:主要处理网络请求,将已完成的网络请求回调函数推进事件触发线程
  • 定时器线程:将已完成待执行的定时器回调函数推进事件触发线程
  • 事件触发线程:存储宏微任务的线程

基本流程

异步队列的执行机制,简单来说

  1. 当主执行栈里的任务清空之后,开始读取异步任务队列中的任务
  2. 先读取微任务队列中的任务,依次读取执行直至队列清空
  3. 然后从宏任务中读取第一个任务执行
  4. 从第2步开始重复,直到宏任务队列为空

同步任务 -> 全部微任务 -> UI Rendering -> 宏任务 -> 全部微任务 -> UI Rendering -> 下一个宏任务 -> ...

如果在执行过程中

  • 触发新的宏任务,会将其推进宏任务队列,等待读取

  • 触发新的微任务,会将其推进当前的微任务队列,在本次微任务队列中完成执行
    同步任务 -> 全部微任务 -> UI Rendering -> 宏任务(触发新的宏任务和微任务) -> 全部微任务(包含新触发的微任务) -> UI Rendering -> 下一个宏任务(新触发的宏任务被推进宏任务列表等待执行) -> ...

  • 触发新的 UI Rendering,会将其推进下一个 UI Rendering
    同步任务 -> 全部微任务 -> UI Rendering(触发新的 RAF) -> 宏任务 -> 全部微任务 -> UI Rendering(包含之前触发的新RAF) -> 下一个宏任务 -> ...

操作触发的浏览器事件回调

// html
<div class="parent" onclick="handleClick()">
    <div class="child" onclick="handleClick()"/>
</div>

// js
function handleClick() {
    Promise.resolve().then(() => console.log('promise then'))
    setTimeout(() => console.log('setTimeout msg'), 0)
}

上面的代码,如果用户点击 child 元素
类似于用宏任务的触发方式,直接注册了 parentchild 元素的 click 回调函数
child click -> child promise then -> parent click -> parent promise then -> child setTimeout msg -> parent setTimeout msg

代码触发的浏览器事件回调

同样是上面的代码,如果使用 JS 代码触发事件

document.querySelector('.child').click()

那么和 dispatchEvent 类似,都是一种同步任务的触发方式
把两次的 click 事件都推入主执行栈队列
child click -> parent click -> child promise then -> parent promise then -> child setTimeout msg -> parent setTimeout msg

Node 中的执行机制

与 Web 端 Event loop 依赖浏览器线程一样,Node 端 Event loop 也依赖一位新同学: libuv

  • libuv 是 Node 的新跨平台抽象层,核心是提供 i/o 的事件循环和异步回调
  • libuv使用异步,事件驱动的编程方式
  • libuv的API包含有时间,非阻塞的网络,异步文件操作,子进程等等。
  • Event Loop就是在libuv中实现的。

6个阶段

Node的 Event loop一共分为6个阶段,会按照顺序反复运行
每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行
当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段

image.png

每个细节具体如下:

  1. timers: 执行 setTimeout 和 setInterval 中到期的 callback,由 poll 调度进入该阶段
  2. pending: 某些系统操作级别的回调,在这个阶段执行
  3. idle, prepare: 仅在内部使用。
  4. poll: 执行 I/O 回调,在适当的情况下回阻塞在这个阶段。
  5. check: 执行 setImmediate 的回调函数
  6. close: 执行close事件的 callback
timers
  • timers 阶段会执行 setTimeout 和 setInterval 回调,由 poll 调度进入该阶段
  • timers 阶段如果触发了新的 setTimeout 和 setInterval,会推入到下一次的 timers 阶段,不会在本次 timers 阶段执行
poll

这一阶段主要处理两件事情

  1. 回到 timers 阶段执行回调
  2. 执行 I/O 回调

执行逻辑:


image.png

Node 10.x 及以前的基本流程

在 Node 10.x 及以前。Event loop 的每个阶段,都是先执行宏任务队列,再执行微任务队列
全部宏任务 -> 全部 nextTick 任务 -> 全部微任务

Node 11.x 及以后的基本流程

Node.js 在升级到 11.x 后,Event Loop 运行原理发生了变化。一个宏任务执行完成就执行微任务队列,和浏览器一致了
宏任务 -> 全部 nextTick 任务 -> 全部微任务 -> 下一个宏任务 -> 全部 nextTick 任务 -> 全部微任务

总结

在 Web 端,Event loop 依赖各个浏览器厂商的实现
除了正常的宏微任务外,还拥有独特的 UI Rendering 和 MutationObserver
依靠浏览器各线程的配合,完成 Event loop 的循环

而在 Node 端,Event loop 依赖 libuv 的实现,同时在 Node 11 版本前后有差异
Node 端拥有 6 个事件阶段,每个阶段都可以进行 Event loop 循环

参考文章

Tasks, microtasks, queues and schedules
一次弄懂Event Loop(彻底解决此类面试问题)
面试题:说说事件循环机制(满分答案来了)

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

推荐阅读更多精彩内容