Promise 的本质:不是异步处理,而是流程控制

先说一个容易被忽略的事实:Promise 的 executor 是同步执行的。

javascript 复制代码
const p = new Promise((resolve, reject) => {
    console.log('ss');  // 这行同步执行,不等
    setTimeout(() => {
        reject('网络错误');
    }, 2000);
});

console.log('ss') 在 new Promise 的那一刻就跑了。两秒后才触发 reject。这两件事发生在完全不同的时间点上---而 Promise 的作用,就是让开发者能够精确控制"先做什么、后做什么、出错做什么"

这不是异步处理。这是流程控制。


JS 的执行模型:一条主线程,两条队列

先回到最基础的 1.js

javascript 复制代码
console.log('start');
setTimeout(() => {
    console.log('222');    
}, 1000);
console.log('end');

输出永远是 start → end → 222

原因在于 JS 的单线程架构。如 readme 所言:

CPU 执行时间不能霸占,几十毫秒的轮询分配给进程。JS 设计为单线程---足够简单,先把同步代码快速执行掉,异步任务放入 event loop,跳过,等同步代码清空后再回头取出来执行。

这是铁律:异步回调永远在同步代码之后。 delay 设成 0 也没用。

2.js 展示了单线程的另一个侧面:

ini 复制代码
let a = 1;
let b = 2;
let c = 3;
console.log(a+b+c);

这三个变量声明,在 C++ 或 Java 里可以多线程并发赋值。JS 不行,只能一条线串行下来。代价是执行效率不如多线程,收益是整个执行模型足够简单---执行顺序清晰到不需要思考竞态问题。

但问题是:真实世界里到处都是"不知道什么时候完成"的操作---网络请求、定时器、文件读取、事件回调。单线程怎么管理这些?

答案就是 event loop + 微任务队列。而 Promise,是微任务队列的入口。


Promise 怎么控制流程:resolve 和 reject 是两条分支

3.js 完整展示了 Promise 的流程控制模型:

typescript 复制代码
const p = new Promise((resolve, reject) => {
    console.log('ss');
    setTimeout(() => {
        reject('网络错误');
    }, 2000);
});

p.then((data) => {
    console.log(data);
}).catch((err) => {
    console.log('失败了');
    console.log(err);
}).finally(() => {
    console.log('finally');
});

拆开来看,这个流程由三个关键动作组成:

  1. executor 立即执行---Promise 容器是同步建立的。耗时任务放在里面,不阻塞主线程。
  2. resolve 或 reject 手动触发 ---异步任务完成后,由开发者决定走成功分支还是失败分支。resolve 的数据流入 .then(),reject 的原因流入 .catch()
  3. finally 无论如何都执行---流程收尾,不管成功失败。

这不是"用更优雅的写法处理回调"。这是在定义流程分支:成功路径走 then,失败路径走 catch,最终收尾走 finally。每一步都在开发者的控制之下。

对比一下裸回调的写法:

javascript 复制代码
setTimeout(() => {
    // 成功怎么办?失败怎么办?不知道,全塞这里
    // 三周后回来改,自己都看不懂
}, 2000);

裸回调的问题不是丑。是流程失控 ---发起异步任务和定义"做完之后干什么"被强制写在一起,拆不开。Promise 把它们拆开了:new Promise 负责发起,.then() .catch() 负责定义后续流程。控制权回到了开发者手里。


fetch 的流程控制:谁先谁后,一目了然

4.html 里的 fetch 调用:

typescript 复制代码
console.log('start');
console.log(fetch('https//api.deepseek.com/chat/completion', {
    method: 'post'
})).then((data) => {
    // 拿到数据后的处理
}).catch((err) => {
    console.log(err);
});
console.log('end');

fetch 返回的就是 Promise。这个设计不是偶然的---发网络请求天生就有三个不确定性:可能成功、可能失败、完成时间未知。Promise 刚好对应这三个维度:resolve 处理成功,reject 处理失败,而"什么时候完成"交给 event loop 自动调度。

整个流程的时间线:

scss 复制代码
start → fetch(立即返回Promise) → end → [网络返回] → then/catch 回调入队 → 执行
 ↑         ↑                        ↑                    ↑
同步      同步不阻塞                同步                 异步回调

关键点在第二步:fetch 调用本身不阻塞。 Promise 一返回,主线程继续往下走到 console.log('end')。网络请求在后台跑,跑完之后 Promise resolve,回调自动触发。

这就是流程控制的精髓:不用"等",用"注册"。 把后续步骤注册到 Promise 上,主线程解放,该干嘛干嘛。等条件满足了,注册的步骤自动执行。


用 Promise 造 sleep:流程控制的范本

JS 没有原生的 sleep。因为 sleep 会阻塞线程,单线程环境里一阻塞整个页面就死了。5.html 用 Promise 造了一个不会阻塞的 sleep:

javascript 复制代码
function sleep(t) {
    console.log('同步');
    const p = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, t);
    });
    return p;
}

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

拆开这个 sleep 的流程:

  • sleep(2000) --- 调用瞬间返回一个 Promise,console.log('同步') 执行,主线程不卡
  • 两秒的空档 --- 主线程完全空闲,可以响应用户操作、执行其他代码
  • resolve() 触发 --- Promise 履约,.then() 回调入微任务队列
  • console.log('2s后再做') --- 回调执行

整个过程中,主线程只做了三件事:创建 Promise、注册 then 回调、两秒后执行回调。中间的空档没有任何阻塞。

如果把 sleep 比作操作系统的进程挂起:

操作系统 Promise 版 sleep
进程创建 new Promise
进程挂起(等 I/O) setTimeout 等待中
I/O 完成,中断通知 resolve()
调度器恢复进程 then 回调入队并执行

Promise 本质上是一个运行在 JS 单线程之上的轻量级流程调度器。


控制流程,不是消除异步

关于 Promise 最大的误解,是以为 async/await "让异步变成了同步"。不是的。

javascript 复制代码
async function loadUsers() {
    const users = await fetch('/api/users').then(r => r.json());
    const details = await fetch(`/api/users/${users[0].id}/details`).then(r => r.json());
    return details;
}

await 做的事情:暂停当前 async 函数的执行 → 把控制权还给主线程 → 等 Promise resolve → 从暂停点恢复执行。

这和操作系统里协程的"协作式多任务"是同一个原理。await 不是阻塞,是让出执行权。 让出期间主线程照常运转。等 Promise 好了,再回来继续。

所以 async/await 并没有"消除异步"。它只是把 Promise 的流程控制能力写成了看起来像同步代码的样子。底层的机制完全一样:Promise → microtask 队列 → event loop 调度 → 回调执行。


Promise 这个名字不骗人。new Promise 就是一个承诺---承诺这个异步任务的结果会被妥善处理。但名字里没说的部分才是重点:Promise 让开发者在单线程语言里重新拿回了执行流的控制权。

不是"处理异步"。是"控制流程"。

代码:fe · moMo/ai_doubao_ysh - 码云 - 开源中国 日期:6-10

相关推荐
dotnet901 小时前
PDF 页面尺寸上限是 14400。iText 直接加载成功的大图可能超过这个限制,需要在 setPageSize 之前等比缩放。
前端·javascript·html
threelab1 小时前
Three.js 几何图形变换 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
云水一下1 小时前
TypeScript 从零基础到精通(七):从配置到全栈项目落地
前端·javascript·typescript
十九画生2 小时前
从同步到异步:重新理解 JavaScript 的执行机制
javascript
半个落月2 小时前
JavaScript 同步异步与 Promise 详解 —— 从 Event Loop 到手写 sleep
javascript
触底反弹2 小时前
深入理解 JavaScript 同步与异步:从 Event Loop 到 async/await
javascript
浮生望2 小时前
JavaScript 异步编程核心:从同步阻塞到 Promise 事件循环
javascript·promise
假如让我当三天老蒯2 小时前
暂时性死区是否和闭包是相背的呢(自学用)
前端·javascript
渣波2 小时前
前端开发主页面小技巧
前端·javascript