Что такое функции-генераторы в Redux Saga?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое функции-генераторы в Redux Saga?
Функции-генераторы (generator functions) — это основа Redux Saga. Они позволяют описывать сложные асинхронные потоки в виде синхронного кода, используя ключевое слово yield для паузирования и возобновления выполнения. Это делает асинхронную логику более читаемой и тестируемой.
Основы функций-генераторов
// Синтаксис функции-генератора
function* myGenerator() {
const value1 = yield 'первое значение';
const value2 = yield 'второе значение';
return 'конец';
}
// Генератор НЕ выполняется сразу
const gen = myGenerator();
console.log(gen); // Object [Generator]
// Вызов next() выполняет код до первого yield
const step1 = gen.next();
console.log(step1); // { value: 'первое значение', done: false }
const step2 = gen.next();
console.log(step2); // { value: 'второе значение', done: false }
const step3 = gen.next();
console.log(step3); // { value: 'конец', done: true }
Ключевое свойство генераторов: они паузируются на yield и возобновляются при следующем вызове next().
Как Redux Saga использует генераторы
// Классический Redux Thunk (без Saga)
function fetchUserThunk(userId) {
return async (dispatch) => {
dispatch({ type: 'LOADING' });
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
dispatch({ type: 'SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'ERROR', payload: error });
}
};
}
// Redux Saga (с генератором)
function* fetchUserSaga(action) {
try {
yield put({ type: 'LOADING' });
const response = yield fetch(`/api/users/${action.payload}`);
const data = yield response.json();
yield put({ type: 'SUCCESS', payload: data });
} catch (error) {
yield put({ type: 'ERROR', payload: error });
}
}
Преимущества Saga:
- Синхронный стиль кода (легче читать)
- Легче тестировать (мокируем значения в yield)
- Лучше управление побочными эффектами
- Встроенная отмена операций
Redux Saga эффекты (Effects)
Redux Saga предоставляет специальные функции для работы с побочными эффектами:
import { put, call, select, take, fork } from 'redux-saga/effects';
// 1. put() — отправка действия (dispatch)
function* mySaga() {
yield put({ type: 'INCREMENT' }); // Эквивалент dispatch()
}
// 2. call() — вызов функции
function fetchUser(userId) {
return fetch(`/api/users/${userId}`).then(r => r.json());
}
function* mySaga() {
const user = yield call(fetchUser, 123);
console.log(user);
}
// 3. select() — доступ к state
function* mySaga() {
const state = yield select();
const currentUser = yield select(state => state.user);
}
// 4. take() — ожидание действия
function* mySaga() {
const action = yield take('INCREMENT'); // Ждет действия
console.log('INCREMENT произошел:', action);
}
// 5. fork() — запуск параллельного процесса
function* mySaga() {
// Не ждет завершения, просто запускает
yield fork(backgroundTask);
// Код продолжает выполняться
}
Полный пример Redux Saga
import { put, call, takeEvery } from 'redux-saga/effects';
// 1. API функция
const fetchUserAPI = (userId) =>
fetch(`/api/users/${userId}`).then(r => r.json());
// 2. Saga генератор
function* fetchUserSaga(action) {
try {
// call() выполняет функцию и ждет результат
const user = yield call(fetchUserAPI, action.payload);
// put() отправляет действие в Redux
yield put({
type: 'FETCH_USER_SUCCESS',
payload: user
});
} catch (error) {
yield put({
type: 'FETCH_USER_ERROR',
payload: error.message
});
}
}
// 3. Вотчер saga
function* userSagaWatcher() {
// takeEvery слушает действия FETCH_USER_REQUEST
yield takeEvery('FETCH_USER_REQUEST', fetchUserSaga);
}
// 4. Регистрация в store
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
// Запуск saga
sagaMiddleware.run(userSagaWatcher);
Контроль потока в Saga
// 1. takeEvery — обрабатывает каждое действие
function* watchAndFetch() {
yield takeEvery('FETCH_DATA', fetchData);
// Если пользователь кликнет 10 раз, сработает 10 запросов
}
// 2. takeLatest — отменяет предыдущий, если новый пришел
function* watchAndFetch() {
yield takeLatest('FETCH_DATA', fetchData);
// Если пользователь кликнет 10 раз быстро, сработает только последний
}
// 3. takeLeading — игнорирует новые, пока не завершится текущий
function* watchAndFetch() {
yield takeLeading('FETCH_DATA', fetchData);
// Первый клик работает, остальные игнорируются до завершения
}
Тестирование Saga с генераторами
// Функция Saga
function* fetchUserSaga(action) {
try {
const user = yield call(fetchUserAPI, action.payload);
yield put({ type: 'SUCCESS', payload: user });
} catch (error) {
yield put({ type: 'ERROR', payload: error });
}
}
// Тест
import { call, put } from 'redux-saga/effects';
describe('fetchUserSaga', () => {
it('должен fetch пользователя и dispatch SUCCESS', () => {
const action = { payload: 1 };
const gen = fetchUserSaga(action);
// Шаг 1: проверяем, что вызывается API
let result = gen.next();
expect(result.value).toEqual(call(fetchUserAPI, 1));
// Шаг 2: мокируем результат API
const user = { id: 1, name: 'John' };
result = gen.next(user);
expect(result.value).toEqual(
put({ type: 'SUCCESS', payload: user })
);
// Шаг 3: проверяем завершение
result = gen.next();
expect(result.done).toBe(true);
});
it('должен dispatch ERROR при ошибке', () => {
const action = { payload: 1 };
const gen = fetchUserSaga(action);
gen.next(); // Пропускаем call()
// Мокируем ошибку
const error = new Error('Network error');
let result = gen.throw(error);
expect(result.value).toEqual(
put({ type: 'ERROR', payload: error })
);
});
});
Сложные сценарии с Saga
1. Отмена операции (Race)
import { race, delay } from 'redux-saga/effects';
function* fetchWithTimeout(userId) {
const { response, timeout } = yield race({
response: call(fetchUserAPI, userId),
timeout: delay(5000) // 5 секунд
});
if (timeout) {
yield put({ type: 'FETCH_TIMEOUT' });
} else {
yield put({ type: 'FETCH_SUCCESS', payload: response });
}
}
2. Параллельные операции (All)
import { all } from 'redux-saga/effects';
function* fetchBothUserAndPosts(userId) {
const [user, posts] = yield all([
call(fetchUserAPI, userId),
call(fetchPostsAPI, userId)
]);
yield put({
type: 'FETCH_COMPLETE',
payload: { user, posts }
});
}
3. Поллинг (повторные попытки)
import { delay } from 'redux-saga/effects';
function* pollData(userId) {
while (true) {
try {
const data = yield call(fetchUserAPI, userId);
yield put({ type: 'POLL_SUCCESS', payload: data });
} catch (error) {
console.log('Poll error, retry in 5s');
}
// Ждем 5 секунд
yield delay(5000);
}
}
Генераторы против async/await
// Генератор + Saga
function* fetchUserSaga(userId) {
try {
const user = yield call(fetchUserAPI, userId);
yield put({ type: 'SUCCESS', payload: user });
} catch (error) {
yield put({ type: 'ERROR', payload: error });
}
}
// async/await эквивалент (более простой)
async function fetchUserAsync(userId) {
try {
const user = await fetchUserAPI(userId);
dispatch({ type: 'SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'ERROR', payload: error });
}
}
Когда выбирать Saga:
- Сложные асинхронные потоки
- Нужна отмена операций
- Множество параллельных задач
- Интеграция со множеством API
Когда выбирать async/await + Redux Thunk:
- Простая логика
- Новый проект
- Меньше boilerplate
Заключение
Функции-генераторы в Redux Saga:
- Синтаксис:
function*с использованиемyield - Контроль: Паузируются и возобновляются по demand
- Эффекты:
put,call,select,take,forkдля управления побочными эффектами - Тестируемость: Легко тестировать, проверяя последовательность эффектов
- Контроль потока:
takeEvery,takeLatest,race,allдля сложных сценариев - Отмена: Встроенная поддержка отмены асинхронных операций
Генераторы в Saga позволяют писать асинхронный код, выглядящий синхронно, что улучшает читаемость и тестируемость сложных приложений.