首页你好哇,世界马蹄疾
Async专题:迟到的承诺
马蹄疾
2019-05-04 22:48:37

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); } } } }
#JavaScript
@Async
0comments withand markdown
输入"@+用户名+空格"可以AT某人,AT某人时可以按TAB键补全。
输入"登记过的邮箱"会自动补全剩余信息,可以登记原账号,也可以修改用户名再登记原账号。
先登记后评论,乖
性别: