zoukankan      html  css  js  c++  java
  • 从Redux源码探索最佳实践

    前言

    Redux 已经历了几个年头,很多 React 技术栈开发者选用它,我也是其中一员。期间看过数次源码,从最开始为了弄清楚某一部分运行方式来解决一些 Bug,到后来看源码解答我的一些假设性疑问,到最后想揭开它的面纱获得更多指导。在这个过程中我逐渐对 Redux 有了更多认识和收获,因此也决定写下这篇文章来和更多开发者一起交流。本文主要是赏析源码实现技巧,从源码层面介绍 Redux 使用中需要注意的地方。

    用法简述

    Redux 可以解耦 React(View层)与数据管理和对数据的操作,保持 React(层)的纯净,使职责划分清晰。同时降低了 React 数据传递难度与不可控性。它还提供可预测化的状态管理。Redux 采用了中间件机制,既保证了自身的最少代码量,又增加了可扩展性。下图为 Redux 的工作流程图。

    01_workflow

    工作流程

    1. View 层通过 dispatch 方法发出 action,通知 store 用户有新操作。
    2. store 收到通知会将处理权利交给 reducer,并传递给 reducer 两个参数:previousState 和 action,而 reducer 函数正是由你编写。
    3. reducer 针对收到的用户操作(action)进行对应的数据处理,最后将处理完生成的新数据返回给 store。
    4. store 收到新数据会通知 View 进行更新。

    细节梳理

    • action 和 actionCreator 只是两种写法,actionCreator 允许用户编写的 action 中携带的数据是个变量,因此更通用。
    • dispatch 方法使用了中间件机制,增强了 dispatch 功能,如在每次 dispatch 时打印日志。
    • 当用一个 reducer 来处理整个项目的所有 action 操作过于复杂时,可借助 combineReducers 分开处理。
    • state 是 store 在当前状态生成的数据对象。

    走进源码

    先看下文件目录,各文件的作用已在下图中标出。除内部工具函数外,本文会按照目录中的模块逐个梳理。同时,为保证文中能尽量多的留下一些干货,不去逐行梳理源码,会着重讲解结论和梳理代码常用技巧。

    02_files

    Index

    作为入口文件,抛出了 Redux 可以使用的全部 API(见下图)。可以看到,每个 API 对应目录中一个文件。其中 __DO_NOT_USE__ActionTypes 是内部使用的 actionTypes,是随机生成的,自定义的 actionType 基本不会和这个冲突,因此这个 API 一般是用来做判断的。

    03_indexexport

    代码技巧

    这里用到一个常用来判断代码混淆的技巧,通过定义一个空函数,随后判断这个空函数的 name 是否改变来确定代码是否进行了混淆。

    04_iscrushed

    CreateStore

    这是 Redux 最核心的一个 API。有多核心?它覆盖了 Redux 的整个工作流程(见下图),如果你想自己实现一个简单的 Redux,看这个 API 就足够了!

    05_createstoreflow

    createStore 用来生成 store,该方法的签名为 ( reducer, preloadedState?, enhancer? ) => {dispatch, subscribe, getState, replaceReducer, [$observable]}。下面分析参数和返回值用法以及在工作流中的作用。

    reducer参数: 是提供给用户实现根据发出的 action 更新 state 的函数,根据源码中调用方式(见下图)可知其签名为 ( previousState, action ) => newState,在工作流中注册了更新数据的函数等待被调用。

    06_reducerapi

    enhancer参数: 即 applyMiddleware() 返回的函数,createStore 方法中,有没有 enhancer 参数直接决定后面会走向哪里(见下图),但是不必恐慌,这只是为了提供给用户更多用法,即使这里 return 出去了,走一圈流程后,createStore 还是会返回上述签名中的内容,提供的更多用法会在 applyMiddleware 中说到。

    07_enhancer

    store.subscribe方法: 注册监听事件,并返回了取消监听的方法:( listener ) => unsubscribe,等待数据更新后被调用,在工作流中用来注册根据 state 变化来更新 View 的事件。

    store.getState方法: 获取当前 state 数据对象,签名为 () => currentState

    store.replaceReducer方法: 用来更新 reducer 参数传入的函数,签名为 ( nextReducer ) => undefined

    store.[$$observable]方法: 可以理解为 store.subscribe 的一种 observable 形式的封装,功能和 store.subscribe 一致,供相应工具使用。该方法使用并不多,不再赘述。

    store.dispatch方法: store 中最核心的方法,没有之一,打通了整个工作流程,其签名为 ( action ) => action。它首先调用了 reducer 参数传入的函数更新数据,随后遍历并调用了 store.subscribe 方法注册的监听事件,告知数据发生了变更。为了在使用 createStore 生成 store 时就直接生成一个初始 state 对象,这个方法内调用了一次 dispatch({ type: ActionTypes.INIT }),由于 ActionTypes.INIT 类型不存在于你定义的 reducer 中任何处理函数,因此会返回你定义的初始状态,这也是 preloadedState 参数很少使用的原因。

    使用注意

    • createStore 中直接调用 reducer 来生成数据并未额外操作数据,因此使用时需注意:
      • 返回的 newState 要和参数 state 没有引用关系。
      • 任何未知 action,必须返回当前状态(参数 state 获取到的状态)。若当前状态未定义,必须返回初始状态(自定义的 initState)。
    • isDispatching 规范了 reducer 函数中不允许使用 getState、subscribe 和 dispatch 方法。
    • store 并不储存所有数据,而是储存的更改数据的方法(reducer),因此生成初始数据需要内部先调用 dispatch({ type: ActionTypes.INIT }),并且在需要更新 reducer 时要调用 replaceReducer 才能更新。为保证数据实时性,更新 reducer 后需要再调用一次 dispatch({ type: ActionTypes.REPLACE }) 来更新数据。

    代码技巧

    技巧一: 有三个形参时,实现第二个形参可以选填:

    08_nonesecondparam

    技巧二: isDispatching 规范 reducer 中不允许使用 getState、subscribe 和 dispatch 的实现方法:
    关键代码在 dispatch 方法中(见下图),可以看到在开始执行 reducer 之前 isDispatching 先置为 true,一直到 reducer 全部执行完才会再设置为 false,随后只要分别在 getState、subscribe、dispatch 三个方法中判断当 isDispatching 为 true 时抛出异常。

    09_isdispatching

    ApplyMiddleware

    这是很重要的一个工具方法,代码量很小,但无论从代码技巧上还是使用地位上都很重要。

    Redux 中间件的作用实际是增强了 dispatch 功能。看源码最后 return { …store, dispatch} 可以知道是用增强后的 dispatch 替代了原来的 store.dispatch。下图为 applyMiddleware 在工作流中的作用。

    10_applymiddlewareflow

    用法

    根据 applyMiddleware 的调用方式再结合 createStore 中提到的 enhancer 参数总结一下用法。

    1. const store = createStore(reducer, applyMiddleware(…middlewares))
    2. const store = createStore(reducer, {}, applyMiddleware(…middlewares))
    3. applyMiddleware(…middlewares)(createStore)(reducer, preloadedState)

    同时还可以借助 Redux 提供的 compose 方法来使用,只需将上述用法中的 applyMiddleware(…middlewares) 替换成 compose(applyMiddleware(middleware1),…,applyMiddleware(middlewareN)) 即可。

    注意一种错误用法,该用法在 createStore 中做了限制:
    createStore(reducer, applyMiddleware(middleware1), applyMiddleware(middleware2), …)

    写法

    applyMiddleware 源码中调用中间件的方法见下图。

    11_callmiddlewares

    其中用到了 compose 方法,这个方法是Redux的一个API,后面详细讲解,调用 compose(middleware1, middleware2, middleware3)(store.dispatch) 相当于调用 middleware1( middleware2( middleware3( store.dispatch ) ) )

    现在我们以源码里中间件的调用方式倒推 middleware 的写法。

    1. 源码中 compose(middleware1, middleware2, middleware3)(store.dispatch)middleware1( middleware2( middleware3( store.dispatch ) ) ) 调用后生成了新的 dispatch,所以 middleware 函数应是 (next) => dispatch(next) => (action) => action, 其中 next 是上一个 middleware 的返回值。

    2. middleware 函数在传入 compose 之前用 middleware(middlewareAPI) 获得了 dispatch 和 getState 供中间件内部使用,因此还需要能接收这个参数,于是:
      ({dispatch, getState}) => (next) => (action) => action

    没错,到这里 middleware 函数就已经写完了!

    可以看到 middleware 的写法 ({dispatch, getState}) => (next) => (action) => action 是一种典型的柯里化函数,柯里化函数很适合做偏底层一些的函数抽象,方便再次封装时拿到任何一层的返回结果去做相应操作,随后也可选择是否再继续调用、何时调用。另外通过 middleware 结合 compose 的使用知道,柯里化函数很方便做函数组合。

    最后,放出一张 redux-thunk 中间件源码截图,供大家检验。看 redux-thunk 又有一个问题需要解决:刚刚说 next 是上一个中间件返回的 dispatch,同时 middlewareAPI 中也有 dispatch。这两个在 redux-thunk 中都用到了,那区别是什么呢?下面代码技巧中解决这个问题。

    12_reduxthunk

    代码技巧

    细心的同学可能发现了 middlewareAPI 中的 getState 就是 store.getState,而 dispatch 却是一个抛出异常的新函数!

    13_dispatcherror

    另外, 为什么不直接将 dispatch 变量当成参数,而要再包一层函数呢?

    14_middlewareapi

    其实 dispatch 的这两种定义方式的区别就在于赋值时机不同:

    • { dispatch: dispatch } 这种方式定义,MiddlewareAPI.dispatch 被赋值的永远是抛了一段异常的 dispatch 变量。

    • { dispatch: (...args) => dispatch(…args) } 这种方式定义,函数内的 dispatch 只有在执行 MiddlewareAPI.dispatch 时才会去找此时 dispatch 变量到底是哪个函数。第一次执行中间件操作时,在中间件内部使用的 dispatch 变量是那个只抛了异常的函数。第一次执行完毕后,由上图可以看到 dispatch 会被重新赋值,因此当用户调用从 redux-thunk 传出的 dispatch 时,已经是增强后的 dispatch 了。

    多个中间件调用顺序

    直接看一个小 demo,执行顺序已用序号注释在后面,也可用断点看执行顺序。

    15_middlewareorder

    通过 demo 可以看出,会先执行 f 中间件中的 fFn 再执行 g 中的 gFn。同时 gFn 的执行是需要在 f 中间件中调用 next(action) 才能执行到。如果你也想开发中间件,不要忘记这点。

    compose

    在 applyMiddleware 中我们知道调用 compose(middleware1, middleware2, middleware3)(store.dispatch) 相当于调用 middleware1( middleware2( middleware3( store.dispatch ) ) )。compose 源码中只用了数组的原生方法 reduce 就优雅的解决了函数层层嵌套的问题(见下图)。

    20_compose

    reduce 的基本用法可以参看 MDN,里面的小例子也很好。

    下面为源码中 reduce 解决函数嵌套的运行原理,其中 m1,m2,m3,m4 为中间件。

    21_composeapply

    这对我们平时写代码是个很好的启发,reduce 方法还很擅长做求和、去重、数组扁平化、数据分类等复杂操作,可以替代很多递归操作来实现功能(参见MDN中的小例子)。下面将用多种方法实现异步回调,也可以实现依次执行动画的需求,一起来感受一下reduce写法的简介吧。

    // 写法一:回调嵌套写法
    function fn0() { // 此写法不易阅读且无法封装函数实现无限嵌套
        console.log(0)
        setTimeout(function fn1() {
            console.log(1)
            setTimeout(function fn2(){
                console.log(2)
                setTimeout(function fn3(){
                    console.log(3)
                }, 3000)
            }, 2000)
        }, 1000)
    }
    
    fn0()
    
    // 写法二:递归实现
    function fn0(next) {
        console.log(0)
        setTimeout(next, 1000)
    }
    function fn1(next) {
        return () => {
            console.log(1)
            setTimeout(next, 2000)
        }
    }
    function fn2(next) {
        return ()=>{
            console.log(2)
            setTimeout(next, 3000)
        }
    }
    function fn3(next) {
        return ()=>{
            console.log(3)
        }
    }
    
    function recursiveReduce (...fns) { // 用递归实现源码中 compose 方法
        if (fns.length < 2) { // 空数组或只有一个元素
            return fns[0]
        }
        return recursiveReduce((...args) => fns[0](fns[1](...args)), ...fns.slice(2))
    }
    
    recursiveReduce(fn0, fn1, fn2, fn3)()
    
    // 写法三:reduce 写法,其中 fn0, fn1, fn2, fn3 函数定义复用写法二的
    function compose(...fns) { // 与源码中的 compose 方法一致
        return fns.reduce((a, b) => (...args) => a(b(...args)))
    }
    
    compose(fn0, fn1, fn2, fn3)()
    
    // 写法四:自定义 next
    function fn0(next){
        console.log(0)
        setTimeout(next, 1000)
    }
    function fn1(next){
        console.log(1)
        setTimeout(next, 2000)
    }
    function fn2(next){
        console.log(2)
        setTimeout(next, 3000)
    }
    function fn3(next){
        console.log(3)
    }
    
    function nextFn(...fns) {  // 借鉴 express 中间件实现方法
        let i = 0;
        function next() {
            const fn = fns[i++];
            if (!fn) return
            fn(next)
        }
        next()
    }
    
    nextFn(fn0, fn1, fn2, fn3)
    
    

    测试一下几种写法的运行速度(见下表),由快到慢依次是:回调嵌套写法、自定义 next、reduce 写法、递归实现。

    Chrome 中:

    写法 运行速度(ops/sec)
    回调嵌套写法 56,919
    递归实现 44,346
    reduce 写法 47,450
    自定义 next 49,950

    Firfox 中:

    写法 运行速度(ops/sec)
    回调嵌套写法 10,660
    递归实现 8,411
    reduce 写法 8,411
    自定义 next 9,095

    Safari 中:

    写法 运行速度(ops/sec)
    回调嵌套写法 298,880
    递归实现 117,379
    reduce 写法 161,663
    自定义 next 220,944

    CombineReducers

    该方法代码虽多但只做了一件事,就是允许你定义多个 reducer 函数然后帮你合并成一个,在工作流中的作用见下图。

    16_combinereducersflow

    既然是要合并 reducer,那么合并后的函数也要和 reducer 写法一致。因此:
    comineReducers: (reducers) => (state, action) => newState
    这里的 reducers 是一个对象,如 { a: reducer1, b: reducer2 }。不过一般都会让 key 和 value 的函数名一致,es6 语法即可写为 { reducer1, reducer2 },核心代码如下:

    17_combinereducerscore

    从图中可看到直接调用了 reducer,并未对生成数据做其他处理(这和 createStore 中调用 reducer 是一致的),同时 hasChange 只做了浅比较,这样一来我们编写的时候需要注意什么呢?浅比较又有什么好处呢?不妨接着看完使用注意。

    使用注意

    • 通过 combineReducers 合并会使生成的 state 对象树在顶层增加一层。
    • 代码中有大量校验,其中就限制了编写的 reducer 不能返回 undefined,如果想清空数据返回 null,想还原数据返回原来的 state。
    • 源码中 comineReducers 生成的 rootReducer 被执行的时候会依次执行每个 reducer。当 reducer 中有两个方法都处理了同一个 action,那么这两个处理方法都会被执行。为避免这这种不确定性可能导致的 bug,将所有 action.type 的字符串都统一定义在一个文件中是很有帮助的,当然这样做还有其他好处。
    • 源码在进行 hashChange 判断时,对每个 reducer 生成的数据都是进行的浅比较,最后通过 hasChange 判断应返回 nextState 还是 state。因此如果 state 发生了变更,要保证 reducer 返回的 state 和原 state 没有引用关系,否则无法更新。另外这里用浅比较的好处是如果没有更改或者没有命中任何 action 处理方法返回原 state,这样可以避免更新提高性能。

    BindActionCreators

    该方法代码量少做的事情也简单,目的是优化 store.dispatch(actionCreator(data)) 这种调用方式,下图为优化前后使用姿势对比。

    18_bindactioncreatorsapi

    根据上面的对比很容易得出结论,这个方法只是将 store.dispatch 调用进行了封装,简化了调用写法,核心代码见下图:

    19_bindactioncreatorscore

    总结

    本文围绕源码实现技巧和使用注意事项展开,希望尽可能给小伙伴们提供一些思想上的启发和开发上的帮助。阅读源码就像读一本好书,每次阅读都会有不同的收获。在这个过程中,我总结了自己的阅读源码的方法供大家参考。

    1. 准备一个使用源码的 demo,随时用来运行调试,大部分库也可以选用它本身提供的例子。
    2. 快速梳理清楚源码的结构及每部分功能的大致位置。
    3. 明确目标,想好自己看完代码想有什么结论,决定切入点。阅读源码时从不同切入点去读最后都会有不一样的收获,在这个过程中也会慢慢熟悉整个源码的设计思想及编写者的习惯。
    4. 第一遍阅读时如果有什么猜想及时记录下来但不去马上投入研究,要紧跟能达到你目标的骨干流程去看。未来这些猜想会成为对源码理解升华的必要条件。
    5. 看源码过程中充分利用函数名,对象名,类名等快速对每一小段代码有个初始定位。遇到复杂部分可以直接 debug 执行流程,也可以借助注释的帮助,还可以自己尝试去一步步实现一下基本流程,一个好的库在各种名字和注释方面也是做的很好的,因此在写自己代码的时候也尽量去做好这部分工作,降低阅读和维护成本。

    Redux 源码设计上采用了很多函数式编程的思想,以后还会继续研究函数式编程相关内容,这对改善代码设计很有帮助,欢迎有兴趣的小伙伴一起交流。

  • 相关阅读:
    Centos6.5环境中安装vsftp服务
    MySQL数据库的数据备份和恢复(导入和导出)命令操作语法【转】
    linux系统被入侵后处理经历【转】
    Linux lsof命令详解和使用示例【转】
    Oracle 表空间和用户权限管理【转】
    如何在 Linux 中找出最近或今天被修改的文件
    Linux 服务器系统监控脚本 Shell【转】
    1张图看懂RAID功能,6张图教会配置服务器【转】
    简析TCP的三次握手与四次分手【转】
    TCP协议中的三次握手和四次挥手(图解)【转】
  • 原文地址:https://www.cnblogs.com/bldxh/p/10316425.html
Copyright © 2011-2022 走看看