本系列的上一篇文章组件对复用性有害?探讨了如何在前端开发中编写可复用的界面元素。本篇文章中将从性能和算法的角度比较 Binding.scala 和其他框架的渲染机制。
Binding.scala 实现了一套精确数据绑定机制,通过在模板中使用 bind 和 for/yield 来渲染页面。你可能用过一些其他 Web 框架,大多使用脏检查或者虚拟 DOM 机制。和它们相比,Binding.scala 的精确数据绑定机制使用更简单、代码更健壮、性能更高。
ReactJS 虚拟 DOM 的缺点
比如, 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 函数很复杂,这个过程就白白浪费了很多计算资源。
- ReactJS 框架比较虚拟 DOM 差异的过程,既慢又容易出错。比如,假如你想要在某个 <ul> 列表的顶部插入一项 <li> ,那么 ReactJS 框架会误以为你修改了 <ul> 的每一项 <li>,然后在尾部插入了一个 <li>。
这是因为 ReactJS 收到的新旧两个虚拟 DOM 之间相互独立,ReactJS 并不知道数据源发生了什么操作,只能根据新旧两个虚拟 DOM 来猜测需要执行的操作。自动的猜测算法既不准又慢,必须要前端开发者手动提供 key 属性、shouldComponentUpdate 方法、componentDidUpdate 方法或者 componentWillUpdate 等方法才能帮助 ReactJS 框架猜对。
AngularJS 的脏检查
除了类似 ReactJS 的虚拟 DOM 机制,其他流行的框架,比如 AngularJS 还会使用基于脏检查的定值算法来渲染页面。
脏检查算法和 ReactJS 有一样的缺点,无法得知状态修改的意图,这使得 AugularJS 必须反复执行 $digest 轮循、反复检查各个 ng-controller 中的各个变量。除此之外,AngularJS 更新 DOM 的范围往往会比实际所需大得多,所以会比 ReactJS 还要慢。
Binding.scala 的精确数据绑定
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 。
相关链接
- Binding.scala 项目主页
- Binding.scala • TodoMVC 项目主页
- Binding.scala • TodoMVC DEMO
- Binding.scala • TodoMVC 以外的其他 DEMO
- JavaScript 到 Scala.js 移植指南
- Scala.js 项目主页
- Scala API 参考文档
- Scala.js API 参考文档
- Scala.js DOM API 参考文档
- Binding.scala 快速上手指南
- Binding.scala API 参考文档
- Binding.scala 的 Gitter 聊天室
More than React 系列文章
《More than React(一)为什么ReactJS 不适合复杂交互的前端项目?》
《More than React(四)HTML 也可以静态编译?》
作者简介
杨博是 Haxe 和 Scala 社区的活跃贡献者,发起和维护的开源项目包括 protoc-gen-as3 、 Stateless Future 、 haxe-continuation 、 Fastring 、 Each 、 Microbuilder 、 Binding.scala 。杨博曾在网易任主程序和项目经理,开发过多款游戏。现在 ThoughtWorks 任 Lead Consultant,为客户提供移动、互联网、大数据、人工智能和深度学习领域的解决方案。
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论