React Diff
React
问题复现
在上次小思的分享中,给我们演示了在React中遍历元素加上key的重要性。在这次演示中大家发现了一个很奇怪的问题,下面先把这个问题复现一下:
var Item = React.createClass({
render: function(){
return (
<div style={{marginRight: 20, display: "inline-block"}}>
<span>
{this.props.value}
</span>
</div>
)
},
componentWillUnmount: function(){
alert("卸载" + this.props.value);
}
});
var List = React.createClass({
getInitialState: function(){
return {
data: ["a", "b", "c"]
}
},
// 删除一个元素
removeFirstItem: function(){
const data = this.state.data;
const newData = data.filter((item, index) => {
return index > 0;
});
this.setState({
data: newData
});
},
render: function(){
return (
<div style={{textAlign: "center"}}>
<button onClick={this.removeFirstItem}>
删除元素
</button>
<div>
{this.state.data.map(item => <Item value={item}/>)}
</div>
</div>
)
}
});
ReactDOM.render(<List/>, document.getElementById('demo'))
上面我们定义了两个组件,List和Item。在List组件中我们遍历了Item, 注意我们并没给Item设置key。点击button我们会删除数组的第一元素。我们希望删除元素的同时,会触发第一个元素componentWillUnmount的生命周期函数。比如删除数组中的"a",我们希望alert中的消息是“卸载a”。操作下面的示例,你会发现结果和我们想的不一样!
点击按钮后,第一个元素会从列表中删除,但是alert中的消息都是“卸载d”。
导致这个现象的原因其实是由于React Diff算法,之前对应React Diff算法确实也比较片面。导致理解上出现了偏差。借此上面的实例,我们加深一下对React Diff算法理解。
React Diff
React Diff算法详细介绍网上也比较多,我这里就不过多介绍了。在这里主要介绍一下React Diff的过程。然后通过它来分析出我们日常中遇到一些问题的原因,来进一步加深理解。
React对树分层比较
React Diff算法之所以高效,因为它只会逐层比较同层的节点。

对比不同类型的元素
当节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。
如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。

我们看一个例子:
var Apple = React.createClass({
componentDidMount: function(){
console.info("挂载Apple组件");
},
render: function(){
return <div>Apple</div>
},
componentWillUnmount: function(){
console.info("卸载Apple组件");
}
});
var AppleTree = React.createClass({
componentDidMount: function(){
console.info("挂载AppleTree组件");
},
render: function(){
return (
<div>
<h2>苹果树</h2>
<Apple/>
</div>
);
},
componentWillUnmount: function(){
console.info("卸载AppleTree组件");
}
});
var Peach = React.createClass({
componentDidMount: function(){
console.info("挂载Peach组件");
},
render: function(){
return <div>Peach</div>
},
componentWillUnmount: function(){
console.info("卸载Peach组件");
}
});
var PeachTree = React.createClass({
componentDidMount: function(){
console.info("挂载PeachTree组件");
},
render: function(){
return (
<div>
<h2>桃子树</h2>
<Peach/>
</div>
)
},
componentWillUnmount: function(){
console.info("卸载PeachTree组件");
}
});
var FriutTreeDemo = React.createClass({
getInitialState: function(){
return {
changeFlag: true
}
},
changeTree: function() {
this.setState({changeFlag: !this.state.changeFlag});
},
render: function(){
const tree1 = (
<div><AppleTree/></div>
);
const tree2 = (
<div><PeachTree/></div>
);
return (
<div style={{textAlign: "center", marginBottom:5 , border: "1px solid #000000"}}>
<button onClick={this.changeTree}>改变结构</button>
{this.state.changeFlag ? tree1 : tree2}
</div>
)
}
});
ReactDOM.render(<FriutTreeDemo/>, document.getElementById('friut-tree-demo'));
点击按钮在控制的输出为

对比同一类型的元素
当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
var ColorDemo = React.createClass({
getInitialState: function(){
return {
changeFlag: true
}
},
changeTree: function() {
this.setState({changeFlag: !this.state.changeFlag});
},
render: function(){
const tree1 = (
<div style={{color: "red"}}>你好</div>
);
const tree2 = (
<div style={{color: "green"}}>你好</div>
);
return (
<div style={{textAlign: "center"}}>
<button onClick={this.changeTree}>改变结构</button>
{this.state.changeFlag ? tree1 : tree2}
</div>
)
}
});
ReactDOM.render(<ColorDemo/>, document.getElementById('color-demo'));
对比同一类型的组件元素
当对比两个相同组件元素,会继续比较组件包含的树节点。可以通过shouldComponentUpdate()来优化。
var Student = React.createClass({
componentDidMount: function(){
console.info("挂载Student组件");
},
shouldComponentUpdate: function(){
return true;
},
render: function(){
return <div>{this.props.name}</div>
}
});
var StudentDemo = React.createClass({
getInitialState: function(){
return {
changeFlag: true
}
},
changeTree: function() {
this.setState({changeFlag: !this.state.changeFlag});
},
render: function(){
const tree1 = (
<Student name="张三"/>
);
const tree2 = (
<Student name="李四"/>
);
return (
<div style={{textAlign: "center"}}>
<button onClick={this.changeTree}>改变结构</button>
{this.state.changeFlag ? tree1 : tree2}
</div>
)
}
});
ReactDOM.render(<StudentDemo/>, document.getElementById('student-demo'));
处理子元素列表
上面我们介绍了,React Diff算法会逐一比较同层的元素节点。在子元素列表末尾新增元素时,更新开销比较小,比如:
var FsDemo = React.createClass({
getInitialState: function(){
return {
changeFlag: true
}
},
changeTree: function() {
this.setState({changeFlag: !this.state.changeFlag});
},
render: function(){
const tree1 = (
<ul>
<li>苹果</li>
<li>香蕉</li>
</ul>
);
const tree2 = (
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橘子</li>
</ul>
);
return (
<div style={{textAlign: "center"}}>
<button onClick={this.changeTree}>改变结构</button>
{this.state.changeFlag ? tree1 : tree2}
</div>
)
}
});
ReactDOM.render(<FsDemo/>, document.getElementById('fs-demo'));
如果是简单的将新增元素插入到表头, 那么更新开销会比较大。
var FsDemo2 = React.createClass({
getInitialState: function(){
return {
changeFlag: true
}
},
changeTree: function() {
this.setState({changeFlag: !this.state.changeFlag});
},
render: function(){
const tree1 = (
<ul>
<li>苹果</li>
<li>香蕉</li>
</ul>
);
const tree2 = (
<ul>
<li>橘子</li>
<li>苹果</li>
<li>香蕉</li>
</ul>
);
return (
<div style={{textAlign: "center"}}>
<button onClick={this.changeTree}>改变结构</button>
{this.state.changeFlag ? tree1 : tree2}
</div>
)
}
});
ReactDOM.render(<FsDemo2/>, document.getElementById('fs-demo2'));
keys
为了解决上述问题,React 引入了 key 属性。
var FsDemo3 = React.createClass({
getInitialState: function(){
return {
changeFlag: true
}
},
changeTree: function() {
this.setState({changeFlag: !this.state.changeFlag});
},
render: function(){
const tree1 = (
<ul>
<li key="a">苹果</li>
<li key="b">香蕉</li>
</ul>
);
const tree2 = (
<ul>
<li key="o">橘子</li>
<li key="a">苹果</li>
<li key="b">香蕉</li>
</ul>
);
return (
<div style={{textAlign: "center"}}>
<button onClick={this.changeTree}>改变结构</button>
{this.state.changeFlag ? tree1 : tree2}
</div>
)
}
});
ReactDOM.render(<FsDemo3/>, document.getElementById('fs-demo3'));
防止key重复
给元素添加key时,要注意避免重复的值
var FsDemo4 = React.createClass({
getInitialState: function(){
return {
changeFlag: true
}
},
changeTree: function() {
this.setState({changeFlag: !this.state.changeFlag});
},
render: function(){
const tree1 = (
<ul>
<li key="a">苹果</li>
<li key="b">香蕉</li>
</ul>
);
const tree2 = (
<ul>
<li key="a">橘子</li>
<li key="a">苹果</li>
<li key="a">香蕉</li>
</ul>
);
return (
<div style={{textAlign: "center"}}>
<button onClick={this.changeTree}>改变结构</button>
{this.state.changeFlag ? tree1 : tree2}
</div>
)
}
});
ReactDOM.render(<FsDemo4/>, document.getElementById('fs-demo4'));
分析开头的问题
当删除一个元素时,两个树结构如下(虚线表示我们自定义的组件包含的树):

由于子元素没有加key,所以react diff会依次比较。我们从第一个元素进行比较,因为都是相同的组件,所以react diff会继续比较子元素,发现<span>a<span>变成了<span>b<span>。所以就会销毁<span>a<span>,添加<span>b<span>。然后比较第二个元素,同理,销毁<span>b</span>,添加<span>c</span>。当比较最后一个元素时,组件元素的节点被删除了,就直接将整个组件元素删除,然后触发了componentWillUnmount函数。