从 0 实现一个 mini redux

2022-05-11 16:30:51 浏览数 (1)

前言

本文从 redux 原理出发,一步步实现一个自己的 mini-redux,主要目的是了解其内部之间的各种关系,所以本篇不会讲解太多关于 redux 的用法

redux 是什么

redux 是一种可预测的状态管理库,在 react 中,它解决的是多个组件之间的通信问题

在没有使用 redux 的情况下,如果两个组件(非父子关系)之间需要通信的话,可能需要多个中间组件来为他们进行消息传递,这样既浪费了资源,代码也会变得更复杂

redux 提出了单一数据源 store 来存储状态数据,所有的组件都可以通过 action 来修改 store,也可以从 store 中获取最新状态。使用了 redux 就可以完美解决组件之间的通信问题

redux 的设计原则

redux 的三大设计原则:

  • 单一数据源
  • 状态是只读的
  • 使用纯函数编写 reducer

单一数据源

意思是整个 react 项目里的 state 都存放在一起,单一数据源主要是为了解决状态一致性的问题

在传统的 MVC 架构中,需要创建无数个 Model,而 Model 之间可以互相监听、触发事件甚至循环或嵌套触发事件,这些在 redux 中都是不允许的

在 redux 的思想里,一个应用永远只有唯一的数据源,这个设计也是有一些好处的,对于开发者来说,它可以更容易调试和观察状态的变化

也不用担心数据源对象过于庞大的问题,redux 提供的 combineReducers 函数可以解决这个问题

状态是只读的

这里说的状态,指的是上面说的存放在 store 中的状态数据,你不能直接对其中的状态数据进行改动只能间接的通过发送 action 来改动状态。间接的改动状态,这是一个很关键的设计,也是单向数据流的重点之一,对于每个动作的发生,最终会影响到什么状态上的改动,一个接一个的执行顺序等等,都是可预测的

使用纯函数编写 reducer

纯函数的概念:函数的返回结果只依赖其参数,并且执行过程中不会产生副作用

在 redux 中,我们通过定义 reducer 来更改状态,每个 reducer 都是纯函数,这意味着它没有副作用,相同的输入必定有相同的输出

ps:修改外部的变量、调用 DOM API 修改页面,发送 Ajax 请求,调用 window.reload 刷新浏览器甚至是console.log 打印数据,都是副作用

就问你纯不纯

redux 的几个基本概念

store

store 是存储数据的地方,它是一个对象,有这么几个方法

  • getState() 获取当前状态
  • dispatch(action) 派发 action
  • subscribe(handler) 监听数据的变化

action

action 可以理解为操作数据的行为

action 一般的写法如下:

代码语言:javascript复制
const add = (val) => {
    return {
        type: 'ADD',
        value: val
    }
}

通过 type 去定义这个 action 是干嘛的,在 reducer 中要进行什么操作

dispatch

dispatch 的作用就是派发一个 action,让 reducer 进行数据的处理

一般写法:

代码语言:javascript复制
dispatch({
    type: 'ADD',
    value: 1
})

reducer

reducer 里是真正更改数据的地方,dispatch 派发的 action 最终由 reducer 来进行数据的处理,并且每次的更改都是返回新的 state,这样做的目的是为了让 state 变的可预测

middleware

在创建 store 的时候 createStore 可以传入三个参数,第三个参数就是中间件,使用 redux 提供的 applyMiddleware 方法来调用,applyMiddleware 等于是对 dispatch 进行了增强,这样的话,在 dispatch 的过程中可以做一些其他的事情,比如记录 state 的变化、异步请求等等

从 0 实现一个 mini-redux

redux 的核心,就是 createStore 这个函数,store、getState、dispatch 都是这个函数返回的

redux 的大致原理就是发布订阅模式:通过 dispatch 派发 action 更改 store,通过 subscribe 订阅 store 的变化,去更新对应的 view

createStore

用过 createStore 方法的都知道,创建一个 store 需要三个参数

代码语言:javascript复制
/**
 * 创建 store 
 * @param {*} reducer 
 * @param {*} initState 初始 state
 * @param {*} enhancer 中间件
 */
const createStore = (reducer, initState, enhancer) => {

}

这个函数会返回几个功能函数

代码语言:javascript复制
/**
 * 创建 store 
 * @param {*} reducer 
 * @param {*} initialState 初始 state
 * @param {*} enhancer 中间件
 */
const createStore = (reducer, initialState, enhancer) => {
  return {
    getState,
    dispatch,
    subscribe,
    replaceReducer
  }
}

下面来实现这几个方法

getState 的实现

getState 方法的作用就是返回当前的 state

代码语言:javascript复制
let currentState; // 当前 state
/**
 * 返回最新的 state
 */
const getState = () => {
  return currentState;
};
subscribe 的实现

subscribe 的作用是订阅 state 的变化,使用者通过这个方法订阅,当 state 变化后,执行监听函数subscribe 是一个高阶函数,它的返回值一个函数,执行该函数可以移除当前的监听函数

代码语言:javascript复制
let subQueue = []; // 创建一个监听队列
/**
 * 监听 state 的变动
 * @param {*} listener 数据变化时要执行的函数 
 */
const subscribe = (listener) => {
  // 把监听函数放入监听队列里
  subQueue.push(listener);
  // 移除监听事件
  return () => {
    subQueue = subQueue.filter((l) => l !== listener);
  };
};
dispatch 的实现

dispatch 方法接收一个 action 参数,来执行 reducer,执行完成后会执行所有的监听函数

代码语言:javascript复制
let currentReducer = reducer;
let isDispatch = false;
/**
  * 派发 action 并执行所有 监听函数
  * @param {*} action 
  */
const dispatch = (action) => {
  // 这里使用 isDispatch 做标识,上一个处理完成后才能处理下一个
  if(isDispatch) {
    throw new Error('dispatching')
  }

  try {
    currentState = currentReducer(currentState, action)
    isDispatch = true;
  } finally {
    isDispatch = false;
  }

  // 执行所有监听函数
  subQueue.forEach((listener) => listener())
  return action
}
replaceReducer 的实现

replaceReducer 的作用就是替换当前 reducer,执行 createStore 的时候,会接收一个默认的 reducer,如果后面想要重新换一个,就需要用到 replaceReducer 了

代码语言:javascript复制
/**
 * 替换 reducer
 * @param {*} reducer
 */
const replaceReducer = (reducer) => {
 // 直接把新的 reducer 覆盖掉旧的就行了
 currentReducer = reducer;
 // 替换之后派发一次 dispatch
 dispatch({ type: 'MINI_REDUX_REPLACE' });
};

替换之后派发一次 dispatch 的目的是初始化一下新的 reducer

完整版 createStore

要想理解并实现中间件,内容还是蛮多的,所以本篇先不写中间件相关的内容

代码语言:javascript复制
/**
 * 创建 store
 * @param {*} reducer
 * @param {*} initialState 初始 state
 * @param {*} enhancer 中间件
 */
const createStore = (reducer, initialState, enhancer) => {
  let currentState; // 当前 state
  let subQueue = []; // 创建一个监听队列
  let currentReducer = reducer;
  let isDispatch = false;

  if (initialState) {
    currentState = initialState;
  }

  /**
   * 返回最新的 state
   */
  const getState = () => {
    return currentState;
  };

  /**
   * 监听 state 的变动
   * @param {*} listener 数据变化时要执行的函数
   */
  const subscribe = (listener) => {
    // 把监听函数放入监听队列里
    subQueue.push(listener);
    // 移除监听事件
    return () => {
      subQueue = subQueue.filter((l) => l !== listener);
    };
  };

  /**
   * 派发 action 并执行所有 监听函数
   * @param {*} action
   */
  const dispatch = (action) => {
    // 这里使用 isDispatch 做标识,上一个处理完成后才能处理下一个
    if (isDispatch) {
      throw new Error('dispatching');
    }

    try {
      currentState = currentReducer(currentState, action);
      isDispatch = true;
    } finally {
      isDispatch = false;
    }

    // 执行所有监听函数
    subQueue.forEach((listener) => listener());
    return action;
  };

  /**
   * 替换 reducer
   * @param {*} reducer
   */
  const replaceReducer = (reducer) => {
    if (reducer) {
      // 直接把新的 reducer 覆盖掉旧的就行了
      currentReducer = reducer;
    }
    // 替换之后派发一次 dispatch
    dispatch({ type: 'MINI_REDUX_REPLACE' });
  };

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

export default createStore;

要想在项目中跑起来,光实现一个 createStore 是不够的

createStore 建造了一个仓库,还需要配送点(Provider)和送货员(connect)才能到用户(组件)手里

Provider、connect

首先,我们需要清楚他们三者之间的职责:

createStore:生成 store,返回一系列功能函数

Provider:把 createStore 返回的一系列函数传递到每个子组件里

connect:把 store 里的数据关联到组件上

Provider 的实现

Provider 的主要作用就是把 store 里的数据传递下去

代码语言:text复制
// Provider.jsx
import React, { createContext } from 'react';

export const StoreContext = createContext(null);

const Provider = (props = {}) => {
  const { store, children } = props;
  return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};

export default Provider;

connect 的实现

connect 是一个高阶组件,第二个参数为需要关联数据的组件,返回一个新组件

connect 的作用就是把 store 的数据关联到对应组件里,并监听 store 的变化,数据变化后更新对应组件

代码语言:text复制
// connect.jsx
import React, { useContext, useEffect, useState } from 'react';
import { StoreContext } from './Provider';

const connect = (mapStateToProps, mapDispatchToProps) => (WrapComponent) => {
  const ConnectComponent = () => {
    const { getState, dispatch, subscribe } = useContext(StoreContext);
    const [props, setProps] = useState({
      getState,
      dispatch,
    });

    let stateToProps;
    let dispatchToProps;

    const update = () => {
      if (mapStateToProps) {
        stateToProps = mapStateToProps(getState());
      }

      if (mapDispatchToProps) {
        dispatchToProps = mapDispatchToProps(dispatch);
      }

      setProps({
        ...props,
        ...stateToProps,
        ...dispatchToProps,
      });
    };

    useEffect(() => {
      update();
      subscribe(() => update());
    }, []);

    return <WrapComponent {...props} />;
  };

  return ConnectComponent;
};

export default connect;

总结

一个基础版的 mini redux 就实现完了,有空了把中间件相关的东西输出一下

这是我学习完相关内容之后输出的一个笔记,有写的不对的地方,还请各位铁汁指正 抱拳了老铁

体验在线 demo:点我点我点我

github:https://github.com/isxiaoxin/mini_redux

首发自:从 0 实现一个 mini redux - 小鑫の随笔

0 人点赞