一文搞定Redux/React-Redux/中间件实现原理

782 阅读12分钟

很早之前实现过Redux源码,但是没有形成文档。最近得空整理这篇文章,我们将一起学习:

  • 函数式编程:柯里化、compose聚合函数(优点灵活、缺点思路绕)、洋葱模型
  • redux的设计原理和源码实现
  • applyMiddleware函数原理和实现
  • 中间件函数
  • React-Redux实现原理

函数式编程

柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

const add = (v1, v2, v3) => v1 + v2 + v3;
const result = add(1, 2, 3);
// 上述过程转换为下面,就是一个柯里化过程
const sum = (v1) => {
    return (v2) => {
        return (v3) => {
            return v1 + v2 + v3;
        }
    }
}
const result1 = sum(1)(2)(3)

柯里化是函数返回函数,这样就可以依次执行

compose函数

数组求和

如何实现数组的求和

const arr = [1, 2, 3, 4];

// 1.for循环
const fn1 = () => {
    let res = 0;
    for (let i = 0; i < arr.length; i++) {
        res += arr[i]
    }
    return res;
}

// 2.利用Array.prototype.reduce函数
const fn2 = () => {
    const reducer = (res, curValue) => res + curValue;
    arr.reduce(reducer)
}

可以通过两种方式实现数组的求和:

  • 循环
  • Array.prototype.reduce

注:实际上reduce类似于pipe函数(从左往右执行),但是这里不展开

函数依次执行

如题,我希望将我定义的三个函数依次执行

function fn1(arg) {
    console.info("fn1", arg)
    return arg;
}
function fn2(arg) {
    console.info("fn2", arg)
    return arg;
}
function fn3(arg) {
    console.info("fn3", arg)
    return arg;
}
  • 1.直接执行
fn1("max");
fn2("max");
fn3("max");
// 执行顺序 fn1 -> fn2 -> fn3
  • 2.链式,容易进入嵌套地狱,洋葱模型
fn3(fn2(fn1("max")))
// 执行顺序 fn1 -> fn2 -> fn3
  • 3.compose聚合函数
function compose(...fns) {
    return fns.reduce((a, b) => (...args) => a(b(...args)))
}
const newFn = compose(fn1, fn2, fn3)
newFn("max"); // 执行顺序 fn3 -> fn2 -> fn1

函数和普通值的区别是函数需要接收参数,所以reduce返回的是一个函数A,A接收args作为,作为最里层函数b的参数,b(...args)执行的返回结果又作为函数a的参数

考虑到compose函数没有参数的情况,获取只接受一个函数的情况,可以对上面的compose函数做下扩展

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

上面的newFn等同于const newFn = (...args) => fn1(fn2(fn3(...args)))。执行newFn("max")等同于执行fn1(fn2(fn3("max")))

上面的例子就是一个简单的compose函数(从右向左执行)。到目前好像和redux还没有什么关系,咱们继续往下看

洋葱模型

第一次听说洋葱模型还是在学习Koa的时候,因为流程酷似一个洋葱而得名。

ycq.PNG 层层包裹的函数,底层函数把执行结果抛给上一层函数,是不是和上面提到的compose函数很像?

  • 上图中的store.dispatch是增强后的dispatch
  • mid3里面的dispatch是createStore返回的原始dispatch ycq2.webp
  • 从上面两张图可以看出,洋葱模型中每个函数都会接收一个next,这个next参数是函数的时候(一般表示更里一层的中间件函数),函数自己控制这个next的执行时机
  • next的类型需要做判断,因为next有两种情况:
    • 1.函数:可能是下一层中间件函数、也可能是最里层的中间件函数,接收到的是函数作为参数(Redux的中间件就是这种情况,因为最后接收到的是原生dispatch)
    • 2.其他变量:只有一种情况就是最内层接收到参数

通过简化Koa的核心代码koa-compose来加深理解

// 下面是3个中间件函数
function fn1(next) {
    console.info("当前执行fn1的前置");
    next();
    console.info("当前执行fn1的后置")
}

function fn2(next) {
    console.info("当前执行fn2的前置");
    next();
    console.info("当前执行fn2的后置")
}

function fn3(next) {
    console.info("当前执行fn3的前置");
    next();
    console.info("当前执行fn3的后置")
}
// 这个也是koa-compose的简化版
function compose (middleware) {
  return function (next) {
    function dispatch (i) {
      console.info(next)
      let fn = middleware[i] 
      if (i === middleware.length) {
        fn = next
      }
      if (!fn) return;
      return fn(function next () {
        return dispatch(i + 1)
      })
    }
    
    return dispatch(0) 
  }
}
var composeMiddles = compose([fn1,fn2,fn3])
composeMiddles()
/**
当前执行fn1的前置
当前执行fn2的前置
当前执行fn3的前置

当前执行fn3的后置
当前执行fn2的后置
当前执行fn1的后置
*/

上面的composeMiddles函数等同于

(...args) => fn1(null, function next() {
    fn2(null, function next() {
       fn3(null, function next() {
           return;
       })
    })
})

所以:fn1函数最先执行、最后结束

那个啥,应该懂了吧?

111.webp

Reducer

Reducer就是纯函数,接收旧的 state 和 action,返回新的 state

(prevState, action) => nextState

因为和Array.prototype.reduce(reducer, initialValue)里回调函数类似而得名。保持reducer纯净很重要,固定的入参数决定了固定的输出结果,应该只是一个纯粹的执行工具。所以不要在Reducer函数中:

  • 修改传入参数
  • 执有副作的操作,如 API 请求和路由跳转等
  • 调用非纯函数,如Date.now()、Math.random()等

Redux的使用规则

知道怎么用才能知道需要实现什么功能,如果你很熟悉Redux的使用规则,可以直接跳过该章节

三大原则

  • 数据唯一性原则:全局只有一个store
  • 保持只读状态:state是只读的,要修改state的方法就通过触发action
  • 数据改变只能通过纯函数进行:也就是reducers

Redux Flow

redux-flow.webp

  • React Components提交Action派发更新
  • store通过调用Reducer修改store中的值
  • store中的值在修改后,影响到React components(用subscribe + forceUpdate实现)
  • Redux只是纯粹的状态管理器,默认只支持同步

数据的改变流程:

dispatch.PNG

使用规则

// store.js
import { createStore, applyMiddleware } from "redux";

// 定义Reducer
function reducer(state = 0, action) {
    switch(action.type) {
        case "add":
            return state + 1;
        case "minus":
            return state - action.payload || 1;
        default:
            return state;
    }
}

// createStore接收Reducer、applyMiddleware作为参数
const store = createStore(reducer, applyMiddleware(thunk));

export default store;
// App.jsx
import store from "./store";

class App extends React.Component {
    componentDidMount() {
        // 这里通过订阅数据变化,实现数据更新
        store.subscribe(() => {
            this.forceUpdate();
        })
    }
    
    add = () => {
        // 同步更新
        store.dispatch({ type: "add" })
    }
    
    // 这里的dispatch接收的Action是一个函数,需要用到中间件
    asyncAdd = () => {
        store.dispatch((dispatch, getState) => {
            setTimeout(() => {
                dispatch({ type: "add" })
            }, 100)
        })
    }
    
    render() {
        console.info(store.getState()); // 打印当前store中的值
        return (
            <button onClick={this.add}>add</button>
            <button onClick={this.asyncAdd}>asyncAdd</button>
        )
    }
}

Redux源码

从上面大致可以梳理出(不考虑middleware)

  • 1.createStore函数创建、返回store对象包含
    • 2.getState 获取当前状态值state
    • 3.dispatch 提交更新
    • 4.subscribe 订阅state变化

除了上面可以直接看出来的,在实现的过程中创建以后需要一个初始化的过程

  • 5.reducer初始化、修改状态函数
/**
1.createStore函数创建、返回store
@param reducer Reducer函数
@return store
*/
function createStore(reducer) {
    let currentState; // 当前保存的状态值
    let currentListeners = []; // 所有subscribe的监听函数
    
    // 2.获取当前状态值
    function getState() {
        return currentState;
    }
    
    // 3.dispatch
    function dispatch(action) {
        currentState = reducer(currentState, action);
        // state改变,执行订阅的函数
        currentListeners.forEach(listener => listener());
    }
    
    // 4.消息监听
    function subscribe(listener) {
        currentListener.push(listener)
        return () => { // 取消监听
            const index = currentListener.indexOf(listener);
            currentListener.splice(index, 1)
        }
    }
}

// 5.reducer初始化、修改状态函数。也就是主动执行一次dispatch。否则 currentState 没有初始值
dispatch({ type: Symbol() });

return {
    getState,
    dispatch,
    subscribe
}

以上就是Redux的源码,是不是很简单?

applyMiddleware

applyMiddleware函数是Redux最精髓部分。中间件拦截的是dispatch提交到执行reducer的过程,从而增强dispatch的功能

applyMiddleware源码

  • 改造一个中间件的核心思想是返回新的dispatch而不是替换原dispatch
  • 强烈推荐看下官网如何实现一个中间件内容
1.createStore函数改造

改造下createStore函数,检测第二个中间件参数,如果有就把createStore和reducer交给中间件进行扩展处理,代码如下

/**
创建一个store
@param reducer Reducer函数
@param enhancer 增强,也就是中间件
*/
function createStore(reducer, enhancer) {
    if (enhancer) {
        // 如果有加强,则加强下store.dispatch
        return enhancer(createStore)(reducer) // 柯理化
    }
    let currentState; // 当前保存的状态值
    // ........
}

执行createStore(reducer, applyMiddleware(thunk, logger)),等同于applyMiddleware(thunk, logger)(createStore)(reducer)

2.applyMiddleware函数框架

第一步可以推断出applyMiddleware函数接收第一个参数是中间件,返回函数(2次)接收的分别是createStore、reducer的柯里化过程。如下:

function applyMiddleware(...middlewares) {
    return createStore => reducer => {
        const store = createStore(reducer); // 这个还是原来的store。和createStore函数执行的没有区别
        let dispatch = store.dispatch;
        // todo 加强dispatch
        // 执行一次dispatch,相当于 所有中间件函数 依次执行和store.dispatch执行
        
        // dispatch = ...
        return {...store, dispatch}; // 返回的还是那些东西,只是dispatch被包装过(加强)
    }
}

到目前为止,已经初步完成了applyMiddleware函数的架子,从上面看,我们剩下要做的就是加强dispatch。同时我们要知道:执行一次dispatch,相当于所有中间件函数依次执行和store.dispatch执行。

开始将store.dispatch赋值给dispatch变量,我们想要强化dispatch,也就是要给dispatch重新赋值,让dispatch可以实现依次执行的目的

3.compose函数

到此时,就需要使用到上面的洋葱模型,来将各个中间件进行包装,先写好compose函数

function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

上面的compose函数,返回的是一个待执行的函数,看下下面结果

function fn1(arg) {
    console.info("执行fn1")
   return arg + 1;
}
function fn2(arg) {
    console.info("执行fn2")
   return arg + 1;
}
function fn3(arg) {
    console.info("执行fn3")
   return arg + 1;
}
var newFn = compose(fn1, fn2, fn3)
newFn(1)
/**
    执行fn3
    执行fn2
    执行fn1
    4
*/

上面的newFn等同于(...args) => fn1(fn2(fn3(...args)))

4.dispatch增强

如果你熟悉redux-looger中间件,可以看到每次修改都会打印修改前后的值变化。那么中间件也要具备访问状态库的能力

截屏2023-03-30 23.22.15.png

const midapi = {
    getState: store.getState,
    dispatch: action => dispatch(action), // 这里执行的dispatch是增强后的dispatch,会在后面进行修改,完成增强
}
const chain = middlewares.map((middleware) => middleware(midapi))

这样每一个中间件就都可以通过getState访问当前状态库值,以及控制更新状态库的dispatch。同时中间件函数是下面这样

const middleware = ({ dispatch, getState }) {
    return next => ...
}

然后就是让中间件函数依次执行(洋葱模型)得到一个增强后的dispatch

dispatch = compose(...chain)(store.dispatch)

然后将这个增强的dispatch返回,就完成了applyMiddleware函数的功能

applyMiddleware完整代码
function applyMiddleware(...middlewares) {
    return createStore => reducer => {
        const store = createStore(reducer);
        let dispatch = store.dispatch;
        const midapi = {
            getState: store.getState,
            dispatch: (action) => dispatch(action) // 这里执行的dispatch,就是下面compose赋值的dispatch,也就是增强的dispatch
        }
        // chain就是中间件函数的返回值数组,这个数组里每个函数都能访问到状态管理库
        const chain = middlewares.map((middleware) => middleware(midapi))
        dispatch = compose(...chain)(store.dispatch); // 实现dispatch的增强并返回
        return {...store, dispatch};
    }
}

function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    // 这里的args指向的是 dispatch = compose(...chain)(store.dispatch) 传入的 store.dispatch
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

logger中间件源码

function looger({ dispatch, getState }) {
    return next => action => {
        const prevState = getState();
        console.info("值更新前", prevState);
        const nextValue = next(action);
        const nextState = getState();
        console.info("值更新后", nextState);
        return nextValue;
    }
}
  • 上面logger函数接收的参数{dispatch, getState}就是midapi
  • next:接收应该是store.dispatch。所以注意,logger要作为applyMiddleware最后一个参数
  • action:plain object

Redux-thunk中间件源码

function thunk({ dispatch, getState }) {
    return next => action => {
        if (typeof action === "function") {
            return action(dispatch, getState)
        }
        return next(action);
    }
}
  • action判断
    • action是函数,则执行这个action函数,并把dispatch、getState作为参数传递给这个action函数,执行dispatch的时机交给action函数
    • action是object,则直接执行next,并把action传递给下一层(logger)
  • 注意:action接收到的dispatch函数((action) => dispatch(action)),函数内执行的dispatch,已经不是store.dispatch。而是增强后的dispatch(dispatch = compose(...))

总结

applyMiddleware(thunk, logger)为例再梳理一下applyMiddleware到底做了什么

  • 1、执行const chain = middlewares.map((middleware) => middleware(midapi)),等同于[thunk(midapi), thunk(midapi)]
  • 2、执行dispatch = compose(...chain)(store.dispatch)时,等同于thunk(midapi)(logger(midapi)(原生dispatch)),所以logger的next是原生dispatch;而thunk的next是logger(midapi)(原生dispatch)。最终增强后的dispatch是一个接收action为参数的可执行函数(其实也就是thunk的action => ...)
  • 3、参考上文asyncAdd函数,执行store.dispatch(() => ...),也就是执行增强后的dispatch,第一步进入thunk
    • action是函数:将增强dispatch、getState作为参数传入action函数,dispatch执行时机交给action函数
    • action是object:直接执行next(action)
  • 4、当asyncAdd的action函数执行dispatch后,再一次调用了增强dispatch,会重复第三次的判断,只是此时接收到的action是一个object。就会执行next(action),从第二步我们已经知道next是logger(midapi)(原生dispatch),进入logger后,会执行logger的next(原生dispatch)实现修改store值

所以thunk会进入两次

如果对compose函数方式实现的中间件难以消化,可以参考好文推荐,用next = store.dispatch的方式来实现next的修改,实现组合调用,可以有助于你消化和理解dispatch变化的过程

React-Redux实现原理

上文中已经看到,如果我想要监听Redux数据变化的唯一方式是通过subscribe的方式。如何优雅的处理这样的监听,就是React-Redux要做的事。

使用规则

同样的,在梳理原理之前先看使用规则

// 1.Provider包裹
import { Provider } from "react-redux"
import store from "./store";

<Provider store={store}>
    <App/>
</Provider>
// 2.使用connect消费
class App extends React.Component {
    // ...
    render() {
        return <button onClick={this.props.add.bind(this, 1)}>{this.props.count}</button>;
    }
}

function mapStateToProps(store) {
    return {
        count: store.count,
    }
}

function mapDispatchToProps(dispatch) {
    return {
        add: (data) => dispatch({ type: "xx", payload: data })
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(App)

Provider

Provider其实就是一个组件,接收store并放入到context中供全局使用

import React, { createContext } from 'react'
const initialStoreState = {}
export const ProviderContext = createContext(initialStoreState)
export default class Provider extends React.Component {  
    constructor(props) {    
        super(props)    
        this.state = {
            store: props.store
        }
    }
    render() {    
        return (
            <ProviderContext.Provider value={this.state}>
                {this.props.children}
            </ProviderContext.Provider>
        )
    }
}

connect

Provider是数据提供,那么connect就是消费了,通过它的使用规则connect(mapStateToProps, mapDispatchToProps)(App)可以推导出connect是一个函数,接收两个参数,返回一个HOC高阶函数

import { ProviderContext } from "./provider"
function connect(mapStateToProps, mapDispatchToProps) {
    return function(Component) {
        return class Connect extends React.Component {
            static contextType = ProviderContext;
            componentDidMount() {
                // store变化的时候更新。简单实现
                this.context.store.subscribe(() => this.forceUpdate())
            }
            render() {
                return (
                    <Component
                        // 传入该组件的props,需要由connect这个高阶组件原样传回原组件
                        { ...this.props }
                        { ...mapStateToProps(this.context.store.getState()) }
                        { ...mapDispatchToProps(this.context.store.dispatch) }
                    />
                )
            }
        }
    }
}

除了上面之外,还有一些hooks用法,比如useSelectoruseDispatch等,都是通过context实现,原理一致

QA

执行dispatch后打印state(store.getState()),是修改前还是修改后的?

  • 1.如果没有使用中间件的情况下,直接执行dispatch修改状态库值,打印的是修改后的
  • 2.如果使用了如thunk中间件的情况下,store.dispatch是增强后diapatch
    • 如果dispatch接收的是一个函数,那么答应的肯定是修改前的状态值。
    • 如果接收的是一个object的action,则是修改后的状态值

写在最后

这篇文章是我的第 88 篇文章,翻阅了N多文档和文章,也是目前为止耗时最久、字数最多的一篇。函数式编程本身就是比较费解,强烈建议下载上面code在本地调试看下函数调用过程,啰里吧嗦也是希望能够把问题讲清楚,也在努力提高“讲故事”的能力