带你了解redux和react-redux

1.为什么要用Redux

随着单页应用变得越来越复杂,前端代码需要管理各种各样的状态,它可以是服务器的响应,也可能是前端界面的状态。当这个状态变得任意可变,那么你就可能在某个时间点失去对整个应用状态的控制。

Redux 就是为了解决这个问题而诞生的。

简短地说,Redux 为整个应用创建并管理一棵状态树,并通过限制更新发生的时间和方式,而使得整个应用状态的变化变得可以被预测。除此之外,Redux 有着一整套丰富的生态圈,包括教程、中间件、开发者工具及文档,这些都可以在官方文档中找到。

2.什么是Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

Redux是将整个应用状态存储到一个地方上称为store,里面保存着一个状态树store tree,组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图。

3.安装Redux

npm install redux --save

多数情况下,我们还需要使用 React 绑定库和开发者工具。

npm install redux-devtools --save-dev

4.三大原则

在使用 Redux 之前,你必须要谨记它的三大原则:单一数据源、 state 是只读的和使用纯函数执行修改。

1.单一数据源

整个应用的 state 都被储存在一棵树中,并且这棵状态树只存在于唯一一个 store 中。

这使得来自服务端的 state 可以轻易地注入到客户端中;并且,由于是单一的 state 树,代码调试、以及“撤销/重做”这类功能 的 实现也变得轻而易举。

2.只读的 state

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

这就表示无论是用户操作或是请求数据都不能直接修改 state,相反它们只能通过触发 action 来变更当前应用状态。其次,action 就是普通对象,因此它们可以被日志打印、序列化、储存,以及用于调试或测试的后期回放。

3.使用纯函数执行修改

为每个 action 用纯函数编写 reducer 来描述如何修改 state 树

5.核心API

Action

  • Action官方解释:把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
  • Action就是一个普通的对象,其中的type属性是必须的,表示 Action 的名称

redux约定 Action 内使用一个字符串类型的type字段来表示将要执行的动作名称。

{
    type: 'INCREASE'
}

除了type 之外,Action还可以携带需要的数据。

{
    type: 'INCREASE',
    text: 'Learn Redux'
}

Action Creator

 

View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
function increase(text) {
  return {
    type: INCREASE,
    text:text
  }
}
const action = addTodo('Learn Redux')

上面代码中,increase函数就是一个 Action Creator,在 Redux 中的 Action Creator 只是简单的返回一个 action而已

 

在Redux 中只需把 Action Creator的结果传给 dispatch() 方法即可发起一次 dispatch 过程。

dispatch(increase(text))

注意: store 里能直接通过 store.dispatch() 调用dispatch()方法

Reducer

  • Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
  • Reducer 就是一个普通的函数(纯函数)
  • 当被Redux调用的时候会给Reducer传递两个参数:State 和 Action
  • 它会根据 Action 的type属性来对旧的 State 进行操作,返回新的State

 

const defaultState = 10
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case Constants.INCREASE:
      return state + 1
    case Constants.DECREASE:
      return state - 1
    default:
      return state
  }
}
const state = reducer(10, {
  type: Constants.INCREASE
})

上面代码中,reducer函数收到名为INCREASE的 Action 后,就返回一个新的 State,作为加法的计算结果。

实际开发中,Reducer 函数不用像上面这样手动去调用,store.dispatch方法会触发 Reducer 的自动调用,为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入到createStore函数中

 

import { createStore } from 'redux'
import reducer from '../reducer'

// 创建store
const store = createStore(reducer)

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

combineReducers

真正开发项目的时候State会涉及很多功能,在一个Reducer函数中处理所有逻辑会非常混乱,所以需要拆分成多个子Reducer,每个子Reducer只处理它管理的那部分State数据。然后在由一个主rootReducers来专门管理这些子Reducer

Redux提供了一个方法:combineReducers专门来管理这些子Reducer

import {createStore, combineReducers} from 'redux'

const list = (state = [], action) => {
  switch (action.type) {
    case ADD_ITEM:
      return [createItem(action.text), ...state]
    default:
      return state
  }
}

const = (state = defaultState, action) => {
  switch (action.type) {
    case Constants.INCREASE:let nextState = todoApp(previousState, action)
      return state + 1
    case Constants.DECREASE:
      return state - 1
    default:
      return state
  }
}

let rootReducers = combineReducers({list, counter})

 

注意: 当使用combination的时候,combination会把所有子Reducer都执行一遍,子Reducer通过action.type匹配操作,因为是执行所有子Reducer,所以如果两个子Reducer匹配的action.type是一样的,那么都会匹配成功。

Store

  • Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
  • Redux 提供createStore这个函数,用来生成 Store。
  • 再次强调一下: Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer组合 而不是创建多个 store
  • 根据已有的reducer来创建 store 是非常容易的。在前一个章节中,我们使用 combineReducers()将多个 reducer 合并成为一个。现在我们将其导入,并传递给createStore函数。
import { createStore } from 'redux'
import reducer from '../reducer'

const store = createStore(reducer)

Store提供暴露出四个API方法

  • store.getState(): 获取应用当前State。
  • store.subscribe():添加一个变化监听器。
  • store.dispatch():分发 action。修改State。
  • store.replaceReducer():替换 store 当前用来处理 state 的 reducer。
import { createStore } from 'redux'
let { subscribe, dispatch, getState, replaceReducer} = createStore(reducer)

下面是createStore方法的一个简单实现,可以了解一下 Store 是怎么生成的。

let createStore = (reducer) => {
    let state;
    //获取状态对象
    //存放所有的监听函数
    let listeners = [];
    let getState = () => state;
    //提供一个方法供外部调用派发action
    let dispath = (action) => {
        //调用管理员reducer得到新的state
        state = reducer(state, action);
        //执行所有的监听函数
        listeners.forEach((l) => l())
    }
    //订阅状态变化事件,当状态改变发生之后执行监听函数
    let subscribe = (listener) => {
        listeners.push(listener);
    }
    dispath();
    return {
        getState,
        dispath,
        subscribe
    }
}
let combineReducers=(renducers)=>{
    //传入一个renducers管理组,返回的是一个renducer
    return function(state={},action={}){
        let newState={};
        for(var attr in renducers){
            newState[attr]=renducers[attr](state[attr],action)

        }
        return newState;
    }
}
export {createStore,combineReducers};

其实简单来说Redux就是个发布订阅系统。

6.Redux 工作流

 

 

 

Redux工作的流程图

1.首先,用户发出 Action

store.dispatch(action)

2.然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State

let nextState = countReduce(previousState, action)

3.State 一旦有变化,Store 就会调用监听函数

// 设置监听函数
store.subscribe(listener)

4.listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View

function listerner() {
  let newState = store.getState();
  component.setState(newState);   
}

7.Redux使用案例

 

下面是是一个redux使用的案例

通过createStore来创建一个store,创建成功后会返回三个API(subscribedispatchgetState)。我们通过subscribe来订阅store中数据的变化,当有变化时会执行回调函数,通过getState获取最新数据输出,最后我们通过dispatch传入action来触发数据改变。

import { createStore } from 'redux'

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


const actions = {
  increase: () => ({type: 'INCREASE'}),
  decrease: () => ({type: 'DECREASE'})
}


const store = createStore(reducer);

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

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

// 发起一系列 action
store.dispatch(actions.increase()) // {count: 1}
store.dispatch(actions.increase()) // {count: 2}
store.dispatch(actions.increase()) // {count: 3}

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

8.Redux中间件

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

Redux本身就提供了非常强大的数据流管理功能,但这并不是它唯一的强大之处,它还提供了利用中间件来扩展自身功能

 

Redux中间件概念

为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?

  • Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
  • View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
  • Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。

想来想去,只有在发送 Action 的这个步骤,即store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造。

 

简单来讲,Redux middleware 提供了一个分类处理 action 的机会。在 middleware 中,我们可以检阅每一个流过的 action,并挑选出特定类型的 action 进行相应操作,以此来改变 action。这样说起来可能会有点抽象,我们直接来看图,这是在没有中间件情况下的 redux 的数据流:

 

上面是很典型的一次 redux 的数据流的过程,但在增加了 middleware 后,我们就可以在这途中对 action 进行截获,并进行改变。且由于业务场景的多样性,单纯的修改 dispatch 和 reduce 人显然不能满足大家的需要,因此对 redux middleware 的设计是可以自由组合,自由插拔的插件机制。也正是由于这个机制,我们在使用 middleware 时,我们可以通过串联不同的 middleware 来满足日常的开发,每一个 middleware 都可以处理一个相对独立的业务需求且相互串联:

 

如上图所示,派发给 redux Store 的 action 对象,会被 Store 上的多个中间件依次处理,如果把 action 和当前的 state 交给 reducer 处理的过程看做默认存在的中间件,那么其实所有的对 action 的处理都可以有中间件组成的。值得注意的是这些中间件会按照指定的顺序一次处理传入的 action,只有排在前面的中间件完成任务之后,后面的中间件才有机会继续处理 action,同样的,每个中间件都有自己的“熔断”处理,当它认为这个 action 不需要后面的中间件进行处理时,后面的中间件也就不能再对这个 action 进行处理了。

而不同的中间件之所以可以组合使用,是因为 Redux 要求所有的中间件必须提供统一的接口,每个中间件的逻辑虽然不一样,但只要遵循统一的接口就能和redux以及其他的中间件对话了。

理解中间价的机制

由于redux 提供了 applyMiddleware 方法来加载 middleware,因此我们首先可以看一下 redux 中关于 applyMiddleware 的源码:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 利用传入的createStore和reducer和创建一个store
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

从上面的代码我们不难看出,applyMiddleware 这个函数的核心就在于在于组合 compose,通过将不同的 middlewares 一层一层包裹到原生的 dispatch 之上,然后对 middleware 的设计采用柯里化的方式,以便于compose ,从而可以动态产生 next 方法以及保持 store 的一致性。

说起来可能有点绕,直接来看一个啥都不干的中间件是如何实现的:

const doNothingMidddleware = (dispatch, getState) => next => action => next(action)

上面这个函数接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数,但需要注意的是并不是所有的中间件都会用到这两个函数。然后 doNothingMidddleware 返回的函数接受一个 next 类型的参数,这个 next 是一个函数,如果调用了它,就代表着这个中间件完成了自己的职能,并将对 action 控制权交予下一个中间件。但需要注意的是,这个函数还不是处理 action 对象的函数,它所返回的那个以 action 为参数的函数才是。最后以 action 为参数的函数对传入的action 对象进行处理,在这个地方可以进行操作,比如:

1)调动dispatch派发一个新 action 对象

2)调用 getState 获得当前 Redux Store 上的状态

3)调用 next 告诉 Redux 当前中间件工作完毕,让 Redux 调用下一个中间件

4)访问 action 对象 action 上的所有数据

在具有上面这些功能后,一个中间件就足够获取 Store 上的所有信息,也具有足够能力可用之数据的流转。看完上面这个最简单的中间件,下面我们来看一下 redux 中间件内,最出名的中间件 redux-thunk 的实现:

unction createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

redux-thunk的代码很简单,它通过函数是变成的思想来设计的,它让每个函数的功能都尽可能的小,然后通过函数的嵌套组合来实现复杂的功能,我上面写的那个最简单的中间件也是如此(当然那是个瓜皮中间件)。redux-thunk 中间件的功能也很简单。首先检查参数 action 的类型,如果是函数的话,就执行这个 action,并把 dispatch, getState, extraArgument 作为参数传递进去,否则就调用 next 让下一个中间件继续处理 action 。

 

需要注意的是,每个中间件最里层处理 action 参数的函数返回值都会影响 Store 上的 dispatch 函数的返回值,但每个中间件中这个函数返回值可能都不一样。就比如上面这个 react-thunk 中间件,返回的可能是一个 action 函数,也有可能返回的是下一个中间件返回的结果。因此,dispatch 函数调用的返回结果通常是不可控的,我们最好不要依赖于 dispatch 函数的返回值。

redux的异步流

前面我们已经对redux-thunk进行了讨论,它通过多参数的 currying(柯里化函数) 以实现对函数的惰性求值,从而将同步的 action 转为异步的 action。在理解了redux-thunk后,我们在实现数据请求时,action就可以这么写了:

function fetchData(url, params) {
    return (dispatch, getState) => {
        dispatch({
           type: 'FETCHR_INIT', // 请求开始
        });
        fetch(url, params)
            .then(result => {
                dispatch({
                    type: 'FETCHR_SUCCESS', data: result, // 请求成功
                });
            })
            .catch(err => {
                dispatch({
                    type: 'FETCH_ERROR', error: err, //请求失败
                });
            });
        };
}

 

以上就是redux-thunk的简单用法

其实还有很多常用的Redux异步请求的库如:redux-promise, redux-saga等。

 

总结:其实Redux中间件就是对dispatch方法的封装

每个中间件必须要定义成一个函数,返回一个接受 next参数的函数,而这个接受next参数的函数又返回一个接受 action参数的函数 。 next参数本身也是一个函数,中间 件调用这个 next 函数通知 Redux 自己的处理工作已经结束 。

9.React-Redux

react-redux插件是专门用于在React中使用redux的,具有高效且灵活的特性。 react-redux官网地址。

本质上 react-redux也是react高阶组件HOC的一种实现。其基于 容器组件和展示组件相分离 的开发思想来实现的。

其核心是通过两部分来实现:

1、Provider

2、container通过connect来解除手动调用store.subscrible。

Provider

Provider 是react-redux向我们提供的用来作为 顶层组件 的普通组件。它只需要一个属性——store,用来存放我们的顶层 state 然后将它分发给所有 connect 的组件,不论它在哪儿。

作用:所有的容器组件都需要能够访问Redux的store,如果不使用Provider组件,就只能在所有容器组件的属性中传入store,假如在组件树中深层嵌套了容器组件,可能有的展示组件也需要传入store属性。但是使用Provider组件包裹上应用根组件后,应用中的所有容器组件就都能访问到Redux的store了。

provider用法如下,绑定之后,再经过connect处理,就可以在组件中通过props访问对应信息了。

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'


let store = createStore(todoApp)
render(
  // 绑定store
 
   
 
,
  document.getElementById('root')
)

 

 

Provide实现原理

其实react-redux提供的Provide组件是基于react的 React.createContext实现的。

Provider将store传递给子组件,具体如何和组件绑定就是conect做的事情了。

import MyContext from "./MyContext";
// MyContext就是通过React.createContext()生成context  const MyContext = React.createContext(null);

const Provider = ({ store, children }) => {
  return (
    {children}
  );
};

export default Provider;

connect

React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。是一个柯里化(Currying)的函数,它先要接受两个参数:(数据绑定mapStateToProps 和 事件绑定 mapDispatchToProps),再接收一个参数,就是要绑定的组件本身。

作用:connect连接组件和store,该操作并不修改原组件而是返回一个新的增强了关联store的组件。

connect的使用

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

connect有两个参数,分别是 mapStateToProps,mapDispatchToProps

(1) mapStateToProps负责输入逻辑,即将state映射到 UI 组件的参数(props)(外部的数据(即state对象)如何转换为UI 组件的参数)

(2) mapDispatchToProps负责输出逻辑,即将用户对UI 组件的操作映射成 Action。(用户发出的动作如何变为 Action对象,从 UI 组件传出去)

mapStateToProps

返回值,是一个对象.

从字面上理解,就是把状态(store中的state)映射成属性(组件中的props)

const mapStateToProps = (state) => ( // 正常我们在react-redux中会这样书写
  {number: state.number}
)

这样我们在组件中通过this.props就可以获取store中的数据number

mapDispatchToProps

mapDispatchToPropsconnect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。

如果mapDispatchToProps是一个函数,会把dispatch当做一个参数传进去。

 

const mapDispatchToProps = (dispatch) => {
  return {
    onClick: () => {
      dispatch({
        type: 'ADD'
      });
    }
  };
}
// 返回了一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。

bindActionCreators(mapDispatchToProps,this.store.dispatch)

10.React-Redux example

我们来看一个结合了React-Redux事例。

下面是一个计数器组件,它是一个纯的 UI 组件。所有的数据都是从store中取。

class Counter extends Component {
  render() {
    const { value, onIncrease } = this.props
    return (
      
        {value}              
    )   } }

上面代码中,这个 UI 组件有两个参数:value和onIncrease。前者需要从state计算得到,后者需要向外发出 Action。

接着,定义value到state的映射,以及onIncrease到dispatch的映射。

function mapStateToProps(state) {
  return {
    value: state.count
  }
}


function mapDispatchToProps(dispatch) {
  return {
    onIncrease: () => dispatch(increaseAction)
  }
}


// Action Creator
const increaseAction = { type: 'INCREASE' }

然后,使用connect方法生成容器组件。

const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

然后,定义这个组件的 Reducer。

// Reducer
function counter(state = { count: 0 }, action) {
  const count = state.count
  switch (action.type) {
    case 'INCREASE':
      return { count: count + 1 }
    default:
      return state
  }
}

最后,生成store对象,并使用Provider在根组件外面包一层。

const persistedState = loadState();
const store = createStore(
  counter,
);


ReactDOM.render(
  
    
  ,
  document.getElementById('root')
);

虽然这个例子可能很简单,但基本包含了React-Redux的基本用法。

React-Redux总结:

1)通过context可以让Context.Provider子组件都能够访问到同一个数据,通过修改Context.Provider的value可以去重新渲染子组件的数据

2)通过将store放到context让所有子组件去通过redux的模式去修改渲染state

3)直接在组件中混入redux相关内容导致组件复用性很差,所以将redux相关逻辑放入connect封装的高阶组件中,然后通过props的形式传递state给原组件

4)有组件都会接收全部state,需要通过mapStateToProps来描述接收哪些state;修改state也需要获取到dispatch,通过mapDispatchToProps来获取dispatch进行state修改。

5)修改任意state都会导致所有组件重新渲染,原因是mapStateToProps、mapDispatchToProps会返回一个新的对象导致props更新,需要通过在Connect中的shouldComponentUpdate来对两次使用到的state和外部直接传入的props进行对比再决定是否重新渲染

如果组件需要用到的数据用mapStateToProps映射到组件中,如果用不到就不需要映射。

 

最后说白了React-Redux做了两件事,1.作为最顶级的组件,向子组件们分发状态,来让 React 组件响应式地渲染。2.监听子组件的回调,事件有权利回到最顶层影响顶层状态。

 

 

参考《 深入浅出React和Redux 》以及https://redux.js.org/ 

 

你可能感兴趣的:(【React.js点滴知识,】)