朗朗上口的react 生命周期(下)update(更新)

有关出生阶段请参考上一篇《深入React的生命周期(上):出生阶段(Mount)》

更新阶段

  • 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容

  • 生命周期流程图简单如下: 组件让你把用户界面分成独立的,可重复使用的部分,并且将每个部分分开考虑。React.Co...
    Simple_Learn阅读 1,079评论 0 0
  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,448评论 1 33
  • 组件的生命周期方法分以下三个阶段。 Mounting当创建组件的实例并将其插入到DOM中时,将调用这些方法:con...
    _八神光_阅读 1,093评论 0 0
  • React 生命周期很多人都了解,但通常我们所了解的都是单个组件的生命周期,但针对Hooks 组件、多个关联组件(...
    前端js阅读 7,037评论 3 7
  • 现在的人不管是学生还是工作的人总在抱怨不是学习压力大就是房价贵,总再抱怨社会等外界因素却从不找自己内在的因素。可能...
    青柚柠檬宝贝阅读 354评论 0 0