本文是对开源图书React In-depth: An exploration of UI development的归纳和增强。同时也融入了自己在开发中的一些心得。
你或许会问,阅读完这篇文章之后,对工作中开发React相关的项目有帮助吗?实话实说帮助不会太大。这篇文章不会教你使用一项新技术,不会帮助你提高编程技巧,而是完善你的React知识体系,例如区分某些概念,明白一些最佳实践是怎么来的等等。如果硬是要从功利的角度来考虑这些知识带来的价值,那么会是对你的面试非常有帮助,这篇文章里知识点在面试时常常会被问到,为什么我知道,因为我吃过它们的亏。
React组件的生命周期划分为出生(mount),更新(update)和死亡(unmount),然而我们怎么知道组件进入到了哪个阶段?只能通过React组件暴露给我们的钩子(hook)函数来知晓。什么是钩子函数,就是在特定阶段执行的函数,比如constructor只会在组件出生阶段被调用一次,这就算是一个“钩子”。反过来说,当某个钩子函数被调用时,也就意味着它进入了某个生命阶段,所以你可以在钩子函数里添加一些代码逻辑在用于在特定的阶段执行。当然这不是绝对的,比如render函数既会在出生阶段执行,也会在更新阶段执行。顺便多说一句,“钩子”在编程中也算是一类设计模式,比如github的webhooks。顾名思义它也是钩子,你能够通过Webhook订阅github上的事件,当事件发生时,github就会像你的服务发送POST请求。利用这个特性,你可以监听master分支有没有新的合并事件发生,如果你的服务收到了该事件的消息,那么你就可以例子执行部署工作。
我们按照阶段的时间顺序对每一个钩子函数进行讲解。
有关出生阶段请参考上一篇《深入React的生命周期(上):出生阶段(Mount)》
- componentWillReceiveProps()
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
更新阶段会在三种情况下触发:
:一个组件并不能主动更改它拥有的
- props
属性,它的
- props
属性是由它的父组件传递给它的。强制对
- props
进行重新赋值会导致程序报错。
- props
:
- state
的更改是通过
- state
接口实现的。同时设计
- 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
真的发生了变化。这可不是我说的,Facebook官方花了一整篇文章说这件事:(A => B) !=> (B => A)。比如看下面这个组件:
- props
- 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 />
的
- MyButton
都会被调用吗?
- componentWillReceiveProps
会,即使每次更新的值都是一样的。
之所以出现这样的情况原因其实非常简单,因为React并不知道传入的属性是否发生了更改。而为什么React不尝试去做一个是否相等的判断呢?
因为办不到,新传入的属性和旧属性可能引用的是同一块内存区域(引用类型),所以单纯的用
判断是否相等并不准确。可行的解决办法之一就是对数据进行深度拷贝然后进行比较,但是这对大型数据结构来说性能太差,还能会碰上循环引用的问题。
- ===
所以React将这个变化通过钩子函数暴露出来,千万不要以为当
被调用就意味着
- componentWillReceiveProps
发生了更改,如果需要在变化时做一些事情,务必要手动的进行比较。
- props
- shouldComponentUpdate()
很重要,它可以决定是否继续当前的生命周期。默认情况该函数返回
- shouldComponentUpdate
即继续当前的生命周期;也可以返回
- true
终止当前的生命周期,阻止进一步的
- false
与接下来的步骤。
- render
我们上面刚刚说过,React并不会对
进行深度比较,这对
- props
也同样适用。所以即使
- state
与
- props
并未发生了更改,
- state
也会被再次调用,包括接下来的步骤
- shouldComponentUpdate
、
- componentWillUpdate
、
- render
也都会再次运行一次。这很明显会给性能造成不小的伤害。
- componentDidUpdate
传递给
的参数包括即将改变的
- shouldComponentUpdate
和
- props
,形参的名称是
- state
和
- nextProps
,在这个函数里你同时又能通过
- nextState
关键字访问到当前的
- this
和
- state
,所以你在这里你是“全知”的,可以完全按照你自己的业务逻辑判断是否
- props
与
- state
是否发生了更改,并且决定是否要继续接下来的步骤。
- props
也就通常我们在优化React性能时的第一步。这一步的优化不仅仅是优化组件自身的流程,同时也能节省去子组件的重新渲染的代价 。
- shouldComponentUpdate
当然如果你对判断
是否发生改变的检测逻辑要求比较简单的话,比如只是浅度(shallow)的判断(即判断对象的引用是否发生了更改)对象是否发生了更改,那么可以利用
- props
:
- 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>
- ;
- }
- });/
是React支持的一种允许多个组件共用代码的一种机制。
- minins
插件的工作非常简单,它为你重写了
- PureRenderMixin
函数,并对对象进行了浅度对比,具体代码可以从这里和这里找到。
- shouldComponentUpdate
在ES6中你也可以通过直接继承
而不是
- React.PureComponent
来实现这个功能。用React官方的原话说就是
- React.Component
is exactly like
- React.PureComponent
, but implements
- React.Component
with a shallow prop and state comparison.
- shouldComponentUpdate()
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
中的log信息都会被打印出来,即每次都会重新出发
- MyButton
- render
如果我们把
方法做一些修改:
- onClick
- onClick() {
- const arr = this.state.arr;
- arr.push(2);
- this.setState({
- arr: arr,
- })
- }
这个方法同样使得
变量发生了变化,但是仅仅是值而不是引用,此时当再一次点击按钮(
- arr
)时,
- MyButton
都不会再次进行渲染了。也就是说
- MyButton
提前为我们进行了shallow comparison.
- PureComponent
使用这种只修改引用,不修改数据内容的immutable data也常常作为优化React的一个手段之一。immutable.js就能为我们实现这个需求,每一次修改数据时你得到的其实是新的数据引用,而不会修改到原有的数据。同时Redux中的reducer想达到的效果其实也相似,
的重点是它的纯洁性(pure),在执行时不会造成副作用,即避免对传入数据引用的修改,同时也方便比较出组件状态的更新。
- reducer
- componentWillUpdate()
方法和
- componentWillUpdate
方法很相似,都是在即将发生渲染前触发,在这里你能够拿到
- componentWillMount
和
- nextProps
,同时也能访问到当前即将过期的
- nextState
和
- props
。如果有需要的话你可以把它们暂存起来便于以后使用。
- state
与
不同的是,在这个方法中你不可以使用
- componentWillMount
,否则会立即触发另一轮的渲染并且又再一次调用
- setState
,陷入无限循环中。
- componentWillUpdate
- componentDidUpdate()
和Mount阶段类似,当组件进入
阶段时意味着最新的原生DOM已经渲染完成并且可以通过
- componentDidUpdate
进行访问。该函数会传入两个参数,分别是
- refs
和
- prevProps
,顾名思义是之前的状态。你仍然可以通过
- prevState
关键字访问当前的状态,因为可以访问原生DOM的关系,在这里也适用于做一些第三方需要操纵类库的操作。
- this
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中移除时,即会触发这个钩子函数。这里没有太多需要注意的地方,在这个函数中通常会做一些“清洁”相关的工作
最后再次强调,本文是开源图书React In-depth: An exploration of UI development的归纳。基本上想了解生命周期看这一本书就够了,看完也无敌了。希望这篇中文简约版也会对你有帮助。
本文同时也发布在我的知乎专栏,欢迎大家关注
来源: https://juejin.im/post/5a0852325188255ea95b6f26