一、本课目标
学完这一课,你要建立七个认知:
- 组件通信的本质:组件通信是"状态在组件树中流动"的方式。
- 父传子:状态通过 props 向下传。
- 子传父:事件通过回调向上传。
- 跨层级通信:Context / Provide / Environment 让状态跳过中间层。
- 状态提升的边界:什么时候提升,什么时候不提升。
- 组件通信的性能考量:避免不必要的重渲染。
- 实战能力:能设计清晰、可维护、高性能的组件通信。
本课核心线索:
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(简答题)
什么时候应该提升状态?什么时候不应该提升?
参考答案:
应该提升的信号:
- 多个组件需要同一个状态
- 兄弟组件需要通信
- 状态需要跨层级传递
不应该提升的信号:
- 状态只被当前组件使用
- 状态是派生数据
- 状态是 UI 状态(如输入框内容、展开状态)
判断标准:问自己"这个状态被哪些组件使用?"
· 只有当前组件 → 局部状态
· 多个组件 → 提升到最近公共祖先
· 跨层级 → 提升到更高层或 Context
题目 12(代码分析题)
下面代码有什么问题?如何修正?
tsx
// TSX (React Native)
function Parent({ items }) {
return (
<View>
{items.map(item => (
<Child onSelect={() => handleSelect(item.id)} />
))}
</View>
)
}
参考答案: 两个问题:
- 每次渲染创建新函数,导致子组件不必要的重渲染。
- 列表中没有 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 课:状态管理进阶------全局状态、状态管理库、状态持久化、状态同步。