摘要:return res。< div >當前點擊了{num}次 div >。

點擊關注“ 有贊coder

獲取更多技術乾貨哦~

作者:蘇木

團隊:增長中心

前言:React Hooks被越來越多的人認可,整個社區都以積極的態度去擁抱它。在最近的一段時間筆者也開始在一些項目中嘗試去使用React Hooks。原本以爲React Hooks很簡單,和類組件差不多,看看API就能用起來了。結果在使用中遇到了各種各樣的坑,通過閱讀React Hooks相關的文章發現React Hooks和類組件有很多不同。由此,想和大家做一些分享。

如果要在項目中使用React Hooks,強烈推薦先安裝 eslint-plugin-react-hooks (由React官方發佈)。在很多時候,這個eslint插件在我們使用React Hooks的過程中,會幫我們避免很多問題。

本文主要講以下內容:

  1. 函數式組件和類組件的不同

  2. React Hooks依賴數組的工作方式

  3. 如何在React Hooks中獲取數據

一、函數式組件和類組件的不同

React Hooks由於是函數式組件,在異步操作或者使用useCallBack、useEffect、useMemo等API時會形成閉包。

先看一下以下例子。在點擊了 展示現在的值 按鈕三秒後,會 alert 點擊次數:

function Demo() {

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

const handleClick = () => {

setTimeout( () => {

alert(num);

}, 3000 );

};

return (

< div >

< div >當前點擊了{num}次</ div >

< button onClick={() => { setNum(num + 1) }}>點我</ button >

< button onClick={handleClick}>展示現在的值</ button >

</

div

);

};

我們按照下面的步驟去操作:

  • 點擊 num 到3
  • 點擊 展示現在的值 按鈕
  • 在定時器回調觸發之前,點擊增加 num 到5。

可以猜一下 alert 會彈出什麼?

分割線

其最後彈出的數據是3。

爲什麼會出現這樣的情況,最後的 num 不是應該是5嗎?

上面例子中, num 僅是一個數字而已。 它不是神奇的“data binding”, “watcher”, “proxy”,或者其他任何東西。它就是一個普通的數字像下面這個一樣:

const num = 0 ;

// ...

setTimeout( () => {

alert(num);

}, 3000 );

// ...

我們組件第一次渲染的時候,從 useState() 拿到 num 的初始值爲0,當我們調用 setNum(1) ,React會再次渲染組件,這一次 num 是1。如此等等:

// 第一次渲染

function Demo() {

const num = 0 ; // 從useState()獲取

// ...

setTimeout( () => {

alert(num);

}, 3000 );

// ...

}

// 在點擊了一次按鈕之後

function Demo() {

const num = 1 ; // 從useState()獲取

// ...

setTimeout( () => {

alert(num);

}, 3000 );

// ...

}

// 又一次點擊按鈕之後

function Demo() {

const num = 2 ; // 從useState()獲取

// ...

setTimeout( () => {

alert(num);

}, 3000 );

// ...

}

在我們更新狀態之後,React會重新渲染組件。每一次渲染都能拿到獨立的 num 狀態,這個狀態值是函數中的一個常量。

所以在 num 爲3時,我們點擊了 展示現在的值 按鈕,就相當於:

function Demo() {

// ...

setTimeout( () => {

alert( 3 );

}, 3000 )

// ...

}

即便num的值被點擊到了5。但是觸發點擊事件時,捕獲到的 num 值爲3。

上面的功能,我們嘗試用類組件實現一遍:

class Demo extends Component {

state = {

num : 0 ,

}

handleClick = () => {

setTimeout( () => {

alert( this .state.num);

}, 3000 );

}

render() {

const { num } = this .state;

return (

< div >

< p >當前點擊了{num}次</ p >

< button onClick={() => { this.setState({ num: num + 1 }) }}>點擊</ button >

< button onClick={this.handleClick}>展示現在的值</ button >

</

div

);

}

};

我們按照之前同樣的步驟去操作:

  • 點擊 num 到3
  • 點擊 展示現在的值 按鈕
  • 在定時器回調觸發之前,點擊增加 num 到5

這一次彈出的數據是5。

爲什麼同樣的例子在類組件會有這樣的表現呢?

我們可以仔細看一下handleClick方法:

handleClick = () => {

setTimeout( () => {

alert( this .state.num);

}, 3000 )

}

這個類方法從this.state.num中讀取數據,在React中state是不可變的。然而,this是可變的。

通過類組件的 this ,我們可以獲取到最新的state和props。

所以如果在用戶再點擊了 展示現在的值 按鈕的情況下我們對 點擊 按鈕又點擊了幾次, this.state 將會改變。 handleClick 方法從一個“過於新”的 state 中得到了 num

這樣就引起了一個問題, 如果說我們UI在概念上是當前應用狀態的一個函數,那麼事件處理程序和視覺輸出都應該是渲染結果的一部分。我們的事件處理程序應該有一個特定的props和state

然而在類組件中,我們通過 this.state 讀取的數據並不能保證其是一個特定的state。 handleClick 事件處理程序並沒有與任何一個特定的渲染綁定在一起。

從上面的例子,我們可以看出React Hooks在某一個特定渲染中state和props是與其相綁定的,然而類組件並不是。

二、React Hooks依賴數組的工作方式

在React Hooks提供的很多API都有遵循依賴數組的工作方式,比如useCallBack、useEffect、useMemo等等。

使用了這類API,其傳入的函數、數據等等都會被緩存。被緩存的內容其依賴的props、state等值就像上面的例子一樣都是“不變”的。只有當依賴數組中的依賴發生變化,它纔會被重新創建,得到最新的props、state。所以在用這類API時我們要特別注意,在依賴數組內一定要填入依賴的props、state等值。

這裏給大家舉一個反例:

function Demo() {

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

const handleClick = useCallback( () => {

setNum(num + 1 );

}, []);

return (

< div >

< p >當前點擊了{num}次</ p >

< button onClick={handleClick}>點擊</ button >

</

div

);

}

useCallback 本質上是添加了一層依賴檢查。當我們函數本身只在需要的時候才改變。

在上面的例子中,我們無論點擊多少次 點擊 按鈕, num 的值始終爲1。這是因爲 useCallback 中的函數被緩存了,其依賴數組爲空數組,傳入其中的函數會被一直緩存。

handleClick 其實一直都是:

const handleClick = () => {

setNum( 0 + 1 );

};

即便函數再次更新, num 的值變爲1,但是React並不知道你的函數中依賴了 num ,需要去更新函數。

唯有在依賴數組中傳入了 num ,React纔會知道你依賴了 num ,在 num 的值改變時,需要更新函數。

function Demo() {

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

const handleClick = useCallback( () => {

setNum(num + 1 );

}, [num]); // 添加依賴num

return (

< div >

< p >當前點擊了{num}次</ p >

< button onClick={handleClick}>點擊</ button >

</

div

);

};

點擊 點擊 按鈕,num的值不斷增加。

(其實這些歸根究底,就是React Hooks會形成閉包)

三、如何在React Hooks中獲取數據

在我們用習慣了類組件模式,我們在用React Hooks中獲取數據時,一般剛開始大家都會這麼寫吧:

function Demo( props ) {

const { query } = props;

const [list, setList] = useState([]);

const fetchData = async () => {

const res = await axios( `/getList?query=${query}` );

setList(res);

};

useEffect( () => {

fetchData(); // 這樣不安全(調用的fetchData函數使用了query)

}, []);

return (

< ul >

{list.map(({ text }) => {

return (

< li key={text}>{ text }</ li >

);

})}

</

ul

);

};

其實這樣是不推薦的一種模式,要記住effect外部的函數使用了哪些props和state很難。這也是爲什麼 通常你會想要在effect內部去聲明它所需要的函數。 這樣就能容易的看出那個effect依賴了組件作用域中的哪些值:

function Demo( props ) {

const { query } = props;

const [list, setList] = useState([]);

useEffect( () => {

const fetchData = async () => {

const res = await axios( `/getList?query=${query}` );

setList(res);

};

fetchData();

}, [query]);

return (

< ul >

{list.map(({ text }) => {

return (

< li key={text}>{ text }</ li >

);

})}

</

ul

);

};

但是如果你在不止一個地方用到了這個函數或者別的原因,你無法把一個函數移動到effect內部,還有一些其他辦法:

  • 如果這函數不依賴state、props內部的變量。可以把這個函數移動到你的組件之外。這樣就不用其出現在依賴列表中了。

  • 如果其不依賴state、props。但是依賴內部變量,可以將其在effect之外調用它,並讓effect依賴於它的返回值。

  • 萬不得已的情況下,你可以把函數加入effect的依賴項,但把它的定義包裹進 useCallBack 。這就確保了它不隨渲染而改變,除非它自身的依賴發生了改變。

另外一方面,業務一旦變的複雜,在React Hooks中用類組件那種方式獲取數據也會有別的問題。

我們做這樣一個假設,一個請求入參依賴於兩個狀態分別是query和id。然而id的值需要異步獲取(只要獲取一次,就可以在這個組件卸載之前一直用),query的值從props傳入:

function Demo( props ) {

const { query } = props;

const [id, setId] = useState();

const [list, setList] = useState([]);

const fetchData = async (newId) => {

const myId = newId || id;

if (!myId) {

return ;

}

const res = await axios( `/getList?id=${myId}&query=${query}` );

setList(res);

};

const fetchId = async () => {

const res = await axios( '/getId' );

return res;

};

useEffect( () => {

fetchId().then( id => {

setId(id);

fetchData(id);

});

}, []);

useEffect( () => {

fetchData();

}, [query]);

return (

< ul >

{list.map(({ text }) => {

return (

< li key={text}>{ text }</ li >

);

})}

</

ul

);

};

在這裏,當我們的依賴的 query 在異步獲取 id 期間變了,最後請求的入參,其 query 將會用之前的值。(引起這個問題的原因還是閉包,這裏就不再複述了)

對於從後端獲取數據,我們應該用React Hooks的方式去獲取。這是一種關注數據流和同步思維的方式。

對於剛纔這個例子,我們可以這樣解決:

function Demo( props ) {

const { query } = props;

const [id, setId] = useState();

const [list, setList] = useState([]);

useEffect( () => {

const fetchId = async () => {

const res = await axios( '/getId' );

setId(res);

};

fetchId();

}, []);

useEffect( () => {

const fetchData = async () => {

const res = await axios( `/getList?id=${id}&query=${query}` );

setList(res);

};

if (id) {

fetchData();

}

}, [id, query]);

return (

< ul >

{list.map(({ text }) => {

return (

< li key={text}>{ text }</ li >

);

})}

</

ul

);

}

一方面這種方式可以讓我們的代碼更加清晰,一眼就能看明白獲取這個接口的數據依賴了哪些state、props,讓我們更多的去關注 數據流的改變 。另外一方面也避免了 閉包可能會引起的問題

但是同步思維的方式也會有一些坑,比如這樣的場景,有一個列表,這個列表可以通過子元素的按鈕增加數據:

function Children( props ) {

const { fetchData } = props;

return (

< div >

< button onClick={() => { fetchData(); }}>點擊</ button >

</

div

);

};

function Demo() {

const [list, setList] = useState([]);

const fetchData = useCallback( async () => {

const res = await axios( `/getList` );

setList([...list, ...res]);

}, [list]);

useEffect( () => {

fetchData();

}, [fetchData]);

return (

< div >

< ul >

{list.map(({ text }) => {

return (

< li key={text}>{ text }</ li >

);

})}

</ ul >

< Children fetchData={fetchData} />

</ div >

);

};

這種場景下,會一直加載數據,造成死循環。

每次調用 fetchData 函數會更新 list list 更新後 fetchData 函數就會被更新。 fetchData 更新後 useEffect 會被調用, useEffect 中又調用了 fetchData 函數。 fetchData 被調用導致 list 更新...

當出現這種 根據前一個狀態更新狀態 的時候,我們可以用useReducer去替換useState:

function Children( props ) {

const { fetchData } = props;

return (

< div >

< button onClick={() => { fetchData(); }}>點擊</ button >

</

div

);

};

const initialList = [];

function reducer( state, action ) {

switch (action.type) {

case 'increment' :

return [...state, ...action.payload];

default :

throw new Error ();

}

}

export default function Demo() {

const [list, dispatch] = useReducer(reducer, initialList);

const fetchData = useCallback( async () => {

const res = await axios( `/getList` );

dispatch({

type : 'increment' ,

payload : res

});

}, []);

useEffect( () => {

fetchData();

}, [fetchData]);

return (

< div >

< ul >

{list.map(({ text }) => {

return (

< li key={text}>{ text }</ li >

);

})}

</ ul >

< Children fetchData={fetchData} />

</ div >

);

};

React會保證 dispatch 在組件的聲明週期內保持不變。所以上面的例子中不需要依賴 dispatch

用了 useReducer 我們就可以移除 list 依賴。不會再出現死循環的情況。

通過dispatch了一個action來描述發生了什麼。這使得我們的 fetchData 函數和 list 狀態解耦。我們的 fetchData 函數不再關心怎麼更新狀態,它只負責告訴我們發生了什麼。更新的邏輯全都交由reducer去統一處理。

(我們使用函數式更新也能解決這個問題,但是更推薦使用useReducer)

在某些場景下 useReducer 會比useState更適用。例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於之前的state等。並且,使用  useReducer  還能給那些會觸發深更新的組件做性能優化,因爲你可以向子組件傳遞  dispatch  而不是回調函數。

如果大家遇到其它的一些複雜場景,用上面介紹的方法無法解決。那就試試用useRef吧。

文章如有疏漏、錯誤歡迎批評指正。

擴展閱讀

  1. 使用 Puppeteer 搭建統一海報渲染服務

  2. Vant 2.0 發佈: 持之以恆,不亂節奏

  3. React 中 getDerivedStateFromProps 的三個場景

  4. 用函數式的方式思考——遞歸

  5. Vant Weapp 1.0 正式版發佈

  6. 如何搭建一個高可用的服務端渲染工程

  7. 有讚美業店鋪裝修前端解決方案

Vol.303

相關文章