Принцип редукции и реализация

внешний интерфейс функциональное программирование React.js

Реализация Redux.

В этой статье реализован простойreduxа такжеreact-redux, основное содержание

  1. reduxИдеи дизайна и принципы реализации
  2. redux 中间件Идеи дизайна и принципы реализации
  3. react-reduxИдеи дизайна и принципы реализации

reduxЭто менеджер состояний, который хранит данные, например, когда мы создаемstore.js, в котором мы храним эти данные, и нам достаточно обратиться в любом месте к этому файлу, чтобы получить соответствующее значение статуса:

let state = {
  count: 1
}

Читаем и модифицируем следующее состояние:

console.log(state.count)
state.count = 2

Теперь реализуем модификацию и использование состояния (счетчика)! Конечно, есть очевидная проблема с вышеизложенным:

  1. Этот менеджер состояния может управлять толькоcount, непригодный.
  2. ИсправлятьcountПосле этого используйтеcountгде вы не можете получать уведомления.

реализовать подписку

Мы можем использовать шаблон публикации-подписки для решения этой проблемы. Давайте обернем это в функциюredux

function createStore(initState) {
  let state = initState
  let listeners = []

  /* 订阅函数 */
  function subscribe(listener) {
    listeners.push(listener)
  }

  function changeState(newState) {
    state = newState
    /* 执行通知 */
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
  }

  function getState() {
    return state
  }

  return { subscribe, changeState, getState }
}

На данный момент мы завершили простой менеджер состояний.

  1. stateданные могут быть свободно определены
  2. Мы модифицируем состояние и прослушиваем изменения в месте подписки, что может обеспечить мониторинг.
let initState = {
  count: 1,
  info: {
    age: 18
  }
}

let store = createStore(initState)

store.subscribe(() => {
  let state = store.getState()
  console.log('subscribe function one: ', state)
})
store.subscribe(() => {
  let state = store.getState()
  console.log('subscribe function two: ', state)
})

store.changeState({ ...store.getState(), count: store.getState().count + 1 })
store.changeState({
  ...store.getState(),
  info: { age: store.getState().info.age - 1 }
})

// ==== result
// subscribe function one:  { count: 2, info: { age: 18 } }
// subscribe function two:  { count: 2, info: { age: 18 } }
// subscribe function one:  { count: 2, info: { age: 17 } }
// subscribe function two:  { count: 2, info: { age: 17 } }

Здесь нужно понимать, чтоcreateStore,при условииchangeState,getState,subscribeтри способности.

В приведенной выше функции мы вызываемstore.changeStateможно изменитьstate的值,这样就存在很大的弊端了。 Напримерstore.changeState({})

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

Поэтому нам нужно ограничение для измененияstateзначение, не допуская возникновения непредвиденных ситуаций.stateЗначение очищено или используется неправильно. Мы можем решить эту проблему в два этапа:

  1. dispatch: делатьstateпересмотреть план, сказатьstore, каков мой план модификации.
  2. reducer: Исправлятьstore.changeStateметод, скажите ему изменитьstate, по нашему плану модифицировать.

То есть мы будемstore.changeStateПереписатьstore.dispatch, передайте еще один в функциюreducerфункция для ограничения модификации значений состояния.

Реализовать редукторы

Редьюсер — это чистая функция, которая принимает состояние и возвращает новое состояние.

function createStore(reducer, initState) {
  let state = initState
  let listeners = []

  /* 订阅函数 */
  function subscribe(listener) {
    listeners.push(listener)
  }

  /* state 值的修改 */
  function dispatch(action) {
    state = reducer(state, action)
    /* 执行通知 */
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
  }

  function getState() {
    return state
  }

  return { subscribe, dispatch, getState }
}

попробуем использоватьdispatchа такжеreducerдля достижения автоинкремента и автодекремента

let initState = {
  count: 1,
  info: {
    age: 18
  }
}

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'DECREMENT':
      return { ...state, count: state.count - 1 }
    default:
      return state
  }
}

let store = createStore(reducer, initState)

store.subscribe(() => {
  let state = store.getState()
  console.log('subscribe function: ', state)
})

store.dispatch({ type: 'INCREMENT' }) // 自增
store.dispatch({ type: 'DECREMENT' }) // 自减
store.dispatch({ count: 2 }) // 计划外:不生效

мы знаемreducerявляется функцией ограничения, которая принимает староеstate, верните новый, как и планировалосьstate. В нашем проекте большое количествоstate, каждыйstateТребуются функции ограничений.Как бы они выглядели, если бы все они были написаны вместе?

Все планы написаны в одномreducerфункция, приведет кreducerфункции огромны и сложны. Следующее будет инкапсулироватьcombineReducersгранулироватьreducerфункция.

Реализовать combReducers

Гранулированный редуктор

Исходя из опыта, мы обязательно разделим многие компоненты в соответствии с размерностью компонентов.reducerфункцию, а затем объединить их с функцией.

Давайте управлять двумяstate,Одинcounter,Одинinfo.

let state = {
  counter: { count: 0 },
  info: { age: 18 }
}

их соответствующиеreducer

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 }
    default:
      return state
  }
}

function infoReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT-AGE':
      return { age: state.age + 1 }
    default:
      return state
  }
}

Давайте попробуем достичьcombineReducersфункция

  1. Передайте параметр объекта,keyзначениеstateдерево состоянийkeyстоимость,valueдля соответствующегоreducerфункция.
  2. Перебирает параметры объекта, выполняя каждыйreducerфункция, пройти вstate[key], функция получает каждыйreducerНовейшиеstateстоимость.
  3. связьstateзначение и возврат. вернуть новый объединенныйreducerфункция.
function combineReducers(reducers) {
  /* reducerKeys = ['counter', 'info']*/
  const reducerKeys = Object.keys(reducers)

  /*返回合并后的新的reducer函数*/
  return function combination(state = {}, action) {
    /*生成的新的state*/
    const nextState = {}

    /*遍历执行所有的reducers,整合成为一个新的state*/
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i]
      const reducer = reducers[key]
      /*之前的 key 的 state*/
      const previousStateForKey = state[key]
      /*执行 分 reducer,获得新的state*/
      const nextStateForKey = reducer(previousStateForKey, action)

      nextState[key] = nextStateForKey
    }
    return nextState
  }
}

использоватьcombineReducers:

const reducers = combineReducers({
  counter: counterReducer,
  info: infoReducer
})

let store = createStore(reducers, initState)

store.subscribe(() => {
  let state = store.getState()
  console.log('subscribe function: ', state)
})

store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT-AGE' })

Однако этого недостаточно, положимreducerРазделить по размеру компонента, черезcombineReducersобъединены. Но есть еще проблема,stateМы по-прежнему писали это вместе, что могло вызватьstateДеревья огромные, неинтуитивные и сложные в уходе. нам нужно разделить, т.state,ОдинreducerНапишите произведение.

гранулированное состояние

переписатьcombineReducersфункция, не более чем простая, вcreateStoreвыполнять в функцииdispatch({ type: Symbol() })

function createStore(reducer, initState) {
  let state = initState
  let listeners = []

  /* 订阅函数 */
  function subscribe(listener) {
    listeners.push(listener)
  }

  function dispatch(action) {
    state = reducer(state, action)
    /* 执行通知 */
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
  }

  /* 注意!!!只修改了这里,用一个不匹配任何计划的 type,来获取初始值 */
  dispatch({ type: Symbol() })

  function getState() {
    return state
  }

  return { subscribe, dispatch, getState }
}

Будуstateв их соответствующиеreducer:

function counterReducer(state = { count: 1 }, action) {
  //...
}

function infoReducer(state = { age: 18 }, action) {
  //...
}

// 合并 reducer
const reducers = combineReducers({
  counter: counterReducer,
  info: infoReducer
})

// 移除 initState
let store = createStore(reducers)

console.log(store.getState()) // { counter: { count: 1 }, info: { age: 18 } }

Давайте подумаем, какой эффект может принести эта линия?

  1. createStoreпри использовании, которое не соответствует ни одномуtypeизaction, для запускаstate = reducer(state, action)
  2. потому чтоaction.typeне совпадает, каждый субreducerвойдетdefaultэлемент, вернуть самоинициализированныйstate, так что инициализированныйstateдерево вверх.

Внедрение промежуточного программного обеспечения Redux

если вы использовалиExpressа такжеKoaтаких как серверные библиотеки, то вы, вероятно, уже знакомы с концепцией промежуточного программного обеспечения.

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

Пример промежуточного ПО

Сначала реализуемredux-loggerПлагин, то есть функция журнала печати, записывает изменения до и после модификацииstateа такжеaction, мы можем переписатьstore.dispatchреализовать:

const reducers = combineReducers({ counter: counterReducer })

let store = createStore(reducers)
const next = store.dispatch

// 重写 dispatch
store.dispatch = action => {
  console.log('prevState: ', store.getState())
  console.log('action', action)
  next(action)
  console.log('nextState: ', store.getState())
}

store.dispatch({ type: 'INCREMENT' })

выходной результат

prevState:  { counter: { count: 1 } }
action { type: 'INCREMENT' }
nextState:  { counter: { count: 2 } }

Теперь мы реализовали простойredux-loggerпромежуточное ПО. У меня есть другое требование, мне нужно записать причину каждой ошибки данных, давайте расширимdispatch

store.dispatch = action => {
  try {
    next(action)
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

Таким образом, каждый раз, когда в диспетчере возникает исключение, мы будем его записывать.

Сотрудничество с несколькими промежуточными программами

Теперь мне нужно записывать и журналы, и исключения, что мне делать? Конечно очень просто, две функции совмещены!

store.dispatch = action => {
  try {
    console.log('prevState: ', store.getState())
    console.log('action', action)
    next(action)
    console.log('nextState: ', store.getState())
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

А если будет другой спрос? затем изменитьdispatchфункция? Как насчет еще 10 потребностей? К тому времениdispatchФункция должна быть слишком большой и грязной, чтобы ее поддерживать!

Нам необходимо подумать о том, как реализовать масштабируемую модель сотрудничества с несколькими промежуточными программами.

  1. мы кладемloggerMiddlewareизвлечен
const loggerMiddleware = action => {
  console.log('prevState: ', store.getState())
  console.log('action', action)
  next(action)
  console.log('nextState: ', store.getState())
}
  1. мы кладемexceptionMiddlewareизвлечен
const exceptionMiddleware = action => {
  try {
    /*next(action)*/
    loggerMiddleware(action)
  } catch (err) {
    console.error('错误报告: ', err)
  }
}
store.dispatch = exceptionMiddleware
  1. Текущий код имеет очень серьезную проблему, т.exceptionMiddlewareЭто написано мертвымloggerMiddleware, мы должны позволитьnext(action)Станьте динамичным, любое промежуточное ПО может быть
const exceptionMiddleware = next => action => {
  try {
    /*loggerMiddleware(action);*/
    next(action)
  } catch (err) {
    console.error('错误报告: ', err)
  }
}
/*loggerMiddleware 变成参数传进去*/
store.dispatch = exceptionMiddleware(loggerMiddleware)
  1. Одна и та же причина,loggerMiddlewareвнутриnextтеперь равноstore.dispatch, Привести кloggerMiddlewareНикакое другое промежуточное ПО не может быть расширено в нем! мы также ставимnextнаписано как динамический
const loggerMiddleware = next => action => {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

До сих пор мы исследовали высоко масштабируемую модель сотрудничества промежуточного программного обеспечения!

const store = createStore(reducer)
const next = store.dispatch

const loggerMiddleware = next => action => {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = next => action => {
  try {
    next(action)
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next))

В это время мы с радостью построили новыйloggerMiddleware.js,ОдинexceptionMiddleware.jsфайл, я хочу разделить два промежуточного программного обеспечения на отдельные файлы. Будут ли проблемы?

loggerMiddlewareсодержит внешние переменныеstore, что делает невозможным разделение промежуточного ПО. Затем мы кладемstoreТакже передайте его как параметр~

const store = createStore(reducer)
const next = store.dispatch

const loggerMiddleware = store => next => action => {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = store => next => action => {
  try {
    next(action)
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
store.dispatch = exception(logger(next))

До сих пор мы действительно реализовали два независимых промежуточных ПО!

Теперь у меня есть требование вывести текущую метку времени перед печатью журнала. Используйте промежуточное ПО для достижения!

const timeMiddleware = store => next => action => {
  console.log('time', new Date().getTime())
  next(action)
}

const time = timeMiddleware(store)
store.dispatch = exception(time(logger(next)))

Внедрение промежуточного программного обеспечения

В предыдущем разделе мы полностью реализовали правильное промежуточное ПО! Но использование промежуточного программного обеспечения не очень дружелюбно

let store = createStore(reducers)
const next = store.dispatch

const loggerMiddleware = store => next => action => {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = store => next => action => {
  try {
    next(action)
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

const timeMiddleware = store => next => action => {
  console.log('time', new Date().getTime())
  next(action)
}

const time = timeMiddleware(store)
const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
store.dispatch = exception(time(logger(next)))

На самом деле нам нужно знать только три мидлвара, а остальные детали можно инкапсулировать! мы расширяемcreateStoreреализовать!

Давайте сначала посмотрим на ожидаемое использование

/*接收旧的 createStore,返回新的 createStore*/
const newCreateStore = applyMiddleware(
  exceptionMiddleware,
  timeMiddleware,
  loggerMiddleware
)(createStore)

/*返回了一个 dispatch 被重写过的 store*/
const store = newCreateStore(reducer)

выполнитьapplyMiddleware

const applyMiddleware = function(...middlewares) {
  /*返回一个重写createStore的方法*/
  return function rewriteCreateStoreFunc(oldCreateStore) {
    /*返回重写后新的 createStore*/
    return function newCreateStore(reducer, initState) {
      /*1. 生成store*/
      const store = oldCreateStore(reducer, initState)
      /*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
      /* const chain = [exception, time, logger]*/
      const chain = middlewares.map(middleware => middleware(store))
      let dispatch = store.dispatch
      /* 实现 exception(time((logger(dispatch))))*/
      chain.reverse().map(middleware => {
        dispatch = middleware(dispatch)
      })

      /*2. 重写 dispatch*/
      store.dispatch = dispatch
      return store
    }
  }
}

Теперь есть небольшая проблема, у нас есть дваcreateStore.

/*没有中间件的 createStore*/
let store = createStore(reducers, initState)

/*有中间件的 createStore*/
const rewriteCreateStoreFunc = applyMiddleware(
  exceptionMiddleware,
  timeMiddleware,
  loggerMiddleware
)
const newCreateStore = rewriteCreateStoreFunc(createStore)
const store = newCreateStore(reducer, initState)

Чтобы сделать пользователей более унифицированными, мы можем легко заставить их использовать одинаково, мы изменим следующиеcreateStoreметод

function createStore(reducer, initState, rewriteCreateStoreFunc) {
  /*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
  if (rewriteCreateStoreFunc) {
    const newCreateStore = rewriteCreateStoreFunc(createStore)
    return newCreateStore(reducer, initState)
  }
  /*否则按照正常的流程走*/
  //...
}

окончательное использование

const rewriteCreateStoreFunc = applyMiddleware(
  exceptionMiddleware,
  timeMiddleware,
  loggerMiddleware
)
const store = createStore(reducer, initState, rewriteCreateStoreFunc)

compose

нашapplyMiddlewareВход[A, B, C]Перевести вA(B(C(next))), достигается так

const chain = [A, B, C]
let dispatch = store.dispatch
chain.reverse().map(middleware => {
  dispatch = middleware(dispatch)
})

reduxпредоставленный А.composeспособ помочь нам сделать это

const chain = [A, B, C]
dispatch = compose(...chain)(store.dispatch)

Посмотрите, как он это делает

export default function compose(...funcs) {
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

КонечноcomposeФункции могут быть трудны для понимания новичками, вам нужно только то, что они делают!

опустить initState

Иногда мы создаемstoreне передается, когдаinitStateКак мы используем?

const store = createStore(reducer, {}, rewriteCreateStoreFunc)

reduxпозвольте нам написать

const store = createStore(reducer, rewriteCreateStoreFunc)

Нам просто нужно изменитьcreateStoreфункция, если второй аргументobjecт, мы думаем, что онinitState,еслиfunctionМы думаем, что онrewriteCreateStoreFunc.

function craeteStore(reducer, initState, rewriteCreateStoreFunc) {
  if (typeof initState === 'function') {
    rewriteCreateStoreFunc = initState
    initState = undefined
  }
  //...
}

Реализация реакции-редукции

Выше мы выполнили простойredux. Если компонент хочет получить доступ к общедоступному состоянию из хранилища, ему необходимо выполнить четыре шага:importпредставлятьstore,getStateполучить статус,dispatchИзменить статус,subscribeОбновления подписки, код относительно избыточен,

а такжеreact-reduxОн предоставляет решение для операции слияния:react-reduxпоставкаProviderа такжеconnectдва API,ProviderБудуstoreвставитьthis.contextЧтобы сэкономитьimportэтот шаг,connecт будетgetState,dispatchслился вthis.props, и автоматически подписываться на обновления, упрощая еще три шага,

Реализовать поставщика

import React from 'react'
import PropTypes from 'prop-types'

export default class Provider extends React.Component {
  // 需要声明静态属性childContextTypes来指定context对象的属性,是context的固定写法
  static childContextTypes = {
    store: PropTypes.object
  }

  // 实现getChildContext方法,返回context对象,也是固定写法
  getChildContext() {
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  // 渲染被Provider包裹的组件
  render() {
    return this.props.children
  }
}

ЗаканчиватьProviderПосле этого мы можем передать компонентthis.context.storeВозьми эту формуstore, не надо отделятьimport store.

реализовать подключение

Давайте подумаем, как добитьсяconnect, Давайте рассмотримconnectКак использовать:

connect(mapStateToProps, mapDispatchToProps)(App)

Мы уже знаем,connectперениматьmapStateToProps,mapDispatchToPropsДва метода, а затем возвращают функцию более высокого порядка, эта функция более высокого порядка получает компонент и возвращает компонент более высокого порядка (фактически, это добавление некоторых свойств и функций к входящему компоненту)connectПо поступающимmap,Будуstateа такжеdispatch(action)монтировать подкомпонентыprops, мы сразу выпускаемconnectКод реализации в несколько строк не сложен:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import bindActionCreators from '../redux/bindActionCreators'

export default function connect(mapStateToProps, mapDispatchToProps) {
  return function(Component) {
    class Connect extends React.Component {
      componentDidMount() {
        // 从context获取store并订阅更新
        this.context.store.subscribe(this.handleStoreChange.bind(this))
      }
      handleStoreChange() {
        // 触发的方法有多种,这里为了简洁起见,直接forceUpdate强制更新,读者也可以通过setState来触发子组件更新
        this.forceUpdate()
      }

      render() {
        const dispathProps =
          typeof mapDispatchToProps &&
          bindActionCreators(mapDispatchToProps, this.context.store.dispatch)

        return (
          <Component
            // 传入该组件的props,需要由connect这个高阶组件原样传回原组件
            {...this.props}
            // 根据mapStateToProps把state挂到this.props上
            {...mapStateToProps(this.context.store.getState())}
            // 根据mapDispatchToProps把dispatch(action)挂到this.props上
            {...dispathProps}
          />
        )
      }
    }
    // 接收context的固定写法
    Connect.contextTypes = {
      store: PropTypes.object
    }
    return Connect
  }
}

конец статьи

См. код:Реализовать редукцию и реакцию-редукцию