日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React18之update流程從零實現詳解_React

作者:sunnyhuang519626 ? 更新時間: 2023-02-25 編程語言

引言

本系列是講述從0開始實現一個react18的基本版本。由于React源碼通過Mono-repo 管理倉庫,我們也是用pnpm提供的workspaces來管理我們的代碼倉庫,打包我們使用rollup進行打包。

倉庫地址

具體章節代碼3個commit

本章我們主要講解通過useState狀態改變,引起的單節點update更新階段的流程。

對比Mount階段

對比我們之前講解的mount階段,update階段也會經歷大致的流程, 只是處理邏輯會有不同:

之前的章節我們主要講了reconciler(調和) 階段中mount階段:

  • beginWork:向下調和創建fiberNode樹,
  • completeWork:構建離屏DOM樹以及打subtreeFlags標記。
  • commitWork:根據placement創建dom
  • useState: 對應調用mountState

這一節的update階段如下:

begionWork階段:

  • 處理ChildDeletion的刪除的情況
  • 處理節點移動的情況 (abc -> bca)

completeWork階段:

  • 基于HostText的內容更新標記更新flags
  • 基于HostComponent屬性變化標記更新flags

commitWork階段:

  • 基于ChildDeletion, 遍歷被刪除的子樹
  • 基于Update, 更新文本內容

useState階段:

  • 實現相對于mountStateupdateState

下面我們分別一一地實現單節點的update更新流程

beginWork流程

對于單一節點的向下調和流程,主要在childFibers文件中,分2種,一種是文本節點的處理reconcileSingleTextNode, 一種是標簽節點的處理reconcileSingleElement

復用fiberNode

update階段的話,主要有一點是要思考如何復用之前mount階段已經創建的fiberNode

我們先以reconcileSingleElement為例子講解。

當新的ReactElement的type 和 key都和之前的對應的fiberNode都一樣的時候,才能夠進行復用。我們先看看reconcileSingleElement是復用的邏輯。

function reconcileSingleElement(
  returnFiber: FiberNode,
  currentFiber: FiberNode | null,
  element: ReactElementType
) {
  const key = element.key;
  // update的情況 <單節點的處理 div -> p>
  if (currentFiber !== null) {
    // key相同
    if (currentFiber.key === key) {
      // 是react元素
      if (element.$$typeof === REACT_ELEMENT_TYPE) {
        // type相同
        if (currentFiber.type === element.type) {
          const existing = useFiber(currentFiber, element.props);
          existing.return = returnFiber;
          return existing;
        }
      }
    }
  }
}
  • 首先我們需要判斷currentFiber是否存在,當存在的時候,說明是進入了update階段。
  • 根據currentFiberelement的tag 和 type判斷,如果相同才可以復用。
  • 通過雙緩存樹(useFiber)去復用fiberNode。

useFiber

復用的邏輯本質就是調用了useFiber, 本質上,它是通過雙緩存書指針alternate,它接受已經渲染對應的fiberNode以及新的Props 巧妙的運用我們之前創建wip的邏輯,可以很好的復用fiberNode

/**
 * 雙緩存樹原理:基于當前的fiberNode創建一個新的fiberNode, 而不用去調用new FiberNode
 * @param {FiberNode} fiber 正在展示的fiberNode
 * @param {Props} pendingProps 新的Props
 * @returns {FiberNode}
 */
function useFiber(fiber: FiberNode, pendingProps: Props): FiberNode {
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

對于reconcileSingleTextNode

刪除舊的和新建fiberNode

當不能夠復用fiberNode的時候,我們除了要像mount的時候新建fiberNode(已經有的邏輯),還需要刪除舊的fiberNode

我們先以reconcileSingleElement為例子講解。

beginWork階段,我們只需要標記刪除flags。以下2種情況我們需要額外的標記舊fiberNode刪除

  • key不同
  • key相同,type不同
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode) {
  if (!shouldTrackEffects) {
    return;
  }
  const deletions = returnFiber.deletions;
  if (deletions === null) {
    // 當前父fiber還沒有需要刪除的子fiber
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}

我們將需要刪除的節點,通過數組形式賦值到父節點deletions中,并標記ChildDeletion有節點需要刪除。

對于reconcileSingleTextNode, 當渲染視圖中是HostText就可以直接復用。整體代碼如下:

function reconcileSingleTextNode(
  returnFiber: FiberNode,
  currentFiber: FiberNode | null,
  content: string | number
): FiberNode {
  // update
  if (currentFiber !== null) {
    // 類型沒有變,可以復用
    if (currentFiber.tag === HostText) {
      const existing = useFiber(currentFiber, { content });
      existing.return = returnFiber;
      return existing;
    }
    // 刪掉之前的 (之前的div, 現在是hostText)
    deleteChild(returnFiber, currentFiber);
  }
  const fiber = new FiberNode(HostText, { content }, null);
  fiber.return = returnFiber;
  return fiber;
}

completeWork流程

當在beginWork做好相應的刪除和移動標記后,在completeWork主要是做更新的標記。

對于單一的節點來說,更新標記分為2種,

  • 第一種是文本元素的更新,主要是新舊文本內容的不一樣。
  • 第二種是類似div的屬性等更新。這個我們下一節進行講解。

這里我們只對HostText中的類型進行講解。

case HostText:
  if (current !== null && wip.stateNode) {
    //update
    const oldText = current.memoizedProps.content;
    const newText = newProps.content;
    if (oldText !== newText) {
      // 標記更新
      markUpdate(wip);
    }
  } else {
    // 1. 構建DOM
    const instance = createTextInstance(newProps.content);
    // 2. 將DOM插入到DOM樹中
    wip.stateNode = instance;
  }
  bubbleProperties(wip);
  return null;

從上面我們可以看出,我們根據文本內容的不同,進行當前節點wip進行標記。

function markUpdate(fiber: FiberNode) {
  fiber.flags |= Update;
}

commitWork流程

通過beginWorkcompleteWork之后,我們得到了相應的標記。在commitWork階段,我們就需要根據相應標記去處理不同的邏輯。本節主要講解更新刪除階段的處理。

更新update

在之前的章節中,我們講解了commitWorkmount階段,我們現在根據update的flag進行邏輯處理。

// flags update
if ((flags & Update) !== NoFlags) {
  commitUpdate(finishedWork);
  finishedWork.flags &= ~Update;
}

commitUpdate

對于文本節點,commitUpdate主要是根據新的文本內容,更新之前的dom的文本內容。

export function commitUpdate(fiber: FiberNode) {
  switch (fiber.tag) {
    case HostText:
      const text = fiber.memoizedProps.content;
      return commitTextUpdate(fiber.stateNode, text);
  }
}
export function commitTextUpdate(textInstance: TestInstance, content: string) {
  textInstance.textContent = content;
}

刪除ChildDeletion

beginWork過程中,對于存在要刪除的子節點,我們會保存在當前父節點的deletions, 所以在刪除階段,我們需要根據當前節點的deletions屬性進行對要刪除的節點進行不同的處理。

// flags childDeletion
if ((flags & ChildDeletion) !== NoFlags) {
  const deletions = finishedWork.deletions;
  if (deletions !== null) {
    deletions.forEach((childToDelete) => {
      commitDeletion(childToDelete);
    });
  }
  finishedWork.flags &= ~ChildDeletion;
}

如果當前節點存在要刪除的子節點的話,我們需要對每一個子節點進行commitDeletion的操作。

commitDeletion

commitDeletion函數的是對每一個要刪除的子節點進行處理。它的主要功能有幾點:

  • 對于不同類型的fiberNode, 當節點刪除的時候,自身和所有子節點都需要執行的不同的卸載邏輯。例如:函數組件的useEffect的return函數執行,ref的解綁,class組件的componentUnmount等邏輯處理。
  • 由于fiberNode和dom節點不是一一對應的,所以要找到fiberNode對應的dom節點,然后再執行刪除dom節點的操作。
  • 最后將刪除的節點的childreturn指向刪掉。

基于上面的2點分析,我們很容易就想到,commitDeletion肯定會執行DFS向下遍歷,進行不同子節點的刪除邏輯處理。

/**
 * rootHostNode 找到對應的DOM節點。
 * commitNestedComponent DFS遍歷節點的進行卸載相關的邏輯
 * @param {FiberNode} childToDelete
 */
function commitDeletion(childToDelete: FiberNode) {
  let rootHostNode: FiberNode | null = null;
  // 遞歸子樹
  commitNestedComponent(childToDelete, (unmountFiber) => {
    switch (unmountFiber.tag) {
      case HostComponent:
        if (rootHostNode === null) {
          rootHostNode = unmountFiber;
        }
        // TODO: 解綁ref
        return;
      case HostText:
        if (rootHostNode === null) {
          rootHostNode = unmountFiber;
        }
        return;
      case FunctionComponent:
        // TODO: useEffect unmount 解綁ref
        return;
      default:
        if (__DEV__) {
          console.warn("未處理的unmount類型", unmountFiber);
        }
        break;
    }
  });
  // 移除rootHostNode的DOM
  if (rootHostNode !== null) {
    const hostParent = getHostParent(childToDelete);
    if (hostParent !== null) {
      removeChild((rootHostNode as FiberNode).stateNode, hostParent);
    }
  }
  childToDelete.return = null;
  childToDelete.child = null;
}

commitNestedComponent

commitNestedComponent中主要是完成我們上面說的2點。

  • DFS深度遍歷子節點
  • 找到當前要刪除的fiberNode對應的真正的DOM節點

接受2個參數。1. 當前的fiberNode, 2. 遞歸到不同的子節點的同時,需要執行的回調函數執行不同的卸載流程。

function commitNestedComponent(
  root: FiberNode,
  onCommitUnmount: (fiber: FiberNode) => void
) {
  let node = root;
  while (true) {
    onCommitUnmount(node);
    if (node.child !== null) {
      // 向下遍歷
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === root) {
      // 終止條件
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === root) {
        return;
      }
      // 向上歸
      node = node.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

這里可能比較繞,我們下面通過幾個例子總結一下,這個過程的主要流程。

總結

如果按照如下的結構,要刪除外層div元素,會經歷如下的流程

<div>
   <Child />
   <span>hcc</span>
   yx
</div>
function Child() {
  return <div>hello world</div>
}
  • div的fiberNode的父節的標記ChildDeletion以及存放到deletions中。
  • 當執行到commitWork階段的時候,遍歷deletions數組。
  • 執行的div對應的HostComponent, 然后執行commitDeletion
  • commitDeletion中執行commitNestedComponent向下DFS遍歷。
  • 在遍歷的過程中,每一個節點都是執行一個回調函數,基于不同的類型執行不同的刪除操作,以及記錄我們要刪除的Dom節點對應的fiberNode。
  • 所以首先是div執行onCommitUnmount, 由于它是HostComponent,所以將rootHostNode賦值給了div
  • 向下遞歸到Child節點,由于它存在子節點,繼續遞歸到child-div節點,繼續遍歷到hello world節點。它不存在子節點。
  • 然后找到Child的兄弟節點,以此執行,先子后兄。直到回到div節點。

下一節預告

下一節我們講解通過useState改變狀態后,如何更新節點以及函數組件hooks是如何保存數據的。

原文鏈接:https://juejin.cn/post/7186264376356110393

欄目分類
最近更新