Shiny从入门到入定——14-The reactive graph
发表于:2024-09-26 | 分类: IT
字数统计: 3.6k | 阅读时长: 13分钟 | 阅读量:

反应图

14.1 引言

为了理解反应式计算,首先需要理解反应图。在本章中,我们将深入探讨反应图的细节,尤其关注事情发生的精确顺序。特别是,你将了解无效化的重要性,这是一个确保Shiny进行最少工作的关键过程。你还将了解reactlog包,它可以自动为实际应用程序绘制反应图。

如果你有一段时间没有查看第3章了,我强烈建议你在继续之前重新熟悉一下。它为我们将在这里更详细探讨的概念奠定了基础。

14.2 反应式执行的逐步介绍

为了解释反应式执行的过程,我们将使用图14.1中显示的图形。它包含三个反应式输入、三个反应式表达式和三个输出。请记住,反应式输入和表达式统称为反应式生产者;反应式表达式和输出是反应式消费者

图14.1 包含三个输入、三个反应式表达式和三个输出的虚构应用程序的完整反应图

组件之间的连接是有方向的,箭头表示反应的方向。这个方向可能会让你感到惊讶,因为很容易想到一个消费者依赖于一个或多个生产者。然而,很快你就会看到,反应流更准确地被建模为相反的方向。

底层应用程序并不重要,但如果你需要一些具体的东西来帮助你理解,你可以假设它源自这个不太有用的应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ui <- fluidPage(
numericInput("a", "a", value = 10),
numericInput("b", "b", value = 1),
numericInput("c", "c", value = 1),
plotOutput("x"),
tableOutput("y"),
textOutput("z")
)

server <- function(input, output, session) {
rng <- reactive(input$a * 2)
smp <- reactive(sample(rng(), input$b, replace = TRUE))
bc <- reactive(input$b * input$c)

output$x <- renderPlot(hist(smp()))
output$y <- renderTable(max(smp()))
output$z <- renderText(bc())
}

14.3 会话开始

图14.2展示了应用程序启动后且服务器函数首次执行完毕时的反应图。

图14.2 应用程序加载后的初始状态。对象之间没有连接,所有反应式表达式都被标记为无效(灰色)。有六个反应式消费者和六个反应式生产者

该图中有三个重要信息:

  • 元素之间没有连接,因为Shiny没有反应式之间关系的先验知识。
  • 所有反应式表达式和输出都处于起始状态,即无效(灰色),意味着它们尚未运行。
  • 反应式输入已就绪(绿色),表明它们的值可用于计算。

14.3.1 执行开始

现在我们开始执行阶段,如图14.3所示。

图14.3 Shiny开始执行任意的观察者/输出,标记为橙色

在这个阶段,Shiny选择一个无效的输出并开始执行(橙色)。你可能会好奇Shiny是如何决定执行哪个无效输出的。简而言之,你应该假设它是随机的:你的观察者和输出不应该关心它们执行的顺序,因为它们被设计为独立运行。

14.3.2 读取反应式表达式

执行输出可能需要从反应式中获取值,如图14.4所示。

图14.4 输出需要反应式表达式的值,因此它开始执行该表达式

读取反应式会以两种方式改变图形:

  • 反应式表达式也需要开始计算其值(变为橙色)。请注意,输出仍在计算中:它正在等待反应式表达式返回其值,以便自己的执行可以继续进行,就像在R中进行常规函数调用一样。
  • Shiny在输出和反应式表达式之间建立关系(即我们绘制一个箭头)。箭头的方向很重要:表达式记录它被输出使用;输出不记录它使用表达式。这是一个微妙的区别,但当你学习无效化时会更加清楚其重要性。

14.3.3 读取输入

这个特定的反应式表达式碰巧读取了一个反应式输入。同样,建立了依赖/被依赖的关系,所以在图14.5中我们添加了另一个箭头。

图14.5 反应式表达式也从反应式值中读取,因此我们添加了另一个箭头

与反应式表达式和输出不同,反应式输入没有要执行的内容,因此可以立即返回。

14.3.4 反应式表达式完成

在我们的示例中,反应式表达式读取了另一个反应式表达式,而后者又读取了另一个输入。我们将跳过对这些步骤的逐一描述,因为它们是我们已经描述过的内容的重复,并直接跳转到图14.6。

图14.6 反应式表达式已完成计算,因此变为绿色

现在,反应式表达式已完成执行并变为绿色,表示它已就绪。它缓存了结果,因此除非其输入发生变化,否则不需要重新计算。

14.3.5 输出完成

现在,反应式表达式已返回其值,输出可以完成执行,并变为绿色,如图14.7所示。

图14.7 输出已完成执行并变为绿色

14.3.6 执行下一个输出

现在第一个输出已完成,Shiny 会选择另一个来执行。这个输出会变成橙色(如图14.8所示),并开始从响应式生产者读取值。

图14.8 下一个输出开始计算,变为橙色

已完成的响应式对象可以立即返回它们的值;无效的响应式对象将启动它们自己的执行图。这个循环将重复进行,直到每个无效的输出进入完成(绿色)状态。

14.3.7 执行完成,输出刷新

现在所有的输出都已完成执行并处于空闲状态(如图14.9所示)。

图14.9 所有输出和响应式表达式都已完成并变为绿色

这一轮响应式执行已完成,除非系统受到某种外部作用(例如,Shiny 应用程序的用户在用户界面中移动滑块),否则不会进行更多工作。从响应式的角度来看,这个会话现在处于静止状态。

让我们先暂停一下,思考一下我们所做的事情。我们读取了一些输入,计算了一些值,并生成了一些输出。但更重要的是,我们还发现了响应式对象之间的关系。当一个响应式输入发生变化时,我们知道需要更新哪些响应式对象。

14.4 输入发生变化

上一步结束时,我们的Shiny会话处于完全空闲状态。现在假设应用程序的用户更改了滑块的值。这会导致浏览器向服务器函数发送一条消息,指示Shiny更新相应的响应式输入。这将启动一个无效化阶段,该阶段包含三个部分:无效化输入通知依赖项,然后移除现有的连接

14.4.1 无效化输入

无效化阶段从已更改的输入/值开始,我们将其填充为灰色,这是我们通常用于无效化的颜色(如图14.10所示)。

图14.10 用户与应用程序交互,使一个输入无效

14.4.2 通知依赖项

现在,我们按照之前绘制的箭头方向,将每个节点着色为灰色,并将我们跟随的箭头着色为浅灰色。这产生了图14.11。

图14.11 无效化从输入开始,按照从左到右的每个箭头流动。在无效化过程中,Shiny 跟随的箭头被着色为较浅的灰色

14.4.3 移除关系

接下来,每个无效的响应式表达式和输出都会“擦除”所有进入和离开它的箭头,从而得到图14.12,并完成无效化阶段。

图14.12:无效的节点会忘记它们之前的所有关系,以便重新发现它们

从一个节点发出的箭头是一次性通知,它们将在下一次值发生变化时触发。现在它们已经触发了,完成了它们的使命,我们可以将它们擦除。

为什么我们要擦除进入无效节点的箭头,即使它们来自的节点并未无效化,这一点可能不太明显。虽然这些箭头代表尚未触发的通知,但无效的节点不再关心它们:响应式消费者只关心通知以使自己无效化,而这已经发生了。

我们如此重视这些关系,但现在却把它们抛弃了,这似乎有点反常!但这是Shiny响应式编程模型的关键部分:虽然这些特定的箭头很重要,但它们现在已经过时了。确保我们的图保持准确性的唯一方法是,在它们变得陈旧时擦除箭头,并让Shiny在重新执行时重新发现这些节点周围的关系。我们将在第14.5节中再次讨论这个重要话题。

14.4.4 重新执行

现在,我们处于与执行第二个输出时非常相似的情况,此时既有有效也有无效的反应式。是时候做我们之前做过的事情了:执行无效的输出,每次执行一个,从图14.13开始。

图14.13 现在,重新执行的过程与执行过程相同,但因为我们不是从零开始,所以要做的工作就少了很多

同样,我不会展示具体细节,但最终结果将是一个所有节点都被标记为绿色的静止反应式图。这个过程的巧妙之处在于,Shiny只做了最小量的工作——我们只做了更新那些实际受输入变化影响的输出所需的工作。

14.4.5 练习

  1. 为以下服务器函数绘制反应式图,并解释为什么反应式没有运行。
1
2
3
4
5
server <- function(input, output, session) {
sum <- reactive(input$x + input$y + input$z)
prod <- reactive(input$x * input$y * input$z)
division <- reactive(prod() / sum())
}
  1. 以下反应式图通过使用Sys.sleep()模拟长时间运行的计算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
x1 <- reactiveVal(1)
x2 <- reactiveVal(2)
x3 <- reactiveVal(3)

y1 <- reactive({
Sys.sleep(1)
x1()
})
y2 <- reactive({
Sys.sleep(1)
x2()
})
y3 <- reactive({
Sys.sleep(1)
x2() + x3() + y2() + y2()
})

observe({
print(y1())
print(y2())
print(y3())
})

如果x1改变,图重新计算需要多长时间?x2x3呢?

  1. 如果你尝试创建一个带有循环的反应式图,会发生什么?
1
2
3
x <- reactiveVal(1)
y <- reactive(x + y())
y()

14.5 动态性

在第14.4.3节中,你了解到Shiny“忘记”了它在记录时花费大量精力建立的反应式组件之间的连接。这使得Shiny的反应式具有动态性,因为它可以在你的应用程序运行时发生变化。这种动态性非常重要,我想用一个简单的例子来强调它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ui <- fluidPage(
selectInput("choice", "A or B?", c("a", "b")),
numericInput("a", "a", 0),
numericInput("b", "b", 10),
textOutput("out")
)

server <- function(input, output, session) {
output$out <- renderText({
if (input$choice == "a") {
input$a
} else {
input$b
}
})
}

你可能会期望反应式图看起来如图14.14所示。

图14.14 如果Shiny静态地分析反应式,那么反应式图总是会将`choice`、`a`和`b`连接到`out`

但是,由于Shiny在输出被标记为无效后会动态地重建图,因此它实际上看起来会像图14.15中的任何一个图,这取决于input$choice的值。这确保了当输入被标记为无效时,Shiny会做最少量的工作。在这种情况下,如果input$choice被设置为“b”,那么input$a的值不会影响output$out,因此没有必要重新计算它。

图14.15 但是,Shiny的反应式图是动态的,所以图要么将`out`连接到`choice`和`a`(左图),要么连接到`choice`和`b`(右图)

值得注意的是(正如Yindeng Jiang在他们的博客中所做的那样),一个微小的更改将导致输出始终依赖于ab

1
2
3
4
5
6
7
8
9
10
output$out <- renderText({
a <- input$a
b <- input$b

if (input$choice == "a") {
a
} else {
b
}
})

这对正常的R代码输出没有影响,但在这里却有所不同,因为反应式依赖是在你从输入中读取值时建立的,而不是在你使用该值时建立的。

14.6 reactlog包

手动绘制反应式图是一种强大的技术,可以帮助你理解简单的应用程序,并构建出反应式编程的准确心理模型。但对于拥有许多动态部分的实际应用程序来说,这样做很痛苦。如果我们能使用Shiny对其了解的内容自动绘制图形,那岂不是太好了?这就是reactlog包的工作,它生成所谓的反应日志(reactlog),展示了反应式图如何随时间演变。

要查看反应日志,你首先需要安装reactlog包,使用reactlog::reactlog_enable()启用它,然后启动你的应用程序。你有两个选项:

  • 在应用程序运行时,按Cmd + F3(在Windows上是Ctrl + F3),以显示到目前为止生成的反应日志。

  • 在应用程序关闭后,运行shiny::reactlogShow()以查看整个会话的日志。

reactlog使用与本章相同的图形约定,因此你应该会立即感到熟悉。最大的不同是,reactlog绘制了所有依赖项,即使它们当前未被使用,以保持自动布局的稳定性。当前不活跃的连接(但过去或将来会活跃)被绘制为细虚线。

图14.16展示了reactlog为我们上面使用的应用程序绘制的反应式图。截图中有一个惊喜:有三个额外的反应式输入(clientData$output_x_heightclientData$output_x_widthclientData$pixelratio)没有出现在源代码中。这些存在是因为图表对输出的大小有隐式依赖;每当输出大小改变时,图表都需要重新绘制。

图14.16 由reactlog绘制的假设应用程序的反应式图

请注意,虽然反应式输入和输出有名称,但反应式表达式和观察者没有,因此它们用其内容标记。为了更容易理解,你可能想使用reactive()observe()label参数,这样它们就会出现在反应日志中。你可以使用表情符号来使特别重要的反应式在视觉上脱颖而出。

14.7 总结

在本章中,你精确地学习了反应式图是如何工作的。特别是,你首次了解了无效化阶段,它不会立即导致重新计算,而是将反应式消费者标记为无效,以便在需要时重新计算。无效化周期也很重要,因为它清除了之前发现的依赖项,以便它们可以自动重新发现,从而使反应式图具有动态性。

现在你已经掌握了全局,下一章将提供一些关于支持反应式值、表达式和输出的底层数据结构的额外细节,并讨论与定时无效化相关的概念。

加关注

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

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

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

上一篇:
Shiny从入门到入定——15-Reactive building blocks
下一篇:
Scikit-learn机器学习实战-PCA