前幾天面試問道 react 的相關知識,對我打擊比較大,感受對 react 認識很是膚淺,因此在這裏從新梳理一下,想一想以前沒有仔細思考過的東西。javascript
另外有說的不對的地方還請幫我指正一下,先謝謝各位啦。java
目錄索引:react
React 有一套合理的運行機制去控制程序在指定的時刻該作什麼事,當一個生命週期鉤子被觸發後,緊接着會有下一個鉤子,直到整個生命週期結束。面試
生命週期表明着每一個執行階段,好比組件初始化,更新完成,立刻要卸載等等,React 會在指定的時機執行相關的生命週期鉤子,使咱們能夠有機在程序運行中會插入本身的邏輯。算法
咱們寫代碼的時候每每會有不少組件以及他們的子組件,各自調用不一樣的生命週期,這時就要解決誰先誰後的問題,在 react v16 以前是採用了遞歸調用的方式一個一個執行,而在如今 v16 的版本中則採用了與之徹底不一樣的處理(調度)方式,名叫 Fiber,這個東西 facebook 作了有兩年時間,實現很是複雜。redux
具體 Fiber 它是一個什麼東西呢?不要着急,咱們先從最基本的生命週期鉤子看起。性能優化
首先看一下 React V16.4 後的生命週期概況(圖片來源) 異步
constructor()
- 類構造器初始化static getDerivedStateFromProps()
- 組件初始化時主動觸發render()
- 遞歸生成虛擬 DOMcomponentDidMount()
- 完成首次 DOM 渲染static getDerivedStateFromProps()
- 每次 render() 以前執行shouldComponentUpdate()
- 校驗是否須要執行更新操做render()
- 遞歸生成虛擬 DOMgetSnapshotBeforeUpdate()
- 在渲染真實 DOM 以前componentDidUpdate()
- 完成 DOM 渲染componentWillUnmount()
- 組件銷燬以前被直接調用static getDerivedStateFromProps()
這個鉤子會在每一個更新操做以前(即便props沒有改變)執行一次,使用時應該保持謹慎。componentDidMount()
和 componentDidUpdate()
執行的時機是差很少的,都在 render
以後,只不過前者只在首次渲染後執行,後者首次渲染不會執行getSnapshotBeforeUpdate()
執行時能夠得到只讀的新 DOM 樹,此函數的返回值爲 componentDidUpdate(prevProps, prevState, snapshot)
的第三個參數關於 Fiber,強烈建議聽一下知乎上程墨Morgan的 live 《深刻理解React v16 新功能》,這裏潛水員的例子和圖片也是引用於此 live。async
咱們知道 React 是經過遞歸的方式來渲染組件的,在 V16 版本以前的版本里,當一個狀態發生變動時,react 會從當前組件開始,依次遞歸調用全部的子組件生命週期鉤子,並且這個過程是同步執行的且沒法中斷的,一旦有很深很深的組件嵌套,就會形成嚴重的頁面卡頓,影響用戶體驗。函數
React 在V16版本以前的版本里引入了 Fiber 這樣一個東西,它的英文涵義爲纖維,在計算機領域它排在在進程和線程的後面,雖然 React 的 Fiber 和計算機調度裏的概念不同,可是能夠方便對比理解,咱們大概能夠想象到 Fiber 多是一個比線程還短的時間片斷。
Fiber 把當前須要執行的任務分紅一個個微任務,安排優先級,而後依次處理,每過一段時間(很是短,毫秒級)就會暫停當前的任務,查看有沒有優先級較高的任務,而後暫停(也可能會徹底放棄)掉以前的執行結果,跳出到下一個微任務。同時 Fiber 還作了一些優化,能夠保持住以前運行的結果以到達複用目的。
咱們能夠把調度當成一個潛水員在海底尋寶,v16 以前是經過組件遞歸的方式進行尋寶,從父組件開始一層一層深刻到最裏面的子組件,也就是以下圖所示。
而替換成了 Fiber 後,海底變成的狹縫(簡單理解爲遞歸變成了遍歷),潛水員會每隔一小段時間浮出水面,看看有沒有其餘尋寶任務。注意此時沒有尋到寶藏的話,那麼以前潛水的時間就浪費了。就這樣潛水員會一直下潛和冒泡,具體以下圖所示。
從生命週期那張圖片縱向來看,Fiber 將整個生命週期分紅了三個階段:
componentWillMount()
,componentWillUpdate()
,componentWillReceiveProps()
的三個生命週期鉤子被加上了 UNSAFE
標記簡而言之:以 render() 爲界,以前執行的生命週期都有可能會打斷並屢次調用,以後的生命週期是不可被打斷的且只會調用一次。因此儘可能把反作用的代碼放在只會執行一次的 commit 階段。
除了上面經常使用的鉤子,React 還提供了以下鉤子:
static getDerivedStateFromError()
在 render 階段執行,經過返回 state 更新組件狀態componentDidCatch()
在 commit 階段執行,能夠放一些有反作用的代碼理解了生命週期和三個執行階段,就能夠比較容易理解組件狀態的更新機制了。
這個方法可讓咱們更新組件的 state 狀態。第一個參數能夠是對象,也能夠是 updater 函數,若是是函數,則會接受當前的 state 和 props 做爲參數。第二個參數爲函數,是在 commit 階段後執行,準確的說是在 componentDidUpdate()
後執行。
setState() 的更新過程是異步的(除非綁定在 DOM 事件中或寫在 setTimeout 裏),並且會在最後合併全部的更新,以下:
Object.assign(
previousState,
{quantity: state.quantity + 1},
{quantity: state.quantity + 1},
...
)
複製代碼
之因此設計成這樣,是爲了不在一次生命週期中出現屢次的重渲染,影響頁面性能。
若是咱們想強制刷新一個組件,能夠直接調用該方法,調用時會直接執行 render()
這個函數而跳過 shouldComponentUpdate()
。
function wait() {
return new Promise(resolve => {
setTimeout(() => {
resolve();
console.log("wait");
}, 0);
});
}
//......省略組件建立
async componentDidMount() {
await wait();
this.setState({
name: "new name"
});
console.log("componentDidMount");
}
componentDidUpdate() {
console.log("componentDidUpdate");
}
render() {
console.log(this.state);
return null
}
//......省略組件建立
// 輸出結果以下
// wait
// {name: "new name"}
// componentDidUpdate
// componentDidMount
// 注意 componentDidUpdate 的輸出位置,通常狀況下
// componentDidUpdate 都是在componentDidMount 後面
// 執行的,可是這裏由於setState 寫在了 await 後面
// 因此狀況相反。
複製代碼
瞭解 react 生命週期和更新機制確實有利於編寫代碼,特別是當代碼量愈來愈大時,錯用的 setState 或生命週期鉤子均可能埋下愈來愈多的雷,直到有一天沒法維護。。。
個人我的建議以下:
getDerivedStateFromProps()
當成是 UNSAFE_componentWillReceiveProps()
的替代品,由於 getDerivedStateFromProps()
會在每次 render() 以前執行,即便 props 沒有改變