初窥redux-saga原理

1,433 阅读5分钟

Q&A

Q:什么是redux中间件?

A:提供位于 action 被发起之后,到达 reducer 之前的扩展

Q:redux-saga的目标是什么?

A: redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

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

为了更加精细准确地控制每一个异步流程。

  1. 利用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 };
}

  1. 通过iterator影响内部状态(iter.next(result)),注入异步操作结果,将异步的过程以同步的方式去书写。
  2. 利用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
	  	⬇
		procreturn task
	  	⬇ next
  	  ➡ digestEffect ➡ 消费effect 
next  ↑   ⬇   
  	  ← runEffect ➡ 根据effect类型去搞事情 take时会使用channel.take 将任务放入channel

从runEffect回到digestEffect这一步便是阻塞/不阻塞的关键了,当fork一个generator,我们新开了一个迭代器。原来主任务的next并不会在新迭代器done掉的时候再调用。


dispatch(action)
	⬇
  reducer
	⬇ 
channel.put(action) 根据类型取出任务执行,相当于proc开始执行


引用