React 的更新流程
- React 的渲染流程是: JSX → 虚拟 DOM → 真实 DOM
- React 的更新流程是: props/state 改变 → render 函数重新执行→产生新的 DOM 树→新旧 DOM 树进行 diff→ 计算出差异进行更新→更新到真实的 DOM
- React 在 props 或 state 发生改变时,会调用 React 的 render 方法创建一颗不同的树
- React 需要基于这两颗不同的树之间的差别来判断如何有效的更新 UI。如果一颗树参考另一颗树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为O(n3),其中n 是树中元素的个数,
https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
。如果采用该算法,开销太过昂贵,React 的更新会变得非常低效
- 于是 React 对这个算法进行了优化,将其优化成了 O(n),优化策略如下:
- 同层节点之间相互比较,不会跨节点比较;
2.不同类型的节点,产生不同的树结构;
3.开发中,可以通过 key 来指定那些节点在不同的渲染下保持稳定;
对比不同类型的元素
- 当节点为不同的元素,React 会拆掉原有的树,并且建立起新的树
- 当一个元素从
<a>
变成<img>
,从<Article>
变成<Comment>
,或从<Button>
变成<div>
都会触发一个完整的重建 流程;
- 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行
componentWillUnmount()
方法;
- 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行
componentWillMount()
方法, 紧接着componentDidMount()
方法;
- 比如下面的代码更改,React 就会销毁 Counter 组件并且重新装载一个新的组件,而不会对 Counter 进行复用
<div>
<Counter />
</div>
<span>
<Counter />
</span>
对比同一类型的元素
- 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
- 比如下面的代码更改:
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性;
<div className="before" title="stuff">
<div className="after" title="stuff">
- 当更新 style 属性时,React 仅更新有所更变的属性。
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。
<div style={{ color: 'red', fontWeight: 'bold' }}>
<div style={{ color: 'green', fontWeight: 'bold' }}>
- 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps() 和 componentWillUpdate() 方 法
- 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;
对子节点进行递归
- 在默认条件下,当递归 DOM 节点的子元素时,React 会同
时遍历两个子元素的列表;当产生差异时,生成一个
mutation。
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
- 前面两个比较是完全相同的,所以不会产生mutation;
- 最后一个比较,产生一个mutation,将其插入到新的
DOM树中即可;
- React会对每一个子元素产生一个mutation,而不是保 持
<li>first</li>
和<li>second</li>
的不变;
- 这种低效的比较方式会带来一定的性能问题;
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>third</li>
<li>first</li>
<li>second</li>
</ul>
keys 的优化
- 我们再使用列表时,如果没有给列表元素的 key 属性设置值, 控制台都会输出一个警告
Warning: Each child in a list should have a unique "key" prop
- 这个设置的 key 属性就是来帮我们优化性能的
- 如果是在列表的最后添加一个元素, 这种情况有无 key 意义不大
- 如果在列表的前面插入数据,在没有key 的情况下,所有的列表元素都需要进行修改
- 当子元素拥有 key 时,React 使用 key来匹配原有树上的子元素以及最新树上的子,这个时候如果是在前面插入元素,只需要将插入的元素插入即可,后面的元素只需要进行移位,而不需要任何修改
- key 的注意事项:
- key 应该是唯一的
- key 不要使用随机数(随机数在下一次 render 时,会重新生成一个数字,两次很难一致,就导致 key 匹配不上)
- 使用 index 作为 key,对性能是没有优化的
render 函数
- 在我们组件嵌套的时候,通常父组件的 render 函数被调用时,所有子组件的 render 函数都会被重新调用。
- 在开发中如果我们只是修改了 app 中的数据,所有组件都需要重新render,进行 diff 算法,性能必然很低
- 事实上,很多组件没有必要重新 render
- 调用 render 应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的 render 方法
- 如何来控制 render 方法是否被调用呢? 通过
shouldComponentUpdate
方法即可
-
shouldComponentUpdate
这个方法接收两个参数 :
- nextProps 修改之后,最新的 props 属性
- nextState 修改之后,最新的 state 属性
- 返回值为 true,那么就需要调用 render 方法
- 返回值为 false,那么就不需要调用 render 方法
- 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法
- shouldComponentUpdate方法中比较 props 和 state 前后有没有发生变化,只需要进行浅比较就好,不需要使用深比较,深比较比较消耗性能,有可能 react 优化的性能,都被深比较又吃掉
类组件使用 PureComponent 优化 render 调用
- 如果所有的类,我们都需要手动来实现
shouldComponentUpdate
,那么会给我们开发者增加非常多的工作量。
- 我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
- props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;
- 事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
- 在原型类上设置
pureComponentPrototype.isPureReactComponent = true
- 在检测是否需要更新
checkShouldComponentUpdate
函数中如果是PureReactComponent的话就对将 props 和 state 的前后值进行比较的结果进行或运算并返回
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
函数式组件使用高阶组件 memo 优化 render
- 函数式组件由于不能继承自PureComponent,需要使用高阶组件 memo 来优化 render 函数调用
- 我们需要使用 memo 函数定一个MemoXXX 组件,将原来的函数组件传给 memo 函数,然后在使用的时候使用 MemoXXX 组件即可
const MemoHeader = memo(function Header() {
console.log("Header被调用");
return <h2>我是Header组件</h2>
})
class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
render() {
console.log("App render函数被调用");
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<MemoHeader/>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
}
export default function memo<Props>(
type: React$ElementType,
compare?: (oldProps: Props, newProps: Props) => boolean,
) {
if (__DEV__) {
if (!isValidElementType(type)) {
console.error(
'memo: The first argument must be a component. Instead ' +
'received: %s',
type === null ? 'null' : typeof type,
);
}
}
return {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
}
- 然后在updateMemoComponent函数中,会使用这个比较器进行比较,如果没传的话,默认就使用shallowEqual
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateExpirationTime,
renderExpirationTime: ExpirationTime,
): null | Fiber {
.......
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
......
}