React技術揭密學習(二)

學習React技術揭祕

Render階段 - 協調器 - Reconciler工作

  • render階段開始於performSyncWorkOnRoot或者performConcurrentWorkOnRoot.
  • 取決於同步還是異步更新
// performSyncWorkOnRoot會調用該方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot會調用該方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
  • shouldYield: 判斷瀏覽器當前是否存在剩餘時間.
  • workInProcess: 代表當前已經創建的workInProcess fiber
  • performUnitOfWork:
    • 創建下一個Fiber節點, 並賦值給workInProcess
    • 並將workInProcess與已經創建的Fiber節點鏈接起來, 構成一個fiber樹
  • Fiber Reconciler重構了Stack Reconciler, 通過遍歷實現可中斷的遞歸
  • performUnitOfWork. 工作分爲"遞"和"歸"
  • "遞"階段
    • rootFiber開始向下深度優先遍歷. 爲每個fiber節點調用beginWork方法
    • 該方法會根據傳入的Fiber節點創建子Fiber節點, 並將兩個Fiber節點鏈接起來.
    • 如果有兄弟節點, 並根據節點, 依次創建兄弟節點
    • 遍歷到葉子節點時, 會進入歸階段.
  • "歸"階段
    • 歸節點調用completeWork階段, 處理fiber節點
    • 某個fiber節點完成執行完completeWork
      • 如果其存在兄弟Fiber節點, 會進入兄弟節點的遞階段
      • 如果不存在兄弟fiber節點, 會進入父級Fiber的歸階段
  • "遞"和"歸"會交替執行, 直到"歸"到rootFiber. render階段的工作完成

單一文本的葉子節點不會執行beginWork/completeWork

beginWork

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函數體
}
  • current: 當前組件對應的Fiber節點在上一次更新時的Fiber節點. workInProcess.alternate

  • workInProcess: 當前組件對應的fiber節點

  • renderLanes: 優先級相關

  • 區分mount還是update

    • mount時: current === null
    • update時: current !== null
  • begin工作分爲兩部分:

    • update時: current節點存在, 滿足一定條件, 進行復用. 克隆current.childworkInProcess.child
    • mount時: 除了fiberRootNode以外, current === null. 根據fiber.tag不同, 創建不同類型的子fiber節點
  • Update: 滿足如下情況. -> didReceiveUpdate = false;直接複用上一次的fiber節點

    • oldProps === newProps && workInProcess.type === current.type不變
    • includesSomeLane(renderLanes, updateLanes), 當前節點的優先級不夠
  • Mount:

    • 根據不同類型的tag, 進入不同類型的Fiber的創建邏輯
    • 最終都會執行reconcileChildren: 根據fiber節點創建子fiber節點
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update時:如果current存在可能存在優化路徑,可以複用current(即上一次更新的Fiber節點)
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // 省略處理
      }
       // 複用current
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  }


  // mount時:根據tag不同,創建不同的子Fiber節點
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他類型
  }
}
  • reconcileChildren工作:
    • mount: 創建新的子fiber節點
    • update: 會將當前fiber節點和上次更新的fiber節點對比(diff), 產生結果比較新的fiber節點
    • 最終都會生成新的子fiber節點, 賦值給workInProcess.child, 作爲本次beginWork的返回值
    • 並作爲下一次: performUnitOfWork執行時workInProcess的傳參
    • reconcileChildFibers會爲生成的Fiber節點帶上effectTag屬性
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 對於mount的組件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 對於update的組件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}
  • effectTag
    • render(Reconciler)階段時在內存中工作的. 工作接受後, 通知Renderer進行dom操作
    • dom的操作保存在effectTag
  • 通知RendererFiber節點對應的dom插入到頁面中需要滿足兩個條件
    • fiber.stateNode存在, 即fiber節點保存了對應的dom節點
    • Fiber節點上存在Placement effectTag
  • fiber.stateNode會在completeWork中完成
  • mount時只用rootFiber會被賦值Placement effectTag. commit階段一次性插入完成
  • beginWork流程圖

commitWork

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略
  • HostComponent: 原生組件對應的fiber節點
  • Update時:
    • Fiber節點對應的dom節點已經存在了, 不需要生成dom節點, 主要處理props
      • onClick, onChange等回調函數的註冊
      • 處理style prop
      • 處理DANGEROUSLY_SET_INNER_HTML prop
      • 處理children prop
    • 主要調用updateHostComponent
      • 處理完的props賦值給workInProcess.updateQueue
      • 並最終會在commit階段(Renderer)渲染在頁面上
      • workInProgress.updateQueue = (updatePayload: any);
      • updatePayload爲數組, 偶數爲變化的prop key, 奇數爲prop value
  • Mount時
    • 主要邏輯:
      • fiber節點增加對應的dom節點
      • 將子孫dom節點插入到剛生成的dom節點上
      • 與update時中的updateHostComponent類似的props處理過程
  • completeWork階段屬於歸階段函數. 每次調用appendAllChildren時, 都會將已生成的子孫dom節點插入到當前生成的dom節點下.
  • "歸"到rootFiber時, 已經有一顆已經構建好的內存中的dom樹
case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update的情況
    updateHostComponent(
      current,
      workInProgress,
      type,
      newProps,
      rootContainerInstance,
    );
  } else {
    // mount的情況
    // ...省略服務端渲染相關邏輯
    const currentHostContext = getHostContext();
    // 爲fiber創建對應DOM節點
    const instance = createInstance(
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
        workInProgress,
      );
    // 將子孫DOM節點插入剛生成的DOM節點中
    appendAllChildren(instance, workInProgress, false, false);
    // DOM節點賦值給fiber.stateNode
    workInProgress.stateNode = instance;

    // 與update邏輯中的updateHostComponent類似的處理props的過程
    if (
      finalizeInitialChildren(
        instance,
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
      )
    ) {
      markUpdate(workInProgress);
    }
  }
  return null;
}
  • effectList
  • 解決需要所有fiber節點, 找到標記effectTagfiber
  • completeWork的上層函數completeUnitOfWork中:
  • 每次執行完completeWork, 且存在effectTagfiber節點, 會保存在effectList單向鏈表中
  • 第一個節點保存在fiber.firstEffect
  • 最後一個節點保存在fiber.lastEffect
  • 在"歸"階段, 所有effectTag的Fiber節點, 都會追加到這條鏈表中
  • 最終形成一條以rootFiber.firstEffect爲起點的effectList
  • commit階段只需要遍歷effectList即可
                       nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
  • render階段全部完成工作後.

    • performSyncWorkRoot函數中, fiberNode傳遞給commitRoot.
    • 開啓commit階段的工作
  • completeWork流程圖

Commit階段 - 渲染器 - Renderer器工作

  • fiberRootNode作爲傳參
  • fiberRootNode.firstEffect上保存了一條需要執行副作用Fiber節點單向鏈表
  • 這些fiber節點的updateQueue上保存了變化的props
  • 這些副作用對應的dom操作, 在commit階段執行
  • 一些聲明週期鉤子函數(componentDidXXX), hook(useEffect)在commit階段執行.
  • commit階段的主要工作
    • before mutation階段: 執行dom操作前
    • mutation階段: 執行dom操作
    • layout階段: 執行dom操作後
  • before mutation之前layout之後還有一些工作
    • useEffect的觸發
    • 優先級相關重置
    • ref的綁定的解綁
  • before mutation之前
    • 變量賦值, 狀態重置
    • 最後會賦值一個firstEffect, commit三個子階段都會用到
  • layout之後
    • useEffect相關處理
    • 性能追蹤相關
    • commit出發一些生命週期函數, hook

before mutation

  • 遍歷effectList並調用commitBeforeMutationEffects函數處理
// 保存之前的優先級,以同步優先級執行,執行完畢後恢復之前優先級
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);

// 將當前上下文標記爲CommitContext,作爲commit階段的標誌
const prevExecutionContext = executionContext;
executionContext |= CommitContext;

// 處理focus狀態
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;

// beforeMutation階段的主函數
commitBeforeMutationEffects(finishedWork);

focusedInstanceHandle = null;
  • commitBeforeMutationEffects
    • 處理DOM節點渲染/刪除後的autoFocus, blur的邏輯
    • 調用getSnapshotBeforeUpdate生命週期鉤子
    • 調度useEffect
function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const current = nextEffect.alternate;

    if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
      // ...focus blur相關
    }

    const effectTag = nextEffect.effectTag;

    // 調用getSnapshotBeforeUpdate
    if ((effectTag & Snapshot) !== NoEffect) {
      commitBeforeMutationEffectOnFiber(current, nextEffect);
    }

    // 調度useEffect
    if ((effectTag & Passive) !== NoEffect) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        // 以某個優先級調用一個回調函數
        scheduleCallback(NormalSchedulerPriority, () => {
          // 調用useEffect
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  • 調用getSnapshotBeforeUpdate
    • render階段的任務可能被中斷重新開始, componentWillXXX鉤子可能被多次觸發. 變爲不安全的
    • 使用新的getSnapshotBeforeUpdate, 在commit階段內同步執行, 所以只會觸發一次
  • 異步調度
    • flushPassiveEffects方法從全局變量rootWithPendingPassiveEffects獲取effectList
    • effectList包含了需要執行副作用的Fiber節點
      • 插入dom節點(PLACEMENT)
      • 更新dom節點(UPDATE)
      • 刪除dom節點(DELETION)
      • 當一個FunctionComponent含有useEffect或者useLayoutEffect時, 它的fiber節點也會含有Passive effectTag
    • flushPassiveEffect內部遍歷effectList, 執行effect回調函數
    • useEffect分成三步:
      1. before mutation階段scheduleCallback中調度flushPassiveEffects
      2. layout階段之後, 將effectList賦值給rootWidthPendingPassiveEffects
      3. scheduleCallBack觸發flushPassiveEffects, flushPassiveEffects內部遍歷rootWidthPendingPassiveEffects
    • 原因:
      • 在瀏覽器完成渲染和繪製以後, 傳給useEffect的函數會延遲調用.
      • 使得它試用於許多瀏覽器常見的副作用場景, 比如設置訂閱和事件處理等情況
      • 因此不應該在函數中執行阻塞瀏覽器更新屏幕的操作
      • 防止同步執行時阻塞瀏覽器渲染

mutation階段

  • 同樣遍歷effectList, 執行函數, commitMutationEffects
nextEffect = firstEffect;
do {
  try {
      commitMutationEffects(root, renderPriorityLevel);
    } catch (error) {
      invariant(nextEffect !== null, 'Should be working on an effect.');
      captureCommitPhaseError(nextEffect, error);
      nextEffect = nextEffect.nextEffect;
    }
} while (nextEffect !== null);
  • commitMutationEffects
    • 根據ContentRest effectTag重置文本節點
    • 更新ref
    • 根據effectTag分別處理, 其中effectTag分別包括(Placement|Update|Deletion|Hydrating)
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // 遍歷effectList
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;
    // 根據 ContentReset effectTag重置文字節點
    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect);
    }
    // 更新ref
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }
    // 根據 effectTag 分別處理
    const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
    switch (primaryEffectTag) {
      // 插入DOM
      case Placement: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        break;
      }
      // 插入DOM 並 更新DOM
      case PlacementAndUpdate: {
        // 插入
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        // 更新
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // SSR
      case Hydrating: {
        nextEffect.effectTag &= ~Hydrating;
        break;
      }
      // SSR
      case HydratingAndUpdate: {
        nextEffect.effectTag &= ~Hydrating;
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 更新DOM
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 刪除DOM
      case Deletion: {
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break;
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  • Placement effect: fiber節點對應的dom節點需要插入到頁面中
    1. 獲取父級dom節點
    2. 獲取fiber的兄弟節點, getHostSibling非常耗時, 因爲同一個fiber節點不止包含HostComponent*
    3. 根據dom節點是否存在, 判斷執行parentNode.insertBefore或者parentNode.appendChild
  • Update effect: fiber節點需要更新, 調用commitWork
    • FunctionComponent mutation
      • fiber.tagFunctionComponent, 會調用commitHookEffectListUnmount.
      • 遍歷effectList, 執行所有useLayoutEffect hook的銷燬函數
    • HostComponent mutation
      • 調用commitUpdate
      • 最終在updateDOMProperties中將render階段 completeWork中爲fiber節點賦值的updateQueue對應的內容, 渲染在頁面上
for (let i = 0; i < updatePayload.length; i += 2) {
  const propKey = updatePayload[i];
  const propValue = updatePayload[i + 1];

  // 處理 style
  if (propKey === STYLE) {
    setValueForStyles(domElement, propValue);
  // 處理 DANGEROUSLY_SET_INNER_HTML
  } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
    setInnerHTML(domElement, propValue);
  // 處理 children
  } else if (propKey === CHILDREN) {
    setTextContent(domElement, propValue);
  } else {
  // 處理剩餘 props
    setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
  }
}
  • Deletion Effect: 當fiber節點含有effectTag時, 調用commitDeletion
    • 遞歸調用fiber節點以及子孫fiber節點. 調用fiber.tagClassComponentcomponentWillUnComponent.
    • 從頁面中移除fiber節點對應的dom節點
    • 解綁ref
    • 調度useEffect的銷燬函數

layout

  • 代碼是在dom渲染完成後執行的
  • 該階段觸發的生命週期鉤子和hook可以直接訪問到已經改變後的dom
  • 遍歷effectList, 執行函數
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    // 調用生命週期鉤子和hook
    if (effectTag & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }

    // 賦值ref
    if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }

    nextEffect = nextEffect.nextEffect;
  }
}
  • commitLayoutEffects

    1. commitLayoutEffectOnFiber調用生命週期鉤子和hook相關操作
      • ClassComponent調用ComponentDidMount/ComponentDidUpdate
      • 調用this.state()第二個參數函數
      • FunctionComponent調用useLayoutEffect回調函數, 調度useEffect的銷燬和回調函數
      • useLayoutEffect(): 從上一次更新的銷燬到這次更新執行, 是同步執行的
      • useEffect(): 需要先調度, 在Layout階段執行完成後, 再異步執行
    2. commitAttachRef: 賦值ref: 獲取dom實例, 更新dom
  • mutation階段執行後, layout開始前, 切換root.current = finishWork

  • 因爲layout執行的生命週期函數和hook需要獲取新的dom信息

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