Redux学习总结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/liujie19901217/article/details/74552167

Why use Redux?

React 只是 DOM 的一个抽象层,是 MVC 中的 V 层,并不是完整的Web应用解决方案,如果应用的交互比较多,那么只使用React,会使得代码复杂,不易阅读,Redux使用Flux架构的概念,与函数式编程结合,为React提供状态管理,适用于:多交互、多数据源的应用场景。

Redux简介

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。

Redux的工作流程

这里写图片描述
特别说明:图片来源于Redux从设计到源码

相关核心概念

  • Store: 保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。
  • State: Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。
  • Action: State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。
  • Action Creator: View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。
  • Reducer: Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。
  • dispatch: 是View发出Action的唯一方法。

redux整个工作流程

  1. 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法;
  2. 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State;
  3. State一旦有变化,Store就会调用监听函数,来更新View。

特别说明:可以看出,在reudx的整个工作流程中数据都是单向流动的,这种方式保证了流程的清晰。

Redux三大基础原则

  • 单一数据源
    整个应用的state被存储在一棵Object tree 中,他只有一个单一的store
  • state是只读的
    唯一改变state的办法就是触发action,action是一个描述要发生什么的对象
  • 纯函数的形式执行修改
    为了描述action如何改变state tree ,你需要编写reducer,每一个reducer都是一个纯函数

redux基础

Action

State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过store.dispatch()action 传到 store
Action的本质是一个普通的javascript对象,用来表示即将改变 state 的意图。我们约定:action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。 多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。

Action是将数据放入 store 的唯一途径。无论是从 UI 事件、网络回调,还是其他诸如 WebSocket 之类的数据源所获得的数据,最终都会被 dispatch 成 action。

约定俗成:action 必须拥有一个 type 域,它指明了需要被执行的 action type。Type 可以被定义为常量,然后从其他 module 导入。比起用 Symbols 表示 type,使用 String 是更好的方法,因为 string 可以被序列化。
特别注意: Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action,它会运送数据到 Store。

Action Creator

Action Creator 就是生成action的方法,view 要发送多种消息,不一定都要手写,定义一个函数来生成action,会方便很多。
例如:

const ADD_TODO = '添加 TODO';
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}
const action = addTodo('Learn Redux');

reducer(用来更新state)

设计 State 结构

在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。

Action 处理

在确定了 state 对象的结构之后,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(previousState, action) => newState

reducer的形式为 (state, action) => state纯函数
描述了 action 如何把 旧的state 转变成新的 statestate 的形式取决于你,可以是基本类型、数组、对象、甚至是 Immutable.js生成的数据结构。惟一的要点是:当 state 变化时需要返回全新的对象,而不是修改传入的参数。
特别注意:需要谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

纯函数

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

  • 不得改写参数
  • 不能调用系统 I/O 的API
  • 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

我们将以指定 state 的初始状态(initialState)作为开始。Redux 首次执行时,state 为 undefined,此时我们可借机设置并返回应用的初始 state。

import { VisibilityFilters } from './actions';
const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
};

function todoApp(state, action) {
  if (typeof state === 'undefined') {
    return initialState;
  }
  // 这里暂不处理任何 action,
  // 仅返回传入的 state。
  return state;
}

这里一个技巧是使用 ES6 参数默认值语法 来精简代码。

function todoApp(state = initialState, action) {
  // 这里暂不处理任何 action,
  // 仅返回传入的 state。
  return state;
}

现在可以处理 SET_VISIBILITY_FILTER。需要做的只是改变 state 中的 visibilityFilter。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

注意:

  • 不要修改 state
    使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用 { ...state, ...newState }达到相同的目的。
  • 在 default 情况下返回旧的 state
    遇到未知的 action 时,一定要返回旧的 state。

处理多个 action

还有两个 action 需要处理。让我们先处理 ADD_TODO。

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
          }
        ]
      })
    default:
      return state
  }
}

如上,不直接修改 state 中的字段,而是返回新对象。新的 todos 对象就相当于旧的 todos 在末尾加上新建的 todo。而这个新的 todo 又是基于 action 中的数据创建的。
最后,TOGGLE_TODO 的实现也很好理解:

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
    })
  })

我们需要修改数组中指定的数据项而又不希望导致突变, 因此我们的做法是在创建一个新的数组后, 将那些无需修改的项原封不动移入, 接着对需修改的项用新生成的对象替换。

Store

Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Store 就是把它们联系到一起的对象。Store 有以下职责:

  1. 维持应用的 state;
  2. 提供 getState() 方法获取 state;
  3. 提供 dispatch(action) 方法更新 state;
  4. 通过 subscribe(listener) 注册监听器;
  5. 通过 subscribe(listener) 返回的函数注销监听器。

特别注意:
Redux 应用只有一个单一的 store。
根据已有的 reducer 来创建 store 是非常容易的。一般使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传递 createStore()(用来生成store)。

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

createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。

//创建store
let store = createStore(todoApp, window.STATE_FROM_SERVER)

State

Store对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
当前时刻的 State,可以通过store.getState()拿到。

发起actions

import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions';

//打印初始状态
console.log(store.getState());

//每次state更新时,打印日志
//注意subscribe()返回一个函数用来注销监听器
//unsubscribe是subscribe()返回的用来注销监听器的函数
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'));
store.dispatch(addTodo('Learn about reducers'));
store.dispatch(addTodo('Learn about store'));
store.dispatch(toggleTodo(0));
store.dispatch(toggleTodo(1));
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED));

//停止监听 state 更新
unsubscribe();

store.dispatch()

store.dispatch()是 View 发出 Action 的唯一方法。

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

store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});

上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。
需要知道的是: store.dispatch方法会触发 Reducer 的自动执行,不需要我们手动去调用Reducer。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。

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

上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。

store.subscribe()

Store 允许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。

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

store.subscribe(listener);

显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listener,就会实现 View 的自动渲染。
store.subscribe方法返回一个函数,调用这个函数就可以解除监听。

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);
//解除监听
unsubscribe();

数据流

严格的单向数据流是 Redux 架构的设计核心。
Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 调用 store.dispatch(action)
    Action 就是一个描述”发生了什么”的普通对象。比如:
{ 
type: 'ADD_TODO', 
text: 'Read the Redux docs.'
};

注意: 可以在任何地方调用 store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中。
2. Redux store 调用传入的 reducer 函数
Store 会把两个参数传入 reducer当前的 state 树和 action
例如:在应用中,根 reducer 可能接收这样的数据:

 // 当前应用的 state(todos 列表和选中的过滤器)
 let previousState = {
   visibleTodoFilter: 'SHOW_ALL',
   todos: [
     {
       text: 'Read the docs.',
       complete: false
     }
   ]
 }
 // 将要执行的 action(添加一个 todo)
 let action = {
   type: 'ADD_TODO',
   text: 'Understand the flow.'
 }
 // render 返回处理后的应用状态
 let nextState = todoApp(previousState, action);

注意:reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。
3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树
根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。

combineReducers() 的使用方法:
假设你有两个 reducer:
一个是 todo 列表,另一个是当前选择的过滤器设置:

//todo 列表
 function todos(state = [], action) {
   // 省略处理逻辑...
   return nextState;
 }
//当前选择的过滤器设置
 function visibleTodoFilter(state = 'SHOW_ALL', action) {
  // 省略处理逻辑
   return nextState;
 }

 let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 })

当触发 action 后,combineReducers 返回的 todoApp 会负责调用两个 reducer:

 let nextTodos = todos(state.todos, action);
 let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);

然后会把两个结果集合并成一个 state 树:

 return {
   todos: nextTodos,
   visibleTodoFilter: nextVisibleTodoFilter
 };

虽然combineReducers() 是一个很方便的辅助工具,你也可以选择不用,你可以自行实现自己的根 reducer!
4. Redux store 保存了根 reducer 返回的完整 state 树
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。

现在,可以应用新的 state 来更新 UI。如果你使用了 React Redux这类的绑定库,这时就应该调用 component.setState(newState) 来更新。

combineReducers(reducers)

随着应用变得复杂,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分。
combineReducers辅助函数的作用是:把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer调用 createStore
合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。 state 对象的结构由传入的多个 reducer 的 key 决定。

返回值

一个调用 reducers 对象里所有 reducer 的 reducer,并且构造一个与 reducers 对象结构相同的 state 对象。

相关规则

每个传入 combineReducers 的 reducer 都需满足以下规则:

所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。

永远不能返回 undefined。当过早 return 时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 combineReducers 会抛异常。

如果传入的 state 就是 undefined,一定要返回对应 reducer 的初始 state。根据上一条规则,初始 state 禁止使用 undefined。使用 ES6 的默认参数值语法来设置初始 state 很容易,但你也可以手动检查第一个参数是否为 undefined。
最终,state 对象的结构会是这样的:

{
  reducer1: ...
  reducer2: ...
}

通过为传入对象的 reducer 命名不同来控制 state key 的命名。例如,你可以调用combineReducers({ todos: myTodosReducer, counter: myCounterReducer })将 state 结构变为{ todos, counter }
应用中不要创建多个 store!相反,使用 combineReducers 来把多个 reducer 创建成一个根 reducer

在 Redux 中,只有一个 store,但是 combineReducers 让你拥有多个 reducer,同时保持各自负责逻辑块的独立性。

如果 state 是普通对象,永远不要修改它!比如,reducer 里不要使用 Object.assign(state, newData),应该使用 Object.assign({}, state, newData)。这样才不会覆盖旧的 state。也可以使用 Babel 阶段 1 中的 ES7 对象的 spread 操作 特性中的 return { …state, …newData }。

异步操作

Redux的基本流程是这样的:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。

Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
怎样处理异步的情况呢?怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。

中间件(middleware)

为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
(1)Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
(2)View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
想来想去,只有发送 Action 的这个步骤,即store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造。

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

上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。
中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。

中间件的用法

applyMiddleware(…middlewares)

applyMiddleware是 Redux 的原生方法,作用是:将所有中间件组成一个数组,依次执行。源码如下:

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer);
    var dispatch = store.dispatch;
    var chain = [];

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    };
    chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);
    return {...store, dispatch}
  }
}

上面代码中,所有中间件被放进了一个数组chain,然后嵌套执行,最后执行store.dispatch。可以看到,中间件内部(middlewareAPI)可以拿到getState和dispatch这两个方法。

异步操作的基本思路

同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。

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

以向服务器取出数据为例,三种 Action 可以有两种不同的写法。

// 写法一:名称相同,参数不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

// 写法二:名称不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

除了 Action 种类不同,异步操作的 State 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。

let state = {
  // ... 
  isFetching: true,
  didInvalidate: true,
  lastUpdated: 'xxxxxxx'
};

上面代码中,State 的属性isFetching表示是否在抓取数据。didInvalidate表示数据是否过时,lastUpdated表示上一次更新时间。
现在,整个异步操作的思路就很清楚了。
操作开始时,送出一个 Action,触发 State 更新为”正在操作”状态,View 重新渲染
操作结束后,再送出一个 Action,触发 State 更新为”操作结束”状态,View 再一次重新渲染

redux-thunk 中间件

异步操作的一种解决方案就是:写出一个返回函数的 Action Creator,然后使用redux-thunk中间件改造store.dispatch。正常情况下store.dispatch方法的参数只能是对象,不能是函数。但是改造之后,store.dispatch方法的参数可以是函数。

相关API

compose(…functions)

用来从右到左来组合多个函数。
这是函数式编程中的方法,为了方便,被放到了 Redux 里。 当需要把多个 store 增强器 依次执行的时候,需要用到它。

bindActionCreators(actionCreators, dispatch)

把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。

相关参考:
Redux 中文文档
redux middleware 详解
redux applyMiddleware 原理剖析
Redux 入门教程(一):基本用法
Redux 入门教程(二):中间件与异步操作
Redux 入门教程(三):React-Redux 的用法
浅入react-native使用redux
redux学习笔记
Redux系列x:源码解析
React 实践心得:react-redux 之 connect 方法详解
深入浅出 - Redux
关于Redux的一些总结(一):Action & 中间件 & 异步

猜你喜欢

转载自blog.csdn.net/liujie19901217/article/details/74552167
今日推荐