一、React的diff算法
1.1 React更新机制
首先看一下Raect的渲染流程:
再看一下React的更新流程:
1.2 React更新流程
React在state或props发生改变时,调用render函数,渲染一颗新的虚拟DOM树;
React需要基于这两颗不同的树之间的差异来判别如何有效的更新UI:
- 同层之间相互比较,不会跨节点比较;
- 不同类型的节点,产生不同的树结构(如果以div标签为根的树改为了p标签,那div的所有子元素都重新渲染,产生p为根的树);
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
如图所示,只会进行同层之间的比较
tip:为什么不建议使用index作为key?
在列表渲染时,使用index作为key是没有性能提升的,因为diff算法在对key进行比较时,如果key为index索引,那么在列表中间插入一个值时,后面的所有index都会发生改变,原来元素的index和新的元素的index自然就会不同,即使他们是同一个元素,但是key为index,index不一致就会进行重渲染而不是移动原来的元素到新的位置。
二、SCU
在render函数执行时存在这么一个问题:
每当调用setState进行数据更新时,就会执行render函数来重新渲染我们的DOM结构,如果我们只更新了父组件中的数据,但是子组件内依赖的props并没有发生改变,只要调用render,就都会重新渲染,即使子组件的数据没有发生改变,这就会导致性能低下。
shouldComponentUpdate生命周期
React提供了一个生命周期方法 shouldComponentUpdate(简称为SCU),这个方法接受参数,并且要有返回值:
该方法有两个参数:
- 参数一:nextProps:props修改之后新的props属性;
- 参数二:nextState:state修改之后新的state属性。
这个生命周期函数可以决定render函数是否执行,返回值是一个boolean类型:
- 如果它返回的是一个true,就执行render;
- 如果返回的是false,那render不会执行,就不会重新渲染DOM。
- 默认返回的是true,也就是说只要state发生改变,就会调用render函数来重新渲染DOM
PrueComponent
但是如果每次都在组件内部使用 shouldComponentUpdate, 那么开发起来真的是好麻烦啊;
- 而使用shouldComponentUpdate的目的不外乎是当state中的数据发生改变时,来决定 shouldComponentUpdate 返回的是true还是false,来决定组件要不要重新渲染。
- React已经提供了一个方法实现这点,只需要将class组件继承自 PureComponent类就好了。
demo如下:App组件有Home和Banner两个子组件,Home有App传递的props,Banner没有porps
import React, { PureComponent } from 'react'
import Home from './Home'
import Banner from './Banner'
export class App extends PureComponent {
constructor() {
super()
this.state = {
message: "Hello World"
}
}
chanegText() {
this.setState({
message: "你好,世界"
})
}
render() {
console.log("App render")
const { message } = this.state
return (
<div>
<h2>{message}</h2>
<button onClick={e => this.chanegText()}>修改文本</button>
<Home message={message} />
<Banner />
</div>
)
}
}
export default App
import React, { PureComponent } from 'react'
export class Home extends PureComponent {
constructor(props) {
super(props)
}
render() {
console.log("Home render")
const { message } = this.props
return (
<div>
<h2>Home</h2>
<div>{message}</div>
</div>
)
}
}
export default Home
import React, { PureComponent } from 'react'
export class Banner extends PureComponent {
constructor() {
super()
}
render() {
console.log("Banner render")
return (
<div>Banner</div>
)
}
}
export default Banner
memo 高阶组件
函数式组件如何实现PrueComponent那样在props没有改变时,不要重新渲染DOM树呢?;
只需要使用高阶组件memo就可以了:
import { memo } from "react"
const PreFile = memo(function() {
return (
<div>
PreFile
</div>
)
})
export default PreFile
React官网:zh-hans.react.dev/reference/r…
三、useSelector的第二个参数shallEqual
useSelector 的作用是将state映射到组件中:
- 参数一:将state映射到需要的数据中;
- 参数二:shallEqual, 从react-redux包中导入,可以进行比较来决定是否组件重新渲染;
import { connect } from 'react-redux';
import { shallowEqual } from 'react-redux';
const MyComponent = ({ data }) => {
// 组件内部逻辑
};
const mapStateToProps = (state) => ({
data: state.data,
});
export default connect(mapStateToProps, null, null, { areStatePropsEqual: shallowEqual })(MyComponent);
四、useCallback,useMemo,useRef
useCallback
- 当需要将一个函数传递给子组件时,最好使用useCallback进行优化,将优化之后的函数,传递给子组件;
如何进行性能的优化呢?
- useCallback会返回一个函数的 memoized(记忆的)值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { memo, useCallback, useState } from 'react'
// Message组件
const Message = memo((props) => {
const { addCount } = props
console.log('Message组件被渲染')
return (
<div>
<button onClick={addCount}>数字增加</button>
</div>
)
})
// App组件
const App = memo(() => {
const [message, setMessage] = useState('Hello World')
const [count, setCount] = useState(10)
// 使用useCallback优化的函数:数字 + 1
const addCount = useCallback(() => {
setCount(count + 1)
},[count])
// 普通函数:数字 + 1
// function addCount() {
// setCount(count + 1 )
// }
// 普通函数:修改文本
function changeMessage() {
setMessage(Math.random())
}
return (
<div>
<h2>App</h2>
<div>{count}</div>
<button onClick={addCount}>+1</button>
<div>{message}</div>
{/* 点击修改文本按钮时子组件不会重新渲染,
因为传递给子组件的函数是通过useCallback优化过的,
如果使用的是普通函数,则会重新渲染子组件 */}
<button onClick={changeMessage}>修改文本</button>
{/* 使用Message组件, 传递addCount方法 */}
<Message addCount={addCount} />
</div>
)
})
export default App
可以看到使用了useCallback优化之后的addCount函数传递给子组件,在message发生改变时并没有重新渲染子组件,但是如果传递给子组件的是一个普通的函数,那么在message改变的时候也会重新渲染子组件
但是在 addCount 函数触发时也会让子组件重新渲染,那如果想要子组件在addCount函数触发时也不要重新渲染应该怎么做呢?
答案:使用 useRef,将addCount 方法进一步优化
下面介绍学习一下 useRef
useRef
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
最常用的ref是两种用法:
- 用法一:引入DOM(或者组件,但是需要是class组件)元素;
- 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
import React, { memo } from 'react'
import { useRef } from 'react'
const App = memo(() => {
const titleRef = useRef()
const inputRef = useRef()
function getTitleDom() {
console.log(titleRef.current)
}
function getFocus() {
inputRef.current.focus()
}
return (
<div>
App
<h2 ref={titleRef}>App</h2>
<input type="text" ref={inputRef} />
<button onClick={getTitleDom}>获取h2的Dom</button>
<button onClick={getFocus}>输入框获取焦点</button>
</div>
)
})
export default App
useMemo
useMemo实际的目的也是为了进行性能的优化。
- useMemo返回的也是一个 memoized(记忆的)值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
优化的场景:
- 进行大量的计算操作,是否有必须要每次渲染时都重新计算;
- 对子组件传递相同内容的对象时,用useMemo进行性能优化;
a. useMemo与useCallback的区别:
- useMemo返回的是一个值;
- useCallback返回的是一个函数;
下面两条语句作用相等
const foo = useCallback(fn, [])
const foo2 = useMemo(() => fn, [])
import React, { memo, useState, useMemo } from 'react'
function calcCountTotal(num) {
console.log('计算被调用')
let res = 0
for (let i = 0; i <= num; i++) {
res += i
}
return res
}
const HelloWorld = memo((props) => {
console.log('子组件被渲染')
return (
<div>
<h2>Hello World组件</h2>
</div>
)
})
const App = memo(() => {
console.log('组件被渲染')
const [count, setCount] = useState(10)
const [num, setNum] = useState(0)
// 只有count发生改变时,才会回调这个useMemo中的回调函数
const result = useMemo(() => {
return calcCountTotal(count)
}, [count])
// const result = calcCountTotal(10)
function addCount() {
setCount(count + 1)
}
function addNum() {
setNum(num + 1)
}
const info = useMemo(() => ({name: 'lyx', age: 18}), [])
return (
<div>
Memo
<div>{count}的阶乘:{result}</div>
<button onClick={addCount}>+1</button>
<hr />
<div>{num}</div>
<button onClick={addNum}>+1</button>
<hr />
{/* 使用useMemo优化后,只要info对象不发生改变,子组件就不会重新渲染 */}
<HelloWorld info={info} />
</div>
)
})
export default App
可以看到只有count发生改变时才会重新调用 calcCountTotal 函数,如果不适用 useMemo 对值进行优化,那么在 num 发生改变时,也会重新调用calcCountTotal 函数,这是会影响到性能的。
并且使用 useMemo 优化 info 后,只要 info 对象不发生改变,子组件就不会重新渲染。