本博客适用于React初学者。
React现在更推荐使用函数组件,React设计之初就基于函数式编程思想,数据与视图单向绑定,输入相同的数据永远输出相同的视图。
函数组件更符合函数式思想,我们将从函数组件的角度来了解React State,并会与Class组件中的State进行简单对比。
声明一个简单的状态
在声明状态前,我们需要了解一个概念:钩子(Hook)。
React 钩子是一种在React组件中处理状态和生命周期的方法。它不依赖于Class组件,可以降低组件的复杂性,且能封装和复用组件逻辑。
我们通过useState
钩子,可以轻松地声明并使用一个状态。
引用React官方的例子:
1 | function Counter({ initialCount }) { |
在该官方例子中,为了维持count
的值,让其能在整个页面运行期间可用,使用useState
来声明count
,并且用该钩子返回的set方法就可以用来更新状态。
在React中,一个组件相当于一个函数,更确切的说,是一个纯函数。纯函数是指:
- 输入相同的值永远输出相同的值;
- 无副作用,即不会更改输出以外的任何东西。
有副作用的函数举例:
1 | function add3(list) { |
把上面的add3
改成无副作用版本:
1 | function add3(list) { |
我们知道,函数在执行完毕后,内部的上下文就会被销毁。而有时候我们又需要持续保存某些值,这就需要函数式编程中的另一个东西:闭包。
useState
会创建一个闭包,从而产生一个能在函数执行完后依然保留的值。如果调用set方法更新该值,会触发函数重新执行,从而渲染出新的视图。
我们可以通过下面这个简单的例子来了解闭包。
1 | function createCount() { |
声明一个函数createCount
,当函数执行完毕了,返回两个引用了count
的函数increase
和log
。由于count
被其他函数引用,因此即便createCount
执行完了,count
依然不会销毁,这就形成了闭包。
闭包有很多用途,除了React state的实现以外,还经常用于模拟面向对象编程中的私有属性和私有方法。
useState
无法在函数组件外使用,且需要声明在函数组件的开头,在概念上相当于静态声明,不会因为props的变化而改变。
使用了useState
后的函数组件,依然可以理解为纯函数,useState
只相当于静态声明,相当于使用一个外部wrapper创建的闭包来维持状态,并把状态传给函数组件。
很多初学者会有疑问,为什么不能直接用let
,而是用useState
。
要知道,在纯函数里,输入相同的值永远输出相同的值,函数内部并无状态,因此let
声明的值在组件每次执行时都会重新初始化,执行完就被销毁,也就意味着无法将其持久保存。
还有一个误区,一些初学者会给state直接赋值,这样即便state更新了,也无法通知到视图更新,这就涉及到Flex架构思想,后面我们会简单进行了解。
状态的提升
一个组件并不能实现所有需求,组件需要合理划分,不同开发人员需要分工合作。
这个时候如果不同组件之间需要相互通信,就需要把状态提升到二者的父组件中。
常见的通信场景有:
- 父组件 ➱ 子组件:props;
- 子组件 ➱ 父组件:callback;
- 兄弟组件:状态提升到父组件中。
常见的方式有两种:
- 将State声明在父组件中;
- 使用Context。
前者把状态声明在父组件中,通过props把状态和set方法传递给子组件。后者常见于兄弟组件需要通信但不在同一层级、层级差大,或是全局需要用到的状态。
参考官方文档:使用Context之前的考虑。如果只是因为层级比较多,想避免逐级传递,则更推荐组件组合的方式。
为了了解如何将State声明在父组件中,还是用React官方例子,我们稍加重构,增加两个组件Content和ButtonGroup,Content负责展示,ButtonGroup负责对count
进行加、减或重置。
1 | function Content({ count }) { |
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官方例子:
1 | const initialState = { count: 0 }; |
reducer
是纯函数,负责计算新状态,里面定义了两个action:
- increment:增加值;
- decrement:减少值。
不同的action有不同的计算逻辑。dispatch
是一个一个event handler,当它被触发时,他会把action传递给reducer
,reducer
再依据不同的action计算并返回新值,触发State更新,最终导致View的更新。
函数式VS面向对象
如何在类组件中使用状态?
我们改写一下Counter组件,将其转成类组件看看。
1 | class Counter extends Component { |
Class组件是面向对象思维,状态即是为一个对象的属性,只要对象不被析构,状态就一直存在,reader
方法则用来输出View。
Class组件中有许多概念,在函数组件中都被抽象和封装了。诸如Class的生命周期,在函数组件中被封装为Hook API,通过useEffect
引入函数副作用来实现。
函数组件和类组件是两种不同的编程范式,而JS本身作为一门具有函数式思想的语言,或许更宜向函数组件靠拢,以发挥其优势。
函数式组件的优点也是十分显著的:
- 函数组件更符合函数式编程思想,而函数式编程是React的思想之一;
- Hooks是比高阶组件和Render Props更优雅的逻辑复用方式;
- Hooks中的
useEffect
是生命周期的概念在函数组件中的实现方式,用起来更简单。
更多关于React和其哲学的,可以参考官方文档。