null

Redux middleware функции / Усилители - погружение в эльфийский

Потихоньку погружаясь в redux и начиная осозновать правила мироздания его идеологии, многие приходят к следующему вопросу: "А куда я должен прилепить свои запросы к серверу?". В ответ на этот вопрос, вас знакомят с понятием 'middleware' функций, что на русский можно перевести как усилитель. И если в то, зачем эти функции нужны и почему такие вещи как запросы к API нужно именно здесь понять можно. А вот взгляд на синткасис объявления 'middleware' у человека недавно познакомившейся со всей этой кухней react, redux, es6 и прочего веселья вызывает ужас вперемешку с криками 'ЧООО?' и расшатывание только восстановившейся психики после привыкания к предыдущим новым терминам и концепциям. Давайте взглянем на это и попытаемся спуститься глубже, чтобы понять как устроена middleware.

const middleware = store => next => action => {
}

 

Да, да господа. Middleware - ф-ция, которая принимает store, возвращает ф-цию которая принимает dispatch, которая возвращает ф-цию которая принимает action. Заправлено это всё лямбда синтаксисом из ES6. Зачем? Источники говорят, что это чудо пришло к нам из функционального программирования и лежащим в его основе мат. аппарата. Можно почитать здесь.

Мы получили, что middleware функция  по факту принимает 3 аргумента: store, next и action. Теперь предлагаю взглянуть в код redux и посмотреть, как там всё устроено.

 

Структура библиотеки выглядит так:

Redux представляет нам api из 5 функций: createStore, combineReducers, applyMiddleware, bindActionCreators, compose.  Сегодня я хочу остановиться лишь на рассмотрении трех из них (подчеркнуты).

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

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false
  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

 

createStore как ясно из названия, возвращает хранилище приложения. Аргументами функции могут являться 3 параметра: редьюсер, начальное состояние приложение и enchancer (по факту, эта функция которая возвращается  после вызова applyMiddleware). Функция начинается с обработки входных аргументов и выяснения того, имеются ли в наличии какие-либо 'middleware'. Рассмотрены также ситуация, что начальное состояние не было укзаано, а вторым аргументом передана функция, тогда второй аргумент считается что это и есть 'усилитель'. Если усилитель имеется, то вместо хранилища возвращается результат выполнения этой функции. Т.е. поток управления передаётся на неё.

return enhancer(createStore)(reducer, preloadedState)

Иначе формируется объект состояния приложения. В котором нас интересуют лишь свойства dispatch и getState.

 return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

 

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

В общих чертах createStore мы рассмотрели. Теперь можно обратиться к коду applyMiddleware

import compose from './compose'

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

 

applyMiddleware принимает массив middleware функций, формат которых мы рассмотрели вначале статьи, а возвращает функцию усилитель (в коде createStore она была параметром enchance). Здесь же мы видим, что applyMiddleware возвращает лямбду в лямбде. Я предлагаю, попытаться абстрагироваться от такого подхода и представлять синтаксис (x) => (y) => {}, как функцию принимающую 2 аргумента x, y.

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

return enhancer(createStore)(reducer, preloadedState)

Enchance - остается пустой. И после вызова функции createStore из applyMiddlware мы получим просто объект хранилища, который будет содержать описанный выше dispatch и другие менее интересные нам вещи.

 const store = createStore(reducer, preloadedState, enhancer)

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

 

    let dispatch = store.dispatch
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))

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

 

dispatch = compose(...chain)(store.dispatch)

.Следующим этапом является преобразование dispatch в цепочку функций. Функция compose же выглядит так:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

Чтобы понять данный код, стоит воспользоваться описанием функции reduce, также чтобы увидеть как dispatch становится списком функций завязанных друг с другом через next, можно повыполняв пошагово код инициализации store и дойдя до этого участка. Каждая функция  должна вызывать next(action), чтобы не прервать цепочку. В конце можно увидеть, что результатом работы createStore с указанным 3 аргументом с вызывом в нём applyMiddleware будет объект хранилища, но с преобразованным dispatch

 return {
      ...store,
      dispatch
    }

 

На сегодня я вас покину. Удачи!