摘要:作為聲明式的框架,接管了所有頁面更新相關的操作。是用于內部操作的實例,這里將它的初始化為空數組并插入一個新的。連續次后,期望的結果應該是。原因很簡單,因為次的時候,取到的都是在完后不會同步更新。
前言
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 (); } } export default App;"Welcom to React"
{ desc }
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 的策略。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/99298.html
摘要:在學習源碼的過程中,給我幫助最大的就是這個系列文章,于是決定基于這個系列文章談一下自己的理解。到此為止,首次渲染就完成啦總結從啟動到元素渲染到頁面,并不像看起來這么簡單,中間經歷了復雜的層級調用。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過...
摘要:前言是一個十分龐大的庫,由于要同時考慮和,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過程。在學習源碼的過程中,給我幫助最大的就是這個系列文章,于是決定基于這個系列文章談一下自己的理解。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常...
摘要:依賴注入和控制反轉,這兩個詞經常一起出現。一句話表述他們之間的關系依賴注入是控制反轉的一種實現方式。而兩者有大量的代碼都是可以共享的,這就是依賴注入的使用場景了。下一步就是創建具體的依賴內容,然后注入到需要的地方這里的等于這個對象。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級...
摘要:本篇開始介紹自定義組件是如何渲染的。組件將自定義組件命名為,結構如下經過編譯后,生成如下代碼構建頂層包裝組件跟普通元素渲染一樣,第一步先會執行創建為的。調用順序已在代碼中注釋。先看圖,這部分內容將在下回分解 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非...
摘要:本文將要講解的調用棧是下面這個樣子的平臺無關相關如果看源碼,我們會留意到很多相關的代碼,我們暫時先將其忽略,會在后續的文章中進行講解。現在我們來看一下各實例間的關系目前為止的調用棧平臺無關相關下一篇講解三總結本文講解了轉化為的過程。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 R...
閱讀 3234·2021-11-18 10:02
閱讀 1936·2021-09-22 10:54
閱讀 2989·2019-08-30 15:43
閱讀 2576·2019-08-30 13:22
閱讀 1575·2019-08-29 13:57
閱讀 1041·2019-08-29 13:27
閱讀 731·2019-08-26 14:05
閱讀 2512·2019-08-26 13:30