反应图
14.1 引言
为了理解反应式计算,首先需要理解反应图。在本章中,我们将深入探讨反应图的细节,尤其关注事情发生的精确顺序。特别是,你将了解无效化的重要性,这是一个确保Shiny进行最少工作的关键过程。你还将了解reactlog
包,它可以自动为实际应用程序绘制反应图。
如果你有一段时间没有查看第3章了,我强烈建议你在继续之前重新熟悉一下。它为我们将在这里更详细探讨的概念奠定了基础。
14.2 反应式执行的逐步介绍
为了解释反应式执行的过程,我们将使用图14.1中显示的图形。它包含三个反应式输入、三个反应式表达式和三个输出。请记住,反应式输入和表达式统称为反应式生产者
;反应式表达式和输出是反应式消费者
。
组件之间的连接是有方向的,箭头表示反应的方向。这个方向可能会让你感到惊讶,因为很容易想到一个消费者依赖于一个或多个生产者。然而,很快你就会看到,反应流更准确地被建模为相反的方向。
底层应用程序并不重要,但如果你需要一些具体的东西来帮助你理解,你可以假设它源自这个不太有用的应用程序。
1 | ui <- fluidPage( |
14.3 会话开始
图14.2展示了应用程序启动后且服务器函数首次执行完毕时的反应图。
该图中有三个重要信息:
- 元素之间没有连接,因为Shiny没有反应式之间关系的先验知识。
- 所有反应式表达式和输出都处于起始状态,即无效(灰色),意味着它们尚未运行。
- 反应式输入已就绪(绿色),表明它们的值可用于计算。
14.3.1 执行开始
现在我们开始执行阶段,如图14.3所示。
在这个阶段,Shiny选择一个无效的输出并开始执行(橙色)。你可能会好奇Shiny是如何决定执行哪个无效输出的。简而言之,你应该假设它是随机的:你的观察者和输出不应该关心它们执行的顺序,因为它们被设计为独立运行。
14.3.2 读取反应式表达式
执行输出可能需要从反应式中获取值,如图14.4所示。
读取反应式会以两种方式改变图形:
- 反应式表达式也需要开始计算其值(变为橙色)。请注意,输出仍在计算中:它正在等待反应式表达式返回其值,以便自己的执行可以继续进行,就像在R中进行常规函数调用一样。
- Shiny在输出和反应式表达式之间建立关系(即我们绘制一个箭头)。箭头的方向很重要:表达式记录它被输出使用;输出不记录它使用表达式。这是一个微妙的区别,但当你学习无效化时会更加清楚其重要性。
14.3.3 读取输入
这个特定的反应式表达式碰巧读取了一个反应式输入。同样,建立了依赖/被依赖的关系,所以在图14.5中我们添加了另一个箭头。
与反应式表达式和输出不同,反应式输入没有要执行的内容,因此可以立即返回。
14.3.4 反应式表达式完成
在我们的示例中,反应式表达式读取了另一个反应式表达式,而后者又读取了另一个输入。我们将跳过对这些步骤的逐一描述,因为它们是我们已经描述过的内容的重复,并直接跳转到图14.6。
现在,反应式表达式已完成执行并变为绿色,表示它已就绪。它缓存了结果,因此除非其输入发生变化,否则不需要重新计算。
14.3.5 输出完成
现在,反应式表达式已返回其值,输出可以完成执行,并变为绿色,如图14.7所示。
14.3.6 执行下一个输出
现在第一个输出已完成,Shiny 会选择另一个来执行。这个输出会变成橙色(如图14.8所示),并开始从响应式生产者读取值。
已完成的响应式对象可以立即返回它们的值;无效的响应式对象将启动它们自己的执行图。这个循环将重复进行,直到每个无效的输出进入完成(绿色)状态。
14.3.7 执行完成,输出刷新
现在所有的输出都已完成执行并处于空闲状态(如图14.9所示)。
这一轮响应式执行已完成,除非系统受到某种外部作用(例如,Shiny 应用程序的用户在用户界面中移动滑块),否则不会进行更多工作。从响应式的角度来看,这个会话现在处于静止状态。
让我们先暂停一下,思考一下我们所做的事情。我们读取了一些输入,计算了一些值,并生成了一些输出。但更重要的是,我们还发现了响应式对象之间的关系。当一个响应式输入发生变化时,我们知道需要更新哪些响应式对象。
14.4 输入发生变化
上一步结束时,我们的Shiny会话处于完全空闲状态。现在假设应用程序的用户更改了滑块的值。这会导致浏览器向服务器函数发送一条消息,指示Shiny更新相应的响应式输入。这将启动一个无效化阶段,该阶段包含三个部分:无效化输入
、通知依赖项
,然后移除现有的连接
。
14.4.1 无效化输入
无效化阶段从已更改的输入/值开始,我们将其填充为灰色,这是我们通常用于无效化的颜色(如图14.10所示)。
14.4.2 通知依赖项
现在,我们按照之前绘制的箭头方向,将每个节点着色为灰色,并将我们跟随的箭头着色为浅灰色。这产生了图14.11。
14.4.3 移除关系
接下来,每个无效的响应式表达式和输出都会“擦除”所有进入和离开它的箭头,从而得到图14.12,并完成无效化阶段。
从一个节点发出的箭头是一次性通知,它们将在下一次值发生变化时触发。现在它们已经触发了,完成了它们的使命,我们可以将它们擦除。
为什么我们要擦除进入无效节点的箭头,即使它们来自的节点并未无效化,这一点可能不太明显。虽然这些箭头代表尚未触发的通知,但无效的节点不再关心它们:响应式消费者只关心通知以使自己无效化,而这已经发生了。
我们如此重视这些关系,但现在却把它们抛弃了,这似乎有点反常!但这是Shiny响应式编程模型的关键部分:虽然这些特定的箭头很重要,但它们现在已经过时了。确保我们的图保持准确性的唯一方法是,在它们变得陈旧时擦除箭头,并让Shiny在重新执行时重新发现这些节点周围的关系。我们将在第14.5节中再次讨论这个重要话题。
14.4.4 重新执行
现在,我们处于与执行第二个输出时非常相似的情况,此时既有有效也有无效的反应式。是时候做我们之前做过的事情了:执行无效的输出,每次执行一个,从图14.13开始。
同样,我不会展示具体细节,但最终结果将是一个所有节点都被标记为绿色的静止反应式图。这个过程的巧妙之处在于,Shiny只做了最小量的工作——我们只做了更新那些实际受输入变化影响的输出所需的工作。
14.4.5 练习
- 为以下服务器函数绘制反应式图,并解释为什么反应式没有运行。
1 | server <- function(input, output, session) { |
- 以下反应式图通过使用Sys.sleep()模拟长时间运行的计算:
1 | x1 <- reactiveVal(1) |
如果x1
改变,图重新计算需要多长时间?x2
或x3
呢?
- 如果你尝试创建一个带有循环的反应式图,会发生什么?
1 | x <- reactiveVal(1) |
14.5 动态性
在第14.4.3节中,你了解到Shiny“忘记”了它在记录时花费大量精力建立的反应式组件之间的连接。这使得Shiny的反应式具有动态性,因为它可以在你的应用程序运行时发生变化。这种动态性非常重要,我想用一个简单的例子来强调它:
1 | ui <- fluidPage( |
你可能会期望反应式图看起来如图14.14所示。
但是,由于Shiny在输出被标记为无效后会动态地重建图,因此它实际上看起来会像图14.15中的任何一个图,这取决于input$choice
的值。这确保了当输入被标记为无效时,Shiny会做最少量的工作。在这种情况下,如果input$choice
被设置为“b”,那么input$a
的值不会影响output$out
,因此没有必要重新计算它。
值得注意的是(正如Yindeng Jiang在他们的博客中所做的那样),一个微小的更改将导致输出始终依赖于a
和b
:
1 | output$out <- renderText({ |
这对正常的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_height
、clientData$output_x_width
和clientData$pixelratio
)没有出现在源代码中。这些存在是因为图表对输出的大小有隐式依赖;每当输出大小改变时,图表都需要重新绘制。
请注意,虽然反应式输入和输出有名称,但反应式表达式和观察者没有,因此它们用其内容标记。为了更容易理解,你可能想使用reactive()和observe()的label
参数,这样它们就会出现在反应日志中。你可以使用表情符号来使特别重要的反应式在视觉上脱颖而出。
14.7 总结
在本章中,你精确地学习了反应式图是如何工作的。特别是,你首次了解了无效化阶段,它不会立即导致重新计算,而是将反应式消费者标记为无效,以便在需要时重新计算。无效化周期也很重要,因为它清除了之前发现的依赖项,以便它们可以自动重新发现,从而使反应式图具有动态性。
现在你已经掌握了全局,下一章将提供一些关于支持反应式值、表达式和输出的底层数据结构的额外细节,并讨论与定时无效化相关的概念。
加关注
关注公众号“生信之巅”。
敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!