likes
comments
collection
share

前端面试:解释一下什么是JS的事件循环机制

作者站长头像
站长
· 阅读数 51

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 的主要步骤:

  1. 创建 MutationObserver 实例: 使用 MutationObserver 构造函数创建一个新的实例,同时传入一个回调函数。回调函数会在 DOM 元素的变化时被触发。
  2. 配置观察选项: 通过 MutationObserverobserve 方法配置观察选项,指定要观察的目标元素,以及要观察的变化类型。
  3. 执行回调函数: 当所观察的 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");
  1. 执行 console.log("Start") ,将其压入执行栈并输出 "Start"。
  2. 遇到 setTimeout,它是宏任务,被推入宏任务队列。
  3. 遇到 Promise.resolve().then,它是微任务,被推入微任务队列。
  4. 执行 console.log("End") ,将其压入执行栈并输出 "End"。
  5. 执行栈为空,事件循环从微任务队列中取出微任务,执行 console.log("Promise") ,输出 "Promise"。
  6. 微任务执行完毕,事件循环检查宏任务队列,取出 setTimeout 的回调函数,输出 "Timeout"。

事件循环的流程

JavaScript 引擎会不断地执行以下步骤来实现事件循环:

  • 从任务队列中取出一个任务,推入执行栈。
  • 执行栈中的任务开始执行。
  • 执行完任务后,如果栈中还有任务,继续执行下一个任务。
  • 如果栈为空,但任务队列中还有任务,将下一个任务推入执行栈。
  • 重复以上步骤,直到执行栈和任务队列都为空。

示例

让我们通过一个示例来理解事件循环的工作方式:

console.log("Hello");

setTimeout(function() {
  console.log("Async task");
}, 2000);

console.log("World");
  1. 执行 console.log("Hello") ,将其压入执行栈并输出 "Hello"。
  2. 遇到 setTimeout,它是异步任务,被推入任务队列。
  3. 继续执行 console.log("World") ,将其压入执行栈并输出 "World"。
  4. 执行栈为空,事件循环从任务队列中取出 setTimeout 的回调函数,将其推入执行栈。
  5. 执行 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
评论
请登录