Promise.then() 的两个回调参数
then
是 Promise 原型上的一个方法, 返回一个 promise 对象。
它可以接受两个回调参数: 1. onResolved
, 2. onRejected
。
这两个参数各自有一个参数, 分别是: 执行成功的数据 和 执行失败的数据。 其实我们平时用的最多的就是第一个回调函数, 用它来处理请求成功后的数据。比如:
new Promise((resolve, reject) => {
resolve('成功了');
}).then((data) => {
// 这里的data就是 异步请求成功后 resolve接收到的值
console.log('success: ', data); // output: success: 成功了
})
但其实, 它还有第二个回调函数, 用来捕获并处理失败时的数据(catch 正是基于此的一种简写形式):
new Promise((resolve, reject) => {
reject('success');
}).then(
(data) => { console.log('success: ', data); },
(err) => { console.log('failed: ', err); }
)
Promise 链
请问,以下形式是一个 Promise 链吗?
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) { alert(result);
promise.then(function(result) { alert(result);
promise.then(function(result) { alert(result);
新手常犯的一个经典错误:从技术上讲,我们也可以将多个 then
添加到一个 promise 上。但这并不是 promise 链(chaining)。
这里所做的只是一个 promise 的几个处理程序。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务。所以,在上面的代码中,所有 alert 都显示相同的内容:1。
只有在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。这才是 Promise 的链式调用。
Promise 链式调用的两个注意点:
1. 链式调用时,上一次 .then() 处理的数据变成了 undefined ?
我们来看下面这个例子:
new Promise((resolve, reject) => {
resolve('成功了')
})
.then(
(data) => { console.log('onResolved1', data); },
(error) => { console.log('onRejected1', error); }
)
.then(
(data) => { console.log('onResolved2', data); },
(error) => { console.log('onRejected2', error); }
)
↑ 第二个 .then() 会打印什么结果呢?
会发现在第2个 .then() 中, 虽然也走了处理成功的回调, 但是数据却丢失了, 变成了 undefined 。 说明上一个 .then() 返回的 promise 对象, 它的数据肯定丢失了, 我们可以再来打印一下这个 promise:
不难看出,这里的 result 确实变成了 undefined 。这是因为:在第一次的 .then() 中,处理成功的 onResolved 回调参数没有显示地使用 return 返回数据
,在 Promise 内部相当于只写了 return 。因此,在下一次的链式调用时, 后续 .then() 的 onResolved 就只能拿到 undefined 了。
所以,想要在链式调用中让 result 结果一直传递下去就必须在处理函数中(不管是 onResolved 还是 onRejected )显示地返回数据,否则数据就会丢失。
而对于返回值的类型,有两种情况:一个是返回固定值,一个是返回 Promise。如果返回的是 Promise,那么 Promise 内部会将里面的 result 结果取出来:
.then(
(data) => {
console.log('onResolved1', data)
// 情况1: 返回固定值
return 'success';
// 情况2: 返回promise
return new Promise(resolve => {
resolve(data)
});
}
)
.then((data) => { console.log('onResolved2', data)})
以上两种情况, 都可以正常执行, 让下一个.then()中的回调函数拿到数据:
2. onResolved 和 onRejected 的执行时机
既然 then 里面有两个回调, 我怎么知道链式调用的时候会走哪一个呢?
const p = new Promise((resolve, reject) => {
reject('失败了')
}).then(
(data) => { console.log('onResolved1', data)},
(error) => {
// 场景1: 没有 return
console.log('onRejected1', error)
// 场景2: return 固定值
return 'failed'
// 场景3: return Promise.reject
return Promise.reject(error)
// 场景4: return Promise.resolve
return Promise.resolve(error)
// 场景5: 抛出错误:
throw error;
}
)
p.then(
(data) => { console.log('onResolved2', data)},
(error) => { console.log('onRejected2', error)}
)
会发现:
上一个.then 中 onResolved 的返回值 | 下一个.then 的执行结果 |
---|---|
return undefined | 执行 onResolved |
return 固定值 | 执行 onResolved |
return Promise.resolve | 执行 onResolved |
return Promise.reject | 执行 onRejected |
throw 数据 | 执行 onRejected |
如上表所示,上一个 .then( ) 中 onResolved 回调函数返回不同的结果, 会决定下一个 .then( ) 去执行不同的回调。(换句话说: 和上一个.then() 执行哪个回调无关, 只和其返回值有关
)
Promise 异常穿透与错误处理
catch 是什么?
catch 是 Promise 原型是的一个方法,在 promise 被拒绝时,可以在其中执行异步函数处理错误。实际上,此方法是 then 的一种完全模拟,只是个简写形式:
// catch 等价于 =>
Promise.prototype.then(undefined, onRejected)
它会立即返回一个等价的 Promise
对象,这可以允许你继续链式调用其他 promise 的方法,比如:
.catch(error => {
console.log(error);
return "错误处理完了"
})
.then(data => {
console.log(data); // output => "错误处理完了"
})
虽然这样写不常见。
.then 对比 .catch
这两个代码片段是否相等?换句话说,对于任何处理程序(handler),它们在任何情况下的行为都相同吗?
promise.then(f1).catch(f2);
promise.then(f1, f2);
答案是不相等。
不同之处在于,如果 f1
中出现 error,那么在第一个链式调用中,error 会被 catch 捕获,并在 f2
中被处理,但是在第二种写法中不会,这是因为 error 是沿着链传递的,而在第二段代码中,f1
和 f2
处于同一层级,二者只会执行其一,下面没有链,所以 error 不会被处理。
隐式的 try…catch
Promise 的 error 是如何在链式调用中向下传递的并被处理的呢?
通常,一遇到异常抛出,浏览器就会顺着 Promise 链寻找下一个就近的 onRejected
失败回调函数或者由 .catch()
指定的回调函数。你可以想象成 promise 的整个执行器(executor)和 promise 的处理程序周围有一个隐式的 “try...catch
”。
这一错误的传递就被称为 promise 的异常穿透
。
new Promise((resolve, reject) => {
reject('失败了')
})
.then(
(data) => { console.log('onResolved1', data); },
(error) => { console.log('onRejected2', error); }
)
.catch(
(error) => {
console.log('catch', error)
}
)
上述例子中,"失败了"
就会被离它最近的 then 中的 onRejected
函数所处理,而不会被 catch 所捕获。
异步回调中的错误无法捕获
需要注意的是:定时器这种异步中的错误,promise 捕获不到的:
new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error("Whoops!");
}, 1000);
})
.catch(error => {
console.log('catch error:', error);
});
正如之前所讲,函数代码周围有个隐式的 “try..catch
”。所以,所有同步错误都会得到处理。
但是这里的错误并不是在 executor 运行时生成的,而是在稍后生成的。这和浏览器的执行机制有关,你可以理解成:在延时等待 1s 后,抛出错误这一操作才会执行,但此时外面的 Promise 早就执行结束退出主线程了。因此,promise 无法处理它,程序会在此崩掉。
executor 中的异步错误解决方案
所以在 executor 执行器中,不管最终拿到什么结果,建议都使用 resolve
或 reject
去接收,以便后续的链式调用,这是一种好习惯:
// 直接使用 reject 接收 error
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Whoops!"))
}, 1000);
})
当然,你也可以先用 try...catch
捕获 throw 出的 error,然后用 reject
去接收。请注意!try...catch
必须写在定时器里面,不然又捕获不到了,原因刚刚已经说过了。
new Promise((resolve, reject) => {
setTimeout(() => {
try {
throw new Error("Whoops!");
} catch(error) {
reject(error)
}
}, 1000);
})
以上两种都是可以的。
try...catch 不能捕获 promise 的错误
注意这里说的 try...catch
并不是上一节说的 Promise 内部 “隐式的 try...catch”,而是想在 Promise 外部通过使用try...catch
捕获其内部的错误,请看下面两个示例:
function fn1() {
try {
new Promise((resolve, reject) => {
throw new Error('promise1 error')
})
} catch (error) {
console.log(e.message);
}
}
function fn2() {
try {
Promise.reject('promise2 error');
} catch (error) {
console.log(error);
}
}
以上两个 try...catch 都不能捕获到 error,因为 promise 内部的错误不会冒泡出来,而是被 promise 吃掉了。所以,在 try...catch 看来,这只是一个 promise,而不是语法错误,至于里面具体是什么,它并不知道也不会处理。
只有通过 promise 的 then 和 catch 才可以捕获,所以单用 Promise 一定要写 catch !
如果我是愣头青,非要使用 try...catch
呢?
async/await 解决方案
async/await 很好的解决了愣头青的烦恼,相比于 promise.then
,await
只是获取 promise 的结果的一个更优雅的语法,并且也更易于读写。
如果一个 promise 正常 resolve,await promise
返回的就是其结果。但是如果 promise 被 reject,它将 throw 这个 error,就像在这一行有一个 throw
语句那样,try...catch
就能顺理成章地捕获到了。
请看以下常见的请求例子:
// 模拟请求
function http(params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (params === 0) reject("error")
resolve("success")
}, 1000);
})
}
// 业务调用
async function query(params) {
try {
const data = await http(params)
console.log('data:', data);
} catch (error) {
console.log('error:', error);
}
}
query(0)
如何中断promise
最后,如果等到最后.catch()处理完, 想结束promise链, 不想再让其链式调用下去了, 可以作如下操作:
.catch((err) => {
console.log('onRejected', err);
// 中断promise链:
return new Promise(() => {})
})
通过返回一个状态一直为 pending 的 promise 即可。