开篇:第1课留下的一句话

第1课里,我们把 React Native 的核心思想压缩成了一句话:

// 概念表达
视图 = 函数(状态)                                                    // 视图是状态经过函数计算出来的结果

这句话听起来简单,却是 React Native 编程方式的总开关。理解它,很多写法会变得顺理成章;不理解它,就容易用命令式的习惯去写声明式的代码,越写越别扭。

这一课,我们就把这句话拆开、讲透,并看它在 React Native 中到底怎么体现。


一、从一个最小例子说起

先看一个计数器:

// JavaScript + React Native
import { useState } from 'react';                                   // 从 React 引入 useState,用来在函数组件中创建状态
import { View, Text, Button } from 'react-native';                  // 从 React Native 引入 View、Text、Button 三个组件

export default function App() {                                     // 定义函数组件 App,并默认导出
  const [count, setCount] = useState(0);                            // 创建状态 count,初始值 0;setCount 用来更新它

  return (                                                          // 返回界面描述(JSX)
    <View>                                                          // 外层容器
      <Text>你点击了 {count} 次</Text>                               // 文本内容绑定 count,count 变文字就变
      <Button title="点我" onPress={() => setCount(count + 1)} />   // 点击按钮时更新 count,界面会自动重新渲染
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

运行后,屏幕显示“你点击了 0 次”。点一下按钮,变成“你点击了 1 次”。再点,变成 2 次。

问一个问题:是谁把屏幕上的“0”改成了“1”?

你可能会说:是 setCount 改的。再追问:setCount 怎么改的?它找到了那个 <Text>,然后把里面的文字换掉了吗?

不是。你没有写任何“找到 Text、修改文字”的代码。你只做了一件事:更新 count。然后界面自己变了。

这就是声明式 UI 的第一个关键点:

你不直接改界面,你改状态;界面由状态计算出来。


二、声明式和命令式,到底差在哪

2.1 命令式:一步步指挥

还是计数器。用命令式思路写,大概是这样:

// JavaScript + React Native
import { useRef } from 'react';                                     // 引入 useRef,用来创建跨渲染保持不变的引用
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button 三个组件

function App() {                                                    // 定义函数组件 App
  const textRef = useRef(null);                                     // 创建 ref,初始为 null,之后指向页面上的 Text
  const countRef = useRef(0);                                       // 创建 ref,初始为 0,用来保存当前计数

  const handlePress = () => {                                       // 定义按钮点击处理函数
    countRef.current = countRef.current + 1;                        // 手动把 countRef 中的数字加一
    textRef.current.setNativeProps({                                // 通过 ref 找到 Text,直接修改它的原生属性
      text: `你点击了 ${countRef.current} 次`,                        // 把 Text 的文字替换成新的计数文字
    });                                                             // 结束 setNativeProps 调用
  };                                                                // 结束 handlePress 函数

  return (                                                          // 返回界面描述
    <View>                                                          // 最外层容器
      <Text ref={textRef}>你点击了 0 次</Text>                        // 文本组件,用 ref 关联,初始显示 0 次
      <Button title="点我" onPress={handlePress} />                 // 按钮组件,点击时执行 handlePress
    </View>                                                         // 结束最外层容器
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

命令式写法关心“过程”:用 countRef 记住数字,按钮点击时加一,用 textRef 找到 Text,手动把内容改掉。每一步都要你亲自指挥。

2.2 声明式:描述结果

声明式写法关心“结果”:

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState,用来创建状态
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button 组件

function App() {                                                    // 定义函数组件 App
  const [count, setCount] = useState(0);                            // 创建状态 count,初始值 0

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>你点击了 {count} 次</Text>                               // 文本内容直接绑定 count
      <Button title="点我" onPress={() => setCount(count + 1)} />   // 点击按钮时更新 count
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

你只描述:当 count 是 0 时,界面显示什么;当 count 是 1 时,界面显示什么。至于怎么从旧界面变到新界面,交给 React Native。

2.3 核心区别

一句话总结:

  • 命令式:我告诉你每一步怎么做;
  • 声明式:我告诉你结果是什么样,过程你自己想办法。

这里有一个容易混淆的点。声明式并不是“没有过程”,而是“过程被框架封装了”。你依然需要理解框架是怎么工作的,只是不需要手动写每一步。

2.4 一个更直观的类比

想象你在餐厅点菜。

命令式:你走进厨房,告诉厨师先切菜、再热锅、再放油、再炒三分钟、再放盐、再装盘。

声明式:你看菜单,点一份“宫保鸡丁”。至于厨师怎么做,你不用管。

React Native 就是那个厨师。你告诉它界面应该长什么样,它负责把界面做出来。


三、拆解“视图 = 函数(状态)”

这句话有三个关键词:状态、函数、视图。

3.1 状态:会变的数据

状态就是组件内部会变化的数据。函数组件里最常见的是 useState:

// JavaScript + React Native
const [count, setCount] = useState(0);                              // 创建状态 count,初始值 0;setCount 是更新函数

count 是当前状态值,setCount 是更新函数,初始值是 0。

状态可以是数字、字符串、布尔值、对象、数组:

// JavaScript + React Native
const [name, setName] = useState('小明');                            // 字符串状态
const [isLoggedIn, setIsLoggedIn] = useState(false);                 // 布尔状态
const [todos, setTodos] = useState([]);                              // 数组状态
const [user, setUser] = useState({ name: '', age: 0 });              // 对象状态

状态的特点:它会变,变了之后界面应该跟着变。

这里有一个关键点:状态不是普通变量。普通变量变了,界面不会重新渲染。只有通过 set 函数更新的状态,才会触发重新渲染。

3.2 函数:组件就是函数

在 React Native 中,一个函数组件就是一个普通 JavaScript 函数:

// JavaScript + React Native
import { Text } from 'react-native';                                // 引入 Text 组件

function App() {                                                    // 定义函数组件 App
  return <Text>你好</Text>;                                         // 返回一个 Text 组件
}                                                                   // 结束 App 组件

它接收输入,返回输出。输入是 props、state 等数据,输出是 JSX,也就是界面描述。

可以把它想象成一个计算过程:

// 概念表达
输入:状态                                                           // 状态是函数的输入
计算:组件函数                                                       // 组件函数是计算过程
输出:界面描述                                                       // 界面描述是函数的输出

3.3 视图:JSX 是界面描述

组件返回的 JSX,看起来像 HTML,但它不是真正的原生视图,而是一份“描述”:

// JavaScript + React Native (JSX)
<Text>你点击了 {count} 次</Text>                                     // 描述:这里要放一个 Text,内容是“你点击了 count 次”

这份描述告诉 React Native:这里要放一个 Text,内容是“你点击了 count 次”。

React Native 拿到描述后,再把它变成 Android 或 iOS 上的原生控件。所以,视图不是“你手动创建出来的对象”,而是“函数根据状态算出来的结果”。

3.4 等号:不是赋值,是“由……决定”

“视图 = 函数(状态)”里的等号,不是 JavaScript 的赋值。它的意思是:

视图由函数和状态共同决定。

状态变,函数重新执行,视图重新计算。你不需要手动改视图,视图是算出来的。

换个说法:状态是自变量,函数是映射关系,视图是因变量。

3.5 一个容易忽略的细节

你可能会问:函数每次重新执行,是不是意味着整个界面都要重新创建一遍?

不是。React Native 会做一件事:比较新旧界面描述,找出差异,只更新变化的部分。这个过程叫“协调”或“diff”。

所以,虽然你写的是“整个界面重新计算”,但实际运行时,框架只更新必要的部分。这是 React Native 性能的关键。


四、在 React Native 中的具体体现

4.1 函数组件就是函数

// JavaScript + React Native
import { Text } from 'react-native';                                // 引入 Text 组件

function Hello() {                                                  // 定义函数组件 Hello
  return <Text>你好,React Native</Text>;                            // 返回一个 Text 组件
}                                                                   // 结束 Hello 组件

Hello 是一个函数。它没有继承类,也没有手动创建原生视图,只是返回了一段界面描述。这就是“函数”这一部分的体现:组件是函数。

4.2 useState 就是状态

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState Hook
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button 组件

function Counter() {                                                // 定义函数组件 Counter
  const [count, setCount] = useState(0);                            // 创建状态 count,初始值 0

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>当前计数:{count}</Text>                                  // 显示当前计数
      <Button title="加一" onPress={() => setCount(count + 1)} />   // 点击后 count 加一
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 Counter 组件

count 是状态,setCount 用来更新状态。状态一变,Counter 会重新执行。这就是“状态”这一部分的体现:状态是函数的输入之一。

4.3 JSX 就是返回值

// JavaScript + React Native (JSX)
return (                                                            // 返回界面描述
  <View>                                                            // 外层容器
    <Text>当前计数:{count}</Text>                                    // 显示当前计数
    <Button title="加一" onPress={() => setCount(count + 1)} />     // 点击后 count 加一
  </View>                                                           // 结束 View
);                                                                  // 结束 return

这段 JSX 就是函数的返回值。它不是最终的原生视图,而是“视图应该长什么样”的描述。这就是“视图”这一部分的体现:视图是函数的输出。

4.4 状态更新触发重新执行

点击按钮,setCount(count + 1) 被调用。React Native 知道状态变了,于是重新执行 Counter 函数。

重新执行时,count 已经是新值。函数返回新的 JSX。React Native 比较新旧 JSX,找出差异,高效更新真正的原生界面。

整个过程可以粗略理解为:

// 概念表达
点击按钮 -> setCount -> count 变化 -> Counter 重新执行 -> 返回新视图 -> 界面更新

你没有写“更新界面”的代码。界面更新是状态变化的自然结果。

4.5 重新渲染是重新执行函数,不是重启 App

这一点很重要。组件函数在应用运行过程中会被执行很多次。每次状态变化,相关组件函数就会重新执行。

所以,组件函数应该尽量“干净”:相同的输入,返回相同的输出。不要在函数执行过程中随便修改外部变量,也不要发请求、订阅事件。那些事情应该放在事件处理或副作用里。

4.6 一个完整的执行流程

让我们把计数器从首次渲染到点击更新的完整流程走一遍:

首次渲染:

  1. App 函数被调用;
  2. useState(0) 返回 [0, setCount];
  3. 函数返回 JSX,其中 count 是 0;
  4. React Native 把 JSX 映射成原生视图;
  5. 屏幕显示“你点击了 0 次”。

点击按钮:

  1. onPress 被调用;
  2. setCount(0 + 1) 执行;
  3. React Native 记录状态需要更新;
  4. App 函数被重新调用;
  5. useState(0) 这次返回 [1, setCount];
  6. 函数返回新的 JSX,其中 count 是 1;
  7. React Native 比较新旧 JSX,发现只有文字变了;
  8. 只更新那个 Text 的原生内容;
  9. 屏幕显示“你点击了 1 次”。

整个过程,你只写了一行 setCount(count + 1)。剩下的都由框架完成。


五、纯函数与副作用

5.1 什么是纯函数

“视图 = 函数(状态)”中的函数,最好是一个“纯函数”。纯函数的意思是:相同的输入,总是得到相同的输出;不修改外部变量;不做副作用。

比如:

// JavaScript
function add(a, b) {                                                // 定义纯函数 add
  return a + b;                                                     // 返回 a 和 b 的和
}                                                                   // 结束 add 函数

这就是纯函数。输入 1 和 2,永远返回 3。

下面这个不是纯函数:

// JavaScript
let total = 0;                                                      // 定义外部变量 total

function addToTotal(n) {                                            // 定义非纯函数 addToTotal
  total = total + n;                                                // 修改了外部变量 total
  return total;                                                     // 返回修改后的 total
}                                                                   // 结束 addToTotal 函数

因为它修改了外部变量 total。相同的输入,可能得到不同的输出。

5.2 为什么组件渲染要纯

在 React Native 组件中,渲染过程中不要做这些事:

  • 不要直接修改 state;
  • 不要修改外部变量;
  • 不要发网络请求;
  • 不要订阅事件;
  • 不要手动操作原生视图。

渲染过程只做一件事:根据当前状态,计算并返回界面描述。

为什么?因为组件函数会被执行很多次。如果渲染过程中做副作用,可能会导致重复请求、数据混乱、界面不可预测。

5.3 副作用放在哪里

副作用应该放在两个地方:

  1. 事件处理函数:用户点击、输入等触发的操作;
  2. useEffect:组件挂载、更新、卸载时执行的操作。
// JavaScript + React Native
const handlePress = () => {                                         // 事件处理函数
  fetch('https://api.example.com/data');                            // 发请求,这是副作用
};                                                                  // 结束 handlePress
// JavaScript + React Native
useEffect(() => {                                                   // useEffect 中执行副作用
  fetch('https://api.example.com/data');                            // 组件挂载后发请求
}, []);                                                             // 空数组表示只执行一次

第4课会专门讲 useEffect。


六、不可变更新:让状态真正“变新”

6.1 为什么需要不可变更新

React Native 判断状态有没有变,通常看“引用有没有变”。如果你直接修改原对象或原数组,引用没变,React Native 可能认为没变,界面就不会更新。这是一个很容易踩的坑。

6.2 错误写法

// JavaScript + React Native
const [todos, setTodos] = useState([{ id: 1, text: '学习', done: false }]);  // 创建状态 todos

const toggleTodo = (id) => {                                        // 定义切换完成状态的函数
  const todo = todos.find((t) => t.id === id);                      // 找到对应的待办
  todo.done = !todo.done;                                           // 直接修改它的 done 属性
  setTodos(todos);                                                  // 把同一个数组传回去
};                                                                  // 结束 toggleTodo

这里直接改了 todo.done,然后又把同一个 todos 传回去。引用没变,React Native 可能不会更新。

6.3 正确写法

// JavaScript + React Native
const toggleTodo = (id) => {                                        // 定义切换完成状态的函数
  setTodos(                                                         // 更新 todos
    todos.map((todo) =>                                             // 用 map 遍历每一项
      todo.id === id ? { ...todo, done: !todo.done } : todo         // id 匹配则创建新对象翻转 done,否则原样返回
    )                                                               // 结束 map
  );                                                                // 结束 setTodos
};                                                                  // 结束 toggleTodo

这里用 map 生成了一个新数组,用 { ...todo, done: !todo.done } 生成了一个新对象。状态是新的,React Native 就能感知到变化。

6.4 常用不可变操作

数组添加:

// JavaScript + React Native
setTodos([...todos, newTodo]);                                      // 用展开运算符创建新数组并追加

数组删除:

// JavaScript + React Native
setTodos(todos.filter((todo) => todo.id !== id));                   // 用 filter 创建新数组,过滤掉指定 id

数组更新:

// JavaScript + React Native
setTodos(todos.map((todo) =>                                        // 用 map 创建新数组
  todo.id === id ? { ...todo, done: !todo.done } : todo             // 匹配的项创建新对象,其他项原样返回
));                                                                 // 结束 setTodos

对象更新:

// JavaScript + React Native
setUser({ ...user, name: '新名字' });                                // 用展开运算符创建新对象,覆盖 name

6.5 不可变更新的意义

不可变更新的意义,就是让“状态”真正成为新的输入,让“函数”重新计算出新的视图。

如果状态引用没变,函数重新执行时拿到的还是旧数据,视图就不会变。所以,不可变更新不是“建议”,而是“必须”。


七、状态驱动 UI 的典型场景

7.1 条件渲染:登录状态

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button 组件

function App() {                                                    // 定义函数组件 App
  const [isLoggedIn, setIsLoggedIn] = useState(false);              // 创建状态 isLoggedIn,初始值 false

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      {isLoggedIn ? <Text>欢迎回来</Text> : <Text>请登录</Text>}     // 根据 isLoggedIn 决定显示哪段文字
      <Button                                                       // 按钮
        title={isLoggedIn ? '退出' : '登录'}                          // 按钮文字也由 isLoggedIn 决定
        onPress={() => setIsLoggedIn(!isLoggedIn)}                  // 点击时翻转 isLoggedIn
      />                                                            // 结束 Button
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

状态 isLoggedIn 决定显示哪段文字,也决定按钮文字。状态变,视图变。

7.2 列表渲染:map

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text } from 'react-native';                          // 引入 View、Text 组件

function App() {                                                    // 定义函数组件 App
  const [fruits] = useState(['苹果', '香蕉', '橙子']);               // 创建状态 fruits,初始三条数据

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      {fruits.map((fruit, index) => (                               // 遍历 fruits
        <Text key={index}>{fruit}</Text>                            // 每个水果生成一个 Text
      ))}                                                           // 结束 map
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

fruits 是状态,map 把状态转换成一组 Text。状态和视图之间是映射关系。

7.3 表单输入:受控组件

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, TextInput } from 'react-native';               // 引入 View、Text、TextInput 组件

function App() {                                                    // 定义函数组件 App
  const [name, setName] = useState('');                             // 创建状态 name,初始空字符串

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <TextInput                                                    // 输入框
        value={name}                                                // 输入框的值由 name 决定
        onChangeText={setName}                                      // 输入变化时更新 name
        placeholder="请输入名字"                                     // 占位提示
      />                                                            // 结束 TextInput
      <Text>你好,{name}</Text>                                      // 显示问候语,由 name 决定
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

输入框的值由 name 决定,输入变化时更新 name。name 变了,界面重新计算。这就是受控组件。

7.4 开关:Switch

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Switch } from 'react-native';                  // 引入 View、Text、Switch 组件

function App() {                                                    // 定义函数组件 App
  const [isOn, setIsOn] = useState(false);                          // 创建状态 isOn,初始值 false

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Switch value={isOn} onValueChange={setIsOn} />               // 开关的值由 isOn 决定,切换时更新 isOn
      <Text>{isOn ? '已开启' : '已关闭'}</Text>                      // 根据 isOn 显示状态文字
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

7.5 主题切换

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button 组件

function App() {                                                    // 定义函数组件 App
  const [dark, setDark] = useState(false);                          // 创建状态 dark,初始值 false

  return (                                                          // 返回界面描述
    <View style={{ flex: 1, backgroundColor: dark ? '#222' : '#fff' }}>  // 背景色由 dark 决定
      <Text style={{ color: dark ? '#fff' : '#000' }}>              // 文字颜色也由 dark 决定
        {dark ? '暗色模式' : '亮色模式'}                              // 显示文字由 dark 决定
      </Text>                                                       // 结束 Text
      <Button title="切换主题" onPress={() => setDark(!dark)} />    // 点击切换 dark
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

同一个状态 dark,同时决定背景色、文字颜色和显示文字。这就是“视图由状态计算”。


八、常见误区

8.1 直接修改 state

// 错误写法
todos.push(newTodo);                                                // 直接修改原数组
setTodos(todos);                                                    // 传回同一个引用

应该:

// 正确写法
setTodos([...todos, newTodo]);                                      // 创建新数组

8.2 用普通变量当状态

// 错误写法
let count = 0;                                                      // 普通变量

function App() {                                                    // 定义组件
  return <Text>{count}</Text>;                                      // 普通变量变化不会触发重新渲染
}                                                                   // 结束组件

应该用 useState。

8.3 在渲染中做副作用

// 错误写法
function App() {                                                    // 定义组件
  fetch('https://example.com');                                     // 渲染中发请求,会导致重复请求
  return <Text>你好</Text>;                                         // 返回界面描述
}                                                                   // 结束组件

渲染只负责计算 UI,副作用应放在事件处理或 useEffect 中。

8.4 依赖旧状态连续更新

// 可能不准
setCount(count + 1);                                                // 使用当前渲染中的 count
setCount(count + 1);                                                // 再次使用同一个 count,结果只加了一

应该用函数式更新:

// JavaScript + React Native
setCount((prev) => prev + 1);                                       // 使用上一次的状态值
setCount((prev) => prev + 1);                                       // 再次使用最新的状态值,结果加二

prev 是上一次的状态值,能保证连续更新正确。

8.5 忘记 key

// 错误写法
{fruits.map((fruit) => (                                            // 遍历数组
  <Text>{fruit}</Text>                                              // 没有 key
))}                                                                 // 结束 map

应该:

// JavaScript + React Native (JSX)
{fruits.map((fruit) => (                                            // 遍历数组
  <Text key={fruit}>{fruit}</Text>                                  // 加上唯一的 key
))}                                                                 // 结束 map

key 帮助 React 识别列表项。没有 key 或 key 不唯一,可能导致渲染错误。


九、状态提升与单向数据流

9.1 什么是状态提升

当两个组件需要共享状态时,可以把状态放到它们的共同父组件中,这叫“状态提升”。

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button 组件

function Child({ count, onAdd }) {                                  // 定义子组件 Child,接收 count 和 onAdd
  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>子组件显示:{count}</Text>                               // 显示父组件传下来的 count
      <Button title="加一" onPress={onAdd} />                       // 点击时调用父组件传下来的 onAdd
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 Child 组件

export default function App() {                                     // 定义并默认导出 App 组件
  const [count, setCount] = useState(0);                            // 创建状态 count,初始值 0

  return (                                                          // 返回界面描述
    <Child count={count} onAdd={() => setCount(count + 1)} />       // 把 count 和更新函数传给子组件
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

父组件持有状态,通过 props 把 count 传给子组件,通过 onAdd 把更新函数传给子组件。数据向下流,事件向上传。

9.2 单向数据流

这就是单向数据流:

// 概念表达
数据向下传:props                                                    // 父组件通过 props 把数据传给子组件
事件向上传:回调函数                                                  // 子组件通过回调函数通知父组件更新

状态始终在父组件,子组件只负责显示和触发事件。这样数据流向清晰,容易追踪。


十、练习题

练习题1:选择题——视图=函数(状态)的含义

题目:下面哪一项最符合“视图 = 函数(状态)”?

A. 视图和状态是同一个东西
B. 视图由状态经过函数计算得到
C. 函数直接修改原生视图
D. 状态由视图计算得到

参考答案:B

解读:状态是输入,函数是计算过程,视图是输出。状态变化时,函数重新执行,得到新的视图描述,框架再更新界面。


练习题2:填空题——useState

题目:补全代码,使计数器显示 count,点击按钮后 count 加一。

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button

export default function App() {                                     // 定义并默认导出 App
  const [count, setCount] = useState(0);                            // 创建状态 count,初始值 0

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>{____}</Text>                                           // 显示 count
      <Button title="加一" onPress={() => ____} />                  // 点击更新 count
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

参考答案:第一空填 count,第二空填 setCount(count + 1)。

解读:count 是状态值,显示在 Text 中。按钮点击时调用 setCount 更新状态。状态变,组件重新执行,界面更新。


练习题3:改错题——直接修改 state

题目:下面代码想添加一条待办事项,但可能不会更新界面。请找出问题并修正。

// JavaScript + React Native
const [todos, setTodos] = useState([]);                             // 创建状态 todos

const addTodo = () => {                                             // 定义添加函数
  todos.push({ id: 1, text: '新事项', done: false });               // 直接修改原数组
  setTodos(todos);                                                  // 传回同一个引用
};                                                                  // 结束 addTodo

参考答案:

// JavaScript + React Native
const addTodo = () => {                                             // 定义添加函数
  setTodos([...todos, { id: 1, text: '新事项', done: false }]);     // 创建新数组并追加
};                                                                  // 结束 addTodo

解读:直接 push 修改了原数组,引用没变,React Native 可能认为状态没变。应该用展开运算符创建一个新数组。


练习题4:代码阅读题——预测输出

题目:阅读代码,说出点击按钮后屏幕上显示什么。

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button

function App() {                                                    // 定义函数组件 App
  const [isOn, setIsOn] = useState(false);                          // 创建状态 isOn,初始值 false

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>{isOn ? '开' : '关'}</Text>                             // 根据 isOn 显示文字
      <Button title="切换" onPress={() => setIsOn(!isOn)} />        // 点击切换 isOn
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

参考答案:初始显示“关”,点击后显示“开”,再点击又显示“关”。

解读:isOn 是状态,三元运算符根据它的值决定显示“开”还是“关”。点击按钮更新状态,界面重新计算。


练习题5:动手题——开关状态

题目:创建一个组件,用一个布尔状态 isOn 控制显示“已开启”或“已关闭”,并提供一个按钮切换状态。

参考答案:

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button 组件

export default function App() {                                     // 定义并默认导出 App 组件
  const [isOn, setIsOn] = useState(false);                          // 创建状态 isOn,初始值 false

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>{isOn ? '已开启' : '已关闭'}</Text>                      // 根据 isOn 显示状态文字
      <Button title="切换" onPress={() => setIsOn(!isOn)} />        // 点击翻转 isOn
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

解读:布尔状态非常适合表示开关、登录、显示隐藏等二选一场景。状态变,视图跟着变。


练习题6:简答题——为什么渲染函数要纯

题目:为什么组件函数在渲染过程中不应该发请求或修改外部变量?

参考答案:因为组件函数可能会被多次执行。如果渲染过程中做副作用,可能会导致重复请求、数据混乱、界面不可预测。渲染只应该根据当前状态计算并返回界面描述。

解读:React Native 的声明式模型依赖“相同输入得到相同输出”。副作用应该放在事件处理或 useEffect 中。


练习题7:条件渲染题——登录状态

题目:根据 isLoggedIn 状态,显示“欢迎回来”或“请登录”。

参考答案:

// JavaScript + React Native (JSX)
{isLoggedIn ? <Text>欢迎回来</Text> : <Text>请登录</Text>}          // 三元表达式根据 isLoggedIn 决定显示哪段文字

解读:条件渲染是“视图 = 函数(状态)”的直接体现。状态不同,函数返回的视图描述不同。


练习题8:事件处理题——按钮更新状态

题目:创建一个按钮,点击后让 count 增加 1。

参考答案:

// JavaScript + React Native (JSX)
<Button title="加一" onPress={() => setCount(count + 1)} />         // 按钮,点击时更新 count

解读:事件处理负责把用户操作转换成状态更新。状态更新后,界面自动重新计算。


练习题9:列表渲染题——map

题目:有一个数组 const fruits = ['苹果', '香蕉', '橙子'],请用 map 渲染成一组 <Text>。

参考答案:

// JavaScript + React Native (JSX)
{fruits.map((fruit, index) => (                                     // 遍历 fruits
  <Text key={index}>{fruit}</Text>                                  // 每项生成一个 Text,key 用索引
))}                                                                 // 结束 map

解读:map 把数组状态转换成一组视图描述。每个元素需要 key,帮助 React 高效更新列表。


练习题10:受控组件题——TextInput

题目:创建一个文本输入框,输入内容显示在下方文字中。

参考答案:

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, TextInput } from 'react-native';               // 引入 View、Text、TextInput

function App() {                                                    // 定义函数组件 App
  const [name, setName] = useState('');                             // 创建状态 name,初始空字符串

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <TextInput value={name} onChangeText={setName} />             // 输入框的值由 name 决定,输入变化时更新 name
      <Text>你好,{name}</Text>                                      // 显示问候语,由 name 决定
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

解读:输入框的值由 name 状态决定。输入变化时更新 name。状态变,界面重新计算。这就是受控组件。


练习题11:函数式更新题——连续加一

题目:下面代码连续调用两次 setCount,可能只加一。请改成函数式更新。

// JavaScript + React Native
setCount(count + 1);                                                // 使用当前渲染中的 count
setCount(count + 1);                                                // 再次使用同一个 count

参考答案:

// JavaScript + React Native
setCount((prev) => prev + 1);                                       // 使用上一次的状态值
setCount((prev) => prev + 1);                                       // 再次使用最新的状态值

解读:函数式更新使用上一次的状态值 prev,避免连续更新时拿到旧值。


练习题12:不可变更新题——对象

题目:有一个用户对象 user,请把它的 name 改成“新名字”,并保持不可变更新。

参考答案:

// JavaScript + React Native
setUser({ ...user, name: '新名字' });                                // 展开原对象,覆盖 name,生成新对象

解读:展开运算符复制原对象,再覆盖 name,生成一个新对象。这样 React Native 能感知状态变化。


练习题13:主题切换题

题目:用 dark 状态控制背景色和文字颜色,并提供一个按钮切换。

参考答案:

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button

function App() {                                                    // 定义函数组件 App
  const [dark, setDark] = useState(false);                          // 创建状态 dark,初始值 false

  return (                                                          // 返回界面描述
    <View style={{ flex: 1, backgroundColor: dark ? '#222' : '#fff' }}>  // 背景色由 dark 决定
      <Text style={{ color: dark ? '#fff' : '#000' }}>              // 文字颜色由 dark 决定
        {dark ? '暗色模式' : '亮色模式'}                              // 显示文字由 dark 决定
      </Text>                                                       // 结束 Text
      <Button title="切换主题" onPress={() => setDark(!dark)} />    // 点击切换 dark
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

解读:同一个状态可以影响多个视图属性。状态是输入,视图是输出。


练习题14:状态提升题

题目:父组件有一个 count 状态,子组件显示它,并提供一个按钮让父组件加一。请写出关键代码。

参考答案:

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button

function Child({ count, onAdd }) {                                  // 定义子组件 Child,接收 count 和 onAdd
  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>计数:{count}</Text>                                     // 显示父组件传下来的 count
      <Button title="加一" onPress={onAdd} />                       // 点击时调用父组件传下来的 onAdd
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 Child 组件

export default function App() {                                     // 定义并默认导出 App 组件
  const [count, setCount] = useState(0);                            // 创建状态 count,初始值 0

  return (                                                          // 返回界面描述
    <Child count={count} onAdd={() => setCount(count + 1)} />       // 把 count 和更新函数传给子组件
  );                                                                // 结束 return
}                                                                   // 结束 App 组件

解读:状态放在父组件,通过 props 向下传数据,通过回调向上传事件。这就是单向数据流。


练习题15:综合应用题——简单待办事项

题目:创建一个待办事项应用:

  1. 初始有三条待办;
  2. 点击事项切换完成状态,完成显示删除线;
  3. 底部按钮添加新事项。

参考答案:

// JavaScript + React Native
import { useState } from 'react';                                   // 引入 useState
import { View, Text, Button, StyleSheet, ScrollView } from 'react-native';  // 引入需要的组件

export default function TodoApp() {                                 // 定义并默认导出 TodoApp 组件
  const [todos, setTodos] = useState([                              // 创建状态 todos,初始三条数据
    { id: 1, text: '学习声明式UI', done: false },                   // 第一条,未完成
    { id: 2, text: '完成练习题', done: false },                     // 第二条,未完成
    { id: 3, text: '搭建环境', done: true },                        // 第三条,已完成
  ]);                                                               // 结束 useState

  const toggleTodo = (id) => {                                      // 定义切换完成状态的函数
    setTodos(                                                       // 更新 todos
      todos.map((todo) =>                                           // 遍历每一项
        todo.id === id ? { ...todo, done: !todo.done } : todo       // id 匹配则翻转 done,否则原样返回
      )                                                             // 结束 map
    );                                                              // 结束 setTodos
  };                                                                // 结束 toggleTodo

  const addTodo = () => {                                           // 定义添加新待办的函数
    const newId = todos.length                                      // 判断 todos 是否为空
      ? Math.max(...todos.map((t) => t.id)) + 1                     // 有数据时取最大 id + 1
      : 1;                                                          // 没数据时从 1 开始
    setTodos([...todos, { id: newId, text: `新事项 ${newId}`, done: false }]);  // 用展开运算符追加
  };                                                                // 结束 addTodo

  return (                                                          // 返回界面描述
    <View style={styles.container}>                                 // 外层容器
      <Text style={styles.heading}>我的待办</Text>                   // 标题
      <ScrollView style={styles.list}>                              // 可滚动列表
        {todos.map((todo) => (                                      // 遍历 todos
          <Text                                                     // 每条渲染一个 Text
            key={todo.id}                                           // key 用唯一 id
            style={[styles.todoItem, todo.done && styles.todoDone]} // 组合样式,完成时加删除线
            onPress={() => toggleTodo(todo.id)}                     // 点击切换完成状态
          >                                                         // 开始 Text 标签
            {todo.done ? '✓ ' : '○ '}{todo.text}                    // 前缀符号 + 文本
          </Text>                                                   // 结束 Text 标签
        ))}                                                         // 结束 map
      </ScrollView>                                                 // 结束 ScrollView
      <Button title="添加新事项" onPress={addTodo} />                // 添加按钮
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 TodoApp 组件

const styles = StyleSheet.create({                                  // 创建样式表
  container: { flex: 1, padding: 20, paddingTop: 60 },              // 容器样式
  heading: { fontSize: 28, fontWeight: 'bold', marginBottom: 20 },  // 标题样式
  list: { flex: 1, marginBottom: 16 },                              // 列表样式
  todoItem: { fontSize: 18, padding: 12, borderBottomWidth: 1, borderBottomColor: '#eee' },  // 待办项样式
  todoDone: { textDecorationLine: 'line-through', color: '#aaa' },  // 已完成样式
});                                                                 // 结束样式表

解读:todos 是状态,TodoApp 是函数,界面是 TodoApp(todos) 的结果。toggleTodo 用 map 和展开运算符生成新数组、新对象,实现不可变更新。addTodo 用展开运算符追加新事项。状态一变,函数重新执行,界面自动更新。


十一、总结

这一课的核心,就是把“视图 = 函数(状态)”这句话落到 React Native 的具体写法上:

  1. 函数组件就是函数;
  2. useState 管理状态;
  3. JSX 是函数返回的视图描述;
  4. 状态更新触发函数重新执行;
  5. 不可变更新保证状态真正“变新”;
  6. 事件处理把用户操作转换成状态更新;
  7. 条件渲染、列表渲染、表单、主题切换,都是状态到视图的映射;
  8. 状态提升让多个组件共享状态,单向数据流让数据流向清晰。

记住一句话:

不要问“我怎么改界面”,要问“哪个状态变了,界面应该长什么样”。

这就是声明式 UI 在 React Native 中最具体的体现。


第3课预告

下一课,我们进入 【React Native娓娓道来】第3课 组件组合与列表渲染:把界面拆成可复用的函数。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐