理解React:Fiber架構(gòu)和新舊生命周期

? React Fiber原理


React架構(gòu)

  • 1)Virtual DOM 層牲距,描述頁面長什么樣
  • 2)Reconciler 層攻礼,負責調(diào)用組件生命周期方法虚婿,進行Diff運算等
  • 3)Renderer 層屋剑,根據(jù)不同的平臺茵臭,渲染出相應的頁面震叮,如 ReactDOM 和 ReactNative

React15遺留問題

StackReconciler
  • 1)瀏覽器的整體渲染是多線程的胧砰,包括GUI渲染線程、JS引擎線程苇瓣、事件觸發(fā)線程尉间、定時觸發(fā)器線程和異步http請求線程。頁面繪制和JS運算是互斥的線程击罪,兩者不能同時進行哲嘲。
  • 2)React15使用JS的函數(shù)調(diào)用棧(Stack Reconciler)遞歸渲染界面,因此在處理DOM元素過多的復雜頁面的頻繁更新時媳禁,大量同步進行的任務(樹diff和頁面render)會導致界面更新阻塞眠副、事件響應延遲、動畫卡頓等竣稽,因此React團隊在16版本重寫了React Reconciler架構(gòu)囱怕。

React16問題解決

FiberReconciler
  • 1)Fiber Reconciler架構(gòu)可以允許同步阻塞的任務拆分成多個小任務霍弹,每個任務占用一小段時間片,任務執(zhí)行完成后判斷有無空閑時間娃弓,有則繼續(xù)執(zhí)行下一個任務典格,否則將控制權(quán)交由瀏覽器以讓瀏覽器去處理更高優(yōu)先級的任務,等下次拿到時間片后台丛,其它子任務繼續(xù)執(zhí)行耍缴。整個流程類似CPU調(diào)度邏輯,底層是使用了瀏覽器APIrequestIdleCallback挽霉。
  • 2)為了實現(xiàn)整個Diff和Render的流程可中斷和恢復防嗡,單純的VirtualDom Tree不再滿足需求,React16引入了采用單鏈表結(jié)構(gòu)的Fiber樹炼吴,如下圖所示本鸣。
  • 3)FiberReconciler架構(gòu)將更新流程劃分成了兩個階段:1.diff(由多個diff任務組成疫衩,任務時間片消耗完后被可被中斷硅蹦,中斷后由requestIdleCallback再次喚醒) => 2.commit(diff完畢后拿到fiber tree更新結(jié)果觸發(fā)DOM渲染,不可被中斷)闷煤。左邊灰色部分的樹即為一顆fiber樹童芹,右邊的workInProgress為中間態(tài),它是在diff過程中自頂向下構(gòu)建的樹形結(jié)構(gòu)鲤拿,可用于斷點恢復假褪,所有工作單元都更新完成之后,生成的workInProgress樹會成為新的fiber tree近顷。
  • 4)fiber tree中每個節(jié)點即一個工作單元生音,跟之前的VirtualDom樹類似,表示一個虛擬DOM節(jié)點窒升。workInProgress tree的每個fiber node都保存著diff過程中產(chǎn)生的effect list缀遍,它用來存放diff結(jié)果,并且底層的樹節(jié)點會依次向上層merge effect list饱须,以收集所有diff結(jié)果域醇。注意的是如果某些節(jié)點并未更新,workInProgress tree會直接復用原fiber tree的節(jié)點(鏈表操作)蓉媳,而有數(shù)據(jù)更新的節(jié)點會被打上tag標簽譬挚。
<FiberNode> : {
    stateNode,    // 節(jié)點實例
    child,        // 子節(jié)點
    sibling,      // 兄弟節(jié)點
    return,       // 父節(jié)點
}
FiberTree

? React新舊生命周期


React16.3之前的生命周期

image
  1. componentWillMount()
    此生命周期函數(shù)會在在組件掛載之前被調(diào)用,整個生命周期中只被觸發(fā)一次酪呻。開發(fā)者通常用來進行一些數(shù)據(jù)的預請求操作减宣,以減少請求發(fā)起時間,建議的替代方案是考慮放入constructor構(gòu)造函數(shù)中玩荠,或者componentDidMount后漆腌;另一種情況是在在使用了外部狀態(tài)管理庫時丰歌,如Mobx,可以用于重置Mobx Store中的的已保存數(shù)據(jù)屉凯,替代方案是使用生命周期componentWilUnmount在組件卸載時自動執(zhí)行數(shù)據(jù)清理立帖。

  2. componentDidMount()
    此生命周期函數(shù)在組件被掛載之后被調(diào)用,整個生命周期中只觸發(fā)一次悠砚。開發(fā)者同樣可以用來進行一些數(shù)據(jù)請求的操作晓勇;除此之外也可用于添加事件訂閱(需要在componentWillUnmount中取消事件訂閱);因為函數(shù)觸發(fā)時dom元素已經(jīng)渲染完畢灌旧,第三種使用情況是處理一些界面更新的副作用绑咱,比如使用默認數(shù)據(jù)來初始化一個echarts組件,然后在componentDidUpdate后進行echarts組件的數(shù)據(jù)更新枢泰。

  3. componentWillReceiveProps(nextProps, nexState)
    此生命周期發(fā)生在組件掛載之后的組件更新階段描融。最常見于在一個依賴于prop屬性進行組件內(nèi)部state更新的非完全受控組件中,非完全受控組件即組件內(nèi)部維護state更新衡蚂,同時又在某個特殊條件下會采用外部傳入的props來更新內(nèi)部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)
    此生命周期發(fā)生在組件掛載之后的組件更新階段年叮。
    值得注意的是子組件更新不一定是由于props或state改變引起的,也可能是父組件的其它部分更改導致父組件重渲染而使得當前子組件在props/state未改變的情況下重新渲染一次玻募。
    函數(shù)被調(diào)用時會被傳入即將更新的nextPropsnextState對象只损,開發(fā)者可以通過對比前后兩個props對象上與界面渲染相關(guān)的屬性是否改變,再決定是否允許這次更新(return true表示允許執(zhí)行更新七咧,否則忽略更新跃惫,默認為true)。常搭配對象深比較函數(shù)用于減少界面無用渲染次數(shù)艾栋,優(yōu)化性能爆存。在一些只需要簡單淺比較props變化的場景下,并且相同的state和props會渲染出相同的內(nèi)容時裹粤,建議使用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)
    此生命周期發(fā)生在組件掛載之后的更新階段拇泣。當組件收到新的props或state,并且shouldComponentUpdate返回允許更新時矮锈,會在渲染之前調(diào)此方法霉翔,不可以在此生命周期執(zhí)行setState。在此生命周期中開發(fā)者可以在界面實際渲染更新之前拿到最新的nextPropsnextState苞笨,從而執(zhí)行一些副作用:比如觸發(fā)一個事件债朵、根據(jù)最新的props緩存一些計算數(shù)據(jù)到組件內(nèi)子眶、平滑界面元素動畫等:
 // 需要搭配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)
    此生命周期發(fā)生在組件掛載之后的更新階段,組件初次掛載不會觸發(fā)序芦。當組件的props和state改變引起界面渲染更新后臭杰,此函數(shù)會被調(diào)用,不可以在此生命周期執(zhí)行setState谚中。我們使用它用來執(zhí)行一些副作用:比如條件式觸發(fā)必要的網(wǎng)絡請求來更新本地數(shù)據(jù)渴杆、使用render后的最新數(shù)據(jù)來調(diào)用一些外部庫的執(zhí)行(例子:定時器請求接口數(shù)據(jù)動態(tài)繪制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()
    此生命周期發(fā)生在組件卸載之前,組件生命周期中只會觸發(fā)一次宪塔。開發(fā)者可以在此函數(shù)中執(zhí)行一些數(shù)據(jù)清理重置磁奖、取消頁面組件的事件訂閱等。

React16.3之后的生命周期

image

React16.3之后React的Reconciler架構(gòu)被重寫(Reconciler用于處理生命周期鉤子函數(shù)和DOM DIFF)某筐,之前版本采用函數(shù)調(diào)用棧遞歸同步渲染機制即Stack Reconciler比搭,dom的diff階段不能被打斷,所以不利于動畫執(zhí)行和事件響應南誊。React團隊使用Fiber Reconciler架構(gòu)之后身诺,diff階段根據(jù)虛擬DOM節(jié)點拆分成包含多個工作任務單元(FiberNode)的Fiber樹(以鏈表實現(xiàn)),實現(xiàn)了Fiber任務單元之間的任意切換和任務之間的打斷及恢復等等弟疆。Fiber架構(gòu)下的異步渲染導致了componentWillMount戚长、componentWillReceiveProps盗冷、componentWillUpdate三個生命周期在實際渲染之前可能會被調(diào)用多次怠苔,產(chǎn)生不可預料的調(diào)用結(jié)果,因此這三個不安全生命周期函數(shù)不建議被使用仪糖。取而代之的是使用全新的兩個生命周期函數(shù):getDerivedStateFromPropsgetSnapshotBeforeUpdate柑司。

  1. getDerivedStateFromProps(nextProps, currentState)
  • 1)定義
    此生命周期發(fā)生在組件初始化掛載和組件更新階段,開發(fā)者可以用它來替代之前的componentWillReceiveProps生命周期锅劝,可用于根據(jù)props變化來動態(tài)設(shè)置組件內(nèi)部state攒驰。
    函數(shù)為static靜態(tài)函數(shù),因此我們無法使用this直接訪問組件實例故爵,也無法使用this.setState直接對state進行更改玻粪,以此可以看出React團隊想通過React框架的API式約束來盡量減少開發(fā)者的API濫用。函數(shù)調(diào)用時會被傳入即將更新的props和當前組件的state數(shù)據(jù)作為參數(shù)诬垂,我們可以通過對比處理props然后返回一個對象來觸發(fā)的組件state更新劲室,如果返回null則不更新任何內(nèi)容。
  • 2)濫用場景一:直接復制props到state上面
    這會導致父層級重新渲染時结窘,SimpleInput組件的state都會被重置為父組件重新傳入的props很洋,不管props是否發(fā)生了改變。如果你說使用shouldComponentUpdate搭配著避免這種情況可以嗎隧枫?代碼層面上可以喉磁,不過可能導致后期shouldComponentUpdate函數(shù)的數(shù)據(jù)來源混亂谓苟,任何一個prop的改變都會導致重新渲染和不正確的狀態(tài)重置,維護一個可靠的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) {
    // 這會覆蓋所有組件內(nèi)的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未改變孕暇,導致組件無法正確重置狀態(tài)纱皆,表現(xiàn)就是input輸入框組件的值還是上次遺留的輸入。

  • 4)優(yōu)化的使用場景一:使用完全可控的組件
    完全可控的組件即沒有內(nèi)部狀態(tài)的功能組件芭商,其狀態(tài)的改變完全受父級props控制派草,這種方式需要將原本位于組件內(nèi)的state和改變state的邏輯方法抽離到父級。適用于一些簡單的場景铛楣,不過如果父級存在太多的子級狀態(tài)管理邏輯也會使邏輯冗余復雜化近迁。
function SimpleInput(props) {
  return <input onChange={props.onChange} value={props.attr} />;
}
  • 5)優(yōu)化的使用場景二:使用有key值的非可控組件
    如果我們想讓組件擁有自己的狀態(tài)管理邏輯,但是在適當?shù)臈l件下我們又可以控制組件以新的默認值重新初始化簸州,這里有幾種方法參考:
/* 
  1. 設(shè)置一個唯一值傳入作為組件重新初始化的標志
     通過對比屬性手動讓組件重新初始化
*/
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. 設(shè)置一個唯一值作為組件的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. 提供一個外部調(diào)用函數(shù)以供父級直接調(diào)用以重置組件狀態(tài)
     父級通過refs來訪問組件實例鉴竭,拿到組件的內(nèi)部方法進行調(diào)用
  */
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)
    此生命周期發(fā)生在組件初始化掛載和組件更新階段,界面實際render之前岸浑。開發(fā)者可以拿到組件更新前的prevPropsprevState搏存,同時也能獲取到dom渲染之前的狀態(tài)(比如元素寬高、滾動條長度和位置等等)矢洲。此函數(shù)的返回值會被作為componentWillUpdate周期函數(shù)的第三個參數(shù)傳入璧眠,通過搭配componentDidUpdate可以完全替代之前componentWillUpdate部分的邏輯,見以下示例读虏。

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

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

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 調(diào)整滾動位置使得這些新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的返回值作為此函數(shù)執(zhí)行時傳入的第三個參數(shù)。

  2. componenetWillUnmount
    ...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盖桥,一起剝皮案震驚了整個濱河市灾螃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌揩徊,老刑警劉巖腰鬼,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異塑荒,居然都是意外死亡熄赡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門袜炕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來本谜,“玉大人,你說我怎么就攤上這事偎窘∥谥” “怎么了剖踊?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵蛾魄,是天一觀的道長。 經(jīng)常有香客問我,道長油啤,這世上最難降的妖魔是什么比庄? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任元践,我火速辦了婚禮讯蒲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘把篓。我一直安慰自己纫溃,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布韧掩。 她就那樣靜靜地躺著紊浩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疗锐。 梳的紋絲不亂的頭發(fā)上坊谁,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音滑臊,去河邊找鬼口芍。 笑死,一個胖子當著我的面吹牛雇卷,可吹牛的內(nèi)容都是我干的鬓椭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼聋庵,長吁一口氣:“原來是場噩夢啊……” “哼膘融!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祭玉,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎春畔,沒想到半個月后脱货,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡律姨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年振峻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片择份。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡扣孟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出荣赶,到底是詐尸還是另有隱情凤价,我是刑警寧澤鸽斟,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站利诺,受9級特大地震影響富蓄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜慢逾,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一立倍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侣滩,春花似錦口注、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至葛躏,卻和暖如春澈段,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舰攒。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工败富, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摩窃。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓兽叮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親猾愿。 傳聞我的和親對象是個殘疾皇子鹦聪,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容