原文来自:https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
先解释下问什么要翻译这篇文章:React16推出的新的生命周期,解决了一些问题。但这些新的生命周期本人实在是太陌生了。并且在用16版本的时候,本人几乎用不到这些新的生命周期。为了加深印象,决定翻译此文章。
这篇文章详细介绍了React组件类的API。假设你已经非常熟悉React的相关基础概念,比如说Component、Props、State和生命周期。如果你不理解,那就先弄懂它们。
概览
React允许你用类或者函数去定义一个组件。用类定义的组件可以提供很多特征,这些特征会在接下来讲到。你需要通过extends React.Component
定义一个React组件类:
1 | class Welcome extends React.Component { |
这个组件类里,用户必须定义render的实现。其他方法是可选的。
React强烈反对构建自定义的基类组件。因为React中,代码复用的主要通过组合实现,并不是继承。
注意:
React不会强制用户使用ES6语法。如果你不想用ES6,也可以使用create-react-class模块或者类似的自定义抽象。你可以阅读:不用ES6写React。
组件生命周期
组件都有生命周期的方法,这些方法允许你在一个阶段的某些特殊时期重载生命周期,运行自定义代码。你可以将生命周期图当做备忘录。 生命周期
下面这个列表中,经常用到的生命周期会用粗体表示。其他的很少用到。
挂载阶段
当一个组件实例被创建出来,并插入到DOM中,下面这些方法会按顺序执行:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
注意:
下面的这些方法,是要被废掉的。因此在你的项目中,尽量避免使用它们。- UNSAFE_componentWillMount()
更新阶段
props和state的改变会引发更新。当组件重新调用render的时候,下面的方法会按序发生:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
注意:
下面的这些方法,是要被废掉的。因此在你的项目中,尽量避免使用它们。- UNSAFE_componentWillUpdate()
- UNSAFE_componentWillReceiveProps()
卸载阶段
当组件从DOM节点中移出时,调用下面的方法:
- componentWillUnmount()
错误处理
当render中发生异常时,会调用下面方法。异常捕获来自子节点的渲染中、生命周期中、构建中。
- componentDidCatch()
其他API
组件同样提供了其他的方法:
- setState()
- forceUpdate()
类属性
- defaultProps
- displayName
实例属性
- props
- state
参考
常用生命周期
这一节里列举的方法在你的项目中会经常使用。可视化参考,你可以看生命周期图。
render()
一个组件类,必须实现它的render方法。
当它被调用时,render会检查this.props和this.state,同时返回下面其中一个类型:
- React elements。典型的是通过JSX创建。比如说,
<div />
和<MyComponent />
都是React elements,它们分别指导React渲染DOM节点和用户自定义组件。 - Arrays and fragments。允许你通过render返回多个elements。阅读文档fragments
- Portals。允许你在另外一个DOM节点渲染组件。阅读文档portals
- String and numbers。做为文本节点渲染到DOM中。
- Booleans or null。渲染空。(当test是boolean类型是,支持
return test && <Child />
这种形式。)
render()必须是纯函数,意味着render不能修改state。当state,props一致的时候,render返回的永远是一样的。并且render并不会与浏览器直接交互。
如果你需要和浏览器交互,应该在componentDidMount或者其他生命周期里实现。保证render是纯函数可以让思维更加简洁。
注意:
当shouldComponentUpdate()返回false时。render不会调用。
constructor()
如果你不需要初始化state,或者绑定事件,你就不需要实现你的constructor
React组件在挂载之前,会调用constructor。组件的constructor调用里,super(props)
必须在所用语句前调用。否则,在constructor里,this.props返回undefined,从而导致bug。
通常,constructor主要用于下面两种目的:
- 初始化state
- 绑定事件
在constructor里,你不能调用setState。如果组件需要一个本地状态,只需要在constructor里直接对this.state赋值即可:1
2
3
4
5
6constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
this.state这种直接赋值的方式,只能出现在constructor里。也就是说,其它地方,你只能使用this.setState()
。
为了避免在constructor里引入其它副作用,对于某种情况,你可以写在componentDidMount()
里。
注意:避免将props拷贝到state中!下面案例在某些场景里会出现问题
1
2
3
4
5 constructor(props) {
super(props);
// Don't do this!
this.state = { color: props.color };
}
上面的问题非常没有必要,并且会产生bug。因为可以直接从props中获取color属性,为什么要多创建一个state呢;另外,因为在constructor中初始化的,所以后续对color的更新,不会反馈到state中。但是上面的情况只是在某些案例中会有问题。
如果你本来就要忽略props的更新,你可以用这种方式。在那种情况下,将props.color重命名为initialColor或者defaultColor会更加说的通。如果你想重置这个组件的初始状态,你可以改变他的key值。
我们将某些依赖于props的state叫做派生状态。可以阅读avoid derived state,了解更多。
componentDidMount()
componentDidMount在组件插入DOM之后,立即被调用。DOM的初始化操作,也是在这个函数中实现的。如果你需要从远程拉取数据初始化,componentDidMount是最好实现的地方。
同样,关于订阅的一些方法,也在这里设置。如果你订阅了方法,记得在componentWillUnmount里取消订阅。例如:如果你在componentDidMount设置了setTimeout句柄,那你就在componentWillUnmount里对这个句柄clearTimeout。
componentDidMount里如果立即调用setState,会立刻触发额外的render,但这个render发生在浏览器渲染到屏幕之前。这样就能确保:即便render在这个情况下调用了两次,用户没法看到中间状态(PS:不太理解这句话,但是render可能是在浏览器渲染之前调用,因为只有render调用了,才能返回对应的类型,去指导浏览器做出相应的渲染。如果在浏览器更新之后调用,render返回的新的VNode,为了状态与视图一致,势必会再次渲染浏览器。)。这种方式需要小心使用,因为这经常会产生性能问题。大多数情况下,你其实可以将这个初始状态在constructor里实现。
但是,在某些情况下:比如需要依赖获取提示信息的位置,设置state时,你只能在componentDidMount里去获取并setState。
componentDidUpdate()
1 | componentDidUpdate(prevProps, prevState, snapshot) |
componentDidUpdate()会在update发生之后立即调用。这个方法不会发生在第一次render之后。
通常,我们会在这里操作DOM。同样,由于当前props和之前props不同,调用远程接口,在componentDidUpdate里实现也是很好的实践:
1 | componentDidUpdate(prevProps) { |
你可以在componentDidUpdate直接调用setState,但是必须在一个条件里,如比上面栗子所示。否则,就会死循环。同时,他也会造成额外的render,即便这些render用户不可见,但是对应用也造成了性能影响。如果你想『镜像』一些来自props的state,你可以考虑直接使用props。请阅读:why copying props into state causes bugs。
如果组件里实现了getSnapshotBeforeUpdate的生命周期,这个getSnapshotBeforeUpdate返回的值会做为componentDidUpdate的第三个参数:”snapshot”,否则这个值就是undefined。
注意:
如果shouldComponentUpdate()返回的是false,componentDidUpdate不会被调用。
componentWillUnmount
在组件卸载,销毁之前,componentWillUnmount会被调用。componentWillUnmount里实现所有清理流程,比如clearTimeout, removeEventListener,cancel请求。
在componentWillUnmount里,不要调用setState,因为这时的setState不会产生额外的render。一旦一个组件卸载掉了,他就永远不可能再次挂载。这个组件就永远的走了。。。
不常用的生命周期
这一节里的方法不经常用。偶尔用他们会很方便,但是大多数组件都不太会用。在生命周期图里,你可以通过勾选『显示不常用的生命周期』看到他们。
shouldComponentUpdate
利用shouldComponentUpdate告知React当前的state或props的改变是否会影响到组件的输出。默认情况下,只要改变props或state都会重新调用render。并且大多数情况,你可以不用改这个函数,采用他的默认行为即可。
当新的props和state接受到时,shouldComponentUpdate在render之前调用。默认返回的是true。这个方法在初次render或者forceUpdate的时候,是不会被调用的。
当需要性能优化的时候,往往会用到shouldComponentUpdate。不要为了阻止重新render去用这个函数,因为这很可能会产生bug。如果在你需要手写shouldComponentUpdate的情况下,考虑使用PureComponent替换。PureComponent会做浅层的props,state比较,从而减少你跳过必要更新到可能。
如果你一定要手写shouldComponentUpdate,你可以比较this.props和nextProps,this.state和nextState。返回false,告知React这次的更新需要忽略掉。需要注意的是,父元素的return false不会影响到子元素state改变造成的render。
我们非常不赞成在shouldComponentUpdate使用深比较或者利用JSON.stringify()比较,这会非常影响性能。
目前,如果shouldComponentUpdate返回false,UNSAFE_componentWillUpdate(), render(), 和 componentDidUpdate() 都不会被调用。未来,React会将shouldComponentUpdate视为一个提示,而不是严格的指令。这样,即便它返回的是false,也会使组件再次调用render。
static getDerivedStateFromProps()
1 | static getDerivedStateFromProps(props, state) |
翻译成中文:从props中得到派生状态。由此可见,派生状态依赖于props。getDerivedStateFromProps发生在render之前,发生在第一次render和更新触发的render之前。他返回的是一个对象更新state;或者也可以返回null,表示什么也不更新。
当状态随着时间的变化,依赖于props的改变,getDerivedStateFromProps才会被用到。比如说,利用它会非常方便实现<Transition>
。因为他需要比较<Transition>
之前的chldren和改变之后的chldren,来决定哪些需要淡入,淡出。
派生状态会导致代码变得冗长,使得你很难思考组件的逻辑。但是,你可以使用其它更简便的方式替换:
- 如果由于props的改变,你需要操作一个副作用(比如,抓取数据或动画),你可以在componentDidUpdate里实现。
- 如果当props改变,你需要重新计算某些值得时候,利用带有记忆的辅助函数。
- 如果当props改变时,你需要重置某些状态,可以考虑将组件转变为傻瓜组件,或者改变这个组件的key值。
getDerivedStateFromProps无法获取组件的实例,即在getDerivedStateFromProps里,this返回的是undefined。如果你愿意,你可以通过组件的props和state提取出以props和state为参数的纯函数,在getDerivedStateFromProps和其他类方法之间实现代码复用。
这个方法会在每次render都之前无条件触发。这个会与UNSAFE_componentWillReceiveProps形成对比,UNSAFE_componentWillReceiveProps只会在父组件render的时候才会被触发,并不会因为setState而触发。
getSnapshotBeforeUpdate()
1 | getSnapshotBeforeUpdate(prevProps, prevState) |
getSnapshotBeforeUpdate在最近一次输出的render被committed到DOM之前调用,即发生在DOM更新之前。它允许你从DOM那,在DOM改变之前获取一些信息,比如说当时的scroll的位置。这个函数返回的值会做为componentDidUpdate的参数。
getSnapshotBeforeUpdate不会经常用到,但是在某些交互的场景下会用到。比如说一个聊天的线程需要处理scroll的位置。
这个函数必须返回一个snapshot值或者(null)。比如说:
1 | class ScrollingList extends React.Component { |
栗子:在render调用之后,DOM更新之前,获取一个距离。在DOM更新之后,根据当时计算出的距离,重置scrollTop。从理解上来看,栗子想表达的意思是我们每增加一个item,scroll都会向上移动新增item高度的距离。
在上面的例子中,scrollHeight必须在getSnapshotBeforeUpdate计算。因为在『render』和『commit』阶段中,存在一段延迟。也就是说,getSnapshotBeforeUpdate计算的是当时的dom数据,而不是更新之后的dom数据。
componentDidCatch()
1 | componentDidCatch(error, info) |
错误边界是用来捕获子组件发生的错误,记录错误原因,并用UI显示。它优化了原先因为子组件异常而使整个组件树崩溃的不好体验。错误边界发生在子组件rendering阶段,生命周期阶段,整个组件树的构建阶段。
如果类组件定义了componentDidCatch这个方法,就表示这个组件会成为错误边界。在这个方法里调用setState可以将错误信息以界面的形式显示出来。componentDidCatch只能用来从异常中恢复,不要尝试将它用在控制流中,即不要将它控制我们的业务逻辑等。
想知道更多错误边界,可以看:Error Handling in React 16
注意:
错误边界只能捕获子组件产生的错误,无法捕获自身发生的错误。
待废弃方法
下面的方法虽然仍然有效,但是我们不推荐使用。你可以通过这篇文章自动更新你的组件。
UNSAFE_componentWillReceiveProps()
注意:
这个生命周期经常会导致bug,不一致问题。因此,React将在未来废除这个方法。如果因为props改变,你需要操作一个副作用(拉取数据或动画),推荐用componentDidUpdate方法替代。
其它情况,你可以参考:这篇文章。
如果你需要用componentWillReceiveProps计算因props改变而造成的某些值改变的话,你可以用一些辅助函数
如果你想因为props改变,重置state,可以考虑将组件换成完全可控的,或者利用key属性。
在非常非常罕见的时候,你可能会用到getDerivedStateFromProps做为最后的方案。
UNSAFE_componentWillReceiveProps是已挂载的组件介绍到新的props的时候,才会被调用。如果因为props改变,你需要更新state,你需要将当前的props和nextProps对比,然后用setState。
如果因为父组件导致当前组件重新render,那么这个方法会被无条件调用。所以确定这里会有个比较。
React不会在未挂载阶段调用UNSAFE_componentWillReceiveProps。这个方法只会在props改变,和父组件重新render的时候调用。使用this.setState()通常并不会触发UNSAFE_componentWillReceiveProps。
UNSAFE_componentWillUpdate()
1 | UNSAFE_componentWillUpdate(nextProps, nextState) |
当组件接受到新的props或state,UNSAFE_componentWillUpdate会在render之前调用。第一次render之前,是不会触发这个函数的。
不要在这个函数里调用this.setState(),或者其他触发更新行为:dispatch redux action。
这个方法,可以被componentDidUpdate换种方式替换。如果你是想获取DOM信息的话,你可以考虑使用getSnapshotBeforeUpdate。
其它API
不像上节那些废弃的方法,下面的方法你可以在你的组件中调用:setState和forceUpdate。
setState()
1 | setState(updater[, callback]) |
setState将组件状态的更新排列到队列中,并告知React这个组件,以及他的子组件都需要根据新的state重新render。基本上都是通过这个方法来更新界面的。
将setState设想成一个request,而不是立即更新的指令。为了优化性能,React会晚点执行它,这样就可以一下更新一些组件。React不会保证state会立即改变。
setState不会立即更新组件。通常都会被批量或延迟更新。这样就会导致我们经常在setState之后,立即执行this.state返回的还是原来的值。如果你不想这样,你可以使用componentDidUpdate或者用setState的callback来实现。这两个方法都能确保在state改变之后调用。如果你想基于原来的state去setState,可以重点看下updater参数。
setState都会触发render,除非shouldComponentUpdate返回的是false。
setState第一个参数是updater:
1 | (state, props) => stateChange |
state是对应用更改时组件状态的应用,不要直接修改state值。通过直接创建一个新对象表示这个改变。比如说,假设根据props.step增加value值:
1 | this.setState((state, props) => { |
updater中的state和props,都能确保未来会被更新。updater输出的是和state的一个浅层合并。
updater的第二个参数是可选的回调函数,这个回调函数是在setState完成,component重新render之后才会调用。通常情况下,我们会用componentDidUpdate替换这个方法。
同样,updater也接受object:
1 | setState(stateChange[, callback]) |
这个会将stateChange浅层合并到state中,也就是说,他之后合并state.xxx,并不会合并state.xx.xxx之后的东东:
1 | this.setState({quantity: 2}) |
这种方式的setState也是异步调用的。在同一个生命周期中,多次调用,会被批量处理。比如说,你想在一个生命周期中多次更行item.quantity:
1 | Object.assign( |
在这个例子中,后续的调用将会覆盖先前的调用的值,所以quantity只会加1.如果下面一个state是依赖当前state,我们推荐使用function的模式:
1 | this.setState((state) => { |
更多信息,如下所示:
- State and Lifecycle guide
- In depth: When and why are setState() calls batched?
- In depth: Why isn’t this.state updated immediately?
forceUpdate
1 | component.forceUpdate(callback) |
默认情况下,你的组件会在state、props改变的之后,自动render。但是,如果组件的render还依赖于别的数据,你可以使用这个方法。调用这个方法会跳过shouldComponentUpdate(想想也是合情合理),触发组件render。对于子组件来说,会走正常的生命周期流程,包括子组件中的shouldComponentUpdate。
正常情况下,尽量避免使用这个方法。
Class属性
defaultProps
defaultProps是定义在组件类上的。当属性是undefined的时候,才会用到defaultProps;null是不会的:
1 | class CustomButton extends React.Component { |
如果props.color没有提供,这个就是’blue’。
1 | render() { |
但是props.color是null,color就会是null
1 | render() { |
displayName
displayName使用在debugging信息。通常情况下,你不需要设置他,因为他是根据function的名称或者class定义组件而来的。除非为了调试,你想设置一个不同的名称或者创建一个高阶组件时,你可以设置这个属性。详情可见:Wrap the Display Name for Easy Debugging。
实例属性
props
this.props是在组件被调用的时候定义的。可以查看Components and Props获取更多信息。
this.props.children是比较特殊的属性。通常由JSX表达式中的子标签而不是标签本身定义。
state
state包含组件的数据,这些数据总会发生改变。state是用户定义的,他应该是plain JS object。什么plain Object,可以参考这段代码。
如果某些值并不是用来render的,最好不要放在state中。这些值可以放在组件实例上。如:this._id。
阅读State and Lifecycle关注更多关于state的信息。
永远不要直接操作state,应该通过setState的方式。需要将this.state视为不可操作的对象。(PS:应该是为了前后对比,生命周期中,总用state, nextState)。
个人观点或问题
1. setState在constructor中使用会怎么样?
React会报warning:Can’t call setState on a component that is not yet mounted。如下: set state in constructor
想想也是,setState设置组件render状态,在还未挂载的组件上setState,其实是不科学的,还不如直接对this.state赋值.
2. 重置组件状态
本人经常用的方法是利用生命周期:componentWillReceiveProps来重置组件状态。本文给出的方案是:直接改变组件的key值。哇塞~豁然开朗。超级棒~
3. getDerivedStateFromProps 和 componentWillReceiveProps有什么不同
getDerivedStateFromProps
会在每次render都之前无条件触发。这个会与UNSAFE_componentWillReceiveProps
形成对比,UNSAFE_componentWillReceiveProps只会在父组件render的时候才会被触发,并不会因为setState而触发。如果父组件重新render,子组件的UNSAFE_componentWillReceiveProps
会被无条件调用。
4. In depth: When and why are setState() calls batched?
重点:不管你有多少个update,不管你发生在多少个组件里,只要在同一个事件里,都只会调用一次render
大体意思:在React16以及之前的版本,React会在一个event句柄里批量更行update。换而言之,在event之外,则立即更新。但是在React17版本,React将会默认批量更新,不管你是在event里面还是外面。
假设,Child
,Parent
在click事件里都调用了setState,为了不要让Child
render两次,可行的方案是采用批量更新。
如下:
1 | class H extends Component { |
当<W />
点击按钮,子组件调用了自身方法,设置自身state的同时,调用了父组件的onClick方法,父组件设置父组件自身的state。如果是立即更新,<W />
会调用两次render,显然,消耗性能。React采用批量更新,因为子组件、父组件在一个事件里,因此,子组件只调用了一次render。
那如何立即调用两次呢?很明显,将setState转成异步。
1 | class H extends Component { |
将事件里的setState异步出来,因为不在一个句柄,所以setState立即调用。此时我们会发现,在setState立即调用之后,直接调用this.state.value,我们会发现得到的是更新后的值。这个时候若还用之前的代码:
1 | Promise.resolve() |
我们会发现parentValue 永远会比childValue大1。如下: setState立即更新
梳理下整个的调用流程:child setState -> child re-render -> console.log -> parent setState -> parent render -> child re-render。
那么如何将不在一个事件里的更新做批量处理呢?React提供了另外一个方法,这个方法在React16里,不属于安全方法,但是到了React17里会更新。ReactDOM.unstable_batchedUpdates
1 | Promise.resolve() |
只有当我们退出ReactDOM.unstable_batchedUpdates
之后,React才会调用一次render。其实,对于React事件内部,是被unstable_batchedUpdates
包裹起来的。这就是为什么不管你有多少个update,不管你发生在多少个组件里,只要在同一个事件里,都只会调用一次render。
Plain Object
来自http://api.jquery.com/Types/#PlainObject
像用花括号{}或new Oject()创建出来的对象,是plain object。已下几种不是:
- Function
- Array
- String
- Booleans
- Numbers
- NaN
- BOM
- …