浏览器事件循环(Browser Event Loop)是浏览器用于处理用户输入、网络请求、渲染和其他异步事件的机制。这个循环确保了 JavaScript 代码的执行是非阻塞的,允许浏览器同时处理多个任务,从而提高用户体验。以下是浏览器事件循环的详细说明:
调用栈: 当一个 JavaScript 脚本开始执行时,它会被放入调用栈。调用栈是一个数据结构,用于跟踪执行上下文(函数调用)的堆栈。
同步任务: 在调用栈中的代码是同步任务,它们会按照执行的顺序逐一执行。如果有函数调用,它们会被压入调用栈,直到执行完成。
异步任务触发: 当浏览器遇到异步任务,例如定时器、事件监听器、网络请求等,它会将这些任务放入任务队列中。
任务队列: 任务队列是一个先进先出(FIFO)的数据结构,用于存储异步任务。有多个任务队列,其中包括宏任务队列和微任务队列)。
宏任务队列: 包括 DOM 操作、用户交互事件、定时器等。宏任务完成后,会将一个新的同步任务放入调用栈。
微任务队列: 包括 Promise 回调、MutationObserver 回调等。微任务会在当前宏任务执行完成后、下一个宏任务执行前执行。
事件循环(Event Loop): 当调用栈为空时,事件循环开始工作。它会检查宏任务队列,如果有任务,将任务推入调用栈执行。执行完毕后,再检查微任务队列,如果有微任务,将其依次推入调用栈执行。这个过程会一直重复,形成一个循环。
宏任务执行: 从宏任务队列中选择一个任务,执行完毕后,再选择下一个宏任务。
微任务执行: 在宏任务执行完毕后,依次执行微任务队列中的所有任务。
这个事件循环的机制确保了 JavaScript 的异步执行,同时避免了阻塞主线程。这对于处理用户交互、网络请求等异步任务是非常重要的,以确保应用程序的响应性和性能。
同步任务是按照它们被调用的顺序依次执行的任务,一个接一个地在调用栈中运行。在执行同步任务时,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 引擎的异步执行和非阻塞特性。
宏任务和微任务都是异步任务的一种,但它们之间存在一些区别。
宏任务: 包括整体的代码、setTimeout、setInterval、AJAX 请求、DOM 操作等。宏任务会在当前调用栈执行完毕后,从宏任务队列中取出一个任务执行。
微任务: 包括 Promise 的回调、MutationObserver、process.nextTick 等。微任务会在当前宏任务执行完毕后、下一个宏任务执行前触发,且微任务会在宏任务中的异步操作之前执行。
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 和 微任务 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
console.log("fn12");
执行,立即输出 fn12
。resolve()
)。但由于其 .then()
部分(包含 console.log("fn17");
)被放置在最外层,所以它将在整个 Promise 链被解析之后才执行。console.log("fn13");
,立即输出 fn13
。.then()
部分(包含 console.log("fn16");
)同样需要等待更内层的 Promise 解决才执行。console.log("fn14");
,立即输出 fn14
。.then()
部分(包含 console.log("fn15");
)被放置在微任务队列中,并准备执行。同步代码执行完毕,事件循环开始处理微任务队列:
.then()
回调,输出 fn15
。.then()
回调,输出 fn16
。.then()
回调,输出 fn17
。这个顺序的关键是理解 Promise 的 .then()
部分是如何被放置在微任务队列中的,以及它们是如何根据嵌套结构被逐一解决的。每个 .then()
只有在其相应的 Promise 被解决后才会被放入队列,且内层的 Promise 必须在外层的 Promise 之前解决。因此,虽然 fn17
是第一个 .then()
的回调,但它被放置在微任务队列中的顺序实际上是在 fn15
和 fn16
之后。
在Vue.js中,事件循环主要涉及到Vue实例的生命周期、响应式数据的更新、以及Vue异步操作的处理。
Vue生命周期钩子:
created
生命周期钩子中,Vue实例已经创建,但尚未挂载到DOM中。在这个阶段,可以进行一些异步操作,这些异步操作会在事件循环的下一个周期中执行。数据更新响应:
Vue.nextTick方法:
Vue.nextTick
是Vue提供的一个工具方法,用于在DOM更新后执行回调函数。Vue.nextTick
来确保在下一次事件循环中执行回调。Vue.nextTick
会将回调函数推送到微任务队列中,确保在DOM更新后执行。异步组件加载:
import
语法实现。当使用异步组件时,组件的加载是异步的。Vue异步操作:
$nextTick
、$set
等,都涉及到事件循环的概念。在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
方法会设置dialogVisible
为true
,显示Modal。然后使用this.$nextTick
来确保在下一次事件循环中执行回调函数,这个回调函数用于获取Modal对话框的宽度。通过这种方式,能够确保在Modal渲染到DOM上后再去获取其属性,避免了在Modal还未渲染完成时就尝试获取其属性的问题。