beizhu
type
Post
status
Published
date
May 29, 2020
slug
summary
copy from wenjie, 分析的很到位,也非常详细。遇到宏任务,先处理宏任务——将宏任务放入Event Queue,然后再处理微任务——将微任务放入Event Queue。
tags
JS
category
技术
icon
password
宏任务与微任务
在点击事件冒泡捕获的处理中,通过使用setTimeout 的方式,想要调整某个代码的执行顺序的话,其具体的执行顺序,由一套 机制 去实现,这套机制就是通过定义 宏任务与微任务 来安排执行顺序。
异步机制:javascript事件循环
概念

内存堆:这是内存分配发生的地方。当V8引擎遇到变量声明和函数声明的时候,就把它们存储在堆里面。
调用栈:这是你的代码执行时的地方。当引擎遇到像函数调用之类的可执行单元,就会把它们推入调用栈。
JS单线程,指的是在JS引擎中,解析执行JS代码的调用栈是唯一的,所有的JS代码都在这一个调用栈里按照调用顺序执行,不能同时执行多个函数。

Web APIs:还有很多引擎之外的 API,我们把这些称为浏览器提供的 Web API,比如说 事件监听函数、DOM、HTTP/AJAX请求、setTimeout等等。
回调队列(Event Queue):按照先进先出的顺序存储所有的回调函数。在任意时间,只要Web API容器中的事件达到触发条件,就可以把回调函数添加到回调队列中去。
- *事件循环 ** (Event Loop):持续的检测调用栈和回调队列,如果检测到调用栈为空,它就会通知回调队列把队列中的第一个回调函数推入执行栈。
机制

JS运行时环境的工作机制:
- JS引擎:(唯一主线程)按顺序解析代码,遇到函数声明,入堆,遇到函数调用,入栈;
- 如果是同步函数调用,直接执行得到结果,同步函数弹出栈,继续下一个函数调用;
- 如果是异步函数调用,分发给Web API(多个辅助线程),进入Event Table并注册函数,异步函数弹出栈,继续下一个函数调用;
- Web API中,异步函数在相应辅助线程中处理完成后,即异步函数达到触发条件了,Event Table就将回调函数推入回调队列中。
- Event Loop:不停地检查主线程的调用栈与回调队列,当调用栈空时,就把回调队列中的第一个任务推入栈中执行,不断循环。
例子
下面是一段简易的
ajax 请求代码:- ajax进入Event Table,注册回调函数
success。
- 执行
console.log('代码执行结束')。
- ajax事件完成,回调函数
success进入Event Queue。
- 主线程从Event Queue读取回调函数
success并执行。
下面是一段更详细的例子:
执行过程是这样的:
- JS引擎会检查整段代码的语法错误,如果没有错误,就从头开始深度解析
- 首先遇到setTimeout函数调用,把它推入执行栈顶
- 解析函数体,发现setTimeout函数是Web API的一种,因此就把它分发到Web API模块然后推出栈
- 因为定时器设置了0ms延迟,因此Web API模块立即把它的匿名回调函数推入到回调函数函数队列。事件循环检测执行栈是否是空闲,但是当前栈并不空闲,因为...
- 当setTimeout函数一被分发到Web API模块,JS引擎发现了两个函数声明,把它们存储在堆内存里,然后遇到了sayHi函数的调用,就把它推入了栈顶
- sayHi函数调用了console.log函数,因此console.log就被推入了栈顶
- JS引擎开始解析console.log的函数体,它接收了一个消息去打印‘Hello’,然后被弹出栈
- JS引擎返回到函数sayHi的执行,遇到函数的结束符号}之后,把sayHi弹出栈
- sayHi函数一出栈,紧接着sayBye函数被调用,它就被推入栈顶,被解析,调用console.log,把console.log推入栈顶,打印一条消息,弹出栈。然后sayBye函数弹出栈
- 事件循环检测到执行栈终于空闲了,通知回调队列,然后回调队列把其中的匿名函数推入执行栈
- 匿名函数(就是setTimeout的回调函数)被解析,调用console.log,console.log推入栈顶
- console.log执行完毕、再出栈
- 匿名函数再被推出栈,程序结束
另一个异步机制:宏任务与微任务
但是,JS异步还有一个机制,就是遇到宏任务,先处理宏任务——将宏任务放入Event Queue,然后再处理微任务——将微任务放入Event Queue。
注意,这里两个Event Queue不是同一个queue。
当js引擎的主线程执行栈为空时,它会优先从微任务queue里遍历注册的回调函数并一一执行,然后再从宏任务的queue里遍历执行注册的回调函数,如下图:

- Web API中,异步函数在相应辅助线程中处理完成后,即异步函数达到触发条件了(比如setTimeout设置的10s后),如果异步函数是宏任务,则入宏任务消息队列,如果是微任务,则入微任务消息队列;
- Event Loop不停地检查主线程的调用栈与回调队列,当调用栈空时,就把微任务消息队列中的第一个任务推入栈中执行,执行完成后,再取第二个微任务,直到微任务消息队列为空;然后 去宏任务消息队列中取第一个宏任务推入栈中执行,当该宏任务执行完成后,在下一个宏任务执行前,再依次取出微任务消息队列中的所有微任务入栈执行。
上述过程不断循环,每当微任务队列清空,可作为本轮事件循环的结束。
宏任务 (macrotask/task)
包括:
- I/O(例如点击一次
button,上传一个文件,与程序产生交互的这些都可以称之为I/O)
- setTimeout
- setInterval
- setImmediate(仅Node)
- requestAnimationFrame(仅浏览器)
- xhr
- postMessage
- MessageChannel
对于
setInterval(fn,ms) 来说,不是每过 ms 秒会执行一次 fn ,而是每过 ms 秒,会有 fn 进入Event Queue。
一旦 setInterval 的回调函数 fn 执行时间超过了延迟时间 ms ,那么就完全看不出来有时间间隔了。P.S. 有些地方会列出来
UI Rendering,说这个也是宏任务,可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤P.S.
requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行微任务 (microtask/job)
包括:
- Promise.then catch finally
- process.nextTick(仅Node)
- MutationObserver(仅浏览器)
P.S. new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。 async/await底层是基于Promise封装的,所以await前面的代码相当于new Promise,是同步进行的,await后面的代码相当于then,才是异步进行的。
例子
最终输出结果为:1 > 2 > 3 > 4
再来看一个嵌套的示例:
最后输出结果是Promise1 > setTimeout1 > Promise2 > setTimeout2
Promise1
Node 环境
Node中事件循环
Node用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。 Node也是单线程,但是在处理Event Loop上与浏览器稍微有些不同。
在node里,有一些常用的异步API,这里简单介绍下他们:
setImmediate()
setImmediate()在一次Event Loop执行完毕后立刻调用。setTimeout则是通过计算一个延迟时间后进行执行。所以如下示例,不能保证输出顺序。
而如果是下面这样,则一定是setImmediate先输出。
process.nextTick()
process.nextTick()会将回调函数放入队列中,在下一轮Tick时取出执行这是因为 ,process.nextTick()中的回调函数执行的优先级要高于setImmediate()。
Node里,事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者, setImmediate()属于check观察者。
process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。 事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!