@atry
2016-11-29T18:53:42.000000Z
字数 3148
阅读 1717
前端
binding.scala
data-binding
scala.js
web
本系列的上一篇文章组件对复用性有害?探讨了如何在前端开发中编写可复用的界面元素。本篇文章中将从性能和算法的角度比较 Binding.scala 和其他框架的渲染机制。
Binding.scala 实现了一套精确数据绑定机制,通过在模板中使用 bind
和 for
/yield
来渲染页面。你可能用过一些其他 Web 框架,大多使用脏检查或者虚拟 DOM 机制。和它们相比,Binding.scala 的精确数据绑定机制使用更简单、代码更健壮、性能更高。
比如, ReactJS 使用虚拟 DOM 机制,让前端开发者为每个组件提供一个 render
函数。render
函数把 props
和 state
转换成 ReactJS 的虚拟 DOM,然后 ReactJS 框架根据 render
返回的虚拟 DOM 创建相同结构的真实 DOM.
每当 state
更改时,ReactJS 框架重新调用 render
函数,获取新的虚拟 DOM 。然后,框架会比较上次生成的虚拟 DOM 和新的虚拟 DOM 有哪些差异,然后把差异应用到真实 DOM 上。
这样做有两大缺点:
state
更改,render
函数都要生成完整的虚拟 DOM. 哪怕 state
改动很小,render
函数也会完整计算一遍。如果 render
函数很复杂,这个过程就白白浪费了很多计算资源。<ul>
列表的顶部插入一项 <li>
,那么 ReactJS 框架会误以为你修改了 <ul>
的每一项 <li>
,然后在尾部插入了一个 <li>
。这是因为 ReactJS 收到的新旧两个虚拟 DOM 之间相互独立,ReactJS 并不知道数据源发生了什么操作,只能根据新旧两个虚拟 DOM 来猜测需要执行的操作。自动的猜测算法既不准又慢,必须要前端开发者手动提供 key
属性、shouldComponentUpdate
方法、componentDidUpdate
方法或者 componentWillUpdate
等方法才能帮助 ReactJS 框架猜对。
除了类似 ReactJS 的虚拟 DOM 机制,其他流行的框架,比如 AngularJS 还会使用基于脏检查的定值算法来渲染页面。
脏检查算法和 ReactJS 有一样的缺点,无法得知状态修改的意图,这使得 AugularJS 必须反复执行$digest
轮循、反复检查各个ng-controller中的各个变量。除此之外,AngularJS 更新 DOM 的范围往往会比实际所需大得多,所以会比 ReactJS 还要慢。
Binding.scala 使用精确数据绑定算法来渲染 DOM 。
在 Binding.scala 中,你可以用 @dom
注解声明数据绑定表达式。@dom
会自动把 =
之后的代码包装成 Binding
类型。
比如:
@dom val i: Binding[Int] = 1
@dom def f: Binding[Int] = 100
@dom val s: Binding[String] = "content"
@dom
既可用于 val
也可以用于 def
,可以表达包括 Int
、 String
在内的任何数据类型。
除此之外,@dom
方法还可以直接编写 XHTML,比如:
@dom val comment: Binding[Comment] = <!-- This is a HTML Comment -->
@dom val br: Binding[HTMLBRElement] = <br/>
@dom val seq: Binding[BindingSeq[HTMLBRElement]] = <br/><br/>
这些 XHTML 生成的 Comment 和 HTMLBRElement 是 HTML Node 的派生类。而不是 XML Node。
每个 @dom
方法都可以依赖其他数据绑定表达式:
val i: Var[Int] = Var(0)
@dom val j: Binding[Int] = 2
@dom val k: Binding[Int] = i.bind * j.bind
@dom val div: Binding[HTMLDivElement] = <div>{ k.bind.toString }</div>
通过这种方式,你可以编写 XHTML 模板把数据源映射为 XHTML 页面。这种精确的映射关系,描述了数据之间的关系,而不是 ReactJS 的 render
函数那样描述运算过程。所以当数据发生改变时,只有受影响的部分代码才会重新计算,而不需要重新计算整个 @dom
方法。
比如:
val count = Var(0)
@dom def status: Binding[String] = {
val startTime = new Date
"本页面初始化的时间是" + startTime.toString + "。按钮被按过" + count.bind.toString + "次。按钮最后一次按下的时间是" + (new Date).toString
}
@dom def render = {
<div>
{ status.bind }
<button onclick={ event: Event => count := count.get + 1 }>更新状态</button>
</div>
}
以上代码可以在ScalaFiddle实际运行一下试试。
注意,status
并不是一个普通的函数,而是描述变量之间关系的特殊表达式,每次渲染时只执行其中一部分代码。比如,当 count
改变时,只有位于 count.bind
以后的代码才会重新计算。由于 val startTime = new Date
位于 count.bind
之前,并不会重新计算,所以会一直保持为打开网页首次执行时的初始值。
有些人在学习 ReactJS 或者 AngularJS 时,需要学习 key
、 shouldComponentUpdate
、 $apply
、 $digest
等复杂概念。这些概念在 Binding.scala 中根本不存在。因为 Binding.scala 的 @dom
方法描述的是变量之间的关系。所以,Binding.scala 框架知道精确数据绑定关系,可以自动检测出需要更新的最小部分。
本文比较了虚拟 DOM 、脏检查和精确数据绑定三种渲染机制。
AngularJS | ReactJS | Binding.scala | |
---|---|---|---|
渲染机制 | 基于脏检查的定值算法 | 虚拟DOM | 精确数据绑定 |
数据变更时的运算步骤 |
|
|
|
检测页面更新范围的准确性 | 不准 | 默认情况下不准,需要人工提供`key`和`shouldComponentUpdate`才能准一点 | 准 |
需要前端工程师理解多少API和概念才能正确更新页面 | 很多 | 很多 | 只有`@dom`和`bind`两个概念 |
总体性能 | 非常差 | 差 | 好 |
这三种机制中,Binding.scala 的精确数据绑定机制概念更少,功能更强,性能更高。我将在下一篇文章中介绍 Binding.scala 如何在渲染 HTML 时静态检查语法错误和语义错误,从而避免 bug 。