Implement a mini Redux (Basic Edition)

Time:2020-10-1

preface

Starting from the principle of Redux, this paper realizes a simple Redux step by step. The main purpose is to understand the internal relations of redux. Before reading this article, you should know how Redux is used, and I won’t explain too much about the usage of redux.

Introduction to Redux

First of all, Redux has nothing to do with react. Redux can be used in any framework.
Redux is a JavaScript state manager, which is a new front-end “architecture mode”.

There is also a library commonly used with Redux – react Redux, which combines the Redux architecture mode with the React.js A combined library is the Redux architecture React.js The embodiment of.

design idea

  1. Web application is a state machine, and view and state are one-to-one corresponding.
  2. All the states are stored in an object.

When to use Redux

  1. The user’s usage is complex
  2. Users with different identities have different ways to use them (such as ordinary users and administrators)
  3. Multiple users can collaborate
  4. Interact with the server a lot, or use websocket
  5. View wants to get data from multiple sources

From the perspective of components:

  1. The state of a component needs to be shared
  2. A state needs to be available anywhere
  3. A component needs to change its global state
  4. One component needs to change the state of another

Redux workflow

Implement a mini Redux (Basic Edition)

  1. Redux stores the entire application state in one place (usually called store)
  2. When we need to modify the state, we must dispatch an action (action is an object with a type field)
  3. The special state processing function reducer receives the old state and action, and returns a new state
  4. Set up the subscription through subscribe, and all subscribers will be informed each time the action is dispatched.

From this process, we can see that the core of Redux is an observer pattern. Once the store changes, all subscribers are notified and the view (in this case, the react component) is re rendered after receiving the notification.

Redux case

In order to simplify the explanation, I use the example similar to the official website to rewrite the case
Redux demo

Create a new file redux.js , and then import it directly and observe the console output

import { createStore } from 'redux'
const defaultState = {
    value: 10
}
//Reducer processing function
function reducer (state = defaultState, action) {
    console.log(state, action)
    switch (action.type) {
        case 'INCREMENT':
            return {
                ...state,
                value: state.value + 1
            }
        case 'DECREMENT':
            return {
                ...state,
                value: state.value - 1
            }
        default:
            return state
    }
}
const store = createStore(reducer)

const init = store.getState()
console.log (` starting with:${ init.value >)

function listener () {
    const current = store.getState()
    console.log (` the current figures are:${ current.value >)
}
store.subscribe (listener) // listen for changes in state

store.dispatch({ type: 'INCREMENT' })
//The current number is: 11
store.dispatch({ type: 'INCREMENT' })
//The current number is: 12
store.dispatch({ type: 'DECREMENT' })
//The current number is: 11

export default store

Output results:

{value: 10} {type: "@@redux/INIT1.a.7.g.7.t"}
At first, the number was 10
{value: 10} {type: "INCREMENT"}
The current number is: 11
{value: 11} {type: "INCREMENT"}
The current number is: 12
{value: 12} {type: "DECREMENT"}
The current number is: 11

All operations on data must go through the dispatch function, which accepts a parameter action. The action is a common JavaScript object, and the action must contain atypeField, tell it what to modify, only if it allows it to be modified.

Every time we call the reducer function, we print the state and action, which we manually pass store.dispatch The method dispatches the action three times, but you will find that it outputs four times. This is because the internal initialization of Redux automatically executes the dispatch method once. You can see that the type of the first execution has no impact on our data (because of the value of type)@@redux/INIT1.a.7.g.7.tThe data type of our own Redux will not be named like this, so it will not be repeated with it), that is, the default output state value

Hands on implementation of a Redux

  1. First of all, before implementing a Redux manually, let’s first see what methods of Redux are involved in the above code. First, we introduce the
import { createStore } from 'redux'
//Pass in the reducer
const store = createStore(reducer)

The createstore will return an object containing three methods, so we can list the rudiments of redux.

New mini- redux.js

export function createStore (reducer) {

    const getState = () => {}
    const subscribe = () => {}
    const dispatch = () => {}
    
    return { 
        getState, 
        subscribe, 
        dispatch 
    }
}
  1. store.getState () is used to get the state data. In fact, it simply returns the state parameter. therefore
export function createStore (reducer) {
    let currentState = {}
    const getState = () => currentState
    
    return { 
        getState
    }
}
  1. The dispatch method will receive an action and the execution will call itreducerReturns a new state
export function createStore (reducer) {
    let currentState = {}
    const getState = () => currentState
    const dispatch = (action) => {
        Currentstate = reducer (currentstate, action) // override the original state
    }
    return { 
        getState,
        dispatch
    }
}
  1. Set the listening function (set subscription) through subscribe. Once the state changes, this function will be executed automatically (notify all subscribers).

How to achieve it? We can directly use the subscribe function to add the events you want to listen to to to the array, and then execute the listener function of the listeners array when executing the dispatch method.

export function createStore (reducer) {
    let currentState = {}
    Let currentlisteners = [] // listener function. Multiple listeners can be added
    
    const getState = () => currentState
    const subscribe = (listener) => {
        currentListeners.push(listener)
    }
    const dispatch = (action) => {
        Currentstate = reducer (currentstate, action) // override the original state
        currentListeners.forEach(listener => listener())
    }
    return { 
        getState,
        subscribe,
        dispatch
    }
}

In the beginning, we opened the example of Redux, which is actually thestore.getState()After dispatching an action, the reducer returns the new state and executes the listening functionstore.getState()The value of state changes.

function listener () {
    const current = store.getState()
    console.log (` the current figures are:${ current.value >)
}
store.subscribe (listener) // listen for changes in state

The above code has nothing to do with react. It is just a Redux example. But think about it. When we use Redux and react together, we will do one more step.

 constructor(props) {
    super(props)
    this.state = store.getState()
    this.storeChange = this.storeChange.bind(this)
    store.subscribe(this.storeChange)
}

storeChange () {
    this.setState(store.getState())
}

The method of monitoring in react needs to usethis.setState()This is because the change of state in react must depend onthis.setStatemethod. Therefore, for the react project, the render method or the setstate method of the component is put into the listen (listening function) to realize the automatic rendering of the view and change the state value in the page.

Finally, notice that when initializing, dispatch will automatically execute once and continue to change the code

export function createStore (reducer) {
    let currentState = {}
    Let currentlisteners = [] // listener, which can listen to multiple events
    
    const getState = () => currentState

    const subscribe = (listener) => {
        currentListeners.push(listener)
    }

    const dispatch = (action) => {
        Currentstate = reducer (currentstate, action) // override the original state
        currentListeners.forEach(listener => listener())
    }
    //Try to write as complex as possible, so that it will not duplicate our custom action
    dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' })
    return { 
        getState, 
        subscribe, 
        dispatch 
    }
}

Here, we replace the file we wrote with the Redux introduced

import { createStore } from './mini-redux'

When we carried out it, we found that the results were not as good as we expected

{} {type: "@@mini-redux/~GSDG4%FDG#*&"}
The initial number is: undefined
{} {type: "INCREMENT"}
The current number is Nan
{type: "INCREMENT"}
The current number is Nan
{value: NaN} {type: "DECREMENT"}
The current number is Nan

What’s the matter with this? The default value of {state} is written by us from the initial value of {state}

const defaultState = {
    value: 10
}
function reducer (state = defaultState, action) {
    switch (action.type) {
        // ...
        default:
            return state
    }
}

However, in our implementation of Redux, we manually set it as an empty object. Here, our temporary solution is not to assign a value to it, and let it be undefined, so that the default parameters of the reducer will take effect. When Redux initializes the first dispatch, it will be automatically assigned to the default value of state (the default value of the ES6 function) of the first parameter passed in by the reducer, so it is modified as follows:

export function createStore (reducer) {
    let currentState 
    Let currentlisteners = [] // listener, which can listen to multiple events
    
    const getState = () => currentState

    const subscribe = (listener) => {
        currentListeners.push(listener)
    }

    const dispatch = (action) => {
        Currentstate = reducer (currentstate, action) // override the original state
        currentListeners.forEach(listener => listener())
    }
    dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' })
    return { 
        getState, 
        subscribe, 
        dispatch 
    }
}

This mini- redux.js , we can achieve the same output effect as the original redux.

Perfect Redux

Next, we will continue to add knowledge

  1. The createstore actually has three parameters, namely
createStore(reducer, [preloadedState], enhancer)

Second parameter[preloadedState] (any)Is optional: initial state

The third parameterenhancer(function)Also optional: for adding Middleware

In general, the state specified by preloadedstate has higher priority than the state specified by reducer. The existence of this mechanism allows us to specify the initial data by specifying the default parameters in the reducer, and also provides the possibility of injecting data into the store through the server or other mechanisms.

For the third parameter, we will say in the next part that we will continue to improve the code. We need to judge the second and third optional parameters.

export function createStore (reducer, preloadedState, enhancer) {

    //When the second parameter does not pass preloadedstate, but directly passes function, it will directly regard this function as enhancer
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState
        preloadedState = undefined
    }
    //When the third parameter is passed but not function, an error will be reported
    if (typeof enhancer !== 'undefined') {
        if (typeof enhancer !== 'function') {
            throw new Error('Expected the enhancer to be a function.')
        }
        return enhancer(createStore)(reducer, preloadedState)
    }
    //Reducer must be a function
    if (typeof reducer !== 'function') {
        throw new Error('Expected the reducer to be a function.')
    }

    Let currentstate = preloadedstate // the second parameter is not passed. By default, undefined is assigned to currentstate
    Let currentlisteners = [] // listener, which can listen to multiple events
    
    // ...
}

On the third parameter, determine why it is returned
return enhancer(createStore)(reducer, preloadedState)We will say in the next part that this one should be ignored first.

  1. We did it store.subscribe () method, but it is still incomplete. The subscribe method can add a listener function, listener. It also has a return value, which returns a function to remove the listener. In addition, we still need to judge the type.
export function createStore (reducer, preloadedState, enhancer) {
    // ...
    Let currentlisteners = [] // listener, which can listen to multiple events

    const subscribe = (listener) => {
        if (typeof listener !== 'function') {
            throw new Error('Expected listener to be a function.')
        }
        currentListeners.push(listener)
        //Through filter filtering, the array is removed from the event name that has been added to the array before execution
        return () => {
            currentListeners = currentListeners.filter(l => l !== listener);
        }
    }
    // ...
}

You can also remove the listener by looking for the array subscript

const subscribe = (listener) => {
    if (typeof listener !== 'function') {
        throw new Error('Expected listener to be a function.')
    }
    currentListeners.push(listener)
    //Through filter filtering, the array is removed from the event name that has been added to the array before execution
    return () => {
        let index = currentListeners.indexOf(listener)
        currentListeners.splice(index, 1)
    }
}

To remove a listener is actually to unsubscribe. The usage is as follows:

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

Unsubscribe(); // cancel listening
  1. After the diaptch method is executed, it returns to action, and then we also need to judge for it
export function createStore (reducer, preloadedState, enhancer) {
    // ...
    let isDispatching = false
    const dispatch = (action) => {
        //Used to determine whether an action is a normal object
        if (!isPlainObject(action)) {
            throw new Error('Actions must be plain objects. ')
        }
        //To prevent multiple dispatch requests from changing the status at the same time, it must be after the previous dispatch is finished that the next one is dispatched
        if (isDispatching) {
            throw new Error('Reducers may not dispatch actions.')
        }
    
        try {
            isDispatching = true
            Currentstate = reducer (currentstate, action) // override the original state
        } finally {
            isDispatching = false
        }
    
        currentListeners.forEach(listener => listener())
        return action
    }
}

//It is used to determine whether a value is a normal object (ordinary object, that is, the object created directly in literal form or by calling new object())
export function isPlainObject(obj) {
    if (typeof obj !== 'object' || obj === null) return false

    let proto = obj
    while (Object.getPrototypeOf(proto) !== null) {
        proto = Object.getPrototypeOf(proto)
    }

    return Object.getPrototypeOf(obj) === proto
}

// ...

In isplainobject function, it is continuously judged by while Object.getPrototypeOf (proto)! = = null and execute. Finally, proto will point to Object.prototype . then judge again Object.getPrototypeOf (obj) = = = proto. If true, it means that obj is created by literal or by calling new object().

The function of keeping the action object as a simple object is to make it convenient for the reducer to process, without dealing with other situations (such as function / class instances, etc.)

So far, we have implemented the most basic and usable Redux code. In the next part, we will continue to improve the Redux code, and finally release all the Redux code of the basic version:

export function createStore (reducer, preloadedState, enhancer) {

    //When the second parameter does not pass preloadedstate, but directly passes function, it will directly regard this function as enhancer
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState 
        preloadedState = undefined
    }
    //When the third parameter is passed but not function, an error will be reported
    if (typeof enhancer !== 'undefined') {
        if (typeof enhancer !== 'function') {
            throw new Error('Expected the enhancer to be a function.')
        }
        return enhancer(createStore)(reducer, preloadedState)
    }
    //Reducer must be a function
    if (typeof reducer !== 'function') {
        throw new Error('Expected the reducer to be a function.')
    }

    Let currentstate = preloadedstate // the second parameter is not passed. By default, undefined is assigned to currentstate
    Let currentlisteners = [] // listener, which can listen to multiple events
    let isDispatching = false
    
    const getState = () => currentState

    const subscribe = (listener) => {
        if (typeof listener !== 'function') {
            throw new Error('Expected listener to be a function.')
        }
        currentListeners.push(listener)
        //Through filter filtering, the array is removed from the event name that has been added to the array before execution
        return () => {
            currentListeners = currentListeners.filter(l => l !== listener);
        }
    }

    const dispatch = (action) => {
        //Used to determine whether an action is a normal object
        if (!isPlainObject(action)) {
            throw new Error('Actions must be plain objects. ')
        }
        //To prevent multiple dispatch requests from changing the status at the same time, it must be after the previous dispatch is finished that the next one is dispatched
        if (isDispatching) {
            throw new Error('Reducers may not dispatch actions.')
        }
    
        try {
            isDispatching = true
            Currentstate = reducer (currentstate, action) // override the original state
        } finally {
            isDispatching = false
        }

        currentListeners.forEach(listener => listener())
        return action
    }
    dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' })

    return { 
        getState, 
        subscribe, 
        dispatch 
    }
}

//It is used to determine whether a value is a normal object (ordinary object, that is, the object created directly in literal form or by calling new object())
export function isPlainObject(obj) {
    if (typeof obj !== 'object' || obj === null) return false

    let proto = obj
    while (Object.getPrototypeOf(proto) !== null) {
        proto = Object.getPrototypeOf(proto)
    }

    return Object.getPrototypeOf(obj) === proto
}

reference material:

Introduction to Redux (1): basic usage

  • PS: personal technology blog GitHub warehouse, if you think it’s good, welcome star, give me some encouragement~