Write a redux, react redux and the extension of Middleware (asynchronous thunk, printing logger)

The beggar version of the manual roll up is as follows. It only realizes the most basic functions: subscription, dispatch and access. However, it can be used easily. It's no problem to make a counter, and the core functions of redux can be seen more simply and roughly

Note: the length of this article is long. It is recommended to read it slowly after collection. You can also directly pull it down from github and try it yourself~ github portal

--------------------------------------------------------------Start of text-------------------------------------------------------------
The simple version of redux, the real redux source code is at the end of the article, the source code is long, nearly 300 lines, less than 100 lines are removed from the comments and compatible development version, and more scenarios are processed. The redux I wrote has only realized its core functions, and I don't care about too many compatibility issues. However, the code is a little less, and everyone will accept it a little more. I hope you can understand it.

const REDUX_INIT_TYPE = "@@REDUX_INIT_TYPE"
export function createStore(reducer, preloadedState, enhancer) {
	/**
	 * Why exchange?
	 * Look at the name. You can see that the second parameter preloadedState represents the default State. If it is not passed, it is empty,
	 * This is the default value of state, which is useful during initialization, that is, the first corresponding reducer in the dispatch
	 *
	 * In general, the reducer will have the default option, which will return the default state. When defining the reducer, a default value will be passed to the state. As a result, if the preloadedState is not passed, the init state set by the user will be read by default
	 *
	 * When preloadedState is passed but enhancer is not passed, and preloadedState is a method, it means that the user may have passed the wrong way, then the second parameter should be moved to the third parameter, and preloadedState should be set to undefined
	 * Otherwise, when initializing state, it cannot be initialized normally (the parameter has a default value, but when the received value is not undefined, it will not be set to the default value automatically!
	 */
	if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
		enhancer = preloadedState;
		preloadedState = undefined;
	}

	// If the reinforcer is a method, return the reinforcer enhanced createStore directly to perform operations on the reducer, that is, to wrap it up
	if (typeof enhancer === "function") {
		return enhancer(createStore)(reducer);
	}

	let currentState = preloadedState;
	const currentListenerList = [];

	function getState() {
		return currentState;
	}

	/**
	 * Dispatch the operation to let the reducer perform the operation to update the state
	 * Then call all listeners in turn
	 */
	function dispatch(action) {
        currentState = reducer(currentState, action);
		currentListenerList.forEach((listener) => listener());
		return action;
	}

	// Subscription listener
	function subscribe(listener) {
		currentListenerList.push(listener);
	}

	// Initialize currentState, because the type cannot find the corresponding operation in reducer, so the default value of walk
	dispatch({
		type: REDUX_INIT_TYPE,
	});

	return {
		getState,
		dispatch,
		subscribe,
	};
}

Written here, redux can be used, but it can only be synchronized, which is not supported for setTimeout.

// Application middleware
export function applyMiddleware(...middleWares) {
	return (createStore) => (...args) => {
		// Create a new store according to the parameters passed in
		let store = createStore(...args);

		// Save the original dispatch first, and then you need to reference it
		let oldDispatch = store.dispatch;

		/**
         * Execute all the middleware, and then return a new array,
         * After middleware execution, it will return a function and accept dispatch as a parameter
         * Such as thunk's source code
         * function createThunkMiddleware(extraArgument) {
         *      return ({ dispatch, getState }) => next => action => {
         *          if (typeof action === 'function') {
         *              return action(dispatch, getState, extraArgument);
         *          }
         *          return next(action);
         *      };
         * }
         * export default createThunkMiddleware();
         * What we're actually returning is
         * ({ dispatch, getState }) => next => action => {
         *      if (typeof action === 'function') {
         *          return action(dispatch, getState);
         *      }
         *      return next(action);
         * };
         * It takes an object parameter, {dispatch, getState}
         * Return a function. Next is a parameter. This next is actually a dispatch. It also takes an action as a parameter and returns a result
         * next => action => {
         *      if (typeof action === 'function') {
         *          return action(dispatch, getState);
         *      }
         *      return next(action);
         * }
         * Compare with the dispatch written before
         * function dispatch(action) {
         *      currentState = reducer(currentState, action);
         *      return action;
         * }
         * The basic shape is the same, but it is a reinforcement of dispatch,
         * If the action accepted by the dispatch is a function, execute the function first, and then return the result. Otherwise, directly return the result after execution
         * This is the reason why dispatch returns an action in the first place, which is used to transfer the action to the next middleware, that is, all middleware executed this time use the same action
         * 
         * Each middleware accepts a dispatch as a parameter. After execution, it returns an action as the next middleware dispatch action
         * 
         * So create a common data first
         * The dispatch here is the original one
         */
        const midData = {
            getState: store.getState,
            dispatch: (...args) => oldDispatch(...args)
        }
        let newMiddleWares = middleWares.map(mw => mw(midData));

        // Then use middleware to strengthen the dispath of the store
        let newDispatch = compose(...newMiddleWares)(store.dispatch);

        return {
            ...store,
            dispatch: newDispatch
        }
	};
}

After writing applyMiddleWare, you can support third-party middleware, such as asynchronous operation (setTimeout, ajax, etc.), printing state data, etc

/**
 * Sequence execution middleware, which is composed of [a, b] = > A (b (... Args));
 * This method is simply too cool. I don't think of using this method to make function recursive calls without looking at the source code.
 * I'm still not familiar with array method!
 */
export function compose(...funcs) {
	return funcs.reduce((prevFn, currentFn) => (...args) => currentFn(prevFn(...args)));
}

The real Redux logger is cool. It beautifies console.log. You can jump to specific beautification core.js of Redux logger
I also wrote a simple use of console.log to refer to the north. If you are interested, you can go and have a look Portal

export const fake_logger = ({dispatch, getState}) => nextDispatch => action => {
	console.log(action.type + "Executed!!!");
	console.log(getState())
	return nextDispatch(action);
}
export const fake_thunk = ({dispatch, getState}) => nextDispatch => action => {
	if(typeof action === "function") {
		return action(dispatch, getState)
	}
	return nextDispatch(action);
}

Now that I have finished the Redux part, I can run it "Perfectly" in my own examples. Have a good time, but it's very troublesome to use it in react. Every component needs to call this.forceUpdate() manually after the state changes to force the component to refresh. It's very inconvenient, so with react Redux, react Redux supports Redux well in react Use of. Let's start with react redux.

Let's see how to use it first

// Parent component
const App = () => {
  return (
    <Provider store={store}>
      <RootRouter />
    </Provider>
  );
}
// Subassemblies
const Wrapper = ({ counter }) => {
	return (
		<p>{counter.xxx}</p>
	)
}
export default connect(({ counter }) => ({
    counter
}), (dispath) => ({
    add() {
        dispath({
        	type: "add"
		})
    }
}))(Wrapper);

When I saw Provider, I thought of context at the first time. Unfamiliar students can go to see the article I wrote about context Portal , and then I wrote a version according to my own guess. The code is as follows

import React, { createContext, Component } from "react";

const { Provider, Consumer } = createContext(null);

export class SProvider extends Component {
	constructor(props) {
		super(props);
	}

	componentDidMount() {
		this.props.store.subscribe(() => {
			this.forceUpdate();
		});
	}

	render() {
		/**
		 * At the beginning, I directly passed this.props.store to value, but it will cause the component not to refresh. I think that when react makes shallow comparison, it thinks that the methods are the same, because the state is obtained through the getState method
		 * So I put forward the state and dispatch, so that when I compare them, the state will change (and it turns out to be successful.)
		 * I haven't seen the source code of react Redux yet. If there is any error, please point it out, leave a message to me and say, "please spray it lightly."
		 */
		return <Provider value={{
            state: this.props.store.getState(),
            dispatch: this.props.store.dispatch
        }}>{this.props.children}</Provider>;
	}
}

/**
 * connect It is a high-order function. It encapsulates the received components once and twice, and executes the parameters passed in by the user once with redux, and then passes them to the components in the form of props
 * @params {function} getStateFn Accept state as a parameter and return a user-defined object
 * @params {function | object} dispatchs It may be an object with key: action inside
 * function Accept dispatch as a parameter and return a user-defined object
 * Key: () = > dispatch (action) will be returned;
 */
export function connect(getStateFn, dispatchs) {
	return (PropsComponent) => (props) => (
		<Consumer>
			{(store) => {
                let newDispatchs = {};

                // If an object is passed in, each element of the object needs to be wrapped with a dispatch
                if(typeof dispatchs === "object") {
                    Object.keys(dispatchs).forEach((item) => {
                        newDispatchs[item] = () => store.dispatch(dispatchs[item])
                    })
                } else if(typeof dispatchs === "function") {
                    newDispatchs = dispatchs(store.dispatch);
                }

				return <PropsComponent {...getStateFn(store.state)} {...newDispatchs} {...props} />;
			}}
		</Consumer>
	);
}

Anyway, it can be run. I'll see the source code of react Redux later. If there is any discrepancy, I'll change it again. I believe in my judgment! Do it! go for it!

Everyone can see that this sentence is really not easy, can you give me a compliment ~ thank you very much!!!

The real source code starts with the translation of the source code annotation. If you are interested in it, you can have a look. Or go to github to download his source package directly Portal

import $$observable from 'symbol-observable'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'


/**
* Creates a Redux store that holds the state tree.
* Create a state management container to store the state tree
* The only way to change the data in the store is to call `dispatch()` on it.
* There is only one way to update the state in this container, that is, to call dispatch to notify him
*
* There should only be a single store in your app. To specify how different
* parts of the state tree respond to actions, you may combine several reducers
* into a single reducer function by using `combineReducers`.
*
* @param {Function} reducer A function that returns the next state tree, given
* the current state tree and the action to handle.
* Given the current state tree and the operation to be processed, returns the function of the next state tree.
*
* @param {any} [preloadedState] The initial state. You may optionally specify it
* to hydrate the state from the server in universal apps, or to restore a
* previously serialized user session.
* If you use `combineReducers` to produce the root reducer function, this must be
* an object with the same shape as `combineReducers` keys.
*
* Initial state. You can choose to specify it in the universal apps to get the state from the server, or recover the previously serialized user session. If you use 'combinedreducers' to generate a root restore function, the object must have the same shape as the' combinedreducers' key.
*
* @param {Function} [enhancer] The store enhancer. You may optionally specify it
* to enhance the store with third-party capabilities such as middleware,
* time travel, persistence, etc. The only store enhancer that ships with Redux
* is `applyMiddleware()`.
* enhancer Is the meaning of the intensifier
* Storage enhancer. You can optionally specify it to enhance storage with third-party capabilities such as middleware, time travel, persistence, and so on. The only storage enhancer included with Redux is "applyMiddleware()".
*
* @returns {Store} A Redux store that lets you read the state, dispatch actions
* and subscribe to changes.
*/
export default function createStore(reducer, preloadedState, enhancer) {
 if (
   (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
   (typeof enhancer === 'function' && typeof arguments[3] === 'function')
 ) {
   // It looks like you are passing several store enhancers to createStore()
   // This is not supported
   // Put them together to make a simple 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.'
   )
 }

 // When the second parameter preloadedState is a method and there is no third parameter, exchange the second parameter with the third parameter
 /**
  * Why exchange?
  * Look at the name. You can see that the second parameter preloadedState represents the default State. If it is not passed, it is empty,
  * This is the default value of state, which is useful during initialization, that is, the first corresponding reducer in the dispatch
  *
  * In general, the reducer will have the default option, which will return the default state. When defining the reducer, a default value will be passed to the state. As a result, if the preloadedState is not passed, the init state set by the user will be read by default
  *
  * When preloadedState is passed but enhancer is not passed, and preloadedState is a method, it means that the user may have passed the wrong way, then the second parameter should be moved to the third parameter, and preloadedState should be set to undefined
  * Otherwise, when initializing state, it cannot be initialized normally (the parameter has a default value, but when the received value is not undefined, it will not be set to the default value automatically!
  */
 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
   enhancer = preloadedState
   preloadedState = undefined
 }

 /**
  * When enhancer is not empty, combining the above code, it can be concluded that if there is one of the following two parameters, the current statement will be executed
  * So that is to say, if the second parameter is method, it will become the third parameter by default
  * enhancer It should be a high-level component, using middleware to encapsulate createStore once
 **/
 if (typeof enhancer !== 'undefined') {
   if (typeof enhancer !== 'function') {
     throw new Error('Expected the enhancer to be a function.')
   }
   return enhancer(createStore)(reducer, preloadedState)
 }

 if (typeof reducer !== 'function') {
   // The expected reducer should be a function
   throw new Error('Expected the reducer to be a function.')
 }

 /**
  * If preloadedState is not a function, but a value or object or something, it will come here
 **/
 let currentReducer = reducer
 let currentState = preloadedState
 let currentListeners = []
 let nextListeners = currentListeners
 let isDispatching = false


 /**
  * This makes a shallow copy of currentListeners so we can use
  * nextListeners as a temporary list while dispatching.
  * This creates a shallow copy of the currentListener, so we can use the nextListener as a temporary list at dispatch time
  * This prevents any bugs around consumers calling
  * subscribe/unsubscribe in the middle of a dispatch.
  */
 function ensureCanMutateNextListeners() {
   if (nextListeners === currentListeners) {
     nextListeners = currentListeners.slice()
   }
 }


 /**
  * Reads the state tree managed by the store.
  *
  * @returns {any} The current state tree of your application.
  */
 function getState() {
   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 currentState
 }


 /**
  * Adds a change listener. It will be called any time an action is dispatched,
  * and some part of the state tree may potentially have changed. You may then
  * call `getState()` to read the current state tree inside the callback.
  * Add a change listener.
  * It is called at any time when the operation is dispatched, and some parts of the state tree may have changed.
  * You can then call 'getState()' to read the current state tree in the callback function.
  * 
  * You may call `dispatch()` from a change listener, with the following
  * caveats:
  *
  * 1. The subscriptions are snapshotted just before every `dispatch()` call.
  * If you subscribe or unsubscribe while the listeners are being invoked, this
  * will not have any effect on the `dispatch()` that is currently in progress.
  * However, the next `dispatch()` call, whether nested or not, will use a more
  * recent snapshot of the subscription list.
  *
  * 2. The listener should not expect to see all state changes, as the state
  * might have been updated multiple times during a nested `dispatch()` before
  * the listener is called. It is, however, guaranteed that all subscribers
  * registered before the `dispatch()` started will be called with the latest
  * state by the time it exits.
  *
  * @param {Function} listener A callback to be invoked on every dispatch.
  * @returns {Function} A function to remove this change listener.
  */
 function subscribe(listener) {
   if (typeof listener !== 'function') {
     throw new Error('Expected the listener to be a function.')
   }


   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-reference/store#subscribelistener for more details.'
     )
   }


   let isSubscribed = true


   ensureCanMutateNextListeners()
   nextListeners.push(listener)


   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-reference/store#subscribelistener for more details.'
       )
     }


     isSubscribed = false


     ensureCanMutateNextListeners()
     const index = nextListeners.indexOf(listener)
     nextListeners.splice(index, 1)
     currentListeners = null
   }
 }


 /**
  * Dispatches an action. It is the only way to trigger a state change.
  * Assign an action. This is the only way to trigger a state change.
  *
  * The `reducer` function, used to create the store, will be called with the
  * current state tree and the given `action`. Its return value will
  * be considered the **next** state of the tree, and the change listeners
  * will be notified.
  * The 'reduce' function used to create the store will be called by the current state tree and the given 'action'.
  * Its return value will be considered as the * * next * * state of the tree and will notify the change listener.
  *
  * The base implementation only supports plain object actions. If you want to
  * dispatch a Promise, an Observable, a thunk, or something else, you need to
  * wrap your store creating function into the corresponding middleware. For
  * example, see the documentation for the `redux-thunk` package. Even the
  * middleware will eventually dispatch plain object actions using this method.
  * The basic implementation only supports normal object operations.
  * If you want to assign a Promise, an Observable, a thunk or something, you need to encapsulate the store creation function into the corresponding middleware.
  * For example, see the documentation for the "Redux thunk" package. Even middleware will eventually use this method to schedule common object operations.
  *
  * @param {Object} action A plain object representing "what changed". It is
  * a good idea to keep actions serializable so you can record and replay user
  * sessions, or use the time travelling `redux-devtools`. An action must have
  * a `type` property which may not be `undefined`. It is a good idea to use
  * string constants for action types.
  *
  * @returns {Object} For convenience, the same action object you dispatched.
  *
  * Note that, if you use a custom middleware, it may wrap `dispatch()` to
  * return something else (for example, a Promise you can await).
  * According to the incoming action, call currentReducer to modify currentState, and call all the listening functions in the current listening queue in turn to return the incoming action
  */
 function dispatch(action) {
   if (!isPlainObject(action)) {
     throw new Error(
       'Actions must be plain objects. ' +
         'Use custom middleware for async actions.'
     )
   }


   if (typeof action.type === 'undefined') {
     throw new Error(
       'Actions may not have an undefined "type" property. ' +
         'Have you misspelled a constant?'
     )
   }


   if (isDispatching) {
     throw new Error('Reducers may not dispatch actions.')
   }


   try {
     isDispatching = true
     currentState = currentReducer(currentState, action)
   } finally {
     isDispatching = false
   }


   const listeners = (currentListeners = nextListeners)
   for (let i = 0; i < listeners.length; i++) {
     const listener = listeners[i]
     listener()
   }


   return action
 }


 /**
  * Replaces the reducer currently used by the store to calculate the state.
  * Replace the reducer currently used by the store to calculate the state.
  *
  * You might need this if your app implements code splitting and you want to
  * load some of the reducers dynamically. You might also need this if you
  * implement a hot reloading mechanism for Redux.
  * If your application implements code segmentation and you want to load some reducer s dynamically, you may need this. If you implement the hot overload mechanism for Redux, you may need this as well.
  *
  * @param {Function} nextReducer The reducer for the store to use instead.
  * @returns {void}
  */
 function replaceReducer(nextReducer) {
   if (typeof nextReducer !== 'function') {
     throw new Error('Expected the nextReducer to be a function.')
   }


   currentReducer = nextReducer


   // This action has a similiar effect to ActionTypes.INIT.
   // Any reducers that existed in both the new and old rootReducer
   // will receive the previous state. This effectively populates
   // the new state tree with any relevant data from the old one.
   dispatch({ type: ActionTypes.REPLACE })
 }


 /**
  * Interoperability point for observable/reactive libraries.
  * Point of interoperability of observable / reactive libraries.
  *
  * @returns {observable} A minimal observable of state changes.
  * For more information, see the observable proposal:
  * https://github.com/tc39/proposal-observable
  */
 function observable() {
   const outerSubscribe = subscribe
   return {
     /**
      * The minimal observable subscription method.
      * @param {Object} observer Any object that can be used as an observer.
      * The observer object should have a `next` method.
      * @returns {subscription} An object with an `unsubscribe` method that can
      * be used to unsubscribe the observable from the store, and prevent further
      * emission of values from the observable.
      */
     subscribe(observer) {
       if (typeof observer !== 'object' || observer === null) {
         throw new TypeError('Expected the observer to be an object.')
       }


       function observeState() {
         if (observer.next) {
           observer.next(getState())
         }
       }


       observeState()
       const unsubscribe = outerSubscribe(observeState)
       return { unsubscribe }
     },


     [$$observable]() {
       return this
     }
   }
 }


 // 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 })


 return {
   dispatch,
   subscribe,
   getState,
   replaceReducer,
   [$$observable]: observable
 }
}

Tags: React github less Session

Posted on Wed, 13 May 2020 22:28:54 -0700 by mady