React笔记(四)
1.旧组件生命周期
-
在类式组件上存在着一些生命周期函数,其贯穿了整个React的组件的生命周期。旧版的整体的组件生命周期如下图所示。旧版的组件生命周期分为两个阶段。挂载卸载阶段与更新阶段。render则是独立于这两个阶段作为渲染阶段存在。
-
在组件挂载卸载阶段中,存在四步操作
-
最初,对于一个类式组件,要先将其实例化,只有实例化之后才可以通过React将
JSobject
的对象渲染成一块DOM。那么为了实例化,首先调用的就是构造器constructor
。其接受参数props
,并且存在着书写规范,如果使用了constructor
,就必须要constructor
内书写super
,并将传给constructor
的props
,传入super
内,否则会导致指向错误。可以在构造器里,完成状态的初始化赋值,事件的绑定等。constructor(props){ super(props); //初始化state this.state = {xxx:xxx}; }
-
在实例化之后,会执行
componentWillMount
,这个生命周期钩子发生在组件已经被实例化之后,但却未挂载在页面上进行渲染时。可以理解为在调用render方法渲染组件前的最后一次修改状态的机会。 -
当组件已经挂载在页面上,并完成第一次渲染后,这时会执行
componentDidMount
。此时组件渲染的DOM节点已经在页面上生成。这个生命周期函数在实际开发中是比较常用的。经常用于开启定时器,发送ajax请求,订阅消息等。 -
当组件取消挂载时,这个组件会进行卸载与销毁,此时会执行
componentWillUnmount
,在这个生命周期函数内完成组件的卸载与销毁。所有在componentDidMount
添加的任务都需要在此生命周期函数内撤销,例如清除开启的定时器,移除事件监听器,或者撤销还未获取的请求信息对状态的影响。下面是应用示例。class Child extends Component { constructor(props) { super(props); this.state = { loading: true, content: '', count: 0 }; this.btn = React.createRef(); this.isMount = true; } alertText(){ alert('这是一个警告'); } componentDidMount() { //未接收到返回值的异步请求 setTimeout(() => { this.isMount && this.setState({ loading: false, content: '加载完成' }) }, 10000); //绑定事件监听器 this.btn.current.addEventListener('click', this,this.alertText, false); //设置定时器 this.timer = setInterval(() => { let { count } = this.state; count++; this.setState({ count }); }) } //销毁组件内部开启的任务,不销毁的话,卸载组件后会出现错误 componentWillUnmount(){ //移除事件监听器 this.btn.current.removeEventListener("click", this.alertText, false); //撤销还未获取的请求信息对状态的影响 this.isMount = false; //清除开启的定时器 clearInterval(this.timer); } render() { return ( <div> {this.state.loading ? <p>加载中</p> : <p>{this.state.content}</p>} <button ref={this.btn}>点击我弹出alert</button> </div> ) } } export default class Demo extends Component { state = { show: true }; render() { return ( <div> {this.state.show && <Child />} <button onClick={ () => { this.setState({ show: false }) } }>点击我移除child组件</button> </div> ) } }
-
对于为什么不在页面刷新前的
componentWillMount
中进行异步请求等,有以下原因。如果是异步请求,即使在页面刷新前的componentWillMount
中使用,也避免不了画面的二次渲染,即使使用,也不会减少开销。此外componentDidMount
时,组件已经挂载到DOM树上了,此时根据请求的响应值来对DOM进行填充也更加妥当。所以相对于componentWillMount
,componentDidMount
更适用于上述功能。最后componentWillMount
作为旧版的生命周期函数,更应该减少使用。
-
-
在组件更新阶段中,也存在四步操作
-
首先是
componentWillReceiveProps
,当父组件的改变传给当前组件的props时,会运行此生命周期函数,其接收一个参数nextProps
,即修改后的props。在此函数运行时。组件内部的props还未改变,因此可以通过比较this.props
和nextProps
来决定对状态的赋值,并渲染组件。但在组件挂载期间,此函数不会被调用。 -
在接收了父组件更改后的props,或组件内部调用了
setState
设置了新的状态时,这是会触发一个门卫生命周期函数shouldComponentUpdate
,其接收两个参数nextProps
和nextState
,并返回一个布尔型的值代表是否运行接下来的生命周期函数,以及调用render
进行重新渲染。目的是为了提高性能,但大多数情况下不使用此方法优化性能。并且当使用forceUpdate
时,会跳过这个门卫函数,直接进行页面的重新渲染。//只有当状态中value变更时才进行重新渲染 shouldComponentUpdate: function(nextProps, nextState){ return this.state.value === nextState.value; }
-
在组件进行页面的重新渲染之前,会调用
componentWillUpdate
函数,与shouldComponentUpdate
接收的参数相同。同样,初始渲染也不会调用此方法。应用场景比较局限,并且常常搭配其下一个生命周期函数componentDidUpdate
实现一些功能,比如说在数据更新时修改页面上的DOM。下列代码实现了当数据不断更新时锁定当前的滚动条的位置。export default class Demoe extends Component { constructor(props) { super(props); this.state = { content: [], scrollY: 0 }; this.listDiv = React.createRef(); } //倒序增加展示内容 componentDidMount() { this.timer = setInterval(() => { var content = [`新闻${this.state.content.length + 1}`, ...this.state.content]; this.setState({ content }); }, 1000); } componentWillUnmount() { clearInterval(this.timer); } //获取数据变更后,但画面渲染前时滚动条的偏移量并保存。 //这里的偏移量不要放在state中保存,放在实例对象上保存,如果在这个函数中使用setState,会导致无限循环渲染。 componentWillUpdate(nextProps, nextState) { if (this.state.content.length < nextState.content.length) { this.previousScrollOffset = this.listDiv.current.scrollHeight - this.listDiv.current.scrollTop; } } //当画面渲染出新的数据后,将保存的滚动条偏移量赋值给DOM元素,令其保持原滚动条的位置 componentDidUpdate(prevProps, prevState) { if (this.previousScrollOffset !== null) { this.listDiv.current.scrollTop = this.listDiv.current.scrollHeight - this.previousScrollOffset; this.previousScrollOffset = null; } } render() { return ( <div style={{ height: 100, width: 150, backgroundColor: 'blue', color: 'white', overflowY: 'scroll' }} ref={this.listDiv}> <ul> {this.state.content.map( (e) => { return <li>{e}</li> }) } </ul> </div> ) } }
-
当组件更新渲染完毕后,会运行
componentDidUpdate
,这个方法与componentWillUpdate
类似,但不同的是,其会接收组件在更新props或state之前的props与state。从上述的实例也可以看出,其一般用于访问与修改DOM。
-
-
在渲染阶段,只有一个函数
render
。其在class组件内是唯一一个必须要实现的生命周期函数。其会检查组件的props
和state
的变化,并返回将要渲染成的虚拟DOM等。当shouldComponentUpdate
不返回false时,props
的变化,使用setState
或forceUpdate
都会导致此函数被调用。 -
除了这些React自动运行的生命周期函数,在React上还存在着两个需要手动运行的生命周期函数
setState
和forceUpdate
。-
首先
setState
的书写方式为setState(updater:(preState,props)=>state,[callback])
,并需要注意以下几点。
1.updater
为返回stateChange
对象的函数。
2.updater
可以接收到更改前,即当前的state
和props
。
3.callback
是可选的回调函数,它在状态更新完毕,页面也更新后(render
调用后)才被调用。
平时使用的直接向setState
传递新状态的方式属于这种方式的语法糖。此外,这个函数在使用时,不会立即更新组件,而是会以一个异步的,批量的,推迟的方式去更新组件,所以在使用setState
后,不能直接获取组件更新后状态。而且由于其是异步更新,所以当更新后的state依赖于更新前的state时,建议向setState
的第一个参数传递函数。state = { value:'123' }; //打印123,获取到的是更新前的state componentDidMount(){ this.setState({value:'456'}); console.log(this.state.value); } //打印456,获取到的是更新后的state componentDidMount(){ this.setState({value:'456'},()=>{ console.log(this.state.value); }); } //打印456,获取到的是更新后的state componentDidMount(){ this.setState({value:'456'}); } componentDidUpdate(){ console.log(this.state.value); }
-
setState
是为了更新组件内状态以更新页面渲染,forceUpdate
则是为了强制页面渲染,这个函数可以无视shouldComponentUpdate
,直接调用render
函数进行渲染。常常用于页面的内容依赖于除state
和props
以外的其他数据时。另外如果在父组件中使用此函数,父组件的shouldComponentUpdate
会跳过,但子组件的shouldComponentUpdate
依旧会执行。export default class Demoe extends Component { constructor(props) { super(props); this.value = '123'; } //即使整个组件内没有状态,点击按钮后,依旧会触发render函数,进行页面的重新渲染 btnOnClick = ()=>{ this.value = '456'; this.forceUpdate(); } render() { return ( <div > {this.value} <button onClick={this.btnOnClick}>点我更新</button> </div> ) } }
-
2.新组件生命周期
-
相对于旧版的生命周期,新版的生命周期删除了一部分函数,同时作为替代也提供了一些新的生命周期函数。如下图所示
-
新添加了
getDerivedStateFromProps
替换旧版生命周期中的componentWillReceiveProps
,与componentWillReceiveProps
获取的参数相同,但不同的是,其会返回一个state
对象以替换原state
。相比于旧版生命周期,其函数内部无需再调用setState
方法,减少了组件的重绘次数。此外,与旧函数不同的是,此函数再每次渲染前都会触发,而不是仅在父组件重新渲染时触发。//需要加静态修饰符,组件内部禁止访问this相关的内容 //并且由于state派生于props,导致代码冗余,使得组件难以维护,因此不建议使用。 static getDerivedStateFromProps(props, state) { console.log("getDerivedStateFromProps", props, state); return null; }
-
另外一个生命周期函数为
getSnapshotBeforeUpdate
,它替换旧版生命周期中componentWillUpdate
。两者之间的区别是,由于异步渲染,旧版生命周期中的componentWillUpdate
读取到的DOM元素状态不一定是和最终的渲染画面内的DOM元素状态相同,但新版的getSnapshotBeforeUpdate
会与最终的渲染画面内的DOM元素状态(componentDidUpdate
中的)一致的。并且其返回值会作为componentDidUpdate
的第三个参数。相应的,上面的示例可以更改为export default class Demoe extends Component { constructor(props) { super(props); this.state = { content: [], scrollY: 0 }; this.listDiv = React.createRef(); } //倒序增加展示内容 componentDidMount() { this.timer = setInterval(() => { var content = [`新闻${this.state.content.length + 1}`, ...this.state.content]; this.setState({ content }); }, 1000); } componentWillUnmount() { clearInterval(this.timer); } //获取数据变更后,但画面DOM更新前滚动条的偏移量并保存。 //传入的不再是修改后的state与props,而是修改前的state与props,此时组件的state与props已经修改了 getSnapshotBeforeUpdate(preProps, preState) { if (preState.content.length < this.state.content.length) { return this.listDiv.current.scrollHeight - this.listDiv.current.scrollTop; } else{ return null; } } //当画面渲染出新的数据后,将从getSnapshotBeforeUpdate处接收到的滚动条偏移量赋值给DOM元素,令其保持原滚动条的位置 componentDidUpdate(prevProps, prevState, snapshot) { if (snapshot !== null) { this.listDiv.current.scrollTop = this.listDiv.current.scrollHeight - snapshot; } } render() { return ( <div style={{ height: 100, width: 150, backgroundColor: 'blue', color: 'white', overflowY: 'scroll' }} ref={this.listDiv}> <ul> {this.state.content.map( (e) => { return <li>{e}</li> }) } </ul> </div> ) }
3.错误边界
-
在组件内部,存在着贯穿着整个生命周期的错误边界函数,可以在其子组件树中的任何位置捕获错误,并渲染出备用页面。但其只能捕获组件树以下组件中的错误,而无法捕获其本身的错误。并且在开发环境中,错误会冒泡到window上,并中断这些被捕获的错误,但在生产环境上却不会出现冒泡。
//getDerivedStateFromError配合componentDidCatch //生命周期函数,一旦后台组件报错,就会触发 static getDerivedStateFromError(error){ console.log(error); //在render之前触发 //返回一个对象以更新state return { hasError: true, }; } //会在画面DOM完成重新渲染后被调用,因此,常用于记录错误 componentDidCatch(error,info){ console.log(error,info); }
参考文章
React:组件的生命周期 - SegmentFault 思否