引言
近日,在SNZ/IAF实习的过程中,利用空闲时间又学习了一些React,对一些概念感触颇深,故随笔记录。
概述
本文重点聊三个比较重要的概念:
- 事件处理
- 受控组件
- 状态提升
这三个模块的知识是我感觉React中比较核心的几个概念,有许多常见的问题以及常用的技巧与处理方法,必须熟练掌握。下面我们就简单介绍一下。
事件处理:
当input之类的控件发生变化(如改变内容),事件即发生。可由onChange来监听并执行某些函数,同时,JSX的回调函数可自动/手动(根据回调函数的类型)捕获事件对象,如下方示例中的e。事件对象中含有许多属性,常用的如e.target.value(控件的值)、e.target.name(控件的name属性的值)……
// ...
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
// ...
<input value={temperature} onChange={this.handleChange} />
// ...
阻止默认行为
在React中,你不能通过返回 false
的方式阻止默认行为。你必须显式的使用 preventDefault
。例如:
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
this绑定的问题
我们必须谨慎对待 JSX 回调函数中的 this
,在 JavaScript 中,class 的方法默认不会绑定this
。如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
。
这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 ()
,例如 onClick={this.handleClick}
,你应该为这个方法绑定 this
。
如果觉得使用 bind
很麻烦,这里有两种方式可以解决。如果你正在使用实验性的 public class fields 语法,你可以使用 class fields 正确的绑定回调函数:
class LoggingButton extends React.Component {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
// 注意: 这是 *实验性* 语法。
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
如果你没有使用 class fields 语法,你可以在回调中使用箭头函数:
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
此语法问题在于每次渲染 LoggingButton
时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。
总结 |
对于class中的回调函数,我们有三种书写方式: 1、onClick={this.handleClick}的写法,但handleClick函数需要手动bind一下this:this.handleClick = this.handleClick.bind(this); 2、onClick={this.handleClick}的写法,但令handleClick为指定箭头函数: handleClick = () => { console.log('this is:', this); } 3、直接用onClick={() => this.handleClick()的写法,缺点是会略微降低性能。 |
向事件处理程序传递参数
在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id
是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind
来实现。
在这两种情况下,React 的事件对象 e
会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。
其实还是在区分“bind绑定”与“箭头函数”两种回调函数一些写法上的不同。
受控组件:
在 HTML 中,表单元素(如<input>
、 <textarea>
和 <select>
)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()
来更新。
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
标准范式
这里以input表单为例:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
由于在表单元素上设置了 value
属性,因此显示的值将始终为 this.state.value
,这使得 React 的 state 成为唯一数据源。由于 handlechange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动。你也可以将 value 传递给其他 UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码(e.g. JSX中的回调函数、表单状态“迁移至”state的手动维护、对事件对象的处理等)。
处理多个输入
这里讲的“处理多个输入”是指一个回调函数用于多个可能略有不同的组件。
比如,当需要处理多个 input
元素时,我们可以给每个元素添加 name
属性,并让处理函数根据 event.target.name
的值选择要执行的操作。例如:
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
render() {
return (
<form>
<label>
参与:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
来宾人数:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
由于 setState()
自动将部分 state 合并到当前 state, 只需调用它更改部分 state 即可。
受控输入空值
在受控组件上指定 value
的 prop 会阻止用户更改输入。如果你指定了 value
,但输入仍可编辑,则可能是你意外地将 value
设置为 undefined
或 null
。
下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)
ReactDOM.render(<input value="hi" />, mountNode);
setTimeout(function() {
ReactDOM.render(<input value={null} />, mountNode);
}, 1000);
状态提升:
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。
在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
虽然提升 state 方式比双向绑定方式需要编写更多的“样板”代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。由于“存在”于组件中的任何 state,仅有组件自己能够修改它,因此 bug 的排查范围被大大缩减了。此外,你也可以使用自定义逻辑来拒绝或转换用户的输入。
如果某些数据可以由 props 或 state 推导得出,那么它就不应该存在于 state 中。
标准范式:
以一个温度判断器为例:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
思路
如果想令若干个子组件共享同一个属性,那么可将这个属性(状态)“提升”至他们的父组件中,通过this.props获取(原来是通过this.state获取)。但props是只读的,子组件无法直接修改“提升”后的属性,所以需要父组件给子组件传递回调函数,通过回调函数来修改这些属性。(是将子组件视为受控组件的处理思路)
Comments 1 条评论
博客作者 Violette Heraty
Enjoyed reading this, very good stuff, thankyou. “Talk sense to a fool and he calls you foolish.” by Euripides.