MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript中的异步编程与类型处理

2023-01-144.7k 阅读

异步编程基础概念

在前端开发中,异步编程是处理那些不希望阻塞主线程执行的任务的关键技术。JavaScript 是单线程语言,这意味着它在同一时间只能执行一个任务。如果有一个长时间运行的任务,比如网络请求或者文件读取,直接在主线程中执行会导致页面卡顿,用户体验变差。异步编程允许这些任务在后台执行,主线程继续处理其他任务,当异步任务完成时,通过特定的机制通知主线程进行相应的处理。

在 JavaScript 中,常见的异步编程方式有回调函数、Promise 和 async/await。TypeScript 作为 JavaScript 的超集,完全支持这些异步编程方式,并在此基础上提供了更强大的类型系统来增强异步代码的可维护性和健壮性。

回调函数

回调函数是 JavaScript 中最基本的异步编程方式。当一个异步操作完成时,会调用传入的回调函数来处理结果。例如,使用 setTimeout 模拟一个异步任务:

function printAfterDelay(message: string, delay: number) {
    setTimeout(() => {
        console.log(message);
    }, delay);
}

printAfterDelay('Hello, async world!', 2000);

在这个例子中,setTimeout 接收一个回调函数和一个延迟时间。2 秒后,回调函数会被执行,打印出消息。然而,回调函数存在回调地狱(Callback Hell)的问题,当多个异步操作嵌套时,代码会变得难以阅读和维护。例如:

getData((data1) => {
    processData1(data1, (data2) => {
        processData2(data2, (data3) => {
            processData3(data3, (finalResult) => {
                console.log(finalResult);
            });
        });
    });
});

这种层层嵌套的代码结构使得代码的可读性和可维护性急剧下降。

Promise

Promise 是一种更优雅的异步编程解决方案,它代表一个异步操作的最终完成(或失败)及其结果值。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。一旦 Promise 的状态从 pending 变为 fulfilledrejected,就不会再改变。

创建和使用 Promise

在 TypeScript 中,可以使用 Promise 构造函数创建一个 Promise 对象。例如,模拟一个异步的网络请求:

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        // 模拟异步操作,例如网络请求
        setTimeout(() => {
            const success = true;
            if (success) {
                resolve('Data fetched successfully');
            } else {
                reject('Failed to fetch data');
            }
        }, 1000);
    });
}

fetchData()
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,fetchData 函数返回一个 Promise。Promise 构造函数接收一个执行器函数,该函数有两个参数 resolvereject。如果异步操作成功,调用 resolve 并传入结果;如果失败,调用 reject 并传入错误信息。then 方法用于处理 Promise 成功的情况,catch 方法用于捕获 Promise 失败的错误。

Promise 链式调用

Promise 的一个强大功能是链式调用,可以将多个异步操作连接起来,避免回调地狱。例如:

function step1(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Step 1 completed');
        }, 1000);
    });
}

function step2(result1: string): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            const newResult = result1 + ', and Step 2 completed';
            resolve(newResult);
        }, 1000);
    });
}

function step3(result2: string): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            const finalResult = result2 + ', and Step 3 completed';
            resolve(finalResult);
        }, 1000);
    });
}

step1()
   .then(step2)
   .then(step3)
   .then((finalResult) => {
        console.log(finalResult);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,step1step2step3 都是返回 Promise 的函数。通过 .then 方法将它们链式调用起来,每个 then 方法接收上一个 Promise 成功的结果作为参数,并返回一个新的 Promise。这样就可以清晰地处理多个异步操作的顺序执行,而不会出现回调地狱的问题。

Promise 组合

除了链式调用,Promise 还提供了一些静态方法来组合多个 Promise。例如,Promise.all 方法可以将多个 Promise 包装成一个新的 Promise,只有当所有传入的 Promise 都成功时,新的 Promise 才会成功,并且其结果是一个包含所有成功结果的数组。如果有任何一个 Promise 失败,新的 Promise 就会失败,并返回第一个失败的 Promise 的错误信息。

function fetchData1(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data 1 fetched');
        }, 1000);
    });
}

function fetchData2(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data 2 fetched');
        }, 1500);
    });
}

Promise.all([fetchData1(), fetchData2()])
   .then((results) => {
        console.log(results); // 输出: ['Data 1 fetched', 'Data 2 fetched']
    })
   .catch((error) => {
        console.error(error);
    });

Promise.race 方法则是返回一个新的 Promise,只要传入的 Promise 中有一个成功或失败,新的 Promise 就会以相同的状态完成,并返回第一个完成的 Promise 的结果或错误。

function fetchData3(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data 3 fetched');
        }, 2000);
    });
}

function fetchData4(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data 4 fetched');
        }, 1000);
    });
}

Promise.race([fetchData3(), fetchData4()])
   .then((result) => {
        console.log(result); // 输出: 'Data 4 fetched'
    })
   .catch((error) => {
        console.error(error);
    });

async/await

async/await 是在 Promise 的基础上进一步简化异步代码的语法糖。async 函数总是返回一个 Promise,await 只能在 async 函数内部使用,它用于暂停 async 函数的执行,等待一个 Promise 完成,并返回该 Promise 的 resolved 值。

async 函数基础

async function asyncFunction(): Promise<string> {
    return 'Hello from async function';
}

asyncFunction()
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,asyncFunction 是一个 async 函数,它返回一个 Promise。即使没有显式地使用 Promise 构造函数,async 函数会自动将返回值包装成一个已 resolved 的 Promise。

await 的使用

function fetchData(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data fetched');
        }, 1000);
    });
}

async function processData() {
    try {
        const result = await fetchData();
        console.log(result); // 输出: 'Data fetched'
    } catch (error) {
        console.error(error);
    }
}

processData();

processData 函数中,await fetchData() 暂停了函数的执行,直到 fetchData 返回的 Promise 被 resolved 或 rejected。如果 Promise 被 resolved,await 表达式的值就是 Promise 的 resolved 值;如果 Promise 被 rejected,await 会抛出错误,在 try...catch 块中可以捕获到这个错误。

处理多个异步操作

使用 async/await 可以更直观地处理多个异步操作,无论是顺序执行还是并行执行。例如,顺序执行多个异步操作:

async function step1(): Promise<string> {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return 'Step 1 completed';
}

async function step2(result1: string): Promise<string> {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return result1 + ', and Step 2 completed';
}

async function step3(result2: string): Promise<string> {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return result2 + ', and Step 3 completed';
}

async function main() {
    const result1 = await step1();
    const result2 = await step2(result1);
    const finalResult = await step3(result2);
    console.log(finalResult);
}

main();

对于并行执行多个异步操作,可以结合 Promise.all 使用 async/await:

async function fetchData1(): Promise<string> {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return 'Data 1 fetched';
}

async function fetchData2(): Promise<string> {
    await new Promise((resolve) => setTimeout(resolve, 1500));
    return 'Data 2 fetched';
}

async function mainParallel() {
    const [result1, result2] = await Promise.all([fetchData1(), fetchData2()]);
    console.log(result1, result2);
}

mainParallel();

异步编程中的类型处理

在 TypeScript 中,异步编程的类型处理非常重要,它可以帮助我们在开发过程中尽早发现错误,提高代码的可靠性。

Promise 的类型定义

当创建一个 Promise 时,可以明确指定其 resolved 值和 rejected 原因的类型。例如:

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = true;
            if (success) {
                resolve('Data fetched successfully');
            } else {
                reject(new Error('Failed to fetch data'));
            }
        }, 1000);
    });
}

fetchData()
   .then((result: string) => {
        console.log(result);
    })
   .catch((error: Error) => {
        console.error(error.message);
    });

在这个例子中,fetchData 函数返回的 Promise 的 resolved 值类型为 string,rejected 原因类型为 Error。在 thencatch 方法中,也相应地指定了参数的类型,这样 TypeScript 编译器可以进行类型检查,确保代码的类型安全。

async 函数的返回类型

async 函数的返回类型会自动推断为 Promise,我们也可以显式地指定返回类型。例如:

async function asyncFunction(): Promise<string> {
    return 'Hello from async function';
}

如果 async 函数内部抛出错误,错误类型也会反映在 Promise 的 rejected 原因类型中。例如:

async function asyncFunctionWithError(): Promise<string> {
    throw new Error('An error occurred');
}

asyncFunctionWithError()
   .then((result) => {
        console.log(result);
    })
   .catch((error: Error) => {
        console.error(error.message);
    });

泛型在异步操作中的应用

在处理多个 Promise 的组合时,泛型可以帮助我们更精确地定义类型。例如,Promise.all 的类型定义如下:

Promise.all<T>(iterable: Iterable<Promise<T>>): Promise<T[]>;

这里的 T 是一个泛型类型参数,代表每个 Promise 的 resolved 值类型。通过泛型,Promise.all 返回的 Promise 的 resolved 值类型是一个包含所有传入 Promise 的 resolved 值的数组,类型与 T 一致。例如:

function fetchNumber(): Promise<number> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(42);
        }, 1000);
    });
}

function fetchString(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Answer');
        }, 1500);
    });
}

Promise.all([fetchNumber(), fetchString()])
   .then((results: [number, string]) => {
        console.log(results);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,Promise.all 接收一个包含 fetchNumberfetchString 返回的 Promise 的数组。由于 fetchNumber 返回的 Promise 的 resolved 值类型为 numberfetchString 返回的 Promise 的 resolved 值类型为 string,所以 Promise.all 返回的 Promise 的 resolved 值类型为 [number, string],这样可以更精确地处理结果的类型。

异步错误处理

在异步编程中,错误处理至关重要。不正确的错误处理可能导致应用程序崩溃或出现难以调试的问题。

Promise 的错误处理

在 Promise 中,可以使用 catch 方法捕获 Promise 链中任何一个 Promise 失败的错误。例如:

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('Failed to fetch data'));
        }, 1000);
    });
}

fetchData()
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error.message);
    });

如果在 then 方法中抛出错误,也可以被 catch 方法捕获。例如:

function fetchData(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data fetched');
        }, 1000);
    });
}

fetchData()
   .then((result) => {
        throw new Error('An error occurred in then');
        return result;
    })
   .catch((error) => {
        console.error(error.message);
    });

此外,还可以使用 finally 方法,无论 Promise 是 resolved 还是 rejected,finally 中的代码都会执行。例如:

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('Failed to fetch data'));
        }, 1000);
    });
}

fetchData()
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error.message);
    })
   .finally(() => {
        console.log('This will always be printed');
    });

async/await 的错误处理

在 async/await 中,通常使用 try...catch 块来捕获错误。例如:

async function processData() {
    try {
        const result = await fetchData();
        console.log(result);
    } catch (error) {
        console.error(error.message);
    }
}

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('Failed to fetch data'));
        }, 1000);
    });
}

processData();

如果 async 函数内部的任何 await 表达式抛出错误,都会被 try...catch 块捕获。这种错误处理方式比 Promise 的 catch 方法更直观,尤其是在处理多个 await 操作时。

异步编程在前端框架中的应用

在现代前端框架如 React、Vue 和 Angular 中,异步编程起着关键作用。

React 中的异步编程

在 React 中,经常需要处理异步数据获取,例如从 API 加载数据。可以使用 useEffect 钩子结合 Promise 或 async/await 来实现。例如,使用 useEffectfetch API 加载数据:

import React, { useState, useEffect } from'react';

interface User {
    name: string;
    age: number;
}

const UserComponent: React.FC = () => {
    const [user, setUser] = useState<User | null>(null);
    const [error, setError] = useState<string | null>(null);

    useEffect(() => {
        async function fetchUser() {
            try {
                const response = await fetch('/api/user');
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                const data: User = await response.json();
                setUser(data);
            } catch (error) {
                setError((error as Error).message);
            }
        }

        fetchUser();
    }, []);

    if (error) {
        return <div>{error}</div>;
    }

    if (!user) {
        return <div>Loading...</div>;
    }

    return (
        <div>
            <p>Name: {user.name}</p>
            <p>Age: {user.age}</p>
        </div>
    );
};

export default UserComponent;

在这个例子中,useEffect 钩子在组件挂载时调用 fetchUser 函数。fetchUser 是一个 async 函数,它使用 fetch API 加载用户数据,并处理可能的错误。通过 useState 钩子来管理加载状态、用户数据和错误信息。

Vue 中的异步编程

在 Vue 中,可以在 created 生命周期钩子或 methods 中使用异步操作。例如,使用 axios 库进行数据请求:

<template>
    <div>
        <div v-if="error">{{ error }}</div>
        <div v-if="loading">Loading...</div>
        <div v-if="user">
            <p>Name: {{ user.name }}</p>
            <p>Age: {{ user.age }}</p>
        </div>
    </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';

interface User {
    name: string;
    age: number;
}

export default defineComponent({
    data() {
        return {
            user: null as User | null,
            error: null as string | null,
            loading: false
        };
    },
    created() {
        this.fetchUser();
    },
    methods: {
        async fetchUser() {
            this.loading = true;
            try {
                const response = await axios.get('/api/user');
                this.user = response.data;
            } catch (error) {
                this.error = (error as Error).message;
            } finally {
                this.loading = false;
            }
        }
    }
});
</script>

在这个 Vue 组件中,created 钩子在组件创建时调用 fetchUser 方法。fetchUser 是一个 async 函数,使用 axios 进行数据请求,并处理错误和加载状态。

Angular 中的异步编程

在 Angular 中,使用 Observableasync 管道来处理异步操作。例如,使用 HttpClient 服务进行数据请求:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

interface User {
    name: string;
    age: number;
}

@Component({
    selector: 'app-user',
    templateUrl: './user.component.html',
    styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
    user$: Observable<User> | null = null;
    error: string | null = null;

    constructor(private http: HttpClient) {}

    ngOnInit() {
        this.user$ = this.http.get<User>('/api/user');
        this.user$.subscribe({
            next: (user) => {
                console.log(user);
            },
            error: (error) => {
                this.error = error.message;
            }
        });
    }
}

在这个 Angular 组件中,ngOnInit 生命周期钩子使用 HttpClientget 方法返回一个 Observable。通过 subscribe 方法处理 Observablenexterror 事件,分别处理成功获取的数据和错误。在模板中,可以使用 async 管道来订阅 Observable 并显示数据。

异步编程的性能优化

在异步编程中,性能优化也是一个重要的方面。

减少不必要的异步操作

有时候,一些操作并不需要异步执行,可以将其改为同步操作以提高性能。例如,简单的计算操作不需要使用异步,直接在主线程中执行即可。

function calculateSum(a: number, b: number): number {
    return a + b;
}

// 避免不必要的异步操作
// async function calculateSumAsync(a: number, b: number): Promise<number> {
//     return new Promise((resolve) => {
//         setTimeout(() => {
//             resolve(a + b);
//         }, 0);
//     });
// }

控制并发请求数量

在进行多个网络请求时,如果同时发起过多请求,可能会导致网络拥塞和性能下降。可以使用队列或限制并发数量的方法来优化。例如,使用 Promise.racePromise.all 结合来控制并发请求数量:

function fetchData(url: string): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`Data from ${url}`);
        }, 1000);
    });
}

const urls = ['/api/data1', '/api/data2', '/api/data3', '/api/data4'];
const maxConcurrent = 2;

function fetchDataWithLimit() {
    let index = 0;
    const results: string[] = [];
    const promises: Promise<string>[] = [];

    function next() {
        if (index < urls.length) {
            const promise = fetchData(urls[index]);
            promises.push(promise);
            promise.then((result) => {
                results.push(result);
                promises.splice(promises.indexOf(promise), 1);
                next();
            });
            index++;
        } else if (promises.length === 0) {
            console.log(results);
        }
    }

    for (let i = 0; i < maxConcurrent; i++) {
        next();
    }
}

fetchDataWithLimit();

在这个例子中,通过 next 函数控制每次并发请求的数量为 maxConcurrent,当有一个请求完成时,再发起下一个请求,从而避免过多请求导致的性能问题。

合理使用缓存

对于一些频繁请求且数据变化不频繁的接口,可以使用缓存来减少网络请求次数。例如,使用 Map 来实现简单的缓存:

const cache = new Map<string, string>();

function fetchData(url: string): Promise<string> {
    if (cache.has(url)) {
        return Promise.resolve(cache.get(url)!);
    }

    return new Promise((resolve) => {
        setTimeout(() => {
            const data = `Data from ${url}`;
            cache.set(url, data);
            resolve(data);
        }, 1000);
    });
}

在这个例子中,每次请求数据时先检查缓存中是否存在,如果存在则直接返回缓存中的数据,否则发起网络请求并将结果存入缓存。

通过以上对异步编程和类型处理的深入探讨,以及在前端框架中的应用和性能优化,希望能帮助开发者更好地掌握 TypeScript 中的异步编程技术,编写更健壮、高效的前端应用程序。