Redux:引擎盖之下之旅

2025-06-05

Redux:引擎盖之下之旅

照片由 Hosea Georgeson 在 Unsplash 上拍摄

但是等等,我们现在有了 React hooks,我们不再需要 Redux 了,对吧?

如果你不是 React 开发者,React Hooks 是 React 的最新功能,它确实很棒⚡,但它不会取代 Redux。如果你仍然不确定,我强烈推荐 Eric Elliot 的文章《React Hooks 会取代 Redux 吗?》

现在,如果你想继续阅读而不阅读 Elliot 的文章,以下是 tl;dr:

  • Redux 不仅仅是一个库,它的架构在构建可扩展和可维护的代码方面被证明非常有效。
  • 虽然您可以使用 createContext 和 React Hooks 重新创建 Redux 的功能,但这样做并没有明显的好处,而且您将无法使用 Redux devtools 中强大的调试功能。

我希望您能够信服并加入我们的旅程。在我们开始之前,请先浏览一下我们在 Redux 中经常看到的函数式编程概念手册。如果您对这些概念有信心,可以直接跳到旅程的开头。

目录

函数式编程概念手册

我们不会尝试在这里详尽地解释这些概念,因为我认为在一篇文章中涵盖所有这些概念是徒劳的。不过,我会尽量解释得足够详细,以便您能从本文中获益最多。

纯函数

  • 函数的返回值由传递给它们的参数决定。
  • 它们不会访问或修改其范围之外的值。

闭包

闭包是在新函数创建时创建的,它允许这些函数访问外部作用域。

function outer() {
  const savedInClosure = true;
  return function() {
    if (savedInClosure) {
      console.log('I always have closure');
    }
  };
}

const doYouHaveClosure = outer();
doYouHaveClosure(); // 'I always have closure'
Enter fullscreen mode Exit fullscreen mode

高阶函数

接收函数作为参数并/或返回另一个函数的函数。没错,上面的代码就是一个高阶函数,你注意到了吗?😉。

柯里化

柯里化是一种将一个接受多个参数的函数转换成一系列每次只接受一个参数的函数的技术。现在,你可能会尖叫着“我为什么要这么做?” 答案很简单,就是“特化函数和复杂性分离”。让我们来看一个典型的柯里化示例:

// Before currying
const add_notCurrying = (x, y) => x + y;

// after currying
const add_currying = x => y => x + y;

// specialize functions
const add2 = add_currying(2);

add2(8); // 10
Enter fullscreen mode Exit fullscreen mode

现在假设你的经理来找你,告诉你:“add 函数在提交第一个参数之前必须进行一系列检查和 API 调用,而提交第二个参数时必须进行完全不同的检查和 API 调用”。在非柯里化版本中,你必须把所有这些复杂性都塞进一个函数里,而在柯里化版本中,add你可以把它们分开。

函数组合

函数组合是将函数组合起来构建更复杂函数的过程,在上面的例子中,我们已经进行了一些函数组合。然而,我在这里要解释的技术,第一次看到可能会让你头疼:

const myFuncs = [func1, func2, func3, func4];

const compose = arr => arr.reduce((a, b) => (...args) => a(b(...args)));

const chain = compose(myFuncs);
Enter fullscreen mode Exit fullscreen mode

哇喔……,相信我,如果你没有函数式编程经验,就像我第一次看到这个的时候一样,那么“🤬🤬🤬🤬”这样的反应在我看来是最健康的反应。除非你精通函数式编程,否则这不会直观,而且可能需要一些时间才能理解,但是。现在,你需要知道,Compose 所做的只是帮助我们实现类似这样的函数。

const composed = (...args) => func1(func2(func3(func4(...args))));
Enter fullscreen mode Exit fullscreen mode

正如你所见,我们从 compose 中得到的最终函数,从右到左调用数组中的函数,并将每个函数的返回值作为参数传递给前一个函数。现在,记住这个框架,尝试看一下上面代码的重构版本。

const myFuncs = [
  () => {
    console.log(1);
  },
  () => {
    console.log(2);
  },
  () => {
    console.log(3);
  },
  () => {
    console.log(4);
  }
];

let chain = myFuncs[0];

for (let index = 1; index < myFuncs.length; index++) {
  const currentRingInTheChain = myFuncs[index];

  // This is necessary to avoid recursion. Basically we storing different instances of functionsChainSoFar in closure scopes
  const functionsChainSoFar = chain;

  chain = (...args) => functionsChainSoFar(currentRingInTheChain(...args));
}

chain(); // 4 , 3, 2, 1
Enter fullscreen mode Exit fullscreen mode

我希望以上解释能让你明白到底是什么compose意思,但如果你还不是100%确定,也别太担心。这可能需要一些时间,也需要你转变思维。

奖励回合:您认为以下代码将记录什么?

const myFuncs = [
  func => () => {
    console.log(1);
    func();
  },
  func => () => {
    console.log(2);
    func();
  },
  func => () => {
    console.log(3);
    func();
  },
  func => () => {
    console.log(4);
    func();
  }
];

const hakuna = () => console.log('Mattata');

const secret = compose(myFuncs)(hakuna);

secret(); // what do you think this will log?
Enter fullscreen mode Exit fullscreen mode

尝试一下,但如果您遇到困难,请不要担心,我们会在文章中再次讨论这个问题。

旅程开始

开始本次旅程的最佳方式是了解如何创建 Redux Store,以及其中有哪些组件在起作用。那么,让我们先来看看文档中的这个示例。

import { applyMiddleware, createStore } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

import monitorReducersEnhancer from './enhancers/monitorReducers';
import loggerMiddleware from './middleware/logger';
import rootReducer from './reducers';

export default function configureStore(preloadedState) {
  const middlewares = [loggerMiddleware, thunkMiddleware];
  const middlewareEnhancer = applyMiddleware(...middlewares);

  const enhancers = [middlewareEnhancer, monitorReducersEnhancer];
  const composedEnhancers = composeWithDevTools(...enhancers);

  const store = createStore(rootReducer, preloadedState, composedEnhancers);

  return store;
}
Enter fullscreen mode Exit fullscreen mode

这里有很多事情要做,我们使用了 redux-thunk,附加了 redux-devtools-extensions 等等。所以,让我们分而治之,将上面的代码分成四个域。

  1. reducers
  2. 功能createStore
  3. enhancers
  4. middlewares

第一:rootReducer新国家的缔造者

函数rootReducer是三个参数中的第一个,createStore您很可能已经知道 reduxreducers是接受当前状态和动作并返回新状态的函数。您可能还已经知道必须reducers纯函数
但是,您是否想过“为什么 reducer 必须是纯函数?” 🤔。嗯,有一个很好的理由,但不幸的是,我没有一段代码可以指出并告诉您“如果不是纯函数,它总是会中断”。然而,必须是纯函数这一事实reducers正是 Redux 目标的核心,那就是“具有可预测状态突变的状态存储”。Redux 通过坚持三个自我强加的原则来实现这一目标:

  • 单一事实来源
  • 状态为只读
  • 使用纯函数来改变状态

如果您没有立即明白这一点,请不要担心,我们将在本文中再次看到这些原则。

所以,reducer 是纯函数。它们将当前状态和动作作为参数,并返回一个新的状态对象,明白了👍。但是combineReducers,这个神奇的函数是如何工作的呢?嗯,combineReducers这是一个很棒的实用函数,可以帮助我们保持代码模块化,但实际上并没有什么神奇之处。combineReducers是一个高阶函数,它所做的就是:

  • 从传递给它的 Reducer 对象中提取一个数组(注意,Reducer 键与状态树的形状相匹配)
  • 返回一个新reducer函数。
    • 该函数将通过循环遍历 Reducer 键数组并调用相应的方法来创建下一个状态reducer
    • 最后,它将返回下一个状态。

看一下精简版本combineReducers

const reducers = {
  someState: reducerOfSomeState,
  anotherState: reducerOfAnotherState
};

function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);

  return function combinedReducer(state = {}, action) {
    const nextState = {};
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i];
      const reducer = reducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);

      nextState[key] = nextStateForKey;
    }
    return nextState;
  };
}

const rootReducer = combineReducers(reducers);
Enter fullscreen mode Exit fullscreen mode

最后,通过查看combineReducers,您可能已经注意到一个重要的见解,那就是,每次调用时,应用程序中的rootReducers所有都reducers将被调用来创建下一个状态。

第二:createStore店铺制造商

最简单的形式是createStore返回一个状态对象和一些方法。然而,它也接受一些额外的参数来增强store 的功能,稍后会详细介绍。现在,让我们先来了解一下更简单的版本createStore

我们已经了解了Redux 所基于的三个原则。现在,让我们重新审视它们,并尝试构建我们自己的 Redux 副本 🛠:

  • 单一事实来源≈我们应该有一个单一的存储对象。
  • 状态是只读的≈状态对象不应该直接被修改,而应该使用方法描述并发出修改。(如果你不明白我们是如何从“状态是只读的”中得出这个结论的,那也无妨,毕竟只有四个字。不过,文档对这一点进行了详细说明,并明确了该原则的意图。)
  • 使用纯函数进行更改≈reducer必须是纯函数。

遵循上述原则,我们的 Redux 副本可能看起来像这样:

// An action to initialize our state
const ActionTypes = {
  INIT: `@@redux/INIT${Math.random()
    .toString(36)
    .substring(7)}`
};

function createStore(rootReducer, initialState) {
  let currentState = initialState;

  const dispatch = action => {
    currentState = rootReducer(action);
  };

  const getState = () => currentState;

  // setting the initial state tree.
  dispatch({ type: ActionTypes.INIT });
  return {
    dispatch,
    getState
  };
}

const myAwesomeStore = createStore(rootReducer, {});
Enter fullscreen mode Exit fullscreen mode

这几行代码可能看起来不多,但它们相当于 Redux 的核心功能。当然,Redux 添加了一些检查,以帮助开发人员避免一些愚蠢的错误,例如在 Reducer 内部调用 dispatch 或不使用普通对象进行调用。此外,我们的副本至少目前dispatch还不支持middleware或。enhancers

第三:middleWares中间的

我知道了🤯,
好吧好吧,不过说真的,从概念上把它们想象成dispatcher和之间的中间人会很有帮助rootReducer剧透:在增强器部分,我们会看到它比这更复杂一些。
因为操作会经过中间件,所以它们可以被更改、取消或执行任何其他操作。如何有效地使用中间件有很多细微差别,但在本文中,我们将仅关注它们在 Redux 内部的工作原理。所以,让我们通过研究一个可能是你见过的最简单的中间件来了解这一点。

const middledWare = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
  }
  return next(action);
};
Enter fullscreen mode Exit fullscreen mode

如果你直接跳过第一行,直接跳到最终函数的主体,你可能会觉得逻辑很简单。然而,一旦你的目光回到第一行,你脑子里的“柯里化”铃声就会响起。另外,如果你对此感到困惑,不要灰心,因为你并不孤单,事实上,这个问题是文档“为什么中间件签名使用柯里化?”中的常见问题解答之一。在下一节中,我们将看到 Redux 内部如何使用这个函数签名applyMiddleware,现在只需记住上面中间件签名中的以下内容即可。

  1. dispatch第一个函数将使用具有两个属性和getState(middleWareApi)的对象进行调用。
  2. 第二个函数被调用next(下一个中间件)。
  3. 最后一个函数充当一个动作dispatch并通过一个动作被调用。

有趣的事实🤓:您可能没有注意到,但上面的代码实际上是redux-thunk 的源代码。

第四:enhancers增强createStore

你可能已经猜到了,enhancers是高阶函数,它接受createStore并返回一个新的增强版本createStore。看一下这个示例实现。

const ourAwesomeEnhancer = createStore => (reducer, initialState, enhancer) => {
  const store = createStore(monitoredReducer, initialState, enhancer);
  //  add enhancer logic

  return {
    ...store
    //   you can override the some store properties or add new ones
  };
};
Enter fullscreen mode Exit fullscreen mode

虽然你很少需要自己编写enhancers,但你可能已经在使用至少一个applyMiddleware。哦,是的,这可能会让一些人感到震惊,但的概念middlewares并不在 Redux 中createStoreenhancer。我们通过使用Redux 附带的唯一 ,为我们的 store 添加了中间件功能applyMiddleware

具体来说,实际的增强器是返回的函数,applyMiddleware但它们在文档中可以互换引用。

enhancer函数首先从内部调用createStore,并没有什么神奇或过于复杂的地方。你很快就会看到。然而,在查看代码之前,我们需要解决一个紧迫的问题🚧。由于enhancers接收createStore并返回的是增强版的createStore,因此你可以看到,使用这些术语来解释的机制enhancer很快就会变得复杂。因此,为了本节的目的,我将介绍我称之为占位符的术语

  • originalStoreMaker 您可以从 Redux 导入的函数createStore
  • storeMaker :任何具有与原始 storeMaker相同签名函数(接受相同的参数并返回相同的 API)。

好了,现在我们来看一些代码。看一下上面的 Redux 副本,现在修改为 accept enhancer

function createStore(rootReducer, initialState, enhancer) {
  let currentState = initialState;

  // Now accepts enhancers
  if (typeof enhancer !== 'undefined' && typeof enhancer === 'function') {
    return enhancer(createStore)(reducer, preloadedState);
  }

  const dispatch = action => {
    currentState = rootReducer(action);
  };

  const getState = () => currentState;

  // setting the initial state tree.
  dispatch({ type: ActionTypes.INIT });
  return {
    dispatch,
    getState
  };
}
Enter fullscreen mode Exit fullscreen mode

正如我所说,没有什么神奇之处。它只是一个接受storeMaker 参数并返回增强型storeMaker 的函数。当然,这并不是说它enhancer不能很复杂。而是说, 的复杂性enhancer被封装在它内部,并由它试图实现的目标决定,而不是由它如何与storeMakerenhancer交互决定。这种细微的区别非常重要,因为我们在本节的其余部分将研究Redux 中最广泛使用的 的实现applyMiddleware

applyMiddleWare

function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args);
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      );
    };

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    };
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

好了,这就是全部内容,现在让我们来解开它。首先,让我们快速理解一下顶部的 curring 部分。我们真正需要知道的是这些函数调用时会使用什么参数,幸运的是,我们已经知道了:

  • applyMiddleware需要middlewares返回一个enhancer
  • enhancers获取一个storeMaker并返回一个增强的storeMaker

从此我们可以将注意力重新集中到最终函数的主体上,并注意它在闭包中的内容。

// In closure: [middlewares], createStore

// This final function is a storeMaker
(...args) => {
  const store = createStore(...args);
  let dispatch = () => {
    throw new Error(
      'Dispatching while constructing your middleware is not allowed. ' +
        'Other middleware would not be applied to this dispatch.'
    );
  };

  const middlewareAPI = {
    getState: store.getState,
    dispatch: (...args) => dispatch(...args)
  };
  const chain = middlewares.map(middleware => middleware(middlewareAPI));
  dispatch = compose(...chain)(store.dispatch);

  return {
    ...store,
    dispatch
  };
};
Enter fullscreen mode Exit fullscreen mode

现在好多了,代码中的某个地方会用和调用这个storeMaker。跳转到函数内部,前两行创建 store 并将一个函数赋值给名为 的变量。正如错误信息所说,这样做是为了防止开发人员在storeMaker内部意外调用rootReducerinitialStatedispatchdispach

// In closure: middlewares and the original createStore.

// + more code above
const store = createStore(...args);
let dispatch = () => {
  throw new Error(
    'Dispatching while constructing your middleware is not allowed. ' +
      'Other middleware would not be applied to this dispatch.'
  );
};
// + more code below
Enter fullscreen mode Exit fullscreen mode

在查看第二段代码之前,请尝试回忆一下我们之前在 Redux 中见过的a 的签名middleware 。这里,每个函数的第一个柯里化函数被调用。执行完这部分代码后,我们将得到一个函数数组,每个函数的闭包中都有一个指向对象的引用middlewaremiddleWareAPI

// In closure: middlewares and the original createStore.

// + more code below
const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
};

const chain = middlewares.map(middleware => middleware(middlewareAPI));
// + more code below
Enter fullscreen mode Exit fullscreen mode

做好准备,下一行可能是代码中最令人畏惧的部分。很大程度上是因为compose函数。尽管如此,还是尝试一下💪,并记住这个提示:变量中的所有函数都chain返回一个函数。

// In closure: middlewares and the original createStore.

// + more code below
dispatch = compose(...chain)(store.dispatch);
// + more code below
Enter fullscreen mode Exit fullscreen mode

如果你看过我们的函数式编程概念手册,看到上面的代码可能会让你想起一些事情。因为这段代码看起来和函数组合子部分中加分环节的代码非常相似。说到这,你猜那里的代码会打印什么呢?……

好吧,我们再看看。

const myFuncs = [
  func => () => {
    console.log(1);
    func();
  },
  func => () => {
    console.log(2);
    func();
  },
  func => () => {
    console.log(3);
    func();
  },
  func => () => {
    console.log(4);
    func();
  }
];

const hakuna = () => console.log('Mattata');

const secret = compose(myFuncs)(hakuna);

secret(); // 1, 2, 3, 4, Matata
Enter fullscreen mode Exit fullscreen mode

是的,如果你尝试在控制台中运行代码,你会看到它打印了1, 2, 3, 4, Matata。代码似乎是从左到右运行的。除非用compose调用返回的函数之后hakuan,否则我们就没有数组了!从左到右是从哪里来的?这是因为闭包和回调。好吧,我猜这没什么帮助😅。不过不用担心,我会尽量解释得更清楚一些,但首先为了避免混淆,我需要再次介绍新的占位符术语

  • level1FuncmyFuncs数组内的任何函数。
  • level2Func :由level1Func返回的任何函数

好吧,让我们回顾一下我们想要实现的目标。我们希望所有level2Func都能按照从左到右的顺序运行。我们可以在数组中看到,每个level1Func都接受一个回调作为参数,然后在其level2Func内部调用该回调。因此,如果每个level1Func都能以某种方式与下一个 level2Func一起调用,那么我们似乎就能实现目标了

好的,齿轮转动起来⚙⚙,我们快要接近目标了。现在我们知道,Compose 会返回一个函数,它会从右到左调用函数,并将每个返回值传递给数组中的前一个函数。但是天哪,在我脑子里运行这段代码太难了😵。或许我们可以看看它看起来会是什么样子。

const composed = (...args) => func1(func2(func3(func4(...args))));
Enter fullscreen mode Exit fullscreen mode

啊哈!由于composed函数的调用顺序是从右到左的,所以每个level1func都会被下一个 level2func调用。干得好,你明白了👏。这正是我们最终得到一个类似于链式函数的方式,它从左到右运行。最后要强调的是,hakuna函数是composed传递的第一个参数,因此它是链中的最后一个函数。

现在,带着这个新的理解,让我们回顾一下 中的代码行applyMiddleware。我希望你现在已经明白这个链是如何构建的了:每个中间件都会调用一个中间件,而链中的最后一个函数会将store.dispatch新状态(但不会创建新状态)设置到 store 中。

// In closure: middlewares and the original createStore.

// + more code below
dispatch = compose(...chain)(store.dispatch);
// + more code below
Enter fullscreen mode Exit fullscreen mode

最后,因为这毕竟是一个storeMaker函数,所以我们返回商店并当然覆盖该dispach属性。

return {
  ...store,
  dispatch
};
Enter fullscreen mode Exit fullscreen mode

礼品店

以上就是 Redux 核心工作原理的全部内容。Redux 还提供了其他一些方法,虽然它们不会改变您对 Redux 工作原理的理解,但仍然值得一提。以下是简要列表。

  • replaceReducer:允许您替换 store 的 rootReducer。有趣的是,在某些情况下,您可以使用它来添加新的 reducer,而不仅仅是替换整个rootReducer
  • 订阅:使您能够传递回调,该回调将在任何操作被分派后被调用。
  • observable:可以在 RxJS 等库中使用。还允许您订阅更改。

恭喜你成功了🎊🎊👏👏。现在你了解了 Redux 的底层工作原理,并希望能够体会到函数式编程的强大之处。

文章来源:https://dev.to/fawwaz/redux-the-under-the-hood-tour-2k87
PREV
使用 Stripe、Nuxt.js 和 vercel 接受付款
NEXT
如何免费使用 node js 发送电子邮件 AWS GenAI LIVE!