Redux
redux是什么?
redux是一个状态管理工具,将react组件中的状态提升到顶层,方便各组件件之间进行数据交互。
为什么需要redux
- 在react中数据是单向传递的,也就是我们常说的单项数据流,它只能使用props向下进行传递,使用state在组件内进行数据传递,无法向上传递。
- 管理不断变化的state是非常困难的,当应用程序较为复杂时,组件内的state因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪。
由此,产生了状态管理工具,给react提供可预测的状态管理。
Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
Redux核心概念
在redux中主要包含Store、Action、Reducer三个部分。
Store
- 作用:存储数据
- redux中只有一个store
- 改变store中数据的唯一方法是松果store.dispatch(action.type)
const store = redux.createStore(reducer) //reducer为自定义的纯函数
Action
- action是一个普通的JavaScript对象,用来描述更新的type和传送的数据;
- 组件通过派发(dispatch)行为(action)给store,而不是直接通知其它组件;
- 使用action的好处:可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是追溯、可预测的
目前我们的action是固定的对象,真实应用中,我们会通过函数来定义,返回一个action;
//way1
const action1 = {type: 'ADD', info:{name:'123',age: '10'}}
//way2
const changeOriginRankingAction = (res) => ({
type: actionTypes.CHANGE_ORIGIN_RANKING,
originRanking: res.playlist
})
reducer
- reducer是一个纯函数,可以使用单独的一个reducer,也可以将多个reducer合并为一个reducer
- 作用:将传入的state和action结合起来生成一个新的state
function reducer(state = initialState, action) {
switch (action.type) {
case "ADD_ FRIEND":
return { ...state, friends: [...state.friends, action.info] }
case "INC_ AGE":
return {
state, friends: state.friends.map((item, index) => {
if (index === action.index) {
return { ...item, age: item.age + 1 }
}
return item;
})
}
case "CHANGE_ NAME":
return {
...state, friends: state.friends.map((item, index) => {
if (index == action.index) {
return { ...item, name: action.newName }
}
return item;
})
}
default:
return state;
}
}
Redux执行过程
- redux简易流程:
在html页面中,用户发出动作触发action,reducer接收到触发action的类型与数据,对其进行处理,并将处理后的新State传送给Store更新,store将更新后的数据传入react组件中
- redux详细流程:
注:在使用redux的过程中,我们需要对store进行订阅,这样数据才能及时更新
- 订阅应该在派发(dispatch)前
- 订阅state发生的改变(通常在useEffect、componentDidmount)
- 订阅后需要在componentWillUnmount或useEffect中进行取消
unscribe = store.subscribe(()=>{
console.log('count',store.getState().count)
//使用setState或useState进行改变试图上的数据
})
//需要在componentWillUnmount或useEffect的回调函数中取消订阅
unscribe()
redux三大原则
单一数据源
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
- Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改
state是只读的
- 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题
使用纯函数进行修改
- 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
redux的结构划分
如果将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护,因此,有必要对redux的结构进行划分
结构划分
- 对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。
- 创建store文件夹,存放redux相关代码
-
创建store/index.js文件,创建store/reducer.js文件,创建store/actionCreators.js文件,创建store/constants.js文件
- 在actionCreators.js中写action相关内容
- 在constants.js中用变量定义action相关的type
- 在reducer.js中引入constants.js,并写reducer相关代码,再将reducer导出
- 在index.js中引入redux 、reducer.js,并创建redux.createStore(reducer),并导出store
react-redux
react-redux是什么?
react-redux主要包含两个部分,Provider与Connect
Provider: provider相当于一个顶级组件,通常我们将APP组件包裹在Provider组件中,这样的话,所有组件就都可以在react-redux的控制之下了,但是store必须作为参数放到Provider组件中去
<Provider store = {store}>
<App />
<Provider>
Provider组件能够让它包裹的组件能够访问到store中的数据
Connect:connect是一个高阶组件,主要形式如下:
connect(mapStateToProps, mapDispatchToProps)(MyComponent)
connect通过mapStateToProps与mapDispatchToProps将store中的数据与方法传入到参数组件的props中
const mapStateToProps = (state) => {
return {
// prop : state.xxx | 意思是将state中的某个数据映射到props中
foo: state.bar
}
}
const mapDispatchToProps = (dispatch) => { // 默认传递参数就是dispatch
return {
onClick: () => {
dispatch({
type: 'increatment'
});
}
};
}
//渲染的时候就可以使用this.props.foo 与this.props.onClick
class Foo extends Component {
constructor(props){
super(props);
}
render(){
return(
// 这样子渲染的其实就是state.bar的数据了
<div>this.props.foo</div>
<button onClick = {this.props.onClick}>点击increase</button>
)
}
}
Foo = connect()(Foo);
export default Foo;
为什么需要react-redux
-
react-redux可以借助connect高阶函数防止一些冗余的、重复的代码,例如:
- 避免在每个组件中重复书写订阅更新、
- 在订阅回调中: 1、获取当前 store 状态, 2、提取此 UI 所需的数据,3、用数据更新 UI
redux和react没有直接的关系,可以在React, Angular, Ember, jQuery, or vanilla JavaScript中 使用Redux。
- connect简易源码如下:
redux中的中间件
- 由于reducer是一个纯函数,不能在其中发起网络请求等具有副作用的操作,因此,在进行这类操作时我们需要借助中间件。
- 中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码,比如日志记录、调用异步接口、添加代码调试功能等等。
- 常见的redux中间件有redux-thunk、redux-promise、redux-saga
redux-thunk
reducer为纯函数不能发起网络请求,redux-thunk中间件通过在action中实现异步操作来发起网络请求,主要原理是在action 中发起异步请求,等待请求的数据返回来后,在触发dispatch(action函数),将返回得分数据作为action函数的参数传给action
-
该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数
- dispatch函数用于我们之后再次派发action
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态
redux-thunk的应用
- 作用:完成redux中的异步操作
- yarn add redux-thunk
- 在创建store时传入应用了middleware的enhance函数
- 将enhancer作为第二个参数传入到createStore中;
const enhancer = applyMiddleware(thunkMiddleWare)
const store = createStore(reducer,enhancer)
- 定义返回一个函数的action,此时的action不是返回对象而是函数
- 该函数在dispatch后会被执行
const getHomeMultidataAction = () => {
return (dispatch) => {
axios.get('url').then(res => {
dispatch(changeDataAction(data.list))
})
}
}
redux-promise
redux-promise相对redux-thunk更简单,它直接把异步操作作为action的payload,当异步任务有了结果后自动再触发该action,进入到reducer更新state。
redux-promise的使用
- 安装npm i redux-promise
- 配置redux-promise中间件
import promiseMiddleware from "redux-promise";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
redicers,
composeEnhancers(applyMiddleware(promiseMiddleware))
);
- redux-promise的使用
import { getTodoByIdType } from "../types";
// 请求API
const getTodoById = async (payload) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${payload}`
);
const response = await res.json();
return response;
};
// 直接将异步请求设置成payload
// 当异步有了结果会自动触发该action,然后进入到reducer更新state
export const getTodoByIdPromiseAction = (payload) => {
return {
type: getTodoByIdType,
payload: getTodoById(payload),
};
};
在React组件dispatch(getTodoByIdPromiseAction(1))后会进入到getTodoByIdPromiseAction方法进行异步请求,当请求返回结果后把异步结果作为payload再触发同步action即getTodoByIdPromiseAction,进入到相应reducer更新state。
redux-saga
Redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活,主要原理是将redux中的异步请求抽取出来建立一个saga文件
redux-saga的应用
- 安装 npm i redux-saga
redux-saga单独把异步任务抽取出来了,所以需要有单独的saga文件来处理异步任务。我们创建一个sagas文件夹,里面存放saga文件。- 建立saga.js文件
import { call, takeEvery, takeLatest, put, select } from "redux-saga/effects";
import { getTodoByIdType, fetchTodoByIdType } from "../types";
// 异步接口
const getTodoById = async (payload) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${payload}`
);
const response = await res.json();
return response;
};
// dispatch 的 action 会被作为参数自动接收
function* fetchTodo(action) {
try {
// 调用接口方法
const todo = yield call(getTodoById, action.payload);
console.log("todo", todo);
// 获取state
let item1 = yield select((state) => state.todo.item);
console.log(item1);
// 提交 action
yield put({ type: getTodoByIdType, payload: todo });
// 获取state
let item2 = yield select((state) => state.todo.item);
console.log(item2);
} catch (e) {
// 请求错误置空处理
yield put({ type: getTodoByIdType, payload: {} });
}
}
// 监听 我们 fetchTodoByIdType的action,当监听后会自动执行fetchTodo方法
function* listenGetTodo() {
yield takeEvery(fetchTodoByIdType, fetchTodo); // 始终监听
// yield takeLatest(fetchTodoByIdType, fetchuserr); // 监听最新的一次
}
// 使用数组导出,当有多个方法需要监听的时候放在数组即可
const saga1 = [listenGetTodo()];
export default saga1;
- rootSaga
用来组合所有的saga文件,类似combineReducers
// sagas/index.js
import { all } from "redux-saga/effects";
import saga1 from "./saga1";
import saga2 from "./saga2";
function* rootSaga() {
// 多个saga
yield all([...saga1, ...saga2]);
}
export default rootSaga;
- 以中间件形式配置使用,并启动
saga。
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas/index";
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
redicers,
composeEnhancers(applyMiddleware(sagaMiddleware))
);
// 启动saga,相当于监听
sagaMiddleware.run(rootSaga);
参考文章
还参考了两个文章,忘记记下链接了(因为有部分内容是很久之前的md笔记,当初没想过要发到网上来,就没有记录链接,后续找到会放上来)