React 生命週期的演變之路

React 生命週期的演變



React 16.3以前的生命週期

  1. 組件初次裝載
    • constructor()
    • componentWillMount()
    • render()
    • componentDidMount()
  2. 組件運行時
    • componentWillReceiveProps()
    • shouldComponentUpdate()
    • componentWillUpdate()
    • render()
    • componentDidUpdate()
  3. 組件卸載
    • componentWillUnmount()

爲何要改變?


綜上能夠看出,React 16.3以前的生命週期很是完整,基本涵蓋了組件生命的每個週期。爲何要變呢?其實主要是性能問題,具體有如下幾點緣由javascript

  1. js 是單線程語言,當組件樹過於深時,每次組件更新耗時增長,阻斷瀏覽器其它動做,造成卡頓
  2. 部分經驗不足的程序員,錯誤的使用生命週期,致使程序異常 例如:在componentWillMount 中放置事件綁定和異步請求函數,在服務端渲染時,組件不會觸發componentWillUnmount 致使的重複請求,重複監聽,內存溢出等。
  3. 主要緣由React將在17後,啓用React Fiber 開始異步渲染。

什麼是React Fiber?


React Fiber是個什麼東西呢?官方的一句話解釋是 React Fiber是對核心算法的一次從新實現 。這麼說彷佛太虛無縹緲,因此仍是要詳細說一下。html

首先了解React Fiber以前的侷限

在現有React中,更新過程是同步的,這可能會致使性能問題。java

當React決定要加載或者更新組件樹時,會作不少事,好比調用各個組件的生命週期函數,計算和比對Virtual DOM,最後更新DOM樹,這整個過程是同步進行的,也就是說只要一個加載或者更新過程開始,那React就以不破樓蘭終不還的氣概,一氣呵成運行到底,中途毫不停歇。react

表面上看,這樣的設計也是挺合理的,由於更新過程不會有任何I/O操做嘛,徹底是CPU計算,因此無需異步操做,的確只要一路狂奔就好了,可是,當組件樹比較龐大的時候,問題就來了。程序員

假如更新一個組件須要1毫秒,若是有200個組件要更新,那就須要200毫秒,在這200毫秒的更新過程當中,瀏覽器那個惟一的主線程都在專心運行更新操做,無暇去作任何其餘的事情。想象一下,在這200毫秒內,用戶往一個input元素中輸入點什麼,敲擊鍵盤也不會得到響應,由於渲染輸入按鍵結果也是瀏覽器主線程的工做,可是瀏覽器主線程被React佔着呢,抽不出空,最後的結果就是用戶敲了按鍵看不到反應,等React更新過程結束以後,咔咔咔那些按鍵一會兒出如今input元素裏了。ajax

這就是所謂的界面卡頓,很很差的用戶體驗。算法

現有的React版本,當組件樹很大的時候就會出現這種問題,由於更新過程是同步地一層組件套一層組件,逐漸深刻的過程,在更新完全部組件以前不中止,函數的調用棧就像下圖這樣,調用得很深,並且很長時間不會返回。 redux

由於JavaScript單線程的特色,每一個同步任務不能耗時太長,否則就會讓程序不會對其餘輸入做出相應,React的更新過程就是犯了這個禁忌,而React Fiber就是要改變現狀。

React Fiber的方式

破解JavaScript中同步操做時間過長的方法其實很簡單——分片。數組

把一個耗時長的任務分紅不少小片,每個小片的運行時間很短,雖然總時間依然很長,可是在每一個小片執行完以後,都給其餘任務一個執行的機會,這樣惟一的線程就不會被獨佔,其餘任務依然有運行的機會。瀏覽器

React Fiber把更新過程碎片化,執行過程以下面的圖所示,每執行完一段更新過程,就把控制權交還給React負責任務協調的模塊,看看有沒有其餘緊急任務要作,若是沒有就繼續去更新,若是有緊急任務,那就去作緊急任務。

維護每個分片的數據結構,就是Fiber。

有了分片以後,更新過程的調用棧以下圖所示,中間每個波谷表明深刻某個分片的執行過程,每一個波峯就是一個分片執行結束交還控制權的時機。

具體可看下面文章 React Fiber

能夠看出在React Fiber使用後,異步的渲染對組件的生命週期產生了必定影響,由於每一次組件更新再也不是按照以前整個流程同步更新下來,而是劃分紅了兩部分。render以前和render以後

render以前
  • constructor()
  • componentWillMount()
  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
render以後
  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

組件在render()以後,界面已經渲染完成,因此不受影響,主要受影響的是render()以前的生命週期函數,咱們來具體看一下render()以前的幾個函數。

constructor()

組件的構造函數,整個生命週期內只調用一次,因此不受影響。

componentWillMount()

組件初次加載時,在render()以前調用,在使用Fiber後,有可能執行後不繼續執行render()函數,在下次時間片時又被調用,因此可能一次渲染屢次調用的狀況。 coumponentWillReceiveProps() componentWillUpdate() 同理。

componentWillUpdate()

用於組件的性能優化,函數返回 true 和 false 。由於該函數只用於判斷是否繼續執行render()函數,對於render()最終是否執行,或是由於Fiber的異步緣由屢次調用都不會產生影響。

因此主要受影響的就是 componentWillMount() coumponentWillReceiveProps() componentWillUpdate() 這3個函數,React 官方也是準備在後續17版本中移除這3個生命週期函數,不過官方也出了2個新的生命週期函數用來替代缺乏的功能。這兩個函數就是:

  • getDeriverdStateFromProps()
  • getSnapshotBeforeUpdate()

在react 16.3以後,生命週期圖就變成了這樣

咱們再來看新增的這兩個生命週期函數:

getDerivedStateFromProps是一個靜態函數,因此函數體內不能訪問this,簡單說,就是應該一個純函數,純函數是一個好東西啊,輸出徹底由輸入決定。

static getDerivedStateFromProps(nextProps, prevState) {
    // 這一輩子命週期方法是靜態的,它在組件實例化或接收到新的 props 時被觸發
    // 經過 nextProps, prevState 進行數據處理,如需更新組件state則返回一個對象,
    // 則將被用於更新 state ;如不需更新則返回一個 null ,則不觸發 state 的更新

    // 配合 `componentDidUpdate` 使用,這一方法能夠取代 `componentWillReceiveProps`
  }

複製代碼

getSnapshotBeforeUpdate(prevProps, prevState) 從圖中能夠看出這個函數是在render以後調用的,按道理來講,咱們對於這以後的操做均可以直接寫在componentDidUpdate裏面,後來仔細瞭解了一下,發現這個生命週期函數是在render和瀏覽器真正渲染的中間,具體以下圖:

對於這個函數如何使用,其實我也找不到合適的例子來說解,咱們暫時能夠看一下官方給的示例:

class ScrollingList extends React.Component {
  listRef = null;

  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) {
      return (
        this.listRef.scrollHeight - this.listRef.scrollTop
      );
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, 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) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.setListRef}> {/* ...contents... */} </div>
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}
複製代碼

在這個示例中,利用getSnapshotBeforeUpdate ,在列表添加新行以後才調整列表的滾動位置。

React Hook



React 組件的兩種形態

  • Class(有狀態)組件
  • Function(無狀態)組件

在一個組件不復雜時,咱們一般用函數式組件來編寫,可是常常隨着業務的變化,組件內部須要有狀態維護,因此常常在又把無狀態組件改爲有狀態組件。能不能讓函數式組件也能夠有狀態呢?這個時候就出現了Hook,可是Hook遠遠不止這些,按照React官方的意見,但願你們儘可能用函數式組件去替代Class組件,因此Hook就要可以承擔Class組件中生命週期函數的做用。

爲何引入Hooks?

react官方給出的動機是用來解決長時間使用和維護react過程當中遇到的一些難以免的問題。好比:

  1. 難以重用和共享組件中的與狀態相關的邏輯
  2. 邏輯複雜的組件難以開發與維護,當咱們的組件須要處理多個互不相關的 local state 時,每一個生命週期函數中可能會包含着各類互不相關的邏輯在裏面。
  3. 類組件中的this增長學習成本,類組件在基於現有工具的優化上存在些許問題。
  4. 因爲業務變更,函數組件不得不改成類組件等等。

在進一步瞭解以前,咱們須要先快速的瞭解一些基本的 Hooks 的用法。

一個最簡單的Hooks

首先讓咱們看一下一個簡單的有狀態組件:

class Example extends React.Component {
 constructor(props) {
  super(props);
  this.state = {
   count: 0
  };
 }

 render() {
  return (
   <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
  );
 }
}
複製代碼

咱們再來看一下使用hooks後的版本:

import { useState } from 'react';

function Example() {
 const [count, setCount] = useState(0);

 return (
  <div>
   <p>You clicked {count} times</p>
   <button onClick={() => setCount(count + 1)}>
    Click me
   </button>
  </div>
 );
}
複製代碼

是否是簡單多了!能夠看到,Example變成了一個函數,但這個函數卻有本身的狀態(count),同時它還能夠更新本身的狀態(setCount)。這個函數之因此這麼了不起,就是由於它注入了一個hook--useState,就是這個hook讓咱們的函數變成了一個有狀態的函數。

能夠看出hook徹底增強了函數式組件的能力,在不增長函數式組件更多複雜性時,變得更增強大。Hooks出現的目標就是想讓咱們更多的去使用函數式組件。咱們能夠在babel中觀察兩種寫法編譯出來的結果。

上面只是舉了一個簡單的用法,hook的能力遠不止於此。

什麼是State Hooks?

回到一開始咱們用的例子,咱們分解來看到底state hooks作了什麼:

import { useState } from 'react';

function Example() {
 const [count, setCount] = useState(0);

 return (
  <div>
   <p>You clicked {count} times</p>
   <button onClick={() => setCount(count + 1)}>
    Click me
   </button>
  </div>
 );
}
複製代碼

聲明一個狀態變量

import { useState } from 'react';

function Example() {
 const [count, setCount] = useState(0);

複製代碼

useState是react自帶的一個hook函數,它的做用就是用來聲明狀態變量。useState這個函數接收的參數是咱們的狀態初始值(initial state),它返回了一個數組,這個數組的第[0]項是當前當前的狀態值,第[1]項是能夠改變狀態值的方法函數。

因此咱們作的事情其實就是,聲明瞭一個狀態變量count,把它的初始值設爲0,同時提供了一個能夠更改count的函數setCount。

更新狀態

<button onClick={() => setCount(count + 1)}>
  Click me
 </button>
複製代碼

當用戶點擊按鈕時,咱們調用setCount函數,這個函數接收的參數是修改過的新狀態值。接下來的事情就交給react了,react將會從新渲染咱們的Example組件,而且使用的是更新後的新的狀態,即count=1。這裏咱們要停下來思考一下,Example本質上也是一個普通的函數,爲何它能夠記住以前的狀態?

一個相當重要的問題

這裏咱們就發現了問題,一般來講咱們在一個函數中聲明的變量,當函數運行完成後,這個變量也就銷燬了(這裏咱們先不考慮閉包等狀況),好比考慮下面的例子:

function add(n) {
  const result = 0;
  return result + 1;
}

add(1); //1
add(1); //1
複製代碼

無論咱們反覆調用add函數多少次,結果都是1。由於每一次咱們調用add時,result變量都是從初始值0開始的。那爲何上面的Example函數每次執行的時候,都是拿的上一次執行完的狀態值做爲初始值?答案是:是react幫咱們記住的。至於react是用什麼機制記住的,咱們能夠再思考一下。

假如一個組件有多個狀態值怎麼辦?

首先,useState是能夠屢次調用的,因此咱們徹底能夠這樣寫:

function ExampleWithManyStates() {
 const [age, setAge] = useState(42);
 const [fruit, setFruit] = useState('banana');
 const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
複製代碼

其次,useState接收的初始值沒有規定必定要是string/number/boolean這種簡單數據類型,它徹底能夠接收對象或者數組做爲參數。惟一須要注意的點是,以前咱們的this.setState作的是合併狀態後返回一個新狀態,而useState是直接替換老狀態後返回新狀態。最後,react也給咱們提供了一個useReducer的hook,若是你更喜歡redux式的狀態管理方案的話。

從ExampleWithManyStates函數咱們能夠看到,useState不管調用多少次,相互之間是獨立的。這一點相當重要。爲何這麼說呢?

其實咱們看hook的「形態」,有點相似以前被官方否認掉的Mixins這種方案,都是提供一種「插拔式的功能注入」的能力。而mixins之因此被否認,是由於Mixins機制是讓多個Mixins共享一個對象的數據空間,這樣就很難確保不一樣Mixins依賴的狀態不發生衝突。

而如今咱們的hook,一方面它是直接用在function當中,而不是class;另外一方面每個hook都是相互獨立的,不一樣組件調用同一個hook也能保證各自狀態的獨立性。這就是二者的本質區別了。

react是怎麼保證多個useState的相互獨立的?

仍是看上面給出的ExampleWithManyStates例子,咱們調用了三次useState,每次咱們傳的參數只是一個值(如42,‘banana'),咱們根本沒有告訴react這些值對應的key是哪一個,那react是怎麼保證這三個useState找到它對應的state呢?

答案是,react是根據useState出現的順序來定的。咱們具體來看一下

//第一次渲染
 useState(42); //將age初始化爲42
 useState('banana'); //將fruit初始化爲banana
 useState([{ text: 'Learn Hooks' }]); //...

 //第二次渲染
 useState(42); //讀取狀態變量age的值(這時候傳的參數42直接被忽略)
 useState('banana'); //讀取狀態變量fruit的值(這時候傳的參數banana直接被忽略)
 useState([{ text: 'Learn Hooks' }]); //...

複製代碼

假如咱們改一下代碼:

let showFruit = true;
function ExampleWithManyStates() {
 const [age, setAge] = useState(42);
 
 if(showFruit) {
  const [fruit, setFruit] = useState('banana');
  showFruit = false;
 }
 
 const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
複製代碼

這樣一來,

//第一次渲染
 useState(42); //將age初始化爲42
 useState('banana'); //將fruit初始化爲banana
 useState([{ text: 'Learn Hooks' }]); //...

 //第二次渲染
 useState(42); //讀取狀態變量age的值(這時候傳的參數42直接被忽略)
 // useState('banana'); 
 useState([{ text: 'Learn Hooks' }]); //讀取到的倒是狀態變量fruit的值,致使報錯
複製代碼

鑑於此,react規定咱們必須把hooks寫在函數的最外層,不能寫在ifelse等條件語句當中,來確保hooks的執行順序一致。

什麼是Effect Hooks?

咱們在上一節的例子中增長一個新功能:

import { useState, useEffect } from 'react';

function Example() {
 const [count, setCount] = useState(0);

 // 相似於componentDidMount 和 componentDidUpdate:
 useEffect(() => {
  // 更新文檔的標題
  document.title = `You clicked ${count} times`;
 });

 return (
  <div>
   <p>You clicked {count} times</p>
   <button onClick={() => setCount(count + 1)}>
    Click me
   </button>
  </div>
 );
}
複製代碼

咱們對比着看一下,若是沒有hooks,咱們會怎麼寫?

class Example extends React.Component {
 constructor(props) {
  super(props);
  this.state = {
   count: 0
  };
 }

 componentDidMount() {
  document.title = `You clicked ${this.state.count} times`;
 }

 componentDidUpdate() {
  document.title = `You clicked ${this.state.count} times`;
 }

 render() {
  return (
   <div>
    <p>You clicked {this.state.count} times</p>
    <button onClick={() => this.setState({ count: this.state.count + 1 })}>
     Click me
    </button>
   </div>
  );
 }
}

複製代碼

咱們寫的有狀態組件,一般會產生不少的反作用(side effect),好比發起ajax請求獲取數據,添加一些監聽的註冊和取消註冊,手動修改dom等等。咱們以前都把這些反作用的函數寫在生命週期函數鉤子裏,好比componentDidMount,componentDidUpdate和componentWillUnmount。而如今的useEffect就至關與這些聲明周期函數鉤子的集合體。它以一抵三。

同時,因爲前文所說hooks能夠反覆屢次使用,相互獨立。因此咱們合理的作法是,給每個反作用一個單獨的useEffect鉤子。這樣一來,這些反作用再也不一股腦堆在生命週期鉤子裏,代碼變得更加清晰。

useEffect作了什麼?

咱們再梳理一遍下面代碼的邏輯:

function Example() {
 const [count, setCount] = useState(0);

 useEffect(() => {
  document.title = `You clicked ${count} times`;
 });

複製代碼

首先,咱們聲明瞭一個狀態變量count,將它的初始值設爲0。而後咱們告訴react,咱們的這個組件有一個反作用。咱們給useEffecthook傳了一個匿名函數,這個匿名函數就是咱們的反作用。在這個例子裏,咱們的反作用是調用browser API來修改文檔標題。當react要渲染咱們的組件時,它會先記住咱們用到的反作用。等react更新了DOM以後,它再依次執行咱們定義的反作用函數。

這裏要注意幾點:

第一,react首次渲染和以後的每次渲染都會調用一遍傳給useEffect的函數。而以前咱們要用兩個聲明周期函數來分別表示首次渲染(componentDidMount),和以後的更新致使的從新渲染(componentDidUpdate)。

第二,useEffect中定義的反作用函數的執行不會阻礙瀏覽器更新視圖,也就是說這些函數是異步執行的,而以前的componentDidMount或componentDidUpdate中的代碼則是同步執行的。這種安排對大多數反作用說都是合理的,但有的狀況除外,好比咱們有時候須要先根據DOM計算出某個元素的尺寸再從新渲染,這時候咱們但願此次從新渲染是同步發生的,也就是說它會在瀏覽器真的去繪製這個頁面前發生。

useEffect怎麼解綁一些反作用

這種場景很常見,當咱們在componentDidMount裏添加了一個註冊,咱們得立刻在componentWillUnmount中,也就是組件被註銷以前清除掉咱們添加的註冊,不然內存泄漏的問題就出現了。

怎麼清除呢?讓咱們傳給useEffect的反作用函數返回一個新的函數便可。這個新的函數將會在組件下一次從新渲染以後執行。這種模式在一些pubsub模式的實現中很常見。看下面的例子:

import { useState, useEffect } from 'react';

function FriendStatus(props) {
 const [isOnline, setIsOnline] = useState(null);

 function handleStatusChange(status) {
  setIsOnline(status.isOnline);
 }

 useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  // 必定注意下這個順序:告訴react在下次從新渲染組件以後,同時是下次調用ChatAPI.subscribeToFriendStatus以前執行cleanup
  return function cleanup() {
   ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
 });

 if (isOnline === null) {
  return 'Loading...';
 }
 return isOnline ? 'Online' : 'Offline';
}

複製代碼

這裏有一個點須要重視!這種解綁的模式跟componentWillUnmount不同。componentWillUnmount只會在組件被銷燬前執行一次而已,而useEffect裏的函數,每次組件渲染後都會執行一遍,包括反作用函數返回的這個清理函數也會從新執行一遍。因此咱們一塊兒來看一下下面這個問題。

爲何要讓反作用函數每次組件更新都執行一遍?

咱們先看之前的模式:

componentDidMount() {
  ChatAPI.subscribeToFriendStatus(
   this.props.friend.id,
   this.handleStatusChange
  );
 }

 componentWillUnmount() {
  ChatAPI.unsubscribeFromFriendStatus(
   this.props.friend.id,
   this.handleStatusChange
  );
 }

複製代碼

很清除,咱們在componentDidMount註冊,再在componentWillUnmount清除註冊。但假如這時候props.friend.id變了怎麼辦?咱們不得再也不添加一個componentDidUpdate來處理這種狀況:

componentDidUpdate(prevProps) {
  // 先把上一個friend.id解綁
  ChatAPI.unsubscribeFromFriendStatus(
   prevProps.friend.id,
   this.handleStatusChange
  );
  // 再從新註冊新但friend.id
  ChatAPI.subscribeToFriendStatus(
   this.props.friend.id,
   this.handleStatusChange
  );
 }
複製代碼

看到了嗎?很繁瑣,而咱們但useEffect則沒這個問題,由於它在每次組件更新後都會從新執行一遍。因此代碼的執行順序是這樣的:

  1. 頁面首次渲染
  2. 替friend.id=1的朋友註冊
  3. 忽然friend.id變成了2
  4. 頁面從新渲染
  5. 清除friend.id=1的綁定
  6. 替friend.id=2的朋友註冊

怎麼跳過一些沒必要要的反作用函數

按照上一節的思路,每次從新渲染都要執行一遍這些反作用函數,顯然是不經濟的。怎麼跳過一些沒必要要的計算呢?咱們只須要給useEffect傳第二個參數便可。用第二個參數來告訴react只有當這個參數的值發生改變時,才執行咱們傳的反作用函數(第一個參數)

useEffect(() => {
 document.title = `You clicked ${count} times`;
}, [count]); // 只有當count的值發生變化時,纔會從新執行`document.title`這一句
複製代碼

當咱們第二個參數傳一個空數組[]時,其實就至關於只在首次渲染的時候執行。當前提是這個Effect並不依賴其它可變的參數。 關於更多依賴項的問題能夠查看官方文檔,文檔有很是詳細的說明。若是個人 effect 的依賴頻繁變化,我該怎麼辦?如何處理函數

有關於Hook更多的內容請查閱官方文檔Hook簡介

相關文章
相關標籤/搜索