Algebraic Effects是一個在編程語言研究領域新興的機制,雖然目前尚未工業語言實現它,可是在React社區會常常聽到關於它的討論。React最近的不少新特性的背後其實是Algebraic Effects的概念。所以,我花了一些時間來了解Algebraic Effects,但願體悟到React團隊是如何理解這些新特性的。html
每個Algebraic Effect都是一次【程序控制權】的巡迴:react
【effect發起者】發起effect,並暫停執行(暫時交出程序控制權)
-> 沿着調用棧向上查找對應的effect handler(相似於try...catch的查找方式)
-> effect handler執行(得到程序控制權)
-> effect handler執行完畢,【effect發起者】繼續執行(歸還程序控制權)git
例子(這並非合法的JavaScript):github
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; // perform an effect to get a default name! } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: 'Gendry' }; try { makeFriends(arya, gendry); } handle (effect) { // effect handler! if (effect === 'ask_name') { const defaultName = await getDefaultNameFromServer(); resume with defaultName; // jump back to the effect issuer, and pass something back! } } console.log('done!');
注意幾點:編程
effect發起者不須要知道effect是如何執行的(解耦),effect的執行邏輯由調用者來定義。redux
這一點與try...catch相同,拋出錯誤的人不須要知道錯誤是如何被處理的。
getName
能夠當作純函數。易於測試。
effect執行完之後,會回到effect發起處,並提供effect的執行結果。segmentfault
這一點與try...catch不一樣,try...catch沒法恢復執行。
makeFriends
。用async / await實現上面的例子:promise
async function getName(user) { let name = user.name; if (name === null) { name = await getDefaultNameFromServer(); } return name; } async function makeFriends(user1, user2) { user1.friendNames.add(await getName(user2)); user2.friendNames.add(await getName(user1)); } const arya = { name: null }; const gendry = { name: 'Gendry' }; makeFriends(arya, gendry) .then(() => console.log('done!'));
能夠發現,makeFriends
如今變成異步的了。這是由於異步性會感染全部上層調用者。若是要將某個同步函數改爲async函數,是很是困難的,由於它的全部上層調用者都須要修改。
而在前面Algebraic Effects的例子中,中間調用者makeFriends
對Algebraic Effects是無感的。只要在某個上層調用者提供了effect handler就好。網絡
注意另外一點,getName
直接耦合了反作用方法getDefaultNameFromServer
。而在前面Algebraic Effects的例子中,反作用的執行邏輯是【在運行時】【經過調用關係】【動態地】決定的。這大大加強了getName的可複用性。架構
在async / await的例子中,經過依賴注入可以達到與Algebraic Effects相似的可複用性。若是getName
經過依賴注入來獲得反作用方法getDefaultNameFromServer
,那麼getName
函數在可複用性上,確實與使用Algebraic Effects時相同。可是前面所說的【異步性會感染全部上層調用者】的問題依然存在,getName
和makeFriends
都要變成異步的。
與async / await相似,Generator Function的調用者在調用Generator Function時也是有感的。Generator Function將程序控制權交給它的直接調用者,而且只能由直接調用者來恢復執行、提供結果值。
直接調用者也能夠選擇將程序控制權沿着執行棧繼續向上交。這樣的話,直接調用者(下面例子的makeFriends
)本身也要變成Generator Function(被感染,與async / await相似),直到遇到能提供【結果值】的調用者(下面例子的main
)。
function* getName(user) { let name = user.name; if (name === null) { name = yield 'ask_name'; } return name; } function* makeFriends(user1, user2) { user1.friendNames.add(yield* getName(user2)); user2.friendNames.add(yield* getName(user1)); } async function main() { const arya = { name: null }; const gendry = { name: 'Gendry' }; let gen = makeFriends(arya, gendry); let state = gen.next(); while(!state.done) { if (state.value === 'ask_name') { state = gen.next(await getDefaultNameFromServer()); } } } main().then(()=>console.log('done!'));
能夠看出,在可複用性上,getName
沒有直接耦合反作用方法getDefaultNameFromServer
,而是讓某個上層調用者來完成反作用。這一點與使用Algebraic Effects時相同。
redux-sagas就使用Generator Functions,將反作用的執行從saga中抽離出來,saga只須要發起反作用。這使得saga成爲純函數,易於測試。
可是,依然存在感染調用者的問題。
在React Fiber架構:可控的「調用棧」這篇文章中,咱們討論了React Fiber架構是一種可控的執行模型,每一個fiber執行完本身的工做之後就會將控制權交還給調度器,由調度器來決定何時執行下一個fiber。
雖然JavaScript不支持Algebraic Effects(事實上,支持Algebraic Effects的語言屈指可數),可是在React Fiber架構的幫助下,React能夠模擬一些很實用的Algebraic Effects。
<Suspend>
就是一個例子。當React在渲染的過程當中遇到還沒有就緒的數據時,可以暫停渲染。等到數據就緒的時候再繼續:
// cache相關的API來自React團隊正在開發的react-cache: // https://github.com/facebook/react/tree/master/packages/react-cache const cache = createCache(); const UserResource = createResource(fetchUser); // fetchUser is async const User = (props) => { const user = UserResource.read( // synchronously! cache, props.id ); return <h3>{user.name}</h3>; } function App() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <User id={123} /> </Suspense> </div> ); }
react-cache是React團隊正在開發的工具,將<Suspense>
用於數據獲取的場景,讓須要等待數據的組件「暫停」渲染。
目前已經上線的,經過React.lazy
來暫停渲染的能力,其實也是相似的原理。
理論上,UserResource.read
能夠看作發起了一個Algebraic Effect。User發出這個effect之後,控制權暫時交給了React(由於React是User的調用者)。React scheduler提供了對應的effect handler,檢查cache中是否有對應id的user:
- 若是在cache中,則當即將控制權交還給User,並提供對應的user數據。 - 若是不在cache中,則調用fetchUser從網絡請求對應id的user,在此過程當中,渲染暫停,`<Suspense>`渲染fallback視圖。獲得結果之後,將控制權交還給User,並提供對應的user數據。
實際上,它是經過throw來模擬Algebraic Effect的。若是數據還沒有準備好,UserResource.read
會拋出一個特殊的promise。得益於React Fiber架構,調用棧並非React scheduler -> App -> User
,而是:先React scheduler -> App
而後React scheduler -> User
。所以User組件拋出的錯誤會被React scheduler接住,React scheduler會將渲染「暫停」在User組件。這意味着,App組件的工做不會丟失。等到promise解析到數據之後,從User fiber開始從新渲染就行了(至關於控制權直接交還給User)。
若是直接使用調用棧來管理組件樹的渲染(遞歸渲染),那麼App組件的渲染工做會由於User拋出值而丟失,下次渲染須要從頭開始。
React團隊將hooks都看作Algebraic Effect。useState的返回值取決於它的所處「虛擬調用棧」,即它在組件樹中的位置,即fiber。好比一個組件樹中有2個Counter組件,那麼這兩個Counter組件實例所處的上下文是不同的,所以它們的useState返回值是獨立的。
Algebraic effects, Fibers, Coroutines...
Algebraic Effects for the Rest of Us