第04篇,前端知识之异步EventLoop

124 阅读7分钟

前言

异步编程允许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引擎线程的执行。

定时器触发线程

定时器触发线程用于处理setTimeoutsetInterval等定时器函数。当定时器的时间到达时,定时器触发线程会将定时器的回调函数放入事件队列中,等待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 语法糖

asyncawait是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来处理异步任务的使用。如果有错误或者疑问还请评论区留言。