react-dom入口函數以及基本數據類型。閱讀React包的源碼版本爲16.8.6。
在第一章節我們瞭解到,react
包本質上是一個數據結構
建立的抽象屏障,提供起來供react的其它包,諸如react-dom
,react-native
調用。在這一章中,進入react-dom
的源碼閱讀。
根據package.json
的main
字段入口,我們可以找到react-dom
的入口文件爲src/client/ReactDOM.js
。我們發現該文件最後的代碼export default ReactDOM
僅僅對外暴露了一個對象模塊。我們簡單看一下這個對象模塊。
// 函數內部代碼均先省略
const ReactDOM: Object = {
createPortal,
findDOMNode() { },
hydrate() {},
render() {},
unstable_renderSubtreeIntoContainer() {},
unmountComponentAtNode() {},
unstable_createPortal() {},
unstable_interactiveUpdates() {},
unstable_discreteUpdates,
unstable_flushDiscreteUpdates,
flushSync,
unstable_createRoot,
unstable_createSyncRoot,
unstable_flushControlled,
}
其實這裏的對象模塊就是對面暴露的react-dom
提供的Api部分。我們可以看到包括最熟悉的render
方法,用於服務端渲染的hydrate
,還有findDOMNode
,createPortal
等。
我們本章節就來查看下最常使用的render
函數的源碼大體邏輯結構。
// 調用方式 ReactDOM.render(element, container[, callback])
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
// 判斷dom節點是否正確
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
react-dom
源碼中使用了flow
來定義數據類型,函數入參中如element: React$Element<any>
這種寫法就是flow
的語法。近似於typescript
。
render
函數在除去DEV
調試部分邏輯後,剩餘的代碼非常簡單,判斷傳入的container
節點是否爲Dom
節點,是就進入legacyRenderSubtreeIntoContainer
函數,我們來跟着代碼接着看。
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
// 是否複用dom節點,服務端渲染調用
forceHydrate: boolean,
callback: ?Function,
) {
// 從 container 中獲得root節點
let root: _ReactSyncRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// 沒有root,創建root節點, 移除所有子節點
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
// 有無callback
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
// 有無callback 邏輯同上
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
legacyRenderSubtreeIntoContainer
首先取出container
中的root
節點,根據有無root節
點來劃分不同的創建更新邏輯。首次使用render
函數的時候是不存在root
節點的,此時通過legacyCreateRootFromDOMContainer
創建一個root
節點給container._reactRootContainer
。然後如果存在callback
就進行調用,最後進行了一個unbatchedUpdates
。存在root
節點的時候,就省去了創建root
節點部分的代碼,直接進行callback
的判斷和updateContainer
。
我們先來看創建root
節點的legacyCreateRootFromDOMContainer
部分的代碼。
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): _ReactSyncRoot {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
// 不需要進行 shouldHydrate 過程,即我們正常的render過程
if (!shouldHydrate) {
let warned = false;
let rootSibling;
// 當有子節點的時候,一直循環,刪除完子節點
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
}
// Legacy roots are not batched.
/**
* LegacyRoot 爲一個常量標識符,具體細節如下
* export type RootTag = 0 | 1 | 2;
* export const LegacyRoot = 0;
* export const BatchedRoot = 1;
* export const ConcurrentRoot = 2;
*/
return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}
前面提到過,forceHydrate
這個布爾值是用於標識是否是服務端渲染的,在瀏覽器環境下是不觸碰這部分的邏輯的,這個相關部分就先跳過。那麼legacyCreateRootFromDOMContainer
就做了兩件事情:
- 刪除
container
容器部分的所有子節點。這也就是爲什麼我們使用ReactDom.render
渲染在目標節點之後,節點的子元素全部消失的原因。 - 返回了
ReactSyncRoot
類,實例化了一個root
根節點的實例。
接下來的ReactSyncRoot
代碼更簡單:
function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
hydrate: boolean,
) {
// Tag is either LegacyRoot or Concurrent Root
const root = createContainer(container, tag, hydrate);
this._internalRoot = root;
}
我們追尋createContainer
函數,發現這個函數文件在react-reconciler/src/ReactFiberReconciler
包中。我們跟着去查看一下:
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate);
}
// 在 `react-reconciler/src/ReactFiberRoot`文件中
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(tag);
// 相互指
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
return root;
}
// fiber root 結構的真身
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
// root 節點對應的Fiber對象
this.current = null;
// dom 節點
this.containerInfo = containerInfo;
// 持久化更新會用到
this.pendingChildren = null;
this.pingCache = null;
this.finishedExpirationTime = NoWork;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.firstBatch = null;
this.callbackNode = null;
this.callbackExpirationTime = NoWork;
this.firstPendingTime = NoWork;
this.lastPendingTime = NoWork;
this.pingTime = NoWork;
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
this.pendingInteractionMap = new Map();
}
}
終於在FiberRootNode
中發現了rootRoot的真身,就是一個帶標識的對象。其中比較重要的一個爲containerInfo
,就是reactElement
將要渲染上的容器節點信息。我們還能發現,很多標識賦值了NoWork
,NoWork
設計到後續我們更新會提及的ExpirationTime
的概念,是React
更新算法的基礎。目前你可以就把NoWork
理解爲一個標識0
的常量(源碼export const NoWork = 0;
)。
我們最後來看current
,在createFiberRoot
中將其指向了createHostRootFiber
創建的uninitializedFiber
。這個uninitializedFiber
就是reactElement
對應的fiber
節點,我們一起來看一下這部分邏輯。
// 位於react-reconciler/src/ReactFiber.js
function createHostRootFiber(tag: RootTag): Fiber {
let mode;
// 根據 tag 的不同,獲得不同的mode模式
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BatchedMode | StrictMode;
} else if (tag === BatchedRoot) {
mode = BatchedMode | StrictMode;
} else {
mode = NoMode;
}
if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
// This enables DevTools to start capturing timing at any point–
// Without some nodes in the tree having empty base times.
mode |= ProfileMode;
}
return createFiber(HostRoot, null, null, mode);
}
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// pendingProps 將要更新
this.pendingProps = pendingProps;
// 之前的props
this.memoizedProps = null;
// update對象
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects,標記組件生命週期,以及組件是否需要更新
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime = NoWork;
this.childExpirationTime = NoWork;
this.alternate = null;
if (enableProfilerTimer) {
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
}
這部分邏輯比較長,我們來拆成兩部來看。createHostRootFiber
總共做了兩件事情,根據tag
存在的標識,調整了mode
字段。然後使用mode
字段創建了FiberNode
對象。
這裏我們稍微提一下使用|
和&
來進行一個打標的設計模式。比如我現在有三個屬性的標識符a/b/c
,我們用二進制來定義它們,保證每個模式1
所在的位置不同。
var a = 0b001;
var b = 0b010;
var c = 0b100;
我們現在對一個demo
變量進行屬性的賦值,比如我想要這個demo
變量擁有屬性a和屬性c。那我只需要var demo = a | c
。在後續我對demo
進行一個擁有屬性判斷的時候,我只需要使用&
,如果得到的結果大於0,即轉換爲true
,就說明demo
擁有該屬性。如我想要判斷demo
是否含有a
屬性,只需要if (demo | a) { /* ... */ }
即可。如果我想要給demo
添加一個屬性,比如添加屬性b
,只需要將demo |= b
即可,如果不是很瞭解一元操作符的同學,可以去mdn
上面查一下相關的資料就能明白。
我們前面在legacyCreateRootFromDOMContainer
函數的註釋中提到過,rootTag是通過LegacyRoot | BatchedRoot | ConcurrentRoot
取得的三個模式的綜合。所以createHostRootFiber
這裏我們走的是最後一個else
分支,mode=NoMode
。然後創建Fiber
節點。
Fiber
節點就是對應每一個ReactElement
的節點了,它上面記載了很多我們熟悉的屬性,比如ref
,比如props
相關的pendingProps
,memoizedProps
。然後還需要關注一下的概念就是expirationTime
。expirationTime
前面root
的時候也提到了,這是節點更新操作的依據,在後續的源碼部分也會單獨拆分一章節來闡述它。
還需要提一下的是我註釋了Fiber
相關的幾個屬性sibling,return,child
。react
中的Fiber
節點對應的是一個單向列表結構。比如我有這樣的一個jsx
結構:
function Demo() {
return (
<ul>
<li></li>
<li></li>
<li></li>
</ul>
);
}
那麼這個結構在Fiber
中會這樣存在ul.child -> li(1).sibling -> li(2).sibling -> li(3)
。每個節點的return
則對應總的父節點li(1).return -> ul
。
這一章當中,我們簡單看了一下ReactDom.render
的總體函數邏輯和創建數據結構部分的源碼。首次創建的時候,render
會創建一個FiberRootNode
對象,該對象作爲整個React應用的根節點,同時給RootNode
創建對應的Fiber
對象。每一個Fiber
對象對應一個ReactElement
元素,它帶有各種用於React
調度的屬性元素,DOM
以單向鏈表的數據結存在於React
應用當中。
下一章我們會接着render
函數的邏輯進入unbatchedUpdates
部分代碼,大體介紹一下React-dom
在更新中的一些框架設計。