React-全局状态管理的群魔乱舞

大家好,我是柒八九。

前面,我们针对-前端框架-React系列,讲了很多东西。

  1. React-Fiber机制1
  2. React-Fiber机制2
  3. React 元素 VS 组件

分别从不同的角度,来介绍React中比较重要的概念和容易让人产生混淆的知识点。

而从根本上讲,「React 是一个用于构建用户界面的 JavaScript 库」

❝它的「核心」「跟踪组件状态的变化」并将更新的状态投射到屏幕上。 ❞

而如果要想成为一个真正的功能完善的前端应用,需要借助一些工具库(Redux/Mobx)来管理应用的数据状态。当然,只使用React中提供的数据管理API(context/reducer/state/props)也能构建一个比较简单的应用。但是如果你的前端应用功能和数据过于复杂。这些API就会显得「捉襟见肘」

今天,我们就来谈谈,React中状态管理的群魔乱舞。

你能所学到的知识点

  1. 全局状态管理库需要解决的问题 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  2. 状态管理生态系统的发展史 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
  3. 解决「远程状态管理」问题的专用库的崛起 「推荐阅读指数」 ⭐️⭐️⭐️
  4. 全局状态管理库和模式的新浪潮 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  5. 现代库如何解决状态管理的核心问题 「推荐阅读指数」 ⭐️⭐️⭐️

随着React应用程序的规模和复杂性的增加,处理「全局状态管理」将是一个挑战。一般的建议是,只有在你需要的时候才去找全局状态管理解决方案。

React 本身并没有为如何解决全局状态管理提供任何强有力的指导方针。因此,随着时间的推移,React 生态系统收集了许多方法和库来解决这个问题。

如何从中挑选这些库,变的让人捉摸不透。正如我们看到的,在早期,无论何种的React应用都「无脑」的投入到Redux的生态中。

随着,社区的完善和进步,大家逐渐发现Redux并不是解决React状态管理的「银弹」。所以,各种不同的库和方法,如雨后春笋般出现。与此同时,提出了很多「设计思路」「心智模式」。这就在选择状态管理库的时候,让人很抓狂。

而接下来,我们来分析一下React中状态管理的新贵

  • Recoil[1]

  • Jotai[2]

  • Zustand[3]

  • Valtio[4]

等库中所涉及的设计理念和心智模式。

全局状态管理库需要解决的问题

  1. 从组件树的「任何地方」读取存储的状态
  2. 写入存储状态的能力
  3. 提供「优化渲染」的机制
  4. 提供「优化内存使用」的机制
  5. 「并发模式的兼容性」
  6. 数据的「持久化」
  7. 「上下文丢失」问题
  8. 「props失效」问题
  9. 「孤儿」问题

从组件树的任何地方读取存储的状态

「这是状态管理库的最基本功能」

它允许开发者将他们的状态「持久化在内存中」,并避免在大型的项目中,通过props将顶层数据,一层一层向下传递的问题。在早期开发React应用时,我们总是通过Redux来解决此类问题。

在实践中,当涉及到实际「状态存储」时,有两种主要方法。

❝第一种是「由React自身维护」。这通常意味着利用 React提供的API,如useStateuseRefuseReducer,结合React上下文来传播一个共享值。 「但是」,这种情况,在遇到「大量数据」的传递时候,性能优化是一个不小的挑战。 ❞

❝第二种方式是「将数据存储在React外部」,然后以「单例」的形式存储。并且通过「发布-订阅」的模式来使得React组件树中的某个节点能够及时准确的获取到最新的值。从而避免因为一个值的变更,使得整个组件树重新发生渲染。 「然而」,因为它是内存中的一个「单一值」,你不能为「不同的子树」提供不同的数据状态。 ❞

写入存储状态的能力

一个库应该提供一个直观的API来读取和写入存储的数据。

一个直观的API应该是符合人们现有心智模式的。很多时候,心智模式的冲突会导致使用该库的学习和应用曲线陡增。在React中,一个常见的心智模式的冲突是状态的「可变与不可变」

React中的「组件看作是一个使用stateprops来计算UI表现的函数」,而这个函数是依靠「数据引用相等」「不可变的更新操作」来判断是否触发重新渲染。但是,JS是「动态弱类型」语言,在运行阶段,不同的数据类型是可以随意切换的。

Redux 遵循这种模式,要求「所有的状态更新都以不可变的方式进行」。像这样的选择是有取舍的。在这种情况下,一个弊端就是你必须写大量的模板,以满足那些早已习惯数据可随时变更的人进行数据更新。

这就是为什么像Immer[5]这样的库很受欢迎,它允许开发者编写可变风格的代码。

在一些「后-redux」的全局状态管理解决方案中还有其他一些库,如Valtio[6],也允许开发者使用可变风格的API。


提供优化渲染的机制

然而,随着数据量的增加,当状态发生变化时的「调和过程」是一件耗时操作。经常导致大型应用的「运行时」性能不佳。

在这种模式下,全局状态管理库需要在「状态被更新时检测出重新渲染的时间,并且只重新渲染必要的内容」

优化这一过程是状态管理库需要解决的最大挑战之一。

通常有两种主要的方法。

❝第一种是允许开发者「手动优化」这个过程。 手动优化的一个例子是「通过选择器函数订阅一块存储的状态」。通过选择器读取状态的组件只有在该特定状态更新时才会重新渲染。 ❞

❝第二种是为开发者「自动处理」,这样他们就不必考虑手动优化。 Valtio 是另一个例子,它在JS引擎下使用Proxy来自动跟踪事物的更新,并自动管理一个组件何时应该重新渲染。 ❞

提供优化内存使用的机制

对于非常大的前端应用,不正确地「内存管理」会默默地导致应用数据直线上升。

特别是当用户从低配设备上访问这些大型应用程序时,数据增大,设备无法及时进行数据回收,就导致了应用卡顿等性能问题。

利用React「生命周期」来存储状态意味着更容易利用组件卸载时的「自动垃圾收集」。--> 组件卸载,存储在组件实例中的数据没有被引用,然后在新的一期GC中就会被JS引擎回收,从而有效的减低了应用内存。

对于像Redux这样提倡「单一全局存储模式」的库,你需要对其中的存储的数据进行「手动回收」。因为它将继续持有对你的数据的引用,这样它就不会自动被垃圾收集。

同样,使用一个在React之外的状态管理库存储数据,意味着它不与任何特定的组件绑定,可能需要手动管理。

其他问题

除了上面的基础问题外,在与React集成时还有一些其他的常见问题需要考虑。

与并发模式的兼容性

「并发模式」允许React在「渲染过程中 "暂停 "并切换优先级」。以前,这个过程是完全同步的。

React引入并发特性,通常会引入「边缘案例」。对于状态管理库来说,如果在渲染过程中读取的值发生了变化,那么两个组件就有可能从外部存储中读取不同的值。

这就是所谓的 「数据撕裂」。这个问题导致React团队为库创建者(Redux/Mobx)创建了useSyncExternalStorehook来解决这个问题。

useSyncExternalStore 这个 hook 并不是给我们在日常项目中用的,它是给第三方类库如 ReduxMobx 等内部使用的。

它通过「强制的同步状态更新」,使得外部 store 可以「支持并发读取」。它实现了对外部数据源订阅时不在需要 useEffect,并且推荐用于任何与 React 外部状态集成的库。

数据的持久化

拥有完全可「持久化」的状态是非常有用的,这样你就可以从某处存储中保存和恢复应用程序的状态。一些库为你处理这个问题,而另一些库可能需要开发者自行对数据进行处理。

上下文丢失问题

这是将多个 react渲染器 混合在一起的应用程序的一个问题。例如,你可能有一个同时利用 react-domreact-three-fiber 库的应用程序。在这种情况下,React 无法调和两个独立的上下文。

例如,存在如下的示例:

import React, { createContext, useContext, useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import { Canvas } from 'react-three-fiber'

// 定义全局Context 
const Context = createContext(0)
const { Provider, Consumer } = Context

const Square = () => {
  // 使用顶层组件中的数据
  const rotation = useContext(Context)
  return (
    <group rotation={[0, 0, -rotation]}>
      // 这里做动画操作
    </group>
  )
}
// 定义一个Provider
const TickProvider = ({ children }) => {
  const [rotation, setRotation] = useState(0)
  
  useEffect(() => {
    // 定期对指定数据进行修改操作
    setTimeout(() => {
      setRotation(r => r + 0.01)
    }, 100)
  }, [rotation])
  return <Provider value={rotation}>{children}</Provider>
}

上面基本的Context和组件都定义好了,然后我们需要在react-domreact-three-fiber中传递context数据,使得功能能够正常运作。

// 

原文地址:https://cloud.tencent.com/developer/article/2081823

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom