Deep In React (一) 高性能React组件

栏目: IOS · Android · 发布时间: 6年前

内容简介:在React内部,存在着初始化渲染和更新渲染的概念。初始化渲染会在组件第一次挂载时,渲染所有的节点当我们希望改变某个子节点时

在React内部,存在着初始化渲染和更新渲染的概念。

初始化渲染会在组件第一次挂载时,渲染所有的节点

Deep In React (一) 高性能React组件

当我们希望改变某个子节点时

Deep In React (一) 高性能React组件

我们所期望React帮我们实现的渲染行为是这样的

Deep In React (一) 高性能React组件

我们希望当我们的props向下传递时,只有对应需要更新的节点进行更新并重新渲染,其余节点不需要更新和重新渲染。

但是事实上,在默认的情况下,结果却是这样的

Deep In React (一) 高性能React组件

所有的组件树都被重新渲染,因为对于React而言,只要有props或者state发生了改变,我的组件就要重新渲染,所以除了绿色的节点,所有的黄色节点也被渲染了。

例子:

const Foo = ({foo}) => { 
  console.log('Foo is rendering!');
  return (<div>Foo  {foo}</div>);
}

const Bar = ({bar}) => {
  console.log('Bar is rendering!');
  return (<div>Bar   {bar}</div>);
}

const FooBarGroup = ({foo, bar}) => {
  console.log('FooBar is rendering!');
  return (
    <div>
      <Foo foo={foo} />
      <Bar bar={bar} />
    </div>
  )
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      foo: 0,
      bar: 0
    };
    this.handleFooClick = this.handleFooClick.bind(this);
    this.handleBarClick = this.handleBarClick.bind(this);
  }

  handleFooClick (e)  {
    e.preventDefault();
    const newFoo = this.state.foo + 1;
    this.setState({foo: newFoo});
  }

  handleBarClick(e)  {
    e.preventDefault();
    const newBar = this.state.bar + 1;
    this.setState({bar: newBar});
  }

  render() {
  const {foo, bar} = this.state;
    return (
      <div className="App">
        <button onClick={this.handleFooClick}>Foo</button>
        <button onClick={this.handleBarClick}>Bar</button>
        <FooBarGroup
          foo={foo}
          bar={bar}
        />
      </div>
    );
  }
}
复制代码

demo on stackblitz

当我们点击Foo按钮的时候,因为只有传入Foo组件和FooBarGroup组件的foo这个props更新了,我们希望上只有Foo组件和FooBarGroup组件会被再次渲染。但是打开console你会发现,console中会出现Bar组件渲染时打印的log。证明Bar组件也被重新渲染了。

shouldComponentUpdate

工作原理

避免冗余渲染是一个常见的React性能优化方向。造成冗余渲染的原因是在默认情况下,shouldComponentUpdate()这个生命周期函数总是返回true( source code )意味着所有的组件在默认的情况下都会在组件树更新时去触发render方法。

React官方对于shouldComponentUpdate的工作与React组件树更新的机制有一个还不错的解释。React组件的更新决策可以分为两步,通过shouldComponetUpdate来确认是否需要重新渲染,React vDOM diff来确定是否需要进行DOM更新操作。 shouldComponentUpdate In Action

Deep In React (一) 高性能React组件

在上图中,C2节点不会重新触发render函数因为shouldComponentUpdate在C2节点返回了false,更新请求在此处被截断,相应的C2节点下的C4、C5节点也就不会触发render函数。

对于C3节点,由于shouldComponentUpdate返回了true,所以需要进行进一步的Vitural DOM的diff(以下简称vDOM diff,该diff算法由react提供,不在这细讲)。

而父组件的vDOM diff其实是对于子组件遍历进行以上过程。同上,C3的子组件C6由于shouldComponentUpdate返回了true,所以需要进行下一步vDOM diff,diff后发现需要更新,所以会重新触发渲染。而C7节点由于shouldComponentUpdate返回了false,所以便不再进行进一步的vDOM diff。而C8节点在vDOM diff后发现vDOM相等,最后其实也不会更新。

如何使用

上面提到了,默认情况下,shouldComponentUpdate这个方法总是会返回True。如果我们需要去显式的去决定我们的组件是否需要更新,那就意味着我们可以去显式调用这个生命周期函数。

class Foo extends React.Component {

  shouldComponentUpdate(nextProps) {
    return this.props.foo !== nextProps.foo;
  }

  render(){
    console.log('Foo is rendering!');
    return (
      <div>{ this.props.foo }</div>
    )
  }
}

const Bar = ({bar}) => {
  console.log('Bar is rendering!');
  return (<div>{bar}</div>);
}

const FooBarGroup = ({foo, bar}) => {
  console.log('FooBarGroup is rendering!');
  return (
    <div>
      <Foo foo={foo} />
      <Bar bar={bar} />
    </div>
  )
}

复制代码

demo on stackblitz

这时再去查看console,我们会发现只有当foo更新的时候,Foo组件才会真正的去调用render方法。

PureComponent

使用PureComponent

但是如果所有的组件我们都要去自己实现shouldComponentUpdate方法, 有的时候未免会显得很麻烦。不过好在React包里面提供了PureComponent这个实现。

React.PureComponent

PureComponent内部实现了一个基于props和state浅比较的shouldComponentUpdate方法,基于这种浅比较,当props和state没有发生改变时,组件的render方法便不会被调用到。

class Foo extends React.PureComponent {

  /*
  我们不需要手动实现shouldCompoUpdate方法了
    shouldComponentUpdate(nextProps) {
      return this.props.foo !== nextProps.foo;
    }
    */

  render(){
    console.log('Foo is rendering!');
    return (
      <div>{ this.props.foo }</div>
    )
  }
}
复制代码

demo on stackblitz

PureComponent中的陷阱

我的props改变了,为什么我的组件没有更新?

由于PureComponent的shouldComponentUpdate是基于浅比较 shallowEqual.js 的,对于复杂对象,如果深层数据发生了改变,shouldComponentUpdate方法还是会返回false。

比如

class Foo extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      foo: ['test']
    }
  }

  handleClick = (e) => {
    e.preventDefault();
    const foo = this.state.foo;
    foo.push('test');
    //push是一个mutable的操作,foo的引用并没有改变
    this.setState({foo});
  }

  render(){
    console.log('Foo is rendering!');
    return (
      <div>
      <button onClick={this.handleClick}>Foo balabala</button>
      { this.state.foo.length }
      </div>
    )
  }
}

复制代码

上面这段代码当我的button进行点击时,即使我的this.state.foo发生了改变,但是我的组件却不会有任何更新。因为我的this.state.foo( [‘test’] )与nextState.foo( [‘test’, ‘test’] )在shouldComponentUpdate进行的浅比较(实际使用 Object.is )时其实是两个相同的两个数组。

demo on stackblitz

解决办法

// concat会返回一个新数组,引用发生改变, 浅比较(Object.is)会认为这是两个不同的数组
handleClick = (e) => {
    e.preventDefault();
    const foo = this.state.foo;
    const bar = foo.concat('test');
    this.setState({foo: bar});
}

复制代码

我的props没有变, 为什么我的组件更新了?

然而有的时候, 即使我是PureComponent, 在组件的props看上去没有发生改变的时候, 组件还是被重新渲染了, interesting。

const Foo = ({foo}) => {
  console.log('Foo is rendering!');
  return (<div>{foo}</div>);
}

const Bar = ({bar}) => {
  console.log('Bar is rendering!');
  return (<div>{bar}</div>);
}

const FooBarGroup = ({foo, bar}) => {
  console.log('FooBar is rendering!');
  return (
    <div>
      <Foo foo={foo} />
      <Bar bar={bar} />
    </div>
  )
}

class PureRenderer extends React.PureComponent {
  render(){
    console.log('PureRenderer is rendering!!');
    const { text } = this.props;
    return (
      <div>
        {text}
      </div>
    )
  }
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      foo: 0,
      bar: 0
    }
  }

  handleFooClick = (e) => {
    e.preventDefault();
    const newFoo = this.state.foo + 1;
    this.setState({foo: newFoo});
  }

  handleBarClick = (e) => {
    e.preventDefault();
    const newBar = this.state.bar + 1;
    this.setState({bar: newBar});
  }

  render() {
  const {foo, bar} = this.state;
    return (
      <div className="App">
        <button onClick={this.handleFooClick}>Foo</button>
        <button onClick={this.handleBarClick}>Bar</button>
        <FooBarGroup
          foo={foo}
          bar={bar}
        />
        <PureRenderer text="blablabla" onClick={() => console.log('blablabla')} />
      </div>
    );
  }
}
复制代码

demo on stackblitz

我的PureRenderer明明已经是一个PureComponent了。但是当我点击foo或者bar button时,我还是能发现我的render方法被调到了。我似乎并没有进行任何props的修改?

导致这种情况是因为props。onClick传入的是 ()=>{console.log('balabalabla' 。这就意味着我每次传入的都是一个新的函数实体。对于两个不同的实例进行浅比较, 我总会得到这两个对象不相等的结果。(引用比较)

解决办法其实也很简单, 就是持久化下来我们的对象

const onClickHandler = () => console.log('blablabla')

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      foo: 0,
      bar: 0
    };
  }

  render() {
  const {foo, bar} = this.state;
    return (
      <div className="App">
        <button onClick={this.handleFooClick}>Foo</button>
        <button onClick={this.handleBarClick}>Bar</button>
        <FooBarGroup
          foo={foo}
          bar={bar}
        />
        <DateRerender text="blablabla" onClick={onClickHandler}/>
      </div>
    );
  }
复制代码

这样就能避免不必要的重复渲染了。

React 性能检测

1. 与Chrome集成的TimeLine工具

React在开发者模式下通过调用User Timing API可以很好的Chrome的火炬图进行结合来对组件的性能进行检测。

Profiling Components with the Chrome Performance Tab

Deep In React (一) 高性能React组件

可以看到所有的React相关的函数调用和操作都出现在了Timeline之中。

2. Why did you update?

还有一个有意思的库也可以帮助我们做到这些。

why-did-you-update

这个库会提供一个高阶组件, 将你的组件使用这个高阶组件包裹一下, 打开console你就能发现这个库对于你的组件的profiling。

Deep In React (一) 高性能React组件

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

阿里巴巴正传:我们与马云的“一步之遥”

阿里巴巴正传:我们与马云的“一步之遥”

方兴东、刘伟 / 江苏凤凰文艺出版社 / 2015-1 / 45.00

十几年来,方兴东与马云每年一次,老友聚首,开怀畅谈,阿里上市前,作者再次与马云深度对话,阿里上市前的布局,深入探讨了一系列人们关心的话题。 本书忠实记录了阿里壮大、马云封圣的历史。作者通过细致梳理和盘点,对阿里巴巴的15年成长史进行了忠实回顾。从海博翻译社到淘宝网,从淘宝商城到天猫,从支付宝到阿里云计算,从拉来软银的第一笔投资到纽交所上市,作者对其中涉及到的人物、细节都有生动展现;对于马云、......一起来看看 《阿里巴巴正传:我们与马云的“一步之遥”》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具