Hooks中的useState
React
的數據是自頂向下單向流動的,即從父組件到子組件中,組件的數據存儲在props
和state
中,實際上在任何應用中,數據都是必不可少的,我們需要直接的改變頁面上一塊的區域來使得視圖的刷新,或者間接地改變其他地方的數據,在React
中就使用props
和state
兩個屬性存儲數據。state
的主要作用是用於組件保存、控制、修改自己的可變狀態,state
在組件內部初始化,可以被組件自身修改,而外部不能訪問也不能修改,可以認為state
是一個局部的、只能被組件自身控制的數據源,而對於React
函數組件,useState
即是用來管理自身狀態hooks
函數。
Hooks
對於React Hooks
這個Hooks
的意思,阮一峰大佬解釋說,React Hooks
的意思是,組件盡量寫成純函數,如果需要外部功能和副作用,就用鈎子把外部代碼鈎進來,React Hooks
就是那些鈎子。我覺得這個解釋非常到位了,拿useState
來說,在寫函數組件的時候是將這個函數勾過來使用,而在這個函數內部是保存着一些內部的作用域變量的,也就是常說的閉包,所以Hooks
也可以理解為將另一個作用域變量以及函數邏輯勾過來在當前作用域使用。
對於為什么要用React Hooks
,總結來說還是為了組件復用,特別在更加細粒度的組件復用方面React Hooks
表現更好。在React
中代碼復用的解決方案層出不窮,但是整體來說代碼復用還是很復雜的,這其中很大一部分原因在於細粒度代碼復用不應該與組件復用捆綁在一起,HOC
、Render Props
等基於組件組合的方案,相當於先把要復用的邏輯包裝成組件,再利用組件復用機制實現邏輯復用,自然就受限於組件復用,因而出現擴展能力受限、Ref
隔斷、Wrapper Hell
等問題,那么我們就需要有一種簡單直接的代碼復用方式,函數,將可復用邏輯抽離成函數應該是最直接、成本最低的代碼復用方式,但對於狀態邏輯,仍然需要通過一些抽象模式(如Observable
)才能實現復用,這正是Hooks
的思路,將函數作為最小的代碼復用單元,同時內置一些模式以簡化狀態邏輯的復用。比起上面提到的其它方案,Hooks
讓組件內邏輯復用不再與組件復用捆綁在一起,是真正在從下層去嘗試解決(組件間)細粒度邏輯的復用問題此外,這種聲明式邏輯復用方案將組件間的顯式數據流與組合思想進一步延伸到了組件內。
對於使用React Hooks
的動機,官方解釋如下:
Hooks
解決了我們在過去五年來編寫和維護react
遇到的各種看似不相關的問題,不論你是否正在學習react
,每天都在使用它,甚至是你只是在使用一些與React
具有相似組件模型的框架,你或多或少都會注意到這些問題。
跨組件復用含狀態的邏輯stateful logic
十分困難:
React
沒有提供一種將復用行為綁定attach
到組件的方法,比如將其連接到store
,類似redux
這類狀態管理庫的connect
方法,如果您已經使用React
一段時間,您可能熟悉通過render props
和higher-order
高階組件等模式,來試圖解決這些問題,但是這些模式要求您在使用它們時重構組件,這可能很麻煩並且使代碼難以為繼,使用Hooks
,您可以從組件中提取有狀態的邏輯,以便可以獨立測試並重復使用,如果你在React DevTools
中看到一個典型的React
應用程序,你可能會發現一個由包含 providers, consumers
消費者,higher-order
高階組件,render props
和其他抽象層的組件組成的包裝器地獄,雖然我們可以在DevTools
中過濾它們,但這反應出一個更深層次的問題:React
需要一個更好的原生方法來共享stateful logic
。使用Hooks
,你可以把含有state
的邏輯從組件中提取抽象出來,以便於獨立測試和復用,同時,Hooks
允許您在不更改組件結構的情況下重用有狀態的邏輯,這樣就可以輕松地在許多組件之間或與社區共享Hook
。
復雜的組件變得難以理解:
我們往往不得不維護一個在開始十分簡單,但卻慢慢演變成了一個無法管理的stateful logic
含有state
邏輯的混亂的和一堆含有副作用的組件,隨着開發的深入它們會變得越來越大、越來越混亂,各種邏輯在組件中散落的到處都是,每個生命周期鈎子中都包含了一堆互不相關的邏輯。比如,我們的組件可能會在componentDidMount
和componentDidUpdate
中執行一些數據拉取的工作,但是在相同的componentDidMount
方法可能還包含一些無關邏輯,比如設置事件監聽(之后需要在componentWillUnmount
中清除),一起更改的相互關聯的代碼被拆分,但完全不相關的代碼最終組合在一個方法中,這使得引入錯誤和不一致變得太容易了,最終的結果是強相關的代碼被分離,反而是不相關的代碼被組合在了一起,這顯然會輕易的導致bug
和異常,在許多情況下,我們也不太可能將這些組件分解成更小的組件,因為stateful logic
到處都是,測試他們也很困難,這也是為什么很多人喜歡將React
和狀態管理的庫組合使用的原因之一,但是這通常會引入太多的抽象,要求您在不同的文件之間跳轉,並使得重用組件變得更加困難,為此,Hooks
允許您根據相關的部分(例如設置訂閱或獲取數據)將一個組件拆分為更小的函數,而不是基於生命周期方法強制拆分,您還可以選擇使用reducer
管理組件的本地狀態,以使其更具可預測性。
難以理解的class
:
除了代碼復用和代碼管理會遇到困難外,我們還發現class
是學習React
的一大屏障,你必須去理解JavaScript
中this
的工作方式,這與其他語言存在巨大差異,還不能忘記綁定事件處理器,沒有穩定的語法提案,這些代碼非常冗余,大家可以很好地理解props
、 state
和自頂向下的數據流,但對class
卻一籌莫展,即便在有經驗的React
開發者之間,對於函數組件與class
組件的差異也存在分歧,甚至還要區分兩種組件的使用場景,另外,React
已經發布五年了,我們希望它能在下一個五年也與時俱進,就像Svelte
、Angular
、Glimmer
等其它的庫展示的那樣,組件預編譯會帶來巨大的潛力,尤其是在它不局限於模板的時候。最近,我們一直在使用Prepack
來試驗component folding
,也取得了初步成效,但是我們發現使用class
組件會無意中鼓勵開發者使用一些讓優化措施無效的方案,class
也給目前的工具帶來了一些問題,例如,class
不能很好的壓縮,並且會使熱重載出現不穩定的情況,因此,我們想提供一個使代碼更易於優化的API
,為了解決這些問題,Hook
使你在非class
的情況下可以使用更多的React
特性,從概念上講,React
組件一直更像是函數,而Hook
則擁抱了函數,同時也沒有犧牲React
的精神原則,Hook
提供了問題的解決方案,無需學習復雜的函數式或響應式編程技術。
useState
最簡單的useState
的使用如下https://codesandbox.io/s/fancy-dust-kbd1i?file=/src/App.tsx
。
// App.tsx
import { useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
console.log("refresh");
const addCount = () => setCount(count + 1);
return (
<>
<div>{count}</div>
<button onClick={addCount}>Count++</button>
</>
);
}
當頁面在首次渲染時會render
渲染<App />
函數組件,其實際上是調用App()
方法,得到虛擬DOM
元素,並將其渲染到瀏覽器頁面上,當用戶點擊button
按鈕時會調用addCount
方法,然后再進行一次render
渲染<App />
函數組件,其實際上還是調用了App()
方法,得到一個新的虛擬DOM
元素,然后React
會執行DOM diff
算法,將改變的部分更新到瀏覽器的頁面上。也就是說,實際上每次setCount
都會重新執行這個App()
函數,這個可以通過console.log("refresh")
那一行看到效果,每次點擊按鈕控制台都會打印refresh
。
那么問題來了,頁面首次渲染和進行+1
操作,都會調用App()
函數去執行const [count, setCount] = useState(0);
這行代碼,那它是怎么做到在+ +
操作后,第二次渲染時執行同樣的代碼,卻不對變量n
進行初始化也就是一直為0
,而是拿到n
的最新值。
考慮到上邊這個問題,我們可以簡單實現一個useMyState
函數,上邊在Hooks
為什么稱為Hooks
這個問題上提到了可以勾過來一個函數作用域的問題,那么我們也完全可以實現一個Hooks
去勾過來一個作用域,簡單來說就是在useMyState
里邊保存一個變量,也就是一個閉包里邊保存了這個變量,然后這個變量保存了上次的值,再次調用的時候直接取出這個之前保存的值即可,https://codesandbox.io/s/fancy-dust-kbd1i?file=/src/use-my-state-version-1.ts
。
// index.tsx
import { render } from "react-dom";
import App from "./App";
// 改造一下讓其導出 讓我們能夠強行刷新`<App />`
export const forceRefresh = () => {
console.log("Force fresh <App />");
const rootElement = document.getElementById("root");
render(<App />, rootElement);
};
forceRefresh();
// use-my-state-version-1.ts
import { forceRefresh } from "./index";
let saveState: any = null;
export function useMyState<T>(state: T): [T, (newState: T) => void] {
saveState = saveState || state;
const rtnState: T = saveState;
const setState = (newState: T): void => {
saveState = newState;
forceRefresh();
};
return [rtnState, setState];
}
// App.tsx
import { useMyState } from "./use-my-state-version-1";
import "./styles.css";
export default function App() {
const [count, setCount] = useMyState(0);
console.log("refresh");
const addCount = () => setCount(count + 1);
return (
<>
<div>{count}</div>
<button onClick={addCount}>Count++</button>
</>
);
}
可以在codesandbox
中看到現在已經可以實現點擊按鈕進行++
操作了,而不是無論怎么點擊都是0
,但是上邊的情況太過於簡單,因為只有一個state
,如果使用多個變量,那就需要調用兩次useState
,我們就需要對其進行一下改進了,不然會造成多個變量存在一個saveState
中,這樣會產生沖突覆蓋的問題,改進思路有兩種:1
把做成一個對象,比如saveState = { n:0, m:0 }
,這種方式不太符合需求,因為在使用useStatek
的時候只會傳遞一個初始值參數,不會傳遞名稱; 2
把saveState
做成一個數組,比如saveState:[0, 0]
。實際上React
中是通過類似單鏈表的形式來代替數組的,通過next
按順序串聯所有的hook
,使用數組也是一種類似的操作,因為兩者都依賴於定義Hooks
的順序,https://codesandbox.io/s/fancy-dust-kbd1i?file=/src/use-my-state-version-2.ts
。
// index.tsx
import { render } from "react-dom";
import App from "./App";
// 改造一下讓其導出 讓我們能夠強行刷新`<App />`
export const forceRefresh = () => {
console.log("Force fresh <App />");
const rootElement = document.getElementById("root");
render(<App />, rootElement);
};
forceRefresh();
// use-my-state-version-2.ts
import { forceRefresh } from "./index";
let saveState: any[] = [];
let index: number = 0;
export function useMyState<T>(state: T): [T, (newState: T) => void] {
const curIndex = index;
index++;
saveState[curIndex] = saveState[curIndex] || state;
const rtnState: T = saveState[curIndex];
const setState = (newState: T): void => {
saveState[curIndex] = newState;
index = 0; // 必須在渲染前后將`index`值重置為`0` 不然就無法借助調用順序確定`Hooks`了
forceRefresh();
};
return [rtnState, setState];
}
// App.tsx
import { useMyState } from "./use-my-state-version-2";
import "./styles.css";
export default function App() {
const [count1, setCount1] = useMyState(0);
const [count2, setCount2] = useMyState(0);
console.log("refresh");
const addCount1 = () => setCount1(count1 + 1);
const addCount2 = () => setCount2(count2 + 1);
return (
<>
<div>{count1}</div>
<button onClick={addCount1}>Count1++</button>
<div>{count2}</div>
<button onClick={addCount2}>Count2++</button>
</>
);
}
可以看到已經可以實現在多個State
下的獨立的狀態更新了,那么問題又又來了,<App />
用了saveState
和index
,那其他組件用什么,也就是說多個組件如果解決每個組件獨立的作用域,解決辦法1
每個組件都創建一個saveState
和index
,但是幾個組件在一個文件中又會導致saveState
、index
沖突。解決辦法2
放在組件對應的虛擬節點對象上,React
采用的也是這種方案,將saveState
和index
變量放在組件對應的虛擬節點對象FiberNode
上,在React
中具體實現saveState
叫做memoizedState
,實際上React
中是通過類似單鏈表的形式來代替數組的,通過next
按順序串聯所有的hook
。
可以看出useState
是強依賴於定義的順序的,useState
數組中保存的順序非常重要在執行函數組件的時候可以通過下標的自增獲取對應的state
值,由於是通過順序獲取的,這將會強制要求你不允許更改useState
的順序,例如使用條件判斷是否執行useState
這樣會導致按順序獲取到的值與預期的值不同,這個問題也出現在了React.useState
自己身上,因此React
是不允許你使用條件判斷去控制函數組件中的useState
的順序的,這會導致獲取到的值混亂,類似於下邊的代碼則會拋出異常。
const App = () => {
let state;
if(true){
[state, setState] = React.useState(0);
}
return (
<div>{state}</div>
)
}
<!-- React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks-->
這里當然只是對於useState
的簡單實現,對於React
真正的實現可以參考packages/react-reconciler/src/ReactFiberHooks.js
,當前的React
版本是16.10.2
,也可以簡略看一下相關的type
。
type Hooks = {
memoizedState: any, // 指向當前渲染節點`Fiber` 上一次完整更新之后的最終狀態值
baseState: any, // 初始化`initialState` 已經每次`dispatch`之后`newState`
baseUpdate: Update<any> | null, // 當前需要更新的`Update` 每次更新完之后會賦值上一個`update` 方便`react`在渲染錯誤的邊緣數據回溯
queue: UpdateQueue<any> | null, // 緩存的更新隊列 存儲多次更新行為
next: Hook | null, // `link`到下一個`hooks` 通過`next`串聯所有`hooks`
}
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://juejin.cn/post/6963559556366467102
https://juejin.cn/post/6944908787375734791
https://juejin.cn/post/6844903990958784526
https://juejin.cn/post/6865473218414247944
https://juejin.cn/post/6844903999083118606
https://github.com/brickspert/blog/issues/26
https://react.docschina.org/docs/hooks-state.html
https://jelly.jd.com/article/61aed4a97f05d46ce6b791f4
https://blog.csdn.net/Marker__/article/details/105593118
https://www.ruanyifeng.com/blog/2019/09/react-hooks.html
https://react.docschina.org/docs/hooks-faq.html#how-does-react-associate-hook-calls-with-components