山寨一个-redux

24次阅读

共计 8232 个字符,预计需要花费 21 分钟才能阅读完成。

本文主要是说一说怎么通过自己的理解来实现一个“简易”的 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 还有很多方法实现这里并没有一一列举出来,如果有兴趣可以继续深入。
在看源码的时候,可以先把与核心功能无关的代码注释掉,这样看起来会轻松一些。

正文完
 0