一、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 中更正為都調用。
即更新後的生命周期為:
- 挂載階段
- 更新階段
- 解除安裝階段
2. 生命周期,誤區
誤解一:
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
,然後解除安裝不需要的舊元件,最後新元件執行挂載完成的回調。