Сравнение схемы асинхронного потока данных Redux

внешний интерфейс React.js Promise Redux
Сравнение схемы асинхронного потока данных Redux

Основная идея Redux — строгий односторонний поток данных, который можно передать только черезdispatch(action)Способ модификации магазина, процесс выглядит следующим образом:

view ->  action -> reducer -> store

В реальном бизнесе часто бывает большое количество асинхронных сценариев, самый примитивный подход — использование React-компонентов.componentDidMountПри инициализации асинхронного потока передайтеcallbackилиpromiseспособ вызоваdispatch(action), делая так положитьviewслой иmodelСлои смешиваются друг с другом, связь серьезная, а последующее обслуживание очень сложно.
предыдущая статьяИнтерпретация принципа промежуточного программного обеспечения ReduxВы можете знать, что промежуточное программное обеспечение (middleware) было переписаноdispatchметод, поэтому более гибкое управлениеdispatchсинхронизации, что очень эффективно для обработки асинхронных сценариев. Поэтому авторы Redux также рекомендуют использовать промежуточное ПО для обработки асинхронных потоков. Распространенное промежуточное ПО в сообществе:redux-thunk,redux-promise,redux-saga,redux-observableЖдать.

redux-thunk: простой и грубый

Как асинхронное промежуточное ПО, написанное самим автором Redux, принцип очень прост: сам Redux обрабатывает только простые синхронные действия объекта, но его можноredux-thunkОбработчики перехвата типа function (функция)action, через обратный вызов для управления общим триггеромaction, чтобы достичь цели асинхронности. Его типичное использование выглядит следующим образом:

//constants 部分省略
//action creator
const createFetchDataAction = function(id) {
    return function(dispatch, getState) {
        dispatch({
            type: FETCH_DATA_START, 
            payload: id
        })
        api.fetchData(id) 
            .then(response => {
                dispatch({
                    type: FETCH_DATA_SUCCESS,
                    payload: response
                })
            })
            .catch(error => {
                dispatch({
                    type: FETCH_DATA_FAILED,
                    payload: error
                })
            }) 
    }
}
//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case FETCH_DATA_START : 
        // 处理 loading 等
    case FETCH_DATA_SUCCESS : 
        // 更新 store 等处理
    case FETCH_DATA_FAILED : 
        // 提示异常
    }
}

можно увидеть с помощьюredux-thunkназад,action creatorВозвращаемое действие может быть функцией, и сама функция в нужный моментdispatchПодходящее нормальное действие. И в этом нет никакой магии,redux-thunkЕго основной исходный код выглядит следующим образом:

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

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

redux-promise: выполнять обещания до конца

redux-thunkпосле того, как обещание, возвращенное из API, разрешаетсяdispatchв другое действие, затем прямо дать обещание как действиеdispatch, пусть промежуточное программное обеспечение обрабатывает процесс разрешения, разве нельзя было бы написать меньше.then().catch()такой код?redux-promiseИменно это решило проблему. То же самое для перехода к данным из бэкэнда, и его типичное использование:

const FETCH_DATA = 'FETCH_DATA'
//action creator
const getData = function(id) {
    return {
        type: FETCH_DATA,
        payload: api.fetchData(id) // 直接将 promise 作为 payload
    }
}
//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case FETCH_DATA: 
        if (action.status === 'success') {
             // 更新 store 等处理
        } else {
                // 提示异常
        }
    }
}

Таким образом, по сравнению сredux-thunkСтиль письма сильно похудел. Его основной исходный код иredux-thunkАналогично, еслиactionилиaction.payloadдаPromiseТип разрешает это, вызывая текущийactionкопию и установите полезную нагрузку в результат успеха/неудачи промиса.

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action))  {// 判断是否是标准的 flux 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-promiseнаписаниеreducerКогда действие получено, оно было разрешено, поэтому, если вы хотите его обработатьloadingВ этом случае необходимо написать дополнительный код и добавить его к простому объекту, такому как действие.statusАтрибуты дадут людям ощущение неравномерности, что может заключаться в том, что в яйцах легко запутаться, если шаги слишком велики.

redux-thunkа такжеredux-promiseИспользование на самом деле похоже, оба запускают функцию/обещание и позволяют промежуточному программному обеспечению решать самоdispatchСинхронизация действительно асинхронных данных, достаточная для большинства сценариев. Однако для сценариев с более сложными асинхронными ситуациями нам часто приходится писать много бизнес-кода, и после возврата асинхронного результата его может потребоваться соответствующим образом изменить.storeТаким образом, мы сталкиваемся с запутанной проблемой: бизнес-код помещается вactionслой илиreducerвнутри? Например, администратор замораживает учетную запись пользователя и одновременно требует ее обновления.storeвнутриAllUserListа такжеPendingUserlist, на данный момент есть два варианта:

  1. Активировать кнопку при нажатии кнопкиPEND_USERЗатем действие редуктора одновременно обновляется в соответствующем переключателе редуктора.AllUserListа такжеPendingUserlist
  2. Срабатывает при нажатии кнопкиREFRESH_USER_LISTа такжеREFRESH_PENDING_USER_LISTДва действия, а затем обновить два места в редюсереstore.
    Вообще говоря, для пользователя более разумно инициировать действие действием, но это может быть повторно использовано в другом месте.REFRESH_USER_LISTТам, где действие разделено и обновлено, повторное использование более удобно, но в это время необходимо идти на компромисс.

redux-saga: точный и элегантный

а такжеredux-sagaОн может очень хорошо решить эту проблему, он добавляет к исходному потоку данных Redux.sagaслоя (не обращайте внимания на это странное имя 😂), отслеживайте действия и получайте новые действия для работы с магазином, которые будут представлены далее.redux-observableОпять же, основное использование можно резюмировать следующим образом:Acion in,action out.

Для вопроса только что,redux-sagaзаписывается как:

//action creator
const refreshUserListAction = (id)=>({type:REFRESH_USER_LIST,id:pendedUser.id})
const refreshPendingUserListAction = (id)=>({type:REFRESH_PENGDING_USER_LIST,id:pendedUser.id})
//saga
function* refreshLists() {
  const pendedUser = yield call(api.pendUser)
  // 将同时触发(put)两个 action
  yield put(refreshUserListAction())
  yield put(refreshPendingUserListAction())
}

function* watchPendUser() {
  while ( yield take(PEND_USER) ) {
    yield call(refreshLists) // 监听 PEND_USER 的 action,并执行(call)refreshLists 方法
  }
}
//reducer 省略

Таким образом, бизнес-логика очень понятна: два триггера запускаются одним «PEND_USER».REFRESHдействие и введитеreducer. И отделить бизнес-кодactionслой иreducerуровень, который уменьшает связанность кода и очень полезен для последующего обслуживания и тестирования.
Для более сложной асинхронности, такой как условия гонки,redux-sagaЭто даже лучше:

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

Если система сделана с помощью react/redux, причина ошибки очевидна:actionпо прибытииreducerкогда следуетactionОн больше не нужен. Если вы используетеredux-thunk/redux-promiseЕсть два способа решить эту проблему:

  1. Определить, что находится в текущем хранилище, когда промис возвращаетсяidи до начала обещанияidЭто то же самое:
    function fetchWeiboComment(id){
     return (dispatch, getState) => {
         dispatch({type: 'FETCH_COMMENT_START', payload: id});
         dispatch({type: 'SET_CURRENT_WEIBO', payload: id}); // 设置 store 里 currentWeibo 字段
         return api.getComment(id)
             .then(response => response.json())
             .then(json => { 
                 const { currentWeibo } = getState(); // 判断当前 store 里的 id 和 promise 开始前的 id 是否相同:
                 (currentFriend === id) && dispatch({type: 'FETCH_COMMENT_DONE', playload: json})
             });
     }
    }
  2. Включите Weibo в действиеid, судите об этом при обработке редуктораidа такжеurlвнутреннийidТо же самое или нет, здесь нет кода.

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

import { takeLatest } from `redux-saga`

function* fetchComment(action) {
    const comment = yield call(api.getComment(action.payload.id))
    dispatch({type: 'FETCH_COMMENT_DONE', payload: comment})
}

function* watchLastFetchWeiboComment() {
  yield takeLatest('FETCH_COMMENT_START', fetchComment)
}

takeLatestметод фильтрацииaction, следующийFETCH_COMMENT_STARTизactionОтменить предыдущий, когда он прибудетFETCH_COMMENT_STARTизactionсрабатывает, последний сетевой запрос (состояние ожидания), который не вернул результат в это время, будет отменен.
Кроме тогоredux-sagaОн также предоставляет больше методов для обработки таких сценариев, как блокировка и параллелизм асинхронных запросов.Redux-saga Китайская документация.

Поэтому, если в проекте много сложных асинхронных сценариев, очень подходит redux-saga.

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

redux-observable: более элегантные операции

можно увидетьredux-sagaидеи и предыдущиеredux-thunkЕсть большая разница, он реактивный (Reactive Programming):

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

Для слоя действий, отправной точки потока данных, ему нужно только запуститьFETCH_COMMENT_STARTПоток событий может завершить обновление всех данных, не заботясь о последующей обработке изменений данных.
Говоря об отзывчивости, мы должны упомянутьRxJSсейчас,RxJSЭто мощная библиотека реактивного программирования, которая обеспечивает мощные возможности композиции и управления потоками данных.RxJSИдея «все течет» в книге может быть очень запутанной для пользователей, мало знакомых с функциональным программированием (FP), но она внезапно станет ясной после того, как они овладеют ею. существуетRxJS, наблюдатель (Observer) подписывается на наблюдаемый (Observable), ниже — наблюдаемые и традиционныеPromise,GeneratorКонтраст:

можно увидетьObservableМогуасинхронныйвернутьнесколькоВ результате обеспечивается больший оперативный контроль над данными. а такжеredux-observableосновывается наRxJSРеализовано промежуточное ПО, которое создает побочные эффекты, создавая и отменяя асинхронные действия.
redux-observableСлой, отвечающий за асинхронность, называетсяEpic(Не обращайте внимания на странное название), Epic берет функцию, которая принимает поток действий в качестве параметра и возвращает поток действий.
Давайте рассмотрим простой пример:

//epic
const fetchWeiboCommentEpic = action$=>
    action$.ofType(FETCH_COMMENT_START) //ofType 表示过滤type 为 FETCH_COMMENT_START 的 action
        .switchMap(action=>//switchMap 的作用类似 saga 中的 takeLatest,新的 action 会将老的 action 取消掉
            Observable.fromPromise(api.getComment(action.payload.id))// 将 promise 转化成 Observable
                .map(comment=>({type: 'FETCH_COMMENT_DONE', payload: comment})) // 将返回的 Obsevable 映射(map)成一个普通 action
                .catch(err=>({type: 'FETCH_COMMENT_ERROR', payload: err})) // 这里的 err 也是一个 Observable,被捕获并映射成了一个 action
            )

Настроеноredux-observableПосле того, как промежуточное ПО может слушатьFETCH_COMMENT_STARTизactionИ асинхронно инициировать запрос и вернуть успех или неудачу с соответствующими даннымиaction. Видно, что благодаряRxJSмощный, такой какswitchMapоператор,redux-observableМожет завершить сложный процесс управления данными с помощью короткого кода. мы все еще можемfetchWeiboCommentEpicдля добавления более сложных операций, например, при полученииFETCH_COMMENT_STARTЗадержка 500 мс для повторной отправки запроса и получения искусственно отмененного действияFETCH_COMMENT_FORCE_STOPКогда запрос завершается (например, пользователь нажимает кнопку, чтобы отменить загрузку), после получения комментария Weibo он напомнит «обновить успешно»:

//epic
const fetchWeiboCommentEpic = action$=>
    action$.ofType(FETCH_COMMENT_START) 
        .delay(500) // 延迟 500ms 再启动
        .switchMap(action=>
            Observable.fromPromise(api.getComment(action.payload.id))
                .map(comment=>[
                    {type: 'FETCH_COMMENT_DONE', payload: comment},
                    {type: 'SET_NOTIFICATION', payload: comment} // 同时提醒 “刷新成功”
                ])
                .catch(err=>({type: 'FETCH_COMMENT_ERROR', payload: err}))
                .takeUntil(action$.ofType('FETCH_COMMENT_FORCE_STOP')) // 人为取消加载
            )

Давайте рассмотрим другой сценарий: когда пользователь вводит текст в поле поиска, результат извлекается из серверной части в режиме реального времени и возвращает наиболее подходящее приглашение (аналогично тому, что отображается при поиске в Google). Пользовательский ввод продолжает срабатыватьUSER_TYPINGдействие, продолжайте запрашивать серверную часть, на этот раз используйтеredux-thunkС ним будет сложнее иметь дело, иredux-observableЭто можно сделать элегантно:

const replaceUrl=(query)=>({type:'REPLACE_URL',payload:query})
const receiveResults = results=>({type:'SHOW_RESULTS',payload:results})
const searchEpic = action$=>action$.ofType('USER_TYPING')
    .debounce(500) // 这里做了 500ms 的防抖,500ms 内不停的触发打字的操作将不会发起请求,这样大大节约了性能
    .map(action => action.payload.query) // 返回 action 里的 query 字段,接下来的函数收到参数便是 query 而不是 action 整个对象了
    .filter(query => !!query) // 过滤掉 query 为空的情况
    .switchMap(query =>
        .takeUntil(action$.ofType('CLEARED_SEARCH_RESULTS'))
        .mergeMap(() => Observable.merge( // 将两个 action 以 Observable 的形式 merge 起来
          Observable.of(replaceUrl(`?q=${query}`)), 
          Observable.fromPromise(api.search(query))
            .map(receiveResults) 
        ))
    );

Кроме тогоRxJSтакже обеспечиваетWebSocketSubjectобъекты, с которыми можно легко и элегантно обращатьсяwebsocketЖдем сцены, здесь ее разворачивать не буду.
redux-observableкоторый предоставилObservableСравниватьGeneratorболее гибкая, благодаря мощномуRxJS,redux-observableОн более мощен в асинхронной обработке, что, вероятно, является наиболее элегантным в настоящее время.reduxАсинхронное решение тоже. Однако недостаток также очевиден, то есть слишком сложно начать работу.RxJSОсновные понятия не так просты для студентов, не знакомых с реактивным программированием. но через этот контактRxJSТакже очень полезно расширить свой кругозор. Поэтому вы можете попробовать использовать его в небольших проектах со сложными асинхронными сценариями.redux-observable, а стоимость обучения для всей команды должна учитываться в крупномасштабных совместных проектах с участием нескольких человек. В этом случае обычно используетсяredux-sagaбудет более рентабельным. В настоящее время используется внутри страныredux-observableИх не так много, и я надеюсь пообщаться с вами здесь больше.redux-observableсоответствующий практический опыт.

Суммировать

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