本文初稿写于2020年12月5日
react版本为 React v17.0.1
适合读者: 使用react 15 16 17版本的读者
原创声明:本文原创,转载请标注掘金地址即可
BUG复现
- 官方文档
- 文档中的demo1 使用数组index作为key
- 文档中的demo2 使用id作为key
- 官方给出的codepen 后续是否会失效或变化笔者无法保证 本文后方会附上js部分代码
请打开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逻辑的详细过程会在其他文章中进行
- 四个TODO组件会在diff逻辑中在同一个数组里
- react发现他们设置了key会拿到新旧两个数组进行分析,并把旧数组元素放到Map里以key为键名
- 得出diff路径数组后进行遍历,旧数组里有相同key会直接从Map里取出,没有则会新增,不存在则删除
- 对于此处用例,四个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>
);
- 父节点进行了setState,其所有子节点如果没有设置memo或者shouldComponentUpdate逻辑,都会触发render逻辑产生新的虚拟DOM
- 此处传递了id到ToDo组件,无论是否使用memo或者shouldComponentUpdate,都会触发ToDo组件的render逻辑;key相同复用的是component部分,传入了props并触发了render逻辑
<label>{props.id}</label>diff流程来到此处会复用label节点,会生成新的文本节点<input />diff流程来到此处会复用input节点,没有文本节点,input组件无法diff出区别,因此输入值没有跟随变化
总结
- 没有发现其他BUG
- 随意设置key可能会产生上述BUG
- 使用唯一id设置key能够提升性能
- 如果没有可以不设置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')
);