← 返回目录 | 下一课:生命周期 →

第6课: 列表渲染与Key

动态数据展示详解

🎯 学习目标

1. 列表渲染基础

列表渲染:在React中,我们使用JavaScript的map()方法来渲染列表。map方法会将数组中的每个元素转换为一个React元素。

1.1 基本列表渲染

function NumberList() {
    const numbers = [1, 2, 3, 4, 5];
    
    return (
        <ul>
            {numbers.map((number) => (
                <li key={number}>{number}</li>
            ))}
        </ul>
    );
}

1.2 渲染对象列表

function UserList() {
    const users = [
        { id: 1, name: '张三', age: 25 },
        { id: 2, name: '李四', age: 30 },
        { id: 3, name: '王五', age: 28 }
    ];
    
    return (
        <div>
            <h2>用户列表</h2>
            <ul>
                {users.map((user) => (
                    <li key={user.id}>
                        {user.name} - {user.age}岁
                    </li>
                ))}
            </ul>
        </div>
    );
}

2. Key详解

Key的作用:Key是React用来识别哪些元素改变了、添加了或被移除的特殊属性。它帮助React高效地更新DOM。

2.1 为什么需要Key?

重要警告:如果没有Key,React会使用索引作为默认值,这会导致性能问题和潜在的bug。

2.2 Key的工作原理

// 没有Key的情况(不推荐)
function BadList() {
    const items = ['A', 'B', 'C'];
    
    return (
        <ul>
            {items.map((item, index) => (
                <li>{item}</li>  // 没有Key,使用索引
            ))}
        </ul>
    );
}

// 有Key的情况(推荐)
function GoodList() {
    const items = [
        { id: 1, text: 'A' },
        { id: 2, text: 'B' },
        { id: 3, text: 'C' }
    ];
    
    return (
        <ul>
            {items.map((item) => (
                <li key={item.id}>{item.text}</li>  // 使用唯一ID作为Key
            ))}
        </ul>
    );
}

2.3 Key的最佳实践

选择Key的原则:

3. Key的选择策略

3.1 使用唯一ID(最佳)

function BestKeyExample() {
    const todos = [
        { id: 'todo-1', text: '学习React' },
        { id: 'todo-2', text: '写代码' },
        { id: 'todo-3', text: '阅读文档' }
    ];
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>
                    {todo.text}
                </li>
            ))}
        </ul>
    );
}

3.2 使用索引(仅限静态列表)

function IndexKeyExample() {
    // 静态列表,不会重新排序或过滤
    const staticItems = ['首页', '关于', '联系'];
    
    return (
        <nav>
            {staticItems.map((item, index) => (
                <a key={index} href={`#${item}`}>
                    {item}
                </a>
            ))}
        </nav>
    );
}

避免使用索引的场景:

3.3 生成唯一Key

function GeneratedKeyExample() {
    const items = ['苹果', '香蕉', '橙子'];
    
    return (
        <ul>
            {items.map((item, index) => (
                <li key={`${item}-${index}`}>
                    {item}
                </li>
            ))}
        </ul>
    );
}

4. 列表渲染的常见操作

4.1 过滤列表

function FilteredList() {
    const users = [
        { id: 1, name: '张三', age: 25, active: true },
        { id: 2, name: '李四', age: 30, active: false },
        { id: 3, name: '王五', age: 28, active: true }
    ];
    
    // 只显示活跃用户
    const activeUsers = users.filter(user => user.active);
    
    return (
        <div>
            <h2>活跃用户</h2>
            <ul>
                {activeUsers.map(user => (
                    <li key={user.id}>
                        {user.name} - {user.age}岁
                    </li>
                ))}
            </ul>
        </div>
    );
}

4.2 排序列表

function SortedList() {
    const products = [
        { id: 1, name: 'iPhone', price: 5999 },
        { id: 2, name: 'MacBook', price: 9999 },
        { id: 3, name: 'iPad', price: 3299 }
    ];
    
    // 按价格排序
    const sortedProducts = [...products].sort((a, b) => a.price - b.price);
    
    return (
        <div>
            <h2>商品列表(按价格排序)</h2>
            <ul>
                {sortedProducts.map(product => (
                    <li key={product.id}>
                        {product.name} - ¥{product.price}
                    </li>
                ))}
            </ul>
        </div>
    );
}

4.3 添加和删除列表项

function TodoApp() {
    const [todos, setTodos] = useState([
        { id: 1, text: '学习React', completed: false },
        { id: 2, text: '写代码', completed: true }
    ]);
    const [newTodo, setNewTodo] = useState('');
    
    const addTodo = () => {
        if (newTodo.trim() === '') return;
        
        const todo = {
            id: Date.now(),
            text: newTodo,
            completed: false
        };
        
        setTodos([...todos, todo]);
        setNewTodo('');
    };
    
    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
        ));
    };
    
    return (
        <div>
            <input 
                type="text"
                value={newTodo}
                onChange={(e) => setNewTodo(e.target.value)}
                placeholder="添加新任务"
            />
            <button onClick={addTodo}>添加</button>
            
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input 
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span style={{
                            textDecoration: todo.completed ? 'line-through' : 'none'
                        }}>
                            {todo.text}
                        </span>
                        <button onClick={() => deleteTodo(todo.id)}>
                            删除
                        </button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

5. 多维列表渲染

5.1 嵌套列表

function NestedList() {
    const categories = [
        {
            id: 1,
            name: '水果',
            items: ['苹果', '香蕉', '橙子']
        },
        {
            id: 2,
            name: '蔬菜',
            items: ['胡萝卜', '白菜', '西红柿']
        }
    ];
    
    return (
        <div>
            {categories.map(category => (
                <div key={category.id}>
                    <h3>{category.name}</h3>
                    <ul>
                        {category.items.map((item, index) => (
                            <li key={`${category.id}-${index}`}>
                                {item}
                            </li>
                        ))}
                    </ul>
                </div>
            ))}
        </div>
    );
}

5.2 表格渲染

function TableExample() {
    const data = [
        { id: 1, name: '张三', age: 25, city: '北京' },
        { id: 2, name: '李四', age: 30, city: '上海' },
        { id: 3, name: '王五', age: 28, city: '广州' }
    ];
    
    return (
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>姓名</th>
                    <th>年龄</th>
                    <th>城市</th>
                </tr>
            </thead>
            <tbody>
                {data.map(row => (
                    <tr key={row.id}>
                        <td>{row.id}</td>
                        <td>{row.name}</td>
                        <td>{row.age}</td>
                        <td>{row.city}</td>
                    </tr>
                ))}
            </tbody>
        </table>
    );
}

6. 条件渲染列表

6.1 空列表处理

function EmptyListExample() {
    const [items, setItems] = useState([]);
    
    return (
        <div>
            {items.length === 0 ? (
                <p>列表为空</p>
            ) : (
                <ul>
                    {items.map(item => (
                        <li key={item.id}>{item.text}</li>
                    ))}
                </ul>
            )}
        </div>
    );
}

6.2 搜索过滤

function SearchFilter() {
    const [searchTerm, setSearchTerm] = useState('');
    const users = [
        { id: 1, name: '张三', email: 'zhangsan@example.com' },
        { id: 2, name: '李四', email: 'lisi@example.com' },
        { id: 3, name: '王五', email: 'wangwu@example.com' }
    ];
    
    const filteredUsers = users.filter(user =>
        user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
        user.email.toLowerCase().includes(searchTerm.toLowerCase())
    );
    
    return (
        <div>
            <input 
                type="text"
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
                placeholder="搜索用户..."
            />
            
            {filteredUsers.length === 0 ? (
                <p>没有找到匹配的用户</p>
            ) : (
                <ul>
                    {filteredUsers.map(user => (
                        <li key={user.id}>
                            {user.name} ({user.email})
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
}

7. 列表渲染的性能优化

性能优化建议:

import { memo } from 'react';

// 使用memo优化列表项组件
const ListItem = memo(function ListItem({ item, onDelete }) {
    console.log(`渲染: ${item.text}`);
    
    return (
        <li>
            {item.text}
            <button onClick={() => onDelete(item.id)}>
                删除
            </button>
        </li>
    );
});

function OptimizedList() {
    const [items, setItems] = useState([
        { id: 1, text: '项目1' },
        { id: 2, text: '项目2' },
        { id: 3, text: '项目3' }
    ]);
    
    const deleteItem = (id) => {
        setItems(items.filter(item => item.id !== id));
    };
    
    return (
        <ul>
            {items.map(item => (
                <ListItem 
                    key={item.id} 
                    item={item} 
                    onDelete={deleteItem} 
                />
            ))}
        </ul>
    );
}

💡 动手练习

任务:创建一个任务管理应用

要求:

// 参考答案
function TaskManager() {
    const [tasks, setTasks] = useState([
        { id: 1, text: '学习React', completed: false },
        { id: 2, text: '写代码', completed: true },
        { id: 3, text: '阅读文档', completed: false }
    ]);
    const [newTask, setNewTask] = useState('');
    const [filter, setFilter] = useState('all');
    const [searchTerm, setSearchTerm] = useState('');
    
    const addTask = () => {
        if (newTask.trim() === '') return;
        
        const task = {
            id: Date.now(),
            text: newTask,
            completed: false
        };
        
        setTasks([...tasks, task]);
        setNewTask('');
    };
    
    const deleteTask = (id) => {
        setTasks(tasks.filter(task => task.id !== id));
    };
    
    const toggleTask = (id) => {
        setTasks(tasks.map(task =>
            task.id === id
                ? { ...task, completed: !task.completed }
                : task
        ));
    };
    
    const filteredTasks = tasks.filter(task => {
        const matchesFilter = 
            filter === 'all' ||
            (filter === 'completed' && task.completed) ||
            (filter === 'active' && !task.completed);
        
        const matchesSearch = task.text
            .toLowerCase()
            .includes(searchTerm.toLowerCase());
        
        return matchesFilter && matchesSearch;
    });
    
    const completedCount = tasks.filter(task => task.completed).length;
    const activeCount = tasks.length - completedCount;
    
    return (
        <div>
            <h2>任务管理</h2>
            
            <div>
                <input 
                    type="text"
                    value={newTask}
                    onChange={(e) => setNewTask(e.target.value)}
                    placeholder="添加新任务"
                />
                <button onClick={addTask}>添加</button>
            </div>
            
            <div>
                <input 
                    type="text"
                    value={searchTerm}
                    onChange={(e) => setSearchTerm(e.target.value)}
                    placeholder="搜索任务..."
                />
            </div>
            
            <div>
                <button 
                    onClick={() => setFilter('all')}
                    style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
                >
                    全部 ({tasks.length})
                </button>
                <button 
                    onClick={() => setFilter('active')}
                    style={{ fontWeight: filter === 'active' ? 'bold' : 'normal' }}
                >
                    未完成 ({activeCount})
                </button>
                <button 
                    onClick={() => setFilter('completed')}
                    style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal' }}
                >
                    已完成 ({completedCount})
                </button>
            </div>
            
            <ul>
                {filteredTasks.map(task => (
                    <li key={task.id}>
                        <input 
                            type="checkbox"
                            checked={task.completed}
                            onChange={() => toggleTask(task.id)}
                        />
                        <span style={{
                            textDecoration: task.completed ? 'line-through' : 'none'
                        }}>
                            {task.text}
                        </span>
                        <button onClick={() => deleteTask(task.id)}>
                            删除
                        </button>
                    </li>
                ))}
            </ul>
            
            {filteredTasks.length === 0 && (
                <p>没有找到任务</p>
            )}
        </div>
    );
}

📝 关键概念总结