React 把组件看做状态机(有限状态机), 使用state来控制本地状态, 使用props来传递状态. 前面咱们探讨了 React 如何映射状态到 UI 上(初始渲染), 那么接下来咱们谈谈 React 时如何同步状态到 UI 上的, 也就是:javascript
React 是如何更新组件的? java
React 是如何对比出页面变化最小的部分?react
这篇文章会为你解答这些问题.git
你已经了解了React (15-stable版本)内部的一些基本概念, 包括不一样类型的组件实例、mount过程、事务、批量更新的大体过程(尚未? 不用担忧, 为你准备好了从源码看组件初始渲染、接着从源码看组件初始渲染);github
准备一个demo, 调试源码, 以便更好理解;web
Keep calm and make a big deal !算法
ReactDefaultBatchingStrategy
事务perform以前(调用ReactUpdates.batchUpdates)到这个事务的最后一个close方法调用后结束;ReactDefaultBatchingStrategy
事务结束时调用runBatchedUpdates
批量更新全部组件;updateComponent
方法来决定本身的组件如何更新, 其中 ReactDOMComponent 会采用diff算法对比子元素中最小的变化, 再批量处理.这个更新过程像是一套流程, 不管你经过setState(或者replaceState)仍是新的props去更新一个组件, 都会起做用.编程
让咱们从这套更新流程的开始部分讲起...数组
首先, 开始一次batch的入口是在ReactDefaultBatchingStrategy
里, 调用里面的batchedUpdates
即可以开启一次batch:app
// 批处理策略 var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, batchedUpdates: function(callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 开启一次batch if (alreadyBatchingUpdates) { return callback(a, b, c, d, e); } else { // 启动事务, 将callback放进事务里执行 return transaction.perform(callback, null, a, b, c, d, e); } }, };
在 React 中, 调用batchedUpdates
有不少地方, 与更新流程相关的以下
// ReactMount.js ReactUpdates.batchedUpdates( batchedMountComponentIntoNode, // 负责初始渲染 componentInstance, container, shouldReuseMarkup, context, ); // ReactEventListener.js dispatchEvent: function(topLevelType, nativeEvent) { ... try { ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); // 处理事件 } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } },
第一种状况, React 在首次渲染组件的时候会调用batchedUpdates
, 而后开始渲染组件. 那么为何要在这个时候启动一次batch呢? 不是由于要批量插入, 由于插入过程是递归的, 而是由于组件在渲染的过程当中, 会依顺序调用各类生命周期函数, 开发者极可能在生命周期函数中(如componentWillMount
或者componentDidMount
)调用setState
. 所以, 开启一次batch就是要存储更新(放入dirtyComponents), 而后在事务结束时批量更新. 这样以来, 在初始渲染流程中, 任何setState
都会生效, 用户看到的始终是最新的状态.
第二种状况, 若是你在HTML元素上或者组件上绑定了事件, 那么你有可能在事件的监听函数中调用setState
, 所以, 一样为了存储更新(放入dirtyComponents), 须要启动批量更新策略. 在回调函数被调用以前, React事件系统中的dispatchEvent
函数负责事件的分发, 在dispatchEvent
中启动了事务, 开启了一次batch, 随后调用了回调函数. 这样一来, 在事件的监听函数中调用的setState
就会生效.
也就是说, 任何可能调用 setState 的地方, 在调用以前, React 都会启动批量更新策略以提早应对可能的setState
React 调用batchedUpdates
时会传进去一个函数, batchedUpdates
会启动ReactDefaultBatchingStrategyTransaction
事务, 这个函数就会被放在事务里执行:
// ReactDefaultBatchingStrategy.js var transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化事务 var ReactDefaultBatchingStrategy = { ... batchedUpdates: function(callback, a, b, c, d, e) { ... return transaction.perform(callback, null, a, b, c, d, e); // 将callback放进事务里执行 ... };
ReactDefaultBatchingStrategyTransaction
这个事务控制了批量策略的生命周期:
// ReactDefaultBatchingStrategy.js var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates), // 批量更新 }; var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function() { ReactDefaultBatchingStrategy.isBatchingUpdates = false; // 结束本次batch }, }; var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
不管你传进去的函数是什么, 不管这个函数后续会作什么, 都会在执行完后调用上面事务的close方法, 先调用flushBatchedUpdates
批量更新, 再结束本次batch.
// ReactBaseClasses.js : ReactComponent.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } }; // => ReactUpdateQueue.js: enqueueSetState: function(publicInstance, partialState) { // 根据 this.setState 中的 this 拿到内部实例, 也就是组件实例 var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState'); // 取得组件实例的_pendingStateQueue var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); // 将partial state存到_pendingStateQueue queue.push(partialState); // 调用enqueueUpdate enqueueUpdate(internalInstance); } // => ReactUpdate.js: function enqueueUpdate(component) { ensureInjected(); // 注入默认策略 // 若是没有开启batch(或当前batch已结束)就开启一次batch再执行, 这一般发生在异步回调中调用 setState // 的状况 if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 若是batch已经开启就存储更新 dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; } }
也就是说, 调用 setState 会首先拿到内部组件实例, 而后把要更新的partial state存到其_pendingStateQueue中, 而后标记当前组件为dirtyComponent
, 存到dirtyComponents
数组中. 而后就接着继续作下面的事情了, 并无当即更新, 这是由于接下来要执行的代码里有可能还会调用 setState, 所以只作存储处理.
首先, 一个事务在执行的时候(包括initialize、perform、close阶段), 任何一阶段都有可能调用一系列函数, 而且开启了另外一些事务. 那么只有等后续开启的事务执行完, 以前开启的事务才继续执行. 下图是咱们刚才所说的第一种状况, 在初始渲染组件期间 setState 后, React 启动的各类事务和执行的顺序:
从图中能够看到, 批量更新是在ReactDefaultBatchingStrategyTransaction
事务的close阶段, 在flushBatchedUpdates
函数中启动了ReactUpdatesFlushTransaction
事务负责批量更新.
咱们接着看flushBatchedUpdates
函数, 在ReactUpdates.js中
var flushBatchedUpdates = function () { // 启动批量更新事务 while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); transaction.perform(runBatchedUpdates, null, transaction); ReactUpdatesFlushTransaction.release(transaction); } // 批量处理callback if (asapEnqueued) { asapEnqueued = false; var queue = asapCallbackQueue; asapCallbackQueue = CallbackQueue.getPooled(); queue.notifyAll(); CallbackQueue.release(queue); } } };
flushBatchedUpdates
启动了一个更新事务, 这个事务执行了runBatchedUpdates
进行批量更新:
// ReactUpdates.js function runBatchedUpdates(transaction) { var len = transaction.dirtyComponentsLength; // 排序保证父组件优先于子组件更新 dirtyComponents.sort(mountOrderComparator); // 表明批量更新的次数, 保证每一个组件只更新一次 updateBatchNumber++; // 遍历 dirtyComponents for (var i = 0; i < len; i++) { var component = dirtyComponents[i]; var callbacks = component._pendingCallbacks; component._pendingCallbacks = null; ... // 执行更新 ReactReconciler.performUpdateIfNecessary( component, transaction.reconcileTransaction, updateBatchNumber, ); ... // 存储 callback以便后续按顺序调用 if (callbacks) { for (var j = 0; j < callbacks.length; j++) { transaction.callbackQueue.enqueue( callbacks[j], component.getPublicInstance(), ); } } } }
前面 setState 后将组件推入了dirtyComponents
, 如今就是要遍历dirtyComponents
数组进行更新了.
ReactReconciler
会调用组件实例的performUpdateIfNecessary
. 若是接收了props, 就会调用此组件的receiveComponent
, 再在里面调用updateComponent
更新组件; 若是没有接受props, 可是有新的要更新的状态(_pendingStateQueue不为空)就会直接调用updateComponent
来更新:
// ReactCompositeComponent.js performUpdateIfNecessary: function (transaction) { if (this._pendingElement != null) { ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context); } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) { this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); } else { this._updateBatchNumber = null; } }
接下里就是重头戏updateComponent
了, 它决定了组件若是更新本身和它的后代们. 须要特别注意的是, React 内部三种不一样的组件类型, 每种组件都有本身的updateComponent
, 有不一样的行为.
对于 ReactCompositeComponent (矢量图):
updateComponent
所作的事情 :
对于 ReactDOMComponent:
updateComponent
所作的事情 :
updateChildren
, 对比先后变化、标记变化类型、存到updates中(diff算法主要部分);对于 ReactDOMTextComponent :
上面只是每一个组件本身更新的过程, 那么 React 是如何一次性更新全部组件的 ? 答案是递归.
观察 ReactCompositeComponent 和 ReactDOMComponent 的更新流程, 咱们发现 React 每次走到一个组件更新过程的最后部分, 都会有一个判断 : 若是 nextELement 和 prevElement key 和 type 相等, 就会调用receiveComponent
. receiveComponent
和updateComponent
同样, 每种组件都有一个, 做用就至关于updateComponent 接受了新 props 的版本. 而这里调用的就是子元素的receiveComponent
, 进而进行子元素的更新, 因而就造成了递归更新、递归diff. 所以, 整个流程就像这样(矢量图) :
这种更新完一级、diff完一级再进入下一级的过程保证 React 只遍历一次组件树就能完成更新, 但代价就是只要先后 render 出元素的 type 和 key 有一个不一样就删除重造, 所以, React 建议页面要尽可能保持稳定的结构.
你可能会说 React 用 virtual DOM 表示了页面结构, 每次更新, React 都会re-render出新的 virtual DOM, 再经过 diff 算法对比出先后变化, 最后批量更新. 没错, 很好, 这就是大体过程, 但这里存在着一些隐藏的深层问题值得探讨 :
class C extends React.Component { render () { return ( <div className='container'> "dscsdcsd" <i onClick={(e) => console.log(e)}>{this.state.val}</i> <Children val={this.state.val}/> </div> ) } } // virtual DOM(React element) { $$typeof: Symbol(react.element) key: null props: { // props 表明元素上的全部属性, 有children属性, 描述子组件, 一样是元素 children: [ ""dscsdcsd"", {$$typeof: Symbol(react.element), type: "i", key: null, ref: null, props: {…}, …}, {$$typeof: Symbol(react.element), type: class Children, props: {…}, …} ] className: 'container' } ref: null type: "div" _owner: ReactCompositeComponentWrapper {...} // class C 实例化后的对象 _store: {validated: false} _self: null _source: null }
每一个标签, 不管是DOM元素仍是自定义组件, 都会有 key、type、props、ref 等属性.
也就是说, 若是元素惟一标识符或者类别或者属性有变化, 那么它们re-render后对应的 key、type 和props里面的属性也会改变, 先后一对比便可找出变化. 综上来看, React 这么表示页面结构确实可以反映先后全部变化.
React diff 每次只对同一层级的节点进行比对 :
上图的数字表示遍历更新的次序.
从父节点开始, 每一层 diff 包括两个地方
element diff—— 先后 render 出来的 element 的对比, 这个对比是为了找出先后节点是否是同一节点, 会对比先后render出来的元素它们的 key 和 type. element diff 包括两个地方, 组件顶层DOM元素对比和子元素的对比:
组件顶层DOM元素对比 :
// ReactCompositeComponent.js/updateComponent => _updateRenderedComponent _updateRenderedComponent: function(transaction, context) { // re-render 出element var nextRenderedElement = this._renderValidatedComponent(); // 对比先后变化 if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { // 若是 key && type 没变进行下一级更新 ReactReconciler.receiveComponent(...); } else { // 若是变了移除重造 ReactReconciler.unmountComponent(prevComponentInstance, false); ... var child = this._instantiateReactComponent(...); var nextMarkup = ReactReconciler.mountComponent(...); this._replaceNodeWithMarkup(...); } }
子元素的对比:
// ReactChildReconciler.js updateChildren: function(...) { ... for (name in nextChildren) { // 遍历 re-render 出的elements ... if ( prevChild != null && shouldUpdateReactComponent(prevElement, nextElement) ) { // 若是key && type 没变进行下一级更新 ReactReconciler.receiveComponent(...); nextChildren[name] = prevChild; // 更新完放入 nextChildren, 注意放入的是组件实例 } else { // 若是变了则移除重建 if (prevChild) { removedNodes[name] = ReactReconciler.getHostNode(prevChild); ReactReconciler.unmountComponent(prevChild, false); } var nextChildInstance = instantiateReactComponent(nextElement, true); nextChildren[name] = nextChildInstance; var nextChildMountImage = ReactReconciler.mountComponent(...); mountImages.push(nextChildMountImage); } } // 再除掉 prevChildren 里有, nextChildren 里没有的组件 for (name in prevChildren) { if ( prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name)) ) { prevChild = prevChildren[name]; removedNodes[name] = ReactReconciler.getHostNode(prevChild); ReactReconciler.unmountComponent(prevChild, false); } } },
shouldComponentUpdate 函数:
function shouldUpdateReactComponent(prevElement, nextElement) { var prevEmpty = prevElement === null || prevElement === false; var nextEmpty = nextElement === null || nextElement === false; if (prevEmpty || nextEmpty) { return prevEmpty === nextEmpty; } var prevType = typeof prevElement; var nextType = typeof nextElement; // 若是先后变化都是字符串、数字类型的则容许更新 if (prevType === 'string' || prevType === 'number') { return nextType === 'string' || nextType === 'number'; } else { // 不然检查 type && key return ( nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key ); } }
element diff 检测 type && key 都没变时会进入下一级更新, 若是变化则直接移除重造新元素, 而后遍历同级的下一个.
subtree diff ——组件顶层DOM元素包裹的全部子元素(也就是props.children里的元素)与以前版本的对比, 这个对比是为了找出同级全部子节点的变化, 包括移除、新建、同级范围的移动;
// ReactMultiChild.js _updateChildren: function(...) { var prevChildren = this._renderedChildren; var removedNodes = {}; var mountImages = []; // 拿到更新后子组件实例 var nextChildren = this._reconcilerUpdateChildren(); ... // 遍历子组件实例 for (name in nextChildren) { ... var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; // 由于子组件的更新是在原组件实例上更改的, 所以与以前的组件做引用比较便可判断 if (prevChild === nextChild) { // 发生了移动 updates = enqueue( updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex), ); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { ... // 有新的组件 updates = enqueue( updates, this._mountChildAtIndex( nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context, ), ); nextMountIndex++; } nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } // Remove children that are no longer present. for (name in removedNodes) { // removedNodes 记录了全部的移除节点 if (removedNodes.hasOwnProperty(name)) { updates = enqueue( updates, this._unmountChild(prevChildren[name], removedNodes[name]), ); } } if (updates) { processQueue(this, updates); // 批量处理 } this._renderedChildren = nextChildren; },
React 会将同一层级的变化标记, 如 MOVE_EXISTING、REMOVE_NODE、TEXT_CONTENT、INSERT_MARKUP 等, 统一放到 updates 数组中而后批量处理.
React 是一个激动人心的库, 它给咱们带来了史无前例的开发体验, 但当咱们沉浸在使用 React 快速实现需求的喜悦中时, 有必要去探究两个问题 : Why and How?
为何 React 会如此流行, 缘由是什么? 组件化、快速、足够简单、all in js、容易扩展、生态丰富、社区强大...
React 反映了哪些思想/理念/思路 ? 状态机、webComponents、virtual DOM、virtual stack、异步渲染、多端渲染、单向数据流、反应式更新、函数式编程...
React 这些理念/思路受什么启发 ? 怎么想到的 ? 又怎么实现的? ...