Hooks 与 React 生命周期
一、Hooks 组件
函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。
但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useState
、 useEffect()
和 useLayoutEffect()
。
即:Hooks 组件(使用了Hooks的函数组件)有生命周期,而函数组件(未使用Hooks的函数组件)是没有生命周期的。
下面,是具体的 class 与 Hooks 的生命周期对应关系:
为方便记忆,大致汇总成表格如下。
class 组件 | Hooks 组件 |
---|---|
constructor | useState |
getDerivedStateFromProps | useState 里面 update 函数 |
shouldComponentUpdate | useMemo |
render | 函数本身 |
componentDidMount | useEffect |
componentDidUpdate | useEffect |
componentWillUnmount | useEffect 里面返回的函数 |
componentDidCatch | 无 |
getDerivedStateFromError | 无 |
二、单个组件的生命周期
1. 生命周期
V16.3 之前
我们可以将生命周期分为三个阶段:
分开来讲:
-
挂载阶段
-
组件更新阶段
-
卸载阶段
-
这种生命周期会存在一个问题,那就是当更新复杂组件的最上层组件时,调用栈会很长,如果在进行复杂的操作时,就可能长时间阻塞主线程,带来不好的用户体验,Fiber 就是为了解决该问题而生。
V16.3 之后
Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。
对于异步渲染,分为两阶段:
其中,
reconciliation
阶段是可以被打断的,所以reconcilation
阶段执行的函数就会出现多次调用的情况,显然,这是不合理的。所以 V16.3 引入了新的 API 来解决这个问题:
-
static getDerivedStateFromProps
:该函数在挂载阶段和组件更新阶段都会执行,即每次获取新的props
或state
之后都会被执行,在挂载阶段用来代替componentWillMount
;在组件更新阶段配合componentDidUpdate
,可以覆盖componentWillReceiveProps
的所有用法。同时它是一个静态函数,所以函数体内不能访问
this
,会根据nextProps
和prevState
计算出预期的状态改变,返回结果会被送给setState
,返回null
则说明不需要更新state
,并且这个返回是必须的。 -
getSnapshotBeforeUpdate
: 该函数会在render
之后, DOM 更新前被调用,用于读取最新的 DOM 数据。返回一个值,作为
componentDidUpdate
的第三个参数;配合componentDidUpdate
, 可以覆盖componentWillUpdate
的所有用法。
注意:V16.3 中只用在组件挂载或组件
props
更新过程才会调用,即如果是因为自身 setState 引发或者forceUpdate 引发,而不是由父组件引发的话,那么static getDerivedStateFromProps
也不会被调用,在 V16.4 中更正为都调用。即更新后的生命周期为:
-
挂载阶段
-
更新阶段
-
卸载阶段
-
生命周期,误区
-
误解一:getDerivedStateFromProps
和 componentWillReceiveProps
只会在 props
改变 时才会调用
实际上,只要父级重新渲染,getDerivedStateFromProps
和 componentWillReceiveProps
都会重新调用,不管 props
有没有变化。所以,在这两个方法内直接将 props 赋值到 state 是不安全的。
// 子组件class PhoneInput extends Component { state = { phone: this.props.phone }; handleChange = e => { this.setState({ phone: e.target.value }); }; render() { const { phone } = this.state; return <input onChange={this.handleChange} value={phone} />; } componentWillReceiveProps(nextProps) { // 不要这样做。 // 这会覆盖掉之前所有的组件内 state 更新! this.setState({ phone: nextProps.phone }); }}// 父组件class App extends Component { constructor() { super(); this.state = { count: 0 }; } componentDidMount() { // 使用了 setInterval, // 每秒钟都会更新一下 state.count // 这将导致 App 每秒钟重新渲染一次 this.interval = setInterval( () => this.setState(prevState => ({ count: prevState.count + 1 })), 1000 ); } componentWillUnmount() { clearInterval(this.interval); } render() { return ( <> <p> Start editing to see some magic happen :) </p> <PhoneInput phone='call me!' /> <p> This component will re-render every second. Each time it renders, the text you type will be reset. This illustrates a derived state anti-pattern. </p> </> ); }} class PhoneInput extends Component { state = { phone: this.props.phone }; handleChange = e => { this.setState({ phone: e.target.value }); }; render() { const { phone } = this.state; return <input onChange={this.handleChange} value={phone} />; } componentWillReceiveProps(nextProps) { // 不要这样做。 // 这会覆盖掉之前所有的组件内 state 更新! this.setState({ phone: nextProps.phone }); } } // 父组件 class App extends Component { constructor() { super(); this.state = { count: 0 }; } componentDidMount() { // 使用了 setInterval, // 每秒钟都会更新一下 state.count // 这将导致 App 每秒钟重新渲染一次 this.interval = setInterval( () => this.setState(prevState => ({ count: prevState.count + 1 })), 1000 ); } componentWillUnmount() { clearInterval(this.interval); } render() { return ( <> <p> Start editing to see some magic happen :) </p> <PhoneInput phone='call me!' /> <p> This component will re-render every second. Each time it renders, the text you type will be reset. This illustrates a derived state anti-pattern. </p> </> ); } }
实例可点击这里查看
当然,我们可以在 父组件App 中 shouldComponentUpdate
比较 props 的 email 是不是修改再决定要不要重新渲染,但是如果子组件接受多个 props(较为复杂),就很难处理,而且 shouldComponentUpdate
主要是用来性能提升的,不推荐开发者操作 shouldComponetUpdate
(可以使用 React.PureComponet
)。
我们也可以使用 在 props 变化后修改 state。
class PhoneInput extends Component { state = { phone: this.props.phone }; componentWillReceiveProps(nextProps) { // 只要 props.phone 改变,就改变 state if (nextProps.phone !== this.props.phone) { this.setState({ phone: nextProps.phone }); } } // ...} state = { phone: this.props.phone }; componentWillReceiveProps(nextProps) { // 只要 props.phone 改变,就改变 state if (nextProps.phone !== this.props.phone) { this.setState({ phone: nextProps.phone }); } } // ... }
但这种也会导致一个问题,当 props 较为复杂时,props 与 state 的关系不好控制,可能导致问题
解决方案一:完全可控的组件
function PhoneInput(props) { return <input onChange={props.onChange} value={props.phone} />;} return <input onChange={props.onChange} value={props.phone} />; }
完全由 props 控制,不派生 state
解决方案二:有 key 的非可控组件
class PhoneInput extends Component {
state = { phone: this.props.defaultPhone };
handleChange = event => {
this.setState({ phone: event.target.value });
};
render() {
return <input onChange={this.handleChange} value={this.state.phone} />;
}}<PhoneInput defaultPhone={this.props.user.phone} key={this.props.user.id}/> state = { phone: this.props.defaultPhone }; handleChange = event => { this.setState({ phone: event.target.value }); }; render() { return <input onChange={this.handleChange} value={this.state.phone} />; } } <PhoneInput defaultPhone={this.props.user.phone} key={this.props.user.id} />
当 key
变化时, React 会创建一个新的而不是更新一个既有的组件
误解二:将 props 的值直接复制给 state
应避免将 props 的值复制给 state
constructor(props) { super(props); // 千万不要这样做 // 直接用 props,保证单一数据源 this.state = { phone: props.phone };} super(props); // 千万不要这样做 // 直接用 props,保证单一数据源 this.state = { phone: props.phone }; }
三、多个组件的执行顺序
1. 父子组件
-
static getDerivedStateFromProps
-
shouldComponentUpdate
第 二 阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件以下函数,最后触发父组件的。
React 会按照上面的顺序依次执行这些函数,每个函数都是各个子组件的先执行,然后才是父组件的执行。
所以执行顺序是:
父组件 getDerivedStateFromProps —> 父组件 shouldComponentUpdate —> 子组件 getDerivedStateFromProps —> 子组件 shouldComponentUpdate —> 子组件 getSnapshotBeforeUpdate —> 父组件 getSnapshotBeforeUpdate —> 子组件 componentDidUpdate —> 父组件 componentDidUpdate
-
getSnapshotBeforeUpdate()
-
componentDidUpdate()
卸载阶段
componentWillUnmount()
,顺序为 父组件的先执行,子组件按照在 JSX 中定义的顺序依次执行各自的方法。
注意 :如果卸载旧组件的同时伴随有新组件的创建,新组件会先被创建并执行完 render
,然后卸载不需要的旧组件,最后新组件执行挂载完成的回调。
二、单个组件的生命周期
1. 生命周期
V16.3 之前
我们可以将生命周期分为三个阶段:
分开来讲:
-
挂载阶段
-
组件更新阶段
-
卸载阶段
这种生命周期会存在一个问题,那就是当更新复杂组件的最上层组件时,调用栈会很长,如果在进行复杂的操作时,就可能长时间阻塞主线程,带来不好的用户体验,Fiber 就是为了解决该问题而生。
V16.3 之后
Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。
对于异步渲染,分为两阶段:
其中,reconciliation
阶段是可以被打断的,所以 reconcilation
阶段执行的函数就会出现多次调用的情况,显然,这是不合理的。
所以 V16.3 引入了新的 API 来解决这个问题:
-
static getDerivedStateFromProps
:该函数在挂载阶段和组件更新阶段都会执行,即每次获取新的props
或state
之后都会被执行,在挂载阶段用来代替componentWillMount
;在组件更新阶段配合componentDidUpdate
,可以覆盖componentWillReceiveProps
的所有用法。同时它是一个静态函数,所以函数体内不能访问
this
,会根据nextProps
和prevState
计算出预期的状态改变,返回结果会被送给setState
,返回null
则说明不需要更新state
,并且这个返回是必须的。 -
getSnapshotBeforeUpdate
: 该函数会在render
之后, DOM 更新前被调用,用于读取最新的 DOM 数据。返回一个值,作为
componentDidUpdate
的第三个参数;配合componentDidUpdate
, 可以覆盖componentWillUpdate
的所有用法。
注意:V16.3 中只用在组件挂载或组件 props
更新过程才会调用,即如果是因为自身 setState 引发或者forceUpdate 引发,而不是由父组件引发的话,那么static getDerivedStateFromProps
也不会被调用,在 V16.4 中更正为都调用。
即更新后的生命周期为:
-
挂载阶段
-
更新阶段
-
卸载阶段
2. 生命周期,误区
误解一:getDerivedStateFromProps
和 componentWillReceiveProps
只会在 props
改变 时才会调用
实际上,只要父级重新渲染,getDerivedStateFromProps
和 componentWillReceiveProps
都会重新调用,不管 props
有没有变化。所以,在这两个方法内直接将 props 赋值到 state 是不安全的。
实例可点击这里查看
当然,我们可以在 父组件App 中 shouldComponentUpdate
比较 props 的 email 是不是修改再决定要不要重新渲染,但是如果子组件接受多个 props(较为复杂),就很难处理,而且 shouldComponentUpdate
主要是用来性能提升的,不推荐开发者操作 shouldComponetUpdate
(可以使用 React.PureComponet
)。
我们也可以使用 在 props 变化后修改 state。
但这种也会导致一个问题,当 props 较为复杂时,props 与 state 的关系不好控制,可能导致问题
解决方案一:完全可控的组件
完全由 props 控制,不派生 state
解决方案二:有 key 的非可控组件
当 key
变化时, React 会创建一个新的而不是更新一个既有的组件
误解二:将 props 的值直接复制给 state
应避免将 props 的值复制给 state
三、多个组件的执行顺序
1. 父子组件
-
static getDerivedStateFromProps
-
shouldComponentUpdate
第 二 阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件以下函数,最后触发父组件的。
React 会按照上面的顺序依次执行这些函数,每个函数都是各个子组件的先执行,然后才是父组件的执行。
所以执行顺序是:
父组件 getDerivedStateFromProps —> 父组件 shouldComponentUpdate —> 子组件 getDerivedStateFromProps —> 子组件 shouldComponentUpdate —> 子组件 getSnapshotBeforeUpdate —> 父组件 getSnapshotBeforeUpdate —> 子组件 componentDidUpdate —> 父组件 componentDidUpdate
-
getSnapshotBeforeUpdate()
-
componentDidUpdate()
卸载阶段
componentWillUnmount()
,顺序为 父组件的先执行,子组件按照在 JSX 中定义的顺序依次执行各自的方法。
注意 :如果卸载旧组件的同时伴随有新组件的创建,新组件会先被创建并执行完 render
,然后卸载不需要的旧组件,最后新组件执行挂载完成的回调。