由redux到react-redux再到rematch

桑睿识
2023-12-01

这比较适合看过redux但看了跟没看一样的同学,帮助记忆理清里面的基础知识

一. Redux

1. Store

Store是保存数据的地方,可以把它看做一个容器。整个应用只能有一个Store。

Redux提供createStore这个函数生成store

import { createStore } from 'redux';
const store = createStore(fn);

// store也可以接受第二个参数:state的初始值,他会覆盖reducer的state参数中的默认初始值
const store = createStore(fn, initState);

2. State

当你想拿到某个时刻的数据state时,需要生成对应的store快照。

const state = store.getState();

3. Action

由于用户触碰不到代码(State),只能通过触碰View来通知代码服务(store)改变State,这个由View发起的通知就是Action。

Action是一个对象,其中type是必须的,表示Action的名称。其他属性可以自由设置。

const action = {
  type: "ADD",
  payload: "data"
}

可以理解为,要触发一个ADD行为,携带的参数是payload

3.3 Action Creator

类似于工厂模式,Action肯定会有很多种消息,因此可以写一个Action Creator来生产一个Action

function AddCreator(text) {
  return {
    type: "ADD",
    payload: text
  }
}

4. Dispatch

你可以将action理解为一个通知单,那么View如何发出这个通知单给代码服务(store)呢,就是通过dispatch

只要给dispatch传入一个action参数,View就把通知单发出去了!

import { createStore } from 'redux';
const store = createStore(fn);

const action = {
  type: "ADD",
  payload: "data"
}

store.dispatch(action);

5.Reducer

View把通知单传给代码服务后,代码服务(Store)总要根据action中的内容去进行相应的处理的。reducer就是用来识别action并进行相应处理的处理器。他接收当前State和Action作为参数,返回一个新的State。

//其实就是先去判断action要执行哪个操作,再把对应的state更新并返回
function reducer = (state = initState, action) => {
  switch(action.type) {
    case 'ADD':
      return state + action.payload;
    default:
      return state
  }
}

注意这个处理器是store的,也就是说,store的创建是伴随reducer参数来初始化的。

import { createStore } from 'redux';
const store = createStore(reducer);

6. Subscribe

Store也提供了subscribe方法来设置监听函数,一旦State发生变化,就会自动执行这个函数。

store.subscribe返回一个函数,调用这个函数可以解除监听。

const listener = function() {
	console.log(store.getState())
}
let unsubscribe = store.subscribe(listener);

// 解除监听
unsubscribe();

过度------------简易使用

你知道的,毕竟store不是react组件中的state,store中state的更新并不会让View也同时变化,你需要手动调用render重新渲染。

const Counter = ({ value, onIncrement, onDecrement }) => (
  <div>
  <h1>{value}</h1>
  <button onClick={onIncrement}>+</button>
  <button onClick={onDecrement}>-</button>
  </div>
);

const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT': return state + 1;
    case 'DECREMENT': return state - 1;
    default: return state;
  }
};

const store = createStore(reducer);

const render = () => {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => store.dispatch({type: 'INCREMENT'})}
      onDecrement={() => store.dispatch({type: 'DECREMENT'})}
    />,
    document.getElementById('root')
  );
};

render();
// 通过监听state的改变来更新视图
store.subscribe(render);

7. 中间件

当我们想要在store的整个流程中添加一些其他功能时,我们该往哪里添加?

处理流程中涉及逻辑进程的主要就是dispatch和reducer了,但reducer要求是纯函数,不能涉及副作用,那只能在dispatch中做手脚了。

先看一下想添加一个日志功能,并将更新后的state打印出来该如何实现:

let next = store.dispatch;
store.dispatch = function changeDispatch(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

上面对store.dispatch进行了改造,添加了一些功能,这就是中间件的雏形。中间件其实就是一个函数。

7.1 applyMiddlewares()

Redux提供了原生方法applyMiddlewares来加入中间件,增强store.dispatch的功能。

他可以将中间件组成一个数组,然后依次执行。

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();

const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(logger, ...otherMiddlewares)
);

8. 如何处理异步操作

Action发出后,过一段时间再执行Reducer,这就是异步。

那么Reducer作为纯函数肯定是不可以在这里添加处理异步逻辑的,那么就得在store.dispatch中做手脚了。

想一想,如果不能同步的进行dispatch(异步) -> reducer,那不如将dispatch分为两步走,dispatch(异步获取) -> dispatch(同步发送action) -> reducer

// 操作发起时的 Action
// 操作成功时的 Action
// 操作失败时的 Action

具体如何改造多次的dispatch可以使用redux-thunk或者redux-promise,他们处理后,dispatch可以接受不同的参数,不再局限于action对象。

二. React-Redux

回忆下之前的Redux,state更新时我们需要设置监听函数实时re-render才能更新View,想要在其他组件中使用store都得引入一下。

React-Redux解决了上述的痛点:

  • 提供Provider把state注入全局,这样子组件就不用单独引入了
  • 响应式。无需我们手动监听,state的更新会自动帮我们更新View
  • 通过connnct把state和dispatch注入到当前组件的props上

1. Provider

React-Redux提供了组件,使得整个app能够访问到store中的数据,Provider是用context封装的。

2. connect

React-Redux将组件分为两种,一种是UI组件,另一种是容器组件。

UI组件只通过接受props来显示UI,容器组件负责管理数据和业务逻辑(也就是承接state和dispatch)

React-Redux提供一个connect方法能够把组件和store连接起来,把state、dispatch方法都放入到当前组件上,同时是state变成响应式。其实就是一个高阶组件包装了一下我们写的UI组件。

// index.js
import React from 'react';
import ReactDom from "react-dom"
//引入Provider
import {Provider} from "react-redux";

import Counter from "./counter/ReactReduxCounter";

import store from "./redux/basic/store"

ReactDom.render(
    <Provider store={store}>
        <Counter></Counter>
    </Provider>,
    document.getElementById("root")   
)

我们看到通过用<Provider/>来包装我们真正的reactDOM,就可以让里面所有的子组件都使用store了。

注意:用到store的子组件应该是通过connect包装后的。

// Counter.js
import React, { Component } from 'react';
import {connect} from "react-redux"

//子组件
class Counter extends Component {
    //方法调用后自动更新数据
    increment=()=>{
        this.props.increment()
    }
    decrement=()=>{
        this.props.decrement()
    }
    render() {
        return (
            <div>
                <div>{this.props.num}</div>
                
                <button onClick={this.increment}>点我+1</button>
                <button onClick={this.decrement}>点我-1</button>
            </div>
        );
    }
}

//该函数作为connect的第一个参数,能拿到state
//映射state到组建的props上
function mapStateToProps(state){
    return {
        num:state.counter
    }
}

//该函数作为connect的第二个参数,能拿到dispatch
//映射dispatch方法到组建的props上
function mapDispatchToProps(dispatch){
    return {
        increment(){
            dispatch({
                type:"increment"
            })
        },
        decrement(){
            dispatch({
                type:"decrement"
            })
        }
    }
}

//connet函数执行返回一个高阶组件
//调用这个高阶组件,传入当前组件Counter作为参数,返回一个增强的Counter组件
//这个增强的组件props里有store的state和dispach方法
export default connect( mapStateToProps , mapDispatchToProps )(Counter)
/// store.js
import { createStore } from "redux"

const defaultState={
    counter:0
}

//纯函数
let reducers =(state = defaultState ,action)=>{
    switch (action.type){
        case "increment":
            console.log("increment")
            return {
                counter:state.counter+1
            }
        case "decrement":
            return {
                counter:state.counter-1
            }
        default :
        return state 
    }
}
const store = createStore(reducers)
export default store

3. mapStateToProps()和mapDispatchToProps()

注意到,上述connect()中传递了两个参数:mapStateToProps()和mapDispatchToProps()。

通过观察connect源码(下一节)发现,他是利用了context封装了一个高阶组件,在封装过程中,会调用上述两个方法,把store中的state和dispatch取出来放到子组件的props里。这样我们在编写组件的时候,就可以无感知的调用state和dispatch了。

  • mapStateToProps

    • 他是一个函数,接受state作为参数,返回一个对象,这个对象是你想从state中取出的数据

    • const mapStateToProps = (state) => {
        return {
          counter: state.counter
        }
      }
      
  • maoDispatchToProps

    • 他是一个函数,接受dispatch和ownProps(容器组件的props)两个对象,返回一个对象,这个对象中的值应该是一个包含可以触发dispatch的方法。

    • const mapDispatchToProps = (dispatch, ownProps) => {
        return {
          increment(num) {
      			dispatch({
              type: "increment",
              payload: num
            })
          }
        }
      }
      

4. connect高阶组件的大致实现

function connect(mapStateToProps, mapDispatchToProps) {
  return function(WrappedComponent) {
    class Connect extends React.Component {
      componentDidMount() {
        // 组件加载完成后订阅store变化,如果store有变化则更新UI
        this.unsubscribe = this.context.store.subscribe(this.handleStoreChange.bind(this));
      }
      componentWillUnmount() {
        // 组件销毁后,取消订阅事件
        this.unsubscribe();
      }
      handleStoreChange() {
        // 更新UI
        this.forceUpdate();
      }
      render() {
        return (
          <WrappedComponent
            {...this.props}
            {...mapStateToProps(this.context.store.getState())} // 参数是store里面的数据
            {...mapDispatchToProps(this.context.store.dispatch)} // 参数是store.dispatch
          />
        );
      }
    }
    Connect.contextTypes = {
      store: PropTypes.object
    };
    return Connect;
  };
}

5. React-Redux的hook版

可能我们觉得每次都要用connect封装一下组件太麻烦了,还要写mapStateToProps和mapDispatchToProps,react-redux为我们提供了一些hooks,比如常用的useSelector和useDispatch。

  • useSelector接收两个参数:selector和equalityFn。第一个参数是一个函数以获取你想要的state,第二个可选参数也是个函数,用于比较状态是否发生改变,如果函数执行结果为false就表示状态发生改变,组件将重新渲染。
  • useDispatch直接调用可获得dispatch
import { useSelector, useDispatch } from 'react-redux'

const Home = (props) => {
  const dispatch = useDispatch();
  const number = useSelector(state => state.number);
  
  const handleClick = () => {
    dispatch({
      type: 'ADD',
      payload: 1
    })
  }
  
  return (
  	<h2>{number}</h2>
    <button onClick={handleClick}/>
  )
}

6. Redux和React-Redux的关系

依初初初学者短见,我们想要在React项目中使用共享数据时,使用的store都是通过Redux提供的createStore创建的store。React-Redux只是帮我们封装了一些方法,以帮助我们在子组件中更便利的使用state和dispatch等。

三. Rematch

如果说React-Redux是在Redux的基础上帮助子组件更便捷的使用state和dispatch。

那Rematch则是在和React-Redux搭配基础上帮我们更具封装性的创建store,加上更便利的使用state和dispatch。

1. 创建store

之前我们是通过redux的createStore的方法来创建store。

import { createStore } from 'redux';
const store = createStore(reducer, initState);

如果还想要添加一些异步处理,那可能还要加入中间件,reducer也变得很复杂。

而Rematch提供了init可以让我们把initState、reducers、异步处理方法封装在一起。

// store.js
import { init } from '@rematch/core';

const count = {
  state: 0,
  reducers: {
    increment: (state, payload) => state + payload,
    decrement: (state, payload) => state - payload
  },
  effects: {  // 带有异步处理数据的reducer
    async incrementAsync(payload) {
      await func();
      this.increment(payload);
    }
  }
}

export const store = init(count);

如果到此为止,你可以用上述生成的store正常的配合react-redux的provider、connect去使用他。

2. 模块化

rematch建议我们将数据分类保存到models文件夹下的不同的文件里。最后在models/index.js中统一导出。

例如:

  • models/count.js
  • Models/user.js
// models/index.js
export { default as count } from './count'
export { default as user } from './user'
// store.js
import { init } from '@rematch/core'
import * as models from './models'

export const store = init({models})

于是我们在每个组件上,可以直接引入store来取到想要的某个模块中的store和dispatch

// counter.js
import { store } from './store.js'

const counter = () => {
  const { counter, user } = store.getState();
  const add = store.dispatch.counter.add;
  const changName = store.dispatch.user.changeName;
	
  const handleClick = () => {
    add({
      type: 'ADD',
      payload: 1
    })
  }
  
  return (
  	<h2>{counter.number}</h2>
    <h2>{user.name}</h2>
    <button onClick={handleClick}/>
  )
}

但注意,以上state不是响应式的,也就是说触发dispatch改变state并不会让视图更新,我们依然需要结合react-redux中的connect或者hook来让其变成响应式。

 类似资料: