山寨一个 redux

框架

浏览数:475

2019-9-7

本文主要是说一说怎么通过自己的理解来实现一个“简易”的redux,目的不是copy一个redux出来,而是动手实现redux的核心功能,从而帮助我们理解和使用redux。

事实上,redux的核心功能代码并不多,其他大量的代码都是为了应对实际使用中“不按套路出牌”的情况,所以为了便于理解,我们只实现核心功能不处理特殊情况和异常。

最后,我们也会参看redux的源码,来理解和学习redux是如何实现的。

理解redux

首先,我们按照自己的理解来梳理一下redux。

  1. redux本质上就是一个容器,我们可以将应用中所有需要使用的状数据都存放在容器里面。 在js里,我们直接用一个对象来表示容器就行了。
  2. 通过对容器添加订阅函数,当容器的数据变更时,我们将接收到响应从而进行相应的处理。
  3. 通过向容器发送一个action对象,来通知容器对数据进行变更。
  4. 容器通过调用我们编写的reducer函数,得到最新的状态数据,替换掉旧的状态数据。

实现createStore

第一版实现了redux最主要的api:createStore。这个函数返回我们通常所说的store对象,我们要实现的store对象上包含3个方法。分别是

  1. getState。返回store当前的状态state。
  2. subscribe。用于向store添加订阅函数。
  3. dispatch。用于向store发送变更的指令action对象。

下面是代码

{
  const createStore = (reducer, prelodedState) => {
    let state = prelodedState;
    // 存放所有订阅函数
    const listeners = [];
    // 获取当前的state
    const getState = () => state;

    const dispatch = action => {
      // 将当前的state和action传入reducer,计算出变更后的state
      state = reducer(state, action);
      // state变更后,遍历执行所有订阅函数
      listeners.forEach(listener => listener());
    }

    const subscribe = listener => {
      listeners.push(listener);
      // 返回函数,用于移除订阅
      return () => {
        const i = listeners.indexOf(listener);
        listeners.splice(i, 1);
      }
    }

    // 创建store之后,初始化state
    dispatch({});

    return {
      getState,
      dispatch,
      subscribe,
    }
  }

  window.Redux = {
    createStore,
  }
}

下面是使用上述redux实现的计数器Counter例子
Redux_Counter

加入combineReducers

在计数器中,state只是一个单一的number数据类型,reducer也很简单,但是在实际应用中,state往往是一个复杂的对象,同时需要多个reducer来分别计算state下面对应的部分。
以todo为例:

它的state和reducer可能长这样

const state = {
    todo: [
      {
        text: '吃饭',
        completed: true,
      }, 
      {
        text: '睡觉',
        completed: false,
      }
    ],
    filter: 'FILTER_ALL',    // 显示所有,不管是否完成  
}

const reducer = (state = { todo: [], filter: 'FILTER_ALL' }, action) => {
  switch (action.type) {
    case 'TODO_ADD':
      return {
        ...state,
        todo: state.todo.concat({text: action.text, completed: false}),
      }
    // TODO_REMOVE ...
    // TODO_TOGGLE ...
    case 'FILTER_SET':
      return {
        todo: state.todo.slice(),
        filter: action.filter,
      }
    default:
      return state;
  }
}

我们可以看到state主要分为todo列表和过滤器两部分,在reducer中,两个部分的处理逻辑混合在了一起,处理TODO_ADD的逻辑还要通过解构state将filter一同返回,处理FILTER_SET的逻辑还要负责拷贝一个新的todo一同返回。
这样会导致处理不同state的代码混合在一起,增加了复杂性和代码冗余,所以有必要将reducer拆分为独立的函数,各自处理state中对应的数据。

首先试一下手动合并多个reducer

// reducer:处理state下的数组todo
const todo = (state = [], action) => {
  switch (action.type) {
    case 'TODO_ADD':
      return [...state, { text: action.text, completed: false }];
      // TODO_REMOVE TODO_TOGGLE
    default:
      return state;
  }
}
// reducer:处理state下的过滤器filter
const filter = (state = 'FILTER_ALL', action) => {
  switch (action.type) {
    case 'FILTER_SET':
      return action.filter;
    default:
      return state;
  }
}
// 手动合并reducer,将state下的数据拆开分别调用对应的处理函数,
// 最终组合成一个新的state返回
const reducer = (state = {}, action) => {
  return {
    todo: todo(state.todo, action),
    filter: filter(state.filter, action),
  }
}

下面我们自己实现一个combineReducers函数,用于合并多个reducer

const combineReducers = (reducers) => {
    return (state = {}, action) => {
      // 获取reducers的所有健值
      const keys = Object.keys(reducers);
      // 传入{}作为初始值(新的state)
      return keys.reduce((prevState, key) => {
        // 将key对应的旧的状态state[key]和action传入reducers中key对应的value处理函数
        // 计算出新的state
        prevState[key] = reducers[key](state[key], action);
        return prevState;
      }, {});
    }
}

下面是加入combineReducers函数后,实现的todo例子
Redux_Todo

加入扩展机制

如果仅仅只有上面所说的功能,肯定是满足不了实际的需求的。比如需要统一规范地处理异步任务或者需要对某个api进行扩展或定制。

redux提供了两种扩展机制:中间件、增强器。

中间件是对dispatch方法的扩展,目前为止,如果调用dispatch传入一个action对象,这个action会直接抵达store对象,进而执行reducer并调用订阅函数。中间件就是在dispatch之后 action到达store之前对action进行解析处理的机制,经过一个个中间件函数处理之后,再将action传给store。

增强器可以对整个store进行扩展,而不仅仅是dispatch方法。

所以中间件就是一种增强器,中间件是通过增强器实现的,因为对dispatch方法的扩展比较常见和实用,所以将插入中间件的机制单独实现为applyMiddleware方法。

所以我们先看增强器怎么实现,然后再看怎么实现中间件。

加入增强器

超市总喜欢把散装的产用塑料盘子和保鲜膜包装一下再出售,包装过后的产品 颜值、便携和身价都得到了增强。
redux里的增强器也类似于这种包装机制,如果想对store的getState方法进行增强,就将它包装成一个新的函数,只要保证最终还是会调用store本来的getState方法就行了。

下面是对getState的增强,getState每次被调用的时候都会打印一句话

// 增强getState的增强器
const getStateEnhancer = (store) => {
  const originalGetState = store.getState;
  store.getState = () => {
    console.log('----- getState is invoked -----');
    return originalGetState();
  }
}
// 创建store
const store = Redux.createStore(reducer, initialState);
// 增强store
getStateEnhancer(store);

这样虽然可以实现,但是太那啥了,后面我们会看看redux的源码是怎么实现的。

加入中间件

中间件是对dispatch方法的增强,也就是对dispatch方法进行包装,生成一个新的dispatch方法。

在新dispatch里面依次插入中间件函数,每个中间件都可以访问到getState、dispatch和action以及下一个中间件函数。

所以,在一个中间件内部通过解析action,中间件可以选择调用下一个中间件将action继续传递下去,也可以选择再次调用dispatch,让action重新在中间件中流转一遍。

最后一个中间件调用的下一个中间件函数指向包装之前的dispatch,这样action在经过中间件的处理之后,最终抵达store。

redux规定,中间件必须遵循如下所示的规范。

const middleware = ({dispatch, getState}) => next => action => {
    // do something
    next(action);
}

首先,中间件middleware必须是一个函数,这个函数会被注入一个对象作为参数,返回一个新的函数。新的函数的参数next代表下一个中间件,新函数再次返回一个函数,最后这个返回的函数才是中间件执行逻辑的地方,执行完以后调用next,把action传给下一个中间件,如果没有下一个中间件了,这个next就指向store原本的dispatch方法。

下面根据中间件的接口规范模拟实现的添加中间件的applyMiddleware方法

  /**
   * 组合函数,将多个函数组合为一个函数
   * 比如a、b、c三个函数 
   * 执行 compose(a, b, c) 返回的函数近似于 (...args) => a(b(c(...args)))
   * */ 
  const compose = (...funcs) => {
    if (funcs.length === 0) return f => f;

    if (funcs.length === 1) return funcs[0];

    return funcs.reduce((prevFunc, curFunc) => (...args) => prevFunc(curFunc(...args)));
  }

  /**
   *  注入中间件 
   *  @param {store} 需要注入中间件的store对象
   *  @param {...middlewares} 按顺序传入的中间件
   */
  const applyMiddleware = (store, ...middlewares) => {
    let dispatch;
    // 注入中间件的参数对象,里面的dispatch指向新的dispatch函数
    const injectApi = {
      dispatch: (...args) => dispatch(...args),
      getState: store.getState,
    }
    // 执行map之前,每个middleware大约长这样:({dispatch, getState}) => next => action => {};
    // 对每个中间件注入参数调用以后,大约长这样:next => action => {};
    const chain = middlewares.map(middleware => middleware(injectApi));
    // 得到新的dispatch方法
    store.dispatch = compose(...chain)(store.dispatch);
    dispatch = store.dispatch;
  }
    
    // 测试中间件,只打印一句话
    const middleware_1 = ({ dispatch, getState }) => next => action => {
      console.log('middleware_1');
      next(action);
    }
    const middleware_2 = ({ dispatch, getState }) => next => action => {
      console.log('middleware_2');
      next(action);
    }
    const middleware_3 = ({ dispatch, getState }) => next => action => {
      console.log('middleware_3');
      next(action);
    }
    
    // 使用式例
    // 创建store
    const store = Redux.createStore(reducer, initialState);
    // 注入中间件
    applyMiddleware(store, middleware_1, middleware_2, middleware_3);

对于applyMiddleware,我们重点看一下下面这句代码

store.dispatch = compose(...chain)(store.dispatch);

也就是多个中间件函数是怎么组合成一个函数,并且怎么和dispatch联系在一起的。

// 假如现在有上面所说的三个中间件 middleware_1、middleware_2 和 middleware_3
// 执行下面这句代码以后
// const chain = middlewares.map(middleware => middleware(injectApi));
// chain 大概长下面这样
chain = [
  next => action => { console.log('middleware_1'); next(action); }, // middleware_1
  next => action => { console.log('middleware_2'); next(action); }, // middleware_2
  next => action => { console.log('middleware_3'); next(action); }, // middleware_3
]

// 执行了 compose(...chain) 以后
// compose(...chain)返回的函数大概长下面这样
(...args) => {
  return ((...args) => {
    return middleware_1(middleware_2(...args))
  })(middleware_3(...args))
}

// 紧接着 (store.dispatch) 调用该返回函数的时候
// store.dispatch 传入 middleware_3,middleware_3变成下面这样
action => { console.log('middleware_3'); store.dispatch(action); }

// 变换后的middleware_3作为参数传入middleware_2,middleware_2变成下面这样
action => { console.log('middleware_2'); middleware_3(action); }

// 变换后的middleware_2作为参数传入middleware_1,middleware_1变成下面这样
action => { console.log('middleware_1'); middleware_2(action); }

// 所以 compose(...chain)(store.dispatch); 最终返回的函数是如下所示的middleware_1
action => { console.log('middleware_1'); middleware_2(action); }
// 然后在函数内部再调用middleware_2,middleware_2在内部再去调用middleware_3
// 这样就实现了中间件的顺序执行,并且最后一个中间件将调用 旧的dispatch函数

下面,我们在计数器Counter中添加 日志中间件 和 thunk中间件,看一下最终的效果
Redux_middleware_Counter

参看redux源码

combineReducers

这个函数里面需要关注的一点就是对于state是否变化的处理。

// 只保留了该方法的核心代码
function combineReducers(reducers) {
  ...
  return function combination(state = {}, action) {
    // 标志state是否改变
    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey
      // 对于前后两次同一个key对应的state值,采用浅比较的方式
      // 如果是同一个引用,就认为没有改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 有一个key对应的value改变了就返回新的state
    // 所有key对应的value都没改变才使用旧的state
    return hasChanged ? nextState : state
  }
}

combineReducers采用浅比较的方式判断是返回新的state还是旧的state,如果根state下的每个属性值前后两次都是同一个引用的话,就将返回旧的state。

这也是redux所说的不直接修改state的原因,因为像react-redux这样的绑定库也采用浅比较的方式来判断state是否变化,如果直接修改state会导致react-redux认为state没有变化,从而不会触发渲染。

增强器

先看看一个什么都不做的增强器的格式

const enhancer = createStore => (reducer, prelodedState, enhancer) => {
  const store = createStore(reducer, prelodedState, enhancer);
  // 增强store的代码
  return store;
}

redux的增强器

// 只保留了增强器相关的代码
// createStore可以接收3个参数
// 第一个参数永远是reducer,第二个和第三个是可选参数
// 第二个参数如果是函数就当作enhancer,否则作为state的初始状态
// 第三个参数如果有的话,必须是enhancer函数类型
function createStore(reducer, preloadedState, enhancer) {
  ...
  // 如果有增强器
  if (typeof enhancer !== 'undefined') {
    // 将自己传给enhancer,得到一个新的createStore函数
    // 然后再把剩余的两个参数传给新的createStore
    return enhancer(createStore)(reducer, preloadedState)
  }
  ...
  return {
    dispatch,
    getState,
    ...
  }
}

最后

redux还有很多方法实现这里并没有一一列举出来,如果有兴趣可以继续深入。
在看源码的时候,可以先把与核心功能无关的代码注释掉,这样看起来会轻松一些。

作者:July