Skip to content

Key的作用与优化

学习目标

通过本章学习,你将深入理解:

  • key属性的作用和重要性
  • key的选择原则和最佳实践
  • 使用index作为key的问题和场景
  • key与React Diff算法的深层关系
  • key对性能的具体影响
  • 常见错误和完整解决方案
  • 复杂场景下的key策略
  • React 19中的key优化

第一部分:key的作用

1.1 为什么需要key

jsx
// ❌ 没有key的问题演示
function WithoutKeyProblem() {
  const [items, setItems] = useState(['A', 'B', 'C']);
  
  const addToStart = () => {
    setItems(['New', ...items]);
  };
  
  return (
    <div>
      <button onClick={addToStart}>添加到开头</button>
      <ul>
        {items.map(item => (
          <li>
            <input type="checkbox" />
            <span>{item}</span>
          </li>
        ))}
      </ul>
      {/* 
        问题:
        1. 控制台警告:Each child should have a unique "key"
        2. 添加新项后,checkbox的选中状态会错位
        3. 原因:React无法识别哪个元素是新的,哪个是旧的
      */}
    </div>
  );
}

// ✅ 使用key解决
function WithKeySolution() {
  const [items, setItems] = useState([
    { id: 1, text: 'A' },
    { id: 2, text: 'B' },
    { id: 3, text: 'C' }
  ]);
  
  const addToStart = () => {
    setItems([
      { id: Date.now(), text: 'New' },
      ...items
    ]);
  };
  
  return (
    <div>
      <button onClick={addToStart}>添加到开头</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            <input type="checkbox" />
            <span>{item.text}</span>
          </li>
        ))}
      </ul>
      {/* 
        有key后:
        1. 没有警告
        2. checkbox状态正确保持
        3. React知道哪个是新元素,正确地只插入新元素
      */}
    </div>
  );
}

1.2 key与Diff算法的关系

jsx
// Diff算法如何使用key

// 场景1:在列表开头插入元素
// 旧列表
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
  <li key="c">C</li>
</ul>

// 新列表(在开头插入D)
<ul>
  <li key="d">D</li>
  <li key="a">A</li>
  <li key="b">B</li>
  <li key="c">C</li>
</ul>

// 有key的情况:
// React通过key识别:
// 1. key="d"是新的 → 插入新元素
// 2. key="a","b","c"已存在 → 复用DOM
// 结果:只创建1个新DOM元素

// 无key的情况:
// React按位置比较:
// 1. 位置0: "A"变为"D" → 更新DOM
// 2. 位置1: "B"变为"A" → 更新DOM
// 3. 位置2: "C"变为"B" → 更新DOM
// 4. 位置3: 无变为"C" → 插入DOM
// 结果:更新3个DOM + 插入1个DOM(效率低)

function DiffAlgorithmDemo() {
  const [items, setItems] = useState([
    { id: 'a', text: 'A' },
    { id: 'b', text: 'B' },
    { id: 'c', text: 'C' }
  ]);
  
  const [operations, setOperations] = useState([]);
  
  const addToStart = () => {
    const newItem = { id: 'd', text: 'D' };
    setItems([newItem, ...items]);
    setOperations(['插入新元素D']);
  };
  
  const reverse = () => {
    setItems([...items].reverse());
    setOperations(['重新排列元素(无新增)']);
  };
  
  const deleteFirst = () => {
    setItems(items.slice(1));
    setOperations(['删除第一个元素']);
  };
  
  return (
    <div>
      <div>
        <button onClick={addToStart}>开头插入</button>
        <button onClick={reverse}>反转</button>
        <button onClick={deleteFirst}>删除第一个</button>
      </div>
      
      <div>
        <h4>操作记录:</h4>
        <ul>
          {operations.map((op, i) => (
            <li key={i}>{op}</li>
          ))}
        </ul>
      </div>
      
      <div>
        <h4>列表:</h4>
        <ul>
          {items.map(item => (
            <li key={item.id}>
              {item.text}
              <input type="text" defaultValue={item.text} />
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

1.3 key的唯一性要求

jsx
function KeyUniqueness() {
  const items = [
    { id: 1, name: 'A' },
    { id: 2, name: 'B' },
    { id: 1, name: 'C' }  // ⚠️ id重复!
  ];
  
  return (
    <div>
      {/* ❌ 错误:key重复 */}
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      {/* Warning: Encountered two children with the same key */}
      
      {/* ✅ 正确:确保key唯一 */}
      <ul>
        {items.map((item, index) => (
          <li key={`${item.id}-${index}`}>{item.name}</li>
        ))}
      </ul>
      
      {/* 💡 key只需要在兄弟节点中唯一,不同列表可以有相同key */}
      <div>
        <ul>
          {items.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
        
        <ol>
          {items.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ol>
        {/* 两个不同的列表,可以有相同的key */}
      </div>
    </div>
  );
}

第二部分:key的选择

2.1 使用唯一ID(最佳实践)

jsx
function BestKeyPractice() {
  // ✅ 最佳:数据库ID
  const users = [
    { userId: 101, name: 'Alice' },
    { userId: 102, name: 'Bob' }
  ];
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.userId}>{user.name}</li>
      ))}
    </ul>
  );
}

// ✅ 好:UUID
import { v4 as uuidv4 } from 'uuid';

function UUIDKey() {
  const [items, setItems] = useState([
    { id: uuidv4(), text: 'Item 1' },
    { id: uuidv4(), text: 'Item 2' }
  ]);
  
  const addItem = () => {
    setItems([
      ...items,
      { id: uuidv4(), text: `Item ${items.length + 1}` }
    ]);
  };
  
  return (
    <div>
      <button onClick={addItem}>添加</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

// ✅ 可以:时间戳(单线程添加)
function TimestampKey() {
  const [items, setItems] = useState([]);
  
  const addItem = () => {
    setItems([
      ...items,
      { id: Date.now(), text: 'New Item' }
    ]);
  };
  
  return (
    <div>
      <button onClick={addItem}>添加</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

2.2 使用index作为key(谨慎)

jsx
// ✅ 可以使用index的场景
function SafeIndexUsage() {
  // 条件1:列表是静态的(不会变化)
  const staticItems = ['首页', '关于', '联系我们'];
  
  // 条件2:列表不会重排序
  // 条件3:列表不会过滤
  // 条件4:列表项没有ID
  
  return (
    <nav>
      {staticItems.map((item, index) => (
        <a key={index} href={`/${item}`}>
          {item}
        </a>
      ))}
    </nav>
  );
}

// ❌ 不能使用index的场景
function UnsafeIndexUsage() {
  const [items, setItems] = useState([
    { text: 'A' },
    { text: 'B' },
    { text: 'C' }
  ]);
  
  const shuffle = () => {
    setItems([...items].sort(() => Math.random() - 0.5));
  };
  
  const addToStart = () => {
    setItems([{ text: 'New' }, ...items]);
  };
  
  const remove = (index) => {
    setItems(items.filter((_, i) => i !== index));
  };
  
  return (
    <div>
      <button onClick={shuffle}>打乱</button>
      <button onClick={addToStart}>添加到开头</button>
      
      <ul>
        {/* ❌ 不好:列表会重排序,使用index会导致问题 */}
        {items.map((item, index) => (
          <li key={index}>
            <input type="text" defaultValue={item.text} />
            <button onClick={() => remove(index)}>删除</button>
          </li>
        ))}
      </ul>
      {/* 
        问题:
        - 打乱顺序后,输入框的值和items对不上
        - 删除元素后,可能删错
      */}
    </div>
  );
}

// ✅ 正确:使用稳定的ID
function StableKeyUsage() {
  const [items, setItems] = useState([
    { id: 1, text: 'A' },
    { id: 2, text: 'B' },
    { id: 3, text: 'C' }
  ]);
  
  const shuffle = () => {
    setItems([...items].sort(() => Math.random() - 0.5));
  };
  
  const addToStart = () => {
    setItems([{ id: Date.now(), text: 'New' }, ...items]);
  };
  
  const remove = (id) => {
    setItems(items.filter(item => item.id !== id));
  };
  
  return (
    <div>
      <button onClick={shuffle}>打乱</button>
      <button onClick={addToStart}>添加到开头</button>
      
      <ul>
        {items.map(item => (
          <li key={item.id}>
            <input type="text" defaultValue={item.text} />
            <button onClick={() => remove(item.id)}>删除</button>
          </li>
        ))}
      </ul>
      {/* 
        优点:
        - 无论怎么打乱,每个元素的key都稳定
        - 输入框的值总是跟随正确的item
        - 删除操作准确无误
      */}
    </div>
  );
}

2.3 组合key

jsx
// 多级列表的key组合
function CompositeKeyExample() {
  const categories = [
    {
      id: 1,
      name: '分类1',
      subcategories: [
        {
          id: 1,  // ⚠️ 注意:子分类的id可能与其他分类的子分类重复
          name: '子分类1-1',
          items: [
            { id: 1, name: '项目1-1-1' },  // ⚠️ 项目id也可能重复
            { id: 2, name: '项目1-1-2' }
          ]
        },
        {
          id: 2,
          name: '子分类1-2',
          items: [
            { id: 1, name: '项目1-2-1' },  // id=1,与上面重复
            { id: 2, name: '项目1-2-2' }
          ]
        }
      ]
    },
    {
      id: 2,
      name: '分类2',
      subcategories: [
        {
          id: 1,  // id=1,与分类1的子分类重复
          name: '子分类2-1',
          items: [
            { id: 1, name: '项目2-1-1' }
          ]
        }
      ]
    }
  ];
  
  return (
    <div className="multi-level-list">
      {categories.map(category => (
        // 第一层:category.id是唯一的
        <div key={category.id} className="category">
          <h2>{category.name}</h2>
          
          {category.subcategories.map(sub => (
            // 第二层:组合category.id和sub.id确保唯一
            <div key={`${category.id}-${sub.id}`} className="subcategory">
              <h3>{sub.name}</h3>
              
              <ul>
                {sub.items.map(item => (
                  // 第三层:组合所有层级的id确保唯一
                  <li key={`${category.id}-${sub.id}-${item.id}`}>
                    {item.name}
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

// 更简洁的组合key方法
function CompositeKeyHelper() {
  // 创建组合key的辅助函数
  const createKey = (...parts) => parts.join('-');
  
  return (
    <div>
      {categories.map(cat => (
        <div key={cat.id}>
          {cat.subcategories.map(sub => (
            <div key={createKey(cat.id, sub.id)}>
              {sub.items.map(item => (
                <li key={createKey(cat.id, sub.id, item.id)}>
                  {item.name}
                </li>
              ))}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

2.4 key的生成策略

jsx
// 策略1:使用数据的自然ID
function NaturalIDKey({ products }) {
  return (
    <ul>
      {products.map(product => (
        <li key={product.sku}>  {/* 使用SKU作为key */}
          {product.name}
        </li>
      ))}
    </ul>
  );
}

// 策略2:组合多个字段
function CompositeFieldKey({ orders }) {
  return (
    <ul>
      {orders.map(order => (
        <li key={`${order.userId}-${order.timestamp}`}>
          订单 #{order.orderId}
        </li>
      ))}
    </ul>
  );
}

// 策略3:内容哈希
import { hashCode } from './utils';

function ContentHashKey({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={hashCode(JSON.stringify(item))}>
          {item.content}
        </li>
      ))}
    </ul>
  );
}

// 策略4:自动生成ID
function AutoGeneratedKey() {
  const [items, setItems] = useState([]);
  const nextId = useRef(1);
  
  const addItem = (text) => {
    setItems([
      ...items,
      { id: nextId.current++, text }
    ]);
  };
  
  return (
    <div>
      <button onClick={() => addItem('New Item')}>添加</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

第三部分:key的性能影响

3.1 正确的key提升性能

jsx
function PerformanceComparison() {
  const [items, setItems] = useState(
    Array.from({ length: 1000 }, (_, i) => ({
      id: i,
      text: `Item ${i}`,
      value: Math.random()
    }))
  );
  
  const deleteFirst = () => {
    setItems(items.slice(1));
  };
  
  const addToStart = () => {
    setItems([
      { id: Date.now(), text: 'New', value: Math.random() },
      ...items
    ]);
  };
  
  const shuffle = () => {
    setItems([...items].sort(() => Math.random() - 0.5));
  };
  
  return (
    <div>
      <div>
        <button onClick={deleteFirst}>删除第一项</button>
        <button onClick={addToStart}>开头添加</button>
        <button onClick={shuffle}>打乱顺序</button>
      </div>
      
      <ul>
        {items.map(item => (
          <li key={item.id}>
            <input type="text" defaultValue={item.text} />
            <span>{item.value.toFixed(3)}</span>
          </li>
        ))}
      </ul>
      
      {/* 
        性能分析:
        
        使用item.id作为key:
        - 删除第一项:React识别删除了id=0的项,其他999项保持不变
        - 开头添加:React只需插入1个新元素
        - 打乱顺序:React知道元素只是移动位置,复用所有DOM
        
        使用index作为key:
        - 删除第一项:所有index都变了,React认为所有项都变了
        - 需要更新999个DOM元素
        - 性能差距:10-100倍
      */}
    </div>
  );
}

3.2 错误的key降低性能

jsx
// ❌ 最差:使用随机数
function WorstKeyPractice() {
  const items = ['A', 'B', 'C'];
  
  return (
    <ul>
      {items.map(item => (
        <li key={Math.random()}>  {/* ❌ 每次渲染都是新key */}
          <input type="text" defaultValue={item} />
        </li>
      ))}
    </ul>
  );
  
  // 后果:
  // - 每次渲染,React都认为是全新的元素
  // - 完全销毁重建所有DOM
  // - 输入框的值会丢失
  // - 极差的性能
}

// ❌ 很差:使用不稳定的值
function BadKeyPractice() {
  const items = ['A', 'B', 'C'];
  
  return (
    <ul>
      {items.map(item => (
        <li key={Date.now() + item}>  {/* ❌ 时间戳不稳定 */}
          {item}
        </li>
      ))}
    </ul>
  );
}

// ❌ 差:使用对象作为key
function ObjectAsKey() {
  const items = [
    { data: { id: 1 }, name: 'A' },
    { data: { id: 2 }, name: 'B' }
  ];
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.data}>  {/* ❌ 对象会转为字符串"[object Object]" */}
          {item.name}
        </li>
      ))}
    </ul>
  );
}

第四部分:复杂场景的key处理

4.1 动态列表的key

jsx
function DynamicListKey() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');
  
  // 过滤后的todos
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
  
  return (
    <div>
      <div>
        <button onClick={() => setFilter('all')}>全部</button>
        <button onClick={() => setFilter('active')}>进行中</button>
        <button onClick={() => setFilter('completed')}>已完成</button>
      </div>
      
      <ul>
        {filteredTodos.map(todo => (
          // ✅ 使用todo.id作为key,即使过滤改变,key仍然稳定
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

4.2 分页列表的key

jsx
function PaginatedListKey({ items, pageSize }) {
  const [currentPage, setCurrentPage] = useState(1);
  
  const currentItems = useMemo(() => {
    const start = (currentPage - 1) * pageSize;
    const end = start + pageSize;
    return items.slice(start, end);
  }, [items, currentPage, pageSize]);
  
  return (
    <div>
      <ul>
        {currentItems.map(item => (
          // ✅ 使用item.id,而不是页内索引
          <li key={item.id}>
            {item.name}
          </li>
        ))}
      </ul>
      
      <div>
        <button onClick={() => setCurrentPage(p => p - 1)}>上一页</button>
        <span>第 {currentPage} 页</span>
        <button onClick={() => setCurrentPage(p => p + 1)}>下一页</button>
      </div>
    </div>
  );
}

第五部分:最佳实践

5.1 key选择决策树

jsx
// 选择key的决策流程
function KeyDecisionTree() {
  const decisionFlow = `
    数据有唯一ID吗?
    ├─ 是 → 使用数据ID作为key ✅
    │   例如:key={item.id}

    └─ 否 → 列表会重排序、过滤或改变吗?
        ├─ 是 → 生成稳定的唯一ID ✅
        │   例如:key={uuidv4()} 或 key={Date.now()}

        └─ 否 → 列表是否静态不变?
            ├─ 是 → 可以使用index ⚠️
            │   例如:key={index}

            └─ 否 → 组合多个字段生成唯一key ✅
                例如:key={`${item.type}-${item.name}`}
  `;
  
  console.log(decisionFlow);
}

5.2 key的常见错误

jsx
// ❌ 错误1:使用不稳定的key
function MistakeUnstableKey() {
  const items = ['A', 'B', 'C'];
  
  return (
    <ul>
      {items.map(item => (
        <li key={Math.random()}>{item}</li>  // 每次都变
      ))}
    </ul>
  );
}

// ❌ 错误2:key重复
function MistakeDuplicateKey() {
  const items = [
    { id: 1, name: 'A' },
    { id: 1, name: 'B' },  // id重复
    { id: 2, name: 'C' }
  ];
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// ❌ 错误3:用index但列表会变化
function MistakeIndexWithChanges() {
  const [items, setItems] = useState(['A', 'B', 'C']);
  
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>  // 列表会删除、添加、排序
          <input defaultValue={item} />
        </li>
      ))}
    </ul>
  );
}

// ❌ 错误4:key包含对象
function MistakeObjectInKey() {
  const items = [
    { data: { id: 1 }, name: 'A' }
  ];
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.data}>{item.name}</li>  // 对象转字符串
      ))}
    </ul>
  );
}

// ✅ 正确的修复
function CorrectKeyUsage() {
  const [items, setItems] = useState([
    { id: uuidv4(), name: 'A' },
    { id: uuidv4(), name: 'B' },
    { id: uuidv4(), name: 'C' }
  ]);
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <input defaultValue={item.name} />
        </li>
      ))}
    </ul>
  );
}

第六部分:实战案例

6.1 Todo列表的key

jsx
function TodoListWithKey() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习React', completed: false, priority: 'high' },
    { id: 2, text: '写代码', completed: false, priority: 'medium' },
    { id: 3, text: '看文档', completed: true, priority: 'low' }
  ]);
  
  const [filter, setFilter] = useState('all');
  const [sortBy, setSortBy] = useState('default');
  
  // 过滤
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(t => !t.completed);
      case 'completed':
        return todos.filter(t => t.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
  
  // 排序
  const sortedTodos = useMemo(() => {
    const result = [...filteredTodos];
    
    switch (sortBy) {
      case 'priority':
        const order = { high: 0, medium: 1, low: 2 };
        return result.sort((a, b) => order[a.priority] - order[b.priority]);
      case 'name':
        return result.sort((a, b) => a.text.localeCompare(b.text));
      default:
        return result;
    }
  }, [filteredTodos, sortBy]);
  
  const addTodo = (text) => {
    setTodos([
      ...todos,
      {
        id: Date.now(),  // ✅ 使用时间戳作为唯一ID
        text,
        completed: false,
        priority: 'medium'
      }
    ]);
  };
  
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };
  
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <div>
      <div className="controls">
        <select value={filter} onChange={e => setFilter(e.target.value)}>
          <option value="all">全部</option>
          <option value="active">进行中</option>
          <option value="completed">已完成</option>
        </select>
        
        <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
          <option value="default">默认排序</option>
          <option value="priority">按优先级</option>
          <option value="name">按名称</option>
        </select>
      </div>
      
      <ul className="todo-list">
        {sortedTodos.map(todo => (
          // ✅ 关键:使用todo.id作为key
          // 无论怎么过滤、排序,每个todo的key都不变
          <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>
            <span className={`priority-${todo.priority}`}>
              {todo.priority}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

6.2 可拖拽列表的key

jsx
function DraggableListWithKey() {
  const [items, setItems] = useState([
    { id: 'item-1', text: 'Item 1', order: 0 },
    { id: 'item-2', text: 'Item 2', order: 1 },
    { id: 'item-3', text: 'Item 3', order: 2 }
  ]);
  
  const [draggedId, setDraggedId] = useState(null);
  
  const handleDragStart = (id) => {
    setDraggedId(id);
  };
  
  const handleDrop = (dropId) => {
    if (!draggedId) return;
    
    const dragIndex = items.findIndex(item => item.id === draggedId);
    const dropIndex = items.findIndex(item => item.id === dropId);
    
    const newItems = [...items];
    const [dragged] = newItems.splice(dragIndex, 1);
    newItems.splice(dropIndex, 0, dragged);
    
    setItems(newItems);
    setDraggedId(null);
  };
  
  return (
    <ul>
      {items.map(item => (
        // ✅ 使用稳定的item.id作为key
        // 拖拽改变顺序时,React知道元素只是移动,不是新建
        <li
          key={item.id}
          draggable
          onDragStart={() => handleDragStart(item.id)}
          onDragOver={e => e.preventDefault()}
          onDrop={() => handleDrop(item.id)}
          style={{
            opacity: draggedId === item.id ? 0.5 : 1,
            cursor: 'move'
          }}
        >
          {item.text}
        </li>
      ))}
    </ul>
  );
}

练习题

基础练习

  1. 理解key的作用,创建有key和无key的列表对比
  2. 对比使用index和id作为key的差异
  3. 修复key相关的警告
  4. 实现一个使用组合key的嵌套列表

进阶练习

  1. 实现一个可拖拽排序的列表,体验key的重要性
  2. 优化大列表的key选择策略
  3. 处理动态列表的key(支持增删改查)
  4. 实现一个性能对比工具,展示不同key策略的性能差异

高级练习

  1. 分析key对React Diff算法的具体影响
  2. 实现一个智能key生成器
  3. 优化复杂嵌套列表的key策略
  4. 创建一个key问题诊断工具

通过本章学习,你已经深入理解了key的作用、选择和优化策略。正确使用key是React列表渲染的核心,也是性能优化的关键。记住这个黄金法则:永远使用稳定且唯一的ID作为key,避免使用index(除非列表完全静态)!继续学习,成为React列表渲染专家!