本博客适用于React初学者。
React现在更推荐使用函数组件,React设计之初就基于函数式编程思想,数据与视图单向绑定,输入相同的数据永远输出相同的视图。
函数组件更符合函数式思想,我们将从函数组件的角度来了解React State,并会与Class组件中的State进行简单对比。
声明一个简单的状态
在声明状态前,我们需要了解一个概念:钩子(Hook)。
React 钩子是一种在React组件中处理状态和生命周期的方法。它不依赖于Class组件,可以降低组件的复杂性,且能封装和复用组件逻辑。
我们通过useState
钩子,可以轻松地声明并使用一个状态。
引用React官方的例子:
jsfunction 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中,一个组件相当于一个函数,更确切的说,是一个纯函数。纯函数是指:
- 输入相同的值永远输出相同的值;
- 无副作用,即不会更改输出以外的任何东西。
有副作用的函数举例:
jsfunction add3(list) {
list.push(3);
return list;
}
const array = [1, 2];
add3(array); // -> [1, 2, 3]
add3(array); // -> [1, 2, 3, 3]
把上面的add3
改成无副作用版本:
jsfunction add3(list) {
return [...list, 3];
}
const array = [1, 2];
add3(array); // -> [1, 2, 3]
add3(array); // -> [1, 2, 3]
我们知道,函数在执行完毕后,内部的上下文就会被销毁。而有时候我们又需要持续保存某些值,这就需要函数式编程中的另一个东西:闭包。
useState
会创建一个闭包,从而产生一个能在函数执行完后依然保留的值。如果调用set方法更新该值,会触发函数重新执行,从而渲染出新的视图。
我们可以通过下面这个简单的例子来了解闭包。
jsfunction 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
的函数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
进行加、减或重置。
jsfunction 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官方例子:
jsconst 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:
- increment:增加值;
- decrement:减少值。
不同的action有不同的计算逻辑。dispatch
是一个一个event handler,当它被触发时,他会把action传递给reducer
,reducer
再依据不同的action计算并返回新值,触发State更新,最终导致View的更新。
函数式VS面向对象
如何在类组件中使用状态?
我们改写一下Counter组件,将其转成类组件看看。
jsclass 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本身作为一门具有函数式思想的语言,或许更宜向函数组件靠拢,以发挥其优势。
函数式组件的优点也是十分显著的:
- 函数组件更符合函数式编程思想,而函数式编程是React的思想之一;
- Hooks是比高阶组件和Render Props更优雅的逻辑复用方式;
- Hooks中的
useEffect
是生命周期的概念在函数组件中的实现方式,用起来更简单。
更多关于React和其哲学的,可以参考官方文档。
评论
发表评论