黄冈市网站建设_网站建设公司_ASP.NET_seo优化
2025/12/25 3:38:27 网站建设 项目流程

嘿,各位正在 React 门前反复横跳的新手小伙伴们!👋
是不是经常被“数据该放哪”、“怎么传给子组件”、“子组件想改父组件数据怎么办”这三个终极哲学问题搞得头大?别担心,今天咱们不聊虚的,直接通过一个经典的React + Stylus + Vite实战项目——Todos,带你一次性打通 React 组件通信的任督二脉!
不仅有代码,还有深度解析。准备好咖啡,我们要开始“套娃”了!


一、 项目背景:为什么我们要“套娃”?
在 Vue 里,你可能习惯了v-model的便捷,但在 React 的世界里,一切都是单向数据流。数据就像顺流而下的河水,从父组件流向子组件。
我们的项目结构如下:

  • App.js(大管家):持有所有数据(todos),负责逻辑处理。
  • TodoInput(输入框):负责产生新任务。
  • TodoList(展示列表):展示任务,并允许用户勾选完成或删除。
  • TodoStats(统计看板):展示剩余任务,提供一键清理。

二、 环境准备:Stylus 与 Vite 的碰撞
首先,我们使用的是 Vite 环境。在 React 中引入 CSS 预处理器(如 Stylus)非常简单。
1. 如何引入 Stylus
在 Vite 中,你只需要安装stylus

npm init stylus

然后像这样在App.jsx中引入即可:
JavaScript

import './styles/app.styl' // 直接引入,Vite 会自动帮你处理编译

为什么用 Stylus?因为它简洁,没有大括号和分号的束缚,和 React 的组件化思维很搭。


三、 核心灵魂:App 组件(数据中心化)
在 React 中,如果多个组件(比如输入框和列表)需要共享同一份数据,最正宗的做法就是状态提升(Lifting State Up)。我们将todos放在它们的共同父组件App中。
1. useState 的高级用法:惰性初始化
看这行代码:
JavaScript

const [todos, setTodos] = useState(() => { const saved = localStorage.getItem('todos'); return saved ? JSON.parse(saved) : []; });

💡 超级关键点: useState 可以接收一个函数作为参数。这叫“惰性初始化”。
为什么要这么做? 如果直接写 localStorage.getItem,每次组件重新渲染(render)时都会执行一遍 IO 读取。传一个函数,React 只会在组件第一次挂载时执行它。性能优化,从细节做起!

2. useEffect 的副作用管理
我们要实现“持久化存储”,即刷新页面数据不丢。
JavaScript

useEffect(() => { localStorage.setItem('todos', JSON.stringify(todos)); }, [todos]); // 只有当 todos 发生变化时,才会触发保存

这里使用了useEffect。它的第二个参数[todos]是依赖项,保证了我们只在数据变动时才去写磁盘,优雅!


四、 兄弟组件通信:间接的“曲线救国”
很多新手问:TodoInput 产生的数据,怎么传给 TodoList?
答案: 兄弟组件之间不能直接打招呼!它们必须通过共同的“老爹” App。

  1. TodoInput调用父组件传来的方法,把新数据传回父组件(子传父)。
  2. 父组件更新todos状态。
  3. 父组件把更新后的todos传给TodoList(父传子)。

这就是“父组件负责持有数据,管理数据”的核心原则。


五、 子父通信:自定义事件的“上报”
由于 React 的props 是只读的,子组件绝对不能直接修改父组件传过来的变量。
1. 子组件如何修改父组件的自由变量?
秘诀:父组件不仅把数据传给子,还把“修改数据的方法”也传过去。
JavaScript

// App.jsx 中 const addTodo = (text) => { setTodos([...todos, { id: Date.now(), // 使用时间戳作为唯一 ID text, completed: false, }]); } return ( <TodoInput onAdd={addTodo} /> // 传递方法 )

💡 超级关键点:唯一 ID。遍历数据(map)时必须有key。为什么?React 用虚拟 DOM 算法比对差异时,靠key识别哪个元素变了。如果用index,删掉中间一个元素会导致后续所有元素重绘,性能炸裂。这里我们用Date.now()快速生成唯一 ID。


六、 详解 TodoInput:模拟“双向绑定”
React 不支持v-model,因为它推崇“显式优于隐式”。我们要实现类似功能,需要通过单向绑定 + onChange 监听
JavaScript

const TodoInput = ({ onAdd }) => { const [inputValue, setInputValue] = useState(''); const handleSubmit = (e) => { e.preventDefault(); // 阻止表单默认提交刷新页面 if(!inputValue.trim()) return; onAdd(inputValue); // 调用父组件传来的函数 setInputValue(''); // 清空输入框 } return ( <form className="todo-input" onSubmit={handleSubmit}> <input type="text" value={inputValue} // 绑定状态 onChange={e => setInputValue(e.target.value)} // 监听输入 /> <button type="submit">Add</button> </form> ) }

逻辑闭环:状态改变 -> 触发onChange-> 更新inputValue-> 视图重新渲染。虽然麻烦一点,但每一步都清清楚楚!


七、 详解 TodoList:Props 的清晰解构
在子组件中处理props时,推荐直接在函数参数里或者函数体第一行进行解构。
JavaScript

const TodoList = (props) => { const { todos, onDelete, onToggle } = props; // 清晰的解构 // ... 后面直接使用 todos,而不是 props.todos }

这样做的好处是:一眼就能看出这个组件依赖哪些数据,代码阅读感拉满。
列表渲染与三目运算符
TodoList中,我们使用了大量的三目运算符来控制视图:
JavaScript

{todos.length === 0 ? ( <li className="empty">No todos yet!</li> ) : ( todos.map(...) )}

这是 React 的基本功。记住:React 的大括号{}里可以写任何 JS 表达式。三目运算符是实现条件渲染最干净的方式。


八、为什么 ID 必须是“唯一”的?
TodoList组件里,我们看到todos.map循环时,每个<li>都有一个key={todo.id}。很多新手为了省事会直接用数组的索引index,但这正是万恶之源
1. 为什么不能用 Index?
React 在更新 DOM 时,会通过key来判断哪些元素是新加的、哪些被删除了。

  • 情景模拟:如果你有三个任务 A、B、C,索引分别是 0、1、2。当你删掉了中间的 B,剩下的 A 和 C 索引就变成了 0 和 1。
  • React 的困惑:React 会以为你删掉了 C(原来的索引 2 没了),然后把 B 的内容改成了 C。这不仅浪费性能,在涉及表单输入或动画时,还会产生非常诡异的 UI Bug。

2. 代码中如何实现“唯一 ID”?
在我们的App.jsxaddTodo方法中,是这样处理的:

JavaScript

const addTodo = (text) => { setTodos([...todos, { // 💡 超级关键点:使用时间戳生成唯一 ID id: Date.now(), text, completed: false, }]); }

专业讲解:

  • Date.now():它返回自 1970 年 1 月 1 日 00:00:00 UTC 以来经过的毫秒数。对于像 TodoList 这种个人使用的单机应用,用户点击按钮的速度是不可能超过 1 毫秒一次的,所以这个数字在当前应用中是绝对唯一的。
  • 更专业的方案:在大型商业项目中,我们通常会使用crypto.randomUUID()或者uuid库来生成更长、更复杂、碰撞率几乎为零的字符串 ID。


3. 渲染时的“身份标识”
TodoList.jsx中:
JavaScript

{todos.map(todo => ( <li key={todo.id} className={todo.completed ? 'completed' : ''}> {/* ...内容 */} </li> ))}

有了这个todo.id,React 的Diff 算法(找差异的算法)就能像激光手术一样精准:它知道你只是删掉了 ID 为1734950400000的那一项,而其他项完全不需要重新渲染。


4. ID 的三大纪律

  1. 稳定性:ID 生成后就不应该变(所以不能用Math.random(),因为它每次渲染都会变)。
  2. 唯一性:在当前列表中,不能有两个相同的 ID。
  3. 预测性:通过 ID 我们可以快速在setTodos中定位数据,比如todos.filter(t => t.id !== id)

九、 数据流操作:添加、删除与切换
App.jsx中,我们定义了几个关键操作:

  1. 添加 (addTodo): 使用解构赋值[...todos, newTodo]保证数据的不可变性(Immutability)。不要用push
const addTodo = (text) => { setTodos([...todos, { id: Date.now(),// 时间戳 text, completed: false, }]); }
  1. 删除 (deleteTodo): 使用filter
    JavaScript
const deleteTodo = (id) => { setTodos(todos.filter(todo => todo.id !== id)); }
  1. 切换状态 (toggleTodo): 使用map
    JavaScript
const toggleTodo = (id) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }
  1. 💡 专业术语:这里体现了"数据驱动视图&quot; 。子组件只需发出一个“请求”(调用 ID),由父组件统一更新数据,正确且高效。

十、 总结:React 通信全景图
通过这个项目,我们要记住 React 组件通信的三板斧:

  1. 父传子:通过props直接传。
  2. 子传父:父传一个 callback 函数给子,子在需要时调用。
  3. 兄弟传:状态提升到父组件,通过父组件当中转站。

为什么子组件不能直接修改数据?
因为“统一,正确”。如果每个子组件都能随意修改父组件的数据,调试代码时你会发现根本找不着是谁把数据改坏了。单向数据流保证了数据的可追溯性。


希望这篇文章能帮你搞定 React 组件通信!如果觉得有用,记得点赞、收藏、关注三连哦!我们下期再见!🚀

原文: https://juejin.cn/post/75869399

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询