Event Loop的总结

189 阅读4分钟
  1. JS是单线程的 JS中的代码都是串行的, 前面没有执行完毕后面不能执行

  2. 宏任务和微任务 在JS的异步代码中又区分"宏任务(MacroTask)"和"微任务(MicroTask)" 宏任务: 宏/大的意思, 可以理解为比较费时比较慢的任务 微任务: 微/小的意思, 可以理解为相对没那么费时没那么慢的任务

  3. 常见的宏任务和微任务 MacroTask(宏任务): setTimeout, setInterval, setImmediate(IE独有)... MicroTask(微任务): Promise, MutationObserver ,process.nextTick(node独有) ...

浏览器中的Event Loop

浏览器Event Loop中有一个宏任务队列和一个微任务队列,所有的宏任务和微任务都会放到自己的执行队列中, 所有放到队列中的任务都采用"先进先出原则", 也就是多个任务同时满足条件, 那么会先执行先放进去的

执行顺序:

  1. 从上至下执行所有同步代码
  2. 在执行过程中遇到宏任务就放到宏任务队列中,遇到微任务就放到微任务队列中
  3. 当所有同步代码执行完毕之后, 就执行微任务队列中满足需求所有回调
  4. 当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调
  5. 每执行完一个宏任务都会立刻检查微任务队列中是否有微任务, 如果有就立刻清空
	console.log('script start');
	
	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 => Promise => script end => promise1 => promise2 => setTimeout

NodeJS中的Event Loop:

NodeJS事件环和浏览器事件环区别

  1. 任务队列个数不同 浏览器事件环有2个事件队列(宏任务队列和微任务队列) NodeJS事件环有6个事件队列
  2. 微任务队列不同 浏览器事件环中有专门存储微任务的队列 NodeJS事件环中没有专门存储微任务的队列
  3. 微任务执行时机不同 浏览器事件环中每执行完一个宏任务都会去清空微任务队列 NodeJS事件环中只有同步代码执行完毕和其它队列之间切换的时候回去清空微任务
  4. 微任务优先级不同 浏览器事件环中如果多个微任务同时满足执行条件, 采用先进先出 NodeJS事件环中如果多个微任务同时满足执行条件, 会按照优先级执行,在NodeJS中process.nextTick微任务的优先级高于Promise.resolve微任务

NodeJS中的任务队列

	   ┌───────────────────────┐
	┌> │       timers          │ 执行setTimeout 和 setInterval中到期的callback
	│  └──────────┬────────────┘
	│  ┌──────────┴────────────┐
	│  │    pending callbacks  │ 执行系统操作的回调, 如:tcp, udp通信的错误callback
	│  └──────────┬────────────┘
	│  ┌──────────┴────────────┐
	│  │      idle, prepare    │ 只在内部使用
	│  └──────────┬────────────┘
	│  ┌──────────┴────────────┐
	│  │        poll           │ 执行与I/O相关的回调(除了close回调、定时器回调和setImmediate之外,几乎所有回调都执行)          
	│  └──────────┬────────────┘
	│  ┌──────────┴────────────┐
	│  │        check          │ 执行setImmediate的callback
	│  └──────────┬────────────┘
	│  ┌──────────┴────────────┐
	└──┤    close callbacks    │ 执行close事件的callback,例如socket.on("close",func)
	   └───────────────────────┘

执行顺序:

  1. 从上至下执行所有同步代码,并将所有宏任务放入对应的队列
  2. 当同步代码执行完毕后,执行满足条件的微任务
  3. 当所有满足条件的微任务完毕之后, 执行timers队列中的任务
  4. 当timers队列中的任务执行完毕后,执行满足条件的微任务
  5. 当所有满足条件的微任务完毕之后, 执行下一个队列中的任务,依次循环执行
	setTimeout(function () {
	    console.log("setTimeout");
	});
	Promise.resolve().then(function () {
	    console.log("Promise");
	});
	console.log("同步代码 Start");
	process.nextTick(function () {
	    console.log("process.nextTick");
	});
	setImmediate(function () {
	    console.log("setImmediate");
	});
	console.log("同步代码 End");

上述代码的执行顺序为:同步代码 Start => 同步代码 End => process.nextTick => Promise => setTimeout => setImmediate

注意点: 执行完poll, 会查看check队列是否有内容, 有就切换到check 如果check队列没有内容, 就会查看timers是否有内容, 有就切换到timers 如果check队列和timers队列都没有内容, 为了避免资源浪费就会阻塞在poll

Event Loop面试题: 在NodeJS中指定的延迟时间是有一定的误差的, 所以导致了以下代码输出结果随机的问题

setTimeout(function () {
    console.log("setTimeout");
}, 0);
setImmediate(function () {
    console.log("setImmediate");
});

解决办法:将上述代码放入与I/O相关的回调函数中 因为 readFile 的回调在 poll 中执行,发现有 setImmediate,所以会立即跳到 check 阶段执行回调,再去 timers 阶段执行 setTimeout 所以输出一定是 setImmediate,setTimeout

fs.readFile(__filename, () => {
    setTimeout(function () {
        console.log("setTimeout");
    }, 0);
    setImmediate(function () {
        console.log("setImmediate");
    });
});