首页投稿(暂停使用,暂停投稿)首页推荐WEB前端程序开发

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

2018-01-30  本文已影响0人  jdkwky
网络图片

昨天在修改两颗树的渲染效果代码,惊奇发现第一颗树能成功渲染,第二颗树无论如何也渲染不出来,渲染的数据总是第一颗树的数据,焦头烂额之后,查看了一下树组件的代码,发现自己已经入坑,由于树组件是公司的代码不方便贴出来,我自己写了一个简易的模拟一下出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()函数中,如下图:

debug

不过此时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中的另外一个概念了:生命周期


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函数的

解决方案

  1. Greeter中的解决方案

    • 在Greeter组件中添加componentWillReceiveProps(nextProps)生命周期函数,当props有变化时,及时将state中的数据替换掉
    • 不将数据放到state中,在render中直接用props中的data去渲染
  2. 调用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>:''} 
    <!--原理同上,还是通过包裹,变成两个不同的组件-->
    

源码地址:https://github.com/jdkwky/reactDemo

上述全是个人理解,如有问题欢迎讨论交流

上一篇 下一篇

猜你喜欢

热点阅读