redux&react-redux源码分析

181 阅读8分钟

截屏2021-03-25 15.51.34.png

Redux

1.redux解决什么问题

  • redux 出现为了解决state里的数据问题
  • react中,数据在组件中是单向流动的
  • 数据从一个方向父组件流向子组件(通过props),由于这个特征,非父子关系的组件(或者兄弟组件)之间的通信比较麻烦

2.redux设计思想

  • redux是将整个应用状态存储到一个地方,称为store
  • 里面保存一棵状态树state tree
  • 组件可以派发dispatch行为actionstore,而不是直接通知其他组件
  • 其他组件可以通过订阅store中的状态(state)来刷新自己的视图

redux思想图

3.redux三大原则

  • 整个应用中state被存储在一棵object tree中,并且这个object tree只存储唯一一个store
  • state是只读的,唯一改变state的方法就是触发actionaction是一个用于描述已发生事件的普通对象,使用纯函数来执行修改,为了描述action如何改变state tree,你需要编写reducers
  • 单一数据源的设计让react的组件之间通信更加方便,同时也便于状态的统一管理

4.原生计算器

原生组件使用

4.1.public/index.html

<!--
 * @Author: dfh
 * @Date: 2021-03-07 13:46:20
 * @LastEditors: dfh
 * @LastEditTime: 2021-03-07 14:16:29
 * @Modified By: dfh
 * @FilePath: /day27-redux/public/index.html
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta name="description" content="Web site created using create-react-app" />
  <title>React App</title>
</head>

<body>
  <div id="root"></div>
  <div id="counter">
    <p id="counter-value"></p>
    <button id="add-btn">Add +1</button>
    <button id="minus-btn">Minus -1</button>
  </div>
</body>

</html>

4.2.src/index.js

import { createStore } from './redux';
//获取原生组件
const counterValue = document.getElementById('counter-value');
const addBtn = document.getElementById('add-btn');
const minusBtn = document.getElementById('minus-btn');

//定义操作常量
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
//初始值
const initialState = { num: 0 };
//定义reducer
const reudcer = (state = initialState, action) => {
    switch (action.type) {
        case INCREMENT:
            return { num: state.num + 1 };
        case DECREMENT:
            return { num: state.num - 1 }
        default:
            return state;
    }
}

//创建store
const store = createStore(reudcer, { num: 1 })

function render() {
    counterValue.innerHTML = store.getState().num + '';
}

//订阅reducer
store.subscribe(render);

render();

//绑定事件
addBtn.addEventListener('click', () => {
    store.dispatch({ type: INCREMENT });//派发事件
})

minusBtn.addEventListener('click', () => {
    store.dispatch({ type: DECREMENT });
})

4.3.redux/createStore

/**
 * 
 * @param {'*'} reducer 处理器
 * @param {*} preloadedState 仓库的初始状态
 */
function creatStore(reducer, preloadedState) {
    //定义一个状态变量,并赋值上默认值
    let state = preloadedState;
    const listeners = [];

    function getState() {//获取状态
        return state;
    }

    /**
     * 订阅方法,返回一个取消订阅函数
     * @param {*} listener 订阅事件
     */
    function subscribe(listener) {
        listeners.push(listener);//订阅
        return () => {//用于销毁时
            const idx = listeners.indexOf(listener);
            listeners.splice(idx, 1);
        }
    }

    /**
     * 派发
     * @param {*} action 派发的动作
     */
    function dispatch(action) {
        //执行执行器,获取新的状态
        state = reducer(state, action);
        //状态改变通知
        listeners.forEach(listener => listener());
        //react ssr时用的上返回值
        return action;
    }

    //在创建仓库的时候,会先派发一次action,会让reducer设置的默认值生效
    dispatch({ type: '@@REDUX/INIT' });
    const store = {
        getState,
        subscribe,
        dispatch,
    }
    return store;
}
export default creatStore;

4.4.redux/index.js

export { default as createStore } from './createStore';

5.react中使用

react中使用

5.1.src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Counter from './components/Counter'

ReactDOM.render(<Counter />, document.getElementById('root'));

5.2.components/Counter.js

import React from 'react';
import { createStore } from '../redux';

//定义操作常量
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

//初始值
const initialState = { num: 0 };

//定义reducer
const reudcer = (state = initialState, action) => {
    switch (action.type) {
        case INCREMENT:
            return { num: state.num + 1 };
        case DECREMENT:
            return { num: state.num - 1 }
        default:
            return state;
    }
}

//创建store
const store = createStore(reudcer, { num: 0 });

function Counter() {
    const [num, setNum] = React.useState(0);

    React.useEffect(() => {
        //订阅
        const unsubscribe = store.subscribe(() => setNum(store.getState().num))
        return () => {//销毁
            unsubscribe();
        }
    }, [])

    return <div>
        <p>{num}</p>
        <button onClick={() => store.dispatch({ type: INCREMENT })}>add +1</button>
        <button onClick={() => store.dispatch({ type: DECREMENT })}>minus -1</button>
    </div>
}

export default Counter;

6.bindActionCreators

bindActionCreators

6.1.components/Counter.js

  import React from 'react';
+ import { createStore, bindActionCreators } from '../redux';

//定义操作常量
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

//初始值
const initialState = { num: 0 };

//定义reducer
const reudcer = (state = initialState, action) => {
    switch (action.type) {
        case INCREMENT:
            return { num: state.num + 1 };
        case DECREMENT:
            return { num: state.num - 1 }
        default:
            return state;
    }
}

//创建store
const store = createStore(reudcer, { num: 0 });

+ function add() {
+    return { type: INCREMENT };
+ }

+ function minus() {
+    return { type: DECREMENT };
+ }

+ const actions = { add, minus };

+ const boundActions = bindActionCreators(actions, store.dispatch);

function Counter() {
    const [num, setNum] = React.useState(0);

    React.useEffect(() => {
        //订阅
        const unsubscribe = store.subscribe(() => setNum(store.getState().num))
        return () => {//销毁
            unsubscribe();
        }
    }, [])

    return <div>
        <p>{num}</p>
+       <button onClick={boundActions.add}>add +1</button>
+       <button onClick={boundActions.minus}>minus -1</button>
    </div>
}

export default Counter;

6.2.redux/bindActionCreators.js

/**
 * 绑定action创建者和store.dispatch方法
 * @param {*} actionCreators actions对象 
 * @param {*} dispatch 
 */
function bindActionCreators(actionCreators, dispatch) {
    const boundActionCreators = {};
    for (const key in actionCreators) {
        //add或者minus函数
        const actionCreator = actionCreators[key];
        boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
    return boundActionCreators;
}

/**
 * 
 * @param {*} actionCreator 
 * @param {*} dispatch 
 */
function bindActionCreator(actionCreator, dispatch) {
    const boundActionCreator = function (...args) {
        //{type:ADD}
        const action = actionCreator.apply(this, args);
        dispatch(action);
    }
    return boundActionCreator;
}

export default bindActionCreators;

6.3.redux/index.js

	export { default as createStore } from './createStore';
+ export { default as bindActionCreators } from './bindActionCreators';

7.combineReducers

combineReducers

7.1.src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1'
import Counter2 from './components/Counter2'

ReactDOM.render(<div>
    <Counter1 />
    <Counter2 />
</div>, document.getElementById('root'));

7.2.componets/Counter1.js

import React from 'react'
import { bindActionCreators } from '../redux'
import store from '../store';
import actions from '../store/actions/counter1';
const boundAction = bindActionCreators(actions, store.dispatch);
function Counter1() {
    const [num, setNum] = React.useState(store.getState().Counter1.num);
    React.useEffect(() => {
        return store.subscribe(() => {
            setNum(store.getState().Counter1.num);
        })
    })
    return <div>
        <p>{num}</p>
        <button onClick={boundAction.add1}>+</button>
        <button onClick={boundAction.minus1}>-</button>
    </div>
}
export default Counter1;

7.3.components/Counter2.js

import React from 'react'
import { bindActionCreators } from '../redux'
import store from '../store';
import actions from '../store/actions/counter2';
const boundAction = bindActionCreators(actions, store.dispatch);
function Counter2() {
    const [num, setNum] = React.useState(store.getState().Counter1.num);
    React.useEffect(() => {
        return store.subscribe(() => {
            setNum(store.getState().Counter2.num);
        })
    })
    return <div>
        <p>{num}</p>
        <button onClick={boundAction.add2}>+</button>
        <button onClick={boundAction.minus2}>-</button>
    </div>
}
export default Counter2;

7.4.store/index.js

import { createStore } from '../redux';
import reducers from './reducers';

const store = createStore(reducers);
export default store;

7.5.store/action-types.js

const ADD1 = 'ADD1';
const MINUS1 = 'MINUS1';

const ADD2 = 'ADD2';
const MINUS2 = 'MINUS2';

export {
    ADD1,
    ADD2,
    MINUS1,
    MINUS2
}

7.6.store/reducers/index.js

import { combineReducers } from '../../redux';
import Counter1 from './counter1';
import Counter2 from './counter2';

const rootReducers = combineReducers({
    Counter1,
    Counter2
})
export default rootReducers;

7.7.store/reducers/counter1.js

import * as types from '../action-types';

const initialState = { num: 1 };
function reducer(state = initialState, action) {
    switch (action.type) {
        case types.ADD1:
            return { num: state.num + 1 };
        case types.MINUS1:
            return { num: state.num - 1 };
        default:
            return state;
    }
}
export default reducer;

7.8.store/reducers/counter2.js

import * as types from '../action-types';

const initialState = { num: 1 };
function reducer(state = initialState, action) {
    switch (action.type) {
        case types.ADD2:
            return { num: state.num + 1 };
        case types.MINUS2:
            return { num: state.num - 1 };
        default:
            return state;
    }
}
export default reducer;

7.9.store/actions/counter1.js

import * as types from '../action-types';

const actions = {
    add1() {
        return { type: types.ADD1 };
    },
    minus1() {
        return { type: types.MINUS1 };
    }
}
export default actions;

7.10.store/actions/counter2.js

import * as types from '../action-types';

const actions = {
    add2() {
        return { type: types.ADD2 };
    },
    minus2() {
        return { type: types.MINUS2 };
    }
}
export default actions;

7.11.combineReducers实现

  • redux/combineReducers.js
/**
 * 把一个reducers对象变成一个reducer函数
 * @param {*} reducers 
 * @returns 
 */
function combineReducers(reducers) {
    return (state = {}, action) => {//返回函数就是我们最终的跟reducer
        const nextState = {};//声明一个空对象,用来保存最终的状态(总状态)
        let changed = false;
        for (let key in reducers) {
            const reducer = reducers[key];//分reducer
            const previouseStateForKey = state[key];//分状态
            const nextStateForKey = reducer(previouseStateForKey, action);//计算新的分状态
            if (previouseStateForKey !== nextStateForKey) {//判断新老分状态是否一样
                changed = true;
            }
            nextState[key] = nextStateForKey;
        }
        //返回最终状态
        return changed ? nextState : state;
    }
}
export default combineReducers;
  • redux/indexjs
	export { default as createStore } from './createStore';
	export { default as bindActionCreators } from './bindActionCreators';
+ export { default as combineReducers } from './combineReducers';

8.react-redux

react-redux

8.1.src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1'
import Counter2 from './components/Counter2'
import { Provider } from './react-redux'
import store from './store'

ReactDOM.render(<Provider store={store}>
    <Counter1 />
    <Counter2 />
</Provider>, document.getElementById('root'));

8.2.components/Counter1.js

import React from 'react'
import { connect } from '../react-redux';
import actions from '../store/actions/counter1';
class Counter1 extends React.Component {
    render() {
        const { num, add1, minus1 } = this.props
        return <div>
            <p>{num}</p>
            <button onClick={add1}>+</button>
            <button onClick={minus1}>-</button>
        </div>
    }
}
const mapStateToProps = state => state.Counter1;
export default connect(
    mapStateToProps,
    actions
)(Counter1);

8.3.components/Counter2.js

import React from 'react'
import { connect } from '../react-redux';
import actions from '../store/actions/counter2';
class Counter1 extends React.Component {
    render() {
        const { num, add2, minus2 } = this.props
        return <div>
            <p>{num}</p>
            <button onClick={add2}>+</button>
            <button onClick={minus2}>-</button>
        </div>
    }
}
const mapStateToProps = state => state.Counter2;
export default connect(
    mapStateToProps,
    actions
)(Counter1);

8.4.react-redux/Provider.js

import React from 'react';
import ReactReduxContext from './ReactReduxContext';

function Provider(props) {
    return (
        <ReactReduxContext.Provider value={{ store: props.store }}>
            {props.children}
        </ReactReduxContext.Provider>
    )
}
export default Provider;

8.5.react-redux/ReactReduxContext.js

import React from 'react';
export default React.createContext();

8.6.react-redux/connect.js

import React from 'react';
import ReactReduxContext from './ReactReduxContext';
import { bindActionCreators } from '../redux';

/**
 * 把组件和仓库关联
 * @param {*} mapStateToProps 仓库状态映射为属性
 * @param {*} mapDispatchToProps store.dispatch映射为属性
 * @returns 
 */
function connect(mapStateToProps, mapDispatchToProps) {
    return OldComponent => {
        return props => {
            const { store } = React.useContext(ReactReduxContext);
            const { getState, dispatch, subscribe } = store;
            const prevState = getState();//仓库状态
            //映射后的状态,useMemo做优化
            const stateProps = React.useMemo(() => mapStateToProps(prevState), [prevState]);
            
            let dispatchProps = React.useMemo(() => {
                if (typeof mapDispatchToProps === 'object') {
                    return bindActionCreators(mapDispatchToProps, dispatch);
                } else if (typeof mapDispatchToProps === 'function') {
                    return mapDispatchToProps(dispatch, props);
                } else {
                    return { dispatch };
                }
            }, [dispatch])


            //用于数据改变刷新
            const [, forceUpdate] = React.useReducer(x => x + 1, 0);
            React.useLayoutEffect(() => {//订阅
                return subscribe(forceUpdate)
            }, [subscribe])

            return <OldComponent {...props} {...stateProps} {...dispatchProps} />
        }
    }
}

export default connect;

8.7.react-redux/index.js

export { default as Provider } from './Provider';
export { default as connect } from './connect';

9.hooks

  • useDispatch
  • useSelector

9.1.components/Counter1.js

import React from 'react'
import { useSelector, useDispatch } from '../react-redux'

function Counter1() {
    const state = useSelector(state => state.Counter1);
    const dispatch = useDispatch();
    return <div>
        <p>{state.num}</p>
        <button onClick={() => dispatch({ type: 'ADD1' })}>+</button>
        <button onClick={() => dispatch({ type: 'MINUS1' })}>-</button>
    </div>
}
export default Counter1;

9.2.react-redux/hooks/index.js

export { default as useDispatch } from './useDispatch';
export { default as useSelector } from './useSelector';

9.3.react-redux/index.js

	export { default as Provider } from './Provider';
	export { default as connect } from './connect';
+ export { useSelector, useDispatch } from './hooks';

9.4.react-redux/hooks/useDispatch.js

import React from 'react';
import ReactReduxContext from '../ReactReduxContext';

const useDispatch = () => {
    return React.useContext(ReactReduxContext).store.dispatch;
}
export default useDispatch;

9.5.react-redux/hooks/useSelector.js

import React from 'react';
import ReactReduxContext from '../ReactReduxContext';

function useSelector(selector) {
    const { store } = React.useContext(ReactReduxContext);
    const selectorState = useSelectorWithStore(selector, store);
    return selectorState;
}

function useSelectorWithStore(selector, store) {
    const { getState, subscribe } = store;
    const storeState = getState();//总状态
    const selectorState = selector(storeState);//获取分状态
    //为了刷新用
    const [, forceUpdate] = React.useReducer(x => x + 1, 0);
    React.useLayoutEffect(() => {
        return subscribe(forceUpdate)//订阅和销毁
    }, [store])
    return selectorState;
}

export default useSelector;

10.中间件

  • 如果没有中间件的运用,redux的工作流程会是action->reducer,这是相当于同步操作,由dispatch触发action后,直接去reducer`执行相应的动作
  • 但是如果没有中间件,对于一些复杂逻辑会存在问题。比如:我们点击一个按钮->请求数据->新数据渲染视图,此时因为请求数据是异步的,这个时候同步的redux是无法满足的,因此引入了中间件的概念,有了中间件redux的工作流程变成了action->middlewares->reducer,点击按钮就相当于dispatch触发了action,接着获取服务器数据middlewares执行,当middlewares成功获取服务器数据后就会触发reducer对应的动作更新视图
  • 中间件的机制可以让我们改变数据流,实现如异步actionaction过滤,日志输出

图解

图解2

10.1.日志中间件

  • src/redux-logger.js
function logger(middlewareApi) {//middlewareApi={getState,dispatch}
    return next => {//原始的dispatch
        return action => {//动作
            const { getState } = middlewareApi;
            console.log('老状态', getState());
            next(action);//执行dispatch
            console.log('新状态', getState());
        }
    }
}
export default logger;

10.2.promise中间件

  • src/redux-promise.js
const promise = middlewareApi => next => action => {
    const { dispatch } = middlewareApi;
    //判断是否是一个promise动作
    if (typeof action.then === 'function') {
        return action.then(dispatch)
    } 
    next(action);//派发动作
}
export default promise;

10.3.thunk中间件

  • src/redux-thunk.js
const thunk = middlewareApi => next => action => {
    const { dispatch } = middlewareApi;
    if (typeof action === 'function') {
        return action(dispatch)
    }
    next(action);
}
export default thunk;

10.4.redux/compose.js

function compose(...fns) {
    return fns.reduce((a, b) => (...args) => a(b(...args)));
}
export default compose;

10.5.redux/applyMiddleware.js

import compose from './compose'

/**
 * @param {*} middlewares 多个中间件
 * @returns 
 */
function applyMiddleware(...middlewares) {
    return createStore => {
        return reducers => {
            const store = createStore(reducers);
            let dispatch;
            const middlewareApi = {
                getState: store.getState,
                dispatch: (action) => dispatch(action)
            }
            const chain = middlewares.map(middleware => middleware(middlewareApi));
            dispatch = compose(...chain)(store.dispatch);
            return {
                ...store,
                dispatch
            }
        }
    }
}

export default applyMiddleware;

10.6.src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1'
import Counter2 from './components/Counter2'
import Counter3 from './components/Counter3'
import { Provider } from './react-redux'
import store from './store'

ReactDOM.render(<Provider store={store}>
    <Counter1 />
    <Counter2 />
+   <Counter3 />
</Provider>, document.getElementById('root'));

10.7.components/Counter3.js

import React from 'react';
import { connect } from '../react-redux'
import actions from '../store/actions/counter3'
class Counter3 extends React.Component {
    render() {
        const { num, asyncAdd, pMinus } = this.props;
        return <div>
            <p>{num}</p>
            <button onClick={asyncAdd}>asyncAdd</button>
            <button onClick={pMinus}>promise minus</button>
        </div>
    }
}

const mapStateToProps = state => state.Counter3;
export default connect(mapStateToProps, actions)(Counter3);

10.8.store/index.js

 	import { createStore,applyMiddleware } from '../redux';
  import reducers from './reducers';
+ import logger from '../redux-logger'
+ import promise from '../redux-promise'
+ import thunk from '../redux-thunk'

+ const store = applyMiddleware(promise, thunk, logger)(createStore)(reducers);
  export default store;

10.9.store/actions/counter3.js

import * as types from '../action-types';

const actions = {
    asyncAdd() {
        return dispatch => {
            setTimeout(() => {
                dispatch({ type: types.ADD3 })
            }, 3000)
        }
    },
    pMinus() {
        return dispatch => {
            new Promise((resolve) => {
                resolve(1)
            }).then(res => {
                dispatch({ type: types.MINUS3 })
            })
        }
    }
}

export default actions;

10.10.store/reducers/counter3.js

import * as types from '../action-types';

const initialState = { num: 1 };
function reducer(state = initialState, action) {
    switch (action.type) {
        case types.ADD3:
            return { num: state.num + 1 };
        case types.MINUS3:
            return { num: state.num - 1 };
        default:
            return state;
    }
}
export default reducer;

10.11.store/action-types.js

const ADD1 = 'ADD1';
const MINUS1 = 'MINUS1';

const ADD2 = 'ADD2';
const MINUS2 = 'MINUS2';

+ const ADD3 = 'ADD3';
+ const MINUS3 = 'MINUS3';

export {
    ADD1,
    ADD2,
    MINUS1,
    MINUS2,
+   ADD3,
+   MINUS3
}