第6章 副作用管理与异步流程:类型安全与并发控制
6.1 useEffect的语义与执行时机深度解析
useEffect是React中处理副作用的核心Hook,深入理解其语义和执行时机对于编写正确的React代码至关重要。
6.1.1 依赖项对比的浅层检查机制
useEffect的依赖项检查使用浅层比较(Shallow Comparison),这决定了其使用方式和注意事项。
Object.is与深度比较的差异
TEXT1依赖项比较机制: 2 3React使用Object.is进行依赖项比较: 4- 基本类型:值相等 5- 对象类型:引用相等 6- 数组类型:引用相等 7 8Object.is行为: 9Object.is(1, 1) // true 10Object.is('a', 'a') // true 11Object.is({}, {}) // false(不同引用) 12Object.is([], []) // false(不同引用) 13Object.is(NaN, NaN) // true 14Object.is(+0, -0) // false
TYPESCRIPT1// 依赖数组中的对象引用陷阱 2function Component() { 3 const [user, setUser] = useState({ id: 1, name: 'John' }); 4 5 // 问题:每次渲染都创建新对象 6 useEffect(() => { 7 console.log('User changed:', user); 8 }, [{ id: user.id, name: user.name }]); // 每次渲染都是新对象! 9 10 return <div />; 11} 12 13// 解决方案1:使用原始值 14function ComponentFixed1() { 15 const [user, setUser] = useState({ id: 1, name: 'John' }); 16 17 useEffect(() => { 18 console.log('User changed:', user); 19 }, [user.id, user.name]); // 使用原始值 20 21 return <div />; 22} 23 24// 解决方案2:使用useMemo缓存对象 25function ComponentFixed2() { 26 const [user, setUser] = useState({ id: 1, name: 'John' }); 27 28 const userKey = useMemo( 29 () => ({ id: user.id, name: user.name }), 30 [user.id, user.name] 31 ); 32 33 useEffect(() => { 34 console.log('User changed:', user); 35 }, [userKey]); 36 37 return <div />; 38} 39 40// 解决方案3:使用JSON.stringify(不推荐用于大对象) 41function ComponentFixed3() { 42 const [user, setUser] = useState({ id: 1, name: 'John' }); 43 44 useEffect(() => { 45 console.log('User changed:', user); 46 }, [JSON.stringify(user)]); // 字符串比较 47 48 return <div />; 49}
不可变数据的重要性
TYPESCRIPT1// 可变数据的陷阱 2function Component() { 3 const [items, setItems] = useState([1, 2, 3]); 4 5 const addItem = () => { 6 // 错误:直接修改数组 7 items.push(4); 8 setItems(items); // 引用没变,不会触发重渲染 9 }; 10 11 return <button onClick={addItem}>Add</button>; 12} 13 14// 正确的不可变更新 15function ComponentFixed() { 16 const [items, setItems] = useState([1, 2, 3]); 17 18 const addItem = () => { 19 // 创建新数组 20 setItems([...items, 4]); 21 }; 22 23 const updateItem = (index: number, value: number) => { 24 // 创建新数组并更新指定位置 25 setItems(items.map((item, i) => (i === index ? value : item))); 26 }; 27 28 const removeItem = (index: number) => { 29 // 创建新数组并移除指定位置 30 setItems(items.filter((_, i) => i !== index)); 31 }; 32 33 return <button onClick={addItem}>Add</button>; 34} 35 36// 使用Immer简化不可变更新 37import produce from 'immer'; 38 39function ComponentWithImmer() { 40 const [items, setItems] = useState([1, 2, 3]); 41 42 const addItem = () => { 43 setItems(produce(items, draft => { 44 draft.push(4); // 看起来是修改,实际是创建新数组 45 })); 46 }; 47 48 return <button onClick={addItem}>Add</button>; 49}
6.1.2 清理函数(Cleanup)的资源释放模式
useEffect可以返回一个清理函数,用于释放资源、取消订阅等。
订阅取消、竞态取消(Race Condition)与内存泄漏预防
TYPESCRIPT1// 场景1:事件订阅 2function EventListener() { 3 useEffect(() => { 4 const handleResize = () => { 5 console.log('Window resized'); 6 }; 7 8 window.addEventListener('resize', handleResize); 9 10 // 清理函数:取消订阅 11 return () => { 12 window.removeEventListener('resize', handleResize); 13 }; 14 }, []); 15 16 return <div />; 17} 18 19// 场景2:定时器 20function Timer() { 21 const [count, setCount] = useState(0); 22 23 useEffect(() => { 24 const interval = setInterval(() => { 25 setCount(c => c + 1); 26 }, 1000); 27 28 // 清理函数:清除定时器 29 return () => { 30 clearInterval(interval); 31 }; 32 }, []); 33 34 return <div>{count}</div>; 35} 36 37// 场景3:竞态条件处理 38function SearchResults({ query }: { query: string }) { 39 const [results, setResults] = useState<string[]>([]); 40 41 useEffect(() => { 42 let cancelled = false; 43 44 const fetchResults = async () => { 45 const response = await fetch(`/api/search?q=${query}`); 46 const data = await response.json(); 47 48 // 检查是否被取消 49 if (!cancelled) { 50 setResults(data); 51 } 52 }; 53 54 fetchResults(); 55 56 // 清理函数:标记为已取消 57 return () => { 58 cancelled = true; 59 }; 60 }, [query]); 61 62 return ( 63 <ul> 64 {results.map((result, i) => ( 65 <li key={i}>{result}</li> 66 ))} 67 </ul> 68 ); 69} 70 71// 场景4:使用AbortController(更现代的竞态处理) 72function SearchResultsModern({ query }: { query: string }) { 73 const [results, setResults] = useState<string[]>([]); 74 75 useEffect(() => { 76 const controller = new AbortController(); 77 78 const fetchResults = async () => { 79 try { 80 const response = await fetch(`/api/search?q=${query}`, { 81 signal: controller.signal, 82 }); 83 const data = await response.json(); 84 setResults(data); 85 } catch (error) { 86 if (error.name !== 'AbortError') { 87 console.error('Fetch error:', error); 88 } 89 } 90 }; 91 92 fetchResults(); 93 94 // 清理函数:取消请求 95 return () => { 96 controller.abort(); 97 }; 98 }, [query]); 99 100 return ( 101 <ul> 102 {results.map((result, i) => ( 103 <li key={i}>{result}</li> 104 ))} 105 </ul> 106 ); 107}
useEffectEvent实验性API的前瞻
TYPESCRIPT1// useEffectEvent(实验性API) 2// 解决Effect依赖过多的问题 3 4import { useEffectEvent } from 'react'; // 实验性导入 5 6function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) { 7 // useEffectEvent创建的函数不依赖任何值 8 // 但可以在Effect内部访问最新的props和state 9 const onConnected = useEffectEvent((connectionId: string) => { 10 // 这里可以访问最新的theme,而不需要将其加入依赖数组 11 showNotification(`Connected to ${roomId} with ${theme} theme`, connectionId); 12 }); 13 14 useEffect(() => { 15 const connection = createConnection(roomId); 16 connection.connect(); 17 connection.onConnected = (id) => { 18 onConnected(id); // 调用EffectEvent 19 }; 20 21 return () => connection.disconnect(); 22 }, [roomId]); // 只需要roomId,不需要theme 23 24 return <div />; 25}
6.1.3 严格模式下的故意双重挂载
React的严格模式(StrictMode)会故意双重调用某些函数来帮助检测副作用。
useEffect的行为差异与组件幂等性测试
TYPESCRIPT1// 开发环境下(StrictMode开启): 2// 1. 组件挂载 3// 2. Effect执行 4// 3. 清理函数执行 5// 4. Effect再次执行 6 7// 生产环境下: 8// 1. 组件挂载 9// 2. Effect执行 10 11// 问题示例:非幂等的Effect 12function NonIdempotentComponent() { 13 useEffect(() => { 14 // 每次执行都会添加一个新监听器 15 const id = addGlobalListener(() => { 16 console.log('Event received'); 17 }); 18 19 return () => { 20 removeGlobalListener(id); 21 }; 22 }, []); 23 24 return <div />; 25} 26 27// 在StrictMode下,这个组件会添加两个监听器! 28 29// 解决方案:确保Effect幂等 30function IdempotentComponent() { 31 useEffect(() => { 32 // 使用Ref确保只执行一次 33 const hasRun = useRef(false); 34 35 if (!hasRun.current) { 36 hasRun.current = true; 37 const id = addGlobalListener(() => { 38 console.log('Event received'); 39 }); 40 41 return () => { 42 removeGlobalListener(id); 43 }; 44 } 45 }, []); 46 47 return <div />; 48} 49 50// 更好的解决方案:确保清理函数正确 51function BetterIdempotentComponent() { 52 useEffect(() => { 53 const id = addGlobalListener(() => { 54 console.log('Event received'); 55 }); 56 57 return () => { 58 removeGlobalListener(id); 59 }; 60 }, []); 61 62 return <div />; 63} 64// 这个版本在StrictMode下也是正确的: 65// 第一次Effect → 添加监听器 66// 清理函数 → 移除监听器 67// 第二次Effect → 添加监听器 68// 最终结果:只有一个监听器
开发环境与生产环境的行为差异调试
TYPESCRIPT1// 检测当前环境的工具 2const isDevelopment = process.env.NODE_ENV === 'development'; 3const isStrictMode = isDevelopment; // StrictMode只在开发环境生效 4 5// 调试技巧:添加日志观察Effect执行 6function DebugEffect() { 7 useEffect(() => { 8 console.log('Effect executed'); 9 10 return () => { 11 console.log('Cleanup executed'); 12 }; 13 }, []); 14 15 return <div />; 16} 17 18// 在StrictMode下的输出: 19// Effect executed 20// Cleanup executed 21// Effect executed 22 23// 在生产环境下的输出: 24// Effect executed
6.2 异步操作的类型安全封装与AI生成
异步操作是React应用中的常见需求,TypeScript的类型系统可以帮助我们构建类型安全的异步流程。
6.2.1 Promise状态在组件中的类型表示
使用Discriminated Union模式可以精确表示异步操作的各种状态。
Loading、Success、Error的Discriminated Union模式
TYPESCRIPT1// 异步状态类型 2type AsyncState<T> = 3 | { status: 'idle' } 4 | { status: 'loading' } 5 | { status: 'success'; data: T } 6 | { status: 'error'; error: Error }; 7 8// 使用示例 9interface User { 10 id: string; 11 name: string; 12 email: string; 13} 14 15function UserProfile({ userId }: { userId: string }) { 16 const [state, setState] = useState<AsyncState<User>>({ status: 'idle' }); 17 18 useEffect(() => { 19 setState({ status: 'loading' }); 20 21 fetchUser(userId) 22 .then(user => { 23 setState({ status: 'success', data: user }); 24 }) 25 .catch(error => { 26 setState({ status: 'error', error }); 27 }); 28 }, [userId]); 29 30 // 类型安全的渲染 31 switch (state.status) { 32 case 'idle': 33 return <div>Click to load</div>; 34 case 'loading': 35 return <Loading />; 36 case 'success': 37 return ( 38 <div> 39 <h1>{state.data.name}</h1> 40 <p>{state.data.email}</p> 41 </div> 42 ); 43 case 'error': 44 return <Error message={state.error.message} />; 45 } 46} 47 48// useAsync的类型实现 49interface UseAsyncReturn<T> { 50 state: AsyncState<T>; 51 execute: (...args: any[]) => Promise<T>; 52 reset: () => void; 53} 54 55function useAsync<T>( 56 asyncFunction: (...args: any[]) => Promise<T>, 57 immediate = false 58): UseAsyncReturn<T> { 59 const [state, setState] = useState<AsyncState<T>>({ status: 'idle' }); 60 61 const execute = useCallback( 62 async (...args: any[]) => { 63 setState({ status: 'loading' }); 64 65 try { 66 const data = await asyncFunction(...args); 67 setState({ status: 'success', data }); 68 return data; 69 } catch (error) { 70 const err = error instanceof Error ? error : new Error(String(error)); 71 setState({ status: 'error', error: err }); 72 throw err; 73 } 74 }, 75 [asyncFunction] 76 ); 77 78 const reset = useCallback(() => { 79 setState({ status: 'idle' }); 80 }, []); 81 82 useEffect(() => { 83 if (immediate) { 84 execute(); 85 } 86 }, [immediate, execute]); 87 88 return { state, execute, reset }; 89} 90 91// 使用useAsync 92function UserList() { 93 const { state, execute } = useAsync(fetchUsers, true); 94 95 switch (state.status) { 96 case 'idle': 97 case 'loading': 98 return <Loading />; 99 case 'success': 100 return ( 101 <ul> 102 {state.data.map(user => ( 103 <li key={user.id}>{user.name}</li> 104 ))} 105 </ul> 106 ); 107 case 'error': 108 return <Error message={state.error.message} />; 109 } 110}
错误边界集成
TYPESCRIPT1// 错误边界组件 2interface ErrorBoundaryState { 3 hasError: boolean; 4 error: Error | null; 5} 6 7interface ErrorBoundaryProps { 8 fallback: React.ComponentType<{ error: Error }>; 9 children: React.ReactNode; 10} 11 12class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { 13 constructor(props: ErrorBoundaryProps) { 14 super(props); 15 this.state = { hasError: false, error: null }; 16 } 17 18 static getDerivedStateFromError(error: Error): ErrorBoundaryState { 19 return { hasError: true, error }; 20 } 21 22 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 23 console.error('Error caught by boundary:', error, errorInfo); 24 // 可以在这里发送错误报告 25 } 26 27 render() { 28 if (this.state.hasError && this.state.error) { 29 const Fallback = this.props.fallback; 30 return <Fallback error={this.state.error} />; 31 } 32 33 return this.props.children; 34 } 35} 36 37// 使用错误边界 38function App() { 39 return ( 40 <ErrorBoundary fallback={ErrorFallback}> 41 <UserProfile userId="123" /> 42 </ErrorBoundary> 43 ); 44} 45 46function ErrorFallback({ error }: { error: Error }) { 47 return ( 48 <div className="error-fallback"> 49 <h2>Something went wrong</h2> 50 <p>{error.message}</p> 51 </div> 52 ); 53}
6.2.2 AbortController与请求取消的集成
AbortController是现代浏览器提供的标准API,用于取消异步操作。
Fetch API的类型封装与竞态条件消除
TYPESCRIPT1// 类型化的Fetch封装 2interface FetchOptions extends RequestInit { 3 timeout?: number; 4} 5 6async function typedFetch<T>( 7 url: string, 8 options: FetchOptions = {} 9): Promise<T> { 10 const { timeout = 30000, ...fetchOptions } = options; 11 12 const controller = new AbortController(); 13 const timeoutId = setTimeout(() => controller.abort(), timeout); 14 15 try { 16 const response = await fetch(url, { 17 ...fetchOptions, 18 signal: controller.signal, 19 }); 20 21 clearTimeout(timeoutId); 22 23 if (!response.ok) { 24 throw new Error(`HTTP error! status: ${response.status}`); 25 } 26 27 return await response.json() as T; 28 } catch (error) { 29 clearTimeout(timeoutId); 30 throw error; 31 } 32} 33 34// 在Hook中使用 35function useApi<T>( 36 url: string | null, 37 options?: FetchOptions 38) { 39 const [state, setState] = useState<AsyncState<T>>({ status: 'idle' }); 40 const abortControllerRef = useRef<AbortController | null>(null); 41 42 const execute = useCallback(async () => { 43 if (!url) return; 44 45 // 取消之前的请求 46 abortControllerRef.current?.abort(); 47 48 const controller = new AbortController(); 49 abortControllerRef.current = controller; 50 51 setState({ status: 'loading' }); 52 53 try { 54 const data = await typedFetch<T>(url, { 55 ...options, 56 signal: controller.signal, 57 }); 58 setState({ status: 'success', data }); 59 } catch (error) { 60 if (error.name !== 'AbortError') { 61 setState({ 62 status: 'error', 63 error: error instanceof Error ? error : new Error(String(error)), 64 }); 65 } 66 } 67 }, [url, options]); 68 69 useEffect(() => { 70 execute(); 71 72 return () => { 73 abortControllerRef.current?.abort(); 74 }; 75 }, [execute]); 76 77 return { state, refetch: execute }; 78}
_cleanup函数的类型约束与Async Iterator模式
TYPESCRIPT1// Async Iterator模式 2async function* fetchPaginated<T>( 3 baseUrl: string 4): AsyncGenerator<T[], void, unknown> { 5 let page = 1; 6 let hasMore = true; 7 8 while (hasMore) { 9 const response = await fetch(`${baseUrl}?page=${page}`); 10 const data = await response.json(); 11 12 if (data.items.length === 0) { 13 hasMore = false; 14 } else { 15 yield data.items; 16 page++; 17 } 18 } 19} 20 21// 在React中使用Async Iterator 22function PaginatedList({ url }: { url: string }) { 23 const [items, setItems] = useState<any[]>([]); 24 const [loading, setLoading] = useState(false); 25 const iteratorRef = useRef<AsyncGenerator<any[], void, unknown> | null>(null); 26 27 const loadMore = async () => { 28 if (!iteratorRef.current) { 29 iteratorRef.current = fetchPaginated(url); 30 } 31 32 setLoading(true); 33 const result = await iteratorRef.current.next(); 34 setLoading(false); 35 36 if (!result.done) { 37 setItems(prev => [...prev, ...result.value]); 38 } 39 }; 40 41 useEffect(() => { 42 return () => { 43 // 清理:停止迭代 44 iteratorRef.current?.return?.(); 45 }; 46 }, []); 47 48 return ( 49 <div> 50 {items.map((item, i) => ( 51 <div key={i}>{JSON.stringify(item)}</div> 52 ))} 53 <button onClick={loadMore} disabled={loading}> 54 {loading ? 'Loading...' : 'Load More'} 55 </button> 56 </div> 57 ); 58}
6.2.3 Suspense与Error Boundaries的异步边界处理
Suspense和Error Boundaries是React处理异步操作的声明式方案。
错误类型的传播与捕获
TYPESCRIPT1// 自定义错误类型 2class ApiError extends Error { 3 constructor( 4 message: string, 5 public statusCode: number, 6 public response?: Response 7 ) { 8 super(message); 9 this.name = 'ApiError'; 10 } 11} 12 13class NetworkError extends Error { 14 constructor(message: string) { 15 super(message); 16 this.name = 'NetworkError'; 17 } 18} 19 20class TimeoutError extends Error { 21 constructor(message: string) { 22 super(message); 23 this.name = 'TimeoutError'; 24 } 25} 26 27// 错误类型守卫 28function isApiError(error: Error): error is ApiError { 29 return error.name === 'ApiError'; 30} 31 32function isNetworkError(error: Error): error is NetworkError { 33 return error.name === 'NetworkError'; 34} 35 36// 错误边界中的类型处理 37interface ErrorBoundaryProps { 38 children: React.ReactNode; 39 fallback: React.ComponentType<{ error: Error; reset: () => void }>; 40} 41 42interface ErrorBoundaryState { 43 error: Error | null; 44} 45 46class TypedErrorBoundary extends React.Component< 47 ErrorBoundaryProps, 48 ErrorBoundaryState 49> { 50 state: ErrorBoundaryState = { error: null }; 51 52 static getDerivedStateFromError(error: Error): ErrorBoundaryState { 53 return { error }; 54 } 55 56 reset = () => { 57 this.setState({ error: null }); 58 }; 59 60 render() { 61 if (this.state.error) { 62 const Fallback = this.props.fallback; 63 return <Fallback error={this.state.error} reset={this.reset} />; 64 } 65 66 return this.props.children; 67 } 68} 69 70// 类型化的错误回退组件 71function ErrorFallback({ 72 error, 73 reset, 74}: { 75 error: Error; 76 reset: () => void; 77}) { 78 let message = 'An unexpected error occurred'; 79 let action: React.ReactNode = ( 80 <button onClick={reset}>Try again</button> 81 ); 82 83 if (isApiError(error)) { 84 if (error.statusCode === 404) { 85 message = 'The requested resource was not found'; 86 } else if (error.statusCode === 401) { 87 message = 'Please log in to continue'; 88 action = <a href="/login">Go to login</a>; 89 } else { 90 message = `Server error: ${error.message}`; 91 } 92 } else if (isNetworkError(error)) { 93 message = 'Network connection failed. Please check your internet connection.'; 94 } 95 96 return ( 97 <div className="error-fallback"> 98 <h2>Error</h2> 99 <p>{message}</p> 100 {action} 101 </div> 102 ); 103}
ReactErrorInfo的类型定义与错误上报
TYPESCRIPT1// React ErrorInfo类型 2interface ErrorInfo { 3 componentStack: string; 4} 5 6// 错误上报服务 7interface ErrorReport { 8 error: { 9 name: string; 10 message: string; 11 stack?: string; 12 }; 13 componentStack: string; 14 timestamp: number; 15 url: string; 16 userAgent: string; 17} 18 19class ErrorReportingService { 20 static report(error: Error, errorInfo: ErrorInfo) { 21 const report: ErrorReport = { 22 error: { 23 name: error.name, 24 message: error.message, 25 stack: error.stack, 26 }, 27 componentStack: errorInfo.componentStack, 28 timestamp: Date.now(), 29 url: window.location.href, 30 userAgent: navigator.userAgent, 31 }; 32 33 // 发送到错误上报服务 34 fetch('/api/errors', { 35 method: 'POST', 36 headers: { 'Content-Type': 'application/json' }, 37 body: JSON.stringify(report), 38 }).catch(console.error); 39 } 40} 41 42// 在错误边界中使用 43class ReportingErrorBoundary extends React.Component< 44 { children: React.ReactNode; fallback: React.ComponentType<{ error: Error }> }, 45 { hasError: boolean; error: Error | null } 46> { 47 state = { hasError: false, error: null }; 48 49 static getDerivedStateFromError(error: Error) { 50 return { hasError: true, error }; 51 } 52 53 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 54 ErrorReportingService.report(error, errorInfo); 55 } 56 57 render() { 58 if (this.state.hasError && this.state.error) { 59 const Fallback = this.props.fallback; 60 return <Fallback error={this.state.error} />; 61 } 62 return this.props.children; 63 } 64}
本章深入探讨了React副作用管理和异步流程的类型安全实现。从useEffect的依赖项检查机制,到清理函数的资源释放模式,再到异步操作的类型安全封装,我们建立了完整的副作用管理知识体系。同时,我们探讨了Suspense、Error Boundaries和AbortController等现代异步处理方案。
副作用管理是React应用开发的核心技能,掌握这些知识对于编写健壮、可维护的React应用至关重要。在下一部分中,我们将深入探讨Fiber架构和并发渲染,建立对React内核的深层理解。