React数据流与PureComponent

691 阅读7分钟

前言

此文所用代码基于React v16.11.0

在一阵子的项目实践之后一直未对React数据流做一个总结,此次在学习陈屹老师的《深入React技术栈》(书中React版本为v15)时了解到了更多,同时翻阅了更多的文章,加上实践代码,故以此文总结,若有不正之处,还请大佬们指教

React单向数据流及render()函数

This is commonly called a “top-down” or “unidirectional” data flow. Any state is always owned by some specific component, and any data or UI derived from that state can only affect components “below” them in the tree. ——React——State and Lifecycle #The Data Flows Down

自顶向下,单向数据流是React的特点之一,state被对应的组件所拥有,state只能向下传递。
当组件内部使用setState更新state数据时,组件会调用render()方法,因为我们改变了组件的内部状态。

默认情况下(shouldComponentUpdate默认返回true),其组件下的子组件也会调用render函数,进行Fier Tree(也就是virtual DOM)的构建,而至于Fiber Tree如何在真实DOM上进行渲染,就交由Fiber判断了

tips :React.render构建的React elements Tree一直被称为"virtual DOM(虚拟DOM)",在早期一直帮助人们解释React。但是virtual DOM的概念容易给人造成混淆(比如RN在IOS及安卓上),所以现在在官方文档中就不再出现了,但可以在FAQ中看到。Virtual DOM and Internals。本文中皆称为Fiber Tree

我在render函数中使用了console.log()标记,大家可以打开代码查看:CodePen打开

// App组件: 含Father1组件及Father2组件
class App extends React.Component{
  ...
  
  render(){
    console.log("App render!");
    return(
      <div>
        <Father1 />
        <Father2 />
      </div>
    );
  }
}

// Father1组件: 无state
class Father1 extends React.Component{
  constructor(props) {
    super(props);
  }
  render(){
    console.log('Father1 render!')
    return(
      <h1>Father1</h1>
    )
  }
}

// Father2组件: 挂载后定时器更新state,含Son1组件及Son2组件,Son1组件接收father2Data作为props
class Father2 extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      father2Data:'original father2Data'
    }
  }
  componentDidMount(){
    setTimeout(()=>{
      this.setState({
        father2Data:'update father2Data!'
      });
    }, 2000);
  }
  render(){
    console.log(this.state);
    console.log('Father2 render!');
    return (
      <div>
        <h1>Father2</h1>
        <Son1 data={this.state.father2Data} />
        <Son2 />
      </div>)
  }
}

// Son1组件
class Son1 extends React.Component{
  ...
  
  render(){
    console.log('Son1 render!');
    return(
      <p>Son1, 接收来自Father2组件的<strong>father2Data: {this.props.data}</strong></p>
    )
  }
}

// Son2组件
class Son2 extends React.Component{
  ...
  
  render(){
    console.log('Son2 render!')
    return(
      <p>Son2</p>
    )
  }
}

先看看初始界面及控制台:


所有组件完成了渲染

定时器2000ms到,更新Father2组件的state,我们看看界面及控制台

可以看到有Father2、Son1、Son2组件调用了render函数进行对Fiber Tree的构建

接下来Fiber再比对组件或者元素类型等等,判断如何进行真实DOM的渲染,但此部分不在本文范围内。

使用PureComponent进行一定的优化

综上,我们可以了解到在Fiber Tree的构建过程中经常会因为shouldComponentUpdate默认返回true而引起不必要的渲染,那么我们是否可以通过对shouldComponentUpdate的控制减少这些不必要的渲染。

理想情况是通过深比较对比前后的props及state,但是这一操作非常昂贵,由此,官方推出了PureComponent。为什么叫PureComponent呢,因为希望它有相同的输入(props, state)时,会有相同的输出。

其shouldComponentUpdate方法仅仅只做了浅比较,在一定程度上减少了不必要的Fiber Tree重渲染

React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.

If your React component’s render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.

class Son2 extends React.PureComponent{
  constructor(props) {
    super(props);
  }
  render(){
    console.log('Son2 render!')
    return(
      <p>Son2</p>
    )
  }
}

CodePen打开

PureComponent注意事项

由于PureComponent是浅比较,因此需要特别注意state中对象及数组类型的使用,下面举例说明

  • props对象的引用发生改变
  • setState对数组和对象进行操作
  • 使用了props.children设置子组件

接下来根据代码分点举例

props对象的引用发生改变

父组件传递给子组件props的对象地址改变时,PureComponent同样会执行render

class Father2 extends React.Component{
   //...
  render(){
    console.log(this.state);
    console.log('Father2 render!');
    const test = {}; //父组件调用render时test变量的地址发生改变
    return (
      <div>
        <h1>Father2</h1>
        <Son1 data={this.state.father2Data} />
        <Son2 data = {test}/> //test地址发生改变,故Son2组件也会调用render方法
      </div>)
  }
}

CodePen打开

若props传递的对象及数组并不需要动态改变,可以把引用放在state或是对象之中

class Father2 extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      father2Data:'original father2Data',
      test:{}
    }
    //或者
    // this.state = {
    //   father2Data:'original father2Data'
    // };
    // this.test = {};
  }
  
  // ....
  render(){
    console.log(this.state);
    console.log('Father2 render!');
    return (
      <div>
        <h1>Father2</h1>
        <Son1 data={this.state.father2Data} />
        <Son2 data = {this.state.test}/>
        // 或者
        // <Son2 data = {this.test}/>
      </div>)
  }
}

CodePen打开

如此,对象或数组的地址并没有改变,也就不会引起再次渲染Fiber Tree了

setState对数组和对象进行操作

PureComponent中,调用setState时,若前后的引用地址未发生改变,会引起组件的不刷新,导致UI与state不一致。

class Example extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      array: ['aa','bb']  // 初始内容
    };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(e) {
    console.log("——————e.target.value——————");
    console.log(e.target.value);
    const a = this.state.array;  // 1.array地址并未发生改变
    a.push(e.target.value)
    this.setState({
      array: a  // 2.所以不会再次调用render,故不会刷新UI
    });
    console.log("——————setState之后, this.state——————");
    console.log(this.state);
    
  }
  render() {
    console.log("Example render!");
    return (
      <div>
        <p>数组拼接内容:{this.state.array}</p>
        <input onChange={this.handleChange} />
      </div>
    );
  }
}

CodePen打开

可以看到,尽管state.array内容发生了改变,但其引用地址并未发生改变,由于是浅比较,故shouldComponentUpdate返回false,所以不会触发render

此情况下有两种应对方法:1.使用Immutable.js 2.利用Array.prototype.concat()不改变原数组,返回新数组

这里我们使用Immutab.js进行示范

class Example extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      list: Immutable.List(['aa', 'bb']) //使用Immutable
    };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(e) {
    console.log("——————e.target.value——————");
    console.log(e.target.value);
    const oldList = this.state.list;
    const newList = oldList.push(e.target.value); //Immutable.List在push之后返回新的List,与原List地址不相同,同时OldList的内容也并未发生改变
    this.setState({
      list: newList
    });
    console.log("——————setState之后, this.state——————");
    console.log(this.state);
    
  }
  render() {
    console.log("Example render!");
    return (
      <div>
        <p>数组拼接内容:{this.state.list}</p>
        <input onChange={this.handleChange} />
      </div>
    );
  }
}

CodePen打开

Immutable Data一旦被创建就不能再更改数据,如果进行修改、添加、删除等操作,都会返回一个新的Immutable对象。由于这个特性,Immutable Data在纯函数中也有非常多的应用。

使用了props.children设置子组件

当我们利用props.children设置子组件时,shouldComponentUpdate均会返回true

class ItemList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      test:"origin data"
    };
  }
  componentDidMount(){
    setTimeout(()=>{
      this.setState({
        test:"updated data"
      })
    }, 1000);
  }
  render() {
    console.log("ItemList render!");
    return (
      <div>
        <h1>ItemList</h1>
        <Item>
          <p>Jonithan</p>
        </Item>
      </div>
    );
  }
}
class Item extends React.PureComponent {
  // ...
  render() {
    console.log('Item render!');
    return <div>{this.props.children}</div>;
  }
}

CodePen打开

定时器1000ms时间到后,尽管Item是PureComponent,但也会重新渲染。其本质其实和props对象的引用发生改变是一样的。因为其中的JSX部分:

<Item>
  <p>Jonithan</p>
</Item>

翻译后其实是:

<Item children = {React.createElement('p',{},'Jonithan')} />

每次返回的对象引用地址不一样,所以PureComponent的shouldComponentUpdate返回true

结语

此部分的学习是通过陈屹老师的《深入React技术栈》了解到的,于是再探索一番,看了更多的文章,同时也是对React数据流、Fiber的一个大致总结。确实在学习文章时有人提到是否存在过早优化,或者收益小的问题,但是我认为使用PureComponent并不会增加多少代码,仅仅是要注意浅比较所存在的问题,同时Immutable的引入也提升了代码的健壮性和可维护行。

私以为具体还是看实际场景。

参考资料

《深入React技术栈》——陈屹
React——State and Lifecycle #The Data Flows Down
React——Reconciliation
React——React Only Updates What’s Necessary
ReactJS - Does render get called any time “setState” is called?
React.PureComponent
React Fiber Architecture
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
react组件性能优化探索实践
React组件性能优化实例解析