React渲染機(jī)制及相關(guān)優(yōu)化方案
一、react渲染步驟
準(zhǔn)備階段(Prepare Phase)
在準(zhǔn)備階段,React 會收集組件的依賴關(guān)系,建立組件樹的數(shù)據(jù)結(jié)構(gòu),確定組件的更新優(yōu)先級,并生成用于渲染的工作單元。計(jì)算階段(Compute Phase)
在計(jì)算階段,React 會根據(jù)組件的更新優(yōu)先級和調(diào)度策略,將工作單元分成多個(gè)批次進(jìn)行處理。每個(gè)批次都會執(zhí)行一小部分工作單元,以保證用戶界面的響應(yīng)性。渲染階段(Render Phase)
在渲染階段,React 會根據(jù)工作單元的類型和優(yōu)先級,執(zhí)行相應(yīng)的渲染操作。這包括創(chuàng)建新的虛擬 DOM 節(jié)點(diǎn)、更新現(xiàn)有的虛擬 DOM 節(jié)點(diǎn),以及卸載不再需要的組件。提交階段(Commit Phase)
在提交階段,React 會將更新后的虛擬 DOM 節(jié)點(diǎn)映射到實(shí)際的 DOM,更新用戶界面。這個(gè)階段還會執(zhí)行一些副作用操作,如執(zhí)行useEffect。
二、concurrent機(jī)制以及產(chǎn)生作用的機(jī)會
注:React 的并發(fā)模式(Concurrency Mode)是一種用于處理大型和復(fù)雜應(yīng)用程序的特性,旨在提高應(yīng)用程序的性能和響應(yīng)能力。解決react中狀態(tài)更新就會觸發(fā)該組件及該組件下所有子組件無腦更新而引發(fā)的性能問題;同時(shí)提供部分控制作業(yè)調(diào)度優(yōu)先級的能力給開發(fā)者使用
在傳統(tǒng)的 React 渲染模式中,更新操作是同步進(jìn)行的,即在進(jìn)行更新時(shí),會立即進(jìn)行組件的重新渲染,可能會阻塞主線程,導(dǎo)致頁面響應(yīng)變慢或失去響應(yīng)出現(xiàn)掉幀問題。
而concurrent mode通過引入一種新的調(diào)度算法和優(yōu)先級機(jī)制,將更新操作劃分為多個(gè)優(yōu)先級,使得 React 可以更好地管理和分配任務(wù),以實(shí)現(xiàn)更平滑的用戶體驗(yàn)。
concurrent mode主要具備以下幾個(gè)特性:異步渲染、優(yōu)先級調(diào)度、遞增式渲染
補(bǔ)充:concurrent mode 主要工作在渲染流程的 Compute Phase 及 Render Phase,因?yàn)樗鼈兪羌兇獾?JS 計(jì)算意味著可以被拆分,而 commit 階段由于帶有 DOM 更新,不可能 DOM 變更到一半中斷,因此必須一次性執(zhí)行完成
1. 優(yōu)先級調(diào)度:
concurrent mode 通過對任務(wù)進(jìn)行優(yōu)先級劃分,React 可以根據(jù)優(yōu)先級動(dòng)態(tài)地分配和重新分配任務(wù)。基于此React 可以更好地響應(yīng)用戶交互和其他高優(yōu)先級的任務(wù),同時(shí)提供了 “useDeferredValue” 、“useTransition” 兩個(gè)hooks用于調(diào)度作業(yè)任務(wù)的優(yōu)先級。
2. 遞增式渲染:
1)concurrent mode 下的渲染是逐步進(jìn)行的,React 將大量需要重新渲染的組件的工作基于時(shí)間片的理念劃分為多個(gè)小片段工作,在瀏覽器的每一幀的空閑時(shí)間中去執(zhí)行這些渲染工作,而不是一下子全部直接執(zhí)行,這樣有效的避免了掉幀情況的出現(xiàn)。
2)這里也就說明了為什么React官方說 componentWillMount 可能被調(diào)用多次的原因,正是因?yàn)榈蛢?yōu)先級任務(wù)的 render 階段可能被重復(fù)的中斷和重新執(zhí)行,而 componentWillMount 就包含在 render 階段中。
注意:工作拆分的最小單元應(yīng)該是一個(gè)fiber節(jié)點(diǎn),當(dāng)某個(gè)fiber節(jié)點(diǎn)本身的計(jì)算就十分巨大時(shí)依然會導(dǎo)致卡幀,不過我們可以通過調(diào)整工作的優(yōu)先級使得用戶的體驗(yàn)是平滑的
三、簡單模擬實(shí)現(xiàn) concurrent mode 的遞增式渲染
- 下面使用 requestIdleCallback 函數(shù)模擬時(shí)間片,在每一幀的空閑時(shí)間進(jìn)行js計(jì)算從而達(dá)到遞增式渲染的效果
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="index.js"></script> </head> <body> <div id="root"></div> <script> // 調(diào)用render提供掛載容器 "root" render(document.getElementById('root')) </script> </body> </html>
index.js
// 頁面需要渲染的組件 function Counter() { return { type: 'span', value: 'hello world', next: { type: 'p', value: 'hello LiHua' } } } const CounterElementDescriptors = { type: 'Function', fn: Counter } // 記錄當(dāng)前工作 let presentWork = null // 記錄根元素 let rootElementDescriptor = null // 記錄掛載容器 let elementsContainer = null // 處理單元任務(wù) function performUnitOfWork(deadline) { // 判斷當(dāng)前是否還有待執(zhí)行任務(wù) if (presentWork == null) return commitRoot(rootElementDescriptor) // 當(dāng)前幀超時(shí),調(diào)用 requestIdleCallback 把任務(wù)推到下一幀空閑時(shí)間執(zhí)行 if (deadline.didTimeout) return requestIdleCallback(executeWorkLoop) // 若是組件則處理依賴關(guān)系、若是元素則生成真實(shí)dom if (presentWork.type === "Function") { rootElementDescriptor = presentWork const firstChildren = presentWork.fn() firstChildren.parent = presentWork presentWork.children = firstChildren presentWork = firstChildren performUnitOfWork(deadline) } else { const dom = document.createElement(presentWork.type) dom.innerHTML = presentWork.value presentWork.dom = dom presentWork = presentWork.next performUnitOfWork(deadline) } } // 控制循環(huán)執(zhí)行工作 function executeWorkLoop(deadline) { performUnitOfWork(deadline) } // 提供render函數(shù),用于獲取掛載容器和開始渲染計(jì)算工作 function render(element) { elementsContainer = element presentWork = CounterElementDescriptors requestIdleCallback(executeWorkLoop) } // 模擬commit階段 function commitRoot(rootElement) { let renderCHildrenElements = rootElement.children do { elementsContainer.appendChild(renderCHildrenElements.dom) renderCHildrenElements = renderCHildrenElements.next }while(renderCHildrenElements) }
四、與優(yōu)先級調(diào)度有關(guān)的兩個(gè)hooks
1. useTransition
官方解釋:useTransition 是一個(gè)讓你在不阻塞 UI 的情況下來更新狀態(tài)的 React Hook。
- 通過 useTransition 我們可以將一部分的狀態(tài)更新工作劃分為低優(yōu)先級的異步任務(wù),使它不阻塞主要任務(wù)的執(zhí)行
- 同時(shí)我們可以依據(jù) useTransition 返回的標(biāo)志狀態(tài)在渲染期間優(yōu)雅地展示加載狀態(tài),從而提高用戶界面的交互體驗(yàn)和流暢性
- useTransition 主要語法如下:
import { useTransition } from "react"; function TabContainer() { // isPending 標(biāo)志,告訴你是否存在待處理的低優(yōu)先級工作。 // startTransition 函數(shù) 允許你將該部分的狀態(tài)更新標(biāo)記為低優(yōu)先級。 const [isPending, startTransition] = useTransition(); function handle() { startTransition(() => { // 低優(yōu)先級的狀態(tài)更新工作 {......} }); } return ( {......} ) }
2. useDeferredValue
官方解釋:useDeferredValue 是一個(gè) React Hook,可以讓你延遲更新 UI 的某些部分。
- 通過 useDeferredValue 我們可以將一部分的UI更新工作劃分為低優(yōu)先級的任務(wù),使它不阻塞主要任務(wù)的執(zhí)行
- useTransition 主要語法如下:
import { useDeferredValue, useState, } from "react"; function TabContainer() { const [query, setQuery] = useState(''); // 定義的 deferredQuery 獲取的是query的延遲版本 const deferredQuery = useDeferredValue(query); function handle(data) { setQuery(data) } return ( <> <List listData={deferredQuery} /> { ......} </> ) }
3. useTransition 與 useDeferredValue 的區(qū)別
- useTransition 用于控制過渡狀態(tài),可以在過渡狀態(tài)中執(zhí)行任務(wù),并提供過渡狀態(tài)的布爾值來判斷是否處于過渡狀態(tài)。
- useDeferredValue 用于延遲某個(gè)值的更新,以避免在渲染過程中處理昂貴的計(jì)算或數(shù)據(jù)獲取,確保界面的流暢性。
- 雖然它們都與并發(fā)模式相關(guān),但用途和作用略有不同,具體使用哪一個(gè)需要看具體場景。
4. 應(yīng)用場景
1)長列表渲染:當(dāng)渲染大量列表項(xiàng)時(shí),可以對列表項(xiàng)的渲染任務(wù)調(diào)節(jié)為低優(yōu)先級異步任務(wù),以保證用戶界面的響應(yīng)性能。
2)大型表單處理:對于包含大量輸入字段的表單,可以使用合理使用對于hooks將表單提交和驗(yàn)證等任務(wù)進(jìn)行優(yōu)化調(diào)節(jié),以避免阻塞用戶界面。
3)圖片懶加載:當(dāng)頁面中包含大量圖片時(shí),可以使用 useTransition 將圖片的加載劃分為多個(gè)低優(yōu)先級異步任務(wù),在渲染期間逐步加載圖片,以減少對用戶界面的阻塞。
4)異步數(shù)據(jù)加載:當(dāng)頁面中的數(shù)據(jù)需要從后端異步加載時(shí),可以使用 useTransition 將數(shù)據(jù)的加載劃分為多個(gè)異步任務(wù),以保證用戶界面的響應(yīng)性能。
五、一個(gè)小例子
- 以下以長列表渲染為例子做演示
基礎(chǔ)代碼,未作優(yōu)化處理:
import React, { useCallback, useState } from 'react' const index: React.FC = () => { const [list, setList] = useState<any[]>([]) const handleSearch = useCallback((value: string) => { const newList = [] for (let i = 0; i < 5000; i++) { newList.push(value + '-' + i) } setList(newList) }, []) return ( <> <input onChange={(e) => handleSearch(e.target.value)} type='text' /> <div> {list.map(item => <div key={item}>數(shù)據(jù)項(xiàng):{item}</div>)} </div> </> ) } export default index
當(dāng)我們進(jìn)行持續(xù)的輸入時(shí)是十分的卡頓的,效果如下:
1. 下面使用 useTransition 進(jìn)行優(yōu)化
- 降低 “setList(newList)” 的優(yōu)先級,使其不阻塞用戶輸入事件的觸發(fā)
代碼修改如下:
import React, { useCallback, useState, useTransition } from 'react' const index: React.FC = () => { const [list, setList] = useState<any[]>([]) const [isPending, startTransition] = useTransition() const handleSearch = useCallback((value: string) => { startTransition(() => { const newList = [] for (let i = 0; i < 5000; i++) { newList.push(value + '-' + i) } setList(newList) }) }, []) return ( <> <input onChange={(e) => handleSearch(e.target.value)} type='text' /> <div> {isPending? '加載中。。。' : list.map(item => <div key={item}>數(shù)據(jù)項(xiàng):{item}</div>)} </div> </> ) } export default index
優(yōu)化后效果如下:
2. 使用 useDeferredValue 進(jìn)行優(yōu)化
- 降低 “列表部分UI” 更新渲染的優(yōu)先級,使其不阻塞用戶輸入事件的觸發(fā)
代碼修改如下:
import React, { memo, useDeferredValue, useState } from 'react' const Item = ({ text }: any) => { return ( <div> 數(shù)據(jù)項(xiàng):{text} </div> ) } const List = memo(({ inputValue }: { inputValue: string }) => { let items = []; for (let i = 0; i < 5000; i++) { items.push(<Item key={i} text={inputValue + '-' + i} />); } return ( <> {items} </> ); }) const index: React.FC = () => { const [inputValue, setInputValue] = useState('') const deferredInputValue = useDeferredValue(inputValue) return ( <> <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} type='text' /> <List inputValue={deferredInputValue} /> </> ) } export default index
優(yōu)化后效果如下:
補(bǔ)充:為什么VUE不需要設(shè)計(jì) Concurrent Mode
- 出于vue響應(yīng)式系統(tǒng)的設(shè)計(jì)實(shí)現(xiàn)思路的不同,也就體現(xiàn)了為什么。
1)在vue中,響應(yīng)式系統(tǒng)通過 proxy 實(shí)現(xiàn)對 render函數(shù) 的依賴收集和觸發(fā)更新,基于追蹤組件依賴的響應(yīng)式數(shù)據(jù)的變化,可以更為精準(zhǔn)的實(shí)現(xiàn)組件的更新,大大避免了不必要的渲染和更新操作,規(guī)避了react中狀態(tài)更新就會觸發(fā)組件及該組件下所有子組件無腦更新的問題。
2)同時(shí)vue的異步更新策略也有助于提高性能和響應(yīng)能力。Vue會在下一個(gè)事件循環(huán)周期中批量更新組件,這樣可以避免頻繁的DOM操作和重復(fù)渲染,提高渲染效率。
3)但vue中暫時(shí)沒有 useTransition 和 useDeferredValue 類似的功能操作,無法調(diào)度控制作業(yè)的優(yōu)先級
以上就是React渲染機(jī)制及相關(guān)優(yōu)化方案的詳細(xì)內(nèi)容,更多關(guān)于React渲染機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react?express實(shí)現(xiàn)webssh?demo解析
這篇文章主要為大家介紹了詳解react?express實(shí)現(xiàn)webssh?demo解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04解決React報(bào)錯(cuò)Expected an assignment or funct
這篇文章主要為大家介紹了React報(bào)錯(cuò)Expected an assignment or function call and instead saw an expression解決方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12react-router JS 控制路由跳轉(zhuǎn)實(shí)例
這篇文章主要介紹了react-router JS 控制路由跳轉(zhuǎn)實(shí)例,react實(shí)現(xiàn)路由可以直接使用react-router。有興趣的可以了解一下2017-06-06詳解create-react-app 2.0版本如何啟用裝飾器語法
這篇文章主要介紹了詳解create-react-app 2.0版本如何啟用裝飾器語法,cra2.0時(shí)代如何啟用裝飾器語法呢? 我們依舊采用的是react-app-rewired, 通過劫持webpack cofig對象, 達(dá)到修改的目的2018-10-10react純函數(shù)組件setState更新頁面不刷新的解決
在開發(fā)過程中,經(jīng)常遇到組件數(shù)據(jù)無法更新,本文主要介紹了react純函數(shù)組件setState更新頁面不刷新的解決,感興趣的可以了解一下2021-06-06