朗朗上口的react 生命周期(上)

React组件的生命周期划分为出生(mount),更新(update)和死亡(unmount),然而我们怎么知道组件进入到了哪个阶段?只能通过React组件暴露给我们的钩子(hook)函数来知晓。什么是钩子函数,就是在特定阶段执行的函数,比如constructor只会在组件出生阶段被调用一次,这就算是一个“钩子”。反过来说,当某个钩子函数被调用时,也就意味着它进入了某个生命阶段,所以你可以在钩子函数里添加一些代码逻辑在用于在特定的阶段执行。当然这不是绝对的,比如render函数既会在出生阶段执行,也会在更新阶段执行。顺便多说一句,“钩子”在编程中也算是一类设计模式,比如github的Webhooks。顾名思义它也是钩子,你能够通过Webhook订阅github上的事件,当事件发生时,github就会像你的服务发送POST请求。利用这个特性,你可以监听master分支有没有新的合并事件发生,如果你的服务收到了该事件的消息,那么你就可以例子执行部署工作。

我们按照阶段的时间顺序对每一个钩子函数进行讲解。

出生

  • constructor
  • getDefaultProps() (React.createClass) orMyComponent.defaultProps (ES6 class)
  • getInitialState() (React.createClass) or this.state = ... (ES6 constructor)
  • componentWillMount()
  • render()
  • componentDidMount()

首先我们要引入一个概念:组件(Component)。组件非常好理解,就是可以复用的模板。例如通过按钮组件(模板)我们可以实例化出多个相似的按钮出来。这和代码中类(Class)的概念是相同的。并且在ES6代码中定义组件时也是通过类来实现的:

import React from 'react';

class MyButton extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <button>My Button</button>
    )
  }
}

也可以通过ES2015的语法接口React.createClass来定义组件:

const MyButton = React.createClass({
  render: function() {
    return (
      <button>My Button</button>      
    );
  }
});

如果你的babel配置文件.babelrcpresets指定了es2015,那么在编译之后的文件中,你会发现class MyButton extends React.Component语句编译之后的结果就是React.createClass

注意到当我们在使用class定义组件时,继承(extends)了React.Component类。但实际上这并不是必须的。比如你完全可以写成纯函数的形式:

const MyButton = () => {
  return <h1>My Button</h1>
}

这就是无状态(stateless)组件,顾名思义它是没有自己独立状态的,这个概念被用于React的设计模式:High Order Component和Container Component中。具体可以参考我的另一篇文章面试系列之三:你真的了解React吗(中)组件间的通信以及React优化

它的局限也很明显,因为没有继承React.Component的缘故,你无法获得各种生命周期函数,也无法访问状态(state),但是仍然能够访问传入的属性(props),它们是作为函数的参数传入的。

定义组件时并不会触发任何的生命周期函数,组件自己也并不会存在生命周期这一说,真正的生命周期开始于组件被渲染至页面中。

让我们看一段最简单的代码:

import React from 'react';
import ReactDOM from 'react-dom';

class MyComponent extends React.Component {
  render() {
    return <div>Hello World!</div>;
  }
};

ReactDOM.render(<MyComponent />, document.getElementById('mount-point'));

在这段代码中,MyComponnet组件通过ReactDOM.render函数被渲染至页面中。如果你在MyComponent组件的各个生命周期函数中添加日志的话,会看到日志依次在控制台输出。

为了说明一些问题,我们尝试对代码做一些修改:

import MyButton from './Button';
class MyComponent extends React.Component {
  render() {
    const button = <MyButton />
    return <div>Hello World!</div>;
  }
};

在组件的render函数中,我们使用到了另一个组件MyButton,但是它并没有出现在最终返回的DOM结构中。问题来了,当MyComponnet组件渲染至页面上时,Mybutton组件的生命周期函数会开始调用吗?<MyButton />究竟代表了什么?

我们先回答第二个问题。<MyButton />看上去确实有些奇怪,但是别忘了它是JSX语法。如果你去看babel编译之后的代码就会发现,其实它把<MyButton />转化为函数调用:React.createElement(MyButton, null)。也就是说<XXX />语法,实际上返回的是一个XXX类型的React元素(Element)。React元素说白了就是一个纯粹的object对象,基本由key(id), props(属性), ref, type(元素类型)四个属性组成(children属性包含在props中)。为什么要用“纯粹”这个形容词,是因为虽然它和组件有关,但是它并不包含组件的方法,此时此刻,它仅仅是一个包含若干属性的对象。如果你觉得这一切看上去都无比熟悉的话,那么你猜对了,元素代表的其实是虚拟DOM(Virtual DOM)上的节点,是对你在页面上看到的每一个DOM节点的描述。

那么我们可以回答第一个问题了,仅仅是生成一个React元素是不会触发生命周期函数调用的。

当我们把React元素传递给ReactDOM.render方法,并且告诉它具体在页面上渲染元素的位置之后,它会给我们返回组件的实例(Instance)。在JS语法中,我们通过new关键字初始化一个类的实例,而在React中,我们通过ReactDOM.render方法来初始化一个组件的实例。但一般情况下我们不会用到这个实例,不过你也可以保留它的引用赋值给一个变量,当测试组件的时候可以派上用场

Default Porps & Default State

如果被问起constructor之后的下一个生命周期函数是什么,绝大部分人会回答componentWillMount。准确来说应该是getDefaultPropsgetInitialState

而为什么大部分人对这两个函数陌生,是因为这两个函数只是在ES2015语法中创建组件时暴露出来,在ES6语法中我们通过两个赋值语句实现了同样的效果。

比如添加默认属性的getDefaultProps函数在ES6中是通过给组件类添加静态字段defaultProps实现的:

class MyComponent extends React.Component() {
  //...
}
MyComponent.defaultProps = { age: 'unknown' }

在实际计算属性的过程中,将传入属性与默认属性进行合并成为最终使用的属性,用伪代码写的意思就是

this.props = Object.assign(defaultProps, passedProps);

注意知识点要来了,看下面这个组件:

class App extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <div>{this.props.name}</div>
  }
}
App.defaultProps = { name: 'default' };

我给这个组件设置了一个默认属性name,值为default。那么在

  1. <App name={null} />
  2. <App name={undefined} /> 这两种情况下,this.props.name值会是什么?也就是最终输出会是什么?

正确答案是如果给name传入的值是null,那么最终页面上的输出是空,也就是null会生效;如果传入的是undefined,那么React认为这个值是undefined货真价实的未定义,则会使用默认值,最终页面上的输出是default

而获取默认状态的函数getInitialState在ES6中是通过给this.state赋值实现的

class Person extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  //...
}

componentWillMount()

componentWillMount函数在第一次render之前被调用,并且只会被调用一次。当组件进入到这个生命周期中时,所有的stateprops已经配置完毕,我们可以通过this.propsthis.state访问它们,也可以通过setState重新设置状态。总之推荐在这个生命周期函数里进行状态初始化的处理,为下一步render做准备

render()

当一切配置都就绪之后,就能够正式开始渲染组件了。render函数和其他的钩子函数不同,它会同时在出生和更新阶段被调用。在出生阶段被调用一次,但是在更新阶段会被调用多次。

无论是编写哪个阶段的render函数,请牢记一点:保证它的“纯粹”(pure)。怎样才算纯粹?最基本的一点是不要尝试在render里改变组件的状态。因为通过setState引发的状态改变会导致再一次调用render函数进行渲染,而又继续改变状态又继续渲染,导致无限循环下去。如果你这么做了你会在开发模式下收到警告:

Warning: Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

另一个需要注意的地方是,你也不应该在render中通过ReactDOM.findDOMNode方法访问原生的DOM元素(原生相对于虚拟DOM而言)。因为这么做存在两个风险:

  1. 此时虚拟元素还没有被渲染到页面上,所以你访问的元素并不存在
  2. 因为当前的render即将执行完毕返回新的DOM结构,你访问到的可能是旧的数据。

并且如果你真的这么做了,那么你会得到警告:

Warning: App is accessing findDOMNode inside its render(). render() should be a pure function of props and state. It should never access something that requires stale data from the previous render, such as refs. Move this logic to componentDidMount and componentDidUpdate instead.

componentDidMount()

当这个函数被调用时,就意味着可以访问组件的原生DOM了。如果你有经验的话,此时不仅仅能够访问当前组件的DOM,还能够访问当前组件孩子组件的原生DOM元素。

你可能会觉得所有这一切应当。

在之前讲解每个周期函数时,都只考虑单个组件的情况。但是当组件包含孩子组件时,孩子组件的钩子函数的调用顺序就需要留意了。

比如有下面这样的树状结构的组件

image

在出生阶段时componentWillMountrender的调用顺序是

A -> A.0 -> A.0.0 -> A.0.1 -> A.1 -> A.2.

这很容易理解,因为当你想渲染父组件时,务必也要立即开始渲染子组件。所以子组件的生命周期开始于父组件之后。

componentDidMount的调用顺序是

A.2 -> A.1 -> A.0.1 -> A.0.0 -> A.0 -> A

componentDidMount的调用顺序正好是render的反向。这其实也很好理解。如果父组件想要渲染完毕,那么首先它的子组件需要提前渲染完毕,也所以子组件的componentDidMount在父组件之前调用。

正因为我们能在这个函数中访问原生DOM,所以在这个函数中通常会做一些第三方类库初始化的工具,包括异步加载数据。比如说对c3.js的初始化

import React from 'react';
import ReactDOM from 'react-dom';
import c3 from 'c3';

export default class Chart extends React.Component {

  componentDidMount() {
    this.chart = c3.generate({
      bindto: ReactDOM.findDOMNode(this.refs.chart),
      data: {
        columns: [
          ['data1', 30, 200, 100, 400, 150, 250],
          ['data2', 50, 20, 10, 40, 15, 25]
        ]
      }
    });
  }

  render() {
    return (
      <div ref="chart"></div>
    );
  }
}

因为能够访问原生DOM的缘故,你可能会在componentDidMount函数中重新对元素的样式进行计算,调整然后生效。因此立即需要对DOM进行重新渲染,此时会使用到forceUpdate方法

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

推荐阅读更多精彩内容

  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,419评论 1 33
  • 生命周期流程图简单如下: 组件让你把用户界面分成独立的,可重复使用的部分,并且将每个部分分开考虑。React.Co...
    Simple_Learn阅读 1,070评论 0 0
  • 40、React 什么是React?React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIE...
    萌妹撒阅读 1,001评论 0 1
  • 3. 组件生命周期 React严格定义了组件的生命周期,生命周期可能会经历如下三个过程: 装载过程(Mount):...
    怀念不能阅读 624评论 1 3
  • 我老公生病的时候,除非特别严重的肠胃问题,他总是会说,人不舒服就应该吃点想吃的好东西,然后只要还可以活动自如...
    走路外八字阅读 483评论 0 0