更新阶段

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate()

更新阶段会在三种情况下触发:

  • 更改props:一个组件并不能主动更改它拥有的props属性,它的props属性是由它的父组件传递给它的。强制对props进行重新赋值会导致程序报错。

  • 更改statestate的更改是通过setState接口实现的。同时设计state是需要技巧的,哪些状态可以放在里面,哪些不可以;什么样的组件可以有state,哪些不可以有;这些都需要遵循一定原则的。这个话题有机会可以单独拎出来说

  • 调用forceUpdate方法:这个我们在上一阶段已经提到了,强制组件进行更新。

setState是异步的

组件的更新原因很大一部分是因为调用setState接口更新state所致,我们常常以同步的方式调用setState,但实际上setState方法是异步的。比如下面的这段代码:

onClick() {
  this.setState({
    count: 1,
  });
  console.log(this.state.count)
}

在一个组件的点击事件处理函数中,我们更新了state中的count,然后立即尝试去读取最新的count。事实是你读取的结果不是1,二应该是之前的值。

更致命的错误是类似这样在同一个块级中连续调用setState的代码

this.setState({ ...this.state, foo: 42 });
this.setState({ ...this.state, isBar: true });

在这种情况下,第一次设置的foo值会被第二次的设置覆盖而还原

componentWillReceiveProps(nextProps)

当传递给组件的props发生改变时,组件的componentWillReceiveProps即会被触发调用,方法传递的参数的是发更更改的之后的props值(通常我们命名为nextProps)。在这个方法里,你可以通过this.props访问当前的属性值,可以通过nextProps访问即将更新的属性值,或者将它们进行对比,或者将它们进行计算,最终确定你需要更新的状态(state)并最终调用setState方法对状态进行更新。在这个钩子函数中调用setState方法并不会触发再一次渲染。

非常有意思的是,虽然props的更改会引起componentWillReceiveProps的调用;但componentWillReceiveProps的调用并不意味着props真的发生了变化。这可不是我说的,Facebook官方花了一整篇文章说这件事:(A => B) !=> (B => A)。比如看下面这个组件:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 1,
    }
    this.onClick = this.onClick.bind(this);
  }
  onClick() {
    this.setState({
      number: 1,
    })
  }
  render() {
    return (
      <MyButton onClick={this.onClick} data-number={this.state.number} />
    );
  }
}

每一次点击事件都会重新使用setState接口对state进行更新,但每次更新的值都是相同的,即number:1。并且把当前组件的状态以属性的形式传递给<MyButton />。问题来了,那么当我每次点击按钮时,按钮MyButtoncomponentWillReceiveProps都会被调用吗?

会,即使每次更新的值都是一样的。

之所以出现这样的情况原因其实非常简单,因为React并不知道传入的属性是否发生了更改。而为什么React不尝试去做一个是否相等的判断呢?

因为办不到,新传入的属性和旧属性可能引用的是同一块内存区域(引用类型),所以单纯的用===判断是否相等并不准确。可行的解决办法之一就是对数据进行深度拷贝然后进行比较,但是这对大型数据结构来说性能太差,还能会碰上循环引用的问题。

所以React将这个变化通过钩子函数暴露出来,千万不要以为当componentWillReceiveProps被调用就意味着props发生了更改,如果需要在变化时做一些事情,务必要手动的进行比较。

shouldComponentUpdate()

shouldComponentUpdate很重要,它可以决定是否继续当前的生命周期。默认情况该函数返回true即继续当前的生命周期;也可以返回false终止当前的生命周期,阻止进一步的render与接下来的步骤。

我们上面刚刚说过,React并不会对props进行深度比较,这对state也同样适用。所以即使propsstate并未发生了更改,shouldComponentUpdate也会被再次调用,包括接下来的步骤componentWillUpdaterendercomponentDidUpdate也都会再次运行一次。这很明显会给性能造成不小的伤害。

传递给shouldComponentUpdate的参数包括即将改变的propsstate,形参的名称是nextPropsnextState,在这个函数里你同时又能通过this关键字访问到当前的stateprops,所以你在这里你是“全知”的,可以完全按照你自己的业务逻辑判断是否stateprops是否发生了更改,并且决定是否要继续接下来的步骤。shouldComponentUpdate也就通常我们在优化React性能时的第一步。这一步的优化不仅仅是优化组件自身的流程,同时也能节省去子组件的重新渲染的代价 。

当然如果你对判断props是否发生改变的检测逻辑要求比较简单的话,比如只是浅度(shallow)的判断(即判断对象的引用是否发生了更改)对象是否发生了更改,那么可以利用PureRenderMixin

import PureRenderMixin from 'react-addons-pure-render-mixin'; // ES6
const createReactClass = require('create-react-class');

createReactClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <div className={this.props.className}>foo</div>;
  }
});

minins是React支持的一种允许多个组件共用代码的一种机制。PureRenderMixin插件的工作非常简单,它为你重写了shouldComponentUpdate函数,并对对象进行了浅度对比,具体代码可以从这里这里找到。

在ES6中你也可以通过直接继承React.PureComponent而不是React.Component来实现这个功能。用React官方的原话说就是

React.PureComponent is exactly like React.Component, but implements shouldComponentUpdate() with a shallow prop and state comparison.

Pure

我们再次强调,PureComponent为你实现的只是对引用是否发生了更改的判断,甚至可以说它只是简单的用===进行的判断,所以这也是我们称之为pure的原因。为了具体说明问题,我们举一个实际的例子

/* MyButton.js: */
import React from 'react';

class MyButton extends React.PureComponent {
  constructor(props) {
    super(props);
  }
  render() {
    console.log('render');
    return <button onClick={this.props.onClick}>My Button</button>
  }
}
export default MyButton;

/* App.js: */
import React from 'react';
import MyButton from './Button.js';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      arr: [1],
    }
    this.onClick = this.onClick.bind(this);
  }
  onClick() {
    this.setState({
      arr: [...this.state.arr, 2],
    });
  }
  render() {
    return (
      <MyButton onClick={this.onClick} data-arr={this.state.arr} />
    );
  }
}

export default App;

在上面的这个例子中,每一次点击都会修改state中的arr变量,arr变量的引用和值都发生了更改。重点是MyButton组件继承的是React.PureComponent。那么每一次点击时,MyButton中的log信息都会被打印出来,即每次都会重新出发render

如果我们把onClick方法做一些修改:

onClick() {
  const arr = this.state.arr;
  arr.push(2);
  this.setState({
    arr: arr,
  })
}

这个方法同样使得arr变量发生了变化,但是仅仅是值而不是引用,此时当再一次点击按钮(MyButton)时,MyButton都不会再次进行渲染了。也就是说PureComponent提前为我们进行了shallow comparison.

使用这种只修改引用,不修改数据内容的immutable data也常常作为优化React的一个手段之一。immutable.js就能为我们实现这个需求,每一次修改数据时你得到的其实是新的数据引用,而不会修改到原有的数据。同时Redux中的reducer想达到的效果其实也相似,reducer的重点是它的纯洁性(pure),在执行时不会造成副作用,即避免对传入数据引用的修改,同时也方便比较出组件状态的更新。

componentWillUpdate()

componentWillUpdate方法和componentWillMount方法很相似,都是在即将发生渲染前触发,在这里你能够拿到nextPropsnextState,同时也能访问到当前即将过期的propsstate。如果有需要的话你可以把它们暂存起来便于以后使用。

componentWillMount不同的是,在这个方法中你不可以使用setState,否则会立即触发另一轮的渲染并且又再一次调用componentWillUpdate,陷入无限循环中。

componentDidUpdate()

和Mount阶段类似,当组件进入componentDidUpdate阶段时意味着最新的原生DOM已经渲染完成并且可以通过refs进行访问。该函数会传入两个参数,分别是prevPropsprevState,顾名思义是之前的状态。你仍然可以通过this关键字访问当前的状态,因为可以访问原生DOM的关系,在这里也适用于做一些第三方需要操纵类库的操作。

update阶段各个钩子函数的调用顺序也与mount阶段相似,尤其是componentDidUpdate,子组件的该钩子函数优先于父组件调用

因为可以访问DOM的缘故,我们有可能需要在这个钩子函数里获取实际的元素样式,并且写入state中,比如你的代码可能会长这样:

componentDidUpdate(prevProps, prevState) {
// BAD: DO NOT DO THIS!!!
  let height = ReactDOM.findDOMNode(this).offsetHeight;
  this.setState({ internalHeight: height });
}

如果默认情况下你的shouldComponentUpdate()函数总是返回true的话,那么这样在componentDidUpdate里更新state的代码又会把我们带入无限render的循环中。如果你必须要这么做,那么至少应该把上一次的结果缓存起来,有条件的更新state:

componentDidUpdate(prevProps, prevState) {
  // One possible fix...
  let height = ReactDOM.findDOMNode(this).offsetHeight;
  if (this.state.height !== height ) {
    this.setState({ internalHeight: height });
  }
}

死亡阶段

componentWillUnmount()

当组件需要从DOM中移除时,即会触发这个钩子函数。这里没有太多需要注意的地方,在这个函数中通常会做一些“清洁”相关的工作

  1. 将已经发送的网络请求都取消掉
  2. 移除组件上DOM的Event Listener

总结

最后再次强调,本文是开源图书React In-depth: An exploration of UI development的归纳。基本上想了解生命周期看这一本书就够了,看完也无敌了。希望这篇中文简约版也会对你有帮助。

参考

React + Redux 性能优化(二)工具篇: Immutablejs

建议在阅读完上一篇[React + Redux 性能优化(一):理论篇](http://qingbob.com/redux-performance-01-basic/)之后再开始本文的旅程,本文的很多概念和结论,都在上篇做了详细的讲解这会是一篇长文,我们首先会讨论使用 Im...… Continue reading

JavaScript 奇怪事件簿

发布于 2018年02月22日