我们看一个具有惊人行为的反应性管道的例子,讨论它发生的原因,以及如何改进它。
今天我们想展示一下在实践中出现的反应式编程的具体问题。我们将讨论它为什么会发生以及我们可以做些什么。
小编这里推荐一个群:691040931 里面有大量的书籍和面试资料哦有技术的来闲聊 没技术的来学习
构建一个例子
假设我们正在构建一个虚构的设置应用程序,它具有飞行模式,Wi-Fi和蜂窝数据的开关。启用飞行模式后,必须禁用Wi-Fi和蜂窝电话。禁用飞行模式时,我们必须为Wi-Fi和蜂窝交换机采用两种原始设置。最后,我们想要跟踪何时启用Wi-Fi和蜂窝网络。
我们开始使用我们编写的最小反应库来构建逻辑:
let airplaneMode = Observable<Bool>(false)
let cellular = Observable<Bool>(true)
let wifi = Observable<Bool>(true)` </pre>
为了确定蜂窝状态,我们将倒置飞机模式与蜂窝设置结合起来:
let notAirplaneMode = airplaneMode.map { !$0 }
let cellularEnabled = notAirplaneMode.flatMap { na in
cellular.map { $0 && na }
}
理想情况下,我们会一直使用的功能就像combineLatest
在上面的代码片段,但我们的反应式库只提供map
和flatMap
,这也适用。
我们观察cellularEnabled
房产并打印其价值。如果我们打开飞行模式,我们得到了一个false
为cellularEnabled
:
cellularEnabled.observe { print($0) }
airplaneMode.send(true)
/*
true
false
*/
到目前为止一切顺利。我们做一些复制和粘贴添加 wifiEnabled
和我们最终感兴趣的价值,wifiAndCellular
:
let wifiEnabled = notAirplaneMode.flatMap { na in
wifi.map { $0 && na }
}
let wifiAndCellular = wifiEnabled.flatMap { we in
cellularEnabled.map { $0 && we }
}
现在我们可以观察后一个属性,看看是否启用了Wi-Fi和蜂窝。我们打印一些破折号弄清楚发生了什么-我们在开始时true
进行wifiAndCellular
,然后我们启用飞行模式,我们得到false
两次:
wifiAndCellular.observe { print($0) }
print("–––")
airplaneMode.send(true)
/*
true
–––
false
false
*/
结果中的毛刺
wifiAndCellular
被召唤的观察者两次,因为新值经过两条路径,经过wifiEnabled
和cellularEnabled
。这是一个有趣的效果,但它还不是我们想要展示的问题。如果我们再次禁用飞行模式,我们会看到观察者首先被调用, true
然后使用false
:
wifiAndCellular.observe { print($0) }
print("–––")
airplaneMode.send(true)
print("–––")
airplaneMode.send(false)
/*
true
–––
false
false
–––
false
true
*/
用两个不同的值调用观察者是令人惊讶的,但它出现的原因和以前一样:值沿着两条不同的路径行进并最终重新加入。下图显示了我们发生了什么:

这里我们看到所有属性的依赖关系。通过发送新值airplaneMode
,其子项将被触发。之后notAirplaneMode
,该值沿着左侧分支向下移动到我们的观察者,wifiAndCellular
此时cellularEnabled
右侧分支中的值尚未更新。
在调用观察者之后我们打印第一个输出, notAirplaneMode
继续其他子节点,值沿着右边的分支向我们的观察者移动,现在可以打印正确的值:

反应性毛刺问题
在实践中,这种奇怪的行为不一定是有问题的。如果我们从反应管道获取值并将其绑定到标签,我们可能会在设置正确的值之前暂时设置错误的值。这不是最有效的,但它仍然有效。但是,如果我们对接收到的值执行其他操作,例如启动网络请求,写入文件或附加到数组,那么我们收到多个中间值而不是单个最终结果肯定是有问题的。其中一些中间结果甚至可能是错误的值,导致意外的行为和错误。
由开发人员为他们的图表选择合适的组合器来减轻不必要的影响。在这种情况下,我们应该使用类似的函数zip
- 我们将路径连接在一起 - 而不是flatMap
,以便在继续之前等待来自两个分支的值。
在我们进入解决方案之前,让我们看一下另一个抽象问题。我们的三种用途flatMap
是一种反应型&&
。我们可以用这种方式编写一个结合了可观察布尔值的函数:
func &&(lhs: Observable<Bool>, rhs: Observable<Bool>) -> Observable<Bool> {
return lhs.flatMap { l in
rhs.map { $0 && l }
}
}
我们想编写这个函数并使用它,但不幸的是我们正在调用flatMap
它的实现。我们应该把这个选择留给使用框架的开发人员。这是因为,在某些情况下,他们可能不想使用flatMap
,而是zip
像我们当前的例子中那样。
拓扑排序
我们的反应库的修改版本提供了一个解决方案。我们将示例代码复制到另一个带有更新库的游乐场。输出非常不同,因为该库以不同方式处理观察值。现在,每次我们发送新值时,我们只会得到一个打印值:
true
–––
false
–––
true
为了理解库的作用,我们再次查看图表。我们在不同的高度(或深度)级别重新组织图形。图的底部是高度为零,并且每个级别向上,节点获得更大的数字:

当我们发送新值时,airplaneMode
会在下面的动画中显示出来。以下是它的要点:发送一个新值将 airplaneMode
所有子节点放在队列中。airplaneMode
只有一个孩子,在触发后,我们继续前进notAirplaneMode
,有两个孩子。两者都放入队列,按高度排序。在这种情况下,无论是 wifiEnabled
与cellularEnabled
具有相同的高度,所以它并不重要,首先处理。处理完毕后wifiEnabled
,我们将其子(map
操作)放入队列中。现在我们在队列中有两个不同高度的元素,因为cellularEnabled
有更高的数字,我们先处理它。这会将其子项放入队列中,从而导致两个项目再次具有相同的高度,因此我们继续使用其中一个项目。当我们处理第一个子节点时,它的子节点cellularAndWifi
被放入队列中,并且在处理完第二个.map { ... }
节点之后 ,我们不会将其子节点放入队列中,因为它已经存在。
简而言之,价值以另一种方式流过图表; 它们按照您对反应框架的期望顺序,更加同步地向下分流不同的分支。我们现在不必区分 flatMap
和zip
,因为框架以正确的顺序触发所有观察者。
优点和缺点
我们现在可以在&&
每个步骤中使用运算符,包括我们zip
在以前版本的框架中必须使用的最后一个步骤:
let cellularEnabled = notAirplaneMode && cellular
let wifiEnabled = notAirplaneMode && wifi
let wifiAndCellular = wifiEnabled && cellularEnabled` </pre>
使用队列算法,我们不必考虑如何将我们的反应属性联系在一起,我们可以使用像&&
。权衡是队列的内部处理变得更加复杂。
这种算法称为拓扑排序,因为它以拓扑方式对图的节点进行排序,以便按正确的顺序处理它们。
扫码进交流群 有技术的来闲聊 没技术的来学习

网友评论