likes
comments
collection
share

redux相关的笔记之一

作者站长头像
站长
· 阅读数 36

前言

近来无事,翻了下曾经的笔记,发现两年前探索redux时的一些笔记,虽说两年时间之久,redux早已迭代多个版本,也废弃了一些api,但笔记并不是各种api如何使用等入门内容,而是redux相关的实现,所以尽管两年过去,再看也仍有收获。

笔记内容很多,重新整理了一下,打算分三篇文章(redux -> react-redux -> redux-saga),本文为第一篇,聊的是redux。

redux

本文节奏为从使用到实现,既如此,我们先来看看最终的例子。

最终例子展示

我们需要实现两个计数器(一号和二号),两个计数器都有简单加和减的功能,且每次变更时都会打印新的和旧的状态。另外,一号计数器的加和减皆为异步执行。所以这里就将一号计数器命名为AsyncCounter,二号计数器命名为Counter。

效果如图:

redux相关的笔记之一

createStore

最终例子的功能和内容都显得有些复杂了,我们循序渐进,从简单的开始,先从实现计数器2也就是Counter开始。

Counter非常简单,就是一个同步的计数器,仅仅是使用redux的createStore即可。

先看代码,其中路径中的@为src:

  • 先初始化一个store
// scr/store/actionTypes.js
export const ADD = "ADD";
export const MINUS = "MINUS";

// 文件:scr/store/reducers/counter.js
import { ADD, MINUS } from '@/store/actionTypes';

function add() { return { type: ADD } }
function minus() { return { type: MINUS } }
const actionsCreators = { add, minus };

export default actionsCreators;

// 文件:scr/store/index.js
import { createStore } from "redux";
import counterReducer from "./reducers/counter";

export default createStore(counterReducer);
  • Counter组件
import { useState, useEffect } from 'react';
import store from '@/store';
import { ADD, MINUS } from '@/store/actionTypes';

function Counter() {
  const forceUpdate = useState({})[1];
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      forceUpdate({});
    });
    return () => {
      unsubscribe(); 
    }
  }, []);

  const number = store.getState().number;

  const add = () => {
    store.dispatch({ type: ADD });
  }

  const minus = () => {
    store.dispatch({ type: MINUS });
  }

  return (
    <div>
      <div>{number}</div>
      <button onClick={add}>+</button>
      <button onClick={minus}>-</button>
    </div>
  )
}

export default Counter;

ok,以上代码就能够使我们的Counter执行起来了。

从以上代码中,我们可以看出核心为createStore方法,而想要了解createStore,我们得从它的使用下手,在store入口文件我们返回的是createStore的产物,我们在Counter组件中使用这个返回的产物,主要用到了三个方法,分别为:

  • subscribe:用于订阅, 当状态改变后执行订阅的方法,在本例子中订阅的方法为更新视图。
  • dispatch:用于派发一个动作,简而言之为执行一个指定动作去改变状态。
  • getState:用于获取store里的状态。

从这三个方法来看,很明显的发布订阅的味道,那么createStore这个方法其实就是实现一个发布与订阅而已。

实现发布与订阅很简单,使用闭包的特性,保存仓库的状态以及订阅的方法(也称为监听器)。然后在派发(dispatch)动作执行完了之后,执行那些之前保存的监听器。

思路很简单,我们直接看代码:

function createStore(reducer) {
  let state;
  const listeners = [];
  function subscribe(listener) {
    listeners.push(listener);
    return () => {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  }

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

  function getState() {
    return state;
  }

  dispatch({ type: "@@redux/init" });
  return {
    subscribe,
    dispatch,
    getState,
  };
}

export default createStore;

以上代码注意第23行,我们在createStore的时候先执行了一次dispatch,这里的用意主要是为了保存各个reducer的初始值,而且由于是createStore阶段,并没有任何监听器,所以这里不会导致任何的副作用,仅是完成状态的初始化而已。

那么createStore也就实现了,接着我们把原来代码中的createStore改为自己写的这个,Counter是能完美运行的。

applyMiddleware

有了上面实现的createStore,我们来尝试着实现AsyncCounter组件。

  • store改造

增加两个异步的actionTypes

// scr/store/actionTypes.js
export const ASYNC_ADD = "ASYNC_ADD";
export const ASYNC_MINUS = "ASYNC_MINUS";

增加asyncCounter的reducer

// 文件:scr/store/reducers/asyncCounter.js
import { ASYNC_ADD, ASYNC_MINUS } from "../actionTypes";

const initialState = { number: 0 };
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case ASYNC_ADD:
      return { number: state.number + 1 };
    case ASYNC_MINUS:
      return { number: state.number - 1 };
    default:
      return state;
  }
};

export default reducer;

将store应用的reducer改为asyncCounter的reducer

// 文件:scr/store/index.js
import { createStore } from "@/redux"; // @/redux为自己实现的redux
import asyncCounterReducer from "./reducers/asyncCounter";

export default createStore(asyncCounterReducer);
  • AsyncCounter组件
import { useState, useEffect } from 'react';
import store from '@/store';
import { ASYNC_ADD, ASYNC_MINUS } from '@/store/actionTypes';

function AsyncCounter() {
  const forceUpdate = useState({})[1];
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      forceUpdate({});
    });
    return () => {
      unsubscribe(); 
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const number = store.getState().asyncCounter.number;

  const asyncAdd = () => {
    setTimeout(() => store.dispatch({ type: ASYNC_ADD }), 1000);
  }

  const asyncMinus = () => {
    setTimeout(() => store.dispatch({ type: ASYNC_MINUS }), 1000);
  }

  return (
    <div>
      <div>{number}</div>
      <button onClick={asyncAdd}>+</button>
      <button onClick={asyncMinus}>-</button>
    </div>
  )
}

export default AsyncCounter;

这样下来,我们好像实现了这个异步计数器,但其实这违背了我们的初衷,我们这个action称为AsyncAdd或者AsyncMinus,但却是通过组件自己实现的异步然后派发一个同步,并非派发了一个异步动作。

所以AsyncCounter组件的asyncAdd和asyncMinus都应该改为:

const asyncAdd = () => {
  store.dispatch({ type: ASYNC_ADD });
}
const asyncMinus = () => {
  store.dispatch({ type: ASYNC_MINUS });
}

但,如此改了之后,异步就丢失了,我们应该去改reducer吗?

并不,实际上我们需要请redux的中间件机制出山,也就是applyMiddleware。

redux的中间件算是个人比较不喜欢的部分,感觉挺繁琐的,所以一直使用的mobx,这是题外话了。

回归正题,redux中间件的格式是固定的,这里我们以实现这个异步中间件为例。

function thunk() {
  return function (next) {
    return function (action) {
      if (action.type.startsWith("ASYNC")) {
        setTimeout(() => {
          next(action);
        }, 1000);
      } else {
        next(action);
      }
      return action;
    };
  };
}

这个thunk即为一个中间件,它的格式也如代码这样,本身为一个函数,需要返回一个函数,且返回的函数中还需返回一个函数,至于最后是否返回action其实问题不大,

这种格式挺繁琐的,但其实每一个函数的返回都是有其作用的,为了方便描述,我们将这三个嵌套的函数分别命名为一、二、三级函数吧。

最外面那层也就是一级函数,它是有参数的,他的参数为store的getState方法和一个类似store的dispatch方法。我们可以在这个中间件任何地方获得当前仓库状态以及派发动作。

二级函数接收的参数为next,这个next的意思可以理解为执行下一个中间件,因为redux是允许多个中间件合作的,只不过这些中间件是串行工作的。

三级函数接收的参数就是派发的动作了。

了解了这些,我们再写一个中间件,这个中间件的功能则为打印变化前后的状态。

function logger({ getState }) {
  return function (next) {
    return function (action) {
      console.log("老状态", getState());
      next(action);
      console.log("新状态", getState());
      return action;
    };
  };
}

ok,两个中间件都完成了,我们使用applyMiddleware将他们串起来工作。

// 文件:scr/store/index.js
import { applyMiddleware } from "redux";
import { createStore } from "@/redux"; // @/redux为自己实现的redux
import asyncCounterReducer from "./reducers/asyncCounter";

export default applyMiddleware(thunk, logger)(createStore)(asyncCounterReducer);

redux相关的笔记之一

如图,中间件就都工作起来了。

整个applyMiddleware的使用,既繁琐又神奇,我们仅是将中间件传给applyMiddleware,它就能让中间件们串行地工作起来。

而这个applyMiddleware的实现的难点也就在于将中间件们串起来。

直接实现applyMiddleware比较难,我们从一个简化的applyMiddleware的题目开始。

我们继续依据中间件的格式,写下如下三个中间件:

const promise = () => (next) => (action) => {
  new Promise((resolve) => {
    setTimeout(() => {
      console.log("promise");
      resolve();
    }, 500);
  }).then(() => {
    next(action);
  });
};

const thunk = () => (next) => (action) => {
  setTimeout(() => {
    console.log("thunk");
    next(action);
  }, 1000);
};

const logger = () => (next) => (action) => {
  console.log("logger");
  next(action);
};

然后我们派发一个动作:

const dispatch = () => {
  console.log("dispatch action");
};

现在要求实现一个compose函数,然后他们按照 半秒后输出promise -> 再一秒后输出thunk -> 输出logger -> 输出dispatch的顺序完成执行。

const middlewares = [promise, thunk, logger].map((middleware) => middleware());

const composed = compose(...middlewares);

const dispatch = () => {
  console.log("dispatch action");
};

composed(dispatch)();

这道题,我们重点观察三个中间件以及最后的输出要求,发现所谓的串行,其实每个中间件调用了next方法才去执行下一个中间件,那这个compose就很简单了。

function compose(...args) {
  return function (arg) {
    for (let i = args.length - 1; i >= 0; i--) {
      arg = args[i](arg);
    }
    return arg;
  };
}

几行代码即可搞定,如果想代码行数更少一点,可以像redux源码一样,采用reduce方法,不过我认为for循环会更好理解一些,这里变采用for循环的方式来实现。

最后执行一下:

redux相关的笔记之一

ok,这个compose实现了,那么applyMiddleware就变的很简单了,注意一下中间件各级函数的入参即可,这里就直接贴代码了。

function applyMiddleware(...middlewares) {
  return function (createStore) {
    return function (reducer) {
      const store = createStore(reducer);
      let dispatch;
      const middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => dispatch(action),
      };
      // 这个compose就是上面实现的compose
      const chain = compose(
        ...middlewares.map((middleware) => middleware(middlewareAPI))
      );
      dispatch = chain(store.dispatch); // 将被串起来的中间与原始dispatch再串一下
      return {
        ...store,
        dispatch, // 替换掉原始的dispatch,保证每一次dispatch都会先走完中间件的逻辑
      };
    };
  };
}

export default applyMiddleware;

注意第5行和第14行,第5行定义一个空的dispath,那是能给一级函数传入一个改写后的dispatch,而非原始的store的dispatch,因为改写后的dispatch才是包含了被串起来的中间件们。而我们在AsyncCounter组件中调用的dispatch也就是这个改写后的dispatch。

然后我们将redux的applyMiddleware改为自己写的applyMiddleware,AsyncCounter能完美运行起来。

combineReducers

为了更好的维护,我们会将每一个不同的reducer分开写,也如本文一样,现在有了两个reducer了,分别是counter的reducer和asyncCounter的reducer,但createStore却只接受一个reducer,为了解决这个问题,redux给出了combineReducers方法,正如其名,用于合并reducer。

  • 合并reducer
// 文件:scr/store/reducers/index.js
import { combineReducers } from "redux";
import asyncCounter from "./asyncCounter";
import counter from "./counter";

export default combineReducers({
  asyncCounter,
  counter,
});

// 文件:scr/store/index.js
import combinedReducer from "./reducers";

export default applyMiddleware(thunk, logger)(createStore)(combinedReducer);
  • Counter组件以及AsyncCounter组件 将两个计数器组件的number取值改一改即可
// Counter组件
const number = store.getState().counter.number;

// AsyncCounter组件
const number = store.getState().asyncCounter.number;

完成这些改动后,跑起来就是本文一开始的最终例子的效果了。

接下来,我们继续从使用处下手来理解combineReducers方法。

从以上改动可以看出,我们仅是在状态的取值方面有所变化,派发动作是没有变化的,那么不难猜出combineReducers方法其实就是将状态依据reducer的命名来分开保存,也就是在各个状态外面套多了一层。

将counter的{ number: 0 }和asyncCounter的{ number: 0 }合并成:

{
  counter: { number: 0 },
  asyncCounter: { number: 0 },
}

那么combineReducers方法的实现就很简单了:

function combineReducers(reducers) {
  return function combination(state = {}, action) {
    const nextState = {};
    for (let key in reducers) {
      const prevStateForKey = state[key];
      const reducerForKey = reducers[key];
      const nextStateForKey = reducerForKey(prevStateForKey, action);
      nextState[key] = nextStateForKey;
    }

    return nextState;
  };
}

export default combineReducers;

最后,我们把redux的combineReducers改成自己的combineReducers,两个计数器组件也能完美运行起来。

结尾

ok,到此,redux核心的三个方法也就实现了,我们的最终例子也完美运行了起来,而完整代码则在本文最后。那么本文所有内容到此就结束了,本文通过最终需要实现的例子为引,一步一步揭开redux三个核心api---createStore、applyMiddleware以及combineReducers的神秘面纱,并实现了他们,以此了解redux基本工作机制与原理。

最后,希望这篇文章能对大家有所帮助,另外,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹

点击查看完整代码

转载自:https://juejin.cn/post/7365757742381940777
评论
请登录