React@16.8.6原理淺析(源碼淺析)

本系列文章總共三篇:javascript

課前小問題

  1. 爲何有時連續屢次 setState只有一次生效?
  2. 執行完setState獲取state的值能獲取到嗎?
  3. setState是同步的仍是異步的?
  4. 有些屬性爲何沒法從props裏面獲取到(如 ref)?
  5. 受控表單組件若是設置了value就沒法輸入內容是什麼緣由?
  6. 爲什麼 react 的事件對象沒法保留?

目錄結構

頂層目錄

React 採用 monorepo 的管理方式。倉庫中包含多個獨立的包,以便於更改能夠一塊兒聯調,而且問題只會出如今同一地方。html

  • packages 包含元數據(好比 package.json)和 React 倉庫中全部 package 的源碼(子目錄 src)。若是你須要修改源代碼, 那麼每一個包的 src 子目錄是你最須要花費精力的地方。
  • fixtures 包含一些給貢獻者準備的小型 React 測試項目。
  • build 是 React 的輸出目錄。源碼倉庫中並無這個目錄,可是它會在你克隆 React 而且第一次構建它以後出現。
  • 還有一些其餘的頂層目錄,可是它們幾乎都是工具類的,而且在貢獻代碼時基本不會涉及。

20191230134804.png

packages

  • react、react-dom 就不說了
  • react-reconciler 是 16.x 中新實現的 fiber reconciler 的核心代碼
  • scheduler 是 react 調度模塊的核心代碼,以前是放在 react-reconciler 中的,後來獨立了出來
  • events 是和 react 事件相關的代碼
  • shared 是不一樣 packages 公用的一些代碼
  • 其它 packages 咱們這裏不作探討

QQ截圖20191230204140.png

核心模塊

React 「Core」 中包含全部全局 React API,好比:java

  • React.createElement()
  • React.Component
  • React.Children

React 核心只包含定義組件必要的 API。它不包含協調算法或者其餘平臺特定的代碼。它同時適用於 React DOM 和 React Native 組件。React 核心代碼在源碼的 packages/react 目錄中。在 npm 上發佈爲 react 包。相應的獨立瀏覽器構建版本稱爲 react.js,它會導出一個稱爲 React 的全局對象。react

渲染器

React 最初只是服務於 DOM,可是這以後被改編成也能同時支持原平生臺的 React Native。所以,在 React 內部機制中引入了「渲染器」這個概念。
渲染器用於管理一棵 React 樹,使其根據底層平臺進行不一樣的調用。
渲染器一樣位於 packages/ 目錄下:git

reconciler

即使 React DOM 和 React Native 渲染器的區別很大,但也須要共享一些邏輯。特別是協調算法須要儘量類似,這樣可讓聲明式渲染,自定義組件,state,生命週期方法和 refs 等特性,保持跨平臺工做一致。
爲了解決這個問題,不一樣的渲染器彼此共享一些代碼。咱們稱 React 的這一部分爲 「reconciler」。當處理相似於 setState() 這樣的更新時,reconciler 會調用樹中組件上的 render(),而後決定是否進行掛載,更新或是卸載操做。
Reconciler 沒有單獨的包,由於他們暫時沒有公共 API。相反,它們被如 React DOM 和 React Native 的渲染器排除在外。
這部分源碼在 /packages/react-reconcilergithub

scheduler

在上一篇中我說在 react 中從產生更新到最終操做DOM這之間能夠叫作 reconciliation(協調)的過程,其實這中間還能夠再進行細分,其中產生的更新會放在一個更新隊列裏,如何調度這些更新讓它們進行下一步任務這個部分叫作 scheduler,而 react 採用叫作 Cooperative Scheduling (合做式調度)的方式來調度任務,簡單來講就是充分利用瀏覽器的空閒時間來執行任務,有空閒時間就執行對應的任務,沒有就把執行權交給瀏覽器,在瀏覽器中就是經過 requestIdleCallback 這個 API 來實現的,可是由於這個 API 存在的一些問題以及瀏覽器的兼容性問題,因此 react 經過 requestAnimationFrame、setTimeout 和 MessageChannel 來模擬了 requestIdleCallback 的行爲。如今 react 把這部分代碼單獨拎出來做爲一個 package。
這部分源碼在 /packages/scheduler 中。算法

事件系統

react 本身實現了一套事件系統,和原生的 DOM 事件系統相比減小了內存消耗,抹平了瀏覽器差別,那麼 react 是如何作到的呢,主要是採用瞭如下策略:npm

  • 採用事件委託的方式將事件都註冊在 document 對象上
  • react 內部建立了一個本身的事件對象 SyntheticEvent (合成事件),將原生事件進行了封裝,咱們在 react 中操縱的其實是這個封裝的對象
  • react 內部經過對象池的形式來建立和銷燬事件對象

這部分的源碼在 /packages/events 中。json

內置對象

FiberRoot

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();
  }
}
複製代碼

Fiber

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;
}
複製代碼

ExpirationTime

在上一篇中咱們說到爲了將任務排出優先級 react 最開始只是定死了幾個 Priority(優先級)變量,可是這樣會出現飢餓問題,低優先級的任務可能一直被打斷,後來 react 引入了 expirationTime(過時時間)的概念,這樣即便是低優先級的任務只要過時時間一到也能強制當即執行,那麼 expirationTime 是如何計算出來的呢,能夠參考以下的過程:

若是沒有看懂也沒有關係,我這裏計算了幾種狀況下的 expirationTime,你能夠找找規律:

image.png

咱們能夠發現對於同步任務好比 ReactDOM.render 來講,expirationTime 就是很大的整數(32位系統中的最大整數),若是是低優先級的異步任務那麼計算出來的時間以 25 爲基數進行增加,而若是是高優先級的異步任務(好比用戶交互)計算出來的時間是一 10 爲基數進行增加,且相同的 currentTime 高優先級的 expirationTime 要大於低優先級的 expirationTime,react 這麼作的目的:一是讓 25/10 ms 之內觸發的更新能有相同的過時時間,這樣就能夠批量更新以提高性能;二是讓高優先級的任務過時時間大於低優先級以提升它的優先級。

核心 API

createElement

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;
};
複製代碼

本節解決的問題

  1. 有些屬性爲何沒法從props裏面獲取到(如 ref)

ReactDOM.render

ReactDom.render 的源代碼位於 /react-dom/src/client/ReactDOM.js

總體流程

前期準備階段

前期準備階段所作的事情歸納起來就三點:

  1. 建立 fiberRoot 和 rootFiber 對象
  2. 計算 expirationTime
  3. 建立 update 並將更新放入隊列中

schedule

見下方

render

見下方

commit

見下方

setState

總體流程

前期準備階段

前期準備階段所作的事情歸納起來就三點:

  1. 計算 expirationTime
  2. 建立 update 並將更新放入隊列中

schedule

須要注意的是若是是經過 react element 上綁定的事件函數裏面調用的 setState 方法,會在執行 setState 方法以前設置 workPhase = BatchedEventPhase;,因此在 scheduleUpdateOnFiber 方法中會進入下圖的分支。

image.png

而且在 setState 執行完以後纔會調用 flushSyncCallbackQueue 執行更新,此時採用調用  renderRoot
image.png

而若是不是經過事件機制調用的 setState 會當即執行  flushSyncCallbackQueue,就會當即 renderRoot
image.png

詳情 見下方

render

見下方

commit

見下方

本節解決的問題

  1. 爲何有時連續屢次 setState只有一次生效?
  2. 執行完setState獲取state的值能獲取到嗎?
  3. setState是同步的仍是異步的?

Schedule

概述

找到觸發更新節點對應的 fiberRoot 節點,而後調對該節點的更新,分爲兩種狀況:同步和異步,同步又能夠分爲兩種:是不是 LegacyUnbatchedPhase,若是是就不須要調度直接進入下一階段(render phase),若是不是就放到下一幀當即執行,對於異步任務則須要根據優先級算出一個過時時間,而後再和隊列裏排隊的任務進行比較找出立刻要過時的那個任務在下一幀進入下一個階段執行(render phase)。

總體流程

核心方法

scheduleWork

  • 判斷嵌套更新,超過 50 次的嵌套更新就報錯
  • 找到 fiberRoot 對象並設置 expirationTime
  • 判斷是否有高優先級的任務打斷當前任務
  • 根據當前 expirationTime 是否等於 Sync 分爲兩個大的階段假設咱們就把它們叫作同步階段和異步階段
    • 同步階段又能夠分爲兩種狀況
      • workPhase 等於 LegacyUnbatchedPhase 時調用 renderRoot
      • 其它 workPhase 調用 scheduleCallbackForRoot,而且當 workPhase 爲 NotWorking 時調用 flushSyncCallbackQueue
    • 異步階段經過 getCurrentPriorityLevel 獲取 priorityLevel,而後調用 scheduleCallbackForRoot

流程圖:React@16.8.6原理解讀——調度(一)

scheduleCallbackForRoot

  • 判斷當前 root.callbackNode 是否比新傳入的任務優先級低,若是優先級低那麼取消那個任務
  • 若是新任務的 expirationTime 是 Sync 就調用 scheduleSyncCallback
  • 若是新任務的 expirationTime 不是 Sync 就計算出還剩多長時間任務過時(timeout)而後調用 scheduleCallback

流程圖:React@16.8.6源碼解析——調度(二)

scheduleSyncCallback

將傳入的 callback 放入 syncQueue 中,而後調用 Scheduler_scheduleCallback 設置優先級爲 Scheduler_ImmediatePriority,callback 爲 flushSyncCallbackQueueImpl
流程圖:React@16.8.6源碼解析——調度(二)

scheduleCallback

將傳入的 reactPriorityLevel 轉換爲 schedule 中的 priorityLevel 而後調用 Scheduler_scheduleCallback
流程圖:React@16.8.6源碼解析——調度(二)

Scheduler_scheduleCallback(unstable_scheduleCallback)

根據傳入的 priorityLevel 和 timeout 計算出新的 expirationTime,根據新的 expirationTime 和傳入的 callback 建立一個 newNode,而後看看當前第一個等待調度的任務(firstCallbackNode)是不是空,若是是空就把 newNode 做爲 firstCallbackNode 而後調用 scheduleHostCallbackIfNeeded,不然就比較 newNode 的過時時間是不是當前列表中最先的,若是是也把它設置爲 firstCallbackNode 而後執行 scheduleHostCallbackIfNeeded
流程圖:React@16.8.6源碼解讀——調度(三)

scheduleHostCallbackIfNeeded

若是 firstCallbackNode 不爲空就執行 requestHostCallback(flushWork, expirationTime);
流程圖:React@16.8.6源碼解讀——調度(三)

requestHostCallback

設置 scheduledHostCallback 爲傳入的 callback 若是當前有一個 callback 正在執行或過時時間小於 0 則當即調用 port.postMessage 表示當即執行 scheduledHostCallback 並傳入是否超時(didTimeout)不然調用 requestAnimationFrameWithTimeout(animationTick);
流程圖:React@16.8.6源碼解讀——調度(四)

requestAnimationFrameWithTimeout

模擬 requestAnimationFrame 在下一幀執行傳入的 callback,由於 requestAnimationFrame 在頁籤是後臺運行時不執行,因此又經過 setTimeout 設置了一個了一個定時器來解決這個問題,若是 requestAnimationFrame 生效了就取消定時器,反之亦然。
流程圖:React@16.8.6源碼解讀——調度(四)

animationTick

若是 scheduledHostCallback 不爲空就接着調用 requestAnimationFrameWithTimeout 安排下一幀的任務 不然就是沒有等待的任務了就退出,計算出下一幀運行的時間(nextFrameTime),若是小於 8ms 就設置爲 8ms ,接着計算出當前幀的過時時間(frameDeadline)若是有任務就接着調用 port.postMessage。
流程圖:React@16.8.6源碼解讀——調度(四)

port.onmessage

port.postMessage 後就能夠被 port.onmessage 接收到,收到以後判斷當前幀是否還有剩餘時間,若是沒有檢查下要執行的任務(scheduledHostCallback)是否超時,超時就設置 didTimeout = true 沒超時就接着調用 requestAnimationFrameWithTimeout,而後退出;若是剩餘時間還有就執行 scheduledHostCallback(didTimeout)。
流程圖:React@16.8.6源碼解讀——調度(四)

flushWork

它接受的參數就是 port.onmessage 傳入的 didTimeout,若是 didTimeout 爲真(說明當前幀沒有時間了)判斷第一個要執行的任務 (firstCallbackNode)的 expirationTime 是否小於當前時間,小於的話就不斷執行 flushFirstCallback 直到 firstCallbackNode 爲空或 firstCallbackNode.expirationTime 大於等於當前時間;若是 didTimeout 爲假(說明當前幀還有時間)那就不斷執行 flushFirstCallback 直到 firstCallbackNode 爲空或當前幀已經沒有剩餘時間了,最後不管是上面何種狀況都會再執行 scheduleHostCallbackIfNeeded 判斷一下是否還有須要執行的任務。
流程圖:React@16.8.6源碼解讀——調度(六)

flushFirstCallback

執行鏈表裏的第一個任務(firstCallbackNode)並傳入是否超時(didUserCallbackTimeout),這裏 ImmediatePriority 也會當作超時,firstCallbackNode 可能會再返回一個 callback,將新的回調函數插入到列表中,根據它的到期時間排序,若是新的回調是列表中優先級最高的就調用 scheduleHostCallbackIfNeeded 安排下一次執行。
流程圖:React@16.8.6源碼解析——調度(五)

flushSyncCallbackQueueImpl

對於 scheduleSyncCallback 來講最終執行的** **scheduledHostCallback 就是 flushSyncCallbackQueueImpl
這個方法中就是循環執行 syncQueue 數組中的任務。
流程圖:React@16.8.6源碼解析——調度(五)

flushSyncCallbackQueue

還記得最開始若是處於同步階段而且 workPhase 爲 NotWorking 時執行完 scheduleCallbackForRoot 就會調用這個方法,這個方法首先去調用 Scheduler_cancelCallback 取消 immediateQueueCallbackNode,接着會執行 flushSyncCallbackQueueImpl 也就是上面那個方法,immediateQueueCallbackNode 的 callback 對應的就是 flushSyncCallbackQueueImpl,因此我認爲這個方法就是當即調用 flushSyncCallbackQueueImpl 去執行 syncQueue 中的回調任務而不是等待下一幀執行。

Render(reconciliation)

概述

從 rootFiber 開始循環遍歷 fiber 樹的各個節點,對於每一個節點會根據節點類型調用不一樣的更新方法,好比對於 class 組件會建立實例對象,調用 updateQueue 計算出新的 state,執行生命週期函數等,再好比對於 HostComponent 會給它的 children 建立 fiber 對象,當一側子樹遍歷完成以後會開始執行完成操做,即建立對應 dom 節點並添加到父節點下以及設置父節點的 effect 鏈,而後遍歷兄弟節點對兄弟節點也執行上述的更新操做,就這樣將整棵樹更新完成以後就能夠進入下一階段(commit phase)。

總體流程

核心方法

renderRoot

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)

prepareFreshStack

這個方法是任務開始以前的一些準備工做,以前一直好奇 workInProgress 是那裏初始化的,其實就是在這裏,這裏會調用 createWorkInProgress 根據 rootFiber 拷貝出一個 workInProgress 的 fiber 對象來,接着還會設置一些其它全局變量。

workLoopSync

workLoopSync 比較簡單內部循環調用 performUnitOfWork,判斷條件是 performUnitOfWork 的返回值 workInProgress 是否爲空。

workLoop

workLoop 和 workLoopSync 比較相似,區別就是循環的終止條件新增了 shouldYield,shouleYield 方法判斷當前是否應該被打斷,若是當前任務沒有超時而且任務的時間片已經不夠用了就會被打斷,這時候 workLoop 循環就會終止。

prerformUnitOfWork

performUnitOfWork 是 workLoopSync 和 workLoop 兩個方法都會調用的方法,在其內部會調用 beginWork 方法,beginWork 方法會返回下一個要執行的任務(next),若是 next 爲空表示已經遍歷到葉子節點了,則調用 completeUnitOfWork 能夠執行完成邏輯了,關於這塊的執行細節能夠參考上一篇。

beginWork

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)

completeUnitOfWork

當 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)

completeWork

這個方法內部就是一個 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)
**

Commit

概述

提交階段主要作的事情就是對 render 階段產生的 effect 進行處理,處理分爲三個階段

  • 階段一:在 dom 操做產生以前,這裏主要是調用 getSnapshotBeforeUpdate 這個生命週期方法
  • 階段二:處理節點的增刪改,對於刪除操做須要作特殊處理要同步刪除它的子節點而且調用對應的生命週期函數
  • 階段三:dom 操做完成以後還須要調用對應的生命週期函數,而且執行 updateQueue 中的 callback

總體流程

核心方法

commitRoot

commitRoot 接受 fiberRoot 對象,而後調用 commitRootImpl 方法並把 fiberRoot 對象傳遞給它,若是執行的過程當中有 Passive effect 產生就會調用 flushPassiveEffects 去執行這些 effect,Passive effect 和 hooks 有關,這裏暫且不表。
流程圖:React@16.8.6源碼淺析——提交階段(commitRoot)

commitRootImpl

首先根據傳入的 fiberRoot 獲取到 finishedWork 也就是以前階段完成的 rootFiber,而後重置一些變量,接着處理 effect 鏈,若是 rootFiber 也有 effect,那也須要加到 effect 鏈上,接着經過三個循環來分別處理這些 effect:

  • 第一個循環對每個 effect 調用了 commitBeforeMutationEffects 方法
  • 第二個循環對每個 effect 調用了 commitMutationEffects 方法
  • 第三個循環對每個 effect 調用了 commitLayoutEffects 方法

最後調用 flushSyncCallbackQueue 若是 syncQueue 還有其它任務則執行它們
流程圖:React@16.8.6源碼淺析——提交階段(commitRoot)

commitBeforeMutationEffects

該方法會判斷 effect 上是否有 Snapshot,Snapshot 會在 render 階段判斷 class 組件是否有 getSnapshotBeforeUpdate 這個生命週期時加上,若是有就調用 commitBeforeMutationEffectOnFiber,在這個方法裏會判斷 fiber 的類型,若是是 function 組件會調用 commitHookEffectList (這裏我不太明白爲何 function 組件會有 Snapshot),若是是 class 組件就會執行 getSnapshotBeforeUpdate 這個方法並將返回的結果設置到實例的 __reactInternalSnapshotBeforeUpdate 屬性上,這個再 componentDidUpdate 的時候會用到。
流程圖:React@16.8.6源碼淺析——提交階段(commitBeforeMutationEffects)

commitMutationEffects

在該方法中會對一些和 dom 操做相關的 effect 進行執行:

  • ContentReset:表示重置節點中的文本
  • Ref:以前設置的 ref 要解除關聯
  • Placement:找到其兄弟節點和父節點,若是父節點是 dom 節點那麼就經過 insertBefore 將目標節點插入到兄弟節點以前,若是父節點不是 dom 節點(HostRoot/HostPortal)就找到它們對應的 container 再執行 inserBefore。
  • PlacementAndUpdate:說明既有 Placement 又有 Update,先調用 Placement 對應的操做,而後調用 commitWork,commitwork 會對 dom 節點進行屬性的設置。
  • Update:調用 commitWork。
  • Deletion:刪除操做稍顯複雜,由於刪除的節點其下可能還有其它節點,因此須要遍歷其子樹執行刪除操做,內部是一個遞歸的過程,對於 dom 節點會調用 removeChild,對於 class 組件會先解除 ref 的引用(safelyDetachRef)而後調用 componentWillUnmount。

注:

  • 關於 Placement 對於父節點不是 dom 節點的插入,能夠參考這個流程圖
  • 刪除操做的遍歷方式相似樹的深度優先遍歷

流程圖:React@16.8.6源碼淺析——提交階段(commitMutationEffects)
**

commitLayoutEffects

該方法是整個 commit 階段最後一個循環執行的方法,內部主要調用兩個方法 commitLayoutEffectOnFiber 和 commitAttachRef,第一個方法內部是一個 switch 對於不一樣的節點進行不一樣的操做:

  • ClassComponent:執行 componentDidMount 或 componentDidUpdate,最後調用 commitUpdateQueue 處理 update,這裏不一樣於 processUpdateQueue,這裏主要處理 update 上面的 callback,好比 setState 方法的第二個參數或是生成異常 update 對應的 callback(componentDidCatch)
  • HostRoot:也會調用 commitUpdateQueue,由於 ReactDOM.render 方法的第三個參數也能夠接受一個 callback
  • HostComponent:判斷若是有 autoFocus 則調用 focus 方法來獲取焦點
  • 其它類型暫且不表

至於 commitAttachRef 方法其實就是將節點的實例對象掛載到 ref.current 上
流程圖:React@16.8.6源碼淺析——提交階段(commitLayoutEffects)

合成事件

注入

注入階段是在 ReactDOM.js 文件一加載就去執行的,主要目的就是建立三個全局變量一邊之後使用

源碼地址

github.com/LFESC/react…

流程圖

React@16.8.6源碼淺析——事件機制(注入)

核心方法

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 而不是它本身,這樣有利於減小內存開銷提升性能,而對於交互類型和非交互類型的事件會綁定不一樣的事件處理函數。

流程圖

React@16.8.6源碼淺析——事件機制(監聽)

核心方法

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,表示在捕獲階段觸發仍是在冒泡階段觸發。

觸發

流程圖

React@16.8.6源碼淺析——事件機制(觸發)

核心方法

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。

本節解決的問題

  1. 受控表單組件若是設置了value就沒法輸入內容是什麼緣由?
  2. 爲什麼 react 的事件對象沒法保留?

事件對象

流程圖
React@16.8.6源碼淺析——事件機制(事件對象)

源碼地址

github.com/LFESC/react…

核心方法

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 中。

Github

包含帶註釋的源碼、demos和流程圖
github.com/kwzm/learn-…

相關文章
相關標籤/搜索