第八章 React Context 与 性能 下

文章出处:www.advanced-react.com/

专栏地址:juejin.cn/column/7443...

避免不必要的Context重新渲染:切分Providers

除了所有上下文(Context)消费者组件会在值发生变化时重新渲染这一事实之外,不仅要着重强调 "值发生变化" 这一点,还要强调所有消费者组件都会如此进行重新渲染这一点,这很重要。如果我给我们的导航 API 引入实际上并不依赖状态的打开和关闭功能,如下情况:

js 复制代码
const SomeComponent = () => {
    // no dependencies, open won't change
    const open = useCallback(() => setIsNavExpanded(true), []);
    
    // no dependencies, open won't change
    const close = useCallback(() => setIsNavExpanded(false), []);
    
    const value = useMemo(() => {
        return { isNavExpanded, open, close };
    }, [isNavExpanded, open, close]);
    
    return ...
}

SomeComponent组件会在Context的Provider发生变化时,而进行重新渲染,尽管open函数并没有发生任何变化。

并且无论进行多少记忆化(memoization)操作都无法阻止它。例如,下面这种做法是行不通的:

js 复制代码
const SomeComponent = () => {
    const { open } = useNavigation();
    
    return useCallback(open, []);
}

不过,我们可以使用一种名为 "拆分提供者(splitting providers)" 的有趣技术来达成期望的结果。

代码是下面这样的。我们不再把所有内容都放在一个Context里面,我们可以生成两个Context:一个Context用于存储isNavExpanded值,另一个存储其他值。

js 复制代码
// store the state here
const ContextData = React.createContext({
    isNavExpanded: false,
});
// store the open/close functions here
const ContextApi = React.createContext({
    open: () => {},
    close: () => {},
});

然后,我们不再使用单一的Provider,而是使用两个:

js 复制代码
const NavigationController = ({ children }) => {
    ...
    
    return (
         <ContextData.Provider value={data}>
             <ContextApi.Provider value={api}>
                 {children}
             </ContextApi.Provider>
         </ContextData.Provider>
    )
}

我们会把已经缓存的data和api传递给对应的Provider

js 复制代码
const NavigationController = ({ children }) => {
    ...
    
    const data = useMemo(() => { isNavExpanded }, [isNavExpanded]);
    
    const api = useMemo(() => { open, close }, [open, close]);
    
    return (
         <ContextData.Provider value={data}>
             <ContextApi.Provider value={api}>
                 {children}
             </ContextApi.Provider>
         </ContextData.Provider>
    )
}

很遗憾,我们不得不在这里去掉切换(toggle)功能。它依赖于状态,所以我们没办法把它放进 API(相关模块或组件)里,而且把它包含在数据部分也没什么实际意义。

现在,我们只需要引入两个钩子(hooks)来对上下文(Context)进行抽象处理即可:

js 复制代码
const useNavigationData = () => useContext(ContextData);
const useNavigationApi = () => useContext(ContextApi);

之后,SomeComponent可以自由地使用open函数了。它可以自由地调用展开/折叠 函数,但SomeComponent的一些子组件不会因此而重新渲染:

js 复制代码
const SomeComponent = () => {
    const { open } = useNavigationApi();
    
    return ...
}

我们之前是使用useNavigation钩子来获取isNavExpanded,而现在我们使用useNavigationData,不用改变其他代码:

js 复制代码
const AdjustableColumnsBlock = () => {
    cosnt { isNavExpanded } = useNavigationData();
    
    return isNavExpanded ? <TwoColumns /> : <ThreeColumns />
}

代码示例: advanced-react.com/examples/08...

当然,我们可以根据自身需求尽可能细致地拆分这些提供者(providers)。这完全取决于什么做法对您的应用程序来说是合理的,以及因上下文(Context)导致的重新渲染是否确实有害。

Reducers 与 切分Providers

正如你可能从上面内容注意到的那样,我不得不从我们的应用程序中去掉切换功能。遗憾的是,这个切换功能依赖于状态,所以如果我把它添加到 API 提供者中,它也会开始依赖状态,这样一来,之前所做的分离就没什么意义了:

js 复制代码
const NavigationController = ({ children }) => {
    ...
    // depends on isNavExpanded
    const toggle = useCallback(() => setIsNavExpanded(!isNavExpanded), [isNavExpanded]);
    
    // with toggle it has to depend on isNavExpanded through toggle function
    // so will change with every state update
    const api = useMemo(() => ({ open, close, toggle}), [open, close, toggle]);
    
    return (
         <ContextData.Provider value={data}>
             <ContextApi.Provider value={api}>
                 {children}
             </ContextApi.Provider>
         </ContextData.Provider>
    )
}

这不是最理想的方式。任何使用这个状态的人,需要自己实现一个切换函数:

js 复制代码
const ExpandButton = () => {
    const { isNavExpanded, open, close } = useNavigation();
    
    return (
        <button onClick={isNavExpanded ? close: open}>
            {isNavExpanded ? 'Collpase' : 'Expand'}
        </button>
    );
};

这不是最理想的情况。理想情况是,这个钩子可以自动处理这些常见情况。

好在,我们可以用useReducer来实现这个。

useReducer是另一种管理组件状态的方法。在useReducer中,我们不再手动式的有意思的操作状态,reducers模式允许我们派发不同的"actions"。这个模式在处理复杂的状态,或者复杂的状态操作时,是非常有用的。

在这个例子中,手动操作数据的代码是这样的:

js 复制代码
const [isNavExpanded, setIsNavExpanded] = useState();

const toggle = () => setIsNavExpanded(!isNavExpanded);
const open = () => setIsNavExpanded(true);
const close = () => setIsNavExpanded(false);

当我们引入reducer:

js 复制代码
const [state, dispatch] = useReducer(reducer, {
    isNavExpanded: true,
})

我们这样声明操作函数:

js 复制代码
const toggle = () => dispatch({ type: 'toggle-sidebar' });
const open = () => dispatch({ type: 'open-sidebar' });
const close = () => dispatch({ type: 'close-sidebar' });

注意,现在没有一个函数是依赖状态了,它们仅仅是在分发一个action。

之后,我们引入reducer函数。我们需要在reducer函数里面实现所有类型的action的数据操作:

js 复制代码
const reducer = (state, action) => {
    ...
}

为了实现它,我们使用了简单的 switch/case 操作:

js 复制代码
const reducer = (state, action) => {
    switch(action.type) {
        case 'open-sidebar':
            retrun { ...state, isNavExpanded: true };
        case 'close-sidebar':
            retrun { ...state, isNavExpanded: false };
        case 'toggle-siderbar:
            return {
                ...state,
                isNavExpanded: !state.isNavExpanded,
            };
    }
}

之后,我们需要做的是把这些函数加到api里面:

js 复制代码
const NavigationController = () => {
    // state and dispatch are returned from the useReducer
    const [state, dispatch] = useReducer(reducer, { ... });
    const api = useMemo(() => {
    return {
            open: () => dispatch({ type: 'open-sidebar' }),
            close: () => dispatch({ type: 'close-sidebar' }),
            toggle: () => dispatch({ type: 'toggle-sidebar' }),
        }
    // don't depend on the state directly anymore!
    }, []);
}

现在,当我们传递api给Provider时,没有一个Context消费者会因为状态变化而重新渲染:value(这几个函数的索引值)并没有变化!我们可以放心四处使用toggle函数了,不用担心相关的性能问题。

代码示例: advanced-react.com/examples/08...

这个reducer模式在处理复杂状态操作时,是非常有用的。但从重新渲染器的角度,它和useState是一样的:通过dispatch触发的状态更新,一样会导致组件的重新渲染。

相关推荐
xjt_09013 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农15 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king40 分钟前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法