欢迎光临
我们一直在努力

【现代声明式UI学与练】第6课 副作用与生命周期——如何管理副作用、如何与外部系统交互、如何避免副作用陷阱

一、本课目标

学完这一课,你要建立六个认知:

  • 副作用的本质:副作用是"渲染函数之外的操作",是 视图 = 函数(状态) 之外的一切。
  • 副作用与渲染的关系:渲染函数必须纯,副作用必须隔离。
  • 副作用的正确位置:事件回调、副作用 API、生命周期。
  • 生命周期 API 对照:useEffect、LaunchedEffect、task、aboutToAppear。
  • 副作用的常见陷阱:竞态条件、内存泄漏、依赖数组错误。
  • 实战能力:能正确管理副作用,避免常见 bug。
  • 本课核心线索:

    视图 = 函数(状态)
    ↓
    函数必须纯 → 副作用不能放在函数里
    ↓
    副作用隔离到事件、副作用 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 避免竞态条件的方法:

  • 用 cancelled 标志:
  • // 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])

  • 用 AbortController:
  • // 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。

    赞(0)
    未经允许不得转载:171主机测评 » 【现代声明式UI学与练】第6课 副作用与生命周期——如何管理副作用、如何与外部系统交互、如何避免副作用陷阱
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址