15 反应式构建块
既然你已经理解了反应式图背后的理论,并且有了一些实践经验,那么现在是时候更详细地讨论反应式如何融入R编程语言中了。反应式编程有三个基本构建块:反应式值
、反应式表达式
和观察者
。你已经看到了反应式值和表达式的大部分重要部分,所以本章将花更多时间讨论观察者和输出(你将了解到输出是一种特殊的观察者)。你还将学习控制反应式图的另外两个工具:隔离
和定时无效化
。
本章将再次使用反应式控制台,这样我们就可以直接在控制台中试验反应式,而无需每次都启动一个Shiny应用程序。
1 | library(shiny) |
15.1 反应式值
反应式值有两种类型:
使用reactiveVal()创建的单个反应式值。
使用reactiveValues()创建的反应式值列表。
它们在获取和设置值时的接口略有不同:
1 | x <- reactiveVal(10) |
不幸的是,这两个类似的对象有着截然不同的接口,但没有办法将它们标准化。然而,尽管它们看起来不同,但行为是相同的,所以你可以根据自己的语法偏好来选择使用哪一个。在本书中,我使用reactiveValues()
,因为它的语法一目了然,但在我的代码中,我倾向于使用reactiveVal()
,因为它的语法清楚地表明正在发生一些不寻常的事情。
重要的是要注意,这两种类型的反应式值都具有所谓的引用语义。大多数R对象在修改时具有复制语义,这意味着如果你将相同的值赋给两个名称,一旦你修改了其中一个,连接就会断开:
1 | a1 <- a2 <- 10 |
但反应式值则不是这样——它们总是保持对相同值的引用,因此修改任何副本都会修改所有值:
1 | b1 <- b2 <- reactiveValues(x = 10) |
我们将在第16章中讨论为什么你可能需要创建自己的反应式值。否则,你将遇到的大多数反应式值都将来自server
函数的输入参数。这些与你自己创建的reactiveValues()
略有不同,因为它们是只读的:你不能修改这些值,因为Shiny会根据浏览器中的用户操作自动更新它们。
15.1.1 练习
这两个反应式值列表之间有什么区别?比较获取和设置单个反应式值的语法。
1
2l1 <- reactiveValues(a = 1, b = 2)
l2 <- list(a = reactiveVal(1), b = reactiveVal(2))设计并执行一个小实验,以验证
reactiveVal()
也具有引用语义。
15.2 反应式表达式
回忆一下,反应式有两个重要特性:它是惰性
的并且具有缓存
。这意味着它只有在真正需要时才会工作,如果连续调用两次,它会返回之前的值。
我们还没有涵盖两个重要的细节:反应式表达式如何处理错误,以及为什么on.exit()
在它们内部有效。
15.2.1 错误
反应式表达式以与缓存值完全相同的方式缓存错误。例如,考虑这个反应式:
1 | r <- reactive(stop("Error occured at ", Sys.time(), call. = FALSE)) |
如果我们等待一两秒钟,我们可以看到我们得到了与之前相同的错误:
1 | Sys.sleep(2) |
在反应式图中,错误的处理方式与值相同:错误通过反应式图传播的方式与常规值完全相同。唯一的区别是当错误遇到输出或观察者时会发生什么:
- 输出中的错误将在应用程序中显示。
- 观察者中的错误将导致当前会话终止。如果你不希望发生这种情况,你需要将代码包装在
try()
或tryCatch()
中。
这个相同的系统还支持req()
(第8.1.2节),它发出一种特殊类型的错误。这种特殊错误会导致观察者和输出停止它们正在做的事情,但不会导致失败。默认情况下,它会使输出重置为其初始空白状态,但如果你使用req(..., cancelOutput = TRUE)
,它们将保留其当前显示。
15.2.2 on.exit()
你可以将reactive(x())
视为function() x()
的快捷方式,它自动添加了惰性和缓存。这主要在你想要了解Shiny是如何实现的时候很重要,但这也意味着你可以使用那些只能在函数内部工作的函数。其中最有用的是on.exit()
,它允许你在反应式表达式完成时运行代码,无论反应式是否成功返回错误或失败。这就是on.exit()
在第8.2.2节中工作的原因。
15.2.3 练习
使用
reactlog
包来观察以下应用程序中错误通过反应式传播的情况,确认它遵循与值传播相同的规则。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16ui <- fluidPage(
checkboxInput("error", "error?"),
textOutput("result")
)
server <- function(input, output, session) {
a <- reactive({
if (input$error) {
stop("Error!")
} else {
1
}
})
b <- reactive(a() + 1)
c <- reactive(b() + 1)
output$result <- renderText(c())
}修改上述应用程序,使用
req()
代替stop()
。验证事件是否仍然以相同的方式传播。当你使用cancelOutput
参数时会发生什么?
15.3 观察者和输出
观察者和输出是反应式图中的终端节点。它们在两个重要方面与反应式表达式不同:
它们是急切的且健忘的——它们尽可能快地运行,并且不记得之前的操作。这种急切性是“传染性的”,因为如果它们使用了一个反应式表达式,那么该反应式表达式也将被评估。
观察者返回的值被忽略,因为它们被设计为与称为副作用的函数一起工作,如
cat()
或write.csv()
。
观察者和输出由同一个底层工具提供支持:observe()
。这设置了一个代码块,每次它使用的反应式值或表达式更新时,该代码块就会运行。请注意,当你创建观察者时,它会立即运行——它必须这样做才能确定其反应式依赖项。
1 | y <- reactiveVal(10) |
在这本书中,我很少使用observe(),因为它是为用户友好的observeEvent()提供动力的底层工具。通常,你应该坚持使用observeEvent()
,除非它无法实现你想要的功能。在本书中,我只会向你展示一个必须使用observe()
的案例,即第16.3.3节。
observe()
也为响应式输出提供动力。响应式输出是一种特殊的观察者,它们具有两个重要属性:
当你将它们赋值给输出时,它们就会被定义,即
output$text <- ...
会创建观察者。它们具有一些有限的检测能力,可以检测到自己是否不可见(即它们位于非活动标签页中),因此不必重新计算。
重要的是要注意,observe()
和响应式输出并不“执行”某些操作,而是“创建”某些东西(然后根据需要采取行动)。这种思维方式有助于你理解这个例子中的情况:
1 | x <- reactiveVal(1) |
每当x
发生变化时,观察者就会被触发。观察者本身会调用observe()
来设置另一个观察者。因此,每次x
发生变化时,都会获得另一个观察者,从而导致其值再次被打印。
作为一般规则,你应该只在服务器函数的顶层创建观察者或输出。如果你发现自己试图嵌套它们或在输出内部创建观察者,那么你应该坐下来,画出你想要创建的响应式图表的草图——几乎可以肯定存在更好的方法。在更复杂的应用程序中,这个错误可能更难直接发现,但你可以始终使用reactlog
:只需查找观察者(或输出)中意外的变化,然后追踪到是什么导致了这些变化。
15.4 代码隔离
为了结束本章,我将讨论两个重要的工具,用于精确控制响应式图表的失效方式和时间。在本节中,我将讨论isolate(),这是observeEvent()
和eventReactive()的底层工具,它允许你在不需要时避免创建响应式依赖。在下一节中,你将学习invalidateLater(),它允许你按计划生成响应式失效。
15.4.1 isolate()
观察者通常与响应式值结合使用,以便跟踪状态随时间的变化。例如,考虑以下代码,它跟踪x
发生变化的次数:
1 | r <- reactiveValues(count = 0, x = 1) |
如果你运行这段代码,你会立即陷入无限循环,因为观察者会对x
和count
产生响应式依赖;由于观察者修改了count
,它会立即重新运行。
幸运的是,Shiny提供了isolate()
来解决这个问题。此函数允许你访问响应式值或表达式的当前值,而不会产生对它的依赖:
1 | r <- reactiveValues(count = 0, x = 1) |
和observe()
一样,很多时候你不需要直接使用isolate()
,因为有两个有用的函数封装了最常见的用法:observeEvent()
和eventReactive()
。
15.4.2 observeEvent() 和 eventReactive()
当你看到上面的代码时,你可能会想起第3.6节,并好奇为什么我没有使用observeEvent()
:
1 | observeEvent(r$x, { |
实际上,我本可以使用它,因为observeEvent(x, y)
等价于observe({x; isolate(y)})
。它优雅地将你想要监听的内容与你想要执行的操作分离开来。而eventReactive()
则为响应式值执行类似的任务:eventReactive(x, y)
等价于reactive({x; isolate(y)})
。
observeEvent()
和eventReactive()
都有额外的参数,允许你控制它们的操作细节:
- 默认情况下,这两个函数都会忽略产生NULL的任何事件(或在操作按钮的特殊情况下为0)。使用
ignoreNULL = FALSE
来处理NULL值。 - 默认情况下,当你创建这两个函数时,它们都会运行一次。使用
ignoreInit = TRUE
来跳过这次运行。 - 仅对于
observeEvent()
,你可以使用once = TRUE
来使处理程序仅运行一次。
这些参数很少需要,但知道它们的存在是很有用的,这样你就可以在需要时从文档中查找详细信息。
15.4.3 练习
使用服务器函数完成下面的应用程序,该函数仅在按钮被按下时更新out
的值为x
的值。
(注意:由于这是一个练习,我不会直接给出完整的代码,但你可以按照以下思路编写:
- 在UI中定义一个动作按钮和一个用于显示结果的文本输出。
- 在服务器函数中,使用
observeEvent()
监听按钮点击事件。 - 在
observeEvent()
的回调函数中,更新文本输出的值。)
1 | ui <- fluidPage( |
15.5 定时失效
isolate()
减少了响应式图被失效的时间。而本节的主题 invalidateLater()
则做相反的事情:它允许你在数据没有变化时使响应式图失效。在 3.5.1 节中,你通过 reactiveTimer() 看到了它的一个例子,但现在是时候讨论它背后的底层工具了:invalidateLater()
。
invalidateLater(ms)
会导致任何响应式消费者在将来的某个时间点(ms
毫秒后)失效。这对于创建动画和连接到 Shiny 响应式框架之外可能随时间变化的数据源非常有用。例如,以下响应式表达式将每半秒自动生成 10 个新的随机正态分布数:
1 | x <- reactive({ |
而这个观察者将使用一个随机数增加累积和:
1 | sum <- reactiveVal(0) |
在后续部分中,你将学习如何使用 invalidateLater()
从磁盘读取变化的数据,如何避免 invalidateLater()
陷入无限循环,以及关于失效确切发生时间的偶尔重要的细节。
15.5.1 轮询
invalidateLater()
的一个有用应用是将 Shiny 连接到 R 外部变化的数据。例如,你可以使用以下响应式表达式每秒重新读取一次 CSV 文件:
1 | data <- reactive({ |
这会将变化的数据连接到 Shiny 的响应式图中,但它有一个严重的缺点:当你使响应式失效时,你也会使所有下游消费者失效,因此即使数据相同,所有下游工作也必须重做。
为了避免这个问题,Shiny 提供了 reactivePoll(),它接受两个函数:一个执行相对便宜的检查以查看数据是否已更改,另一个更昂贵的函数实际执行计算。我们可以使用 reactivePoll()
重写前面的响应式,如下所示。
1 | server <- function(input, output, session) { |
这里我们使用了 file.mtime(),它返回文件最后一次修改的时间,作为是否需要重新加载文件的廉价检查。
当文件变化时读取文件是一个常见任务,因此 Shiny 提供了一个更具体的辅助函数,它只需要文件名和读取函数:
1 | server <- function(input, output, session) { |
如果你需要从其他来源(例如数据库)读取变化的数据,你需要自己编写 reactivePoll()
代码。
15.5.2 长时间运行的响应式
如果你正在执行长时间的计算,你需要考虑一个重要问题:何时执行 invalidateLater()
?例如,考虑以下响应式:
1 | x <- reactive({ |
假设Shiny在0时刻开始反应式运行,它将在500毫秒时请求失效。反应式运行需要1000毫秒,所以现在时间到了1000毫秒,它立即失效并需要重新计算,这又会触发另一次失效:我们陷入了一个无限循环。
另一方面,如果你在末尾运行invalidateLater()
,它将在完成后500毫秒失效,所以反应式将每1500毫秒重新运行一次。
1 | x <- reactive({ |
这是选择invalidateLater()
而不是我们之前使用的更简单的reactiveTimer()
的主要原因:它让你对失效发生的确切时间有更大的控制权。
15.5.3 定时器准确性
invalidateLater()
中指定的毫秒数是一个礼貌的请求,而不是一个强制要求。当你请求失效发生时,R可能正在做其他事情,所以你的请求必须等待。这实际上意味着这个数值是一个最小值,失效可能会比你预期的时间更长。在大多数情况下,这并不重要,因为小的差异不太可能影响用户对应用程序的感知。然而,在许多小错误会累积的情况下,你应该计算实际经过的时间并使用它来调整你的计算。
例如,以下代码根据速度和经过的时间计算距离。而不是假设invalidateLater(100)
总是精确延迟100毫秒,我计算经过的时间并在计算位置时使用它。
1 | velocity <- 3 |
如果你不是在仔细做动画,那么请随意忽略invalidateLater()
中固有的变化。只需记住,它是一个礼貌的请求,而不是一个要求。
15.5.4 练习
为什么这个反应式表达式永远不会被执行?你的解释应该涉及反应式图和失效机制。
1 | server <- function(input, output, session) { |
如果你熟悉SQL,请使用reactivePoll()
来仅当新行被添加时才重新读取一个虚构的“Results”表。你可以假设Results表有一个时间戳字段,该字段包含记录被添加的日期和时间。
15.6 总结
在本章中,你深入了解了使Shiny工作的基石:反应式值、反应式表达式、观察者和定时评估。现在,我们将注意力转向反应式值和观察者的特定组合,这种组合使我们能够摆脱反应式图的一些约束(无论好坏)。
加关注
关注公众号“生信之巅”。
敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!