React 为什么会 re-renders
Hi,大家好,我今天分享的主题是 React 为什么会重新渲染(Why React Re-Renders)。
作为一名开发人员,我使用 React 快 5 年时间,但对于 React 重新渲染(re-render)的过程:它是如何工作的其实并不是非常了解。最近我阅读一些文章和通过实践,对 re-render 有了进一步的认识,想和大家一起探讨下。本次分享主要包含三个内容:
- 为什么会 re-render,它做了什么
- 什么情况下会导致 re-render
- 如何避免非必要的 re-render,提升应用的性能
一、为什么会 re-render?
在讨论 React re-render 时,我们需要注意两个重要阶段:
- 初始化渲染阶段:组件第一次出现在屏幕中。
- 重新渲染阶段:指已经在屏幕中挂载的组件,第二次或多次连续的渲染。
React 主要工作是保证应用 UI 与 React State 同步,而 re-render 目的则是计算出哪些(UI)是需要更新的。
当 React 需要使用新数据更新应用程序时,就会 re-render——通常这是由于用户与应用交互、异步请求、订阅模型(model)传入的外部数据导致的。
re-render 实际上做了啥?
-
首先,以函数式组件为例:当点击 button 时,state 会更新
const App = () => { const [state, setState] = useState(1); const onClick = () => { setState(state + 1); }; return ( <> <button onClick={onClick}>click here</button> <Child /> </> ); };
re-render 会再次执行函数,且该组件下的所有子节点都会 re-render
-
其次,re-render 并不意味着操作 DOM
- jQuery 时代,我们根据数据 Data 的变更,通过
$.xxx
方法手动的更新 DOM。比如:在一个列表中,每次 jQuery 操作都是直接操作 DOM,性能消耗较大;其次,新增和删除操作的复杂度还好,但是修改数据会非常麻烦 - React 中,我们通过数据 Data 驱动 Components,每次 render 都会生成 VirtualDOM——一个纯JS对象。而我们(开发者)大部分工作到这里就停止了,后续将由 React 计算出每次更新的差别,自动更新 DOM,并且最大化的保证性能
- jQuery 时代,我们根据数据 Data 的变更,通过
二、什么情况下会导致 re-render?
- 组件的 state 更新:codesandbox
- ❓不管哪个 state 变量改变时,整个 App 都会 re-renders?
- 在 React 应用中,data 不能「向上流动」,re-render 只会影响声明/拥有该状态的组件以及该组件的后代。
- ❓不管哪个 state 变量改变时,整个 App 都会 re-renders?
- 父组件 render: codesandbox
-
❓ 组件的 props 改变将会触发 re-render:props not relevant
只有使用了 memoization 的组件(React.memo, useMemo),讨论 props 改变才变得重要。
我们讨论一个非 memoization 组件,它的 props 改变导致的 re-render 其实没有意义。为了使 props 改变,它需要父组件更新。这意味着父组件必须重新渲染,这将触发子组件的重新渲染,与其 props 无关。
-
- 使用 context: codesandbox
- 当 Context 提供者(Provider)的值发生更改时,使用此 Context 的所有组件都将 re-render,即使它们不直接使用数据的变更部分。
- Redux VS Mobx
- hooks 链式调用: codesandbox
- hooks 内发生的一切都“属于”使用它的组件。hooks 可以链接,链中的每个 hooks 仍然“属于”宿主组件
- hooks 内部的状态变化将触发宿主组件(使用它的组件)的不可预防的重新渲染。
- 如果 hooks 使用 Context 和 Context 的值更改,它将触发宿主组件的不可预防的重新渲染。
- hooks 内发生的一切都“属于”使用它的组件。hooks 可以链接,链中的每个 hooks 仍然“属于”宿主组件
三、如何避免非必要的 re-render?
开发中我们经常遇到一个场景:仅仅更新一个 state 状态后,会触发多次 re-render,感觉莫名其妙,程序虽然运行起来了,但是好像里面都是黑盒。
关于如何避免 re-render 我们经常会使用 React.memo 声明一个纯函数组件——在使用 React.memo 之前我们或许还可以试试其他的方法。
- ❌ 不要在 render 函数中创建其他的 Components(codesandbox)。每次 re-render React 会**重新加载(re-mount)**这个组件:先将它销毁,再重新创建。这样比普通的 re-render 更慢,还可能会造成一些 bug
- re-renders 期间可能会有异常闪动
- re-renders 时 state 都会重置
- re-render 时都会触发没有依赖的 useEffect
- 如果组件是 focused 状态,焦点会丢失
- 状态下移,把可变的部分拆到平行组件里:这个方法在管理大型组件的状态时,可能很有用。因为通常某些状态仅用于渲染树中「隔离」的一小部分(codesandbox)。一个典型的例子,比如页面中通过 button 来管理一个对话框 dialog 的状态
- 🏝 我们通常根据 UI 来设计组件,如果再细粒度一些,以 state 隔离为目标来设计组件,达到低耦合、提升可维护性的目的,也可以逐步沉淀出基础组件、业务组件。
- children as props
- 上面例子其实用了 props.children,React props 传递任何东西,并不会被触发 re-render。
- component as props
-
那用其他 props 属性可以吗?可以!比如:
<Changed left={<Expansive1 />} right={<Expansive2 />} />
<Changed />
re-render 并不会导致<Expansive />
re-render
-
- 使用 memo
-
Lodash 中的记忆化(traditional memorization with lodash)
import memoize from 'lodash/memoize'; function swatch(color) { console.log(color); return color; } const memoizedSwatch = memoize(swatch); swatch('red'); swatch('blue'); swatch('red'); swatch('blue'); // color :>> red // color :>> blue // color :>> red // color :>> blue memoizedSwatch('red'); memoizedSwatch('blue'); memoizedSwatch('red'); memoizedSwatch('blue'); // color :>> red // color :>> blue
-
使用 React.memo 包裹组件,可以阻止渲染树中某个节点被触发 re-render 向下传播——除非这个组件的 props 被改变。
-
React.memo 直接告诉 React:除非 props 有更新,否则我不需要 re-renders
-
在渲染没有依赖来源(without props)的大型组件时,非常有用
-
所有非原始类型的 values 必须被 memorized,才能使 React.memo 正常工作
-
🏝 components as props or children
- React.memo 必须使用在作为 children/props 传递的元素。Memoizing 父组件不会生效,因为 children 和 props 是一个对象,因此它们在每次 re-render 时都会改变
-
使用 useMemo/useCallback 提升 re-renders 的性能
- 反模式:非必要的 useMemo/useCallback(AntiPattern:unnecessary useMemo/useCallback on props)
- 记忆化 props 不会阻止一个子组件 re-renders。如果父组件 re-renders,它将触发无视 props 触发子组件的重新更新
- 必要的 useMemo/useCallback(Necessary useMemo/useCallback)
- 被 React.memo 包裹的子组件,所有非原始类型的值,应该使用 memorized(child component is wrapped in React.memo, all props that are not primitive values have to be memorized)
- 如果组件使用非原始值作为 hooks 的依赖,如 useEffect, useMemo, useCallback,它应该被 memoized
- 🏝 useMemo 昂贵的计算
-
useMemo 有代价:它会消耗一点额外内存并导致初始化稍慢,它不应该被用于每次计算(unless you’re actually calculating prime numbers, which you shouldn’t do on the frontend anyway)。在 React 中挂载和更新组件是最昂贵的计算。
-
与组件更新相比,纯 JS 操作可以忽略不计。所以,一个典型的 useMemo 用例应该是缓存 React elements,例如:返回新元素的 map 函数
// map 函数生成元素 const items = useMemo(() => { return values.map((val) => <Child value={{ value: val }} />); }, []);
-
- useCallback 与 useMemo 相似,它用来 memoized 一个函数
Conclusion
- 使用 React.memo 的组件,引用类型 props 需要使用 memoization
- 使用引用类型作为 hooks 依赖,依赖项需要使用 memoization
- 不应该被用于每次计算,除非计算性能消耗比较大,反而可以用 useMemo 缓存 React Elements,比如 map 函数
FAQ
Q: 为什么 memo 不是 React 的默认行为?
A:性能优化都是有成本的,对于很多没有子节点的组件,通过 memo 进行浅比较反而会造成更大的浪费。
Link
转载自:https://juejin.cn/post/7162717557235908644