2 个名词
- microtask: 称之为小型任务
- macrotask(task queue): 称之为大型任务
事件循环模型
当执行栈(call stack)为空时,一个事件循环会按照下面的步骤进行:
- 从macrotask队列中选择最老一个任务taskA
- 如果taskA不存在,即macrotask队列是空的,那么进行步骤6
- 设置当前运行的任务为taskA
- 运行taskA(即执行taskA它的回调函数)
- 设置当前运行的任务为空,并从macrotask中移除taskA
- 开始处理microtask
- (a)从microtask中选择最老的任务taskX
- (b)如果taskX不存在,即microtask是空的,那么跳到步骤(g)
- (c)设置当前运行的任务为taskX
- (d)运行taskX(即执行taskX的回调函数)
- (e)设置当前运行的任务为空,并从microtask中移除taskX
- (f)从microtask选择下一个最老的任务,步骤(b)
- (g)重复以上步骤,直到microtask中的任务全部执行完毕
- 继续回到步骤1
精简的概括就是:
- 先从 macrotask 队列中选择最老的一个任务开始执行,然后移除这个最老的任务
- 再执行 microtask 队列中所有的任务,然后移除他们
- 继续下一轮,重复以上 2 步
需要知道的一些事情:
- 当一个macrotask任务正在运行的时候,新的事件可以被注册,也即创建新的任务。例如:
- promiseA.then()的回调是一个任务
- promiseA如果处于resolve/reject状态时,那么该任务被推到事件循环的当前轮次的microtask中
- promiseA处于pending状态时,那么该任务将会被推到事件循环的下一个轮次的microtask中。
- setTimeout(callback, n)的回调是一个任务,将会被推到macrotask中,及时n=0。
- promiseA.then()的回调是一个任务
- 如果事件循环正在执行microtask中的任务,那么你可以继续往microtask中添加任务,这些任务都会在本轮次的循环中执行。
- 只有等到本轮次的microtask中的所有任务执行完毕,才会执行下一轮的macrotask任务。
- 常见的microtask任务:
process.nextTick
,Promise
,Object.observe
,MutationObserver
- 常见的macrotask任务:
setTimeout
,setInterval
,setImmediate
,Dom事件(click, scroll, mouseup等),ajax
,I/O, UI rendering等。还需要注意的是,整个的脚本也是一个macrotask任务。
经过上面的分析,可以看如下例子输出:
1 | console.log('script start') |
call stack(执行栈)与 event loop(事件循环)之间的关系
为了形象的表示,可以参看下图:
关于他们之间的关系的说明,此处就不在叙述了,具体可以参看这里
参考文章如下:
microtask and macrotask
event loop
task-queue spec
call stack