GUI應(yīng)用程序架構(gòu)的十年變遷(四)

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.

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ū)塊鏈正在招生中

各位小伙伴們截亦,歡迎試聽和咨詢:


掃碼添加小助手微信,備注"公開課柬讨,來(lái)源簡(jiǎn)書",進(jìn)入分享群
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末袍啡,一起剝皮案震驚了整個(gè)濱河市踩官,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌境输,老刑警劉巖蔗牡,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異嗅剖,居然都是意外死亡辩越,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門信粮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)黔攒,“玉大人,你說(shuō)我怎么就攤上這事强缘《蕉瑁” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵旅掂,是天一觀的道長(zhǎng)赏胚。 經(jīng)常有香客問(wèn)我,道長(zhǎng)商虐,這世上最難降的妖魔是什么觉阅? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮秘车,結(jié)果婚禮上典勇,老公的妹妹穿的比我還像新娘。我一直安慰自己鲫尊,他們只是感情好痴柔,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疫向,像睡著了一般咳蔚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搔驼,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天谈火,我揣著相機(jī)與錄音,去河邊找鬼舌涨。 笑死糯耍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播温技,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼革为,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了舵鳞?” 一聲冷哼從身側(cè)響起震檩,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜓堕,沒(méi)想到半個(gè)月后抛虏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡套才,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年迂猴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片背伴。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沸毁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挂据,到底是詐尸還是另有隱情以清,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布崎逃,位于F島的核電站掷倔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏个绍。R本人自食惡果不足惜勒葱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巴柿。 院中可真熱鬧凛虽,春花似錦、人聲如沸广恢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)钉迷。三九已至至非,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間糠聪,已是汗流浹背荒椭。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舰蟆,地道東北人趣惠。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓狸棍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親味悄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子草戈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容