危险

为之则易,不为则难

0%

20_React 补充

今日目标

✔ 掌握如何进行状态和业务逻辑的复用。

✔ 了解 React 内置的其他 Hook。

基本需求

建议非 TS 环境下进行学习测试!

1
npx create-react-app test --template typescript

index.tsx

1
2
3
4
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.querySelector('#root'))

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component } from 'react'
import Image from './components/Image'
import Position from './components/Position'

export default class App extends Component {
render() {
return (
<div>
<Image />
<Position />
</div>
)
}
}

components/Image.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { Component } from 'react'
import dva from '../assets/dva.png'

export default class Image extends Component {
state = {
x: 0,
y: 0,
}
move = (e: MouseEvent) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
componentDidMount(): void {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount(): void {
document.removeEventListener('mousemove', this.move)
}
render() {
return (
<div>
<img
src={dva}
width='100'
height='100'
alt=''
style={{
position: 'absolute',
left: this.state.x,
top: this.state.y,
}}
/>
</div>
)
}
}

components/Position.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react'

export default class Position extends Component {
state = {
x: 0,
y: 0,
}
move = (e: MouseEvent) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
componentDidMount(): void {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount(): void {
document.removeEventListener('mousemove', this.move)
}
render() {
return (
<div>
<h3>
x: {this.state.x} y: {this.state.y}
</h3>
</div>
)
}
}

render-props

有一个叫做 render 的属性,可以通过 props 进行获取,官方文档

基本概述

  • 思考:如果两个组件中的部分功能相似或相同,该如何处理?

  • 解决:复用相似的部分(可以联想函数封装)。

  • 复用什么:1. state 2. 操作 state 的方法(业务逻辑),一般组件的 UI 不会相同。

  • 解决方案有哪些?

    • mixins,命名冲突、数据来源不清晰,官方文档

    • render-props,父组件传递 render 属性(只不过 render 对应的值是一个函数,函数的返回值就是要渲染的结构/组件),子组件通过 props 进行接收。

    • HOC,本质是一个函数,通过函数调用返回加工后的新组件。

    • 上面两种方式不是新的 API,而是利用 React 自身特点的编码技巧,演化而成的固定模式(写法)。

    • Hooks。

基本使用

  • 思路:将要复用的 state 和操作 state 的方法封装到一个组件中。

  • 问题 1:渲染的 UI 内容不一样,该怎么办?

  • 在使用组件时,添加一个值为函数的 prop,通常把这个 prop 命名为 render,在组件内部调用这个函数,函数的返回值就是需要渲染的 UI。

  • 问题 2:如果获取组件内部的状态

  • 在组件内部调用方法的时候,把状态当成参数进行传递。

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component } from 'react'
import Image from './components/Image'
import Position from './components/Position'
import Mouse from './components/Mouse'

export default class App extends Component {
render() {
return (
<div>
<Mouse render={(state) => <Image {...state} />} />
<Mouse render={(state) => <Position {...state} />} />
</div>
)
}
}

components/Mouse.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Component } from 'react'

type State = {
x: number
y: number
}

type Props = {
// React.ReactElement => JSX
render: (state: State) => React.ReactElement
}

export default class Mouse extends Component<Props> {
state = {
x: 0,
y: 0,
}
move = (e: MouseEvent) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
componentDidMount(): void {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount(): void {
document.removeEventListener('mousemove', this.move)
}
render() {
return this.props.render(this.state)
}
}

components/Image.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from 'react'
import dva from '../assets/dva.png'

type Props = {
x: number
y: number
}

export default class Image extends Component<Props> {
render() {
return (
<div>
<img
src={dva}
width='100'
height='100'
alt=''
style={{
position: 'absolute',
left: this.props.x,
top: this.props.y,
}}
/>
</div>
)
}
}

components/Position.tsx

1
2
3
4
5
6
7
8
9
10
11
12
type Props = {
x: number
y: number
}

export default function Position({ x, y }: Props) {
return (
<h3>
x: {x} y: {y}
</h3>
)
}

代码优化

  • 注意:并不是该模式叫 render props 就必须规定传递的 prop 为 render ,实际上可以使用任意名称的 prop。

  • 把 prop 是一个函数并且告诉组件要渲染什么内容的技术叫做:render props 模式。

  • 推荐:使用 children 代替 render 属性。

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component } from 'react'
import Image from './components/Image'
import Position from './components/Position'
import Mouse from './components/Mouse'

export default class App extends Component {
render() {
return (
<div>
<Mouse>{(state) => <Image {...state} />}</Mouse>
<Mouse>{(state) => <Position {...state} />}</Mouse>
</div>
)
}
}

components/Mouse.tsx

1
2
3
4
5
export default class Mouse extends Component<Props> {
render() {
return this.props.children(this.state)
}
}

缺点分析

滚动位置的状态和业务逻辑的封装。

components/Mouse.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Component } from 'react'

type State = {
left: number
top: number
}
type Props = {
// 注意返回值
children: (state: State) => React.ReactElement
}

export default class Scroll extends Component<Props, State> {
state = {
top: 0,
left: 0,
}
scroll = (e: Event) => {
this.setState({
left: window.pageXOffset,
top: window.pageYOffset,
})
}
componentDidMount() {
window.addEventListener('scroll', this.scroll)
}
componentWillUnmount() {
window.removeEventListener('scroll', this.scroll)
}
render() {
return this.props.children(this.state)
}
}

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Component } from 'react'
import Image from './components/Image'
import Position from './components/Position'
import Mouse from './components/Mouse'
import Scroll from './components/Scroll'

export default class App extends Component {
render() {
return (
<div style={{ height: 3000 }}>
<Mouse>{(state) => <Image {...state} />}</Mouse>
<Scroll>{(scrollState) => <Mouse>{(mouseState) => <Position {...scrollState} {...mouseState} />}</Mouse>}</Scroll>
</div>
)
}
}

components/Position.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Props = {
x: number
y: number
left: number
top: number
}

export default function Position({ x, y, left, top }: Props) {
return (
<h3 style={{ position: 'fixed' }}>
x: {x} y: {y} left: {left} top: {top}
</h3>
)
}

高阶组件 HOC

概述

  • 目的:实现状态和业务逻辑的复用,增强一个组件的能力。

  • 采用包装(装饰)模式,比如说:手机壳。

  • 手机:获取保护功能。

  • 手机壳 :提供保护功能。

  • 高阶组件就相当于手机壳,通过包装组件,增强组件功能。

思路分析

  • 高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件。

  • 高阶组件的命名:withMousewithRouterwithXXX

  • 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态和业务逻辑,通过 prop 将复用的状态传递给被包装的组件。

1
2
const CatWithMouse = withMouse(Cat)
const PositionWithMOuse = withMouse(Position)
1
2
3
4
5
6
7
8
9
10
// 高阶组件内部创建的类组件:
const WithMouse = (Base) => {
class Mouse extends React.Component {
// 处理鼠标的位置等操作
render() {
return <Base {...this.state} />
}
}
return Mouse
}

使用步骤

  1. 创建一个函数,名称约定以 with 开头。

  2. 指定函数参数(作为要增强的组件) 传入的组件只能渲染基本的 UI。

  3. 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回。

  4. 在内部创建的组件的 render 中,需要渲染传入的基本组件,增强功能,通过 props 的方式给基本组件传值。

  5. 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中。

components/withMouse.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react'

type Props = {
[key: string]: any
}

export default function withMouse(Base: any) {
class Mouse extends React.Component<Props> {
state = {
x: 0,
y: 0,
}
move = (e: MouseEvent) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
componentDidMount(): void {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount(): void {
document.removeEventListener('mousemove', this.move)
}
render() {
return <Base {...this.state} {...this.props} />
}
}
return Mouse
}

Image.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react'
import dva from '../assets/dva.png'

type Props = {
x: number
y: number
name: string
}

export default class Image extends Component<Props> {
render() {
return (
<div>
name: {this.props.name}
<img
src={dva}
width='100'
height='100'
alt=''
style={{
position: 'absolute',
left: this.props.x,
top: this.props.y,
}}
/>
</div>
)
}
}

Position.tsx

1
2
3
4
5
6
7
8
9
10
11
12
type Props = {
x: number
y: number
}

export default function Position({ x, y }: Props) {
return (
<h3>
x: {x} y: {y}
</h3>
)
}

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from 'react'
import Image from './components/Image'
import Position from './components/Position'
import withMouse from './components/withMouse'

const ImageHOC = withMouse(Image)
const PositionHOC = withMouse(Position)

export default class App extends Component {
render() {
return (
<div style={{ height: 3000 }}>
<ImageHOC name='ifer' />
<PositionHOC />
</div>
)
}
}

名字优化

components/withMouse.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from 'react'

type Props = {
[key: string]: any
}

function getDisplayName(Base: any) {
// displayName 作为组件的静态方法
return Base.displayName || Base.name || 'Component'
}

export default function withMouse(Base: any) {
class Mouse extends React.Component<Props> {
state = {
x: 0,
y: 0,
}
// #mark
static displayName = `WithMouse${getDisplayName(Base)}`
move = (e: MouseEvent) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
componentDidMount(): void {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount(): void {
document.removeEventListener('mousemove', this.move)
}
render() {
return <Base {...this.state} {...this.props} />
}
}

return Mouse
}

缺点演示

组件结构嵌套过深,封装麻烦,相对 render-props 使用简单些。

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component } from 'react'
import Image from './components/Image'
import Position from './components/Position'
import withMouse from './components/withMouse'
import withScroll from './components/withScroll'

const ImageHOC = withMouse(Image)
const ScrollPositionHOC = withScroll(withMouse(Position))

export default class App extends Component {
render() {
return (
<div style={{ height: 3000 }}>
<ImageHOC name='ifer' />
<ScrollPositionHOC name='xxx' />
</div>
)
}
}

components/withScroll.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react'

type Props = {
[key: string]: any
}

// React.ComponentType 组件类型
// React.ComponentType<any> 不关心组件的 props 类型
export default function withScroll(Base: React.ComponentType<any>) {
class Scroll extends Component<Props> {
state = {
top: 0,
left: 0,
}
scroll = (e: Event) => {
this.setState({
left: window.pageXOffset,
top: window.pageYOffset,
})
}
componentDidMount() {
window.addEventListener('scroll', this.scroll)
}
componentWillUnmount() {
window.removeEventListener('scroll', this.scroll)
}
render() {
// return this.props.children(this.state)
return <Base {...this.state} {...this.props} />
}
}
return Scroll
}

components/Position.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Props = {
x: number
y: number
left: number
top: number
name: string
}

export default function Position({ x, y, left, top, name }: Props) {
return (
<h3>
x: {x} y: {y} left: {left} top: {top} name: {name}
</h3>
)
}

自定义 Hooks

官方文档

基本使用

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component } from 'react'
import Position from './components/Position'
import Image from './components/Image'

export default class App extends Component {
render() {
return (
<div style={{ height: 3000 }}>
<Position />
<Image />
</div>
)
}
}

components/Image.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { useEffect, useState } from 'react'
import dva from '../assets/dva.png'

export default function Image() {
const [mouse, setMouse] = useState({
x: 0,
y: 0,
})
useEffect(() => {
const move = (e: MouseEvent) => {
setMouse({
x: e.pageX,
y: e.pageY,
})
}
document.addEventListener('mousemove', move)
return () => {
document.removeEventListener('mousemove', move)
}
}, [])
return (
<div>
<img
src={dva}
width='100'
height='100'
alt=''
style={{
position: 'absolute',
left: mouse.x,
top: mouse.y,
}}
/>
</div>
)
}

components/Position.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useEffect, useState } from 'react'

export default function Position() {
const [mouse, setMouse] = useState({
x: 0,
y: 0,
})
useEffect(() => {
const move = (e: MouseEvent) => {
setMouse({
x: e.pageX,
y: e.pageY,
})
}
document.addEventListener('mousemove', move)
return () => {
document.removeEventListener('mousemove', move)
}
}, [])
return (
<h3>
x: {mouse.x} y: {mouse.y}
</h3>
)
}

代码优化

hooks/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useState, useEffect } from 'react'
export function useMouse() {
const [mouse, setMouse] = useState({
x: 0,
y: 0,
})
useEffect(() => {
const move = (e: MouseEvent) => {
setMouse({
x: e.pageX,
y: e.pageY,
})
}
document.addEventListener('mousemove', move)
return () => {
document.removeEventListener('mousemove', move)
}
}, [])
return mouse
}

components/Position.tsx

1
2
3
4
5
6
7
8
9
10
import { useMouse } from '@/hooks'

export default function Position() {
const mouse = useMouse()
return (
<h3>
x: {mouse.x} y: {mouse.y}
</h3>
)
}

components/Image.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import dva from '../assets/dva.png'
import { useMouse } from '@/hooks'

export default function Image() {
const mouse = useMouse()
return (
<div>
<img
src={dva}
width='100'
height='100'
alt=''
style={{
position: 'absolute',
left: mouse.x,
top: mouse.y,
}}
/>
</div>
)
}

class 组件性能优化

减轻 state

  • 减轻 state:只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等)。

  • 注意:不做渲染的数据不要放在 state 中,比如定时器 id 等,Vue 中也一样不要把和渲染无关的数据放到 data 中。

  • 对于这种需要在多个方法中用到的数据,可以直接挂载到 this 上,例如 this.xxx = 'bbb'

1
2
3
4
5
6
7
8
9
10
class Hello extends Component {
componentDidMount() {
// timerId存储到this中,而不是state中
this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() { … }
}

避免不必要的重新渲染

  • 默认情况下只要父组件更新,所有的后代组件也会重新进行渲染。

  • 问题:子组件没有任何变化时也会重新渲染 (接收到的 props 没有发生任何的改变)。

  • 如何避免不必要的重新渲染呢?

  • 解决方式:使用钩子函数 shouldComponentUpdate(nextProps, nextState)

  • 作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染。

  • 触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate => render)。

问题演示

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component } from 'react'
import Child1 from './components/Child1'
import Child2 from './components/Child2'
export default class App extends Component {
state = {
name: 'ifer',
age: 18,
}
handleClick = () => {
this.setState({ age: this.state.age + 1 })
}
render() {
console.log('app')
const { name, age } = this.state
return (
<div>
App
<button onClick={this.handleClick}>+1</button>
<hr />
<Child1 name={name} />
<Child2 age={age} />
</div>
)
}
}

components/Child1.tsx

1
2
3
4
5
6
7
8
import React, { Component } from 'react'

export default class Child1 extends Component<{ name: string }> {
render() {
console.log('child1')
return <div>Child1: {this.props.name}</div>
}
}

components/Child2.tsx

1
2
3
4
5
6
7
8
import React, { Component } from 'react'

export default class Child2 extends Component<{ age: number }> {
render() {
console.log('child2')
return <div>Child2: {this.props.age}</div>
}
}

问题解决

components/Child1.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'

export default class Child1 extends Component<{ name: string }> {
shouldComponentUpdate(nextProps: { name: string }) {
if (this.props.name !== nextProps.name) {
return true
}
return false
}
render() {
console.log('child1')
return <div>Child1: {this.props.name}</div>
}
}

纯组件

  • 纯组件:React.PureComponentReact.Component 功能相似。

  • 区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较。

  • 原理:纯组件内部通过分别对比前后两次 props 和 state 的值,来决定是否需要重新渲染组件。

  • 注意:只有在性能优化的时候才使用纯组件,不建议所有组件无脑使用,因为纯组件也需要消耗性能才能进行对比。

1
2
3
4
5
class Hello extends React.PureComponent {
render() {
return <div>纯组件</div>
}
}

为什么不要修改原数据

shouldComponentUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react'
export default class App extends React.Component {
state = {
age: 1,
}
updateAge = () => {
// !#1 this.state.age
this.state.age += 1
this.setState({
// !#2 nextState.age
age: this.state.age,
})
}
shouldComponentUpdate(nextProps: any, nextState: any) {
if (nextState.age !== this.state.age) {
return true
} else {
return false
}
}
render() {
const { age } = this.state
return (
<div>
<p>name: {age}</p>
<button onClick={this.updateAge}>修改 name</button>
</div>
)
}
}

React.memo

基本概述

  • 是什么:React.memo 是一个高阶组件,用来记忆(memorize)组件。

  • 场景:当你想要避免函数组件 props 没有变化而产生的不必要更新时,就要用到 React.memo 了。

  • 作用:记忆组件上一次的渲染结果,在 props 没有变化时复用该结果,避免函数组件不必要的更新

  • 参数(Child):需要被记忆的组件,或者说是需要避免不必要更新的组件。

  • 返回值(MemoChild):React 记住的 Child 组件。

代码演示

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from 'react'
import Child1 from './components/Child1'
import Child2 from './components/Child2'

export default function App() {
const [name] = useState('ifer')
const [age, setAge] = useState(18)
const handleClick = () => setAge(age + 1)
console.log('app~~~')
return (
<div>
App
<button onClick={handleClick}>+1</button>
<hr />
<Child1 age={age} />
<Child2 name={name} />
</div>
)
}

components/Child1.tsx

1
2
3
4
5
function Child1({ age }: { age: number }) {
console.log('child1~~~')
return <div>Child1: {age}</div>
}
export default Child1

components/Child2.tsx

1
2
3
4
export default function Child2({ name }: { name: string }) {
console.log('child2~~~')
return <div>Child2: {name}</div>
}

问题 1:点击按钮只修改了 Child1 组件中用到的 age,Child2 也被渲染了。

解决:使用 React.memo 包裹 Child2。

1
2
3
4
5
6
import React from 'react'
function Child2({ name }: { name: string }) {
console.log('child2~~~')
return <div>Child2: {name}</div>
}
export default React.memo(Child2)

问题 2:给 Child2 组件传递了一个函数试试,发现 React.memo() 失效。

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useState } from 'react'
import Child1 from './components/Child1'
import Child2 from './components/Child2'

export default function App() {
const [age, setAge] = useState(18)
const handleClick = () => setAge(age + 1)
const updateName = () => {}
console.log('app~~~')
return (
<div>
App
<button onClick={handleClick}>+1</button>
<hr />
<Child1 age={age} />
{/* #1 */}
<Child2 updateName={updateName} />
</div>
)
}

components/Child2.tsx

1
2
3
4
5
6
7
import React from 'react'
// #2
function Child2({ updateName }: { updateName: () => void }) {
console.log('child2~~~')
return <div>Child2</div>
}
export default React.memo(Child2)

浅层对比

  • 也就是说,对于对象类型的 prop 来说,只会比较引用,对于 Child1 组件来说,每次父组件更新接收到的都是一个全新的函数。

  • 如果你要手动控制比较过程,可以使用 React.memo 的第二个参数,如果返回 true,表示记住(不重新渲染)该组件;如果返回 false,表示重新渲染该组件。

image-20210830220716165

解决方式 1: 手动比较。

components/Child1.tsx

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
function Child1({ name, updateName }: { name: string; updateName: () => void }) {
console.log('child1')
return <div>Child1: {name}</div>
}
export default React.memo(Child1, (prevProps, nextProps) => {
// name 不相等时,才需要更新
// 不相等成立表达式返回的结果则是 true
// false 才表示不更新,所以需要取反
return !(prevProps.name !== nextProps.name)
})

解决方式 2: useRef。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState, useRef } from 'react'
import Child1 from './components/Child1'
import Child2 from './components/Child2'

export default function App() {
const [name] = useState('ifer')
const [age, setAge] = useState(18)
const handleClick = () => {
setAge(age + 1)
}
const updateName = useRef(() => {})
console.log('app')
return (
<div>
App
<button onClick={handleClick}>+1</button>
<hr />
<Child1 name={name} updateName={updateName.current} />
<Child2 age={age} />
</div>
)
}

useCallback

  • useCallback Hook:记住函数的引用,在组件每次更新时返回相同引用的函数。

  • useMemo Hook:记住任意数据(数值、对象、函数等),在组件每次更新时返回相同引用的数据【功能之一】。

  • 场景:useCallback 可以在组件每次更新时都能获取到相同引用的函数,需要配合 React.memo 高阶函数一起使用

  • 作用:记忆传入的回调函数,这个被记住的回调函数会一直生效,直到依赖项发生改变。

image-20210830230820580

  • 第一个参数:必选,需要被记忆的回调函数。

  • 第二个参数:必选,依赖项数组,用于指定回调函数中依赖(用到)的数据(类似于 useEffect 的第二个参数)。

  • 即使没有依赖,也得传入空数组([]),此时,useCallback 记住的回调函数就会一直生效。

  • 返回值:useCallback 记住的回调函数,直到依赖项发生改变。

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState, useCallback } from 'react'
import Child1 from './components/Child1'
import Child2 from './components/Child2'

export default function App() {
const [name] = useState('ifer')
const [age, setAge] = useState(18)
const handleClick = () => {
setAge(age + 1)
}
const updateName = useCallback(() => {}, []) // 依赖项变了 updateName 才会变化
console.log('app')
return (
<div>
App
<button onClick={handleClick}>+1</button>
<hr />
<Child1 name={name} updateName={updateName} />
<Child2 age={age} />
</div>
)
}

🤔 注意:useCallback 需要配置 React.memo 使用才有意义,不然反而性能更低,因为 useCallback 来包裹函数也是需要开销的。

useMemo

使用场景:类似于 useCallback,可以在组件更新期间保持任意数据引用相等,一般用来处理对象类型的数据。

对比:useCallback 只能记忆函数,而 useMemo 可以记忆任意数据。

作用:记忆任意数据,这个被记住的数据会一直生效,直到依赖项发生改变

image-20210901200419531

  • 第一个参数:必选,回调函数,该回调函数一上来就会被调用,并通过返回值指定需要被记住的数据

  • 第二个参数:必选,依赖项数组,用于指定回调函数中依赖的数据。同样,没有依赖项时,传入空数组([])。

  • 返回值:useMemo 记住的数据,直到依赖项发生改变。

  • 如何选择使用哪一个?如果处理的是函数,推荐使用 useCallback,如果处理的是其他数据(比如对象),推荐使用 useMemo。

  • useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useState, useMemo } from 'react'
import Child1 from './components/Child1'
import Child2 from './components/Child2'

export default function App() {
const [name] = useState('ifer')
const [age, setAge] = useState(18)
const handleClick = () => {
setAge(age + 1)
}
const updateName = useMemo(() => {
return () => {}
}, []) // 依赖项变了 updateName 才会变化
console.log('app')
return (
<div>
App
<button onClick={handleClick}>+1</button>
<hr />
<Child1 name={name} updateName={updateName} />
<Child2 age={age} />
</div>
)
}

🤔 大量计算得到的结果,可以使用 useMemo 进行优化。

1
2
3
4
5
6
7
8
9
10
11
import { useMemo } from 'react'
export default function App() {
const total = useMemo(() => {
let total = 0
for (let i = 0; i < 1000000000; i++) {
total += i
}
return total
}, [])
return <div>{total}</div>
}

useReducer

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useReducer } from 'react'

type CountAction = {
type: 'INCREMENT'
payload: number
}

function counterReducer(state: number, action: CountAction) {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
default:
return state
}
}

export default function App() {
const [count, dispatch] = useReducer(counterReducer, 0)
return (
<div>
<h3>{count}</h3>
<button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>+1</button>
</div>
)
}

抽离 reducer

store/reducers/counter.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
type CountAction = {
type: 'INCREMENT'
payload: number
}

export default function counterReducer(state: number, action: CountAction) {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
default:
return state
}
}

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
import { useReducer } from 'react'
import counterReducer from './store/reducers/counter'

export default function App() {
const [count, dispatch] = useReducer(counterReducer, 0)
return (
<div>
<h3>{count}</h3>
<button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>+1</button>
</div>
)
}

进阶使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useReducer } from 'react'
import Child1 from './components/Child1'
import Child2 from './components/Child2'
import counterReducer from './store/reducers/counter'
// #1
import GlobalContext from './context'

export default function App() {
const [count, dispatchCount] = useReducer(counterReducer, 0)
return (
<GlobalContext.Provider
value={{
count,
dispatchCount,
}}
>
<Child1 />
<Child2 />
</GlobalContext.Provider>
)
}

components/Child1.tsx

1
2
3
4
5
6
7
8
9
10
11
12
import { useContext } from 'react'
import GlobalContext from '../context'

export default function Child1() {
const { count, dispatchCount } = useContext(GlobalContext) as any
return (
<div>
<h3>Child1: {count}</h3>
<button onClick={() => dispatchCount({ type: 'INCREMENT', payload: 3 })}>+3</button>
</div>
)
}

components/Child2.ts

1
2
3
4
5
6
7
8
9
10
11
12
import { useContext } from 'react'
import GlobalContext from '../context'

export default function Child1() {
const { count, dispatchCount } = useContext(GlobalContext) as any
return (
<div>
<h3>Child2: {count}</h3>
<button onClick={() => dispatchCount({ type: 'INCREMENT', payload: 3 })}>+3</button>
</div>
)
}

forwardRef

父组件如何获取子函数组件中的 DOM。

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRef } from 'react'
import Test from './Test'

export default function App() {
// #1
const childInputRef = useRef<HTMLInputElement>(null)
const handleClick = () => {
childInputRef.current?.focus()
}
return (
<div>
父组件
<button onClick={handleClick}>聚焦</button>
<hr />
<Test ref={childInputRef} />
</div>
)
}

Test.tsx

1
2
3
4
5
6
7
8
9
10
11
12
import { forwardRef } from 'react'

// #2
const Test = forwardRef<HTMLInputElement>((props, ref) => {
return (
<div>
<input type='text' ref={ref} />
</div>
)
})

export default Test

useImperativeHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { forwardRef, useImperativeHandle, useRef } from 'react'

const Test = forwardRef((props, ref) => {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
// 拦截住,使用自己在 ref~~
inputRef.current.focus()
},
}))
return (
<div>
<input ref={inputRef} />
</div>
)
})
export default Test

useLayoutEffect

用法和 useEffect 一致,与 useEffect 的差别是执行时机,useLayoutEffect 是在浏览器绘制节点之前执行,会阻塞 DOM 的更新!

Slow 3G 环境下测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState, useEffect, useLayoutEffect } from 'react'

export default function App() {
const [msg, setMsg] = useState('Hello')

// 渲染完之后异步执行,应该先看到 'Hello',然后再出现 'World',存在 “闪烁” 现象
/* useEffect(() => {
let i = 0
while (i <= 1000000000) {
i++
}
setMsg('World')
}, []) */
// 渲染之前同步执行,会阻塞,所以只会看到 'World',不会 “闪烁”
useLayoutEffect(() => {
let i = 0
while (i <= 1000000000) {
i++
}
setMsg('World')
}, [])
return (
<div className='App'>
<h1>{msg}</h1>
</div>
)
}
  • 优先使用 useEffect,因为它是异步执行的,不会阻塞渲染。

  • 会影响到渲染的操作尽量放到 useLayoutEffect 中去,避免出现闪烁问题。

  • useLayoutEffect 和 componentDidMount 是等价的,会同步调用,阻塞渲染。

  • useLayoutEffect 在服务端渲染的时候使用会有一个 warning,因为它可能导致首屏实际内容和服务端渲染出来的内容不一致。

getDerivedStateFromProps

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react'
import Count from './Count'

const App = () => {
return (
<div>
<Count count={888} />
</div>
)
}

export default App

Count.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react'
export default class Count extends React.Component {
constructor(props) {
super(props)
console.log('#1 constructor')
this.state = {
count: 0,
}
}
addCount = () => {
// !改不了了
this.setState({ count: this.state.count + 1 })
}
static getDerivedStateFromProps(props, state) {
console.log('#2 getDerivedStateFromProps')
// 根据 props 得到一个派生的状态,应用场景:state 在任何时候都取决于 props 可以使用此钩子
return props
}
render() {
console.log('#3 render')
const { count } = this.state
return (
<div>
<h2>{count}</h2>
<button onClick={this.addCount}>+1</button>
</div>
)
}
componentDidMount() {
console.log('#4 componentDidMount')
}
}

getSnapshotBeforeUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from 'react'
export default class App extends React.Component {
state = { count: 0 }
addCount = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
console.log('#1 render')
const { count } = this.state
return (
<div>
<h2>{count}</h2>
<button onClick={this.addCount}>+1</button>
</div>
)
}
// 能捕获到数据更新到 DOM 之前的一些信息(例如滚动位置),此生命周期的任何返回值将作为 componentDidUpdate 的第 3 个参数
getSnapshotBeforeUpdate() {
console.log('#2 getSnapshotBeforeUpdate')
// 任何值都可以作为 Snapshot(快照)
// return null
return 'xxx'
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevProps, prevState, snapshot)
console.log('#3 componentDidUpdate')
}
}

flushSync

改变了数据,想操作 DOM,App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState } from 'react'
import { flushSync } from 'react-dom'

function App() {
const [count, setCount] = useState(0)

const handleClick = () => {
flushSync(() => {
setCount(count + 1)
})
console.log(document.querySelector('p').innerHTML)
}
return (
<div className='App'>
<p>{count}</p>
<button onClick={handleClick}>+1</button>
</div>
)
}

export default App