一、元素渲染
元素(elements
)是构成React应用的最小单元,元素描述了想要在屏幕中看到的内容,如:
const element = <h1>Hello, world</h1>;
和DOM元素不同的是,React元素是纯对象,创建的代价低。并且React会进行优化处理,只把有必要的变化更新到DOM上。此外,元素和组件的概念,是不一样的,组件是由元素组成的。
二、将元素渲染进DOM
在React中,使用ReactDOM.render()
方法来将React元素渲染进一个DOM中。如:
ReactDOM.render(
element,
document.getElementById('root')
)
React元素是不可变的,所以一旦一个元素创建完成后,我们是无法改变其内容或者属性的。一个元素就像是动画里的一帧,它代表UI在某一时间点的样子。如果非要使用元素来构成可变化的UI界面,就需要使用setInterval
了,如:
function tick() {
const element = (
<div>Now is {new Date().toLocaleTimeString()}</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在实际开发中,大多数React应用只会调用一次ReactDOM.render()
,所以更好的方式是使用有状态组件
三、组件和Props
组件(component
)能够将UI划分为独立的、可复用的部分,这样我们就只需专注于构建每一个单独的部件。
从概念上看,组件就像是函数:接受任意的输入(称为属性,Props
),返回React元素。React中有两种定义组件的方式:函数定义
和类定义
1、函数定义组件
这种方式是最简单的定义组件的方式,就像写一个JS函数一样,如:
function Welcome (props) {
return <h1>Hello, {props.name}</h1>;;
}
2、类定义组件
还可以使用ES6里的类来定义一个组件,如下所示:
class Welcome extends React.Component {
render () {
return <h1>Hello, {this.props.name}<h1>;
}
}
这种方式比起函数定义
方式则更加灵活
3、组件渲染
先前,我们遇到的React元素只是呈现一个DOM标签,如:
const element = <div />
然而,React元素也可以是用户自定义的组件
,如:
const element = <Welcome name="Tom" />
Welcome组件中声明了一个属性name="Tom"
,而这个属性,将以props.name
的方式传递给组件,如下方式:
function Welcome (props) {
return <h1>Hello, {props.name}</h1>;
}
此时,对于以下的代码:
ReactDOM.render(
<Welcome name="RuphiLau" />,
document.getElementById('root')
)
最终就会以<h1>Hello, RuphiLau</h1>
的方式呈现。在这个过程中,发生了如下的事情:
- 对
<Welcome name="RuphiLau" />
元素调用了ReactDOM.render()
丰富 - React将
{ name: 'RuphiLau' }
作为props实参来调用Welcome组件 - Welcome完成渲染,返回
<h1>Hello, RuphiLau</h1>
元素 - ReactDOM计算最小更新代价,然后更新DOM
4、组合组件
组件是可以组合的。即组件内部可以引用其他组件,如:
function Welcome (props) {
return <h1>Hello, {props.name}</h1>;
}
function App () {
return (
<div>
<Welcome name="Tom" />
<Welcome name="Jack" />
<Welcome name="Mike" />
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
注意: 在React中,组件必须返回单一
的根元素,这也是为什么App组件中需要用<div>
标签包裹的原因。如以下的方式,是错误的(因为它有3个根元素):
function App () {
return (
<Welcome name="Tom" />
<Welcome name="Jack" />
<Welcome name="Mike" />
)
}
5、属性是只读的
考虑以下这种情况:
function sum (a, b) {
return a + b;
}
这种函数称为纯函数
:它不改变自己的输入值,且总是对相同的输入返回相同的结果。
与之对立的,则是非纯函数
,如:
function withdraw (account, amount) {
account.total -= amount;
}
非纯函数
在函数内改变了输入的参数。在React中,无论是通过function
还是class
声明组件,我们都不应该修改它自身的属性(props
)。虽然React相当灵活,但是它也有一个严格的规定:所有的React组件都必须像纯函数那样来使用它们的props
四、State与生命周期
使用类定义组件
有一些额外的好处,如拥有本地状态
这一特性。
以下是一个类定义组件
class Clock extends React.Component {
render () {
return (
<div>
<h1>Hello, world!</h1>
<h2>Now is {this.props.date.toLocaleTimeString()}</h2>
</div>
);
}
}
需要注意的有:
类名
即为组件名
(无论是函数定义组件还是类定义组件,组件名称的首字母都必须大写,并且继承自React.Component
)- 使用
render()
方法,用来返回需要呈现的内容
1、在类中加入state
state是属于一个组件自身的。我们可以在类的构造函数constructor
中来初始化状态,如:
constructor (props) {
super(props)
this.state = {
date: new Date()
}
}
如此一来,我们就可以在render()
函数中使用this.state.xxx
来引用一个状态
2、生命周期
在应用里,往往都会有许许多多的组件。在组件销毁后,回收和释放它们所占据的资源非常重要。
在时钟应用
的例子里,我们需要在第一次渲染到DOM的时候设置一个定时器,并且需要在相应的DOM销毁后,清除这个定时器。那么,这种情况下,React为我们提供了生命周期的钩子函数,方便我们进行使用。在React中,生命周期分为:
1)Mount
已插入真实DOM
2)Update
正在重新渲染
3)Unmount
已移出真实DOM
而相应的,生命周期钩子函数有:
componentWillMount
componentDidMount
componentWillUpdate(newProps, nextState)
componentDidUpdate(prevProps, prevState)
componentWillUnmount()
此外,还有两种特殊状态的处理函数:
componentWillReceiveProps(nextProps)
已加载的组件收到新的参数时调动shouldComponentUpdate(nextProps, nextState)
组件判断是否重新渲染时调用
因此,基于生命周期钩子函数,我们可以实现一个时钟应用如下:
class Clock extends React.Component {
constructor (props) {
super(props);
this.state = {
date: new Date()
}
}
tick () {
this.setState({
date: new Date()
});
}
componentDidMount () {
this.timerId = setInterval(() => {
this.tick()
}, 1000);
}
componentWillUnmount () {
clearInterval(this.timerId);
}
render () {
return (
<div>Now is {this.state.date.toLocaleTimeString()}</div>
);
}
}
需要注意的是:
1)render()
里用不到的state
,不应该声明在state
里
2)不能直接使用this.state.xxx = xxx
的方式来改变一个state
的值,应该使用this.setState()
。如:
setName () {
this.setState({
name: 'RuphiLau'
})
}
this.setState()
会自动覆盖this.state
里相应的属性,并触发render()
重新渲染。
3)状态更新可能是异步的
React可以将多个setState()
调用合并成一个调用来提升性能。且由于this.props
和this.state
可能是异步更新的,所以不应该依靠它们的值来计算下一个状态。这种情况下,可以给setState
传入一个函数,如:
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
五、事件处理
React元素的事件与DOM元素类似,不过也有一些区别,如:
1)React事件使用camelCase
命名(onClick
),而不是全小写的形式(onclick
)
2)使用JSX,传入的是事件的句柄,而不是一个字符串
如以下的HTML:
<button onclick="increment()">ADD</button>
使用React的方式描述如:
<button onClick={increment}>ADD</button>
还有一个不同在于,在原生DOM中,我们可以通过返回false
来阻止默认行为,但是这在React中是行不通的,在React中需要明确使用preventDefault()
来阻止默认行为。如:
function ActionLink () {
function handleClick (e) {
e.preventDefault();
alert('Hello, world!');
}
return (
<a href="#" onClick={handleClick}>Click Me</a>
);
}
这里,事件回调函数里的event
是经过React特殊处理过的(遵循W3C标准),所以我们可以放心地使用它,而不用担心跨浏览器的兼容性问题。
注意: 在使用事件回调函数的时候,我们需要特别注意this
的指向问题,因为在React里,除了构造函数和生命周期钩子函数里会自动绑定this为当前组件外,其他的都不会自动绑定this
的指向为当前组件,因此需要我们自己注意好this的绑定问题,
通常而言,在一个类方式声明的组件里使用事件回调,我们需要在组件的constructor
里绑定回调方法的this指向,如:
class Counter extends React.Component {
constructor (props) {
super(props);
this.state = {
counter: 0
}
// 在这里绑定指向
this.increment = this.increment.bind(this);
}
increment () {
this.setState({
counter: this.state.counter + 1
});
}
render () {
return (
<div>
The counter now is: {this.state.counter}
<button onClick={this.increment}>+1</button>
</div>
);
}
}
当然,我们还有另外一种方法来绑定指向,就是使用实验性
的属性初始化语法,如:
class Counter extends React.Component {
increment: () => {
this.setState({
counter: this.state.counter + 1
});
}
// ...
}
3)像事件处理程序传递参数
我们可以为事件处理程序传递额外的参数,方式有以下两种:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
需要注意的是,使用箭头函数的情况下,参数e
要显式传递,而使用bind的情况下,则无需显式传递(参数e
会作为最后一个参数传递给事件处理程序)