Open JingshunYang opened 5 years ago
js的一个特点是单线程,即浏览器中js引擎中负责解析执行js代码的线程只有一个。这是因为在浏览器环境中,我们常常需要对DOM做各种各样的操作。假设js是多线程的,那么当两个js线程同时对一个DOM进行一项操作,比如线程A希望删除这个DOM,而线程B希望改变其样式,这时就涉及到了复杂的同步问题。因此,为了保证不发生和上述场景类似的问题,js的执行只由一个线程完成。
而js中有许多原生的异步事件,诸如 setTimeout,setInterval,事件监听,Ajax请求等等。那么单线程的js是如何实现异步的呢?其核心在于js的事件循环机制。
每个js线程拥有独立的Event loop,大多数的代码会依据正常的函数调用规则来执行,而遇到特殊的任务源,如 setTimeout/setInterval 则由他们将不同的任务分发到对应的任务队列中。
任务又分为宏任务(Macrotask) 与 微任务(Microtask) 两种。在浏览器中, 宏任务:包括主代码块,setTimeout/setInterval回调,I/O,UI Rendering等; 如有必要,浏览器会在一个宏任务完成之后,下一个宏任务开始之前,重新渲染页面。 微任务:包括promise回调,MutationObserver回调。
事件循环的过程可以用下图来表示,概括起来,事件循环的一轮迭代主要包括3个步骤:
console.log('script start');
setTimeout(function() { console.log('setTimeout'); }, 0);
Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });
console.log('script end');
首先,Macrotask队列中主代码块最先被执行,这里会首先输出 `script start` ,遇到 `setTimeout` 会将它的回调函数分发到Macrotask队列中,然后继续执行,遇到 `Promise` ,再将 `Promise` 的两个回调函数依次分发到Microtask队列中,接着向下执行输出 `script end` ,此时,主代码块执行结束。开始处理Microtask队列中的任务,即promise的两个回调函数,所以控制台接着输出 `promise1` `promise2` ,到目前为止Microtask队列为空,一轮事件循环完成。 然后开始第二轮事件循环,从Macrotask队列中取出setTimeout回调并执行,控制台输出 `setTimeout` ,Microtask队列仍为空,第二轮事件循环结束。至此,程序运行完毕,控制台的所有输出汇总如下:
script start script end promise1 promise2 setTimeout
### Case Study 2 我们再来看一个涉及到html 的例子,我们创建两个div,inner div嵌套在outer div里面,代码如下: ```html <div class="outer"> <div class="inner"></div> </div>
// 首先获取文档中的两个元素 var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // 创建MutationObserver监听outer div的属性改化,如果发生变化就调用函数 new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); // 声明一个事件监听器 function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // 将监听器分别和两个div绑定 inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);
那么现在我们点击内部的div,控制台会如何输出呢? 首先,点击inner div相当于一次I/O事件,会触发inner div绑定的 onClick 函数 ,同时由于event bubble也会触发和outer div绑定的 onClick 函数,两个onClick函数作为Macrotask分发至Macrotask队列中。
onClick
接着,从Macrotask中取出onClick函数执行,接下来就和上一个例子差不多了,控制台输出 click,setTimeout 将其回调函数分发至Macrotask队列,Promise将其回调函数分发至Microtask队列,然后执行到outer.setAttribute,注意它修改了outer div的属性,会触发MutationObserver绑定的回调函数,而该回调函数会分发至Microtask队列中。至此,inner div的事件监听器执行完毕。 此时,Macrotask队列和Microtask的情况分别是:
click
setTimeout
Promise
outer.setAttribute
MutationObserver
Macrotask队列:onClick | setTimeout callback Microtask队列:Promise callback | Mutation observer
然后,开始处理所有的Microtask,控制台依次输出promise mutate,一轮事件循环结束。 此时,Macrotask队列和Microtask的情况分别是:
promise
mutate
Macrotask队列:onClick | setTimeout callback Microtask队列:空
第二轮事件循环开始执行第二个onClick函数,和上一个一样,控制台输出 click,setTimeout 将其回调函数分发至Macrotask队列,Promise将其回调函数分发至Microtask队列,然后outer.setAttribute 触发MutationObserver绑定的回调函数,而该回调函数会分发至Microtask队列中。 此时,Macrotask队列和Microtask的情况分别是:
Macrotask队列:setTimeout callback | setTimeout callback Microtask队列:Promise callback | Mutation observer
然后,开始处理所有的Microtask,控制台依次输出promise mutate,第二轮事件循环结束。 此时,Macrotask队列和Microtask的情况分别是:
Macrotask队列:setTimeout callback | setTimeout callback Microtask队列:空
接着继续执行setTimeout的回调函数,控制台输出timeout timeout。至此,程序运行完毕,控制台的所有输出汇总如下:
timeout
click promise mutate click promise mutate timeout timeout
Tasks, microtasks, queues and schedules 深入理解js事件循环机制(浏览器篇) 《JavaScript核心技术开发揭秘》
想到还有种场景,如果通过inner.click()来触发事件的话,console的输出顺序会是什么呢😉
前言
js的一个特点是单线程,即浏览器中js引擎中负责解析执行js代码的线程只有一个。这是因为在浏览器环境中,我们常常需要对DOM做各种各样的操作。假设js是多线程的,那么当两个js线程同时对一个DOM进行一项操作,比如线程A希望删除这个DOM,而线程B希望改变其样式,这时就涉及到了复杂的同步问题。因此,为了保证不发生和上述场景类似的问题,js的执行只由一个线程完成。
而js中有许多原生的异步事件,诸如 setTimeout,setInterval,事件监听,Ajax请求等等。那么单线程的js是如何实现异步的呢?其核心在于js的事件循环机制。
宏任务、微任务
每个js线程拥有独立的Event loop,大多数的代码会依据正常的函数调用规则来执行,而遇到特殊的任务源,如 setTimeout/setInterval 则由他们将不同的任务分发到对应的任务队列中。
任务又分为宏任务(Macrotask) 与 微任务(Microtask) 两种。在浏览器中, 宏任务:包括主代码块,setTimeout/setInterval回调,I/O,UI Rendering等; 如有必要,浏览器会在一个宏任务完成之后,下一个宏任务开始之前,重新渲染页面。 微任务:包括promise回调,MutationObserver回调。
事件循环过程
事件循环的过程可以用下图来表示,概括起来,事件循环的一轮迭代主要包括3个步骤:
Case Study 1
setTimeout(function() { console.log('setTimeout'); }, 0);
Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });
console.log('script end');
script start script end promise1 promise2 setTimeout
那么现在我们点击内部的div,控制台会如何输出呢? 首先,点击inner div相当于一次I/O事件,会触发inner div绑定的
onClick
函数 ,同时由于event bubble也会触发和outer div绑定的onClick
函数,两个onClick
函数作为Macrotask分发至Macrotask队列中。接着,从Macrotask中取出onClick函数执行,接下来就和上一个例子差不多了,控制台输出
click
,setTimeout
将其回调函数分发至Macrotask队列,Promise
将其回调函数分发至Microtask队列,然后执行到outer.setAttribute
,注意它修改了outer div的属性,会触发MutationObserver
绑定的回调函数,而该回调函数会分发至Microtask队列中。至此,inner div的事件监听器执行完毕。 此时,Macrotask队列和Microtask的情况分别是:然后,开始处理所有的Microtask,控制台依次输出
promise
mutate
,一轮事件循环结束。 此时,Macrotask队列和Microtask的情况分别是:第二轮事件循环开始执行第二个onClick函数,和上一个一样,控制台输出
click
,setTimeout
将其回调函数分发至Macrotask队列,Promise
将其回调函数分发至Microtask队列,然后outer.setAttribute
触发MutationObserver
绑定的回调函数,而该回调函数会分发至Microtask队列中。 此时,Macrotask队列和Microtask的情况分别是:然后,开始处理所有的Microtask,控制台依次输出
promise
mutate
,第二轮事件循环结束。 此时,Macrotask队列和Microtask的情况分别是:接着继续执行setTimeout的回调函数,控制台输出
timeout
timeout
。至此,程序运行完毕,控制台的所有输出汇总如下:参考资料
Tasks, microtasks, queues and schedules 深入理解js事件循环机制(浏览器篇) 《JavaScript核心技术开发揭秘》