React火了很久了,一直都停留在照葫芦画瓢按照example凑数的基础上,但是如果真的要了解一个框架,它的生命周期和核心API是最重要的部分了,下面我们就来聊聊React的生命周期、核心API的用法以及React工具集,在使用React进行深度开发的时候,一定会事半功倍。本文假设你已经了解了React的基本开发知识,如果不了解,强烈推荐阮老师的这篇《React 入门实例教程》

一、生命周期

1、创建一个类和实例化的基础方法

1
2
3
4
5
6
7
8
9
10
11
12
//创建组件类
var ComponentBox = React.createClass({
//other lifecycle method
render: function () {
//return JSX code
}
});
//实例化组件
var compInstance = React.render(
<ComponentBox />,
document.getElementById('content') //DOM Element
);

2、了解this.state和this.prop

使用过React的人都会知道它有两个属性,state还有props,他们两者都可以作为render中的data输入源,那两者的区别、联系以及正确的用法又是怎么样的呢。参见这篇《props-vs-state》

  • prop是组件的配置项,是可选的,当组件接收这个参数后它就是不变的。组件改变不了它的props,父组件的props还担当着收集子组件配置项的功能。
  • state,在组件初始化的时候会赋予state初始的状态,当组件的状态发生变化的时候,组件内部自行管理着state的变化,所以state可以说是组件的私有值。state参数是可有可无的,但当你的组件是“Stateful Component”的时候你就应该考虑使用state了。

两者在组件变化的时候的状态改变如下:

- props state
Can get initial value from parent Component? Yes Yes
Can be changed by parent Component? Yes No
Can set default values inside Component?* Yes Yes
Can change inside Component? No Yes
Can set initial value for child Components? Yes Yes
Can change in child Components? Yes No

那么两者如何合理的使用和规划呢。举例来说,在官网《thinking-in-react》有这样的一个例子,组件如下图,当需要有过滤和搜索功能的时候,其实组件就是一个“具有状态的组件”了,需要有state管理对应的状态。通过分析,我们可以得到对应的变量规划:

  • 用于表示所有产品列表的 products
  • 用于表示过滤后的列表 (filterProducts)
  • 表示是否使用过滤的功能(checkValue)
  • 使用搜索过滤的关键词(searchValue)

要分析变量是否是state,主要考虑这几个问题:

  • 变量是否会通过父组件传递参数,如果是,那么它肯定不是state,这里products 肯定不是state
  • 变量是否会出现变化,如果不会出现变化,那么它肯定不是state
  • 你会不会根其他的state或者props计算这个值,如果是经过计算得到的,那么它一定不是state,由此可见filterProducts也不是state
    再考虑 checkValue和 searchValue在本质上是随组件内部改变的,所以,这两个变量应该作为state管理。所以最后的程序如下

See the Pen React filter list demo by zhangmeng (@zhangmeng712) on CodePen.

3、生命周期详情分析

1) 初始化阶段

  • getDefaultProps
  • getInitialState
  • propTypes
  • Mixins
  • statics
  • displayName

getDefaultProps初始化参数使用,当组件类被创建的时候会被调用一次。输入为函数类型,返回object为this.props的初始化值,当父组件没有指定具体参数的时候,参数会在这个方法中被详细映射,此外 为了增加程序的可读性和容错性,建议在这个方法中指定好参数的初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*Input.jsx*/
/*Form是input的父组件*/
module.exports = React.createClass({
displayName: 'Input',
getDefaultProps: function () {
return {
//父组件中具体的参数映射,为了程序更加清晰,建议所有的props都在getDefaultProps中定义
model: {
name: '',
type: '',
value: '',
error: false,
enabled: true
}
};
}
});
/* Form.jsx */
module.exports = React.createClass({
displayName: 'Form',
render: function () {
return (<form>
<Input model={fieldModel}
ref={fieldModel.name}/>
</form>);
}
})

getInitialState初始化state使用,在组件mount之前被调用一次。输入为函数,返回object为this.state的初始化值。
propTypes用于属性的验证使用,输入为object。如果输入的类型和验证中设置的类型不符,在dev环境会给出提示。除了React内置的类型如 React.PropTypes.array,还支持自定义类型,详情见Reusable Components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var ComponentBox = React.createClass({
//默认值设置
getDefaultProps: function () {
return {
initalX: 14,
initalProp: '11', //内置校验
customProp: 'hi' //自定义校验
}
},
//允许校验属性的方法,只在dev环境会显示warn
propTypes: {
//React内置类型
initalProp: React.PropTypes.array,
//自定义类型
customProp: function (props, propName,componentName) {
if (props[propName] !== 'hello') {
return new Error('Validation failed, customProp value needs to be hello');
}
}
}
});
var compInstance = React.render(
<ComponentBox />,
document.getElementById('content') //DOM Element
);


Mixins输入为array类型,用于定义组件间共享的方法,其中的方法可以是生命周期的方法,也可以是自定义方法。不过有几点需要注意:

  • 定义在其内部的方法会优先于类上的方法执行
  • 在Mixins中和类上同时定义render方法会抛出异常(Uncaught Error: Invariant Violation: ReactClassInterface: You are attempting to define render on your component more than once)
  • 在Mixins数组中定义同样名称的非生命周期方法也会抛出异常(Uncaught Error: Invariant Violation: ReactClassInterface: You are attempting to define logFunc on your component more than once. )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var commonMixins = {
getDefaultProps: function() {
return {
initalY: 20
}
}
};
var commonMixins1 = {
//定义2个render定义会抛出异常
// render: function () {
//
// }
// 定义同样名称的非生命周期方法也会抛出异常
logFunc: function() {
console.log('Mixins log method')
}
};
var commonMixins2 = {
logFunc: function() {
console.log('Mixins log method')
}
};
var ComponentBox = React.createClass({
mixins: [commonMixins,commonMixins1,commonMixins2]
});
var compInstance = React.render( < ComponentBox / > ,
document.getElementById('content') //DOM Element
);

statics类上的静态方法,可以在实例化之前被类自己调用。
displayName用于在调试信息中标示组件,JSX会自动的设置它的值

2) Mounting阶段

  • componentWillMount
  • componentDidMount
    componentWillMount只会被调用一次。运行于初始化之后,render方法之前。当在此函数中调用setState后,render会显示被修改的state内容,注意,尽管state内容被改变了,但是不会再多次调用render。
    componentDidMount在render执行之后被调用,这个方法只会被调用一次。在这个方法中,可以通过React.findDOMNode(this)对组件的dom元素进行操作,子组件的componentDidMount方法会优先于父组件的componentDidMount方法被调用。我们会在这个方法中执行ajax请求或者调用timer或者用其他类库进行交互。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
componentDidMount: function () {
console.log('-------componentDidMount execute-------')
var input = $('input[type="text"]');
input.focus();
var cityName = this.props.propValue;
$.get('http://api.openweathermap.org/data/2.5/weather?q=' + cityName , function (data,status) {
if (status === 'success') {
var weather = data.weather || [];
if (weather.length && weather[0]) {
this.setProps({
weather: weather[0].description
});
}
}
}.bind(this))
}

3) 数据更新阶段

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate
    componentWillReceiveProps当新的props参数被发现时,就会调用这个方法,普通的render之后是不调用这个函数的。改变之前的props参数可以通过 this.props获得,新的参数可以通过第一个入参获得。在这个方法中调用setState不会触发额外的render调用。
1
2
3
componentWillReceiveProps: function (nextProps) {
console.log('-------componentWillReceiveProps execute-------', 'old weather is this.props.weather:', this.props.weather, 'new props is nextProps.weahter:' + nextProps.weather);
}

shouldComponentUpdate当props和state变化后被触发,初次render和强制更新的时候此方法不会被调用。当你希望某个props或者state的值改变的时候,不需要render被再次执行,就可以在shouldComponentUpdate中return false来实现,而此时接下来的 componentWillUpdate 和componentDidUpdate也不会执行。默认shouldComponentUpdate会自动返回true,但是你可以通过比较参数的变化来重写这个函数,如下:

1
2
3
4
5
6
7
8
shouldComponentUpdate: function (nextProps, nextState) {
console.log('-------shouldComponentUpdate execute-------')
var flag = true;
if (nextProps.weather == 'light rain' ) {
flag = false;
}
return flag;
}

componentWillUpdate判断完是否能调用render之后,就会执行componentWillUpdate,这个函数是用作render之前发生更新的改变。在这个方法中不能使用setState方法,如果要更新state,在componentWillReceiveProps函数中进行处理。
componentDidUpdate当参数变更完成,render执行完成DOM完全被更新之后会触发,可用于操作新更新的DOM元素,入参为 prevProps和 prevState,以防操作中需要变更前的数据

4) Unmounting阶段

  • componentWillUnmount
    componentDidUpdate当组件从DOM中销毁的时候调用,可在函数中对timer和不需要的dom元素进行清理
    关于组件的生命周期执行顺序和props更新流程等可以参考以下的例子:

    See the Pen React lifycycle testing by zhangmeng (@zhangmeng712) on CodePen.

二、你应该掌握的API用法

1、常用 API

  • React.createClass
  • React.render
  • React.findDOMNode
  • React.createElement:创建一个virtual dom进行渲染,可以使用React.render进行加载
  • React.Children: 用于处理组件中的this.props.children
    • React.Children.map
    • React.Children.only
    • React.Children.forEach
    • React.Children.count

2 组件相关API

  • this.setState:设置state,除了支持key value的传送方式,还支持传入fn, 注意通过上述的描述可以调用它的生命周期方法为:componentDidMount以及componentWillMount(不会触发刷新render,但是可以赋值)。
  • this.setProps:设置props
  • this.props.children:表示在实例化的时候传入组件的所有子节点,可以通过React.Children来统一处理渲染到页面上
  • this.props.refs:render中Dom Node可以用ref来标示,这样就可以利用React.findDOMNode(this.refs.xx)来获取对应的DOM元素了。ref可以传入名称,也可以传入函数更多详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//setState的函数传入,入参为之前的state对象,还有当前的props对象
this.setState(function (prevState, currentProps) {
return {
stateValue: 'new stateValue'
}
});
//操作this.props.children
var ComponentBox = React.createClass({
render: function () {
return (<ul>
{
//能够渲染
React.Children.map(this.props.children, function (child) {
return <li>{child}</li>
})
}
{
// 返回不是object ?渲染不出
React.Children.forEach(this.props.children, function (child) {
return <li>{child}</li>
})
}
</ul>)
}
});
var compInstance = React.render(
<ComponentBox><span>list1</span><span>list2</span></ComponentBox>,
document.getElementById('content') //DOM Element
);
//refs的用法
<input ref={ function(component){ React.findDOMNode(component).focus();} } />
<input type="text" ref="myTextInput" defaultValue={this.props.propValue} />
var input = React.findDOMNode(this.refs.myTextInput)

3、Add-on API

  • Animation API:

    • ReactCSSTransitionGroup 用于控制动画的标签。
    • transitionName 定义动画的class前缀,以下例为基础默认在标签内新增的的元素,动画效果的className为example-enter example-enter-active;元素被删除时候效果的className为 example-leave example-leave-active
    • transitionAppear v0.13开始有的方法,用于书写初始化的动画效果,默认是false。注意:一定要先让ReactCSSTransitionGroup这个标签渲染出来,然后再在标签内增加元素,否则效果不生效。
    • 有人在使用这个Add-on的时候遇到了动画时序的问题,也可以使用自定义的动画解决,参考这个例子

      <ReactCSSTransitionGroup transitionName="example" transitionAppear={true}>
                        {items}
                        </ReactCSSTransitionGroup>
      

      See the Pen react animtion test by zhangmeng (@zhangmeng712) on CodePen.

  • Test-Utils API:Test-Utils API: 一般的类库很少提供这种Add-on,更多是通过工具对其进行补充,例如Angular的protractor,以及Polymer的web-componnets-tester,这点React还是想的很全面的,当然它也有对其组件测试的Jest框架,但是通过它的Test-Utils可以结合其它的测试框架进行测试。

三 组件的拆分和数据的传递

一般来说,稍微复杂的组件可以被拆分成若干组件(拆分本着一个组件只做一件事情的原则,参见Single_responsibility_principle)。合理的组件拆分会让React组件的开发复用性更强,那么在组件中如何管理组件间数据的传递?具体可以参考《How to communicate between React components》这篇文章,讲解的非常详细。

参考资料