-
Notifications
You must be signed in to change notification settings - Fork 53
深入到源码:解读 redux 的设计思路与用法 #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
awesome |
👍 总结的很深入,学习了 |
漂亮,先顶后看 |
👍 |
学习了 |
另外一处
|
@think2011 感谢指正。 |
已存 |
楼主总结的很棒,对于初学者来说是一篇很好的文章,我推荐给我很多朋友看了,觉得非常棒 |
This was referenced Sep 18, 2016
非常棒啊!! |
收藏! |
Mark! |
mark |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
前言
redux 是 facebook 提出的 flux 架构的一种优秀实现;而且不局限于为 react 提供数据状态处理。它是零依赖的,可以配合其他任何框架或者类库一起使用。要想配合 react ,还得引入 react-redux。
redux 团队的野心比较大,并不想让 redux 局限于 react 生态链中的一环。他们让 redux 自身保持简洁以便适配各种场景,让社区发展出各种
redux-*
中间件或者插件,从而形成它自己的生态系统。redux 的核心很简洁。这篇文章将专注于解读 redux 核心的设计思路,以 Isomorphism-react-todomvc 这个项目为示例。它很可能是目前实现最完备的 react/redux todomvc demo,包含 react/redux 服务端渲染、路由filter、websocket同步等。
可以猛戳 DEMO 地址。在控制台里能看到
redux-logger
中间件输出的 action 日志,它们清晰地反映了业务逻辑是怎样的 。如果有其他人在编辑 todolist,基于 websocket 服务端推送技术的支持,你也可以直接看到别人的操作过程。理念与设计
为什么要有 action ?
每个 web 应用都至少对应一个数据结构,而导致这个数据结构状态更新的来源很丰富;光是用户对 view 的操作(dom 事件)就有几十种,此外还有 ajax 获取数据、路由/hash状态变化的记录和跟踪等。
来源丰富不是最可怕的,更可怕的是每个来源提供的数据结构并不统一。DOM 事件还好,前端可以自主控制与设计; ajax 获取的数据,其结构常常是服务端开发人员说了算,他们面对的业务场景跟前端并不相同,他们往往会为了自己的便利,给出在前端看来很随意的数据结构。
即便是最专业的服务端开发人员,给出最精准的
restful
数据,它也会包含meta
数据,表明此次返回是否存在错误,如果存在错误,则提供错误信息。除非是 facebook 最近提出的graphql
+relay
模式,不然我们总得对各个来源的数据做一个前期处理。我们得用专门的处理函数,在各个数据来源里筛选出我们真正需要的数据,不把那些无关紧要的、甚至是脏的数据污染了我们的全局数据对象。
这种对数据来源做萃取工作的函数,就叫
action
。它叫这个名字,不是因为它「数据预处理」的功能,而是在 web 应用中所有的数据与状态的变化,几乎都来自「事件」。dom 事件,ajax 成功或失败事件,路由 change 事件, setTimeout 定时器事件,以及自定义事件。任意事件都可能产生需要合并到全局数据对象里的新数据或者线索。action 跟 event (事件)并不等同。比如在表单的
keyup
事件中,我们只在e.keyCode
等于回车键或者取消键时,才触发一类action
。dom 事件提供的数据是 event 对象,里面主要包含跟 dom 相关的数据,我们无法直接合并到全局数据对象里,我们只将感兴趣的部分传入 action 函数而已。所以,是 event 响应函数里主动调用了 action 函数,并且传入它需要的数据。
为什么要有 reducer ?
action 仅仅是预处理,将脏数据筛选掉,它未必产生了可以直接合并到全局对象的数据与结构,它甚至可能只是提供了线索,表示「需要获取某某数据,但不在我这儿」。action 函数的设计,也为它「只提供线索」的做法提供了支持,action 函数必须返回一个带有 type 属性的
plain object
。如上所示,action 函数的设计理念如下:
reducer
就是迎接 action 函数返回的线索的「数据再处理函数」, action 是「预处理函数」。因为 action 返回的数据有个固定的结构,所以 reducer 函数也有个固定结构。
如上所示,每个 action.type 的 case (A/B/C),都有一个专门对应的数据处理函数 (handleA/handleB/handleC),处理完之后返回新的 state 即可。
reducer 只是一个
模式匹配
的东西,真正处理数据的函数,是额外在别的地方写的,在 reducer 中调用罢了。reducer 为什么叫 reducer 呢?因为 action 对象各种各样,每种对应某个 case ,但最后都汇总到 state 对象中,从多到一,这是一个减少( reduce )的过程,所以完成这个过程的函数叫
reducer
。为什么要有 combineReducers ?
reducer 的第一个参数是全局 state 对象。你想想看,全局意味着什么?
state 对象的树形结构必定会随着 web 应用的复杂性而变得越来越深。当某个
action.type
所对应的 case 只是要修改state.a.b.c.d.e.f
这个属性时,我的handleCase
函数写起来就非常难看,我必须在这个函数的头部验证 state 对象有没有那个属性。我们需要这种模式:
如上所示,写起来是普通的 reducer ,但拿到的不是全局
state
。实现方法很简单,遍历一个「全是方法」的「函数储存对象」,返回新对象,这个新对象的
key
跟「函数储存对象」一样,它的value
则是「函数储存对象」的同名方法接受(state[key], action)
参数的返回值。redux 的 combineReducers 源码如下:
相信你也注意到了,
mapValue
只是一级深度的映射,目前redux
并没有提供简便的映射到state.a.b
一级以上深度的 state 的方法。这是它目前的不足之处。设想我们做一个移动端 webapp,有很多个 view 在单页中,比如 index 页,list 页,detail 页,redux 提供的「一级分解」一下子就让各个 view 消耗完了,如果还要分割每个 view 的 state ,看起来会很麻烦。
在这里提供几个解决思路:
第一个方案是
superGetter/superSetter
,第二个方案是嵌套
combineReducers
。需要注意的是,
redux
的combineReducers(reducers)
的返回值 rootReducers, 总是返回新的 state,它不是修改旧 state,而是创建空对象,然后将 key/value 往上面挂载。只有在reducers
对象上的 key 才会被迁移。也就是说:第三个方案更为激进,目前
redux
没有提供,需要修改其源码。这个模式是transformer
(转换器)有了上面的修改,我们就可以针对 action.type 来选择全局 state 的更新路径了。
如上所示,有了 transformers,不必再嵌套 combineReducers。不过,换个角度看,这个模式只是将原本要在 handleA(state) 里要做的属性查询工作,搬到了 transfromer 中,让 handleA 可以直接处理它需要的 state 对象而已。
总的而言,
combineReducers
不是一个必需品,它只是用来分发全局对象的属性到各个reducer
中去,如果你觉得它太绕,你可以选择直接在每个handleCase
函数中查询 state 属性,合成 newState 并返回即可。这时候,你只需要一个 reducers 函数,它的switch
语句处理所有可能的action.type
;想想就是一个超长的函数。为什么要有 createStore ?
既然
redux
建议只维护一个全局 state ,为什么要搞一个createStore
函数呢?直接创建一个空对象,然后缓存起来,不断投入到reducer(state, action)
更新状态不就行了?这会儿该说到「函数式编程」里的几个概念了。「无副作用函数」与「不变值」。
上面提到的 action 跟 reducer 函数,都是普通的纯函数。对于 action 函数 来说,输入相同的参数无限次,它的返回值也相同。而有了「不变值」,我们得到的好处是,在 react component 的 shouldComponentUpdate(nextProps, nextState) 里,可以直接拿当前 props 跟 nextProps 做
===
对比,如果相等,说明不用更新,如果不相等,则更新到视图。如果不是返回新 state,只是修改旧 state,我们就很难做到「回退/撤销」以及跟踪全局状态。对比两个数据是否一致,也无法用
===
,而得用deepEqual
深度遍历来对比值,很耗费性能。另外, 上面提到的 action 函数,它只是返回一个
plain object
而已,除此之外,它什么也没做。是谁把它传递到reducers(state, action)
调用?reducers|state|action
这三个东西由谁来协调?此时,
createStore(reducer, initialState)
呼之欲出;它接受一个 reducer 函数跟 initialState 初始化的全局状态对象,返回几个「公共方法」:dispatch|getState|subscribe
。这里我只列举了对我们有重要意义的三个,还剩两个不太重要,可自行参考redux
文档。createStore
做的事情在《Javascript 高级程序设计》一书里有讲解,很简明易懂。如上所示,
redux
返璞归真的核心代码,没有什么原型继承、面向对象这类绕来绕去的事物。createStore
的返回值是一个对象,通常我们保存在store
这个变量名里。其实 store 是一个只有方法,没有数据属性的对象,用JSON.stringify
去系列化它,得到的是空对象。真正的state
包含在闭包中,通过公有方法getState
来获取。而
dispatch
方法,是store
对象提供的更改currentState
这个闭包变量的唯一建议途径。注意,我是说唯一建议,不是说唯一途径,因为 getSate 拿到的是 currentState 的对象引用,我们还是可以在外头改动它,虽然不建议。subscribe
方法是一个简单的事件侦听方法,在 dispatch 里更新完 currentState 后调用,不管是什么 action 触发的更新他,它都会调用,并且没有任何参数,只是告诉你state
更新了。这个方法在后面的提到的服务端同构应用之「镜像 store 」中有妙用。至此,
createStore
与store
的全部重要内容都揭示了,它们就是如此简洁。为什么要有 bindActionCreators ?
通过
createStore
我们拿到了store
, 通过store.dispatch(action)
我们可以免去手动调用reducer
的负担,只处理action
就可以了,一切都很方便。只是,有两种意义上的action
,一种是action
函数,另一种是action
对象,action
函数接受参数并返回一个action
对象。action
函数是工厂模式,专门生产action
对象。所以我们可以通过重新命名,更清晰的区别两者,action
函数就叫actionCreator
,它的返回值叫action
。store.dispatch(action)
这里的 action 是一个对象,不是函数,它是 actionCreator 返回的,所以实际上要这样调用store.dispatch(actionCreator(...args))
,很麻烦是吧?原本的
reducer(state, action)
模式,我们用createStore(reducer, initialState)
转换成store.dispatch(action)
,现在发现还不够,怎么做?再封装一层呗,这就是函数式思想的体现,通过反复组合,将多参数模式,转化为单参数模式。怎么组合?
对于单个 actionCreator ,我们可以轻易地
bindActionCreator
。对于多个 actionCreator,我们可以像
reducers
一样,组织成一个key/action
的组合嘛。如上所示,我们用
bindActionCreators
得到了真正具有改变全局 state 能力的许多函数,剩下的事情,就是将这些函数分发到各个地方,由各个event
自主调用即可(正如在「为什么需要 action ?」 一节里介绍的)。redux 工作流程是怎样的?
至此,我们来梳理一下,
actionCreator|reducer|combineReducers|createStore|bindActionCreators
这些函数的书写与组合的过程以及顺序。首先,我们要先设计一些「常量」,因为 action.type 通常是字符串常量。为了便于集中管理,以及利于压缩代码,我们最好将常量放在单独的文件夹里,根据类型的不同放置在不同的文件中。
以 [Isomorphism-react-todomvc] 为例,
constants
(中译:常量)文件夹里有如下文件:我们的「常量设计」,可以清晰地反应我们整个 web 应用的业务架构设计;这方面没弄好,随着应用的复杂性增加,会越来越难以维护。当然,比设计常量更靠前的是,设计整个应用的 state 树的结构,这方面不同业务有不同的设计思路,这里无法多做介绍。
由于 todomvc 的业务逻辑很简单,所以它的 state 设计是这样的:
有了常量,我们就可以写
actionCreator
了,它们被放置在 actions 文件夹里。action 是预处理,下一个环节是再处理函数
reducer
,它们被放置在reducers
文件夹里。如上所示,
todos.js
负责处理state.todos
属性,filter.js
负责处理state.activeFilter
属性,所以我们需要用combineReducers
将它们组织起来。目前我们有了
actionCreators
(就是在 actions 文件夹下的 index.js 的模块输出) 以及rootReducer
函数(就是上面reducers/index.js
的模块输出),接下来,就是用createStore
把rootReducer
给吞掉。我们调用
createStore
拿到 store 之后,就拿到了store.dispatch
,然后用bindActionCreators
将actionCreators
对象跟dispatch
,粘合在一起。如果你用
react-redux
,你就用它提供的<Provider></Provider
+connect
组织单项数据流。如果你只用
redux
,你可以封装一个render
函数,在store.subscribe
事件回调里使用。如下所示:如上所示,组织
redux
的流程莫过于:为什么需要 applyMiddlewares ?
reducer(state, action)
这个调用方式所反映的reducer
跟action
的关系很近,action 就是 reducer 的第二个参数嘛。然而,上面所示的redux
流程上看,它们却隔着createStore|store.dispatch|bindActionCreators
三个 API ,才最后汇集到一处。当我们失去对
reducer
的直接控制权之后,这意味着我们的调试不方便了。原本我们可以像下面那样做:就算只是为了调试代码,打印出 action 日志,我们也值得设计解决方案。
applyMiddlewares
就是一个有用的思路。它的原理很简单,在《JavaScript 高级程序设计》里也有提到,就是模块模式。然后我们可以这样写中间件了。
注意,在每个中间件里存在两个
dispatch
功能。一个是{ dispatch, getState }
,这是在middlewareAPI
对象里的 dispatch 方法,另一个是next
,它是chain
链条的最后一环dispatch = compose(...chain, dispatch)
。如果你不想在将 action 传递到在你之后的中间件里,你应该直接显式地调用
dispatch
,不要调用next
。如果你发现这个 action 对象不包含你感兴趣的数据,是你要忽略的 action,这时应该传给 next,它可能是其他中间件的处理目标。redux 的中间件模式,将 dispatch 的步骤拉长并且细化,使得我们可以处理更多类型的 action,比如带函数的,比如带 promise 的等等,我们可以在真正的 store.dispatch 调用之前,先把看似不合格的 action 对象消化掉,吐出 store.dispatch 能直接调用的数据结构即可。
除了这个
rerdux-thunk
(上面的示例真是它的源码,不信请点击这里)中间件之外,还可以写很多不同类型的,其中 redux-logger 就是一开始说的对调试 redux 代码很必要的中间件。redux 服务端渲染怎么处理?
如果你现在(2015.08.24)去 redux 的官方文档里查阅,你会发现,
server rendering
这一块还是不可点击的空白状态。然而,我们既然已经深入到源码层次,自己找出一条途径,也是可以的(只适用于 node.js)。首先,一开始我们就说过,
redux
是无依赖的,所以它可以直接用在 node.js 运行时里。关键在于react-redux
的服务端渲染方式,它所提供的connect
与Provider
组合,扰乱了我们对 react component 的掌控与认知。我目前的做法是,将跟
redux
有关的所谓的smart component
(智能组件),放到containers
文件夹里,普通的 react component,放在components
文件夹里,在客户端时,我们渲染containers
里的redux
组件。而在服务端,我们渲染普通组件,redux
组件要传的参数,我们一一构造出来即可。具体实现可以参考 Isomorphism-react-todomvc 项目。
镜像 store 模式
这里介绍的所谓镜像 store 模式,并非 redux 官方文档里提到的,而是在实践过程中我所发现的有趣用法,大家看看就好,仅供参考,不要误以为是官方推荐模式即可。
思路很简单,既然每个 actionCreator 返回的都是 plain javascript object,它们都是可以被
JSON.stirngify
系列化的。也就是说可以post
到服务端,如果服务端也有一个同样的 store,它 store.dispatch 一下,不就跟客户端一致了?这样的话,我们只需要传更轻量的
action
数据,这种做法犹如graphql
一般。另外,在服务端的 store.subscribe 中我们绑定一个 websocket.emit 函数,就可以把服务端根据 action 所做的数据更新同步到所有浏览器端了。结尾
这篇文章写得长了,就此结尾罢。
总体而言,
redux
是一个优秀的新技术,厉害到自己开辟新生态,不容小觑。它也有一些缺陷,比如不容易处理 state 的深度 path 路径问题,比如分发太多 action 到 react component 时,一层层验证 propTypes 的繁琐(虽然别的 flux 实现也有这个问题)等。比起其他 flux 模式, redux 已然优越。推荐使用。
The text was updated successfully, but these errors were encountered: