手写出一个异步可用的 mini Promise,帮助理解状态机和回调队列
为什么要手写一遍 Promise?
js
fetch('/api/user')
.then((res) => res.json())
.then((user) => fetch(`/api/orders/${user.id}`))
.then((res) => res.json())
.then((orders) => console.log(orders))
.catch((err) => console.error('出错了', err));
代码很清晰。
- 为什么
.then()能一直.then()下去? new Promise()里面的代码,为什么是同步立刻执行的?- 为什么
resolve()在setTimeout里面,.then()的回调也能正常工作? Promise和setTimeout的回调,谁先执行?
这些问题的答案,都在 Promise 的源码里。但是源码不易于理解,可以尝试手写一个Promise出来
可以尝试分三步,先写出一个能跑的mini版本的Promise:
- 状态机 + then 方法搭建基础框架
- 引入回调队列解决异步
- .catch() 和 .finally()
状态机 + then 方法搭建基础框架
1.1 Promise 其实就是个状态机
Promise 只有三种状态:
scss
pending(等待中) ──resolve()──▶ fulfilled(成功了)
│
└────────reject()──▶ rejected(失败了)
两个关键约束:
- 状态只能从
pending变出去,不能反复横跳 - 一旦变成
fulfilled或rejected,就永远是这个状态了
这就像一个开关,只能按一次。
1.2 用代码实现状态机
js
// 定义状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MiniPromise {
constructor(executor) {
// 初始状态:pending
this.state = PENDING;
// 成功后的值
this.value = undefined;
// 失败原因
this.reason = undefined;
// resolve 函数 ------ 把状态从 pending 变成 fulfilled
const resolve = (value) => {
// 注意:只有 pending 才能改状态!
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
}
};
// reject 函数 ------ 把状态从 pending 变成 rejected
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
}
};
// 立刻执行 executor,把 resolve 和 reject 传给它
// 用 try/catch 包住,如果执行过程抛异常,直接 reject
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
}
验证状态不可逆
打开浏览器控制台,把上面的类粘贴进去,然后跑:
js
const p = new MiniPromise((resolve, reject) => {
resolve('第一次成功');
reject('然后失败'); // 这行没用,状态已经定了
resolve('又成功一次'); // 这行也没用
});
console.log(p.state); // 'fulfilled'
console.log(p.value); // '第一次成功' 只有第一次生效!
测试抛异常:
js
const p2 = new MiniPromise((resolve, reject) => {
throw new Error('我崩了');
});
console.log(p2.state); // 'rejected'
console.log(p2.reason); // Error: 我崩了
executor是同步执行 的。这也是面试高频考点------new Promise(() => console.log('hi'))里的console.log会立刻执行。
resolve 可以接收另一个 Promise
这是一个容易忽略但很重要的特性:resolve 的参数可以是另一个 Promise。
js
const p1 = new Promise((resolve) => {
setTimeout(() => resolve('p1 的结果'), 1000);
});
const p2 = new Promise((resolve) => {
resolve(p1); // resolve 的参数是另一个 Promise!
});
p2.then((value) => console.log(value));
// 1 秒后打印:p1 的结果
p2 的 resolve 传了 p1,那 p2 自己的状态就不算数了,p1 是什么状态 p2 就是什么状态。 这叫「状态移交」。
验证一下:
js
const p1 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('p1 失败了')), 1000);
});
const p2 = new Promise((resolve) => {
setTimeout(() => resolve(p1), 500);
});
p2.then((v) => console.log('成功', v)).catch((e) => console.log('失败', e.message));
// 1 秒后打印:失败 p1 失败了
// p2 虽然 500ms 就 resolve 了,但 resolve 的是 p1,p1 是 reject,所以 p2 也跟着 reject
Promise 链式调用的基础------.then() 的回调返回一个 Promise 时,后面的 .then() 会等这个 Promise。
1.3 加上 then 方法
有了状态,怎么知道什么时候成功、什么时候失败呢?靠 .then():
js
class MiniPromise {
// ... 上面的代码
then(onFulfilled, onRejected) {
// 如果已经成功了,直接调用成功回调
if (this.state === FULFILLED) {
onFulfilled(this.value);
}
// 如果已经失败了,直接调用失败回调
if (this.state === REJECTED) {
onRejected(this.reason);
}
}
}
测试一下:
js
const p = new MiniPromise((resolve) => {
resolve('hello world');
});
p.then(
(value) => console.log('成功:', value), // 成功:hello world
(reason) => console.log('失败:', reason)
);
看起来挺像那么回事了。但是还有一个问题,如下:
异步场景崩了
js
const p = new MiniPromise((resolve) => {
setTimeout(() => {
resolve('异步结果');
}, 1000);
});
p.then((value) => console.log(value));
// 😱 什么都没打印!
为什么? 因为代码执行顺序是这样的:
arduino
时间线:
0ms: new MiniPromise → executor 执行 → setTimeout 注册到定时器 → then 被调用
├─ 此时 state 还是 'pending',then 里的 if 全都不匹配
└─ 回调没有被执行,也没有被存起来
1000ms: setTimeout 触发 → resolve('异步结果') 执行
└─ 但没有人监听这个变化了
问题出在:状态还是 pending 的时候调 then,代码什么都没做。
引入回调队列支持异步
2.1 先把回调存起来
就像点外卖:你下单的时候饭还没做好,外卖 App 不会傻等着,而是先把你的订单记录下来,饭做好了再通知你。
Promise 同理:当 .then() 被调用时,如果状态还是 pending,我们就把回调存进一个数组(队列),等 resolve/reject 执行时,再逐个调用。
js
class MiniPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
// 🆕 两个回调队列,专门存「待执行」的回调
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
// 🆕 状态一变,就把队列里的回调全执行一遍
this.onFulfilledCallbacks.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
// 🆕 错误回调也一样
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
if (this.state === FULFILLED) {
onFulfilled(this.value);
} else if (this.state === REJECTED) {
onRejected(this.reason);
} else if (this.state === PENDING) {
// 🆕 状态还没定?把回调存进队列,等状态变了再执行
this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.reason));
}
}
}
验证:异步也能跑了
js
const p = new MiniPromise((resolve) => {
setTimeout(() => resolve('异步成功!'), 1000);
});
p.then((value) => console.log(value));
// 1 秒后打印:异步成功! ✅
尝试注册多个 then
js
const p = new MiniPromise((resolve) => {
setTimeout(() => resolve('数据来了'), 2000);
});
// 注册三个 then,每个都会在 2 秒后执行
p.then((v) => console.log('第 1 个 then:', v));
p.then((v) => console.log('第 2 个 then:', v));
p.then((v) => console.log('第 3 个 then:', v));
// 2 秒后依次打印:
// 第 1 个 then: 数据来了
// 第 2 个 then: 数据来了
// 第 3 个 then: 数据来了
同一个 Promise 实例可以多次
.then(),每个 then 都会在状态变更后执行。这就是回调队列的作用。
用 MiniPromise 改写一段回调代码
假设有这样一个用回调的 API:
js
function fetchUser(id, callback) {
setTimeout(() => {
callback(null, { id, name: '大熊猫' });
}, 500);
}
用 MiniPromise 包装它:
js
function fetchUserPromise(id) {
return new MiniPromise((resolve, reject) => {
fetchUser(id, (err, user) => {
if (err) {
reject(err);
} else {
resolve(user);
}
});
});
}
// 使用
fetchUserPromise(1).then(
(user) => console.log('用户:', user.name),
(err) => console.error('出错:', err)
);
// 500ms 后打印:用户:大熊猫
这就是 Promise 化(Promisify)的基本思路。
.catch() 和 .finally()
3.1 .catch() ------ 其实就是 .then(null, 回调)
js
class MiniPromise {
// ...
catch(onRejected) {
// .catch(fn) 等价于 .then(null, fn)
return this.then(null, onRejected);
}
}
就这么简单!.catch 不是独立的新机制,它就是 .then 的马甲。
测试一下
js
new MiniPromise((resolve, reject) => {
setTimeout(() => reject('网络错误'), 500);
})
.catch((err) => {
console.error('捕获到了:', err); // 捕获到了: 网络错误
return '已处理';
})
.then((v) => console.log('继续执行:', v)); // 继续执行: 已处理
这里的catch后面的then没有运行,是因为没有返回新的Promise,所以无法实现链式调用。放在下篇文章中继续讲解。
3.2 .finally() ------ 不管成功失败都执行
js
finally(onFinally) {
// finally 的回调不接收参数,也不影响链上的值
return this.then(
(value) => {
// 成功路径:先执行 finally 回调,再把原值传下去
return MiniPromise.resolve(onFinally()).then(() => value);
},
(reason) => {
// 失败路径:先执行 finally 回调,再把错误传下去
return MiniPromise.resolve(onFinally()).then(() => {
throw reason;
});
}
);
}
这里用到了
MiniPromise.resolve(),我们目前还没实现。你可以先把它理解成「把 onFinally() 的返回值包装成 Promise,确保可以.then()」。
测试 finally
js
new MiniPromise((resolve) => resolve('success'))
.finally(() => console.log('清理工作!'))
.then((v) => console.log('值:', v));
// 清理工作!
// 值: success
完整代码
把上面的代码汇总,就是你手写的第一个 mini Promise:
js
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MiniPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
if (this.state === FULFILLED) {
onFulfilled(this.value);
} else if (this.state === REJECTED) {
onRejected(this.reason);
} else if (this.state === PENDING) {
this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.reason));
}
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(onFinally) {
return this.then(
(value) => MiniPromise.resolve(onFinally()).then(() => value),
(reason) =>
MiniPromise.resolve(onFinally()).then(() => {
throw reason;
})
);
}
}
就 50 行代码,已经是一个能跑异步、有 catch/finally 的 Promise 了。
虽然能跑异步了,但跟浏览器/Node.js 里真正的 Promise 比,还差很多:
链式调用 p.then().then()、then 返回新 Promise 、值穿透 p.then().then(v => ...)、then 里返回 Promise 自动展开、微任务时序、静态方法 all/race 等,这些方法放在下篇文章中再依次实现。
它的三个天生缺陷
在继续深入之前,先正视 Promise 的几个固有问题。了解工具的边界,比会用工具更重要:
1. 无法取消。 Promise 一旦创建就开始执行,没有原生的取消机制。 AbortController 提供了一种外部取消信号,但 Promise 本身仍然不可取消。
2. 不设回调时,内部错误不会冒泡到外部。
js
const p = new Promise((resolve) => {
resolve(x + 2); // x 没定义,报 ReferenceError
});
// 没有 .catch(),这个错误不会影响外部代码
// 但 Node.js 会触发 unhandledRejection 事件,未来版本可能直接崩进程
3. pending 状态时,你无法知道进度。 是刚开始还是快完成了?Promise 不告诉你。如果需要进度信息,得用别的手段(比如事件)。
这些缺陷不是设计失误,是取舍。世上没有完美的方案,只有适合的方案。
实践
- 包装 fs.readFile :用 MiniPromise 包装 Node.js 的
fs.readFile,实现一个读文件的 Promise 版本 - 加载图片 :用 MiniPromise 包装
new Image()的加载过程,把onload/onerror映射成 Promise 的 resolve / reject - 延迟函数 :用 MiniPromise 写一个
delay(ms)函数,返回一个 ms 毫秒后 resolve 的 Promise - 超时控制 :写一个
timeout(promise, ms)函数,如果 promise 在 ms 毫秒内没完成就 reject
js
// 1. 包装 fs.readFile
const fs = require('fs');
function readFilePromise(path) {
return new MiniPromise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
err ? reject(err) : resolve(data);
});
});
}
// 2. 加载图片
function loadImagePromise(url) {
return new MiniPromise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`图片加载失败: ${url}`));
img.src = url;
});
}
loadImagePromise('https://example.com/photo.jpg')
.then((img) => document.body.appendChild(img))
.catch((err) => console.error(err.message));
// 3. 延迟函数
function delay(ms) {
return new MiniPromise((resolve) => setTimeout(resolve, ms));
}
delay(1000).then(() => console.log('1 秒到了'));
// 4. 超时控制
function timeout(promise, ms) {
return new MiniPromise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('超时了!')), ms);
promise.then(
(v) => {
clearTimeout(timer);
resolve(v);
},
(e) => {
clearTimeout(timer);
reject(e);
}
);
});
}
🎯 本篇面试题
第 1 题:状态不可逆
js
const p = new Promise((resolve, reject) => {
resolve('first');
reject('second');
resolve('third');
});
p.then(console.log).catch(console.error);
输出什么?为什么?
输出:first
Promise 的状态只能从 pending 变一次。resolve('first') 把状态改成了 fulfilled,后续的 reject 和 resolve 都不会再生效。这就是「状态不可逆」。
第 2 题:new Promise 里的代码是同步还是异步?
js
console.log(1);
const p = new Promise((resolve) => {
console.log(2);
resolve(3);
});
console.log(4);
p.then((v) => console.log(v));
console.log(5);
输出顺序?
输出:1 2 4 5 3
Promise 构造函数里的代码是同步立即执行 的,所以 1 → 2 → 4 → 5 按顺序打印。.then() 的回调是微任务,在本轮事件循环末尾执行,所以 3 最后。
第 3 题:resolve 后代码还会跑吗?
js
new Promise((resolve) => {
resolve('ok');
console.log('还在跑');
});
console.log('还在跑') 会执行吗?
会。resolve() 不是 return,它只是改状态,不会阻止后续代码执行。正确写法是 return resolve('ok')。
第 4 题:回调队列什么时候触发?
js
const p = new Promise((resolve) => {
setTimeout(() => resolve('done'), 1000);
});
p.then(console.log); // 第一个注册
p.then(console.log); // 第二个注册
两个 .then 的回调都会执行吗?输出几个 done?
两个都会执行,输出两次 done。
.then() 可以多次注册同一个 Promise,每个注册的回调都会被收集到各自队列中,resolve 时依次执行。这点和事件监听器类似。
第 5 题:手写题------用 Promise 包装回调
把下面的回调函数改写成 Promise:
js
function loadScript(url, callback) {
const script = document.createElement('script');
script.src = url;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error('加载失败'));
document.head.appendChild(script);
}
js
function loadScriptPromise(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error('加载失败'));
document.head.appendChild(script);
});
}
// 使用
loadScriptPromise('https://example.com/app.js')
.then((script) => console.log('加载成功'))
.catch((err) => console.error(err));
回调的 callback(null, value) 对应 resolve(value),回调的 callback(err) 对应 reject(err)。
下一篇文章将会输出链式调用、resolvePromise 解析过程 。 .then() 到底是怎么把结果一层层传下去的,以及为什么 p.then(() => new Promise(...)) 能正常工作。
欢迎关注公众号:程序员蜡笔熊,期待与您的讨论