javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript Promise原理与应用

一文带你详细拆解JavaScript中Promise的原理和真实应用

作者:森叶

本文从中文语义出发,逐层深入到 .then 源码、resolve 与 then 的联动机制、await 的编译真相,最后用一道面试实战题把所有知识串起来,读完之后,Promise 对你来说不再是一个需要记忆的 API,而是一个可以用直觉推导的思维模型

我一直觉得 Promise 最大的理解障碍不是技术,而是命名。.then 这个词太抽象了——“然后”?然后什么?然后就完了?但如果你把它翻译成中文的**“等”**,一切就清晰了。

promise.then(fn)promise.等(fn)

等还没有结果。等完了,然后呢?还需要继续等吗?

再看 Promise 这个词本身——承诺

“我承诺帮你干这件事,事情被包在承诺里,你就等着,干完了叫你。”

一个"承诺",一个"等",就是 Promise 全部的秘密。

本文从中文语义出发,逐层深入到 .then 源码、resolve 与 then 的联动机制、await 的编译真相,最后用一道面试实战题把所有知识串起来。读完之后,Promise 对你来说不再是一个需要记忆的 API,而是一个可以用直觉推导的思维模型。

一、承诺:Promise 的中文本义

Promise 翻译成中文就是承诺。而且这个词和 Promise 的语义几乎完美对应:

“我承诺给你一个结果,但不是现在。”

const 承诺 = fetch('/api');
// 我承诺会给你数据,但现在还没拿到
// 你先拿着这个"承诺",等我兑现

Promise 的三种状态用"承诺"来理解天然成立:

pending    → 承诺还没兑现,等着
fulfilled  → 承诺兑现了,拿到了结果
rejected   → 承诺违约了,出错了

resolvereject 也可以直接翻译:

new Promise((兑现, 违约) => {
  if (成功) 兑现(结果);    // 信守承诺
  else 违约(错误);          // 承诺作废
});

而整个 Promise 机制,就是三个角色的协作:

承诺人(executor):  我来干活
等待人(.then):     我等着,干完了叫我
结果(resolve 的值):干完的交付物

我承诺帮你干,你就等着,干完了叫你。 这就是 Promise 的全部。

二、用"等"重新理解.then链

先看一段最普通的 Promise 代码:

fetch('/api')
  .then(data => parse(data))
  .then(result => save(result))
  .then(() => console.log('完成'));

现在把 .then 替换成"等":

发请求
  .等(数据回来了 → 解析)
  .等(解析完了 → 保存)
  .等(保存完了 → 打印"完成")

读起来是不是像说人话了?每个"等"都在问同一个问题:

最后一点最关键——等完了可能还要等。这就是 Promise 链能无限串下去的原因。

而链式调用就是承诺的传递

A 承诺:我帮你拿数据          → 你等
A 兑现了,你拿到数据
B 承诺:我帮你解析这个数据     → 你等
B 兑现了,你拿到解析结果
C 承诺:我帮你存到数据库       → 你等
C 兑现了,完事

每一步干完活的人说"我搞定了",下一个人才开始干。活是一个一个承诺出去的,你就一个一个等。

三、"等"的三种结局

当"等"到了上一步的结果,你的回调函数执行了,它的返回值决定了下一个"等"的命运:

结局一:返回一个普通值 → 等到了,立刻交付

promise.等(data => {
  return data + 1;   // 返回普通值 2
});
// 下一个"等"立刻拿到 2,不用真的等

结局二:返回一个新的 Promise → 还得继续等

promise.等(data => {
  return fetch('/api2');   // 返回新的 Promise(新承诺)
});
// 下一个"等"被锁住了,要等 fetch 完成才能继续

结局三:不返回任何值 → 等到了个寂寞

promise.等(data => {
  console.log(data);   // 用了,但没 return
});
// 下一个"等"拿到 undefined,值断了

结局二是整个 Promise 机制最核心的特性。 正是因为"等完了还可以继续等",才让异步操作能像水管一样串联起来。

四、从源码看"等"的实现

下面是一个简化但忠实于 Promise/A+ 规范的实现。我在关键位置标注了"等"和"承诺"的语义:

class MyPromise {
  constructor(executor) {
    this.status = 'pending';     // 承诺还没兑现
    this.value = undefined;       // 兑现后的结果
    this.callbacks = [];          // 排队等的人

    const resolve = (value) => {
      if (this.status !== 'pending') return;
      this.status = 'fulfilled';  // 承诺兑现了!
      this.value = value;
      this.callbacks.forEach(cb => this._handle(cb));  // 通知所有排队的人
    };

    executor(resolve);   // 把"兑现"的能力交给承诺人
  }

  then(onFulfilled) {
    // ★ 每次"等",都会产生一个新的"承诺"
    let resolve2;
    const 新承诺 = new MyPromise((resolve) => {
      resolve2 = resolve;     // 把新承诺的兑现开关拿出来,先不按
    });

    const callback = { onFulfilled, resolve2 };

    if (this.status === 'fulfilled') {
      this._handle(callback);         // 承诺已兑现,直接处理
    } else {
      this.callbacks.push(callback);   // 还没兑现,留个电话等通知
    }

    return 新承诺;   // 返回的永远是一个新的承诺,不是具体的值
  }

  _handle({ onFulfilled, resolve2 }) {
    queueMicrotask(() => {
      const result = onFulfilled(this.value);

      if (result instanceof MyPromise) {
        // ★ 等到的结果还是一个承诺 → 把自己的开关交出去
        result.then(resolve2);
      } else {
        // ★ 等到的是一个确切的值 → 直接按下开关
        resolve2(result);
      }
    });
  }
}

整个源码的核心逻辑用"等"和"承诺"来概括就一句话:

承诺兑现了,看结果是不是还是一个承诺。是 → 继续等;不是 → 交付。

五、resolve和.then是怎么联动的

看完源码结构,一个最关键的问题浮出水面:resolve 写在 constructor 里,.then 写在外面,它们互不知道对方什么时候执行。那结果是怎么传递过去的?

答案是 this.callbacks 这个数组——它是 resolve.then 之间的桥梁。

        callbacks(共享的信箱)
              │
     ┌────────┴────────┐
     │                  │
  resolve              .then
  (承诺人)            (等待人)
     │                  │
  "我干完了,           "我来等结果,
   看看有没有人等着"     看看是不是已经干完了"

谁先谁后?两种情况都能处理。

情况一:先.then,后resolve(最常见)

const p = new Promise((resolve) => {
  setTimeout(() => resolve('hello'), 1000);
});
p.then(value => console.log(value));

时刻 0ms:
  .then 执行,发现 status 是 pending
  → 把 { onFulfilled, resolve2 } 存进 callbacks
  → 就像留了个电话号码:"兑现了打这个号通知我"

时刻 1000ms:
  resolve('hello') 被调用
  → status 改为 fulfilled,value 存为 'hello'
  → 遍历 callbacks,逐个调 _handle
  → onFulfilled('hello') 执行

先留电话,活干完了打电话通知。

情况二:先resolve,后.then

const p = Promise.resolve('hello');
p.then(value => console.log(value));

.then 执行,发现 status 已经是 fulfilled
→ 不存 callbacks,直接调 _handle
→ onFulfilled('hello') 执行

到了才发现活早干完了,结果就在柜台上,直接拿走。

两边各自只关心自己的事,但合在一起恰好覆盖了所有时序可能:

resolve 的逻辑:
"我干完了"
→ 改 status,存 value
→ callbacks 里有人吗?有就逐个通知,没有就算了(值存着,谁来都能拿)

.then 的逻辑:
"我来等"
→ status 是 fulfilled 吗?
→ 是 → 直接拿值走人
→ 不是 → 把自己塞进 callbacks,等着被叫

这个设计还有一个精妙的约束——状态不可逆

const resolve = (value) => {
  if (this.status !== 'pending') return;   // 兑现过了就不能再变
};
pending → fulfilled    ✅ 可以
fulfilled → pending    ❌ 不行

一旦承诺兑现就永远是兑现的,结果永久缓存在 this.value 里。不管多少个 .then 来,拿到的都是同一个结果,不会过期。普通的发布-订阅(EventEmitter)做不到这一点——事件触发了你没监听就错过了。但承诺不会,承诺兑现了就是兑现了,什么时候来取都行。

六、result.then(resolve2)—— 把自己的命运交给别人

源码中最精妙的一行:

_handle({ onFulfilled, resolve2 }) {
  queueMicrotask(() => {
    const result = onFulfilled(this.value);

    if (result instanceof MyPromise) {
      result.then(resolve2);       // 这一行
    } else {
      resolve2(result);
    }
  });
}

resolve2 是谁的?是"新承诺"(.then 返回的那个 Promise)的兑现开关。正常情况下,这个开关应该由 _handle 自己按下。但当 result 是一个 Promise 时:

result.then(resolve2);

翻译成中文:

嘿 result,我不按这个开关了。
你什么时候兑现了承诺,你帮我按。
你兑现了什么值,那就是我的值。

这就是控制权转移——新承诺把自己的命运锁定到了 result 身上。

对比两个分支:

resolve2(result);          // 自己按开关:我等到了确切的值,直接交付
result.then(resolve2);     // 把开关交给别人:我等到的还是一个承诺,让它来决定我的命运

用生活场景打比方:

你去餐厅点了菜(发起 .then)
服务员给你一个取餐号(返回新的 Promise)

情况一:菜做好了,直接上桌
  → resolve2(菜),你吃上了

情况二:服务员说"这道菜的食材要等隔壁店送来"
  → 服务员把你的取餐号转给了隔壁店
  → 隔壁店送到了 → 你的号才被叫到
  → result.then(resolve2)

七、new Promise()vs.then()—— 自己承诺 vs 被安排的承诺

// 自己承诺:你完全控制什么时候兑现
const a = new Promise((resolve) => {
  setTimeout(() => resolve('hello'), 1000);
  // 你自己决定 1 秒后兑现承诺
});

// 被安排的承诺:你控制不了
const b = a.then(value => {
  return value + ' world';
});
// b 什么时候兑现,取决于 a 什么时候兑现
// 以及你的回调返回的是值还是新的承诺

而且 .then() 返回的永远是一个新的承诺,不是具体的值:

const a = Promise.resolve(1);

const b = a.then(v => v + 1);     // b 是承诺,不是 2
const c = a.then(v => 'hello');   // c 是承诺,不是 'hello'
const d = a.then(v => undefined); // d 是承诺,不是 undefined

因为 .then 的第一件事就是 new 一个新的 Promise 然后 return 它。你的回调返回值只决定了这个承诺以什么值兑现,而不是替代承诺本身。

这也是 .then 能无限链下去的原因——每一步返回的都是承诺,承诺就有 .等 方法。如果返回的是 2,那 2.等(fn) 就报错了,链直接断了。

八、等到了个寂寞:不返回值的陷阱

大多数人这样写 .then

fetch('/api').then(data => {
  console.log(data);    // 用了,但没 return
});

回到源码,没有 return 意味着 result = undefined

_handle({ onFulfilled, resolve2 }) {
    const result = onFulfilled(this.value);
    //    ↑ 没有 return,result 是 undefined

    if (result instanceof MyPromise) {
      result.then(resolve2);       // 走不到这里
    } else {
      resolve2(result);            // resolve2(undefined)
    }
}

下一个"等"等到了 undefined——承诺倒是兑现了,但兑现了个寂寞

fetch('/api')
  .等(data => {
    console.log(data);   // 有值
    // 没有 return
  })
  .等(result => {
    console.log(result); // undefined,值断了
  });

很多人觉得没问题,因为后面没人接了。但这说明他们把 .then 当成了事件监听器,而不是管道变换器——上一节的输出应该是下一节的输入。

在实际代码里,这个细节也很致命:

chain.then(() => promise)     // ✅ 箭头函数隐式 return promise
chain.then(() => { promise }) // ❌ 花括号,没 return,等了个寂寞,链直接穿透

只差一对花括号,一个是"等到了还要继续等",一个是"承诺兑现了个空气"。

九、await—— 让你假装不用等的语法糖

幻觉

async function foo() {
  const value = await somePromise;
  console.log(value);   // 看起来直接拿到了值
}

真相

function foo() {
  return somePromise.等(value => {
    console.log(value);   // 值还是在回调参数里
  });
}

await 做的事情就是让编译器帮你把函数劈开——遇到 await 就是一刀,后半部分整个塞进 .then 的回调里:

async function foo() {
  // -------- 第一半:同步执行 --------
  console.log('开始');
  const value = await somePromise;
  // -------- 第二半:塞进 .then 回调 --------
  console.log(value);
  return value + 1;
}

// 编译器翻译后:
function foo() {
  console.log('开始');
  return somePromise.then(value => {
    console.log(value);
    return value + 1;
  });
}

多个 await 就是多次劈开:

async function foo() {
  const a = await p1;    // 第一刀
  const b = await p2;    // 第二刀
  return a + b;
}

// 等价于:
function foo() {
  return p1.等(a => {
    return p2.等(b => {
      return a + b;
    });
  });
}

值永远在回调里,从来没有"逃出"过

如果你追问:那外面怎么拿到 a + b 的?

const c = await foo();

展开:

foo().等(c => {
  // c 在回调参数里
});

再往外套一层?

async function outer() {
  const result = await main();
}
// 展开:
main().等(result => { ... });

一直追到调用栈最顶层:

main();   // 返回 Promise,没人再 await 它了

值从来没有被赋值给回调外部的任何变量。 它只是从一个"等"的回调参数,传到下一个"等"的回调参数,一路传下去。

所以 const c = await foo() 不是"赋值",是传参resolve(a + b) 把值传给了 .then(c => ...) 的参数 cawait 只是让这个参数看起来像是赋值给了一个局部变量。

await 没有发明任何新的取值方式,它只是让你不用手写嵌套的"等"了。你每次写下 await,其实都是在说:“我先等等。” 只不过编译器替你排好了队,让你以为自己没在等而已。

十、实战:并行执行,串行输出

理解了"承诺"和"等",来看一道面试级别的问题:

实现一个队列,任务 push 时立即执行(并行),但结果按 push 顺序输出(串行)。

function createQueue(onResult) {
  let 等待链 = Promise.resolve();   // 初始:一个已经兑现的承诺

  return {
    push(task) {
      const promise = task();       // 立即执行(并行)
      等待链 = 等待链
        .等(() => promise)            // 等上一个输出完 → 返回当前任务的承诺 → 继续等
        .等(onResult);                // 等任务兑现 → 输出结果
    },
    done() {
      return 等待链;
    }
  };
}

用"等"来读这段代码:

push(A):
  等待链(已兑现)
    .等(→ promiseA)     // 不用等,直接执行,但返回了 promiseA → 要等它兑现
    .等(onResult)       // 等 A 兑现 → 输出 A

push(B):
  等待链(现在是 A 的输出承诺)
    .等(→ promiseB)     // A 还没输出完,这个回调还不执行
    .等(onResult)       // 等 B 兑现 → 输出 B

push(C):
  等待链(现在是 B 的输出承诺)
    .等(→ promiseC)     // B 还没输出完,继续排队等
    .等(onResult)       // 等 C 兑现 → 输出 C

关键洞察:任务在 push 时就已经开始执行了(并行),但"等"链保证了结果按顺序释放。等链轮到某个任务时,它的承诺可能早就兑现了,那就直接通过,不浪费时间。

"等"不一定真的要花时间等。它只保证了顺序,而没有牺牲并行性。

就像你同时找了三个人帮忙,但跟他们说:

"你们仨同时干,但交活的时候排好队,
 A 先交,B 再交,C 最后交,
 别管谁先干完,顺序不能乱。"

干活并行,交活串行。承诺的"等"就是这个排队交活的机制。

完整测试:

function delay(ms, value) {
  return () => new Promise(resolve => {
    console.log(`[启动] ${value}`);
    setTimeout(() => resolve(value), ms);
  });
}

const results = [];
const queue = createQueue(result => {
  results.push(result);
  console.log(`[输出] ${result}`);
});

queue.push(delay(300, 'A'));   // 最慢
queue.push(delay(200, 'B'));
queue.push(delay(100, 'C'));   // 最快

queue.done().then(() => {
  console.log(results);
  // [启动] A
  // [启动] B        ← 三个同时启动
  // [启动] C
  // [输出] A        ← 但输出严格按顺序
  // [输出] B
  // [输出] C
});

C 最先完成,但它要 A 和 B 先输出。A 最慢,但它排第一个,不用等任何人。

十一、更高的视角:类是维度的上升

回过头来看 Promise 的设计——resolve 写在 constructor 里,.then 写在方法里,两边互不知道对方什么时候执行。但它们通过 status + callbacks 这两个共享状态,无论谁先谁后,结果都对

平时我们写代码,都是在单一时间线上思考:

// 我是发送方,我只管发
socket.send(data);

// 我是接收方,我只管收
socket.onmessage = (data) => { ... };

这两段代码各管各的,它们之间的协调要靠开发者自己在脑子里对齐时序。

但 Promise 做了什么?把两条时间线折叠进一个对象里

const p = new Promise((resolve) => {
  // 时间线 A:发送/生产 —— 承诺人
  干活(结果 => resolve(结果));
});

p.then(结果 => {
  // 时间线 B:接收/消费 —— 等待人
});

两条原本独立的时间线,通过 new Promise() 这个"高维容器"被收纳到了同一个实体里。它们在时间上可能相隔很久,但在逻辑上被绑定在了一起。

这个模式不只在 Promise 里,到处都是:

Promise:       封装了"兑现承诺"和"等待承诺"两条时间线
EventEmitter:  封装了"发事件"和"收事件"两条时间线
Redux Store:   封装了"写状态"和"读状态"两条时间线
数据库事务:     封装了"多个操作"和"成功/回滚"两条时间线

每一个都是同样的模式——把多个可能发生在不同时刻的逻辑,折叠进一个高维容器里统一管理。

所以类的封装,不只是"把数据和方法放在一起"这么简单。它真正的价值是维度的上升

将不同时间维度上的逻辑,收纳进同一个空间维度的实体里,让它们能够协作。

Promise 管理的是"承诺与兑现"的关系,EventEmitter 管理的是"发布与订阅"的关系,事务管理的是"操作与一致性"的关系。好的类设计让人觉得"优雅",正是因为它不是在解决当下这一刻的问题,而是在管理一段跨越时间的关系

下次设计一个类的时候,可以问自己一个问题:

“我是在封装数据,还是在折叠时间线?”

如果是后者,你大概率在做真正有价值的抽象。

十二、总结

概念用"承诺"和"等"来理解
new Promise(executor)我承诺帮你干这件事,executor 里就是要干的活
resolve(value)承诺兑现了,交付结果
reject(error)承诺违约了,告知原因
.then(fn)等承诺兑现,然后执行 fn
.then 的返回值永远是一个新的承诺,不是具体的值
fn 返回普通值新承诺立刻兑现
fn 返回 Promise新承诺锁定到返回的 Promise,继续等
fn 不返回值新承诺兑现了个寂寞(undefined)
callbacks 数组resolve 和 .then 的联动桥梁——先到的留电话,后到的查结果
状态不可逆承诺兑现了就是兑现了,什么时候来取都行
await编译器帮你把函数劈开,后半部分塞进 .then 的回调里
await 的值从来没有"逃出"回调,只是从一个"等"传到下一个"等"
并行执行串行输出活同时干,但承诺排着队兑现
类的本质维度的上升——把多条时间线折叠进一个对象里

如果 JavaScript 是中国人发明的,Promise 一定叫"承诺",.then 一定叫 .等resolve 一定叫"兑现",reject 一定叫"违约"。

而你每次写下 await,其实都是在说:“我先等等。”

只不过编译器替你排好了队,让你以为自己没在等而已。

以上就是一文带你详细拆解JavaScript中Promise的原理和真实应用的详细内容,更多关于JavaScript Promise原理与应用的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文