现代JS中的流程控制:详解Callbacks 、Promises 、Async/Await

JavaScript常常声称是_异步_。那是什么意思?它如何影响发展?近年来这种方法有何变化?html

请思考如下代码:node

result1 = doSomething1();
result2 = doSomething2(result1);

大多数语言都处理每一行同步。第一行运行并返回结果。第二行在第一行完成后运行不管须要多长时间。git

单线程处理

JavaScript在单个处理线程上运行。在浏览器选项卡中执行时,其余全部内容都会中止,由于在并行线程上不会发生对页面DOM的更改;将一个线程重定向到另外一个URL而另外一个线程尝试追加子节点是危险的。es6

这对用户来讲是显而易见。例如,JavaScript检测到按钮单击,运行计算并更新DOM。完成后,浏览器能够自由处理队列中的下一个项目。github

(旁注:其余语言如PHP也使用单个线程,但能够由多线程服务器(如Apache)管理。同时对同一个PHP运行时页面的两个请求能够启动两个运行隔离的实例的线程。)web

使用回调进行异步

单线程引起了一个问题。当JavaScript调用“慢”进程(例如浏览器中的Ajax请求或服务器上的数据库操做)时会发生什么?这个操做可能须要几秒钟 - 甚至几分钟。浏览器在等待响应时会被锁定。在服务器上,Node.js应用程序将没法进一步处理用户请求。数据库

解决方案是异步处理。而不是等待完成,一个进程被告知在结果准备好时调用另外一个函数。这称为callback,它做为参数传递给任何异步函数。例如:编程

doSomethingAsync(callback1);
console.log('finished');

// call when doSomethingAsync completes
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}

doSomethingAsync()接受一个回调函数做为参数(只传递对该函数的引用,所以几乎没有开销)。doSomethingAsync()须要多长时间并不重要;咱们所知道的是callback1()将在将来的某个时刻执行。控制台将显示:api

finished
doSomethingAsync complete

回调地狱

一般,回调只能由一个异步函数调用。所以可使用简洁的匿名内联函数:数组

doSomethingAsync(error => {
  if (!error) console.log('doSomethingAsync complete');
});

经过嵌套回调函数,能够串行完成一系列两个或多个异步调用。例如:

async1((err, res) => {
  if (!err) async2(res, (err, res) => {
    if (!err) async3(res, (err, res) => {
      console.log('async1, async2, async3 complete.');
    });
  });
});

不幸的是,这引入了回调地狱 - 一个臭名昭着的概念(http://callbackhell.com/) !代码难以阅读,而且在添加错误处理逻辑时会变得更糟。

回调地狱在客户端编码中相对较少。若是您正在进行Ajax调用,更新DOM并等待动画完成,它能够深刻两到三个级别,但它一般仍然能够管理。

操做系统或服务器进程的状况不一样。Node.js API调用能够接收文件上载,更新多个数据库表,写入日志,并在发送响应以前进行进一步的API调用。

Promises

ES2015(ES6)推出了Promises。回调仍然可使用,但Promises提供了更清晰的语法chains异步命令,所以它们能够串行运行(更多相关内容)。

要启用基于Promise的执行,必须更改基于异步回调的函数,以便它们当即返回Promise对象。该promises对象在未来的某个时刻运行两个函数之一(做为参数传递):

  • resolve :处理成功完成时运行的回调函数
  • reject :发生故障时运行的可选回调函数。

在下面的示例中,数据库API提供了一个接受回调函数的connect()方法。外部asyncDBconnect()函数当即返回一个新的Promise,并在创建链接或失败后运行resolve()或reject():

const db = require('database');

// connect to database
function asyncDBconnect(param) {

  return new Promise((resolve, reject) => {

    db.connect(param, (err, connection) => {
      if (err) reject(err);
      else resolve(connection);
    });

  });

}

Node.js 8.0+提供了util.promisify()实用程序,将基于回调的函数转换为基于Promise的替代方法。有几个条件:

  1. 将回调做为最后一个参数传递给异步函数
  2. 回调函数必须指向一个错误,后跟一个值参数。

例子:

// Node.js: promisify fs.readFile
const
  util = require('util'),
  fs = require('fs'),
  readFileAsync = util.promisify(fs.readFile);

readFileAsync('file.txt');

各类客户端库也提供promisify选项,但您能够本身建立几个:

// promisify a callback function passed as the last parameter
// the callback function must accept (err, data) parameters
function promisify(fn) {
  return function() {
      return new Promise(
        (resolve, reject) => fn(
          ...Array.from(arguments),
        (err, data) => err ? reject(err) : resolve(data)
      )
    );
  }
}

// example
function wait(time, callback) {
  setTimeout(() => { callback(null, 'done'); }, time);
}

const asyncWait = promisify(wait);

ayscWait(1000);

异步链

任何返回Promise的东西均可以启动.then()方法中定义的一系列异步函数调用。每一个都传递了上一个解决方案的结果:

asyncDBconnect('http://localhost:1234')
  .then(asyncGetSession)      // passed result of asyncDBconnect
  .then(asyncGetUser)         // passed result of asyncGetSession
  .then(asyncLogAccess)       // passed result of asyncGetUser
  .then(result => {           // non-asynchronous function
    console.log('complete');  //   (passed result of asyncLogAccess)
    return result;            //   (result passed to next .then())
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });

同步函数也能够在.then()块中执行。返回的值将传递给下一个.then()(若是有)。

.catch()方法定义了在触发任何先前拒绝时调用的函数。此时,不会再运行.then()方法。您能够在整个链中使用多个.catch()方法来捕获不一样的错误。

ES2018引入了一个.finally()方法,不管结果如何都运行任何最终逻辑 - 例如,清理,关闭数据库链接等。目前仅支持Chrome和Firefox,但技术委员会39已发布了 .finally() polyfill.

function doSomething() {
  doSomething1()
  .then(doSomething2)
  .then(doSomething3)
  .catch(err => {
    console.log(err);
  })
  .finally(() => {
    // tidy-up here!
  });
}

使用Promise.all()进行多个异步调用

Promise .then()方法一个接一个地运行异步函数。若是顺序可有可无 - 例如,初始化不相关的组件 - 同时启动全部异步函数并在最后(最慢)函数运行解析时结束更快。

这能够经过Promise.all()来实现。它接受一组函数并返回另外一个Promise。例如:

Promise.all([ async1, async2, async3 ])
  .then(values => {           // array of resolved values
    console.log(values);      // (in same order as function array)
    return values;
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });

若是任何一个异步函数调用失败,则Promise.all()当即终止。

使用Promise.race的多个异步调用()

Promise.race()与Promise.all()相似,只是它会在first Promise解析或拒绝后当即解析或拒绝。只有最快的基于Promise的异步函数才能完成:

Promise.race([ async1, async2, async3 ])
  .then(value => {            // single value
    console.log(value);
    return value;
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });

可是有什么别的问题吗?

Promises 减小了回调地狱但引入了别的问题。

教程常常没有提到_整个Promise链是异步的。使用一系列promise的任何函数都应返回本身的Promise或在最终的.then(),. catch()或.finally()方法中运行回调函数。

学习基础知识相当重要。

更多的关于Promises的资源:

Async/Await

Promises 可能使人生畏,所以ES2017引入了async and await。 虽然它可能只是语法糖,它使Promise更完善,你能够彻底避免.then()链。 考虑下面的基于Promise的示例:

function connect() {

  return new Promise((resolve, reject) => {

    asyncDBconnect('http://localhost:1234')
      .then(asyncGetSession)
      .then(asyncGetUser)
      .then(asyncLogAccess)
      .then(result => resolve(result))
      .catch(err => reject(err))

  });
}

// run connect (self-executing function)
(() => {
  connect();
    .then(result => console.log(result))
    .catch(err => console.log(err))
})();

用这个重写一下async/await:

  • 外部函数必须以async语句开头
  • 对异步的基于Promise的函数的调用必须在await以前,以确保在下一个命令执行以前完成处理。
async function connect() {

  try {
    const
      connection = await asyncDBconnect('http://localhost:1234'),
      session = await asyncGetSession(connection),
      user = await asyncGetUser(session),
      log = await asyncLogAccess(user);

    return log;
  }
  catch (e) {
    console.log('error', err);
    return null;
  }

}

// run connect (self-executing async function)
(async () => { await connect(); })();

await有效地使每一个调用看起来好像是同步的,而不是阻止JavaScript的单个处理线程。 此外,异步函数老是返回一个Promise,所以它们能够被其余异步函数调用。

async/await 代码可能不会更短,但有至关大的好处:

一、语法更清晰。括号更少,错误更少。

二、调试更容易。能够在任何await语句上设置断点。
三、错误处理更好。try / catch块能够与同步代码同样使用。

四、支持很好。它在全部浏览器(IE和Opera Mini除外)和Node 7.6+中都获得了支持。

可是并不是全部都是完美的......

切勿滥用async/await

async / await仍然依赖于Promises,它最终依赖于回调。你须要了解Promises是如何工做的,而且没有Promise.all()和Promise.race()的直接等价物。而且不要忘记Promise.all(),它比使用一系列不相关的await命令更有效。

同步循环中的异步等待

在某些时候,您将尝试调用异步函数中的同步循环。例如:

async function process(array) {
  for (let i of array) {
    await doSomething(i);
  }
}

它不会起做用。这也不会:

async function process(array) {
  array.forEach(async i => {
    await doSomething(i);
  });
}

循环自己保持同步,而且老是在它们的内部异步操做以前完成。

ES2018引入了异步迭代器,它与常规迭代器同样,但next()方法返回Promise。所以,await关键字能够与for循环一块儿用于串行运行异步操做。例如:

async function process(array) {
  for await (let i of array) {
    doSomething(i);
  }
}

可是,在实现异步迭代器以前,最好将数组项映射到异步函数并使用Promise.all()运行它们。例如:

const
  todo = ['a', 'b', 'c'],
  alltodo = todo.map(async (v, i) => {
    console.log('iteration', i);
    await processSomething(v);
});

await Promise.all(alltodo);

这具备并行运行任务的好处,可是不可能将一次迭代的结果传递给另外一次迭代,而且映射大型数组可能在性能消耗上是很昂贵。

try/catch 有哪些问题了?

若是省略任何await失败的try / catch,async函数将以静默方式退出。若是您有一组很长的异步await命令,则可能须要多个try / catch块。

一种替代方案是高阶函数,它捕获错误,所以try / catch块变得没必要要(thanks to @wesbos for the suggestion):

async function connect() {

  const
    connection = await asyncDBconnect('http://localhost:1234'),
    session = await asyncGetSession(connection),
    user = await asyncGetUser(session),
    log = await asyncLogAccess(user);

  return true;
}

// higher-order function to catch errors
function catchErrors(fn) {
  return function (...args) {
    return fn(...args).catch(err => {
      console.log('ERROR', err);
    });
  }
}

(async () => {
  await catchErrors(connect)();
})();

可是,在应用程序必须以与其余错误不一样的方式对某些错误作出反应的状况下,此选项可能不实用。

尽管有一些陷阱,async / await是JavaScript的一个优雅补充。更多资源:

JavaScript 旅程

异步编程是一项在JavaScript中没法避免的挑战。回调在大多数应用程序中都是必不可少的,但它很容易陷入深层嵌套的函数中。

Promises 抽象回调,但有许多语法陷阱。 转换现有函数多是一件苦差事,而.then()链仍然看起来很混乱。

幸运的是,async / await提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写JavaScript的方式!

(译者注:Craig Buckler讲解JavaScript的文章都还不错,基本是用一些比较通俗的语言和代码事例讲解了JavaScript的一些特性和一些语法可能出现的问题。感兴趣的朋友能够看一下(https://www.sitepoint.com/aut...))

相关文章
相关标签/搜索