July 26, 2021
By: Tom

React Diff

问题复现

在上次小思的分享中,给我们演示了在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函数。

Tags: react