这是一个你可以在许多网站上找到的短语,它被认为(?)有效:
React Context通常用于避免道具钻孔,但众所周知存在性能问题。当上下文值更改时,所有组件使用CCD_ 1的将重新渲染。
此外:
React团队提出了一个
useSelectedContext
挂钩来防止性能问题以上下文为尺度。有一个社区库:使用上下文选择器
然而,对我来说,上述内容毫无意义。难道我们不想重新渲染所有使用useContext
的组件吗?绝对地上下文值更改后,所有使用该值的组件都必须重新渲染。否则,UI将不会与状态同步。那么,性能问题究竟是什么?
我们可以讨论如何不重新呈现上下文提供程序中不使用useContext
的其他子组件,这是可以实现的(react-docs):
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
通过使用上面的模式,我们可以避免重新渲染所有不使用useContext
的子组件。
概括一下:在我看来,以正确的方式使用Context不存在性能问题。只有那些应该重新渲染的组件才会重新渲染。然而,几乎所有的参考文献都坚持认为存在潜在的性能问题,并强调这是Context的警告之一。我是不是错过了什么
注意:我将回答您的问题,并对我在互联网上和个人经验中收集到的内容发表最后评论。
TL;DR:性能问题不断提醒您,当您使用Context API
时,即使没有显式写入它,也实际上是在将这些道具传递给使用上下文状态的组件,并且在上,对于访问该状态的每个组件,每次状态更改,这些组件都将被重新呈现。
回答你帖子中的每个问题:
问题1:我们不想重新渲染所有使用
useContext
的组件吗?
是的,正如你所说的那样。
问题2:那么,性能问题究竟是什么?
道具更改时,组件将重新渲染。当使用Context API
时,即使没有明确地将Context状态作为道具传递,每次状态更改都会触发对该组件以及依赖或接收该状态作为道具的子组件的重新渲染。你可以在这个文档上阅读,例如:
Context提供了一种在组件之间共享类似值的方法,而不必显式地在树的每个级别传递道具
这并不是一个问题,因为文档建议您使用它来存储全局状态,该状态不会发生太大变化,例如:
- 主题
- 身份验证/登录状态
- 语言/i18n
这些是以下数据类型:
- 如果更改,则应触发";全局";重新渲染,因为它会影响整个应用程序
- 每次应用程序交互时不要改变那么多(或根本不改变)
- 将用于不同的嵌套级别
问题3:我遗漏了什么吗?
好吧,正如你已经假设的那样,有很多情况与";全局状态";这并没有改变多少。对于这些情况,还有一些其他选项可以用于处理Context API
解决的同一组情况,但代码开销要大得多。其中之一是useContext
0,它没有这种开销,原因最明显:Redux
从应用程序中创建了一个并行的store
,并且不会将值作为道具传递给每个组件。另一方面,最显著的开销之一是项目必须使用的工具来容纳该库。
但为什么人们一开始就开始使用Redux
(和其他库)
在React
的过去版本中处理全局状态是一件事。你可以用很多不同的方式、观点和方法来解决这个问题。迟早,人们开始创建和使用工具和库,用被认为是";"更好";出于特定或个人原因。
后来,这些工具/lib开始变得更加复杂;连接器";或";中间件";这里和那里。例如,可以添加到Redux
中处理请求的一个工具是名为Redux Thunk
的lib,它允许在操作内部执行请求(如果我没有错的话),打破了从Redux
将操作写成纯函数的概念。如今,随着React Query
/TanStack Query
的增长,与请求相关的状态也开始被处理为并行的"状态";全局状态";即使具有高速缓存和更多的功能,也减少了Redux Thunk
和Redux
的使用;全局状态";来自请求。
在Context API
发布到一个稳定和改进的版本后,人们开始在许多项目中使用它,并作为全球状态经理。迟早,每个人都开始注意到与过多的重新渲染有关的性能问题,因为每次都有几个道具在变化。其中一些只是回到了Redux
和其他库,但对于其他库,事实证明Context API
非常好,实用,涉及较少的开销,并且如果按预期使用,它与React
一起嵌入。不必处理性能问题的要求与前面描述的相同:
- 一个全局状态,它不会如此频繁地变化,并且
- 将用于不同的嵌套级别
如果不嵌套太多组件,Context API
还有一些其他选项可以顺利工作。例如:如果在Route
级别而不是在App
级别创建上下文,则为多页表单。正如你所说:
在我看来,以正确的方式使用Context时不存在性能问题
但你可以说,几乎每一个工具都是在其原始使用概念之外使用的。
编辑:在OP指出后,在阅读官方文档中关于Context API的内容后,道具不会传递给每个孩子,只针对那些使用上下文的孩子。因此:对于那些将这些道具传递给它们的组件,给每个子组件。
并且,在回答为什么Context API
存在性能问题的问题时,我计划创建一个repo来复制和理解,但我的赌注是:这可能与每个Context
被称为";部件";React自己处理,而不是创建一个";平行结构";例如Redux
/Jotai
。
概括一下:在我看来,以正确的方式使用Context不存在性能问题。只有那些应该重新渲染的组件才会重新渲染。
如果您的提供者只提供了一个简单的值,那么您的重述是正确的。
但在现实中,提供者通常提供一个包含许多树枝和树叶的大树对象。然而,每个消费者可能只需要其中的一小部分,甚至是该树上的一个叶子值。
在这种情况下,存在性能问题,因为上下文API是一个整体销售解决方案。即使更新了单个叶值,仍然需要更新树根对象的引用,以便发出更改的信号。但这反过来会通知每个消费者useContext
。
现在您缺少的一点是:
确实,它们中的每一个都应该被通知更改,但并不是所有的都应该重新渲染。最理想的情况是,只有那些依赖于更新的叶值的Consumer才应该重新渲染。
在目前的状态下,Context API没有提供任何对这个问题的细粒度控制,因此像use-context-selector
这样的东西将选择器模式重新引入我们的视野。
从根本上说,这是一个pub-sub模型,如果你没有允许subs决定收听哪个频道的机制,你唯一能做的就是向所有subs广播所有内容。这就像把附近的每个人都叫醒,只是告诉他们"爱丽丝收到了一封新邮件",这显然不是最好的。
裸机Redux设置中也存在同样的问题。这就是为什么react-redux
的选择器模式曾经非常流行的原因。
如果您彻底阅读文档,contextAPI不会出现性能问题:)
当人们使用上下文API作为多个值的存储时,问题就开始了,并且并非所有组件都需要所有值。
通常,react将上下文值视为单个值,并且它是不可变的(否则什么都不起作用),因此当上下文值发生任何变化时,所有使用上下文的组件都将重新呈现,即使它们没有使用更改的信息。
话虽如此,每次您有稍微复杂的本地(ish)状态时,您不必使用Redux或任何其他全局状态管理库(如果您有真正的本地状态,请将该状态放入组件中)。
您可以构建自己的小型外部存储,并且只在上下文中公开一个API——API永远不会改变。下面是一个如何做到这一点的例子。
我不能把所有的代码都内联。。但是
这可能是商店,还有许多其他选择,当然,重要的部分是使用useSyncExternalStore
,它将我们的外部商店连接到react生命周期。
import { set, unset, get, PropertyPath } from "lodash";
import { useSyncExternalStore } from "react";
export type SimpleReactiveStore = ReturnType<typeof createSimpleReactiveStore>;
/**
*
* @returns a simple store you can use with your react components.
*/
export function createSimpleReactiveStore() {
const data: object = {};
const registry: ((data: object) => void)[] = [];
function subscribe(listener: (data: object) => void) {
registry.push(listener);
return () => {
registry.splice(registry.indexOf(listener), 1);
};
}
/**
*
* @param path
* @param value has to be an immutable value if you want it to trigger a re-render.
*/
function setValue<T>(path: PropertyPath, value: T) {
if (value === undefined) {
unset(data, path);
} else {
set(data, path, value);
}
registry.forEach((li) => li(data));
}
function getValue<T>(path: PropertyPath) {
return get(data, path) as T;
}
function useValue<T>(path: PropertyPath, defaultValue: T) {
const value = useSyncExternalStore(
subscribe,
() => getValue<T>(path) || defaultValue
);
return value;
}
return {
useValue,
setValue,
getValue,
};
}
这可能是暴露API的钩子,您可以在上下文提供者或任何其他组件中使用它
function useTodos() {
const store = useRef(createSimpleReactiveStore());
const api = useMemo(
() => ({
addTodo: (todo: Todo) => {
const list = store.current.getValue<string[]>(TODO_LIST_PATH) || [];
//we store the ids in an array so that they will have a particular order
store.current.setValue(TODO_LIST_PATH, [...list, todo.id]);
store.current.setValue(todo.id, todo);
},
setTodo: (todo: Todo) => store.current.setValue(todo.id, todo),
useTodo: (id: string) =>
store.current.useValue<Todo | undefined>(id, undefined),
useTodoList: () => store.current.useValue<string[]>(TODO_LIST_PATH, []),
}),
[]
);
return api;
}
我开发了一个库,只是为了以最佳且简单的方式使用Context。它的名称是react上下文切片。它允许您轻松定义Context的切片,并使用单个钩子useSlice
获取它们。它是这样的:
// slices.js
import getHookAndProviderFromSlices from "react-context-slices"
export const {useSlice, Provider} = getHookAndProviderFromSlices({
count: {initialArg: 0},
// rest of slices of Context you want to define
})
// app.js
import {useSlice} from "./slices"
const App = () => {
const [count, useCount] = useSlice("count")
return <>
<button onClick={() => setCount(c => c + 1)}>+</button>{count}
</>
}
正如你所看到的,它很容易使用,而且速度很快。如果你看到应用程序的性能受到影响,这表明你定义了一个包含太多无关信息的切片,你必须将其分解为更多的切片。
因此,我同意您的观点,即上下文API如果使用正确,不会影响性能(据我所知)。此库将使用Context API的样板文件数量减少到0。您只需要用四个可选参数定义Context的切片:initialArg
、reducer
、init
和isGetInitialStateFromStorage
。前三个与React文档为useReducer
钩子指定的相同。因此,如果你需要的话,请查看这些信息,了解它们的作用。第四个作为其名称指定,用于从本地存储(对于web)或AsyncStorage(对于React Native)读取或恢复切片的初始状态。
这里的其他答案基本正确,但有点误导。澄清重新渲染和再次评估之间的区别非常重要。如果上下文中有状态树,则每次上下文对象更改时,使用该上下文的每个组件都将重新评估,但只有当组件返回的JSX自上次渲染以来发生更改时,才会再次渲染。只要您正在存储组件体内运行的任何复杂数据转换,重新评估通常并不昂贵。
给定以下应用程序:
const Context = React.createContext<{ foo: string; bar: string }>({ foo: '', bar: '' })
function Foo() {
const { foo } = React.useContext(Context);
console.log('rendering Foo');
return <h1>{foo}</h1>;
}
function Bar() {
const { bar } = React.useContext(Context);
console.log('rendering Bar');
return <h2>{bar}</h2>;
}
function App() {
const [foo, setFoo] = React.useState('foo');
const [bar, setBar] = React.useState('bar');
return (
<Context.Provider value={{ foo, bar }}>
<div className="App">
<Foo />
<Bar />
</div>
<input value={foo} onChange={e => setFoo(e.target.value)} />
<input value={bar} onChange={e => setBar(e.target.value)} />
</Context.Provider>
);
}
如果用户更新foo
输入的值,控制台将记录
rendering Foo
rendering Bar
每按一次键。但是,如果在开发工具"渲染"面板中启用油漆闪烁,则只能看到Foo
组件闪烁。您需要有一个非常大和复杂的组件树,才能开始注意到与组件重新评估相关的任何放缓,但在面向客户的商业应用程序中,这绝对是需要关注的问题。与大多数反应一样,你应该在浏览器开发工具中关注你的FPS,使用CPU节流来模拟低功耗设备,并在屏幕更新时间超过20ms时进行性能优化。在实践中,与将全局状态树保留在上下文中相比,您臃肿的UI框架、JS库中的CSS和大量冗余的graphql请求更有可能导致性能问题。