1. 首页

第2篇:迟到的承诺之细说Async专题

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);
            }
        }
    }
}

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:第2篇:迟到的承诺之细说Async专题

链接:https://www.javascriptc.com/2940.html

« Nginx 与前端开发
第1篇:事件循环之细说Async专题»
Flutter 中文教程资源

相关推荐

QR code