苏格团队
不知道小伙伴有没有注意到, 自从 react 更新到 16.3 版本后, 以前使用的 componentWillMount,
- componentWillReceiveProps
- ,
- componentWillUpdate
三个生命周期函数都有 eslint 报警, 让我们使用 UNSAFE_前缀的新的生命周期函数. 不禁有疑问 "react 这是意欲何为啊"? 为什么要加 UNSAFE_前缀?
为什么要对这些生命周期函数报警?
1,componentWillMount
componentWillMount 生命周期发生在首次渲染前, 一般使用的小伙伴大多在这里初始化数据或异步获取外部数据赋值. 初始化数据, react 官方建议放在 constructor 里面. 而异步获取外部数据, 渲染并不会等待数据返回后再去渲染.
案例一: 如下是安装时监听外部事件调度程序的组件示例
- class Example extends React.Component {
- state = {
- value: ''
- };
- componentWillMount() {
- this.setState({
- value: this.props.source.value
- });
- this.props.source.subscribe(this.handleChange);
- }
- componentWillUnmount() {
- this.props.source.unsubscribe(this.handleChange );
- }
- handleChange = source => {
- this.setState({
- value: source.value
- });
- };
- }
复制代码
试想一下, 假如组件在第一次渲染的时候被中断, 由于组件没有完成渲染, 所以并不会执行 componentWillUnmount 生命周期 (注: 很多人经常认为 componentWillMount 和 componentWillUnmount 总是配对, 但这并不是一定的. 只有调用 componentDidMount 后, React 才能保证稍后调用 componentWillUnmount 进行清理). 因此 handleSubscriptionChange 还是会在数据返回成功后被执行, 这时候 setState 由于组件已经被移除, 就会导致内存泄漏. 所以建议把异步获取外部数据写在 componentDidMount 生命周期里, 这样就能保证 componentWillUnmount 生命周期会在组件移除的时候被执行, 避免内存泄漏的风险.
现在, 小伙伴清楚为什么了要用 UNSAFE_componentWillMount 替换 componentWillMount 了吧 (注意: 这里的 UNSAFE 并不是指安全性, 而是表示使用这些生命周期的代码将更有可能在未来的 React 版本中存在缺陷, 特别是一旦启用了异步渲染)
2,componentWillReceiveProps
componentWillReceiveProps 生命周期是在 props 更新时触发. 一般用于 props 参数更新时同步更新 state 参数. 但如果在 componentWillReceiveProps 生命周期直接调用父组件的某些有调用 setState 的函数, 会导致程序死循环.
案例二: 如下是子组件 componentWillReceiveProps 里调用父组件改变 state 的函数示例
- ...
- class Parent extends React.Component{
- constructor(){
- super();
- this.state={
- list: [],
- selectedData: {}
- };
- }
- changeSelectData = selectedData => {
- this.setState({
- selectedData
- });
- }
- render(){
- return (
- <Clild list={this.state.list} changeSelectData={this.changeSelectData}/>
- );
- }
- }
- ...
- class Child extends React.Component{
- constructor(){
- super();
- this.state={
- list: []
- };
- }
- componentWillReceiveProps(nextProps){
- this.setState({
- list: nextProps.list
- })
- nextProps.changeSelectData(nextProps.list[0]); // 默认选择第一个
- }
- ...
- }
复制代码
如上代码, 在 Child 组件的 componentWillReceiveProps 里直接调用 Parent 组件的 changeSelectData 去更新 Parent 组件 state 的 selectedData 值. 会触发 Parent 组件重新渲染, 而 Parent 组件重新渲染会触发 Child 组件的 componentWillReceiveProps 生命周期函数执行. 如此就会陷入死循环. 导致程序崩溃.
所以, React 官方把 componentWillReceiveProps 替换为 UNSAFE_componentWillReceiveProps, 让小伙伴在使用这个生命周期的时候注意它会有缺陷, 要注意避免, 比如上面例子, Child 在 componentWillReceiveProps 调用 changeSelectData 时先判断 list 是否有更新再确定是否要调用, 就可以避免死循环.
3,componentWillUpdate
componentWillUpdate 生命周期在视图更新前触发. 一般用于视图更新前保存一些数据方便视图更新完成后赋值. 案例三: 如下是列表加载更新后回到当前滚动条位置的案例
- class ScrollingList extends React.Component {
- listRef = null;
- previousScrollOffset = null;
- componentWillUpdate(nextProps, nextState) {
- if (this.props.list.length <nextProps.list.length) {
- this.previousScrollOffset = this.listRef.scrollHeight - this.listRef.scrollTop;
- }
- }
- componentDidUpdate(prevProps, prevState) {
- if (this.previousScrollOffset !== null) {
- this.listRef.scrollTop = this.listRef.scrollHeight - this.previousScrollOffset;
- this.previousScrollOffset = null;
- }
- }
- render() {
- return (
- `<div>` {/* ...contents... */}`</div>`
- );
- }
- setListRef = ref => { this.listRef = ref; };
- }
复制代码
由于 componentWillUpdate 和 componentDidUpdate 这两个生命周期函数有一定的时间差 (componentWillUpdate 后经过渲染, 计算, 再更新 DOM 元素, 最后才调用 componentDidUpdate), 如果这个时间段内用户刚好拉伸了浏览器高度, 那 componentWillUpdate 计算的 previousScrollOffset 就不准确了. 如果在 componentWillUpdate 进行 setState 操作, 会出现多次调用只更新一次的问题, 把 setState 放在 componentDidUpdate, 能保证每次更新只调用一次.
所以, react 官方建议把 componentWillUpdate 替换为 UNSAFE_componentWillUpdate. 如果真的有以上案例的需求, 可以使用 16.3 新加入的一个周期函数 getSnapshotBeforeUpdate. 下面会有具体说明, 这里暂时卖个关子.
有什么替换方案?
1,getDerivedStateFromProps
getDerivedStateFromProps 是官方在 16.3 新加入的生命周期函数, props 变化时被调用, 若是父组件重新渲染, 也会被调用. 它返回新的 props 值.
案例四: 如下是 getDerivedStateFromProps 的使用实例
- class Example extends React.Component {
- static getDerivedStateFromProps(nextProps, prevState) {
- if(nextProps.name !== prevState.name) {
- return {
- name: nextProps.name
- }
- }
- }
- }
复制代码
可以看到, getDerivedStateFromProps 接收最新的 Props 值 nextProps, 上一个 state 值 prevState 两个参数, 返回返回一个对象来更新 state, 或者返回 null 表示不需要更新 state. 要注意的是, getDerivedStateFromProps 不能访问 this, 所以如果要跟上一个 props 值作比较, 只能是把上一个 props 值存到 state 里作为镜像. 到这里你一定有疑问, 为什么不把上一个 props 值传给 getDerivedStateFromProps? 官方给的解析如下:
在第一次调用 getDerivedStateFromProps(实例化后) 时, prevProps 参数将为 null, 需要在访问 prevProps 时添加 if-not-null 检查.
没有将以前的 props 传递给这个函数, 在未来版本的 React 中释放内存的一个步骤. (如果 React 不需要将先前的道具传递给生命周期, 那么它不需要将先前的道具对象保留在内存中.)
综上可知, getDerivedStateFromProps 正是官方新加入的用以替代 componentWillReceiveProps 的方案. 如果说, 你的项目会考虑往后的版本兼容, 建议改用 getDerivedStateFromProps.
2,getSnapshotBeforeUpdate
getSnapshotBeforeUpdate 是跟 getDerivedStateFromProps 一起, 在 16.3 新加入的生命周期函数. 触发的时机在最近的更改被提交到 DOM 元素前, 使得组件可以在更改之前获得当前值, 此生命周期返回的任意值都会作为第三个参数传给 componentDidUpdate. 一般当我们需要在更新 DOM 前需要保存 DOM 当前的状态时会使用这个生命周期, 比较常见是用于 DOM 更新前获取滚动位置, 更新后恢复到该滚动位置. 比如上面的案例三, componentWillUpdate 更好的替换方案就是 getSnapshotBeforeUpdate,getSnapshotBeforeUpdate 到 componentDidUpdate 只经过了更新 DOM 这一操作.
案例五: 如下为案例三的更好的替换方案
- class ScrollingList extends React.Component {
- listRef = null;
- getSnapshotBeforeUpdate(prevProps, prevState) {
- if (prevProps.list.length <this.props.list.length) {
- return this.listRef.scrollHeight - this.listRef.scrollTop;
- }
- return null;
- }
- componentDidUpdate(prevProps, prevState, snapshot) {
- if (snapshot !== null) {
- this.listRef.scrollTop = this.listRef.scrollHeight - snapshot;
- }
- }
- render() {
- return (
- `<div>` {/* ...contents... */}`</div>`
- );
- }
- setListRef = ref => { this.listRef = ref; };
- }
复制代码
最后, 关于 componentWillMount 的替换方案, 官方建议把该生命周期函数的逻辑处理放到 componentDidMount 里面去.
随着 React 版本迭代, 会否兼容 UNSAFE 类生命周期
React 官网上的计划是:
16.3: 为不安全生命周期引入别名 UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate. (旧的生命周期名称和新的别名都可以在此版本中使用.)
未来的 16.x 版本: 为 componentWillMount,componentWillReceiveProps 和 componentWillUpdate 启用弃用警告. (旧的生命周期名称和新的别名都可以在此版本中使用, 但旧名称会记录 DEV 模式警告.)
17.0: 删除 componentWillMount,componentWillReceiveProps 和 componentWillUpdate. (从现在开始, 只有新的 "UNSAFE_" 生命周期名称将起作用.)
结论
其实, 说了这么多. 就两点:
1,React 意识到 componentWillMount,componentWillReceiveProps 和 componentWillUpdate 这三个生命周期函数有缺陷, 比较容易导致崩溃. 但是由于旧的项目已经在用以及有些老开发者习惯用这些生命周期函数, 于是通过给它加 UNSAFE_来提醒用它的人要注意它们的缺陷.
2,React 加入了两个新的生命周期函数 getSnapshotBeforeUpdate 和 getDerivedStateFromProps, 目的为了即使不使用这三个生命周期函数, 也能实现只有这三个生命周期能实现的功能.
ps: 本文部分内容借鉴参考文章 ReactV16.3 即将更改的生命周期
来源: https://juejin.im/post/5b9b55695188255c5f53d9b8