我與hooks的這一年, 萬字長文總結(jié)

轉(zhuǎn)自:掘金 -?又在吃魚 https://juejin.cn/post/6912309761066729485
前言
這一年注定是不平凡的一年,經(jīng)歷了疫情在家兩個月封城。剛好 hooks 出來了,學(xué)習(xí)了一下,發(fā)現(xiàn)真香,根本停不下來,分享一下用了將近一年的心得,在 2020 年最后一天上了末班車
動機(官方)
組件之間很難重用有狀態(tài)邏輯 復(fù)雜的組件變得難以理解 類 class 混淆了人和機器 更符合 FP 的理解, React 組件本身的定位就是函數(shù),一個吃進數(shù)據(jù)、吐出 UI 的函數(shù) 
常用 hook
useState
???const?[state,?setState]?=?useState(initialState)
useState 有一個參數(shù),該參數(shù)可以為任意數(shù)據(jù)類型,一般用作默認值 useState 返回值為一個數(shù)組,數(shù)組的第一個參數(shù)為我們需要使用的 state,第二個參數(shù)為一個 setFn。 完整例子
function?Love()?{
????const?[like,?setLike]?=?useState(false)
????const?likeFn?=?()?=>?(newLike)?=>?setLike(newLike)
????return?(
??????<>
????????你喜歡我嗎:?{like???'yes'?:?'no'}
????????<button?onClick={likeFn(true)}>喜歡button>
????????<button?onClick={likeFn(false)}>不喜歡button>
??????>
????)
??}
關(guān)于使用規(guī)則:
只在 React 函數(shù)中調(diào)用 Hook;不要在循環(huán)、條件或嵌套函數(shù)中調(diào)用 Hook。讓我們來看看規(guī)則 2 為什么會有這個現(xiàn)象, 先看看 hook 的組成
function?mountWorkInProgressHook()?{
?//?注意,單個?hook?是以對象的形式存在的
?var?hook?=?{
??memoizedState:?null,
??baseState:?null,
??baseQueue:?null,
??queue:?null,
??next:?null
?};
?if?(workInProgressHook?===?null)?{
????????firstWorkInProgressHook?=?workInProgressHook?=?hook;
????????/*?等價
????????????let?workInProgressHook?=?hooks
????????????firstWorkInProgressHook?=?workInProgressHook
????????*/
?}?else?{
??workInProgressHook?=?workInProgressHook.next?=?hook;
?}
?//?返回當前的?hook
?return?workInProgressHook;
}
每個 hook 都會有一個 next 指針,hook 對象之間以單向鏈表的形式相互串聯(lián), 同時也能發(fā)現(xiàn) useState 底層依然是 useReducer?再看看更新階段發(fā)生了什么
//?ReactFiberHooks.js
const?HooksDispatcherOnUpdate:?Dispatcher?=?{
??????//?...
?????useState:?updateState,
??}
??function?updateState(initialState)?{
????return?updateReducer(basicStateReducer,?initialState);
??}
function?updateReducer(reducer,?initialArg,?init)?{
????const?hook?=?updateWorkInProgressHook();
????const?queue?=?hook.queue;
????if?(numberOfReRenders?>?0)?{
????????const?dispatch?=?queue.dispatch;
????????if?(renderPhaseUpdates?!==?null)?{
????????????//?獲取Hook對象上的?queue,內(nèi)部存有本次更新的一系列數(shù)據(jù)
????????????const?firstRenderPhaseUpdate?=?renderPhaseUpdates.get(queue);
????????????if?(firstRenderPhaseUpdate?!==?undefined)?{
????????????????renderPhaseUpdates.delete(queue);
????????????????let?newState?=?hook.memoizedState;
????????????????let?update?=?firstRenderPhaseUpdate;
????????????????//?獲取更新后的state
????????????????do?{
????????????????????//?useState?第一個參數(shù)會被轉(zhuǎn)成?useReducer
????????????????????const?action?=?update.action;
????????????????????newState?=?reducer(newState,?action);
????????????????????//按照當前鏈表位置更新數(shù)據(jù)
????????????????????update?=?update.next;
????????????????}?while?(update?!==?null);
????????????????hook.memoizedState?=?newState;
????????????????//?返回新的?state?以及?dispatch
????????????????return?[newState,?dispatch];
????????????}
????????}
????}
????//?...
}
結(jié)合實際讓我們看下面一組 hooks
????let?isMounted?=?false
????if(!isMounted)?{
????????[name,?setName]?=?useState("張三");
????????[age]?=?useState("25");
????????isMounted?=?true
????}
????[sex,?setSex]?=?useState("男");
????return?(
????????
??);
首次渲染時 hook 順序為
name => age => sex
二次渲染的時根據(jù)上面的例子,調(diào)用的 hook 的只有一個
setSex
所以總結(jié)一下初始化階段構(gòu)建鏈表,更新階段按照順序去遍歷之前構(gòu)建好的鏈表,取出對應(yīng)的數(shù)據(jù)信息進行渲染當兩次順序不一樣的時候就會造成渲染上的差異。
為了避免出現(xiàn)上面這種情況我們可以安裝?eslint-plugin-react-hooks
//?你的?ESLint?配置
{
??"plugins":?[
????//?...
????"react-hooks"
??],
??"rules":?{
????//?...
????"react-hooks/rules-of-hooks":?"error",?//?檢查?Hook?的規(guī)則
????"react-hooks/exhaustive-deps":?"warn"?//?檢查?effect?的依賴
??}
}
useEffect
useEffect(effect,?array)
effect 每次完成渲染之后觸發(fā), 配合 array 去模擬類的生命周期
如果不傳,則每次 componentDidUpdate 時都會先觸發(fā) returnFunction(如果存在),再觸發(fā) effect [] 模擬 componentDidMount [id] 僅在 id 的值發(fā)生變化以后觸發(fā) 清除 effect
useEffect(()?=>?{
??ChatAPI.subscribeToFriendStatus(props.id,?handleStatusChange);
??return?()?=>?{
????ChatAPI.unsubscribeFromFriendStatus(props.id,?handleStatusChange);
??};
});
useLayoutEffect
跟 useEffect 使用差不多,通過同步執(zhí)行狀態(tài)更新可解決一些特性場景下的頁面閃爍問題 useLayoutEffect 會阻塞渲染,請謹慎使用 對比看看? 

import?React,?{?useLayoutEffect,?useEffect,?useState?}?from?'react';
import?'./App.css'
function?App()?{
????const?[value,?setValue]?=?useState(0);
????useEffect(()?=>?{
????????if?(value?===?0)?{
????????????setValue(10?+?Math.random()?*?200);
????????}
??????},?[value]);
????const?test?=?()?=>?{
????????setValue(0)
????}
????const?color?=?!value????'red'?:?'yellow'
?return?(
??<React.Fragment>
????????????<p?style={{?background:?color}}>value:?{value}p>
???<button?onClick={test}>點我button>
??React.Fragment>
?);
}
export?default?App;
useContext
const?context?=?useContext(Context)
useContext 從名字上就可以看出,它是以 Hook 的方式使用 React Context, 先簡單介紹 Context 的概念和使用方式
import?React,?{?useContext,?useState,?useEffect?}?from?"react";
const?ThemeContext?=?React.createContext(null);
const?Button?=?()?=>?{
??const?{?color,?setColor?}?=?React.useContext(ThemeContext);
??useEffect(()?=>?{
????console.info("Context?changed:",?color);
??},?[color]);
??const?handleClick?=?()?=>?{
????console.info("handleClick");
????setColor(color?===?"blue"???"red"?:?"blue");
??};
??return?(
????<button
??????type="button"
??????onClick={handleClick}
??????style={{?backgroundColor:?color,?color:?"white"?}}
????>
??????toggle?color?in?Child
????button>
??);
};
//?app.js
const?App?=?()?=>?{
??const?[color,?setColor]?=?useState("blue");
??return?(
????<ThemeContext.Provider?value={{?color,?setColor?}}>
??????<h3>
????????Color?in?Parent:?<span?style={{?color:?color?}}>{color}span>
??????h3>
??????<Button?/>
????ThemeContext.Provider>
??);
};
useReducer
const?[state,?dispatch]?=?useReducer(reducer,?initialArg,?init)
語法糖跟 redux 差不多,放個基礎(chǔ) ??
function?init(initialCount)?{
????return?{count:?initialCount};
}
function?reducer(state,?action)?{
????switch?(action.type)?{
????????case?'increment':
????????????return?{count:?state.count?+?1};
????????case?'decrement':
????????????return?{count:?state.count?-?1};
????????case?'reset':
????????????return?init(action.payload);
????????default:
????????????throw?new?Error();
????}
}
function?Counter({initialCount})?{
????const?[state,?dispatch]?=?useReducer(reducer,?initialCount,?init);
????return?(
????????<>
????????Count:?{state.count}
<button
????onClick={()?=>?dispatch({type:?'reset',?payload:?initialCount})}>
????Reset
button>
<button?onClick={()?=>?dispatch({type:?'increment'})}>+button>
<button?onClick={()?=>?dispatch({type:?'decrement'})}>-button>
>
);
}
useRef
const?refContainer?=?useRef(initialValue);
useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對象在組件的整個生命周期內(nèi)保持不變
解決引用問題--useRef 會在每次渲染時返回同一個 ref 對象
解決一些 this 指向問題
對比 createRef -- 在初始化階段兩個是沒區(qū)別的,但是在更新階段兩者是有區(qū)別的。
我們知道,在一個局部函數(shù)中,函數(shù)每一次 update,都會在把函數(shù)的變量重新生成一次。所以我們每更新一次組件, 就重新創(chuàng)建一次 ref, 這個時候繼續(xù)使用 createRef 顯然不合適,所以官方推出?useRef。useRef 創(chuàng)建的 ref 仿佛就像在函數(shù)外部定義的一個全局變量,不會隨著組件的更新而重新創(chuàng)建。但組件銷毀,它也會消失,不用手動進行銷毀
總結(jié)下就是 ceateRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用
useMemo
const?memoizedValue?=?useMemo(()?=>?computeExpensiveValue(a,?b),?[a,?b]);
一個常用來做性能優(yōu)化的 hook,看個 ??
const?MemoDemo?=?({?count,?color?})?=>?{
???useEffect(()?=>?{
???????console.log('count?effect')
???},?[count])
???const?newCount?=?useMemo(()?=>?{
???????console.log('count?觸發(fā)了')
???????return?Math.round(count)
???},?[count])
???const?newColor?=?useMemo(()?=>?{
???????console.log('color?觸發(fā)了')
???????return?color
???},?[color])
???return?<div>
???????<p>{count}p>
???????<p>{newCount}p>
???{newColor}div>
}
我們這個時候?qū)魅氲?count 值改變 的,log 執(zhí)行循序
count 觸發(fā)了
count effect
可以看出有點類似 effect, 監(jiān)聽 a、b 的值根據(jù)值是否變化來決定是否更新 UI memo 是在 DOM 更新前觸發(fā)的,就像官方所說的,類比生命周期就是 shouldComponentUpdate 對比?React.Memo?默認是是基于 props 的淺對比,也可以開啟第二個參數(shù)進行深對比。在最外層包裝了整個組件,并且需要手動寫一個方法比較那些具體的 props 不相同才進行 re-render。使用?useMemo 可以精細化控制,進行局部 Pure
useCallback
const?memoizedCallback?=?useCallback(
?()?=>?{
???doSomething(a,?b);
?},
?[a,?b],
);
useCallback 的用法和上面 useMemo 差不多,是專門用來緩存函數(shù)的 hooks
//?下面的情況可以保證組件重新渲染得到的方法都是同一個對象,避免在傳給onClick的時候每次都傳不同的函數(shù)引用
import?React,?{?useState,?useCallback?}?from?'react'
function?MemoCount()?{
???const?[value,?setValue]?=?useState(0)
???memoSetCount?=?useCallback(()=>{
???????setValue(value?+?1)
???},[])
???return?(
???????<div>
???????????<button
???????????????onClick={memoSetCount}
???????????????>
???????????????Update?Count
???????????button>
???????????<div>{value}div>
???????div>
???)
}
export?default?MemoCount
自定義 hooks
自定義 Hook 是一個函數(shù),其名稱以 “use” 開頭,函數(shù)內(nèi)部可以調(diào)用其他的 Hook 一般我將 hooks 分為這幾類
util
顧名思義工具類,比如 useDebounce、useInterval、useWindowSize 等等。例如下面 useWindowSize
import?{?useEffect,?useState?}?from?'react';
export?default?function?useWindowSize(el)?{
???const?[windowSize,?setWindowSize]?=?useState({
???????width:?undefined,
???????height:?undefined,
???});
???useEffect(
???????()?=>?{
???????????function?handleResize()?{
???????????????setWindowSize({
???????????????????width:?window.innerWidth,
???????????????????height:?window.innerHeight,
???????????????});
???????????}
???????????window.addEventListener('resize',?handleResize);
???????????handleResize();
???????????return?()?=>?window.removeEventListener('resize',?handleResize);
???????},
???????[el],
???);
???return?windowSize;
}
API
像之前的我們有一個公用的城市列表接口,在用 redux 的時候可以放在全局公用,不用的話我們就可能需要復(fù)制粘貼了。有了 hooks 以后我們只需要 use 一下就可以在其他地方復(fù)用了
import?{?useState,?useEffect?}?from?'react';
import?{?getCityList?}?from?'@/services/static';
const?useCityList?=?(params)?=>?{
???const?[cityList,?setList]?=?useState([]);
???const?[loading,?setLoading]?=?useState(true)
???const?getList?=?async?()?=>?{
???????const?{?success,?data?}?=?await?getCityList(params);
???????if?(success)?setList(data);
???????setLoading(false)
???};
???useEffect(
???????()?=>?{getList();},
???????[],
???);
???return?{
???????cityList,
???????loading
???};
};
export?default?useCityList;
//?bjs
function?App()?{
???//?...
???const?{?cityList,?loading?}?=?useCityList()
???//?...
}
logic
邏輯類,比如我們有一個點擊用戶頭像關(guān)注用戶或者取消關(guān)注的邏輯,可能在評論列表、用戶列表都會用到,我們可以這樣做
import?{?useState,?useEffect?}?from?'react';
import?{?followUser?}?from?'@/services/user';
const?useFollow?=?({?accountId,?isFollowing?})?=>?{
????const?[isFollow,?setFollow]?=?useState(false);
????const?[operationLoading,?setLoading]?=?useState(false)
????const?toggleSection?=?async?()?=>?{
????????setLoading(true)
????????const?{?success?}?=?await?followUser({?accountId?});
????????if?(success)?{
????????????setFollow(!isFollow);
????????}
????????setLoading(false)
????};
????useEffect(
????????()?=>?{
????????????setFollow(isFollowing);
????????},
????????[isFollowing],
????);
????return?{
????????isFollow,
????????toggleSection,
????????operationLoading
????};
};
export?default?useFollow;
只需暴露三個參數(shù)就能滿足大部分場景
UI
還有一些和 UI 一起綁定的 hook, 但是這里有點爭議要不要和 ui 一起混用。就我個人而言一起用確實幫我解決了部分復(fù)用問題,我還是分享出來。
import?React,?{?useState?}?from?'react';
import?{?Modal?}?from?'antd';
//?TODO?為了兼容一個頁面有多個?modal,?目前想法通過唯一?key?區(qū)分,后續(xù)優(yōu)化
export?default?function?useModal(key?=?'open')?{
????const?[opens,?setOpen]?=?useState({
????????[key]:?false,
????});
????const?onCancel?=?()?=>?{
????????setOpen({?[key]:?false?});
????};
????const?showModal?=?(type?=?key)?=>?{
????????setOpen({?[type]:?true?});
????};
????const?MyModal?=?(props)?=>?{
????????return?<Modal?key={key}?visible={opens[key]}?onCancel={onCancel}?{...props}?/>;
????};
????return?{
????????showModal,
????????MyModal,
????};
}
//?使用
function?App()?{
????const?{?showModal,?MyModal?}?=?useModal();
????return?<>
??????????<button?onClick={showModal}>展開button>
??????????<MyModal?onOk={console.log}?/>
???????>
}
邏輯跨端
最近聽了第十五屆的 D2 大會當軒大佬的《跨端的另一種思路》——Write Once 的分享,核心就是如何渲染、如何布局等 UI 層面的變化要遠遠大于業(yè)務(wù)邏輯層面,甚至是小程序和 Flutter,其大致的開發(fā)范式都沒有發(fā)生太大的改變,F(xiàn)lutter 開發(fā)范式和 React 非常相似,同樣是聲明式 UI,同樣存在 VirtualDOM。
同樣一段 useCount 的代碼,通過抽象 AST 到 dart, 如下

上面的算是高級應(yīng)用,我們?nèi)张e個簡單例子。一個項目要做 pc 站點又要做移動端,在不考慮雙端業(yè)務(wù)是否合理的情況下,這種情況 ui 能復(fù)用的地方不太多,但是業(yè)務(wù)邏輯能大量通過 hooks 進行復(fù)用,也算是是一個偽邏輯跨端
總結(jié)
越來越多的 react 配套的三方庫都上了 hooks 版,像 react-router、redux 都出了 hooks。同時也出現(xiàn)了一些好用的 hooks 庫,比如 ahooks 這種。自從用了 hooks 以后我就兩個字,真香。
推薦閱讀
