春招面试复盘,拥抱redux数据流管理
一提到redux,你会想到什么
- 数据状态管理
- store action dispatch reducer (redux)
- Provider Connect (react-redux)
…
这样看的话,redux确实里面涉及很多API,但是仅仅知道这些API是不够的的,要理解redux如何对项目中的数据流进行管理,才是真正要去学习的。
因此这篇文章,我就当是自己把这个redux理通一遍,从why(这个以前写过就不再重复了) what how 三个角度,尽量去讲通一下。可能有些地方写的并不是很全面,也可能有一些地方说的有些错误,欢迎评论区指正。
文章大体结构:
但是在这,我不会按一步步讲下来,有关why我想大家应该都大致了解它为什么要出来,为了更方便的管理数据,之前也写过文章围绕组件化,带你入坑Raect学习 【详】,里面讲了传统组件传值,以及为什么需要redux来进行数据管理,但是那时候还没有搞懂redux里面的具体操作。
现在也是借这个机会,把有关redux操作的流程理一遍。其中很多知识点都在过程中写到了。
先声明👇👇👇
在初体验篇部分,我会单单使用 redux 来理解里面的一些概念, 以一个简单的todolist 来举例。
在完善篇部分,我会使用 redux + react-redux(更方便的使用redux) + axios + redux-thunk 实现进行异步请求数据。
好的,废话不多说,开干吧。
先导篇 —— 掌握一些redux知识点
一些关键要素和工作流:
Store:一个单一的数据源(里面存放了state),注意它是只读的
Action:对变化的描述
Reducer:一个函数,负责对变化(也就是action)进行分发和处理,最终把最新的数据返回给Store
三者紧密配合,形成了Redux工作流:
从图中我们可以看到,redux中的数据流式单向的,数据发生改变的唯一途径就是:在view视图层派发Action,Action会被我们的Reducer读取,然后Reducer根据Action内容的不同执行不同的计算(会存在很多action),最后得很话生成新的state,而这个state会更新到store对象中,又进一步驱动视图层做出对应的改变。
初体验篇 —— 透过TODOLIST小项目走一遍流程
通过一个todolist项目区更好的理解redux中这些api中的数据流动。
- 首先的话,我们使用npx create-react-app redux-todolist 创建一个新的项目,因为我们这边使用的是redux,因此我们安装好 yarn add redux
- 修改一个src下index.js的代码:
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList'
ReactDOM.render(
<TodoList />,
document.getElementById('root')
);
- 把原先的app.js删掉重新创建一个TodoList.js文件:
import React, { Component } from 'react';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = { }
}
render() {
return (
<div>
hello jingda
</div>
);
}
}
export default TodoList;
运行此刻的代码,我们可以成功在页面输出hello jingda
让我们修改一下代码,让页面有一个input框和一个提交按钮,当然我们可以选择初始化一些list数据,
代码如下:
import React, { Component } from 'react';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
list:[
'1',
'2',
'3'
]
}
}
render() {
return (
<div>
<div>
<input placeholder="请输入待办吧"></input>
<button>提交</button>
</div>
<div>
<ul>
{
this.state.list.map((item,index) => {
return <li>{index} --- {item}</li>
})
}
</ul>
</div>
</div>
);
}
}
export default TodoList;
我们需要完成的功能就是,在输入框中写下自己的待办,然后按提交按钮,这个待办就到list中去了,之后便显示在页面上。
首先我们要创建一个store:下面这样的目录结构,为什么store下面要index.js,和reducer.js两个文件呢,前面我们说到了,在当视图层派发一个action出来之后,使用由我们的reducer进行处理的,当它处理完后会把state传给store
因此这里面的代码如下:
store/index.js:在这个文件里面,我们引用了redux中的createStore创建一唯一的store,它接收了一个参数reducer,而这个reducer返回的就是处理完由视图层派发过来的action之后得到的state状态。
import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
export default store;
store/reducer.js:
const defaultState = {
inpuetValue:'请输入',
list:[
'jing1',
'jing2'
]
}//默认数据
export default (state = defaultState,action) => {
return state
}
到这里,我们创建好了项目的store,也初始化了数据。让我们回到TodoList组件中去引用store
import store from './store'
测试一下有没有引入:
在constructor方法中打印一下传过来的store里面的数据:
console.log(store.getState())
可以看到组件已经可以拿到store里面的数据了,下面我们要做的就是:
1、将这些数据显示在页面上。
2、把输入的数据先保存。
3、当input输入数据,点击提交,list多出一条记录的功能。
4、点击list每条数据的删除按钮,实现删除功能。
把store的数据展示在页面上只需要修改一下之前的代码:
constructor(props) {
super(props);
// console.log(store.getState())
this.state = store.getState()
}
、、、、、
<ul>
{
this.state.list.map((item,index) => {
return <li>
{index} --- {item}
<button>删除</button>
</li>
})
}
</ul>
同时我们要先保存一下输入的数据:
<input
placeholder={this.state.inpuetValue}
onChange={this.onChangeValue.bind(this)
</input>
后面三步都要经历这样的过程:创建action,并通过store.dispatch自动派发给reducer,reducer处理state数据,再把state传回给store
。这里我就一起写了。
准备工作就是在提交和删除按钮上分别绑定一个事件
<button onClick={() => addInput()}>提交</button>
<button onClick={(index) => this.deleteInput(index)}>删除</button>
创建一个actionType.js文件,action的type类型值先定义一下:
export const CHANGE_INPUT = "CHANGE_INPUT"
export const ADD_INPUT = "ADD_INPUT"
export const DELETE_INPUT = "DELETE_INPUT"
- 创建action,并派发
onChangeValue(e) {
console.log(e.target.value)
const action = {
type:CHANGE_INPUT,
value:e.target.value
}
store.dispatch(action)
}
addInput() {
const action = {
type:ADD_INPUT,
}
store.dispatch(action)
}
deleteInput(index) {
const action = {
type:DELETE_INPUT,
index
}
store.dispatch(action)
}
- reducer处理state
export default (state = defaultState,action) => {
if(action.type === CHANGE_INPUT) {
let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
newState.inpuetValue = action.value//对应组件传过来的index
//console.log(newState.value)
return newState
}
if(action.type === ADD_INPUT) {
let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
newState.list.push(action.inpuetValue) //对应我们组件传过来的value
return newState
}
if(action.type === DELETE_INPUT) {
let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
newState.list.splice(action.index,1) //对应组件传过来的index
return newState
}
return state
}
到这里我们似乎已经完成了前面说到的那些步骤,但是此时去页面上添加数据,是不能成功的,我们还需要完成一个操作-订阅Redux的状态
。
this.storeChange = this.storeChange.bind(this)
store.subscribe(this.storeChange)//订阅redux的状态
storeChange() {
this.setState(store.getState())
}
好啦,基本功能就实现啦,稍微整理一下代码。我们可以发现在组件页面上,每次有个事件我们就要创建一个action,每次修改我们要去组件里面翻,现在让我们把这些个action分离出来,单独写在一个文件里面,更加有助于管理。
在store/actionCreatores.js里面:
import { CHANGE_INPUT,ADD_INPUT,DELETE_INPUT } from './actionType'
export const changeInputAction = (value) => ({
type:CHANGE_INPUT,
value
})
export const addInputAction = () => ({
type:ADD_INPUT,
})
export const deleteInputAction = (index) => ({
type:DELETE_INPUT,
index
})
修改组件页面的方法代码:
onChangeValue(e) {
console.log(e.target.value)
const action = changeInputAction(e.target.value)
store.dispatch(action)
}
addInput() {
const action = addInputAction()
store.dispatch(action)
}
deleteInput(index) {
const action = deleteInputAction(index)
store.dispatch(action)
}
一个简单的todolist到这里就完成了。稍微理了一下redux中的数据流向。也把一些文件稍微整理了一下,但还没有完哦。虽然能够正确的实现功能,但项目中我们可能需要的更多,比如涉及下面的一些问题:
- 使用redux还要订阅之类的,有些复杂,react-redux又是如何去简化操作?
- 在reducer里面我使用的是深拷贝创建一个新的数据,但深拷贝本身是耗性能的,如何解决,immutable如何实现不可变数据?
- 上面我只有一个组件页面,因此只用到一个reducer,如果有多个组件呢?state如何管理,redux又是如何解决,我们又如何设计目录结构去更好管理?
- 当使用axios数据请求时?异步操作如何处理,redux-thunk如何使用呢?
…
还有一些其他的知识点,让我们开启下一个步吧:
完善篇 —— react-redux +redux-thunk 如何处理数据流
在上面的todolist小项目中,我想你已经大概了解了redux的使用基本流程,在这一步,我会把更多的篇幅写在结构化模块化,以及使用react-redux的一些新的知识上。
下面是一些要用到的API或知识点,稍微介绍一下,下面使用会更清楚一些:
涉及的知识点介绍
- react-redux
在使用react-redux的时候,组件分成了两大类,UI(负责UI呈现)和容器组件(负责管理数据和逻辑) - Connect
react-redux提供的一个方法,用于从UI组件中生成容器组件。
为什么需要从UI组件中生成?因为如果一个组件既有UI又有逻辑,那么我们会把它拆分成外层是容器组件负责逻辑,只要把数据传给它就好了,内层是UI组件接收外层传过来的数据,实现视图渲染。
connect方法有两个参数
mapStateToProps:
1、负责输入逻辑,将state映射到UI组件的参数(props上)
2、mapStateToProps会订阅Store,没带state更新,就会自动执行,重新计算UI组件的参数,从而触发UI组件的重新渲染。
mapDispatchToProps:
1、负责输出逻辑,将用户对UI组件的操作映射成Action
2、定义了那些操作应该当作Action,传给store
- Provider
<Provider store={store}>
<App />
<Provider/>
当我们用connect方法生成容器组件后,需要让组件拿到state对象,才能生成UI组件的参数。选择在根组件外面包一层,这样子组件就可以默认拿到state
- redux-thunk
实现异步操作的中间件,写一个返回函数的Action creator,然后使用redux-thunk中间件改造store.dispatch (具体的使用下面会提到) - immutable
持久化数据,一旦被创建就不会被更改,修改immutable对象的时候返回新的immutable,但是原数据不会更改。我们知道在reducer里面,数据是不能直接变更的,这里可以使用immutable
主流程
- 在我们的3001 端口下是前端项目 react,在3000端口下是一个接口(自己用express搭的,勿喷。完全是为了效果),思考一下后端如何从前端向后端发起请求,把数据显示到页面上?
- 数据请求过来了放哪?组件里面吗?还是收归Store 统一管理?
- axios 请求涉及异步,写这样的异步Action怎么写?
好吧,不多说啦,开始吧
(里面涉及的依赖请自行安装哦 axios redux react-redux redux-thunk immutable)
从项目结构开始:
api负责数据请求
pages负责组件管理,每个组件都有自己的store
store这是一个全局的store,掌管了全部组件的state,和reducer
假设我们在做一个商品信息shoplist的展示, 刚开始为空,然后我需要从后端把他的信息请求过来,并且把shopList的值修改掉
1、数据请求:
- redux-thunk使用:
import {createStore,applyMiddleware} from 'redux'
import thunk from 'redux-thunk' //异步处理
import reducer from './reducer'
const store = createStore(reducer,applyMiddleware(thunk))
export default store
- api/config.js
//axiox 请求的封装
import axios from 'axios';
//1、设置统一的地址范围
export const baseUrl = 'http://localhost:3000/shop';
//2、返回一个axios 实例
const axiosInstance = axios.create({
baseURL:baseUrl
});
export {
axiosInstance
}
api/request.js
import {axiosInstance} from "./config";
//所以的请求都在这里统一管理
//axios
//url 如果改了怎么办?
//首页
export const getShopListRequest = id => {
return axiosInstance.get('/shop')
}
2、触发action :
当涉及异步请求的时候,我们需要设计两个action,一个用来同步修改action的值,一个用来发请求,返回一个函数,这个函数也具备dispatch的能力。
export const shopList = data => ({
type:SHOP_LIST,
data
})
//api 两个action axios 只在action
export const getShopList = () => {
return dispatch => {
getShopListRequest()
.then(res => {
console.log(res)
dispatch(shopList(res));
})
}
}
3、组件 接收处理:
在App.js中使用Provider:
<Provider store={store}>
<Shop />
</Provider>
在Shop组件中使用connect:
const Shop = (props) => {
const { shoplist,shopListDispatch } = props
console.log(shoplist)
useEffect(() => {
shopListDispatch();
},[])
return(
<div></div>
)
}
const mapStateToProps = state => ({
shoplist: state.getIn(["shoplist"])
})
const mapDispatchToProps = dispatch => {
return{
shopListDispatch() {
dispatch(getShopList())
},
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Shop);
reducer的处理与整合
- reducer处理action:
const defaultStatus=fromJS({
shopList:[]
})
export default (state=defaultStatus,action)=>{
switch(action){
case actionType.SHOP_LIST:
return state.set('shopList',action.data);
default:
return state
}
}
- 多个reducer怎么整合:
import {combineReducers} from 'redux-immutable' //不可变的状态
import {reducer as userReducer} from '../pages/Shop/store'
// import {reducer as rankReducer} from '../pages/Food/store'
export default combineReducers({
shop:shopReducer,
// food:foodReducer
})
到这里,
1、最开始的shoplist数据为空
2、在actionCreators.js里面创建了两个action,一个是同步的,一个是异步的
3、在异步的这个action里面return了一个函数,这个函数有一个dispatch参数,并且执行了,发起异步请求的 getShopListRequest()方法,
4、然后我们通过请求http://localhost:3000/shop/shop,得到了下面的数据:
5、在reducer.js里面可以接收action,可以打印一下我们的action:
总结
最后总结一波:
在前面做todolist的项目的时候,我们说到几个点,现在来回答一下:
- react-redux 使用了Provider 和Connect 把组件分成容器组件和UI组件,其中Connect的第一个参数为我们实现了订阅功能,因此我们可以不用再自己写
订阅Redux的状态
. - 在写reducer的时候,我们不使用深拷贝,而是使用了immutable不可变数据,获取你应该去了解一些API,像,.fromJS,.setIn,.getIn…
- 为了更好的去管理若干个组件的数据,通过combineReducers 把组件的reducer整合起来。
- 异步请求,在actionCreators内部编写逻辑,处理请求结果。而不只是单纯的返回一个action对象。(当然在此之前,你得先去创建store的地方引用一波)
个人觉得,第一个todolist小项目是理解redux数据流向比较好的例子,也是比较容易理解。但是在项目中肯定不是就单单那么简单,react-redux中使用的Provider Connect 可能更加适合功能比较复杂的项目,但前提也是需要一些成本。不过现在很多都是用的这个。所以我们也要了解,并且熟悉它们的用法。
最重要的中间的目录结构,我觉得我没有讲的很清楚,也许你可以拿一个你做的项目看,里面涉及的redux管理可能是应该考虑到了。
好啦,redux有关的,我就梳理到这里了,可能自己理的还是有些复杂,因为全程大部分是以项目为主展开。
我是婧大,一个大三学崽,目前正在准备实习面试。
这段时间应该主要是理理一些知识点,欢迎一起学习。wx:lj18379991972 💕💕💕
你的点赞是给我最大的支持🤞
(后面那部分,我感觉自己好像理解的并不是很好,如果你是大佬,麻烦看到错的地方指点以下我这个白菜。如果你也不是很懂,希望通过这个文章,知道你自己可能还需要去了解的东西。可能是我实践的也少,对问题看待的深度和广度也是有欠缺。会继续学习的。。)