介绍
现在你已经掌握了一系列有用的技巧,这些技巧赋予了你创建各种有用应用程序的能力。接下来,我们将把注意力转向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 | temp_c <- 10 |
到目前为止还不错:temp_c
变量有值10,temp_f
变量有值50,我们可以改变temp_c
的值:
1 | temp_c <- 30 |
但是改变temp_c
的值并不会影响temp_f
的值:
1 | temp_f |
变量可以随时间变化,但它们不会自动变化。
13.2.2 使用函数怎么样?
你也可以通过函数来解决这个问题:
1 | temp_c <- 10 |
(这是一个有点奇怪的函数,因为它没有参数,而是从其封闭环境40中访问temp_c
,但它是一个完全有效的R代码。)
这解决了反应性试图解决的第一个问题:每当你访问temp_f()
时,你都会得到最新的计算结果:
1 | temp_c <- -3 |
然而,它并没有减少计算量。每次你调用temp_f()
时,它都会重新计算,即使temp_c
没有改变:
1 | temp_f() |
在这个简单的例子中,计算成本不高,所以重复计算没什么大不了的,但这仍然是多余的:如果输入没有改变,我们为什么要重新计算输出呢?
13.2.3 事件驱动编程
由于变量和函数都不适用,我们需要创建一些新的东西。在过去的几十年里,我们可能会直接跳到事件驱动编程。事件驱动编程是一个吸引人的简单范式:你注册回调函数,这些函数会在事件发生时执行。
我们可以使用R6实现一个非常简单的事件驱动工具包,如下例所示。在这里,我们定义了一个DynamicValue
,它有三个重要的方法:get()和set()
用于访问和更改基础值,onUpdate()
用于注册在值被修改时要运行的代码。如果你不熟悉R6,不用担心细节,而是关注下面的例子。
1 | DynamicValue <- R6::R6Class("DynamicValue", list( |
因此,如果Shiny在五年前就被发明出来,它可能看起来会更像这样,其中temp_c
使用<<-
来在需要时更新temp_f
。
1 | temp_c <- DynamicValue$new() |
事件驱动编程解决了不必要的计算问题,但它又产生了一个新问题:你必须仔细跟踪哪些输入会影响哪些计算。不久之后,你就会在正确性(即在任何内容发生更改时都更新所有内容)和性能(即尝试仅更新必要的部分,并希望你没有遗漏任何边缘情况)之间权衡取舍,因为同时做到这两点非常困难。
13.2.4 反应性编程
反应性编程通过将上述解决方案的特性结合起来,优雅地解决了这两个问题。现在,我们可以向您展示一些真正的Shiny代码,它使用了一个特殊的Shiny模式,即reactiveConsole(TRUE)
,这使得我们可以直接在控制台中实验反应性。
1 | library(shiny) |
与事件驱动编程一样,我们需要某种方式来指示我们有一个特殊类型的变量。在Shiny中,我们使用reactiveVal()创建一个反reactive value。反应式值有特殊的语法来获取其值(像调用无参数函数一样调用它)和设置其值(像调用单参数函数一样调用它来设置其值)。
1 | temp_c <- reactiveVal(10) # create |
现在我们可以创建一个依赖于这个值的反应式表达式:
1 | temp_f <- reactive({ |
在创建应用程序时,你已经了解到反应式表达式会自动跟踪其所有依赖项。因此,如果之后temp_c
发生变化,temp_f
会自动更新:
1 | temp_c(-3) |
但如果temp_c()
没有变化,那么temp_f()
就不需要重新计算,可以直接从缓存中获取:
1 | temp_f() |
反应式表达式有两个重要特性:
它是惰性的:在被调用之前,它不会进行任何工作。
它是缓存的:在第二次及后续调用时,它不会进行任何工作,因为它缓存了之前的结果。
我们将在第14
章中再次讨论这些重要特性。
13.3 反应式编程简史
如果你想了解更多其他语言的反应式编程知识,那么了解其发展历史可能会有所帮助。你可以在VisiCalc(第一个电子表格软件)中看到反应式编程的萌芽,它起源于40多年前:
我想象了一个神奇的黑板,当你擦去一个数字并写入新的东西时,其他所有的数字都会自动改变,就像用数字进行的文字处理一样。
电子表格与反应式编程密切相关:你使用公式声明单元格之间的关系,当一个单元格发生变化时,其所有依赖项都会自动更新。因此,你可能已经在不知不觉中进行了大量反应式编程!
虽然反应性的概念已经存在很长时间了,但直到20世纪90年代末,学术界才开始对其进行认真研究。反应式编程的研究始于FRAN(functional reactive animation,功能性反应式动画),这是一种将随时间变化和用户输入融入功能编程语言中的新颖系统。这催生了一系列丰富的文献,但对编程实践的影响却很小。
直到2010年代,反应式编程才通过JavaScript UI框架的快节奏世界进入主流编程领域。像Knockout、Ember和Meteor(Joe Cheng’s personal inspiration for Shiny)这样的开创性框架表明,反应式编程可以极大地简化UI编程。短短几年内,反应式编程通过React、Vue.js和Angular等广受欢迎的框架主导了网络编程,这些框架要么本质上具有反应性,要么设计为与反应式后端协同工作。
值得一提的是,“反应式编程”是一个相当泛泛的术语。虽然所有反应式编程的库、框架和语言大体上都是关注编写响应值变化的程序,但是它们在术语、设计和实现方面却存在巨大的差异。在本书中,每当提到“反应式编程”时,我们特指在Shiny中实现的反应式编程。因此,如果你阅读了关于反应式编程的资料,而这些资料并不是专门针对Shiny的,那么这些概念甚至术语都不太可能与编写Shiny应用相关。对于那些对其他反应式编程框架有一定经验的读者来说,Shiny的方法与Meteor和MobX类似,与ReactiveX系列或任何将自己标记为功能反应式编程的内容有很大不同。
13.4 小结
现在你已经理解了为什么需要反应式编程,并且对它的发展历史有了一些了解,接下来的一章将讨论其底层理论的更多细节。最重要的是,你将巩固对反应图的理解,反应图将反应值、反应表达式和观察者连接起来,并精确控制何时运行。
加关注
关注公众号“生信之巅”,聊天窗口回复“85d7”获取下载链接。
敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!