likes
comments
collection
share

JavaScript 中的流量控制:回调、承诺、异步/等待

作者站长头像
站长
· 阅读数 45

在本文中,我们将从高层次上了解如何在 JavaScript 中使用异步代码。我们将从回调开始,继续承诺,然后以更现代的异步/等待结束。每个部分都将提供示例代码,概述需要注意的要点,并链接到更深入的资源。

原文:Flow Control in JavaScript: Callbacks, Promises, async/await (sitepoint.com)

内容:

  1. 单线程处理
  2. 使用回调异步
    • 回调地狱
  3. 承诺
    • 异步链接
    • 充满希望的未来?
  4. 异步/等待
    • 承诺,承诺
    • 尝试/捕捉丑陋
  5. JavaScript 之旅

JavaScript经常被声称是_异步_的。那是什么意思?它如何影响发展?近年来,这种方法发生了怎样的变化?

请考虑以下代码:

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

大多数语言_同步_处理每一行。第一行运行并返回结果。第二条线在第一条线完成后运行——无论需要多长时间

单线程处理

JavaScript 在单个处理线程上运行。在浏览器选项卡中执行时,其他所有内容都会停止。这是必要的,因为对页面 DOM 的更改不能在并行线程上进行;让一个线程重定向到不同的 URL 而另一个线程尝试附加子节点是危险的。

这对用户来说很少很明显,因为处理以小块快速发生。例如,JavaScript 检测按钮单击、运行计算并更新 DOM。完成后,浏览器可以自由处理队列中的下一项。

使用回调异步

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

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

例如:

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


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 调用。

承诺

ES2015(ES6)引入了承诺。回调仍然在表面之下使用,但 promise 提供了一种更清晰的语法来_链接_异步命令,以便它们串联运行(下一节将详细介绍)。

若要启用基于承诺的执行,必须更改基于异步回调的函数,以便它们立即返回 promise 对象。该对象_承诺_在将来的某个时候运行两个函数之一(作为参数传递):

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

在下面的示例中,数据库 API 提供了一个接受回调函数的方法。外部函数立即返回一个新的承诺,并在建立连接或失败后运行:connect``asyncDBconnect``resolve``reject

const db = require('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 的替代方案。有几个条件:

  • 回调必须作为最后一个参数传递给异步函数
  • 回调函数必须预期错误,后跟值参数

例:


const
  util = require('util'),
  fs = require('fs'),
  readFileAsync = util.promisify(fs.readFile);

readFileAsync('file.txt');

异步链接

任何返回 promise 的东西都可以启动一系列在方法中定义的异步函数调用。每个都传递来自前一个的结果:.then()``resolve

asyncDBconnect('http://localhost:1234')
  .then(asyncGetSession)      
  .then(asyncGetUser)         
  .then(asyncLogAccess)       
  .then(result => {           
    console.log('complete');  
    return result;            
  })
  .catch(err => {             
    console.log('error', err);
  });

同步函数也可以在块中执行。返回的值将传递给下一个(如果有)。.then()``.then()

该方法定义一个函数,该函数在触发任何先前的函数时调用。此时,将不会运行其他方法。在整个链中可以使用多种方法来捕获不同的错误。.catch()``reject``.then()``.catch()

ES2018引入了一种方法,该方法无论结果如何,都可以运行任何最终逻辑,例如,清理,关闭数据库连接等。所有现代浏览器都支持它:.finally()

function doSomething() {
  doSomething1()
  .then(doSomething2)
  .then(doSomething3)
  .catch(err => {
    console.log(err);
  })
  .finally(() => {
    
  });
}

充满希望的未来?

承诺减少了回调地狱,但引入了自己的问题。

教程通常没有提到_整个承诺链是异步的_。任何使用一系列 promise 的函数都应该返回自己的 promise 或在 final 或方法中运行回调函数。.then()``.catch()``.finally()

语法通常看起来比回调更复杂,有很多错误,调试可能会有问题。但是,学习基础知识至关重要。

异步/等待

承诺可能令人生畏,因此ES2017引入了.虽然它可能只是语法糖,但它使承诺更加甜蜜,你可以完全避免链条。请考虑以下基于承诺的示例:async``await``.then()

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


(() => {
  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;
  }
}


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

await有效地使每个调用看起来好像是同步的,同时不占用JavaScript的单个处理线程。此外,函数总是返回一个承诺,因此它们反过来可以被其他函数调用。async``async

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

  • 语法更简洁。括号更少,出错更少。
  • 调试更容易。可以在任何语句上设置断点。await
  • 错误处理更好。 块的使用方式与同步代码相同。try/catch
  • 支持很好。它在所有现代浏览器和 Node 7.6+ 中实现。

也就是说,并非一切都是完美的...

承诺,承诺

async/await依赖于承诺,而承诺最终依赖于回调。这意味着您仍然需要了解承诺是如何运作的。

此外,当使用多个异步操作时,没有直接等同于 Promise.all 或 Promise.race很容易忘记 ,这比使用一系列不相关的命令更有效。Promise.all``await

JavaScript 之旅

异步编程是 JavaScript 中无法避免的挑战。回调在大多数应用程序中是必不可少的,但它很容易纠缠于深度嵌套的函数中。

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

幸运的是,提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写 JavaScript 的方式,甚至可以让你欣赏承诺——如果你以前没有的话!async/await

转载自:https://juejin.cn/post/7239321198035107899
评论
请登录