Implementing a tinyredux from 0

Time:2021-6-24

Implementing a tinyredux from 0

Seriously,reduxHas been very small, remove the comments code is 300 lines, you can read, the comments are also very detailed.
Redux is more about the change of thinking: data change + view update. The two are separated and managed by themselves. Moreover, with Redux, you don’t need to deal with the communication between components, and you can de granularity components at will. In addition, because the state is managed to Redux, a large number of stateless components can be used. Stateless components have no instances to reduce memory, no life cycle, and the call stack is simple

Now, let’s start from scratch!!

so tiny !

Redux is such a process: trigger an action — Redux do some logic, return state — trigger listener. This is the event mechanism of graphical interface (addeventlistener on the web)!
So a minimum Redux:

class Store {
    constructor(reducer, state = {}) {
        this.state = state
        this.listeners = []
        this.reducer = reducer
    }

    dispatch(action) {
        this.state = this.reducer(this.state, action)
        this.listeners.forEach(listener => listener())
    }

    getState() {
        return this.state
    }

    subscribe(listener) {
        this.listeners.push(listener)
    }
}

Our store and Redux store provide the following APIs:

  1. Dispatch triggers an action
  2. Getstate returns the current state
  3. Add a listener to subscribe

Let’s use this minimal example to implement a counterOnline address

function reducer(state, action) {
   switch (action.type) {
       case 'addOne': {
           return {
               ...state,
               count: state.count + 1
           }
       }
       default: {
           return state
       }
   }
}

const store = new Store(reducer, {count: 0})

store.subscribe(() => {
    console.log('subscribe test:', store.getState())
})

store.dispatch({type: 'addOne'})
store.dispatch({type: 'addOne'})

Another soul Middleware

ReduxChinese documentOnmiddlewareI’ve already talked about the part of. Now let’s look at this from another perspective,
Firstly, middleware is an extension mechanism provided by Redux before and after dispatch. For example, the logging function needs to record the status before an action in dispath, and then record it again after the logic is processed by the reducer. That’s what it isAspect oriented programmingOh!
FashionableAOP! With Java, whether it’s a static proxy or a dynamic proxy, it’s very complicated to write. But JS implementation is very simple

function enhancer(originF) {
  return function(...args) {
    console.log('before')
    var result = originF(...args)
    console.log('after')
    return result
  }
}

The enhancer method accepts a method a and returns an enhanced method B. For B, we can enhance it again, so it can be called in chain

var fEnhancer = function (originF) {
    return function (...args) {
        console.log('this is fEnhancer before')
        var r = originF(...args)
        console.log('this is fEnhancer after')
        return r
    }
}

var hEnhancer = function (originF) {
    return function (...args) {
        console.log('this is hEnhancer before')
        var r = originF(...args)
        console.log('this is hEnhancer after')
        return r
    }
}

var gEnhancer = function (originF) {
    return function (...args) {
        console.log('this is gEnhancer before')
        var r = originF(...args)
        console.log('this is gEnhancer after')
        return r
    }
}

function justPrint() {
    console.log('justPrint...')
}

fEnhancer(hEnhancer(gEnhancer(justPrint)))()

This example outputs [online address] ():

this is fEnhancer before
this is hEnhancer before
this is gEnhancer before
justPrint...
this is gEnhancer after
this is hEnhancer after
this is fEnhancer after

For fenhancer (henhancer (genhancer (justprint)), the equivalent writing method is as follows:

var enhancerArray = [gEnhancer, hEnhancer, fEnhancer]
function enhancerFun(originF) {
    let of = originF
    enhancerArray.forEach(enhancer => {
        of = enhancer(of)
    })
    return of
}

The more corrupt writing method is the implementation of Redux (cleverly using the reduce method of array)

var enhancerArray = [gEnhancer, hEnhancer, fEnhancer]
function enhancerFun2(originF) {
    return enhancerArray.reduce((a, b) => (...args) => a(b(...args)))(originF)
}

Back to Redux, we need to enhance the dispatch, so we only need enhancerfun (store. Dispatch). Here are two questions:
The first problem is that this is used in our dispatch, and this enhanced call: VAR r = originf(), this is lost here. The solution is as follows:

class Store {
    constructor(reducer, state) {
        this.state = state
        this.listeners = []
        this.reducer = reducer

        this.dispatch = this.dispatch.bind(this)
        this.getState = this.getState.bind(this)
        this.subscribe = this.subscribe.bind(this)
    }
    ...
}

In this way, there is no problem calling the store method anywhere

Second question: in gnenhancer, we want to call store. Getstate() to record the state before and after the dispatch call. What should we do( We can’t go to the import store every time, because when we write enhancer,
Maybe I don’t know where the store is at all.) The method is as follows

var fEnhancer = function ({ getState, dispatch }) {
    return function (originF) {
        return function (...args) {
            console.log('this is fEnhancer before', getState())
            var r = originF(...args)
            console.log('this is fEnhancer after', getState())
            return r
        }
    }
}

In the form of closure, we let the logic inside fenhancer use getstate directly.

What is middleware? Here, fenhancer is a standard Redux middleware. Yes, Redux logger is not needed. Let’s use fenhancer. Corresponding applymiddleware:

function applyMiddleware(store, ...args) {
    console.log(args)
    const enArr = args.map(middleware => middleware({
        getState: store.getState,
        dispatch: store.dispatch
    }))


    let of = store.dispatch
    enArr.forEach(en => {
        of = en(of)
    })

    store.dispatch = of
}

Now, let’s enhance the reducer at the beginning!!Online address

auxiliary function

By this point, tineyredux is actually over. But for the convenience of developers, Redux provides two auxiliary functions: combine reducers and bind action creators.
Bindactioncreators is to dispatch for you by default when you originally call actioncreator: actioncreator () = > store. Dispatch (actioncreator ()).
It can also be understood as’ enhancement ‘:

function bindActionCreator(creator, dispatch) {
    return function (...args) {
        Dispatch (creator (args)) // < --- can also be understood as' enhanced '
    }
}

export default function bindActionCreators(creators, dispatch) {
    const keys = Object.keys(creators)
    const result = {}
    keys.forEach(key => {
        result[key] = bindActionCreator(creators[key], dispatch)
    })
    return result
}

Combine reducers is to solve other pain points, such as the following stores and reducers:

{
    clock: {
        count: 0
    },
    yk: {
        age: 0
    }
    ...
}

function reducer(state, action) {
    switch (action.type) {
        case 'clock_add':...
        case 'clock_cnum'...
        case 'yk_older': ...
        case 'yk_forever18': ...
        default: {
            return state
        }
    }
}

In most cases, we find that our application, clock data part, corresponds to clock’s own logic, and the modification logic of YK data part only cares about itself (usually it’s the data of two pages).
So here’s one“Big switch”It can be segmented.

function clockReducer(state, action) {
    switch (action.type) {
        case 'clock_addOne': ...
        case 'clock_cnum': ...
        default: {
            return state
        }
    }
}

function ykReducer(state, action) {
    switch (action.type) {
        case 'yk_older': ...
        case 'yk_forever18': ...
        default: {
            return state
        }
    }
}

function reducer(state, action) {
  return {
      clock: clockReducer(state, action),
      yk: ykReducer(state, action),
  }
}

Combine reducers is to merge small reducers

function combineReducers(reducers) {
    return function (state, action) {
        const keys = Object.keys(reducers)

        const newState = {}
        keys.forEach(key => {
            newState[key] = reducers[key](state[key], action)
        })
        return newState
    }
}

Digression: if there are too many small reducers, there will be some performance problems: for every action, all reducers are gone. If we have a special scene,
As we just said, the logic of a piece of data can only be used for one reducer. The following variants can be used (only one reducer will be executed, and the prefix of action should be consistent with the key in the store)

function combineReducersVariant(reducers) {
    return function (state, action) {
        const lineIndex = action.type.indexOf("_")
        const actionKey = action.type.substring(0, lineIndex)
        const newS = reducers[actionKey](state[actionKey], action)

        return state[actionKey] === newS ? state : {
            ...state,
            [actionKey]: newS
        }
    }
}

Here is a complete function to protect all features of middleware, bind action creators and combine reducersComplete example

The code is hosted in GIT

Installation: NPM install tiny Redux — save

Related articles

  1. Implementing a tiny react from 0 (1)
  2. Implement react router from 0
  3. Implementing a tiny react Redux from 0
  4. Why do we need reselect