React 可视化开发工具 Shadow Widget 非正经入门(之四:flux、m
本系列博文从 Shadow Widget 作者的视角,解释该框架的设计要点。本篇解释 Shadow Widget 在 MVC、MVVM、Flux 框架之间如何做选择。
1. React Flux 框架Facebook 官方为 React 提出了 flux 框架,也自己实现了一个 flux.js,尽管这个库设计得很差劲,但所有第三方为 React 开发的单向数据流方案,起点都是该库官方所提的 Flux concepts,下面是经典结构图:
Action 可简单理解为指令(或命令),由命令字 type 与命令参数 data(或称 payload)组成。Dispatcher 是分发器,Store 是数据与逻辑处理器,Store 会在 Dispatcher 注册针对各个命令字的响应回调函数。View 就是 React Component,View 常使用 Store 中的数据并订阅 Store 发生变化来刷新自身显示。 几个部件之间数据单向流动,如下: Action -> Dispatcher -> Store -> View 形成单向流动的原理较简单,大致这样,Store 在 Dispatch 注册的回调函数,由 Action 触发,Dispatcher 解析命令字,找出相应回调用函数实现调用即可。当 Dispatcher 按如下方式触发回调时,回调函数具备事件的特性。 setTimeout( function() { callback(); },0); 如果立即调用 callback,那只是回调,如果延时 0 秒会让 callback 在下个周期被调用,就成事件了,单向数据流因此得到保证。 当然,上面介绍非常简略,把核心机制讲明白,reflux、redux 让注册回调变事件也都用这个机制。当然,事件化回调的处理过程可能很复杂,比如 Dispatcher 还提供 2. React 中的 MVCReact 实现的虚拟 DOM 部分(即核心库
当你只使用 React 的核心库,未使用 reflux、redux 等单向数据流机制时,所用的 MVC 就是上图样子。如何构造 Controller 与 Model 是自由的,甚至你想将它改造成 MVVM 也是自由的,毕竟 React 的核心库只提供虚拟 DOM 映射,与 HTML 原生的 DOM 一起提供 Flux、MVC、MVVM 这三者是对等的架构,我们不能直接将 Flux 框架往 MVC 上套。 3. 复杂环境对 MVC 框架的影响在 React 中使用 MVC 主要缺陷是:当应用规模变大,
React 虚拟 DOM 对真实 DOM 做了一次抽像,附加 引入 Flux 能有针对性的缓解上述困难。其一,用单数据流向串接各 View,让与 Model 交互的那个 View(也称 Controller View)承担设计复杂性,其它 View 只做简单工作,如展示界面、简单响应鼠标点击等操作。
其二,用 Action 与 Dispatcher 简化 Controller,不弄那么多 Controller,归总到一个 Dispatcher。其三,采用 Functional Reactive Programming 方法构造响应式的单向数据流机制,以此应对异步时序问题。 React 生态链中有多种 Flux 实现,他们本质一样,表面差别不算大,通常几句话就能概括。reflux 采用多 store 方案,把用于集中分发的 Dispatcher 简化掉了,redux 采用单 store 方案,把分发 Action 后的处理分解给众多 Reducer 函数,也就是说,上图多个 Store 的功能,用 "单 Store + 众多 Reducer 函数" 替换。 4. Shadow Widget 与 Redux 走在两个方向上Redux 最大优点是实施彻底函数式编程,最大缺点也是彻底函数式。它本身并未简化设计复杂性,只是转移复杂性,但按官方原生的 Flux 概念,我们是按对象方式理解一个个 Store 的,在设计时,处理 Store 与 View,以及与 Action 之间关系时,都按对象方式去思考的,现在把复杂性转移到众多 reducer 函数上,函数式思维不利于设计分解(相对对象化思维而言)。 Redux 之所以能盛行,与 React 自身限制有关。React 的虚拟 DOM 树限制数据单向(向下)传递,跨节点读取属性极不方便,如果我们把所有服务于 render 的 state 数据,独立到节点之外的全局函数(reducer)中去组装呢?所有用到的 state 串一起,形成一个大的全局变量(就是单 Store),reducer 函数想怎么读就怎么读。这个方案以大幅度函数式改造为代价,来突破 React 的限制。 Shadow Widget 做的正相反,尝试维持对象化思维习惯,把 Store 与 ViewModel 合一(后面还有详细说明)以便减轻思考负担;通过建立 Widget 树,用 5. Controller View 数据传递我们研究一下 Controller View 与 Store 对接及与下级 View 的连接关系,取上图局部,放大讲解,如下:
当 Store 中有数据更新,通知 Controller View 更新界面,Controller View 就从 Store 读得 state 数据,来更新自己的 state。而自身 state 变化将触发下级 View 联动更新,变化的信息在各子级借助 props 属性实现传递。 为下文讲解作准备,这里我们先拎一拎 Store 该具备的特性:
6. 向 MVVM 演化我们换一个角度看 flux 框架,传递 Action 相当于
这么弱化、简化后,Flux 框架就剩 Store 与 View,参照 MVC 框架,这里 Store 与 MVC 中 Model 是对应的,某种程度上说,Flux 概念与 MVC 具备一定兼容性。 reflux 的 Store 仿 React Component 设计 API,学习成本进一步降低,遗憾的是它是多 Store 结构,一个 Store 对应一个 View(有时对应多个),Store 变多后容易让开发者感到困惑,许多属性设计一时想不清楚该放在 Store,还是放在 View,经常换来换去。这里我没说多 Store 设计不对,单 Store 有单 Store 的问题。而是,多 Store 与 多 View 之间如何思考定位有点拧巴,不像 MVVM 那么直接。
MVVM 采用双向绑定,View 的变动自动反映到 ViewModel,这是非常简单易用的方式,MVVM 在人性化方面比前端其它框架好出很多,因为设计一项功能,开发者首先想的是界面怎么体现,加个按钮,还是加个输入框,然后围绕着按钮或输入框,思考有什么动作,比如,点击按钮后下一步做什么。换成 Flux 思考方式,Store 与 View 之间如何交互要多思考一次,还不以 "界面该怎么呈现" 为思考原点,因为 Action 与 Dispatch 的设计促使你先考虑 Store 的数据结构。 如果让 MVVM 再支持 "所见即所得" 的可视化设计,它的易用性将拉开 Flux 更远,加上 Flux 天然的函数式编程倾向,叠加 react-router 等工具,也自然以路由指令、Action命令、状态数据为思考出发点。比如 react-router 强调,以 "路由" 如何设置为功能开发的第一出发点,不像 MVVM 是以交互界面设计为第一出发点。所以,说句实话,React 生态链上的工具比 Vue 难用得多,这也是 React 急需 Shadow Widget 之类工具的理由。 现在我们明确了引入 MVVM 的收益,非常值得做。问题关键是,它如何与 Flux 共存? 首先,Flux 中的 Store 与 Controller View 可以合并,大胆一点,肯定不会死人。以 reflux 现有设计为例,如果一个 React Component 节点不显示到界面,比如 其次,由前面总结的 Flux 中 Store 该具备的 3 项特性,与 MVVM 的双向数据绑定需求高度重合,以 Shadow Widget 已实现的功能举例:
当然,这些 Flux 中 Store 的需求是附加在 React Component 之上的,如果 Component 想显示界面(而不是用作纯 Store,把界面隐藏起来),尽管显示好了,无非这样的节点还同时具备 Store 的功能。 改造后 Shadow Widget 的 MVVM 如下图:
其中,双合一 Flux 要求的 Action 与 Dispatcher 已被各节点的 至于 Model,它最简的形态就是各 View 节点的 值得一提的是:Shadow Widget 的 MVVM 与 Flux 框架是兼容的,与 Functional Reactive Programming 编程也是兼容的。上图按 Flux 方式绘图,若要体现 MVVM,这么绘制:
上图中,区分 View 与 ViewModel 的主要依据是:一个 Component 节点是否纳入编程,若纳入编程(定义投影定义,或 idSetter 函数)应视作 ViewModel,否则应视作 View,即使这个 View 使用一些 7. 对照 Vue 的 MVVM 举个例子一个 Vue 的 MVVM 例子如下。
对应于 Shadow Widget,界面 View 定义如下: <div $=BodyPanel key='body'> <div $=Panel height='{null}' $for='' dual-data='{['项目1']}'> <div $=P> <span $=Input key='input' type='text' value=''></span> <span $=Button key='btn' $id__='btn_todo'>添加</span> </div> <div $=Ul $for='item in duals.data'> <div $=Li $key='"txt_" + index' $html='item.text'></div> </div> </div> </div> VM 定义如下: idSetter['btn_todo'] = function(value,oldValue) { if (value <= 2) { if (value == 1) { // init process this.setEvent( { $onClick: function(event) { var inputComp = this.componentOf('//input'); var text = inputComp.duals.value.trim(); if (text) { var dataComp = this.componentOf(0); dataComp.duals.data = ex.update(dataComp.duals.data,{ $push:[{text:text}],}); inputComp.duals.value = ''; } },}); } return; } }; Shadow Widget 的 MVVM 与 Vue 相比,更突出从 "界面布局" 出发思考设计,更倾向于函数式编程风格。比如:
8. 函数如何作为数据进行传递Shadow Widget 要支持界面可视化设计,可视设计的产出是界面元素的叠加物,当这种叠加物含有函数定义时,保存设计成果,或缓存设计结果(用于 undo 与 redo)将很成问题。因为函数定义要附带上下文才有意义,另外,函数定义体(即 JS 脚本)可以是任意字符,混在界面定义中,给结构化的解析设计结果也带来挑战。所以,Shadow Widget 限制可视设计过程中使用函数化数据,设计态的 props 数据传递不能有函数对象。 在 Shadow Widget 中,与 JSX 对等的界面数据化描述格式叫 json-x,因为 JSON 数据不能带 function 定义,在数据的序列化方面与 JSON 接近,所以就叫 json-x 格式了。界面的可视化设计过程中,输出的(或缓存的),就是这个基于 json-x 的数据。 Shadow Widget 借助在 不过在设计态,某些第三方库需要让特定构件捆绑函数对象,比如封装 slides.js 形成直方图、饼图等样板,在可视化设计中,捆绑的函数就要启用,否则可视化交互设计中直方图、饼图等不被绘制。 Shadow Widget 为这类需求提供两种解决方案。其一,使用 初始化列表(注意,不是 其二,类似 9. 总结我一直认为,开发语言、编程框架只是人类思维的辅助表达器,人脑观照世界,见山是山,见水是水,人要一个个去认,事物要一件件识别,探究复杂的事物,都是分层拆解的思路。具体到前端开发,客户需求高频变化,在并不纯粹的浏览器方框之中,过分强调纯粹的函数式编程肯定要误人子弟。 见过 React 家族的太多开发者,太多工具陷在追求 "纯正" 的泥淖里,无法自拔,阿弥陀佛!但愿我的观点是正确的。 本文参考资料:
本专栏历史文章:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |