本系列博文從 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
本專欄歷史文章: