📕重学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 处理器。


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

目录
相关文章
|
3月前
|
前端开发 JavaScript
用JavaScript 实现一个简单的 Promise 并打印结果
用 JavaScript 实现一个简单的 Promise 并打印结果
|
3月前
|
JSON 前端开发 JavaScript
在 JavaScript 中,如何使用 Promise 处理异步操作?
通过以上方式,可以使用Promise来有效地处理各种异步操作,使异步代码更加清晰、易读和易于维护,避免了回调地狱的问题,提高了代码的质量和可维护性。
|
3月前
|
前端开发 JavaScript Java
一文带你了解和使用js中的Promise
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,正向全栈进发。如果我的文章对你有帮助,请关注我,将持续更新更多优质内容!🎉🎉🎉
31 0
一文带你了解和使用js中的Promise
|
3月前
|
JSON 前端开发 JavaScript
浅谈JavaScript中的Promise、Async和Await
【10月更文挑战第30天】Promise、Async和Await是JavaScript中强大的异步编程工具,它们各自具有独特的优势和适用场景,开发者可以根据具体的项目需求和代码风格选择合适的方式来处理异步操作,从而编写出更加高效、可读和易于维护的JavaScript代码。
43 1
|
4月前
|
JavaScript 前端开发
JS try catch用法:异常处理
【10月更文挑战第12天】try/catch` 是 JavaScript 中非常重要的一个特性,它可以帮助我们更好地处理程序中的异常情况,提高程序的可靠性和稳定性。
68 1
|
4月前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:深入了解 Promise 和 async/await
【10月更文挑战第8天】JavaScript 中的异步编程:深入了解 Promise 和 async/await
|
4月前
|
前端开发 JavaScript 小程序
JavaScript的ES6中Promise的使用以及个人理解
JavaScript的ES6中Promise的使用以及个人理解
41 1
|
5月前
|
前端开发 JavaScript
JavaScript中的Promise:简化异步编程
JavaScript中的Promise:简化异步编程
|
5月前
|
Web App开发 前端开发 JavaScript
js之 Promise | 12-8
js之 Promise | 12-8
|
5月前
|
前端开发 JavaScript
ES6新标准下JS异步编程Promise解读
ES6新标准下JS异步编程Promise解读
49 3

热门文章

最新文章