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

原文链接:hateonion.me/2018/10/17/…

React的渲染机制

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

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

initial render

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

want to change

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

our thoughts

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

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

example

所有的组件树都被重新渲染,因为对于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

shouldcomponentupdate

在上图中,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

profiler

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

2. Why did you update?

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

why-did-you-update

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

why did you update

3. React 16.5引入的profiling tool

React在16.5以后引入了更加强大的profilling tool,后续有时间再拿一篇文章出来讲一讲。 Introducing the React Profiler – React Blog

最后一点:性能可能不是那么的重要

其实大多数时候, React已经足够的快了。 所以当性能不是明显的痛点的时候, 我们没有必要花费大量的时间去强行让我们的每个组件都做到性能的最优化。 将更多的时间花在更重要的事情上(比如重构, 更好的代码组织), 写出更可维护更可读更优美的代码, 才是我们每一个开发人员需要更加关注的事情。

参考资料

高性能React组件 - By 淘宝前端团队

High Performance React - By Ben Edelstein

Optimiz Performance - By React Docs

猜你喜欢

转载自juejin.im/post/5bc83d885188255c3b7dbb93