We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
新知识很多,且学且珍惜。
在选择要系统地学习一个新的 __框架/库 __之前,首先至少得学会先去思考以下两点:
然后,才会带着更多的好奇心去了解:它的由来、它名字的含义、它引申的一些概念,以及它具体的使用方式...
本文尝试通过 自我学习/自我思考 的方式,谈谈对 redux-saga 的学习和理解。
『Redux-Saga』是一个 库(Library),更细致一点地说,大部分情况下,它是以 Redux 中间件 的形式而存在,主要是为了更优雅地 管理 Redux 应用程序中的 副作用(Side Effects)。
那么,什么是 Side Effects?
来看看 Wikipedia 的专业解释(敲黑板,划重点):
Side effects are the most common way that a program interacts with the outside world (people, filesystems, other computers on networks).
映射在 Javascript 程序中,Side Effects 主要指的就是:异步网络请求、__本地读取 localStorage/Cookie __等外界操作:
Asynchronous things like__ data fetching__ and impure things like accessing the browser cache
虽然中文上翻译成 “副作用”�,但并不意味着不好,这完全取决于特定的 Programming Paradigm(编程范式),比如说:
Imperative programming is known for its frequent utilization of side effects.
所以,在 Web 应用,侧重点在于 Side Effects 的 优雅管理(manage),而不是 消除(eliminate)。
说到这里,很多人就会有疑问:相比于 redux-thunk 或者 redux-promise, 同样在处理 Side Effects(比如:异步请求)的问题上,redux-saga 会有什么优势?
这里是指 redux-saga vs redux-thunk。
首先,从简单的字面意义就能看出:背后的思想来源不同 —— Thunk vs Saga Pattern。
这里就不展开讲述了,感兴趣的同学,推荐认真阅读以下两篇文章:
其次,再从程序的角度来看:使用方式上的不同。
Note:以下示例会省去部分 Redux 代码,如果你对 Redux 相关知识还不太了解,那么《Redux 卍解》了解一下。
一般情况下,actions 都是符合 FSA 标准的(即:a plain javascript object),像下面这样:
{ type: 'ADD_TODO', payload: { text: 'Do something.' } };
它代表的含义是:每次执行 dispatch(action) 会通知 reducer ++将 action.payload(数据) 以 action.type 的方式(操作)++__同步更新__到 本地 store 。
dispatch(action)
而一个 丰富多变的 Web 应用,payload 数据往往来自于远端服务器,为了能将 __异步获取数据 __这部分代码跟 UI 解耦,redux-thunk 选择以 middleware 的形式来增强 redux store 的 dispatch 方法(即:支持了 dispatch(function)),从而在拥有了 ++异步获取数据能力++ 的同时,又可以进一步将 ++数据获取相关的业务逻辑++ 从 View 层分离出去。
dispatch(function)
来看看以下代码:
// action.js // --------- // actionCreator(e.g. fetchData) 返回 function // function 中包含了业务数据请求代码逻辑 // 以回调的方式,分别处理请求成功和请求失败的情况 export function fetchData(someValue) { return (dispatch, getState) => { myAjaxLib.post("/someEndpoint", { data: someValue }) .then(response => dispatch({ type: "REQUEST_SUCCEEDED", payload: response }) .catch(error => dispatch({ type: "REQUEST_FAILED", error: error }); }; } // component.js // ------------ // View 层 dispatch(fn) 触发异步请求 // 这里省略部分代码 this.props.dispatch(fetchData({ hello: 'saga' }));
如果同样的功能,用 redux-saga 如何实现呢?它的优势在哪里?
先来看下代码,大致感受下(后面会细讲):
// saga.js // ------- // worker saga // 它是一个 generator function // fn 中同样包含了业务数据请求代码逻辑 // 但是代码的执行逻辑:看似同步 (synchronous-looking) function* fetchData(action) { const { payload: { someValue } } = action; try { const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue }); yield put({ type: "REQUEST_SUCCEEDED", payload: response }); } catch (error) { yield put({ type: "REQUEST_FAILED", error: error }); } } // watcher saga // 监听每一次 dispatch(action) // 如果 action.type === 'REQUEST',那么执行 fetchData export function* watchFetchData() { yield takeEvery('REQUEST', fetchData); } // component.js // ------- // View 层 dispatch(action) 触发异步请求 // 这里的 action 依然可以是一个 plain object this.props.dispatch({ type: 'REQUEST', payload: { someValue: { hello: 'saga' } } });
将从上面的代码,与之前的进行对比,可以归纳以下几点:
最简单完整的一个单向数据流,从 hello saga 说起。
先来看看,如何将 store 和 saga 关联起来?
import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import rootSaga from './sagas'; import rootReducer from './reducers'; // 创建 saga middleware const sagaMiddleware = createSagaMiddleware(); // 注入 saga middleware const enhancer = applyMiddleware(sagaMiddleware); // 创建 store const store = createStore(rootReducer, /* preloadedState, */ enhancer); // 启动 saga sagaMiddleWare.run(rootSaga);
代码分析:
createSagaMiddleware
store.dispatch(action)
整合以上分析:程序启动时,run(rootSaga) 会开启 sagaMiddleware 对某些 action 进行监听,当后续程序中有触发 dispatch(action) (比如:用户点击)的时候,由于数据流会经过 sagaMiddleware,所以 sagaMiddleware 能够判断当前 action 是否有被监听?如果有,就会进行相应的操作(比如:发送一个异步请求);如果没有,则什么都不做。
run(rootSaga)
所以来看看,初始化程序时,rootSaga 具体可以做些什么?
// sagas/index.js import { fork, takeEvery�, put } from 'redux-saga/effects'; import { push } from 'react-router-redux'; import ajax from '../utils/ajax'; export default function* rootSaga() { // 初始化程序(欢迎语 :-D) console.log('hello saga'); // 首次判断用户是否登录 yield fork(function* fetchLogin() { try { // 异步请求用户信息 const user = yield call(ajax.get, '/userLogin'); if (user) { // 将用户信息存入 本地 store yield put({ type: 'UPDATE_USER', payload: user }) } else { // 路由跳转到 403 页面 yield put(push('/403')); } } catch (e) { // 请求异常 yield put(push('/500')); } }); // watcher saga 监听 dispatch 传过来的 action // 如果 action.type === 'FETCH_POSTS' 那么 请求帖子列表数据 yield takeEvery('FETCH_POSTS', function* fetchPosts() { // 从 store 中获取用户信息 const user = yield select(state => state.user); if (user) { // TODO: 获取当前用户发的帖子 } }); }
如同前面所说,rootSaga 里面的代码会在程序启动时,会依次被执行:
takeEvery
{ type: 'FETCH\_POSTS' }
PS:通常情况下,在无需进行 saga 按需加载 的情况下,rootSaga 里会集中 引入并注册 程序中所有用到的 watcher saga(就像 combine rootReducer 那样)。
最后再看看,程序启动后,一个完整的单向数据流是如何形成的?
import React from 'react'; import { connect } from 'react-redux'; // 关联 store 中 state.posts 字段 (即:帖子列表数据) @connect(({ posts }) => ({ posts })) class App extends React.PureComponent { componentDidMount() { // dispatch(action) 触发数据请求 this.props.dispatch({ type: 'FETCH_POSTS' }); } render() { const { posts = [] } = this.props; return ( <ul> { posts.map((post, index) => (<li key={index}>{ post.title }</li>)) } </ul> ); } } export default App;
当组件 <App /> 被执行挂载后,通过 dispatch({ type: 'FETCH\_POSTS' }) 通知 sagaMiddleware 寻找到 匹配的 watcher saga 后,执行对应的 woker saga,从而发起数据异步请求 ...... 最终 <App/> 会在得到最新 posts 数据后,执行 re-render 更新 UI。
<App />
dispatch({ type: 'FETCH\_POSTS' })
<App/>
至此,以上三个部分代码实现了基于 redux-saga 的一次 __完整单向数据流,__如果用一张图来表现的话 ,应该是这样:
文章看到这里,对于一个 redux-saga 新手而言,可能会留有这样的疑惑: 上述代码中 put/call/fork/takeEvery 这些方法是干什么用的?这就是接下来要详细讨论的 saga effects。
前面说到,saga 是一个 generator function,这就意味着它的执行原理必然是下面这样:
function isPromise(value) { return value && typeof value.then === 'function'; } const iterator = saga(/* ...args */); // 方法一: // 一步一步,手动执行 let result; result = iterator.next(); result = iterator.next(result.value); result = iterator.next(result.value); // ... // done!! // 方法二: // 函数封装,自主执行 function next(args) { const result = iterator.next(args); if (result.done) { // 执行结束 console.log(result.value); } else { // 根据 yielded 的值,决定什么时候继续执行(resume) if (isPromise(result.value)) { result.value.then(next); } else { next(result.value) } } } next();
也就是说,generator function 在未执行完前(即:result.done === false),它的控制权始终掌握在__ 执行者(caller)__手中,即:
而 caller 本身要实现上面上述功能需要依赖原生 API :iterator.next(value) ,value 就是 yield expression 的返回值。
iterator.next(value)
举个例子:
function* gen() { const value = yield Promise.reslove('hello saga'); console.log('value: ', value); // value?? }
单纯的看 gen 函数,没人知道 value 的值会是多少?
这完全取决于 gen 的执行者(caller),如果使用上面的 next 方法来执行它,value 的值就是 'hello saga',因为 next 方法对 expression 为 promise 时,做了特殊处理(这不就是缩小版的 co 么~ wow~⊙o⊙)。
换句话说,expression 可以是任何值,关键是 caller 如何来解释 expression,并返回合理的值 !
以此结论,推理来看:
讲了这么多,那么 effect 到底是什么呢?先来看看官方解释:
An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware.
意思是说:effect 本质上是一个普通对象,包含着一些指令信息,这些指令最终会被 saga middleware 解释并执行。
用一段代码来解释上述这句话:
function* fetchData() { // 1. 创建 effect const effect = call(ajax.get, '/userLogin'); console.log('effect: ', effect); // effect: // { // CALL: { // context: null, // args: ['/userLogin'], // fn: ajax.get, // } // } // 2. 执行 effect,即:调用 ajax.get('/userLogin') const value = yield effect; console.log('value: ', value); }
可以明显的看出:
这里的 __call effect __表示执行 ajax.get('user/Login') ,又因为它的返回值是 promise, 为了等待异步结果返回,fetchData 函数会暂时处于 阻塞 状态。
ajax.get('user/Login')
除了上述所说的 call effect 之外,redux-saga 还提供了很多其他 effect 类型,它们都是由对应的 effect factory 生成,在 saga 中应用于不同的场景,比较常用的是:
其中,比较难以理解的就属:如何区分 call 和 fork?什么是阻塞/非阻塞?这是接下来要讲的。
前面已经提到,saga 中 call 和 fork 都是用来执行指定函数 fn,区别在于:
举个例子,假设 fn 函数返回一个 promise:
// 模拟数据异步获取 function fn() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('hello saga'); }, 2000); }); } function* fetchData() { // 等待 2 秒后,打印欢迎语(阻塞) const greeting = yield call(fn); console.log('greeting: ', greeting); // 立即打印 task 对象(非阻塞) const task = yield fork(fn); console.log('task: ', task); }
显然,fork 的异步非阻塞特性更适合于在后台运行一些不影响主流程的代码(比如:后台打点/开启监听),这往往是加快页面渲染的一种方式,有点类似于 Egg 的 runInBackground,倘若在这种情况下,你依然要获取返回结果,可以这样做:
const task = yield fork(fn); // 0.16.0 api task.done().then((greeting) => { console.log('greeting: ', greeting); }); // 1.0.0-beta.0 api task.toPromise().then((greeting) => { console.log('greeting: ', greeting); });
PS:这里的函数 fn 是一个 normal function,其实它还可以是一个 generator function(被称作是 Child Saga)。
最后的最后,再简单聊聊 saga 中的错误处理方式?
在 saga 中,无论是请求失败,还是代码异常,均可以通过 try catch 来捕获。
倘若访问一个接口出现代码异常,可能是网络请求问题,也可能是后端数据格式问题,但不管怎样,给予日志上报或友好的错误提示是不可缺少的,这也往往体现了代码的健壮性,一般会这么做:
function* saga() { try { const data = yield call(fetch, '/someEndpoint'); return data; } catch(e) { // 日志上报 logger.error('request error: ', e); // 错误提示 antd.message.error('请求失败'); } }
这是最正确的处理方式,但这里更想讨论的是:++如果忘记写 try catch 进行异常捕获,结果会怎么样?++
就好比下面这样:
function* saga1 () { /* ... */ } function* saga2 () { throw new Error('模拟异常'); } function* saga3 () { /* ... */ } function* rootSaga() { yield fork(saga1); yield fork(saga2); yield fork(saga3); } // 启动 saga sagaMiddleware.run(rootSaga);
假设 saga2 出现代码异常了,且没有进行异常捕获,这样的异常会导致整个 Web App 崩溃么?答案是:肯定的!
来具体解释下:
redux-saga 中执行 sagaMiddleware.run(rootsaga) 或 fork(saga) 时,均会返回一个 task 对象(上文中说到),嵌套的 task 之间会存在__ 父子关系,__就比如上述代码:
sagaMiddleware.run(rootsaga)
fork(saga)
现在某一个 childTask 异常了(比如这里的: saga2),那么它的 parentTask(如:rootTask)收到通知先会执行自身的 cancel 操作,再通知其他 childTask(如:saga1,saga3) 同样执行 cancel 操作。(这其实正是 Saga Pattern 的思想)
但这就意味着,用户可能会因为一个按钮点击引发的异常,而导致整个 Web 应用的功能均无法使用!!
那么,面对这样的问题,如何优化呢?隔离 childTask 是首先想到的一种方案。
export default function* root() { yield spawn(saga1); yield spawn(saga2); yield spawn(saga3); }
使用 spawn 替换 fork,它们的区别在于 spawn 返回 __ isolate task__,不存在 父子关系,也就是说,即使 saga2 挂了,rootSaga 也不受影响,saga1 和 saga3 自然更不会受影响,依然可以正常工作。
但这样的方案并不是让人最满意的!如果因为某一次网络原因,导致 saga2 挂了,在不刷新页面的情况下,用户连重试的机会都不给,显然是不合理的,那么如果可以做到 saga 自动重启呢?社区里已经有一个比较好的方案了:
function* rootSaga () { const sagas = [ saga1, saga2, saga3 ]; yield sagas.map(saga => spawn(function* () { while (true) { try { yield call(saga); } catch (e) { console.log(e); } } }) ); }
上述代码通过在最上层为每一个 childSaga 添加异常捕获,并通过 while(true) {} 循环自动创建新的 childTask 取代 异常 childTask,以保证功能依然可用(这就类似于 Egg 中某一个 woker 进程 挂了,自动重启一个新的 woker 进程一样)。
while(true) {}
OK,差不多就先讲这些吧... 完!
The text was updated successfully, but these errors were encountered:
写的很好,知其然也知其所以然
Sorry, something went wrong.
非常棒,redux-saga是dva中比较难理解的一块
Saga来源于DDD 长时处理Process Manager
No branches or pull requests
在选择要系统地学习一个新的 __框架/库 __之前,首先至少得学会先去思考以下两点:
然后,才会带着更多的好奇心去了解:它的由来、它名字的含义、它引申的一些概念,以及它具体的使用方式...
本文尝试通过 自我学习/自我思考 的方式,谈谈对 redux-saga 的学习和理解。
学前指引
『Redux-Saga』是一个 库(Library),更细致一点地说,大部分情况下,它是以 Redux 中间件 的形式而存在,主要是为了更优雅地 管理 Redux 应用程序中的 副作用(Side Effects)。
那么,什么是 Side Effects?
Side Effects
来看看 Wikipedia 的专业解释(敲黑板,划重点):
映射在 Javascript 程序中,Side Effects 主要指的就是:异步网络请求、__本地读取 localStorage/Cookie __等外界操作:
虽然中文上翻译成 “副作用”�,但并不意味着不好,这完全取决于特定的 Programming Paradigm(编程范式),比如说:
所以,在 Web 应用,侧重点在于 Side Effects 的 优雅管理(manage),而不是 消除(eliminate)。
说到这里,很多人就会有疑问:相比于 redux-thunk 或者 redux-promise, 同样在处理 Side Effects(比如:异步请求)的问题上,redux-saga 会有什么优势?
Saga vs Thunk
首先,从简单的字面意义就能看出:背后的思想来源不同 —— Thunk vs Saga Pattern。
这里就不展开讲述了,感兴趣的同学,推荐认真阅读以下两篇文章:
其次,再从程序的角度来看:使用方式上的不同。
Note:以下示例会省去部分 Redux 代码,如果你对 Redux 相关知识还不太了解,那么《Redux 卍解》了解一下。
redux-thunk
一般情况下,actions 都是符合 FSA 标准的(即:a plain javascript object),像下面这样:
它代表的含义是:每次执行
dispatch(action)
会通知 reducer ++将 action.payload(数据) 以 action.type 的方式(操作)++__同步更新__到 本地 store 。而一个 丰富多变的 Web 应用,payload 数据往往来自于远端服务器,为了能将 __异步获取数据 __这部分代码跟 UI 解耦,redux-thunk 选择以 middleware 的形式来增强 redux store 的 dispatch 方法(即:支持了
dispatch(function)
),从而在拥有了 ++异步获取数据能力++ 的同时,又可以进一步将 ++数据获取相关的业务逻辑++ 从 View 层分离出去。来看看以下代码:
如果同样的功能,用 redux-saga 如何实现呢?它的优势在哪里?
redux-saga
先来看下代码,大致感受下(后面会细讲):
将从上面的代码,与之前的进行对比,可以归纳以下几点:
深入学习
先来看看,如何将 store 和 saga 关联起来?
代码分析:
createSagaMiddleware
创建 sagaMiddleware(当然创建时,你也可以传递一些可选的配置参数)。store.dispatch(action)
,数据流都会经过 sagaMiddleware 这一道工序,进行必要的 “加工处理”(比如:发送一个异步请求)。整合以上分析:程序启动时,
run(rootSaga)
会开启 sagaMiddleware 对某些 action 进行监听,当后续程序中有触发dispatch(action)
(比如:用户点击)的时候,由于数据流会经过 sagaMiddleware,所以 sagaMiddleware 能够判断当前 action 是否有被监听?如果有,就会进行相应的操作(比如:发送一个异步请求);如果没有,则什么都不做。所以来看看,初始化程序时,rootSaga 具体可以做些什么?
如同前面所说,rootSaga 里面的代码会在程序启动时,会依次被执行:
takeEvery
方法会注册一个 watcher saga,对{ type: 'FETCH\_POSTS' }
的 action 实施监听,后续会执行与之匹配的 worker saga(比如:fetchPosts)。PS:通常情况下,在无需进行 saga 按需加载 的情况下,rootSaga 里会集中 引入并注册 程序中所有用到的 watcher saga(就像 combine rootReducer 那样)。
最后再看看,程序启动后,一个完整的单向数据流是如何形成的?
当组件
<App />
被执行挂载后,通过dispatch({ type: 'FETCH\_POSTS' })
通知 sagaMiddleware 寻找到 匹配的 watcher saga 后,执行对应的 woker saga,从而发起数据异步请求 ...... 最终<App/>
会在得到最新 posts 数据后,执行 re-render 更新 UI。至此,以上三个部分代码实现了基于 redux-saga 的一次 __完整单向数据流,__如果用一张图来表现的话 ,应该是这样:
文章看到这里,对于一个 redux-saga 新手而言,可能会留有这样的疑惑: 上述代码中 put/call/fork/takeEvery 这些方法是干什么用的?这就是接下来要详细讨论的 saga effects。
Effects
前面说到,saga 是一个 generator function,这就意味着它的执行原理必然是下面这样:
也就是说,generator function 在未执行完前(即:result.done === false),它的控制权始终掌握在__ 执行者(caller)__手中,即:
而 caller 本身要实现上面上述功能需要依赖原生 API :
iterator.next(value)
,value 就是 yield expression 的返回值。举个例子:
单纯的看 gen 函数,没人知道 value 的值会是多少?
这完全取决于 gen 的执行者(caller),如果使用上面的 next 方法来执行它,value 的值就是 'hello saga',因为 next 方法对 expression 为 promise 时,做了特殊处理(这不就是缩小版的 co 么~ wow~⊙o⊙)。
换句话说,expression 可以是任何值,关键是 caller 如何来解释 expression,并返回合理的值 !
以此结论,推理来看:
讲了这么多,那么 effect 到底是什么呢?先来看看官方解释:
意思是说:effect 本质上是一个普通对象,包含着一些指令信息,这些指令最终会被 saga middleware 解释并执行。
用一段代码来解释上述这句话:
可以明显的看出:
这里的 __call effect __表示执行
ajax.get('user/Login')
,又因为它的返回值是 promise, 为了等待异步结果返回,fetchData 函数会暂时处于 阻塞 状态。除了上述所说的 call effect 之外,redux-saga 还提供了很多其他 effect 类型,它们都是由对应的 effect factory 生成,在 saga 中应用于不同的场景,比较常用的是:
其中,比较难以理解的就属:如何区分 call 和 fork?什么是阻塞/非阻塞?这是接下来要讲的。
Call vs Fork
前面已经提到,saga 中 call 和 fork 都是用来执行指定函数 fn,区别在于:
举个例子,假设 fn 函数返回一个 promise:
显然,fork 的异步非阻塞特性更适合于在后台运行一些不影响主流程的代码(比如:后台打点/开启监听),这往往是加快页面渲染的一种方式,有点类似于 Egg 的 runInBackground,倘若在这种情况下,你依然要获取返回结果,可以这样做:
PS:这里的函数 fn 是一个 normal function,其实它还可以是一个 generator function(被称作是 Child Saga)。
最后的最后,再简单聊聊 saga 中的错误处理方式?
Error Handling
倘若访问一个接口出现代码异常,可能是网络请求问题,也可能是后端数据格式问题,但不管怎样,给予日志上报或友好的错误提示是不可缺少的,这也往往体现了代码的健壮性,一般会这么做:
这是最正确的处理方式,但这里更想讨论的是:++如果忘记写 try catch 进行异常捕获,结果会怎么样?++
就好比下面这样:
假设 saga2 出现代码异常了,且没有进行异常捕获,这样的异常会导致整个 Web App 崩溃么?答案是:肯定的!
来具体解释下:
redux-saga 中执行
sagaMiddleware.run(rootsaga)
或fork(saga)
时,均会返回一个 task 对象(上文中说到),嵌套的 task 之间会存在__ 父子关系,__就比如上述代码:现在某一个 childTask 异常了(比如这里的: saga2),那么它的 parentTask(如:rootTask)收到通知先会执行自身的 cancel 操作,再通知其他 childTask(如:saga1,saga3) 同样执行 cancel 操作。(这其实正是 Saga Pattern 的思想)
但这就意味着,用户可能会因为一个按钮点击引发的异常,而导致整个 Web 应用的功能均无法使用!!
那么,面对这样的问题,如何优化呢?隔离 childTask 是首先想到的一种方案。
使用 spawn 替换 fork,它们的区别在于 spawn 返回 __ isolate task__,不存在 父子关系,也就是说,即使 saga2 挂了,rootSaga 也不受影响,saga1 和 saga3 自然更不会受影响,依然可以正常工作。
但这样的方案并不是让人最满意的!如果因为某一次网络原因,导致 saga2 挂了,在不刷新页面的情况下,用户连重试的机会都不给,显然是不合理的,那么如果可以做到 saga 自动重启呢?社区里已经有一个比较好的方案了:
上述代码通过在最上层为每一个 childSaga 添加异常捕获,并通过
while(true) {}
循环自动创建新的 childTask 取代 异常 childTask,以保证功能依然可用(这就类似于 Egg 中某一个 woker 进程 挂了,自动重启一个新的 woker 进程一样)。OK,差不多就先讲这些吧... 完!
The text was updated successfully, but these errors were encountered: