React笔记(八)
1.Redux
-
Redux
是一个状态管理容器,提供一个可预测化的状态管理,在一般情况下不被使用。当某个组件的状态需要让其他组件随时可以拿到时,或一个组件需要改变另一个组件的状态时。才会使用,总体原则是,能不用就尽量不用。 -
当父子组件间通信时,可以使用props进行值或方法的传递,当兄弟组件间通信时,可以使用状态提升。当两个陌生组件间通信时,还可以使用消息订阅与发布。消息订阅与发布可以理解为需要接收信息的组件为订阅方,传输信息的为发布方,当发布方发布消息时,订阅方会会收到消息。也就是一个订报与发报的关系。
// 引入消息订阅与发布依赖,未安装则需要安装 import PubSub from 'pubsub-js'; export default function Demo(props) { return <> <ChildA/> <ChildB/> </> } function ChildA(){ // 点击按钮后触发Puhsub的发布方法,进行消息发布 return <button onClick={()=>PubSub.publish("publish_A","传值")}>我是组件A,点我传值</button> } function ChildB(){ return <> <p>我是组件B</p> <GrandSonC/> </> } function GrandSonC(){ const [data,setData] = React.useState('目前还未传值'); React.useEffect(()=>{ // 订阅消息,订阅到消息时触发状态更改,刷新页面 // 这里订阅的第一个参数要与上方发布的第一个参数相同,并充当后面回调函数中的msg const token = PubSub.subscribe("publish_A",(msg,data)=>{ console.log(msg); setData(data); }) // 组件卸载时取消订阅 return ()=>{PubSub.unsubscribe(token)} },[]); return <> <p>我是组件C,我接收到的订阅传值为:{data}</p> </> }
-
而
Redux
相对于消息订阅与发布要复杂的多,但是能够实现的功能也多的多。如果需要陌生组件间共享的内容特别多时,这时消息订阅与发布便不再使用。此时使用Redux
更好。 -
在Redux中共有四个部分,也就是下图所示的四个部分,分别是表示状态的UI组件(React Components),传递想法的动作发生器(Action Creators),存储共享状态的仓库(Store),修改共享状态的工具(Reducers)。在这四个部分中,只有Store不存在复数,也就是说,除了此部分唯一以外,其余部分在项目中均可以存在多个。
-
这四部分的具体工作流程为:
- 背景为A,B组件公用一套状态,A可以修改,B可以获取。(打电话点餐,A下单,B到店获取)
- A组件内部触发事件后,将想要对状态进行何种更改发送给动作发生器。(A下单订餐,给饭店打电话并告诉想要的菜品)
- 动作发生器接收到后创建一个动作并派送给状态仓库,告诉动作类型与数据。(饭店接话人根据客人的语言得知其想吃什么,并记录下来告诉老板)
- 状态仓库把接收到的内容,和当前的状态,递交给修改工具。(饭店老板看见后,查看库房的食物,并带着食物去找厨子)
- 修改工具根据接收到的动作,和当前的状态,创建一个新的状态并返回给状态仓库。(厨子根据原材料与菜单进行加工,把加工好的食物递给老板)
- 状态仓库将修改好的状态放置在仓库内,等待B组件获取。(饭店老板给B打电话,告诉餐好了,可以来取了,B去饭店取餐)
具体的使用方法为
import { createStore } from 'redux'; // 定义常量,防止拼写错误 const ADD = 'add'; const SUB = 'sub'; // 定义动作制作器,将接收到的消息处理后,派送给状态仓库 function addActionCreator(num){ return {type:ADD,data:num}; } function subActionCreator(num){ return {type:SUB,data:num}; } // 定义默认状态 const initState = 0; // 定义操作函数处理状态 function DemoReducer(preState = initState, action){ var state = preState; switch(action.type){ case ADD: state+=parseInt(action.data); break; case SUB: state-=parseInt(action.data); } return state; } // 根据reducer,定义状态仓库 const DemoStore = createStore(DemoReducer); export default function Demo(props) { return <> <CompA/> <CompB/> </> } function CompA(){ const inputRef = React.useRef(); return <> <input type="number" ref={inputRef} /> {/* 点击按钮分发动作 */} <button onClick={()=>{DemoStore.dispatch(addActionCreator(inputRef.current.value))}}>点我数字加</button> <button onClick={()=>{DemoStore.dispatch(subActionCreator(inputRef.current.value))}}>点我数字减</button> </> } function CompB(){ const initCount = DemoStore.getState(); const [count,setCount] = useState(initCount); React.useEffect(()=>{ // 进行订阅,当Redux状态更改时,获取新的值赋予给当前组件状态 const unSubScribe = DemoStore.subscribe(()=>{setCount(DemoStore.getState())}); // 取消订阅 return unSubScribe; },[]); return <p>{count}</p> }
其中存在几个重要的API
createStore
,接收一个reducer方法作为参数,用于制作Store。
- 背景为A,B组件公用一套状态,A可以修改,B可以获取。(打电话点餐,A下单,B到店获取)
-
subscribe
,接受一个回调函数作为参数,用于创建订阅,在状态更改时调用回调函数并返回一个取消订阅的函数。getState
,获取当前Store内的状态。
-
在Redux中存在三大原则:单一数据源,State是只读的,使用纯函数来执行修改
- 单一数据源,代表着在一个React应用中,只存在一个Store树,所有的共享State放置在一起工程一个Store。这使得开发起来较为容易,并可以轻松实现"撤销/重做”等功能。
- State是只读的,在实际操作时只允许通过触发action来修改state,不可以直接修改state。
- 使用纯函数来执行修改,reducers必须是纯函数,接收到相同的参数时,返回的State必须相同。
-
当存在有多个State时,这时可以使用
combineReducers
来进行操作,也就是对于Reducer的拆分。import { createStore,combineReducers } from 'redux'; // 定义常量,防止拼写错误、 // 多个Reducer时要注意不要使用相同的,因为在Dispatch时所有的Reducer都会参与运行 const ADD = 'add'; const SUB = 'sub'; const ADD2 = 'add2'; const SUB2 = 'sub2'; // 定义动作制作器,将接收到的消息处理后,派送给状态仓库 function addActionCreator(num){ return {type:ADD,data:num}; } function subActionCreator(num){ return {type:SUB,data:num}; } function add2ActionCreator(num){ return {type:ADD2,data:num}; } function sub2ActionCreator(num){ return {type:SUB2,data:num}; } // 定义默认状态 const initState = 0; // 定义操作函数处理状态 function DemoReducer(preState = initState, action){ var state = preState; switch(action.type){ case ADD: state+=parseInt(action.data); break; case SUB: state-=parseInt(action.data); } return state; } const initState2 = 0; function DemoReducer2(preState = initState2, action){ var state = preState; switch(action.type){ case ADD2: state+=parseInt(action.data); break; case SUB2: state-=parseInt(action.data); } return state; } // 结合reducer,并定义状态仓库 const CombinedState = combineReducers({DemoReducer,DemoReducer2}); const DemoStore = createStore(CombinedState); export default function Demo(props) { return <> <CompA/> <CompB/> <CompC/> </> } function CompA(){ const inputRef = React.useRef(); return <> <input type="number" ref={inputRef} /> <button onClick={()=>{DemoStore.dispatch(addActionCreator(inputRef.current.value))}}>点我数字加</button> <button onClick={()=>{DemoStore.dispatch(subActionCreator(inputRef.current.value))}}>点我数字减</button> </> } function CompB(){ // 获取State时方式改变要加上.Reducer进行获取 const initCount = DemoStore.getState().DemoReducer; const [count,setCount] = useState(initCount); React.useEffect(()=>{ // 进行订阅,当Redux状态更改时,获取新的值赋予给当前组件状态 const unSubScribe = DemoStore.subscribe(()=>{setCount(DemoStore.getState().DemoReducer)}); // 取消订阅 return unSubScribe; },[]); return <p>{count}</p> } function CompC(){ console.log(DemoStore.getState()); const initCount = DemoStore.getState().DemoReducer2; const [count,setCount] = useState(initCount); const inputRef = React.useRef(); React.useEffect(()=>{ // 进行订阅,当Redux状态更改时,获取新的值赋予给当前组件状态 const unSubScribe = DemoStore.subscribe(()=>{setCount(DemoStore.getState().DemoReducer2)}); // 取消订阅 return unSubScribe; },[]); return <><input type="number" ref={inputRef} /> <button onClick={()=>{DemoStore.dispatch(add2ActionCreator(inputRef.current.value))}}>点我数字加</button> <button onClick={()=>{DemoStore.dispatch(sub2ActionCreator(inputRef.current.value))}}>点我数字减</button> <p>{count}</p></> }
-
Redux符合严格的单向数据流,即**dispatch派发动作=>调用reducer函数操作State=>合并State形成Store=>返回一个完成的State树,调用所有订阅监听器。**这一数据流即完成了上部分整体流程的实现,并实现了三大原则。
2.React-Redux
-
在使用时可以根据情况选择是否使用React-Redux进行实现。在React-Redux中存在有两类组件,UI组件与容器组件。Redux直接与容器组件关联,容器组件将从Redux处获取的状态传递给UI组件。在连接中则是使用connect将容器组件与UI连接上。
-
其中容器组件是通过一个方法生成的。其负责管理数据与业务逻辑,带有内部状态,并使用Redux的API。在使用时,store则是通过父组件向容器组件传递。
// actions,reducers,store不用更改 export default function Demo(props) { return <> <CompA/> <CompB/> {/* 需要引入容器组件,而非原本直接引入组件,并将Store作为store参数传递给容器组件 */} <CompCContainer store={DemoStore}/> </> } // 没有更改 function CompA(){ const inputRef = React.useRef(); return <> <input type="number" ref={inputRef} /> <button onClick={()=>{DemoStore.dispatch(addActionCreator(inputRef.current.value))}}>点我数字加</button> <button onClick={()=>{DemoStore.dispatch(subActionCreator(inputRef.current.value))}}>点我数字减</button> </> } // 没有更改 function CompB(){ // 获取State时方式改变要加上.Reducer进行获取 const initCount = DemoStore.getState().DemoReducer; const [count,setCount] = useState(initCount); React.useEffect(()=>{ // 进行订阅,当Redux状态更改时,获取新的值赋予给当前组件状态 const unSubScribe = DemoStore.subscribe(()=>{setCount(DemoStore.getState().DemoReducer)}); // 取消订阅 return unSubScribe; },[]); return <p>{count}</p> } // 将接收到的store与容器组件接收到的props传递给UI组件 function mapStateToProps(state,props){ return { count:state.DemoReducer2, ...props }; } // 将派送给Store的动作传递给UI组件,接收dispatch方法 function mapDispatchToProps(dispatch,props){ return { CountControl:function(action){ dispatch(action); }, } } // 使用connect创建容器组件,前项为store中的状态,后项为派送给store的动作 const CompCContainer = connect(mapStateToProps,mapDispatchToProps)(CompC); // 去除掉内部state与生命周期函数,直接从props上获取,变成UI组件 function CompC(props){ const inputRef = React.useRef(); console.log('name',props.name); return <><input type="number" ref={inputRef} /> <button onClick={()=>{props.CountControl(add2ActionCreator(inputRef.current.value));props.func()}}>点我数字加</button> <button onClick={()=>{props.CountControl(sub2ActionCreator(inputRef.current.value))}}>点我数字减</button> <p>{props.count}</p></> }
3.Redux中间件
-
在Redux中,可以通过添加中间件,来为Redux增添功能。中间件可以完成在action流中对action进行操作。即在dispatch分发阶段将传入的action进行操作,并传递给下一个reducer。
-
上述的Redux实现了同步操作,即action作为一个对象出现。当action作为一个函数时,其就变成了一个异步操作。
-
Redux Thunk即应用于异步操作,在使用之前需要先对其进行安装
npm install --save redux-thunk
。针对1中的代码添加异步操作,可以变为import { createStore,applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; // 定义常量,防止拼写错误 const ADD = 'add'; const SUB = 'sub'; // 定义动作制作器,将接收到的消息处理后,派送给状态仓库 function addActionCreator(num){ return {type:ADD,data:num}; } function subActionCreator(num){ return {type:SUB,data:num}; } // 异步动作制作器,返回的不再是对象,而是一个接收绑定函数作为参数的函数 // 一般情况下,异步动作中都会调用同步动作 function addAsyncActionCreator(num){ return (dispatch)=>{ setTimeout(()=>{ dispatch(addActionCreator(num)); },5000); }; } // 定义默认状态 const initState = 0; // 定义操作函数处理状态 function DemoReducer(preState = initState, action){ var state = preState; switch(action.type){ case ADD: state+=parseInt(action.data); break; case SUB: state-=parseInt(action.data); } return state; } // 根据reducer,定义状态仓库 // 引入thunk,并使用applyMiddleware方法将中间件引入 const DemoStore = createStore(DemoReducer,applyMiddleware(thunk)); export default function Demo(props) { return <> <CompA/> <CompB/> </> } function CompA(){ const inputRef = React.useRef(); return <> <input type="number" ref={inputRef} /> {/* 点击按钮派发动作 */} <button onClick={()=>{DemoStore.dispatch(addActionCreator(inputRef.current.value))}}>点我数字加</button> <button onClick={()=>{DemoStore.dispatch(subActionCreator(inputRef.current.value))}}>点我数字减</button> {/* 正常使用,调用动作发生器并派发动作 */} <button onClick={()=>{DemoStore.dispatch(addAsyncActionCreator(inputRef.current.value))}}>点我数字异步加</button> </> } function CompB(){ const initCount = DemoStore.getState(); const [count,setCount] = useState(initCount); React.useEffect(()=>{ // 进行订阅,当Redux状态更改时,获取新的值赋予给当前组件状态 const unSubScribe = DemoStore.subscribe(()=>{setCount(DemoStore.getState())}); // 取消订阅 return unSubScribe; },[]); return <p>{count}</p> }
-
除了使用redux-thunk,还可以使用Redux-Sage作为异步操作时的中间件,相对于redux-thunk,其不需要为每一个异步操作都定义一个action,并且其提供了声明式的Effect,并使用Generator功能,让异步流程更易于读取,写入与测试,减少回调地狱。但是相应的也更麻烦一点。
-
安装:
npm install --save redux-saga
-
其中包含有多个Effect方法,即用在yield之后的方法。下面将对一些常用的Effect方法进行使用。
import { createStore,applyMiddleware } from 'redux'; import { all, put, takeEvery, delay, takeLatest } from 'redux-saga/effects'; import createSagaMiddleware from 'redux-saga'; // 定义常量,防止拼写错误 const ADD = 'add'; const SUB = 'sub'; const ADD_ASYNC = 'addAsync'; const SUB_ASYNC = 'subAsync'; // 定义动作制作器,将接收到的消息处理后,派送给状态仓库 function addActionCreator(num){ return {type:ADD,data:num}; } function subActionCreator(num){ return {type:SUB,data:num}; } // 异步动作制作器 function addAsyncActionCreator(num){ return {type:ADD_ASYNC,data:num}; } function subAsyncActionCreator(num){ return {type:SUB_ASYNC,data:num}; } // 定义默认状态 const initState = 0; // 定义操作函数处理状态 function DemoReducer(preState = initState, action){ var state = preState; switch(action.type){ case ADD: state+=parseInt(action.data); break; case SUB: state-=parseInt(action.data); } return state; } // saga部分 // 监控部分,使用takeLast只执行最后一次action。并调用对应的函数 // 也就是说在相同时间内快速点击,只会调用一次 function* watchAdd(){ yield takeLatest(ADD_ASYNC,addAsync); } // 监控部分,使用takeEvery保证并行执行每一次action。并调用对应的函数 // 也就是说在相同时间内快速点击,每次都会调用 function* watchSub(){ yield takeEvery(SUB_ASYNC,subAsync); } // 执行部分,直接将接收到的动作,通过转换后进行调用,派发给reducer function* addAsync(action){ yield delay(1000); yield put({type:ADD,data:action.data}); } function* subAsync(action){ yield delay(1000); yield put({type:SUB,data:action.data}); } // 合并多个sage function* countSaga(){ yield all([watchAdd(),watchSub()]); } // 创建saga const sageMiddleware = createSagaMiddleware(); // 根据reducer,定义状态仓库 const DemoStore = createStore(DemoReducer,applyMiddleware(sageMiddleware)); // 动态运行saga,其位置必须在定义状态仓库之后 sageMiddleware.run(countSaga); export default function Demo(props) { return <> <CompA/> <CompB/> </> } function CompA(){ const inputRef = React.useRef(); return <> <input type="number" ref={inputRef} /> {/* 点击按钮派发动作 */} <button onClick={()=>{DemoStore.dispatch(addActionCreator(inputRef.current.value))}}>点我数字加</button> <button onClick={()=>{DemoStore.dispatch(subActionCreator(inputRef.current.value))}}>点我数字减</button> {/* 正常使用,调用动作发生器并派发动作 */} <button onClick={()=>{DemoStore.dispatch(addAsyncActionCreator(inputRef.current.value))}}>点我数字异步加</button> <button onClick={()=>{DemoStore.dispatch(subAsyncActionCreator(inputRef.current.value))}}>点我数字异步减</button> </> } function CompB(){ const initCount = DemoStore.getState(); const [count,setCount] = useState(initCount); React.useEffect(()=>{ // 进行订阅,当Redux状态更改时,获取新的值赋予给当前组件状态 const unSubScribe = DemoStore.subscribe(()=>{setCount(DemoStore.getState())}); // 取消订阅 return unSubScribe; },[]); return <p>{count}</p> }
-
参考文章
React 消息订阅与发布机制 - 掘金 (juejin.cn)
React-Redux - 简书 (jianshu.com)