思考为什么不要使用 return await

5,488 阅读4分钟

在 ESLint 中有一条规则 no-return-await。下面的代码会触发该条规则校验不通过:

async function foo() {
  return await bar();
}

这条规则的解释是,async function 的返回值总是封装在 Promise.resolvereturn await 实际上并没有做任何事情,只是在 Promise resolve 或 reject 之前增加了额外的时间。

Promise.resolve()

了解 Promise 对象的知道 Promise.resolve() 作用是将给定的参数转换为 Promise 对象

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

那么其实上面的问题所在就是 Promise.resolve() 的参数情况:

下面截取自 《ECMAScript 6 入门》的 Promise.resolve()

  • Promise 实例

    如果参数是一个 Promise 实例,Promise.resolve将不做任何修改、原封不动地返回这个实例。

  • thenable 对象

    thenable对象指的是具有then方法的对象

    let thenable = {
      then: function(resolve, reject) {
        resolve(42);
      }
    };
    

    Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法

    let thenable = {
      then: function(resolve, reject) {
        resolve(42);
      }
    };
    
    let p1 = Promise.resolve(thenable);
    p1.then(function (value) {
      console.log(value);  // 42
    });
    
  • then() 方法的对象,或基础类型值

    如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved

    const p = Promise.resolve('Hello');
    
    p.then(function (s) {
      console.log(s)
    });
    // Hello
    

    由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。

  • 无参数

    Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

    Promise.resolve()
    // Promise {<fulfilled>: undefined}
    

问题分析

函数 foo() 是一个 async function,会返回一个 Promise 对象,这个 Promise 对象其实是将函数内 return 语句后面的值使用 Promise.resolve 封装后返回。所以,执行 foo() 之后,我们明确知道得到的结果是一个 Promise 对象,并且在 foo() 的外部我们还会以某种方式比如await 来继续使用这个结果。

另外,我们知道 await 命令后面如果是一个 Promise 对象,则返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。所以函数内 return 后的 await 的作用就是获取 bar() 的执行结果。

如此一来,async function 中使用 return await 相当于先获取结果然后又将结果变成 Promise 实例。

无论 return await 后面跟的是不是 Promise 对象,那么我们在函数外面都会得到一个 Promise 对象,所以函数内 return 后面的 await 就显得多余了。

虽然上面的写法不推荐使用,但是并不会产生运行错误,只是会造成性能上的损失。唯一影响,可能是其他开发者看到后,会笑话你基础不扎实吧。

推荐写法

首先,如果 async function 没有 return 语句的话,默认返回 undefined,在 Devtool 中测试如下:

async function foo() {}
foo();
// Promise {<fulfilled>: undefined}
//  [[PromiseState]]: "fulfilled"
//  [[PromiseResult]]: undefined

既然 return await 中的 await 是多余的,那么最直接的推荐写法就是去掉 await

async function foo() {
    return bar();
}

无论 bar() 执行结果是一个 Promise 还是一个普通值,我们将它交给 foo 函数外面的程序去处理。

如果非要在 foo 函数中求得 bar() 的执行结果,那么也可以像下面这样写:

async function foo() {
    const x = await bar();
    return x;
}

但是我觉得这完全只是为了规避 ESLint 的报错而写的一种障眼法,实际它return await bar() 一样没有任何好处。

return await promiseValuereturn promiseValue 的比较

返回值隐式的传递给 Promise.resolve,并不意味着 return await promiseValue;return promiseValue; 在功能上相同。return foo;return await foo;,有一些细微的差异:

  • return foo; 不管 foo 是 promise 还是 rejects 都将会直接返回 foo
  • 相反地,如果 foo是一个 Promisereturn await foo; 将等待 foo 执行(resolve)或拒绝(reject),如果是拒绝,将会在返回前抛出异常

看下面的代码:

function bar() {
  throw new Error('报错了!');
}

async function foo() {
    try {
        return await bar();
    } catch (error) {
      console.log('知道了');
    }
}
foo();
// 知道了
// Promise {<fulfilled>: undefined}

async function foo2() {
    try {
        return bar();
    } catch (error) {
      console.log('我不知道');
    }
}
foo2();
// undefined

如果想要在调试的堆栈中得到 bar() 抛出的错误信息,那么此时应该使用 return await

参考