天天看點

react項目8點優化

本文篇幅較長,将從 編譯階段 -> 路由階段 -> 渲染階段 -> 細節優化 -> 狀态管理 -> 海量資料源,長清單渲染

方向分别加以探讨。

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

1 真實項目中痛點

當我們用​

​create-react-app​

​​或者​

​webpack​

​​建構​

​react​

​工程的時候,有沒有想過一個問題,我們的配置能否讓我們的項目更快的建構速度,更小的項目體積,更簡潔清晰的項目結構。随着我們的項目越做越大,項目依賴越來越多,項目結構越來越來複雜,項目體積就會越來越大,建構時間越來越長,久而久之就會成了一個又大又重的項目,是以說我們要學會适當的為項目‘減負’,讓項目不能輸在起跑線上。

2 一個老項目

拿我們之前接觸過的一個​

​react​

​​老項目為例。我們沒有用​

​dva​

​​,​

​umi​

​​快速搭建react,而是用​

​react​

​​老版本腳手架建構的,這對這種老的​

​react​

​項目,上述的問題都會存在,下面讓我們一起來看看。

react項目8點優化

我們首先看一下項目結構。

再看看建構時間。

為了友善大家看建構時間,我簡單寫了一個​

​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)
    }
}      

建構時間如下:

react項目8點優化

打包後的體積:

react項目8點優化

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 删除備援代碼

⑤按需加載,按需引入。

優化後項目結構

react項目8點優化

優化建構時間如下:

react項目8點優化

一次 ​

​compilation​

​ 時間 從23秒優化到了4.89秒

優化打包後的體積:

react項目8點優化

由此可見,如果我們的​

​react​

​是自己徒手搭建的,一些優化技巧顯得格外重要。

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

我們在做​

​react​

​​項目的時候,會用到​

​antd​

​​之類的ui庫,值得思考的一件事是,如果我們隻是用到了​

​antd​

​​中的個别元件,比如​

​<Button /> ​

​​,就要把整個樣式庫引進來,打包就會發現,體積因為引入了整個樣式大了很多。我們可以通過​

​.babelrc​

​實作按需引入。

瘦身前

react項目8點優化

​.babelrc​

​​ 增加對 ​

​antd​

​ 樣式按需引入。

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

瘦身後

react項目8點優化

總結

如果想要優化​

​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項目8點優化

這樣一來,我們既做到了路由的懶加載,又彌補了​

​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>
    }
}      

效果

react項目8點優化

當我們在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>
    }
}      

效果

react項目8點優化

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

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

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

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

react項目8點優化

頁面有三個展示區域分别,做了三次請求,觸發了三次​

​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>
}      

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

react項目8點優化

總結

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

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>
    }
}      

效果

react項目8點優化

我們點選按鈕,發現 ​

​<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​

​ 避免重複聲明。

對于無狀态元件,資料更新就等于函數上下文的重複執行。那麼函數裡面的變量,方法就會重新聲明。比如如下情況。

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​

​也應運而生。但是有的情況下,多餘的更新在所難免,比如如下這種情況。這種更新會由父元件 -> 子元件 傳遞下去。

react項目8點優化
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>
}      

效果

react項目8點優化

針對這一現象,我們可以通過使用​

​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項目8點優化

沒錯,用的就是 ​

​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​

​頁面為例子:

react項目8點優化

如上 紅色區域,是基本不變的資料,多個頁面可能需要的資料,我們可以統一放在狀态管理中,藍色區域是随時更新的資料,直接請求接口就好。

總結

不變的資料,多個頁面可能需要的資料,放在狀态管理中,對于時常變化的資料,我們可以直接請求接口

八 海量資料優化-時間分片,虛拟清單

時間分片

時間分片的概念,就是一次性渲染大量資料,初始化的時候會出現卡頓等現象。我們必須要明白的一個道理,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 個資料的清單,看一下需要多長時間。

react項目8點優化

我們看到 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>
    }
}      

效果

react項目8點優化

​setTimeout​

​​ 可以用 ​

​window.requestAnimationFrame()​

​​ 代替,會有更好的渲染效果。我們​

​demo​

​使用清單做的,實際對于清單來說,最佳方案是虛拟清單,而時間分片,更适合熱力圖,地圖點位比較多的情況。

虛拟清單

筆者在最近在做小程式商城項目,有長清單的情況, 可是肯定說 虛拟清單 是解決長清單渲染的最佳方案。無論是小程式,或者是​

​h5​

​​ ,随着 ​

​dom​

​元素越來越多,頁面會越來越卡頓,這種情況在小程式更加明顯 。稍後,筆者講專門寫一篇小程式長清單渲染緩存方案的文章,感興趣的同學可以關注一下筆者。

虛拟清單是按需顯示的一種技術,可以根據使用者的滾動,不必渲染所有清單項,而隻是渲染可視區域内的一部分清單元素的技術。正常的虛拟清單分為 渲染區,緩沖區 ,虛拟清單區。

如下圖所示。

react項目8點優化

為了防止大量​

​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>
    }
}      

效果

react項目8點優化

具體思路

① 初始化計算容器的高度。截取初始化清單長度。這裡我們需要div占位,撐起滾動條。

② 通過監聽滾動容器的 ​

​onScroll​

​​事件,根據 ​

​scrollTop​

​ 來計算渲染區域向上偏移量, 我們要注意的是,當我們向下滑動的時候,為了渲染區域,能在可視區域内,可視區域要向上的滾動; 我們向上滑動的時候,可視區域要向下的滾動。

③ 通過重新計算的 ​

​end​

​​ 和 ​

​start​

​ 來重新渲染清單。

性能優化點

① 對于移動視圖區域,我們可以用 ​

​transform​

​​ 來代替改變 ​

​top​

​值。

② 虛拟清單實際情況,是有 ​

​start​

​​ 或者 ​

​end​

​​ 改變的時候,在重新渲染清單,是以我們可以用之前 ​

​shouldComponentUpdate​

​ 來調優,避免重複渲染。

總結

​react​

​​ 性能優化是一個攻堅戰,需要付出很多努力,将我們的項目做的更完美,希望看完這篇文章的朋友們能找到​

​react​

​​優化的方向,讓我們的​

​react​

​項目飛起來。

react項目8點優化

react項目8點優化

​文章就分享到這,歡迎關注“前端大神之路”​

react項目8點優化

​​