React使用Context的一些優(yōu)化建議
常用 API
React.createContext
const MyContext = React.createContext(defaultValue);
創(chuàng)建一個 Context 對象。當(dāng) React 渲染一個訂閱了這個 Context 對象的組件,這個組件會從組件樹中離自身最近的那個匹配的 Provider 中讀取到當(dāng)前的 context 值。
Context.Provider
<MyContext.Provider value={/* 某個值 */}>
每個 Context 對象都會返回一個 Provider React 組件,它允許消費組件訂閱 context 的變化。
Provider 接收一個 value 屬性,傳遞給消費組件。一個 Provider 可以和多個消費組件有對應(yīng)關(guān)系。多個 Provider 也可以嵌套使用,里層的會覆蓋外層的數(shù)據(jù)。
useContext
const store = useContext(MyContext)
接收一個 context 對象(React.createContext 的返回值)并返回該 context 的當(dāng)前值。當(dāng)前的 context 值由上層組件中距離當(dāng)前組件最近的 <MyContext.Provider> 的 value prop 決定。
當(dāng) Provider 的 value 值發(fā)生變化時,它內(nèi)部的所有消費組件都會重新渲染
了解了 API 后,我們來看一個簡單的例子。
示例
index.js
const MyContext = React.createContext(null);
function reducer(state, action) {
switch (action.type) {
case 'addCount': {
return {
...state,
count: state.count + 1
}
}
case 'addNum': {
return {
...state,
num: state.num + 1
}
}
default: return state;
}
}
const MyProvider = ({ children }) => {
const [store, dispatch] = useReducer(reducer, { count: 0, num: 0 })
return <MyContext.Provider value={{store, dispatch}}>{children}</MyContext.Provider>
};
export default () => {
return (
<MyProvider>
<ChildCount />
<ChildNum />
<Child />
</MyProvider>
);
}
ChildCount.js
export default () => {
const { state, dispatch } = React.useContext(MyContext);
console.log('re-render ChildCount', state.count)
return (
<>
<div>count is: {state.count}</div>
<button onClick={() => dispatch({ type: 'addCount' })}> AddCount </button>
</>
)
}
ChildNum.js
export default () => {
const { state, dispatch } = React.useContext(MyContext);
console.log('re-render ChildNum', state.num)
return (
<>
<div>num is: {state.num}</div>
<button onClick={() => dispatch({ type: 'addNum' })}> AddNum </button>
</>
)
}
Child.js
export default () => {
console.log('re-render Child')
return <div>Child</div>
}
點擊 AddCount 按鈕,輸出:
re-render ChildCount 1re-render ChildNum 0
點擊 AddNum 按鈕,輸出:
re-render ChildCount 1re-render ChildNum 1
我們可以發(fā)現(xiàn),Context.Provider 下的所有消費組件,在 Provider.value 變化后,都會 re-render
改變 count 、num 任意一個值,ChildCount,ChildNum 都會 re-render
針對以上 re-render 情況,有以下方案可以優(yōu)化
優(yōu)化
針對子組件做函數(shù)記憶
React.memo
我們?nèi)缦滦薷乃械?Child 組件
export default React.memo(() => {
const { state, dispatch } = React.useContext(MyContext);
console.log('re-render ChildCount', state.count)
return (
<>
<div>count is: {state.count}</div>
<button onClick={() => dispatch({ type: 'addCount' })}> AddCount </button>
</>
)
})
點擊 AddCount 后發(fā)現(xiàn),依然打印出
re-render ChildCount 1
re-render ChildNum 0
我們重新認(rèn)識下 React.memo
React.memo 默認(rèn)情況下僅僅對傳入的 props 做淺比較,如果是內(nèi)部自身狀態(tài)更新(useState, useContext等),依然會重新渲染,在上面的例子中,useContext 返回的 state 一直在變化,導(dǎo)致就算被 memo 包裹的組件依然觸發(fā)更新了。
useMemo
我們?nèi)缦滦薷乃械?Child 組件
export default () => {
const { state, dispatch } = React.useContext(MyContext);
return useMemo(() => {
console.log('re-render ChildCount', state.count)
return (
<>
<div>count is: {state.count}</div>
<button onClick={() => dispatch({ type: 'addCount' })}> AddCount </button>
</>
)
}, [state.count, dispatch])
}
點擊 addCount 后發(fā)現(xiàn),只打印出了
re-render ChildCount 1
點擊 addNum 后發(fā)現(xiàn),只打印出了
re-render ChildNum 1
useMemo 可以做更細(xì)粒度的緩存,我們可以在依賴數(shù)組里來管理組件是否更新
我們可以思考一下,有沒有一種辦法,不用 useMemo 也可以做到按需渲染。就像 react-redux 中 useSelector 一樣實現(xiàn)按需渲染
動手實現(xiàn) useSelector
我們先想一下,在上面的例子中,觸發(fā)子組件re-render的原因是什么?
沒錯就是因為 Provider.value 的值一直在變更,那我們要想個辦法讓子組件感知不到 value 的變更,同時在 value 的某個值發(fā)生變更的時候,能夠觸發(fā)消費 value 的子組件 re-render
我們使用 觀察者模式 實現(xiàn)
1、我們使用 useMemo 緩存首次的 value,讓子組件感知不到 value 的變化
2、如果 value 不變化,那子組件就不會re-render,此時我們需要在真正 value 變化的時候,re-render 子組件,我們需要一個 hooks(useSelector) 幫助我們實現(xiàn)子組件 re-render
3、子組件在初始化時,useSelector 要幫助其訂閱 state 變更的回調(diào)函數(shù),并返回最新的 state(函數(shù)內(nèi)部獲取前后兩次的 state 做對比,不一樣則強制更新組件)
4、在 Context.Provider 中創(chuàng)建一個收集子組件訂閱state變更回調(diào)的集合,在其內(nèi)部監(jiān)聽 state(value),如果變更則遍歷集合,執(zhí)行所有回調(diào)函數(shù)
基于以上,我們依次實現(xiàn)了Context.Provider, useSelector, useDispatch
Context.Provider
const MyProvider = ({children}) => {
const [state, dispatch] = useReducer(reducer, initState);
// ref state
const stateRef = useRef(null);
stateRef.current = state;
// ref 訂閱回調(diào)數(shù)組
const subscribersRef = useRef([]);
// state 變化,遍歷執(zhí)行回調(diào)
useEffect(() => {
subscribersRef.current.forEach(sub => sub());
}, [state]);
// 緩存 value, 利用 ref 拿到最新的 state, subscribe 狀態(tài)
const value = useMemo(
() => ({
dispatch,
subscribe: cb => {
subscribersRef.current.push(cb);
return () => {
subscribersRef.current = subscribersRef.current.filter(item => item !== cb);
};
},
getState: () => stateRef.current
}),
[]
)
return <MyContext.Provider children={children} value={value} />;
}
useSelector
export const useSelector = selector => {
// 強制更新
const [, forceRender] = useReducer(v => v + 1, 0);
const store = useContext(MyContext);
// 獲取當(dāng)前使用的 state
const selectedStateRef = useRef(null)
selectedStateRef.current = selector(store.getState());
// 對比更新回調(diào)
const checkForUpdates = useCallback(() => {
// 獲取變更后的 state
const newState = selector(store.getState());
// 對比前后兩次 state
if (newState !== selectedStateRef.current) forceRender({});
}, [store]);
// 訂閱 state
useEffect(() => {
const subscription = store.subscribe(checkForUpdates);
return () => subscription();
}, [store, checkForUpdates]);
// 返回需要的 state
return selectedStateRef.current;
}
useDispatch
export const useDispatch = () => {
const store = useContext(MyContext);
return store.dispatch
}
我們用上面重寫的 API,改寫下剛開始的例子
index.js
export default () => {
return (
<MyProvider>
<ChildCount />
<ChildNum />
<Child />
</Provider>
);
}
ChildCount.js
export default () => {
const dispatch = useDispatch();
const count = useSelector(state => state.count);
console.log('re-render ChildCount', count)
return (
<>
<div>count is: {count}</div>
<button onClick={() => dispatch({ type: 'addCount' });}> AddCount </button>
</>
)
};
ChildNum.js
export default () => {
const dispatch = useDispatch();
const num = useSelector(state => state.num);
console.log('re-render ChildNum', num)
return (
<>
<div>num is: {num}</div>
<button onClick={() => dispatch({ type: 'addNum' });}> AddNum </button>
</>
)
}
Child.js
export default () => {
console.log('re-render Child')
return <div>Child</div>
}
點擊AddCount: 只打印了 re-render ChildCount 1
點擊AddNum: 只打印了 re-render ChildNum 1
以上通過對 Context 使用中的一些思考,我們簡單的實現(xiàn)了 useSelector,實現(xiàn)了 Context 組件的按需渲染
總結(jié)
在使用 Context API 的時候,要避免不必要的re-render,可以使用 useMemo 做細(xì)粒度更新,也可以使用 useSelector 實現(xiàn)按需渲染
以上就是React使用Context的一些優(yōu)化建議的詳細(xì)內(nèi)容,更多關(guān)于React Context的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談react-router HashRouter和BrowserRouter的使用
本篇文章主要介紹了淺談react-router HashRouter和BrowserRouter的使用,具有一定的參考價值,有興趣的可以了解一下2017-12-12
解決React報錯`value` prop on `input` should&
這篇文章主要為大家介紹了React報錯`value` prop on `input` should not be null解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
React Native 混合開發(fā)多入口加載方式詳解
這篇文章主要介紹了React Native 混合開發(fā)多入口加載方式詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
react實現(xiàn)動態(tài)增減表單項的示例代碼
在做項目的時候,甲方給的信息有限,網(wǎng)頁的備案信息寫成固定的,之后驗收的時候,甲方要求把這個備案信息寫成動態(tài)的,可以自增減,下面通過實例代碼給大家介紹react實現(xiàn)動態(tài)增減表單項的示例,感興趣的朋友跟隨小編一起看看吧2024-05-05
淺談React組件props默認(rèn)值的設(shè)置
本文主要介紹了淺談React組件props默認(rèn)值的設(shè)置,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
React 模塊聯(lián)邦多模塊項目實戰(zhàn)詳解
這篇文章主要介紹了React 模塊聯(lián)邦多模塊項目實戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10

