第八章 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触发的状态更新,一样会导致组件的重新渲染。

相关推荐
HCl+NaOH=NaCl+H_2O12 分钟前
Quasar组件 Carousel走马灯
javascript·vue.js·ecmascript
℘团子এ16 分钟前
vue3中预览Excel文件
前端·javascript
shmily麻瓜小菜鸡1 小时前
在 Angular 中, `if...else if...else`
前端·javascript·angular.js
bloglin999991 小时前
npm和nvm和nrm有什么区别
前端·npm·node.js
2501_910227542 小时前
web3 前端常见错误类型以及错误捕获处理
前端·web3
哎哟喂_!2 小时前
Node.js 同步加载问题详解:原理、危害与优化策略
前端·chrome·node.js
__BMGT()2 小时前
C++ QT图片查看器
前端·c++·qt
OK_boom3 小时前
React-useRef
javascript·react.js·ecmascript
未来之窗软件服务3 小时前
solidwors插件 开发————仙盟创梦IDE
前端·javascript·数据库·ide·仙盟创梦ide
小白学大数据3 小时前
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
javascript·redis·分布式·scrapy