8

Conclusion first

  1. Redux is a state management library and an architecture
  2. Redux has nothing to do with React, but it is a solution to the inability to share state in React components
  3. Pure Redux is just a state machine. All the states are stored in the store. To change the state in the store, you can only dispatch an action.
  4. The sent action needs to be processed by the reducer, passing in the state and action, and returning the new state
  5. The subscribe method can register a callback method, and the callback will be executed when the dispatch action occurs
  6. Redux is actually a publish-subscribe model
  7. Redux supports enhancer. Enhancer is actually a decorator pattern, passing in the current createStore and returning an enhanced createStore
  8. Redux uses the applyMiddleware function to support middleware, and its return value is actually an enhancer
  9. Redux's middleware is also a decorator pattern, passing in the current dispatch and returning an enhanced dispatch
  10. Pure Redux has no View layer

Why did Redux appear?

We use the React stack by default, when the pages are small and simple, there is no need to use Redux at all. Redux came into existence to deal with complex components. That is, when the component is complex to three or even four layers (as shown in the figure below), component 4 wants to change the state of component 1

react 组件树

State hoisting, as React does, promotes state to the same parent component (grandparent component in the diagram). But once there are more levels, the root component needs to manage a lot of state, which is inconvenient to manage.

So there was a context (React 0.14 was definitely introduced), and the data sharing of "far-home components" can be realized through the context. But it also has disadvantages. Using context means that all components can modify the state in the context, just like anyone can modify the shared state, which leads to unpredictable program operation, which is not what we want.

Facebook proposed the Flux solution, which introduced the concept of one-way data flow (yes, React does not have the concept of one-way data flow, Redux is a one-way data flow concept that integrates Flux), and the architecture is shown in the following figure:

Flux 流程图

Flux is not listed here. Simple understanding, in the Flux architecture, View should notify Dispatcher (dispatcher) through Action (action), Dispatcher (dispatcher) to modify Store, Store and then modify View

What are the problems or shortcomings of Flux?

There are dependencies between stores, server-side rendering is difficult, stores mix logic and state

It was 2018 when the author was learning the React technology stack. It was the already popular solution of React + Redux. Flux has been eliminated. Understanding Flux is to lead to Redux.

The advent of Redux

Redux mainly solves the problem of state sharing

Official website: Redux is a JavaScript state container that provides predictable state management

Its author is Dan Abramov

Its structure is:

Redux 流程图

It can be seen that Redux is just a state machine without a View layer. The process can be described as follows:

  • Write a reducer yourself (pure function, indicating what data will be returned by doing an action)
  • Write an initState yourself (store initial value, writable or not)
  • Generate store through createStore, this variable contains three important properties

    • store.getState: get the unique value (using the closure brother)
    • store.dispatch: Action behavior (change the only specified property of the data in the store)
    • store.subscribe: subscribe (publish-subscribe mode)
  • Dispatch an action via store.dispatch
  • The reducer handles the action and returns a new store
  • If you subscribed, you will be notified when the data changes

According to the behavior process, we can write a Redux by hand, the following is in the table, let's talk about the characteristics first

Three principles

  • single source of truth

    • The global state of the entire application is stored in an object tree, and this object tree exists in only one store
  • State is read-only

    • The only way to change the state is to trigger an action, an action is an ordinary object describing the time that has occurred
  • Use pure functions to perform modifications

    • To describe how actions change the state tree, you need to write pure reducers

The three principles are for better development. According to the concept of one-way data flow , the behavior becomes traceable.

Let's start writing a Redux

handwritten redux

In accordance with the behavior process and principles, we must avoid problems such as random modification of data and traceability of behavior.

Basic: 23 lines of code to let you use redux

 export const createStore = (reducer, initState) => {
  let state = initState
  let listeners = []

  const subscribe = (fn) => {
    listeners.push(fn)
  }

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach((fn) => fn())
  }

  const getState = () => {
    return state
  }

  return {
    getState,
    dispatch,
    subscribe,
  }
}

make a test case

 import { createStore } from '../redux/index.js'

const initState = {
  count: 0,
}

const 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
  }
}

const store = createStore(reducer, initState)

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

store.dispatch({
  type: 'INCREMENT',
})
PS: I am using ES6 modules in node and need to upgrade Node version to 13.2.0

Second Edition: Difficulty Breakthrough: Middleware

Ordinary Redux can only do the most basic return data according to the action, dispatch is just a command to fetch data, for example:

 dispatch({
  type: 'INCREMENT',
})
// store 中的 count + 1

But in development, we sometimes need to view logs, asynchronous calls, record daily, etc.

What to do, make a plugin

In Redux, a similar concept is called middleware

中间件

Redux's createStore has three parameters

 createStore([reducer], [initial state], [enhancer]);

The third parameter is enhancer, which means enhancer. Its role is to replace the ordinary createStore and transform it into a createStore with middleware attached. A few analogies:

  • Tony Stark was originally an ordinary rich man, after adding enhancers (armor), he became Iron Man
  • After the central government issued a disaster relief fund, and after adding the booster (the management of the big and small officials), there was only a drop of money in the hands of the disaster victims.
  • Luffy hits people with armed color, and armed color is a middleware

What the enhancer has to do is: the thing is still that thing, but it has gone through some processes to strengthen it . These steps are done by the applyMiddleware function. In industry jargon, it's a decorator pattern . It is written roughly as:

 applyMiddleware(...middlewares)
// 结合 createStore,就是
const store = createStore(reudcer, initState, applyMiddleware(...middlewares))

So we need to transform createStore first, and judge that when there is an enhancer, we need to pass the value to the middleware

 export const createStore = (reducer, initState, enhancer) => {
    if (enhancer) {
        const newCreateStore = enhancer(createStore)
        return newCreateStore(reducer, initState)
    }

    let state = initState;
    let listeners = [];
    ...
}

If there is an enhancer, pass in the createStore function first. The generated newCreateStore is the same as the original createStore, and the store will be generated according to the reducer and initState. Can be simplified to:

 if (enhancer) {
  return enhancer(createStore)(reducer, initState)
}

PS: Why is it written like this, because redux is written in a functional way

Why createStore can be passed by value, because functions are also objects and can also be passed as parameters (old iron closure)

In this way, our applyMiddleware is naturally clear

 const applyMiddleware = (...middlewares) => {
    return (oldCreateStore) => {
        return (reducer, initState) => {
            const store = oldCreateStore(reducer, initState)
            ...
        }
    }
}

The store here represents the store in the normal version, and then we need to enhance the attributes in the store

I'd call it this: Five lines of code cost a woman $180,000 for me

 export const applyMiddleware = (...middlewares) => {
  return (oldCreateStore) => {
    return (reducer, initState) => {
      const store = oldCreateStore(reducer, initState)
      // 以下为新增
      const chain = middlewares.map((middleware) => middleware(store))
      // 获得老 dispatch
      let dispatch = store.dispatch
      chain.reverse().map((middleware) => {
        // 给每个中间件传入原派发器,赋值中间件改造后的dispatch
        dispatch = middleware(dispatch)
      })
      // 赋值给 store 上的 dispatch
      store.dispatch = dispatch
      return store
    }
  }
}

Now write a few middleware to test

 // 记录日志
export const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this.state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

// 记录异常
export const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action)
  } catch (error) {
    console.log('错误报告', error)
  }
}

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

Introduce into the project and run

 import { createStore, applyMiddleware } from '../redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'

const initState = {
  count: 0,
}

const 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
  }
}

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

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

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

Running discovery has implemented the most important function of redux - middleware

测试代码

To analyze the functional programming of middleware, take loggerMiddleware as an example:

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

In the applyMiddleware source code,

 const chain = middlewares.map((middleware) => middleware(store))

It is equivalent to passing the value of the normal version of the store to each middleware

 let dispatch = store.dispatch
chain.reverse().map((middleware) => (dispatch = middleware(dispatch)))

It is equivalent to passing in store.dispatch to each middleware, that is, next, the original dispatch = next . At this time, the middleware is already a finished product. The code (action) => {...} is the function const dispatch = (action) => {} . When you execute dispatch({ type: XXX }) execute this middleware (action) => {...}

PS: Currying is difficult to understand at first, but you can understand it with a lot of habits

Third Edition: Structural Complexity and Splitting

Middleware may be a bit complicated to understand, let's look at other concepts first to change ideas

After an application grows, it is obviously unscientific to maintain the code with only one JavaScript file. In Redux, in order to avoid this kind of situation, it provides combineReducers to use the entire multiple reducers, using methods such as:

 const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

Pass in an object in combinReducers , what kind of state corresponds to what kind of reducer. That's it, then combinReducers how to achieve it? Because it is relatively simple, do not do much analysis, go directly to the source code:

 export const combinReducers = (...reducers) => {
  // 拿到 counter、info
  const reducerKey = Object.keys(reducers)
  // combinReducers 合并的是 reducer,返回的还是一个 reducer,所以返回一样的传参
  return (state = {}, action) => {
    const nextState = {}
    // 循环 reducerKey,什么样的 state 对应什么样的 reducer
    for (let i = 0; i < reducerKey.length; i++) {
      const key = reducerKey[i]
      const reducer = reducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey
    }
    return nextState
  }
}

Create a new reducer folder in the same level directory, and create reducer.js , info.js , index.js

 // reducer.js
export default (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
      }
    case 'DECREMENT': {
      return {
        count: state.count - 1,
      }
    }
    default:
      return state
  }
}
 // info.js
export default (state, action) => {
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name,
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description,
      }
    default:
      return state
  }
}

Merge export

 import counterReducer from './counter.js'
import infoReducer from './info.js'

export { counterReducer, infoReducer }

Let's test it now

 import { createStore, applyMiddleware, combinReducers } from '../redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'

const initState = {
  counter: {
    count: 0,
  },
  info: {
    name: 'johan',
    description: '前端之虎',
  },
}

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

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

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

combinReducers also done

测试代码

Since the reducer is split, whether the state can also be split, and whether it needs to be passed, in our usual writing method, the state is generally not passed. Two transformations are needed here, one is that each reducer contains its state and reducer; the other is to transform createStore, so that initState can be passed or not, and initialized data

 // counter.js 中写入对应的 state 和 reducer
let initState = {
  counter: {
    count: 0,
  },
}

export default (state, action) => {
  if (!state) {
    state = initState
  }
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
      }
    case 'DECREMENT': {
      return {
        count: state.count - 1,
      }
    }
    default:
      return state
  }
}
 // info.js
let initState = {
  info: {
    name: 'johan',
    description: '前端之虎',
  },
}

export default (state, action) => {
  if (!state) {
    state = initState
  }
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name,
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description,
      }
    default:
      return state
  }
}

Retrofit createStore

 export const createStore = (reducer, initState, enhancer) => {

    if (typeof initState === 'function') {
        enhancer = initState;
        initState = undefined
    }
    ...
    const getState = () => {
        return state
    }
    // 用一个不匹配任何动作来初始化store
    dispatch({ type: Symbol() })

    return {
        getState,
        dispatch,
        subscribe
    }
}

in the main file

 import { createStore, applyMiddleware, combinReducers } from './redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

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

console.dir(store.getState())

So far, we have implemented a seven-seven-eight-eight redux

Complete Redux

unsubscribe

 const subscribe = (fn) => {
  listeners.push(fn)
  return () => {
    const index = listeners.indexOf(listener)
    listeners.splice(index, 1)
  }
}

The store obtained by the middleware

Now the middleware can get the complete store, he can even modify our subscribe method. According to the minimum open strategy , we only need to give getState, and modify the store passed to the middleware in applyMiddleware

 // const chain = middlewares.map(middleware => middleware(store))
const simpleStore = { getState: store.getState }
const chain = middlewares.map((middleware) => middleware(simpleStore))

compose

In our applyMiddleware, convert [A, B, C] to A(B(C(next))), the effect is:

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

Redux provides a compose, as follows

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

2 lines of code replaceReducer

To replace the current reudcer, use the scenario:

  • code splitting
  • dynamic loading
  • Real-time reloading mechanism
 const replaceReducer = (nextReducer) => {
  reducer = nextReducer
  // 刷新一次,广播 reducer 已经替换,也同样把默认值换成新的 reducer
  dispatch({ type: Symbol() })
}

bindActionCreators

What does bindActionCreators do? It hides dispatch and actionCreator through closures, so that other places cannot perceive the existence of redux. Generally combined with the connect of react-redux

Paste the source code directly here:

 const bindActionCreator = (actionCreator, dispatch) => {
  return function () {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export const bindActionCreators = (actionCreators, dispatch) => {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error()
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

Above, we have completed all the code in Redux. In general, the more than 100 lines of code here are all of Redux. The real Redux is nothing more than adding some comments and parameter verification.

Summarize

We list the terms related to Redux and sort out what it does

  • createStore

    • Create a store object, including getState, dispatch, subscribe, replaceReducer
  • reducer

    • Pure function, accept old state, action, generate new state
  • action

    • Action, which is an object, must include a type field, indicating that the view issues a notification to tell the store to change
  • dispatch

    • Dispatch, trigger action, generate new state. is the only way for a view to issue an action
  • subscribe

    • Subscribe, only subscribed, when dispatched, the subscription function will be executed
  • combineReducers

    • Merge reducers into one reducer
  • replaceReudcer

    • Functions that replace reducers
  • middleware

    • Middleware, extending the dispatch function

The brick house once drew a flowchart about Redux

流程图

Understand in a different way

As we said, Redux is just a state management library, which is driven by data and initiates an action, which will trigger the data update of the reducer to update to the latest store

Integrate with React

Take the newly made Redux and put it in React, and try what is called a Redux + React set. Note that we don't use React-Redux here, just take the combination of these two

Create the project first

 npx create-react-app demo-5-react

Introduce handwritten redux library

Introduce createStore in App.js , and write the initial data and reducer, and monitor the data in useEffect. After listening, when an action is initiated, the data will change. See the code:

 import React, { useEffect, useState } from 'react'
import { createStore } from './redux'
import './App.css'

const initState = {
  count: 0,
}

const 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
  }
}

const store = createStore(reducer, initState)

function App() {
  const [count, setCount] = useState(store.getState().count)

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setCount(store.getState().count)
    })
    return () => {
      if (unsubscribe) {
        unsubscribe()
      }
    }
  }, [])

  const onHandle = () => {
    store.dispatch({
      type: 'INCREMENT',
    })
    console.log('store', store.getState().count)
  }
  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onHandle}>add</button>
    </div>
  )
}

export default App

After the button is clicked, the data changes accordingly

效果图

PS: Although we can subscribe to the store and change data in this way, the code for subscription is too repetitive, and we can use high-order components to extract it. This is also what React-Redux does

Combining with native JS+HTML

We said that Redux is a separate existence from Redux, it not only acts as a data manager in Redux, but also acts as a starting position in native JS + HTML

 <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="container">
      <div id="count">1</div>
      <button id="btn">add</button>
    </div>
    <script type="module">
      import { createStore } from './redux/index.js'

      const initState = {
        count: 0,
      }

      const 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
        }
      }

      const store = createStore(reducer, initState)

      let count = document.getElementById('count')
      let add = document.getElementById('btn')
      add.onclick = function () {
        store.dispatch({
          type: 'INCREMENT',
        })
      }
      // 渲染视图
      function render() {
        count.innerHTML = store.getState().count
      }
      render()
      // 监听数据
      store.subscribe(() => {
        let state = store.getState()
        console.log('state', state)
        render()
      })
    </script>
  </body>
</html>

The effect is as follows:

效果图

state ecology

We talk about Redux from Flux, and then from Redux to talk about various middleware, among which React-saga is middleware for solving asynchronous behavior. It mainly adopts the concept of Generator, which is compared with React-thunk and React- Promise, it does not put asynchronous behavior on the action creator like the other two, but treats all asynchronous operations as "threads", triggers it through action, and emits action as output when the operation is completed

 function* helloWorldGenerator() {
  yield 'hello'
  yield 'world'
  yield 'ending'
}

const helloWorld = helloWorldGenerator()

hewlloWorld.next() // { value: 'hello', done: false }
hewlloWorld.next() // { value: 'world', done: false }
hewlloWorld.next() // { value: 'ending', done: true }
hewlloWorld.next() // { value: undefined, done: true }

To put it simply: when encountering the yield expression, the execution of the following operations is suspended, and the value of the expression immediately following the yield is used as the return value value, waiting for the next method to be called, and then continue to execute.

Dva

What is Dva?

Official website: Dva is first of all a data flow solution based on Redux + Redux-saga. In order to simplify the development experience, Dva has additional built-in react-router and fetch, so it can be understood as a lightweight application framework

Simply put, it integrates the most popular data flow solution, a React technology stack:

dva = React-Router + Redux + Redux-saga + React-Redux

Its data flow diagram is:

Dva 流程图

The view dispatches an action, changes the state (ie store), the state is bound to the view, and responds to the view

Others are not listed, you can go to the Dva official website to check, here is the Model, which contains 5 attributes

  • namespace

    • The namespace of the model, which is also its attribute on the global state, can only be used as a string, and does not support the creation of multi-layer namespaces by .
  • state

    • initial value
  • reducers

    • Pure function, define reducer in key/value format. Used to process synchronous erasure, the only place that can be modified state is triggered by action
    • The format is: (state, action) => newState or [(state, action) => newState, enhancer]
  • effects

    • Handle asynchronous operations and business logic, and define effects in key/value format
    • Do not modify state directly. triggered by action
    • call: perform an asynchronous operation
    • put: issue an Action, similar to dispatch
  • subscriptions

    • subscription
    • Executed at app.start() , the data source can be the current time, the server's websocket link, keyboard input, history routing changes, geolocation changes, etc.

Mobx

Whether View is subscribed or monitored, different frameworks have different technologies. In short, when the store changes, so does the view.

Mobx uses a reactive data streaming scheme. I will write a separate article in the future. This article is too long, so I won't write it first.

Supplement: One-way data flow

Let's first introduce data transfer in React, that is, communication problems

  • Send messages to child components
  • Send a message to the parent component
  • Send messages to other components

React only provides one way of communication: passing parameters.

That is, the parent passes the value to the child, the child cannot modify the data passed by the parent, and props are immutable. What if the child component wants to pass data to the parent component? Notify parent components by passing values through events in props

Warehouse address: https://github.com/johanazhu/jo-redux

This article participated in the SegmentFault Sifu essay "How to "anti-kill" the interviewer?" , you are welcome to join.

山头人汉波
391 声望554 粉丝