花椒和邻居's Blog 花椒和邻居's Blog
首页
前端
开源
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • Java程序员
收藏
关于
随笔
GitHub (opens new window)

花椒和邻居

工作了三年半的前端实习生
首页
前端
开源
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • Java程序员
收藏
关于
随笔
GitHub (opens new window)
  • 前端开发笔记

    • 重识 HTML,掌握页面基本结构和加载过程
    • CSS:页面布局的基本规则和方式
    • JavaScript 如何实现继承?
    • JavaScript 引擎如何执行 JavaScript 代码?
    • 单线程的 JavaScript 如何管理任务?
    • 一个网络请求是怎么进行的?
    • HTTP 协议和前端开发有什么关系?
    • 深入剖析浏览器中页面的渲染过程
    • 改善编程思维:从事件驱动到数据驱动
    • 掌握前端框架模板引擎的实现原理
    • 为什么小程序特立独行
    • 单页应用与前端路由库设计原理
    • 代码构建与 Webpack 必备技能
    • 提升编程体验:组件化与模块化设计
    • AngularReactVue 三大前端框架的设计特色
    • 如何设计合适的状态管理方案
      • 如何搭建前端监控体系为业务排忧解难
      • 如何进行性能分析的自动化实现
      • 前端性能优化与解决方案
      • 如何进行技术方案调研与设计
      • 如何设计一个前端项目
      • 通过前端工程化提升团队开发效率
      • 大型前端项目的痛点和优化方案
      • 如何通过前期准备和后期复盘让项目稳定上线
    • JavaScript

    • 3D地图相关

    • 前端
    • 前端开发笔记
    花椒和邻居
    2022-04-23
    目录

    如何设计合适的状态管理方案

    在前端应用的开发过程中,对于规模较小、各个模块间依赖较少的项目,一般不会引入复杂的状态管理工具。

    但实际上,业务的发展和变化很快,随着我们项目在不断地变大、各个页面中需要共享和通信的内容越来越多,与此同时项目也加入了新成员、不同开发的习惯不一致,项目到后期可能会出现事件的传递、全局数据满天飞的情况。

    这样会有什么隐患呢?当我们想要改某个状态数据的时候,无法直观确认这些改动会不会影响到其他地方。更让人头疼的是,在测试的时候并没有发现存在问题(毕竟有在项目规模较大的情况下,回归测试的成本也会越来越重),然后触发了线上事故。

    这是否又意味着所有项目都应该在最初的时候引入状态管理方案呢?除非项目在最初就已经定位为大型或复杂的应用,否则我们则需要根据预期来选型、根据需要引进不同的工具库来进行管理。同时,如果我们的项目在不断的发展和迭代过程中,则可能需要进行状态管理方案的变更。

    关于技术选型和项目设计的内容,我会在后续介绍。因为在掌握如何进行方案选型之前,我们还需要了解现有的各种方案。所以,今天我们主要来认识下常见的前端状态管理方案。

    要介绍数据在前端应用中是怎么流动的,首先我们要了解数据是如何管理的。

    数据管理

    一般来说,各个组件的数据都会在组件自身的实例当中进行维护。但如果一些数据涉及跨组件共享、全局共享的情况,则需要对数据进行统一的保存和维护,大多数情况下我们都会使用共享对象的方式。

    共享对象

    共享对象的原理很简单,当我们需要多个地方使用相同的数据,我们就把它们放置在一个地方,大家都去那里获取和更新。

    Drawing 0.png

    比如,我们可以简单用一个叫globalData.js的文件来管理全局用的数据。

    // globalData.js
    // globalData 用来存全局数据
    let globalData = {};
    // 获取全局数据
    // 传 key 获取对应的值
    // 不传 key 获取全部值
    export function getGlobalData(key) {
      return key ? globalData[key] : globalData;
    }
    // 设置全局数据
    export function setGlobalData(key, value) {
      // 需要传键值对
      if (key === undefined || value === undefined) {
        return;
      }
      globalData = { ...globalData, [key]: value };
      return globalData;
    }
    

    除了全局数据以外,局部数据的管理同样可以使用共享对象的方式进行,我们将需要共享的数据维护在一个对象中,需要的时候则主动进行获取。
    那如果应用中存在多个共享对象,我们又该怎样管理这些对象呢?

    根据模块进行划分

    在大型应用中,维护的全局数据也会更加复杂,通过合适的方式进行拆分,可以更方便地进行管理,比如根据数据的类型、使用场景、涉及的功能,从而拆分模块的方式来进行管理。

    除此之外,我们还可以使用树状结构的方式来管理这些全局对象。

    使用树状结构管理数据

    在介绍第 1 讲的时候,我就有介绍前端页面中,DOM 节点是基于树状结构管理的。同时,前端应用即便通过模块化和组件化一层层地进行了封装,最终依然会呈现为树状。

    因此,我们可以根据组件的树状作用域,结合共享对象的管理,来注入树状的数据结构。比如在 Angular 中,就是使用依赖注入的方式,配合树状的组件管理,来实现数据的共享或是隔离。

    现在,我们知道应用中需要共享的数据可以通过怎样的方式进行管理,那么当数据发生变化的时候,又该怎样通知到依赖方吗?

    数据变更

    前面的第 15 讲中,我介绍了如何将应用和界面抽象成数据管理,其实被抽象的数据也就是我们常说的状态。

    前端项目中的数据并不只是简单地存在于应用中,相互之间会进行交互和影响。数据间的相互作用,便是我们常说的数据通信或是状态管理,包括但不限于以下的一些方式:

    • 事件监听与触发;

    • 单向数据流;

    • 响应式数据流。

    我们来分别看一下。

    事件监听与触发

    事件监听与触发的设计,一般来说会基于发布-订阅模式。想必你也比较熟悉,我们对浏览器点击、输入框的输入操作事件的绑定,就是典型的事件机制。

    我们也可以通过事件管理的方式,来进行数据的交互,比如 Websocket 机制。

    事件监听和触发的原理其实很简单。监听者在进行事件监听后,会被添加到该事件的监听者队列中。事件被触发后,则可以根据该事件的监听者队列,来通知相应的监听者。

    在第 1 讲中,我们介绍了事件委托机制,在很多前端框架中,事件委托会挂载在页面的组件根实例上。除了挂载在组件根实例这样的方式来绑定作为事件中心,我们也可以自行创建一个事件中心来进行管理。

    事件通知机制很方便,可以随意控制触发的时机,也可以任意的地方监听或是触发。但前面也说过,事件机制的弊端也是很明显,就是每一个事件的触发对应一个或者多个监听,关系是一对多。

    设计不合理的地方,甚至可能出现一个事件会被多处触发的问题,常常是事件散落在各个地方、数据流也难以跟踪。需要定位的时候,只能通过全局搜索的方式来跟踪数据的去向,导致维护难度大大上升。

    以简单的博客系统来为例:

    Drawing 1.png

    这里只有事件 1 和事件 2 的触发和监听,存在跨组件甚至跨页面的事件通知。

    如果在一个规模较大的应用中,可能会导致满屏的事件,同时事件之间并没有什么规律可循。这样我们每次改动事件相关的代码,例如多传一个参数、改变某个参数,都可能会导致未知的错误,需要全局搜索出相关的事件,并一一进行回归测试才可以。

    除此之外,绑定的事件如果不能及时销毁甚至会导致内存泄漏等问题。那么,是不是在大型复杂的前端应用中,就不适合使用事件机制呢?并不是。

    在大型前端项目 VS Code 中就使用了事件机制,并配合依赖注入的框架设计了一套事件的管理模式,包括:

    • 提供标准化的Event和Emitter能力;

    • 通过注册Emitter,并对外提供类似生命周期的方法onXxxxx的方式,来进行事件的订阅和监听;

    • 通过提供通用类Disposable,统一管理相关资源的注册和销毁;

    • 通过使用同样的方式this._register()注册事件和订阅事件,将事件相关资源的处理统一挂载到dispose()方法中。

    由于 VS Code 中使用了依赖注入,对于模块的实例创建、销毁等都会统一交给框架来进行,从而很方便地解决了事件机制的弊端。

    很多时候,我们不只是选择某个技术方案这么简单,对于方案如何在项目中落地、是否需要进行适当的调整,都是需要进行思考的。

    下面我们继续看一下,在前端领域中更加普遍的状态管理方式:单向数据流。

    单向数据流

    如今很多前端框架都会搭配使用状态管理工具,来处理应用中的数据变更。

    其中,Vuex/Redux/Flux 这些热门的状态管理工具库设计思想都基于单向数据流,它们采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

    以 Redux 为例,为了保证数据是单向流动的:

    Drawing 2.png

    • 使用单一的数据源:所有的状态数据都存储在store中;

    • 状态数据只读:只能通过action来触发数据变更;

    • 修改无副作用:修改现有数据的副本,并通过传递副本来更新数据。

    在全局数据的使用变频繁之后,我们在定位问题的时候还会遇到不知道这个数据为何改变的情况,因为所有引用到这个全局数据的地方都可能对它进行改变。

    这种情况下,给数据的流动一个方向,则可以方便地跟踪数据的来源和去处。

    同样以博客系统为例:

    Drawing 3.png

    可以看到,使用了单向数据流的状态管理方案之后,数据的变更来源、更新方向都相比事件触发要清晰。因此,也被更多开发者所推崇。

    除了单向数据流的方案以外,前端项目中比较热门的还有响应式的数据管理方式。

    响应式数据流

    响应式数据流的设计基于响应式编程方式,响应式编程同样基于观察者模式,它是一种面向数据流和变化传播的声明式编程方式。在前端领域,常见的异步编程场景包括事件处理、用户输入、HTTP 响应等。

    对于这样的异步数据流,可以使用响应式编程的方式来进行设计,通过订阅某个数据流,可以对数据进行一系列流式处理,例如过滤、计算、转换、合流等,配合函数式编程可以实现很多优秀的场景。

    以 Rxjs 为例,对用户的一些交互,也可以通过订阅的方式来获取需要的信息:

    const observable = Rx.Observable.fromEvent(input, "input") // 监听 input 元素的 input 事件
      .map((e) => e.target.value) // 一旦发生,把事件对象 e 映射成 input 元素的值
      .filter((value) => value.length >= 1) // 接着过滤掉值长度小于 1 的
      .distinctUntilChanged() // 如果该值和过去最新的值相等,则忽略
      .subscribe(
        // subscribe 拿到数据
        (x) => console.log(x),
        (err) => console.error(err)
      );
    // 订阅
    observable.subscribe((x) => console.log(x));
    

    Angular 框架本身推荐使用的状态管理工具便是 Rxjs,在 Angular 中结合依赖注入的方式,来进行数据流的订阅和触发,也可以很方便地对数据源和流向进行管理。

    除了 Rxjs,基于响应式数据流设计的状态管理库包括还有 Mobx。其中,Rxjs 提供了基于可观察对象(Observable)的响应式服务,Mobx 提供了基于状态管理的响应式服务。基于响应式数据流的设计,我们可以通过各种合流的方式、订阅分流的方式,来将应用中的数据流动从头到尾串在一起。这样,我们可以很清晰地知道当前节点上的数据来自哪里,是用户的操作还是来自网络请求。

    对于很多复杂程度较低的前端应用来说,响应式数据流的入门成本比较高。但在一些复杂应用的场景,合理地使用响应式编程,可以有效地降低各个模块间的依赖,更加容易地进行整体数据流动管理和维护。

    除了天然异步的前端、客户端等 GUI 开发以外,响应式编程在大数据处理中也同样拥有高并发、分布式、依赖解耦等优势,在这种同步阻塞转异步的并发场景下会有较大的性能提升,淘宝业务架构就是使用响应式的架构。

    小结

    我们的项目里,常常会面临应用中某些状态和数据相互影响、相互依赖的问题。

    今天我介绍了一些常见的状态管理的解决方案,包括事件监听、单向数据流、响应式数据流等。那么,它们之间有明显的优劣吗?

    其实最终还是取决于项目本身的架构、业务场景,对于简单的项目来说,使用事件监听就可以方便地解决父子组件通信的问题;当项目规模增加之后,则可能面临全局事件乱飞的问题,此时使用单向数据流或是响应式数据流的方式都可以解决该问题。

    实际上,复杂的前端项目中常常会同时存在多种状态管理方式,同时也会结合业务本身进行适当的调整。除了选择合适的状态管理方案以外,我们还可以通过设计合理的状态来简化一些复杂的问题场景,比如来解决前端常见的防抖和节流问题,同样可以设计状态机的方式来解决。

    我们在思考如何解决问题的同时,还可以找到问题的根源,并尝试分析和解决。只有这样,我们才可以脱离被层出不穷的问题缠身的状况,更加专注地进行项目优化。

    除了本文介绍的,你还了解到哪些状态管理的方式呢?欢迎在留言区进行讨论~


    # 精选评论

    # **哈:

    问几个问题:Vuex:1. 为什么mutation中只能是同步?2. 为什么必须通过commit mutation的方式更改stateRedux:1. 为什么reducer必须是纯函数?2. 为什么reducer必须返回新的state,而不可以修改原有state?

    去GitHub编辑 (opens new window)
    上次更新: 2022/04/23, 06:56:29
    AngularReactVue 三大前端框架的设计特色
    如何搭建前端监控体系为业务排忧解难

    ← AngularReactVue 三大前端框架的设计特色 如何搭建前端监控体系为业务排忧解难→

    最近更新
    01
    为什么小程序特立独行
    04-23
    02
    单页应用与前端路由库设计原理
    04-23
    03
    代码构建与 Webpack 必备技能
    04-23
    更多文章>
    Theme by Vdoing | Copyright © 2022-2023 花椒和邻居 | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式