js 事件循环

JavaScript事件循环机制详解
本文详细介绍了JavaScript的事件循环机制,包括同步任务、异步任务的执行顺序,宏任务和微任务的区别,以及在浏览器和Node.js环境中的事件循环区别。通过示例解释了宏任务(如setTimeout)和微任务(如Promise)的执行顺序,强调了微任务在宏任务执行完毕后立即执行。文章还提到了渲染与事件循环的关系,以及requestAnimationFrame在渲染前执行的特点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58EA9jdK-1658137917944)(https://s.poetries.work/images/20210516161332.png)]

  • 默认代码从上到下执行,执行环境通过script来执行(宏任务)
  • 在代码执行过程中,调用定时器 promise click事件…不会立即执行,需要等待当前代码全部执行完毕
  • 给异步方法划分队列,分别存放到微任务(立即存放)和宏任务(时间到了或事情发生了才存放)到队列中
  • script执行完毕后,会清空所有的微任务
  • 微任务执行完毕后,会渲染页面(不是每次都调用)
  • 再去宏任务队列中看有没有到达时间的,拿出来其中一个执行
  • 执行完毕后,按照上述步骤不停的循环

例子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vscj5VW-1658137917945)(https://s.poetries.work/images/20210516162330.png)]
在这里插入图片描述

自动执行的情况 会输出 listener1 listener2 task1 task2

在这里插入图片描述
在这里插入图片描述

如果手动点击click 会一个宏任务取出来一个个执行,先执行click的宏任务,取出微任务去执行。会输出 listener1 task1 listener2 task2

在这里插入图片描述

console.log(1)

async function asyncFunc(){
  console.log(2)
  // await xx ==> promise.resolve(()=>{console.log(3)}).then()
  // console.log(3) 放到promise.resolve或立即执行
  await console.log(3) 
  // 相当于把console.log(4)放到了then promise.resolve(()=>{console.log(3)}).then(()=>{
  //   console.log(4)
  // })
  // 微任务谁先注册谁先执行
  console.log(4)
}

setTimeout(()=>{console.log(5)})

const promise = new Promise((resolve,reject)=>{
  console.log(6)
  resolve(7)
})

promise.then(d=>{console.log(d)})

asyncFunc()

console.log(8)

// 输出 1 6 2 3 8 7 4 5

1. 浏览器事件循环

涉及面试题:异步代码执行顺序?解释一下什么是 Event Loop

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变

js代码执行过程中会有很多任务,这些任务总的分成两类:

  • 同步任务
  • 异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明:

在这里插入图片描述

我们解释一下这张图:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

那主线程执行栈何时为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数

以上就是js运行的整体流程

面试中该如何回答呢? 下面是我个人推荐的回答:

  • 首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行
  • 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
  • 当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行
  • 任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行
  • 当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
setTimeout(function() {
  console.log(1)
}, 0);
new Promise(function(resolve, reject) {
  console.log(2);
  resolve()
}).then(function() {
  console.log(3)
});
process.nextTick(function () {
  console.log(4)
})
console.log(5)
  • 第一轮:主线程开始执行,遇到setTimeout,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick,同样将回调函数扔到微任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick两个微任务,先执行哪个呢?process.nextTick指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。
  • 第二轮:从宏任务队列开始,发现setTimeout回调,输出1执行完毕,因此结果是25431

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为

在这里插入图片描述

console.log('script start');

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

console.log('script end');

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobsmacrotask 称为 task

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

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务

微任务

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

在这里插入图片描述

宏任务

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O 网络请求完成、文件读写完成事件
  • UI rendering
  • 用户交互事件(比如鼠标点击、滚动页面、放大缩小等)

宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务

在这里插入图片描述

所以正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中

  • JavaScript 引擎首先从宏任务队列(macrotask queue)中取出第一个任务
  • 执行完毕后,再将微任务(microtask queue)中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行;
  • 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。

在这里插入图片描述

总结起来就是:一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务

2. Node 中的 Event loop

当 Node.js 开始启动时,会初始化一个 Eventloop,处理输入的代码脚本,这些脚本会进行 API 异步调用,process.nextTick() 方法会开始处理事件循环。下面就是 Node.js 官网提供的 Eventloop 事件循环参考流程

  • Node 中的 Event loop 和浏览器中的不相同。
  • NodeEvent loop 分为6个阶段,它们会按照顺序反复运行

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 每次执行执行一个宏任务后会清空微任务(执行顺序和浏览器一致,在node11版本以上)
  • process.nextTick node中的微任务,当前执行栈的底部,优先级比promise要高

整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。我们来分别看下这六个阶段都做了哪些事情。

  • Timers 阶段:这个阶段执行 setTimeoutsetInterval的回调函数,简单理解就是由这两个函数启动的回调函数。
  • I/O callbacks 阶段:这个阶段主要执行系统级别的回调函数,比如 TCP 连接失败的回调。
  • idle,prepare 阶段:仅系统内部使用,你只需要知道有这 2 个阶段就可以。
  • poll 阶段poll 阶段是一个重要且复杂的阶段,几乎所有 I/O 相关的回调,都在这个阶段执行(除了setTimeoutsetIntervalsetImmediate 以及一些因为 exception 意外关闭产生的回调)。检索新的 I/O 事件,执行与 I/O 相关的回调,其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段,所有的事件循环以及回调处理都在这个阶段执行。这个阶段的主要流程如下图所示。

在这里插入图片描述

  • check 阶段setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分,如下代码所示。
const fs = require('fs');
setTimeout(() => { // 新的事件循环的起点
    console.log('1'); 
}, 0);
setImmediate( () => {
    console.log('setImmediate 1');
});
/// fs.readFile 将会在 poll 阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {
    if (err) throw err;
    console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
    console.log('poll callback');
});
// 首次事件循环执行
console.log('2');

在这一代码中有一个非常奇特的地方,就是 setImmediate 会在 setTimeout 之后输出。有以下几点原因:

  • setTimeout 如果不设置时间或者设置时间为 0,则会默认为 1ms
  • 主流程执行完成后,超过 1ms 时,会将 setTimeout 回调函数逻辑插入到待执行回调函数 poll 队列中;
  • 由于当前 poll 队列中存在可执行回调函数,因此需要先执行完,待完全执行完成后,才会执行check:setImmediate

因此这也验证了这句话,先执行回调函数,再执行 setImmediate

  • close callbacks 阶段:执行一些关闭的回调函数,如 socket.on('close', ...)

除了把 Eventloop 的宏任务细分到不同阶段外。node 还引入了一个新的任务队列 Process.nextTick()

可以认为,Process.nextTick() 会在上述各个阶段结束时,在进入下一个阶段之前立即执行(优先级甚至超过 microtask 队列)

事件循环的主要包含微任务和宏任务。具体是怎么进行循环的呢

在这里插入图片描述

  • 微任务:在 Node.js 中微任务包含 2 种——process.nextTickPromise微任务在事件循环中优先级是最高的,因此在同一个事件循环中有其他任务存在时,优先执行微任务队列。并且process.nextTick 和 Promise也存在优先级,process.nextTick 高于 Promise
  • 宏任务:在 Node.js 中宏任务包含 4 种——setTimeoutsetIntervalsetImmediateI/O。宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列

我们可以看到有一个核心的主线程,它的执行阶段主要处理三个核心逻辑。

  • 同步代码。
  • 将异步任务插入到微任务队列或者宏任务队列中。
  • 执行微任务或者宏任务的回调函数。在主线程处理回调函数的同时,也需要判断是否插入微任务和宏任务。根据优先级,先判断微任务队列是否存在任务,存在则先执行微任务,不存在则判断在宏任务队列是否有任务,有则执行。
const fs = require('fs');
// 首次事件循环执行
console.log('start');
/// 将会在新的事件循环中的阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {
    if (err) throw err;
    console.log('read file success');
});
setTimeout(() => { // 新的事件循环的起点
    console.log('setTimeout'); 
}, 0);
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
    console.log('Promise callback');
});
/// 执行 process.nextTick
process.nextTick(() => {
    console.log('nextTick callback');
});
// 首次事件循环执行
console.log('end');

分析下上面代码的执行过程

  • 第一个事件循环主线程发起,因此先执行同步代码,所以先输出 start,然后输出 end
  • 第一个事件循环主线程发起,因此先执行同步代码,所以先输出 start,然后输出 end;
  • 再从上往下分析,遇到微任务,插入微任务队列,遇到宏任务,插入宏任务队列,分析完成后,微任务队列包含:Promise.resolve 和 process.nextTick,宏任务队列包含:fs.readFile 和 setTimeout
  • 先执行微任务队列,但是根据优先级,先执行 process.nextTick 再执行 Promise.resolve,所以先输出 nextTick callback 再输出 Promise callback
  • 再执行宏任务队列,根据宏任务插入先后顺序执行 setTimeout 再执行 fs.readFile,这里需要注意,先执行 setTimeout 由于其回调时间较短,因此回调也先执行,并非是 setTimeout 先执行所以才先执行回调函数,但是它执行需要时间肯定大于 1ms,所以虽然 fs.readFile 先于setTimeout 执行,但是 setTimeout 执行更快,所以先输出 setTimeout ,最后输出 read file success
// 输出结果
start
end
nextTick callback
Promise callback
setTimeout
read file success

在这里插入图片描述

当微任务和宏任务又产生新的微任务和宏任务时,又应该如何处理呢?如下代码所示:

const fs = require('fs');
setTimeout(() => { // 新的事件循环的起点
    console.log('1'); 
    fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {
        if (err) throw err;
        console.log('read file sync success');
    });
}, 0);
/// 回调将会在新的事件循环之前
fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {
    if (err) throw err;
    console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
    console.log('poll callback');
});
// 首次事件循环执行
console.log('2');

在上面代码中,有 2 个宏任务和 1 个微任务,宏任务是 setTimeout 和 fs.readFile,微任务是 Promise.resolve

  • 整个过程优先执行主线程的第一个事件循环过程,所以先执行同步逻辑,先输出 2。
  • 接下来执行微任务,输出 poll callback
  • 再执行宏任务中的 fs.readFile 和 setTimeout,由于 fs.readFile 优先级高,先执行 fs.readFile。但是处理时间长于 1ms,因此会先执行 setTimeout 的回调函数,输出 1。这个阶段在执行过程中又会产生新的宏任务 fs.readFile,因此又将该 fs.readFile 插入宏任务队列
  • 最后由于只剩下宏任务了 fs.readFile,因此执行该宏任务,并等待处理完成后的回调,输出 read file sync success
// 结果
2
poll callback
1
read file success
read file sync success

Process.nextick() 和 Vue 的 nextick

在这里插入图片描述

Node.js 和浏览器端宏任务队列的另一个很重要的不同点是,浏览器端任务队列每轮事件循环仅出队一个回调函数接着去执行微任务队列;而 Node.js 端只要轮到执行某个宏任务队列,则会执行完队列中所有的当前任务,但是当前轮次新添加到队尾的任务则会等到下一轮次才会执行。

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout

上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 会先于其他 microtask 执行

在这里插入图片描述

setTimeout(() => {
 console.log("timer1");

 Promise.resolve().then(function() {
   console.log("promise1");
 });
}, 0);

// poll阶段执行
fs.readFile('./test',()=>{
  // 在poll阶段里面 如果有setImmediate优先执行,setTimeout处于事件循环顶端 poll下面就是setImmediate
  setTimeout(()=>console.log('setTimeout'),0)
  setImmediate(()=>console.log('setImmediate'),0)
})

process.nextTick(() => {
 console.log("nextTick");
});
// nextTick, timer1, promise1,setImmediate,setTimeout

对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,下图中的 Tick 就代表了 microtask

在这里插入图片描述

谁来启动这个循环过程,循环条件是什么?

当 Node.js 启动后,会初始化事件循环,处理已提供的输入脚本,它可能会先调用一些异步的 API、调度定时器,或者 process.nextTick(),然后再开始处理事件循环。因此可以这样理解,Node.js 进程启动后,就发起了一个新的事件循环,也就是事件循环的起点。

总结来说,Node.js 事件循环的发起点有 4 个:

  • Node.js 启动后;
  • setTimeout 回调函数;
  • setInterval 回调函数;
  • 也可能是一次 I/O 后的回调函数。

无限循环有没有终点

当所有的微任务和宏任务都清空的时候,虽然当前没有任务可执行了,但是也并不能代表循环结束了。因为可能存在当前还未回调的异步 I/O,所以这个循环是没有终点的,只要进程在,并且有新的任务存在,就会去执行

Node.js 是单线程的还是多线程的?

主线程是单线程执行的,但是 Node.js 存在多线程执行,多线程包括 setTimeout 和异步 I/O 事件。其实 Node.js 还存在其他的线程,包括垃圾回收、内存优化

EventLoop 对渲染的影响

  • 想必你之前在业务开发中也遇到过 requestIdlecallback 和 requestAnimationFrame,这两个函数在我们之前的内容中没有讲过,但是当你开始考虑它们在 Eventloop 的生命周期的哪一步触发,或者这两个方法的回调会在微任务队列还是宏任务队列执行的时候,才发现好像没有想象中那么简单。这两个方法其实也并不属于 JS 的原生方法,而是浏览器宿主环境提供的方法,因为它们牵扯到另一个问题:渲染。
  • 我们知道浏览器作为一个复杂的应用是多线程工作的,除了运行 JS 的线程外,还有渲染线程、定时器触发线程、HTTP 请求线程,等等。JS 线程可以读取并且修改 DOM,而渲染线程也需要读取 DOM,这是一个典型的多线程竞争临界资源的问题。所以浏览器就把这两个线程设计成互斥的,即同时只能有一个线程在执行
  • 渲染原本就不应该出现在 Eventloop 相关的知识体系里,但是因为 Eventloop 显然是在讨论 JS 如何运行的问题,而渲染则是浏览器另外一个线程的工作。但是 requestAnimationFrame的出现却把这两件事情给关联起来
  • 通过调用 requestAnimationFrame 我们可以在下次渲染之前执行回调函数。那下次渲染具体是哪个时间点呢?渲染和 Eventloop 有什么关系呢?
    • 简单来说,就是在每一次 Eventloop 的末尾,判断当前页面是否处于渲染时机,就是重新渲染
  • 有屏幕的硬件限制,比如 60Hz 刷新率,简而言之就是 1 秒刷新了 60 次,16.6ms 刷新一次。这个时候浏览器的渲染间隔时间就没必要小于 16.6ms,因为就算渲染了屏幕上也看不到。当然浏览器也不能保证一定会每 16.6ms 会渲染一次,因为还会受到处理器的性能、JavaScript 执行效率等其他因素影响。
  • 回到 requestAnimationFrame,这个 API 保证在下次浏览器渲染之前一定会被调用,实际上我们完全可以把它看成是一个高级版的 setInterval。它们都是在一段时间后执行回调,但是前者的间隔时间是由浏览器自己不断调整的,而后者只能由用户指定。这样的特性也决定了 requestAnimationFrame 更适合用来做针对每一帧来修改的动画效果
  • 当然 requestAnimationFrame 不是 Eventloop 里的宏任务,或者说它并不在 Eventloop 的生命周期里,只是浏览器又开放的一个在渲染之前发生的新的 hook。另外需要注意的是微任务的认知概念也需要更新,在执行 animation callback 时也有可能产生微任务(比如 promise 的 callback),会放到 animation queue 处理完后再执行。所以微任务并不是像之前说的那样在每一轮 Eventloop 后处理,而是在 JS 的函数调用栈清空后处理

但是 requestIdlecallback 却是一个更好理解的概念。当宏任务队列中没有任务可以处理时,浏览器可能存在“空闲状态”。这段空闲时间可以被 requestIdlecallback 利用起来执行一些优先级不高、不必立即执行的任务,如下图所示:

<think> 首先,用户提供了几个引用,这些是关于JavaScript事件循环的。用户的问题是想了解“JavaScript事件循环的原理、机制及应用”。所以,我需要基于这些引用构建一个清晰的回答。 引用内容总结: - [^1]: 讨论事件循环如何影响应用性能和优化策略。 - [^2]: 解释事件循环的核心机制,包括调用栈、任务队列和微任务队列。 - [^3]: 描述事件循环的基本流程,包括异步任务的处理和node.js中的事件驱动。 - [^4]: 对比node.js和浏览器中的事件循环模型,提到libuv引擎。 - [^5]: 详细说明事件循环的工作流程,包括微任务和宏任务的优先级。 我的回答需要覆盖: 1. **原理**:事件循环的基本概念,为什么需要它(因为JavaScript是单线程的)。 2. **机制**:事件循环的具体工作方式,如调用栈、任务队列(宏任务和微任务)、事件队列等。 3. **应用**:事件循环在Web开发和node.js中的应用,以及它对性能的影响和优化策略。 回答结构: - 由于用户要求“回答结构清晰,帮助用户逐步解决问题”,我应该分步解释事件循环。 - 开头简要介绍事件循环的重要性。 - 然后逐步解释原理、机制和应用。 - 最后生成相关问题(根据系统级指令)。 格式化要求: - 行内数学表达式用$...$,但这里可能用不到数学表达式,除非涉及时间或性能计算。 - 独立公式用$$...$$,同样可能不需要。 - LaTeX语法正确:确保任何数学内容正确。 - 使用中文回答。 - 生成相关问题:在末尾添加相关问题。 - 在引用段落末尾添加引用标识,比如[^1],基于提供的引用。 组织回答: 1. **引言**:简述事件循环是什么及其重要性。 2. **原理**:解释为什么JavaScript是单线程的,以及事件循环如何解决阻塞问题。 3. **机制**:详细描述事件循环的工作流程: - 调用栈(Call Stack) - 任务队列(Task Queue):宏任务(如setTimeout)和微任务(如Promise) - 事件循环循环过程 - 浏览器和node.js的不同(基于引用[4]) 4. **应用**:讨论事件循环在实践中的应用,如异步操作、性能影响和优化策略(基于引用[1])。 5. **总结**:重申关键点。 6. **相关问题**:生成3-5个相关问题。 确保真实可靠:基于标准知识和引用内容。 最终回答大纲: - 标题:JavaScript事件循环的原理、机制及应用 - 内容: 1. JavaScript事件循环简介 2. 工作原理:单线程和事件驱动 3. 核心机制: - 调用栈 - 事件队列(宏任务和微任务) - 事件循环流程 4. 在浏览器和node.js中的应用差异 5. 性能影响和优化建议 6. 总结 - 相关问题:如“事件循环如何避免阻塞UI?”“微任务和宏任务的区别?”“如何在node.js中优化事件循环?” 添加引用:在相关段落末尾添加[^1], [^2]等。 相关问题示例: 1. JavaScript事件循环中的宏任务和微任务有什么区别? 2. 如何优化JavaScript代码以减少事件循环延迟? 3. node.js事件循环和浏览器事件循环的主要差异是什么? </think> ### JavaScript事件循环的原理、机制及应用 JavaScript事件循环是处理异步操作的核心机制,它允许单线程的JavaScript执行非阻塞代码,提升应用性能和响应速度。理解这一机制对于Web开发和node.js应用至关重要[^1][^2]。下面我将逐步解释其原理、机制和应用。 #### 1. **事件循环的原理** JavaScript是单线程语言,这意味着它一次只能执行一个任务。如果所有操作都同步执行,会导致UI冻结或其他任务阻塞(如网络请求)。事件循环通过异步处理来解决这一问题: - **核心原理**:事件循环利用调用栈(Call Stack)和多个队列管理任务。同步代码直接入栈执行,异步代码则被移入队列等待执行,确保主线程不被阻塞[^3][^5]。 - **为什么需要事件循环**?浏览器和服务器环境中,用户交互或I/O操作频繁。事件循环允许JavaScript在处理异步事件(如点击、定时器或网络请求)时保持高效,避免卡顿[^1][^2]。 #### 2. **事件循环的机制** 事件循环机制基于调用栈、任务队列(包括宏任务和微任务队列)的协作。其工作流程可分为几个步骤: - **调用栈(Call Stack)**:存储当前执行的同步任务。任务依次入栈,Last-In-First-Out(LIFO)执行[^3][^5]。 - **任务队列(Task Queues)**:分为宏任务队列(macrotask queue)和微任务队列(microtask queue)。 - **宏任务**:包括setTimeout、setInterval、事件监听回调等,由事件触发线程管理[^5]。 - **微任务**:包括Promise回调、MutationObserver等,优先级高于宏任务[^5]。 - **事件循环流程**: 1. 执行所有同步代码(清空调用栈)。 2. 检查微任务队列:如有任务,全部执行完毕(直到队列为空)。 3. 执行一个宏任务(从宏任务队列取一个任务执行)。 4. 重复步骤2和3,形成循环[^2][^5]。 - **示例代码说明**: ```javascript console.log("Start"); // 同步任务,直接执行 setTimeout(() => console.log("Timeout"), 0); // 宏任务,入宏任务队列 Promise.resolve().then(() => console.log("Promise")); // 微任务,入微任务队列 console.log("End"); // 同步任务 // 输出顺序: Start -> End -> Promise -> Timeout (微任务优先) ``` - 解释:同步代码先执行,然后微任务队列优先清空,最后宏任务队列处理[^5]。 #### 3. **在不同环境中的应用** 事件循环机制在浏览器和node.js中略有差异,但核心不变: - **浏览器环境**:事件循环由浏览器引擎(如Chrome V8)处理,重点关注UI响应。例如,用户点击事件作为宏任务入队,确保页面流畅[^1][^3]。 - **node.js环境**:基于libuv引擎实现事件循环,支持高并发I/O操作。node.js将任务分为多个阶段(如timers、poll、check),优化服务器性能[^4]。例如: - 文件读取操作异步处理,避免阻塞主线程。 - 宏任务(如setTimeout)在timers阶段执行,微任务(如Promise)在poll阶段后执行[^4]。 #### 4. **性能影响与优化策略** 事件循环直接影响应用性能:事件队列堆积会导致延迟或卡顿[^1]。优化方法包括: - **减少宏任务开销**:避免密集宏任务拆分任务(如分批处理setTimeout)[^1][^5]。 - **优先使用微任务**:Promise比setTimeout更高效,减少渲染延迟[^2]。 - **避免阻塞操作**:将CPU密集型任务移入Web Workers或node.js子进程[^1]。 - **监控工具**:使用Chrome DevTools或node.js的async_hooks模块检测事件循环延迟[^4]。 #### 总结 JavaScript事件循环通过单线程结合异步队列,高效管理任务执行。其核心是宏任务和微任务的优先级机制,适用于浏览器UI响应和node.js服务器端高并发场景[^1][^4]。掌握原理有助于编写高性能代码,避免常见性能瓶颈。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小码哥・Martin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值