【React Native娓娓道来】第11课 错误处理与调试:Error Boundary、日志与性能分析
开篇:程序总会出错
前十课,我们一路顺风顺水地写代码、做功能。但真实开发中,程序一定会出错。
网络请求失败、服务器返回了意料之外的数据、用户输入了奇怪的内容、某个组件在某些设备上崩溃……这些问题不会因为你写得认真就消失。区别只在于:有的应用出错后白屏,用户一脸懵;有的应用出错后给出友好提示,用户知道发生了什么,甚至能继续用。
这一课要解决的问题是:
怎么让应用在出错时表现得体面?怎么快速定位问题?怎么发现性能瓶颈?
我们会讲三件事:
- 错误处理:Error Boundary、try/catch、全局错误捕获;
- 日志与调试:console、React Native DevTools、React DevTools;
- 性能分析:为什么卡顿,怎么找到卡顿的地方,怎么优化。
一、错误处理的基本思路
1.1 错误的分类
在 React Native 中,错误大致分三类:
渲染错误:组件在渲染过程中抛出了异常。
// JavaScript + React Native
function UserProfile({ user }) { // 定义组件
return <Text>{user.name}</Text>; // user 是 undefined 就崩了
} // 结束组件
异步错误:网络请求、定时器、Promise 中的错误。这类错误不会自动被 React 捕获。
// JavaScript + React Native
fetch('https://api.example.com') // 发请求
.then((res) => res.json()) // 解析 JSON
.then((data) => setData(data)); // 更新状态
// 如果请求失败,错误会被吞掉或抛出未处理的 Promise rejection
事件处理错误:点击按钮、输入文字时触发的函数抛出的异常。
// JavaScript + React Native (JSX)
<Button onPress={() => { throw new Error('出错了'); }} /> // 点击时抛错
不同类型的错误,处理方式不同。
1.2 处理原则
- 渲染错误:用 Error Boundary 兜底;
- 异步错误:用 try/catch 或
.catch()处理; - 事件处理错误:用 try/catch 包裹,或让 Error Boundary 兜底;
- 全局兜底:捕获未处理的错误,记录日志,给出提示。
二、Error Boundary:渲染错误的兜底
2.1 什么是 Error Boundary
Error Boundary 是一个 React 组件,它能捕获子组件树中渲染阶段抛出的错误,并显示一个降级 UI,而不是让整个应用白屏。
注意:Error Boundary 只能捕获渲染错误。异步错误和事件处理错误它捕获不到。
2.2 类组件写法
Error Boundary 目前只能用类组件写。它需要实现 getDerivedStateFromError 或 componentDidCatch。
// JavaScript + React Native
import { Component } from 'react'; // 引入 Component
import { View, Text, Button, StyleSheet } from 'react-native'; // 引入组件
class ErrorBoundary extends Component { // 定义 Error Boundary
state = { hasError: false, error: null }; // 初始状态
static getDerivedStateFromError(error) { // 渲染错误时调用
return { hasError: true, error }; // 返回新状态,触发降级 UI
} // 结束 getDerivedStateFromError
componentDidCatch(error, info) { // 错误发生后调用
console.error('渲染错误:', error, info); // 记录日志
// 这里可以把错误上报到服务器
} // 结束 componentDidCatch
handleReset = () => { // 定义重置函数
this.setState({ hasError: false, error: null }); // 重置状态
}; // 结束 handleReset
render() { // 渲染方法
if (this.state.hasError) { // 如果有错误
return ( // 返回降级 UI
<View style={styles.container}> // 居中容器
<Text style={styles.title}>出了点问题</Text> // 标题
<Text style={styles.message}>{this.state.error?.message}</Text> // 错误信息
<Button title="重试" onPress={this.handleReset} /> // 重试按钮
</View> // 结束容器
); // 结束 return
} // 结束判断
return this.props.children; // 正常渲染子组件
} // 结束 render
} // 结束 ErrorBoundary
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, // 居中容器
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 8 }, // 标题样式
message: { fontSize: 14, color: '#888', marginBottom: 16, textAlign: 'center' }, // 错误信息样式
}); // 结束样式表
export default ErrorBoundary; // 导出组件
关键点:
getDerivedStateFromError:渲染出错时调用,返回新的 state,触发降级 UI 渲染;componentDidCatch:错误发生后调用,适合记录日志、上报错误;handleReset:让用户可以"重试",把hasError设回false。
2.3 使用 Error Boundary
// JavaScript + React Native
import ErrorBoundary from './ErrorBoundary'; // 引入 Error Boundary
export default function App() { // 定义并默认导出 App
return ( // 返回界面描述
<ErrorBoundary> // 包裹 Error Boundary
<MainApp /> // 主应用
</ErrorBoundary> // 结束包裹
); // 结束 return
} // 结束 App
MainApp 及其所有子组件的渲染错误都会被捕获。
2.4 多个 Error Boundary
不要只放一个 Error Boundary 在最外层。更细粒度的 Error Boundary 能让错误影响范围更小。
// JavaScript + React Native (JSX)
<View> // 外层容器
<ErrorBoundary> // 包裹 Header
<Header /> // 头部
</ErrorBoundary> // 结束包裹
<ErrorBoundary> // 包裹 Feed
<Feed /> // 动态流
</ErrorBoundary> // 结束包裹
<ErrorBoundary> // 包裹 Sidebar
<Sidebar /> // 侧边栏
</ErrorBoundary> // 结束包裹
</View> // 结束容器
如果 Feed 崩溃了,Header 和 Sidebar 还能正常显示。
2.5 Error Boundary 捕获不到什么
- 异步错误(网络请求、setTimeout);
- 事件处理函数中的错误;
- 服务端渲染的错误;
- Error Boundary 自身的错误。
这些需要靠 try/catch 或全局错误处理来兜底。
三、异步错误的处理
3.1 try/catch
异步函数中的错误用 try/catch:
// JavaScript + React Native
const loadData = async () => { // 定义加载函数
try { // 开始 try
setLoading(true); // 打开加载
const res = await fetch('https://api.example.com/data'); // 发请求
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 检查状态码
const json = await res.json(); // 解析 JSON
setData(json); // 更新数据
} catch (e) { // 捕获错误
setError(e.message); // 更新错误状态
} finally { // 收尾
setLoading(false); // 关闭加载
} // 结束 try
}; // 结束 loadData
这是第4课就讲过的三态模式。它依然是最常用的异步错误处理方式。
3.2 Promise 的 catch
如果不用 async/await,用 .catch():
// JavaScript + React Native
fetch('https://api.example.com/data') // 发请求
.then((res) => res.json()) // 解析 JSON
.then((data) => setData(data)) // 更新数据
.catch((e) => setError(e.message)); // 捕获错误
3.3 请求超时
fetch 本身没有超时机制。可以用 AbortController 实现:
// JavaScript + React Native
const fetchWithTimeout = async (url, timeout = 10000) => { // 定义带超时的请求
const controller = new AbortController(); // 创建控制器
const timer = setTimeout(() => controller.abort(), timeout); // 超时后取消请求
try { // 开始 try
const res = await fetch(url, { signal: controller.signal }); // 发请求
return res; // 返回响应
} catch (e) { // 捕获错误
if (e.name === 'AbortError') { // 如果是超时
throw new Error('请求超时'); // 抛出友好错误
} // 结束判断
throw e; // 抛出其他错误
} finally { // 收尾
clearTimeout(timer); // 清除定时器
} // 结束 try
}; // 结束 fetchWithTimeout
3.4 重试机制
网络请求失败时,可以自动重试:
// JavaScript + React Native
const fetchWithRetry = async (url, retries = 3) => { // 定义带重试的请求
for (let i = 0; i < retries; i++) { // 循环重试
try { // 开始 try
const res = await fetch(url); // 发请求
if (res.ok) return res; // 成功则返回
} catch (e) { // 捕获错误
if (i === retries - 1) throw e; // 最后一次失败则抛出
await new Promise((r) => setTimeout(r, 1000 * (i + 1))); // 等待后重试
} // 结束 try
} // 结束循环
}; // 结束 fetchWithRetry
每次失败后等待一段时间再重试,等待时间逐次增加,这叫"指数退避"。
四、事件处理中的错误
事件处理函数中的错误不会被 Error Boundary 捕获,需要自己用 try/catch:
// JavaScript + React Native
const handleSubmit = async () => { // 定义提交函数
try { // 开始 try
await submitForm(values); // 提交表单
} catch (e) { // 捕获错误
Alert.alert('提交失败', e.message); // 弹出提示
} // 结束 try
}; // 结束 handleSubmit
如果事件处理函数中有多个可能的错误点,可以统一包裹:
// JavaScript + React Native
const handlePress = () => { // 定义点击处理
try { // 开始 try
doSomething(); // 执行操作
} catch (e) { // 捕获错误
console.error(e); // 打印错误
Alert.alert('操作失败', '请稍后再试'); // 弹出提示
} // 结束 try
}; // 结束 handlePress
五、全局错误处理
5.1 未处理的 Promise rejection
有些异步错误没有被 catch,会成为"未处理的 Promise rejection"。可以全局监听:
// JavaScript + React Native
const originalHandler = global.ErrorUtils?.getGlobalHandler?.(); // 保存原始处理器
global.ErrorUtils?.setGlobalHandler?.((error, isFatal) => { // 设置全局处理器
console.error('全局错误:', error, isFatal); // 打印错误
// 上报错误
if (originalHandler) { // 如果存在原始处理器
originalHandler(error, isFatal); // 调用原始处理器
} // 结束判断
}); // 结束设置
ErrorUtils 是 React Native 提供的全局错误处理工具。它可以捕获 JS 层面的未处理错误。
5.2 错误上报
生产环境中,错误应该上报到服务器或第三方服务,方便排查:
// JavaScript + React Native
function reportError(error, context = {}) { // 定义错误上报函数
fetch('https://your-api.com/errors', { // 发送请求
method: 'POST', // POST 方法
headers: { 'Content-Type': 'application/json' }, // 请求头
body: JSON.stringify({ // 请求体
message: error.message, // 错误信息
stack: error.stack, // 堆栈
context, // 上下文
timestamp: new Date().toISOString(), // 时间戳
}), // 结束请求体
}).catch(() => { // 捕获上报错误
// 上报失败也不能再抛错
}); // 结束请求
} // 结束 reportError
在 Error Boundary 的 componentDidCatch 中调用:
// JavaScript + React Native
componentDidCatch(error, info) { // 错误发生后调用
reportError(error, { componentStack: info.componentStack }); // 上报错误
} // 结束 componentDidCatch
5.3 用户友好的提示
错误上报是给开发者看的。用户看到的不应该是 TypeError: Cannot read property 'name' of undefined,而应该是"加载失败,请稍后再试"。
// JavaScript + React Native
function getErrorMessage(error) { // 定义错误信息转换函数
if (error.message.includes('Network')) return '网络连接失败,请检查网络'; // 网络错误
if (error.message.includes('timeout')) return '请求超时,请稍后再试'; // 超时
if (error.message.includes('401')) return '登录已过期,请重新登录'; // 未授权
return '出了点问题,请稍后再试'; // 默认提示
} // 结束 getErrorMessage
六、日志与调试
6.1 console 基础
console.log 是最基础的调试手段:
// JavaScript + React Native
console.log('普通日志'); // 普通日志
console.warn('警告'); // 警告
console.error('错误'); // 错误
console.info('信息'); // 信息
在 Expo 中,日志会输出到 Metro 终端。
6.2 console 的进阶用法
分组:
// JavaScript + React Native
console.group('用户信息'); // 开始分组
console.log('姓名:小明'); // 组内日志
console.log('年龄:18'); // 组内日志
console.groupEnd(); // 结束分组
表格:
// JavaScript + React Native
console.table([ // 打印表格
{ name: '小明', age: 18 }, // 第一行
{ name: '小红', age: 20 }, // 第二行
]); // 结束
计时:
// JavaScript + React Native
console.time('请求耗时'); // 开始计时
await fetch('https://api.example.com'); // 执行操作
console.timeEnd('请求耗时'); // 结束计时并打印
断言:
// JavaScript + React Native
console.assert(user.age > 0, '年龄应该大于 0'); // 条件为假时打印
6.3 条件日志
只在开发环境打印日志:
// JavaScript + React Native
if (__DEV__) { // 开发模式判断
console.log('调试信息'); // 打印日志
} // 结束判断
__DEV__ 是 React Native 提供的全局变量,开发模式下为 true,生产模式下为 false。生产环境中,这段代码会被移除。
6.4 生产环境移除 console
生产环境中,console.log 会影响性能,也可能泄露敏感信息。可以在 babel.config.js 中配置移除:
// JavaScript
module.exports = { // 导出配置
presets: ['babel-preset-expo'], // 预设
env: { // 环境配置
production: { // 生产环境
plugins: ['transform-remove-console'], // 移除 console
}, // 结束生产
}, // 结束环境
}; // 结束导出
七、调试工具
7.1 React Native DevTools
React Native 现在内置了 DevTools,可以通过 Metro 终端按 j 打开。它包含:
- Console:查看日志;
- Sources:查看源码,设置断点;
- Memory:查看内存使用;
- Performance:性能分析;
- Components:查看组件树和 props。
7.2 React DevTools
React DevTools 可以查看组件树、props、state、Hook 状态。这是理解组件渲染行为最直观的工具。
在 Expo 中,可以通过 DevTools 打开:
# Bash 终端命令
npx react-devtools # 启动 React DevTools
它会显示组件树,点击任意组件就能看到它的 props、state 和 Hook 值。
7.3 使用断点调试
在 VS Code 中,可以配置断点调试:
- 安装 React Native Tools 扩展;
- 在
.vscode/launch.json中配置调试任务; - 在代码中点击行号设置断点;
- 按 F5 启动调试。
断点让你可以暂停代码执行,查看变量值,逐步执行。比 console.log 更强大。
7.4 网络调试
查看网络请求可以用:
- React Native DevTools 的 Network 面板;
- 在请求前后加日志。
// JavaScript + React Native
const apiFetch = async (url, options) => { // 定义请求函数
console.log('请求:', url, options); // 打印请求信息
const res = await fetch(url, options); // 发请求
console.log('响应:', res.status, url); // 打印响应信息
return res; // 返回响应
}; // 结束 apiFetch
7.5 元素检查器
在 Expo 开发中,摇一摇设备或在终端按 m 打开开发者菜单,选择"Show Element Inspector",然后点击任意界面元素,就能看到它的样式和层级。
八、性能分析
8.1 为什么卡顿
React Native 的卡顿通常来自三个方面:
- JS 线程繁忙:大量计算、频繁 setState、复杂渲染;
- UI 线程繁忙:大量原生视图、复杂布局、动画没走原生驱动;
- Bridge 通信频繁:JS 和原生之间大量数据传输。
新架构下 Bridge 被 JSI 取代,通信开销降低,但 JS 线程和 UI 线程的瓶颈依然存在。
8.2 React DevTools Profiler
React DevTools 的 Profiler 可以记录组件渲染时间,找出哪些组件渲染最频繁、耗时最长。
使用方法:
- 打开 React DevTools;
- 切换到 Profiler 标签;
- 点击录制,操作应用;
- 停止录制,查看火焰图。
火焰图会显示每个组件的渲染耗时。频繁渲染或耗时很长的组件就是优化目标。
8.3 避免不必要的重新渲染
React.memo:如果组件的 props 没有变化,跳过渲染。
// JavaScript + React Native
import { memo } from 'react'; // 引入 memo
const TodoItem = memo(function TodoItem({ text, done, onPress }) { // 用 memo 包裹组件
return ( // 返回界面描述
<Pressable onPress={onPress}> // 可点击容器
<Text>{done ? '✓ ' : '○ '}{text}</Text> // 显示文本
</Pressable> // 结束 Pressable
); // 结束 return
}); // 结束 TodoItem
useCallback:缓存函数引用,避免每次渲染都创建新函数。
// JavaScript + React Native
const handlePress = useCallback(() => { // 缓存函数
toggleTodo(id); // 执行操作
}, [id]); // 依赖 id
useMemo:缓存计算结果,避免重复计算。
// JavaScript + React Native
const sortedList = useMemo(() => { // 缓存计算结果
return [...list].sort((a, b) => a.name.localeCompare(b.name)); // 排序
}, [list]); // 依赖 list
8.4 FlatList 性能优化
长列表是性能问题的重灾区。FlatList 提供了多个优化属性:
// JavaScript + React Native (JSX)
<FlatList // 高效列表
data={data} // 数据源
keyExtractor={(item) => item.id} // 从每项提取唯一 key
renderItem={renderItem} // 渲染函数
initialNumToRender={10} // 初始渲染 10 项
maxToRenderPerBatch={10} // 每批渲染 10 项
windowSize={5} // 可视区域外保留 5 屏
removeClippedSubviews={true} // 移除屏幕外视图
getItemLayout={(data, index) => ({ // 提供布局信息
length: ITEM_HEIGHT, // 每项高度
offset: ITEM_HEIGHT * index, // 偏移量
index, // 索引
})} // 结束
/>
renderItem 要稳定:
// JavaScript + React Native
const renderItem = useCallback( // 缓存渲染函数
({ item }) => <TodoItem {...item} />, // 渲染每一项
[] // 无依赖
); // 结束 useCallback
如果 renderItem 每次都是新函数,FlatList 会认为它变了,导致所有项重新渲染。
8.5 图片优化
图片是另一个性能热点。
- 使用合适尺寸的图片,不要加载过大的图;
- 使用
resizeMode控制缩放方式; - 长列表中的图片用
expo-image缓存; - 避免在渲染中动态创建大量图片。
8.6 动画性能
动画要走原生驱动:
// JavaScript + React Native
Animated.timing(value, { // timing 动画
toValue: 1, // 目标值
duration: 300, // 时长
useNativeDriver: true, // 使用原生驱动
}).start(); // 启动
Reanimated 默认就在 UI 线程执行,性能更好。
8.7 启动性能
启动时间是用户体验的重要指标。优化手段:
- 减少首屏渲染的组件数量;
- 延迟加载非关键内容;
- 使用
InteractionManager推迟非紧急任务; - 减少同步的存储读取。
// JavaScript + React Native
import { InteractionManager } from 'react-native'; // 引入 InteractionManager
useEffect(() => { // 注册副作用
const task = InteractionManager.runAfterInteractions(() => { // 交互完成后执行
loadNonCriticalData(); // 加载非关键数据
}); // 结束
return () => task.cancel(); // 清理
}, []); // 空数组
8.8 内存泄漏
常见的内存泄漏:
- 组件卸载后仍在更新状态;
- 定时器没有清除;
- 事件监听没有取消。
第4课讲的清理函数就是解决这个问题的:
// JavaScript + React Native
useEffect(() => { // 注册副作用
const timer = setInterval(() => {}, 1000); // 启动定时器
const sub = Keyboard.addListener('keyboardDidShow', () => {}); // 订阅键盘事件
return () => { // 返回清理函数
clearInterval(timer); // 清除定时器
sub.remove(); // 取消订阅
}; // 结束清理
}, []); // 空数组
九、综合示例:带错误处理和性能优化的列表
把这一课的知识组合起来。
// JavaScript + React Native
import React, { useState, useEffect, useCallback, memo } from 'react'; // 引入 Hook
import { View, Text, FlatList, ActivityIndicator, Button, Pressable, StyleSheet } from 'react-native'; // 引入组件
class ErrorBoundary extends React.Component { // 定义 Error Boundary
state = { hasError: false }; // 初始状态
static getDerivedStateFromError() { // 渲染错误时调用
return { hasError: true }; // 返回新状态
} // 结束
componentDidCatch(error, info) { // 错误后调用
console.error('渲染错误:', error, info.componentStack); // 打印错误
} // 结束
render() { // 渲染方法
if (this.state.hasError) { // 如果有错误
return ( // 返回降级 UI
<View style={styles.center}> // 居中容器
<Text>页面出错了</Text> // 提示
<Button title="重试" onPress={() => this.setState({ hasError: false })} /> // 重试
</View> // 结束容器
); // 结束 return
} // 结束判断
return this.props.children; // 正常渲染子组件
} // 结束 render
} // 结束 ErrorBoundary
const PostItem = memo(function PostItem({ title, onPress }) { // 用 memo 包裹组件
return ( // 返回界面描述
<Pressable style={styles.item} onPress={onPress}> // 可点击容器
<Text style={styles.title}>{title}</Text> // 显示标题
</Pressable> // 结束 Pressable
); // 结束 return
}); // 结束 PostItem
function PostList() { // 定义列表组件
const [posts, setPosts] = useState([]); // 文章列表状态
const [loading, setLoading] = useState(true); // 加载状态
const [error, setError] = useState(null); // 错误状态
const load = useCallback(async () => { // 缓存加载函数
try { // 开始 try
setLoading(true); // 打开加载
setError(null); // 清空错误
const res = await fetch('https://jsonplaceholder.typicode.com/posts'); // 发请求
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 检查状态码
const json = await res.json(); // 解析 JSON
setPosts(json); // 更新数据
} catch (e) { // 捕获错误
setError(e.message); // 更新错误
} finally { // 收尾
setLoading(false); // 关闭加载
} // 结束 try
}, []); // 无依赖
useEffect(() => { // 注册副作用
load(); // 首次加载
}, [load]); // 依赖 load
const renderItem = useCallback( // 缓存渲染函数
({ item }) => ( // 渲染每一项
<PostItem // 渲染 PostItem
title={item.title} // 传入标题
onPress={() => console.log('点击了', item.id)} // 传入点击回调
/>
), // 结束
[] // 无依赖
); // 结束 useCallback
if (loading) { // 加载中
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<ActivityIndicator size="large" /> // 加载指示器
</View> // 结束容器
); // 结束 return
} // 结束判断
if (error) { // 出错
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<Text>加载失败:{error}</Text> // 错误信息
<Button title="重试" onPress={load} /> // 重试按钮
</View> // 结束容器
); // 结束 return
} // 结束判断
return ( // 成功
<FlatList // 高效列表
data={posts} // 数据源
keyExtractor={(item) => item.id.toString()} // 从每项提取唯一 key
renderItem={renderItem} // 渲染函数
initialNumToRender={10} // 初始渲染 10 项
maxToRenderPerBatch={10} // 每批渲染 10 项
windowSize={5} // 窗口大小
removeClippedSubviews // 移除屏幕外视图
ItemSeparatorComponent={() => <View style={styles.separator} />} // 分隔线
/>
); // 结束 return
} // 结束 PostList
export default function App() { // 定义并默认导出 App
return ( // 返回界面描述
<ErrorBoundary> // 包裹 Error Boundary
<PostList /> // 列表组件
</ErrorBoundary> // 结束包裹
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, // 居中容器
item: { padding: 16 }, // 列表项样式
title: { fontSize: 16, fontWeight: 'bold' }, // 标题样式
separator: { height: 1, backgroundColor: '#eee' }, // 分隔线样式
}); // 结束样式表
这个例子综合运用了:Error Boundary 捕获渲染错误;try/catch 处理异步错误;三态处理请求;memo 优化列表项;useCallback 稳定 renderItem 和 load;FlatList 性能优化属性。
十、错误处理与"视图 = 函数(状态)"
错误处理也可以纳入"视图 = 函数(状态)"的框架:
// 概念表达
视图 = 函数(数据状态, 加载状态, 错误状态) // 三种状态共同决定视图
loading为true时,视图是加载指示器;error不为空时,视图是错误提示;- 两者都为空时,视图是正常内容。
错误是状态的一种,视图根据错误状态渲染降级 UI。这样,错误处理就不再是"补丁",而是状态驱动的一部分。
十一、练习题
练习题1:选择题——Error Boundary 能捕获什么
题目:Error Boundary 能捕获下面哪种错误?
A. 网络请求失败
B. 组件渲染时访问 undefined 属性
C. setTimeout 中的错误
D. 点击事件处理函数中的错误
参考答案:B
解读:Error Boundary 只能捕获子组件树渲染阶段抛出的错误。异步错误、事件处理错误它都捕获不到。
练习题2:填空题——Error Boundary 方法
题目:Error Boundary 需要实现哪个静态方法,用于在错误发生时更新 state?
// JavaScript + React Native
static ____(error) { // 补全方法名
return { hasError: true }; // 返回新状态
}
参考答案:getDerivedStateFromError
解读:getDerivedStateFromError 在渲染错误时被调用,返回新的 state,触发降级 UI 渲染。
练习题3:改错题——异步错误没被捕获
题目:下面代码的错误为什么不会被 Error Boundary 捕获?如何修正?
// JavaScript + React Native
function App() { // 定义组件
useEffect(() => { // 注册副作用
fetch('https://api.example.com').then((res) => res.json()); // 发请求
}, []); // 空数组
return <Text>你好</Text>; // 返回文本
} // 结束组件
参考答案:Error Boundary 捕获不到异步错误。需要在 fetch 链上加 .catch(),或者在 async 函数中用 try/catch。
解读:Error Boundary 只处理渲染错误。异步错误需要单独处理。
练习题4:代码阅读题——Error Boundary 重试
题目:阅读代码,说出点击"重试"按钮后会发生什么。
// JavaScript + React Native
handleReset = () => { // 定义重置函数
this.setState({ hasError: false, error: null }); // 重置状态
}; // 结束
参考答案:hasError 被设回 false,Error Boundary 重新渲染 this.props.children,尝试再次渲染子组件。
解读:重置状态让 Error Boundary 回到正常渲染路径。
练习题5:动手题——简单的 Error Boundary
题目:写一个 Error Boundary,渲染错误时显示"出错了"和一个重试按钮。
参考答案:
// JavaScript + React Native
class ErrorBoundary extends React.Component { // 定义 Error Boundary
state = { hasError: false }; // 初始状态
static getDerivedStateFromError() { // 渲染错误时调用
return { hasError: true }; // 返回新状态
} // 结束
render() { // 渲染方法
if (this.state.hasError) { // 如果有错误
return ( // 返回降级 UI
<View> // 外层容器
<Text>出错了</Text> // 提示
<Button title="重试" onPress={() => this.setState({ hasError: false })} /> // 重试
</View> // 结束容器
); // 结束 return
} // 结束判断
return this.props.children; // 正常渲染
} // 结束 render
} // 结束 ErrorBoundary
解读:这是 Error Boundary 的最小实现。
练习题6:简答题——为什么异步错误需要单独处理
题目:为什么 Error Boundary 捕获不到异步错误?
参考答案:Error Boundary 捕获的是 React 渲染过程中抛出的同步错误。异步错误发生在渲染流程之外,React 无法感知,所以需要 try/catch 或 .catch() 单独处理。
解读:理解这一点,才能在正确的地方放正确的错误处理。
练习题7:try/catch 题
题目:写一个 loadData 函数,请求数据,失败时设置错误状态,最后关闭加载状态。
参考答案:
// JavaScript + React Native
const loadData = async () => { // 定义加载函数
try { // 开始 try
setLoading(true); // 打开加载
const res = await fetch('https://api.example.com/data'); // 发请求
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 检查状态码
const json = await res.json(); // 解析 JSON
setData(json); // 更新数据
} catch (e) { // 捕获错误
setError(e.message); // 更新错误
} finally { // 收尾
setLoading(false); // 关闭加载
} // 结束 try
}; // 结束 loadData
解读:try 放正常逻辑,catch 捕获错误,finally 做收尾。
练习题8:请求超时题
题目:用 AbortController 实现一个带超时的 fetch。
参考答案:
// JavaScript + React Native
const fetchWithTimeout = async (url, timeout = 10000) => { // 定义带超时的请求
const controller = new AbortController(); // 创建控制器
const timer = setTimeout(() => controller.abort(), timeout); // 超时后取消
try { // 开始 try
return await fetch(url, { signal: controller.signal }); // 发请求
} catch (e) { // 捕获错误
if (e.name === 'AbortError') throw new Error('请求超时'); // 超时错误
throw e; // 其他错误
} finally { // 收尾
clearTimeout(timer); // 清除定时器
} // 结束 try
}; // 结束 fetchWithTimeout
解读:AbortController 让请求可以被取消。超时后调用 abort(),请求抛出 AbortError。
练习题9:条件日志题
题目:只在开发环境下打印日志。
参考答案:
// JavaScript + React Native
if (__DEV__) { // 开发模式判断
console.log('调试信息'); // 打印日志
} // 结束判断
解读:__DEV__ 是 React Native 提供的全局变量,开发模式为 true。
练习题10:console 计时题
题目:用 console.time 和 console.timeEnd 测量一段异步代码的耗时。
参考答案:
// JavaScript + React Native
console.time('请求耗时'); // 开始计时
await fetch('https://api.example.com'); // 执行操作
console.timeEnd('请求耗时'); // 结束计时并打印
解读:console.time 开始计时,console.timeEnd 结束并打印耗时。
练习题11:React.memo 题
题目:用 React.memo 包裹一个列表项组件,避免 props 不变时重新渲染。
参考答案:
// JavaScript + React Native
import { memo } from 'react'; // 引入 memo
const TodoItem = memo(function TodoItem({ text, done, onPress }) { // 用 memo 包裹
return ( // 返回界面描述
<Pressable onPress={onPress}> // 可点击容器
<Text>{done ? '✓ ' : '○ '}{text}</Text> // 显示文本
</Pressable> // 结束 Pressable
); // 结束 return
}); // 结束 TodoItem
解读:memo 让组件在 props 没变时跳过渲染。列表项是典型的使用场景。
练习题12:useCallback 题
题目:用 useCallback 稳定 renderItem 函数。
参考答案:
// JavaScript + React Native
const renderItem = useCallback( // 缓存渲染函数
({ item }) => <TodoItem {...item} />, // 渲染每一项
[] // 无依赖
); // 结束 useCallback
解读:如果 renderItem 每次渲染都是新函数,FlatList 会认为它变了,导致所有项重新渲染。
练习题13:FlatList 性能题
题目:写出 FlatList 的三个性能优化属性,并说明作用。
参考答案:
initialNumToRender:初始渲染的项数,减少首屏渲染时间;windowSize:可视区域外保留的"屏幕数",控制渲染范围;removeClippedSubviews:移除屏幕外的视图,节省内存。
解读:这些属性让 FlatList 只渲染必要的项,大幅提升长列表性能。
练习题14:综合题——带错误处理的网络请求组件
题目:创建一个组件,加载数据时显示加载指示器,失败时显示错误信息和重试按钮,成功时显示数据。
参考答案:
// JavaScript + React Native
import { useState, useEffect, useCallback } from 'react'; // 引入 Hook
import { View, Text, ActivityIndicator, Button } from 'react-native'; // 引入组件
export default function DataView() { // 定义组件
const [data, setData] = useState(null); // 数据状态
const [loading, setLoading] = useState(true); // 加载状态
const [error, setError] = useState(null); // 错误状态
const load = useCallback(async () => { // 缓存加载函数
try { // 开始 try
setLoading(true); // 打开加载
setError(null); // 清空错误
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 发请求
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 检查状态码
setData(await res.json()); // 更新数据
} catch (e) { // 捕获错误
setError(e.message); // 更新错误
} finally { // 收尾
setLoading(false); // 关闭加载
} // 结束 try
}, []); // 无依赖
useEffect(() => { // 注册副作用
load(); // 首次加载
}, [load]); // 依赖 load
if (loading) { // 加载中
return <View style={{ flex: 1, justifyContent: 'center' }}><ActivityIndicator size="large" /></View>; // 显示指示器
} // 结束判断
if (error) { // 出错
return ( // 返回界面描述
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> // 居中容器
<Text>加载失败:{error}</Text> // 错误信息
<Button title="重试" onPress={load} /> // 重试按钮
</View> // 结束容器
); // 结束 return
} // 结束判断
return ( // 成功
<View style={{ padding: 20 }}> // 外层容器
<Text>{data.title}</Text> // 显示数据
</View> // 结束容器
); // 结束 return
} // 结束 DataView
解读:这是异步请求的完整形态:三态管理、try/catch/finally、重试按钮。
练习题15:综合应用题——带错误边界和优化的文章列表
题目:创建一个文章列表应用,要求:
- 用 Error Boundary 包裹列表;
- 加载中显示指示器,失败显示重试;
- 用
memo和useCallback优化列表项; - 用
FlatList的性能属性优化长列表。
参考答案:
// JavaScript + React Native
import React, { useState, useEffect, useCallback, memo } from 'react'; // 引入 Hook
import { View, Text, FlatList, ActivityIndicator, Button, Pressable, StyleSheet } from 'react-native'; // 引入组件
class ErrorBoundary extends React.Component { // 定义 Error Boundary
state = { hasError: false }; // 初始状态
static getDerivedStateFromError() { // 渲染错误时调用
return { hasError: true }; // 返回新状态
} // 结束
componentDidCatch(error, info) { // 错误后调用
console.error('渲染错误:', error, info.componentStack); // 打印错误
} // 结束
render() { // 渲染方法
if (this.state.hasError) { // 如果有错误
return ( // 返回降级 UI
<View style={styles.center}> // 居中容器
<Text>页面出错了</Text> // 提示
<Button title="重试" onPress={() => this.setState({ hasError: false })} /> // 重试
</View> // 结束容器
); // 结束 return
} // 结束判断
return this.props.children; // 正常渲染
} // 结束 render
} // 结束 ErrorBoundary
const ArticleItem = memo(function ArticleItem({ title }) { // 用 memo 包裹组件
return ( // 返回界面描述
<Pressable style={styles.item}> // 可点击容器
<Text style={styles.title}>{title}</Text> // 显示标题
</Pressable> // 结束 Pressable
); // 结束 return
}); // 结束 ArticleItem
function ArticleList() { // 定义列表组件
const [posts, setPosts] = useState([]); // 文章列表状态
const [loading, setLoading] = useState(true); // 加载状态
const [error, setError] = useState(null); // 错误状态
const load = useCallback(async () => { // 缓存加载函数
try { // 开始 try
setLoading(true); // 打开加载
setError(null); // 清空错误
const res = await fetch('https://jsonplaceholder.typicode.com/posts'); // 发请求
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 检查状态码
setPosts(await res.json()); // 更新数据
} catch (e) { // 捕获错误
setError(e.message); // 更新错误
} finally { // 收尾
setLoading(false); // 关闭加载
} // 结束 try
}, []); // 无依赖
useEffect(() => { // 注册副作用
load(); // 首次加载
}, [load]); // 依赖 load
const renderItem = useCallback( // 缓存渲染函数
({ item }) => <ArticleItem title={item.title} />, // 渲染每一项
[] // 无依赖
); // 结束 useCallback
if (loading) { // 加载中
return <View style={styles.center}><ActivityIndicator size="large" /></View>; // 显示指示器
} // 结束判断
if (error) { // 出错
return ( // 返回界面描述
<View style={styles.center}> // 居中容器
<Text>加载失败:{error}</Text> // 错误信息
<Button title="重试" onPress={load} /> // 重试按钮
</View> // 结束容器
); // 结束 return
} // 结束判断
return ( // 成功
<FlatList // 高效列表
data={posts} // 数据源
keyExtractor={(item) => item.id.toString()} // 从每项提取唯一 key
renderItem={renderItem} // 渲染函数
initialNumToRender={10} // 初始渲染 10 项
maxToRenderPerBatch={10} // 每批渲染 10 项
windowSize={5} // 窗口大小
removeClippedSubviews // 移除屏幕外视图
ItemSeparatorComponent={() => <View style={styles.separator} />} // 分隔线
/>
); // 结束 return
} // 结束 ArticleList
export default function App() { // 定义并默认导出 App
return ( // 返回界面描述
<ErrorBoundary> // 包裹 Error Boundary
<ArticleList /> // 列表组件
</ErrorBoundary> // 结束包裹
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, // 居中容器
item: { padding: 16 }, // 列表项样式
title: { fontSize: 16, fontWeight: 'bold' }, // 标题样式
separator: { height: 1, backgroundColor: '#eee' }, // 分隔线样式
}); // 结束样式表
解读:Error Boundary 兜底渲染错误,三态处理异步请求,memo 和 useCallback 优化渲染,FlatList 性能属性优化长列表。错误处理和性能优化不是"补丁",而是应用架构的一部分。
十二、总结
这一课我们学习了 React Native 中错误处理和调试的核心知识:
- 错误分三类:渲染错误、异步错误、事件处理错误;
- Error Boundary 捕获渲染错误,显示降级 UI,需要类组件实现;
- Error Boundary 捕获不到异步错误和事件错误,需要 try/catch;
- 异步错误用
try/catch/finally处理,三态管理是标准模式; - 请求可以用
AbortController做超时控制,可以加自动重试; - 全局错误可以用
ErrorUtils捕获,错误应该上报到服务器; - 用户看到的应该是友好的错误提示,不是技术堆栈;
console有分组、表格、计时、断言等进阶用法;__DEV__用于区分开发和生产环境;- React DevTools 的 Profiler 可以找出渲染热点;
memo、useCallback、useMemo减少不必要的重渲染;FlatList有多个性能优化属性,长列表必须配置;- 动画要走原生驱动,Reanimated 默认在 UI 线程执行;
- 内存泄漏来自未清理的定时器和监听器。
记住一句话:
错误是状态的一种,性能问题是渲染路径的问题,两者都可以用"视图 = 函数(状态)"的思路来理解和解决。
第12课预告
下一课,我们进入 【React Native娓娓道来】第12课 网络与 API 交互进阶:请求封装、缓存与离线处理。
更多推荐



所有评论(0)