异步的 JavaScript:从Callback到 Promises 和 Async/Await

329 阅读8分钟

前言

在本文中,我想解决一些日常中的 JavaScript 语言新手经常提出的问题。async/await 是如何工作的,它与 Promises 有什么关系,我们为什么需要它。为了有效地做到这一点,我需要对真正的异步或非阻塞代码有一个实用的定义。此外,我还将简要回顾过去,以展示在进行多个相互依赖的异步调用以使用回调组合最终结果的程序中普遍存在的痛点,让我们开始吧。

异步/非阻塞 是什么意思?

在代码中,异步是指一种并发执行任务或操作的方式,无需等待一个任务完成再开始另一个任务。这种方法允许多个任务同时运行,从而有效利用系统资源并提高应用程序的整体响应能力。在处理可能需要可变或不可预测的时间量的操作时,异步编程特别有用,例如网络请求、文件 I/O 或涉及与外部资源交互的任何操作。它有助于防止阻塞程序的主要执行流程,使其在等待耗时操作完成的同时继续处理其他任务。(文字比较多,但我尽量按照大白话来解释)

再说白一点,这种场景如何在现实世界重现的?

想象一家咖啡店,顾客来这里订购各种类型的咖啡和小吃。在这家咖啡店里,只有一个咖啡师,负责接单、冲咖啡、准备点心。

在异步场景中,咖啡师接到客户的订单并开始冲泡咖啡或准备小吃。咖啡师可以在冲泡咖啡或准备小吃的同时接受其他顾客的多份订单,而不是等待咖啡冲泡完成或小吃准备好后再接见下一位顾客。这样,咖啡师可以同时有效地处理多个订单。

在咖啡师点单的同时,咖啡机和烤箱在后台同时工作,分别冲泡咖啡和烘焙点心。一旦咖啡或小吃准备好,咖啡师就会将其提供给相应的顾客,并继续接受新订单或完成的订单。

此场景演示了异步编程的概念。咖啡师(主要执行流程)可以继续接受订单(处理任务),而不会被冲泡咖啡或准备小吃(长时间运行的操作)的耗时过程所阻塞。咖啡机和烤箱(外部资源)同时工作,让咖啡店高效运作,及时为顾客服务。

在同步场景中,咖啡师接到顾客的订单并开始冲泡咖啡或准备小吃,然后等待咖啡冲泡完成或准备好小吃,然后再继续为下一位顾客服务。在咖啡师等待的时候,没有其他顾客可以下单,等候顾客的队伍越来越长。

在这种情况下,咖啡师(主要执行流程)被冲泡咖啡或准备零食(长时间运行的操作)的耗时过程所阻塞。因此,咖啡店的运营效率低下,客户的订单等待时间更长。

同步咖啡店类比展示了同步编程的局限性,其中任务按顺序执行,主执行流在等待耗时操作完成时被阻塞。这可能会导致资源使用效率低下以及应用程序或系统速度变慢、响应速度变慢。

或者就是说:让子弹飞一会儿?

这与 JavaScript 有什么关系?

JavaScript 和事件循环的上下文中,咖啡店类比有助于说明同步和异步编程之间的区别,以及它们如何影响 JavaScript 应用程序的整体性能和响应能力。

JavaScript 是单线程的,这意味着它一次只能执行一个操作。事件循环是 JavaScript 并发模型的核心部分,负责管理任务的执行,例如处理用户交互、网络请求、定时器和其他事件。

在同步 JavaScript 应用程序(类似于同步咖啡店)中,主要执行流程(咖啡师)一次处理一个任务,等待每个任务完成后再继续下一个任务。这种方法可能会导致性能问题和应用程序无响应,尤其是在处理耗时操作时,因为在等待这些操作完成时主执行流被阻塞。

另一方面,JavaScript 中的异步编程(类似于异步咖啡店)允许主要执行流程(咖啡师)在等待耗时操作完成的同时继续处理其他任务。事件循环通过持续监控调用堆栈和任务队列在管理异步任务方面发挥着至关重要的作用。当调用栈为空时,事件循环将任务从任务队列中出队,并将它们压入调用栈中执行。此过程使 JavaScript 能够在不阻塞主执行流的情况下并发处理多个任务,从而提高应用程序的整体性能和响应能力。

通过利用回调、Promisesasync/await 等异步编程技术,JavaScript 开发人员可以创建能够高效处理耗时操作的应用程序,允许事件循环(咖啡师)同时继续处理其他任务,最终提供一个更好的用户体验。

上代码

  1. 基于callback的流程(多个callback的复杂性反复摩擦):

function brewCoffee(callback) {  
    setTimeout(() => {  
        callback(null, 'Coffee');  
    }, 1000);  
}  

function prepareSnack(callback) {  
    setTimeout(() => {  
        callback(null, 'Snack');  
    }, 500);  
}  

brewCoffee((error, coffee) => {  
        if (error) {  
            console.error('Error brewing coffee:', error);  
        } else {  
            console.log(coffee);  
            prepareSnack((error, snack) => {  
            if (error) {  
                console.error('Error preparing snack:', error);  
            } else {  
                console.log(snack);  
                // Additional nested callbacks would increase complexity  
            }  
        });  
    }  
});
  1. 使用Promises(then和catch)的改进版:
function brewCoffee() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Coffee');
    }, 1000);
  });
}

function prepareSnack() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Snack');
    }, 500);
  });
}

brewCoffee()
  .then(coffee => {
    console.log(coffee);
    return prepareSnack();
  })
  .then(snack => {
    console.log(snack);
  })
  .catch(error => {
    console.error('Error:', error);
  });
  1. 使用async / await的 举栗代码:
// 提问,是先打印coffee 还是snack?


async function serveOrder() {
  try {
    const coffee = await brewCoffee();
    console.log(coffee);

    const snack = await prepareSnack();
    console.log(snack);
  } catch (error) {
    console.error('Error:', error);
  }
}

serveOrder();

这些示例演示了在从基于回调的流程过渡到 Promises,并最终过渡到 async/await 语法时,代码如何变得更具可读性和可管理性。async/await 流程使代码更易于理解和维护,因为它非常类似于同步代码,同时仍然受益于异步编程的优势。

但是这里发生了什么?🥚🐇

我开始喜欢在更基本的层面上理解事物,并且我会做出有根据的猜测,因为你在本文中已经做到了这一点。要真正理解 Promises,让我们解构它的魔力。最好的方法是构建我们自己的 Promise 模式版本,尽管是简化的。我们将其称为 AsyncTask它会将then**catch*作为方法公开,并允许将then*链接起来。

class AsyncTask {
	constructor(executor) {
		this.callbacks = [];
		this.catchCallbacks = [];
		this.state = 'pending';
		this.result = null;

		const resolve = (value) = > {
			if (this.state === 'pending') {
				this.state = 'fulfilled';
				this.result = value;
				this.callbacks.forEach((callback) = > callback(value));
			}
		};

		const reject = (reason) = > {
			if (this.state === 'pending') {
				this.state = 'rejected';
				this.result = reason;
				this.catchCallbacks.forEach((callback) = > callback(reason));
			}
		};

		try {
			executor(resolve, reject);
		} catch (error) {
			reject(error);
		}
	}

	then(onFulfilled) {
		return new AsyncTask((resolve, reject) = > {
			const wrappedOnFulfilled = (value) = > {
				try {
					resolve(onFulfilled(value));
				} catch (error) {
					reject(error);
				}
			};

			if (this.state === 'fulfilled') {
				wrappedOnFulfilled(this.result);
			} else if (this.state === 'pending') {
				this.callbacks.push(wrappedOnFulfilled);
			}
		});
	} catch (onRejected) {
		if (this.state === 'rejected') {
			onRejected(this.result);
		} else if (this.state === 'pending') {
			this.catchCallbacks.push(onRejected);
		}
		return this;
	}
}

这本质上是 Promise 模式的核心。它是对回调的抽象。显然,它周围有大量额外的工具和优化,因为它现在是该语言的原生语言,但仅此而已。

这就是我们在之前的咖啡店示例中使用它的方式:

function brewCoffee(order) {
	return new AsyncTask((resolve, reject) = > {
		console.log('Brewing coffee:', order);

		setTimeout(() = > {
			console.log('Coffee ready:', order);
			resolve(order);
		}, Math.random() * 2000);
	});
}

function prepareSnack(order) {
	return new AsyncTask((resolve, reject) = > {
		console.log('Preparing snack:', order);

		setTimeout(() = > {
			console.log('Snack ready:', order);
			resolve(order);
		}, Math.random() * 3000);
	});
}

function serveItems(items) {
	console.log('Serving:', items.join(' and '));
	return 'Served: ' + items.join(' and ');
}

function logServedStatus(status) {
	console.log(status);
}

const coffeeOrder = brewCoffee('Cappuccino');
const snackOrder = prepareSnack('Croissant');

coffeeOrder
    .then((coffee) = > snackOrder.then((snack) = > [coffee, snack])
        .then(serveItems)
        .then(logServedStatus))
    .catch ((error) = > {
            console.log('Error:', error);
    });

console.log('Taking the next customer...');

我们在这里无法真正模拟的是 async/await 关键字的用法。重要的是要理解这只是允许您假装代码同步运行的语法糖。在底层它仍然使用 Promises。

结论

异步编程不仅是现代 JavaScript 中的一个基本概念,也是其他流行编程语言(如 GoJavaC#)中的一个基本概念。通过采用CallbackPromisesJavaScript 中最新的 async/await 语法等异步技术,开发人员可以创建响应速度更快、性能更高并且能够有效处理耗时操作的应用程序。

了解 JavaScript 中的异步编程将使掌握其他语言中的类似概念变得更加容易,因为它们通常具有相同的原则和目标。例如,Go 使用 goroutines 和通道来实现并发,Java 使用线程和类CompletableFuture,而 C# 利用对象的async/await模式Task

从基于回调的流向 Promisesasync/await 的演变,极大地提高了 JavaScript 代码的可读性和可维护性,使开发人员更容易管理复杂的操作并确保流畅的用户体验。咖啡店的类比突出了异步编程的重要性,展示了它如何以并发方式高效处理任务,最终产生性能更好的应用程序。

随着编程语言的不断发展和成熟,开发人员必须及时了解异步编程中的最新技术和最佳实践。通过在 JavaScript 中掌握这些概念并理解它们在其他语言中的应用,开发人员可以创建高质量、高性能的应用程序,以满足现代用户对各种平台和技术的需求。

使用AI的同时,也要记得增强自身,不要过于依赖天行健 - 君子以自强不息