React实验室|随意设置key会导致BUG

1,143 阅读3分钟

本文初稿写于2020年12月5日
react版本为 React v17.0.1
适合读者: 使用react 15 16 17版本的读者
原创声明:本文原创,转载请标注掘金地址即可

BUG复现

请打开demo1观察代码结构后,跟随笔者进行如下操作

  • 点击3次 Add New to End
  • 从上到下依次输入a b c d
  • 点击Sort by Latest

可以看到bug出现了 input框并没有按预想的进行倒序

扩展

  • 把input节点换成React组件
const Input = () => (
  <input />
)
  • Bug依然存在

input组件设置父组件传入的id

  • 往Input组件传入id={props.id}
const Input = (props) => (
  <div>
    {props.id + 1}
    <input id={props.id}/>
  </div>
)

  • Bug依然存在

可控组件

  • input表单项与state关联 形成可控组件
  • codepen
const ToDo = props => {
  const [value, setValue] = React.useState('')
  console.log(props.id, value,'===')
  return (
  <tr>
    <td>
      <label>{props.id}</label>
    
    </td>
    <td>
      <input value={value} onChange={(e) => setValue(e.target.value)}/>
    </td>
    <td>
      <label>{props.createdAt.toTimeString()}</label>
    </td>
  </tr>
);
} 

  • Bug依然存在
  • console.log(props.id, value,'===')打印结果
sort之前
1 "a" 
2 "b"
3 "c"
4 "d"
sort之后
4 "a" "==="
3 "b" "==="
2 "c" "==="
1 "d" "==="

原因分析

预备知识

  • react16 diff
  • react16 setState
  • react16 合成事件
  • 本文为笔者第一篇文章,后续会补充上述知识点
  • react 17与16没有太大变化 变化在于底层调度优先级策略的修改 不影响我们对该行为的分析

流程分析

  • 从点击Sort by Latest这一步开始进行分析
点击按钮 -> 触发react的合成事件 -> 调用回调函数sortByLatest -> 触发setState  
setState函数调用 -> 合并ToDoList组件的状态 -> 触发diff逻辑 diff逻辑的详细过程会在其他文章中进行
  1. 四个TODO组件会在diff逻辑中在同一个数组里
  2. react发现他们设置了key会拿到新旧两个数组进行分析,并把旧数组元素放到Map里以key为键名
  3. 得出diff路径数组后进行遍历,旧数组里有相同key会直接从Map里取出,没有则会新增,不存在则删除
  4. 对于此处用例,四个key值都存在,四个ToDo组件都会进行复用 但props会传入新的props
const ToDo = props => (
  <tr>
    <td>
      <label>{props.id}</label>
    </td>
    <td>
      <input />
    </td>
    <td>
      <label>{props.createdAt.toTimeString()}</label>
    </td>
  </tr>
);
  1. 父节点进行了setState,其所有子节点如果没有设置memo或者shouldComponentUpdate逻辑,都会触发render逻辑产生新的虚拟DOM
  2. 此处传递了id到ToDo组件,无论是否使用memo或者shouldComponentUpdate,都会触发ToDo组件的render逻辑;key相同复用的是component部分,传入了props并触发了render逻辑
  3. <label>{props.id}</label>diff流程来到此处会复用label节点,会生成新的文本节点
  4. <input />diff流程来到此处会复用input节点,没有文本节点,input组件无法diff出区别,因此输入值没有跟随变化

总结

  1. 没有发现其他BUG
  2. 随意设置key可能会产生上述BUG
  3. 使用唯一id设置key能够提升性能
  4. 如果没有可以不设置key,也不能说降低了性能,key的出现本来就不是面向使用者的

官方代码

const ToDo = props => (
  <tr>
    <td>
      <label>{props.id}</label>
    </td>
    <td>
      <input />
    </td>
    <td>
      <label>{props.createdAt.toTimeString()}</label>
    </td>
  </tr>
);

class ToDoList extends React.Component {
  constructor() {
    super();
    const date = new Date();
    const todoCounter = 1;
    this.state = {
      todoCounter: todoCounter,
      list: [
        {
          id: todoCounter,
          createdAt: date,
        },
      ],
    };
  }

  sortByEarliest() {
    const sortedList = this.state.list.sort((a, b) => {
      return a.createdAt - b.createdAt;
    });
    this.setState({
      list: [...sortedList],
    });
  }

  sortByLatest() {
    const sortedList = this.state.list.sort((a, b) => {
      return b.createdAt - a.createdAt;
    });
    this.setState({
      list: [...sortedList],
    });
  }

  addToEnd() {
    const date = new Date();
    const nextId = this.state.todoCounter + 1;
    const newList = [
      ...this.state.list,
      {id: nextId, createdAt: date},
    ];
    this.setState({
      list: newList,
      todoCounter: nextId,
    });
  }

  addToStart() {
    const date = new Date();
    const nextId = this.state.todoCounter + 1;
    const newList = [
      {id: nextId, createdAt: date},
      ...this.state.list,
    ];
    this.setState({
      list: newList,
      todoCounter: nextId,
    });
  }

  render() {
    return (
      <div>
        <code>key=index</code>
        <br />
        <button onClick={this.addToStart.bind(this)}>
          Add New to Start
        </button>
        <button onClick={this.addToEnd.bind(this)}>
          Add New to End
        </button>
        <button onClick={this.sortByEarliest.bind(this)}>
          Sort by Earliest
        </button>
        <button onClick={this.sortByLatest.bind(this)}>
          Sort by Latest
        </button>
        <table>
          <tr>
            <th>ID</th>
            <th />
            <th>created at</th>
          </tr>
          {this.state.list.map((todo, index) => (
            <ToDo key={index} {...todo} />
          ))}
        </table>
      </div>
    );
  }
}

ReactDOM.render(
  <ToDoList />,
  document.getElementById('root')
);