目录
学习EventLoop机制帮助我们理解代码执行顺序。首先上例子:
setTimeout(function () {
console.log("setTimeout1");//4
});
new Promise(function (resolve, reject) {
console.log("promise1"); //1
resolve(100);
console.log("promise2");//2
}).then(function (res) {
console.log(res);//3
setTimeout(function () {
console.log("setTimeout2");//5
});
});
输出顺序为:promise1 promise2 100 setTimeout1 setTimeout2
1、简述EventLoop
Js单线程异步实现:从宏观来看,通过浏览器多线程实现。从微观来看,通过EventLoop(事件循环)实现异步。
js单线程语言意味着所有的代码执行是排队按顺序执行,但是有些代码执行是需要等待一段时间后才能获取执行结果执行完毕。那么如果严格按照顺序执行的话,代码执行效率就会非常的低,用户体验就很差。
任务分为同步任务和异步任务,js(浏览器)解决的这个问题办法就是“任务队列”,把需要执行的任务排队。
第一步,同步任务先执行,把所有的同步任务全都在主线程上执行,形成一个执行栈。
第二步,异步任务等待,主线程之外,存在一个“任务队列”,存放异步执行代码,如定时器、时间监听函数等,进入等待状态。
第三部,主线程中的所有同步任务执行完毕,执行栈被清空,读取“任务队列”检查队列中的异步任务,提取到执行栈开始执行。
主线程不断重复上面的三步
event loop 的机制,就是任务队列、js主线程、异步操作之间的互相协作。
2、宏任务和微任务
任务队列分为宏任务和微任务,微任务拥有更高的优先级。当遍历任务队列时,先检查微任务队列,如果里面有任务就全部执行,执行后再执行下一个宏任务。执行每个宏任务之前都要检查微任务队列是否有任务,如果有,优先执行微任务队列。微任务会全部执行,而宏任务会一个一个来执行。微任务执行完后,再去取一个宏任务来进行。
macro-task(宏任务):包括整体代码script,setTimeout,setInterval,setImmediate,I/O ,UI rendering
micro-task(微任务):Promise,object.observe,process.nextTick,MutationObserver,postMessage
3、EventLoop过程
堆的空间比较大,存放数组、对象等,栈的空间比较小,存放基础数据类型,对象引用,函数的调用。
在JS中,基本数据类型变量大小固定,并且操作简单容易,所以把它们放入栈中存储。 引用类型变量大小不固定,所以把它们分配给堆中,让他们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。
栈内存由于它的特点,所以它的系统效率较高。 堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈。
EventLoop过程:①先执行全局script任务,执行完主线程执行栈空;②检查微任务队列里的所有任务,依次执行,全部执行完成;③检查宏任务,读取第一个宏任务执行(执行宏任务时可能产生新的微任务)
EventLoop分为两种,分别是浏览器的EventLoop和node.js的EventLoop
4、浏览器的EventLoop
一个EventLoop有一个或多个任务队列,每个EventLoop有一个微任务队列
console.log("start");
setTimeout(() => {
console.log("setTimeout");
new Promise((resolve) => {
console.log("promise inner1");
resolve(100);
}).then(function (res) {
console.log(res);
console.log("promise then1");
});
}, 0);
new Promise((resolve) => {
console.log("promise inner2");
resolve(200);
}).then(function (res) {
console.log(res);
console.log("promise then2");
});
执行结果:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
return Promise.resolve().then(() => {
console.log("async2 promise.then");
});
}
console.log("start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
async1();
new Promise((resolve) => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise1.then");
});
console.log("end");
5、Node.js的EventLoop
- Node-core:Node.js API的核心js库
- 绑定:负责包装和暴露libuv和js的其他低级功能
- V8: chrome开源的js 引擎,也是js可以在服务端运行的基础。
- Libuv: node.js底层的i/o引擎,c语言编写事件驱动的一步I/O库 nodejs异步编程的基础 libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎 。
nodejs的EventLoop机制的阶段操作顺序为timers、pending callbacks、idle,prepare、poll、check、close callbacks六个阶段。
阶段概述:
timers定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
idle, prepare:仅系统内部使用。
poll轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
check检测:setImmediate() 回调函数在这里执行。
close callbacks关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。
每一个阶段都有一个装有callbacks的fifo(先进先出)队列,当event loop运行到一个指定阶段时,node将执行该阶段的fifo队列,当队列callback执行完或者执行callbacks数量超过该阶段的上限时,event loop会转入下一下阶段
如果poll 阶段为空闲状态时,event loop将检查timers,如果有1个或多个timer时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue
process.nextTick():会让eventLoop停下来执行nextTick中的回调,再继续执行其他任务
推荐阅读 | 链接 | 备注 |
Event Loops标准 | https://html.spec.whatwg.org/multipage/webappapis.html#event-loops | 标准对Event Loops的说明 |
Node.js 事件循环,定时器和 process.nextTick() | https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/#what-is-the-event-loop | 介绍了nodejs的事件循环和定时器 |
调用栈 | 介绍js运行调用栈 | |
JS中的栈内存堆内存 |