-
Notifications
You must be signed in to change notification settings - Fork 54
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
对 React 状态管理的理解及方案对比 #36
Comments
👍 |
刚好之前也写过这样的状态管理对比的文章 riskers/blog#32 |
@riskers 嗯,👍 还写了 demo |
@xcatliu 额,本来我是想突出 mobx 相对 redux 的一些优势的,结果没搞完 - - 个人感觉 redux 有时候不如 mobx 爽... 唯一的缺点就是异步逻辑不如 saga 优雅 |
大佬能帮我写一个使用mobx的react状态管理的例子吗?我主要是不明白inject是怎么用的,看了官方的文档,但是官方的文档没有能直接运行的例子,所以一直整不明白。 |
bindActionCreators这里,官方文档中有提到
应该是考虑同构的问题吧 |
牛逼牛逼 |
mobx有几点不爽。
|
@huyansheng3 |
@sunyongjian 父组件render,会触发子组件调用 willReceiveProps,修正下。 |
@sunyongjian 我想问下,如果context api 一直是采用的,还需要这些状态管理库吗? 我个人理解,是没必要了吧 |
@cllgeek 赞同~ 如果只是共享 state,我觉得足够了。但是大部分情况下,我还是会直接用状态管理,有时候页面应用会越来越复杂,直接上 redux 这种,能够解决很多代码组织,异步方案问题,引用最佳实践,减轻自己的工作量。 |
嗯 |
1.所以说状态管理库解决的问题是:组件之间的共享数据传递的问题,一个组件自己的非其他组件需要的数据,还是由这个组件自己的state来管理。 我就是因为有:什么时候该使用状态管理,什么时候使用state? 的疑问而google到你这篇博文的。 2.于是我就想到,即使有些数据并不是组件之间用来共享的,而是某个组件自己的,那么我也创建一个 store的话, 谢谢。 |
|
既然Context有缺陷,那我们就自己维护一个吧😀,欢迎了解concent,一个可预测、零入侵、渐进式、高性能的增强型状态管理方案 https://concentjs.github.io/concent-doc/ |
@fantasticsoul 所有关于 redux 的文章下面都能看到你的评论 😂 |
酒香也怕巷子深呀,需要让更多人了解concent |
我认为状态提示其实是不好的解决方案,为什么子组件自己的属性要提升到父组件,就因为父组件需要使用?不应该是子组件管理自己的store,父组件需要使用或者获取的时候发送事件进行通知? |
可以看下前面的讨论再发表意见哈,我觉得前面的同学有解答了。 |
引入
在组内做了一次 React 状态管理的分享,总结了一些 redux 的问题,安利了一下 mobx 更方便的地方。整理成文字,便是此文。注:本文未整理完,大部分都是列的大纲,预计周二前整理完。ps: 应该整理不完
目录
React 状态、通信
Flux —> Redux —> Mobx
redux,mobx 对比
React 为什么需要状态管理
不妨先回忆一下 react 的特点。
React 特点
React 官网是这么简介的。JavaScript library for building user interfaces.专注 view 层 的特点决定了它不是一个全能框架,相比 angular 这种全能框架,React 功能较简单,单一。比如说没有前端路由,没有状态管理,没有一站式开发文档等。
react 组件是根据 state (或者 props)去渲染页面的,类似于一个函数,输入 state,输出 view。不过这不是完整意义上的 MDV(Model Driven View),没有完备的 model 层。顺便提一句,感觉现在的组件化和 MDV 在前端开发中正火热,大势所趋...
从我们最开始写 React 开始,就了解这条特点了。state 流向是自组件从外到内,从上到下的,而且传递下来的 props 是只读的,如果你想更改 props,只能上层组件传下一个包装好的 setState 方法。不像 angular 有 ng-model, vue 有 v-model, 提供了双向绑定的指令。React 中的约定就是这样,你可能觉得这很繁琐,不过 state 的流向却更清晰了,单向数据流在大型 spa 总是要讨好一些的。
这些特点决定了,React 本身是没有提供强大的状态管理功能的,那 React 组件是如何通信呢?原生大概是三种方式。
React 组件通信
之前有总结过 React 的通信问题,再次简单提一下,并说明它们存在的问题。其实大概就是两种方式,其一是状态提升,即把需要通信的 state 提升到两者共同的父组件,实现共享和 Reaction。不过状态提升又分为 Continer 组件定义和使用 Context 属性传递,这两者虽然都算是状态提升,但是实际不太一样,所以我们会分为三种方式。
如图。A,B 组件下的 A1,B1 要实现 state 通信。
在 A,B 之上增加一个 Container 组件,并把 A1,B1 需要共享的状态提升,定义到 Container,通过 props 传递 state 以及 changeState 的方法。
此方式存在的一个问题是,以后如果有一个 C 组件的 state,与 A 要做通信,就会再添加一个 Container 组件,如果是 A 的 state 要跟 C 共享,更是毁灭性打击,之前提升到 Container 的 state,还要再提升一层。这种无休止的状态提升问题,后期的通信成本非常高,几乎是重写。
乍一看,Context 像是一个好的方案,它解决了无限状态提升的问题,都统一放到定义 Context 的组件就好了。不过 React 官网倒是不建议用它 Why Not To Use Context,理由是它一个实验性的特性,未来可能移除。不过我个人觉得已经不太可能移除了,因为基本 React 的 Provider 组件都是基于此做的。
下面是 Context 定义的一段代码:
真正不要使用 Context 的原因是,它在状态更新通知组件方面存在缺陷。Context 的机制是这样的,假如 context 中定义存在变化的值,比如上图的 value,Context 组件会重新渲染(执行 SCU,willReceivedProps),重新生成 context 对象。此举会使得 Context 下面的所有组件都重新 render,才可以接收到最新的 context 对象。
假如某一层,比如组件 A,没有 re-render,我设置了
shouldComponentUpdate = false
,或者是一个 pureComponent,都会阻碍 context 对象的传递,相当于在流向中加了一堵墙。而解决这个问题的办法,就是使用发布订阅去改造。context 对象不再发生改变,而且子组件只第一次构建的时候接收 context 对象,这意味着 context 对象要内部实现订阅和发布的功能,即组件使用的时候订阅,对象属性变化的时候通知组件 render。关于此详细内容可以到如何安全的使用 Context查看(需要全局翻墙)
这个没什么好说的,就是一个组件订阅,一个组件发布,了解观察者模式的都清除,这里 也有详细的代码。想指出的是这种方式虽然解决了状态提升带来的问题,但是 state 保存在各个组件中,如果要做通信,需要写很多订阅/发布的代码,而且这部分代码也不好封装。最重要的是,数据流变得很乱,互相之间都有依赖,让代码维护起来比较困难。
其实在这里就会想到,假如说让 Event 中心统一管理状态,统一分发呢,然后每个组件只需要接收,订阅的工作交给 hoc 去处理,我们只需要告诉 hoc 我们要什么 state,它遍从 Event 中心去取。当 state 发生改变,Event 中心 pub 事件,hoc 去处理 re-render 的工作,感觉这就是目前状态管理 library 都在做的,大同小异。
问题
所以随着 spa 的增大,这些方式都会有一些问题,状态无休止提升,context 不好用,发布订阅数据流向混乱,跟组件的耦合性较高,数据流不清晰,
大型 spa 组件通信成本太高。
所有我们希望有一个独立管理的地方,像这样:
它可能会有一个 store 的概念,store 去存储数据和状态,还需要可以被订阅,即 store 中的状态发生变化要做到及时的通知。再进一步就是可以跟组件的交互,通信都交给 store 来处理。
还可以区分变化和副作用,做不同的处理。
目前社区中熟知的状态管理库就那几个,接下来按照这几个库的发布顺序介绍一下。
Flux
Flux 是随着 React 的诞生,而提出的一种状态管理的解决方案。由于 MVC 模式在大型前端应用里变得流向复杂,以及 Model 和 View 的双向绑定问题。便提出了这样的结构:
从 flux 开始,就是严格的数据流向,只能通过 actions 改变 store,actions 是借助 Dipatcher.dispatch
这样的 API 发出,然后再修改 store,由 store 去更新 view。
几个主要的概念:
flux 在 store 驱动组件这一层,没有做很好的支持,如果要做需要频繁的绑定事件。另外 action 也没有很好的异步方案。这些工作都需要用户自己去摸索处理。所以在 redux 去处理好这些问题,并且提出了更好的思想的时候,flux 很快就被替代了。不过 flux 为 redux 做了很好的借鉴,流程,action 对象,单向数据流。
Redux
如果说 flux 是一种思想的话,redux 就是对 flux 最好的实现。redux 把 flux 的多个 store 概念干掉了,只有一个 store,并且内部由 reducer 计算生成新的 state tree。把 dispatcher 跟 action 解耦,action 就是一个简单的 actionCreator。redux 也基于 koa 的中间件思想,丰富了自己的拓展性。
几个主要的概念:
如果你是 flux 的使用者,redux 出现的时候,你基本很容易接受这个思想,除了一些函数式的思维。内容方面在这不过多阐述了,这不是本次的主题,面向的也是使用过的同学。
Redux 生命周期
不过在源码里,都是在 createStore 初始化的时候,dispatch 方法里实现的。Redux 的 store,跟我们上面在“发布订阅”设想的一种思想就很契合了,store 不仅作为一个 state 的存储中心,还是事件中心。所有的订阅以及事件的分发,都在 store 里面处理了。而具体如何展示、计算数据(state),是用户传入的 reducer 决定的。关于 store 和 component 的结合,通过 react-redux 中的两个组件,就可以很好的解决了,connect 也的确是 hoc,你只需要告诉它你用什么属性,它就帮你自动处理 reaction 的 re-render。
特点
总结了几个:
CQRS
命令与查询职责分离,说实话这个概念跟 redux 有关我是在知乎的某个问答看到的。当把 redux 的 store 看做一个 model 的时候,之前对 model 的查询、命令(增删改)对应的就像 redux 里 的 reducer、action 。我们不讨论这两者到底有何内在的联系,编程的思想本来就是相同的。如果你有兴趣,你也可以了解一下 Event Sourcing 和 DDD,都会找到跟 redux 相似的影子。推荐
单向数据流
从 flux 开始提出,也是目前状态管理比较喜欢采用的,因为简单清晰的数据流向。
很多函数式的思想。比如 Reducer,Immutable Data,Pure Func,Redux 内部实现, compose 等。
通过 state 的引用变化,判断是否更新组件。
既然 state 的不可变数据,基于此理论基础,使用最简单的比较数据引用,去判断是不是要更新 view。
天生支持时间回溯
因为每次的 state 都是新的引用,上一次的 state 还可以保存,感觉上是有了基础,实际要做很多工作。可以借鉴 redux-devtools 。
中间件拓展
redux 最强大的地方便在于中间件的拓展。有一句话叫“好的设计不如好的拓展性”。
一些问题
这是我们要提及的重点。我在学习和使用 redux 的时候,遇到了很多的问题,总结来就是以下四点,相信大家也会碰到...
约定多、理解难
胶水代码
Effects
手动优化
约定
对于初学者来说,一堆的约定看着就头大,也不知道为啥这么写。我大概列举几个:
三大原则,store 唯一,state 只读, reducer 纯函数。
这个是我们学习 redux 初期就烂熟于心的概念了,几乎所有 redux 相关的入门教程,都会有这样的介绍,总之,就算不明白,我们也记住了。首先,store 设计成唯一的对象,一个对象能带来很多它简单的特点,比如所有的 reducer 计算都合并到这个 store,所有的组件修改,注入都是基于这一个对象;初始化同构应用的状态(服务端渲染)也更方便,服务端直接挂载一个
__ initalStore__
就可以了。state 只读有函数式的感觉,另外也是单向数据流的关键,你不能直接修改 state 对象,只可以通过 action 去修改,这个跟 flux 是相似的思想。reducer 是纯函数,也来自于 FP。不能有副作用的操作,通常是 state 的一些重组,拷贝和计算。使用 redux,会有很多函数式的思想,所以很多人都会学一波函数式,比如说我... FP-Code建议我们用单独的文件存放 action;不要定义 action 对象,要用 actionCreator 创建。
为了不让子组件感受到 redux 的存在,通过 bindActionCreators 生成这种
() => dispatch(actionCreator);
形式的 function。 如上图,最后可能就是 increase 这样的一个函数,然后组件内部去调用此方法,比如用 connect 这样的组件注入 actions 方法。最开始写 redux 的时候,我见过很多人在组件内部调用 dispatch 方法的,因为 connect 的第二个参数MapDispatchToProps
不传的时候,它会默认把 dispatch 方法传下来。然后就直接在组件内部写dispatch({ type: ’INCREASE‘})
这样的代码,这跟使用 bindActionCreator 的就形成了不同的风格。另外,大项目中是不建议组件直接用 dispatch 方法的,redux 建议的是 smart + dumb,即有一个 container 组件跟 redux 做交易,下面的组件要做成可复用的,跟 redux 无强耦合的组件... 所以,我理解的 actionCreator 的作用大概是这样。reducer 强调纯函数,计算。返回新的 state。
返回新的 state(redux 中的 state 概念理解为 reducer 返回的计算结果),意味着不可变数据,不可变数据操作就设计到一个拷贝的问题。深浅拷贝的方法,深拷贝的性能问题,节约内存等。可能会引入 immutable 这样的库,又增加了代码复杂度,侵入性较强,而且不是 plainObject,存在转换问题,可能会引入 redux-immutable 中的 combineReducers 去处理,内部做转换。。。总之解决的方案总是有,只是有一定的成本。redux 社区才人还是很多的。
state 设计的尽量扁平,最多两三层。
嵌套太多层的话,取值的时候比较麻烦,也需要有容错处理(当然来一个 lodash 就好了)。这只是某个 state 的层级设计问题。还有对整个 store 的设计,比如根据模块划分,共享的状态和私有的区分。谈到这,又引申出什么样的状态该放到 redux 中,大多数情况下,用 redux 很好的处理了复杂的异步逻辑问题,所以复杂的 Effect 推荐放到 redux。另外就是一些需要共享的,比如 userInfo,AppState,特殊的界面状态。
这里总结了一些使用 redux 的时候,官网和社区推荐的一些规范或者约定。如果遵守约定还好,如果风格不一的话,其实这些约定就失去了原本的意义了。其实我更想表达的是,对于新手来说,学习成本真的挺高的。然后就是 Effects 的一些问题。
Effects
这里的 Effects 主要指异步请求。最开始的方式是引入 redux-thunk,redux-promise 这样的中间件,去改造 store 的原生 dispatch 方法(compose 形成的函数调用链),使得它的参数不再局限于一个 action 对象,而是可以接收 function 或者 promise 对象,通过这一层把 Effect 影响去除,变成原来的 dispatch 一个 action 对象(代码层面就是经过函数调用链,最后执行原来的 dispatch 方法)。如下图:
引入 thunk 后的数据流向大概是这样的。组件内部通过事件或者生命周期,去调用 hoc 注入进来的 actions 方法,通常是通过 actionCreator 创建的。由于中间件的机制,我们可以在 actionCreator 中 return 一个新的 function,自然而然的,异步代码和一些逻辑就写在了这里,也就是图中黄色标记的。比如我们 request 一个请求,当 response 的时候,我们再执行 dispatch 方法,把返回的 data 传递到 reducer。reducer 通过计算、拷贝,合并生成新的 state tree,然后会执行 listeners,无论你是通过 connect 还是订阅的方式,总之你的组件会 re-render 了,接收新的 state。大概的流程大家都很清楚了应该,问题在于实际业务中,actionCreator 这里会有很多逻辑代码,开始我们可能把最基本的 fetch,loading,error 的 catch 这样的东西通过 function 封装,去解决最通用的场景。不过涉及到复杂的业务,就会在 then 中出现很多的业务逻辑代码,比如有时候需要轮询的机制,有时候需要 dispatch 多个 action,处理起来不那么顺手,而且actionCreator 中的代码越来越多,不再是一个简单的 function,action 不再纯净,违背了它的原意。所以 saga 出现了。
saga 全局下跑着 generator,需要 watch 的 actionType,通常是 async 的,用 takeEvery 或者 takeLatest 这样的方法做监听,如
yield* takeEvery('FETCH_REQUESTED', fetchData)
,当 type 匹配到FETCH_REQUESTED
,就会进入 saga,执行 fetchData(saga 中每个异步任务被定义为 Effect),不匹配的就仍然走到 reducer。跟 thunk 不同的是异步代码写到了 saga 中,也是图中标黄的。整个流程看起来更清晰,每一部分的职责单一, actionCreator 和 Effect、业务逻辑也解耦了。多人开发的大型项目中,这样的代码组织的确是可读性更高,如果你对整个数据流比较清晰,便可以很快定位到代码。当然 saga 的优势不止这一点,它还封装了一些复杂的业务场景。比如某个异步任务调用多次的时候,saga 可以 takeLatest,也就是只保留最新的任务,而 thunk 是比较难处理的,之前的任务不容易取消。另外 saga 也提供了 cancel 的 API 可以取消 task。它还封装了一些promise.all, promise race
的功能,也默认支持堵塞和非堵塞的调用。总之,异步处理的很多常见它都帮我们想到了,好好看看文档,你便可以很容易并且很优雅的写出相关代码。不过 saga 也有一些问题。比如错误的 catch,基本需要每个 task 要加一个 try catch,不然最后 saga 抛出的错,你根本不知道是哪里出了问题,调试不尽人意,babel 的 source-map 有时也会定位失误。还有一个问题便是,redux 本来定义的文件就够多了,又出现了一个 saga,感觉像是把 actionCreator 里的异步代码换了个地方而已。另外,最开始的重复的代码也很多,重复的监听 saga 任务,call Effect,put action,便又需要封装一个通用的 fetchSaga 的方法,去处理常见的异步任务。引入 saga 的好处是优雅的处理的异步 action,提供的 API 也很强大。但是引入一方面是学习成本,一方面又是封装的时间成本,这都是项目选型需要考虑的。如果你的项目有大量的读取操作,轮询的状态,比较麻烦的异步任务,那引入 saga 带来的效果是比较明显的。反之,则是增加了开发和维护成本,还是要注意场景。对 saga 的研究也没有特别深入,不再过多赘述了,网上也有很多的文章,推荐几个:
1.redux-saga 实践总结, 2.
胶水代码
redux 的胶水代码也是开发时比较痛苦的地方。
actions、reducers、actionTypes 都要定义
在使用 redux 的项目中,当你要共享状态或者处理异步的时候,假如说放到 redux 中,就不得不定义一堆类似的文件。有的人觉得这是比较好的约束和规范,而有的人就觉得这种形式比较繁琐,这个问题就仁者见仁智者见智了。
saga 里面的各种 yield => 封装
关于这一部分,社区也有很多方案,比较成熟的 dva, 封装的已经很好了,把 actions、reducers、actionTypes 以 model 的概念封装,尽管 redux 本来 model 的概念很弱。saga 的也封装到了 effects 中。
手动优化
跟大部分技术栈一样,redux 也需要手动调优,下面是我总结的几个:
mapStateToProps 采用 Reselect 做缓存
当你采用 connect 去给组件注入 state 的时候,需要 mapStateToProps 函数去指定你需要的 state。而这里有个问题,就是可能你这个组件只使用了某个 state,但是其他无关的 action 触发的时候,mapState 函数都会重新执行计算。我在这里就踩过坑,比如我的 mapState 是这样的:
在 reducer 中我把 data 通过 immutablejs 处理了,组件中使用的时候需要 toJS 转换成原生对象,而每次执行返回的都是新对象。所以每次包装后的组件做 shadowEqual 的时候,都认为 state 发生变化了,导致无关的 action 也会引发该组件的 re-render。这是由 connect 的机制决定的,hoc 内部调用 store 的 subscribe 函数,注册一个事件,当 dispatch 一个 action,所有 reducer 执行后,会 pub 这个事件,此事件就会对比两次 state 的引用是否变化,决定组件是否更新 state 以及 render。所以这里每次返回的某个 state 如果都是新引用,是有问题,并且 mapStateToProps 函数也不应该存放大量的计算代码。
reselect 就是用来解决上述重复计算的问题的,其实 reselect 可以理解为一个缓存函数,当你做过一次计算或者处理,该结果被缓存到哈希表(就是一个对象)中,下次遇到同样的输入,直接从缓存中调取,就不用重新计算了,也就不会有坑,效率也好一些。一个缓存函数类似于这样:
利用函数式的高阶函数和 js 的闭包就可以实现最基本的缓存函数,简单来说,reselect 就是这样的原理。当你 f 的结果需要的计算越多,执行的代码越多,缓存的意义就越大。
有时候需要 SCU 填 state 设计不合理的坑
React 组件本身就拥有 shouldUpdate 的能力,比如组件 state 的引用是否变化。另外,React 也提供了 shouldComponentUpdate 这样的函数,让你可以自己控制 update 的细度。
------------------------------------------分割线------------------------------------------------
pureComponent shallowEqual
scu 的简化版,手动作浅层比较。
state 的层次设计,拷贝、设值问题
对后端数据拆分
then,dva 去封装的思想或许是更好的选择。try mobx...
mobx
redux 还是遵循的 setState 一套流程,mobx 推出的时候,一个主张就是干掉 setState 的机制。
API
Observable
Computed
Autonrun
Action
Observer
数据流
依赖收集
生命周期
优点
oop 好上手,少些代码
思考🤔
Mobx 拓展
Mobx,Redux 比较
社区
使用度关注度,redux 更多,社区 redux 也是完胜。
简单对比
总结
选哪个呢?
最后
虽然我通过一些 redux 的问题,去引出 mobx,但是并不意味着 redux 就是不好的。首先每个技术栈都是根据场景,团队需要去选择的。另外,手动调优的工作不仅仅是 view 层的应用中,而是整个前端的链路都是需要的,也没有完美的、一劳永逸的方案,只有自己不断的学习和优化,我们才能更快的进步。
拓展
https://www.youtube.com/watch?v=xsSnOQynTHs redux 自此开始火
https://zhuanlan.zhihu.com/p/25989654 conf MobX vs Redux
xufei/blog#47 单页数据流方案探索,rxjs
#21 redux 中间件
https://medium.com/@mweststrate/how-to-safely-use-react-context-b7e343eff076
The text was updated successfully, but these errors were encountered: