从0到1了解 Redux 实现原理

在前文的文章中,我们讲到了 React 数据流 相关的知识。在本文中将介绍 redux 的实现原理。

在本文开始之前,有一些内容需要忘记:

  • redux 和 react 是没有关系的,它可以被用在任何框架中
  • connect 不属于 redux,它属于 react-redux,可以看该篇文章有过介绍
  • redux 就是一个状态管理器

下面的图是 redux 的工作流

简单的状态管理

所谓的状态其实就是数据,例如用户中的 name

js 复制代码
let state = {
  name: 'shuangxu',
};

// 使用状态
console.log(state.name);

// 更改状态
state.name = 'FBB';

上述代码中存在问题,当我们修改了状态之后无法通知到使用状态的函数,需要引入发布订阅模式来解决这个问题

js 复制代码
const state = {
  name: 'shuangxu',
};
const listeners = [];

const subscribe = (listener) => {
  listeners.push(listener);
};

const changeName = (name) => {
  state.name = name;
  listeners.forEach((listener) => {
    listener?.();
  });
};

subscribe(() => console.log(state.name));

changeName('FBB');
changeName('LuckyFBB');

在上述代码中,我们已经实现了更改变量能够通知到对应的监听函数。但是上述代码并不通用,需要将公共方法封装起来。

js 复制代码
const createStore = (initialState) => {
  let state = initialState;
  let listeners = [];

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter((fn) => fn !== listener);
    };
  };

  const changeState = (newState) => {
    state = { ...state, ...newState };
    listeners.forEach((listener) => {
      listener?.();
    });
  };

  const getState = () => state;

  return {
    subscribe,
    changeState,
    getState,
  };
};

// example
const { getState, changeState, subscribe } = createStore({
  name: 'shuangxu',
  age: 19,
});

subscribe(() => console.log(getState().name, getState().age));

changeState({ name: 'FBB' }); // FBB 19
changeState({ age: 26 }); // FBB 26

changeState({ sex: 'female' });

约束状态管理器

上述的实现能够更改状态和监听状态的改变。但是上述改变 state 的方式过于随便了,我们可以任意修改 state 中的数据,changeState({ sex: "female" }),即使 sex 不存在于 initialState 中,所以我们需要约束只能够修改 name/age 属性

通过一个 plan 函数来规定UPDATE_NAMEUPDATE_AGE方式更新对应属性

js 复制代码
const plan = (state, action) => {
  switch (action.type) {
    case 'UPDATE_NAME':
      return {
        ...state,
        name: action.name,
      };
    case 'UPDATE_AGE':
      return {
        ...state,
        age: action.age,
      };
    default:
      return state;
  }
};

更改一下 createStore 函数

js 复制代码
const createStore = (plan, initialState) => {
  let state = initialState;
  let listeners = [];

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter((fn) => fn !== listener);
    };
  };

  const changeState = (action) => {
    state = plan(state, action);
    listeners.forEach((listener) => {
      listener?.();
    });
  };

  const getState = () => state;

  return {
    subscribe,
    changeState,
    getState,
  };
};

const { getState, changeState, subscribe } = createStore(plan, {
  name: 'shuangxu',
  age: 19,
});

subscribe(() => console.log(getState().name, getState().age));

changeState({ type: 'UPDATE_NAME', name: 'FBB' }); // FBB 19
changeState({ type: 'UPDATE_AGE', name: '28' }); // FBB 28
changeState({ type: 'UPDATE_SEX', sex: 'female' }); // FBB 19

代码中的 plan 就是 redux 中的 reducer,changeState 就是 dispatch。

拆分 reducer

reducer 做的事情比较简单,接收 oldState,通过 action 更新 state。

但是实际项目中可能存在不同模块的 state,如果都把 state 的执行计划写在同一个 reducer 中庞大有复杂。

因此在常见的项目中会按模块拆分不同的 reducer,最后在一个函数中将 reducer 合并起来。

js 复制代码
const initialState = {
  user: { name: 'shuangxu', age: 19 },
  counter: { count: 1 },
};

// 对于上述 state 我们将其拆分为两个 reducer
const userReducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_NAME':
      return {
        ...state,
        name: action.name,
      };
    case 'UPDATE_AGE':
      return {
        ...state,
        age: action.age,
      };
    default:
      return state;
  }
};

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      };
    default:
      return state;
  }
};

// 整合 reducer
const combineReducers = (reducers) => {
  // 返回新的 reducer 函数
  return (state = {}, action) => {
    const newState = {};
    for (const key in reducers) {
      const reducer = reducers[key];
      const preStateForKey = state[key];
      const nextStateForKey = reducer(preStateForKey, action);
      newState[key] = nextStateForKey;
    }
    return newState;
  };
};

代码跑起来!!

js 复制代码
const reducers = combineReducers({
  counter: counterReducer,
  user: userReducer,
});

const store = createStore(reducers, initialState);
store.subscribe(() => {
  const state = store.getState();
  console.log(state.counter.count, state.user.name, state.user.age);
});
store.dispatch({ type: 'UPDATE_NAME', name: 'FBB' }); // 1 FBB 19
store.dispatch({ type: 'UPDATE_AGE', age: '28' }); // 1 FBB 28
store.dispatch({ type: 'INCREMENT' }); // 2 FBB 28
store.dispatch({ type: 'DECREMENT' }); // 1 FBB 28

拆分 state

在上一节的代码中,我们 state 还是定义在一起的,会造成 state 树很庞大,在项目中使用的时候我们都在 reducer 中定义好 initialState 的。

在使用 createStore 的时候,我们可以不传入 initialState,直接使用store = createStore(reducers)。因此我们要对这种情况作处理。

拆分 state 和 reducer 写在一起。

js 复制代码
const initialUserState = { name: 'shuangxu', age: 19 };

const userReducer = (state = initialUserState, action) => {
  switch (action.type) {
    case 'UPDATE_NAME':
      return {
        ...state,
        name: action.name,
      };
    case 'UPDATE_AGE':
      return {
        ...state,
        age: action.age,
      };
    default:
      return state;
  }
};

const initialCounterState = { count: 1 };

const counterReducer = (state = initialCounterState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      };
    default:
      return state;
  }
};

更改 createStore 函数,可以自动获取到每一个 reducer 的 initialState

js 复制代码
const createStore = (reducer, initialState = {}) => {
  let state = initialState;
  let listeners = [];

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter((fn) => fn !== listener);
    };
  };

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => {
      listener?.();
    });
  };

  const getState = () => state;

  // 仅仅用于获取初始值
  dispatch({ type: Symbol() });

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

dispatch({ type: Symbol() })代码能够实现如下效果:

  • createStore 的时候,一个不匹配任何 type 的 action,来触发state = reducer(state, action)
  • 每个 reducer 都会进到 default 项,返回 initialState

总结

通过上述的几个步骤,我们已经实现了一个 redux,完全和 react 没有关系,只是提供了一个数据处理中心。

  • createStore

    创建 store 对象,包含 getState/dispatch/subscribe 等方法,能够获取 state/更改数据/监听数据的改变

  • reducer

    一个计划函数,接收旧的 state 和 action 返回一个新的 state

  • combineReducers

    多 reducer 合并成一个 reducer

相关推荐
不爱说话郭德纲3 小时前
Stylus、Less 和 Sass 的使用与区别
前端·css·面试·less·sass·stylus
ThisIsClark4 小时前
【后端面试总结】MySQL面试总结
mysql·面试·职场和发展
ThisIsClark4 小时前
【后端面试总结】MySQL索引
mysql·面试·职场和发展
就是有点傻5 小时前
C#中面试的常见问题006
开发语言·面试·c#·wpf
前端郭德纲6 小时前
React Native 性能调试指南
javascript·react native·react.js
米奇妙妙wuu6 小时前
react实现模拟chatGPT问答页
前端·react.js·chatgpt·前端框架
loey_ln13 小时前
观察者模式和发布订阅模式
javascript·观察者模式·react.js
奇妙之二进制17 小时前
2025年春招修订版《C/C++笔面试系列》(1) C语言经典笔面试题(上)
c语言·c++·面试
窗边的anini17 小时前
在React中使用SVG的几种方式
前端·react.js·svg
兑生18 小时前
力扣面试150 填充每个节点的下一个右侧节点指针 II BFS 逐层构建法
leetcode·面试·宽度优先