Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
前言:hooks
出了已有大半年了,關注的公眾號也大都推了關于hooks
的文章,可是因為工作中一直用的是class
,所以一直沒有用,也沒有學,趁著這段時間項目不那么干,將hooks
系統性的學習一下,并做筆記記錄一下。
<span id="目錄">目錄</span>
- useState
- userEffect
- userEffect實現componentWillUnmont
- 父子組件傳值
- userContext
- userReducer
- useReducer替代Redux案例
- useMemo
- useRef
- useCallBack
- 自定義函數
一:<span id="useState">useState</span>
在組件中,我們難免使用state
來進行數據的實時響應,這是react
框架的一大特性,只需更改state
,組件就會重新渲染,試圖也會響應更新。<br />
不同于react
在class
可以直接定義state
,或者是在constructor
中使用this.state
來直接定義state
值,在hooks
中使用state
需要useState
函數,如下:
import React, { useState, useEffect } from 'react';
function Hooks() {
const [count, setCount] = useState(0);
const [age] = useState(16);
useEffect(() => {
console.log(count);
});
return (
<div>
<p>小女子芳年{age}</p>
<p>計數器目前值為{count}</p>
<button type="button" onClick={() => { setCount(count + 1); }}>點擊+1</button>
<button type="button" onClick={() => { setCount(count - 1); }}>點擊-1</button>
</div>
);
}
export default Hooks;
在上面的例子中,我們使用了useState
定義了兩個state
變量,count
和age
,其中定義count
的時候還定義了setCount
,就是用來改變count
值的函數。在class
類中,改變state
是使用setState
函數,而在hooks
中是定義變量的同時定義一個改變變量的函數。<br />
userState
是一個方法,方法返回值為當前state以及更新state的函數,所以,在上面的例子中,我們用const [count, setCount] = useState(0);
將count和setCount解構出來,而userState
方法的參數就是state的初始值。當然count和與之對應的改變函數名稱并不一定非得是setCount,名稱可以隨便起,只要是一塊解構出來的即可。<br />
在class
組件中,我們可以用setState
一次更改多個state
值而只渲染一次,同樣的,在hooks
中,我們調用多個改變state
的方法,也只是渲染一次。
二:<span id="userEffect">userEffect</span> 回目錄
在class
組件中,有生命周期的概念,最常用的,我們通常會在componentDidMount
這個生命周期中做數據請求,偶爾,我們也會用一些其它的生命周期,像是componentDidUpdata
,componentWillReceiveProps
等。在hooks
中,沒有生命周期的概念,但是,有副作用函數useEffect。<br />
使用useEffect
,和使用useState
相同,必須得先引入import React, { useState, useEffect } from 'react';
,默認情況下,useEffect會在第一次和每次更新之后都會執行,useEffect
函數接受兩個參數,第一個參數是一個函數,每次執行的就是函數中的內容,第二個函數是個數組,數組中可選擇性寫state
中的數據,代表只有當數組中的state
發生變化是才執行函數內的語句。如果是個空數組,代表只執行一次,類似于componentDidUpdata
。所以,向后端請求可以寫成下面這種方式:
// 頁面進來只調用一次
useEffect(()=>{
axios.get('/getYearMonth').then(res=> {
console.log('getYearMonth',res);
setValues(oldValues => ({
...oldValues,
fileList:res.data.msg
}));
})
},[]);
effect函數會在瀏覽器完成畫面渲染之后延遲調用<br />
在一個hooks函數中,可以同時存在多個effect函數,所以,當有需求每次更新都執行useEffect中的代碼時,可以用一個useEffect請求數據,用其他的useEffect做另外的事情。只需根據第二個參數即可區別不同作用。
//官方示例性能優化
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新
三:<span id="userEffect實現componentWillUnmount">userEffect實現componentWillUnmount</span> 回目錄
部分情況下,需要在組件卸載是做一些事情,例如移除監聽事件等,在class
組件中,我們可以在componentWillUNmount
這個生命周期中做這些事情,而在hooks
中,我們可以通過useEffect
第一個函數參數中返回一個函數來實現相同效果。
// 官方示例
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
個人示例:
import React, { useState, useEffect } from 'react';
import { Switch, Route, Link } from 'react-router-dom';
function Index() {
useEffect(() => {
console.log('useEffect:come-index');
return () => {
console.log('useEffect:leave-index');
};
}, []);
return <div>這是首頁</div>;
}
function List() {
useEffect(() => {
console.log('useEffect:come-list');
return () => {
console.log('useEffect:leave-list');
};
}, []);
return <div>這是列表頁</div>;
}
function HooksEffect() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
return () => {
console.log('-------------------');
};
}, [count]);
return (
<div>
<p>你點擊了{count}次</p>
<button
type="submit"
onClick={() => { setCount(count + 1); }}
>
點擊+1
</button>
<ul>
<li><Link to="/index">首頁</Link></li>
<li><Link to="/list">列表</Link></li>
</ul>
<Switch>
<Route path="/index" exact component={Index} />
<Route path="/list" component={List} />
</Switch>
</div>
);
}
export default HooksEffect;
在上面的例子中,全部用了清除副作用的return 函數,其中,hooksEffect
組件為父組件,list
和index
為子組件,如果在子組件的useEffect
中不使用第二個參數空數組,則父組件的每次更新都會引發子組件的useEffect
的調用,在父組件的useEffect
函數中,第二個參數數組中為count
,代表每次count
的變化都會引起useEffect
函數的觸發以及返回函數的調用。
四:<span id="父子組件傳值">父子組件傳值</span> 回目錄
父子組件傳值在實際開發中是必不可少的,在class
組件中,我們可以直接給子組件添加屬性,然后在子組件通過props
即可獲取到父組件的值。但是在hooks
中,組件都是函數,沒有props
,所以不能用相同的方式傳值<br />
在hooks
中,組件都是函數,所以我們可以通過參數的方式進行傳值,也可以通過content
來進行傳值,這一小節主要是講通過參數方式進行傳值,案例如下:
import React, { useState } from 'react';
function Show({ count, age, clear }) {
return (
<div>
數量:{count}
年齡:{age}
<button
type="button"
onClick={() => { clear(); }}
>
復原
</button>
</div>
);
}
function HooksContext() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你點擊了{count}次</p>
<button
type="button"
onClick={() => { setCnt(count + 1);
setAge(age + 1); }}
>
點擊+1
</button>
<Show count={count} age={age} clear={clear} />
</div>
);
}
export default HooksContext;
在上面的案例中,通過給Show組件屬性賦值,然后在Show函數組件中以解構參數的方式獲取父組件的值。這種傳值方式和類組件本質上還是一樣的。
五:<span id="userContext">userContext</span> 回目錄
使用userContext
,不僅可以實現父子組件傳值,還可以跨越多個層級進行傳值,例如父組件可以給孫子組件甚至重孫子組件進行直接傳值等,redux
全局狀態管理本質上也是對content
的一種應用。<br />
在hooks
中使用content
,需要使用createContext
,useContext
,廢話不多說,直接示例展示用法
// context.js 新建一個context
import { createContext } from 'react';
const ShowContext = createContext('aaa');
export default ShowContext;
// HooksContext.jsx 父組件,提供context
import React, { useState } from 'react';
import Show from './Show.jsx';
import ShowContext from './context';
function HooksContext() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你點擊了{count}次</p>
<button
type="button"
onClick={() => { setCnt(count + 1); setAge(age + 1); }}
>
點擊+1
</button>
<ShowContext.Provider value={{ count, age, clear }}>
<Show />
</ShowContext.Provider>
</div>
);
}
export default HooksContext;
// Show.jsx 子組件,使用context
import React, { useContext } from 'react';
import ShowContext from './context';
function Show() {
const { count, age, clear } = useContext(ShowContext);
return (
<div>
數量:{count}
年齡:{age}
<button
type="button"
onClick={() => { clear(); }}
>
復原
</button>
</div>
);
}
export default Show;
上面是一個完整的使用content
實現父子組件傳值的過程,如果Show
組件下還有子組件,無論多少層,都可以用useContext
直接取到HooksContext
父組件提供的值,而context.js
文件是新建一個context
,新建必須要單獨列出來,否則子組件無法使用useContext
。<br />
content
提供了一種樹狀結構,被Context.Provider
所包裹的所有組件,都可以直接取數據。redux
就是利用了context
的這種特性實現全局狀態管理。在下面的幾小節中,我們會講hooks
中context
搭配useReducer
來實現redux
的功能。
六:<span id="userReducer">userReducer</span> 回目錄
userReducer
是useState
的替代方案,它接收一個形如(state,action) => newState
的reducer
,并返回當前的state
以及其配套的dispatch
方法。
總的來說呢,userReducer
可以接受兩個參數,第一個參數就是和redux
中的reducer
一樣的純函數,第二個參數是state
的初始值,并返回當前state
以及dispatch
。<br />
還是以官方示例的計數器為例
import React, { useReducer } from 'react';
function countReducer(state, action) {
switch (action.type) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
default:
return state;
}
}
function HooksEffect() {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<div>
<p>你點擊了{count}次</p>
<button
type="button"
onClick={() => { dispatch({ type: 'add' }); }}
>
點擊+1
</button>
<button
type="button"
onClick={() => { dispatch({ type: 'minus' }); }}
>
點擊-1
</button>
</div>
);
}
export default HooksEffect;
相比起redux還需要connect高階函數包裹一下才能將dispatch和state注入到props中,hooks中使用reducer更加簡潔。在下面一小節中,我們會用案例來實現redux。
七:<span id="useReducer替代Redux案例">useReducer替代Redux案例</span> 回目錄
在本小節中,我們會用context
和useReducer
來實現redux
的效果。依然是使用計數器這個功能,先貼代碼,后面會詳細講解:
// count.js 定義context和reducer,導出context和包含reducer的context包裹組件。
import React, { createContext, useReducer } from 'react';
function countReducer(state, action) {
switch (action.type) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
default:
return state;
}
}
const ADDCOUNT = 'add';
const MINUSCOUNT = 'minus';
export const CountContext = createContext();
export const CountWrap = (props) => {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<CountContext.Provider
value={{ count, dispatch, ADDCOUNT, MINUSCOUNT }}
>
{props.children}
</CountContext.Provider>
);
};
// ReducerToRedux.jsx,連接組件,
import React from 'react';
import Button from './Button';
import Show from './Show';
import { CountWrap } from './count';
function ReducerToRedux() {
return (
<div>
<CountWrap>
<Show />
<Button />
</CountWrap>
</div>
);
}
export default ReducerToRedux;
// Show.jsx 顯示當前數值的組件
import React, { useContext } from 'react';
import { CountContext } from './count';
function ReducerToRedux() {
const { count } = useContext(CountContext);
return (
<div>現在的計數器值為:{count}</div>
);
}
export default ReducerToRedux;
// Button.jsx 按鈕組件,可以實現計數器的增和減
import React, { useContext } from 'react';
import { CountContext } from './count';
function ReducerToRedux() {
const { dispatch, ADDCOUNT, MINUSCOUNT } = useContext(CountContext);
return (
<div>
<button
type="button"
onClick={() => { dispatch({ type: MINUSCOUNT }); }}
>點我-1</button>
<button
type="button"
onClick={() => { dispatch({ type: ADDCOUNT }); }}
>點我+1</button>
</div>
);
}
export default ReducerToRedux;
通過reduce
r和context
實現計數器的功能,我們共用了四個文件,當然count.js
這個文件本應該拆分成三個文件,常量單獨定義一個文件,reducer
純函數也應該單獨定一個文件,不過代碼不多,就暫時合一塊了。<br />
在count.js
中,我們導出CountContext
和CountWrap
,其中,CoutWrap
就是provider
,也就是只要被CountWrap
包裹過的組件,就可以使用userContent
取到傳遞數據,而CountContext
就是用createContext
新建的一個content
,使用useContext
取傳遞數據的時候會用到。同時,在這個文件中,我們還將從useReducer
解構出的count
和dispatch
,以及常量增減通過provider傳遞給包裹組件,使被包裹的組件可以通過useContext
取到這些數據。函數countReducer
就是和redux
中的reducer
一樣的純函數,子組件dispatch
action
,reducer
則是接受當前state
和action
,通過判斷action
,返回新的state
。<br />
在Button
組件中,我們通過countContext
取到dispatch
及常量,改變count
這個數值,在Show
組件中,只是展示count
,在ReducerToRedux
文件中,是做一個連接器,用CountWrap
包裹Button
和Show
組件。<br />
可能說的有些啰嗦,看上去有些復雜,其實稍一整理,原理很簡單,自己寫一遍整理清楚邏輯使用上就很簡單了。<br />
講到這里,其實hooks
已經能應對絕大部分場景了,下面兩小節,我們會講一下useMemo
和useRef
,用于優化渲染以及處理特殊情況。
八:<span id="useMemo">useMemo</span> 回目錄
把“創建”函數和依賴項數組作為參數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助于避免在每次渲染時都進行高開銷的計算。<br />
useMemo是函數式組件官方提供的性能優化的一個方法,接受兩個參數,第一個參數是要執行的函數,第二個參數是state中的值或者父組件傳下來的值,代表只有當第二個參數的值發生變化時,才執行函數。其中,第二個參數是數組,可以同時優化多個state或者父組件傳下來的參數,首次渲染組件是,如果頁面用到要優化的值,函數會執行。<br />
我們還是以計數器以及年齡為例
import React, { useState, useMemo } from 'react';
function Show({ count, age, clear }) {
function ageChange(value) {
console.log(value);
return value + 2;
}
const myAge = useMemo(() => ageChange(age), [age]);
return (
<div>
數量:{count} 我的年齡:{myAge}
<button
type="button"
onClick={() => { clear(); }}
>復原</button>
</div>
);
}
function HooksUseMome() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你點擊了{count}次</p>
<button
type="button"
onClick={() => { setAge(age + 1); }}
>點擊年齡+1</button>
<button
type="button"
onClick={() => { setCnt(count + 1); }}
>點擊計數器+1</button>
<Show count={count} age={age} clear={clear} />
</div>
);
}
export default HooksUseMome;
在上面的例子中,父組件小女子
初始年齡為16歲,而到子組件經過ageChange
函數,返回我的
年齡永遠比小女子
年齡大兩歲。<br />
但是如果沒有useMemo,當父組件的計數器count
值發生變化時,子組件的ageChange
函數也會執行,這不是我們想要的結果,我們只想當小女子
的年齡發生變化時,再執行ageChange
函數。所以,用useMemo可以實現我們想要的效果。如上面代碼所示const myAge = useMemo(() => ageChange(age), [age]);
,使用useMemo,第二個參數是age
,這樣,只有當age
發生變化時,才執行其中的函數。<br />
在類組件中,有shouldComponentDidUpdata
生命周期,我們可以在其中做監測,當檢測到state
值沒發生變化時,直接不渲染組件,而useMemo
和這個生命周期還有些許不同。它是當檢測的state
發生變化時而執行某些函數,避免額外的開銷,節省性能。
九: <span id="useRef">useRef</span> 回目錄
在項目開發中,我們比較少用到ref
,一般我們不直接操作DOM
,都是通過狀態來控制DOM
,不過在某些情況下,可能還是會用到ref
,這一節我們通過對input
輸入框數據的雙向綁定來認識useRef
import React, { useState, useRef } from 'react';
function HooksUseRef() {
const [inputValue, setInputValue] = useState();
const inputRef = useRef(null);
function inputChangeHandle(e) {
setInputValue(e.target.value);
}
function inputRefChangeHandle() {
console.log(inputRef.current.value);
}
return (
<div>
<div>
<input
value={inputValue}
onChange={inputChangeHandle}
type="text"
/>
<span>使用state綁定inputValue值</span>
</div>
<div>
<input
ref={inputRef}
onChange={inputRefChangeHandle}
type="text"
/>
<span>使用Ref綁定inputValue值</span>
</div>
</div>
);
}
export default HooksUseRef;
在上面的案例中,我們如果要取input
的值,如果是state
雙向綁定,可以直接取inputValue
,如果是用ref
,則可以通過inputRef.current.value
取到值
通過const inputRef = useRef(null);
,我們獲取到的是一個對象,而current
屬性就是其中的dom
元素。
十: <span id="useCallBack">useCallBack</span> 回目錄
把內聯回調函數及依賴項數組作為參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時才會更新。
簡而言之,useCallBack
是用來緩存函數的,在class
類中,我們通常在constructor
中使用this.fn = this.fn.bind(this)
來綁定this,是每次調用的fn
都是之前的fn
,而不用開辟新的函數。而useCallback
同樣有此功能,useCallBack
和useMemo
的不同點在于useMemo
相當于緩存state
,而useCallBack
相當于緩存函數,官方給的解釋是這樣的useCallback(fn, deps) 相當于 useMemo(() => fn, deps)。
.
我們下面還是用計數器和年齡做例子
import React, { useState, useEffect, useCallback } from 'react';
function Show({ countCallBack, ageCallBack }) {
const [count, setCount] = useState(() => { countCallBack(); });
const [age, setAge] = useState(() => { ageCallBack(); });
useEffect(() => {
setCount(countCallBack());
}, [countCallBack]);
useEffect(() => {
setAge(ageCallBack());
}, [ageCallBack]);
return (
<div>
數量:{count} 年齡:{age}
</div>
);
}
function HooksCallBack() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
const countCallBack = useCallback(() => {
return count;
}, [count]);
const ageCallBack = useCallback(() => {
return age;
}, []);
return (
<div>
<p>小女子芳年{age}</p>
<p>你點擊了{count}次</p>
<button
type="button"
onClick={() => { setAge(age + 1); }}
>點擊年齡+1</button>
<button
type="button"
onClick={() => { setCnt(count + 1); }}
>點擊計數器+1</button>
<Show countCallBack={countCallBack} ageCallBack={ageCallBack} />
</div>
);
}
export default HooksCallBack;
在上面的例子中,只有點擊計數器按鈕,子組件才會跟著更新,點擊年齡按鈕子組件則不跟著更新。使用useCallback
如果沒有依賴,則只會執行一次,只有依賴改變,才會返回新的函數,我們可以根據這個規則實現bind
的效果。
十一: <span id="自定義函數">自定義函數</span> 回目錄
這一小節,我們做一個監聽瀏覽器窗口的自定義函數,廢話不多說,直接上例子:
import React, { useState, useEffect, useCallback } from 'react';
function useWinSize() {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const resizeHandle = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
}, []);
useEffect(() => {
window.addEventListener('resize', resizeHandle);
return () => {
window.removeEventListener('resize', resizeHandle);
};
}, []);
return size;
}
function HooksFunction() {
let size = useWinSize();
return (
<div>
瀏覽器窗口尺寸{`${size.width}*${size.height}`}
</div>
);
}
export default HooksFunction;
上面的代碼就不多解釋了,所需要注意的是自定義函數需要以use
開頭,且后面應該用大寫字母與use
分隔開。到此呢,hooks
先寫到這里,基本上也能面對絕大多數的業務場景,其它的hooksAPI
等以后開發中如果有用到,再來補充。<br />
在下一節中,我們將會把TS
從基礎到項目應用整個的梳理出來,分兩篇來完成,待續。。。