当前位置: 首页 > Web前端 > HTML5

React可视化开发工具ShadowWidget非正经介绍(第四篇:flux、mvc、mvvm)

时间:2023-04-05 18:57:47 HTML5

本系列博文从ShadowWidget作者的角度讲解框架的设计要点。本文解释了ShadowWidget如何在MVC、MVVM和Flux框架之间进行选择。1、ReactFlux框架Facebook官方给React提出了一个flux框架,也自己实现了一个flux.js。虽然这个库设计的很烂,但所有第三方为React开发的单向数据流解决方案都是从这个库开始的下面是Flux官方概念的经典结构图:Action可以简单理解为一条指令(或命令),它包括命令字类型和命令参数数据(或有效载荷)。Dispatcher是一个分发器,Store是一个数据和逻辑处理器,Store会在Dispatcher中为每个命令字注册一个响应回调函数。View是ReactComponent,View经常使用Store中的数据,订阅Store中的变化来刷新自己的展示。数据在几个组件之间的单向流转如下:Action->Dispatcher->Store->View单向流的原理比较简单,大致是这样的,Dispatch中Store注册的回调函数是由Action,Dispatcher解析commandword,找到对应的回调函数实现调用。当Dispatcher如下触发回调时,回调函数具有事件的特征。setTimeout(function(){callback();},0);如果立即调用回调,则它只是一个回调。如果延迟为0秒,回调会在下一个周期被调用,成为一个事件,从而保证数据的单向流动。当然,上面的介绍很简短,把核心机制解释清楚了。Reflux和redux也是利用这种机制来注册回调变化事件。当然,基于事件的回调的处理过程可能非常复杂。例如,Dispatcher也提供了waitFor()的接口来等待一个或多个Action,我们就不详细介绍了。2、React中的MVCReact实现的虚拟DOM部分(即核心库react.js和react-dom.js)是MVC中的“V”。MVC框架图如下:当你只使用React的核心库,不使用reflux、redux等单向数据流机制时,使用的MVC如上图所示。如何构造Controller和Model是自由的,即使你想把它改造成MVVM,你也是自由的。毕竟React的核心库只是提供虚拟DOM映射,和HTML的原生DOM一起提供“View”。后面我们真的要介绍怎么改成MVVM。Flux、MVC、MVVM是等价的架构,我们不能直接把Flux框架应用到MVC上。3、复杂环境对MVC框架的影响。在React中使用MVC的主要缺点是:当应用规模变大时,M、V、C之间的依赖关系会变得复杂。下图并不太复杂,只用到了2个Module。React虚拟DOM对真实DOM进行了抽象,加入了props、state等概念,再加上异步时序干扰,一开始勉强能玩的MVC变得非常难用,开发、调试、定位问题都变成了难的。Flux的引入可以针对性的缓解上述难点。首先,每个View都串联成单一的数据流,这样与Model交互的View(也叫ControllerView)承担了设计的复杂性,其他View只做简单的工作,比如显示界面,简单响应鼠标点击。第二,用Action和Dispatcher来简化Controller,不要用那么多Controller,总结为一个Dispatcher。第三,使用FunctionalReactiveProgramming方法构建响应式单向数据流机制来处理异步时序问题。React生态链中有很多种Flux的实现。它们本质上是一样的,表面上的差别不是太大,通常可以三言两语概括。Reflux采用多店方案,简化了用于集中分发的Dispatcher。Redux采用单store的方案,将Actions分发到很多Reducer函数后进行处理分解。Store+众多Reducer函数”替换。4.ShadowWidget和Redux分两个方向。Redux最大的优点是实现了完全的函数式编程,最大的缺点也是完全的函数式编程。它本身并没有简化设计复杂度,只是将复杂度进行了转移,但是按照官方原生的Flux概念,我们理解每个Storebyobject。设计的时候,在处理Store和View,Action的关系的时候,都是从对象的角度去思考,现在把复杂度转移到了很多reducer函数上。函数式思维不利于设计分解(相对于面向对象的思维)。Redux之所以能够盛行,与React自身的局限性有关。React的虚拟DOM树限制了数据的单向(向下)传输,跨节点读取属性极其不便。如果我们将所有服务渲染的状态数据独立地组装在节点外的全局函数(reducer)中会怎样??所有使用过的状态串起来形成一个大的全局变量(也就是单个Store),reducer函数可以随心所欲地读取它。该方案以大幅度的功能改造为代价,突破了React的局限性。ShadowWidget则相反。尽量保持面向对象的思维习惯,整合Store和ViewModel(稍后详述),减轻思维负担;通过构建Widget树,使用this.componentOf()快速检索相关节点,以方便访问属性;重新设计duals双源属性,建立自动识别数据变化,驱动单向数据流的机制。5、ControllerView数据传递我们来研究一下ControllerView与Store的连接以及与下层View的连接关系。把上图部分放大解释一下,如下:当Store中有数据更新时,通知ControllerView更新界面,ControllerView会从Store中更新界面。读取状态数据以更新您自己的状态。自身状态的变化会触发下层View的联动更新,变化的信息会借助props属性传递到各个子层级。为了准备后面的讲解,这里先提一下Store应该具备的功能:提供事件通知功能,当Store中的状态数据发生变化时,通知ControllerView刷新界面。将状态数据暴露给控制器视图有两种设计选择。一种是让事件通知包含状态数据,另一种是让事件通知不携带数据,ControllerView要主动查询Store。结合FRP编程的特点,第二种设计比较好。如果数据连续多次更新,则应将从Store读取的数据合并为一个,取最新的值。什么时候通知ControllerView刷新可能比较复杂,涉及多种条件的组合,比如只有ActionA和ActionB同时发生才能触发事件通知。6.向MVVM的演进让我们从另一个角度来看flux框架。通过Action相当于“发出”,并认为它被削弱了。另外,Dispatcher也可以弱化。与官方的Reactflux相比,reflux的一个重要改进是去掉了Dispatcher,工具的复杂度因此降低了很多。经过这样的弱化和简化,Flux框架就剩下了Store和View。参考MVC框架,Store对应的是MVC中的Model。在某种程度上,Flux的概念与MVC是兼容的。回流的Store模仿了ReactComponent的设计API,进一步降低了学习成本。不幸的是,它是一个多商店结构。一个Store对应一个View(有时对应多个)。Stores比较多的时候,很容易让开发者迷惑,很多属性都是一时设计的。分不清是放在Store里还是放在View里,经常来回切换。这里我不是说多店设计不对,单店有单店问题。而是如何在多个Stores和多个View之间思考定位有点曲折,不像MVVM那样直接。MVVM采用双向绑定,View的变化自动反映到ViewModel。这是一种非常简单易用的方法。MVVM在人性化方面比其他前端框架要好很多,因为开发者在设计一个功能的时候,首先想到的是界面如何Reflect,加一个按钮,或者加一个输入框,然后把按钮或者输入包围起来框,想想要采取什么行动,例如,点击按钮后下一步要做什么。换成Flux的思维方式,需要更多思考Store和View之间如何交互,不要把“界面应该如何呈现”作为思考的原点,因为Action和Dispatch的设计提示你先考虑Store的数据结构。如果让MVVM支持“所见即所得”的视觉设计,其易用性将把Flux推得更远。除了Flux天生的函数式编程倾向,叠加的工具如react-router自然会使用路由指令,Action命令和状态数据是思考的出发点。比如react-router强调如何设置“路由”是功能开发的第一出发点,不像MVVM是把交互界面的设计作为第一出发点。所以说实话,React生态链上的工具比Vue难用多了,这也是React急需ShadowWidget这样的工具的原因。现在我们清楚了引入MVVM的好处,这是非常值得的。关键问题是,它如何与Flux共存?首先Flux中的Store和ControllerView是可以合并的。大胆一点,绝对不会死。以现有的回流设计为例,如果一个ReactComponent节点没有显示在界面上,比如

节点带有style.display='none',完全有资格构建Store节点。其次,上面总结的StoreinFlux应该具备的3个特性,与MVVM的双向数据绑定需求高度重合。以ShadowWidget实现的功能为例:双源属性有事件通知功能,可以监听。可以通过修改双源属性的值来触发事件,也可以通过刷新触发表达式来触发事件。ControllerView和Store合二为一,状态数据也合二为一,免去了两者之间的同步。ShadowWidget的可计算性支持any、all、strict三种条件同步机制,相当于reflux提供的条件组合。例如,要在触发事件之前要求动作A和动作B都发生,脚本表达式可以用前缀“all:”表示。当然,这些Flux中的Store的要求是添加到ReactComponent中的。如果Component要显示界面(而不是作为一个纯粹的Store,隐藏界面),就算显示了,也无非就是这样一个节点也有Store的Function。ShadowWidget改造后的MVVM如下:其中,二合一的“Store+ControllerView”是“VM”还是“VM+V”,取决于ReactComponent是否需要展示在接口,如果它也被用作接口元素是“VM+V”。Flux所需的Action和Dispatcher被替换为每个节点的duals.attr属性,其中属性名(attr)相当于Action的命令字(type),属性值相当于数据(有效载荷)的行动。每个duals.attr都可以被自己的节点或其他节点监听。当duals.attr的值发生变化时,会以事件的方式自动回调对应的监听函数。至于Model,最简单的形式就是每个View节点的duals.xxx属性。如果遇到复杂的问题,不妨定义一个专职的数据服务,用不显示界面的ControllerView来定义。如上所述,这就是“VM”。但是当它只处理duals.attr数据而不处理其他数据时,“VM”的角色退化为“M”。比如ajax数据服务(用来向服务端请求数据,保存数据到服务端)可以用一个
节点构造,style.display='none',它使用duals.attr1,duals。attr2等接口对外提供数据读、写、听等服务。值得一提的是:ShadowWidget的MVVM兼容Flux框架,也兼容FunctionalReactiveProgramming。上图是用Flux的方式绘制的。为了体现MVVM,画成这样:上图中,区分View和ViewModel的主要依据是:一个Component节点是否包含在编程中,是否包含在编程中(投影定义的定义,或者idSetter函数)应该算是ViewModel,否则应该算是View,即使这个View使用了一些trigger,$for,$if等控制指令。7、针对Vue的MVVM举个例子Vue的MVVM的例子如下。针对ShadowWidget,界面View定义如下:添加
VM定义如下:idSetter['btn_todo']=function(value,oldValue){if(value<=2){if(value==1){//initprocessthis.setEvent({$onClick:function(event){varinputComp=this.componentOf('//input');vartext=inputComp.duals.value.trim();if(text){vardataComp=this.componentOf(0);dataComp.duals.data=ex.update(dataComp.duals.data,{$push:[{text:text}],});inputComp.duals.v值='';}},});}返回;}};与Vue相比,ShadowWidget的MVVM更突出的是从“界面布局”来思考设计,更倾向于函数式编程风格。例如:在一个VM中,ShadowWidget将Model分散在每一层的多个ReactCompone中,数据服务以duals.xxx的形式提供。另一方面,Vue将定义数据集中在一个地方。ShadowWidget首先考虑如何设计界面,确定界面元素后,再考虑哪个节点更方便捆绑相关数据,所以数据服务是去中心化的,而Vue考虑的是如何提前设计数据结构,必须是集中。因此,本例中,使用Vue时,在Model中定义了data.newTodo,使用ShadowWidget时,将其视为流程数据,不需要在对外接口中体现。数据比较分散,处理函数也是单独定义的,所以上面ShadowWidget的事件函数需要使用this.componentOf()来动态寻找相关节点。Vue集中定义数据,与数据相关的节点、动作、事件等功能也被锁定。这两种方法各有优缺点。Vue方法简单明了,ShadowWidget更加动态和功能化。它使用起来比较复杂,但是可以更自由地处理各种变化,功能也更多,比如可以动态增删改换每一层的节点。8.功能如何作为数据传递ShadowWidget支持界面的可视化设计。视觉设计的输出是界面元素的叠加。当overlay包含函数定义时,保存设计结果或缓存设计结果(用于撤消和重做)将是非常有问题的。因为函数定义只有伴随着上下文才有意义。另外,函数定义体(即JS脚本)可以是任意字符,混杂在接口定义中,这也给结构化分析和设计结果带来了挑战。所以ShadowWidget在视觉设计过程中限制了函数数据的使用,设计状态下的道具数据传递不能有函数对象。在ShadowWidget中,相当于JSX的接口数据描述格式称为json-x,因为JSON数据不能用函数定义,在数据序列化方面接近JSON,所以称为json-x格式。在界面可视化设计过程中,输出(或缓存)的是基于json-x的数据。ShadowWidget使用main[widget_path]中预注册的投影类定义实现函数的动态绑定,也使用idSetter[id_string]预注册idSetter函数,两者都让界面的可视化设计避免函数对象的传递。投影定义和idSetter函数未捆绑。但是在设计状态下,一些第三方库需要将函数对象与特定的组件进行捆绑,比如封装slides.js,形成直方图、饼图等模板。不绘制图形、饼图等。ShadowWidget针对此类需求提供了两种解决方案。首先,使用初始化列表(注意,不是W.$onLoad),这个列表中的初始化函数在设计状态下也会被调用,通常用于注册特定厂商的库UI节点。库UI仅供设计参考,不干预中间设计结果的保存或缓存。在$$onLoad初始化函数中,可以捆绑投影类,也可以传入idSetter函数。第二,注册一个自己开发的WTC类,比如T.rewgt.DelayTimer,然后接口的转义标签可以用来引用。9.总结我一直认为,开发语言和编程框架只是人类思维的辅助表达。人脑观察世界,见山是山,见水是水。复杂的东西都是层层拆解思路。具体到前端开发,客户需求变化频繁。在不纯的浏览器盒子里,过分强调纯函数式编程肯定会误导人。我见过太多React家族的开发者,也见过太多的工具陷入追求“纯粹”的泥潭,无法自拔,阿弥陀佛!希望我的观点是正确的。本文参考资料:阮一峰:MVC、MVP、MVVM图解facebook/flux:FluxConceptsfluxxor.com:什么是Flux?AndrewRay:TheReactJSControllerViewPattern本专栏历史文章:介绍一个让React与Vue技术竞争的项目React可视化开发工具ShadowWidget非正式介绍(第一部分:React的三宗罪)React可视化开发工具ShadowWidget非正式介绍(第二部分:分离界面设计)React可视化开发工具ShadowWidget非正式介绍(第三部分:双源属性和数据驱动)