前端必知:JS同步异步与Promise,终于有人讲明白了!

你是否曾经疑惑过:为什么 setTimeout 后面的代码会先执行?为什么 Promise 能解决回调地狱?JS 明明是单线程,却能处理网络请求、定时任务?本文将带你从计算机底层进程线程讲起,逐步揭开 JS 异步机制的神秘面纱,彻底掌握 Promise 的核心用法

一、进程与线程:先搞懂"董事长"和"经理"

在了解 JS 之前,我们需要先认识两个操作系统层面的概念:进程线程

  • 进程(Process) :可以理解为一个正在运行的程序实例。操作系统会为它分配独立的内存空间、文件句柄等资源。每个进程都有一个唯一的 PID(进程 ID)。就像一个董事长,负责统筹全局、分配资源。
  • 线程(Thread) :是进程内的执行单元,一个进程至少有一个线程(主线程)。线程共享进程的资源,负责具体的代码执行。就像经理,在董事长的资源支持下,真正去干活。

C++、Java 这类系统级语言,支持多进程多线程架构。它们可以同时开启多个线程并行执行任务,效率极高,但复杂度也高(需要处理锁、竞态条件等)。

而 JavaScript 的设计理念恰恰相反:简单、易用、避免复杂性。

二、JS 为什么是单线程?

浏览器中的 JavaScript 最初设计用于操作 DOM、响应用户交互。如果支持多线程同时修改同一个 DOM 节点,就会出现冲突(比如一个线程删除节点,另一个线程修改其样式)。为了避免这种复杂性,JS 选择了一条最简单的路------单线程

所谓单线程,意味着 JS 引擎在一个时刻只能执行一段代码,所有的任务都需要排队执行。

但是,单线程带来了一个严重问题:如果某段代码执行时间很长(比如网络请求、定时任务),后续代码就会被阻塞,页面会"卡死"。这显然无法接受。

于是,JS 的设计者引入了异步机制:把那些耗时操作(定时器、网络请求、事件监听)交给浏览器或 Node.js 的底层去处理,等它们完成后再把回调函数放回队列中执行。这样,主线程永远不会被阻塞。

三、JS 执行机制:同步优先,异步靠"事件循环"

JS 代码执行时,究竟发生了什么?我们来看一个最经典的例子(对应 1.js):

javascript 复制代码
// 同步代码
console.log('start');

// 异步代码
setTimeout(() => {
    console.log('222');
}, 1000)

console.log('end');

输出顺序是什么?

答案是:startend → (1秒后) 222

为什么会这样?背后的执行机制如下:

  1. 启动进程:JS 引擎启动一个进程,分配资源。
  2. 开启主线程:主线程开始执行代码。
  3. 同步任务优先 :遇到 console.log('start'),立即执行。遇到 setTimeout,浏览器会将其定时器交给 Web APIs(底层模块)处理,并把它的回调函数放入任务队列(Task Queue) 中,主线程继续往下执行。
  4. 继续执行同步任务 :执行 console.log('end')
  5. 事件循环(Event Loop)登场 :当主线程中的所有同步代码执行完毕后,事件循环会反复检查任务队列,一旦队列中有任务,就将其取出放到主线程执行。
  6. 执行异步回调 :1秒后,定时器触发,回调函数被推入任务队列,事件循环将它取出,执行 console.log('222')

核心口诀:先同步,后异步;同步跑完,再看队列。

下面这张图可以帮助你记忆:

scss 复制代码
┌─────────────┐      ┌─────────────┐
│  调用栈      │◄─────│  任务队列    │
│ (执行同步码) │      │ (存放回调)   │
└─────────────┘      └─────────────┘
       ▲                     │
       │                     │
       └────事件循环(Event Loop)────┘

更深入:宏任务与微任务

JS 中的异步任务其实分为两种队列:

  • 宏任务(MacroTask)setTimeoutsetInterval、I/O 操作、UI 渲染等。
  • 微任务(MicroTask)Promise.then/catch/finallyMutationObserver 等。

执行顺序:每执行完一个宏任务,就会清空当前所有的微任务,然后再取下一个宏任务。

javascript 复制代码
setTimeout(() => console.log('宏任务1'), 0);
Promise.resolve().then(() => console.log('微任务1'));
console.log('同步代码');
// 输出:同步代码 → 微任务1 → 宏任务1

理解这个顺序,对后面学习 Promise 非常有帮助。

四、异步流程控制:为什么需要 Promise?

假设我们有这样一个需求:先请求用户列表(A),拿到每个用户的 id 后再去请求详细信息(B)。如果用传统的回调嵌套,就会出现"回调地狱":

javascript 复制代码
getUsers((users) => {
    users.forEach(user => {
        getUserDetail(user.id, (detail) => {
            console.log(detail);
        })
    })
})

代码横向发展,难以维护。这时,ES6 引入的 Promise 闪亮登场,优雅地解决了异步流程控制问题。

五、Promise 完全指南

5.1 什么是 Promise?

Promise 是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。它提供统一的 API 来处理成功和失败的情况。

5.2 基本用法(对应 3.js

javascript 复制代码
const p = new Promise((resolve, reject) => {
    console.log('许诺言');  // 这句会立即执行!
    // 里面可以放耗时任务,比如定时器、fetch等
    setTimeout(() => {
        // 成功时调用 resolve
        // resolve(666);
        // 失败时调用 reject
        reject("网络错误");
    }, 2000)
});

console.log(p.__proto__);  // 打印 Promise 原型

p
    .then((data) => {
        console.log(data);      // 如果 resolve 被调用,这里会收到 666
        console.log('end');
    })
    .catch((err) => {
        console.log(err);       // 输出 "网络错误"
    })
    .finally(() => {
        console.log('finally'); // 无论成功失败都会执行
    });

关键点解析:

  • executor 函数new Promise(executor) 中的参数,会立即同步执行 ,所以 '许诺言' 会第一时间打印。
  • resolvereject :它们是两个函数,由 JS 引擎提供。当异步任务成功时,调用 resolve(result),会将结果传递给 then 回调;失败时调用 reject(error),将错误传递给 catch
  • thencatch :返回的都是新的 Promise 对象,支持链式调用。
  • 状态变化Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦改变,就不会再变。

5.3 实际场景:使用 fetch(对应 4.html

javascript 复制代码
console.log('start');

// fetch 底层就是 Promise
fetch('https://api.deepseek.com/chat/completions', {
    method: 'post'
}).then((response) => {
    return response.json();  // 又是一个 Promise
}).then((data) => {
    console.log(data);
}).catch((err) => {
    console.log(err);
});

console.log('end');
// 输出:start → end → (请求完成后输出 data 或 err)

fetch 发起网络请求是异步的,不会阻塞主线程,所以 'end' 会先打印。

5.4 手写 sleep 函数 ------ 用 Promise 封装 setTimeout(对应 5.html

JS 本身没有内置 sleep 函数,但我们可以用 Promise 轻松实现:

javascript 复制代码
function sleep(t) {
    return new Promise((resolve, reject) => {
        console.log('同步代码:Promise 里的 executor 立即执行');
        setTimeout(() => {
            resolve();   // 延迟 t 毫秒后,调用 resolve 让 Promise 成功
        }, t);
    });
}

// 使用 sleep
sleep(2000)
    .then(() => {
        console.log('2s 后再做');
    });

输出:

同步代码:Promise 里的 executor 立即执行 → (等待 2 秒) → 2s 后再做

这样,我们就创造了一个"延时执行"的控制流,而且不会阻塞主线程!

六、综合示例:同步异步混合场景

下面这段代码融合了同步代码、setTimeoutPromise 微任务,试着预测输出顺序:

javascript 复制代码
console.log('1');  // 同步

setTimeout(() => console.log('2'), 0);  // 宏任务

Promise.resolve().then(() => console.log('3'));  // 微任务

console.log('4');  // 同步

输出顺序:1 → 4 → 3 → 2

解析:

  1. 同步代码 14 先执行。
  2. 同步执行完毕后,事件循环检查微任务队列,发现 Promise.then 的回调,执行打印 3
  3. 微任务清空后,再取下一个宏任务(setTimeout),打印 2

掌握这个顺序,你就真正理解了 JS 的事件循环机制!

七、总结与心得

概念 核心要点
进程与线程 进程是资源分配单位(董事长),线程是执行单位(经理)。JS 单线程简化了开发。
JS 执行机制 同步任务优先执行,异步任务交给底层,完成后将回调放入任务队列,事件循环不断取出执行。
宏任务与微任务 每个宏任务执行完都会清空微任务队列。Promise 相关回调属于微任务。
Promise 容器 + 状态机,通过 resolve/reject 改变状态,用 then/catch 注册回调,优雅解决异步流程控制。
手写 sleep new Promise(resolve => setTimeout(resolve, t)) 即可实现。

希望这篇文章能帮你彻底打通 JS 异步的任督二脉。如果在实际项目中遇到复杂的异步依赖,不妨先画一下流程图,然后用 Promise 链式调用或 async/await 去实现。

相关推荐
bonechips2 小时前
JS:同步与异步,从单线程到 Promise 的编程之路
前端·javascript
先吃饱再说2 小时前
为什么 `setTimeout` 会“插队”?JS 事件循环与 Promise 通关笔记
前端·javascript·promise
Web打印2 小时前
打印PDF面单顺序会乱 使用HttpPrinter连接打印机打印PDF快递面单,顺序会乱,请问有没有碰到过这样的问题呢?是怎么解决的
javascript
uhakadotcom2 小时前
在 Python 开发中 transitions 的使用
后端·面试·github
JAVA9652 小时前
JAVA面试-并发篇 07-CAS底层原理是什么有什么缺陷如何解决
java·开发语言·面试
Web打印2 小时前
Httpprinter 2 、3 升级到 Httpprinter4、5的 注意事项
javascript
如意IT2 小时前
浏览器CDP自动化检测技术-Error和Worker
前端·javascript·自动化·chromium·指纹浏览器
右耳朵猫AI2 小时前
JS/TS周刊2026W22 | Deno 2.8、Node.js v26.2.0、Firefox 151、Storybook 10.4、npm 12.0
javascript·node.js·firefox
晓13132 小时前
【Cocos Creator 3.x】篇——第三章 脚本编程
前端·javascript·游戏引擎