依据父组件state的改变,子组件重新render--memo、useMemo、useCallback

1,748 阅读5分钟

当父组件为函数组件时

父组件

App.js
import React,{ useState , useEffect, useCallback} from 'react'
import FunTwo  from './pages/FunTwo'
function App() {
    let [twoA, setTwoA] = useState([
            {id: 1, username: '朱允炆', flag: false},
            {id: 2, username: '朱棣', flag: true},
            {id: 3, username: '朱高炽', flag: false}
    ])
    // 父组件中的事件处理函数
    const handleChange = useCallback((id) => {
      const arr = twoA.map(item => {
        if (item.id === id) item.flag = !item.flag
        	return item
    	})
      setTwoA(twoA=arr)
    },[])

    return (
    	<div className="App" >
        <ul>
          {
            twoA.map(item => <FunTwo key={item.id} twoA={item} handleChange={handleChange}flag={item.flag}></FunTwo>)
          }
        </ul>
    </div>
  );
}

子组件

import React, { useMemo, memo } from 'react'

const FunTwo = memo((props) => {
    console.log('function two render');

    return (
        <div>
            <li style={{color: props.twoA.flag?'pink': ''}}>
                <input type="checkbox" checked={props.twoA.flag}  onChange={()props.handleChange(props.twoA.id)}/>
                {props.twoA.id}--{props.twoA.username}
            </li>
        </div>
    )
},(pre,next) => {
    console.log(pre.handleChange == next.handleChange); // true
    console.log(pre.flag == next.flag);
})
export default FunTwo

问题一:如果在函数组件中定义事件处理函数(handleChange),传递给子组件调用,子组件通过调用父组件传递的方法修改父组件中定义的state(twoA)中的某一个item时会重新调用父组件的render方法,将三个item重新传递给子组件渲染。虽然只改变了一个item,但是会使子组件重新渲染三次。 解决:可以在子组件中使用 memo将子组件包裹,如果不设置依赖项。子组件会默认在回调函数中判断父组件传递给子组件的所有props,前后两次是否相等。如果相等,子组件中的就不会重新render,反之就会重新执行render。(如果函数子组件回调函数中的 pre、next不相等,可以自己设置props中相等的属性作为依赖项)。

问题二:在父组件中给子组件传递方法(handleChange)时,父组件中每次重新更改状态,都会造成父组件的重新渲染,进而会造成每次都会重新生成handleChange事件处理函数,父组件每次都会将最新生成的handleChange重新传递给子组件。【同样会造成子组件中的memo的回调函数中pre、next的前后两种状态不一致,如果在'问题一'中不设置依赖项,就默认会比较pre和next是否相等(这里不相等),从而也会造成子组件中没有发生改变的数据的重新渲染】。 解决:在父组件中使用useCallback将handleChange函数包裹,就会将handleChange 方法缓存下来,进而就不会重新生成handleChange,然后传递给自组件,这时候可以在子组件的memo回调函数中使用console.log(pre.handleChange == next.handleChange);会发现前后两次的方法是相等的(返回true)。

子组件为类组件

import React, { Component, PureComponent } from 'react';
export default class Two extends PureComponent {
	    render() {
        console.log("two---render");
        return (
            <div>
                <li style={{color: this.props.twoA.flag?'pink': ''}}>
                    <input type="checkbox" checked={this.props.twoA.flag}  onChange={() =>this.props.handleChange(this.props.twoA.id)}/>
                    {this.props.twoA.id}--{this.props.twoA.username}
                </li>
            </div>
        )
    }
}

React15.3中新加了一个 PureComponent 类,PureComponent 也就是纯组件,取代其前身 PureRenderMixin ,

PureComponent 是优化 React 应用程序最重要的方法之一,易于实施,只要把继承类从 Component 换成 PureComponent 即可,可以减少不必要的 render操作的次数,从而提高性能,而且可以少写 shouldComponentUpdate 函数,节省了点代码。

原理

当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较:

基本类型: 根据外部传入的数据,新的数据与旧的数据是否一致,如果一致的话,render不会执行。
引用类型: 根据外部传入的数据,新的数据与旧的数据地址是否一致,如果地址一致的话,render不会执行。

问题三: 父组件使用函数组件,子组件使用类组件。如果父组件不使用useCallback,会使父组件渲染时每次都会重新生成handleChange方法,进而在子组件中通过PureComponent的浅层比较发现前后两次的props不相同,导致即使props没有发生改变也会重新渲染子组件。

解决:

1、父组件中使用useCallback将事件处理函数进行包裹,使该方法进行缓存从而不会每次都会重新生成。 2、可以使用React中的生命周期函数 shouldComponentUpdate(将子组件的继承改为Component),自定义更新条件。

  shouldComponentUpdate(preProps, preState) {
        if(preProps.flag !== this.props.flag) return true
        return false
    } 

父组件为类组件

export default class ClassApp extends Component {
    state ={ 
        twoA: [
            {id: 1, username: '朱允炆', flag: false},
            {id: 2, username: '朱棣', flag: true},
            {id: 3, username: '朱高炽', flag: false}
        ]
    }
    
      
    handleChange = (id) => {
        console.log(id)
        const arr = this.state.twoA.map(item => {
            if (item.id === id) item.flag = !item.flag
            return item
        })
        this.setState({
            twoA: arr
        })
    }
    render() {
          <ul>
          {
              // twoA.map(item => <Two key={item.id} twoA={item} handleChange={this.handleChange} flag={item.flag}></Two>)
              twoA.map(item => <FunTwo key={item.id} twoA={item} handleChange={this.handleChange} flag={item.flag}></FunTwo>)

          }
          </ul>
          </div>
        )
    }
}

当父组件为类组件时,父组件中不存在事件处理函数在父组件状态发生改变,父组件重新render,导致的handleChange事件处理函数重新生成问题。如果子组件为类组件可以直接在子组件中直接使用使用PureComponent进行浅层比较,确定子组件是否重新渲染。如果为函数组件可以直接使用memo将子组件进行包裹。

补充useMemo

父组件

const Parent = () => {
  const [clickCount, setClickCount] = useState(0)

  const timeOption = useMemo(() => {
    return {clickCount}
  }, [clickCount])


  return (
    <div>
      <button onClick={() => {setClickCount(clickCount + 1)}}>GET CURRENT TIME</button>
      <Child count={timeOption}/>
    </div>
  )
}

子组件

const Child = memo((props) => {
  console.log(props)
  console.log(123)
  const date = new Date()
  return (
    <div>
      <p>当前时间:{date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</p>
    </div>
  )
}

此时:在父组件中的clickCount没有发生改变,使用useMemo()可以将这个结果缓存,在父组件更新状态时不会每次都重新生成timeOption,因此在memo()的第二个参数中通过比较前后两次的props是相同的,从而不会触发子组件的更新。

结论

子组件是否发生改变

  • memo 实现组件状态改变前后props是否发生改变,决定组件是否重新加载。它站在整个组件上。返回值是一个高阶组件。
  • useMemo 实现属性的缓存,针对某一个属性的改变,确定子组件是否重新加载,而且它的返回值是一个数值。
  • useCallback 实现方法的缓存,针对某一个方法的改变,确定子组件是否重新加载,它的返回值是一个方法。