晚餐时间带你手写redux/react-redux与常用中间件原理实现| 8月更文挑战

375 阅读8分钟

这是我参与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号前端切图师,感谢大家的阅读。此文纯属各方面学习之后个人理解,如果有错误和纰漏,感谢能给予指正。有帮助的话请❤️关注+点赞+收藏+评论+转发❤️