redux-saga 初识

原文连接html

redux-saga 是一个管理 Redux 应用异步操做的中间件,功能相似redux-thunk + async/await, 它经过建立 Sagas 将全部的异步操做逻辑存放在一个地方进行集中处理。ios

redux-saga 的 effects

redux-saga中的 Effects 是一个纯文本 JavaScript 对象,包含一些将被 saga middleware 执行的指令。这些指令所执行的操做包括以下三种:git

  • 发起一个异步调用(如发一块儿一个 Ajax 请求)
  • 发起其余的 action 从而更新 Store
  • 调用其余的 Sagas

Effects 中包含的指令有不少,具体能够异步API 参考进行查阅github

redux-saga 的特色

  • 方便测试,例如:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
  • action 能够保持其纯净性,异步操做集中在 saga 中进行处理
  • watch/worker(监听->执行) 的工做形式
  • 被实现为 generator
  • 对含有复杂异步逻辑的应用场景支持良好
  • 更细粒度地实现异步逻辑,从而使流程更加清晰明了,遇到 bug 易于追踪和解决。
  • 以同步的方式书写异步逻辑,更符合人的思惟逻辑

从 redux-thunk 到 redux-saga

假如如今有一个场景:用户在登陆的时候须要验证用户的 username 和 password 是否符合要求。redux

使用 redux-thunk 实现

获取用户数据的逻辑(user.js):axios

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

验证登陆的逻辑(login.js):swift

import request from 'axios';
import { loadUserData } from './user';

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

redux-saga

异步逻辑能够所有写进 saga.js 中:api

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST
    try {
      let { data } = yield call(loginRequest, { user, pass }); //阻塞,请求后台数据
      yield fork(loadUserData, data.uid); //非阻塞执行loadUserData
      yield put({ type: LOGIN_SUCCESS, data }); //发起一个action,相似于dispatch
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(userRequest, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

难点解读

对于 redux-saga, 仍是有不少比较难以理解和晦涩的地方,下面笔者针对本身以为比较容易混淆的概念进行整理:数组

take 的使用

take 和 takeEvery 都是监听某个 action, 可是二者的做用却不一致,takeEvery 是每次 action 触发的时候都响应,而 take 则是执行流执行到 take 语句时才响应。takeEvery 只是监听 action, 并执行相对应的处理函数,对什么时候执行 action 以及如何响应 action 并无多大的控制权,被调用的任务没法控制什么时候被调用,而且它们也没法控制什么时候中止监听,它只能在每次 action 被匹配时一遍又一遍地被调用。可是 take 能够在 generator 函数中决定什么时候响应一个 action 以及 响应后的后续操做。
例如在监听全部类型的 action 触发时进行 logger 操做,使用 takeEvery 实现以下:app

import { takeEvery } from 'redux-saga'

function* watchAndLog(getState) {
  yield* takeEvery('*', function* logger(action) {
      //do some logger operation //在回调函数体内
  })
}

使用 take 实现以下:

import { take } from 'redux-saga/effects'

function* watchAndLog(getState) {
  while(true) {
    const action = yield take('*')
    //do some logger operation //与 take 并行 
  })
}

其中 while(true) 的意思是一旦到达流程最后一步(logger),经过等待一个新的任意的 action 来启动一个新的迭代(logger 流程)。

阻塞和非阻塞

call 操做是用来发起异步操做的,对于 generator 来讲,call 是阻塞的操做,它在 Generator 调用结束以前不能执行或处理任何其余事情。,可是 fork 倒是非阻塞操做,当 fork 调动任务时,该任务会在后台执行,此时的执行流能够继续日后面执行而不用等待结果返回。

例如以下的登陆场景:

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if(token) {
      yield call(Api.storeItem({token}))
      yield take('LOGOUT')
      yield call(Api.clearItem('token'))
    }
  }
}

若在 call 在去请求 authorize 时,结果未返回,可是此时用户又触发了 LOGOUT 的 action,此时的 LOGOUT 将会被忽略而不被处理,由于 loginFlow 在 authorize 中被堵塞了,没有执行到 take('LOGOUT')那里

同时执行多个任务

如若遇到某个场景须要同一时间执行多个任务,好比 请求 users 数据 和 products 数据, 应该使用以下的方式:

import { call } from 'redux-saga/effects'
//同步执行
const [users, products] = yield [
  call(fetch, '/users'),
  call(fetch, '/products')
]

//而不是
//顺序执行
const users = yield call(fetch, '/users'),
      products = yield call(fetch, '/products')

当 yield 后面是一个数组时,那么数组里面的操做将按照 Promise.all 的执行规则来执行,genertor 会阻塞知道全部的 effects 被执行完成

源码解读

在每个使用 redux-saga 的项目中,主文件中都会有以下一段将 sagas 中间件加入到 Store 的逻辑:

const sagaMiddleware = createSagaMiddleware({sagaMonitor})
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

其中 createSagaMiddleware 是 redux-saga 核心源码文件 src/middleware.js 中导出的方法:

export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) {
 ...
 
 function sagaMiddleware({ getState, dispatch }) {
    const channel = stdChannel()
    channel.put = (options.emitter || identity)(channel.put)

    sagaMiddleware.run = runSaga.bind(null, {
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
      logger,
      onError,
      effectMiddlewares,
    })

    return next => action => {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      const result = next(action) // hit reducers
      channel.put(action)
      return result
    }
  }
 ...
 
 }

这段逻辑主要是执行了 sagaMiddleware(),该函数里面将 runSaga 赋值给 sagaMiddleware.run 并执行,最后返回 middleware。 接着看 runSaga() 的逻辑:

export function runSaga(options, saga, ...args) {
...
  const task = proc(
    iterator,
    channel,
    wrapSagaDispatch(dispatch),
    getState,
    context,
    { sagaMonitor, logger, onError, middleware },
    effectId,
    saga.name,
  )

  if (sagaMonitor) {
    sagaMonitor.effectResolved(effectId, task)
  }

  return task
}

这个函数里定义了返回了一个 task 对象,该 task 是由 proc 产生的,移步 proc.js:

export default function proc(
  iterator,
  stdChannel,
  dispatch = noop,
  getState = noop,
  parentContext = {},
  options = {},
  parentEffectId = 0,
  name = 'anonymous',
  cont,
) {
  ...
  const task = newTask(parentEffectId, name, iterator, cont)
  const mainTask = { name, cancel: cancelMain, isRunning: true }
  const taskQueue = forkQueue(name, mainTask, end)
  
  ...
  
  next()
  
  return task

  function next(arg, isErr){
  ...
      if (!result.done) {
        digestEffect(result.value, parentEffectId, '', next)
      } 
  ...
  }
}

其中 digestEffect 就执行了 effectTriggerd()runEffect(),也就是执行 effect,其中 runEffect() 中定义了不一样 effect 执行相对应的函数,每个 effect 函数都在 proc.js 实现了。

除了一些核心方法以外,redux-saga 还提供了一系列的 helper 文件,这些文件的做用是返回一个类 iterator 的对象,便于后续的遍历和执行, 在此不具体分析。

参考文档

相关文章
相关标签/搜索