Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性.
React Hook 為何而存在
1.在組件之間復用狀態邏輯很難
雖然我們可以用render props 和高階組件來解決這一問題,但是這不僅會容易形成「嵌套地獄」,還會面臨一個很尷尬的問題,那就是複雜邏輯難寫!
2.複雜組件難以理解
各種生命周期函數內充斥著各種狀態邏輯處理和副作用,且難以復用、零散,比如一個調用列表數據的接口方法getList分別要寫到componentDidMount 和componentDidUpdate中等等。
3.難以理解的class
this的指向問題(經常在某一處忘記bind(this)然後bug找半天)、組件預編譯技術(組件摺疊)會在class中遇到優化失效的case、class不能很好的壓縮、class在熱重載時會出現不穩定的情況。
有了 react hook 就不用寫class(類)了,組件都可以用function來寫,不用再寫生命周期鉤子,最關鍵的,不用再面對this了!
State Hook首先我們先看下官方給出的簡單例子
這是一個簡單的累加計數器,我們先看class聲明的組件
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>你點擊了{this.state.count}次</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
點我
</button>
</div>
);
}
}這是一個非常非常簡單的組件了,相信了解過react的人都能夠看懂
那麼我們再看一下使用hook的版本
import React, { useState } from 'react';
function Example() {
// 聲明一個叫 "count" 的 state 變量
const [count, setCount] = useState(0);
return (
<div>
<p>你點擊了{count}次</p>
<button onClick={() => setCount(count + 1)}>
點我
</button>
</div>
);
}是不是hook要簡單了一點?可以看到,這是一個函數,與以往不同的是它擁有了自己的狀態(count),就像class中的this.state.count;同時它還可以通過setCount()來更新自己的狀態,就像class中的this.setState()。
為什麼會這樣呢?仔細看第一行我們引入了useState這個hook,就是這個hook讓我們的無狀態組件成為了一個有狀態的組件。
狀態值的聲明、讀取、更新那麼我們分解一下看看到底這個hook都為我們做了什麼
const [count, setCount] = useState(0);首先useState的作用就是來聲明狀態變量,這個函數接收的參數是我們要為變量賦予的初始值,它返回了一個數組,索引[0]是當前的狀態值,[1]是用來更新這個狀態值的方法
所以這一句其實就是聲明了一個count變量,就好比
this.state = {
count: 0
};並且通過useState(0)傳入參數0來為count賦了一個初始值0,同時提供了一個像this.setState一樣可以更新它的方法setCount
然後我們引用讀取這個變量的時候直接 {count}就好了,不用再this.state.count 這麼長了
<p>你點擊了{count}次</p>當我們想更新這個值的時候,直接調用setCount(新的值)就可以了。
怎麼樣,是不是很簡單很容易理解?
但是,這個函數是怎麼記住之前的狀態的?通常來說我們在函數中聲明一個變量,函數運行完也就跟著銷毀了,重複調用的時候會重新聲明,那這個Example函數是怎麼做到記住之前聲明的狀態變量的?
State Hook 解決存儲持久化的方案我所知道的可以通過js存儲持久化狀態的方法有: class類、全局變量、DOM、閉包
通過學習react hook源碼解析了解到是用閉包實現的~
function useState(initialState){
let state = initialState;
function dispatch = (newState, action)=>{
state = newState;
}
return [state, dispatch]
}像不像redux?給定一個初始state,然後通過dispatch一個action,經由reducer改變state,再返回新的state,觸發組件的重新渲染。
但是僅僅這樣還滿足不了要求,我們需要一個新的數據結構來保存上一次的state和這一次的state,以便可以在初始化流程調用useState和更新流程調用useState時可以取到對應的正確值。假定這個數據結構叫Hook:
type Hook = {
memoizedState: any, // 上一次完整更新之後的最終狀態值
queue: UpdateQueue<any, any> | null, // 更新隊列
};考慮到第一次組件mounting和後續的updating邏輯差異,定義兩個不同的useState函數來實現,分別叫做mountState和updateState
function useState(initialState){
if(isMounting){
return mountState(initialState);
}
if(isUpdateing){
return updateState(initialState);
}
}
// 第一次調用組件的 useState 時實際調用的方法
function mountState(initialState){
let hook = createNewHook();
hook.memoizedState = initalState;
return [hook.memoizedState, dispatchAction]
}
function dispatchAction(action){
// 使用數據結構存儲所有的更新行為,以便在 rerender 流程中計算最新的狀態值
storeUpdateActions(action);
// 執行 fiber 的渲染
scheduleWork();
}
// 第一次之後每一次執行 useState 時實際調用的方法
function updateState(initialState){
// 根據 dispatchAction 中存儲的更新行為計算出新的狀態值,並返回給組件
doReducerWork();
return [hook.memoizedState, dispatchAction];
}
function createNewHook(){
return {
memoizedState: null,
baseUpdate: null
}
}以上就是基本的實現思路,內容參考自源碼解析React Hook構建過程
聲明多個state變量useState是可以多次調用的
function ExampleWithManyStates() {
// 聲明多個 state 變量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '學習 Hook' }]);
// ...
}useState也支持接收對象或者數組作為參數。與this.setState不同的是,this.setState是合併狀態後返回一個新的狀態,而useState只是直接替換老狀態後返回新狀態。
Hook 規則hook本質也是javaScript函數,但是他在使用的時候需要遵循兩條規則,並且react要求強制執行這兩條規則,不然就會出現奇怪的bug。
1.只能在最頂層使用hook
不要在循環,條件或者嵌套函數中調用hook,確保總是在react函數的最頂層調用,目的是為了確保hook在每一次渲染中都按照同樣的順序被調用,這讓react能夠在多次的useState和useEffect(另一種hook,下面會說)調用之間保持狀態的正確,來保證多個state的相互獨立和一一對應的關係。
2.只在react函數中調用hook
不要在普通js函數中調用hook
Effect Hook官方文檔中一句話說明 Effect Hook 可以讓你在函數組件中執行副作用操作,可能一句話並不能讓人很好的理解它是幹嘛用的,我們可以接著看上面那個最簡單的計數器的慄子,在它的基礎上加一個小功能
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
//設置瀏覽器標題內容
document.title = `你點擊了${count} 次`;
});
return (
<div>
<p>你點擊了{count}次</p>
<button onClick={() => setCount(count + 1)}>
點我
</button>
</div>
);
}這段代碼增加了一個將document的title設置為包含了點擊次數的消息,如果通過class組件,應該怎麼寫呢?我們對比一下:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `你點擊了 ${this.state.count} 次`;
}
componentDidUpdate() {
document.title = `你點擊了 ${this.state.count} 次`;
}
render() {
return (
<div>
<p>你點擊了{this.state.count}次</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
點我
</button>
</div>
);
}
}通過對比,可以看出好像effect hook就相當於生命周期的componentDidMount,componentDidUpdate;其實,我們寫的有狀態組件,通常都會產生副作用,比如ajax請求,瀏覽器事件的綁定和解綁,手動修改dom,記錄日誌等等;副作用還分為需要清除的和不需要清除的,所以effect hook 還相當於一個componentWillUnmount(比如我們有個需求是需要論詢向伺服器請求最新數據,那麼我們就需要在組件卸載的時候來清理掉這個輪詢操作)
清除副作用componentDidMount(){
//輪詢獲取數據
this.getNewData()
}
componentWillUnmount(){
//組件卸載前清除輪詢操作
this.unGetNewData()
}我們完全可以在函數式組件中使用effect hook 來清除這個副作用,用法是在effect函數中return一個函數(清除操作)
useEffect(()=>{
getNewData()
return function cleanup() {
unGetNewData()
}
})effect中返回一個函數,這是effect可選的清除機制。每個effect都可以返回一個清除函數,看你的需要。
react會在組件卸載的時候執行清除操作。effect在每次渲染的時候都會執行,並且是每次渲染之前都會去執行cleanup來清除上一個effect副作用。
這種解綁模式同componentWillUnmount不一樣。componentWillUnmount只會在組件被銷毀前執行一次,而effect hook裡的函數,每次組件渲染都會執行一遍,包括副作用函數和它return的清理操作
effect的性能優化大家一看到「每一次」都會執行,首先就會想到跟性能有關的問題,那麼其實effect是可以跳過的,同樣通過對比class組件來理解effect hook,class組件中使用componentDidUpdate來進行前後邏輯的比較
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
//判斷狀態值count改變了,再觸發副作用操作
document.title = `你點擊了${this.state.count}次`;
}
}同樣的在effect中,有第二個參數,起同樣的作用
useEffect(() => {
document.title = `你點擊了${count}次`;
}, [count]); // 僅在 count 更改時更新第二個參數為一個數組,如果數組中有多個元素,即使只有一個元素發生了改變,react也會執行effect(即只有數組中所有元素都未變化,react才會跳過這次effect);若為空數組[],則表示該effect只會執行一次(包括副作用和return的清除副作用操作)
自定義Hook也就是我們最初的目的:邏輯復用!試想一下,加入我們現在要實現這樣一個功能組件,點擊button隨機切換背景顏色
用我們所熟悉的class組件是這樣實現的
import React, { Component } from "react";
export default class ChangeColor extends Component {
constructor() {
super();
this.state = {
color: "red"
};
this.colors = ["red", "bule", "green", "yellow", "black"];
}
changeColor() {
const index = Math.floor(Math.random() * this.colors.length);
this.setState({ color: this.colors[index] });
}
render() {
return (
<div>
<div
style={{
width: 400,
height: 100,
border: "1px solid #ccc",
background: this.state.color
}}
></div>
<button onClick={() => this.changeColor()}>隨機切換</button>
</div>
);
}
}那麼用hook是怎麼實現的呢
import React, { useState } from "react";
export default function ChangeColor() {
const colors = ["red", "bule", "green", "yellow", "black"];
const [color, setColor] = useState("red");
function changeColor() {
const index = Math.floor(Math.random() * colors.length);
setColor(colors[index]);
}
return (
<div>
<div
style={{
width: 400,
height: 100,
border: "1px solid #ccc",
background: color
}}
></div>
<button onClick={changeColor}>隨機切換</button>
</div>
);
}需要注意的一點是,自定義hook是一個函數,其名稱以 "use" 開頭,它的內部可以調用其他的hook(無論是api提供的還是我們自定義的)
自定義 Hook 必須以 「use」 開頭嗎?必須如此。這個約定非常重要。不遵循的話,由於無法判斷某個函數是否包含對其內部 Hook 的調用,React 將無法自動檢查你的 Hook 是否違反了 Hook 的規則。
在兩個組件中使用相同的 Hook 會共享 state 嗎?不會。自定義 Hook 是一種重用狀態邏輯的機制(例如設置為訂閱並存儲當前值),所以每次使用自定義 Hook 時,其中的所有 state 和副作用都是完全隔離的。
還有哪些react提供的hook這裡我只講述了useState 和 useEffect兩個最重要也是最常用的hook,其實react還提供了很多hook:
useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect