为什么不能直接用window对象?
- 全局污染,重复声明
- 直接取值和赋值,数据变更不清晰
- 渲染粒度不可控
- 无法进行时间旅行
当页面的比较简单,组件之间的层级关系比较浅时,自上而下的单向数据流的方式不会有问题。如果页面一复杂,组件的嵌套层级一深,这种单向数据流的传递方式,将会使你陷入到嵌套地狱”。
状态管理本身,解决的就是这种“嵌套”地狱的问题,解决的是跨层级组件之间的数据通信和状态共享。
状态管理工具的本质: 管理共享
内存中的状态
实现内容:
- 共享内存
- 管理状态
- 页面通信
- 组件通信
- 刷新失效?
原理:
单页应用(SPA)的各个组件本身是共享内存的,如果将状态保存在内存中就可以读写统一内存中的变量,从而达到状态共享的目的。
状态管理工具:
- Vue: Vuex(Pinia)
- Angular: Service和Rxjs
- React: Flux、Redux、Mobx、 Rxjs、Recoil、 Jotai、 Zustand
根据不同前端框架的定义,Vue和Angular双向数据绑定,计算属性等,数据是响应式的控制视图刷新,这些使得Vue和Angular需要状态管理的场景减少;此外其本身就包含了完整的状态管理工具,比如Vue的Vuex和Pinia,Angular的Service(RXis)等
而官方定调React是一个纯UI层的前端框架,UI=fn(state),React将状态的变动完全交给开发者,因此会出现许多状态管理工具。
React状态管理工具的分类
- React自带: Local State(props) 和Context
- 单向数据流: Flux、Redux(Redux-toolkit)
- 双向数据绑定: Mobx
- 原子型状态管理: Recoil、Jotai
- 异步操作密集型: Rxjs
每一种状态管理工具都有其不同的适用性,不同场景下需要合理的选择状态管理工具。
context
在这个案例中,可以通过使用creatContext和useContext实现跨组件通信。但 当context的值发生变化时,所有调用了useContext的组件都会进行重新渲染。因为这个api不能细分粒度的判断哪个组件依赖了context的哪些属性。甚至它的穿透性也比较强,能透过React.memo和依赖的对比,在使用时需要考虑到这一点。
优点:
React中的Context解决了react中,props或者state进行多级数据传递,则数据需要自顶下流经过每一级组件,无法跨级的问题。
缺点:
- Context相当于全局变量,难以追溯数据的变更情况
- 使用Context的组件内部耦合度太高,不利于组件的复用和单元测试
- 会产生不必要的更新(比如会穿透memo和dependicies等)
- Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合.
- 粒度也不太好控制,不能细粒度的区分组件依赖了哪一个Context
- 多个Context会存在层层嵌套的问题:因为每一层context只能存单一值,代码可读性下降,不好追踪数据变化
那么具体使用场景有:
- 全局的不需要经常变更的配置,比如主题、语言等
- 不同的页面中使用相同的属性
Flux&Redux
Flux:
Flux的缺点
- UI组件和容器组件的拆分过于复杂
- Action和Dispatcher绑定在一起
- 不支持多个store
- store被频繁的引入和调用
Redux:
Redux的三大原则:
- 单一数据源:
在redux中,整个应用的全局State(再次注意是全局state),都会保存在一个store中,一个单一数据源 state tree 也简化了应用的调试和和监控;它也让你在开发中能将应用数据持久化到本地,从而加速开发周期。此外,有一些功能以前很难实现,比如“撤销/重做”,在单一数据源的原则下,使用 Redux 实现将非常容易
- Store中的State是只读的:
我们不能直接修改store中的state, store中的state是只读的。唯一能改变store中的state的方式就是通过action
- 使用纯函数来执行修改:
可以改变store中的state接受纯函数来接受aciton,该纯函数叫reducer,
因为Redux的上述特性,使得Redux可以做时间旅行。时间旅行: 顾名思义,就是可以随时穿越到以前和未来让应用程序切换到任意时间的状态。因此,如果复杂的场景,特别是存在页面组件间复杂的通信的场景非常适合用Redux来管理状态。
优点:
Redux 比较适合用于大型 Web 项目,尤其是一些交互足够复杂、组件通信频繁的场景,状态可预测和回溯是非常有价值的。还有一种场景,比如需要事故重现,这种定义和上报事故异常和重现的场景,Redux也很有意义。
缺点:
Redux的缺点也很明显,首先为了实现纯函数的Reducer,Redux必须处理各种各样的副作用,需要引入一系列的副作用中间件,加重的心智负担,此外Action.Dispatch.Reducer的模式需要写过多的样版代码,虽然通过React hooks和Redux toolkit可以减少一定的样板代码,但是复杂度还是摆在哪里。因此中小项目,也不太推荐使用Redux,可能Context或者React hooks中的useReducer就能满足你的需求。
Mobx
优点:
Mobx 的优势在于上手简单,可以直接修改状态,不需要编写繁琐的 Action 和 Reducer,也不需要引入各种复杂的中间件,局部精确更新,免去了粒度控制烦恼,自始至终一份引用,不需要 immutable,也没有复制对象的额外开销。因此前端数据流不太复杂的情况,使用 Mobx,因为更加清晰,也便于维护。
缺点:
因为Mobx的灵活,Mobx的代码风格很难统一;Mobx是不能实现时间旅行和回溯的,因此不太适合前端数据流比较复杂的场景。随着Reacthooks,比如useReducer等的,以及React自身的原子型状态管理工具Recoil。Mobx的使用场景会被进一步压缩,目前的项目中使用Mobx的场景已经越来越小。
Recoil
状态具有原子性,都是Auto可以任意组合,可以实现完美的局部更新。
Recoil主要特点,就是较为官方,提供了与 Concurrent 模式及其他 React 新特性兼容的可能性,主打的是性能。此外因为其原子性的特点,比较容易做到细粒度的状态控制。也能跟Redux实现状态回溯相比较Redux而言,还有一个特点就是理解起来没有很复杂,不需要写很多样板代码等Recoil还有一个特点就是可以实现状态快照。比如填充首屏数据和数据状态回滚等。
Zustand
Zustand是主打轻量级(2kb)的状态管理工具,没有Redux那样臃肿的设计,也没有兼容React类组件的历史包袱。Zustand状态管理工具体积很小,因此很适合移动端的网页。与Redux相比就是可以直接setState,而不用通过dispatch和reducer。不能实现时间旅行。
实现一个简易的状态管理工具:
- 基于发布订阅模式实现store:
- 通过creatStore创建全局状态:
Redux实践:
- 将所有页面的状态都放在redux是不合理的,复杂度也高,我们可以考虑将一部分放在local state中,另一部分放在redux中,存在的问题是这两部分的状态可能会存在依赖问题。
- 在数据流中,对于同步且没有副作用的操作【纯函数】,可以管理数据,控制视图实现更新。如果存在副作用【异步回调】要先处理副作用函数,在发出action后、到达reducer前使用中间件【redux-thunk、redux-saga】进行处理。
Redux-toolkit:
Redux toolkit可以简化Redux开发,包括配置 store、定义 reducer,不可变的更新逻辑、甚至可以立即创建整个状态的“切片 slice",而无需手动编写任何 action creator 或者 action type。此外,Redux toolkit提供了完整的React的hooks,可以方便React函数组件中使用Redux toolkit。
例:
1.使用conText(多个页面都会用到)
2.redux-toolkit改写(其实这个场景Zunstand实现更好...)
样板代码: