理解React:Fiber架構和新舊生命週期

➣ React Fiber原理


React架構

  • 1)Virtual DOM 層,描述頁面長什麼樣
  • 2)Reconciler 層,負責調用組件生命週期方法,進行Diff運算等
  • 3)Renderer 層,根據不同的平臺,渲染出相應的頁面,如 ReactDOM 和 ReactNative

React15遺留問題

  • 1)瀏覽器的整體渲染是多線程的,包括GUI渲染線程、JS引擎線程、事件觸發線程、定時觸發器線程和異步http請求線程。頁面繪製和JS運算是互斥的線程,兩者不能同時進行。
  • 2)React15使用JS的函數調用棧(Stack Reconciler)遞歸渲染界面,因此在處理DOM元素過多的複雜頁面的頻繁更新時,大量同步進行的任務(樹diff和頁面render)會導致界面更新阻塞、事件響應延遲、動畫卡頓等,因此React團隊在16版本重寫了React Reconciler架構。

React16問題解決

  • 1)Fiber Reconciler架構可以允許同步阻塞的任務拆分成多個小任務,每個任務佔用一小段時間片,任務執行完成後判斷有無空閒時間,有則繼續執行下一個任務,否則將控制權交由瀏覽器以讓瀏覽器去處理更高優先級的任務,等下次拿到時間片後,其它子任務繼續執行。整個流程類似CPU調度邏輯,底層是使用了瀏覽器APIrequestIdleCallback
  • 2)爲了實現整個Diff和Render的流程可中斷和恢復,單純的VirtualDom Tree不再滿足需求,React16引入了採用單鏈表結構的Fiber樹,如下圖所示。
  • 3)FiberReconciler架構將更新流程劃分成了兩個階段:1.diff(由多個diff任務組成,任務時間片消耗完後被可被中斷,中斷後由requestIdleCallback再次喚醒) => 2.commit(diff完畢後拿到fiber tree更新結果觸發DOM渲染,不可被中斷)。左邊灰色部分的樹即爲一顆fiber樹,右邊的workInProgress爲中間態,它是在diff過程中自頂向下構建的樹形結構,可用於斷點恢復,所有工作單元都更新完成之後,生成的workInProgress樹會成爲新的fiber tree。
  • 4)fiber tree中每個節點即一個工作單元,跟之前的VirtualDom樹類似,表示一個虛擬DOM節點。workInProgress tree的每個fiber node都保存着diff過程中產生的effect list,它用來存放diff結果,並且底層的樹節點會依次向上層merge effect list,以收集所有diff結果。注意的是如果某些節點並未更新,workInProgress tree會直接複用原fiber tree的節點(鏈表操作),而有數據更新的節點會被打上tag標籤。
<FiberNode> : {
    stateNode,    // 節點實例
    child,        // 子節點
    sibling,      // 兄弟節點
    return,       // 父節點
}

➣ React新舊生命週期


React16.3之前的生命週期

  1. componentWillMount()
    此生命週期函數會在在組件掛載之前被調用,整個生命週期中只被觸發一次。開發者通常用來進行一些數據的預請求操作,以減少請求發起時間,建議的替代方案是考慮放入constructor構造函數中,或者componentDidMount後;另一種情況是在在使用了外部狀態管理庫時,如Mobx,可以用於重置Mobx Store中的的已保存數據,替代方案是使用生命週期componentWilUnmount在組件卸載時自動執行數據清理。

  2. componentDidMount()
    此生命週期函數在組件被掛載之後被調用,整個生命週期中只觸發一次。開發者同樣可以用來進行一些數據請求的操作;除此之外也可用於添加事件訂閱(需要在componentWillUnmount中取消事件訂閱);因爲函數觸發時dom元素已經渲染完畢,第三種使用情況是處理一些界面更新的副作用,比如使用默認數據來初始化一個echarts組件,然後在componentDidUpdate後進行echarts組件的數據更新。

  3. componentWillReceiveProps(nextProps, nexState)
    此生命週期發生在組件掛載之後的組件更新階段。最常見於在一個依賴於prop屬性進行組件內部state更新的非完全受控組件中,非完全受控組件即組件內部維護state更新,同時又在某個特殊條件下會採用外部傳入的props來更新內部state,注意不要直接將props完全複製到state,否則應該使用完全受控組件Function Component,一個例子如下:

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = e => his.setState({ email: e.target.value });

  componentWillReceiveProps(nextProps) {
    if (nextProps.userID !== this.props.userID) {
      this.setState({ email: nextProps.email });
    }
  }
}
  1. shouldComponentUpdate(nextProps)
    此生命週期發生在組件掛載之後的組件更新階段。
    值得注意的是子組件更新不一定是由於props或state改變引起的,也可能是父組件的其它部分更改導致父組件重渲染而使得當前子組件在props/state未改變的情況下重新渲染一次。
    函數被調用時會被傳入即將更新的nextPropsnextState對象,開發者可以通過對比前後兩個props對象上與界面渲染相關的屬性是否改變,再決定是否允許這次更新(return true表示允許執行更新,否則忽略更新,默認爲true)。常搭配對象深比較函數用於減少界面無用渲染次數,優化性能。在一些只需要簡單淺比較props變化的場景下,並且相同的state和props會渲染出相同的內容時,建議使用React.PureComponnet替代,在props更新時React會自動幫你進行一次淺比較,以減少不必要渲染。
class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = e => his.setState({ email: e.target.value });

  shouldComponentUpdate(nextProps, nextState) {
    if (
      nextProps.userID === this.props.userID &&
      nextState.email == this.state.email
    ) return false;
  }
}
  1. componenetWillUpdate(newProps, newState)
    此生命週期發生在組件掛載之後的更新階段。當組件收到新的props或state,並且shouldComponentUpdate返回允許更新時,會在渲染之前調此方法,不可以在此生命週期執行setState。在此生命週期中開發者可以在界面實際渲染更新之前拿到最新的nextPropsnextState,從而執行一些副作用:比如觸發一個事件、根據最新的props緩存一些計算數據到組件內、平滑界面元素動畫等:
 // 需要搭配css屬性transition使用
 componentWillUpdate : function(newProps,newState){
    if(!newState.show)
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
    else
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
  },
  componentDidUpdate : function(oldProps,oldState){
    if(this.state.show)
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
    else
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
  }
  1. componenetDidUpdate(prevProps, prevState)
    此生命週期發生在組件掛載之後的更新階段,組件初次掛載不會觸發。當組件的props和state改變引起界面渲染更新後,此函數會被調用,不可以在此生命週期執行setState。我們使用它用來執行一些副作用:比如條件式觸發必要的網絡請求來更新本地數據、使用render後的最新數據來調用一些外部庫的執行(例子:定時器請求接口數據動態繪製echarts折線圖):
  ...
  componentDidMount() {
    this.echartsElement = echarts.init(this.refs.echart);
    this.echartsElement.setOption(this.props.defaultData);
    ...
  }
  componentDidUpdate() {
    const { treeData } = this.props;
    const optionData = this.echartsElement.getOption();
    optionData.series[0].data = [treeData];
    this.echartsElement.setOption(optionData, true);
  }
  1. componentWillUnmount()
    此生命週期發生在組件卸載之前,組件生命週期中只會觸發一次。開發者可以在此函數中執行一些數據清理重置、取消頁面組件的事件訂閱等。

React16.3之後的生命週期

React16.3之後React的Reconciler架構被重寫(Reconciler用於處理生命週期鉤子函數和DOM DIFF),之前版本採用函數調用棧遞歸同步渲染機制即Stack Reconciler,dom的diff階段不能被打斷,所以不利於動畫執行和事件響應。React團隊使用Fiber Reconciler架構之後,diff階段根據虛擬DOM節點拆分成包含多個工作任務單元(FiberNode)的Fiber樹(以鏈表實現),實現了Fiber任務單元之間的任意切換和任務之間的打斷及恢復等等。Fiber架構下的異步渲染導致了componentWillMountcomponentWillReceivePropscomponentWillUpdate三個生命週期在實際渲染之前可能會被調用多次,產生不可預料的調用結果,因此這三個不安全生命週期函數不建議被使用。取而代之的是使用全新的兩個生命週期函數:getDerivedStateFromPropsgetSnapshotBeforeUpdate

  1. getDerivedStateFromProps(nextProps, currentState)
  • 1)定義
    此生命週期發生在組件初始化掛載和組件更新階段,開發者可以用它來替代之前的componentWillReceiveProps生命週期,可用於根據props變化來動態設置組件內部state。
    函數爲static靜態函數,因此我們無法使用this直接訪問組件實例,也無法使用this.setState直接對state進行更改,以此可以看出React團隊想通過React框架的API式約束來儘量減少開發者的API濫用。函數調用時會被傳入即將更新的props和當前組件的state數據作爲參數,我們可以通過對比處理props然後返回一個對象來觸發的組件state更新,如果返回null則不更新任何內容。
  • 2)濫用場景一:直接複製props到state上面
    這會導致父層級重新渲染時,SimpleInput組件的state都會被重置爲父組件重新傳入的props,不管props是否發生了改變。如果你說使用shouldComponentUpdate搭配着避免這種情況可以嗎?代碼層面上可以,不過可能導致後期shouldComponentUpdate函數的數據來源混亂,任何一個prop的改變都會導致重新渲染和不正確的狀態重置,維護一個可靠的shouldComponentUpdate會更難。
class SimpleInput extends Component {
  state = { attr: ''  };

  render() {
    return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {
    // 這會覆蓋所有組件內的state更新!
    return { attr: nextProps.attr };
  }
}
  • 3)使用場景: 在props變化後選擇性修改state
class SimpleInput extends Component {
  state = { attr: ''  };

  render() {
    return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {
    if (nextProps.attr !== currentState.attr) return { attr: nextProps.attr };
    return null;
  }
}

可能導致的bug:在需要重置SimpleInput組件的情況下,由於props.attr未改變,導致組件無法正確重置狀態,表現就是input輸入框組件的值還是上次遺留的輸入。

  • 4)優化的使用場景一:使用完全可控的組件
    完全可控的組件即沒有內部狀態的功能組件,其狀態的改變完全受父級props控制,這種方式需要將原本位於組件內的state和改變state的邏輯方法抽離到父級。適用於一些簡單的場景,不過如果父級存在太多的子級狀態管理邏輯也會使邏輯冗餘複雜化。
function SimpleInput(props) {
  return <input onChange={props.onChange} value={props.attr} />;
}
  • 5)優化的使用場景二:使用有key值的非可控組件
    如果我們想讓組件擁有自己的狀態管理邏輯,但是在適當的條件下我們又可以控制組件以新的默認值重新初始化,這裏有幾種方法參考:
/* 
  1. 設置一個唯一值傳入作爲組件重新初始化的標誌
     通過對比屬性手動讓組件重新初始化
*/
class SimpleInput extends Component {
  state = { attr: this.props.attr, id=""  }; // 初始化默認值

  render() {
    return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {
    if (nextProps.id !== currentState.id)
      return { attr: nextProps.attr, id: nextProps.id };
    return null;
  }
}

/*
  2. 設置一個唯一值作爲組件的key值
     key值改變後組件會以默認值重新初始化
  */
class SimpleInput extends Component {
  state = { attr: this.props.attr  }; // 初始化默認值

  render() {
    return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }
}

<SimpleInput
  attr={this.props.attr}
  key={this.props.id}
/>

/*
  3. 提供一個外部調用函數以供父級直接調用以重置組件狀態
     父級通過refs來訪問組件實例,拿到組件的內部方法進行調用
  */
class SimpleInput extends Component {
  state = { attr: this.props.attr  }; // 初始化默認值

  resetState = (value) => {
    this.setState({ attr: value });
  }

  render() {
    return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }
}

<SimpleInput
  attr={this.props.attr}
  ref={this.simpleInput}
/>


  1. componentDidMount()
    ...

  2. shouldComponentUpdate(nextProps, nexState)
    ...

  3. getSnapshotBeforeUpdate(prevProps, prevState)
    此生命週期發生在組件初始化掛載和組件更新階段,界面實際render之前。開發者可以拿到組件更新前的prevPropsprevState,同時也能獲取到dom渲染之前的狀態(比如元素寬高、滾動條長度和位置等等)。此函數的返回值會被作爲componentWillUpdate週期函數的第三個參數傳入,通過搭配componentDidUpdate可以完全替代之前componentWillUpdate部分的邏輯,見以下示例。

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

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 判斷是否在list中添加新的items 
    // 捕獲滾動​​位置以便我們稍後調整滾動位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 調整滾動位置使得這些新items不會將舊的items推出視圖
    // snapshot是getSnapshotBeforeUpdate的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...list items... */}</div>
    );
  }
}
  1. componenetDidUpdate(prevProps, prevState, shot)
    此生命週期新增特性:getSnapshotBeforeUpdate的返回值作爲此函數執行時傳入的第三個參數。

  2. componenetWillUnmount
    ...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章