Better

Ethan的博客,欢迎访问交流

React 之 redux、react-redux 深入理解与函数式编程

redux 作为 react 数据状态管理首选库,重要性不言而喻了,这里主要从源码的角度学习 redux 和 react-redux 工作原理,因为源码中用到了一些函数式编程概念,比如函数组合,因此该文章还有一些函数式编程知识。

实现 redux

redux 的主要内容有

  • createStore api
  • applyMiddleware && compose api
  • 工具函数:combineReducers && bindActionCreators

实现 createStore

基本使用:传入一个纯函数 reducer 和 enhancer,数据存储就是用的闭包实现。函数本身十分简单,中间件增强可能会拐个弯,函数返回 getState,dispatch 和 subscribe 函数。

基本代码如下:

export function createStore(reducer, enhancer) {
    // 如果有中间件,则直接调用中间件
    if (enhancer) {
        return enhancer(createStore)(reducer);
    }
    let currentState = {};
    let currentListeners = [];

    function getState() {
        return currentState;
    }

    function subscribe(listener) {
        currentListeners.push(listener);
    }

    function dispatch(action) {
        currentState = reducer(currentState, action)
        currentListeners.forEach(v => v());
        return action;
    }

    // 命中default,发起初始化
    dispatch({ type: '@@MINI/MINI-REDUX' })

    return { getState, subscribe, dispatch };
}

实现 applyMiddleware 和 compose

中间件机制通常需要 getStatedispatch 函数,比如 thunk 组件就是增强 dispatch 能力,使得 dispatch 支持函数的能力

中间件是会存在很多个的,这里涉及到一个函数组合。代码如下

// compose(fn1,fn2,fn3) => (args) => fn1(fn2(fn3(args)))
export function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    // reduce 特别适合做累加/累乘/累计调用操作
    return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}

实现 applyMiddleware 功能

export function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args);
        let dispatch = store.dispatch;
        // 构建 midApi
        const midApi = {
            getState: store.getState,
            // 不能直接写成 store.dispatch,为了能在 middleware 中调用 compose 的最新的 dispatch(闭包),精彩
            dispatch: (...args) => dispatch(...args)
        }
        // 绑定 midApi
        const middlewareChain = middlewares.map(middleware => middleware(midApi));
        // 绑定 next
        dispatch = compose(...middlewareChain)(store.dispatch);
        return {
            ...store,
            dispatch
        }
    }
}

实现 thunk 中间件

thunk 的功能其实十分简单,因为 dispatch 默认是支持对象参数的,thunk 使得它支持传递函数的功能,源码其实十分简单

const thunk = ({ dispatch, getState }) => next => action => {
    // 如果符合我们的要求,需要重新dispatch,调用dispatch即可
    if (typeof action === 'function') {
        // 这样函数中就可以拿到 dispatch 和 getState 啦
        return action(dispatch, getState)
    }
    // 如果不符合我们的要求,直接调用下一个中间件,使用next
    return next(action)
}

export default thunk;

实现 bindActionCreators

这是一个工具函数,如果不使用它,我们需要手动一个一个 dispatch 每一个 action,它只是提供了批量处理的能力。因此源码其实也十分简单,简化版大致如下

function bindActionCreator(creator, dispatch) {
    return (...args) => dispatch(creator(...args));
}
export function bindActionCreators(creators, dispatch) {
    return Object.keys(creators).reduce((ret, item) => {
        ret[item] = bindActionCreator(creators[item], dispatch);
        return ret;
    }, {})
}

实现 react-redux

react-redux 的主要内容为

  • 提供 Provider 组件
  • 提供 connect 函数

有了 Redux 设计后,接下来的工作就是如何在 React 中正确工作,react-redux 这一部分我们可以基于 React Context api 轻松实现,以下是基于 React 的旧 Context api 实现,注意在 React 16 中该部分 api 已被重新设计。

Provider 组件的功能具体为

  • 接收 store props,实现全局数据管理
  • 使用 Context api 进行数据共享

具体代码如下

export class Provider extends React.Component {
    static childContextTypes = {
        store: PropTypes.object
    }
    getChildContext() {
        return { store: this.store }
    }
    constructor(props, context) {
        super(props, context);
        this.store = props.store;
    }

    render() {
        return this.props.children
    }
}

接下来就是 connect 实现了,其是一个典型的高阶函数设计,接受 mapStateToPropsmapDispatchToProps 用来得知需要从 store 中获取哪些数据,返回一个函数用来接受组件,最终返回组件,因此我们可以轻松得到一下原型设计

export function connect(mapStateToProps = state => state, mapDispatchToProps = {}) {
    return function(WrapComponent) {
        return class ConnectComponent extends React.Component {

        }
    }
}

接下来思考功能

  • 使用 Context api 获取 store 数据
  • 执行 mapStateToPropsmapDispatchToProps 只传递需要的数据
  • 订阅数据变化,实现组件重新更新

代码如下

export function connect(mapStateToProps = state => state, mapDispatchToProps = {}) {
    return function(WrapComponent) {
        return class ConnectComponent extends React.Component {
            static contextTypes = {
                store: PropTypes.object
            }
            constructor(props, context) {
                super(props);
                this.state = {
                    props: {}
                };
            }
            componentDidMount() {
                const { store } = this.context;
                store.subscribe(() => this.update());
                this.update();
            }
            update() {
                const { store } = this.context;
                const stateProps = mapStateToProps(store.getState());
                // action 执行执行没有意义,需要 dispatch 才能更新 reducer 数据
                const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
                this.setState({
                    props: {
                        ...this.state.props,//本身的props
                        ...stateProps,
                        ...dispatchProps
                    }
                })
            }

            render() {
                // 将所有数据作为 props 透传
                return <WrapComponent {...this.state.props}></WrapComponent>
            }
        }
    }
}

函数式编程

imperative 和 declarative

  • 指令式编程(imperative)关注告诉计算机如何一步步的做某件事情,这也是大多数人习惯的代码风格;
  • 声明式编程(declarative)则关注每一步的输出,函数式编程则从本质上更具声明式特点。
  • 声明式编程比指令式编程要容易理解。计算机擅长指令式,而我们擅长声明式。

如果我们合理的使用了函数式编程,我们的代码主体可能就会变成一个个「独立且拥有合适名称的纯函数」,若干「纯函数的组合」,「集中处理的副作用」。想要了解整体代码逻辑,只需去阅读函数组合在语意上做了什么事情,副作用做了什么,想要了解具体某个纯函数做了什么时,可以单独去查看其定义,而不用去关心其它的函数。

纯函数非常容易写测试,这就能保障我们大多数的代码不会容易出问题,如果真的出现了 bug ,bug 可能主要会出现在少数副作用相关的地方,能更容易被找到和修复。

关于函数的参数,你可能听说过两个词:Arguments 和 parameters:parameters 的个数可以通过 fn.length 获取,其在定义时决定,而arguments.length 属性则在运行时决定。

在 JS 中,return 返回的值也可以是一个函数,这其实是我们熟悉的高阶函数。实际上高阶函数可能算是函数式编程的基石之一了,函数式编程从某种意义上讲就是在不断的创建各种高阶函数。

函数式编程倾向于使用一元函数,但是实际上我们很难保证所用的函数都是一元函数,存在一些办法我帮我们进行元数的转换,有两种常见的方案,partial(偏函数)和 curry(柯里化)。

  • 偏函数:partial(偏函数)是一种减少函数元数(arity)的方法,其核心是,它会创建一个新函数,这个新函数的一些参数是预设好的。
  • curry(柯里化):柯里化则可以看作偏函数的一种特例,其元数会被减少到 1 ,其通过一系列连续的函数调用实现,每个函数都接受一个参数,一旦所有的参数被这些函数调用具体化了,原始函数会基于收集到的所有参数执行。

写好一个函数

  • 函数的名称
  • 减少副作用
  • 纯函数

编程实际上离不开副作用,我们应该做的实际上是尽可能的减少副作用,我们应当确保副作用是我们故意添加的(而非无意的引入代码中的),而且添加的副作用应当尽可能的明显。

函数组合

  • 狭义上讲,组合指的是将一个函数的输出立即传入另外一个函数当作输入
  • 关于 Composition ,有很多工具函数,比如 compose 和 pipe
    • compose: 函数从右向左执行;
    • pipe: 函数从左向右执行;

递归

  • 递归是函数式编程中常见的模式。相比循环而言,递归可能可读性更高。
  • 递归需要满足以下两个条件:
    • 基线条件(base case):函数不再调用自己的条件
    • 递归条件(recursive case): 函数调用自己的条件
  • 递归虽然易懂,但其最大的缺点在于耗内存。
    • tail calls,可能大家听说最多的递归优化方案就是尾调用,不过尾调用需要系统,语言,编译器,运行环境等等都支持才行,目前只有 Safari 浏览器支持,需要在 strict 模式,且递归直接返回函数时才生效;
    • CPS:一种欺骗性的突破栈内存限制的方法;
    • trampolines:一种真实有效的节省内存的方式;

列表操作:常见的有 map,filter,reduce 三种

一些其它的概念

  • transduction
  • monad

来源



留言