Let’s start with a simple question: what is a Redux? A: In the React project, resolve the complex hierarchy of component data communication issues, update the Redux data, and the component view will be updated. This is the answer most React developers have. That’s what I thought at first, including the author. Entry-level workers are stuck in a tool/library usage scenario, API usage, usage configuration, etc., and will inevitably need to dig deeper into what they are using if they want to move up to the middle and advanced levels. So I decided to parse Redux from zero to one at the code level based on my own experience and understanding of Redux. The author has looked through many articles that need to dig gold ah zhi Hu ah language bird to analyze the source code of Redux, and this article is not to directly start to show and explain the source code, but to implement a Redux from the design idea and purpose of Redux. Hopefully this article will be helpful and enlightening for developers who are new to Redux or want to learn more about it.





Redux is just a tool to provide data state

To understand Redux, you need to tear it apart from the existing image and label in your mind. It doesn’t belong in React, it’s just a data state management tool. Remember, put React aside for now.





It all started with Redux’s design ideas

  • Single data stream
  • STORE (also known as raw data) is read-only
  • Changing data requires a pure function

All code implementations will revolve around these three design ideas





Make assumptions based on design ideas

The magic of MV/ XX-like frameworks is to change the data and drive the view synchronously. In any framework, data is the first thing to exist. So let’s assume the following data:

Var store = {name: 'lemon', age: 25}Copy the code

The entire application’s data is stored in the store above, and there is only one such store in the application, which is the idea of a single data stream. For example, store.name is used not only on the profile page but also on the profile page. The name data in the view will come from only one store. Anyway, we’ve got a store with all the data we need for the various views, and we’re just keeping it all in there. It’s pretty cool.

We need to edit the name on the personal page, and then the name of the personal space page is updated. Boy, isn’t that easy? So we want to write the following code:

store.name = 'lemon zhang'
Copy the code

Oh, sorry, don’t forget the Redux design idea: STORE read only. That would be nice, but I’m sorry we can’t. So we thought we’d just add methods to the Store and let the API change the data we want. Call this method dispatch, and you have the following code

store.dispatch({name: 'lemon zhang'});

console.log(store.name); // 'lemon zhang'

Copy the code





store.dispatch({name: 'lemon zhang'}); Store. name === 'lemon zhang' // true store.name === 'lemon Zhang '// falseCopy the code

Summary: According to the Redux design philosophy, we need to have a store that is read-only and can only change our data through the method Dispatch. The implementation behind Dispatch does not support side effects.

Let’s just do it based on the hypothesis

So let’s just go through the code

Var initState = {name: 'lemon', age: 25};Copy the code

Ok, we need to write a method to generate the store we need. Let’s call this method createStore. We have assumed what store looks like in our mind, so let’s implement it in createStore.

var initState = { name: 'lemon', age: 25 }; function createStore(state = initState) { const store = { ... state }; function dispatch(key, value) { store[key] = value; } function getStore() { return store } return { getStore, dispatch } } var store = createStore(); var user = store.getStore(); user.name === 'lemon'; // true user.age === 25; // true store.dispatch('name', 'lemon zhang'); store.dispatch('age', 26'); user.name === 'lemon zhang'; // true user.age === 26; // trueCopy the code

Ah, isn’t that easy? Looks like there’s nothing magical about Redux. As a result, we took the above code happily to the project use, the problem is a pile.

Var state = {detail: {user: {name: 'lemon', age: 25}}} designs such as store.dispatch('name', 'lemon Zhang ') will not work and cannot traverse the hierarchyCopy the code

For a large complex project, there are a lot of data to be managed by the state. If all the data are expanded into the store, it will inevitably lead to the problem of duplicate keys. But if you break it down by module and set up the store in a hierarchical way, then dispatch won’t work. In addition, it is not applicable in the increasingly complex interaction scenarios of the front end, such as paging load, which requires repeated paging requests to splice data sequentially into Settings in store, bringing the hell of repeated getStore.





So, these issues led us to rethink how to design createStore to make it more reasonable. This led us to introduce the concept of “Object Structure Description”, which has a more familiar name: Reducer. Reducer describes the structure of objects in various scenarios. What are the scenarios? Previous page, next page, increment, and decrement are all scenarios, also known by a more familiar name: Action.

Var action = {name: 'increase', // data: 2 // data generated when the scene occurs}Copy the code

Now we have a new concept action, let’s design reducer according to action

Var initState = {name: 'lemon', age: 25, favor: {ball: {basketBall: 'well', footBall: 'good' }, TV: { bilibili: 'usually', aqiyi: 'never' } } }; function reducer(state = initState, action) { switch(action.name) { case 'change_user_name': state.name = action.data; return state; case 'increae_age': state.age += 1; return state; case 'update_favor_ball_baskteBall': state.favor.ball.basketBall = action.data; return state; . default: return state; }}Copy the code

The concept of scene + object structure description is introduced, which enables us to deal with data changes more semantically. Multi-level structure can also be changed more fine-grained in various scenarios. Come on, I can’t wait to revamp createStore. The full code is below

Createstore. js function createStore(reducer, initState = {}) {if (typeof reducer! == 'function') { throw new Error('reducer must be function') } let store = initState; function getStore() { return store; } function dispatch(action) { if (typeof action.name === 'undefined') { throw new Error('format of action must exist name and data'); } store = reducer(action)} // Initialize store dispatch({name: 'init'}); return { getStore, dispatch } }Copy the code

Let’s see how awesome the new createStore is

var initState = { name: 'lemon', age: 25, favor: { ball: { basketBall: 'well', footBall: 'good' }, TV: { bilibili: 'usually', aqiyi: 'never' } } }; function reducer(state = initState, action) { switch (action.name) { case 'change_user_name': state.name = action.data; return state; case 'increae_age': state.age += 1; return state; case 'update_favor_ball_baskteBall': state.favor.ball.basketBall = action.data; return state; default: return state; } } function createStore(reducer, initState = {}) { if (typeof reducer ! == 'function') { throw new Error('reducer must be function') } let store = initState; function getStore() { return store; } function dispatch(action) { if (typeof action.name === 'undefined') { throw new Error('format of action must exist name and data'); } store = reducer(store, action)} // Store dispatch({name: 'init'}); return { getStore, dispatch } } const store = createStore(reducer, initState); store.dispatch({ name: 'change_user_name', data: 'lemon zhang' }); store.dispatch({ name: 'increae_age' }); store.dispatch({ name: 'update_favor_ball_baskteBall', data: 'no good' }); const data = store.getStore(); console.log(data.name); // lemon zhang console.log(data.age); // 26 console.log(data.favor.ball); // test2.html:92 {basketBall: "no good", footBall: "good"}Copy the code

How elegant it is, at least for now, that it can be used, so to speak, this is the very foundation of Redux’s design. Ok, I’m back to putting the above code to use. Continued to find some problems. The project is very large. If all the areas related to data state management are put into a Reducer, it will be bloated and impossible to maintain. Then we should organize our Reducer according to modules, and there will be many Reducer.

Look, Mom, we've been split. // author.js var author = { name: 'lemon', age: 25, }; function Authorreducer(state = author, action) { switch(action.name) { case 'change_user_name': state.name = action.data; return state; case 'increae_age': state.age += 1; return state; default: return state; } } // authorFavor.js var myfavor = { favor: { ball: { basketBall: 'well', footBall: 'good' }, TV: { bilibili: 'usually', aqiyi: 'never' } } } function MyFavorreducer(state = myfavor, action) { switch(action.name) { case 'update_favor_ball_baskteBall': state.favor.ball.basketBall = action.data; return state; default: return state; }} in accordance with the principle of modular development above the code can be divided into 2 JS files, the latter mentioned don't meng force, remember these two thingsCopy the code

Next, we need to design a Reducer management method that can seamlessly use the Reducer with the original createStore. Let’s call this method combineReducer

// Write the combinereducers.js file function combineReducers(reducers) {const reducerKeys = object.keys (reducers) const FinalReducers = {} for (let I = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } Const finalReducerKeys = object. keys(finalReducers) // Return a Reducer return function combination(state = {}, action) { let hasChanged = false const nextState = {} 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') { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! == Object.keys(state).length return hasChanged ? nextState : state } }Copy the code

The principle is relatively simple. Multiple reducers are organized using key + Reducer mapping, and only one Reducer is returned at last. When dispacth, a FOR loop is executed. Let’s see how we use combineReducers

import AuthorReducer from './author.js'
import AuthorFavorReducer from './authorFavor.js'
import createStore from './createStore.js';
import combineReducers from './combineReducers.js'
const store = createStore(combineReducers({
    author: AuthorReducer,
    favor: AuthorFavorReducer
}));
store.dispatch({
    name: 'change_user_name',
    data: 'lemon zhang'
});
store.dispatch({
    name: 'increae_age'
});
store.dispatch({
    name: 'update_favor_ball_baskteBall',
    data: 'no good'
});
const data = store.getStore();
console.log(data.name); // lemon zhang
console.log(data.age); // 26
console.log(data.favor.ball); // test2.html:92 {basketBall: "no good", footBall: "good"}
Copy the code

Everything is perfect. Enjoy the ride. After we organized Reducer more gracefully, there was still a problem in the project, that is, all kinds of dispatch were filled with the code of each module. You should know that all kinds of action names were just magic strings. Once we wanted to change an action name, There are dozens of action names with the same name in the project that need to be changed and replaced. It’s really uncomfortable. How do you solve it? First of all, we need to unify the management of our actions. First, we need to use constants to manage various action names instead of magic characters, and then introduce the factory mode actionCreator to generate various actions.

// bindActionCreator. Js const INCREASEAGE = 'increae_age'; const CHANGENAME = 'change_user_name'; const UPDATEBALL = 'update_favor_ball_baskteBall' function makeIncreaseAgeActions() { return { name: INCREASEAGE } } function makeChangeNameActions(name) { return { name: CHANGENAME, data: name } } function makeUpdateBasketBallActions(status) { return { name: UPDATEBALL, data: status } } function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) } }Copy the code

Finally, let’s see what happens to our code.

import AuthorReducer from './author.js' import AuthorFavorReducer from './authorFavor.js' import createStore from './createStore.js'; import combineReducers from './combineReducers.js' import {makeIncreaseAgeActions, makeChangeNameActions, makeUpdateBasketBallActions, bindActionCreator} from './bindActionCreator'; const store = createStore(combineReducers({ author: AuthorReducer, favor: AuthorFavorReducer })); const changeName = bindActionCreator(makeChangeNameActions, store.dispatch); const increaseAge = bindActionCreator(makeIncreaseAgeActions, store.dispatch); const updateBasketBall = bindActionCreator(makeUpdateBasketBallActions, store.dispatch); // These methods can be exported anywhere using more semantic changeName(); increaseAge(); updateBasketBall(); const data = store.getStore(); console.log(data.name); // lemon zhang console.log(data.age); // 26 console.log(data.favor.ball); // test2.html:92 {basketBall: "no good", footBall: "good"}Copy the code

Come to here, the code above is can completely be reasonable and elegant manage our data, but now he was not interesting enough, we want him to be able to notice after the update data, then receive inform access can take to do more interesting things, such as update the story to update the view, also can take to deal with other business logic, It’s all pretty good. We introduced the observer pattern to make the Store have observed properties. Come and reinvent createStore.

Createstore. js function createStore(reducer, initState = {}) {if (typeof reducer! == 'function') { throw new Error('reducer must be function') } let store = initState; const listeners = []; Function getStore() {return store; } function subscribe(fc) { listeners.push(fc); return function unSubscribe() { listeners.splice(listeners.indexOf(fc), 1); } } function dispatch(action) { if (typeof action.name === 'undefined') { throw new Error('format of action must exist name and data'); } store = reducer(action); listeners.forEach(fc => fc()); } // Initialize store dispatch({name: 'init'}); return { getStore, dispatch, subscribe } }Copy the code

Let’s see how it works

import AuthorReducer from './author.js' import AuthorFavorReducer from './authorFavor.js' import createStore from './createStore.js'; import combineReducers from './combineReducers.js' import {makeIncreaseAgeActions, makeChangeNameActions, makeUpdateBasketBallActions, bindActionCreator} from './bindActionCreator'; const store = createStore(combineReducers({ author: AuthorReducer, favor: AuthorFavorReducer })); Store. Subscribe (() => {console.log(' Update done ', store.getStore()) }) const changeName = bindActionCreator(makeChangeNameActions, store.dispatch); const increaseAge = bindActionCreator(makeIncreaseAgeActions, store.dispatch); const updateBasketBall = bindActionCreator(makeUpdateBasketBallActions, store.dispatch); // These methods can be exported anywhere using more semantic changeName(); increaseAge(); updateBasketBall();Copy the code

Our Redux is starting to get interesting, but not interesting enough, and we need to provide a middleware mechanism that leaves it completely open for Dispatch to do more than just trigger the action and eventually change the store. Back to createStore, a third parameter is provided to access the middleware mechanism. Note that this middleware mechanism is the most difficult part of Redux to understand and needs to be understood step by step.

Createstore. js function createStore(Reducer, initState = {}, CreateStore Reducer state is passed into the reducer state if (typeof enhancer! == 'undefined') { if (typeof enhancer ! == 'function') {throw new Error('Expected the enhancer to be a function.') enhancer(createStore)(reducer, initState) } if (typeof reducer ! == 'function') { throw new Error('reducer must be function') } let store = initState; const listeners = []; Function getStore() {return store; } function subscribe(fc) { listeners.push(fc); return function unSubscribe() { listeners.splice(listeners.indexOf(fc), 1); } } function dispatch(action) { if (typeof action.name === 'undefined') { throw new Error('format of action must exist name and data'); } store = reducer(store, action); listeners.forEach(fc => fc()); } // Initialize store dispatch({name: 'init'}); return { getStore, dispatch, subscribe } }Copy the code

We will give all the createStore Reducer initState and let enhancer play by itself. The design behind it is complicated and involves the idea of functional programming. So let’s start with COMPOSE

function compose(funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code

Do you know reduce? Do not know the trouble to go to MDN to view the documentation will not be described here. It is a headache to see the following code for easy understanding

function add1(a) {
    return a + 1
}
function add2(a) {
    return a + 2
}
function add3(a) {
    return a + 3
}
const add = compose([add1, add2, add3]);
add(5) === add1(add2(add3(5)))
Copy the code

Let’s move on to see how all the middleware is organized

Function applyMiddleware(middlewares) {return enhancer(createStore)(reducer, InitState) return createStore => (... args) => { const store = createStore(... Args) // Generate store let dispatch according to the incoming reducer initState = () => {throw new Error('Dispatching while constructing your '+ 'Other Middleware would not be applied to this dispatch.')} // The core API is injected into each middleware to be const middlewareAPI = { getStore: store.getStore, dispatch: MiddlewareAPI => args => {// do something} const chain = Middlewares. Map (Middleware => Middleware (middlewareAPI)) // Puts dispatches into a chain call of middleware to return a new dispatch // This requires compose(chain) to return a message of the form dispatch => (... Arags) => {} // So the correct way to write middleware is middlewareAPI => dispatch => args => {// do something} dispatch = Compose (chain)(store.dispatch) : compose(chain)(store.dispatch) // Returns a store, note that the dispatch is already a new dispatch // each execution of the dispatch will trigger the chained middleware return {... store, dispatch } } }Copy the code

For this section of the core method has been resolved, if not clear to read several times, basically every paragraph has a comment. Let’s test the waters by writing middleware. Write a redux-log middleware that prints a log every time it executes for easy tracking.

Const reduxLog = middlewareAPI => dispatch => args => {console.log(' Action -> ', args); dispatch(args); }Copy the code

Isn’t that easy? Let’s test it out

import {AuthorReducer, AuthorFavorReducer, initState} from './reducer.js' import combineReducers from './combineReducers.js' import {makeIncreaseAgeActions, makeChangeNameActions, makeUpdateBasketBallActions, bindActionCreator} from './bindActionCreator'; import {reduxLog} from './middleware'; import applyMiddleware from './applyMiddleware'; import compose from './compose'; const store = createStore(combineReducers({ author: AuthorReducer, favor: AuthorFavorReducer }), initState, compose(applyMiddleware([reduxLog]))); Store. Subscribe (() => {console.log(' Update done ', store.getStore()) }) const changeName = bindActionCreator(makeChangeNameActions, store.dispatch); const increaseAge = bindActionCreator(makeIncreaseAgeActions, store.dispatch); const updateBasketBall = bindActionCreator(makeUpdateBasketBallActions, store.dispatch); // These methods can be exported anywhere using more semantic changeName(); increaseAge(); updateBasketBall();Copy the code

Let’s see what happens

conclusion

At this point, redux has been parsed. Gradually optimize and improve by assuming from the design idea. Experience Redux’s design in every detail. Finally, let’s take a look at how Redux is structured.

You can see no difference, but the official website Redux still has a lot of details, such as parameter detection and so on, this article is not analyzed one by one, compared to many online source code analysis article not much to say a first analysis, I feel that it is more to know why. If my article helps you, don’t forget to like it, ok? PS(all the code for this article can be found in the Github repository Mini-Redux), don’t forget to hit the star