Flux概念,将一个应用分成四个部分
1.View,视图层
2.Action,视图层发出的消息,动作
3.Dispatcher,用来接收Actions、执行回调函数
4.Store,用来存放应用的状态,一旦发生变动,提醒views更新页面
Flux的特点就是,单向数据流
1.用户访问View
2. View发出用户的Action
3. Dispatcher收到Action,要求Store进行相应的更新
4. Store 更新后,发出一个"change"事件
5. View收到"change’事件后,更新页面
Redux,
- js应用的状态容器,不仅仅用于react,vue也能用,只是现在多用于react,因为React 只是 DOM 的一个抽象层,并不是 Web 应用的完整解决方案。没有官方标配的状态管理器
- 浏览器安装Redux Devtools插件,让你在浏览器中能看到redux仓库的数据状态
- 使用这个插件要在createStore中配置window.REDUX_DEVTOOLS_EXTENSION && window.REDUX_DEVTOOLS_EXTENSION()
或者
安装@redux-devtools/extension
在createStore中,最新的redux推荐使用@reduxjs/toolkit称作RTK
基本结构:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/extension';
function reducer(state,action){
switch(action.type){
default:
return state
}
}
// 无需中间件时
const store = createStore(
reducer,
composeWithDevTools()
);
// 需要中间件时这样写
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(...middleware)
)
);
安装Redux:npm install redux
- 创建一个reducer纯函数,用于createStore,这个函数与useReducer hook接收的reducer函数一样,会接收到上一次的state与一个action动作对象
1.可以给state参数设置默认值,来描述仓库state数据的初始值
2.action是初始化动作,由Redux生成,里面的type字段的值是一个随机数,通过dispatch来派发一个action对象来规定type值
function reducer(state,action){
console.log(state,action)
//通过不同的type返回对state做不同的处理并返回
//注意不能直接对state做修改返回,而是生出一个全新的state来返回
switch(action.type){
default:
return state
}
}
1.该函数在创建仓库时,默认会触发一次
2.该函数在后续调用仓库的dispatch方法时,会再次触发
3.该函数需要根据state与action生成一份全新的state数据,并返回
4.该函数返回的数据,就是state。也就是仓库的getState()返回的值
const store = createStore(reducer)
- store是一个仓库对象,他有以下常用方法
- getState() 返回当前仓库中的state数据
- dispatch(action) 派发一个动作,用于修改仓库中的state
- subscribe(callback) 用于订阅仓库的数据变化,当仓库数据发生变化时会触发calback
react-redux
- react-redux 是用于更好的在 react 中链接 redux 使用的。主要使用手法有两种:
Provider + 高阶组件的方式
Provider + hooks的方式 - Provider 是 react-redux 提供个一个组件。将该组件用于项目根,用该组件将根组件包裹,且将 store 仓库对象作为数据传递给 Provider
- Provider+高阶组件的方式:
- 语法:const NewComponent = connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
1.mapStateToProps 映射仓库state到WrappedComponent的Props上的一个函数
2.mapDispatchToProps 映射仓库修改state的操作到WrappedComponent的Props上的一个函数
3.WrappedComponent 需要被包装的组件
4.NewComponent 返回的新组件 - mapStateToProps()接收两个参数,
1.state仓库的state数据,
2.props当前组件收到的props数据
这个函数return的数据会合并到组件的props中去
该函数默认会触发一次,当仓库数据发生变化,这个函数会再次触发,就无需subscribe来监听数据变化 - mapDispatchToProps()
必须返回一个对象,里面一般存放来修改数据的函数,这个对象也会合并到组件的props中去
- 语法:const NewComponent = connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
- 上面的方式看起来非常的复杂,所以不做详细描述了,用下面这个方式更方便
- Provider+hooks的方式:
用Provider包裹你的根组件,在main.jsx文件中,引入创建的仓库store
import React from "react";
import ReactDOM from "react-dom/client";
// 引入 react-redux 的 Provider
import { Provider } from "react-redux";
// 引入 store
import store from "./store";
import App from "./App";
// 创建容器
const root = ReactDOM.createRoot(document.querySelector("#root") as Element);
root.render(
//将App组件包裹,store就是绑定你创建的的仓库对象
<Provider store={store}>
<App />
</Provider>
);
在子组件中,引入下面两个方法:import { useSelector, useDispatch } from ‘react-redux’
- useSelector(),用于勾出仓库的某数据
- const age = useSelector(state=>state.age) 这里假设勾出state数据中有一个age属性
- useDispatch(),用于派发一个action动作对数据做操作
- const dispatch = useDispatch()
- 在事件中,dispatch({type:‘add’})派发一个add动作,随即就会在renducer中执行对应的动作
reducer的拆分
- 当一个项目越做越大,状态越来越多,renducer中的case越来越多在同一个文件中,不利于维护,所以需要对其进行拆分
- 如何拆:
store下创建一个reducers文件夹来存放拆分的case项
//这里的initState只属于这个arrReducer小模块,并不是仓库最终的state
const initState = [{uname:'张三',age:18}]
//导出这个arrReducer函数
export default function arrReducer(state = initState,acation){
switch(action.type){
case "arr/push":
return [...state,action.payload]
case "arr/unshift":
return [action.payload,...state]
default:
return state
}
}
- 如何合并:
- 使用redux的combineReducers方法
- 语法:const rootReducer = combineReducers({key1:reducer1,key2:reducer2})
key:自定义的键名,最终的state的key
reducer:拆分出去的后,引入进来的名字
//导入
import { combineReducers ,createStore} from 'redux'
import { composeWithDevTools } from '@redux-devtools/extension';
//引入拆分出去的reducer文件
import arrReducer from './arrReducer'
//调用combineReducers做合并,生成合并后的rootReducer
const rootReducer = combineReducers({
arr:arrReducer
})
const store = createStore(rootReducer,composeWithDevTools())
export default store
- 在之后组件中,通过前面的Provider+hook的方式
useSelector(state=>state.arr)通过state.之前合并时的key就能拿到拆分出去的对应的数据
useDispatch()({type:‘arr/push’})这里的dispatch照常使用就行,注意type的值要对应,格式大小写不能错
中间件
- 用于对action动作执行之前做一些处理
- 在store下新建middlewares文件夹,用于放中间件函数文件
- 例如:创建一个a.ts文件
在a.ts文件中定义中间件函数:
export default (storeAPI) => (next) => (action)=> {
next(action)
}
在创建store的createStore中
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/extension';
//引入中间件函数
import aMidd from './middlewares/a'
import bMidd from './middlewares/b'
// 需要中间件时
const store = createStore(
reducer,
composeWithDevTools(
//在上一个aMidd的中间件函数中next()了,才会继续走下面的bMidd中间件
applyMiddleware(aMidd,bMidd)
)
);
redux-logger中间件
通过npm安装:npm i redux-logger
引入之后直接放到applyMiddleware中使用:import logger from ‘redux-logger’
让你打印仓库日志消息
这个中间件必须放在中间件流程applyMiddleware的最后一个
redux-thunk中间件
通过npm安装:npm i redux-thunk
引入之后直接放到applyMiddleware中使用:import thunk from ‘redux-thunk’
让你的dispatch可以传函数来执行异步操作
immer库中的produce使用
下载依赖:npm i immer
//引入immer的produce
import { produce } from 'immer'
//定义一个原数据
const oldState = [{uname:'张三',age:18},{uname:'李四',age:20}]
//调用produce方法,对数据做一些操作
const newState = produce(oldState,(draft)=>{
draft[1].age = 18
draft.push({uname:'王五',age:20})
})
通过这样的方式,原数据不会被修改,返回一个新的数据,避免了原生js写法要拷贝数据等一些繁琐的操作
把immer用于redux中
- 方式一
import { createStore } from "redux";
import { composeWithDevTools } from "@redux-devtools/extension";
import { produce } from "immer";
//定义初始的数据
const initialState = {
count: 0,
};
function rootReducer(state = initialState, action: any) {
switch (action.type) {
case 'inc':
//利用这种方式,返回的是一个在原数据的基础上修改后的新数据,当数据的层级多时,
//不需要拷贝,展开等操作,直接按最简便的方式对newState进行修改就行
return produce(state, (newState) => {
newState.count++;
});
case 'dnc':
return produce(state, (newState) => {
newState.count--;
});
case 'incbynum':
return produce(state, (newState) => {
newState.count += action.payload;
});
default:
return state;
}
}
export default createStore(
rootReducer,
composeWithDevTools()
);
- 方式二,进一步简化
把rootReducer换一个方式来描述
const rootReducer = produce((state = initialState, action: any) {
switch (action.type) {
case 'inc':
state.count++;
//在produce中的return默认就是返回新数据,当然也可以写return state,不过return不能省略
return
case 'dnc':
state.count--;
return
case 'incbynum':
state.count += action.payload;
return
default:
return state;
}
})
@reduxjs/toolkit的使用
通过这个方法可以极大的省略前面所写的一系列繁琐操作
@reduxjs/toolkit中已经包含了redux的核心,redux-thunk,@reduxjs/devetools,immer库等等
- 安装:npm i @reduxjs/toolkit,同时别忘了使用npm i react-redux
- 从创建主仓库开始:
// 引入 配置仓库的 函数
import { configureStore } from "@reduxjs/toolkit";
// 这里引入的是切片文件, 也就是相当于前面把reducer拆分出去的文件,默认引入的就是一个 reducer 函数
import countReducer from "./stores/count";
// 调用 configureStore, 并配置好 reducer 选项,返回的就是一个 store 对象
const store = configureStore({
// 这个 reducer 配置,类似于 combineReducers 的写法
reducer: {
// 把引入的countReducer配置到这里
count: countReducer,
},
});
// 导出 store
export default store;
- 接着创建切片文件
这里在store的stores下创建一个count.js作为示例
//引入createSlice方法
import { createSlice } from "@reduxjs/toolkit";
//调用createSlice方法编写切片的配置会返回一个切片给countSlice接收
const countSlice = createSlice({
// 切片的名字,全局唯一。而且该 name 会作为 actionType 的前缀。
name: "count",
// initialState来初始化的state数据
initialState: {
num: 0,
},
//配置reducer,每一个方法就像是前面配置reducer中的case,而这些方法在组件中通过countSlice.actions.xxx来使用
reducers: {
inc(state, action) {
// 由于整个RTK集成了immer.所以你可以直接操作state来修改
state.num++;
},
dec(state) {
state.num--;
},
},
});
// 切片对象中有两个东西,你可能在上面看不到,但记住有这两个东西,上面的配置就是来配置这两个东西
// 1. actions
// 是一个对象,对象中的每一项都是一个 reducers中的动作函数(动作创建函数)
// 每一项 调用之后返回的就是一个同步的动作 {type: 'xxxx'},这里就返回{ type: 'count/inc' }
// 组件通过 dispatch(actions.xxx()) 代码会执行到 reducers.xxx 这个函数中去
// 组件通过 dispatch(actions.xxx(payload)) 代码会执行到 reducers.xxx 这个函数中去, 对应函数中 action.payload 就是这里传递过去的 payload
// 2. reducer
// 需要导出给到 configureStore 去使用
// 每一个 reducer 都会接受到两个参数,state 与 action,
// 为了组件方便使用,具名导出切片的动作,给到组件使用来修改state数据,也可以只导出actions,在使用时通过actions.xxx(payload)来使用
export const inc = countSlice.actions.inc;
export const dec = countSlice.actions.dec;
//默认导出countSlice的reducer给主仓库的configureStore使用
export default countSlice.reducer;
- 别忘了react-redux的Provider要在main.js中配置
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
//引入导出的主仓库
import store from "./store/index";
import App from "./App";
//使用react-redux 的 Provider包裹根组件,并绑定主仓库
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);
- 最后就是在组件中使用了
//引入react-redux的一系列方法
import { useSelector, useDispatch } from "react-redux";
// 引入切片导出的动作创建函数
import { inc, dec } from "./stores/count";
function App() {
//勾出仓库中count切片中的数据num
const num = useSelector((state: any) => state.count.num);
const dispatch = useDispatch();
return (
<div>
<h1>App - {num}</h1>
//这里派发动作与之前有区别,括号不能省略,是通过这个函数来创建动作再派发过去,括号中如果写参数就会传入到payload中
<button onClick={() => dispatch(inc())}>inc</button>
<button onClick={() => dispatch(dec())}>dec</button>
</div>
);
}
export default App;
数据持久化
- 手动做存储
在仓库的reducers的方法中设置localStorage.setItem() - 使用第三方npm包
npm install store2
//因为存只能字符串,取数据就要把字符串转成想要的格式
import store2 from 'store2'
//用store2可以免了转数据类型的各种操作,直接存,取出来就是存的时候的格式,store2帮我们做了转换的步骤
store2.set('flag',true)
store2.get('flag')
store2.set('obj',{sname:'张三',age:18})
store2.get('obj')
store2.session.set('msg',{name:'123',age:123})
store2.session.get('msg')
//使用自动持久化的库
安装npm i redux-persist
主仓库中
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import appReducer from './modules/app'
//导入自动持久化的API
// persistReducer(config,要操作持久化的reducer切片)、config:配置项
// persistStore(store)用于main.tsx中用PersistGate组件的persistor
import { persistReducer, persistStore } from 'redux-persist'
//导入存储到localStorage还是sessionStorage
import storage from 'redux-persist/lib/storage'
// import storage from 'redux-persist/lib/storage/session'这个表示存储到session
const rootReducer = combineReducers({
//使用自动持久化的库,对有需要存储数据的切片库做持久化存储
app:persistReducer({
key:'app',//存储在storage的一个标识,前面会自动加上persist:
storage,//这里配置存到localStorage还是sessionStorage
blacklist:[]//黑名单,表示在该切片中不想存储的数据
},appReducer)
})
const store = configureStore({
reducer:rootReducer
})
export type RootState = ReturnType<typeof store.getState>
// 使用 persistStore 方法,生成 persistor,用于入口文件中 PersistGate 组件的 同名属性
export const persistor = persistStore(store);
export default store
在main.tsx中
import ReactDOM from 'react-dom/client'
import App from './App'
import { BrowserRouter as Router } from 'react-router-dom'
//导入仓库导出的自动化存储的工具persistor
import store,{ persistor } from './store'
//导入react-redux的Provider包裹根组件让根组件下的所有组件可以使用仓库
import { Provider } from 'react-redux'
//导入持久化存储库的PersistGate组件来包裹根组件
import { PersistGate } from 'redux-persist/integration/react'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<Provider store={store}>
<PersistGate persistor={persistor}>
<Router>
<App />
</Router>
</PersistGate>
</Provider>
)