# React 渲染
对于 React 渲染,不要仅仅理解成类组件触发 render
函数,函数组件本身执行,事实上,从调度更新任务到调和 fiber
,再到浏览器渲染真实 DOM
,每一个环节都是渲染的一部分,至于对于每个环节的性能优化,React 在底层已经处理了大部分优化细节,包括设立任务优先级、异步调度、diff 算法、时间分片都是 React 为了提高性能,提升用户体验采取的手段。开发者只需要告诉 React 哪些组件需要更新,哪些组件不需要更新。
React 提供了 PureComponent
,shouldComponentUpdated
,memo
等优化手段。
# render 阶段
render
的作用是根据一次更新中产生的新状态值,通过 React.createElement
,替换成新的状态,得到新的 React element 对象,新的 element 对象上,保存了最新状态值。 createElement
会产生一个全新的 props
。到此 render
函数使命完成了。
接下来,React 会调和由 render
函数产生 chidlren
,将子代 element
变成 fiber
(这个过程如果存在 alternate
,会复用 alternate
进行克隆,如果没有 alternate
,那么将创建一个),将 props
变成 pendingProps
,至此当前组件更新完毕。然后如果 children
是组件,会继续重复上一步,直到全部 fiber
调和完毕。完成 render
阶段。
# React 控制 render 的方法
对 render
的控制,究其本质,主要有以下两种方式:
- 从父组件直接隔断子组件的渲染,经典的就是
memo
,缓存element
对象。 - 组件从自身来控制是否
render
,比如:PureComponent
,shouldComponentUpdate
。
# 缓存 React.element 对象
一种父对子的渲染控制方案,来源于一种情况,父组件 render
,子组件有没有必要跟着父组件一起 render
,如果没有必要,则就需要阻断更新流。
/* Children */
function Child({ number }) {
console.log("Child render")
return <div>Child {number}</div>
}
/* Parent */
export default class Index extends React.Component {
state = {
numberA: 0,
numberB: 0,
}
render() {
return (
<div>
<Child number={this.state.numberA} />
<button onClick={() => this.setState({ numberA: this.state.numberA 1 })}>
numberA 1 {this.state.numberA}
</button>
<button onClick={() => this.setState({ numberB: this.state.numberB 1 })}>
numberB 1 {this.state.numberB}
</button>
</div>
)
}
}
对于子组件 Child
,只有 props
中 numberA
更新才是有用的, numberB
更新带来渲染,Child
根本不需要。但是如果不处理子组件的话,就会出现如下情况。无论改变 numberA
还是改变 numberB
,子组件都会重新渲染,显然这不是想要的结果。
export default class Index extends React.Component {
constructor(props) {
super(props)
this.state = {
numberA: 0,
numberB: 0,
}
this.component = <Child number={this.state.numberA} />
}
controlComponentRender = () => {
const { props } = this.component
// 只有当 props.number !== this.state.numberA 时,才会重新渲染
if (props.number !== this.state.numberA) {
return (this.component = React.cloneElement(this.component, { number: this.state.numberA }))
}
// 否则返回缓存的 component
return this.component
}
render() {
return (
<div>
{this.controlComponentRender()}
<button onClick={() => this.setState({ numberA: this.state.numberA 1 })}>
numberA 1 {this.state.numberA}
</button>
<button onClick={() => this.setState({ numberB: this.state.numberB 1 })}>
numberB 1 {this.state.numberB}
</button>
</div>
)
}
}
可以在函数组件用 useMemo
达到同样的效果:
export default function Index() {
const [numberA, setNumberA] = useState(0)
const [numberB, setNumberB] = useState(0)
const component = useMemo(() => <Child number={numberA} />, [numberA])
return (
<div>
{component}
<button onClick={() => setNumberA(numberA 1)}>numberA 1 {numberA}</button>
<button onClick={() => setNumberB(numberB 1)}>numberB 1 {numberB}</button>
</div>
)
}
# useMemo
用法
代码语言:javascript复制const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
computeExpensiveValue
是一个函数,返回值是一个计算的结果,这个结果会被缓存起来,直到a
或者b
发生变化[a, b]
是一个数组,数组中的值是依赖项,只有当依赖项发生变化时,才会重新计算computeExpensiveValue
的值memoizedValue
是一个缓存的值,只有当依赖项发生变化时,才会重新计算computeExpensiveValue
的值
原理
useMemo
会记录上一次执行create
的返回值,并把它绑定在函数组件对应的fiber
对象上,只要组件不销毁,缓存值就一直存在,但是依赖项发生变化时,会重新执行create
函数,重新计算缓存值
应用
- 可以缓存
element
对象,从而达到按条件渲染组件,优化性能的作用 - 如果组件中不期望每次
render
都重新计算一些值,可以使用useMemo
缓存这些值,从而避免不必要的计算 - 可以把函数和属性缓存起来,作为
PureComponent
的绑定方法,或配合其他 Hooks 一起使用
# Pure Component
纯组件是一种发自组件本身的渲染优化策略,当开发类组件选择了继承 PureComponent
,就意味这要遵循其渲染规则。规则就是浅比较 state
和 props
是否相等。
/* 纯组件本身 */
class Children extends React.PureComponent {
state = {
name: "Cell",
age: 18,
obj: {
num: 1,
},
}
changeObjeNum = () => {
const { obj } = this.state
obj.num
this.setState({ obj })
}
render() {
console.log("组件渲染")
return (
<div>
<div>组件本身改变 state </div>
<button onClick={() => this.setState({ name: "Cell" })}>state 相同</button>
<button onClick={() => this.setState({ age: this.state.age 1 })}>state 不同</button>
<button onClick={this.changeObjeNum}>state 为引用类型</button>
</div>
)
}
}
/* 父组件 */
export default function Home() {
const [numberA, setNumberA] = useState(0)
const [numberB, setNumberB] = useState(0)
return (
<div>
<div>父组件改变 props</div>
<button onClick={() => setNumberA(numberA 1)}>改变传递给子组件的 state</button>
<button onClick={() => setNumberB(numberB 1)}>改变不传递给子组件的 state</button>
<Children number={numberA} />
</div>
)
}
- 对于
props
,PureComponent
会浅比较props
是否发生改变,再决定是否渲染组件,所以只有点击numberA
才会促使组件重新渲染 - 对于
state
,也会浅比较处理,当上述触发 ‘state
相同情况’ 按钮时,组件没有渲染 - 浅比较只会比较基础数据类型,对于引用类型,比如 Demo 中
state
的obj
,单纯的改变obj
下属性是不会促使组件更新的,因为浅比较两次obj
还是指向同一个内存空间
PureComponent
注意事项:
避免使用箭头函数
- 不要给是
PureComponent
子组件绑定箭头函数,因为父组件每一次render
,如果是箭头函数绑定的话,都会重新生成一个新的箭头函数,PureComponent
对比新老props
时候,因为是新的函数,所以会判断不想等,而让组件直接渲染,PureComponent
作用终会失效
PureComponent
的父组件是函数组件的情况,绑定函数要用 useCallback
或者 useMemo
处理
- 在用
class
function
组件开发项目的时候,如果父组件是函数,子组件是PureComponent
,那么绑定函数要小心,因为函数组件每一次执行,如果不处理,还会声明一个新的函数,所以PureComponent
对比同样会失效
export default function () {
const callback = React.useCallback(function handlerCallback() {}, [])
return <Child callback={callback} />
}
`useCallback` 和 `useMemo` 的区别
useCallback
第一个参数就是缓存的内容,useMemo
需要执行第一个函数,返回值为缓存的内容,比起 useCallback
, useMemo
更像是缓存了一段逻辑,或者说执行这段逻辑获取的结果。对于缓存 element
也可以用 useCallback
。
# shouldComponentUpdate
有时,把控制渲染,性能调优交给 React 组件本身处理显然是靠不住的,React 需要提供给使用者一种更灵活配置的自定义渲染方案,使用者可以自己决定是否更新当前组件,shouldComponentUpdate
就能达到这种效果。
/* 子组件 */
class Index extends React.Component {
state = {
numA: 0,
numB: 0,
}
shouldComponentUpdate(newProp, newState, newContext) {
if (newProp.prposNumA !== this.props.prposNumA || newState.numA !== this.state.numA) {
return true
}
return false
}
render() {
console.log("组件渲染")
const { numA, numB } = this.state
return (
<div>
<button onClick={() => this.setState({ numA: numA 1 })}> 改变 numA</button>
<button onClick={() => this.setState({ numB: numB 1 })}> 改变 numB</button>
<div>Learn React</div>
</div>
)
}
}
/* 父组件 */
export default function Home() {
const [numberA, setNumberA] = useState(0)
const [numberB, setNumberB] = useState(0)
return (
<div>
<button onClick={() => setNumberA(numberA 1)}>改变传递给子组件的 state</button>
<button onClick={() => setNumberB(numberB 1)}>改变传递给子组件的 state</button>
<Index prposNumA={numberA} prposNumB={numberB} />
</div>
)
}
shouldComponentUpdate
可以根据传入的新的 props
和 state
,或者 newContext
来确定是否更新组件。
有一种情况就是如果子组件的 props
是引用数据类型,比如 object
,还是不能直观比较是否相等。那么如果想有对比新老属性相等,怎么对比呢,而且很多情况下,组件中数据可能来源于服务端交互,对于属性结构是未知的。
immutable.js
可以解决此问题,immutable.js
不可变的状态,对 Immutable
对象的任何修改或添加删除操作都会返回一个新的 Immutable
对象。鉴于这个功能,所以可以把需要对比的 props
或者 state
数据变成 Immutable
对象,通过对比 Immutable
是否相等,来证明状态是否改变,从而确定是否更新组件。
# React.memo
代码语言:javascript复制React.memo(Component, compare)
React.memo
可作为一种容器化的控制渲染方案,可以对比 props
变化,来决定是否渲染组件。
- 参数
Component
原始组件本身compare
是一个函数,可以根据一次更新中props
是否相同决定原始组件是否重新渲染
- 特点
React.memo
: 第二个参数 返回true
组件不渲染 , 返回false
组件重新渲染- 和
shouldComponentUpdate
相反: 返回true
组件渲染 , 返回false
组件不渲染
- 和
memo
当二个参数compare
不存在时,会用浅比较原则处理props
,相当于仅比较props
版本的pureComponent
memo
同样适合类组件和函数组件
# 打破渲染限制
forceUpdate
- 类组件更新如果调用的是
forceUpdate
而不是setState
,会跳过PureComponent
的浅比较和shouldComponentUpdate
自定义比较 - 原理是组件中调用
forceUpdate
时候,全局会开启一个hasForceUpdate
的开关。当组件更新的时候,检查这个开关是否打开,如果打开,就直接跳过shouldUpdate
- 类组件更新如果调用的是
context
穿透- 上述的几种方式,都不能本质上阻断
context
改变,而带来的渲染穿透,所以开发者在使用context
要格外小心,既然选择了消费context
,就要承担context
改变,带来的更新作用
- 上述的几种方式,都不能本质上阻断
# 渲染控制流程图
# render 注意点
# 有没有必要在乎组件不必要渲染
在正常情况下,无须过分在乎 React 没有必要的渲染,要理解执行 render
不等于真正的浏览器渲染视图,render
阶段执行是在 js 当中,js 中运行代码远快于浏览器的 Rendering 和 Painting 的,更何况 React 还提供了 diff
算法等手段,去复用真实 DOM 。
# 什么时候需要注意渲染节流
对于以下情况,需要采用渲染节流:
- 数据可视化的模块组件(展示了大量的数据)
- 一次更新,可能伴随大量的
diff
,数据量越大也就越浪费性能 - 对于数据展示模块组件,有必要采取
memo
,shouldComponentUpdate
等方案控制自身组件渲染
- 一次更新,可能伴随大量的
- 含有大量表单的页面
- React 一般会采用受控组件的模式去管理表单数据层,表单数据层完全托管于
props
或是state
,而用户操作表单往往是频繁的,需要频繁改变数据层,所以很有可能让整个页面组件高频率render
- React 一般会采用受控组件的模式去管理表单数据层,表单数据层完全托管于
- 越是靠近 app root 根组件越值得注意
- 根组件渲染会波及到整个组件树重新 render ,子组件 render ,一是浪费性能,二是可能执行
useEffect
,componentWillReceiveProps
等钩子,造成意想不到的情况发生
- 根组件渲染会波及到整个组件树重新 render ,子组件 render ,一是浪费性能,二是可能执行
# 开发注意点
- 对于大量数据展示的模块,有必要用
shouldComponentUpdate
,PureComponent
来优化性能 - 对于表单控件,最好办法单独抽离组件,独自管理自己的数据层,这样可以让
state
改变,波及的范围更小 - 如果需要更精致化渲染,可以配合
immutable.js
- 组件颗粒化,配合
memo
等 api ,可以制定私有化的渲染空间