Хороший способ узнать, как все работает, - создать его с нуля. Большую часть времени мы используем библиотеку Javascript как черный ящик. Мы заботимся только о том, как использовать общедоступные API, которые они предоставляют, не заботясь о том, как это работает. Это руководство будет посвящено раскрытию магии Redux и покажет, как написать его с нуля. Вы можете найти его полезным в следующих аспектах:
- Лучше понять, как Redux работает под капотом.
- Постарайтесь думать с точки зрения автора. Может быть, в следующий раз у вас появится собственная библиотека!
- Изучите новые шаблоны для написания кода, которых вы никогда раньше не писали.
Если вы не знаете, что такое Redux, или никогда не использовали его раньше, вы можете изучить множество руководств. В этом руководстве предполагается, что вы уже знакомы с базовыми знаниями Redux.
Основные API
Прежде чем приступить к написанию Redux, нам нужно выяснить, с чего начать кодирование! Давайте сначала подведем итог тому, что нам нужно знать для создания Redux:
- Основная концепция Redux - это контейнер с предсказуемым состоянием для приложений Javascript.
- API магазина: магазин содержит все дерево состояний.
createStore(reducer, state, enhancer)
: создать магазин ReduxgetState()
: получить состояние магазинаdispatch(action)
: обновить состояние магазина на основе нового действияsubscribe(listener)
: подпишитесь на обновление магазина, добавив слушателя измененийreplaceReducer(nextReducer)
: расширенный API, который мы отложили в этом руководстве
3. combineReducers(reducers object)
: объединить несколько редукторов в один
4. applyMiddleware(middlewares)
: вернуть усилитель для createStore
для использования
Очевидно, Redux сосредоточен вокруг store
. Имеет смысл начать создавать store
API.
API магазина
createStore
Мы знаем, что createStore
принимает reducer
, state
и enhancer
в качестве входных данных и выводит объект store
. Итак, мы можем сначала построить каркас createStore
:
function createStore(reducer, initialState, enhancer) { const store = {}; const state = initialState; const listeners = []; store.getState = () => state; store.dispatch = (action) => {}; store.subscribe = (listener) => listeners.push(listener); return store; }
Здесь мы не используем store.state
для сохранения состояния. Он гарантирует, что единственный способ получить state
от store
- через общедоступный API getState
.
Отправлять
dispatch
использует reducer
для получения нового state
и уведомления каждого слушателя об изменении состояния. Итак, dispatch
может выглядеть так:
store.dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); };
Подписывайся
Текущий метод subscribe
позволяет нам добавить слушателя к store
. Но удалить слушателя невозможно. Для этого мы можем определить unsubscribe
метод:
store.unsubscribe = (listener) => { const index = listeners.indexOf(listener); if (index >= 0) { listeners.splice(index, 1); } };
У автора Redux Дана Абрамова есть хитрый трюк. Вместо определения нового метода он делает unsubscribe
method как возвращаемое значение subscribe
.
store.subscribe = (listener) => { listeners.push(listener); return () => { const index = listeners.indexOf(listener); listeners.splice(index, 1); }; }
Обратите внимание, что нам даже не нужно проверять, существует ли listener
в массиве listeners
, потому что listener
уже должен быть помещен в listeners
внутри замыкания.
CombinReducers
Когда приложение становится больше, функция reducer
может быть очень длинной. Хорошей практикой является разделение основного reducer
на несколько reducers
. Каждый меньший reducer
обрабатывает только поддерево состояния. Как только каждое reducer
завершит свою работу и получит новое подсостояние, мы можем скомпоновать все подсостояния и построить все новое дерево состояний. combineReducers
- это просто служебная функция, позволяющая сэкономить время на шаблонный код. Его роль заключается в замене следующего фрагмента:
export default function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } }
с участием:
const todoApp = combineReducers({ visibilityFilter, todos })
Вход combineReducers
- это простой объект, ключи которого являются ключами состояния, а значения - именем reducers
. На выходе получается полный метод reducer
. Сначала должно быть легко настроить скелетный код:
function combineReducers(obj) { return (state = {}, action) => {} }
Затем нам просто нужно пройтись по каждой клавише obj
и назначить каждой клавише соответствующий reducer
.
function combineReducers(obj) { return (state = {}, action) => { const newState = {}; for (let key in obj) { newState[key] = obj[key](state[key], action); } return newState; } }
Наконец-то пришло самое интересное! Вы можете подумать, что Redux действительно прост для понимания и реализации. Но этот раздел может изменить ваше мнение. Если вы действительно понимаете, как работает промежуточное ПО, и можете реализовать applyMiddleware
в качестве награды, вы получите гораздо более глубокое понимание экосистемы Redux и того, почему ее так легко расширить.
Промежуточное ПО обеспечивает стороннюю точку расширения между отправкой действия и моментом его достижения редуктором. Люди используют промежуточное ПО Redux для ведения журналов, отчетов о сбоях, взаимодействия с асинхронным API, маршрутизации и многого другого.
Начнем с очень простого промежуточного программного обеспечения для ведения журнала. Он может регистрировать каждое отправленное действие вместе с вычисленным после него состоянием. Как этого добиться?
Простая идея - изменить метод dispatch
, как показано ниже:
store.dispatch = (action) => { console.log('action', action); state = reducer(state, action); console.log('state', state); listeners.forEach(listener => listener()); };
Это работает, но добавление нерелевантного кода регистрации в dispatch
- плохая идея. Нам нужен способ извлечь его из основного dispatch
метода. Вторая идея может выглядеть лучше, которая определяет обернутый метод dispatch
:
function loggingDispatch(store, action) { console.log('action', action); store.dispatch(action); console.log('state', store.getState()); }
Обернутый метод украшает dispatch
, добавляя дополнительную логику ведения журнала. Но неудобно определять новый конкретный dispatch
каждый раз, когда нам нужно новое промежуточное ПО. Давайте добавим еще одно промежуточное ПО filterDeleteDispatch
в черный список DELETE
:
function filterDeleteDispatch(store, action) { if (action.type === 'DELETE') return; store.dispatch(action); }
Мы можем вручную объединить эти два промежуточного программного обеспечения в одно, определив новое промежуточное программное обеспечение loggingAndFilterDeleteDispatch
. Он довольно негибкий, потому что пользователю могут понадобиться разные комбинации разных промежуточных программ. Чтобы связать промежуточное ПО вместе, входные и выходные данные каждого промежуточного ПО должны быть одного типа.
store.dispatch --> Middleware 1 --> dispatch v1 --> Middleware 2 --> dispatch v2
Вся система похожа на лук. store.dispatch
- сердцевина лука. Затем мы продолжаем заполнять различными промежуточными программами. Итак, в Redux ввод и вывод каждого промежуточного программного обеспечения являются dispatch
-подобными функциями:
function middleware(dispatch) { return function newDispatch(action) { dispatch(action); } }
Давайте напишем наше промежуточное ПО для ведения журналов на основе приведенного выше шаблона:
function logger(dispatch) { return function newDispatch(action) { console.log('action', action); dispatch(action); console.log('state', store.getState()); } }
Ждать?! Мы что-то упускаем в приведенном выше коде? Где мы можем получить доступ к контексту store
? Приведенный выше код - это ожидаемое промежуточное ПО для ведения журналов, которое мы хотим иметь. Уловка заключается в следующем: если мы знаем, как выглядит метод, но не имеем контекста, мы можем создать фабричный метод для генерации этого метода. В нашем случае мы можем создать createLoggerMiddleware
метод для генерации этого промежуточного программного обеспечения регистратора.
function createLoggerMiddleware(store) { return function logger(dispatch) { return function newDispatch(action) { console.log('action', action); dispatch(action); console.log('state', store.getState()); } } }
Выглядит так просто, правда? Мы только что создали оболочку поверх функции logger
с переданной store
. Давайте сделаем то же самое и для промежуточного программного обеспечения filterDelete:
function createFilterDeleteMiddleware(store) { return function filterDelete(dispatch) { return function newDispatch(action) { if (action === 'DELETE') return; dispatch(action); } } }
Предположим, мы хотим сначала обернуть store.dispatch
промежуточным программным обеспечением регистратора, а затем промежуточным программным обеспечением filterDelete, мы можем сделать следующее:
const logger = createLoggerMiddleware(store); const filterDelete = createFilterDeleteMiddleware(store); const newDispatch = filterDelete(logger(store.dispatch));
Очевидно newDispatch
содержит логику обоих промежуточных программ, а также исходный store.dispatch
. Есть ли способ сделать приведенный выше код более общим? Мы можем определить функцию decorateDispatch
, которая принимает store
и список фабрик промежуточного программного обеспечения, а затем вывести оформленный dispatch
.
function decorateDispatch(store, middlewareFactories) { let dispatch = store.dispatch; middlewareFactories.forEach(factory => { dispatch = factory(store)(dispatch); }); return dispatch; }
Пользователь Redux никогда не должен беспокоиться об украшении dispatch
. Это необходимо сделать на store
этапе инициализации createStore
на основе списка промежуточного программного обеспечения. Таким образом, мы можем изменить createStore
, чтобы он мог принимать в качестве входных данных фабрики промежуточного программного обеспечения.
function createStore(reducer, initialState, middlewareFactories=[]) { const store = {}; const state = initialState; const listeners = []; store.getState = () => state; store.dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; store.subscribe = (listener) => listeners.push(listener); return store; }; store.dispatch = decorateDispatch(store, middlewareFactories); return store; }
Дополнительно
createStore API
Автор Redux Дэн Абрамов определяет немного другой API для createStore
. Основная причина - гарантировать, что магазин можно украсить только один раз.
function createStore(reducer, initialState, enhancer) { if (typeof enhancer === 'function') { return enhancer(createStore)(reducer, initialState); } ... }
enhancer
принимает createStore
и выводит украшенный createStore
. Идея очень похожа на то, как разработано промежуточное программное обеспечение. Чтобы реализовать это, мы можем сначала построить скелетный код:
function enhancer(createStore) { return function newCreateStore(reducer, initialState) { createStore(reducer, initialState); } }
Опять же, enhancer
ничего не знает о промежуточном программном обеспечении. Помните трюк, о котором мы упоминали ранее? Мы можем повторно использовать его здесь и создать фабрику усилителя, которая внедряет промежуточное ПО в качестве контекста:
function enhancerFactory(...middlewares) { return function enhancer(createStore) { return function newCreateStore(reducer, initialState) { createStore(reducer, initialState); } } }
Не удивляйся. enhanceFactory
- это то же самое, что и Redux API applyMiddleware
! Завершим оставшуюся часть кода на applyMiddleware
:
function applyMiddleware
(...middlewareFactories) {
return function enhancer(createStore) {
return function newCreateStore(reducer, initialState) {
const store = createStore(reducer, initialState);
let dispatch = store.dispatch;
middlewareFactories.forEach(factory => {
dispatch = factory(store)(dispatch);
});
store.dispatch = dispatch;
return store;
}
}
}
Здесь мы позаимствовали логику из decorateDispatch
, чтобы украсить dispatch
. Реализация в Redux немного отличается, но основная идея та же.
Карри функции
Функции Карри обычно трудны для понимания. Но у нас уже есть функции карри в нашем коде выше, вы заметили?
function createLoggerMiddleware(store) { return function logger(dispatch) { return function newDispatch(action) { console.log('action', action); dispatch(action); console.log('state', store.getState()); } } }
такой же как
const createLoggerMiddleware = store => dispatch => action => { console.log('action', action); dispatch(action); console.log('state', store.getState()); }
applyMiddleware
также можно написать в этом стиле:
const applyMiddleware =
(...middlewareFactories) => createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
middlewareFactories.forEach(factory => {
dispatch = factory(store)(dispatch);
});
store.dispatch = dispatch;
return store;
}
Промежуточное ПО Redux-thunk
Зная, как работает промежуточное программное обеспечение, довольно легко создать промежуточное программное обеспечение redux-thunk, которое может обрабатывать асинхронный запрос. Из примера промежуточного программного обеспечения логгера мы можем легко написать что-то вроде этого:
const createThunkMiddleware = store => dispatch => action => { if (typeof action === 'function') { return action(dispatch, store.getState()); } return dispatch(action); }
Заворачивать
Это руководство представляет собой пошаговое руководство по созданию основных API-интерфейсов Redux с нуля. Некоторые детали реализации не совпадают с официальной библиотекой Redux, но основная идея та же. Redux также включает много средств обработки ошибок, которые, я думаю, не важны в этом руководстве. Раздел промежуточного программного обеспечения может не иметь смысла при первом чтении. Ничего страшного, со временем в этом будет больше смысла.
Если вы нашли эту статью полезной, нажмите 👏 .