• 回调更容易形成深度嵌套的结构(也称为回调地狱)。如下所示:

    a(() => {
      b(() => {
        c(() => {
          d(() => {
            // and so on ...
          });
        });
      });
    });

    如果将这些函数转换为 Promise,则可以将它们链接起来以生成更可维护的代码。像这样:

    Promise.resolve()
      .then(a)
      .then(b)
      .then(c)
      .then(d)
      .catch(console.error);

    在上面的示例中,Promise 对象公开了.then.catch方法,我们稍后将探讨这些方法。

    1.1 如何将现有的回调 API 转换为 Promise?

    我们可以使用 Promise 构造函数将回调转换为 Promise。

    Promise 构造函数接受一个回调,带有两个参数resolvereject

    构造函数立即返回一个对象,即 Promise 实例。当在 promise 实例中使用.then方法时,可以在Promise “完成” 时得到通知。让我们来看一个例子。

    Promise 仅仅只是回调?

    并不是。承诺不仅仅是回调,但它们确实对.then.catch方法使用了异步回调。Promise 是回调之上的抽象,我们可以链接多个异步操作并更优雅地处理错误。来看看它的实际效果。

    Promise  反面模式(Promises 地狱)

    a(() => {
      b(() => {
        c(() => {
          d(() => {
            // and so on ...
          });
        });
      });
    });

    不要将上面的回调转成下面的 Promise 形式:

    a().then(() => {
      return b().then(() => {
        return c().then(() => {
          return d().then(() =>{
            // ⚠️ Please never ever do to this! ⚠️
          });
        });
      });
    });

    上面的转成,也形成了 Promise 地狱,千万不要这么转。相反,下面这样做会好点:

    a()
      .then(b)
      .then(c)
      .then(d)

    超时

    你认为以下程序的输出的是什么?

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('time is up ⏰');
      }, 1e3);

      setTimeout(() => {
        reject('Oops 🔥');
      }, 2e3);
    });

    promise
      .then(console.log)
      .catch(console.error);

    是输出:

    time is up ⏰
    Oops! 🔥

    还是输出:

    time is up ⏰

    是后者,因为当一个Promise resolved 后,它就不能再被rejected

    一旦你调用一种方法(resolvereject),另一种方法就会失效,因为 promise 处于稳定状态。让我们探索一个 promise 的所有不同状态。

    1.2 Promise 状态

    Promise 可以分为四个状态:

    一个小白的角度看JavaScript Promise 完整指南-LMLPHP

    1.3 Promise 实例方法

    Promise API 公开了三个主要方法:thencatchfinally。我们逐一配合事例探讨一下。

    Promise then

    then方法可以让异步操作成功或失败时得到通知。它包含两个参数,一个用于成功执行,另一个则在发生错误时使用。

    promise.then(onSuccess, onError);

    你还可以使用catch来处理错误:

    promise.then(onSuccess).catch(onError);

    Promise 链

    then 返回一个新的 Promise ,这样就可以将多个Promise 链接在一起。就像下面的例子一样:

    Promise.resolve()
      .then(() => console.log('then#1'))
      .then(() => console.log('then#2'))
      .then(() => console.log('then#3'));

    Promise.resolve立即将Promise 视为成功。因此,以下所有内容都将被调用。输出将是

    then#1
    then#2
    then#3

    Promise catch

    Promise  .catch方法将函数作为参数处理错误。如果没有出错,则永远不会调用catch方法。

    假设我们有以下承诺:1秒后解析或拒绝并打印出它们的字母。

    const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3));
    const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 1e3));
    const c = () => new Promise((resolve, reject) => setTimeout(() => { console.log('c'), reject('Oops!') }, 1e3));
    const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 1e3));

    请注意,c使用reject('Oops!')模拟了拒绝。

    Promise.resolve()
      .then(a)
      .then(b)
      .then(c)
      .then(d)
      .catch(console.error)

    输出如下:


    一个小白的角度看JavaScript Promise 完整指南-LMLPHP

    在这种情况下,可以看到abc上的错误消息。

    我们可以使用then函数的第二个参数来处理错误。但是,请注意,catch将不再执行。

    Promise.resolve()
      .then(a)
      .then(b)
      .then(c)
      .then(d, () => console.log('c errored out but no big deal'))
      .catch(console.error)

    一个小白的角度看JavaScript Promise 完整指南-LMLPHP

    由于我们正在处理 .then(..., onError)部分的错误,因此未调用catchd不会被调用。如果要忽略错误并继续执行Promise链,可以在c上添加一个catch。像这样:

    Promise.resolve()
      .then(a)
      .then(b)
      .then(() => c().catch(() => console.log('error ignored')))
      .then(d)
      .catch(console.error)


    一个小白的角度看JavaScript Promise 完整指南-LMLPHP

    当然,这种过早的捕获错误是不太好的,因为容易在调试过程中忽略一些潜在的问题。

    Promise finally

    finally方法只在 Promise 状态是 settled 时才会调用。

    如果你希望一段代码即使出现错误始终都需要执行,那么可以在.catch之后使用.then

    Promise.resolve()
      .then(a)
      .then(b)
      .then(c)
      .then(d)
      .catch(console.error)
      .then(() => console.log('always called'));

    或者可以使用.finally关键字:

    Promise.resolve()
      .then(a)
      .then(b)
      .then(c)
      .then(d)
      .catch(console.error)
      .finally(() => console.log('always called'));

    1.4 Promise 类方法

    我们可以直接使用 Promise 对象中四种静态方法。

    Promise.resolve 和  Promise.reject

    这两个是帮助函数,可以让 Promise 立即解决或拒绝。可以传递一个参数,作为下次 .then 的接收:

    Promise.resolve('Yay!!!')
      .then(console.log)
      .catch(console.error)

    上面会输出 Yay!!!

    Promise.reject('Oops 🔥')
      .then(console.log)
      .catch(console.error)

    使用 Promise.all 并行执行多个 Promise

    通常,Promise 是一个接一个地依次执行的,但是你也可以并行使用它们。

    假设是从两个不同的api中轮询数据。如果它们不相关,我们可以使用Promise.all()同时触发这两个请求。

    在此示例中,主要功能是将美元转换为欧元,我们有两个独立的 API 调用。一种用于BTC/USD,另一种用于获得EUR/USD。如你所料,两个 API 调用都可以并行调用。但是,我们需要一种方法来知道何时同时完成最终价格的计算。我们可以使用Promise.all,它通常在启动多个异步任务并发运行并为其结果创建承诺之后使用,以便人们可以等待所有任务完成。

    const axios = require('axios');

    const bitcoinPromise = axios.get('https://api.coinpaprika.com/v1/coins/btc-bitcoin/markets');
    const dollarPromise = axios.get('https://api.exchangeratesapi.io/latest?base=USD');
    const currency = 'EUR';

    // Get the price of bitcoins on
    Promise.all([bitcoinPromise, dollarPromise])
      .then(([bitcoinMarkets, dollarExchanges]) => {
        const byCoinbaseBtc = d => d.exchange_id === 'coinbase-pro' && d.pair === 'BTC/USD';
        const coinbaseBtc = bitcoinMarkets.data.find(byCoinbaseBtc)
        const coinbaseBtcInUsd = coinbaseBtc.quotes.USD.price;
        const rate = dollarExchanges.data.rates[currency];
        return rate * coinbaseBtcInUsd;
      })
      .then(price => console.log(`The Bitcoin in ${currency} is ${price.toLocaleString()}`))
      .catch(console.log);

    如你所见,Promise.all接受了一系列的 Promises。当两个请求的请求都完成后,我们就可以计算价格了。

    我们再举一个例子:

    const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
    const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
    const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
    const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

    console.time('promise.all');
    Promise.all([a(), b(), c(), d()])
      .then(results => console.log(`Done! ${results}`))
      .catch(console.error)
      .finally(() => console.timeEnd('promise.all'));

    解决这些 Promise 要花多长时间?5秒?1秒?还是2秒?

    这个留给你们自己验证咯。

    Promise race

    Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

    const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
    const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
    const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
    const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

    console.time('promise.race');
    Promise.race([a(), b(), c(), d()])
      .then(results => console.log(`Done! ${results}`))
      .catch(console.error)
      .finally(() => console.timeEnd('promise.race'));

    输出是什么?

    输出 b。使用 Promise.race,最先执行完成就会结果最后的返回结果。

    你可能会问:Promise.race的用途是什么?

    我没胡经常使用它。但是,在某些情况下,它可以派上用场,比如计时请求或批量处理请求数组。

    Promise.race([
      fetch('http://slowwly.robertomurray.co.uk/delay/3000/url/https://api.jsonbin.io/b/5d1fb4dd138da811182c69af'),
      new Promise((resolve, reject) => setTimeout(() => reject(new Error('request timeout')), 1000))
    ])
    .then(console.log)
    .catch(console.error);

    一个小白的角度看JavaScript Promise 完整指南-LMLPHP

    如果请求足够快,那么就会得到请求的结果。

    一个小白的角度看JavaScript Promise 完整指南-LMLPHP

    1.5 Promise 常见问题

    串行执行 promise 并传递参数

    这次,我们将对Node的fs使用promises API,并将两个文件连接起来:

    const fs = require('fs').promises; // requires node v8+

    fs.readFile('file.txt''utf8')
      .then(content1 => fs.writeFile('output.txt', content1))
      .then(() => fs.readFile('file2.txt''utf8'))
      .then(content2 => fs.writeFile('output.txt', content2, { flag: 'a+' }))
      .catch(error => console.log(error));

    在此示例中,我们读取文件1并将其写入output 文件。稍后,我们读取文件2并将其再次附加到output文件。如你所见,writeFile promise返回文件的内容,你可以在下一个then子句中使用它。

    如何链接多个条件承诺?

    你可能想要跳过 Promise 链上的特定步骤。有两种方法可以做到这一点。

    const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3));
    const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 2e3));
    const c = () => new Promise((resolve) => setTimeout(() => { console.log('c'), resolve() }, 3e3));
    const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 4e3));

    const shouldExecA = true;
    const shouldExecB = false;
    const shouldExecC = false;
    const shouldExecD = true;

    Promise.resolve()
      .then(() => shouldExecA && a())
      .then(() => shouldExecB && b())
      .then(() => shouldExecC && c())
      .then(() => shouldExecD && d())
      .then(() => console.log('done'))

    如果你运行该代码示例,你会注意到只有ad被按预期执行。

    另一种方法是创建一个链,然后仅在以下情况下添加它们:

    const chain = Promise.resolve();

    if (shouldExecA) chain = chain.then(a);
    if (shouldExecB) chain = chain.then(b);
    if (shouldExecC) chain = chain.then(c);
    if (shouldExecD) chain = chain.then(d);

    chain
      .then(() => console.log('done'));

    如何限制并行 Promise?

    要做到这一点,我们需要以某种方式限制Promise.all

    假设你有许多并发请求要执行。如果使用 Promise.all 是不好的(特别是在API受到速率限制时)。因此,我们需要一个方法来限制 Promise 个数, 我们称其为promiseAllThrottled

    // simulate 10 async tasks that takes 5 seconds to complete.
    const requests = Array(10)
      .fill()
      .map((_, i) => () => new Promise((resolve => setTimeout(() => { console.log(`exec'ing task #${i}`), resolve(`task #${i}`); }, 5000))));

    promiseAllThrottled(requests, { concurrency: 3 })
      .then(console.log)
      .catch(error => console.error('
    Oops something went wrong', error));

    输出应该是这样的:


    一个小白的角度看JavaScript Promise 完整指南-LMLPHP

    以上代码将并发限制为并行执行的3个任务。

    实现promiseAllThrottled 一种方法是使用Promise.race来限制给定时间的活动任务数量。

    /**
     * Similar to Promise.all but a concurrency limit
     *
     * @param {Array} iterable Array of functions that returns a promise
     * @param {Object} concurrency max number of parallel promises running
     */
    function promiseAllThrottled(iterable, { concurrency = 3 } = {}) {
      const promises = [];

      function enqueue(current = 0, queue = []) {
        // return if done
        if (current === iterable.length) { return Promise.resolve(); }
        // take one promise from collection
        const promise = iterable[current];
        const activatedPromise = promise();
        // add promise to the final result array
        promises.push(activatedPromise);
        // add current activated promise to queue and remove it when done
        const autoRemovePromise = activatedPromise.then(() => {
          // remove promise from the queue when done
          return queue.splice(queue.indexOf(autoRemovePromise), 1);
        });
        // add promise to the queue
        queue.push(autoRemovePromise);

        // if queue length >= concurrency, wait for one promise to finish before adding more.
        const readyForMore = queue.length < concurrency ? Promise.resolve() : Promise.race(queue);
        return readyForMore.then(() => enqueue(current + 1, queue));
      }

      return enqueue()
        .then(() => Promise.all(promises));
    }

    promiseAllThrottled一对一地处理 Promises 。它执行Promises并将其添加到队列中。如果队列小于并发限制,它将继续添加到队列中。达到限制后,我们使用Promise.race等待一个承诺完成,因此可以将其替换为新的承诺。这里的技巧是,promise 自动完成后会自动从队列中删除。另外,我们使用 race 来检测promise 何时完成,并添加新的 promise 。

    参考书籍

       


    本文分享自微信公众号 - 胡哥有话说(hugeyouhuashuo)。
    如有侵权,请联系 [email protected] 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    09-10 16:56