前言
真正在项目中用到React,正是React版本从15到16.3(项目当前使用版本)又到16.4的变化期。React从15到16最令人困惑的改变莫过于生命周期函数的剧烈变动,由此引发的一些新的实践方法。事实上,我们项目在从15升级到16,在生命周期函数这块可以说是新旧混杂,作为新来者难免会有很多困惑。我一直有个想法就是在接下来新功能的开发中尽量使用React16新推出的功能以及新周期函数,同时在开发中涉及到先前的代码时做一些优化,使代码结构更加清晰易读。所以感觉很有必要把React从15到16生命周期函数的演化,原因以及如何使用做一个整理总结。
目录结构大致如下:
React 15 到 16 生命周期函数变动
预废弃:
componentWillMount,componentWillUpdate,componentWillReceiveProps
已新增:
static getDerivedStateFromProps(props, state)
getSnapshotBeforeUpdate(prevProps, prevState)
UNSAFE_componentWillMount
UNSAFE_componentWillReceiveProps
UNSAFE_componentWillUpdate
迁移路径
目前新增了三个生命周期,其实也就是在预废弃的周期函数前加一个USAFE_前缀。React今后版本的发布会逐步淘汰componentWillMount,componentWillReceiveProps 以及 componentWillUpdate
根据迁移路径,我的建议是:
1. 如果项目中React 版本已经是16.3及以上,在新代码中不要用被废弃(deprecated)或者标位UNSAFE的生命周期函数。
2. 在开发中如果要修改已有代码,可以顺便把就代码中的预废弃的周期函数更改为新周期函数,当然前提是要评估好风险。
UNSAFE 周期函数废弃或不推荐原因
关键字:Async Render
到目前为止(React 16.4),React的渲染机制遵循同步渲染:
1) 首次渲染: willMount > render > didMount,
2) props更新时: receiveProps > shouldUpdate > willUpdate > render > didUpdate
3) state更新时: shouldUpdate > willUpdate > render > didUpdate
3) 卸载时: willUnmount
期间每个周期函数各司其职,输入输出都是可预测,一路下来很顺畅。
BUT 从React 17 开始,渲染机制将会发生颠覆性改变,这个新方式就是 Async Render。
首先,async render不是那种服务端渲染,比如发异步请求到后台返回newState甚至新的html,这里的async render还是限制在React作为一个View框架的View层本身。
通过进一步观察可以发现,预废弃的三个生命周期函数都发生在虚拟dom的构建期间,也就是render之前。在将来的React 17中,在dom真正render之前,React中的调度机制可能会不定期的去查看有没有更高优先级的任务,如果有,就打断当前的周期执行函数(哪怕已经执行了一半),等高优先级任务完成,再回来重新执行之前被打断的周期函数。这种新机制对现存周期函数的影响就是它们的调用时机变的复杂而不可预测,这也就是为什么”UNSAFE”。
从框架的稳定性考虑,不重写现存的很成熟但不适应新机制的周期函数,另起炉灶为async render重新打造一套新周期函数也在情理之中。
React 16 生命周期使用小结
React 16.4 生命周期 官方流程图
static getDerivedStateFromProps(props, state)
这是一个我重点关注的周期函数,某种意义,它是作为componentWillReceiveProps的代替品出现,而在React 16 之前componentWillReceiveProps是一个最容易被滥用(misuse)的周期函数。所以很有必要好好研究一下getDerivedStateFromProps。
说句老实话,React 16之前的周期函数的命名非常语义化,willUpdate && didUpdate, willMount && didMount,哪怕没有做过React,根据周期函数的名称,也大概知道React的基本流程。但是从16以后,willMount, willUpdate都要被扔掉了,新增的周期函数与保留的周期从语义上很难将渲染流程连贯下来。
就getDerivedStateFromProps而言,我看到这个函数时马上有两个问题:
1. 什么叫 Derived State
2. 这个函数为什么是static
什么叫 Derived State
getDerivedStateFromProps exists for only one purpose. It enables a component to update its internal state as the result of changes in props.
由于组件的props改变而引发了state改变,这个state就是derived state. derived from props. 举个栗子:一个展示用户联系人列表的控件:
// Parent Component
.....
render() {
return
}
....
// Component EmailList
// 随父组件传入的props userid的不同而变化的state friends就是一个derived state
EmailList extends Component {
constructor() {
super(props);
this.state = {friends: []}
}
componentDidMount() {
loadAsyncList(this.props.userid)
.then(data => this.setState({friends: data.list}));
}
}
Why static
static 是ES6的写法,当我们定义一个函数为static时,就意味着无法通过this调用我们在类中定义的方法(原理和js中原型链继承相关,具体我就不说了,可自行搜索)。
再来看函数参数:static getDerivedStateFromProps(nextProps, prevState)
通过static的写法和函数参数,可以感觉React在和我说:请只根据newProps来设定derived state,不要通过this这些东西来调用帮助方法,可能会越帮越乱。用专业术语说:getDerivedStateFromProps应该是个纯函数,没有副作用(side effect)。
getDerivedStateFromProps调用时机
Mounting 时:无论是16.3还是16.4,都会触发。
Updating 时:
React 16.3: 只有props改变,才会调用这个周期函数来更新state 正确
React 16.4: 只有props改变,才会调用这个周期函数来更新state. 错误
事实上,在16.4中,在任何一次render前,getDerivedStateFromProps都会被触发。这其中包括:
1. new props. 2. setState 3. forceUpdate
通过以上叙述,在16.4中,我们可以得出一条实践:
在getDerivedStateFromProps中,在条件限制下(if/else)调用setState。如果不设任何条件setState,这个函数超高的调用频率,不停的setState,会导致频繁的重绘,既有可能产生性能问题,同时也容易产生bug。
derived state 滥用根源(一):controlled vs uncontrolled
controlled和uncontrolled这两个词经常用于描述form,如果我们用state来控制form中各种input,那么这个from就是controlled。
但是此处描述的controlled和uncontrolled是站在父组件的角度来看子组件。
controlled component: 子组件没有state(有state意味着component可以通过setState来control自身的渲染),他的一切行为完全由父组件决定,因此是可控的controlled。
// 自己感受一下。连onChange这种事情都要爸爸代劳!
function EmailInput(props) {
return <input onChange={props.onChange} value={props.email} />;
}
uncontrolled component: 子组件自身有internal state,不受父组件控制,自己玩自己的,因此是uncontrolled。
有个奇特存在就是derived state,它和父组件传入的props联动,从性质说又是子组件的state,可以通过setState来设置,那么在这种情况下,controlled还是uncontrolled就很难。
derived state 滥用根源(二):本不需要你
最近两天我自己在项目中遇到一个小问题很有意思,案例非常简单:
后台有一张记录上传资料信息的表,表字段是表名和来源。
前台有两个联动控件:
1)一个搜索框,当用户输入字符时后台会用模糊匹配返回符合关键字的数据表名。
2)数据表来源,默认是全部。切换来源会重置(reset)搜索框。(包括清空搜索框中的用户输入以及根据来源重新获取搜索框下拉列表中的结果)。
我的第一反应当然是:我靠,这不正是一个典型的derived state吗?我在父组件添加一个resetSearch的state,默认为false,作为props传入子组件,当用户改变数据源时,将resetSearch置为true,这时子组件中的getDerivedStateFromProps被触发,进行搜索框清理工作。
首先声明,这么做是没有问题的,我最初也是这样实现的,要点如下:
// 父组件
class DataList extends Component {
constructor(props) {
super(props);
this.state = {
resetSearch: false,
source: '全部'
}
}
setSource = (source) => {
this.setState({
source: parseInt(source),
resetSearch: true
}, () => {
// 远程获取新的数据表名
this.getData();
// resetSearch重置为false以备下次调用
this.setState({resetSearch: false});
});
};
render() {
......
表名:
this.afterSearchChange}>
来源:
}
}
// 子组件
class Search extends Component {
static getDerivedStateFromProps(props, state) {
if (props.resetSearch) {
return {
.... //相关state
}
}
}
}
尽管可以用,但是逻辑还是挺麻烦的,两边组件的逻辑都要做一些相应的处理(比如父组件中将resetSearch设为true后,马上又要设置回false,不然会有bug)。为一个不大的小功能改一堆东西挺不爽的。
我是看到官网中下面这段标红的话意识到我也可能是滥用’derived state’和getDerivedStateFromProps周期函数了。
getDerivedStateFromProps alternatives
根据官网给出的alternative,我为组件加了一个key,它的值就是父组件中数据源source的值。这样当父组件中source切换时,的key值也会变化,这样这个组件就会被销毁并重新render,客观上达到了reset的效果。简单又明了!
// 父组件中移除resetSearch state,为加key
this.state.source}>
//子组件中移除getDerivedStateFromProps
getDerivedStateFromProps vs componentWillReceiveProps
尽管getDerivedStateFromProps 推出是作为 componentWillReceiveProps的‘安全‘版本,但是两者的触发还是有些不同。
在16.4中,getDerivedStateFromProps更全能,无论是mounting还是updating都会被触发。componentWillReceiveProps只会updating阶段,并且是父组件触发的render才被调用。
getDerivedStateFromProps 使用场景
getDerivedStateFromProps被React官方归类为不常用的生命周期,能不用就尽量不用,前面用那么多篇幅讲这个生命周期主要是为了加深对Reac运行机制的理解。
备注:最后一条正是我之前讲的关于reset的案例。
getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate 是在render之后触发,它的要点在于触发时,Dom还没有更新,开发者可以做一些事情,返回值会作为第三个参数传递给接下来将要触发的componentDidUpdate。
getSnapshotBeforeUpdate vs componentWillUpdate
可以把getSnapshotBeforeUpdate视作componentWillUpdate的“安全“版。。在componentWillUpdate触发时,Dom同样也还没有更新。
它们之间最大的不同还是触发时机,componentWillUpdate在updating阶段的render之前触发。
其实,两者的使用经典场景其实是一样的:在beforeUpdate中记录“旧“dom的信息作为snapshot。
再多说一句,componentWillUpdate所谓不安全是指在React 17版中的async render机制下,由于优先级权限,render之前触发的componentWillUpdate可能会反复调用,获取到的一些“旧“dom的信息不一定准确。
作为一个不常用的生命周期,getSnapshotBeforeUpdate React 16给的建议当然还是:能不用就尽量不要用。
其他生命周期
再简单总结一下其他15版本就有的但是常用的生命周期。简单罗列几个要点,以备快速翻看。
componentDidMount()
componentDidUpdate(prevProps, prevState, snapshot)
shouldComponentUpdate(nextProps, nextState)