學(xué)習(xí)React-Hook,我們應(yīng)該思考哪些東西
組合優(yōu)于繼承,我們一直在尋求「 解耦 」來(lái)把復(fù)雜的業(yè)務(wù)代碼簡(jiǎn)單化。而Hook的最大優(yōu)點(diǎn)就是代碼復(fù)用,更簡(jiǎn)潔。而且Hook在寫法上也遵循了「 單一職責(zé)模式 」。
今天主要分享的是在學(xué)習(xí)Hook之前,我們應(yīng)該做哪些思考。
- 數(shù)據(jù)綁定、狀態(tài)管理
- 生命周期、組件掛載卸載
- 組件傳值
- 節(jié)點(diǎn)元素獲取
- 復(fù)用邏輯抽離
數(shù)據(jù)綁定
在react中state的概念是內(nèi)部狀態(tài)的管理,它的變更直接會(huì)影響頁(yè)面的渲染。
在hook中把setState拆開了。每一個(gè)單個(gè)的state的都會(huì)是一個(gè)單個(gè)的狀態(tài),單個(gè)狀態(tài)的變更不會(huì)影響其他state。我們可以通過useState實(shí)現(xiàn)單個(gè)狀態(tài)的初始化定義。
useState的參數(shù)可以是一個(gè)數(shù)字、字符串、布爾值、數(shù)組或?qū)ο?,也可以是一個(gè)函數(shù)。
同樣我們避免不了會(huì)使用集合(對(duì)象)來(lái)處理一些邏輯。
- const [count, setCount] = useState(0);
- const [count, setCount] = useState(() => 0);
- const [obj, setObj] = useState({});
- setObj((prevObj) => {
- // 也可以使用 Object.assign
- return { ...prevObj, age: 23 };
- });
一般我們會(huì)定義一個(gè)初始值initialState,如果這個(gè)初始值,需要額外的計(jì)算開銷,我們可以定義一個(gè)函數(shù)來(lái)處理。需要注意的是,useState 函數(shù)只會(huì)在初始渲染的時(shí)候被調(diào)用。
- const [state, setState] = useState(() => {
- // 額外的操作someExpensiveComputation
- const initialState = someExpensiveComputation(props);
- return initialState;
- });
對(duì)于多個(gè)state集合的處理,還有另一種方案就是useReducer。
- 比如單個(gè) state 的狀態(tài)會(huì)影響多個(gè) state 的值的時(shí)候。
- 比如多個(gè) state 的狀態(tài)會(huì)隨著某種類型的改變而改變。
如下多個(gè) state 會(huì)隨著登錄、登出、刷新 token 這三種狀態(tài)的改變而改變。
- const [state, dispatch] = React.useReducer(
- (prevState, action) => {
- switch (action.type) {
- case "RESTORE_TOKEN":
- return {
- ...prevState,
- userToken: action.token,
- isLoading: false,
- };
- case "SIGN_IN":
- return {
- ...prevState,
- isSignout: false,
- userToken: action.token,
- };
- case "SIGN_OUT":
- return {
- ...prevState,
- isSignout: true,
- userToken: null,
- };
- }
- },
- {
- isLoading: true,
- isSignout: false,
- userToken: null,
- }
- );
副作用
hook 提供了一種新的概念來(lái)代替生命周期函數(shù),就是useEffect副作用。它被看作是從 React 的純函數(shù)式世界通往命令式世界的逃生通道。
它的執(zhí)行時(shí)機(jī)是在屏幕元素渲染結(jié)束后延遲執(zhí)行。
它的作用有:
- 它可以監(jiān)控 state 值的變化
- 它可以處理只運(yùn)行一次的邏輯,有點(diǎn)類似生命周期 componentDidMount 和 componentWillUnmount 的思維模式,
- 添加訂閱、設(shè)置定時(shí)器、發(fā)送網(wǎng)絡(luò)請(qǐng)求
- 更多其他
- // 通過的第二個(gè)參數(shù)來(lái)實(shí)現(xiàn)只執(zhí)行一次或監(jiān)控state值
- useEffect(() => {
- // ...
- }, []);
- // useEffect第一個(gè)參數(shù)的返回函數(shù)就是componentWillUnmount的思想,在組件卸載之前進(jìn)行
- useEffect(() => {
- const subscription = props.source.subscribe();
- return () => {
- // 清除訂閱
- subscription.unsubscribe();
- };
- });
但是這種延遲執(zhí)行的機(jī)制不能滿足我們所有的場(chǎng)景,如果我們想要實(shí)現(xiàn)屏幕繪制和副作用同步執(zhí)行,比如實(shí)時(shí)修改dom結(jié)構(gòu)等這樣的場(chǎng)景,useEffect無(wú)法滿足,會(huì)出現(xiàn)閃屏的效果。
我們可以通過useLayoutEffect來(lái)實(shí)現(xiàn),它的執(zhí)行時(shí)機(jī)是在組件加載完成后,屏幕繪制之前進(jìn)行。但這樣也有缺點(diǎn)就是阻塞屏幕渲染,可能會(huì)出現(xiàn)白屏或停頓。
所以u(píng)seLayoutEffect的使用場(chǎng)景:
- 防止閃爍,比較耗時(shí)的計(jì)算
- Dom操作
- componentDidMount和componentDidUpdate的場(chǎng)景
如果只是單獨(dú)的獲取(get操作)就沒有必要使用useLayoutEffect。
組件傳值
組件傳值的核心:
- 父?jìng)髯?,通過在子組件設(shè)置屬性;
- 子傳父,通過回調(diào)。
- 多級(jí)組件,通過中間狀態(tài)管理
- // 父組件
- function Home() {
- const [currentTab, setCurrentTab] = useState("msg");
- return (
- <>
- <View style={styles.logo}>
- // 父?jìng)髯?nbsp;
- <TabView currentTab={currentTab} setCurrentTab={setCurrentTab} />
- // 子傳父
- <CodeView code={code} changeCode={(code)=>setCurrentTab(code)} />
- </View>
- <Text>{currentTab}</Text>
- </>
- );
- }
- //子組件
- function TabView({ currentTab, setCurrentTab }) {
- return (
- <View style={styles.logo}>
- <Text>{currentTab}</Text>
- <Button
- title="修改tab"
- onPress={() => {
- setCurrentTab("pass");
- }}
- />
- </View>
- );
- }
- //子傳父
- function CodeView({ code, changeCode }) {
- return (
- <View style={styles.logo}>
- <Text>{code}</Text>
- <Button
- title="修改tab"
- onPress={changeCode}
- />
- </View>
- );
- }
多組件的傳值,可以通過context來(lái)處理。
- export const MyContent = React.createContext({});
- function Home() {
- return (
- <MyContent.Provider
- value={{
- currentTab,
- phoneValue,
- codeValue,
- setPhoneValue,
- setCodeValue,
- }}
- >
- <FormItem />
- <SwitchItemView />
- </MyContent.Provider>
- );
- }
- function FormItem() {
- const { phoneValue, setPhoneValue } = useContext(MyContent);
- return (
- <View style={styles.logo}>
- <Text>{phoneValue}</Text>
- {/* ...*/}
- </View>
- );
- }
- function SwitchItemView() {
- const { codeValue, setCodeValue } = useContext(MyContent);
- return (
- <View style={styles.logo}>
- <Text>{phoneValue}</Text>
- {/* ...*/}
- </View>
- );
- }
元素節(jié)點(diǎn)操作
hook通過useRef來(lái)創(chuàng)建節(jié)點(diǎn)對(duì)象,然后通過ref掛載,通過current來(lái)獲取。
- function TextInputWithFocusButton() {
- const inputEl = useRef(null);
- const onButtonClick = () => {
- inputEl.current.focus();
- };
- return (
- <>
- <input ref={inputEl} type="text" />
- <button onClick={onButtonClick}>Focus the input</button>
- </>
- );
- }
我們可能會(huì)封裝一些邏輯,自定義一些屬性,暴露給父元素,那么我們就會(huì)用到useImperativeHandle和forwardRef。
- function FancyInput(props, pref) {
- const inputRef = useRef();
- useImperativeHandle(ref, () => ({
- focus: () => {
- inputRef.current.focus();
- }
- }));
- return <input ref={inputRef} ... />;
- }
- FancyInput = forwardRef(FancyInput);
- <FancyInput ref={inputRef} />
- // 父組件可以直接調(diào)用inputRef.current.focus()
因?yàn)?ref 對(duì)象不會(huì)把當(dāng)前 ref 值的變化通知給我們,所以我們必須通過useState和useCallback實(shí)現(xiàn)。
- function MeasureExample() {
- const [height, setHeight] = useState(0);
- const measuredRef = useCallback((node) => {
- if (node !== null) {
- setHeight(node.getBoundingClientRect().height);
- }
- }, []);
- return (
- <>
- <h1 ref={measuredRef}>Hello, world</h1>
- <h2>The above header is {Math.round(height)}px tall</h2>
- </>
- );
- }
自定義hook
在代碼中,我們會(huì)有一些共用的邏輯,我們可以抽離出來(lái)比如自定義的防抖節(jié)流,自定義 Hook 是一個(gè)函數(shù),其名稱以 “use” 開頭,函數(shù)內(nèi)部可以調(diào)用其他的 Hook。
- const useDebounce = (fn, ms = 30, deps = []) => {
- let timeout = useRef();
- useEffect(() => {
- if (timeout.current) clearTimeout(timeout.current);
- timeout.current = setTimeout(() => {
- fn();
- }, ms);
- }, deps);
- const cancel = () => {
- clearTimeout(timeout.current);
- timeout = null;
- };
- return [cancel];
- };
- export default useDebounce;
- const Home = (props) => {
- const [a, setA] = useState(0);
- const [b, setB] = useState(0);
- const [cancel] = useDebounce(
- () => {
- setB(a);
- },
- 2000,
- [a]
- );
- const changeIpt = (e) => {
- setA(e.target.value);
- };
- return (
- <div>
- <input type="text" onChange={changeIpt} />
- {a}
- </div>
- );
- };
性能優(yōu)化
單向數(shù)據(jù)流,各組件層次分明,狀態(tài)明確,但是當(dāng)項(xiàng)目體量大,組件嵌套多的時(shí)候,性能損耗也非常大,所以我們會(huì)做一些性能優(yōu)化的工作。
可能的優(yōu)化場(chǎng)景有:
- 單個(gè) state 的更新會(huì)影響全局,有一點(diǎn)需要注意的是,被context包裹的組件,只要value的任何一個(gè)變動(dòng),都會(huì)重新渲染,useMemo和useCallback就會(huì)失效。
- 相同的輸入,不再重新計(jì)算
- 父組件的函數(shù)在子組件使用的時(shí)候
其實(shí)useMemo和useCallback的核心思想相同,都是記錄上一次的輸入,如果下一次輸入與上一次相同,將不會(huì)計(jì)算,直接獲取上一次的結(jié)果。他們的區(qū)別只是形式上的,useMemo返回一個(gè) memoized 值。而useCallback返回的是memoized回調(diào)函數(shù)。
useMemo緩存計(jì)算結(jié)果的值。
useCallback主要用于緩存函數(shù)。
useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps)。
- const memoizedCallback = useCallback(() => {
- doSomething(a, b);
- }, [a, b]);
- // useMemo
- const [count, setCount] = useState(1);
- const [val, setValue] = useState("");
- const expensive = useMemo(() => {
- let sum = 0;
- for (let i = 0; i < count * 100; i++) {
- sum += i;
- }
- return sum;
- }, [count]);
- return (
- <div>
- <h4>
- {count}-{expensive}
- </h4>
- {val}
- <div>
- <button onClick={() => setCount(count + 1)}>+c1</button>
- <input value={val} onChange={(event) => setValue(event.target.value)} />
- </div>
- </div>
- );
捎帶了解一下memoized,簡(jiǎn)單講就是把函數(shù)的計(jì)算結(jié)果緩存起來(lái),比如遞歸。
- const memoize = function(fn) {
- const cache = {};
- return function() {
- const key = JSON.stringify(arguments);
- var value = cache[key];
- if(!value) {
- console.log('新值,執(zhí)行中...'); // 為了了解過程加入的log,正式場(chǎng)合應(yīng)該去掉
- value = [fn.apply(this, arguments)]; // 放在一個(gè)數(shù)組中,方便應(yīng)對(duì)undefined,null等異常情況
- cache[key] = value;
- } else {
- console.log('來(lái)自緩存'); // 為了了解過程加入的log,正式場(chǎng)合應(yīng)該去掉
- }
- return value[0];
- }
- }
- module.exports = memoize;
- const memoize = require('./memoize.js');
- const log = console.log;
- // 斐波那契數(shù)組
- const fibonacci = (n) => {
- return n < 2
- ? n
- : fibonacci(n - 1) + fibonacci(n - 2);
- };
- const memoizeFibonacci = memoize(fibonacci);
- log(memoizeFibonacci(45)); // 新值,執(zhí)行中...; 1134903170 // 等待時(shí)間比較長(zhǎng)
- log(memoizeFibonacci(45)); // 來(lái)自緩存; 1134903170
- log(memoizeFibonacci(45)); // 來(lái)自緩存; 1134903170
- log(memoizeFibonacci(45)); // 來(lái)自緩存; 1134903170
本文轉(zhuǎn)載自微信公眾號(hào)「 驚天碼盜」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 驚天碼盜公眾號(hào)。