React的通信方式与状态管理:Redux与Mobx
kerwin-《千锋react全家桶》笔记,部分参考了:
- https://www.html.cn/qa/react/17960.html
- https://redux-saga.js.org/
- https://github.com/redux-saga/redux-saga
一整个整理下来可谓是十分酸爽 😃,欢迎指正
一 基本通信方式
1.1 props
-
父组件中,给子组件添加属性,属性值为函数
-
子组件内部通过
props
执行该函数,以修改父组件的状态
-
可通过状态提升的方式实现兄弟组件之间的通信。即让兄弟的状态全部由父组件管理,兄弟组件则通过
props
来更新自己的状态
1.2 Ref
-
子组件内部维护自身的
state
-
父组件中,通过
React.createRef()
创建ref对象。并给子组件添加ref
属性,属性值为该对象 -
父组件通过
this.ref对象.current.state
获取子组件内部的state
状态
1.3 订阅发布
const center={
//保存回调函数
subscribers:[],
//订阅:将回调函数保存起来
subscribe(callback){
this.subscribers.push(callback)
},
//发布,遍历所有回调函数并执行
publish(value){
this.subscribers.forEach(callback=>callback(value))
}
}
//组件一般在初始化的时候进行订阅,
center.subscribe((value)=>{
...
})
//发布数据,所有订阅的组件都会监听到更新
center.publish('人民日报')
1.4 context(生产者消费者)
生产者管理状态并暴露方法,消费者在特定环境内可使用生产者提供的状态和方法。
-
通过
Context=React.createContext()
创建一个上下文对象。其中有两个角色:- 生产者:
<Context.Provider></Context.Provider>
- 消费者:
<Context.Consumer></Context.Consumer>
- 生产者:
-
生产者组件通过前者包裹消费者组件,并设置
value
属性,属性值是一个对象,提供一些状态和方法 -
消费者通过后者包裹一个回调函数
(value)=>{return ...dom...}
,消费者可以通过value.
获取生产者的状态或者调用生产者提供的方法const GlobalContext=React.createContext()//创建对象 //生产者 <GlobalContext.Provider value={{ info:this.state.info, changeInfo:(v)=>{ this.setState({ info:v }) } }}> ...一些享受服务的组件... </GlobalContext.Provider> //消费者 <GlobalContext.Consumer> { (value)=>{ return <div onClick={()=>{value.changeInfo('参数')}}> {value.info} </div> } } </GlobalContext.Consumer>
二 redux
redux属于flux模式的一种实现形式
2.1 执行过程
-
创建reducer:接收两个参数:原状态与action,对状态进行修改后返回新状态
-
创建store:引入
createStore
并传入reducer
-
订阅:通过
subscribe
订阅,并通过getState()
获取状态 -
发布:通过
dispatch
发布action
-
监听:
reducer
拦截到action
后修改状态并返回新状态//reducer.js const reducer=(prevState={...},action={...})=>{ //对原状进行深拷贝,这种写法仅对第一层管用。留坑,后面有解决方案 let newState={...prevState} switch(action.type){ case 'open-show': newState.isShow=true return newState; case 'getlist': newState.list=action.payload return newState; default:return prevState } } export default reducer
//store.js import {createStore} from 'redux' import reducer from './reducer' const store=createStore(reducer) export default store
store.subscribe(()=>{ store.getState() })
store.dispatch({ type:'...' payload:'...' })
使用纯函数reducer执行state更新。纯函数表示对外界没有副作用,同样的输入得到同样的输出
2.2 redux原理
function createStore(reducer){
let state=reducer() //reducer没传参数是初始状态
//订阅
let callbackList=[]
function subscribe(callback){
callbackList.push(callback)
}
//分发
function dispatch(action){
state=reducer(state,action) //处理事件并覆盖状态
//通知订阅者
for(let i in callbackList){
callbackList[i]&&callbackList[i]()
}
}
//返回状态
function getState(){
return state
}
return{
subscribe,
dispatch,
getState
}
}
2.3 常规目录结构
- redux
- actionCreator :定义每个组件涉及的action
- AComponentAction.js
- BComponentAction.js
- reducers:定义每个组件的reducer
- AComponentreducer.js
- BComponentreducer.js
AComponentAction.js
function show(){
return{type:'test-show'}
}
export {show}
import {show} from './actionCreator/AComponentAction'
store.dispatch(show())
重组:1.combineReducers
用于合并reducer;2.此时getState()
包含多个模块
import {combineReducers,createStore} from'redux'
import {AComponentreducer,BComponentreducer} from './reducers'
const reducer=combineReducers({
CityReducer,TabbarReducer
})
const store=createStore(reducer)
2.4 开发者工具
-
在store.js中配置(上线前要删除)
import {applyMiddleware,combineReducers,createStore,compose} from'redux' const composeEnhancers=window._REDUX_DEVTOOLS_EXTENSION_COMPOSE_||compose; const store=createStore(reducer,/* preloadedState,*/composeEnhancers(applyMiddleware(reduxThunk,reduxPromise)))
三 redux中间件
当处理异步事件时,需要使用到中间件,包括redux-thunk
、redux-promise
、redux-saga
等。此时需要从redux
中引入applyMiddleware
来使用中间件。
3.1 redux-thunk
import {applyMiddleware,createStore} from'redux'
import reducer from './reducer'
import reduxThunk from 'redux-thunk'
const store = createStore(reducer,applyMiddleware(reduxThunk))
此时action既可以是对象也可以是函数,中间件会判断返回的是对象还是函数,如果是对象则按之前的流程进行,忽略中间件;如果是函数,就按照异步的方式进行。最后对结果进行dispatch
store.dispatch(getListAction())
function getListAction(){
return (dispatch,store)=>{
axios.get(...).then(res=>{
dispatch({
type:'getlist'
value:res.data
})
})
}
}
3.2 redux-promise
与前者效果差不多,只是写的方式不一样。
import {applyMiddleware,createStore} from'redux'
import reducer from './reducer'
import reduxPromise from 'redux-promise'
const store = createStore(reducer,applyMiddleware(reduxPromise))
store.dispatch(getListAction())
第一种写法
function getListAction(){
return axios.get(...).then(res=>{
return{
type:'getlist'
value:res.data
}
})
}
}
async function getListAction(){
let list=await axios.get(...).then(res=>{
return{
type:'getlist'
value:res.data
}
})
}
return list
}
3.3 redux-saga
对于前两者方式,action的格式不统一,异步操作分散,如果需要为每一个异步操作都如此定义一个action,不容易维护。saga是基于生成器的中间件,统一了action 的格式,更易于维护
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import watchSaga from './watchSaga' //自身定义的saga文件,实现监听
const sagaMiddleware = createSagaMiddleware() //创建saga中间件
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(watchSaga) //用saga中间件开启saga文件
- take:监听dispatch的action
- fork:非阻塞立即执行生成器函数
- takeEvery:上面两个的合体
- put:抛出最后的action
- all
//watchSaga.js
import { call, put, takeEvery,take, fork} from 'redux-saga/effects'
function *watchSaga(){
while(true){
yield take("getlist")
yield fork(getList)
}
//简写:takeEvery('getlist',getList)
}
function *getList(){
let res=yield call(getListAction) //阻塞式调用异步事件
yield put({
type:'change-list',
payload:res
})
}
function getListAction(){
// 返回promise对象的异步事件
}
export default watchSaga
如果有多个saga:
-
通过
all
进行合并//watchSaga.js import { all } from 'redux-saga/effects' import watchSaga1 from ./sagas/watchSaga1 import watchSaga2 from ./sagas/watchSaga2 function *watchSaga(){ yield all([watchSaga1(),watchSaga2()]) } export default watchSaga
-
省略每个子saga的
watchSaga
,将其汇总在这里import { takeEvery } from 'redux-saga/effects' import getList1 from './..' //引入生成器函数 import getList2 from './..' //引入生成器函数 function *watchSaga(){ takeEvery('getlist1',getList1) takeEvery('getlist2',getList2) } export default watchSaga
三 redux-persist
对数据进行持久化保存
import {persistStore,persistReducer} from 'redux-persist'
import storage from 'redux-persistlib/storage'
const persistConfig={
key:'root',
storage,
widthList:['cityName']
}
//包装原来的reducer
const persistedReducer=persistReducer(persistConfig,reducer)
const store=createStore(persistedReducer)
//持久化store
let persistor=persistStore(store)
export {store,persistor}
import {PersistGate} from 'redux-persist/intergration/react'
const App=()=>{
return(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
根组件
</PersistGate>
</Provider>
)
}
四 react-redux
4.1 性能问题
redux需要自己取消订阅
useEffect(()=>{
...
let subscribe=store.subscribe(()=>{
...
})
return ()=>{
subscribe()
}
},[])
react-redux
可以帮助取消订阅
4.2 基本使用
-
Provider
import {Provider} from 'react-redux' import store from './..' <Provider store={store}> ... </Provider>
-
connect(给子组件的属性,给子组件的回调)
当有人改了状态,作为包装组件,订阅了之后会再传
import {connect} from 'react-redux' export default connect((state)=>{ return{ a:1, isShow:state.TabbarReducer.show } })(App)
this.props.a
可装入同步的和异步的action,实现自动分发(dispatch)
import {connect} from 'react-redux' export default connect(null,{ actionA(value){return{ type:'...', payload:value }}, show hide })(App)
this.props.actionA(value)
4.3 基本原理
(1)connect是高阶组件(HOC,high order component)
(2)Provider组件,可以让容器组件拿到state,使用了context
跟着老师自己学着封装了一下,应该有错误的理解,也不知道怎么处理异步。(留坑)
import store from './redux/store'
export default function testConnect(callback,funcobj){
let value=callback(store.getState()) //获取值
let dispatchObj={}
for (let key in funcobj){
dispatchObj[key]=()=>{store.dispatch(funcobj[key]())}
}
return (Mycomponent)=>{
return (props)=>{ //返回函数组件,若组件外层有包裹,接收
return <div>
<Mycomponent {...value} {...dispatchObj} {...props}/>
</div>
}
}
}
export default testConnect(()=>{
return{
a:1,
b:2
}
})(MyComponent)
?为什么props是路由参数:(自己的理解,留坑)
因为MyComponent是Route组件component属性的的属性值。Route组件的作用是接受组件,并将路由信息解构给组件、返回带了路由信息的组件。
<Route path="" component={MyComponent}></Route>
class Route extends React.Component{
render(){
var MyComponent=this.props.component
return <div>
<MyComponent history={} match={} .../>
</div>
}
}
此时给MyComponent外面又包裹了一层函数组件,那么Route传递下来的路由信息将被外层的函数组件接收到,进而再解构给原组件
return (props)=>{
return <div>
<MyComponent {...value} {...props} {...obj}/>
</div>
}
五 Mobx
与redux相比的优点
- 直接修改
- 多个store
- 可观察对象
5.1 基本使用
-
observable:将状态变为可观察
-
autorun:监听可观察对象
import { observable, autorun } from "mobx" //创建可观察对象 let observableObj=observable({ name:'啦啦啦', age:100 }) //注册监听 autorun(()=>{ console.log("对象的name属性改变了",observableObj.get("name")) }) setTimeOut(()=>{ observableObj.set("name","哈哈哈") })
该写法是的状态可以随处修改
5.2 action
action限制了状态修改自由,让状态集中修改,便于维护
import { observable,action,configure } from "mobx"
configure({
enforceAction:'always' //开启严格模式
})
//创建可观察对象
const store=observable({
name:'啦啦啦',
age:100,
changeName(value){
this.name=value
}
},{
changeName:action //标记方法action
})
使用runInAction处理异步
import { observable,action,runInAction } from "mobx"
import axios from 'axios'
const store=observable({
list=[],
getList(){...}
},{
getList:action
})
//写法1
getList(){
axios({...}).then(res=>{
runInAction(()=>{
this.list=res.data
})
})
}
//写法2
async getList(){
let res=await axios({...}).then(res=>{
return res.data
})
runInAction(()=>{
this.list=res
})
}
5.3 装饰器写法
所谓装饰器,就是将对象按一定规则包装后吐出来
import {observable,configure, action} from 'mobx'
class Store{
@observable name:'啦啦啦',
@action changeName(){...}
}
export default new Store()
在用装饰器语法前需要进行配置
-
vscode配置:首选项——设置——搜索experimentalDecoration
-
安装支持
-
创建
.babelrc
和config-overrides.js
-
安装依赖与修改脚本
npm i @babel/core @babel/plugin-proposal-decorators @babel/preset-env
//.babelrc { "presets":[ "@babel/preset-env" ], "plugins":[ [ "@babel/plugin-proposal-decorators", { "legacy":true } ] ] }
//config-overrides.js const path=require(' path') const { override, addDecoratorsLegacy }=require('customize-cra') function resolve(dir){ return path.join(_dirname, dir) } const customize=()=>(config, env)=>{ config.resolve.alias['@']=resolve('src') if(env==='production'){ config.externals={ 'react':'React', 'react-dom':' ReactDoM' }I } return config }; module.exports=override(addDecoratorsLegacy(), customize())
//安装依赖 npm i customize-cra create-app-rewrired
//package.json "scripts":{ "start":"react-app-rewired start", "build":"react-app-rewired build", "test":"react-app-rewired test", "eject":"react-app-rewired eject" },
5.4 mobx-react
可以自动监听
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'mobx-react'
import store from './..'
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root'))
以属性的方式获取和修改store
import {inject,observer} from 'mobx-react'
@inject("store") //与在Privider中的属性key一致
@observer
class App extends React.Component{...}
this.props.store
对于函数式组件,需要用<Observer>
包起来
import {Observer} from 'mobx-react'
<Observer>
{
()=>{
return DOM
}
}
</Observer>
六 Immutable
在上面的第二节中留了个深拷贝的坑,Immutable是用于解决深拷贝问题的方案
6.1 旧方案
下面的方案只对第一层管用
- 解构
slice
concat
Object.assign
需要保证没有undifine
,因为该方法会忽略undifine
的属性
JSON.parse(JSON.stringify())
6.2 基本使用
-
Map(obj)
:将普通对象转为map类型。此时在map结构上修改不会应该旧状态import {Map} from 'immutable' let oldImmuObj=Map(obj) //转换 let newImmuObj=oldImmuObj.set("name","xiaoming") //更改 newImmuObj.get("name") //获取
-
List(Arr)
:将数组转为特殊对象。被List转换后的数据结构可以通过原数组对象的方法对其进行操作import {List} from 'immutable' let oldArr=List([1,2,3]) //转换 let newArr=oldArr.push(4) //更改
6.3 实际应用
6.3.1 单层
-
自始而终:在源头转换
state={ info:Map({ name:"kerwin", age:100, }), food:List(['水果茶','番茄','蛋糕']), }
对于只含一层的info对象和food数组,其改变状态的方式如下:
this.setState({ info:this.state.info.set("name","xiaoming").set("age",18) info:this.state.info.set("food",this.state.info.get("food").splice(index,1)) )}
实际上,数组元素是一个对象{{0:'水果茶'},{1:'番茄'},{2:'蛋糕'}},上面的写法很明显是多层结构,显而易见,这种写法很复杂。
-
按需转换:在过程中转换——
toJS()
:转换为js对象
上面的写法无法预知后端数据来执行对应转换。下面的写法是在用到的时候进行map或list转换。最后通过返回原始结构state={ info:{ name:"kerwin", age:100, }, food:['水果茶','番茄','蛋糕'], }
let old=Map(this.state.info) //转为Map let newObj=old.set("name","xiaoming") //修改 this.setState({ info:newObj.toJS() //恢复为js对象 )}
6.3.2 SCU
对于复杂类型,可以使用
JSON.stringify
转为字符串后再对比。但如果属性值有undefined
会出错。
通过get
获取属性值进行对比
<Child filter={this.state.info.get("name")}/>
shouldComponentUpdate(nextProps,nextState){
if(this.props.name===nextProps.name){
return false
}
return true
}
6.3.3 复杂结构
fromJS()
:自动将普通js对象转为对应的map和list,帮助我们解决了无法根据后端发来的数据进行对应转换的问题setIn(['第一层key',...,'目标key'],内容)
:操作深层对象updateIn(['第一层数组key',...,'目标数组key'],(list)=>对list操作并返回)
:操作深层数组
import {fromJS} from 'immutable'
state={
info:fromJS({
name:"kerwin",
age:100,
}),
food:fromJS(['水果茶','番茄','蛋糕']),
charecter:fromJS({
language:['Chinese','English','Japanese'],
location:{
province:'广东',
city:'江门'
}
})
}
-
引入:如果想对深层的结构进行修改,单纯使用
set
会很复杂:this.setState({ charecter:this.state.charecter .set("location",this.state.charecter.get("location").set("city","广州")), //多层 )}
为了更简单地对深层的结果进行修改,可以使用setIn(['第一层key',...,'目标key'],内容)
this.setState({
//多层
charecter:this.state.charecter.setIn(["location",'city'],"广州") //先改对象
.setIn(['language',index],111), //再改数组元素
//单层也可以使用setIn
info:this.state.info.setIn(["name"],'haha')
)}
在setIn()
中,数组可以传入索引值修改数组元素。但数组更适合用updateIn(['数组key'],(list)=>对list操作并返回)
this.setState({
charecter:this.state.charecter.updateIn(['language'],(list)=>list.spice(index,1)),
)}
6.3.4 reducer中应用
-
在reducer中将初始值用
fromJS()
转换 -
对状态通过
set
、setIn
、updateIn
直接修改即可const reducer=(prevState=fromJS({ show:true, city:'北京' }),action={})=>{ switch(action.type){ case:'change-show': return prevState.set("show",false) ... } }
-
在函数内进行
fromJS()
转换 -
为了与初始状态一致,返回状态时再用
toJS()
const reducer=(prevState={ city:'北京' },action={})=>{ let newState=fromJS(prevState) switch(action.type){ case:'change-city': return newState.set("city",action.playload).toJS() ... } }