Hooks中的useState


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM