前言
如同人有生老病死 , 自然界有日月更替,每个组件在网页中也会被创建、更新和删除,如同有生命的机体一样 。
React 严格定义了组件的生命周期,生命周期可能会经历如下三个过程:
- 装载过程( Mount),也就是把组件第一次在 DOM 树中渲染的过程;
- 更新过程( Update ),当组件被重新渲染的过程;
- 卸载过程( Unmount),组件从 DOM 中删除的过程 。
三种不同的过程, React 库会依次调用组件的一些成员函数,这些函数称为生命周期函数。
(一)装载过程
当组件第一次被渲染的时候,依次调用的函数
-
constructor: 构造函数,要创造一个组件类的实例。
- 用于继承父类的props
- 初始化state
- 绑定成员函数的this
getlnitialState: 函数的返回值会用来初始化组件的 this.state ,在一个组件声明周期
只调用一次,不建议放置多次调用代码
。但是,这个方法只有用 React.createClass 方法创造的组件类才会发生作用(PS:现在已经被遗弃,ES声明在构造函数中)。getDefaultProps:函数的返回值可以作为 props 的初始值。但是,这个函数只在 React.createClass 方法创造的组件类才会用到 (PS:现在已经被遗弃,ES声明使用组件的 defaultProps属性)。
componentWillMount:将要装载时。即使调用 this.setState修改状态也不会引发重新绘制。换句话说,所有可以在这个 componentWillMount 中做的事情,都可以提前到 constructor 中间去做,可以认为这个函数存在的主要目的就是为了和 componentDidMount 对称 。
render: 返回渲染内容。没有也必须返回null或者false,告诉React不需要渲染DOM元素。
componentDidMount:组件加载完成。render 函数被调用完之后,
并非立即调用
,是render函数返回的内容已经渲染到DOM树上
。此时才触发此函数。
我们还是以 ControlPanel 为例,在 ControlPanel 中有三个 Counter 组件,我们稍微
修改 Counter 的代码,让装载过程中所有生命周期函数都用 console.log 输出函数名和caption 的值。
constructor(props){
....
+ console.log(`enter constructor, ${this.props.caption}`);
}
+ componentWillMount() {
+ console.log(`enter componentWillMount,${this.props.caption}`);
+ }
+ componentDidMount() {
+ console.log(`enter componentDidMount,${this.props.caption}`);
+ }
render(){
- console.log('render');
+ console.log(`enter render, ${this.props.caption}`);
...
}
现在我们看打印结果:
上面代码可以看出:componentWillMount紧跟render函数。但只有当所有三个组件的render 函数都被调用之后, 三个组件的 componentDidMount
才连在一起被调用 。
因为 render 函数本身并不往 DOM 树上渲染或者装载内容,它只是返回一个 JSX 表示的对象
,然后由 React 库来根据返回对象决定如何渲染 。而 React 库肯定是要把所有组件返回的结果综合起来,才能知道该如何产生对应的 DOM修改 。 所以,只有 React 库调用三个 Counter 组件的 render 函数之后,才有可能完成装载,这时候才会依次调用各个组件的 componentDidMount 函数作为装载过程的收尾 。
componentWillMount和componentDidMount 的区别
componentWillMount 可以在服务器端被调用,也可以在浏览器端被调用
;
而 componentDidMount只能在浏览器端被调用
,在服务器端使用 React 的时候不会被调用 。
PS:
componentDidMount类似window.onload。因为项目前期已经用 jQuery 开发了很多功能,需要继续使用这些基于 jQuery 的代码,就需要是利用 componentDidMount 函数。但是 componentDidMount 中调用 jQuery 代码只处理了装载过程,要和 jQuery 完全结合,又要考虑 React 的更新过程,就需要使用下面要讲的 componentDidUpdate 函数 。
(二)更新过程
当组件当 props 或者state 被修改的时候,就会引发组件的更新过程 ,依次调用的函数(但是不是所有更新过程都会执行全部函数)。
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
-
componentDidUpdate
(1)componentWillReceiveProps(nextProps)
只要是父组件的 render 函数被调用
,在 render 函数里面被谊染的子组件就会经历更新过程,不管父组件传给子组件的 props 有没有改变,都会触发子组件的 componentWillReceiveProps 函数 。
// src/views/ControlPanel/index.jsx
render(){
return (
<Counter caption="Third" />
+ <button onClick={ () => this.forceUpdate () }>
+ Click me to repaint!
+ </button>
</div>
)
}
// src/views/ControlPanel/Counter.jsx
constructor(props){
....
- console.log(`enter constructor, ${this.props.caption}`);
+ // console.log(`enter constructor, ${this.props.caption}`);
}
- componentWillMount() {
+ /*componentWillMount() {
console.log(`enter componentWillMount,${this.props.caption}`);
- }
+ }*/
- componentDidMount() {
+ /*componentDidMount() {
console.log(`enter componentDidMount,${this.props.caption}`);
+ }*/
+ componentWillReceiveProps (nextProps) {
+ console.log(`enter componentWillReceiveProps,${nextProps.caption}`);
}
render(){
- console.log(`enter render, ${this.props.caption}`);
+ console.log(`ControlPanel render, ${this.props.caption}`);
...
}
现在我们看打印结果:
上面代码中,nextProps 代表的是这一次渲染传入的 props 值, this.props 代表的上一次渲染时的 props 值,在父容器调用 this .forceUpdate 重绘,但是props并未更改(每个 React 组件都可以通过 forceUpdate 函数强行引发一次重新绘制 )。
但是当我们点击第一个组件的+ 按钮时。可以看见控制台打印为:
ControlPanel render, First // render函数打印结果
finish // 点击+ 按钮更新state的成功回调。
可以发现 this.setState 不会引发 componentWillReceiveProps函数 被调用 。
PS 所以在 React 的组件组合中,完全可以只渲染一个子组件,而其他组件完全不需要渲染,这是提高 React 性能的重要方式.
(2)shouldComponentUpdate(nextProps, nextState)
除了 render 函数, shouldComponentUpdate 可能是 React 组件生命周期中最重要的一
个函数了 。
因为 render 函数决定了该渲染什么,而说 shouldComponentUpdate 函数重要,是因为它决定了一个组件什么时候不需要渲染 。并且是唯一两个必须要返回值的函数。
shouldComponentUpdate 函数返回布尔值。默认为true,若为false,则后续的render函数不讲执行。使用得当能提高React性能。
现在给 Counter 组件类增加 shouldComponentUpdate 函数的定义
shouldComponentUpdate(nextProps , nextState) {
return (nextProps.caption !== this.props.caption) || (nextState.count !==this.state.count);
}
上面可以发现。点击父容器因为props一样。并未触发render函数重绘。但是修改子组件state引起render函数重绘。
(3)componentWillUpdate 和 componentDidUpdate
如果组件的 shouldComponentUpdate 函数返回 true。 React 接下来就会依次调用对应
组件的 componentWillUpdate 、 render 和 componentDidUpdate 函数 。
当在服务器端使用 React 渲染时,这一对函数中的 Did 函数,也就是 componentDidUpdate 函数,并不是只在浏览器端才执行的,无论更新过程发生在服务器端还是浏览器端,该函数都会被调用 。
这是区别装载过程的componentDidMount函数的。
使用 React 做服务器端渲染时,基本不会经历更新过程,因为服务器端只需要产出 HTML 字符串,一个装载过程就足够产出 HTML了,所以正常情况下服务器端不会调用 componentDidUpdate 函数。
但是客户端,当 React 组件被更新时,原有的内容被重新绘制,这时候就需要在 componentDidUpdate 函数再次调用 jQuery 代码 。
(三)卸载过程
React 组件的卸载过程只涉及一个函数 componentWillUnmount
。
当 React 组件要从DOM 树上删除掉之前,对应的 componentWillUnmount 函数会被调用,所以这个函数适合做一些清理性的工作 。比如,在componentDidMount 中用非 React 的方法创造了一些 DOM 元素或者定时器之类,如果撒手不管可能会造成内存泄露,那就需要在 componentWillUnmount 中清理掉。
和装载过程与更新过程不一样
,这个函数没有配对的 Did 函数,就一个函数,因为卸载完就完了,没有“卸载完再做的事情” 。
PS 此图是下载别人的。原文地址
(三)组件向外传递数据和实例
前文说过。在 JavaScript 中,函数是一等公民,函数本身就可以被看做一种对象,既可以像其他
对象一样作为 prop(prop可以是任意一种js的数据类型
) 的值从父组件传递给子组件,又可以被子组件作为函数调用。
现在我们来写个demo。需要子组件更新后。在父组件底部同步显示count的总数。
代码太多。可以查看版本链接
代码很简单。就不解释呢。
运行图
(四)React 组件 state 和 prop 的局限
上面demo中。每个 Counter 组件有自己的状态记录当前计数,而父组件ControlPanel 也有一个状态来存储所有 Counter 计数总和,也就是说,数据发生了重复 。带来的一个问题就是如何保证重复的数据一致,如果数据存多份而且不一致,那就很难决定到底使用哪个数据作为正确结果了 。
一种方案: 以一个组件的state为准,将Counter变为stateless组件,只负责展示。ControlPanel 变为容器组件。将变化全部放在这个state内部。通过func来回调执行方案。但是这样会导致修改父容器的render函数。其他子组件全部更新(你也可以判断传入props实现子组件不更新)。但是这样一旦子组件过多怎么管理子组件数据?
另一种方案。就是加个顶层容器来记录所有的state。但是问题也出现?props需要传两层,不符合低耦合的设计规范。
这是就需要全局的状态管理。Flux,Redux或者MobX 。下一章会细说状态管理所解决的问题。