关于创建组件的一段历史
【早期】创建一个组件有三种方法:通过 React.createClass、ES6 class、Function。
【现在】随着React v16.0的发布,React中React.createClass这个方法的实现被移除,facebook提供一个单独的包:create-react-class,用于实现React.createClass的功能。
React.createClass
【两个方法】使用React.createClass方法创建组件需要设置两个方法:getInitialState(初始化state)、getDefaultProps(提供默认的props)
【需要注意】getDefaultProps只会在组件创建的时候调用一次,getInitialState会在组件每次装载的时候调用
【一个🌰】在这个例子中我们给父子组件分别定义了getDefaultProps和getInitialState,由于父组件中加载了两个子组件,所以子组件的getInitialState调用了两次,但是getDefaultProps只调用了一次。
【程序运行结果】如下所示,getDefaultProps只会在组件创建的时候调用一次,getInitialState会在组件每次装载的时候调用一次。父组件中包含了两个子组件,所以init child state打印了两次。
在上面的🌰中,我们定义了getDefaultProps和getInitialState,它们对于组件来说都是生命周期函数。下面具体说明一下组件的生命周期函数。
组件的生命周期
生命周期(life-cycle)是相对于组件(component)来说的,每个组件都会经历一个从装载(Mount)到卸载(Unmount)的过程。
以下讨论的生命周期都是针对于通过ES6 class创建的组件来分析的,这些通过ES6 class创建的组件会被添加tag(tag值为:ClassComponent)。这类组件有以下生命周期函数
他们的执行情况如下图所示

Unmounting阶段
在组件卸载阶段,这个阶段会调用componentWillUnmount。
会导致组件卸载的情况:组件被删除。
在componentWillUnmount中可以做:清除定时器、取消网络请求、清除在componentDidMount中定义的监听器
源码分析
当组件卸载的时候,对于ES6 class创建的组件,会执行tag为ClassComponent的对应系列程序,最终会调用callComponentWillUnmountWithTimer这个方法,进而调用componentWillUnmount方法。(ReactFiberCommitWork.js)
实例说明
如下所示,点击button后子组件会被删除,在移除之前会调用componentWillUnmount(除此之外,不会再调用其他生命周期函数),输出component will unmount。
Mounting阶段
这个阶段会依次调用constructor、componentWillMount、render、componentDidMount。
其中constructor、componentWillMount、componentDidMount有且只会被调用一次。
React会先判断组件有没有装载,如果没有就执行装载的流程,如果有装载就执行更新的流程。(ReactFiberBeginWork.js)
constructor(props, context)
在constructor初始化state,执行this.state={...}(相当于调用getInitialState方法)
在constructor中必须调用super(props),这样才能使得this.props有值
workInProgress.type其指向当前正在被装载的组件,执行new ctor(props, context)时,会调用组件的生命周期函数constructor。(ReactFiberClassComponent.js)
componentWillMount()
在这个生命周期函数中可以执行this.setState更新state,使得后续访问的state为更新后的state。并且调用this.setState不会触发二次更新。
在mountClassInstance方法中有一句注释:If we had additional state updates during this life-cycle, let's process them now。当state有变化时,会调用updater.enqueueReplaceState将更新加入到更新队列中,如果updateQueue有值,就会调用processUpdateQueue方法,执行更新操作。 (ReactFiberClassComponent.js)
componentDidMount()
组件装载完成后调用componentDidMount,可以在这个方法中建立网络连接获取数据,或者声明监听器
在mountClassInstance方法中,也有componentDidMount的对应逻辑。在这里只是将effectTag的值设置为Update。 (ReactFiberClassComponent.js)
在Fiber执行Commit时,会执行commitLifeCycles,如果该组件的effetctTag为Update,就会执行生命周期函数componentDidMount或componentDidUpdate。逻辑为:如果当前组件第一次Mount,那么执行componentDidMount,如果已经装载过,则执行componentDidUpdate。(ReactFiberCommitWork)
Updating阶段
这个阶段会依次调用componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate。
只有当props有变化时,才会调用componentWillReceiveProps。
如果shouldComponentUpdate返回false,componentWillUpdate、render、componentDidUpdate不会再执行。
在Mount阶段的源码分析有提到:React会先判断组件有没有装载,如果没有就执行装载的流程,如果有装载就执行更新的流程。所以执行更新流程时,会执行updateClassInstance方法,这个方法中定义了如何执行componentWillReceiveProps,componentWillUpdate。(ReactFiberClassComponent.js)
componentWillReceiveProps(newProps, newContext)
同componentWillMount一样,在componentWillReceiveProps,也给予了用户一次修改state的机会,同样在这个方法里调用setState是不会触发二次更新的。
在updateClassInstance方法中有这样一段代码。很简单的逻辑,就是当componentWillReceiveProps方法存在时,并且props或context有变化 时,要执行callComponentWillReceiveProps。
在这里需要特别注意一个事:当父组件更新时,子组件的oldProps与newProps始终是不相等的,所以父组件更新时,子组件的componentWillReceiveProps总是会被调用。但是当子组件更新state时,由于oldProps和newProps是相等的,所以不会调用componentWillReceiveProps。
在执行完callComponentWillReceiveProps后,如果更新队列updateQueue!==null,那么需要更新state的值,这里是直接调用processUpdateQueue更新state,所以不会触发二次更新。
callComponentWillReceiveProps会调用组件的componentWillReceiveProps方法,并且如果state有改变,将更新加入到更新队列中。
shouldComponentUpdate(newProps, newState, newContext)
shouldComponentUpdate,在组件每次更新的时候都会调用,除非组件调用了forceUpdate。
同样在updateClassInstance方法中有这样一个逻辑,先计算shouldUpdate,然后根据shouldUpdate去判断是否执行更新
在checkShouldComponentUpdate函数中,从workInProgress.updateQueue.hasForceUpdate这句话我们可以知道,如果在更新过程中有调用forceUpdate方法,会跳过shouldComponentUpdate方法,执行更新流程。
componentWillUpdate(newProps, newState, newContext)
在updateClassInstance方法中,当shouldUpdate为true时就会执行更新程序,如果componentWillUpdate存在,就会执行它
componentDidUpdate(prevProps, prevState)
在updateClassInstance方法中,当shouldUpdate为true时就会执行更新程序,如果componentDidUpdate存在,设置effectTag为Update,并等待更新完成。
在componentDidMount的分析中提到,(当组件更新完成,会执行commitLifeCycles方法,逻辑为:如果当前组件第一次Mount,那么执行,如果已经装载过,则执行componentDidUpdate)。
Error Handing
componentDidCatch(error, info)
当子组件的周期函数中有错误发生时,错误会被父组件的componentDidCatch捕获
总结
React宏观上的渲染
React数据传递是置顶向下的,React整体上的渲染也是置顶向下的。可以将React组件的渲染操作想象成一个先入先出的队列,组件入队列之前执行render以及render之前的生命周期函数,出队列之前执行render之后的生命周期函数。如下所示:
从源码看组件的渲染
在组件开始渲染时,Fiber会首先创建一颗Fiber tree,并clone一个的副本:workInProgress。
更新时,对比current和workInProgress的差异,Fiber将有变化的Component的effectTag为Update。
在对比current和workInProgress的过程中会调用constructor、componentWillMount、componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate。如果在componentWillMount和componentWillReceiveProps中调用了setState,对state的更改会被加入更新队列,随后将立即执行更新队列得到新的state。
当完成current和workInProgress比较后,Fiber就会执行Commit完成渲染,这个阶段会调用componentDidMount、componentDidUpdate、componentWillUnmount。如果componentDidMount、componentDidUpdate有调用setState,会触发二次渲染。
无状态组件的渲染
通过Function创建的组件是无状态组件,它是没有生命周期,也不需要管理state,每次都会被重新渲染。
有状态组件的渲染
通过ES6 class创建的组件是有生命周期函数的,可以管理自己的state。
这类组件的渲染分为了三个阶段:Mount、Unmount、Update。组件的Mount阶段只会在组件装载的时候执行一次,Update阶段会在组件props或state有变化时执行。
Mount阶段会执行的周期函数有:constructor、componentWillMount、render、componentDidMount。
Update阶段会执行的周期函数有:componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate
在React v16.0以后新增一个阶段:Error Handing,这个阶段会执行周期函数:componentDidCatch
props变化引起的update
组件的props变化导致组件更新时,会调用componentWillReceiveProps。
注意:父组件每次更新后,传递给子组件的props都会变化。这就意味着父组件更新后,子组件默认都会更新。
state变化引起的update
组件自身的state变化导致组件更新时,不会调用componentWillReceiveProps。
注意:子组件更新不会引起父组件更新,因为在React中更新时局部的。
改变state
通过setState来更改组件的state,并且setState是一个异步的动作。调用setState对state的更改都会被放到更新队列中。所以在生命周期函数中调用setState后,立即访问state还会是旧的值。
性能优化
默认情况下,父组件更新,其下的所有子组件都会更新,因为虽然props的值没有变化,但是它的引用变了。
默认情况下,shouldComponentUpdate默认返回true,组件state即使没有变化,只要调用了setState,组件都会更新。
可以在shouldComponentUpdate里,通过判断oldProps和newProps的值是否相等,oldState和newState的值是否相等来控制组件的更新,达到性能优化的目的。