React 源码剖析系列:生命周期的管理艺术
DelilahRyla
8年前
<h3>前言</h3> <p>目前,前端领域中 React 势头正盛,很少能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。</p> <p>对于 React,其组件生命周期(Component Lifecycle)是它的核心概念,本文从源码入手,来剖析 React 生命周期的管理艺术。</p> <p>阅读本文需要对 React 有一定的了解,如果你不知何为组件的生命周期,请详读 React 生命周期的文档。</p> <p>如果你对 React 组件的生命周期存在些许疑惑,如生命周期如何顺序管理;setState 如何实现异步操作,又是何时真正更新等,那么本文值得阅读。</p> <p>React 的主要思想是通过构建可复用组件来构建用户界面。所谓组件其实就是 有限状态机,通过状态渲染对应的界面,且每个组件都有自己的生命周期,它规定了组件的状态和方法需要在哪个阶段进行改变和执行。</p> <p>有限状态机(FSM),表示有限个状态以及在这些状态之间的转移和动作等行为的模型。一般通过状态、事件、转换和动作来描述有限状态机,下面是描述组合锁状态机的模型图,包括5个状态、5个状态自转换、6个状态间转换和1个复位 RESET 转换到状态 S1。状态机,能够记住目前所处的状态,根据当前的状态可以做出相应的决策,并且在进入不同的状态时,可以做不同的操作。通过状态机将复杂的关系简单化,利用这种自然而直观的方式可以让代码更容易理解。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/5a24c4983142cb48cc2609c9b1936867.jpg"></p> <p>React 正是利用这一概念,通过管理状态来实现对组件的管理。例如,某个组件有显示和隐藏两个状态,通常会设计两个方法 show() 和 hide() 来实现切换;而 React 只需要设置状态 setState({ showed: true/false }) 即可实现。同时,React 还引入了组件的生命周期概念。通过它就可以实现组件的状态机控制,从而达到 “生命周期-状态-组件” 的和谐画面。</p> <p>虽然组件、状态机、生命周期这三者都不是 React 独创,如果熟悉 Web Components 标准,它与其中的自定义组件的生命周期的概念相似。但就目前而言,React 是将这三种概念结合地相对清晰流畅的界面库。</p> <h3>初探 React 生命周期</h3> <p>在自定义 React 组件时,根据需要会在组件生命周期的不同阶段实现不同的逻辑。为了查看 组件生命周期的执行顺序,你可以使用 react-lifecycle mixin,将此 mixin 添加到需要观察的组件中,当任何生命周期方法被调用时,都能在控制台观察到对应的生命周期的调用时状态。</p> <p><img src="https://simg.open-open.com/show/1c6dd5a6a21c75d8e7449266e129f704.jpg"></p> <p>通过反复试验,得到了组件的生命周期在不同状态下的执行顺序:</p> <ul> <li> <p>当首次装载组件时,按顺序执行 getDefaultProps、getInitialState、componentWillMount、render 和 componentDidMount;</p> </li> </ul> <ul> <li> <p>当卸载组件时,执行 componentWillUnmount;</p> </li> </ul> <ul> <li> <p>当重新装载组件时,此时按顺序执行 getInitialState、componentWillMount、render 和 componentDidMount,但并不执行 getDefaultProps;</p> </li> </ul> <ul> <li> <p>当再次渲染组件时,组件接受到更新状态,此时按顺序执行 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。</p> </li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/f6859b53f176c67fe69ce1b33af2b212.jpg"></p> <h3>疑问</h3> <ul> <li> <p>为何 React 会按上述顺序执行生命周期?</p> </li> <li> <p>为何 React 多次 render 时,会执行生命周期的不同阶段?</p> </li> <li> <p>为何 getDefaultProps 只执行了1次?</p> </li> </ul> <h3>详解 React 生命周期</h3> <p>自定义组件(ReactCompositeComponent)的生命周期主要通过三种状态进行管理:MOUNTING、RECEIVE_PROPS、UNMOUNTING,它们负责通知组件当前所处的状态,应该执行生命周期中的哪个步骤,是否可以更新 state。三个状态对应三种方法,分别为:mountComponent、updateComponent、unmountComponent,每个方法都提供了两种处理方法,will 方法在进入状态之前调用,did 方法在进入状态之后调用,三种状态三种方法五种处理方法,此外还提供两种特殊状态的处理方法。</p> <ul> <li> <p>mountComponent -> MOUNTING</p> </li> <li> <p>updateComponent -> RECEIVE_PROPS</p> </li> <li> <p>unmountComponent -> UNMOUNTING</p> </li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/2a675c4d6b9255581b29ede58e190743.jpg"></p> <h3>createClass 创建自定义组件</h3> <p>createClass 创建自定义组件的入口方法,负责管理生命周期中的 getDefaultProps。getDefaultProps 方法只执行一次,这样所有实例初始化的 props 将会被共享。</p> <p>通过 createClass 创建自定义组件,利用原型继承 ReactCompositeComponentBase 父类,按顺序合并 mixins,设置初始化 defaultProps,创建元素 ReactElement。</p> <p><img src="https://simg.open-open.com/show/d318e79e7c74998760564d057e53dccf.jpg"></p> <h3>状态一:MOUNTING</h3> <p>mountComponent 负责管理生命周期中的 getInitialState、componentWillMount、render 和 componentDidMount。</p> <p>由于 getDefaultProps 是通过 Constructor 进行管理,因此也是整个生命周期中最先开始执行,而 mountComponent 只能望洋兴叹,无法调用到 getDefaultProps。这就解释了为何 getDefaultProps 只执行1次的原因。</p> <p>由于通过 ReactCompositeComponentBase 返回的是一个虚拟节点,因此需要利用 instantiateReactComponent 去得到实例,再使用 mountComponent 拿到结果作为当前自定义元素的结果。</p> <p>首先通过 mountComponent 装载组件,此时,将状态设置为 MOUNTING,利用 getInitialState 获取初始化 state,初始化更新队列。</p> <p>若存在 componentWillMount,则执行;如果此时在 componentWillMount 中调用 setState,是不会触发 reRender,而是进行 state 合并。</p> <p>到此时,已经完成 MOUNTING 的工作,更新状态为 NULL,同时 state 也将执行更新操作,此刻在 render 中可以获取更新后的 this.state 数据。</p> <p>其实,mountComponent 本质上是通过 递归渲染 内容的,由于递归的特性,父组件的 componentWillMount 一定在其子组件的 componentWillMount 之前调用,而父组件的 componentDidMount 肯定在其子组件的 componentDidMount 之后调用。</p> <p>当渲染完成之后,若存在 componentDidMount 则触发。这就解释了 componentWillMount - render - componentDidMount 三者之间的执行顺序。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/4b44c6eb77c44dde2788bf1a925fefa5.jpg"></p> <p>instantiateReactComponent 通过判断元素类型(类型包括:object、string、function)创建元素实例,这里不做过多介绍,当讲解到 React Virtual DOM 时,再详细介绍此方法。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/937af2b7ecb90ec6843df62195dae136.jpg"></p> <h3>状态二:RECEIVE_PROPS</h3> <p>updateComponent 负责管理生命周期中的 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。</p> <p>首先通过 updateComponent 更新组件,如果前后元素不一致说明需要进行组件更新,此时将状态设置为RECEIVING_PROPS。</p> <p>若存在 componentWillReceiveProps,则执行;如果此时在 componentWillReceiveProps 中调用 setState,是不会触发 reRender,而是进行 state 合并。</p> <p>到此时,已经完成 RECEIVING_PROPS 工作,更新状态为 NULL,同时 state 也将执行更新操作,此刻 this.state 可以获取到更新后的数据。</p> <p>注意:此时 this.state 虽然获取到更新数据,但只能在内部源码中使用,我们在开发时,若在 componentWillReceiveProps 中调用 setState,那么在 componentWillReceiveProps、shouldComponentUpdate 和 componentWillUpdate 中还是无法获取到更新后的 this.state,即此时访问的this.state 仍然是未更新的数据,只有在 render 和 componentDidUpdate 中才能获取到更新后的this.state。</p> <p>调用 shouldComponentUpdate 判断是否需要进行组件更新,如果存在 componentWillUpdate,则执行。</p> <p>updateComponent 本质上也是通过 递归渲染 内容的,由于递归的特性,父组件的 componentWillUpdate 一定在其子组件的 componentWillUpdate 之前调用,而父组件的 componentDidUpdate 肯定在其子组件 componentDidUpdate 之后调用。</p> <p>当渲染完成之后,若存在 componentDidUpdate,则触发,这就解释了 componentWillReceiveProps - componentWillUpdate - render - componentDidUpdate 它们之间的执行顺序。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/88360268be74a9868322d2bd9f8c99c1.jpg"></p> <p>注意:禁止在 shouldComponentUpdate 和 componentWillUpdate 中调用 setState,会造成循环调用,直至耗光浏览器内存后崩溃。(请继续阅读,寻找答案)</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/d11a0a4526fa241519899ef745b2bc6f.jpg"></p> <h3>状态三:UNMOUNTING</h3> <p>unmountComponent 负责管理生命周期中的 componentWillUnmount。</p> <p>首先将状态设置为 UNMOUNTING,若存在 componentWillUnmount,则执行;如果此时在 componentWillUnmount 中调用 setState,是不会触发 reRender。更新状态为 NULL,完成组件卸载操作。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/ec64ef3271b7e5331026f5745aa1f4e1.jpg"></p> <h3>setState 更新机制</h3> <p>当调用 setState 时,会对 state 以及 _pendingState 更新队列进行合并操作,但其实真正更新 state 的幕后黑手是replaceState。</p> <p>replaceState 会先判断当前状态是否为 MOUNTING,如果不是即会调用 ReactUpdates.enqueueUpdate 执行更新。</p> <p>当状态不为 MOUNTING 或 RECEIVING_PROPS 时,performUpdateIfNecessary 会获取 _pendingElement、_pendingState、_pendingForceUpdate,并调用 updateComponent 进行组件更新。</p> <p>如果在 shouldComponentUpdate 或 componentWillUpdate 中调用 setState,此时的状态已经从 RECEIVING_PROPS -> NULL,则 performUpdateIfNecessary 就会调用 updateComponent 进行组件更新,但 updateComponent 又会调用 shouldComponentUpdate 和 componentWillUpdate,因此造成循环调用,使得浏览器内存占满后崩溃。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/32729938dc6f4507193b94119403fcdf.jpg"> <img src="https://simg.open-open.com/show/0b0dbab9288173e88a9106301b40ce0f.jpg"></p> <p>总结</p> <p>React 通过三种状态:MOUNTING、RECEIVE_PROPS、UNMOUNTING,管理整个生命周期的执行顺序;</p> <p>setState 会先进行 _pendingState 更新队列的合并操作,不会立刻 reRender,因此是异步操作,且通过判断状态(MOUNTING、RECEIVE_PROPS)来控制 reRender 的时机;</p> <p>不建议在 getDefaultProps、getInitialState、shouldComponentUpdate、componentWillUpdate、render 和 componentWillUnmount 中调用 setState,特别注意:不能在 shouldComponentUpdate 和 componentWillUpdate中调用 setState,会导致循环调用。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/455a8f3a5821ab486b27effe04e0256e.jpg"></p> <h3>参考资料</h3> <ul> <li> <p>Component API</p> </li> <li> <p>Component Specs and Lifecycle</p> </li> <li> <p>Thinking in React</p> </li> <li> <p>Polymer registering elements</p> </li> <li> <p>JavaScript 与有限状态机</p> </li> </ul> <p>最后,想了解更多React的,可以通过这本《深入React技术栈》了解。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/9df147bc5cef2c5ea0e3644a6db4da05.jpg"></p> <p> </p> <p> </p> <p>来自:http://mp.weixin.qq.com/s/h5fiydAmuXnyhF_JtxnKgA</p> <p> </p>