手撕源码之—从0实现Redux

1,286 阅读4分钟

设计原则和思想

  • 单一数据源,让React的组件之间的通信更加方便,同时也便于状态的统一管理
  • 单向数据流,保证的数据的纯净,不被其他操作污染。
  • Redux是将整个应用状态存储到到一个地方,称为store,里面保存一棵状态树state
  • 组件派发action给store里的reducer, reducer计算并返回新的state, store更新state,并通知订阅它的组件进行重新渲染
  • 其它组件可以通过订阅store中的状态(state)来刷新自己的视图

首先用react-cli脚手架来创建一个项目

  • 删掉没用的文件
  • 安装redux
npx create-react-app redux-hand
yarn add redux

先用原生的redux实现一个简单的计数器

  • 创建reducerstore
  • 创建App组件,在组件挂载时订阅sotre,组件销毁前取消订阅
  • 点击+/- 按钮时dispatch一个action(包含typepayload两个属性),修改store里面的状态
  • store里面的状态更新完,执行subscribe函数,更新组件状态
import React from "react";
import ReactDom from "react-dom";
import { createStore } from "redux";

// 派发动作
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";

// 初始状态
let initState = { number: 0 };

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
  }
};

// 根据reducer创建store
let store = createStore(reducer);

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { number: 0 };
  }

  componentDidMount() {
    // 组件挂载时订阅sotre
    this.unsubscribe = store.subscribe(() =>
      this.setState({ number: store.getState().number })
    );
  }
  componentWillUnmount() {
    // 组件销毁前取消订阅
    this.unsubscribe();
  }

  render() {
    return (
      <div style={{ margin: "30px" }}>
        <h2>{this.state.number}</h2>
        <button title="加" onClick={() => store.dispatch({ type: INCREMENT })}>
          +
        </button>
        <button title="减" onClick={() => store.dispatch({ type: DECREMENT })}>
          -
        </button>
      </div>
    );
  }
}

ReactDom.render(<App />, document.getElementById("root"));

实现Redux中的核心方法

1、实现createStore

  • createStore用来创建store对象,给外部使用,一般来说一个应用中只有一个store
  • createStore方法返回一个对象,包含三个方法
  • getState 获取store里的状态,直接将store里的状态返回即可
  • subscribe订阅,会将订阅的函数保存在currentListeners中,然后返回unsubscribe函数取消订阅(就是将订阅的函数从currentListeners中删除)。
  • dispatch派发action,会先执行reducer函数,获取最新状态,然后从依次执行currentListeners中订阅的函数。
import ActionTypes from "./utils/actionTypes";

export default function createStore(reducer, preloadedState) {
  let currentReducer = reducer;
  let currentState = preloadedState;
  let currentListeners = [];
  function getState() {
    return currentState;
  }

  function subscribe(listener) {
    currentListeners.push(listener);

    return function unsubscribe() {
      const index = currentListeners.indexOf(listener);
      currentListeners.splice(index, 1);
    };
  }

  function dispatch(action) {
    currentState = currentReducer(currentState, action);
    for (let i = 0; i < currentListeners.length; i++) {
      const listener = currentListeners[i];
      listener();
    }
    return action;
  }

  dispatch({ type: ActionTypes.INIT });

  const store = {
    dispatch,
    subscribe,
    getState,
  };
  return store;
}

上面派发的action是个普通对象,如果我们想派发一个函数,或者做异步处理改怎么办

2、接下来我们看一下bindActionCreators的用法

  • 声明 addminus 两个函数,返回值是action对象
  • 调用bindActionCreators方法将 actionsstore.dispatch方法进行绑定
function add() {
  return { type: INCREMENT };
}
function minus() {
  return { type: DECREMENT };
}
const actions = { add, minus };
const boundActions = bindActionCreators(actions, store.dispatch);
  • 在 render函数中使用boundActions
render() {
    return (
      <div style={{ margin: "30px" }}>
        <h2>{store.getState().number}</h2>
        <button title="加" onClick={() => store.dispatch({ type: INCREMENT })}>
          +
        </button>
        <button title="减" onClick={() => store.dispatch({ type: DECREMENT })}>
          -
        </button>
        <button
          title="异步加一"
          onClick={() => setTimeout(boundActions.add, 1000)}
        >
          异步+
        </button>
      </div>
    );
  }

3、实现bindActionCreator

  • 本质上是将传入的函数用dispatch方法进行了一次包装,返回了一个新的函数
  • 在绑定后的函数中可以直接拿到dispatch方法进行调用,修改store里的状态
function bindActionCreator(actionCreator, dispatch) {
  return function (...args) {
    return dispatch(actionCreator.apply(this, args));
  };
}

export default function bindActionCreators(actionCreators, dispatch) {
  // 传入的是单个函数,直接绑定并返回
  if (typeof actionCreators === "function") {
    return bindActionCreator(actionCreators, dispatch);
  }
  const boundActionCreators = {};
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key];
    if (typeof actionCreator === "function") {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
  }
  return boundActionCreators;
}

4、store中只能有一个reducerstate,当我们有多个模块有多个reducer时,需要用combineReducersreducer进行合并,我们先看一下怎么使用

  • 对原有项目按redux在项目中的真实使用进行改造,最终目录如下

  • counter1的reducer,counter2类似
import * as types from "../action-types";
let initialState = { number: 0 };
export default function (state = initialState, action) {
  switch (action.type) {
    case types.ADD1:
      return { number: state.number + 1 };
    case types.MINUS1:
      return { number: state.number - 1 };
    default:
      return state;
  }
}
  • 合并counter1、counter2的reducer
// src/store/reducers/index.js
import { combineReducers } from "redux";
import counter1 from "./counter1";
import counter2 from "./counter2";
let rootReducer = combineReducers({
  counter1,
  counter2,
});
export default rootReducer;
  • 根据合并后的reducer创建store
import { createStore } from "../redux";
import reducer from "./reducers";
const store = createStore(reducer);
export default store;

5、实现combineReducers

  • combineReducers是一个高阶函数,实质上是给store生成一个合并后的总state
  • nextState会将每个reducer的key做为key, 将每个reducer返回的分state作为value存入nextState进行合并,作为返回给store的最终状态
  • dispatch 一个action时,会首先交给combination函数进行处理,combination中会将 action传给所有的reducer,返回最新的分state,放在nextState返回给store
function combineReducers(reducers) {
  return function combination(state = {}, action) {
    let nextState = {};
    for (let key in reducers) {
      nextState[key] = reducers[key](state[key], action);
    }
    return nextState;
  };
}
export default combineReducers;

将项目中用到的redux切换到自己实现的redux,最终效果是一样的

代码地址

下一节:从1实现react-redux

如果这篇文章对你有帮助,请帮我点个赞吧