跳出反应式图
16.1 引言
Shiny的反应式编程框架极其有用,因为它能自动确定在输入变化时更新所有输出所需的最小计算集。但这个框架也故意施加了一些限制,有时你需要摆脱这些限制去做一些有风险但必要的事情。
在本章中,你将学习如何结合reactiveValues()和observe()
/observeEvent()
将反应式图的右侧连接到左侧。这些技术之所以强大,是因为它们让你能够手动控制图的一部分。但它们也很危险,因为它们会让你的应用做不必要的工作。最重要的是,你现在可以创建无限循环,让你的应用陷入永无止境的更新循环中。
如果你发现本章中探讨的想法很有趣,你可能还想看看shinySignals和rxtools包。这两个都是实验性包,旨在探索“高阶”反应式,即通过其他反应式编程创建的反应式。我不建议你在“真实”应用中使用它们,但阅读源代码可能会很有启发性。
1 | library(shiny) |
16.2 反应式图没有捕获什么?
在第14.4节中,我们讨论了当用户导致输入失效时会发生什么。作为应用开发者,你可能还有两个重要的情况需要使输入失效:
你调用一个更新函数并设置值参数。这会向浏览器发送消息以更改输入的值,然后通知R输入值已更改。
你修改了一个反应式值(使用
reactiveVal()
或reactiveValues()
创建)的值。
重要的是要理解,在这两种情况下,反应式值和观察者之间并没有创建反应式依赖关系。虽然这些操作会导致图失效,但它们并没有通过新的连接被记录下来。
为了具体说明这个想法,请考虑以下简单的应用,其反应式图如图16.1所示。
1 | ui <- fluidPage( |
当你按下清除按钮时会发生什么?
input$clr
失效,然后观察者也会失效。- 观察者重新计算,重新建立对
input$clr
的依赖,并告诉浏览器更改输入控件的值。 - 浏览器更改nm的值。
input$nm
失效,导致hi()
失效,然后是output$hi
。output$hi
重新计算,强制hi()
重新计算。
这些操作都没有改变反应式图,所以它仍然如图16.1所示,并且图没有捕获从观察者到input$nm
的连接。
16.3 案例研究
接下来,让我们看一些有用的案例,在这些案例中,你可能需要结合reactiveValues()
和observeEvent()
或observe()
来解决一些非常具有挑战性(甚至不可能)的问题。这些是你的应用中有用的模板。
16.3.1 多个输入修改一个输出
首先,我们将解决一个非常简单的问题:我希望有一个文本框,它可以由多个事件更新。
1 | actionButton("drink", "drink me"), |
在下一个例子中,事情会变得稍微复杂一些,我们有一个应用,其中有两个按钮,分别用于增加和减少值。我们使用reactiveValues()
来存储当前值,然后使用observeEvent()
在按下相应的按钮时增加或减少该值。这里的主要额外复杂性在于,r$n
的新值取决于之前的值。
1 | ui <- fluidPage( |
图16.2显示了此示例的反应式图。再次注意,反应式图不包括从观察者到反应式值n
的任何连接。
16.3.2 累加输入
如果你想通过累加数据来支持数据输入,那么这也是一个类似的模式。这里的主要区别在于,我们使用updateTextInput()在用户点击添加按钮后重置文本框。
1 | ui <- fluidPage( |
我们可以让这个应用更加实用,方法是提供一个删除按钮,并确保添加按钮不会创建重复的名称:
1 | ui <- fluidPage( |
16.3.3 暂停动画
另一个常见的用例是提供一个开始和停止按钮,以便你控制一些重复发生的事件。此示例使用运行中的反应式值来控制数字是否递增,并使用invalidateLater()
来确保在运行时每250毫秒使观察者失效一次。
1 | ui <- fluidPage( |
请注意,在这种情况下,我们不能轻松地使用observeEvent()
,因为我们需要根据running()
是TRUE还是FALSE来执行不同的操作。由于我们不能使用observeEvent()
,因此我们必须使用isolate()
——如果我们不使用它,这个观察者还会对n
产生反应式依赖,而n
正是它更新的对象,因此它会陷入无限循环。
希望这些示例能让你对使用reactiveValues()
和observe()
进行编程有所感受。它非常直观:当发生这种情况时,执行那个操作;当发生那种情况时,执行另一个操作。这在小范围内更容易理解,但当更大的部分开始交互时就更难理解了。因此,一般来说,你会希望尽可能少地使用它,并将其隔离开来,以便尽可能少的观察者修改反应式值。
16.3.4 练习
提供一个服务器函数,当点击“normal”时,绘制100个正态分布的随机数的直方图,当点击“uniform”时,绘制100个均匀分布的随机数。
1 | ui <- fluidPage( |
修改你上面的代码以使其与以下UI一起工作:
1 | ui <- fluidPage( |
根据你之前的答案重写代码,以消除对observe()
/observeEvent()
的使用,而仅使用reactive()
。为什么你可以在第二个UI中这样做,而在第一个UI中却不能?
16.4 反模式 (Anti-patterns)
一旦你掌握了这种模式,就很容易养成坏习惯:
1 | server <- function(input, output, session) { |
在这个简单的例子中,与使用reactive()
的替代方案相比,这段代码并没有做太多额外的工作:
1 | server <- function(input, output, session) { |
但仍然存在两个缺点:
如果表格或图表位于当前不可见的标签页中,观察者仍然会绘制/显示它们。
如果
head()
函数出错,observe()
将终止应用,但reactive()
会传播错误,以便显示。然而,如果reactive()
抛出错误,它不会被传播。
随着应用变得越来越复杂,情况会逐渐变得更糟。很容易退回到第13.2.3节中描述的事件驱动编程情况。你最终会花费大量精力来分析应用中的事件流,而不是依赖Shiny自动为你处理。
比较这两个反应式图是有启发性的。图16.3显示了第一个示例的图。这是误导性的,因为它看起来不像nrows
与df()
有连接。使用如图16.4所示的反应式,可以很容易地看到它们之间的精确连接。拥有一个尽可能简单的反应式图对于人类和Shiny来说都很重要。简单的图对人类来说更容易理解,对Shiny来说也更容易优化。
16.5 总结
在过去的四章中,您深入了解了Shiny使用的响应式编程模型。您学习了为什么响应式编程很重要(它允许Shiny仅完成所需的工作,不多也不少),以及响应式图的细节。您还稍微了解了一些基本构建块在内部的工作原理,以及如何在需要时利用它们来摆脱响应式图的限制。
在接下来的七章中,您将学习如何保持Shiny应用程序的可维护性、性能和安全性,随着其规模和影响力的不断增长。
代码获取
关注公众号“生信之巅”,聊天窗口回复“85d7”获取下载链接。
敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!