网上讲事件循环的文章很多,这里mark下自己的认识。
每一个线程都有自己的事件循环(event loop)机制,事件循环按照事件队列里的任务(task)“依次”执行。
浏览器中的工作线程也有自己的event loop,浏览器确保多个task可以按照一定顺序执行,并在多个task之间可能进行UI渲染。
概念
Tasks: 任务队列
Microtasks: 微任务队列
Stack: js执行堆栈
js有两种事件,一种是task,一种是microtask
浏览器会执行Tasks中的每个task,在多个task间择机进行rendering。
microTask在每一个task执行后紧接着执行,前提是当前执行栈stack是空闲的,没有其他的js任务在执行。
如何理解当前栈是空闲的?
当没有js在执行的时候,就认为其是空闲的。比如当前正在执行一个函数,如果这个函数还没有返回,就认为当前栈不是空闲的。
一个有意思的case
1 | <div id="outer"> |
1.inner.click()执行完当前js后,事件向父元素冒泡,我们认为其冒泡过程是作为click()执行的,整个过程中执行栈均没有空闲,无法执行microtask。
2.手动点击inner,触发了inner的监听事件,执行完inner的监听事件后,冒泡到outer,执行outer的监听事件。由于在冒泡到outer前,inner的Listener已经执行完成并且返回了,所以虽然此轮任务尚未结束,但此时执行栈是空闲的,可以执行microtask。
备注
有些文章把上面的task也称之为macrotask,以便和microtask区分。
macro-task包括:script(整体代码,主进程), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task包括:process.nextTick, Promise回调, Object.observe(已废弃), MutationObserver(DOM变化监听器)
1 | setTimeout(function(){ console.log(0); }, 0); |
Promise新建属于当前stack, 创建后会立即执行,首先输出2。Promise的回调属于micro-task任务,在本轮事件循环的末尾执行,输出1。setTimeout回调的执行则发生在下一轮event中,最后输出0。
在 JavaScript 函数中,只有 return / yield / throw 会中断函数的执行,其他的都无法阻止函数运行到结束的
不同的浏览器实现存在差异,有些浏览器下setTimeout的回调可能会先于Promise回调执行
参考:
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/