You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
接着我们需要将 addItem 函数变得更通用,其实它就是对 state 中的 list 属性数据进行操作,那么其实可以改为:
function handleList(params) {
let { type, payload } = params;
swtich (type) {
case 'ADD_ITEM':
let list = state.list.slice();
list.push(payload);
return {
...state,
list
};
case 'UPDATE_LIST':
....
}
}
MV* 模式
在前端开发领域,MV* 模式是被广泛使用的。比如 MVVM 就是基于 MVC 演变出来的,它提供了 View-Model 模块,自动地将 Model 模块中的数据与 View 模块中视图进行绑定,省却了很多手动更新视图等的操作。
但是 MV* 模式也存在很多问题,MVC 模式本身,由于它的实现太过自由,导致很多人在使用的时候,稍微不注意就会出现一些误用。这些误用可能在初期看不出问题,但是到了后期维护会难以拓展并且导致一些问题。比如一个常见的误用就是在 Controller 中同时实现了对视图的更新与数据层 Model 的数据同步两个操作,这样不仅违背了单一职责原则,还违背了数据流单一方向的原则。模块职责不单一,后期在拓展性与维护性就比较差。如果数据流不是单一流向就比较难做事件回溯,而且修改数据的“入口” 比较多,导致数据层比较混乱,而且在面对复杂业务的时候,有时候我们不仅需要关心数据本身,而且需要关心这个数据是从前一个状态经历了怎么样的变化变成了现在这样的状态。
Flux 与 Redux
面对上面的问题,Facebook 提出一个新的应用架构——Flux。在我看来,Flux 是一个更规范化的 MVC 架构,它规范了如何去修改数据层的数据,然后数据层的数据发生变化之后,统一通过事件分发来更新视图层,这样就消除了上面所说的数据流的单一流向问题,在解决这个问题后,引入 Event Souring 即事件回溯,记录数据变化的事件而不仅是数据本身,来达到更好地复现应用的问题的目的。而 Redux 是基于 Flux 架构又做了一点优化的架构实现。至于他们的具体区别这里不累述,推荐一篇文章 https://zhuanlan.zhihu.com/p/20263396,有兴趣的读者可以阅读一下。
Redux 是一个纯粹的状态管理库,它与 react 没有关系,可以与任何框架或者库进行配合使用。当你的组件间数据传递的方向与方式变得复杂的时候,redux 或许就是你的解决方案。
再造 Redux
"What I Cannot Create, I Do Not Understand"。
情景再现
假设我们现在有一个 ToDoList 的应用:
我们会看到代码中会存在很多问题,比如 addItem 函数不应该掺杂了像 renderList 这样的代码,这样代码的职责不单一,代码耦合度比较高。按照逻辑,addItem 只需要关心修改的数据,而不需要关心这个数据修改之后 UI 层面会做怎么样的变化。
一开始可能只有 renderList 这一个函数,但是随着应用的复杂度上升,数据修改之后需要重新渲染的地方可能会变得更多,所以需要将数据的变化与 UI 层变化做一个自动的关联,因此观察者模式是一个很自然而然的引入:
从上面的代码可以看到,当整个 state 的值改变的时候,就会触发监听者回调函数。有了这个机制之后,原来的 addItem 函数就可以变成:
看起来与原来变化不大甚至更复杂了,但是拓展性好了很多,比如 state 中的 list 属性不仅被 renderList 需要,而且被 renderListFirstItem 需要, 那么这个时候只需要:
此时可以调用多个渲染函数,addItem 函数对此无感知。但 addItem 函数现在看起来还是很累赘,还能再度简化吗?答案当然是可以的。
既然上面说到两状态值之间需要保持独立,那么是不是让 addItem 函数的返回值变成状态值,然后将这个返回值覆盖原 state 就可以了?代码如下:
经过修改之后,addItem 函数与 changeState 函数解耦了,然后我们只需在调用方式上做一些修改:
接着我们需要将 addItem 函数变得更通用,其实它就是对 state 中的 list 属性数据进行操作,那么其实可以改为:
函数调用就会变为:
到这一步,状态管理已经初具雏形了,但还是存在代码通用性以及 state 仍是一个全局变量,容易被其他代码进行修改等问题,我们将上面代码封装成一个库。
createStore
像刚刚所说,我们需要一个用于管理 state 的对象,那么就取名为 store。我们来实现创建 store 的函数:
由于现在 state 是一个局部变量,如果函数需要修改 state,那么就需要将这个函数传入到 createStore 里面,那么函数就变成了:
根据前面的章节,我们触发 state 某些属性值的修改是通过有个固定属性 type 来说明操作类型加上 payload 对象装载其他参数的对象的形式的:
而这种形式我们称之为 Command 即命令,上面这个对象我们称之为 Action。根据前面的代码,对于原本接收这个对象的函数----changeState 函数,我们称之为 dispatch 函数(分发器)。(注:概念来自 CQRS 架构)
那么我们将一开始所说的事件监听者模式与 dispatch(changeState) 函数封装到 createStore 里面:
至此,我们已经实现了一个极简版本的 redux。
通过这样,拓展性与数据的单向流动性都得到了实现。像前面章节说的,拓展性是通过事件监听者模式来实现的,现在当数据发生变化时,触发其他模块函数的执行只需要通过 subscribe 来达到,而修改数据的函数本身对此无感知。数据单向流动性,是通过在所有需要触发修改数据的地方传入一个说明操作类型与 payload 的命令对象,通过 dispatch 这个统一的函数将操作进行分发,所有修改数据的操作只能通过 dispatch 函数来触发,完成了数据修改动作之后,dispatch 会通过事件监听触发对应的回调,这也完成了数据从 disaptch 出发,到 state,再到视图层或其他地方的单向流动性。
管理 State 对象
接下来我们继续将上面的代码进行优化。
当应用变得庞大,state 对象也会相应地变得很大。而对应 state 对象的操作也会随之增多,换言之,就是调用 createStore 函数时传入的参数处理 (switch) 分支会变得很大,这样不利于代码维护。所以我们会提出将这些处理分支分开到一个个函数中,最后在传入 createStore 的时候再统一合并,这样代码的维护性就提高了。
我们来实现一个 combine 函数:
这样就可以将庞大的 state 对象通过多个方法进行整合,分而治之。举个例子:
像上面的代码,我们传入了两个对 state 中的数据作处理的函数。那么 state 的结构的类似:
至于 list 属性或者是 count 属性内部的数据结构是怎么样的,就由 listHandler 或 countHandler 来决定。我们回头看一下 combine 函数最后返回的函数的实现,实际上可以改写为这样:
我们都知道在 javascript 中 reduce 函数的用法,它相当于把一个给定的初始值(这里相当于第二个参数传入的空对象),通过一系列的函数执行,将初始值进行一个"折叠",得到一个最终的结果。而我们这里的 handler 处理函数就相当于在 reduce 函数中的一个个逻辑处理部分,称为 "reducer"。其实这个命名的来源,在Redux 的中文文档中也有提及。所以此后我们将 handler 函数称为 "reducer"。
使用中间件增强功能
在日常开发中,我们总是会遇到像打印一个变量在变化前的值与在变化后的值是怎么样的,还有对于全局的错误进行处理的需求的时候。像打印日志,如果使用前面代码,如下:
但是这样的代码是属于手动添加的,这种通用功能,是否可以通过开放一个接口,将其集成到状态处理库中,而对于编写处理函数 reducer 的人员来说,他无需编写额外的代码就可以拥有这种通用的能力呢?
首先,我们从最基本的代码开始,要实现上面的功能,那么我们可以抽象成一个函数,然后重写 dispatch 函数:
上面的代码通过全局劫持重写了 store 的 dispatch 函数,实现了当触发 dispatch 函数的之前和之后都会打印该时刻的 state 的值,这样其他开发人员在使用 dispatch 的时候都会拥有这项能力。但是这里实现还是存在问题,它在函数内部依赖了一些外部全局变量,所以为了通用与调用方便,我们将其参数化:
按照上面的模式,我们应该能很快地写出处理全局错误的函数:
但是问题来了,我们怎么将这两个函数进行整合?强行结合也是可以的:
问题是这样看起来非常别扭,而且当类似的全局需求多了,嵌套会越来越深,我们能否编写一个函数将这些函数能够统一串联在一起?
我们回过头来审视刚才实现的函数,其实 errorHandler 函数执行的并不是真正的 dispatch,而是下一个函数(logger)返回的一个函数,只不过这个函数内部(可能)执行了 dispatch 函数。也即是说 errorHandler 可以改写为下面这种形式:
可以看到,我们在原来的模式上面添加了一层 next 参数的柯里化,这样的话,能够更加灵活,这里的 next 函数你可以理解为是由另外一个函数经过增强之后的 dispatch 函数。
看上面的例子,可以看到有一些重复的代码,比如 getState 和 dispatch 的传值,我们将全局 logger 也改写为像上面全局错误处理的函数一样:
假设这两个函数放在一个数组里,那么 getState 和 dispatch 的传值就可以:
然后对于第二层 next 参数的固定,其实就是决定各个函数(errorHandler 和 logger)的执行顺序,我们使用一个函数来达到决定各个函数的执行顺序的目的。
compose
这里的 compose 可能和 Koa 框架的 compose 有些许不同,它的原理就是:
compose 同样适用了柯里化,固定了函数数组,返回了一个函数,所以使用到我们的代码中就是:
结合上面我们所说的 logger,errorHandler 函数来看,reduceRight 在执行的时候,其实是在固定 logger,errorHandler 函数的 next 参数,按照顺序,其实 logger 函数的 next 函数是 store.dispatch 函数,而 errorHandler 函数的 next 参数是 logger 函数经过 next 参数固定之后得到的函数。
最终得到的函数依然是形参为 "action, payload" 的函数,符合 store.dispatch 原来的形式。这样就可以增强原来 dispatch 的功能了。
总结
将上面的代码进行总结,我们会得到下面这个函数:
有些同学可能会对这里为什么是传入一个 createStore 函数而不是一个 store 对象有疑问,这样是为了保持灵活性的。你可以将 applyMiddleware 函数看作是另外一种中间件,只不过它不是针对 dispatch 的,而是针对 createStore 函数的,通过上面这种函数形式,它可以增强 createStore 函数:
如果有其他想要增强 createStore 函数的中间件,也可以写成这样:
这样代码就可以变成:
这样 createStore 函数也要做出相对应的改变:
根据上面的代码,其实 createStore 内部的 enhancer 函数是由 compose 返回的,由前面的部分我们知道,enhancer 类型的函数第二层需要固定的参数是 createStore 函数,然后返回一个形参和 createStore 函数一样,返回值也是 store 的函数,也即是所谓增强后的 createStore 函数。
换言之,compose 的时候,applyMiddleware 函数得到的 createStore 函数其实是 enhancer 函数在传入 createStore 函数后执行得到的那个函数,其实 applyMiddleware 在这里得到的 createStore 是一个增强后的 createStore 函数,这一点和中间件中的 compose 是类似的。
至此,我们基本实现了 Redux 的几个 API:createStore, combineReducer, compose, applyMiddleware,与 store 本身的几个 API:dispatch,getState,subscribe。完整代码如下:
React-Redux
就像前面所讲,redux 是一个状态管理工具,能与任何框架配合使用,但是由于前端的 DSL 方案很多,想要做到最小成本地使用 Redux 就需要一些工具来帮助接入。
比如在 React 中,如果你想和 react 配合使用,那么就需要在多个组件中都能取到这个全局的 store 管理对象。可能我们会考虑在组件的 props 属性里面传进去,但是一旦组件的层级比较多,这个方案就比较麻烦了,而且有可能会遇到这个组件根本不需要使用这个 store 对象,但是还是需要从 props 传进去,因为它的子组件需要使用。
另一方面,当我们通过 dispatch 修改了 state 数据之后,store 的 event emitter 需要触发组件渲染(setState),这些工作,我们需要有一个工具来帮我们完成——React-Redux。
首先 React-Redux 提供了一个 Provider 组件:
可以看到 Provider 其实是一个高阶组件,它使用了 React 的 context api 来达到多层级传参的目的。通过包裹一层,即使这个 API 不稳定,那么后期只需要更换实现,上层逻辑代码是不需要变化的。通过指定 childContext,可以让后代(不仅是直接子级)可以通过 context 属性就可以取到 store。
此外,React-Redux 提供 connect 函数,减少很多使用 Redux 的重复代码,能够帮助组件自动实现当 store 变化触发渲染(setState)等操作。
上面的 mapStateToProps 函数是用来解决对于某 container 类的组件面对庞大的 store 树的时候,通过 mapStateToProps 的返回结果来减少 state 对象的层级的。
通过 mapDispatchToProps 我们可以在函数中取到 store 的 dispatch 函数,并将封装了 dispatch 的函数在 props 参数中传给组件,使得组件能够触发 dispatch 函数。
附录
State 为什么是全状态替换?而不是针对字段作属性依赖?
redux 只是一个纯粹的状态管理库,它好就好在够纯粹,只关注于状态的前后变化,而无需关心状态值内部值变化的细节,也无需关心变化之后到底是怎么 diff 出到底哪里变化了做对应变化。
但也由于太纯粹,所以导致出现各种 xxx-redux, 但是个人觉得这个不能怪 redux,只能说前端领域 View 层 DSL 方案确实比较多。
Reducer 一定要写成纯函数吗?
我们在上面设计的时候,可以看到 reducer 函数中要用到的 state 对象其实是通过参数传入的,但是为什么不能这样设计呢:
如果在函数中插入 store.getState 函数,可能因为调用次序等问题,并不能保证每次获取到的 state 都是相同的,这样就没有办法保证相同的输入会有相同的输出,这也就违背了 Redux 中设计思想。让 reducer 接收旧的 state 和 action,返回一个新的 state,只有让 reducer 保持纯函数使得这个行为可预测,是它的设计原则。
其实在实际运用中,你是可以在 reducer 中修改 state 的值的,因为 state 一般来说是一个对象:
上面这些代码就非常邪恶,在编写中要杜绝这样的代码,如果存在这样的代码,reducer 就存在了副作用,此时对于开发维护就非常不友好了。
如果你想要使用修改的方式而不是 Object.assign 的方式来修改,那你可以借助 immer 这种库创建不可变对象来辅助 Redux 的使用。
The text was updated successfully, but these errors were encountered: