异步与回调

210 阅读3分钟

本篇文章,我们来简单了解一下以下几点:

  1. 同步与异步的区别
  2. 什么是回调?
  3. 什么是回调地狱?
  4. 解决回调地狱的方法

同步与异步的区别

同步(Synchronous)和异步(Asynchronous)是编程中常用的两个概念,用来描述程序中的操作是如何执行的。

同步代码和异步代码的区别在于它们的执行顺序和是否会阻塞程序的执行。

同步

在同步代码中,程序会按照代码的顺序一步一步执行,每个操作都必须等待上一个操作完成后才能执行。因此,如果某个操作比较耗时,程序就会被阻塞,直到该操作完成为止。在这种情况下,程序不能执行后面的代码,直到当前操作完成。

  1. 简单的同步代码
const name = 'liliRX';
const greeting = `Hello, my name is ${name}!`;
console.log(greeting);
// "Hello, my name is liliRX!"

在上面代码中,浏览器是按照代码的顺序逐行地执行程序,在上一行完成后才会执行下一行。

  1. 耗时久的同步代码
function fibonacci(n) {
  if (n <= 1) {
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

// 计算斐波那契数列第 40 项,需要运行约 30 秒左右
var result = fibonacci(40);
console.log(result);

在上述代码中,fibonacci函数是一个计算斐波那契数列的同步函数,它的运行时间会随着参数 n 的增大而呈指数级增长。当参数为 40 时,函数需要运行约 30 秒左右,期间程序会一直等待计算完成,无法执行其他操作。

在这种情况下,如果我们的程序需要执行一些其他的任务,比如响应用户的操作或者处理其他的请求,就会出现用户界面卡顿、响应缓慢等问题,影响用户体验。

异步

在异步代码中,程序会继续执行后面的代码,不必等待当前操作完成。异步操作会在后台执行,并在操作完成后通过回调函数通知程序,告诉它操作已经完成。因此,异步代码不会阻塞程序的执行,可以提高程序的效率和响应速度。

常见的异步代码

  1. 定时器

在 JavaScript 中,可以使用 setTimeoutsetInterval 函数来创建定时器,用于在一定时间后执行代码。这种定时器的使用方式是异步的

// 3 秒后执行回调函数
setTimeout(function() {
  console.log('3 seconds later');
}, 3000);

// 每隔 1 秒执行一次回调函数
setInterval(function() {
  console.log('1 second later');
}, 1000);
  1. 事件处理程序

通过事件处理程序来响应用户的操作,比如点击按钮、滚动页面等等。

// 给按钮添加点击事件处理程序
var button = document.querySelector('button');
button.addEventListener('click', function() {
  console.log('Button clicked');
});

// 监听滚动事件
window.addEventListener('scroll', function() {
  console.log('Window scrolled');
});
  1. AJAX请求

在前端开发中,经常需要通过 AJAX 发送异步请求,获取服务器返回的数据。可以使用 XMLHttpRequestfetch 等 API 来发送异步请求。

// 使用 XMLHttpRequest 发送异步请求
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

// 使用 fetch 发送异步请求
fetch('/api/data')
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    console.log(data);
  });

总结

简而言之,同步代码是按照顺序执行的,会阻塞程序的执行,而异步代码则是在后台执行的,不会阻塞程序的执行,并且在操作完成后通过回调函数来通知程序操作已经完成。在处理一些耗时操作时(尤其在处理网络请求和IO操作时),使用异步代码可以提高程序的效率和响应速度。

什么是回调?

回调函数是一种特殊的函数,它作为参数传递给另一个函数,并在该函数完成后被调用。

回调函数通常用于异步编程中,用于处理操作完成后的结果。

以下简单分析一下它在异步中的使用:

  1. 定时器
// 设置一个定时器,3秒后执行回调函数
setTimeout(function() {
  console.log('3 seconds passed.');
}, 3000);

在这个例子中,setTimeout 函数接收两个参数,第一个参数是回调函数,第二个参数是等待的时间(以毫秒为单位)。当时间到达后,回调函数将被调用。

2.事件处理程序

// 给按钮添加点击事件处理程序
var button = document.querySelector('button');
button.addEventListener('click', function() {
  console.log('Button clicked.');
});

在这个例子中,addEventListener 函数接收两个参数,第一个参数是事件类型,第二个参数是回调函数。当按钮被点击时,回调函数将被调用。

  1. AJAX
//发送 AJAX 请求
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

在这个例子中,onreadystatechange 属性是一个回调函数,它会在 AJAX 请求的状态发生变化时被调用。当请求完成并返回数据时,回调函数将被调用,处理服务器返回的数据。

总结

总之,回调函数是一种常见的异步编程技术,用于处理操作完成后的结果。回调函数可以作为参数传递给其他函数,在适当的时候被调用。回调函数可以用于处理定时器、事件处理程序、AJAX 请求等各种场景。

什么是回调地狱?

回调地狱:在异步编程中,由于多个异步操作之间存在依赖关系,我们需要通过回调函数来实现异步操作的串联。如果异步操作的数量比较多,而且它们之间存在复杂的依赖关系,就容易导致回调函数嵌套的层数过多,代码难以理解和维护,从而形成所谓的“回调地狱”。(通俗地讲,就是回调函数用得多了)

一个简单的案例来演示回调地狱:

getUser(function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetail(orders[0].id, function(orderDetail) {
      getPayment(orderDetail.paymentId, function(payment) {
        showOrderDetail(user, orders[0], orderDetail, payment);
      });
    });
  });
});

在上述代码中,我们需要依次获取用户信息、用户的订单列表、订单的详情信息以及订单的支付信息,并将这些信息传递给 showOrderDetail 函数进行展示。由于这些操作都是异步的,我们需要通过回调函数来实现它们之间的串联。但是,由于存在多个异步操作的依赖关系,导致回调函数嵌套的层数非常深,代码难以维护。

解决回调地狱的方法

回调地狱是指由于异步编程中的回调函数嵌套过多,导致代码难以维护、难以阅读的情况。为了解决这个问题,可以采用以下几种方法:

  1. 使用 Promise:Promise 是异步编程中的一种解决方案,可以避免回调函数的嵌套,使代码更加简洁易读。
  2. 使用 async/await:async/await 是 ES7 中的新特性,它是基于 Promise 的语法糖,使得异步编程更加简单和易于理解。
  3. 使用事件驱动的方式:事件驱动是一种非阻塞的异步编程方式,可以在异步任务完成时触发一个事件,从而避免回调函数的嵌套。
  4. 使用函数式编程:函数式编程的特点是将代码尽量模块化、函数化,通过组合不同的函数实现复杂的逻辑,避免回调函数的嵌套。

以上几种方法都可以有效地解决回调地狱的问题,使得异步编程更加简单和易于维护。其中,Promise 和 async/await 是最常用的两种方法,它们都是基于 Promise 的语法糖,可以让代码更加简洁易读。

Promise

Promise 是 JavaScript 异步编程的一种解决方案,它用于处理异步操作,可以将异步操作的结果以同步的方式返回,从而简化了异步编程的复杂度。Promise 的状态分为三种:pending(进行中)、fulfilled(已成功)和rejected(已失败),一旦状态发生变化,就会触发相应的回调函数。

Promise简单案例

function getData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      var data = Math.random();
      if (data > 0.5) {
        resolve(data);
      } else {
        reject('error');
      }
    }, 1000);
  });
}

getData()
  .then(function(result) {
    console.log(result);
  })
  .catch(function(error) {
    console.log(error);
  });

在上述代码中,我们定义了一个 getData 函数,它返回一个 Promise 对象。在 Promise 对象的构造函数中,我们定义了一个异步操作,使用 setTimeout 模拟了一个异步操作的过程。如果异步操作成功,则调用 resolve 函数返回结果;如果异步操作失败,则调用 reject 函数返回错误信息。

在调用 getData 函数时,我们可以使用 then 方法注册成功的回调函数,使用 catch 方法注册失败的回调函数。当异步操作完成后,如果成功,则会触发 then 方法注册的回调函数;如果失败,则会触发 catch 方法注册的回调函数。

Promise 可以很好地解决回调地狱的问题,使得异步编程更加简单和易于维护。

async/await

async/await 是 ES7 中的新特性,它是基于 Promise 的语法糖,使得异步编程更加简单和易于理解。async/await 的核心是使用 async 关键字声明一个异步函数,函数中可以使用 await 关键字等待 Promise 对象的执行结果。当 Promise 对象返回结果后,await 会将结果转换为同步的方式返回。

以下是一个 async/await 的例子:

function getData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      var data = Math.random();
      if (data > 0.5) {
        resolve(data);
      } else {
        reject('error');
      }
    }, 1000);
  });
}

async function fetchData() {
  try {
    var result = await getData();
    console.log(result);
  } catch (error) {
    console.log(error);
  }
}

fetchData();

在上述代码中,我们使用 async 关键字声明了一个异步函数 fetchData,其中使用 await 关键字等待 getData 函数的执行结果。当 getData 函数执行完成后,await 会将结果以同步的方式返回,使得代码更加简洁易读。

在异步操作中,使用 try...catch 可以捕获可能的异常,并进行相应的处理。在上述代码中,如果异步操作成功,则会输出结果;如果失败,则会输出错误信息。

async/await 可以很好地解决回调地狱的问题,使得异步编程更加简单和易于维护。它是 Promise 的语法糖,可以让代码更加简洁和易读。