Promise对象
JavaScript 中异步编程一直是重难点。传统的回调函数虽然解决了问题,但它们复杂、难以维护,且容易引发回调地狱。Promise
对象的出现,以其独特的状态管理和链式调用,为异步编程带来了革命性的改变。阮一峰老师将深入探讨 Promise
的基本概念、使用方法以及其在现代 JavaScript 开发中的重要性,带你掌握异步编程的新范式。
含义
Promise
是异步编程的一种解决方案,比传统的回调函数更合理和强大。Promise
是一个容器,存放着未来会发生的事件(例如回调函数),从语法上说,Promise
是一个对象,从它可以获取异步操作的消息。它的出现大大简化了异步编程。
Promise
对象有两个特点:
- 对象状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 - 状态一旦发生就不会再改变。对
Promise
对象而言,状态只有从pending
变为fulfilled
和从pending
变为rejected
,只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved
(已定型),后续随时都能监听到这个状态。事件Event
与它不同,错过了就不再能监听到。
Promise
也有一些缺点:
- 无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 - 如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部。 - 当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
基本用法
Promise
对象是一个构造函数,用于生成 Promise
实例。它接受一个函数作为参数,该函数的两个参数分别是 resolve
和 reject
,由 JavaScript 引擎提供,不用自己部署。
Promise
接受一个函数作为参数,该函数有两个参数 resolve
和 reject
。resolve
的作用是 Promise
对象状态从 pending
变为 resolved
;reject
的作用是把状态从 pending
变为 rejected
。
new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
Promise
实例生成后,可以用 .then
方法分别指定 resolve
和 reject
状态的回调函数,接受两个回调函数作为参数。第一个回调函数是 Promise
对象状态变为 resolved
时调用,第二个回调函数是 Promise
对象状态变为 rejected
时调用。这两个函数都接受 Promise
传递的值作为参数。其中,第二个参数是可选的。
new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
}).then(function(value) {
// success
}, function(error) {
// failure
});
Promise
对象新建后会立即执行,且调用 resolve
和 reject
不会终止 Promise
的参数函数执行。.then
方法指定的回调函数会在当前脚本所有 同步任务 执行完才会执行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise start');
resolve();
console.log('Promise end');
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise start
// Promise end
// Hi!
// resolved.
一般的,调用 resolve
或者 reject
后,Promise
的使命就完成了,后继操作应该放到 then
方法里面,而不应该直接写在 resolve
或 reject
的后面。所以,最好在它们前面加上 return
语句,这样就不会有意外。
Promise.prototype.then()
Promise
原型对象上具有 .then
方法,作用是为 Promise
实例添加状态改变时的回调函数。返回的是一个新的 Promise
实例,因此可以采用链式写法。
new Promise((resolve, reject) => {
resolve(1);
}).then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
return value * 2;
})
Promise.prototype.catch()
Promise.prototype.catch
方法是 .then(null, rejection)
的别名,用于指定发生错误时的回调函数。当 Promise
对象状态变为 reject
或 .then
方法运行中抛出错误时,会调用 catch
方法指定的回调函数。
new Promise((resolve, reject) => {
reject('error');
}).catch((error) => {
console.log(error);
})
Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch
捕获。一般来说,不要在 then
方法里面定义 reject
回调函数,应该使用 catch
方法捕获错误 ❌。
new Promise((resolve, reject) => {
resolve();
}).then((value) => {
// some ode
}).then((value) => {
// some ode
}).catch((error) => {
console.log(error);
})
与 try...catch
语句不同的是,如果没有使用 catch
方法指定错误处理的回调函数,Promise
对象抛出的错误不会传递到外层代码,即不会有任何反应。只有 Promise
指定在下一轮 事件循环 再抛出错误,才会冒泡到外层。
// 不冒泡
new Promise((resolve, reject) => {
return x + 1
})
console.log('everything is ok') // 正常打印
// 冒泡
new Promise((resolve, reject) => {
resolve(1)
setTimeout(() => { throw new Error('error') }, 0)
}).catch((error) => {
console.log(error)
})
Promose.all()
Promise.all
方法用于将多个 Promise
实例,包装成一个新的 Promise
实例。其参数一般接受一个数组,如果不是数组则要求必须具有 Iterator
接口,且每一项都必须是 Promise
实例。如果不是,则会调用 Promise.resolve
方法,将参数转为 Promise
实例,再进一步处理。
Promise.all
方法返回的新的 Promise
实例,该实例的状态由数组内每一项 PRomise
决定,分为两种情况:
- 只有当参数中的所有
Promise
实例都变为fulfilled
状态,才会变为fulfilled
状态 - 只要有一个变为
rejected
状态,就会变为rejected
状态,第一个变为rejected
状态的实例的返回值,会传递给Promise.all
方法返回的新实例的回调函数
⚠ 注意
如果数组内的 Promise
实例,有自己的 .catch
方法,那么它们抛出的错误不会传递到 Promise.all
方法上,会由自身 .catch
方法捕获;如果没有,则会传递到 Promise.all
方法上。
const p1 = new Promise((resolve, reject) => {
reject('p1 error');
})
const p2 = new Promise((resolve, reject) => {
reject('error');
}).catch((error) => {
console.log('p2 error') // p2 error
})
Promise.all([p1, p2]).then((value) => {
console.log(value);
}).catch((error) => {
console.log(error) // p1 error
})
Promise.race()
Promise.race
方法与 Promise.all
方法相同,同样是将多个 Promise
实例,包装成一个新的 Promise
实例。每个参数都是 Promise
实例,如果不是则调用 Promise.resolve
转换。
不同的是,只要有一个 Promise
实例状态变为 fulfilled
或 rejected
,新的 Promise
实例就会变为 fulfilled
或 rejected
状态,并返回第一个变为 fulfilled
或 rejected
状态的实例的返回值。
Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]).then(response => {
console.log(response)
}).catch(error => {
console.log(error)
})
Promise.resolve()
Promise.resolve
方法用于将现有对象转为 Promise
对象,Promise.resolve
方法等价于下方这种写法:
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
参数分为四种情况:
- 参数是一个
Promise
实例,则返回该实例 - 参数是一个具有
.then
方法的对象,则把该方法转为Promise
对象,然后立即执行.then
方法 - 参数不是具有
.then
方法的对象或根本不是对象,则返回一个fulfilled
状态的Promise
对象,并且返回该参数 - 不带任何参数,则直接返回一个
fulfilled
状态的Promise
对象
Promise.reject()
Promise.reject
方法返回一个 rejected
状态的 Promise
实例,并将给定的参数作为 rejected
状态的理由,返回的 Promise
实例的回调函数会立即执行。
Promise.reject('出错了')
// 等价于
new Promise((resolve, reject) => reject('出错了'))
⚠ 注意
Promise.reject
方法与 Promise.resolve
方法不同的是,Promise.reject
方法的参数会原封不动作为 reject
状态的理由变成后续方法的参数。
let thenable = {
then(resolve, reject) {
reject('出错了');
}
}
Promise.reject(thenable).catch(e => {
console.log(e === thenable) // true
})
Promise.try()
有一种情况:想要把某个函数放到 Promise
内执行,这样就能在 .then
内执行后续的回调函数,.catch
执行报错后处理。但是不确定该函数是同步函数还是异步函数。
一般的,写法如下所示:
Promise.resolve().then(fn)
但是这么写有一个缺点,如果 fn
是一个同步函数,那么它会在本轮事件循环最后执行。
Promise.resolve().then(() => console.log('promise fn'))
console.log('console')
// console
// promise fn
想要同步函数同步执行,异步函数异步执行,有两个解决方法:
async
js(async () =>{ fn() })().then().catch()
通过立即执行函数,将
async
函数包裹起来,立即执行里面的async
函数,如果是同步的就同步执行;如果是异步的则.then
执行后续。最后通过.catch
捕获错误new Promise
js( () => new Promise((resolve) => { resolve(fn()) }) )
通过
new Promise
将函数包裹起来,如果是同步的则同步执行,如果是异步的则.then
执行后续。最后通过.catch
捕获错误
基于此,目前有一个提案提供了 PRomise.try()
方法代替上面的写法。
const fn = () => console.log('fn')
Promise.try(fn)
Promise.try()
方法可以用 Promise.catch
统一捕获所有同步或异步的报错。
部署有用的附加方法
done()
Promise
内部错误不会冒泡,如果最后一个 .catch
内部还抛出错误,则无法捕捉到。为此可以在 Promise
原型上挂一个 done
方法,作用是保证抛出任何有可能出现的错误
asyncFunc()
.then(f1)
.catch(r1)
.then(f2)
.done()
实现代码如下:
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 抛出一个全局错误
setTimeout(() => { throw reason }, 0)
})
}
finally()
finally
方法用于指定不管 Promise
对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
实现代码如下:
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
总结
Promise
对象是一种用于异步编程的解决方案,能够简化异步操作的处理。
Promise
对象有三个状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败),一旦状态改变,就不会再变,称为resolved(已定型)。
Promise
的构造函数接受一个函数,该函数有两个参数 resolve
和 reject
,分别用于改变状态。Promise
实例可以通过 .then
方法添加回调函数,处理成功或失败的情况,并通过链式调用实现复杂的异步流程。Promise.prototype.catch
方法用于捕获错误,而Promise.all
和Promise.race
方法用于处理多个 Promise
实例。
Promise.resolve
和 Promise.reject
方法用于将值或错误转化为 Promise
对象,Promise.try
方法用于统一处理同步和异步函数的执行。done
和 finally
方法用于确保错误被捕获和在 Promise
完成后执行某些操作。