如何减少React中无关组件的重渲染

简介: 你是否同我一样,总是会遇到一些莫名其妙的渲染问题,有时为了解决bug,需要耗费相当气力来debug呢?快来一起学习下react re-render 这些小技巧吧,或许能帮你减少组件树中无关组件的重渲染及重挂载,可以提升性能,同时也能提高用户体验哟。案例代码:https://github.com/buzingar/re-render-demos

React 中的重新渲染是什么?

当谈到 React 的性能时,我们需要关注两个主要阶段:

  • 初始渲染——当组件第一次出现在屏幕上时发生
  • 重新渲染——已经在屏幕上的组件后续的渲染

当 React 更新应用程序数据时,就会发生 re-render,如用户与应用程序交互通过异步请求某些订阅模型传入一些外部数据。

哪些 re-render 是必需的?

  • 必需的 re-render—— 作为更改源的组件,或直接使用新数据的组件。
  • 例如,如果用户在 input 中输入,管理其状态的组件需要在每次按键时更新自己,就会重新渲染。
  • 多余的 re-render—— 由于错误或低效的应用程序架构,通过数据传递,组件被重新渲染。
  • 例如,用户在 input 中输入,每次按键时都重新渲染整个页面,那么页面就是不必要地重新渲染了。

React 足够快,大多数情况下都能够在用户没有注意到的情况下重新渲染页面,但如果 re-render 发生得太频繁或在非常重的组件上,可能会导致用户体验出现“卡顿迟滞”,这就需要我们进行性能优化了。

React 组件何时会 re-render?

组件重新渲染大概有四个原因:

  • 状态变化
  • 父级组件重新渲染
  • Context 变化
  • hooks 变化

一个误区:当组件的 props 改变时,会发生重新渲染。其实这是由于父组件重新渲染传播给子组件而造成的,与 props 变化与否无直接关系,详见下文。

状态更改

当组件的状态发生变化时,它将重新渲染,常发生在回调或 useEffect 钩子中。
状态更改是所有 re-render 的“根”源。

import { useEffect, useState } from"react";
constApp= () => {
const [state, setState] =useState(0);
constonClick= () => {
setState(state+1);
  };
console.log("重新渲染次数: ", state);
useEffect(() => {
console.log("re-mount");
  }, []);
return (
<><h2>打开控制台,点击按钮</h2><p>每次点击都会输出log,因为状态改变,页面重新渲染</p><p>re-rendercount: {state}</p><buttononClick={onClick}>点击{state}</button></>  );
};
exportdefaultApp;

父级组件重新渲染

如果父组件重新渲染,则当前组件也会重新渲染。也就是说,当一个组件重新渲染,其所有子组件都会重新渲染。
re-render 总是沿着树“向下”:子节点的重新渲染不会触发父节点的重新渲染。

import { useState } from"react";
constChild= () => {
console.log("子组件 re-render,字体颜色改变");
constr=Math.ceil(Math.random() *255);
constg=Math.ceil(Math.random() *255);
constb=Math.ceil(Math.random() *255);
return<pstyle={{ color: 'rgb('+r+','+g+','+b+')' }}>child</p>;};
constApp= () => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
console.log("当前组件re-render 次数: ", state);
return (
<><h2>打开控制台,点击按钮</h2><p>每次点击都会输出log,因为状态改变,页面重新渲染</p><Child/><buttononClick={onClick}>点击{state}</button></>  );
};
exportdefaultApp;

Context 变化

Context Provider中的值value发生变化时,所有使用该 Context 的组件都将 re-render,即使它们不直接使用变化的那部分数据。
这些 re-render 不能通过缓存 memo 直接阻止,但也有一些办法可以阻止由上下文引起的 re-render。

import { useState, createContext, useContext, useMemo, ReactNode } from"react";
constContext=createContext<{ value: number }>({ value: 1 });
constProvider= ({ children }: { children: ReactNode }) => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
constvalue=useMemo(
    () => ({
value: state,
    }),
    [state]
  );
return (
<Context.Providervalue={value}><buttononClick={onClick}>点击</button>      {children}
</Context.Provider>  );
};
constuseValue= () =>useContext(Context);
constChild1= () => {
// 依赖变化的valueconst { value } =useValue();
console.log("Child1 re-renders: ", value);
return<></>;};
constChild2= () => {
// 依赖变化的valueconst { value } =useValue();
console.log("Child2 re-renders: ", value);
return<></>;};
constApp= () => {
return (
<Provider><h2>打开控制台,点击按钮</h2><p>两个child页面都会重新渲染</p><Child1/><Child2/></Provider>  );
};
exportdefaultApp;

hooks 变化

hook 内部发生的一切都“属于”使用它的组件。一些规则:

  • hook 内部的状态更改将触发“host”组件不可避免的 re-render;
  • 如果 hook 使用了 Context,而 Context 的值发生了变化,它将触发“host”组件不可避免的 re-render;

hook 可以链式调用。链中的每个 hook 仍然“属于”“host”组件,并且相同的规则适用于它们中的任何一个。

host:宿主

import { useState, createContext, useContext, useMemo, ReactNode } from"react";
constContext=createContext<{ value: number }>({ value: 1 });
constProvider= ({ children }: { children: ReactNode }) => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
constvalue=useMemo(
    () => ({
value: state,
    }),
    [state]
  );
return (
<Context.Providervalue={value}><buttononClick={onClick}>点击</button>      {children}
</Context.Provider>  );
};
constuseValue= () =>useContext(Context);
// hooks chainconstuseSomething= () => {
constcount=useValue();
returncount.value;
};
constChild= () => {
constvalue=useSomething();
console.log("Child re-renders:", value);
return<></>;};
constApp= () => {
return (
<Provider><h2>打开控制台,点击按钮</h2><p>hooks链式作用,Child将re-render</p><Child/></Provider>  );
};
exportdefaultApp;

props 变化

未 memoized 组件,props 不改变,child 依旧重新渲染
re-render 未缓存的组件,组件的 props 是否改变并不重要。为了改变 props,它们需要由父组件更新。这意味着父组件必须重新渲染,这将触发子组件的重新渲染,而不管它的 props 是什么。
只有在使用缓存技巧时(React.memo, useMemo),那么 props 的改变就变得重要了。

import { useState, memo } from"react";
constChild= ({ data }: { data: string; state?: number }) => {
console.log("Child re-renders");
return<p>{data}</p>;};
constChildMemo=memo(Child);
constvalue="test";
constApp= () => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<><h2>打开控制台,点击按钮</h2><p>prop未变化,Child依旧重渲染了</p><buttononClick={onClick}>点击{state}</button><Childdata={value} /><p>props如果有变化的值,即便child未用到,也会导致child重渲染</p><ChildMemodata={value} state={state} /></>  );
};
exportdefaultApp;

在渲染函数中创建组件

不推荐的写法

在另一个组件的渲染函数中创建组件是一种很不好的开发模式,对性能很不友好。在每次重新渲染时,React 都会重新挂载这个组件(即销毁它并从头重新创建它),这比正常的重新渲染要慢得多。可能会出现问题:

  • 页面内容“闪烁”
  • 重置状态
  • 不触发依赖
  • 丢失焦点
import { useState, useEffect } from"react";
constComponent= () => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
// 每次渲染时,都会创建这个重型组件,性能很差constVerySlowComponent= () => {
console.log("Very slow component re-renders");
useEffect(() => {
console.log("Very slow component re-mounts");
    }, []);
return<div>Veryslowcomponent</div>;  };
return (
<><buttononClick={onClick}>点击</button><VerySlowComponent/></>  );
};
constApp= () => {
return (
<><h2>打开控制台,点击按钮</h2><p>每次点击,重型组件都会重新挂载</p><Component/></>  );
};
exportdefaultApp;

如何减少重新渲染?

state 下放

当重型组件管理状态时,此模式非常有用,并且 state 仅用于呈现树的一小部分独立部分。

一个典型的例子是,在呈现页面重要部分的复杂组件中,通过单击按钮打开/关闭Dialog。在这种情况下,控制Dialog外观的状态、Dialog本身和触发更新的按钮可以封装在更小的组件中。因此,较大的组件不会在这些状态更改时重新渲染。

import { useState } from"react";
constVerySlowComponent= () => {
console.log("Very slow component re-render");
return<div>Veryslowcomponent</div>;};
// 事件交互与重型组件平铺在同一个父组件constFullComponent= () => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<><h3>一般写法,平铺在一起</h3><p>事件交互时,影响重型组件也跟着一起re-render</p><p>re-rendercount: {state}</p><buttononClick={onClick}>点击</button><VerySlowComponent/></>  );
};
// 拆分组件一,事件交互部分组件,内聚constComponentWithButton= () => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<><p>re-rendercount: {state}</p><buttononClick={onClick}>点击</button></>  );
};
// 拆分后组件,包含两个子组件constSplitComponent= () => {
return (
<><h3>拆分后组件,包含两个子组件:交互组件&重型组件</h3><p>事件交互时,只有交互组件会re-render,重型组件不受影响</p><ComponentWithButton/><VerySlowComponent/></>  );
};
exportdefaultfunctionApp() {
return (
<><h2>打开控制台,点击按钮</h2><p>平铺代码</p><FullComponent/><hr/><p>拆分代码</p><SplitComponent/></>  );
}

逻辑更内聚,确保影响范围最小,性能会更好。细粒度控制,不会影响不相关组件。

children 作为 props

类似于上一种方式:它将状态更改封装在较小的组件中。这里的不同之处在于,state 是在封装渲染树中较慢部分的元素上使用的,因此不能那么容易地提取它。

一个典型的例子是附加到组件根元素的 onScroll 或 onMouseMove 回调函数。
在这种情况下,状态管理和使用该状态的组件可以被提取到更小的组件中,而速度较慢的组件可以作为子组件传递给它。从较小组件的角度来看,子组件只是 props,所以它们不会受到状态变化的影响,因此不会重新渲染。

importReact, { useState, ReactNode } from"react";
constVerySlowComponent= () => {
console.log("Very slow component re-renders");
return<div>Veryslowcomponent</div>;};
// 平铺写法,事件交互,会影响无关组件constFullComponent= () => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
// 注意交互区域包含重型组件,也就是重型组件也参与交互,但不依赖变化的值return (
<divonClick={onClick} style={{ background: "#dfa" }}><p>Re-rendercount: {state}</p><h3>平铺写法,事件交互,会影响无关组件</h3><p>事件交互时,重型组件也跟着re-render</p><p>注意交互区域包含重型组件,也就是重型组件也参与交互,但不依赖变化的值</p><VerySlowComponent/></div>  );
};
constComponentWithClick= ({ children }: { children: ReactNode }) => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
// 把重型组件从其父级组件中去除,适用children代替其位置,通过children展示重型组件return (
<divonClick={onClick} style={{ background: "#afd" }}><p>Re-rendercount: {state}</p>      {children}
</div>  );
};
constSplitComponent= () => {
return (
<><ComponentWithClick><><h3>把重型组件从其父级组件中去除,用children代替其位置,通过children展示重型组件</h3><p>点击交互区域,重型组件不再re-render</p><VerySlowComponent/></></ComponentWithClick></>  );
};
exportdefaultfunctionApp() {
return (
<><h2>打开控制台,点击按钮</h2><FullComponent/><hr/><SplitComponent/></>  );
}

组件作为 props

与前面的模式基本相同,具有相同的行为:它将状态封装在一个较小的组件中,重的组件作为 props 传递给它。props 不受状态变化的影响,所以重的组件不会重新渲染。
当少数重型组件独立于状态时可以有用,但不能抽取成 children 来使用。

import { useState, ReactNode } from"react";
constVerySlowComponent= () => {
console.log("Very slow component re-renders");
return<div>Veryslowcomponent</div>;};
constAnotherSlowComponent= () => {
console.log("Another slow component re-renders");
return<div>Anotherslowcomponent</div>;};
constFullComponent= () => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<divonClick={onClick} style={{ background: "#19b" }}><h3>糟糕的写法</h3><p>点击交互,重型组件re-render</p><p>Re-rendercount: {state}</p><VerySlowComponent/><p>Something</p><p>Something</p><p>Something</p><AnotherSlowComponent/></div>  );
};
constComponentWithClick= ({
upup,
down,
}: {
upup: ReactNode;
down: ReactNode;
}) => {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<divonClick={onClick} style={{ background: "#b19" }}><p>Re-rendercount: {state}</p>      {upup}
<p>Something</p><p>Something</p><p>Something</p>      {down}
</div>  );
};
constSplitComponent= () => {
constup= (
<><h3>交互区域内部的多个不连续重型组件,被当作props分别传入</h3><p>交互时,重型组件不再随着re-render</p><VerySlowComponent/></>  );
constdown=<AnotherSlowComponent/>;
return (
<><ComponentWithClickupup={up} down={down} /></>  );
};
exportdefaultfunctionApp() {
return (
<><h2>打开控制台,点击按钮</h2><FullComponent/><hr/><SplitComponent/></>  );
}

React.memo

简单组件,无 props

使用 React.memo 包裹的组件,其下游组件不会被重新渲染,除非该组件的 props 发生了变化。

importReact, { useState } from"react";
constChild= () => {
console.log("Child re-renders");
constr=Math.ceil(Math.random() *255);
constg=Math.ceil(Math.random() *255);
constb=Math.ceil(Math.random() *255);
return<pstyle={{ color: 'rgb('+r+','+g+','+b+')' }}>child</p>;};
// Child 组件被React.memo() 包裹,组件没拆分,也能阻止重新渲染constChildMemo=React.memo(Child);
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<><h2>打开控制台,点击按钮</h2><buttononClick={onClick}>点击 {state}</button><p>未被React.memo()包裹,组件会重新渲染,字体颜色会改变</p><Child/><p>被React.memo()包裹,组件不会重新渲染,字体颜色不会改变</p><ChildMemo/></>  );
}

含 props 组件

要让 React.memo 生效,所有不是简单数据类型的 props 都必须被记住(memorized)

importReact, { useState, useMemo } from"react";
constChild= ({ data }: { data: { value: string } }) => {
console.log("Child re-renders", data.value);
return<>{data.value}</>;};
// 只组件被memo还不行,如果props是对象,就需要把props也memo处理constChildMemo=React.memo(Child);
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
constmemoValue=useMemo(() => ({ value: "second" }), []);
return (
<><h2>打开控制台,点击按钮</h2><p>Secondchilddoesn't re-render</p><buttononClick={onClick}>点击</button><p>props为复杂数据类型,未被useMemo记忆,导致组件re-renders</p><ChildMemodata={{ value: "first" }} /><p>props为复杂数据类型但被useMemo记忆,组件不会re-renders</p><ChildMemodata={memoValue} /></>  );
}

组件作为 children 或 props

当组件作为 props 或 children 传递时,"React.memo"必须应用于这些元素上,只是 memo 父组件不起作用

importReact, { useState, useMemo, ReactNode } from"react";
constChild= ({ data }: { data: { value: string } }) => {
console.log("Child re-renders", data.value);
return<p>{data.value}</p>;};
constChildMemo=React.memo(Child);
constParent= ({
left,
children,
}: {
children: ReactNode;
left?: ReactNode;
}) => {
// parent一直都会re-renderconsole.log("parent re-render");
return (
<divstyle={{ display: "flex", justifyContent: "center" }}><asidestyle={{ background: "#abc", marginRight: "16px" }}>{left}</aside><mainstyle={{ background: "#cba" }}>{children}</main></div>  );
};
constParentMemo=React.memo(Parent);
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
constmemoValue=useMemo(() => ({ value: "memoized data" }), []);
return (
<><h2>打开控制台,点击按钮</h2><buttononClick={onClick}>点击</button><p>memoized父,未memoizedre-render</p>      {/* 父memo,子未memo,作为props和children */}
<ParentMemoleft={<Childdata={{ value: "left child of ParentMemo" }} />}><Childdata={{ value: "child of ParentMemo" }} /></ParentMemo><p>未Memoized父,Memoized将不会re-render;适用上一条原则,要React.memo生效,props需被记忆</p>      {/* 父未memo,子memo,作为props和children */}
<Parentleft={<ChildMemodata={memoValue} />}><ChildMemodata={memoValue} /></Parent></>  );
}

useMemo/useCallback

仅缓存 props

不推荐:props 上不必要的使用 useMemo/useCallback

单独缓存 props 并不会阻止子组件的重新渲染。如果父组件重新渲染,它将触发子组件的重新渲染,而不管它的 props 是什么。

importReact, { useState, useMemo } from"react";
constChild= ({ data }: { data: { value: string } }) => {
console.log("Child re-renders", data.value);
return<p>Child:value一直是{data.value},未改变</p>;};
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
constmemoValue=useMemo(() => ({ value: "child" }), []);
return (
<><h2>打开控制台,点击按钮</h2><p>非必要的使用useMemo/useCallback(二者同质)</p><p>组件没有使用React.memo包裹,只是使用useMemo记忆value,依旧会重新渲染</p><p>子组件props未改变,但依旧重新渲染</p><buttononClick={onClick}>点击</button><Childdata={memoValue} /></>  );
}

useMemo 处理子组件的复杂数据类型 props

importReact, { useState, useMemo } from"react";
constChild= ({ data }: { data: { value: number } }) => {
console.log("Child re-render:", data.value);
return<p>{data.value}</p>;};
constChildMemo=React.memo(Child);
constvalues= [1, 2, 3];
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
console.log("当前组件re-render");
constitems=useMemo(() => {
returnvalues.map((val) =><Childdata={{ value: val }} key={val} />);
  }, []);
constvals=useMemo(() => {
returnvalues.map((val) => ({
value: val,
    }));
  }, []);
return (
<><h2>打开控制台,点击按钮</h2><p>使用useMemo记忆子组件列表,不用分别使用memo记忆子组件,useMemo记忆propsvalue值</p><buttononClick={onClick}>改变 {state},当前组件重新渲染</button>      {/* 1、使用useMemo记忆整体列表,不会重新渲染 */}
      {items}
      {/* 2、使用memo记忆组件,使用useMemo记忆对象类型props,不会重新渲染 */}
      {values.map((val, index) => (
// <ChildMemo data={vals[index]} key={val} />// 3、未使用useMemo记忆对象类型props,重新渲染<ChildMemodata={{ value: val }} key={val} />      ))}
</>  );
}

当前组件 hook 依赖复杂数据类型

如果一个组件在 useEffect、useMemo、useCallback 等钩子中使用复杂数据类型作为依赖项,那么它应该被缓存。

importReact, { useState, useMemo, useEffect } from"react";
exportdefaultfunctionApp() {
// 复杂数据类型constval= { value: "not memoized" };
constmemoValue=useMemo(() => ({ value: "memoized" }), []);
const [value] =useState({ value: "memoized? possibly" });
const [state, setState] =useState(0);
constonClick= () => {
setState(state+1);
  };
useEffect(() => {
console.log("我被memo,再点你也看不见我");
  }, [memoValue]);
useEffect(() => {
console.log("我在state, 一样看不见我");
  }, [value]);
useEffect(() => {
console.log("val 我比较可爱,一点我就出来啦");
  }, [val]);
return (
<><h2>打开控制台,点击按钮</h2><buttononClick={onClick}>点击了{state}</button></>  );
}

很费算力的情景

useMemo 的一个用例是避免在每次重新渲染时进行昂贵的计算。
useMemo 有它的成本(消耗一点内存并使初始渲染稍微变慢),所以不应该在每次计算中都使用它。大多数情况下,挂载和更新组件是最昂贵的计算。
因此,useMemo 的典型用例是缓存 React 元素。通常是现有渲染树的一部分或生成渲染树的结果,如返回新元素的 map 函数。
与组件更新相比,“纯”javascript 操作(如排序或筛选数组)的成本通常可以忽略不计。

importReact, { useState, useMemo } from"react";
constExpensiveChild= ({ data }: { data: { value: number } }) => {
console.log("Expensive Child re-renders", data.value);
return<p>{data.value}</p>;};
constvalues= [1, 2, 3];
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
constitems=useMemo(() => {
returnvalues.map((val) => (
<ExpensiveChilddata={{ value: val }} key={val} />    ));
  }, []);
return (
<><h2>打开控制台,点击按钮</h2><p>CPU密集型子组件不会重新渲染</p><buttononClick={onClick}>点击 {state}</button>      {items}
</>  );
}

优化列表性能

除了常规的重新渲染规则和模式外,key 属性还会影响 React 中列表的性能。

仅仅提供 key 属性不会提高列表的性能。为了防止列表元素的重新渲染,将列表元素用 React.memo 包裹,并遵循以下最佳实践:

key 中的 value 应该是一个字符串,它在列表中的每个元素的重新渲染之间是一致的。通常,使用元素的 id 或数组的索引。

如果列表是静态的,即元素没有添加/删除/插入/重新排序,那么使用数组的下标作为key也是可以的。

不过在动态列表中使用数组的索引会导致:

  • 如果项目有状态或任何未受控元素(如表单输入),则会出现错误
  • 如果项目被包裹在 React.memo 中,则会降低性能

静态列表

importReact, { useEffect, useState } from"react";
constChild= ({ value }: { value: number }) => {
console.log("Child re-renders", value);
return<li>{value}</li>;};
// const ChildMemo = Child;// memo子组件constChildMemo=React.memo(Child);
constvalues= [1, 2, 3];
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
console.log("父级组件由于state变化,re-render");
useEffect(() => {
console.log("父级组件 re-mount");
  }, []);
return (
<><h2>打开控制台,点击按钮</h2><p>静态列表,分别使用value和index作为key,都可以的</p><p>子组件都不会重新渲染</p><buttononClick={onClick}>点击 {state}</button><ul>        {values.map((val, index) => (
<ChildMemovalue={val} key={index} />        ))}
</ul><ol>        {values.map((val) => (
<ChildMemovalue={val} key={val} />        ))}
</ol></>  );
}

动态列表

importReact, { useState } from"react";
constChild= ({ value }: { value: string }) => {
console.log("Child re-renders", value);
return<div>{value}</div>;};
constvalues= [3, 1, 2];
constChildMemo=React.memo(Child);
exportdefaultfunctionApp() {
const [state, setState] =useState<"ascend"|"descend">("ascend");
constonClick= () => {
setState(state==="ascend"?"descend" : "ascend");
  };
constsortedValues=state==="ascend"?values.sort() : values.sort().reverse();
return (
<><h2>打开控制台,点击按钮</h2><p>动态列表,分别使用idvalue和index作为key</p><p>index作为key的组件会重新渲染,value作为key的组件不会</p><buttononClick={onClick}>切换排序 {state}</button>      {/* 借助React Developer Tools观察组件 */}
      {/* 切换排序状态,只有index 0、2 会重新渲染,因为key=0的组件,切换状态前后value发生了变化,会重新渲染 */}
      {sortedValues.map((val, index) => (
<ChildMemovalue={`Child of index: ${val}`} key={index} />))}{/* 同一个组件,props(key/value)都没有改变,状态改变前后还是同一个组件,组件只是发生了移动,并未重新渲染 */}{sortedValues.map((val, index) => (<ChildMemo value={`Childofid: ${val}`} key={val} />))}</>);}

随机值作为列表中的key

不推荐的用法

随机生成的值不应该用作列表中 key 属性的值。它们将导致 React 在每次重新渲染时重新挂载元素,导致

  • 性能很差的列表
  • 如果列表有状态或任何未受控元素(如表单输入),则会出现 bug
importReact, { useState, useEffect } from"react";
constChild= ({ value }: { value: number }) => {
console.log("Child re-renders", value);
// 类似componentDidMount(),证明挂载了此组件useEffect(() => {
console.log("Child re-mounts");
  }, []);
return<div>{value}</div>;};
constChildMemo=React.memo(Child);
constvalues= [1, 2, 3];
exportdefaultfunctionApp() {
// 状态改变,此组件重新渲染,无memo的子组件也会重新渲染const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<><h2>打开控制台,点击按钮</h2><buttononClick={onClick}>点击 {state}</button>      {/* memo失效,组件的props随每次改变state都会变化,key在不断变化,组件非同一个,就会重新挂载 */}
      {values.map((val) => (
<ChildMemovalue={val} key={Math.random()} />      ))}
</>  );
}

使用 Context 时如何防止重新渲染

缓存 Provider 的值

如果 Context Provider 不是放在应用的根位置,且组件有可能因为其祖先的更改而重新渲染,那么此组件的值应该被记住。

importReact, {
useState,
createContext,
useContext,
useMemo,
ReactNode,
} from"react";
constContext=createContext<{ value: number }>({ value: 1 });
constContext2=createContext<{ value: number }>({ value: 1 });
constProvider= ({ children }: { children: ReactNode }) => {
const [state, setState] =useState(1);
//   const onClick = () => {//     setState(state + 1);//   };constdata=useMemo(
    () => ({
value: state,
    }),
    [state]
  );
return<Context.Providervalue={data}>{children}</Context.Provider>;};
constProvider2= ({ children }: { children: ReactNode }) => {
const [state, setState] =useState(1);
//   const onClick = () => {//     setState(state + 1);//   };// 这里没有memoconstdata= {
value: state,
  };
//   const data = useMemo(//     () => ({//       value: state,//     }),//     [state]//   );return<Context.Providervalue={data}>{children}</Context.Provider>;};
constuseValue= () =>useContext(Context);
constuseValue2= () =>useContext(Context2);
constChild1= () => {
const { value } =useValue();
const { value: value2 } =useValue2();
console.log("Child1 re-renders: ", value, value2);
return<></>;};
constChild2= () => {
const { value } =useValue();
const { value: value2 } =useValue2();
console.log("Child2 re-renders: ", value, value2);
return<></>;};
constChild1Memo=React.memo(Child1);
constChild2Memo=React.memo(Child2);
exportdefaultfunctionApp() {
const [state, setState] =useState(1);
constonClick= () => {
setState(state+1);
  };
return (
<Provider><Provider2><h2>打开控制台,点击按钮</h2><p>切换Provider2中的data,一个使用useMemo,另一个没使用memo,查看控制台输出</p><p>没有memoizevalue时,两个组件会被非必需的重新渲染</p><buttononClick={onClick}>button {state}</button><Child1Memo/><Child2Memo/></Provider2></Provider>  );
}

分割数据和 API

如果在 Context 中有 data 和 api 的组合(getter 和 setter),它们可以被分割到同一个组件下的不同提供者。这样,仅使用 API 的组件就不会在数据更改时重新渲染。

import {
useState,
createContext,
useContext,
ReactNode,
Dispatch,
SetStateAction,
} from"react";
// 两个contextconstContextData=createContext<number>(1);
constContextApi=createContext<Dispatch<SetStateAction<number>>>(
  () =>undefined);
// Provider组件constProvider= ({ children }: { children: ReactNode }) => {
const [state, setState] =useState(1);
return (
<ContextData.Providervalue={state}>      {/* 提供setState函数,api上下文 */}
<ContextApi.Providervalue={setState}>{children}</ContextApi.Provider></ContextData.Provider>  );
};
// 事件所在组件,不会重新渲染constChild1= () => {
constuseApi= () =>useContext(ContextApi);
constapi=useApi();
console.log("使用API的子组件 re-renders");
constonClick= () => {
api(Math.random() *10);
  };
return<buttononClick={onClick}>在context中设置随机数</button>;};
// 使用data,会重新渲染constChild2= () => {
constuseData= () =>useContext(ContextData);
constvalue=useData();
console.log(`使用Data (${value}) 的子组件 re-renders`);
return<p>{value}</p>;};
exportdefaultfunctionApp() {
return (
<Provider><h2>打开控制台,点击按钮</h2><p>只使用data的子组件会re-render</p><p>触发方法所在组件不会更新</p><Child1/><Child2/></Provider>  );
}

将数据分割成块

如果 Context 管理几个独立的数据块,则可以将它们拆分为同一 Provider 下的更小的 Providers。这样,只有更改块的消费者 consumers 才会重新渲染。

import { useState, createContext, useContext, ReactNode } from"react";
constContextData1=createContext<number>(123);
constContextData2=createContext<string>("abc");
constProvider= ({ children }: { children: ReactNode }) => {
const [numState, setNumState] =useState(123);
const [strState, setStrState] =useState("abc");
// 由两个provider分别提供number和string类型的数据return (
<ContextData1.Providervalue={numState}><ContextData2.Providervalue={strState}>        {/* 点击更改number */}
<buttononClick={() =>setNumState(numState+1)}>改变number</button>        {/* 点击更改string */}
<buttononClick={() =>setStrState(`${strState}d`)}>改变string</button>        {children}
</ContextData2.Provider></ContextData1.Provider>  );
};
constChildNum= () => {
constuseNumData= () =>useContext(ContextData1);
constnum=useNumData();
console.log("依赖 num data 子组件 re-render");
return<p>{num}</p>;};
constChildStr= () => {
constuseStrData= () =>useContext(ContextData2);
conststr=useStrData();
console.log("依赖 string data 子组件 re-render");
return<p>{str}</p>;};
exportdefaultfunctionApp() {
return (
<Provider><h2>打开控制台,点击按钮</h2><p>拆分多个不同的providers提供独立的数据,子组件独立更新</p><ChildNum/><ChildStr/></Provider>  );
}
目录
相关文章
|
10天前
|
前端开发 JavaScript 容器
React 元素渲染
10月更文挑战第7天
19 1
|
5天前
|
监控 前端开发 UED
在 React 18 中利用并发渲染提高应用性能
【10月更文挑战第12天】利用并发渲染需要综合考虑应用的特点和需求,合理运用相关特性和策略,不断进行优化和调整,以达到最佳的性能提升效果。同时,要密切关注 React 的发展和更新,以便及时利用新的技术和方法来进一步优化应用性能。你还可以结合具体的项目实践来深入理解和掌握这些方法,让应用在 React 18 的并发渲染机制下发挥出更好的性能优势。
|
10天前
|
前端开发 JavaScript 算法
React 渲染优化策略
【10月更文挑战第6天】React 是一个高效的 JavaScript 库,用于构建用户界面。本文从基础概念出发,深入探讨了 React 渲染优化的常见问题及解决方法,包括不必要的渲染、大量子组件的渲染、高频事件处理和大量列表渲染等问题,并提供了代码示例,帮助开发者提升应用性能。
38 6
|
8天前
|
前端开发 JavaScript
React 条件渲染
10月更文挑战第9天
17 0
|
2月前
|
资源调度 前端开发 API
React Suspense与Concurrent Mode:异步渲染的未来
React的Suspense与Concurrent Mode是16.8版后引入的功能,旨在改善用户体验与性能。Suspense组件作为异步边界,允许子组件在数据加载完成前显示占位符,结合React.lazy实现懒加载,优化资源调度。Concurrent Mode则通过并发渲染与智能调度提升应用响应性,支持时间分片和优先级调度,确保即使处理复杂任务时UI仍流畅。二者结合使用,能显著提高应用效率与交互体验,尤其适用于数据驱动的应用场景。
63 20
|
2月前
|
前端开发
React 如何使用条件渲染
【8月更文挑战第17天】React 如何使用条件渲染
40 3
|
2月前
|
前端开发 JavaScript 中间件
|
2月前
|
前端开发 JavaScript 数据管理
React 中无渲染组件
【8月更文挑战第31天】
27 0
|
2月前
|
前端开发 JavaScript 搜索推荐
|
2月前
|
前端开发 JavaScript 开发者
React Server Component 使用问题之为什么选择使用 React 官方的 renderToString 来渲染 HTML,如何解决
React Server Component 使用问题之为什么选择使用 React 官方的 renderToString 来渲染 HTML,如何解决