即使有了 async/await,原生 Promise 对于编写最理想的并行 JS 仍然十分重要

原文  https://medium.com/@bluepnume...javascript

随着 es2017 即将到来,async/await 的时代也就不远了。在以前的文章中 我建议要充分掌握 Promise ,由于它是创建 async/await 的基础。理解 Promise 有助于理解 async/await 的基础概念,并有助于你编写更好的 async 函数。java

可是,即便你已紧跟 async 潮流(这是我我的喜欢的)而且彻底理解 Promise,在异步函数中继续使用它们仍然还有一些很是使人信服的理由。async/await 绝对不会让你彻底摆脱每一种状况。为何?很简单:node

  • 你可能仍然须要去编写一些运行在浏览器上的代码。git

  • 单纯用 async/await 来编写并行代码有时候是不可能或不容易的。github

为浏览器编写代码?Babel 不就能够解决吗?

很明显除非你是纯粹为 node 服务端编写,不然你将不得不考虑在浏览器中运行你的 javascript。经过 Babel 编译,可使 ES2015+ 编写的代码运行在较老的浏览器中,或者还可使用 Facebook 的一款优秀编译器 Regenerator,Babel 甚至会能将 async/await 编译为向下兼容的代码。ajax

问题获得解决,而后呢?好吧,这并不彻底是。c#

关键点在于,生成的代码并不必定是你想在客户端上运行的。例以下面一个简单的 async 函数,它使用异步映射函数对数组进行连续映射:数组

async function serialAsyncMap(collection, fn) {
  let result = [];
  
  for (let item of collection) {
    result.push(await fn(item));
  }
  
  return result;
}

这是由 Babel/Regenerator 编译后的 56 行代码:promise

var serialAsyncMap = function () {
  var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(collection, fn) {
    var result, _iterator, _isArray, _i, _ref2, item;
return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            result = [];
            _iterator = collection, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();

请参阅完整代码浏览器

这仅仅是编译结果中的 7 行代码。顺便说下,在 bundle 前甚至引入了 regeneratorRuntime_asyncToGenerator。虽然 Regenerator 是一个很好的技术功能,可是它并不能编译出最精简的代码来运行在浏览器中,我猜想它并非最优或性能最佳的代码。它也很难阅读、理解和调试。

假设若是咱们使用原生 Promise 来编写相同的函数:

function serialAsyncMap(collection, fn) {
  
  let results = [];
  let promise = Promise.resolve();
  
  for (let item of collection) {
    promise = promise.then(() => {
      return fn(item).then(result => {
        results.push(result);
      });
    });
  }
  return promise.then(() => {
    return results;
  });
}

或者更简洁的版本:

function serialAsyncMap(collection, fn) {
  
  let results = [];
  let promise = Promise.resolve();
  
  for (let item of collection) {
    promise = promise.then(() => fn(item))
                     .then(result => results.push(result));
  }
  
  return promise.then(() => results);
}

原生 Promise 的确比 Regenerator 编译后的 async/await 代码更加精简、易读及便于调试。调试与环境中 source-map 的支持力度密切相关(一般是必不可少的调试环境,如低版本的IE浏览器)。

有其它选择吗?

确实有几种来替代 Regenerator 编译 async/await 的方式,它提取 async 代码并尝试转换成更传统的 .then.catch 标记。以个人经验来讲,对于简单的函数这些转换运做较良好,在 awaitreturn,最多再加上 try/catch 块。但更复杂的 async 函数(加上一些条件 await 语句或循环)编译后的代码就像是一坨意大利面。

至少对我来讲,这还不够好;若是我不能简单的看着编译后的代码想象出编译的结果看起来会是什么样,那我可以轻松调试代码的机会就也变得很小。

浏览器彻底支持 async/await 是须要很长时间的,因此请不要屏息等待并在客户端编写你熟悉的 Promise 代码。

好吧好吧,因此在客户端我仍然须要写 Promise,但只要我运行在 node 服务端就可使用 async/await 了,对吗?

没错,但也不必定。

一般你能够在 JS 服务器端用一些 async 函数和 await 语法,好比作一些 http 请求,都没什么问题。你甚至可使用 Promise.all 来并行异步任务(尽管我认为这样有点不适当,用 async/await 运行并行更好)。

但当你想写一些比 “串联运行一些异步任务” 或 “并行运行一些异步任务” 更复杂的事情时会发生什么呢?

举一个例子

咱们想作一个披萨,考虑如下几点。

  • 咱们单独作生面团。

  • 咱们单独作调味酱。

  • 咱们想先品尝下调味酱再决定用哪一种奶酪搭配比萨饼。

因此,让咱们从一个超简单的纯 async/await 解决方案开始:

async function makePizza(sauceType = 'red') {
  
  let dough  = await makeDough();
  let sauce  = await makeSauce(sauceType);
  let cheese = await grateCheese(sauce.determineCheese());
  
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}

这有一个很大的优点:它十分简单、很容易阅读和理解。首先咱们作生面团,而后咱们作调味酱,而后咱们磨奶酪。简单!

可是这并不彻底是最佳的。咱们得一步一步地作事情,实际咱们应该让 JS 引擎同时运行这些任务。所以而不是:

|-------- dough --------> |-------- sauce --------> |-- cheese -->

咱们想要的东西更像:

|-------- dough -------->
|-------- sauce --------> |-- cheese -->

用这种方式,这个任务完成得更快了。让咱们再试一下:

async function makePizza(sauceType = 'red') {
  
  let [ dough, sauce ] =
    await Promise.all([ makeDough(), makeSauce(sauceType) ]);
  let cheese = await grateCheese(sauce.determineCheese());
  
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}

好的,使用 Promise.all 后咱们的代码看起来更酷些,至少如今是最佳的,对吧?嗯...并非。我甚至在等待生面团和调味酱作好后才能开始磨奶酪。若是我很快作好调味酱怎么办?如今个人执行看起来像这样:

|-------- dough -------->
|--- sauce ---> |-- cheese -->

注意,在我想要磨奶酪前,还须要等待生面团和调味酱何时作好?在磨奶酪前我只需把调味酱作好,因此我在这里浪费时间。而后让咱们回到绘图板,尝试使用 Promise.all,而不是 async/await:

function makePizza(sauceType = 'red') {
  
  let doughPromise  = makeDough();
  let saucePromise  = makeSauce(sauceType);
  let cheesePromise = saucePromise.then(sauce => {
    return grateCheese(sauce.determineCheese());
  });
  
  return Promise.all([ doughPromise, saucePromise, cheesePromise ])
    .then(([ dough, sauce, cheese ]) => {
      
      dough.add(sauce);
      dough.add(cheese);
      
      return dough;
    });
}

这样操做起来好多了。一旦全部的依赖关系实现,如今每一个任务将会尽快地完成。因此惟一能够阻止我磨奶酪的事情就是等待调味酱。

|--------- dough --------->
|---- sauce ----> |-- cheese -->

可是为了这样作,咱们不得不所有退出编写 async/await 代码而且所有用 Promise。咱们尝试着回到 async/await 上。

async function makePizza(sauceType = 'red') {
  
  let doughPromise = makeDough();
  let saucePromise = makeSauce(sauceType);
  
  let sauce  = await saucePromise;
  let cheese = await grateCheese(sauce.determineCheese());
  let dough  = await doughPromise;
  
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}

好吧,因此如今咱们是最佳的,并回到 async/await 块... 但这仍然感受像一个倒退。咱们必须预先设置每一个 Promise,因此要作好心理准备。咱们同时还依赖这些 Promise 来运行,在任意 await 前将指定任务设置好。这在阅读代码时体现并非很明显,而且未来可能会被意外的分解或破坏。因此这多是我最不喜欢的实现。

让咱们再试一次。咱们能够再试一次:

async function makePizza(sauceType = 'red') {
  
  let prepareDough  = memoize(async () => makeDough());
  let prepareSauce  = memoize(async () => makeSauce(sauceType));
  let prepareCheese = memoize(async () => {
    return grateCheese((await prepareSauce()).determineCheese());
  });
  
  let [ dough, sauce, cheese ] = 
    await Promise.all([
      prepareDough(), prepareSauce(), prepareCheese()
    ]);
    
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}

这是我最喜欢的解决方案。不用预先设置 Promise,它们隐式地并行运行,咱们能够设置三个 memoized 任务(确保每次只运行一次),并在 Promise.all 中调用它们以并行运行。

这里除 Promise.all 外,咱们几乎没有涉及其余的 Promise,尽管它们在 async/await 的底层运转。这个模式我在另外一篇关于缓存和并行的文章中更详细地介绍过。但在我看来,这致使了最佳并行和可读性/可维护性的完美结合。

固然,我老是愿意被你们证实是错的,因此若是你有一个更喜欢的 makePizza 实现,请让我知道!

因此咱们很快地作了一个披萨,点在哪里呢?

点在于,若是你在计划写彻底并行的代码,即使是用最新 node.js 版本,知道怎么将 Promise 和 async/await 混合在一块儿仍然是一个很是必要的技能。不管你最喜欢的 makePizza 实现是怎样的,你仍然须要考虑如何将 Promise 连接组合在一块儿,使函数运行时尽量减小没必要要的延迟。

async/await 就到这里,若是你不了解 Promise 在你的代码中如何运行, 你将会卡在这里并找不到明显的方式来优化你的并行任务。

到了这里……

不要惧怕,让辅助函数从你的业务逻辑中抽象出来 Promise/并行逻辑。一旦你了解 Promise 的工做原理,这样可使你的代码摆脱意大利面同样的杂乱,而且使你的异步程序/业务逻辑函数更清晰的体现出想要作什么,而不是老是挤在样板里。

执行此功能,若是用户登陆,它将每十秒钟检查一次,并在 Promise 中检测到如下状况时进行 resolves:

function onUserLoggedIn(id) {
  
  return ajax(`user/${id}`).then(user => {
    
    if (user.state === 'logged_in') {
      return;
    }
    
    return new Promise(resolve => {
      return setTimeout(resolve, 10 * 1000));
    }).then(() => {
      return onUserLoggedIn(id);
    })
  });
}

这并非我想要执行的函数 - 业务逻辑和 promise/delay 逻辑很是紧密地耦合在一块儿。在我想对函数作些调整前不得不去阅读和理解这整段内容。
为了改进这一点,我能够将 async/promise 逻辑拆成一些独立的辅助函数,并使个人业务逻辑更简洁:

function delay(time) {
  return new Promise(resolve => {
    return setTimeout(resolve, time));
  });
}
function until(conditionFn, delayTime = 1000) {
  return Promise.resolve().then(() => {
    return conditionFn();
    
  }).then(result => {
    
    if (!result) {
      return delay(delayTime).then(() => {
        return until(conditionFn, delayTime);
      });
    }
  });
}

或者这些辅助函数的超简洁版本:

let delay = time =>
    new Promise(resolve =>
        setTimeout(resolve, time)
    );
let until = (cond, time) =>
    cond().then(result =>
        result || delay(time).then(() =>
            until(cond, delay)
        )
    );

而后 onUserLoggedIn 变的与流程控制逻辑不那么紧密地耦合在一块儿。

function onUserLoggedIn(id) {
  return until(() => {
    return ajax(`user/${id}`).then(user => {
      return user.state === 'logged_in';
    });
  }, 10 * 1000);
}

如今我更但愿可以在未来轻松的阅读和理解 onUserLoggedIn。只要我记得接口的 until 函数,就不用每次从新梳理它的逻辑。我能够把它扔到一个 promise-utils 文件中并忽略它是如何执行的,最重要的是能够把注意力集中在本身的应用逻辑。

是的,咱们讨论的是 async/await,对吧?嗯,今天是咱们的幸运日,因为 async/await 和 Promise 是彻底能共同使用的,咱们刚刚无心中创造了一个能够继续使用的辅助函数,甚至具备 async 功能:

async function onUserLoggedIn(id) {
  return await until(async () => {
    let user = await ajax(`user/${id}`);
    return user.state === 'logged_in';
  }, 10 * 1000);
}

因此不管代码是基于 Promise 仍是基于 async/await,规则都是同样的。若是你发现并行逻辑陷入你的 async 业务函数中,必定要考虑是否能够抽出一点。固然要在合理范围内。

这里有一个至关大的抽象集合,可能有点帮助。

因此,若是你想从这篇文章中有所收货,就是这些:若是你正在编写 async/await 代码,你不只应该理解 Promise 是如何工做的,并且在必要时你还应该使用它们来构建你的 async/await 代码。单独的 async/await 不会给你足够的功能来彻底避免 Promise 思惟。

Thanks!

— Daniel

相关文章
相关标签/搜索