重学Redux

278 阅读16分钟

image.png

一、createStore创建仓库


createStore接收两个参数,第一个参数是reducer,第二个参数是初始state状态。此时的state状态权重最大。

createStore返回一个store对象,该对象有三个核心方法getState()``subscribe()``dispatch(),分别用来获取最新状态、添加订阅渲染函数、触发状态更新。

用法

import React from 'react';
import { createStore } from '../redux/createStore';
// reducer
const reducer = (oldState, action) => {
    switch (action.type) {
        case 'ADD':
            return { number: oldState.number + 1 };
        case 'MINUS':
            return { number: oldState.number - 1 };
        default:
            return oldState;
    }
}
// store
let store = createStore(reducer, { number: 10 });

class Counter extends React.Component {
    state = {
        number: store.getState().number
    }
    componentDidMount() {
        // 添加订阅,状态变化后,触发setState,状态变更会执行render方法重新渲染
        this.unSubscribe = store.subscribe(() => {
            this.setState({ number: store.getState().number });
        })
    }
    componentWillUnmount() {
        this.unSubscribe();
    }
    render() {
        return (
            <div>
                <p>{this.state.number}</p>
                <button onClick={() => store.dispatch({ type: 'ADD' })}>加</button>
                <button onClick={() => store.dispatch({ type: 'MINUS' })}>减</button>
            </div>
        )
    }
}
export default Counter;

createStore的实现原理:

发布订阅模式,执行dispatch时,触发subscribe订阅的执行函数。一般subscribe订阅的是组件的渲染函数。

// /redux/createStore.js
/**
 * 
 * @param {*} reducer 处理器
 * @param {*} preloadedState 初始状态
 */
export const createStore = (reducer, preloadedState) => {
    let state = preloadedState || {};
    let listeners = [];
    function getState() {
        return state;
    }
    // 添加订阅,返回销毁函数
    function subscribe(listener) {
        listeners.push(listener);
        return () => {
            let index = listeners.indexOf(listener);
            listeners.splice(index, 1);
        }
    }
    // 计算状态,发布
    function dispatch(action) {
        state = reducer(state, action);
        listeners.forEach(listener => listener());
        return state;
    }
    // 默认派发一次,让reducer设置的默认state生效
    dispatch({type: '@@REDUX/INIT'})
    
    const store = {
        getState,
        subscribe,
        dispatch
    }
    return store;
}

二、bindActionCreaters 绑定actionCreaters


actionCreater

所谓actionCreater,就是生成action的函数。

function add(text) {
    return { type: 'ADD', payload: text };
}

bindActionCreaters

bindActionCreaters的功能是绑定actionsdispatch,当调用定义的actionCreator时,自动触发dispatch(action)。简化使用上的操作。

用法

import React from 'react';
import { createStore, bindActionCreators } from '../redux';
// reducer
const reducer = (oldState, action) => {
    switch (action.type) {
        case 'ADD':
            return { number: oldState.number + 1 };
        case 'MINUS':
            return { number: oldState.number - 1 };
        default:
            return oldState;
    }
}
let store = createStore(reducer, { number: 10 });

// actionCreator
function add(event, text) {
    return { type: 'ADD', payload: text };
}
function minus(event, text) {
    return { type: 'MINUS', payload: text };
}
// 创建actionCreator对象
const actions = { add, minus };
// 绑定actionCreator对象和dispatch,当调用actions中的方法时,自动dispatch action
const boundActions = bindActionCreators(actions, store.dispatch);

class Counter extends React.Component {
    state = {
        number: store.getState().number
    }
    componentDidMount() {
        this.unSubscribe = store.subscribe(() => {
            this.setState({ number: store.getState().number });
        })
    }
    componentWillUnmount() {
        this.unSubscribe();
    }
    render() {
        return (
            <div>
                <p>{this.state.number}</p>
                <button onClick={boundActions.add}></button>
                <button onClick={boundActions.minus}></button>
                <button onClick={(event) => boundActions.add(event, 2)}>加</button>
                <button onClick={(event) => boundActions.add(event, 1)}>减</button>
            </div>
        )
    }
}
export default Counter;

bindActionCreaters实现原理

function bindActionCreator(actionCreator, dispatch) {
    // args默认是event合成事件
    return function (...args) {
        dispatch(actionCreator.apply(this, args));
    }
}
/**
 * @param {*} actionCreators actionCreators对象
 * @param {*} dispatch 事件派发
 */
function bindActionCreators(actionCreators, dispatch) {
    const boundActionCreators = {};
    for (const key in actionCreators) {
        const actionCreator = actionCreators[key];
        boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
    return boundActionCreators;
}

export default bindActionCreators;

三、combineReducers合并reducer


分reducer counter1

# 分reducer  
// /reducers/counter1.js

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

const actions = {
    add1(event, text) {
        return { type: types.ADD1, payload: text }
    },
    minus1(event, text) {
        return { type: types.MINUS1, payload: text }
    },
    minus(event, text) {
        return { type: types.MINUS, payload: text }
    }
}

export default actions;

分reducer counter2

# 分reducer  
// /reducers/counter2.js

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

const actions = {
    add2(event, text) {
        return { type: types.ADD2, payload: text }
    },
    minus2(event, text) {
        return { type: types.MINUS2, payload: text }
    },
    minus(event, text) {
        return { type: types.MINUS, payload: text }
    }
}

export default actions;

合并reducer

import { combineReducers } from '../../redux';
import counter1 from './counter1';
import counter2 from './counter2';
let reducers = {
    counter1,
    counter2
}
let rootReducer = combineReducers(reducers);
export default rootReducer;

combineReducers实现原理

/**
 * 把一个reducers对象变成一个reducer函数
 * @param {*} reducers 
 */
function combineReducers(reducers) {
    let rootReducer = function(state = {}, action) {
        let nextState = {};
        for (let key in reducers) {
            // 分reducer
            const reducer = reducers[key];
            // 老的分状态
            const previousStateForKey = state[key];
            // 计算新的状态
            const nextStateForKey = reducer(previousStateForKey, action);
            // 新状态保存到nextState
            nextState[key] = nextStateForKey;
        }
        return nextState;
    }
    return rootReducer;
}

export default combineReducers;

四、一个完整的例子


image.png

└── src
    ├── components
    │   ├── Counter1.js             # 待渲染的组件Counter1
    │   └── Counter2.js             # 待渲染的组件Counter2
    ├── redux
    │   ├── createStore.js          # 创建store
    │   ├── bindActionCreators.js   # 绑定action和dispatch
    │   ├── combineReducers.js      # 合并reducer
    │   └── index.js                # 入口
    ├── store
    │   ├── actions                 # actionCreator
    │   │   ├── counter1.js
    │   │   └── counter2.js
    |   ├── reducers                # 组件对应的分reducer
    │   │   ├── counter1.js
    │   │   ├── counter2.js
    │   │   └── index.js    
    │   └── index.js                # 创建store,传入rootReducer
    └── index.js

1.合并reducer

// store/reducers/index.js

import { combineReducers } from '../../redux';
import counter1 from './counter1';
import counter2 from './counter2';
let reducers = {
    counter1,
    counter2
}
let rootReducer = combineReducers(reducers);
export default rootReducer;
// store/reducers/counter1.js

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

const initialState = { number: 5 };
const counter1 = (oldState = initialState, action) => {
    switch (action.type) {
        case types.ADD1:
            return { number: oldState.number + 1 };
        case types.MINUS1:
        case types.MINUS:
            return { number: oldState.number - 1 };
        default:
            return oldState;
    }
}

export default counter1;

2.创建store

// /store/index.js

import { createStore } from '../redux';
// 导入reducers
import rootReducer from './reducers';
// 创建store
let store = createStore(rootReducer);

export default store;

3. 创建actionCreator

// /store/actions/counter1.js

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

const actions = {
    add1(event, text) {
        return { type: types.ADD1, payload: text }
    },
    minus1(event, text) {
        return { type: types.MINUS1, payload: text }
    },
    minus(event, text) {
        return { type: types.MINUS, payload: text }
    }
}

export default actions;

4. 组件使用示例

// /src/components/Counter1.js

import React from 'react';
import { bindActionCreators } from '../redux';
import store from '../store';
import actions from '../store/actions/counter1.js';

// 绑定actionCreator对象
const boundActions = bindActionCreators(actions, store.dispatch);

class Counter1 extends React.Component {
    state = {
        number: store.getState().counter1.number
    }
    componentDidMount() {
        this.unSubscribe = store.subscribe(() => {
            this.setState({ number: store.getState().counter1.number });
        })
    }
    componentWillUnmount() {
        this.unSubscribe();
    }
    render() {
        return (
            <div>
                <p>{this.state.number}</p>
                <button onClick={boundActions.add1}></button>
                <button onClick={boundActions.minus1}></button>
                <button onClick={boundActions.minus}></button>
            </div>
        )
    }
}
export default Counter1;

五、React-redux


从上面的例子我们发现,每个组件用使用redux,需要订阅事件、绑定Action,获取状态调用getState(),比较繁琐,下面我们看一种简化操作的办法。

一个例子看看provider和connect

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')
);
// /src/components/Counter1.js

import React from 'react';
import actions from '../store/actions/counter1.js';
import { connect } from '../react-redux';

class Counter1 extends React.Component {
    render() {
        const { number } = this.props;
        return (
            <div>
                <p>{number}</p>
                <button onClick={this.props.add1}></button>
                <button onClick={this.props.minus1}></button>
                <button onClick={() => this.props.dispatch({type: 'MINUS'})}>减</button>
            </div>
        )
    }
}
// 一个映射函数,可以把仓库的状态进行映射出来分状态,分状态会成为组件属性对象。
let mapStateToProps = state => state.counter1;
// connect的第二个参数可以直接传actions,也可以传mapDispatchToProps,也可以不传。
let mapDispatchToProps = (dispatch) => {
    return {
        add1() {
            dispatch({ type: 'ADD1' });
        },
        minus1() {
            dispatch({ type: 'MINUS1' });
        },
        minus() {
            dispatch({ type: 'MINUS' });
        }
    }
}
// actions也会进行绑定,成为当前组件属性对象。
// export default connect(mapStateToProps, actions)(Counter1);
export default connect(mapStateToProps, mapDispatchToProps)(Counter1);

Provider

Provider的原理是React.createContext()上下文对象。子组件都可以通过props访问store

// /react-redux/ReactReduxContext.js

import React from 'react';
const ReactReduxContext = React.createContext();

export default ReactReduxContext;
// react-redux/Provider.js

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

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

export default Provider;

connect

负责将store和组件进行关联。
主要功能:

  • mapStateToProps 组件state添加到组件props上
  • mapDispatchToProps action添加到组件props上
  • subscribe添加订阅,更新页面
import React from 'react';
import { bindActionCreators } from '../redux';
import ReactReduxContext from './ReactReduxContext';

/**
 * @param {*} mapStateToProps 
 * @param {*} mapDispatchToProps actions
 */
function connect(mapStateToProps, mapDispatchToProps) {
    // 返回一个新的函数组件
    return function (OldComponent) {
        // 组件包装一层,这里的props是传递给组件的属性
        return function (props) {
            const { store } = React.useContext(ReactReduxContext);
            const { getState, dispatch, subscribe } = store;
            const prevState = getState();
            // 调用mapStateToProps,返回组件的state
            const stateProps = React.useMemo(() => mapStateToProps(prevState), [prevState]);
            // 绑定actions和dispatch,可以通过props.add调用dispatch({type: ADD})
            let dispatchProps = React.useMemo(() => {
                let dispatchProps;
                // 1. mapDispatchToProps是一个对象(actions)
                if (typeof mapDispatchToProps === 'object') {
                    dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
                // 2.mapDispatchToProps是一个函数
                } else if (typeof mapDispatchToProps === 'function') {
                    dispatchProps = mapDispatchToProps(dispatch)
                // 没有传mapDispatchToProps参数,this.props.dispatch({})直接调用
                } else {
                    dispatchProps = { dispatch };
                }
                return dispatchProps;
            }, [store.dispatch]);
            // 添加订阅,利用useReducer,状态发生变化后会执行render实现页面刷新
            const [, forceUpdate] = React.useReducer(x => x + 1, 0);
            // 当前组件渲染后执行,执行一次
            React.useLayoutEffect(() => {
                return subscribe(forceUpdate); // 返回取消订阅函数
            }, [store]); // eslint-disable-line react-hooks/exhaustive-deps

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


export default connect;

六、React-redux的两个hooks


我们看到connect写法有些复杂,react-redux提供了两个hooks简化写法。

useDispatch

事件派发函数

import React from 'react';
import ReactReduxContext from '../ReactReduxContext';
function useDispatch() {
    const { store } = React.useContext(ReactReduxContext);
    return store.dispatch;
}

export default useDispatch;

useSelector

mapStateToProps功能一致,用于获取组件的state

import React from 'react';
import ReactReduxContext from '../ReactReduxContext';
function useSelectorWithStore(selector, store) {
    let state = store.getState();
    // 获取组件对应状态
    let selectedState = selector(state);
    // 更新视图
    const [, forceUpdate] = React.useReducer(x => x + 1, 0);
    React.useEffect(() => {
        return store.subscribe(forceUpdate);
    });
    return selectedState;
}
function useSelector(selector) {
    const { store } = React.useContext(ReactReduxContext);
    const selectedState = useSelectorWithStore(selector, store);
    return selectedState;
}

export default useSelector;

一个例子

// /components/Counter1.js

import React from 'react';
import { useSelector, useDispatch } from '../react-redux';
class Counter1 extends React.Component {
    render() {
        let dispatch = useDispatch();
        let mapStateToProps = state => state.counter1;
        let state = useSelector(mapStateToProps);
        return (
            <div>
                <p>{state.number}</p>
                <button onClick={() => dispatch({type: 'ADD1'})}>加</button>
                <button onClick={() => dispatch({type: 'MINUS1'})}>减</button>
                <button onClick={() => dispatch({type: 'MINUS'})}>减</button>
            </div>
        )
    }
}

export default Counter1;

七、中间件middleWare


  • 中间件的机制,可以改变数据流,实现异步action,action过滤,AOP日志输出等功能。
  • 没有中间件,Redux的工作流程是 action => reducer,同步操作。
  • 有中间件,Redux的工作流程是 action => middleware => reducer。中间件可以扩充dispatch的能力,添加一些自定义的处理。

image.png

applyMiddleware的原理

function applyMiddleware(middleware) {
    return function (createStore) {
        return function(reducer) {
            let store = createStore(reducer);
            let dispatch = middleware(store)(store.dispatch);
            // 重写dispatch方法
            return {
                ...store,
                dispatch
            }
        }
    }
}

编写中间件

中间件的编码格式是固定的

# 改变之前的创建store的方式

// /store/index.js

import { createStore } from '../redux';
// 导入reducers
import rootReducer from './reducers';
// 创建store
// let store = createStore(rootReducer);

function compose(...fns) {
    return function(args) {
        return fns.reduceRight((args, fn) => {
            return fn(args);
        }, args);
    }
}

// 应用中间件(采用分层的思想)
function applyMiddleware(...middlewares) {
    return function (createStore) {
        return function(reducer) {
            let store = createStore(reducer);
            let dispatch;
            let middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action)
            }
            let chain = middlewares.map(middleware => middleware(middlewareAPI));
            dispatch = compose(...chain)(store.dispatch);
            // 重写dispatch方法
            return {
                ...store,
                dispatch
            }
        }
    }
}

// 在调用dispatch(action)的前后做一些事
function logger({ getState, dispatch }) {
    return function(next) {
        return function (action) {
            console.log('prev todo...');
            next(action);
            console.log('next todo...');
        }
    }
}

// 支持action传入的是函数
function thunk({ getState, dispatch }) {
    return function(next) {
        return function (action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            return next(action);
        }
    }
}

// 支持action传入promise
function promise({ getState, dispatch }) {
    return function(next) {
        return function(action) {
            if (typeof action.then === 'function') {
                return action.then(newAction => dispatch(newAction))
            }
            return next(action);
        }
    }
}

// 创建store
let store = applyMiddleware(promise, thunk, logger)(createStore)(rootReducer);

export default store;

测试中间件

// index.js

import store from './store';
store.subscribe(() => console.log(store.getState()));

// 测试logger中间件
store.dispatch({type: 'ADD1'})

// 测试chunk中间件
store.dispatch((dispatch, getState) => {
    setTimeout(() => {
        dispatch({type: 'ADD1'});
    }, 3000)
})

// 测试promise中间件
store.dispatch(new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve({type: 'ADD1'});
    }, 2000)
}))

八、connect-react-router


connect-react-router,用来连接reduxrouter

1)通常有两个主要功能:

  • 可以通过派发动作实现路由跳转。
  • 可以在store仓库拿到最新路径信息。

2)connect-react-router主要有4个关键方法:

  • push:是一个actionCreator,用来生成跳转路径的action
  • routerMiddleware:是一个中间件,主要功能是根据push函数提供的action,调用history方法进行页面跳转。
  • ConnectedRouter:是一个组件,主要用于实现监听路径变化的功能,当路径发生变化,派发特定的action
  • connectRouter:是一个reducer,主要功能是识别ConnectedRouter组件dispatchaction,然后更新store中的路径状态信息。

image.png

└── src
    ├── components
    │   ├── Counter.js              # 待渲染的组件Counter1
    │   └── Home.js                 # 待渲染的组件Counter2
    ├── connected-react-router
    │   ├── action-types.js         # 创建store
    │   ├── actions                 # push locationChange
    │   ├── ConnectedRouter.js      # 连接路由组件
    │   ├── connectRouter.js        # 一个reducer,当切换路由时,更新store的路由信息
    │   ├── routerMiddleware.js     # 中间件,重写dispatch方法,拦截派发动作,实现路由跳转
    │   └── index.js                # 入口
    ├── store
    │   ├── actions                 # actionCreator
    │   │   └── counter.js          # go => push action
    |   ├── reducers                # 组件对应的分reducer
    │   │   ├── counter.js
    │   │   └── index.js            # combineReducers
    │   └── index.js                # 创建store,传入rootReducer
    └── index.js                    # 入口,使用了ConnectedRouter组件,传递history属性(本质上还是Router组件)

使用

// index.js 入口

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Route, Link } from 'react-router-dom';
import { ConnectedRouter } from './connected-react-router';
import history from './history';
import store from './store';
import Home from './components/Home';
import Counter from './components/Counter';

ReactDOM.render(
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <ul>
                <li><Link to="/" exact="true">首页</Link></li>
                <li><Link to="/counter">计数器</Link></li>
            </ul>
            <Route path="/" exact={true} component={Home}></Route>
            <Route path="/counter" component={Counter}></Route>
        </ConnectedRouter>
    </Provider>,
    document.getElementById('root')
);

ConnectedRouter 需要store,用来保存路由信息。使用react-redux提供的Providerstore传递到子组件。

// /src/components/Counter.js

import React from 'react';
import { connect } from 'react-redux';
import actions from '../store/actions/counter';

class Counter extends React.Component {
    handleClick = () => {
        this.props.go('/');
    }
    render() {
        return (
            <div>
                <p>{this.props.number}</p>
                <button onClick={this.props.add}>+</button>
                <button onClick={this.props.minus}>-</button>
                <button onClick={this.handleClick}>Counter</button>
            </div>
        )
    }
}

export default connect(state => state.counter, actions)(Counter);

1. 通过派发动作实现路由跳转

路由跳转和路径变化时更新store的两个action

// action-types.js

// 派发动作,切换路由
export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD';
// 向仓库派发动作,更新最新路由信息
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE';

生成路由跳转和路径变化两个action的actionCreator

// /src/connected-reaact-router/actions.js

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

/**
 * 这是一个用来跳转路径的actionCreator
 * @param {*} path 跳转路径
 */
function push(path) {
    return {
        type: types.CALL_HISTORY_METHOD,
        payload: {
            method: 'push',
            args: [path]
        }
    }
}

/**
 * 路径变化的actionCreator
 * @param {*} location 
 * @param {*} action 
 */
function locationChange(location, action) {
    return {
        type: types.LOCATION_CHANGE,
        payload: {
            location,
            action
        }
    }
}

export {
    push,
    locationChange
};

counter组件的actions,通过connect放到props

import * as types from '../action-types';
import { push } from "../../connected-react-router";

const actions = {
    add() {
        return { type: types.ADD };
    },
    minus() {
        return { type: types.MINUS };
    },
    go(path) {
        return push(path);
    }
}

export default actions;

routerMiddleware中间件,重写dispatch,当派发路径切换动作时,进行路由跳转。

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

function routerMiddleware(history) {
    return function(middlewareAPI) { // {getState, dispatch}
        return function(next) {
            return function(action) {
                if (action.type !== types.CALL_HISTORY_METHOD) {
                    return next(action);
                }
                // 拿到push.js中的action
                const { payload: { method, args }} = action;
                // 路径跳转
                history[method](...args);
            }
        }
    }
}

export default routerMiddleware;

使用中间件,创建store

import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from '../connected-react-router';
import rootReducer from './reducers';
import history from '../history';

// routerMiddleware中间件,重写dispatch方法,如果发现是router,则保存最新路由信息
const store = applyMiddleware(routerMiddleware(history))(createStore)(rootReducer);

export default store;

2. 在store仓库拿到最新路径信息

ConnectedRouter组件,用来监测路由变化,路由变化时,派发事件。

import React from 'react';
import { connect, ReactReduxContext } from 'react-redux';
import { Router } from 'react-router';
import { locationChange } from './actions';

// 依赖store
class ConnectedRouter extends React.Component {
    componentDidMount() {
        // 当路径发生变化的时候,会执行回调,传递最新的location和action
        this.props.history.listen((location, action) => {
            // 派发事件
            this.props.dispatch(locationChange(location, action));
        })
    }
    render() {
        const { history, children } = this.props;
        return (
            <Router history={history}>
                { children }
            </Router>
        )
    }
}

export default connect(state => state)(ConnectedRouter);

connectRouterreducer,当派发路由切换动作时,更新store中的路由信息

import * as types from './action-types';
function connectRouter(history) {
    const initialState = {
        location: history.location,
        action: history.action
    }
    // 返回一个reducer
    return function (state = initialState, action) {
        // 如果是触发路由action,则覆盖老状态的路由信息
        if (action.type === types.LOCATION_CHANGE) {
            return { ...state, ...action.payload };
        } else {
            return state;
        }
    }
}

export default connectRouter;

reducers入口

import { combineReducers } from 'redux';
import { connectRouter } from '../../connected-react-router';
import history from '../../history';
import counter from './counter';

let reducers = {
    counter,
    router: connectRouter(history)
}
let rootReducer  = combineReducers(reducers);

export default rootReducer;

九、redux-saga


redux-saga是一个redux的中间件,为redux提供额外的功能。

我们知道reducers的操作都是同步且纯粹的,reducer都是纯函数,在执行过程中不会对外部产生副作用。

实际开发中,可能有一些异步请求,或者不纯的操作(改变外部状态等),这些在函数式编程规范中被称为副作用。

redux-saga就是用来处理上述副作用的一个中间件。类似于前面写的promise,thunk、logger中间件。

redux-saga工作原理

  • 使用Generator函数来yield Effects
  • Generator函数的作用是可以暂停执行,再次指向的时候从上次暂停的地方继续执行。
  • Effect是一个对象,该对象包含给middleware解释执行的信息。
  • 可以使用effects API,如 forkcalltakeputcancel来创建Effect

redux-saga分类

  • root saga,启动saga的唯一入口。
  • watcher saga,监听被dispatchactions,当接收到action或者知道其被触发时,调用worker执行任务。
  • worker saga,执行具体的工作任务,如调用API,进行异步请求,获取异步封装结果。

redux-saga工作流程

image.png

  • channel是一个管道,其实就是一个发布订阅模式实现的,用来控制生成器函数执行的函数。
  • runSaga是调度核心,负责向channel订阅函数take,执行channel中的发布函数put来控制生成器函数的执行,以及dispath(action)来真正修改状态,更新页面。
  • rootSaga是用户定义的,root saga。

redux-saga目录结构

└── src
    ├── components
    │   └── Counter.js              # 待渲染的组件Counter
    ├── redux-saga
    │   ├── channel.js              # 提供take和put方法,用来控制生成器函数的执行
    │   ├── effects.js              # 返回指令对象
    │   ├── effectTypes.js          # 类型,TASK和PUT
    │   ├── runSaga.js              # 核心方法,负责执行生成器函数,调用channel方法,调用dispatch方法更新视图
    │   └── index.js                # createSagaMiddleware 返回一个中间件,重写dispatch方法
    ├── store
    │   ├── actions                 # actionCreator
    │   │   └── counter.js
    |   ├── reducers                # 组件对应的分reducer
    │   │   ├── counter.js
    │   │   └── index.js    
    |   ├── sagas                   # 根saga,一个生成器函数
    │   │   └── index.js    
    │   └── index.js                # 创建store,传入rootReducer
    └── index.js

注册阶段

saga生成器函数

// src/store/sagas/index.js

function *rootSaga() {
    console.log('启动rootSaga')
    while (true) {
        // task监听动作:等待有人向仓库派发一个ASYNC_ADD这样的命令,等到了就继续执行,等不到就停在这里
        yield take(types.ASYNC_ADD); // { type: 'TAKE', actionType: 'ASYNC_ADD' };
        // put派发一个动作:store.dispatch({type: types.ADD})
        yield put({ type: types.ADD }); // { type: 'PUT', action: { type: 'ADD' } }
    }
}

创建中间件,创建store,传入中间件,执行中间件的run方法,开始执行runSaga函数。

// /src/store/index.js

import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import createSagaMiddleware from '../redux-saga';
import rootSaga from './sagas';

// 创建saga中间件,dispatch时,发布put方法,继续往下执行
let sagaMiddleware = createSagaMiddleware();
// 创建store,传入中间件
let store = applyMiddleware(sagaMiddleware)(createStore)(rootReducer);
// 启动saga,执行runSaga方法
sagaMiddleware.run(rootSaga);

export default store;

创建中间件,重写dispatch当用户调用dispatch时,同步action直接触发,异步action,执行channelput方法,调用dispatch,然后调用next继续往下执行生成器函数。

提供的run方法,实际上是执行runSaga函数,这个函数会执行rootSata生成器函数。

// /src/redux-saga/index.js

import runSaga from "./runSaga";
import createChannel from './channel';
import { take } from "./effects";

function createSagaMiddleware() {
    let channel = createChannel(); // {take, put}
    let boundRunSaga;
    // 返回一个中间件
    function sagaMiddleWare({ getState, dispatch }) {
        boundRunSaga = runSaga.bind(null, { getState, dispatch, channel });
        return function(next) {
            // 重写dispatch方法
            return function(action) {
                // 触发dispatch(action),非异步action,直接这里触发
                let result = next(action);
                // 发布take保存的函数
                channel.put(action);
                return result;
            }
        }
    }
    // 中间件上的run方法,用于启动saga
    sagaMiddleWare.run = (saga) => boundRunSaga(saga);

    return sagaMiddleWare;
}

export default createSagaMiddleware;

runSaga执行生成器函数,控制生成器函数的暂停和执行。异步actionput时调用。

// /src/redux-saga/runSaga.js

import * as effectTypes from './effectTypes';

/**
 * 执行或者启动saga的方法,类似于co库
 * @param {*} saga 
 */
function runSaga(env, saga) {
    const { getState, dispatch, channel } = env;
    // saga可能是一个生成器,也可能是一个迭代器
    let it = typeof saga === 'function' ? saga() : saga;

    function next(value) {
        let { value: effect, done } = it.next(value);
        // {type: "TAKE", actionType: "ASYNC_ADD"}
        // {type: "PUT", action: {type: "ADD"}}
        if (!done) {
            // effect是一个迭代器
            if (typeof effect[Symbol.iterator] === 'function') {
                runSaga(env, effect);
                next();
            } else {
                switch (effect.type) {
                    case effectTypes.TAKE:
                        channel.take(effect.actionType, next); // 订阅next函数,停止向下执行,等到dispatch(action),发布next()函数,后,继续往下执行
                        break; // 没有 next(),暂停执行
                    case effectTypes.PUT:
                        dispatch(effect.action); // 异步action在这里触发,触发同步的action  ASYNC_ADD => ADD
                        next(); // 派发完后立刻向下执行
                        break;
                    default:
                        break;
                }
            }
        }
    }
    next();
}

export default runSaga;

十、dva


dva 首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch ,所以也可以理解为一个轻量级的应用框架。

dva集成了react全家桶,集成了reduxreact-reduxreact-router-domconnected-react-router

image.png

用法

// index.js

import React from 'react';
import dva, { connect, ConnectedRouter } from './dva';
import { Router, Route, Link, routerRedux } from 'dva/router';

// dva是一个函数,执行它可以得到一个app实例
const app = dva();
// model方法定义一个模型
app.model({
  namespace: 'counter',
  state: { number: 0 },
  reducers: {
    add(state) {
      return { number: state.number + 1 };
    }
  },
  effects: {
    // action动作,effects = redux/effects(take, put, ...)
    *asyncAdd(action, { call, put, select }) {
      yield call(delay, 1000);
      yield put({ type: 'add' }); // 不用加命名空间前缀
      let state = yield select(state => state.counter);
      console.log('state', state);
    },
    *goto({ payload }, { put }) {
      // push是一个actionCreator,执行会返回一个action = {type: 'CALL_HISTORY_METHOD', payload: {method: 'push'}}
      yield put(routerRedux.push(payload));
    }
  }
})

function Counter(props) {
  return (
    <div>
      <p>{props.number}</p>
      <button onClick={() => props.dispatch({ type: 'counter/add' })}>+</button>
      <button onClick={() => props.dispatch({ type: 'counter/asyncAdd' })}>async +</button>
      <button onClick={() => props.dispatch({ type: 'counter/goto', payload: '/' })}>跳转到home</button>
    </div>
  )
}

function delay(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  })
}

const ConnectedCounter = connect(state => state.counter)(Counter);
const Home = () => <div>home</div>
app.router((api) => (
  <ConnectedRouter history={api.history}>
    <>
      <Link to="/">home</Link>
      <Link to="/counter">counter</Link>
      <Route path="/" exact="true" component={Home}></Route>
      <Route path="/counter" component={ConnectedCounter}></Route>
    </>
  </ConnectedRouter>
));

app.start('#root');

原理实现

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider, connect } from 'react-redux';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import prefixNamespace from './prefixNamespace';
import * as sagaEffects from 'redux-saga/effects';
import createSagaMiddleware from 'redux-saga';
import { createBrowserHistory } from 'history';
import { routerMiddleware, connectRouter, ConnectedRouter } from 'connected-react-router';
export { connect, ConnectedRouter };
let history = createBrowserHistory();

function dva() {
    const app = {
        _models: [], // 当调用app.model时,保存传入的model
        model,
        _router: null,
        router,
        start
    }
    // 命名空间:reducer => counter/add: reducer(state, action)
    const initialReducers = {
        router: connectRouter(history)
    };
    // 定义数据模型
    function model(model) {
        // 给reducer添加命名空间
        let prefixedModel = prefixNamespace(model);
        app._models.push(prefixedModel);
    }
    // 定义路由,要渲染的节点
    function router(router) {
        app._router = router;
    }
    // 启动函数
    function start(root) {
        // 构建reducers对象
        for (const model of app._models) {
            initialReducers[model.namespace] = getReducer(model);
        }
        function createReducer() {
            return combineReducers(initialReducers);
        }
        // 合并reduer,获取根reduer
        let rootReducer = createReducer();

        // saga
        let sagas = getSagas(app);
        let sagaMiddleware = createSagaMiddleware();
        let store = applyMiddleware(routerMiddleware(history), sagaMiddleware)(createStore)(rootReducer);
        sagas.forEach(sagaMiddleware.run);

        // 渲染
        ReactDOM.render(
            <Provider store={store}>
                {app._router({ history })}
            </Provider>,
            document.querySelector(root)
        )
    }
    return app;
}
// effects
function getSagas(app) {
    let sagas = [];
    for (const model of app._models) {
        sagas.push(getSaga(model.effects, model));
    }
    return sagas;
}
function getSaga(effects, model) {
    return function *() {
        for (const key in effects) {
            const watcherSaga = getWatcherSaga(key, model.effects[key], model);
            yield sagaEffects.fork(watcherSaga);
        }
    }
}
function getWatcherSaga(key, effect, model) {
    return function *() {
        yield sagaEffects.takeEvery(key, function *(action) {
            yield effect(action, {
                ...sagaEffects,
                // 重写put方法,兼容不加counter的写法
                put: (action) => {
                    return sagaEffects.put({
                        ...action,
                        type: prefix(action.type, model.namespace)
                    })
                }
            });
        });
    }
}
function prefix(actionType, namespace) {
    // 已经加了前缀就直接返回,不加前缀帮忙加上
    if (actionType.indexOf('/') === -1) {
        return `${namespace}/${actionType}`;
    } else {
        return actionType;
    }
}

// reducer
function getReducer(model) {
    let { state: initialState, reducers } = model;
    let reducer = (state = initialState, action) => {
        let reducer = reducers[action.type];
        // 找到对应处理函数,则返回处理函数的处理后的状态
        if (reducer) return reducer(state);
        // 没有找到对应处理函数,返回老状态
        return state;
    }
    return reducer;
}
/**
state = { number: 0 }
reducers = {
    add(state) {
        return { number: state.number + 1 };
    }
}
 */

export default dva;

十一、dva-cli


安装

npm install dva-cli -g

创建项目

dva new dva-antdesign

安装antd和按需加载插件

npm i antd -S
npm i babel-plugin-import -D

配置.webpackrc,使babel-plugin-import生效。

{
    "extraBabelPlugins": [
        ["import", {"libraryName": "antd", "libraryDirectory": "es", "style": "css"}]
    ]
}

十二、umi


umi是一个类似于next.js的脚手架工具,与dva深入融合,并通过约定、自动生成和解析代码等方式来辅助开发,减少我们开发者的代码量。

安装

npm install umi -g

生成页面

umi g page index

配置启动命令

"scripts": {
    "dev": "umi dev",
    "build": "umi build"
}