React Hooks 漸漸被國內前端團隊所接受,但基於 Hooks 的數據流方案卻還未固定,咱們有 「100 種」 相似的選擇,卻各有利弊,讓人難以取捨。前端
本週筆者就深刻談一談對 Hooks 數據流的理解,相信讀完文章後,能夠從百花齊放的 Hooks 數據流方案中看到本質。react
基於 React Hooks 談數據流,咱們先從最不容易產生分歧的基礎方案提及。git
單組件最簡單的數據流必定是 useState
:github
function App() {
const [count, setCount] = useState();
}
複製代碼
useState
在組件內用是毫無爭議的,那麼下個話題就必定是跨組件共享數據流了。redux
跨組件最簡單的方案就是 useContext
:緩存
const CountContext = createContext();
function App() {
const [count, setCount] = useState();
return (
<CountContext.Provider value={{ count, setCount }}> <Child /> </CountContext.Provider> ); } function Child() { const { count } = useContext(CountContext); } 複製代碼
用法都是官方 API,顯然也是毫無爭議的,但問題是數據與 UI 不解耦,這個問題 unstated-next 已經爲你想好解決方案了。微信
unstated-next 能夠幫你把上面例子中,定義在 App
中的數據單獨出來,造成一個自定義數據管理 Hook:ide
import { createContainer } from "unstated-next";
function useCounter() {
const [count, setCount] = useState();
return { count, setCount };
}
const Counter = createContainer(useCounter);
function App() {
return (
<Counter.Provider> <Child /> </Counter.Provider> ); } function Child() { const { count } = Counter.useContainer(); } 複製代碼
數據與 App
就解耦了,這下 Counter
不再和 App
綁定了,Counter
能夠和其餘組件綁定做用了。函數
這個時候性能問題就慢慢浮出了水面,首當其衝的就是 useState
沒法合併更新的問題,咱們天然想到利用 useReducer
解決。性能
useReducer
可讓數據合併更新,這也是 React 官方 API,毫無爭議:
import { createContainer } from "unstated-next";
function useCounter() {
const [state, dispath] = useReducer(
(state, action) => {
switch (action.type) {
case "setCount":
return {
...state,
count: action.setCount(state.count),
};
case "setFoo":
return {
...state,
foo: action.setFoo(state.foo),
};
default:
return state;
}
return state;
},
{ count: 0, foo: 0 }
);
return { ...state, dispatch };
}
const Counter = createContainer(useCounter);
function App() {
return (
<Counter.Provider> <Child /> </Counter.Provider> ); } function Child() { const { count } = Counter.useContainer(); } 複製代碼
這下即使要同時更新 count
和 foo
,咱們也能經過抽象成一個 reducer
的方式合併更新。
然而還有性能問題:
function ChildCount() {
const { count } = Counter.useContainer();
}
function ChildFoo() {
const { foo } = Counter.useContainer();
}
複製代碼
更新 foo
時,ChildCount
和 ChildFoo
同時會執行,但 ChildCount
沒用到 foo
呀?這個緣由是 Counter.useContainer
提供的數據流是一個引用總體,其子節點 foo
引用變化後會致使整個 Hook 從新執行,繼而全部引用它的組件也會從新渲染。
此時咱們發現能夠利用 Redux useSelector
實現按需更新。
首先咱們利用 Redux 對數據流作一次改造:
import { createStore } from "redux";
import { Provider, useSelector } from "react-redux";
function reducer(state, action) {
switch (action.type) {
case "setCount":
return {
...state,
count: action.setCount(state.count),
};
case "setFoo":
return {
...state,
foo: action.setFoo(state.foo),
};
default:
return state;
}
return state;
}
function App() {
return (
<Provider store={store}> <Child /> </Provider>
);
}
function Child() {
const { count } = useSelector(
(state) => ({ count: state.count }),
shallowEqual
);
}
複製代碼
useSelector
可讓 Child
在 count
變化時才更新,而 foo
變化時不更新,這已經接近較爲理想的性能目標了。
但 useSelector
的做用僅僅是計算結果不變化時阻止組件刷新,但並不能保證返回結果的引用不變化。
對於上面的場景,拿到 count
的引用是不變的,但對於其餘場景就不必定了。
舉個例子:
function Child() {
const user = useSelector((state) => ({ user: state.user }), shallowEqual);
return <UserPage user={user} />; } 複製代碼
假設 user
對象在每次數據流更新引用都會發生變化,那麼 shallowEqual
天然是不起做用,那咱們換成 deepEqual
深對比呢?結果是引用依然會變,只是重渲染不那麼頻繁了:
function Child() {
const user = useSelector(
(state) => ({ user: state.user }),
// 當 user 值變化時才重渲染
deepEqual
);
// 但此處拿到的 user 引用仍是會變化
return <UserPage user={user} />; } 複製代碼
是否是以爲在 deepEqual
的做用下,沒有觸發重渲染,user
的引用就不會變呢?答案是會變,由於 user
對象在每次數據流更新都會變,useSelector
在 deepEqual
做用下沒有觸發重渲染,但由於全局 reducer 隱去組件本身的重渲染依然會從新執行此函數,此時拿到的 user
引用會不斷變化。
所以 useSelector
deepEqual
必定要和 useDeepMemo
結合使用,才能保證 user
引用不會頻繁改變:
function Child() {
const user = useSelector(
(state) => ({ user: state.user }),
// 當 user 值變化時才重渲染
deepEqual
);
const userDeep = useDeepMemo(() => user, [user]);
return <UserPage user={user} />; } 複製代碼
固然這是比較極端的狀況,只要看到 deepEqual
與 useSelector
同時做用了,就要問問本身其返回的值的引用會不會發生意外變化。
對於極限場景,即使控制了重渲染次數與返回結果的引用最大程度不變,仍是可能存在性能問題,這最後一塊性能問題就處在查詢函數上。
上面的例子中,查詢函數比較簡單,但若是查詢函數很是複雜就不同了:
function Child() {
const user = useSelector(
(state) => ({ user: verySlowFunction(state.user) }),
// 當 user 值變化時才重渲染
deepEqual
);
const userDeep = useDeepMemo(() => user, [user]);
return <UserPage user={user} />; } 複製代碼
咱們假設 verySlowFunction
要遍歷畫布中 1000 個組件的 n 3 次方次,那組件的重渲染時間消耗與查詢時間相比徹底不值一提,咱們須要考慮緩存查詢函數。
一種方式是利用 reselect 根據參數引用進行緩存。
想象一下,若是 state.user
的引用不頻繁變化,但 verySlowFunction
很是慢,理想狀況是 state.user
引用變化後才從新執行 verySlowFunction
,但上面的例子中,useSelector
並不知道還能這麼優化,只能傻傻的每次渲染重複執行 verySlowFunction
,哪怕 state.user
沒有變。
此時咱們要告訴引用,state.user
是否變化纔是從新執行的關鍵:
import { createSelector } from "reselect";
const userSelector = createSelector(
(state) => state.user,
(user) => verySlowFunction(user)
);
function Child() {
const user = useSelector(
(state) => userSelector(state),
// 當 user 值變化時才重渲染
deepEqual
);
const userDeep = useDeepMemo(() => user, [user]);
return <UserPage user={user} />; } 複製代碼
在上面的例子中,經過 createSelector
建立的 userSelector
會一層層進行緩存,當第一個參數返回的 state.user
引用不變時,會直接返回上一次執行結果,直到其應用變化了纔會繼續往下執行。
這也說明了函數式保持冪等的重要性,若是
verySlowFunction
不是嚴格冪等的,這種緩存也沒法實施。
看上去很美好,然而實戰中你可能發現沒有那麼美好,由於上面的例子都創建在 Selector 徹底不依賴外部變量。
若是咱們要查詢的用戶來自於不一樣地區,須要傳遞 areaId
加以識別,那麼能夠拆分爲兩個 Selector 函數:
import { createSelector } from "reselect";
const areaSelector = (state, props) => state.areas[props.areaId].user;
const userSelector = createSelector(areaSelector, (user) =>
verySlowFunction(user)
);
function Child() {
const user = useSelector(
(state) => userSelector(state, { areaId: 1 }),
deepEqual
);
const userDeep = useDeepMemo(() => user, [user]);
return <UserPage user={user} />; } 複製代碼
因此爲了避免在組件函數內調用 createSelector
,咱們須要儘量將用到外部變量的地方抽象成一個通用 Selector,並做爲 createSelector
的一個先手環節。
但 userSelector
提供給多個組件使用時緩存會失效,緣由是咱們只建立了一個 Selector 實例,所以這個函數還須要再包裝一層高階形態:
import { createSelector } from "reselect";
const userSelector = () =>
createSelector(areaSelector, (user) => verySlowFunction(user));
function Child() {
const customSelector = useMemo(userSelector, []);
const user = useSelector(
(state) => customSelector(state, { areaId: 1 }),
deepEqual
);
}
複製代碼
因此對於外部變量結合的環節,還須要 useMemo
與 useSelector
結合使用,useMemo
處理外部變量依賴的引用緩存,useSelector
處理 Store 相關引用緩存。
基於 Hooks 的數據流方案不能算完美,我在寫做這篇文章時就感受到這種方案屬於 「淺入深出」,簡單場景還容易理解,隨着場景逐步複雜,方案也變得愈來愈複雜。
但這種 Immutable 的數據流管理思路給了開發者很是自由的緩存控制能力,只要透徹理解上述概念,就能夠開發出很是 「符合預期」 的數據緩存管理模型,只要精心維護,一切就變得很是有秩序。
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)