从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

相关推荐
Neituijunsir1 小时前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
TonyH20022 小时前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
小飞猪Jay2 小时前
面试速通宝典——10
linux·服务器·c++·面试
数据分析螺丝钉3 小时前
力扣第240题“搜索二维矩阵 II”
经验分享·python·算法·leetcode·面试
无理 Java3 小时前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc
掘金泥石流3 小时前
React v19 的 React Complier 是如何优化 React 组件的,看 AI 是如何回答的
javascript·人工智能·react.js
鱼跃鹰飞4 小时前
Leecode热题100-295.数据流中的中位数
java·服务器·开发语言·前端·算法·leetcode·面试
TANGLONG2224 小时前
【C语言】数据在内存中的存储(万字解析)
java·c语言·c++·python·考研·面试·蓝桥杯
狐小粟同学5 小时前
链表面试编程题
数据结构·链表·面试
lucifer3115 小时前
深入解析 React 组件封装 —— 从业务需求到性能优化
前端·react.js