一、进程与线程:浏览器的底层基石

基础概念

  • 进程:操作系统中正在运行的一个程序实例,是CPU 运行指令时保存和加载上下文所需的时间与资源集合,是操作系统资源分配的最小单位。

  • 线程:CPU 执行具体指令所需的最小单位,依附于进程存在,一个进程可以包含多个线程,至少有一个线程
    在这里插入图片描述
    以上以手机为例进行理解:一个手机有一个操作系统,你会安装很多应用程序(app),手机有一定的内存,当你打开一个app时,操作系统就会分配一定的内存空间CPU运行,这个app就是一个进程

  • 浏览器中的进程与线程
    我们日常使用浏览器多开 Tab 页,本质上就是为每个 Tab 单独创建一个进程,这样做的好处是:

单个 Tab 崩溃不会影响整个浏览器
资源隔离更安全,避免恶意页面窃取其他页面数据

而在每个进程内部,又包含多个关键线程:

渲染线程:负责页面的 HTML、CSS 解析与布局绘制
JS 引擎线程:负责解析和执行 JavaScript 代码
HTTP 请求线程:处理网络请求(如 Ajax、Fetch)
事件触发线程、定时器线程等

JavaScript 可以直接操作 DOM,为了避免 DOM 渲染冲突,渲染线程与 JS 引擎线程必须互斥,不能同时工作。这也是 JS 执行会阻塞页面渲染的根本原因。具体说明如下
1. 浏览器渲染流程分为多个阶段:

  • 解析 HTML 构建 DOM 树
  • 解析 CSS 构建 CSSOM 树
  • 将 DOM 和 CSSOM 合并成渲染树
  • 计算布局(重排)
  • 绘制页面(重绘)

2. JavaScript 执行时会阻塞 DOM 渲染的原因:

  • 因为 JavaScript 可能访问或修改 DOM 属性
  • 可能读取或改变 CSS 样式
  • 这些操作都需要依赖最新的渲染树状态

3. 互斥机制的具体表现:

  • 当 JavaScript 引擎执行时,GUI 渲染线程会被挂起
  • GUI 更新会被保存在一个队列中,等待 JavaScript 执行完毕
  • 如果 JavaScript 执行时间过长,会导致页面渲染卡顿

4. 优化建议:

  • 将 JavaScript 脚本放在 body 底部
  • 使用 defer 或 async 属性异步加载脚本
  • 避免频繁操作 DOM,使用文档片段或虚拟 DOM
  • 复杂计算使用 Web Worker 在后台线程执行

二、V8 引擎:单线程与异步的诞生

V8是Chrome和Node.js所使用的JS引擎,它在执行JS代码时默认只开一个线程
正是这种单线程特性,催生了JS的异步编程模式

  • 遇到同步任务:直接执行。
  • 遇到异步任务:先挂起,存入任务队列,等待同步任务执行完毕后再执行异步任务。

这种“先同步,后异步”的执行流程,就是我们常说的事件循环的基础。

三、事件循环:微任务与宏任务

任务分类

在异步任务中,又分微任务与宏任务。
微任务:指在异步任务中耗时更短的任务,优先级更高,会在当前同步代码执行完毕后立即执行
常见的微任务有:

  • Promise.then()
  • process.nextTick() (Node.js 环境)
  • MutationObserver (浏览器环境)

宏任务:指在异步任务中耗时更长的任务,优先级较低,会在微任务全部清空后才会执行
常见的宏任务有:

  • 全局 script 代码
  • setTimeout() / setInterval()
  • AJAX请求、I/O 操作
  • UI 渲染(UI-rendering)

完整执行顺序
事件循环机制的执行流程可以总结为 4 步:

先执行同步代码,执行过程中遇到异步任务,将其存入对应的任务队列,微任务存入微任务队列,宏任务存入宏任务队列
同步代码执行完毕后,立即执行微任务队列中的所有任务
微任务全部执行结束后,如有需要则执行页面渲染
渲染完成后,执行宏任务队列中的任务

过程持续循环,直到所有任务都被处理完毕。

四、async/await

ES2017 引入async/await 语法,替代 Promise ,让异步代码看起来更像同步代码。
核心规则

  • async:函数前加 async,修饰函数(函数声明 / 表达式 /箭头函数),表示这是一个**异步函数,等价于函数内部自动返回了一个Promise** 实例对象。

异步函数的返回值会被自动包装成 Promise(即使你返回普通值,也会变成 Promise.resolve(值))
如果函数内部抛出错误,返回的 Promise 会变成 rejected 状态。

  • await:必须配合 async 使用,只能在 async 函数内部使用,作用是等待一个 Promise 完成(resolve/reject),如果 await 后面不是 Promise 对象,它就无法 “等待” 该操作完成。

    等待期间,JS 引擎会暂停当前 async 函数的执行,去执行其他代码(不会阻塞主线程)。 等 Promise 完成后,await会返回 Promise resolve 值;如果 Promise 被拒绝(reject),会抛出错误,需要用 try/catch 捕获。
    await fn() 会把 fn() 当作同步代码看待,并将 await之后的代码加入到微任务队列中,等待当前同步代码和微任务执行完毕后再执行

// async/await 基础用法 
async function asyncDemo() { 
  console.log('1. async 函数内同步代码'); 
  const res = await Promise.resolve('await 结果'); 
  console.log('3. await 之后的代码(微任务)'); 
  console.log('res:', res);
} 

console.log('0. 全局同步代码'); 
asyncDemo(); 
console.log('2. 全局同步代码结束');

//输出结果:
//0. 全局同步代码
//1. async 函数内同步代码
//2. 全局同步代码结束
//3. await 之后的代码(微任务)
//res: await 结果
上述代码示例表明:async 函数内的同步代码会立即执行,await 之后的代码会被放入微任务队列。
// async/await 处理异步请求 
async function fetchData() {
    try {
        console.log('开始请求数据');
        // 模拟网络请求 
        const response = await new Promise(resolve => {
            setTimeout(() => {
                resolve({ data: '用户信息' });
            }, 1000);
        });
        console.log('请求成功:', response.data);
        return response.data;
    } catch (err) {
        console.error('请求失败:', err);
    }
} 
fetchData().then(data => {
    console.log('最终处理数据:', data);
});
console.log('同步代码继续执行');

运行结果:

image.png
可以看到,async/await 让异步代码的写法和同步代码几乎一致,可读性大大提升。

写到最后

  1. 从进程、线程到 async/await,我们可以了解:
  2. 浏览器是多进程多线程架构,每个 Tab 是一个独立进程,内部包含渲染线程、JS 引擎线程等 V8 引擎是单线程执行
  3. JS,因此诞生了异步编程模型 事件循环是 JS异步的核心,通过同步代码优先异步代码,微任务优先于宏任务的执行顺序,保证了异步代码的有序执行
  4. async/await

理解了这些底层原理,有助于我们更好了解JavaScript中的异步编程,实现更复杂高效的功能。

部分内容参考:https://juejin.cn/post/7618784480570261544

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐