MasteringShiny1.3 基础反应性Basic Reactivity

今天我们学习第一章的第三节。本节我们学习基础的反应式编程,有许多概念性的内容,但能加深我们对Shiny的理解。

第三节, Basic reactivity

1. 简要介绍

本节将对反应式编程做一个温和的介绍,学习Shiny应用程序中最常用的反应式结构的基本知识。反应式编程的关键思想是指定一个依赖关系图,这样,当一个input发生变化时,所有相关的output都会自动更新。我们将从研究server函数开始,更详细地讨论input和output参数如何工作。然后回顾最简单的反应式(input直接连接到output),然后讨论反应式如何让我们避免重复的工作。最后,我们将回顾新的Shiny用户遇到的一些常见问题。

2. Server函数

如下,每一个Shiny应用的内容基本结构是一样的。

library(shiny)
ui <- fluidPage( # front end interface)
server <- function(input, output, session) { # back end logic}
shinyApp(ui, server)

在上一节中我们介绍了前端的基本知识,并不复杂,每个使用者得到的HTML使用界面都是一样的。而server则需要满足不同使用这不同的操作,实现独立性,要复杂很多。

为了实现这种独立性,Shiny在每次启动一个新的会话(session)时都会调用server()函数。就像其他R函数一样,当server函数被调用时,它会创建一个新的本地环境,该环境独立于该函数的其他调用。这使得每个会话都有一个独特的状态,同时也隔离了函数内部创建的变量。这就是为什么在Shiny中所有的反应式编程都是在server函数中进行的。

server函数需要三个参数,input, outputsession。目前我们仅关注inputoutput参数即可。

2.1 输入Input

server中的input参数是一个类似列表的对象,包含了所有从浏览器发送的输入数据,根据输入ID命名。例如,如果你的用户界面包含一个数字输入控件,输入ID为count,如下:

ui <- fluidPage(  numericInput('count', label = 'Number of values', value = 100))

我们可以用input$count访问该输入的值。它最初将包含数值100,当用户在浏览器中改变数值时,它将被自动更新。此外,input参数是只读的,我们不能在server中对其进行修改,如下。

server <- function(input, output, session) { input$count <- 10 }
shinyApp(ui, server)#> Error: Can't modify read-only reactive value 'count'

这个错误的发生是因为输入反映了浏览器中的情况,我们并不能在R中对其进行修改,这样会造成矛盾。但是我们可以在server中通过updata*族函数对网页中的input进行修改。

另外要读取input中的数值,我们必须将其置于一个reacitve context下,也就是想render*族函数或reactive()函数当中,不然会见到如下的错误

server <- function(input, output, session) {  message('The value of input$count is ', input$count)}shinyApp(ui, server)#> Error: Can't access reactive value 'count' outside of reactive consumer.#> ℹ Do you need to wrap inside reactive() or observer()?
2.2 输出output

与input相似,output也是一个列表样的对象,根据output在ui中的ID来命名。区别在于它用于输出而不是作为输入。*output始终要和render*配合使用。而render*族函数做了以下两件事:

  • 它设置了一个特殊的反应式上下文,自动跟踪输出使用了哪些输入

  • 它将我们的R代码的输出转换为适合在网页上显示的HTML。

3. 反应式编程 Reactive programming

通过反应式编程,我们能将input和output相互联系,实现交互性。尝试以下代码:

ui <- fluidPage( textInput('name', 'What's your name?'),#文字输入 textOutput('greeting')#文字输出)
server <- function(input, output, session) { output$greeting <- renderText({ paste0('Hello ', input$name, '!')#对输入的文字打招呼 })}

运行后可以看到我们的输出框的内容是跟随输入框的内容而发生改变的,这就是反应式编程的魔力。它实际上告诉Shiny当需要时该如何修改output中的内容,并由Shiny决定是否修改。也就是我们只是告诉Shiny必要时该怎么做,而不是直接命令它。

3.1 命令式编程与声明式编程 Imperative vs declarative programming

这两者间的区别就在于命令和配方间的区别:

  • 在命令式编程中,我们发出一个特定的命令,它就会立即执行。这是在分析脚本中习惯的编程风格:命令R加载你的数据,对其进行转换,将其可视化,并将结果保存到磁盘。

  • 在声明式编程中,我们表达更高层次的目标或描述重要的约束条件,并依靠其他人来决定如何和/或何时将其转化为行动。这就是在Shiny中使用的编程风格。

3.2 惰性

声明式编程的特点就是我们的App会充满惰性,也就是只有我们在网页中看到的部分的代码会运行,这大大减少了工作量。但是一个缺点就是,当server中的某一部分不存在时,它不会抱错也不会运行。如下,我们搞错了output的ID,由于不存在,并不会运行。

如果你发现你的server中某一部分代码不允许,检查ID是否一致。

server <- function(input, output, session) {  output$greting <- renderText({                     #此处的ID应为greeting    paste0('Hello ', input$name, '!')  })}
3.3 反应图 reactive graph

由于上述的惰性,Shiny中server部分的代码并不是按照顺序进行的,这点需要注意。要搞清楚运行顺序,就要看reactive graph,它展示了输入和输出是如何连接的。比如上面代码用reactive graph看就如下:

此图简单明了,不多赘述。我们经常这样描述,greetingname有一种反应性依赖 reactive dependency。在第14节中我们会学习如何为我们的App创建reactive graph。当我们的应用越来越复杂时,它会变得非常有用。

3.4 反应性表达式 reactive expression

反应性表达式是另一个非常重要的成分,可以很好地减少代码。简单来说,反应式表达式接受输入并产生输出,所以它结合了输入和输出的特征。

最简单的形式如下:

  • 将要输出的内容包装在reactive({})中,赋值给string

  • string此时既可以作为输入也可以作为输出

server <- function(input, output, session) { string <- reactive(paste0('Hello ', input$name, '!')) output$greeting <- renderText(string())}

此时的reactive graph如下:

3.5 执行顺序

再次注意我们的代码运行顺序完全由反应图决定。这与大多数R代码不同,后者的执行顺序是由行的顺序决定的。例如,我们可以翻转我们简单的server函数中两行的顺序,将string放在output下方,但App仍然能够正常运行。

server <- function(input, output, session) {  output$greeting <- renderText(string())  string <- reactive(paste0('Hello ', input$name, '!')) }

4. 反应性表达式

正如我们上面提到的Reactive expression既有输入的特征又有输出的特征:

  • 像input一样,可以在输出中使用反应式的结果

  • 像output一样,反应式依赖于input,并自动知道它们何时需要更新。

我们可以用生产者producers来指代反应性输入和表达式,用消费者consumers来指代反应性表达式和输出,如下图:

下面我们通过构建一个更复杂的App来体会反应性表达式的好处。

4.1 前期准备

假设我们要做一个应用,展示两个数据集的分布图,并做假设检验。我们想提前写好一些函数:

  • freqpoly() 函数可以可视化两个数据集的分布曲线

  • t_test() 函数可以做假设检验

library(ggplot2)#freqpolyfreqpoly <- function(x1, x2, binwidth = 0.1, xlim = c(-3, 3)) { df <- data.frame( x = c(x1, x2), g = c(rep('x1', length(x1)), rep('x2', length(x2))) )
ggplot(df, aes(x, colour = g)) + geom_freqpoly(binwidth = binwidth, size = 1) + coord_cartesian(xlim = xlim)}
#t_testt_test <- function(x1, x2) { test <- t.test(x1, x2) # use sprintf() to format t.test() results compactly sprintf( 'p value: %0.3f\n[%0.2f, %0.2f]', test$p.value, test$conf.int[1], test$conf.int[2] )}

下面构建两个正态分布的数据,并使用上面的函数,得到结果如下:

x1 <- rnorm(100, mean = 0, sd = 0.5)x2 <- rnorm(200, mean = 0.15, sd = 0.9)freqpoly(x1, x2)cat(t_test(x1, x2))#> p value: 0.040#> [-0.32, -0.01]d
4.2 构建App

首先是构建UI部分,fluidRow()column()是布局函数,我们之后会学习

  • 第一行有三列输入控制(分布1,分布2,和画图)

  • 第二行有一个宽列,用于绘图,还有一个窄列,用于假设检验。

ui <- fluidPage( fluidRow( column(4, 'Distribution 1', numericInput('n1', label = 'n', value = 1000, min = 1), numericInput('mean1', label = 'µ', value = 0, step = 0.1), numericInput('sd1', label = 'σ', value = 0.5, min = 0.1, step = 0.1) ), column(4, 'Distribution 2', numericInput('n2', label = 'n', value = 1000, min = 1), numericInput('mean2', label = 'µ', value = 0, step = 0.1), numericInput('sd2', label = 'σ', value = 0.5, min = 0.1, step = 0.1) ), column(4, 'Frequency polygon', numericInput('binwidth', label = 'Bin width', value = 0.1, step = 0.1), sliderInput('range', label = 'range', value = c(-3, 3), min = -5, max = 5) ) ), fluidRow( column(9, plotOutput('hist')), column(3, verbatimTextOutput('ttest')) ))

然后构建server部分

server <- function(input, output, session) {  output$hist <- renderPlot({    x1 <- rnorm(input$n1, input$mean1, input$sd1)    x2 <- rnorm(input$n2, input$mean2, input$sd2)        freqpoly(x1, x2, binwidth = input$binwidth, xlim = input$range)  }, res = 96)  output$ttest <- renderText({    x1 <- rnorm(input$n1, input$mean1, input$sd1)    x2 <- rnorm(input$n2, input$mean2, input$sd2)        t_test(x1, x2)  })}

运行后效果如下:

4.3 反应图

我们首先要明确一点,shiny会将output看做一个整体,当其中的一个部分改变时,output整体都会重新运行。拿出ttest部分的代码为例,x1和x2中的六个参数同时作为t_test的输入,其中任何一个参数改变,都会导致x1和x2重新计算。

output$ttest <- renderText({ x1 <- rnorm(input$n1, input$mean1, input$sd1) x2 <- rnorm(input$n2, input$mean2, input$sd2) t_test(x1, x2) })

反应图如下,可以发现几乎所有input都直接去output相连接,会导致两个问题:

  • 应用程序很难理解,因为有这么多的联系。该应用程序没有任何部分是可以拉出来孤立地分析的

  • 应用程序效率低下,因为它所做的工作超过了需要。比如,如果改变bindwidth,x1和x2数据也将被重新计算;如果你改变n1的值,x2将被更新(且在两个地方)。

4.4 简化反应图

在下面的server函数中,我们将重复的代码拉出来,变成两个新的反应式,x1和x2,模拟两个分布的数据。为了创建一个反应式,我们调用reactive()并将结果赋值给一个变量。在后续的使用中,我们像调用一个函数一样调用该变量就可以了。反应式已经根据已有的参数计算并储存了当前值,当其中的参数不改变时,反应式的值也不会改变。

server <- function(input, output, session) {  x1 <- reactive(rnorm(input$n1, input$mean1, input$sd1))  x2 <- reactive(rnorm(input$n2, input$mean2, input$sd2))  output$hist <- renderPlot({    freqpoly(x1(), x2(), binwidth = input$binwidth, xlim = input$range)  }, res = 96)  output$ttest <- renderText({    t_test(x1(), x2())  })}

修改后反应图如下,现在修改bindwidth,只有图画部分会改变,x1和x2不会被重新计算:

这里介绍一下编程的 '三原则 ':每当你复制和粘贴某样东西三次时,你应该想办法减少重复(通常是通过写一个函数)。这样我们的代码又好理解,又可以方便的更新,又减少了计算。而在Shiny中,当粘贴一次时,我们就最好这样做。

5. 控制评估的时间

现在我们已经熟悉了反应性reactivity的基本思想,我们将讨论两种更高级的技术,它们允许增加或减少反应性表达式的执行频率。

为了说明我们先简化一下前文的应用,两个数据共享n但lambda不同,用画图展示

ui <- fluidPage( fluidRow( column(3, numericInput('lambda1', label = 'lambda1', value = 3), numericInput('lambda2', label = 'lambda2', value = 5), numericInput('n', label = 'n', value = 1e4, min = 0) ), column(9, plotOutput('hist')) ))server <- function(input, output, session) { x1 <- reactive(rpois(input$n, input$lambda1)) x2 <- reactive(rpois(input$n, input$lambda2)) output$hist <- renderPlot({ freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40)) }, res = 96)}

5.1 Timed invalidation

现在我们想要上面应用展示的图片不断更新,可以用到reactiveTimer()函数。该反应性表达式对一个隐藏的input有依赖性,就是时间。如下代码,在500ms后,x1和x2会自动失活(invalidate),此时x1和x2之间需要重新计算后与hist重新建立连接.。

server <- function(input, output, session) {  timer <- reactiveTimer(500)#设置时间    x1 <- reactive({    timer() #让x1依赖timer(),下同    rpois(input$n, input$lambda1)  })  x2 <- reactive({    timer()    rpois(input$n, input$lambda2)  })    output$hist <- renderPlot({    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))  }, res = 96)}

5.2 点击

当我们运行需要1s,而每0.5s就会重新计算时,我们的应用永远无法完成工作,导致积压。此时我们可以通过提供按钮的方式来改善,特别是在计算量很大时非常有用。此时就用到了actionButton()

ui <- fluidPage( fluidRow( column(3, numericInput('lambda1', label = 'lambda1', value = 3), numericInput('lambda2', label = 'lambda2', value = 5), numericInput('n', label = 'n', value = 1e4, min = 0), actionButton('simulate', 'Simulate!') #按钮的ID为simulate ), column(9, plotOutput('hist')) ))server <- function(input, output, session) { x1 <- reactive({ input$simulate #当点击按钮时重新计算x1,下同 rpois(input$n, input$lambda1) }) x2 <- reactive({ input$simulate rpois(input$n, input$lambda2) }) output$hist <- renderPlot({ freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40)) }, res = 96)}

但我们只是在反应式内部使x1依赖click,其他参数改变时x1还是会重新计算,而我们想要只有点击时才重新计算。这时就要配合event*族函数,只有点击时,第二个参数的部分才会运行。

server <- function(input, output, session) {  x1 <- eventReactive(input$simulate, {#第一个参数为点击,第二个参数为要运行的代码    rpois(input$n, input$lambda1)  })  x2 <- eventReactive(input$simulate, {    rpois(input$n, input$lambda2)  })  output$hist <- renderPlot({    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))  }, res = 96)}

反应图如下,x1和x2不再对lambda1、lambda2和n有反应性依赖:改变它们的值将不会触发计算:

6. 观察者Observers

到目前为止,我们一直专注于应用程序内部发生的事情。但有时我们需要将手伸到应用程序之外。可能是将一个文件保存到共享网络驱动器,将数据发送到网络API,更新数据库,或者(最常见的)向控制台打印调试信息。这些动作并不影响我们的应用程序的外观,所以不用使用output和render函数。而是需要使用观察者observer

这部分内容会在第十五节学习,而在这里主要介绍一个debug很好的工具--observeEvent函数。与eventReactive()函数相似,它有两个重要参数eventExprhandlerExpr。前者是被依赖的input或表达式,后者是随后运行的代码。

如下代码,每当name更新,就会在终端返回相应的信息:

ui <- fluidPage( textInput('name', 'What's your name?'), textOutput('greeting'))
server <- function(input, output, session) { string <- reactive(paste0('Hello ', input$name, '!')) output$greeting <- renderText(string()) observeEvent(input$name, { #name是被依赖的 message('Greeting performed') #返回得信息 })}

7. 小结

本节我们我们对Shiny的后端有了一个初步详细的了解,学习了server的书写。对反应式编程有了个大概,后续我们还会深入理解他们。本节所学的概念有些多,但可以帮助我们更好的学习和使用Shiny。下一节我们通过构建一个App来结束第一章的学习。

编辑:周朝政

校审:陈振宇 罗鹏

(0)

相关推荐

  • 基于R语言的shiny网页工具开发基础系列-01

    shiny是一个直接用R来制作交互式网页应用 (interactive web applications (apps)) 的R包 一.欢迎使用shiny 如下就是一个简单朴素的shiny app界面 ...

  • 基于R语言的shiny网页工具开发基础系列-04

    l4-反应输出 了解小工具如何和反应输出联系,反应输出即无何时用户改变小工具都会自动更新的对象 展示反应输出 是时候给app注入灵魂了,此篇介绍如何构建一个反应输出在app中展示. 只要用户触发小工具 ...

  • 基于R语言的shiny网页工具开发基础系列-06

    L6-反应表达式 用反应表达式,快速构建,模块化app ⚠️此篇的线上数据可能有时无法顺利抓取,要多试几次 使用反应表达式 用户会赞叹快速的app,但是你的app有大量运算影响速度了该怎么办呢? 此篇 ...

  • R : Shiny|搭建单细胞数据分析云平台

    男, 一个长大了才会遇到的帅哥, 稳健,潇洒,大方,靠谱. 一段生信缘,一棵技能树, 一枚大型测序工厂的螺丝钉, 一个随机森林中提灯觅食的津门旅客. 前言 shiny官网(https://shiny. ...

  • 基于R语言的shiny网页工具开发小技巧系列-02

    六年前还在上海工作的时候,机缘巧合接触了使用R语言的shiny体系搭建网页工具的技术,就一直身体力行的在我们生物信息学圈子里面推广它. 自己一个人能做的很有限,很庆幸这些年有各式各样的小伙伴加入我们& ...

  • 基于R语言的shiny网页工具开发基础系列-02

    l2-shiny的页面布局 基于上篇对shiny app 结构的了解 是时候开始从零构建一个shiny app了 二.构建一个用户界面 此篇旨在如何构建app对用户界面,如何布局用户界面然后加文字图片 ...

  • 关于 tf.data.TextLineDataset() 和常见dataset函数

    官方原话: class TextLineDataset(dataset_ops.Dataset): """A `Dataset` comprising lines fro ...

  • Python理解函数调用的原理及其概念

    本文将介绍与函数有关的所有概念,并让你很容易理解.这个主题很容易理解,但是由于实践经验较少而很难理解. 涉及的主题: 介绍 函数参数及其类型 全局和局部变量 将数据序列传递给功能 匿名函数-Lambd ...

  • 基于R语言的shiny网页工具开发基础系列-03

    l3-更复杂的页面部件 shiny 小部件提供了一个用户给app传送信息的方式 为什么加上控制小工具 上节已经学会在用户界面放置一些简单的元素,但显示更复杂的内容需要用到小部件widgets widg ...

  • 基于R语言的shiny网页工具开发基础系列-05

    l5-更复杂的反应app 创建一个更复杂的依赖R脚本和额外数据的有灵魂的(能反应的)app 使用R脚本和数据 此篇旨在展示如何载入数据,R脚本,包,用来构建app. 构建一个复杂的数据,可视化美国的人 ...