一、本课目标
学完这一课,你要建立六个认知:
本课核心线索:
视图 = 函数(状态)
↓
函数必须纯 → 副作用不能放在函数里
↓
副作用隔离到事件、副作用 API、生命周期
↓
不同框架的副作用 API 对照
↓
常见陷阱:竞态、泄漏、依赖错误
↓
实战:正确管理副作用
二、副作用的本质
2.1 一句话说透
副作用是"渲染函数之外的操作"。
视图 = 函数(状态)
→ 函数只负责返回 UI 描述
→ 函数之外的操作,都是副作用
副作用包括:
– 发网络请求
– 订阅事件
– 操作 DOM
– 启动定时器
– 写日志
– 读写本地存储
– 操作数据库
– 调用原生 API
2.2 为什么渲染函数必须纯
核心原因:框架可能多次调用渲染函数。
React 严格模式:开发环境调用两次
Compose:重组可能多次
SwiftUI:body 可能多次计算
如果渲染函数有副作用:
第一次调用:发请求 A
第二次调用:发请求 B
→ 两次请求,浪费资源
→ 可能有竞态条件
→ 结果不确定
具体示例:
// TSX (React Native)
// 错误:渲染中发请求
function UserProfile({ userId }) {
// 每次渲染都发请求!
fetch(`/api/user/${userId}`).then(…)
return <Text>Loading…</Text>
}
问题:
· 用户改个无关状态就触发一次请求
· 严格模式下每次渲染发两次请求
· 竞态条件:先发的请求后返回,覆盖后发的结果
2.3 副作用的三个特征
特征 说明 示例
非纯 相同输入可能产生不同结果 发请求、随机数
外部交互 与外部系统交互 网络、存储、原生 API
需要管理 需要正确的时机和清理 订阅、定时器
2.4 副作用与渲染的关系
┌─────────────────────────────────────────┐
│ 渲染函数(纯) │
│ – 读取状态 │
│ – 计算派生数据 │
│ – 返回 UI 描述 │
│ – 不做副作用 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 副作用(隔离) │
│ – 事件回调:onClick、onChange │
│ – 副作用 API:useEffect、LaunchedEffect │
│ – 生命周期:onAppear、aboutToAppear │
└─────────────────────────────────────────┘
关键理解: 渲染函数只计算 UI 描述,副作用在渲染函数之外执行。
2.5 各框架副作用 API 对照
框架 副作用 API 触发时机
React Native useEffect 依赖变化后
Compose LaunchedEffect 进入组合时
Flutter initState / dispose 组件创建/销毁时
SwiftUI task / onAppear View 出现时
ArkUI aboutToAppear / aboutToDisappear 组件创建/销毁时
三、副作用的正确位置
3.1 事件回调
适用场景: 用户交互触发的副作用。
// TSX (React Native)
function LoginForm() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
// 事件回调:用户点击时执行副作用
const handleLogin = async () => {
// 副作用:发请求
const result = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
})
// 处理结果
}
return (
<View>
<TextInput value={username} onChangeText={setUsername} />
<TextInput value={password} onChangeText={setPassword} secureTextEntry />
<Button title="登录" onPress={handleLogin} />
</View>
)
}
关键点: 副作用在用户点击时执行,不在渲染时执行。
3.2 副作用 API
适用场景: 状态变化触发的副作用。
React Native:
// TSX (React Native)
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
// useEffect:userId 变化时执行副作用
useEffect(() => {
// 副作用:发请求
let cancelled = false
fetch(`/api/user/${userId}`)
.then(r => r.json())
.then(data => {
if (cancelled) return
setUser(data)
})
// 清理函数:组件卸载或 userId 变化时执行
return () => { cancelled = true }
}, [userId]) // 依赖数组
if (!user) return <Spinner />
return <UserCard user={user} />
}
Compose:
// Kotlin (Jetpack Compose)
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
// LaunchedEffect:userId 变化时执行副作用
LaunchedEffect(userId) {
// 副作用:发请求
user = fetchUser(userId)
}
if (user == null) {
Spinner()
} else {
UserCard(user!!)
}
}
SwiftUI:
// Swift (SwiftUI)
struct UserProfile: View {
let userId: String
@State private var user: User?
var body: some View {
Group {
if let user = user {
UserCard(user: user)
} else {
Spinner()
}
}
// task:View 出现或 userId 变化时执行副作用
.task(id: userId) {
// 副作用:发请求
user = await fetchUser(userId)
}
}
}
ArkUI:
// ArkTS (ArkUI)
@Component
struct UserProfile {
@Prop userId: string
@State user: User | null = null
// aboutToAppear:组件创建时执行副作用
aboutToAppear() {
// 副作用:发请求
fetchUser(this.userId).then(data => {
this.user = data
})
}
build() {
if (this.user === null) {
Spinner()
} else {
UserCard({ user: this.user })
}
}
}
3.3 生命周期
适用场景: 组件创建/销毁时的副作用。
Flutter:
// Dart (Flutter)
class UserProfile extends StatefulWidget {
final String userId;
const UserProfile({required this.userId});
State<UserProfile> createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
User? user;
StreamSubscription? subscription;
void initState() {
super.initState()
// 副作用:组件创建时发请求
fetchUser(widget.userId).then((data) {
setState(() => user = data)
})
// 副作用:订阅事件
subscription = eventStream.listen((event) {
// 处理事件
})
}
void dispose() {
// 清理:组件销毁时取消订阅
subscription?.cancel()
super.dispose()
}
Widget build(BuildContext context) {
if (user == null) return Spinner();
return UserCard(user: user!);
}
}
3.4 副作用位置的选择
问:副作用是什么触发的?
→ 用户交互 → 事件回调
→ 状态变化 → 副作用 API(useEffect、LaunchedEffect)
→ 组件创建/销毁 → 生命周期(initState、aboutToAppear)
选择原则:
触发方式 副作用位置 示例
用户点击 事件回调 onClick={handleClick}
状态变化 useEffect useEffect(() => {…}, [dep])
组件挂载 useEffect + 空依赖 useEffect(() => {…}, [])
组件卸载 useEffect 清理函数 return () => cleanup()
定时器 useEffect useEffect(() => { setInterval(…) })
订阅 useEffect useEffect(() => { subscribe(…) })
四、副作用 API 详解
4.1 useEffect 的三个部分
React Native 的 useEffect 由三部分组成:
// TSX (React Native)
useEffect(() => {
// 1. 副作用函数:执行副作用
console.log('副作用执行')
// 2. 清理函数(可选):清理副作用
return () => {
console.log('清理副作用')
}
}, [dep1, dep2]) // 3. 依赖数组:控制执行时机
执行时机:
依赖数组 执行时机
不传 每次渲染后执行
[] 仅挂载时执行一次
[dep] dep 变化后执行
4.2 useEffect 的三种使用模式
模式一:只在挂载时执行一次。
// TSX (React Native)
useEffect(() => {
// 副作用:初始化
console.log('组件挂载')
return () => {
// 清理:组件卸载
console.log('组件卸载')
}
}, []) // 空依赖数组
模式二:依赖变化时执行。
// TSX (React Native)
useEffect(() => {
// 副作用:query 变化时重新搜索
search(query)
return () => {
// 清理:取消上一次搜索
cancelSearch()
}
}, [query]) // query 变化时执行
模式三:每次渲染都执行。
// TSX (React Native)
useEffect(() => {
// 副作用:每次渲染都执行
console.log('渲染完成')
}) // 不传依赖数组
注意: 模式三很少使用,容易导致性能问题。
4.3 清理函数的重要性
为什么需要清理函数?
副作用需要清理的场景:
– 定时器:clearInterval
– 订阅:unsubscribe
– 请求:取消请求(AbortController)
– 事件监听:removeEventListener
– WebSocket:close
没有清理函数的后果:
// TSX (React Native)
// 错误:没有清理函数
useEffect(() => {
const timer = setInterval(() => {
console.log('tick')
}, 1000)
// 没有清理,组件卸载后定时器继续运行 → 内存泄漏
}, [])
正确做法:
// TSX (React Native)
// 正确:有清理函数
useEffect(() => {
const timer = setInterval(() => {
console.log('tick')
}, 1000)
return () => {
clearInterval(timer) // 清理定时器
}
}, [])
4.4 各框架副作用 API 对照
React Native:
// TSX (React Native)
useEffect(() => {
// 副作用
return () => {
// 清理
}
}, [deps])
Compose:
// Kotlin (Jetpack Compose)
LaunchedEffect(key) {
// 副作用
// 协程取消时自动清理
}
DisposableEffect(key) {
// 副作用
onDispose {
// 清理
}
}
SwiftUI:
// Swift (SwiftUI)
.task(id: key) {
// 副作用
// task 取消时自动清理
}
.onAppear {
// 副作用
}
.onDisappear {
// 清理
}
ArkUI:
// ArkTS (ArkUI)
aboutToAppear() {
// 副作用
}
aboutToDisappear() {
// 清理
}
Flutter:
// Dart (Flutter)
void initState() {
// 副作用
}
void dispose() {
// 清理
super.dispose()
}
五、副作用的常见陷阱
5.1 陷阱一:竞态条件
问题: 多个请求并发,先发的请求后返回,覆盖后发的结果。
// TSX (React Native)
// 错误:有竞态条件
function Search({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => setResults(data))
}, [query])
return <List items={results} />
}
问题分析:
用户输入 "a" → 发请求 A
用户输入 "ab" → 发请求 B
请求 B 先返回 → setResults(B 的结果)
请求 A 后返回 → setResults(A 的结果) ← 覆盖了 B 的结果
结果:显示 "a" 的结果,但输入框是 "ab"
修正: 用清理函数取消过期请求。
// TSX (React Native)
// 正确:用 cancelled 标志
function Search({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
let cancelled = false
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
if (cancelled) return // 过期请求,忽略
setResults(data)
})
return () => { cancelled = true }
}, [query])
return <List items={results} />
}
或者用 AbortController:
// TSX (React Native)
function Search({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
const controller = new AbortController()
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(data => setResults(data))
.catch(err => {
if (err.name === 'AbortError') return // 请求被取消,忽略
// 处理其他错误
})
return () => { controller.abort() }
}, [query])
return <List items={results} />
}
5.2 陷阱二:内存泄漏
问题: 组件卸载后,副作用还在运行。
// TSX (React Native)
// 错误:组件卸载后定时器还在运行
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1) // 组件卸载后调用 setState → 警告
}, 1000)
// 没有清理
}, [])
return <Text>{count}</Text>
}
修正: 清理定时器。
// TSX (React Native)
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)
}, 1000)
return () => { clearInterval(timer) } // 清理
}, [])
return <Text>{count}</Text>
}
5.3 陷阱三:依赖数组错误
错误一:依赖数组缺少依赖。
// TSX (React Native)
// 错误:依赖数组缺少 userId
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`/api/user/${userId}`) // 用了 userId,但依赖数组没写
.then(r => r.json())
.then(setUser)
}, []) // 空依赖数组
return <UserCard user={user} />
}
问题: userId 变化时,effect 不会重新执行,显示旧的用户数据。
修正: 把 userId 加到依赖数组。
// TSX (React Native)
useEffect(() => {
fetch(`/api/user/${userId}`)
.then(r => r.json())
.then(setUser)
}, [userId]) // 加上 userId
错误二:依赖数组有对象/数组。
// TSX (React Native)
// 错误:每次渲染创建新对象,导致 effect 每次执行
function UserProfile({ user }) {
useEffect(() => {
console.log('user 变化')
}, [user]) // user 是对象,每次渲染引用可能变化
}
修正: 用具体字段作为依赖。
// TSX (React Native)
useEffect(() => {
console.log('user.id 变化')
}, [user.id]) // 用具体字段
5.4 陷阱四:useEffect 中改状态导致无限循环
// TSX (React Native)
// 错误:无限循环
function Bad() {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1) // 改状态
}, [count]) // count 变化触发 effect,effect 又改 count
return <Text>{count}</Text>
}
修正: 明确 effect 的目的。
// TSX (React Native)
function Good() {
const [count, setCount] = useState(0)
useEffect(() => {
// 只在挂载时执行一次
setCount(1)
}, []) // 空依赖数组
return <Text>{count}</Text>
}
5.5 陷阱五:用 useEffect 同步派生数据
// TSX (React Native)
// 错误:用 useEffect 同步派生数据
function User({ firstName, lastName }) {
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName) // 多余的状态同步
}, [firstName, lastName])
return <Text>{fullName}</Text>
}
修正: 派生数据直接算。
// TSX (React Native)
function User({ firstName, lastName }) {
const fullName = firstName + ' ' + lastName // 直接算
return <Text>{fullName}</Text>
}
5.6 陷阱六:在渲染中执行副作用
// TSX (React Native)
// 错误:渲染中执行副作用
function Bad() {
const [data, setData] = useState(null)
fetch('/api/data').then(r => r.json()).then(setData) // 渲染中发请求
return <Text>{data}</Text>
}
修正: 副作用放在 useEffect 中。
// TSX (React Native)
function Good() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData)
}, [])
return <Text>{data}</Text>
}
5.7 陷阱七:清理函数中引用旧状态
// TSX (React Native)
// 错误:清理函数引用旧状态
function Bad({ userId }) {
useEffect(() => {
const ws = new WebSocket(`ws://api/user/${userId}`)
return () => {
// 问题:这里引用的是创建时的 userId
ws.send(`unsubscribe:${userId}`)
ws.close()
}
}, [userId])
}
修正: 清理函数中不需要引用状态,或者用 ref。
// TSX (React Native)
function Good({ userId }) {
useEffect(() => {
const ws = new WebSocket(`ws://api/user/${userId}`)
return () => {
ws.close() // 直接关闭,不需要引用 userId
}
}, [userId])
}
5.8 各框架副作用陷阱对照
陷阱 React Native SwiftUI Compose ArkUI Flutter
竞态条件 cancelled 标志 task 自动取消 LaunchedEffect 自动取消 手动处理 手动处理
内存泄漏 清理函数 task 自动取消 DisposableEffect aboutToDisappear dispose
依赖错误 依赖数组 task(id:) LaunchedEffect(key) 手动处理 手动处理
无限循环 空依赖/条件 手动处理 手动处理 手动处理 手动处理
派生数据同步 直接算 直接算 直接算 直接算 直接算
六、副作用与生命周期
6.1 生命周期的三个阶段
挂载(Mount)→ 更新(Update)→ 卸载(Unmount)
各框架生命周期对照:
阶段 React Native SwiftUI Compose ArkUI Flutter
挂载 useEffect(() => {}, []) onAppear LaunchedEffect(Unit) aboutToAppear initState
更新 useEffect(() => {}, [dep]) task(id:) LaunchedEffect(key) @Watch didUpdateWidget
卸载 useEffect 清理函数 onDisappear DisposableEffect aboutToDisappear dispose
6.2 挂载时的副作用
场景: 组件创建时初始化。
// TSX (React Native)
function UserProfile({ userId }) {
useEffect(() => {
// 挂载时执行:初始化
initializeAnalytics()
}, [])
return <Text>User Profile</Text>
}
// Swift (SwiftUI)
struct UserProfile: View {
var body: some View {
Text("User Profile")
.onAppear {
// 挂载时执行:初始化
initializeAnalytics()
}
}
}
// Kotlin (Jetpack Compose)
@Composable
fun UserProfile() {
LaunchedEffect(Unit) {
// 挂载时执行:初始化
initializeAnalytics()
}
Text("User Profile")
}
6.3 更新时的副作用
场景: 依赖变化时重新执行。
// TSX (React Native)
function Search({ query }) {
useEffect(() => {
// query 变化时执行:搜索
search(query)
}, [query])
return <Text>Search: {query}</Text>
}
// Kotlin (Jetpack Compose)
@Composable
fun Search(query: String) {
LaunchedEffect(query) {
// query 变化时执行:搜索
search(query)
}
Text("Search: $query")
}
// Swift (SwiftUI)
struct Search: View {
let query: String
var body: some View {
Text("Search: \\(query)")
.task(id: query) {
// query 变化时执行:搜索
await search(query)
}
}
}
6.4 卸载时的清理
场景: 组件销毁时清理资源。
// TSX (React Native)
function Timer() {
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => {
// 卸载时清理:清除定时器
clearInterval(timer)
}
}, [])
return <Text>Timer</Text>
}
// Kotlin (Jetpack Compose)
@Composable
fun Timer() {
DisposableEffect(Unit) {
val timer = Timer()
timer.start()
onDispose {
// 卸载时清理:停止定时器
timer.stop()
}
}
Text("Timer")
}
// Swift (SwiftUI)
struct Timer: View {
var body: some View {
Text("Timer")
.onAppear {
// 挂载时启动
startTimer()
}
.onDisappear {
// 卸载时清理
stopTimer()
}
}
}
6.5 生命周期的最佳实践
1. 挂载时初始化,卸载时清理
→ 成对出现
2. 订阅和取消订阅成对出现
→ subscribe / unsubscribe
3. 定时器和清除定时器成对出现
→ setInterval / clearInterval
4. 请求和取消请求成对出现
→ fetch / abort
5. 依赖变化时重新执行,清理上一次
→ useEffect(() => {…}, [dep])
七、常见误区
7.1 在渲染中执行副作用
// TSX (React Native)
// 错误:渲染中发请求
function Bad() {
fetch('/api/data').then(setData)
return <Text>Loading</Text>
}
// 正确:副作用放 useEffect
function Good() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(setData)
}, [])
return <Text>{data}</Text>
}
7.2 忘记清理函数
// TSX (React Native)
// 错误:没有清理
useEffect(() => {
const timer = setInterval(() => {}, 1000)
}, [])
// 正确:有清理
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => clearInterval(timer)
}, [])
7.3 依赖数组错误
// TSX (React Native)
// 错误:缺少依赖
useEffect(() => {
fetch(`/api/user/${userId}`)
}, []) // 缺少 userId
// 正确:完整依赖
useEffect(() => {
fetch(`/api/user/${userId}`)
}, [userId])
7.4 用 useEffect 同步状态
// TSX (React Native)
// 错误:用 useEffect 同步派生数据
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + lastName)
}, [firstName, lastName])
// 正确:直接算
const fullName = firstName + lastName
7.5 忽略竞态条件
// TSX (React Native)
// 错误:忽略竞态条件
useEffect(() => {
fetch(`/api/search?q=${query}`).then(setResults)
}, [query])
// 正确:处理竞态
useEffect(() => {
let cancelled = false
fetch(`/api/search?q=${query}`).then(data => {
if (!cancelled) setResults(data)
})
return () => { cancelled = true }
}, [query])
7.6 在 useEffect 中改状态导致无限循环
// TSX (React Native)
// 错误:无限循环
useEffect(() => {
setCount(count + 1)
}, [count])
// 正确:明确目的
useEffect(() => {
setCount(1)
}, [])
7.7 清理函数引用旧状态
// TSX (React Native)
// 错误:清理函数引用旧状态
useEffect(() => {
return () => {
console.log(count) // 引用的是创建时的 count
}
}, [])
// 正确:用 ref 获取最新值
const countRef = useRef(count)
useEffect(() => {
countRef.current = count
})
useEffect(() => {
return () => {
console.log(countRef.current) // 最新值
}
}, [])
八、习题(15 道)
题目 1(选择题)
副作用的本质是什么?
A. 渲染函数的一部分
B. 渲染函数之外的操作
C. 状态的一部分
D. UI 描述的一部分
参考答案:B
解读: 副作用是"渲染函数之外的操作"。视图 = 函数(状态) 要求函数只负责返回 UI 描述,函数之外的操作都是副作用。
题目 2(判断题)
渲染函数中可以发网络请求。
参考答案:错误
解读: 渲染函数必须纯。发请求属于副作用,应该放在 useEffect、LaunchedEffect、task、onAppear 等副作用 API 中。渲染中发请求会导致多次调用、竞态条件、内存泄漏。
题目 3(填空题)
React Native 中副作用放在 ______,Compose 中副作用放在 ______,SwiftUI 中副作用放在 ______。
参考答案:useEffect;LaunchedEffect;task / onAppear
解读: 不同框架的副作用 API 不同,但核心原则一致:渲染函数只负责返回 UI 描述,副作用只发生在事件或生命周期中。
题目 4(简答题)
为什么渲染函数必须纯?请从框架多次调用的角度分析。
参考答案: 框架可能多次调用渲染函数:
· React 严格模式:开发环境调用两次
· Compose:重组可能多次
· SwiftUI:body 可能多次计算
如果渲染函数有副作用,多次调用会产生不同结果:
· 多次发请求,浪费资源
· 竞态条件,结果不确定
· 内存泄漏
所以渲染函数必须纯,副作用必须隔离。
题目 5(代码分析题)
下面代码有什么问题?如何修正?
// TSX (React Native)
function UserProfile({ userId }) {
fetch(`/api/user/${userId}`).then(setUser)
return <Text>Loading…</Text>
}
参考答案: 渲染中发请求。每次渲染都发请求,严格模式下发两次请求,有竞态条件。
修正: 副作用放 useEffect。
// TSX (React Native)
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
let cancelled = false
fetch(`/api/user/${userId}`)
.then(r => r.json())
.then(data => {
if (!cancelled) setUser(data)
})
return () => { cancelled = true }
}, [userId])
if (!user) return <Spinner />
return <UserCard user={user} />
}
题目 6(代码改错题)
下面代码有什么问题?如何修正?
// TSX (React Native)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)
}, 1000)
}, [])
参考答案: 没有清理函数,组件卸载后定时器继续运行,导致内存泄漏。
修正: 加清理函数。
// TSX (React Native)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
题目 7(跨框架对比题)
写出 React Native、Compose、SwiftUI 中副作用 API 的核心写法。
参考答案:
React Native:
// TSX (React Native)
useEffect(() => {
// 副作用
return () => {
// 清理
}
}, [deps])
Compose:
// Kotlin (Jetpack Compose)
LaunchedEffect(key) {
// 副作用(协程取消时自动清理)
}
DisposableEffect(key) {
// 副作用
onDispose {
// 清理
}
}
SwiftUI:
// Swift (SwiftUI)
.task(id: key) {
// 副作用(task 取消时自动清理)
}
解读: 三种框架的副作用 API 本质相同——在渲染函数之外执行副作用,在适当时候清理。差异在自动清理机制:Compose 和 SwiftUI 自动取消,React Native 需要手动清理。
题目 8(选择题)
以下哪种场景最适合用 useEffect?
A. 用户点击按钮
B. 组件挂载时初始化
C. 渲染中计算派生数据
D. 条件渲染
参考答案:B
解读: useEffect 适合组件挂载、依赖变化、卸载等场景。用户点击用事件回调,派生数据直接算,条件渲染用 if/else。
题目 9(判断题)
useEffect 的依赖数组为空时,effect 每次渲染都执行。
参考答案:错误
解读: useEffect 的依赖数组为空 [] 时,effect 仅在挂载时执行一次。不传依赖数组时,effect 每次渲染都执行。
题目 10(填空题)
React Native 中副作用有三种使用模式:、、______。
参考答案:只在挂载时执行一次([]);依赖变化时执行([dep]);每次渲染都执行(不传依赖)
解读: 三种模式对应不同的依赖数组。最常用的是前两种,第三种很少使用。
题目 11(简答题)
什么是竞态条件?如何用 useEffect 避免竞态条件?
参考答案: 竞态条件是多个请求并发时,先发的请求后返回,覆盖后发请求的结果。
用 useEffect 避免竞态条件的方法:
// TSX (React Native)
useEffect(() => {
let cancelled = false
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
if (!cancelled) setResults(data)
})
return () => { cancelled = true }
}, [query])
// TSX (React Native)
useEffect(() => {
const controller = new AbortController()
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
return () => { controller.abort() }
}, [query])
题目 12(代码分析题)
下面代码有什么问题?如何修正?
// TSX (React Native)
function User({ firstName, lastName }) {
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
return <Text>{fullName}</Text>
}
参考答案: 用 useEffect 同步派生数据,导致两次渲染,逻辑上 fullName 不该是状态。
修正: 派生数据直接算。
// TSX (React Native)
function User({ firstName, lastName }) {
const fullName = firstName + ' ' + lastName
return <Text>{fullName}</Text>
}
题目 13(设计题)
设计一个实时搜索组件:
· 输入框输入关键词
· 输入后 300ms 发请求搜索
· 显示搜索结果
· 处理竞态条件
写出状态设计、副作用、清理逻辑。
参考答案:
// TSX (React Native)
function Search() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
if (!query) {
setResults([])
return
}
// 防抖:300ms 后才发请求
const timer = setTimeout(() => {
setIsLoading(true)
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
setResults(data)
setIsLoading(false)
})
.catch(() => {
setIsLoading(false)
})
}, 300)
// 清理:取消上一次定时器
return () => clearTimeout(timer)
}, [query])
return (
<View>
<TextInput value={query} onChangeText={setQuery} />
{isLoading ? <Spinner /> : <List items={results} />}
</View>
)
}
状态:query、results、isLoading
副作用:防抖 + 搜索
清理:清除定时器
解读: 用 setTimeout 实现防抖,清理函数取消上一次定时器。这样每次输入后 300ms 才发请求,避免频繁请求。
题目 14(设计题)
设计一个 WebSocket 订阅组件:
· 组件挂载时建立连接
· 接收消息更新状态
· 组件卸载时关闭连接
· 处理重连
写出状态设计、副作用、清理逻辑。
参考答案:
// TSX (React Native)
function LiveUpdates() {
const [messages, setMessages] = useState([])
const [isConnected, setIsConnected] = useState(false)
useEffect(() => {
let ws = new WebSocket('ws://api/live')
let reconnectTimer = null
const connect = () => {
ws = new WebSocket('ws://api/live')
ws.onopen = () => {
setIsConnected(true)
}
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
setMessages(prev => […prev, message])
}
ws.onclose = () => {
setIsConnected(false)
// 重连:3 秒后重新连接
reconnectTimer = setTimeout(connect, 3000)
}
ws.onerror = () => {
ws.close()
}
}
connect()
// 清理:关闭连接和重连定时器
return () => {
if (reconnectTimer) clearTimeout(reconnectTimer)
ws.close()
}
}, [])
return (
<View>
<Text>{isConnected ? '已连接' : '未连接'}</Text>
{messages.map((msg, i) => (
<Text key={i}>{msg.text}</Text>
))}
</View>
)
}
状态:messages、isConnected
副作用:建立 WebSocket 连接、接收消息、重连
清理:关闭连接、清除重连定时器
解读: 组件挂载时建立连接,接收消息更新状态,卸载时关闭连接。重连机制用 setTimeout 实现,清理函数清除所有资源。
题目 15(思考题)
为什么说"副作用是 视图 = 函数(状态) 之外的操作"?请从函数纯度的角度分析。
参考答案: 视图 = 函数(状态) 的核心是"函数是纯的"——给定相同状态,函数返回相同 UI 描述。
副作用破坏了函数的纯度:
· 发请求:结果不确定,依赖网络
· 定时器:随时间变化,不是状态的函数
· 订阅:外部事件驱动,不由状态决定
· 写日志:修改外部状态
从函数纯度的角度:
纯函数:
f(x) = y
相同 x 必然返回相同 y
不纯函数:
f(x) = y + 副作用
相同 x 可能返回不同 y(副作用影响)
或者:执行了额外的操作(副作用本身)
从渲染管线角度:
1. 状态变化
2. 框架标记需要重新渲染
3. 重新执行函数,读取状态
4. 得到新的 UI 描述
5. 框架对比新旧描述,如果有差异就通知底层更新
如果第 3 步有副作用:
· 多次执行 → 多次副作用
· 结果不确定 → 框架无法正确对比
· 资源泄漏 → 内存问题
终极结论: 副作用是 视图 = 函数(状态) 之外的操作。渲染函数只负责"根据状态计算 UI 描述",副作用必须隔离到事件回调、副作用 API 或生命周期中。这样保证函数的纯度,保证 视图 = 函数(状态) 的确定性。
九、下节预告
第 7 课:组件通信——父传子、子传父、跨层级通信、状态提升与 Context。




