閱讀源碼成了今年的學習目標之一,在選擇 Vue 和 React 之間,我想先閱讀 React 。 在考慮到讀哪一個版本的時候,我想先接觸到源碼早期的思想可能會更輕鬆一些,最終我選擇閱讀
0.3-stable
。 那麼接下來,我將從幾個方面來解讀這個版本的源碼。javascript
引起組件更新的方法就是 this.setState
,按照註釋代碼看來 this.setState
是不可變的,則 this._pendingState
是用來存放掛起的 state
,他不會直接更新到 this.state
,讓咱們來看到代碼:html
// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
setState: function(partialState) {
// 若是「掛起狀態」存在,則與之合併,不然與現有狀態合併。
this.replaceState(merge(this._pendingState || this.state, partialState));
},
replaceState: function(completeState) {
var compositeLifeCycleState = this._compositeLifeCycleState;
// 生命週期校驗
invariant(
this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
'replaceState(...): Can only update a mounted (or mounting) component.'
);
invariant(
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
'replaceState(...): Cannot update while unmounting component or during ' +
'an existing state transition (such as within `render`).'
);
// 將合併完的狀態給掛起狀態,若不知足下面更新條件,則只存入掛起狀態結束此函數
this._pendingState = completeState;
// 若是咱們處於安裝或接收道具的中間,請不要觸發狀態轉換,由於這二者都已經這樣作了。
// 若複合組件生命週期不在掛載中和更新 props 時,咱們會操做更新方法
if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
// 變動複合組件生命週期爲更新 state
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
// 準備更新 state ,並釋放掛起狀態
var nextState = this._pendingState;
this._pendingState = null;
// 進入 React 調度事務,加入 _receivePropsAndState 方法
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
transaction.perform(
this._receivePropsAndState,
this,
this.props,
nextState,
transaction
);
ReactComponent.ReactReconcileTransaction.release(transaction);
// 調度事務完成後置空複合組件生命週期
this._compositeLifeCycleState = null;
}
},
};
複製代碼
那麼到此爲止,你能夠知道 this.setState
並不是事實更新 this.state
的,好比咱們看到在 componentWillMount
中去使用 this.setState
並不會立刻更新到 this.state
,那麼咱們繼續閱讀後面代碼:java
// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
_receivePropsAndState: function(nextProps, nextState, transaction) {
// shouldComponentUpdate 方法不存在或返回 true
if (!this.shouldComponentUpdate ||
this.shouldComponentUpdate(nextProps, nextState)) {
// Will set `this.props` and `this.state`.
this._performComponentUpdate(nextProps, nextState, transaction);
} else {
// 若是肯定某個組件不該該更新,咱們仍然須要設置props和state。
// shouldComponentUpdate 返回 false 的狀況
this.props = nextProps;
this.state = nextState;
}
},
_performComponentUpdate: function(nextProps, nextState, transaction) {
// 存入舊的 props 和 state
// 用於傳入 componentDidUpdate
var prevProps = this.props;
var prevState = this.state;
if (this.componentWillUpdate) {
this.componentWillUpdate(nextProps, nextState, transaction);
}
// 更新 props 和 state
this.props = nextProps;
this.state = nextState;
// 更新組件
this.updateComponent(transaction);
if (this.componentDidUpdate) {
transaction.getReactOnDOMReady().enqueue(
this,
this.componentDidUpdate.bind(this, prevProps, prevState)
);
}
},
updateComponent: function(transaction) {
// 這裏的更新比較硬核
// 先把已渲染的舊的組件賦值至 currentComponent
var currentComponent = this._renderedComponent;
// 直接渲染新的組件(是否是很硬核)
var nextComponent = this._renderValidatedComponent();
// 若是是一樣的組件則進入此判斷
// 經過 constructor 來判斷是否爲同一個
// 好比:
// React.DOM.a().constructor !== React.DOM.p().constructor
// React.DOM.a().constructor === React.DOM.a().constructor
// 或
// React.createClass({ render: () => null }).constructor ===
// React.createClass({ render: () => null }).constructor
if (currentComponent.constructor === nextComponent.constructor) {
// 若新的組件 props 下 isStatic 爲真則不更新
// 知道這一個能夠對部分組件進行手動優化,以避免沒必要要的計算
if (!nextComponent.props.isStatic) {
// 這裏會調用對應的方法
// ReactCompositeComponent.receiveProps
// ReactNativeComponent.receiveProps
// ReactTextComponent.receiveProps
// 除了 ReactTextComponent 都會調用 ReactComponent.Mixin.receiveProps 來更新 ref 相關
// 這個咱們稍後來解讀
currentComponent.receiveProps(nextComponent.props, transaction);
}
} else {
// These two IDs are actually the same! But nothing should rely on that.
// 舊的 _rootNodeID 和新的 _rootNodeID
var thisID = this._rootNodeID;
var currentComponentID = currentComponent._rootNodeID;
// 卸載舊組件
currentComponent.unmountComponent();
// 掛載新組件(也挺硬核的)
var nextMarkup = nextComponent.mountComponent(thisID, transaction);
// 在新 _rootNodeID 下更新 markup 標記
ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID(
currentComponentID,
nextMarkup
);
// 賦值新的組件
this._renderedComponent = nextComponent;
}
},
};
複製代碼
上面代碼看來,一個是不替換組件的狀況下更新組件,另外一個則是直接更新 markup
標記。咱們按照順序一個個看過來吧,先看到 ReactCompositeComponent.receiveProps
:node
// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
receiveProps: function(nextProps, transaction) {
// 校驗參數
if (this.constructor.propDeclarations) {
this._assertValidProps(nextProps);
}
// 更新 ref
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
// 更新複合組件生命週期爲更新 props
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
// 執行鉤子函數,在這個函數內執行 this.setState 是不會當即更新 this.state 的
if (this.componentWillReceiveProps) {
this.componentWillReceiveProps(nextProps, transaction);
}
// 進入複合組件生命週期更新 state
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
// When receiving props, calls to `setState` by `componentWillReceiveProps`
// will set `this._pendingState` without triggering a re-render.
// 若是上面執行過 componentWillReceiveProps ,而且裏面操做了 this.setState
// 那麼 this._pendingState 會有值,而且是與 this.state 合併過的
var nextState = this._pendingState || this.state;
// 釋放 this._pendingState
this._pendingState = null;
// 執行的是 currentComponent._receivePropsAndState 方法
// 可是這個 currentComponent 必定是 ReactCompositeComponent
this._receivePropsAndState(nextProps, nextState, transaction);
// 置空複合組件生命週期
this._compositeLifeCycleState = null;
},
};
複製代碼
再是咱們來看看 ReactNativeComponent.receiveProps
:app
// core/ReactNativeComponent.js
ReactNativeComponent.Mixin = {
receiveProps: function(nextProps, transaction) {
// 平常校驗
invariant(
this._rootNodeID,
'Trying to control a native dom element without a backing id'
);
assertValidProps(nextProps);
// 平常更新 ref
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
// 重點來了,更新 DOM 屬性
this._updateDOMProperties(nextProps);
// 更新 DOM 子節點
this._updateDOMChildren(nextProps, transaction);
// 都執行完後更新 props
this.props = nextProps;
},
_updateDOMProperties: function(nextProps) {
// 這裏開始解讀更新 DOM 屬性
// 保存舊 props
var lastProps = this.props;
// 遍歷新 props
for (var propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = lastProps[propKey];
// 以新 props 鍵爲準取對應的值
// 若 2 個值相等則跳過
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
continue;
}
// CSS 樣式
if (propKey === STYLE) {
if (nextProp) {
nextProp = nextProps.style = merge(nextProp);
}
var styleUpdates;
// 遍歷 nextProp
for (var styleName in nextProp) {
if (!nextProp.hasOwnProperty(styleName)) {
continue;
}
// 舊的 styleName 與新的 styleName 值不一樣時
// 將新的值加入 styleUpdates
if (!lastProp || lastProp[styleName] !== nextProp[styleName]) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
// 操做更新 CSS 樣式
if (styleUpdates) {
// ReactComponent.DOMIDOperations => ReactDOMIDOperations
// 他會經過 ID 對真實 node 進行相應的更新
ReactComponent.DOMIDOperations.updateStylesByID(
this._rootNodeID,
styleUpdates
);
}
// 判斷如果 dangerouslySetInnerHTML 則在不一樣的狀況下進行相應的更新
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
var lastHtml = lastProp && lastProp.__html;
var nextHtml = nextProp && nextProp.__html;
if (lastHtml !== nextHtml) {
ReactComponent.DOMIDOperations.updateInnerHTMLByID(
this._rootNodeID,
nextProp
);
}
// 判斷 content 的狀況更新
} else if (propKey === CONTENT) {
ReactComponent.DOMIDOperations.updateTextContentByID(
this._rootNodeID,
'' + nextProp
);
// 對事件進行監聽
// 比較好奇的是舊的 propKey 若存在着事件監聽,這裏彷佛沒有作什麼處理
// 這樣不就內存溢出了嗎?難道說不會有這種狀況???
// 想多了啦,更新 props 的狀況,一樣的事件會被覆蓋
// 在對應 this._rootNodeID 的狀況下。(但願如此,沒有證明過,可是理解如此)
} else if (registrationNames[propKey]) {
putListener(this._rootNodeID, propKey, nextProp);
} else {
// 剩餘的就是更新 DOM 屬性啦
ReactComponent.DOMIDOperations.updatePropertyByID(
this._rootNodeID,
propKey,
nextProp
);
}
}
},
_updateDOMChildren: function(nextProps, transaction) {
// 來更新 DOM 子節點了
// 當前 this.props.content 類型
var thisPropsContentType = typeof this.props.content;
// 是否 thisPropsContentEmpty 爲空
var thisPropsContentEmpty =
this.props.content == null || thisPropsContentType === 'boolean';
// 新的 nextProps.content 類型
var nextPropsContentType = typeof nextProps.content;
// 是否 nextPropsContentEmpty 爲空
var nextPropsContentEmpty =
nextProps.content == null || nextPropsContentType === 'boolean';
// 最後使用的 content :
// 若 thisPropsContentEmpty 不爲空則取 this.props.content 不然
// this.props.children 類型爲 string 或 number 的狀況下取 this.props.children 不然
// null
var lastUsedContent = !thisPropsContentEmpty ? this.props.content :
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
// 使用內容 content :
// 若 nextPropsContentEmpty 不爲空則取 nextProps.content 不然
// nextProps.children 類型爲 string 或 number 的狀況下取 nextProps.children 不然
// null
var contentToUse = !nextPropsContentEmpty ? nextProps.content :
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
// Note the use of `!=` which checks for null or undefined.
// 最後使用的 children :
// 若 lastUsedContent 不爲 null or undefined 則取 null 不然
// 取 this.props.children ,以 content 優先
var lastUsedChildren =
lastUsedContent != null ? null : this.props.children;
// 使用 children :
// 若 contentToUse 不爲 null or undefined 則取 null 不然
// 取 nextProps.children ,以 content 優先
var childrenToUse = contentToUse != null ? null : nextProps.children;
// 須要使用 content 狀況
if (contentToUse != null) {
// 是否須要移除 children 判斷結果:
// 最後使用的 children 存在而且 children 再也不須要使用
var childrenRemoved = lastUsedChildren != null && childrenToUse == null;
if (childrenRemoved) {
// 更新子節點
this.updateMultiChild(null, transaction);
}
// 若沒知足上面條件則說明不須要更新掉 children
// 而且新舊 content 不相等的狀況下進行 DOM 操做
if (lastUsedContent !== contentToUse) {
ReactComponent.DOMIDOperations.updateTextContentByID(
this._rootNodeID,
'' + contentToUse
);
}
} else {
// 反之看是否須要移除 content
// 若最後使用的 content 存在且 content 再也不須要使用
var contentRemoved = lastUsedContent != null && contentToUse == null;
if (contentRemoved) {
// 進行 DOM 操做
ReactComponent.DOMIDOperations.updateTextContentByID(
this._rootNodeID,
''
);
}
// 更新子節點
// 壓扁更新,與掛載時同樣
this.updateMultiChild(flattenChildren(nextProps.children), transaction);
}
},
};
複製代碼
關於 DOM 操做一系列的方法這裏不許備作解讀,能夠直接查看源碼 core/ReactDOMIDOperations.js
,道理都是同樣的。可是,這裏須要看下 updateMultiChild
方法,由於這裏已經涉及到 Diff 實現,可是在講 Diff 以前,咱們先把 ReactTextComponent.receiveProps
給解讀掉,其實方法裏面很簡單,就是操做了 ReactDOMIDOperations
相關的方法,具體實現直接看源碼就行,那麼接下來,咱們來看到 updateMultiChild
:less
// core/ReactMultiChild.js
// 直接看到 updateMultiChild
var ReactMultiChildMixin = {
enqueueMarkupAt: function(markup, insertAt) {
this.domOperations = this.domOperations || [];
this.domOperations.push({insertMarkup: markup, finalIndex: insertAt});
},
enqueueMove: function(originalIndex, finalIndex) {
this.domOperations = this.domOperations || [];
this.domOperations.push({moveFrom: originalIndex, finalIndex: finalIndex});
},
enqueueUnmountChildByName: function(name, removeChild) {
if (ReactComponent.isValidComponent(removeChild)) {
this.domOperations = this.domOperations || [];
this.domOperations.push({removeAt: removeChild._domIndex});
removeChild.unmountComponent && removeChild.unmountComponent();
delete this._renderedChildren[name];
}
},
/** * Reconciles new children with old children in three phases. * * - Adds new content while updating existing children that should remain. * - Remove children that are no longer present in the next children. * - As a very last step, moves existing dom structures around. * - (Comment 1) `curChildrenDOMIndex` is the largest index of the current * rendered children that appears in the next children and did not need to * be "moved". * - (Comment 2) This is the key insight. If any non-removed child's previous * index is less than `curChildrenDOMIndex` it must be moved. * * @param {?Object} children Flattened children object. */
updateMultiChild: function(nextChildren, transaction) {
// 一些補全判斷操做
if (!nextChildren && !this._renderedChildren) {
return;
} else if (nextChildren && !this._renderedChildren) {
this._renderedChildren = {}; // lazily allocate backing store with nothing
} else if (!nextChildren && this._renderedChildren) {
nextChildren = {};
}
// 用於更新子節點時,記錄的父節點 ID 前綴加 dot
var rootDomIdDot = this._rootNodeID + '.';
// DOM markup 標記緩衝
var markupBuffer = null; // Accumulate adjacent new children markup.
// DOM markup 標記緩衝等待插入的數量
var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
// 新子節點的循環用索引 index
var loopDomIndex = 0; // Index of loop through new children.
var curChildrenDOMIndex = 0; // See (Comment 1)
// 遍歷新的 children
for (var name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {continue;}
var curChild = this._renderedChildren[name];
var nextChild = nextChildren[name];
// 經過 constructor 來判斷 curChild 和 nextChild 是否爲同一個
if (shouldManageExisting(curChild, nextChild)) {
if (markupBuffer) {
// 若 DOM markup 標記緩衝存在,將其加入隊列
// 標記位置爲 loopDomIndex - numPendingInsert
// 這裏和下面是同樣的道理,請看到循環結束後
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
// 清空 DOM markup 標記緩衝
markupBuffer = null;
}
// 初始化 DOM markup 標記緩衝等待插入的數量爲 0
numPendingInsert = 0;
// _domIndex 在掛載中依次按照順序進行排序,若他小於目前的子節點順序
// 則進行移動操做,移動操做則是記錄原 index 和現 index (也就是新子節點的循環用索引 index )
if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
// 我沒有辦法聯想到此狀況
this.enqueueMove(curChild._domIndex, loopDomIndex);
}
// curChildrenDOMIndex 則取大值
curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
// 硬核式遞歸更新!!一樣會進入到 Diff
!nextChild.props.isStatic &&
curChild.receiveProps(nextChild.props, transaction);
// 更新 _domIndex 屬性
curChild._domIndex = loopDomIndex;
} else {
// 若 curChild 和 nextChild 不爲同一個的時候
if (curChild) { // !shouldUpdate && curChild => delete
// 卸載舊子節點加入隊列,並操做卸載組件
this.enqueueUnmountChildByName(name, curChild);
// curChildrenDOMIndex 則取大值
curChildrenDOMIndex =
Math.max(curChild._domIndex, curChildrenDOMIndex);
}
if (nextChild) { // !shouldUpdate && nextChild => insert
// 對應位置傳入新子節點
this._renderedChildren[name] = nextChild;
// 生成新的 markup 標記
// ID 爲父 ID 加 dot 加如今的 name
var nextMarkup =
nextChild.mountComponent(rootDomIdDot + name, transaction);
// 累加 DOM markup 標記緩衝
markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
// DOM markup 標記緩衝等待插入的數量
numPendingInsert++;
// 新的子節點 _domIndex 更新
nextChild._domIndex = loopDomIndex;
}
}
// 若新子節點存在,則新子節點的循環用索引 index 累加 1
loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
}
if (markupBuffer) {
// 將 DOM markup 標記緩衝加入隊列
// 這裏的 loopDomIndex - numPendingInsert 能夠解釋下
// 會使得 markupBuffer 存在的狀況就是進入第二個分支,那麼一樣的,
// 會使得 numPendingInsert 增長的狀況也是第二個分支,那麼在這裏插入的 DOM markup 標記
// 是最後插入的,他須要從整個循環 DOM 索引減去等待數量來肯定插入位置
// 舉個例子,你在進入第二個分支時,舊節點存在的狀況下必定會被移除
// 新節點存在的狀況下必定會被生成 DOM markup 標記 而且累加相應的數量
// loopDomIndex 也會隨之增長,loopDomIndex 也必定大於等於 numPendingInsert
// 如:舊節點 <div></div><p></p>
// 新節點 <div></div><span></span><p></p>
// 這種狀況下 loopDomIndex 爲 3 , numPendingInsert 爲 2 ,插入位置爲 1
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
}
// 遍歷舊 children
for (var childName in this._renderedChildren) { // from other direction
if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
var child = this._renderedChildren[childName];
if (child && !nextChildren[childName]) {
// 舊的存在,新的不存在加入隊列
this.enqueueUnmountChildByName(childName, child);
}
}
// 執行 DOM 操做隊列
this.processChildDOMOperationsQueue();
},
processChildDOMOperationsQueue: function() {
if (this.domOperations) {
// 執行隊列
ReactComponent.DOMIDOperations
.manageChildrenByParentID(this._rootNodeID, this.domOperations);
this.domOperations = null;
}
},
};
複製代碼
在上面這個執行隊列,咱們須要看到相關的 DOM 操做:dom
// domUtils/DOMChildrenOperations.js
var MOVE_NODE_AT_ORIG_INDEX = keyOf({moveFrom: null});
var INSERT_MARKUP = keyOf({insertMarkup: null});
var REMOVE_AT = keyOf({removeAt: null});
var manageChildren = function(parent, childOperations) {
// 用於得到 DOM 中原生的 Node
// 符合 MOVE_NODE_AT_ORIG_INDEX 和 REMOVE_AT
var nodesByOriginalIndex = _getNodesByOriginalIndex(parent, childOperations);
if (nodesByOriginalIndex) {
// 移除對應的 Node
_removeChildrenByOriginalIndex(parent, nodesByOriginalIndex);
}
// 對應的插入
_placeNodesAtDestination(parent, childOperations, nodesByOriginalIndex);
};
複製代碼
那麼到此, Diff 實現算是解讀完成,最後關於 ref 咱們在這裏也直接解讀掉, ref 爲引用,看到官方註釋:「 ReactOwners are capable of storing references to owned components. 」,那麼首先咱們得知道 [OWNER]
是什麼,他是:「引用組件全部者的屬性鍵。」,那麼他的值就是該組件的全部者(也就是父組件實例),這句話的依據在哪裏呢?函數
// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
_renderValidatedComponent: function() {
// render 方法執行前,咱們將 this 也就是當前複合組件傳入 ReactCurrentOwner.current
// render 方法執行結束後,咱們將置空 ReactCurrentOwner.current
ReactCurrentOwner.current = this;
var renderedComponent = this.render();
ReactCurrentOwner.current = null;
return renderedComponent;
},
};
複製代碼
那麼執行 render
方法時,發生了什麼?回憶一下。返回的是 ReactCompositeComponent
或者 ReactNativeComponent
或者 ReactTextComponent
,那麼他們在被實例化的過程當中得到了 ReactCurrentOwner.current
:oop
// core/ReactComponent.js
var ReactComponent = {
Mixin: {
construct: function(initialProps, children) {
// Record the component responsible for creating this component.
// 記錄負責建立此組件的組件。
// 將其記錄下來。
this.props[OWNER] = ReactCurrentOwner.current;
},
}
};
複製代碼
那麼講了這麼多,他和 ref 有什麼關係呢,那還確實有關係。在掛載、更新、卸載組件時都會發生 ref 的更新,若你對子組件添加了 ref 屬性,那麼他對應的鍵會出如今他擁有者的 this.refs
上,那麼你就能夠經過擁有者調用引用上的方法。學習
那麼到此,實現組件更新。