阅读 3951

浅谈redux、react-redux、redux-saga原理

写在前面

react自从2013年推出到如今已经走过了很多个年头,react生态redux、react-redux、redux-saga也已经成为react开发者的配套标准,使用起来已经相当熟悉,本文简单聊聊redux、react-redux和redux-saga的实现原理。

redux

这张经典的图描述了redux的工作流程,简单来说就是view层触发一个action到dispatcher,dispatcher将aciton传入reducer进行计算,得到新的state后传入view层进行状态更新。

下边是一个简易版的createStore.js代码

export function createStore(reducer, enhandler) {
    if (enhandler) {
        return enhandler(createStore)(reducer)
    }
    let state = {}; //全局state存放的地方
    let observers = []; // 观察者队列
    // getter
    function 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' }) // 初始化state数据
    return {getState, dispatch, subscribe}
}
复制代码

可以看到,createStore是一个高阶函数,接收reducer函数和enhandler函数作为参数,并返回getState, dispatch, subscribe三个函数,其中

reducer用来根据当前的state和传入的action计算出新的state,redux规定了reducer必须为纯函数;

enhandler是执行applyMiddleware后的中间件扩展函数,用来对dispatch进行增强;

getState单纯地返回state,相当于getter;

dispatch是唯一修改state的入口,相当于setter;

subscribe是对state的订阅,当state产生变化时,会触发对应的回调函数。

简单来说,createStore将全局state作为一个闭包变量保存,保证了外界无法直接读取修改,同时返回了操作state的句柄,以及对于state变化提供了监听。

reducer部分,一个例子根据不同的action类型分别对state进行计算,返回新的state

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: 
            return state || initialState;
    }
}
复制代码

通常我们会将reducer合并后再传入createStore

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

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

export default rootReducer;
复制代码

combineReducers的代码

export const combineReducers = (reducers) => {
    return (state = {}, action) => {
      return Object.keys(reducers).reduce((nextState, key) => {
          nextState[key] = reducers[key](state[key], action);
          return nextState;
        },
        {} 
      );
    };
}
复制代码

可以看到,在createStore传入rootReducer后,得到的state结构为

{
    app: {},
    comp: {}
}
复制代码

combineReducers将state进行分发, 例如appReducer只传入app key对应的数据,达到了拆分数据,单独进行处理的效果。

redux同时也提供了扩展的功能,我们知道,这一步扩展在dispatch到reducer之间实现,即通过对dispatch的增强,来达到扩展其他功能的效果,下边是简易版applyMiddleware.js的实现

function compose(...fns) {
    if (fns.length === 0) return arg => arg    
    if (fns.length === 1) return fns[0]    
    return fns.reduce((res, cur) =>(...args) => res(cur(...args)))
}

export const 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 }
        }
    }
}
复制代码

可以看到,applyMiddleware是一个柯里化函数,刚好对应了enhandler(createStore)(reducer)的调用方式,其中最关键的两行代码

const middlewareArr = middlewares.map(middleware => middleware(params)) 
dispatch = compose(...middlewareArr)(dispatch)
复制代码

此处对传入的中间件依次进行初始化,将getState, dispatch塞入中间件中,使得中间件有访问store的能力,在完成中间件的第一步调用后,再利用compose函数将多个中间件串联起来,传入旧的dispatch进行第二次调用,最终返回增强后的dispatch。

看看一个中间件的例子,同样地中间件也是一个柯里化函数

export default function logger({ getState }) {
    return (next) => (action) => {
      let returnValue = next(action)
  
      console.log('state after dispatch', getState())
      
      return returnValue
    }
  }
复制代码

也就是说,applyMiddleware、中间件logger都是柯里化后的函数,利用了其延迟执行的特点,分步进行调用,最终

applyMiddleware(middleware1, middleware2, middleware3)经过处理后,会得到

dispatch = middleware1(middleware2(middleware3(action)))的执行方式,即洋葱模型

总之,redux的实现,总体思路是将state提取到统一的地方进行管理,state设置为只读,并只能通过reducer进行更改。约定action为对象,reducer为纯函数,并且不干涉异步场景的处理,只提供middler机制开放出扩展的功能。 实现原理上看,代码体现了函数式编程的思想,多次运用高阶函数,函数柯里化等技巧,代码设计得相当简洁和巧妙。

react-redux

redux与react一起使用时,我们需要手动在组件里通过subscribe监听state的变化并更新组件,为了解决这样的问题,redux官方提供了react-redux的库,通过connect的方式连接state和react组件,达到自动监听的效果,使用方式如下

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);
  
复制代码

主要看看provider.js和connect.js的实现

provider.js简单版

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

export default class Provider extends React.Component {  
    // 需要声明静态属性childContextTypes来指定context对象的属性,是context的固定写法  
    static childContextTypes = {    
        store: PropTypes.object  
    } 

    constructor(props, context) {    
        super(props, context)
        this.store = props.store  
    } 

    // 实现getChildContext方法,返回context对象,也是固定写法  
    getChildContext() {    
        return { store: this.store }  
    }  

    // 渲染被Provider包裹的组件  
    render() {    
        return this.props.children  
    }
}
复制代码

可以看到,利用了react的conetext功能,只需要在最顶级套上Provider组件,其他所有组件均可从conetext获取到state,避免了props的层层传递

再看看connect的简单实现

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

export function connect(mapStateToProps, mapDispatchToProps) {    
    return function(Component) {      
        class Connect extends React.Component {        
            componentDidMount() {          
                //从context获取store并订阅更新          
                this.context.store.subscribe(this.handleStoreChange.bind(this));        
            }       
            handleStoreChange() {          
                // 触发更新          
                // 触发的方法有多种,这里为了简洁起见,直接forceUpdate强制更新,读者也可以通过setState来触发子组件更新          
                this.forceUpdate()        
            }        
            render() {          
                const {store} = this.context;
                const {getState, dispatch} = store;
                return (            
                    <Component              
                        // 传入该组件的props,需要由connect这个高阶组件原样传回原组件              
                        { ...this.props }              
                        // 根据mapStateToProps把state挂到this.props上              
                        { ...mapStateToProps(getState()) }               
                        // 根据mapDispatchToProps把dispatch(action)挂到this.props上              
                        { ...mapDispatchToProps(dispatch) }                 
                    />              
                )        
            }      
        }      
        //接收context的固定写法      
        Connect.contextTypes = {        
            store: PropTypes.object      
        }      
        return Connect    
    }
}
复制代码

connect是一个高阶组件,接收mapStateToProps和mapDispatchToProps参数,其中mapStateToProps的作用是将特定的state映射到组件的props上,mapDispatchToProps将dispatch(action)映射到props上,并在componentDidMount统一进行store的subscribe监听,当state变化时,被connect的所有组件都会进行一次render。

总结:Provider的本质是利用context统一传递,connect本质是将监听和获取state的逻辑进行统一抽取复用,这也是高阶组件的常用功能,被connect的组件变成了UI型组件,只需要从props中获取到状态进行渲染即可。

redux-saga

提到redux-saga,通常会提到redux-thunk,两者都是redux的中间件,都是对异步场景的处理。redux-thunk非常简短,只有十几行的代码,简单实现如下

export default function thunk({ dispatch, getState }) {
  return (next) => (action) => {
    if (typeof action === 'function') {
      action(dispatch, getState())
    }
    next(action)
  }
}
复制代码

可以看到,thunk支持了function形式的action,将dispatch句柄交由function去处理,我们可以在action进行异步调用,等到结果返回时再进行dispatch达到支持异步的效果,一个简单的使用例子

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

const fetchData = (text) => (dispatch) => {
    new Promise((resolve) => {
        setTimeout(() => {
            resolve();
        }, 2000)
    }).then(() => {
        dispatch(addCountAction(text))
    })
}
复制代码

这里假定异步结果2s后返回,返回后再进行dispatch的调用。

redux-thunk的大概流程:

redux-thunk虽然支持了异步场景,但其存在的缺点也很明显:

1、使用回调的方式来实现异步,容易形成层层回调的面条代码

2、异步逻辑散落在各个action中,难以进行统一管理

因此,出现了redux-saga更强大的异步管理方案,可以代替redux-thunk使用。

redux-saga的大概流程:

其主要特点

1、使用generator的方式实现,更加符合同步代码的风格;

2、统一监听action,当命中action时,执行对应的saga任务,并且支持各个saga之间的互相调用,使得异步代码更方便统一管理。

在saga中,出现了新的概念,其中effect指一个普通的js对象,描述一个指定的动作,saga指一个generator函数。

首先看看saga的接入:

//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: 将异步执行 increment 任务
function* addAsync(action) {
  yield fork(delay, 1000)
  yield put(formatAction(action, namespace))
}


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

复制代码

先通过createSagaMiddleware生成sagaMiddleware,注册成为redux的中间件,再调用中间件暴露的run方法,run的作用是统一初始化rootSaga,开启对action的监听。其中createSagaMiddleware的简单版如下:

export default function sagaMiddlewareFactory() {
    let _store; //闭包store,后续sagaMiddleware可以访问到
    function sagaMiddleware(store) {
        _store = store;
        return (next) => (action) => {
            next(action);
            channel.put(action)
        }
    }
    // 启动rootSaga,即进行入口saga的自执行
    sagaMiddleware.run = function(rootSaga) {
        let iterator = rootSaga();
        proc(iterator, _store);
    }
    return sagaMiddleware;
}
复制代码

sagaMiddleware是一个符合redux标准的中间件,并在sagaMiddleware挂载了run方法,run方法中调用了proc,看看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)
        }
    }
}
复制代码

可以看到,proc是一个generator的自执行器,通过递归的方式实现,在result.done为true时表示完成generator的执行。我们假定只yield三种类型,promise、effect和普通语法,分别进行对应处理。例如effect,当在saga中 yield put(action)时,只是调用了put普通函数,返回的了一个put类型的effect,effect是一个普通的js对象,看看effect的定义:

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

复制代码

所以,effect描述了该任务的类型和相关参数,effect的执行是在runEffect环节,即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();
}

function runTakeEvery({signal, saga, ...args}, next, store) {
    function *takeEveryGenerator() {
        while(true) {
            let action = yield take(signal);
            yield fork(saga, action);
        }
    }
    
    runFork({saga: takeEveryGenerator}, next, store);
}
复制代码

runEffect执行对应的effect,比如put,可以看到本质是对dispatch的封装。saga提供的其他辅助函数takeEvery等,是对低级effect的封装。

那么当我们使用takeEvery监听到了action,调用take进行监听,函数中调用的channel.take是什么意思呢?看看channel实现例子:

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();
复制代码

可以看到,channel的实现是一个简单生产消费者的模式,take生成任务,put消费任务。这里就可以看到take为什么是阻塞的了,当take一个action类型时,实际上是往channel中put了一个任务,只有在该action被dispatch时,调用put消费,此处take effect所在的saga的迭代器才会被继续执行下去,所以执行到take时,实际上是迭代器next没有进行下一步的迭代,导致saga 阻塞。

总结:从这个简易的模型可以看出,redux-saga其实是在dispatch和reducer之间架设了一层异步处理层,专门来处理异步任务。 在sagaMiddleware初始化run时,对入口的saga进行了自执行,开始了对action的监听。遇到yield的effect时交由对应的runEffect执行,命中action时则派生对应的saga任务,这就是redux-saga大概的原理。

至此,完成了对于redux、react-redux、redux-saga的原理的简单分析,不仅可以从中学习优秀的设计思路,也能在业务使用中做到知其所以然。

demo地址:github.com/lianxc/lear…

参考资料:

juejin.cn/post/684490…

www.jianshu.com/p/1608786c9…

juejin.cn/post/684490…

segmentfault.com/a/119000001…

written by:先崇@ppmoney

文章分类
前端
文章标签