Photo credit: unsplash.com/photos/7ZWV…
preface
Redux is a state container for JS applications, providing predictable state management. Allows you to develop applications that behave steadily and predictably, run in different environments (client, server, native), and are easy to test.
Based on the sample
import { createStore } from 'redux'
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 }
case 'counter/decremented':
return { value: state.value - 1 }
default:
return state
}
}
let store = createStore(counterReducer)
store.subscribe(() = > console.log(store.getState()))
store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}
Copy the code
Redux workflow
Several core concepts
-
Store: A place where data is stored. You can think of it as a container. The entire app can only have one Store.
-
State: The Store object contains all the data. If you want data at a particular point in time, you need to take a snapshot of the Store. This set of data at a particular point in time, called State, is obtained through the Store’s getState() method.
-
Action: An Action is a common object. Changing the State of the object causes the View to change. You can’t touch State, you can only touch View. Therefore, the change in State must be caused by the View. An Action is a notification from the View that the State should change.
-
Action Creator: There are as many actions as the View wants to send. It would be too cumbersome to write it all by hand, so let’s define a function to generate the Action, which is called Action Creator.
-
After receiving the Action, the Reducer: Store must give the Reducer a new State so that the View will change. The computation process of this State is called Reducer. Reducer is a function that takes the Action and the current State as parameters and returns a new State.
-
Dispatch: is the only way a View can issue an Action.
The entire user interaction process:
-
The user (through the View) issues the Action through the Dispatch method.
-
The Store then automatically calls the Reducer with two parameters: the current State and the received Action, and the Reducer returns the new State.
-
When the State changes, the Store calls the SUBSCRIBE listener to update the View.
As you can see, the data flows in one direction throughout the process, which ensures a clear process.
Redux’s three principles
Single data source
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 state is to fire an action, which is a generic object that describes an event that has occurred.
Use the Reducer pure function to perform the changes
To describe how an action can change a state tree, you need to write pure reducers.
Redux design idea
In the last paragraph of the Motivation chapter in the redux.js official documentation, the Redux author explicitly states:
Following in the steps of Flux.CQRS, and Event Sourcing.Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.
Let’s first understand the concepts of Flux, CQRS, ES (Event Sourcing).
Event Sourcing
Event Sourcing, also known as Event Sourcing, is an architectural pattern proposed by Martin Fowler. It has several features:
-
The whole system is event-driven, and all business is completed by event-driven.
-
Events are first class citizens, and the system’s data is based on events, which are stored in some kind of storage.
-
The latest state of the object is obtained through event tracing.
For example, we usually have two ways of keeping accounts, directly recording the result of each bill or recording the income/expenditure of each time, then we can also calculate the result by ourselves, ES is the latter.
CQRS (Command Query Responsibility Segregation)
CQRS, short for Command Query Responsibility Segregation, commonly known as read-write separation.
CQRS classifies operations in a system into two categories: “Commands” and “Queries.” A command is a general term for operations that cause changes to the data, that is, we often say add, update, delete these operations, are commands. Queries, on the other hand, are exactly what they are: operations that do not change the data, but just look up the data based on certain criteria.
The core idea of CQRS is to separate these two different types of operations and implement them in two separate “services.” The term “service” here generally refers to two independently deployed applications. In some special cases, it can also be deployed on different interfaces within the same application.
Flux
Flux is an architectural idea that data always “flows one way” in the following process, and no “two-way flow” of data occurs in any adjacent part, which ensures a clear process. The biggest feature of Flux is the “one-way flow” of data.
Action -> Dispatcher -> Store -> View
Copy the code
The whole process is as follows:
- First you have to have actions, by defining someaction creatorMethod to create actions to provide to dispatcher as needed
- The View layer triggers actions through user interactions such as onClick
- The Dispatcher dispatches the triggered Action to all registered Store callbacks
- The Store callback emits one after updating its data based on the Action it receivedchangeThe event notifies the View that the data has changed
- The View is going to listen to thatchangeEvent to get corresponding new data to update the component UI
Redux and Flux
Redux has only one Store
Flux allows multiple stores, but Redux allows only one. Compared to Flux, a Store is clearer and easier to manage. Flux stores the application data in multiple stores and performs update logic in the Store. When the Store changes, it notifies the Controller-View to update its data. Redux consolidates the stores into a single Store, from which the full State of an application can be derived.
In addition, the updated logic in Redux is not executed in the Store but in the Reducer. The advantage of a single Store is that all data results are centralized and convenient to operate. As long as it is passed to the outermost component, the inner component does not need to maintain State. It can all be passed down from the parent via props.
There is no concept of Dispatcher in Redux
Redux removes the Dispatcher and uses the Store’s store.dispatch () method to pass the action to the Store, since all action processing will go through the store.dispatch () method. Redux cleverly exploited this by implementing mechanisms similar to those found in Koa and RubyRack. Middleware lets you block and insert code after you dispatch an action and before you get to the Store, allowing you to manipulate the action and Store.
Redux source
Redux source address: github.com/reduxjs/red…
Index. ts Entry file
You can see the source code here
.export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
Copy the code
Import file is mainly exported createStore, combineReducers, bindActionCreators, applyMiddleware, compose API.
Createstore. ts Main flow file
You can see the source code here
const store = createStore(reducer, preloadedState, enhancer)
Copy the code
import $$observable from './utils/symbol-observable'
import {
Store,
PreloadedState,
StoreEnhancer,
Dispatch,
Observer,
ExtendState
} from './types/store'
import { Action } from './types/actions'
import { Reducer } from './types/reducers'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'
import { kindOf } from './utils/kindOf'
export default function createStore<
S.A extends Action.Ext = {},
StateExt = never>( reducer: Reducer<S, A>, enhancer? : StoreEnhancer<Ext, StateExt> ): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Extexport default function createStore<
S.A extends Action.Ext = {},
StateExt = never>( reducer: Reducer<S, A>, preloadedState? : PreloadedState<S>, enhancer? : StoreEnhancer<Ext, StateExt> ): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Extexport default function createStore<
S.A extends Action.Ext = {},
StateExt = never>( reducer: Reducer<S, A>, preloadedState? : PreloadedState<S> | StoreEnhancer<Ext, StateExt>, enhancer? : StoreEnhancer<Ext, StateExt> ): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {// If preloadedState and enhancer are not both functions, an error is reported.
if((typeof preloadedState === 'function' && typeof enhancer === 'function') | | (typeof enhancer === 'function' && typeof arguments[3= = ='function')) {throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.')}// If preloadedState is function and enhancer is undefined, initState is not initialized, but middleware is passed.
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// Assign preloadedState to enhancer.
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
// preloadedState: undefined
preloadedState = undefined
}
/ / enhancer
if (typeofenhancer ! = ='undefined') {
// If enhancer is not a function, throw an error.
if (typeofenhancer ! = ='function') {
throw new Error(
`Expected the enhancer to be a function. Instead, received: '${kindOf( enhancer )}'`)}// Execute enhancer function
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState<S>
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}
// Enhancer does not throw an error for the function
if (typeofreducer ! = ='function') {
throw new Error(
`Expected the root reducer to be a function. Instead, received: '${kindOf( reducer )}'`)}// Perform variable assignment
let currentReducer = reducer
let currentState = preloadedState as S
let currentListeners: (() = > void|) []null = []
// Shallow copy queue
let nextListeners = currentListeners
// No Dispatch is running
let isDispatching = false
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// Get the current state
function getState() :S {
// Throw an error if dispatch is in progress.
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.')}/ / return to the state
return currentState as S
}
// The subscribe method sets the listening function, which is automatically executed when dispatch is triggered.
function subscribe(listener: () => void) {
// An error is thrown when the listener is not a function
if (typeoflistener ! = ='function') {
throw new Error(
`Expected the listener to be a function. Instead, received: '${kindOf( listener )}'`)}// Throw an error while in dispatch
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.')}// The subscription tag is used
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
// Return the cancel listener function
return function unsubscribe() {
if(! isSubscribed) {return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
isSubscribed = false
// Save the subscription snapshot
ensureCanMutateNextListeners()
// Find and delete the listener
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null}}function dispatch(action: A) {
// Acticon must be a function constructed from Object
if(! isPlainObject(action)) {throw new Error(
`Actions must be plain objects. Instead, the actual type was: '${kindOf( action )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`)}// Action. Type Exists
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.')}// Determine whether the dispatch is in progress
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')}try {
isDispatching = true
/ / reducer for execution
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// Listen to queue
// All listeners are assigned to listeners
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
// Execute the listening function
listener()
}
return action
}
// Replace reducer, which is used in scenarios such as hot replacement and on-demand loading
function replaceReducer<NewState.NewActions extends A> (
nextReducer: Reducer<NewState, NewActions>
) :Store<ExtendState<NewState.StateExt>, NewActions.StateExt.Ext> & Ext {
// nextReducer is not a function
if (typeofnextReducer ! = ='function') {
throw new Error(
`Expected the nextReducer to be a function. Instead, received: '${kindOf( nextReducer )}`)};// Update currentReducer to nextReducer
(currentReducer as unknown as Reducer<NewState, NewActions>) = nextReducer
dispatch({ type: ActionTypes.REPLACE } as A)
return store as unknown as Store<
ExtendState<NewState, StateExt>,
NewActions,
StateExt,
Ext
> &
Ext
}
.....
dispatch({ type: ActionTypes.INIT } as A)
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
return store
}
Copy the code
ApplyMiddleware middleware
In actual projects, there are generally synchronous and asynchronous operations, so Flux, Redux and other ideas, eventually to be landed in synchronous asynchronous processing.
In Redux, synchronization occurs when the Reducer calculates State immediately after the Action is sent. In this case, after the Action is sent, the Reducer is executed after a period of time.
In order to automatically execute Reducer after asynchronous operation, Redux introduces the concept of Middleware Middleware.
const store = createStore(todos, ['Use Redux'], applyMiddleware(logger))
Copy the code
applyMiddleware.ts
You can see the source code here
import compose from './compose'.export default function applyMiddleware(. middlewares: Middleware[]) :StoreEnhancer<any> {
return (createStore: StoreEnhancerStoreCreator) = ><S, A extends AnyAction>( reducer: Reducer<S, A>, preloadedState? : PreloadedState<S>) => {// execute the createStore method and assign the value to store const store = createStore(reducer, PreloadedState) // defines a dispatch that will throw an error let dispatch: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other } // Define the middlewareAPI, the store in the middleware, that will be passed to the middleware as a parameter. const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ... args) => dispatch(action, ... Args)} // Call each middlewareAPI => Next => Action =>{}, const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose<typeof dispatch>(... Chain)(store.dispatch) // Return enhanced store return {... store, dispatch } } }Copy the code
As you can see from the code above, applyMiddleware returns a function that takes the createStore parameter.
return (createStore) = >{
return(reducer,preloadedState?) {... }}Copy the code
The ts in createStore.,
if (typeofenhancer ! = ='undefined') {
if (typeofenhancer ! = ='function') {
throw new Error(
`Expected the enhancer to be a function. Instead, received: '${kindOf( enhancer )}'`)}return enhancer(createStore)(
reducer,
preloadedState
)
}
Copy the code
We execute enhancer middleware, pass in the createStore parameter, and it returns the reducer preloadedState function.
Compose function
Combine multiple functions from right to left. This is the method of functional programming, put in Redux for convenience. It is needed when multiple Store enhancers need to be executed in sequence.
dispatch = compose<typeofdispatch>(... chain)(store.dispatch)Copy the code
export default function compose(. funcs:Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) = > arg
}
if (funcs.length === 1) {
return funcs[0]}return funcs.reduce((a, b) = >(. args:any) = >a(b(... args))) }Copy the code
Focus on this code:
return funcs.reduce((a, b) = > (. args) = >a(b(... args)))Copy the code
Reduce function MDN document
If funcs is an array of three functions [f1, f2, f3], execute funcs.reduce((a, b) => (… args) => a(b(… Args))), then reduce execution process is as follows:
-
A = f1, b = f2, returnValue1 = (… args) => f1(f2(… args))
-
A = returnValue1, b = F3, returnValue2 = (… args) => returnValue1(f3(… Args), namely, f1, f2, f3 (… The args))).
combineReducers
The combineReducers helper function combines an object that has several different reducer functions as its value into a final reducer function. The combineReducers helper function then calls the createStore method on this reducer.
The merged reducer can call each sub-reducer and combine their returned results into a single state object. The state object returned by combineReducers() will name the state returned by each reducer passed in according to the key it was passed to combineReducers().
CombineReducers source: github.com/reduxjs/red…
export default function combineReducers(reducers: ReducersMapObject) {
// Use the object. keys method to generate an array of keys from the enumerable attributes in reducers
const reducerKeys = Object.keys(reducers)
const finalReducers: ReducersMapObject = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// Omit the code for development environment judgment....
// Reducer should be a function
if (typeof reducers[key] === 'function') {
// Assign to finalReducers
finalReducers[key] = reducers[key]
}
}
// Generate an array of keys from the enumerable attributes in finalReducers
const finalReducerKeys = Object.keys(finalReducers)
// Omit the code for development environment judgment.....return function combination(
state: StateFromReducersMapObject<typeof reducers> = {},
action: AnyAction
) {...// Omit the code for development environment judgment....
// Use the hasChanged variable to determine whether the state changed before and after
let hasChanged = false
// Declare a nextState object to store the updated state
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
// Obtain the key and value of finalReducerKeys (function)
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const actionType = action && action.type
throw new Error(
`When called with an action of type ${
actionType ? `"The ${String(actionType)}"` : '(unknown type)'
}, the slice reducer for key "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
)
}
nextState[key] = nextStateForKey
// Compare two keys, if they are not equal, change the keyhasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length
return hasChanged ? nextState : state
}
}
Copy the code
bindActionCreators
Turn an object whose value is a different Action Creator into an object with the same key. Each Action Creator is also wrapped with Dispatches so that they can be called directly.
The only situation where bindActionCreators will be used is when you need to upload ActionCreator down to a component, but don’t want the component to be aware of the Redux and don’t want to apply the Dispatch or Redux Store.
Source: github.com/reduxjs/red…
.// bindActionCreator generates action 2. To perform an action
function bindActionCreator<A extends AnyAction = AnyAction> (actionCreator: ActionCreator, dispatch: Dispatch) {
// This is a closure
return function (this: any. args:any[]) {
// Calls arguments directly with the passed actionCreator
return dispatch(actionCreator.apply(this, args))
}
}
export default function bindActionCreators(
actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
dispatch: Dispatch
) {
/ / actionCreators for function
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// Error handling
if (typeofactionCreators ! = ='object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, but instead received: '${kindOf( actionCreators )}'. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? `)}const boundActionCreators: ActionCreatorsMapObject = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
Copy the code
The resources
Redux 中 国 网 站;
Redux goes from design to source