记bug:明明两次返回的组件中的props不一致,为什么dom不

昨天在修改两颗树的渲染效果代码,惊奇发现第一颗树能成功渲染,第二颗树无论如何也渲染不出来,渲染的数据总是第一颗树的数据,焦头烂额之后,查看了一下树组件的代码,发现自己已经入坑,由于树组件是公司的代码不方便贴出来,我自己写了一个简易的模拟一下出bug的情况
包
1. react
2. react-dom
3. redux
组件
1. Container组件(容器组件)
2. Greeter组件(用于模拟树组件)
组件代码
1. Container 组件
import React from 'react';
import Greeter from './Greeter.jsx';
class Container extends React.Component {
<!--根据value值的不同去返回props不同但是类型相同的两个组件-->
getGreetRender (value){
if (value > 0) {
return <Greeter data = { 'hello wky' } />
} else {
return <Greeter data = { 'wky hello' } />
}
}
render(){
return <div >
<button onClick = { this.props.onClick } > 点击 </button>
<p>{this.props.value}</p>
{ this.getGreetRender(this.props.value) }
</div>
}
}
export default Container;
2. Greeter组件
import React, { Component } from 'react';
import lstyles from '../css/greetl.less';
class Greeter extends Component {
constructor(props) {
super(props);
this.state = {
data: props.data
}
console.log(this.state)
}
componentWillMount() {
const {data} = this.props;
this.setState({ data: data });
}
// componentWillReceiveProps(nextProps){
// const {data} = nextProps;
// this.setState({data:data});
// }
render() {
const { data } = this.state;
// const {data} = this.props;
console.log(data)
// const name = propsData.data;
return ( < div className = { `${lstyles.greet}` } > { data } < /div >)
}
}
export default Greeter;
Greeter组件中在 componentWillMount()函数中将props中的data存到了state中。
这样当我们点击按钮数字增加时,Greeter组件并不会更新,但是debug发现,其实代码走到了Greeter组件的render()函数中,如下图:

不过此时data:“wky hello”
此时,第一个疑问点:我明明返回了一个新的Greeter组件,为什么不是从constructor开始执行呢?
react官方文档给出,jsx并不是一种新的语法,而是react.createElement()的语法糖;并且我import进来的Greeter就并不是Greeter的对象,而是Greeter类型函数,这个Greeter类型通过react.createElement()生成dom节点。
此时,第二个疑问点:难道react.createElement()在我props值变了之后,不会重新去创建这个元素么?
如果重新创建这个元素的话,很显然当value值大于0返回Greeter组件的时候,会走入到constructor函数中,但是事实上并没有走到,那么这就说明并没有重新创建,那么这就引出了react 虚拟dom节点diff算法
什么是DOM Diff算法(节选自 王沛 深入浅出React(四):虚拟DOM Diff算法解析)
Web界面由DOM树来构成,当其中某一部分发生变化时,其实就是对应的某个DOM节点发生了变化。在React中,构建UI界面的思路是由当前状态决定界面。前后两个状态就对应两套界面,然后由React来比较两个界面的区别,这就需要对DOM树进行Diff算法分析。
即给定任意两棵树,找到最少的转换步骤。但是标准的的Diff算法复杂度需要O(n^3),这显然无法满足性能要求。要达到每次界面都可以整体刷新界面的目的,势必需要对算法进行优化。这看上去非常有难度,然而Facebook工程师却做到了,他们结合Web界面的特点做出了两个简单的假设,使得Diff算法复杂度直接降低到O(n)
不同节点类型的比较为了在树之间进行比较,我们首先要能够比较两个节点,在React中即比较两个虚拟DOM节点,当两个节点不同时,应该如何处理。这分为两种情况:(1)节点类型不同 ,(2)节点类型相同,但是属性不同。本节先看第一种情况。
当在树中的同一位置前后输出了不同类型的节点,React直接删除前面的节点,然后创建并插入新的节点。假设我们在树的同一位置前后两次输出不同类型的节点。renderA: <div /> renderB: <span /> => [removeNode <div />], [insertNode <span />]
相同类型节点的比较
第二种节点的比较是相同类型的节点,算法就相对简单而容易理解。React会对属性进行重设从而实现节点的转换。例如:
renderA: <div id="before" /> renderB: <div id="after" /> => [replaceAttribute id "after"]
虚拟DOM的style属性稍有不同,其值并不是一个简单字符串而必须为一个对象,因此转换过程如下:
renderA: <div style={{color: 'red'}} /> renderB: <div style={{fontWeight: 'bold'}} /> => [removeStyle color], [addStyle font-weight > 'bold']
详细信息请看原文 http://www.infoq.com/cn/articles/react-dom-diff/
通过dom diff算法,会人为返回的两个dom元素其实是一个,只不过props属性值变化了。
此时,第三个疑问产生了,为什么组件的props变化了,但是却没有重新刷新我的组件呢?
这时就要了解react中的另外一个概念了:生命周期

组件在初始化时会触发5个钩子函数:
1、getDefaultProps()
设置默认的props,也可以用dufaultProps设置组件的默认属性。
2、getInitialState()在使用es6的class语法时是没有这个钩子函数的,可以直接在constructor中定义this.state。此时可以访问this.props。
3、componentWillMount()组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改state。
4、 render()react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行。此时就不能更改state了。
5、componentDidMount()组件渲染之后调用,可以通过this.getDOMNode()获取和操作dom节点,只调用一次。
在更新时也会触发5个钩子函数:6、componentWillReceivePorps(nextProps)
组件初始化时不调用,组件接受新的props时调用。
7、shouldComponentUpdate(nextProps, nextState)react性能优化非常重要的一环。组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候。不过调用this.forceUpdate会跳过此步骤。
8、componentWillUpdate(nextProps, nextState)组件初始化时不调用,只有在组件将要更新时才调用,此时可以修改state
9、render()不多说
10、componentDidUpdate()组件初始化时不调用,组件更新完成后调用,此时可以获取dom节点。
还有一个卸载钩子函数11、componentWillUnmount()
组件将要卸载时调用,一些事件监听和定时器需要在此时清除。
在Greeter组件中,我们在componentWillMount()函数中将props.data值放到了state中,当props更新时并没有及时的更新state中的数据,所有导致了看上去组件并没有刷新的样子,其实不是组件没有刷新,而是组件中的state数值没有变化,组件还是会走render函数的
解决方案
-
Greeter中的解决方案
- 在Greeter组件中添加componentWillReceiveProps(nextProps)生命周期函数,当props有变化时,及时将state中的数据替换掉
- 不将数据放到state中,在render中直接用props中的data去渲染
-
调用Greeter时的解决方案
如果组件是别人已经写好的,不方便修改的情况下,我们可以通过更改调用方法,还是以Greeter为例:- 在调用Greeter组件时,在返回属性不同的Greeter组件时,最外面在包一层不同的dom元素
getGreetRender (value){ if (value > 0) { return <p><Greeter data = { 'hello wky' } /></p> } else { return <div><Greeter data = { 'wky hello' } /></div> } } 这样在虚拟dom diff的时候 会认为这是两个不同的dom节点,所以就会去重新生成Greeter节点
- 用三目运算符
getGreetRender (value){ if (value > 0) { return <Greeter data = { 'hello wky' } /> } else { return <Greeter data = { 'wky hello' } /> } } {this.props.value>0?<div>{ this.getGreetRender(this.props.value) }</div>:""} {this.props.value<=0?<div>{this.getGreetRender(this.props.value)}</div>:''} <!--原理同上,还是通过包裹,变成两个不同的组件-->
- key(key值不同会认为是不同的组件)
getGreetRender (value){ if (value > 0) { return <Greeter key={+new Date} data = { 'hello wky' } /> } else { return <Greeter key={+new Date} data = { 'wky hello' } /> } } // 保证前后两次返回的Greeter组件中的key值不同即可
源码地址:https://github.com/jdkwky/reactDemo
上述全是个人理解,如有问题欢迎讨论交流