Хороший способ узнать, как все работает, - создать его с нуля. Большую часть времени мы используем библиотеку Javascript как черный ящик. Мы заботимся только о том, как использовать общедоступные API, которые они предоставляют, не заботясь о том, как это работает. Это руководство будет посвящено раскрытию магии Redux и покажет, как написать его с нуля. Вы можете найти его полезным в следующих аспектах:

  1. Лучше понять, как Redux работает под капотом.
  2. Постарайтесь думать с точки зрения автора. Может быть, в следующий раз у вас появится собственная библиотека!
  3. Изучите новые шаблоны для написания кода, которых вы никогда раньше не писали.

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

Основные API

Прежде чем приступить к написанию Redux, нам нужно выяснить, с чего начать кодирование! Давайте сначала подведем итог тому, что нам нужно знать для создания Redux:

  1. Основная концепция Redux - это контейнер с предсказуемым состоянием для приложений Javascript.
  2. API магазина: магазин содержит все дерево состояний.
  • createStore(reducer, state, enhancer): создать магазин Redux
  • getState(): получить состояние магазина
  • 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 также включает много средств обработки ошибок, которые, я думаю, не важны в этом руководстве. Раздел промежуточного программного обеспечения может не иметь смысла при первом чтении. Ничего страшного, со временем в этом будет больше смысла.

Если вы нашли эту статью полезной, нажмите 👏 .