自动化技术文我爱编程

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

2018-04-28  本文已影响39人  51reboot

Unidirectional User Interface Architecture:单向数据流

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

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主要由以下几个部分构成:

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

下面我们来看一个具体的代码对比,首先是以经典的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的重要组成部分包括:

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

Model-View-Update

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

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

Model-View-Intent

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

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

精彩回顾

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

公告通知

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

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


扫码添加小助手微信,备注"公开课,来源简书",进入分享群
上一篇 下一篇

猜你喜欢

热点阅读