渐进式手写Redux

您好, 如果喜欢我的文章或者想上岸大厂,可以关注公众号「量子前端」,将不定期关注推送前端好文、分享就业资料秘籍,也希望有机会一对一帮助你实现梦想

我们还是先回顾一下Redux的基本使用,看一下下面这个简单的例子,包含了Redux中的store、action和reducer三大概念:

typescript 复制代码
import { createStore } from 'redux';

const initState = {
  milk: 0,
};

function reducer(state = initState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return { ...state, milk: state.milk + action.count };
    case 'TAKE_MILK':
      return { ...state, milk: state.milk - action.count };
    default:
      return state;
  }
}

let store = createStore(reducer);

// subscribe其实就是订阅store的变化,一旦store发生了变化,传入的回调函数就会被调用
// 如果是结合页面更新,更新的操作就是在这里执行
store.subscribe(() => console.log(store.getState()));

// 将action发出去要用dispatch
store.dispatch({ type: 'PUT_MILK' }); // milk: 1
store.dispatch({ type: 'PUT_MILK' }); // milk: 2
store.dispatch({ type: 'TAKE_MILK' }); // milk: 1

例子虽然短小,但是包含了Redux的全部核心功能,我们的手写目标就是替换引入createStore的redux库,要替换它,我们需要知道它都有些什么东西。目前看只有一个createStore API,接收reducer函数作为参数,返回一个store,而所有的功能都在store上。

store中包含了什么能力呢?

  • subscribe:订阅state变化,用于触发页面更新;

  • dispatch:发出action的方法,每次dispatch action都会执行reducer生成新的state,然后去执行subscribe;

  • getState:简单方法,返回当前的state;

接下来我们开始手写一个基本雏形:

typescript 复制代码
const createStore = (reducer: any) => {
  let state: any;
  let listeners: Array<() => void> = [];

  /**
   * @description: 订阅,添加订阅器
   * @param {function} fn
   */
  const subscribe = (fn: () => void) => {
    listeners.push(fn);
  };

  /**
   * @description: 执行变更,触发所有回调
   * @param {*} action
   */
  const dispatch = (action: any) => {
    state = reducer(state, action);
    listeners.forEach((fn) => fn());
  };

  /**
   * @description: 获取当前state
   * @return {*}
   */
  const getState = () => {
    return state;
  };

  return {
    subscribe,
    dispatch,
    getState,
  };
};

export createStore;

代替一下redux来源库:

typescript 复制代码
import { createStore } from './redux';

跑一下结果:

符合预期。

初步实现了,和redux的预期效果一致。可喜可贺,接下里我们继续增强Redux。当store庞大的时候,有多个模块化reducer的时候,我们需要使用combineReducers API来合并reducer再消费,就像这样:

typescript 复制代码
const reducer = combineReducers({milk: milkReducer, rice: riceReducer});

我们接下来实现这个API,首先分析入参,就是输入多个reducer,输出一个reducer。

typescript 复制代码
const combineReducers = (reducerMap: { [key in string]: any }) => {
  const reducerKeys = Object.keys(reducerMap); // 拿到所有reducer map
  const reducer = (state = {}, action) => {
    const newState = {};

    for (let i = 0; i < reducerKeys.length; i++) {
      // reducerMap里面每个键的值都是一个reducer,我们把它拿出来运行下就可以得到对应键新的state值
      // 然后将所有reducer返回的state按照参数里面的key组装好
      // 最后再返回组装好的newState就行
      const key = reducerKeys[i];
      const currentReducer = reducerMap[key];
      const prevState = state[key];
      newState[key] = currentReducer(prevState, action);
    }

    return newState;
  };

  return reducer;
};

我们改一下测试用例:

typescript 复制代码
// 使用combineReducers组合两个reducer
const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer});

let store = createStore(reducer);

store.subscribe(() => console.log(store.getState()));

// 操作
store.dispatch({ type: 'PUT_MILK', count: 1 }); // milk: 1
store.dispatch({ type: 'PUT_MILK', count: 1 }); // milk: 2
store.dispatch({ type: 'TAKE_MILK', count: 1 }); // milk: 1

// 操作大米的action
store.dispatch({ type: 'PUT_RICE', count: 1 }); // rice: 1
store.dispatch({ type: 'PUT_RICE', count: 1 }); // rice: 2
store.dispatch({ type: 'TAKE_RICE', count: 1 }); // rice: 1

跑一下结果,如图:

结果没问题,再次可喜可贺,而官方的实现方式也是这样的,只是在中间加入了更多的异常处理。最后我们再实现一下applyMiddleware,它是Redux中很重要的概念,就是redux中间件。我们可以在状态变更中插入变更日志,就像这样:

typescript 复制代码
function logger(store) {
  return function (next) {
    return function (action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result;
    };
  };
}

// 在createStore的时候将applyMiddleware作为第二个参数传进去
const store = createStore(reducer, applyMiddleware(logger));

可以看到,中间件在createStore中作为第二个参数传入,官方称为enhancer,顾名思义是一个增强器,用于增强辅助store能力。因此我们在手写就需要改造一开始的createStore方法了,就像这样:

typescript 复制代码
const createStore = (reducer, enhancer) = () => {};

并且加入判断:

typescript 复制代码
const createStore = ((reducer, enhancer) = () => {
  if (enhancer && typeof enhancer === 'function') {
    const newCreateStore = enhancer(createStore);
    const newStore = newCreateStore(reducer);
    return newStore;
  }

  // 执行之前的逻辑
});

如果传入了enhancer并且是个函数,我们就走新的逻辑,否则就走之前实现的逻辑。

结合之前logger中间件的结构可以知道applyMiddleware包了两层函数,因此我们也把applyMiddleware的结构先设计出来。

typescript 复制代码
function applyMiddleware(middleware) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      // ...
    }
  }
}

那这里面做了什么呢?中间件本质是加强dispatch,我们拿到加强后的dispatch,最后再把dispatch和原始store返回即可。

typescript 复制代码
function applyMiddleware(middleware) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      const store = createStore(reducer);
      // dispatch加强函数
      const func = middleware(store);
      // 解构出原始的dispatch
      const { dispatch } = store;
      // 得到增强版的dispatch
      const newDispatch = func(dispatch);
      return { ...store, dispatch: newDispatch };
    }
    return newCreateStore;
  }
  return enhancer;
}

三行代码就实现了~再跑一下测试用例,拿到了对应logger中间件的结构,如图:

那如何实现多个中间件呢?入参的数量就是无限的了,因此我们需要一个组合函数,把所有函数串联起来:

typescript 复制代码
const compose = (f1, f2, f3, f4) => {
  return (args) => f1(f2(f3(f4(args)))
}

在middleware中,将多个中间件不断地升级dispatch,就像这样:

typescript 复制代码
function applyMiddleware(...middlewares) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      const store = createStore(reducer);
      // dispatch加强函数
      const chain = middlewares.map((middleware) => middleware(store));
      // 解构出原始的dispatch
      const { dispatch } = store;
      // 用compose得到一个组合了所有newDispatch的函数
      const newDispatchGen = compose(...chain);
      // 获得加强版dispatch
      const newDispatch = newDispatchGen(dispatch);
      return { ...store, dispatch: newDispatch };
    }
    return newCreateStore;
  }
  return enhancer;
}

最后再加个logger来测试下:

typescript 复制代码
function logger2(store) {
  return function (next) {
    return function (action) {
      let result = next(action);
      console.log('logger2');
      return result;
    };
  };
}

let store = createStore(reducer, applyMiddleware(logger, logger2));

新的logger也打印出来了,可喜可贺。因此我们也知道Redux中间件包裹了几层函数了:

  • 第一层,store参数;

  • 第二层,dispatch参数,可以是多个;

  • 第三层,最终返回值,增强过的dispatch,最终逻辑;

如果喜欢我的文章或者想上岸大厂,可以关注公众号「量子前端」,将不定期关注推送前端好文、分享就业资料秘籍,也希望有机会一对一帮助你实现梦想。

相关推荐
喵叔哟30 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django