4251 words
21 minutes
2. 本地和全局变量
2025-02-22 21:49:49
2025-02-23 17:15:18

理解何时使用局部状态#

NOTE

在我们考虑 React 之前,让我们先了解 JavaScript 函数是如何工作的。JavaScript 函数可以是纯函数或非纯函数。纯函数只依赖于其参数,只要参数相同,就会返回相同的值。状态(state)在参数之外保持一个值,依赖于状态的函数就会变成非纯函数。

React 组件也是函数,也可以是纯的。如果我们在 React 组件中使用状态,它就会变成非纯的。然而,如果状态是局部的(仅限于组件内部),它就不会影响其他组件,我们称这种特性为’封装性’。 在本节中,我们将学习 JavaScript 函数,以及 React 组件与 JavaScript 函数的相似之处。然后我们将讨论局部状态在概念上是如何实现的。

函数和参数#

在 JavaScript 中,函数接收一个参数并返回一个值。例如,这是一个简单的函数:

const addOne = (n) => n + 1;
TIP

这是一个纯函数,对于相同的参数总是返回相同的值。通常情况下,我们倾向于使用纯函数,因为它们的行为是可预测的。

函数也可以依赖于全局变量,例如下面这样:

let base = 1; const addBase = (n) => n + base;
WARNING

只要 base 不改变,addBase 函数的工作方式就和 addOne 完全一样。但是,如果在某个时候我们把 base 改成 base=2,它的行为就会不同。这完全不是一件坏事,实际上这是一个强大的特性,因为你可以从外部改变函数的行为。缺点是你不能简单地把 addBase 函数拿来在其他地方随意使用,因为你需要知道它依赖于一个外部变量。如你所见,这是一个权衡。

IMPORTANT

如果 base 是一个单例(内存中的单一值),这种模式并不是首选,因为代码的可重用性会降低。为了避免单例并稍微减轻这个缺点,一个更模块化的方法是创建一个容器对象 :::,如下所示:

const createContainer = () => { let base = 1; const addBase = (n) => n + base; const changeBase = (b) => { base = b; }; return { addBase, changeBase }; }; const { addBase, changeBase } = createContainer();

这样就不再是单例了,你可以创建任意多个容器。与将 base 作为单例的全局变量不同,容器是相互隔离的,更容易重用。你可以在代码的一部分使用一个容器,而不会影响使用不同容器的其他代码部分。 小提示::::tip[小提示] 虽然容器中的 addBase 从数学角度来说不是纯函数,但是如果 base 不改变,调用 addBase 总能得到相同的结果(这种特性有时被称为幂等性)。

React components and props#

NOTE

从概念上讲,React 是一个将状态转换为用户界面(UI)的函数。当你使用 React 编程时,React 组件实际上就是一个 JavaScript 函数,它的参数被称为 props。

一个显示数字的函数组件看起来是这样的:

const Component = ({ number }) => { return <div>{number}</div>; };

这个组件接收一个 number 参数,并返回一个 JavaScript 语法扩展(JSX)元素,用于在屏幕上显示这个数字。

什么是jsx#

NOTE

JSX 是一种使用尖括号语法来生成 React 元素的扩展语法。React 元素是一个用来表示 UI 部分的数据结构。当 React 元素使用 JSX 语法时,我们通常称它们为 JSX 元素。

让我们来创建另一个显示数字加1的组件,如下所示:

const AddOne = ({ number }) => { return <div>{number + 1}</div>; };

这个组件接收 number 参数并返回 number + 1。它的行为与前面章节中的 addOne 完全一样,而且这也是一个纯函数。唯一的区别是参数是一个 props 对象,返回值是 JSX 格式。

理解使用 useState 实现局部状态#

如果我们使用 useState 来创建局部状态会怎样呢?让我们把 base 变成一个状态,并显示一个可以与之相加的数字,如下所示:

const AddBase = ({ number }) => { const [base, changeBase] = useState(1); return <div>{number + base}</div>; };
CAUTION

从技术上讲,这个函数不是纯函数,因为它依赖于不在函数参数中的 base。

AddBase 中的 useState 做了什么?让我们回想一下前面章节中的 createContainer。就像 createContainer 返回 base 和 changeBase 一样,useState 以元组的形式(即包含两个或更多值的结构,在这里是两个)返回 base 和 changeBase。虽然在这段代码中我们看不到 base 和 changeBase 是如何创建的,但概念上是类似的。

TIP

如果我们假设 useState 的行为是在未改变时返回相同的 base,那么 AddBase 函数就是幂等的,就像我们在 createContainer 中看到的那样。

IMPORTANT

这个使用了 useState 的 AddBase 函数是封装的,因为 changeBase 只在函数声明的作用域内可用。在函数外部无法改变 base。这种 useState 的用法就是局部状态,因为它是封装的,不会影响组件外部的任何东西,所以确保了局部性;这种用法在适当的情况下是首选。

局部状态的局限性#

什么时候局部状态不适合使用?

当我们想要打破局部性的时候。在 AddBase 组件的例子中,如果我们想要从代码的完全不同部分改变 base,局部状态就不适合了。如果你需要从函数组件外部改变状态,这就是需要使用全局状态的时候。

状态变量从概念上讲是一个全局变量。全局变量用于从函数外部控制 JavaScript 函数的行为。同样,全局状态用于从组件外部控制 React 组件的行为。然而,使用全局状态会使组件的行为变得不那么可预测。这是一个权衡。我们不应该过度使用全局状态。应该考虑将局部状态作为主要手段,只在次要情况下使用全局状态。从这个意义上说,了解局部状态可以覆盖多少用例是很重要的。

在本节中,我们学习了 React 中的局部状态,以及它与 JavaScript 函数的关系。接下来,我们将学习使用局部状态的一些模式。

有效使用局部状态#

有一些模式你需要了解,以便能够有效地使用局部状态。在本节中,我们将学习如何提升状态(lifting states up),这意味着在组件树的更高层定义状态,以及提升内容(lifting content up),这意味着在组件树的更高层定义内容。

状态提升 (Lifting state up)#

让我们假设我们有两个计数器组件,如下所示:

const Component1 = () => { const [count, setCount] = useState(0); return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> Increment Count </button> </div> ); }; const Component2 = () => { const [count, setCount] = useState(0); return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> Increment Count </button> </div> ); };
NOTE

因为在这两个组件中分别定义了两个独立的局部状态,所以这两个计数器是独立工作的。如果我们想要共享状态,让它们作为一个共享的计数器工作,我们可以创建一个父组件并将状态提升到父组件中。

这里是一个示例,展示了一个包含 Component1 和 Component2 作为子组件的父组件,并向它们传递 props:

const ParentComponent = () => { const [count, setCount] = useState(0); return ( <div> <Component1 count={count} /> <Component2 count={count} /> <button onClick={() => setCount((c) => c + 1)}> Increment Count </button> </div> ); };

在这个示例中,父组件 ParentComponent 管理一个共享的状态,并将其传递给子组件 Component1 和 Component2。

const Component1 = ({ count, setCount }) => { return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> 增加计数 </button> </div> ); }; const Component2 = ({ count, setCount }) => { return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> 增加计数 </button> </div> ); }; const ParentComponent = () => { const [count, setCount] = useState(0); return ( <> <Component1 count={count} setCount={setCount} /> <Component2 count={count} setCount={setCount} /> </> ); };
TIP

因为 count 状态只在父组件中定义了一次,所以这个状态被 Component1 和 Component2 共享。这仍然是组件中的一个局部状态;子组件可以使用来自父组件的状态。

CAUTION

这种模式在大多数使用局部状态的场景中都能很好地工作;但是,这里有一个关于性能的小问题需要注意。当我们提升状态时,父组件的重新渲染会导致其整个子树(包括所有子组件)都重新渲染。在某些使用场景中,这可能会成为一个性能问题。

内容提升 (Lifting content up)#

在复杂的组件树中,我们可能会有一些组件并不依赖于我们正在提升的状态。

在下面的例子中,我们在前面示例的 Component1 中添加了一个新的 AdditionalInfo 组件:

const AdditionalInfo = () => { return <p>一些信息</p> }; const Component1 = ({ count, setCount }) => { return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> 增加计数 </button> <AdditionalInfo /> </div> ); }; const Parent = () => { const [count, setCount] = useState(0); return ( <> <Component1 count={count} setCount={setCount} /> <Component2 count={count} setCount={setCount} /> </> ); };
WARNING

当 count 发生变化时,Parent 会重新渲染,然后 Component1、Component2 和 AdditionalInfo 也都会重新渲染。然而,AdditionalInfo 在这种情况下其实不需要重新渲染,因为它并不依赖于 count。如果这对性能有影响,那么这种额外的重新渲染应该被避免。

为了避免额外的重新渲染,我们可以提升内容。在这种情况下,由于 Parent 会随着 count 重新渲染,我们可以创建一个 GrandParent,如下所示:

const AdditionalInfo = () => { return <p>Some information</p> }; const Component1 = ({ count, setCount, additionalInfo }) => { return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> Increment Count </button> {additionalInfo} </div> ); }; const Parent = ({ additionalInfo }) => { const [count, setCount] = useState(0); return ( <> <Component1 count={count} setCount={setCount} additionalInfo={additionalInfo} /> <Component2 count={count} setCount={setCount} /> </> ); }; const GrandParent = () => { return <Parent additionalInfo={<AdditionalInfo />} />; };

通过这种方式,当 count 发生变化时,AdditionalInfo 不会重新渲染。这是一种不仅可以用于性能优化,还可以用于组织组件树结构的技术。

这种模式的一个变体是使用 children 属性。下面的示例使用 children 属性实现了相同的功能,只是编码风格不同:

const AdditionalInfo = () => { return <p>Some information</p> }; const Component1 = ({ count, setCount, children }) => { return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> Increment Count </button> {children} </div> ); }; const Parent = ({ children }) => { const [count, setCount] = useState(0); return ( <> <Component1 count={count} setCount={setCount}> {children} </Component1> <Component2 count={count} setCount={setCount} /> </> ); }; const GrandParent = () => { return ( <Parent> <AdditionalInfo /> </Parent> ); };

children 是一个特殊的 prop 名称,在 JSX 格式中表示为嵌套的子元素。如果你需要传递多个元素,给 props 命名可能会更合适。这主要是一个代码风格的选择,开发者可以选择他们喜欢的任何方式。

在本节中,我们学习了一些有效使用局部状态的模式。如果我们正确地提升状态和内容,我们应该能够仅使用局部状态就解决各种用例。接下来,我们将学习如何使用全局状态。

使用全局状态#

什么是全局状态#

在本书中,全局状态简单来说就是非局部状态。如果一个状态在概念上属于单个组件并被该组件封装,那它就是局部状态。因此,如果一个状态不属于单个组件而是可以被多个组件使用,那它就是全局状态。可能存在一个所有组件都依赖的应用级局部状态。在这种情况下,这个应用级局部状态可以被视为全局状态。从这个意义上说,我们无法明确区分局部状态和全局状态。在大多数情况下,如果你考虑状态在概念上属于哪里,你就能判断出它是局部的还是全局的。 当人们谈论全局状态时,主要有两个方面:

  • 一个是单例,意味着在某些上下文中,状态只有一个值。

  • 另一个是共享状态,这意味着状态值在不同组件之间共享,但它不一定是 JavaScript 内存中的单一值。非单例的全局状态可以有多个值。为了说明非单例全局状态是如何工作的,这里有一个展示 JavaScript 中非单例变量的例子:

const createContainer = () => { let base = 1; const addBase = (n) => n + base; const changeBase = (b) => { base = b; }; return { addBase, changeBase }; }; const container1 = createContainer(); const container2 = createContainer(); container1.changeBase(10); console.log(container1.addBase(2)); // 显示 "12" console.log(container2.addBase(2)); // 显示 "3"

在这个例子中,base 是容器中的一个作用域变量。由于 base 在每个容器中都是隔离的,改变 container1 中的 base 不会影响 container2 中的 base。

在 React 中,概念是类似的。如果全局状态是单例的,我们在内存中只有一个值。如果全局状态是非单例的,我们可能在组件树的不同部分(子树)中有多个值。

何时使用全局状态#

在 React 中,有两个使用全局状态的指导原则:

  • 当传递 props 不太合适时
  • 当我们已经在 React 之外有了一个状态时

让我们分别讨论这两种情况。

传递 props 不太合适的情况#

如果你需要在组件树中相距较远的两个组件中使用某个状态,将状态放在共同的根组件中并一层层向下传递到这两个组件可能并不是一个好主意。

例如,如果我们的组件树有三层深度,并且需要将状态提升到顶层,它看起来会是这样:

const Component1 = ({ count, setCount }) => { return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> 增加计数 </button> </div> ); }; const Parent = ({ count, setCount }) => { return ( <> <Component1 count={count} setCount={setCount} /> </> ); }; const GrandParent = ({ count, setCount }) => { return ( <> <Parent count={count} setCount={setCount} /> </> ); }; const Root = () => { const [count, setCount] = useState(0); return ( <> <GrandParent count={count} setCount={setCount} /> </> ); };

这种方式对于保持局部性来说是完全可以的,而且也是推荐的做法;但是,让中间组件仅仅用来传递 props 可能会很繁琐。通过多层中间组件传递 props 可能不会带来良好的开发体验,因为这看起来像是不必要的额外工作。此外,当状态更新时,中间组件也会重新渲染,这可能会影响性能。

在这种情况下,使用全局状态会更合适,而且中间组件也不需要关心状态的传递。

下面是一个使用全局状态的伪代码示例,展示了如何处理前面的例子:

const Component1 = () => { // useGlobalCountState 是一个伪造的 hook const [count, setCount] = useGlobalCountState(); return ( <div> {count} <button onClick={() => setCount((c) => c + 1)}> 增加计数 </button> </div> ); }; const Parent = () => { return ( <> <Component1 /> </> ); }; const GrandParent = () => { return ( <> <Parent /> </> ); }; const Root = () => { return ( <> <GrandParent /> </> ); };

在这个例子中,只有 Component1 使用了全局状态。与局部状态和 props 传递不同,中间组件 Parent 和 GrandParent 完全不需要知道全局状态的存在。

const globalState = { authInfo: { name: 'React' }, }; const Component1 = () => { // useGlobalState 是一个伪造的 hook const { authInfo } = useGlobalState(); return ( <div> {authInfo.name} </div> ); };

在这个例子中,globalState 存在并定义在 React 之外。useGlobalState 是一个 hook,它可以连接到 globalState 并在 Component1 中提供 authInfo。

在本节中,我们了解到全局状态是一个不能作为局部状态的状态。全局状态主要是作为局部状态的补充来使用的,有两种模式下使用全局状态效果很好:一种是在 props 传递不合理的情况下,另一种是在应用中已经存在全局状态的情况。

总结#

在本章中,我们讨论了局部状态和全局状态。局部状态在可能的情况下是首选,我们学习了一些有效使用局部状态的技术。然而,在局部状态不适用的地方,全局状态也有其作用,这就是为什么我们要研究何时应该使用全局状态。

在接下来的三章中,我们将学习在 React 中实现全局状态的三种模式;具体来说,下一章我们将从利用 React context 开始。

2. 本地和全局变量
https://0bipinnata0.my/posts/react/micro-state-management/02-本地和全局变量/
Author
0bipinnata0
Published at
2025-02-22 21:49:49