美文网首页iOS 开发每天分享优质文章ios 知识点
反应式编程—增量编程:了解反应性毛刺

反应式编程—增量编程:了解反应性毛刺

作者: iOS_小久 | 来源:发表于2019-06-25 21:07 被阅读103次

我们看一个具有惊人行为的反应性管道的例子,讨论它发生的原因,以及如何改进它。

今天我们想展示一下在实践中出现的反应式编程的具体问题。我们将讨论它为什么会发生以及我们可以做些什么。

小编这里推荐一个群: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在上面的代码片段,但我们的反应式库只提供mapflatMap,这也适用。

我们观察cellularEnabled房产并打印其价值。如果我们打开飞行模式,我们得到了一个falsecellularEnabled

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被召唤的观察者两次,因为新值经过两条路径,经过wifiEnabledcellularEnabled。这是一个有趣的效果,但它还不是我们想要展示的问题。如果我们再次禁用飞行模式,我们会看到观察者首先被调用, 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,有两个孩子。两者都放入队列,按高度排序。在这种情况下,无论是 wifiEnabledcellularEnabled具有相同的高度,所以它并不重要,首先处理。处理完毕后wifiEnabled,我们将其子(map操作)放入队列中。现在我们在队列中有两个不同高度的元素,因为cellularEnabled有更高的数字,我们先处理它。这会将其子项放入队列中,从而导致两个项目再次具有相同的高度,因此我们继续使用其中一个项目。当我们处理第一个子节点时,它的子节点cellularAndWifi被放入队列中,并且在处理完第二个.map { ... }节点之后 ,我们不会将其子节点放入队列中,因为它已经存在。

简而言之,价值以另一种方式流过图表; 它们按照您对反应框架的期望顺序,更加同步地向下分流不同的分支。我们现在不必区分 flatMapzip,因为框架以正确的顺序触发所有观察者。

优点和缺点

我们现在可以在&&每个步骤中使用运算符,包括我们zip在以前版本的框架中必须使用的最后一个步骤:

let cellularEnabled = notAirplaneMode && cellular
let wifiEnabled = notAirplaneMode && wifi
let wifiAndCellular = wifiEnabled && cellularEnabled` </pre>

使用队列算法,我们不必考虑如何将我们的反应属性联系在一起,我们可以使用像&&。权衡是队列的内部处理变得更加复杂。

这种算法称为拓扑排序,因为它以拓扑方式对图的节点进行排序,以便按正确的顺序处理它们。


扫码进交流群 有技术的来闲聊 没技术的来学习

691040931

原文转载地址:https://talk.objc.io/episodes/S01E76-understanding-reactive-glitches

相关文章

网友评论

    本文标题:反应式编程—增量编程:了解反应性毛刺

    本文链接:https://www.haomeiwen.com/subject/muffcctx.html