内容简介:本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出来我的来我的个人博客 获得无与伦比的阅读体验 生命周期,顾名思义,就是从生到死的过程。
本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出
来我的 GitHub repo 阅读完整的专题文章
来我的个人博客 获得无与伦比的阅读体验 生命周期,顾名思义,就是从生到死的过程。
而生命周期钩子,就是从生到死过程中的关键节点。
普通人的一生有哪些生命周期钩子呢?
- 出生
- 考上大学
- 第一份工作
- 买房
- 结婚
- 生子
- 孩子的生命周期钩子
- 退休
- 临终遗言
每到关键节点,我们总希望有一些沉思时刻,因为这时候做出的决策会改变人生的走向。
React组件也一样,它会给开发者一些沉思时刻,在这里,开发者可以改变组件的走向。
异步渲染下的生命周期
React花了两年时间祭出Fiber渲染机制。
简单来说,React将diff的过程叫做Reconciliation。以前这一过程是一气呵成的,Fiber机制把它改成了异步。异步技能将在接下来的版本中逐步解锁。
明明是一段同步代码,怎么就异步了呢?
原理是Fiber把任务切成很小的片,每执行一片就把控制权交还给主线程,待主线程忙完手头的活再来执行剩下的任务。当然如果某一片的执行时间就很长(比如死循环),那就没主线程什么事了,该崩溃崩溃。
这会给生命周期带来什么影响呢?
影响就是挂载和更新之前的生命周期都变的不可靠了。
为什么这么讲?因为Reconciliation这个过程有可能暂停然后继续执行,所以挂载和更新之前的生命周期钩子就有可能不执行或者多次执行,它的表现是不可预期的。
因此16之后的React生命周期迎来了一波大换血,以下生命周期钩子将被逐渐废弃:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
看出特点了么,都是带有 will
的钩子。
目前React为这几个生命周期钩子提供了别名,分别是:
- UNSAFE_componentWillMount
- UNSAFE_componentWillReceiveProps
- UNSAFE_componentWillUpdate
React17将只提供别名,彻底废弃这三个大活宝。取这么个别名意思就是让你用着恶心。
constructor()
React借用class类的 constructor
充当初始化钩子。
React几乎没做什么手脚,但是因为我们只允许通过特定的途径给组件传递参数,所以 constructor
的参数实际上是被React规定好的。
React规定 constructor
有三个参数,分别是 props
、 context
和 updater
。
-
props
是属性,它是不可变的。 -
context
是全局上下文。 -
updater
是包含一些更新方法的对象,this.setState
最终调用的是this.updater.enqueueSetState
方法,this.forceUpdate
最终调用的是this.updater.enqueueForceUpdate
方法,所以这些API更多是React内部使用,暴露出来是以备开发者不时之需。
在React中,因为所有class组件都要继承自 Component
类或者 PureComponent
类,因此和原生class写法一样,要在 constructor
里首先调用 super
方法,才能获得 this
。
constructor
生命周期钩子的最佳实践是在这里初始化 this.state
。
当然,你也可以使用属性初始化器来代替,如下:
import React, { Component } from 'react'; class App extends Component { state = { name: 'biu', }; } export default App; 复制代码
componentWillMount()
:skull:这是React不再推荐使用的API。
这是组件挂载到DOM之前的生命周期钩子。
很多人会有一个误区:这个钩子是请求数据然后将数据插入元素一同挂载的最佳时机。
其实 componentWillMount
和挂载是同步执行的,意味着执行完这个钩子,立即挂载。而向服务器请求数据是异步执行的。所以无论请求怎么快,都要排在同步任务之后再处理,这是辈分问题。
也就是说,永远不可能在这里将数据插入元素一同挂载。
并不是说不能在这里请求数据,而是达不到你臆想的效果。
它被废弃的原因主要有两点:
- 本来它就没什么用。估计当初是为了成双成对所以才创造了它吧。
-
如果它声明了定时器或者订阅器,在服务端渲染中,
componentWillUnmount
生命周期钩子中的清除代码不会生效。因为如果组件没有挂载成功,componentWillUnmount
是不会执行的。姚明说的:没有挂载就没有卸载。 - 在异步渲染中,它的表现不稳定。
初始化 this.state
应该在 constructor
生命周期钩子中完成,请求数据应该在 componentDidMount
生命周期钩子中完成,所以它不仅被废弃了,连继任者都没有。
static getDerivedStateFromProps(props, state)
:alien:这是React v16.3.0发布的API。
首先,这是一个静态方法生命周期钩子。
也就是说,定义的时候得在方法前加一个 static
关键字,或者直接挂载到class类上。
简要区分一下实例方法和静态方法:
-
实例方法,挂载在
this
上或者挂载在prototype
上,class类不能直接访问该方法,使用new
关键字实例化之后,实例可以访问该方法。 -
静态方法,直接挂载在class类上,或者使用新的关键字
static
,实例无法直接访问该方法。
问题是,为什么 getDerivedStateFromProps
生命周期钩子要设计成静态方法呢?
这样开发者就访问不到 this
也就是实例了,也就不能在里面调用实例方法或者setsState了。
import React, { Component } from 'react'; class App extends Component { render() { return ( <div>React</div> ); } static getDerivedStateFromProps(props, state) {} } export default App; 复制代码
这个生命周期钩子的使命是根据父组件传来的props按需更新自己的state,这种state叫做衍生state。返回的对象就是要增量更新的state。
它被设计成静态方法的目的是保持该方法的纯粹,它就是用来定义衍生state的,除此之外不应该在里面执行任何操作。
这个生命周期钩子也经历了一些波折,原本它是被设计成 初始化
、 父组件更新
和 接收到props
才会触发,现在只要渲染就会触发,也就是 初始化
和 更新阶段
都会触发。
render()
作为一个组件,最核心的功能就是把元素挂载到DOM上,所以 render
生命周期钩子是一定会用到的。
render
生命周期钩子怎么接收模板呢?当然是你return给它。
但是不推荐在return之前写过多的逻辑,如果逻辑过多,可以封装成一个函数。
render() { // 这里可以写一些逻辑 return ( <div> <input type="text" /> <button>click</button> </div> ); } 复制代码
注意,千万不要在 render
生命周期钩子里调用 this.setState
,因为 this.setState
会引发render,这下就没完没了了。主公,有内奸。
componentDidMount()
这是组件挂载到DOM之后的生命周期钩子。
这可能是除了 render
之外最重要的生命周期钩子,因为这时候组件的各方面都准备就绪,天地任你闯。
这就是社会哥,人狠话不多。
componentWillReceiveProps(nextProps)
:skull:这是React不再推荐使用的API。
componentWillReceiveProps
生命周期钩子只有一个参数,更新后的props。
该声明周期函数可能在两种情况下被触发:
- 组件接收到了新的属性。
- 组件没有收到新的属性,但是由于父组件重新渲染导致当前组件也被重新渲染。
初始化时并不会触发该生命周期钩子。
同样,因为Fiber机制的引入,这个生命周期钩子有可能会多次触发。
shouldComponentUpdate(nextProps, nextState)
这个生命周期钩子是一个开关,判断是否需要更新,主要用来优化性能。
有一个例外,如果开发者调用 this.forceUpdate
强制更新,React组件会无视这个钩子。
shouldComponentUpdate
生命周期钩子默认返回true。也就是说,默认情况下,只要组件触发了更新,组件就一定会更新。React把判断的控制权给了开发者。
不过周到的React还提供了一个 PureComponent
基类,它与 Component
基类的区别是 PureComponent
自动实现了一个 shouldComponentUpdate
生命周期钩子。
对于组件来说,只有状态发生改变,才需要重新渲染。所以 shouldComponentUpdate
生命周期钩子暴露了两个参数,开发者可以通过比较 this.props
和 nextProps
、 this.state
和 nextState
来判断状态到底有没有发生改变,再相应的返回true或false。
什么情况下状态没改变,却依然触发了更新呢?举个例子:
父组件给子组件传了一个值,当父组件状态变化,即便子组件接收到的值没有变化,子组件也会被迫更新。这显然是非常不合理的,React对此无能为力,只能看开发者的个人造化了。
import React, { Component } from 'react'; import Child from './Child'; class App extends Component { state = { name: 'React', star: 1 }; render() { const { name, star } = this.state; return ( <div> <Child name={name} /> <div>{star}</div> <button onClick={this.handle}>click</button> </div> ); } handle = () => { this.setState(prevState => ({ star: ++prevState.star })); } } export default App; 复制代码
import React, { Component } from 'react'; class Child extends Component { render() { return <h1>{this.props.name}</h1>; } shouldComponentUpdate(nextProps, nextState) { if (this.props === nextProps) { return false; } else { return true; } } } export default Child; 复制代码
同时要注意引用类型的坑。
下面这种情况, this.props
和 nextProps
永远不可能相等。
import React, { Component } from 'react'; import Child from './Child'; class App extends Component { state = { name: 'React', star: 1 }; render() { return ( <div> <Child name={{ friend: 'Vue' }} /> <div>{this.state.star}</div> <button onClick={this.handle}>click</button> </div> ); } handle = () => { this.setState(prevState => ({ star: ++prevState.star })); } } export default App; 复制代码
import React, { Component } from 'react'; class Child extends Component { render() { return <h1>{this.props.friend}</h1>; } shouldComponentUpdate(nextProps, nextState) { if (this.props === nextProps) { return false; } else { return true; } } } export default Child; 复制代码
解决方法有两个:
-
比较
this.props.xxx
和nextProps.xxx
。 - 在父组件用一个变量将引用类型缓存起来。
所以 this.state
和 nextState
是只能用第一种方法比较了,因为React每次更新state都会返回一个新对象,而不是修改原对象。
componentWillUpdate(nextProps, nextState)
:skull:这是React不再推荐使用的API。
shouldComponentUpdate
生命周期钩子返回true,或者调用 this.forceUpdate
之后,会立即执行该生命周期钩子。
要特别注意, componentWillUpdate
生命周期钩子每次更新前都会执行,所以在这里调用 this.setState
非常危险,有可能会没完没了。
同样,因为Fiber机制的引入,这个生命周期钩子有可能会多次调用。
getSnapshotBeforeUpdate(prevProps, prevState)
:alien:这是React v16.3.0发布的API。
顾名思义,保存状态快照用的。
它会在组件即将挂载时调用,注意,是即将挂载。它甚至调用的比 render
还晚,由此可见 render
并没有完成挂载操作,而是进行构建抽象UI的工作。 getSnapshotBeforeUpdate
执行完就会立即调用 componentDidUpdate
生命周期钩子。
它是做什么用的呢?有一些状态,比如网页滚动位置,我不需要它持久化,只需要在组件更新以后能够恢复原来的位置即可。
getSnapshotBeforeUpdate
生命周期钩子返回的值会被 componentDidUpdate
的第三个参数接收,我们可以利用这个通道保存一些不需要持久化的状态,用完即可舍弃。
很显然,它是用来取代 componentWillUpdate
生命周期钩子的。
意思就是说呀,开发者一般用不到它。
componentDidUpdate(nextProps, nextState, snapshot)
这是组件更新之后触发的生命周期钩子。
搭配 getSnapshotBeforeUpdate
生命周期钩子使用的时候,第三个参数是 getSnapshotBeforeUpdate
的返回值。
同样的, componentDidUpdate
生命周期钩子每次更新后都会执行,所以在这里调用 this.setState
也非常危险,有可能会没完没了。
componentWillUnmount()
这是组件卸载之前的生命周期钩子。
为什么组件快要卸载了还需要沉思时刻呢?
因为开发者要擦屁股吖。
React的最佳实践是,组件中用到的事件监听器、订阅器、定时器都要在这里销毁。
当然我说的事件监听器指的是这种:
componentDidMount() { document.addEventListener('click', () => {}); } 复制代码
因为下面这种React会自动销毁,不劳烦开发者了。
render( return ( <button onClick={this.handle}>click</button> ); ) 复制代码
componentDidCatch(error, info)
:alien:这是React v16.3.0发布的API。
它主要用来捕获错误并进行相应处理,所以它的用法也比较特殊。
定制一个只有 componentDidCatch
生命周期钩子的 ErrorBoundary
组件,它只做一件事:如果捕获到错误,则显示错误提示,如果没有捕获到错误,则显示子组件。
将需要捕获错误的组件作为 ErrorBoundary
的子组件渲染,一旦子组件抛出错误,整个应用依然不会崩溃,而是被 ErrorBoundary
捕获。
import React, { Component } from 'react'; class ErrorBoundary extends Component { state = { hasError: false }; render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } componentDidCatch(error, info) { this.setState({ hasError: true }); } } export default ErrorBoundary; 复制代码
import React from 'react'; import ErrorBoundary from './ErrorBoundary'; import MyWidget from './MyWidget'; const App = () => { return ( <ErrorBoundary> <MyWidget /> </ErrorBoundary> ); } export default App; 复制代码
生命周期
这么多生命周期钩子,实际上总结起来只有三个过程:
- 挂载
- 更新
- 卸载
挂载和卸载只会执行一次,更新会执行多次。
一个完整的React组件生命周期会依次调用如下钩子:
old lifecycle
-
挂载
- constructor
- componentWillMount
- render
- componentDidMount
-
更新
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
-
卸载
- componentWillUnmount
new lifecycle
-
挂载
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
-
更新
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
-
卸载
- componentWillUnmount
组件树生命周期调用栈
应用初次挂载时,我们以 render
和 componentDidMount
为例,React首先会调用根组件的 render
钩子,如果有子组件的话,依次调用子组件的 render
钩子,调用过程其实就是递归的顺序。
等所有组件的 render
钩子都递归执行完毕,这时候执行权在最后一个子组件手里,于是开始触发下一轮生命周期钩子,调用最后一个子组件的 componentDidMount
钩子,然后调用栈依次往上递归。
组件树的生命周期调用栈走的是一个Z字形。
如果根组件没有定义A生命周期钩子而子组件定义了,那调用栈就从这个子组件的A生命周期钩子开始。
另外,只要组件内定义了某个生命周期钩子,即便它没有任何动作,也会执行。
app.render(); child.render(); grandson.render(); // divide grandson.componentDidMount(); child.componentDidMount(); app.componentDidMount(); // divide app.render(); child.render(); grandson.render(); // divide grandson.componentDidUpdate(); child.componentDidUpdate(); app.componentDidUpdate(); 复制代码
当然,componentWillMount、componentWillReceiveProps和componentWillUpdate生命周期钩子有可能被打断执行,也有可能被多次调用,表现是不稳定的。所以React决定逐步废弃它们。
不过了解整个应用生命周期的正常调用顺序,还是有助于理解React的。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。