JS Promise 复习笔记
JS Promise 复习笔记 & 题库
用于 JS 面试/复习的一份 Promise 速查手册 + 题库(可直接放到任意笔记软件里)
目录
一、Promise 基础知识
1.1 Promise 是什么
- Promise 是一个表示异步操作最终结果的对象。
- 用来解决:
- 回调地狱(callback hell)
- 回调难以组合、错误难以统一处理等问题。
关键特点:
- 有三种状态:
pending/fulfilled/rejected - 状态一旦从
pending变为fulfilled或rejected就不可逆。 - 通过
.then()/.catch()/.finally()注册回调。
最常见的创建方式:
```js const p = new Promise((resolve, reject) => { // executor 执行器:同步执行 if (/* 成功 */) { resolve(value); // 让 p 变为 fulfilled } else { reject(reason); // 让 p 变为 rejected } }); ```
1.2 三种状态与状态流转
pending:进行中fulfilled:已成功rejected:已失败
状态流转:
pending -> fulfilled(调用resolve)pending -> rejected(调用reject或抛出异常)- 一旦变为
fulfilled或rejected,就锁死,不会再变(不可逆)。
1.3 then / catch / finally
1.3.1 then
p.then(onFulfilled, onRejected);
then返回一个新的 Promise(注意不是原来的那个)。- 返回值处理规则:
onFulfilled/onRejected返回 普通值 → 新 Promise 变为 fulfilled,值就是返回值。- 返回一个 Promise → 新 Promise 状态跟随该 Promise。
- 回调中 抛出异常 → 新 Promise 变为 rejected。
- 任意一个参数如果不是函数,则会被忽略(值穿透)。
1.3.2 catch
p.catch(onRejected);
- 等价于
.then(null, onRejected)。 - 通常用于链尾统一错误处理。
1.3.3 finally
p.finally(() => {
// 不接收上一个结果
});
- 无论成功还是失败都会执行。
- 不会改变链上传递的值/错误(除非 finally 里抛错或返回一个拒绝的 Promise)。
1.4 执行顺序与微任务
关键点:
new Promise(executor)里的 executor 立即同步执行。then/catch/finally注册的回调会放入 微任务队列(microtask),在当前宏任务结束后、下一个宏任务开始前执行。setTimeout回调是 宏任务。
示例:
console.log(1);
const p = new Promise((resolve) => {
console.log(2);
resolve();
console.log(3);
});
p.then(() => console.log(4));
console.log(5);
执行顺序:
- 同步:
1 2 3 5 - 微任务:
4
最终输出:1 2 3 5 4
二、Promise 进阶知识
2.1 Promise 链与值传递
Promise.resolve(1)
.then(x => {
console.log(x); // 1
return x + 1; // 返回 2
})
.then(x => {
console.log(x); // 2
return Promise.resolve(x + 1); // 返回一个 Promise
})
.then(x => {
console.log(x); // 3
});
特点:
- 每个
.then都会返回一个 新的 Promise。 - 上一个回调的返回值会传入下一个
.then。 - “值穿透”:如果
then没有传对应的回调(比如.then(null, onRejected)),或者参数不是函数,状态和值会原样传下去。
2.2 错误处理与错误冒泡
Promise.resolve()
.then(() => {
throw new Error('err in then');
})
.then(() => {
console.log('不会执行');
})
.catch(err => {
console.log('catch:', err.message); // err in then
})
.then(() => {
console.log('错误处理之后继续执行');
});
要点:
- 任意一个
then中抛出的异常,会被最近的catch捕获。 catch本身也返回 Promise,可以继续链式调用。catch后如果不再抛错,链会恢复为 fulfilled 状态。
2.3 Promise 静态方法
2.3.1 Promise.resolve / Promise.reject
Promise.resolve(42); // 立即得到一个 fulfilled 的 Promise
Promise.reject('error'); // 立即得到一个 rejected 的 Promise
Promise.resolve(promiseLike):- 如果是 Promise,直接返回;
- 如果是 thenable,会“吸收”其结果;
- 普通值则包装成 fulfilled Promise。
2.3.2 Promise.all
Promise.all([p1, p2, p3])
.then(values => {
// 所有成功,values 是结果数组
})
.catch(err => {
// 任意一个失败,立即 rejected,err 是第一个失败的原因
});
- 并发执行,等全部成功;只要有一个失败就整体失败。
- 结果顺序与数组顺序一致,与完成先后无关。
- 有任一 Promise 一直 pending,则整体一直 pending。
2.3.3 Promise.race
Promise.race([p1, p2, p3])
.then(value => {
// 第一个 settled 的结果,无论是成功还是失败
})
.catch(err => {
// 若第一个是失败
});
适合做超时控制等场景。
2.3.4 Promise.allSettled
Promise.allSettled([p1, p2, p3])
.then(results => {
// 每一项都有 status: 'fulfilled' | 'rejected'
// 以及 value / reason
});
- 等所有 Promise 都 settled(成功或失败)。
- 永远返回 fulfilled,结果里包含每项的
status和value/reason。
2.3.5 Promise.any
Promise.any([p1, p2, p3])
.then(value => {
// 第一个 fulfilled 的结果
})
.catch(aggregateError => {
// 所有都 rejected,才会进入这里
});
- 只要有一个 Promise fulfilled,就立即 fulfilled。
- 如果所有都 rejected,就返回一个
AggregateError。
2.4 事件循环与微任务顺序
典型面试题代码:
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('end');
执行顺序:
- 同步:
start→end - 微任务:
promise1→promise2 - 宏任务:
timeout
最终:start, end, promise1, promise2, timeout
2.5 async / await 与 Promise 的关系
async函数 一定返回一个 Promise。await后面可以跟 Promise 或普通值:- 如果是 Promise,会等待其 settled,然后得到结果或抛出错误;
- 如果是普通值,相当于
Promise.resolve(这个值)。
async function foo() {
try {
const res = await fetchData();
return res + 1;
} catch (e) {
console.error(e);
throw e; // 重新抛出,让调用者处理
}
}
await只是在语法上“看起来同步”,本质上还是基于 Promise + 微任务。
2.6 常见实战模式(简单版)
2.6.1 串行执行多个异步任务
const tasks = [api1, api2, api3];
tasks.reduce(
(prev, cur) => prev.then(cur),
Promise.resolve()
);
2.6.2 简易并发限制(思路)
- 核心:维护一个“正在执行中的 Promise 数量”,超出就排队。
- 面试时可以说清楚思路,或简单写出控制队列的版本(不必非常完善)。
2.6.3 Promise 实现超时
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
}
三、面试高频考点 Checklist
自测一下下面这些点是不是都能顺畅讲出来:
- Promise 三种状态 & 状态不可逆。
new Promise中 executor 是同步执行的。then/catch返回新 Promise,返回值如何影响后续状态。- 值穿透 & 错误冒泡。
- 微任务 vs 宏任务,几段常见代码的执行顺序。
Promise.all/race/allSettled/any的差异和使用场景。- async/await 的本质,错误如何捕获,如何并发多个请求。
- 如何手写简易 Promise 核心(状态机、回调队列、异步执行)。
- 实战题:实现超时、重试、并发限制等。
四、Promise 题库(含参考答案)
建议先自己做,再对答案。
4.1 基础理解题
Q1:Promise 有哪几种状态?状态之间如何变化?
参考答案:
- 状态:
pending、fulfilled、rejected。 - 流转:
pending -> fulfilled/pending -> rejected,且只会发生一次;状态一旦变为 fulfilled 或 rejected 就不可逆,不会回到 pending,也不会在 fulfilled / rejected 之间互相切换。
Q2:new Promise(fn) 中传入的函数 fn 是同步还是异步执行?
参考答案:
fn(executor)在构造 Promise 时立即同步执行。then/catch中的回调才是异步的(放入微任务队列)。
Q3:then 的两个参数分别是干什么的?如果只传一个呢?
参考答案:
- 第一个参数:成功回调
onFulfilled; - 第二个参数:失败回调
onRejected。 - 如果只传一个回调(通常是成功回调),失败会“冒泡”到后面的
catch,相当于该then没有处理错误。
Q4:catch 和 .then(null, onRejected) 有什么区别?
参考答案:
- 行为基本等价:都是在链上处理 rejected 状态。
catch可读性更好,也更符合统一错误处理习惯,链式调用时更清晰。
Q5:finally 会改变 Promise 的结果吗?
参考答案:
- 一般不会。
finally不接收上一个结果,执行完之后仍然会把之前的值/错误继续向下传。 - 只有在
finally内部抛出错误,或者返回一个 rejected Promise 时,才会改变后续链的状态。
Q6:Promise 回调是微任务还是宏任务?
参考答案:
then/catch/finally注册的回调是 微任务(microtask)。
Q7:Promise.resolve(promiseLike) 会做什么?
参考答案:
- 如果参数是 Promise:直接返回这个 Promise;
- 如果是 thenable 对象:会按照其
then方法“吸收”其状态和结果; - 如果是普通值:返回一个 fulfilled Promise,value 为该值。
Q8:Promise.all([]) 传空数组会怎样?
参考答案:
- 立即返回一个 fulfilled Promise,结果为
[]。
Q9:Promise.all 和 Promise.race 的关键区别?
参考答案:
Promise.all:- 等待列表中所有 Promise 完成;
- 只要有一个 rejected,整体就 rejected;
- 结果数组顺序与传入顺序一致。
Promise.race:- 谁先 settled(不管成功或失败),就采用谁的结果/错误;
- 常用于超时控制等场景。
Q10:async 函数默认返回什么?函数内部的返回值会怎样?
参考答案:
async函数返回一个 Promise。- 函数内部
return x等价于return Promise.resolve(x)。
4.2 代码执行顺序 / 输出题
Q11:下面代码输出什么?
console.log(1);
const p = new Promise((resolve) => {
console.log(2);
resolve();
console.log(3);
});
p.then(() => console.log(4));
console.log(5);
参考答案:
- 输出顺序:
1 2 3 5 4。 - 解析:
- 同步部分:
console.log(1)→2→3→5。 resolve()只是将 then 回调放入微任务队列,不会立刻执行;- 同步任务执行完后,执行微任务 → 打印
4。
- 同步部分:
Q12:下面代码输出什么?
Promise.resolve()
.then(() => {
console.log('A');
throw new Error('err');
})
.then(() => {
console.log('B');
})
.catch(() => {
console.log('C');
})
.then(() => {
console.log('D');
});
参考答案:
- 输出:
A C D。 - 解析:
- 第一层 then:打印
A,然后抛出错误 → 后面的 then 被跳过; - 错误被
catch捕获 → 打印C; catch处理完后返回的是 fulfilled 状态(除非再次抛错),所以最后一个then执行 → 打印D。
- 第一层 then:打印
Q13:下面代码输出什么?
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('end');
参考答案:
- 输出:
start,end,promise1,promise2,timeout。 - 解析:
- 同步:
start→end; - 微任务:
promise1→promise2; - 宏任务(下一个事件循环):
timeout。
- 同步:
Q14:下面代码输出什么?
const p1 = Promise.resolve().then(() => {
console.log('p1 then1');
return 'p1';
});
p1.then(() => {
console.log('p1 then2');
});
const p2 = Promise.resolve().then(() => {
console.log('p2 then1');
});
console.log('sync');
参考答案:
- 输出顺序:
sync,p1 then1,p2 then1,p1 then2。 - 解析:
- 同步阶段:只输出
sync; - 微任务队列入队顺序:
- 第一次
Promise.resolve().then(p1 then1); - 第二次
Promise.resolve().then(p2 then1);
- 第一次
- 依次执行微任务:
- 执行 p1 then1(打印
p1 then1),返回值'p1'让p1.then的回调(p1 then2)入队; - 执行 p2 then1(打印
p2 then1); - 执行新入队的 p1 then2(打印
p1 then2)。
- 执行 p1 then1(打印
- 同步阶段:只输出
Q15:下面的日志顺序?
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
参考答案:
输出顺序:
- 同步:
script startasync1 startasync2promise1script end
- 微任务:
async1 end(await 之后的部分)promise2
- 宏任务:
setTimeout
最终:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
Q16:下面会打印几次 “ok”?
const p = new Promise((resolve) => {
resolve('ok');
resolve('again'); // 无效
});
p.then(console.log);
p.then(console.log);
参考答案:
- 打印两次
"ok"。 - 解析:
- 状态只以第一次
resolve为准; - 同一个 Promise 可以被多次
then,每个then都会执行,对应相同的结果。
- 状态只以第一次
Q17:输出什么?
Promise.reject('error')
.then(() => {
console.log('then');
})
.catch(err => {
console.log('catch', err);
})
.then(() => {
console.log('after catch');
});
参考答案:
- 输出:
catch error
after catch
- 解析:
- 初始 rejected → 直接跳过第一个
then; catch捕获错误并打印;catch之后如果没有抛错,则链恢复为 fulfilled → 后面的then继续执行。
- 初始 rejected → 直接跳过第一个
Q18:输出什么?
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log);
参考答案:
- 输出:
1。 - 解析:
then的参数如果不是函数(例如数字、Promise 实例),会被忽略;- 等价于:
Promise.resolve(1)
.then(x => x)
.then(x => x)
.then(console.log);
- 整条链都在传递
1。
Q19:Promise.all 中如果有一个 Promise 一直 pending,会发生什么?
参考答案:
Promise.all返回的 Promise 会一直保持 pending,既不会 fulfilled 也不会 rejected。
Q20:下面代码的输出?
let count = 0;
function test() {
return new Promise(resolve => {
count++;
if (count === 1) {
resolve(test());
} else {
resolve('done');
}
});
}
test().then(console.log);
参考答案:
- 输出:
done。 - 解析:
- 第一次调用
test():count = 1,resolve(test()),参数是一个 Promise; - 内部再次调用
test():count = 2,resolve('done'); - 外层 Promise 状态跟随内层 Promise,最终结果为
'done'。
- 第一次调用
4.3 手写 & 开放题(面试高频)
下面这些题可以自己写一写,再对照思路/答案。
Q21:实现一个 sleep(ms) 函数,用 Promise 形式使用:
sleep(1000).then(() => console.log('wake up'));
参考实现:
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
Q22:实现一个带超时的请求封装 requestWithTimeout(promise, ms)。
思路:
- 使用
Promise.race,将真实请求与一个“超时 Promise”一起竞争。
参考实现:
function requestWithTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
}
Q23:用 Promise 封装一个 Node-style 回调函数(如 fs.readFile)成 readFilePromise。
通用 promisify 思路:
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};
}
Q24:手写一个简易版 Promise(说出关键点即可)
面试时可从结构上描述:
- 内部维护三个状态:
pending、fulfilled、rejected,初始为pending; - 维护两个回调队列:
onFulfilledCallbacks/onRejectedCallbacks; resolve/reject:- 如果状态仍是 pending,则切换状态;
- 按顺序执行对应队列中的回调;
then(onFulfilled, onRejected):- 返回一个新的 Promise;
- 根据当前状态:
- 如果已 fulfilled / rejected,异步执行对应回调;
- 如果 pending,把回调推入队列;
- 处理回调返回值:
- 普通值 →
resolve新 Promise; - Promise → 采用其结果(thenable 处理);
- 抛错 →
reject新 Promise。
- 普通值 →
class SimplePromise {
constructor(executor) {
// 1. 初始化状态
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
// 2. 存放回调的数组 (发布订阅)
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
// 3. 定义 resolve
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
// 还没执行的回调,统统执行
this.onResolvedCallbacks.forEach(fn => fn());
}
};
// 4. 定义 reject
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
// 5. 立即执行 executor
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
// ------------------------------------------
// 简化版 then:只管传值,不管递归
// ------------------------------------------
then(onFulfilled, onRejected) {
// 参数兜底 (值穿透)
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
// 为了链式调用,必须返回新 Promise
return new SimplePromise((resolve, reject) => {
// 封装一个通用处理函数
const handle = (callback, data) => {
// 使用 setTimeout 模拟异步 (微任务太长,用宏任务模拟即可)
setTimeout(() => {
try {
// 1. 执行用户的回调,拿到返回值 x
const x = callback(data);
// 2. 【核心简化】直接把 x 传给下一个 resolve
// (标准版这里需要递归解析 x,这里省略了,直接当做普通值传递)
resolve(x);
} catch (err) {
reject(err); // 报错就走 reject
}
});
};
// 状态判断
if (this.status === 'fulfilled') {
handle(onFulfilled, this.value);
} else if (this.status === 'rejected') {
handle(onRejected, this.reason);
} else {
// Pending 状态:订阅!存入队列
this.onResolvedCallbacks.push(() => handle(onFulfilled, this.value));
this.onRejectedCallbacks.push(() => handle(onRejected, this.reason));
}
});
}
}
Q25:使用 async/await 并发请求两个接口 api1、api2,并在两个都完成后再处理结果。
参考实现:
async function getData() {
const [res1, res2] = await Promise.all([api1(), api2()]);
// do something with res1, res2
return { res1, res2 };
}
- 对比错误写法(串行):
const a = await api1();
const b = await api2(); // 这里 api2 一定在 api1 完成后才开始
Q26:怎么用 Promise 实现“重试机制”,最多重试 3 次?
参考实现:
function retry(fn, times) {
return new Promise((resolve, reject) => {
function attempt(remaining) {
fn()
.then(resolve)
.catch(err => {
if (remaining <= 0) {
reject(err);
} else {
attempt(remaining - 1);
}
});
}
attempt(times);
});
}
- 思路:
- 包一层外部 Promise;
- 内部使用递归
attempt,失败且还有次数就继续尝试。
Q27: 实现红绿灯
const light = (duration, color) => {
return new Promise((resolve) => {
console.log(`${color} start`);
setTimeout(() => {
console.log(`${color} end`);
resolve();
}, duration);
});
};
const run = () => {
light(2000, 'red')
.then(() => light(2000, 'green'))
.then(() => light(1000, 'yellow'))
.then(run); // 或者 .then(() => run())
};
run();
进阶版本
const light = (duration, color) => {
return new Promise((resolve) => {
console.log(`${color} start`);
setTimeout(() => {
console.log(`${color} end`);
resolve();
}, duration);
});
};
async function run() {
while (true) {
await light(2000, 'red');
await light(2000, 'green');
await light(1000, 'yellow');
}
}
run();
Q28: 实现Fetch的超时控制
Q29: 实现并发控制
要求实现一个 asyncPool(limit, tasks) 函数
一、 核心解题思路:自助餐模型
想象有 limit 个盘子(并发限制),和 一锅包子(任务列表)。
- 抢盘子:一开始先派 limit 个人,每人拿个盘子去夹包子(启动 limit 个任务)。
- 占位吃:每人手里只能拿 1 个包子,吃完才能拿下一个。
- 接力跑:一旦有人吃完了(finally),他把盘子空出来了,立刻再去锅里夹下一个包子(run() 递归),直到锅空了为止。
- 最后买单:有一个服务员在旁边计数,只有当吃掉的包子数 === 包子总数时,才通知老板结账(resolve)。
二、 解题公式:1个容器,3个变量,4步逻辑
1 个容器
外层必须包裹 return new Promise((resolve) => { ... })。
3 个变量
- index:发牌员。指向下一个要领取的任务(只增不减)。
- finished:计数员。记录已经做完的任务数(用来判断何时 resolve)。
- results:记账本。用来存结果。
4 步逻辑 (在 run 函数里)
-
哨兵检查:if (index >= len) return(没包子了,收工)。
-
领任务:i = index++(闭包保存当前下标,同时指针后移)。
-
做任务:task().then(存结果).finally(下一步)。
-
递归与结束:
-
finally 里先 finished++。
-
判断:如果 finished === len
→→resolve。
-
否则:run()(继续领下一个)。
-
const asyncPool = (limit, tasks) => {
// 1. 容器:Promise
return new Promise((resolve) => {
// 2. 变量:结果数组、发牌指针、完成计数
const results = [];
let index = 0;
let finished = 0;
// 边界:空数组直接返回
if (tasks.length === 0) return resolve([]);
// 3. 核心:Worker 函数
const run = () => {
// 3.1 哨兵:没任务了就退下
if (index >= tasks.length) return;
// 3.2 领任务:闭包保存当前下标 i
const i = index;
index++; // 指针后移
// 3.3 执行
tasks[i]()
.then(res => results[i] = res)
.catch(err => results[i] = err)
.finally(() => {
// 3.4 结算与接力
finished++; // 完成一个
if (finished === tasks.length) {
resolve(results); // 全做完了,交卷
} else {
run(); // 没做完,当前线程空闲,自动去领下一个
}
});
};
// 4. 启动:先由 limit 个工人把线程占满
for (let i = 0; i < Math.min(limit, tasks.length); i++) {
run();
}
});
};
Q30:退避重试
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const retryWithBackoff = async (fn, maxRetry = 3, baseDelay = 1000) => {
let lastError
for (let i = 0; i < maxRetry; i++) {
try {
return await fn()
} catch (e) {
lastError = e
if (i === maxRetry - 1) {
throw lastError
}
const delay = Math.pow(2, i) * baseDelay
// 等待一段时间再重试
await sleep(delay)
}
}
}
Q31: 实现promisy
简单来说,promisify 就是一个转换器。
它把一个 “老式的、用回调函数接收结果的函数” (比如 Node.js 里的 fs.readFile),变成一个 “现代的、返回 Promise 的函数” 。
/**
* @param {Function} originalFn 原生只要接收回调的函数
* @return {Function} 返回一个返回 Promise 的新函数
*/
function promisify(originalFn) {
// 1. 返回一个新的函数(高阶函数)
return function(...args) {
// 2. 这个新函数必须返回一个 Promise
return new Promise((resolve, reject) => {
// 3. 我们定义一个“替身回调”,用来替换原本调用者应该传的回调
const callback = (err, data) => {
// Node.js 风格:第一个参数是 error
if (err) {
reject(err); // 有错就 reject
} else {
resolve(data); // 没错就 resolve 结果
}
};
// 4. 调用原函数
// 关键点 A: 拼接参数。原参数 (...args) + 我们的替身回调
// 关键点 B: 绑定 this。使用 .call 或 .apply 保证原函数的 this 不丢失
originalFn.apply(this, [...args, callback]);
});
};
}
总结
promisify 的实现公式:
闭包包装 + Promise 容器 + 拦截回调 + Apply 调用
你只需要记住:我们在 Promise 内部生成了一个回调函数,把它塞给原函数。当原函数执行完调用这个回调时,我们将结果通过 Promise 的 resolve/reject 弹射出去。
Q32: 实现 Scheduler 类,add(promiseCreator),同时运行任务最多 N 个。
- limit (窗口数) :假设银行只有 2 个窗口。
- add (取号) :客户来了,取个号。
- 直接办理:如果窗口有人空闲(运行任务数 < limit),直接去办理。
- 排队:如果窗口都满了,客户就坐在休息区等候(放入 queue 队列)。
- 叫号:一旦某个窗口的人办完了(finally),柜员就会对着休息区喊:“下一位!”(从队列取出一个任务继续运行)。
class Schedule {
constructor(limit) {
this.limit = limit;
this.taskList = [];
this.currentProcess = 0;
}
add(promiseTask) {
return new Promise((resolve, reject) => {
// 存入队列,等待调度
this.taskList.push({
task: promiseTask,
resolve,
reject
});
// 尝试去跑
this.run();
});
}
run() {
// 1. 【门卫检查】:如果满员了,或者队列空了,直接停止,不许往下走
if (this.currentProcess >= this.limit || this.taskList.length === 0) {
return;
}
// 2. 【取出任务】:通过了门卫检查,才能拿任务
const currentTask = this.taskList.shift();
const { task, resolve, reject } = currentTask;
// 3. 【占用名额】
this.currentProcess++;
// 4. 【执行任务】
task()
.then(res => resolve(res)) // 成功,通过“遥控器”告诉外面
.catch(err => reject(err)) // 失败
.finally(() => {
// 5. 【释放名额 & 递归】
this.currentProcess--;
this.run();
});
}
}
五、附录:典型代码片段
建议自己手打一遍,加深记忆。
5.1 经典 async/await + Promise 执行顺序题
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
输出顺序(务必自己先做,再对照):
script start async1 start async2 promise1 script end async1 end promise2 setTimeout
使用建议:
-
面试前按顺序看:
- 第一章 + 第二章 → 概念、执行顺序、API 全过一遍;
- 第三章 checklist → 自查哪些点还说不完整;
- 第四章题库 → 闭卷做完,再看答案;
- 最后回到附录代码,再手写几遍。
-
真正被问到时:
- 先画出“同步 → 微任务 → 宏任务”的大致顺序;
- 逐行模拟执行,心态放松,不急着一口气报答案。
首先模拟实现一个有问题的版本
function Bromise(executor) {
var onResolve_ = null
var onReject_ = null
//模拟实现resolve和then,暂不支持rejcet
this.then = function (onResolve, onReject) {
onResolve_ = onResolve
};
function resolve(value) {
//setTimeout(()=>{
onResolve_(value)
// },0)
}
executor(resolve, null);
}
开始执行
function executor(resolve, reject) {
resolve(100)
}
//将Promise改成我们自己的Bromsie
let demo = new Bromise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)
上面代码的问题,执行就会报错 Uncaught TypeError: onResolve_ is not a function
执行到这里 let demo = new Bromise(executor) 其实就报错了,因为then 函数的回调还没绑定,执行的时候onResolve_还没有
易错点
这是一个非常关键的问题!很多开发者在刚开始写 async/await 时都会在这里栽跟头。
简单直接的回答是:
map 可以用 await,但效果和 for 循环完全不同!
for...of+await= 串行(一个接一个,排队)。map+await= 并发(一起上,不排队)。
而且,如果你单独用 map 而不配合 Promise.all,你会得到一个全是 Promise 的数组,而不是结果。
1. 直观演示:发生了什么?
假设我们要烤 3 个面包,每个耗时 1 秒。
如果用 for 循环 (串行)
// 耗时:3秒
for (const bread of breads) {
await bake(bread); // 烤完这个,才进下一次循环
}
如果用 map (并发)
// 耗时:1秒 (因为是同时烤的)
const results = breads.map(async (bread) => {
return await bake(bread);
});
console.log(results);
// 输出:[Promise { <pending> }, Promise { <pending> }, Promise { <pending> }]
// 注意:这里拿到的不是面包,而是“面包兑换券”!
2. 为什么 map 拿不到直接结果?
map 是一个同步方法。它的工作原理是:
“我看一眼数组第一个元素,执行回调函数,把返回值扔进新数组;立刻看第二个元素,执行回调,扔进新数组……”
当你的回调函数是 async 时:
async函数被调用,立刻返回一个 Promise(状态是 pending)。map收到了这个 Promise,把它放进新数组。map继续处理下一个……
所以,map 执行完那一瞬间,你得到的是一个 Promise 数组。
3. 正确用法:搭配 Promise.all
既然 map 返回的是一堆 Promise,我们需要一个东西来等待这一堆 Promise 全部变成结果。这就是 Promise.all 的最佳使用场景。
这是前端开发中最常用的并发写法(背下来):
async function bakeAll() {
const breads = [1, 2, 3];
// 1. map 负责把“原料”变成“正在执行的任务(Promise)”
const promises = breads.map(async (bread) => {
const res = await bake(bread);
return res;
});
// promises 现在是 [Promise, Promise, Promise]
// 2. Promise.all 负责等待它们全部完成
const results = await Promise.all(promises);
return results; // [面包1, 面包2, 面包3]
}
总结与对比
| 方法 | 代码写法 | 特点 | 适用场景 |
|---|---|---|---|
| for...of | for (const x of arr) { await fn(x) } | 串行 (阻塞) | 前一个必须做完才能做下一个(如:按顺序爬虫、数据库事务)。 |
| forEach | arr.forEach(async x => await fn(x)) | 失控 (无法等待) | 千万别用! 主程序不会等你,你也拿不到结果。 |
| map | Promise.all(arr.map(async x => ...)) | 并行 (并发) | 互不依赖的任务(如:批量上传图片、批量请求接口)。 |
结论
- 如果你想并发(同时跑),请用
Promise.all+map。 - 如果你想串行(排队跑),请用
for循环。 - 永远不要指望
map自己能await出结果,它只会给你一堆 Promise。