前言
异步编程允许JavaScript在执行耗时任务(如网络请求、文件读取等)时,不会阻塞主线程的执行,从而保持页面的响应性和流畅性。这一特性使得JavaScript能够在处理复杂逻辑和大量数据时,依然保持高效和稳定。
本文旨在探讨JavaScript异步编程,从进程和线程的基本概念出发,逐步揭示浏览器渲染进程的内部工作原理。主要内容包括解析相关概念,EventLoop、Promise以及async/await等核心机制。
进程和线程
进程
进程是操作系统分配资源的最小单位,它包含了运行一个程序所需的全部信息。每个进程都有自己独立的内存空间和系统资源,进程间通信需要通过特定的机制(如管道、共享内存等)。
线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。线程间通信相对进程间通信更加高效,因为它们共享同一个进程空间。
浏览器
浏览器是一个多进程的应用程序,它包含了多个进程,每个进程都有其特定的职责。其中,最主要的进程是浏览器进程和渲染进程。
- 每一个TAB页就是一个进程
- 浏览器主进程控制其它子进程的创建和销毁,浏览器界面显示,比如用户交互、前进、后退等操作,将渲染的内容绘制到用户界面上
- 渲染进程就是我们说的浏览器内核,负责页面的渲染、脚本执行、事件处理,每个TAB页都有一个渲染进程
- 网络进程 处理网络请求、文件访问等操作
- GPU进程 用于3D绘制
- 第三方插件进程
渲染进程
渲染进程是浏览器为每个打开的网页创建的独立进程。它负责网页的渲染和展示,以及与用户的交互。渲染进程包含了多个线程,每个线程都有其特定的任务。
GUI渲染线程
GUI渲染线程负责渲染网页的HTML、CSS和图像等内容。它会解析DOM树和CSS样式表,计算布局和绘制页面。由于渲染操作需要占用大量的CPU资源,因此GUI渲染线程通常是单线程的,以避免多线程带来的复杂同步问题。
JS引擎
JS引擎(如V8引擎)是渲染进程中的一个重要组成部分,它负责执行网页中的JavaScript代码。由于JavaScript需要与DOM进行交互,因此JS引擎线程与GUI渲染线程是互斥的,即同一时间只能有一个线程在执行。当JS引擎线程执行JavaScript代码时,GUI渲染线程会被阻塞,直到JS引擎线程执行完毕。
事件触发线程
事件触发线程用于处理网页中的事件(如点击、滚动、键盘输入等)。当事件发生时,事件触发线程会将事件放入事件队列中,等待JS引擎线程的处理。由于事件触发线程与JS引擎线程是独立的,因此事件可以被立即捕获并放入队列中,而不会阻塞JS引擎线程的执行。
定时器触发线程
定时器触发线程用于处理setTimeout
和setInterval
等定时器函数。当定时器的时间到达时,定时器触发线程会将定时器的回调函数放入事件队列中,等待JS引擎线程的处理。与事件触发线程类似,定时器触发线程也是独立的,不会阻塞JS引擎线程的执行。
HTTP请求线程
HTTP请求线程用于处理网页中的网络请求(如Ajax请求、图片加载等)。当网络请求发出时,HTTP请求线程会异步地处理请求并等待响应。当响应到达时,HTTP请求线程会将响应数据放入事件队列中,等待JS引擎线程的处理。由于HTTP请求线程是异步的,因此它不会阻塞JS引擎线程的执行。
EventLoop
EventLoop是JavaScript实现异步回调的核心机制。它不断检查调用栈和任务队列(也称为消息队列),当调用栈为空时,从任务队列中取出下一个任务执行。EventLoop使得JavaScript能够在不阻塞主线程的情况下处理异步任务。
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
console.log('end');
// 输出顺序:start, end, timeout
在上述代码中,setTimeout
的回调函数被放入了任务队列中,等待EventLoop的调度。由于EventLoop会先执行调用栈中的任务,因此console.log('start')
和console.log('end')
会先被打印出来。当调用栈为空时,EventLoop会从任务队列中取出setTimeout
的回调函数并执行,因此console.log('timeout')
最后被打印出来。
下面是我自己画的一个事件轮询的图示:
宏任务和微任务
宏任务队列
宏任务一般是由宿主环境自己提供的,例如浏览器就给我们提供了定时器,ajax等。 setTimeout, setInterval, ajax,DOM事件等都属于可以归于异步宏任务队列
微任务队列
微任务是由V8引擎控制的,在创建全局执行上下文的时候,也会在V8引擎内部创建一个微任务队列,微任务在浏览器端一般指promise、async/await
document.body.style = 'background:red';
console.log(1);
setTimeout(() => {
alert(2) // 此时可以看见页面呈红色
}, 0);
Promise.resolve().then(() => {
alert(1) // 此时还不能看见页面呈红色
});
console.log(3);
结合代码可以理解及予以证明事件循环的执行流程如下:
- 执行全局代码(同步任务)。
- 检查并执行微任务队列中的所有任务,直到队列为空。
- 渲染UI(如果需要)。
- 从宏任务队列中取出下一个任务执行。
- 重复上述步骤。
Promise
Promise是JavaScript中用于处理异步操作的一种结构化方式。它解决了传统回调形式的回调地狱问题。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
let promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
resolve('操作成功');
// 或者 reject('操作失败');
}, 1000);
});
promise.then((result) => {
console.log(result); // 输出:操作成功
}).catch((error) => {
console.error(error);
});
在上述代码中,我们创建了一个Promise对象,并在其构造函数中执行了一个异步操作(setTimeout
)。当异步操作成功时,我们调用resolve
函数将Promise的状态设置为fulfilled,并将结果值传递给then
方法的回调函数。如果异步操作失败,我们可以调用reject
函数将Promise的状态设置为rejected,并将错误信息传递给catch
方法的回调函数。
Promise还支持链式调用和并行执行多个异步操作等功能,这使得异步代码更加清晰和易于维护。具体的Promise
语法可以参考阮一峰Promise
async/await 语法糖
async
和await
是ES8引入的用于简化Promise使用的关键字。它们使得异步代码看起来更像是同步代码,从而提高了代码的可读性和可维护性。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('数据加载完成');
}, 1000);
});
}
async function loadData() {
try {
let data = await fetchData();
console.log(data); // 输出:数据加载完成
} catch (error) {
console.error(error);
}
}
loadData();
在上述代码中,我们定义了一个fetchData
函数,它返回一个Promise对象。然后,我们定义了一个loadData
异步函数,并在其中使用await
关键字等待fetchData
函数的执行结果。由于await
会暂停loadData
函数的执行直到fetchData
函数返回结果(或抛出错误),因此我们可以使用try...catch
语句来捕获可能的错误。
async/await
使得异步代码更加直观和易于理解,同时保留了异步操作的非阻塞特性。它们是处理JavaScript异步编程的强大工具。具体的async/await
语法可以参考阮一峰async/await
最后
通过上述内容和图示,肯定能对JavaScript异步编程的原理和机制有一个进一步理解,并了解使用Promise和async/await
来处理异步任务的使用。如果有错误或者疑问还请评论区留言。