[React Hooks長文總結系列一]初出茅廬,狀態與反作用

寫在開頭

React Hooks在個人上一個項目中獲得了充分的使用,對於這個項目來講,咱們跳過傳統的類組件直接過渡到函數組件,確實是一個不小的挑戰。在項目開發過程當中也發現項目中的其餘小夥伴(包括我本身)有時候會存在使用不當的狀況,所以對官方的幾個鉤子函數作一個較爲全面的總結。vue

函數式組件出現的緣由

爲何會出現函數式組件,由於傳統的類組件確實有很多缺點:react

  • 類組件中的 this 指向有點繞
  • 經過選項去組織代碼,在組件比較大的時候會很痛苦,由於類組件天生分離,不符合內聚性原則
  • 組件複用不方便,尤爲是 mixin,很容易帶來數據來源指向不清楚的問題

函數式組件竟然「有狀態了」

咱們知道,在過去,函數式組件被稱做「傻瓜組件」,由於它並不具備自身的狀態,一般被用來作一些渲染視圖的工做,即UI = render(props)。這是一個純粹的輸入輸出模型,無任何反作用。可是React Hooks的出現,讓函數式組件擁有自身的狀態成爲了可能。web

函數式組件在運行過程當中會被調用不少次,假如咱們將狀態保存在函數體裏面,毫無疑問是不可行的。由於函數是一種「用完即銷燬」的東西。api

這正是是Hooks所作的事情:將一個函數組件的狀態保存在函數外面。準確來講,是這個函數組件對應的Hooks鏈表。當函數式組件須要用到該狀態的時候,經過Hooks這一鉤子將狀態從函數體外部「鉤進來」。數組

函數式組件其實也有「生命週期」

函數式組件的生命週期能夠分爲如下三部分:async

初次渲染(first-render) ---> 重渲染 (re-render) ---> 銷燬(destroy編輯器

當咱們第一次使用函數式組件的時候,會觸發初次渲染(first-render);若其 props 改變,就會調用該 render 函數,觸發重渲染(re-render)。函數

每一次的渲染,都是獨立的。這正是函數式組件的美妙之處。性能

那麼react如何決定要不要調用 render 函數來更新 UI 視圖呢?這取決於data有沒有更新。從整個組件樹來看,data指的是整個組件的state;從具體到某個功能組件來看,data也能夠被認爲是props和自身state的結合體。fetch

render 的執行取決於 data 變化,而 data 中的 state 數據是保存在鏈表中的。

鏈表的特性是啥?就是每一個元素都有一個next指針指向下一個元素,一環扣一環關聯起來。因此爲何 hooks 不能用在條件判斷/循環/嵌套中,由於這些都不能保證每次渲染時讀取 hooks 鏈表的順序是徹底一致的。尤爲對於狀態讀取來講,讀取順序和初次渲染鏈表記錄的順序不一致,會直接致使一些 useState 鉤子讀取到錯誤的狀態值。

useSate,狀態保存之處

用法

const [count, setCount] = useState(0);

原理

首先,useState 會生成一個狀態和修改狀態的函數。這個狀態會保存在函數式組件外面,每次重渲染時,這一次渲染都會去外面把這個狀態鉤回來,讀取成常量並寫進該次渲染中。

經過調用修改狀態的函數,會觸發重渲染。到這裏咱們總結:props 的改變和 setState 的調用,都會觸發 re-render

因爲每次渲染都是獨立的,因此每次渲染都會讀到一個獨立的狀態值,這個狀態值,就是經過鉤子鉤到的 state 並讀取到的常量。

這就是所謂的capture value特性,每次的渲染都是獨立的,每次渲染的狀態其實都只是常量罷了。

深刻本質

讓咱們看深刻一下本質,看看 useStatere-render 到底如何關聯起來:

  1. 函數式組件初次渲染,一個個的 useState 依次執行,生成hooks鏈表,裏面記錄了每一個 state 的初始值和對應的 setter 函數
  2. 這個鏈表會掛在這個函數式組件的外面,能夠被 useState 或相應 setter 訪問
  3. 當某個時刻調用了 setSetter,將會直接改變這個hooks鏈表
  4. hooks鏈表其實就是這個函數式組件的狀態表,它的改變等效於狀態改變,會引發函數式組件重渲染
  5. 這個函數式組件重渲染,執行到 useState 時,由於初次執行已經掛載過一個 hooks 鏈表了,這個時候就會直接讀取鏈表的相應值

這也就是爲何叫useState,而不是createState

useRef,DOM訪問與外部狀態保存

useRef有啥用

useRef主要有兩個做用:

  • 用來訪問DOM;
  • 用來保存變量到當前函數式組件外部。

訪問DOM

咱們先來看看前者怎麼用吧:

const inputRef = useRef(null);

const handleClick = () => {
  inputRef.current?.focus();
}

return (
  <input ref={inputRef} />
  <button onClick={handleClick}>點擊</button>
)

這樣就能夠方便地訪問DOM節點。

保存可變值

前面咱們提到,useState能夠方便地保存狀態值,可是因爲函數式組件的capture value特性,使得咱們並不能以一種比較方便的形式獲取到更改後的狀態值。

const [num, setNum] = useState(0);

const increaseNum = () => {
    setNum(prev => prev + 1);
    console.log(num); // 打印的仍然是舊值,由於num在這一幀被常量化了
}

useRef將會建立一個ref對象,並把這個ref對象保存在函數式組件外部,這樣的好處在於:

  1. 獨立於capture value以外存儲,不用擔憂得到過期變量的問題;
  2. 能夠同步修改狀態。

咱們試驗以下:

const numRef = useRef(0);

const increaseNum = () => {
    numRef.current += 1;
    console.log(numRef.current); // 能獲取最新值
}

可是要注意⚠️:因爲引用沒變,上述操做並不會引發函數式組件的重渲染。 這是一個很容易引發錯誤的地方!

useEffect,生命週期與觀察者

用法及建議

useEffect 的模型十分之簡潔,以下:

useEffect(effectFn, deps);

useEffect 能夠模擬舊時代的三個生命週期:componentDidMountshouldComponentUpdatecomponentWillUnmount,至關於三個生命週期合併爲一個 api。

所謂shouldComponentUpdate,其實就是去除deps依賴數組,如此一來這個反作用的 effectFn 會在首次渲染以後和每次重渲染以後執行,至關於模擬了 shouldComponentUpdate 這一輩子命週期,以下:

useEffect(() => {
  // xxx
});

而所謂componentDidMount,則是傳入一個空數組做爲依賴,由於當有 deps 數組時,裏面 effectFn 是否執行取決於 deps 數組內的數據是否變化,空數組內無數據,因此對比天然也就無變化,使用以下:

useEffect(() => {
  // xxx
}, []);

componentWillUnmount,則是在effectFn中返回一個清除函數,以下:

useEffect(() => {
  // 執行反作用
  // ...
  return () => {
    // 清除上面的反作用
    // ...
  };
}, []);

此外咱們應該始終遵循一個原則:那就是不要對 deps 依賴撒謊。不然會引起一系列 bug。固然編輯器的 linter 也不會容許咱們這樣作,這一點很是關鍵。

原理

effectFn 就是當依賴變化時執行的反作用函數,這裏的反作用,並非一個貶義詞,而是一箇中性詞。

函數內部與外部發生的任何交互都算反作用,好比打印個日誌、開啓一個定時器,發一個請求,讀取全局變量等等等等。

好,如今這個 effectFn 能夠返回一個清理函數cleanUp,用於清除這個反作用。典型的清理函數,如:clearIntervalclearTimeout,如:

useEffect(() => {
  const timer = setTimeout(() => console.log("over"), 1000);
  return () => clearTimout(timer);
});

useEffect 實際上是每次渲染完成後都會執行,可是 effectFn 是否執行,就要看依賴有沒有變化了。執行 useEffect 的時候,會拿此次渲染的依賴跟上次渲染的對應依賴對比,若是沒變化,就不執行 effectFn,若是有變化,才執行 effectFn

若是連依賴都沒有,那 react 就認爲每次都有變化,每次運行 useEffect 必運行 effectFn

useEffect 有典型的三大特色:

  • 會在每次渲染完成後才執行,不會阻塞渲染,從而提升性能
  • 在每次運行 effectFn 以前,要把前一次運行 effectFn 遺留的cleanUp函數執行掉(若是有的話)
  • 在組件銷燬時,會把最後一次運行effectFn 遺留的 cleanUp 函數執行掉。

deps 數組裏面的各個依賴與上次的依賴是否相同,須要經過Object.is來比較,好比:

Object.is(2222); // true

Object.is([], []); // false

這樣就會有一個隱患,當 deps 數組裏面的子元素爲引用類型的時候,每次對比都會是false,從而執行effectFn。由於 Object.is 對比引用類型的時候,比較的是兩個指針是否指向堆內存中的同一個地址。

useEffect 的執行機制,是在初次渲染時,執行到 useEffect 就將內部的 effectFn 放到兩個地方:一個是 hooks 鏈表中,另一個則是EffectList 隊列中。在渲染完成後,會依次執行 EffectList 裏面的 effectFn 集合。

因此,說白了,要不要 re-render,徹底取決於鏈表裏面的東西有沒有變化。

細節

不一樣於 vue 裏面有async mounted,在 useEffect 裏面的 effectFn,應該始終堅持一個原則:要麼不返回,要麼返回一個 cleanUp 清除函數。像下面這樣寫是不行的:

// 錯誤的用法❌
useEffect(async () => {
  const response = await fetch("...");
  // ...
});

另外咱們很容易發現:咱們並不須要把 useState 返回的第二個 Setter 函數做爲useEffect 的依賴。實際上,React 內部已經對 Setter 函數作了 Memoization 處理,所以每次渲染拿到的 Setter 函數都是徹底同樣的,不須要把這個Setter函數放到deps數組裏面。

相關文章
相關標籤/搜索