这是我参与8月更文挑战的第二天,活动详情查看:8月更文挑战
起始
在使用react进行开发项目时,总是需要一个状态管理工具.在vue中有vuex,而在react中,相信很多公司都会选择redux,而redux并非为了react而设计.这样在我们使用的过程中就没有那么方便,所以就有了react-redux,dva之类的解决方案.
使用dva的时候会有一套相对完整的解决方案,但是并非是所有项目都会使用dva,使用react-redux,我们就需要为redux加入各种中间件,比如redux-thunk,redux-promise......,这篇文章主要实现redux,react-redux,redux-thunk,redux-promise.这些库的常用api.在此之前需要一些储备知识,react和文章中实现的api平常使用时必然要掌握的,除此之外还需要掌握函数式编程的只是,如果有模糊的地方可以看这一篇文章.
redux实现
在实现redux之前首先需要来看一下我们平常的使用,来确定实现的需求.
import {createStore, applyMiddleware, combineReducers} from '../redux/index'
export function countReducer(state = 0, action) {
// console.log('countReducer', state, action);
switch (action.type) {
case "ADD":
return state + (action.payload || 1);
case "MINUS":
return state - (action.payload || 1);
default:
return state;
}
}
const store = createStore(combineReducers({counter: countReducer}), applyMiddleware(thunk, redux_promise));
export default store;
而在页面中 我们使用getState来获取状态,使用dispatch来改变状态,使用subscribe来订阅改变之后的执行.
对于redux我们首先需要实现createStore, 而对于createStore导出的实例需要有getState,diapatch和subscribe,对于加强store我们还需要applyMiddleWare和combineReducers
createState
循序渐进先来实现createStore让程序跑起来,这createStore中,主要做保存和暴露state,reducer规则的调用者dispatch,和订阅更新的subscribe.
const createStore = (reducer) => {
//保存状态的变量
let currentState
//这里作为一个依赖收集,变化时遍历执行数组内的函数来通知页面更新
let currentListens = []
//getState实现
const getState = () => {
return currentState
}
//dispatch函数实现
const dispatch = (action) => {
//在每次使用dispatch时,其实时调用我们定义的规则得到输出
currentState = reducer(currentState, action)
// state发生变化通知组件
currentListens.forEach(fn => fn())
}
//订阅实现
const subscribe = (callback) => {
currentListens.push(callback)
let length = currentListens.length - 1;
//在这里记录当前加入的下标,return一个函数出去用作组件卸载时解除订阅.
return () => {
currentListens.splice(length, 1);
}
}
// 因为此时getState拿到为null所以需要手动触发,派发初始值
dispatch({type:"REDUX/XXXXXXXXXX"})
return {
getState,
dispatch,
subscribe
}
};
export default createStore
写到这里,就可以得到一个没有任何加持的redux,可以正常使用,页面通过最原始的getState,diapatch和subscribe可以正常使用,此时并不能添加中间件.
中间件applyMiddleware
在使用diapstch时,只能传入一个对象,来匹配我们所定义的reducer,所以中间件就是对dispatch的增强,让他可以处理一个函数或者promise的情况,中间的效果也比较单一,比如redux-thun只处理函数,redux-promise只处理promise,而且要做到每一个中间件都不会互相污染.我们就要用到函数时编程的聚合和柯里化.
首先需要一个聚合函数,在聚合函数中将每一项函数的返回值作为下一个函数的参数进行,返回一个待执行的函数.如果无法理解这个函数,写几个demo进行执行和log,就会发现其中的妙用.(在上方链接中称为组合函数)
function compose(...args) {
if(args.length === 0) {
return (arg) => arg
}
if(args.length === 1) {
return args[0]
}
return args.reduce((a, b) => {
return (...args) => a(b(...args))
})
}
再此之前要先重新createStore函数,因为在使用中,中间件接受参数,作为createStore的第二个参数传入,所以在createStore函数的最上方添加
const createStore = (reducer, enhancer) => {
//这里首先判断是否传入中间件,如果没有我们照常执行即可
if(enhancer) {
//在中间件函数中需要用到原本来createStore函数来获取正常的store,dispatch....,和reducer
return enhancer(createStore)(reducer)
}
......
}
在applyMiddleWare中接收到的中间件可能有多个,所以需要让他们单独处理,并且需要他们返回处理后的结果交由下一个中间件处理,并且不会相互影响.
const applyMiddleware = (...middlewares) => {
//middlewares就是我们在createStore中传入的各个中间件
return createStore => reducer => {
// 拿到store(store中有getState,dispatch,subscribe)
const store = createStore(reducer);
let dispatch = store.dispatch;
const midApi = {
getState: store.getState,
// 因为中间件不止一个,如果传入dispatch的话会可能会互相干扰
// 作为函数传入会使dispatch之后是一个经过层层封装的dispatch,各个中间件不会相互影响
dispatch: (action) => {
return dispatch(action)
}
}
const middlewareChain = middlewares.map(middleware => middleware(midApi))
// 重新赋值一个函数 因为中间件不止一个 所以需要像洋葱模型一样执行 在最后一个中间件收到的参数将是传入的store.dispatch
dispatch = compose(...middlewareChain)(store.dispatch)
// 使dispatch变成经过处理的聚合(组合)函数.
return {
...store,
dispatch
}
}
}
对于中间件编写比较简单,主要时对于函数式编程的理解,可以使用官方与本文中编写的中间件和redux项目混用.
redux-thunk
export default function thunk({getState, dispatch}) {
return next => action => {
console.log('thunk', next, action);
if(typeof action === 'function') {
//在这里调用action之后 actions内部会调用dispatch 所以会导致所有的中间件重新执行
return action(dispatch, getState)
}
return next(action);
}
}
redux-promise
export default function redux_promise({getState, dispatch}) {
// 使用compose聚合函数之后 每一次会将下一个要执行的中间件操作通过参数传入
return next => action => {
console.log('promise', next, action);
//在这里调用action.then之后 actions内部会调用dispatch 所以会导致所有的中间件重新执行
return isPromise(action)? action.then(dispatch): next(action)
}
}
function isPromise(action) {
return action instanceof Promise;
}
在上方执行next()代表中间件无处理,前往下一个中间件,如果有处理,则会调用diapatch改变action,所以需要重新遍历所有中间件再次处理.
combineReducers
此外在使用的时候也经常会用到combineReducers,突然忘记的同学可以百度一下,相信会一瞬间记忆涌上脑海.
这个api比较简单,相信一看就懂,也可以正常使用.
export default function combineReducers(reducers) {
return (state = {}, action) => {
let nextState = {}
let hasChanged = false
Object.keys(reducers).forEach(item => {
let reducer = reducers[item]
nextState[item] = reducer(state[item], action)
hasChanged = hasChanged || nextState[item] !== state[item]
})
return nextState;
}
}
写好了redux和他的一些插件,虽然可以使用,但是使用起来还是很麻烦,再写一个react-redux来帮助我们使用redux.
react-redux
和之前一样,写之前先分析我们需要实现那些东西.首先我们在使用之前先用react-redux中的provider组件包裹我们的跟组件以保证我们的组件可以使用到状态.
在类组件中使用会用到connect高阶组件,会接受mapStateToPrpos和mapDispatchToProps,来嵌入到props上.
在函数组件中会使用useSelector来获取状态和useDispatch来获取修改状态的函数.
Provider
首先实现来实现Provider,接受一个store参数,store就是使用createStore创建的状态,在Provider内部使用Context来管理状态.
// 通过context传递store
const Context = createContext()
export const Provider = ({ store, children }) => {
return (
<Context.Provider store={store} >
{children}
</Context.Provider>
)
}
connect
高阶函数,接受两次参数,第一次是mapStateToPrpos和mapDispatchToProps,第二次接受的是一个组件,将mapStateToPrpos和mapDispatchToProps中的返回的数据通过props传递给组件之后将组件返回.
export const bindActionCreators = (creators, dispatch) => {
let obj = {}
// 核心逻辑
for (const key in creators) {
obj[key] = bindActionCreator(creators[key], dispatch)
}
return obj;
}
const bindActionCreator = (creator, dispatch) => {
//返回一个函数 将参数传递给真正的dispatch进行执行
return (...args) => dispatch(creator(args))
}
// 模拟类组件中的forceUpdate 官网推荐 每次调用第二个函数都会执行定义的第一个参数(规则)
const useForceUpdate = () => {
const [, forceUpdate] = useReducer(x => x + 1, 0)
return forceUpdate;
}
export const connect = (mapStateToProps, mapDispatchToProps) => (Cmp) => props => {
//通过store.subscribe传入在每次store更新时调用刷新
const forceUpdate = useForceUpdate()
const store = useContext(Context)
const { getState, dispatch, subscribe } = store;
//获取需要的状态
const stateProps = mapStateToProps(getState())
// 获取mapDispatchToProps中返回的的数据
// mapDispatchToProps有两种使用方式 函数或者对象 函数时处理比较简单,将dispatch传入即可
// 在对象是需要将对象拿出来每一项用dispatch包裹返回
let dispatchProps = {}
if (typeof mapDispatchToProps === 'object') {
dispatchProps = {
dispatch,
...bindActionCreators(mapDispatchToProps, dispatch)
}
} else if (typeof mapDispatchToProps === 'function') {
dispatchProps = {
dispatch,
...mapDispatchToProps(dispatch)
}
}
//订阅更新
useLayoutEffect(() => {
const unSubscribe = store.subscribe(() => {
forceUpdate()
})
return () => {
//组件卸载时取消订阅
if (unSubscribe) {
unSubscribe()
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [store])
//将接受组件propr注入之后输出
return (
<Cmp {...props} {...stateProps} {...dispatchProps} />
)
}
这样connect就写完了,接下来还有最后两个函数useSelector和useDispatch.
useSelector/useDispatch
export const useDispatch = () => {
const store = useContext(Context);
//只需要将store中的dispatch返回即可
return store.dispatch;
}
export const useSelector = (selector) => {
const forceUpdate = useForceUpdate()
const store = useContext(Context)
const {getState} = store
//订阅更新 这里实现比较简单,组件内多次使用useSelecot会出现重复刷新问题
useLayoutEffect(() => {
const unSubscribe = store.subscribe(() => {
forceUpdate()
})
return () => {
if (unSubscribe) {
unSubscribe()
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [store])
//将store作为参数吧传入的函数执行,得到状态进行返回
const state = selector(getState())
return state;
}
到这里就完成了实现,经过测试可以和官方库混用,就此告一段落,希望对你有所帮助。源码中的实现更加精彩,等待你的探索,加油.
最后
我是007号前端切图师,感谢大家的阅读。此文纯属各方面学习之后个人理解,如果有错误和纰漏,感谢能给予指正。有帮助的话请❤️关注+点赞+收藏+评论+转发❤️