Hooks中的useState

WindrunnerMax發表於2021-12-21

Hooks中的useState

React的資料是自頂向下單向流動的,即從父元件到子元件中,元件的資料儲存在propsstate中,實際上在任何應用中,資料都是必不可少的,我們需要直接的改變頁面上一塊的區域來使得檢視的重新整理,或者間接地改變其他地方的資料,在React中就使用propsstate兩個屬性儲存資料。state的主要作用是用於元件儲存、控制、修改自己的可變狀態,state在元件內部初始化,可以被元件自身修改,而外部不能訪問也不能修改,可以認為state是一個區域性的、只能被元件自身控制的資料來源,而對於React函式元件,useState即是用來管理自身狀態hooks函式。

Hooks

對於React Hooks這個Hooks的意思,阮一峰大佬解釋說,React Hooks的意思是,元件儘量寫成純函式,如果需要外部功能和副作用,就用鉤子把外部程式碼鉤進來,React Hooks就是那些鉤子。我覺得這個解釋非常到位了,拿useState來說,在寫函式元件的時候是將這個函式勾過來使用,而在這個函式內部是儲存著一些內部的作用域變數的,也就是常說的閉包,所以Hooks也可以理解為將另一個作用域變數以及函式邏輯勾過來在當前作用域使用。
對於為什麼要用React Hooks,總結來說還是為了元件複用,特別在更加細粒度的元件複用方面React Hooks表現更好。在React中程式碼複用的解決方案層出不窮,但是整體來說程式碼複用還是很複雜的,這其中很大一部分原因在於細粒度程式碼複用不應該與元件複用捆綁在一起,HOCRender Props 等基於元件組合的方案,相當於先把要複用的邏輯包裝成元件,再利用元件複用機制實現邏輯複用,自然就受限於元件複用,因而出現擴充套件能力受限、Ref 隔斷、Wrapper Hell等問題,那麼我們就需要有一種簡單直接的程式碼複用方式,函式,將可複用邏輯抽離成函式應該是最直接、成本最低的程式碼複用方式,但對於狀態邏輯,仍然需要通過一些抽象模式(如Observable)才能實現複用,這正是Hooks的思路,將函式作為最小的程式碼複用單元,同時內建一些模式以簡化狀態邏輯的複用。比起上面提到的其它方案,Hooks讓元件內邏輯複用不再與元件複用捆綁在一起,是真正在從下層去嘗試解決(元件間)細粒度邏輯的複用問題此外,這種宣告式邏輯複用方案將元件間的顯式資料流與組合思想進一步延伸到了元件內。

對於使用React Hooks的動機,官方解釋如下:
Hooks解決了我們在過去五年來編寫和維護react遇到的各種看似不相關的問題,不論你是否正在學習react,每天都在使用它,甚至是你只是在使用一些與React具有相似元件模型的框架,你或多或少都會注意到這些問題。
跨元件複用含狀態的邏輯stateful logic十分困難:
React沒有提供一種將複用行為繫結attach到元件的方法,比如將其連線到store,類似redux這類狀態管理庫的connect方法,如果您已經使用React一段時間,您可能熟悉通過render propshigher-order高階元件等模式,來試圖解決這些問題,但是這些模式要求您在使用它們時重構元件,這可能很麻煩並且使程式碼難以為繼,使用Hooks,您可以從元件中提取有狀態的邏輯,以便可以獨立測試並重復使用,如果你在React DevTools中看到一個典型的React應用程式,你可能會發現一個由包含 providers, consumers消費者,higher-order高階元件,render props和其他抽象層的元件組成的包裝器地獄,雖然我們可以在DevTools中過濾它們,但這反應出一個更深層次的問題:React需要一個更好的原生方法來共享stateful logic。使用Hooks,你可以把含有state的邏輯從元件中提取抽象出來,以便於獨立測試和複用,同時,Hooks允許您在不更改元件結構的情況下重用有狀態的邏輯,這樣就可以輕鬆地在許多元件之間或與社群共享Hook
複雜的元件變得難以理解:
我們往往不得不維護一個在開始十分簡單,但卻慢慢演變成了一個無法管理的stateful logic含有state邏輯的混亂的和一堆含有副作用的元件,隨著開發的深入它們會變得越來越大、越來越混亂,各種邏輯在元件中散落的到處都是,每個生命週期鉤子中都包含了一堆互不相關的邏輯。比如,我們的元件可能會在componentDidMountcomponentDidUpdate中執行一些資料拉取的工作,但是在相同的componentDidMount方法可能還包含一些無關邏輯,比如設定事件監聽(之後需要在componentWillUnmount中清除),一起更改的相互關聯的程式碼被拆分,但完全不相關的程式碼最終組合在一個方法中,這使得引入錯誤和不一致變得太容易了,最終的結果是強相關的程式碼被分離,反而是不相關的程式碼被組合在了一起,這顯然會輕易的導致bug和異常,在許多情況下,我們也不太可能將這些元件分解成更小的元件,因為stateful logic到處都是,測試他們也很困難,這也是為什麼很多人喜歡將React和狀態管理的庫組合使用的原因之一,但是這通常會引入太多的抽象,要求您在不同的檔案之間跳轉,並使得重用元件變得更加困難,為此,Hooks允許您根據相關的部分(例如設定訂閱或獲取資料)將一個元件拆分為更小的函式,而不是基於生命週期方法強制拆分,您還可以選擇使用reducer管理元件的本地狀態,以使其更具可預測性。
難以理解的class:
除了程式碼複用和程式碼管理會遇到困難外,我們還發現class是學習React的一大屏障,你必須去理解JavaScriptthis的工作方式,這與其他語言存在巨大差異,還不能忘記繫結事件處理器,沒有穩定的語法提案,這些程式碼非常冗餘,大家可以很好地理解props state和自頂向下的資料流,但對class卻一籌莫展,即便在有經驗的React開發者之間,對於函式元件與class元件的差異也存在分歧,甚至還要區分兩種元件的使用場景,另外,React已經發布五年了,我們希望它能在下一個五年也與時俱進,就像SvelteAngularGlimmer等其它的庫展示的那樣,元件預編譯會帶來巨大的潛力,尤其是在它不侷限於模板的時候。最近,我們一直在使用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的時候只會傳遞一個初始值引數,不會傳遞名稱; 2saveState做成一個陣列,比如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 />用了saveStateindex,那其他元件用什麼,也就是說多個元件如果解決每個元件獨立的作用域,解決辦法1每個元件都建立一個saveStateindex,但是幾個元件在一個檔案中又會導致saveStateindex衝突。解決辦法2放在元件對應的虛擬節點物件上,React採用的也是這種方案,將saveStateindex變數放在元件對應的虛擬節點物件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

相關文章