浏览器事件循环(事件轮询)

浏览器事件循环(事件轮询)

文章目录

  • 浏览器事件循环(事件轮询)
    • 1.浏览器事件循环流程
    • 2.同步任务、异步任务、宏任务和微任务概念
        • 同步任务:
        • 异步任务:
        • 宏任务:
        • 微任务:
    • 3.宏任务和微任务区别
        • 异步任务的分类:
        • 异步任务执行顺序:
    • 4.Vue中涉及事件循环
    • 5.$nextTick

1.浏览器事件循环流程

浏览器事件循环(Browser Event Loop)是浏览器用于处理用户输入、网络请求、渲染和其他异步事件的机制。这个循环确保了 JavaScript 代码的执行是非阻塞的,允许浏览器同时处理多个任务,从而提高用户体验。以下是浏览器事件循环的详细说明:

  1. 调用栈: 当一个 JavaScript 脚本开始执行时,它会被放入调用栈。调用栈是一个数据结构,用于跟踪执行上下文(函数调用)的堆栈。

  2. 同步任务: 在调用栈中的代码是同步任务,它们会按照执行的顺序逐一执行。如果有函数调用,它们会被压入调用栈,直到执行完成。

  3. 异步任务触发: 当浏览器遇到异步任务,例如定时器、事件监听器、网络请求等,它会将这些任务放入任务队列中。

  4. 任务队列: 任务队列是一个先进先出(FIFO)的数据结构,用于存储异步任务。有多个任务队列,其中包括宏任务队列和微任务队列)。

    • 宏任务队列: 包括 DOM 操作、用户交互事件、定时器等。宏任务完成后,会将一个新的同步任务放入调用栈。

    • 微任务队列: 包括 Promise 回调、MutationObserver 回调等。微任务会在当前宏任务执行完成后、下一个宏任务执行前执行。

  5. 事件循环(Event Loop): 当调用栈为空时,事件循环开始工作。它会检查宏任务队列,如果有任务,将任务推入调用栈执行。执行完毕后,再检查微任务队列,如果有微任务,将其依次推入调用栈执行。这个过程会一直重复,形成一个循环。

    • 宏任务执行: 从宏任务队列中选择一个任务,执行完毕后,再选择下一个宏任务。

    • 微任务执行: 在宏任务执行完毕后,依次执行微任务队列中的所有任务。

这个事件循环的机制确保了 JavaScript 的异步执行,同时避免了阻塞主线程。这对于处理用户交互、网络请求等异步任务是非常重要的,以确保应用程序的响应性和性能。

2.同步任务、异步任务、宏任务和微任务概念

同步任务:

同步任务是按照它们被调用的顺序依次执行的任务,一个接一个地在调用栈中运行。在执行同步任务时,JavaScript 引擎会一直等待任务执行完毕,然后才继续执行下一个任务。

console.log('Task 1');
console.log('Task 2');
console.log('Task 3');
// ...
异步任务:

异步任务是不会阻塞后续代码执行的任务。它们将在将来的某个时间点执行,可以是由浏览器环境触发的事件,也可以是由开发者手动触发的异步操作。

// 异步任务示例:定时器
setTimeout(function() {
  console.log('1000ms');
}, 1000);

// 异步任务示例:事件监听器
document.addEventListener('click', function() {
  console.log('点击');
});
宏任务:

宏任务是由浏览器环境提供的任务,包括 I/O 操作、渲染、事件处理等。在事件循环的每一轮中,只会执行一个宏任务。常见的宏任务包括 setTimeout、setInterval、DOM 操作、AJAX 请求等。

// 宏任务示例:setTimeout
setTimeout(function() {
  console.log('2000ms');
}, 2000);

// 宏任务示例:AJAX 请求
fetch('https://api.abc.com/data')
  .then(response => response.json())
  .then(data => console.log('AJAX'));
微任务:

微任务是在当前宏任务执行完毕后、下一个宏任务执行前触发的任务。它们有着更高的优先级,会在宏任务中的异步操作之前执行。常见的微任务包括 Promise 的回调、MutationObserver 等。

// 微任务示例:Promise
Promise.resolve().then(function() {
  console.log('微任务1');
});

// 微任务示例:MutationObserver
const observer = new MutationObserver(function() {
  console.log('微任务2');
});

observer.observe(document.body, { attributes: true });

document.body.setAttribute('class', 'some-class');

在事件循环中,首先执行当前调用栈中的同步任务,然后检查并执行宏任务队列中的一个宏任务,接着执行微任务队列中的所有微任务。这个过程会一直重复,确保了 JavaScript 引擎的异步执行和非阻塞特性。

3.宏任务和微任务区别

宏任务和微任务都是异步任务的一种,但它们之间存在一些区别。

异步任务的分类:
  1. 宏任务: 包括整体的代码、setTimeout、setInterval、AJAX 请求、DOM 操作等。宏任务会在当前调用栈执行完毕后,从宏任务队列中取出一个任务执行。

  2. 微任务: 包括 Promise 的回调、MutationObserver、process.nextTick 等。微任务会在当前宏任务执行完毕后、下一个宏任务执行前触发,且微任务会在宏任务中的异步操作之前执行。

异步任务执行顺序:
  1. 执行同步任务,按照代码顺序逐一执行。
  2. 执行当前宏任务,从宏任务队列中取出一个任务执行。
  3. 执行微任务队列中的所有微任务。
  4. 重复步骤 2 和 3,直到宏任务队列为空。
console.log('同步 1');

// 宏任务
setTimeout(function() {
  console.log('宏任务 1');

  // 微任务
  Promise.resolve().then(function() {
    console.log('微任务 1');
  });
}, 0);

// 宏任务
setTimeout(function() {
  console.log('宏任务 2');

  // 微任务
  Promise.resolve().then(function() {
    console.log('微任务 2');
  });
}, 0);

console.log('同步 2');
//同步 1
//同步 2
//宏任务 1
//微任务 1
//宏任务 2
//微任务 2

在上面的示例中,同步任务(同步1、同步2)首先执行,然后是两个宏任务(宏任务1、宏任务2)。在每个宏任务执行后,会依次执行微任务(微任务1、微任务2)。这种执行顺序确保了微任务比宏任务更具优先级,微任务会在下一个宏任务之前执行。

  1. 首先,执行同步代码,输出 ‘同步 1’ 和 ‘同步 2’。
  2. 然后,两个 setTimeout 中的回调函数被分别添加到宏任务队列。
  3. 接着,执行微任务队列中的任务,即两个 Promise 的回调函数。输出 ‘微任务 1’ 和 ‘微任务 2’。
  4. 再次回到宏任务队列,执行第一个 setTimeout 的回调函数,输出 ‘宏任务 1’。在这个宏任务中,又产生了一个微任务,即 Promise 的回调函数,输出 ‘微任务 1’。
  5. 继续执行宏任务队列,执行第二个 setTimeout 的回调函数,输出 ‘宏任务 2’。同样,在这个宏任务中,产生了一个微任务,即 Promise 的回调函数,输出 ‘微任务 2’。

微任务总是在当前宏任务执行完毕后、下一个宏任务执行前执行。微任务 1 和 微任务 2 在它们所属的宏任务(setTimeout 的回调函数)执行完毕后才得以执行。

ps:

new Promise((resolve, reject) => {
  console.log("fn12");
  resolve();
  new Promise((resolve, reject) => {
  console.log("fn13");
  resolve();
  new Promise((resolve, reject) => {
  console.log("fn14");
  resolve();
  }).then(function () {
  console.log("fn15");
  });
  }).then(function () {
  console.log("fn16");
  });
  }).then(function () {
  console.log("fn17");
  });
//fn12
//fn13
//fn14
//fn15
//fn16
//fn17
  1. 首先,console.log("fn12"); 执行,立即输出 fn12
  2. 然后,第一个 Promise 解决 (resolve())。但由于其 .then() 部分(包含 console.log("fn17");)被放置在最外层,所以它将在整个 Promise 链被解析之后才执行。
  3. 接下来,代码进入第二个 Promise,并执行 console.log("fn13");,立即输出 fn13
  4. 第二个 Promise 被解决。但是,其 .then() 部分(包含 console.log("fn16");)同样需要等待更内层的 Promise 解决才执行。
  5. 然后,代码进入第三个 Promise,并执行 console.log("fn14");,立即输出 fn14
  6. 第三个 Promise 被解决,其 .then() 部分(包含 console.log("fn15");)被放置在微任务队列中,并准备执行。

同步代码执行完毕,事件循环开始处理微任务队列:

  1. 首先执行第三个 Promise 的 .then() 回调,输出 fn15
  2. 接下来是第二个 Promise 的 .then() 回调,输出 fn16
  3. 最后执行第一个 Promise 的 .then() 回调,输出 fn17

这个顺序的关键是理解 Promise 的 .then() 部分是如何被放置在微任务队列中的,以及它们是如何根据嵌套结构被逐一解决的。每个 .then() 只有在其相应的 Promise 被解决后才会被放入队列,且内层的 Promise 必须在外层的 Promise 之前解决。因此,虽然 fn17 是第一个 .then() 的回调,但它被放置在微任务队列中的顺序实际上是在 fn15 和 fn16 之后。

4.Vue中涉及事件循环

在Vue.js中,事件循环主要涉及到Vue实例的生命周期、响应式数据的更新、以及Vue异步操作的处理。

  1. Vue生命周期钩子:

    • Vue实例在创建、挂载、更新、销毁等阶段都有对应的生命周期钩子函数。
    • 这些生命周期钩子函数在特定的时机被触发,它们在事件循环中的执行顺序受到影响,从而影响Vue实例的行为。
    • 例如,在created生命周期钩子中,Vue实例已经创建,但尚未挂载到DOM中。在这个阶段,可以进行一些异步操作,这些异步操作会在事件循环的下一个周期中执行。
  2. 数据更新响应:

    • Vue的响应式系统通过数据的变化来触发视图的更新。
    • 当数据发生变化时,Vue会通过事件循环的微任务队列将更新操作推送到队列中,然后在当前任务执行完成后立即执行微任务队列中的更新操作。
    • 这保证了数据更新的响应性,同时避免了在同一个任务中频繁地进行DOM更新,提高了性能。
  3. Vue.nextTick方法:

    • Vue.nextTick是Vue提供的一个工具方法,用于在DOM更新后执行回调函数。
    • 在某些场景下,比如修改了数据但想要立即获取更新后的DOM状态,可以使用Vue.nextTick来确保在下一次事件循环中执行回调。
    • 这是因为DOM更新是异步的,Vue.nextTick会将回调函数推送到微任务队列中,确保在DOM更新后执行。
  4. 异步组件加载:

    • Vue支持异步组件加载,通过import语法实现。当使用异步组件时,组件的加载是异步的。
    • 异步组件的加载过程涉及到事件循环,确保在组件加载完成后才会进行渲染。
    • 这有助于提高应用的性能,避免一开始就加载所有组件,而是按需加载。
  5. Vue异步操作:

    • 在Vue中,一些异步操作,比如$nextTick$set等,都涉及到事件循环的概念。
    • 通过这些异步操作,Vue能够在下一个事件循环周期中执行一些需要等待的任务,以确保在适当的时机进行DOM更新或其他操作。

5.$nextTick

在Vue中,一个经典的例子是使用this.$nextTick来确保在DOM更新完成后执行一些操作。这在处理DOM更新的时候非常有用,特别是当需要获取更新后的DOM状态时。

假设有一个按钮,点击按钮后触发显示一个Element UI的Modal对话框,并且想在Modal对话框显示后获取它的某些属性,例如宽度。

<template>
  <div>
    <el-button @click="showModal">显示Modalel-button>
    <el-dialog :visible.sync="dialogVisible" title="我是一个Dialog">
      
      
    el-dialog>
  div>
template>

<script>
export default {
  data() {
    return {
      dialogVisible: false,
      modalWidth: null
    };
  },
  methods: {
    showModal() {
      this.dialogVisible = true;

      // 此时 Modal 还未渲染到 DOM 上
      console.log('Modal 尚未渲染到 DOM 上,此时宽度为:', this.modalWidth);

      // 使用 $nextTick 来确保在下一次事件循环中执行回调
      this.$nextTick(() => {
        // 此时 Modal 已经渲染到 DOM 上
        this.modalWidth = this.$refs.dialog.$el.clientWidth;
        console.log('Modal 已渲染到 DOM 上,宽度为:', this.modalWidth);
      });
    }
  }
};
script>

当按钮被点击时,showModal方法会设置dialogVisibletrue,显示Modal。然后使用this.$nextTick来确保在下一次事件循环中执行回调函数,这个回调函数用于获取Modal对话框的宽度。通过这种方式,能够确保在Modal渲染到DOM上后再去获取其属性,避免了在Modal还未渲染完成时就尝试获取其属性的问题。

你可能感兴趣的:(前端面试相关,前端,vue.js,javascript,事件循环,事件轮询)