React 核心團(tuán)隊成員解釋“代數(shù)效應(yīng)與 React”
React核心團(tuán)隊成員Sebastian Markbåge[1](React Hooks的發(fā)明者)曾說:我們在React中做的就是踐行代數(shù)效應(yīng)(Algebraic Effects)。
那么,代數(shù)效應(yīng)是什么呢?它和React有什么關(guān)系呢。
什么是代數(shù)效應(yīng)
代數(shù)效應(yīng)是函數(shù)式編程中的一個概念,用于將副作用從函數(shù)調(diào)用中分離。
接下來我們用虛構(gòu)的語法來解釋。
假設(shè)我們有一個函數(shù)getTotalPicNum,傳入2個用戶名稱后,分別查找該用戶在平臺保存的圖片數(shù)量,最后將圖片數(shù)量相加后返回。
- function getTotalPicNum(user1, user2) {
- const num1 = getPicNum(user1);
- const num2 = getPicNum(user2);
- return picNum1 + picNum2;
- }
在getTotalPicNum中,我們不關(guān)注getPicNum的實現(xiàn),只在乎“獲取到兩個數(shù)字后將他們相加的結(jié)果返回”這一過程。
接下來我們來實現(xiàn)getPicNum。
"用戶在平臺保存的圖片數(shù)量"是保存在服務(wù)器中的。所以,為了獲取該值,我們需要發(fā)起異步請求。
為了盡量保持getTotalPicNum的調(diào)用方式不變,我們首先想到了使用async await:
- async function getTotalPicNum(user1, user2) {
- const num1 = await getPicNum(user1);
- const num2 = await getPicNum(user2);
- return picNum1 + picNum2;
- }
但是,async await是有傳染性的 —— 當(dāng)一個函數(shù)變?yōu)閍sync后,這意味著調(diào)用他的函數(shù)也需要是async,這破壞了getTotalPicNum的同步特性。
有沒有什么辦法能保持getTotalPicNum保持現(xiàn)有調(diào)用方式不變的情況下實現(xiàn)異步請求呢?
沒有。不過我們可以虛構(gòu)一個。
我們虛構(gòu)一個類似try...catch的語法 —— try...handle與兩個操作符perform、resume。
- function getPicNum(name) {
- const picNum = perform name;
- return picNum;
- }
- try {
- getTotalPicNum('kaSong', 'xiaoMing');
- } handle (who) {
- switch (who) {
- case 'kaSong':
- resume with 230;
- case 'xiaoMing':
- resume with 122;
- default:
- resume with 0;
- }
- }
當(dāng)執(zhí)行到getTotalPicNum內(nèi)部的getPicNum方法時,會執(zhí)行perform name。
此時函數(shù)調(diào)用棧會從getPicNum方法內(nèi)跳出,被最近一個try...handle捕獲。類似throw Error后被最近一個try...catch捕獲。
類似throw Error后Error會作為catch的參數(shù),perform name后name會作為handle的參數(shù)。
與try...catch最大的不同在于:當(dāng)Error被catch捕獲后,之前的調(diào)用棧就銷毀了。而handle執(zhí)行resume后會回到之前perform的調(diào)用棧。
對于case 'kaSong',執(zhí)行完resume with 230;后調(diào)用棧會回到getPicNum,此時picNum === 230
再次申明,try...handle的語法是虛構(gòu)的,只是為了演示代數(shù)效應(yīng)的思想。
總結(jié)一下:代數(shù)效應(yīng)能夠?qū)⒏弊饔茫ɡ又袨檎埱髨D片數(shù)量)從函數(shù)邏輯中分離,使函數(shù)關(guān)注點保持純粹。
并且,從例子中可以看出,perform resume不需要區(qū)分同步異步。
代數(shù)效應(yīng)在React中的應(yīng)用
那么代數(shù)效應(yīng)與React有什么關(guān)系呢?最明顯的例子就是Hooks。
對于類似useState、useReducer、useRef這樣的Hook,我們不需要關(guān)注FunctionComponent的state在Hook中是如何保存的,React會為我們處理。
我們只需要假設(shè)useState返回的是我們想要的state,并編寫業(yè)務(wù)邏輯就行。
- function App() {
- const [num, updateNum] = useState(0);
- return (
- <button onClick={() => updateNum(num => num + 1)}>{num}</button>
- )
- }
如果這個例子還不夠明顯,可以看看官方的Suspense Demo[2]
在Demo中ProfileDetails用于展示用戶名稱。而用戶名稱是異步請求的。
但是Demo中完全是同步的寫法。
- function ProfileDetails() {
- const user = resource.user.read();
- return <h1>{user.name}</h1>;
- }
代數(shù)效應(yīng)與Generator
從React15到React16,協(xié)調(diào)器(Reconciler)重構(gòu)的一大目的是:將老的同步更新的架構(gòu)變?yōu)楫惒娇芍袛喔隆?/p>
異步可中斷更新可以理解為:更新在執(zhí)行過程中可能會被打斷(瀏覽器時間分片用盡或有更高優(yōu)任務(wù)插隊),當(dāng)可以繼續(xù)執(zhí)行時恢復(fù)之前執(zhí)行的中間狀態(tài)。
這就是代數(shù)效應(yīng)中try...handle的作用。
其實,瀏覽器原生就支持類似的實現(xiàn),這就是Generator。
但是Generator的一些缺陷使React團(tuán)隊放棄了他:
類似async,Generator也是傳染性的,使用了Generator則上下文的其他函數(shù)也需要作出改變。這樣心智負(fù)擔(dān)比較重。
Generator執(zhí)行的中間狀態(tài)是上下文關(guān)聯(lián)的。
考慮如下例子:
- function* doWork(A, B, C) {
- var x = doExpensiveWorkA(A);
- yield;
- var y = x + doExpensiveWorkB(B);
- yield;
- var z = y + doExpensiveWorkC(C);
- return z;
- }
每當(dāng)瀏覽器有空閑時間都會依次執(zhí)行其中一個doExpensiveWork,當(dāng)時間用盡則會中斷,當(dāng)再次恢復(fù)時會從中斷位置繼續(xù)執(zhí)行。
只考慮“單一優(yōu)先級任務(wù)的中斷與繼續(xù)”情況下Generator可以很好的實現(xiàn)異步可中斷更新。
但是當(dāng)我們考慮“高優(yōu)先級任務(wù)插隊”的情況,如果此時已經(jīng)完成doExpensiveWorkA與doExpensiveWorkB計算出x與y。
此時B組件接收到一個高優(yōu)更新,由于Generator執(zhí)行的中間狀態(tài)是上下文關(guān)聯(lián)的的,所以重新計算y時無法復(fù)用之前已經(jīng)計算出的x,需要重新計算。
如果通過全局變量保存之前執(zhí)行的中間狀態(tài),又會引入新的復(fù)雜度。
更詳細(xì)的解釋可以參考這個issue[3]
基于這些原因,React沒有采用Generator實現(xiàn)協(xié)調(diào)器。
代數(shù)效應(yīng)與Fiber
Fiber并不是計算機(jī)術(shù)語中的新名詞,他的中文翻譯叫做纖程,與進(jìn)程(Process)、線程(Thread)、協(xié)程(Coroutine)同為程序執(zhí)行過程。
在很多文章中將纖程理解為協(xié)程的一種實現(xiàn)。在JS中,協(xié)程的實現(xiàn)便是Generator。
所以,我們可以將纖程(Fiber)、協(xié)程(Generator)理解為代數(shù)效應(yīng)思想在JS中的體現(xiàn)。
React Fiber可以理解為:
React內(nèi)部實現(xiàn)的一套狀態(tài)更新機(jī)制。支持任務(wù)不同優(yōu)先級,可中斷與恢復(fù),并且恢復(fù)后可以復(fù)用之前的中間狀態(tài)。
其中每個任務(wù)更新單元為React Element對應(yīng)的Fiber節(jié)點。





























