资讯专栏INFORMATION COLUMN

Redux专题:中间件

ybak / 2497人阅读

摘要:好处就是不再需要能够处理异步的中间件了。不过,它是一个研究中间件很好的范本。执行它,返回的是由第二层函数组成的中间件数组。也就是说呀同学们,除了最后一个中间件的是原始的之外,倒数往前的中间件传入的都是上一个中间件的逻辑函数。

</>复制代码

  1. 本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出
    来我的 GitHub repo 阅读完整的专题文章
    来我的 个人博客 获得无与伦比的阅读体验

Redux暴露非常少的API,优雅的将单向数据流落地。但有这些,Redux的作者Dan Abramov仍然觉得远远不够。一个工具的强大之处体现在它的扩展能力上。Redux的中间件机制让这种扩展能力同样变的异常优雅。

中间件在前端的意思是插入某两个流程之间的一段逻辑。具体到Redux,就是在dispatch一个动作前后插入第三方的处理函数。

使用

还记得吗?Store构造器createStore有三个参数,第三个参数叫做enhancer,翻译过来就是增强器。我们先将enhancer按下不表,并且告诉你其实Redux的另一个APIapplyMiddleware就是一个enhancer。

</>复制代码

  1. import { createStore, combineReducers, applyMiddleware } from "redux";
  2. import thunk from "redux-thunk";
  3. import logger from "redux-logger";
  4. import { userReducer } from "./user/reducer";
  5. import { todoReducer } from "./todo/reducer";
  6. const reducers = combineReducers({
  7. userStore: userReducer,
  8. todoStore: todoReducer,
  9. });
  10. const enhancer = applyMiddleware(thunk, logger);
  11. const store = createStore(reducers, null, enhancer);
  12. export default store;

只需要把所有中间件依次传入applyMiddleware,就生成了一个增强器,它们就可以发挥作用了。

如果preloadedState为空,enhancer可以作为第二个参数传入。看源代码:

</>复制代码

  1. if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
  2. enhancer = preloadedState;
  3. preloadedState = undefined;
  4. }
  5. if (typeof enhancer !== "undefined") {
  6. if (typeof enhancer !== "function") {
  7. throw new Error("Expected the enhancer to be a function.");
  8. }
  9. return enhancer(createStore)(reducer, preloadedState);
  10. }
  11. if (typeof reducer !== "function") {
  12. throw new Error("Expected the reducer to be a function.");
  13. }
服务器请求

一个组件免不了向服务器请求数据,然而开发者不希望组件内部有过多的逻辑,请求应该封装成函数给组件调用,同时组件需要实时获取请求的状态以便展示不同的界面。最好的办法就是将请求也纳入Redux的管理中。

</>复制代码

  1. import api from "./api";
  2. export const fetchMovieAction = () => {
  3. dispatch({ type: "FETCH_MOVIE_START" });
  4. api.fetchMovie().then(res => {
  5. dispatch({ type: "FETCH_MOVIE_END", payload: { movies: res.data } });
  6. }).catch(err => {
  7. dispatch({ type: "FETCH_MOVIE_ERROR", error: true, payload: { msg: err } });
  8. });
  9. };

</>复制代码

  1. import React, { Component } from "react";
  2. import { connect } from "react-redux";
  3. import { fetchMovieAction } from "./actions";
  4. import Card from "./Card";
  5. class App extends Component {
  6. render() {
  7. const { movies } = this.props;
  8. return (
  9. {movies.map(movie => )}
  10. );
  11. }
  12. componentDidMount() {
  13. this.props.fetchMovie();
  14. }
  15. }
  16. const mapState = (state) => {
  17. return {
  18. movies: state.payload.movies,
  19. };
  20. };
  21. const mapDispatch = (dispatch) => {
  22. return {
  23. fetchMovie: () => dispatch(fetchMovieAction()),
  24. };
  25. };
  26. export default connect(mapState, mapDispatch)(App);

大功告成了。

只需要将请求封装成一个函数,然后伪装成Action被发射出去,请求调用前后,真正的Action会被发射,在Store中存储请求的状态,并且能够被组件订阅到。

异步Action

你是不是发现了什么?对咯,这里的Action不是一个纯对象。

因为请求一定是一个函数,为了让请求入会,只能反过头来修改大会章程。但是大会章程岂能随便推翻,这时意见领袖出来说话了:

</>复制代码

  1. 当初规定Action必须是一个纯对象不是为了搞个人崇拜,而是出于实际需要。因为reducer必须是一个纯函数,这决定了dispatch的参数Action必须是一个带type字段的纯对象。现如今我们要拉异步请求入会,而中间件又可以中途拦截做一些处理,那Action为什么不能是一个函数呢?Action必须是一个纯对象这种说法是完完全全的教条主义!

大家还动脑筋想出了一个异步Action的名头,这下函数类型的Action终于名正言顺了。

闭包

你是不是还发现了什么?对咯,请求函数中的dispatch哪去了。

不知道,可能会报错吧(无辜脸)。

其实我们还有一件事没干:把dispatch方法偷渡到请求函数中。

</>复制代码

  1. export const fetchMovieAction = () => {
  2. return (dispatch) => {
  3. dispatch({ type: "FETCH_MOVIE_START" });
  4. api.fetchMovie().then(res => {
  5. dispatch({ type: "FETCH_MOVIE_END", payload: { movies: res.data } });
  6. }).catch(err => {
  7. dispatch({ type: "FETCH_MOVIE_ERROR", error: true, payload: { msg: err } });
  8. });
  9. };
  10. };

很简单哪,加一个闭包,dispatch从返回函数的参数中偷渡进来。

脑洞

我们要的不就是一个dispatch方法么,我能不能这样:

</>复制代码

  1. export const fetchMovie = (dispatch) => {
  2. dispatch({ type: "FETCH_MOVIE_START" });
  3. api.fetchMovie().then(res => {
  4. dispatch({ type: "FETCH_MOVIE_END", payload: { movies: res.data } });
  5. }).catch(err => {
  6. dispatch({ type: "FETCH_MOVIE_ERROR", error: true, payload: { msg: err } });
  7. });
  8. };

</>复制代码

  1. const mapDispatch = (dispatch) => {
  2. return {
  3. fetchMovie: () => fetchMovie(dispatch),
  4. };
  5. };

貌似是能行得通的,只不过这时候请求函数已经不能叫Action了。考虑到之前请求函数伪装成Action浑水摸鱼,还要插入中间件来帮助特殊处理,我们这样做也不过分是吧。

好处就是不再需要能够处理异步Action的中间件了。

坏处就是这不符合规范,是我的脑洞,闯了祸不要打我(蔑视)。

redux-thunk

前面多次提到处理异步Action的中间件,到底是何方神圣?

市面上流行的方案有很多种,我们挑最简单的一种来说一说(都不点赞怪我咯)。

redux-thunk算是Redux官方出品的异步请求中间件,但是它没有集成到Redux中,原因还是为了扩展性,社区可以提出各种方案,开发者各取所需。

让我们来探讨一下redux-thunk的思路:原来Action只有一种,就是纯对象,现在Action有两种,纯对象和异步请求函数。只不过多了一种情况,不算棘手嘛。如果Action是一个对象,不为难它直接放走;如果Action是一个函数,就地执行,调用异步请求前后,真正的Action自然会释放出来,又回到第一步,放它走。

这是redux-thunk简化后的代码,其实源代码也跟这差不多。是不是很恐慌?

</>复制代码

  1. const thunk = ({ dispatch, getState }) => next => action => {
  2. if (typeof action === "function") {
  3. return action(dispatch, getState);
  4. }
  5. return next(action);
  6. };

上面的函数就部署在我的个人博客中用来处理异步请求,完全没有问题。既然它这么简单,而且可以预计它万年不会变,那我为什么要凭空多一个依赖包。就将它放在我的眼皮底下不是挺好的嘛。

不过,它是一个研究中间件很好的范本。

我们先将thunk先生降级成普通函数的写法:

</>复制代码

  1. const thunk = function({ dispatch, getState }) {
  2. return function(next) {
  3. return function(action) {
  4. if (typeof action === "function") {
  5. return action(dispatch, getState);
  6. }
  7. return next(action);
  8. }
  9. }
  10. };
compose

我知道compose是Redux的五大护法之一,可为什么挑在这个时候讲它呢?

先不告诉你。

compose在函数式编程中的含义是组合。假如你有一堆函数要依次执行,而且上一个函数的返回结果是下一个函数的参数,我们怎样写看起来最装逼?

</>复制代码

  1. const result = a(b(c(d(e("redux")))));

这种写法让人一眼就看穿了调用细节,装逼明显是不够的。

我们来看Redux是怎么实现compose的:

</>复制代码

  1. export default function compose(...funcs) {
  2. if (funcs.length === 0) {
  3. return arg => arg;
  4. }
  5. if (funcs.length === 1) {
  6. return funcs[0];
  7. }
  8. return funcs.reduce((a, b) => (...args) => a(b(...args)));
  9. }

诶,我看见reduce了,然后...就没有然后了。

假设我们现在有三个函数:

</>复制代码

  1. const funcA = arg => console.log("funcA", arg);
  2. const funcB = arg => console.log("funcB", arg);
  3. const funcC = arg => console.log("funcC", arg);

执行reduce的第一步返回的accumulator(accumulator是reduce中的概念),结果显而易见:

</>复制代码

  1. (...args) => funcA(funcB(...args));

执行reduce的第二步返回的accumulator,注意到,这时reduce已经执行完了,返回的是一个函数。

</>复制代码

  1. (...args) => funcA(funcB(funcC(...args)));

特别提醒:执行compose最终返回的是一个函数。也就是说开发者得这么干compose(a, b, c)()才能让传入的函数依次执行。

另外需要注意的是:传入的函数是从右到左依次执行的。

applyMiddleware

废话少说,先上源代码:

</>复制代码

  1. export default function applyMiddleware(...middlewares) {
  2. return createStore => (...args) => {
  3. const store = createStore(...args);
  4. let dispatch = () => {
  5. throw new Error(
  6. `Dispatching while constructing your middleware is not allowed. ` +
  7. `Other middleware would not be applied to this dispatch.`
  8. );
  9. };
  10. const middlewareAPI = {
  11. getState: store.getState,
  12. dispatch: (...args) => dispatch(...args),
  13. };
  14. const chain = middlewares.map(middleware => middleware(middlewareAPI));
  15. dispatch = compose(...chain)(store.dispatch);
  16. return { ...store, dispatch };
  17. }
  18. }

还记得中间件闭包好几层的写法吗?现在我们就来一层一层的剥开它。

middlewareAPI是一个对象,正好是传给第一层中间件函数的参数。执行它,返回的chain是由第二层函数组成的中间件数组。贴一下redux-thunk第二层转正后的样子:

</>复制代码

  1. function(next) {
  2. return function(action) {
  3. if (typeof action === "function") {
  4. return action(dispatch, getState);
  5. }
  6. return next(action);
  7. }
  8. }

中间件第二层函数接收一个next参数,那这个next具体指什么呢?我先透露一下,next是整个Redux中间件机制的题眼,理解了next就可以对Redux中间件的理解达到大彻大悟的化境。

之前我们已经拆解了compose的内部机制,从右到左执行,最右边的中间件的参数就是store.dispatch,它返回的值就是倒数第二个中间件的next。它返回什么呢?我们再剥一层:

</>复制代码

  1. function(action) {
  2. if (typeof action === "function") {
  3. return action(dispatch, getState);
  4. }
  5. return next(action);
  6. }

别看redux-thunk麻雀虽小,大家发现没有,第三层函数才是它的逻辑,前面两层都是配合redux的演出。也就是说呀同学们,除了最后一个中间件的next是原始的dispatch之外,倒数往前的中间件传入的next都是上一个中间件的逻辑函数。

</>复制代码

  1. Redux中间件本质上是将dispatch套上一层自己的逻辑。

最终applyMiddleware里得到的这个dispatch是经过无数中间件精心包装,植入了自己的逻辑的dispatch。然后用这个臃肿的dispatch覆盖原有的dispatch,将Store的API返回。

每一个Action就是这样穿过重重的逻辑代码才能最后被发射成功。只不过处理异步请求的中间件不再往下走,直到异步请求发生,真正的Action被发射出来,才会走到下一个中间件的逻辑。

构建dispatch过程中禁止执行dispatch

middlewareAPI中的dispatch为什么是一个抛出错误的函数?

我们现在已经知道,applyMiddleware的目的只有一个:用所有中间件组装成一个超级dispatch,并将它覆盖原生的dispatch。但是如果超级dispatch还没组装完成,就被中间件调用了原生的dispatch,那这游戏别玩了。

所以Redux来了一手掉包。

middlewareAPI初始传入的dispatch是一个炸弹,中间件的开发者胆敢在头两层闭包函数的外层作用域调用dispatch,炸弹就会引爆。而一旦超级dispatch构建完成,这个超级dispatch就会替换掉炸弹。

怎么替换呢?

函数也是引用类型对吧,炸弹dispatch之所以用let定义,就是为了将来修改它的引用地址:

</>复制代码

  1. let dispatch = () => {
  2. throw new Error(
  3. `Dispatching while constructing your middleware is not allowed. ` +
  4. `Other middleware would not be applied to this dispatch.`
  5. );
  6. };
  7. // ...
  8. dispatch = compose(...chain)(store.dispatch);

当然,这是对中间件开发者的约束,如果你只是一个中间件的使用者,这无关紧要。

applyMiddleware的花式调用

我们注意到,执行applyMiddleware返回的是一个函数,这个函数有唯一的参数createStore。

WTF?

applyMiddleware不是createStore的参数之一么:

</>复制代码

  1. const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));

怎么createStore也成了applyMiddleware的参数了?

贵圈真乱。

首先我们明确一点,applyMiddleware是一个增强器,增强器是需要改造Store的API的,这样才能达到增强Store的目的。所以applyMiddleware必须传入createStore以生成初始的Store。

所以生成一个最终的Store其实可以这样写:

</>复制代码

  1. const enhancedCreateStore = applyMiddleware(middleware1, middleware2, middleware3)(createStore);
  2. const store = enhancedCreateStore(reducer);

那通常的那种写法,Redux内部是怎么处理的呢?

</>复制代码

  1. if (typeof enhancer !== "undefined") {
  2. if (typeof enhancer !== "function") {
  3. throw new Error("Expected the enhancer to be a function.")
  4. }
  5. return enhancer(createStore)(reducer, preloadedState)
  6. }

上面是createStore源代码中的一段。

如果enhancer存在并且是一个函数,那么直接传入createStore执行,再传入reducer和preloadedState执行(这时候再传入enhancer就没完没了了),然后直接返回。

喵,后面还有好多代码呢,怎么就返回了?

不,就这么任性。

这么看下来,以下写法才是正宗的Redux:

</>复制代码

  1. const store = applyMiddleware(middleware1, middleware2, middleware3)(createStore)(reducer);

以下写法只是Redux为开发者准备的语法糖:

</>复制代码

  1. const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
洋葱圈模型

想必大家都听说过中间件的洋葱圈模型,这个比喻非常形象,乍听上去,啊,好像明白了。但是大家真的对洋葱圈模型有一个具象化的理解吗?

假设现在有三个中间件:

</>复制代码

  1. const middleware1 = ({ dispatch, getState }) => next => action => {
  2. console.log("middleware1 start");
  3. next(action);
  4. console.log("middleware1 end");
  5. }
  6. const middleware2 = ({ dispatch, getState }) => next => action => {
  7. console.log("middleware2 start");
  8. next(action);
  9. console.log("middleware2 end");
  10. }
  11. const middleware3 = ({ dispatch, getState }) => next => action => {
  12. console.log("middleware3 start");
  13. next(action);
  14. console.log("middleware3 end");
  15. }

现在将它传入applyMiddleware:

</>复制代码

  1. function reducer(state = {}, action) {
  2. console.log("reducer return state");
  3. return state;
  4. }
  5. const middlewares = [middleware1, middleware2, middleware3];
  6. const store = createStore(reducer, applyMiddleware(...middlewares));

我们看一下打印的结果:

</>复制代码

  1. middleware1 start
  2. middleware2 start
  3. middleware3 start
  4. reducer return state
  5. middleware3 end
  6. middleware2 end
  7. middleware1 end

对结果感到惊讶吗?其实理解函数调用栈的同学就能明白为什么是这样的结果。reducer执行之前也就是dispatch真正执行之前的日志好理解,dispatch被一层一层包装,一层一层的深入调用。但是dispatch执行完以后呢?这时候的执行权在调用栈最深的那一层逻辑那里,也就是最接近原始dispatch的逻辑函数那里,所以之后的执行顺序是从最深处往上调用。

总的看下来,一个Action的更新Store之旅就像穿过一个洋葱圈的旅行。一堆中间件簇拥着Action钻到洋葱的中心,Action执行自己的使命更新Store后就地圆寂,然后中间件带着它的遗志再从洋葱的中心钻出来。

回看compose

其实我解释上面的打印日志,还有一个关节没有打通。

记得applyMiddleware的源代码吗?内部调用了compose来执行chain。

我们强调过,compose的函数类型参数的执行顺序是从右到左的,我相信大家在不少的地方都见到过这样的表述。但是大家想过没有,为什么要从右到左执行?原生JavaScript除了实现reduce之外还有一个reduceRight,从左到右执行并没有什么技术障碍,那么为什么要让执行顺序这么别扭呢?

答案就在上面的打印日志里。

打印日志很好哇,根据传入的顺序执行。对,执行compose是从右到左,但是compose返回的终极dispatch是一层一层从外面包裹的呀,最后一个中间件也就是最左边的中间件的逻辑,包裹在最外面一层,自然它的日志最先被打印出来。

所以compose被设计成参数从右到左执行,不是有技术障碍,也不是Redux特立独行,而是其中本来就要经历一次反转,compose只有再反转一次才能将它扭转过来。

</>复制代码

  1. Redux专题一览
  2. 考古
    实用
    中间件
    时间旅行

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97998.html

相关文章

  • Redux专题:考古

    摘要:光凭一个是无法实现血缘关系疏远的组件之间的状态同步的。就是为解决这个问题而生的。,处理动作的派发,相当于架构的。我们的主角是,它也是目前社区最受欢迎的状态管理框架。专题一览考古实用中间件时间旅行 本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出来我的 GitHub repo 阅读完整的专题文章来我的 个人博客 获得无与伦比的阅读体验 React的横空出世给...

    toddmark 评论0 收藏0
  • React专题:react,redux以及react-redux常见一些面试题

    摘要:我们可以为元素添加属性然后在回调函数中接受该元素在树中的句柄,该值会作为回调函数的第一个参数返回。使用最常见的用法就是传入一个对象。单向数据流,比较有序,有便于管理,它随着视图库的开发而被概念化。 面试中问框架,经常会问到一些原理性的东西,明明一直在用,也知道怎么用, 但面试时却答不上来,也是挺尴尬的,就干脆把react相关的问题查了下资料,再按自己的理解整理了下这些答案。 reac...

    darcrand 评论0 收藏0
  • Redux专题:实用

    摘要:在英文中的意思是有效载荷。有一个动作被发射了顾名思义,替换,这主要是方便开发者调试用的。相同的输入必须返回相同的输出,而且不能对外产生副作用。怎么办呢开发者得手动维护一个订阅器,才能监听到状态变化,从而触发页面重新渲染。 本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出来我的 GitHub repo 阅读完整的专题文章来我的 个人博客 获得无与伦比的阅读体...

    Big_fat_cat 评论0 收藏0
  • React 328道最全面试题(持续更新)

    摘要:希望大家在这浮夸的前端圈里,保持冷静,坚持每天花分钟来学习与思考。 今天的React题没有太多的故事…… 半个月前出了248个Vue的知识点,受到很多朋友的关注,都强烈要求再出多些React相前的面试题,受到大家的邀请,我又找了20多个React的使用者,他们给出了328道React的面试题,由我整理好发给大家,同时发布在了前端面试每日3+1的React专题,希望对大家有所帮助,同时大...

    kumfo 评论0 收藏0
  • B站Up主-山地人-这位老哥2019年的前端自学计划进展如何?——讲一个B站Up主自学前端85天的故

    摘要:前言自从上次在掘金发布年山地人的前端完整自学计划讲一个站主山地人的天前端自学故事以来,一眨眼山地人老哥在站做主已经有天了。所以这个体系里的一些框架包括也是山地人年自学计划的一部分。月底,山地人老哥开启了的两个专题。 前言 自从上次在掘金发布【2019年山地人的前端完整自学计划——讲一个B站UP主山地人的40天前端自学故事】 以来,一眨眼山地人老哥在B站做Up主已经有85天了。 时隔一个...

    cocopeak 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<