React18 技術概覽 - 基礎篇 (還等什麼,抓緊來看看吧)

金虹橋程序員 2021-09-18 18:18:53 阅读数:624

react18 react 看看 看吧

最近學習了react源碼,發現社區裏面的大部分概念跟react 18的源碼實現有些差异,比如effect list、requestIdleCallback等很多概念在新版本(React 18)中已經逐漸移除,理念也發生了變更,所以特此做個歸納總結,方便大家學習,有問題可以隨時留言或者私信!(感興趣的同學也歡迎一起學習)

核心流程如下圖所示

image.png

React是當前最流行的前端框架,react當中有幾個核心的概念:渲染器、調和器和調度器

渲染器

React 最初只是服務於 DOM,但是這之後被改編成也能同時支持原生平臺的 React Native。因此,在 React 內部機制中引入了“渲染器”這個概念。

渲染器用於管理一棵 React 樹,使其根據底層平臺進行不同的調用。

渲染器同樣比特於 packages/ 目錄下:

  • React DOM Renderer 將 React 組件渲染成 DOM。它實現了全局 ReactDOMAPI,這在npm上作為 react-dom 包。這也可以作為單獨瀏覽器版本使用,稱為 react-dom.js,導出一個 ReactDOM 的全局對象.
  • React Native Renderer 將 React 組件渲染為 Native 視圖。此渲染器在 React Native 內部使用。
  • React Test Renderer 將 React 組件渲染為 JSON 樹。這用於 Jest 的快照測試特性。在 npm 上作為 react-test-renderer 包發布。

另外一個官方支持的渲染器的是 react-art。它曾經是一個獨立的 GitHub 倉庫,但是現在我們將此加入了主源代碼樹。

調和器

即便 React DOM 和 React Native 渲染器的區別很大,但也需要共享一些邏輯。特別是協調算法需要盡可能相似,這樣可以讓聲明式渲染,自定義組件,state,生命周期方法和 refs 等特性,保持跨平臺工作一致。

為了解决這個問題,不同的渲染器彼此共享一些代碼。我們稱 React 的這一部分為 “reconciler”。當處理類似於 setState() 這樣的更新時,reconciler 會調用樹中組件上的 render(),然後决定是否進行掛載,更新或是卸載操作。

Reconciler 沒有單獨的包,因為他們暫時沒有公共 API。相反,它們被如 React DOM 和 React Native 的渲染器排除在外。

Stack reconciler

“stack” reconciler 是 React 15 及更早的解决方案。

stack reconciler采用遞歸的方式創建虛擬DOM並提交Dom Mutation,整個過程同步並且無法中斷工作或將其拆分為塊。如果組件樹的層級很深,遞歸會占用線程很多時間,遞歸更新時間超過了16ms,用戶交互就會卡頓。

Fiber reconciler

“fiber” reconciler 是一個新嘗試,致力於解决 stack reconciler 中固有的問題,同時解决一些曆史遺留問題。

Fiber 從 React 16 開始變成了默認的 reconciler。

它的主要目標是:

  • 能够把可中斷的任務切片處理。
  • 能够調整優先級,重置並複用任務。
  • 能够在父元素與子元素之間交錯處理,以支持 React 中的布局。
  • 能够在 render() 中返回多個元素。
  • 更好地支持錯誤邊界。

並發模式進行以下操作:

頂部是個slider,拖放後會懟整個chart區域縮放

stack-reconciler.gif

火焰圖調用信息如下

image.png

並發模式進行同樣的操作:

fiber-reconciler.gif

火焰圖調用信息如下

image.png

通過對比,可以很明顯的感受到並發模式下的流暢性

調度器

調度器主要包含兩塊:時間調度、優先級調度

瀏覽器每一幀需要執行的任務

時間調度

下面是react源碼中的幾種調度方式,有先後關系

方式一:isInputPending

參考文檔:https://wicg.github.io/is-inp...

在運行需要顯示某些內容的脚本時,開發人員今天需要做出判斷。

如果脚本可能計算很長時間才能運行並且用戶在發生這種情况時進行了某種輸入,那麼瀏覽器將需要等到脚本完成後才能分派輸入事件。這會造成在響應輸入事件之前有很長的延遲,用戶體驗並不是很好,因此開發人員通常會將長脚本任務分解成更小的塊,以允許用戶代理在塊之間調度事件。每次脚本執行時,它都需要以某種方式發布一條消息,調用 requestAnimation 幀和 requestIdleCallback 的組合,或者采用其它方式來生成可以被調度的事情,之後它可以在空閑時再次被調用。即使在最好的情况下,脚本每次產生時也可能需要很多毫秒才能再次運行。所以不幸的是,這也不是一個很好的用戶體驗,因為部分初始的脚本被延遲了很久才執行,盡管他需要這麼久的時間。

為了避免這種取舍,FacebookChromium 中提出並實現了 isInputPending() API,它可以提高網頁的響應能力,但是不會對性能造成太大影響。

isInputPending api 的目標是它現在將允許開發人員消除這種權衡。不再完全屈服於用戶代理,並且在屈服後必須承擔一個或多個事件循環的成本,長時間運行的脚本現在可以運行到完成,同時仍然保持響應。

目前 isInputPending API 僅在 Chromium 的 87 版本開始提供,其他瀏覽器並未實現。 

方式二:setImmediate

方式一主要使用在Chromium引擎的瀏覽器中,方式二從設計上,優先考慮了IE的兼容性

該方法用來把一些需要長時間運行的操作放在一個回調函數裏,在瀏覽器完成後面的其他語句後,就立刻執行這個回調函數。

IE支持

方式三:MessageChannel

Channel Messaging API的MessageChannel 接口允許我們創建一個新的消息通道,並通過它的兩個MessagePort 屬性發送數據。

在以下示例中,您可以看到使用MessageChannel構造函數實例化了一個channel對象。當iframe加載完畢,我們使用MessagePort.postMessage方法把一條消息和MessageChannel.port2傳遞給iframe。handleMessage處理程序將會從iframe中(使用MessagePort.onmessage監聽事件)接收到信息,將數據其放入innerHTML中。

var channel = new MessageChannel();
var para = document.querySelector('p');
var ifr = document.querySelector('iframe');
var otherWindow = ifr.contentWindow;
ifr.addEventListener("load", iframeLoaded, false);
function iframeLoaded() {
otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
}
channel.port1.onmessage = handleMessage;
function handleMessage(e) {
para.innerHTML = e.data;
} 

方式四:setTimeout

給大家都懂,略過

調度器的切片時間

切片間隔時間是5ms,最大間隔時間是300ms,源碼如下

// Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it yields multiple times per frame.
// It does not attempt to align with frame boundaries, since most tasks don't
// need to be frame aligned; for those that do, use requestAnimationFrame.
let yieldInterval = 5;
let deadline = 0;
// TODO: Make this configurable
// TODO: Adjust this based on priority?
const maxYieldInterval = 300;
let needsPaint = false;

切片和React相互關系

任務拆分

將調和階段(Reconciler)遞歸遍曆 VDOM 這個大任務分成若幹小任務,每個任務只負責一個節點的處理。

  • workLoopSync or workLoopConcurrent
  • performUnitOfWork

任務掛起、恢複、終止

在新 workInProgress tree 的創建過程中,會同 currentFiber 的對應節點進行 Diff 比較,生成對應的falgs,同時也會複用和 currentFiber 對應的節點對象,减少新創建對象帶來的開銷。也就是說無論是創建還是更新、掛起、恢複以及終止操作都是發生在 workInProgress tree 創建過程中的。workInProgress tree 構建過程其實就是循環的執行任務和創建下一個任務。

掛起

當第一個小任務完成後,先判斷這一幀是否還有空閑時間,沒有就掛起下一個任務的執行,記住當前掛起的節點,讓出控制權給瀏覽器執行更高優先級的任務。

恢複

在瀏覽器渲染完一幀後,判斷當前幀是否有剩餘時間,如果有就恢複執行之前掛起的任務。如果沒有任務需要處理,代錶調和階段完成,可以開始進入渲染階段。

終止

其實並不是每次更新都會走到提交階段。當在調和過程中觸發了新的更新,在執行下一個任務的時候,判斷是否有優先級更高的執行任務,如果有就終止原來將要執行的任務,開始新的 workInProgressFiber 樹構建過程,開始新的更新流程。這樣可以避免重複更新操作

任務優先級

下列是源碼中提供的任務優先級,除了無優先任務外,其它任務數值越小優先級越高

// 無優先級任務
export const NoPriority = 0;
// 立即執行任務
export const ImmediatePriority = 1;
// 用戶阻塞任務
export const UserBlockingPriority = 2;
// 正常任務
export const NormalPriority = 3;
// 低優先級任務
export const LowPriority = 4;
// 空閑執行任務
export const IdlePriority = 5;
版权声明:本文为[金虹橋程序員]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210918181852724h.html