React使用Context的一些優(yōu)化建議
常用 API
React.createContext
const MyContext = React.createContext(defaultValue);
創(chuàng)建一個 Context 對象。當 React 渲染一個訂閱了這個 Context 對象的組件,這個組件會從組件樹中離自身最近的那個匹配的 Provider
中讀取到當前的 context 值。
Context.Provider
<MyContext.Provider value={/* 某個值 */}>
每個 Context 對象都會返回一個 Provider React 組件,它允許消費組件訂閱 context 的變化。
Provider 接收一個 value
屬性,傳遞給消費組件。一個 Provider 可以和多個消費組件有對應關系。多個 Provider 也可以嵌套使用,里層的會覆蓋外層的數(shù)據(jù)。
useContext
const store = useContext(MyContext)
接收一個 context 對象(React.createContext
的返回值)并返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider>
的 value
prop 決定。
當 Provider 的 value 值發(fā)生變化時,它內部的所有消費組件都會重新渲染
了解了 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
我們如下修改所有的 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
我們重新認識下 React.memo
React.memo 默認情況下僅僅對傳入的 props 做淺比較,如果是內部自身狀態(tài)更新(useState, useContext等),依然會重新渲染,在上面的例子中,useContext 返回的 state 一直在變化,導致就算被 memo 包裹的組件依然觸發(fā)更新了。
useMemo
我們如下修改所有的 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 可以做更細粒度的緩存,我們可以在依賴數(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 變更的回調函數(shù),并返回最新的 state(函數(shù)內部獲取前后兩次的 state 做對比,不一樣則強制更新組件)
4、在 Context.Provider
中創(chuàng)建一個收集子組件訂閱state變更回調的集合,在其內部監(jiān)聽 state(value)
,如果變更則遍歷集合,執(zhí)行所有回調函數(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 訂閱回調數(shù)組 const subscribersRef = useRef([]); // state 變化,遍歷執(zhí)行回調 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); // 獲取當前使用的 state const selectedStateRef = useRef(null) selectedStateRef.current = selector(store.getState()); // 對比更新回調 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 組件的按需渲染
總結
在使用 Context API 的時候,要避免不必要的re-render
,可以使用 useMemo
做細粒度更新,也可以使用 useSelector
實現(xiàn)按需渲染
以上就是React使用Context的一些優(yōu)化建議的詳細內容,更多關于React Context的資料請關注腳本之家其它相關文章!
相關文章
react ant-design Select組件下拉框map不顯示的解決
這篇文章主要介紹了react ant-design Select組件下拉框map不顯示的解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03React的createElement和render手寫實現(xiàn)示例
這篇文章主要為大家介紹了React的createElement和render手寫實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08react源碼層深入刨析babel解析jsx實現(xiàn)
同作為MVVM框架,React相比于Vue來講,上手更需要JavaScript功底深厚一些,本系列將閱讀React相關源碼,從jsx -> VDom -> RDOM等一些列的過程,將會在本系列中一一講解2022-10-10React+echarts?(echarts-for-react)?實現(xiàn)中國地圖及省份切換功能
這篇文章主要介紹了React+echarts?(echarts-for-react)?畫中國地圖及省份切換,有足夠的地圖數(shù)據(jù),可以點擊到街道,示例我只出到市級,本文結合實例代碼給大家介紹的非常詳細需要的朋友可以參考下2022-11-11詳解在create-react-app使用less與antd按需加載
這篇文章主要介紹了詳解在create-react-app使用less與antd按需加載,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12詳解react-router 4.0 下服務器如何配合BrowserRouter
這篇文章主要介紹了詳解react-router 4.0 下服務器如何配合BrowserRouter,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12