五、Redux进阶:UI组件、容器组件、无状态组件、异步请求、Redux中间件:Redux-thunk、redux-saga,React-redux

一、UI组件和容器组件

  1. UI组件负责页面的渲染(傻瓜组件)
  2. 容器组件负责页面的逻辑(聪明组件)

当一个组件内容比较多,同时有逻辑处理和UI数据渲染时,维护起来比较困难。这个时候可以拆分成"UI组件"和"容器组件"。 拆分的时候,容器组件把数据和方法传值给子组件,子组件用props接收。

需要注意的是: 子组件调用父组件方法函数时,并传递参数时,可以把方法放在箭头函数中(直接在函数体使用该参数,不需要传入箭头函数)。

拆分实例

未拆分前原组件

jsx 复制代码
import React, {Component} from 'react';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
import { Input, Button, List } from 'antd';
// 引用store
import store from './store';
import { inputChangeAction, addItemAction, deleteItemAction } from './store/actionCreators';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }
  
  render() {
    return (
      <div style={{margin: '10px'}}>
        <div className="input">
          <Input
            style={{width: '300px', marginRight: '10px'}}
            value={this.state.inputValue}
            onChange={this.handleInputChange}
          />
          <Button type="primary" onClick={this.handleClick}>提交</Button>
        </div>
        <List
          style={{marginTop: '10px', width: '300px'}}
          bordered
          dataSource={this.state.list}
          renderItem={(item, index) => (<List.Item onClick={this.handleDelete.bind(this, index)}>{item}</List.Item>)}
        />
      </div>
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }

  // 输入内容时(input框内容改变时)
  handleInputChange(e) {
    const action = inputChangeAction(e.target.value);
    store.dispatch(action);
  }

  // 添加一项
  handleClick () {
    const action = addItemAction();
    store.dispatch(action);
  }
  
  // 点击删除当前项
  handleDelete (index) {
    const action = deleteItemAction(index);
    store.dispatch(action);
  }
}

export default TodoList;

拆分后-容器组件

jsx 复制代码
import React, {Component} from 'react';

// 引用store
import store from './store';
import { inputChangeAction, addItemAction, deleteItemAction } from './store/actionCreators';
import TodoListUI from './TodoListUI';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }
  
  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}
        handleInputChange={this.handleInputChange}
        handleClick={this.handleClick}
        handleDelete={this.handleDelete}
      />
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }

  // 输入内容时(input框内容改变时)
  handleInputChange(e) {
    const action = inputChangeAction(e.target.value);
    store.dispatch(action);
  }

  // 添加一项
  handleClick () {
    const action = addItemAction();
    store.dispatch(action);
  }

  // 点击删除当前项
  handleDelete (index) {
    const action = deleteItemAction(index);
    store.dispatch(action);
  }
}

export default TodoList;

拆分后-UI组件

jsx 复制代码
import React, { Component } from 'react';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
import { Input, Button, List } from 'antd';

class TodoListUI extends Component {
  render() {
    return (
      <div style={{margin: '10px'}}>
        <div className="input">
          <Input
            style={{width: '300px', marginRight: '10px'}}
            value={this.props.inputValue}
            onChange={this.props.handleInputChange}
          />
          <Button type="primary" onClick={this.props.handleClick}>提交</Button>
        </div>
        <List
          style={{marginTop: '10px', width: '300px'}}
          bordered
          dataSource={this.props.list}
          // renderItem={(item, index) => (<List.Item onClick={(index) => {this.props.handleDelete(index)}}>{item}-{index} </List.Item>)}
          renderItem={(item, index) => (<List.Item onClick={() => {this.props.handleDelete(index)}}>{item}-{index} </List.Item>)}
        />
        {/* 子组件调用父组件方法函数时,并传递参数时,可以把方法放在箭头函数中(直接在函数体使用该参数,不需要传入箭头函数)。 */}
      </div>
    )
  }
}

export default TodoListUI;

二、无状态组件

当一个组件只有render函数时,可以用无状态组件代替。

  1. 无状态组件比普通组件性能高; 因为无状态组件只是函数,普通组件是class声明的类要执行很多生命周期函数和render函数。
  2. 无状态组件中的函数接收一个参数作为父级传过来的props。

例如下面这个例子 普通组件:

jsx 复制代码
class TodoList extends Component {
  render() {
    return <div> {this.props.item} </div>
  }
}

无状态组件:

jsx 复制代码
const TodoList = (props) => {
  return(
    <div> {props.item} </div>
  )}

三、Redux 中发送异步请求获取数据

1、引入axios,使用axios发送数据请求

jsx 复制代码
import axios from 'axios';

2、在componentDidMount中调用接口

jsx 复制代码
componentDidMount() {
  axios.get('/list.json').then(res => {
    const data = res.data;
    // 在actionCreators.js中定义好initListAction,并在reducer.js中作处理(此处省略这部分)
    const action = initListAction(data);
    store.dispatch(action);
  })
}

四、使用Redux-thunk 中间件实现ajax数据请求

1、安装和配置Redux-thunk

1.1、安装Redux-thunk

javascript 复制代码
npm install redux-thunk --save

1.2、正常使用redux-thunk中间件在store中的写法

javascript 复制代码
// 引用applyMiddleware
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

// 创建store时,第二个参数传入中间件
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

export default store;

redux-thunk使用说明

1.3、redux-thunk中间件 和 redux-devtools-extension 一起使用的写法

javascript 复制代码
// 引入compose
import { createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
  applyMiddleware(thunk),
);

const store = createStore(reducer, enhancer);

export default store;

Redux DevTools插件配置说明

2、redux-thunk 的作用和优点

  1. 不使用redux-thunk中间件,store接收的action只能是对象;有了redux-thunk中间件,action也可以是一个函数。这样子就可以在action中做异步操作等。
  2. store接收到action之后发现action是函数而不是对象,则会执行调用这个action函数。
  3. 可以把复杂的异步数据处理从组件的生命周期里摘除出来(放到action中),避免组件过于庞大,方便后期维护、自动化测试。

3、使用redux-thunk的流程

  1. 在创建store时,使用redux-thunk。详见以上配置说明。

  2. 在actionCreators.js中创建返回一个方法的action,并导出。在这个方法中执行http请求。

javascript 复制代码
import types from './actionTypes';
import axios from 'axios';

export const initItemAction = (value) => ({
  type: types.INIT_TODO_ITEM,
  value: value
})

// 当使用redux-thunk后,action不仅可以是对象,还可以是函数
// 返回的如果是方法会自动执行
// 返回的方法可以接收到dispatch方法,去派发其它action
export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/initList').then(res => {
      const action = initItemAction(res.data);
      dispatch(action);
    })
  }
}

export const inputChangeAction = (value) => ({
  type: types.CHANGE_INPUT_VALUE,
  value: value
})

export const addItemAction = (value) => ({
  type: types.ADD_TODO_ITEM
})

export const deleteItemAction = (index) => ({
  type: types.DELETE_TODO_ITEM,
  value: index
})
  1. 在组件中引用这个action,并在componentDidMount中派发该action给store
jsx 复制代码
import React, {Component} from 'react';

import store from './store';
import { getTodoList } from './store/actionCreators';

class TodoList extends Component {

  ...

  // 初始化数据(使用redux-thunk派发/执行一个action函数)
  componentDidMount() {
    const action = getTodoList();
    store.dispatch(action);
  }

  ...
}

export default TodoList;

4、具体执行流程

  1. 组件加载完成后,把处理异步请求的action函数派发给store;
  2. 因使用了redux-thunk中间件,所以可以接收一个action函数(正常只能接收action对象)并执行该方法;
  3. 在这个方法中执行http异步请求,拿到结果后再次派发一个正常的action对象给store;
  4. store发现是action对象,则根据拿来的值修改store中的状态。

五、什么是Redux的中间件

  1. 中间件指的是action 和 store 中间。
  2. 中间件实现是对store的dispatch方法的升级。

几个常见中间件的作用(对dispatch方法的升级)

  1. redux-thunk:使store不但可以接收action对象,还可以接收action函数。当action是函数时,直接执行该函数。
  2. redux-log:每次dispatch时,在控制台输出内容。
  3. redux-saga:也是处理异步逻辑,把异步逻辑单独放在一个文件中管理。

六、redux-saga中间件入门

1、安装和配置redux-saga

1.1、安装redux-saga

javascript 复制代码
npm install --save redux-saga

javascript 复制代码
yarn add redux-saga

1.2、正常使用redux-saga中间件在store中的写法

javascript 复制代码
import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducer';
import mySaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mySaga);

export default store;

redux-saga使用说明

1.3、redux-saga中间件 和 redux-devtools-extension 一起使用的写法

javascript 复制代码
import { createStore, applyMiddleware, compose} from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducer';
import mySaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
  applyMiddleware(sagaMiddleware),
);
const store = createStore(reducer, enhancer);
sagaMiddleware.run(mySaga);

export default store;

Redux DevTools插件配置说明

2、redux-saga 的作用和与redux-thunk的比较

  1. redux-saga也是解决异步请求的。但是redux-thunk的异步处理还是在aciton中,而redux-saga的异步处理是在一个单独的文件(sagas.js)中处理。
  2. redux-saga同样是作异步代码拆分的中间件,可以使用redux-saga完全代替redux-thunk。(redux-saga使用起来更复杂,更适合大型项目)
  3. redux-thunk只是把异步请求放到action中,并没有多余的API。而redux-saga是单独放在一个文件中处理,并且有很多PAI。
  4. 使用流程上的区别; 4.1. 使用redux-thunk时,从组件中派发action(action函数)时,监测到是函数,会在action中接收并处理,然后拿到结果后再派发一个普通action交给store的reducer处理,更新store的状态。 4.2. 使用redux-saga时,从组件中派发action(普通action对象)时,会先交给sagas.js匹配处理异步请求。拿到结果后再使用put方法派发一个普通action交给store的reducer处理,更新store的状态。

3、使用redux-saga的流程

  1. 在创建store时,使用redux-saga。详见以上配置说明。

  2. 在actionCreators.js中创建一个普通的action,并导出。

javascript 复制代码
import types from './actionTypes';
// import axios from 'axios';

export const initItemAction = (value) => ({
  type: types.INIT_TODO_ITEM,
  value: value
})

// redux-thunk的写法,异步请求依然在这个文件中
// export const getTodoList = () => {
//   return (dispatch) => {
//     axios.get('/initList').then(res => {
//       const action = initItemAction(res.data);
//       dispatch(action);
//     })
//   }
// }

// redux-saga的写法,这里返回一个普通action对象;
// sagas.js中会用takeEvery监听这个type类型,然后执行对应的异步请求
export const getTodoList = () => ({
  type: types.GET_INIT_ACTION,
})

export const inputChangeAction = (value) => ({
  type: types.CHANGE_INPUT_VALUE,
  value: value
})

export const addItemAction = (value) => ({
  type: types.ADD_TODO_ITEM
})

export const deleteItemAction = (index) => ({
  type: types.DELETE_TODO_ITEM,
  value: index
})
  1. 在store文件夹中,创建一个文件sagas.js,使用redux-saga的takeEvery方法监听刚才派发的type类型,然后执行对应的函数,执行异步请求代码。拿到结果后再使用redux-saga的put方法派发一个普通的action对象,交给store的reducer处理。
javascript 复制代码
import { takeEvery, put } from 'redux-saga/effects';
import types from './actionTypes';
import axios from 'axios';
import { initItemAction } from './actionCreators';

function* getInitList() {
  try {
    const res = yield axios.get('/initList');
    const action = initItemAction(res.data);
    yield put(action);
  } catch(e) {
    console.log('接口请求失败');
  }
}

// generator 函数
function* mySaga() {
  yield takeEvery(types.GET_INIT_ACTION, getInitList);
}

export default mySaga;
  1. 在组件中引用这个action,并在componentDidMount中派发该action给store
jsx 复制代码
import React, {Component} from 'react';

import store from './store';
import { getTodoList } from './store/actionCreators';

class TodoList extends Component {

  ...

  // 初始化数据(使用redux-saga派发一个普通action对象,经由sagas.js的generator 函数匹配处理后,再交由store的reducer处理)
  componentDidMount() {
    const action = getTodoList();
    store.dispatch(action);
  }

  ...
}

export default TodoList;

4、具体执行流程

  1. 组件加载完成后,把一个普通的action对象派发给store;
  2. 因使用了redux-saga中间件,所以会被sagas.js中的generator函数匹配到,并交给对应的函数(一般也是generator函数)处理;
  3. sagas.js的函数拿到结果后,使用redux-saga的put方法再次派发一个普通action对象给store;
  4. sagas.js中没有匹配到对应的类型,则store交由reducer处理并更新store的状态。

七、如何使用React-redux完成TodoList功能

安装React-redux

jsx 复制代码
npm install react-redux --save

1、把redux写法改成React-redux写法

1.1、 入口文件(src/index.js)的修改

  • 使用react-redux的Provider组件(提供器)包裹所有组件,把 store 作为 props 传递到每一个被 connect() 包装的组件。
  • 使组件层级中的 connect() 方法都能够获得 Redux store,这样子内部所有组件就都有能力获取store的内容(通过connect链接store)。

原代码

jsx 复制代码
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './todoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));

修改后代码 ```jsx import React from 'react'; import ReactDOM from 'react-dom'; import TodoList from './TodoList'; import { Provider } from 'react-redux'; import store from './store';

// Provider向内部所有组件提供store,内部组件都可以获得store const App = ( )

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

perl 复制代码
<br/>
#### 1.2、组件(TodoList.js)代码的修改

Provider的子组件通过react-redux中的connect连接store,写法:
```jsx
connect(mapStateToProps, mapDispatchToProps)(Component)
  • mapStateToProps:store中的数据映射到组件的props中;
  • mapDispatchToProps:把store.dispatch方法挂载到props上;
  • Component:Provider中的子组件本身;

导出的不是单纯的组件,而是导出由connect处理后的组件(connect处理前是一个UI组件,connect处理后是一个容器组件)。

原代码

jsx 复制代码
import React, { Component } from 'react';
import store from './store';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleChange = this.handleChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }

  render() {
    return(
      <div>
        <div>
          <input value={this.state.inputValue} onChange={this.handleChange} />
          <button onClick={this.handleClick}>提交</button>
        </div>
        <ul>
          {
            this.state.list.map((item, index) => {
              return <li onClick={() => {this.handleDelete(index)}} key={index}>{item}</li>
            })
          }
        </ul>
      </div>
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }
  
  handleChange(e) {
    const action = {
      type: 'change-input-value',
      value: e.target.value
    }
    store.dispatch(action);
  }

  handleClick() {
    const action = {
      type: 'add-item'
    }
    store.dispatch(action)
  }

  handleDelete(index) {
    const action = {
      type: 'delete-item',
      value: index
    }
    store.dispatch(action);
  }
}

export default TodoList;

修改后代码 省去了订阅store使用store.getState()更新状态的操作。组件会自动更新数据。

jsx 复制代码
import React, { Component } from 'react';
import { connect } from 'react-redux';

class TodoList extends Component {
  render() {
    // const { inputValue, handleChange, handleClick, list, handleDelete} = this.props;

    return(
      <div>
        <div>
          <input value={this.props.inputValue} onChange={this.props.handleChange} />
          <button onClick={this.props.handleClick}>提交</button>
        </div>
        <ul>
          {
            this.props.list.map((item, index) => {
              return <li onClick={() => {this.props.handleDelete(index)}} key={index}>{item}</li>
            })
          }
        </ul>
      </div>
    )
  }
}

// 把store的数据 映射到 组件的props中
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}

// 把store的dispatch 映射到 组件的props中
const mapDispatchToProps = (dispatch) => {
  return {
    handleChange(e) {
      const action = {
        type: 'change-input-value',
        value: e.target.value
      }
      dispatch(action);
    },
    handleClick() {
      const action = {
        type: 'add-item'
      }
      dispatch(action)
    },
    handleDelete(index) {
      const action = {
        type: 'delete-item',
        value: index
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

1.3、store/index.js 代码不需要修改 ```jsx import { createStore } from 'redux'; import reducer from './reducer'

const store = createStore(reducer);

export default store;

ini 复制代码
<br/>
#### 1.4、store/reducer.js 代码也不需要修改
```jsx
const defaultState = {
  inputValue: '',
  list: []
}
export default (state = defaultState, action) => {
  const { type, value } = action;
  let newState = JSON.parse(JSON.stringify(state));

  switch(type) {
    case 'change-input-value':
      newState.inputValue = value;
      break;
    case 'add-item':
      newState.list.push(newState.inputValue);
      newState.inputValue = '';
      break;
    case 'delete-item':
      newState.list.splice(value, 1);
      break;
    default:
      return state;
  }

  return newState;
}

2、代码精简及性能优化

  • 因现在组件(TodoList.js)中代码只是用来渲染,是UI组件。并且没有状态(state),是个无状态组件。所以可以改成无状态组件,提高性能。
  • 但connect函数返回的是一个容器组件。
jsx 复制代码
import React from 'react';
import { connect } from 'react-redux';

const TodoList = (props) => {
  const { inputValue, handleChange, handleClick, list, handleDelete} = props;

  return(
    <div>
      <div>
        <input value={inputValue} onChange={handleChange} />
        <button onClick={handleClick}>提交</button>
      </div>
      <ul>
        {
          list.map((item, index) => {
            return <li onClick={() => {handleDelete(index)}} key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
}


// 把store的数据 映射到 组件的props中
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}

// 把store的dispatch 映射到 组件的props中
const mapDispatchToProps = (dispatch) => {
  return {
    handleChange(e) {
      const action = {
        type: 'change-input-value',
        value: e.target.value
      }
      dispatch(action);
    },
    handleClick() {
      const action = {
        type: 'add-item'
      }
      dispatch(action)
    },
    handleDelete(index) {
      const action = {
        type: 'delete-item',
        value: index
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
相关推荐
@大迁世界3 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路12 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug15 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213817 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中39 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路42 分钟前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全
前端工作日常1 小时前
我学习到的AG-UI的概念
前端