400 行代碼構建一個迷你版 React!
作者 | Zachary Lee
翻譯、整理|編程界
React v19 beta 已經發布。與 React 18 相比,它提供了許多用戶友好的 API,但其核心原則基本保持不變。你可能已經使用 React 有一段時間了,但你知道它的內部工作原理嗎?
本文將幫助你構建一個約 400 行代碼的 React 版本,它支持異步更新並可以中斷——這是 React 的核心功能,許多高級 API 都依賴於此。以下是最終效果的動圖:
我使用了 React 官網提供的井字棋教程示例,效果良好。
目前,它託管在我的 GitHub 上,你也可以訪問在線版本親自試試。
github.com/ZacharyL2/mini-react
JSX 和 createElement
在深入瞭解 mini-react.ts 的原理之前,瞭解 JSX 的意義很重要。我們可以用 JSX 來描述 DOM 並輕鬆應用 JavaScript 邏輯。然而,瀏覽器並不能直接理解 JSX,因此我們編寫的 JSX 需要編譯成瀏覽器能夠理解的 JavaScript。
我在這裏使用了 babel,但你當然可以使用其他構建工具,它們生成的內容會類似。
你可以看到它調用了 React.createElement,它提供了以下選項:
-
type:表示當前節點的類型,例如 div。
-
config:表示當前元素節點的屬性,例如 {id: "test"}。
-
children:子元素,可能是多個元素、簡單文本或由 React.createElement 創建的更多節點。
如果你是一個經驗豐富的 React 用戶,你可能記得在 React 18 之前,爲了正確編寫 JSX,你需要從'react'導入 React。從 React 18 開始,這已經不再必要,增強了開發者體驗,但 React.createElement 仍然在底層被調用。
對於我們簡化的 React 實現,我們需要配置 Vite 並使用 react({jsxRuntime: 'classic'}) 直接將 JSX 編譯成 React.createElement 實現。
然後我們可以實現我們自己的:
Render
接下來,我們基於之前創建的數據結構實現一個簡化版的 render 函數,將 JSX 渲染到真實 DOM。
這裏是在線實現鏈接。目前它只渲染一次 JSX,所以不處理狀態更新。
Fiber 架構和併發模式
Fiber 架構和併發模式主要是爲了解決一旦整個元素樹遞歸完成就無法中斷,可能長時間阻塞主線程的問題。高優先級任務如用戶輸入或動畫可能無法及時處理。
在 React 的源碼中,工作被分成小單元。每當瀏覽器空閒時,它處理這些小工作單元,放棄對主線程的控制,以便瀏覽器能夠及時響應高優先級任務。一旦所有小單元工作完成,結果就會映射到真實 DOM。
兩個關鍵點是如何放棄主線程以及如何將工作分解成可管理的單元。
requestIdleCallback
requestIdleCallback 是一個實驗性 API,當瀏覽器空閒時執行回調。並非所有瀏覽器都支持。在 React 中,它用於 scheduler 包中,該包具有比 requestIdleCallback 更復雜的調度邏輯,包括更新任務優先級。
但這裏我們只考慮異步可中斷性,所以這是模仿 React 的基本實現:
MessageChannel 的使用原因
主要是使用宏任務處理每輪單元任務。但爲什麼是宏任務?
這是因爲我們需要使用宏任務來放棄主線程控制,讓瀏覽器在此空閒期間更新 DOM 或接收事件。由於瀏覽器將 DOM 更新作爲一個單獨的任務,在此期間不會執行 JavaScript。
主線程一次只能運行一個任務——要麼執行 JavaScript,要麼處理 DOM 計算、樣式計算、輸入事件等。而微任務不會放棄主線程控制。
爲什麼不使用 setTimeout?
這是因爲現代瀏覽器認爲嵌套的 setTimeout 調用超過五次是阻塞的,並將其最小延遲設爲 4 毫秒,所以它不夠精確。
算法
請注意,React 在不斷演變,我描述的算法可能不是最新的,但足以理解其基本原理。
這是 React 包如此龐大的一個主要原因。
下面是顯示工作單元之間連接的示意圖:
在 React 中,每個工作單元稱爲 Fiber 節點。它們通過類似鏈表的結構連接在一起:
-
child:從父節點指向第一個子元素的指針。
-
return/parent:所有子元素都有一個指向父元素的指針。
-
sibling:從第一個子元素指向下一個兄弟元素的指針。
有了這個數據結構,我們來看看具體的實現。
我們只是擴展了 render 邏輯,重構了調用順序爲 workLoop -> performUnitOfWork -> reconcileChildren -> commitRoot。
-
workLoop:通過連續調用 requestIdleCallback 獲得空閒時間。如果當前是空閒狀態且有單元任務要執行,則執行每個單元任務。
-
performUnitOfWork:執行的具體單元任務。這是鏈表思想的體現。具體來說,每次只處理一個 fiber 節點,並返回下一個要處理的節點。
-
reconcileChildren:協調當前 fiber 節點,實際上是虛擬 DOM 的比較,並記錄要進行的更改。你可以看到我們直接在每個 fiber 節點上修改並保存,因爲現在只是對 JavaScript 對象的修改,並不涉及真實 DOM。
-
commitRoot:如果當前需要更新(根據 wipRoot)且沒有下一個單元任務要處理(根據! nextUnitOfWork),則意味着需要將虛擬更改映射到真實 DOM。commitRoot 就是根據 fiber 節點的更改來修改真實 DOM。
有了這些,我們可以真正使用 fiber 架構進行可中斷的 DOM 更新,但我們仍然缺少一個觸發器。
觸發更新
在 React 中,常見的觸發器是 useState,最基本的更新機制。讓我們實現它來點燃我們的 Fiber 引擎。
這裏是具體實現:
它巧妙地將鉤子的狀態保存在 fiber 節點上,並通過一個隊列修改狀態。從這裏你也可以看到爲什麼 React 鉤子的調用順序不能改變。
結論
我們已經實現了一個支持異步和可中斷更新的最小化 React 模型,無需依賴項,並且如果不包括註釋和類型,代碼行數可能不到 400 行。希望這能對你有所幫助。
感謝閱讀!
閱讀原文|https://webdeveloper.beehiiv.com/p/build-react-400-lines-code
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/IBexPVvFT0Qp1ycC98tLuw