为什么 ReactJS 不适合复杂的前端项目?

《More than React》系列的文章会一共分为五篇。本文是第一篇,介绍用ReactJS开发时遇到的种种问题。后面四篇文章的每一篇将会分别详细讨论其中一个问题,以及Binding.scala如何解决这个问题。

背景介绍

去年 4 月,我第一次在某个客户的项目中接触到ReactJS 。

我发现ReactJS要比我以前用过的AngularJS简单很多,它提供了响应式的数据绑定功能,把数据映射到网页上,使我可以轻松实现交互简单的网站。

然而,随着我越来越深入的使用ReactJS,我发现用ReactJS编写交互复杂的网页很困难。 我希望有一种方式,能够像ReactJS一样简单解决简单问题。此外,还要能简单解决复杂问题。

于是我把ReactJS用Scala重新写了一个。代码量从近三万行降到了一千多行。

用这个框架实现的TodoMVC应用,只用了154行代码。而用ReactJS实现相同功能的TodoMVC,需要488行代码

下图是用Binding.scala实现的TodoMVC应用。

这个框架就是Binding.scala

问题一:ReactJS组件难以在复杂交互页面中复用

ReactJS中的最小复用单位是组件。ReactJS的组件比AngularJS的Controller和View 要轻量些。 每个组件只需要前端开发者提供一个 render 函数,把 propsstate 映射成网页元素。

这样的轻量级组件在渲染简单静态页面时很好用, 但是如果页面有交互,就必须在组件间传递回调函数来处理事件。

我将在《More than React(二)组件对复用性有害?》中用原生DHTML API、ReactJS和Binding.scala实现同一个需要复用的页面,介绍Binding.scala如何简单实现、简单复用复杂的交互逻辑。

问题二:ReactJS的虚拟DOM 算法又慢又不准

ReactJS的页面渲染算法是虚拟DOM差量算法。

开发者需要提供 render 函数,根据 propsstate 生成虚拟 DOM。 然后 ReactJS 框架根据 render 返回的虚拟 DOM 创建相同结构的真实 DOM.

每当 state 更改时,ReacJS 框架重新调用 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 框架猜对。

我将在《More than React(三)虚拟DOM已死?》中比较ReactJS、AngularJS和Binding.scala渲染机制,介绍简单性能高的Binding.scala精确数据绑定机制。

问题三:ReactJS的HTML模板功能既不完备、也不健壮

ReactJS支持用JSX编写HTML模板。

理论上,前端工程师只要把静态HTML原型复制到JSX源文件中, 增加一些变量替换代码, 就能改造成动态页面。 理论上这种做法要比Cycle.js、Widok、ScalaTags等框架更适合复用设计师提供的HTML原型。

不幸的是,ReactJS对HTML的支持残缺不全。开发者必须手动把classfor属性替换成classNamehtmlFor,还要把内联的style样式从CSS语法改成JSON语法,代码才能运行。 这种开发方式下,前端工程师虽然可以把HTML原型复制粘贴到代码中,但还需要大量改造才能实际运行。 比Cycle.js、Widok、或者、ScalaTags省不了太多事。

除此之外,ReactJS还提供了propTypes机制校验虚拟DOM的合法性。 然而,这一机制也漏洞百出。 即使指定了propTypes,ReactJS也不能在编译前提前发现错误。只有测试覆盖率很高的项目时才能在每个组件使用其他组件时进行校验。 即使测试覆盖率很高,propTypes仍旧不能检测出拼错的属性名,如果你把onClick写成了onclick, ReactJS就不会报错,往往导致开发者额外花费大量时间排查一个很简单的bug。

我将在《More than React(四)HTML也可以编译?》中比较ReactJS和Binding.scala的HTML模板,介绍Binding.scala如何在完整支持XHTML语法的同时静态检查语法错误和语义错误。

问题四:ReactJS与服务器通信时需要复杂的异步编程

ReactJS从服务器加载数据时的架构可以看成MVVM(Model–View–ViewModel)模式。 前端工程师需要编写一个数据库访问层作为Model,把ReactJS的state当做ViewModel,而render当做View。 Model负责访问数据库并把数据设置到state(即View Model)上,可以用Promise和fetch API实现。 然后,render,即View,负责把View Model渲染到页面上。

在这整套流程中,前端程序员需要编写大量闭包组成的异步流程, 设置、访问状态的代码五零四散, 一不小心就会bug丛生,就算小心翼翼的处理各种异步事件,也会导致程序变得复杂,既难调试,又难维护。

我将在《More than React(五)为什么别用异步编程?》中比较ReactJS和Binding.scala的数据同步模型,介绍Binding.scala如何自动同步服务器数据,避免手动异步编程。

结论

尽管Binding.scala初看上去很像ReactJS, 但隐藏在Binding.scala背后的机制更简单、更通用,与ReactJS和Widok截然不同。

所以,通过简化概念,Binding.scala灵活性更强,能用通用的方式解决ReactJS解决不了的复杂问题。

比如,除了上述四个方面以外,ReactJS的状态管理也是老大难问题,如果引入Redux或者react-router这样的第三方库来处理状态,会导致架构变复杂,分层变多,代码绕来绕去。而Binding.scala可以用和页面渲染一样的数据绑定机制描述复杂的状态,不需要任何第三方库,就能提供服务器通信、状态管理和网址分发的功能。

以下表格中列出了上述Binding.scala和ReactJS的功能差异:

Binding.scala ReactJS
复用性 最小复用单位 方法 组件
复用难度 不论交互内容还是静态内容都容易复用 容易复用静态内容组件,但难以复用交互组件
页面渲染算法 算法 精确的数据绑定 虚拟 DOM
性能
正确性 自动保证正确性 需要开发者手动设置 key 属性,不然复杂的页面会错乱。
HTML 模板 语法 Scala XML 字面量 JSX
是否支持 HTML 或 XHTML 语法 完整支持 XHTML 残缺支持。正常的 XHTML 无法编译。开发者必须手动把 classfor 属性替换成 classNamehtmlFor,还要把内联的 style 样式从 CSS 语法改成 JSON 语法。
如何校验模板语法 自动编译时校验 运行时通过 propTypes 校验但无法检测简单的拼写错误。
服务器通讯 机制 自动远程数据绑定 MVVM + 异步编程
实现难度 简单 复杂
其他 如何分派网址或者锚点链接 支持把网址当成普通的绑定变量来用,无需第三方库。 不支持,需要第三方库 react-router
功能完备性 完整的前端开发解决方案 本身只包含视图部分功能。需要额外掌握 react-router 、 Redux 等第三方库才能实现完整的前端项目。
学习曲线 API 简单,对没用过 Scala 的人来说也很好懂 上手快。但功能太弱导致后期学习第三方库时曲线陡峭。
Binding.scala ReactJS

两个多月前,我在Scala.js的论坛发布Binding.scala时,当时Scala.js社区最流行的响应式前端编程框架是Widok。Tim Nieradzik是Widok的作者。他在看到我发布的框架后,称赞这个框架是Scala.js社区最有前途的 HTML 5渲染框架。

他是对的,两个月后,现在Binding.scala已经成为Scala.js社区最流行的响应式前端编程框架。

Awesome Scala网站对比了Scala的响应式前端编程框架,Binding.scala的活跃程度和流行度都比Udash、Widok等其他框架要高。

我在最近的几个项目中,也逐渐放弃JavaScript和ReactJS,改用Scala.js和Binding.scala搭建新时代的前端技术栈。

相关链接

1 5 收藏 15 评论

关于作者:ThoughtWorks

ThoughtWorks是一家全球IT咨询公司,追求卓越软件质量,致力于科技驱动商业变革。擅长构建定制化软件产品,帮助客户快速将概念转化为价值。同时为客户提供用户体验设计、技术战略咨询、组织转型等咨询服务。 个人主页 · 我的文章 · 72 ·   

相关文章

可能感兴趣的话题



直接登录
最新评论
  • Nicky立   2016/08/17

    作者完全没有考虑,时间,效率,成本等个个方面的因素,并不是性能好一定要去用。C语言性能更好为什么不全用C去写。jquery性能差但是用的人很多。什么都不考虑就说好,完全一个无脑者。

    • 杨博 Consultant 2016/08/19

      您可能没看完文章。本文主要提到了ReactJS的四个问题,以及其他若干小问题。其中只有第二个问题是性能问题。

      希望您看完文章之后多给我一点反馈,谢谢!

  • Nicky立   2016/08/18

    昨天还看了下,那语言底层用了16M代码量来转换js。还是跑在jvm上。必须了解java,新手根本没必要花那么大的代价去学习。

    • 杨博 Consultant 2016/08/19

      您好,转换后的js是很小的,比如TodoMVC的代码是96.6KB(http://todomvc.com/examples/binding-scala/js/js-opt.js),并不是16M。如果您指的是Scala编译器,那么大概是100M左右,也不是16M。

      还有,Scala.js是跑在浏览器的,并不是JVM。

      学习Scala并不需要了解Java。这是谣言。

      希望您不传谣不信谣不造谣。谢谢!

  • 馒の头/wq 前端 2016/08/21

    看完文章,说的很实在啊,

    现在就在开发一个react 项目,异步编程在更新数据  确实较为复杂。

    如果能在在一个组件上为数据绑定了api在设置了 他的 同步时机 然后自动获取 岂不简单

    不知道此项目 是怎么做‘自动远程数据绑定’的。

    我对此项目最大的疑问是 得用scala 语言来写? 只会js 啊。语言只是一种表现手法,js界现在没有 解决这些问题的 套件么

    • 杨博 Consultant 2016/08/23

      语言不仅是表现手法,更是思维工具。

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

  • 馒の头/wq 前端 2016/08/21

    还有一个问题是  前端的通用问题是

    react js 实在太大,编译 完 随随便便上m(虽然只有一张图片大小,但想着网络环境差就变得缓慢时心里就不舒服)

    所以说简单项目上 reactjs 也不是很好啊。

    看到 demo中 几十k 的代码 很羡慕

  • -   2016/08/22

    赞赏作者的创新精神,不过猜测作者主要是后端开发人员吧。前端开发很少人会用scala.js去把sacla转换为js来编码的。前端开发肯定是js框架为主的。还有标题起得是不是有点过了,人家facebook的网站够大了吧。人家也是用的react框架。怎么会不适合。还有判断一个框架的好坏,不仅仅是看框架本身,还有看社区,看框架扩展性,react有大量的人员参与开发,有路由模块,各种组件。作者推广自己的框架希望在语言上还是需要谦虚点。不要把新手带进坑里。

    • 杨博 Consultant 2016/08/23

      谢谢您的反馈。

      我很理解JavaScript程序员维护自己喜爱语言的心情。其实我自己在刚接触ReactJS时,我也是ReactJS的簇拥,也在团队推动大家学习ReactJS。

      我也很同意判断框架的好坏,更重要的是生态环境。

      不过如果比生态环境的话,Binding.scala的生态环境其实比ReactJS大概好两三倍吧。比如Binding.scala就利用了scalaz、each这些函数式编程的基础设施,JavaScript社区根本写不出来。再如Binding.scala的构建脚本要比npm+gulp+webpack简单得多。典型的Scala.js项目的sbt配置脚本大概是同样功能的gulp脚本的五分之一到十分之一。

      Binding.scala本身非常精简,但却提供了完整的前端开发解决方案。相比之下,ReactJS本身只包含视图部分功能,需要额外掌握 react-router 、 Redux 等第三方库才能实现完整的前端项目。比如Binding.scala内置的bind功能就可以实现极为灵活的路由功能(参见 https://github.com/ThoughtWorksInc/todo/blob/8fb2839/js/src/main/scala/com/thoughtworks/todo/Main.scala#L41 )。此外,这个系列的文章的第五篇会比较ECMAScript 2015基于Promise的异步编程和Binding.scala的远程数据绑定。

      我挺理解一般前端程序员对陌生技术栈的畏惧感。不过您可以先学一下试试,毕竟门外汉指着大门说里面有坑,我觉得可能可信度不高。到时候如果您真的发现有坑,希望您能帮我指正一下。

  • luobotang 前端工程师 2016/08/23

    怎么感觉全是在吹自己….

  • 我觉得博主对React的了解似乎并不深,你说的第2点,虚拟DOM算法慢和不准,我无法赞同,针对你说的ul下的li的问题,可以在每一个li上加key来避免,其次,虚拟DOM算法花费的时间相对于复杂的DOM操作话费的时间,实在不值一提,虽然大组件下虚拟DOM确实也很花费时间,但是有Immutable.js来优化性能。针对博主的第3点,Redux,不解释。我的观点不一定对,大神请轻拍

  • 看了作者一篇半文章,<虚拟dom已死>,和这里半篇。别拿todo list说事了,react.js是有了正式产品之后,为了初学者更快入门都写个玩具出来。难不成你拿这个todolist demo当产品。。。。如果成功,那时候我会敬佩你的运营

  • 其他点不说了  就说第三点吧, 作者一定是没有用过  typescript的 tsx 来写 jsx .  如果能再配合 visualStudio  ,那编码体验没有挑剔的了

跳到底部
返回顶部