为了账号安全,请及时绑定邮箱和手机立即绑定

Modern JS中的流控制:CallBacks->Promises->Async/Await

今天来聊一聊JS中的异步发展,还有推荐的异步调用写法.

单线程模式

JS运行在一个单处理线程上运行。当你操作一个标签时,其他的JS代码就会等待该操作执行完毕。浏览器的DOM操作不会发生在并行县城上。而当一个节点试图添加一个子节点时,将进程重定向到另一个不同的URL时非常不明智的。
不过这种情况很少发生,因为进程处理在一个小语法块中很快就执行完了。例如,JavaScript检测到按钮点击,运行计算并更新DOM。完成后,浏览器可以处理队列中的下一个操作。

PHP也是一门单线程语言,但是处理PHP的服务器时多线程的,比如说Apache,所以同时向同一个PHP页面发出两个请求可以创建两个运行PHP的线程。


异步和回调

单线程引发了一个问题。当JavaScript调用浏览器中的Ajax请求或服务器上的数据库操作时,会发生什么情况?该操作可能需要几秒钟 - 甚至几分钟。浏览器在等待响应时会被锁定。在服务器上,Node.js应用程序将无法处理其他用户请求。

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

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

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

doSomethingAsync()接受一个回调函数作为一个参数(仅指该函数传递的开销)。不管doSomethingAsync()会执行多久;我们只知道callback1()将在未来某个时候执行。控制台将显示:

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

不幸的是,这引入了回调地狱 - 一个臭名昭著的概念。代码会难以阅读,并且在添加处理错误逻辑时会变得更糟。
当然,在客户端编码中,回调地狱是相对罕见的。如果进行Ajax调用,更新DOM并等待动画完成,它可以深入两到三个层次,但它通常仍然可以管理。
但是,如果使用Node.js API调用在发送响应之前接收文件上传,更新多个数据库表,写入日志以及进一步调用API,这种回调就会变得很深。


Promises

ES2015(ES6)推出了Promises。回调仍在表面之下使用,但Promises提供了一种更清晰的语法,可以链接异步命令,以便它们以串行方式运行。
要启用基于Promise的执行,必须更改基于异步回调的函数,以便它们立即返回一个Promise对象。该对象承诺在未来某个时刻运行两个函数中的一个(作为参数传递):

  • 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的替代方案。有几个条件:

  • 该回调必须作为最后一个参数是异步函数

  • 回调函数的参数为 (err, result),前面是可能的错误,后面是正常的结果
// Node.js: promisify fs.readFile
const
  util = require('util'),
  fs = require('fs'),
  readFileAsync = util.promisify(fs.readFile);

readFileAsync('file.txt');

其实这个功能不是很难实现,如下:

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

Asynchronous Chaining

任何返回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()方法定义了一个函数,该函数在任何之前的reject被触发时被调用。此时,不会再运行.then()方法。可以在整个链中有多个.catch()方法来捕获不同的错误。
ES2018引入了一个.finally()方法,该方法可以运行任何最终逻辑,而不管结果如何 - 例如清理,关闭数据库连接等。目前仅支持Chrome和Firefox。

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

Multiple Asynchronous Calls with Promise.all()

Promise .then()方法一个接一个地运行异步函数。如果顺序无关紧要(例如,初始化不相关的组件),那么同时启动所有异步函数并在最后调用的(最慢)函数运行resolve结束后会更快。
这可以通过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()会立即终止。

Multiple Asynchronous Calls with Promise.race()

Promise.race()类似于Promise.all(),除了它在第一个Promise resolves或rejects后立即resolves或rejects。只有最快的基于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);
  });

Async/Await

Promises 可能令萌新瑟瑟发抖,所以ES2017引入了Async/Await。虽然它可能只是语法上的糖,但它使Promises更加甜美,并且可以完全避免.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的单个处理线程。另外,asyn函数总是返回一个Promise,这样它们可以被其他异步函数调用

异步/等待代码可能不会更短,但有相当多的好处:

  1. 语法更清晰。有更少的括号和更少的错误。
  2. 调试更容易。可以在任何await语句上设置断点。
  3. 错误处理更好。 try / catch块的使用方式与同步代码相同。

不知道有没有愚蠢的慕课网小伙伴把异步放在同步循环里面执行过,ES6提出了异步迭代器。
但是,在实现异步迭代器之前,最好将数组项映射到异步函数并使用Promise.all()运行它们。例如:

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

await Promise.all(alltodo);

这有利于并行运行任务,但不能将一次迭代的结果传递给另一次迭代,还有就是映射大型数组的计算成本很高。

点击查看更多内容
3人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
PHP开发工程师
手记
粉丝
1.6万
获赞与收藏
1807

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消