从项目谈起,为何要用async / await 替代Promise?

3,262 阅读5分钟
原文链接: github.com

之前阅读过一篇文章《Async/Await替代Promise的6个理由》,现在async / await语法已经处于Stage3阶段

兼容性

  • 服务端方面,在Node.js 7.6版本后,async / await语法已经被Node.js支持,如Koa2已经抛弃generator/yield语法,拥抱async / await语法。
  • 客户端方面,也可以通过Babel让我们随心所欲地使用最新的Ecmascript语法。

从业务说起

单从我个人来讲,是不太喜欢使用这样的最新语法的。一直以来写代码还是拥抱Promise为主。但自从用上了async / await语法后好像就会对这种语法产生依赖感(看起来就像是同步阻塞的代码一样,还不用像generator语法那样手动释放,或使用co那样的流程控制库)

看下面这些常见的业务场景:

  1. 假设我们在编写一个刷题的程序,这个程序是智能的(对,它懂你),你每次做完一道题并提交,服务端会根据你提交的结果来判断你有没有掌握当前知识点,如果没有掌握,服务端会再次给你推送下一道题。
  2. 假设我们在编写一个用户手动对图像进行鉴别的软件,服务端给了两个接口,接口submit用于提交上一张图像的鉴别结果,只有当接口submit提交成功,才能去请求接口next,用于请求下一张图片的链接。
const requestSubmit = () => {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: '/submit',
      success: (data) => {
        resolve(data);
      },
      error: (err) => {
        reject(err);
      },
    });
  });
};

const requestNext = () => {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: '/next',
      success: (data) => {
        resolve(data);
      },
      error: (err) => {
        reject(err);
      },
    });
  });
};

我们如果使用Promise语法:

requestSubmit()
  .then((data) => {
    // dosomething
    return requestNext();
  })
  .then((data) => {
    // dosomething
  })
  .catch((err) => {
    console.log(err);
  });

虽然使用Promisecatch只能捕获到最近一次Promise.then的错误,但是实际上现在,其实可以这么捕获每次的错误!

不要忘记Promise.then的第二个参数:

requestSubmit()
  .then((data) => {
    // dosomething
    return requestNext();
  }, (err) => {
    console.log(err);
  })
  .then((data) => {
    // dosomething
  },(err) => {
    console.log(err);
  });

现在有一个问题,服务端可能会校验用户登录态,或参数是否正确等情况。如果用户登录态失效,会在第一个ajax请求中,仍会走success回调,并不会抛出错误,此时还是得去手动修改requestSubmit函数。

此外,Promise.catch方法只会捕获最近一次错误,那这个错误究竟在哪里抛出,其实并不得而知。

如:

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error("oops");
    })
}
makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  })

但是注意:原文其实说得不全,我们这么捕获Promise的错误是可以的:

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise(), err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  })
    .then(() => callAPromise(), err => {
    console.log(err);
  })
    .then(() => callAPromise(), err => {
    console.log(err);
  })
    .then(() => callAPromise(), err => {
    console.log(err);
  })
    .then(() => {
      throw new Error("oops");
    }, err => {
    console.log(err);
  });
}
makeRequest();

如果把上述业务场景改为async / await语法:

(async function() {
  try {
    const res1 = await axios.post('/submit');
    // dosomething
  } catch (err) {
    console.log(err);
  } finally {
    console.log('go next!');
  }

  try {
    const res2 = await axios.get('/next');
    // dosomething
  } catch (err) {
    console.log(err);
  } finally {
    console.log('done!');
  }
})();

无论从异常捕获方面还是从代码可读性方面,都会感觉更胜一筹。

优势

这里其实大多数就是捡取那篇文章:

简洁

使用async / await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promiseresolve值,也不需要定义多余的data变量,还避免了嵌套代码。这些小的优点会迅速累计起来,这在之后的代码示例中会更加明显。

错误处理

async / awaittry / catch可以同时处理同步和异步错误。在下面的Promise示例中,try / catch不能处理JSON.parse的错误,因为它在Promise中。我们需要使用.catch,这样错误处理代码非常冗余。并且,在我们的实际生产代码会更加复杂。

const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // JSON.parse可能会出错
        const data = JSON.parse(result)
        console.log(data)
      })
      // 取消注释,处理异步代码的错误
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}

使用async / await的话,catch能处理JSON.parse错误:

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

条件语句

这一点在上边也说了:

下面示例中,需要获取数据,然后根据返回数据决定是直接返回,还是继续获取更多的数据。

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

这些代码看着就头痛。嵌套(6层),括号,return语句很容易让人感到迷茫,而它们只是需要将最终结果传递到最外层的Promise

上面的代码使用async / await编写可以大大地提高可读性:

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}

中间值

你很可能遇到过这样的场景,调用promise1,使用promise1返回的结果去调用promise2,然后使用两者的结果去调用promise3。你的代码很可能是这样的:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return promise2(value1)
        .then(value2 => {        
          return promise3(value1, value2)
        })
    })
}

如果promise3不需要value1,可以很简单地将promise嵌套铺平。如果你忍受不了嵌套,你可以将value 1 & 2 放进Promise.all来避免深层嵌套:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {      
      return promise3(value1, value2)
    })
}

错误栈

这里可见上边真实的业务例子。

调试

async/await能够使得代码调试更简单。2个理由使得调试Promise变得非常痛苦:

  • 不能在返回表达式的箭头函数中设置断点

  • 如果你在.then代码块中设置断点,使用Step Over快捷键,调试器不会跳到下一个.then,因为它只会跳过异步代码。使用await / async时,你不再需要那么多箭头函数,这样你就可以像调试同步代码一样跳过await语句。