一、React介紹
溫馨提醒:想要獲取更好的觀看效果,可以點(diǎn)擊查看本篇文章的原文檔(React-Hook快速入門(一) (notion.so)
react是基本的頁面渲染庫,基于不同的平臺有
- react-dom: 瀏覽器
- react-native: app環(huán)境
- react-vr: vr平臺
二、為什么要使用React-Hook
在React-Hook誕生之前,React通常使用class作為組件。而function只能作為受控組件,它本身是沒有自己的狀態(tài)(state),只能通過接受props來被動渲染。引入React-Hook后,函數(shù)組件可以通過useState來擁有自己的狀態(tài);useEffect整合了各類生命周期,使得代碼邏輯更清晰;自定義hook使得代碼更容易復(fù)用。以下是官網(wǎng)對于React-Hook的介紹。
簡而言之,hook主要解決了以下的一些問題:
- 大型組件很難拆分和重構(gòu),也很難測試。
- 業(yè)務(wù)邏輯分散在組件的各個方法之中,導(dǎo)致重復(fù)邏輯或關(guān)聯(lián)邏輯。
- 組件類引入了復(fù)雜的編程模式,比如 render props 和高階組件。
核心思想:組件的最佳寫法應(yīng)該是函數(shù),而不是類。React Hooks 的意思是,組件盡量寫成純函數(shù),如果需要外部功能和副作用,就用鉤子把外部代碼"鉤"進(jìn)來。
三、useState
基本用法
const [state, setState] = useState(initValue)
useState使React函數(shù)組件擁有了狀態(tài)。
- 括號里的initValue是state的初始值。
- 數(shù)組解構(gòu)的第一個參數(shù)是最新的state值,每次state的值得改變將觸發(fā)頁面重新渲染。
- 數(shù)組解構(gòu)的第二個參數(shù)是state的更新函數(shù),通過給setState(newState)傳遞參數(shù)newState來改變狀態(tài)值(state),并引發(fā)頁面的重新渲染。
import React, { useState } from 'react';
const Example = () => {
// 聲明一個叫 "count" 的 state 變量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
setState是同步還是異步?
[圖片上傳失敗...(image-8c34b3-1621553852262)]
https://codesandbox.io/s/agitated-flower-9lhi2?file=/src/App.tsx:0-570
在執(zhí)行增加num(state值)的前后打印num,發(fā)現(xiàn)打印的結(jié)果均是0而不是最新值1,似乎setState是一個異步操作?
import { useState } from "react";
import "./styles.css";
export default function App() {
const [num, setNum] = useState(0);
const addNum = () => {
console.log("執(zhí)行setNum之前", num);
setNum(num + 1)
console.log("執(zhí)行setNum之后", num);
};
return (
<div className="App">
<h1>setState是同步還是異步的?</h1>
<p>數(shù)字:{num}</p>
<button onClick={addNum}>增加</button>
</div>
);
}
接著將代碼更改如下,在點(diǎn)擊增加按鈕的回調(diào)函數(shù)中一次性調(diào)用兩次setNum,結(jié)果發(fā)現(xiàn)頁面上顯示的數(shù)字是1而不是2。這是什么情況呢?
import { useState } from "react";
import "./styles.css";
export default function App() {
const [num, setNum] = useState(0);
const addNum = () => {
console.log("執(zhí)行setNum之前", num);
setNum(num + 1)
console.log("執(zhí)行setNum之后", num);
};
return (
<div className="App">
<h1>setState是同步還是異步的?</h1>
<p>數(shù)字:{num}</p>
<button onClick={addNum}>增加</button>
</div>
);
}
這是因?yàn)樵趫?zhí)行了setNum函數(shù)后,React并不會立馬去更新num。這樣在連續(xù)第二次調(diào)用setState時,num值仍然為0,因此連續(xù)調(diào)用兩次setNum后最新結(jié)果是1。React這樣做的目的是,連續(xù)多次的調(diào)用setState會合并state,而不是立馬去更新state,這樣就不會導(dǎo)致頁面在短暫時間進(jìn)行多次渲染,從而節(jié)省了頁面開銷。
那么我想連續(xù)調(diào)用兩次,而且最終結(jié)果顯示為2,該怎么實(shí)現(xiàn)?這個時候可以使用函數(shù)式更新
函數(shù)式更新
通過在useState傳入一個函數(shù),該函數(shù)的參數(shù)state即為更新之前的state值,通過在這個函數(shù)中執(zhí)行兩次num + 1的操作,最后返回的state將會比先前值+2,這樣保證了狀態(tài)的一致性,也驗(yàn)證了setState本身是一個同步函數(shù),只是它的狀態(tài)更新機(jī)制像一個異步函數(shù)!
import { useState } from "react";
import "./styles.css";
export default function App() {
const [num, setNum] = useState(0);
const addNum = () => {
console.log("執(zhí)行setNum之前", num);
setNum((state) => {
state += 1;
state += 1
console.log("執(zhí)行setNum的時候", state);
return state;
});
console.log("執(zhí)行setNum之后", num);
};
return (
<div className="App">
<h1>setState是同步還是異步的?</h1>
<p>數(shù)字:{num}</p>
<button onClick={addNum}>增加</button>
</div>
);
}
惰性初始化
useState可以存字符串、數(shù)值等基本類型,也可以存數(shù)組、字符串等引用類型。那么可不可以存函數(shù)呢?
https://codesandbox.io/s/usestateduoxingchushihua-jthil?file=/src/App.tsx
通過callback存入一個函數(shù),在點(diǎn)擊更改函數(shù)按鈕時,變更存儲的函數(shù);在點(diǎn)擊執(zhí)行函數(shù)按鈕時,調(diào)用存入的函數(shù)。然而結(jié)果并不是像我們期望的那樣。在初始化state和點(diǎn)擊更改函數(shù)按鈕時,都自動執(zhí)行了存入的函數(shù)!原因是React的useState有著惰性初始化的特性
import { useState } from "react";
import "./styles.css";
export default function App() {
const [callback, setCallback] = useState(() => {alert('init')})
return (
<div className="App">
<h1>useState惰性初始化</h1>
<button onClick={() => {setCallback(() => {alert('change')})}}>更改函數(shù)</button>
<button onClick={callback}>執(zhí)行函數(shù)</button>
</div>
);
}
傳入函數(shù)給useState,React并不會認(rèn)為你要存的是一個函數(shù)。相反他會認(rèn)為這是一個非常消耗性能的計(jì)算state的操作,他會立即去執(zhí)行還函數(shù),并將該函數(shù)的返回值作為新的state。
那么useState如何存一個函數(shù)呢?
其實(shí)很簡單,只要將上述代碼修改如下即可。
import { useState } from "react";
import "./styles.css";
export default function App() {
const [callback, setCallback] = useState(() => () => {alert('init')})
return (
<div className="App">
<h1>useState惰性初始化</h1>
<button onClick={() => {setCallback(() => () => {alert('change')})}}>更改函數(shù)</button>
<button onClick={callback}>執(zhí)行函數(shù)</button>
</div>
);
}
四、useEffect
基本用法
useEffect(() => {
// do some effect function
}, [dependence]);
在DOM更新完畢之后執(zhí)行副作用函數(shù),可以取代class的生命周期函數(shù)。
- 當(dāng)沒有依賴項(xiàng),會在組件每次更新后執(zhí)行
- 依賴項(xiàng)為空數(shù)組:會在組件掛載和卸載時執(zhí)行
- 依賴項(xiàng)為變量時,會在這些變量改變后才執(zhí)行
清除effect
通常,組件卸載時需要清除 effect 創(chuàng)建的諸如訂閱或計(jì)時器 ID 等資源。要實(shí)現(xiàn)這一點(diǎn),useEffect 函數(shù)需返回一個清除函數(shù)。
https://codesandbox.io/s/wispy-frog-bb3wp?file=/src/App.tsx
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
export default function Test() {
const [num, setNum] = useState<number>(0);
useEffect(() => {
console.log("在return之后執(zhí)行");
return () => {
console.log("return先執(zhí)行");
};
}, [num]);
return (
<div>
<h2>Test測試頁面</h2>
<Link to="/other">前往Other</Link>
<p>數(shù)字:{num}</p>
<button
onClick={() => {
setNum(num + 1);
}}
>
增加
</button>
</div>
);
}
- React會在第一次渲染時執(zhí)行useEffect中的函數(shù),韓是不會執(zhí)行return。
- effect 在之后的每次渲染的時候都會執(zhí)行。此時先執(zhí)行return函數(shù),再執(zhí)行effect中的副作用。
- React 會在組件卸載的時候執(zhí)行清除操作(即執(zhí)行return)
依賴項(xiàng)的注意事項(xiàng)
一般選擇什么作為useEffect的依賴項(xiàng)?選擇依賴項(xiàng)時需要注意些什么呢?
https://codesandbox.io/s/trusting-cherry-7i2wt?file=/src/App.tsx
分別選擇常量、狀態(tài)對象和普通對象作為依賴項(xiàng)??梢园l(fā)現(xiàn),選擇常量時,除了第一次渲染以外,之后每次頁面渲染都不會去調(diào)用useEffect;選擇狀態(tài)對象時,每次該狀態(tài)改變時(即頁面重新渲染后),都會去調(diào)用useEffect;而選擇一般對象時,會陷入無限循環(huán)的去調(diào)用useEffect。
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [num, setNum] = useState(0);
const [text, setText] = useState<string[]>([]);
const constValue = "小花";
const object = { name: "小花" };
useEffect(() => {
console.log("頁面重新渲染了");
setText(["小花", "小明", "囂張"]);
}, [constValue]);
return (
<div className="App">
<h1>選擇依賴項(xiàng)的注意事項(xiàng)</h1>
<p>數(shù)字:{num}</p>
<button
onClick={() => {
setNum(num + 1);
}}
>
增加
</button>
{text.map((item) => (
<div>{item}</div>
))}
</div>
);
}
- 絕不可以使用非狀態(tài)的對象作為依賴,因?yàn)槊看谓M件更新后,該對象的地址都會發(fā)生改變,最終導(dǎo)致不停地的調(diào)用useEffect。
- 可以使用常量和狀態(tài)對象作為依賴,因?yàn)闋顟B(tài)對象在組建更新后并不會改變,除非調(diào)用setState改變。
五、自定義hook
Hook 使用規(guī)則
- 只能在函數(shù)最外層調(diào)用 Hook。不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用。
- 只能在 React 的函數(shù)組件中調(diào)用 Hook。不要在其他 JavaScript 函數(shù)中調(diào)用。(還有一個地方可以調(diào)用 Hook —— 就是自定義的 Hook 中,我們稍后會學(xué)習(xí)到。)
為什么只能在函數(shù)最外層調(diào)用hook?
自定義一個useArray hook
遵循React的規(guī)則,使用React提供的基礎(chǔ)hook,自定義一個hook。該hook封裝了對數(shù)組的操作,在其他地方可以方便的調(diào)用這個hook來完成對數(shù)組的操作
點(diǎn)擊查看useArray的實(shí)現(xiàn)
https://codesandbox.io/s/fervent-hodgkin-g0ipv?file=/src/App.tsx
注意事項(xiàng)
- 自定義hook一定要使用use開頭的命名規(guī)范,否則會直接報(bào)錯
- 不要在回調(diào)函數(shù)中調(diào)用hook,應(yīng)該把需要用到的回調(diào)方法通過hook返回,然后在需要用到的函數(shù)組件最外層獲取該方法,這樣就可以在回調(diào)函數(shù)中調(diào)用該方法。