为什么我放弃Redux

声明:我是一个前端新手(或者连新手都算不上),纯粹玩票性质的做个小项目,文章仅表达个人在使用Redux过程中的一些思考,里面有一些问题是因为我对Redux细节掌握的不够彻底造成的误用带来的,比如第五楼的评论就对这种问题做了很好的解答,希望不会对其他前端新手造成一些误导。
相关技术背景:曾经做过一些桌面GUI程序的开发和手机端APP的开发,现在主要是基于Elixir和Scala开发后端应用。
背景 这两周要写一个基于Web的下载管理工具,即可以通过网页远程控制家里的NAS设备进行下载的功能。以前没怎么接触过网站开发,这个任务对我来说是一个不小的挑战。
看了些文档,用Ruby on Rails搭了用户管理系统和设备绑定系统,又用Mqtt协议实现了下载器的远程控制,难度不高,两三天就处理好了。本以为任务很快就能做完,没想到掉进了前端开发的深坑里。周末闲来无事,聊一下这两周里对前端开发的一些感悟。
(前面有很多废话,各位大神可以直接跳到正文部分观看)
揭开神秘的面纱 以前对前端的接触,仅限于买过一套Bootstrap模板,做了个简单的产品宣传页。这次因为要做设备管理,远程控制等一系列工作,涉及到很多内容,就想趁此机会,系统的学习一下前端开发。
沐浴更衣之后,先去runoob上把Html相关的内容从头至尾过了一遍,基本上掌握了Html的设计脉络。因为以前设计过一款聊天软件,当时为了好玩,想在聊天页面里面插入各种动态元素,如表情,气泡,动画之类的东西,所以自己撸了一个排版引擎,为了实现流畅滑动,就必须要用到局部渲染技术,即只渲染用户能看到的部分,然后就计算各种行间距,甚至字体间距之类东西,看需要渲染哪个部分的内容,当时也遇到过块元素与内联元素的问题,不过想的没有Html这么清楚。现在看Html的文档,有种恍然大悟的感觉。
看完了Html,没看CSS那部分,就先奔着这次的主角——JavaScript去了。以前写QML的时候用过一些JavaScript,语法方面还算熟悉。接着找了几篇帖子,看了一下JavaScript与Html是如何结合的,基本上流程就是先根据Html建立一个DOM树,然后再用JavaScript在建好的树上做一些敲敲打打的工作。
这中间涉及到三件事

  1. 如何快速定位
  2. 找到元素之后如何修改元素
  3. 如何绑定元素事件
有了大概轮廓,再看jQuery的文档也就清晰了,基本上就是围绕着这三件事封装了一些API,具体的API文档可以先不看,用到的时候再查就好了,毕竟人的脑容量有限,看太多,前面的就又忘了。学习jQuery并不是为了用他开发,而是为了理解前端开发打基础,具体的业务逻辑,可以用一些更上层的开发库来解决。
AngularJS VS React 在学习前端开发的过程中,AngularJS和React出现的频率最高,也是目前最火的两个前端开发框架。准备在这两个里面选择一个,然后就可以开整了。
先对比一下两个框架的设计哲学:
这是AngularJS版的Hello world
名字 :
Hello {{name}}

这是React版的Hello world

同样是实现输入框绑定的功能,两者的代码量差别很大,而且React还引入了JSX语法,造成了不能直接套用Html静态模板的问题,另外在我心里,Google的地位就像游戏界的暴雪一样,他做的库肯定比Facebook家的好,G家的Golang就是我最爱的编程语言呢。我很聪明,所以甭想骗我,看人家AngularJS设计的多简洁,肯定好用,AngualrJS我来啦。
AngularJS的坑 开始一切都很好,用上了AngularJS腰不酸了,背不疼了,腿也不抽筋了。显示设备列表是吧,走着,一个API请求再加一个ng-repeat轻松搞定,G家的东西效率就是高,连http请求都提前给封装好了呢。
等等...这是什么情况?
我要打开设备配置,我要添加下载任务,我要创建很多对话框,为什么要先在Body里面定义对话框模板,然后再通过事件绑定调用,中间还要做依赖注入和scope管理,为什么我不能直接定义一个组件,按钮点击的时候动态生成一下。毕竟只有点击这个按钮的时候才会跳出这个控制面板啊,为什么要污染全局空间呢?我的代码量越来越大了,怎么感觉管理动态元素越来越难,不断地声明模板,不断地携带者变量来回穿梭于不同的scope,说好的组件化编程呢?
回归React 迷茫了,再这样下去不是办法啊,开发效率越来越低了怎么办,苦恼ing... 不想干活了,先看看React的文档吧,看看他有没有好主意。为什么当初看他的文档感觉一直在打算坑我,现在看却觉得句句都是肺腑之言呢,连里面的标点符号都变得性感了呢。人啊,只有踩过坑之后才知道珍惜。
组件化,组件化,组件化,这不正是我需要的功能吗?原来React在设计之初就把它放在了第一位,而我却没有重视起来,惭愧惭愧。单向数据流和双向绑定看似差别不大,但在复杂的场景里,单向数据流可以让每次状态变更都是可预测的,大大减轻了思维复杂度。
用了两天时间把项目用React重构了一下,另外学习了Webpack管理工具,ES6语法,越写越清晰,越写越舒服,果然,选对了合适的工具,开发变得简单多了呢。
AngularJS和React的适用环境 根据这几天的项目经验来看,如果你的项目里涉及到大量的表单处理,那么AngularJS是最简单,也是最高效的开发工具。如果你的项目涉及到很多对话框,组件,组件与组件之间的消息传递的话,React是最好的框架,并不是代码量变小,而是让你的思路更顺了。AngularJS适合处理线性逻辑,在复杂的多对多逻辑处理上,React具备更好的结构。
正文 前面的都是废话,终于要进入正文了(别怪我,谁让我从小作文不及格呢)。React可以很好地解决组件定义问题,但是如何解决组件与组件之间的通信问题呢? 作为一个前端新手,选一个成熟框架是最稳妥的方案,搜索了大量资料之后,选择了Redux。
Redux的模式还是很清晰的,通过数据做事件同步,所有的数据都维护在一个全局的store里,通过Action 声明信号,通过Reducer设计状态变化逻辑(基本上就是业务逻辑),深入贯彻了单向数据流的思想,都是先触发action,进而reduce改变状态,然后将改变好的状态刷新到UI上,将UI设计进行了简化。
我按照Redux的思想设计了项目的事件流程,开始只是觉得很啰嗦,后来渐渐觉得思维开始变得不顺,暴露出了越来越多的问题,虽然都可以解决,但是就是觉得别扭,就好像用AngularJS设计组件一样,虽然可以设计出来,但是总觉得有股劲憋着难受。
1. 以状态来同步组件的局限性 首先,Redux只能同步状态,不能同步事件,但是有些事件是瞬时性的,用状态来模拟会变得很别扭,比如我的Mqtt客户端检测到用户设备1中的id为123456的任务下载完成了,我应该提示消息组件发出一个提示信号,请问应该怎么设计一个state表示某个任务下载完成这个事件呢?
2. 数据初始状态定义分散 我有一个设备管理器组件,负责显示用户的设备列表以及添加,删除设备和修改设备配置。按照OO的思想,设备数据应该和设备管理组件绑定在一起,根据数据设计一些方法,来决定UI显示和事件动作。现在我必须把状态初始化定义在Reducer里面,这样可以保证store在初始化的时候数据永远是合法的。这就造成了一个问题,如果设备管理器组件不知道初始状态是何结构,怎么设计交互界面,这说明设备管理器组件和初始状态的绑定关系更加紧密,而Redux的设计里,必须把初始化状态与UI分离,这样当我重新定义UI的时候,还要在Reducer里面去修改状态结构,容易造成状态混乱。
3. 全局变量造成Bug难以追踪 这是最讽刺的地方,Redux设计的初衷就是将数据流统一起来,可以快速的定位Bug。但是我觉得,这样强行将所有逻辑从一个树形结构拍扁放进一个全局变量的行为是非常奇葩的。先不说这会造成什么问题,从目前我接触过的所有GUI开发库来说(我觉得一个复杂SPA页面的结构跟GUI开发非常类似),没有一个会推崇用一个全局变量来同步不同模块的状态。不知道为什么在Web开发领域会流行全局变量的模式。
接着我们来看看为什么会造成问题,首先,将处理逻辑从顶层控件中剥离放入Reducer中会造成Reducer臃肿,虽然UI层看上去貌似清晰了,但是Reducer会变得越来越大。理论上这样可以帮助我们定位state相关的bug应该出现在action调用或者reducer处理中,首先action调用可能出现在各个模块,这个定位难度并没有因此降低,其次,因为所有的逻辑都放在Reducer中处理,造成reducer逻辑非常庞大,而且参数经常是全局变量,很容易因为一个reducer的误操作影响到其他的状态,造成链式反应。真正的定位难度并不会降低多少,反而当UI需要重新设计时,因为UI与逻辑的分离,反而容易造成逻辑更新不同步问题。
4. 全局store的效率问题 这个问题是最致命的问题,也是我最终决定放弃Redux的根本原因。就是全局Store在更新UI的时候会带来严重的效率问题。
这个问题是我在开发项目的时候无意中发现的,项目中有个模块是显示下载任务实时下载速度的,需要每秒刷新一次,我将下载任务的相关数据保存在全局store里,在reducer部分进行了拆分,专门有一个reducer处理任务状态的实时刷新功能,然后将合并好的状态同步到UI组件中。当时为了显示效率,我只在任务管理模块中订阅了任务列表状态,即实时下载速度等相关信息,但是我发现,在其他所有模块中,如果在其他模块中添加componentWillReceiveProps函数,会发现不论是否订阅了这个状态,任何一个状态的更新,到会导致所有的UI组件收到state,然后计算diff,以决定是否更新dom。
也就是说,Redux不能做到UI刷新的Filter,任何一个小的变动,都会导致全部UI部件的刷新。如果你恰好有个小模块需要经常刷新的话,那对不起,所有的模块都被迫跟着刷新,造成了计算的浪费。
总结 【为什么我放弃Redux】毕竟接触前端的时间还比较短,认识还很浅薄,难免有一些问题,希望各位前端大神批评指教。我自己根据信号和槽的思想重新设计了一个简单的模块同步机制,采用OO的思想,数据与逻辑绑定,但是通过注册机制来实现消息追踪和监控,效率更高,结构也更简单,抽空我把它整理一下,发到github上去,也许能解决一些人的问题。

    推荐阅读