Html.Lazy

有一个称为 elm/html 的包用于在屏幕上显示内容。为了了解如何对其进行优化,我们首先需要了解它是如何工作的!

什么是 DOM?

如果你要创建一个 HTML 文件,你可以直接像这样编写 HTML

<div>
  <p>Chair alternatives include:</p>
  <ul>
    <li>seiza</li>
    <li>chabudai</li>
  </ul>
</div>

你可以将此视为在幕后生成一些 DOM 数据结构

黑色方块表示带有数百个属性的重量级 DOM 对象。当其中任何一个更改时,都可能触发页面内容的昂贵重新渲染和重新排版。

什么是虚拟 DOM?

如果你要创建一个 Elm 文件,可以使用 elm/html 来编写类似这样的内容

viewChairAlts : List String -> Html msg
viewChairAlts chairAlts =
  div []
    [ p [] [ text "Chair alternatives include:" ]
    , ul [] (List.map viewAlt chairAlts)
    ]

viewAlt : String -> Html msg
viewAlt chairAlt =
  li [] [ text chairAlt ]

你可以将 viewChairAlts ["seiza","chabudai"] 视为在幕后生成一些“虚拟 DOM”数据结构

白色方块表示轻量级的 JavaScript 对象。它们仅具有你指定的属性。创建它们绝不会导致重新呈现或重新排版。重点是,与 DOM 节点相比,分配这些对象要便宜得多!

渲染

如果我们始终在 Elm 中使用这些虚拟节点,那它如何转换为我们在屏幕上看到的 DOM 呢?当一个 Elm 程序启动时,它会像这样进行

  • 调用 init 以获取初始的 Model
  • 调用 view 以获取初始的虚拟节点。

现在我们有了虚拟节点,可以在真正的 DOM 中创建一个完全的副本

非常好!但当事情发生变化时会怎样呢?在每帧中重复执行整个 DOM 都不可行,那么我们该怎么做呢?

差异化

一旦拥有初始 DOM,我们就会切换到主要处理虚拟节点。每当 Model 发生变化时,我们都会再次运行 view。从那里,我们“差异化”所得的虚拟节点,以找出如何尽可能少的接触 DOM。

想象一下我们的Model获得了新的椅子备选,我们希望为它添加一个新的li节点。在幕后,Elm对当前虚拟节点和下一个虚拟节点进行差异检测以发现任何更改。

它注意到了添加了第三个li。我用绿色标记了它。现在Elm确切地知道如何修改真实DOM以使其匹配。只需插入该新的li

此差异化过程使尽可能少地修改DOM成为可能。而且如果没有发现差异,我们根本不需要修改DOM!因此,此过程有助于最大程度地减少需要进行的渲染和重排。

但是我们能做的工作更少吗?

Html.Lazy

Html.Lazy 模块让不构建虚拟节点成为可能!核心思想是lazy函数。

lazy : (a -> Html msg) -> a -> Html msg

回到我们的椅子示例,我们调用了viewChairAlts ["seiza","chabudai"],但我们也可以很简单地直接调用lazy viewChairAlts ["seiza","chabudai"]。惰性版本分配了一个这样的“惰性”节点:

该节点只保留对函数和参数的引用。Elm可以将函数和参数放在一起生成整个结构(如果需要的话),但并非总是需要!

Elm的一大优点是函数的“相同输入,相同输出”保证。因此,每当我们在比较虚拟节点时遇到两个“惰性”节点时,我们都会问:该函数相同吗?参数相同吗?如果所有这些相同,我们知道生成的虚拟节点也相同!因此,我们可以完全跳过构建虚拟节点!如果其中任何一个已更改,我们则构建虚拟节点并进行常规差异化。

注意:那么,什么时候两个值“相同”?为了优化性能,我们在幕后使用JavaScript的===运算符。

  • 结构相等用于IntFloatStringCharBool
  • 引用相等用于记录、列表、自定义类型、字典等。

结构相等意味着44相同,无论如何产生这些值。引用相等意味着内存中的实际指针必须相同。使用引用相等始终很便宜O(1),即使数据结构有数千或数百万个条目。因此,这主要是为了确保使用lazy永远不会意外地减慢你的代码。所有检查都很便宜!

用法

放置惰性节点的理想位置是你应用程序的根。许多应用程序被设置为具有不同的视觉区域,例如页眉、侧边栏、搜索结果等。当人们正在修改其中一个区域时,他们很少修改其他区域。这为lazy调用创建了非常自然的分界线!

例如,在我的TodoMVC实现中,view被定义为:

view : Model -> Html Msg
view model =
  div
    [ class "todomvc-wrapper"
    , style "visibility" "hidden"
    ]
    [ section
        [ class "todoapp" ]
        [ lazy viewInput model.field
        , lazy2 viewEntries model.visibility model.entries
        , lazy2 viewControls model.visibility model.entries
        ]
    , infoFooter
    ]

请注意,文本输入、条目和控件都在独立的惰性节点中。因此,我可以在输入中输入任意个字符,而无需为条目或控件生成虚拟节点。它们不会改变!所以第一个窍门是尝试在应用程序的根部使用惰性节点。

在长条目列表中使用惰性也可能非常有用。在 TodoMVC 应用中,重点是向待办事项列表中添加条目。你可能会有数百个条目,但是它们的变化非常少。这是使用惰性的绝佳候选!通过将 viewEntry entry 切换成 lazy viewEntry entry,我们可以跳过大量几乎没用的分配。所以第二个窍门是尝试在各个条目变化不频繁的重复结构上使用惰性节点。

总结

接触 DOM 比正常用户界面发生的任何事情更昂贵。根据我的基准测试,你可以采用任何高级数据结构来实现想要的功能,但最终只有成功使用 lazy 才重要。

在下一页,我们将学习一项技巧,使用 lazy 实现更多功能!

个与 "" 相匹配的结果

    没有与 "" 相匹配的结果