📕重学JavaScript:Promise 的then()、catch() 和 finally()

简介: 大部分时候,你要用的 Promise 对象是 Web API 或第三方 API 返回的。我们要设置 Promise 对象,让它在变成 fulfilled 的时候执行我们想要的成功的代码,而在变成 rejected 的时候执行我们想要的失败的代码。

📕重学JavaScript:Promise 的then()、catch() 和 finally()

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️

大部分时候,你要用的 Promise 对象是 Web API 或第三方 API 返回的。我们要设置 Promise 对象,让它在变成 fulfilled 的时候执行我们想要的成功的代码,而在变成 rejected 的时候执行我们想要的失败的代码。

你可以把 Promise 对象想象成一个包裹着异步操作的盒子。📦 这个盒子有很多 API 方法,让我们可以执行成功/失败的代码、处理错误、管理多个异步操作等等。

Promise 对象有两种方法,一种是实例方法,就是针对某个 Promise 对象用的;另一种是静态方法,就是直接用 Promise 这个类来调用的。

Promise 对象有 3 个实例方法,它们分别是 then()catch()finally()

📌then()

Promise 对象有一个叫做 then() 的方法,可以让您在 Promise 变成 fulfilledrejected 的时候执行一些代码。它需要两个函数作为参数。第一个函数是用来处理 fulfilled 状态的,第二个函数是用来处理 rejected 状态的。

我们先来看一个处理 fulfilled 状态的例子。👇

var promise = new Promise( (resolve, reject) => {
   
    setTimeout(() => {
   
        resolve( "我解决了" );
    }, 1000);
});

var handleFulfilled = value => {
    console.log( value ); };
promise.then( handleFulfilled );

// 我解决了

在这个例子里, then() 会把 handleFulfilled() 这个函数绑定到 Promise 对象上,让它在 Promise 变成 fulfilled 的时候执行。而且, handleFulfilled() 这个函数还会收到 Promise 对象的 value (就是我们给 resolve() 这个函数传的值)作为参数。等了一秒钟之后,Promise 对象变成了 fulfilled 状态, handleFulfilled() 这个函数就被执行了,并且把我们给 resolve() 这个函数传的值打印出来了。

我们再来看一个处理 rejected 状态的例子。👇

var promise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        reject( "出现错误" );
    }, 1000);

});

var handleFulfilled = value => {
    console.log( value ); };
var handleRejected = reason => {
    console.log( reason ); };
promise.then( handleFulfilled, handleRejected );

// 出现错误

handleRejected() 这个函数的作用就像是一个错误处理器,它可以捕获 reject() 抛出的错误。我们把 reject() 这个函数传的错误原因作为参数给处理器。在这个例子里,等了一秒钟之后,Promise 对象变成了 rejected 状态,我们的处理器就被执行了。它只是把原因打印出来了,并且不让错误继续传播。

then() 这个方法会返回一个新的 Promise 对象。当原来的 Promise 对象被解决了,并且执行了两个处理器中的一个时,返回的 Promise 对象的最终状态要看 handleFulfilled()handleRejected() 这两个处理器里面发生了什么。

就像 resolve()reject() 负责改变原来的 Promise 对象的状态一样, handleFulfilled()handleRejected() 也负责改变 then() 返回的 Promise 对象的状态。

如果这些处理器中的任何一个有返回值,那么返回的 Promise 对象就会用那个值来变成 fulfilled 。如果他们没有返回值,那么返回的 Promise 对象就会用 undefined 来变成 fulfilled 。如果这些处理器中的任何一个抛出了错误,那么返回的 Promise 对象就会用那个错误来变成 rejected

var origPromise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        resolve( "fulfilled" );
    }, 1000);

});

var handleFulfilled = value => {
    
  console.log( value ); 
  return "fulfilled";
};
var returnedPromise = origPromise.then( handleFulfilled );

console.log( "1", returnedPromise );

setTimeout(() => {
   
  console.log( "2:", returnedPromise );
}, 2000);

/*
OUTPUT
1: Promise { <state>: "pending" }

fulfilled

2: Promise { 
    <state>: "fulfilled", 
    <value>: "fulfilled" 
  }
*/

在这个例子里, then() 这个方法返回了一个新的 Promise 对象,我们叫它 returnedPromise 。它一开始是 pending 状态。当 origPromise 在一秒钟之后变成 fulfilled 状态时,就会执行 handleFulfilled() 这个处理器,并且返回了一个字符串。因为它有返回值,所以 returnedPromise 就会用那个值或字符串来变成 fulfilled 状态。我们在第 21 行有第二个 setTimeout() ,它会在两秒钟之后打印 returnedPromise ,也就是在一秒钟之后,并且在两个 Promise 对象都被解决之后。

如果在这个例子里,如果 handleFulfilled() 这个处理器里面出了错,而不是有返回值,那么 returnedPromise 就会变成 rejected 状态,并且用 handleFulfilled() 抛出的错误原因来变成 rejected 状态。如果没有指定原因,那么就会用 undefined 来变成 rejected 状态。

var origPromise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        resolve( "fulfilled" );
    }, 1000);

});

var handleFulfilled = value => {
    
    console.log( value ); 
    throw("出现错误"); 
};
var returnedPromise = origPromise.then( handleFulfilled );

console.log( "1:", returnedPromise );
setTimeout(() => {
   
  console.log( "2:", returnedPromise );
}, 2000);

/*
OUTPUT
1: Promise { <state>: "pending" }

fulfilled

Uncaught (in promise) 出现错误
2: Promise { 
        <state>: "rejected",
        <reason>: "出现错误" 
    }
*/

handleRejected() 这个处理器也是一样的。如果它有返回值,那么 returnedPromise 就会用那个值来变成 fulfilled 状态。如果出了错, returnedPromise 就会用那个错误来变成 rejected 状态。

then()里不带任何处理会发生什么呢?

当我们用 then() 的时候不传任何处理, then() 的两个参数都可以不传。如果我们不传它们,返回的 Promise 对象就会和原来的 Promise 对象一样。

var origPromise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        resolve( "fulfilled" );
    }, 1000);

});

var returnedPromise = origPromise.then();

console.log( "1:", returnedPromise );

setTimeout(() => {
   
  console.log( "2:", returnedPromise );
}, 2000);

/*
OUTPUT

1: Promise { <state>: "pending" }

2: Promise { 
        <state>: "fulfilled", 
        <value>: "fulfilled" 
    }
*/

在这个例子里,我们没有给 then() 这个方法传任何处理。所以,当 origPromise 有了一个值的时候, returnedPromise 也会有同样的值。

如果 origPromise 因为某个原因变成了 rejected 状态, returnedPromise 也会因为同样的原因变成 rejected 状态。

方法链

then() 这个方法能返回一个新的 Promise 对象是一个很强大的功能。我们可以把很多个 then() 方法连起来,形成一个 then() 方法链。每个 then() 方法里面的处理器都会按照它们在链里面的顺序执行。每个 then() 方法里面的处理器返回的值都会传给下一个 then() 方法里面的 handleFulfilled 处理器。每个 then() 方法里面的处理器抛出的错误都会被链里面第一个有定义 rejected 处理器的后续 then() 方法捕获。如果后面的 then() 方法都没有定义 rejected 处理器,那么就会出现未捕获的异常。

var thingsToBuyPromise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        resolve( "书" );
    }, 1000);

});

thingsToBuyPromise
    // 1st
    .then( value => {
   
        console.log( "1. " + value ); // 1. 书
        return "牛奶"; 
    })
    // 2nd
    .then( value => {
   
        console.log( "2. " + value ); // 2. 牛奶
        return ("面包"); 
    })
    // 3rd
    .then( value => {
   
        console.log( "3. " + value ); // 3. 面包
        throw( "算了不买了" ); 
    })
    // 4th
    .then( undefined, reason => {
   
        console.log( reason );
        throw( "买个新的" );
    })
    // 5th
    .then( undefined, reason => {
    
        console.log( reason );
        return "水果"; 
    })
    // 6th
    .then( value => {
   
        console.log( "1. " + value ); // 1. 水果
        return "蔬菜";
    })
    // 7th
    .then( value => {
   
        console.log( "2. " + value ); // 2. 蔬菜
        return "That's it...";
    });
/* 

OUTPUT:

1. 书
2. 牛奶
3. 面包
算了不买了
买个新的
1. 水果
2. 蔬菜

*/

在这个例子里, thingsToBuyPromise 的值是“书”。这个值被传给了第一个 then() 里面的 fulfilled 处理器。这个处理器返回了另一个值“牛奶”,它让第一个 then() 返回的 Promise 对象变成了 fulfilled 状态。这就触发了第二个 then() 里面的 fulfilled 处理器,它收到了值“牛奶”并返回了另一个值“面包”。这让第二个 then() 返回的 Promise 对象也变成了 fulfilled 状态。然后,第三个 then() 里面的 fulfilled 处理器被执行,但是它抛出了一个错误。这个错误被第四个 then() 里面的 rejected 处理器捕获。这个 then() 也抛出了一个错误,这个错误被第五个 then() 捕获。你可能已经明白了这个过程。

你可以试着把第四个和第五个 then() 去掉,看看会发生什么。第三个 then() 抛出的错误会导致一个未捕获的异常,因为后面的 then() 方法都没有定义 rejected 处理器来捕获这个错误。因为有错误,第六个和第七个 then() 里面的处理器都不会被执行。

如果你想知道为什么我们在上面的例子里把第四个和第五个 then() 里面的 fulfilled 处理器设成了 undefined ,那是因为我们只关心在这部分链里捕获错误。其实,Promise API 有一个叫做 catch() 的方法来专门做这件事。

🚩catch()

顾名思义,就是用来捕获错误的。它的效果就跟一个没有 fulfilled 处理器的 then() 一样: then(undefined, handleRejected){...} 。其实,这就是 catch() 里面做的事情,它用 then() ,第一个参数是 undefined ,第二个参数是 rejected 处理函数。这个处理函数是 catch() 唯一需要的参数。

var promise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        reject( "出现错误" );
    }, 1000);

});

var handleRejected = reason => {
    console.log(reason); }
promise.catch( handleRejected );

/* 
OUTPUT:
出现错误
*/

then() 一样, catch() 也会返回一个 Promise 对象,所以和 then() 一样,它也可以使用方法链。我们来把我们的链的例子改一下,加上 catch()

var thingsToBuyPromise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        resolve( "书" );
    }, 1000);

});

thingsToBuyPromise
    // 1st
    .then( value => {
   
        console.log( "1. " + value ); // 1. 书
        return "牛奶"; 
    })
    // 2nd
    .then( value => {
   
        console.log( "2. " + value ); // 2. 牛奶
        return ("面包"); 
    })
    // 3rd
    .then( value => {
   
        console.log( "3. " + value ); // 3. 面包
        throw( "算了不买了" ); 
    })
    // 4th
    .catch( reason => {
   
        console.log( reason );
        throw( "买个新的" );
    })
    // 5th
    .catch( reason => {
    
        console.log( reason );
        return "水果"; 
    })
    // 6th
    .then( value => {
   
        console.log( "1. " + value ); // 1. 水果
        return "蔬菜";
    })
    // 7th
    .then( value => {
   
        console.log( "2. " + value ); // 2. 蔬菜
        return "That's it...";
    });
/* 

OUTPUT:

1. 书
2. 牛奶
3. 面包
算了不买了
买个新的
1. 水果
2. 蔬菜

*/

我们做的事情就是把上一个例子里的第四个和第五个 then() 换成了 catch() 。其他的都没变。但是这样做肯定更方便,看起来更清楚,而不用在哪里写 undefined

在 Promise 链里,我们可以随便用多少个 then()catch() 方法,或者它们的组合。

到现在为止,我们已经知道了 catch() 方法可以捕获以下错误:

  1. 在执行器函数里直接抛出的错误。
  2. 在 Promise 链里任何前面的 then()catch() 方法里面的处理器抛出的错误。

它还可以捕获在调用 resolve()reject() 函数之前执行器函数里抛出的错误。看看下面的例子。我们在调用 resolve() 之前抛出了一个错误。这会让 Promise 对象变成 rejected 状态,并且用抛出的错误作为原因。因为 Promise 对象变成了 rejected 状态,所以 catch() 里面的处理器就会被执行。

在这个例子里,如果我们用 reject() 代替 resolve() ,那么结果也是一样的。Promise 对象会变成 rejected 状态,原因是抛出的错误,而不是传给 reject() 函数的原因。

有些情况下,错误不能被捕获

但是,如果我们在调用 resolve()reject() 后抛出错误,则该错误将被忽略。

var promise = new Promise( (resolve, reject) => {
   

    resolve( "fulfilled" );
    throw( "出现错误" ); // silenced

});

promise.then(    
    value => {
    // 执行到这里
        console.log( value ); 
    }, 
    reason => {
    // 不会被执行
        console.log( reason ); 
    } 
);

/* OUTPUT

fulfilled

*/

这是因为我们知道,抛出错误就相当于让 Promise 对象变成 rejected 状态。但是我们已经用 resolve() 让 Promise 对象变成了 fulfilled 状态。一旦变成了 fulfilled 状态,Promise 对象的状态就不能再变了,所以抛出的错误就被忽略了。如果我们在上面的例子里用 reject() 代替 resolve() ,也是一样的。Promise 对象会变成 rejected 状态,原因是传给 reject() 的值,而抛出的错误也会被忽略。

一个通用的建议是,如果你要用构造函数来创建 Promise 对象,请确保在执行器函数里面最后一件事就是调用 resolve()reject()

不能被 catch() 的情况

现在我们知道 catch() 能够捕获哪种错误,但在一种情况下 catch() 不起作用。它无法捕获异步代码中发生的错误。考虑以下示例:

var promise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        throw( "出现错误" );
        resolve( "fulfilled" );
    }, 1000);

});

var handleRejected = reason => {
    console.log(reason); };
// 没有执行
promise.catch( handleRejected );

在这个例子里,在我们用 resolve() 让 Promise 对象变成 fulfilled 状态之前, setTimeout() 里面的回调函数出了错。它不像我们之前看到的例子那样直接在执行器函数里面。你可以说 Promise 对象不知道这个错误,所以我们的 catch() 里面的处理器没有捕获到这个错误,结果就是一个未捕获的异常。

总的来说, catch() 只能捕获以下错误:

  1. 在用 resolve()reject() 改变 Promise 对象状态之前直接在执行器函数里面抛出的错误。
  2. 因为用 reject() 让原来的 Promise 对象变成 rejected 状态而抛出的错误。
  3. 在 Promise 链里任何前面的 then()catch() 里面的处理器抛出的错误。

但是它不能捕获异步代码里面抛出的错误。

📌finally()

如果我们有一个 catch() 方法,那么我们也肯定有一个 finally() 方法。这个方法的主要作用是执行一些无论 Promise 对象是变成 fulfilled 还是 rejected 状态都要做的事情,比如清理一些东西。

比如说,如果我们用 AJAX 提交了一个表单,并且显示了一个转圈的图标来表示正在处理,那么无论 AJAX 请求返回的是成功还是失败的响应,只要有响应,我们就要把那个转圈的图标隐藏掉。所以隐藏图标的代码就可以放在 finally() 方法里面的处理器里。我们也可以把这个代码放在 then() 里面的两个处理器里,但是这样就会重复,这不是一个好的编码习惯。

finally() 方法需要一个函数作为参数。但是和 then()catch() 里面的处理器不一样, finally() 里面的函数不需要任何参数。这是因为这个函数会在 fulfilledrejected 状态都执行,并且它不知道它收到的值是成功的值还是失败的原因。

var promise = new Promise( (resolve, reject) => {
   

    setTimeout(() => {
   
        resolve( "fulfilled" );
    }, 1000);

});

var handleFinally = () => {
    
    console.log( "finally" ); 
}
promise.finally( handleFinally );

/* 
finally
*/

就像 then() 一样, finally() 也返回一个promise对象,因此它也可以被链接。但 then()finally() 在返回的promise的结算方式上存在一些差异。

var origPromise = new Promise( (resolve, reject) => {
    
    resolve( "fulfilled" ); 
});

var handleFinally = () => "fulfilled by finally";

var returnedPromise = origPromise.finally( handleFinally );

setTimeout( () => {
    
    console.log( returnedPromise ); 
}, 1000 );

/* 
Promise { 
    <state>: "fulfilled", 
    <value>: "fulfilled" 
}
*/

在之前用 then() 的例子里,从 then() 返回的 Promise 对象会用它的处理器返回的值来变成 fulfilled 状态。但是在上面的例子里, finally() 里面的 returnedPromise 会用和 origPromise 一样的值来变成 fulfilledrejected 状态,而不是它的处理器返回的值。这是因为就像 finally() 里面的函数不需要任何参数一样, finally() 也不会有任何返回值。我们希望它只做一些简单的清理工作,并且不会影响 Promise 链里面的信息流。所以我们在 finally 处理器里面返回的任何值都会被忽略。

但是不管多么简单,只要有代码,就有可能出错, finally() 也不例外。所以,如果 finally() 处理器里面出了错,那么 returnedPromise 就会用那个错误来变成 rejected 状态。

var origPromise = new Promise( (resolve, reject) => {
    
    resolve( "fulfilled" ); 
});

var handleFinally = () => {
    throw( "出现错误" ) };

var returnedPromise = origPromise.finally( handleFinally );

setTimeout( () => {
    
    console.log( returnedPromise ); 
}, 1000 );

/*
Uncaught (in promise) 出现错误
Promise { 
    <state>: "rejected", 
    <reason>: "出现错误" 
}
*/

从技术上讲,我们可以使用 then()catch()finally() 的任意组合,但典型的Promise链如下所示......

...
...
.then( handleFulfilled1 )
.then( handleFulfilled2 )
.then( handleFulfilled3 )
.catch( handleRejected )
.finally( handleSettled )

所以基本上,我们用 then() 来处理异步操作的结果,并把需要的参数传给 Promise 链里面的下一个 then() 处理器。我们在 Promise 链的最后用 catch() 来处理错误,最后,我们用 finally() 来做清理。另外,在实际中,建议用 then() 来处理成功的情况,用 catch() 来处理失败的情况。这就是为什么我们没有在上面的 then() 里面加上 rejected 处理器。


🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨

目录
相关文章
|
6天前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
16 1
|
6天前
|
前端开发 JavaScript
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
29 4
|
6天前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:Promise 和 Async/Await
在现代的 JavaScript 开发中,异步编程是至关重要的。本文将介绍 JavaScript 中的异步编程概念,重点讨论 Promise 和 Async/Await 这两种常见的处理异步操作的方法。通过本文的阐述,读者将能够更好地理解和应用这些技术,提高自己在 JavaScript 开发中处理异步任务的能力。
|
6天前
|
JSON 前端开发 JavaScript
【JavaScript技术专栏】JavaScript异步编程:Promise、async/await解析
【4月更文挑战第30天】JavaScript中的异步编程通过Promise和async/await来解决回调地狱问题。Promise代表可能完成或拒绝的异步操作,有pending、fulfilled和rejected三种状态。它支持链式调用和Promise.all()、Promise.race()等方法。async/await是ES8引入的语法糖,允许异步代码以同步风格编写,提高可读性和可维护性。两者结合使用能更高效地处理非阻塞操作。
|
4天前
|
前端开发 JavaScript
前端 js 经典:Promise
前端 js 经典:Promise
10 1
|
6天前
|
前端开发 JavaScript
在JavaScript中,回调函数、Promise和async/await这三种异步处理机制的优缺点
JavaScript的异步处理包括回调函数、Promise和async/await。回调函数简单易懂,但可能导致回调地狱和错误处理困难。Promise通过链式调用改善了这一情况,但仍有回调函数需求和学习成本。async/await提供同步风格代码,增强可读性和错误处理,但需ES8支持,不适用于并发执行。根据项目需求选择合适机制。
|
6天前
|
前端开发 JavaScript
js开发:请解释Promise是什么,以及它如何解决回调地狱(callback hell)问题。
Promise是JavaScript解决异步操作回调地狱的工具,代表未来可能完成的值。传统的回调函数嵌套导致代码难以维护,而Promise通过链式调用`.then()`和`.catch()`使异步流程清晰扁平。每个异步操作封装为Promise,成功时`.then()`传递结果,出错时`.catch()`捕获异常。ES6的`async/await`进一步简化Promise的使用,使异步代码更接近同步风格。
24 1
|
6天前
|
JavaScript
JS异常处理——throw和try、catch以及debugger
JS异常处理——throw和try、catch以及debugger
|
6天前
|
前端开发 JavaScript API
JavaScript学习笔记(一)promise与async
JavaScript学习笔记(一)promise与async
|
6天前
|
前端开发 JavaScript UED
JavaScript中的异步编程和Promise
【2月更文挑战第3天】在Web开发中,JavaScript是一门非常重要的编程语言,而异步编程是JavaScript中的一个关键概念。本文将介绍JavaScript中的异步编程特点,以及如何使用Promise来更加优雅地处理异步操作,帮助开发者更好地理解和应用这一技术。
19 3