浅析JS的Event Loop机制
Posted 大前端学堂
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析JS的Event Loop机制相关的知识,希望对你有一定的参考价值。
根据JS的运行环境,Event Loop(事件循环)分为浏览器的Event Loop和NodeJS的Event Loop。
一、浏览器的Event Loop
浏览器端JS异步的实现宏观上是依靠浏览器的多线程,微观上是依靠的Event Loop,即事件循环。
先看一段示例代码:
console.log('1')
setTimeout(function(){
console.log('2')
},0)
Promise.resolve().then(function(){
console.log('3')
})
console.log('4')
/*打印结果*/
//1
//4
//3
//2
异步任务放到任务队列里。定时器先被放入任务队列中,为什么promise先执行呢?这里涉及异步任务的宏任务和微任务的概念。
浏览器环境下的 异步任务 分为 宏任务(macroTask) 和 微任务(microTask):
宏任务包括:
script代码块
setTimeout/setInterval
setImmediate
I/O
UI rendering
微任务包括:
Promise
Object.observe(监听对象变化的api)
MutationObserver(监听DOM树变化的api)
postMessage
当满足执行条件时,宏任务(macroTask) 和 微任务(microTask) 会各自被放入对应的队列:宏任务队列(Macrotask Queue) 和 微任务队列(Microtask Queue) 中等待执行。
一个Event Loop 可以有一个或多个宏任务队列,但是只能有一个微任务队列。
浏览器的Event Loop 处理模型
基本执行顺序:
首先执行script标签里的同步代码,script被称为全局任务,也属于宏任务(macroTask)
当任务队列里一个宏任务执行完后,执行微任务队列里的所有微任务
微任务全部执行完,再取任务队列中的一个宏任务执行
注意:我们经常用的requestAnimationFrame既不在宏任务队列,也不在微任务队列。它是处在最终的渲染阶段。
了解到这里,我们分别看两个例子:
console.log("start");
setTimeout(() => {
console.log("setTimeout");
new Promise(resolve => {
console.log("promise inner1");
resolve();
}).then(() => {
console.log("promise then1");
});
}, 0);
new Promise(resolve => {
console.log("promise inner2");
resolve();
}).then(() => {
console.log("promise then2");
});
/*打印结果*/
//start
//promise inner2
//promise then2
//setTimeout
//promise inner1
//promise then1
首先执行script这个宏任务里的同步代码,依次输出start、promise inner2,然后进入微任务队列执行所有的微任务,输出promise then2,再次执行下一个宏任务setTimeout,依次输出setTimeout、promise inner1,最后再次进入微任务队列执行所有的微任务,输出promise then1。
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
return Promise.resolve().then(_ => {
console.log("async2 promise");
});
}
console.log("start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
/*打印结果*/
//start
//async1 start
//promise1
//async2 promise
//promise2
//async1 end
//setTimeout
首先执行script这个宏任务里的同步代码,依次输出start、async1 start、promise1,然后进入微任务队列执行所有的微任务,依次输出async2 promise、promise2、async1 end,再次执行下一个宏任务setTimeout,输出setTimeout。
二、NodeJS的Event Loop
浏览器的Event loop是在html5中定义的规范,而NodeJS中则由libuv库实现。libuv库流程大体分为6个阶段:
timers: 执行定时器(setTimeout/setInterval)的回调
pending callbackss: 系统操作的回调
idle,prepare: 内部使用
poll: 等待新的I/0事件
check: 执行setImmediate回调
close callbacks: 内部使用
每一个阶段都有一个callbacks的先进先出的队列需要执行。当event loop运行到一个指定阶段时,该阶段的fifo队列将会被执行,当队列callback执行完或者执行的callbacks数量超过该阶段的上限时,event loop会转入下一个阶段。
NodeJS的Event Loop 处理模型
Poll阶段的两个主要功能:
计算应该被block多久
处理poll队列的事件
注意: process.nextTick()是一个异步的node API,但不属于event loop的阶段。调用这个方法,可以使event loop暂停,优先执行这个方法的回调函数。
了解到这里,我们分别看两个例子:
const fs = require('fs');
function someAsyncOperation(callback) {
fs.readFile(__dirname, callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
someAsyncOperation(() => {
const startCallback = Date.now();
while (Date.now() - startCallback < 200) {
// 200ms内执行某些行为
}
});
/* 执行结果 */
//204ms have passed since I was scheduled
执行someAsyncOperation方法后,花4ms读取文件,然后进入poll队列执行回调函数,200ms内执行某些行为,200ms后退出poll队列,检查是否有定时器,如果有且已到时间则执行这个定时器的回调函数。并不是定时器设置多长时间后执行就可以执行,需要等待poll队列里的回调函数都执行完,才能执行这个定时器。
const fs = require("fs");
fs.readFile(__filename, _ => {
setTimeout(_ => {
console.log("setTimeout");
}, 0);
setImmediate(_ => {
console.log("setImmediate");
process.nextTick(_ => {
console.log("nextTick2");
});
});
process.nextTick(_ => {
console.log("nextTick1");
});
});
/* 执行结果 */
//nextTick1
//setImmediate
//nextTick2
//setTimeout
读取文件后进入poll队列,有nextTick就暂停event loop,优先执行nextTick的回调函数,打印nextTick1,然后进入check阶段,执行setImmediate,打印setImmediate,有nextTick就暂停event loop,优先执行nextTick的回调函数,打印nextTick2,然后进入定时器阶段,执行到时的定时器的回调函数,打印setTimeout。
本文为原创内容,若转载请注明出处,转发感激不尽。
以上是关于浅析JS的Event Loop机制的主要内容,如果未能解决你的问题,请参考以下文章
前端基础 | JS异步执行机制——事件循环(Event Loop)