狀態(tài)變化需要響應(yīng),這種響應(yīng)有時候是連鎖反應(yīng)。具體來看一個傳統(tǒng)的 HTML 表單界面,相信上網(wǎng)時間長一點(diǎn)的應(yīng)該都很熟悉了:

這個簡單的表單,要做得好用了,變化響應(yīng)情況并不簡單:
1. 沒有輸入之前,兩個按鈕都應(yīng)該灰掉。
2. 任何一個輸入框有輸入后,Reset 按鈕都被激活。
3. 兩個輸入框都有輸入內(nèi)容之后,Submit 按鈕被激活。
4. 任何一個輸入框變空時,Submit 按鈕要灰掉。
5. Reset 被點(diǎn)擊,清除兩個輸入框的內(nèi)容,連鎖[1]。
6. Submit 被點(diǎn)擊,對每個輸入進(jìn)行有效性檢查
a. 全部有效,保存,并前往下一個界面。
b. 任何一個輸入無效,將相應(yīng)的輸入框變成紅色背景,并在其右側(cè)顯示錯誤信息。
其實(shí)還可做得更好,比如在輸入時,立即進(jìn)行有效性檢查,彈出自動完成提示等等。
當(dāng)然,要先知道有變化了,才談得上響應(yīng)。通常我們可以主動查詢狀態(tài)(輪詢),也可注冊到變化源上等通知(回調(diào)),還可以用一個中間組件專門處理注冊與通知派發(fā)(事件分發(fā))。
回調(diào)用得比較廣泛,寫起也直接,但是在有連鎖時邏輯會很零散,有依賴關(guān)系時也需要主動查詢,示意如下:
firstName.onChange :
if lastName not empty, submit.enable()
else if self is empty, submit.disable();
lastName.onChange :
if firstName not empty, submit.enable()
else
if self is empty, submit.disable();
reset.onClick :
firstName.clear();
lastName.clear();
這種零散,在程序略微有點(diǎn)長之后,就是一個很大的心智負(fù)擔(dān),修改規(guī)則時也非常容易錯漏??紤]到變化響應(yīng)邏輯實(shí)際上有兩部分:業(yè)務(wù)及狀態(tài)修改,把新狀態(tài)反映到界面上;有一種改進(jìn)方法,是后面部分集中起來。比如:
firstName.onChange : updateUI();
lastName.onChange : updateUI();
reset.onClick : resetFields(); updateUI();
resetFields:
firstName.clear();
lastName.clear();
updateUI :
if lastName is empty,
if firstName is empty, submit.disable()
else
submit.enable()
輪循比較難安排響應(yīng)的時間點(diǎn),沒變化時也要轉(zhuǎn)有點(diǎn)浪費(fèi),比如:
timer.on(500ms, updateUI);
不過在大量頻繁變化的情況下,可以省掉派遣、回調(diào)的開銷,保持穩(wěn)定的性能。
如果有事件派發(fā)組件的話,程序可能是這樣的:
on textfield-change (src) : updateUI();
on button-click(src) :
if ( src is submit ) submit();
if ( src is reset ) resetFields();
updateUI();
不過,總是逃不掉寫那個 updateUI(),如果界面復(fù)雜了,還是一樣煩人的。其實(shí), 真正有書寫價值的只有兩條:
submit.enabled = (firstName not empty) && (lastName not empty);
reset.click => resetFields();
如果給事件派發(fā)組件再加點(diǎn)功能,其實(shí)可以省掉自己去處理 onChange 的:
dispatcher.connect( src: [firstName.text, lastName.text],
target: rest.enabled,
expr:{...} );
dispatcher.on( event: click, src: reset, do: resetFields );
connect()一種可能的實(shí)現(xiàn)辦法是:當(dāng) TextField 在編輯時,它內(nèi)部會不停的修改 .text 屬性,而 dispatcher 把自己注冊成 firstName 和 lastName 兩個對象的 text 屬性的觀察者,此時它被觸發(fā),執(zhí)行 expr 并將結(jié)果值賦給 target 屬性。
對于這種實(shí)現(xiàn)方法做一點(diǎn)拔高,我們可以認(rèn)為:
1. 編寫變化響應(yīng)代碼更聚焦在屬性和規(guī)則上了,也就變得直接。
2. 框架提供的機(jī)制替換每次要寫的膠水代碼,省事兒了。
3. 概念上,屬性變化要可被觀測。
能告知自己有變化的元素,取個名字叫 Observable;要收到變化通知進(jìn)行處理的,取個名字叫 Observer。觀察者會自動在觀測對象變化時被觸發(fā),這就是響應(yīng)式(Reactive)編程了
考慮更復(fù)雜的情況,

(from https://sites.google.com/site/fujitarium/Houdini/sop/vdb)
這是 sidefx Houdini 的一個場景,通過右側(cè)的網(wǎng)絡(luò)定義出左側(cè)圖形效果,網(wǎng)絡(luò)上的一個節(jié)點(diǎn)稱為一個 OP,節(jié)點(diǎn)上下的凸塊是輸入、輸出屬性,連接線表示輸出起點(diǎn)OP的屬性值給終點(diǎn)屬性。如果調(diào)整一下上圖中比較靠上的節(jié)點(diǎn),下游的節(jié)點(diǎn)都會受到影響,最終左側(cè)的結(jié)果也會立即改變。
對應(yīng)這個場景的代碼,如果能像下面這么寫:
(platonic1.out -> convert1.in);
(convert1.out |-> vdbfrompolygons1.in1
|-> scatter1.in);
(vdbfrompolygons1.out -> vdbfracturel.in1
(sphere1.out -> vdbfracturel.in2);
(scatter1.out -> vdbfracturel.in3);
(vdbfracturel.out -> convertvdb1.in);
然后,platonic1 的運(yùn)算結(jié)果如果有變化,就會自動逐級傳下去,最生成正確的結(jié)果,是不是很爽?
這種變化影響傳播的鏈條,顯然可以看作一種流(Stream)。而函數(shù)式編程范式里,將函數(shù)做為數(shù)據(jù)進(jìn)行流動,是其實(shí)程序邏輯的基礎(chǔ)。從1980年代的 SISAL 語言開始,函數(shù)式語言研究者并已經(jīng)把這個模式下實(shí)現(xiàn)全部程序邏輯需要的流操作,很好的抽象整理了一套出來:map/filter/reduce。Reactive 的響應(yīng)式,加上 Functional 的流操作,這就是 Functional Reactive Programming (FRP) 啦。
FRP 的精髓就在于,框架替開發(fā)者寫膠水代碼,將變化響應(yīng)、傳遞表達(dá)得更直接。所以,框架一般還得帶一個執(zhí)行調(diào)度器,不然為了把多個異步活動串進(jìn)響應(yīng)流里,還得寫異步任務(wù)調(diào)度的膠水代碼。這就是現(xiàn)在最熱鬧的 Rx 系列啦。
看上去 RxAndroid 有點(diǎn)重,Agera 就輕量多了。一直不喜歡 ReactiveCocoa/RxSwift 的實(shí)現(xiàn),RxJS 還不錯。