React組件模型啓示錄

這個話題很難寫。前端

可是反過來講,愛因斯坦有句名言:若是你不能把一個問題向一個六歲孩子解釋清楚,那麼你不真的明白它。java

因此解釋清楚一個問題的關鍵,不是去擴大化,而是相反,最小化。node

Let's begin.react

組件

組件不是一個很清晰的編程概念。UML裏的組件圖基本上就是一個圖示,遠不能和具備數學完備性的State Diagram相比,也不能和靜態結構的Class Diagram和時序交互的Sequence Diagram相比。但你們一般仍是會畫一個出來,便於程序員理解系統的運行時結構,或者代碼結構。c++

你很容易在Google裏搜索到一些Component Diagram的圖例,因此這裏不貼圖了。你在組件圖上能夠看到有這樣一些概念是重要的:程序員

  1. port,它指的是包含一組相關函數的接口,一個interface或者一個protocol;數據庫

  2. user和provider,誰提供port和誰使用port;編程

這兩個概念都不須要很複雜的闡述,直覺的理解沒問題;redux

但問題是他們定義得特別粗,接口怎麼實現的?是function call?rpc?message passing?event?沒說,事實上是都行。後端

常見的Component Diagram畫的通常是run-time的instance,black box邏輯,強調的是instance之間的依賴關係。

另外一種關於組件的常見說法,是組件是爲了重用。這把問題聊到了另外一個空間去了。重用是靜態概念,它指的是代碼裏的一個模塊,類、結構等等,而不是指run-time實例。

可是這兩種說法不矛盾。由於核心的問題,不管運行時仍是靜態代碼,組件首先強調的是黑盒思惟,這點不是問題,封裝是開發者熟悉的邏輯;可是組件必須集成爲系統,不管在靜態代碼層面仍是運行時,組件之間都有依賴關係,在項目具備必定規模時這尤爲重要。

在靜態代碼層面,任何語言都有庫和源碼模塊話機制,include,import,require等語法關鍵字或函數創建了這種依賴關係;在運行時,組件(或對象實例)之間可能有動態產生的綁定關係;A對象要具備B的引用,才能使用B的方法;或者要下降耦合,採用觀察者模式,消息總線或消息路由,Pub/Sub,等等。相對而言,後者更爲重要一些,前者你總能經過分拆模塊避免循環,靜態依賴關係總歸是比較清楚的,它定了就定了,不會運行時發生變化。

因此這篇文章主要談運行時組件依賴關係的處理,特指在一個應用以內,不是微服務或者多個服務器組成的分佈式系統。

React

可能能夠不用一上來就談React,可是這樣作最簡單。

React的基本代碼單元稱爲React Component;它聲稱是View Component,但也能夠是純state的Component,在render方法裏render其餘view component便可;有經驗的React開發者知道這被社區稱爲Container Component。

若是從Component的角度看,React的Component有一個很是特別的設計:Component之間只有一種通信機制!就是經過Props傳遞對象或函數,原則上Component之間是不會經過引用互相調用方法甚至發送消息的。換句話說,全部Component都是匿名的。開發者不應在運行時查找某個Component實例訪問其數據或方法,調用其方法的只有React框架。

從這個意義上說,React象一個Inversion of Control(IOC)模式,全部有態組件都插在React框架之上,他們能夠在willReceiveProps或者render方法被調用時得到傳遞進來的數據或方法,他們也能夠經過調用setState方法觸發一次更新,但這幾乎就是所有了。

React的官方開發者提供了一套叫作flux的數據流方式,須要持久化(生命週期比視圖長)的狀態存入store,社區也有不少改良的工做,包括流行的redux,mobx等等;可是本質上說,react本身具備完備的態處理能力,只要把兩個有相關性的組件的關聯狀態放到他們的共同祖先便可;只是這樣作,若是沒有特殊的處理的化並不靈活,在設計變動時大量代碼要修改,還不如使用redux等框架來得方便;anyway,這一點不是咱們這篇文章要討論的主題,它指的是靜態代碼層面的模塊化問題,我找時間寫文章專述。咱們回到React組件是如何組合和互動這個話題上。

咱們從新強調一下React組件的匿名問題。對一個組件而言,它要能工做,固然須要和外部組件互動,可是React組件在這裏作了一個極致的設計:

一切依賴性都是注入的;注入的依賴性來自哪一個外部組件,組件內部一無所知。

依賴性注入(Dependency Injection)一詞,對熟悉可測試性(tesability)的開發者來講不陌生,但大多數狀況下這停留在測試領域,不多影響設計。絕大多數應用在頂層都有一些相似全局變量的模塊,也就是組件圖中表達的那些;使用這些模塊的其餘模塊都能用全局的name找到它們,找到就可使用了。可是在React裏,NO! 即便在頂層,每一個組件的外部依賴也都是注入的。

因此你看到React的組件模型實際上只包含三個元素:

  1. 父組件向子組件傳遞的prop是對象或值

  2. 父組件向子組件傳遞的prop是方法(bound)

  3. 父子組件們用一個tree表示,是單向的傳遞數據或方法的(即層層注入)

觀察者與資源建模

咱們先說第一個要素:父組件向子組件傳值。本質上,它是子組件對父組件或父組件可觀察的某個資源狀態的一個觀察。

從語法上來講,它比寫Observer Pattern要來得方便,由於子組件沒有Subscribe的負擔,是反過來作的,父組件把子組件的依賴性(須要觀察的對象)塞進來。

由於React是function programming風格,這樣寫更方便;不方便的地方是觀察變化的邏輯是在willReceiveProps裏,須要本身比較新版本和副本的區別,若是有差異,調用setState方法更新本身。

可是這種觀察能實現全部須要的觀察嗎?好比SomethingStarted,SomethingResumed,SomethingOpened?

確實可能遇到一些棘手的狀況難以簡單用值的變化來表述一種變化,可是咱們反過來想這個問題,數據庫是用CRUD實現的,Restful API設計採用資源建模也只有有限的verb,他們都工做的很好;工做的好的緣由是他們都是用資源而不是行爲建模的,若是確實須要爲行爲建模,咱們也可使用狀態機,對單一模塊而言,在各類粒度上狀態機都是很好的建模方式;在狀態機模型下,狀態就必定能夠用離散值來表示,好比運行狀態能夠是started, stopped, resumed, failed,等等。

這樣的建模方式是否比本身發明不少message類型更爲有效呢?我的見解是的,這是一種遠好於用行爲語義定義事件的方式。不管crud仍是restful都有極爲普遍的實踐,能夠被認爲是被證明可行的方式。

從這些意義上說,React的組件建模方式具備相似crud或http verb的統一抽象,是避免出現大量程序員本身發明混亂語義的好辦法。

Bound方法傳遞

父組件向子組件傳遞的Bound方法,應該看做是父組件向子組件提供的一種觸發狀態變化的代理。好比你去酒店,你叫服務生來開門,這是一種相似function call或者message passing的機制,可是服務生也能夠給你一張卡你本身去開門,這就是一種代理;和觀察資源同樣,由於使用了統一的Prop機制,在組件內部看,這種代理也是匿名的,組件並不知道究竟是誰在提供這項功能,它只是在須要的時候使用而已。

這件事情是前端特有的,受限制於HTML的結構。

不少功能組件都不僅是基於觀察邏輯工做,他們還會須要提供功能性服務,功能性服務的入口從哪裏觸發,看應用和系統結構而定,它可能來自用戶操做,可能來自操做系統,也可能來自API請求,後面咱們還會仔細說這個問題。

Tree

事實上絕大多數App,其組件都是能夠用一個tree來表示的,只不過在項目規模不大的時候,你們更喜歡把頂層組件就堆在一塊兒互相引用,這樣變化的時候最靈活。

但React組件的Composition結構更符合組件設計的原則:組件和組件能夠方便的組合起來實現更大的組件,並且最重要的,它仍然是隻有React定義的只有Prop傳遞的組件。一種自類似性。或者叫作Composability。

組件更新

簡單說一下React組件的更新過程;若是一個組件觀察到變化,或者被子組件調用了方法,須要更新狀態,這時若是變化隻影響到自身和某些子組件,它只要直接setState觸發變化便可,React回調用它的render方法觸發一連串的變化,更新是自上至下的,因此比較容易作到更新收斂;若是變化會影響到組件樹上某個非子組件的變化,那麼應該經過上面傳遞下來的Bound方法觸發更高層的組件先作狀態遷移。這個設計會致使在狀態設計上出現mediator模式,anyway,這也是常見模式和基本功了。

這裏須要強調的是,理論上這種更新是同步的,雖然React由於效率問題作了其餘的工做,它的VDOM渲染其實是有Batch和異步的,細節不說了。

混亂的組件通信

那麼若是咱們不說前端,若是寫後端,或者寫系統應用,用React的這個模式構建所有組件樹可行嗎?答案是不,也沒必要要。

這一節的題目叫作混亂的組件通信,咱們來仔細掰扯一下細節,由於組件模型雖然很常說可是對通信過程沒有約定。

第一個登場的是function call。

function call無論是同步的仍是異步的,它沒有區分(1)它是否改變了被調用對象的狀態(2)它是否須要返回值。若是它不須要返回值,它就和emit了一個event沒什麼分別。若是它須要一個返回值,那麼調用者是user角色,被調用者是provider角色,若是被調用者的狀態發生了變化,這至關於crud裏的cud,不然是read。

理解了對function call的分類方式,那麼event和message passing也就好理解了。message和function call同樣是模棱兩可的。在Sequence Diagram裏,有去必然有回的message被稱爲synchronous message,有去無回的叫作asynchrnous;可是咱們避免這個術語,和咱們在JS裏說的不是一回事。可是這個分類方式是對的。

相比之下只有event很純粹,它就是有去無回的。

OK,你看咱們的分類方法很是簡單,就是單向的或者有去有回的。可是內在的故事不簡單。

State & IO

單向的event,它有可能trigger一個模型內的state或者resource變化(後面統稱爲State)。

雙向的通信,是一種承諾,即便是失敗或錯誤也要有返回,咱們稱之爲IO。注意這個定義是我本身發明的,它僅僅表示雙向通信。

雙向通信難道就不會trigger模型內的state變化嗎?這固然是很是可能的。可是問題的關鍵點就在這裏:

對於一個提供IO服務也可能由於IO改變其內部狀態的模塊,你是否在代碼層面上把IO和State分離了呢?

咱們專一於說JavaScript的事件模型;若是你把模塊寫成狀態機,模塊接收到的event會race嗎?固然不會。IO呢?極可能。併發的本質就是IO的併發,event在單線程的事件模型下沒有併發的概念。

那麼這裏就有一個特別簡單的建模方式,你能夠腦補一個雞蛋三明治。

三明治兩邊的麪包(其實只有一邊有面包的邏輯也是同樣的),能夠看做一個是向外提供的IO服務,另外一個是本身須要使用的IO服務;而中間的雞蛋,是這個模塊的State,所有State,狀態機。

進來的IO若是有資源衝突,能夠排隊;出去的IO若是有返回結果,返回結果要看成一個Event來處理。若是某些Event致使當前正在服務或者排隊的IO請求失敗,進來的IO請求隊列清空,所有返回錯誤;若是對象出現生命週期結束,其發出的和服務的IO都要清空,返回失敗或者abort。你看這超級容易,就是callback隊列和handle隊列而已。

咱們把中間這層雞蛋,稱爲該模塊的模型(model),它封裝了共享資源,實現了內部和外部狀態。

在這個模型上前端和後端有沒有區別呢?仍是有的,雖然二者均可以看做在對外提供服務,一個是服務機器另外一個是服務人。後端的服務在對外提供服務的那層面包上,前端呢,前端都是Event進來的。

級聯

在級聯這個問題上,React的組件模型顯示出了它的簡單抽象的威力。若是咱們可以把全部模塊的雞蛋部分,象React組件那樣級聯起來:

  1. React的框架的render過程要本身手寫,並且也不大現實搞成functional風格的,只要遵循其自上至下的更新邏輯便可。

  2. React的依賴性全注入的組件形式是很是誘人的,可是在設計變動時要修改mediator在組件樹上的所在位置也有些惱人。這裏會有一些比較tricky的寫法,可是好消息是對大多數應用而言,其實粗粒度的組件數量尚未一個React寫的網頁裏的組件數量多,因此這件事情也不見得要作到極致去,組件數量很少的時候Pub/Sub工做的也很好。可是對於明確的Leaf Node組件,這樣寫是推薦的。

  3. 同步更新。能所有組件同步更新雞蛋層是很是值得追求的目標。由於它讓你的模型具備一個全局的顯式狀態設計,包含組件相關的數據完整性定義;若是處處是異步狀態更新,這個設計自己就有麻煩,其邏輯完備性不容易檢驗,狀態機很容易根據State/Event組合排查設計完備性和合理性,而同步更新是消滅態空間爆炸的利器,不然狀態之間要排列組合了。

Event Model

JavaScript是Event Model。Event Model編程的核心就是用狀態建模,狀態同步更新容易保證數據完整性。建模的開始是看有那些共享資源須要封裝,把組件一個一個寫出來,而後組合起來。

過程在這裏是二等公民,它主要致力於上面說的麪包層的IO處理。從這個意義上說,callback仍是promise仍是async根本不是重點,沒有什麼值得爭執的,哪一個合適用哪一個。在狀態建模之下,IO過程都被碎片化了,試圖用長途奔襲的方式串聯大量IO操做很難保障設計正確性,光寫出來能跑幾回成功測試的代碼是沒意義的,從這個意義上說我不贊同那些僞線程框架。

事務鎖的問題不是這篇討論的重點。事件模型下用狀態機和IO排隊解決衝突是第一方法,90%以上用這個方法;剩下10%是用opportunistic lock的方式一次性commit多個數據更新狀態,這個也很容易,但須要注意讀入的數據是儘可能同步的(有時這沒法保證,但應該去detect非法組合和重試)。

理想的事件模型應該是計算不消耗時間的;實際上這固然不可能。因此主進程的主要目的是維護全局狀態層,即全部的雞蛋;文件和網絡IO操做Node大多作得很好,須要算力的任務要用Cluster/Worker了,這是Node的短板,只是要求不高的狀況下可用。

若是你的後端或者系統應用是很是stateful的,包括文件持久化的資源,node是很好的選擇;若是隻是對稱的無態邏輯,資源都在數據庫裏,node沒什麼意義;若是算力要求高,數據集也大,不適合在進程間拋來拋去,千萬別用node,go/java/c++都是好得多的選擇。

Rx

我基本沒有Rx的開發經驗,只是看了半本書。

上面說的所有文字,均可以看做是基於事件模型的reactive編程;可是rx框架是另外一個故事,它沒有事件模型假設,有不少語言實現,並且它考慮的問題不是一個應用級的,是分佈式系統級的。

但rx是否是一個好的選擇呢?好比說只用於數據層?

有可能。可是它用於組件層的話,它有幾個問題:

1,它沒約定單向,這個只能本身來;
2,它須要顯式觀察,即subscribe,我的認爲這不如React的注入機制,後者真正讓組件象樂高積木同樣容易組合的,沒有外部需求的組件纔是真正的組件,纔可能隨意拆裝使用;

因此我以爲它寫在組件內觀察被注入進來的狀態變化可能更合適,固然用於密集的異步IO更新的數據集是確定沒問題的。

Final

把關鍵點陳列一下,該說的前面都說過了。

  1. 依賴性注入的組件

  2. 狀態機和資源建模

  3. 狀態或資源變化即事件,不要額外發明語義了

  4. 理解State和IO的區別

  5. 全局級聯的狀態更新,同步!

~~~~~~~~~~~~~~~~

題外話:

最近在重構一箇中等規模項目,在組件模型上想了不少;可是React的原做者們並無特別的以爲他們的設計是unusual的。Jordan Walke的大部分視頻都在談react如何使用。

但在我來看,或者從後端或者系統程序的角度看,react的組件模型在使用上真正符合了組件的定義:無外部依賴,這一點比node裏的module們require來require去高明太多。

相關文章
相關標籤/搜索