跳至主要内容

React State的使用及原理

本博客适用于React初学者。

React现在更推荐使用函数组件,React设计之初就基于函数式编程思想,数据与视图单向绑定,输入相同的数据永远输出相同的视图。

函数组件更符合函数式思想,我们将从函数组件的角度来了解React State,并会与Class组件中的State进行简单对比。

声明一个简单的状态

在声明状态前,我们需要了解一个概念:钩子(Hook)

React 钩子是一种在React组件中处理状态和生命周期的方法。它不依赖于Class组件,可以降低组件的复杂性,且能封装和复用组件逻辑。

我们通过useState钩子,可以轻松地声明并使用一个状态。

引用React官方的例子:

js
function Counter({ initialCount }) { const [count, setCount] = useState(initialCount); return ( <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> </> ); }

在该官方例子中,为了维持count的值,让其能在整个页面运行期间可用,使用useState来声明count,并且用该钩子返回的set方法就可以用来更新状态。

在React中,一个组件相当于一个函数,更确切的说,是一个纯函数。纯函数是指:

  1. 输入相同的值永远输出相同的值;
  2. 无副作用,即不会更改输出以外的任何东西。

有副作用的函数举例:

js
function add3(list) { list.push(3); return list; } const array = [1, 2]; add3(array); // -> [1, 2, 3] add3(array); // -> [1, 2, 3, 3]

把上面的add3改成无副作用版本:

js
function add3(list) { return [...list, 3]; } const array = [1, 2]; add3(array); // -> [1, 2, 3] add3(array); // -> [1, 2, 3]

我们知道,函数在执行完毕后,内部的上下文就会被销毁。而有时候我们又需要持续保存某些值,这就需要函数式编程中的另一个东西:闭包

useState会创建一个闭包,从而产生一个能在函数执行完后依然保留的值。如果调用set方法更新该值,会触发函数重新执行,从而渲染出新的视图。

我们可以通过下面这个简单的例子来了解闭包。

js
function createCount() { let count = 0; function increase() { count++; } function log() { console.log(count); } return [increase, log]; } const [increase, log] = createCount(); log(); // 0 increase(); log(); // 1

声明一个函数createCount,当函数执行完毕了,返回两个引用了count的函数increaselog。由于count被其他函数引用,因此即便createCount执行完了,count依然不会销毁,这就形成了闭包。

闭包有很多用途,除了React state的实现以外,还经常用于模拟面向对象编程中的私有属性和私有方法。

useState无法在函数组件外使用,且需要声明在函数组件的开头,在概念上相当于静态声明,不会因为props的变化而改变。

使用了useState后的函数组件,依然可以理解为纯函数,useState只相当于静态声明,相当于使用一个外部wrapper创建的闭包来维持状态,并把状态传给函数组件。

很多初学者会有疑问,为什么不能直接用let,而是用useState

要知道,在纯函数里,输入相同的值永远输出相同的值,函数内部并无状态,因此let声明的值在组件每次执行时都会重新初始化,执行完就被销毁,也就意味着无法将其持久保存。

还有一个误区,一些初学者会给state直接赋值,这样即便state更新了,也无法通知到视图更新,这就涉及到Flex架构思想,后面我们会简单进行了解。

状态的提升

一个组件并不能实现所有需求,组件需要合理划分,不同开发人员需要分工合作。

这个时候如果不同组件之间需要相互通信,就需要把状态提升到二者的父组件中。

常见的通信场景有:

  1. 父组件 ➱ 子组件:props;
  2. 子组件 ➱ 父组件:callback;
  3. 兄弟组件:状态提升到父组件中。

常见的方式有两种:

  1. 将State声明在父组件中;
  2. 使用Context

前者把状态声明在父组件中,通过props把状态和set方法传递给子组件。后者常见于兄弟组件需要通信但不在同一层级、层级差大,或是全局需要用到的状态。

参考官方文档:使用Context之前的考虑。如果只是因为层级比较多,想避免逐级传递,则更推荐组件组合的方式。

为了了解如何将State声明在父组件中,还是用React官方例子,我们稍加重构,增加两个组件Content和ButtonGroup,Content负责展示,ButtonGroup负责对count进行加、减或重置。

js
function Content({ count }) { return <>Count: {count}</>; } function ButtonGroup({ initialCount, setCount }) { return <> <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> </> } function Counter({ initialCount }) { const [count, setCount] = useState(initialCount); return ( <> <Content count={count} /> <ButtonGroup initialCount={initialCount} setCount={setCount} /> </> ); }

count状态声明在Counter组件中,通过props把值和set方法分别传递给Content组件和ButtonGroup组件,以此实现了Content和ButtonGroup之间的通信。

State的延伸探索

React State和Redux很像,都基于Flux架构,一个Flux应用包含四个部分:

  • Dispatcher,处理动作的分发;
  • Store,负责数据处理和存储;
  • Action,驱动Dipatcher的对象;
  • View,展示数据的视图。

我们可以参考Redux官方给的数据流图。

React提供useState的完整版:useReducer。实际上,前者就是基于后者实现的,后者基本上可以替代Redux使用,更适合用于管理包含多个子值的state对象。

以下是React官方例子:

js
const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'increment' })}>+</button> </> ); }

reducer是纯函数,负责计算新状态,里面定义了两个action:

  1. increment:增加值;
  2. decrement:减少值。

不同的action有不同的计算逻辑。dispatch是一个一个event handler,当它被触发时,他会把action传递给reducerreducer再依据不同的action计算并返回新值,触发State更新,最终导致View的更新。

函数式VS面向对象

如何在类组件中使用状态?

我们改写一下Counter组件,将其转成类组件看看。

js
class Counter extends Component { constructor(props) { super(props); const { initialCount } = props; this.state = { count: initialCount, }; } render() { const { initialCount } = this.props; const { count } = this.state; return ( <> Count: {count} <button onClick={() => this.setState({ count: initialCount })}>Reset</button> <button onClick={() => this.setState({ count: count - 1 })}>-</button> <button onClick={() => this.setState({ count: count + 1 })}>+</button> </> ); } }

Class组件是面向对象思维,状态即是为一个对象的属性,只要对象不被析构,状态就一直存在,reader方法则用来输出View。

Class组件中有许多概念,在函数组件中都被抽象和封装了。诸如Class的生命周期,在函数组件中被封装为Hook API,通过useEffect引入函数副作用来实现。

函数组件和类组件是两种不同的编程范式,而JS本身作为一门具有函数式思想的语言,或许更宜向函数组件靠拢,以发挥其优势。

函数式组件的优点也是十分显著的:

  1. 函数组件更符合函数式编程思想,而函数式编程是React的思想之一;
  2. Hooks是比高阶组件Render Props更优雅的逻辑复用方式;
  3. Hooks中的useEffect是生命周期的概念在函数组件中的实现方式,用起来更简单。

更多关于React和其哲学的,可以参考官方文档

评论

此博客中的热门博文

归并排序的两种方式

概述 归并排序是目前最高效的排序算法之一,它兼具稳定和低复杂度的特点,其最坏时间复杂度 O(nlogn) ,空间复杂度 O(n) 。 归并排序采用计算机科学的分治思想,因此有两种实现方式:自顶向下和自底向上。

博客迁移至 Blogger 小记

最早博客是基于 Hexo 搭建,并部署在 Github Page 上。后来因为访问速度原因,迁至大陆腾讯云裸机器上(因为做活动一年¥100不到),使用 Nginx 代理静态文件。裸机器到期后迁至腾讯云 cos 里,成本倒是降低了,但不久备案被吊销,博客停更。