前端面试:解释一下什么是JS的事件循环机制
JavaScript的事件循环机制是管理代码执行顺序的关键机制,它可以确保同步和异步任务的有序执行,同时也需要注意合理运用以避免影响页面渲染。
执行栈和任务队列
为了理解事件循环,我们需要了解两个重要的概念:执行栈(Call Stack)和任务队列(Callback Queue)。
- 执行栈: 执行栈是用来存储当前执行上下文(函数调用)的地方。当我们调用一个函数时,它会被压入执行栈。当函数执行完毕,它会从栈中弹出。JavaScript 引擎会从栈顶依次执行任务。
- 任务队列: 任务队列是存放异步任务的地方。当异步任务完成时,它会被推送到任务队列中。
宏任务和微任务
除了普通的异步任务,JavaScript 的事件循环还涉及到宏任务和微任务的概念。这些概念有助于更细致地控制代码的执行顺序。
- 宏任务(Macro Task): 宏任务代表了一组要执行的任务,每个任务都会创建一个新的宏任务。事件循环会在执行完一个宏任务后,去检查是否有微任务需要执行,然后再从宏任务队列中取下一个宏任务。
- 微任务(Micro Task): 微任务是宏任务中的一个小任务集合,它们的优先级高于宏任务。当一个宏任务执行完毕后,在取下一个宏任务之前,会检查是否有微任务需要执行,如果有,会依次执行所有的微任务。
常见的宏任务
- setTimeout
- setInterval
- I/O
常见的微任务
- promise.then
- mutationObserver(用于监听 DOM 元素的变化并在变化发生时执行回调函数)
额外介绍一下mutationObserver这个方法
MutationObserver
是 JavaScript 中的一个异步 API,用于监听 DOM 元素的变化并在变化发生时执行回调函数。它是在 DOM 树发生变化时的一种替代方案,相比传统的事件监听,MutationObserver
提供了更强大和灵活的能力。
使用 MutationObserver 的主要步骤:
- 创建 MutationObserver 实例: 使用
MutationObserver
构造函数创建一个新的实例,同时传入一个回调函数。回调函数会在 DOM 元素的变化时被触发。 - 配置观察选项: 通过
MutationObserver
的observe
方法配置观察选项,指定要观察的目标元素,以及要观察的变化类型。 - 执行回调函数: 当所观察的 DOM 元素发生变化时,MutationObserver 实例会执行事先指定的回调函数,你可以在回调函数中执行相应的操作。
以下是一些常见的观察选项和变化类型:
childList
:观察目标元素子节点的增加、删除或更改。attributes
:观察目标元素属性的改变。characterData
:观察目标元素文本内容或注释的改变。
示例代码:
// 目标元素
const targetElement = document.getElementById('target');
// 创建 MutationObserver 实例
const observer = new MutationObserver(function(mutationsList, observer) {
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('子节点发生变化');
} else if (mutation.type === 'attributes') {
console.log('属性发生变化');
}
}
});
// 配置观察选项
const config = { attributes: true, childList: true, subtree: true };
// 开始观察目标元素
observer.observe(targetElement, config);
在这个示例中,我们创建了一个 MutationObserver
实例,观察了目标元素的子节点和属性的变化,并在回调函数中根据变化类型执行相应的操作。
MutationObserver
的优点是它可以捕获到更细粒度的 DOM 变化,同时也避免了使用传统的事件监听方式时可能出现的性能问题。它在处理复杂的 DOM 变化、监听第三方库对 DOM 的操作等方面非常有用。
示例
让我们通过一个示例来理解宏任务和微任务的执行顺序:
console.log("Start");
setTimeout(function() {
console.log("Timeout");
}, 0);
Promise.resolve().then(function() {
console.log("Promise");
});
console.log("End");
- 执行
console.log("Start")
,将其压入执行栈并输出 "Start"。 - 遇到
setTimeout
,它是宏任务,被推入宏任务队列。 - 遇到
Promise.resolve().then
,它是微任务,被推入微任务队列。 - 执行
console.log("End")
,将其压入执行栈并输出 "End"。 - 执行栈为空,事件循环从微任务队列中取出微任务,执行
console.log("Promise")
,输出 "Promise"。 - 微任务执行完毕,事件循环检查宏任务队列,取出
setTimeout
的回调函数,输出 "Timeout"。
事件循环的流程
JavaScript 引擎会不断地执行以下步骤来实现事件循环:
- 从任务队列中取出一个任务,推入执行栈。
- 执行栈中的任务开始执行。
- 执行完任务后,如果栈中还有任务,继续执行下一个任务。
- 如果栈为空,但任务队列中还有任务,将下一个任务推入执行栈。
- 重复以上步骤,直到执行栈和任务队列都为空。
示例
让我们通过一个示例来理解事件循环的工作方式:
console.log("Hello");
setTimeout(function() {
console.log("Async task");
}, 2000);
console.log("World");
- 执行
console.log("Hello")
,将其压入执行栈并输出 "Hello"。 - 遇到
setTimeout
,它是异步任务,被推入任务队列。 - 继续执行
console.log("World")
,将其压入执行栈并输出 "World"。 - 执行栈为空,事件循环从任务队列中取出
setTimeout
的回调函数,将其推入执行栈。 - 执行
console.log("Async task")
,输出 "Async task"。
练习
题目一
function app() {
setTimeout(() => {
console.log("1-1");
Promise.resolve().then(() => {
console.log("2-1");
});
});
console.log("1-2");
Promise.resolve().then(() => {
console.log("1-3");
setTimeout(() => {
console.log("3-1");
});
});
}
app();
结果
1-2
1-3
1-1
2-1
3-1
题目二
console.log('start')
setTimeout(() => {
console.log('children2')
Promise.resolve().then(() => {
console.log('children3')
})
}, 0)
new Promise(function (resolve, reject) {
console.log('children4')
setTimeout(function () {
console.log('children5')
resolve('children6')
}, 0)
}).then(res => {
console.log('children7')
setTimeout(() => {
console.log(res)
}, 0)
})
结果
start
children4
children2
children3
children5
children7
children5
题目三
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
答案
注意:async/await底层是基于Promise封装的,所以await前面的代码相当于new Promise,是同步进行的,await后面的代码相当于.then回调,才是异步进行的。
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
转载自:https://juejin.cn/post/7272181631821037627