阅读 1891

Promise中多个回调函数之间的数据传递

译文地址:2ality.com/2017/08/pro…
译者:bryan.yang,美团金融FE

  在基于Promise的代码中,会有很多的回调函数,每个回调函数都有自己的变量作用域。如果你想在这些回调函数之间共享数据该怎么办呢?以下这篇文章将会给出几种几种解决方法。

1.引出问题

  以下面这段Promise回调代码中常见的问题为例:在第一个Promise回调中(line A)声明了变量connection,而这个变量将会在接下来的两个回调中(line B and line C)引用。

db.open()
.then(connection => { // (A)
    return connection.select({ name: 'Jane' });
})
.then(result => {
    // 获取结果
    // 用 `connection` 进行更多的查询操作 (B)
})
···
.catch(error => {
    // 捕获错误
})
.finally(() => {
    connection.close(); // (C)
});复制代码

  在代码中我们使用了Promise.prototype.finally(),它是ES新添加的一个特性,工作原理与try语句块中的finally类似。

2.有副作用的解决方式

  第一种解决方式就是直接将共享的数据存储在外层的作用域的变量中(line A),然后在每个回调中使用。

let connection; // (A)
db.open()
.then(conn => {
    connection = conn;
    return connection.select({ name: 'Jane' });
})
.then(result => {
    // 获取结果
    // 使用 `connection` 执行更多的查询操作 (B)
})
···
.catch(error => {
    // 捕获错误
})
.finally(() => {
    connection.close(); // (C)
});复制代码

  不难看出,由于connection变量位于外层域中,所以在B与C的回调中都能够使用这个变量。

3.嵌套作用域的方式

  在介绍这个方式之前我们先来看一下以同步的方式实现的代码:

try {
    const connection = await db.open();
    const result = await connection.select({ name: 'Jane' });
    ···
} catch (error) {
    // 捕获错误
} finally {
    connection.close();
}复制代码

  在同步的方式中,我们要想共用connection这个数据,通常也是在外层作用域中声明一个变量来存储,代码如下:

const connection = await db.open();// (A)
try {
    const result = await connection.select({ name: 'Jane' });
    ···
} catch (error) {
    // 捕获错误
} finally {
    connection.close();        
}复制代码

  其实我们可以使用Promise实现类似的效果 ---- 嵌套Promise,代码如下:

db.open() // (A)
.then(connection => { // (B)
    return connection.select({ name: 'Jane' }) // (C)
    .then(result => {
        // 获取结果
        // 使用 `connection` 执行更多查询操作
    })
    ···
    .catch(error => {
        // 捕获错误
    })
    .finally(() => {
        connection.close();
    });    
})复制代码

在上面这段代码中有两个Promise:

  • 第一个Promise从第A行开始。变量connection是函数open()执行的异步返回结果。
  • 第二个Promise被嵌套在then方法的回调中(line B)。从第C行开始。 注意在位置C中的return语句, 它确保了这两个Promise能够正确地一起使用。
    通过嵌套,第二个Promise中的所有回调都能够引用connection变量。

  可能你已经注意到,在同步和嵌套Promise的实现方式中,db.open()这个同步方法抛出的错误并不会被捕获。这篇文章A follow-up blog post on Promise.try()将会告诉你如何来完善这段异步代码。在同步的代码中你可以将db.open()使用try语句来捕获相关异常。

4.多值返回

  下面的这些代码将会演示多个回调之间传递数据的另一种方法。当然,这些代码并不总是可用的。实际上,你并不能用这段代码来连接你正在运行的数据库。我们先来看一个能够运行的栗子。
  正如我们正在解决的问题一样:在这段代码中,位于位置A的Promise中的intermediate变量需要在位置B中引用。

return asyncFunc1()
.then(result1 => { // (A)
    const intermediate = ···;
    return asyncFunc2();
})
.then(result2 => { // (B)
    console.log(intermediate);
    ···
});复制代码

  我们可以通过在第一个回调中使用Promise.all()传递多个值给第二个回调的方式来解决这个问题:

return asyncFunc1()
.then(result1 => {
    const intermediate = ···;
    return Promise.all([asyncFunc2(), intermediate]); // (A)
})
.then(([result2, intermediate]) => {
    console.log(intermediate);
    ···
})复制代码

  需要注意的是在第一个回调中直接返回一个数组并不能生效,因为如果直接返回一个数组,then()回调将会接收到一个由Promise和一个普通值组成的数组,这并不是我们想要的结果。这里使用Promise.all(),它将会使用Promise.resolve()方法将数组中的每一个元素转换成Promise,然后执行转换之后的Promise,并将结果放到一个普通数组中(如果每个Promise执行成功)。这个方法也有是局限的,你不能把你想要共享的数据传递到catch和finally语句块中。
  最后这种方法将会从Promise.all()支持Object参数的版本受益。然后你将可以标记每一个返回值了。