先来看个例子引出生命周期
钩子函数(生命周期回调函数)
, 会在特定的时刻调用。需求:定义组件实现以下功能:
- 让指定的文本做显示/隐藏的渐变动画
- 从完全可见,到彻底消失,耗时2S
- 点击”不活了“按钮从界面中卸载组件
效果如下:
具体代码实现:
<script type="text/babel">
// 创建组件
// 生命周期回调函数 <=> 生命周期钩子函数(我定义的,react帮我调用的)
class Life extends React.Component {
// opacity属性设置一个div元素的透明度级别,默认值为1(完全不透明)
state = { opacity: 1 }
death = () => {
// 清除定时器
// clearInterval(this.timer)
// 卸载组件
// unmountComponentAtNode:卸载组件在哪个节点里
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
// 组件挂载完毕之后调用
componentDidMount() {
// 定时器是挂载在window上的
// 当组件卸载完之后,定时器也不会消失
this.timer = setInterval(() => {
// 获取原状态
let { opacity } = this.state
// 减小0.1
opacity -= 0.1
if (opacity <= 0) opacity = 1
// 更新状态,设置新的透明度
this.setState({ opacity: opacity })
}, 200)
}
// 组件将要卸载时调用
componentWillUnmount() {
// 清除定时器
clearInterval(this.timer)
}
//render调用的时机:初始化渲染,状态更新之后
render() {
console.log('render')
// render被调1+n次(1是初始化显示,n是更改状态的次数)
// 每次更新state状态都会调用一次render并新建一个计时器
// 设置一个定时器,让h2标签里的字体透明度从1到0再到1一直循环下去
// setInterval(() => {
// 获取原状态
// let { opacity } = this.state
// 减小0.1
// opacity -= 0.1
// if (opacity <= 0) opacity = 1
// 更新状态,设置新的透明度
// this.setState({ opacity: opacity })
// }, 200)
{/*
为什么不能将计时器放在render中??
如果setInterval计时器放在render中,该计时器内部又封装了this.setState方法,
第一次函数挂载到页面中,调用setInterval函数,函数每过200ms就会对state中的数据进行更改,
每次更改后又会调用render对组件更新重新渲染,重新渲染时又触发了serInterval函数,
setInterval函数一直在被调用,而之前的计时器又没有被关闭,页面在不断的生成计时器,
导致CPU不断上升,所以计时器不能放在render中。
*/}
return (
<div>
<h2 style={{ opacity: this.state.opacity }}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
// 渲染组件(把组件挂载(mount)到页面上)
ReactDOM.render(<Life />, document.getElementById('test'))
</script>
案例:
需求:
- 点击+1 按钮,求和数自动+1;
- 点击卸载组件,组件从页面中卸载;
- 点击不更改任何状态中的数据,强制更新一下,不更改状态中的数据,组件强制更新;
效果如下:
代码如下:
<script type="text/babel">
// setState 和 forceUpdate 流程
// 创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props)
// 初始化状态
this.state = { count: 0 }
}
// 加1按钮的回调
add = () => {
// 获取原状态
const { count } = this.state
// 更新状态
this.setState({ count: count + 1 })
}
// 卸载组件按钮的回调
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
// 强制更新按钮的回调
force = () => {
this.forceUpdate() //通过forceUpdate方法可以做到即使不使用state属性也可以更新界面
}
// 初始化阶段
// 组件将要挂载之前的钩子
componentWillMount() {
console.log('Count---componentWillMount')
}
// 组件挂载完毕的钩子
componentDidMount() {
console.log('Count---componentDidMount')
}
// 更新阶段
// 控制组件更新的“阀门” 。
// 如果不写这个钩子,react底层会自动补一个默认返回值为true的shouldComponentUpdate(),表示可以更新,
// 如果返回值为false,则不会执行后续的生命周期函数
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
return true
}
//组件将要更新的钩子
componentWillUpdate() {
console.log('Count---componentWillUpdate')
}
// 组件更新完毕的钩子
componentDidUpdate() {
console.log('Count---componentDidUpdate')
}
// 卸载阶段
// 组件将要卸载的钩子
componentWillUnmount() {
console.log('Count---componentWillUnmount')
}
render() {
console.log('Count---render')
const { count } = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(< Count />, document.getElementById('test'))
</script>
组件挂载到页面时:
constructor(当前类的构造函数) => componentWillMount(组件将要挂载之前的钩子) => render(渲染组件到页面) => componentDidMount (组件挂载完毕的钩子)
组件更新页面时(setState()
):
shouldComponentUpdate(控制组件更新的“阀门”:如果返回值为false,则不会执行后续的生命周期函数
)=> componentWillUpdate(组件将要更新的钩子)=> render(渲染组件到页面)=> componentDidUpdate(组件更新完毕的钩子)
组件强制更新页面时(forceUpdate()
):绕开了shouldComponentUpdate阀门,即使不更新state中的数据,shouldComponentUpdate的返回值为false,也会更新页面
componentWillUpdate(组件将要更新的钩子)=> render(渲染组件到页面)=> componentDidUpdate(组件更新完毕的钩子)
组件卸载:
componentWillUnmount(组件将要卸载的钩子)
总结:
生命周期的三个阶段(旧)如下:
初始化阶段
:由ReactDOM.render()
触发—页面初次渲染
初始化
的事,例如:开启定时器,发送网络请求,订阅消息)(出生那天)更新阶段
:由组件内部this.setState()
或父组件重新render
触发
卸载阶段
:由ReactDOM.unmountComponentAtNode()
触发
收尾
的事,例如:关闭定时器,取消订阅消息)(死亡那天)案例:
需求:
- 点击
换车
按钮,显示车从”奔驰“ 变成 ”宝马“。
效果如下:
代码如下:
<script type="text/babel">
// 父组件render流程
// 父组件A
class A extends React.Component {
// 初始化状态
state = { carName: '奔驰' }
changeCar = () => {
this.setState({ carName: '宝马' })
}
render() {
return (
<div>
<div>我是A组件</div>
{/*
点击换车,调用了changeCar,更改了A组件里的状态,
导致A组件调用了render,正是父组件重新render
*/}
<button onClick={this.changeCar}>换车</button>
{/*
父组件A将state中的carName传给子组件B的props.carName中
*/}
<B carName={this.state.carName} />
</div>
)
}
}
// 子组件B
class B extends React.Component {
//组件将要接收新的props的钩子
// 必须经过setStateA组件改变,然后调用render,这时B组件重新接收一个新的值,重新调用render
componentWillReceiveProps(nextProps) {
// this.props 表示旧的props中的数据
// nextProps 表示传入新的props
console.log('B---componentWillReceiveProps', nextProps)
}
render() {
console.log('B---render')
return (
{/*
子组件接收carName,从props中取出
*/}
<div>我是B组件,接收到的车是:{this.props.carName}</div>
)
}
}
// 渲染组件
ReactDOM.render(<A />, document.getElementById('test'))
</script>
父组件render(父组件A传值给子组件B):
componentWillReceiveProps
(组件将要接收新的props的钩子) => shouldComponentUpdate(控制组件更新的“阀门”) => componentWillUpdate(组件将要更新的钩子) => render(渲染) => componentDidUpdate(组件更新完毕的钩子)
componentWillReceiveProps:在第一次传入props时不调用,只有在props更新时才会执行,可以用此钩子监听子组件props的改变。
17.0:新的生命周期函数删除了
componentWillMount
、componentWillReceiveProps
和componentWillUpdate
,增加了getDerivedStateFromProps
和getSnapshotBeforeUpdate
在写这个生命周期函数时,要在getDerivedStateFromProps加上static
,否则会报错。
原因是:在Count组件中,getDerivedStateFromProps生命周期钩子定义在了实例身上,将会被忽略,不能写成这个钩子是给实例用的,实例不能调用这个钩子。要定义成一个静态方法,加上一个关键字static。
加上static之后,页面打印出了getDerivedStateFromProps,但是页面又显示一个报错提示:
原因是类Count自身上的getDerivedStateFromProps函数应该返回一个
状态对象
,或者null,必须要有返回值
。但是却返回了undefined。
当返回值是一个状态对象时:
class Count extends React.Component {
// 构造器
constructor(props) {
console.log('Count---constructor')
super(props)
// 初始化状态
this.state = { count: 0 }
}
// 从props中得到一个派生的状态
// 若state 的值在任何时候都取决于 props,那么可以使用
static getDerivedStateFromProps(props,state) {
console.log('getDerivedStateFromProps', props,state)
//return {count:108}
return props
}
}
// 渲染组件
ReactDOM.render(< Count count='199' />, document.getElementById('test'))
return {count:108}利用这个返回值可以指定一个状态,导致状态不能更改了,只要返回一个状态对象,对象里面包含的key,在初始状态里也有,那么就不以初始化的状态为主了,以返回的状态对象为主。
getDerivedStateFromProps()可以接收props参数。给Count组件传递props(< Count count=‘199’ />),然后getDerivedStateFromProps(props)可以接收到,返回props(return props),所以从props中得到一个派生的状态
(不是自己写的),即 state 的值在任何时候都取决于 props
,无论是初始化,或者修改都不起作用。
getDerivedStateFromProps()可以接收state参数,即初始化的状态对象。
言而总之,新的静态
getDerivedStateFromProps
生命周期方法在组件实例化之后以及重新渲染之前调用。它可以返回一个对象来更新state
,或者返回null
来表示新的props
不需要任何state
的更新。
如果是如下的写法:
// 在更新之前获取快照
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('getSnapshotBeforeUpdate')
}
当点击 点我+1的按钮时,会出现报错,原因是:一个
snapshot value
,或者null
必须得返回。但是却返回一个undefined。说明,这个getSnapshotBeforeUpdate钩子函数必须要有返回值
。
正确的写法:
// 在组件更新之前获取快照
// 快照可以是任何值,如字符串,数组,对象,函数
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('getSnapshotBeforeUpdate')
return 'atguigu' //这个返回值将作为snapshotValue传给componentDidUpdate
}
在官方文档中规定:
getSnapshotBeforeUpdate()
在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(备份之前的数据)(例如,滚动位置)。此生命周期方法的任何返回值
将作为参数传递给componentDidUpdate()
。- 此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。
- 应返回 snapshot 的值(或
null
)。
解释:
snapshot value
是组件在发生更新之前从DOM中捕获的一些信息,如表格高度,滚动条的位置等。
getSnapshotBeforeUpdate生命周期方法的任何返回值
将作为参数传递给 componentDidUpdate()
// 组件更新完毕的钩子
componentDidUpdate(preProps, preState, snapshotValue) {
console.log('Count---componentDidUpdate', preProps, preState, snapshotValue)
//打印结果:{count:'199'} {count:0} atguigu
}
效果:
需求:
- 在一个固定高度(150px)的容器中,每隔1s出现一条新闻(30px),并显示在最上方,超出高度的新闻以滚动条的形式显示,类式朋友圈,微博动态。
- 鼠标控制滚动条滚动到某一位置,界面不动,不会自动跳转到最顶部新出现的新闻上去。
代码实现:
css:
jsx:
<script type="text/babel">
class NewsList extends React.Component {
state = { newsArr: [] }
componentDidMount() {
setInterval(() => {
// 获取原状态
const { newsArr } = this.state
// 模拟一条新闻
const news = '新闻' + (newsArr.length + 1)
// 更新状态
this.setState({ newsArr: [news, ...newsArr] })
//news:新的新闻,...newsArr:原先的新闻
}, 1000)
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return this.refs.list.scrollHeight //拿到更新之前的内容区的高度
}
componentDidUpdate(prevProps, prevState, height) {
//新的新闻出现之后的高度减去之前的高度
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
// this.refs.list.scrollTop = this.refs.list.scrollTop + this.refs.list.scrollHeight - height
// 6: 30 = 0 + 180 - 150
// 7: 60 = 30+ 210 - 180
}
render() {
return (
<div>
<div className="list" ref="list">
{ //map()实现遍历,map()方法定义在JavaScript的Array中,
//它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
this.state.newsArr.map((news, index) => {
return <div key={index} className="news">{news}</div>
})
}
{/*
新闻7
新闻6
新闻5
新闻4
新闻3
新闻2
新闻1
*/}
</div>
</div>
);
}
}
ReactDOM.render(<NewsList />, document.getElementById('test'))
</script>
scrollHeight:这个
只读属性
指的是一个元素内容高度
的度量,包括由于溢出导致的视图中不可见内容
。scrollTop:指的是一个元素的
内容顶部
到它的可视窗口的顶部
的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的scrollTop
值为0
。
解释代码:
overflow: auto;
(当内容溢出时,会提供一个滚动条
)state = { newsArr: [] }
componentDidMount() {
setInterval(() => {
// 获取原状态
const { newsArr } = this.state
// 模拟一条新闻
const news = '新闻' + (newsArr.length + 1)
// 更新状态
this.setState({ newsArr: [news, ...newsArr] })
//news:新的新闻,...newsArr:原先的新闻
}, 1000)
}
{ //map()实现遍历,map()方法定义在JavaScript的Array中,
//它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
this.state.newsArr.map((news, index) => {
return <div key={index} className="news">{news}</div>
})
}
拿到内容区的高度
,传给componentDidUpdate这个钩子作为第三个参数,最关键的一步,在componentDidUpdate这个钩子里实现上述这个功能:动态地调整这个this.refs.list.scrollTop往上窜地高度:拿到更新之后的内容区高度,减去更新之前的内容区高度,加上原先计算前的scrollTop 就能设置新加的元素往上窜地高度,从而实现上述功能。componentDidUpdate(prevProps, prevState, height) {
//新的新闻出现之后的高度减去之前的高度
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
// this.refs.list.scrollTop = this.refs.list.scrollTop + this.refs.list.scrollHeight - height
}
总结:
生命周期的三个阶段(新)如下:
初始化阶段:由ReactDOM.render()触发—初次渲染
getDerivedStateFromProps()
更新阶段:由组件内部this.setSate()或父组件重新render触发
getDerivedStateFromProps()
getSnapshotBeforeUpdate()
卸载组件:由ReactDOM.unmountComponentAtNode()触发
现在使用会出现警告,下一个大版本需要加上UNSAFE_前缀才能使用,以后可能会被彻底废弃,不建议使用。