• Redux


    写在前面

    Redux 是 React 基于 Flux 思想开发的状态管理工具。

    Redux 适用于多交互、多数据源的大型应用,对于一些简单的小应用,使用 Redux 反而是大材小用了。

    Redux 的设计思想就是:整个应用只有一个状态机,这个状态机里存放了整个应用的全部数据信息,和对数据的各种操作。应用中的每个组件用到对应的数据状态时都要从状态机获取,当视图想要改变状态时也需要通过状态机提供的特定接口提交修改的相关信息,然后状态机根据提交的修改申请调用对应的操作改变对应的状态,并将改变后的最新的状态返回给视图。

    这里说下数据和状态的关系,数据是会变化的,数据在某个时刻的快照就是一个状态,就像日常给人拍照一样,人就是那个数据, 照片就是人在某一刻的状态。

    Redux 是可用于 React 框架的状态管理库,还有一个是 React-Redux 是 React 官方的插件,专门用于在 React 项目中更方便地使用 Redux。

    也就是说 Redux 可以脱离 React 框架使用,但 React-Redux 只能用在 React 框架中。

    而且 React-Redux 是在 Redux 基础上进行了更好地封装,因此在开发 React 项目时要使用 Redux ,就首选 React-Redux。

    本篇博客就基于一个基于原生 Redux 的 “todo list” 项目介绍 Redux,下篇博客就使用 React-Redux 改进该项目,以此来搞懂 React-Redux 做了什么样的优化。

    该项目的目录结构如下:

    1. action

    action 是动作的意思,在 Redux 中,改变应用的数据状态的唯一办法就是向状态机提交一个描述改变信息的动作,然后由状态机进行状态的改变。

    Redux 中提交一个动作申请的语句是 store.dispatch(action)

    因此这个 action 就是一个普通的对象,该对象必须包含一个 ``type` 键,键值为状态改变的类型。其余的对象结构由自己来定,剩余的参数都会在动作提交时传递过去。例如:

    {
      type: ''toggle_todo,
      index: 5
    }
    

    1.1 type 定义为常量

    在 Flux 中,传统的想法是将每个 action type 定义为 string 常量,而且常量一般是全大写。

    使用 string 常量在项目很大时是很有用处的,首先,比如有多个地方用到了该 action,如果想要修改该动作类型的名字时,如果有定义常量,直接修改常量的值就行,只需要修改一处,但如果没有使用的话,需要在任何使用该 action 的地方都要修改。其次,在项目的团队维护时,可以将这些动作类型常量汇集到一个文件,这样可以通过这个文件知道项目都有哪些 action 类型,防止类型重名啥的。

    总之,还是建议养成良好的代码结构习惯,将 type 定义为常量,也可进一步将常量抽离为一个文件。

    1.2 使用 Action Creators 创建 action

    关于 action 还有一个约定俗成的做法就是,通常不直接使用 store.dispatch(action) 内联 action 对象,而是使用一个函数创建 action。

    该函数就是 Action Creator ,返回一个 action 对象

    例如,一般不直接如下方式内联 action

    // event handler 里的某处
    dispatch({
      type: 'ADD_TODO',
      text: 'Use Redux'
    })
    

    而是抽离出一个 action 创建文件,用函数创建 action

    actionCreators.js

    export function addTodo(text) {
      return {
        type: 'ADD_TODO',
        text
      }
    }
    

    AddTodo.js

    import { addTodo } from './actionCreators'
    
    // event handler 里的某处
    dispatch(addTodo('Use Redux'))
    

    这些 action 创建函数一般都抽离成一个单独的文件,通过 export / import 的方式使用,项目结构中通常建立一个 actions 目录下放这些 Action Creators

    2. reducer

    reducer 其实就是 Redux 中的状态产生机,reducer 是一个函数:(previousState, action) => newState,其接收当前状态和一个改变当前状态的动作对象,根据动作类型,返回改变后的新状态。

    reducer 指定了应用状态如何响应 actions 提交的变化,对状态的具体改变操作就是在 reducer 中实现的。

    reducer 必须是一个纯函数

    reducer 返回的 newState 必须是个全新的对象,不能返回改变后的之前的 state

    2.1 设计 state 结构

    首先是确定 state 结构,这个 state 结构是整个应用的数据结构,一般是一个单一的对象,把 state 里面的内容当作数据库一样维护。

    以下示例来源于官网:

    {
      visibilityFilter: 'SHOW_ALL',
      todos: [
        {
          text: 'Consider using Redux',
          completed: true,
        },
        {
          text: 'Keep all state in a single tree',
          completed: false
        }
      ]
    }
    

    这个 state 一般有一个初始 state 就是初始状态值,可以在 reducer 函数中初始化,也可以在创建 store 时初始化,在创建 store 时的初始化会覆盖 reducer 函数中的初始化。

    2.2 处理 action

    根据 action 的 type 来进行相应的状态处理。以下示例来源于官网:

    import {
      ADD_TODO,
      TOGGLE_TODO,
      VisibilityFilters
    } from './actions'
    
    function todoApp(state = initialState, action) {
      switch (action.type) {
        case SET_VISIBILITY_FILTER:
          return Object.assign({}, state, {
            visibilityFilter: action.filter
          })
        case ADD_TODO:
          return Object.assign({}, state, {
            todos: [
              ...state.todos,
              {
                text: action.text,
                completed: false
              }
            ]
          })
        case TOGGLE_TODO:
          return Object.assign({}, state, {
            todos: state.todos.map((todo, index) => {
              if (index === action.index) {
                return Object.assign({}, todo, {
                  completed: !todo.completed
                })
              }
              return todo
            })
          })
        default:
          return state
      }
    }
    

    所以通过以上代码可知,对改变 state 的 action 的具体执行操作就是在 reducer 中实现的的。但是上述的 reducer 太过复杂,可以根据其操作的数据进行分类,将 reducer 拆分成多个 reducer。

    如上述的操作中,有对 todo 列表的操作:ADD_TODO 和 TOGGLE_TODO,可以被单独划分成一个 reducer,对筛选列表的操作:SET_VISIBILITY_FILTER 单独成一个 reducer,然后再进行合并。

    2.3 拆分 reducer

    拆分的原则是:一个应用只能有一个主 reducer,主 reducer 接收全局的状态对象和动作并返回全局的新状态对象,划分的子 reducer 接收自己负责的数据状态,返回新的局部的数据状态

    function todos(state = [], action) {
      switch (action.type) {
        case ADD_TODO:
          return [
            ...state,
            {
              text: action.text,
              completed: false
            }
          ]
        case TOGGLE_TODO:
          return state.map((todo, index) => {
            if (index === action.index) {
              return Object.assign({}, todo, {
                completed: !todo.completed
              })
            }
            return todo
          })
        default:
          return state
      }
    }
    
    function visibilityFilter(state = SHOW_ALL, action) {
      switch (action.type) {
        case SET_VISIBILITY_FILTER:
          return action.filter
        default:
          return state
      }
    }
    
    function todoApp(state = {}, action) {
      return {
        visibilityFilter: visibilityFilter(state.visibilityFilter, action),
        todos: todos(state.todos, action)
      }
    }
    
    //Redux 提供了 combineReducers() 工具类来自动合并
    import { combineReducers } from 'redux'
    
    const todoApp = combineReducers({
      visibilityFilter,
      todos
    })
    

    2.4 合并 reducers

    以下两种方式等价:

    const reducer = combineReducers({
      a: doSomethingWithA,
      b: processB,
      c: c
    })
    
    function reducer(state = {}, action) {
      return {
        a: doSomethingWithA(state.a, action),
        b: processB(state.b, action),
        c: c(state.c, action)
      }
    }
    

    项目结构中通常建立一个 reducers 目录下,用于存放主 reducer 和 子 reducer

    3. store

    上述的状态机 reducer 是 Redux 的状态管理机器主体,但是还缺少一个将 action 的触发和 reducer 的调用联合起来的调度员。这个调度员就是 store 实例。

    store 实例是为 UI 和 内部状态提供交互方法的,实现的方法就是暴露出一些供 UI 元素调用的和内部状态调用的接口,让 UI 元素和数据状态联系起来。

    首先得创建一个 store 实例,创建方法就是使用 redux 导出的 createStore 方法。该方法接收的第一个参数是这个应用的 reducer 函数,第二个参数是可选的, 用于设置 state 初始状态。

    import { createStore } from 'redux'
    import todoApp from './reducers'
    let store = createStore(todoApp)
    

    3.1 store.getState()

    store.getState() 方法用于从 store 实例中获取整个应用的整个状态。

    3.1 store.dispatch(action)——UI操作状态

    store.dispatch(action) 就是 store 实例提供给 UI 界面操作应用状态信息的唯一接口。一般用在 UI 页面元素的事件回调中,事件回调一般会改变数据状态,因此就是通过该接口操作数据状态的。

    3.2 store.subscribe(listener)——状态操作UI

    store.subscribe(listener) 中的 listener 是一个函数,该函数会在 store 管理的 state 变化时被调用。

    请注意,在应用状态发生变化时会自动调用该注册的监听函数,并不会自动 render 页面。

    所以该接口需要我们手动调用重新 render 页面。通过该接口在状态改变时操作 UI,执行 render 函数重新渲染 UI。

    index.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import store from './store';
    
    const render = () => ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
      document.getElementById('root')
    );
    
    render()
    
    store.subscribe(render)  //在 render 的地方监听 state 的改变并重新 render
    

    因此上述代码结构中就创建了 store 目录来存放创建的 store 实例,并 export 导出供其他组件使用

    4. redux-thunk

    redux-thunk 是一个 redux 的插件库,专门用于 redux 中的异步请求。

    例如用 ajax 局部请求 todoList 的数据时,因为 reducer 必须是纯函数,因此不能在 reducer 中发起请求。因此常常使用如下方法:

    app.js

    function App() {
        useEffect(()=>{
            ajax('/todos').then((todos) => {
                store.dispatch(fetchTodos(todos));
            });
        },[])
        return (
            <div className="App">
                <AddTodo/>
                <Todos/>
                <Filter/>
            </div>
        );
    }
    

    actions/fetchTodos

    export const fetchTodos = (todos)=>{
        return {
            type: fetch_todos,
            todos
        }
    }
    

    使用了 redux-thunk,将发送 异步请求 的步骤放到了 action 生成函数中,并且使得 store.dispatch() 可以接收一个函数了。如下:

    app.js

    useEffect(()=>{
           store.dispatch(fetchTodos())
    },[])
    

    actions/fetchTodos

    import ajax from '../ajax';
    
    export const fetch_todos = 'fetch_todos';
    
    //可以返回一个含有返回值的函数
    /*export const fetchTodos = () => {
        return (dispatch, getState) => {
            dispatch({type: fetch_todos, isFetching: true})
            return ajax('/todos').then((todos) => {
                dispatch({type: fetch_todos, todos, isFetching: false});
            });
        };
    };*/
    
    export const fetchTodos = () => {
        return (dispatch, getState) => {
            ajax('/todos').then((todos) => {
                dispatch({type: fetch_todos, todos});
            });
        };
    };
    

    store.js

    import { createStore , applyMiddleware} from 'redux'
    import thunk from 'redux-thunk';
    import reducer from '../reducers';
    
    const store = createStore(
        reducer,
        applyMiddleware(thunk)
    )
    
    export default store
    

    源码链接

    完整 Demo 请移步 todoList-redux

  • 相关阅读:
    恢复IE下载对话框[转]
    意外删除Oracle数据文件(dbf),恢复oralce库的解决办法Oracle错误代码:ORA01033
    [转].net的一些问题
    解决了一个ASP.NET无法接受中文参数值的情况
    修改IIS6的默认设置,扩充上传文件的大小
    在ASP.NET中Request取不到正确的中文参数问题解决办法[base64编码/解码]
    使用微软的TreeView控件有的客户端有脚本错误的问题
    [转]几种调用WebService的方法
    电脑操作精典密芨60式 【转】
    初始化时间下列框的脚本
  • 原文地址:https://www.cnblogs.com/lovevin/p/13520603.html
Copyright © 2020-2023  润新知