redux概念介绍

  

  这一部分仅仅介绍react基本的概念,因为react不仅仅可以用在react中,还可以用在其他框架甚至原生 js 中。  所以这里只介绍通用的概念。 

redux使用场景

   redux和vue中的vuex是类似的,他们的使用都是为了使得组件之间数据的通信和实现良好的代码结构。 

  

  但并不是说做react的项目就一定需要使用redux,因为如果项目的通信简单,那么完全没有没有必要。 而如果:

  • 用户的使用方式复杂
  • 与服务器大量交互,或者使用了WebSocket
  • View需要从多个来源获取数据

  则需要使用redux,即多交互、多数据源。

 另外,从组件的角度来看,如果某个组件的状态需要共享、某个状态需要在任何地方都可以拿到、一个组件需要改变全局状态、一个组件需要改变另一个组件状态(实际上前面说四者所提到的都是组件间的通信都是需要redux的

  如果对于上述几种方式也不适用redux,那么状态的管理就非常混乱 。  

  因为web应用是一个状态机,视图和状态是一一对应的。并且对于redux而言,所有的状态都保存在一个对象里面。 

Redux基本概念

1、store

  和vue中的store一样,store可以理解为一个存储数据的仓库,一个应用就这么一个仓库,但本质上这个store是一个对象。 Redux通过 createStore 这个函数,来生成store对象:

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

  即createStore的作用就是为了生成一个store对象。 

2、 state

  state就是当前的状态,那么store和state是什么关系呢? 我们可以认为 store 对象包括所有数据,如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。 

  通过 store.getState() 我们可以得到某一时点的 state。 

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

const state = store.getState();

  Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。

  简单的说: getState() 会返回当前的state树。(当前很重要)。 即state是当前数据的状态。 

3、action

  action从表面理解就是一个动作。

  我们先来理解vue触发变化的方式 。

  我们知道像vue这种mvvm框架实现的是当数据发生变化时,视图同样发生了变化,视图发生变化,也会导致数据的变化,在vue中通过视图使得数据变化的方法只有一种,就是通过mutation来触发变化,而异步的action也是对mutation进行了保证,最终还是通过mutation来变化的。

  而react与此类似, 当数据变化时,视图会发生变化,而当视图发生变化时redux会通过 触发action 使得数据发生变化。 

  而action本身是一个对象,这个对象中的type属性是必须的,表明这个action的名称, 其他属性没有强制要求,但是有一套自己的规范,比如可以有payload(vue中也是广泛使用的)来传递这种变化。 

  

  注意: action是触发state变化的唯一方式, 通过action , 可以改变当前唯一的store对象。 如:

const action = {
  type: 'ADD_TODO',
  payload: 'Learn Redux'
};

  

4、 action creater 

  之前已经说了, 通过view来改变state的唯一方式就是触发action来改变store中的数据,并且这个action是一个对象,但是往往view的触发很多,并且有可能触发的类型相同只是传递的内容不同,那么我们每次都来写一个对象(并且写这个对象时每次都要写相同的类型),是很麻烦的。 所以我们可以定义一个函数来生成 action (实际上就是传递必要的参数返回一个符合action的对象)。 这个函数就是 action creater 。 

  

const ADD_TODO = '添加 TODO';

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

const action = addTodo('Learn Redux');

  如上所示,我们就可以使用addTodo这个action creater很方便地来构造足够多的action。

5、 store.dispath()

  在3和4中我们都在讨论view层通过action来改变store从而改变当前的state,但是action只是一个对象而已,我们怎么才能把这个对象传递到 store 呢? 

  store.dispatch() 就是 view 发出 Action对象的唯一方法。 

    dispatch的中文意思就是派遣、发送的意思。 即将action发送到store, 因为dispatch是为了store服务的,所以也就可以理解为 dispatch 是 store 的方法。 

如下所示:

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

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

通过action creater,我们可以更方便地通过dispatch来发送这个action。

6、 Reducer

  刚刚提到: 我们可以通过store.dispatch()将action送到store仓库,但是将这个action送到仓库之后, store又是怎么处理这个action的呢?  这时就需要用到 reducer 在store中来处理这个action了。  

因此,到这里,我们就能理清楚这几个重要概念的关系了。 dispatch这家伙是store的手下,负责跑腿来运送非常重要的 action, action 可以看做一封信, 只是一个静态的东西, 而view层可以看做写信的东西, 我们可以通过触发view层来写信(发送action)。  那么reducer就是store内部的伙计,这伙计负责接收由 dispatch 送来的信,然后根据信的内容及时的改变store中的内容 。  而store中的内容也是可以随时改变的,然后告诉外面也做出相应的改变。 即 store、view是一个表里如一的汉子!

 

  reducer 在中文来说有还原剂的意思,就是当你action来到的时候,负责按照action的情况来还原当前真正地store

  

  言归正传, reducer实际上就是一个函数,他接收Action和当前的State作为参数, 返回一个新的State。 如下:

const reducer = function (state, action) {
  // ...
  return new_state;
};

  

  下面是一个例子:

const defaultState = 0;
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};

const state = reducer(1, {
  type: 'ADD',
  payload: 2
});

  即首先定义了一个reducer函数,这个函数在内部使用switch来决定当遇到不同类型的 action 时应该怎么处理。 然后紧接着就开始调用这个reducer实现state的更新。 这里的reducer实现的是 state 的添加,当然,我们也可以定义不同的 reducer 来实现 state 的减法等等。 所以说 reducer 一定是和不同的 action 相互对应的,有一个 action ,就要有一个 reducer 来处理这个action。 

   

  而在实际应用中,我们只需要写好reducer函数就好了,而不需要每次都把相应的 action 进行手工调用, 因为这在程序中是不现实的!   即store.dispacth 方法就会触发 Reducer 的自动执行。 所以啊,Store就需要知道相应的Reducer函数! 做法也很简单,就是在生成 store 的时候,将 Reducer 传入到 createStore 方法中,如下所示:

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

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

        疑问上面再生成store的时候,传入了一个reducer, 但是实际上应该有很多个reducer啊,难道要创建很多个 store ?  但是store在一个应用中应该只有一个才对,否则应用的状态就无法管理。 那么我们可以将很多个reducer写成一个数组然后传进去吗?  这里应该怎么解决呢? 

    vue的做法是将所有的类似于reducer的东西放在mutations中,然后Mutations和 state等一块传入 Vuex.Store() 中被导出,接着传入构造函数vue中。 

    如果按照vue的解决思路,那么redux中的reducer也应当是一个对象的集合,其中每一个键值对就是函数名和函数。 是这样吗? 

补充: 所以啊,reducer函数可以写成下面这样(使用switch针对不同的类型):

const chatReducer = (state = defaultState, action = {}) => {
  const { type, payload } = action;
  switch (type) {
    case ADD_CHAT:
      return Object.assign({}, state, {
        chatLog: state.chatLog.concat(payload)
      });
    case CHANGE_STATUS:
      return Object.assign({}, state, {
        statusMessage: payload
      });
    case CHANGE_USERNAME:
      return Object.assign({}, state, {
        userName: payload
      });
    default: return state;
  }
};

为什么这个函数叫做 Reducer 呢?因为它可以作为数组的reduce方法的参数。请看下面的例子,一系列 Action 对象按照顺序作为一个数组 

const actions = [
  { type: 'ADD', payload: 0 },
  { type: 'ADD', payload: 1 },
  { type: 'ADD', payload: 2 }
];

const total = actions.reduce(reducer, 0); // 3

上面代码中,数组actions表示依次有三个 Action,分别是加0、加1和加2。数组的reduce方法接受 Reducer 函数作为参数,就可以直接得到最终的状态3

  

7、 纯函数

  首先得知道什么是纯函数 --- 只要是同样的输入,必定得到同样的输出。 

     《纯函数的好处》这篇文章介绍了纯函数的好处,就是同样地输入就有同样地输出它是可靠的,是可以预测的。 

  

  追求纯的理由:

  • 可缓存性。
  • 可移植性、自文档化 --- 不会有偷偷摸摸的小动作 。(函数式编程所需要的)
  • 可预测性。 
  • 合理性。 

  

  注意,纯函数必须遵守下面的一些约束(因为下面的方法都会导致结果不同): 

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

  

  由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。

// State 是一个对象
function reducer(state, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一个数组
function reducer(state, action) {
  return [...state, newItem];
}

    

  最好把 State 对象设成只读。你没法改变它,要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变的对象

8、 store.subscribe() 

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

  

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

store.subscribe(listener);

  

  即store发生变化之后, listener函数就会自动执行。 

  

  显然,只要把 View 的更新函数(即当state变化时,更新view)(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。

  store.subscribe方法返回一个函数,调用这个函数就可以解除监听。

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

unsubscribe();

  

Reducer的拆分

  Reducer 函数负责生成 State。由于整个应用只有一个 State 对象,包含所有数据,对于大型应用来说,这个 State 必然十分庞大,导致 Reducer 函数也十分庞大。

  即这就是我之前所说的 reducer函数不止一个,应该针对不同类型的action有不同的reducer 。 

  即reducer函数可能是下面这样的:

const chatReducer = (state = defaultState, action = {}) => {
  const { type, payload } = action;
  switch (type) {
    case ADD_CHAT:
      return Object.assign({}, state, {
        chatLog: state.chatLog.concat(payload)
      });
    case CHANGE_STATUS:
      return Object.assign({}, state, {
        statusMessage: payload
      });
    case CHANGE_USERNAME:
      return Object.assign({}, state, {
        userName: payload
      });
    default: return state;
  }
};

  即针对不同的type,我们给出不同的改变state的解决方案。 

  上面代码中,三种 Action 分别改变 State 的三个属性。

ADD_CHAT:chatLog属性
CHANGE_STATUS:statusMessage属性
CHANGE_USERNAME:userName属性

  

  这三个属性之间没有联系,这提示我们可以把 Reducer 函数拆分。不同的函数负责处理不同属性,最终把它们合并成一个大的 Reducer 即可。(这里和vue的思路也是一致的)

  

const chatReducer = (state = defaultState, action = {}) => {
  return {
    chatLog: chatLog(state.chatLog, action),
    statusMessage: statusMessage(state.statusMessage, action),
    userName: userName(state.userName, action)
  }
};

  即reducer函数处理的任务不同,我们将其拆分为3个小的函数,每一个小函数精确地管理state的某个部分。   

  这样一拆,Reducer 就易读易写多了。而且,这种拆分与 React 应用的结构相吻合:一个 React 根组件由很多子组件构成。这就是说,子组件与子 Reducer 完全可以对应。

  另外, Redux 提供了一个 combineReducers 方法,用于Reducer的拆分。 用这个犯法就可以将小的方法合并成一个大的了。 如下所示:

  

import { combineReducers } from 'redux';

const chatReducer = combineReducers({
  chatLog,
  statusMessage,
  userName
})

export default todoApp;

    

  上面的代码通过combineReducers方法将三个子 Reducer 合并成一个大的函数。

  这样的写法利用了es6方法,即在同名的时候可以这么写,那么属性名和属性值指的是同一个,但是如果 不同名称,我们就需要像下面这样写:

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

  总之,combineReducers()做的就是产生一个整体的 Reducer 函数。该函数根据 State 的 key 去执行相应的子 Reducer,并将返回结果合并成一个大的 State 对象。

  

  

  

工作流程

  redux概念介绍

首先,用户发出 Action。

store.dispatch(action);

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

let nextState = todoApp(previousState, action);

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

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

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

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

推荐文章: http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_one_basic_usages.html