先说一个容易被忽略的事实: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');
});
拆开来看,这个流程由三个关键动作组成:
- executor 立即执行---Promise 容器是同步建立的。耗时任务放在里面,不阻塞主线程。
- resolve 或 reject 手动触发 ---异步任务完成后,由开发者决定走成功分支还是失败分支。resolve 的数据流入
.then(),reject 的原因流入.catch()。 - 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