宏任务与微任务
在点击事件冒泡捕获的处理中,通过使用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
请求代码:
1 | let data = []; |
ajax进入Event Table,注册回调函数
success
。执行
console.log('代码执行结束')
。ajax事件完成,回调函数
success
进入Event Queue。主线程从Event Queue读取回调函数
success
并执行。
下面是一段更详细的例子:
1 | setTimeout(function(){ |
执行过程是这样的:
- 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,才是异步进行的。
P.S. 在Promise/A+的规范中,Promise
的实现可以是微任务,也可以是宏任务,但是普遍的共识表示(至少Chrome
是这么做的),Promise
应该是属于微任务阵营的*
例子
1 | setTimeout(_ => console.log(4)) |
最终输出结果为:1 > 2 > 3 > 4
再来看一个嵌套的示例:
1 | Promise.resolve().then(()=>{ |
最后输出结果是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
则是通过计算一个延迟时间后进行执行。
所以如下示例,不能保证输出顺序。
1 | setTimeout(_ => console.log('setTimeout')) |
而如果是下面这样,则一定是setImmediate先输出。
1 | setTimeout(_ => console.log('setTimeout'), 20) |
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,将会没完没了,主线程根本不会去读取”事件队列”!
1 | process.nextTick(function foo() { |
参考
https://juejin.im/post/5e972f3c518825739d40874a