Shiny从入门到入定——13-Why reactivity?
发表于:2024-04-27 | 分类: IT
字数统计: 3k | 阅读时长: 11分钟 | 阅读量:

介绍

现在你已经掌握了一系列有用的技巧,这些技巧赋予了你创建各种有用应用程序的能力。接下来,我们将把注意力转向Shiny魔力背后的反应性理论:

  • 在第13章中,你将了解为什么需要反应性编程模型,以及关于R之外的反应性编程历史的一些知识。

  • 在第14章中,你将学习反应图的全部细节,它将决定反应组件何时更新。

  • 在第15章中,你将了解底层的构建块,尤其是观察者和定时失效。

  • 在第16章中,你将学习如何使用reactiveVal()observe()来突破反应图的限制。

当然,对于日常开发Shiny应用程序,你并不需要理解所有这些细节。但是,提高你的理解将有助于你从一开始就编写正确的应用程序,当某些行为不符合预期时,你可以更快地缩小问题的范围。

13 为什么需要反应性?

13.1 引言

Shiny给人的初步印象通常是“神奇”。当你刚开始使用时,这种神奇感非常棒,因为它能让你迅速构建出简单的应用程序。但是,软件中的神奇通常会导致幻灭:如果没有一个稳固的思维模型,当你尝试超出其演示和示例的边界时,要预测软件将如何表现就极其困难。而当事情不按你预期的方式发展时,调试几乎是不可能的。

幸运的是,Shiny的“神奇”是有益的。正如汤姆·戴尔(Tom Dale)谈到他的Ember.js JavaScript框架时所说:“我们做了很多神奇的事情,但这是有益的神奇,这意味着它可以分解为合理的原始元素。”这是Shiny团队希望Shiny所具备的品质,尤其是在反应性编程方面。当你剥去反应性编程的层层外衣时,你不会发现一堆启发式方法、特殊情况和黑客攻击;相反,你会发现一个巧妙但最终相当直接的机制。一旦你形成了对反应性的准确思维模型,你就会明白Shiny并没有耍花招:神奇来自于以一致的方式组合简单概念。

在本章中,我们将尝试在没有反应性编程的情况下进行开发,以此说明反应性编程的必要性,然后简要介绍与Shiny相关的反应性编程的历史。

13.2 为什么我们需要反应性编程?

反应性编程是一种编程风格,它关注随时间变化的值,以及依赖于这些值的计算和操作。反应性对于Shiny应用程序至关重要,因为它们具有交互性:用户改变输入控件(拖动滑块、在文本框中输入内容、勾选复选框等),这会触发服务器上的逻辑运行(读取CSV文件、筛选数据、拟合模型等),最终导致输出更新(图表重绘、表格更新等)。这与大多数R代码有很大的不同,后者通常处理相对静态的数据。

为了让Shiny应用程序发挥最大效用,我们需要确保反应式表达式和输出仅在它们的输入发生变化时更新。我们希望输出与输入保持同步,同时确保不会进行比必要更多的工作。为了了解反应性为何如此有用,我们将尝试解决一个不使用反应性的简单问题。

13.2.1 为什么不能使用变量?

从某种意义上说,你已经知道如何处理“随时间变化的值”:它们被称为“变量”。在R中,变量代表值,它们可以随时间变化,但它们并不是为帮助你处理变化而设计的。以下是一个简单的将温度从摄氏度转换为华氏度的例子:

1
2
3
4
temp_c <- 10
temp_f <- (temp_c * 9 / 5) + 32
temp_f
#> [1] 50

到目前为止还不错:temp_c变量有值10,temp_f变量有值50,我们可以改变temp_c的值:

1
temp_c <- 30

但是改变temp_c的值并不会影响temp_f的值:

1
2
temp_f
#> [1] 50

变量可以随时间变化,但它们不会自动变化。

13.2.2 使用函数怎么样?

你也可以通过函数来解决这个问题:

1
2
3
4
5
6
7
8
temp_c <- 10
temp_f <- function() {
message("Converting")
(temp_c * 9 / 5) + 32
}
temp_f()
#> Converting
#> [1] 50

(这是一个有点奇怪的函数,因为它没有参数,而是从其封闭环境40中访问temp_c,但它是一个完全有效的R代码。)

这解决了反应性试图解决的第一个问题:每当你访问temp_f()时,你都会得到最新的计算结果:

1
2
3
4
temp_c <- -3
temp_f()
#> Converting
#> [1] 26.6

然而,它并没有减少计算量。每次你调用temp_f()时,它都会重新计算,即使temp_c没有改变:

1
2
3
temp_f() 
#> Converting
#> [1] 26.6

在这个简单的例子中,计算成本不高,所以重复计算没什么大不了的,但这仍然是多余的:如果输入没有改变,我们为什么要重新计算输出呢?

13.2.3 事件驱动编程

由于变量和函数都不适用,我们需要创建一些新的东西。在过去的几十年里,我们可能会直接跳到事件驱动编程。事件驱动编程是一个吸引人的简单范式:你注册回调函数,这些函数会在事件发生时执行。

我们可以使用R6实现一个非常简单的事件驱动工具包,如下例所示。在这里,我们定义了一个DynamicValue,它有三个重要的方法:get()set()用于访问和更改基础值,onUpdate()用于注册在值被修改时要运行的代码。如果你不熟悉R6,不用担心细节,而是关注下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DynamicValue <- R6::R6Class("DynamicValue", list(
value = NULL,
on_update = NULL,

get = function() self$value,

set = function(value) {
self$value <- value
if (!is.null(self$on_update))
self$on_update(value)
invisible(self)
},

onUpdate = function(on_update) {
self$on_update <- on_update
invisible(self)
}
))

因此,如果Shiny在五年前就被发明出来,它可能看起来会更像这样,其中temp_c使用<<-来在需要时更新temp_f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
temp_c <- DynamicValue$new()
temp_c$onUpdate(function(value) {
message("Converting")
temp_f <<- (value * 9 / 5) + 32
})

temp_c$set(10)
#> Converting
temp_f
#> [1] 50

temp_c$set(-3)
#> Converting
temp_f
#> [1] 26.6

事件驱动编程解决了不必要的计算问题,但它又产生了一个新问题:你必须仔细跟踪哪些输入会影响哪些计算。不久之后,你就会在正确性(即在任何内容发生更改时都更新所有内容)和性能(即尝试仅更新必要的部分,并希望你没有遗漏任何边缘情况)之间权衡取舍,因为同时做到这两点非常困难。

13.2.4 反应性编程

反应性编程通过将上述解决方案的特性结合起来,优雅地解决了这两个问题。现在,我们可以向您展示一些真正的Shiny代码,它使用了一个特殊的Shiny模式,即reactiveConsole(TRUE),这使得我们可以直接在控制台中实验反应性。

1
2
library(shiny)
reactiveConsole(TRUE)

与事件驱动编程一样,我们需要某种方式来指示我们有一个特殊类型的变量。在Shiny中,我们使用reactiveVal()创建一个反reactive value。反应式值有特殊的语法来获取其值(像调用无参数函数一样调用它)和设置其值(像调用单参数函数一样调用它来设置其值)。

1
2
3
4
5
6
temp_c <- reactiveVal(10) # create
temp_c() # get
#> [1] 10
temp_c(20) # set
temp_c() # get
#> [1] 20

现在我们可以创建一个依赖于这个值的反应式表达式:

1
2
3
4
5
6
7
temp_f <- reactive({
message("Converting")
(temp_c() * 9 / 5) + 32
})
temp_f()
#> Converting
#> [1] 68

在创建应用程序时,你已经了解到反应式表达式会自动跟踪其所有依赖项。因此,如果之后temp_c发生变化,temp_f会自动更新:

1
2
3
4
5
temp_c(-3)
temp_c(-10)
temp_f()
#> Converting
#> [1] 14

但如果temp_c()没有变化,那么temp_f()就不需要重新计算,可以直接从缓存中获取:

1
2
temp_f()
#> [1] 14

反应式表达式有两个重要特性:

  • 它是惰性的:在被调用之前,它不会进行任何工作。

  • 它是缓存的:在第二次及后续调用时,它不会进行任何工作,因为它缓存了之前的结果。

我们将在第14章中再次讨论这些重要特性。

13.3 反应式编程简史

如果你想了解更多其他语言的反应式编程知识,那么了解其发展历史可能会有所帮助。你可以在VisiCalc(第一个电子表格软件)中看到反应式编程的萌芽,它起源于40多年前:

我想象了一个神奇的黑板,当你擦去一个数字并写入新的东西时,其他所有的数字都会自动改变,就像用数字进行的文字处理一样。

——Dan Bricklin

电子表格与反应式编程密切相关:你使用公式声明单元格之间的关系,当一个单元格发生变化时,其所有依赖项都会自动更新。因此,你可能已经在不知不觉中进行了大量反应式编程!

虽然反应性的概念已经存在很长时间了,但直到20世纪90年代末,学术界才开始对其进行认真研究。反应式编程的研究始于FRAN(functional reactive animation,功能性反应式动画),这是一种将随时间变化和用户输入融入功能编程语言中的新颖系统。这催生了一系列丰富的文献,但对编程实践的影响却很小。

直到2010年代,反应式编程才通过JavaScript UI框架的快节奏世界进入主流编程领域。像KnockoutEmberMeteor(Joe Cheng’s personal inspiration for Shiny)这样的开创性框架表明,反应式编程可以极大地简化UI编程。短短几年内,反应式编程通过ReactVue.jsAngular等广受欢迎的框架主导了网络编程,这些框架要么本质上具有反应性,要么设计为与反应式后端协同工作。

值得一提的是,“反应式编程”是一个相当泛泛的术语。虽然所有反应式编程的库、框架和语言大体上都是关注编写响应值变化的程序,但是它们在术语、设计和实现方面却存在巨大的差异。在本书中,每当提到“反应式编程”时,我们特指在Shiny中实现的反应式编程。因此,如果你阅读了关于反应式编程的资料,而这些资料并不是专门针对Shiny的,那么这些概念甚至术语都不太可能与编写Shiny应用相关。对于那些对其他反应式编程框架有一定经验的读者来说,Shiny的方法与MeteorMobX类似,与ReactiveX系列或任何将自己标记为功能反应式编程的内容有很大不同。

13.4 小结

现在你已经理解了为什么需要反应式编程,并且对它的发展历史有了一些了解,接下来的一章将讨论其底层理论的更多细节。最重要的是,你将巩固对反应图的理解,反应图将反应值、反应表达式和观察者连接起来,并精确控制何时运行。

加关注

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

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

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

下一篇:
Shiny从入门到入定——12-Tidy evaluation