GUI应用程序架构的十年变迁(四)

Unidirectional User Interface Architecture:单向数据流

Unidirectional User Interface Architecture架构的概念源于后端常见的CROS/Event Sourcing模式,其核心思想即是将应用状态被统一存放在一个或多个的Store中,并且所有的数据更新都是通过可观测的Actions触发,而所有的View都是基于Store中的状态渲染而来。该架构的最大优势在于整个应用中的数据流以单向流动的方式从而使得有用更好地可预测性与可控性,这样可以保证你的应用各个模块之间的松耦合性。与MVVM模式相比,其解决了以下两个问题:

  • 避免了数据在多个ViewModel中的冗余与不一致问题

  • 分割了ViewModel的职责,使得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强调,双向数据绑定极不利于代码的扩展与维护。

从具体的代码实现角度来看,双向数据绑定会导致更改的不可预期性(UnPredictable),就好像Angular利用Dirty Checking来进行是否需要重新渲染的检测,这导致了应用的缓慢,简直就是来砸场子的。而在采用了单向数据流之后,整个应用状态会变得可预测(Predictable),也能很好地了解当状态发生变化时到底会有多少的组件发生变化。另一方面,相对集中地状态管理,也有助于你不同的组件之间进行信息交互或者状态共享,特别是像Redux这种强调Single Store与SIngle State Tree的状态管理模式,能够保证以统一的方式对于应用的状态进行修改,并且Immutable的概念引入使得状态变得可回溯。

譬如Facebook在Flux Overview(https://facebook.github.io/flux/docs/overview.html)中举的例子,当我们希望在一个界面上同时展示未读信息列表与未读信息的总数目的时候,对于MV*就有点恶心了,特别是当这两个组件不在同一个ViewModel/Controller中的时候。一旦我们将某个未读信息标识为已读,会引起控制已读信息、未读信息、未读信息总数目等等一系列模型的更新。特别是很多时候为了方便我们可能在每个ViewModel/Controller都会设置一个数据副本,这会导致依赖连锁更新,最终导致不可预测的结果与性能损耗。而在Flux中这种依赖是反转的,Store接收到更新的Action请求之后对数据进行统一的更新并且通知各个View,而不是依赖于各个独立的ViewModel/Controller所谓的一致性更新。从职责划分的角度来看,除了Store之外的任何模块其实都不知道应该如何处理数据,这就保证了合理的职责分割。这种模式下,当我们创建新项目时,项目复杂度的增长瓶颈也就会更高,不同于传统的View与ViewLogic之间的绑定,控制流被独立处理,当我们添加新的特性,新的数据,新的界面,新的逻辑处理模块时,并不会导致原有模块的复杂度增加,从而使得整个逻辑更加清晰可控。

这里还需要提及一下,很多人应该是从React开始认知到单向数据流这种架构模式的,而当时Angular 1的缓慢与性能之差令人发指,但是譬如Vue与Angular 2的性能就非常优秀。借用Vue.js官方的说法,

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.

总而言之,笔者认为双向数据流与单向数据流相比,性能上孰优孰劣尚无定论,最大的区别在于单向数据流与双向数据流相比有更好地可控性,这一点在上文提及的函数响应式编程中也有体现。若论快速开发,笔者感觉双向数据绑定略胜一筹,毕竟这种View与ViewModel/ViewLogic之间的直接绑定直观便捷。而如果是注重于全局的状态管理,希望维护耦合程度较低、可测试性/可扩展性较高的代码,那么还是单向数据流,即Unidirectional Architecture较为合适。一家之言,欢迎讨论。

Flux:数据流驱动的页面

Flux不能算是绝对的先行者,但是在Unidirectional Architecture中却是最富盛名的一个,也是很多人接触到的第一个Unidirectional Architecture。Flux主要由以下几个部分构成:

  • Stores:存放业务数据和应用状态,一个Flux中可能存在多个Stores

  • View:层次化组合的React组件

  • Actions:用户输入之后触发View发出的事件

  • Dispatcher:负责分发Actions

根据上述流程,我们可知Flux模式的特性为:

  • Dispatcher:Event Bus中设置有一个单例的Dispatcher,很多Flux的变种都移除了Dispatcher依赖。

  • 只有View使用可组合的组件:在Flux中只有React的组件可以进行层次化组合,而Stores与Actions都不可以进行层次化组合。React组件与Flux一般是松耦合的,因此Flux并不是Fractal,Dispatcher与Stores可以被看做Orchestrator。

  • 用户事件响应在渲染时声明:在React的render()函数中,即负责响应用户交互,也负责注册用户事件的处理器

下面我们来看一个具体的代码对比,首先是以经典的Cocoa风格编写一个简单的计数器按钮:

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模式实现,会是下面这个样子:

# 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)

)

其数据流图为:

Redux:集中式的状态管理

Redux是Flux的所有变种中最为出色的一个,并且也是当前Web领域主流的状态管理工具,其独创的理念与功能深刻影响了GUI应用程序架构中的状态管理的思想。Redux将Flux中单例的Dispatcher替换为了单例的Store,即也是其最大的特性,集中式的状态管理。并且Store的定义也不是从零开始单独定义,而是基于多个Reducer的组合,可以把Reducer看做Store Factory。Redux的重要组成部分包括:

  • Singleton Store:管理应用中的状态,并且提供了一个dispatch(action)函数。

  • Provider:用于监听Store的变化并且连接像React、Angular这样的UI框架

  • Actions:基于用户输入创建的分发给Reducer的事件

  • Reducers:用于响应Actions并且更新全局状态树的纯函数

根据上述流程,我们可知Redux模式的特性为:

  • 以工厂模式组装Stores:Redux允许我以createStore()函数加上一系列组合好的Reducer函数来创建Store实例,还有另一个applyMiddleware()函数可以允许在dispatch()函数执行前后链式调用一系列中间件。

  • Providers:Redux并不特定地需要何种UI框架,可以与Angular、React等等很多UI框架协同工作。Redux并不是Fractal,一般来说Store被视作Orchestrator。

  • User Event处理器即可以选择在渲染函数中声明,也可以在其他地方进行声明。

Model-View-Update

又被称作Elm Architecture,上面所讲的Redux就是受到Elm的启发演化而来,因此MVU与Redux之间有很多的相通之处。MVU使用函数式编程语言Elm作为其底层开发语言,因此该架构可以被看做更纯粹的函数式架构。MVU中的基本组成部分有:

  • Model:定义状态数据结构的类型

  • View:纯函数,将状态渲染为界面

  • Actions:以Mailbox的方式传递用户事件的载体

  • Update:用于更新状态的纯函数

根据上述流程,我们可知Elm模式的特性为:

  • 到处可见的层次化组合:Redux只是在View层允许将组件进行层次化组合,而MVU中在Model与Update函数中也允许进行层次化组合,甚至Actions都可以包含内嵌的子Action

  • Elm属于Fractal架构:因为Elm中所有的模块组件都支持层次化组合,即都可以被单独地导出使用

Model-View-Intent

MVI是一个基于RxJS的响应式单向数据流架构。MVI也是Cycle.js的首选架构,主要由Observable事件流对象与处理函数组成。其主要的组成部分包括:

  • Intent:Observable提供的将用户事件转化为Action的函数

  • Model:Observable提供的将Action转化为可观测的State的函数

  • View:将状态渲染为用户界面的函数

  • Custom Element:类似于React Component那样的界面组件


根据上述流程,我们可知MVI模式的特性为:

  • 重度依赖于Observables:架构中的每个部分都会被转化为Observable事件流

  • Intent:不同于Flux或者Redux,MVI中的Actions并没有直接传送给Dispatcher或者Store,而是交于正在监听的Model

  • 彻底的响应式,并且只要所有的组件都遵循MVI模式就能保证整体架构的fractal特性

精彩回顾

1、GUI应用程序架构的十年变迁(一)
2、GUI应用程序架构的十年变迁(二)
3、GUI应用程序架构的十年变迁(三)

公告通知

自动化运维班、架构师班、区块链正在招生中

各位小伙伴们,欢迎试听和咨询:


扫码添加小助手微信,备注"公开课,来源简书",进入分享群
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容