什么是 Promise
在谈 Promise 是什么之前,我们首先需要知道一个概念【异步编程】,那异步编程是什么呢?
简单来说,异步编程是一种编程范式,目的是避免阻塞主线程,通过事件循环 + 任务队列的方式把任务的执行从顺序流变成了事件流,从而去解决单线程中的同步阻塞的问题。
在早期异步编程的解决方案是使用 callback,但是随着 callback 的使用出现了 callback 层层嵌套(回调地狱)和错误处理困难等问题。
为了解决 callback 带来的问题 ES6 提供了一个新的异步解决方案,也就是 Promise。它通过链式调用把纵向嵌套的回调拉平为横向流水线来解决回调地狱的问题,同时使用 then 和 catch 方法来优雅的进行错误处理。
异步操作只是实现异步编程的手段,实际开发中常用的有:callback、Promise、async/await(Promise 的语法糖)、setTimeout、addEventListener 等。
Promise 的状态
Promise 实例中有两个内置属性:
- [[PromiseState]] :存储状态
- [[PromiseResult]] : 存储结果
我们首先来看下Promise的状态,它有三种状态:pending、fulfilled(resolved)、 rejected,这三种状态具备以下特点:
- 状态的流转是单向的,并且只能是pending => fulfilled、pending => rejected。
- 状态一旦改变就会被锁死,不受外界的影响。
为了更好的理解Promise状态的特性,需要说明一下Promise的设计思想是“单一终值”只关心异步操作的最终结果。而Promise的设计是基于Promise/A+规范,它是一种约束规范,简单说下以下几点:
- 只定义了三种状态,且三种状态单向冻结。
- 没有cancel方法和abort状态,new Promise中的代码是同步立即执行的。所以Promise创建就会立即执行,无法中途取消。
- 由于Promise的设计是”单一终值的“,不传递中间过程,所以pending是处于一种黑盒状态,即当Promise处于pending状态的时候无法得知目前所处阶段是刚开始还是即将完成。
- 错误不自动外抛,因为错误会内化为 rejected 状态,不转为同步异常,所以需要使用.then或.catch来消费这个 rejected 状态。
- then必须返回新的promise (支持缺省参数、异常穿透、多次调用)。
- 支持thenable链式解析 (任何带有 then 方法的对象或函数都是 thenable)。
Promise 的常用 API
一、Promise 原型方法
Promise的原型方法只有三个:then、catch、finally。
Promise.prototype.then()
给 promise 注册成功 / 失败的回调,并返回新的 promise,值会穿透错误会向后传播。
- 是微任务。
- 忘记return,下一层会拿到undefined。
- throw 抛错,新的promise会rejected。
- 循环引用会触发TypeError。
// 循环引用 const p = Promise.resolve(1); const p2 = p.then(() => p2); // 回调返回了 p2 自己,形成循环,触发 TypeError // 忘记return Promise.resolve(1) .then(v => { console.log(v); }) // 没有 return .then(v => console.log(v)); // undefinedPromise.prototype.catch()
是 .then (null, onRejected) 的语法糖,用来捕获链式调用中前面任何 Promise 抛出的异常或 reject 状态。
- 是微任务。
- 不会捕获它自己回调里的错误,需要下一个 catch 来接。
- 返回的是一个 promise 对象,后面还可以继续链式调用。
和 try...catch 的区别:
- try...catch 是语法结构,捕获当前调用栈里的同步异常。
- catch 是方法调用以微任务形式执行,只能捕获当前 Promise 前面节点中的 Rejection。
Promise.prototype.finally()
是.then (onFinally, onFinally) 的语法糖,不过 promise 的状态是成功还是失败都会执行。
- 是微任务。
- 无参数,所以无法得知 promise 的状态。
- 值会透传(resolve 和 reject 的值都会透传)。
- finally 里 抛出异常 或返回 rejected Promise,才会变成 rejected。
在实际开发中,会在 finally 中关闭加载动画 loading,添加日志埋点。
const p = Promise.resolve('ok'); p .then(v => console.log('成功:', v)) // 处理 fulfilled .catch(e => console.log('失败:', e)) // 处理 rejected .finally(() => { hideLoading(); // 无论成败都关掉 loading sendLog('request'); // 埋点 });二、Promise 的常用静态方法
Promise.all()
是并行执行,全部成功才成功,一个失败就整体失败的方法。
- 接受一个可迭代对象作为入参,如果参数是非可迭代对象则会抛出 TypeError;如果传入 空数组会立刻被 resolve 一个空数组。
它的出参有两种:
- 全部成功:按入参顺序返回一个数组。
- 有一个rejected,返回第一个被rejected的结果,但其余请求不会被中断(结果被忽略),需要在外部手动取消。
const controller = new AbortController(); const fetch1 = fetch('/api/1', { signal: controller.signal }); const fetch2 = fetch('/api/2', { signal: controller.signal }); Promise.all([fetch1, fetch2]).catch(() => { controller.abort(); // 手动取消其他请求 });Promise.allSettled()
也是并行执行,但和 all 方法不同的是,它会等待所有 promise 都落定后再给结果,返回值包含了结果和状态。它的特点是永远不会 reject,不管成功失败都会 resolve。
Promise.allSettled([ Promise.resolve('ok'), Promise.reject('err') ]).then(results => console.log(results)); // [{status: 'fulfilled', value: 'ok'}, {status: 'rejected', reason: 'err'}]Promise.race()
是竞速,是先看谁状态落定,最先状态改变的决定整体结果,不管是成功还是失败。
- 只返回第一个状态落定的结果,其余的请求会继续但结果忽略。
- 入参如果是空数组,则会永远处于 pending 状态。
const p1 = new Promise(res => setTimeout(() => res('慢'), 100)); const p2 = Promise.reject('快'); // 立即 rejected Promise.race([p1, p2]) .catch(e => console.log(e)); // "快"Promise.any()
取第一个成功的结果,全部失败才 reject,会抛 AggregateError,里面包含所有失败的原因,比 race 更容错。
const promises = [ Promise.reject('err1'), new Promise(res => setTimeout(() => res('ok'), 100)), Promise.reject('err2') ]; // race:第一个 settled 是 reject,直接失败 Promise.race(promises).catch(e => console.log('race:', e)); // race: err1 // any:忽略 reject,等待第一个 fulfilled Promise.any(promises).then(v => console.log('any:', v)); // any: ok(100ms 后)Promise.resolve()
它针对不同参数有不同处理方式,分别是:
- 传入 promise,原样返回,不做二次包装。
- 传入 thenable,将其转换为 promise 对象,立即执行它的 then 方法。
- 传入普通值,立即返回 fulfilled 状态的普通值。
开发中的使用场景:微任务插队把同步快速变 异步、确保返回值一定是 Promise。
// 1. Promise 实例:原样返回 const p = new Promise(() => {}); Promise.resolve(p) === p; // true // 2. Thenable:递归解析 const thenable = { then(resolve, reject) { reject(new Error('from thenable')); // 可以是 rejected! } }; Promise.resolve(thenable).catch(e => console.log(e.message)); // "from thenable" // 3. 普通值:fulfilled Promise.resolve(42).then(v => console.log(v)); // 42Promise.reject()
是用来创建一个已被拒绝的 promise 的快捷方式:把任何值立即变 rejected 状态的静态方法,对参数不解析,传入什么就以什么作为失败原因原样返回。
开发中的使用场景:提前抛错,统一错误处理,条件失败短路。
function fetchUser(id) { if (!id) { return Promise.reject(new Error('id 不能为空')); } return fetch(`/api/user/${id}`); }Promise 的基础应用
一、取消 Promise
方案一:使用原生的取消机制 AbortController
const controller = new AbortController(); const signal = controller.signal; fetch('/api/data', { signal }) .then(response => response.json()) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.log('请求已取消'); } }); // 取消请求 controller.abort();方案二:利用 Promise.race 竞速模拟取消
function withCancel(promise, cancelPromise) { return Promise.race([ promise, cancelPromise.then(() => { throw new Error('Cancelled'); }) ]); } const task = new Promise(resolve => setTimeout(() => resolve('done'), 5000)); const cancel = new Promise(resolve => setTimeout(resolve, 2000)); // 2秒后触发取消 withCancel(task, cancel).catch(err => console.log(err.message)); // "Cancelled"除了示例的两种方式,还可以使用一个外部标志位(cancel token模式)来控制Promise的执行流程,以及一些三方库。
二、改变 Promise 的状态
在new Promise()中改变Promise的状态的方法只有三种:resolve、reject、throw。
const p = new Promise((resolve, reject) => { resolve('数据加载成功'); // 状态变为 fulfilled }); p.then(v => console.log(v)); // "数据加载成功" const p1 = new Promise((resolve, reject) => { reject(new Error('数据加载失败')); // 状态变为 rejected }); p1.catch(e => console.log(e.message)); // 数据加载失败 const p2 = new Promise((resolve, reject) => { throw new Error('参数非法'); // 等效于 reject(new Error('参数非法')) }); p2.catch(e => console.log(e.message)); // "参数非法"需要注意一下reject和throw的区别:
- reject只改变状态,不会中断函数的执行,也就是写在reject后面的代码依旧会继续执行。
- throw会立即中断当前函数的执行。
三、中断 Promise
// 返回一个pending状态的promise(但不推荐) // 更优雅的处理还是使用AbortController、throw/reject Promise.resolve('start') .then(v => { console.log(v); return new Promise(() => {}); }) .then(v => console.log('这行永远不会执行')) .catch(e => console.log('这行也永远不会执行'));Promise 系列持续更新中...
《从零手写 Promise:深入理解异步编程核心原理》
《Promise 工程化实战:请求并发、链式封装与错误治理》
《EventLoop 深度解析:从浏览器原理到项目实战》