事件循环
事件循环是JavaScript运行时环境中的一种机制,用来处理异步任务和事件。负责管理代码的执行顺序,确保异步任务和事件能够被正确地处理,可以把事件循环看做一次无限循环,在每次循环中检查任务队列,执行队列中的任务,并处理事件。
举个通俗的例子🌰:
假设你是一名服务员,你的工作是不断接受顾客的订单,并将它们完成。但是你不能同时处理多个订单,你必须一次只能处理一个订单。那么你会如何处理呢?
首先,你会接受一个订单,然后按照顺序将订单完成。但是,如果有一个订单很复杂,需要花费很长时间才能完成,那么其他的订单就会等待。这种等待就好像 JavaScript 中的同步代码执行。
但是,如果你的工作是把订单交给另一个人处理,然后等待他处理完毕后再继续处理下一个订单呢?这样的话,你可以不必等待每个订单处理完毕,而是可以继续接受新的订单。这种处理方式就好像 JavaScript 中的异步代码执行。
在 JavaScript 中,事件循环就像是一个服务员,它不断地接受任务(或者称为事件),并按照特定的顺序执行它们。如果某个事件需要等待一些时间(例如,从服务器获取数据、执行动画等),事件循环就会继续执行下一个任务,而不会等待这个事件完成。
这就是事件循环的工作原理:它不断地循环遍历任务队列,检查是否有任务需要执行。如果有,就执行它;如果没有,就等待新的任务到来。
JavaScript 中的事件循环在浏览器和 Node.js 环境中都有。在浏览器中,事件循环负责处理 DOM 事件、Ajax 请求、定时器等任务;在 Node.js 环境中,事件循环负责处理 I/O 操作、文件读写、网络请求等任务。
宏任务
宏任务是指在事件循环中被放入到执行队列中的任务,例如 setTimeout、setInterval、ajax 请求、DOM 事件等。它们会在主线程执行,每次只执行一个宏任务,然后等待下一个循环。
常见的宏任务有:
- setTimeout 和 setInterval:用于设置定时任务,分别在一定的时间后执行一次或者定时循环执行。
- DOM 事件:例如 click、mouseover 等用户交互事件。
- XMLHttpRequest 和 Fetch:用于发送网络请求。
- History API 和 Location API:用于浏览器历史记录和地址栏操作。
- requestAnimationFrame:用于执行动画效果的刷新。
- 页面加载事件:如DOMContentLoaded、load 等。
- 文件读取操作:例如 FileReader、File API 等。
- Web Workers:用于在后台执行 JavaScript 代码的多线程环境。
微任务
微任务是在宏任务执行完毕后,在同一事件循环中立即执行的任务。微任务包括 Promise 的回调函数、async/await 等。微任务通常会在当前宏任务执行完毕后立即执行,然后执行下一个宏任务。
常见的微任务有:
-
process.nextTick
-
MutationObserver
-
Promise里的then、catch、finally
-
queueMicrotask(Node.js 中):Node.js 中的一个函数,用于添加微任务到微任务队列。
async和await
async/await 是 ES2017(也称为 ES8)中引入的异步编程语法,用于简化 Promise 的使用。async 函数用于定义一个返回 Promise 对象的异步函数,而 await 关键字用于暂停函数执行,等待 Promise 对象的解析。
流程分析
-
执行一个宏任务,同步代码是一个特殊的宏任务,(执行栈中没有就从任务队列中获取。执行栈:JavaScript是单线程的语言,它使用执行栈来管理代码的执行顺序,用来存储函数调用的上下文;任务队列:用来存储异步任务的队列);
-
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中;
-
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行);
-
当前宏任务执行完毕,开始检查渲染,然后渲染线程接管进行渲染;
-
渲染完毕后,JavaScript 线程继续接管,开始下一个循环。
举个例子🌰:
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4)));
Promise.resolve().then(() => console.log(5));
setTimeout(() => console.log(6));
console.log(7);
// 结果
/*
1 7 3 5 2 6 4
*/
再来一题:
Promise.resolve().then(() => {
// 微任务1
console.log('Promise1')
setTimeout(() => {
// 宏任务2
console.log('setTimeout2')
}, 0)
})
setTimeout(() => {
// 宏任务1
console.log('setTimeout1')
Promise.resolve().then(() => {
// 微任务2
console.log('Promise2')
})
}, 0)
// p1 s1 p2 s2
来一道复杂一点的:
console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);
const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
setTimeout(() => {
console.log('stack [4]')
setTimeout(() => console.log("macro [5]"), 0);
p.then(() => console.log('micro [6]'));
}, 0);
console.log("stack [7]");
});
console.log("macro [8]");
// 答案
stack [1]
macro [8]
stack [7], stack [7], stack [7]
macro [2]
macro [3]
stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]
macro [5], macro [5], macro [5]
最后最难的一题(对我来说):
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')
function handler () {
console.log('click') // 直接输出
Promise.resolve().then(_ => console.log('promise')) // 注册微任务
setTimeout(() => console.log('timeout')) // 注册宏任务
requestAnimationFrame(_ => console.log('animationFrame')) // 注册宏任务
$outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务
}
new MutationObserver(_ => {
console.log('observer')
}).observe($outer, {
attributes: true
})
$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)
//答案:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout