本文是 React 核心開發者 Sebastian Markbåge 撰寫,闡述了他設計 React 的初衷及與 React 相關的最基礎的理論概念。閱讀此文,你能站在更高的高度去思考關於 React 的過去、現在和未來。通過此文,我想正式闡述自己關於 React 的心智模型。目的是解釋為什麼我們會這樣設計 React,同時你也可以根據這些論點反推出 React。
不可否認,此文中的部分論據或前提尚存爭議,而且部分示例的設計可能存在 bug 或疏忽。而且這只是它的最初階段,如果你有更好的完善它的想法可以隨時提交 pull request。同時本文不會介紹代碼庫的詳細細節,而是由淺入深的幫助你看清 React 由簡單到複雜的設計過程。
React.js 的真實實現中充滿了具體問題的解決方案,漸進式的解法,算法優化,歷史遺留代碼,debug 工具以及其他一些可以讓它真的具有高可用性的內容。這些代碼可能並不穩定,因為未來瀏覽器的變化和功能權重的變化隨時面臨改變。所以具體的代碼很難徹底解釋清楚。
我偏向於選擇一種更加簡潔的心智模型來作介紹。
變換(Transformation)設計 React 的核心前提是認為 UI 只是把數據通過映射關係變換成另一種形式的數據。同樣的輸入必會有同樣的輸出。這恰好就是純函數。
functionNameBox(name){return{fontWeight:'bold',labelContent:name};}'SebastianMarkbåge'->{fontWeight:'bold',labelContent:'SebastianMarkbåge'};抽象(Abstraction)你不可能僅用一個函數就能實現複雜的 UI。重要的是,你需要把 UI 抽象成多個隱藏內部細節,又可復用的函數。通過在一個函數中調用另一個函數來實現複雜的 UI,這就是抽象。
functionFancyUserBox(user){return{borderStyle:'1pxsolidblue',childContent:['Name:',NameBox(user.firstName+''+user.lastName)]};}{firstName:'Sebastian',lastName:'Markbåge'}->{borderStyle:'1pxsolidblue',childContent:['Name:',{fontWeight:'bold',labelContent:'SebastianMarkbåge'}]};組合(Composition)為了真正達到重用的特性,只重用葉子然後每次都為他們創建一個新的容器是不夠的。還必須可以將包含其他抽象的容器進行二次組合。我理解的「組合」就是將兩個或者多個不同的抽象合併為一個新的「抽象」。
functionFancyBox(children){return{borderStyle:'1pxsolidblue',children:children};}functionUserBox(user){returnFancyBox(['Name:',NameBox(user.firstName+''+user.lastName)]);}狀態(State)UI 不單單是對服務端響應數據或業務邏輯狀態的複製。實際上還有很多狀態是針對某個具體的渲染目標。例如,在一個 text field 中打字,此時不一定要將打字過程中的狀態複製到其他頁面或者你的手機設備。再如,滾動位置這個狀態是一個典型例子,你幾乎不會複製它到多個渲染目標。
我們傾向於在更新 state 時使用不可變的數據模型。同時一個可以改變 state 的函數就像是一個原子操作,我們需要由上至下把它們串聯起來。
functionFancyNameBox(user,likes,onClick){returnFancyBox(['Name:',NameBox(user.firstName+''+user.lastName),'Likes:',LikeBox(likes),LikeButton(onClick)]);}//實現細節varlikes=0;functionaddOneMoreLike(){likes++;rerender();}//初始化FancyNameBox({firstName:'Sebastian',lastName:'Markbåge'},likes,addOneMoreLike);注意:本例更新狀態時會帶來副作用(addOneMoreLike 函數中)。我實際的想法是當一個「update」傳入時我們返回下一個版本的狀態,但那樣會比較複雜。此示例待更新
緩存(Memoization)對於純函數,使用相同的參數一次次調用未免太浪費資源。我們可以創建一個函數的 memorized 版本,用來追蹤最後一個參數和結果。這樣如果我們繼續使用同樣的值,就不需要反覆執行它了。
functionmemoize(fn){varcachedArg;varcachedResult;returnfunction(arg){if(cachedArg===arg){returncachedResult;}cachedArg=arg;cachedResult=fn(arg);returncachedResult;};}varMemoizedNameBox=memoize(NameBox);functionNameAndAgeBox(user,currentTime){returnFancyBox(['Name:',MemoizedNameBox(user.firstName+''+user.lastName),'Ageinmilliseconds:',currentTime-user.dateOfBirth]);}列表(Lists)大部分 UI 都是展示列表數據中不同 item 的列表結構。這樣就產生了一個天然的父子層級。為了管理列表中的每一個 item 的 state ,我們可以創造一個 Map 容納具體 item 的 state。
functionUserList(users,likesPerUser,updateUserLikes){returnusers.map(user=>FancyNameBox(user,likesPerUser.get(user.id),()=>updateUserLikes(user.id,likesPerUser.get(user.id)+1)));}varlikesPerUser=newMap();functionupdateUserLikes(id,likeCount){likesPerUser.set(id,likeCount);rerender();}UserList(data.users,likesPerUser,updateUserLikes);注意:現在我們向 FancyNameBox 傳了多個不同的參數。這打破了我們的 memoization 因為我們每次只能存儲一個值。更多相關內容在下面。
連續性(Continuations)不幸的是,自從 UI 中有太多的列表,我們就需要大量的重複性樣板代碼來顯示的管理這些列表。
我們可以通過推遲一些函數的執行,進而把一些模板移出業務邏輯。比如,使用「柯里化」(JavaScript 中的 bind)。然後我們可以從核心的函數外面傳遞 state,這樣就沒有樣板代碼了。
這樣雖然並沒有減少樣板代碼,但至少把它從關鍵業務邏輯中剝離出來了。
functionFancyUserList(users){returnFancyBox(UserList.bind(null,users));}constbox=FancyUserList(data.users);constresolvedChildren=box.children(likesPerUser,updateUserLikes);constresolvedBox={...box,children:resolvedChildren};狀態映射(State Map)前面我們提到過「組合」,可以使用組合避免重複使用相同的樣板模式。我們可以把執行和傳遞 state 邏輯挪動到被復用很多的低層級的函數中去。
functionFancyBoxWithState(children,stateMap,updateState){returnFancyBox(children.map(child=>child.continuation(stateMap.get(child.key),updateState)));}functionUserList(users){returnusers.map(user=>{continuation:FancyNameBox.bind(null,user),key:user.id});}functionFancyUserList(users){returnFancyBoxWithState.bind(null,UserList(users));}constcontinuation=FancyUserList(data.users);continuation(likesPerUser,updateUserLikes);緩存映射(Memoization Map)一旦我們想在一個列表緩存中 memoize 多個 item 就會變得很困難。因為你需要制定複雜的緩存算法來平衡調用頻率和內存占有率。
還好 UI 在同一個位置會相對的穩定。相同的位置一般每次都會接受相同的參數。這樣以來,使用一個集合來做 memoization 是一個非常好用的策略。
我們可以用對待 state 同樣的方式,在組合的函數中傳遞一個 memoization 緩存。
functionmemoize(fn){returnfunction(arg,memoizationCache){if(memoizationCache.arg===arg){returnmemoizationCache.result;}constresult=fn(arg);memoizationCache.arg=arg;memoizationCache.result=result;returnresult;};}functionFancyBoxWithState(children,stateMap,updateState,memoizationCache){returnFancyBox(children.map(child=>child.continuation(stateMap.get(child.key),updateState,memoizationCache.get(child.key))));}constMemoizedFancyNameBox=memoize(FancyNameBox);代數效應(Algebraic Effects)多層抽象需要共享瑣碎數據時,一層層傳遞數據非常麻煩。如果能有一種方式可以在多層抽象中快捷地傳遞數據,同時又不需要牽涉到中間層級,那該有多好。React 中我們把它叫做「context」。
有時候數據依賴並不是嚴格按照抽象樹自上而下進行。舉個例子,在布局算法中,你需要在實現他們的位置之前了解子節點的大小。
現在,這個例子有一點超綱。我會使用 代數效應 這個由我發起的 ECMAScript 新特性提議。如果你對函數式編程很熟悉,它們 在避免由 monad 強制引入的儀式一樣的編碼。
functionThemeBorderColorRequest(){}functionFancyBox(children){constcolor=raisenewThemeBorderColorRequest();return{borderWidth:'1px',borderColor:color,children:children};}functionBlueTheme(children){returntry{children();}catcheffectThemeBorderColorRequest->[,continuation]{continuation('blue');}}functionApp(data){returnBlueTheme(FancyUserList.bind(null,data.users));}最後, 送人玫瑰,手留余香,覺得有收穫的朋友可以點讚,關注一波 ,我們組建了高級前端交流群,如果您熱愛技術,想一起討論技術,交流進步,不管是面試題,工作中的問題,難點熱點都可以在交流群交流,為了拿到大Offer,邀請您進群,入群就送前端精選100本電子書以及下方前端精選資料添加下方小助手二維碼就可以進群。讓我們一起學習進步.

推薦閱讀
(點擊標題可跳轉閱讀)
[極客前沿]-你不知道的React18新特性
[極客前沿]-寫給前端的 K8s 上手指南
[極客前沿]-寫給前端的Docker上手指南
[面試必問]-你不知道的 React Hooks 那些糟心事
[面試必問]-一文徹底搞懂React調度機制原理
[面試必問]-一文徹底搞懂React 合成事件原理
[面試必問]-全網最簡單的React Hooks源碼解析
[面試必問]-一文掌握Webpack編譯流程
[面試必問]-一文深度剖析Axios源碼
[面試必問]-一文掌握JavaScript函數式編程重點
[面試必問]-阿里,網易,滴滴,頭條等20家面試真題
[面試必問]-全網最全 React16.0-16.8 特性總結
[架構分享]-微前端qiankun+docker+nginx自動化部署[架構分享]-石墨文檔 Websocket 百萬長連接技術實踐
[自我提升]-Javascript條件邏輯設計重構
[自我提升]-送給React開發者十九條性能優化建議
[自我提升]-頁面可視化工具的前世今生
[大前端之路]-連前端都看得懂的《Nginx 入門指南》
[軟實力提升]-金三銀四,如何寫一份面試官心中的簡歷
覺得本文對你有幫助?請分享給更多人
關注「React中文社區」加星標,每天進步
點個讚👍🏻,順便點個在看支持下我吧