React.PureComponent vs React.Component vs 函数式组件, 以demo的方式

面试题中经常会遇到,React.PureComponentReact.Component 有什么区别, React官网也已经给出了答案

React.PureComponentReact.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。

可以整理出两个点

  1. React.PureComponent 内部实现了shouldComponentUpdate,如果赋予 React 组件相同的 props 和 state,则该组件不会重新渲染

  2. 新旧 props/state 都只是浅比较

概念听来终觉浅,本文就利用几个小demo来实际检验一下他们之间的区别,加深下印象,同时也加上了和函数式组件的对比。毕竟React hooks出来之后,函数式组件已经一统天下了

所有Demo的代码可以在这里获取

这里利用React Developer Tools来观察React组件render的情况

react-devtool.png

1 赋予 React 组件相同的 props 和 state

DEMO 1 新旧props相同

在这个Demo中,写了两个文件,parent.jschildren.js
前者设置了一个定时器,每一秒执行一次this.setState,值得注意的是这里每次设置的parentCount都是 0,也就是不作更改。
然后将parentCount传入到三个子组件中,这三个子组件分别是 React.PureComponetReact.Component和 函数式组件。

// parent.js

import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { ComponentChild, FunctionalChild, PureChild } from "./components/children";

class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      parentCount: 0
    }
    this.interval = null;
  }
  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState({
        number: 0 // 设置和初始值相等的值
      })
    }, 1000)
  }
  componentWillUnmount () {
    clearInterval(this.interval)
  }
  render() {
    const { parentCount } = this.state
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          {/* React.PureComponent */}
          <PureChild parentCount={parentCount} />
          {/* React.Component */} 
          <ComponentChild parentCount={parentCount} />
          {/* 函数式组件 */}
          <FunctionalChild parentCount={parentCount} />
        </header>
      </div>
    );
  }

}

export default Parent;
复制代码
// children

import React, { Component, PureComponent } from "react";

class PureChild extends PureComponent {
  render() {
    console.log("PureChild 组件 render");
    return <span>{`Pure Child Component ${new Date().valueOf()}`}-{this.props.parentCount}</span>;
  }
}

class ComponentChild extends Component {
  render() {
    console.log("ComponentChild 组件 render");
    return <span>{`Child Component - ${new Date().valueOf()}`}-{this.props.parentCount}</span>;
  }
}

function FunctionalChild(props) {
  console.log("FunctionalChild 组件render");
  return (
    <span>{`Functional Child Component - ${new Date().valueOf()}`}-{props.parentCount}</span>
  );
}
export {
  FunctionalChild,
  PureChild,
  ComponentChild
}
复制代码

可以看到除了 Pure Component 组件之外,其他都会重新进行渲染,无论传入的parentCount这个prop是否更改. 这种情况下的重渲完全没有必要,白白浪费了性能。

app-props.gif
对于函数式组件,可以使用 React.memo 对其进行性能优化
app-props-memo.gif

DEMO 2 新旧state相同

更改DEMO 1的代码,将定时设置state的操作移入到子组件内部,每次设置的值也都是一样

// parent.js
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { ComponentChild, FunctionalChild, PureChild } from "./components/children";

class Parent extends React.Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <PureChild />
          <ComponentChild />
          <FunctionalChild />
        </header>
      </div>
    );
  }
}

export default Parent;
复制代码
// children.js
import React, { Component, PureComponent, useState, useEffect } from "react";

class PureChild extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      counter: 0
    }
    this.interval = null;
  }
  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState({
        counter: 0
      })
    }, 1000)
  }
  componentWillUnmount() {
    clearInterval(this.interval)
  }
  render() {
    console.log("PureChild 组件 render");
    return <span>{`Pure Child Component ${new Date().valueOf()}`}</span>;
  }
}

class ComponentChild extends Component {
  constructor(props) {
    super(props)
    this.state = {
      counter: 0
    }
    this.interval = null;
  }
  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState({
        counter: 0
      })
    }, 1000)
  }
  componentWillUnmount() {
    clearInterval(this.interval)
  }
  render() {
    console.log("ComponentChild 组件 render");
    return <span>{`Child Component - ${new Date().valueOf()}`}</span>;
  }
}

function FunctionalChild() {
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setCounter(0)
    }, 1000);
    return () => clearInterval(interval)
  }, [])
  console.log("FunctionalChild 组件render");
  return (
    <span>{`Functional Child Component - ${new Date().valueOf()}`}</span>
  );
}
export {
  FunctionalChild,
  PureChild,
  ComponentChild
}
复制代码

最终发现只有 React.Component 写法的组件更新了。其实这里函数式组件没有更新是因为hooks的原因。调用 State Hook的更新函数并传入当前的state 时,React将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较state
app-state.gif

2 新旧props/state 都只是浅比较

DEMO 3 props浅比较

同样是两个文件,这里将父组件中的parentCount更改为了一个数组parentArray,并且每隔一秒push入一个值,然后在子组件中打印数组的长度

// parent.js

import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { ComponentChild, FunctionalChild, PureChild, FunctionalChildMemo } from "./components/children-props-shallow-compare";

class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      parentArray: [1, 2, 3, 4]
    }
    this.interval = null;
  }
  componentDidMount() {
    this.interval = setInterval(() => {
      const { parentArray } = this.state;
      this.setState({
        parentArray: [...parentArray, parentArray.length + 1]
      })
    }, 1000)
  }
  componentWillUnmount() {
    clearInterval(this.interval)
  }
  render() {
    const { parentArray } = this.state
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <PureChild parentArray={parentArray} />
          <ComponentChild parentArray={parentArray} />
          <FunctionalChild parentArray={parentArray} />
          <FunctionalChildMemo parentArray={parentArray} />
        </header>
      </div>
    );
  }

}

export default Parent;
复制代码
// children.js
import React, { Component, PureComponent } from "react";

class PureChild extends PureComponent {
  render() {
    console.log("PureChild 组件 render");
    return <span>{`Pure Child Component ${this.props.parentArray.length}`}</span>;
  }
}

class ComponentChild extends Component {
  render() {
    console.log("ComponentChild 组件 render");
    return <span>{`Child Component - ${this.props.parentArray.length}`}</span>;
  }
}

function FunctionalChild(props) {
  console.log("FunctionalChild 组件render");
  return (
    <span>{`Functional Child Component - ${props.parentArray.length}`}</span>
  );
}
function FunctionalChild2(props) {
  console.log("React.memo FunctionalChild 组件render");
  return (
    <span>{`React.memo Functional Child Component - ${props.parentArray.length}`}</span>
  );
}
const FunctionalChildMemo = React.memo(FunctionalChild2)
export {
  FunctionalChild,
  FunctionalChildMemo,
  PureChild,
  ComponentChild
}
复制代码

app-shallow-compare.gif
可见只有 Pure Component 和使用 React.memo包裹的组件都没有更新。这里如果是将数组渲染成一个列表,就会发生数据和视图对应不上的问题。当然这里只是为了验证浅比较使用demo,实际开发中应该会是类似下面这样的写法,就不会有上述所说的问题

// ...
      const { parentArray } = this.state;
      
      this.setState({
        parentArray:[...parentArray, parentArray.length + 1]
      })
// ...
复制代码

DEMO 4 state 浅比较

这里将数组更改移入子组件内部

// parent.js
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { ComponentChild, FunctionalChild, PureChild } from "./components/children-state-shallow-compare";

class Parent extends React.Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <PureChild />
          <ComponentChild />
          <FunctionalChild />
        </header>
      </div>
    );
  }

}

export default Parent;
复制代码
// children js
import React, { Component, PureComponent, useState, useEffect } from "react";

class PureChild extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      array: [1,2,3,4]
    }
  }
  componentDidMount() {
    setInterval(() => {
      const { array } = this.state;
      array.push(array.length + 1)
      this.setState({
        array
      })
    }, 1000)
  }
  render() {
    console.log("PureChild 组件 render");
    return <span>{`Pure Child Component ${this.state.array.length}`}</span>;
  }
}

class ComponentChild extends Component {
  constructor(props) {
    super(props)
    this.state = {
      array: [1,2,3,4]
    }
  }
  componentDidMount() {
    setInterval(() => {
      const { array } = this.state;
      array.push(array.length + 1)
      this.setState({
        array
      })
    }, 1000)
  }
  render() {
    console.log("ComponentChild 组件 render");
    return <span>{`Child Component - ${this.state.array.length}`}</span>;
  }
}

function FunctionalChild(props) {
  const [array, setArray] = useState([1,2,3,4]);
  useEffect(() => {
    setInterval(() => {
      array.push(array.length + 1)
      setArray(array)
    }, 1000)
  }, [])
  console.log("FunctionalChild 组件render");
  return (
    <span>{`Functional Child Component - ${array.length}`}</span>
  );
}
export {
  FunctionalChild,
  PureChild,
  ComponentChild
}
复制代码

app-state-shallow-compare.gif
可以看到这里只有 React.Component组件进行了重新渲染。这里值得注意的是函数式组件也没有更新,验证了State hook的更新函数使用的Object.is() 也是浅比较

参考

reactjs.org/

blog.logrocket.com/react-pure-…

medium.com/technofunne…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享