Плюсы / минусы использования redux-saga с генераторами ES6 по сравнению с redux-thunk с ES2017 async / await


488

Сейчас много говорят о последнем мальчике в городе редуксе, Редукс Реду-Сага / Редукс-Сага . Он использует функции генератора для прослушивания / диспетчеризации действий.

Прежде чем обернуть голову, я хотел бы знать плюсы / минусы использования redux-saga вместо подхода, описанного ниже, где я использую redux-thunkс async / await.

Компонент может выглядеть так, диспетчеризировать действия как обычно.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Тогда мои действия выглядят примерно так:

// auth.js

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

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

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 });
    }
}

// more actions...

// 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 });
    }
}

// more actions...

6
Смотрите также мой ответ, сравнивающий redux-thunk и redux-saga здесь: stackoverflow.com/a/34623840/82609
Себастьян Лорбер

22
Что до того, ::как вы this.onClickделаете?
Даунхилски

37
@ZhenyangHua это сокращение для привязки функции к объекту ( this), иначе this.onClick = this.onClick.bind(this). Более длинную форму обычно рекомендуется использовать в конструкторе, так как сокращение повторяет привязку при каждом рендере.
hampusohlsson

7
Понимаю. Спасибо! Я вижу, что люди bind()много используют для перехода thisк функции, но я начал использовать () => method()сейчас.
Даунхилски

2
@Hosar Я какое-то время использовал в своей работе redux & redux-saga, но через пару месяцев фактически перешел на MobX из-за меньших накладных расходов
hampusohlsson

Ответы:


461

В redux-saga, эквивалент приведенного выше примера будет

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

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

Первое, на что нужно обратить внимание, это то, что мы вызываем функции API, используя форму yield call(func, ...args) . callне выполняет эффект, он просто создает простой объект, такой как{type: 'CALL', func, args} . Выполнение делегируется промежуточному программному обеспечению redux-saga, которое заботится о выполнении функции и возобновлении генератора с его результатом.

Основным преимуществом является то, что вы можете протестировать генератор за пределами Redux, используя простые проверки на равенство

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Обратите внимание, что мы высмеиваем результат вызова API, просто вставляя проверенные данные в next метод итератора. Дразнить данные намного проще, чем дразнить функции.

Второе, на что стоит обратить внимание, это призыв к yield take(ACTION). Thunks вызывается создателем действия при каждом новом действии (например LOGIN_REQUEST). то есть действия постоянно толкнули на санки и санки не имеют никакого контроля о том, когда прекратить обработку этих действий.

В Redx-сагах генераторы вытягивают следующее действие. то есть они имеют контроль, когда слушать какие-то действия, а когда нет. В приведенном выше примере инструкции потока размещены внутри while(true)цикла, поэтому он будет прослушивать каждое входящее действие, что несколько имитирует поведение толчка.

Тяговый подход позволяет реализовать сложные потоки управления. Предположим, например, что мы хотим добавить следующие требования

  • Обработка действий пользователя LOGOUT

  • при первом успешном входе в систему сервер возвращает токен, срок действия которого истекает с некоторой задержкой, сохраненной в expires_inполе. Придется обновлять авторизацию в фоновом режиме каждые expires_inмиллисекунды.

  • Примите во внимание, что при ожидании результата вызовов API (либо первоначального входа в систему, либо обновления) пользователь может выйти из промежуточного режима.

Как бы вы реализовали это с помощью Thunks; одновременно обеспечивая полное тестовое покрытие для всего потока? Вот как это может выглядеть с Sagas:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

В приведенном выше примере мы выражаем наше требование параллелизма с помощью race. Если take(LOGOUT)победит в гонке (т.е. пользователь нажал на кнопку «Выйти»). Гонка автоматически отменит authAndRefreshTokenOnExpiryфоновое задание. И если он authAndRefreshTokenOnExpiryбыл заблокирован во время call(authorize, {token})разговора, он также будет отменен. Отмена распространяется вниз автоматически.

Вы можете найти работающую демонстрацию вышеупомянутого потока


@ yassine откуда эта delayфункция? Ах, нашел его: github.com/yelouafi/redux-saga/blob/…
февраля

122
redux-thunkКод вполне читаем и самостоятельно объяснить. Но redux-sagasодин действительно нечитаемым, в основном из - за этих глаголов-подобных функций: call, fork, take, put...
SYG

11
@ syg, я согласен, что колл, форк, бери и пут могут быть более семантически дружелюбными. Тем не менее, именно те функции, подобные глаголам, делают все побочные эффекты тестируемыми.
Даунхиллски

3
Функция @syg с этими странными глаголами все еще более читаема, чем функция с цепочкой глубоких обещаний
Ясир

3
эти "странные" глаголы также помогают вам осмыслить отношение саги к сообщениям, исходящим от избыточности. вы можете выводить типы сообщений из избыточного числа - часто для запуска следующей итерации, и вы можете помещать новые сообщения обратно, чтобы транслировать результат вашего побочного эффекта.
worc

104

Я добавлю свой опыт использования саги в производственную систему в дополнение к довольно полному ответу автора библиотеки.

Pro (используя сагу):

  • Тестируемость. Тестировать саги очень легко, так как call () возвращает чистый объект. Тестирование Thunks обычно требует, чтобы вы включили mockStore внутри вашего теста.

  • Redux-Saga поставляется с множеством полезных вспомогательных функций о задачах. Мне кажется, что идея саги заключается в создании своего рода фонового рабочего процесса / потока для вашего приложения, который выступает в качестве недостающего элемента в архитектуре реагирующего редукса (actionCreators и редукторы должны быть чистыми функциями.) Что приводит к следующему пункту.

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

Против:

  • Синтаксис генератора.

  • Много идей для изучения.

  • API стабильность. Кажется, что Redx-сага все еще добавляет функции (например, каналы?), И сообщество не такое большое. Существует опасение, если библиотека когда-нибудь сделает несовместимое обновление.


9
Просто хочу кое-что прокомментировать, создатель действия не должен быть чистой функцией, о которой неоднократно заявлял сам Дэн.
Марсон Мао

14
На данный момент, как и в случае расширения сообщества, очень рекомендуется использовать Redux-Sagas. Кроме того, API стал более зрелым. Попробуйте удалить Con для API stabilityобновления, чтобы отразить текущую ситуацию.
Denialos

1
У саги больше стартов, чем у thunk, и последний коммит тоже после thunk
amorenew

2
Да, у FWIW redux-saga теперь 12 тысяч звезд, у redux-thunk 8 тысяч
Brian Burns

3
Я собираюсь добавить еще одну проблему саг, это то, что саги полностью отделены от действий и создателей действий по умолчанию. Хотя Thunks напрямую связывают создателей действий со своими побочными эффектами, саги оставляют создателей действий полностью отделенными от саг, которые их слушают. Это имеет технические преимущества, но может усложнить понимание кода и может затуманить некоторые однонаправленные концепции.
theaceofthespade

33

Я просто хотел бы добавить некоторые комментарии из моего личного опыта (с использованием саг и thunk):

Саги отлично подходят для тестирования:

  • Вам не нужно имитировать функции, обернутые эффектами
  • Поэтому тесты чисты, читабельны и легко пишутся
  • При использовании саг создатели действий в основном возвращают обычные литералы объектов. Это также легче проверить и утверждать, в отличие от обещаний Тунка.

Саги более могущественны. Все, что вы можете сделать в создателе действий одного толка, вы также можете сделать в одной саге, но не наоборот (или, по крайней мере, нелегко). Например:

  • дождитесь отправки действия / действий ( take)
  • отменить существующую процедуру ( cancel, takeLatest, race)
  • несколько подпрограмм могут слушать то же действие ( take, takeEvery, ...)

Sagas также предлагает другие полезные функции, которые обобщают некоторые общие шаблоны приложений:

  • channels прослушивать внешние источники событий (например, веб-сокеты)
  • модель вилки ( fork, spawn)
  • дроссель
  • ...

Саги отличный и мощный инструмент. Однако с властью приходит ответственность. Когда ваше приложение растет, вы можете легко потеряться, выяснив, кто ожидает отправки действия или что все происходит при отправке какого-либо действия. С другой стороны, это проще и легче рассуждать. Выбор того или другого зависит от многих аспектов, таких как тип и размер проекта, какие побочные эффекты должен обрабатывать ваш проект или предпочтения команды разработчиков. В любом случае просто сделайте ваше приложение простым и предсказуемым.


8

Просто личный опыт:

  1. Что касается стиля кодирования и читабельности, одно из наиболее значительных преимуществ использования redux-saga в прошлом - это избежать ада обратного вызова в redux-thunk - больше не нужно использовать много вложений then / catch. Но теперь, с популярностью асинхронного / ожидающего thunk, можно также написать асинхронный код в стиле синхронизации при использовании избыточного-thunk, что можно рассматривать как улучшение в избыточном мышлении.

  2. Может потребоваться написать намного больше стандартного кода при использовании redux-saga, особенно в Typescript. Например, если кто-то хочет реализовать асинхронную функцию выборки, обработка данных и ошибок может быть выполнена непосредственно в одном модуле thank в action.js с одним единственным действием FETCH. Но в redux-saga, возможно, потребуется определить действия FETCH_START, FETCH_SUCCESS и FETCH_FAILURE и все связанные с ними проверки типов, потому что одна из функций в redux-saga - использовать этот богатый механизм «токенов» для создания эффектов и инструктирования Редукс магазин для легкого тестирования. Конечно, можно написать сагу, не используя эти действия, но это сделало бы ее похожей на Thunk.

  3. С точки зрения структуры файлов, во многих случаях, как представляется, redux-saga более явный. Можно легко найти асинхронный код в каждом файле sagas.ts, но в избыточном коде его нужно будет увидеть в действиях.

  4. Простое тестирование может быть еще одной взвешенной функцией в Redux-Saga. Это действительно удобно. Но одна вещь, которую необходимо пояснить, состоит в том, что тест «вызова» redux-saga не будет выполнять фактический вызов API в тестировании, поэтому необходимо будет указать пример результата для шагов, которые могут использовать его после вызова API. Поэтому, прежде чем писать в redux-saga, было бы лучше спланировать сагу и ее соответствующие sagas.spec.ts в деталях.

  5. Redux-saga также предоставляет множество продвинутых функций, таких как параллельное выполнение задач, помощников по параллелизму, таких как takeLatest / takeEvery, fork / spawn, которые намного мощнее, чем thunks.

В заключение лично я хотел бы сказать: во многих нормальных случаях и приложениях от малого до среднего размера, используйте async / await style redux-thunk. Это сэкономит вам множество стандартных кодов / действий / typedefs, и вам не нужно будет переключаться между многими различными sagas.ts и поддерживать определенное дерево саг. Но если вы разрабатываете большое приложение с очень сложной асинхронной логикой и нуждаетесь в таких функциях, как параллелизм / параллельный шаблон, или если у вас высокий спрос на тестирование и обслуживание (особенно в разработке, управляемой тестированием), возможно, что при этом вы сможете сэкономить вашу жизнь. ,

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


5

Рассмотрев несколько различных крупномасштабных проектов React / Redux на своем опыте, Sagas предоставляет разработчикам более структурированный способ написания кода, который намного проще тестировать и труднее ошибиться.

Да, это немного странно, но большинство разработчиков достаточно понимают это за один день. Я всегда говорю людям не беспокоиться о том yield, с чего начать, и что, как только вы напишете пару тестов, это придет к вам.

Я видел пару проектов, в которых thunks рассматривались так, как если бы они были контроллерами из паттерна MVC, и это быстро превращается в необратимый беспорядок.

Мой совет - использовать Sagas там, где вам нужны триггеры типа B, относящиеся к одному событию. Я считаю, что для всего, что может затрагивать несколько действий, проще написать клиентское промежуточное ПО и использовать мета-свойство действия FSA для его запуска.


2

Thunks против саг

Redux-Thunkи Redux-Sagaотличаются по нескольким важным аспектам, оба являются библиотеками промежуточного ПО для Redux (промежуточное ПО Redux - это код, который перехватывает действия, поступающие в хранилище с помощью метода dispatch ()).

Действие может быть буквально любым, но если вы следуете передовым методам, это простой объект javascript с полем типа и необязательными полями полезной нагрузки, мета и ошибок. например

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Помимо отправки стандартных действий, Redux-Thunkпромежуточное ПО позволяет отправлять специальные функции, называемые thunks.

Thunks (в Redux) обычно имеют следующую структуру:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

То есть a thunk- это функция, которая (необязательно) принимает некоторые параметры и возвращает другую функцию. Внутренняя функция принимает a dispatch functionи getStateфункцию - обе они будут предоставлены Redux-Thunkпромежуточным программным обеспечением.

Redux-Saga

Redux-SagaПромежуточное программное обеспечение позволяет выражать сложную логику приложения в виде чистых функций, называемых сагами. Чистые функции желательны с точки зрения тестирования, потому что они предсказуемы и повторяемы, что делает их относительно легко тестировать.

Саги реализуются через специальные функции, называемые функциями генератора. Это новая особенность ES6 JavaScript. По сути, выполнение включается и выходит из генератора везде, где вы видите оператор yield. Подумайте об yieldутверждении, которое заставляет генератор приостановить работу и вернуть полученное значение. Позже вызывающая сторона может возобновить работу генератора после оператора, следующего за yield.

Функция генератора определяется так. Обратите внимание на звездочку после ключевого слова функции.

function* mySaga() {
    // ...
}

Как только логин саги зарегистрирован Redux-Saga. Но затем yieldвзятие первой строки приостановит сагу, пока действие с типом не 'LOGIN_REQUEST'будет отправлено в магазин. Как только это произойдет, выполнение будет продолжено.

Для более подробной информации смотрите эту статью .


1

Одна быстрая заметка. Генераторы отменяемы, async / await - нет. Так что для примера из вопроса, это действительно не имеет смысла, что выбрать. Но для более сложных потоков иногда нет лучшего решения, чем использование генераторов.

Таким образом, другая идея могла бы состоять в том, чтобы использовать генераторы с редуктором, но для меня это похоже на попытку изобрести велосипед с квадратными колесами.

И конечно, генераторы легче тестировать.


0

Вот проект , который сочетает в себе лучшие части (профи) как redux-sagaи redux-thunkвы можете обрабатывать все побочные эффекты на сагах, получая обещание от dispatchingсоответствующего действия: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

1
использование then()внутри компонента React противоречит парадигме. Вы должны обрабатывать измененное состояние, componentDidUpdateа не ждать разрешения обещания.

3
@ Maxincredible52 Это не верно для рендеринга на стороне сервера.
Диего Хаз

По моему опыту, точка зрения Макса все еще верна для рендеринга на стороне сервера. Это, вероятно, должно быть обработано где-то на уровне маршрутизации.
ThinkingInBits

3
@ Maxincredible52, почему это против парадигмы, где ты это прочитал? Я обычно делаю похож на @Diego Хаз , но сделать это в componentDidMount (как в React документы, сетевые вызовы должны быть предпочтительнее делать там) , поэтому мы имеемcomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

Более простой способ - использовать redux-auto .

из документа

redux-auto исправил эту асинхронную проблему, просто позволив вам создать функцию «action», которая возвращает обещание. Для сопровождения вашей «стандартной» функции логика действий.

  1. Нет необходимости в другом асинхронном промежуточном ПО Redux. например, Thunk, обещание промежуточного программного обеспечения, сага
  2. Позволяет легко передать обещание в редуксе и управлять им
  3. Позволяет вам совмещать внешние сервисные вызовы с тем, где они будут преобразованы
  4. Присвоение имени файлу "init.js" вызовет его один раз при запуске приложения. Это хорошо для загрузки данных с сервера при запуске

Идея состоит в том, чтобы каждое действие было в определенном файле . совместное размещение вызова сервера в файле с функциями редуктора для «ожидающих», «выполненных» и «отклоненных». Это делает обработку обещаний очень легкой.

Он также автоматически присоединяет вспомогательный объект (называемый «асинхронный») к прототипу вашего состояния, позволяя вам отслеживать в вашем пользовательском интерфейсе запрошенные переходы.


2
Я сделал +1, даже если это неуместный ответ, потому что разные решения тоже нужно учитывать
amorenew

12
Я думаю, что они там, потому что он не раскрыл, что он является автором проекта
jreptak
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.