Лучшие практики асинхронной потоковой передачи Redux

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

исходный адрес

Лучшие практики асинхронной потоковой передачи Redux

Прежде чем читать эту статью, я надеюсь, что у вас есть четкое представление о Redux.Если вы не знакомы с ним, вы можете прочитать эту статью:Демистификация управления состоянием React

Если вы считаете эту статью полезной, вы можете нажать звездочку, чтобы поощрить ее. Весь код в этой статье можно загрузить из репозитория github. Читатели могут открыть его следующим образом:

git clone https://github.com/happylindz/blog.git
cd blog/code/asynchronousAction/
cd xxx/
yarn
yarn start
скопировать код

Мы знаем, что в мире Redux действие Redux возвращает JS-объект, который принимается и обрабатывается Reducer и возвращается в новое состояние, которое кажется очень красивым. Весь процесс можно представить так:

view -> actionCreator -> action -> reducer -> newState ->(map) container component

Но в реальной разработке бизнеса нам нужно иметь дело с асинхронными запросами, такими как: запрос фоновых данных, задержка выполнения эффекта, setTimout, setInterval и т. д., поэтому, когда Redux сталкивается с асинхронными операциями, как с этим обращаться?

Сначала мы расширяем простой пример, затем реализуем его различными способами, основные эффекты таковы:

Обработка асинхронности без промежуточного программного обеспечения

Здесь я использую API официального сайта CNode, чтобы получить заголовок статьи на главной странице и отобразить их все, и справа есть кнопка X, нажмите кнопку X, чтобы удалить заголовок. Для асинхронных запросов мы используем упакованную библиотеку axios.Вы можете инициировать асинхронные запросы следующим образом:

const response = await axios.get('/api/v1/topics')

тогда в вашемpackage.jsonДобавить поле прокси в файл

{
  "proxy": "https://cnodejs.org",
  //...
}
скопировать код

поэтому, когда вы посещаетеlocalhost:3000/api/v1/topicsФон Node автоматически перенаправит запрос в CNode для вас, а затем вернет вам полученный результат.Этот процесс прозрачен для вас, что может эффективно избежать междоменных проблем.

cd asynchronous_without_redux_middleware/
yarn 
yarn start
скопировать код

Старые правила, давайте сначала посмотрим на структуру проекта:

├── actionCreator
│   └── index.js
├── actionTypes
│   └── index.js
├── constants
│   └── index.js
├── index.js
├── reducers
│   └── index.js
├── store
│   └── index.js
└── views
    ├── newsItem.css
    ├── newsItem.js
    └── newsList.js
скопировать код

Всего у нас есть три типа действий в асинхронных запросах, а именно FETCH_START, FETCH_SUCCESS, FETCH_FAILURE, поэтому есть четыре состояния (константы), соответствующие представлению: INITIAL_STATE, LOADING_STATE, SUCCESS_STATE, FAILURE_STATE.

actionCreator инкапсулирует actionTypes еще одним слоем, и возвращаемые действия по-прежнему являются синхронными действиями, а основная логика завершается компонентом представления.

// views/newsList.js
const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        fetchNewsTitle: async() => {
            dispatch(actionCreator.fetchStart())
            try {
                const response = await axios.get('/api/v1/topics')
                if(response.status === 200) {
                    dispatch(actionCreator.fetchSuccess(response.data))
                }else {
                    throw new Error('fetch failure')
                }
            } catch(e) {
                dispatch(actionCreator.fetchFailure())
            }
        }
    }
}

Мы видим, что перед тем, как инициировать асинхронный запрос, мы сначала инициируем действие FETCH_START, а затем начинаем инициировать асинхронный запрос.При успешном запросе мы инициируем действие FETCH_SUCCESS и передаем данные, а когда запрос не выполняется, мы инициируем действие FETCH_FAILURE.

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

  1. Всякий раз, когда мы инициируем асинхронный запрос, нам всегда нужно писать такой повторяющийся код и вручную обрабатывать полученные данные.На самом деле, мы предпочитаем самостоятельно переваривать и обрабатывать последующие шаги после асинхронного возврата.Для бизнес-уровня мне нужно только дать Отправьте сигнал, например: действие FETCH_START, не заботьтесь о последующем контенте, приложение может помочь мне справиться с этим.
  2. Когда у нас есть один и тот же асинхронный код в разных компонентах, нам лучше его абстрагировать и извлечь в общее место для обслуживания.
  3. Нет обработки гонки: нажмите кнопку, чтобы получить заголовок CNode и отобразить его, потому что время возврата асинхронного запроса неизвестно, а запрос по клику может появиться после нескольких кликов.Последний результат запроса.

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

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        fetchNewsTitle:() => {
            xxx.fetchStart() 
        }
    }
}

Одна из идей состоит в том, чтобы самостоятельно извлекать эти асинхронные вызовы в общую общую папку асинхронных операций, и каждый компонент, которому нужно вызывать асинхронные операции, будет ходить в эту директорию для получения требуемых функций, но в этом есть проблема, т.к. для инициации. , то поле dispatch обязательно, а это значит, что переменная dispatch должна явно передаваться в каждом вызове, а именно:

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        fetchNewsTitle:() => {
            xxx.fetchStart(dispatch) 
        }
    }
}

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

Асинхронное действие

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

view -> asyncAction -> wait -> action -> reducer -> newState -> container component

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

  • Синхронизация: store.dispatch(actionCreator())
  • Асинхронный случай: asyncAction(store.dispatch)

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

Redux-thunk: лаконично и просто

Redux itself can only handle synchronization of action, but can handle other types of action intercepted by middleware, such as functions (thunk), then the callback trigger common action, in order to achieve asynchronous processing, all Redux asynchronous program at this point It is аналогичный.

Сначала мы перепишем наш предыдущий пример с помощью redux-thunk.

cd asynchronous_with_redux_thunk/
yarn 
yarn start
скопировать код

Во-первых, вам нужно внедрить в хранилище промежуточного программного обеспечения redux-thunk.

import { createStore, applyMiddleware } from 'redux'
import reducer from '../reducers'
import thunk from 'redux-thunk'

// ...

export default createStore(reducer, initValue, applyMiddleware(thunk))

Таким образом, промежуточное ПО redux-thunk может обработать действие, прежде чем передать его редюсеру.

Мы переписываем наш объект actionCreator, и нам нужно так много синхронных действий, которые можно сделать всего одним методом.

// actionCreator/index.js
import * as actionTypes from '../actionTypes'
import axios from 'axios'

export default {
    fetchNewsTitle: () => {
        return async (dispatch, getState) => {
            dispatch({
                type: actionTypes.FETCH_START,
            })
            try {
                const response = await axios.get('https://cnodejs.org/api/v1/topics')
                if(response.status === 200) {
                    dispatch({
                        type: actionTypes.FETCH_SUCCESS,
                        news: response.data.data.map(news => news.title),
                    })
                }else {
                    throw new Error('fetch failure')
                }
            } catch(e) {
                dispatch({
                    type: actionTypes.FETCH_FAILURE
                })
            }
        }
    },
    // ...
}

На этот раз fecthNewsTitle уже не просто возвращает JS-объект, а возвращает функцию, в которой можно получить текущее состояние и диспетчеризацию, а затем сюда инкапсулируются все предыдущие асинхронные операции. Это середина redux-thunk, которая позволяет диспетчеризации обрабатывать не только объекты JS, но и функции.

После этого мы просто вызываем бизнес-код:

// views/App.js
const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        fetchNewsTitle: () => {
            dispatch(actionCreator.fetchNewsTitle())
        }
    }
}

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

redux-thunk — это общее решение, основная идея которого состоит в том, чтобы сделать действие переходником, например так:

  • Синхронизация: диспетчеризация (действие)
  • Асинхронный случай: диспетчеризация (преобразователь)

redux-thunk, кажется, делает много работы, но его довольно просто реализовать:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

На первый взгляд, существует много уровней вызовов цепочек стрелочных функций. На самом деле это связано с механизмом промежуточного программного обеспечения. Нам нужно только позаботиться о том, чтобы при передаче действия промежуточному программному обеспечению оно определяло, является ли действие функцией или нет.Если это функция, то перехватите текущее действие, потому что в текущем закрытии есть переменные dispatch и getState, и передайте две переменные в функцию и выполните ее.Это ключевая причина, по которой мы можем использовать dispatch и getState когда actionCreator возвращает функцию. Когда redux-thunk видит, что переданное действие является функцией, он перехватывает и выполняет его. Возвращаемое значение действия больше не волнует, потому что оно вообще не передается.Если это не функция, оно отпустит действие и позволит следующему промежуточному программному обеспечению обработать его (next(action))

Таким образом, наш предыдущий пример можно понимать как:

view -> 函数 action -> redux-thunk 拦截 -> 执行函数并丢弃函数 action -> 一系列 action 操作 -> reducer -> newState -> container component
скопировать код

Нетрудно понять, что мы инкапсулируем асинхронную операцию, изначально размещенную в публичном каталоге, в экшене, и через механизм промежуточного программного обеспечения можно получить значение диспетчеризации внутри экшена, чтобы в экшене можно было генерировать больше синхронных объектов экшена. .

Решение redux-thunk достаточно для повседневного использования в небольших приложениях, но для больших приложений вы можете найти некоторые неудобства.Для объединения нескольких действий, отмены действий и обработки суждений о гонках, очевидно, немного бессильны, мы поговорим об этих вещах позже. .

Идея redux-thunk великолепна, но по факту код в определенной степени похож, например, весь код фактически обрабатывается на три части: запрос, успех и неудача, что заставляет нас естественно думать о промисе, который также делится на ожидание, выполнено и отклонено три состояния.

Redux-обещание: слишком худой

Promise представляет собой промис, который используется для решения проблемы ада асинхронных обратных вызовов.Во-первых, давайте посмотрим на исходный код промежуточного программного обеспечения redux-promise:

import { isFSA } from 'flux-standard-action';

function isPromise(val) {
  return val && typeof val.then === 'function';
}

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}

Как redux-Thunk, мы откладываем в сторону сложные вызовы функций straced arrow, одно промежуточное программное обеспечение, - это определить, является ли действие или действие. Poyload - это объект для обещания, и если это так, он также перехватывает и ждет, чтобы объект обещания разрешил вернуть отправку Вызывается после данных. Аналогичным образом, действие обещания не будет передано редуктору для обработки. Если это не объект обещания, он не будет обработан.

Таким образом, асинхронный поток действий становится таким:

view -> Promise action -> redux-promise 拦截 -> 等待 promise resolve -> 将 promise resolve 返回的新的 action(普通) 对象 dispatch -> reducer -> newState -> container component
скопировать код

Через промежуточное программное обеспечение redux-promise мы можем написать действия обещания, мы модифицируем предыдущий пример:

cd asynchronous_with_redux_promise/
yarn 
yarn start
скопировать код

Давайте изменим actionCreator:

// actionCreator/index.js
export default {
    fetchNewsTitle: () => {
        return axios.get('/api/v1/topics').then(response => ({
            type: actionTypes.FETCH_SUCCESS,
            news: response.data,
        })).catch(err => ({
            type: actionTypes.FETCH_FAILURE,
        }))
    },
}

Изменить промежуточное программное обеспечение магазина redux-promise

// store/index.js

import { createStore, applyMiddleware } from 'redux'
import reducer from '../reducers'
import reduxPromise from 'redux-promise'

export default createStore(reducer, initValue, applyMiddleware(reduxPromise))

Эффект: нет промежуточного состояния загрузки

Но если вы используете redux-promise, это эквивалентно отсрочке выполнения действия, а затем повторной отправке действия после получения результата. Это проблема, то есть не получается инициировать действие fetch_start, потому что в ActionCreator нет Dispatch, хотя REDUX-Promise дает Action задержать выполнение, но нет возможности инициировать multi-Action запрос.

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

const thunk = ({ dispatch, getState }) => next => action => {
  if(typeof action.async === 'function') {
      return action.async(dispatch, getState);
  }
  return next(action);
}

Если объект действия можно понимать таким образом, то мы не требуем, чтобы асинхронный объект действия, обрабатываемый промежуточным программным обеспечением Promise, был объектом обещания, только поле реформы объекта действия является полем обещания, а объект действия может иметь другие поля. содержать больше информации. Итак, мы можем сами написать промежуточное ПО:

// myPromiseMiddleware
const isPromise = (obj) => {
    return obj && typeof obj.then === 'function';
}

export default ({ dispatch }) => (next) => (action) => {
    const { types, async, ...rest } = action
    if(!isPromise(async) || !(action.types && action.types.length === 3)) {
        return next(action)
    }
    const [PENDING, SUCCESS, FAILURE] = types
    dispatch({
        ...rest,
        type: PENDING,
    })
    return action.async.then(
        (result) => dispatch({ ...rest, ...result, type: SUCCESS }),
        (error) => dispatch({ ...rest, ...error, type: FAILURE })
    )
}

Нетрудно понять, что промежуточное ПО также принимает JS-объект действия.Этот объект должен удовлетворять тому, что асинхронное поле является объектом Promise, а длина поля типа равна 3. В противном случае это не тот объект действия, который нам нужен. Поле типа, которое мы передаем, представляет собой массив. Это FETCH_START, FETCH_SUCCESS и FETCH_FAILURE, которые эквивалентны созданию слоя соглашения, позволяющего промежуточному программному обеспечению помочь нам переварить такие асинхронные действия. Когда асинхронный объект обещания возвращается, вызовите FETCH_SUCCESS , FETCH_FAILURE действие.

Переписываем actionCreator

// actionCreator/index.js
export default {
    myFetchNewsTitle: () => {
        return {
            async: axios.get('/api/v1/topics').then(response => ({
                news: response.data,
            })),
            types: [ actionTypes.FETCH_START, actionTypes.FETCH_SUCCESS, actionTypes.FETCH_FAILURE ]
        }
    },
}

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

Redux-сага: мощная

redux-saga также является промежуточным программным обеспечением для решения асинхронных действий redux, но оно отличается от предыдущего решения и открывает новый путь:

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

Сделайте асинхронное поведение отдельным слоем (называемым сагой) в архитектуре, ни в создателе действия, ни в редюсере.

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

Подробную документацию можно найти по адресу:Redux-saga Beginner Tutorial

Далее я также приведу множество примеров, иллюстрирующих преимущества redux-saga.

Давайте сначала перепишем наш старый пример:

cd asynchronous_with_redux_saga/
yarn 
yarn start
скопировать код

Первый взгляд на actionCreator:

import * as actionTypes from '../actionTypes'
export default {
    fetchNewsTitle: () => {
        return {
            type: actionTypes.FETCH_START
        }
    },
  // ...
}

Стало очень чисто, ведь логика обработки асинхронности уже есть в креаторе и перенесена в сагу, давайте посмотрим, как написана сага.

// sagas/index.js

import { put, takeEvery } from 'redux-saga/effects'
import * as actionTypes from '../actionTypes'
import axios from 'axios'

function* fetchNewsTitle() {
    try {
        const response = yield axios.get('/api/v1/topics')
        if(response.status === 200) {
            yield put({
                type: actionTypes.FETCH_SUCCESS,
                news: response.data,
            })
        }else {
            throw new Error('fetch failure')
        }
    } catch(e) {
        yield put({
            type: actionTypes.FETCH_FAILURE
        })
    }
}

export default function* fecthData () {
    yield takeEvery(actionTypes.FETCH_START, fetchNewsTitle)
}

Можно обнаружить, что асинхронная операция, написанная здесь, в основном такая же, как асинхронная операция, написанная ранее.Приведенный выше код не понимает, takeEvery отслеживает все действия, когда находитaction.type === FETCH_STARTПри выполнении функции fetchNewsTitle обратите внимание, что это просто для отслеживания действия и не будет перехватывать действие, а это означает, что действие FETCH_START по-прежнему будет обрабатываться редюсером, а оставшуюся функцию fetchNewsTitle легко понять, то есть выполнить так называемую асинхронную операцию. отправлять.

Наконец, нам нужно использовать промежуточное ПО саги в магазине.

// store/index.js
import createSagaMiddleware from 'redux-saga'
import mySaga from '../sagas'

const sagaMiddleware = createSagaMiddleware()

// ...

export default createStore(reducer, initValue, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(mySaga)

Зарегистрировав промежуточное ПО саги и запустив задачу саги, то есть fecthData, упомянутый ранее.

На таком простом примере мы видим, что сага отделяет все операции с побочными эффектами от actionCreator и reducer и выполняет автоматическую обработку, отслеживая действия.По сравнению со схемой создания асинхронного действия, она может гарантировать, что действия, запускаемые компонентами, являются чистыми. объект.

Справочный ответ:Краткое изложение практики Redux-saga

Есть свои сценарии применения с асинхронным способом написания redux-thunk.Разницы между достоинствами и недостатками нет.Так называемое существование разумно. По сравнению с этим методом redux-saga имеет следующие характеристики:

  1. Жизненный цикл другой, redux-saga можно понимать как длительную транзакцию, работающую в фоновом режиме, а redux-thunk — это действие, потому что redux-saga может больше.
  2. redux-saga имеет много декларативных и простых в тестировании эффектов, таких как неблокирующие вызовы и задачи прерывания, которые очень подходят для сценариев со сложной бизнес-логикой.
  3. Самая привлекательная часть redux-saga заключается в том, что она сохраняет первоначальный смысл действия, делает действие простым и разделяет все места с побочными эффектами.Эти функции позволяют сохранить ясность кода в сценариях с простой бизнес-логикой.

На мой взгляд: метод redux-thunk + async/await имеет низкую стоимость обучения и больше подходит для менее сложных сценариев асинхронного взаимодействия. Его нельзя использовать для оценки конкуренции, комбинаций нескольких действий, отмены асинхронности и т. д. Redux-saga все еще может позволить вам писать код четко и интуитивно в сложных сценариях асинхронного взаимодействия, но стоимость обучения относительно высока.

Выше мы представили три асинхронных схемы redux.На самом деле существует множество асинхронных схем redux, таких как: redux-observale, которая использует rxjs для написания асинхронных запросов, и redux-loop, которая достигается написанием статей о редьюсерах.Асинхронный эффект . На самом деле, существуют тысячи схем, каждая из которых имеет свою фракцию.Каждая схема имеет свой подходящий сценарий.Наиболее ценно выбрать асинхронную схему редукции, которую вы используете в соответствии с вашими реальными потребностями.

Сравнение асинхронных схем thunk и saga

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

  1. Без обработки анти-дрожания, если пользователь лихорадочно нажимает кнопку, асинхронные запросы будут непрерывно инициироваться, что незаметно расходует пропускную способность.
  2. Нет никакого конкурса. Нажмите кнопку, чтобы получить заголовок CNODE и сделать его. Поскольку время возврата асинхронного запроса является неопределенным, запрос на щелчок может появиться после нескольких кликов. Последний результат запроса.
  3. Обработку отмены я не делал, просто подумайте, в некоторых сценариях в процессе ожидания пользователь может отменить асинхронную операцию, и результат в это время отображаться не будет.

Ниже мы перепишем пример, используем redux-thunk и redux-saga для решения вышеуказанных проблем и сравним их.

Пример, который мы сделаем, выглядит следующим образом:

  1. Есть две кнопки, используемые для имитации асинхронных запросов, которые отвечают на данные в течение 5 с и 1 с соответственно.Нам нужно обеспечить последовательность нажатий кнопок, то есть, когда данные возвращаются через 5 с, последнее значение 1000 не будет перезаписано на убедитесь, что данные, отображаемые на странице, всегда будут отображаться — это данные, полученные по последнему клику.
  2. Обработка против сотрясений, повторное нажатие кнопки в течение 1000 мс не сработает.
cd race_with_redux_thunk/
yarn 
yarn start
скопировать код

Проверьте actionCreator:

// actions/index.js
import * as actionTypes from '../actionTypes'
let nextId = 0
let prev = 0
export const updateData = (ms) => {
     return (dispatch) => {
        let id = ++nextId
        let now = + new Date()
        if(now - prev < 1000) {
            return;
        }
        prev = now;

        const checkLast = (action) => {
            if(id === nextId) {
                dispatch(action)
            }
        }
        setTimeout(() => {
            checkLast({
                type: actionTypes.UPDATE_DATA,
                payload: ms,                    
            })
        }, ms)
    }
}
  1. Обработка конкуренции: вы можете добавить переменную модуля nextId в actionCreator, сгенерировать идентификатор с тем же значением, что и текущий nextId, когда функция выполняется, и, наконец, когда данные возвращаются, определить, имеет ли текущий идентификатор то же значение, что и текущий идентификатор. nextId.Если он один и тот же, это доказывает, что на этот раз операция является последней операцией, тем самым гарантируя, что запрос является последним.
  2. Обработка против сотрясения: кроме того, переменная prev используется для записи времени последнего щелчка, и оценивается разница между текущим временем и 1 с, чтобы определить, следует ли выполнять последующие операции, и значение prev будет обновлено.

Если redux-saga перепишет этот пример, каков будет эффект?

cd race_with_redux_saga/
yarn 
yarn start
скопировать код

Во-первых, редуктор не так уж и хлопотлив, ему нужно только подать сигнал

// actions/index.js
import * as actionTypes from '../actionTypes'

export const updateData = (ms) => {
     return {
        type: actionTypes.INITIAL,
        ms
     }
}

Дело в том, чтобы следить за задачей саги INITIAL

// sagas/index.js
import { put, call, take, fork, cancel } from 'redux-saga/effects'
import * as actionTypes from '../actionTypes'

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))

function* asyncAction({ ms }) {
    let promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve(ms)
        }, ms)
    })
    let payload = yield promise
    yield put({
        type: actionTypes.UPDATE_DATA,
        payload: payload
    })
}

export default function* watchAsyncAction() {
    let task 
    while(true) {
        const action = yield take(actionTypes.INITIAL)
        if(task) {
            yield cancel(task)
        }
        task = yield fork(asyncAction, action)
        yield call(delay, 1000)
    }
}

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

take отслеживает действие типа INITIAL.Сначала определяет, есть ли задача, которая не выполнялась ранее.Если есть, отменяет задачу, чтобы обеспечить оценку конкуренции.Затем делает неблокирующий вызов через fork, и наконец, приостанавливается на одну секунду. вызов представляет собой блокирующий вызов. Здесь В течение определенного периода времени задача саги больше не будет обрабатывать новые входящие действия, поэтому все НАЧАЛЬНЫЕ действия в течение этого периода будут игнорироваться, так что будет выполняться обработка против встряхивания, и put эквивалентно операции отправки.

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

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

Наконец, давайте сравним различия в написании между thunk и saga на примере. Эффект примера следующий:

Нажмите кнопку, чтобы увеличить счетчик на 1 в секунду, нажмите еще раз, чтобы отменить счетчик приращения или автоматически отменить счетчик приращения через 5 с.

Давайте посмотрим, как работает Redux-thunk:

cd cancellable_counter_with_redux_thunk/
yarn 
yarn start
скопировать код

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

import {
    INCREMENT,
    COUNTDOWN,
    COUNTDOWN_CANCEL,
    COUNTDOWN_TERMIMATED
} from '../actionTypes'

let incrementTimer
let countdownTimer

const action = type => ({ type })

export const increment = () => action(INCREMENT)

export const terminateCountDown = () => (dispatch) => {
    clearInterval(incrementTimer)
    clearInterval(countdownTimer)
    dispatch(action(COUNTDOWN_TERMIMATED))
}

export const cancelCountDown = () => (dispatch) => {
    clearInterval(incrementTimer)
    clearInterval(countdownTimer)
    dispatch(action(COUNTDOWN_CANCEL))
}

export const incrementAsync = (time) => (dispatch) => {
    incrementTimer = setInterval(() => {
        dispatch(increment())
    }, 1000)
    dispatch({
        value: time,
        type: COUNTDOWN,
    })
    countdownTimer = setInterval(() => {
        time--
        if(time <= 0) {
            dispatch(cancelCountDown())
        }else {
            dispatch({
                value: time,
                type: COUNTDOWN,
            })
        }
    }, 1000)
}

IncrementAsync запускает два таймера для увеличения счетчика и обратного отсчета. TerminateCountDown — это кнопка ручного триггера, которая приводит к очистке двух таймеров. Внутри таймера countdownTimer по истечении времени, когда время меньше 0, срабатывает CancelCountDown для отмены всех таймингов устройство.

Мы видим, что redux-thunk немного бессилен при работе с таким асинхронным управлением процессами, ему нужно создавать несколько таймеров для параллельного изменения данных, когда сцена более сложная, код становится немного запутанным и менее читабельным.

Перепишите предыдущий пример с помощью redux-saga:

cd cancellable_counter_with_redux_saga/
yarn 
yarn start
скопировать код
// sagas/index.js
import { 
    INCREMENT_ASYNC, 
    INCREMENT, 
    COUNTDOWN,
    COUNTDOWN_TERMIMATED,
    COUNTDOWN_CANCEL,
} from '../actionTypes'
import { take, put, cancelled, call, race, cancel, fork } from 'redux-saga/effects'
import { delay } from 'redux-saga'

const action = type => ({ type })

function* incrementAsync() {
    while(true) {
        yield call(delay, 1000)
        yield put(action(INCREMENT))
    }
}

function* countdown({ ms }) {
    let task = yield fork(incrementAsync)
    try {
        while(true) {  
            yield put({
                type: COUNTDOWN,
                value: ms--,
            }) 
            yield call(delay, 1000)
            if(ms <= 0) {
                break;
            }
        }
    } finally {
        if(!(yield cancelled())) {
            yield put(action(COUNTDOWN_CANCEL))
        }
        yield cancel(task)
    }
}

export default function* watchIncrementAsync() {
    while(true) {
        const action = yield take(INCREMENT_ASYNC)
        yield race([
            call(countdown, action),
            take(COUNTDOWN_TERMIMATED)
        ])
    }
}

Это все, что касается асинхронного управления Redux, я надеюсь, что у каждого есть что-то полезное!