JS 的 Event Loop机制理解
JS 的 Event Loop机制理解
Event Loop是什么?
event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
-
浏览器的Event Loop是在html5的规范中明确定义。
-
NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档。
-
libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。
宏任务和微任务
JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
宏任务macro-task
script(整体代码),setTimeout,setInterval,setImmediate(Node独有),I/O,UI rendering。
微任务micro-task
process.nextTick(node独有),Promise,Object.observe(已废弃),MutationObserver(html5新特性)
-
setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
-
来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
-
其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
浏览器的Event Loop


浏览器的Event Loop完整流程图
JavaScript代码的具体流程:
-
执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
-
全局Script代码执行完毕后,调用栈Stack会清空;
-
主线程遇到异步任务,指给对应的异步进程进行处理(WEB API)
-
异步进程处理完毕(Ajax返回、DOM事件处罚、Timer到等),将相应的异步任务推入任务队列;
-
-
从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
-
继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
-
microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
-
取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
-
执行完毕后,调用栈Stack为空;
-
重复第3-7个步骤;
-
重复第3-7个步骤;
-
......
异步进程:
-
类似onclick等,由浏览器内核的DOM binding模块处理,事件触发时,回调函数添加到任务队列中;
-
setTimeout等,由浏览器内核的Timer模块处理,时间到达时,回调函数添加到任务队列中;
-
Ajax,由浏览器内核的Network模块处理,网络请求返回后,添加到任务队列中。
microtask、macrotask两者的区别:
microtask queue:唯一,整个事件循环当中,仅存在一个;执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;
macrotask queue:不唯一,存在一定的优先级(用户I/O部分优先级更高)(未验证);异步执行,同一事件循环中,只执行一个。
这里归纳3个重点:
-
宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
-
微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空;
-
图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。
来举个简单的栗子:
console.log('script start 1');
setTimeout(function() {
console.log('setTimeout 2');
}, 0);
Promise.resolve().then(function() {
console.log('promise1 3');
}).then(function() {
console.log('promise2 4');
});
console.log('script end 5');
// script start 1
// script end 5
// promise1 3
// promise2 4
// setTimeout 2
再来一个栗子:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// async2 end
// Promise
// script end
// promise1
// promise2
// async1 end
// setTimeout
这里需要先理解async/await
async/await 在底层转换成了 promise 和 then 回调函数。也就是说,这是 promise 的语法糖。每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。
async/await的实现,离不开 Promise。从字面意思来理解,async是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。
关于73以下版本和73版本的区别:
在老版本73版本以下,先执行promise1和promise2,再执行async1。
在73版本,先执行async1再执行promise1和promise2。
那么整个事件循环中何时进行ui render呢?
console.log('global start');
setTimeout(function() {
// 应该是这里执行前开始渲染ui,试试用alert阻塞下。
alert(' ui 已经渲染完毕了吗? ');
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
alert(' ui 开始渲染 ');
})
div.innerHTML = 'end';
console.log('global end');
// global start
// promise1
// promise2
// then1
// 弹框: ui 已经渲染完毕了吗?
// UI 渲染 (UI渲染在当前循环走完后执行)
// 弹框: ui 已经渲染完毕了吗?
// timeout1
再来一个稍微复杂一点的栗子
HTML: <div class="outer" style="width:200px;height:200px;background-color: #ccc"> <div class="inner" style="width:100px;height:100px;background-color: #ddd">begin</div> </div> JS: var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); var i = 0; new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { i++; if(i === 1) { inner.innerHTML = 'end'; } console.log('click'); setTimeout(function() { alert('锚点'); console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); // click // promise // mutate // click // promise // mutate // 弹框:锚点 // timeout // 弹框:锚点 // timeout
如果是JS触发click呢 ?
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
inner.click();
// click
// 由于js执行栈还没清空,仍有 inner.click()任务,所以不能执行微任务,继续冒泡
// click
// promise
// mutate
// promise
// 由于 上一个MutationObserver任务还未执行,不再添加第二个MutationObserver任务
// timeout

浙公网安备 33010602011771号