实现一个能跑的迷你版Promise(一)

手写出一个异步可用的 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() 的回调也能正常工作?
  • PromisesetTimeout 的回调,谁先执行?

这些问题的答案,都在 Promise 的源码里。但是源码不易于理解,可以尝试手写一个Promise出来

可以尝试分三步,先写出一个能跑的mini版本的Promise:

  1. 状态机 + then 方法搭建基础框架
  2. 引入回调队列解决异步
  3. .catch() 和 .finally()

状态机 + then 方法搭建基础框架

1.1 Promise 其实就是个状态机

Promise 只有三种状态:

scss 复制代码
pending(等待中) ──resolve()──▶ fulfilled(成功了)
       │
       └────────reject()──▶ rejected(失败了)

两个关键约束:

  • 状态只能从 pending 变出去,不能反复横跳
  • 一旦变成 fulfilledrejected就永远是这个状态了

这就像一个开关,只能按一次。

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 的结果

p2resolve 传了 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 不告诉你。如果需要进度信息,得用别的手段(比如事件)。

这些缺陷不是设计失误,是取舍。世上没有完美的方案,只有适合的方案。


实践

  1. 包装 fs.readFile :用 MiniPromise 包装 Node.js 的 fs.readFile,实现一个读文件的 Promise 版本
  2. 加载图片 :用 MiniPromise 包装 new Image() 的加载过程,把 onload / onerror 映射成 Promise 的 resolve / reject
  3. 延迟函数 :用 MiniPromise 写一个 delay(ms) 函数,返回一个 ms 毫秒后 resolve 的 Promise
  4. 超时控制 :写一个 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,后续的 rejectresolve 都不会再生效。这就是「状态不可逆」。


第 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(...)) 能正常工作。

欢迎关注公众号:程序员蜡笔熊,期待与您的讨论

相关推荐
Csvn3 小时前
`??` 和 `||` 搞混,线上用户头像全挂了
前端
kyriewen3 小时前
白宫前脚下了限制令,OpenAI 后脚就把 GPT-5.6 发了
前端·gpt·openai
用户40269244819084 小时前
CRMEB Pro 新增后台接口全链路:路由、权限、验证器、返回格式一次讲清
前端·后端
泉城老铁5 小时前
springboot+vue+ ffmpeg 实现视频的拉流播放
前端
PedroQue995 小时前
uni-router v1.8.0新增冷启动守卫补执行
前端·uni-app
xiaok5 小时前
部署之后,本地浏览器还在读取旧缓存导致页面一直显示loading中
前端
用户059540174465 小时前
Redis缓存一致性踩坑实录:线上故障排查6小时,我用pytest+内存快照把它永久关进了笼子
前端·css
星栈5 小时前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:第一版先把列表和详情跑通
前端·rust·前端框架
用户1733598075376 小时前
Vue 3 SPA 首屏优化:从 3s 到 1.2s 的 5 个实践
前端·vue.js