閱讀原始碼成了今年的學習目標之一,在選擇 Vue 和 React 之間,我想先閱讀 React 。 在考慮到讀哪個版本的時候,我想先接觸到原始碼早期的思想可能會更輕鬆一些,最終我選擇閱讀
0.3-stable
。 那麼接下來,我將從幾個方面來解讀這個版本的原始碼。
- React 原始碼學習(一):HTML 元素渲染
- React 原始碼學習(二):HTML 子元素渲染
- React 原始碼學習(三):CSS 樣式及 DOM 屬性
- React 原始碼學習(四):事務機制
- React 原始碼學習(五):事件機制
- React 原始碼學習(六):元件渲染
- React 原始碼學習(七):生命週期
- React 原始碼學習(八):元件更新
是什麼引起元件更新
引發元件更新的方法就是 this.setState
,按照註釋程式碼看來 this.setState
是不可變的,則 this._pendingState
是用來存放掛起的 state
,他不會直接更新到 this.state
,讓我們來看到程式碼:
// 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;
}
},
};
複製程式碼
setState 觸發了什麼
那麼到此為止,你可以知道 this.setState
並非事實更新 this.state
的,比如我們看到在 componentWillMount
中去使用 this.setState
並不會馬上更新到 this.state
,那麼我們繼續閱讀後面程式碼:
// 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;
}
},
};
複製程式碼
各個元件的 receiveProps 方法
上面程式碼看來,一個是不替換元件的情況下更新元件,另一個則是直接更新 markup
標記。我們按照順序一個個看過來吧,先看到 ReactCompositeComponent.receiveProps
:
// 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
:
// 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);
}
},
};
複製程式碼
Diff
關於 DOM 操作一系列的方法這裡不準備做解讀,可以直接檢視原始碼 core/ReactDOMIDOperations.js
,道理都是一樣的。但是,這裡需要看下 updateMultiChild
方法,因為這裡已經涉及到 Diff 實現,但是在講 Diff 之前,我們先把 ReactTextComponent.receiveProps
給解讀掉,其實方法裡面很簡單,就是操作了 ReactDOMIDOperations
相關的方法,具體實現直接看原始碼就行,那麼接下來,我們來看到 updateMultiChild
:
// 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 操作:
// 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);
};
複製程式碼
refs 引用
那麼到此, 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
:
// 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
上,那麼你就可以通過擁有者呼叫引用上的方法。
那麼到此,實現元件更新。