这段时间仔细看了看 React 的文档,收获不少,这篇文章我就来说一说其中的一点收获,状态提升
本文不涉及 双向绑定的内容
为什么有状态提升
因为有单向数据流,我们知道,在 React 中,每个组件只关心它内部的状态,甚至组件无法知道自己是函数组件还是 class 组件。所以我们的 state 为一个局部量,或者说是被封装起来的,它只对这个组件内部是有效的,在组件外面或者其他组件中不能直接使用这个组件的 state。
下面写一个很常见的受控组件
class NumberInput extends React.Component {
constructor(props) {
super(props);
this.state = {
number = '';
}
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(e) {
this.setState({number: e.target.value});
}
render() {
return (
<input
value={this.state.number}
onChange={this.handleInputChange} />
)
}
}
这个组件可以很容易的使用自己的 state
,但要是别的组件要使用这里的值,那就需要一些操作了,也就是状态提升,将 state
提升到它的父组件,然后它的父组件的所有子组件就能共享这个 state
了。
React 便为我们提供了一个对象 props
,它可以接收参数,并可以将它传递给子组件,这里要注意的是,props 是只读的,下面看一个例子
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'kevin',
};
}
render() {
return <SayHello name={this.state.name}/>;
}
}
function SayHello(props) {
return <h1>hello {props.name}</h1>;
}
可以看到,App
是 class 类型的父组件,SayHello
是函数类型的子组件,在父组件中,有一个 state,在render()
中 将 state.name
传递给了 SayHello 组件,这个传递过程用到的就是 props 。子组件接受 props 作为它的函数参数,这样,子组件内部就可以使用父组件给他的 name 属性了。
其实,React 不只规定一个组件不能直接访问另一组件的 state,它还规定一个组件的 state 的值只能由这个组件的 setState() 方法来改变,包括子组件使用 props 也不能直接更改,因为 props 是只读的。但是子组件可以调用父组件的方法,去更改父组件的 state,关于这部分,下面会有一个大栗子。
什么时候使用状态提升
我们可能在日常中遇到这样一个问题,两个组件需要共享数据,一起来完成某一项操作。那这个时候就可以使用到状态提升了。我们知道,React 的数据是向下流动的,所以,我们可以把这些共享的数据放到这两个组件的共同父组件中,那这两个组件就都能使用这一数据了。这个思想,就称为状态提升( Lefting State Up )。
使用状态提升
下面举一个例子,我们实现一个实时的进制转换,这里只转换 10 进制和 16 进制,来演示状态提升的用法。我们的需求是输入十进制的数字,立马就能将转换好的十六进制输出,反之亦然。下面是效果图
因为我暂时还没有过大项目的经验,所以一般都是从父组件开始,然后完成子组件,当然这个例子也比较适用于这样的流程。如果大家喜欢从子组件开始,然后完成父组件,那么可以去 React 官网,那里有一个例子
第一个输入框
完成一个简单的父组件,我们先添加一个十进制数的输入框组件,这个输入框为受控组件,以实现实时获取输入
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
number: '',
}
this.handleDecChange = this.handleDecChange.bind(this);
}
handleDecChange(number) {
this.setState({number});
}
render() {
const number = this.state.number;
return (
<div>
<h1>十进制与十六进制互相转换</h1>
<NumberInput
number={number}
onNumberChange={this.handleDecChange}/>
</div>
)
}
}
这个组件定义了自己的 state,里面有一个 number 属性,为字符串类型。有一个自己的方法handleDecChange( )
,我们前面说了,一个组件的 state 只能由这一组件的方法来改变,并且只能通过 setState( )
来改变。这里的 handleDecChange( )
就是要传递给子组件,让子组件帮忙改变它的 state 的。
组件的事件处理程序必须绑定 this 到 该组件的构造函数中
这是因为在 React 的类组件中,当我们把事件处理函数引用作为回调传递过去, 事件处理程序方法会丢失其隐式绑定的上下文。当事件被触发并且处理程序被调用时,this
的值会回退到默认绑定,即值为 undefined
,这是因为类声明和原型方法是以严格模式运行。
详细的内容请看 这份优秀的资源
回到我们的代码中。render() 中,定义一个变量来保存 state 中的 number,方便调用。
之后就是这个组件返回到页面的内容。这里也需要注意,React 的 render() 只能返回一个节点,当然,这个节点可以包含任意多的子节点。上面返回了 div,里面包含了 NumberInput 节点,给该节点传递了一个 number 属性和一个onNumberChange()
方法,这个方法就是父组件的 handleDecChange( )
,以便子节点 NumberInput 来操作父节点 App 的内容( state )。
下面来看看子节点,其实就是文章开头的那个 受控组件 稍微改动了一点。
class NumberInput extends React.Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(e) {
this.props.onNumberChange(e.target.value ); // 使用父组件的方法修改父组件的state
}
render() {
const number = this.props.number;
return (
<div>
<span>十进制:</span>
<input
value={number} // 使用父组件的 state
onChange={this.handleInputChange} /> <br />
</div>
)
}
}
子节点接收父组件的 props,前面看到,这个 props 中有一个属性 number,有一个方法 onNumberChange()
。子组件有一个自己的方法 handleInputChange( )
,这个方法调用父组件的 onNumberChange()
,将 e.target.value
传递进去,调用父组件的 setState() 修改父组件的 state。render() 方法中,有一个 span 标签,用来渲染标题,input 就是我们输入内容的 input 框,input 的 value 是 父组件的 number,有一个 onChange()
方法,用来监听输入框内容的变化,变化的时候就会调用 handeInputChange()
。这样,就形成了一个完整的流程。
文章中,我是先写父组件,然后再写子组件,而实际运行过程中,当输入框中内容变化的时候,子组件会先运行,然后调用父组件的方法,修改父组件的 state 值,之后,React 本身发现 state 改变,便会触发元素渲染。
第二个输入框
上面我们只加了一个输入框,而我们的需求是要有两个输入框,实时进行进制转换。那么下面就添加第二个输入框
首先定义一个变量,来表示进制
const radixStr = {
d: '十进制:',
h: '十六进制:'
}
然后将父组件做一下修改
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
number: '',
radix: 'd' // 添加表示进制的 state 属性
}
this.handleDecChange = this.handleDecChange.bind(this);
this.handleHexChange = this.handleHexChange.bind(this); // 为新函数绑定 this
}
handleDecChange(number) {
this.setState({radix: 'd', number}); // 将进制属性传递进去
}
handleHexChange(number) { // 添加新的函数,当十六进制输入框变化的时候调用
this.setState({radix: 'h', number});
}
render() {
const radix = this.state.radix;
const number = this.state.number;
const dec = radix === 'h' ? toDec(number) : number; // 判断进制,并进行转换
const hex = radix === 'd' ? toHex(number) : number;
return (
<div>
<h1>十进制与十六进制互相转换</h1>
<NumberInput
radix='d' // 表示进制的属性
number={dec}
onNumberChange={this.handleDecChange}/>
<NumberInput // 十六进制的输入框组件
radix='h'
number={hex}
onNumberChange={this.handleHexChange}/>
</div>
)
}
}
对子组件也需要一些修改
class NumberInput extends React.Component {
……
render() {
const number = this.props.number;
const radix = this.props.radix; // 接收进制类型
return (
<div>
<span>{radixStr[radix]}</span> // 渲染出标题
<input
value={number} // 使用父组件的 state
onChange={this.handleInputChange} /> <br />
</div>
)
}
}
上面父组件中有使用到两个方法,我一并贴到下面
function toDec(hex) {
return (parseInt(hex + "", 16));
}
function toHex(dec) {
return (Number(dec).toString(16));
}
这里只转换整数哈(真叫懒)
一个父组件,渲染两个不同的子组件,还要调用不同函数进行转换,是怎么个流程呢,下面就来说说
首先,可以看到子组件的变化仅仅只是在渲染标题上有不同,所以重头还是在父组件上。
父组件的 state 多了一个 radix 属性,表示进制的类型,默认为 ‘d’,也就是十进制。render() 方法中有这样一句
const dec = radix === 'h' ? toDec(number) : number;
这句会判断 state 中的进制,如果是十六进制,那么就调用 toDec() 将 number 其转换为十进制,如果是十进制,那就直接赋值。另外一个 hex 变量也是一个道理。
传递给子组件的两个函数,handleDecChange()
和 handleHexChange()
中会将进制类型进行了赋值,传递给 setState()
方法。
下面我们整体说一下进制转换的过程,当我们在 十进制 的输入框中输入数字,就会触发子组件的 Input 元素里的 onChange()
方法,这个方法会调用 props.onNumberChange()
,将 e.target.value
传递进去。props.onNumberChange()
就是父组件的 handleInputChange()
,而针对不同进制的输入框会调用不同的方法,我们当前是在十进制的输入框中改变了 value ,所以父组件就会调用 handleDecChange()
,这个方法会修改 radix 为 ‘d’,即表示当前是十进制的输入框有变化,然后会将 state 中的 number 属性通过 setState 修改为从子组件传递来的 e.target.value
。React发现 state 发生了变化,render() 方法便会执行(实际并没有这么简单,会涉及到生命周期,但先后顺序是没错的),dec 和 hex 两个变量就会进行判断赋值,最后,两个 NumberInput 组件分别带着自己的参数渲染出各自的组件,我们看到的就是两个输入框中的值都有变化。
总结
说了这么多,其实大多都说了其中的流程了,状态提升的思想其实并不难。就是将子组件的 state 提示到父组件,然后这个父组件的任何子组件就都能使用这个 state,从而达到多个组件之间共享 state 的目的。
文中多有不恰当之处,希望各位走过路过,多多指点。