前言
当我们在编写 JavaScript 代码时,经常会涉及到异步操作,比如网络请求、定时器等。为了处理这些异步任务,并保证代码的执行顺序和正确性,JavaScript 引入了事件循环(Event Loop)机制。 事件循环是 JavaScript 异步编程的核心机制,它负责协调和调度异步任务的执行。简单来说,事件循环就像一个循环在后台不断运行,它会从任务队列中取出任务并执行,直到任务队列为空。
进程 & 线程
进程和线程是计算机操作系统中的两个重要概念。
进程
进程是指正在运行中的一个程序,它是计算机资源分配的基本单位。每个进程都有独立的内存空间、代码和数据段,并且可以拥有自己的文件句柄、网络连接等系统资源。
线程
线程则是进程中的独立执行单元,它是操作系统进行调度的基本单位。每个线程都共享进程的内存空间和系统资源,但拥有自己的堆栈和寄存器,可以独立地执行特定的代码段。
总的来说,进程是操作系统资源分配的基本单位,线程是 CPU 调度的基本单位。
可以将公司比喻为一个进程,而员工则是该进程中的线程。
公司作为一个进程,拥有独立的资源,如办公空间、设备、资金等。它有自己的目标和任务,比如提供产品或服务、实现盈利等。公司管理层相当于操作系统,负责调度和分配资源,制定规则和策略来实现公司的目标。
而员工则是公司进程中的线程,他们共享公司的资源,如办公设备、网络连接等。每个员工都有自己的专长和职责,可以独立地执行特定的任务。员工之间可以通过协作来完成更复杂的工作,也可以通过交流和共享知识来提高整个团队的效率。
类似于进程和线程之间的关系,公司中的员工可以并行执行不同的任务,从而提高工作的效率。不同的线程(员工)可以独立地执行特定的工作,并通过共享资源和协作来实现公司的目标。
异步编程
异步编程是一种处理任务的方式,它允许程序在进行耗时操作时不会被阻塞,而是继续执行其他任务。在异步编程中,任务可以分为宏任务和微任务两种类型。
宏任务(macrotask)
宏任务是一种在异步编程中用于处理耗时操作的任务类型。它们通常被放入事件队列中,按照顺序执行。
以下是一些常见的宏任务:
- 整体的 script 代码:整个脚本代码作为一个宏任务执行,即初始的全局代码。
- 定时器:使用 setTimeout 或 setInterval 注册的定时器回调函数会作为宏任务执行。它们在指定的延迟时间后被添加到宏任务队列,并按照顺序执行。
- I/O 操作:包括文件读写、网络请求等涉及输入输出的操作,它们通常是异步的,执行完成后将作为宏任务添加到队列中执行。
- UI 渲染:当浏览器需要重绘或重新布局页面时,会将相应的渲染任务作为宏任务执行。这些任务通常与用户交互和界面更新相关。
- setImmediate:在 Node.js 环境中,setImmediate 函数可以注册的回调函数也属于宏任务。它会在当前执行栈的末尾执行。
微任务(microtask)
微任务是一种在异步编程中用于处理轻量级任务的任务类型。与宏任务不同,微任务通常直接在已经运行的任务(如函数)执行完毕后立即执行,而不需要像宏任务那样等待一段时间。
以下是一些常见的微任务:
- Promise.then() 方法:Promise 对象的 then 方法返回的回调函数会被作为微任务执行。
- MutationObserver:当 DOM 树发生变化时,MutationObserver 监听器的回调函数也会被添加到微任务队列中执行。
- process.nextTick():在 Node.js 环境中,process.nextTick 函数注册的回调函数也属于微任务。它会在当前执行栈的末尾执行。
- Object.observe():当对象的属性发生变化时,Object.observe 方法注册的回调函数会被作为微任务执行。但这个方法已经被废弃。
微任务的执行优先级高于宏任务,因此在事件循环的每一轮中,当当前宏任务执行完毕后,会依次执行所有微任务,直到微任务队列为空,然后才会继续执行下一个宏任务。
事件循环(event-loop)
为了处理异步任务,JavaScript 引入了事件循环机制。事件循环会不断地从宏任务队列和微任务队列中取出任务,并按照一定的规则执行它们。
事件循环可以描述为以下步骤:
- 执行同步代码:从上到下按顺序执行当前执行栈中的同步代码,这些同步代码属于宏任务。
- 查询是否有异步任务需要执行:检查异步任务队列中是否有任务需要执行,如果有,则进入下一步。这些异步任务包括定时器回调、网络请求、事件回调等。
- 执行微任务:执行所有微任务队列中的任务。微任务会优先于宏任务执行,确保它们在下一个宏任务执行之前完成。
- 渲染页面:如果需要,浏览器会进行页面的渲染或布局操作,以更新用户界面。
- 执行宏任务:从宏任务队列中取出排在最前面的任务执行。宏任务队列中的任务可能是定时器回调、事件回调等。执行完当前宏任务后,返回第2步,开始下一轮事件循环。
看下面代码:
javascript
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(4);
resolve()
setTimeout (() => {
console.log(6);
})
}).then(() => {
console.log(5);
})
}, 1000)
console.log(3);
这段代码执行的过程如下:
- 首先同步执行
console.log(1)
,输出数字 1。 - 然后创建一个定时器,在 1000 毫秒后将回调函数添加到宏任务队列中。
- 继续同步执行
console.log(3)
,输出数字 3。 - 定时器时间未到,进入下一个事件循环。
- 时间到达后,将回调函数添加到宏任务队列中。
- 取出宏任务队列中的回调函数,同步执行
console.log(2)
,输出数字 2。 - 创建一个 Promise 对象,并将其中的回调函数添加到微任务队列中,跳过
setTimeout
中的第二个定时器。 - 在微任务队列中取出 Promise 对象的回调函数,执行
console.log(4)
,输出数字 4。 - 调用 Promise 对象的
resolve()
方法,将状态设置为 resolved,并将其中的回调函数添加到微任务队列中。 - 在微任务队列中取出 Promise 对象回调函数中的
console.log(5)
,输出数字 5。 - 再次查询是否有需要执行的宏任务和微任务,发现
setTimeout
中的第二个定时器需要执行,执行console.log(6)
,输出数字 6。
再看下面代码:
javascript
console.log('stard');
async function async1() {
await async2() // 浏览器给await开小灶提速
console.log('saync1 end');
}
async function async2() {
console.log('saync2 end');
}
async1()
setTimeout(function() {
console.log(('setTimeout'));
}, 0)
new Promise((resolve) => {
console.log('promise');
resolve()
})
.then(() => {
console.log('thn1');
})
.then(() => {
console.log('then2');
})
console.log('end');
以下是代码执行的详细顺序:
-
同步代码执行阶段:
- 执行
console.log('stard')
,打印字符串 'stard' - 调用
async1()
函数
- 执行
-
异步任务添加阶段:
- 添加宏任务:
setTimeout
,等待 0 毫秒后执行 - 添加微任务:
Promise
的回调函数,即resolve()
的执行 - 添加微任务:
async1()
的回调函数 - 添加微任务:第一个
then()
的回调函数 - 添加微任务:第二个
then()
的回调函数
- 继续同步代码执行阶段:
- 执行
console.log('end')
,打印字符串 'end' - 创建一个新的 Promise 对象,并立即执行传入的回调函数,打印字符串 'promise'
- 异步任务执行阶段:
-
执行微任务队列中的任务:
- 执行
Promise
的回调函数,打印字符串 'thn1' - 执行
async1()
的回调函数,打印字符串 'saync1 end' - 执行第二个
then()
的回调函数,打印字符串 'then2'
- 执行
-
执行宏任务队列中的任务:
- 等待 0 毫秒后执行的
setTimeout
的回调函数,打印字符串 'setTimeout'
- 等待 0 毫秒后执行的
因此,最终的输出顺序为:
- stard
- saync2 end
- promise
- end
- saync1 end
- thn1
- then2
- setTimeout
总结
事件循环是 JavaScript 异步编程的核心机制之一,它负责协调和管理 JavaScript 运行时的异步任务。
事件循环分为两个阶段:同步代码执行阶段和异步代码执行阶段。在同步代码执行阶段,JavaScript 会顺序执行所有同步代码,直到遇到异步任务(如定时器、事件监听等)。遇到异步任务时,JavaScript 会将其添加到任务队列中,并继续执行后面的同步代码。异步任务包括宏任务和微任务,其中宏任务包括定时器和 I/O 操作等,而微任务包括 Promise 的回调函数、MutationObserver 等。
在同步代码执行完毕后,JavaScript 就会开始执行异步任务。具体来说,它会先检查微任务队列中是否有任务,如果有则依次执行完所有微任务。当微任务队列为空时,JavaScript 再从宏任务队列中取出一个任务执行。执行完该任务后,再次检查微任务队列并依次执行微任务,以此类推,直到所有任务执行完毕。
需要注意的是,由于 JavaScript 是单线程的,因此在执行任何任务时都不会被其他任务打断。只有在某个任务执行完毕后,才会检查队列中是否有待执行的任务。因此,在编写 JavaScript 异步代码时,要特别注意避免长时间的同步阻塞,以免影响整个程序的性能和响应速度。
总之,事件循环是 JavaScript 异步编程的核心机制,了解其原理和执行顺序对于编写高效的异步代码至关重要。