React 副作用 Redux-saga 🐎[深入学习中...]

1,353 阅读5分钟

redux 作为react 的状态管理的时候,副作用处理方案比较多,这里我们讨论 redux-saga 的内容。

需要的准备工作

  1. react 基础
  2. redux 基础
  3. redux 中间基础
  4. ES6+ 生成器相关知识

常见的 redux 中间件

  • redux-logger 中间件
  • redux-promise 中间件
  • redux-thunk 中间件
  • redux-saga 中间件

redux 中间件基础

首先要知道 redux 中间件的作用,其实就是处理副作用,比如:数据层的数据请求,也就是 js 的异步任务。

redux 使用中间件

  1. 引入 createStore 方法,并创建一个 store:
import { createStore } from 'redux'

const store = createStore(reducer)
  1. redux 支持中间件, redux 通过暴露出来的 applyMiddleware 方法来支持 redux 中间件。
import { createStore, applyMiddleware } from 'redux'

const store = createStore(reducer, applyMiddleware(/*your middleware*/));
// reducer 是纯函数
  1. redux-saga 提供了创建中间件的方法
import createSagaMiddleware from 'redux-saga'const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));

redux-saga 中间件的编写

API

redux-saga 对外的暴露 API 可以分为两个部分,这两个部分: 主要部分、副作用部分

主要部分

export { CANCEL, SAGA_LOCATION } from '@redux-saga/symbols'
export { default } from './internal/middleware'
export { runSaga } from './internal/runSaga'
export { END, isEnd, eventChannel, channel, multicastChannel, stdChannel } from './internal/channel'
export { detach } from './internal/io'
import * as buffers from './internal/buffers'
export { buffers }

副作用部分

export {
  take,takeMaybe,put,putResolve,all,race,call,apply,cps,fork,spawn,join,
  cancel,select,actionChannel,cancelled,flush,getContext,setContext,delay,
} from './internal/io'
export { debounce, retry, takeEvery, takeLatest, takeLeading, throttle } from './internal/io-helpers'
import * as effectTypes from './internal/effectTypes';
export { effectTypes };

从暴露的 api 的位置,我们知道大部分的 redux-saga 的 api 在 packages/core/src/internal 实现。

redux-saga 仅仅是 redux 的中间件,但是我们学着学着就把 redux-saga 理解为 react 的中间件。react 中目前还没有中间件的概念。

saga 文件

我们在写 redux-saga 处理副作用的时候,我们一般是将 saga 处理副作用单独的写在不同的文件中。

在创建了 store 之后,有一件事是特别重要的,sagaMiddleware 还需要运行根 saga。

sagaMiddleware.run(rootSaga)

通过源码我们知道 sagaMiddleware 其实就是 sagaMiddlewareFactory ,这个函数调用创建的是一个函数 sagaMiddleware,sagaMiddleware 挂载了 run 方法和 setContext 方法。

源码:

// ...
function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
  let boundRunSaga

  function sagaMiddleware({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, {
      ...options,
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
    })

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

  sagaMiddleware.run = (...args) => {
    return boundRunSaga(...args)
  }

  sagaMiddleware.setContext = props => {

    assignWithSymbols(context, props)
  }

  return sagaMiddleware
}

run 方法是执行了绑定方法boundRunSaga(...args), RunSaga 背后生辰的 task。 也就是说只有执行了 run 方法, redux-saga 才是启动了任务!因为这里面作用特别多处理,我们可以先形而下的理解怎么用。后面在关注原理。

小结

从 redux 中间件简单的使用,和 redux-saga 中间件自生使用的特点区别,理解 redux-saga。

写 saga 文件

写 saga 文件其实,就是将文件异步任务拆分,方便我们管理。

流程

流程种类1:

  • 组件派发 action, saga 的 sagaHelper 中 takeEvery 接收到 action.type
  • takeEvery 迭代处理后执行副作用函数 effect generator 函数,

redux-saga 接收组件 dispatch 过来的 action

redux-saga 中如何接收组件派发过来的 action 呢?

  • takeEvery 函数调用得到的一个 iterator 对象,看源码:

首先将 action.type(patternOrChannel) 和 副作用函数 worker 单独拿出里,然后重新组装数据: yTake、yFork。调用 fsmIterator 去制作一个迭代器

// takeEvery.js
export default function takeEvery(patternOrChannel, worker, ...args) {
  const yTake = { done: false, value: take(patternOrChannel) }
  const yFork = ac => ({ done: false, value: fork(worker, ...args, ac) })

  let action,
    setAction = ac => (action = ac)

  return fsmIterator(
    {
      q1() {
        return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
      },
      q2() {
        return { nextState: 'q1', effect: yFork(action) }
      },
    },
    'q1',
    `takeEvery(${safeName(patternOrChannel)}, ${worker.name})`,
  )
}
  • fsmIterator 组装数据制作 iterator 遍历器
export default function fsmIterator(fsm, startState, name) {
  let stateUpdater,
    errorState,
    effect,
    nextState = startState

  function next(arg, error) {
    if (nextState === qEnd) {
      return done(arg)
    }
    if (error && !errorState) {
      nextState = qEnd
      throw error
    } else {
      stateUpdater && stateUpdater(arg)
      const currentState = error ? fsm[errorState](error) : fsm[nextState]()
      ;({ nextState, effect, stateUpdater, errorState } = currentState)
      return nextState === qEnd ? done(arg) : effect
    }
  }

  return makeIterator(next, error => next(null, error), name)
}
  • iterator 内容
export function makeIterator(next, thro = kThrow, name = 'iterator') {
  const iterator = { meta: { name }, next, throw: thro, return: kReturn, isSagaIterator: true }

  if (typeof Symbol !== 'undefined') {
    iterator[Symbol.iterator] = () => iterator
  }
  return iterator
}

当我们使用 takeEvery 来捕获一个 action.type 时候,就会创建一个迭代器,然后指定副作用 worker 函数。

简单示例

/* eslint-disable no-constant-condition */

import { put, takeEvery, delay } from 'redux-saga/effects'

export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

export default function* rootSaga() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

在副作用 wokder 函数中将 action 派发个reducer

我们可以在 wokder effect 函数中执行异步任务,setTimeOut, ajax 请求任务等等...在完成了 effect 后,我们就可以交给 reducer 让ruducer 去计算新的 state, reducer 在将 state 返回给 store, store 中被订阅 state, 会被视图层更新。

put/dispatch

put 就是棒我们将 saga 中间件中的 action 派发给 reducer。 看源码:

export function put(channel, action) {
  if (is.undef(action)) {
    action = channel
    // `undefined` instead of `null` to make default parameter work
    channel = undefined
  }
  return makeEffect(effectTypes.PUT, { channel, action })
}

// 制作一个 effect, 返回了一个 effect 对象
const makeEffect = (type, payload) => ({
  [IO]: true,
  combinator: false,
  type,
  payload,
})
import { take, put, call, fork, select } from 'redux-saga/effects'
import { take, put, call, fork, race, cancelled } from 'redux-saga/effects'
import { put, takeEvery, delay } from 'redux-saga/effects'
import { retry, call, put, takeEvery, delay, all, race, fork, spawn, take, select } from 'redux-saga/effects'
import { take, put, call, fork, select, all } from 'redux-saga/effects'
import { take, put, call, fork, select, takeEvery, all } from 'redux-saga/effects'

export default function* root() {
  yield fork(startup)
  yield fork(nextRedditChange)
  yield fork(invalidateReddit)
}

export default function* rootSaga() {
  yield fork(watchIncrementAsync)
}

export default function* rootSaga() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

export default function* rootSaga() {
  yield all([
    takeEvery('ACTION_ERROR_IN_PUT', errorInPutSaga),
    takeEvery('ACTION_ERROR_IN_SELECT', errorInSelectSaga),
    takeEvery('ACTION_ERROR_IN_CALL_SYNC', errorInCallSyncSaga),
    takeEvery('ACTION_ERROR_IN_CALL_ASYNC', errorInCallAsyncSaga),
    takeEvery('ACTION_ERROR_IN_CALL_INLINE', errorInCallInlineSaga),
    takeEvery('ACTION_ERROR_IN_FORK', errorInForkSaga),
    takeEvery('ACTION_ERROR_IN_SPAWN', errorInSpawnSaga),
    takeEvery('ACTION_ERROR_IN_RACE', errorInRaceSaga),
    takeEvery('ACTION_CAUGHT_ERROR', caughtErrorSaga),
    fork(function* inlinedSagaName() {
      while (true) {
        yield take('ACTION_INLINE_SAGA_ERROR')
        yield call(throwAnErrorSaga)
      }
    }),
    takeEvery('ACTION_IN_DELEGATE_ERROR', errorInDelegateSaga),
    takeEvery('ACTION_FUNCTION_EXPRESSION_ERROR', funcExpressionSaga),
    takeEvery('ACTION_ERROR_IN_RETRY', errorInRetrySaga),
    takeEvery('ACTION_ERROR_PRIMITIVE', primitiveErrorSaga),
  ])
}

export default function* root() {
  yield all([
    fork(watchNavigate),
    fork(watchLoadUserPage),
    fork(watchLoadRepoPage),
    fork(watchLoadMoreStarred),
    fork(watchLoadMoreStargazers)
  ])
}

export default function* root() {
  yield all([fork(getAllProducts), fork(watchGetProducts), fork(watchCheckout)])
}

学习更新中...