从源码全面剖析 React 组件更新机制
React 把组件看作状态机(有限状态机),使用state来控制本地状态,使用props来传递状态. 前面我们探讨了 React 如何映射状态到 UI 上(初始渲染),那么接下来我们谈谈 React 时如何同步状态到 UI 上的,也就是: React 是如何更新组件的? React 是如何对比出页面变化最小的部分? 这篇文章会为你解答这些问题. 在这之前你已经了解了React (15-stable版本)内部的一些基本概念,包括不同类型的组件实例、mount过程、事务、批量更新的大致过程(还没有? 不用担心,为你准备好了从源码看组件初始渲染、接着从源码看组件初始渲染); 准备一个demo,调试源码,以便更好理解; Keep calm and make a big deal ! React 是如何更新组件的?TL;DR
这个更新过程像是一套流程,无论你通过setState(或者replaceState)还是新的props去更新一个组件,都会起作用. 那么具体是什么?让我们从这套更新流程的开始部分讲起... 调用 setState 之前首先,开始一次batch的入口是在 // 批处理策略 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,e); } else { // 启动事务,将callback放进事务里执行 return transaction.perform(callback,null,e); } },}; 在 React 中,调用 // 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 在首次渲染组件的时候会调用 第二种情况,如果你在HTML元素上或者组件上绑定了事件,那么你有可能在事件的监听函数中调用 也就是说,任何可能调用 setState 的地方,在调用之前,React 都会启动批量更新策略以提前应对可能的setState 那么调用 batchedUpdates 后发生了什么?React 调用 // ReactDefaultBatchingStrategy.js var transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化事务 var ReactDefaultBatchingStrategy = { ... batchedUpdates: function(callback,e) { ... return transaction.perform(callback,e); // 将callback放进事务里执行 ... };
// 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方法,先调用 调用 setState 后发生了什么// 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中,然后标记当前组件为 什么时候批量更新?首先,一个事务在执行的时候(包括initialize、perform、close阶段),任何一阶段都有可能调用一系列函数,并且开启了另一些事务. 那么只有等后续开启的事务执行完,之前开启的事务才继续执行. 下图是我们刚才所说的第一种情况,在初始渲染组件期间 setState 后,React 启动的各种事务和执行的顺序:
从图中可以看到,批量更新是在 怎么批量更新的?开启批量更新事务、批量处理callback我们接着看 var flushBatchedUpdates = function () { // 启动批量更新事务 while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); transaction.perform(runBatchedUpdates,transaction); ReactUpdatesFlushTransaction.release(transaction); } // 批量处理callback if (asapEnqueued) { asapEnqueued = false; var queue = asapCallbackQueue; asapCallbackQueue = CallbackQueue.getPooled(); queue.notifyAll(); CallbackQueue.release(queue); } } }; 遍历dirtyComponents
// 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 后将组件推入了 根据不同情况执行更新
// 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._context,this._context); } else { this._updateBatchNumber = null; } } 调用组件实例的updateComponent接下里就是重头戏 对于 ReactCompositeComponent (矢量图):
对于 ReactDOMComponent:
对于 ReactDOMTextComponent :
上面只是每个组件自己更新的过程,那么 React 是如何一次性更新所有组件的 ? 答案是递归. 递归调用组件的updateComponent观察 ReactCompositeComponent 和 ReactDOMComponent 的更新流程,我们发现 React 每次走到一个组件更新过程的最后部分,都会有一个判断 : 如果 nextELement 和 prevElement key 和 type 相等,就会调用
这种更新完一级、diff完一级再进入下一级的过程保证 React 只遍历一次组件树就能完成更新,但代价就是只要前后 render 出元素的 type 和 key 有一个不同就删除重造,React 建议页面要尽量保持稳定的结构. React 是如何对比出页面变化最小的部分?你可能会说 React 用 virtual DOM 表示了页面结构,每次更新,React 都会re-render出新的 virtual DOM,再通过 diff 算法对比出前后变化,最后批量更新. 没错,很好,这就是大致过程,但这里存在着一些隐藏的深层问题值得探讨 :
React 如何表示页面结构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: {…},…},type: class Children,…} ] 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 的?React diff 每次只对同一层级的节点进行比对 :
上图的数字表示遍历更新的次序. 从父节点开始,每一层 diff 包括两个地方
And that‘s it !React 是一个激动人心的库,它给我们带来了前所未有的开发体验,但当我们沉浸在使用 React 快速实现需求的喜悦中时,有必要去探究两个问题 : Why and How? 为什么 React 会如此流行,原因是什么? 组件化、快速、足够简单、all in js、容易扩展、生态丰富、社区强大... React 反映了哪些思想/理念/思路 ? 状态机、webComponents、virtual DOM、virtual stack、异步渲染、多端渲染、单向数据流、反应式更新、函数式编程... React 这些理念/思路受什么启发 ? 怎么想到的 ? 又怎么实现的? ... (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |