Javascript高级-回调与异步

386 阅读17分钟

✊不积跬步,无以至千里;不积小流,无以成江海。

生动例子解释async/await、promise和回调之间的关系

故事背景:

假设你是一位勇敢的冒险家小明,你的目标是探索神秘的宝藏岛并带回珍贵的宝藏。

  1. 回调时代:

你来到港口,准备乘船前往宝藏岛。但是船不会马上出发,你需要等待船准备好。于是你跟船夫说:“当船准备好了,叫我一声(这就是一个回调函数)。

但是如果你的冒险过程中有很多这样的等待场景,并且一个等待依赖另一个等待,很快你的代码就会变得像迷宫一样复杂,这就是回调地狱。

  1. Promise 时代:

后来,有了一种新的方式叫 Promise。现在当你去港口,船夫会给你一个承诺(Promise),告诉你船一定会准备好,并且你可以用更清晰的方式处理这个过程。

这样你的冒险计划看起来更有条理,不会那么混乱了。

  1. async/await 时代:

现在,又有了一种更厉害的魔法叫 async/await。你可以像写同步代码一样写异步代码。现在你的冒险变得更加顺利和容易理解。你不再需要为复杂的异步流程而头疼,就像在进行一场有序的冒险之旅。

为什么有了promise之后还要有async/await

一、代码可读性

  1. Promise 虽然改善了回调地狱的问题,但代码仍然是基于链式调用的方式。比如:
fetchData().then(data => 
processData(data)).then(processedData => 
saveData(processedData)).catch(error => 
handleError(error));

而使用 async/await 可以让代码看起来更像同步代码,更符合人们的思维习惯,极大地提高了代码的可读性。例如:

async function doStuff() {
  try {
    const data = await fetchData();
    const processedData = processData(data);
    await saveData(processedData);
  } catch (error) {
    handleError(error);
  }
}

二、错误处理更直观

  1. 在 Promise 中,错误处理需要通过.catch()方法来进行,多个 Promise 链式调用时,错误处理可能会变得复杂。而在 async/await 中,可以使用传统的try/catch块来处理错误,更加直观和容易理解。

三、代码组织和维护性

  1. 对于复杂的异步操作序列,使用 async/await 可以将代码组织成更清晰的函数结构,使得代码更易于维护。例如,在一个大型项目中,如果有多个异步操作需要按顺序执行,使用 async/await 可以让开发者更容易理解和修改代码逻辑。

综上所述,虽然 Promise 已经是一种很好的异步编程解决方案,但 async/await 进一步提高了代码的可读性、错误处理的直观性以及代码的组织和维护性。

异步代码语法糖的发展过程

在 JavaScript 中,“async/await”确实可以被视为一种让异步代码更易于理解和编写的“语法糖”。以下是关于它们的发展历史和来龙去脉:

一、异步编程的早期挑战

在 JavaScript 发展的早期,处理异步操作主要依赖回调函数。比如进行网络请求、读取文件等操作都是异步的,通常会使用回调函数来处理结果。

例如使用 XMLHttpRequest 进行网络请求:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/data');
xhr.onload = function() {
  if (xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

这种方式在处理简单的异步场景时还可以,但当异步操作嵌套较多时,就会导致“回调地狱”(callback hell),代码变得难以阅读和维护。

二、Promise 的出现

为了解决回调地狱的问题,ES6(ECMAScript 2015)引入了 Promise。Promise 代表一个异步操作的最终完成或失败,并提供了一种更优雅的方式来处理异步操作。

例如:

fetch('https://example.com/data')
 .then(response => response.json())
 .then(data => console.log(data))
 .catch(error => console.error(error));

Promise 使得异步代码可以以链式调用的方式组织起来,提高了代码的可读性。

三、async/await 的诞生

虽然 Promise 改善了异步编程的状况,但代码仍然不够直观。于是在 ES2017(ECMAScript 2017)中引入了“async/await”。

“async”函数返回一个 Promise 对象,可以在函数内部使用“await”来等待一个 Promise 被解决。这使得异步代码看起来更像同步代码,极大地提高了异步编程的可读性和可维护性。

例如:

async function fetchData() {
  try {
    const response = await fetch('https://example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

fetchData().then(data => console.log(data));

同步地摇色子、异步地摇色子

同步的摇骰子

在 JavaScript 中模拟同步地摇色子,可以使用 setTimeout 函数来模拟掷色子的延迟效果。以下是一个简单的示例代码,展示如何同步地摇色子并输出结果:

function rollDice() {
  return Math.floor(Math.random() * 6) + 1;
}

function shakeDice() {
  // 模拟掷色子的延迟效果
  function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async function shake() {
    console.log('Shaking dice...');
    await delay(1000); // 延迟1秒

    const dice1 = rollDice();
    const dice2 = rollDice();

    console.log('Dice 1:', dice1);
    console.log('Dice 2:', dice2);
  }

  shake();
}

shakeDice();

在上述代码中,我们定义了 rollDice 函数来模拟摇色子并返回一个随机的骰子点数。

然后,我们定义了 shakeDice 函数来模拟同步地摇色子。在 shakeDice 函数内部,我们使用了 setTimeout 和 Promise 的结合来模拟摇色子的延迟效果。

我们定义了 delay 函数,它返回一个 Promise,并使用 setTimeout 在指定的毫秒数后解析该 Promise。这样,我们可以通过 await delay(1000) 来实现延迟1秒的效果。

接下来,我们使用 async 和 await 关键字定义了内部的 shake 异步函数。在 shake 函数中,我们首先打印出 "Shaking dice...",然后通过 await delay(1000) 延迟1秒。

随后,我们调用 rollDice 函数两次来模拟掷两个骰子,并将结果保存在 dice1 和 dice2 变量中。

最后,我们打印出骰子的结果。

通过调用 shakeDice 函数,我们可以执行同步地摇色子的操作。当调用 shakeDice 函数时,它会按顺序执行内部的异步函数,并模拟出摇色子的延迟效果。最终,我们可以在控制台上看到摇色子的结果。

请注意,由于使用了 async 和 await,这段代码需要在支持 ES6 或更高版本的 JavaScript 环境中运行。

new Promise(resolve => setTimeout(resolve, ms))

这句代码是创建一个 Promise 对象,并使用 setTimeout 函数来延迟 Promise 的解析。

逐步解释这句代码的含义:

  1. new Promise() 创建一个新的 Promise 对象。Promise 是 JavaScript 中处理异步操作的一种机制,它表示一个可能会在未来完成失败的操作。
  2. resolve 是一个函数,它是 Promise 构造函数的参数。当 Promise 成功解析时,我们可以调用 resolve 函数来触发 Promise 的成功状态。
  3. setTimeout() 是一个 JavaScript 函数,它用于在指定的延迟时间后执行一个函数或表达式。
  4. setTimeout(resolve, ms) 将 resolve 函数作为参数传递给 setTimeout 函数。这意味着在经过指定的延迟时间(ms 毫秒)后,resolve 函数将被调用。

因此,整个表达式 new Promise(resolve => setTimeout(resolve, ms)) 的作用是创建一个 Promise 对象,并在经过指定的延迟时间后解析(resolve)该 Promise。这样,我们可以使用 await 关键字来等待这个 Promise 的解析,从而实现延迟的效果。

例如,如果我们使用 await delay(1000),它将等待 1000 毫秒(即 1 秒),然后继续执行后续的代码。这样就可以模拟出延迟的效果,让后续的操作在一定的时间间隔后执行。

async 和 await 语法

asyncasync 关键字用于声明一个函数是异步函数(async function)。异步函数内部可以包含 await 关键字。

语法:

async function functionName() {
  // 异步操作
}

异步函数在执行时,会返回一个 Promise 对象。这个 Promise 对象将在异步操作完成后进行解析(resolve),并且可以使用 await 关键字来等待 Promise 的解析结果。

awaitawait 关键字只能在异步函数内部使用,用于等待一个 Promise 对象的解析结果。await 关键字会暂停异步函数的执行,直到等待的 Promise 对象解析完成,并返回解析结果。

语法:

const result = await promise;

await 关键字后面跟着一个 Promise 对象,它可以是任何返回 Promise 的异步操作,例如异步函数调用、fetch 请求、定时器等。当 await 关键字执行时,它会等待 Promise 对象的解析结果,并将解析结果赋值给 result 变量。

在使用 await 关键字时,需要将其放置在 async 函数内部,并且 async 函数本身需要被调用或使用其他方式触发执行。

异步的摇骰子

在 JavaScript 中异步地摇色子,可以使用 Promise 和 setTimeout 来模拟异步操作的延迟效果。以下是一个可运行的代码示例:

function rollDice() {
  return new Promise(resolve => {
    setTimeout(() => {
      const dice = Math.floor(Math.random() * 6) + 1;
      resolve(dice);
    }, 1000); // 延迟1秒模拟摇色子的过程
  });
}

async function shakeDice() {
  console.log('Shaking dice...');

  const dice1Promise = rollDice();
  const dice2Promise = rollDice();

  const [dice1, dice2] = await Promise.all([dice1Promise, dice2Promise]);

  console.log('Dice 1:', dice1);
  console.log('Dice 2:', dice2);
}

shakeDice();

在上述代码中,我们定义了 rollDice 函数,它返回一个 Promise 对象。在 Promise 的回调函数中,我们使用 setTimeout 来模拟延迟1秒的摇骰子过程,并将骰子的点数作为解析结果传递给 resolve 函数。

然后,我们定义了一个 async 函数 shakeDice,用于异步地摇色子。在 shakeDice 函数中,我们首先打印出 "Shaking dice..."。

接下来,我们调用 rollDice 函数两次,分别返回两个 Promise 对象 dice1Promise 和 dice2Promise,代表两个骰子的摇色子操作。

使用 await Promise.all([dice1Promise, dice2Promise]),我们等待两个 Promise 对象同时解析,并将解析结果存储在 dice1 和 dice2 变量中。这里使用 Promise.all 方法可以并行地等待多个 Promise 对象的解析结果。

最后,我们打印出两个骰子的结果。

通过调用 shakeDice 函数,我们可以执行异步地摇色子的操作。在摇色子过程中,我们使用了异步操作的延迟效果,并通过 await 关键字等待两个骰子的解析结果。最终,我们可以在控制台上看到异步摇色子的结果。

请注意,由于使用了 async 和 await,这段代码需要在支持 ES6 或更高版本的 JavaScript 环境中运行。

setTimeout 的回调函数

setTimeout(() => {  
const dice = Math.floor(Math.random() * 6) + 1;  
resolve(dice);

这段代码是在 setTimeout 的回调函数中执行的操作。

逐步解释这段代码的含义:

  1. setTimeout() 是一个 JavaScript 函数,它用于在指定的延迟时间后执行一个函数或表达式。
  2. setTimeout(() => { ... }, delay) 接受两个参数:第一个参数是一个函数或表达式,表示要执行的操作;第二个参数是延迟的时间(以毫秒为单位)。
  3. () => { ... } 是一个箭头函数(也称为匿名函数),它是作为 setTimeout 的第一个参数传递的回调函数。箭头函数的作用是定义一个函数体,以便在延迟时间结束后执行。
  4. 在箭头函数的函数体中,我们生成一个随机数来模拟骰子的点数。Math.random() 用于生成一个介于 0 到 1 之间的随机数,然后通过乘以 6 和加 1 的操作,将其转换为一个介于 1 到 6 之间的随机整数(骰子的点数)。
  5. resolve(dice) 是调用 Promise 的 resolve 方法,并将生成的随机数(骰子的点数)作为参数传递给它。这样,Promise 的状态将被设置为已解析,并将生成的随机数作为解析结果。

因此,这段代码的作用是在延迟时间结束后,生成一个随机数(骰子的点数),并通过 resolve 方法将该随机数作为 Promise 的解析结果。这样,在调用 rollDice 函数并等待它的解析结果时,我们将得到一个模拟骰子点数的异步操作。

const [dice1, dice2] = await Promise.all([dice1Promise, dice2Promise]);

这段代码使用了 await 关键字和 Promise.all 方法来等待多个 Promise 对象同时解析,并将它们的解析结果分配给相应的变量。

逐步解释这段代码的含义:

  1. Promise.all([dice1Promise, dice2Promise]) 是一个将多个 Promise 对象作为参数的方法。它返回一个新的 Promise 对象,该 Promise 对象将在传入的所有 Promise 对象都解析完成后进行解析。
  2. await 关键字用于等待一个 Promise 对象的解析结果。在这段代码中,我们使用 await 等待 Promise.all([dice1Promise, dice2Promise]) 的解析结果。
  3. const [dice1, dice2] 是一种解构赋值语法,用于从一个数组中提取元素并分配给相应的变量。在这段代码中,我们将 Promise.all 返回的解析结果数组的第一个元素赋值给 dice1 变量,将第二个元素赋值给 dice2 变量。

因此,这段代码的作用是等待 dice1Promise 和 dice2Promise 两个 Promise 对象同时解析,并将它们的解析结果分配给 dice1 和 dice2 变量。通过这种方式,我们可以同时获取两个骰子的点数,并在后续的代码中使用这些值进行处理。

同步和异步的区别

使用同步和异步方法摇骰子有以下区别:

  1. 执行顺序:同步方法是按照代码的顺序依次执行,代码会一直等待当前操作完成后执行下一个操作。异步方法则是不会阻塞代码的执行,它会在进行耗时的操作时,继续执行后续的代码,待操作完成后再执行相应的回调函数或者通过 await 等待结果。
  2. 阻塞:同步方法会阻塞代码的执行,即代码会一直等待当前操作完成后才能继续执行后续代码。异步方法则不会阻塞代码的执行,它会在进行异步操作时,立即返回并允许后续代码继续执行。
  3. 响应性:异步方法能够提升应用的响应性能,因为它不会阻塞主线程,允许同时处理其他任务或事件。这在处理大量或耗时的操作时特别有用,可以避免应用程序的冻结或卡顿。
  4. 代码结构:异步方法通常使用回调函数、Promise 或 async/await 来处理异步操作,这使得代码更具可读性和可维护性。相比之下,同步方法可能需要使用回调地狱(callback hell)或者复杂的控制流程来处理异步操作,使代码变得难以理解和维护。

总的来说,异步方法允许在进行耗时的操作时,不阻塞代码的执行,提高了应用的响应性能,并通过更简洁的代码结构提供了更好的可读性和可维护性。而同步方法则会阻塞代码的执行,可能导致应用程序的冻结或卡顿,以及复杂的代码结构。

什么是异步、轮询与回调

异步、轮询和回调是与处理异步操作相关的概念。

  1. 异步(Asynchronous):异步是指在进行某个操作时,不会阻塞程序的执行,而是允许程序继续执行其他任务。异步操作通常是耗时的操作,比如网络请求、文件读写等,为了避免阻塞主线程,将这些操作放在后台执行,并在操作完成后通知程序。
  2. 轮询(Polling):轮询是一种常见的处理异步操作的方式。它通过定期(或间隔性)地查询某个资源的状态或结果,来判断操作是否完成。在轮询中,程序会重复地发起查询请求,直到获取到所需的结果或达到指定的条件。
  3. 回调(Callback):回调是一种处理异步操作的模式。在回调模式中,我们将一个函数(回调函数)作为参数传递给另一个函数,当异步操作完成时,调用该回调函数来处理结果。回调函数可以用于处理异步操作的结果、错误处理或执行其他逻辑。

例如,当进行网络请求时,可以使用异步操作来避免阻塞主线程。轮询可以用于定期查询请求的状态,直到请求完成或达到超时条件。回调函数则可以用于在请求完成后处理返回的数据或处理错误。

什么是回调地狱

回调地狱(Callback Hell)是指在回调函数嵌套过多、代码结构复杂的情况下,代码变得难以理解和维护的现象。

在使用回调函数处理异步操作时,如果多个异步操作之间存在依赖关系,通常需要在一个回调函数中嵌套另一个回调函数。当异步操作嵌套层级过多时,代码会出现多层嵌套的回调函数,导致代码结构复杂,可读性变差,称为回调地狱。

回调地狱的问题包括:

  1. 可读性差:多层嵌套的回调函数使得代码难以阅读和理解,增加了代码的复杂性。
  2. 可维护性差:由于嵌套的回调函数,修改和调试代码变得困难,容易出错,增加了代码的维护成本。
  3. 错误处理困难:对于多个异步操作的错误处理,需要在每个回调函数中进行处理,导致错误处理代码分散且容易遗漏。
  4. 可扩展性差:在回调地狱中添加新的异步操作变得复杂,需要进一步嵌套回调函数,使代码变得混乱。

为了解决回调地狱问题,出现了 Promise、Async/Await 等新的异步编程模型。这些模型提供了更优雅、可读性更好的方式处理异步操作,避免了多层嵌套的回调函数,使代码更加清晰、简洁和易于维护。

Node风格的回调

Node.js 风格的回调是一种在 Node.js 中广泛采用的异步编程模式,它基于回调函数来处理异步操作的结果或错误。

Node.js 风格的回调通常具有以下特征:

  1. 回调函数作为最后一个参数:异步函数通常将回调函数作为其参数列表中的最后一个参数。回调函数用于处理异步操作的结果或错误信息。
  2. 错误优先的回调函数:回调函数的第一个参数通常是错误对象(通常命名为 err 或 error),用于传递异步操作可能发生的错误。如果没有错误发生,则该参数为 null 或 undefined
  3. 回调函数的其他参数:在错误参数之后,回调函数可能会包含其他参数,用于传递异步操作的结果或其他相关信息。

下面是一个使用 Node.js 风格的回调的示例:

function fetchData(callback) {
  // 模拟异步操作
  setTimeout(() => {
    const data = 'Hello, World!';
    const error = null;

    // 调用回调函数,传递错误和数据
    callback(error, data);
  }, 2000);
}

// 调用异步函数并处理回调结果
fetchData((err, result) => {
  if (err) {
    console.error('Error:', err);
  } else {
    console.log('Data:', result);
  }
});

在上述示例中,fetchData 函数是一个模拟的异步操作,通过回调函数将结果返回。在回调函数中,根据是否有错误,进行相应的处理。

Node.js 风格的回调模式在 Node.js 生态系统中被广泛采用,但它也容易导致回调地狱(callback hell)的问题。为了解决这个问题,可以使用 Promise、Async/Await 等更现代的异步编程模型来提高代码的可读性和可维护性。