荆门市网站建设_网站建设公司_C#_seo优化
2026/1/15 12:44:06 网站建设 项目流程

在前端开发的学习路径中,Todo List(待办事项清单)被称为“Hello World”级别的实战项目。虽然看起来简单,但它涵盖了 CRUD(增删改查)、组件拆分、状态管理等核心逻辑。

今天我们将通过一份基于 Vite + React + Stylus 的源码,深入拆解 React 的核心开发模式。

1. 项目架构与组件化思维

React 的核心思想是组件化。我们将 UI 拆分成独立的、可复用的部分。在这个项目中,我们的组件结构非常清晰:

  • App(父组件): 整个应用的容器,负责“持有数据”和“管理逻辑”。它是唯一的数据源(Source of Truth)
  • TodoInput(子组件): 负责用户输入,将新任务传递给父组件。
  • TodoList(子组件): 负责展示任务列表,处理完成/删除操作。
  • TodoStarts(子组件): 负责展示统计信息(总数、剩余、已完成),并提供清除功能。

这种结构体现了 React 开发的一个重要原则:状态提升(Lifting State Up)。因为TodoList需要展示数据,TodoStarts需要统计数据,所以我们将数据统一放在它们的共同父组件App中管理。

2. 核心知识点拆解

2.1 State 管理与 Hooks 高级用法

App.jsx中,我们使用了useState来管理任务列表数据。

// App.jsx const [todos, setTodos] = useState(() => { // 💡 知识点:Lazy Initialization (惰性初始化) const saved = localStorage.getItem('todos'); return saved ? JSON.parse(saved) : []; });

解析:

通常我们写 useState([]),但这里传入了一个函数。

  • 为什么?localStorage.getItem是同步操作,如果直接写在组件体内,每次组件重新渲染都会执行,影响性能。
  • 惰性初始化:传入函数后,React 只会在组件首次渲染时执行该逻辑,后续渲染会跳过,这是性能优化的一个小技巧。

2.2 受控组件 (Controlled Components)

TodoInput.jsx中,我们处理用户输入的方式与 Vue 的v-model不同。

// TodoInput.jsx const [inputValue, setInputValue] = useState(''); // ... render <input type="text" value={inputValue} // 1. 绑定状态 onChange={e => setInputValue(e.target.value)} // 2. 监听变化并更新状态 />

解析:

React 提倡单向数据流。

  1. 输入框的值由 React state (inputValue) 控制。

  2. 用户的输入触发onChange

  3. setInputValue 更新 state,React 重新渲染组件,输入框显示新值。

    这被称为受控组件,它保证了数据和视图的严格同步。

2.3 组件通信:父传子 (Props)

数据如何从App流向TodoList?通过Props

// App.jsx (父组件) <TodoList todos={todos} ... /> // TodoList.jsx (子组件) const TodoList = (props) => { const { todos } = props; // 解构获取数据 // ... }

解析:

父组件将 todos 数组作为属性传递给子组件,子组件只能读取,严禁直接修改 Props。这是 React 数据流向单一性的铁律。

2.4 组件通信:子传父 (回调函数)

子组件想要修改数据怎么办?例如用户在 TodoInput 点击了添加,或者在 TodoList 点击了删除。

答案:父组件传递“修改数据的方法”给子组件。

场景一:添加任务

// App.jsx (父组件定义方法) const addTodo = (text) => { setTodos([...todos, { id: Date.now(), text, completed: false }]) } // 传递给子组件 <TodoInput onAdd={addTodo} /> // TodoInput.jsx (子组件调用) const handleSubmit = (e) => { e.preventDefault(); onAdd(inputValue); // 调用父组件传下来的函数 setInputValue(''); }

场景二:兄弟组件通信(间接)

TodoInput 添加的数据,最终显示在了 TodoList 中。它们之间没有直接联系,而是通过:

  1. TodoInput通知App更新 state。

  2. Appstate 变化,触发重新渲染。

  3. App将新的todos传给TodoList

    这就是状态共享的魅力。

2.5 交互逻辑详解:不可变数据的删除与更新

在 React 中,更新状态(State)有一个核心原则:永远不要直接修改 State,而是用新数据替换旧数据。这一点在删除和勾选操作中体现得淋漓尽致。

1. 删除功能:Filter 的妙用

点击删除按钮时,我们需要移除列表中的某一项。在App.jsx中,我们没有使用数组的splice方法(因为它会直接修改原数组),而是使用了filter

// App.jsx const deleteTodo = (id) => { // 💡 知识点:Immutable Update (不可变更新) // filter 返回一个新数组,不包含被删除的那一项 setTodos(todos.filter(todo => todo.id !== id)) }

在 UI 组件TodoList.jsx中,我们需要注意事件绑定的写法:

// TodoList.jsx <button onClick={() => onDelete(todo.id)}>Delete</button>

⚠️ 新手易错点:

一定要写成箭头函数 () => onDelete(todo.id)。

如果写成 onClick={onDelete(todo.id)},函数会在组件渲染时立即执行,导致无限循环报错。

2. 勾选切换:Map 与展开运算符

这是 React 数组更新中最常见的模式。当用户点击 Checkbox 时,我们需要把数组中特定 ID的那一项的completed状态取反,同时保持其他项不变。

// App.jsx const toggleTodo = (id) => { setTodos(todos.map(todo => // 遍历每一个 item,找到 id 匹配的那一个 todo.id === id ? { ...todo, // 💡 知识点:Spread Operator (展开运算符) 复制原有属性 completed: !todo.completed // 覆盖 completed 属性 } : todo // id 不匹配的项保持原样 )) }

逻辑解析:

  1. todos.map:生成一个全新的数组,保证不修改原todos引用。
  2. ...todo:利用 ES6 的展开运算符,将旧对象的所有属性(id, text)复制到新对象中。
  3. completed: !todo.completed:单独覆盖我们需要修改的属性。

TodoList.jsx中,我们将这个逻辑绑定到 input 的onChange事件上,同时利用checked属性实现受控组件的双向绑定效果:

// TodoList.jsx <input type="checkbox" checked={todo.completed} // 视图状态依赖数据 onChange={() => onToggle(todo.id)} // 数据更新依赖交互 />

同时也利用这个状态来动态控制 CSS 类名,实现完成时的划线样式:

<li key={todo.id} className={todo.completed ? 'completed' : ''}>

2.6 列表渲染与 Key

TodoList.jsx中,我们使用map方法渲染列表:

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

解析:

  • Key 的重要性:React 需要key来识别哪些元素改变了、添加了或删除了。这里我们使用了todo.id(时间戳) 作为唯一标识。切记不要使用数组索引(index)作为 key,这在列表顺序变化时会导致严重的渲染错误或性能问题。
  • 条件渲染:我们使用三元运算符todos.length === 0 ? (...) : (...)来处理空状态的展示。

2.7 副作用处理 (useEffect)

如何实现数据持久化,刷新页面数据不丢失?我们使用了useEffect

// App.jsx useEffect(() => { localStorage.setItem('todos', JSON.stringify(todos)); }, [todos])

解析:

  • useEffect用于处理副作用(Side Effects),比如数据获取、订阅、手动修改 DOM 等。
  • 依赖数组[todos]:这意味着每当todos状态发生变化时,React 就会执行这个函数,将最新的数据同步到 LocalStorage。

2.8 逻辑复用与短路运算

TodoStarts.jsx中,有一个很优雅的条件渲染写法:

// TodoStarts.jsx { completed > 0 && ( <button onClick={onClearCompleted}>Clear Completed</button> ) }

解析:

利用 JavaScript 的逻辑与 (&&) 运算。只有当 completed > 0 为真时,后面的 Button 才会渲染。这比写 if/else 要简洁得多。

3. 总结

通过这个小小的 Todo List,我们完整实践了 React 开发的“黄金法则”:

  1. 数据驱动视图:UI 是 State 的投影 (UI = f(State))。
  2. 单向数据流:数据向下流动 (Props),事件向上冒泡 (Callback)。
  3. 不可变性:在App.jsxdeleteTodo等方法中,我们使用了filtermap返回新数组,而不是直接修改原数组 (push/splice),这对 React 识别状态变化至关重要。
// ❌ 错误示范 todos.push(newItem); // ✅ 正确示范 (不可变更新) setTodos([...todos, newItem]);

4. 项目源码

4.1 App.jsx

import{useState,useEffect}from'react'import'./styles/app.styl'importTodoListfrom'./components/TodoList.jsx'importTodoInputfrom'./components/TodoInput.jsx'importTodoStartsfrom'./components/TodoStarts.jsx'functionApp(){// 子组件共享的数据状态const[todos,setTodos]=useState(()=>{// 高级用法constsaved=localStorage.getItem('todos');returnsaved?JSON.parse(saved):[];});// 子组件修改数据的方法constaddTodo=(text)=>{setTodos([...todos,{id:Date.now(),text,completed:false}])}constdeleteTodo=(id)=>{setTodos(todos.filter(todo=>todo.id!==id))}consttoggleTodo=(id)=>{setTodos(todos.map(todo=>todo.id===id?{...todo,completed:!todo.completed}:todo))}constclearCompleted=()=>{setTodos(todos.filter(todo=>!todo.completed))}constactiveCount=todos.filter(todo=>!todo.completed).length;constcompletedCount=todos.filter(todo=>todo.completed).length;useEffect(()=>{localStorage.setItem('todos',JSON.stringify(todos));},[todos])return(<div className="todo-app"><h1>My Todo List</h1>{/* 自定义事件 */}<TodoInput onAdd={addTodo}/><TodoList todos={todos}onDelete={deleteTodo}onToggle={toggleTodo}/><TodoStarts total={todos.length}active={activeCount}completed={completedCount}onClearCompleted={clearCompleted}/></div>)}exportdefaultApp;

4.2 TodoInput.jsx

import{useState}from'react'constTodoInput=(props)=>{console.log(props);const{onAdd}=props;// react 不支持 Vue 中的 v-model 那样的双向绑定,react 认为这样性能不好// react 只支持单向绑定,性能好 + onChange 实现数据和视图的同步const[inputValue,setInputValue]=useState('');consthandleSubmit=(e)=>{e.preventDefault();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>)}exportdefaultTodoInput;

4.3 TodoList.jsx

constTodoList=(props)=>{const{todos,onDelete,onToggle}=props;return(<ul className="todo-list">{todos.length===0?(<li className="empty">No todos yet!</li>):(todos.map(todo=>(<li key={todo.id}className={todo.completed?'completed':''}><label><input type="checkbox"checked={todo.completed}onChange={()=>onToggle(todo.id)}/><span>{todo.text}</span></label><button onClick={()=>onDelete(todo.id)}>Delete</button></li>)))}</ul>)}exportdefaultTodoList;

4.4 TodoStarts.jsx

constTodoStarts=(props)=>{const{total,active,completed,onClearCompleted}=props;return(<div className="todo-starts"><p>Total:{total}|Active:{active}|Completed:{completed}</p>{completed>0&&(<button onClick={onClearCompleted}className="clear-btn">Clear Completed</button>)}</div>)}exportdefaultTodoStarts;

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

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

立即咨询