Async 专题 02 —— 迟到的承诺
Promise是一个表现为状态机的异步容器。
它有以下几个特点:
- 状态不受外界影响。Promise只有三种状态:
pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。状态只能通过Promise内部提供的resolve()
和reject()
函数改变。 - 状态只能从
pending
变为fulfilled
或者从pending
变为rejected
。并且一旦状态改变,状态就会被冻结,无法再次改变。
new Promise((resolve, reject) => {
reject('reject');
setTimeout(() => resolve('resolve'), 5000);
}).then(console.log, console.error);
// 不要等了,它只会打印一个 reject
- 如果状态发生改变,任何时候都可以获得最终的状态,即便改变发生在前。这与事件监听完全不一样,事件监听只能监听之后发生的事件。
const promise = new Promise(resolve => resolve('biu'));
promise.then(console.log);
setTimeout(() => promise.then(console.log), 5000);
// 打印 biu,相隔大约 5 秒钟后又打印 biu
正是源于这些特点,Promise才敢于称自己为一个承诺
。
同步代码与异步代码
Promise是一个异步容器,那哪些部分是同步执行的,哪些部分是异步执行的呢?
console.log('kiu');
new Promise((resolve, reject) => {
console.log('miu');
resolve('biu');
console.log('niu');
}).then(console.log, console.error);
console.log('piu');
我们看执行结果。
kiu
miu
niu
piu
biu
可以看到,Promise构造函数的参数函数是完完全全的同步代码,只有状态改变触发的then回调才是异步代码。为啥说Promise是一个异步容器?它不关心你给它装的是啥,它只关心状态改变后的异步执行,并且承诺给你一个稳定的结果。
从这点来看,Promise真的只是一个异步容器而已。
Promise.prototype.then()
then方法接受两个回调作为参数,状态变成fulfilled
时会触发第一个回调,状态变成rejected
时会触发第二个回调。你可以认为then回调是Promise这个异步容器的界面和输出,在这里你可以获得你想要的结果。
then函数可以实现链式调用吗?可以的。
但你想一下,then回调触发的时候,Promise的状态已经冻结了。这时候它就是被打开盒子的薛定谔的猫,它要么是死的,要么是活的。也就是说,它不可能再次触发then回调。
那then函数是如何实现链式调用的呢?
原理就是then函数自身返回的是一个新的Promise实例。再次调用then函数的时候,实际上调用的是这个新的Promise实例的then函数。
既然Promise只是一个异步容器而已,换一个容器也不会有什么影响。
const promiseA = new Promise((resolve, reject) => resolve('biu'));
const promiseB = promiseA.then(value => {
console.log(value);
return value;
});
const promiseC = promiseB.then(console.log);
结果是打印了两个 biu。
const promiseA = new Promise((resolve, reject) => resolve('biu'));
const promiseB = promiseA.then(value => {
console.log(value);
return Promise.resolve(value);
});
const promiseC = promiseB.then(console.log);
Promise.resolve()
我们后面会讲到,它返回一个状态是fulfilled
的Promise实例。
这次我们手动返回了一个状态是fulfilled
的新的Promise实例,可以发现结果和上一次一模一样。说明then函数悄悄的将return 'biu'
转成了return Promise.resolve('biu')
。如果没有返回值呢?那就是转成return Promise.resolve()
,反正得转成一个新的状态是fulfilled
的Promise实例返回。
这就是then函数返回的总是一个新的Promise实例的内部原理。
想要让新Promise实例的状态从pending
变成rejected
,有什么办法吗?毕竟then方法也没给我们提供reject
方法。
const promiseA = new Promise((resolve, reject) => resolve('biu'));
const promiseB = promiseA.then(value => {
console.log(value);
return x;
});
const promiseC = promiseB.then(console.log, console.error);
查看这里的输出结果。
biu
ReferenceError: x is not defined
at <anonymous>:6:5
只有程序本身发生了错误,新Promise实例才会捕获这个错误,并把错误暗地里传给reject
方法。于是状态从pending
变成rejected
。
Promise.prototype.catch()
catch方法,顾名思义是用来捕获错误的。它其实是then方法某种方式的语法糖,所以下面两种写法的效果是一样的。
new Promise((resolve, reject) => {
reject('biu');
}).then(
undefined,
error => console.error(error),
);
new Promise((resolve, reject) => {
reject('biu');
}).catch(
error => console.error(error),
);
Promise内部的错误会静默处理。你可以捕获到它,但错误本身已经变成了一个消息,并不会导致外部程序的崩溃和停止执行。
下面的代码运行中发生了错误,所以容器中后面的代码不会再执行,状态变成rejected
。但是容器外面的代码不受影响,依然正常执行。
new Promise((resolve, reject) => {
console.log(x);
console.log('kiu');
resolve('biu');
}).then(console.log, console.error);
setTimeout(() => console.log('piu'), 5000);
所以大家常常说"Promise会吃掉错误"。
如果状态已经冻结,即便运行中发生了错误,Promise也会忽视它。
new Promise((resolve, reject) => {
resolve('biu');
console.log(x);
}).then(console.log, console.error);
setTimeout(() => console.log('piu'), 5000);
Promise的错误如果没有被及时捕获,它会往下传递,直到被捕获。中间没有捕获代码的then函数就被忽略了。
new Promise((resolve, reject) => {
console.log(x);
resolve('biu');
}).then(
value => console.log(value),
).then(
value => console.log(value),
).then(
value => console.log(value),
).catch(
error => console.error(error),
);
Promise.prototype.finally()
所谓finally就是一定会执行的方法。它和then或者catch不一样的地方在于,finally方法的回调函数不接受任何参数。也就是说,它不关心容器的状态,它只是一个兜底的。
new Promise((resolve, reject) => {
// 逻辑
}).then(
value => {
// 逻辑
console.log(value);
},
error => {
// 逻辑
console.error(error);
}
);
new Promise((resolve, reject) => {
// 逻辑
}).finally(
() => {
// 逻辑
}
);
如果有一段逻辑,无论状态是fulfilled
还是rejected
都要执行,那放在then函数中就要写两遍,而放在finally函数中就只需要写一遍。
另外,别被finally这个名字带偏了,它不一定要定义在最后的。
new Promise((resolve, reject) => {
resolve('biu');
}).finally(
() => console.log('piu'),
).then(
value => console.log(value),
).catch(
error => console.error(error),
);
finally函数在链条中的哪个位置定义,就会在哪个位置执行。从语义化的角度讲,finally
不如叫anyway
。
Promise.all()
它接受一个由Promise实例组成的数组,然后生成一个新的Promise实例。这个新Promise实例的状态由数组的整体状态决定,只有数组的整体状态都是fulfilled
时,新Promise实例的状态才是fulfilled
,否则就是rejected
。这就是all
的含义。
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).then(
values => console.log(values),
).catch(
error => console.error(error),
);
Promise.all([Promise.resolve(1), Promise.reject(2), Promise.resolve(3)]).then(
values => console.log(values),
).catch(
error => console.error(error),
);
数组中的项目如果不是一个Promise实例,all函数会将它封装成一个Promise实例。
Promise.all([1, 2, 3]).then(
values => console.log(values),
).catch(
error => console.error(error),
);
Promise.race()
它的使用方式和Promise.all()
类似,但是效果不一样。
Promise.all()
是只有数组中的所有Promise实例的状态都是fulfilled
时,它的状态才是fulfilled
,否则状态就是rejected
。
而Promise.race()
则只要数组中有一个Promise实例的状态是fulfilled
,它的状态就会变成fulfilled
,否则状态就是rejected
。
就是&&
和||
的区别是吧。
它们的返回值也不一样。
Promise.all()
如果成功会返回一个数组,里面是对应Promise实例的返回值。
而Promise.race()
如果成功会返回最先成功的那一个Promise实例的返回值。
function fetchByName(name) {
const url = `https://api.github.com/users/${name}/repos`;
return fetch(url).then(res => res.json());
}
const timingPromise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('网络请求超时')), 5000);
});
Promise.race([fetchByName('veedrin'), timingPromise]).then(
values => console.log(values),
).catch(
error => console.error(error),
);
上面这个例子可以实现网络超时触发指定操作。
Promise.resolve()
它的作用是接受一个值,返回一个状态是fulfilled
的Promise实例。
Promise.resolve('biu');
new Promise(resolve => resolve('biu'));
它是以上写法的语法糖。
Promise.reject()
它的作用是接受一个值,返回一个状态是rejected
的Promise实例。
Promise.reject('biu');
new Promise((resolve, reject) => reject('biu'));
它是以上写法的语法糖。
嵌套Promise
如果Promise有嵌套,它们的状态又是如何变化的呢?
const promise = Promise.resolve(
(() => {
console.log('a');
return Promise.resolve(
(() => {
console.log('b');
return Promise.resolve(
(() => {
console.log('c');
return new Promise(resolve => {
setTimeout(() => resolve('biu'), 3000);
});
})()
)
})()
);
})()
);
promise.then(console.log);
可以看到,例子中嵌套了四层Promise。别急,我们先回顾一下没有嵌套的情况。
const promise = Promise.resolve('biu');
promise.then(console.log);
我们都知道,它会在微任务时机执行,肉眼几乎看不到等待。
但是嵌套了四层Promise的例子,因为最里层的Promise需要等待几秒才resolve,所以最外层的Promise返回的实例也要等待几秒才会打印日志。也就是说,只有最里层的Promise状态变成fulfilled
,最外层的Promise状态才会变成fulfilled
。
如果你眼尖的话,你就会发现这个特性就是Koa中间件机制的精髓。
Koa中间件机制也是必须得等最后一个中间件resolve(如果它返回的是一个Promise实例的话)之后,才会执行洋葱圈另外一半的代码。
function compose(middleware) {
return function(context, next) {
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1);
}));
} catch (err) {
return Promise.reject(err);
}
}
}
}