【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context

一、本课目标

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

  1. 组件通信的本质:组件通信是"状态在组件树中流动"的方式。
  2. 父传子:状态通过 props 向下传。
  3. 子传父:事件通过回调向上传。
  4. 跨层级通信:Context / Provide / Environment 让状态跳过中间层。
  5. 状态提升的边界:什么时候提升,什么时候不提升。
  6. 组件通信的性能考量:避免不必要的重渲染。
  7. 实战能力:能设计清晰、可维护、高性能的组件通信。

本课核心线索:

txt 复制代码
视图 = 函数(状态)
  ↓
状态在组件树中流动 → 组件通信
  ↓
父传子:props 向下
  ↓
子传父:事件向上
  ↓
跨层级:Context / Provide / Environment
  ↓
状态提升:找到最近公共祖先
  ↓
性能考量:避免不必要的重渲染
  ↓
实战:清晰、可维护、高性能

二、组件通信的本质

2.1 一句话说透

组件通信是"状态在组件树中流动"的方式。

txt 复制代码
组件树:
        App
       /   \
    Header  Content
           /      \
        List      Detail

状态放在 App,如何传给 List 和 Detail?状态放在 List,如何通知 App?

这就是组件通信要解决的问题。

2.2 组件通信的三种方式

txt 复制代码
1. 父传子:状态通过 props 向下传
2. 子传父:事件通过回调向上传
3. 跨层级:Context / Provide / Environment 让状态跳过中间层

三种方式的关系:

txt 复制代码
        ┌──────────────┐
        │   父组件      │
        │  state       │
        └──────┬───────┘
               │ ① props 向下
               ↓
        ┌──────────────┐
        │   子组件      │
        │  只读 props   │
        └──────┬───────┘
               │ ② 事件向上
               ↓
        ┌──────────────┐
        │   父组件      │
        │  改 state     │
        └──────────────┘

跨层级:③ Context 跳过中间层

2.3 单向数据流回顾

第 3 课讲过:单向数据流是 视图 = 函数(状态) 的必然要求。

txt 复制代码
状态从父到子(props),事件从子到父(回调)
  → 状态来源可追踪
  → 组件可预测
  → 易于调试

组件通信就是单向数据流的具体实现。

2.4 各框架组件通信对照

框架 父传子 子传父 跨层级

React Native props 回调 prop Context

SwiftUI let / @Binding 闭包 @Environment

ArkUI @Prop / @Link 回调 @Provide / @Consume

Compose 参数 lambda CompositionLocal

Flutter 构造函数参数 回调 InheritedWidget

关键结论: 五种框架的组件通信本质相同------父传子、子传父、跨层级。差异只在 API。


三、父传子:props 向下传

3.1 基本写法

父组件持有状态,通过 props 传给子组件。

React Native:

tsx 复制代码
// TSX (React Native)
// 父组件
function Parent() {
  const [user, setUser] = useState({ name: 'Alice', age: 30 })

  return (
    // 通过 props 向下传
    <Child name={user.name} age={user.age} />
  )
}

// 子组件
function Child({ name, age }) {
  // 只读 props
  return (
    <View>
      <Text>Name: {name}</Text>
      <Text>Age: {age}</Text>
    </View>
  )
}

SwiftUI:

swift 复制代码
// Swift (SwiftUI)
// 父组件
struct Parent: View {
    @State private var user = User(name: "Alice", age: 30)

    var body: some View {
        // 通过 let 字段向下传
        Child(name: user.name, age: user.age)
    }
}

// 子组件
struct Child: View {
    let name: String  // 只读 props
    let age: Int

    var body: some View {
        VStack {
            Text("Name: \(name)")
            Text("Age: \(age)")
        }
    }
}

ArkUI:

ts 复制代码
// ArkTS (ArkUI)
// 父组件
@Component
struct Parent {
  @State user: User = { name: 'Alice', age: 30 }

  build() {
    // 通过 @Prop 向下传
    Child({ name: this.user.name, age: this.user.age })
  }
}

// 子组件
@Component
struct Child {
  @Prop name: string  // 只读 props
  @Prop age: number

  build() {
    Column() {
      Text(`Name: ${this.name}`)
      Text(`Age: ${this.age}`)
    }
  }
}

Compose:

kotlin 复制代码
// Kotlin (Jetpack Compose)
// 父组件
@Composable
fun Parent() {
    val user = remember { mutableStateOf(User("Alice", 30)) }

    // 通过参数向下传
    Child(name = user.value.name, age = user.value.age)
}

// 子组件
@Composable
fun Child(name: String, age: Int) {
    // 只读参数
    Column {
        Text("Name: $name")
        Text("Age: $age")
    }
}

Flutter:

dart 复制代码
// Dart (Flutter)
// 父组件
class Parent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final user = User(name: 'Alice', age: 30);
    return Child(name: user.name, age: user.age);
  }
}

// 子组件
class Child extends StatelessWidget {
  final String name;  // 只读 props
  final int age;
  const Child({required this.name, required this.age});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Name: $name'),
        Text('Age: $age'),
      ],
    );
  }
}

3.2 props 的三个原则

原则一:props 是只读的。

tsx 复制代码
// TSX (React Native)
// 错误:修改 props
function Child({ user }) {
  user.name = 'New'  // 破坏单向数据流
  return <Text>{user.name}</Text>
}

// 正确:只读 props
function Child({ user }) {
  return <Text>{user.name}</Text>
}

原则二:props 向下传,不向上传。

txt 复制代码
父组件 → props → 子组件
子组件 → props → 孙组件
  → props 只能向下传

原则三:props 变化触发子组件重新渲染。

tsx 复制代码
// TSX (React Native)
function Parent() {
  const [count, setCount] = useState(0)
  return <Child count={count} />  // count 变化,Child 重新渲染
}

3.3 props 的传递方式

方式一:直接传递。

tsx 复制代码
// TSX (React Native)
<Child name={user.name} age={user.age} />

方式二:展开传递。

tsx 复制代码
// TSX (React Native)
<Child {...user} />  // 等价于 <Child name={user.name} age={user.age} />

方式三:传递整个对象。

tsx 复制代码
// TSX (React Native)
<Child user={user} />  // 子组件接收整个 user 对象

选择原则:

txt 复制代码
1. 子组件需要哪些字段就传哪些字段
   → 避免传递不需要的字段

2. 如果子组件需要大部分字段
   → 传递整个对象

3. 展开传递要谨慎
   → 容易传递不需要的字段

3.4 props 的默认值

React Native:

tsx 复制代码
// TSX (React Native)
function Greeting({ name = 'Guest', age = 0 }) {
  return <Text>Hello, {name} ({age})</Text>
}

SwiftUI:

swift 复制代码
// Swift (SwiftUI)
struct Greeting: View {
    var name: String = "Guest"  // 默认值
    var age: Int = 0

    var body: some View {
        Text("Hello, \(name) (\(age))")
    }
}

ArkUI:

ts 复制代码
// ArkTS (ArkUI)
@Component
struct Greeting {
  @Prop name: string = 'Guest'  // 默认值
  @Prop age: number = 0

  build() {
    Text(`Hello, ${this.name} (${this.age})`)
  }
}

Compose:

kotlin 复制代码
// Kotlin (Jetpack Compose)
@Composable
fun Greeting(name: String = "Guest", age: Int = 0) {
    Text("Hello, $name ($age)")
}

Flutter:

dart 复制代码
// Dart (Flutter)
class Greeting extends StatelessWidget {
  final String name;
  final int age;
  const Greeting({this.name = 'Guest', this.age = 0});

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $name ($age)');
  }
}

3.5 各框架父传子对照

框架 props 机制 默认值 只读

React Native 函数参数 = default 是

SwiftUI let 字段 = default 是

ArkUI @Prop = default 是

Compose 函数参数 = default 是

Flutter 构造函数参数 = default 是


四、子传父:事件向上传

4.1 基本写法

父组件通过回调 prop 传给子组件,子组件调用回调通知父组件。

React Native:

tsx 复制代码
// TSX (React Native)
// 父组件
function Parent() {
  const [count, setCount] = useState(0)

  // 回调函数
  const handleIncrement = () => setCount(count + 1)

  return (
    // 回调通过 props 向下传
    <Child count={count} onIncrement={handleIncrement} />
  )
}

// 子组件
function Child({ count, onIncrement }) {
  return (
    <View>
      <Text>Count: {count}</Text>
      {/* 调用回调,通知父组件 */}
      <Button title="+1" onPress={onIncrement} />
    </View>
  )
}

SwiftUI:

swift 复制代码
// Swift (SwiftUI)
// 父组件
struct Parent: View {
    @State private var count = 0

    var body: some View {
        Child(
            count: count,
            onIncrement: { count += 1 }  // 闭包作为回调
        )
    }
}

// 子组件
struct Child: View {
    let count: Int
    let onIncrement: () -> Void  // 闭包

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("+1") {
                onIncrement()  // 调用闭包
            }
        }
    }
}

ArkUI:

ts 复制代码
// ArkTS (ArkUI)
// 父组件
@Component
struct Parent {
  @State count: number = 0

  build() {
    Child({
      count: this.count,
      onIncrement: () => { this.count++ }  // 回调
    })
  }
}

// 子组件
@Component
struct Child {
  @Prop count: number
  onIncrement: () => void = () => {}  // 回调

  build() {
    Column() {
      Text(`Count: ${this.count}`)
      Button('+1').onClick(() => {
        this.onIncrement()  // 调用回调
      })
    }
  }
}

Compose:

kotlin 复制代码
// Kotlin (Jetpack Compose)
// 父组件
@Composable
fun Parent() {
    var count by remember { mutableStateOf(0) }

    Child(
        count = count,
        onIncrement = { count++ }  // lambda 作为回调
    )
}

// 子组件
@Composable
fun Child(count: Int, onIncrement: () -> Unit) {
    Column {
        Text("Count: $count")
        Button(onClick = onIncrement) {  // 调用 lambda
            Text("+1")
        }
    }
}

Flutter:

dart 复制代码
// Dart (Flutter)
// 父组件
class Parent extends StatefulWidget {
  @override
  State<Parent> createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Child(
      count: count,
      onIncrement: () => setState(() => count++),  // 回调
    );
  }
}

// 子组件
class Child extends StatelessWidget {
  final int count;
  final VoidCallback onIncrement;  // 回调

  const Child({required this.count, required this.onIncrement});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: onIncrement,  // 调用回调
          child: Text('+1'),
        ),
      ],
    );
  }
}

4.2 回调的三个原则

原则一:回调是 props,子组件只调用,不修改。

tsx 复制代码
// TSX (React Native)
function Child({ onIncrement }) {
  // 正确:调用回调
  return <Button title="+1" onPress={onIncrement} />

  // 错误:修改回调
  // onIncrement = () => {}  // 修改 props
}

原则二:回调由父组件定义,决定状态如何变化。

tsx 复制代码
// TSX (React Native)
// 父组件决定状态如何变化
<Child onIncrement={() => setCount(count + 1)} />
<Child onIncrement={() => setCount(count + 10)} />
<Child onIncrement={() => setCount(0)} />

原则三:回调可以传递参数。

tsx 复制代码
// TSX (React Native)
// 回调传递参数
<Child onSelect={(id) => setSelectedId(id)} />

function Child({ onSelect }) {
  return (
    <View>
      {items.map(item => (
        <Button
          key={item.id}
          title={item.name}
          onPress={() => onSelect(item.id)}  // 传递参数
        />
      ))}
    </View>
  )
}

4.3 回调的命名约定

常见命名:

txt 复制代码
on + 事件名:
  onClick、onPress、onChange、onSelect
  onIncrement、onDecrement、onSubmit

on + 名词 + 动词:
  onUserSelect、onItemDelete、onFormSubmit

命名原则:

txt 复制代码
1. 用 on 开头,表示事件
2. 用动词或动词短语,描述事件
3. 避免用 handle 开头(handle 是父组件的函数名)

4.4 回调的传递方式

方式一:直接传递函数引用。

tsx 复制代码
// TSX (React Native)
const handleIncrement = () => setCount(count + 1)
<Child onIncrement={handleIncrement} />

方式二:内联箭头函数。

tsx 复制代码
// TSX (React Native)
<Child onIncrement={() => setCount(count + 1)} />

方式三:传递参数化回调。

tsx 复制代码
// TSX (React Native)
<Child onSelect={(id) => setSelectedId(id)} />

选择原则:

txt 复制代码
1. 简单回调 → 内联箭头函数
2. 复杂回调 → 提取为函数
3. 参数化回调 → 内联箭头函数或 useCallback

4.5 各框架子传父对照

框架 回调机制 常见命名

React Native 回调 prop onPress、onChange

SwiftUI 闭包 action、闭包参数

ArkUI 回调 onClick、onChange

Compose lambda onClick、onValueChange

Flutter VoidCallback onPressed、onChanged


五、跨层级通信

5.1 为什么需要跨层级通信

问题: 状态需要从祖先传到深层后代,中间层不需要这个状态。

txt 复制代码
        App(持有 theme 状态)
         │
        Header(不需要 theme)
         │
        Content(不需要 theme)
         │
        Button(需要 theme)

用 props 传递: 中间层必须接收并透传 props,即使它们不需要。

tsx 复制代码
// TSX (React Native)
// 问题:中间层必须透传 props
function App() {
  const [theme, setTheme] = useState('light')
  return <Header theme={theme} />
}

function Header({ theme }) {
  return <Content theme={theme} />  // 透传
}

function Content({ theme }) {
  return <Button theme={theme} />  // 透传
}

function Button({ theme }) {
  return <Text>Theme: {theme}</Text>
}

问题: props drilling(props 逐层透传),中间层耦合了不需要的 props。

5.2 Context 解决方案

Context 让状态跳过中间层,直接传给需要的后代。

React Native:

tsx 复制代码
// TSX (React Native)
// 1. 创建 Context
const ThemeContext = React.createContext('light')

// 2. 祖先提供状态
function App() {
  const [theme, setTheme] = useState('light')

  return (
    <ThemeContext.Provider value={theme}>
      <Header />
    </ThemeContext.Provider>
  )
}

// 3. 中间层不需要透传
function Header() {
  return <Content />
}

function Content() {
  return <Button />
}

// 4. 后代直接读取
function Button() {
  const theme = useContext(ThemeContext)
  return <Text>Theme: {theme}</Text>
}

SwiftUI:

swift 复制代码
// Swift (SwiftUI)
// 1. 定义 EnvironmentKey
private struct ThemeKey: EnvironmentKey {
    static let defaultValue = "light"
}

extension EnvironmentValues {
    var theme: String {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

// 2. 祖先提供状态
struct App: View {
    @State private var theme = "light"

    var body: some View {
        Header()
            .environment(\.theme, theme)
    }
}

// 3. 中间层不需要透传
struct Header: View {
    var body: some View { Content() }
}

struct Content: View {
    var body: some View { Button() }
}

// 4. 后代直接读取
struct Button: View {
    @Environment(\.theme) var theme: String

    var body: some View {
        Text("Theme: \(theme)")
    }
}

ArkUI:

ts 复制代码
// ArkTS (ArkUI)
// 1. 祖先提供状态
@Component
struct App {
  @Provide('theme') theme: string = 'light'

  build() {
    Header()
  }
}

// 2. 中间层不需要透传
@Component
struct Header {
  build() {
    Content()
  }
}

@Component
struct Content {
  build() {
    Button()
  }
}

// 3. 后代直接读取
@Component
struct Button {
  @Consume('theme') theme: string

  build() {
    Text(`Theme: ${this.theme}`)
  }
}

Compose:

kotlin 复制代码
// Kotlin (Jetpack Compose)
// 1. 创建 CompositionLocal
val LocalTheme = compositionLocalOf { "light" }

// 2. 祖先提供状态
@Composable
fun App() {
    var theme by remember { mutableStateOf("light") }

    CompositionLocalProvider(LocalTheme provides theme) {
        Header()
    }
}

// 3. 中间层不需要透传
@Composable
fun Header() {
    Content()
}

@Composable
fun Content() {
    Button()
}

// 4. 后代直接读取
@Composable
fun Button() {
    val theme = LocalTheme.current
    Text("Theme: $theme")
}

Flutter:

dart 复制代码
// Dart (Flutter)
// 1. 创建 InheritedWidget
class ThemeProvider extends InheritedWidget {
  final String theme;

  const ThemeProvider({
    required this.theme,
    required super.child,
  });

  static ThemeProvider of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
  }

  @override
  bool updateShouldNotify(ThemeProvider oldWidget) {
    return theme != oldWidget.theme;
  }
}

// 2. 祖先提供状态
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ThemeProvider(
      theme: 'light',
      child: Header(),
    );
  }
}

// 3. 中间层不需要透传
class Header extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Content();
}

class Content extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Button();
}

// 4. 后代直接读取
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final theme = ThemeProvider.of(context).theme;
    return Text('Theme: $theme');
  }
}

5.3 Context 的适用场景

适合用 Context 的场景:

txt 复制代码
1. 主题:light / dark
2. 语言:中文 / 英文
3. 用户信息:当前登录用户
4. 权限:当前用户权限
5. 全局配置:API 地址、超时时间

不适合用 Context 的场景:

txt 复制代码
1. 局部状态:只有几个组件用
2. 频繁变化的状态:Context 变化触发所有消费者重渲染
3. 业务状态:应该用状态管理库

5.4 Context 的性能考量

问题: Context 变化触发所有消费者重渲染。

tsx 复制代码
// TSX (React Native)
// 问题:theme 变化触发所有消费者重渲染
const ThemeContext = React.createContext('light')
const UserContext = React.createContext(null)

function App() {
  const [theme, setTheme] = useState('light')
  const [user, setUser] = useState(null)

  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={user}>
        <Content />
      </UserContext.Provider>
    </ThemeContext.Provider>
  )
}

优化: 拆分 Context,让不同状态独立。

tsx 复制代码
// TSX (React Native)
// 优化:拆分 Context
<ThemeContext.Provider value={theme}>
  <Content />
</ThemeContext.Provider>

// 另一个地方
<UserContext.Provider value={user}>
  <Content />
</UserContext.Provider>

优化原则:

txt 复制代码
1. 按变化频率拆分 Context
   → 频繁变化的和稳定的分开

2. 按使用范围拆分 Context
   → 不同组件用的分开

3. 避免在 Context 中放频繁变化的大对象
   → 拆分为多个小 Context

5.5 各框架跨层级通信对照

框架 跨层级机制 读取方式 性能优化

React Native Context useContext 拆分 Context

SwiftUI Environment @Environment 自动优化

ArkUI @Provide / @Consume @Consume 自动优化

Compose CompositionLocal LocalX.current 自动优化

Flutter InheritedWidget X.of(context) 自动优化


六、状态提升的边界

6.1 什么时候提升状态

状态提升的三个信号:

txt 复制代码
1. 多个组件需要同一个状态
   → 提升到最近公共祖先

2. 兄弟组件需要通信
   → 提升到父组件

3. 状态需要跨层级传递
   → 提升到更高层,或用 Context

6.2 什么时候不提升状态

不提升的三个信号:

txt 复制代码
1. 状态只被当前组件使用
   → 保持在局部

2. 状态是派生数据
   → 直接算,不提升

3. 状态是 UI 状态
   → 保持在局部(如输入框内容、展开状态)

6.3 状态提升的边界

示例:搜索框和结果列表。

tsx 复制代码
// TSX (React Native)
// 方案一:状态提升到父组件
function SearchPage() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  useEffect(() => {
    if (query) {
      fetch(`/api/search?q=${query}`).then(setResults)
    }
  }, [query])

  return (
    <View>
      <SearchBar value={query} onChange={setQuery} />
      <ResultList results={results} />
    </View>
  )
}

分析: query 被 SearchBar 和 useEffect 使用,提升到 SearchPage 是正确的。

tsx 复制代码
// TSX (React Native)
// 方案二:状态保持在局部
function SearchBar() {
  const [query, setQuery] = useState('')
  // query 只被 SearchBar 使用
  return <TextInput value={query} onChangeText={setQuery} />
}

分析: 如果 query 只被 SearchBar 使用,保持在局部即可。

判断标准:

txt 复制代码
问:这个状态被哪些组件使用?
  → 只有当前组件 → 局部状态
  → 多个组件 → 提升到最近公共祖先
  → 跨层级 → 提升到更高层或 Context

6.4 状态提升的粒度

问题: 状态提升粒度太粗,导致不必要的重渲染。

tsx 复制代码
// TSX (React Native)
// 问题:整个 SearchPage 重渲染
function SearchPage() {
  const [query, setQuery] = useState('')

  return (
    <View>
      <Header />
      <SearchBar value={query} onChange={setQuery} />
      <ResultList query={query} />
      <Footer />
    </View>
  )
}

优化: 把状态放在更小的组件中。

tsx 复制代码
// TSX (React Native)
// 优化:把搜索逻辑封装到一个组件中
function SearchPage() {
  return (
    <View>
      <Header />
      <SearchSection />  {/* 状态在这里 */}
      <Footer />
    </View>
  )
}

function SearchSection() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  useEffect(() => {
    if (query) {
      fetch(`/api/search?q=${query}`).then(setResults)
    }
  }, [query])

  return (
    <View>
      <SearchBar value={query} onChange={setQuery} />
      <ResultList results={results} />
    </View>
  )
}

优化原则:

txt 复制代码
1. 把相关的状态和 UI 封装到一个组件中
2. 避免状态提升到无关组件所在层级
3. 用组件封装代替状态提升

6.5 各框架状态提升对照

框架 状态提升机制 边界判断

React Native useState + props 最近公共祖先

SwiftUI @State + @Binding 最近公共祖先

ArkUI @State + @Link 最近公共祖先

Compose remember + 参数 最近公共祖先

Flutter State + 构造函数 最近公共祖先


七、组件通信的性能考量

7.1 props 变化触发重渲染

问题: props 变化触发子组件重新渲染。

tsx 复制代码
// TSX (React Native)
function Parent() {
  const [count, setCount] = useState(0)
  return <Child count={count} />  // count 变化,Child 重新渲染
}

优化: 用 React.memo 避免不必要的重渲染。

tsx 复制代码
// TSX (React Native)
const Child = React.memo(function Child({ count }) {
  return <Text>Count: {count}</Text>
})

7.2 内联对象和函数导致重渲染

问题: 每次渲染创建新对象/函数,导致子组件不必要的重渲染。

tsx 复制代码
// TSX (React Native)
// 问题:每次渲染创建新对象和函数
function Parent() {
  return (
    <Child
      style={{ color: 'red' }}  // 新对象
      onPress={() => {}}        // 新函数
    />
  )
}

优化: 提取到组件外部或使用 useCallback。

tsx 复制代码
// TSX (React Native)
const styles = { color: 'red' }

function Parent() {
  const handlePress = useCallback(() => {}, [])
  return <Child style={styles} onPress={handlePress} />
}

7.3 回调传递参数导致重渲染

问题: 参数化回调每次渲染创建新函数。

tsx 复制代码
// TSX (React Native)
// 问题:每次渲染创建新函数
function Parent({ items }) {
  return (
    <View>
      {items.map(item => (
        <Child onSelect={() => handleSelect(item.id)} />
      ))}
    </View>
  )
}

优化: 传递 id,让子组件调用回调时传参。

tsx 复制代码
// TSX (React Native)
function Parent({ items }) {
  const handleSelect = useCallback((id) => {
    // 处理选择
  }, [])

  return (
    <View>
      {items.map(item => (
        <Child key={item.id} id={item.id} onSelect={handleSelect} />
      ))}
    </View>
  )
}

function Child({ id, onSelect }) {
  return <Button title="选择" onPress={() => onSelect(id)} />
}

7.4 Context 变化触发所有消费者重渲染

问题: Context 变化触发所有消费者重渲染。

tsx 复制代码
// TSX (React Native)
// 问题:theme 变化触发所有消费者重渲染
const ThemeContext = React.createContext('light')

function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={theme}>
      <Content />  {/* 所有消费者重渲染 */}
    </ThemeContext.Provider>
  )
}

优化: 拆分 Context,只传递需要的状态。

tsx 复制代码
// TSX (React Native)
// 优化:只传递 theme
const ThemeContext = React.createContext('light')

function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={theme}>
      <ThemeConsumer />  {/* 只有 ThemeConsumer 重渲染 */}
    </ThemeContext.Provider>
  )
}

7.5 组件通信性能检查清单

txt 复制代码
组件通信性能检查清单:

1. 子组件是否使用了 memo/equatable?
   → 避免 props 变化时的重渲染

2. 内联对象和函数是否提取到组件外部?
   → 避免每次渲染创建新对象/函数

3. 回调是否传递参数?
   → 传递 id,让子组件调用时传参

4. Context 是否拆分?
   → 按变化频率和使用范围拆分

5. 状态提升粒度是否合适?
   → 封装到相关组件中,避免无关组件重渲染

6. 是否使用了不可变更新?
   → 状态更新创建新对象,不修改原对象

7.6 各框架性能优化对照

框架 避免重渲染 稳定引用 拆分 Context

React Native React.memo useCallback、useMemo 拆分 Context

SwiftUI EquatableView 自动 自动优化

ArkUI @Reusable 手动 自动优化

Compose 自动跳过 remember 自动优化

Flutter const 手动 自动优化


八、常见误区

8.1 直接修改 props

tsx 复制代码
// TSX (React Native)
// 错误:修改 props
function Child({ user }) {
  user.name = 'New'  // 破坏单向数据流
  return <Text>{user.name}</Text>
}

// 正确:只读 props
function Child({ user }) {
  return <Text>{user.name}</Text>
}

8.2 在子组件中改父组件状态

tsx 复制代码
// TSX (React Native)
// 错误:子组件直接改父组件状态
function Parent() {
  const [count, setCount] = useState(0)
  return <Child setCount={setCount} />
}

function Child({ setCount }) {
  return <Button title="+1" onPress={() => setCount(1)} />  // 子组件决定状态变化
}

// 正确:父组件决定状态变化
function Parent() {
  const [count, setCount] = useState(0)
  return <Child onIncrement={() => setCount(count + 1)} />
}

function Child({ onIncrement }) {
  return <Button title="+1" onPress={onIncrement} />
}

8.3 props drilling

tsx 复制代码
// TSX (React Native)
// 错误:props 逐层透传
function App() {
  const [theme, setTheme] = useState('light')
  return <Header theme={theme} />
}

function Header({ theme }) {
  return <Content theme={theme} />  // 透传
}

function Content({ theme }) {
  return <Button theme={theme} />  // 透传
}

function Button({ theme }) {
  return <Text>Theme: {theme}</Text>
}

// 正确:用 Context
const ThemeContext = React.createContext('light')

function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={theme}>
      <Header />
    </ThemeContext.Provider>
  )
}

function Header() {
  return <Content />
}

function Content() {
  return <Button />
}

function Button() {
  const theme = useContext(ThemeContext)
  return <Text>Theme: {theme}</Text>
}

8.4 状态提升过度

tsx 复制代码
// TSX (React Native)
// 错误:状态提升到 App
function App() {
  const [input, setInput] = useState('')
  return (
    <View>
      <Header />
      <Content />
      <InputBox value={input} onChange={setInput} />
    </View>
  )
}

// 正确:状态放在 InputBox 内部
function App() {
  return (
    <View>
      <Header />
      <Content />
      <InputBox />
    </View>
  )
}

function InputBox() {
  const [input, setInput] = useState('')
  return <TextInput value={input} onChangeText={setInput} />
}

8.5 Context 放太多状态

tsx 复制代码
// TSX (React Native)
// 错误:Context 放太多状态
const AppContext = React.createContext({
  theme: 'light',
  user: null,
  language: 'zh',
  notifications: [],
  // ... 很多状态
})

// 正确:按变化频率拆分
const ThemeContext = React.createContext('light')
const UserContext = React.createContext(null)
const LanguageContext = React.createContext('zh')

8.6 回调中引用旧状态

tsx 复制代码
// TSX (React Native)
// 错误:回调中引用旧状态
function Parent() {
  const [count, setCount] = useState(0)

  const handleIncrement = () => {
    setCount(count + 1)  // 引用创建时的 count
  }

  return <Child onIncrement={handleIncrement} />
}

// 正确:用函数式更新
function Parent() {
  const [count, setCount] = useState(0)

  const handleIncrement = () => {
    setCount(c => c + 1)  // 用最新值
  }

  return <Child onIncrement={handleIncrement} />
}

8.7 忘记给回调加 key

tsx 复制代码
// TSX (React Native)
// 错误:列表中的回调没有 key
{items.map(item => (
  <Child onSelect={() => handleSelect(item.id)} />  // 没有 key
))}

// 正确:加 key
{items.map(item => (
  <Child key={item.id} onSelect={() => handleSelect(item.id)} />
))}

九、习题(15 道)

题目 1(选择题)

组件通信的本质是什么?

A. 手动操作控件

B. 状态在组件树中流动的方式

C. 命令式更新界面

D. 循环创建视图

参考答案:B

解读: 组件通信是"状态在组件树中流动"的方式。父传子用 props,子传父用回调,跨层级用 Context。


题目 2(判断题)

props 是只读的,子组件不能修改 props。

参考答案:正确

解读: props 是只读的。修改 props 破坏单向数据流,父组件不知道数据变了,不会重新渲染。


题目 3(填空题)

组件通信的三种方式是:、、______。

参考答案:父传子(props 向下传);子传父(事件向上传);跨层级(Context / Provide / Environment)

解读: 三种方式对应单向数据流的不同场景:父传子用 props,子传父用回调,跨层级用 Context。


题目 4(简答题)

什么是 props drilling?如何解决?

参考答案: props drilling 是状态需要从祖先传到深层后代,中间层必须接收并透传 props,即使中间层不需要这个状态。

问题:

· 中间层耦合了不需要的 props

· 中间层变化时需要修改所有透传路径

· 代码冗余,难以维护

解决方案:用 Context(React)、Environment(SwiftUI)、Provide/Consume(ArkUI)、CompositionLocal(Compose)、InheritedWidget(Flutter)。

Context 让状态跳过中间层,直接传给需要的后代。


题目 5(代码分析题)

下面代码有什么问题?如何修正?

tsx 复制代码
// TSX (React Native)
function Child({ user }) {
  user.name = 'New'
  return <Text>{user.name}</Text>
}

参考答案: 子组件修改了 props,破坏了单向数据流。

修正: 只读 props。

tsx 复制代码
// TSX (React Native)
function Child({ user }) {
  return <Text>{user.name}</Text>
}

如果需要修改,通过回调通知父组件。

tsx 复制代码
// TSX (React Native)
function Child({ user, onUpdate }) {
  return (
    <View>
      <Text>{user.name}</Text>
      <Button title="改名" onPress={() => onUpdate('New')} />
    </View>
  )
}

题目 6(代码改错题)

下面代码有什么问题?如何修正?

tsx 复制代码
// TSX (React Native)
function Parent() {
  const [count, setCount] = useState(0)
  return <Child setCount={setCount} />
}

function Child({ setCount }) {
  return <Button title="+1" onPress={() => setCount(1)} />
}

参考答案: 子组件直接改父组件状态,子组件决定了状态如何变化,破坏了单向数据流。

修正: 父组件决定状态如何变化。

tsx 复制代码
// TSX (React Native)
function Parent() {
  const [count, setCount] = useState(0)
  return <Child onIncrement={() => setCount(count + 1)} />
}

function Child({ onIncrement }) {
  return <Button title="+1" onPress={onIncrement} />
}

题目 7(跨框架对比题)

写出 React Native、SwiftUI、Compose 中父传子和子传父的核心写法。

参考答案:

React Native:

tsx 复制代码
// TSX (React Native)
// 父传子
<Child count={count} />
// 子传父
<Child onIncrement={() => setCount(count + 1)} />

SwiftUI:

swift 复制代码
// Swift (SwiftUI)
// 父传子
Child(count: count)
// 子传父
Child(onIncrement: { count += 1 })

Compose:

kotlin 复制代码
// Kotlin (Jetpack Compose)
// 父传子
Child(count = count)
// 子传父
Child(onIncrement = { count++ })

解读: 三种框架的组件通信本质相同------父传子用 props/参数,子传父用回调。差异只在语法。


题目 8(选择题)

以下哪种场景最适合用 Context?

A. 局部状态

B. 兄弟组件通信

C. 主题(light / dark)

D. 列表渲染

参考答案:C

解读: Context 适合跨层级通信的场景,如主题、语言、用户信息、权限、全局配置。局部状态用 useState,兄弟组件通信用状态提升,列表渲染用列表 API。


题目 9(判断题)

Context 变化会触发所有消费者重渲染。

参考答案:正确

解读: Context 变化会触发所有消费者重渲染。所以 Context 应该按变化频率和使用范围拆分,避免不必要的重渲染。


题目 10(填空题)

SwiftUI 中跨层级通信用 ______,ArkUI 中用 ______,Compose 中用 ______。

参考答案:@Environment;@Provide / @Consume;CompositionLocal

解读: 不同框架的跨层级通信 API 不同,但本质相同------让状态跳过中间层,直接传给需要的后代。


题目 11(简答题)

什么时候应该提升状态?什么时候不应该提升?

参考答案:

应该提升的信号:

  1. 多个组件需要同一个状态
  2. 兄弟组件需要通信
  3. 状态需要跨层级传递

不应该提升的信号:

  1. 状态只被当前组件使用
  2. 状态是派生数据
  3. 状态是 UI 状态(如输入框内容、展开状态)

判断标准:问自己"这个状态被哪些组件使用?"

· 只有当前组件 → 局部状态

· 多个组件 → 提升到最近公共祖先

· 跨层级 → 提升到更高层或 Context


题目 12(代码分析题)

下面代码有什么问题?如何修正?

tsx 复制代码
// TSX (React Native)
function Parent({ items }) {
  return (
    <View>
      {items.map(item => (
        <Child onSelect={() => handleSelect(item.id)} />
      ))}
    </View>
  )
}

参考答案: 两个问题:

  1. 每次渲染创建新函数,导致子组件不必要的重渲染。
  2. 列表中没有 key。

修正: 传递 id,让子组件调用时传参。

tsx 复制代码
// TSX (React Native)
function Parent({ items }) {
  const handleSelect = useCallback((id) => {
    // 处理选择
  }, [])

  return (
    <View>
      {items.map(item => (
        <Child key={item.id} id={item.id} onSelect={handleSelect} />
      ))}
    </View>
  )
}

function Child({ id, onSelect }) {
  return <Button title="选择" onPress={() => onSelect(id)} />
}

题目 13(设计题)

设计一个主题切换系统:

· 用户可以在设置页切换主题

· 主题影响所有页面的颜色

· 主题状态跨层级传递

写出状态设计、跨层级通信、性能优化。

参考答案:

tsx 复制代码
// TSX (React Native)
// 1. 创建 Context
const ThemeContext = React.createContext({
  theme: 'light',
  toggleTheme: () => {},
})

// 2. 祖先提供状态
function App() {
  const [theme, setTheme] = useState('light')

  const toggleTheme = useCallback(() => {
    setTheme(t => t === 'light' ? 'dark' : 'light')
  }, [])

  // useMemo 避免每次渲染创建新对象
  const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])

  return (
    <ThemeContext.Provider value={value}>
      <MainNavigator />
    </ThemeContext.Provider>
  )
}

// 3. 中间层不需要透传
function MainNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Settings" component={SettingsScreen} />
    </Stack.Navigator>
  )
}

// 4. 后代直接读取
function HomeScreen() {
  const { theme } = useContext(ThemeContext)
  return <View style={theme === 'light' ? lightStyles : darkStyles}>...</View>
}

function SettingsScreen() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  return (
    <View>
      <Text>当前主题:{theme}</Text>
      <Button title="切换主题" onPress={toggleTheme} />
    </View>
  )
}

状态:theme(在 App 中)

跨层级通信:ThemeContext

性能优化:useCallback、useMemo

解读: 主题状态提升到 App,通过 Context 跨层级传递。useCallback 和 useMemo 避免不必要的重渲染。


题目 14(设计题)

设计一个表单系统:

· 多个字段(姓名、邮箱、密码)

· 父组件管理表单状态

· 子组件负责渲染输入框

· 提交时验证并显示错误

写出状态设计、组件通信、性能优化。

参考答案:

tsx 复制代码
// TSX (React Native)
function FormPage() {
  // 状态提升到父组件
  const [form, setForm] = useState({
    name: '',
    email: '',
    password: '',
  })
  const [errors, setErrors] = useState({})

  // 回调:更新字段
  const updateField = useCallback((field, value) => {
    setForm(prev => ({ ...prev, [field]: value }))
    // 清除该字段的错误
    setErrors(prev => ({ ...prev, [field]: null }))
  }, [])

  // 回调:提交表单
  const handleSubmit = useCallback(() => {
    const newErrors = validate(form)
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors)
      return
    }
    submitForm(form)
  }, [form])

  return (
    <View>
      <FormField
        label="姓名"
        value={form.name}
        error={errors.name}
        onChange={value => updateField('name', value)}
      />
      <FormField
        label="邮箱"
        value={form.email}
        error={errors.email}
        onChange={value => updateField('email', value)}
      />
      <FormField
        label="密码"
        value={form.password}
        error={errors.password}
        onChange={value => updateField('password', value)}
        secureTextEntry
      />
      <Button title="提交" onPress={handleSubmit} />
    </View>
  )
}

// 子组件:纯展示 + 事件向上
const FormField = React.memo(function FormField({ label, value, error, onChange, secureTextEntry }) {
  return (
    <View>
      <Text>{label}</Text>
      <TextInput
        value={value}
        onChangeText={onChange}
        secureTextEntry={secureTextEntry}
      />
      {error && <Text style={{ color: 'red' }}>{error}</Text>}
    </View>
  )
})

状态:form、errors(在 FormPage 中)

组件通信:父传子(value、error)、子传父(onChange)

性能优化:React.memo、useCallback

解读: 表单状态提升到父组件,子组件只负责渲染输入框。通过回调通知父组件更新状态,通过 props 传递值和错误信息。


题目 15(思考题)

为什么说"组件通信是单向数据流的具体实现"?请从 视图 = 函数(状态) 的角度分析。

参考答案: 视图 = 函数(状态) 要求状态是唯一输入源,视图由状态完全决定。

组件通信是状态在组件树中流动的方式:

txt 复制代码
父组件持有状态
  ↓ props 向下
子组件读取状态
  ↑ 事件向上
父组件改状态
  ↓
父组件重新执行函数,返回新 UI 描述
  ↓
子组件拿到新 props,重新执行函数

从单向数据流角度:

· 父传子:状态从父到子,保证子组件读到最新状态

· 子传父:事件从子到父,保证状态只在父组件修改

· 跨层级:Context 让状态跳过中间层,但仍然遵守单向数据流

从 视图 = 函数(状态) 角度:

txt 复制代码
父组件的函数:Parent(state) → <Child props={state} />
子组件的函数:Child(props) → UI 描述
  ↓
状态变化 → 父组件函数重新执行 → 新 props → 子组件函数重新执行 → 新 UI 描述

如果破坏了单向数据流:

· 子组件直接改父组件状态 → 状态来源不唯一 → 视图不可预测

· 多个组件各自持有状态 → 状态不同步 → 视图不确定

· props 双向绑定 → 数据流网状 → 难以追踪

终极结论: 组件通信是单向数据流的具体实现。父传子、子传父、跨层级三种方式,都遵守"状态向下,事件向上"的单向数据流原则。这保证了 视图 = 函数(状态) 的确定性------状态来源唯一,视图由状态完全决定。


十、下节预告

第 8 课:状态管理进阶------全局状态、状态管理库、状态持久化、状态同步。

相关推荐
我命由我123452 小时前
Photoshop - Photoshop 矢量蒙版
学习·职场和发展·产品运营·求职招聘·职场发展·产品经理·photoshop
律宏阔3 小时前
Flutter 列表图片内存优化:CachedNetworkImage 与 ClipRRect 的取舍
前端·flutter
律宏阔3 小时前
Dart 3 Record:解决 Future.wait 异构返回值的类型问题
前端·flutter
大圣编蚕3 小时前
Rancher证书轮换过期导致不能访问UI问题处理
ui·rancher
熊猫钓鱼>_>3 小时前
越顺,越空:当 AI 把学习 “优化“ 到消失
人工智能·学习·ai·llm·agent·ai编程·metaai
可乐ea3 小时前
Agent 评测换判据:用终态数据库状态判定任务是否完成
数据库·ui·oracle·mcp工具·agent评测·终态断言·智能体可靠性
一条破秋裤3 小时前
02_注册设备号_从主次设备号到成对注销
学习
@codercjw4 小时前
企业实战电机和传感器选型
学习
zhangrelay5 小时前
用ROS2Lyrical实验报告册思路重做蓝桥云课ROS1实验并改进
linux·笔记·学习·ubuntu·机器人