本系列文章總共三篇:javascript
React 採用 monorepo 的管理方式。倉庫中包含多個獨立的包,以便於更改能夠一塊兒聯調,而且問題只會出如今同一地方。html
packages
包含元數據(好比 package.json
)和 React 倉庫中全部 package 的源碼(子目錄 src
)。若是你須要修改源代碼, 那麼每一個包的 src
子目錄是你最須要花費精力的地方。fixtures
包含一些給貢獻者準備的小型 React 測試項目。build
是 React 的輸出目錄。源碼倉庫中並無這個目錄,可是它會在你克隆 React 而且第一次構建它以後出現。React 「Core」 中包含全部全局 React
API,好比:java
React 核心只包含定義組件必要的 API。它不包含協調算法或者其餘平臺特定的代碼。它同時適用於 React DOM 和 React Native 組件。React 核心代碼在源碼的 packages/react 目錄中。在 npm 上發佈爲 react 包。相應的獨立瀏覽器構建版本稱爲 react.js,它會導出一個稱爲 React 的全局對象。react
React 最初只是服務於 DOM,可是這以後被改編成也能同時支持原平生臺的 React Native。所以,在 React 內部機制中引入了「渲染器」這個概念。
渲染器用於管理一棵 React 樹,使其根據底層平臺進行不一樣的調用。
渲染器一樣位於 packages/
目錄下:git
ReactDOM
API,這在npm上做爲 react-dom
包。這也能夠做爲單獨瀏覽器版本使用,稱爲 react-dom.js
,導出一個 ReactDOM
的全局對象.即使 React DOM 和 React Native 渲染器的區別很大,但也須要共享一些邏輯。特別是協調算法須要儘量類似,這樣可讓聲明式渲染,自定義組件,state,生命週期方法和 refs 等特性,保持跨平臺工做一致。
爲了解決這個問題,不一樣的渲染器彼此共享一些代碼。咱們稱 React 的這一部分爲 「reconciler」。當處理相似於 setState()
這樣的更新時,reconciler 會調用樹中組件上的 render()
,而後決定是否進行掛載,更新或是卸載操做。
Reconciler 沒有單獨的包,由於他們暫時沒有公共 API。相反,它們被如 React DOM 和 React Native 的渲染器排除在外。
這部分源碼在 /packages/react-reconciler。github
在上一篇中我說在 react 中從產生更新到最終操做DOM這之間能夠叫作 reconciliation(協調)的過程,其實這中間還能夠再進行細分,其中產生的更新會放在一個更新隊列裏,如何調度這些更新讓它們進行下一步任務這個部分叫作 scheduler,而 react 採用叫作 Cooperative Scheduling (合做式調度)的方式來調度任務,簡單來講就是充分利用瀏覽器的空閒時間來執行任務,有空閒時間就執行對應的任務,沒有就把執行權交給瀏覽器,在瀏覽器中就是經過 requestIdleCallback 這個 API 來實現的,可是由於這個 API 存在的一些問題以及瀏覽器的兼容性問題,因此 react 經過 requestAnimationFrame、setTimeout 和 MessageChannel 來模擬了 requestIdleCallback 的行爲。如今 react 把這部分代碼單獨拎出來做爲一個 package。
這部分源碼在 /packages/scheduler 中。算法
react 本身實現了一套事件系統,和原生的 DOM 事件系統相比減小了內存消耗,抹平了瀏覽器差別,那麼 react 是如何作到的呢,主要是採用瞭如下策略:npm
這部分的源碼在 /packages/events 中。json
FiberRoot 是整個應用的入口對象,它是一個 javascript 對象,內部記錄了不少和應用更新相關的全局信息,好比要掛載的 container。react-native
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
// 當前應用對應的Fiber對象,是Root Fiber
this.current = null;
// root節點,render方法接收的第二個參數
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.pingCache = null;
// finishedWork 對應的過時時間
this.finishedExpirationTime = NoWork;
// 完成 reconciliation 階段的 RootFiber 對象,接下來要進入 commit 階段
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();
}
}
複製代碼
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) {
// Instance
// 標記不一樣的組件類型
this.tag = tag;
// ReactElement裏面的key
this.key = key;
// ReactElement.type,也就是咱們調用`createElement`的第一個參數
this.elementType = null;
// 異步組件resolved以後返回的內容,通常是`function`或者`class`
this.type = null;
// 跟當前Fiber對象對應的那個 element(DOM、class實例等)
this.stateNode = null;
// Fiber
// 指向 parent fiber
this.return = null;
// 指向第一個 child
this.child = null;
// 指向兄弟節點
this.sibling = null;
// 數組中節點的索引,在 diff 算法中進行比對
this.index = 0;
this.ref = null;
// 新的變更帶來的新的props
this.pendingProps = pendingProps;
// 上一次渲染完成以後的props
this.memoizedProps = null;
// 該Fiber對應的組件產生的Update會存放在這個隊列裏面
this.updateQueue = null;
// 上一次渲染的時候的state
this.memoizedState = null;
this.contextDependencies = null;
this.mode = mode;
// Effects
// 用來記錄自身的 Effect
this.effectTag = NoEffect;
// 單鏈表用來快速查找下一個side effect
this.nextEffect = null;
// 子樹中第一個side effect
this.firstEffect = null;
// 子樹中最後一個side effect
this.lastEffect = null;
// 表明任務在將來的哪一個時間點應該被完成
this.expirationTime = NoWork;
// 子樹中的最先過時時間
this.childExpirationTime = NoWork;
// 和它對應的 Fiber 對象
// current <=> workInProgress
this.alternate = null;
}
複製代碼
在上一篇中咱們說到爲了將任務排出優先級 react 最開始只是定死了幾個 Priority(優先級)變量,可是這樣會出現飢餓問題,低優先級的任務可能一直被打斷,後來 react 引入了 expirationTime(過時時間)的概念,這樣即便是低優先級的任務只要過時時間一到也能強制當即執行,那麼 expirationTime 是如何計算出來的呢,能夠參考以下的過程:
若是沒有看懂也沒有關係,我這裏計算了幾種狀況下的 expirationTime,你能夠找找規律:
咱們能夠發現對於同步任務好比 ReactDOM.render 來講,expirationTime 就是很大的整數(32位系統中的最大整數),若是是低優先級的異步任務那麼計算出來的時間以 25 爲基數進行增加,而若是是高優先級的異步任務(好比用戶交互)計算出來的時間是一 10 爲基數進行增加,且相同的 currentTime 高優先級的 expirationTime 要大於低優先級的 expirationTime,react 這麼作的目的:一是讓 25/10 ms 之內觸發的更新能有相同的過時時間,這樣就能夠批量更新以提高性能;二是讓高優先級的任務過時時間大於低優先級以提升它的優先級。
React.createElement(
type,
[props],
[...children]
)
複製代碼
createElement 是 react 中建立一個 element 的方法,它能夠建立一個指定類型的元素,類型參數能夠是元素 DOM 標籤字符串,或是一個 react component 類型(類或函數)或是 Fragment 類型。
createElement 源碼位於 /react/src/ReactElement.js 中
export function createElement(type, config, children) {
// 初始化變量
const props = {};
// ...
// 步驟一:初始化屬性
// 將 config 上面定義的屬性定義到 props 上
// 注意:排除了 RESERVED_PROPS 裏面的屬性名(key,ref等)
if (config != null) {
// ...
}
// 步驟二:將 children 掛載到 props.children 上
// 若是是多個 children 就將其轉換爲數組
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// ...
props.children = childArray;
}
// 步驟三:解析 defaultProps
if (type && type.defaultProps) {
// ...
}
// 步驟四:將處理好的變量傳給 ReactElement 構造函數
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
複製代碼
const ReactElement = function (type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element
// 這個標籤容許咱們惟一地將其標識爲一個React元素
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
return element;
};
複製代碼
ReactDom.render 的源代碼位於 /react-dom/src/client/ReactDOM.js
前期準備階段所作的事情歸納起來就三點:
前期準備階段所作的事情歸納起來就三點:
須要注意的是若是是經過 react element 上綁定的事件函數裏面調用的 setState 方法,會在執行 setState 方法以前設置 workPhase = BatchedEventPhase;,因此在 scheduleUpdateOnFiber 方法中會進入下圖的分支。
找到觸發更新節點對應的 fiberRoot 節點,而後調對該節點的更新,分爲兩種狀況:同步和異步,同步又能夠分爲兩種:是不是 LegacyUnbatchedPhase,若是是就不須要調度直接進入下一階段(render phase),若是不是就放到下一幀當即執行,對於異步任務則須要根據優先級算出一個過時時間,而後再和隊列裏排隊的任務進行比較找出立刻要過時的那個任務在下一幀進入下一個階段執行(render phase)。
將傳入的 callback 放入 syncQueue 中,而後調用 Scheduler_scheduleCallback 設置優先級爲 Scheduler_ImmediatePriority,callback 爲 flushSyncCallbackQueueImpl
流程圖:React@16.8.6源碼解析——調度(二)
將傳入的 reactPriorityLevel 轉換爲 schedule 中的 priorityLevel 而後調用 Scheduler_scheduleCallback
流程圖:React@16.8.6源碼解析——調度(二)
根據傳入的 priorityLevel 和 timeout 計算出新的 expirationTime,根據新的 expirationTime 和傳入的 callback 建立一個 newNode,而後看看當前第一個等待調度的任務(firstCallbackNode)是不是空,若是是空就把 newNode 做爲 firstCallbackNode 而後調用 scheduleHostCallbackIfNeeded,不然就比較 newNode 的過時時間是不是當前列表中最先的,若是是也把它設置爲 firstCallbackNode 而後執行 scheduleHostCallbackIfNeeded
流程圖:React@16.8.6源碼解讀——調度(三)
若是 firstCallbackNode 不爲空就執行 requestHostCallback(flushWork, expirationTime);
流程圖:React@16.8.6源碼解讀——調度(三)
設置 scheduledHostCallback 爲傳入的 callback 若是當前有一個 callback 正在執行或過時時間小於 0 則當即調用 port.postMessage 表示當即執行 scheduledHostCallback 並傳入是否超時(didTimeout)不然調用 requestAnimationFrameWithTimeout(animationTick);
流程圖:React@16.8.6源碼解讀——調度(四)
模擬 requestAnimationFrame 在下一幀執行傳入的 callback,由於 requestAnimationFrame 在頁籤是後臺運行時不執行,因此又經過 setTimeout 設置了一個了一個定時器來解決這個問題,若是 requestAnimationFrame 生效了就取消定時器,反之亦然。
流程圖:React@16.8.6源碼解讀——調度(四)
若是 scheduledHostCallback 不爲空就接着調用 requestAnimationFrameWithTimeout 安排下一幀的任務 不然就是沒有等待的任務了就退出,計算出下一幀運行的時間(nextFrameTime),若是小於 8ms 就設置爲 8ms ,接着計算出當前幀的過時時間(frameDeadline)若是有任務就接着調用 port.postMessage。
流程圖:React@16.8.6源碼解讀——調度(四)
port.postMessage 後就能夠被 port.onmessage 接收到,收到以後判斷當前幀是否還有剩餘時間,若是沒有檢查下要執行的任務(scheduledHostCallback)是否超時,超時就設置 didTimeout = true 沒超時就接着調用 requestAnimationFrameWithTimeout,而後退出;若是剩餘時間還有就執行 scheduledHostCallback(didTimeout)。
流程圖:React@16.8.6源碼解讀——調度(四)
它接受的參數就是 port.onmessage 傳入的 didTimeout,若是 didTimeout 爲真(說明當前幀沒有時間了)判斷第一個要執行的任務 (firstCallbackNode)的 expirationTime 是否小於當前時間,小於的話就不斷執行 flushFirstCallback 直到 firstCallbackNode 爲空或 firstCallbackNode.expirationTime 大於等於當前時間;若是 didTimeout 爲假(說明當前幀還有時間)那就不斷執行 flushFirstCallback 直到 firstCallbackNode 爲空或當前幀已經沒有剩餘時間了,最後不管是上面何種狀況都會再執行 scheduleHostCallbackIfNeeded 判斷一下是否還有須要執行的任務。
流程圖:React@16.8.6源碼解讀——調度(六)
執行鏈表裏的第一個任務(firstCallbackNode)並傳入是否超時(didUserCallbackTimeout),這裏 ImmediatePriority 也會當作超時,firstCallbackNode 可能會再返回一個 callback,將新的回調函數插入到列表中,根據它的到期時間排序,若是新的回調是列表中優先級最高的就調用 scheduleHostCallbackIfNeeded 安排下一次執行。
流程圖:React@16.8.6源碼解析——調度(五)
對於 scheduleSyncCallback 來講最終執行的** **scheduledHostCallback 就是 flushSyncCallbackQueueImpl
這個方法中就是循環執行 syncQueue 數組中的任務。
流程圖:React@16.8.6源碼解析——調度(五)
還記得最開始若是處於同步階段而且 workPhase 爲 NotWorking 時執行完 scheduleCallbackForRoot 就會調用這個方法,這個方法首先去調用 Scheduler_cancelCallback 取消 immediateQueueCallbackNode,接着會執行 flushSyncCallbackQueueImpl 也就是上面那個方法,immediateQueueCallbackNode 的 callback 對應的就是 flushSyncCallbackQueueImpl,因此我認爲這個方法就是當即調用 flushSyncCallbackQueueImpl 去執行 syncQueue 中的回調任務而不是等待下一幀執行。
從 rootFiber 開始循環遍歷 fiber 樹的各個節點,對於每一個節點會根據節點類型調用不一樣的更新方法,好比對於 class 組件會建立實例對象,調用 updateQueue 計算出新的 state,執行生命週期函數等,再好比對於 HostComponent 會給它的 children 建立 fiber 對象,當一側子樹遍歷完成以後會開始執行完成操做,即建立對應 dom 節點並添加到父節點下以及設置父節點的 effect 鏈,而後遍歷兄弟節點對兄弟節點也執行上述的更新操做,就這樣將整棵樹更新完成以後就能夠進入下一階段(commit phase)。
renderRoot 是整個 render phase 的核心方法,是整個階段的入口方法。
進入方法後,首先會判斷若是新的 fiberRoot 和 以前正在處理的 fiberRoot(workInProgressRoot)不一致或當前的 expirationTime 並不等於正在執行渲染的任務的 expirationTime(renderExpirationTime),那麼就執行 prepareFreshStack,若是 isSync 爲真( expirationTime === Sync 或 priorityLevel === ImmediatePriority)而且 是異步任務而且還過時了,那麼當即執行 renderRoot,傳入的 expirationTime 是當前時間;若是 isSync 不爲真說明當前任務沒有超時,那麼設置 currentEventTime = NoWork; 這樣下一次請求 currentTime 時就能夠獲得一個新的時間。
接下來就進入到重頭戲,若是 isSync 爲真就調用 workLoopSync 不然調用 workLoop,這兩個方法咱們會在下面單獨講解,這裏先暫且不表,接下來就若是兩個方法中有報錯就執行異常處理,沒有報錯判斷 workInProgress 是否還有,若是還有說明還有任務要作就接着執行 renderRoot,若是任務順利完成就進入到下一階段也就是 commit 階段,調用 commitRoot。
流程圖:React@16.8.6源碼淺析——渲染階段(renderRoot)
這個方法是任務開始以前的一些準備工做,以前一直好奇 workInProgress 是那裏初始化的,其實就是在這裏,這裏會調用 createWorkInProgress 根據 rootFiber 拷貝出一個 workInProgress 的 fiber 對象來,接着還會設置一些其它全局變量。
workLoopSync 比較簡單內部循環調用 performUnitOfWork,判斷條件是 performUnitOfWork 的返回值 workInProgress 是否爲空。
workLoop 和 workLoopSync 比較相似,區別就是循環的終止條件新增了 shouldYield,shouleYield 方法判斷當前是否應該被打斷,若是當前任務沒有超時而且任務的時間片已經不夠用了就會被打斷,這時候 workLoop 循環就會終止。
performUnitOfWork 是 workLoopSync 和 workLoop 兩個方法都會調用的方法,在其內部會調用 beginWork 方法,beginWork 方法會返回下一個要執行的任務(next),若是 next 爲空表示已經遍歷到葉子節點了,則調用 completeUnitOfWork 能夠執行完成邏輯了,關於這塊的執行細節能夠參考上一篇。
beiginWork 方法接收以前完成的 fiber 節點(current),正在執行的 fiber 節點(workInProgress)和當前的 expirationTime。
若是是初次渲染,設置 didReceiveUpdate 爲 false。
若是並不是初次渲染,判斷是否 props 有變化或 context 有變化,若是有變化則將 didReceiveUpdate 設置爲 true,不然判斷 workInProgress 的 expirationTime 是否小於傳入的 expirationTime(renderExpirationTime),小於的話設置 didReceiveUpdate 爲 false,而且根據 workInProgress 的類型對不一樣類型的元素作了一些處理就退出了,由於當前當前任務沒有須要執行的更新。
接着對於初次渲染或有更新的狀況,咱們再次根據 workInProgress 的類型去調用不一樣類型元素的更新方法,好比對於 ClassComponent 會調用 updateClassComponent,該方法會返回下一個要執行 performUnitOfWork 的節點,也就是它的子節點。
流程圖:React@16.8.6源碼淺析——渲染階段(beginWork)
當 beginWork 返回的節點(next)爲空時,就會調用 completeUnitOfWork,說明已經將遍歷到該組件的葉子了,接下來會向上找到父節點執行完成操做,而後遍歷兄弟節點,整個遍歷的流程能夠參考前一篇的內容。
已進入該方法首先會設置 workInProgress 爲傳入的節點,而後進入一個循環,首先判斷一下 workInProgress 是否被標記了 Incomplete(有異常),若是沒有異常就執行 completeWork,若是 completeWork 返回的結果(next)不爲空,就會直接 return next 讓 performUnitOfWork 去處理,若是 next 爲空就給父節點(returnFiber)掛載 effect,將當前節點和其子樹的 effect 掛載到父節點上;若是有異常會執行 unwindWork 方法,unwindWork 也返回一個節點(next),若是 next 不爲空就直接 return 交給 performUnitOfWork 去處理,而後清空其父節點的 effect 鏈只標記一個 Incomplete effect。
接着判斷是否有兄弟節點(siblingFiber),若是有就返回讓 performUnitOfWork 去執行,不然設置 workInProgress 爲父節點(returnFiber)繼續執行循環,知道 workInProgress 爲空,此時說明已經遍歷到根節點了,標記 workInProgressRootExitStatus = RootCompleted 說明根節點已經完成了,接下來就要能夠進入 commit 階段了,最後返回 null。
流程圖:React@16.8.6源碼淺析——渲染階段(completeUnitOfWork)
這個方法內部就是一個 switch 根據 workInProgress.tag
對不一樣類型的節點執行不一樣的方法,其中最經常使用的就是對 HostComponent 的操做,對於 HostComponent 若是是初次掛載會經過 createInstance 方法建立 dom 節點,而後經過 appendAllChildren 方法將建立好的 dom 節點掛載到父節點上,而後會調用 finalizeInitialChildren 方法跟 dom 節點綁定事件,將屬性設置到對應的 dom 節點上(好比 style),而後判斷若是是表單元素是否設置了 autoFocus 若是設置了就給 workInProgress 標記 update;對於 HostText 也是先判斷是不是初次掛載若是是就經過 createTextInstance 建立 text 節點並賦值給 workInProgress.stateNode 若是是更新流程就調用 updateHostText 給 workInProgress 標記一個 update。
流程圖:React@16.8.6源碼淺析——渲染階段(completeWork)
**
提交階段主要作的事情就是對 render 階段產生的 effect 進行處理,處理分爲三個階段
commitRoot 接受 fiberRoot 對象,而後調用 commitRootImpl 方法並把 fiberRoot 對象傳遞給它,若是執行的過程當中有 Passive effect 產生就會調用 flushPassiveEffects 去執行這些 effect,Passive effect 和 hooks 有關,這裏暫且不表。
流程圖:React@16.8.6源碼淺析——提交階段(commitRoot)
首先根據傳入的 fiberRoot 獲取到 finishedWork 也就是以前階段完成的 rootFiber,而後重置一些變量,接着處理 effect 鏈,若是 rootFiber 也有 effect,那也須要加到 effect 鏈上,接着經過三個循環來分別處理這些 effect:
最後調用 flushSyncCallbackQueue 若是 syncQueue 還有其它任務則執行它們
流程圖:React@16.8.6源碼淺析——提交階段(commitRoot)
該方法會判斷 effect 上是否有 Snapshot,Snapshot 會在 render 階段判斷 class 組件是否有 getSnapshotBeforeUpdate 這個生命週期時加上,若是有就調用 commitBeforeMutationEffectOnFiber,在這個方法裏會判斷 fiber 的類型,若是是 function 組件會調用 commitHookEffectList (這裏我不太明白爲何 function 組件會有 Snapshot),若是是 class 組件就會執行 getSnapshotBeforeUpdate 這個方法並將返回的結果設置到實例的 __reactInternalSnapshotBeforeUpdate 屬性上,這個再 componentDidUpdate 的時候會用到。
流程圖:React@16.8.6源碼淺析——提交階段(commitBeforeMutationEffects)
在該方法中會對一些和 dom 操做相關的 effect 進行執行:
注:
- 關於 Placement 對於父節點不是 dom 節點的插入,能夠參考這個流程圖
- 刪除操做的遍歷方式相似樹的深度優先遍歷
流程圖:React@16.8.6源碼淺析——提交階段(commitMutationEffects)
**
該方法是整個 commit 階段最後一個循環執行的方法,內部主要調用兩個方法 commitLayoutEffectOnFiber 和 commitAttachRef,第一個方法內部是一個 switch 對於不一樣的節點進行不一樣的操做:
至於 commitAttachRef 方法其實就是將節點的實例對象掛載到 ref.current 上
流程圖:React@16.8.6源碼淺析——提交階段(commitLayoutEffects)
注入階段是在 ReactDOM.js 文件一加載就去執行的,主要目的就是建立三個全局變量一邊之後使用
injectEventPluginOrder
該方法接受一個字符串數組,該數組定義了要注入事件插件的順序,而後調用 recomputePluginOrdering 方法,該方法會按照傳入的插件順序往 plugins 數組中添加 plugin,而且對 plugin 中的每一個事件調用 publishEventForPlugin 方法,下面的代碼以 change plugin 爲例以便你能夠直觀的瞭解該方法所作的事情。
const publishedEvents = pluginModule.eventTypes;
// const eventTypes = {
// change: {
// phasedRegistrationNames: {
// bubbled: 'onChange',
// captured: 'onChangeCapture',
// },
// dependencies: [
// TOP_BLUR,
// TOP_CHANGE,
// TOP_CLICK,
// TOP_FOCUS,
// TOP_INPUT,
// TOP_KEY_DOWN,
// TOP_KEY_UP,
// TOP_SELECTION_CHANGE,
// ],
// },
// };
for (const eventName in publishedEvents) {
invariant(
publishEventForPlugin(
publishedEvents[eventName], // ChangeEventPlugin.eventTypes.change
pluginModule, // ChangeEventPlugin
eventName, // change
),
'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',
eventName,
pluginName,
);
}
}
複製代碼
publishEventForPlugin
該方法首先會設置 eventNameDispatchConfigs 這個全局變量,接着遍歷 phasedRegistrationNames 該對象存儲的是你會在 react 元素上綁定的時間名,格式以下:
phasedRegistrationNames: {
bubbled: 'onChange',
captured: 'onChangeCapture',
}
複製代碼
bubbled 表示在冒泡階段觸發,captured 表示在捕獲階段觸發,接着對每一項調用 publishRegistrationName 方法
publishRegistrationName
該方法會將傳入的參數設置到 registrationNameModules 和 registrationNameDependencies 這兩個全局變量上
全局變量
經過以上方法最終會造成以下的全局變量,咱們以 ChangeEventPlugin 爲例
ChangeEventPlugin:
const eventTypes = {
change: {
phasedRegistrationNames: {
bubbled: 'onChange',
captured: 'onChangeCapture',
},
dependencies: [
TOP_BLUR,
TOP_CHANGE,
TOP_CLICK,
TOP_FOCUS,
TOP_INPUT,
TOP_KEY_DOWN,
TOP_KEY_UP,
TOP_SELECTION_CHANGE,
],
},
};
複製代碼
eventNameDispatchConfigs:
{
change: ChangeEventPlugin.eventTypes.change,
// ...other plugins
}
複製代碼
registrationNameModules:
{
onChange: ChangeEventPlugin,
onChangeCapture: ChangeEventPlugin
}
複製代碼
registrationNameDependencies:
{
onChange: ChangeEventPlugin.eventTypes.change.dependencies,
onChangeCapture: ChangeEventPlugin.eventTypes.change.dependencies
}
複製代碼
監聽階段是在 render 階段中的 completeWork 方法中會去對 HostComponent 調用 updateHostComponent 方法,在這個方法裏面會對 dom 節點的 props 進行設置,其中就包括了事件相關的屬性,咱們在這裏對事件進行綁定,綁定的節點是 document 而不是它本身,這樣有利於減小內存開銷提升性能,而對於交互類型和非交互類型的事件會綁定不一樣的事件處理函數。
finalizeInitialChildren
掛載事件的入口方法,在 render 階段的 completeWork 中被調用,在該方法中會調用 setInitialProperties。
setInitialProperties
該方法會默認對一些 dom 節點綁定事件即便你沒有設置事件,好比對 iframe 會綁定 load 事件,接着會執行 setInitialDOMProperties 方法。
setInitialDOMProperties
該方法會對 dom 節點上設置的 props 進行處理,這些 props 有 style、dangerouslySetInnerHTML、children 等,固然還有和事件相關的屬性,還記得咱們在前一節注入裏面設置過的全局變量 registrationNameModules 嗎,這裏就派上用場了,能夠經過它來判斷是否有事件相關的屬性該綁定了,若是有咱們會調用 ensureListeningTo 方法。
ensureListeningTo
該方法會接受 reactDOM.render 接受的第二個參數 container 和事件名(好比 onClick),判斷 container 是不是 document 或 DocumentFragment 節點,若是是就把它傳遞給 listenTo 方法,不然經過 element.ownerDocument 獲取對應的 document 而後傳遞給 listenTo 方法。
listenTo
該方法首先會給傳入的 element 建立一個 listeningSet,該對象用於存儲該元素監聽了哪些事件,接着經過咱們在注入階段生成的全局對象 registrationNameDependencies 獲取要綁定的事件所依賴的其它事件(dependencies),對 dependencies 進行遍歷,對須要在捕獲階段監聽的事件調用 trapCapturedEvent,其它事件就調用 trapBubbledEvent 方法,最後將事件放入 listeningSet 中。
trapCapturedEvent/trapBubbledEvent
這兩個方法都會調用 trapEventForPluginEventSystem,區別是 trapCapturedEvent 方法第三個參數會穿 true,trapBubbledEvent 第三個參數會傳 false。
trapEventForPluginEventSystem
首先判斷傳入的事件是不是一個 Interactive 事件,也就是是不是用戶交互相關的事件,若是是就將 dispatch 設置爲 dispatchInteractiveEvent 不然設置爲 dispatchEvent,而後根據第三個參數也就是 capture 來調用 addEventCaptureListener 或 addEventBubbleListener。
注:react 中定義的交互事件在這裏能夠看到
addEventCaptureListener/addEventBubbleListener
這兩個方法都會調用 element.addEventListener 區別在於第三個參數一個是 true 一個是 false,表示在捕獲階段觸發仍是在冒泡階段觸發。
dispatchInteractiveEvent
該方法會調用 discreteUpdates,該方法會調用 runWithPriority 並以 UserBlockingPriority 這個優先級去調用 dispatchEvent 方法。
dispatchEvent
該方法首先經過 getEventTarget 方法獲取事件 target 對象(nativeEventTarget),注意 event target 對應的是事件觸發的元素而不是事件綁定的元素,接着獲取 target 對象對應的 fiber 對象(targetInst),若是自身找不到就向上尋找,若是發現該節點尚未掛載到 dom 上那就把 targetInst 設置爲 null,最後調用 dispatchEventForPluginEventSystem 方法。
dispatchEventForPluginEventSystem
該方法先將上一步傳入的和事件相關的參數(topLevelType,nativeEvent,targetInst)存儲到一個對象上(bookKeeping),由於可能會屢次建立該對象因此 react 這裏使用了對象池的方式建立,而後調用了 batchedEventUpdates 方法傳入 handleTopLevel 和 bookKeeping,執行完成以後把 bookkeeping 對象歸還到對象池中。
batchedEventUpdates
該方法首先會判斷 isBatching 這個變量是否爲真,若是爲真就直接執行接受的第一個方法,也就是上一步傳入的 handleTopLevel,不然將 isBatching 置爲 true,而後去執行 batchedEventUpdatesImpl 方法傳入 handleTopLevel 和 bookkeeping,執行完成以後會執行 batchedUpdatesFinally 方法。
batchedEventUpdatesImpl
當咱們看到一個方法後面有 Impl 極可能它是經過依賴注入來實現的,這裏就是這樣,它會根據平臺來定義該方法的實現,在 dom 環境中咱們實際調用的方法是 batchedEventUpdates,該方法判斷當前的 workPhase 是否不是 NotWorking,若是不是 NotWorking 說明咱們可能已經處於 batch 階段,這個時候咱們只需執行傳入的方法而後退出,若是當前處於 NotWorking 狀態咱們將 workPhase 置爲 BatchedEventPhase 而後執行傳入的方法,執行完成以後恢復以前的 workPhase 而後執行 flushSyncCallbackQueue。
注:flushSyncCallbackQueue 該方法咱們在上面講過了
handleTopLevel
首先經過一個循環建立一個 ancestors 數組,通常來說裏面就只有一個對象就是 dom 節點對應的 fiber 對象,接着遍歷這個數組,獲取 eventTarget、事件名(topLevelType)和原生的事件對象(nativeEvent),將其傳入 runExtractedPluginEventsInBatch 方法中。
runExtractedPluginEventsInBatch
該方法會首先調用 extractPluginEvents 去建立一個 event 對象,而後再調用 runEventsInBatch 方法執行它。
extractPluginEvents
該方法會遍歷 plugins(就是注入階段建立的 plugins),而後調用 plugin(好比說 ChangeEventPlugin)的 extractEvents 方法,最後將建立好的 events 返回。
注:event 對象的生成咱們放到下一節來說。
runEventsInBatch
該方法會調用 forEachAccumulated 傳入要處理的 events 就是上一步傳入的 events 和 executeDispatchesAndReleaseTopLevel 方法,forEachAccumulated 是一個工具方法,它的做用只是對 events 中的每一項調用 executeDispatchesAndReleaseTopLevel 方法。
executeDispatchesAndReleaseTopLevel
該方法什麼都沒作只是調用了 executeDispatchesAndRelease 方法並把 event 對象傳給它。
executeDispatchesAndRelease
該方法會調用 executeDispatchesInOrder 而後判斷 event 是否須要持久保留,若是不須要就釋放掉它。
注:這裏就能夠解釋第 6 個問題,爲何 event 對象沒法保留,由於在事件處理函數執行完就把它銷燬了,除非你手動調用 event.persist() 方法。 源碼地址
executeDispatchesInOrder
終於到了事件最終執行的地方了,首先咱們要獲取 event 對象上的 dispatchListeners 和 dispatchInstances,而後遍歷 dispatchListeners 判斷 event 是否阻止冒泡了(isPropagationStopped)若是阻止冒泡了咱們就跳出循環,若是沒有阻止咱們就調用 executeDispatch 方法傳入對應的 listener(dispatchListeners[i])和 instance(dispatchInstances[i]),執行完後要將 dispatchListeners 和 dispatchInstances 清空。
executeDispatch
該方法首先獲取事件類型(event.type),設置 event.currentTarget 爲傳入的 instance 對應的 dom 節點,而後調用 invokeGuardedCallbackAndCatchFirstError 方法傳入 type、listener 和 event,其實該方法內部會作一些錯誤的捕獲,本質上就是直接調用了 listener 並將 event 傳入進去。
batchedUpdatesFinally
在 batchedEventUpdates 中執行完 batchedEventUpdatesImpl 就會執行 batchedUpdatesFinally,在這個方法中會首先判斷 restoreQueue 或 restoreTarget 是否爲空,若是不爲空就說明有受控組件須要處理,而後調用 flushDiscreteUpdatesImpl 對應 dom 環境下就是 flushDiscreteUpdates 會當即執行更新,接着會調用 restoreStateIfNeeded 該方法會將受控組件的 value 設置爲 props.value。
流程圖
React@16.8.6源碼淺析——事件機制(事件對象)
extractEvents
每個 event plugin 都有一個 extractEvents 方法用來生成事件對象,咱們以 ChangeEventPlugin 爲例進行講解。
首先獲取對應的 dom 節點,生命兩個變量 getTargetInstFunc, handleEventFunc,而後經過三個 if else 判斷來給 getTargetInstFunc 賦值,這裏的判斷是判斷當前 dom 節點應該使用什麼事件,好比對於 select 元素應該使用 change 事件,那它對應的 getTargetInstFunc 就爲 getTargetInstForChangeEvent
接着咱們調用 getTargetInstFunc 這個方法,這個方法內部判斷 event 事件是不是對應的事件,好比 getTargetInstForChangeEvent 判斷事件名是不是 change,若是是就返回 targetInst(對應的 fiber 對象),而後判斷返回的結果是否存在,若是存在就去執行 createAndAccumulateChangeEvent 建立 event 對象並返回,這裏這麼作事由於全部事件綁定都會去掉每個 plugin 的 extractEvents 方法,因此須要在內部判斷是否須要建立對應類型的 event 對象。
createAndAccumulateChangeEvent
該方法首先調用 SyntheticEvent.getPooled
方法建立一個 event 對象,建立的方式也採用對象池的方式,而後設置 event.type 爲 change,而後調用 enqueueStateRestore 和 accumulateTwoPhaseDispatches 最後將 event 返回。
SyntheticEvent
在 SyntheticEvent.getPooled 中若是對象池中沒有可用的對象就會調用合成事件(SyntheticEvent)構造函數來建立一個合成事件,這個事件對象是對原生事件對象的封裝,它實現了原生對象的方法(preventDefault、stopPropagation)也添加了本身的一些方法(persist),你能夠經過 nativeEvent 這個屬性獲取原生的事件對象。
enqueueStateRestore
將 target 放到 restoreQueue 數組中,設置 restoreTarget 爲 target 以便之後能夠恢復它的 value。
accumulateTwoPhaseDispatches
該方法會調用 forEachAccumulated 方法傳入 event 和 accumulateTwoPhaseDispatchesSingle,其實咱們以前講過 forEachAccumulated 這個方法,這就是一個工具方法,它只是去調用 accumulateTwoPhaseDispatchesSingle 並把 event 傳入進去。
accumulateTwoPhaseDispatchesSingle
該方法內部又調用了 traverseTwoPhase 傳入的參數是 fiber(targetInst)、accumulateDirectionalDispatches 和 event。
traverseTwoPhase
該方法會從傳入的 fiber 對象開始向上找到全部父節點爲 HostComponent 的 fiber 節點放入 path 數組中,而後遍歷 path 調用傳入的方法(accumulateDirectionalDispatches),第一次遍歷是從最後一個元素開始遍歷,accumulateDirectionalDispatches 方法傳入的第二個參數是 'captured',第二次遍歷是從第一個元素開始遍歷,傳遞的第二個參數是 'bubbled' 這兩個遍歷的順序正好符合捕獲和冒泡的順序,因此執行 listeners 的時候就不須要判斷哪一個是捕獲階段哪一個是冒泡階段,直接按照數組的順序執行便可,順便一提第一個參數是遍歷的那個 fiber 節點,第三個參數是 event 對象。
accumulateDirectionalDispatches
在這裏咱們終於要獲取咱們設置的事件處理函數了,首先咱們調用 listenerAtPhase 來獲取到 onChange 或 onChangeCapture 所綁定的事件處理函數(listener),而後將 listener 插入到 event._dispatchListeners,接着把對應的 fiber 對象插入到 event._dispatchInstances 中。
包含帶註釋的源碼、demos和流程圖
github.com/kwzm/learn-…