Shiny从入门到入定——16-Escaping the graph
发表于:2024-09-26 | 分类: IT
字数统计: 826 | 阅读时长: 3分钟 | 阅读量:

跳出反应式图

16.1 引言

Shiny的反应式编程框架极其有用,因为它能自动确定在输入变化时更新所有输出所需的最小计算集。但这个框架也故意施加了一些限制,有时你需要摆脱这些限制去做一些有风险但必要的事情。

在本章中,你将学习如何结合reactiveValues()observe()/observeEvent()将反应式图的右侧连接到左侧。这些技术之所以强大,是因为它们让你能够手动控制图的一部分。但它们也很危险,因为它们会让你的应用做不必要的工作。最重要的是,你现在可以创建无限循环,让你的应用陷入永无止境的更新循环中。

如果你发现本章中探讨的想法很有趣,你可能还想看看shinySignalsrxtools包。这两个都是实验性包,旨在探索“高阶”反应式,即通过其他反应式编程创建的反应式。我不建议你在“真实”应用中使用它们,但阅读源代码可能会很有启发性。

1
library(shiny)

16.2 反应式图没有捕获什么?

在第14.4节中,我们讨论了当用户导致输入失效时会发生什么。作为应用开发者,你可能还有两个重要的情况需要使输入失效:

  • 你调用一个更新函数并设置值参数。这会向浏览器发送消息以更改输入的值,然后通知R输入值已更改。

  • 你修改了一个反应式值(使用reactiveVal()reactiveValues()创建)的值。

重要的是要理解,在这两种情况下,反应式值和观察者之间并没有创建反应式依赖关系。虽然这些操作会导致图失效,但它们并没有通过新的连接被记录下来。

为了具体说明这个想法,请考虑以下简单的应用,其反应式图如图16.1所示。

1
2
3
4
5
6
7
8
9
10
11
12
ui <- fluidPage(
textInput("nm", "name"),
actionButton("clr", "Clear"),
textOutput("hi")
)
server <- function(input, output, session) {
hi <- reactive(paste0("Hi ", input$nm))
output$hi <- renderText(hi())
observeEvent(input$clr, {
updateTextInput(session, "nm", value = "")
})
}

图16.1 反应式图没有记录未命名观察者和nm输入之间的连接;这种依赖关系超出了它的范围

当你按下清除按钮时会发生什么?

  • 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 多个输入修改一个输出

首先,我们将解决一个非常简单的问题:我希望有一个文本框,它可以由多个事件更新。

<- fluidPage(
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  actionButton("drink", "drink me"),
actionButton("eat", "eat me"),
textOutput("notice")
)
server <- function(input, output, session) {
r <- reactiveValues(notice = "")
observeEvent(input$drink, {
r$notice <- "You are no longer thirsty"
})
observeEvent(input$eat, {
r$notice <- "You are no longer hungry"
})
output$notice <- renderText(r$notice)
}

在下一个例子中,事情会变得稍微复杂一些,我们有一个应用,其中有两个按钮,分别用于增加和减少值。我们使用reactiveValues()来存储当前值,然后使用observeEvent()在按下相应的按钮时增加或减少该值。这里的主要额外复杂性在于,r$n的新值取决于之前的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ui <- fluidPage(
actionButton("up", "up"),
actionButton("down", "down"),
textOutput("n")
)
server <- function(input, output, session) {
r <- reactiveValues(n = 0)
observeEvent(input$up, {
r$n <- r$n + 1
})
observeEvent(input$down, {
r$n <- r$n - 1
})

output$n <- renderText(r$n)
}

图16.2显示了此示例的反应式图。再次注意,反应式图不包括从观察者到反应式值n的任何连接。

图16.2 反应式图不捕获从观察者到输入值的连接

16.3.2 累加输入

如果你想通过累加数据来支持数据输入,那么这也是一个类似的模式。这里的主要区别在于,我们使用updateTextInput()在用户点击添加按钮后重置文本框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ui <- fluidPage(
textInput("name", "name"),
actionButton("add", "add"),
textOutput("names")
)
server <- function(input, output, session) {
r <- reactiveValues(names = character())
observeEvent(input$add, {
r$names <- c(input$name, r$names)
updateTextInput(session, "name", value = "")
})

output$names <- renderText(r$names)
}

我们可以让这个应用更加实用,方法是提供一个删除按钮,并确保添加按钮不会创建重复的名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ui <- fluidPage(
textInput("name", "name"),
actionButton("add", "add"),
actionButton("del", "delete"),
textOutput("names")
)
server <- function(input, output, session) {
r <- reactiveValues(names = character())
observeEvent(input$add, {
r$names <- union(r$names, input$name)
updateTextInput(session, "name", value = "")
})
observeEvent(input$del, {
r$names <- setdiff(r$names, input$name)
updateTextInput(session, "name", value = "")
})

output$names <- renderText(r$names)
}

16.3.3 暂停动画

另一个常见的用例是提供一个开始和停止按钮,以便你控制一些重复发生的事件。此示例使用运行中的反应式值来控制数字是否递增,并使用invalidateLater()来确保在运行时每250毫秒使观察者失效一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ui <- fluidPage(
actionButton("start", "start"),
actionButton("stop", "stop"),
textOutput("n")
)
server <- function(input, output, session) {
r <- reactiveValues(running = FALSE, n = 0)

observeEvent(input$start, {
r$running <- TRUE
})
observeEvent(input$stop, {
r$running <- FALSE
})

observe({
if (r$running) {
r$n <- isolate(r$n) + 1
invalidateLater(250)
}
})
output$n <- renderText(r$n)
}

请注意,在这种情况下,我们不能轻松地使用observeEvent(),因为我们需要根据running()是TRUE还是FALSE来执行不同的操作。由于我们不能使用observeEvent(),因此我们必须使用isolate()——如果我们不使用它,这个观察者还会对n产生反应式依赖,而n正是它更新的对象,因此它会陷入无限循环。

希望这些示例能让你对使用reactiveValues()observe()进行编程有所感受。它非常直观:当发生这种情况时,执行那个操作;当发生那种情况时,执行另一个操作。这在小范围内更容易理解,但当更大的部分开始交互时就更难理解了。因此,一般来说,你会希望尽可能少地使用它,并将其隔离开来,以便尽可能少的观察者修改反应式值。

16.3.4 练习

提供一个服务器函数,当点击“normal”时,绘制100个正态分布的随机数的直方图,当点击“uniform”时,绘制100个均匀分布的随机数。

1
2
3
4
5
ui <- fluidPage(
actionButton("rnorm", "Normal"),
actionButton("runif", "Uniform"),
plotOutput("plot")
)

修改你上面的代码以使其与以下UI一起工作:

1
2
3
4
5
ui <- fluidPage(
selectInput("type", "type", c("Normal", "Uniform")),
actionButton("go", "go"),
plotOutput("plot")
)

根据你之前的答案重写代码,以消除对observe()/observeEvent()的使用,而仅使用reactive()。为什么你可以在第二个UI中这样做,而在第一个UI中却不能?

16.4 反模式 (Anti-patterns)

一旦你掌握了这种模式,就很容易养成坏习惯:

1
2
3
4
5
6
7
8
9
server <- function(input, output, session) {
r <- reactiveValues(df = cars)
observe({
r$df <- head(cars, input$nrows)
})

output$plot <- renderPlot(plot(r$df))
output$table <- renderTable(r$df)
}

在这个简单的例子中,与使用reactive()的替代方案相比,这段代码并没有做太多额外的工作:

1
2
3
4
5
6
server <- function(input, output, session) {
df <- reactive(head(cars, input$nrows))

output$plot <- renderPlot(plot(df()))
output$table <- renderTable(df())
}

但仍然存在两个缺点:

  • 如果表格或图表位于当前不可见的标签页中,观察者仍然会绘制/显示它们。

  • 如果head()函数出错,observe()将终止应用,但reactive()会传播错误,以便显示。然而,如果reactive()抛出错误,它不会被传播。

随着应用变得越来越复杂,情况会逐渐变得更糟。很容易退回到第13.2.3节中描述的事件驱动编程情况。你最终会花费大量精力来分析应用中的事件流,而不是依赖Shiny自动为你处理。

比较这两个反应式图是有启发性的。图16.3显示了第一个示例的图。这是误导性的,因为它看起来不像nrowsdf()有连接。使用如图16.4所示的反应式,可以很容易地看到它们之间的精确连接。拥有一个尽可能简单的反应式图对于人类和Shiny来说都很重要。简单的图对人类来说更容易理解,对Shiny来说也更容易优化。

图16.3 使用响应式值和观察者会导致图表的一部分断开连接

图16.4 使用响应式编程使得组件之间的依赖关系非常清晰

16.5 总结

在过去的四章中,您深入了解了Shiny使用的响应式编程模型。您学习了为什么响应式编程很重要(它允许Shiny仅完成所需的工作,不多也不少),以及响应式图的细节。您还稍微了解了一些基本构建块在内部的工作原理,以及如何在需要时利用它们来摆脱响应式图的限制。

在接下来的七章中,您将学习如何保持Shiny应用程序的可维护性、性能和安全性,随着其规模和影响力的不断增长。

代码获取

关注公众号“生信之巅”,聊天窗口回复“85d7”获取下载链接。

生信之巅微信公众号 生信之巅小程序码

敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!Notice: When you use the scripts in this article, please cite the link of this webpage. Thank you!

下一篇:
Shiny从入门到入定——15-Reactive building blocks