文章506
标签266
分类65

Promise中的all、race和any

为了解决回调地狱问题,在现代JS中加入了Promise;并且Promise包括了all、race和any三个不同的方法;

本文分别讲述了这三个方法,以及具体的使用场景;

文章源代码:


Promise中的all、race和any

All方法

方法介绍

Promise.all可以将多个Promise实例包装成一个新的Promise实例;

同时,整个Promise数组在成功和失败的返回值是不同的:

  • Promise序列会全部执行通过才认为是成功,否则认为是失败;
  • 当所有Promise都成功时,返回一个结果数组;
  • 只要存在一个失败的Promise,则返回最先被reject失败状态的值;

下面是一个使用all的例子:

all_demo.js

let p1 = Promise.resolve("p1成功");
let p2 = Promise.resolve("p2成功");
let p3 = Promise.reject("p3失败");

Promise.all([p1, p2]).then((res) => {
    console.log(res); // [ 'p1成功', 'p2成功' ]
}).catch(err => {
    console.log(err);
});

Promise.all([p1, p2, p3]).then((res) => {
    console.log(res);
}).catch(err => {
    console.log(err); // p3失败
});

使用场景

① 同时处理多个异步请求

Promse.all在处理多个异步处理时非常有用,比如:

一个页面上需要等两个或多个ajax的数据全部返回以后才正常显示,在此之前只显示loading图标;

下面为一个例子:

all_demo_2.js

let wake = (time) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(`${time / 1000}秒后醒来`)
        }, time)
    })
}

let p1 = wake(3000)
let p2 = wake(2000)

Promise.all([p1, p2]).then((result) => {
    console.log(result)       // [ '3秒后醒来', '2秒后醒来' ]
}).catch((error) => {
    console.log(error)
})

需要特别注意的是:

  • Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚!
  • Promisze.all会等待所有的Promise返回后

这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题;

例如,在图片批量上传的时候很有用,可以知道什么时候这批图片全部上传完毕,保证了并行,同时知道最终的上传结果;


② 保证最低加载时间

当进行页面请求时,如果请求时间太短,Loading图标就会一闪而过,体验并不好;

这时可以使用Promise.all()保证最低Loading时间;

例如,下面的代码可以保证加载动画至少出现200ms:

all_demo_3.js

let getUserInfo = function (user) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve('Hello'), Math.floor(400 * Math.random()));
    });
}

let showUserInfo = function (user) {
    return getUserInfo().then(info => {
        console.log('用户信息', info);
        return true;
    });
}

let timeout = function (delay, result) {
    return new Promise(resolve => {
        setTimeout(() => resolve(result), delay);
    });
}

// loading时间显示需要
const time = +new Date();
let showToast = function () {
    console.log('show loading...');
}
let hideToast = function () {
    console.log('hide loading time: ' + (new Date() - time) + " ms");
}

// 执行代码示意
showToast();
Promise.all([showUserInfo(), timeout(200)]).then(() => {
    hideToast();
});

多次执行结果:

show loading...
用户信息 Hello
hide loading time: 266 ms
show loading...
用户信息 Hello
hide loading time: 205 ms
show loading...
用户信息 Hello
hide loading time: 358 ms

可以看到加载从显示到隐藏,一定不会小于200ms;


Race方法

方法介绍

Promse.race就是赛跑的意思,就是说Promise.race([p1, p2, p3…])中哪个Promise的结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态;

总结来说就是:

  • Promise序列中第一个执行完毕的是通过,则认为成功,如果第一个执行完毕的Promise是拒绝,则认为失败;(即只看第一个执行完毕的Promise);

例如:

race_demo.js

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('success')
    }, 1000)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('failed')
    }, 500)
})

Promise.race([p1, p2]).then((result) => {
    console.log(result)
}).catch((error) => {
    console.log(error)  // 返回的是 'failed'
})

使用场景

① 根据加载时长展示

在上面上面的加载例子中仔细一想,有些怪怪的:如果请求本来很快,还非要显示一个Loading过程,这不是舍本逐末了吗?

所以需求应该是这样:

  • 如果请求可以在200ms内完成,则不显示loading;
  • 如果要超过200ms,则至少显示200ms的loading;

此时,这个需求可以考虑使用Promise.race()方法,执行代码示意如下(getUserInfo、showUserInfo等方法都不变):

race_demo_2.js

let getUserInfo = function (user) {
    ...
}

let showUserInfo = function (user) {
    ...
}

let timeout = function (delay, result) {
    ...
}

const time = +new Date();

let showToast = function () {
    ...
}

let hideToast = function () {
    ...
}

// 执行代码示意
let promiseUserInfo = showUserInfo();
Promise.race([promiseUserInfo, timeout(200)]).then((display) => {
    if (!display) {
        showToast();
        Promise.all([promiseUserInfo, timeout(200)]).then(() => {
            hideToast();
        });
    }
});

于是,要么用户信息无Loading瞬间显示,要么显示至少200ms的loading,这样的体验就会更细致了;

执行结果如下:

用户信息 Hello

show loading...
用户信息 Hello
hide loading time: 407 ms

② 可取消的Promise

案例出自Michael Clark,代码如下:

race_demo_3.js

function timeout(delay) {
    let cancel;
    const wait = new Promise(resolve => {
        const timer = setTimeout(() => resolve(false), delay);
        cancel = () => {
            clearTimeout(timer);
            resolve(true);
        };
    });
    wait.cancel = cancel;
    return wait;
}

function doWork() {
    const workFactor = Math.floor(600 * Math.random());
    const work = timeout(workFactor);

    const result = work.then(canceled => {
        if (canceled)
            console.log('Work canceled');
        else
            console.log('Work done in', workFactor, 'ms');
        return !canceled;
    });
    result.cancel = work.cancel;
    return result;
}

function attemptWork() {
    const work = doWork();
    return Promise.race([work, timeout(300)])
        .then(done => {
            if (!done)
                work.cancel();
            return (done ? 'Work complete!' : 'I gave up');
        });
}

attemptWork().then(console.log);

执行结果:

Work done in 21 ms
Work complete!

Work canceled
I gave up

所示例子中,doWork是一个花费0~600ms的工作,如果工作大于300ms则撤销,如果工作小于300ms则完成;


③ 长时间执行的批处理

代码出自Chris Jensen,可以保持并行请求的数量固定;

const _ = require('lodash')

async function batchRequests(options) {
    let query = { offset: 0, limit: options.limit };

    do {
        batch = await model.findAll(query);
        query.offset += options.limit;

        if (batch.length) {
            const promise = doLongRequestForBatch(batch).then(() => {
                // Once complete, pop this promise from our array
                // so that we know we can add another batch in its place
                _.remove(promises, p => p === promise);
            });
            promises.push(promise);

            // Once we hit our concurrency limit, wait for at least one promise to
            // resolve before continuing to batch off requests
            if (promises.length >= options.concurrentBatches) {
                await Promise.race(promises);
            }
        }
    } while (batch.length);

    // Wait for remaining batches to finish
    return Promise.all(promises);
}

batchRequests({ limit: 100, concurrentBatches: 5 });

Any方法

方法介绍

和All方法类似,Any也是接收一个Promise数组;

同时,整个Promise数组在成功和失败的返回值也是不同的:

  • Promise序列只要有一个执行通过,则认为成功,如果全部拒绝,则认为失败;

下面是一个使用Any的例子:

let p1 = Promise.resolve("p1成功");
let p2 = Promise.resolve("p2成功");
let p3 = Promise.reject("p3失败");
let p4 = Promise.reject("p4失败")

Promise.any([p1, p2]).then((res) => {
    console.log(res); // p1成功
}).catch(err => {
    console.log(err);
});

Promise.any([p1, p2, p3]).then((res) => {
    console.log(res); // p1成功
}).catch(err => {
    console.log(err);
});

Promise.any([p3, p4]).then((res) => {
    console.log(res);
}).catch(err => {
    console.log(err); // AggregateError: All promises were rejected
});

注意:Promise.any尚未添加至Node中,所以上面的代码可能无法在Node中执行,需要在Chrome等浏览器中执行;

具体:


使用场景

Promise.any()适合用在通过不同路径请求同一个资源的需求上;

例如,Vue3.0在unpkg和jsdelivr都有在线的CDN资源,都是国外的CDN,国内直接调用不确定哪个站点会抽风,加载慢,这时候可以两个资源都请求,哪个请求先成功就使用哪一个;

比方说:

我们就可以使用下面代码进行请求(使用动态 import 示意):

any_demo_2.js

let startTime = +new Date();
let importUnpkg = import('https://unpkg.com/vue@3.0.11/dist/vue.runtime.esm-browser.js');
let importJsdelivr = import('https://cdn.jsdelivr.net/npm/vue@3.0.11/dist/vue.runtime.esm-browser.js');
Promise.any([importUnpkg, importJsdelivr]).then(vue => {
  console.log('加载完毕,时间是:' + (+new Date() - startTime) + 'ms');
  console.log(vue.version);
});

输出如下:

You are running a development build of Vue.
Make sure to use the production build (*.prod.js) when deploying for production.
加载完毕,时间是:620ms
3.0.11
You are running a development build of Vue.
Make sure to use the production build (*.prod.js) when deploying for production.

620ms完成,但是实际上,两个JS的请求时间差异是挺大的;

不过没关系,有了 Promise.any() ,就可以使用最快的那一个;

此外,Promise.any()还有一个好处,那就是如果 unpkg 这个网站挂了,也不会影响 Vue 资源的加载,因为一个请求失败了,会继续请求其他的资源,也就是会去请求 jsdelivr 的资源;

这样保证了资源尽可能可用,但是尽可能使用加载最快的资源;

在这种场景下就很实用;


附录

源代码:

文章参考:


本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/2021/05/14/Promise中的all、race和any/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可