分类: 新兴技术

More than React系列文章:

More than React(一)为什么ReactJS不适合复杂的前端项目?

More than React(二)React.Component损害了复用性?

More than React(三)虚拟DOM已死?

More than React(四)HTML也可以静态编译?


本系列的上一篇文章《React.Component损害了复用性?》探讨了如何在前端开发中编写可复用的界面元素。本篇文章将从性能和算法的角度比较 Binding.scala 和其他框架的渲染机制。

Binding.scala 实现了一套精确数据绑定机制,通过在模板中使用 bindfor/yield 来渲染页面。你可能用过一些其他 Web 框架,大多使用脏检查或者虚拟 DOM 机制。和它们相比,Binding.scala 的精确数据绑定机制使用更简单、代码更健壮、性能更高。

ReactJS虚拟DOM的缺点

比如, ReactJS 使用虚拟 DOM 机制,让前端开发者为每个组件提供一个 render 函数。render 函数把 propsstate 转换成 ReactJS 的虚拟 DOM,然后 ReactJS 框架根据 render 返回的虚拟 DOM 创建相同结构的真实 DOM。

每当 state 更改时,ReactJS 框架重新调用 render 函数,获取新的虚拟 DOM 。然后,框架会比较上次生成的虚拟 DOM 和新的虚拟 DOM 有哪些差异,进而把差异应用到真实 DOM 上。

这样做有两大缺点:

  1. 每次 state 更改,render 函数都要生成完整的虚拟 DOM,哪怕 state 改动很小,render函数也会完整计算一遍。如果 render 函数很复杂,这个过程就会白白浪费很多计算资源。
  2. 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 ,可以表达包括 IntString 在内的任何数据类型。

除此之外,@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 生成的 CommentHTMLBRElement 是 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 时,需要学习 keyshouldComponentUpdate$apply$digest 等复杂概念。这些概念在 Binding.scala 中根本不存在。因为 Binding.scala 的 @dom 方法描述的是变量之间的关系。所以,Binding.scala 框架知道精确数据绑定关系,可以自动检测出需要更新的最小部分。

结论

本文比较了虚拟 DOM 、脏检查和精确数据绑定三种渲染机制。

AngularJS ReactJS Binding.scala
渲染机制 脏检查 虚拟DOM 精确数据绑定
数据变更时的运算步骤
  1. 重复检查数据是否更改
  2. 大范围更新页面,哪怕这部分页面根本没有修改
  1. 重新生成整个虚拟DOM
  2. 比较新旧虚拟DOM的差异
  3. 根据差异更新页面
  1. 直接根据数据映射关系,更新最小范围页面
检测页面更新范围的准确性 不准 默认情况下不准,需要人工提供keyshouldComponentUpdate才能准一点
需要前端工程师理解多少API和概念才能正确更新页面 很多 很多 只有@dombind两个概念
总体性能 非常差

这三种机制中,Binding.scala 的精确数据绑定机制概念更少,功能更强,性能更高。我将在下一篇文章中介绍 Binding.scala 如何在渲染 HTML 时静态检查语法错误和语义错误,从而避免 bug 。

相关链接

 


你想看到的洞见,都在这里

wechat

Share

发表评论

评论

  1. 为何不直接自己开发个Js库,使用Scala.js,意味着成堆成堆的scala语法概念。这么看来Binding.scala绝对比React或者Angular复杂,因为它除了js语法,还包含scala的大量语法糖,甚至为了兼容ng和react还要知道scala-react,scala-angular等等,瞬间变成万金油,PHP的高难度学习版。

    纵观Scala开发人员为了节省几行代码,微弱的性能,往往要花费大量的时间去学习和思考如何书写,而且很难做到熟练使用Scala,最奇葩的是:基于其他语言之上,也就是说,如果使用Binding.scala,肯定要学习Reactjs,Angular,甚至ES5、6等等,这样再学习Binding.scala的意义只为了节省几行代码,微弱的性能。性价比非常低廉(不如省点时间照顾家人)。

    • 不用JavaScript的原因是因为JavaScript写不出来。JavaScript缺乏类型系统,对函数式编程的支持也很孱弱,没办法实现完整的Monad。Scala的类型系统虽然比idris弱,类型推断也比Haskell差。但在主流语言中,却是唯一能够实现 @dom 注解和 bind 语法的语言。

      JavaScript 的生态环境碎片化,缺乏整套解决方案,开发者需要自己来组合gulp、npm、bower、webpack、babel,才能实现 Scala.js 和 Binding.scala 内置的很多很简单的功能。从节省时间照顾家人的角度讲,推荐你试一下 Binding.scala 的整套解决方案。

      我可以推荐一套全栈Scala的项目模板。https://github.com/Algomancer/Full-Stack-Scala-Starter 对新手来说很容易上手。

      • framework 的一揽子解决方案(如: Angular, 全家桶)和 library 的灵活组合方式 (如: React, 乐高)的争论, 是继”vim 好,还是 emacs 好”, “java 好, 还是 .net 好” 之后的又一个世纪难题.

        • TypeScript的类型系统还是太弱了,不支持type class,也就没办法支持monad;不支持monad,也就没办法用ThoughtWorks Each来做DSL。不用ThoughtWorks Each的话,就没办法在保证类型安全的前提下把“数据绑定”实现得很简洁。
          总之TypeScript无论是语言本身还是生态环境,都不够完善,缺了太多typelevel编程的基本功能。

          相比之下,Scala社区流行的函数式编程库,比如Scalaz、Shapeless、Cats等,几乎不用改一行代码就能在Scala.js运行。所以有了这些成熟框架的支持,如果要在前端开发中享受静态类型检查的好处,可能没有比Scala.js更好的生态环境了。

          至于Vue.js……熊喵同学,你说这些时真的认真比较过Binding.scala和Vue.js吗?
          我觉得你可以用二者写一下这几篇文章的示例代码,然后你就能体会到Vue.js和Binding.scala相比,差距有多大。

          • 其实Reactive Programming在Scala这样的函数式编程语言中,可以直接套用范畴论中的概念,而这些概念,早在多年以前就有很成熟的实践了。所以我用monad实现Binding.scala就挺容易的。

            相比之下,ReactJS受限于孱弱的JavaScript生态环境,就只能自己重写一个JSX编译器,再用虚拟DOM这种笨办法模拟FRP的效果,实现起来很费劲,代码比Binding.scala多了几十倍,却只支持Binding.scala一点零头的功能。我看着觉得挺心酸的。

    • 你不理解有一个强类型系统的好处所以才这么想,语言的问题不是加几个库就能解决的。
      scalajs的核心概念并不多,入手应该不难。如果不考虑兼容现有项目,scalajs-react这些库完全不用去学,手动写scala代码就能更优雅的解决大部分问题。

  2. 没接触过scala,但是有强类型语言的功底, 就凭类型检查这点就喜欢上了Scala.js, 抽时间好好研究, 谢谢楼主的分享, 起码对前端的认识更深了一层, 希望能在研究一段时间之后可以和楼主进行相互受益的讨论!