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组件

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

查看所有标签

猜你喜欢:

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

两周自制脚本语言

两周自制脚本语言

[日]千叶 滋 / 陈筱烟 / 人民邮电出版社 / 2014-6 / 59.00元

《两周自制脚本语言》是一本优秀的编译原理入门读物。全书穿插了大量轻松风趣的对话,读者可以随书中的人物一起从最简单的语言解释器开始,逐步添加新功能,最终完成一个支持函数、数组、对象等高级功能的语言编译器。本书与众不同的实现方式不仅大幅简化了语言处理器的复杂度,还有助于拓展读者的视野。 《两周自制脚本语言》适合对编译原理及语言处理器设计有兴趣的读者以及正在学习相关课程的大中专院校学生。同时,已经......一起来看看 《两周自制脚本语言》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码