網站首頁 編程語言 正文
引言
本系列是講述從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
階段:
- 實現相對于
mountState
的updateState
下面我們分別一一地實現單節點的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
階段。 - 根據
currentFiber
和element
的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流程
通過beginWork
和completeWork
之后,我們得到了相應的標記。在commitWork
階段,我們就需要根據相應標記去處理不同的邏輯。本節主要講解更新
和刪除
階段的處理。
更新update
在之前的章節中,我們講解了commitWork
的mount
階段,我們現在根據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節點的操作。 - 最后將刪除的節點的
child
和return
指向刪掉。
基于上面的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
相關推薦
- 2022-05-08 聊聊docker?單機部署redis集群的問題_docker
- 2022-10-15 python?FastApi實現數據表遷移流程詳解_python
- 2024-03-15 Spring Framework對DAO(Data Access Object)的支持
- 2022-12-09 Python構造函數與析構函數超詳細分析_python
- 2023-01-08 Python?SQLAlchemy建立模型基礎關系模式過程詳解_python
- 2022-12-29 python查看包版本、更新單個包、卸載單個包的操作方法_python
- 2023-03-17 Android?ViewModel與Lifecycles和LiveData組件用法詳細講解_Andr
- 2022-05-25 <C++>搞明白構造函數和析構函數有這一篇就夠了
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支