本文篇幅較長,将從 編譯階段 -> 路由階段 -> 渲染階段 -> 細節優化 -> 狀态管理 -> 海量資料源,長清單渲染
方向分别加以探讨。
一 不能輸在起跑線上,優化babel配置,webpack配置為項
1 真實項目中痛點
當我們用
create-react-app
或者
webpack
建構
react
工程的時候,有沒有想過一個問題,我們的配置能否讓我們的項目更快的建構速度,更小的項目體積,更簡潔清晰的項目結構。随着我們的項目越做越大,項目依賴越來越多,項目結構越來越來複雜,項目體積就會越來越大,建構時間越來越長,久而久之就會成了一個又大又重的項目,是以說我們要學會适當的為項目‘減負’,讓項目不能輸在起跑線上。
2 一個老項目
拿我們之前接觸過的一個
react
老項目為例。我們沒有用
dva
,
umi
快速搭建react,而是用
react
老版本腳手架建構的,這對這種老的
react
項目,上述的問題都會存在,下面讓我們一起來看看。
我們首先看一下項目結構。
再看看建構時間。
為了友善大家看建構時間,我簡單寫了一個
webpack,plugin
ConsolePlugin
,記錄了
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
生命周期函數,加載真實的元件,并渲染元件,我們還可以寫針對路由懶加載狀态定制屬于自己的路由監聽器
beforeRouterComponentLoad
和
afterRouterComponentDidLoaded
,類似
vue
中
watch $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有很好的d
diff
算法去調協相同的節點,但是比如長清單等情況,循環在所難免。
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
的資料驅動,依賴于
state
和
props
的改變,改變
state
必然會對元件
render
函數調用,如果父元件中的子元件過于複雜,一個自元件的
state
改變,就會牽一發動全身,必然影響性能,是以如果把很多依賴請求的元件抽離出來,可以直接減少渲染次數。
2 可以優化元件自身性能,無論從
class
聲明的有狀态元件還是
fun
聲明的無狀态,都有一套自身優化機制,無論是用
shouldupdate
還是用
hooks
中
useMemo
useCallback
,都可以根據自身情況,定制符合場景的渲條 件,使得依賴資料請求元件形成自己一個小的,适合自身的渲染環境。
3 能夠和
redux
,以及
redux
衍生出來
redux-action
,
dva
,更加契合的工作,用
connect
包裹的元件,就能通過制定好的契約,根據所需求的資料更新,而更新自身,而把這種模式用在這種小的,需要資料驅動的元件上,就會起到物盡其用的效果。
四 shouldComponentUpdate ,PureComponent 和 React.memo ,immetable.js 助力性能調優
在這裡我們拿
immetable.js
為例,講最傳統的限制更新方法,第六部分将要将一些避免重新渲染的細節。
1 PureComponent 和 React.memo
React.PureComponent
與
React.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.memo
和
PureComponent
功能類似 ,
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
}
這個的意思就是 僅當
state
中
data1
發生改變的時候,重新更新元件。 控制prop屬性
shouldComponentUpdate(nextProps, nextState) {
/* 當 props 中 data2發生改變的時候,重新更新元件 */
return nextProps.data2 !== this.props.data2
}
這個的意思就是 僅當
props
中
data2
發生改變的時候,重新更新元件。
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
無論是
react
和
vue
,正确使用
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
避免重複聲明。
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
Suspense
和
lazy
可以實作
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
項目每一個細節,那麼就要從每一行代碼開始,從每一元件開始。正所謂不積矽步無以至千裡。
① 學會使用的批量更新
批量更新
這次講的批量更新的概念,實際主要是針對無狀态元件和
hooks
中
useState
,和
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.memo
等
api
也應運而生。但是有的情況下,多餘的更新在所難免,比如如下這種情況。這種更新會由父元件 -> 子元件 傳遞下去。
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
聲明的元件中,沒有像
useMemo
的
API
,但是也并不等于束手無策,我們可以通過
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
中是不會更新渲染的。因為
aaa
的
dep
沒有收集渲染
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
記錄
props
中
a
的值。我們卻用
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
。我們接着往下看????????????。
function demo (){
const [ number , setNumber ] = useState(0)
return <div>
<DemoComponent handerChange={ ()=>{ setNumber(number+1) } } />
</div>
}
或着
function demo (){
const [ number , setNumber ] = useState(0)
const handerChange = ()=>{
setNumber(number+1)
}
return <div>
<DemoComponent handerChange={ handerChange } />
</div>
}
無論是上述那種方式,
pureComponent
和
react.memo
通過淺比較方式,隻能判斷每次更新都是新的
callback
,然後觸發渲染更新。
useCallback
給加了一個記憶功能,告訴我們子元件,兩次是相同的
callback
無需重新更新頁面。至于什麼時候
callback
更改,就要取決于
useCallback
第二個參數。好的,将上述
demo
我們用
useCallback
重寫。
function demo (){
const [ number , setNumber ] = useState(0)
const handerChange = useCallback( ()=>{
setNumber(number+1)
},[])
return <div>
<DemoComponent handerChange={ handerChange } />
</div>
}
這樣
pureComponent
和
react.memo
可以直接判斷是
callback
沒有改變,防止了不必要渲染。
七 中規中矩的使用狀态管理
無論我們使用的是
redux
還是說
redux
衍生出來的
dva
,
redux-saga
等,或者是
mobx
,都要遵循一定'使用規則',首先讓我想到的是,什麼時候用狀态管理,怎麼合理的應用狀态管理,接下來我們來分析一下。
什麼時候使用狀态管理
要問我什麼時候适合使用狀态狀态管理。我一定會這麼分析,首先狀态管理是為了解決什麼問題,狀态管理能夠解決的問題主要分為兩個方面,一 就是解決跨層級元件通信問題 。二 就是對一些全局公共狀态的緩存。
我們那redux系列的狀态管理為例子。
我見過又同學這麼寫的
濫用狀态管理
/* 和 store下面text子產品的list清單,建立起依賴關系,list更新,元件重新渲染 */
@connect((store)=>({ list:store.text.list }))
class Text extends React.Component{
constructor(prop){
super(prop)
}
componentDidMount(){
/* 初始化請求資料 */
this.getList()
}
getList=()=>{
const { dispatch } = this.props
/* 擷取資料 */
dispatch({ type:'text/getDataList' })
}
render(){
const { list } = this.props
return <div>
{
list.map(item=><div key={ item.id } >
{ /* 做一些渲染頁面的操作.... */ }
</div>)
}
<button onClick={ ()=>this.getList() } >重新擷取清單</button>
</div>
}
}
這樣頁面請求資料,到資料更新,全部在目前元件發生,這個寫法我不推薦,此時的資料走了一遍狀态管理,最終還是回到了元件本身,顯得很雞肋,并沒有發揮什麼作用。在性能優化上到不如直接在元件内部請求資料。
不會合理使用狀态管理
還有的同學可能這麼寫。
class Text extends React.Component{
constructor(prop){
super(prop)
this.state={
list:[],
}
}
async componentDidMount(){
const { data , code } = await getList()
if(code === 200){
/* 擷取的資料有可能是不常變的,多個頁面需要的資料 */
this.setState({
list:data
})
}
}
render(){
const { list } = this.state
return <div>
{ /* 下拉框 */ }
<select>
{
list.map(item=><option key={ item.id } >{ item.name }</option>)
}
</select>
</div>
}
}
對于不變的資料,多個頁面或元件需要的資料,為了避免重複請求,我們可以将資料放在狀态管理裡面。
如何使用狀态管理
分析結構
我們要學會分析頁面,那些資料是不變的,那些是随時變動的,用以下
demo
頁面為例子:
如上 紅色區域,是基本不變的資料,多個頁面可能需要的資料,我們可以統一放在狀态管理中,藍色區域是随時更新的資料,直接請求接口就好。
總結
不變的資料,多個頁面可能需要的資料,放在狀态管理中,對于時常變化的資料,我們可以直接請求接口
八 海量資料優化-時間分片,虛拟清單
時間分片
時間分片的概念,就是一次性渲染大量資料,初始化的時候會出現卡頓等現象。我們必須要明白的一個道理,js執行永遠要比dom渲染快的多。 ,是以對于大量的資料,一次性渲染,容易造成卡頓,卡死的情況。我們先來看一下例子
class Index extends React.Component<any,any>{
state={
list: []
}
handerClick=()=>{
let starTime = new Date().getTime()
this.setState({
list: new Array(40000).fill(0)
},()=>{
const end = new Date().getTime()
console.log( (end - starTime ) / 1000 + '秒')
})
}
render(){
const { list } = this.state
console.log(list)
return <div>
<button onClick={ this.handerClick } >點選</button>
{
list.map((item,index)=><li className="list" key={index} >
{ item + '' + index } Item
</li>)
}
</div>
}
}
我們模拟一次性渲染 40000 個資料的清單,看一下需要多長時間。
我們看到 40000 個 簡單清單渲染了,将近5秒的時間。為了解決一次性加載大量資料的問題。我們引出了時間分片的概念,就是用
setTimeout
把任務分割,分成若幹次來渲染。一共40000個資料,我們可以每次渲染100個, 分次400渲染。
class Index extends React.Component<any,any>{
state={
list: []
}
handerClick=()=>{
this.sliceTime(new Array(40000).fill(0), 0)
}
sliceTime=(list,times)=>{
if(times === 400) return
setTimeout(() => {
const newList = list.slice( times , (times + 1) * 100 ) /* 每次截取 100 個 */
this.setState({
list: this.state.list.concat(newList)
})
this.sliceTime( list ,times + 1 )
}, 0)
}
render(){
const { list } = this.state
return <div>
<button onClick={ this.handerClick } >點選</button>
{
list.map((item,index)=><li className="list" key={index} >
{ item + '' + index } Item
</li>)
}
</div>
}
}
效果
setTimeout
可以用
window.requestAnimationFrame()
代替,會有更好的渲染效果。我們
demo
使用清單做的,實際對于清單來說,最佳方案是虛拟清單,而時間分片,更适合熱力圖,地圖點位比較多的情況。
虛拟清單
筆者在最近在做小程式商城項目,有長清單的情況, 可是肯定說 虛拟清單 是解決長清單渲染的最佳方案。無論是小程式,或者是
h5
,随着
dom
元素越來越多,頁面會越來越卡頓,這種情況在小程式更加明顯 。稍後,筆者講專門寫一篇小程式長清單渲染緩存方案的文章,感興趣的同學可以關注一下筆者。
虛拟清單是按需顯示的一種技術,可以根據使用者的滾動,不必渲染所有清單項,而隻是渲染可視區域内的一部分清單元素的技術。正常的虛拟清單分為 渲染區,緩沖區 ,虛拟清單區。
如下圖所示。
為了防止大量
dom
存在影響性能,我們隻對,渲染區和緩沖區的資料做渲染,,虛拟清單區 沒有真實的dom存在。緩沖區的作用就是防止快速下滑或者上滑過程中,會有空白的現象。
react-tiny-virtual-list
react-tiny-virtual-list 是一個較為輕量的實作虛拟清單的元件。這是官方文檔。
import React from 'react';
import {render} from 'react-dom';
import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E', 'F', ...];
render(
<VirtualList
width='100%'
height={600}
itemCount={data.length}
itemSize={50} // Also supports variable heights (array or function getter)
renderItem={({index, style}) =>
<div key={index} style={style}> // The style property contains the item's absolute position
Letter: {data[index]}, Row: #{index}
</div>
}
/>,
document.getElementById('root')
);
手寫一個react虛拟清單
let num = 0
class Index extends React.Component<any, any>{
state = {
list: new Array(9999).fill(0).map(() =>{
num++
return num
}),
scorllBoxHeight: 500, /* 容器高度(初始化高度) */
renderList: [], /* 渲染清單 */
itemHeight: 60, /* 每一個清單高度 */
bufferCount: 8, /* 緩沖個數 上下四個 */
renderCount: 0, /* 渲染數量 */
start: 0, /* 起始索引 */
end: 0 /* 終止索引 */
}
listBox: any = null
scrollBox : any = null
scrollContent:any = null
componentDidMount() {
const { itemHeight, bufferCount } = this.state
/* 計算容器高度 */
const scorllBoxHeight = this.listBox.offsetHeight
const renderCount = Math.ceil(scorllBoxHeight / itemHeight) + bufferCount
const end = renderCount + 1
this.setState({
scorllBoxHeight,
end,
renderCount,
})
}
/* 處理滾動效果 */
handerScroll=()=>{
const { scrollTop } :any = this.scrollBox
const { itemHeight , renderCount } = this.state
const currentOffset = scrollTop - (scrollTop % itemHeight)
/* translate3d 開啟css cpu 加速 */
this.scrollContent.style.transform = `translate3d(0, ${currentOffset}px, 0)`
const start = Math.floor(scrollTop / itemHeight)
const end = Math.floor(scrollTop / itemHeight + renderCount + 1)
this.setState({
start,
end,
})
}
/* 性能優化:隻有在清單start 和 end 改變的時候在渲染清單 */
shouldComponentUpdate(_nextProps, _nextState){
const { start , end } = _nextState
return start !== this.state.start || end !==this.state.end
}
/* 處理滾動效果 */
render() {
console.log(1111)
const { list, scorllBoxHeight, itemHeight ,start ,end } = this.state
const renderList = list.slice(start,end)
return <div className="list_box"
ref={(node) => this.listBox = node}
>
<div
style={{ height: scorllBoxHeight, overflow: 'scroll', position: 'relative' }}
ref={ (node)=> this.scrollBox = node }
onScroll={ this.handerScroll }
>
{ /* 占位作用 */}
<div style={{ height: `${list.length * itemHeight}px`, position: 'absolute', left: 0, top: 0, right: 0 }} />
{ /* 顯然區 */ }
<div ref={(node) => this.scrollContent = node} style={{ position: 'relative', left: 0, top: 0, right: 0 }} >
{
renderList.map((item, index) => (
<div className="list" key={index} >
{item + '' } Item
</div>
))
}
</div>
</div>
</div>
}
}
效果
具體思路
① 初始化計算容器的高度。截取初始化清單長度。這裡我們需要div占位,撐起滾動條。
② 通過監聽滾動容器的
onScroll
事件,根據
scrollTop
來計算渲染區域向上偏移量, 我們要注意的是,當我們向下滑動的時候,為了渲染區域,能在可視區域内,可視區域要向上的滾動; 我們向上滑動的時候,可視區域要向下的滾動。
③ 通過重新計算的
end
和
start
來重新渲染清單。
性能優化點
① 對于移動視圖區域,我們可以用
transform
來代替改變
top
值。
② 虛拟清單實際情況,是有
start
或者
end
改變的時候,在重新渲染清單,是以我們可以用之前
shouldComponentUpdate
來調優,避免重複渲染。
總結
react
性能優化是一個攻堅戰,需要付出很多努力,将我們的項目做的更完美,希望看完這篇文章的朋友們能找到
react
優化的方向,讓我們的
react
項目飛起來。
文章就分享到這,歡迎關注“前端大神之路”