react 遍历对象_探索:跟随《Build your own React》实现一个简易React
文章介紹
build-your-own-react是一篇操作說明書,指導用戶一步步實現一個簡易的React,從中了解到React的大體工作流程。這篇文章是我的觀后整理和記錄,或許對大家會有所幫助。
構建簡易React,分為九個階段:
介紹 createElement 與 render
JSX描述結構,由Babel轉譯為對createElement的調用;
createElement接收 tagName、props、children,返回 ReactElement 對象;
render接收 ReactElement 對象和掛載節點,產生渲染效果。
實現createElement
createElement做以下幾件事:
- props中包括key和ref,需要做一次分離
- children子項可能是String/Number這類原始類型數據。原始類型數據與文本節點對應,因此將其統一處理為TEXT_ELEMENT類型的對象
- 將 children附加到props對象上
- 返回 ReactElement 對象
實現render
render接收到的 ReactElement 對象,其實可以說是虛擬DOM結構的根,通過props.children連接子 ReactElement 對象
render的目的是產生渲染效果。最直觀的方法是從根 ReactElement 開始進行深度優先遍歷,生成整棵 DOM 樹后掛載到根節點上。
function render(element, container) {const { type, props } = element;// 前序創建節點const dom =type === "TEXT_ELEMENT"? document.createTextNode(""): document.createElement(type);Object.keys(props).forEach((name) => {if (isProperty(name)) {dom[name] = props[name];}});props.children.filter(Boolean).forEach((child) => this.render(child, dom));// 后序掛載節點container.appendChild(dom); }這其實類似于React v16之前的 stack reconciler。其特點在于利用調用棧實現遍歷。
介紹并發模式
按照目前的方式進行更新時,需要將整顆虛擬DOM樹一次性處理完畢。當樹層級結構變得復雜,JS計算將長時間占用主線程,會導致卡頓、無法響應的糟糕體驗。
能否實現增量渲染。具體來說,能否將虛擬DOM樹處理劃分為一個個小任務,并在主線程上并發執行呢?
依賴于調用棧,難以將整個過程中斷,也就無法實現任務拆分。不如在內存中自行維護一個支持 DFS 的數據結構,代替調用棧的功能。React控制主動權,自主做任務拆分和維護。這個數據結構就是 Fiber 樹了。
那么如何在主線程上并發執行,或者說怎么確定任務的執行時機。瀏覽器的主線程需要處理HTML解析、樣式計算、布局、系統級任務、JavaScript執行、垃圾回收等一眾任務,由任務隊列調度。當主線程處于空閑狀態時安排上 Fiber 處理那是最好不過。恰好,瀏覽器端提供了一個API——requestIdleCallback(callback),當瀏覽器空閑時會主動執行 callback 函數。但是,可惜的是這個方法目前在各瀏覽器的支持度和穩定性還無法得到保證。因此 React 團隊自行實現了 Scheduler 庫來代替requestIdleCallback 實現任務調度。
上面說的兩個過程就是任務分片和任務調度了,他們一個由 Fiber 實現,一個由 Scheduler 實現。
Fibers
Fiber和ReactElement的關系
ReactElement 對象已經是虛擬DOM的一種表示方法了,一個 ReactElement 對象對應一個 FiberNode,只需給 FiberNode 加上核心信息 type和props。
FiberNode {type: element.type,props: element.props,child: Fiber,sibling: Fiber,parent: Fiber }Fiber如何支持DFS
Fiber 結構的最大特點是child/sibling/parent三個指針,分別指向第一個子節點、緊跟著的兄弟節點、父節點。這三個指針使深度優先遍歷成為可能。
root - div - h1 - p - a - h2- 沿著 child 指針向下遍歷,直到葉子節點。
- 葉子節點依賴 sibling 指針向右遍歷該層兄弟節點。
- 兄弟節點遍歷完畢再沿 parent 指針回到上一層
- 直到回到根節點停止
Fiber和任務分片
前文說過 Fiber 的作用在任務分片。在虛擬DOM樹的處理過程中,最小的處理粒度是一個節點。我們把處理單個FiberNode的任務稱為“unitOfWork”,方便起見,下文稱之為單位任務。
總結
- 一個 ReactElement 對象對應 一個 Fiber 節點,一個 Fiber 節點對應一個單位任務。
- Fiber 節點通過parent/child/sibing三個指針構成 Fiber 樹,Fiber 樹支撐深度優先遍歷。
任務調度
在主線程上,每個空閑的時間片長度不一。我們希望在一個時間片有限的時間內盡量多的執行任務。
因此在處理完一個單位任務之后查詢是否還有空閑,再決定是否執行下一個單位任務。這部分代碼由workLoop函數實現。
// 依賴requestIdleCallback實現調度 let nextOfUnitWork = null; function workLoop(deadline) {let shouldYield = false;while (nextOfUnitWork && !shouldYield) {nextOfUnitWork = performUnitOfWork(nextOfUnitWork);shouldYield = deadline.timeRemaining() < 1;}requestIdleCallback(workLoop); } requestIdleCallback(workLoop);處理單位任務
處理單位任務的函數是performUnitOfWork,在這個函數里做了三件事:
是的,“構建Fiber樹” 和 “Fiber節點處理” 是自上而下同步進行的。
const isProperty = (prop) => prop !== "children";const SimactDOM = {render(element, container) {nextOfUnitWork = {dom: container,props: {children: [element],},};}, };// workLoop依賴requestIdleCallback實現調度 let nextOfUnitWork = null; function workLoop(deadline) {let shouldYield = false;while (nextOfUnitWork && !shouldYield) {nextOfUnitWork = performUnitOfWork(nextOfUnitWork);shouldYield = deadline.timeRemaining() < 1;}requestIdleCallback(workLoop); } requestIdleCallback(workLoop);// 處理 unitOfWork function performUnitOfWork(fiber) {// 創建DOMif (!fiber.dom) {fiber.dom = createDOM(fiber);}// 掛載DOMif (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom);}const elements = fiber.props.children;let index = 0;let prevSibling = null;// 創建 children fiberswhile (index < elements.length) {const element = elements[index];const newFiber = {type: element.type,props: element.props,dom: null,parent: fiber,};if (index === 0) {fiber.child = newFiber;}if (prevSibling) {prevSibling.sibling = newFiber;}index++;prevSibling = newFiber;}// 返回 next unitOfWorkif (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;} }export { SimactDOM as default };仔細閱讀上面的代碼,會發現render調用和任務調度執行,在代碼上并沒有順序聯系。這和我們常見的代碼結構有些許不同。
render 和 commit 階段
在一個任務中直接進行DOM掛載,同時任務分散在多個時間片內并發執行。這會導致部分視圖已更新,部分視圖未更新 的現象。
那么如何防止DOM發生突變(mutate),盡量將其模擬成一個不可變對象呢?方法是將 Fiber樹處理過程和掛載DOM樹過程分離開。就是說分為兩個階段:render 和 commit。
render 階段增量處理Fiber節點,commit階段將結果一次性提交到DOM樹上。
render 階段負責:
- 生成 Fiber 樹
- 為 Fiber 創建對應的 DOM 節點。確保進入 commit 前,每一個 Fiber 上都有節點。但 DOM 節點的更新、插入、刪除由 commit 負責。
commit 階段再次遍歷 Fiber 樹,將 DOM 節點掛載到文檔上。
在內存中維護一顆 Fiber 樹(workInProgress)充當處理的目標對象。整棵 Fiber 樹處理完畢后,一次性渲染到視圖上。
function render(element, container) {// workInProgress Tree 充當目標 wipRoot = {dom: container,props: {children: [element],},}nextUnitOfWork = wipRoot }let nextUnitOfWork = null let wipRoot = nullfunction workLoop(deadline) {let shouldYield = falsewhile (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork)shouldYield = deadline.timeRemaining() < 1}// 進入commit階段的判斷條件:有一棵樹在渲染流程中,并且render階段已執行完畢if (!nextUnitOfWork && wipRoot) {commitRoot()}requestIdleCallback(workLoop) }function commitRoot() {// commit階段遞歸遍歷fiber樹,掛載DOM節點commitWork(wipRoot.child)wipRoot = null }function commitWork(fiber) {if (!fiber) {return}const domParent = fiber.parent.domdomParent.appendChild(fiber.dom)commitWork(fiber.child)commitWork(fiber.sibling) }實現協調
我們開始考慮狀態更新的情況,上述代碼重復執行render將會導致 DOM 節點追加,而非更新。虛擬DOM進行協調簡單來說是實現一顆新樹,比較和記錄新樹和老樹之間的差異。
workInProgress樹負責生成新樹。我們需要一顆老樹,和新樹做對比。這顆老樹也是與視圖對應的Fiber樹,稱為current樹 。workInProgress樹和current樹的關系,類似于緩沖區和顯示區。緩沖區處理完畢,復制給顯示區。
計算兩棵樹的最小修改策略的 Diffing 算法,由
的時間復雜度降維到 ,關鍵因素在于三點:我們來實現 Diffing 算法。
- 依賴alternate確定節點的對應關系
- render階段:根據節點類型變化確定更新策略effectTag
- commit階段:根據effectTag應用具體DOM操作
如何確定兩棵樹中節點的對應關系?
Fiber節點上alternate屬性記錄同一層級對應位置的老Fiber節點。而alternate屬性的賦值是在創建子Fiber節點時進行的。
- 根節點 workInProgressRoot.alternate = currentRoot
- 創建子Fiber節點時,依賴child指針和sibling指針找到current樹中的對應老Fiber節點
- 通過alternate建立新老子層節點的對應關系,到下一層遞歸
這一部分代碼應該更能直觀說明:
let workInProgressRoot = null; let currentRoot = null;// 根節點建立聯系 const SimactDOM = {render(element, container) {deletions = [];workInProgressRoot = {dom: container,props: {children: [element],},alternate: currentRoot,};nextOfUnitWork = workInProgressRoot;}, };function performUnitOfWork () {...let oldFiber = fiber.alternate && fiber.alternate.child;...// 處理一個Fiber節點時,創建其子節點。// 依賴對應老節點的child指針和子節點的sibling指針,確定子節點對應關系// 通過alternate建立新老子層節點的對應關系,到下一層遞歸let index = 0;while (index < elements.length) {const newFiber = {type: element.type,props: element.props,dom: null,parent: fiber,alternate: oldFiber,};....if (oldFiber) {oldFiber = oldFiber.sibling;}index++;}... }// 渲染完畢后,更新current樹,重置workInProgress樹 function commitRoot() {commitWork(workInProgressRoot.child);currentRoot = workInProgressRoot;workInProgressRoot = null; }render階段 :根據節點類型確定更新策略
在 render 階段記錄節點對應的操作標識,由Fiber的effectTag記錄;
- 同類型節點復用DOM元素,只需進行屬性更改("UPDATE")
- 不同類型的節點銷毀原有DOM元素("DELETION"),創建新的DOM元素("PLACEMENT")
commit階段 :應用DOM操作
在 commit 階段根據effectTag應用不同的DOM操作 。
- "DELETION":移除要刪除的DOM節點
- "PLACEMENT":掛載新創建的DOM節點
- "UPDATE":更新DOM節點屬性
支持函數組件
函數組件和原生元素的區別在于:
支持Hooks
全局變量workInProgressFiber存儲當前正在處理的 Fiber 節點,以供useState訪問。
為了支持在一個組件中多次使用useState,hooks 作為隊列在 Fiber 節點中維護。全局變量hookIndex維持useState執行順序和hook的關系。
Fiber {hooks: [ // hook按調用順序存放{state,queue: [action] // 任務分片執行,在未處理到當前節點前。更改狀態將重新執行渲染流程,需要保留未生效的修改 }] } let workInProgressFiber = null; let hookIndex = null;function updateFunctionComponent(fiber) {workInProgressFiber = fiber;hookIndex = 0;workInProgressFiber.hooks = [];const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children); }function useState(initial) {const oldHook =workInProgressFiber.alternate &&workInProgressFiber.alternate.hooks &&workInProgressFiber.alternate.hooks[hookIndex];// 根據老節點的hook確定初始狀態const hook = {state: oldHook ? oldHook.state : initial,queue: [],};// 應用狀態更新if (oldHook) {oldHook.queue.forEach((action) => {hook.state = action(hook.state);});}const setState = (action) => {// 加入更新隊列,在下一次渲染流程中應用。// 開啟渲染流程hook.queue.push(action);deletions = [];workInProgressRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot,};nextOfUnitWork = workInProgressRoot;};workInProgressFiber.hooks.push(hook);hookIndex++;return [hook.state, setState]; }后記
React的功能和優化并沒有完全在上述過程中實現,包括:
同時,你也可以自行添加一些功能,比如:
跟隨原文動手實現一遍,對React的大致工作流程會有更深刻的理解。同時,對React優化的歷程和出發點也有一些體會,不僅僅知道它是怎么做的,還有它為什么要這么做。另外,動手實現的樂趣和成就感是無可替代的。
所以,快跟著原文實現一遍吧。
總結
以上是生活随笔為你收集整理的react 遍历对象_探索:跟随《Build your own React》实现一个简易React的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 以列表形式输出_04 Python之列表
- 下一篇: 特征图注意力_向往的GAT(图注意力模型