在React中,组件的更新本质上都是由setState操作改变state引起的。因此组件更新的入口在于setState,同样经过撸源码和打断点分析画了以下的组件更新的流程图:
setState的定义在组件mountComponent的时候定义:
inst = new Component(publicProps, publicContext, ReactUpdateQueue);复制代码
function ReactComponent(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue;}ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); }};复制代码
所以setState的真正的定义在 ReactUpdateQueue.js
enqueueSetState: function (publicInstance, partialState) { var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState'); if (!internalInstance) { return; } var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); // 将state添加到对应的component的_pendingStateQueue数组中。 queue.push(partialState); enqueueUpdate(internalInstance);}enqueueCallback: function (publicInstance, callback, callerName) { var internalInstance = getInternalInstanceReadyForUpdate(publicInstance); if (!internalInstance) { return null; } // 将callback添加到对应的component的_pendingCallbacks数组中。 if (internalInstance._pendingCallbacks) { internalInstance._pendingCallbacks.push(callback); } else { internalInstance._pendingCallbacks = [callback]; } enqueueUpdate(internalInstance);}复制代码
两个方法最后都调用enqueueUpdate:
function enqueueUpdate(internalInstance) { ReactUpdates.enqueueUpdate(internalInstance);}function enqueueUpdate(component) { // 确认需要的事务是否注入了。 ensureInjected(); // batchingStrategy.isBatchingUpdates为false的时候, // 或者说当不处于批量更新的时候,用事务的方式批量的进行component的更新。 if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 当处于批量更新阶段时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中 dirtyComponents.push(component);}复制代码
这里需要注意enqueueUpdate中根据batchingStrategy.isBatchingUpdates分别进入不同的流程,当isBatchingUpdates为true的时候表示已经处于批量更新的过程中了,这时候会将所有的有改动的组件push到dirtyComponents中。当isBatchingUpdates为false的时候会执行更新操作,这里先认为当isBatchingUpdates为false的时候进行的操作是更新组件,实际上的过程是更复杂的,稍后马上解释具体的过程。这里我们先理解下React中的事务的概念,事务的概念根据源码中的注释就可以非常清楚的了解了:
* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+复制代码
简单来说当使用transaction.perform执行方法method的时候会按顺序先执行WRAPPER里面的initialize方法然后执行method最后再执行close方法。
React提供了基础的事务对象Transaction,不同的事务的区别就在于initialize和close方法的不同,这个可以通过定义getTransactionWrappers方法来传入WRAPPER数组,具体的用法看下源码就好了,不过实际使用中是不会要自己去定义事务的,当然要的话也阻止不了~。
回到enqueueUpdate,其调用的batchingStrategy.batchedUpdates方法在ReactDefaultBatchingStrategy 中定义了:
var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, batchedUpdates: function (callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; if (alreadyBatchingUpdates) { callback(a, b, c, d, e); } else { transaction.perform(callback, null, a, b, c, d, e); } }};复制代码
可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程。
对于enqueueUpdate的效果就是,当执行enqueueUpdate的时候如果isBatchingUpdates为true的话(已经处于批量执行操作),则不会进行更新操作,而是将改动的component添加到dirtyComponents数组中;如果isBatchingUpdates为false的话,会执行batchedUpdates将isBatchingUpdates置为true然后调用enqueueUpdate方法,这个时候会用事务的方式来执行enqueueUpdate。
根据流程图可以知道,事务ReactDefaultBatchingStrategyTransaction的initialize是foo没有任务操作,接着会执行method即:将改动的组件push到dirtyComponent中,最后执行close方法执行flushBatchedUpdate方法再把isBatchingUpdates重置为false。在flushBatchedUpdates方法中事务执行runBatchedUpdates方法将dirtyComponent中的component依次(先父组件在子组件的顺序)进行更新操作。这里具体的更新的过程看流程图就可以理解了,需要注意的是updateChildren方法这个方法是virtual DOM的Diff算法的核心代码,作用就是根据更新前后组件的不同进行有效的更新,具体的部分,之后单独的文章再介绍。
在更新的过程中需要注意的一个方法是_processPendingState方法:
_processPendingState: function (props, context) { var inst = this._instance; var queue = this._pendingStateQueue; var replace = this._pendingReplaceState; this._pendingReplaceState = false; this._pendingStateQueue = null; if (!queue) { return inst.state; } if (replace && queue.length === 1) { return queue[0]; } var nextState = _assign({}, replace ? queue[0] : inst.state); for (var i = replace ? 1 : 0; i < queue.length; i++) { var partial = queue[i]; _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial); } return nextState; }复制代码
可以看到当setState传入的是函数的时候,函数被调用的时候的传入的参数是merge了已经遍历的queue的state的nextState,如果传入的不是函数则直接merge state至nextState。这也解释了,为什么用回调函数的形式使用setState的时候可以解决state是按照顺序最新的state了。
从流程图可以看到在保证组件更新完毕后会将setState中传入的callback按照顺序依次push到事务的callback queue队列中,在事务结束的时候close方法中notifyAll就是执行这些callbacks,这样保证了回调函数是在组件完全更新完成后执行的,也就是setState的回调函数传入的state是更新后的state的原因。
在了解了以上的组件更新的流程后,可以看一个场景,栗子如下:
import React from 'react';import ReactDOM from 'react-dom';import App from './App';ReactDOM.render(, document.getElementById('root'));import React, { Component } from 'react';import Hello from './Hello';class App extends Component { constructor(props) { super(props); this.state = { appText: 'hello App', helloText: 'heiheihei' }; } handleAppClick = () => { console.log('App is clicked ~'); this.setState({ appText: 'App is clicked ~' }); } render() { const { appText, helloText } = this.state; console.log('render App'); return ( ); }}export default App;import React, { Component } from 'react';class Hello extends Component { constructor(props) { super(props); this.state = { text: 'hello Hello' }; } componentWillReceiveProps(nextProps) { this.setState({ text: nextProps.text + '~' }); } handleClick = () => { this.setState({ text: 'Hello is clicked ~' }); this.props.handleAppClick(); } render() { const { text } = this.state; console.log('render Hello'); return ({appText} ); }}export default Hello;复制代码{text}
点击
hello Hello复制代码
后组件的渲染如下,可以看到父组件到子组件按顺序更新了一次:
render Apprender Hello复制代码
而不是:
render Hellorender Apprender Hello复制代码
批量更新的时候组件的顺序由:
dirtyComponents.sort(mountOrderComparator);复制代码
处理的。
到这里你需要知道这个结果产生的原因在于不是只有setState的调用栈会改变isBatchingUpdates的值
回顾《React事件机制》的流程图可以知道事件的统一回调函数dispatchEvent调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理,也就是说点击事件的处理本身就是在一个大的事务中,在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中,在事务结束的时候按照上述的流程进行批量更新,然后将批量执行关闭结束事务。
事务的机制在这里也保证了React更新的效率,此外在更新组件的时候的virtual DOM的Diff算法也起到很大的作用,这个在后续的文章再介绍。
参考资料