生命周期
現在流行的前端框架,無論是angular還是React,又或是Angular2以及以上,都由框架自身提供了生命周期(有的叫生命周期鉤子)供開發者使用。
下面我們看下上面幾個框架的生命周期:
Vue生命周期:
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生命周期(16.0后):
我們下面看一個例子,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 生命周期使用小提示:
- getDerivedStateFromProps被React官方歸類為不常用的生命周期,能不用就盡量不用,前面用那么多篇幅講這個生命周期主要是為了加深對Reac運行機制的理解。
- unsafe
下面開始咱們今天的主題Hooks。
Hooks
React v16.7.0-alpha 中第一次引入了 Hooks 的概念, 為什么要引入這個東西呢?
有兩個原因:
- React 官方覺得 class組件太難以理解,OO(面向對象)太難懂了
- React 官方覺得 , React 生命周期太難理解。
最終目的就是, 開發者不用去理解class, 也不用操心生命周期方法。
但是React 官方又說, Hooks的目的并不是消滅類組件。
此處表示??
但無論如何,既然react官方這樣說了,那咱們就來了解一下這個 Hooks。
1. API
我們來看下Hooks的API,下面是官網上的截圖:
乍一看還是挺多的, 其實有很多的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 就會一直持有其初始值。盡管傳入[]
作為第二個參數有點類似于componentDidMount
和componentWillUnmount
的思維模式,但我們有 更好的 方式 來避免過于頻繁的重復調用 effect。除此之外,請記得 React 會等待瀏覽器完成畫面渲染之后才會延遲調用useEffect
,因此會使得處理額外操作很方便。
我們推薦啟用 eslint-plugin-react-hooks
中的 exhaustive-deps
規則。此規則會在添加錯誤依賴時發出警告并給出修復建議。
1.3 useContext
用到的很少,暫時不做介紹。React Context API 大家都很少用到,有興趣的同學可以去了解一下。
提供了上下文(context)的功能
2. 簡介
上面我們介紹了 useState
、useEffect
和useContext
這三個最基本的 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
,componentWillUnmount
:useEffect
Hook 可以表達所有這些(包括 不那么 常見 的場景)的組合。 -
getSnapshotBeforeUpdate
,componentDidCatch
以及getDerivedStateFromError
:目前還沒有這些方法的 Hook 等價寫法,但很快會被添加。
從 Class 遷移到 Hook
- 生命周期方法要如何對應到 Hook?
- 我該如何使用 Hook 進行數據獲取?
- 有類似實例變量的東西嗎?
- 我應該使用單個還是多個 state 變量?
- 我可以只在更新時運行 effect 嗎?
- 如何獲取上一輪的 props 或 state?
- 為什么我會在我的函數中看到陳舊的 props 和 state ?
- 我該如何實現 getDerivedStateFromProps?
- 有類似 forceUpdate 的東西嗎?
- 我可以引用一個函數組件嗎?
- 我該如何測量 DOM 節點?
- const [thing, setThing] = useState() 是什么意思?
參考
- https://zh-hans.reactjs.org/docs/thinking-in-react.html
- https://angular.io/guide/lifecycle-hooks
- https://cn.vuejs.org/v2/guide/instance.html#%E5%AE%9E%E4%BE%8B%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90
- http://react-china.org/t/react-v16-7-0-alpha-hooks/26839
- react 生命周期各版本對比
- React v15到v16.3, v16.4新生命周期總結以及使用場景
- React生命周期圖
- 全面了解 React 新功能: Suspense 和 Hooks
- custom-react-hooks
- https://upmostly.com/tutorials/react-hooks-simple-introduction/
- https://upmostly.com/tutorials/using-custom-react-hooks-simplify-forms/
- https://cdnjs.com/libraries/bulma
- https://mp.weixin.qq.com/s/rCCO3Dz20ihWL4eUl1Zijg