sunyongjian / blog

个人博客😝😋😄
666 stars 54 forks source link

对 React 状态管理的理解及方案对比 #36

Open sunyongjian opened 6 years ago

sunyongjian commented 6 years ago

引入

在组内做了一次 React 状态管理的分享,总结了一些 redux 的问题,安利了一下 mobx 更方便的地方。整理成文字,便是此文。注:本文未整理完,大部分都是列的大纲,预计周二前整理完。ps: 应该整理不完

目录

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 属性传递,这两者虽然都算是状态提升,但是实际不太一样,所以我们会分为三种方式。

tx1

如图。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 定义的一段代码: image

真正不要使用 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 组件通信成本太高。

statetx

所有我们希望有一个独立管理的地方,像这样:

storetx

它可能会有一个 store 的概念,store 去存储数据和状态,还需要可以被订阅,即 store 中的状态发生变化要做到及时的通知。再进一步就是可以跟组件的交互,通信都交给 store 来处理。 还可以区分变化和副作用,做不同的处理。

目前社区中熟知的状态管理库就那几个,接下来按照这几个库的发布顺序介绍一下。

Flux

Flux 是随着 React 的诞生,而提出的一种状态管理的解决方案。由于 MVC 模式在大型前端应用里变得流向复杂,以及 Model 和 View 的双向绑定问题。便提出了这样的结构: flux

从 flux 开始,就是严格的数据流向,只能通过 actions 改变 store,actions 是借助 Dipatcher.dispatch
这样的 API 发出,然后再修改 store,由 store 去更新 view。

几个主要的概念:

flux 在 store 驱动组件这一层,没有做很好的支持,如果要做需要频繁的绑定事件。另外 action 也没有很好的异步方案。这些工作都需要用户自己去摸索处理。所以在 redux 去处理好这些问题,并且提出了更好的思想的时候,flux 很快就被替代了。不过 flux 为 redux 做了很好的借鉴,流程,action 对象,单向数据流。

Redux

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。

特点

总结了几个:

一些问题

这是我们要提及的重点。我在学习和使用 redux 的时候,遇到了很多的问题,总结来就是以下四点,相信大家也会碰到...

约定多、理解难

胶水代码

Effects

手动优化

约定

对于初学者来说,一堆的约定看着就头大,也不知道为啥这么写。我大概列举几个:

这里总结了一些使用 redux 的时候,官网和社区推荐的一些规范或者约定。如果遵守约定还好,如果风格不一的话,其实这些约定就失去了原本的意义了。其实我更想表达的是,对于新手来说,学习成本真的挺高的。然后就是 Effects 的一些问题。

Effects

这里的 Effects 主要指异步请求。最开始的方式是引入 redux-thunk,redux-promise 这样的中间件,去改造 store 的原生 dispatch 方法(compose 形成的函数调用链),使得它的参数不再局限于一个 action 对象,而是可以接收 function 或者 promise 对象,通过这一层把 Effect 影响去除,变成原来的 dispatch 一个 action 对象(代码层面就是经过函数调用链,最后执行原来的 dispatch 方法)。如下图:

thunk1

引入 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 出现了。

saga1

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 的胶水代码也是开发时比较痛苦的地方。

关于这一部分,社区也有很多方案,比较成熟的 dva, 封装的已经很好了,把 actions、reducers、actionTypes 以 model 的概念封装,尽管 redux 本来 model 的概念很弱。saga 的也封装到了 effects 中。

手动优化

跟大部分技术栈一样,redux 也需要手动调优,下面是我总结的几个:

then,dva 去封装的思想或许是更好的选择。try mobx...

mobx1

mobx

redux 还是遵循的 setState 一套流程,mobx 推出的时候,一个主张就是干掉 setState 的机制。

image

API

依赖收集

Mobx,Redux 比较

社区

image

使用度关注度,redux 更多,社区 redux 也是完胜。

简单对比

总结

最后

虽然我通过一些 redux 的问题,去引出 mobx,但是并不意味着 redux 就是不好的。首先每个技术栈都是根据场景,团队需要去选择的。另外,手动调优的工作不仅仅是 view 层的应用中,而是整个前端的链路都是需要的,也没有完美的、一劳永逸的方案,只有自己不断的学习和优化,我们才能更快的进步。

拓展

https://www.youtube.com/watch?v=xsSnOQynTHs redux 自此开始火

https://zhuanlan.zhihu.com/p/25989654 conf MobX vs Redux

https://github.com/xufei/blog/issues/47 单页数据流方案探索,rxjs

https://github.com/sunyongjian/blog/issues/21 redux 中间件

https://medium.com/@mweststrate/how-to-safely-use-react-context-b7e343eff076

2016gx commented 6 years ago

👍

riskers commented 6 years ago

刚好之前也写过这样的状态管理对比的文章 https://github.com/riskers/blog/issues/32

sunyongjian commented 6 years ago

@riskers 嗯,👍 还写了 demo

xcatliu commented 6 years ago

总结的很到位,redux + redux-thunk 还是目前比较成熟的方案

sunyongjian commented 6 years ago

@xcatliu 额,本来我是想突出 mobx 相对 redux 的一些优势的,结果没搞完 - -

个人感觉 redux 有时候不如 mobx 爽... 唯一的缺点就是异步逻辑不如 saga 优雅

glbjiiii commented 6 years ago

大佬能帮我写一个使用mobx的react状态管理的例子吗?我主要是不明白inject是怎么用的,看了官方的文档,但是官方的文档没有能直接运行的例子,所以一直整不明白。
比如我有两个组件,两个组件放在任意位置,A组件点击一下,B组件隐藏,再点击一下就显示,像这样的用mobx应该怎么写啊,求大佬给个能运行的例子。

hemisu commented 6 years ago

bindActionCreators这里,官方文档中有提到

You might ask: why don't we bind the action creators to the store instance right away, like in classical Flux? The problem is that this won't work well with universal apps that need to render on the server. Most likely you want to have a separate store instance per request so you can prepare them with different data, but binding action creators during their definition means you're stuck with a single store instance for all requests.

应该是考虑同构的问题吧

huyansheng3 commented 6 years ago

牛逼牛逼

huyansheng3 commented 6 years ago

mobx有几点不爽。

  1. 传递到组件的props,到底要不要toJS,不做toJS,感觉混入一点奸细,不够纯对象;做toJS,是递归,会损耗一点性能。
  2. @observer 做细粒度的绑定,保证渲染最小,但react父组件render,会调用组件的 willReceiveProps,除非绑的很细,否则肯定会有浪费。绑的太细,会导致组件不能方便的导出复用,跟mobx耦合太深。但是 redux + immer 配合不可变数据和 pureComponents就可以解决了。
sunyongjian commented 6 years ago

@huyansheng3
e... 其实还好 第一个,toJS 其实就是在你真正需要用数组的时候,才需要转。比如使用 lodash 的一些数组操作方法之前。Observable 的数组,mobx 本身也提供了一些比较好用的方法,这个文档上有。 第二个,首先 observer(component),是不会调用 cwrp 的,而是直接触发组件的 forceupdate。然后,使用 mobx 的一个核心的思想就是尽可能的观测最小粒度的组件,使用的 observer 越多,更新的效率越高、损耗越小。这个是不同于 react 本身的 props 传递思想,而且一般用了 mobx 的组件,之后抽离出来,封装更公共组件,脱离 mobx 也比较麻烦,当然如果大家都用 mobx 就没啥问题了。

huyansheng3 commented 6 years ago

@sunyongjian 父组件render,会触发子组件调用 willReceiveProps,修正下。

cllgeek commented 6 years ago

@sunyongjian 我想问下,如果context api 一直是采用的,还需要这些状态管理库吗? 我个人理解,是没必要了吧

sunyongjian commented 6 years ago

@cllgeek 赞同~ 如果只是共享 state,我觉得足够了。但是大部分情况下,我还是会直接用状态管理,有时候页面应用会越来越复杂,直接上 redux 这种,能够解决很多代码组织,异步方案问题,引用最佳实践,减轻自己的工作量。

cllgeek commented 6 years ago

CocaColf commented 5 years ago

1.所以说状态管理库解决的问题是:组件之间的共享数据传递的问题,一个组件自己的非其他组件需要的数据,还是由这个组件自己的state来管理。 请问这个理解正确吗?

我就是因为有:什么时候该使用状态管理,什么时候使用state? 的疑问而google到你这篇博文的。

2.于是我就想到,即使有些数据并不是组件之间用来共享的,而是某个组件自己的,那么我也创建一个 store的话,岂不是整个react项目里我就不需要使用 state 以及 setState了评论完我自己去写了一个demo,有个体会就是state还是需要的,一个组件如果没有state的话,那么会反而增加一些问题。我刚刚实践的场景是: 点击 提交 把一个input的数据存到store数组里去,但是对于input我使用的是onChange方法,如果这个input我没有发生改变的话,那么点击提交,我在store的action里获取不到数据。所以我通过这个体会到state不能完全没有

谢谢。

sunyongjian commented 5 years ago

@CocaColf

  1. 你这个理解基本是正确的。当组件的某个状态,被其他组件多次、频繁的引用,这时候 state 通信的成本就会很高,而且逻辑会比较混乱,而通过 store 来管理会清晰很多。关于什么时候用 state or store,就仁者见仁了。
  2. 你举的这个例子,我没有体会出 state 跟 store 的区别。onChange 是一回事,submit 是另一回事,就算input 的 value 放 state 就能取到值了吗,还需要做校验。
  3. 通常在项目中,即使是不需要共享的状态,我也会放到 store 里。因为状态管理提供的是一整套的开发规范和流程,比如我的请求就通过 action 触发,我的逻辑就写到 saga 里。而且一个页面的数据,这个页面的子组件经常会使用,通过 store 管理就避免了 props 多层传递的繁琐。 那什么时候用 state 呢,通用的 react 组件,或者一些页面内部的状态(比如显隐,type)之类的。一个是为了组件的通用性,支持 react 原生使用而不是依赖状态管理,通过 props 可以控制组件; 内部状态直接用 state 更简洁,更简单,而且确实没必要用状态管理。 😆 另外 mobx 跟 redux 的思想也有点出入,redux 是不要滥用 store,而 mobx 反其道而行,有替代 state 之势...
fantasticsoul commented 5 years ago

既然Context有缺陷,那我们就自己维护一个吧😀,欢迎了解concent,一个可预测、零入侵、渐进式、高性能的增强型状态管理方案 https://concentjs.github.io/concent-doc/

justjavac commented 5 years ago

@fantasticsoul 所有关于 redux 的文章下面都能看到你的评论 😂

fantasticsoul commented 5 years ago

酒香也怕巷子深呀,需要让更多人了解concent

awefeng commented 4 years ago

我认为状态提示其实是不好的解决方案,为什么子组件自己的属性要提升到父组件,就因为父组件需要使用?不应该是子组件管理自己的store,父组件需要使用或者获取的时候发送事件进行通知?

hemisu commented 4 years ago

我认为状态提示其实是不好的解决方案,为什么子组件自己的属性要提升到父组件,就因为父组件需要使用?不应该是子组件管理自己的store,父组件需要使用或者获取的时候发送事件进行通知?

可以看下前面的讨论再发表意见哈,我觉得前面的同学有解答了。