1. redux同步数据流
dispatch一个action可以形象的理解为 "触发一个事件"。发生了一些事情,我们希望store知道这件事。Reducer就像事件监听器一样,当它们收到关注的action后,它就会更新state作为响应。
- 应用程序中发生了某些事情,例如用户单击按钮
dispatch 一个 action 到 Redux store,例如 dispatch({type: 'counter/increment'}) - store 用之前的 state 和当前的 action 再次运行 reducer 函数,并将返回值保存为新的 state
- store 通知所有订阅过的 UI,通知它们 store 发生更新
- 每个订阅过 store 数据的 UI 组件都会检查它们需要的 state 部分是否被更新。
- 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页
先根据这张redux工作流实现一个redux小demo,实现点击详情页后将tabbar隐藏。
(1) 通过npm安装好redux后,新创建一个store.js,并通过api创建好store
const store = createStore();
export default store;
(2) 在详情页中写好action,通过dispatch分发到store中再交由reducer处理
// dispatch 发布
useEffect(() => {
store.dispatch({
type: 'hide_tabbar'
})
return () => {
store.dispatch({
type: 'show_tabbar'
})
}
}, [])
(3) 获取到dispatch分发的action后,reducer接收prevState和action参数,根据传过来的type作对应处理,再将reducer作为参数传入store中,返回新的state供组件使用
const reducer = (prevState = {show: true}, action) => {
let newState = {...prevState};
switch(action.type){
case 'hide_tabbar':
newState.show = false;
return newState;
case 'show_tabbar':
newState.show = true;
return newState;
default :
return prevState;
}
}
const store = createStore(reducer)
(4)组件通过store.subscribe()和getState()获取到具体的参数,根据参数修改自己的状态,并根据状态决定是否渲染出tabbar
export default class App extends Component {
state = {
isShow : store.getState().show
}
// store 订阅
componentDidMount(){
store.subscribe(() =>{
this.setState({
isShow: store.getState().show
})
})
}
render() {
return (
<div>
<AppRouter>
{this.state.isShow && <TabBar></TabBar>}
</AppRouter>
</div>
)
}
}
因此也就可以自己实现一个简单的createStore()
function myCreateStore(reducer){
let list = [];
let state = reducer(undefined , {});
// 订阅函数 将回调函数推入列表中
function subscribe(callback){
list.push(callback)
}
// 发布函数 通过reducer处理好state后执行列表中的回调函数
function dispatch(action){
state = reducer(state , action)
for(let i in list){
list[i] && list[i]();
}
}
// 获取到新的state
function getState(){
return state;
}
return {
subscribe,
dispatch,
getState
}
}
2. 合并reducer
假设现在又通过redux管理了一个新的状态,那么新的状态也要通过dispatch后再经过reducer处理写在store中,如此一来会使得store中的reducer变得过为冗长且不好管理,因此可以使用combineReducer来进行管理:
city.js(相当于action)
import React , {useState}from 'react'
import store from '../store'
export default function City(props) {
const [cityList] = useState(['北京','上海','广州'])
const changeCity = (item) =>{
store.dispatch({
type: 'change_city',
// payload是指附带的参数
payload: item
})
props.history.push(`/cinema`)
}
return (
<div>
<ul>
{
cityList.map(item =>
<li key={item} onClick = {() => changeCity(item , props)}>{item}</li>
)
}
</ul>
</div>
)
}
store.js(不进行实际操作,仅作存储reducer用)
const reducer = combineReducers({
cityReducer,
tabbarReducer
})
const store = createStore(reducer);
export default store;
cityReducer.js(根据type对传过来的参数进行处理)
const cityReducer = (prevState = {
cityName: '城市'
}, action) => {
let newState = { ...prevState };
switch (action.type) {
case 'change_city':
// 将传过来的payload赋值给cityName
newState.cityName = action.payload;
return newState;
default:
return prevState;
}
}
export default cityReducer
cinema.js(根据新的参数渲染出最终结果)
export default function Cinema(props) {
// 根据reducer修改后的cityName进行渲染
const [cityName] = useState(store.getState().cityReducer.cityName)
const toCity = (props) =>{
props.history.push(`/city`)
}
return (
<div>
<div onClick={() => toCity(props)}>{cityName}</div>
</div>
)
}
3. redux-thunk
如果需要在action中处理异步请求时,则需要用到这个中间件连接reducer和action。
假如不用中间件的话举个例子:
// action
export default function getCinemaListAction() {
//通过axios请求数据 填充影院列表
axios({
url: 'https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=9159391',
methods: 'get',
headers: {
'X-Client-Info': '{ "a": "3000", "ch": "1002", "v": "5.2.1", "e": "167004825688003879895041", "bc": "110100" }',
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res => {
console.log(res.data.data.cinemas)
})
return {
type: 'change_list',
value: [1, 2, 3]
}
}
// reducer
store.dispatch(getCinemaListAction())
reducer中执行action函数时由于action内axios为异步执行,没那么快执行完,因此会直接返回undefined,导致报错
就比如这个例子,返回undefined:
function test(){
setTimeout(() =>{
return(100)
} , 2000)
}
console.log(test())
因此此时可以通过redux-thunk这个中间件来解决这个问题
(1) npm i redux-thunk
(2) store.js中引入reduxThunk后,在createStore时添加中间件
const store = createStore(reducer , applyMiddleware(reduxThunk));
(3) action中将以前的返回一个对象改为返回一个函数,并将dispatch作为参数传入函数中,然后在axios的回调函数中dispatch数据
export default function getCinemaListAction() {
return (dispatch) => {
//通过axios请求数据 填充影院列表
axios({
url: 'https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=9159391',
methods: 'get',
headers: {
'X-Client-Info': '{ "a": "3000", "ch": "1002", "v": "5.2.1", "e": "167004825688003879895041", "bc": "110100" }',
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res => {
dispatch({
type: 'change_list',
payload: res.data.data.cinemas
})
})
}
}
(4) 接下来就是常规操作,根据type找到reducer,在reducer中处理好数据
const cinemaListReducer = (prevState = {
list : []
}, action) => {
let newState = { ...prevState };
switch (action.type) {
case 'change_list':
newState.list = action.payload;
return newState;
default:
return prevState;
}
}
(5) 在view层中订阅,并渲染得到的数据,然后记得通过useEffect机制取消订阅。
const [cinemaList , setCinemaList] = useState(store.getState().cinemaListReducer.list)
useEffect(() => {
if(store.getState().cinemaListReducer.list.length === 0){
// 去后台取数据
store.dispatch(getCinemaListAction())
}else{
// store缓存中取数据
console.log('store cache')
}
// 订阅
let unsubscribe = store.subscribe(() =>{
setCinemaList(store.getState().cinemaListReducer.list)
})
// 通过useEffect的return也就是销毁机制取消订阅
return ()=>{
unsubscribe()
}
}, [])
4. redux-promise
其实和redux-thunk实际没区别,写法上的区别而已:
(1) npm i redux-promise
(2) store注册中间件
(3) action中将redux-thunk返回一个函数的写法改成返回一个Promise,同时也不用接收disptach参数来在回调中执行了,直接return即可。
注意对比和上面action中thunk代码的异同
export default async function getCinemaListAction() {
let list = await axios({
url: 'https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=9159391',
methods: 'get',
headers: {
'X-Client-Info': '{ "a": "3000", "ch": "1002", "v": "5.2.1", "e": "167004825688003879895041", "bc": "110100" }',
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res => {
return {
type: 'change_list',
payload: res.data.data.cinemas
}
})
return list;
}
5. react-redux
上面的redux都是和react无关的,而react-redux可以不用自己去管理麻烦的发布订阅,而是自动帮你实现。
还是用上面那个点进详情页则去掉tabbar栏的案例来说,来看看react-redux的写法:
(1) 首先npm i react-redux
(2) 用Provider包住
const root = ReactDom.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
)
(3) 来到view层组件,不用再像之前那样store.subscribe,只需使用connect包裹住要export的函数,并通过全局的state参数返回一个属性供被包裹的App组件使用
connect有两个参数:connect(给孩子传的属性,给孩子传的回调函数),这个案例中只用到第一个参数
const mapStatetoProps = (state) =>{
return{
isShow: state.tabbarReducer.show
}
}
export default connect(mapStatetoProps)(App)
(4) App中通过this.props就能接收到connect传过来的参数并使用
class App extends Component {
render() {
return (
<div>
<AppRouter>
{this.props.isShow && <TabBar></TabBar>}
</AppRouter>
</div>
)
}
}
(5) 然后再来到detail组件中进行修改,这里的connect就是用到了第二个参数,就不用自己写dispatch了,也是可以直接通过connect传过来的props调用对应的函数即可,直接上代码和之前的detail做对比
function Detail(props) {
let {show , hide} = props;
useEffect(() => {
hide()
return () => {
show()
}
}, [show , hide])
return (
<div>
{/* 动态路由方案1 */}
detail-{props.match.params.filmId}
{/* 动态路由方案2 */}
{/* detail-{props.location.query.filmId} */}
</div>
)
}
const mapDispatchToProps = {
show,
hide
}
export default connect(null , mapDispatchToProps)(Detail)
这是tabbarAction里的show和hide函数
function hide() {
return {
type: "hide_tabbar"
}
}
function show() {
return {
type: "show_tabbar"
}
}
export { show, hide }
最后来看一下上面那个异步cinema组件的改写,相较于改写同步组件能明显感觉到异步组件改写后架构和逻辑更为清晰(对比着看)
function Cinema(props) {
let {cinemaList , getCinemaListAction} = props
useEffect(() => {
if (cinemaList.length === 0) {
getCinemaListAction()
}
}, [cinemaList , getCinemaListAction])
const toCity = (props) => {
props.history.push(`/city`)
}
const toSearch = (props) => {
props.history.push(`/cinema/search`)
}
return (
<div style={{ overflow: 'hidden' }}>
<div onClick={() => toCity(props)} style={{ float: 'left' }}>{props.cityName}</div>
<div onClick={() => toSearch(props)} style={{ float: 'right' }}>search</div>
<ul>
{
props.cinemaList.map(item =>
<li key={item.name}>{item.name}</li>
)
}
</ul>
</div>
)
}
const mapStateToProps = (state) => {
return {
cinemaList: state.cinemaListReducer.list,
cityName: state.cityReducer.cityName
}
}
const mapDispatchToProps = {
getCinemaListAction
}
export default connect(mapStateToProps, mapDispatchToProps)(Cinema)
这个方法其实写起来还是有点麻烦,可以采用useSelector和useDispatch两个hooks来改进,这样就不用写connect高阶组件了。
import { useSelector, useDispatch } from 'react-redux';
function Cinema(props) {
const cinemaList = useSelector(state => state.cinemaListReducer.list);
const cityName = useSelector(state => state.cityReducer.cityName);
const dispatch = useDispatch();
const getCinemaListAction = () => dispatch(getCinemaListAction());
useEffect(() => {
if (cinemaList.length === 0) {
getCinemaListAction();
}
}, [cinemaList, getCinemaListAction]);
const toCity = (props) => {
props.history.push(`/city`);
}
const toSearch = (props) => {
props.history.push(`/cinema/search`);
}
return (
<div style={{ overflow: 'hidden' }}>
<div onClick={() => toCity(props)} style={{ float: 'left' }}>{cityName}</div>
<div onClick={() => toSearch(props)} style={{ float: 'right' }}>search</div>
<ul>
{
cinemaList.map(item =>
<li key={item.name}>{item.name}</li>
)
}
</ul>
</div>
);
}
export default Cinema;
6. 实现connect高阶组件
具体实现看注释
function myConnect(stateCb , dispatchObj){
// obj即stateCb执行后返回来的{a:1}
let obj = stateCb();
// Component即NotFound
return (Component)=>{
// Notfound就是返回一个函数式组件 props即Route传过来的参数,如history等
return (props)=>{
// 函数式组件内部返回的<div>
return(
<div>
{/* dispatchObj就是{fn}这个对象 */}
<Component {...obj} {...props} {...dispatchObj}/>
</div>
)
}
}
}
const fn = () => {
// do something
}
const mapStateToProps = (state) => {
return {
a: 1
}
}
const mapDispatchToProps = {
fn
}
export default myConnect(mapStateToProps , mapDispatchToProps)(NotFound)
7. redux-persist
github可以实现状态持久化记录在内存中的一个库 用起来比较麻烦 要用到时再查文档吧