1. 程式人生 > >react精華之非同步渲染 v0.14.8 跳到了 v15.0.0 同步卡頓問題 為什麼不在 componentWillMount 裡去做AJAX?componentWillMount 可是比

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 Lifecycle before 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 Lifecycle in v16.3

 

 

注意,上圖中幷包含全部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。正確的做法是根據各函式的語義來放置程式碼,並不是越往前越好。