一. 发生更新的时机以及顺序##
- props/state改变
- render函数重新执行
- 产生新的VDOM树
- 新旧DOM树进行diff
- 计算出差异进行更新
- 更新到真实的DOM
二. React更新流程##
React将最好的O(n^3)的tree比较算法优化为O(n)。
- 同层节点之间相互比较,不跨节点。
- 不同类型的节点,产生不同的树结构:如果该节点不同,会将旧tree中该节点的子树全部删掉。直接生成新的子树,挂载到DOM中。
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定。
三. 不同情况##
情况一:对比不同类型的元素
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树。
- 当一个元素改变,会触发一个完整的重建流程。
- 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行componentWillUnmount()方法。
- 当建立一棵新树时,对应得DOM节点会被创建以及插入到DOM中,组件实例将执行componentWillMount()方法,紧接着componentDidMount()方法。
-
子树销毁,元素不会复用。####
情况二:对比同一类型的元素
- 当对比两个相同类型的React元素时,React会保留DOM节点,仅比对及更新有改变的属性, 比如下面例子:
- React知道只需要修改DOM元素上的className属性。
-当更新style属性时,React仅更新有所改变的属性,没有变化的属性不会变。
- 如果是同类型的组件元素:组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps()和componentWillUpdate()方法;
- 下一步,调用render()方法,diff算法将在之前的结果以及新的结果中进行递归。
情况三:对子节点进行递归
- 默认条件下,当递归DOM节点的子元素时,React会同时遍历两个子元素的列表;当产生差异时,生成一个mutation。
- 如上图,前两个比较相同,不会有mutation。
- 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可。
- 当然这是理想情况
如果我们在中间插入一条数据:
- React会对每一个子元素产生一个mutation,而不是保持其不变。
- 这种方式会有一定的性能问题。
所以这时需要key来优化###
四. key优化##
- 在尾部添加数据
- 有无key意义并不大。
- 在前面插入数据
- 这种情况,在没有key的情况下,所有li都需要进行修改。
- 当子元素拥有key时,React使用key来匹配原有树上的子元素以及最新树上的子元素:这种情况下:原有的元素只是发生了位移。
render() {
return (
<div>
<h2>电影列表</h2>
<ul>
{
this.state.movies.map((item,index) => {
return <li key={item}>{item}</li>
})
}
</ul>
<button onClick={e => this.insertMovie()}>添加电影</button>
</div>
)
}
- key的注意事项:
- key应该是唯一的。
- key不要使用随机数(随机数在下一次render时,会重新生成一个数字)。
- 使用index作为key,对性能是没有优化的,id比较合适。
五. 组件嵌套的render调用##
import React, { Component } from 'react'
// Header
function Header() {
console.log("Header被调用");
return <h2>我是Header组件</h2>
}
// Banner
class Banner extends Component {
render() {
console.log('Banner的render函数被调用');
return <h3>我是bannner组件</h3>
}
}
function ProductList() {
console.log("ProductList被调用");
return (
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
<li>商品列表4</li>
<li>商品列表5</li>
</ul>
)
}
// Main
class Main extends Component {
render() {
console.log('Main render函数被调用');
return (
<div>
<Banner />
<ProductList />
</div >
)
}
}
// Footer
function Footer() {
console.log("Footer被调用");
return <h2>我是Footer组件</h2>
}
export default class App extends Component {
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>
<Header />
<Main />
<Footer />
</div>
)
}
increment(){
this.setState({
counter: this.state.counter + 1
})
}
}
- 调用一个无关的函数,界面改变时,按理来说不应该让别的没有改变的东西重新render。
- 这个例子中我们在前面插入了一个<h2>和<button>标签,现在点击按钮时全局都会重新渲染。
- 现在对其进行优化
六. 组件嵌套的render调用的优化##
- 调用完setState后,不想render时阻断其渲染。
- 使用shouldComponentUpdate() {}这个生命函数,默认情况下其返回true,也就是重新渲染;手动设置为false后,将不会重新渲染,但不影响初始化的渲染。
- 我们的目的是:想要阻断时阻断(事件发生后与界面没有依赖),不想阻断时渲染,如下代码。
shouldComponentUpdate(nextProps, nextState){
if(this.state.counter !== nextState.counter){
return true;
}
return false;
}
以上为简单情况,当组件变多后,情况将很复杂,函数/类组件都需要考虑到###
每个类都设置该生命周期函数太麻烦。
我们通过继承PureComponent而不是Component来进行简化,其会对state和props进行比较来决定是否重新render。
shouldComponentUpdate在源码中进行更新时,决定是否需要render。
回溯到源码ReactFiberClassComponent中时,有如下方法:
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
const instance = workInProgress.stateNode;
// 判断有无该生命周期函数
if (typeof instance.shouldComponentUpdate === 'function') {
startPhaseTimer(workInProgress, 'shouldComponentUpdate');
// -----------------
// 核心代码
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
stopPhaseTimer();
return shouldUpdate;
}
// -----------------
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
- 该方法最终返回true/false。
- 这个对应React中PureComponent的特点
- 其中isPureReactComponent属性对应上面放出来源代码中对原型中isPureReactComponent属性的判断。
- 如果有isPureReactComponent属性,则对oldProps,oldState和newProps,newState进行一个浅层比较。
- 通过浅层比较来判断是否发生了改变。
追溯到shallowEqual方法源码中
import is from './objectIs';
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
export default shallowEqual;
- 先判断两个对象,相同则true,返回后取反,表示不需要更新。
- 接着分别判断两个对象,不是对象或者为null时返回false,强制刷新
- 然后将两个对象中的keys取出来,若长度不想等则返回false,若相等,则对其中属性进行比较,不相等则返回false进行刷新。
这就回到我们案例中,只有App,Header,Footer的render被调用。
- PureComponent对props和state进行shallowEqual 。
- Main,Banner,ProductList没有依赖任何props/state,所以没有重新渲染。
- 开发中只需要shallowEqual,深层比较非常浪费性能。
- PureComponent可以解决类组件的render调用,但解决不了函数式组件
七. memo的使用,优化函数式组件##
memo为高阶组件。
const MemoHeader = memo(function Header() {
console.log("Header被调用");
return <h2>我是Header组件</h2>
})
- 我们将原来的函数组件传入memo函数中,生成一个新的组件类型。
- 将Footer也进行转换,这样只有App重新渲染了,
- 但我们没有更改ProductList,其也没有重新渲染,原因是在Main中,重新渲染已经被阻止了。
- 为了以防万一,也可以用memo优化。
理论上:建议所有类组件都用PureComponent,所有函数组件都包裹memo