前言

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

用法简述

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

工作流程

  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 在当前状态生成的数据对象。

走进源码

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

Index

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

代码技巧

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

CreateStore

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

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

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

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

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 }) 来更新数据。

代码技巧

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

技巧二: isDispatching 规范 reducer 中不允许使用 getState、subscribe 和 dispatch 的实现方法:

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

ApplyMiddleware

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

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

用法

根据 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 源码中调用中间件的方法见下图。

其中用到了 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 中都用到了,那区别是什么呢?下面代码技巧中解决这个问题。

代码技巧

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

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

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

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

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

多个中间件调用顺序

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

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

compose

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

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

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

这对我们平时写代码是个很好的启发,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 函数然后帮你合并成一个,在工作流中的作用见下图。

既然是要合并 reducer,那么合并后的函数也要和 reducer 写法一致。因此:

comineReducers: (reducers) => (state, action) => newState

这里的 reducers 是一个对象,如 { a: reducer1, b: reducer2 }。不过一般都会让 key 和 value 的函数名一致,es6 语法即可写为 { reducer1, reducer2 },核心代码如下:

从图中可看到直接调用了 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)) 这种调用方式,下图为优化前后使用姿势对比。

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

总结

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

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

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

从Redux源码探索最佳实践的更多相关文章

  1. Redux源码分析之createStore

    接着前面的,我们继续,打开createStore.js, 直接看最后, createStore返回的就是一个带着5个方法的对象. return { dispatch, subscribe, getSt ...

  2. Redux源码分析之applyMiddleware

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  3. Redux源码分析之基本概念

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  4. Redux源码分析之bindActionCreators

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  5. Redux源码分析之combineReducers

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  6. Redux源码分析之compose

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  7. redux源码解读

    react在做大型项目的时候,前端的数据一般会越来越复杂,状态的变化难以跟踪.无法预测,而redux可以很好的结合react使用,保证数据的单向流动,可以很好的管理整个项目的状态,但是具体来说,下面是 ...

  8. Eureka源码探索(一)-客户端服务端的启动和负载均衡

    1. Eureka源码探索(一)-客户端服务端的启动和负载均衡 1.1. 服务端 1.1.1. 找起始点 目前唯一知道的,就是启动Eureka服务需要添加注解@EnableEurekaServer,但 ...

  9. Redux源码学习笔记

    https://github.com/reduxjs/redux 版本 4.0.0 先了解一下redux是怎么用的,此处摘抄自阮一峰老师的<Redux 入门教程> // Web 应用是一个 ...

随机推荐

  1. 第一百三十八节,JavaScript,封装库--插件

    JavaScript,封装库--插件 库主要是用来封装一般JavaScript的常规操作代码,而拖拽这种特效代码属于功能性代码,并不是必须的,所以这种类型的代码,我们建议另外封装,在需要的时候作为插件 ...

  2. 2017 ACM区域赛(南宁站) 参赛流水账

    day0: 早上四点起床赶飞机,还好没有吵醒室友导致被打死.本来想在飞机上准备一下下周的小测,结果飞机一点都不平稳,只能全程和队友吹逼聊天.下午在宾馆里和johann通关了一部合金弹头,重温了童年的经 ...

  3. EntityFramework :数据库创建

    控制数据库的位置 默认情况下,数据库是创建在localhost\SQLEXPRESS服务器上,并且默认的数据库名为命名空间+context类名,例如我们前面的BreakAway.BreakAwayCo ...

  4. "_dns_free_resource_record", referenced from:问题

    本文转载至 http://blog.csdn.net/woaifen3344/article/details/41309471 _dns_free_resource_r_dns_free环信SDK集成 ...

  5. sizeWithFont:方法使用明细

    个人总结: Computing Metrics for a Single Line of Text– sizeWithFont: 同下面,换行方式默认取NSLineBreakByWordWrappin ...

  6. uiautomatorviewer.bat使用方法

    在android目录下找到uiautomatorviewer.bat,然后双击,页面的第二个按钮连接设备 D:\Program Files\android-sdk-windows\tools\uiau ...

  7. tsinsen A1333. 矩阵乘法

    题目链接:传送门 题目思路:整体二分(二分的是答案,附带的是操作) 把矩阵中的元素对应成插入操作,然后就有插入和询问操作. 然后根据插入操作对于答案的影响,询问操作所匹配的符合答案个数,将操作分为两段 ...

  8. 2154: Crash的数字表格

    2154: Crash的数字表格 Time Limit: 20 Sec  Memory Limit: 259 MBSubmit: 3372  Solved: 1258[Submit][Status][ ...

  9. 隐藏内容但仍保持占位的css写法

    通常显示和隐藏内容都会用display:block;和display:none; 如果想要保持内容的占位可以用visbility:visible; 和visiblity:hidden;来控制内容的显示 ...

  10. 【MarkDown】使用Html样式和折叠语法

    MarkDown很方便,但基本语法有些不足:比如无法使用折叠语法,无法让文字有不同的颜色. 这些功能可以实现,不过需要使用Html语法进行扩展.这篇文章主要是整理一下这些技巧,方便更好的使用. 一.折 ...