1. 程式人生 > 實用技巧 >react專案8點優化

react專案8點優化

本文篇幅較長,將從編譯階段->路由階段->渲染階段->細節優化->狀態管理->海量資料來源,長列表渲染
方向分別加以探討。

一 不能輸在起跑線上,優化babel配置,webpack配置為項

1 真實專案中痛點

當我們用create-react-app或者webpack構建react工程的時候,有沒有想過一個問題,我們的配置能否讓我們的專案更快的構建速度,更小的專案體積,更簡潔清晰的專案結構。隨著我們的專案越做越大,專案依賴越來越多,專案結構越來越來複雜,專案體積就會越來越大,構建時間越來越長,久而久之就會成了一個又大又重的專案,所以說我們要學會適當的為專案‘減負’,讓專案不能輸在起跑線上。

2 一個老專案

拿我們之前接觸過的一個react老專案為例。我們沒有用dva,umi快速搭建react,而是用react老版本腳手架構建的,這對這種老的react專案,上述的問題都會存在,下面讓我們一起來看看。

我們首先看一下專案結構。

再看看構建時間。

為了方便大家看構建時間,我簡單寫了一個webpack,pluginConsolePlugin,記錄了webpack在一次compilation所用的時間。

const chalk = require('chalk') /* console 顏色 */
var slog = require('single-line-log'); /* 單行列印 console */

class ConsolePlugin {
constructor(options){
this.options = options
}
apply(compiler){
/**
* Monitor file change 記錄當前改動檔案
*/
compiler.hooks.watchRun.tap('ConsolePlugin', (watching) => {
const changeFiles = watching.watchFileSystem.watcher.mtimes
for(let file in changeFiles){
console.log(chalk.green('當前改動檔案:'+ file))
}
})
/**
* before a new compilation is created. 開始 compilation 編譯 。
*/
compiler.hooks.compile.tap('ConsolePlugin',()=>{
this.beginCompile()
})
/**
* Executed when the compilation has completed. 一次 compilation 完成。
*/
compiler.hooks.done.tap('ConsolePlugin',()=>{
this.timer && clearInterval( this.timer )
const endTime = new Date().getTime()
const time = (endTime - this.starTime) / 1000
console.log( chalk.yellow(' 編譯完成') )
console.log( chalk.yellow('編譯用時:' + time + '秒' ) )
})
}
beginCompile(){
const lineSlog = slog.stdout
let text = '開始編譯:'
/* 記錄開始時間 */
this.starTime = new Date().getTime()
this.timer = setInterval(()=>{
text += '█'
lineSlog( chalk.green(text))
},50)
}
}

構建時間如下:

打包後的體積:

3 翻新老專案

針對上面這個react老專案,我們開始針對性的優化。由於本文主要講的是react,所以我們不把太多篇幅給webpack優化上。

① include 或 exclude 限制 loader 範圍。

{
test: /\.jsx?$/,
exclude: /node_modules/,
include: path.resolve(__dirname, '../src'),
use:['happypack/loader?id=babel']
// loader: 'babel-loader'
}

② happypack多程序編譯

除了上述改動之外,在plugin中

/* 多執行緒編譯 */
new HappyPack({
id:'babel',
loaders:['babel-loader?cacheDirectory=true']
})

③快取babel編譯過的檔案

loaders:['babel-loader?cacheDirectory=true']

④tree Shaking 刪除冗餘程式碼

⑤按需載入,按需引入。

優化後項目結構

優化構建時間如下:

一次compilation時間 從23秒優化到了4.89秒

優化打包後的體積:

由此可見,如果我們的react是自己徒手搭建的,一些優化技巧顯得格外重要。

關於類似antd UI庫的瘦身思考

我們在做react專案的時候,會用到antd之類的ui庫,值得思考的一件事是,如果我們只是用到了antd中的個別元件,比如<Button />,就要把整個樣式庫引進來,打包就會發現,體積因為引入了整個樣式大了很多。我們可以通過.babelrc實現按需引入。

瘦身前

.babelrc增加對antd樣式按需引入。

["import", {
"libraryName":
"antd",
"libraryDirectory": "es",
"style": true
}]

瘦身後

總結

如果想要優化react專案,從構建開始是必不可少的。我們要重視從構建到打包上線的每一個環節。

二 路由懶載入,路由監聽器

react路由懶載入,是筆者看完dva原始碼中的dynamic非同步載入元件總結出來的,針對大型專案有很多頁面,在配置路由的時候,如果沒有對路由進行處理,一次性會載入大量路由,這對頁面初始化很不友好,會延長頁面初始化時間,所以我們想著用asyncRouter來按需載入頁面路由。

傳統路由

如果我們沒有用umi等框架,需要手動配置路由的時候,也許路由會這樣配置。

<Switch>
<Route path={'/index'} component={Index} ></Route>
<Route path={'/list'} component={List} ></Route>
<Route path={'/detail'} component={ Detail } ></Route>
<Redirect from='/*' to='/index' />
</Switch>

或者用list儲存路由資訊,方便在進行路由攔截,或者配置路由選單等。

const router = [
{
'path': '/index',
'component': Index
},
{
'path': '/list'',
'component': List
},
{
'path': '/detail',
'component': Detail
},
]

asyncRouter懶載入路由,並實現路由監聽

我們今天講的這種react路由懶載入是基於import函式路由懶載入, 眾所周知 ,import執行會返回一個Promise作為非同步載入的手段。我們可以利用這點來實現react非同步載入路由

好的一言不合上程式碼。。。

程式碼

const routerObserveQueue = [] /* 存放路由衛視鉤子 */
/* 懶載入路由衛士鉤子 */
export const RouterHooks = {
/* 路由元件載入之前 */
beforeRouterComponentLoad: function(callback) {
routerObserveQueue.push({
type: 'before',
callback
})
},
/* 路由元件載入之後 */
afterRouterComponentDidLoaded(callback) {
routerObserveQueue.push({
type: 'after',
callback
})
}
}
/* 路由懶載入HOC */
export default function AsyncRouter(loadRouter) {
return class Content extends React.Component {
constructor(props) {
super(props)
/* 觸發每個路由載入之前鉤子函式 */
this.dispatchRouterQueue('before')
}
state = {Component: null}
dispatchRouterQueue(type) {
const {history} = this.props
routerObserveQueue.forEach(item => {
if (item.type === type) item.callback(history)
})
}
componentDidMount() {
if (this.state.Component) return
loadRouter()
.then(module => module.default)
.then(Component => this.setState({Component},
() => {
/* 觸發每個路由載入之後鉤子函式 */
this.dispatchRouterQueue('after')
}))
}
render() {
const {Component} = this.state
return Component ? <Component {
...this.props
}
/> : null
}
}
}

asyncRouter實際就是一個高階元件,將()=>import()作為載入函式傳進來,然後當外部Route載入當前元件的時候,在componentDidMount生命週期函式,載入真實的元件,並渲染元件,我們還可以寫針對路由懶載入狀態定製屬於自己的路由監聽器beforeRouterComponentLoadafterRouterComponentDidLoaded,類似vuewatch $route功能。接下來我們看看如何使用。

使用

import AsyncRouter ,{ RouterHooks }  from './asyncRouter.js'
const { beforeRouterComponentLoad} = RouterHooks
const Index = AsyncRouter(()=>import('../src/page/home/index'))
const List = AsyncRouter(()=>import('../src/page/list'))
const Detail = AsyncRouter(()=>import('../src/page/detail'))
const index = () => {
useEffect(()=>{
/* 增加監聽函式 */
beforeRouterComponentLoad((history)=>{
console.log('當前啟用的路由是',history.location.pathname)
})
},[])
return <div >
<div >
<Router >
<Meuns/>
<Switch>
<Route path={'/index'} component={Index} ></Route>
<Route path={'/list'} component={List} ></Route>
<Route path={'/detail'} component={ Detail } ></Route>
<Redirect from='/*' to='/index' />
</Switch>
</Router>
</div>
</div>
}

效果

這樣一來,我們既做到了路由的懶載入,又彌補了react-router沒有監聽當前路由變化的監聽函式的缺陷。

三 受控性元件顆粒化 ,獨立請求服務渲染單元

可控性元件顆粒化,獨立請求服務渲染單元是筆者在實際工作總結出來的經驗。目的就是避免因自身的渲染更新或是副作用帶來的全域性重新渲染。

1 顆粒化控制可控性元件

可控性元件和非可控性的區別就是dom元素值是否與受到react資料狀態state控制。一旦由react的state控制資料狀態,比如input輸入框的值,就會造成這樣一個場景,為了使input值實時變化,會不斷setState,就會不斷觸發render函式,如果父元件內容簡單還好,如果父元件比較複雜,會造成牽一髮動全身,如果其他的子元件中componentWillReceiveProps這種帶有副作用的鉤子,那麼引發的蝴蝶效應不敢想象。比如如下demo

class index extends React.Component<any,any>{
constructor(props){
super(props)
this.state={
inputValue:''
}
}
handerChange=(e)=> this.setState({ inputValue:e.target.value })
render(){
const { inputValue } = this.state
return <div>
{ /* 我們增加三個子元件 */ }
<ComA />
<ComB />
<ComC />
<div className="box" >
<Input value={inputValue} onChange={ (e)=> this.handerChange(e) } />
</div>
{/* 我們首先來一個列表迴圈 */}
{
new Array(10).fill(0).map((item,index)=>{
console.log('列表迴圈了' )
return <div key={index} >{item}</div>
})
}
{
/* 這裡可能是更復雜的結構 */
/* ------------------ */
}
</div>
}
}

元件A

function index(){
console.log('元件A渲染')
return <div>我是元件A</div>
}

元件B,有一個componentWillReceiveProps鉤子

class Index extends React.Component{
constructor(props){
super(props)
}
componentWillReceiveProps(){
console.log('componentWillReceiveProps執行')
/* 可能做一些騷操作 wu lian */
}
render(){
console.log('元件B渲染')
return <div>
我是元件B
</div>
}
}

元件C有一個列表迴圈

class Index extends React.Component{
constructor(props){
super(props)
}

render(){
console.log('元件c渲染')
return <div>
我是元件c
{
new Array(10).fill(0).map((item,index)=>{
console.log('元件C列表迴圈了' )
return <div key={index} >{item}</div>
})
}
</div>
}
}

效果

當我們在input輸入內容的時候。就會造成如上的現象,所有的不該重新更新的地方,全部重新執行了一遍,這無疑是巨大的效能損耗。這個一個setState觸發帶來的一股巨大的由此元件到子元件可能更深的更新流,帶來的副作用是不可估量的。所以我們可以思考一下,是否將這種受控性元件顆粒化,讓自己更新 -> 渲染過程由自身排程。

說幹就幹,我們對上面的input表單單獨顆粒化處理。

const ComponentInput = memo(function({ notifyFatherChange }:any){
const [ inputValue , setInputValue ] = useState('')
const handerChange = useMemo(() => (e) => {
setInputValue(e.target.value)
notifyFatherChange && notifyFatherChange(e.target.value)
},[])
return <Input value={inputValue} onChange={ handerChange } />
})

此時的元件更新由元件單元自行控制,不需要父元件的更新,所以不需要父元件設定獨立state保留狀態。只需要繫結到this上即可。不是所有狀態都應該放在元件的 state 中. 例如快取資料。如果需要元件響應它的變動, 或者需要渲染到檢視中的資料才應該放到 state 中。這樣可以避免不必要的資料變動導致元件重新渲染.

class index extends React.Component<any,any>{   
formData :any = {}
render(){
return <div>
{ /* 我們增加三個子元件 */ }
<ComA />
<ComB />
<ComC />
<div className="box" >
<ComponentInput notifyFatherChange={ (value)=>{ this.formData.inputValue = value } } />
<Button onClick={()=> console.log(this.formData)} >列印資料</Button>
</div>
{/* 我們首先來一個列表迴圈 */}
{
new Array(10).fill(0).map((item,index)=>{
console.log('列表迴圈了' )
return <div key={index} >{item}</div>
})
}
{
/* 這裡可能是更復雜的結構 */
/* ------------------ */
}
</div>
}
}

效果

這樣除了當前元件外,其他地方沒有收到任何渲染波動,達到了我們想要的目的。

2 建立獨立的請求渲染單元

建立獨立的請求渲染單元,直接理解就是,如果我們把頁面,分為請求資料展示部分(通過呼叫後端介面,獲取資料),和基礎部分(不需要請求資料,已經直接寫好的),對於一些邏輯互動不是很複雜的資料展示部分,我推薦用一種獨立元件,獨立請求資料,獨立控制渲染的模式。至於為什麼我們可以慢慢分析。

首先我們看一下傳統的頁面模式。

頁面有三個展示區域分別,做了三次請求,觸發了三次setState,渲染三次頁面,即使用Promise.all等方法,但是也不保證接下來互動中,會有部分展示區重新拉取資料的可能。一旦有一個區域重新拉取資料,另外兩個區域也會說、受到牽連,這種效應是不可避免的,即便react有很好的ddiff演算法去調協相同的節點,但是比如長列表等情況,迴圈在所難免。

class Index extends React.Component{
state :any={
dataA:null,
dataB:null,
dataC:null
}
async componentDidMount(){
/* 獲取A區域資料 */
const dataA = await getDataA()
this.setState({ dataA })
/* 獲取B區域資料 */
const dataB = await getDataB()
this.setState({ dataB })
/* 獲取C區域資料 */
const dataC = await getDataC()
this.setState({ dataC })
}
render(){
const { dataA , dataB , dataC } = this.state
console.log(dataA,dataB,dataC)
return <div>
<div> { /* 用 dataA 資料做展示渲染 */ } </div>
<div> { /* 用 dataB 資料做展示渲染 */ } </div>
<div> { /* 用 dataC 資料做展示渲染 */ } </div>
</div>
}
}

接下來我們,把每一部分抽取出來,形成獨立的渲染單元,每個元件都獨立資料請求到獨立渲染。

function ComponentA(){
const [ dataA, setDataA ] = useState(null)
useEffect(()=>{
getDataA().then(res=> setDataA(res.data) )
},[])
return <div> { /* 用 dataA 資料做展示渲染 */ } </div>
}

function ComponentB(){
const [ dataB, setDataB ] = useState(null)
useEffect(()=>{
getDataB().then(res=> setDataB(res.data) )
},[])
return <div> { /* 用 dataB 資料做展示渲染 */ } </div>
}

function ComponentC(){
const [ dataC, setDataC ] = useState(null)
useEffect(()=>{
getDataC().then(res=> setDataC(res.data) )
},[])
return <div> { /* 用 dataC 資料做展示渲染 */ } </div>
}

function Index (){
return <div>
<ComponentA />
<ComponentB />
<ComponentC />
</div>
}

這樣一來,彼此的資料更新都不會相互影響。

總結

拆分需要單獨呼叫後端介面的細小元件,建立獨立的資料請求和渲染,這種依賴資料更新 -> 檢視渲染的元件,能從整個體系中抽離出來 ,好處我總結有以下幾個方面。

1 可以避免父元件的冗餘渲染 ,react的資料驅動,依賴於stateprops的改變,改變state必然會對元件render函式呼叫,如果父元件中的子元件過於複雜,一個自元件的state改變,就會牽一髮動全身,必然影響效能,所以如果把很多依賴請求的元件抽離出來,可以直接減少渲染次數。

2 可以優化元件自身效能,無論從class宣告的有狀態元件還是fun宣告的無狀態,都有一套自身優化機制,無論是用shouldupdate還是用hooksuseMemouseCallback,都可以根據自身情況,定製符合場景的渲條 件,使得依賴資料請求元件形成自己一個小的,適合自身的渲染環境。

3 能夠和redux,以及redux衍生出來redux-action,dva,更加契合的工作,用connect包裹的元件,就能通過制定好的契約,根據所需求的資料更新,而更新自身,而把這種模式用在這種小的,需要資料驅動的元件上,就會起到物盡其用的效果。

四 shouldComponentUpdate ,PureComponent 和 React.memo ,immetable.js 助力效能調優

在這裡我們拿immetable.js為例,講最傳統的限制更新方法,第六部分將要將一些避免重新渲染的細節。

1 PureComponent 和 React.memo

React.PureComponentReact.Component用法差不多 ,但React.PureComponent通過props和state的淺對比來實現shouldComponentUpate()。如果物件包含複雜的資料結構(比如物件和陣列),他會淺比較,如果深層次的改變,是無法作出判斷的,React.PureComponent認為沒有變化,而沒有渲染試圖。

如這個例子

class Text extends React.PureComponent<any,any>{
render(){
console.log(this.props)
return <div>hello,wrold</div>
}
}
class Index extends React.Component<any,any>{
state={
data:{ a : 1 , b : 2 }
}
handerClick=()=>{
const { data } = this.state
data.a++
this.setState({ data })
}
render(){
const { data } = this.state
return <div>
<button onClick={ this.handerClick } >點選</button>
<Text data={data} />
</div>
}
}

效果

我們點選按鈕,發現<Text />根本沒有重新更新。這裡雖然改了data但是隻是改變了data下的屬性,所以PureComponent進行淺比較不會update

想要解決這個問題實際也很容易。

 <Text data={{ ...data }} />

無論元件是否是PureComponent,如果定義了shouldComponentUpdate(),那麼會呼叫它並以它的執行結果來判斷是否update。在元件未定義shouldComponentUpdate()的情況下,會判斷該元件是否是PureComponent,如果是的話,會對新舊props、state進行shallowEqual比較,一旦新舊不一致,會觸發渲染更新。

react.memoPureComponent功能類似 ,react.memo作為第一個高階元件,第二個引數 可以對props進行比較 ,和shouldComponentUpdate不同的, 當第二個引數返回true的時候,證明props沒有改變,不渲染元件,反之渲染元件。

2 shouldComponentUpdate

使用shouldComponentUpdate()以讓React知道當state或props的改變是否影響元件的重新render,預設返回ture,返回false時不會重新渲染更新,而且該方法並不會在初始化渲染或當使用forceUpdate()時被呼叫,通常一個shouldComponentUpdate應用是這麼寫的。

控制狀態

shouldComponentUpdate(nextProps, nextState) {
/* 當 state 中 data1 發生改變的時候,重新更新元件 */
return nextState.data1 !== this.state.data1
}

這個的意思就是 僅當statedata1發生改變的時候,重新更新元件。控制prop屬性

shouldComponentUpdate(nextProps, nextState) {
/* 當 props 中 data2發生改變的時候,重新更新元件 */
return nextProps.data2 !== this.props.data2
}

這個的意思就是 僅當propsdata2發生改變的時候,重新更新元件。

3 immetable.js

immetable.js是Facebook 開發的一個js庫,可以提高物件的比較效能,像之前所說的pureComponent只能對物件進行淺比較,,對於物件的資料型別,卻束手無策,所以我們可以用immetable.js配合shouldComponentUpdate或者react.memo來使用。immutable

我們用react-redux來簡單舉一個例子,如下所示 資料都已經被immetable.js處理。

import { is  } from 'immutable'
const GoodItems = connect(state =>
({ GoodItems: filter(state.getIn(['Items', 'payload', 'list']), state.getIn(['customItems', 'payload', 'list'])) || Immutable.List(), })
/* 此處省略很多程式碼~~~~~~ */
)(memo(({ Items, dispatch, setSeivceId }) => {
/* */
}, (pre, next) => is(pre.Items, next.Items)))

通過is方法來判斷,前後Items(物件資料型別)是否發生變化。

五 規範寫法,合理處理細節問題

有的時候,我們在敲程式碼的時候,稍微注意一下,就能避免效能的開銷。也許只是稍加改動,就能其他優化效能的效果。

①繫結事件儘量不要使用箭頭函式

面臨問題

眾所周知,react更新來大部分情況來自於props的改變(被動渲染),和state改變(主動渲染)。當我們給未加任何更新限定條件子元件繫結事件的時候,或者是PureComponent純元件, 如果我們箭頭函式使用的話。

<ChildComponent handerClick={()=>{ console.log(666) }}  />

每次渲染時都會建立一個新的事件處理器,這會導致ChildComponent每次都會被渲染。

即便我們用箭頭函式繫結給dom元素。

<div onClick={ ()=>{ console.log(777) } } >hello,world</div>

每次react合成事件事件的時候,也都會重新宣告一個新事件。

解決問題

解決這個問題事件很簡單,分為無狀態元件和有狀態元件。

有狀態元件

class index extends React.Component{
handerClick=()=>{
console.log(666)
}
handerClick1=()=>{
console.log(777)
}
render(){
return <div>
<ChildComponent handerClick={ this.handerClick } />
<div onClick={ this.handerClick1 } >hello,world</div>
</div>
}
}

無狀態元件

function index(){

const handerClick1 = useMemo(()=>()=>{
console.log(777)
},[]) /* [] 存在當前 handerClick1 的依賴項*/
const handerClick = useCallback(()=>{ console.log(666) },[]) /* [] 存在當前 handerClick 的依賴項*/
return <div>
<ChildComponent handerClick={ handerClick } />
<div onClick={ handerClick1 } >hello,world</div>
</div>
}

對於dom,如果我們需要傳遞引數。我們可以這麼寫。

function index(){
const handerClick1 = useMemo(()=>(event)=>{
const mes = event.currentTarget.dataset.mes
console.log(mes) /* hello,world */
},[])
return <div>
<div data-mes={ 'hello,world' } onClick={ handerClick1 } >hello,world</div>
</div>
}

②迴圈正確使用key

無論是reactvue,正確使用key,目的就是在一次迴圈中,找到與新節點對應的老節點,複用節點,節省開銷。想深入理解的同學可以看一下筆者的另外一篇文章全面解析 vue3.0 diff演算法裡面有對key詳細說明。我們今天來看以下key正確用法,和錯誤用法。

1 錯誤用法

錯誤用法一:用index做key

function index(){
const list = [ { id:1 , name:'哈哈' } , { id:2, name:'嘿嘿' } ,{ id:3 , name:'嘻嘻' } ]
return <div>
<ul>
{ list.map((item,index)=><li key={index} >{ item.name }</li>) }
</ul>
</div>
}

這種加key的效能,實際和不加key效果差不多,每次還是從頭到尾diff。

錯誤用法二:用index拼接其他的欄位

function index(){
const list = [ { id:1 , name:'哈哈' } , { id:2, name:'嘿嘿' } ,{ id:3 , name:'嘻嘻' } ]
return <div>
<ul>
{ list.map((item,index)=><li key={index + item.name } >{ item.name }</li>) }
</ul>
</div>
}

如果有元素移動或者刪除,那麼就失去了一一對應關係,剩下的節點都不能有效複用。

2 正確用法

正確用法:用唯一id作為key

function index(){
const list = [ { id:1 , name:'哈哈' } , { id:2, name:'嘿嘿' } ,{ id:3 , name:'嘻嘻' } ]
return <div>
<ul>
{ list.map((item,index)=><li key={ item.id } >{ item.name }</li>) }
</ul>
</div>
}

用唯一的健id作為key,能夠做到有效複用元素節點。

③無狀態元件hooks-useMemo避免重複宣告。

對於無狀態元件,資料更新就等於函式上下文的重複執行。那麼函式裡面的變數,方法就會重新宣告。比如如下情況。

function Index(){
const [ number , setNumber ] = useState(0)
const handerClick1 = ()=>{
/* 一些操作 */
}
const handerClick2 = ()=>{
/* 一些操作 */
}
const handerClick3 = ()=>{
/* 一些操作 */
}
return <div>
<a onClick={ handerClick1 } >點我有驚喜1</a>
<a onClick={ handerClick2 } >點我有驚喜2</a>
<a onClick={ handerClick3 } >點我有驚喜3</a>
<button onClick={ ()=> setNumber(number+1) } > 點選 { number } </button>
</div>
}

每次點選button的時候,都會執行Index函式。handerClick1,handerClick2,handerClick3都會重新宣告。為了避免這個情況的發生,我們可以用useMemo做快取,我們可以改成如下。

function Index(){
const [ number , setNumber ] = useState(0)
const [ handerClick1 , handerClick2 ,handerClick3] = useMemo(()=>{
const fn1 = ()=>{
/* 一些操作 */
}
const fn2 = ()=>{
/* 一些操作 */
}
const fn3= ()=>{
/* 一些操作 */
}
return [fn1 , fn2 ,fn3]
},[]) /* 只有當資料裡面的依賴項,發生改變的時候,才會重新宣告函式。*/
return <div>
<a onClick={ handerClick1 } >點我有驚喜1</a>
<a onClick={ handerClick2 } >點我有驚喜2</a>
<a onClick={ handerClick3 } >點我有驚喜3</a>
<button onClick={ ()=> setNumber(number+1) } > 點選 { number } </button>
</div>
}

如下改變之後,handerClick1,handerClick2,handerClick3會被快取下來。

④懶載入 Suspense 和 lazy

Suspenselazy可以實現dynamic import懶載入效果,原理和上述的路由懶載入差不多。在React中的使用方法是在Suspense元件中使用<LazyComponent>元件。

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function demo () {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
)
}

LazyComponent是通過懶載入載入進來的,所以渲染頁面的時候可能會有延遲,但使用了Suspense之後,在載入狀態下,可以用<div>Loading...</div>作為loading效果。

Suspense可以包裹多個懶載入元件。

<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
<LazyComponent1 />
</Suspense>

六 多種方式避免重複渲染

避免重複渲染,是react效能優化的重要方向。如果想盡心盡力處理好react專案每一個細節,那麼就要從每一行程式碼開始,從每一元件開始。正所謂不積矽步無以至千里。

① 學會使用的批量更新

批量更新

這次講的批量更新的概念,實際主要是針對無狀態元件和hooksuseState,和class有狀態元件中的this.setState,兩種方法已經做了批量更新的處理。比如如下例子

一次更新中

class index extends React.Component{
constructor(prop){
super(prop)
this.state = {
a:1,
b:2,
c:3,
}
}
handerClick=()=>{
const { a,b,c } :any = this.state
this.setState({ a:a+1 })
this.setState({ b:b+1 })
this.setState({ c:c+1 })
}
render= () => <div onClick={this.handerClick} />
}

點選事件發生之後,會觸發三次setState,但是不會渲染三次,因為有一個批量更新batchUpdate批量更新的概念。三次setState最後被合成類似如下樣子

this.setState({
a:a+1 ,
b:b+1 ,
c:c+1
})

無狀態元件中

    const  [ a , setA ] = useState(1)
const [ b , setB ] = useState({})
const [ c , setC ] = useState(1)
const handerClick = () => {
setB( { ...b } )
setC( c+1 )
setA( a+1 )
}

批量更新失效

當我們針對上述兩種情況加以如下處理之後。

handerClick=()=>{
setTimeout(() => {
this.setState({ a:a+1 })
this.setState({ b:b+1 })
this.setState({ c:c+1 })
}, 0)
}
 const handerClick = () => {
Promise.resolve().then(()=>{
setB( { ...b } )
setC( c+1 )
setA( a+1 )
})
}

我們會發現,上述兩種情況 ,元件都更新渲染了三次 ,此時的批量更新失效了。這種情況在react-hooks中也普遍存在,這種情況甚至在hooks中更加明顯,因為我們都知道hooks中每個useState儲存了一個狀態,並不是讓class宣告元件中,可以通過this.state統一協調狀態,再一次非同步函式中,比如說一次ajax請求後,想通過多個useState改變狀態,會造成多次渲染頁面,為了解決這個問題,我們可以手動批量更新。

手動批量更新

react-dom中提供了unstable_batchedUpdates方法進行手動批量更新。這個api更契合react-hooks,我們可以這樣做。

 const handerClick = () => {
Promise.resolve().then(()=>{
unstable_batchedUpdates(()=>{
setB( { ...b } )
setC( c+1 )
setA( a+1 )
})
})
}

這樣三次更新,就會合併成一次。同樣達到了批量更新的效果。

② 合併state

class類元件(有狀態元件)

合併state這種,是一種我們在react專案開發中要養成的習慣。我看過有些同學的程式碼中可能會這麼寫(如下demo是模擬的情況,實際要比這複雜的多)。

class Index extends React.Component<any , any>{
state = {
loading:false /* 用來模擬loading效果 */,
list:[],
}
componentDidMount(){
/* 模擬一個非同步請求資料場景 */
this.setState({ loading : true }) /* 開啟loading效果 */
Promise.resolve().then(()=>{
const list = [ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ]
this.setState({ loading : false },()=>{
this.setState({
list:list.map(item=>({
...item,
name:item.name.toLocaleUpperCase()
}))
})
})
})
}
render(){
const { list } = this.state
return <div>{
list.map(item=><div key={item.id} >{ item.name }</div>)
}</div>
}
}

分別用兩次this.state第一次解除loading狀態,第二次格式化資料列表。這另兩次更新完全沒有必要,可以用一次setState更新完美解決。不這樣做的原因是,對於像demo這樣的簡單結構還好,對於複雜的結構,一次更新可能都是寶貴的,所以我們應該學會去合併state。將上述demo這樣修改。

this.setState({
loading : false,
list:list.map(item=>({
...item,
name:item.name.toLocaleUpperCase()
}))
})

函式元件(無狀態元件)

對於無狀態元件,我們可以通過一個useState儲存多個狀態,沒有必要每一個狀態都用一個useState

對於這樣的情況。

const [ a ,setA ] = useState(1)
const [ b ,setB ] = useState(2)

我們完全可以一個state搞定。

const [ numberState , setNumberState ] = useState({ a:1 , b :2})

但是要注意,如果我們的state已經成為useEffect,useCallback,useMemo依賴項,請慎用如上方法。

③ useMemo React.memo隔離單元

react正常的更新流,就像利劍一下,從父元件項子元件穿透,為了避免這些重複的更新渲染,shouldComponentUpdate,React.memoapi也應運而生。但是有的情況下,多餘的更新在所難免,比如如下這種情況。這種更新會由父元件 -> 子元件 傳遞下去。

function ChildrenComponent(){
console.log(2222)
return <div>hello,world</div>
}
function Index (){
const [ list ] = useState([ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ])
const [ number , setNumber ] = useState(0)
return <div>
<span>{ number }</span>
<button onClick={ ()=> setNumber(number + 1) } >點選</button>
<ul>
{
list.map(item=>{
console.log(1111)
return <li key={ item.id } >{ item.name }</li>
})
}
</ul>
<ChildrenComponent />
</div>
}

效果

針對這一現象,我們可以通過使用useMemo進行隔離,形成獨立的渲染單元,每次更新上一個狀態會被快取,迴圈不會再執行,子元件也不會再次被渲染,我們可以這麼做。

function Index (){
const [ list ] = useState([ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ])
const [ number , setNumber ] = useState(0)
return <div>
<span>{ number }</span>
<button onClick={ ()=> setNumber(number + 1) } >點選</button>
<ul>
{
useMemo(()=>(list.map(item=>{
console.log(1111)
return <li key={ item.id } >{ item.name }</li>
})),[ list ])
}
</ul>
{ useMemo(()=> <ChildrenComponent />,[]) }
</div>
}

有狀態元件

class宣告的元件中,沒有像useMemoAPI,但是也並不等於束手無策,我們可以通過react.memo來阻攔來自元件本身的更新。我們可以寫一個元件,來控制react元件更新的方向。我們通過一個<NotUpdate>元件來阻斷更新流。

/* 控制更新 ,第二個引數可以作為元件更新的依賴 , 這裡設定為 ()=> true 只渲染一次 */
const NotUpdate = React.memo(({ children }:any)=> typeof children === 'function' ? children() : children ,()=>true)

class Index extends React.Component<any,any>{
constructor(prop){
super(prop)
this.state = {
list: [ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ],
number:0,
}
}
handerClick = ()=>{
this.setState({ number:this.state.number + 1 })
}
render(){
const { list }:any = this.state
return <div>
<button onClick={ this.handerClick } >點選</button>
<NotUpdate>
{()=>(<ul>
{
list.map(item=>{
console.log(1111)
return <li key={ item.id } >{ item.name }</li>
})
}
</ul>)}
</NotUpdate>
<NotUpdate>
<ChildrenComponent />
</NotUpdate>

</div>
}
}

const NotUpdate = React.memo(({ children }:any)=> typeof children === 'function' ? children() : children ,()=>true)

沒錯,用的就是React.memo,生成了阻斷更新的隔離單元,如果我們想要控制更新,可以對React.memo第二個引數入手,demo專案中完全阻斷的更新。

④ ‘取締’state,學會使用快取。

這裡的取締state,並完全不使用state來管理資料,而是善於使用state,知道什麼時候使用,怎麼使用。react並不像vue那樣響應式資料流。在vue中 有專門的dep做依賴收集,可以自動收集字串模版的依賴項,只要沒有引用的data資料, 通過this.aaa = bbb,在vue中是不會更新渲染的。因為aaadep沒有收集渲染watcher依賴項。在react中,我們觸發this.setState或者useState,只會關心兩次state值是否相同,來觸發渲染,根本不會在乎jsx語法中是否真正的引入了正確的值。

沒有更新作用的state

有狀態元件中

class Demo extends React.Component{
state={ text:111 }
componentDidMount(){
const { a } = this.props
/* 我們只是希望在初始化,用text記錄 props中 a 的值 */
this.setState({
text:a
})
}
render(){
/* 沒有引入text */
return <div>{'hello,world'}</div>
}
}

如上例子中,render函式中並沒有引入text,我們只是希望在初始化的時候,用text記錄propsa的值。我們卻用setState觸發了一次無用的更新。無狀態元件中情況也一樣存在,具體如下。

無狀態元件中

function Demo ({ a }){
const [text , setText] = useState(111)
useEffect(()=>{
setText(a)
},[])
return <div>
{'hello,world'}
</div>
}

改為快取

有狀態元件中

class宣告元件中,我們可以直接把資料繫結給this上,來作為資料快取。

class Demo extends React.Component{
text = 111
componentDidMount(){
const { a } = this.props
/* 資料直接儲存在text上 */
this.text = a
}
render(){
/* 沒有引入text */
return <div>{'hello,world'}</div>
}
}

無狀態元件中

在無狀態元件中, 我們不能往問this,但是我們可以用useRef來解決問題。

function Demo ({ a }){
const text = useRef(111)
useEffect(()=>{
text.current = a
},[])
return <div>
{'hello,world'}
</div>
}

⑤ useCallback回撥

useCallback的真正目的還是在於快取了每次渲染時inline callback的例項,這樣方便配合上子元件的shouldComponentUpdate或者React.memo起到減少不必要的渲染的作用。對子元件的渲染限定來源與,對子元件props比較,但是如果對父元件的callback做比較,無狀態元件每次渲染執行,都會形成新的callback,是無法比較,所以需要對callback做一個memoize記憶功能,我們可以理解為useCallback就是callback加了一個memoize。我們接著往下看