事件循环原理
提示:
JavaScript作为一门单线程语言,必须要理解和掌握事件循环(Event Loop)和任务队列(Task Queue)的机制,高效地处理异步任务,保证用户体验的流畅性。
事件循环概述
说明:
在事件循环中,当主线程执行完当前的同步任务后,会检查事件队列中是否有待处理的事件。如果有,主线程会取出事件并执行对应的回调函数。这个循环的过程被称为事件循环(Event Loop),它由主线程和任务队列两部分组成。主线程负责执行同步任务,而异步任务则通过任务队列进行处理。这种机制保证了异步任务在适当的时机能够插入执行,从而实现了JavaScript的
非阻塞异步执行
。
事件循环流程如下:
- 主线程读取JavaScript代码,形成相应的堆和执行栈。
- 当主线程遇到异步任务时,将其委托给对应的异步进程(如Web API)处理。
- 异步任务完成后,将相应的回调函数推入任务队列。
- 主线程执行完同步任务后,检查任务队列,如果有任务,则按照先进先出的原则将任务推入主线程执行。
- 重复执行以上步骤,形成事件循环。
同步任务
说明:
同步任务是按照代码的书写顺序一步一步执行的任务。当主线程执行同步任务时,会阻塞后续的代码执行,直到当前任务执行完成。典型的同步任务包括函数调用、变量赋值、算术运算等。例如:
console.log('Step 1');
let result = add(2, 3);
console.log(result);
console.log('Step 2');
function add(a, b) {
return a + b;
}
在上面的例子中,console.log('Step 1')
执行完毕后才会执行函数调用 add(2, 3)
,并等待 add
函数返回结果后才会继续执行后续代码。
异步任务
说明:
异步任务是在主线程执行的同时,通过回调函数或其他机制委托给其他线程或事件来处理的任务。在执行异步任务时,主线程不会等待任务完成,而是继续执行后续代码。包括:
- 回调函数
callback
Promise/async await
Generator
- 事件监听
- 发布/订阅
- 计时器
requestAnimationFrame
MutationObserver
process.nextTick
I/O
操作
不得不说,异步执行的机制使得 JavaScript 能够更好地处理耗时操作,保持页面的响应性。
在上述例子中,setTimeout
是一个异步任务,它会在1秒后将回调函数推入任务队列,而主线程不会等待这个1秒,而是继续执行后面的 console.log('End')
。当主线程的同步任务执行完成后,它会检查任务队列,将异步任务的回调函数推入执行栈,最终输出 'Timeout callback'
。
任务队列类型
- 任务队列
上面我们讨论了同步任务和异步任务的执行过程,接下来我们将进一步探讨任务队列,了解它的最小颗粒度是如何执行的。
任务队列分为宏任务队列
(macrotask queue)和微任务队列
(microtask queue)两种。JavaScript 引擎遵循事件循环的机制,在执行完当前宏任务后,会检查微任务队列,执行其中的微任务,然后再取下一个宏任务执行。这个过程不断循环,形成事件循环。
1、宏任务(Macrotasks)是一些较大粒度的任务,包括:
- 所有同步任务
- I/O操作,如文件读写、数据库数据读写等
- setTimeout、setInterval
- setImmediate(Node.js环境)
- requestAnimationFrame
- 事件监听回调函数等
- ...
2、微任务(Microtasks)是一些较小粒度、高优先级的任务,包括:
- Promise的then、catch、finally
- async/await中的代码
- Generator函数
- MutationObserver
- process.nextTick(Node.js 环境)
- ...
任务执行过程
首先,必须要明确,在JavaScript中,所有任务都在主线程上执行。任务执行过程分为同步任务和异步任务两个阶段。异步任务的处理经历两个主要阶段:Event Table
(事件表)和 Event Queue
(事件队列)。
Event Table
存储了宏任务的相关信息,包括事件监听和相应的回调函数。当特定类型的事件发生时,对应的回调函数被添加到事件队列中,等待执行。例如,你可以通过addEventListener
来将事件监听器注册到事件表上:
document.addEventListener('click', function() {
console.log('Hello world!');
});
微任务与 Event Queue 密切相关。当执行栈中的代码执行完毕后,JavaScript引擎会不断地检查事件队列。如果队列不为空,就将队列中的事件一个个取出,并执行相应的回调函数。
任务队列的执行流程可概括为:
- 同步任务在主线程排队执行,异步任务在事件队列排队等待进入主线程执行。
- 遇到宏任务则推进宏任务队列,遇到微任务则推进微任务队列。
- 执行宏任务,执行完毕后检查当前层的微任务并执行。
- 继续执行下一个宏任务,执行对应层次的微任务,直至全部执行完毕。
- 这个流程确保了异步任务能够在适当的时机插入执行,保持程序的高效性和响应性。
这个流程确保了异步任务能够在适当的时机插入执行,保持程序的高效性和响应性。
如果看到这里,还觉得有点懵,我们不妨看看下面这个示例解析,一定会让你清晰明了!!!
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
new Promise((resolve) => {
console.log(4);
resolve();
console.log(5);
}).then(() => {
console.log(6);
});
console.log(7);
执行顺序解析:1 => 3 => 4 => 5 => 7 => 6 => 2。
- 创建
Promise
实例是同步的,所以1、3、4、5、7是同步执行的。 then
方法是微任务,放入微任务队列中,在当前脚本执行完毕后立即发生。- 同步任务执行完毕后,执行微任务队列中的微任务。
- 最后,
setTimeout
放入宏任务队列,按照先进先出的原则执行。
注意:出现
async
、await
,等价于promise
、then
。
- 再来一题
搞懂这个说明你没有问题了,
事件循环并不是很难,但很重要
。
详情:
console.log(1)
console.log(2)
setTimeout(() => {
console.log(3)
}, 1000)
new Promise(r => {
console.log(4)
r(5)
console.log(6)
}).then((value) => {
console.log(value)
console.log(7)
})
new Promise((resolve, reject) => {
console.log(8)
resolve(9)
console.log(10)
}).then((value) => {
console.log(value)
console.log(11)
})
function greet () {
setTimeout(() => {
new Promise((resolve, reject) => {
reject('error')
resolve(12)
console.log(13)
}).then((value) => {
console.log(value)
}, (reason) => {
console.log(reason)
})
})
console.log(14)
}
greet()
console.log(15)
// 宏任务排队: 3↓ 13 promise↓: error
// 微任务排队: 5 7 9 11
// 结果
// 1 2 4 6 8 10 14 15 ---> 5 7 9 11 3 13 error (第一个 timeout 没有加延时时间的结果, 没有加延时,默认按照宏任务代码书写的先后顺序执行)
// 1 2 4 6 8 10 14 15 ---> 5 7 9 11 13 error 3 (第一个timeout加了延时的结果,宏任务按照时间的时间优先级执行)
- 最后一题
用
浏览器
运行看看结果验证你的结果
详情:
console.log(1)
console.log(2)
setTimeout(() => {
console.log(3)
}, 1000)
new Promise(r => {
console.log(4)
r(5)
console.log(6)
}).then((value) => {
console.log(value)
console.log(7)
})
new Promise((resolve, reject) => {
console.log(8)
resolve(9)
console.log(10)
}).then((value) => {
console.log(value)
console.log(11)
})
async function greet () {
setTimeout(() => {
new Promise((resolve, reject) => {
reject('error')
resolve(12)
console.log(13)
}).then((value) => {
console.log(value)
}, (reason) => {
console.log(reason)
})
})
console.log(14)
return 20
}
greet().then(value => {
console.log(value)
})
console.log(15)
/**先执行同步任务 -> 微任务 -> 宏任务 */