本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出
來個人 GitHub repo 閱讀完整的專題文章
來個人 我的博客 得到無與倫比的閱讀體驗
生命週期,顧名思義,就是從生到死的過程。javascript
而生命週期鉤子,就是從生到死過程當中的關鍵節點。java
普通人的一輩子有哪些生命週期鉤子呢?react
每到關鍵節點,咱們總但願有一些沉思時刻,由於這時候作出的決策會改變人生的走向。git
React組件也同樣,它會給開發者一些沉思時刻,在這裏,開發者能夠改變組件的走向。github
React花了兩年時間祭出Fiber渲染機制。緩存
簡單來講,React將diff的過程叫作Reconciliation。之前這一過程是一鼓作氣的,Fiber機制把它改爲了異步。異步技能將在接下來的版本中逐步解鎖。服務器
明明是一段同步代碼,怎麼就異步了呢?app
原理是Fiber把任務切成很小的片,每執行一片就把控制權交還給主線程,待主線程忙完手頭的活再來執行剩下的任務。固然若是某一片的執行時間就很長(好比死循環),那就沒主線程什麼事了,該崩潰崩潰。異步
這會給生命週期帶來什麼影響呢?ide
影響就是掛載和更新以前的生命週期都變的不可靠了。
爲何這麼講?由於Reconciliation這個過程有可能暫停而後繼續執行,因此掛載和更新以前的生命週期鉤子就有可能不執行或者屢次執行,它的表現是不可預期的。
所以16以後的React生命週期迎來了一波大換血,如下生命週期鉤子將被逐漸廢棄:
看出特色了麼,都是帶有will
的鉤子。
目前React爲這幾個生命週期鉤子提供了別名,分別是:
React17將只提供別名,完全廢棄這三個大活寶。取這麼個別名意思就是讓你用着噁心。
React借用class類的constructor
充當初始化鉤子。
React幾乎沒作什麼手腳,可是由於咱們只容許經過特定的途徑給組件傳遞參數,因此constructor
的參數其實是被React規定好的。
React規定constructor
有三個參數,分別是props
、context
和updater
。
props
是屬性,它是不可變的。context
是全局上下文。updater
是包含一些更新方法的對象,this.setState
最終調用的是this.updater.enqueueSetState
方法,this.forceUpdate
最終調用的是this.updater.enqueueForceUpdate
方法,因此這些API更可能是React內部使用,暴露出來是以備開發者不時之需。在React中,由於全部class組件都要繼承自Component
類或者PureComponent
類,所以和原生class寫法同樣,要在constructor
裏首先調用super
方法,才能得到this
。
constructor
生命週期鉤子的最佳實踐是在這裏初始化this.state
。
固然,你也可使用屬性初始化器來代替,以下:
import React, { Component } from 'react'; class App extends Component { state = { name: 'biu', }; } export default App;
💀這是React再也不推薦使用的API。
這是組件掛載到DOM以前的生命週期鉤子。
不少人會有一個誤區:這個鉤子是請求數據而後將數據插入元素一同掛載的最佳時機。
其實componentWillMount
和掛載是同步執行的,意味着執行完這個鉤子,當即掛載。而向服務器請求數據是異步執行的。因此不管請求怎麼快,都要排在同步任務以後再處理,這是輩分問題。
也就是說,永遠不可能在這裏將數據插入元素一同掛載。
並非說不能在這裏請求數據,而是達不到你臆想的效果。
它被廢棄的緣由主要有兩點:
componentWillUnmount
生命週期鉤子中的清除代碼不會生效。由於若是組件沒有掛載成功,componentWillUnmount
是不會執行的。姚明說的:沒有掛載就沒有卸載。初始化this.state
應該在constructor
生命週期鉤子中完成,請求數據應該在componentDidMount
生命週期鉤子中完成,因此它不只被廢棄了,連繼任者都沒有。
👽這是React v16.3.0發佈的API。
首先,這是一個靜態方法生命週期鉤子。
也就是說,定義的時候得在方法前加一個static
關鍵字,或者直接掛載到class類上。
簡要區分一下實例方法和靜態方法:
this
上或者掛載在prototype
上,class類不能直接訪問該方法,使用new
關鍵字實例化以後,實例能夠訪問該方法。static
,實例沒法直接訪問該方法。問題是,爲何getDerivedStateFromProps
生命週期鉤子要設計成靜態方法呢?
這樣開發者就訪問不到this
也就是實例了,也就不能在裏面調用實例方法或者setsState了。
import React, { Component } from 'react'; class App extends Component { render() { return ( <div>React</div> ); } static getDerivedStateFromProps(props, state) {} } export default App;
這個生命週期鉤子的使命是根據父組件傳來的props按需更新本身的state,這種state叫作衍生state。返回的對象就是要增量更新的state。
它被設計成靜態方法的目的是保持該方法的純粹,它就是用來定義衍生state的,除此以外不該該在裏面執行任何操做。
這個生命週期鉤子也經歷了一些波折,本來它是被設計成初始化
、父組件更新
和接收到props
纔會觸發,如今只要渲染就會觸發,也就是初始化
和更新階段
都會觸發。
做爲一個組件,最核心的功能就是把元素掛載到DOM上,因此render
生命週期鉤子是必定會用到的。
render
生命週期鉤子怎麼接收模板呢?固然是你return給它。
可是不推薦在return以前寫過多的邏輯,若是邏輯過多,能夠封裝成一個函數。
render() { // 這裏能夠寫一些邏輯 return ( <div> <input type="text" /> <button>click</button> </div> ); }
注意,千萬不要在render
生命週期鉤子裏調用this.setState
,由於this.setState
會引起render,這下就沒完沒了了。主公,有內奸。
這是組件掛載到DOM以後的生命週期鉤子。
這多是除了render
以外最重要的生命週期鉤子,由於這時候組件的各方面都準備就緒,天地任你闖。
這就是社會哥,人狠話很少。
💀這是React再也不推薦使用的API。
componentWillReceiveProps
生命週期鉤子只有一個參數,更新後的props。
該聲明周期函數可能在兩種狀況下被觸發:
初始化時並不會觸發該生命週期鉤子。
一樣,由於Fiber機制的引入,這個生命週期鉤子有可能會屢次觸發。
這個生命週期鉤子是一個開關,判斷是否須要更新,主要用來優化性能。
有一個例外,若是開發者調用this.forceUpdate
強制更新,React組件會無視這個鉤子。
shouldComponentUpdate
生命週期鉤子默認返回true。也就是說,默認狀況下,只要組件觸發了更新,組件就必定會更新。React把判斷的控制權給了開發者。
不過周到的React還提供了一個PureComponent
基類,它與Component
基類的區別是PureComponent
自動實現了一個shouldComponentUpdate
生命週期鉤子。
對於組件來講,只有狀態發生改變,才須要從新渲染。因此shouldComponentUpdate
生命週期鉤子暴露了兩個參數,開發者能夠經過比較this.props
和nextProps
、this.state
和nextState
來判斷狀態到底有沒有發生改變,再相應的返回true或false。
什麼狀況下狀態沒改變,卻依然觸發了更新呢?舉個例子:
父組件給子組件傳了一個值,當父組件狀態變化,即使子組件接收到的值沒有變化,子組件也會被迫更新。這顯然是很是不合理的,React對此無能爲力,只能看開發者的我的造化了。
import React, { Component } from 'react'; import Child from './Child'; class App extends Component { state = { name: 'React', star: 1 }; render() { const { name, star } = this.state; return ( <div> <Child name={name} /> <div>{star}</div> <button onClick={this.handle}>click</button> </div> ); } handle = () => { this.setState(prevState => ({ star: ++prevState.star })); } } export default App;
import React, { Component } from 'react'; class Child extends Component { render() { return <h1>{this.props.name}</h1>; } shouldComponentUpdate(nextProps, nextState) { if (this.props === nextProps) { return false; } else { return true; } } } export default Child;
同時要注意引用類型的坑。
下面這種狀況,this.props
和nextProps
永遠不可能相等。
import React, { Component } from 'react'; import Child from './Child'; class App extends Component { state = { name: 'React', star: 1 }; render() { return ( <div> <Child name={{ friend: 'Vue' }} /> <div>{this.state.star}</div> <button onClick={this.handle}>click</button> </div> ); } handle = () => { this.setState(prevState => ({ star: ++prevState.star })); } } export default App;
import React, { Component } from 'react'; class Child extends Component { render() { return <h1>{this.props.friend}</h1>; } shouldComponentUpdate(nextProps, nextState) { if (this.props === nextProps) { return false; } else { return true; } } } export default Child;
解決方法有兩個:
this.props.xxx
和nextProps.xxx
。因此this.state
和nextState
是隻能用第一種方法比較了,由於React每次更新state都會返回一個新對象,而不是修改原對象。
💀這是React再也不推薦使用的API。
shouldComponentUpdate
生命週期鉤子返回true,或者調用this.forceUpdate
以後,會當即執行該生命週期鉤子。
要特別注意,componentWillUpdate
生命週期鉤子每次更新前都會執行,因此在這裏調用this.setState
很是危險,有可能會沒完沒了。
一樣,由於Fiber機制的引入,這個生命週期鉤子有可能會屢次調用。
👽這是React v16.3.0發佈的API。
顧名思義,保存狀態快照用的。
它會在組件即將掛載時調用,注意,是即將掛載。它甚至調用的比render
還晚,因而可知render
並無完成掛載操做,而是進行構建抽象UI的工做。getSnapshotBeforeUpdate
執行完就會當即調用componentDidUpdate
生命週期鉤子。
它是作什麼用的呢?有一些狀態,好比網頁滾動位置,我不須要它持久化,只須要在組件更新之後可以恢復原來的位置便可。
getSnapshotBeforeUpdate
生命週期鉤子返回的值會被componentDidUpdate
的第三個參數接收,咱們能夠利用這個通道保存一些不須要持久化的狀態,用完便可捨棄。
很顯然,它是用來取代componentWillUpdate
生命週期鉤子的。
意思就是說呀,開發者通常用不到它。
這是組件更新以後觸發的生命週期鉤子。
搭配getSnapshotBeforeUpdate
生命週期鉤子使用的時候,第三個參數是getSnapshotBeforeUpdate
的返回值。
一樣的,componentDidUpdate
生命週期鉤子每次更新後都會執行,因此在這裏調用this.setState
也很是危險,有可能會沒完沒了。
這是組件卸載以前的生命週期鉤子。
爲何組件快要卸載了還須要沉思時刻呢?
由於開發者要擦屁股吖。
React的最佳實踐是,組件中用到的事件監聽器、訂閱器、定時器都要在這裏銷燬。
固然我說的事件監聽器指的是這種:
componentDidMount() { document.addEventListener('click', () => {}); }
由於下面這種React會自動銷燬,不勞煩開發者了。
render( return ( <button onClick={this.handle}>click</button> ); )
👽這是React v16.3.0發佈的API。
它主要用來捕獲錯誤並進行相應處理,因此它的用法也比較特殊。
定製一個只有componentDidCatch
生命週期鉤子的ErrorBoundary
組件,它只作一件事:若是捕獲到錯誤,則顯示錯誤提示,若是沒有捕獲到錯誤,則顯示子組件。
將須要捕獲錯誤的組件做爲ErrorBoundary
的子組件渲染,一旦子組件拋出錯誤,整個應用依然不會崩潰,而是被ErrorBoundary
捕獲。
import React, { Component } from 'react'; class ErrorBoundary extends Component { state = { hasError: false }; render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } componentDidCatch(error, info) { this.setState({ hasError: true }); } } export default ErrorBoundary;
import React from 'react'; import ErrorBoundary from './ErrorBoundary'; import MyWidget from './MyWidget'; const App = () => { return ( <ErrorBoundary> <MyWidget /> </ErrorBoundary> ); } export default App;
這麼多生命週期鉤子,實際上總結起來只有三個過程:
掛載和卸載只會執行一次,更新會執行屢次。
一個完整的React組件生命週期會依次調用以下鉤子:
掛載
更新
卸載
掛載
更新
卸載
應用初次掛載時,咱們以render
和componentDidMount
爲例,React首先會調用根組件的render
鉤子,若是有子組件的話,依次調用子組件的render
鉤子,調用過程其實就是遞歸的順序。
等全部組件的render
鉤子都遞歸執行完畢,這時候執行權在最後一個子組件手裏,因而開始觸發下一輪生命週期鉤子,調用最後一個子組件的componentDidMount
鉤子,而後調用棧依次往上遞歸。
組件樹的生命週期調用棧走的是一個Z字形。
若是根組件沒有定義A生命週期鉤子而子組件定義了,那調用棧就從這個子組件的A生命週期鉤子開始。
另外,只要組件內定義了某個生命週期鉤子,即使它沒有任何動做,也會執行。
app.render(); child.render(); grandson.render(); // divide grandson.componentDidMount(); child.componentDidMount(); app.componentDidMount(); // divide app.render(); child.render(); grandson.render(); // divide grandson.componentDidUpdate(); child.componentDidUpdate(); app.componentDidUpdate();
固然,componentWillMount、componentWillReceiveProps和componentWillUpdate生命週期鉤子有可能被打斷執行,也有可能被屢次調用,表現是不穩定的。因此React決定逐步廢棄它們。
不過了解整個應用生命週期的正常調用順序,仍是有助於理解React的。
React專題一覽