状态变化需要响应,这种响应有时候是连锁反应。具体来看一个传统的 HTML 表单界面,相信上网时间长一点的应该都很熟悉了:
这个简单的表单,要做得好用了,变化响应情况并不简单:
1. 没有输入之前,两个按钮都应该灰掉。
2. 任何一个输入框有输入后,Reset 按钮都被激活。
3. 两个输入框都有输入内容之后,Submit 按钮被激活。
4. 任何一个输入框变空时,Submit 按钮要灰掉。
5. Reset 被点击,清除两个输入框的内容,连锁[1]。
6. Submit 被点击,对每个输入进行有效性检查
a. 全部有效,保存,并前往下一个界面。
b. 任何一个输入无效,将相应的输入框变成红色背景,并在其右侧显示错误信息。
其实还可做得更好,比如在输入时,立即进行有效性检查,弹出自动完成提示等等。
当然,要先知道有变化了,才谈得上响应。通常我们可以主动查询状态(轮询),也可注册到变化源上等通知(回调),还可以用一个中间组件专门处理注册与通知派发(事件分发)。
回调用得比较广泛,写起也直接,但是在有连锁时逻辑会很零散,有依赖关系时也需要主动查询,示意如下:
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();
这种零散,在程序略微有点长之后,就是一个很大的心智负担,修改规则时也非常容易错漏。考虑到变化响应逻辑实际上有两部分:业务及状态修改,把新状态反映到界面上;有一种改进方法,是后面部分集中起来。比如:
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()
轮循比较难安排响应的时间点,没变化时也要转有点浪费,比如:
timer.on(500ms, updateUI);
不过在大量频繁变化的情况下,可以省掉派遣、回调的开销,保持稳定的性能。
如果有事件派发组件的话,程序可能是这样的:
on textfield-change (src) : updateUI();
on button-click(src) :
if ( src is submit ) submit();
if ( src is reset ) resetFields();
updateUI();
不过,总是逃不掉写那个 updateUI(),如果界面复杂了,还是一样烦人的。其实, 真正有书写价值的只有两条:
submit.enabled = (firstName not empty) && (lastName not empty);
reset.click => resetFields();
如果给事件派发组件再加点功能,其实可以省掉自己去处理 onChange 的:
dispatcher.connect( src: [firstName.text, lastName.text],
target: rest.enabled,
expr:{...} );
dispatcher.on( event: click, src: reset, do: resetFields );
connect()一种可能的实现办法是:当 TextField 在编辑时,它内部会不停的修改 .text 属性,而 dispatcher 把自己注册成 firstName 和 lastName 两个对象的 text 属性的观察者,此时它被触发,执行 expr 并将结果值赋给 target 属性。
对于这种实现方法做一点拔高,我们可以认为:
1. 编写变化响应代码更聚焦在属性和规则上了,也就变得直接。
2. 框架提供的机制替换每次要写的胶水代码,省事儿了。
3. 概念上,属性变化要可被观测。
能告知自己有变化的元素,取个名字叫 Observable;要收到变化通知进行处理的,取个名字叫 Observer。观察者会自动在观测对象变化时被触发,这就是响应式(Reactive)编程了
考虑更复杂的情况,
(from https://sites.google.com/site/fujitarium/Houdini/sop/vdb)
这是 sidefx Houdini 的一个场景,通过右侧的网络定义出左侧图形效果,网络上的一个节点称为一个 OP,节点上下的凸块是输入、输出属性,连接线表示输出起点OP的属性值给终点属性。如果调整一下上图中比较靠上的节点,下游的节点都会受到影响,最终左侧的结果也会立即改变。
对应这个场景的代码,如果能像下面这么写:
(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 的运算结果如果有变化,就会自动逐级传下去,最生成正确的结果,是不是很爽?
这种变化影响传播的链条,显然可以看作一种流(Stream)。而函数式编程范式里,将函数做为数据进行流动,是其实程序逻辑的基础。从1980年代的 SISAL 语言开始,函数式语言研究者并已经把这个模式下实现全部程序逻辑需要的流操作,很好的抽象整理了一套出来:map/filter/reduce。Reactive 的响应式,加上 Functional 的流操作,这就是 Functional Reactive Programming (FRP) 啦。
FRP 的精髓就在于,框架替开发者写胶水代码,将变化响应、传递表达得更直接。所以,框架一般还得带一个执行调度器,不然为了把多个异步活动串进响应流里,还得写异步任务调度的胶水代码。这就是现在最热闹的 Rx 系列啦。
看上去 RxAndroid 有点重,Agera 就轻量多了。一直不喜欢 ReactiveCocoa/RxSwift 的实现,RxJS 还不错。