React16源碼解析(二)-創建更新

React源碼解析系列文章歡迎閱讀:
React16源碼解析(一)- 圖解Fiber架構
React16源碼解析(二)-創建更新
React16源碼解析(三)-ExpirationTime
React16源碼解析(四)-Scheduler
React16源碼解析(五)-更新流程渲染階段1
React16源碼解析(六)-更新流程渲染階段2
React16源碼解析(七)-更新流程渲染階段3
React16源碼解析(八)-更新流程提交階段
正在更新中...

在React中創建更新主要有下面三種方式:

1、ReactDOM.render() || hydrate
2、setState
3、forceUpdate

注:
除了上面的還有react 16.8 引進的hooks 中的useState,這個我們後續再講。
hydrate是服務端渲染相關的,這塊我並不會重點講解。

一、ReactDOM.render()

ReactDOM.render().png

調用legacyRenderSubtreeIntoContainer()

const ReactDOM: Object = {
  // ......
  render(
    element: React$Element<any>,//傳入的React組件
    container: DOMContainer,//掛載的容器節點
    callback: ?Function,//掛載後的回調函數
  ) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },
  // ......
}

legacyRenderSubtreeIntoContainer

1、root = 創建ReactRoot
2、調用root.render()

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,//null
  children: ReactNodeList,//傳入進來需要掛在的class component
  container: DOMContainer,//根節點
  forceHydrate: boolean,//false
  callback: ?Function,//掛載完成後的回調函數
) {
  // ......
  // 是否存在根節點   初次渲染是不存在根節點的
  let root: Root = (container._reactRootContainer: any);
  if (!root) {
    // 1、創建ReactRoot 賦值給container._reactRootContainer和root(這裏發生了很多事,一件很重要很重要的事   生成了fiber結構樹。。)
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    // ......
  } else {
    // ......
    // 2、調用root.render()
    root.render(children, callback);
  }
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

legacyCreateRootFromDOMContainer

1、清除所有子元素
2、創建 new ReactRoot節點

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,//根節點
  forceHydrate: boolean,//false
): Root {
  //  服務端渲染相關 是否合併原先存在的dom節點 一般是false
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // 1、清除所有子元素,通過container.lastchild循環來清除container的所有內容,因爲我們的屬於首次渲染,container裏邊不包含任何元素
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      // ......
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  // 2、創建 new ReactRoot節點
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

ReactRoot

從ReactRoot中, 我們把createContainer返回值賦給了 實例的_internalRoot, 往下看createContainer

function ReactRoot(
  container: Container,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  // 這裏創建了一個FiberRoot
  const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

createContainer

從createContainer看出, createContainer實際上是直接返回了createFiberRoot, 而createFiberRoot則是通過createHostRootFiber函數的返回值uninitializedFiber,並將其賦值在root對象的current上, 這裏需要注意一個點就是,uninitializedFiber的stateNode的值是root, 即他們互相引用。

創建一個RootFiber -> createHostRootFiber() -> createFiber() -> new FiberNode()

這裏創建的這個RootFiber裏面的絕大部分屬性都是初始值null或者是NoWork。所以具體代碼我就沒有貼出來了。
這裏我提下有意義的點:
RootFiber上的tag會被賦值爲 HostRoot。這個之後會用來判斷節點類型。

還有這裏創建的FiberRoot還有一個containerInfo置爲ReactDOM.render第二個參數傳入進來的容器節點。這個後續掛載的時候會用到。

export function createContainer(
  containerInfo: Container,
  isConcurrent: boolean,
  hydrate: boolean,
): OpaqueRoot {
  return createFiberRoot(containerInfo, isConcurrent, hydrate);
}

export function createFiberRoot(
  containerInfo: any,
  isConcurrent: boolean,
  hydrate: boolean,
): FiberRoot {
  // 1、創建了一個RootFiber
  const uninitializedFiber = createHostRootFiber(isConcurrent);
  // 2、互相引用
  // RootFiber.stateNode --> FiberRoot
  // FiberRoot.current --> RootFiber
  let root;
  root = {
    current: uninitializedFiber,
    containerInfo: containerInfo,
    // ......
  }
  uninitializedFiber.stateNode = root;
  // 3、return了這個FiberRoot
  return ((root: any): FiberRoot);
}

FiberRoot

這裏牽扯到兩種react中的數據結構,第一個FiberRoot,也就是上面createFiberRoot函數返回的對象。

type BaseFiberRootProperties = {|
  // root節點,render方法接收的第二個參數
  containerInfo: any,
  // 只有在持久更新中會用到,也就是不支持增量更新的平臺,react-dom不會用到
  pendingChildren: any,
  // 當前應用對應的Fiber對象,是Root Fiber
  current: Fiber,

  // 一下的優先級是用來區分
  // 1) 沒有提交(committed)的任務
  // 2) 沒有提交的掛起任務
  // 3) 沒有提交的可能被掛起的任務
  // 我們選擇不追蹤每個單獨的阻塞登記,爲了兼顧性能
  // The earliest and latest priority levels that are suspended from committing.
  // 最老和新的在提交的時候被掛起的任務
  earliestSuspendedTime: ExpirationTime,
  latestSuspendedTime: ExpirationTime,
  // The earliest and latest priority levels that are not known to be suspended.
  // 最老和最新的不確定是否會掛起的優先級(所有任務進來一開始都是這個狀態)
  earliestPendingTime: ExpirationTime,
  latestPendingTime: ExpirationTime,
  // The latest priority level that was pinged by a resolved promise and can
  // be retried.
  // 最新的通過一個promise被reslove並且可以重新嘗試的優先級
  latestPingedTime: ExpirationTime,

  // 如果有錯誤被拋出並且沒有更多的更新存在,我們嘗試在處理錯誤前同步重新從頭渲染
  // 在`renderRoot`出現無法處理的錯誤時會被設置爲`true`
  didError: boolean,

  // 正在等待提交的任務的`expirationTime`
  pendingCommitExpirationTime: ExpirationTime,
  // 已經完成的任務的FiberRoot對象,如果你只有一個Root,那他永遠只可能是這個Root對應的Fiber,或者是null
  // 在commit階段只會處理這個值對應的任務
  finishedWork: Fiber | null,
  // 在任務被掛起的時候通過setTimeout設置的返回內容,用來下一次如果有新的任務掛起時清理還沒觸發的timeout
  timeoutHandle: TimeoutHandle | NoTimeout,
  // 頂層context對象,只有主動調用`renderSubtreeIntoContainer`時纔會有用
  context: Object | null,
  pendingContext: Object | null,
  // 用來確定第一次渲染的時候是否需要融合
  +hydrate: boolean,
  // 當前root上剩餘的過期時間
  // TODO: 提到renderer裏面區處理
  nextExpirationTimeToWorkOn: ExpirationTime,
  // 當前更新對應的過期時間
  expirationTime: ExpirationTime,
  // List of top-level batches. This list indicates whether a commit should be
  // deferred. Also contains completion callbacks.
  // TODO: Lift this into the renderer
  // 頂層批次(批處理任務?)這個變量指明一個commit是否應該被推遲
  // 同時包括完成之後的回調
  // 貌似用在測試的時候?
  firstBatch: Batch | null,
  // root之間關聯的鏈表結構
  nextScheduledRoot: FiberRoot | null,
|};

Fiber

這裏就是createHostRootFiber函數返回的fiber對象。注意這裏其實每一個節點都對應一個fiber對象,不是Root專有的哦。

// Fiber對應一個組件需要被處理或者已經處理了,一個組件可以有一個或者多個Fiber
type Fiber = {|
  // 標記不同的組件類型
  // export const FunctionComponent = 0;
  // export const ClassComponent = 1;
  // export const IndeterminateComponent = 2; // Before we know whether it is function or class
  // export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
  // export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
  // export const HostComponent = 5;
  // export const HostText = 6;
  // export const Fragment = 7;
  // export const Mode = 8;
  // export const ContextConsumer = 9;
  // export const ContextProvider = 10;
  // export const ForwardRef = 11;
  // export const Profiler = 12;
  // export const SuspenseComponent = 13;
  // export const MemoComponent = 14;
  // export const SimpleMemoComponent = 15;
  // export const LazyComponent = 16;
  // export const IncompleteClassComponent = 17;
  tag: WorkTag,

  // ReactElement裏面的key
  key: null | string,

  // ReactElement.type,標籤類型,也就是我們調用`createElement`的第一個參數
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  // 異步組件resolved之後返回的內容,一般是`function`或者`class`
  type: any,

  // The local state associated with this fiber.
  // 跟當前Fiber相關本地狀態(比如瀏覽器環境就是DOM節點)
  stateNode: any,

  // 指向他在Fiber節點樹中的`parent`,用來在處理完這個節點之後向上返回
  return: Fiber | null,

  // 單鏈表樹結構
  // 指向自己的第一個子節點
  child: Fiber | null,
  // 指向自己的兄弟結構
  // 兄弟節點的return指向同一個父節點
  sibling: Fiber | null,
  index: number,

  // ref屬性
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的變動帶來的新的props
  pendingProps: any, 
  // 上一次渲染完成之後的props
  memoizedProps: any,

  // 該Fiber對應的組件產生的Update會存放在這個隊列裏面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的時候的state
  memoizedState: any,

  // 一個列表,存放這個Fiber依賴的context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用來描述當前Fiber和他子樹的`Bitfield`
  // 共存的模式表示這個子樹是否默認是異步渲染的
  // Fiber被創建的時候他會繼承父Fiber
  // 其他的標識也可以在創建的時候被設置
  // 但是在創建之後不應該再被修改,特別是他的子Fiber創建之前
  mode: TypeOfMode,

  // Effect
  // 用來記錄Side Effect
  // Don't change these two values. They're used by React Dev Tools.
  // export const NoEffect = /*              */ 0b00000000000;
  // export const PerformedWork = /*         */ 0b00000000001;

  // You can change the rest (and add more).
  // export const Placement = /*             */ 0b00000000010;
  // export const Update = /*                */ 0b00000000100;
  // export const PlacementAndUpdate = /*    */ 0b00000000110;
  // export const Deletion = /*              */ 0b00000001000;
  // export const ContentReset = /*          */ 0b00000010000;
  // export const Callback = /*              */ 0b00000100000;
  // export const DidCapture = /*            */ 0b00001000000;
  // export const Ref = /*                   */ 0b00010000000;
  // export const Snapshot = /*              */ 0b00100000000;

  // Update & Callback & Ref & Snapshot
  // export const LifecycleEffectMask = /*   */ 0b00110100100;

  // Union of all host effects
  // export const HostEffectMask = /*        */ 0b00111111111;

  // export const Incomplete = /*            */ 0b01000000000;
  // export const ShouldCapture = /*         */ 0b10000000000;
  effectTag: SideEffectTag,

  // 單鏈表用來快速查找下一個side effect
  nextEffect: Fiber | null,

  // 子樹中第一個side effect
  firstEffect: Fiber | null,
  // 子樹中最後一個side effect
  lastEffect: Fiber | null,

  // 代表任務在未來的哪個時間點應該被完成
  // 不包括他的子樹產生的任務
  expirationTime: ExpirationTime,

  // 快速確定子樹中是否有不在等待的變化
  childExpirationTime: ExpirationTime,

  // 在Fiber樹更新的過程中,每個Fiber都會有一個跟其對應的Fiber
  // 我們稱他爲`current <==> workInProgress`
  // 在渲染完成之後他們會交換位置
  alternate: Fiber | null,

  // 下面是調試相關的,收集每個Fiber和子樹渲染時間的

  actualDuration?: number,

  // If the Fiber is currently active in the "render" phase,
  // This marks the time at which the work began.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualStartTime?: number,

  // Duration of the most recent render time for this Fiber.
  // This value is not updated when we bailout for memoization purposes.
  // This field is only set when the enableProfilerTimer flag is enabled.
  selfBaseDuration?: number,

  // Sum of base times for all descedents of this Fiber.
  // This value bubbles up during the "complete" phase.
  // This field is only set when the enableProfilerTimer flag is enabled.
  treeBaseDuration?: number,

  // Conceptual aliases
  // workInProgress : Fiber ->  alternate The alternate used for reuse happens
  // to be the same as work in progress.
  // __DEV__ only
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
|};

root.render()

經過上面的步驟,創建好了ReactRoot。初始化完成了。下面開始root.render。
我們回到legacyRenderSubtreeIntoContainer函數,前面一堆講解的是調用legacyCreateRootFromDOMContainer方法我們得到了一個ReactRoot對象。reactRoot的原型上面我們找到了render方法:

ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,
): Work {
  // 這個就是我們上面創建的FiberRoot對象
  const root = this._internalRoot;
  // ......
  DOMRenderer.updateContainer(children, root, null, work._onCommit);
  return work;
};

updateContainer

這個函數裏面使用了 currentTime 和 expirationTime, currentTime是用來計算expirationTime的,expirationTime代表着優先級, 這個留在後續分析。後續緊接着調用了updateContainerAtExpirationTime。

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  // 這個current就是FiberRoot對應的RootFiber
  const current = container.current;
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

注:這個函數在ReactFiberReconciler.js裏面。

updateContainerAtExpirationTime

這裏將current(即Fiber實例)提取出來, 並作爲參數傳入調用scheduleRootUpdate

export function updateContainerAtExpirationTime(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  const current = container.current;
  // ......
  return scheduleRootUpdate(current, element, expirationTime, callback);
}

scheduleRootUpdate

這個函數主要執行了兩個操作:
1、創建更新createUpdate並放到更新隊列enqueueUpdate,創建更新的具體細節稍後再講哈。因爲待會我們會發現其他地方也用到了。
2、個是執行sheculeWork函數,進入React異步渲染的核心:React Scheduler,這個我後續文章詳細講解。

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  // ......
  // 1、創建一個update對象
  const update = createUpdate(expirationTime);
  update.payload = {element};

  // ......
  // 2、將剛創建的update對象入隊到fiber.updateQueue隊列中
  enqueueUpdate(current, update);

  // 3、開始進入React異步渲染的核心:React Scheduler
  scheduleWork(current, expirationTime);
  return expirationTime;
}

圖解

以上的過程我畫了張圖:
Fiber架構.png

二、setState

雖然我還沒有講解到class component 的渲染過程,但是這個不影響我現在要討論的內容~
如下我們調用this.setState方法的時候,調用了this.updater.enqueueSetState

Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

先不管this.updater什麼時候被賦值的,直接看到ReactFiberClassComponent.js中的enqueueSetState,這就是我們調用setState執行的enqueueSetState方法。

const classComponentUpdater = {
  // ......
  enqueueSetState(inst, payload, callback) {
    // inst 就是我們調用this.setState的this,也就是classComponent實例
    // 獲取到當前實例上的fiber
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    // 計算當前fiber的到期時間(優先級)
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    // 創建更新一個更新update
    const update = createUpdate(expirationTime);

    //payload是setState傳進來的要更新的對象
    update.payload = payload;

    //callback就是setState({},()=>{})的回調函數
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    // 把更新放到隊列UpdateQueue
    enqueueUpdate(fiber, update);

    // 開始進入React異步渲染的核心:React Scheduler
    scheduleWork(fiber, expirationTime);
  },
  // ......
}

看到上面的代碼,是不是發現和上面ReactDOM.render中scheduleRootUpdate非常的相似。其實他們就是同一個更新原理呢~

三、forceUpdate

廢話不多說,先上代碼。也是在ReactFiberClassComponent.js中classComponentUpdater對象中。

const classComponentUpdater = {
  // ......
  enqueueForceUpdate(inst, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);

    //與setState不同的地方
    //默認是0更新,需要改成2強制更新
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'forceUpdate');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  // ......
}

看到代碼的我們很開心,簡直就是enqueueSetState的孿生兄弟。我就不詳說啦。

到這裏我們總結一下上面三種更新的流程:
(1)獲取節點對應的fiber對象
(2)計算currentTime
(3)根據(1)fiber和(2)currentTime計算fiber對象的expirationTime
(4)根據(3)expirationTime創建update對象
(5)將setState中要更新的對象賦值到(4)update.payload,ReactDOM.render是{element}
(6)將callback賦值到(4)update.callback
(7)update入隊updateQueue
(8)進行任務調度

四、update對象

上面三種創建更新的方式中都創建了一個叫update的對象。那這個對象裏面到底是什麼呢?充滿好奇的我們點開createUpdate函數瞧瞧:

export function createUpdate(expirationTime: ExpirationTime): Update<*> {
  return {
    // 過期時間
    expirationTime: expirationTime,

    // export const UpdateState = 0;
    // export const ReplaceState = 1;
    // export const ForceUpdate = 2;
    // export const CaptureUpdate = 3;
    // 指定更新的類型,值爲以上幾種
    // 提下CaptureUpdate,在React16後有一個ErrorBoundaries功能
    // 即在渲染過程中報錯了,可以選擇新的渲染狀態(提示有錯誤的狀態),來更新頁面
    // 0更新 1替換 2強制更新 3捕獲性的更新
    tag: UpdateState,

    // 更新內容,比如`setState`接收的第一個參數
    // 第一次渲染ReactDOM.render接收的是payload = {element};
    payload: null,

    // 更新完成後對應的回調,`setState`,`render`都有
    callback: null,

    // 指向下一個更新
    next: null,

    // 指向下一個`side effect`,這塊內容後續講解
    nextEffect: null,
  };
}

就是返回了個簡單的對象。對象每個屬性的解釋我都寫在上面了。

五、UpdateQueue

UpdateQueue是一個單向鏈表,用來存放update。每個update用next連接。它的結構如下:

//創建更新隊列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    // 應用更新後的state
    // 每次的更新都是在這個baseState基礎上進行更新
    baseState,
    // 隊列中的第一個update
    firstUpdate: null,
    // 隊列中的最後一個update
    lastUpdate: null,
    // 隊列中第一個捕獲類型的update
    firstCapturedUpdate: null,
    // 隊列中最後一個捕獲類型的update
    lastCapturedUpdate: null,
    // 第一個side effect
    firstEffect: null,
    // 最後一個side effect
    lastEffect: null,
    // 第一個和最後一個捕獲產生的`side effect`
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

六、enqueueUpdate

創建了update對象之後,緊接着調用了enqueueUpdate,把update對象放到隊列enqueueUpdate。同時保證current和workInProgress的updateQueue是一致的,即fiber.updateQueue和fiber.alternate.updateQueue保持一致。

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // 保證current和workInProgress的updateQueue是一致的
  // alternate即workInProgress
  const alternate = fiber.alternate;
  // current的隊列
  let queue1;
  // alternate的隊列
  let queue2;
  // 如果alternate爲空
  if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    // 如果queue1仍爲空,則初始化更新隊列
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // 如果alternate不爲空,則取各自的更新隊列
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // 初始化
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(
          alternate.memoizedState,
        );
      } else {
        // 如果queue2存在但queue1不存在的話,則根據queue2複製queue1
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {
      if (queue2 === null) {
        // Only one fiber has an update queue. Clone to create a new one.
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // Both owners have an update queue.
      }
    }
  }
  if (queue2 === null || queue1 === queue2) {
    // 將update放入queue1中
    appendUpdateToQueue(queue1, update);
  } else {
    // 兩個隊列共享的是用一個update
    // 如果兩個都是空隊列,則添加update
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // 如果兩個都不是空隊列,由於兩個結構共享,所以只在queue1加入update
      // 在queue2中,將lastUpdate指向update
      appendUpdateToQueue(queue1, update);
      queue2.lastUpdate = update;
    }
  }

總結上面過程:
(1)queue1取的是fiber.updateQueue;

queue2取的是alternate.updateQueue

(2)如果兩者均爲null,則調用createUpdateQueue()獲取初始隊列
(3)如果兩者之一爲null,則調用cloneUpdateQueue()從對方中獲取隊列
(4)如果兩者均不爲null,則將update作爲lastUpdate

注:兩個隊列共享的是同一個update。

七、scheduleWork

上面三種更新最後都調用了scheduleWork(fiber, expirationTime)進入React異步渲染的核心:React Scheduler。後續文章詳細講解。

任世界紛繁複雜,仍舊保持可愛。
我是小柚子小仙女。文章如有不妥,歡迎指正~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章