欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

链式允诺调用、异常穿透、打破允诺链

最编程 2024-03-05 22:56:45
...

Promise.then() 的两个回调参数

thenPromise 原型上的一个方法, 返回一个 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 执行器中,不管最终拿到什么结果,建议都使用 resolvereject 去接收,以便后续的链式调用,这是一种好习惯:

// 直接使用 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.thenawait 只是获取 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 即可。

参考资料

  • 使用 Promise | MDN
  • Promise 链
  • 使用 promise 进行错误处理
  • JS 异步错误捕获两三事