Redux学习 & React+Redux实战(connect方法各个参数解读)

本文复制了官网许多概念,稍加整合知识点使之变得更合理完整,以个人的认为比较好的学习路线循序渐进各个知识点,额外也会补充些许知识点。大家若有不解,还请多参阅官网。

本文示例背景(最常见的 Web 类示例): TodoList = Todo list + Add todo button + Show / Hide List

了解Redux

Redux 是 JavaScript 应用程序的状态容器,提供可预测的状态管理。

Redux产生缘由

随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。如果把二者分开,能做的很好,但混到一起,就变得一团糟。

React中的State

一些库如 React 试图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了开发者,需要开发者自己setState()管理组件的状态。Redux就是用来对这些组件进行状态管理的。

现实需求

典型的Web应用程序通常由共享数据的多个UI组件组成。通常,多个组件的任务是负责展示同一对象的不同属性。这个对象表示可随时更改的状态。在多个组件之间保持状态的一致性会是一场噩梦,特别是如果有多个通道用于更新同一个对象。

举个,一个带有购物车的网站。在顶部,我们用一个UI组件显示购物车中的商品数量。我们还可以用另一个UI组件,显示购物车中商品的总价。如果用户点击添加到购物车按钮,则这两个组件应立即更新当前的数据。如果用户从购物车中删除商品、更改数目、使用优惠券或者更改送货地点,则相关的UI组件都应该更新出正确的信息。
可以看到,随着功能范围的扩大,一个简单的购物车将会很难保持数据同步。

Redux使用场景

适用场景

多交互、多数据源。

  • 用户的使用方式复杂;
  • 不同身份的用户有不同的使用方式(比如普通用户和管理员);
  • 多个用户之间可以协作;
  • 与服务器大量交互,或者使用了WebSocket;
  • View需要从多个来源获取数据。
不适用场景

以下应用没有必要使用Redux,否则反而会增加项目的复杂程度,得不偿失。

  • UI层非常简单;
  • 用户使用方式非常简单;
  • 页面之间没有协作;
  • 与服务器没有大量交互。

Redux数据模型图

Redux基于简化版本的Flux框架,Flux是Facebook开发的一个框架。在标准的MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格限制了数据只能在一个方向上流动。

Flux

起初由于Facebook公司业务庞大,代码也随着业务的复杂和增多变得非常庞大,代码由此变得脆弱及不可预测,尤其对于刚接触新业务的开发者来说,这是非常严重的问题。于是Facebook的工程师认为MVC架构无法满足他们对业务拓展的需求,于是开发了Flux。Flux是基于Dispatcher的前端应用架构模式,其名字来自拉丁文的Flow。

Flux的核心思想是利用单向数据流和逻辑单向流来应对MVC架构中出现状态混乱的问题。MVC构架模式的缺点Flux适合复杂项目,且组件数据相互共享的应用。

Flux模型图

Redux学习 & React+Redux实战(connect方法各个参数解读)_第1张图片
Flux由3部分组成:Dispatcher、Store和View。其中,Dispatcher(分发器)用于分发事件;Store用于存储应用状态,同时响应事件并更新数据;View表示视图层,订阅来自Store的数据,渲染到页面。

Flux的核心是单向数据流,其运作方式是:Action -> Dispatcher -> Store -> View

整个流程如下:
(1)创建Action(提供给Dispatcher)。
(2)用户在View层交互(比如单击事件)去触发Action。
(3)Dispatcher收到Action,要求Store进行相应的更新。
(4)Store更新,通知View去更新。
(5)View收到通知,更新页面。

从上面的流程可以得知,Flux中的数据流向是单向的,不会发生双向流动的情况,从而保证了数据流向的清晰脉络。而MVC和MVVM中数据的流向是可以双向的,状态在Model和View之间来回“震荡”,很难追踪和预测数据的变化。

Flux的缺陷主要体现在增加了项目的代码量,使用Flux会让项目带入大量的概念和文件;单元测试难以进行,在Flux中,组件依赖Store等其他依赖,使得编写单元测试非常复杂。

Redux

Redux是Flux衍生的诸多“变种”中最出名的。Redux是一个“可预测的状态容器”,而实质也是Flux里面“单向数据流”的思想,但它充分利用函数式的特性,让整个实现更加优雅纯粹,使用起来也更简单。Redux是超越Flux的一次进化。

Redux学习 & React+Redux实战(connect方法各个参数解读)_第2张图片
在Redux中,所有的数据(比如state)都是保存在store的容器中,一个应用也只能有一个store容器。store本质是一个状态树,保存了所有对象的状态。任何UI组件都可以直接通过store访问特定对象的状态。

要想更改对象的状态,需要分发一个action– (store.dispatch(action))。分发在这里意味着将可执行信息发送到store。当一个store接收到一个action,它将把这个action代理给相关的reducer。之后根 reducer 把多个子 reducer 输出合并成一个单一的 state 树,最终 store 保存了根 reducer 返回的完整 state 树。(目前看不明白没关系,下文Redux数据流会讲解)

Redux三大原则

单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

console.log(store.getState())

优点:便于开发和调试

State 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心竞态条件(race condition)的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。

使用纯函数来执行修改

reducers用来描述 action 如何改变 state tree

function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

Reducer 只是一些纯函数,它接收先前的 stateaction,并返回新的 state

Redux核心概念

State

State (也称为 state tree) ,它表示了 Redux 应用的全部状态,通常为一个多层嵌套的对象,由 store 管理且由 getState() 方法获得。

type State = any  //type指的是变量声明的关键字 var/let/const any指的是任何数据类型

约定俗成,顶层 state 或为一个对象,或像 Map 那样的键-值集合,也可以是任意的数据类型。然而你应尽可能确保 state 可以被序列化,而且不要把什么数据都放进去,导致无法轻松地把 state 转换成 JSON。

为了保持状态的一致性,state对象的代码不能随意修改,只能通过action(行为活动)来更新state中的数据。

实例

例如,todo 应用(记事便签)的 state 可能长这样:

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

todos对象表示需要做的事情(text)及其完成状态(completed);visibilityFilter属性则表示需要显示何种状态的便签事项。

Action

Action 就是一个普通 JavaScript 对象用来描述发生了什么。表示即将要改变 state 的意图。

作用

Action 是用来更新 state 的数据的。
Action 是将数据放入 store 的唯一途径。

强制使用 action 来描述所有变化带来的好处是可以清晰地知道应用中到底发生了什么。如果一些东西改变了,就可以知道为什么变。action 就像是描述发生了什么的指示器。

Action 签名
type Action = Object

约定俗成,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。除了 type 之外,action 对象的结构其实完全取决于你自己。

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

当实际项目通常对外隐藏实际变量

export const ADD_TODO = 'ADD_TODO'

export function addTodo(text) {
  return { type: ADD_TODO, text }
}
Action 创建函数

Action 创建函数 就是生成 action 的方法。在 Redux 中的 action 创建函数只是简单的返回一个 action,这样做将使 action 创建函数更容易被移植和测试。

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

之后便可以直接通过store.dispatch(addTodo(text))发起一次dispatch 过程,也减少许多代码量。

Reducer

Reducer是用来把 action 和 state 串起来的纯函数。

纯函数:一个函数的返回结果只依赖于它的参数,相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,这个函数就是纯函数。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

副作用操作:数据获取(发送网络请求),设置订阅以及手动更改 React 组件中的 DOM等。

Reducer 签名

Reducer (也称为 reducing function) 函数接受两个参数:分别是当前的 state 树和要处理的 action,返回新的 state 树。

type Reducer<S, A> = (state: S, action: A) => S
  • 禁止修改传入参数;

  • 禁止执行有副作用的操作,如 API 请求和路由跳转;

  • 禁止调用非纯函数,如 Date.now()Math.random()

  • 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state

返回一个新对象,常用的方法是Object.assign({},{a:'x'}),也可以使用对象展开符{ ...state, ...newState }等等

state 初始状态

Redux 首次执行时,stateundefined,此时我们可借机设置并返回应用的初始 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
}
拆分与合并

对于大的应用来说,不大可能仅仅只写一个这样的函数,所以我们编写很多小函数来分别管理 state 的一部分,之后再将各个小函数绑在一起。

注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

合并这两个 reducer,进而来管理整个应用的 state

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

Redux 提供了 combineReducers() 工具类合并reducer

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

Store

store 维持着应用的 state tree 对象。 Redux 应用只有一个单一的 store。使用根reducer来构建store而不是创建多个 store

store用来将action,state,reducer联系到一起对象:

  • 维持应用的 state
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。
type Store = {
  dispatch: Dispatch
  getState: () => State
  subscribe: (listener: () => void) => () => void
  replaceReducer: (reducer: Reducer) => void
}
  • replaceReducer(nextReducer) 可用于热重载和代码分割。通常不需要用到这个 API。
注册和注销监听器

subscribe当store中数据发生变化就会触发。

// subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() => console.log(store.getState()))

// 停止监听 state 更新
unsubscribe()
Store Creator

创建一个 Redux store 来以存放应用中所有的 state

type StoreCreator = (reducer: Reducer, initialState: ?State) => Store

[preloadedState] (any): 初始时的 state。

还有一种写法:

type StoreCreator =createStore(reducer, [preloadedState], enhancer)

enhancer (Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。一般用于增加Store的功能,但不影响原有效果,具体看下文中间件部分。

import { createStore } from 'redux'

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

let store = createStore(todos, ['Use Redux'])

store.dispatch({
  type: 'ADD_TODO',
  text: 'Read the docs'
})

console.log(store.getState())
// [ 'Use Redux', 'Read the docs' ]
拓展:compose(…functions)

当需要把多个 store 增强器 依次执行的时候,需要用到它。从右到左来组合多个函数。

参数:
(arguments): 需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数,以此类推。例外是最右边的参数可以接受多个参数,因为它将为由此产生的函数提供签名。

compose(funcA, funcB, funcC) 实则 compose(funcA(funcB(funcC()))))

返回值

(Function): 从右到左把接收到的函数合成后的最终函数。

示例

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers'

const store = createStore(
  reducer,
  compose(
    applyMiddleware(thunk),
    DevTools.instrument()
  )
)

Hello Redux

虽然官方有推荐使用脚手架构建redux项目,但还是想试试自己手动创建的效果。

手动创建

1.初始化项目
mkdir first-redux
cd first-redux
npm init -y
2.安装redux
npm install redux

当然还有配套的软件包,暂时没用,先不装

npm install react-redux
npm install --save-dev redux-devtools
3.编码
//index.mjs
import { createStore } from "redux";

//Reducer
function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

//Store
const store=createStore(counter);

//Action Listener [Optional]
store.subscribe(()=>console.log(store.getState()))

//Dispatch Action
store.dispatch({type:'INCREMENT'})
store.dispatch({type:'INCREMENT'})
store.dispatch({type:'DECREMENT'})

代码说明:

  1. 从redux包引入createStore()方法
  2. 创建一个名为counter的纯函数作为Reducer。
  3. 创建一个名为store的Redux存储区,它只能使用Reducer作为参数来构造。
  4. 创建一个变化监听器。每当 dispatch action 的时候就会执行,观测状态树数据变化。
  5. 分发Action,同时可观测状态树数据变化
4.编译看结果
node index.mjs

在这里插入图片描述

脚手架集成搭建

1.安装项目

基于 React 和 Redux 创建一个新的应用程序的推荐方式是使用针对 Create React App 的 官方 Redux+JS 模板

npx create-react-app my-app --template redux
2.启动项目
npm run start

Redux学习 & React+Redux实战(connect方法各个参数解读)_第3张图片

3.重构项目

目前的阶段,不建议去解读脚手架的源码,因为此时已经结合了react-redux,还是先由简入深。

(1)删除src文件夹中除index.js以外的所有文件。

(2)打开index.js,删除所有代码,键入以下内容:

import { createStore } from "redux";

//Reducer
function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

//Store
const store=createStore(counter);

//Action Listener [Optional]
store.subscribe(()=>console.log(store.getState()))

//Dispatch Action
store.dispatch({type:'INCREMENT'})
store.dispatch({type:'INCREMENT'})
store.dispatch({type:'DECREMENT'})

(3)启动项目,观察浏览器控制台

npm run start

使用Redux工具调试

1.在浏览器安装Redux Devtools插件
2.在项目中加入redux-devtools-extension包:npm i redux-devtools-extension
3.store引入redux-devtools的enhancer,确保在enhancer数组最后

import { createStore } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension';
//Reducer
function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

//Store
const store=createStore(counter,composeWithDevTools());

//Action Listener [Optional]
//composeWithDevTools将getState()方法置于store.liftedStore对象了
store.subscribe(()=>console.log(store.liftedStore.getState()))

//Dispatch Action
store.dispatch({type:'INCREMENT'})
store.dispatch({type:'INCREMENT'})
store.dispatch({type:'DECREMENT'})

4.启动项目,在浏览器右键进入Redux DexTools
在这里插入图片描述
Redux学习 & React+Redux实战(connect方法各个参数解读)_第4张图片
Redux Devtools很强大。你可以在action, state和diff(方法差异)之间切换。选择左侧面板上的不同action,观察状态树的变化。你还可以通过进度条来播放actions序列。甚至可以通过工具直接分发操作信息。

进一步学习

Redux数据流(Redux 应用中数据的生命周期)

严格的单向数据流是 Redux 架构的设计核心。

Redux 应用中数据的生命周期遵循下面 4 个步骤:

1.派发行动 store.dispatch(action)

Action 就是一个描述“发生了什么”的普通对象。比如:

    { type: 'LIKE_ARTICLE', articleId: 42 }
    { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
    { type: 'ADD_TODO', text: 'Read the Redux docs.' }

你可以在任何地方调用 store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中。

2.Redux store 调用相关的 reducer 处理 action

Store 会把两个参数传入 reducer: 当前的 state 树和 action。例如,在这个 todo 应用中,根 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.'
}

// reducer 返回处理后的应用状态
let nextState = todoApp(previousState, action)
3.根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树

Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。

下面演示 combineReducers() 如何使用。假如你有两个 reducer:一个是 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
}
4.Redux store 保存了根 reducer 返回的完整 state 树

这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state

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

dispatch function

dispatch function是一个接收 action 或者异步 action的函数,该函数要么往 store 分发一个或多个 action,要么不分发任何 action

函数签名
type BaseDispatch = (a: Action) => Action
type Dispatch = (a: Action | AsyncAction) => any

最常见的dispatch function

let next = store.dispatch;//next 此时就是一个dispatch function
普通的dispatch function 和 在中间件中的Base dispatch function

Base dispatch function 总是同步地把 action 与上一次从 store 返回的 state 发往 reducer,然后计算出新的 state

Base dispatch function In Middleware ,事实上,Middleware 封装了 base dispatch function,允许 dispatch function 处理 action 之外的异步 action 。 Middleware 可以改变、延迟、忽略 action 或异步 action ,也可以在传递给下一个 middleware 之前对它们进行加工处理。

Middleware 中间件

Middleware 通过改写 store 的 dispatch 方法来扩展 Redux 。
Middleware 是一个组合 dispatch function 的高阶函数,返回一个新的 dispatch function。
Redux middleware 本身的实现就是一个 store enhancer。

Middleware使用场景

Middleware 最常见的使用场景是无需引用大量代码或依赖类似 Rx 的第三方库实现异步 actions。它可以让你像 dispatch 一般的 actions 那样 dispatch 异步 actions。处理副作用或异步操作,一般会用到 redux-thunk 和 redux-saga 这两个中间件之一。

Middleware 利用复合函数使其可以组合其他函数,可用于记录 action 日志、调用副作用行为,或将异步的 API 调用变为一组同步的 action。

Middleware执行时机

Middleware提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。Redux 应用中数据的生命周期的第一步之后第二步之前的时机。
Redux学习 & React+Redux实战(connect方法各个参数解读)_第5张图片

Middleware链式组合

Middleware 最优秀的特性就是可以被链式组合。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。你可以在一个项目中使用多个独立的第三方 Middleware。

applyMiddleware(...middleware)

…middleware (arguments): 每个 middleware接受 Store 的 dispatch 和 getState 函数作为命名参数,并返回一个函数。该函数会被传入被称为 next 的下一个middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是 ({ getState, dispatch }) => next => action。

type Middleware = ({ getState, dispatch }) => next => action。

{ getState, dispatch }其实就是 store

手写一个记录日志的Middleware

记录日志:当我们的应用中每一个 action 被发起以及每次新的 state 被计算完成时都将它们记录下来,岂不是很好?当程序出现问题时,我们可以通过查阅日志找出是哪个 action 导致了 state 不正确。

能记录日志的方式有很多种,为何最终选取以下这种形式的函数(Middleware)呢?因为目前而言这种最完美。。至于如何一步步递进到Middleware这种形式的,大家可以看下原文。

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

是不是有点懵?这其实是经过ES6 箭头函数 柯里化 后的样子,原来是这样的:

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}
调用中间将 applyMiddleware
import { createStore, combineReducers, applyMiddleware } from 'redux'

function logger(store) {
...
}

const todoApp = combineReducers(reducers)
const store = createStore(
  todoApp,
  // applyMiddleware() 告诉 createStore() 如何处理中间件
  applyMiddleware(logger)
)

现在任何被发送到 store 的 action 都会经过 logger 中间件 。

除了 applyMiddleware,如果你还用了其它 store enhancer,一定要把 applyMiddleware 放到组合链的前面,因为 middleware 可能会包含异步操作。比如,它应该在 redux-devtools 前面,否则 DevTools 就看不到 Promise middleware 里 dispatchaction 了。(上文compose(…functions)部分)

第三方Middleware

官网示例: 使用 Thunk Middleware 来做异步 Action

redux-logger是官方的日志中间件

//伪代码
import { createLogger } from 'redux-logger'
const loggerMiddleware = createLogger()
const store = createStore(
  rootReducer,
  applyMiddleware(
    loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
  )
)

异步 Action

Redux store 仅支持同步数据流。同步操作,每当 dispatch action 时,state 会被立即更新。事实上,我们更多的是需要通过发起网络请求API来改变state

使用 redux-thunk等中间件可以帮助在 Redux 应用中实现异步性。可以将 thunk 看做 store 的 dispatch() 方法的封装器;我们可以使用 thunk action creator 派遣函数或 Promise,而不是返回 action 对象。

Action 状态

当调用异步 API 时,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻(也可能是超时)。

这两个时刻都可能会更改应用的 state;一般情况下,每个 API 请求都需要 dispatch 至少三种 action:

  • 一种通知 reducer 请求开始的 action
    对于这种 actionreducer 可能会切换一下 state 中的 isFetching 标记。以此来告诉 UI 来显示加载界面。

  • 一种通知 reducer 请求成功的 action
    对于这种 actionreducer 可能会把接收到的新数据合并到 state 中,并重置 isFetching。UI 则会隐藏加载界面,并显示接收到的数据。

  • 一种通知 reducer 请求失败的 action
    对于这种 actionreducer 可能会重置 isFetching。另外,有些 reducer 会保存这些失败信息,并在 UI 里显示出来。

为了区分这三种 action,可能在 action 里添加一个专门的 status 字段作为标记位:

{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

又或者为它们定义不同的 type:

{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
实例

起始页引入redux-thunk

//index.js 
//伪代码
import thunkMiddleware from 'redux-thunk'

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // 允许我们使用 封装过的dispatch() 函数
  )
)

之后我们调用dispatch()实际上就是封装过的dispatch()

Redux学习 & React+Redux实战(connect方法各个参数解读)_第6张图片
代码图来自:tonytony

React+Redux

Redux是不依赖于React而存在的,它本身能支持React、Angular、Ember和jQuery等。要让其在React上运行,就得让二者绑定起来去建立连接。于是就有了react-redux,它能将Redux绑定到React上。

尽管如此,Redux 还是和 React 和 Deku 这类库搭配起来用最好,因为这类库允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

安装 React 绑定库:npm install react-redux

实验环境

建议直接先通过React+Redux官方模板建立项目来练习,就不用引入一堆的库包,也免去许多配置,降低学习成本:

npx create-react-app my-app --template redux

然后将src目录下除index.js文件外的所有文件删除,下文案例以此结构展开编码。

启动项目命令:npm run start

Provider

react-redux 提供了两个重要的对象,Provider 和 connect,前者使 React 组件可被连接(connectable),后者把 React 组件和 Redux 的 store 真正连接起来。

先在最顶层创建一个Provider组件,用于将所有的React组件包裹起来,从而使React的所有组件都成为Provider的子组件。然后将创建好的Store作为Provider的属性传递给Provider。

使组件层级中的 connect() 方法都能够获得 Redux store。

//index.js
import React from "react";
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from "react-redux";
// 导入reducer
import reducer from './reducers';
// store 创建
const store = createStore(
  reducer
)
render(
  <Provider store={store}>                        // Provider需要包裹在整个应用组件的外部
    <App />                                       // React组件
  </Provider>,
  document.getElementById('root')

上述代码 组件尚未定义,大家可自行定义,定义后运行项目,也看不到什么效果,因为Provider 需要结合connect来使用的。

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connect用来连接 React 组件与 Redux store,使得 React 组件能用上Redux 的 store 数据。

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])//参数都是可选的

连接操作不会改变原来的组件类,而是返回一个新的已与 Redux store 连接的组件类。怎么理解?

const Comp  = connect(...args);//`...args`是伪代码,表示各个`connect`的参数
console.log(Comp);//function wrapWithConnect(WrappedComponent) {...}

执行connect()返回的是一个与Redux store 连接的函数,WrappedComponent 表示传递到 connect() 函数的原始组件类,再看:

const MyComp=()=>{
    return(
        <div>Hello</div>
    )
}
const Comp  = connect(..args)(MyComp);
console.log(Comp);

看控制台属性:
Redux学习 & React+Redux实战(connect方法各个参数解读)_第7张图片
可见connect(..args)(WrappedComponent) 返回来的才是我们要的绑定了Redux store的组件。接下来我们看下connect方法的几个参数:

mapStateToProps

mapStateToProps,见名知意:将 state 映射(map)到 props 中,将Redux store中的 state tree 附加到 React 组件 props 中 。

如果connect()定义了mapStateToProps参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。

[mapStateToProps(state, [ownProps]): stateProps] (Function)

mapStateToProps函数有两个参数,state(也就是Redux Store)和ownProps(组件自身的props),其中ownProps参数是可选的,该函数必须返回一个纯对象,这个对象会与组件的 props 合并然后下发给React组件使用。如果指定 ownProps,只要组件接收到新的 propsmapStateToProps 也会被调用。


单看概念感觉有点明白,但是却无从着手,下面做个简单的案例:

通过添加按钮触发添加Action,并在组件中实时回显state的数据变化。我们先看下大致效果图:
Redux学习 & React+Redux实战(connect方法各个参数解读)_第8张图片

为此,我们需要建立四个文件:
Redux学习 & React+Redux实战(connect方法各个参数解读)_第9张图片

1.actions.js

export const ADD_TODO='ADD_TODO';

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

2.reducers.js

import { ADD_TODO } from "./actions";

const initialState = {
  todos: [
    {
      text: "Eat food",
      completed: true,
    },
    {
      text: "Exercise",
      completed: false,
    },
  ],
};

export default function toDo(preloadStates = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return Object.assign({}, preloadStates, {
        todos: [
          ...preloadStates.todos,
          { text: action.text, completed: false },
        ],
      });
    default:
      return preloadStates;
  }
}

3.todolist.js

import React from "react";
import { connect } from "react-redux";

const mapStateToProps = (state, ownProps) => {
  console.log({ state, ownProps });
  return state;
};

const ToDoList = (state) => {
  console.log({ state });
  return (
    <>
      <h3>Redux Store</h3>
      {state.todos.map((todo, index) => {
        return (
          <p key={index}>
            {index} {todo.text} {todo.completed ? "完成" : "未完成"}
          </p>
        );
      })}
      <h3>Component Props</h3>
      <p>{state.name}</p>
    </>
  );
};

export default connect(mapStateToProps)(ToDoList);;

4.index.js

import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import reducer from "./reducers";
import { addTodo } from "./actions";
import  ToDoList  from "./todolist";

const store = createStore(reducer);

render(
  <Provider store={store}>
    <button onClick={()=>{store.dispatch(addTodo("Run"+Math.random().toFixed(3)));}}>模拟改变状态树(添加)</button>
    <ToDoList name='hello'></ToDoList>
  </Provider>,
  document.getElementById("root")
);

Redux学习 & React+Redux实战(connect方法各个参数解读)_第10张图片

当然,你不必将 state 中的数据原封不动地传入组件,可以根据 state 中的数据,动态地输出组件需要的(最小)属性。

const mapStateToProps = (state, ownProps) => {
 return ownProps.type>1?state.xx:state.yy
};
mapDispatchToProps

connect 的第二个参数是 mapDispatchToProps,它的功能是,将 dispatch(action)作为 props 绑定到 组件上。mapDispatchToProps方法返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起。

如果你省略connect 中这个 mapDispatchToProps 参数,那么dispatch 会默认注入到你的组件 props 中。如果指定了 ownProps,该参数的值会传递到组件的 props,而且只要组件接收到新 props,mapDispatchToProps 也会被调用。

mapDispatchToProps(dispatch, ownProps): dispatchProps

方式一:

//reducers.js
import React from "react";
import { connect } from "react-redux";
import { finishTodo } from "./actions";

const mapDispatchToProps=(dispatch,ownProps)=>{
    return{
        finish:(text)=>dispatch(finishTodo(text))
    }
}

const ToDoList = (state) => {
  console.log({ state });
  return (
    <>
      <h3>Redux Store</h3>
      {state.todos.map((todo, index) => {
        return (
          <p key={index}>
            {index} {todo.text} {todo.completed ? "完成" : "未完成"} <button onClick={()=>state.finish(todo.text)}>点击完成</button>
          </p>
        );
      })}
      <h3>Component Props</h3>
      <p>{state.name}</p>
    </>
  );
};

export default connect(a=>a,mapDispatchToProps)(ToDoList);

由于 mapDispatchToProps 方法返回了具有finish属性的对象,这个属性也会成为 ToDoList 的 props

如上所示,调用 finishTodo() 只能得到一个 action 对象 {type:'FINISH_TODO'},要触发这个 action 必须在 store 上调用 dispatch 方法。diapatch 正是 mapDispatchToProps 的第一个参数。但是,为了不让 ToDoList 组件感知到 dispatch 的存在,我们需要将 finishTodo函数包装一下,使之成为直接可被调用的函数(即,调用该方法就会触发 dispatch)。

方式二

Redux 本身提供了 bindActionCreators 函数,来将 action 包装成直接可被调用的函数。效果和上文一致:

import { bindActionCreators } from "redux";

const mapDispatchToProps=(dispatch,ownProps)=>{
    return bindActionCreators({
        finish:finishTodo
    },dispatch)
}

Redux学习 & React+Redux实战(connect方法各个参数解读)_第11张图片
要想实现上面的效果,还需稍微改动:
1.actions.js

export const ADD_TODO='ADD_TODO';
export const FINISH_TODO='FINISH_TODO';

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

export function finishTodo(text){
    return{
        type:FINISH_TODO,
        text
    }
}

2.reducers.js

import { ADD_TODO,FINISH_TODO } from "./actions";

...

export default function toDo(preloadStates = initialState, action) {
	...
      case FINISH_TODO:
        return Object.assign({},preloadStates,{todos: preloadStates.todos.map(it=>{
          if(it.text===action.text){
            return { text: action.text, completed: true };
          }
          return it;
        })})
      ...
  }
}
mergeProps

筛选特定值作为组件的props,默认是:Object.assign({}, ownProps, stateProps,dispatchProps) 的结果。

如果指定了这个参数,mapStateToProps() 与 mapDispatchToProps() 的执行结果和组件自身的 props将传入到这个回调函数中。该回调函数返回的对象将作为 props 传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props来筛选部分的 state 数据,或者把 props 中的某个特定变量与 action creator 绑定在一起。如果你省略这个参数,默认情况下返回 Object.assign({}, ownProps, stateProps,dispatchProps) 的结果。

[mergeProps(stateProps, dispatchProps, ownProps): props] (Function)
const mergeProps=(stateProps, dispatchProps, ownProps)=>{
    return Object.assign({}, ownProps, stateProps,dispatchProps);
}
options

[options] (Object) 如果指定这个参数,可以定制 connector 的行为。

  • [pure = true] (Boolean): 如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。
  • [withRef = false] (Boolean): 如果为 true,connector 会保存一个对被包装组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false。

=================================================================================================

行文至此,如有错误,还望指出;若还可以,多多支持。

=================================================================================================
参考文档:
Redux 中文文档
React+Redux前端开发实战
Redux入门教程(快速上手)
Building a Simple CRUD App with React + Redux

你可能感兴趣的:(React)