浏览器事件循环(事件轮询)
1.浏览器事件循环流程
浏览器事件循环(Browser Event Loop)是浏览器用于处理用户输入、网络请求、渲染和其他异步事件的机制。这个循环确保了 JavaScript 代码的执行是非阻塞的,允许浏览器同时处理多个任务,从而提高用户体验。以下是浏览器事件循环的详细说明:
-
调用栈: 当一个 JavaScript 脚本开始执行时,它会被放入调用栈。调用栈是一个数据结构,用于跟踪执行上下文(函数调用)的堆栈。
-
同步任务: 在调用栈中的代码是同步任务,它们会按照执行的顺序逐一执行。如果有函数调用,它们会被压入调用栈,直到执行完成。
-
异步任务触发: 当浏览器遇到异步任务,例如定时器、事件监听器、网络请求等,它会将这些任务放入任务队列中。
-
任务队列: 任务队列是一个先进先出(FIFO)的数据结构,用于存储异步任务。有多个任务队列,其中包括宏任务队列和微任务队列)。
-
宏任务队列: 包括 DOM 操作、用户交互事件、定时器等。宏任务完成后,会将一个新的同步任务放入调用栈。
-
微任务队列: 包括 Promise 回调、MutationObserver 回调等。微任务会在当前宏任务执行完成后、下一个宏任务执行前执行。
-
-
事件循环(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.宏任务和微任务区别
宏任务和微任务都是异步任务的一种,但它们之间存在一些区别。
异步任务的分类:
-
宏任务: 包括整体的代码、setTimeout、setInterval、AJAX 请求、DOM 操作等。宏任务会在当前调用栈执行完毕后,从宏任务队列中取出一个任务执行。
-
微任务: 包括 Promise 的回调、MutationObserver、process.nextTick 等。微任务会在当前宏任务执行完毕后、下一个宏任务执行前触发,且微任务会在宏任务中的异步操作之前执行。
异步任务执行顺序:
- 执行同步任务,按照代码顺序逐一执行。
- 执行当前宏任务,从宏任务队列中取出一个任务执行。
- 执行微任务队列中的所有微任务。
- 重复步骤 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’ 和 ‘同步 2’。
- 然后,两个 setTimeout 中的回调函数被分别添加到宏任务队列。
- 接着,执行微任务队列中的任务,即两个 Promise 的回调函数。输出 ‘微任务 1’ 和 ‘微任务 2’。
- 再次回到宏任务队列,执行第一个 setTimeout 的回调函数,输出 ‘宏任务 1’。在这个宏任务中,又产生了一个微任务,即 Promise 的回调函数,输出 ‘微任务 1’。
- 继续执行宏任务队列,执行第二个 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
- 首先,
console.log("fn12");
执行,立即输出fn12
。 - 然后,第一个 Promise 解决 (
resolve()
)。但由于其.then()
部分(包含console.log("fn17");
)被放置在最外层,所以它将在整个 Promise 链被解析之后才执行。 - 接下来,代码进入第二个 Promise,并执行
console.log("fn13");
,立即输出fn13
。 - 第二个 Promise 被解决。但是,其
.then()
部分(包含console.log("fn16");
)同样需要等待更内层的 Promise 解决才执行。 - 然后,代码进入第三个 Promise,并执行
console.log("fn14");
,立即输出fn14
。 - 第三个 Promise 被解决,其
.then()
部分(包含console.log("fn15");
)被放置在微任务队列中,并准备执行。
同步代码执行完毕,事件循环开始处理微任务队列:
- 首先执行第三个 Promise 的
.then()
回调,输出fn15
。 - 接下来是第二个 Promise 的
.then()
回调,输出fn16
。 - 最后执行第一个 Promise 的
.then()
回调,输出fn17
。
这个顺序的关键是理解 Promise 的 .then()
部分是如何被放置在微任务队列中的,以及它们是如何根据嵌套结构被逐一解决的。每个 .then()
只有在其相应的 Promise 被解决后才会被放入队列,且内层的 Promise 必须在外层的 Promise 之前解决。因此,虽然 fn17
是第一个 .then()
的回调,但它被放置在微任务队列中的顺序实际上是在 fn15
和 fn16
之后。
4.Vue中涉及事件循环
在Vue.js中,事件循环主要涉及到Vue实例的生命周期、响应式数据的更新、以及Vue异步操作的处理。
-
Vue生命周期钩子:
- Vue实例在创建、挂载、更新、销毁等阶段都有对应的生命周期钩子函数。
- 这些生命周期钩子函数在特定的时机被触发,它们在事件循环中的执行顺序受到影响,从而影响Vue实例的行为。
- 例如,在
created
生命周期钩子中,Vue实例已经创建,但尚未挂载到DOM中。在这个阶段,可以进行一些异步操作,这些异步操作会在事件循环的下一个周期中执行。
-
数据更新响应:
- Vue的响应式系统通过数据的变化来触发视图的更新。
- 当数据发生变化时,Vue会通过事件循环的微任务队列将更新操作推送到队列中,然后在当前任务执行完成后立即执行微任务队列中的更新操作。
- 这保证了数据更新的响应性,同时避免了在同一个任务中频繁地进行DOM更新,提高了性能。
-
Vue.nextTick方法:
-
Vue.nextTick
是Vue提供的一个工具方法,用于在DOM更新后执行回调函数。 - 在某些场景下,比如修改了数据但想要立即获取更新后的DOM状态,可以使用
Vue.nextTick
来确保在下一次事件循环中执行回调。 - 这是因为DOM更新是异步的,
Vue.nextTick
会将回调函数推送到微任务队列中,确保在DOM更新后执行。
-
-
异步组件加载:
- Vue支持异步组件加载,通过
import
语法实现。当使用异步组件时,组件的加载是异步的。 - 异步组件的加载过程涉及到事件循环,确保在组件加载完成后才会进行渲染。
- 这有助于提高应用的性能,避免一开始就加载所有组件,而是按需加载。
- Vue支持异步组件加载,通过
-
Vue异步操作:
- 在Vue中,一些异步操作,比如
$nextTick
、$set
等,都涉及到事件循环的概念。 - 通过这些异步操作,Vue能够在下一个事件循环周期中执行一些需要等待的任务,以确保在适当的时机进行DOM更新或其他操作。
- 在Vue中,一些异步操作,比如
5.$nextTick
在Vue中,一个经典的例子是使用this.$nextTick
来确保在DOM更新完成后执行一些操作。这在处理DOM更新的时候非常有用,特别是当需要获取更新后的DOM状态时。
假设有一个按钮,点击按钮后触发显示一个Element UI的Modal对话框,并且想在Modal对话框显示后获取它的某些属性,例如宽度。文章来源:https://www.toymoban.com/news/detail-803318.html
<template>
<div>
<el-button @click="showModal">显示Modal</el-button>
<el-dialog :visible.sync="dialogVisible" title="我是一个Dialog">
<!-- Modal 内容 -->
<!-- ... -->
</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还未渲染完成时就尝试获取其属性的问题。文章来源地址https://www.toymoban.com/news/detail-803318.html
到了这里,关于浏览器事件循环(事件轮询)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!