Redux 基础知识

核心思想:

(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)Redux 专注于状态管理,把所有的状态都存在一个对象中。

核心概念包括:store、state、action、reducer。

一、概念介绍

1. store

store 就是存放数据的地方,可以把它看作是一个容器。 Redux 应用只有一个单一的 store。redux 提供createStore函数来生成 store,函数参数是 reducer(后面介绍)。

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

2. state

state 是 store 的某个时刻的快照,可以通过 store.getState() 取得当前时刻的 state

const state = store.getState();

3. action

action 用来改变 state。action 是一个对象,其中的 type 属性是必须的,其他的属性一般用来设置改变 state 需要的数据。

const action = {
  type: 'ADD_ONE',
  num: 1
};

store.dispatch() 是发出 action 的唯一方法:

const action = {
  type: 'ADD_ONE',
  num: 1
};
store.dispatch(action);

4. reducer

store 收到 action 以后,必须给出一个新的 state,这样 view 才会发生变化。这种 state 的计算过程就叫做 reducer。它接受 action 和当前 state 作为参数,返回一个新的 state 。

import { createStore } from 'redux';
const store = createStore(reducer);
const reducer = (state = 10, action) => {
  switch (action.type) {
    case 'ADD_ONE':
      return state + action.num;
    default:
      return state;
  }
};

当 store.dispatch 发送过来一个新的 action ,store 就会自动调用 reducer,得到新的 state 。

二、简单实例

//第一步,创建action
const addOne = {
  type: 'ADD',
  num: 1
};
const addTwo = {
  type: 'ADD',
  num: 2
};
const square = {
  type: 'SQUARE'
};

//第二步,创建reducer
let math = (state = 10, action) => {
  switch (action.type) {
    case ADD:
      return state + action.num;
    case SQUARE:
      return state * state;
    default:
      return state;
  }
};

//第三步,创建store
import { createStore } from 'redux';
const store = createStore(math);

//第四步,测试,通过dispatch发出action,并通过getState()取得当前state值
console.log(store.getState()); //默认值为10

store.dispatch(addOne); //发起'+1'的action
console.log(store.getState()); //当前值为10+1=11

store.dispatch(square); //发起'乘方'的action
console.log(store.getState()); //当前值为11*11=121

store.dispatch(addTwo); //发起'+2'的action
console.log(store.getState()); //当前值为121+2=123

三、Redux 工作流

Redux工作流

四、代码组织目录结构

下面对目录结构进行划分

1、一般地,将 action.type 设置为常量,这样有个好处:在书写错误时,会得到报错提示

// constants/ActionTypes.js
export const ADD = 'ADD';
export const SQUARE = 'SQUARE';

2、可以将 addOne 对象和 addTwo 对象整合成 add 函数的形式

// action/math.js
import { ADD, SQUARE } from '../constants/ActionTypes';

export const add = num => ({ type: ADD, num });
export const square = { type: SQUARE };

3、根据 action.type 的分类来拆分 reducer ,最终通过 combineReducers 方法将拆分的 reducer 合并起来。上例中的 action 类型都是数字运算,无需拆分,只需进行如下变化:

// reducer/math.js
import { ADD, SQUARE } from '../constants/ActionTypes';

const math = (state = 10, action) => {
  switch (action.type) {
    case ADD:
      return state + action.num;
    case SQUARE:
      return state * state;
    default:
      return state;
  }
};
export default math;
// reducer/index.js
import { combineReducers } from 'redux';
import math from './math';

const rootReducer = combineReducers({
  math
});
export default rootReducer;

4、将 store 存储到 store/index.js 文件中

// store/index.js
import { createStore } from 'redux';
import rootReducer from '../reducer';

export default createStore(rootReducer);

5、最终,根路径下的 index.js 内容如下所示

import store from './store';
import { add, square } from './action/math';

console.log(store.getState()); //默认值为10

store.dispatch(add(1)); //发起'+1'的action
console.log(store.getState()); //当前值为10+1=11

store.dispatch(square); //发起'乘方'的action
console.log(store.getState()); //当前值为11*11=121

store.dispatch(add(2)); //发起'+2'的action
console.log(store.getState()); //当前值为121+2=123

最终的目录结构:

目录结构

四、UI 层

前面的示例中,只是 redux 的状态改变,下面利用 UI 层来建立 view 和 state 的联系,将根目录下的index.js 的内容更改如下:

import store from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import { add, square } from './action/math';

ReactDOM.render(
  <div store={store}>
    <p>{store.getState().math}</p>
    <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
    <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
    <input type="button" onClick={() => store.dispatch(square)} value="乘方" />
  </div>,
  document.getElementById('root')
);

虽然可以显示数字,但是点击按钮时,却不能重新渲染页面。

1. store.subscribe()

接下来介绍 store.subscribe() 方法了,该方法用来设置监听函数,一旦 state 发生变化,就自动执行这个函数。该方法的返回值是一个函数,调用这个函数可以解除监听。

下面将示例代码更改如下:

import store from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import { add, square } from './action/math';

const render = () =>
  ReactDOM.render(
    <div store={store}>
      <p>{store.getState().math}</p>
      <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
      <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
      <input
        type="button"
        onClick={() => store.dispatch(square)}
        value="乘方"
      />
    </div>,
    document.getElementById('root')
  );

render();
store.subscribe(render);

五、异步

redux 默认只处理同步,对于 API 请求这样的异步任务则无能为力,接下来尝试使用axios的get方法来请求下面这个API。

https://jsonplaceholder.typicode.com/posts/2

获取的数据如下:

{
  "userId": 1,
  "id": 2,
  "title": "qui est esse",
  "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
}

然后,将其 id 值设置为 state.math 的值,代码修改如下:

// constants/ActionTypes.js
export const ADD = 'ADD';
export const SQUARE = 'SQUARE';
export const SET = 'SET';

// action/math.js
import { ADD, SQUARE, SET } from '../constants/ActionTypes';
export const add = num => ({ type: ADD, num });
export const square = { type: SQUARE };
export const setNum = num => ({ type: SET, num });

// reduce/math.js
import { ADD, SQUARE, SET } from '../constants/ActionTypes';
const math = (state = 10, action) => {
  switch (action.type) {
    case ADD:
      return state + action.num;
    case SQUARE:
      return state * state;
    case SET:
      return action.num;
    default:
      return state;
  }
};
export default math;

// index.js
import store from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import { add, square, setNum } from './action/math';
import axios from 'axios';
let uri = 'https://jsonplaceholder.typicode.com/posts/2';
const render = () =>
  ReactDOM.render(
    <div store={store}>
      <p>{store.getState().math}</p>
      <input
        type="button"
        onClick={() => {
          axios.get(uri).then(res => {
            store.dispatch(store.dispatch(setNum(res.data.id)));
          });
        }}
        value="设置Num"
      />
      <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
      <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
      <input
        type="button"
        onClick={() => store.dispatch(square)}
        value="乘方"
      />
    </div>,
    document.getElementById('root')
  );
render();
store.subscribe(render);

但是,虽然 API 是异步操作,但 store.dispatch 并不是异步,而 axios 通过 get 方法请求回来数据后,store.dispatch 在 axios 中的 then 方法中同步取得数据。

如果要使用真正的异步操作,即把 axios 方法封装到 store.dispatch 中,需要使用 redux-thunk 中间件。

1. redux-thunk

首先,使用 npm 进行安装:

$ npm install --save redux-thunk

然后,使用 applyMiddleware 来使用 thunk 中间件:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducer';
export default createStore(rootReducer, applyMiddleware(thunk));

接着来定义 setNum 这个 action creator ,然后在 index.js 文件的 DOM 加载完成后就发出 setNum

[注意]: 如果 action 是一个对象,则它就是一个 action ,如果 action 是一个函数,则它是一个action creator ,即 action 制造器,修改的代码如下:

// action/math.js
import { ADD, SQUARE, SET } from '../constants/ActionTypes';
import axios from 'axios';
const uri = 'https://jsonplaceholder.typicode.com/posts/2';
export const add = num => ({ type: ADD, num });
export const square = { type: SQUARE };
export const setNum = () => (dispatch, getState) => {
  return axios.get(uri).then(res => {
    dispatch({
      type: SET,
      num: res.data.id
    });
  });
};

// index.js
import store from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import { add, square, setNum } from './action/math';
const render = () =>
  ReactDOM.render(
    <div store={store}>
      <p>{store.getState().math}</p>
      <input
        type="button"
        onClick={() => store.dispatch(setNum())}
        value="设置Num"
      />
      <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
      <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
      <input
        type="button"
        onClick={() => store.dispatch(square)}
        value="乘方"
      />
    </div>,
    document.getElementById('root')
  );
render();
store.subscribe(render);

【提示信息】

如果做的更完备一点,应该把异步请求时的提示信息也加上。增加一个 fetch 的 action,用于控制fetch 过程的提示信息及显示隐藏情况,代码更改如下

// action/fetch.js
import {
  SET_FETCH_MESSAGE,
  HIDE_FETCH_MESSAGE
} from '../constants/ActionTypes';
export const startFetch = {
  type: SET_FETCH_MESSAGE,
  message: '开始发送异步请求'
};
export const successFetch = {
  type: SET_FETCH_MESSAGE,
  message: '成功接收数据'
};
export const failFetch = { type: SET_FETCH_MESSAGE, message: '接收数据失败' };
export const hideFetchMessage = { type: HIDE_FETCH_MESSAGE };
// action/math.js
import { ADD, SQUARE, SET } from '../constants/ActionTypes';
import { startFetch, successFetch, failFetch, hideFetchMessage } from './fetch';
import axios from 'axios';
const uri = 'https://jsonplaceholder.typicode.com/posts/2';
export const add = num => ({ type: ADD, num });
export const square = { type: SQUARE };
export const setNum = () => (dispatch, getState) => {
  dispatch(startFetch);
  setTimeout(() => {
    dispatch(hideFetchMessage);
  }, 500);
  return axios
    .get(uri)
    .then(res => {
      setTimeout(() => {
        dispatch(successFetch);
        setTimeout(() => {
          dispatch(hideFetchMessage);
        }, 500);
        dispatch({ type: SET, num: res.data.id });
      }, 1000);
    })
    .catch(err => {
      dispatch(failFetch);
      setTimeout(() => {
        dispatch(hideFetchMessage);
      }, 500);
    });
};
// constants/ActionTypes.js
export const ADD = 'ADD';
export const SQUARE = 'SQUARE';
export const SET = 'SET';
export const SET_FETCH_MESSAGE = 'SET_FETCH_MESSAGE';
export const HIDE_FETCH_MESSAGE = 'HIDE_FETCH_MESSAGE';
// reduce/fetch.js
import {
  SET_FETCH_MESSAGE,
  HIDE_FETCH_MESSAGE
} from '../constants/ActionTypes';
const initState = {
  message: '',
  isShow: false
};
const fetch = (state = initState, action) => {
  switch (action.type) {
    case SET_FETCH_MESSAGE:
      return { isShow: true, message: action.message };
    case HIDE_FETCH_MESSAGE:
      return { isShow: false, message: '' };
    default:
      return state;
  }
};
export default fetch;
// index.js
import store from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import { add, square, setNum } from './action/math';
const render = () =>
  ReactDOM.render(
    <div store={store}>
      <p>{store.getState().math}</p>
      <input
        type="button"
        onClick={() => store.dispatch(setNum())}
        value="设置Num"
      />
      <input type="button" onClick={() => store.dispatch(add(1))} value="+1" />
      <input type="button" onClick={() => store.dispatch(add(2))} value="+2" />
      <input
        type="button"
        onClick={() => store.dispatch(square)}
        value="乘方"
      />
      {store.getState().fetch.isShow && <p>{store.getState().fetch.message}</p>}
    </div>,
    document.getElementById('root')
  );
render();
store.subscribe(render);

六、React-Redux 基础知识点

前面的代码中,我们是通过 store.subscribe() 方法监控 state 状态的变化来更新 UI 层的。而使用 react-redux,可以让组件动态订阅状态树。状态树一旦被修改,组件能自动刷新显示最新数据。

react-redux 将所有组件分成两大类:展示组件和容器组件。展示组件只负责 UI 呈现,所有数据由参数 props 提供;容器组件则负责管理数据和业务逻辑,带有内部状态,可使用 redux 的 API 。要使用 react-redux,就要遵守它的组件拆分规范。

1. provider

react-redux 提供 Provider 组件,可以让容器组件默认可以拿到 state,而不用当容器组件层级很深时,一级级将 state 传下去。

将 index.js 文件更改如下:

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store';
import MathContainer from './container/MathContainer';
import { Provider } from 'react-redux';
ReactDOM.render(
  <Provider store={store}>
    <MathContainer />
  </Provider>,
  document.getElementById('root')
);

按照组件拆分规范,将原来 index.js 中相关代码,分拆到 container/MathContainer 和 component/Math 这两个组件中。

2. connect

react-redux 提供 connect 方法,用于从展示组件生成容器组件。connect 的意思就是将这两种组件(容器组件和展示组件)连接起来:

import { connect } from 'react-redux';
const MathContainer = connect()(Math);

Math 是展示组件,MathContainer 就是由 React-redux 通过 connect 方法自动生成的容器组件.

为了定义业务逻辑,需要给出下面两方面的信息:

  • 输入逻辑:外部的数据(即state对象)如何转换为展示组件的参数

  • 输出逻辑:用户发出的动作如何变为 Action 对象,从展示组件传出去

因此,connect 方法的完整 API 如下:

import { connect } from 'react-redux';
const MathContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Math);

上面代码中,connect 方法接受两个参数:mapStateToProps 和 mapDispatchToProps。它们定义了展示组件的业务逻辑。前者负责输入逻辑,即将 state 映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对展示组件的操作映射成 Action,下面分别介绍这两个参数。

3. mapStateToProps()

mapStateToProps 建立一个从外部的 state 对象到展示组件的 props 对象的映射关系。作为参数,mapStateToProps 执行后应该返回一个对象,里面的每一个键值对就是一个映射。

const mapStateToProps = state => {
  return {
    num: getNum(state)
  };
};

mapStateToProps 的第一个参数总是 state 对象,还可以使用第二个参数,代表容器组件的 props 对象。使用 ownProps 作为参数后,如果容器组件的参数发生变化,也会引发展示组件重新渲染。

const mapStateToProps = (state, ownProps) => {
  return {
    num: getNum(state)
  };
};

mapStateToProps 会订阅 Store ,每当 state 更新的时候,就会自动执行,重新计算展示组件的参数,从而触发展示组件的重新渲染。connect 方法可以省略 mapStateToProps 参数,那样,展示组件就不会订阅 Store,就是说 Store 的更新不会引起展示组件的更新。

4. mapDispatchToProps

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

如果 mapDispatchToProps 是一个函数,会得到 dispatch 和 ownProps (容器组件的 props 对象)两个参数。

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onSetNumClick: () => dispatch(setNum())
  };
};

mapDispatchToProps 作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了展示组件的参数怎样发出 action。

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

因此,上面的写法简写如下所示:

const mapDispatchToProps = {
  onsetNumClick: () => setNum()
};

所以,最终的目录结构就变成下面这个样子(你也可以把 store.js 放在单独的 store 文件夹里面):

目录结构


赏

转载请注明: Deepspace Redux 基础知识

相关文章 
1、React 高阶组件
2、React 基础知识
目录