react性能優化

剛開始寫react可能只是寫出來完成業務就完了,後期審查代碼發現可能不少地方其實均可以優化,以前可能有些地方似是而非,在此小結一下。html

一些概念

Virtual DOM

react引入了一個叫作虛擬DOM的概念,安插在JavaScript邏輯和實際的DOM之間。這一律念提升了Web性能。在UI渲染過程當中,React經過在虛擬DOM中的微操做來實對現實際DOM的局部更新。react

在Web開發中,咱們總須要將變化的數據實時反應到UI上,這時就須要對DOM進行操做。而複雜或頻繁的DOM操做一般是性能瓶頸產生的緣由,React爲此引入了虛擬DOM(Virtual DOM)的機制:在瀏覽器端用Javascript實現了一套DOM API。基於React進行開發時全部的DOM構造都是經過虛擬DOM進行,每當數據變化時,React都會從新構建整個DOM樹,而後React將當前整個DOM樹和上一次的DOM樹進行對比,獲得DOM結構的區別,而後僅僅將須要變化的部分進行實際的瀏覽器DOM更新。並且React可以批處理虛擬DOM的刷新,在一個事件循環(Event Loop)內的兩次數據變化會被合併,例如你連續的先將節點內容從A變成B,而後又從B變成A,React會認爲UI不發生任何變化,而若是經過手動控制,這種邏輯一般是極其複雜的。儘管每一次都須要構造完整的虛擬DOM樹,可是由於虛擬DOM是內存數據,性能是極高的,而對實際DOM進行操做的僅僅是Diff部分,於是能達到提升性能的目的。這樣,在保證性能的同時,開發者將再也不須要關注某個數據的變化如何更新到一個或多個具體的DOM元素,而只須要關心在任意一個數據狀態下,整個界面是如何Render的。chrome

render

react的組件渲染分爲初始化渲染和更新渲染。redux

  • 初始化渲染
    • 在初始化渲染的時候會調用根組件下的全部組件的render方法進行渲染
  • 更新渲染
    • 當咱們要更新某個子組件的時候,咱們期待的是隻變化須要變化的組件,其餘組件保持不變。
    • 可是,react的默認作法是調用全部組件的render,再對生成的虛擬DOM進行對比,如不變則不進行更新。這樣的render和虛擬DOM的對比明顯是在浪費

Chrome Performance

在開發模式下, 在支持的瀏覽器內使用性能工具能夠直觀的瞭解組件什麼時候掛載,更新和卸載小程序

  • 打開Chrome開發工具Performance 標籤頁點擊Record
  • 執行你想要分析的動做。不要記錄超過20s,否則Chrome可能會掛起。
  • 中止記錄。
  • React事件將會被歸類在 User Timing標籤下。
    1.png

優化

bind函數

綁定this的方式:通常有下面幾種方式api

  • constructor中綁定
constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); //構造函數中綁定
}
//而後能夠
<p onClick={this.handleClick}>
  • 使用時綁定
<p onClick={this.handleClick.bind(this)}>
  • 箭頭函數
<p onClick={() => { this.handleClick() }}>
  • 哪一個好呢
    • 答案是第一種方式。
    • 由於第一種,構造函數每一次渲染的時候只會執行 一遍;
    • 而第二種方法,在每次render()的時候都會從新執行一遍函數;
    • 第三種方法的話,每一次render()的時候,都會生成一個新的箭頭函數數組

      shouldComponentUpdate

      shouldComponentUpdate是決定react組件何時可以不從新渲染的函數,返回true時更新,false時不更新。默認返回true,即每次從新渲染,所以咱們能夠重寫個函數從而達到"個性化定製更新"的效果。瀏覽器

  • 栗子
class Title extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    console.log('title render')
    return (
      <div>{this.props.title}</div>
    )
  }
}

class PureCom extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: 'pure',
      num: 0
    }
    this.add = this.add.bind(this);
  }
  add() {
    let { num } = this.state;
    num++;
    this.setState({ num })
  }
  render() {
    console.log('pure render')
    return (
      <div>
        <Title title={this.state.title} />
        <p>{this.state.num}</p>
        <button onClick={this.add}>add</button>
      </div>
    )
  }
}
  • 如今每次點擊add按鈕,父組件state的num都會+1,而title是一直不變的,經過console咱們卻發現,Title組件也在一直render,這就是由於shouldComponentUpdate默認返回true的,也就是父組件更新以後,子組件也會更新。
  • 而後子組件是不必更新的,因此咱們重寫下shouldComponentUpdate方法
class Title extends React.Component {
  constructor(props) {
    super(props)
  }
  shouldComponentUpdate(nextProps, nextState) {
    if (nextProps.title != this.props.title) {
      return true     //只有title變化時才更新
    } else {
      return false
    }
  }
  render() {
    console.log('title render')
    return (
      <div>{this.props.title}</div>
    )
  }
}
  • 如今就對了,點擊父組件的add按鈕並無觸發Title組件的更新。數據結構

    PureComponent

    相似上面的狀況其實咱們常常遇到,所以react提供了PureComponent來解決相似的問題,可讓咱們少寫許多的shouldComponentUpdate。less

class Title extends React.PureComponent {
  constructor(props) {
    super(props)
  }
  render() {
    console.log('title render')
    return (
      <div>{this.props.title}</div>
    )
  }
}
  • 用了PureComponent以後做用和以前是相同的。
  • 原理:當組件更新時,若是組件的 props 和 state 都沒發生改變, render 方法就不會觸發,省去 Virtual DOM 的生成和比對過程,達到提高性能的目的。具體就是 React 自動幫咱們作了一層淺比較
if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate = !shallowEqual(prevProps, nextProps)
  || !shallowEqual(inst.state, nextState);
}

突變的數據

大多數狀況PureComponent均可以解決,可是以前也說過,他是「淺比較」,若是遇到數據結構比較複雜,他是沒法識別的。

class PureCom extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      items: [1, 2, 3],
      title: 'pure',
    }
    this.add = this.add.bind(this);
  }
  add() {
    let { items } = this.state;
    items.push(23);
    this.setState({ items })
  }
  render() {
    console.log('pure render')
    return (
      <div>
        <Title title={this.state.title} />
        <ul>
          {this.state.items.map((e, i) => {
            return <li key={i}>{e}</li>
          })}
        </ul>
        <button onClick={this.add}>add</button>
      </div>
    )
  }
}
  • 點擊add,你會發現沒有任何反應,爲何呢?由於你setState的items實際上是和state裏面的items指向相同引用。原理和下面同樣。
let a={val:1};
let b=a;
b.val=2;
console.log(a)//{val:2}
console.log(b)//{val:2}
  • 解決辦法
    • 1.深拷貝
    add() {
    let items =JSON.parse(JSON.stringify(this.state.items));//黑科技
    //或者let items=deepCopy(this.state.items);
    items.push(23);
    this.setState({ items })
    }
    • 2.數組使用concat,對象使用Object.assign()
    add() {
    let { items } = this.state;
    items=items.concat(23)  //此時的items是一個新數組
    this.setState({ items })
    }
    • 3.使用不可變數據Immutable.js
    add() {
    let { items } = this.state;
    items = update(items, { $push: [23] });
    this.setState({ items })
    }
    • 其中深拷貝若是數據比較複雜消耗會比較大
    • concat,Object.assign用起來很快捷
    • 若是你數據比較複雜,可能Immutable會是最好的選擇。官方推薦::seamless-immutable 和immutability-helper。

      redux

      我的感受redux的渲染機制也是和PureComponent相似的,都是淺比較,所以上面的3種解決辦法也適用於redux.

      16.3+ new API

      一些生命週期會被刪除,將在17.0:刪除componentWillMount,componentWillReceiveProps和componentWillUpdate。

  • 一些變化
    • componentWillMount => componentDidMount
    • componentWillReceiveProps => getDerivedStateFromProps
    • componentWillUpdate => getSnapshotBeforeUpdate
  • static getDerivedStateFromProps
//代替componentWillReceiveProps,由於是靜態方法,不能訪問到 this,避免了一些可能有反作用的邏輯,好比訪問 DOM 等等
//會在第一次掛載和重繪的時候都會調用到,所以你基本不用在constructor里根據傳入的props來setState
static getDerivedStateFromProps(nextProps, prevState) {
  console.log(nextProps, prevState)
  if (prevState.music !== nextProps.music) {
    return {
      music: nextProps.music,
      music_file: music_file,
      index:prevState.index+1
    };
   //document.getElementById('PLAYER').load();                   //這裏不對,應該放在getSnapshotBeforeUpdate 和 componentDidUpdate
  }
  return null;
}


getSnapshotBeforeUpdate(prevProps, prevState) {
    if (this.state.music != prevState.music) {                   //進行aduio的重載
        return true
    }
    return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {       
    if (snapshot !== null) {
        document.getElementById('PLAYER').load();             //重載
    }
}
  • getSnapshotBeforeUpdate
//新的getSnapshotBeforeUpdate生命週期在更新以前被調用(例如,在DOM被更新以前)。今生命週期的返回值將做爲第三個參數傳遞給componentDidUpdate。 (這個生命週期不是常常須要的,但能夠用於在恢復期間手動保存滾動位置的狀況。)

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {              //snapshot
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}
  • 使用componentDidMount 代替 componentWillMount
//有一個常見的錯誤觀念認爲,在componentWillMount中提取能夠避免第一個空的渲染。在實踐中,這歷來都不是真的,由於React老是在componentWillMount以後當即執行渲染。若是數據在componentWillMount觸發的時間內不可用,則不管你在哪裏提取數據,第一個渲染仍將顯示加載狀態。
// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._asyncRequest = asyncLoadData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({ externalData });
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

其餘

  • props儘可能只傳須要的數據,避免多餘的更新
  • 組件儘可能解耦,好比一個input+list組建,能夠將list分紅一個PureComponent,只在list數據變化是更新
  • 若是組件有複用,key值很是重要。所以key的優化,若是有惟一id,儘可能不使用循環獲得的index
  • 暫時這些

最後

你們好,這裏是「 TaoLand 」,這個博客主要用於記錄一個菜鳥程序猿的Growth之路。這也是本身第一次作博客,但願和你們多多交流,一塊兒成長!文章將會在下列地址同步更新……
我的博客:www.yangyuetao.cn
小程序:TaoLand

相關文章
相關標籤/搜索