元素渲染
首先需要在HTML中添加一个根节点,在此节点中的内容都将由React DOM来管理。
<div id="root"></div>
接着通过ReactDOM.render() 的方法来将元素渲染到根DOM节点中。
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
React元素都是不可变的,当元素被创建之后是无法更改的。现阶段唯一更新页面的方法就是创建新元素然后通过ReactDOM.render() 更新。
组件&Props
组件
定义一个组件有两种方式,可以使用函数定义也可使用类定义,主要的区别方式就是一种是无状态组件一钟是有状态组件。后面会讲到这两种组件的区别。
函数定义:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
类定义:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
接下来我们就可以使用用户自定义的组件了。
const element = <Welcome name="Sara" />;
然后同样是使用ReactDOM.render() 的方法将组件渲染到视图上。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
我们也可以在一个组件中嵌套另一个组件,通过这样的方式来实现一些更为丰富的内容。
Props
在刚才的例子我们使用到了props,props里的内容就是jsx的属性,通过props我们就可以实现父组件向子组件之间的通信。当props是只能读的,我们绝不能修改props的内容。
State & 生命周期
在最开始的时候我们更新视图只能重新创建元素再渲染,这里我们将介绍一种更为简便的方法。之前我们也曾提到过有状态和无状态组件,我们就是通过这个状态来实现视图的更新。
首先使用类定义一个组件
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
接下来我们为这个组件添加一个状态。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
注意到组件的构造函数中的super(props);,我们应始终使用props调用基础构造函数。
组件的生命周期
生命周期函数在react新版本中有了一些更改。将会移除
componentWillMount,componentWillReceiveProps,componentWillUpdate 这三个生命周期函数,新增两个生命周期函数 getDerivedStateFromProps,getSnapshotBeforeUpdate。
- componentDidMount :在第一次渲染后调用,只在客户端。可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作。
- shouldComponentUpdate :返回一个布尔值。在组件接收到新的props或者state时被调用。 可以在你确认不需要更新组件时使用。
- componentDidUpdate: 在组件完成更新后立即调用。在初始化时不会被调用。
- componentWillUnmount:在组件从 DOM 中移除之前立刻被调用。
- getDerivedStateFromProps:在每次渲染之前都会调用,不管造成重新渲染的原因,不管初始挂载还是后面的更新都会调用。
- getSnapshotBeforeUpdate:在最新的渲染数据提交给DOM前会立即调用,它让你在组件的数据可能要改变之前获取他们。
现在了解了react的生命周期函数之后,我们就可以在生命周期函数中添加一些操作,让我们的时钟能够每秒钟更新一次。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
我们在组件渲染之后添加了一个定时器每秒执行一次tick()并使用set的方式更新视图,然后在组件卸载之前卸载了定时器。现在我们的时钟就能动起来了。值得注意的是我们直接修改state的值并不会引起视图上的改变,只能使用setState的方式重新渲染组件,而且构造函数式唯一能够初始化state的地方。
事件处理
在react中你需要传入一个函数作为时间处理函数,这和传统的DOM元素写法有写不同。
<button onClick={activateLasers}>
Activate Lasers
</button>
在react中不能使用return false的方式阻止默认行为,必须使用preventDefault。
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
在react中类的方法默认不会绑定this的,当你没有绑定this时,调用this的值会是undefined。所以我们需要像这样绑定this。
this.handleClick = this.handleClick.bind(this);
当你觉得使用bind很麻烦的时候你可以使用实验性的属性初始化器语法 来正确的绑定回调函数:
handleClick = () => {
console.log('this is:', this);
}
条件渲染
React使用JavaScript中的if和条件运算符来创建表示当前状态的元素,然后再来渲染。
function UserGreeting(props) {
return <h1>Welcome back!</h1>;
}
function GuestGreeting(props) {
return <h1>Please sign up.</h1>;
}
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
// Try changing to isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);
这就是一个简单的例子。我们还可以使用变量来储存元素,使我们的代码变得更加简洁易读。
当我们想要阻止一些组件渲染时,让组件中return返回null即可实现,但使用这种方式并不会影响该组件的生命周期方法回调,像componentWillUpdate之类的方法依然可以被调用。
列表&Keys
如果我们想要重复渲染某些组件,手动的一个一个的渲染显然不是明智的做法。这时我们就可选择使用一个循环或是使用map来对需要渲染的对象进行遍历。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
像这样,我们需要渲染的对象就全部储存在了listItems中。接下来只需把整个listItems插入到ul元素中,然后渲染进DOM:
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
当然,我们也可以对现在的组件进行重构,把需要被渲染的数组当做参数,组件接收到这个参数之后输出一个列表。
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
虽然这段代码可以顺利运行,但是我们得到一个警告a key should be provided for list items
,它警告我们创建一个元素时,必须包括一个key值,并且这个key值必须是唯一的,这是为什么呢?
React想要识别DOM中那些元素发生了变化就需要可以的辅助才行,所以我们应当给数组中的每个元素都赋予一个唯一的标识符key。
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
表单
受控组件
在HTML中,类似于<input>
,<textarea>
的表单元素会维持自身的状态,并根据用户的输入进行更新。在React中,这些状态都保存在组件的状态属性中,并且只能用set的方法进行更新。由React控制其值的输入的表单元素就被成为“受控组件”。
就比如说下面这个例子:
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('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
input元素的value属性被我们设置为React数据源上 this.state.value
的值,而且每次按键都会触发handleChange
来更新当前的state,所以值也会随着用户输入的更新而更新。
状态提升
在我们使用React的时候可能会遇到几个组件需要共用状态数据的情况,这时候我们有一种十分优雅的处理方式,就是将这部分需要共享的状态提升至他们的父组件中,这种方法就是状态提升。让我们来看看具体需要怎么做。
一开始我们先创建一个BoilingVerdict
组件,他会接受一个celsius
这个表示温度的变量作为他的props属性,然后根据这这个温度来返回一些内容。
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>水会烧开</p>;
}
return <p>水不会烧开</p>;
}
接着,我们需要写一个带input元素的组件来接受用户的输入,然后将温度的值保存在this.state.temperature
中。之后,BoilingVerdict
组件就会根据用户输入的温度值来渲染出不同的内容。
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
return (
<fieldset>
<legend>输入一个摄氏温度</legend>
<input
value={temperature}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
添加第二个输入框
现在我们接到了一个新的需求,除了需要可以接受摄氏度的输入,我们还需要多一个输入框,让用户可以输入华氏温度,并且这两个输入框还能同步。
我们从Calculator
中抽离出一个TemperatureInput
组件出来,为他添加一个表示温度单位的属性。
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
然后我们就可以根据现有的TemperatureInput
组件来渲染出两个不同单位的温度输入框。
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
虽然现在有了两个输入框,但这并不符合我们的需求,当我们在一个输入框中输入时,另一个输入框并不会跟着一起更新。
而且现在表示温度的数据存在于TemperatureInput
组件中,BoilingVerdict
也不能渲染出结果。
转换函数
在我们进行状态提升之前我们先要写两个转换函数,能够将摄氏度和华氏度相互转换。
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
光有这个转换函数也并不够,我们还需要一个函数能够接受temperature
变量,第二个参数则是我们的转换函数,最后他可以根据一个输入框的值来决定另一个输入框的值。
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
状态提升
接下来就是关键的部分,到现在为止两个TemperatureInput
组件都在自己的state中独立保存着数据。但我们想要的是这两个输入框能够进行同步,当在一个输入框输入时可以在另一个输入框中自动转换。这时我们就需要将TemperatureInput
组件所保存的state提升到Calculator
中。
我们先把TemperatureInput
组件中的this.state.temperature 替换为 this.props.temperature,并假设this.props.temperature是已经存在了。
render() {
// 之前的代码: const temperature = this.state.temperature;
const temperature = this.props.temperature;
因为temperature是作为prop从父组件中传递下来的,而我们知道props是只读,所以Temperature
组件对这个属性是没有控制权的。
当温度有更新时,就会调用到this.props.onTemperatureChange
。
handleChange(e) {
// 之前的代码: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
onTemperatureChange
和temperature
两个props属性均是从父组件中传递过来的。父组件可以通过自己的反对来响应数据的变化,然后再将值渲染到两个输入框组件,到这里思路就基本清晰了。
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>在{scaleNames[scale]}:中输入温度数值</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
接下来就是Calculator
。我们要定义两个输入框的数据源,能让两个输入框渲染出所需要的数据。
当在摄氏度框中输入37时,Calculator
的state就是这样。
{
temperature: '37',
scale: 'c'
}
现在最后来完善一下代码。
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>
);
}
}
此时,我们就完成了两个输入框数据同步的问题。
感悟
React和Vue还有Angular有着很大的不同,几乎所有的内容都是使用JS来完成,并且在React中组件的特征更加明显,想要学好React,一个良好的JS基础是必不可少的。