摘要:作為聲明式的框架,接管了所有頁面更新相關(guān)的操作。是用于內(nèi)部操作的實例,這里將它的初始化為空數(shù)組并插入一個新的。連續(xù)次后,期望的結(jié)果應(yīng)該是。原因很簡單,因為次的時候,取到的都是在完后不會同步更新。
前言
React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過程。在學(xué)習(xí) React 源碼的過程中,給我?guī)椭畲蟮木褪沁@個系列文章,于是決定基于這個系列文章談一下自己的理解。本文會大量用到原文中的例子,想體會原汁原味的感覺,推薦閱讀原文。
本系列文章基于 React 15.4.2 ,以下是本系列其它文章的傳送門:
React 源碼深度解讀(一):首次 DOM 元素渲染 - Part 1
React 源碼深度解讀(二):首次 DOM 元素渲染 - Part 2
React 源碼深度解讀(三):首次 DOM 元素渲染 - Part 3
React 源碼深度解讀(四):首次自定義組件渲染 - Part 1
React 源碼深度解讀(五):首次自定義組件渲染 - Part 2
React 源碼深度解讀(六):依賴注入
React 源碼深度解讀(七):事務(wù) - Part 1
React 源碼深度解讀(八):事務(wù) - Part 2
React 源碼深度解讀(九):單個元素更新
React 源碼深度解讀(十):Diff 算法詳解
正文
在前面的系列文章里,已經(jīng)對 React 的首次渲染和 事務(wù)(transaction)作了比較詳細(xì)的介紹,接下來終于講到它最核心的一個方法:setState。作為聲明式的框架,React 接管了所有頁面更新相關(guān)的操作。我們只需要定義好狀態(tài)和UI的映射關(guān)系,然后根據(jù)情況改變狀態(tài),它自然就能根據(jù)最新的狀態(tài)將頁面渲染出來,開發(fā)者不需要接觸底層的 DOM 操作。狀態(tài)的變更靠的就是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秒后觸發(fā)更新,改變對應(yīng)的文本與樣式。
下面我們來看下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也是通過依賴注入的方式,在組件實例化的時候注入進(jìn)來的。相關(guān)代碼如下:
// 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是用于內(nèi)部操作的 ReactCompositeComponent 實例,這里將它的_pendingStateQueue初始化為空數(shù)組并插入一個新的 state({desc:"end",color:"green"})。
結(jié)合之前 transaction 的內(nèi)容,調(diào)用關(guān)系如下:
三、Transaction 最終操作從上面的調(diào)用關(guān)系圖可以看出,transaction 最終會調(diào)用 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 ); ... } }
接著是調(diào)用 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; // 調(diào)用 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的過程。在實際開發(fā)過程中,如果需要基于之前的 state 值連續(xù)進(jìn)行運算的話,如果直接通過對象去 setState 往往得到的結(jié)果是錯誤的,看以下例子:
// 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});
假設(shè) count 的初始值是 0 。連續(xù) 3 次 setState 后,期望的結(jié)果應(yīng)該是 3 。但實際得到的值是 1 。原因很簡單,因為 3 次 setState 的時候,取到的this.state.count都是 0 (state 在 set 完后不會同步更新)。如果想得到期望的結(jié)果,代碼要改成下面的樣子:
function add(nextState, props, context) { return {count: nextState.count + 1}; } this.setState(add); this.setState(add); this.setState(add);
結(jié)合源碼來看,如果 setState 的參數(shù)類型是 function,每次合并后的nextState都會作為參數(shù)傳入,得到的結(jié)果自然是正確的了:
Object.assign( nextState, typeof partial === "function" ? partial.call(inst, nextState, props, context) : partial, );
_updateRenderedComponent會取出實例的 ReactDOMComponent,然后調(diào)用 render 方法,得出最新的 Virtual DOM 后啟動 Diff 的過程。
四、Diff
ReactReconciler.receiveComponent最終會調(diào)用 ReactDOMComponent 的 receiveComponent 方法,進(jìn)而再調(diào)用 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 個操作,一個是更新屬性,另一個是更新子孫結(jié)點。先來看看更新屬性的操作:
_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 屬性設(shè)置為空 for (styleName in lastStyle) { // 將舊的 style 屬性設(shè)置為空 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 個循環(huán),第一個循環(huán)刪除舊的屬性,第二個循環(huán)設(shè)置新的屬性。屬性的刪除靠的是DOMPropertyOperations.deleteValueForProperty或DOMPropertyOperations.deleteValueForAttribute,屬性的設(shè)置靠的是DOMPropertyOperations.setValueForProperty或DOMPropertyOperations.setValueForAttribute。以 setValueForAttribute 為例子,最終是調(diào)用 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 內(nèi)的所有屬性設(shè)置為空,然后再用新的 style 來填充。得出新的 style 后調(diào)用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); } } ... },
結(jié)合我們的例子,最終會調(diào)用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 。最終會調(diào)用 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; };
最終的調(diào)用關(guān)系見下圖:
五、總結(jié)
本文將 setState 的整個流程從頭到尾走了一遍,下一篇將會詳細(xì)的介紹 Diff 的策略。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/99298.html
摘要:在學(xué)習(xí)源碼的過程中,給我?guī)椭畲蟮木褪沁@個系列文章,于是決定基于這個系列文章談一下自己的理解。到此為止,首次渲染就完成啦總結(jié)從啟動到元素渲染到頁面,并不像看起來這么簡單,中間經(jīng)歷了復(fù)雜的層級調(diào)用。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過...
摘要:前言是一個十分龐大的庫,由于要同時考慮和,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過程。在學(xué)習(xí)源碼的過程中,給我?guī)椭畲蟮木褪沁@個系列文章,于是決定基于這個系列文章談一下自己的理解。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級非常...
摘要:依賴注入和控制反轉(zhuǎn),這兩個詞經(jīng)常一起出現(xiàn)。一句話表述他們之間的關(guān)系依賴注入是控制反轉(zhuǎn)的一種實現(xiàn)方式。而兩者有大量的代碼都是可以共享的,這就是依賴注入的使用場景了。下一步就是創(chuàng)建具體的依賴內(nèi)容,然后注入到需要的地方這里的等于這個對象。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級...
摘要:本篇開始介紹自定義組件是如何渲染的。組件將自定義組件命名為,結(jié)構(gòu)如下經(jīng)過編譯后,生成如下代碼構(gòu)建頂層包裝組件跟普通元素渲染一樣,第一步先會執(zhí)行創(chuàng)建為的。調(diào)用順序已在代碼中注釋。先看圖,這部分內(nèi)容將在下回分解 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非...
摘要:本文將要講解的調(diào)用棧是下面這個樣子的平臺無關(guān)相關(guān)如果看源碼,我們會留意到很多相關(guān)的代碼,我們暫時先將其忽略,會在后續(xù)的文章中進(jìn)行講解。現(xiàn)在我們來看一下各實例間的關(guān)系目前為止的調(diào)用棧平臺無關(guān)相關(guān)下一篇講解三總結(jié)本文講解了轉(zhuǎn)化為的過程。 歡迎關(guān)注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 R...
閱讀 3255·2021-11-18 10:02
閱讀 1960·2021-09-22 10:54
閱讀 2997·2019-08-30 15:43
閱讀 2588·2019-08-30 13:22
閱讀 1586·2019-08-29 13:57
閱讀 1055·2019-08-29 13:27
閱讀 746·2019-08-26 14:05
閱讀 2532·2019-08-26 13:30