react精華之非同步渲染 v0.14.8 跳到了 v15.0.0 同步卡頓問題 為什麼不在 componentWillMount 裡去做AJAX?componentWillMount 可是比
render之後執行componentdidMount
不要再componentWillMount 中做 AJAX
同步渲染的問題
長期以來,React 一直用的是同步渲染,這樣對 React 實現非常直觀方便,但是會帶來效能問題。
假設有一個超大的 React 元件樹結構,有 1000 個元件,每個元件平均使用 1 毫秒,那麼,要做一次完整的渲染就要花費 1000 毫秒也就是 1 秒鐘,然而 JavaScript 執行環境是單執行緒的,也就是說,React 用同步渲染方式,渲染最根部元件的時候,會同步引發渲染子元件,再同步渲染子元件的子元件……最後完成整個元件樹。在這 1 秒鐘內,同步渲染霸佔 JavaScript 唯一的執行緒,其他的操作什麼都做不了,在這 1 秒鐘內,如果使用者要點選什麼按鈕,或者在某個輸入框裡面按鍵,都不會看到立即的介面反應,這也就是俗話說的“卡頓”。
在同步渲染下,要解決“卡頓”的問題,只能是儘量縮小元件樹的大小,以此縮短渲染時間,但是,應用的規模總是在增大的,不是說縮小就能縮小的,雖然我們利用定義 shouldComponentUpdate 的方法可以減少不必要的渲染,但是這也無法從根本上解決大量同步渲染帶來的“卡頓”問題。
非同步渲染:兩階段渲染
React Fiber 引入了非同步渲染,有了非同步渲染之後,React 元件的渲染過程是分時間片的,不是一口氣從頭到尾把子元件全部渲染完,而是每個時間片渲染一點,然後每個時間片的間隔都可去看看有沒有更緊急的任務(比如使用者按鍵),如果有,就去處理緊急任務,如果沒有那就繼續照常渲染。
根據 React Fiber 的設計,一個元件的渲染被分為兩個階段:第一個階段(也叫做 render 階段)是可以被 React 打斷的,一旦被打斷,這階段所做的所有事情都被廢棄,當 React 處理完緊急的事情回來,依然會重新渲染這個元件,這時候第一階段的工作會重做一遍;第二個階段叫做 commit 階段,一旦開始就不能中斷,也就是說第二個階段的工作會穩穩當當地做到這個元件的渲染結束。
兩個階段的分界點,就是 render
函式。render 函式之前的所有生命週期函式(包括 render)都屬於第一階段,之後的都屬於第二階段。
開啟非同步渲染,雖然我們獲得了更好的感知效能,但是考慮到第一階段的的生命週期函式可能會被重複呼叫,不得不對歷史程式碼做一些調整。
在 React v16.3 之前,render 之前的生命週期函式(也就是第一階段生命週期函式)包括這些:
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- componentWillMount
- render
下圖是 React v16.3 之前的完整的生命週期函式圖:
React 官方告誡開發者,雖然目前所有的程式碼都可以照常使用,但是未來版本中會廢棄掉,為了將來,使用 React 的程式應該快點去掉這些在第一階段生命函式中有副作用的功能。不得不說 React 真的很夠意思,提前這麼久告訴大家這個事情,讓大家有足夠的時間去修改自己的程式碼。
一個典型的錯誤用例,也是我被問到做多的問題之一:為什麼不在 componentWillMount 裡去做AJAX?componentWillMount 可是比 componentDidMount 更早呼叫啊,更早呼叫意味著更早返回結果,那樣效能不是更高嗎?
首先,一個元件的 componentWillMount 比 componentDidMount 也早調用不了幾微秒,效能沒啥提高;而且,等到非同步渲染開啟的時候,componentWillMount 就可能被中途打斷,中斷之後渲染又要重做一遍,想一想,在 componentWillMount 中做 AJAX 呼叫,程式碼裡看到只有呼叫一次,但是實際上可能呼叫 N 多次,這明顯不合適。相反,若把 AJAX 放在 componentDidMount,因為 componentDidMount 在第二階段,所以絕對不會多次重複呼叫,這才是 AJAX 合適的位置(當然,React 未來有更好的辦法,在下一小節 Suspense 中可以講到)。
getDerivedStateFromProps
到了 React v16.3,React 乾脆引入了一個新的生命週期函式 getDerivedStateFromProps,這個生命週期函式是一個 static 函式,在裡面根本不能通過 this
訪問到當前元件,輸入只能通過引數,對元件渲染的影響只能通過返回值。沒錯,getDerivedStateFromProps 應該是一個純函式,React 就是通過要求這種純函式,強制開發者們必須適應非同步渲染。
static getDerivedStateFromProps(nextProps, prevState) {
//根據nextProps和prevState計算出預期的狀態改變,返回結果會被送給setState
}
到了 React v16.3,React 生命週期函式全圖如下:
注意,上圖中幷包含全部React生命週期函式,在React v16釋出時,還增加了一個componentDidCatch
,當異常發生時,一個可以捕捉到異常的componentDidCatch
就排上用場了。不過,很快React覺著這還不夠,在v16.6.0又推出了一個新的捕捉異常的生命週期函式getDerivedStateFromError
。
如果異常發生在第一階段(render階段),React就會呼叫getDerivedStateFromError
,如果異常發生在第二階段(commit階段),React會呼叫componentDidCatch
。這個區別也體現出兩個階段的區分對待。
適應非同步渲染的元件原則
明白了非同步渲染的來龍去脈之後,開發者就應該明白,現在寫程式碼必須要為未來的某一次 React 版本升級做好準備,當 React 開啟非同步渲染的時候,你的程式碼應該做到在 render 之前最多隻能這些函式被呼叫:
- 建構函式
- getDerivedStateFromProps
- shouldComponentUpdate
倖存的這些第一階段函式,除了建構函式,其餘兩個全都必須是純函式,也就是不應該做任何有副作用的操作。
實際上,如果之前你的用法規範,除了 shouldComponentUpdate 不怎麼使用第一階段生命週期函式,你還會發現不怎麼需要改動程式碼,比如 componentWillMount 中的程式碼移到建構函式中就可以了。但是如果用法錯亂,比如濫用componentWillReceiveProps,那就不得不具體情況具體分析,從而決定這些程式碼移到什麼位置。
開發者中一個普遍的誤區,就是總想把任務往前提,提到靠前的生命週期函式去,就像我前面說過的在 componentWillMount 中做 AJAX。正確的做法是根據各函式的語義來放置程式碼,並不是越往前越好。