createContext & useContext 上下文 跨组件透传与性能优化篇

共 14839字,需浏览 30分钟

 ·

2023-05-30 10:46


关注  前端技术专栏  ,回复“  资源  ”免费领取全套视频教程
createContext ‍‍
createContext api 可以创建一个 React 的 上下文对象,如果使用了这个上下文对象中Provider组件,就可以拿到上下文中提供的数据或者其它信息。 使用方式:
      
        const defaultValue = {}
      
      
        const MyContext = React.createContext(defaultValue)
      
    
如果要使用创建的上下文,需要通过 Context 对象上的 Provider 最外层包装组件,使用方式如下:
      
        <MyContext.Provider value={{ a123b222fn() => console.log('fn1')))) }}>
      
      
            <A>
      
      
                <MyContext1.Provider value={{ fn: () => console.log('fn2'))) }}>
      
      
                    <B>
      
      
                        <C></C>
      
      
                    </B>
      
      
                </MyContext1.Provider>
      
      
            </A>
      
      
        
          </MyContext.Provider>
        
      
    

需要通过上面的方式传入value值,指定 Context 要暴露的信息。

子组件在匹配过程中只会匹配最新的 Provider,如果 MyContext 和 MyContext1 提供了相同的方法,则 C 组件只会选择 MyContext1 提供的方法。


默认值的作用?

如果匹配不到最新的 Provider 就会使用默认值,默认值一般只有在 对组件进行单元测试 (组件并未嵌入到父组件中)的时候比较有用。
‍  使用useContext获取上下文

通过 createContext 创建出来的上下文对象,在子组件中可以通过  useContext  获取 Provider 提供的内容

      
        const { fn, a, b } = useContext(MyContext);
      
    
可以发现useContext 需要将  MyContext 这个  Context 实例传入 这种用法会存在一个比较尴尬的地方,就是父子组件如果不在一个目录中,如何共享 MyContext 这个 Context 实例呢? 一般这种情况下,可以通过 Context Manager 统一管理上下文的实例,然后通过 export 将实例导出,在子组件中将实例 import 进来。 示例: 创建一个contextmanager文件进行统一管理Context创建实例然后将其导出;
      
        import React from 'react';
      
      
        export const MyContext = React.createContext();
      
      
        export const MyContext1 = React.createContext();
      
    

在需要使用到对应实例的组件中分别去将对应Context实例导入进去使用

      
        import { useContext } from 'react';
      
      
        import { MyContext1 } from '@/utils/contextmanager'
      
      
        
          
const Component = () => { const { fn } = useContext(MyContext1); return <>Component</> }


‍ createContext和useContext实现数据共享
例子:比如子组件中需要修改父组件的 state 状态 一般的做法是将父组件的方法比如 setXXX 通过 props 的方式传给子组件,而一旦子组件多层级的话,就要层层透传。 如果使用 Context 就可以避免这种层层透传 父组件Provider提供上下文value
      
        import React, { useState } from 'react';
      
      
        import Child from './Child';
      
      
        import { MyContext } from '@/utils/contextmanager';
      
      
        
          
const Parent = (props = {}) => { const [step, setStep] = useState(0); const [count, setCount] = useState(0); const [number, setNumber] = useState(0);
return ( <MyContext.Provider value={{ setStep, setCount, setNumber }}> <A> <Child step={step} number={number} count={count} /> </A> </MyContext.Provider> ); }
export default Parent;

子组件 useContext 使用上下文
      
        import React, { useContext, memo } from 'react';
      
      
        import { MyContext } from '@/utils/contextmanager';
      
      
        
          
const Child = memo((props = {}) => { const { setStep, setNumber, setCount } = useContext(MyContext);
return ( <> <p>step is : {props.step}</p> <p>number is : {props.number}</p> <p>count is : {props.count}</p> <hr /> <div> <button onClick={() => { setStep(props.step + 1) }}>step ++</button> <button onClick={() => { setNumber(props.number + 1) }}>number ++</button> <button onClick={() => { setCount(props.step + props.number) }}>number + step</button> </div> </> ); });
export default Child;

效果

b97a20b0d15b419e8b381cff0421c803.webp


关于使用memo是为了说明这个写法在这里是多余的 memo的作用是为了减少组件重新render过程中导致组件的重复渲染,而得到性能上的提升。 但是 Context 发生的变化是无法通过 memo 进行优化的,这个大家需要知道的一个点。
‍  使用useReducer优化Context复杂度
上面的示例虽然实现了多级组件方法共享,但是暴露出一个问题。 就是把所有的方法都放在了 MyContext.Provider.value 属性中传递,必然造成整个 Context Provider 提供的方法越来越多,让维护变的就更复杂了。 这里其实可以通过useReducer包装,通过dispatch去触发action进行数据更新,所以我们可以如下优化一下上面代码 父组件
      
        import React, { useReducer } from 'react';
      
      
        import Child from './Child';
      
      
        import { MyContext } from '@/utils/contextmanager';
      
      
        
          
const initState = { count: 0, step: 0, number: 0 };
const reducer = (state, action) => { switch (action.type) { case 'step': return Object.assign({}, state, { step: state.step + 1 }); case 'number': return Object.assign({}, state, { number: state.number + 1 }); case 'count': return Object.assign({}, state, { count: state.step + state.number }); default: return state; } }
const Parent = (props = {}) => { const [state: { step, number, count }, dispatch] = useReducer(reducer, initState);
return ( <MyContext.Provider value={{ dispatch }}> <Child step={step} number={number} count={count} /> </MyContext.Provider> ); }
export default Parent;

子组件
      
        import React, { useContext, memo } from 'react';
      
      
        import { MyContext } from '@/utils/contextmanager';
      
      
        
          
const Child = memo((props = {}) => { const { dispatch } = useContext(MyContext);
return ( <> <p>step is : {props.step}</p> <p>number is : {props.number}</p> <p>count is : {props.count}</p> <hr /> <div> <button onClick={() => { dispatch({ type: 'setp' }) }}>step ++</button> <button onClick={() => { dispatch({ type: 'number' }) }}>number ++</button> <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button> </div> </> ); });
export default Child;

‍ 将state也通过Context传递给子组件

其实上面的例子,子组件获取父组件的 state 还是通过 props传递的,其实也会存在层层嵌套

如果将整个 state 通过 Context 传入就无需层层组件的 props 传递(如果不需要整个state,可以只将某几个 state 给 Provider)

优化后,父组件

      
        import React, { useReducer, useCallback } from 'react';
      
      
        import Child from './Child';
      
      
        import { MyContext } from '@/utils/contextmanager';
      
      
        
          
const initState = { count: 0, step: 0, number: 0 };
const reducer = (state, action) => { switch (action.type) { case 'step': return Object.assign({}, state, { step: state.step + 1 }); case 'number': return Object.assign({}, state, { number: state.number + 1 }); case 'count': return Object.assign({}, state, { count: state.step + state.number }); default: return state; } }
const Parent = (props = {}) => { const [state, dispatch] = useReducer(reducer, initState); const parentStepHandler = useCallback(() => { dispatch({ type: 'step' }) }, [])
return ( <MyContext.Provider value={{ dispatch, state }}> <button onClick={parentStepHandler}>parent step ++</button> <Child /> </MyContext.Provider> ); }
export default Parent;

优化后,子组件
      
        import React, { useContext, memo } from 'react';
      
      
        import { MyContext } from '@/utils/contextmanager';
      
      
        
          
const Child = memo((props = {}) => { const { state: { step, number, count }, dispatch } = useContext(MyContext);
return ( <> {console.log('[Child] RELOAD-RENDER')} <p>step is : { step }</p> <p>number is : { number }</p> <p>count is : { count }</p> <hr /> <div> <button onClick={() => { dispatch({ type: 'setp' }) }}>step ++</button> <button onClick={() => { dispatch({ type: 'number' }) }}>number ++</button> <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button> </div> </> ); });
export default Child;
效果

47e15cd78161f63c3edd693461539f34.webp


直接使用父组件 state 的性能问题 注意看上面的动图,在点击子组件的 【number + step】 按钮的时候,虽然 count 的值没有发生任何变化,但是一直触发[Child] RELOAD-RENDER 的打印,即使子组件是通过 memo 包装过的。 出现这个问题原因是 memo 只会对 props 进行浅比较,而我们直接将 state 注入到了组件内部,因此 state 的变化必然会触发[Child] RELOAD-RENDER,整个 state 变化是绕过了 memo。
‍  使用useMemo方式来解决上面state透传性能问题
使用 useMemo 优化子组件渲染
      
        import React, { useContext, useMemo } from 'react';
      
      
        import { MyContext } from '@/utils/contextmanager';
      
      
        
          
const Child = (props = {}) => { const { state: { step, number, count }, dispatch } = useContext(MyContext);
const Content = useMemo(() => { return <> {console.log('[Child] RELOAD-RENDER')} <p>step is : { step }</p> <p>number is : { number }</p> <p>count is : { count }</p> <hr /> <div> <button onClick={() => { dispatch({ type: 'setp' }) }}>step ++</button> <button onClick={() => { dispatch({ type: 'number' }) }}>number ++</button> <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button> </div> </> }, [step, number, count, dispatch]);
return Content; };
export default Child;

效果

dd0f753e846ee978d52af13d8c057c5c.webp



通过上面效果可以看到,点击 number + step 按钮不变的时候是不会再触发打印的,所以DOM是没有被重新渲染的。




猜你爱看
vue的$nextTick的使用+源码分析
一文弄懂React 16.8 新特性React Hooks的使用
深入理解JavaScript位运算符



微  博:前端吴佳

QQ群:856363266






长按识别二维码

关注 「前端技术专栏」 加星标

每天给您推送最新原创技术文章



好看,帮点击 在看 ❤️

浏览 69
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报