Первое знакомство с redux-saga

внешний интерфейс JavaScript React.js Redux

Оригинальная ссылка

, если вас интересуют или интересуют акции США, вы можете добавить меня в WeChat: xiaobei060537, и общаться вместе 😝.

redux-saga — это промежуточное ПО для управления асинхронными операциями приложений Redux с аналогичными функциями.redux-thunk + async/await, который хранит всю логику асинхронных операций в одном месте для централизованной обработки путем создания Sagas.

Эффекты редукс-саги

Эффекты в redux-saga — это простой текстовый объект JavaScript, который содержит некоторые инструкции, которые будут выполняться промежуточным программным обеспечением saga. Операции, выполняемые этими инструкциями, включают следующие три:

  • Инициировать асинхронный вызов (например, запрос Ajax)
  • Инициировать другие действия для обновления Магазина
  • Призывайте другие саги

В Эффекты включено много инструкций, которые могут быть асинхронными.Справочник по APIпроверить

Особенности редукс-саги

  • Удобно для тестирования, например:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
  • Действие может поддерживать свою чистоту, а асинхронные операции централизованы в саге для обработки.
  • Рабочая форма watch/worker (прослушивание -> исполнение)
  • реализован как генератор
  • Хорошая поддержка сценариев приложений со сложной асинхронной логикой
  • Асинхронная логика реализована более детально, что приводит к более четким процессам и более простому отслеживанию и устранению ошибок.
  • Написание асинхронной логики синхронным способом больше соответствует логике человеческого мышления.

От redux-thunk к redux-saga

Предположим, что сейчас есть сценарий: пользователю необходимо проверить, соответствуют ли имя пользователя и пароль требованиям при входе в систему.

Реализовано с помощью redux-thunk

Логика получения пользовательских данных (user.js):

// 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):

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:

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, и takeEvery прослушивают действие, но их роли несовместимы: TakeEvery отвечает каждый раз, когда запускается действие, а take отвечает, когда поток выполнения достигает инструкции take. takeEvery просто слушает действие и выполняет соответствующую функцию-обработчик. У него нет большого контроля над тем, когда выполняется действие и как реагировать на действие. Вызванная задача не имеет контроля над тем, когда она вызывается, и у них есть нет контроля над тем, когда прекратить прослушивание.Его можно вызывать только снова и снова каждый раз, когда сопоставляется действие. Но take может решить в функции-генераторе, когда реагировать на действие и что делать дальше.
Например, чтобы выполнить операцию ведения журнала при прослушивании всех типов триггеров действий, используйте takeEvery для достижения следующего:

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)Это означает, что после достижения последнего шага процесса (регистратора) запустите новую итерацию (процесс регистратора), ожидая нового произвольного действия.

блокирующий и не блокирующий

Операция вызова используется для инициирования асинхронных операций.Для генераторов вызов является блокирующей операцией, которая не может выполнить или обработать что-либо еще, пока вызов генератора не завершится. , но 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'))
    }
  }
}

Если результат не возвращается, когда вызов запрашивает авторизацию, но пользователь инициирует действие LOGOUT в это время, то LOGOUT в это время будет проигнорирован и не обработан, потому что loginFlow заблокирован при авторизации и не был выполнен.take('LOGOUT')Там

Выполнять несколько задач одновременно

Если вы столкнулись со сценарием, который требует одновременного выполнения нескольких задач, таких как запрос данных о пользователях и данных о продуктах, вам следует использовать следующие методы:

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Правила выполнения генератора блокируются до тех пор, пока не будут выполнены все эффекты.

Интерпретация исходного кода

В каждом проекте, использующем redux-saga, основной файл будет иметь следующую логику для добавления промежуточного программного обеспечения saga в 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, выполняет его и, наконец, возвращает промежуточное ПО. Затем посмотрите на логику 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
}

Эта функция определяет и возвращает объект задачи, задача генерируется 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)
	  } 
  ...
  }
}

где выполняется дайджестЭффектeffectTriggerd()а такжеrunEffect(), то есть для выполнения эффекта, в котором runEffect() определяет соответствующие функции для выполнения различных эффектов, и каждая функция эффекта реализована в proc.js.

В дополнение к некоторым основным методам, redux-saga также предоставляет ряд вспомогательных файлов, функция которых состоит в том, чтобы возвращать итератороподобный объект для последующего обхода и выполнения, который здесь подробно анализироваться не будет.

Справочная документация