Skip to content

Latest commit

 

History

History
661 lines (540 loc) · 23.8 KB

状态管理.md

File metadata and controls

661 lines (540 loc) · 23.8 KB

状态管理

主要介绍两个状态管理方案:

  • Redux
  • Mobx6

Redux

Redux是JavaScript的状态容器,提供可预测化的状态管理

核心状态和工作流程: Store:存储状态的容器,JavaScript对象 View: 视图,HTML⻚⾯ Actions: 对象,描述对状态进⾏怎样的操作 Reducers:函数,操作状态并返回新的状态

Screen Shot 2021-07-29 at 18.27.54

解决了什么问题

在React中组件通信的数据流是单向的, 顶层组件可以通过props属性向下层组件传递数据, ⽽下层组件不能向上层组件传递数据, 要实现下层组件修改数据, 需要上层组件传递修改数据的⽅法到下层组件. 当项⽬越来越⼤的时候, 组件之间传递数据变得越来越困难.

Screen Shot 2021-07-29 at 22.28.15

使⽤Redux管理数据,由于Store独⽴于组件,使得数据管理独⽴于组件,解决了组件与组件之间传递数据困难的问题。 Screen Shot 2021-07-29 at 22.28.25

Redux 工作流程

  • 组件通过 dispatch ⽅法触发 Action
  • Store 接收 Action 并将 Action 分发给 Reducer
  • Reducer 根据 Action 类型对状态进⾏更改并将更改后的状态返回给 Store
  • 组件订阅了Store中的状态,Store中的状态更新会同步到组件

核心API

const reducer = (state=initState, action) => {
  switch(action.type) {
    // ...
  }
};

const store = Redux.createStore(reducer);
store.getState();
store.subscribe(() => {});
store.dispatch({ type: 'xxx', payload: {}});

react-redux 用法

创建context

import { Provider } from 'react-redux';
ReactDOM.render(<Provider store={ store }><App /></Provider>);

通过connect高阶组件传递store的state给组件

import { connect } from 'react-redux';
const mapStateToProps = state => {};
const mapDispatchToProps = dispatch => {};

const connectedX = connect(mapStateToProps, mapDispatchToProps)(X);

Redux 中间件

什么是中间件

中间件允许我们扩展redux应⽤程序。

加入了中间件的工作流程:

Screen Shot 2021-07-29 at 22.32.06

中间件的开发和使用

中间件模板代码:

export default store => next => action => {}

注册并且使用中间件

import { createStore, applymiddleware } from 'redux'
import logger from 'middlewares/logger';

const enhancer = applyMiddleware(logger)
createStore(reducer, 'initState', enhancer);

例子:thunk,允许我们在redux的工作流程中加入异步代码,如果action是是function就执行action(),否则就当做正常同步的action来执行。

// 模拟thunk的实现
const thunk = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState);
  }

  next(action);
}

使用thunk

// 创建一个async action,如果dispatch一个function,就会执行它并且传入dispatch和getState当做实参。
const loadPost = id => async (dispatch, getState) => {
  const posts = await axios.get(`/api/post/${id}`).then(res => res.data);
  dispatch({ type: LOAD_SUCCESS, payload: posts });
}

// 调用
dispatch(loadPosts(3008))

常用的中间件和工具

常用的中间件还有redux saga,主要是用generator来替代async function。 常用的工具:redux-actions,可以简化Action和Reducer的处理,有点类似之前meraki自己写的那些模板,可以一键创建data的模板和各种reducers。

手写redux

createStore 基本上就是存了state和listeners,更新状态的时候调用reducer(state, action)拿到新的state,然后通知调用listeners的callbacks。

applyMiddleware applyMiddleware主要是返回一个新的dispatcher,store的其它的api都不改变。这个新的dispatcher就是chain,调用的时候会一个一个调用middleware,然后最后调用最原始的dispatcher。

bindActionCreators 这个有个小技巧,具体看代码注释

function createStore (reducer, preloadedState, enhancer) {
  if (typeof reducer !== 'function') throw new Error('redcuer必须是函数');
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('enhancer必须是函数')
    }

    return enhancer(createStore)(reducer, preloadedState);
  }

  let state = preloadedState;     // 状态
  let listeners = [];             // 订阅者
  
  const getState = () => state;

  const subscribe = listener => {
    listeners.push(listener);

    let isSubscribed = true;
    return function unsubscribe() {
      if (!isSubscribed) return;
      isSubscribed = false;

      const idx = listeners.indexOf(listener);
      listeners.spilce(idx, 1);
    }
  }

  const dispatch = (action) => {
    if (!isPlainObject(action)) throw new Error('action必须是一个对象');
    if (typeof action.type === 'undefined') throw new Error('action对象中必须有type属性');

    state = reducer(state, action);

    listeners.forEach(cb => cb());
  }

  // 默认调用一次dispatch方法 存储初始状态(通过reducer函数传递的默认状态)
  dispatch({ type: 'initAction' })

  return {
    getState,
    dispatch,
    subscribe,
  }
};

// 判断参数是否是对象类型
// 判断对象的当前原型对象是否和顶层原型对象相同
// redux源码里面就这样写的
function isPlainObject (obj) {
  if (typeof obj !== 'object' || obj === null) return false;

  // 区分数组和对象
  var proto = obj;
  while (Object.getPrototypeOf(proto) != null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto;
}

function applyMiddleware (...middlewares) {
  return function (createStore) {
    return function (reducer, preloadedState) {
      // 创建 store
      const store = createStore(reducer, preloadedState);

      // 阉割版的 store
      const middlewareAPI = {
        getState: store.getState,
        dispatch: store.dispatch
      }

      // 调用中间件的第一层函数 传递阉割版的store对象
      // chian里面现在存了middleware的下面两层函数
      const chain = middlewares.map(middleware => middleware(middlewareAPI));
      const dispatch = compose(...chain)(store.dispatch);

      return {
        ...store,
        dispatch
      }
    }
  }
}

function compose(...chain) {
  // var chain = [...arguments];   // 伪数组转换成真数组

  /* ----------
    假设调用的时候是applyMiddleware(x, y, z):
    这里反着循环,最后一个middleware(z)的next参数就是reducerDispatch,真正的对于reducer的dispatch
    倒数第二个middleware(y)的next参数就是z
    ......
    最后返回第一个middleware(x)的dispatch,实际上是x最里层的函数action => { doSth(); next(action)) }
    调用的时候就会一直next(action)下去,middleware执行完以后,最后一个next(action)实际上就是reducerDispatch(action)
                                                                                       ---------- */
  return reducerDispatch => {
    let dispatch = reducerDispatch;
    for (let i = chain.length - 1; i >= 0; i--) {
      dispatch = chain[i](dispatch);
    }

    return dispatch;
  }
}

function bindActionCreators (actionCreators, dispatch) {
  var boundActionCreators = {};
  for (var key in actionCreators) {
    // 这里如果不用IIFE的话,actionCreators[key]的key永远会是指向循环最后的key
    // 用IIFE可以让每次的key不要释放,每个actionCreators[key]就会指向正确的key
    // 其实就是闭包,原理就是没有IIFE的时候,() => dispatch(actionCreators[key]()) 这个函数在创建的时候是没有调用key的,在函数执行的时候才调用key,这个时候key就会指向最后一个key。但是IIFE的话就每次都调用了key,等于缓存的当时那个snapshot的key值
    (function (key) {
      boundActionCreators[key] = function () {
        dispatch(actionCreators[key]())
      }
    })(key)
  }

  return boundActionCreators;
}

function combineReducers (reducers) {
  // 1. 检查reducer类型 它必须是函数
  const reducerKeys = Object.keys(reducers);

  reducerKeys.forEach(key => {
    if (typeof reducers[key] !== 'function') throw new Error('reducer必须是函数');
  })

  // 2. 调用每一个小的reducer 将每一个小的reducer中返回的状态存储在一个新的大的对象中
  // redux 源码里面就这么做的……
  return function (state, action) {
    const newState = {};
    reducerKeys.forEach(key => {
      const reducer = reducers[key];
      const prevPartialState = state[key];
      const newPartialState = reducer(prevPartialState, action);

      newState[key] = newPartialState
    });

    return newState;
  }
}

一些问题

Redux遵循的三个原则是什么?

  • 单一事实来源:整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。不像组件传来穿去的话就会有多个事实来源,很难debug而且数据流混乱。
  • 状态是只读的:改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。
  • 使用纯函数进行更改:为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。好处是可以track每个state的改变,容易debug。也就是可预测的状态变化。

拓展:redux的优势:

  • 结果的可预测性:加上reducer的pure属性,让state变化本身可预测。只存在一个真实来源,即 store ,让component拿到的状态也可预测,不存在如何将当前状态与动作和应用的其他部分同步的问题。
  • **可维护性:**因为可预测,所以更可未婚。
  • **易于测试:**Redux 的代码主要是小巧、纯粹和独立的功能。这使代码可测试且独立。
  • 服务器端渲染:你只需将服务器上创建的 store 传到客户端即可。这对初始渲染非常有用,并且可以优化应用性能,从而提供更好的用户体验。
  • 开发人员工具:从操作到状态更改,开发人员可以实时跟踪应用中发生的所有事情。
  • 社区和生态系统:Redux 背后有一个巨大的社区,这使得它更加迷人。一个由才华横溢的人组成的大型社区为库的改进做出了贡献,并开发了各种应用。
  • 组织:Redux 准确地说明了代码的组织方式,这使得代码在团队使用时更加一致和简单。## Redux 有哪些优点?

mobox 和 redux 有什么区别

共同点

  • 为了解决状态管理混乱,无法有效同步的问题统一维护管理应用状态
  • 某一状态只有一个可信数据来源(通常命名为store,指状态容器
  • 操作更新状态方式统一,并且可控(通常以action方式提供更新状态的途径)
  • 支持将store与React组件连接,如react-redux,mobx-react

区别

  • Redux更多的是遵循Flux模式的一种实现,是一个 JavaScript库。Mobx是一个响应式编程的状态管理库,它使得状态管理简单可伸缩。
  • redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
  • redux使用plain object保存数据,需要手动处理变化后的操作。mobx适用observable保存数据,数据变化后自动处理响应的操作
  • redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数。mobx中的状态是可变的,可以直接对其进行修改
  • mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维。redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
  • mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测。而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易

Redux 和 Vuex 有什么区别

本质上,redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案

共同思想

  • 单一的数据源
  • 变化可以预测

区别

  • Vuex改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,无需switch,只需在对应的mutation函数里改变state值即可
  • Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可
  • Vuex数据流的顺序是∶View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)

通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;

Redux 中间件是怎么拿到store 和 action? 然后怎么处理?

redux中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个middleware 接受2个参数, Store 的getState 函数和dispatch 函数,分别获得state和action,最终返回一个函数。该函数会被传入 next (下一个 middleware 的 dispatch 方法),并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的形式是 ({ getState,dispatch }) => next => action

手写react-redux的connect

connect负责连接React和Redux,具体做了三件事:

  • 获取state。connect 通过 context获取 Provider 中的 store,通过 store.getState() 获取整个store tree 上所有state。
  • 包装原组件。将state和action通过props的方式传入到原组件内部 wrapWithConnect 返回—个 ReactComponent 对 象 Connect,Connect 重 新 render 外部传入的原组件 WrappedComponent ,并把 connect 中传入的 mapStateToProps,mapDispatchToProps与组件上原有的 props合并后,通过属性的方式传给WrappedComponent
  • 监听store tree变化connect缓存了store tree中state的状态,通过当前state状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发Connect及其子组件的重新渲染
const connect = (mapStateToProps, mapDispatchToProps) => (BaseComponent) => {
  return class Connect extends React.Component {
    // 通过对context调用获取store
    static contextTypes = {
      store: PropTypes.object
    }

    constructor() {
      super()
      this.state = {
        allProps: {}
      }
    }

    // 第一遍需初始化所有组件初始状态
    componentWillMount() {
      const store = this.context.store;
      store.subscribe(() => this.updateProps()); 
      this.updateProps();
    }

    // 执行action后更新props,使组件可以更新至最新状态(类似于setState)
    updateProps() {
      const store = this.context.store;
      const stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {} 

      const defaultDispatchProps = { dispatch: store.dispatch };
      const dispatchProps = mapDispatchToProps
        ? {
          ...defaultDispatchProps,
          ...mapDispatchToProps(store.dispatch, this.props)
        }
        : defaultDispatchProps

      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render() {
      return <BaseComponent { ...this.state.allProps } />
    }
  }
}

MobX 6

概述

MobX 是一个简单的可扩展的状态管理库,无样板代码风格简约。

目前最新版本为 6,版本 4 和版本 5 已不再支持。

在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法。

MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node。

MobX 通常和 React 配合使用,但是在 AngularVue 中也可以使用 MobX。

2. 核心概念

  • observable:被 MobX 跟踪的状态。
  • action:允许修改状态的方法,在严格模式下只有 action 方法被允许修改状态。
  • computed:根据现有状态衍生出来的状态。
  • flow:执行副作用,它是 generator 函数。可以更改状态值。

3. 工作流程

例子:计数器

在组件中显示数值状态,单击[+1]按钮使数值加一,单击[-1]按钮使数值减一。

store

  • 通过 observable 标识状态,使状态可观察
  • 通过 action 标识修改状态的方法,状态只有通过 action 方法修改后才会通知视图更新
import { action, makeObservable, observable } from "mobx"

export default class CounterStore {
  constructor() {
    this.count = 0
    makeObservable(this, {
      count: observable,
      increment: action,
      decrement: action
    })
  }
  increment() {
    this.count += 1
  }
  decrement() {
    this.count -= 1
  }
}

导入store

创建 Store 类的实例对象并将实例对象传递给组件

// App.js
import Counter from "./Counter"
import CounterStore from "../store/Counter"

const counterStore = new CounterStore()

function App() {
  return <Counter counterStore={counterStore} />
}

export default App

跟store交互

在组件中通过 Store 实例对象获取状态以及操作状态的方法

function Counter({ counterStore }) {
  return (
    <Container>
      <Button onClick={() => counterStore.increment()}>
        INCREMENT
      </Button>
      <Button>{counterStore.count}</Button>
      <Button onClick={() => counterStore.decrement()}>
        DECREMENT
      </Button>
    </Container>
  )
}

export default Counter

数据绑定

当组件中使用到的 MobX 管理的状态发生变化后,使视图更新。通过 observer 方法包裹组件实现目的

import { observer } from "mobx-react-lite"

function Counter() { }

export default observer(Counter)

简化组件代码

function Counter({ counterStore }) {
  const { count, increment, decrement } = counterStore
  return (
    <Container>
      <Button border="left" onClick={increment}>
        INCREMENT
      </Button>
      <Button>{count}</Button>
      <Button border="right" onClick={decrement}>
        DECREMENT
      </Button>
    </Container>
  )
}

当代码如上简化(解构 store)后,修改状态的方法中的 this 指向出现了问题,通过 action.bound 强制绑定 this,使 this 指向 Store 实例对象

import { action, makeObservable, observable } from "mobx"

export default class CounterStore {
  constructor() {
    this.count = 0
    makeObservable(this, {
      count: observable,
      increment: action.bound,
      decrement: action.bound
    })
  }
  increment() {
    this.count += 1
  }
  decrement() {
    this.count -= 1
  }
}

总结:状态变化更新视图的必要条件

  1. 状态必须被标记为 observable
  2. 更改状态的方法必须被标记为 action
  3. 组件必须通过 observer 方法包裹

创建 RootStore

在应用中可存在多个 Store,多个 Store 最终要通过 RootStore 管理,在每个组件都需要获取到 RootStore。

// store/index.js
import { createContext, useContext } from "react"
import CounterStore from "./Counter"

class RootStore {
  constructor() {
    this.counterStore = new CounterStore()
  }
}
const rootStore = new RootStore()
const RootStoreContext = createContext()

export const RootStoreProvider = ({ children }) => {
  return (
    <RootStoreContext.Provider value={rootStore}>
      {children}
    </RootStoreContext.Provider>
  )
}

export const useRootStore = () => {
  return useContext(RootStoreContext)
}
// App.js
import { RootStoreProvider } from "../store"
import Counter from "./Counter"

function App() {
  return (
    <RootStoreProvider>
      <Counter />
    </RootStoreProvider>
  )
}

export default App
import { observer } from "mobx-react-lite"
import { useRootStore } from "../store"

function Counter() {
  const { counterStore } = useRootStore()
  const { count, increment, decrement } = counterStore
  return (
    <Container>
      <Button onClick={increment}>
        INCREMENT
      </Button>
      <Button>{count}</Button>
      <Button onClick={decrement}>
        DECREMENT
      </Button>
    </Container>
  )
}

export default observer(Counter)