React的更新机制
众所周知,React的渲染流程是将JSX转换为虚拟DOM再转换为真实DOM。那React的更新流程呢?
如图:
render函数重新执行后,产生新的DOM树,而React会对新旧的DOM树计算差异,者之间用的就是diff算法。计算后会打补丁patch,更新到真实的DOM。
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI。如果一棵树参考另外一棵树进行完全比较更新(递归),那么即使是最先进的算法 该算法的复杂程度为 O(n^3 ),其中n是树中元素的数量。如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销太过昂贵了,React的更新性能会变得非常低效。
所以React对这个算法进行了优化,将其复杂度降为了O(n),如何进行优化的呢?
- 同层节点之间相互比较,不会垮节点比较
- 不同类型的节点,产生不同的树结构
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定
情况一:对比不同类型的元素
- 当节点为不同的元素,React会拆卸原有的树,并且建立起新的树
- 当一个元素从 < a > 变成 < img >,从 < Article > 变成 < Comment >,或从 < Button > 变成 < div > 都会触发一个完整的重建流程;
- 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行 componentWillUnmount() 方法
- 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法, 紧接着 componentDidMount() 方法
比如下面的代码更改。
- React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用
情况二:对比同一类型的元素
- 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性
- 比如下面的代码更改
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性
- 比如下面的代码更改
- 当更新 style 属性时,React 仅更新有所更变的属性
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight
- 如果是同类型的组件元素
- 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps() 和 componentWillUpdate() 方法
- 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归
情况三:对子节点进行递归
- 在默认条件下,当递归 DOM 节点的子元素时,React 会同 时遍历两个子元素的列表;当产生差异时,生成一个mutation(变化)。
-
我们来看一下在最后插入一条数据的情况
-
前面两个比较是完全相同的,所以不会产生mutation,最后一个比较,产生一个mutation,将其插入到新的 DOM树中即可。
-
但是如果我们是在中间插入一条数据
-
React会对每一个子元素产生一个mutation,而不是保持 < li >星际穿越< /li >和
- 盗梦空间< /li >的不变
-
这种低效的比较方式会带来一定的性能问题。
keys优化
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
movies:["星际穿越","盗梦空间"]
}
}
render() {
return (
<div>
<h2>电影列表</h2>
<ul>
{
this.state.movies.map((item,index) => {
return <li>{item}</li>
})
}
</ul>
<button onClick = { e => this.insertMovie()}>添加电影</button>
</div>
)
}
insertMovie() {
console.log('添加电影');
}
}
页面渲染出来了,我们会发现报这么一个warning。建议我们列表最好加一个唯一的key
所以keys就很好使(特别是在插入数据的时候)
当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。 有了keys以后,React就会对原来的数组内容向后做位移的操作。这样大大的提高了性能。
keys要注意:
- key应该是唯一的
- key不要使用随机数(随机数在下一次render时,会重新生成一个数字)
- 使用index(遍历的索引)作为key,对性能是没有优化的(因为我们在最前面插入数据,原来的数据比如index是0和1,我后面的插入上来到最前面它变成了1和2,这还是没匹配上,匹配不了还是会创建index)
看一个组件嵌套的函数调用
import React, { Component } from 'react';
// Header
function Header() {
console.log("Header被调用");
return <h2>我是Header组件</h2>
}
// Main
class Banner extends Component {
render() {
console.log("Banner render函数被调用");
return <h3>我是Banner组件</h3>
}
}
function ProductList() {
console.log("ProductList被调用");
return (
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
<li>商品列表4</li>
<li>商品列表5</li>
</ul>
)
}
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
但是我们点击一次,所有的东西都被重新render了。
这个太浪费性能了。我们需要做优化,这样我们就得用到
shouldComponentUpdate
使用shouldComponentUpdate
import React, {Component} from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
message: "Hello World"
}
}
render() {
console.log("App render函数被调用");
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.counter !== nextState.counter) {
return true;
}
return false;
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
changeText() {
this.setState({
message: "张三你好"
})
}
}
如果原来的counter不能与最新的couter就return true。
这里shouldComponentUpdate中有两个参数(nextProps,nextState)
开发中我们肯定希望使用这个优化,但是它是类所特有的,而且写太多,都手动写,工作量太大了。
其实我们只需要让这个类继承于PureComponent
(这个类会自动实现这shouldComponentUpdate)
PureComponent
import React, { PureComponent } from 'react';
// Header
function Header() {
console.log("Header被调用");
return <h2>我是Header组件</h2>
}
// Main
class Banner extends PureComponent {
render() {
console.log("Banner render函数被调用");
return <h3>我是Banner组件</h3>
}
}
function ProductList() {
console.log("ProductList被调用");
return (
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
<li>商品列表4</li>
<li>商品列表5</li>
</ul>
)
}
class Main extends PureComponent {
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 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>
<Header/>
<Main/>
<Footer/>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
}
shouldComponentUpdate()return true和 return false决定组件更不更新的源码:
PureComponent的源码
浅层比较所在的位置,
浅层比较会比较这两个东西是否相同
结论:
此外:在开发中只要做浅层比较就行了,最好不要做深层比较,那个会十分消耗性能。(官网有说)
但是PureComponent做不到一个事情。它解决不了函数式组件重复调用的问题
。我们得用到memo。
memo的使用
import React, { PureComponent, memo } from 'react';
// Header
const MemoHeader = memo(function Header() {
console.log("Header被调用");
return <h2>我是Header组件</h2>
})
// Main
class Banner extends PureComponent {
render() {
console.log("Banner render函数被调用");
return <h3>我是Banner组件</h3>
}
}
const MemoProductList = memo(function ProductList() {
console.log("ProductList被调用");
return (
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
<li>商品列表4</li>
<li>商品列表5</li>
</ul>
)
})
class Main extends PureComponent {
render() {
console.log("Main render函数被调用");
return (
<div>
<Banner/>
<MemoProductList/>
</div>
)
}
}
// Footer
const MemoFooter = memo(function Footer() {
console.log("Footer被调用");
return <h2>我是Footer组件</h2>
})
export default 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/>
<Main/>
<MemoFooter/>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
}