接下来对 React 性能相关的问题进行知识回顾。
完整目录概览
React 代码复用
- Render Props
- 高阶组件 (HOC)
- 自定义 Hooks
- Mixins (已被 React 废弃)
Render props
Render props
是一种在 React 组件之间共享代码的简单技术。具体的行为是:
- 子组件接收一个用于渲染指定视图的
prop
属性,该属性的类型是函数。 - 父组件在组件内部定义该函数后,将函数的引入传给子组件
- 子组件将组件内部
state
作为实参传给从外面传来的函数,并将函数的返回结果渲染在指定的视图区域。
1 | // 组件使用 |
准确来说 Render props
是一个用于告知组件需要渲染什么内容的函数属性。props
的命名可以由自己定义,比如用于在内容区域渲染的 prop 名可以叫 render
,同时还可以再接收一个 renderHead
的 prop 用于渲染头部的信息。
高阶函数、高阶组件分别是什么?
高阶函数就是接收其它函数作为参数的函数就称之为高阶函数,像数组的 map
、sort
、filter
都是高阶函数。
高阶组件(Higher-order component, HOC) 是 React 用于复用组件逻辑的一种高级技巧。它具体的行为是:
函数接收一个组件作为参数,在函数体内定义一个新组件,新组件内编写可复用的逻辑并应用到参数组件中。最后再将新组件作为函数的返回值 return 出去。redux
中的 connect
函数就是一个高阶组件。
React 性能优化
- 对比
props/state
新旧值的变化来决定是否渲染组件,参见:父组件在执行 render 时会不会触发子组件的 render 事件?如果会该怎么避免? - 列表渲染时每项添加唯一的
key
。参见:渲染列表为啥要用 key? - 定时器、DOM 事件等在组件销毁时一同销毁,从而避免内存泄露。
- 代码分割,使用异步组件。
- Hooks 使用
useMemo
缓存上一次计算的结果,避免重复计算值。
父组件在执行 render 时会不会触发子组件的 render 事件?如果会该怎么避免?
如果父组件渲染后,子组件接收的 props 也跟着发生了改变,那么默认情况下会触发子组件的渲染。
若子组件接受的 props 没有发生改变,那就得判断子组件的状况。
如果子组件是继承于 Component
声明的组件,并且没有使用 shouldComponentUpdate
做避免重复渲染的处理,那么子组件会触发 render
事件。
为了避免重复渲染,类组件可以使用 shouldComponentUpdate
来决定是否进行渲染。也可以将继承于 Component
组件改为继承 PureComponment
,该组件会浅对比 Props
是否进行改变,从而决定是否渲染组件。
如果是函数组件,可以通过 React.memo
来对函数组件进行缓存。
渲染列表为啥要用 key?
渲染列表时,如果不给列表子项传 key
的话,React 将默认使用 index
作为 key
,同时会在控制台发出警告。
key
在兄弟节点之间必须唯一,要避免使用数组下标 index
作为 key
。因为使用数组下标作为 `key 时,若数组的顺序发生了改变,将会影响 Diffing 算法的效率。
若列表的节点是组件的话,还可能会影响组件的 state
数据。因为组件实例是基于 key
来决定是否更新与复用。当顺序发生了变化,则 key
也会相应得被修改,从而导致子组件间的数据错乱。
React 使用的 Diffing 算法是通过 tag
和 key
判断是否是同一个元素(sameNode
)。使用唯一的 key
有助于 React 识别哪些元素发生改变,如节点添加或删除。这样有助于减少渲染次数,从而优化性能。
如果数组中的数据没有唯一的 key
,可以引入 shortid 预先给数组中每项数据生成唯一的 id
:
1 | const shortid = require('shortid'); |
若确定没有列表的顺序不会发生变化同时没有其他唯一的 key
来标识列表项时才能使用数组的下标。
虚拟 dom 是如何提升性能的
当组件触发更新时,虚拟 DOM 通过 Diffing 算法比对新旧节点的变化以决定是否渲染 DOM 节点,从而减少渲染提升性能。因为修改真实 DOM 所耗费的性能远比操作 JavaScript 多几倍,因此使用虚拟 DOM 在渲染性能上会高效的多。
简述 React Diffing 算法
Diffing 算法(Diffing Algorithm) 会先比较两个根元素的变化:
- 当节点类型变化时,将会卸载原有的树而建立新树。如父节点
<div>
标签被修改为<section>
标签,则它们自身及children
下的节点都会被重新渲染。 - 当 DOM 节点类型相同时,保留相同的 DOM 节点,仅更新发生改变的属性。
- 当组件类型相同时,组件更新时组件实例保持不变,React 将更新组件实例的 props, 并调用生命周期
componentWillReceiveProps()
和componentwillupdate()
,最后再调用render
。若render
中还有子组件,将递归触发 Diff。 - 当列表节点发生变化,列表项没有设置 key 时, 那么 Diffing 算法会逐个对比节点的变化。如果是尾部新增节点,那 Diff 算法会 Diff 到列表末尾,仅新增元素即可,不会有其他的性能损耗。若新增的数据不在数组的尾部而是在中间,那么 Diffing 算法比较到中间时判断出节点发生变化,将会丢弃后面所有节点并重新渲染。
- 当列表节点发生变化,列表项有设置 key 时, React 可以通过
key
来匹配新旧节点间的对应关系,可以很快完成 Diff 并避免重复渲染的问题。
异步组件怎么使用?
通过动态
import()
语法对组件代码进行分割。使用
React.lazy
函数,结合import()
语法引入动态组件。在组件首次渲染时,会自动导入包含MyComponent
的包。1
const MyComponent = React.lazy(() => import('./MyComponent'));
在
React.Suspense
组件中渲染lazy
组件,同时可以使用fallback
做优雅降级(添加loading
效果):1
2
3<React.Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</React.Suspense>封装一个错误捕获组件(比如组件命名为
MyErrorBoundary
),组件内通过生命周期getDerivedStateFromError
捕获错误信息。当异步组件加载失败时,将捕获到错误信息处理后给用户做错误提示功能。1
2
3
4
5<MyErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</React.Suspense>
</MyErrorBoundary>
JSX 是如何编译为 js 代码的?
在 React v17 之前,JSX 会被编译为 React.createElement(component, props, ...children)
函数,执行会返回 vnode
,vnode
通过 patch
之类的方法渲染到页面。
React v17 之后更新了 JSX 转换规则。新的 JSX 转换不会将 JSX 转换为 React.createElement
,而是自动从 React 的 package
中引入新的入口函数(react/jsx-runtime
)并调用。这意味着我们不用在每个组件文件中显式引入 React
。
怎么对组件的参数做类型约束呢?
要对组件的参数做类型约束的话,可以引入 prop-types
来配置对应的 propTypes
属性。Flow
和 TypesScript
则可以对整个应用做类型检查。