高性能 HTML -- Elm 中的 Virtual DOM

原文: http://elm-lang.org/blog/Blazing-Fast-Html.elm
作者: Evan Czaplicki


新的 elm-html 模块允许在 Elm 当中直接使用 HTML 和 CSS.
想用 flexbox? 想要沿用已有的样式表? Elm 现在让这愉快而且飞快.
比如, 重写 TodoMVC 应用时, 代码非常简单,
初步的性能测试表明对比流行框架来说是非常快的:

elm-html 和 Mercury 两个项目都是基于 virtual-dom 项目的,
这就是性能高的原因. 文章前半部分将介绍 virtual DOM,
以及说明 putiry(纯函数)和不可变性(immutability) 是怎么提高性能的.
这可以解释为什么 Om, Mercury, 还有 Elm 都达到了如此高的性能.

性能是一个好的收获, 不过实际的好处是这个方案使得代码更简单, 更容易理解和维护.
简单说, 这使得创建可复用的 HTML 组件和抽象共用模式更容易了.
所以维护大型代码的人应该对 virtual DOM 的方案保持兴趣.

这个方案对于想要开始用 Elm 的人们来说也是好消息.
就是说用 Elm 同时你也能保留原来的 CSS 还有以前习惯的设计和开发流程.
在项目当中使用 Elm 也更简单了. 看一下是怎么工作的.

Virtual DOM

这个类库基于 "virtual DOM" 的想法.
不直接地对 DOM 进行操作, 而是给每个 frame 构建一个抽象版本的 DOM,
node 函数来创建一个简单的数据结构来表示:

node : String -> [Attribute] -> [CssProperty] -> [Html] -> Html

这里声明了一个标签, 一组 HTML 属性, 一组 CSS 属性, 还有一组子节点.
比如, 用 node 来构建简单的 profile 组件, 显示用户的头像和名字:

profile : User -> Html
profile user =
    node "div" [ "className" := "profile" ] []
      [ node "img" [ "src" := user.picture ] [] []
      , node "span" [] [] [ text user.name ]
      ]

注意这里定义了一个 class, 所以整体我们能通过 CSS 来定制样式.
搭配 Elm 的模块系统, 抽象出通用的模式和重用的代码就方便了.
你可以在这里查看完整的 API 和文档,
在可复用组件的章节中我们将看到更多使用的例子.

让 Virtual DOM 变快

Virtual DOM 听起来很慢是不是? 每个 frame 创建一整个新的 scene?
这项技术其实在游戏产业中广泛使用并且对于 DOM 更新也表现得很好,
其中可以看到两种相关的技术: diff 和惰性求值(laziness).

React 普及了 diff 的方法来判断 DOM 需要做怎样的更新.
Diff 意味着在当前的 virtual DOM 和新的 virtual DOM 之间对比出变化量.
这听起来很奇幻, 实际上就是很简单的过程.
首先列出所有的差别, 比如某个

颜色的改变, 或者添加了一个新的.
所有差别找出来以后, 作为指令对 DOM 进行更新,
这里通过 requestAnimationFrame 合并到一个大的操作当中.
这里对 DOM 进行操作的步骤得以完成, 并且一切都是飞快的.
你可以专心写代码, 让代码更好懂, 更好维护.

这个方案给 Elm 带来了完美的支持 HTML 和 CSS 的方案!
而且, Elm 对 纯函数(purity)和不可变性(immutability)已经有了很好的设施,
这些是对 diff 性能进行优化的关键.

根据在 React 和 Om 当中得到的, 惰性计算和 diff 可以大幅提升性能,
尤其是有不可变的数据结构作为支持的时候.
比如, 渲染一个 tasks 的列表:

todoList : [Task] -> Html
todoList tasks =
    node "div" [] [] (map todoItem tasks)

可以想见, 其中很多更新, 任务内容是不会改变的. 没有数据改变, view 也就不用改变.
这时候使用惰性计算就很合适了:

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

todoWidget : State -> Html
todoWidget state =
    lazy todoList state.tasks

相对于每一个 frame 都调用 todoList 的函数,
我们来判断 state.tasks 相对上一个 frame 是否有更新.
没有更新的话, 所有步骤跳过. 没有任何必要调用函数, 做 diff, 修改 DOM!
这些优化是安全的, 因为函数式纯函数, 数据是不可变的.

  • 纯函数(purity) 表示 todoList 函数对于相同的输入总是有相同的计算结果.
    所以如果我们知道 state.tasks 是相同的, 就可以整个跳过 todoList 执行.

  • 不可变性(Immutability)使得判断数据"一致"非常简单.
    不可变性保证了当两个的引用是一致时, 他们从结构上也将是一致的.

所以要确定 todoList 和 state.tasks 是否跟上一个 frame 一致只要判断其引用就可以了.
这个开销非常小, 如果又是一致, 惰性函数经常可以省掉大量工作.
这是个很简单的手法, 却能够大幅提升性能.

如果在关注 Elm, 你大概能注意到一个模式: 纯函数(purity)和不可变性是很重要的.
阅读 hot-swapping in Elm 和 time traveling debugger 来了解更多.

可复用组件

这个方案让创建可复用的组件变得很容易了.
比如, 用户 profile 的列表可以像这样漂亮地抽象出来:

import Html (..)

profiles : [User] -> Html
profiles users =
    node "div" [] [] (map profile users)

profile : User -> Html
profile user =
    node "div" [] []
    [ node "img" [ "src" := user.picture ] [] []
    , text user.name
    ]

这样我们就用了 profile 组件, 接受一个用户的数组, 返回 HTML.
这在哪里都能用, 比模板引擎更好的是, 可以用 Elm 任意地辅助创建组件.
这样还可以开始为社区创建通用组件和模式.

如果想要创建一些复杂的样式, 这些也可以被抽象出来复用.
下面的例子定义了的 font 和 background, 可以在任意的节点里混合匹配使用.

-- small reusable CSS properties
font : [CssProperty]
font =
    [ "font-family" := "futura, sans-serif"
    , "color"       := "rgb(42, 42, 42)"
    , "font-size"   := "2em"
    ]

background : [CssProperty]
background =
    [ "background-color" := "rgb(245, 245, 245)" ]

-- combine them to make individual nodes
profiles : [User] -> Html
profiles users =
    node "div" [] (font ++ background) (map profile users)

这样创建可复用的组件和抽象出通用模式就极为简单了, 但我们还能做更多!

抽象的自由度

当我在写 Elm 项目的前身时, HTML 20 岁了, 人们还要看三篇博客五个 StackOverflow 答案,
就为了搞清楚怎么让内容可以纵向居中.
我最初的目标是彻底抽象思考 GUI. 如果我们能重来, Web 编程会是什么样子?

elm-html 对与这个目标出来很大的力.
首先, 它让 HTML 和 CSS 能被操作, 那么最新的功能的有点都能被用过来
其次, 它让创造新的抽象成为了可能.

这意味着 HTML 和 CSS 成为了构建更漂亮的抽象的城砖.
比如, 可能通过这个库来重新构建 Elm 的标签抽象.
但是更重要的是, 任何人都能试验新的办法来让 view 变得更模块化, 更愉悦.

Paul Chiusano 在他的 provocative post on CSS 很好得解释了这种期待.

我对于 Elm 的目标依然是重新思考 Web 编程, 以一种怪异的特殊的方式..
支持 HTML 和 CSS 是这个方向上的一大步. 我对于借助 elm-html 能做的感到很高兴!

关于架构

正如其他的方案, 人们可能会问的第一个问题是"在大项目里看起来怎么样?"
这个通用的方案跟用 Om 或者 Facebook 的 Flux 架构大型应用是一码事.
我不正式地在 how this works in Elm 里做了概述,
计划很快会写正式的文档和例子出来.

感谢

感谢 React 和 Om 发现和推广这些技术.
特别感谢 Sebastian Markbage, David Nolen, Matt Esch, and Jake Verbaten 帮我理解了他们.

尤其要谢谢 Matt Esch 和 Jake Verbaten, 他们创建了 virtual-dom 和 mercury.
这是我这个类库的基础, 也完完全全是高性能的来源.

你可能感兴趣的:(elm,dom,react.js)