Discover more
非要去深究“狀态”這(zhè)個(gè)詞,從後端服務的(de)角度去解釋更加能讓我們理(lǐ)解。一言概之,我自以爲是地總結,“狀态”的(de)意思就是:現在的(de)樣子。一個(gè)服務現在的(de)樣子主要是由運行時(shí)所産生的(de)内存和(hé)運算(suàn)決定的(de),而且,它有終極的(de)時(shí)間觀念,理(lǐ)論上任何時(shí)刻的(de)狀态都不同,因爲它内部必然會有即使細微的(de)變化(huà)。
前端狀态管理(lǐ)這(zhè)個(gè)概念出現之前,就已經有狀态管理(lǐ)的(de)實踐了(le)。我在2010年開始使用(yòng)jquery,這(zhè)是一個(gè)非常了(le)不起的(de)庫,它将複雜(zá)的(de)DOM操作,通(tōng)過簡化(huà)和(hé)封裝,成爲實質上的(de)行業标準。HTML5标準發布後,采用(yòng)了(le)querySelector這(zhè)個(gè)接口,可(kě)以說它完全是基于開發者對(duì)jquery的(de)認可(kě)度的(de)考慮。那這(zhè)和(hé)狀态管理(lǐ)有什(shén)麽關系呢(ne)?我想說的(de)是,在jquery的(de)年代,雖然“前端狀态管理(lǐ)”這(zhè)個(gè)概念還(hái)沒有産生,但是狀态管理(lǐ)确真實存在。我們用(yòng)一段基于jquery的(de)僞代碼來(lái)看看。
我們在實踐中,使用(yòng)dataset的(de)特性,直接在被操作的(de)DOM節點上對(duì)一個(gè)模态框的(de)隐現狀态進行管理(lǐ)。而在其他(tā)地方,我們可(kě)能會讀出這(zhè)個(gè)狀态值,用(yòng)來(lái)判斷是否要執行某些操作。
你看到的(de)這(zhè)種操作,在我們将“前端狀态管理(lǐ)”的(de)概念抽象化(huà)之前,幾乎随處可(kě)見。當然,這(zhè)是一種原始的(de)狀态管理(lǐ),它隻能處理(lǐ)單一的(de)、分(fēn)散的(de)、臨時(shí)的(de)狀态,無法對(duì)複雜(zá)的(de)、耦合(依賴)的(de)、持久的(de)狀态進行深度處理(lǐ),或者說處理(lǐ)起來(lái)很麻煩。其中的(de)典型案例就是我多(duō)次提及的(de)表單業務,在一個(gè)表單業務中使用(yòng)jquery進行開發,所有的(de)表單數據,必須分(fēn)散的(de)、随意的(de),提交的(de)時(shí)候,必須從很多(duō)個(gè)地方把這(zhè)些數據找出來(lái),甚至還(hái)比較難找。
我們不禁要問:狀态管理(lǐ)的(de)本質是什(shén)麽?起碼,在這(zhè)一階段,我們已經知道一個(gè)事實:
我大(dà)學本科和(hé)碩士所屬專業的(de)大(dà)類學科是管理(lǐ)學,不同專業對(duì)管理(lǐ)的(de)解釋不同,而對(duì)我而言,記錄則是一種有效管理(lǐ)。就像jquery時(shí)代,我們找到一種雖然原始,但行得(de)通(tōng)的(de)方式管理(lǐ)狀态。
但是,我們往往發現,我們正在實踐的(de)東西,有些是非常優秀的(de),但是,必須等到很多(duō)年(3-5年)之後,被其他(tā)團隊的(de)某個(gè)項目帶熱(rè)之後,才證明(míng)我們正在實踐的(de)東西是對(duì)的(de),是走在趨勢上的(de),然而我們沒有堅持到最後,因爲最後我們采用(yòng)了(le)市面上最熱(rè)門的(de)框架或庫。超前的(de)東西往往一開始有很多(duō)人(rén)同時(shí)實踐,但是随著(zhe)時(shí)間流逝,這(zhè)些實踐的(de)人(rén)中,很多(duō)退出了(le),在沉寂的(de)發展中,會有一些堅持下(xià)來(lái)的(de)團隊,最終成爲主流,比如angularjs團隊。但不要著(zhe)急,在angularjs還(hái)默默無聞時(shí),甚至已經初露端倪時(shí),仍然還(hái)是jquery的(de)天下(xià),直接對(duì)DOM節點操作的(de)前端編程範式持續了(le)至少6年,直到react提出virtual dom這(zhè)樣先進的(de)理(lǐ)念之後,jquery才逐漸淡出曆史舞台。angularjs團隊至始至終都沒有超出jquery的(de)統治高(gāo)度。
沒有超出jquery統治高(gāo)度的(de),還(hái)有backbone。我也(yě)多(duō)次提到過這(zhè)個(gè)庫,它至今還(hái)活著(zhe),你可(kě)以通(tōng)過這(zhè)裏查看它的(de)文檔。backbone的(de)成就核心在于它的(de)Model(backbone是一個(gè)真正意義上的(de)前端框架,但是它不處理(lǐ)view部分(fēn),它提供了(le)view的(de)編程範式,但你需要使用(yòng)jquery來(lái)完成所有view的(de)更新)。我們來(lái)看一個(gè)例子吧。
backbone中可(kě)沒有我們現在熟知各種花裏胡哨的(de)操作方式,對(duì)于模型實例中的(de)狀态,隻能通(tōng)過get進行獲取,通(tōng)過set進行修改,set也(yě)支持傳入對(duì)象批量修改。而它最大(dà)的(de)亮點,在于模型上的(de)on方法,這(zhè)個(gè)方法實現了(le)狀态管理(lǐ)的(de)一個(gè)質變。
on/off這(zhè)對(duì)監聽(tīng)方法來(lái)源于jquery,但超越了(le)jquery。jquery中隻針對(duì)DOM的(de)事件系統,而backbone可(kě)以脫離DOM,對(duì)數據變化(huà)進行監聽(tīng)。當數據的(de)變化(huà)被監聽(tīng)之後,就可(kě)以在監聽(tīng)函數中對(duì)view進行修改,而對(duì)于事件的(de)響應,隻需要調用(yòng)set方法修改數據。這(zhè)樣就做(zuò)到了(le)數據和(hé)界面代碼的(de)分(fēn)離解耦,是一大(dà)進步。但backbone也(yě)止步于此。
使用(yòng)set/get這(zhè)種接口方法顯然是一種妥協,而且不符合習(xí)慣的(de)還(hái)有必須加一個(gè)change:
前綴,以和(hé)普通(tōng)事件區(qū)分(fēn)。如果回到2018年去重新設計backbone,get可(kě)以做(zuò)依賴收集,監聽(tīng)可(kě)以被優化(huà),即使仍然采用(yòng)set/get接口,而不采用(yòng)defineProperty這(zhè)樣的(de)黑(hēi)魔法,也(yě)可(kě)以再進一步,然而,時(shí)代終将止步于該止步之處。
對(duì)于開發者而言,雖然可(kě)以通(tōng)過對(duì)狀态變化(huà)的(de)監聽(tīng),來(lái)實現狀态和(hé)界面代碼分(fēn)離,卻最終隻能通(tōng)過set/get這(zhè)樣的(de)方法接口進行數據操作,有違編程的(de)優雅。在這(zhè)條路上,angularjs走的(de)更遠(yuǎn)。同樣是基于jquery打下(xià)的(de)江山,backbone也(yě)是框架,angularjs也(yě)是框架,然而當我們如今回頭去看,雖然angularjs也(yě)沒有爆紅,卻踏踏實實的(de),成爲一代框架典範,成爲前端工程化(huà)之鼻祖,同時(shí),也(yě)是前端狀态管理(lǐ)的(de)一大(dà)遺憾。
如果你使用(yòng)過angularjs,你會喜歡上它,當然前提是在那個(gè)年代。它通(tōng)過一套被稱爲髒檢查機制的(de)響應系統,讓開發者可(kě)以通(tōng)過直接修改狀态屬性值,就可(kě)以改變界面。我們來(lái)看下(xià)代碼。
$scope
是angularjs的(de)内置服務,爲了(le)避免一些作用(yòng)域問題,推薦使用(yòng)controllerAs來(lái)管理(lǐ)。但我們且不去討(tǎo)論這(zhè)些問題,我們看angularjs的(de)代碼,已然見到來(lái)當代所熟悉的(de)編程風格了(le)。我們可(kě)以把$scope當作是一個(gè)狀态的(de)容器,狀态變化(huà),會通(tōng)過angularjs的(de)響應系統,反饋到界面上去,view中所使用(yòng)的(de)數據素材,和(hé)$scope上的(de)屬性名完全對(duì)應。
angularjs的(de)實現原理(lǐ)是,在ng-click的(de)觸發之後,除了(le)調用(yòng)執行$scope.updateName
之外,實際上還(hái)要執行内部的(de)digest循環過程,也(yě)就是髒檢查的(de)内部實現。隻有經曆完這(zhè)個(gè)digest之後,才能更新視圖界面。那麽問題就由此而生,在有些情況下(xià),這(zhè)個(gè)digest确實沒有被觸發,舉個(gè)例子,在原生的(de)ajax請求結束時(shí)修改$scope.name就無法觸發,于是angularjs提供了(le)$apply方法解決這(zhè)個(gè)問題。
不過,一個(gè)angularjs的(de)應用(yòng),它的(de)controller逐漸膨脹,慢(màn)慢(màn)的(de)随著(zhe)業務邏輯的(de)增長(cháng),一個(gè)function被撐到上千行代碼也(yě)有可(kě)能。而在這(zhè)上千行代碼中,要找到一個(gè)狀态以及這(zhè)個(gè)狀态的(de)變化(huà)過程,猶如在大(dà)海中尋找繡花針般,無計可(kě)施。
問題出在哪裏呢(ne)?直到今天,angular已經叠代,舊(jiù)版本幾乎已經停止維護,這(zhè)個(gè)問題仍然沒有被回答(dá)。我個(gè)人(rén)猜測,最本質的(de)根源在于,angularjs沒有按照(zhào)嚴格的(de)MVC進行設計,它缺失了(le)M層,所有的(de)編程邏輯被寫在controller函數中,任由開發者自由發揮,修改狀态來(lái)驅動視圖更新。由于沒有對(duì)Model層進行強約束,導緻代碼裏根本分(fēn)不清哪些是用(yòng)于控制視圖變化(huà)的(de)交互邏輯,哪些是用(yòng)于控制數據變化(huà)的(de)業務邏輯。而這(zhè)個(gè)問題,在後來(lái)的(de)react、vue中其實也(yě)存在。
一個(gè)更嚴重的(de)問題,angularjs的(de)directive(相當于組件)支持雙向數據綁定,導緻外層狀态在内層directive中被修改,在調試問題時(shí),由于無法掌握狀态變化(huà)的(de)順序,使開發者可(kě)以崩潰到砸電腦(nǎo)狀态。并且随著(zhe)W3C标準的(de)推進,“組件”這(zhè)個(gè)概念開始慢(màn)慢(màn)成爲開發的(de)核心概念。而于此同時(shí),同樣基于組件思想而生的(de)react就像橫空出世一樣,像一記銀彈擊碎了(le)jquery多(duō)年美(měi)夢,從此前端編程範式實現了(le)轉變。
react的(de)編程範式核心來(lái)自virtual dom,虛拟DOM的(de)思想直接使得(de)前端編程範式發生轉變。通(tōng)過将DOM樹映射到一個(gè)内存對(duì)象,通(tōng)過對(duì)虛拟DOM的(de)對(duì)比,隻更新實際DOM中的(de)少量節點,從而避免頻(pín)繁操作DOM帶來(lái)的(de)性能損耗。但virtual dom的(de)殺手锏在于,将實際的(de)DOM抽象爲虛拟DOM之後,虛拟DOM是否再回到實際DOM就是可(kě)選擇的(de)事情,因爲基于這(zhè)套理(lǐ)論,虛拟DOM還(hái)可(kě)以爲其他(tā)渲染方式提供驅動力,比如react-native,或者渲染canvas。而驅動virtual dom發生變化(huà)的(de),竟然是狀态。這(zhè)是一個(gè)颠覆性的(de)質變。
react一直提倡一種純UI組件,這(zhè)種組件完全受控于其接收的(de)props的(de)值,像純函數一樣,props在解構全等的(de)情況下(xià),其渲染出來(lái)的(de)效果是一模一樣。而支配props變化(huà)的(de),來(lái)自于頂層組件一層一層的(de)傳遞。最終,一個(gè)界面将要展示成什(shén)麽樣子,取決于一個(gè)狀态,由這(zhè)個(gè)狀态參與組成虛拟DOM,并反映到真實DOM中去。狀态發生變化(huà)時(shí),按照(zhào)一層一層傳遞的(de)單向數據流方式,讓所有的(de)自組件按照(zhào)最頂層組件的(de)狀态進行渲染。
在這(zhè)種思想熏陶下(xià),大(dà)約2016年,整個(gè)前端的(de)編程範式都以此爲基礎,所湧現出來(lái)的(de)vue,也(yě)直接借鑒這(zhè)套思想進行編程。一個(gè)狀态,對(duì)應一個(gè)界面,這(zhè)簡直是天才的(de)想法。基于這(zhè)種思想,把所有的(de)狀态按照(zhào)時(shí)間線錄制下(xià)來(lái),就可(kě)以重放界面變化(huà)的(de)所有,這(zhè)種錄制,将原本龐大(dà)的(de)界面數據,抽象爲狀态這(zhè)一小量數據,這(zhè)種思維在遊戲錄制領域也(yě)被使用(yòng)過。
誠然,雖然“映射”思想颠覆了(le)傳統前端編程更新DOM的(de)範式,但react的(de)編程範式所遵循的(de)單向數據流卻帶來(lái)了(le)巨大(dà)麻煩。這(zhè)種一層一層傳遞的(de)方式,雖然保證了(le)“映射”行爲的(de)純正性,但無法适應實際生産過程中所帶來(lái)的(de)coding和(hé)debug麻煩的(de)問題。如果由于一個(gè)點擊操作需要通(tōng)過10層組件的(de)傳遞,才能對(duì)點擊做(zuò)出響應,那麽很可(kě)能就會出問題。單向數據流聽(tīng)起來(lái)讓數據流很清晰,但是對(duì)應到代碼中,發生一個(gè)事件發生後,這(zhè)個(gè)事件信息,被如何傳遞,成爲極其複雜(zá)的(de)代碼邏輯,那麽看似明(míng)晰的(de)清流,就變成了(le)洶湧的(de)濁流。
中心化(huà)的(de)狀态管理(lǐ)孕育而生,redux、mobx這(zhè)些都是佼佼者。這(zhè)些,就是我們當代真正意義上所稱的(de)狀态管理(lǐ)器。它們當然是爲了(le)管理(lǐ)狀态。redux是一朵奇葩,它火,火到爆炸,但是你去閱讀它的(de)源碼,卻又很簡單。大(dà)道至簡,在這(zhè)些年的(de)編程體驗中,我逐漸體會到,簡單,是一切的(de)根本。redux的(de)使用(yòng)非常簡單,接口就那麽幾個(gè)。然而,它卻衍生出了(le)自己的(de)生态,它把自己的(de)概念體系發展成爲蓬勃的(de)森林(lín)。我的(de)天!原本簡單的(de)東西,非要通(tōng)過概念複雜(zá)化(huà)。設計,核心是簡單,工具,用(yòng)完即走就行。同樣的(de)錯誤,mobx重蹈覆轍。它的(de)設計理(lǐ)念也(yě)非常的(de)具有颠覆性,引入觀察者模式,将狀态的(de)變化(huà)通(tōng)過觀察者模式進行抽象,并且通(tōng)過完美(měi)的(de)封裝,使得(de)更新狀态的(de)方式和(hé)普通(tōng)修改一個(gè)對(duì)象沒有兩樣。然而,過渡設計讓開發者望而卻步。不過話(huà)說回來(lái),在狀态管理(lǐ)這(zhè)個(gè)點上,它們的(de)核心思想保持了(le)一緻,都是:
集中管理(lǐ)實際上是共享的(de)前提,但也(yě)超越了(le)共享這(zhè)一單項功能,集中管理(lǐ)狀态,可(kě)以用(yòng)一個(gè)狀态,概括一組組件,甚至一個(gè)應用(yòng)的(de)整體狀态。在映射思維驅動之下(xià),連路由也(yě)要狀态化(huà),也(yě)就是說,整個(gè)應用(yòng)的(de)一切行爲都由中心化(huà)的(de)狀态管理(lǐ)器決定。框架層、UI層隻是應用(yòng)的(de)殼,而狀态以及狀态的(de)驅動才是應用(yòng)的(de)魂。狀态管理(lǐ)器,通(tōng)過連接器将狀态的(de)變化(huà)反饋到具體的(de)某個(gè)組件中,而這(zhè)個(gè)組件,可(kě)以通(tōng)過原本的(de)單向數據流将狀态傳入更深的(de)子組件中。UI層像提線木(mù)偶一樣,被狀态管理(lǐ)器完完全全的(de)控制著(zhe)。
然而,物(wù)極必反。react官方提出來(lái)的(de)flux,本身就打破了(le)單向數據流的(de)體系,它使數據可(kě)以通(tōng)過多(duō)條管道進行傳遞,而它的(de)核心,就是要建立一條便捷通(tōng)道,跨過多(duō)層組件,使分(fēn)散在兩個(gè)樹枝上的(de)節點組件直接通(tōng)信。雖然flux通(tōng)過非常複雜(zá)的(de)概念,試圖解釋自己是遵循單向數據流,但是不可(kě)否認,它隻是在原來(lái)的(de)單向流之外,又開了(le)一條流,現在有兩條通(tōng)道,但你隻能選擇其中之一,這(zhè)樣你才能單向數據流,從而保證界面和(hé)狀态之間的(de)映射關系。而且,基于context的(de)技術是react官方給出的(de),flux是妥協的(de)産物(wù),它是爲了(le)解決react帶來(lái)的(de)問題而産生的(de),而很不幸,爲了(le)解決這(zhè)個(gè)問題,flux帶來(lái)了(le)更多(duō)的(de)問題,妥協,本質上是債務。
更緻命的(de)是,中心化(huà)的(de)狀态管理(lǐ)面臨嚴重的(de)問題。狀态,本身是必要的(de)。狀态解決了(le)臨時(shí)保持和(hé)最終決定兩個(gè)環節的(de)沖突,以HTML中的(de)select标簽(本質就是一個(gè)組件)爲例子,哪一個(gè)option被選中了(le)呢(ne)?這(zhè)得(de)看當前select的(de)狀态,但這(zhè)并不代表當前被選中的(de)值将作爲你最終提交的(de)值,你提交的(de)值,往往是看不見的(de)option的(de)value屬性值。同樣的(de)情況比比皆是,例如你需要打開一個(gè)模态框,在裏面輸入東西,或者選擇選項,但是模态框會給你一個(gè)cancel的(de)能力,當你點擊cancel的(de)時(shí)候,之前的(de)操作會被重置。而這(zhè)些臨時(shí)保持的(de)狀态,根本沒有必要進入中心化(huà)管理(lǐ)的(de)狀态管理(lǐ)器中,一旦進入中心化(huà)狀态管理(lǐ)器,那麽就遇到内存持久不能釋放,還(hái)要解決數據重置等問題。但尴尬的(de)場(chǎng)景又在于,如果不進中心化(huà)狀态管理(lǐ)器,那映射思想又無法完全實現,一個(gè)狀态一定對(duì)應一個(gè)界面的(de)理(lǐ)想模式無法複現。這(zhè)個(gè)問題,隻要是共享數據,都會碰到,不一定是共享狀态,共享一個(gè)函數,共享一個(gè)模塊,共享一個(gè)類的(de)實例,都會遇到,所以,這(zhè)是共享問題帶來(lái)的(de)普遍性問題。
還(hái)有一個(gè)大(dà)問題,集中管理(lǐ)狀态的(de)代價是狀态的(de)domain問題。一份完整的(de)狀态确實好處多(duō)多(duō),但是帶來(lái)的(de)問題是,原本和(hé)組件無關的(de)狀态變化(huà),也(yě)會因爲狀态整體的(de)變化(huà)被通(tōng)知重新執行virtual dom的(de)diff操作。不過這(zhè)是有辦法解決的(de),無論是通(tōng)過狀态管理(lǐ)器自身的(de)優化(huà)也(yě)好,還(hái)是通(tōng)過組件的(de)優化(huà)也(yě)好,都可(kě)以做(zuò)到根據需要的(de)狀态變化(huà)來(lái)決定自身是否要做(zuò)這(zhè)個(gè)diff。可(kě)是大(dà)狀态始終是個(gè)隐患,不僅面臨内存問題,也(yě)面臨數據重複、難相互依賴等問題。
直到react hooks出現,這(zhè)個(gè)局面又被打破。hooks雖然表面上是讓functional組件也(yě)可(kě)以在不同的(de)生命周期環節上執行某些任務,但是本質上是重新定義組件在什(shén)麽情況下(xià)重新diff。hooks函數的(de)重點不在第一個(gè)參數,而在第二個(gè)參數(依賴)。當依賴發生變化(huà)時(shí),需要執行對(duì)應的(de)變化(huà)。變化(huà)源自狀态的(de)變化(huà)。hooks依賴狀态,但是實際上定義的(de)是動作及其觸發的(de)條件。
從更小粒度去解釋,視圖層的(de)每一個(gè)變化(huà),是完成什(shén)麽動作。這(zhè)就是hooks帶來(lái)的(de)變化(huà)。這(zhè)個(gè)粒度小到什(shén)麽程度呢(ne)?atom。Recoil是facebook發布的(de)一個(gè)狀态管理(lǐ)工具,基于hooks的(de)技術,實現狀态共享。和(hé)集中管理(lǐ)狀态不同,recoil是小到原子級别的(de)狀态管理(lǐ)。通(tōng)過atom定義一個(gè)原子狀态,通(tōng)過selector定義一個(gè)依賴原子狀态的(de)複合狀态(相當于計算(suàn)屬性),再依托hooks,實現狀态共享。由atom定義的(de)原子狀态,它的(de)狀态到底是什(shén)麽并不重要,我們不需要取出來(lái)看看,我們要做(zuò)的(de),是在hooks加持下(xià),把握住狀态變化(huà)的(de)所帶來(lái)的(de)影(yǐng)響,也(yě)就是每一個(gè)動作。
聽(tīng)上去就像在胡扯,事情會變得(de)越來(lái)越複雜(zá)。設計,核心是簡單。這(zhè)裏的(de)簡單,不單單是使用(yòng)起來(lái)很簡單,而且理(lǐ)解起來(lái)也(yě)要很簡單。對(duì)于一個(gè)工具而言,我們要止于至簡。我們追求簡單,但防止過猶不及。在設計工具時(shí),有的(de)時(shí)候,我們可(kě)以設計出超強超靈活的(de)能力,但是,超出簡單部分(fēn)的(de)功能,往往使得(de)使用(yòng)成本和(hé)理(lǐ)解成本極速上升。狀态管理(lǐ)器的(de)本質,是管理(lǐ)狀态,是共享狀态。過度設計,過度技術化(huà),都是過猶不及。當recoil發布的(de)時(shí)候,大(dà)家開始討(tǎo)論actor model,這(zhè)是過猶不及。我們做(zuò)開發設計,并非遵循某個(gè)模型進行設計,而是遵循使用(yòng)者的(de)使用(yòng)習(xí)慣進行設計。當然,我不是說要遷就使用(yòng)者,而是說讓使用(yòng)者付出最低的(de)成本得(de)到最高(gāo)的(de)回報,簡單原則是達到這(zhè)一目标的(de)終極武器。爲了(le)簡單,我們有的(de)時(shí)候不得(de)不放棄一些原本可(kě)以讓工具更強大(dà)的(de)能力,但是不要著(zhe)急,這(zhè)給我們開發工具提出了(le)另外一個(gè)要求,就是擴展性。簡單是對(duì)使用(yòng)者說的(de),而擴展性則是對(duì)開發者說的(de)。通(tōng)過擴展性,簡單工具可(kě)以變成功能強大(dà)的(de)功能,擴展性設計是考驗能力的(de),并非每個(gè)開發者都能做(zuò)到,但是,這(zhè)是基本面。
應用(yòng)沒有“有意識地管理(lǐ)狀态”并非不行,以最早的(de)jquery.data方式管理(lǐ)一個(gè)狀态也(yě)未嘗不可(kě)。但如果需要有意識的(de)進行狀态管理(lǐ),那麽,我們不得(de)不需要一個(gè)狀态管理(lǐ)器。雖然我們可(kě)以挑選市面上已有的(de)狀态管理(lǐ)器,但我們可(kě)能并不需要。我認爲redux之所以經久不衰,核心的(de)核心就在于簡單。然而,并不是簡單我們就一定要用(yòng)它,如果它的(de)簡單正好符合我們的(de)實踐場(chǎng)景,那麽它是不二之選,但是它的(de)簡單在應對(duì)我們的(de)場(chǎng)景的(de)時(shí)候略有不足,由于木(mù)桶原理(lǐ),它将永遠(yuǎn)無法滿足我們的(de)要求。所以,手撸狀态管理(lǐ)器在所難免。
當然,我們手撸,并不代表我們要全撸,例如,我們可(kě)以基于已有的(de)redux,利用(yòng)它的(de)擴展性,撸出自己的(de)狀态管理(lǐ)器。然而核心要點還(hái)是在于,如何提供低成本的(de)使用(yòng)者開發體驗。我們基于redux撸出來(lái)的(de),我們可(kě)以另外取一個(gè)名字,将我們的(de)用(yòng)法和(hé)接口暴露給使用(yòng)者,我們全然不提redux,使用(yòng)者但凡用(yòng)的(de)舒服,沒有吐槽,就代表這(zhè)是成功的(de),此時(shí)我們再提redux,會讓我們的(de)成果充滿樂(yuè)趣。而如果一開始就大(dà)書(shū)特書(shū)我們是基于redux的(de),想加持光(guāng)環,那麽得(de)到的(de)結果必然是,如若不是真的(de)好用(yòng)到爆炸,斷然收不到好評。
既然要開始撸狀态管理(lǐ)器,我們就要設計這(zhè)個(gè)狀态管理(lǐ)器的(de)核心特質,缺少這(zhè)些特質會是我們自己都無法忍受的(de)。
其中,簡潔的(de)狀态更新方式有一個(gè)庫可(kě)以推薦:immer。這(zhè)個(gè)庫隻有一個(gè)接口,它的(de)使用(yòng)方式如下(xià):
大(dà)道至簡!大(dà)道至簡!大(dà)道至簡!immer的(de)作者簡直就是天才。使用(yòng)immer遵循了(le)immutable的(de)社區(qū)範式,同時(shí)又可(kě)以采用(yòng)mutable的(de)操作方式,雖然在實戰中确實還(hái)有些坑,然而,對(duì)比原本寫一大(dà)堆解構符号的(de)做(zuò)法,這(zhè)個(gè)接口簡直是上帝的(de)饋贈。
還(hái)有一個(gè)點需要單獨指出,重放功能實際上并不是強制的(de),因爲對(duì)于大(dà)多(duō)數應用(yòng)而言,要實現完全的(de)重放,其實是不大(dà)可(kě)能的(de),最嚴重的(de)原因有兩點:1)我們無法穿透所有組件,在大(dà)部分(fēn)組件中,我們不可(kě)避免的(de)會用(yòng)到内部狀态,而這(zhè)些内部狀态的(de)變化(huà)我們無法收集到,因此,也(yě)就無法重放由于組件的(de)内部狀态變化(huà)帶來(lái)的(de)界面變化(huà),一旦無法重放界面變化(huà),就會出現問題,因爲DOM的(de)變化(huà)具有副作用(yòng),下(xià)一個(gè)DOM樹的(de)基礎是上一個(gè)DOM樹,如果某些變化(huà)沒有發生,後續變化(huà)所依賴的(de)DOM節點可(kě)能根本就不存在,應用(yòng)會報錯;2)在狀态中,我們不可(kě)避免的(de)使用(yòng)某些實例對(duì)象,基于class的(de)實例對(duì)象有内存依賴,我們無法将它們保存到服務器端,再從服務器端拉出來(lái)進行回放。由于這(zhè)兩個(gè)原因,實際上要完全回放一個(gè)應用(yòng),是很難的(de)。
有的(de)。我們談狀态管理(lǐ),本質上無法擺脫應用(yòng)層面的(de)架構思維,而且這(zhè)裏僅僅是指圍繞數據流的(de)前端應用(yòng)(Management System、Data Application),我們幾乎很少在遊戲這(zhè)種視覺系統中大(dà)談前端狀态管理(lǐ)。既然是Management System,那麽我不禁要問,狀态管理(lǐ)的(de)目的(de),是技術的(de),還(hái)是應用(yòng)的(de)?很顯然,狀态管理(lǐ)的(de)目的(de),是爲了(le)讓應用(yòng)系統良好運作,保障系統工作穩定準确。而我們要面對(duì)的(de)實際情況是什(shén)麽呢(ne)?并非如何在組件之間共享狀态。我們的(de)實際情況是,我們需要在不同子模塊之間協調,甚至,我們需要在不同的(de)客戶端之間協調。這(zhè)裏面的(de)核心點實際上是“業務邏輯”。如果我們同時(shí)擁有手機端和(hé)PC端應用(yòng),雖然它們在視圖層面完全不同,然而在business層面,卻是一定一緻的(de),否則系統就不可(kě)靠了(le)。如何保障業務層面的(de)一緻性呢(ne)?
對(duì)于服務端應用(yòng)而言,特别是今天這(zhè)個(gè)時(shí)代,分(fēn)布式系統已經是基礎範式了(le),多(duō)個(gè)節點如何保障同一個(gè)用(yòng)戶的(de)業務操作是一緻的(de),實際上,它們還(hái)是要共享一個(gè)狀态,這(zhè)個(gè)狀态可(kě)能存在redis中,可(kě)能通(tōng)過Kafka進行同步。業務邏輯的(de)推進,依賴這(zhè)些狀态。看看後端是怎麽處理(lǐ)的(de)?在模型中專注于整理(lǐ)出每一個(gè)業務原子操作,并在接收到特定事件(或請求)時(shí),按照(zhào)業務順序,組裝完整的(de)業務邏輯出來(lái)。而現在前端是怎麽做(zuò)的(de)?以react爲例,幾乎全部的(de)react應用(yòng),都是将業務邏輯寫在組件中,因爲業務操作必須依賴用(yòng)戶的(de)操作,而用(yòng)戶的(de)操作必須在視圖層中予以反饋。所以在react組件中寫業務邏輯非常正常,這(zhè)回到了(le)angularjs的(de)老路子上,特别是強調functional+hooks的(de)寫法之後,我很容易想象到未來(lái)的(de)react組件和(hé)angularjs的(de)controller函數差不多(duō),寫出上千行代碼也(yě)不是不可(kě)能,到時(shí)候沒人(rén)敢動這(zhè)個(gè)組件。
而在這(zhè)一點上,我恰恰發現vue比react好很多(duō)。vue的(de)結構是
<template><script><style>
這(zhè)樣的(de)結構看上去符合早期的(de)編程習(xí)慣,模闆樣式加腳本嘛!然而,如果再去琢磨vue的(de)script部分(fēn),我發現實際上vue所export出去的(de)對(duì)象,本質上是一個(gè)類似模型的(de)定義(類似配置對(duì)象)。
{
data,
computed,
methods,
watch,
}
一個(gè)模型,總是圍繞自己目标所需要的(de)數據進行處理(lǐ)。如果寫過php應用(yòng),大(dà)部分(fēn)php框架都會有模型層,而在編寫模型時(shí),強調的(de),都是隻進行數據的(de)讀寫和(hé)計算(suàn),而不處理(lǐ)任何視圖的(de)東西,處理(lǐ)視圖的(de)東西,需要在控制器中讀取模型上的(de)數據,自己進行組裝。所以,模型,是對(duì)單一目标數據的(de)總體概括,具有業務邏輯的(de)抽象性,無法單獨完成整個(gè)業務流程,但是卻規定了(le)業務邏輯的(de)核心部分(fēn),等待開發者使用(yòng)這(zhè)些部分(fēn),組裝出完整的(de)業務流程。
不過,vue的(de)組件定義不僅僅包含這(zhè)些東西,同時(shí)還(hái)有生命周期函數,子組件引用(yòng),props,視圖事件回調函數等等東西,而這(zhè)些東西的(de)整體,又是爲視圖編程服務的(de),因此,最終它和(hé)模型也(yě)隻是插肩而過。狀态管理(lǐ)的(de)下(xià)一個(gè)方向,我恰恰認爲是去彌補這(zhè)個(gè)領域。前端架構至始至終,都沒有在模型層抽象出猶如後端一緻的(de)模型管理(lǐ),而發展至今,也(yě)應該是時(shí)候去往這(zhè)個(gè)坑填一填了(le)。所以,我在寫完react-tyshemo之後,有一種自豪感。我感覺自己好像觸摸到一點點這(zhè)個(gè)問題的(de)邊緣了(le)。這(zhè)個(gè)庫,可(kě)以做(zuò)到定義狀态就定義狀态,在定義函數中,把狀态的(de)所有演變都定義完整(也(yě)就是和(hé)上述vue組件script中的(de)部分(fēn)子集一緻),然後通(tōng)過connect注入給組件使用(yòng),對(duì)于組件而言,它就像隻能從模型中讀取屬性和(hé)方法一樣,在遇到對(duì)應的(de)交互事件之後,調用(yòng)模型上的(de)方法去驅動模型中的(de)狀态變化(huà),然後返回來(lái)又更新自己。這(zhè)樣就做(zuò)到了(le)數據模型的(de)定義和(hé)視圖層(react組件)的(de)分(fēn)離,在手機端、PC端之間共用(yòng)同一個(gè)模型成爲可(kě)能。
在react生态裏面,炫技的(de)不在少數。但要解決問題,而且要簡單地解決問題。将狀态管理(lǐ)升級到前端模型構建之後,模型邏輯和(hé)視圖邏輯就可(kě)以完全分(fēn)離。而由于我們大(dà)部分(fēn)情況下(xià),會在模型中寫業務邏輯,在視圖中寫交互邏輯,所以隻需要将它們組裝起來(lái)。但是,有趣的(de)地方在于,可(kě)以拆分(fēn)開來(lái)。而且,這(zhè)也(yě)帶來(lái)了(le)另一個(gè)好處,由于業務邏輯的(de)部分(fēn)被獨立出來(lái),那麽在不同端,就可(kě)以被複用(yòng),手機端、PC端、其他(tā)端,可(kě)以基于同一個(gè)模型,但視圖卻可(kě)以不同,視圖因爲隻負責交互邏輯,所以反而更抽象,變量命名都可(kě)以抛開業務單詞使用(yòng)更抽象的(de)詞彙來(lái)命名。一旦業務邏輯的(de)東西通(tōng)過模型層面,和(hé)react視圖編程拆分(fēn)開,那麽,真的(de)就可(kě)以做(zuò)到,react組件負責純UI,而模型負責純業務邏輯,中間在通(tōng)過某種控制器将兩者粘連在一起,會是另一翻編程的(de)景象。
本文主要是想表達,在狀态管理(lǐ)這(zhè)件事上,我們嘗試一切,試圖找到某種通(tōng)用(yòng)的(de)優雅的(de)解決方案,但是,在所有方案中,我們都不得(de)不進行一些妥協。如果我們能夠從曆史的(de)角度去觀察,往往能夠發現,世界上沒有完美(měi)的(de)事物(wù),有一種說法“曆史都是妥協出來(lái)的(de)”,我們可(kě)以換一個(gè)好聽(tīng)的(de)詞,叫“博弈”,但是無論如何,我們都在追求著(zhe),每個(gè)人(rén)的(de)追求不同,代碼風格的(de)優雅,代碼量少,代碼性能極緻,代碼明(míng)顯沒有bug……這(zhè)些追求,驅動著(zhe)我們不斷探索和(hé)思考。