React 源码深度解读(九):单个元素更新
前言
React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过程。在学习 React 源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。本文会大量用到原文中的例子,想体会原汁原味的感觉,推荐阅读原文。
本系列文章基于 React 15.4.2 ,以下是本系列其它文章的传送门:
React 源码深度解读(一):首次 DOM 元素渲染 - Part 1
React 源码深度解读(二):首次 DOM 元素渲染 - Part 2
React 源码深度解读(三):首次 DOM 元素渲染 - Part 3
React 源码深度解读(四):首次自定义组件渲染 - Part 1
React 源码深度解读(五):首次自定义组件渲染 - Part 2
React 源码深度解读(六):依赖注入
React 源码深度解读(七):事务 - Part 1
React 源码深度解读(八):事务 - Part 2
React 源码深度解读(九):单个元素更新
React 源码深度解读(十):Diff 算法详解
正文
在前面的系列文章里,已经对 React 的首次渲染和 事务(transaction)作了比较详细的介绍,接下来终于讲到它最核心的一个方法:setState
。作为声明式的框架,React 接管了所有页面更新相关的操作。我们只需要定义好状态和UI的映射关系,然后根据情况改变状态,它自然就能根据最新的状态将页面渲染出来,开发者不需要接触底层的 DOM 操作。状态的变更靠的就是setState
这一方法,下面我们来揭开它神秘的面纱。
二、setState
介绍开始前,先更新一下例子:
class App extends Component { constructor(props) { super(props); this.state = { desc: 'start', color: 'blue' }; this.timer = setTimeout( () => this.tick(), 5000 ); } tick() { this.setState({ desc: 'end', color: 'green' }); } render() { const {desc, color} = this.state; return ( <div className="App"> <div className="App-header"> <img src="main.jpg" className="App-logo" alt="logo" /> <h1> "Welcom to React" </h1> </div> <p className="App-intro" style={{color: color}}> { desc } </p> </div> ); } } export default App;
state 保存了一个文本信息和颜色,5秒后触发更新,改变对应的文本与样式。
下面我们来看下setState
的源码:
function ReactComponent(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; } ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } };
这里的updater
也是通过依赖注入的方式,在组件实例化的时候注入进来的。相关代码如下:
// ReactCompositeComponent.js mountComponent: function ( transaction, hostParent, hostContainerInfo, context ) { ... // 这里的 transaction 是 ReactReconcileTransaction var updateQueue = transaction.getUpdateQueue(); var doConstruct = shouldConstruct(Component); // 在这个地方将 updater 注入 var inst = this._constructComponent( doConstruct, publicProps, publicContext, updateQueue ); ... } // ReactReconcileTransaction.js var ReactUpdateQueue = require('ReactUpdateQueue'); getUpdateQueue: function () { return ReactUpdateQueue; } // ReactUpdateQuene.js var ReactUpdates = require('ReactUpdates'); enqueueSetState: function (publicInstance, partialState) { ... var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState' ); if (!internalInstance) { return; } var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); }, function enqueueUpdate(internalInstance) { ReactUpdates.enqueueUpdate(internalInstance); }
this.updater.enqueueSetState
最终落地的代码是ReactUpdates.enqueueUpdate
。internalInstance
是用于内部操作的 ReactCompositeComponent 实例,这里将它的_pendingStateQueue
初始化为空数组并插入一个新的 state({desc:'end',color:'green'})。
结合之前 transaction 的内容,调用关系如下:
三、Transaction 最终操作
从上面的调用关系图可以看出,transaction 最终会调用 ReactUpdates 的 runBatchedUpdates 方法。
function runBatchedUpdates(transaction) { var len = transaction.dirtyComponentsLength; ... for (var i = 0; i < len; i++) { var component = dirtyComponents[i]; ... ReactReconciler.performUpdateIfNecessary( component, transaction.reconcileTransaction, updateBatchNumber ); ... } }
接着是调用 ReactReconciler 的 performUpdateIfNecessary,然后到 ReactCompositeComponent 的一系列方法:
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: function ( transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext ) { var inst = this._instance; ... var nextState = this._processPendingState(nextProps, nextContext); ... this._performComponentUpdate( nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext ); }, _processPendingState: function (props, context) { var inst = this._instance; var queue = this._pendingStateQueue; var replace = this._pendingReplaceState; ... var nextState = Object.assign({}, replace ? queue[0] : inst.state); for (var i = replace ? 1 : 0; i < queue.length; i++) { var partial = queue[i]; Object.assign( nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial ); } return nextState; }, _performComponentUpdate: function ( nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext ) { var inst = this._instance; ... this._updateRenderedComponent(transaction, unmaskedContext); ... }, /** * Call the component's `render` method and update the DOM accordingly. */ _updateRenderedComponent: function (transaction, context) { // ReactDOMComponent var prevComponentInstance = this._renderedComponent; // 上一次的Virtual DOM(ReactElement) var prevRenderedElement = prevComponentInstance._currentElement; // 调用 render 获取最新的Virtual DOM(ReactElement) var nextRenderedElement = this._renderValidatedComponent(); ... if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { ReactReconciler.receiveComponent( prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context) ); } ... },
这里最重要的方法分别为_processPendingState
和_updateRenderedComponent
。_processPendingState
是真正更新 state 的地方,可以看到它其实就是一个Object.assign
的过程。在实际开发过程中,如果需要基于之前的 state 值连续进行运算的话,如果直接通过对象去 setState 往往得到的结果是错误的,看以下例子:
// this.state.count = 0 this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1});
假设 count 的初始值是 0 。连续 3 次 setState 后,期望的结果应该是 3 。但实际得到的值是 1 。原因很简单,因为 3 次 setState 的时候,取到的this.state.count
都是 0 (state 在 set 完后不会同步更新)。如果想得到期望的结果,代码要改成下面的样子:
function add(nextState, props, context) { return {count: nextState.count + 1}; } this.setState(add); this.setState(add); this.setState(add);
结合源码来看,如果 setState 的参数类型是 function,每次合并后的nextState
都会作为参数传入,得到的结果自然是正确的了:
Object.assign( nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial, );
_updateRenderedComponent
会取出实例的 ReactDOMComponent,然后调用 render 方法,得出最新的 Virtual DOM 后启动 Diff 的过程。
四、Diff
ReactReconciler.receiveComponent
最终会调用 ReactDOMComponent 的 receiveComponent 方法,进而再调用 updateComponent 方法:
updateComponent: function (transaction, prevElement, nextElement, context) { var lastProps = prevElement.props; var nextProps = this._currentElement.props; ... this._updateDOMProperties(lastProps, nextProps, transaction); this._updateDOMChildren( lastProps, nextProps, transaction, context ); ... },
这个方法只有 2 个操作,一个是更新属性,另一个是更新子孙结点。先来看看更新属性的操作:
_updateDOMProperties: function (lastProps, nextProps, transaction) { var propKey; var styleName; var styleUpdates; // 删除旧的属性 for (propKey in lastProps) { // 筛选出后来没有但之前有的属性 if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) { continue; } if (propKey === STYLE) { var lastStyle = this._previousStyleCopy; // 初始化 styleUpdates,之前所有的 style 属性设置为空 for (styleName in lastStyle) { // 将旧的 style 属性设置为空 if (lastStyle.hasOwnProperty(styleName)) { styleUpdates = styleUpdates || {}; styleUpdates[styleName] = ''; } } this._previousStyleCopy = null; } ... } else if ( DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) { DOMPropertyOperations.deleteValueForProperty(getNode( this), propKey); } } for (propKey in nextProps) { var nextProp = nextProps[propKey]; var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined; // 值相等则跳过 if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) { continue; } if (propKey === STYLE) { if (nextProp) { nextProp = this._previousStyleCopy = Object.assign({}, nextProp); } else { this._previousStyleCopy = null; } if (lastProp) { // Unset styles on `lastProp` but not on `nextProp`. for (styleName in lastProp) { if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) { styleUpdates = styleUpdates || {}; styleUpdates[styleName] = ''; } } // Update styles that changed since `lastProp`. for (styleName in nextProp) { if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName] ) { styleUpdates = styleUpdates || {}; styleUpdates[styleName] = nextProp[ styleName]; } } } else { // Relies on `updateStylesByID` not mutating `styleUpdates`. styleUpdates = nextProp; } } ... } else if ( DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) { var node = getNode(this); // If we're updating to null or undefined, we should remove the property // from the DOM node instead of inadvertently setting to a string. This // brings us in line with the same behavior we have on initial render. if (nextProp != null) { DOMPropertyOperations.setValueForProperty(node, propKey, nextProp); } else { DOMPropertyOperations.deleteValueForProperty(node, propKey); } } } if (styleUpdates) { CSSPropertyOperations.setValueForStyles( getNode(this), styleUpdates, this ); } },
这里主要有 2 个循环,第一个循环删除旧的属性,第二个循环设置新的属性。属性的删除靠的是DOMPropertyOperations.deleteValueForProperty
或DOMPropertyOperations.deleteValueForAttribute
,属性的设置靠的是DOMPropertyOperations.setValueForProperty
或DOMPropertyOperations.setValueForAttribute
。以 setValueForAttribute 为例子,最终是调用 DOM 的 api :
setValueForAttribute: function (node, name, value) { if (!isAttributeNameSafe(name)) { return; } if (value == null) { node.removeAttribute(name); } else { node.setAttribute(name, '' + value); } },
针对 style 属性,由styleUpdates
这个对象来收集变化的信息。它会先将旧的 style 内的所有属性设置为空,然后再用新的 style 来填充。得出新的 style 后调用CSSPropertyOperations.setValueForStyles
来更新:
setValueForStyles: function (node, styles, component) { var style = node.style; for (var styleName in styles) { ... if (styleValue) { style[styleName] = styleValue; } else { ... style[styleName] = ''; } } },
接下来看 updateDOMChildren 。
updateDOMChildren: function (lastProps, nextProps, transaction, context) { var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null; var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null; ... if (nextContent != null) { if (lastContent !== nextContent) { this.updateTextContent('' + nextContent); } } ... },
结合我们的例子,最终会调用updateTextContent
。这个方法来自 ReactMultiChild ,可以简单理解为 ReactDOMComponent 继承了 ReactMultiChild 。
updateTxtContent: function (nextContent) { var prevChildren = this._renderedChildren; // Remove any rendered children. ReactChildReconciler.unmountChildren(prevChildren, false); for (var name in prevChildren) { if (prevChildren.hasOwnProperty(name)) { invariant(false, 'updateTextContent called on non-empty component.' ); } } // Set new text content. var updates = [makeTextContent(nextContent)]; processQueue(this, updates); }, function makeTextContent(textContent) { // NOTE: Null values reduce hidden classes. return { type: 'TEXT_CONTENT', content: textContent, fromIndex: null, fromNode: null, toIndex: null, afterNode: null, }; }, function processQueue(inst, updateQueue) { ReactComponentEnvironment.processChildrenUpdates( inst, updateQueue, ); }
这里的 ReactComponentEnvironment 通过依赖注入的方式注入后,实际上是 ReactComponentBrowserEnvironment 。最终会调用 DOMChildrenOperations 的 processUpdates:
processUpdates: function (parentNode, updates) { for (var k = 0; k < updates.length; k++) { var update = updates[k]; switch (update.type) { ... case 'TEXT_CONTENT': setTextContent( parentNode, update.content ); if (__DEV__) { ReactInstrumentation.debugTool.onHostOperation({ instanceID: parentNodeDebugID, type: 'replace text', payload: update.content.toString(), }); } break; ... } } }, // setTextContent.js var setTextContent = function(node, text) { if (text) { var firstChild = node.firstChild; if (firstChild && firstChild === node.lastChild && firstChild.nodeType === 3) { firstChild.nodeValue = text; return; } } node.textContent = text; };
最终的调用关系见下图:
五、总结
本文将 setState 的整个流程从头到尾走了一遍,下一篇将会详细的介绍 Diff 的策略。