深度探秘 React Todos 应用:从代码到原理的全面解析

4 阅读12分钟

在前端开发领域,React 以其高效的组件化架构和灵活的数据管理机制备受青睐。本文将基于一个 React + Stylus + Vite 构建的 Todos 应用展开深入剖析,详细解读每一个组件的代码逻辑、核心概念以及它们之间的交互关系。

一、技术栈概述

React

React 是 Facebook 开发的用于构建用户界面的 JavaScript 库,它采用虚拟 DOM(Virtual Document Object Model)技术。虚拟 DOM 是真实 DOM 的轻量级抽象,React 通过对比虚拟 DOM 的变化,批量更新真实 DOM,从而显著提升性能。例如,当一个组件的状态发生变化时,React 会先在虚拟 DOM 上计算出需要更新的部分,然后一次性将这些变化应用到真实 DOM 上,避免了频繁直接操作真实 DOM 带来的性能损耗。

Stylus

Stylus 是一款功能强大的 CSS 预处理器,它允许开发者使用变量、嵌套规则、混合(Mixin)和函数等特性来编写 CSS。这使得 CSS 代码更具结构化和可维护性。例如,我们可以定义一个变量 $primary - color: #007BFF;,然后在整个样式表中使用这个变量来设置颜色,当需要修改主色调时,只需修改变量的值即可,无需逐个查找和替换颜色代码。

Vite

Vite 是新一代前端构建工具,它利用浏览器原生 ES 模块导入的特性,实现了快速的冷启动。在开发过程中,Vite 采用即时的模块热替换(HMR),当代码发生变化时,Vite 能精确地更新变化的部分,而无需重新加载整个页面,大大提升了开发效率。比如在修改一个组件的样式时,浏览器会即时显示修改后的效果,无需手动刷新页面。

二、组件解析

TodoInput 组件

javascript

import { useState } from'react'

const TodoInput = (props) => {
    const { onAdd } = props;
    const [inputValue, setInputValue] = useState('')
    const handleSubmit = (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>
    )
}
export default TodoInput
  1. 功能概述:此组件为用户提供一个输入框,用于输入待办事项,并将输入内容提交给父组件进行处理。

  2. 代码解析

    • 状态管理

      • const [inputValue, setInputValue] = useState('') 使用 React 的 useState Hook 创建了一个名为 inputValue 的状态变量,初始值为空字符串。useState 是 React 中用于在函数式组件中添加状态的重要工具,它返回一个数组,第一个元素是当前状态值,第二个元素是用于更新该状态值的函数。在这里,inputValue 用于存储用户在输入框中输入的内容,setInputValue 则用于更新这个状态。
    • 属性接收

      • const { onAdd } = props; 通过对象解构从 props 中提取 onAdd 属性。在 React 中,props(properties 的缩写)是父组件向子组件传递数据和函数的主要方式。这里的 onAdd 是一个函数,由父组件传递过来,用于处理子组件提交的新任务。
    • 事件处理

      • const handleSubmit = (e) => { } 定义了一个名为 handleSubmit 的函数,该函数会在表单提交时被触发。
      • e.preventDefault(); 这行代码阻止了表单的默认提交行为。在 HTML 中,表单提交时会导致页面刷新,而在 React 应用中,我们通常不希望出现这种情况,因此需要阻止默认行为。
      • onAdd(inputValue); 调用从父组件传递过来的 onAdd 函数,并将当前输入框的值 inputValue 作为参数传递给它。这样,父组件就可以接收到新的待办事项并进行相应处理。
      • setInputValue('') 清空 inputValue 的状态,使输入框回到初始状态,为用户输入下一个待办事项做好准备。
    • 渲染部分

      • 返回一个包含输入框和提交按钮的表单。
      • <input type="text" value={inputValue} onChange={e => setInputValue(e.target.value)} /> 这是一个文本输入框。value 属性将输入框的值与 inputValue 状态绑定,实现单向绑定,即输入框的值始终反映 inputValue 的状态。onChange 事件监听器在输入框内容发生变化时触发,它通过 setInputValue(e.target.value) 更新 inputValue 的状态,从而实现输入框内容与状态的同步。
      • <button type="submit">Add</button> 这是一个提交按钮,当用户点击该按钮时,会触发表单的 submit 事件,进而调用 handleSubmit 函数。

TodoList 组件

javascript

const TodoList = (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)}>X</button>
                        </li>
                    ))
                )
            }
        </ul>
    )
}
export default TodoList
  1. 功能概述:该组件负责展示待办事项列表,并为每个任务提供删除和切换完成状态的功能。

  2. 代码解析

    • 属性接收

      • 通过对象解构从 props 中获取 todos(待办事项数组)、onDelete(删除任务的函数)和 onToggle(切换任务完成状态的函数)。这些属性由父组件传递过来,使 TodoList 组件能够获取到最新的待办事项数据以及相应的操作函数。
    • 条件渲染

      • todos.length === 0? ( ) : ( ) 使用三元运算符进行条件渲染。如果 todos 数组的长度为 0,说明没有待办事项,此时渲染 <li className="empty">No todos yet!</li>,提示用户暂无待办事项。否则,遍历 todos 数组,为每个任务渲染一个列表项。
    • 列表项渲染

      • todos.map(todo => ( ) ) 使用数组的 map 方法遍历 todos 数组,为每个 todo 对象生成一个列表项。
      • <li key={todo.id} className={todo.completed? 'completed' : ''}> 每个列表项的 key 属性设置为任务的 id,这在 React 中非常重要,它帮助 React 高效地识别和更新列表中的元素。className 根据任务的 completed 状态动态添加 completed 类名,用于在样式上区分已完成和未完成的任务。
      • <input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} /> 这是一个复选框,checked 属性根据任务的 completed 状态设置是否选中,实现单向绑定。onChange 事件在复选框状态改变时触发,调用 onToggle 函数并传入当前任务的 id,通知父组件切换该任务的完成状态。
      • <span>{todo.text}</span> 显示任务的文本内容。
      • <button onClick={() => onDelete(todo.id)}>X</button> 这是一个删除按钮,onClick 事件在按钮点击时触发,调用 onDelete 函数并传入当前任务的 id,通知父组件删除该任务。

TodoStats 组件

javascript

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

export default TodoStats
  1. 功能概述:此组件用于展示待办事项的统计信息,包括总任务数、未完成任务数和已完成任务数,并在有已完成任务时提供一个按钮,用于清除所有已完成的任务。

  2. 代码解析

    • 属性接收

      • 通过对象解构从 props 中获取 total(总任务数)、active(未完成任务数)、completed(已完成任务数)以及 onClearCompleted(清除已完成任务的函数)。这些属性由父组件传递过来,为组件提供展示和操作所需的数据和函数。
    • 渲染部分

      • <p>Total: {total}</p><p>Active: {active}</p> 和 <p>Completed: {completed}</p> 分别显示总任务数、未完成任务数和已完成任务数。
      • {completed > 0 && ( ) } 使用逻辑与运算符进行条件渲染。当 completed 大于 0 时,说明存在已完成的任务,此时渲染清除已完成任务的按钮。
      • <button onClick={onClearCompleted} className="clear - btn">Clear Completed</button> 这是一个按钮,onClick 事件在按钮点击时触发,调用 onClearCompleted 函数,通知父组件清除所有已完成的任务。className 设置为 clear - btn,用于在样式表中定义按钮的样式。

三、App 组件与组件通信

javascript

import { useState, useEffect } from'react'
import './styles/app.styl'
import TodoList from './components/TodoList'
import TodoInput from './components/Todoinput'
import TodoStats from './components/TodoStats'

function App() {
    const [todos, setTodos] = useState(() => {
        const saved = localStorage.getItem('todos')
        return saved? JSON.parse(saved) : []
    })
    const addTodo = (text) => {
        setTodos([...todos, {
            id: Date.now(),
            text,
            completed: false
        }])
    }

    const deleteTodo = (id) => {
        setTodos(todos.filter(todo => todo.id!== id))
    }
    const toggleTodo = (id) => {
        setTodos(todos.map(todo =>
            todo.id === id? {...todo, completed:!todo.completed } : todo
        ))
    }
    const clearCompleted = () => {
        setTodos(todos.filter(todo =>!todo.completed))
    }
    const activeCount = todos.filter(todo =>!todo.completed).length
    const completedCount = 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}
            />
            <TodoStats
                total={todos.length}
                active={activeCount}
                completed={completedCount}
                onClearCompleted={clearCompleted}
            />
        </div>
    )
}

export default App
  1. 功能概述App 组件是整个应用的核心,它负责管理应用的状态,包括待办事项列表,以及处理各个子组件之间的通信和交互。

  2. 状态管理与数据初始化

    • useState 与数据持久化

      • const [todos, setTodos] = useState(() => { } ) 使用 useState Hook 创建了 todos 状态,用于存储所有待办事项。useState 的初始值是一个函数,这是 useState 的一种高级用法,称为惰性初始化。在这个函数中,首先通过 localStorage.getItem('todos') 尝试从本地存储中获取已保存的待办事项列表。如果获取到了数据(saved 不为 null),则使用 JSON.parse(saved) 将其解析为 JavaScript 对象并返回;否则返回一个空数组。这样,应用在加载时可以从本地存储中恢复之前保存的待办事项数据,实现数据的持久化。
  3. 数据操作函数

    • addTodo 函数

      • const addTodo = (text) => { } 定义了一个用于添加新待办事项的函数。它接收一个参数 text,代表用户输入的新任务内容。
      • setTodos([...todos, { id: Date.now(), text, completed: false }]) 使用展开运算符 ... 将原有的 todos 数组和一个新的任务对象合并成一个新数组,并通过 setTodos 更新 todos 状态。新任务对象包含一个唯一的 id(使用 Date.now() 获取当前时间戳作为 id)、任务文本 text 和初始的完成状态 completed: false
    • deleteTodo 函数

      • const deleteTodo = (id) => { } 定义了一个用于删除待办事项的函数,接收要删除任务的 id 作为参数。
      • setTodos(todos.filter(todo => todo.id!== id)) 使用数组的 filter 方法创建一个新数组,该数组过滤掉了 id 与传入 id 相等的任务对象,然后通过 setTodos 更新 todos 状态,从而实现删除指定任务的功能。
    • toggleTodo 函数

      • const toggleTodo = (id) => { } 定义了一个用于切换任务完成状态的函数,接收要切换状态的任务 id 作为参数。
      • setTodos(todos.map(todo => todo.id === id? {...todo, completed:!todo.completed } : todo )) 使用数组的 map 方法遍历 todos 数组。对于每个任务对象,如果其 id 与传入的 id 相等,则创建一个新的任务对象,将其 completed 状态取反;否则保持原任务对象不变。最后通过 setTodos 更新 todos 状态,实现切换指定任务完成状态的功能。
    • clearCompleted 函数

      • const clearCompleted = () => { } 定义了一个用于清除所有已完成任务的函数。
      • setTodos(todos.filter(todo =>!todo.completed)) 使用数组的 filter 方法创建一个新数组,该数组只包含未完成的任务对象(即 completed 为 false 的任务),然后通过 setTodos 更新 todos 状态,实现清除所有已完成任务的功能。
  4. 统计信息计算

    • 未完成和已完成任务数计算

      • const activeCount = todos.filter(todo =>!todo.completed).length 通过 filter 方法过滤出 completed 为 false 的任务,然后获取其长度,得到未完成任务的数量。
      • const completedCount = todos.filter(todo => todo.completed).length 通过 filter 方法过滤出 completed 为 true 的任务,然后获取其长度,得到已完成任务的数量。这些统计信息将传递给 TodoStats 组件进行展示。
  5. 副作用操作

    • useEffect 与数据同步

      • useEffect(() => { localStorage.setItem('todos', JSON.stringify(todos)) }, [todos]) 使用 useEffect Hook 来执行副作用操作。useEffect 会在组件挂载和更新后执行传入的回调函数。在这里,当 todos 状态发生变化时(依赖数组 [todos] 中的值发生变化),回调函数会将 todos 数组转换为 JSON 字符串,并通过 localStorage.setItem('todos', JSON.stringify(todos)) 保存到本地存储中,确保本地存储中的数据与应用状态保持同步。
      1. 组件通信
    • 父子组件通信

      • TodoInput 组件通过 onAdd={addTodo} 将 App 组件中的 addTodo 函数传递给 TodoInput 组件,实现子组件向父组件提交新任务的功能。当用户在 TodoInput 组件中输入内容并提交时,会调用 addTodo 函数,将新任务添加到 App 组件的 todos 数组中。这体现了 React 中父组件通过向子组件传递函数,让子组件能够触发父组件状态更新的机制。
      • TodoList 组件通过 todos={todos} 接收 App 组件中的 todos 数组,从而展示最新的待办事项列表。同时,通过 onDelete={deleteTodo} 和 onToggle={toggleTodo} 接收 App 组件传递的 deleteTodo 和 toggleTodo 函数,使得 TodoList 组件中的删除和切换任务完成状态的操作能够更新 App 组件的 todos 状态。这展示了父组件向子组件传递数据和操作数据的函数,实现子组件与父组件状态同步的过程。
      • TodoStats 组件通过 total={todos.length}active={activeCount}completed={completedCount} 接收 App 组件计算出的待办事项统计信息,用于展示。并且通过 onClearCompleted={clearCompleted} 接收 App 组件的 clearCompleted 函数,实现当用户点击清除已完成任务按钮时,调用该函数更新 App 组件的 todos 状态。这体现了父组件向子组件传递数据和操作函数,以实现子组件展示和修改父组件状态相关功能。
    • 兄弟组件通信TodoInputTodoList 和 TodoStats 作为兄弟组件,它们之间并不直接通信,而是通过共同的父组件 App 进行间接通信。App 组件作为数据和逻辑的管理者,持有共享数据 todos 以及对这些数据进行操作的函数。App 组件将数据和函数通过 props 传递给各个子组件,使得子组件之间能够通过影响父组件的状态来间接实现数据共享和交互。例如,TodoInput 组件添加新任务,会改变 App 组件的 todos 状态,进而影响 TodoList 和 TodoStats 组件的展示;TodoList 组件中删除任务或切换任务完成状态,同样会改变 App 组件的 todos 状态,从而影响 TodoStats 组件展示的统计信息。这种通过父组件进行数据和状态管理的方式,是 React 中实现兄弟组件通信的常见模式,有助于保持组件之间的低耦合性,提高代码的可维护性和扩展性。

通过对 React Todos 应用的全面且深入的剖析,我们详细了解了每个组件的功能、代码逻辑、状态管理以及组件间的通信机制。这不仅加深了我们对 React 核心概念和应用开发技巧的理解,也为构建更为复杂和强大的前端应用奠定了坚实的基础。无论是初学者学习 React 的基本原理,还是有经验的开发者深入研究组件通信和状态管理的最佳实践,这个案例都具有重要的参考价值。