Shiny从入门到入定——15-Reactive building blocks
发表于:2024-09-26 | 分类: IT
字数统计: 4.5k | 阅读时长: 17分钟 | 阅读量:

15 反应式构建块

既然你已经理解了反应式图背后的理论,并且有了一些实践经验,那么现在是时候更详细地讨论反应式如何融入R编程语言中了。反应式编程有三个基本构建块:反应式值反应式表达式观察者。你已经看到了反应式值和表达式的大部分重要部分,所以本章将花更多时间讨论观察者和输出(你将了解到输出是一种特殊的观察者)。你还将学习控制反应式图的另外两个工具:隔离定时无效化

本章将再次使用反应式控制台,这样我们就可以直接在控制台中试验反应式,而无需每次都启动一个Shiny应用程序。

1
2
library(shiny)
reactiveConsole(TRUE)

15.1 反应式值

反应式值有两种类型:

它们在获取和设置值时的接口略有不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- reactiveVal(10)
x() # get
#> [1] 10
x(20) # set
x() # get
#> [1] 20

r <- reactiveValues(x = 10)
r$x # get
#> [1] 10
r$x <- 20 # set
r$x # get
#> [1] 20

不幸的是,这两个类似的对象有着截然不同的接口,但没有办法将它们标准化。然而,尽管它们看起来不同,但行为是相同的,所以你可以根据自己的语法偏好来选择使用哪一个。在本书中,我使用reactiveValues(),因为它的语法一目了然,但在我的代码中,我倾向于使用reactiveVal(),因为它的语法清楚地表明正在发生一些不寻常的事情。

重要的是要注意,这两种类型的反应式值都具有所谓的引用语义。大多数R对象在修改时具有复制语义,这意味着如果你将相同的值赋给两个名称,一旦你修改了其中一个,连接就会断开:

1
2
3
4
a1 <- a2 <- 10
a2 <- 20
a1 # 未改变
#> [1] 10

但反应式值则不是这样——它们总是保持对相同值的引用,因此修改任何副本都会修改所有值:

1
2
3
4
b1 <- b2 <- reactiveValues(x = 10)
b1$x <- 20
b2$x
#> [1] 20

我们将在第16章中讨论为什么你可能需要创建自己的反应式值。否则,你将遇到的大多数反应式值都将来自server函数的输入参数。这些与你自己创建的reactiveValues()略有不同,因为它们是只读的:你不能修改这些值,因为Shiny会根据浏览器中的用户操作自动更新它们。

15.1.1 练习

  • 这两个反应式值列表之间有什么区别?比较获取和设置单个反应式值的语法。

    1
    2
    l1 <- reactiveValues(a = 1, b = 2)
    l2 <- list(a = reactiveVal(1), b = reactiveVal(2))
  • 设计并执行一个小实验,以验证reactiveVal()也具有引用语义。

15.2 反应式表达式

回忆一下,反应式有两个重要特性:它是惰性的并且具有缓存。这意味着它只有在真正需要时才会工作,如果连续调用两次,它会返回之前的值。

我们还没有涵盖两个重要的细节:反应式表达式如何处理错误,以及为什么on.exit()在它们内部有效。

15.2.1 错误

反应式表达式以与缓存值完全相同的方式缓存错误。例如,考虑这个反应式:

1
2
3
r <- reactive(stop("Error occured at ", Sys.time(), call. = FALSE))
r()
#> Error: Error occured at 2022-08-23 23:10:12

如果我们等待一两秒钟,我们可以看到我们得到了与之前相同的错误:

1
2
3
Sys.sleep(2)
r()
#> Error: Error occured at 2022-08-23 23:10:12

在反应式图中,错误的处理方式与值相同:错误通过反应式图传播的方式与常规值完全相同。唯一的区别是当错误遇到输出或观察者时会发生什么:

  • 输出中的错误将在应用程序中显示。
  • 观察者中的错误将导致当前会话终止。如果你不希望发生这种情况,你需要将代码包装在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
    16
    ui <- 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
2
3
4
5
6
7
8
9
10
y <- reactiveVal(10)
observe({
message("`y` is ", y())
})
#> `y` is 10

y(5)
#> `y` is 5
y(4)
#> `y` is 4

在这本书中,我很少使用observe(),因为它是为用户友好的observeEvent()提供动力的底层工具。通常,你应该坚持使用observeEvent(),除非它无法实现你想要的功能。在本书中,我只会向你展示一个必须使用observe()的案例,即第16.3.3节。

observe()也为响应式输出提供动力。响应式输出是一种特殊的观察者,它们具有两个重要属性:

  • 当你将它们赋值给输出时,它们就会被定义,即output$text <- ...会创建观察者。

  • 它们具有一些有限的检测能力,可以检测到自己是否不可见(即它们位于非活动标签页中),因此不必重新计算。

重要的是要注意,observe()和响应式输出并不“执行”某些操作,而是“创建”某些东西(然后根据需要采取行动)。这种思维方式有助于你理解这个例子中的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- reactiveVal(1)
y <- observe({
x()
observe(print(x()))
})
#> [1] 1
x(2)
#> [1] 2
#> [1] 2
x(3)
#> [1] 3
#> [1] 3
#> [1] 3

每当x发生变化时,观察者就会被触发。观察者本身会调用observe()来设置另一个观察者。因此,每次x发生变化时,都会获得另一个观察者,从而导致其值再次被打印。

作为一般规则,你应该只在服务器函数的顶层创建观察者或输出。如果你发现自己试图嵌套它们或在输出内部创建观察者,那么你应该坐下来,画出你想要创建的响应式图表的草图——几乎可以肯定存在更好的方法。在更复杂的应用程序中,这个错误可能更难直接发现,但你可以始终使用reactlog:只需查找观察者(或输出)中意外的变化,然后追踪到是什么导致了这些变化。

15.4 代码隔离

为了结束本章,我将讨论两个重要的工具,用于精确控制响应式图表的失效方式和时间。在本节中,我将讨论isolate(),这是observeEvent()eventReactive()的底层工具,它允许你在不需要时避免创建响应式依赖。在下一节中,你将学习invalidateLater(),它允许你按计划生成响应式失效。

15.4.1 isolate()

观察者通常与响应式值结合使用,以便跟踪状态随时间的变化。例如,考虑以下代码,它跟踪x发生变化的次数:

1
2
3
4
5
r <- reactiveValues(count = 0, x = 1)
observe({
r$x
r$count <- r$count + 1
})

如果你运行这段代码,你会立即陷入无限循环,因为观察者会对xcount产生响应式依赖;由于观察者修改了count,它会立即重新运行。

幸运的是,Shiny提供了isolate()来解决这个问题。此函数允许你访问响应式值或表达式的当前值,而不会产生对它的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
r <- reactiveValues(count = 0, x = 1)
class(r)
#> [1] "rv_flush_on_write" "reactivevalues"
observe({
r$x
r$count <- isolate(r$count) + 1
})

r$x <- 1
r$x <- 2
r$count
#> [1] 2

r$x <- 3
r$count
#> [1] 3

observe()一样,很多时候你不需要直接使用isolate(),因为有两个有用的函数封装了最常见的用法:observeEvent()eventReactive()

15.4.2 observeEvent() 和 eventReactive()

当你看到上面的代码时,你可能会想起第3.6节,并好奇为什么我没有使用observeEvent()

1
2
3
observeEvent(r$x, { 
r$count <- r$count + 1
})

实际上,我本可以使用它,因为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的值。

(注意:由于这是一个练习,我不会直接给出完整的代码,但你可以按照以下思路编写:

  1. 在UI中定义一个动作按钮和一个用于显示结果的文本输出。
  2. 在服务器函数中,使用observeEvent()监听按钮点击事件。
  3. observeEvent()的回调函数中,更新文本输出的值。)
1
2
3
4
5
ui <- fluidPage(
numericInput("x", "x", value = 50, min = 0, max = 100),
actionButton("capture", "capture"),
textOutput("out")
)

15.5 定时失效

isolate() 减少了响应式图被失效的时间。而本节的主题 invalidateLater() 则做相反的事情:它允许你在数据没有变化时使响应式图失效。在 3.5.1 节中,你通过 reactiveTimer() 看到了它的一个例子,但现在是时候讨论它背后的底层工具了:invalidateLater()

invalidateLater(ms) 会导致任何响应式消费者在将来的某个时间点(ms 毫秒后)失效。这对于创建动画和连接到 Shiny 响应式框架之外可能随时间变化的数据源非常有用。例如,以下响应式表达式将每半秒自动生成 10 个新的随机正态分布数:

1
2
3
4
x <- reactive({
invalidateLater(500)
rnorm(10)
})

而这个观察者将使用一个随机数增加累积和:

1
2
3
4
5
sum <- reactiveVal(0)
observe({
invalidateLater(300)
sum(isolate(sum()) + runif(1))
})

在后续部分中,你将学习如何使用 invalidateLater() 从磁盘读取变化的数据,如何避免 invalidateLater() 陷入无限循环,以及关于失效确切发生时间的偶尔重要的细节。

15.5.1 轮询

invalidateLater() 的一个有用应用是将 Shiny 连接到 R 外部变化的数据。例如,你可以使用以下响应式表达式每秒重新读取一次 CSV 文件:

1
2
3
4
data <- reactive({
on.exit(invalidateLater(1000))
read.csv("data.csv")
})

这会将变化的数据连接到 Shiny 的响应式图中,但它有一个严重的缺点:当你使响应式失效时,你也会使所有下游消费者失效,因此即使数据相同,所有下游工作也必须重做。

为了避免这个问题,Shiny 提供了 reactivePoll(),它接受两个函数:一个执行相对便宜的检查以查看数据是否已更改,另一个更昂贵的函数实际执行计算。我们可以使用 reactivePoll() 重写前面的响应式,如下所示。

1
2
3
4
5
6
server <- function(input, output, session) {
data <- reactivePoll(1000, session,
function() file.mtime("data.csv"),
function() read.csv("data.csv")
)
}

这里我们使用了 file.mtime(),它返回文件最后一次修改的时间,作为是否需要重新加载文件的廉价检查。

当文件变化时读取文件是一个常见任务,因此 Shiny 提供了一个更具体的辅助函数,它只需要文件名和读取函数:

1
2
3
server <- function(input, output, session) {
data <- reactiveFileReader(1000, session, "data.csv", read.csv)
}

如果你需要从其他来源(例如数据库)读取变化的数据,你需要自己编写 reactivePoll() 代码。

15.5.2 长时间运行的响应式

如果你正在执行长时间的计算,你需要考虑一个重要问题:何时执行 invalidateLater()?例如,考虑以下响应式:

1
2
3
4
5
x <- reactive({
invalidateLater(500)
Sys.sleep(1)
10
})

假设Shiny在0时刻开始反应式运行,它将在500毫秒时请求失效。反应式运行需要1000毫秒,所以现在时间到了1000毫秒,它立即失效并需要重新计算,这又会触发另一次失效:我们陷入了一个无限循环。

另一方面,如果你在末尾运行invalidateLater(),它将在完成后500毫秒失效,所以反应式将每1500毫秒重新运行一次。

1
2
3
4
5
x <- reactive({
on.exit(invalidateLater(500), add = TRUE)
Sys.sleep(1)
10
})

这是选择invalidateLater()而不是我们之前使用的更简单的reactiveTimer()的主要原因:它让你对失效发生的确切时间有更大的控制权。

15.5.3 定时器准确性

invalidateLater()中指定的毫秒数是一个礼貌的请求,而不是一个强制要求。当你请求失效发生时,R可能正在做其他事情,所以你的请求必须等待。这实际上意味着这个数值是一个最小值,失效可能会比你预期的时间更长。在大多数情况下,这并不重要,因为小的差异不太可能影响用户对应用程序的感知。然而,在许多小错误会累积的情况下,你应该计算实际经过的时间并使用它来调整你的计算。

例如,以下代码根据速度和经过的时间计算距离。而不是假设invalidateLater(100)总是精确延迟100毫秒,我计算经过的时间并在计算位置时使用它。

1
2
3
4
5
6
7
8
9
10
11
12
velocity <- 3
r <- reactiveValues(distance = 1)

last <- proc.time()[[3]]
observe({
cur <- proc.time()[[3]]
time <- last - cur
last <<- cur

r$distance <- isolate(r$distance) + velocity * time
invalidateLater(100)
})

如果你不是在仔细做动画,那么请随意忽略invalidateLater()中固有的变化。只需记住,它是一个礼貌的请求,而不是一个要求。

15.5.4 练习

为什么这个反应式表达式永远不会被执行?你的解释应该涉及反应式图和失效机制。

1
2
3
4
5
6
server <- function(input, output, session) {
x <- reactive({
invalidateLater(500)
rnorm(10)
})
}

如果你熟悉SQL,请使用reactivePoll()来仅当新行被添加时才重新读取一个虚构的“Results”表。你可以假设Results表有一个时间戳字段,该字段包含记录被添加的日期和时间。

15.6 总结

在本章中,你深入了解了使Shiny工作的基石:反应式值、反应式表达式、观察者和定时评估。现在,我们将注意力转向反应式值和观察者的特定组合,这种组合使我们能够摆脱反应式图的一些约束(无论好坏)。

加关注

关注公众号“生信之巅”。

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

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

上一篇:
Shiny从入门到入定——16-Escaping the graph
下一篇:
Shiny从入门到入定——14-The reactive graph