日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

react 遍历对象_探索:跟随《Build your own React》实现一个简易React

發布時間:2024/9/3 编程问答 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 react 遍历对象_探索:跟随《Build your own React》实现一个简易React 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章介紹

build-your-own-react是一篇操作說明書,指導用戶一步步實現一個簡易的React,從中了解到React的大體工作流程。這篇文章是我的觀后整理和記錄,或許對大家會有所幫助。

構建簡易React,分為九個階段:

  • 介紹createElement與render
  • 實現createElement
  • 實現render
  • 介紹并發模式
  • 實現 Fibers
  • render 和 commit 階段
  • 實現協調
  • 支持函數組件
  • 實現 Hooks
  • 介紹 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 對象
    function createElement (type, config, ...children) {let key = null;let ref = null;let props = {};// 從 props 中分離 key 和 refif (config) {for (const name in config) if (Object.prototype.hasOwnProperty.call(config, name)) {if (name === "key") {key = config[name];} else if (name === "ref") {ref = config[name];} else {props[name] = config[name];}}}}// 處理 children 項,并將 children 附加到 props 上props.children = children.map((child) =>typeof child === "object"? child: {type: "TEXT_ELEMENT",props: {nodeValue: child,children: [],},});return {type,key,ref,props,}; }

    實現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,在這個函數里做了三件事:

  • 創建DOM;
  • 為當前 Fiber 的所有子元素創建 Fiber,并且構建連接;
  • 按照深度優先遍歷的順序(child > sibling > parent),確定下一個待處理任務。
  • 是的,“構建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 算法,由

    的時間復雜度降維到 ,關鍵因素在于三點:
  • 節點很少出現跨層級移動,因此只比較同一層級節點
  • 兩個不同類型的節點往往會產生不同的樹。因此當節點類型不同時,不再比較其子樹,直接銷毀并創建新子樹
  • 同一層級節點可以通過key標識對應關系
  • 我們來實現 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")
    const deletions = []; function reconcileChildren(fiber, elements) {// create children fiberslet oldFiber = fiber.alternate && fiber.alternate.child;let index = 0;let prevSibling = null;while (index < elements.length || oldFiber) {let newFiber = null;const element = elements[index];// 判斷類型是否相同const isSameType = element && oldFiber && element.type === oldFiber.type;// 同類型,復用dom,并建立alternate聯系if (isSameType) {newFiber = {type: oldFiber.type,props: element.props,dom: oldFiber.dom,parent: fiber,alternate: oldFiber,effectTag: "UPDATE",};}// 不同類型,創建新dom,并切斷子樹比較if (element && !isSameType) {newFiber = {type: element.type,props: element.props,dom: null,parent: fiber,alternate: null,effectTag: "PLACEMENT",};}// 不同類型,銷毀舊domif (oldFiber && !isSameType) {deletions.push(oldFiber);oldFiber.effectTag = "DELETION";}if (index === 0) {fiber.child = newFiber;}if (prevSibling) {prevSibling.sibling = newFiber;}index++;prevSibling = newFiber;if (oldFiber) {oldFiber = oldFiber.sibling;}} }function performUnitOfWork(fiber) {// create domif (!fiber.dom) {fiber.dom = createDOM(fiber);}// create chilren fibersreconcileChildren(fiber, fiber.props.children);// return next unitOfWorkif (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;} }

    commit階段 :應用DOM操作

    在 commit 階段根據effectTag應用不同的DOM操作 。

    • "DELETION":移除要刪除的DOM節點
    • "PLACEMENT":掛載新創建的DOM節點
    • "UPDATE":更新DOM節點屬性
    function commitRoot() {deletions.forEach(commitWork);commitWork(workInProgressRoot.child);currentRoot = workInProgressRoot;workInProgressRoot = null; }function commitWork(fiber) {if (!fiber) {return;}const domParent = fiber.parent.dom;if (fiber.effectTag === "DELETION") {domParent.removeChild(fiber.dom)} else if (fiber.effectTag === "PLACEMENT" && fiber.dom !== null) {domParent.appendChild(fiber.dom);} else if (fiber.effectTag === "UPDATE" && fiber.dom !== null) {updateDOM(fiber.dom, fiber.alternate.props, fiber.props);}commitWork(fiber.child);commitWork(fiber.sibling); }const isProperty = (prop) => prop !== "children"; const isEvent = (prop) => prop.startsWith("on"); const isNew = (prev, next) => (key) => prev[key] !== next[key]; const isGone = (_prev, next) => (key) => !key in next; function updateDOM(dom, prevProps, nextProps) {Object.keys(prevProps).forEach((name) => {if (isEvent(name) && (!(name in prevProps) || isNew(name))) {dom.removeEventListener(name.toLowerCase().substring(2), prevProps[name]);}if (isProperty(name) && isGone(name)) {dom[name] = "";}});Object.keys(nextProps).forEach((name) => {if (isEvent(name) && isNew(name)) {dom.addEventListener(name.toLowerCase().substring(2), nextProps[name]);}if (isProperty(name) && isNew(name)) {dom[name] = nextProps[name];}}); }

    支持函數組件

    函數組件和原生元素的區別在于:

  • ReactElement 對象的type值是組件的定義函數,執行定義函數返回子 ReactElement 對象。因此在performUnitOfWork中無需創建 DOM 節點,并且需要調用定義函數獲得子代。
  • 函數組件對應一個 Fiber 節點,但其沒有對應的 DOM 節點。因此在 commit 階段進行DOM操作需要找到真正的父子節點。
  • function performUnitOfWork(fiber) {if (fiber.type instanceof Function) {updateFunctionComponent(fiber);} else {updateHostComponent(fiber);} }// 更新函數組件 function updateFunctionComponent(fiber) {// 調用組件定義函數,獲取子ReactElement對象const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children); }// 更新原生元素 function updateHostComponent(fiber) {// create domif (!fiber.dom) {fiber.dom = createDOM(fiber);}reconcileChildren(fiber, fiber.props.children); }function commitWork(fiber) {if (!fiber) {return;}let parentFiber = fiber.parent;// 插入和更新操作需要找到真正的父dom節點 while (parentFiber.dom === null) {parentFiber = parentFiber.parent;}const domParent = parentFiber.dom;if (fiber.effectTag === "DELETION") {commitDeletion(domParent, fiber);} else if (fiber.effectTag === "PLACEMENT" && fiber.dom !== null) {domParent.appendChild(fiber.dom);} else if (fiber.effectTag === "UPDATE" && fiber.dom !== null) {updateDOM(fiber.dom, fiber.alternate.props, fiber.props);}commitWork(fiber.child);commitWork(fiber.sibling); }function commitDeletion(domParent, fiber) {// 刪除操作需要找到真正的子dom節點if (fiber.dom) {domParent.removeChild(fiber.dom);} else {commitDeletion(domParent, fiber.child);} }

    支持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的功能和優化并沒有完全在上述過程中實現,包括:

  • 在render階段,我們遍歷了整棵Fiber樹。而在React中使用啟發式算法跳過未修改的子樹
  • 在commit階段,我們同樣遍歷了整棵Fiber樹。而在React中則是依賴Effect List存儲有修改的Fiber,避免對 Fiber樹的再次遍歷
  • 在處理單位任務時,我們會為workInProgress樹創建新的Fiber節點 。而在React中會重復使用current樹中的老節點
  • 我們在render階段接收到新的狀態會重新開始渲染流程。而在React中會為每個更新標記一個expiration timestamp,比較更新的優先級。
  • 同時,你也可以自行添加一些功能,比如:

  • 支持style prop 的對象定義
  • 支持列表元素
  • 實現useEffect
  • 在協調過程中支持key標識
  • https://pomb.us/build-your-own-react/?pomb.us

    跟隨原文動手實現一遍,對React的大致工作流程會有更深刻的理解。同時,對React優化的歷程和出發點也有一些體會,不僅僅知道它是怎么做的,還有它為什么要這么做。另外,動手實現的樂趣和成就感是無可替代的。

    所以,快跟著原文實現一遍吧。

    總結

    以上是生活随笔為你收集整理的react 遍历对象_探索:跟随《Build your own React》实现一个简易React的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。