среда, 16 октября 2019 г.

Redux: усилители (enhancers).

Введение


В данной статье хотел бы рассказать про усилители в Redux: Что это такое? Какие задачи решают? Как реализованы?

Enhancer можно назвать усилителем, но мне такое название не нравится (Что он там усиливает?!). Больше подходит название «расширитель» или «расширение». 
Собственно, enhancer это функция, позволяющая добавить к хранилищу дополнительный функционал. 
Исходя из назначения, название «расширитель» подходит больше. Но как бы оно там не называлось, наша задача понять, как работает enhancer, а не выбрать название.

А какие задачи у Enhancer? И тут повторюсь – добавить или изменить функциональность хранилища. Функциональность хранилища меняется путем изменения текущих свойств или добавлением новых.
Например, applyMiddleware (единственный enhancer в составе redux) делает обертку dispatch, заменяя оригинальное одноименное свойство. Как делается обертка можно посмотреть здесь.
Также можно добавить дополнительное свойство к хранилищу, например, dispatchAsync (пример в конце статьи).

Теперь перейдем от назначения к реализации.


Реализация.


Как вообще enhancer применяется к хранилищу? А применяется он во время создания хранилища:
createStore(reducer, initialState, enhancer())
или
createStore(reducer, enhancer())
То есть передается либо вторым, либо третьим аргументом в функцию createStore. И заметьте, передается именно результат выполнения, а не сама функция.

Какая сигнатура у enhancer? Функция enhancer должна вернуть функцию, которая принимает функцию createStore в качестве параметра):
function enhancer1(optionalParams){
return function receiveCreateStore(createStoreFunc){
        }

}
Далее функция (у нас это receiveCreateStore), принимающая createStore (через createStoreFunc), должна вернуть функцию (createEnhancedStore). Эта функция и должна вернуть объект-хранилище.
function enhancer1(){
return function receiveCreateStore(createStoreFunc){
return function createEnhancedStore(…args){
return {
                             // возвращаем хранилище
                             // по задумке - расширенное
                        }
               }

       }
}
Обратите внимание на аргументы функции – args. Сюда будут передаваться 2 параметра: reducer и initialState. Первый и второй аргумент функции createStore.
Это была сигнатура. 

Как правило из последней функции (у нас createEnhancedStore) вызывается опять createStore (скажем так, стандартный… далее объясню) и уже ее результат используется для «обертывания» или дополнения.
Поэтому в сигнатуру я еще добавлю вызов функции createStore и возвращение нового – измененного store. Итого получится следующее:
function enhancer1(){
return function receiveCreateStore(createStoreFunc){
return function createEnhancedStore(…args){
const store = createStoreFunc(…args);
return {
…store,
dispatch: _dispatch,  // замена оригинального dispatch
newDispatch: _newDispatch // добавление нового свойства

                        }
               }
       }
}
Это был готовый каркас функции-расширителя. В таком виде можно и использовать.
А вот упрощенная версия с использованием стрелочной нотации:
const enhancer = () => createStoreFunc => (…args) => {
const store = createStoreFunc(…args);
        return {
…store,
dispatch: _dispatch,  // замена оригинального dispatch
newDispatch: _newDispatch // добавление нового свойства/метода
        }
}
Теперь нам нужно обратиться к исходникам redux, чтобы понять: почему именно такая сигнатура, зачем передается createStore, зачем она вызывается повторно и другие детали реализации.

Оперировать будем исходниками createStore, enhancer’ом applyMiddleware и приведенным каркасом. Потом приведу пример для добавления нового метода к хранилищу (взял опять-таки у Freeman’а).

Сначала возьмем функцию createStore.
 
Нас интересует:
А) кусок 1, где описаны параметры, 
Б) кусок 2, где проверяется, был ли передан enhancer
В) и кусок 3, где вызывается enhancer.

Логика такая: если был передан enhancer, то запускается сначала он и функция createStore передает ему сама себя в качестве параметра.
Enhancer вернет функцию, которая принимает аргументы и возвращает объект store:
return function createEnhancedStore(…args){
    return {
        // возвращаем хранилище
    }
}
Далее createStore запустит возвращенную функцию и передаст ей два аргумента: reducer и initialState (через …args):
 
Это мы дошли до последней функции в каркасе, которая собственно и расширяет хранилище.
Как она это делает? Обычно используется функция createStore которая передавалась при первом запуске enhancer (подчеркну, обычно!). createStore запускается с переданными параметрами (reducer и preloadedState): 
const store = createStoreFunc(…args);
Управление передается «оригинальной» функции createStore, но теперь она запускается только с двумя параметрами, то есть без enhancer.
Поскольку enhancer пустой, то отмеченный на картинке вызов пропускается:
 
Во время выполнения «собирается» store и он же, как объект, возвращается enhancer’у:

 Теперь у нас есть созданный store и можем его расширить:
const enhancer = () => createStoreFunc => (…args) => {
const store = createStoreFunc(…args);
        return {
…store,
dispatch: _dispatch,  // замена оригинального dispatch
newDispatch: _newDispatch // добавление нового свойства/метода
        }
}
Давайте теперь на примере applyMiddleware посмотрим как он делает обертку для dispatch.
 

1) Это не особо относится к делу – здесь формируется массив из аргументов функции applyMiddleware.
2) Тут сразу возвращается функция, принимающая createStore.
3) Здесь функция createStore продолжает выполнение и запускает функцию из пункта 2, передает ей createStore, в ответ получает функцию. Здесь она указывается без параметров, а в «нашем» каркасе с параметрами args. Но на самом деле потом все равно используются параметры arguments.
4) И вот enhancer как раз вызывает «оригинальную» функцию createStore, полученную на шаге 2, передавая ей аргументы reducer и initialState. Как мы помним опять запускается createStore, но уже без вызова enhancer. В переменной store храним результат выполнения.
5) В этом куске делается обертка для dispatch.
6) Возвращается хранилище store (созданное на этапе 4) с усиленным dispatch.


Пример.


Как и обещал, пример добавления нового метода к хранилищу:

export const asyncEnhancer = delay => createStoreFunction => (...args) => {
    const store = createStoreFunction(...args);
    return {
        ...store,
        dispatchAsync: (action) => new Promise((resolve, reject) => {
            setTimeout(() => {
                store.dispatch(action);
                resolve();
            }, delay);
        })
    };
}
Здесь возвращается оригинальный store, но с новым методом - dispatchAsync, который запускает "обычный", но с задержкой.

На этом все.

Ссылки:

Комментариев нет: