js事件循环
2026-03-18
JavaScript是一个单线程语言,这样很容易造成代码阻塞。为了解决这一问题,引出了同步异步的概念
同步:函数返回的时候就能直接获取值
异步:函数返回不能获得值,需要通过异步执行来获取
单线程的好处
- 防止js干扰ui渲染,ui渲染时js运行修改dom会影响ui渲染
- 节省内存
多线程的浏览器
js是单线程,在同一个时间只能做一件事。当js需要执行异步任务时,浏览器会另外启动一个线程去执行该任务。这是因为浏览器是多线程的。浏览器出了js引擎线程,还有定时器线程,http请求线程等线程。
当js引擎线程中需要发送数据请求,就会把这个任务交给异步http请求线程执行,等请求数据返回之后,再将callback里面需要执行的js回调交给js引擎线程去执行。所以这里的异步不是自身实现的,而是浏览器提供的能力。
事件循环
js的任务分为同步和异步
- 同步任务:在主线程上执行的任务,只有一个任务执行完后,才能执行下一个任务
- 异步任务:不进入主线程,而是放在任务队列中,若有多个任务则会在任务队列中排队,任务下一步会被放进执行栈然后主线程执行调用栈的任务
执行栈
执行栈是一个存储函数的栈结构,遵循先进后出的原则。负责跟踪所有要执行的代码。当一个函数执行完成时,就会从堆栈中弹出该执行完成函数。如果有代码需要进去执行的话,就会进行push操作。
js在按顺序执行执行栈中的方法时,每次执行一个方法,都会为它生成独有的执行环境(上下文),当这个方法执行完成后,就会销毁当前的执行环境,并从栈中弹出此方法,然后继续执行下一个方法。
任务队列
任务队列是一个存储异步任务的队列结构,当遇到异步任务时,就讲其放入任务队列中,等待当前执行栈所有同步代码执行完成之后,就会从异步任务重去除已完成的异步任务的回调并将其放入执行栈中继续执行。
宏任务和微任务
任务队列不止一种,根剧任务种类的不同,分为微任务队列和宏任务队列
- 宏任务:script(整体代码)setTimeOut、setInterval、I/O、UI交互事件、setImmendiate(Node.环境)
- 微任务:Promise、MutaionObserver、process.nextTick(Node环境)
Eventloop在处理宏任务和微任务的逻辑
- js引擎首先从宏任务队列中去除第一个任务
- 执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行,过程中产生的新的微任务也需要执行
- 然后再从宏任务队列中去下一个,执行完毕后,再次讲微任务队列中的微任务
也就是说一个Eventloop循环会处理一个宏任务和所有这次循环中产生的微任务
console.log('同步代码1');
//宏任务
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('同步代码2')
resolve()
//微任务
}).then(() => {
console.log('promise.then')
})
console.log('同步代码3');
- 遇到第一个console,是同步代码,加入执行栈执行并出站,打印“同步代码1”
- 遇到setTimeout,他是一个宏任务,加入宏任务队列
- 遇到new Promise中的consloe,使用同步代码,加入执行栈执行并出栈,打印出“同步代码2”
- 遇到Promise then,是微任务,加入微任务队列
- 遇到第三个console,是同步代码,加入执行栈执行并出栈,答应出“同步代码3”
- 此时执行栈为空,去执行任务队列中的微任务,打印出“promise.then”
- 执行完微任务队列中的任务,那去执行宏任务队列中的任务,打印出“setTimeout”
总结
- 在浏览器端,每次执行完宏任务都会去执行微任务里面的队列直到微任务队列为空
- 微任务的执行时长会影响宏任务的时长
宏任务和微任务的区别
js在遇到异步任务时,会将任务交给其他线程来执行,主线程会继续执行后面的同步任务。如setTimeout是宏任务,会交给定时器触发线程去执行,待计时结束,就会将定时器回调任务放入任务队列等待主线程来取出执行
对于微任务,浏览器引擎不需要将异步任务交给其他线程去执行,而是将任务回调存在一个队列中,当执行栈中的人物执行完之后,就去执行微任务队列中的任务,如promise.then
- 微任务:不需要特定的异步线程去执行,没有明确的异步任务去执行,只有回调
- 宏任务:需要特别的异步线程去执行,有明确的异步任务去执行,有回调