Q&A
Q:什么是redux中间件?
A:提供位于 action 被发起之后,到达 reducer 之前的扩展
Q:redux-saga的目标是什么?
Q:什么是Saga模式
A:将一个长活事务(应用)分解成可以交错运行的子事务(多个fork)集合,其中每个子事务都是一个保持数据库一致性(读写state)的真实事务。
Generator方法
Generator.prototype.next()
返回一个由 yield表达式生成的值。 yield 1 => { value: 1, done: false }
Generator.prototype.return() itr.return(value) => { value, done: true }
返回给定的值并结束生成器。
Generator.prototype.throw() itr.throw(value) => { value: undefined, done: true }
向生成器抛出一个错误。 try {}catch(e) { console.log(e) } e为throw value
为什么选择Generator
为了更加精细准确地控制每一个异步流程。
- 利用iterator的可分步执行,可以做到不侵入业务代码实现取消任务的功能。
function demo() {
let cancelled = false;
new Promise((res,rej) => { setTimeout((delayMs) => {console.log('delayMs: ', delayMs); res(delayMs)}, 1000, 1000) })
.then(wrapWithCancel((dd) => { console.log('fuck: ', dd); } ))
.then(resolve)
.then(reject);
return {
promise,
cancel: () => {
cancelled = true;
reject({ reason: 'cancelled' });
}
};
function wrapWithCancel(fn) {
return (data) => {
if (!cancelled) {
return fn(data);
}
};
}
}
function* demo() {
const userId = yield fetchUserId
const money = yield queryMoney
}
function runWithCancel(generator) {
const itr = generator();
let cancelled = false;
const cancel = () => {
cancelled = true;
itr.return({ reason: "cancelled" });
};
function next(arg) {
const { value, done } = itr.next(arg);
if (!done) {
// 假设我们总是接收 Promise,所以不需要检查类型
value.then(data => next(data));
}
}
next();
return { cancel };
}
- 通过iterator影响内部状态(iter.next(result)),注入异步操作结果,将异步的过程以同步的方式去书写。
- 利用iterator的错误捕获特性(iter.throw(error)),注入异步操作异常。以统一的方式(try catch)去管理异常。
try {
new Promise(() => { throw(2) });
}catch(e) {} // Uncaught (in promise) 2
// catch Promise异常
new Promise(() => { throw(2) }).catch(e => console.log(e))
function* demo() {
try {
yield promise
}catch(e) {
console.log('catched');
}
}
const itr = demo();
itr.next(); // { value: Promise, done: false }
//
promise.then(data => itr.next(data), error => itr.throw(error)).catch(error => itr.throw(error));
redux-saga internal
Effect
一个 effect 就是一个 Plain Object JavaScript 对象,包含一些将被 saga middleware 执行的指令。redux-saga 可以处理 promise、iterator、take、put 等类型的 effect,合理地组合不同类型的 effect 可以表达复杂的异步逻辑。
使用 redux-saga 提供的工厂函数来创建 effect。 举个例子,使用
takeEvery(e.ASYNC_INCREMENT, sagaAsyncIncrement) => (
{
@@redux-saga/HELPER: true
name: "takeEvery(e.ASYNC_INCREMENT, sagaAsyncIncrement)"
next: ƒ t(n,t)
return: ƒ (e)
throw: ƒ (e)
Symbol(Symbol.iterator): ƒ ()
}
)
返回的指令用于指示sagaMiddleware完成对应的操作。
同时这个设计的另一个作用是使得单元测试变得简单,在进行一些异步操作时可以不必再去mock数据。
function* fetchProducts() {
const products = yield Api.fetch('/products')
console.log(products)
}
const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // 我们期望得到什么?
// 使用 call
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
console.log(products)
}
const iterator = fetchProducts()
// expects a call instruction
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
Task
一个 Task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,可以同时运行多个 Task。通过 fork 函数来创建 Task:
function* saga() {
...
const task = yield fork(otherSaga, ...args)
...
}
将实现一个功能的异步逻辑封装为一个generator函数,再到saga中运行时表现为一个Task。Task 对象描述了并提供方法去查看控制迭代器的运行状态。
fork model
在 redux-saga 的世界里,你可以使用 2 个 Effects 在后台动态地 fork task
fork 用来创建 attached forks
spawn 用来创建 detached forks
完成:一个 saga 实例在满足以下条件之后进入完成状态: 迭代器自身的语句执行完成 所有的 child-saga 进入完成状态 当一个节点的所有子节点完成时,且自身迭代器代码执行完毕时,该节点才算完成。
错误传播:一个 saga 实例在以下情况会中断并抛出错误: 迭代器自身执行时抛出了异常 其中一个 child-saga 抛出了错误 当一个节点发生错误时,错误会沿着树向根节点向上传播,直到某个节点捕获该错误。
取消:取消一个 saga 实例也会导致以下事情的发生: 取消 mainTask,也就是取消当前 saga 实例等待的 effect 取消所有仍在执行的 child-saga 取消一个节点时,该节点对应的整个子树都将被取消。
function* main() {
yield fork(gen1)
yield fork(gen1)
}
sagaMiddleware.run(main);
mainTask => main
childTask => [gen1, gen2]
function* main() {
yield fork(gen1)
yield spawn(gen1)
}
sagaMiddleware.run(main);
mainTask1 => main
childTask => [gen1]
mainTask2 => gen2
阻塞调用/非阻塞调用
阻塞调用的意思是,Saga 在 yield Effect 之后会等待其执行结果返回,结果返回后才会恢复执行 Generator 中的下一个指令。
非阻塞调用的意思是,Saga 会在 yield Effect 之后立即恢复执行。
function* saga() {
yield take(ACTION) // 阻塞: 将等待 action
yield call(ApiFn, ...args) // 阻塞: 将等待 ApiFn (如果 ApiFn 返回一个 Promise 的话)
yield call(otherSaga, ...args) // 阻塞: 将等待 otherSaga 结束
yield put(...) // 阻塞: 将同步发起 action (使用 Promise.then)
const task = yield fork(otherSaga, ...args) // 非阻塞: 将不会等待 otherSaga
yield cancel(task) // 非阻塞: 将立即恢复执行
// or
yield join(task) // 阻塞: 将等待 task 结束
}
Watcher/Worker
指的是一种使用两个单独的 Saga 来组织控制流的方式。
Watcher: 监听发起的 action 并在每次接收到 action 时 fork 一个 worker。
Worker: 处理 action 并结束它。
function* watcher() {
while(true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}
function* main() {
yield takeEvery(pattern, worker)
}
Channel
生产/消费,所有的worker都将挂在channel上,通常一个应用只有一个默认的stdChannel。
function channel() {
let taker;
function take(cb) {
taker = cb;
}
function put(input) {
if (taker) {
const tempTaker = taker;
taker = null;
tempTaker(input);
}
}
return {
put,
take,
};
}
channel.take() :生产者,把任务放到 channel 中。
channel.put():消费者,store.dispatch(action),从channel中选择符合pattern的任务执行。
redux-saga工作流程
run(saga) ➡ mainTask
⬇
proc ➡ return task
⬇ next
➡ digestEffect ➡ 消费effect
next ↑ ⬇
← runEffect ➡ 根据effect类型去搞事情 take时会使用channel.take 将任务放入channel
从runEffect回到digestEffect这一步便是阻塞/不阻塞的关键了,当fork一个generator,我们新开了一个迭代器。原来主任务的next并不会在新迭代器done掉的时候再调用。
dispatch(action)
⬇
reducer
⬇
channel.put(action) 根据类型取出任务执行,相当于proc开始执行