“Content” : This article briefly analyzes the principle and realization of Redux & Redux ecology. “Require” : Some experience with Redux is required to understand this article. “Gain” : you will Gain
- Write Redux, knowing exactly what you’re doing and what the impact of each line of code is.
- Understand how storeEnhancer Middleware works and make your own depending on your needs.
- Learning about functional paradigms is a great example of how they can be applied in practice.
“Correction” : If there are any mistakes, you are welcome to make comments and feedback
Redux design philosophy
Single source of truth
There can be only one unique global data source, and there is a one-to-one correspondence between state and view
State is read-only
The state is read-only, and when we need to change it, we replace it with a new one, rather than making changes directly to the original data.
Changes are made with pure functions
State updates are done using a Reducer function, which accepts an object (Action) that describes how the state changed to create the new state.
Redux architecture
Redux components
- State: Global state object, unique and immutable.
- Store: An object generated by calling the createStore function that encapsulates the methods defined within the createStore to manipulate global state and used by the user to use Redux.
- Action: An object that describes how the state is changed, has a fixed type property, and is submitted through the Store’s Dispatch method.
- Reducer: A pure function that actually performs state modification. Reducer is defined and passed in by the user and received from dispatch
Action takes the argument, evaluates to return the new state, completes the state update, and then executes the subscribed listener.
- StoreEnhancer: A higher-order encapsulation of createStore to enhance store capabilities. ApplyMiddleware provided by Redux is an official implementation of storeEnhancer.
- Middleware: A higher-order functional encapsulation of dispatch, in which applyMiddleware replaces the dispatch with an implementation containing the middleware chain calls.
Redux constitute
Redux API implementation
Redux Core
createStore
CreateStore is a large closure environment that defines the Store itself, as well as its various apis. There are signs in the environment to detect side effects such as obtaining state, triggering dispatch, change monitoring, etc., so Reducer is strictly controlled as a pure function. All the core ideas of the Redux design are implemented in this file. The entire file is only over 300 lines, which is simple but important. Here is a brief list of the functions implemented in this closure and the source code analysis to strengthen your understanding.
StoreEnhancer is applied if there is one
if (typeofenhancer ! = ='undefined') {
// Type detection
if (typeofenhancer ! = ='function') {... }// enhancer Accept a storeCreator return a storeCreator
// When it is applied, it returns storeCreatetor directly and returns the corresponding store
return enhancer(createStore)(reducer,preloadedState)
}
Copy the code
Otherwise dispatch an INIT action to make the Reducer produce an initial state. Note that INIT is a random number defined internally by Redux, which cannot be clearly defined by Reducer, and the state may be undefined at this time, so the reducer can successfully complete the initialization. We need to follow the following two rules when compiling reducer:
- For actions that do not define type, return the state of the input parameter.
- CreateStore If no initial state was passed in, the reducer must provide a default value.
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)
Copy the code
Finally, the methods defined in the closure are loaded into the Store object and returned
const store = {
dispatch,
subscribe,
getState,
replaceReducer, // Not used, so skip it
[$$observable]: observable // Not used, so skip it
}
return store;
Copy the code
Here is how these methods are implemented
getState
It is specified that getState cannot be called in reducer, and the current state is returned if conditions are met. It is very clear and will not be described again.
function getState(){
if (isDispatching) {
...
}
return currentState
}
Copy the code
dispatch
The built-in Dispatch only provides support for regular object actions, with other support like AsyncAction being put into middleware. Dispatch does two things:
- The call to Reducer produces a new state.
- Call the subscribed listener function.
For an ordinary Object, its prototype is Object */
function isPlainObject(obj){
if (typeofobj ! = ='object' || obj === null) return false
let proto = obj
// proto out of the loop is Object
while (Object.getPrototypeOf(proto) ! = =null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
function dispatch(action: A) {
// Check if it is a normal object
if(! isPlainObject(action)) { ... }// Redux requires action to have a type attribute
if (typeof action.type === 'undefined') {... }Do not use this tool in reducer
if (isDispatching) {
...
}
Call Reducer to produce a new state and replace the current state
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// Call the subscription listener
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
Copy the code
subscribe
Subscribe status updates and return unsubscribe methods. In fact, as long as dispatch is called, even if the reducer does not make any changes to the state, the listening function will also be triggered. Therefore, in order to reduce rendering, state diff will be made in the listener registered by itself in each UI Bindings to optimize performance. Note that the listener allows side effects.
// Make the nextListeners a slice of the currentListeners and then modify the slice to replace the currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function subscribe(listener: () => void) {
// Type detection
if(typeoflistener ! = ='function'){
...
}
No subscriptions are allowed in the reducer
if (isDispatching) {
...
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// Prevent double unsubscribing
if(! isSubscribed) {return
}
Unsubscription is also not allowed in the Reducer
if (isDispatching) {
...
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null}}Copy the code
applyMiddleware
ApplyMiddleware is an official implementation of storeEnhance to give Redux plug-in capabilities for various actions.
storeEnhancer
As you can see from the function signature, this is a higher-order function encapsulation of createStore.
type StoreEnhancer = (next: StoreCreator) = >StoreCreator;Copy the code
The CreateStore entry accepts only one storeEnhancer. If more than one storeEnhancer is passed in, compose is used to compose them. This is crucial to understanding how middleware works in chain calls, so look at that section first.
middleware
type MiddlewareAPI = { dispatch: Dispatch, getState: () = > State }
type Middleware = (api: MiddlewareAPI) = > (next: Dispatch) = > Dispatch
Copy the code
The outermost function receives the middlewareApi, provides the Store portion of the MIDDLEWARE API, and returns a function that participates in compose to make a chain call to middleware.
export default function applyMiddleware(. middlewares) {
return (createStore) = >{
// Initialize store and get dispatch
const store = createStore(reducer, preloadedState)
// Dispath is not allowed in Middlware
let dispatch: Dispatch = () = > {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.')}const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ... args) = >dispatch(action, ... args) }// Inject the API into Middlware
const chain = middlewares.map(middleware= > middleware(middlewareAPI)) // Focus on understanding
// compose passes into dispatch, generating a new chain of layered dispath calls
dispatch = compose<typeofdispatch>(... chain)(store.dispatch)// Replace dispath, return
return {
...store,
dispatch
}
}
}
Copy the code
Another example of middleware: Redux-thunk enables Redux to support asyncAction, which is often used in asynchronous scenarios.
// The outermost layer is a middleware factory function that generates middleware and injects additional parameters into asyncAction
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) = > (next) = >
(action) = > {
// Determine the type of action in the middleware. If it is a function, execute it directly
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument); } // Otherwise continue
return next(action);
};
}
Copy the code
Redux Utils
compose
Compose is a process often used in the functional programming paradigm, which creates a right-to-left flow of data, with the results of the right-side function execution passed as arguments to the left. Compose is a higher-order function that takes n function arguments and returns a function executed in the above data stream. If the array of arguments is also a higher-order function, the result of its compose function will be something like the following. The function returned by the array of higher-order functions will be a chain call from left to right.
export default function compose(. funcs) {
if (funcs.length === 0) {
return (arg) = > arg
}
if (funcs.length === 1) {
return funcs[0]}// simple direct compose
return funcs.reduce(
(a, b) = >
(. args: any) = >a(b(... args)) ) }Copy the code
combineReducers
It’s also a combination, but it’s a tree-like combination. You can create a complex Reducer, as shown in the following figure
The implementation method is also relatively simple, that is, the map object with a function package layer, return a mapedReducer, the following is a simplified implementation.
function combineReducers(reducers){
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
finalReducers[key] = reducers[key]
}
const finalReducerKeys = Object.keys(finalReducers)
// Reducer
return function combination(state, action){
let hasChanged = false
const nextState = {}
// Iterate over and execute
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {... } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length
return hasChanged ? nextState : state
}
}
}
Copy the code
bindActionCreators
Create an Action with actionCreator and dispatch it immediately
function bindActionCreator(ActionCreator, dispatch) {
return function (this. args) {
return dispatch(actionCreator.apply(this, args))
}
}
Copy the code
Redux UI bindings
React-redux
React- Redux is the official redux UI Bindings implementation. It provides two ways to use Redux: HOC and Hooks, corresponding to Class and functional components, respectively. We chose its Hooks implementation for analysis, focusing on how the UI component gets the global state and notifies the UI of updates when the global state changes.
How does the UI get global state
- React Context stores global state
export const ReactReduxContext = /*#__PURE__*/
React.createContext<ReactReduxContextValue | null> (null)
Copy the code
- Encapsulate it as a Provider component
function Provider({ store, context, children }: ProviderProps) {
const Context = context || ReactReduxContext
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
Copy the code
- Provide hook to get store: useStore
function useStore(){
const { store } = useReduxContext()!
return store
}
Copy the code
How do I notify UI updates when State changes
React-redux provides a hook: useSelector. This hook subscribes a listener to redux that is triggered when the state changes. It mainly does the following things.
When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.
- subscribe
const subscription = useMemo(
() = > createSubscription(store),
[store, contextSub]
)
subscription.onStateChange = checkForUpdates
Copy the code
- state diff
function checkForUpdates() {
try {
const newStoreState = store.getState()
constnewSelectedState = latestSelector.current! (newStoreState)if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
latestStoreState.current = newStoreState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err as Error
}
forceRender()
}
Copy the code
- re-render
const [, forceRender] = useReducer((s) = > s + 1.0)
forceRender()
Copy the code
How to use REdux without UI Bindings
In fact, just complete the above three steps can be used, here is an example:
const App = () = >{
const state = store.getState();
const [, forceRender] = useReducer(c= >c+1.0);
// Subscribe to updates, state changes refresh component
useEffect(() = >{
// Unsubscribe when the component is destroyed
return store.subscribe(() = >{
forceRender();
});
},[]);
const onIncrement = () = > {
store.dispatch({type: 'increment'});
};
const onDecrement = () = > {
store.dispatch({type: 'decrement'});
}
return (
<div style={{textAlign:'center', marginTop:'35'}} % >
<h1 style={{color: 'green', fontSize: '500'}} % >{state.count}</h1>
<button onClick={onDecrement} style={{marginRight: '10'}} % >decrement</button>
<button onClick={onIncrement}>increment</button>
</div>)}Copy the code
summary
The Redux core simply implements its “single state”, “state immutable”, “pure function” Settings, and is very small. StoreEnhancer and Middleware are exposed to add functionality to this concept and enrich the ecosystem. The development of Redux also proves that such design ideas make Redux very scalable. Among them, the application of higher-order functions is a plug-in system construction method that I think is worth learning from. Instead of directly setting the life cycle, the core functions are directly packaged with higher-order functions, and then the internal dependence on compose to complete the chain call can reduce the development mentality of external developers. The problem Redux wants to solve is the mapping problem of complex state and view, but Redux itself has no direct implementation, it only does the state management, and then exposes the monitoring ability of state update, the remaining state cache, state comparison, update view is left to the UI-Bindings of various frameworks, This keeps their code simple and stable while providing a “responsive class” state-view development experience for developers who actually use Redux State management at the View layer.
❤️ Thank you
That is all the content of this sharing. I hope it will help you
Don’t forget to share, like and bookmark your favorite things.
Welcome to pay attention to the public number ELab team receiving factory good article ~
We are from the front end department of Bytedance, responsible for the front end development of all bytedance education products.
We focus on product quality improvement, development efficiency, creativity and cutting-edge technology and other aspects of precipitation and dissemination of professional knowledge and cases, to contribute experience value to the industry. Including but not limited to performance monitoring, component library, multi-terminal technology, Serverless, visual construction, audio and video, artificial intelligence, product design and marketing, etc.
Interested students are welcome to post in the comments section or use the internal tweet code to the author’s section at 🤪
Bytedance enrollment internal push code: YCE7SSZ
Post links: jobs.toutiao.com/s/d7sPTXu