React更新机制
React
更新流程如下:
React
在props
或state
发生改变时,会调用React
的render
方法,会创建不同的DOM
树
React
需要基于这两棵不同的树之间的差别来判断如何有效的更新UI
- 如果进行全量比较更新(旧
DOM
树的每个节点都和新DOM
树每个节点比较) ,即使是最先进的算法,其复杂度也需要O(n²)
,其中n
是树中元素的数量 - 这种全量比较更新的算法会使得
React
更新性能变得低效
React
对这个算法的复杂度优化为O(n)
,优化手段如下:
- 同层节点之间相互比较,不会跨节点比较
- 不同类型的节点,产生不同的树结构
- 通过
key
来指定哪些节点在不同的渲染下保持稳定.
key的优化
- 在通过循环遍历生产
DOM
结构时,如果没有绑定key
属性,浏览器会发出警告
- 需要给每个遍历生成的
DOM
节点绑定一个唯一的key
属性
key
的作用:
key
是React
用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识React
在Diff
算法对比更新时,会借助元素的key
来判断该元素是新创建的还是被移动而来的,进而减少不必要重渲染React
还借助key
来判断元素与本地状态的关联关系
key
对新增元素的意义:
- 在
DOM
树最后新增元素: 有无key
属性的意义不大
-
在
DOM
树前面或者中间新增元素:- 没有绑定
key
: 在新增元素的位置到最后的DOM
节点都会进行修改 - 绑定
key
: 通过key
找到对应元素进行移位复用,不需要修改任何DOM
元素,只需将新增元素插入到对应的位置即可
- 没有绑定
key
的注意事项
key
应该是唯一的key
不要使用随机数(随机数在下一次render
时,会重新生成)- 若使用索引作为
key
,对性能则没有优化
render优化
- 当父组件调用
setState
更改数据时,组件本身会重新调用render
函数,但是子组件的render
函数也都会被重新调用
jsx
class App extends Component {
constructor() {
super()
this.state = {
text: 'Hello,React',
}
}
changeText() {
this.setState({text: '你好啊,React'})
}
render() {
console.log('App render');
const { text } = this.state
return (
<div>
<h2>APP-{text}</h2>
<button onClick={e => this.changeText()}>修改文本</button>
<Home/>
<Recommend/>
</div>
)
}
}
- 如果只修改了组件本身的数据,其自身和所有后代组件的
render
函数都会被重新调用,进行diff
算法,这样会造成性能很低 - 调用
render
应该有一个前提,那就是依赖的数据(state、props)发生改变时,再调用render
方法 React
提供了一个生命周期函数shouldComponentUpdate
,可用于控制是否调用render
重渲染
该生命周期函数提供了两个参数和必须要一个返回值
- 第一个参数
newProps
: 修改之后的props
属性 - 第二个参数
newState
: 修改之后的state
属性
- 返回值: 布尔类型,用于决定是否调用
render
,默认为true
,即只要调用setState
都会重新render
javascript
shouldComponentUpdate(newProps, newState) {
if(this.state.text !== newState.text) return true
return false
}
PureComponent
- 在数据非常庞大的时候,如果所有组件都要手动实现
shouldComponentUpdate
,那么工作量则一言难尽
使用
shouldComponentUpdate
的目的: 根据props
或state
中的数据是否发生了改变,来决定是否需要重新执行render
函数
React
提供了一个叫PureComponent
的类,其内部已经实现了这种机制,让组件继承PureComponent
javascript
import { PureComponent } from 'react'
class App extends PureComponent { }
- 那么函数式组件中不能继承
PureComponent
,需要使用高阶函数memo
对其进行包裹,实现这种机制
javascript
import { memo } from 'react'
const App = memo(function(props) {})
数据的不可变性
- 其实
PureComponent
内部只做了浅层比较,即如果在props
或state
中存在引用类型的数据,那么修改其深层数据时,PureComponent
是无法判断的
jsx
class App extends PureComponent {
constructor() {
super()
this.state = {
books: [
{ name:'你不知道的JavaScript', price: 99, count: 1 },
]
}
}
addNewBook() {
const newBook = { name:'React高级设计', price: 118, count: 1 };
// 改变了state中的books
this.state.books.push(newBook)
// 调用setState
this.setState({
books: this.state.books
})
}
render() {
const { books } = this.state
return (
<div>
<button onClick={e => this.addNewBook()}>添加新书籍</button>
</div>
)
}
}
- 当组件继承自
PureComponent
时,上面的做法并不会让界面更新,官方文档中也建议不使用这种做法
- 官方文档的意思是,在修改引用类型的深层数据时,
PureComponent
内部比较的是其引用指针,并且避免这种修改数据的方式 - 官方文档: zh-hans.legacy.reactjs.org/docs/optimi...
- 所以最好改用以下写法,让
state
中books
的获得一个新的引用
javascript
addNewBook() {
const newBook = { name:'React高级设计', price: 118, count: 1 };
this.setState({
books: [...this.state.books, newBook]
})
}
shallowEqual
- 在
checkShouldComponentUpdate
方法中,如果是PureComponent
组件,则使用shallowEqual
函数对state
或props
进行浅层比较
shallowEqual函数源码:
javascript
function shallowEqual(objA: mixed, objB: mixed): boolean {
// shallowEqual函数首先会对新旧state或props进行比较,判断是否为同一个对象
if (is(objA, objB)) {
return true; // 是则不用执行render
}
// 判断新旧state或props是否为对象或者null
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false; // 不成立则执行render
}
// 然后取出新旧 `state` 或 `props` 中所有属性 ,首先判断属性的数量是否相等
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false; // // 不相等则直接重新render
}
// 最后会对新旧state 或 props 中每个属性进行判断
for (let i = 0; i < keysA.length; i++) {
if (
// 新state(或props)的属性是否在旧的中
!hasOwnProperty.call(objB, keysA[i]) ||
// 如果在则判断两个属性是否为相等(引用类型会判断其引用指针)
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false; // 不成立则执行render
}
}
// 最后如果上面都不成立,则不执行render
return true;
}