主题
首屏数据渲染与添加todo(Day6 1/3)
本页对应 React + TypeScript + 医通项目,共 6 段视频,约 1 小时 3 分。
学习目标
- 能够使用
useState声明状态及更新函数,并通过事件触发状态更新完成计数、文本修改等交互。 - 能够解释
useState更新函数调用后函数组件重新执行的过程,并说明状态值未改变时函数组件只多渲染一次的现象。 - 能够使用
useEffect模拟组件挂载、更新和卸载生命周期,并通过依赖数组控制监听对象。 - 能够使用
useRef创建引用对象,获取真实 DOM 元素及用户输入值。 - 能够综合运用
useState、useEffect和定时器完成电子时钟组件,并在组件卸载时清理定时器。 - 能够基于
useState完成 Todo 列表的首屏数据渲染、添加 Todo 和删除 Todo 功能。
知识点
先明确“首屏数据渲染”要解决什么
TodoList 的首屏渲染,本质是:TodoList 组件先拥有一份 todos 状态数据,首屏渲染时把这份数据通过 props 分发给 Main 和 Footer,Main 负责列表,Footer 负责统计。后续新增待办时,还需要一个不重复的 id 作为列表 key。所以本节有两个前置技术点:生成不重复 id,以及把 todos 定义成 TodoList 的状态。
知识点一:生成不重复的 id
为什么需要:列表渲染时 React 需要稳定的 key 来识别每一项。如果 id 重复,列表更新时可能出现复用错乱、勾选状态串行等问题。
怎么想:优先用成熟工具生成唯一值;如果只是练习,也可以利用 Math.random().toString(36) 生成字母数字随机串来模拟。
怎么做:方式一使用 nanoid 包:
bash
npm i nanoidjs
import { nanoid } from 'nanoid'js
nanoid()方式二自己模拟生成字母数字随机组合的字符串。笔记中给出:
js
Math.random().toString(36).slice(2)这里 toString(36) 会把随机数转成 0-9、a-z 组成的 36 进制字符串,slice(2) 去掉前面的 0.。
易错点:nanoid 要按包的实际导入方式使用;自己模拟的随机串长度不固定,也可能碰撞,练习可以,正式项目不要把它当成严格唯一 id。生成 id 的时机应是在新增 todo 时,而不是渲染时临时生成,否则每次渲染 key 都会变化。
知识点二:把 todos 定义成 TodoList 组件的状态
为什么需要:转写稿中反复强调,函数组件要靠 useState 才有状态。若把 todos 写成普通变量,组件重新执行时数据会丢失,也无法触发页面更新。
怎么想:useState 返回一个数组,按位置解构:第一项是状态数据,第二项是修改状态的方法。写法上直接解构,不用像类组件那样考虑 this。
怎么做:在 TodoList 组件中定义:
js
const [todos, setTodos] = useState([
{ id: nanoid(), text: '学习 React', done: false }
])首屏渲染时,todos 的初始值就是首屏要显示的数据。
易错点:不要直接修改 todos 原数组,例如 todos.push(...);应通过 setTodos 传入新数组。转写稿调试时先打印 useState 的返回值,确认它返回数组,再解构使用,这个习惯可以避免“状态和方法拿反”的问题。
知识点三:通过标签属性把 todos 传给 Main 和 Footer
为什么需要:TodoList 是数据持有者,Main 和 Footer 是展示和统计组件。数据放在共同父组件里,子组件通过 props 接收,职责更清晰。
怎么想:React 中父传子用标签属性,子组件通过函数参数接收 props。转写稿中也演示了把状态数据通过属性传给子组件,子组件用参数接收后直接使用。
怎么做:
jsx
<Main todos={todos} />
<Footer todos={todos} />子组件中:
js
function Main(props) {
const { todos } = props
// ...
}易错点:props 是只读的,不要在 Main 或 Footer 里直接改 props.todos;属性名要保持一致,父组件传 todos,子组件就接收 todos。
知识点四:Main 中 map 渲染列表并把 todo 传给 Item
为什么需要:Main 拿到的是数组,需要把每一项转成 JSX。把单个 todo 再传给 Item,可以让 Item 只负责展示一条待办。
怎么想:用 map 遍历 todos,返回 Item 组件;key 使用 todo.id,不要用数组下标。
怎么做:
jsx
function Main({ todos }) {
return (
<ul>
{todos.map(todo => (
<Item key={todo.id} todo={todo} />
))}
</ul>
)
}易错点:map 必须有返回值;key 要写在 map 返回的最外层组件上。转写稿中“定义 key 值,并将遍历的每一项 todo 传递给 Item”就是这一步的核心。
知识点五:Footer 中接收 todos 并计算 total 和 doneNum
为什么需要:底部需要显示总数和已完成数,这些数据可以由 todos 派生出来,不必再单独存一份状态,避免数据不同步。
怎么想:total 就是 todos.length;doneNum 是已完成项数量,用 filter 筛出 done 为真的项再取长度。
怎么做:
jsx
function Footer({ todos }) {
const total = todos.length
const doneNum = todos.filter(todo => todo.done).length
return (
<div>
总数:{total},已完成:{doneNum}
</div>
)
}易错点:不要用 forEach 后忘记累加;filter 的回调要返回布尔条件。若后续勾选完成状态,应通过 setTodos 生成新数组,Footer 会随着新的 todos 重新计算,而不是手动改 total 和 doneNum。
本节笔记未包含图片,以上保留了笔记中的命令与代码。整体流程可以记成一句话:TodoList 持有 todos 状态 → 通过 props 分发给 Main 和 Footer → Main 用 map 渲染并传 todo 给 Item → Footer 用 todos 计算统计值。
先拆结果,再拆步骤
这一节要做的事很具体:文本框输入内容,按下回车,把内容添加到列表中。
为什么需要先拆?因为直接写代码容易把“取输入、监听键盘、改列表”混在一起,出错后不知道是哪一步坏了。转写稿里反复强调先描述结果,再拆步骤,最后给每一步匹配技术点。
怎么想:先写结果句,再拆成三个动作:
- 获取文本框输入内容;
- 按下回车;
- 将内容添加到列表中。
怎么做:给三步匹配技术点:取输入可用 ref、受控组件、e.target.value;回车可用 onKeyUp / onKeyDown;添加到列表用“子传父”。
易错点:不要一上来就写完整组件,先保证每一步能单独验证。
获取输入:ref、受控组件、e.target.value
为什么需要?添加 todo 的第一步是拿到用户输入;拿不到值,后面回车和添加都无从谈起。
怎么想?三条路:ref 直接拿真实 DOM;受控组件把输入同步到 state;事件回调里用 e.target.value 取当前值。
怎么做?转写稿里用 useRef 演示过取 input 值:
jsx
const inputRef = useRef()
<input ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}>
获取
</button>点击后打印 inputRef.current,看到它是真实 DOM,再取 .value 就是用户输入。受控组件则要同时写 value 和 onChange,让 state 成为输入框的值来源。
易错点:useRef 必须通过 .current 取;受控组件只写 value 不写 onChange 会改不动;e.target.value 要在事件回调里取,不要脱离事件对象乱用。
按下回车:绑定键盘事件
为什么需要?输入框里打字不等于提交,必须明确“回车”这个动作。
怎么想?React 里键盘事件常用 onKeyUp、onKeyDown,事件对象里判断按键。
怎么做?在输入框上绑定事件,例如:
jsx
<input
onKeyDown={(e) => {
if (e.key === 'Enter') {
// 执行添加逻辑
}
}}
/>易错点:事件名大小写要写对,是 onKeyDown / onKeyUp;判断的是 e.key === 'Enter';长按回车可能重复触发,实际项目要按需防重复。
添加到列表:用子传父
为什么需要?输入框和列表常常不在同一个组件里。子组件不能直接改父组件状态,所以要“子传父”。
怎么想?父组件持有列表状态和添加函数;子组件只负责取输入、触发回车,然后调用父组件传下来的函数,把值交上去。
怎么做?父组件把函数作为属性传给子组件;子组件通过 props 接收并调用。转写稿里已有父子传值示例:父组件写 <TestState MSG={MSG} count={count} />,子组件通过参数接收 props.MSG、props.count。添加 todo 也是同一思路:父组件传“添加函数”,子组件在回车时调用它。
易错点:不要直接改 props;列表更新要用新的数组和 setState;子组件调用父函数时要把输入值作为参数传出去。
串起来调试
调试顺序建议:先验证能取到输入值,再验证回车能触发,再验证父组件能收到值,最后看列表是否更新。转写稿里也出现过典型调试:useState 没解构会报 useState is not defined;useRef 绑定后要打印 current 确认拿到真实 DOM;调用 setState 会触发函数组件重新执行并重新渲染。
易错点:不要跳过中间验证;每一步都打印或断点确认,比最后一起猜更可靠。
先明确删除 todo 的数据流
删除 todo 看起来只是“点一下按钮”,但真正要处理的是数据归属问题。转写稿里反复验证过:函数组件用 useState 创建状态,调用 set 方法会触发函数组件重新调用,从而完成页面重新渲染;子组件通过 props 接收父组件数据。删除功能也遵循这条数据流。
为什么需要:todo 列表数据通常放在父组件,删除按钮却在某一行子组件里。子组件不能直接改父组件状态,否则数据流会乱。
怎么想:先找“数据在谁身上”。列表数据在父组件,删除方法就定义在父组件;子组件只负责把被点击行的 id 传出去。
怎么做:父组件定义删除方法,子组件通过 onClick 调用父组件传下来的回调,并把当前行 id 作为参数传回。
易错点:不要以为按钮在子组件,删除逻辑就写在子组件;不要直接修改原数组;不要写成 onClick={onDelete(id)},这样会在渲染时立即执行。
点击删除按钮:onClick 传递 id
为什么需要:列表里有很多行,父组件必须知道“删哪一行”。只触发点击事件不够,还要把当前行的唯一标识 id 传出去。
怎么想:删除按钮属于当前 todo 项,当前项身上有 todo.id。点击时用箭头函数包一层,把 id 交给父组件传下来的 onDelete。
怎么做:子组件里写:
jsx
<button onClick={() => onDelete(todo.id)}>删除</button>这里 onDelete 是父组件通过 props 传进来的方法。转写稿里也强调过,函数组件没有 this,不需要写 this.props,直接使用参数接收到的 props 即可。
易错点:onClick={() => onDelete(todo.id)} 不能写成 onClick={onDelete(todo.id)};id 的变量名要和父组件过滤时一致;如果列表项用 key,建议用 todo.id,不要用索引。
删除该行:子传父
为什么需要:React 是单向数据流,子组件不能直接改父组件的 todos。删除动作由子组件触发,但真正修改状态要在父组件完成,这就是“子传父”。
怎么想:父组件把“删除方法”作为属性传给子组件;子组件点击按钮时调用这个方法,并传入 id;父组件收到 id 后,用新数组替换旧数组。
怎么做:父组件最小示例:
jsx
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React' },
{ id: 2, text: '写 todo' }
])
function handleDelete(id) {
setTodos(todos.filter(todo => todo.id !== id))
}
<TodoItem todo={todo} onDelete={handleDelete} />子组件:
jsx
function TodoItem({ todo, onDelete }) {
return (
<li>
{todo.text}
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
)
}转写稿中验证过:set 方法执行后会触发函数组件重新调用,页面重新渲染。所以这里只要 setTodos 得到一个新数组,列表就会刷新。
易错点:不要用 todos.splice(...) 直接改原数组;filter 会返回新数组,适合删除;父组件传的方法名要和子组件接收的 props 名一致;如果出现 useState is not defined,要检查是否从 React 中导入了 useState。
数据在谁身上,方法就定义在谁身上
为什么需要:这是本节笔记的核心原则。数据在父组件,删除方法就必须定义在父组件;否则子组件没有权限改父状态,逻辑会绕。
怎么想:先判断 todos 在哪个组件里。谁持有 todos,谁就负责 setTodos;子组件只接收 onDelete 并调用。
怎么做:父组件持有 todos,定义 handleDelete;子组件只写 onClick={() => onDelete(todo.id)}。删除时用 filter 过滤掉当前 id。
易错点:不要把删除方法写在子组件里再去改父数据;不要直接给 props 赋值;不要漏传 id,否则父组件不知道删哪一行。
调试与验证
为什么需要:删除功能涉及父子传参、事件回调、状态更新,任何一环名字写错都会“点了没反应”。
怎么想:按链路排查:按钮有没有触发 onClick → id 有没有传对 → 父组件有没有收到 → filter 条件是否正确 → setTodos 是否执行。
怎么做:可在子组件点击处打印 todo.id,在父组件 handleDelete 里打印收到的 id,再检查 filter(todo => todo.id !== id)。转写稿中调试 useState 时也用过打印和刷新观察执行流程,这里同理。
易错点:id 类型不一致会导致过滤失败;onDelete 拼写不一致会拿不到方法;删除后状态没变,要检查是否返回了新数组。
课堂画面






代码示例
1. 首屏渲染 + 添加 todo + 删除 todo(完整可运行)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>TodoList:首屏渲染 / 添加 / 删除</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useRef } = React;
// 生成不重复 id(笔记方式二:36 进制随机串;也可 npm i nanoid 后 import { nanoid } from 'nanoid')
const createId = () => Math.random().toString(36).slice(2);
/* ---------- Header:负责「输入 + 回车」,把内容交给父组件 ---------- */
function Header({ onAdd }) {
const inputRef = useRef(null);
const handleKeyUp = (e) => {
if (e.key !== 'Enter') return; // 只处理回车
const value = inputRef.current.value.trim();
if (!value) return; // 空内容不添加
onAdd(value); // 子传父:把内容交给 TodoList
inputRef.current.value = ''; // 清空输入框
};
return (
<header>
<h1>todos</h1>
<input
ref={inputRef}
type="text"
placeholder="输入内容后按回车"
onKeyUp={handleKeyUp}
/>
</header>
);
}
/* ---------- Item:一行 todo,点击删除时把 id 传给父组件 ---------- */
function Item({ todo, onDel }) {
return (
<li>
<label>{todo.name}</label>
<button onClick={() => onDel(todo.id)}>删除</button>
</li>
);
}
/* ---------- Main:接收 todos,map 渲染列表 ---------- */
function Main({ todos, onDel }) {
return (
<ul>
{todos.map((todo) => (
<Item key={todo.id} todo={todo} onDel={onDel} />
))}
</ul>
);
}
/* ---------- Footer:接收 todos,计算 total 和 doneNum ---------- */
function Footer({ todos }) {
const total = todos.length;
const doneNum = todos.filter((todo) => todo.done).length;
return (
<footer>
总数:{total} / 已完成:{doneNum}
</footer>
);
}
/* ---------- TodoList:数据在谁身上,方法就定义在谁身上 ---------- */
function TodoList() {
const [todos, setTodos] = useState([
{ id: createId(), name: '吃饭', done: false },
{ id: createId(), name: '睡觉', done: true },
]);
// 添加
const addTodo = (name) => {
setTodos([...todos, { id: createId(), name, done: false }]);
};
// 删除
const delTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<div>
<Header onAdd={addTodo} />
<Main todos={todos} onDel={delTodo} />
<Footer todos={todos} />
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<TodoList />);
</script>
</body>
</html>关键行与易错点
useState([...]):把 todos 定义成 TodoList 组件的状态(首屏数据),只有状态变化才会触发重新渲染。- 状态通过标签属性往下传:
<Main todos={todos} />、<Footer todos={todos} />,子组件用props解构接收({ todos })。 todos.map(...)渲染列表,key={todo.id}必须用不重复的 id;用数组下标当 key 在增删时会错位。onAdd(value)、onDel(todo.id)都是子传父:父组件把方法以属性形式传给子组件,子组件调用时把数据回传。- 添加用
[...todos, 新项]、删除用todos.filter(...),都是生成新数组;直接todos.push()不会触发重新渲染。 e.key !== 'Enter'用来过滤按键;inputRef.current.value.trim()后要判空,否则会加进去一条空 todo。
2. useRef 获取输入框里的内容(添加 todo 的技术点)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>useRef 获取真实 DOM</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useRef } = React;
function App() {
// 1. 创建 ref 对象
const inputRef = useRef();
const getValue = () => {
console.log(inputRef); // 一个对象,里面有 current
console.log(inputRef.current); // 真实的 input DOM
console.log(inputRef.current.value); // 用户输入的内容
};
return (
<div>
{/* 2. 把 ref 绑定到元素身上 */}
<input ref={inputRef} type="text" placeholder="请输入内容" />
<button onClick={getValue}>获取</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>关键行与易错点
useRef()也要从 React 里解构出来,和useState一样是 hook,不能写在 if / 循环里。- 真实 DOM 要通过
inputRef.current拿,不是inputRef.value。 ref={inputRef}只是绑定,不要在 JSX 里写ref={inputRef.current}。- 用 ref 取值是「非受控」写法,取的是当下真实值;若用受控组件则要写
value={xxx} onChange={...},笔记里列了这两种技术点,按需选择。
3. useState 基础:状态变化才重新渲染
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>useState 基础</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function App() {
// 解构:前面是状态数据,后面是设置状态的方法
const [count, setCount] = useState(99);
console.log('App 渲染了一次'); // 每渲染一次就打印一次
return (
<div>
<h2>{count}</h2>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={() => setCount(9999)}>设置固定值 9999</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>关键行与易错点
useState(99)返回的是数组,要按位置解构:const [状态, 设置状态的方法] = useState(初始值),名字可以随便起,但一般叫xxx/setXxx。- 直接写
count,函数组件里没有 this,不要写this.state.count。 - 调用
setCount会做两件事:把参数作为最新状态,并触发函数组件重新调用(页面重新渲染),所以console.log会再次打印。 - 传固定值
9999时,从 9999 再点 9999 只会多渲染一次、之后不再渲染,这是 useState 对状态没改变做的优化,和类组件不同。 - 不要在
setCount里直接改原值(如count++后再传),要传新的值,如setCount(count + 1)。
常见错误
现象:点击按钮后紧接着
console.log(count),打印的还是旧值,页面却已经更新。 原因:函数组件每次渲染都有一份独立的变量快照,setCount只是登记下一次渲染,不会修改当前作用域里的变量。 修法:用setCount(c => c + 1)这种函数式写法算新值;确实需要“渲染后的值”,就把读取逻辑放进useEffect。现象:控制台报
useState is not defined/useEffect is not defined,或者提示 Hook 只能在函数组件内调用。 原因:Hook 是react的具名导出,忘记import;或者把 Hook 写进了if、循环、普通函数里。 修法:import { useState, useEffect, useRef } from 'react',并保证所有 Hook 都在组件函数的最顶层、每次渲染都按同样顺序执行。现象:电子时钟反复点“显示/隐藏”后,秒数跳得越来越快,像有多个定时器在跑。 原因:
useEffect里setInterval只开了没关,组件卸载后旧定时器仍然存活。 修法:在useEffect回调里return () => clearInterval(timer),把清理动作和开启动作写在同一个 effect 里。现象:定时器或事件回调里读到的
count永远是初始值 0。 原因:依赖数组写成了空数组,effect 里的闭包捕获的是首次渲染的那份变量,之后不再更新。 修法:把用到的状态加进依赖数组,让 effect 重建;或者改用函数式更新 /useRef保存最新值。现象:列表删除时删错行,或者输入框内容在行与行之间“串”。 原因:
key用了index或Math.random(),每次渲染 key 都在变或与数据对不上,React 认错了行。 修法:新增 todo 时就用nanoid()生成 id 存进对象,渲染和删除一律用todo.id。现象:子组件里直接
todos.push(...)或todos.splice(...),页面不刷新甚至报错。 原因:props是只读的,数据源在父组件;而且原地修改数组引用不变,React 认为没变化。 修法:子组件只负责把 id 通过回调传出去,父组件用setTodos(todos.filter(t => t.id !== id))、[...todos, newTodo]生成新数组。现象:受控输入框敲不进字,或者控制台警告
value没有配onChange。 原因:只写了value={text}没写onChange,输入框被锁死成只读。 修法:补上onChange={e => setText(e.target.value)};若只想回车提交、不想每次输入都渲染,就改用useRef取值。
练习
首屏渲染:把
todos提到TodoList的状态里,预置 3 条数据(id 用nanoid()生成),通过属性分别传给Main和Footer。Main用map渲染并给Item传单条 todo,Footer算出total和doneNum后渲染。要求:渲染前先console.log一次 todos,确认数据结构。添加 todo:用受控组件获取输入,
onKeyUp里判断e.key === 'Enter'后提交,trim()后为空则不添加。完成后把受控写法改成useRef写法,对比两种方式各自在“输入时会不会触发重渲染”上的差别。删除 todo:在
Item里给删除按钮绑onClick,把todo.id传给父组件,父组件用filter生成新数组。做完后再加一个“清除已完成”按钮,要求复用同一套“子传父 + 父改数据”的思路。电子时钟:
Timer组件里用useState存当前时间,useEffect里开setInterval并return清理函数。父组件用奇偶数控制Timer的条件渲染,切换时观察控制台打印顺序,说出哪一行对应挂载、哪一行对应卸载。useRef:给一个
input绑ref,放两个按钮——“聚焦”和“取值”。点击后分别调用inputRef.current.focus()和读取inputRef.current.value。思考:不用受控组件也能拿到用户输入,那什么时候必须用受控、什么时候ref就够?
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | useState | 14:35 |
| 2 | useState-深入理解 | 12:57 |
| 3 | 函数组件状态不改变只多渲染一次 | 07:16 |
| 4 | useEffect基本使用 | 20:08 |
| 5 | 电子时钟练习 | 05:55 |
| 6 | useRef | 03:02 |
小结
- Hook 不是新能力,而是把类组件的三件套(state、生命周期、ref)换成了一套更短的语法,能力等价、写法更轻。
useState的本质是“本次渲染的快照 + 一个登记器”,setXxx不修改当前变量,只排队下一次渲染。想通这一句,前面大半错误都能自己定位。useState对相同值做了浅比较优化:赋同样的值只多渲染一次,之后不再渲染。但这是性能优化,不能拿它当副作用拦截器用。useEffect的依赖数组就是“这段逻辑关心谁”:空数组=只在挂载时跑一次;写了变量=该变量变才跑;不写=每次渲染都跑;return的函数=卸载清理。- 一个函数组件可以写多个
useEffect,各管一件事(开定时器、监听某个值、清理监听),比类组件把逻辑全塞进一个componentDidMount更好拆、更好删。 - 数据在谁身上,修改方法就定义在谁身上;子组件只负责“报告发生了什么”(传 id 上去),父组件负责“改数据”。这条决定了添加和删除能不能一次写对。
key是 React 认行的唯一凭据,稳定 id 必须在数据生成的那一刻就产生并跟着数据走,不要在渲染时临时造。- 函数组件没有
this,状态和 ref 都是解构出来的局部变量,命名上保持xxx/setXxx的对应关系,能省掉大量“这变量是哪来的”的排查时间。