本系列博文从 Shadow Widget 做者的视角,解释该框架的设计要点。本篇解释 Shadow Widget 在 MVC、MVVM、Flux 框架之间如何作选择。html
Facebook 官方为 React 提出了 flux 框架,也本身实现了一个 flux.js,尽管这个库设计得不好劲,但全部第三方为 React 开发的单向数据流方案,起点都是该库官方所提的 Flux concepts,下面是经典结构图:前端
Action 可简单理解为指令(或命令),由命令字 type 与命令参数 data(或称 payload)组成。Dispatcher 是分发器,Store 是数据与逻辑处理器,Store 会在 Dispatcher 注册针对各个命令字的响应回调函数。View 就是 React Component,View 常使用 Store 中的数据并订阅 Store 发生变化来刷新自身显示。react
几个部件之间数据单向流动,以下:git
Action -> Dispatcher -> Store -> View
造成单向流动的原理较简单,大体这样,Store 在 Dispatch 注册的回调函数,由 Action 触发,Dispatcher 解析命令字,找出相应回调用函数实现调用便可。当 Dispatcher 按以下方式触发回调时,回调函数具有事件的特性。github
setTimeout( function() { callback(); },0);
若是当即调用 callback,那只是回调,若是延时 0 秒会让 callback 在下个周期被调用,就成事件了,单向数据流所以获得保证。ajax
固然,上面介绍很是简略,把核心机制讲明白,reflux、redux 让注册回调变事件也都用这个机制。固然,事件化回调的处理过程可能很复杂,好比 Dispatcher 还提供 waitFor()
等待一项或多项 Action 的接口,咱们略去不细讲。编程
React 实现的虚拟 DOM 部分(即核心库 react.js
与 react-dom.js
)是 MVC 中的 "V"
,其 MVC 框架图以下:json
当你只使用 React 的核心库,未使用 reflux、redux 等单向数据流机制时,所用的 MVC 就是上图样子。如何构造 Controller 与 Model 是自由的,甚至你想将它改形成 MVVM 也是自由的,毕竟 React 的核心库只提供虚拟 DOM 映射,与 HTML 原生的 DOM 一块儿提供 "View"
。后面咱们真的要介绍怎么改为 MVVM。redux
Flux、MVC、MVVM 这三者是对等的架构,咱们不能直接将 Flux 框架往 MVC 上套。segmentfault
在 React 中使用 MVC 主要缺陷是:当应用规模变大,M, V, C
之间依赖关系会变复杂。下图还不算太复杂,只用到 2 个 Module。
React 虚拟 DOM 对真实 DOM 作了一次抽像,附加 props
、state
等概念,再加上异步时序干扰,原先还勉强玩得转的 MVC,已变得很很差用,开发、调试、定位问题都变困难了。
引入 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 函数" 替换。
Redux 最大优势是实施完全函数式编程,最大缺点也是完全函数式。它自己并未简化设计复杂性,只是转移复杂性,但按官方原生的 Flux 概念,咱们是按对象方式理解一个个 Store 的,在设计时,处理 Store 与 View,以及与 Action 之间关系时,都按对象方式去思考的,如今把复杂性转移到众多 reducer 函数上,函数式思惟不利于设计分解(相对对象化思惟而言)。
Redux 之因此能盛行,与 React 自身限制有关。React 的虚拟 DOM 树限制数据单向(向下)传递,跨节点读取属性极不方便,若是咱们把全部服务于 render 的 state 数据,独立到节点以外的全局函数(reducer)中去组装呢?全部用到的 state 串一块儿,造成一个大的全局变量(就是单 Store),reducer 函数想怎么读就怎么读。这个方案以大幅度函数式改造为代价,来突破 React 的限制。
Shadow Widget 作的正相反,尝试维持对象化思惟习惯,把 Store 与 ViewModel 合一(后面还有详细说明)以便减轻思考负担;经过创建 Widget 树,用 this.componentOf()
快速检索相关节点,以求方便的存取属性;再设计 duals 双源属性,创建一套能自动识别数据变化,并驱动单向数据流的机制。
咱们研究一下 Controller View 与 Store 对接及与下级 View 的链接关系,取上图局部,放大讲解,以下:
当 Store 中有数据更新,通知 Controller View 更新界面,Controller View 就从 Store 读得 state 数据,来更新本身的 state。而自身 state 变化将触发下级 View 联动更新,变化的信息在各子级借助 props 属性实现传递。
为下文讲解做准备,这里咱们先拎一拎 Store 该具有的特性:
要提供事件通知功能,当 Store 中的 state 数据有变化,通知 Controller View 刷新界面。
对 Controller View 暴露 state 数据,有两种设计可选,一是让事件通知中带 state 数据,二是事件通知不带数据,要由 Controller View 主动到 Store 查询。结合 FRP 编程特色,第二个设计更好,若是数据连续屡次更新,从 Store 读数据应合并为一次,取最新值。
什么时候通知 Controller View 刷新可能比较复杂,涉及条件组合,好比要 Action A 与 Action B 都发生后,才能触发事件通知。
咱们换一个角度看 flux 框架,传递 Action 至关于 "emit <Event>"
,将它弱化考虑,另外,Dispatcher 也可弱化,reflux 相比官方的 React flux,一个重要改进就是去掉 Dispatcher,工具复杂性所以降了很多。
这么弱化、简化后,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 节点不显示到界面,好比 <noscript>
节点,或者 comment 注释节点,或者 style.display='none'
的 <div>
节点,彻底胜任用来构造 Store 节点。
其次,由前面总结的 Flux 中 Store 该具有的 3 项特性,与 MVVM 的双向数据绑定需求高度重合,以 Shadow Widget 已实现的功能举例:
双源属性具备事件通知功能,它能够被侦听,修改双源属性的值能够触发事件,刷新 trigger 表达式也能触发事件。
将 Controller View 与 Store 合二为一,state 数据也合二为一,省去了二者之间同步。
Shadow Widget 的可计算性属性支持 any, all ,strict
三种条件同步机制,与 reflux 提供的条件组合等效。好比要求 Action A
与 Action B
都发生后,才触发事件,脚本表达式用 "all:"
前缀指示便可。
固然,这些 Flux 中 Store 的需求是附加在 React Component 之上的,若是 Component 想显示界面(而不是用做纯 Store,把界面隐藏起来),尽管显示好了,无非这样的节点还同时具有 Store 的功能。
改造后 Shadow Widget 的 MVVM 以下图:
其中,双合一 "Store + Controller View"
是 "VM"
或 "VM + V"
,视该 React Component 需不需在界面显示而定,若同时还用做界面元素的就是 "VM + V"
。
Flux 要求的 Action 与 Dispatcher 已被各节点的 duals.attr
属性代替,其中属性名(attr
) 与 Action 的命令字(type
)对等,属性值与 Action 的数据(Payload
)对等。各个 duals.attr
可被自身节点或其它节点侦听,当 duals.attr
取值变化时,相应的侦听函数会按事件方式自动被回调。
至于 Model,它最简的形态就是各 View 节点的 duals.xxx
属性。遇到复杂的,不妨定义专职的数据服务,用不显示界面的 Controller View 来定义,如上所述,这是 "VM"
。但当它只处理 duals.attr
数据,没有其它功能时,"VM"
的角色将退化为 "M"
。好比 ajax 数据服务(用于从服务侧请求数据,往服务侧保存数据),彻底能够用 style.display='none'
的 <div>
节点来构造,它以 duals.attr1, duals.attr2
等接口对外提供数据的读、写、侦听等服务。
值得一提的是:Shadow Widget 的 MVVM 与 Flux 框架是兼容的,与 Functional Reactive Programming 编程也是兼容的。上图按 Flux 方式绘图,若要体现 MVVM,这么绘制:
上图中,区分 View 与 ViewModel 的主要依据是:一个 Component 节点是否归入编程,若归入编程(定义投影定义,或 idSetter 函数)应视做 ViewModel,不然应视做 View,即便这个 View 使用一些 trigger, $for, $if
等控制指令也如此。
一个 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 相比,更突出从 "界面布局" 出发思考设计,更倾向于函数式编程风格。好比:
在一个 VM 中,Shadow Widget 将 Model 分散在各层多个 React Componet 中,数据服务以 duals.xxx
方式提供。而 Vue 集中在一处定义数据。
Shadow Widget 先考虑界面如何设计,肯定界面元素后,再考虑相关数据绑捆到哪一个节点更方便,因此数据服务是分散的,Vue 则提早考虑数据结构如何设计,要集中。因此,这个例子中,用 Vue 时 data.newTodo
定义到 Model,用 Shadow Widget 则视做一个过程数据,没必要在对外接口体现。
数据分散,处理函数也分散定义,因此上面 Shadow Widget 的事件函数,要用 this.componentOf()
动态查找相关节点。Vue 集中定义数据,与数据相关的节点、动做、事件等函数也随之被锁定。这两种方式各有利弊,Vue 方式简单明了,Shadow Widget 更动态、更函数式,使用要复杂些,但应付各类变化自由些,功能更强些,好比各层节点动态增删、改换。
Shadow Widget 要支持界面可视化设计,可视设计的产出是界面元素的叠加物,当这种叠加物含有函数定义时,保存设计成果,或缓存设计结果(用于 undo 与 redo)将很成问题。由于函数定义要附带上下文才有意义,另外,函数定义体(即 JS 脚本)能够是任意字符,混在界面定义中,给结构化的解析设计结果也带来挑战。因此,Shadow Widget 限制可视设计过程当中使用函数化数据,设计态的 props 数据传递不能有函数对象。
在 Shadow Widget 中,与 JSX 对等的界面数据化描述格式叫 json-x,由于 JSON 数据不能带 function 定义,在数据的序列化方面与 JSON 接近,因此就叫 json-x 格式了。界面的可视化设计过程当中,输出的(或缓存的),就是这个基于 json-x 的数据。
Shadow Widget 借助在 main[widget_path]
预登记投影类定义,实现 function 的动态捆绑,还借助 idSetter[id_string]
预登记 idSetter 函数,这二者让界面可视化设计时避开了函数对象的传递,设计态下投影定义与 idSetter 函数不被捆绑。
不过在设计态,某些第三方库须要让特定构件捆绑函数对象,好比封装 slides.js 造成直方图、饼图等样板,在可视化设计中,捆绑的函数就要启用,不然可视化交互设计中直方图、饼图等不被绘制。
Shadow Widget 为这类需求提供两种解决方案。其一,使用 初始化列表(注意,不是
W.$onLoad
),该列表中的初始化函数在设计态也被调用,一般用它注册特定厂商的库化 UI 节点。库化 UI 供设计中引用,它自身不介入中间设计成果的保存或缓存,在 $$onLoad
初始化函数中可捆绑投影类,或传递 idSetter 函数。
其二,相似 T.rewgt.DelayTimer
注册一个自行开发的 WTC 类,而后界面的转义标签就能够用 <div $=rewgt.DelayTimer>
引用它。
我一直认为,开发语言、编程框架只是人类思惟的辅助表达器,人脑观照世界,见山是山,见水是水,人要一个个去认,事物要一件件识别,探究复杂的事物,都是分层拆解的思路。具体到前端开发,客户需求高频变化,在并不纯粹的浏览器方框之中,过度强调纯粹的函数式编程确定要误人子弟。
见过 React 家族的太多开发者,太多工具陷在追求 "纯正" 的泥淖里,没法自拔,阿弥陀佛!希望个人观点是正确的。
本文参考资料:
facebook/flux:Flux Concepts
fluxxor.com:What is Flux?
Andrew Ray:The ReactJS Controller View Pattern
本专栏历史文章: