Напишите Redux вручную и глубоко поймите его принцип

внешний интерфейс React.js

Redux — известная библиотека, она используется во многих местах, я ей пользуюсь несколько лет, сегодня в этой статье я буду реализовывать Redux самостоятельно, чтобы глубже понять ее принципы. Мы по-прежнему придерживаемся старого подхода, начнем с базового использования, а затем реализуем Redux для замены исходного пакета NPM, но функция остается прежней. В этой статье будет реализована только основная библиотека Redux и она будет использоваться в сочетании с другими библиотеками, такими как React-Redux, и позже я напишу отдельную статью. Иногда мы слишком много внимания уделяем использованию, лишь вспоминая различные способы использования, но игнорируя их основные принципы, но если мы хотим действительно улучшить технологию, лучше всего разбираться в ней по порядку, например, Redux и React-Redux. кажутся очень похожими, но их основные концепции и проблемы разные. Redux на самом деле просто простая библиотека управления состоянием без каких-либо вещей, связанных с интерфейсом. React-Redux фокусируется на том, как объединить Redux с React, используя некоторые API React.

Весь код в этой статье был загружен на GitHub, вы можете снять его и поиграть:GitHub.com/Денис — см....

основная концепция

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

Store

Как следует из названия, Store — это склад. Он хранит все состояния и предоставляет некоторые API для работы с ним. Наши последующие операции фактически управляют этим складом. Если наш склад используется для хранения молока, изначально на нашем складе нет ни одного пакета молока, то состояние Магазина такое:

{
	milk: 0
}

Actions

Действие - это действие. Цель этого действия - изменить определенное состояние в Магазине. Магазин - это еще склад выше. Сейчас я хочу поставить ящик молока на склад. Потом "Я хочу поставить ящик молока на складе" является Action. , код такой:

{
  type: "PUT_MILK",
  count: 1
}

Reducers

Я только подумал про "хочу поставить ящик молока на склад", но еще не сделал. Конкретная операция зависит от Редюсера. Редуктор меняет состояние в Магазине в соответствии с полученным Действием. например, я получилPUT_MILK, количество одновременныхcountравен 1, то результатом его ввода являетсяmilkУвеличивается на 1, от 0 до 1, код выглядит так:

const initState = {
  milk: 0
}

function reducer(state = initState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk + action.count}
    default:
      return state
  }
}

Видно, что Redux сам по себе является простым конечным автоматом, Store хранит все состояния, а Action — уведомление об изменении состояния, Reducer меняет соответствующее состояние в Store при получении уведомления.

Простой пример

Давайте рассмотрим простой пример, который включает в себя вышеупомянутые концепции Store, Action и Reducer:

import { createStore } from 'redux';

const initState = {
  milk: 0
};

function reducer(state = initState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk + action.count};
    case 'TAKE_MILK':
      return {...state, milk: state.milk - action.count};
    default:
      return state;
  }
}

let store = createStore(reducer);

// subscribe其实就是订阅store的变化,一旦store发生了变化,传入的回调函数就会被调用
// 如果是结合页面更新,更新的操作就是在这里执行
store.subscribe(() => console.log(store.getState()));

// 将action发出去要用dispatch
store.dispatch({ type: 'PUT_MILK' });    // milk: 1
store.dispatch({ type: 'PUT_MILK' });    // milk: 2
store.dispatch({ type: 'TAKE_MILK' });   // milk: 1

реализовать это самостоятельно

Хотя наш предыдущий пример короткий, он уже содержит основные функции Redux, поэтому первая цель нашего почерка — заменить Redux в этом примере. Чтобы заменить этот Redux, мы должны сначала узнать, что в нем есть.Присмотревшись, кажется, что мы используем только один из его API:

createStore: этот API принимаетreducerметод в качестве параметра, возвращающийstore, основные функции в этомstoreначальство.

посмотриstoreЧто мы использовали выше:

store.subscribe: подписыватьсяstateменяется, когдаstateВыполнять обратный вызов при его изменении, их может быть несколькоsubscribe, обратные вызовы внутри будут выполняться последовательно.

store.dispatch: проблемаactionметод каждый разdispatch actionбудет выполнятьreducerгенерировать новыеstate, затем выполнитеsubscribeЗарегистрированный обратный вызов.

store.getState: простой метод, возвращающий текущийstate.

Видетьsubscribeзарегистрировать обратный звонок,dispatchИнициируйте обратный вызов, что приходит на ум, разве это не модель публикации-подписки?У меня есть предыдущая статья про модель публикации-подписки подробно, здесь прямая имитация.

function createStore() {
  let state;              // state记录所有状态
  let listeners = [];     // 保存所有注册的回调

  function subscribe(callback) {
    listeners.push(callback);       // subscribe就是将回调保存下来
  }

  // dispatch就是将所有的回调拿出来依次执行就行
  function dispatch() {
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  // getState直接返回state
  function getState() {
    return state;
  }

  // store包装一下前面的方法直接返回
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}

Разве приведенный выше код не очень прост, ядро ​​​​Redux также является моделью публикации-подписки, это так просто! Подождите, кажется, чего-то не хватает,reducerШерстяная ткань?reducerЭффект должен измениться, когда событие будет опубликованоstate, так что нашdispatchдолжен быть выполнен перед выполнением обратного вызоваreducer,использоватьreducerВозвращаемое значениеstateназначать,dispatchПереписано следующим образом:

function dispatch(action) {
  state = reducer(state, action);

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

На данный момент мы реализовали все API, использованные в предыдущих примерах, попробуем заменить официальный Redux нашим собственным Redux:

// import { createStore } from 'redux';
import { createStore } from './myRedux';

Вы можете видеть, что вывод такой же, что указывает на то, что с Redux, который мы написали, проблем нет:

image-20200630152344176

После понимания основных принципов Redux у нас не должно возникнуть проблем с повторным просмотром его исходного кода.Исходный портал createStore.

Наконец, давайте разберемся с основным процессом Redux.Обратите внимание, что чистый Redux — это просто конечный автомат.ViewО слой.

image-20200630154356840

В дополнение к этой основной логике в Redux есть несколько очень интересных API, так что давайте запишем их.

почеркcombineReducers

combineReducersЭто также очень широко используемый API.Когда наше приложение становится все более и более сложным, если вся логика написана в одномreducerВнутри, в конце концов, этот файл может иметь тысячи строк, поэтому Redux предоставляетcombineReducers, что позволяет нам писать свои для разных модулейreducer, и, наконец, объединить их. Например, молочный склад, с которого мы начинали, так как наш бизнес развивается очень хорошо, мы добавили еще один склад для амплификации риса, для этих двух складов мы можем создать свои собственные склады.reducer, а затем объедините их следующим образом:

import { createStore, combineReducers } from 'redux';

const initMilkState = {
  milk: 0
};
function milkReducer(state = initMilkState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk + action.count};
    case 'TAKE_MILK':
      return {...state, milk: state.milk - action.count};
    default:
      return state;
  }
}

const initRiceState = {
  rice: 0
};
function riceReducer(state = initRiceState, action) {
  switch (action.type) {
    case 'PUT_RICE':
      return {...state, rice: state.rice + action.count};
    case 'TAKE_RICE':
      return {...state, rice: state.rice - action.count};
    default:
      return state;
  }
}

// 使用combineReducers组合两个reducer
const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer});

let store = createStore(reducer);

store.subscribe(() => console.log(store.getState()));

// 操作🥛的action
store.dispatch({ type: 'PUT_MILK', count: 1 });    // milk: 1
store.dispatch({ type: 'PUT_MILK', count: 1 });    // milk: 2
store.dispatch({ type: 'TAKE_MILK', count: 1 });   // milk: 1

// 操作大米的action
store.dispatch({ type: 'PUT_RICE', count: 1 });    // rice: 1
store.dispatch({ type: 'PUT_RICE', count: 1 });    // rice: 2
store.dispatch({ type: 'TAKE_RICE', count: 1 });   // rice: 1

В приведенном выше коде мы будемstateразделен на два небольшихmilkStateа такжеriceState, окончательный результат выглядит следующим образом:

image-20200630162957760

Зная использование, давайте попробуем написать это сами! писать от рукиcombineReducers, давайте сначала проанализируем, что он сделал, во-первых, его возвращаемое значение - этоreducer,этоreducerтакже будет действовать какcreateStoreПередаются параметры, указывающие, что это возвращаемое значение является общим.reducerфункция с той же структурой. Эта функция также получаетstateа такжеactionзатем вернуть новыйstate, только этот новыйstateсоблюдатьcombineReducersСтруктура данных параметра. Попробуем написать:

function combineReducers(reducerMap) {
  const reducerKeys = Object.keys(reducerMap);    // 先把参数里面所有的键值拿出来
  
  // 返回值是一个普通结构的reducer函数
  const reducer = (state = {}, action) => {
    const newState = {};
    
    for(let i = 0; i < reducerKeys.length; i++) {
      // reducerMap里面每个键的值都是一个reducer,我们把它拿出来运行下就可以得到对应键新的state值
      // 然后将所有reducer返回的state按照参数里面的key组装好
      // 最后再返回组装好的newState就行
      const key = reducerKeys[i];
      const currentReducer = reducerMap[key];
      const prevState = state[key];
      newState[key] = currentReducer(prevState, action);
    }
    
    return newState;
  };
  
  return reducer;
}

Принцип реализации официального исходного кода такой же, как и у нас, но в нем больше обработки ошибок, можете проверить.

почеркapplyMiddleware

middlewareЭто очень важная концепция в Redux.Экология Redux в основном зависит от этого доступа к API.Например, мы хотим написатьloggerПромежуточное ПО можно написать так (это промежуточное ПО взято из официальной документации):

// logger是一个中间件,注意返回值嵌了好几层函数
// 我们后面来看看为什么这么设计
function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

// 在createStore的时候将applyMiddleware作为第二个参数传进去
const store = createStore(
  reducer,
  applyMiddleware(logger)
)

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

type StoreEnhancer = (next: StoreCreator) => StoreCreator

Вышеупомянутая структура означает, чтоenhancerВ качестве функции он получаетStoreCreatorфункция в качестве параметра, и возвращаемое значение также должно бытьStoreCreatorфункция. Обратите внимание, что его возвращаемое значение также являетсяStoreCreatorФункция, то есть мы вынимаем ее возвращаемое значение и продолжаем выполнять, должна получиться такая же, как и предыдущая.createStoreТа же структура возврата, то есть наша предыдущаяcreateStoreКакая структура возвращается, он также должен вернуть структуру, которая является этойstore:

{
  subscribe,
  dispatch,
  getState
}

createStoreслужба поддержкиenhancer

По его словамenhancerопределение, давайте перепишем своеcreateStore, пусть поддерживаетenhancer:

function createStore(reducer, enhancer) {   // 接收第二个参数enhancer
  // 先处理enhancer
  // 如果enhancer存在并且是函数
  // 我们将createStore作为参数传给他
  // 他应该返回一个新的createStore给我
  // 我再拿这个新的createStore执行,应该得到一个store
  // 直接返回这个store就行
  if(enhancer && typeof enhancer === 'function'){
    const newCreateStore = enhancer(createStore);
    const newStore = newCreateStore(reducer);
    return newStore;
  }
  
  // 如果没有enhancer或者enhancer不是函数,直接执行之前的逻辑
  // 下面这些代码都是之前那版
  // 省略n行代码
	// .......
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}

Исходный код, соответствующий этой части, находится здесь.

applyMiddlewareВозвращаемое значение представляет собойenhancer

Мы уже имеемenhancerосновная структураapplyMiddlewareпередается вторым параметром вcreateStoreДа, то есть онenhancer, точнееapplyMiddlewareВозвращаемое значение представляет собойenhancer, потому что мы переходим кcreateStoreэто результат его казниapplyMiddleware():

function applyMiddleware(middleware) {
  // applyMiddleware的返回值应该是一个enhancer
  // 按照我们前面说的enhancer的参数是createStore
  function enhancer(createStore) {
    // enhancer应该返回一个新的createStore
    function newCreateStore(reducer) {
      // 我们先写个空的newCreateStore,直接返回createStore的结果
      const store = createStore(reducer);
      return store
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}

выполнитьapplyMiddleware

Выше у нас уже естьapplyMiddlewareБазовая структура промежуточного программного обеспечения завершена, но функция еще не реализована.Чтобы реализовать его функцию, мы должны сначала выяснить, какую функцию имеет промежуточное программное обеспечение, или использовать предыдущуюloggerПромежуточное ПО в качестве примера:

function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

Это промежуточное ПО работает следующим образом:

image-20200701160700945

может видеть насlet result = next(action);После выполнения этой строкиstateИзменилось, мы сказали раньше, что нам нужно изменитьсяstateТолькоdispatch(action), так вотnext(action)то естьdispatch(action), просто сменили имя. И обратите внимание на возвращаемое значение последнего слояreturn function(action), параметры которогоaction, разве не похожеdispatch(action)на самом деле он новыйdispatch(action), этот новыйdispatch(action)буду называть оригиналdispatchи добавить собственную логику до и после вызова. Итак, здесь структура промежуточного программного обеспечения также ясна:

  1. Промежуточное ПО получаетstoreВ качестве параметра он вернет функцию
  2. Возвращаемая функция получает старыйdispatchфункция в качестве параметра, вернет новую функцию
  3. Новая функция, которая возвращает, является новойdispatchФункция, эта функция может получить входящие два слоя извне.storeи старыйdispatchфункция

Грубо говоря, промежуточное ПО должно укреплятьdispatchфункция, с новымdispatchзаменить старыйdispatch,这不就是个装饰者模式吗? На самом деле, преждеenhancerЭто также шаблон декоратора, переходящий вcreateStore,существуетcreateStoreДобавьте немного кода до и после выполнения и, наконец, верните расширенную версию.createStore.Видно, что паттерны проектирования действительно широко распространены в этих превосходных фреймворках.Если вы не знакомы с паттерном декоратора, то можете прочитать мою предыдущую статью.

Следуя этому ходу мысли, нашаapplyMiddlewareВы можете написать это:

// 直接把前面的结构拿过来
function applyMiddleware(middleware) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      const store = createStore(reducer);
      
      // 将middleware拿过来执行下,传入store
      // 得到第一层函数
      const func = middleware(store);
      
      // 解构出原始的dispatch
      const { dispatch } = store;
      
      // 将原始的dispatch函数传给func执行
      // 得到增强版的dispatch
      const newDispatch = func(dispatch);
      
      // 返回的时候用增强版的newDispatch替换原始的dispatch
      return {...store, dispatch: newDispatch}
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}

Как обычно с нашимиapplyMiddlewareЗамена старого имеет тот же эффект при запуске, а значит, то, что мы написали, не проблема, ха-ха~

image-20200701162841414

поддерживает несколькоmiddleware

нашapplyMiddlewareЕсть еще одна функция, которая заключается в поддержке несколькихmiddleware, например так:

applyMiddleware(
  rafScheduler,
  timeoutScheduler,
  thunk,
  vanillaPromise,
  readyStatePromise,
  logger,
  crashReporter
)

На самом деле это легко поддержать, мы возвращаемсяnewDispatchкоторый будет передан в свою очередьmiddlewareПросто выньте его и выполните.Последовательное выполнение нескольких функций может использовать вспомогательные функцииcompose, эта функция определяется следующим образом. Просто знайте, что у нас здесьcomposeВы не можете просто использовать метод для его выполнения, вы должны вернуть метод, который обертывает все методы.

function compose(...func){
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

этоcomposeЭто может сбивать с толку, я объясню это здесь, например, у нас есть три функции, и все эти три функции получены перед нами.dispatchНовое возвращениеdispatchМетоды:

const fun1 = dispatch => newDispatch1;
const fun2 = dispatch => newDispatch2;
const fun3 = dispatch => newDispatch3;

когда мы использовалиcompose(fun1, fun2, fun3)Какая последовательность выполнения после?

// 第一次其实执行的是
(func1, func2) => (...args) => func1(fun2(...args))
// 这次执行完的返回值是下面这个,用个变量存起来吧
const temp = (...args) => func1(fun2(...args))

// 我们下次再循环的时候其实执行的是
(temp, func3) => (...args) => temp(func3(...args));
// 这个返回值是下面这个,也就是最终的返回值,其实就是从func3开始从右往左执行完了所有函数
// 前面的返回值会作为后面参数
(...args) => temp(func3(...args));

// 再看看上面这个方法,如果把dispatch作为参数传进去会是什么效果
(dispatch) => temp(func3(dispatch));

// 然后func3(dispatch)返回的是newDispatch3,这个又传给了temp(newDispatch3),也就是下面这个会执行
(newDispatch3) => func1(fun2(newDispatch3))

// 上面这个里面用newDispatch3执行fun2(newDispatch3)会得到newDispatch2
// 然后func1(newDispatch2)会得到newDispatch1
// 注意这时候的newDispatch1其实已经包含了newDispatch3和newDispatch2的逻辑了,将它拿出来执行这三个方法就都执行了

Более подробно о принципе компоновки можно прочитать в моей предыдущей статье.

Поэтому мы поддерживаем несколькоmiddlewareКод такой:

// 参数支持多个中间件
function applyMiddleware(...middlewares) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      const store = createStore(reducer);
      
      // 多个middleware,先解构出dispatch => newDispatch的结构
      const chain = middlewares.map(middleware => middleware(store));
      const { dispatch } = store;
      
      // 用compose得到一个组合了所有newDispatch的函数
      const newDispatchGen = compose(...chain);
      // 执行这个函数得到newDispatch
      const newDispatch = newDispatchGen(dispatch);

      return {...store, dispatch: newDispatch}
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}

Наконец, мы добавляем еще одинlogger2Эффект реализации промежуточного ПО:

function logger2(store) {
  return function(next) {
    return function(action) {
      let result = next(action);
      console.log('logger2');
      return result
    }
  }
}

let store = createStore(reducer, applyMiddleware(logger, logger2));

можно увидетьlogger2Он также был распечатан, и все готово.

image-20200701173615349

Теперь мы также можем понять, почему его промежуточное ПО включает в себя несколько слоев функций:

Уровень 1: Цель состоит в том, чтобы передатьstoreпараметр

Второй слой: Структура второго слояdispatch => newDispatch, этот уровень функций нескольких промежуточных программ можетcomposeподняться и образовать большуюdispatch => newDispatch

Третий уровень: этот уровень является окончательным возвращаемым значением, которое на самом делеnewDispatch, улучшенdispatch, это реальная логика промежуточного программного обеспечения.

сюда нашapplyMiddlewareтолько что закончил писать,Соответствующий исходный код можно увидеть здесь, я считаю, что после прочтения этой статьи и последующего просмотра исходного кода проблем не будет!

Весь код в этой статье был загружен на GitHub, вы можете пойти и поиграть с ним:GitHub.com/Денис — см....

Суммировать

  1. Pure Redux — это просто конечный автомат,storeВ нем хранятся все состоянияstate, чтобы изменить состояние внутриstate, Толькоdispatch action.
  2. для выпущенныхactionнужно использоватьreducerиметь дело с,reducerбудет вычислять новыеstateзаменить старыйstate.
  3. subscribeметод может регистрировать методы обратного вызова, когдаdispatch actionвыполнит обратный вызов внутри.
  4. Redux на самом деле является моделью публикации-подписки!
  5. Redux также поддерживаетenhancer,enhancerПо сути, это режим декоратора, переходящий в текущийcreateStore, который возвращает расширенныйcreateStore.
  6. Использование ReduxapplyMiddlewareподдержка промежуточного программного обеспечения,applyMiddlewareВозвращаемое значение на самом делеenhancer.
  7. Промежуточное ПО Redux также является шаблоном декоратора, переходящим в текущийdispatch, который возвращает расширенныйdispatch.
  8. Pure Redux не имеет слоя представления, поэтому его можно использовать в сочетании с различными библиотеками пользовательского интерфейса, такими какreact-redux, план следующей статьи рукописныйreact-redux.

использованная литература

Официальная документация:redux.js.org/

Исходный код GitHub:GitHub.com/Горячее цинкование/Горячее…

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

Добро пожаловать, чтобы обратить внимание на мой общедоступный номербольшой фронт атакиПолучите высококачественные оригиналы впервые~

Цикл статей "Передовые передовые знания":nuggets.capable/post/684490…

Адрес GitHub с исходным кодом из серии статей «Advanced Front-end Knowledge»:GitHub.com/Денис — см....