[React-10] 状态管理

传统MVC框架的缺陷

什么是MVC?

mvc

MVC的全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,是一种软件设计典范。

V即View视图是指用户看到并与之交互的界面。

M即Model模型是管理数据 ,很多业务逻辑都在模型中完成。在MVC的三个部件中,模型拥有最多的处理任务。

C即Controller控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。

MVC只是看起来很美

MVC框架的数据流很理想,请求先到Controller, 由Controller调用Model中的数据交给View进行渲染,但是在实际的项目中,又是允许Model和View直接通信的。然后就出现了这样的结果:


mvc的缺陷

Flux

在2013年,Facebook让React亮相的同时推出了Flux框架,React的初衷实际上是用来替代jQuery的,Flux实际上就可以用来替代Backbone.jsEmber.js等一系列MVC架构的前端JS框架。

其实FluxReact里的应用就类似于Vue中的Vuex的作用,但是在Vue中,Vue是完整的mvvm框架,而Vuex只是一个全局的插件。

React只是一个MVC中的V(视图层),只管页面中的渲染,一旦有数据管理的时候,React本身的能力就不足以支撑复杂组件结构的项目,在传统的MVC中,就需要用到Model和Controller。Facebook对于当时世面上的MVC框架并不满意,于是就有了Flux, 但Flux并不是一个MVC框架,他是一种新的思想。

flux

  • View: 视图层

  • ActionCreator(动作创造者):视图层发出的消息(比如mouseClick)

  • Dispatcher(派发器):用来接收Actions、执行回调函数

  • Store(数据层):用来存放应用的状态,一旦发生变动,就提醒Views要更新页面

Flux的流程:

  1. 组件获取到store中保存的数据挂载在自己的状态上

  2. 用户产生了操作,调用actions的方法

  3. actions接收到了用户的操作,进行一系列的逻辑代码、异步操作

  4. 然后actions会创建出对应的action,action带有标识性的属性

  5. actions调用dispatcher的dispatch方法将action传递给dispatcher

  6. dispatcher接收到action并根据标识信息判断之后,调用store的更改数据的方法

  7. store的方法被调用后,更改状态,并触发自己的某一个事件

  8. store更改状态后事件被触发,该事件的处理程序会通知view去获取最新的数据

Redux

React 只是 DOM 的一个抽象层,并不是 Web 应用的完整解决方案。有两个方面,它没涉及。

  • 代码结构

  • 组件之间的通信

2013年 Facebook 提出了 Flux 架构的思想,引发了很多的实现。2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。

如果你不知道是否需要 Redux,那就是不需要它

只有遇到 React 实在解决不了的问题,你才需要 Redux

简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。

  • 用户的使用方式非常简单

  • 用户之间没有协作

  • 不需要与服务器大量交互,也没有使用 WebSocket

  • 视图层(View)只从单一来源获取数据

需要使用Redux的项目:

  • 用户的使用方式复杂

  • 不同身份的用户有不同的使用方式(比如普通用户和管理员)

  • 多个用户之间可以协作

  • 与服务器大量交互,或者使用了WebSocket

  • View要从多个来源获取数据

从组件层面考虑,什么样子的需要Redux:

  • 某个组件的状态,需要共享

  • 某个状态需要在任何地方都可以拿到

  • 一个组件需要改变全局状态

  • 一个组件需要改变另一个组件的状态

Redux的设计思想:

  1. Web 应用是一个状态机,视图与状态是一一对应的。

  2. 所有的状态,保存在一个对象里面(唯一数据源)。

注意:flux、redux都不是必须和react搭配使用的,因为flux和redux是完整的架构,在学习react的时候,只是将react的组件作为redux中的视图层去使用了。

Redux的使用的三大原则:

  • Single Source of Truth(唯一的数据源)

  • State is read-only(状态是只读的)

  • Changes are made with pure function(数据的改变必须通过纯函数完成)

自己实现Redux

不使用react,直接使用原生的html/js来写一个简易的的redux

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 04</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定义一个方法,用于集中管理state和dispatch, changeState改名了,专业的叫法是reducer
    const createStore = (reducer) => {
      // 定义一个初始的state
      let state = null
      // getState用于获取状态
      const getState = () => state
      
      // 定义一个监听器,用于管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)

      // 定义一个dispatch方法,让每次有action传入的时候返回reducer执行之后的结果
      const dispatch = (action) => {
        // 调用reducer来处理数据
        state = reducer(state, action)
        // 让监听器里的所有方法运行
        listeners.forEach(listener => listener())
      }
      //  初始化state
      dispatch({})
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定义一个计数器的状态
    const countState = {
      count: 10
    }
    // 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
    const changeState = (state, action) => {
      // 如果state是null, 就返回countState
      if (!state) return countState
      switch(action.type) {
        // 处理减
        case 'COUNT_DECREMENT':
          return {
            ...state,
            count: state.count - action.number
          }
        // 处理加        
        case 'COUNT_INCREMENT':
          return {
            ...state,
            count: state.count + action.number
          }
        default:
          return state
      }
    }

    // 创建一个store
    const store = createStore(changeState)
    // 定义一个方法用于渲染计数器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染数据
    renderCount()
    // 监听,只要有dispatch,renderCount就会自动运行
    store.subscribe(renderCount)
  </script>
</body>
</html>

使用Redux框架

Redux的流程:

redux

1.store通过reducer创建了初始状态

2.view通过store.getState()获取到了store中保存的state挂载在了自己的状态上

3.用户产生了操作,调用了actions 的方法

4.actions的方法被调用,创建了带有标示性信息的action

5.actions将action通过调用store.dispatch方法发送到了reducer中

6.reducer接收到action并根据标识信息判断之后返回了新的state

7.store的state被reducer更改为新state的时候,store.subscribe方法里的回调函数会执行,此时就可以通知view去重新获取state

Reducer必须是一个纯函数:

Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。Reducer不是只有Redux里才有,数组方法reduce, 它的第一个参数就是一个reducer

纯函数是函数式编程的概念,必须遵守以下一些约束。

  • 不得改写参数

  • 不能调用系统 I/O 的API

  • 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。

// State 是一个对象
function reducer(state = defaultState, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一个数组
function reducer(state = defaultState, action) {
  return [...state, newItem];
}

关于action/reducer/store的更多概念,请查看官网

注意:

  1. 分离reducer的时候,每一个reducer维护的状态都应该不同
  1. 通过store.getState获取到的数据也是会按照reducers去划分的
  1. 划分多个reducer的时候,默认状态只能创建在reducer中,因为划分reducer的目的,就是为了让每一个reducer都去独立管理一部分状态

因为一个应用中只能有一个大的state,这样的话reducer中的代码将会特别特别的多,那么就可以使用combineReducers方法将已经分开的reducer合并到一起

划分reducer:

我们可以通过在createStore中传入第二个参数来设置默认的state,但是这种形式只适合于只有一个reducer的时候。

最好把 State 对象设成只读。要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变(immutable)的对象。
Redux异步**

通常情况下,action只是一个对象,不能包含异步操作,这导致了很多创建action的逻辑只能写在组件中,代码量较多也不便于复用,同时对该部分代码测试的时候也比较困难,组件的业务逻辑也不清晰,使用中间件了之后,可以通过actionCreator异步编写action,这样代码就会拆分到actionCreator中,可维护性大大提高,可以方便于测试、复用,同时actionCreator还集成了异步操作中不同的action派发机制,减少编码过程中的代码量

常见的异步库:

  • Redux-thunk(就用这个)
  • Redux-saga
  • Redux-effects
  • Redux-side-effects
  • Redux-loop
  • Redux-observable

基于Promise的异步库:

  • Redux-promise
  • Redux-promises
  • Redux-simple-promise
  • Redux-promise-middleware

容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components)

展示组件 容器组件
作用 描述如何展现(骨架、样式) 描述如何运行(数据获取、状态更新)
直接使用 Redux
数据来源 props 监听 Redux state
数据修改 从 props 调用回调函数 向 Redux 派发 actions
调用方式 手动 通常由 React Redux 生成

使用react-redux

可以先结合context来手动连接react和redux。

react-redux提供两个核心的api:

  • Provider: 提供store
  • connect: 用于连接容器组件和展示组件
  1. Provider

    根据单一store原则 ,一般只会出现在整个应用程序的最顶层。

  2. connect

    语法格式为

    connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(component)

    一般来说只会用到前面两个,它的作用是:

    • store.getState()的状态转化为展示组件的props
    • actionCreators转化为展示组件props上的方法

特别强调:

官网上的第二个参数为mapDispatchToProps, 实际上就是actionCreators

只要上层中有Provider组件并且提供了store, 那么,子孙级别的任何组件,要想使用store里的状态,都可以通过connect方法进行连接。如果只是想连接actionCreators,可以第一个参数传递为null

Mobx

作为了解的内容,在项目中使用redux的情况更多。

Mobx是一个功能强大,上手非常容易的状态管理工具。redux的作者也曾经向大家推荐过它,在不少情况下可以使用Mobx来替代掉redux。


mobx-flow.png

这张图来自于官网,把这张图理解清楚了。基本上对于mobx的理解就算入门了。

官网有明确的核心概念使用方法,并配有egghead的视频教程。这里就不一一赘述了。

要特别注意当使用 mobx-react 时可以定义一个新的生命周期钩子函数 componentWillReact。当组件因为它观察的数据发生了改变,它会安排重新渲染,这个时候 componentWillReact 会被触发。这使得它很容易追溯渲染并找到导致渲染的操作(action)。

  • componentWillReact 不接收参数

  • componentWillReact 初始化渲染前不会触发 (使用 componentWillMount 替代)

  • componentWillReact 对于 mobx-react@4+, 当接收新的 props 时并在 setState 调用后会触发此钩子

  • 要触发componentWillReact必须在render里面用到被观察的变量

  • 使用Mobx之后不会触发componentWillReceiveProps

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