redux简单实现(包含redux中核心api)

1,120 阅读6分钟

我们先来看一下redux中暴露的api;

核心方法    ( [ ] 代表参数可选 )

  • createStore(reducer, [preloadedState], [enhancer])
  • compose(...functions)
  • applyMiddleware(...middlewares)
  • combineReducers(reducers)
  • bindActionCreators(actionCreators, dispatch)

Store API

  • getState()
  • dispatch(action)
  • subscribe(listener)
  • getReducer()
  • replaceReducer(nextReducer)

一:createStore(reducer, [preloadedState], [enhancer])

redux的设计基于flux思想,数据流为单向,改变state的唯一途径就是通过dipatch派发action

  • 基于发布订阅模式,实现下代码
const createStore = (reducer, preloadedState, enhancer)=>{
    /* 处理下 createStore(reducer, enhancer) 调用情况*/
    if(typeof preloadedState === "function" && !enhancer){
        enhancer = preloadedState;
        preloadedState = undefined;
    }
    /* 存在中间件传参 */
    if(enhancer && typeof enhancer === "function"){
        /* 执行中间件逻辑,增强dispatch,返回新的store */
        return enhancer(createStore)(reducer, preloadedState)
    }

    let state = preloadedState;
    let listeners = [];

    const getState = ()=> state;

    const dispatch=(action)=>{
        state = reducer(state,action);
         /* 事件派发 */
        listeners.forEach((fn)=>{
            typeof fn === "function" && fn();
        });
       return action
    }

    const subscribe = (listener)=>{
        /* 事件订阅 */
        listeners.push(listener); 
        /* 返回取消订阅函数 */
        return ()=>{
            listeners = listeners.filter(fn=> fn !== listener);
        }
    }

    const replaceReducer = (nextReducer)=>{
        if(typeof nextReducer === "function"){
            reducer = nextReducer;
            dispatch({})
        }
    }
    /* 手动初始state  */
    dispatch({}); 

    return {
        getState,
        dispatch,
        subscribe,
        replaceReducer
    }
}

上面的代码写完了,但需要再抠一些细节, 考虑下下面两个问题:

  1. dispatch方法使用时:代码state = reducer(state,action);如果reducer调用中又存在dispatch派发怎么办?
  2. dispatch中遍历执行fn(),如果fn执行时增加了新的订阅subscribe,或者取消了某个订阅又怎么办?

下面看下真实的redux源码中是怎么处理的

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S>,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function.'
    )
  }

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(
      reducer,
      preloadedState as PreloadedState<S>
    ) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer
  let currentState = preloadedState as S
  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  let isDispatching = false

  /**
   * This makes a shallow copy of currentListeners so we can use
   * nextListeners as a temporary list while dispatching.
   *
   * This prevents any bugs around consumers calling
   * subscribe/unsubscribe in the middle of a dispatch.
   */
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  /**
   * Reads the state tree managed by the store.
   *
   * @returns The current state tree of your application.
   */
  function getState(): S {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState as S
  }

  /**
   * Adds a change listener. It will be called any time an action is dispatched,
   * and some part of the state tree may potentially have changed. You may then
   * call `getState()` to read the current state tree inside the callback.
   *
   * You may call `dispatch()` from a change listener, with the following
   * caveats:
   *
   * 1. The subscriptions are snapshotted just before every `dispatch()` call.
   * If you subscribe or unsubscribe while the listeners are being invoked, this
   * will not have any effect on the `dispatch()` that is currently in progress.
   * However, the next `dispatch()` call, whether nested or not, will use a more
   * recent snapshot of the subscription list.
   *
   * 2. The listener should not expect to see all state changes, as the state
   * might have been updated multiple times during a nested `dispatch()` before
   * the listener is called. It is, however, guaranteed that all subscribers
   * registered before the `dispatch()` started will be called with the latest
   * state by the time it exits.
   *
   * @param listener A callback to be invoked on every dispatch.
   * @returns A function to remove this change listener.
   */
  function subscribe(listener: () => void) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api/store#subscribelistener for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api/store#subscribelistener for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   *
   * The `reducer` function, used to create the store, will be called with the
   * current state tree and the given `action`. Its return value will
   * be considered the **next** state of the tree, and the change listeners
   * will be notified.
   *
   * The base implementation only supports plain object actions. If you want to
   * dispatch a Promise, an Observable, a thunk, or something else, you need to
   * wrap your store creating function into the corresponding middleware. For
   * example, see the documentation for the `redux-thunk` package. Even the
   * middleware will eventually dispatch plain object actions using this method.
   *
   * @param action A plain object representing “what changed”. It is
   * a good idea to keep actions serializable so you can record and replay user
   * sessions, or use the time travelling `redux-devtools`. An action must have
   * a `type` property which may not be `undefined`. It is a good idea to use
   * string constants for action types.
   *
   * @returns For convenience, the same action object you dispatched.
   *
   * Note that, if you use a custom middleware, it may wrap `dispatch()` to
   * return something else (for example, a Promise you can await).
   */
  function dispatch(action: A) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param nextReducer The reducer for the store to use instead.
   * @returns The same store instance with a new reducer in place.
   */
  function replaceReducer<NewState, NewActions extends A>(
    nextReducer: Reducer<NewState, NewActions>
  ): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    // TODO: do this more elegantly
    ;((currentReducer as unknown) as Reducer<
      NewState,
      NewActions
    >) = nextReducer

    // This action has a similar effect to ActionTypes.INIT.
    // Any reducers that existed in both the new and old rootReducer
    // will receive the previous state. This effectively populates
    // the new state tree with any relevant data from the old one.
    dispatch({ type: ActionTypes.REPLACE } as A)
    // change the type of the store by casting it to the new store
    return (store as unknown) as Store<
      ExtendState<NewState, StateExt>,
      NewActions,
      StateExt,
      Ext
    > &
      Ext
  }

  /**
   * Interoperability point for observable/reactive libraries.
   * @returns A minimal observable of state changes.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * The minimal observable subscription method.
       * @param observer Any object that can be used as an observer.
       * The observer object should have a `next` method.
       * @returns An object with an `unsubscribe` method that can
       * be used to unsubscribe the observable from the store, and prevent further
       * emission of values from the observable.
       */
      subscribe(observer: unknown) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          const observerAsObserver = observer as Observer<S>
          if (observerAsObserver.next) {
            observerAsObserver.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  dispatch({ type: ActionTypes.INIT } as A)

  const store = ({
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  } as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
  return store
}

上面源码中可以看到:

  1. 对于问题1:增加isDispatching字段控制,简称:加锁
  • 代码片段(只截取核心部分,其他地方也有用到isDispatching)
 if (isDispatching) {
    throw new Error('Reducers may not dispatch actions.')
}
 try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
}
  1. 对于问题2:增加了nextListeners字段
  • 代码片段
/* 每次改变nextListeners前先进行拷贝 */
function ensureCanMutateNextListeners() {
  if (nextListeners === currentListeners) {
    nextListeners = currentListeners.slice()
  }
}

/* subscribe */
ensureCanMutateNextListeners()
nextListeners.push(listener)

/* unsubscribe */
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)

/* 真正调用时 */
const listeners = (currentListeners = nextListeners);
//  就算listeners中存在方法,执行时进行订阅与取消订阅,我都用原来的listeners

额外: 看看下面的代码分别输出一样嘛

  • 例1
var list = [0,1,2];
list.forEach((item)=>{
  list.push(3);
  console.log(item)
});
  • 例2
var list = [0,1,2];
for(let i=0;i<list.length;i++){
  if(i===0){
    list.push(3)
  }
  console.log(list[i])
}

可以看到,有意思的现象,forEach中其实存在一个闭包function环境,
所以使用listeners.forEach(fn=> {fn()})
一定程度上避免了问题2的发生

二:compose(...functions)

compose是什么函数,compose可以理解为一个柯里化过程,将函数层层执行并最终返回

  • 从右至左调用,比如: compose(f, g, h) 将会返回一个新函数: (...args) => f(g(h(...args))).
  • 基于数组reduce方法实现下代码
const compose =(...funcs)=>{
  if(funcs.length === 0){
    return (arg)=> arg
  }
  if(funcs.length === 1){
    return funcs[0]
  }
  return funcs.reduce((a,b)=> (...args)=> a(b(...args)));   // 这句比较难理解,多读几遍
}

三:applyMiddleware(...middlewares)

最核心的中间件代码,看之前,我们先来看一端redux-chunk中间件源码:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => {
    return next=>{
      return action => {
        if (typeof action === 'function') {
           return action(dispatch, getState, extraArgument);
        }
        return next(action);
      }
    }
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

上面的redux-thunk 源码可以看到需要顶层提供dispatch, getState参数

  • applyMiddleware实现
const applyMiddleware=(...middlewares)=>{
/* 下面是createStore中enhancer */
  return (createStore)=>{
    return (reducer, preloadedState)=>{
      const store = createStore(reducer, preloadedState);
      let dispatch =  ()=>{}
      const middlewareApi = {
        getState: store.getState,
        dispatch: (...args)=> { dispatch(...args) } 
      }
      const chain = middlewares.map(middleware => middleware(middlewareApi));
      dispatch = compose(...chain)(store.dispatch);  // dispatch增强
      return{
        ...store,
        dispatch
      }
    }
  }
}
  • 思考:上面定义时dipatch只是()=> {},作为形参传入到redux-thunk中可以吗?
  • 提示

四:combineReducers(reducers)

将{} 对象形式合成最终的大reducer函数形式

const combineReducers(reducers)=>{
  return (state, action)=>{
    return Object.keys(reducers).reduce((now,item)=>{
      now[item] = reducers[item](state[item], action);
      return now;
    }, {})
  }
}

五:bindActionCreators(actionCreators, dispatch)

一般用不到,在react-redux源码中mapDispatchToProps可以为对象时,使用bindActionCreators进行加工处理

const bindActionCreator = (actionCreator, dispatch)=>{
  return (...args)=>{
    return dispatch(actionCreator(...args))
  }
}

const bindActionCreators=(actionCreators,dispatch)=>{
  return Object.keys(actions).reduce((now,item)=>{
    now[item]= bindActionCreator(actionCreators[item], dispatch)
    // 等同 now[item]= (...args)=> dispatch(actionCreators[item](...args))
    return now
  },{})
}