一起养成写作习惯!这是我参与「掘金日新计划 · 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 => {
// ...
});
这有几点好处:
- 代码执行顺序, 和代码顺序一致.
- 少了很多嵌套结构, 更容易阅读
- 更容易统一做错误处理
- 能使用更高级别的工具: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:
- 因为async/await让代码更简洁, 更直接.
- 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;
}