咱們組的前端妹子在組內分享時談到了 react 的鉤子,趁此機會我也對我所理解的內容進行下總結,方便更多的同窗瞭解。在 React 的 v16.8.0 版本里添加了 hooks 的這種新的 API,咱們很是有必要了解下他的使用方法,並可以結合咱們的業務編寫幾個自定義的 hooks。javascript
官方中提供了幾個內置的鉤子,咱們簡單瞭解下他們的用法。html
須要更新頁面狀態的數據,咱們能夠把他放到 useState
的鉤子裏。例如點擊按鈕一下,數據加 1 的操做:前端
const [count, setCount] = useState(0);
return (<> <p>{ count}</p> <button onClick = { () => setCount(count + 1) }> add 1 </button> </> ); 複製代碼
在 typescript 的體系中,count 的類型,默認就是當前初始值的類型,例如上面例子中的變量就是 number
類型。若是咱們想自定義這個變量的類型,能夠在 useState 後面進行定義:java
const [count, setCount] = useState<number | null>(null); // 變量count爲number類型或者null類型
複製代碼
同時,使用 useState 改變狀態時,是整個把 state 替換掉的,所以,若狀態變量是個 object 類型的數據,我只想修改其中的某個字段,在以前 class 組件內調用 setState 時,他內部會自動合併數據。react
class Home extends React.Component {
state = {
name: 'wenzi',
age: 20,
score: 89
};
update() {
this.setState({
score: 98
}); // 內部自動合併
}
}
複製代碼
但在 function 組件內使用 useState
時,須要本身先合併數據,而後再調用方法,不然會形成字段的丟失。web
const [person, setPerson] = useState({
name: 'wenzi',
age: 20,
score: 89
});
setPerson({
...person,
{
score: 98
}
}); // 先合併數據 { name: 'wenzi', age: 20, score: 98 }
setPerson({
score: 98
}); // 僅傳入要修改的字段,後name和age字段丟失
複製代碼
useEffect 能夠看作是 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數的組合。typescript
useEffect 鉤子在組件初始化完畢時,必定會執行一次,在組件從新渲染的過程當中,是否還要 update,還要看傳入的第 2 個參數。後端
- 當只有回調函數這一個參數時,組件的每次更新,回調都會執行;
- 當有 2 個參數時,只有第 2 參數裏的數據發生變化時,回調才執行;
- 只想在組件初始化完畢時只執行一次,第 2 個參數能夠傳入一個空的數組;
咱們能夠看下這個例子,不管點擊 add按鈕
仍是 settime按鈕
,useEffect 的回調都會執行:數組
const Home = () => {
const [count, setCount] = useState(0);
const [nowtime, setNowtime] = useState(0);
useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
});
return ( <> <p>count: {count} </p> <p>nowtime: {nowtime} </p> <button onClick = {() => setCount(count + 1)}> add 1 </button> <button onClick = {() => setNowtime(Date.now())} > set now time </button> </>); }; 複製代碼
若改爲下面的這樣,回調僅會在 count 發生變化時纔會在控制檯輸出,僅修改 nowtime 的值時沒有輸出:websocket
useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
}, [count]);
複製代碼
useEffect 的回調函數還能夠返回一個函數,這個函數在 effect 生命週期結束以前調用。爲防止內存泄漏,清除函數會在組件卸載前執行。另外,若是組件屢次渲染,則在執行下一個 effect 以前,上一個 effect 就已被清除。
基於上面的代碼,咱們稍微修改一下:
useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
return () => console.log('effect callback will be cleared');
}, [count]);
複製代碼
基於這個機制,在一些存在添加綁定和取消綁定的案例上特別合適,例如監聽頁面的窗口大小變化、設置定時器、與後端的 websocket 接口創建鏈接和斷開鏈接等,均可以預計 useEffect 進行二次的封裝,造成自定義的 hook。關於自定義 hook,下面咱們會講到。
function 組件中定義的變量和方法,在組件從新渲染時,都會從新從新進行計算,例以下面的這個例子:
const Home = () => {
const [count, setCount] = useState(0);
const [nowtime, setNowtime] = useState(0);
const getSum = () => {
const sum = ((1 + count) * count) / 2;
return sum + ' , ' + Math.random(); // 這個random是爲了看到區別
};
return ( <> <p> count: {count}< /p> <p> sum: {getSum()}</p> <p> nowtime: {nowtime}</p> <button onClick = {() => setCount(count + 1)} > add 1 </button> <button onClick = {() => setNowtime(Date.now())}> set now time </button> </>); }; 複製代碼
這裏有 2 個按鈕,一個是 count+1,一個設置當前的時間戳, getSun()
方法是計算從 1 到 count 的和,咱們每次點擊 add 按鈕後,sum 方法都會從新計算和。但是當咱們點擊 settime 按鈕時,getSum 方法也會從新計算,這是沒有必要的。
這裏咱們可使用 useMemo
來修改下:
const sum = useMemo(() => ((1 + count) * count) / 2 + ' , ' + Math.random(), [count]);
<p> {sum} </p>;
複製代碼
點擊查看樣例,修改後就能夠看到,sum 的值只有在 count 發生變化的時候才從新計算,當點擊 settime 按鈕的時候,sum 並無從新計算。這要得益於 useMemo
鉤子的特性:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼
useMemo 返回回調裏 return 的值,並且 memoizedValue 它僅會在某個依賴項改變時才從新計算。這種優化有助於避免在每次渲染時都進行高開銷的計算。若是沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。
在上面的例子裏,只有 count 變量發生變化時,才從新計算 sum,不然 sum 的值保持不變。
useCallback 與 useMemo 類型,只不過 useCallback 返回的是一個函數,例如:
const fn = useCallback(() => {
return ((1 + count) * count) / 2 + ' , ' + nowtime;
}, [count]);
複製代碼
在官方文檔裏,實現了好友的在線與離線功能。這裏咱們本身也學着實現幾個 hook。
咱們經過監聽resize
事件來獲取實時獲取window窗口的寬高,對這個方法進行封裝後能夠在生命週期結束前能自動解綁resize事件:
const useWinResize = () => {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const resize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])
useEffect(() => {
window.addEventListener('resize', resize);
return () => window.removeEventListener('resize', resize);
}, []);
return size;
}
複製代碼
使用起來也很是方便:
const Home = () => {
const {width, height} = useWinResize();
return <div> <p>width: {width}</p> <p>height: {height}</p> </div>;
};
複製代碼
點擊連接useWinResize的使用能夠查看demo演示。
在前端中使用定時器時,一般要在組件生命週期結束前清除定時器,若是定時器的週期發生變化了,還要先清除定時器再從新按照新的週期來啓動。這種最經常使用的場景就是九宮格抽獎,用戶點擊開始抽獎後,先緩慢啓動,而後逐漸變快,接口返回中獎結果後,再開始減速,最後中止。
咱們很容易想到用 useEffect
來實現這樣的一個 hook:
const useInterval = (callback, delay) => {
useEffect(() => {
if (delay !== null) {
let id = setInterval(callback, delay);
return () => clearInterval(id);
}
}, [delay]);
};
複製代碼
咱們把這段代碼用到項目中試試:
const Home = () => {
const [count, setCount] = useState(0);
useInterval(() => {
console.log(count);
setCount(count + 1);
}, 500);
return <div > { count } < /div>;
};
複製代碼
但是這段運行後很奇怪,頁面從 0 到 1 後,就不再變了, console.log(count)
的輸出代表代碼並無卡死,那麼問題出在哪兒了?
React 組件中的 props 和 state 是能夠改變的, React 會重渲染它們且「丟棄」任何關於上一次渲染的結果,它們之間再也不有相關性。
useEffect() Hook 也「丟棄」上一次渲染結果,它會清除上一次 effect 再創建下一個 effect,下一個 effect 鎖住新的 props 和 state,這也是咱們第一次嘗試簡單示例能夠正確工做的緣由。
但 setInterval 不會「丟棄」。 它會一直引用老的 props 和 state 直到你把它換掉 —— 不重置時間你是沒法作到的。
這裏就要用到 useRef
這個 hook 了,咱們把 callback 存儲到 ref 中,當 callback 更新時去更新 ref.current
的值:
const useInterval = (callback, delay) => {
const saveCallback = useRef();
useEffect(() => {
// 每次渲染後,保存新的回調到咱們的 ref 裏
saveCallback.current = callback;
});
useEffect(() => {
function tick() {
saveCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
複製代碼
當咱們使用新的 useInterval
時,發現就能夠自增了,點擊查看樣例useInterval 的簡單使用。
這裏咱們使用一個變量來控制增長的速度:
const [count, setCount] = useState(0);
const [diff, setDiff] = useState(500);
useInterval(() => {
setCount(count + 1);
}, diff);
return ( <div> <p> count: {count} </p> <p> diff: {diff}ms </p> <p> <button onClick = {() => setDiff(diff - 50)}> 加快50ms </button> <button onClick = {() => setDiff(diff + 50)} > 減慢50ms </button> </p> </div>);
複製代碼
分別點擊兩個按鈕,能夠調整count增長的速度。
使用react hook能夠作不少有意思的事情,這裏咱們也僅僅是舉幾個簡單的例子,後續咱們也會更加深刻了解hook的原理。
▼ 我是來騰訊的小小前端開發工程師,長按識別二維碼關注,與你們共同窗習、討論 ▼