生命周期和订阅
到目前为止,您学到的概念涵盖了您将编写的大多数Halogen
组件。大多数组件都有内部状态,呈现HTML
元素,并通过在用户单击、悬停或以其他方式与呈现的HTML
交互时执行操作来做出响应。
但是actions
也可以从其他类型的事件内部产生。下面是一些常见的例子:
- 您需要在组件启动时运行一个
action
(例如,您需要执行一个effect
来获得您的初始状态)或者当组件从DOM
中移除时(例如,清理您获得的资源)。这些被称为生命周期事件。 - 您需要定期运行
action
(例如,您需要每10
秒执行一次更新),或者当一个事件发生在你渲染的HTML
之外时(例如,你需要在DOM
窗口上按下一个键时运行一个action
, 或者您需要处理在第三方组件(如文本编辑器)中发生的事件)。这些由订阅处理。
当我们在下一章学习父组件和子组件时,我们将学习在组件中产生action
的另一种方式。本章将重点介绍生命周期和订阅。
生命周期事件
每个Halogen
组件都可以访问两个生命周期事件:
- 组件可以在初始化时评估一个
action
(Halogen
创建它) - 组件可以在完成时评估一个
action
(Halogen
删除它)
我们指定当组件作为eval
函数的一部分进行初始化和最终确定时要运行的action
(如果有的话),这与我们提供handleAction
函数的位置相同。在下一节中,我们将更详细地了解eval
是什么,但首先让我们看一个正在运行的生命周期示例。
下面的示例与我们的随机数组件几乎相同,但有一些重要的变化:
- 除了现有的
Regenerate
操作之外,我们还添加了Initialize
和Finalize
。 - 我们已经扩展了我们的
eval
以包含一个initialize
字段,该字段声明应在组件初始化时评估我们的Initialize
操作,以及一个finalize
字段, 该声明应在组件完成时评估我们的Finalize
操作的。 - 由于我们有两个新
action
,因此我们在handleAction
函数中添加了两个新case
来描述如何处理它们。
尝试通读示例:
module Main where
import Prelude
import Data.Maybe (Maybe(..), maybe)
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (log)
import Effect.Random (random)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type State = Maybe Number
data Action
= Initialize
| Regenerate
| Finalize
component :: forall query input output m. MonadEffect m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
, finalize = Just Finalize
}
}
initialState :: forall input. input -> State
initialState _ = Nothing
render :: forall m. State -> H.ComponentHTML Action () m
render state = do
let value = maybe "No number generated yet" show state
HH.div_
[ HH.h1_
[ HH.text "Random number" ]
, HH.p_
[ HH.text ("Current value: " <> value) ]
, HH.button
[ HE.onClick \_ -> Regenerate ]
[ HH.text "Generate new number" ]
]
handleAction :: forall output m. MonadEffect m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Initialize -> do
handleAction Regenerate
newNumber <- H.get
log ("Initialized: " <> show newNumber)
Regenerate -> do
newNumber <- H.liftEffect random
H.put (Just newNumber)
Finalize -> do
number <- H.get
log ("Finalized! Last number was: " <> show number)
当这个组件挂载时,我们将生成一个随机数并将其记录到控制台。当用户单击按钮时,我们将不断重新生成随机数,当这个组件从DOM
中移除时,它会记录它在状态中的最后一个数字。
我们在这个例子中做了另一个有趣的改变: 在我们的Initialize
处理程序中,我们调用了handleAction Regenerate
– 我们递归地调用了handleAction
. 正如我们在这里所做的那样,不时从其他actions
中调用actions
会很方便。我们也可以内联Regenerate
的处理程序 —— 下面的代码做同样的事情:
Initialize -> do
newNumber <- H.liftEffect random
H.put (Just newNumber)
log ("Initialized: " <> show newNumber)
在我们转向订阅之前,让我们先谈谈eval
函数。
eval函数,mkEval和EvalSpec
我们一直在我们所有的组件中使用eval
,但到目前为止我们只通过handleAction
函数处理了由我们的Halogen HTML
产生的action
。 但是eval
函数可以描述我们的组件可以评估HalogenM
代码以响应事件的所有方式。
在绝大多数情况下,您不需要太关心下面描述的组件规范和评估规范中涉及的所有类型和函数,但我们将简要分解这些类型,以便您了解发生了什么。
mkComponent
函数接受一个ComponentSpec
,它是一个包含三个字段的记录:
H.mkComponent
{ initialState :: input -> state
, render :: state -> H.ComponentHTML action slots m
, eval :: H.HalogenQ query action input ~> H.HalogenM state action slots output m
}
我们已经在initialState
和render
函数上花费了大量时间。但是eval
函数可能看起来很奇怪 —— 什么是HalogenQ
,handleAction
之类的函数是如何适应的?现在,我们将重点介绍此函数的最常见用法,但您可以在概念参考中找到完整的详细信息。
eval
函数描述了如何处理组件中出现的事件。它通常是通过将mkEval
函数应用于EvalSpec
来构建的,与我们将mkComponent
应用于ComponentSpec
以生成Component
的方式相同。
为方便起见, Halogen
提供了一个名为defaultEval
的已经完成的EvalSpec
,它在组件中出现事件时不执行任何操作。通过使用这个默认值,你可以只覆盖你关心的值,而其余的值可以什么都不做。
以下是我们如何定义目前仅处理action
的eval
函数:
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
-- 假设我们已经在范围内定义了一个`handleAction`函数...
handleAction = ...
注意:initialState
和render
使用缩写record
双关符号设置; 但是,在这种情况下,handleAction
不能设置为双关语,因为它是Records
更新的一部分, Records语言参考中提供了有关record
双关语和record
更新语法的更多信息。
如果需要,您可以覆盖更多字段。例如,如果您需要支持初始化程序,那么您也将覆盖initialize
字段:
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
让我们快速浏览一下EvalSpec
的完整类型:
type EvalSpec state query action slots input output m =
{ handleAction :: action -> HalogenM state action slots output m Unit
, handleQuery :: forall a. query a -> HalogenM state action slots output m (Maybe a)
, initialize :: Maybe action
, receive :: input -> Maybe action
, finalize :: Maybe action
}
EvalSpec
涵盖了组件内部可用的所有类型。幸运的是,您无需在任何地方指定此类型 —— 您只需向mkEval
提供一条record
即可. 我们将在下一章介绍handleQuery
和receive
函数以及query
和output
类型,因为它们只与子组件相关。
由于在正常使用中,您将覆盖defaultEval
中的特定字段,而不是自己写出整个eval
规范,让我们也看看defaultEval
为这些函数中的每一个实现了什么:
defaultEval =
{ handleAction: const (pure unit)
, handleQuery: const (pure Nothing) -- 我们将在介绍子组件时了解这一点
, receive: const Nothing -- 我们将在介绍子组件时了解这一点
, finalize: Nothing
}
现在,让我们转到内部事件的另一个常见来源: 订阅。
订阅
有时您需要处理内部出现的事件,这些事件不是来自与您呈现的Halogen HTML
交互的用户。两个常见的来源是基于时间的actions
和事件,它们发生在您渲染的元素之外(如浏览器窗口)。
在Halogen
中,可以使用halogen-subscriptions库手动创建这些类型的事件。Halogen
组件可以通过提供应该在发射器触发时运行的action
来订阅 Emitter
。
您可以使用halogen-subscriptions
库中的函数订阅事件,但Halogen
提供了一个特殊的帮助函数,用于订阅DOM
中称为eventListener
的事件侦听器。
一个Emitter
产生一个action
流,只要它保持订阅emitter
,你的组件就会评估这些action
。创建一个emitter
并在组件初始化时订阅它是很常见的,尽管您可以随时订阅或取消订阅一个emitter
。
让我们看一下订阅的两个示例: 一个基于Aff
的计时器,它计算自组件mounted
以来的秒数,以及一个基于事件侦听器的流,它报告文档上的键盘事件。
实现一个Timer
我们的第一个示例将使用基于Aff
的计时器来每秒递增。
module Main where
import Prelude
import Control.Monad.Rec.Class (forever)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (Milliseconds(..))
import Effect.Aff as Aff
import Effect.Aff.Class (class MonadAff)
import Effect.Exception (error)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
data Action = Initialize | Tick
type State = Int
component :: forall query input output m. MonadAff m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
initialState :: forall input. input -> State
initialState _ = 0
render :: forall m. State -> H.ComponentHTML Action () m
render seconds = HH.text ("You have been here for " <> show seconds <> " seconds")
handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Initialize -> do
_ <- H.subscribe =<< timer Tick
pure unit
Tick ->
H.modify_ \state -> state + 1
timer :: forall m a. MonadAff m => a -> m (HS.Emitter a)
timer val = do
{ emitter, listener } <- H.liftEffect HS.create
_ <- H.liftAff $ Aff.forkAff $ forever do
Aff.delay $ Milliseconds 1000.0
H.liftEffect $ HS.notify listener val
pure emitter
几乎所有这些代码都应该看起来很熟悉,但有两个新部分:
首先,我们定义了一个可重用的Emitter
,它将每秒广播一个我们选择的值,直到它没有订阅者:
timer :: forall m a. MonadAff m => a -> m (HS.Emitter a)
timer val = do
{ emitter, listener } <- H.liftEffect HS.create
_ <- H.liftAff $ Aff.forkAff $ forever do
Aff.delay $ Milliseconds 1000.0
H.liftEffect $ HS.notify listener val
pure emitter
除非您正在创建与DOM
中的事件侦听器相关联的emitters
,否则您应该使用halal-subscriptions
库中的函数。大多数情况下,您将使用HS.create
创建一个emitter
和一个listener
,但如果您需要手动控制取消订阅,您也可以使用HS.makeEmitter
。
其次,我们使用Halogen
的subscribe
函数附加到emitter
,同时提供我们想要每秒发射的特定action
:
Initialize -> do
_ <- H.subscribe =<< timer Tick
pure unit
subscribe
函数将一个Emitter
作为参数,并返回一个SubscriptionId
。您可以随时将此SubscriptionId
传递给Halogen
取消订阅功能以结束订阅. 组件在完成时会自动结束其拥有的任何订阅,因此无需在此处取消订阅。
您可能还对Ace编辑器示例感兴趣,该示例订阅在第三方JavaScript
组件中发生的事件,并使用它们来触发Halogen
组件中的action
。
使用事件监听器作为订阅
使用订阅的另一个常见原因是当您需要对DOM
中的事件做出反应时,这些事件不是直接由您控制的HTML
元素产生的。例如,我们可能想要监听发生在document
本身上的事件。
在以下示例中,我们订阅document
上的按键事件,保存按住Shift
键时输入的任何字符,并在用户按下Enter
键时停止监听。它演示了使用eventListener
函数附加事件侦听器并使用H.unsubscribe
函数选择何时清理它。
在examples
目录下也有对应的键盘输入的例子。
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Data.String as String
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.Query.Event (eventListener)
import Halogen.VDom.Driver (runUI)
import Web.Event.Event as E
import Web.HTML (window)
import Web.HTML.HTMLDocument as HTMLDocument
import Web.HTML.Window (document)
import Web.UIEvent.KeyboardEvent as KE
import Web.UIEvent.KeyboardEvent.EventTypes as KET
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type State = { chars :: String }
data Action
= Initialize
| HandleKey H.SubscriptionId KE.KeyboardEvent
component :: forall query input output m. MonadAff m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
initialState :: forall input. input -> State
initialState _ = { chars: "" }
render :: forall m. State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.p_ [ HH.text "Hold down the shift key and type some characters!" ]
, HH.p_ [ HH.text "Press ENTER or RETURN to clear and remove the event listener." ]
, HH.p_ [ HH.text state.chars ]
]
handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Initialize -> do
document <- H.liftEffect $ document =<< window
H.subscribe' \sid ->
eventListener
KET.keyup
(HTMLDocument.toEventTarget document)
(map (HandleKey sid) <<< KE.fromEvent)
HandleKey sid ev
| KE.shiftKey ev -> do
H.liftEffect $ E.preventDefault $ KE.toEvent ev
let char = KE.key ev
when (String.length char == 1) do
H.modify_ \st -> st { chars = st.chars <> char }
| KE.key ev == "Enter" -> do
H.liftEffect $ E.preventDefault (KE.toEvent ev)
H.modify_ _ { chars = "" }
H.unsubscribe sid
| otherwise ->
pure unit
在这个例子中,我们使用了H.subscribe'
函数,它将SubscriptionId
传递给emitter
而不是返回它。这是一种替代方法,可以让您将ID
保留在action
类型而不是state
中,这样会更方便。
我们将emitter
直接写入我们的代码中以处理Initialize
操作,该操作在document
上注册一个事件侦听器并在每次按下键时发出HandleKey
。
eventListener
使用purescript-web
库中的类型来处理DOM
以手动构建事件侦听器:
eventListener
:: forall a
. Web.Event.EventType
-> Web.Event.EventTarget.EventTarget
-> (Web.Event.Event -> Maybe a)
-> HS.Emitter a
它需要一种要监听的事件类型(在我们的例子中:keyup
),一个指示在哪里监听事件的目标(在我们的例子中: HTMLDocument
本身),以及一个将发生的事件转换为应该发出的类型的回调函数(在我们的例子中: 我们通过在HandleKey
构造函数中捕获事件来发出我们的Action
类型).
Wrapping Up
Halogen
组件使用Action
类型来处理组件内部出现的各种事件。我们现在已经看到了这种情况可能发生的所有常见方式:
- 用户与我们呈现的
HTML
元素的交互 - 生命周期事件
- 订阅,无论是通过
Aff
和Effect
函数还是来自DOM
上的事件侦听器
您现在已经了解了单独使用Halogen
组件的所有基本知识。在下一章中,我们将学习如何将Halogen
组件组合成父组件和子组件的树。