阅读视图

发现新文章,点击刷新页面。

从进程线程到 async/await,一文吃透前端异步核心原理

事件循环(Event Loop)是 JavaScript 实现单线程非阻塞异步执行的核心机制,也是浏览器与 Node.js 环境中,JS 代码能够有序执行、处理异步任务(网络请求、定时器、DOM 事件等)的底层逻辑。

本文将从进程、线程的基础概念出发,逐步拆解浏览器渲染机制、V8 引擎单线程模型、Event Loop 事件循环,最终落地到 async/await 的原理与实践,帮助前端开发者建立完整的异步编程知识体系。

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

1. 基础概念

  • 进程:进程就是操作系统中正在运行的一个程序实例,是CPU 运行指令时保存和加载上下文所需的时间与资源集合,是操作系统资源分配的最小单位
  • 线程:CPU 执行具体指令所需的最小单位,依附于进程存在,一个进程可以包含多个线程。

2. 浏览器中的进程与线程

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

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

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

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

⚠️ 核心限制:由于 JavaScript 可以直接操作 DOM,为了避免 DOM 渲染冲突,渲染线程与 JS 引擎线程必须互斥,不能同时工作。这也是 JS 执行会阻塞页面渲染的根本原因。

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

V8是Chrome和Node.js所使用的JS引擎,它在执行JS代码时默认只开一个线程

正是这种单线程特性,催生了JS的异步编程模式

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

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

三、Event Loop:微任务与宏任务

1. 任务分类

在异步任务中,又分微任务与宏任务。

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

常见的微任务有:

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

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

常见的宏任务有:

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

2. 完整执行顺序

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

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

这个循环会一直持续,直到所有任务都被处理完毕。

四、async/await

async/await 是 ES2017 引入的语法,本质是 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 之后的代码加入到微任务队列中,等待当前同步代码和微任务执行完毕后再执行

代码示例1

// 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 之后的代码会被放入微任务队列。

代码示例2

// 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 让异步代码的写法和同步代码几乎一致,可读性大大提升

五、总结

从进程、线程到 async/await,我们可以了解:

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

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

别再乱拷贝了!JS 浅拷贝 vs 深拷贝全解析

在 JavaScript 开发中,对象拷贝是一个绕不开的核心话题。无论是状态管理、数据缓存还是函数参数传递,我们都需要谨慎处理数据的复制方式,避免因引用共享导致意外的数据修改。

本文将结合实际开发场景,详细拆解浅拷贝与深拷贝的区别、实现方式及适用场景。

一、拷贝的本质:引用 vs 新对象

JavaScript中的对象(包括数组、函数等)属于引用类型,变量存储时存储的并非是对象本身,而是对象的引用地址

  • 原始类型拷贝:直接复制值,两个变量互不影响。
  • 引用类型拷贝:如果只是简单赋值(const newObj = obj),本质是复制了对象的引用地址,新旧对象指向同一块内存,修改其中一个会直接影响另一个。

真正的 “拷贝”,是基于原对象创建一个新对象,使新对象与原对象在内存上相互独立。根据拷贝的深度,又分为浅拷贝深拷贝

二、浅拷贝:只复制第一层

浅拷贝(Shallow Copy)只会复制对象的第一层属性,如果属性值是引用类型(如子对象、数组),则仍然复制其引用地址。

核心特点

  • 新对象的第一层属性与原对象隔离。
  • 嵌套的子对象 / 数组仍共享引用,修改子对象会影响原对象

常用实现方式

1. 数组专用方法
  • Array.prototype.slice(0) :创建原数组的浅拷贝。

    const arr = [1, 2, { a: 3 }];
    const newArr = arr.slice(0);
    newArr[2].a = 4; // 会修改原数组的 arr[2].a
    
  • 扩展运算符 ... :ES6 新增,语法更简洁。

    const newArr = [...arr];
    
  • Array.prototype.concat() :合并数组并返回新数组。

    const newArr = [].concat(arr);
    

在一个空数组后拼接原数组并赋值给新数组,这个新数组就可以说是由原数组拷贝所得到的。

  • toReversed()reverse() 方法

toReversed() 反转数组,得到一个新数组reverse() 反转数组,改变原数组。通过这两个方法组合,我们就可以实现浅拷贝的效果。

const newArr=arr.toReversed().reverse()
2. 对象通用方法
  • Object.assign({}, obj) :将原对象的可枚举属性复制到新对象。

    const obj = { a: 1, b: { c: 2 } };
    const newObj = Object.assign({}, obj);
    newObj.b.c = 3; // 原对象 obj.b.c 也会变为 3
    
  • Object.assign():是 JavaScript 中用于对象属性复制与合并的核心方法,它能将一个或多个源对象可枚举属性复制到目标对象中,并返回修改后的目标对象。

    核心语法

Object.assign(target, ...sources)

target是接受属性的目标修改对象,...sources是一个或多个提供属性的对象

代码示例:

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 }; 
Object.assign(target, source); 
console.log(target); // { a: 1, b: 4, c: 5 }

若目标对象与源对象存在同名属性,后面的源对象属性会覆盖前面的

三、深拷贝:彻底隔离数据

深拷贝(Deep Copy)会递归复制对象的所有层级,包括嵌套的子对象、数组等,最终得到一个与原对象完全独立的新对象,修改新对象不会对原对象产生任何影响。

核心特点

  • 新对象与原对象在内存上完全隔离。
  • 无论修改哪一层属性,都不会影响对方。

常用实现方式

1. JSON.parse(JSON.stringify(obj))

这是最常用的 “民间” 深拷贝方案,先将对象序列化为 JSON 字符串,再反序列化为新对象。

const obj = { a: 1, b: { c: 2 } };
const newObj = JSON.parse(JSON.stringify(obj));
newObj.b.c = 3; // 原对象不受影响

局限性:无法处理函数、SymbolBigIntundefinedNaNInfinityfunction 等特殊类型,且会丢失原型链。

2. structuredClone()

浏览器原生 API,现代浏览器和 Node.js 17+ 支持,是更标准的深拷贝方案。

const newObj = structuredClone(obj);

局限性:无法拷贝函数、Symbol,也不能处理带有循环引用的对象。

四、总结

  • 浅拷贝:高效、轻量,适合处理扁平结构数据,但要注意嵌套引用的问题。
  • 深拷贝:彻底隔离数据,避免副作用,但性能开销更大。
  • 核心原则:根据数据结构和业务场景选择合适的拷贝方式,避免过度设计。

在实际开发中,我们应优先使用浅拷贝保证性能,只有在数据结构复杂且需要完全隔离时,才考虑深拷贝。理解拷贝的本质,是写出健壮、可维护的 JavaScript 代码的关键一步。

❌