淺談Hooks&&生命周期(2019-03-12)

生命周期

現在流行的前端框架,無論是angular還是React,又或是Angular2以及以上,都由框架自身提供了生命周期(有的叫生命周期鉤子)供開發者使用。

下面我們看下上面幾個框架的生命周期:

Vue生命周期:


Vue-lifecycle

Angular生命周期:

Hook Purpose and Timing
ngOnChanges() Angular(重新)設置數據綁定輸入屬性時的響應。該方法接收[SimpleChanges](https://angular.io/api/core/SimpleChanges)當前和先前屬性值的對象。ngOnInit()在一個或多個數據綁定輸入屬性發生更改 之前和之后調用。
ngOnInit() 在Angular首次顯示數據綁定屬性并設置指令/組件的輸入屬性后初始化指令/組件。在第一次之后 調用一次。 ngOnChanges()
ngDoCheck() 檢測Angular無法或不會自行檢測的更改并對其進行操作。在每次更改檢測運行期間,在ngOnChanges()和之后立即調用ngOnInit()。
[ngAfterContentInit()] 在Angular將外部內容投影到組件的視圖/指令所在的視圖后進行響應。在第一次之后 調用一次ngDoCheck()。
ngAfterContentChecked() 在Angular檢查投射到指令/組件中的內容后響應。在[ngAfterContentInit()](https://angular.io/api/router/RouterLinkActive#ngAfterContentInit)隨后和隨后的每一次調用之后ngDoCheck()
[ngAfterViewInit()] 在Angular初始化組件的視圖和子視圖/指令所在的視圖后響應。在第一次之后 調用一次ngAfterContentChecked()。
ngAfterViewChecked() 在Angular檢查組件的視圖和子視圖/指令所在的視圖后響應。在[ngAfterViewInit()]隨后和隨后的每一次調用之后ngAfterContentChecked()。
ngOnDestroy() 就在Angular破壞指令/組件之前進行清理。取消訂閱Observable并分離事件處理程序以避免內存泄漏。在 Angular破壞指令/組件之前 調用。

React生命周期(16.0之前):


React-Lifecycle1
React-Lifecycle2

React生命周期(16.0后):

React-Lifecycle3

我們下面看一個例子,React代碼中是如何使用生命周期的。

class Demo extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    this.fetchList();
  }

  fetchList() {}

  render() {
    return <span>demo page</span>;
  }
}

我們都知道,在react中,有兩種類型的組件,function組件和class組件。其中class類不僅允許內部狀態(state)的存在,還有完整的生命周期鉤子。

前面說到class類組件有完整的生命周期鉤子。這些生命周期鉤子是從哪來的呢?畢竟class類組件就是原生的class類寫法。

其實React內置了一個Component類,生命周期鉤子都是從它這里來的,麻煩的地方就是每次都要繼承。

綜合以上的對比,我們可以看出,生命周期的出現,主要是為了便于開發&&更好的開發。

React 生命周期使用小提示:

  1. getDerivedStateFromProps被React官方歸類為不常用的生命周期,能不用就盡量不用,前面用那么多篇幅講這個生命周期主要是為了加深對Reac運行機制的理解。
  2. unsafe

下面開始咱們今天的主題Hooks。

Hooks

React v16.7.0-alpha 中第一次引入了 Hooks 的概念, 為什么要引入這個東西呢?

有兩個原因:

  1. React 官方覺得 class組件太難以理解,OO(面向對象)太難懂了
  2. React 官方覺得 , React 生命周期太難理解。

最終目的就是, 開發者不用去理解class, 也不用操心生命周期方法。

但是React 官方又說, Hooks的目的并不是消滅類組件。

此處表示??

但無論如何,既然react官方這樣說了,那咱們就來了解一下這個 Hooks。

1. API

我們來看下Hooks的API,下面是官網上的截圖:

image.png

乍一看還是挺多的, 其實有很多的Hook 還處在實驗階段,很可能有一部分要被砍掉, 目前大家只需要熟悉的, 三個就夠了:

  • useState
  • useEffect
  • useContext

1.1 useState

看例子 - hooksdemo

進去就調用了useState, 傳入 0,對state 進行初始化,此時count 就是0, 返回一個數組, 第一個元素就是 state 的值,第二個元素是更新 state 的函數。

// 下面代碼等同于: const [count, setCount] = useState(0);
  const result = useState(0);
  const count = result[0];
  const setCount = result[1];

利用 count 可以讀取到這個 state,利用 setCount 可以更新這個 state,而且我們完全可以控制這兩個變量的命名,只要高興,你完全可以這么寫:

const [theCount, updateCount] = useState(0);

因為 useState 在 Counter 這個函數體中,每次 Counter 被渲染的時候,這個 useState 調用都會被執行,useState 自己肯定不是一個純函數,因為它要區分第一次調用(組件被 mount 時)和后續調用(重復渲染時),只有第一次才用得上參數的初始值,而后續的調用就返回“記住”的 state 值。

看到這里,心里可能會有這樣的疑問:如果組件中多次使用 useState 怎么辦?React 如何“記住”哪個狀態對應哪個變量?

React 是完全根據 useState 的調用順序來“記住”狀態歸屬的,假設組件代碼如下:

const Counter = () => {
  const [count, setCount] = useState(0);
  const [foo, updateFoo] = useState('foo');
  
  // ...
}

每一次 Counter 被渲染,都是第一次 useState 調用獲得 count 和 setCount,第二次 useState 調用獲得 foo 和 updateFoo(這里我故意讓命名不用 set 前綴,可見函數名可以隨意)。

React 是渲染過程中的“上帝”,每一次渲染 Counter 都要由 React 發起,所以它有機會準備好一個內存記錄,當開始執行的時候,每一次 useState 調用對應內存記錄上一個位置,而且是按照順序來記錄的。React 不知道你把 useState 等 Hooks API 返回的結果賦值給什么變量,但是它也不需要知道,它只需要按照 useState 調用順序記錄就好了。

你可以理解為會有一個槽去記錄狀態。

正因為這個原因,Hooks,千萬不要在 if 語句或者 for 循環語句中使用!

像下面的代碼,肯定會出亂子的:

let showFruit = true;
let fruit, setFruit;
if (showFruit) {
    [fruit, setFruit] = useState("banana");
    showFruit = false;
  }

因為條件判斷,讓每次渲染中 useState 的調用次序不一致了,于是 React 就錯亂了。

條件渲染報錯

1.2 useEffect

除了 useState,React 還提供 useEffect,用于支持組件中增加副作用的支持。

在 React 組件生命周期中如果要做有副作用的操作,代碼放在哪里?

如果您之前編寫過React類組件,則應熟悉componentDidMount,componentDidUpdate和componentWillUnmount等生命周期方法。這副作用的代碼就放在這里。

useEffect Hook是這三種生命周期方法的組合。

useEffect當組件第一次完成加載時運行一次,然后每次更新組件狀態時運行一次。因為按鈕單擊正在修改狀態,即組件useEffect 方法運行。

在 Counter 組件,如果我們想要在用戶點擊“+”或者“-”按鈕之后把計數值體現在網頁標題上,這就是一個修改 DOM 的副作用操作,所以必須把 Counter 寫成 class,而且添加下面的代碼:

介紹一下副作用(做了這件事情,我們還必須要再做一些事情)
我們寫的有狀態組件,通常會產生很多的副作用(side effect),比如發起ajax請求獲取數據,添加一些監聽的注冊和取消注冊,手動修改dom等等。我們之前都把這些副作用的函數寫在生命周期函數鉤子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而現在的useEffect就相當與這些聲明周期函數鉤子的集合體。它以一抵三。

同時,由于前文所說hooks可以反復多次使用,相互獨立。所以我們合理的做法是,給每一個副作用一個單獨的useEffect鉤子。這樣一來,這些副作用不再一股腦堆在生命周期鉤子里,代碼變得更加清晰。

componentDidMount() {
  document.title = `Count: ${this.state.count}`;
}

componentDidUpdate() {
  document.title = `Count: ${this.state.count}`;
}

而有了 useEffect,我們就不用寫一個 class 了,對應代碼如下:

import { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${this.state.count}`;
  });

  return (
    <div>
       <div>{count}</div>
       <button onClick={() => setCount(count + 1)}>+</button>
       <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
};

setEffect 的參數是一個函數,組件每次渲染之后,都會調用這個函數參數,這樣就達到了 componentDidMount 和 componentDidUpdate 一樣的效果。

雖然本質上,依然是 componentDidMount 和 componentDidUpdate 兩個生命周期被調用,但是現在我們關心的不是 mount 或者 update 過程,而是“after render”事件,useEffect 就是告訴組件在“渲染完”之后做點什么事。

讀者可能會問,現在把 componentDidMount 和 componentDidUpdate 混在了一起,那假如某個場景下我只在 mount 時做事但 update 不做事,用 useEffect 不就不行了嗎?

其實,用一點小技巧就可以解決。useEffect 還支持第二個可選參數,只有同一 useEffect 的兩次調用第二個參數不同時,第一個函數參數才會被調用. 所以,如果想模擬 componentDidMount,只需要這樣寫:

useEffect(() => {
    // 這里只有mount時才被調用,相當于componentDidMount
  }, [123]);

在上面的代碼中,useEffect 的第二個參數是 [123],其實也可以是任何一個常數,因為它永遠不變,所以 useEffect 只在 mount 時調用第一個函數參數一次,達到了 componentDidMount 一樣的效果。

如果想執行只運行一次的 effect(僅在組件掛載和卸載時執行),可以傳遞一個空數組([])作為第二個參數。這就告訴 React 你的 effect 不依賴于 props 或 state 中的任何值,所以它永遠都不需要重復執行。這并不屬于特殊情況 —— 它依然遵循輸入數組的工作方式。

如果你傳入了一個空數組([]),effect 內部的 props 和 state 就會一直持有其初始值。盡管傳入 [] 作為第二個參數有點類似于 componentDidMountcomponentWillUnmount 的思維模式,但我們有 更好的 方式 來避免過于頻繁的重復調用 effect。除此之外,請記得 React 會等待瀏覽器完成畫面渲染之后才會延遲調用 useEffect,因此會使得處理額外操作很方便。

我們推薦啟用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告并給出修復建議。

1.3 useContext

用到的很少,暫時不做介紹。React Context API 大家都很少用到,有興趣的同學可以去了解一下。

提供了上下文(context)的功能

2. 簡介

上面我們介紹了 useState、useEffectuseContext這三個最基本的 Hooks,可以感受到,Hooks 將大大簡化使用 React 的代碼。

首先我們可能不再需要 class了,雖然 React 官方表示 class 類型的組件將繼續支持,但是,業界已經普遍表示會遷移到 Hooks 寫法上,也就是放棄 class,只用函數形式來編寫組件。

Hooks 發布后, 會帶來什么樣的改變呢? 毫無疑問, 未來的組件, 更多的將會是函數式組件。

3. Custom React Hooks

我們還可以自定鉤子。這樣我們才能把可以復用的邏輯抽離出來,變成一個個可以隨意插拔的“插銷”,哪個組件要用來,我就插進哪個組件里,so easy!我們來看一個有關表單的例子。

從 Class 遷移到 Hook

  • constructor:函數組件不需要構造函數。你可以通過調用 useState 來初始化 state。如果計算的代價比較昂貴,你可以傳一個函數給 useState。
  • getDerivedStateFromProps:改為 在渲染時 安排一次更新。

盡管你可能 不需要它,但在一些罕見的你需要用到的場景下,你可以在渲染過程中更新 state 。React 會立即退出第一次渲染并用更新后的 state 重新運行組件以避免耗費太多性能。
這里我們把 row prop 上一輪的值存在一個 state 變量中以便比較:

function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row 自上次渲染以來發生過改變。更新 isScrollingDown。
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

初看這或許有點奇怪,但渲染期間的一次更新恰恰就是 getDerivedStateFromProps 一直以來的概念。

  • shouldComponentUpdate:詳見 下方 React.memo.
  • render:這是函數組件體本身。
  • componentDidMount, componentDidUpdate, componentWillUnmountuseEffect Hook 可以表達所有這些(包括 不那么 常見 的場景)的組合。
  • getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前還沒有這些方法的 Hook 等價寫法,但很快會被添加。

從 Class 遷移到 Hook

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容