不要滥用async await

1,035 阅读3分钟

JavaScript中的异步方式

首先我们聊下JavaScript的异步处理的几种方式

  1. Callback - 回调函数
  2. Events - 事件监听
  3. Pub/Sub - 订阅发布
  4. Promise

本文不去细聊这些东西的API和使用,有需要的同学可以参考MDN或者阮一峰大佬的ES6系列书

Callback

回调函数是在JavaScript代码中比较常见的形式,比如jQuery时代,我们去写获取接口内容的代码

$.ajax({url:"/api/cars/list",success:function(result){
        $("#div1").html(result);
    }});

很多小伙伴,包括我身边的一些同行,一提到Callback的时候就直接喷,说怎么怎么辣鸡。其实Callback本身没什么问题,问题就在于如果出现嵌套了,也就是我们常说的回调地狱。

$.ajax({url:"/api/cars/list",success:function(result){
        $("#div1").html(result);
        result.map(result,function(item){
              $.ajax({url:`/api/car/${item.id}`,success:function(result){
              $("#div2").html(result);
                ....
        });
    }});
}});

当然上面就是段伪代码,从jQueryajax模块功能上看请求既有成功,又会可能失败,也有可能超时,一旦出现嵌套,那么这个代码就写的及其冗长而又复杂。因为事实上的业务代码,如果功能设计不太合适,开发者自身认知问题,四五层嵌套的也不少见。

Promise

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

这个是ES6入门的一个例子,比较简单,就是实现个延时功能,100ms后,执行then中的函数,value就是"done"。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

因此比较常见的Promise代码结构是这样的。then中处理成功场景,catch中处理异常场景,finally处理不论成功还是异常,都要执行的代码。

那我们看看,promise如何解决回调地狱问题

fetch('/api/getUserinfo')
  .then((user)=>{
  return fetch(`/api/weather/${user.location}`)
}).then((weather)=>{
  ...
});

我们可以看到,在promise的加持下,我们可以将回调地狱改成链式调用的方式,使得整体代码的可读性和可维护性得到加强。强调下,demo中fetch的用法不代表其真实使用,也不包含异常处理等,仅仅为了释义。

貌似promise的引入好多了,可以链式调用也不是特别爽啊,就没有类似同步代码似的写法么?

async await

async function xxx(){
  const user=await fetch('/api/getUserinfo');
  const weather=await fetch(`/api/weather/${user.location}`);
  ...
}

哇,终于舒服了。

其实在async方案之前,还有Generator方案,在一些框架和库的实现中我们还能看到,比如dva的model层中的effect。这里重点强调下,有些会有人说asyncpromise的语法糖,这个是错误的,asyncGenerator的语法糖,对比下

  1. Generator 出现在ES2015中,async 出现在ES2017中,async 是 Generator 的语法糖;
  2. 执行方式不同,Generator 执行需要使用执行器(next()等方法);async 函数自带执行器,与普通函数的执行一样;
  3. async 的语法语义更加清楚,async 表示异步,await 表示等待;而 Generator 函数的(*)号和 yield 的语义就没那么直接了;
  4. Generator 中 yield 后面只能跟 Thunk 函数或 Promise 对象;而 async 函数中 await 后面可以是 promise 对象或者原始类型的值(会自动转为立即resovle的promise对象);
  5. 返回值不同,Generator 返回遍历器,相比于 async 返回 promise 对象操作更加麻烦。

当然还包括异常处理等,有兴趣的同学可以去仔细挖一挖。

什么样的叫滥用

知乎的博主编程爱好者的一篇文章有张图,我们引用下。大家也可以去看下原文【传送门】

当在回调地狱受苦受难的前端开发,看到async方案的时候,绝对跟看到程序员鼓励师一样,结果乱手一摸就出事了。

从这个例子的对话上看出来,不论是不是任务间存在先后顺序,都在一个等一个去执行,这样是浪费效率的。

根据实际情况我们可以分组,再使用promise.all配合

async function 上厕所(){
  await 解带()
  await 开闸();
  await 收口();
}

async function 加油(){
  await 开盖();
  await 拔枪();
  await 加油();
  ...
}
  
async function 去加油站(){
  const 加油Promise=加油();
  const 上厕所Promise=上厕所();
  await 加油Promise;
  await 上厕所Promise;
  ...
}
  
// 还有种方式就是Promise.all
 async function 去加油站(){
  Promise.all([加油(),上厕所()]).then(()=>{
  ...
    
  });
} 

比较随意的一个例子,可以帮助大家理解下。上厕所和加油这两件事本质上是可以并行处理的,没必要上完厕所再去加油,或者加完油再上厕所。

喜欢的话,大家可以点个关注。想跟笔者深入交流的话,可以搜下公粽号:喵爸的小作坊