Promise是Node.js中的核心概念,如果要洞悉Node.js的异步编程,深入理解Promise是必须要迈过的一道门槛。这里我将以通俗、说人话的方式聊聊Promise的前世今生。
什么是Promise
Promise,英文翻译是“承诺”。官方定义如下:
Promise是一个代表未来可能完成或失败的异步操作及其结果值的对象。在Node.js中,由于其非阻塞I/O的特性,异步编程变得非常重要。Promise允许我们将异步方法的返回值处理得像同步方法一样,即不是立即返回最终值,而是返回一个Promise,该Promise将在未来的某个时刻提供值。
好了,开始说人话。我们在编程写业务逻辑的时候,总会遇到一些异步操作吧,比方说当前在写一块业务逻辑,其中会遇到下载文件,下载文件结束之后,在DB中记录一下,下载文件和数据库入库这两个操作,我需要异步完成就行了,我的业务主流程应该继续跑,该下载文件就下载文件,下载文件之后就入库下载记录,完了,就这样。在Java中,我们会在主业务逻辑中开一个线程池,把下载文件和下载记录入库这一块提交到线程池中执行就行了。然而遗憾的是,Node.js在处理业务时并没有线程池,怎么办呢?于是,Promise就出场了。
Promise翻译为“承诺”,表示对于需要异步进行的代码逻辑,我们建立一个“承诺”,用来封装和跟踪这块异步代码,“承诺”总要去实现嘛,对于这个“承诺”,当这个承诺有结果了(包括成功的结果和失败的结果),再把结果反馈给当前这块异步代码的调用方,也就是前面说的主业务流程。那么,怎么把异步执行的结果反馈给异步代码的调用方呢,这里我们会事先定义好两个回调函数,一个用于处理成功的结果,另一个用于执行处理失败的结果,把这两个回调函数提交给Promise,这样当Promise中执行的异步代码有结果了,就知道下一步该怎么做啦。
Talk is cheap,show me the code!来,一起看代码:
1// 生成指定范围 [min, max] 的随机整数
2function getRandomNumber(min, max) {
3 // 返回一个介于 min 和 max 之间的随机整数
4 return Math.floor(Math.random() * (max - min + 1)) + min;
5}
6
7// 生成 1 到 10 之间的随机整数
8function generateRandomNumber() {
9 // 调用 getRandomNumber(1, 10) 来获取随机数
10 return getRandomNumber(1, 10);
11}
12
13// 创建一个 Promise 对象,根据随机数的结果来解决或拒绝
14let promise = new Promise((resolve, reject) => {
15 const num = generateRandomNumber(); // 生成随机数
16 if (num > 5) {
17 resolve(num); // 如果随机数大于 5,解决 Promise
18 } else {
19 reject("出错啦!"); // 如果随机数不大于 5,拒绝 Promise
20 }
21});
22
23// 使用 then 方法处理 Promise 结果
24promise.then(function (data) { // 成功时的处理
25 console.log("The result is " + data);
26}, function (err) { // 失败时的处理
27 console.log(err);
28});
假设生成随机数是一个很耗时的异步操作,需要很久才返回,而且是非阻塞的异步操作(实际上并不是),生成的随机数的结果值如果大于5,调用回调函数resolve,否则调用回调函数reject。promise.then方法有两个函数类型的参数,第一个正对应于前面的resolve,第二个正对应前面的reject。new Promise()对象的构成函数参数是一个函数,这个函数有两个参数,分别为resolve和reject,这个函数参数签名是固定的写法。Promise构造函数的参数叫做执行器函数。整个代码的执行顺序如下:
- new 一个Promise对象,然后立马执行执行器函数里面的内容,同时把then里面的两个回调函数分别作为实参,传递给形参resolve和reject
- 如果执行器函数体里面执行了resolve(num),就会调用回调函数then方法的第一个参数回调函数,执行回调函数体的内容,同时把实参num传给回调函数的形参data
- 如果执行器函数体里面执行了reject(num),就会调用调用回调函数then方法的第二个参数回调函数,执行回调函数体的内容,同时把实参
"出错啦!"
传给回调函数的形参err
Promise的基本概念
在前面的例子中,两个回调函数的传递,以及Promise执行器函数的执行,都是由Node.js自己内部完成的,我们在应用中是无法干预的。Promise内部维护了一个状态机,具有以下三种状态:
- Pending(进行中):这是Promise的初始状态,表示异步操作尚未完成。
- Fulfilled(已成功):表示异步操作已成功完成。
- Rejected(已失败):表示异步操作失败。
Promise执行器函数中,遇到resolve或者reject,就会把对应的回调函数体内容作为一个微任务,提交给Node.js的事件循环,等待下一次事件循环来调度执行。Promise在设计上是异步的,什么意思呢,也就是执行器函数中,从第一行代码到resolve或者reject之间的代码,请你不要出现同步阻塞的操作,那你说我就要放置一个耗时的同步操作在其中,会怎么样?没怎么样,就同步执行而已,会阻塞到主线程。
链式调用
链式调用是指将多个.then()方法连续链接在一起,以便按顺序处理Promise的结果。每个.then()方法可以返回一个值,这个值会被传递给下一个.then()的回调函数。如果链中的某个环节返回了另一个Promise,那么链会继续等待这个新Promise完成。
1doSomething()
2 .then(result => {
3 // 处理结果
4 return doAnotherThing(result);
5 })
6 .then(newResult => {
7 // 处理新的结果
8 return doThirdThing(newResult);
9 })
10 .then(finalResult => {
11 // 处理最终结果
12 });
在这个例子中,doSomething()返回一个Promise,它完成后会调用第一个.then()。第一个.then()完成其操作后,返回一个新的Promise(由doAnotherThing()返回),链中的下一个.then()将等待这个新Promise完成。这个过程会一直持续,直到链的末尾。
多嘴一句,怎么理解链式调用呢?第一个.then方法返回的是一个新的Promise,这个新Promise和下一个then方法的回调函数体关联起来。这里关联的含义是,下一个then方法的回调函数作为实参传递给前面新Promise执行器函数的形参,也就是第一个形参resolve,同时,在前面新Promise执行器函数体中,执行到resolve这行时,会调用下一个then方法第一个回调函数参数,理解这一句话至关重要。把then方法的函数参数传递给Promise的执行器函数的参数,这一过程是由Node.js内部完成的,所以对我们来说有一定的理解难度。
错误处理
Promise的错误处理是通过.catch()方法实现的,它用于捕获链式调用中任何一个Promise失败(Rejected)的情况。.catch()方法应该放在链的末尾,以确保能够捕获到链中任何位置产生的错误。
1doSomething()
2 .then(result => {
3 // 处理结果
4 return doAnotherThing(result);
5 })
6 .then(newResult => {
7 // 处理新的结果
8 return doThirdThing(newResult);
9 })
10 .then(finalResult => {
11 // 处理最终结果
12 })
13 .catch(error => {
14 // 处理链中产生的任何错误
15 console.error(error);
16 });
如果doSomething、doAnotherThing或doThirdThing中的任何一个函数返回的Promise被Rejected,那么.catch()方法中的回调函数将被执行,并且错误将被传递给这个回调函数。