大纲
React在什么情况下渲染
什么是渲染
渲染是 React 将组件的状态和属性(props)转换为可视化表示(用户界面)的过程。
关键概念:
- 组件:React 应用程序是由组件构成的,这些组件是可重用的 UI 部分,可以管理自己的状态并接收属性(props)。
- 属性(Props) :这些是传递给组件的输入,允许数据从父组件传递到子组件。
- 状态(State) :指的是组件可以管理的内部数据,这些数据可以随时间变化(通常响应用户的交互)。
渲染过程
React 从组件树的根节点开始,逐层查找所有需要更新的组件。对于每个被标记的组件,React 会调用函数(对于函数组件)或 render 方法(对于类组件),以渲染出屏幕内容。
触发渲染的方式
函数式
useState
下面来看一个例子
import { useState } from "react";
export default () => {
const [count, setCount] = useState(0);
return (
<>
useState
{count}
<button
onClick={() => {
setCount(count + 1);
}}
>
改变count
</button>
</>
);
};
这个例子比较简单,点击count触发当前组件渲染。
那么问题来了,如果我在里面嵌套了一个子组件呢??子组件会刷新吗?
import { useState } from "react";
function Son() {
console.log("触发刷新");
return <h1>我是子组件</h1>;
}
export default () => {
const [count, setCount] = useState(0);
return (
<>
useState
{count}
<button
onClick={() => {
setCount(count + 1);
}}
>
改变count
</button>
<Son />
</>
);
};
初始化的时候触发了一次刷新,这是正常的
那么当我点击count,子组件会触发刷新吗?
答案是触发刷新了!!!,即使他没有依赖props,也触发了刷新,触发刷新的原因我们后面再说。现在只需要明白一件事情,setState会触发组件本身和子组件进行刷新。
useReducer
这个其实和useState一样,在React实现层面,useState是对useReducer的一层简单封装。
export default () => {
const [count, forceRender] = useReducer((c) => c + 1, 0);
return (
<>
useReducer
{count}
<button
onClick={() => {
forceRender(count + 1);
}}
>
改变count
</button>
<Son />
</>
);
};
效果是一样的。
render(<App>)
可以再次调用render方法触发更新。 这个也是可以的。
类式组件
this.setState()this.forceUpdate()
总结
上述内容展示了函数组件和类组件触发更新的方式。需要注意的是,当当前组件更新时,无论子组件是否有 props,子组件都会被触发重新渲染。这意味着即使子组件没有直接依赖于父组件的状态或属性,它仍然会随着父组件的更新而更新。这是 React 的设计理念,以保持 UI 的一致性。
阻止React重复渲染
在大多数情况下,我们并不需要阻止React重复渲染,因为React的理念就是纯函数,即使多渲染几次,展示到页面上的内容也是一致的。但是,当某个组件渲染及其珍贵(渲染一次需要很多时间),那就必须阻止重复渲染了!
下面会依次介绍可以阻止React重复渲染的方法
memo
React.memo 是一个高阶组件,用于优化函数组件的渲染性能。它可以通过对比 props 的变化来决定是否需要重新渲染组件。
function Son() {
console.log("触发刷新");
return <h1>我是子组件</h1>;
}
const MemoSon = memo(Son);
将son用memo包裹了一层,直接看运行结果
触发更新之后,看控制台只有一行打印,说明MemoSon没有重复渲染!
原理:
下面简单说下原理,后续会在源码层面进行分析他为什么会阻止重复渲染(点个关注,才能收到后续更新消息哦)
- 浅比较:当使用
React.memo包裹一个组件时,React 会在每次渲染时对该组件的 props 进行浅比较(Object.is)。如果 props 没有发生变化,则 React 会跳过该组件的渲染,直接复用上一次的输出。 - 默认行为:如果组件的 props 没有变化(即未更新),React 将不会调用该组件的 render 方法。这样可以减少不必要的渲染,提高性能。
如果默认的浅比较不适用,可以传递第二个参数给 React.memo,以自定义比较逻辑 。
特殊情况
考虑以下情况,Son 组件接收一个 click 函数:
function Son({ click }) {
console.log("触发刷新");
return <h1>我是子组件</h1>;
}
const MemoSon = memo(Son);
export default ({ children }) => {
const [count, forceRender] = useReducer((c) => c + 1, 0);
const click = () => {};
return (
<>
useReducer
{count}
<button
onClick={() => {
forceRender(count + 1);
}}
>
改变count
</button>
<Son key={1} />
<MemoSon click={click} />
{children}
</>
);
};
son组件接收了一个click函数,那么问题来了,这种情况下会触发渲染吗?
在这个例子中,即使 count 没有变化,点击按钮时 MemoSon 组件仍会触发渲染。这是因为 click 函数在每次渲染时都会重新生成,因此 MemoSon 的 props 发生了变化,导致它重新渲染。
那么有什么解决方式?
- 使用useCallback/useMemo
....
const newClick = useCallback(click, []);
<MemoSon click={newClick} />
...
被useCallback包裹的函数会被缓存,所以只会触发一次更新。
- 自定义更新函数
const MemoSon = memo(Son, () => {
return true;
});
export default ({ children }) => {
const [count, forceRender] = useReducer((c) => c + 1, 0);
const click = () => {};
const newClick = useCallback(click, []);
return (
<>
useReducer
{count}
<button
onClick={() => {
forceRender(count + 1);
}}
>
改变count
</button>
<Son key={1} />
<MemoSon click={click} />
{children}
</>
);
};
memo有第二个参数,当提供了第二个参数后,可以控制渲染时机。
注意
React.memo 的优化效果在于避免不必要的重新渲染,从而提高组件性能。然而,正确使用 memo 的前提是满足以下几个条件:
- 频繁渲染的组件:当组件频繁渲染且传入相同的 props 时,使用
memo可以显著提高性能。 - 重渲染逻辑复杂的组件:如果组件的渲染逻辑复杂,且执行时消耗的性能较高,使用
memo可以减少这种开销。
没必要使用memo的场景
- 无明显延迟的渲染:如果组件的重新渲染速度很快,用户几乎感觉不到延迟,那么使用
memo可能是多余的。甚至还有可能影响性能! - 传入的 props 总是不同:如果每次渲染时传入的 props 都不同(例如,每次都传入新的对象或函数),那么
memo将失去作用。React 会在每次渲染时重新比较 props,因此会导致组件重新渲染。
为了解决 props 总是不同的问题,通常需要结合使用 useMemo 和 useCallback:
useCallback:用于缓存函数,确保在组件重新渲染时,如果依赖没有变化,就复用同一个函数实例。
javascript
复制代码
const handleClick = useCallback(() => {
// 处理点击事件
}, [dependencies]); // 只有当 dependencies 变化时,handleClick 才会变化
useMemo:用于缓存计算值,避免在每次渲染时进行昂贵的计算。
javascript
复制代码
const memoizedValue = useMemo(() => {
// 计算值
}, [dependencies]); // 只有当 dependencies 变化时,memoizedValue 才会重新计算
4. 总结
- 使用
React.memo的有效性依赖于组件的渲染频率和渲染复杂度。如果组件在每次渲染中都接收相同的 props,且渲染成本较高,memo将会带来性能提升。 - 当 props 总是不同或组件渲染迅速且无明显延迟时,
memo并没有必要。 - 结合使用
useMemo和useCallback可以有效地缓存值和函数,避免不必要的重新渲染,从而充分发挥memo的优势。
children
export default ({ children }) => {
const [count, forceRender] = useReducer((c) => c + 1, 0);
return (
<>
useReducer
{count}
<button
onClick={() => {
forceRender(count + 1);
}}
>
改变count
</button>
<Son key={1} />
<MemoSon />
{children}
</>
);
};
通过接受一个children,也可以解决重复渲染的问题。
<DemoFn>
<Son />
</DemoFn>
直接来看结果
可以看到,是没有触发更新的。
React上下文
在 React 中,Context API 允许我们在组件树中共享数据,而无需通过每个级别的 props 逐层传递。Context 提供者(Provider)和消费者(Consumer)是实现这一点的核心。
用法
- 创建 Context: 首先,我们需要创建一个 Context 对象。这通常在组件的外部完成。
import React, { createContext } from 'react';
const MyContext = createContext();
- 使用 Context Provider: 接下来,我们使用
MyContext.Provider将一个值传递给子组件。这个值将对所有嵌套的子组件可用。
const MyProvider = () => {
return (
<MyContext.Provider value={42}>
<ChildComponent />
</MyContext.Provider>
);
};
在上面的示例中,MyProvider 组件将值 42 作为 value 属性传递给 MyContext.Provider。
Context Consumer 的用法
子组件可以通过 MyContext.Consumer 来消费上下文。这种方式需要提供一个函数作为 render prop。
const ChildComponent = () => {
return (
<MyContext.Consumer>
{value => (
<h1>The value from context is: {value}</h1>
)}
</MyContext.Consumer>
);
};
完整示例
下面是一个完整的示例,展示如何创建、提供和消费上下文:
import React, { createContext } from 'react';
// 创建上下文
const MyContext = createContext();
const ChildComponent = () => {
return (
<MyContext.Consumer>
{value => (
<h1>The value from context is: {value}</h1>
)}
</MyContext.Consumer>
);
};
const MyProvider = () => {
return (
<MyContext.Provider value={42}>
<ChildComponent />
</MyContext.Provider>
);
};
// 主应用组件
const App = () => {
return (
<div>
<h1>Context API Example</h1>
<MyProvider />
</div>
);
};
export default App;
渲染机制
稍微改造一下
import React, { createContext, useContext, useState } from "react";
// 创建上下文
const MyContext = createContext();
const ChildComponent = () => {
console.log("ChildComponent触发刷新...");
const { value } = useContext(MyContext);
return <h1>{value}</h1>;
};
const ChildComponent2 = () => {
console.log("ChildComponent2触发刷新...");
return <h1>这是Child2</h1>;
};
const MyProvider = () => {
const [count, setCount] = useState(42);
return (
<>
<MyContext.Provider value={{ value: count, setValue: setCount }}>
<ChildComponent />
<ChildComponent2 />
</MyContext.Provider>
<button
onClick={() => {
setCount(100);
}}
>
改变
</button>
</>
);
};
export default MyProvider;
我们新增了一个子组件,同时有一个改变state的动作,看下结果如何。
可以看到,当Provider组件触发更新,父组件下面的所有子组件都会触发更新,不管用没用到value。
那我们如何避免这个行为呢,答案是用memo
import React, { createContext, memo, useContext, useState } from "react";
// 创建上下文
const MyContext = createContext();
const ChildComponent = () => {
console.log("ChildComponent触发刷新...");
const { value } = useContext(MyContext);
return <h1>{value}</h1>;
};
const ChildComponent2 = () => {
console.log("ChildComponent2触发刷新...");
return <h1>这是Child2</h1>;
};
const Temp = memo(() => {
return (
<>
<ChildComponent />
<ChildComponent2 />
</>
);
});
const MyProvider = () => {
const [count, setCount] = useState(42);
return (
<>
<MyContext.Provider value={{ value: count, setValue: setCount }}>
<Temp />
</MyContext.Provider>
<button
onClick={() => {
setCount(100);
}}
>
改变
</button>
</>
);
};
export default MyProvider;
我们多加一个Temp组件,Temp组件被memo包裹,现在我们看下行为吧。
当我们触发更新时,只有用到useContext的地方才会更新,其余地方不会。
所以上下文程序下的React组件应该用memo进行包裹,防止大量组件进行重复渲染。
这里其实有一个注意的地方是:当使用memo包裹组件时,useState触发更新,其memo组件如果不更新,其下的子组件是不会触发更新的。而Context内部的子组件,如果memo组件本身没更新,但是子组件用到了Context的值,用到value的地方是会触发更新的,这是一个不同的地方。
总结
- 渲染逻辑:React 会在组件的
state或props变化时触发重新渲染,且子组件会随父组件更新。 - 优化策略:
-
- 使用
React.memo优化频繁渲染的函数组件。 - 使用
useCallback和useMemo缓存函数和计算值。 - 使用
React.memo避免 Context Provider 引发的所有子组件更新。
- 使用
- 使用场景:若组件更新频率高且渲染代价大,建议使用优化手段提高性能;若没有明显延迟,不使用优化反而更简单。
通过合理使用 memo、useCallback、useMemo,以及优化 Context API 的性能,我们可以有效避免不必要的组件渲染,提高 React 应用的性能。