Unidirectional User Interface Architecture:單向數(shù)據(jù)流
Unidirectional User Interface Architecture架構(gòu)的概念源于后端常見(jiàn)的CROS/Event Sourcing模式篱瞎,其核心思想即是將應(yīng)用狀態(tài)被統(tǒng)一存放在一個(gè)或多個(gè)的Store中避咆,并且所有的數(shù)據(jù)更新都是通過(guò)可觀測(cè)的Actions觸發(fā),而所有的View都是基于Store中的狀態(tài)渲染而來(lái)桶现。該架構(gòu)的最大優(yōu)勢(shì)在于整個(gè)應(yīng)用中的數(shù)據(jù)流以單向流動(dòng)的方式從而使得有用更好地可預(yù)測(cè)性與可控性,這樣可以保證你的應(yīng)用各個(gè)模塊之間的松耦合性鼎姊。與MVVM模式相比骡和,其解決了以下兩個(gè)問(wèn)題:
避免了數(shù)據(jù)在多個(gè)ViewModel中的冗余與不一致問(wèn)題
分割了ViewModel的職責(zé),使得ViewModel變得更加Clean
Why not Bidirectional(Two-way DataBinding)?
This means that one change (a user input or API response) can affect the state of an application in many places in the code — for example, two-way data binding. That can be hard to maintain and debug.
- easier-reasoning-with-unidirectional-dataflow-and-immutable-data(https://open.bekk.no/easier-reasoning-with-unidirectional-dataflow-and-immutable-data)
Facebook強(qiáng)調(diào)相寇,雙向數(shù)據(jù)綁定極不利于代碼的擴(kuò)展與維護(hù)慰于。
從具體的代碼實(shí)現(xiàn)角度來(lái)看,雙向數(shù)據(jù)綁定會(huì)導(dǎo)致更改的不可預(yù)期性(UnPredictable)唤衫,就好像Angular利用Dirty Checking來(lái)進(jìn)行是否需要重新渲染的檢測(cè)婆赠,這導(dǎo)致了應(yīng)用的緩慢,簡(jiǎn)直就是來(lái)砸場(chǎng)子的佳励。而在采用了單向數(shù)據(jù)流之后休里,整個(gè)應(yīng)用狀態(tài)會(huì)變得可預(yù)測(cè)(Predictable)蛆挫,也能很好地了解當(dāng)狀態(tài)發(fā)生變化時(shí)到底會(huì)有多少的組件發(fā)生變化。另一方面份帐,相對(duì)集中地狀態(tài)管理璃吧,也有助于你不同的組件之間進(jìn)行信息交互或者狀態(tài)共享,特別是像Redux這種強(qiáng)調(diào)Single Store與SIngle State Tree的狀態(tài)管理模式废境,能夠保證以統(tǒng)一的方式對(duì)于應(yīng)用的狀態(tài)進(jìn)行修改畜挨,并且Immutable的概念引入使得狀態(tài)變得可回溯。
譬如Facebook在Flux Overview(https://facebook.github.io/flux/docs/overview.html)中舉的例子噩凹,當(dāng)我們希望在一個(gè)界面上同時(shí)展示未讀信息列表與未讀信息的總數(shù)目的時(shí)候巴元,對(duì)于MV*就有點(diǎn)惡心了,特別是當(dāng)這兩個(gè)組件不在同一個(gè)ViewModel/Controller中的時(shí)候驮宴。一旦我們將某個(gè)未讀信息標(biāo)識(shí)為已讀逮刨,會(huì)引起控制已讀信息、未讀信息堵泽、未讀信息總數(shù)目等等一系列模型的更新修己。特別是很多時(shí)候?yàn)榱朔奖阄覀兛赡茉诿總€(gè)ViewModel/Controller都會(huì)設(shè)置一個(gè)數(shù)據(jù)副本,這會(huì)導(dǎo)致依賴連鎖更新迎罗,最終導(dǎo)致不可預(yù)測(cè)的結(jié)果與性能損耗睬愤。而在Flux中這種依賴是反轉(zhuǎn)的,Store接收到更新的Action請(qǐng)求之后對(duì)數(shù)據(jù)進(jìn)行統(tǒng)一的更新并且通知各個(gè)View纹安,而不是依賴于各個(gè)獨(dú)立的ViewModel/Controller所謂的一致性更新尤辱。從職責(zé)劃分的角度來(lái)看,除了Store之外的任何模塊其實(shí)都不知道應(yīng)該如何處理數(shù)據(jù)厢岂,這就保證了合理的職責(zé)分割光督。這種模式下,當(dāng)我們創(chuàng)建新項(xiàng)目時(shí)塔粒,項(xiàng)目復(fù)雜度的增長(zhǎng)瓶頸也就會(huì)更高结借,不同于傳統(tǒng)的View與ViewLogic之間的綁定,控制流被獨(dú)立處理卒茬,當(dāng)我們添加新的特性映跟,新的數(shù)據(jù),新的界面扬虚,新的邏輯處理模塊時(shí),并不會(huì)導(dǎo)致原有模塊的復(fù)雜度增加球恤,從而使得整個(gè)邏輯更加清晰可控辜昵。
這里還需要提及一下,很多人應(yīng)該是從React開始認(rèn)知到單向數(shù)據(jù)流這種架構(gòu)模式的咽斧,而當(dāng)時(shí)Angular 1的緩慢與性能之差令人發(fā)指堪置,但是譬如Vue與Angular 2的性能就非常優(yōu)秀躬存。借用Vue.js官方的說(shuō)法,
The virtual-DOM approach provides a functional way to describe your view at any point of time, which is really nice. Because it doesn’t use observables and re-renders the entire app on every update, the view is by definition guaranteed to be in sync with the data. It also opens up possibilities to isomorphic JavaScript applications.
Instead of a Virtual DOM, Vue.js uses the actual DOM as the template and keeps references to actual nodes for data bindings. This limits Vue.js to environments where DOM is present. However, contrary to the common misconception that Virtual-DOM makes React faster than anything else, Vue.js actually out-performs React when it comes to hot updates, and requires almost no hand-tuned optimization. With React, you need to implementshouldComponentUpdate everywhere and use immutable data structures to achieve fully optimized re-renders.
總而言之舀锨,筆者認(rèn)為雙向數(shù)據(jù)流與單向數(shù)據(jù)流相比岭洲,性能上孰優(yōu)孰劣尚無(wú)定論,最大的區(qū)別在于單向數(shù)據(jù)流與雙向數(shù)據(jù)流相比有更好地可控性坎匿,這一點(diǎn)在上文提及的函數(shù)響應(yīng)式編程中也有體現(xiàn)盾剩。若論快速開發(fā),筆者感覺(jué)雙向數(shù)據(jù)綁定略勝一籌替蔬,畢竟這種View與ViewModel/ViewLogic之間的直接綁定直觀便捷告私。而如果是注重于全局的狀態(tài)管理,希望維護(hù)耦合程度較低承桥、可測(cè)試性/可擴(kuò)展性較高的代碼驻粟,那么還是單向數(shù)據(jù)流,即Unidirectional Architecture較為合適凶异。一家之言蜀撑,歡迎討論。
Flux:數(shù)據(jù)流驅(qū)動(dòng)的頁(yè)面
Flux不能算是絕對(duì)的先行者剩彬,但是在Unidirectional Architecture中卻是最富盛名的一個(gè)酷麦,也是很多人接觸到的第一個(gè)Unidirectional Architecture。Flux主要由以下幾個(gè)部分構(gòu)成:
Stores:存放業(yè)務(wù)數(shù)據(jù)和應(yīng)用狀態(tài)襟衰,一個(gè)Flux中可能存在多個(gè)Stores
View:層次化組合的React組件
Actions:用戶輸入之后觸發(fā)View發(fā)出的事件
Dispatcher:負(fù)責(zé)分發(fā)Actions
根據(jù)上述流程贴铜,我們可知Flux模式的特性為:
Dispatcher:Event Bus中設(shè)置有一個(gè)單例的Dispatcher,很多Flux的變種都移除了Dispatcher依賴瀑晒。
只有View使用可組合的組件:在Flux中只有React的組件可以進(jìn)行層次化組合绍坝,而Stores與Actions都不可以進(jìn)行層次化組合。React組件與Flux一般是松耦合的苔悦,因此Flux并不是Fractal轩褐,Dispatcher與Stores可以被看做Orchestrator。
用戶事件響應(yīng)在渲染時(shí)聲明:在React的
render()
函數(shù)中玖详,即負(fù)責(zé)響應(yīng)用戶交互把介,也負(fù)責(zé)注冊(cè)用戶事件的處理器
下面我們來(lái)看一個(gè)具體的代碼對(duì)比,首先是以經(jīng)典的Cocoa風(fēng)格編寫一個(gè)簡(jiǎn)單的計(jì)數(shù)器按鈕:
class ModelCounter
constructor: (@value=1) ->
increaseValue: (delta) =>
@value += delta
class ControllerCounter
constructor: (opts) ->
@model_counter = opts.model_counter
@observers = []
getValue: => @model_counter.value
increaseValue: (delta) =>
@model_counter.increaseValue(delta)
@notifyObservers()
notifyObservers: =>
obj.notify(this) for obj in @observers
registerObserver: (observer) =>
@observers.push(observer)
class ViewCounterButton
constructor: (opts) ->
@controller_counter = opts.controller_counter
@button_class = opts.button_class or 'button_counter'
@controller_counter.registerObserver(this)
render: =>
elm = $("<button class=\"#{@button_class}\">
#{@controller_counter.getValue()}</button>")
elm.click =>
@controller_counter.increaseValue(1)
return elm
notify: =>
$("button.#{@button_class}").replaceWith(=> @render())
上述代碼邏輯用上文提及的MVC模式圖演示就是:
而如果用Flux模式實(shí)現(xiàn)蟋座,會(huì)是下面這個(gè)樣子:
# Store
class CounterStore extends EventEmitter
constructor: ->
@count = 0
@dispatchToken = @registerToDispatcher()
increaseValue: (delta) ->
@count += 1
getCount: ->
return @count
registerToDispatcher: ->
CounterDispatcher.register((payload) =>
switch payload.type
when ActionTypes.INCREASE_COUNT
@increaseValue(payload.delta)
)
# Action
class CounterActions
@increaseCount: (delta) ->
CounterDispatcher.handleViewAction({
'type': ActionTypes.INCREASE_COUNT
'delta': delta
})
# View
CounterButton = React.createClass(
getInitialState: ->
return {'count': 0}
_onChange: ->
@setState({
count: CounterStore.getCount()
})
componentDidMount: ->
CounterStore.addListener('CHANGE', @_onChange)
componentWillUnmount: ->
CounterStore.removeListener('CHANGE', @_onChange)
render: ->
return React.DOM.button({'className': @prop.class}, @state.value)
)
其數(shù)據(jù)流圖為:
Redux:集中式的狀態(tài)管理
Redux是Flux的所有變種中最為出色的一個(gè)拗踢,并且也是當(dāng)前Web領(lǐng)域主流的狀態(tài)管理工具,其獨(dú)創(chuàng)的理念與功能深刻影響了GUI應(yīng)用程序架構(gòu)中的狀態(tài)管理的思想向臀。Redux將Flux中單例的Dispatcher替換為了單例的Store巢墅,即也是其最大的特性,集中式的狀態(tài)管理。并且Store的定義也不是從零開始單獨(dú)定義君纫,而是基于多個(gè)Reducer的組合驯遇,可以把Reducer看做Store Factory。Redux的重要組成部分包括:
Singleton Store:管理應(yīng)用中的狀態(tài)蓄髓,并且提供了一個(gè)
dispatch(action)
函數(shù)叉庐。Provider:用于監(jiān)聽Store的變化并且連接像React、Angular這樣的UI框架
Actions:基于用戶輸入創(chuàng)建的分發(fā)給Reducer的事件
Reducers:用于響應(yīng)Actions并且更新全局狀態(tài)樹的純函數(shù)
根據(jù)上述流程会喝,我們可知Redux模式的特性為:
以工廠模式組裝Stores:Redux允許我以
createStore()
函數(shù)加上一系列組合好的Reducer函數(shù)來(lái)創(chuàng)建Store實(shí)例陡叠,還有另一個(gè)applyMiddleware()
函數(shù)可以允許在dispatch()
函數(shù)執(zhí)行前后鏈?zhǔn)秸{(diào)用一系列中間件。Providers:Redux并不特定地需要何種UI框架好乐,可以與Angular匾竿、React等等很多UI框架協(xié)同工作。Redux并不是Fractal蔚万,一般來(lái)說(shuō)Store被視作Orchestrator岭妖。
User Event處理器即可以選擇在渲染函數(shù)中聲明,也可以在其他地方進(jìn)行聲明反璃。
Model-View-Update
又被稱作Elm Architecture昵慌,上面所講的Redux就是受到Elm的啟發(fā)演化而來(lái),因此MVU與Redux之間有很多的相通之處淮蜈。MVU使用函數(shù)式編程語(yǔ)言Elm作為其底層開發(fā)語(yǔ)言斋攀,因此該架構(gòu)可以被看做更純粹的函數(shù)式架構(gòu)。MVU中的基本組成部分有:
Model:定義狀態(tài)數(shù)據(jù)結(jié)構(gòu)的類型
View:純函數(shù)梧田,將狀態(tài)渲染為界面
Actions:以Mailbox的方式傳遞用戶事件的載體
Update:用于更新狀態(tài)的純函數(shù)
根據(jù)上述流程淳蔼,我們可知Elm模式的特性為:
到處可見(jiàn)的層次化組合:Redux只是在View層允許將組件進(jìn)行層次化組合,而MVU中在Model與Update函數(shù)中也允許進(jìn)行層次化組合裁眯,甚至Actions都可以包含內(nèi)嵌的子Action
Elm屬于Fractal架構(gòu):因?yàn)镋lm中所有的模塊組件都支持層次化組合鹉梨,即都可以被單獨(dú)地導(dǎo)出使用
Model-View-Intent
MVI是一個(gè)基于RxJS的響應(yīng)式單向數(shù)據(jù)流架構(gòu)。MVI也是Cycle.js的首選架構(gòu)穿稳,主要由Observable事件流對(duì)象與處理函數(shù)組成存皂。其主要的組成部分包括:
Intent:Observable提供的將用戶事件轉(zhuǎn)化為Action的函數(shù)
Model:Observable提供的將Action轉(zhuǎn)化為可觀測(cè)的State的函數(shù)
View:將狀態(tài)渲染為用戶界面的函數(shù)
-
Custom Element:類似于React Component那樣的界面組件
根據(jù)上述流程,我們可知MVI模式的特性為:
重度依賴于Observables:架構(gòu)中的每個(gè)部分都會(huì)被轉(zhuǎn)化為Observable事件流
Intent:不同于Flux或者Redux逢艘,MVI中的Actions并沒(méi)有直接傳送給Dispatcher或者Store旦袋,而是交于正在監(jiān)聽的Model
徹底的響應(yīng)式,并且只要所有的組件都遵循MVI模式就能保證整體架構(gòu)的fractal特性
精彩回顧
1它改、GUI應(yīng)用程序架構(gòu)的十年變遷(一)
2疤孕、GUI應(yīng)用程序架構(gòu)的十年變遷(二)
3、GUI應(yīng)用程序架構(gòu)的十年變遷(三)
公告通知
自動(dòng)化運(yùn)維班央拖、架構(gòu)師班祭阀、區(qū)塊鏈正在招生中
各位小伙伴們截亦,歡迎試聽和咨詢: