[译]<<Effective TypeScript>> 技巧25 使用 async 函数处理异步代码而不是回调函数

453 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情

本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.

技巧25: 使用 async 函数处理异步代码而不是回调函数

js中, 采用回调函数处理异步代码,会造成'末日金字塔', 也称'回调地狱':

fetchURL(url1, function(response1) {
  fetchURL(url2, function(response2) {
    fetchURL(url3, function(response3) {
    // ...
      console.log(1);
    });
    console.log(2);
  });
  console.log(3);
});
console.log(4);

// Logs:
// 4
// 3
// 2
// 1

正如我们所看到的, log的顺序和代码顺序是反着的.阅读该代码也很困难. 另外如果你想调用平行请求, 或者说是遇到错误提前退出都非常困难.

es2015 引入了Promise来打破末日金字塔, Promise代表将来可以使用的东西(有的代码将其命名为future). 这是一段例子:

const page1Promise = fetch(url1);
page1Promise.then(response1 => {
  return fetch(url2);
}).then(response2 => {
  return fetch(url3);
}).then(response3 => {
  // ...
}).catch(error => {
  // ...
});

这有几点好处:

  1. 代码执行顺序, 和代码顺序一致.
  2. 少了很多嵌套结构, 更容易阅读
  3. 更容易统一做错误处理
  4. 能使用更高级别的工具:Promise.all, Promise.race

es2017 加入了async和await, 处理异步代码更简单:

async function fetchPages() {
  const response1 = await fetch(url1);
  const response2 = await fetch(url2);
  const response3 = await fetch(url3);
  // ...
}

await 会让fetch中每个函数执行完,也就是每个Promise, resolve完. 同时也能利用try, catch机制捕捉.

async function fetchPages() {
  try {
    const response1 = await fetch(url1);
    const response2 = await fetch(url2);
    const response3 = await fetch(url3);
    // ...
  } catch (e) {
    // ...
  }
}

ts对async和await的支持也很完善. 另外还有两个使用Promise和async/await的优点:

  • Promise组合回调函数非常容易
  • Types 在Promise中, 比回调函数更容易通过

如果你想并行获取 pages, 你可以使用Promise.all 对 Promise组合使用:

async function fetchPages() {
  const [response1, response2, response3] = await Promise.all([
    fetch(url1), fetch(url2), fetch(url3)
  ]);
  // ...
}

使用 带有await 的结构赋值函数非常棒. ts能够推测每个 response的类型.但是如果你用回调函数实现会变得非常复杂:

unction fetchPagesCB() {
  let numDone = 0;
  const responses: string[] = [];
  const done = () => {
    const [response1, response2, response3] = responses;
    // ...
  };
  const urls = [url1, url2, url3];
  urls.forEach((url, i) => {
    fetchURL(url, r => {
      responses[i] = url;
      numDone++;
      if (numDone === urls.length) done();
    });
  });
}

同时错误处理也非常复杂.

类型推断在Promise.race(用于返回第一个resolve的Promise)也非常友好.例如:

function timeout(millis: number): Promise<never> {
return new Promise((resolve, reject) => {
     setTimeout(() => reject('timeout'), millis);
  });
}

async function fetchWithTimeout(url: string, ms: number) {
  return Promise.race([fetch(url), timeout(ms)]);
}

fetchWithTimeout 返回值的类型被ts推断为:Promise<Response>. 但是race组合的是联合体类型Promise<Response | never>. 但是ts能够推断到, 首先resolve的前者, 返回值简化为Promise. 所以要尽量的使用Promise.

有时后我们需要使用原始的Promise, 我们在返回SetTimeout函数的时候, 需要将其用Promis进行包装一下. 但是如果你有选择,你应该尽量选择使用 async/await:

  1. 因为async/await让代码更简洁, 更直接.
  2. async强迫函数始终返回Promise

注意第二点, 就算没有await, async强迫函数始终返回Promise:

// function getNumber(): Promise<number>
async function getNumber() {
return 42;
}

同样的:

const getNumber = async () => 42;  // Type is () => Promise<number>

等价于:

const getNumber = () => Promise.resolve(42);  // Type is () => Promise<number>

async始终让函数返回Promise会显得有些奇怪. 但是有一条编程原则: 函数应该始终坚持同步运行, 或者始终异步运行, 不能将同步和异步混合. 例如你想给fetchURL添加一个缓存:

// Don't do this!
const _cache: {[url: string]: string} = {};
function fetchWithCache(url: string, callback: (text: string) => void) {
  if (url in _cache) {
    callback(_cache[url]);
  } else {
    fetchURL(url, text => {
      _cache[url] = text;
      callback(text);
    });
  }
}

这看起来似乎是一个优化, 但是使用起来非常麻烦:

let requestStatus: 'loading' | 'success' | 'error';
function getUser(userId: string) {
  fetchWithCache(`/user/${userId}`, profile => {
requestStatus = 'success';
  });
  requestStatus = 'loading';
}

调用getUser函数后, requestStatus的值到底是啥? 这取决于 url有没有被缓存: 如果缓存,requestStatus就是'loading', 如果没有就是: 'success'.

使用async会让过程让人容易理解:

const _cache: {[url: string]: string} = {};
async function fetchWithCache(url: string) {
  if (url in _cache) {
    return _cache[url];
  }
  const response = await fetch(url);
  const text = await response.text();
  _cache[url] = text;
  return text;
}

let requestStatus: 'loading' | 'success' | 'error';
async function getUser(userId: string) {
  requestStatus = 'loading';
  const profile = await fetchWithCache(`/user/${userId}`);
  requestStatus = 'success';
}

结果显然是 'success'. 另外值得注意的是, async不会对Promise进行再次包装:

// Function getJSON(url: string): Promise<any>
async function getJSON(url: string) {
  const response = await fetch(url);
  const jsonPromise = response.json();  // Type is Promise<any>
  return jsonPromise;
}