历来如此,便对么?-----《狂人日记》html
在 react/redux 体系下的项目,在处理反作用(前端主要表如今异步问题)时,老是无脑上 redux-sage,前人铺的路老是香的,可是前人为何走这条路呢?今天咱们来挖挖他的前世此生。前端
咱们设想一下,若是不用 redux-sage,该如何处理异步问题哩?react
在开始以前,先讲讲为何出现 redux 。git
话说随着 web2.0 时代的到来,前端能够作愈来愈多的事情,为了解决日益复杂的前端单页应用,JS 须要管理愈来愈多的状态,多是服务端数据,多是UI状态,甚至是动效或者临时数据。而这些不断在动态变化的状态管理变得愈加地痛苦,开发者很难知道这些状态何时,因为什么缘由,如何地变化。github
redux 认为上述的复杂度来自于变化和异步。当这两点混合起来时,应用的维护与管理会变得没法预测,因此,须要一些方法来解决问题。web
而在计算机领域,复杂的东西每每能够经过状态机
这一律念获得明确与细化,我的以为 redux 的设计哲学是想将应用拆解为可描述状态
以及如何跳转
的状态机。编程
为了让应用的状态变化可预测,redux 作了一下三个原则约定:json
其中,action 本质是一个 JS 的普通对象,reducers 约定为纯函数。redux
ok,前戏结束,咱们回到正题。api
在同步操做的场景下,redux 接收到 actions 对象后,dispatch => reducer 同步算出 state,总体的流程运转以下:
这时,代码大概长这样:
1// action types
2export const ACTION_A = 'ACTION_A';
3export const ACTION_B = 'ACTION_B';
4
5// action creator
6export const actionA = (a) => ({
7 type: ACTION_A,
8 a
9});
10
11export const actionB = (b) => ({
12 type: ACTION_B,
13 b
14});
15
16// reducer
17export default function todos(state = initialState, action) {
18 switch (action.type) {
19 case ACTION_A:
20 return state.a
21 case ACTION_B:
22 return state.b
23
24 default:
25 return state
26 }
27}复制代码
可是异步场景怎么处理呢?咱们如何在一段异步任务结束后,根据他的结果分发不一样的 action 呢?
可能会写成这样?
1// action creator
2export const actionFetch = (id) => ({
3 type: ACTION_FETCH,
4 id
5});
6
7// reducer
8export function fetch(state = initialState, action) {
9 switch (action.type) {
10 case ACTION_FETCH:
11 fetch(`http://www.subreddit.com/r/${action.id}.json`)
12 .then(
13 response => response.json(),
14 )
15 .then(json =>
16 // 能够屡次 dispatch!
17 // 这里,使用 API 请求结果来更新应用的 state。
18
19 ...
20
21 return resState;
22 )
23
24 default:
25 return state
26 }
27}复制代码
等等,好像哪里不对,reducer 中处理反作用好像违背了纯函数的约定?
在下赵日天,今天就问问违背了会怎么样呢?
看看 redux 的官方解释:
突变是一种不鼓励的作法,由于它一般会打乱调试的过程,以及 React Redux 的
connect
函数:
- 对于调试过程, Redux DevTools 指望重放 action 记录时可以输出 state 值,而不会改变任何其余的状态。突变或者异步行为会产生一些反作用,可能使调试过程当中的行为被替换,致使破坏了应用。
- 对于 React Redux
connect
来讲,为了肯定一个组件(component)是否须要更新,它会检查从mapStateToProps
中返回的值是否发生改变。为了提高性能,connect
使用了一些依赖于不可变 state 的方法。而且使用浅引用(shallow reference)来检测状态的改变。这意味着直接修改对象或者数组是不会被检测到的而且组件不会被从新渲染。Redux 深受函数式编程的影响,创造性的不支持反作用的执行。尤为是 reducer,
必须是符合(state, action) => newState
的纯函数。然而,Redux 的 middleware 能拦截分发的 action 并添加额外的复杂行为,还能够添加反作用。
emmm,好像有那么点道理,那么,不在 reducer 中处理异步逻辑,只能在 action 中搞了?可是 action 不是只能是简单对象吗?
准确地来讲, reducer 只能处理简单对象的 action,因此,只能在 dispatch(action) 的过程当中作文章了。
这里就扯出来了 redux 的中间件机制 middleware
。
每一个 middleware 接受
Store
的dispatch
和getState
函数做为命名参数,并返回一个函数。该函数会被传入 被称为next
的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数能够直接调用next(action)
,或者在其余须要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的dispatch
方法做为next
参数,并借此结束调用链。因此,middleware 的函数签名是({ getState, dispatch }) => next => action
。
Middleware 最多见的使用场景是无需引用大量代码或依赖相似 Rx 的第三方库实现异步 actions。这种方式可让你像 dispatch 通常的 actions 那样 dispatch 异步 actions。
流程大概变成了这样:
React+Redux Cycle(来源:https://www.youtube.com/watch?v=1QI-UE3-0PU)
利用 redux 提供的中间件机制能够写成以下形式:
1//action creator
2export function fetchPosts(subreddit) {
3 return function (dispatch) {
4 dispatch(requestPosts(subreddit))
5
6 return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
7 .then(
8 response => response.json(),
9 )
10 .then(json =>
11 dispatch(FETCH_SUCCESS))
12 )
13 }
14}
15
16
17//reducer
18const reducer = function(oldState, action) {
19 switch(action.type) {
20 case FETCH_SUCCESS :
21 return successState;
22 case FETCH_FAILED :
23 return errorState;
24 }
25}复制代码
但是 dispatch 只能普通对象,还要继续改造 dispatch 吗?
固然能够,不过 redux-thunk 已经帮你作好了这件事。
redux-thunk 的思路是:将 action 由简单对象变为函数,在函数中处理反作用,利用 redux 的 applyMiddleware() 在dispatch 机制中接受 函数形态的 action,在 middleware 链中最后一个任务处理完后发布同步的 action 进入常规的同步过程。
1//action creator
2export function fetchPosts(subreddit) {
3 // 控制反转!
4 // 返回一个接收 `dispatch` 的函数。
5 // Thunk middleware 知道如何把异步的 thunk action 转为普通 action。
6 return function (dispatch) {
7 dispatch(requestPosts(subreddit))
8
9 return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
10 .then(
11 response => response.json(),
12 )
13 .then(json =>
14 dispatch(FETCH_SUCCESS))
15 )
16 }
17}
18
19
20//reducer
21const reducer = function(oldState, action) {
22 switch(action.type) {
23 case FETCH_SUCCESS :
24 return successState;
25 case FETCH_FAILED :
26 return errorState;
27 }
28}
29
30// store
31import { createStore, combineReducers, applyMiddleware } from 'redux';
32import thunk from 'redux-thunk';
33
34// applyMiddleware 为 createStore 注入了 middleware:
35let store = createStore(reducer, applyMiddleware(thunk))复制代码
看起来能够解决异步问题了,而且也是 redux 的做者 Dan 亲力亲为的做品,得力于官网的大力推荐,这个解决方案可谓带着光环出生,但,记得开头所说吗
咱们在上述的探索中其实一直围绕着一个根本的问题,反作用应该在哪里处理?
thunk 的作法虽然很讨巧,可是将反作用的处理过程分散到了 action creator,action 是用来描述动做是什么,并不该该关心什么条件下是什么,他没有这个职责,也不该该有这个职责。
另外,若是不一样的业务须要在相同的fetch后处理不一样的逻辑,就会产生不少的模板代码,而且时序的控制如何处理这些很难解决,毕竟异步的操做没办法保证谁先发就谁先返回,这些痛点概括以下:
对于“找位置”的问题,redux-saga 提出了另外一种解决方案。
在聊方案以前,先说说 saga 是什么?
这个翻译和红框是否是看蒙了?
大丈夫~,其实 saga 是为了解决分布式系统中的长时运行事务(LLT)的数据一致性的问题,出自康奈尔大学的一篇论文。连接
听的更懵逼了吧,我试着举个例子啊。
有一天 Mr.肖蓓泰 给他的女神求婚,女神说我最多10年给你答复吧~。那么若是女神10年内答应了肖蓓泰,就算成功了,不然就是失败了。这就是 LLT。由于事务就是要么全执行,要么全不执行。可是肖蓓泰要是一直等10年是否是有点惨啊,
而后康奈尔大学的 2 位情感专家看不下去了,他们劝肖蓓泰,莫使金樽空对月啊,少年。你能够答应等她(事务T1),而后去找其余女神(释放资源)。若是女神在10年内答应了,就结婚呗(事务 T2),这样 LLT 就结束了,
若是等了十年女神拒绝了,也没有其余女神答应你,那就不等了(事务 T3 回滚 T1)。这几个事务的组合就是一个 saga,固然,会有更多复杂的状况。复制代码
那这跟 redux-saga 有什么关系呢?
redux-saga 是一个用于管理反作用的 redux 中间件,能够想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理反作用。 redux-saga
是一个 redux 中间件,意味着这个线程能够经过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也能够 dispatch redux action。
个人理解:一个 sage 就是一组 行为 generator 的操做集合(事务),不一样的 saga 能够组合(LLT 由小事务组成),redux-sage 来统一调度他们。
你也能够经过 Background on the Saga concept 来产生本身的理解。
回到上面的的 thunk,咱们的代码用 saga 会实现成下面的样子:
1function* fetchPosts() {
2 yield put( actions.requestPosts() )
3 const products = yield call(fetchApi, '/products')
4 yield put( actions.receivePosts(products) )
5}
6
7function* watchFetch() {
8 yield takeLatest('FETCH_POSTS', fetchPosts)
9}复制代码
他的执行过程以下:
将反作用的处理抽离至 saga 层,action 维持原始的职责,对原始的 redux 流程侵入性很是低。各个模块的职责也相对清晰。
因为是使用 generator/iterator 来组织逻辑,拥有更为灵活的流程控制。本文只探讨他的设计理念,对自身包含的诸多的优势再也不赘述,可是灵活每每意味着复杂,其众多的 api 与抽象概念,对于初期开发者仍是学习曲线比较陡峭的。
好了,至此你们应该了解为何 saga 成为了异步处理的宠儿了,可是
但愿你们能够辩证地看待所谓的最佳实践,用本身的理解去探索世界,好比:redux-observable 了解一下呢? 🤪