react歷次版本迭代主要想解決的是兩類導(dǎo)致網(wǎng)頁卡頓的問題卒煞,分別是cpu密集型任務(wù)和io密集型任務(wù)導(dǎo)致的卡頓問題企蹭,react18提出的并發(fā)特性(Concurrent Rendering)就是為了解決上述問題。
Concurrent Rendering
什么是concurrent
concurrent不算是個新鮮概念,react很早之前就開始為其鋪路竖配,早在v16/v17就引入了fiber架構(gòu)和實驗性的concurrent Mode,開啟后整個應(yīng)用會開啟并發(fā)更新模式里逆,但這將帶來較大的breaking changes进胯。因此react18提出了Concurrent Rendering的概念,即沒有并發(fā)模式运悲,只有并發(fā)特性龄减,也就是說并發(fā)特性只是個可選項。默認(rèn)情況下整個應(yīng)用仍使用同步更新(legacy模式)班眯,在使用了并發(fā)特性后相關(guān)的更新再開啟并發(fā)更新希停,不用的話就沒有breaking changes烁巫。
concurrent帶來的變動可以概括為以下兩點:
時間分片
該模式下當(dāng)更新任務(wù)的render過程無法在瀏覽器的一幀內(nèi)完成時,會被分為多個task進(jìn)行可中斷的更新宠能,以此來保證瀏覽器每一幀都有空余時間進(jìn)行繪制亚隙,可以說時間分片是concurrent的實現(xiàn)基礎(chǔ)。
更新優(yōu)先級
該模式下更新任務(wù)會帶有優(yōu)先級违崇,低優(yōu)先級任務(wù)的執(zhí)行將讓位于高優(yōu)先級任務(wù)阿弃。
這句話有兩層含義,后面會有具體示例說明
同一上下文中的高優(yōu)先級任務(wù)將優(yōu)先執(zhí)行
不同上下文中的高優(yōu)先級任務(wù)將打斷正在執(zhí)行的低優(yōu)先級任務(wù)
什么是優(yōu)先級
legacy模式下沒有優(yōu)先級的概念羞延,因此所有任務(wù)都是同步執(zhí)行渣淳。而開啟并發(fā)特性后一切行為的基礎(chǔ)就是任務(wù)的優(yōu)先級。
在react應(yīng)用中我們可能在不同上下文中觸發(fā)setState伴箩,如點擊/輸入事件入愧,異步接口回調(diào),react18中也可能在concurrentAPI中觸發(fā)等等嗤谚。在react18中不同上下文中觸發(fā)的setState的優(yōu)先級是不一樣的棺蛛。react使用lane模型來描述優(yōu)先級,該模型使用31位二進(jìn)制來表示優(yōu)先級, 位數(shù)越泄健(值越信陨蕖)則優(yōu)先級越高。
以下是項目中最常見的幾類任務(wù)的優(yōu)先級
// 離散事件優(yōu)先級椅野,例如:點擊事件终畅,input輸入,focus等觸發(fā)的更新任務(wù)鳄橘,優(yōu)先級最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 連續(xù)事件優(yōu)先級声离,例如:滾動事件,拖動事件等
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默認(rèn)事件優(yōu)先級瘫怜,例如:異步接口回調(diào)中觸發(fā)的更新任務(wù)
export const DefaultEventPriority: EventPriority = DefaultLane;
渲染模式對比
結(jié)合performance對比下legacy與conurrent這兩種渲染模式
Legacy Mode
所謂legacy模式术徊,即傳統(tǒng)的react渲染模式,我們使用reactDOM.render創(chuàng)建的react應(yīng)用都是使用這種模式鲸湃,下面以一個demo為例分析赠涮。
渲染模式對比示例demo 在demo中用一個定時器延遲1000ms模擬接口請求,另一個定時器延遲1040ms模擬觸發(fā)一次點擊事件暗挑,可以明顯看到列表渲染和點擊事件的更新結(jié)果先后展示在視圖中笋除,分析:
可以看到列表請求觸發(fā)的更新和點擊事件觸發(fā)的更新先后進(jìn)行render,而列表更新的整個過程處于一個宏任務(wù)中且耗時200多ms炸裆,瀏覽器每16ms刷新一次垃它,因此導(dǎo)致render的過程中瀏覽器的每一幀都沒有時間繪制,反應(yīng)到真實場景中將使得用戶感到點擊操作卡頓。
總結(jié):legacy模式下的所有更新都是同步調(diào)度国拇,沒有優(yōu)先級之分洛史。
legacy下的更新調(diào)度流程
Concurrent Rendering
將上述demo開啟并發(fā)特性觀察效果,可以看到雖然點擊事件在時序上是后觸發(fā)的酱吝,但其更新結(jié)果卻優(yōu)先提交到了視圖也殖。分析:
注意到點擊事件觸發(fā)時已經(jīng)處于接口數(shù)據(jù)的render過程中,隨后點擊事件觸發(fā)的更新打斷了正在進(jìn)行的render而優(yōu)先執(zhí)行务热,提交到視圖后繼續(xù)進(jìn)行接口數(shù)據(jù)的render忆嗜。
總結(jié):
開啟并發(fā)特性后更新任務(wù)將帶有優(yōu)先級,click事件的更新優(yōu)先級高于接口請求的更新優(yōu)先級崎岂,因而前者會打斷后者的render過程優(yōu)先執(zhí)行捆毫。
當(dāng)更新的render流程過于耗時而超過瀏覽器一幀的時間時,更新任務(wù)將被分割為多個task進(jìn)行可中斷的更新冲甘,每個task的執(zhí)行時間不超過16ms(time slice)冻璃。這使得瀏覽器的每一幀中有空余時間進(jìn)行繪制,點擊事件的更新可以優(yōu)先呈現(xiàn)到視圖中损合。
渲染階段(commit)是不可被打斷的。
并發(fā)模式帶來的優(yōu)勢是顯然易見的娘纷,他使得瀏覽器在任何情況下都有空余時間繪制嫁审,使得在不同性能的設(shè)備上緊急任務(wù)都能優(yōu)先render并提交到視圖。
concurrent下的更新調(diào)度流程圖
如何開啟Concurrent Rendering
react18提供了新的根結(jié)點創(chuàng)建方式:ReactDOM.createRoot()赖晶。使用此API創(chuàng)建的react應(yīng)用將啟用react18全部新特性律适。
import React from 'react';
// 注意這里ReactDOM是從client引入
import ReactDOM from 'react-dom/client';
import App from './contest';
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
出于兼容性考慮,傳統(tǒng)的ReactDOM.renderAPI也會繼續(xù)保留遏插,使用ReactDOM.render創(chuàng)建的react18應(yīng)用的表現(xiàn)與react17完全一致捂贿。
Concurrent Render API
下面是react18新引入的用于開啟并發(fā)特性的API,只有用到這些API時才會開啟并發(fā)更新胳嘲。
startTransition
這是react18新引入的一個API厂僧,它允許我們以一個過渡優(yōu)先級( TransitionLane)來調(diào)度一次更新×伺#可以稱這類更新為過渡任務(wù)颜屠。過渡任務(wù)擁有較低的優(yōu)先級,它帶來的影響可以從以下兩方面分析:
1.過渡任務(wù)的執(zhí)行過程將開啟時間分片
開啟時間分片后鹰祸,當(dāng)任務(wù)耗時過長時可以保證每一幀都能空出時間交給瀏覽器繪制甫窟,使得試圖不卡頓。
2.過渡任務(wù)的由于優(yōu)先級較低蛙婴,因此將讓位于其他高優(yōu)先級任務(wù)粗井。
高優(yōu)先級任務(wù)優(yōu)先執(zhí)行示例demo
以下稱setA(20000)為任務(wù)A,setB(1)為任務(wù)B
對于任務(wù)A,當(dāng)我們不用startTransition調(diào)度時浇衬,可以明顯看出a b以及列表是同時展示出來的懒构,這是由于effect中的兩次更新由于優(yōu)先級一致因此被合并更新,同步執(zhí)行径玖。當(dāng)使用startTransition調(diào)度時明顯看到b優(yōu)先變?yōu)?痴脾,這是由于任務(wù)A優(yōu)先級低于任務(wù)B,因此優(yōu)先執(zhí)行任務(wù)B梳星。即同一上下文中高優(yōu)先級任務(wù)將優(yōu)先執(zhí)行赞赖。
與setTimeout的區(qū)別
上述demo看起來似乎用setTimeout也能達(dá)到類似效果,事實上此API與setTimeout最重要的區(qū)別是處于transtions狀態(tài)的任務(wù)是可以中斷渲染的冤灾,是可以被高優(yōu)先級任務(wù)打斷的前域。對于渲染并發(fā)的場景下,setTimeout 仍然會使頁面卡頓韵吨,因為超時后匿垄,setTimeout 的任務(wù)還是會執(zhí)行且不可被打斷,仍然會阻塞頁面交互归粉。
將demo改造下椿疗,startTransition改為setTimeout,并在200ms后模擬觸發(fā)一次點擊事件(任務(wù)C)糠悼。由于點擊事件處于setTimeout中因此在任務(wù)隊列中它會排在任務(wù)A之后届榄,而列表的渲染時間明顯多于200ms,因此當(dāng)任務(wù)C執(zhí)行時一定還處于任務(wù)A的render過程中倔喂。而我們可以看到任務(wù)C的更新結(jié)果最后展示的铝条,這也印證了setTimeout中的更新任務(wù)一旦開始就是不可被打斷的。
最后將任務(wù)A還原為startTransition調(diào)度席噩,可以看到任務(wù)B班缰,任務(wù)C先后提交,任務(wù)A最后提交悼枢。我們知道點擊事件的優(yōu)先級高于過渡優(yōu)先級埠忘,因此任務(wù)C可以打斷任務(wù)A的render過程優(yōu)先執(zhí)行,這其實就是典型的高優(yōu)先級任務(wù)打斷低優(yōu)先級任務(wù)的執(zhí)行馒索。
與節(jié)流防抖的區(qū)別
節(jié)流防抖解決的是也是頻繁觸發(fā)渲染的問題给梅,但是還是會存在一些問題。比如100ms防抖双揪,當(dāng)列表渲染非扯穑快時,遠(yuǎn)遠(yuǎn)小于100ms
渔期,但是卻需要等待到100ms
后才會開始執(zhí)行更新运吓。而節(jié)流則無法解決更新耗時過長的問題渴邦。比如列表渲染需要耗時1s
,那么在這1s
內(nèi)用戶依舊無法進(jìn)行交互拘哨,其實與setTimeout的問題是類似的谋梭,而trasntions任務(wù)在過渡期間理論上是可以多次被高優(yōu)先級任務(wù)打斷的。
useTransition
startTransition可以調(diào)度一個過渡任務(wù)倦青,過渡任務(wù)有一個過渡期瓮床,可以認(rèn)為過渡任務(wù)的更新在被提交到視圖之前都屬于過渡期,而用戶無法感知當(dāng)前是否處于過渡期产镐。為了解決這個問題隘庄,React 提供了一個帶有 isPending 狀態(tài)的 hook:useTransition 。useTransition 執(zhí)行后返回一個數(shù)組癣亚,數(shù)組有兩個狀態(tài)值:
當(dāng)處于過渡狀態(tài)的標(biāo)志—isPending丑掺。
startTransition,可以把里面的更新任務(wù)變成過渡任務(wù)述雾,等同于與上述的startTransitionAPI街州。
import { useTransition } from 'react'
const [ isPending , startTransition ] = useTransition ()
那么當(dāng)任務(wù)處于過渡狀態(tài)的時,isPending 為 true玻孟,可以作為用戶等待的 UI 呈現(xiàn)唆缴。比如:
{ isPending && <Spinner/> }
useDefferedValue
useDeferredValue 的實現(xiàn)效果也類似于startTransition。
const [a, setA] = useState(0);
const deferredA = useDeferredValue(a);
useDeferredValue實質(zhì)上是基于原始state生產(chǎn)一個新的state(DeferredValue)黍翎,當(dāng)對原始state進(jìn)行setState時琐谤,DeferredValue的值會通過過渡任務(wù)得到,因此視圖中使用DeferredValue就會得到和startTransitionAPI一樣的效果玩敏,事實上這兩個API相當(dāng)于從兩個角度實現(xiàn)過渡任務(wù),本質(zhì)上是一樣的质礼。
其他變更
Automatic Batching
legacy模式下旺聚,除合成事件&生命周期外,在其他的事件回調(diào)中(異步方法眶蕉,原生事件等)的多次setState不會批量處理砰粹,即每次setState都會render一次。
每次setState后我們可以通過ref拿到最新的dom屬性造挽,在legacy模式下可以使用ReactDom.unstable_batchedUpdates強(qiáng)制批量更新碱璃,而在react18應(yīng)用中任何事件回調(diào)中的多次setState都會合并處理。
這是由于新版batchedUpdate的實現(xiàn)基于更新優(yōu)先級饭入,只要更新的優(yōu)先級一致那么更新將合并嵌器。
flushSync
特殊情況需要立即獲取更新結(jié)果時可以使用react18新提供的flushSync。
transitions與Suspense配合解決io瓶頸(不穩(wěn)定)
Suspense 是 React 提供的一種異步處理機(jī)制谐丢,在v16/v17中爽航,Suspense主要是配合React.lazy進(jìn)行組件層面的code spliting蚓让,而未來react希望逐步將Suspense用于所有的異步操作場景,目前已有相關(guān)API/庫進(jìn)行支持讥珍。
demo中幾乎看不到異步代碼历极,完全用同步的思維獲取接口數(shù)據(jù)且不會用帶async/await這種語法糖。我們認(rèn)為數(shù)據(jù)是已經(jīng)存在的衷佃,我們做的只是讀數(shù)據(jù)而非拉數(shù)據(jù)趟卸。
上述demo的執(zhí)行流程如下:
首次調(diào)用userResource.read,會創(chuàng)建一個promise(即fetchUser的返回值)氏义。
由于是同步調(diào)用因此取不到數(shù)據(jù)锄列,此時userResource中會將這個promise throw出去。
React內(nèi)部會catch這個promise(handleEfrror)觅赊,離User組件最近的祖先Suspense組件渲染fallback
// renderRootConcurrent
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
- promsie resolve或reject時重新觸發(fā)一次調(diào)度右蕊,此時再調(diào)用userResource.read會返回resolve/reject的結(jié)果(即fetchUser請求的數(shù)據(jù)),使用該數(shù)據(jù)繼續(xù)render
這里關(guān)鍵是userResource的實現(xiàn)吮螺,目前react有一個專門提供createReouceAPI的庫react-cache饶囚,但目前還處于實驗階段無法用于生產(chǎn)環(huán)境,下面簡單分析下實現(xiàn)原理鸠补。
const cache = {};
export function createResource(fetch) {
const resource = {
read: params => {
// 這里臨時用id做個緩存 react-cache內(nèi)部有一套單獨的緩存清理算法
if (!cache[params]) {
const promise = fetch(params);
let suspender = promise.then(
r => {
cache[params].status = 'resolved';
cache[params].result = r;
},
e => {
cache[params].status = 'rejected';
cache[params].result = e;
}
);
cache[params] = {
promise: suspender,
status: 'pending',
result: null,
};
throw suspender;
} else {
if (cache[params].status === 'resolved') {
return cache[params].result;
}
throw cache[params].promise;
}
},
};
return resource;
}
react18新增的concurrentAPI可以配合suspense使用萝风,當(dāng)startTranstion調(diào)度的更新任務(wù)觸發(fā)任意一個suspense組件掛起時,將導(dǎo)致當(dāng)前組件進(jìn)入pending狀態(tài)紫岩,此時只要關(guān)聯(lián)了suspense的變更就會被‘暫凸娑瑁’提交,直到在內(nèi)存中構(gòu)建完成后才會被會提交泉蝌。此特性一般用于接口返回較快且有l(wèi)oading的頁面歇万,可以在避免閃爍問題的同時,使得在視圖切換前仍然可以保持響應(yīng)勋陪。
將上述demo改造下
transitions+Suspense處理異步示例demo
目前suspense處理異步場景相關(guān)的庫尚不穩(wěn)定贪磺,猜測此特性后面可能由路由庫集成并暴露相關(guān)api給用戶。
移除inUnmount警告
日常開發(fā)中經(jīng)常碰到這個警告诅愚,它的本意是避免由于未及時清理effect hook中的訂閱而導(dǎo)致的內(nèi)存泄漏問題
useEffect(() => {
function handleChange() {
setState(store.getState());
}
store.subscribe(handleChange);
return () => store.unsubscribe(handleChange);
}, []);
但日常開發(fā)中更多的警告場景是在已卸載的組件中進(jìn)行setState
async function handleSubmit() {
setLoading(true);
// 在我們等待時組件可能會卸載
await post('/some-api');
setLoading(false);
}
實際上這里并沒有實際的內(nèi)存泄漏寒锚,promise在resolve之后就會被回收。對于這種警告我們一般的實踐是手動判斷isUnmount违孝,但這實際上只是抑制了警告刹前,并沒有解決實質(zhì)問題且會增加代碼復(fù)雜度,因此是沒有必要的雌桑。react此次更新旨在剔除業(yè)務(wù)代碼中所有的isUnmount判斷喇喉。
組件卸載后setState會不會有其他副作用?
react觸發(fā)的任何更新最終都通過scheduleUpdateOnFiber進(jìn)行調(diào)度校坑,當(dāng)觸發(fā)更新所在組件已卸載時不繼續(xù)進(jìn)行調(diào)度流程轧飞,因此不會產(chǎn)生其他副作用衅鹿。
// scheduleUpdateOnFiber
// 當(dāng)內(nèi)部獲取不到根fiber節(jié)點時就不再繼續(xù)調(diào)度更新
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
function markUpdateLaneFromFiberToRoot(
){
let node = sourceFiber;
// fiber.return代表父節(jié)點,若當(dāng)前fiber對應(yīng)dom已卸載則fiber.return為null
let parent = sourceFiber.return;
while (parent !== null) {
// 方法內(nèi)部會向上遍歷到根fiber節(jié)點
}
// 當(dāng)遍歷結(jié)果不是根fiber時會返回null
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
return null;
}
}
允許組件返回undefined
React 17 中如果組件在 render 中返回了 undefined过咬,React 會在運行時拋錯大渤。
用于三方庫的API
以下API一般用于三方庫的開發(fā),通常不會用于實際業(yè)務(wù)開發(fā)當(dāng)中掸绞。
useInsertionEffect
這個Hook執(zhí)行時機(jī)在 DOM 生成之后泵三,useLayoutEffect執(zhí)行之前,此時無法訪問DOM節(jié)點的引用衔掸。一般用于解決 CSS-in-JS 庫在渲染中動態(tài)注入樣式的性能問題烫幕。
useSyncExternalStore
此API一般用于第三方狀態(tài)管理庫如redux/mobx,它們在控制狀態(tài)時可能并非直接使用react的 state敞映,而是自己在外部維護(hù)了一個store對象较曼,脫離了react的管理,此時若使用concurrentAPI則可能出現(xiàn)兼容性問題振愿,useExternalStore就是為了解決這種問題捷犹,基本實現(xiàn)原理是將render過程強(qiáng)制變?yōu)橥降牟豢芍袛嗟母隆?/p>
SSR
更多信息可見Upgrading to React 18 on the server、New Suspense SSR Architecture in React 18
升級指南
收益點
理論上任何由cpu密集型任務(wù)導(dǎo)致的卡頓問題都可以考慮是否可用并發(fā)特性優(yōu)化冕末。
兼容性
只要不用concurrentAPI那么表現(xiàn)將與舊版本一致萍歉。
redux/mobx兼容性
正常用沒問題(同步更新),但不能使用concurrentAPI調(diào)度store的更新操作档桃。
startTransition調(diào)度mobx更新示例demo
更新開始后枪孩,有 10 個 ShowText 節(jié)點需要render, 每個節(jié)點render時需要耗時 20ms 以上藻肄,這就導(dǎo)致每個 ShowText render結(jié)束以后都需要中斷讓出主線程蔑舞。在協(xié)調(diào)中斷時,修改 store 狀態(tài)嘹屯,后續(xù)的 ShowText 節(jié)點在恢復(fù) render 時攻询,會使用修改以后的 store 狀態(tài)導(dǎo)致最后出現(xiàn)狀態(tài)不一致的情況,因此在實際業(yè)務(wù)中極端情況下可能會出問題抚垄。
這是由于demo中脫離react state而在外部單獨維護(hù)數(shù)據(jù)源,而concurrent是react內(nèi)部狀態(tài)的處理機(jī)制谋逻,因此外部數(shù)據(jù)是無法處理更新中斷的問題(內(nèi)容撕裂問題)呆馁,redux中表現(xiàn)也是如此,不過最新的react-redux8.0.0中已經(jīng)使用useSyncExternalStoreAPI解決了此問題毁兆。
多次render帶來的影響
不安全生命周期
在高優(yōu)先級打斷低優(yōu)先級的case中浙滤,低優(yōu)先級任務(wù)事實上render了兩次,而
componentWillReceiveProps气堕、componentWillMount纺腊、componentWillUpdate 這幾個生命周期鉤子都是在render時觸發(fā)的畔咧,方法內(nèi)部都可以修改state,當(dāng)組件重復(fù)render時揖膜,不正當(dāng)的操作會引來額外的副作用誓沸,因此react將這幾個生命周期方法定義為 unsafe_xxxxx,在并發(fā)模式下可能有問題壹粟,目前項目中沒有用到這幾個鉤子拜隧。
饑餓問題
低優(yōu)先級任務(wù)的render過程多次被高優(yōu)先級任務(wù)打斷而得不到執(zhí)行的現(xiàn)象稱為饑餓問題。react通過過期時間來解決饑餓問題趁仙,不同優(yōu)先級對應(yīng)不同的過期時間洪添。當(dāng)?shù)蛢?yōu)先級任務(wù)一直未執(zhí)行而超過時期時間時該任務(wù)會被視為過期任務(wù),其優(yōu)先級會被提升為同步優(yōu)先級雀费,會立即執(zhí)行干奢。
重復(fù)render問題
由于這種case只有在使用concurrentAPI時才有可能出現(xiàn),因此在開啟concurrent時需確認(rèn)組件中是否有在effect之外處理的特殊邏輯盏袄,即組件每次render都會執(zhí)行的邏輯忿峻。
高優(yōu)先級打斷低優(yōu)先級任務(wù)導(dǎo)致組件重復(fù)render示例demo
關(guān)于batchUpdates
需要排除項目中是否存在強(qiáng)行在兩次setState中立即取值的case,react18中此場景需要結(jié)合flushSync使用
不支持IE11
Concurrent模式下的任務(wù)調(diào)度流程
最后簡單分析下concurrentMode下的任務(wù)更新調(diào)度流程貌矿。
concurrentMode下的任務(wù)更新可以概括為異步可中斷的更新
炭菌,這種基于更新任務(wù)的優(yōu)先級來統(tǒng)籌調(diào)度的模式的架構(gòu)基礎(chǔ)是fiber tree,它使得render過程中可以中斷逛漫。
function workLoopConcurrent() {
// 當(dāng)wip構(gòu)造完成或時間切片用盡時停止工作
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
workLoopConcurrent 都會判斷本次協(xié)調(diào)對應(yīng)的優(yōu)先級和上一次時間片到期中斷的協(xié)調(diào)的優(yōu)先級是否一樣黑低。如果一樣,說明沒有更高優(yōu)先級的更新產(chǎn)生酌毡,可以繼續(xù)上次未完成的協(xié)調(diào)毡泻;如果不一樣,說明有更高優(yōu)先級的更新進(jìn)來诲锹,此時要清空之前已開始的協(xié)調(diào)過程状囱,從根節(jié)點開始重新協(xié)調(diào)。等高優(yōu)先級更新處理完成以后旭蠕,再次從根節(jié)點開始處理低優(yōu)先級更新停团。
而調(diào)度的核心邏輯則主要來源于schduler模塊,schduler是一個獨立于react的專門用于任務(wù)調(diào)度的庫掏熬。react應(yīng)用每產(chǎn)生一個更新任務(wù)都會通過schduler模塊暴露的API(scheduleCallback)來注冊一個更新任務(wù)佑稠,此邏輯主要在ensureRootIsScheduled中完成:
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// 得到當(dāng)前正在調(diào)度的fiber節(jié)點
const existingCallbackNode = root.callbackNode;
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 獲得當(dāng)前根fiber節(jié)點下最高優(yōu)先級的lane
const newCallbackPriority = returnNextLanesPriority();
if (nextLanes === NoLanes) {
return;
}
if (existingCallbackNode !== null) {
// 當(dāng)本次調(diào)度的優(yōu)先級與正在調(diào)度的優(yōu)先級一致時則不繼續(xù)調(diào)度 auto batch
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
return;
}
// 若不一致,說明本次的調(diào)度優(yōu)先級一定高于正在調(diào)度的優(yōu)先級旗芬,取消當(dāng)前的調(diào)度
cancelCallback(existingCallbackNode);
}
// 注冊調(diào)度任務(wù)
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
// 同步優(yōu)先級 進(jìn)行同步調(diào)度 將在本輪事件循環(huán)同步執(zhí)行
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
} else if (newCallbackPriority === SyncBatchedLanePriority) {
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root),
);
} else {
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority,
);
// 非同步優(yōu)先級舌胶,使用schduler進(jìn)行異步調(diào)度
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// 更新標(biāo)記
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
schduler內(nèi)部維護(hù)一個隊列(taskQueue)來管理所有被調(diào)度的任務(wù),通過messageChannel來實現(xiàn)異步調(diào)度疮丛,此過程在unstable_scheduleCallback中完成幔嫂。每次調(diào)度都會從taskQueue中取出最高優(yōu)先級的任務(wù)執(zhí)行辆它,執(zhí)行過程中可能因為切片用盡或任務(wù)隊列已清空而中斷,再次回到隊列消費的邏輯履恩。此過程在workLoop中完成锰茉。