一、状态和生命周期
状态state
, 生命周期 liftcircle
.
之前说过,一旦元素被渲染了之后就不可改变了,但我们可以通过重新渲染的方法使页面得以刷新,同样我们提到过最常用的方法是编写一个可复用的具有状态的组件,这里的状态,就是我们将要说的 state
我们对上述提过的计时器tick
中的计时功能封装成一个函数式组件如下:
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
)
}
然后把他当做一个元素放入 tick
中进行渲染
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
)
}
setInterval(tick, 1000);
在这个例子中,我们将计时功能代码封装成了一个独立的可复用的组件,并通过属性date的方式将参数传入,但还不能到达我们想要的结果,那就是不能再组件内部修改参数值,组件中显示的数据依旧受控于父组件中date
属性传递过来的值,那如果我们把这个date
属性也添加到Clock
内部呢?来看下
ReactDOM.render(
<Clock />,
document.getElementById('root')
)
这时父组件中只保留了对计时组件Clock
的一个单纯的引用。剩下的事情全部依托以组件Clock
自己去实现。要怎么实现这个需求?这里React提出了另一个数据对象,即state
,它用来保存组件内部的数据,与props
类似,不同的是state
是组件私有的,并且由组件本身完全控制。它能实现数据在组件内部的修改和更新。怎么使用这个state
?继续往下讲之前我们先拓展一个知识
我们知道组件有两种定义方式,即函数式组件和类组件,虽然函数式组件更加简洁更加接近原生 javascript
,但类组件却拥有一些额外的属性,这个类组件专有特性,就是状态和生命周期钩子,到这里也能清楚知道状态的关键作用,然而函数式组件没有这两个特性,因此,在需要使用到状态state
情况下,我们需要将函数式组件转换成类组件
函数式组件转化成类组件
尝试把一个函数式组件转化成类组件,官网给出了以下步骤,以Clock
组件为例
- 创建一个继承自
React.Component
类的ES6 class
同名类 - 添加一个名为
render()
的空方法 - 把原函数中的所有内容移至
render()
中 - 在
render()
方法中使用this.props
替代props
- 删除保留的空函数声明
class Clock extents React.Component {
render() {
return (
<div>
<h1>Hello, world</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
)
}
}
到此,Clock
组件已经成功被我们修改成了一个类组件,我们便可以在其中添加本地状态state
和生命周期钩子
class Clock extends React.Component {
// 用类构造函数constructor初始化 this.state
constructor(props) {
// 使用super()将props传递给基础构造函数
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
)
}
}
这样,我们的类组件Clock
就拥有了自己的属性 this.state.date
,也就不需要引用组件向其传递值了,因此,我么可以把组件引用中的date
属性删掉,最终,我们将其渲染到DOM上,只使用组件引用,其他都交给组件Clock
自己去实现
ReactDOM.render(
<Clock />,
document.getElementById('root')
)
到这里就结束了?细心的你会发现,组件Clock
只是实现了当前时间的显示,而我们要改装的功能是一个计时器,计时功能去哪里了?没实现啊?我们需要在组件Clock
中找到一个合适的时机去实现这个功能,为此,React团队引入了 声明周期方法,也叫生命周期钩子
在类组件中添加生命周期方法
在一个具有许多组件的应用程序中,在组件被销毁时释放所占用的资源是非常重要的。就像浏览器的垃圾回收机制,近期内不需要再用的资源,应该及时清除。
当 Clock
第一次渲染到DOM时,我们要设置一个定时器 。 这在 React 中称为 “挂载(mounting)” 。它有一个生命钩子componentDidMount()
当 Clock
产生的 DOM 被销毁时,我们也想清除该计时器。 这在 React 中称为 “卸载(unmounting)” 。它的生命钩子是componentWillUnmount()
我们的计时器是在页面加载之后,页面生成初始化状态,然后由计时器去触发状态的刷新,因此,在挂载完成是去设置计时器是个非常不错的选择
componentDidMount() {
this.timerID = setInterval(
() => this.tick(), 1000
)
}
这样我们就实现了组件计时功能,或许你注意到了,在该例中,我们把timerID
存放在this
中而不是this.state
中。
其实,this.props
和this.state
也是数据对象与普通对象一样用来存放数据,只是他们被React团队赋予了新的职能, this.props
由React本身设定,用来存放在组件引用时的属性键值对对象集,不允许Coder们自己去修改;而this.state
也具有特殊的含义,即存放组件本身的、用于视觉输出的数据,但也不是说在编写React程序的时候就必须用用这两个,我们依然可以自己定义普通的数据结构。
既然state
是用于存放组件视觉输出的数据,那在render()
方法中没有被引用的,就不应该出现在state
中了。
养成良好的编码习惯,编写好计时器时,及时的编写卸载事件。卸载时我们清除的数据也是从this
中拿的。
componentWillUnmount() {
clearInterval(this.timerID);
}
挂载时我们声明了一个tick()
方法,接下来我们就要实现这个方法,是用来触发UI更新。嗯哼?UI更新?我们的页面状态state
不是已经更新了吗?为啥还要UI更新?
这里有一个非常重要的方法:setState()
。我们先把代码补充完整再说明
componentDidMount() {
// ...
}
tick() {
this.setState({
date: new Date()
})
}
componentWillUnmount() {
// ...
}
setState()
是React触发页面更新的第二个办法,第一个办法开篇即说过,即render()
方法。setState
作用就是通知React检查带状态的组件中是否含有脏值。此时react会生成一个虚拟DOM与之前的版本进行对比,只有有必要更新时才会更新。关于 state 与 setState过程 在我的另一篇文章中有详细说明,有兴趣的可以翻过去看看。
为什么不把tick()
方法写到componentDidMount()
中?因为tick()
只是一个普通方法,他不需要在生命周期中触发,也不用自动触发。只要谁调用了触发即可。因此不需要也不能放在生命周期钩子函数中。
现在这个时钟每秒都会走了。整理一下,我们整个计时器代码如下:
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')
);
整个流程的执行过程是这样的:
-
当
<Clock />
被传入ReactDOM.render()
时, React 会调用Clock
组件的构造函数。 因为Clock
要显示的是当前时间,所以它将使用包含当前时间的对象来初始化this.state
。我们稍后会更新此状态。 -
然后 React 调用了
Clock
组件的render()
方法。 React 从该方法返回内容中得到要显示在屏幕上的内容。然后,React 然后更新 DOM 以匹配Clock
的渲染输出。 -
当
Clock
输出被插入到 DOM 中时,React 调用componentDidMount()
生命周期钩子。在该方法中,Clock
组件请求浏览器设置一个定时器来一次调用tick()
。 -
浏览器会每隔一秒调用一次
tick()
方法。在该方法中,Clock
组件通过setState()
方法并传递一个包含当前时间的对象来安排一个 UI 的更新。通过setState()
, React 得知了组件state
(状态)的变化, 随即再次调用render()
方法,获取了当前应该显示的内容。 这次,render()
方法中的this.state.date
的值已经发生了改变, 从而,其输出的内容也随之改变。React 于是据此对 DOM 进行更新。 -
如果通过其他操作将
Clock
组件从 DOM 中移除了, React 会调用componentWillUnmount()
生命周期钩子, 所以计时器也会被停止。
二、正确的使用State(状态)
对于setState()
有三件事情是我们应该要知道的
(1)不要直接修改state
真正触发React对比不同版本的虚拟DOM是setState()
方法,直接修改state
页面不会刷新,这一点与原生javascript
区别较大,需要理解。
// 这么做不会触发React更新页面
this.state.comment = 'hello';
// 使用 setState() 代替
this.setState({ comment: 'hello' });
【注意】在组件中,唯一可以初始化分配this.state
的地方就是构造函数constructor(){}
(2)state(状态)更新可能是异步的
React为了优化性能,有可能会将多个setState()
调用合并为一次更新。这就导致 this.props
和this.state
可能是异步更新的,你不能依赖他们的值计算下一个state(状态)
// counter 计数更新会失败
this.setState({
counter: this.state.counter this.props.increment
})
如果我们有这种需求,可以使用以下setState()
办法:
// ES6 箭头函数法
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
// 常规函数法
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
})
(3)state(状态)更新会被合并
当你调用setState()
, React将合并你提供的对象到当前状态中。例如,你的状态可能包含几个独立的变量,然后你用几个独立的setState方法去调用更新,如下
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
合并是浅合并,所以,this.setState({comments})
在合并过程中不会改变this.state.posts
的值,但是会完全替换this.state.comments
的值
数据向下流动
无论是作为父组件还是子组件,它都无法或者一个组件是否有状体,同时也不需要关心另一个组件是定义为函数组件还是类组件。这就是为什么state
经常被称为 本地状态 或 封装状态 的原因, 他不能被拥有并设置它的组件以外的任何组件访问。那如果需要访问怎么处理?
(1)作为其子组件的props(属性)
// 在组件中使用
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
// 传递给子组件作为props
<FormattedDate date={this.state.date} />
虽然FormattedDate
组件通过props
接收了date
的值,但它仍然不能获知该值是来自于Clock
的state
, 还是 Clock
的props
, 或者一个手动创建的变量.
这种数据关系,一般称为"从上到下"或"单向"的数据流。任何state(状态)
始终由某个特定组件所有,并且从该state
导出的任何数据 或 UI 只能影响树"下方"的组件
如果把组件树想像为 props(属性)
的瀑布,所有组件的 state(状态)
就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。
各组件完全独立
借用上文的Clock
组件,我们创建一个App
组件,并在其中渲染三个Clock
:
function App() {
return (
// 之前说过组件只能返回一个根节点,所以用<div>包起来
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个Clock
都设立它自己的计时器并独立更新,如果App
中有一个数据变量,也能被三个Clock
相互独立修改。
至于何时使用有状态组件,何时使用无状态组件,被认为是组件的一个实现细节,取决于你当时的需求,你可以在有状态组件中使用无状态组件,也可以在无状态组件中使用有状态组件
三、 事件处理
通过 React 元素处理事件跟在 DOM 元素上处理事件非常相似。但是有一些语法上的区别:
- React 事件使用驼峰命名,而不是全部小写
- 通过 JSX , 传递一个函数作为事件处理程序,而不是一个字符串
// html usage
<button onclick="todo()">click me</button>
// React usage
<button onClick={todo}>click me></button>
- 在React中不能通过返回
false
来阻止默认行为。必须明确的调用preventDefault
// html usage
<a href="#" onclick="console.log('clicked'); return false">
Click me
</a>
// React usage
class ActionLink extends React.Component {
function handleClick(e) {
e.preventDefault();
console.log('clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
)
}
在这里,React团队帮Coder们实现了e事件的跨浏览器兼容问题。当使用React时,我们也不需要调用addEventListener
在DOM 元素被创建后添加事件监听器。相反,只要当元素被初始渲染的时候提供一个监听器就可以了。
当使用ES6类定义一个组件时,通常的一个事件处理程序就是类上的一个方法,看个例子,Toggle
组件渲染一个按钮,让用户在 “ON” 和 “OFF” 状态之间切换:
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 这个绑定是必要的,使`this`在回调中起作用
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
绑定类方法
在JSX回调中你必须注意 this
的指向。 在 JavaScript 中,类方法默认没有 绑定 的。如果你忘记绑定 this.handleClick
并将其传递给onClick
,那么在直接调用该函数时,this
会是 undefined
。
这不是 React 特有的行为;这是 JavaScript
中的函数如何工作的一部分,可以使用属性初始值设置来正确地 绑定(bind
) 回调,但这是实验性做法,不建议使用,以后有可能会废弃,如果你没有使用属性初始化语法
(1)可以在回调中使用一个 arrow functions
class LoginButton extends React.Component {
handleClick() {
console.log('this is: ', this)
}
render() {
// 这个语法确保 `this` 被绑定在 handleClick 中
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
);
}
}
(2)使用Function.prototype.bind
方法,相对简洁方便
<button onClick={this.handleClick.bind(this)}>
Click me
</button>
传递参数给事件处理程序
在循环内部,通常需要将一个额外的参数传递给事件处理程序,常用的有一下两种方案;
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this.id)}>Delete Row</button>
上面两个例子中,参数 e
作为 React 事件对象将会被作为第二个参数进行传递。通过箭头函数的方式,事件对象必须显式的进行传递,但是通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。
四、条件渲染
在 React 中,你可以创建不同的组件封装你所需要的行为。然后,只渲染它们之中的一些,取决于你的应用的状态。
整个组件的条件渲染
React 中的条件渲染就可在JS中的条件语句一样,使用JS操作符如if
或者条件控制符来创建渲染当前的元素,并且让React更新匹配的UI。比如我们有一个需求,需要判断用户是否登录,来显示不同组件
function UserGreeting(props) {
return <h1>Welcome back!</h1>
}
function GustGrreeting(props) {
return <h1>Please sign up.</h1>
}
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />
}
return <GuestGreeting />
}
ReactDOM.render(
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);
使用元素变量条件渲染部分内容
你可以用变量来存储元素。这可以帮助您有条件地渲染组件的一部分,而输出的其余部分不会更改。下方两个组件用于显示登出和登入按钮
function LoginButton() {
return(
<button onClick={props.onClick}>Login</button>
)
}
function LogoutButton(props) {
return (
<button onClick={props.onclick}>Logout</button>
)
}
登入登出按钮已做好,接下来需要实现有切换功能的一个有状态的组件,为了更系统化学习,我们把前面的Greeting
组件一起加进来
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoginedIn: false
}
}
handleLoginClick() {
this.setState({ isLoggedIn: true });
}
handleLogoutClick() {
this.setState({ isLoggedIn: false });
}
render() {
const isLoggedIn = this.state.isLoggedIn;
let button = null;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick.bind(this)} />
} else {
button = <LoginButton onclick={this.handleLoginClick.bind(this)} />
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />{button}</div>
</div>
)
}
}
reactDOM.render(
<LoginControl />,
document.getElementById('root')
)
使用if
是很常见的一种做法,当然也有一些更简短的语。JSX
中有几种内联条件的方法,
(1)使用逻辑与&&操作符的内联if用法
我们可以在 JSX
中嵌入任何表达式,方法是将其包裹在花括号中,同样适用于JS的逻辑与&&运算符
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{ unreadMeaasges.length > 0 &&
<h2> You have {unreadMessages.length} unread messages.
}
</div>
)
}
cosnt message = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
该案例是可以正常运行的,因为在 JavaScript
中, true && expression
总是会评估为 expression
,而 false && expression
总是执行为 false
。并且我们可以在表达式中嵌入表达式
(2)使用条件操作符的内联If-else
条件操作符 即三目表达式:condition ? trueExpression : falseExpression
// 条件渲染字符串
<div>The user is {isLoggedIn ? 'currently' : 'not'} logged in.</div>
// 条件渲染组件
<div>
{isLoggedIn ? (
<LogoutButton onClick={this.handleLogoutClick} />
) : (
<LoginButton onClick={this.handleLoginClick} />
)}
</div>
总之,遵循一个原则,哪种方式易于阅读,就选择哪种写法。并且,但条件变得越来越复杂时,可能是提取组件的好时机。
阻止组件渲染
在极少数情况下,您可能希望组件隐藏自身,即使它是由另一个组件渲染的。为此,返回 null
而不是其渲染输出。注意这里是不渲染,不是不显示。
在下面的例子中,根据名为warn
的 props
值,呈现 <WarningBanner />
。如果 props
值为 false
,则该组件不渲染:
function WarningBanner(props) {
if (props.warn) {
return null;
}
return (
<div className="warning">Warning</div>
)
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = { showWarning: true }
}
handleToggleClick() {
this.setState(prevState => ({
showWarning: !prevState.showWarning
}));
}
render() {
return (
<div>
<Warningbanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick.bind(this)}>
{ this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
)
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
)
从组件的 render
方法返回 null
不会影响组件生命周期方法的触发。 例如, componentWillUpdate
和componentDidUpdate
仍将被调用。因此需要组件刚载入时就要判断执行返回null