新手也可以讀懂的 React18 原始碼分析(一)

Daniel_Cheung 發表於 2022-06-15
React

哈嘍大家好,好久沒有跟大家以技術文章的形式見面了。最近本人在使用 React 18 做 Web 專案,所以抽空研究了一下 React 18 的原始碼。接下來想做一個 React 18 原始碼分析的系列,系列文章會以「 demo + 原始碼 」的形式由淺入深地跟大家一起探討新版本的 React 的技術實現,歡迎點贊 / 關注 / 拍磚,一起進步

本系列將預設使用 React v18.1.0 版本,預設執行環境為瀏覽器,讀者可自行到 GitHub 下載 React 原始碼,敬請留意

本章我們將探討 React 專案初始化的時候做了哪些事情:

Demo

我們使用 create-react-app 這個官方腳手架建立一個 React 專案,然後將 index.js 這個檔案修改為以下程式碼

import { createRoot } from 'react-dom/client';

function App() {
    return <h1>Hello dan!!!</h1>
}

const root = createRoot(document.getElementById('root'))
root.render(<App></App>)

執行 npm start 這個指令碼,如果你看到這個非常簡單(醜陋)的頁面顯示出來,那證明專案已經可以正常執行起來了

image.png

函式分析

從 Demo 我們看到,整個專案先通過 createRoot 這個函式建立一個 root 物件,再通過 rootrender 方法將 App 這個元件渲染到網頁上

createRoot

我們先看 createRoot 這個方法具體做了什麼事情。這個方法來自 react-dom 這個包。我們可以在原始碼中 packages / react-dom / src / client / ReactDOMRoot.js 中找到 createRoot 的具體實現(前面在 ReactDOM.js 做了一些關於環境的條件判斷,可先忽略

createRoot 函式有兩個引數 containeroptions,其中 options 是可選引數,本章為了簡單起見先不討論;

該函式大概實現的功能就是:

  1. 建立容器物件 FiberRootNode
  2. 事件委託處理
  3. 根據 FiberRootNode 物件返回 ReactDOMRoot 物件
// 刪除了一些干擾邏輯之後,createRoot 函式大致如下所示

function createRoot(
  container: Element | DocumentFragment,
  options?: CreateRootOptions,
): RootType {

  let isStrictMode = false;
  let concurrentUpdatesByDefaultOverride = false;
  let identifierPrefix = '';
  let onRecoverableError = defaultOnRecoverableError;
  let transitionCallbacks = null;
  
  // 建立容器物件 `FiberRootNode`
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
  markContainerAsRoot(root.current, container);
  
  // 事件監聽處理
  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;
  listenToAllSupportedEvents(rootContainerElement);
  
  // 根據容器物件 `root` 返回 `ReactDOMRoot` 物件
  return new ReactDOMRoot(root);
}

createContainer 函式完成了 建立容器物件 FiberRootNode 的工作。

這個方法來自 react-reconciler 這個包,可以在 packages / react-reconciler / src / ReactFiberReconciler.old.js 中找到,而這個方法內容也很簡單,直接呼叫了同層級 ReactFiberRoot.old.js 檔案的 createFiberRoot 方法來建立並返回一個 FiberRootNode 物件,也稱之為 Fiber 根結點

注意: 這裡的 Fiber 根結點Fiber 節點 是有區別的,詳細可看下面的各自的定義函式

這裡引入一個概念叫做 Fiber,目前我們只需要對他有個初步的印象:Fiber 節點用於儲存 React 元件節點資訊(包括 DOM節點,元件的屬性 / state / effect 等)。這裡可以簡單理解為一個儲存資訊的 JS 物件,後續章節會詳細介紹
function createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride,
identifierPrefix, onRecoverableError, transitionCallbacks) {
  // 建立 FiberRootNode 物件
  // tag 值為 ConcurrentRoot,定義在 packages/react-reconciler/src/ReactRootTags.js 檔案中;
  // tag === ConcurrentRoot === 1 ,表示 “根節點”
  var root = new FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError);
  // 建議看到這裡先彆著急看後面的程式碼,先看看下面 FiberRootNode 的定義和建構函式
  
  // 分割線 ************** 分割線 ************** 分割線 ************** 分割線 **************
  
  // 看完 FiberRootNode 的定義之後,接下來馬上要建立 Fiber 物件
  // createHostRootFiber 會呼叫 packages/react-reconciler/src/ReactFiber.old.js 檔案中的 createFiber 方法建立一個 `Fiber HostRoot節點`
  // `Fiber HostRoot節點` 就是一個 Fiber 物件,只是他的 Tag 等於 3,代表 `HostRoot`
  var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
  
  // 把 `Fiber 根結點` 的 current 屬性指向剛建立的 `Fiber HostRoot節點`
  root.current = uninitializedFiber;
  // `Fiber HostRoot節點` 的 stateNode 屬性指向 `Fiber 根結點`
  uninitializedFiber.stateNode = root;

  // cache 相關的可先忽略
  var initialCache = createCache();
  retainCache(initialCache);

  root.pooledCache = initialCache;
  retainCache(initialCache);
  
  // 初始化一個 state 物件
  var initialState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: initialCache,
      transitions: null
  };
  uninitializedFiber.memoizedState = initialState;
  
  // 初始化 `Fiber HostRoot節點` 的更新佇列
  // 給 Fiber 的 updateQueue 屬性賦值
  /**
  var queue = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
      interleaved: null,
      lanes: NoLanes
    },
    effects: null
  };
  fiber.updateQueue = queue;
  **/
  initializeUpdateQueue(uninitializedFiber);
  
  // 返回 `Fiber 根結點`
  return root;
}

FiberRootNode 的定義:

一個建構函式,物件內儲存Fiber 根節點的資訊,可先關注以下幾個

  • tag:標識節點型別,此處為 ConcurrentRoot
  • containerInfo:Fiber 根節點的 DOM 資訊,表示在這個 DOM 節點內部渲染當前 React 應用
  • current:儲存當前 Fiber 樹(後續章節會講到)
  • 其他屬性可以先大致掃一遍,重要是的後續會逐個介紹

    
    // 在`packages/react-reconciler/src/ReactFiberRoot.old.js`檔案中
    // FiberRootNode 建構函式
    function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError) {
    this.tag = tag;
    this.containerInfo = containerInfo;
    this.pendingChildren = null;
    this.current = null;
    ....
    // 省略其他屬性初始化
    ....

Fiber 節點的定義

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // tag 表示 Fiber 型別
  // packages/react-reconciler/src/ReactWorkTags.js 中定義
  this.tag = tag;
  // 寫在 jsx 元件上的 key 屬性
  this.key = key;
  // createElement的第一個引數,ReactElement 上的 type
  this.elementType = null;
  // 暫時可認為與 elementType 基本一致
  this.type = null;
  // fiber 節點對應的 DOM 節點
  this.stateNode = null;

  // Fiber 結構
  // 指向父節點
  this.return = null;
  // 指向第一個子節點
  this.child = null;
  // 指向兄弟節點
  this.sibling = null;
  // 一般如果沒有兄弟節點的話是 0 當某個父節點下的子節點是陣列型別的時候會給每個子節點一個 index
  this.index = 0;
  // 儲存 ref 屬性物件
  this.ref = null;
  
  // 新的 props 物件
  this.pendingProps = pendingProps;
  // 現有的 props 物件
  this.memoizedProps = null;
  // 儲存更新物件的佇列
  this.updateQueue = null;
  // 現有的 state 物件
  this.memoizedState = null;
  // 依賴物件
  this.dependencies = null;
    
  // 渲染方式
  // React 18 預設是 `ConcurrentMode`: 0b000001
  // packages/react-reconciler/src/ReactTypeOfMode.js 檔案中定義
  this.mode = mode;

  // Effects
  // effect 的 Flag,表明當前的 effect 是`替換`/ `更新` / `刪除` 等操作
  // packages/react-reconciler/src/ReactFiberFlags.js
  this.flags = NoFlags;
  // 子樹的 Flag 合集
  this.subtreeFlags = NoFlags;
  // 需要刪除的 fiber 節點
  this.deletions = null;
  
  // 更新渲染排程優先順序相關
  // packages/react-reconciler/src/ReactFiberLane.old.js 檔案中定義
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  
  // current 樹和 workInprogress 樹之間的相互引用
  // current 樹就是當前的 Fiber 樹
  // workInprogress 樹 就是正在更新的 Fiber 樹
  // 後續講到元件更新會詳細講到
  this.alternate = null;

  if (enableProfilerTimer) {
    // 。。。 省略
  }
 }

總結一下: createContainer 方法通過 createFiberRoot 建立並返回 Fiber 根節點FiberRootNode 物件。同時該物件的 current 屬性指向一個 Fiber HostRoot節點

markContainerAsRoot 方法在容器 DOM 節點上新增一個屬性 __reactContainer${randomKey},屬性的值指向Fiber HostRoot節點。以表明該 DOM 節點為當前 React 應用的容器節點。

listenToAllSupportedEvents 函式完成了 事件委託處理 的工作

packages/react-dom/src/events/DOMPluginEventSystem.js 檔案中,listenToAllSupportedEvents 函式接收一個入參:容器 DOM 節點(也就是createRoot 函式的第一個引數)

大致的原理是:React 18 把所有事件都委託到這個節點上面,一旦原生事件觸發之後,這個節點會根據事件型別以及優先順序,觸發對應 fiber 節點上的事件回撥函式。目前可以先了解一下 React 合成事件,後續章節講到事件機制會詳細講解

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    
    // allNativeEvents 是一個集合,儲存了 React 支援的所有事件
    // Set(81) {'abort', 'auxclick', 'cancel', 'canplay', 'canplaythrough', …}
    allNativeEvents.forEach(domEventName => {
       
      // 這裡最重要的函式就是 `listenToNativeEvent`
      // 用於將事件繫結到容器的 DOM 節點
      // 下面會根據是否響應捕獲階段分邏輯處理(可先忽略)
      // selectionchange 事件也單獨處理(可先忽略)
      // listenToNativeEvent 事件內部呼叫 addTrappedEventListener 函式
      
      if (domEventName !== 'selectionchange') {
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
    // 。。。省略 selectionchange 邏輯
  }
}

addTrappedEventListener 函式主要實現:根據事件獲取對應的優先順序,不同的優先順序在容器 DOM節點註冊不同的事件回撥函式

function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {

   // packages/react-reconciler/src/ReactEventPriorities.js 檔案儲存事件優先順序的定義

   // createEventListenerWrapperWithPriority邏輯 :
   // 1. 呼叫`getEventPriority` 函式實現從`事件名`到 `事件優先順序` 的轉化
   // 2. 根據 `事件優先順序` eventPriority 匹配不同的回撥函式:(dispatchDiscreteEvent,dispatchContinuousEvent, dispatchEvent)
   // 3. 返回事件回撥函式,賦值給 listener
   
  var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); // If passive option is not supported, then the event will be


  var isPassiveListener = undefined;

  if (passiveBrowserEventsSupported) {
    // 邏輯省略
  }

  targetContainer =  targetContainer;
  var unsubscribeListener; 

  // 事件繫結邏輯:
  // 呼叫 addEventCaptureListener(WithPassiveFlag) / addEventBubbleListener((WithPassiveFlag)) 函式進行事件繫結,
  // 內部呼叫原生方法 dom.addEventListener,實現事件繫結
  if (isCapturePhaseListener) {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
    } else {
      unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
    }
  } else {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventBubbleListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
    } else {
      unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
    }
  }
}

總結一下:listenToAllSupportedEvents 函式根據不同的事件型別,給容器 DOM節點註冊不同的回撥函式,子元件的所有事件都由該節點進行分發和觸發

返回 ReactDOMRoot 物件

例項化 ReactDOMRoot 物件,將 Fiber HostRoot節點 傳人建構函式中,儲存在物件的 _internalRoot 屬性

// ReactDOMRoot 建構函式
// 比較簡單,不解釋
function ReactDOMRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot;
}

createRoot 函式最後返回 ReactDOMRoot 物件,完成整個函式的所有工作。接下來,呼叫ReactDOMRoot 物件的 render 方法進行渲染工作

render

render 方法在 packages/react-dom/src/client/ReactDOMRoot.js 檔案中實現,入參是子元件,函式內部呼叫了 updateContainer 方法對子元件(App)進行渲染

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
  children: ReactNodeList,
): void {
  const root = this._internalRoot;
  if (root === null) {
    throw new Error('Cannot update an unmounted root.');
  }
  const container = root.containerInfo;
  // 重要步驟,重點分析
  updateContainer(children, root, null, null);
};

updateContainer 函式在 packages/react-reconciler/src/ReactFiberReconciler.old.js 檔案中定義,主要實現容器的排程任務

Lane 在 React 中用於表示任務的優先順序,目前只需要有個大概的瞭解,後續會詳細講解

schedule 是一個獨立的任務排程模組,目前只用於 React 內部,很多 API 還處於 unstable 狀態,後續有可能會提供給外部專案使用;這個模組也會在後續單獨講解,敬請期待


// 刪除一些干擾邏輯之後的 `updateContainer` 函式

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // 當前的 Fiber 樹
  const current = container.current;
  // 當前事件時間,呼叫 `now` 函式
  const eventTime = requestEventTime();
  // 獲取當前更新的 lane (任務排程優先順序)
  const lane = requestUpdateLane(current);

  // 獲取上下文
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    // 將 FiberRootNode 的 context 屬性指向 context
    container.context = context;
  } else {
    // 將 FiberRootNode 新的 context 屬性指向 context
    container.pendingContext = context;
  }

  // 建立一個`更新物件`:update
  /* var update = {
    eventTime: eventTime, // 事件時間
    lane: lane, // 排程優先順序
    tag: UpdateState, // 標識是 update / delete / ForceUpdate / ...
    payload: null, // payload,儲存 { element: React.element }
    callback: null, // 回撥函式
    next: null // 指向下一個 update
  };
  */
  const update = createUpdate(eventTime, lane);
 
  // 設定更新物件的 paylaod 屬性
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }
  // 把`更新物件` enqueue 到`更新佇列`
  // 後續在將元件更新的時候會細講,這塊還是比較重要的,目前可以大概瞭解
  enqueueUpdate(current, update, lane);
  
  // scheduleUpdateOnFiber 利用到 scheduler 這個包來進行任務排程
  // 通過將渲染方法 performConcurrentWorkOnRoot 註冊到 scheduler 的排程機制中
  // scheduler 會根據任務優先順序執行這個渲染方法,將 APP 元件最終渲染到頁面上
  const root = scheduleUpdateOnFiber(current, lane, eventTime);
  if (root !== null) {
    entangleTransitions(root, current, lane);
  }

  return lane;
}

至此,整個 React 專案的初始化過程就完成了,為了保證本章內容足夠簡單,很多細節都還沒有深入講解。不過,相信讀者讀完本章後,對整個初始化的過程也有了一定的瞭解。在後續的章節中,我們將針對 React 專案的其他階段進行深入剖析。同時也歡迎各位讀者可以在下方給我留言一起交流,共同進步

最後最後,希望疫情能夠早日結束,中國加油,世界加油 !!!