React 生命週期不少人都瞭解,但一般咱們所瞭解的都是 單個組件 的生命週期,但針對 Hooks 組件、多個關聯組件(父子組件和兄弟組件) 的生命週期又是怎麼樣的喃?你有思考和了解過嗎,接下來咱們將完整的瞭解 React 生命週期。前端
關於 組件 ,咱們這裏指的是 React.Component
以及 React.PureComponent
,可是否包括 Hooks 組件喃?react
函數組件 的本質是函數,沒有 state 的概念的,所以不存在生命週期一說,僅僅是一個 render 函數而已。git
可是引入 Hooks 以後就變得不一樣了,它能讓組件在不使用 class 的狀況下使用 state 以及其餘的 React特性,相比與 class 的生命週期概念來講,它更接近於實現狀態同步,而不是響應生命週期事件。但咱們能夠利用 useState
、 useEffect()
和 useLayoutEffect()
來模擬實現生命週期。github
即:Hooks 組件更接近於實現狀態同步,而不是響應生命週期事件。算法
下面,是具體的 生命週期 與 Hooks 的對應關係:數組
constructor
:函數組件不須要構造函數,咱們能夠經過調用 useState
來初始化 state。若是計算的代價比較昂貴,也能夠傳一個函數給 useState
。瀏覽器
const [num, UpdateNum] = useState(0)
複製代碼
getDerivedStateFromProps
:通常狀況下,咱們不須要使用它,咱們能夠在渲染過程當中更新 state,以達到實現 getDerivedStateFromProps
的目的。安全
function ScrollView({row}) {
let [isScrollingDown, setIsScrollingDown] = useState(false);
let [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row 自上次渲染以來發生過改變。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
複製代碼
React 會當即退出第一次渲染並用更新後的 state 從新運行組件以免耗費太多性能。app
shouldComponentUpdate
:能夠用 React.memo
包裹一個組件來對它的 props
進行淺比較dom
const Button = React.memo((props) => {
// 具體的組件
});
複製代碼
注意:React.memo
等效於 PureComponent
,它只淺比較 props。這裏也可使用 useMemo
優化每個節點。
render
:這是函數組件體自己。
componentDidMount
, componentDidUpdate
: useLayoutEffect
與它們兩的調用階段是同樣的。可是,咱們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect
。useEffect
能夠表達全部這些的組合。
// componentDidMount
useEffect(()=>{
// 須要在 componentDidMount 執行的內容
}, [])
useEffect(() => {
// 在 componentDidMount,以及 count 更改時 componentDidUpdate 執行的內容
document.title = `You clicked ${count} times`;
return () => {
// 須要在 count 更改時 componentDidUpdate(先於 document.title = ... 執行,遵照先清理後更新)
// 以及 componentWillUnmount 執行的內容
} // 當函數中 Cleanup 函數會按照在代碼中定義的順序前後執行,與函數自己的特性無關
}, [count]); // 僅在 count 更改時更新
複製代碼
請記得 React 會等待瀏覽器完成畫面渲染以後纔會延遲調用 useEffect
,所以會使得額外操做很方便
componentWillUnmount
:至關於 useEffect
裏面返回的 cleanup
函數
// componentDidMount/componentWillUnmount
useEffect(()=>{
// 須要在 componentDidMount 執行的內容
return function cleanup() {
// 須要在 componentWillUnmount 執行的內容
}
}, [])
複製代碼
componentDidCatch
and getDerivedStateFromError
:目前尚未這些方法的 Hook 等價寫法,但很快會加上。
爲方便記憶,大體彙總成表格以下。
class 組件 | Hooks 組件 |
---|---|
constructor | useState |
getDerivedStateFromProps | useState 裏面 update 函數 |
shouldComponentUpdate | useMemo |
render | 函數自己 |
componentDidMount | useEffect |
componentDidUpdate | useEffect |
componentWillUnmount | useEffect 裏面返回的函數 |
componentDidCatch | 無 |
getDerivedStateFromError | 無 |
咱們能夠將生命週期分爲三個階段:
分開來說:
constructor
:避免將 props 的值複製給 statecomponentWillMount
render
:react 最重要的步驟,建立虛擬 dom,進行 diff 算法,更新 dom 樹都在此進行componentDidMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
componentWillUnMount
這種生命週期會存在一個問題,那就是當更新複雜組件的最上層組件時,調用棧會很長,若是在進行復雜的操做時,就可能長時間阻塞主線程,帶來很差的用戶體驗,Fiber 就是爲了解決該問題而生。
Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先級自由調度這些幀,從而將以前的同步渲染改爲了異步渲染,在不影響體驗的狀況下去分段計算更新。
對於異步渲染,分爲兩階段:
reconciliation
:
componentWillMount
componentWillReceiveProps
shouldConmponentUpdate
componentWillUpdate
commit
componentDidMount
componentDidUpdate
其中,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 中更正爲都調用。
即更新後的生命週期爲:
constructor
static getDerivedStateFromProps
render
componentDidMount
static getDerivedStateFromProps
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
componentWillUnmount
誤解一: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>
</>
);
}
}
複製代碼
固然,咱們能夠在 父組件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
});
}
}
// ...
}
複製代碼
但這種也會致使一個問題,當 props 較爲複雜時,props 與 state 的關係很差控制,可能致使問題
解決方案一:徹底可控的組件
function PhoneInput(props) {
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}
/>
複製代碼
當 key
變化時, React 會建立一個新的而不是更新一個既有的組件
誤解二:將 props 的值直接複製給 state
應避免將 props 的值複製給 state
constructor(props) {
super(props);
// 千萬不要這樣作
// 直接用 props,保證單一數據源
this.state = { phone: props.phone };
}
複製代碼
掛載階段
分 兩個 階段:
render
,解析其下有哪些子組件須要渲染,並對其中 同步的子組件 進行建立,按 遞歸順序 挨個執行各個子組件至 render
,生成到父子組件對應的 Virtual DOM 樹,並 commit 到 DOM。componentDidMount
,最後觸發父組件的。注意:若是父組件中包含異步子組件,則會在父組件掛載完成後被建立。
因此執行順序是:
父組件 getDerivedStateFromProps —> 同步子組件 getDerivedStateFromProps —> 同步子組件 componentDidMount —> 父組件 componentDidMount —> 異步子組件 getDerivedStateFromProps —> 異步子組件 componentDidMount
更新階段
React 的設計遵循單向數據流模型 ,也就是說,數據均是由父組件流向子組件。
第 一 階段,由父組件開始,執行
static getDerivedStateFromProps
shouldComponentUpdate
更新到自身的 render
,解析其下有哪些子組件須要渲染,並對 子組件 進行建立,按 遞歸順序 挨個執行各個子組件至 render
,生成到父子組件對應的 Virtual DOM 樹,並與已有的 Virtual DOM 樹 比較,計算出 Virtual DOM 真正變化的部分 ,並只針對該部分進行的原生DOM操做。
第 二 階段,此時 DOM 節點已經生成完畢,組件掛載完成,開始後續流程。先依次觸發同步子組件如下函數,最後觸發父組件的。
getSnapshotBeforeUpdate()
componentDidUpdate()
React 會按照上面的順序依次執行這些函數,每一個函數都是各個子組件的先執行,而後纔是父組件的執行。
因此執行順序是:
父組件 getDerivedStateFromProps —> 父組件 shouldComponentUpdate —> 子組件 getDerivedStateFromProps —> 子組件 shouldComponentUpdate —> 子組件 getSnapshotBeforeUpdate —> 父組件 getSnapshotBeforeUpdate —> 子組件 componentDidUpdate —> 父組件 componentDidUpdate
卸載階段
componentWillUnmount()
,順序爲 父組件的先執行,子組件按照在 JSX 中定義的順序依次執行各自的方法。
注意 :若是卸載舊組件的同時伴隨有新組件的建立,新組件會先被建立並執行完 render
,而後卸載不須要的舊組件,最後新組件執行掛載完成的回調。
掛載階段
如果同步路由,它們的建立順序和其在共同父組件中定義的前後順序是 一致 的。
如果異步路由,它們的建立順序和 js 加載完成的順序一致。
更新階段、卸載階段
兄弟節點之間的通訊主要是通過父組件(Redux 和 Context 也是經過改變父組件傳遞下來的 props
實現的),知足React 的設計遵循單向數據流模型, 所以任何兩個組件之間的通訊,本質上均可以歸結爲父子組件更新的狀況 。
因此,兄弟組件更新、卸載階段,請參考 父子組件。
走在最後:走心推薦一個在線編輯工具:StackBlitz,能夠在線編輯 Angular、React、TypeScript、RxJS、Ionic、Svelte項目
預告:後續將加入高階組件的生命週期,敬請期待小瓶子的下次更新。
想看更過系列文章,點擊前往 github 博客主頁
走在最後,歡迎關注:前端瓶子君,每日更新