Writing in the front

React Ecosystem Redux, React-Redux, redux-Saga, redux-Saga, redux-Saga, Redux-Saga, Redux-Saga, Redux-Saga, Redux-Saga, Redux-Saga, Redux-Saga This article briefly discusses the implementation principles of REdux, React-Redux, and Redux-Saga.

redux

This classic diagram describes the work flow of REdux. In simple terms, the View layer triggers an action to the Dispatcher, which will transmit aciton to reducer for calculation, and then transmit the new state to the View layer for state update.

Below is a simplified version of the createstore.js code

export function createStore(reducer, enhandler) {
    if (enhandler) {
        return enhandler(createStore)(reducer)
    }
    letstate = {}; // Where the global state is storedletobservers = []; // Observer queue // getterfunction getState() {
        return state;
    }
    // setter
    function dispatch(action) {
        state = reducer(state, action);
        observers.forEach(fn => fn());
    }
    function subscribe(fn) {
        observers.push(fn);
    }
    dispatch({ type: '@@REDUX_INIT'}) // Initialize state datareturn {getState, dispatch, subscribe}
}
Copy the code

As you can see, createStore is a higher-order function that receives reducer function and EnHandler function as parameters and returns getState, Dispatch, subscribe

Reducer is used to calculate new states based on the current state and incoming actions. Redux specifies that Reducer must be a pure function.

Enhandler is a middleware extension function that executes applyMiddleware to enhance Dispatch;

GetState simply returns state, the equivalent of a getter;

Dispatch is the only entry to modify state, acting as a setter;

Subscribe is a subscription to state, and when state changes, the corresponding callback function is triggered.

In simple terms, createStore stores global state as a closure variable, ensuring that changes cannot be read directly by outsiders, returns a handle to manipulate state, and provides listening for state changes.

In the Reducer section, an example evaluates the states according to different action types and returns the new states

import { ADD, SUB } from '.. /action/app';

const initialState = {
    count: 0
}

export const appReducer = function(state, action) {
    switch (action.type) {
        case ADD:
            return {
                ...state,
                count: state.count + action.text
            }
        case SUB:
            return {
                ...state,
                count: state.count - action.text
            }
        default: 
            returnstate || initialState; }}Copy the code

Normally we merge the Reducer and pass it to createStore

import {combineReducers} from '.. /myRedux';
import {appReducer} from './app';
import {compReducer} from './comp';

const rootReducer = combineReducers({
    app: appReducer,
    comp: compReducer
})

export default rootReducer;
Copy the code

CombineReducers code

export const combineReducers = (reducers) => {
    return (state = {}, action) => {
      return Object.keys(reducers).reduce((nextState, key) => {
          nextState[key] = reducers[key](state[key], action);
          returnnextState; }, {}); }; }Copy the code

As you can see, when rootReducer is passed into createStore, the resulting state structure is

{
    app: {},
    comp: {}
}
Copy the code

CombineReducers distribute state. For example, appReducer only passes in the data corresponding to the App key, achieving the effect of splitting the data and processing it separately.

Redux also provides extended functions. We know that this extension is implemented between Dispatch and Reducer, which is to expand other functions by enhancing dispatch. Below is a simple version of applyMiddleware

functioncompose(... fns) {if (fns.length === 0) return arg => arg    
    if (fns.length === 1) return fns[0]    
    returnfns.reduce((res, cur) =>(... args) => res(cur(... args))) }exportconst applyMiddleware = (... middlewares) => {return (createStore) => {
        return (reducer) => {
            const store = createStore(reducer)    
            let{ getState, dispatch } = store const params = { getState, dispatch: (action) => dispatch(action) } const middlewareArr = middlewares.map(middleware => middleware(params)) dispatch = compose(... middlewareArr)(dispatch)return { ...store, dispatch }
        }
    }
}
Copy the code

As you can see, applyMiddleware is a Currified function that corresponds to the call to EnHandler (createStore)(Reducer), with the two most important lines

const middlewareArr = middlewares.map(middleware => middleware(params)) dispatch = compose(... middlewareArr)(dispatch)Copy the code

Here, the incoming middleware is initialized one by one, and getState and Dispatch are inserted into the middleware to enable the middleware to access store. After the first call of the middleware is completed, the compose function is used to connect multiple middleware in series and pass the old Dispatch into the middleware for the second call. The enhanced Dispatch is eventually returned.

Take a look at an example of middleware, which is also a Coriolization function

export default function logger({ getState }) {
    return (next) => (action) => {
      let returnValue = next(action)
  
      console.log('state after dispatch', getState())
      
      return returnValue
    }
  }
Copy the code

In other words, applyMiddleware and middleware Logger are currified functions that take advantage of their delayed execution, calling them step by step, and finally

ApplyMiddleware (MIDDLEware1, Middleware2, middleware3) is processed and gets

Dispatch = MIDDLEware1 (Middleware2 (Middleware3 (Action))

In a word, the general idea of redux is to extract state to a unified place for management. State is set as read-only and can only be changed by reducer. The convention action is the object, reducer is a pure function, and does not interfere with the processing of asynchronous scenarios, only provides middler mechanism open out of the extended function. The realization principle, the code reflects the idea of functional programming, the use of high-order functions, functions and other skills, the code design is quite simple and clever.

react-redux

When redux is used with React, we need to manually subscribe the component to listen for state changes and update the component. To solve this problem, Redux provides the react-Redux library, which connects the state and react components through connect. To achieve the effect of automatic listening, the usage is as follows

index.js
<Provider store={store}>
    <React.StrictMode>
      <App />
      <Comp />
    </React.StrictMode>
</Provider>

app.js
class APP extends React.Component {  

    constructor(props) {    
        super(props)    
    }  

    handleAddItem = () => {
        const {dispatch} = this.props;
        dispatch({
            type: `${namespace}/ADD_ITEM`,
            text: 2
        })
    }

    handleDelItem = () => {
        const {dispatch} = this.props;
        dispatch({
            type: `${namespace}/DEL_ITEM`,
            text: 2
        })
    }

    render() {
        const {comp} = this.props;
        const {list} = comp;
        return (
            <div>
                <ul>
                    {
                        list.map(i => {
                            return <li>{i}</li>
                        })
                    }
                </ul>
                <button onClick={this.handleAddItem}>add li</button>
                <button onClick={this.handleDelItem}>del li</button>
            </div>
        )
    }
}

function mapStateToProps(state){
    return {
        comp: state.comp
    }
}

function mapDispatchToProps(dispatch){
    return {
        dispatch
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(APP);
  
Copy the code

Take a look at the implementation of provider.js and connect.js

The provider. Js simple version

import React from 'react'
import PropTypes from 'prop-types'

exportDefault class Provider extends react.ponent {// Static childContextTypes is declared to specify the properties of the context object childContextTypes = { store: PropTypes.object } constructor(props, context) { super(props, Context) this.store = props. Store} // Implement getChildContext, which returns the context objectgetChildContext() {    
        return{store: this.store}} // Render the component wrapped by the Providerrender() {    
        return this.props.children  
    }
}
Copy the code

As you can see, when using the React conetext function, you only need to install the Provider component at the top level. All other components can obtain state from Conetext, avoiding layer upon layer transfer of props

Take a look at a simple implementation of Connect

import React from 'react';
import PropTypes from 'prop-types';

export function connect(mapStateToProps, mapDispatchToProps) {    
    return function(Component) {      
        class Connect extends React.Component {        
            componentDidMount() {/ / get the store from the context and subscribe to update this. Context. The store. The subscribe (this) handleStoreChange) bind (this)); }handleStoreChange() {// forceUpdate () {// forceUpdate () {// forceUpdate ()setState to trigger child components to update this.forceupdate ()}render() {          
                const {store} = this.context;
                const {getState, dispatch} = store;
                return<Component // Pass in the props of this Component, need to pass back the original Component as is by the connect high-order Component {... This. Props} // Hang state on this. Props {... MapDispatchToProps (getState())} // Hang dispatch(action) on this. Props {// Hang dispatch(action) on this. MapDispatchToProps (dispatch)} />)}} connect. contextTypes = {store: proptypes.object}return Connect    
    }
}
Copy the code

Connect is a higher-order component that receives the mapStateToProps and mapDispatchToProps parameters, where the mapStateToProps is used to map a specific state to the component’s props. MapDispatchToProps maps DispatchToprops (Action) to props, and subscribe to Store at componentDidMount. All connected components are rendered once.

Summary: The essence of Provider is to use context for unified transmission. The essence of connect is to extract and reuse the logic of listening and obtaining state in a unified manner. This is also a common function of higher-order components.

redux-saga

Redux-saga is often referred to as Redux-Thunk, both of which are middleware for Redux and both handle asynchronous scenarios. Redux-thunk is very short, with only a dozen lines of code, and is simply implemented as follows

export default function thunk({ dispatch, getState }) {
  return (next) => (action) => {
    if (typeof action === 'function') {
      action(dispatch, getState())
    }
    next(action)
  }
}
Copy the code

Thunk supports function-form actions and sends the dispatch handle to function. We can call the action asynchronously and dispatch it when the result is returned

const addCountAction = (text) => {  
    return {
        type: ADD,
        text
    } 
}

const fetchData = (text) => (dispatch) => {
    new Promise((resolve) => {
        setTimeout(() => {
            resolve();
        }, 2000)
    }).then(() => {
        dispatch(addCountAction(text))
    })
}
Copy the code

It is assumed that the asynchronous result will be returned 2s later, followed by a call to Dispatch.

Redux-thunk redux-Thunk

Redux-thunk supports asynchronous scenarios, but its disadvantages are obvious:

1, the use of callback way to achieve asynchronous, easy to form layer upon layer of callback noodle code

2. Asynchronous logic is scattered in various actions, making it difficult to conduct unified management

As a result, redux-Saga has a more powerful asynchronous management scheme that can be used in place of Redux-Thunk.

Redux-saga:

Its main features

1. The implementation of generator is more in line with the style of synchronous code;

2, unified monitoring action, when the action is hit, the corresponding saga task will be executed, and support each saga to call each other, making asynchronous code more convenient and unified management.

In Saga, new concepts are introduced, where Effect refers to a generic JS object that describes a specified action and saga refers to a generator function.

First take a look at saga’s access:

//index.js
const sagaMiddleware = createSagaMiddleware();
store = createStore(rootReducer, applyMiddleware(sagaMiddleware, logger));

sagaMiddleware.run(rootSaga)

//rootSaga.js
const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) // Our worker Saga: Execute the increment task asynchronouslyfunction* addAsync(action) {
  yield fork(delay, 1000)
  yield put(formatAction(action, namespace))
}


export default function* rootSaga() {
  yield takeEvery(`${namespace}/ADD_ASYNC`, addAsync)
}

Copy the code

SagaMiddleware is generated by createSagaMiddleware and registered as the middleware of Redux. Then, the run method exposed by the middleware is called. The function of run is to initialize rootSaga uniformly and enable the monitoring of action. The simple version of createSagaMiddleware is as follows:

export default function sagaMiddlewareFactory() {
    let_store; // Closure store, which can be accessed later by sagaMiddlewarefunction sagaMiddleware(store) {
        _store = store;
        return(next) => (action) => { next(action); Channel.put (action)}} // Start rootSaga, i.e. Sagamiddleware.run =function(rootSaga) {
        let iterator = rootSaga();
        proc(iterator, _store);
    }
    return sagaMiddleware;
}
Copy the code

SagaMiddleware is a redux compliant middleware, and mounts the Run method in sagaMiddleware, which calls proc. See the implementation of proc

export default function proc(iterator, store) {
    next();
    function next(err, preValue) {
        let result;
        if (err) {
            result = iterator.throw(err);
        } else {
            result = iterator.next(preValue)
        }
        if (result.done) return result;

        if (isPromise(result.value)) { //yield promise
            let promise = result.value;
            promise.then((success) => next(null, success)).catch((err) => next(err, null))
        } else if (isEffect(result.value)) { //yield effect
            let effect = result.value;
            runEffect[effect.type](effect, next, store)
        } else { //yield others
            next(null, result.value)
        }
    }
}
Copy the code

As you can see, proc is a generator self-executor implemented recursively, indicating completion of generator execution when result.done is true. We assume that only yield, promise, effect, and ordinary syntax are handled separately. For example, when yield put(action) is invoked in Saga, it returns an effect of type PUT. Effect is a common JS object.

export function take(signal) {
    return {
        isEffect: true.type: 'take',
        signal
    }
}

export function put(action) {
    return {
        isEffect: true.type: 'put',
        action
    }
}

export function takeEvery(signal, saga) {
    return {
        isEffect: true.type: 'takeEvery',
        signal, 
        saga
    }
}

Copy the code

So, effect describes the type and parameters of the task, and effect is executed in the runEffect segment, namely runeffect.js:

function runTake({signal}, next, store) {
    channel.take({
        signal,
        callback: (args) => {next(null, args)}
    })
}

function runPut({action}, next, store) {
    const {dispatch} = store;
    dispatch(action);
    next();
}

functionrunTakeEvery({signal, saga, ... args}, next, store) {function *takeEveryGenerator() {
        while(true) {
            let action = yield take(signal);
            yield fork(saga, action);
        }
    }
    
    runFork({saga: takeEveryGenerator}, next, store);
}
Copy the code

RunEffect performs the corresponding effect, such as PUT, and you can see that it is essentially a wrapper around dispatch. Other auxiliary functions provided by Saga, such as takeEvery, are encapsulation of low-level effect.

So what does channel.take mean when we use takeEvery to listen on an action and call take to listen? Take a look at the channel implementation example:

function channel() {
    let _task = null;
    function take(task) {
        _task = task;
    }
    function put(action) {
        const {type. args} = action;if(! _task) {return;
        }
        _task.signal === type && _task.callback(action);
    }
    return {
        take, put
    }
}

export default channel();
Copy the code

As you can see, the implementation of a channel is a simple production consumer pattern, with take generating tasks and PUT consuming tasks. Here you can see why take blocks. When you take an action type, you are actually putting a task into a channel. Only when the action is dispatched is put consumption called. The saga iterator where take effect is located here will continue to be executed. Therefore, when take is executed, it is actually the iterator next that does not carry out the next iteration, resulting in saga blocking.

Conclusion: It can be seen from this simple model that Redux-Saga actually sets up an asynchronous processing layer between dispatch and Reducer to deal with asynchronous tasks. When sagaMiddleware initializes run, the saga of the entry executes itself and starts listening for the action. When a yield effect is encountered, the corresponding runEffect is executed, and when an action is hit, the corresponding saga task is derived. This is the general principle of Redux-saga.

So far, a simple analysis of the principles of REdux, React-Redux, and Redux-Saga has been completed. Not only can we learn excellent design ideas from them, but we can also know why they are used in business.

Demo address: github.com/lianxc/lear…

References:

Juejin. Cn/post / 684490…

www.jianshu.com/p/1608786c9…

Juejin. Cn/post / 684490…

Segmentfault.com/a/119000001…

Written by: first chong@ppmoney