redux-saga上手门槛较高,这对于redux-saga在团队内推广是不利的,对新人来讲redux-saga的官方资料没有给出使人信服的理由让咱们能够去放弃redux-thunk或者async/await方案。 这篇文章是个人学习过程和了解项目的过程当中总结出来的学习笔记,经过分析redux的做用,对比分析redux中处理反作用的方案,说明了redux-saga是什么,有什么用和实现原理。前端
reduxtypescript
redux下降了大型项目的维护难度。面向对象中有一种常见的解耦方式: "接口与实现分离"。即经过定义接口,类开发者只须要将接口暴露给业务方,业务方只须要知道接口定义了什么行为就能够了,而不须要知道其背后的实现,而类做者在更新的时候也只须要保证明现对应的接口便可。其集大成的模式就是依赖注入(如Spring和Angular)。redux和这种模式其实很是类似:若是咱们把接口理解为action,具体实现是reducer,业务方只能经过action去操做整个store的状态,对业务方来讲,也不须要知道reducer的具体实现,更新reducer对业务方来讲也是无感的。redux这种编程模式是另种形式的"接口与实现分离"。编程
"接口与实现分离"的优点相信各位已经很熟悉,就不在这里多说了。redux
然而这种模式不是完好陷的。redux被诟病的地方在于代码量太大,在第一次写的时候,写代码速度是不如直接在组件中处理业务逻辑的,所以redux不适合小型或者快速试错的项目。社区提出了一些缓和这一缺点的方案,如redux-action。设计模式
反作用api
无反作用是指在函数中,除了返回值以外,这个函数不会对调用方产生附加的影响,好比修改函数入参,读取或者修改全局环境变量,写IO,读IO都是反作用。关于无反作用函数的优势这里已经说得很清楚了。数组
redux官方教程中要求reducer是无反作用的。这是为何呢?下面两段代码都表示将一个数组所有元素先加1,而后将值大于10的元素所有剔除数组,而后返回这个数组。promise
function transformArr(arr) {
// 有反作用
for (let i = 0; i < arr.length; i++) {
arr[i] += 1;
if (arr[i] > 10) {
arr.splice(i, 1);
}
}
return arr;
}
function transformArr(arr) {
// 无反作用
const greaterThen10Arr = arr.map(e => e + 1).filter(e => e > 10);
return greaterThen10Arr;
}
复制代码
能够看出,无反作用的代码可以更清晰地表达做者的意图,试想若是你在别人的代码中看到一段对数组的for循环遍历,除了看这个循环对数组作了什么,更让人担惊受怕的是这个循环有没有对数组之外的东西进行了处理,而无反作用的代码只须要看变量名便明白了做者但愿获得一个什么样的数组。一样,若是在一个reducer是有反作用的,那么后续维护,你颇有可能要回去看这个reducer作了什么,会不会对新增的功能产生影响。这是违背redux接口与实现分离的理念的。bash
反作用处理方案网络
redux诞生时,JS社区对如何处理异步的问题还在讨论中,大概是由于这个缘由致使redux没有对反作用的处理方案,然而前端不可能作到全部代码无反作用,那么反作用该放哪?下面是社区的方案。
async/await
这是小型项目最简单的方案,如如下代码,在获取用户id后,根据用户id得到对应数据,最后发出action将数据保存到store中。
function fetchData(userId) {
// 返回一个promise, 内容是数据
}
function fetchUser() {
// 返回一个promise, 内容是用户信息
}
class Component {
componentDidMount() {
const { userInfo: { userId } } = await fetchUser();
store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
const { data } = await fetchData(userId);
store.dispatch({type: 'UPDATE_DATA', payload: data});
}
}
复制代码
那问题来了,若是其余组件也须要复用这个获取数据的逻辑该怎么办呢,稍好的方案是创建一个类来实现复用,以下:
class DataHandler {
static fetchData() {
const { userInfo: { userId } } = await fetchUser();
store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
const { data } = await fetchData(userId);
store.dispatch({type: 'UPDATE_DATA', payload: data});
}
}
class ComponentA {
componentDidMount() {
DataHandler.fetchData();
}
}
class ComponentB {
componentDidMount() {
DataHandler.fetchData();
}
}
复制代码
可是这样就会致使了维护的问题,DataHandler该如何扩展?好比ComponentB但愿网络请求结束后,但愿对上方第五行中的data进行变换后更新store的其余字段,那你可能须要在DataHandler添加一个给B的专属函数用来处理,长期下来DataHandler就会变得愈来愈臃肿,最重要是组件对DataHandler是依赖关系,DataHandler的每一次修改都须要检查全部引用DataHandler的组件,DataHandler会愈来愈变得不可维护。由于咱们须要redux。
redux-thunk
redux-thunk用法就不在这里介绍了,主要优势是灵活好用,团队推广难度低,克服DataHandler的缺点,很是适合小型项目使用。
缺点是对不能对异步进行粒度小的控制(下文会说明),不容易测试。须要说的是这篇文章没法也不会去说明redux-saga比redux-thunk的绝对优点,使用哪一个彻底取决具体场景和团队的选择。
redux-saga最大的缺点是绝对高的理解门槛和缺少能够公开讨论的使用场景----小型项目根本用不上,大型项目不必定适合在公众场合讨论具体的技术细节,这就致使学习资源很缺少----除了API使用外。
redux-saga是个很是强大处理反作用的工具。它提供了对异步流程更细粒度的控制,对每一个异步流程他能够实现暂停、中止、启动三种状态。此外redux-saga利用了generator,对每一个saga,其测试方式能够很是简单。更重要的是其异步处理逻辑放在了saga中,咱们能够监听action触发,而后产生反作用。action依然是普通的redux action,不破坏redux对action的定义。
redux-saga如何工做
redux-saga基于generator。咱们确定都认识generator是什么,可是概念很虚,让人看得云里雾里。咱们随便拉一个generator例子:
function* gen() {
const x = yield 1;
const y = yield 2;
}
const g = gen();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
复制代码
这是很简单的generator例子,但咱们仍是不知道generator能够用来作什么。其实咱们能够把gen当成一个任务清单,而后有一我的拿到这个任务清单而后开始执行任务。好比如下代码:
function* gen() {
const user = {userName: 'xiaoming'};
const article = {articles: [{title: '文章'}]};
const x = yield Promise.resolve(user);
console.assert(x === user); // true
const y = yield Promise.resolve(article);
console.assert(y === article); // true
}
async function worker(gen) {
const g = gen();
let task = g.next();
while(!task.done) {
const response = await task.value;
task = g.next(response);
}
}
worker(gen);
复制代码
worker就是执行任务的那我的,gen就是那个任务清单。若是联想一下redux-saga,redux-saga就是那个worker,咱们须要作的就是编写任务清单-----编写saga,那saga是什么呢?
saga是什么
saga是个奇怪的名字,下面是字典和论文的定义:
字典:A saga is a long story, account, or sequence of events.
论文:A LLT(Long lived transactions) is a saga if it can be written as a sequence of transactions that can be interleaved with other transactions.
复制代码
简单解释就是,saga是一个任务列表,任务执行顺序是有序的,每一个任务的状态能够被改变。有序好理解,那什么是能够被改变呢?下面代码是项目关于网络请求的代码:
function* request(action: PayloadAction) {
try {
yield put(requestActions.start(action));
const response = yield call(apiRequestAndReturnPromise);
yield put({type: `${action.type}`_SUCCESS, payload: response});
} catch (error) {
yield put({type: `${action.type}`_FAIL, payload: error});
}
}
function* cancelSendRequestOnAction(abortOn: string | string[], task: any) {
const { abortingAction } = yield race({
abortingAction: take(abortOn), // 能够是一个数组
taskFinished: join(task),
timeout: call(delay, 10000), // taskFinished doesn't work for aborted tasks
});
if (abortingAction) {
yield cancel(task);
}
}
function* requestWatcher() {
const newTask = yield fork(apiRequest, action); // Task 1
yield fork(cancelSendRequestOnAction, abortOn, newTask); // Task 2
}
复制代码
这段代码可能会有点绕,可能须要你多看几回。
首先requestWatcher用fork启动了两个异步任务,由于使用了fork,Task 2不会等待Task 1结束,而是会在Task 1开始以后立刻执行。
fork返回的newTask是一个saga的任务对象,咱们能够对这个任务进行处理,好比取消。
Task 1是发起网络请求获取数据。没有什么特别的。
Task 2是重点,能够解释什么是saga的状态可改变。Task2的意思是,abortingAction、taskFinished、timeout任意一个action触发时,就返回abortingAction,若是abortingAction就取消刚才发起的网络请求任务。
这是一个颇有意思的特性,在前端除了网络请求,还有如下的异步行为:Promise、setTimeout、setInterval、点击事件(欢迎补充),那利用redux-saga能简单实现很复杂的交互,特别是在编辑器中的应用。
PS:热烈欢迎批评