每个 JS 开发人员都应该知道的异步编程基础知识

2025-05-26

每个 JS 开发人员都应该知道的异步编程基础知识

本文针对的是刚开始使用 JavaScript 进行异步编码的人们,因此我们会避免使用大词、箭头函数、模板文字等,以使事情变得简单。

回调是现代函数式 JavaScript 中最常用的概念之一,如果您曾经使用过 jQuery,那么很可能您已经在不知情的情况下使用过回调(我们稍后会回到它)。

回调函数到底是什么

简单来说,回调函数就是一个作为参数传递给另一个函数的函数。回调函数会在被传递的函数内部执行,并将最终结果返回给调用者。

// I'm sure you've seen a JQuery code snippet like this at some point in your life!
// The parameter we're passing to the `click` method here is a callback function.

$("button").click(function() {
    alert('clicked on button`);
});
Enter fullscreen mode Exit fullscreen mode

简单吧?现在让我们实现一个回调函数,用于在一个虚拟游戏中获取升级分数。

// levelOne() is called a high-order function because // it accepts another function as its parameter. function levelOne(value, callback) { var newScore = value + 5; callback(newScore); } // Please note that it is not mandatory to reference the callback function (line #3) as callback, it is named so just for better understanding. function startGame() { var currentScore = 5; console.log('Game Started! Current score is ' + currentScore); // Here the second parameter we're passing to levelOne is the // callback function, i.e., a function that gets passed as a parameter. levelOne(currentScore, function (levelOneReturnedValue) { console.log('Level One reached! New score is ' + levelOneReturnedValue); }); } startGame();

一旦进入startGame()函数,我们就会levelOne()使用 currentScore 和回调函数 () 作为参数来调用该函数。

当我们以异步方式在函数范围levelOne()内调用时,JavaScript 会执行该函数,而主线程会继续执行代码的剩余部分。startGame()levelOne()

这意味着我们可以执行各种操作,例如从 API 获取数据、进行一些数学运算等等,所有这些操作都可能很耗时,因此我们不会因此阻塞主线程。一旦 function( levelOne()) 完成其操作,它就可以执行我们之前传递的回调函数。

这是函数式编程中一个非常有用的特性,因为回调函数让我们可以异步处理代码,而无需等待响应。例如,你可以使用回调函数向速度较慢的服务器发起 Ajax 调用,然后完全忘掉它,继续执行剩余的代码。一旦该 Ajax 调用得到解决,回调函数就会自动执行。

但是,如果回调函数需要连续执行多个层级,那么回调函数就会变得非常棘手。让我们以上面的例子为例,在游戏中添加更多层级。

function levelOne(value, callback) { var newScore = value + 5; callback(newScore); } function levelTwo(value, callback) { var newScore = value + 10; callback(newScore); } function levelThree(value, callback) { var newScore = value + 30; callback(newScore); } // Note that it is not needed to reference the callback function as callback when we call levelOne(), levelTwo() or levelThree(), it can be named anything. function startGame() { var currentScore = 5; console.log('Game Started! Current score is ' + currentScore); levelOne(currentScore, function (levelOneReturnedValue) { console.log('Level One reached! New score is ' + levelOneReturnedValue); levelTwo(levelOneReturnedValue, function (levelTwoReturnedValue) { console.log('Level Two reached! New score is ' + levelTwoReturnedValue); levelThree(levelTwoReturnedValue, function (levelThreeReturnedValue) { console.log('Level Three reached! New score is ' + levelThreeReturnedValue); }); }); }); } startGame();

等等,刚才发生了什么?我们添加了两个用于关卡逻辑的新函数,levelTwo()levelThree()。在 levelOne 的回调函数中(第 22 行),我们调用了 levelTwo() 函数,并传入了回调函数 和 levelOne 回调函数的结果。然后对 levelThree() 函数重复同样的操作。

回调模因

现在想象一下,如果我们必须为另外 10 个级别实现相同的逻辑,这段代码会变成什么样。你是不是已经慌了?没错!随着嵌套回调函数数量的增加,代码的可读性会越来越差,调试起来更是困难重重。

这通常被亲切地称为回调地狱。有办法摆脱这个回调地狱吗?

保证有更好的方法

JavaScript 从 ES6 开始支持 Promises。Promise 本质上是一个对象,表示异步操作的最终完成(或失败)及其结果值。

// This is how a sample promise declaration looks like. The promise constructor
// takes one argument which is a callback with two parameters, `resolve` and
// `reject`. Do something within the callback, then call resolve if everything
// worked, otherwise call reject.

var promise = new Promise(function(resolve, reject) {
  // do a thing or twenty
  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});
Enter fullscreen mode Exit fullscreen mode

现在让我们尝试用承诺重写我们的回调地狱示例。

function levelOne(value) { var promise, newScore = value + 5; return promise = new Promise(function(resolve) { resolve(newScore); }); } function levelTwo(value) { var promise, newScore = value + 10; return promise = new Promise(function(resolve) { resolve(newScore); }); } function levelThree(value) { var promise, newScore = value + 30; return promise = new Promise(function(resolve) { resolve(newScore); }); } var startGame = new Promise(function (resolve, reject) { var currentScore = 5; console.log('Game Started! Current score is ' + currentScore); resolve(currentScore); }); // The response from startGame is automatically passed on to the function inside the subsequent then startGame.then(levelOne) .then(function (result) { // the value of result is the returned promise from levelOne function console.log('You have reached Level One! New score is ' + result); return result; }) .then(levelTwo).then(function (result) { console.log('You have reached Level Two! New score is ' + result); return result; }) .then(levelThree).then(function (result) { console.log('You have reached Level Three! New score is ' + result); });

我们重写了我们的级别(One/Two/Three)函数,从函数参数中删除回调,而不是调用其中的回调函数,而是用承诺代替。

一旦 startGame 解析成功,我们就可以简单地调用.then()它的方法并处理结果。我们可以使用 串联多个 Promise .then() chaining

这使得整个代码更具可读性,并且更容易理解正在发生的事情以及then接下来发生的事情等等。

承诺通常更好的深层原因是它们更具可组合性,这大致意味着组合多个承诺“有效”,而组合多个回调通常无效。

另外,当我们只有一个回调和一个 Promise 时,确实没有显著的区别。当你有无数个回调而不是无数个 Promise 时,基于 Promise 的代码看起来会更加美观。

好的,我们成功摆脱了回调地狱,并通过 Promise 提高了代码的可读性。但如果我告诉你有一种方法可以让代码更简洁、更易读呢?

(a)等待

JavaScript 自 ECMA 2017 起支持 Async-await。它允许你像编写同步代码一样编写基于 Promise 的代码,但不会阻塞主线程。它让你的异步代码看起来不那么“聪明”,但可读性更高。

老实说,async-awaits 只不过是承诺之上的语法糖,但它使异步代码看起来和行为更像同步代码,这正是它的威力所在。

async如果在函数定义前使用关键字,则可以await在函数内部使用。当使用awaitPromise 时,函数会以非阻塞方式暂停,直到 Promise 完成。如果 Promise 完成,则会返回相应的值。如果 Promise 拒绝,则会抛出被拒绝的值。

现在让我们看看一旦我们用 async-awaits 重写游戏逻辑,它看起来会是什么样子!

function levelOne(value) { var promise, newScore = value + 5; return promise = new Promise(function(resolve) { resolve(newScore); }); } function levelTwo(value) { var promise, newScore = value + 10; return promise = new Promise(function(resolve) { resolve(newScore); }); } function levelThree(value) { var promise, newScore = value + 30; return promise = new Promise(function(resolve) { resolve(newScore); }); } // the async keyword tells the javascript engine that any function inside this function having the keyword await, should be treated as asynchronous code and should continue executing only once that function resolves or fails. async function startGame() { var currentScore = 5; console.log('Game Started! Current score is ' + currentScore); currentScore = await levelOne(currentScore); console.log('You have reached Level One! New score is ' + currentScore); currentScore = await levelTwo(currentScore); console.log('You have reached Level Two! New score is ' + currentScore); currentScore = await levelThree(currentScore); console.log('You have reached Level Three! New score is ' + currentScore); } startGame();

我们的代码立即变得更具可读性,但 Async-await 的作用还不止于此。

错误处理是 Async-await 最突出的功能之一。现在,我们终于可以用 try 和 catch 语句来处理同步和异步错误了,而这在使用 Promise 时非常麻烦,无需重复编写 try-catch 代码块。

与传统的 Promise 相比,下一个最佳改进是代码调试。当我们编写基于箭头函数的 Promise 时,我们无法在箭头函数内部设置断点,因此调试有时会很困难。但有了 async-awaits,调试就像编写同步代码一样简单。

我相信现在你对 JavaScript 中的异步编程已经有了更深入的理解。如果你有任何疑问,请在下方留言。如果你觉得这篇文章有帮助,请在 Twitter 上给我留言

编码愉快!✌️

文章来源:https://dev.to/siwalikm/async-programming-basics-every-js-developer-should-know-in-2018-a9c
PREV
利用 JavaScript 的超能力掌握全栈
NEXT
10 分钟内将 Google 登录添加到你的 React 应用