欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > 【前端】事件循环专题

【前端】事件循环专题

2025/4/18 22:06:59 来源:https://blog.csdn.net/qq_41775119/article/details/147123962  浏览:    关键词:【前端】事件循环专题

引入

以下情况是为什么呢?

//q1
for (var i = 0; i < 3; i++) {setTimeout(() => {console.log(i);}, 1000);
}
// console:
// 3
// 3
// 3//q2
let name;setTimeout(() => {name = 'name';console.log(name);
}, 1000);if (name) {name = 'newname';console.log(name);
}
// console: 'name'
  • q1解释

执行栈先走正常的 for,碰到 setTimeout 时,将 setTimeout 依次放到了任务队列,最后走完 for 后,再将任务队列中的 setTimeout依次拿出放入执行栈执行。

//将var换成let即可解决
for (let i = 0; i < 3; i++) {setTimeout(() => {console.log(i);}, 1000);
}
// console:
// 0
// 1
// 2

为什么var->let就解决了?
let 在 for 中形成了独特的作用域块,当前的 i 只在本轮循环中有效,然后 setTimeout 会找到本轮最接近的 i,从而作出了正确的输出。而我们通过 var 进行的定义,它会污染全局变量,所以在 for 外层,还可以看到 i 的值。

  • q2解释

JavaScript 在碰到 setTimeout 的时候,会将它放入任务队列,等正常语句执行完后才从任务队列里取出来放入执行栈。

原因分析

浏览器的事件循环Event Loop

JavaScript 是单线程的。单线程意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。

需要事件循环的原因:如果前一个任务耗时很长,后一个任务就不得不一直等着,那么我们肯定要对这种情况做一些特殊处理,毕竟很多时候我们并不是完全希望它如此执行。

这种主线程从 “任务队列” 中读取执行事件,不断循环重复的过程,就被称为 事件循环(Event Loop)。详细来说:

  1. 所有同步任务都在主线程上执行,形成一个 “执行栈”(execution context stack)。
  2. 主线程之外,存在一个 “任务队列”(task queue),在走主流程的时候,如果碰到异步任务,那么就在 “任务队列” 中放置这个异步任务。
  3. 一旦 “执行栈” 中所有同步任务执行完毕,系统就会读取 “任务队列”,看看里面存在哪些事件。那些对应的异步任务,结束等待状态,进入执行栈,开始执行。4. 主线程不断重复上面三个步骤。
  • 宏任务 v.s. 微任务

JavaScript 的异步任务,还细分两种任务:

宏任务(Macrotask):script(整体代码)、setTimeout、setInterval、XMLHttpRequest.prototype.onload、I/O、UI 渲染

微任务(Microtask):Promise、MutationObserver

执行栈先执行同步任务->任务队列中优先执行微任务,再执行宏任务。->至于任务队列中的两个settimeout先后执行顺序每个环境输出的都不一样。这里需要实况执行一下看看。

// 位置 1
setTimeout(function () {console.log('timeout1');
}, 1000);// 位置 2
console.log('start');// 位置 3
Promise.resolve().then(function () {// 位置 5console.log('promise1');// 位置 6Promise.resolve().then(function () {console.log('promise2');});// 位置 7setTimeout(function () {// 位置 8Promise.resolve().then(function () {console.log('promise3');});// 位置 9console.log('timeout2')}, 0);
});// 位置 4
console.log('done');/*输出结果:
start
done
promise1
promise2
timeout2
promise3
timeout1
*/console.log("script start");setTimeout(function() {console.log("setTimeout---0");
}, 0);setTimeout(function() {console.log("setTimeout---200");setTimeout(function() {console.log("inner-setTimeout---0");});Promise.resolve().then(function() {console.log("promise5");});
}, 200);Promise.resolve().then(function() {console.log("promise1");}).then(function() {console.log("promise2");});
Promise.resolve().then(function() {console.log("promise3");
});
console.log("script end");/*输出结果:
script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0
*/setTimeout(function() {console.log(4);
}, 0);const promise = new Promise(function executor(resolve) {console.log(1);for (var i = 0; i < 10000; i++) {i == 9999 && resolve();}console.log(2);
}).then(function() {console.log(5);
});console.log(3);
/*输出结果:
1
2
3
5
4
*/

最后一个很奇怪,为什么不是最先是3?
Promise 构造函数的执行是同步任务。虽然 Promise 本身涉及到异步操作,但它的构造函数内的代码会同步执行,也就是会在执行栈(Call Stack)中立刻执行。而在构造函数执行过程中,调用了 resolve()。这个 resolve() 是异步的,它只是将 Promise 的状态从 “pending” 转变为 “fulfilled”,但它不会立刻执行 .then() 中的回调,而是将回调放入 微任务队列,微任务会在当前栈的同步任务执行完之后立刻执行。

浏览器 Event Loop 和 Node.js Event Loop

浏览器的 Event Loop 和 Node.js 的 Event Loop 不同。

Node.js:Node.js 的 Event Loop 是基于 libuv。libuv 已经对 Event Loop 作出了实现。libuv 是一个多平台支持库,主要用于异步 I/O。最初是为 Node.js 开发的。

浏览器:浏览器的 Event Loop 是基于 HTML5 规范的。而 HTML5 规范中只是定义了浏览器中的 Event Loop 的模型,具体实现留给了浏览器厂商。

Node.js的Event Loop

注意许多浏览器事件循环的例子放入Node.js的Event Loop,顺序将不同!

Node 端事件循环中的异步队列也是这两种:Macrotask(宏任务)队列和 Microtask(微任务)队列。

常见的 Macrotask:setTimeout、setInterval、setImmediate、I/O 操作等。

常见的 Microtask:process.nextTick、Promise.then()等。
new Promise() 构造函数依然是同步代码

nextTick 比较特殊,它存有自己的队列。并且它独立于 Event Loop,无论 Event Loop 处于何种阶段,都会在阶段结束的时候清空 nextTick 队列。
process.nextTick() 优先于其他的微任务(microtask)执行。

setTimeout & setImmediate

setTimeout
众所周知,这是一个定时器,指定 n 毫秒后执行定时器里面的内容。

setImmediate:Node.js 发现使用 setTimeout 和 setInterval 有些小弊端,所以设计了个 setImmediate,该方法被设计为一旦在当前轮询阶段完成,就执行这个脚本。

执行计时器的顺序将根据调用它们的上下文而异。如果两者都从主模块内调用,则计时器将受到进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。如果你将这两个函数放入一个 I/O 循环内调用,setImmediate 总是被有限调用。

使用 setImmediate() 相对于 setTimeout 的主要优势是:如果 setImmediate() 是在 I/O 周期内被调度的,那么它将会在任何的定时器之前执行。

  • Node.js 事件循环的工作原理

Node.js 是基于 单线程 的,它通过 事件循环 来处理并发任务。事件循环分为不同的阶段,每个阶段有不同的任务队列,事件循环会不断地检查并处理这些队列中的任务。

事件循环的 7 个阶段:

  1. Timers 阶段:

处理那些已到期的定时器(setTimeout() 和 setInterval())回调函数。定时器的回调会根据它们的延迟时间放到这个阶段。如果定时器的时间已经到达,回调就会被执行。

  1. I/O callbacks 阶段:

处理除了关闭事件、定时器和一些系统操作外的所有 I/O 回调。比如读取文件系统、网络请求的回调等。

  1. idle, prepare 阶段:

这是一个内部阶段,不会直接影响我们写的代码。它的主要作用是为即将到来的事件循环阶段做准备。

  1. poll 阶段:

这是事件循环的核心阶段,它会去执行所有的 I/O 操作回调(如果有)。它会一直等到:
没有 I/O 回调需要执行时,或
当前 I/O 队列中的回调执行完。
如果有 setImmediate() 注册的回调函数,它会在这一阶段排队执行。

  1. check 阶段:

该阶段只处理 setImmediate() 注册的回调函数。如果在 poll 阶段结束后有 setImmediate() 注册的回调,它会在这一阶段执行。

  1. close callbacks 阶段:

该阶段处理那些需要关闭的回调函数。例如,如果你有一个打开的网络连接,并且它被关闭了,关闭回调会在这个阶段执行。

  1. 微任务队列(process.nextTick()、Promise.then()):

虽然微任务(process.nextTick() 和 Promise.then())并不属于事件循环的“阶段”,它们会在每个阶段结束后立即执行,比 I/O 阶段的回调更有优先级。
这意味着,如果你在事件循环的某个阶段中注册了 process.nextTick() 或 Promise.then(),它们会在事件循环的下一个阶段执行前,立刻执行。

  • 事件循环的流程:

开始时执行同步代码,然后进入事件循环。
在每个阶段,Node.js 会检查是否有需要执行的回调函数。如果某个阶段没有回调可执行,事件循环会跳过该阶段。
如果某个阶段有回调可执行,Node.js 会执行它们并继续下一个阶段。
微任务(如 process.nextTick() 和 Promise.then()) 会在当前事件循环的每个阶段之间执行,它们优先于大多数任务。

每当事件循环完成一个宏任务阶段后,会先执行所有的微任务,然后才进入下一个阶段。微任务队列会优先执行,这就解释了为什么 process.nextTick() 和 Promise.then() 中的回调会优先执行.

setTimeout(() => {console.log('timeout');
}, 0);setImmediate(() => {console.log('immediate');
});
/*输出结果
他俩谁先谁后两个结局都正确,根据当时具体的情况
*/setTimeout(function () {console.log(1);
});
console.log(2);
process.nextTick(() => {console.log(3);
});
new Promise(function (resolve, rejected) {console.log(4);resolve()
}).then(res=>{console.log(5);
})
setImmediate(function () {console.log(6)
})
console.log('end');
/*输出结果:
2
4
end
3
5
1
6
*/

内容参考jsliang

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词