124

从项目谈起,为何要用async / await 替代Promise? · Issue #16 · xingbofeng/xingbof...

 6 years ago
source link: https://github.com/xingbofeng/xingbofeng.github.io/issues/16
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

之前阅读过一篇文章《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语句。
kiwenlau reacted with thumbs up emoji All reactions

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK