-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
「8」Redux 进阶 - react 全家桶学习笔记(二) #9
Comments
good |
不错的文章 |
function applyMiddleware(store, middlewares) {
// ...
let dispatch = store.dispatch;
middlewares.forEach((middleware) =>
dispatch = middleware(store)(dispatch); // 注意调用了两次
);
// ...
} 这里的applyMiddleware 最后应该少了一句store.dispatch = dispatch吧 function applyMiddleware(store, middlewares) {
let dispatch = store.dispatch;
middlewares.forEach(middleware => dispatch = middleware(store)(dispatch));
store.dispatch = dispatch;
} |
@hemisu 你这么写也行,或者 return 出去,这里不是重点... |
@BuptStEve 额,我是跟着写下来,然后发现这一步的applyMiddleware不起作用了。 文章理的很详细,谢谢作者! |
@hemisu 😄 ,一切尽在 |
自己 debugger 吧... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
零、前言
在上一篇中介绍了 Redux 的各项基础 api。接着一步一步地介绍如何与 React 进行结合,并从引入过程中遇到的各个痛点引出 react-redux 的作用和原理。
不过目前为止还都是纸上谈兵,在日常的开发中最常见异步操作(如通过 ajax、jsonp 等方法 获取数据),在学习完上一篇后你可能依然没有头绪。因此本文将深入浅出地对于 redux 的进阶用法进行介绍。
一、中间件(MiddleWare)
这是 redux 作者对 middleware 的描述,middleware 提供了一个分类处理 action 的机会,在 middleware 中你可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,给你一次改变 action 的机会。
说得好像很吊...不过有啥用咧...?
1. 日志应用场景[2]
因为改变 store 的唯一方法就是 dispatch 一个 action,所以有时需要将每次 dispatch 操作都打印出来作为操作日志,这样一来就可以很容易地看出是哪一次 dispatch 导致了异常。
1.1. 第一次尝试:强行怼...
显然这种在每一个 dispatch 操作的前后都手动加代码的方法,简直让人不忍直视...
1.2. 第二次尝试:封装 dispatch
聪明的你一定马上想到了,不如将上述代码封装成一个函数,然后直接调用该方法。
矮油,看起来不错哟。
不过每次使用都需要导入这个额外的方法,一旦不想使用又要全部替换回去,好麻烦啊...
1.3. 第三次尝试:猴子补丁(Monkey Patch)
在此暂不探究为啥叫猴子补丁而不是什么其他补丁。
简单来说猴子补丁指的就是:以替换原函数的方式为其添加新特性或修复 bug。
这样一来我们就“偷梁换柱”般的为原 dispatch 添加了输出日志的功能。
1.4. 第四次尝试:隐藏猴子补丁
目前看起来很不错,然鹅假设我们又要添加别的一个中间件,那么代码中将会有重复的
let next = store.dispatch;
代码。对于这个问题我们可以通过参数传递,返回新的 dispatch 来解决。
注意到最后应用中间件的代码其实就是一个链式的过程,所以还可以更进一步优化绑定中间件的过程。
so far so good~! 现在不仅隐藏了显式地缓存原 dispatch 的代码,而且调用起来也很优雅~,然鹅这样就够了么?
1.5. 第五次尝试:移除猴子补丁
注意到,以上写法仍然是通过
store.dispatch = middleware(store);
改写原方法,并在中间件内部通过const next = store.dispatch;
读取当前最新的方法。本质上其实还是 monkey patch,只不过将其封装在了内部,不过若是将 dispatch 方法通过参数传递进来,这样在 applyMiddleware 函数中就可以暂存 store.dispatch(而不是一次又一次的改写),岂不美哉?
接着应用函数式编程的 curry 化(一种使用匿名单参数函数来实现多参数函数的方法。),还可以再进一步优化。(其实是为了使用 compose 将中间件函数先组合再绑定)
以上方法离 Redux 中最终的 applyMiddleware 实现已经很接近了,
1.6. 第六次尝试:组合(compose,函数式方法)
在 Redux 的最终实现中,并没有采用我们之前的
slice + reverse
的方法来倒着绑定中间件。而是采用了map + compose + reduce
的方法。先来说这个 compose 函数,在数学中以下等式十分的自然。
用代码来表示这一过程就是这样。
不了解 reduce 函数的人可能对于以上代码会感到有些费解,举个栗子来说,有函数数组 [f, g, h]传入 compose 函数执行。
(...args) => f(g(...args))
a
,而参数 b 是h
h(...args)
作为参数传入 a,即最后返回的还是一个函数(...args) => f(g(h(...args)))
因此最终版 applyMiddleware 实现中并非依次执行绑定,而是采用函数式的思维,将作用于 dispatch 的函数首先进行组合,再进行绑定。(所以要中间件要 curry 化)
综上如下图所示整个中间件的执行顺序是类似于洋葱一样首先按照从外到内的顺序执行 dispatch 之前的中间件代码,在 dispatch(洋葱的心)执行后又反过来,按照从内到左外的顺序执行 dispatch 之后的中间件代码。
你真的都理解了么?
1.7. middleware 中调用 store.dispatch[6]
正常情况下,如图左,当我们 dispatch 一个 action 时,middleware 通过 next(action) 一层一层处理和传递 action 直到 redux 原生的 dispatch。如果某个 middleware 使用 store.dispatch(action) 来分发 action,就发生了右图的情况,相当于从外层重新来一遍,假如这个 middleware 一直简单粗暴地调用 store.dispatch(action),就会形成无限循环了。(其实就相当于猴子补丁没补上,不停地调用原来的函数)
因此最终版里不是直接传递 store,而是传递 getState 和 dispatch,传递 getState 的原因是可以通过 getState 获取当前状态。并且还将 dispatch 用一个匿名函数包裹
dispatch: (action) => dispatch(action)
,这样不但可以防止 dispatch 被中间件修改,而且只要 dispatch 更新了,middlewareAPI 中的 dispatch 也会随之发生变化。1.8. createStore 进阶
在上一篇中我们使用 createStore 方法只用到了它前两个参数,即 reducer 和 preloadedState,然鹅其实它还拥有第三个参数 enhancer。
enhancer 参数可以实现中间件、时间旅行、持久化等功能,Redux 仅提供了 applyMiddleware 用于应用中间件(就是 1.6. 中的那个)。
在日常使用中,要应用中间件可以这么写。
在上文 applyMiddleware 的实现中留了个悬念,就是为什么返回的是一个函数,因为 enhancer 被定义为一个高阶函数,接收 createStore 函数作为参数。
总的来说 Redux 有五个 API,分别是:
createStore 生成的 store 有四个 API,分别是:
以上 API 我们还没介绍的应该就剩 bindActionCreators 了。这个 API 其实就是个语法糖起了方便地给 action creator 绑定 dispatch 的作用。
二、异步操作
下面让我们告别干净的同步世界,进入“肮脏”的异步世界~。
2.1. 通知应用场景[3]
现在有这么一个显示通知的应用场景,在通知显示后5秒钟隐藏该通知。
首先当然是编写 action
2.1.1. 最直观的写法
最直观的写法就是首先显示通知,然后使用 setTimeout 在5秒后隐藏通知。
然鹅,一般在组件中尤其是展示组件中没法也没必要获取 store,因此一般将其包装成 action creator。
或者更进一步地先使用 connect 方法包装。
到目前为止,我们没有用任何 middleware 或者别的概念。
2.1.2. 异步 action creator
上一种直观写法有一些问题
所以为了解决以上问题,我们可以为通知加上 id,并将显示和消失的代码包起来。
为啥
showNotificationWithTimeout
函数要接收dispatch
作为第一个参数呢?虽然通常一个组件都拥有触发 dispatch 的权限,但是现在我们想让一个外部函数(showNotificationWithTimeout)来触发 dispatch,所以需要将 dispatch 作为参数传入。
2.1.3. 单例 store
可能你会说如果有一个从其他模块中导出的单例 store,那么是不是同样也可以不传递 dispatch 以上代码也可以这样写。
这样看起来似乎更简单一些,不过墙裂不推荐这样的写法。主要的原因是这样的写法强制让 store 成为一个单例。这样一来要实现服务器端渲染(Server Rendering)将十分困难。因为在服务端,为了让不同的用户得到不同的预先获取的数据,你需要让每一个请求都有自己的 store。
并且单例 store 也将让测试变得困难。当测试 action creator 时你将无法自己模拟一个 store,因为它们都引用了从外部导入的那个特定的 store,所以你甚至无法从外部重置状态。
2.1.4. redux-thunk 中间件
首先声明 redux-thunk 这种方案对于小型的应用来说足够日常使用,然鹅对于大型应用来说,你可能会发现一些不方便的地方。(例如对于 action 需要组合、取消、竞争等复杂操作的场景)
首先来明确什么是 thunk...
简单来说 thunk 就是封装了表达式的函数,目的是延迟执行该表达式。不过有啥应用场景呢?
目前为止,在上文中的 2.1.2. 异步 action creator 部分,最后得出的方案有以下明显的缺点
其实问题的本质在于 Redux “有眼不识 function”,目前为止 dispatch 函数接收的参数只能是 action creator 返回的普通的 action。~~所以如果我们让 dispatch 对于 function 网开一面,走走后门潜规则一下不就行啦~~~
实现方式很简单,想想第一节介绍的为 dispatch 添加日志功能的过程。
以上就是 redux-thunk 的源码,就是这么简单,判断下如果传入的 action 是函数的话,就执行这个函数...(withExtraArgument 是为了添加额外的参数,详情见 redux-thunk 的 README.md)
添加了 redux-thunk 中间件后代码可以这么写。
2.2. 接口应用场景
目前我们对于简单的延时异步操作的处理已经了然于胸了,现在让我们来考虑一下通过 ajax 或 jsonp 等接口来获取数据的异步场景。
很自然的,我们会发起一个请求,然后等待请求的响应(请求可能成功或是失败)。
即有基本的三种状态和与之对应的 action:
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
按照这个思路,举一个简单的栗子。
尽管这已经是最简单的调用接口场景,我们甚至还没写一行业务逻辑代码,但讲道理的话代码还是比较繁琐的。
而且其实代码是有一定的“套路”的,比如其实整个代码都是针对请求、成功、失败三部分来处理的,这让我们自然联想到 Promise,同样也是分为 pending、fulfilled、rejected 三种状态。
那么这两者可以结合起来让模版代码精简一下么?
2.2.1. redux-promise 中间件[8]
首先开门见山地使用 redux-promise 中间件来改写之前的代码看看效果。
可以看出 redux-promise 中间件比较激进、比较原教旨。
不但将发起请求的初始状态被拦截了(原因见下文源码),而且使用 action.status 而不是 action.type 来区分两个 action 这一做法也值得商榷(个人倾向使用 action.type 来判断)。
以上是 redux-promise 的源码,十分简单。主要逻辑是判断如果是 Promise 就执行 then 方法。此外还根据是不是 FSA 决定调用的是 action 本身还是 action.payload 并且对于 FSA 会自动 dispatch 成功和失败的 FSA。
2.2.2. redux-promise-middleware 中间件
尽管 redux-promise 中间件节省了大量代码,然鹅它的缺点除了拦截请求开始的 action,以及使用 action.status 来判断成功失败状态以外,还有就是由此引申出的一个无法实现的场景————乐观更新(Optimistic Update)。
乐观更新比较直观的栗子就是在微信、QQ等通讯软件中,发送的消息立即在对话窗口中展示,如果发送失败了,在消息旁边展示提示即可。由于在这种交互方式中“乐观”地相信操作会成功,因此称作乐观更新。
因为乐观更新发生在用户发起操作时,所以要实现它,意味着必须有表示用户初始动作的 action。
因此为了解决这些问题,相对于比较原教旨的 redux-promise 来说,更加温和派一点的 redux-promise-middleware 中间件应运而生。先看看代码怎么说。
如果不需要乐观更新,fetchPosts 函数可以更加简洁。
相对于 redux-promise 简单粗暴地直接过滤初始 action,从 reducer 可以看出,redux-promise-middleware 会首先自动触发一个 FETCH_POSTS_PENDING 的 action,以此保留乐观更新的能力。
并且,在状态的区分上,回归了通过 action.type 来判断状态的“正途”,其中
_PENDING
、_FULFILLED
、_REJECTED
后缀借用了 Promise 规范 (当然它们是可配置的) 。源码地址点我,类似 redux-promise 也是在中间件中拦截了 payload 中有 Promise 的 action,并主动 dispatch 三种状态的 action,注释也很详细在此就不赘述了。
2.3. redux-loop 中间件
简单小结一下,Redux 的数据流如下所示:
UI => action => action creator => reducer => store => react => v-dom => UI
redux-thunk 的思路是保持 action 和 reducer 简单纯粹,然鹅副作用操作(在前端主要体现在异步操作上)的复杂度是不可避免的,因此它将其放在了 action creator 步骤,通过 thunk 函数手动控制每一次的 dispatch。
redux-promise 和 redux-promise-middleware 只是在其基础上做一些辅助性的增强,处理异步的逻辑本质上是相同的,即将维护复杂异步操作的责任推到了用户的身上。
这种实现方式固然很好理解,而且理论上可以应付所有异步场景,但是由此带来的问题就是模版代码太多,一旦流程复杂那么异步代码就会到处都是,很容易导致出现 bug。
因此有一些其他的中间件,例如 redux-loop 就将异步处理逻辑放在 reducer 中。(Redux 的思想借鉴了 Elm,注意并不是“饿了么”,而 Elm 就是将异步处理放在 update(reducer) 层中)。
redux-loop 认为许多其他的处理异步的中间件,尤其是通过 action creator 方式实现的中间件,错误地让用户认为异步操作从根本上与同步操作并不相同。这样一来无形中鼓励了中间件以许多特殊的方式来处理异步状态。
与之相反,redux-loop 专注于让 reducer 变得足够强大以便处理同步和异步操作。在具体实现上 reducer 不仅能够根据特定的 action 决定当前的转换状态,而且还能决定接着发生的操作。
应用中所有行为都可以在一个地方(reducer)中被追踪,并且这些行为可以轻易地分割和组合。(redux 作者 Dan 开了个至今依然 open 的 issue:Reducer Composition with Effects in JavaScript,讨论关于对 reducer 进行分割组合的问题。)
redux-loop 模仿 Elm 的模式,引入了 Effect 的概念,在 reducer 中对于异步等操作使用 Effect 来处理。如下官方示例所示:
虽然这个想法很 Elm 很函数式,不过由于修改了 reducer 的返回类型,这样一来会导致许多已有的 Api 和第三方库无法使用,甚至连 redux 库中的 combineReducers 方法都需要使用 redux-loop 提供的定制版本。因此这也是 redux-loop 最终无法转正的原因:
三、复杂异步操作
3.1. 更复杂的通知场景[9]
让我们的思路重新回到通知的场景,之前的代码实现了:
现在假设可亲可爱的产品又提出了新需求:
这个当然可以实现,只不过如果只用之前的 redux-thunk 实现起来会很麻烦。例如可以在 store 中增加两个数组分别表示当前展示列表和等待队列,然后在 reducer 中手动控制各个状态时这俩数组的变化。
3.2. redux-saga 中间件
首先来看看使用了 redux-saga 后代码会变成怎样~(代码来自生产环境的某 app)
先不要在意代码的细节,简单分析一下上述代码的逻辑:
基于这样逻辑分离的写法,还可以继续满足更加复杂的需求:
redux-saga V.S. redux-thunk[11]
redux-saga 的优点:
redux-saga 的缺点:
3.3. 理解 Saga Pattern[14]
3.3.1. Saga 是什么
Sagas 的概念来源于这篇论文,该论文从数据库的角度谈了 Saga Pattern。
暂且不提这个特定条件是什么,首先一般学过数据库的都知道事务(Transaction)是啥~
3.3.2. 长事务的问题
一般来说是通过给正在进行事务操作的对象加锁,来保证事务并发时不会出错。
例如 A 和 B 都给 C 转 100 块钱。
然鹅,对于长事务来说总不能一直锁住对应数据吧?
为了解决这个问题,假设一个长事务:T,
可以被拆分成许多相互独立的子事务(subtransaction):t_1 ~ t_n。
假如每次购票都一次成功,且没有退票的话,整个流程就如下图一般被正常地执行。
那假如有某次购票失败了怎么办?
3.3.3. Saga 的特殊条件
Saga 通过引入补偿事务(Compensating Transaction)的概念,解决事务失败的问题。
即任何一个 saga 中的子事务 t_i,都有一个补偿事务 c_i 负责将其撤销(undo)。
根据以上逻辑,可以推出很简单的公式:
t_1, t_2, t_3, ..., t_n
t_1, t_2, t_3, ..., t_n, c_n, ..., c_1
篇幅有限在此就不继续深入介绍...
3.4. 响应式编程(Reactive Programming)[15]
redux-saga 中间件基于 Sagas 的理论,通过监听 action,生成对应的各种子 saga(子事务)解决了复杂异步问题。
而接下来要介绍的 redux-observable 中间件背后的理论是响应式编程(Reactive Programming)。
简单来说,响应式编程是针对异步数据流的编程并且认为:万物皆流(Everything is Stream)。
流(Stream)就是随着时间的流逝而发生的一系列事件。
例如点击事件的示意图就是这样。
用字符表示【上上下下左右左右BABA】可以像这样。(注意顺序是从左往右)
那么我们要根据一个点击流来计算点击次数的话可以这样。(一般响应式编程库都会提供许多辅助方法如 map、filter、scan 等)
如上所示,原始的 clickStream 经过 map 后产生了一个新的流(注意原始流不变),再对该流进行 scan(+) 的操作就生成了最终的 counterStream。
再来个栗子~,假设我们需要从点击流中得到关于双击的流(250ms 以内),并且对于大于两次的点击也认为是双击。先想一想应该怎么用传统的命令式、状态式的方式来写,然后再想想用流的思考方式又会是怎么样的~。
这里我们用了以下辅助方法:
更多内容请继续学习 RxJS。
3.5. redux-observable 中间件[16]
redux-observable 就是一个使用 RxJS 监听每个 action 并将其变成可观测流(observable stream)的中间件。
其中最核心的概念叫做 epic,就是一个监听流上 action 的函数,这个函数在接收 action 并进行一些操作后可以再返回新的 action。
redux-observable 通过在后台执行
.subscribe(store.dispatch)
实现监听。Epic 像 Saga 一样也是 Long Lived,即在应用初始化时启动,持续运行到应用关闭。虽然 redux-observable 是一个中间件,但是类似于 redux-saga,可以想象它就像新开的进/线程,监听着 action。
在这个运行流程中,epic 不像 thunk 一样拦截 action,或阻止、改变任何原本 redux 的生命周期的其他东西。这意味着每个 dispatch 的 action 总会经过 reducer 处理,实际上在 epic 监听到 action 前,action 已经被 reducer 处理过了。
所以 epic 的功能就是监听所有的 action,过滤出需要被监听的部分,对其执行一些带副作用的异步操作,然后根据你的需要可以再发射一些新的 action。
举个自动保存的栗子,界面上有一个输入框,每次用户输入了数据后,去抖动后进行自动保存,并在向服务器发送请求的过程中显示正在保存的 UI,最后显示成功或失败的 UI。
使用 redux-observable 中间件编写代码,可以仅用十几行关键代码就实现上述功能。
篇幅有限在此就不继续深入介绍...
四、总结
本文从为 Redux 应用添加日志功能(记录每一次的 dispatch)入手,引出 redux 的中间件(middleware)的概念和实现方法。
接着从最简单的 setTimeout 的异步操作开始,通过对比各种实现方法引出 redux 最基础的异步中间件 redux-thunk。
针对 redux-thunk 使用时模版代码过多的问题,又介绍了用于优化的 redux-promise 和 redux-promise-middleware 两款中间件。
由于本质上以上中间件都是基于 thunk 的机制来解决异步问题,所以不可避免地将维护异步状态的责任推给了开发者,并且也因为难以测试的原因。在复杂的异步场景下使用起来难免力不从心,容易出现 bug。
所以还简单介绍了一下将处理副作用的步骤放到 reducer 中并通过 Effect 进行解决的 redux-loop 中间件。然鹅因为其无法使用官方 combineReducers 的原因而无法被纳入 redux 核心代码中。
此外社区根据 Saga 的概念,利用 ES6 的 generator 实现了 redux-saga 中间件。虽然通过 saga 函数将业务代码分离,并且可以用同步的方式流程清晰地编写异步代码,但是较多的新概念和 generator 的语法可能让部分开发者望而却步。
同样是基于观察者模式,通过监听 action 来处理异步操作的 redux-observable 中间件,背后的思想是响应式编程(Reactive Programming)。类似于 saga,该中间件提出了 epic 的概念来处理副作用。即监听 action 流,一旦监听到目标 action,就处理相关副作用,并且还可以在处理后再发射新的 action,继续进行处理。尽管在处理异步流程时同样十分方便,但对于开发者的要求同样很高,需要开发者学习关于函数式的相关理论。
五、参考资料
以上 to be continued...
The text was updated successfully, but these errors were encountered: