这一章关于信号,他包括:
- signal 介绍
- 使用
foldp函数保持状态 - mailboxs
在Elm 中,信号Signals是创建应用程序的一个基本构建块。可以把一个 signal 想象成一个流,他的值每时每刻都在变化。
信号可以合并,转换以及过滤。让我们来看一个基本的 signal:
module Main (..) where
import Html exposing (Html)
import Mouse
view : Int -> Html
view x =
Html.text (toString x)
main : Signal.Signal Html
main =
Signal.map view Mouse.xhttps://github.com/sporto/elm-tutorial-assets/blob/master/code/020_signals/BasicMouse.elm
在浏览器中运行这段代码,移动鼠标就会看到x坐标值在变化。
首先我们导入了Html模块,用他将x坐标显示为 HTML。紧接着又导入了Mouse模块,他提供了用于鼠标操作的工具。
view函数取一个整数作为参数并返回一段HTML(Int -> Html)。
不过,Html.text需要的是一个字符串,所以我们必须将x转为字符串。这就要用到toString函数。
Elm 程序的 main 函数比较特殊,他要求返回一个静态元素或是一个信号。这里的main返回的是携带Html的信号(Signal.Signal Html)。换句话说,我们的HTML现在可以随信号的变化而变化。
要理解这是怎么做到的,让我们先来分析一下代码的最后一行(Signal.map view Mouse.x)。
Mouse.x会在x改变时给我们一个signal 。这个信号的签名是Signal.Signal Int,他表示这个信号会携带一个整数。
Signal.map的作用是将一个信号转换为另一个信号,类型签名是Signal.map : (a -> result) -> Signal a -> Signal result。让我们来分析一下:
- 第一个参数是一个函数,他接收
a类型的值,返回result类型的值。这个函数会将原信号携带的值转换为一个新值用作输出信号。 - 第二个参数就是原信号,他的类型变量是
a,和第一个参数中的a相同。 - 返回类型是
Signal result,表示输出的 signal 携带的类型是result。
图示:signal
A通过Signal.map产生了signalB。输出信号中的值是原信号中的两倍。
回到我们的例子中:
Signal.map view Mouse.xview是用来转换的函数,他取一个Int并返回Html。
第二个参数(Mouse.x)是一个带有Int值的信号。
而结果正是我们想要的Html值的信号,用作main函数的输出。
Singal.map用转换函数将原信号转换成为一个新信号。当原信号发生变化时,信号中的值统统会被转换到新信号中。
这里是 map 的另一个例子:
double x =
x * 2
doubleSignal =
Signal.map double Mouse.xdouble是一个将输入值加倍的函数,这样看来,doubleSignal就是一个将当前 x 坐标乘 2 的信号。
上一节中,我们创建了一个简单的应用来显示当前鼠标的x坐标。获取到x坐标这个状态之后就被丢弃掉了。但是在大多数应用中,我们希望储存一些用户交互的状态值。
现在我们来创建一个记录鼠标单击的程序。
module Main (..) where
import Html exposing (Html)
import Mouse
view : Int -> Html
view count =
Html.text (toString count)
countSignal : Signal Int
countSignal =
Signal.map (always 1) Mouse.clicks
main : Signal.Signal Html
main =
Signal.map view countSignalhttps://github.com/sporto/elm-tutorial-assets/blob/master/code/020_signals/Clicks.elm
现在这个程序会显示出1,让我们来依次介绍一下。
view函数用来显示数量,但目前是1,不会改变。
countSignal函数返回一个带有整数的信号。Mouse.clicks信号作为输入并被转换为(always 1)。
main函数用view来转换countSignal。
Signal.foldp用来“折叠过去”,他取上一个状态和当前输入进行组合,产生一个新的信号。很像JavaScript的reduce。
我们来使用foldp改写:
module Main (..) where
import Html exposing (Html)
import Mouse
view : Int -> Html
view count =
Html.text (toString count)
countSignal : Signal Int
countSignal =
Signal.foldp (\_ state -> state + 1) 0 Mouse.clicks
main : Signal.Signal Html
main =
Signal.map view countSignalhttps://github.com/sporto/elm-tutorial-assets/blob/master/code/020_signals/ClicksWithFoldp.elm
让我们看下Signal.foldp那行发生了些什么:
Signal.foldp有三个参数:
- 积累函数:
(\_ state -> state + 1) - 初始状态:
0 - 输入信号:
Mouse.clicks
语法\x y -> x + y表示这是一个匿名函数,他等同于 ES6 中的(x, y) => x + y。
foldp每次接到信号就会调用积累函数。- 积累函数接受上一个状态和输入信号用于输出。
- 首次调用
foldp会将输入信号和一个初始状态传递给积累函数。 - 积累函数计算并返回一个新的状态。
foldp保持这个新状态,下一次时将作为前一状态传入积累函数- 最后,
foldp会产生一个输出信号
\_ state -> state + 1积累函数取Mouse.clicks信号中得到值和上一状态作为参数,返回一个新的状态。
不过,来自Mouse.clicks信号的值是(),在 ELm 中这个叫做单位类型。现在我们并不需要他,所以用_忽略掉了。
foldp是构建程序非常重要的构建块,还有一个东东也非常重要,他就是邮箱 Mailboxes。
到目前为止,我们一直在监听像Mouse.x这样的"原始"信号并且用它来在页面上显示信息。但在一个大型应用中,我们想要的是能够与用户交互。比如单击一个链接或按钮。
在 Elm 中我们使用邮箱来做这件事。一个邮箱就是一个从UI接收消息和任务,并发出信号的中转站。
想要更好的理解邮箱,让我们先来创建一个按钮:
module Main (..) where
import Html exposing (Html)
view : String -> Html
view message =
Html.div
[]
[ Html.div [] [ Html.text message ]
, Html.button [] [ Html.text "Click" ]
]
messageSignal : Signal String
messageSignal =
Signal.constant "Hello"
main : Signal Html
main =
Signal.map view messageSignalhttps://github.com/sporto/elm-tutorial-assets/blob/master/code/020_signals/Mailbox01.elm
view函数将显示一条消息和一个用于点击的按钮,这个按钮现在还木有做任何事。
messageSignal是一个用“Hello”字符串构造的信号,意味着他的值将不会改变。
main函数从messageSignal得到字符串并交给view函数转换。在浏览器中运行这段代码,就会看到来自messageSignal的Hello。
我们想要点击按钮来改变消息。这需要用到单击事件Html.Events.onClick。如果你看过了Html.Events模块的文档,就会发现所有的事件都有相同的函数签名:Signal.Address a -> a -> Html.Attribute
任意类型变量的Signal.Address作为第一个参数,第二个和第一个参数具备相同的类型变量,然后返回了一个Html.Attribute。
地址就是一个辨识信号的标识。他允许我们给那个信号发送消息。
想要获取地址发来的消息,我们需要使用一个Mailbox。
可以像这样创建一个邮箱:
mb : Signal.Mailbox String
mb =
Signal.mailbox ""mb是一个返回Mailbox的函数。这个信箱用于处理字符串,他接受String类型的消息并发出一个String类型的信号。空字符串是信箱提供给信号的默认值。
这个函数返回的信箱其实是一个两个属性的记录:
{ address : Signal.Address a
, signal : Signal.Signal a
}记录的address让我们可以发消息给他,并且可以监听他的signal。
下一步是在我们的程序中使用信箱,这样我们就可以发送和刷新消息了。
module Main (..) where
import Html exposing (Html)
import Html.Events as Events
view : Signal.Address String -> String -> Html
view address message =
Html.div
[]
[ Html.div [] [ Html.text message ]
, Html.button
[ Events.onClick address "Hello"
]
[ Html.text "Click" ]
]
mb : Signal.Mailbox String
mb =
Signal.mailbox ""
main : Signal Html
main =
Signal.map (view mb.address) mb.signalhttps://github.com/sporto/elm-tutorial-assets/blob/master/code/020_signals/Mailbox02.elm
view : Signal.Address String -> String -> Html
view address message =
Html.div
[]
[ Html.div [] [ Html.text message ]
, Html.button
[ Events.onClick address "Hello"
]
[ Html.text "Click" ]
]view函数现在改为取Signal.Address作为第一个参数。
Events.onClick address "Hello"用于在Html元素上监听点击事件。在每次点击元素时onClick将向给定的地址发送消息(这里是“Hello”)。
main : Signal Html
main =
Signal.map (view mb.address) mb.signal这里的main函数做了两件事情:
- 使用了一个偏应用的view
(view mb.address)函数,这样view的第一个参数就是邮箱的address。 - 在邮箱的导出的信号上应用偏应用函数view。所以
view会将邮箱的信号作为他的第二个参数。
这里的图帮助我们理解发生了什么:
- 最开始时,
main用信箱的初始值传递给地址然后渲染视图(空字符串) - 当视图渲染完成,我们在按钮上使用
onClick来监听事件 - 当按钮点击时,一个消息发送给邮箱的地址
- 在接到消息后,邮箱会发出一个信号,
main会接到这个信号 - 此时,
main函数再次通过邮箱的地址来渲染视图,消息就来自于邮箱("Hello")