本文是对开源图书React In-depth: An exploration of UI development的概括和加强。同时也融入了本身在开发中的一些心得。javascript
你或许会问,阅读完这篇文章以后,对工做中开发React相关的项目有帮助吗?实话实说帮助不会太大。这篇文章不会教你使用一项新技术,不会帮助你提升编程技巧,而是完善你的React知识体系,例如区分某些概念,明白一些最佳实践是怎么来的等等。若是硬是要从功利的角度来考虑这些知识带来的价值,那么会是对你的面试很是有帮助,这篇文章里知识点在面试时经常会被问到,为何我知道,由于我吃过它们的亏。html
React组件的生命周期划分为出生(mount),更新(update)和死亡(unmount),然而咱们怎么知道组件进入到了哪一个阶段?只能经过React组件暴露给咱们的钩子(hook)函数来知晓。什么是钩子函数,就是在特定阶段执行的函数,好比constructor
只会在组件出生阶段被调用一次,这就算是一个“钩子”。反过来讲,当某个钩子函数被调用时,也就意味着它进入了某个生命阶段,因此你能够在钩子函数里添加一些代码逻辑在用于在特定的阶段执行。固然这不是绝对的,好比render
函数既会在出生阶段执行,也会在更新阶段执行。顺便多说一句,“钩子”在编程中也算是一类设计模式,好比github的Webhooks。顾名思义它也是钩子,你可以经过Webhook订阅github上的事件,当事件发生时,github就会像你的服务发送POST请求。利用这个特性,你能够监听master分支有没有新的合并事件发生,若是你的服务收到了该事件的消息,那么你就能够例子执行部署工做。java
咱们按照阶段的时间顺序对每个钩子函数进行讲解。react
constructor
getDefaultProps()
(React.createClass) orMyComponent.defaultProps
(ES6 class)getInitialState()
(React.createClass) or this.state = ...
(ES6 constructor)componentWillMount()
render()
componentDidMount()
首先咱们要引入一个概念:组件(Component)。组件很是好理解,就是能够复用的模板。例如经过按钮组件(模板)咱们能够实例化出多个类似的按钮出来。这和代码中类(Class)的概念是相同的。而且在ES6代码中定义组件时也是经过类来实现的:git
import React from 'react';
class MyButton extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<button>My Button</button>
)
}
}复制代码
也能够经过ES2015的语法接口React.createClass
来定义组件:github
const MyButton = React.createClass({
render: function() {
return (
<button>My Button</button>
);
}
});复制代码
若是你的babel配置文件.babelrc
中presets
指定了es2015
,那么在编译以后的文件中,你会发现class MyButton extends React.Component
语句编译以后的结果就是React.createClass
。web
注意到当咱们在使用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
方法来初始化一个组件的实例。但通常状况下咱们不会用到这个实例,不过你也能够保留它的引用赋值给一个变量,当测试组件的时候能够派上用场
若是被问起constructor
以后的下一个生命周期函数是什么,绝大部分人会回答componentWillMount
。准确来讲应该是getDefaultProps
和getInitialState
。
而为何大部分人对这两个函数陌生,是由于这两个函数只是在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
。那么在
<App name={null} />
<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
以前被调用,而且只会被调用一次。当组件进入到这个生命周期中时,全部的state
和props
已经配置完毕,咱们能够经过this.props
和this.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 tocomponentWillMount
.
另外一个须要注意的地方是,你也不该该在render
中经过ReactDOM.findDOMNode
方法访问原生的DOM元素(原生相对于虚拟DOM而言)。由于这么作存在两个风险:
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元素。
你可能会以为全部这一切应当。
在以前讲解每一个周期函数时,都只考虑单个组件的状况。可是当组件包含孩子组件时,孩子组件的钩子函数的调用顺序就须要留意了。
好比有下面这样的树状结构的组件
在出生阶段时componentWillMount
和render
的调用顺序是
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
方法
本文同时也发布在个人知乎专栏上,也欢迎你们关注