前端Web开发中的状态管理:状态101

学习在哪里保存你的变量,以改善你的应用设计、性能和可读性。
什么是状态?
在编程中,状态指的是程序中的所有变量。变量表示存储在内存中的数据。
全局状态指的是全局作用域的变量。它们可以从整个应用程序的任何地方访问。
本地状态指的是局部作用域的变量。它们只能在声明它们的文件、组件或函数内访问。
派生状态指的是基于其他变量计算的变量。
  1. const person = { firstName: "Paul", lastName: "Posey", id: 12 };
  2. // displayName 是派生状态
  3. const displayName = person.lastName + ", " + person.firstName;
javascript
当人们谈论前端Web应用中的状态时,他们通常指的是响应式状态。响应式状态跟踪更新并在发生时触发效果。响应式状态管理工具存在于所有框架中。在其中,状态变量本质上是一组getter和setter。当你访问值时,你是在获取它。当你重新赋值时,你是在设置它。创建响应式状态的工具还将有用于实例化状态变量、更新它、访问其先前值等方法。
本地状态管理
当你创建本地状态变量时,问问自己:
这个变量需要重复更新吗?
对这个变量的更新需要触发其他地方的变化吗?
如果不是,普通的letconst就足够了。响应式状态管理工具更强大,但会消耗更多资源。为什么要维护一辆法拉利,而自行车就能满足你的需求呢?
如果你确实需要更新变量并基于更新触发效果,你会想要创建响应式状态。
React: useState
Vue: ref
Angular: signals
在所有三个框架中,你向方法传递一个初始值。当你改变值时,效果将被触发。任何使用这些响应式状态变量的东西都会在更新时收到通知。所以如果你正在显示响应式状态变量的值,新值将被显示。
框架还提供了在更新后触发复杂效果的方法。例如,用户点击"下一页"按钮,所以你的pageNumber状态变量的值递增,你需要进行API调用来获取下一页的结果。你会在React中使用useReducer或useEffect,在Vue中使用watchers,在Angular中使用effects。
在Angular中,你可以使用signal创建响应式状态。
  1. const person = signal({ firstName: "Paul", lastName: "Posey", id: 12 });
typescript
如果你只想分配一个新值,你可以使用set()来更新你的signal。如果你想基于旧值计算一个新值,你使用update()
在Vue中,你可以使用ref创建响应式状态。
  1. const person = ref({ firstName: "Paul", lastName: "Posey", id: 12 });
javascript
ref维护对原始值的引用并创建一个Proxy。你使用.value在Proxy内访问和重新赋值。
  1. if (person.value.firstName === "Paul") {
  2. person.value = { firstName: "Pauline", lastName: "Person", id: 14};
  3. }
javascript
在React中,你使用useState钩子创建一个状态值和一个更新函数。(钩子是一个函数,让你在函数组件内"钩入"React功能。)
  1. const [person, setPerson] = useState({ firstName: "Paul", lastName: "Posey", id: 12 });
javascript
派生状态管理
React: const
Vue: computed
Angular: computed signals
尽可能在功能最弱的状态工具中保持派生状态。
在React中,用const声明派生状态更高效。对响应式状态变量的更新将触发组件重新渲染。如果你更新一个用useState声明的变量并触发另一个用useState声明的变量的更新,组件将重新渲染两次。如果你只使用const,值无论如何都会在第一次重新渲染期间重新计算。
  1. // 触发第二次重新渲染
  2. const [person, setPerson] = useState({ firstName: "Paul", lastName: "Posey", id: 12 });
  3. const [displayName, setDisplayName] = useState(() => person.firstName + " " + person.lastName);

  4. // 不触发第二次重新渲染
  5. const [person, setPerson] = useState({ firstName: "Paul", lastName: "Posey", id: 12 });
  6. const displayName = person.firstName + " " + person.lastName;
javascript
在Vue和Angular中,这是你使用computed()的时候。
  1. const displayName = computed(() => {
  2. return person.value.lastName + ", " + person.value.firstName;
  3. });
javascript
简单地说,每当person更新时,displayName就会更新。
Vue和Angular使用观察者模式并跟踪哪些响应式变量触发哪些效果。而不是重新渲染整个组件,只有需要更新的东西才会更新。
在观察者模式术语中,响应式变量person是主体。当我们在传递给computed()的回调函数中使用person时,该函数被添加到观察者列表中。当主体更新时,观察者被通知,函数将再次运行。这应该听起来很熟悉——addEventListener()也使用观察者模式。使用addEventListener(),主体是一个事件,你的监听器(你传递的事件处理回调函数)是观察者/效果。
依赖注入
React: Context
Vue: Provide/Inject
Angular: Dependency Injection
你的应用程序越复杂,prop-drilling就越成问题。在小型应用程序中,将状态传递给子组件甚至孙组件是可以的。作为经验法则,如果你必须将状态作为prop传递给曾孙组件,是时候重新评估设计了。当多个组件需要使用来自API调用的相同数据时,通常会遇到这个问题。
在你求助于全局状态之前,考虑一个中间解决方案——依赖注入。它仍然是本地状态,但不是使用props,父组件提供状态,它们的子组件可以消费它。这就是为什么"依赖注入""provider/consumer"可以互换使用的原因。
这种模式不仅仅用于响应式状态。如果你有派生状态或复杂计算,你可以使用依赖注入作为一种记忆化。你只需要调用一次昂贵的函数,然后你可以注入结果。
在Vue中,组件使用provide方法创建一个键值对。然后,任何子组件都可以使用inject方法访问该值。
  1. // 父组件
  2. const isLoggedIn = ref(false);
  3. provide("key", isLoggedIn);
javascript
  1. // 子组件
  2. const isLoggedIn = inject("key");
  3. if (isLoggedIn) showData();
javascript
你可以将更新函数注入到消费者组件中。为了避免意外的副作用,更新应该保留在提供者组件中。
Angular也有一个用于依赖注入的inject方法。它提供了多种创建提供者的方法。一个常见的模式是创建一个类服务,但你也可以使用像字符串、布尔值或Date这样的值。如果你需要从消费者触发更新,你的服务类可以定义getter和setter。
依赖注入是React Context的基础。Context提供者包装你的组件树,任何子组件都可以使用useContext钩子访问提供的值。
  1. // 父组件
  2. const [isLoggedIn, setIsLoggedIn] = useState(false);
  3. return (
  4. <AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
  5. <ChildComponent />
  6. AuthContext.Provider>
  7. );
javascript
  1. // 子组件
  2. const { isLoggedIn } = useContext(AuthContext);
  3. if (isLoggedIn) showData();
javascript
何时使用全局状态
React: Redux, Zustand, Context
Vue: Pinia, Vuex
Angular: NgRx, Akita
全局状态应该用于:
用户认证状态
主题偏好
购物车内容
应用程序设置
全局状态不应该用于:
表单数据
组件特定的UI状态
临时数据
全局状态管理工具通常使用发布-订阅模式。组件订阅状态变化,当状态更新时,所有订阅者都会收到通知。
总结
状态管理是前端开发的核心概念。理解不同类型的状态以及何时使用它们将帮助你构建更好的应用程序。
关键要点:
本地状态用于组件特定的数据
派生状态应该尽可能简单
依赖注入是prop-drilling的替代方案
全局状态应该谨慎使用
选择正确的状态管理工具可以提高性能和可维护性