redux、react-redux、 redux-thunk、 redux-saga 类比学习

2,110 阅读6分钟

前言

提出一个问题,为什么会出现 redux ? 它解决了什么问题呢?
React 官网赫然写着 A JavaScript library for building user interfaces, 意思就是专注于构建用户界面的 JavaScript 库。就是用于将 state / props 的数据、JSX 的代码转换成真实的 html 页面。同 Vue.js 一样,React 并不负责全局状态(数据)的管理。React 中数据传递是自上而下,从父组件传递给子组件的过程。

在项目,我们往往会有这样的需求,根据当前登录的用户的信息展示不同的菜单或者数据,这就需要我们在任意组件中能获取到登录用户的信息,要怎么做呢?如果没有这些框架,我们肯定会想办法将这些全局的信息统一存储到一个地方,设计好对应的数据结构存在一个 js 对象中,并且这个对象必须 单例 的,保证每次获取的数据都是同一个。对于修改逻辑,也要遵循既定的规则,保证每次数据修改都是可追溯的。如果没有这些规则的约定,那么状态(数据)的修改变得混沌无序,代码变得乱七八糟,系统潜在的问题增多。

Redux 出现了,用于项目中状态(数据)的管理。并且 Redux 试图让 state 的变化变得可预测。我们接着向下看。

redux

上图展示了 redux 数据流向的过程。React 的组件或者其他组件通过 store.getState()Store 中取数据。修改数据只能通过 store.dispatch(action)Store 提交修改数据的指令, Store 统一处理数据的修改。Reducers 是对应的逻辑处理函数,接收两个参数 (previousState, action) 分别表示之前的状态 state 的值和操作的指令 action,返回一个新的数据 newState。注意Reducers是一个纯函数,不直接对接收的数据进行修改,返回一个新的数据 newState; 同时,一旦 (previousState, action)确定的,多次执行的结果是一样的。整个过程就是 redux 的三大原则:

  • 单一数据源: store 中数据是单例,全局唯一的。

  • State 是只读的: 唯一改变 state 的方法就是触发 action, store.dispatch(action); 直接修改 state 是不起作用的。

  • reducers 是纯函数 保证在(previousState, action)确定的前提下,多次执行的结果是一样的;并且不直接修改 state ,通过 Store 触发 state 的修改。

两外,redux 重要的三个函数:

//初始化store
const store = createStore(reducers);

//获取 state
store.getState();

// 触发 action,请求修改数据
store.dispatch(action);

// 订阅数据变化的回调函数
store.subscribe(()=>{
   //to do
});

redux 简单使用

不使用 React 或者 Vue,我们单纯的先通过一个例子,对 redux 进行简单使用。通过 webpack 搭建一个小的项目,并 npm install redux -D 加入到项目中。目录结构如下:

redux-demo
├─node_modules
├─build
│      webpack.dev.config.js       //开发模式
│      webpack.pro.config.js       //打包模式
└─src
    │  index.html
    │  index.js                    //入口文件
    ├─actions
    │      actionCreator.js        //action creator; action 制造者
    ├─reducers
    │      index.js                //reducers 逻辑
    └─store
            index.js               // store
│  package-lock.json
│  package.json

我们在 index.js 入口文件中,初始化获取 store 中的数据,并给按钮绑定事件,通过 store.dispatch(addCounter(1))store 发送 action。

//index.js
import store from './store';
import {addCounter} from './actions/actionCreator';

// 初始化
const stateData = store.getState();
document.getElementById("counter").innerHTML = stateData.counter;
document.getElementById("otherUsed").innerHTML = stateData.counter;
document.getElementById("myBtn").addEventListener("click", ()=>{
    // 触发
    store.dispatch(addCounter(1));
})

// 订阅
store.subscribe(()=>{
    const stateData = store.getState();
    document.getElementById("counter").innerHTML = stateData.counter;
    document.getElementById("otherUsed").innerHTML = stateData.counter;
})

Reduces 负责根据 action 的 type 对数据逻辑进行处理:

// reducers/index.js
const defaultState = {
    counter: 10
}
export default (state = defaultState, action)=>{
    if(action.type==="add"){
        const newState = JSON.parse(JSON.stringify(state));
        newState.counter = newState.counter + action.value;
        return newState;
    }
    return state;
}

这个例子就是为了说明 redux 与 react 不是强耦合的,完全可以独立使用。
源码地址:github.com/YY88Xu/redu…

react + redux 结合(todoList)

利用 create-react-app 初始化项目,目录结构如下:

todo-list
│  package-lock.json
│  package.json
│  README.md
│  
├─public
│  │  favicon.ico
│  │  index.html
│  │  logo192.png
│  │  logo512.png
│  │  manifest.json
│  │  robots.txt
│  │  
│  └─api
│          initList.json
│          
└─src
    │  App.css
    │  App.js
    │  App.test.js
    │  index.css
    │  index.js
    │  logo.svg
    │  reportWebVitals.js
    │  setupTests.js
    │  
    ├─common
    │      actionTypes.js
    │      
    ├─react-redux                          // react-redux 实现
    │  ├─actions
    │  │      actionCreator.js
    │  │      
    │  ├─components
    │  │      TodoList.js
    │  │      
    │  ├─reducers
    │  │      index.js
    │  │      
    │  └─store
    │          index.js
    │          
    ├─redux                     			// redux 实现
    │  ├─actions
    │  │      actionCreator.js
    │  │      
    │  ├─components
    │  │      TodoList.js
    │  │      
    │  ├─reducers
    │  │      index.js
    │  │      
    │  └─store
    │          index.js
    │          
    ├─redux-saga                                  // redux-saga 实现
    │  ├─actions
    │  │      actionCreator.js
    │  │      
    │  ├─components
    │  │      TodoList.js
    │  │      
    │  ├─reducers
    │  │      index.js
    │  │      
    │  ├─saga
    │  │      index.js
    │  │      
    │  └─store
    │          index.js
    │          
    └─redux-thunk 			        // redux-thunk 实现
        ├─actions
        │      actionCreator.js
        │      
        ├─components
        │      TodoList.js
        │      
        ├─reducers
        │      index.js
        │      
        └─store
                index.js
actions

函数形式,返回 action 对象,通常具有type属性。负责指令的生成,页面通过 store.dispatch(action)store 发送数据修改的请求。

reducers

一个纯函数,接收两个参数 (previousState, action) 第一个表示修改之前的 state 的值,action 是上一步页面通过 store.dispatch(action)store 传递的 action
reducers 通过 actiontype 的值进行不同的处理,返回一个新的变量 newStatereducers不能直接修改传递进来的 previousState.

store

通过 const store = createStore(reducer); 创建一个 store 对象。

需要导入到 React 组件文件中,通过 this.state = store.getState(); 让组件获取 store 中的数据;

并在 componentDidMount 生命周期函数中订阅 store 中数据的变化:

componentDidMount(){
    store.subscribe(()=>{
        this.setState(store.getState());
    });
}

react-redux

react-redux 是将 react 和 redux 结合使用,我们需要先安装下依赖:

npm install react-redux -D
  • <Provider>组件, 可以简化我们每次导入到 React 组件文件 store 的工作。
import { Provider } from "react-redux";
<Provider store={store}>   //store 传进去
    <TodoList/>
</Provider>
  • mapStateToProps 函数,将 store 中的 state 转为组件的 props
  • mapDispatchToProps 函数, 传入 dispatch 用于触发 action。
  • connect 函数。
const mapStateToProps = (state)=>({
    inputValue: state.inputValue,
    list: state.list
})

const mapDispatchToProps = (dispatch) => ({
    hanldeAdd: ()=>{
        dispatch(addItem());
    },
    changeInputValue:  (e)=>{
        dispatch(changeValue(e.target.value));
    },
    deleteItem: (key)=>{
        dispatch(deleteItem(key))
    }
})
// 接收 mapStateToProps 和 mapDispatchToProps 两个参数,
//并返回一个函数,这个函数接受 TodoList 返回一个封装的组件。
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

异步

两个处理异步的 Redux 的中间件 redux-thunkredux-saga

还是提出一个问题? 中间件指的是哪里和哪里的中间呢?
答案是: actions 和 reducers 之间 “ 动一些手脚 ”,先处理异步的请求,再去触发 dispatchstore 发起操作。

如果没有这些中间件,我们也可以实现相应的业务需求。使用这些中间件是将异步逻辑剥离出来的一种方式,代码整洁,维护性更高。

redux-thunk

同 redux 一起使用,增强了 redux 的功能。之前 actions 返回一个对象,异步的 action 可以返回一个函数:

// actionCreator.js
export const getInitList = ()=> {
    return function(dispatch){
        axios.get("/api/initList.json").then(res=>{
            dispatch(initList(res.data))
        })
    }
}

同时,创建 store 的时候也要加上 redux-thunk 这个中间件:

import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk';
import reducer from '../reducers';

const store = createStore(reducer, applyMiddleware(thunk));

export default store;

详细用法见项目 redux-thunk 文件夹下的代码。

redux-saga

redux-saga 也是 redux 的一个中间件,可以处理异步 action 。通过 takeEvery(GET_INIT_LIST, getInitList) 监听 action.type 是GET_INIT_LIST的action操作,会执行 getInitList 方法,去获取异步数据。

// saga/index.js
import {takeEvery, put} from 'redux-saga/effects';
import { GET_INIT_LIST } from "../../common/actionTypes.js";
import { initList} from "../actions/actionCreator.js";
import axios from 'axios';
function* getInitList(){
    try{
        const res = yield axios.get("/api/initList.json");
        const action = initList(res.data);
        yield put(action);
    }catch(e){
        console.log("initList.json网络请求失败")
    }
}

function* mySaga(){
    yield takeEvery(GET_INIT_LIST, getInitList);
}
export default mySaga;

详细用法见项目 redux-saga 文件夹下的代码。

源码

地址: github.com/YY88Xu/todo…

最后

如果有错误或者不严谨的地方,烦请给予指正,十分感谢。如果喜欢或者有所启发,欢迎点赞,对作者也是一种鼓励。