0%

React组件化开发

组件化开发(React二)

组件化开发:如果把一个页面的所有处理逻辑放在一起,处理起来会很复杂,因此将一个页面拆分为不同的功能块

  • 一个完整的页面可以划分为很多个组件
  • 每个组件都用于实现页面的一个功能
  • 每一个组件又可以细分
  • 组件本身又可以在很多地方复用

image-20230324172119865

React组件:

  • 类组件与函数组件
  • 根据内部是否有状态需要维护:无状态组件、有状态组件(this.state,不考虑hooks的情况下函数组件是无状态组件,而类组件不定义this.state也可以看做无状态组件)
  • 根据组件的不同职责:展示型组件、容器型组件

组件之间有重叠,但都是为了数据逻辑和UI展示的分离

  • 函数组件、无状态组件、展示型组件主要关注UI的展示
  • 类组件、有状态组件、容器型组件主要关注数据逻辑

类组件与函数组件

类组件

定义要求:首字母大写、继承自React.Component、实现render函数

  • constructor可选,在其中初始化数据
  • this.state中维护组件内部的数据
  • render是类组件唯一必须实现的方法

函数组件(没有hooks的函数组件)

  • 没有生命周期、会被挂载、没有生命周期函数
  • this关键字不能指向组件实例,因为没有组件实例
  • 没有内部状态state
1
2
3
4
5
6
7
function App(){
return (
<div>Hello World</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)

生命周期

生命周期:从创建到销毁的整个过程,了解生命周期在最合适的地方完成想要的功能

React的生命周期、生命周期函数

  • 装载阶段:组件第一次在DOM树种被渲染的过程——装载完成:componentDidMount
  • 更新阶段:组件状态发生变化,重新更新渲染的过程——更新完成:componentDidUpdate
  • 卸载阶段:组件从DOM树种被移除的过程——即将移除:componentWillUnmount

image-20230324173626696

各函数任务及时刻

constructor:

  • 不初始化state或不进行方法绑定,则可以不实现构造函数
  • 作用一:给this.state赋值对象来初始化state
  • 作用二:为事件绑定this——调用super()

componentDidMount:

  • 组件挂载到DOM Tree后立马调用
  • 作用一:进行依赖于DOM的操作
  • 作用二:发送网络请求的最好位置
  • 作用三:添加订阅

componentDidUpdate:

  • 组件更新后立即调用,首次渲染不会执行这个方法
  • 作用一:组件更新后,此处进行DOM相关操作
  • 作用二:当更新前后props发送了变化,此处发送网络请求

componentWIllUnmount:

  • 组件卸载及销毁之前直接调用
  • 作用一:进行必要的清理操作
  • 作用二:清除timer,取消网络请求或取消上面创建的订阅

不常用生命周期函数

  • getDerivedStateFromProps:state 的值在任何 时候都依赖于 props时使用;该方法返回一个对象 来更新state
  • getSnapshotBeforeUpdate:在React更新DOM 之前回调的一个函数,可以获取DOM更新前的一 些信息(比如说滚动位置)
  • shouldComponentUpdate:该生命周期函数很 常用,但是我们等待讲性能优化时再来详细讲解

image-20230324174415395

组件之间的通信

父组件传值到子组件

  • 父组件通过 属性=值 的形式来传递给子组件数据
  • 子组件通过props参数获取父组件传过来的值

默认值设置:ChildComponent.defaultProps

参数类型设置:ChildComponent.propTypes isRequired代表是否必须

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state = {
message: "I am your father!",
info:"You are my child!"
}
}
render(){
let {message,info} = this.state
return (
<div>
<h2>{message}</h2>
<ChildComponent info = {info}/>
</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react'
import { PropTypes } from 'prop-types'
export class ChildComponent extends Component {

render() {
return ([
<div key="1">{this.props.info}</div>,
<div key="2">{this.props.info2}</div>
])
}
}
ChildComponent.propTypes = {
info: PropTypes.string.isRequired
}
ChildComponent.defaultProps = {
info:"Tell me who are you!",
info2:"You are my Dad!"
}
export default ChildComponent

image-20230324180859034

子组件传值到父组件

  • 通过props给子组件传递一个回调函数,在子组件中调用这个函数

父组件代码:

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";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state = {
message: "I am your father!",
info:"You are my child!"
}
}
dataFromChild = function(info){
this.setState({
message:info
})
}
render(){
let {message,info} = this.state
return (
<div>
<h2>{message}</h2>
<ChildComponent info = {info} dataFromChild = {(message)=> this.dataFromChild(message)}/>
</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import { PropTypes } from 'prop-types'
export class ChildComponent extends Component {
sendDataToFather = function(){
this.props.dataFromChild(555)
}
render() {
return ([
<div key="1" onClick = {()=>{this.sendDataToFather()}}>{this.props.info}</div>,
<div key="2">{this.props.info2}</div>
])
}
}
ChildComponent.propTypes = {
info: PropTypes.string.isRequired
}
ChildComponent.defaultProps = {
info:"Tell me who are you!",
info2:"You are my Dad!"
}
export default ChildComponent

注:各个地方函数的使用要注意,是否需要this,一般传递过去的函数都是箭头函数,调用的函数一般为function

1
2
<ChildComponent info = {info} dataFromChild = {(message)=> this.dataFromChild(message)}/>
<ChildComponent info = {info} dataFromChild = {this.dataFromChild}/>

如果直接传递,那么函数的调用由React确定就不知道this是啥

传递过去箭头函数,那么我们不关心什么时候调用这个箭头函数,而箭头函数包含的是隐式绑定的this,所以调用的函数需要function,要不然找不到this

React中实现插槽

为了使组件具有更好的通用性,组件的内容不能限制为固定的div、span等,因此父组件传元素到子组件中,使得元素类型可以多变

image-20230324214907031

通过组件的children子元素实现(父像子传元素)

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'
export class ChildComponent extends Component {
render() {
const {children} = this.props
return (
<div>
<div>
{children[0]}
</div>
<div>
{children[1]}
</div>
<div>
{children[2]}
</div>
</div>
)
}
}
export default ChildComponent

父组件代码1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<ChildComponent>
<button>按钮</button>
<h2>标题</h2>
<span>span</span>
</ChildComponent>
)
}
}
export default App

结果:

image-20230324215146004

更改父组件的子元素

1
2
3
4
5
<ChildComponent>
<span>按钮</span>
<i>标题</i>
<button>按钮</button>
</ChildComponent>

结果:

image-20230324215351358

通过props实现插槽(父向子传元素)

  • 上面描述过通过props传值到子元素,props可以传的值不仅仅有JavaScript对象,React对象也可以作为参数传过去
  • 可以避免通过children获取时索引不能精准的获取传入的元素

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
const spanJSX = <span>按钮</span>
const iJSX = <i>标题</i>
const buttonJSX = <button>按钮</button>
return (
<ChildComponent spanJSX={spanJSX} iJSX = {iJSX} buttonJSX={buttonJSX}/>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'
export class ChildComponent extends Component {
render() {
const {spanJSX,iJSX,buttonJSX} = this.props
return (
<div>
<div>{spanJSX}</div>
<div>{iJSX}</div>
<div>{buttonJSX}</div>
</div>
)
}
}
export default ChildComponent

结果:

image-20230324220143962

作用域插槽

  • 上面两种插槽方法子组件接受到的都是有父组件写死的元素,但某些时候在子组件中需要根据状态更改组件的内容
  • 所以需要实现元素类型父组件决定,数据或状态子组件决定
  • 类似于这种由父组件子组件共同协作完成的,通常使用传递函数实现

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<ChildComponent itemtyPE = {item=><button>{item}</button>}/>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react'
export class ChildComponent extends Component {
render() {
const {itemtyPE} = this.props
const message = "哈哈哈"
return (
<div>
{itemtyPE(message)}
</div>
)
}
}
export default ChildComponent

image-20230325112140472

非直接父子组件的通信

常见方法:

  • 通过props属性自上而下传递
  • 但是对于某些数据需要在多个组件中共享(如地区、用户信息等等)
  • 如果在顶层App定义这些信息,一层层传递下去,对于一些中间层不需要这些数据的组件来说,是一种冗余的操作

Context方法:

  • context提供了一种在组件之间共享值的方法,而不必显示的通过树的逐层传递props
  • context的目的是共享对于组件树来说是“全局的数据”,如当前用户、主题

注册Context

由于后续会在多个组件中用到这个Context1,所以将Context1的注册放到一个js文件中

1
2
3
4
5
import React from "react";

const Context1 = React.createContext()

export default Context1

Context值的设置

父组件设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
// 通过COntext1.Provider 包围要传递值的子组件,那么这个子组件及其后代组件都可以获取到传递的值
return (
<Context1.Provider value={{color:"red",size:18}}>
<ChildComponent/>
</Context1.Provider>
)
}
}
export default App

Context值的获取

后代类组件Context值的获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react'
import Context1 from '../context/Context1'
import GrandsonComponent from './GrandsonComponent'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.context.color}}>子组件</h2>
<GrandsonComponent/>
</div>
)
}
}
ChildComponent.contextType = Context1 // 通过设置contextType为this.context指定Context,然后通过this.context获取值
export default ChildComponent

后代函数组件Context值获取:

由于函数组件中没有this,所以不能够使用上面类组件获取值的方法,使用Context.Consumer获取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Context1 from "../context/Context1"

export function GrandsonFunComponent(){
return(
<Context1.Consumer>
{
value=>{
return <h2 style={{color:value.color}}>函数孙子组件</h2>
}
}
</Context1.Consumer>
)
}
export default GrandsonFunComponent

多个Context的注册与获取

注册第二个Context:

1
2
3
4
5
import React from "react";

const Context2 = React.createContext()

export default Context2

单组件设置多Context:

父组件采用Context嵌套的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
import Context2 from "./context/Context2";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<Context2.Provider value={{color:"blue"}}>
<Context1.Provider value={{color:"red",size:18}}>
<ChildComponent/>
</Context1.Provider>
</Context2.Provider>
)
}
}
export default App

后代组件获取多Context:

通过设置contextType与Context.Consumer组合获取多个Context

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, { Component } from 'react'
import Context1 from '../context/Context1'
import Context2 from '../context/Context2'
import GrandsonComponent from './GrandsonComponent'
import GrandsonFunComponent from './GrandsonFunComponent'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.context.color}}>子组件</h2>
<Context2.Consumer>
{
value=>{
return <h3 style={{color:value.color}}>子组件2</h3>
}
}
</Context2.Consumer>
<GrandsonComponent/>
<GrandsonFunComponent/>
</div>
)
}
}
ChildComponent.contextType = Context1
export default ChildComponent

Context默认值的使用

使用了Context但不是Context.Provider的后代元素

Context的注册(带默认值):

1
2
3
4
5
import React from "react";

const Context1 = React.createContext({color:"green"})

export default Context1

Context的设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
import Context2 from "./context/Context2";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<div>
<Context2.Provider value={{color:"blue"}}>
<Context1.Provider value={{color:"red",size:18}}>
<ChildComponent/>
</Context1.Provider>
</Context2.Provider>
<ChildComponent/>
</div>
)
}
}
export default App

Context的获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import Context1 from '../context/Context1'
import Context2 from '../context/Context2'
import GrandsonComponent from './GrandsonComponent'
import GrandsonFunComponent from './GrandsonFunComponent'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.context.color}}>子组件</h2>
<GrandsonComponent/>
<GrandsonFunComponent/>
</div>
)
}
}
ChildComponent.contextType = Context1
export default ChildComponent

image-20230325122726808

任意组件之间的通信——事件总线(EventBus)

事件总线实现

setState的细节原理

为什么要setState:React没有数据劫持,直接更改this.state时,React不知道数据已经发生了变化,需要通过this.setState()来告知React数据发生了变化,需要刷新页面

setState的异步/同步

一般情况下,setSTate是异步的

设置为异步的原因:

  • 可以显著提升性能,如果每次调用setState都进行一次更新,那么render会频繁的调用,界面重新渲染,这样效率很低(可以联系到回流-重绘),因此设置为异步获取到数据的多次更新,之后批量更新
  • 如果同步更新了state,但还没执行render函数,那么state与子组件的props不能保持同步,可能出现若干问题

setState批量更新方法

  • 获取到一系列setState的更新值,按先后顺序把旧的state与新的state合并(添加新的属性、更新改变了的属性),在最后将合并好的state赋值给this.state并刷新到页面上(SCU优化)
  • 注意:先合并再赋值给this.state

异步结果的获取:

既然是异步,那么我们便不知道更新啥时候结束,但有些时候又想在更新结束后进行一定的操作,这个操作应该在哪里进行?

方法一:setState的回调函数

  • setState接受两个参数:新的state+回调函数
  • 回调执行顺序:render->componentDidUpdate(因此也可以在这个生命周期函数中对更新结束后的值操作)->setState回调
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
39
40
41
import React from "react";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
counter:3
}
}
buttonClick(){
this.setState({
counter:this.state.counter + 1
},()=>{
console.log("setState回调",this.state)
})
this.setState({
counter:this.state.counter + 1
},()=>{
console.log("setState回调",this.state)
})
this.setState({
counter:this.state.counter + 1
},()=>{
console.log("setState回调",this.state)
})
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
render(){
console.log("render")
const { counter } = this.state
return (
<div>
<h2>{counter}</h2>
<button onClick={()=>{this.buttonClick()}}>更新state</button>
</div>
)
}
}
export default App

image-20230327111305434

注:

  • 多次setState合并,一次render,一次componentDidUpdate,多次回调
  • 这里多次一个提交了3次+1,但是最后结果只加了1,是因为this.state会在多次提交的state与原始state合并结束后再赋值给this.state,也就是这里获取到的this.state每次都是初始值3,相当于提交了3次 counter=4

this.setState的第一个参数也可以是一个函数:

  • 返回值是要更改的值
  • 参数是已经合并后的state而不是this.state
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
39
40
41
42
43
44
45
46
47
48
49
50
import React from "react";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
counter:3
}
}
buttonClick(){
setTimeout(()=>{
console.log("setTimeOut宏任务")
},0)
new Promise((resolve)=>{
resolve("")
}).then((res)=>{
console.log("promise微任务")
})
this.setState((state)=>{
console.log("setState",state)
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
render(){
console.log("render")
const { counter } = this.state
return (
<div>
<h2>{counter}</h2>
<button onClick={()=>{this.buttonClick()}}>更新state</button>
</div>
)
}
}
export default App

执行顺序为:this.setState第一个参数函数->render->componentDidUpdate->this回调

image-20230327112819884

注:

  • 多次setState合并,一次render,一次componentDidUpdate,多次回调
  • setState函数执行、render、componentDidUpdate执行都是在微任务后,宏任务前,因此前面的函数应该都是放在微任务队列中
  • 提交3次+1,最后得到的结果也和预期相同,因为将3次setState的第一个参数函数放入微任务队列中,在原来的state基础上进行更改,然后合并,得到新的state,在调用下一个微任务setState时,用前面合并好的state作为参数,再进行更改与合并操作
  • 虽然setState中是在微任务中分3次调用,但setState的回调是在完成componentDidUpdate再放入微队列中,此时this.state已经更改为最后值,所以获取到的都是最后结果

回调的好处:

  • 在数据更改后处理一些state的逻辑
  • 回调函数会把之前的state和props传入进来
  • 由于setState是一个异步函数,所以对更新后值的处理要放入回调中

方法二:组件的生命周期函数componentDidUpdate

上面已讲

setState的同步

  • React18之前,在组件生命周期和React合成事件中,setState是异步
  • React18之前,在setTimeout或者原生dom事件中,setState是同步

注:

  • 由React去决定运行时机的大多是异步
  • 由setTimeout/或者原生Dom的响应函数中调用这种用户编写决定运行时机的一般是同步

React18之后,setState默认都是异步的

React18之后获取同步的setState

使用特殊的flushSync操作

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import React from "react";
import {flushSync} from "react-dom"
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
counter:3
}
}
buttonClick(){
setTimeout(()=>{
console.log("setTimeOut宏任务")
},0)
new Promise((resolve)=>{
resolve("")
}).then((res)=>{
console.log("promise微任务")
})

flushSync(()=>{
this.setState((state)=>{
console.log("setState",state)
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
render(){
console.log("render")
const { counter } = this.state
return (
<div>
<h2>{counter}</h2>
<button onClick={()=>{this.buttonClick()}}>更新state</button>
</div>
)
}
}
export default App

image-20230327114712050

可以看到使用flushSync中的setState在微任务执行之前就已经运行,说明flushSync确实将setState更改为同步方式运行

React的SCU优化

React更新机制

React渲染流程:

image-20230327115317127

React更新流程:

image-20230327120054249

React在props或state发生改变时,会调用React的render方法,会创建一颗新树,React会基于新树与旧树的不同去判断如何有效的更新UI

  • 如果两棵树进行完全比较更新,算法的时间复杂度为O(n^3),n为树中元素的数量,开销太大,React的更新性能会变得低效

React对diff算法的优化:

  • 同层节点之间相互比较,不会跨节点比较
  • 不同类型的节点,产生不同的树结构
  • 开发中,通过key来指定哪些节点在不同的渲染下保持稳定

keys的优化:

  • 在最后位置插入数据,有无key意义不大
  • 在前面插入数据,没有key,所有的li都需要更改,有key,后续的li只需要移动

key的注意事项:

  • key应该是唯一的
  • key不能使用随机数,每次render的随机数不同
  • 不能使用index作为key,index在操作后也可能会改变,要使用一个不会随render改变的值

shouldComponentUpdate优化(即SCU优化)

image-20230327121337396

  • 在上图中,如果App中值发生了改变,那么App的render函数会重新执行,而在App的执行过程中,会执行其所有子组件的render函数,都进行diff算法,性能很低
  • 子组件调用render函数应该有一个前提——依赖的数据state/props发生改变,此时才调用render函数

shouldComponentUpdate函数

  • 参数:nextProps(修改后的pros)、nextState(修改之后的state)
  • 返回值:true(调用render方法)、false(不调用render方法)、默认返回true(只要state改变就会调用render方法)

在值发生变化时,就可以在shouldComponentUpdate监听自己的state和props是否发生变化从而觉得是否进行render

类组件PureComponent

继承自PureComponent自动实现了SCU优化,不需要自己去编写shouldComponentUpdate函数

如果使用Component,只要调用setState就会执行render函数,而PureComponent会帮我们实现比较setState后state是否改变,如果改变才执行render

不可变数据的力量

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
39
40
41
42
43
import React from "react";
import {flushSync} from "react-dom"
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
books:[
{name:"aaa",count:1},
{name:"bbb",count:1},
{name:"ccc",count:1},
{name:"ddd",count:1}
]
}
}
addNewBook(){
const newBook = {name:"eee",count:1}
this.setState({

})
}

render(){
console.log("render")
const { books } = this.state
return (
<div>
<ul>
{books.map((item,index)=>{
return(
<li key={index}>
<span>name:{item.name}-count:{item.count}</span>
<button>+1</button>
</li>
)
})}
</ul>
<button onClick={e=>this.addNewBook()}>添加新书</button>
</div>
)
}
}
export default App

上述在setState中没进行任何操作,但render依然会执行,而改成PureComponent时不会执行

PureComponent中render执行要求:

  • PureComponent在比较时底层会使用shallowEqual方法,shallowEqual是一种浅层比较,如果比较的两个对象是同一个,则不会调用render,即使this.state发生了改变,也不会调用render
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
constructor(){
super()
this.state ={
books:[
{name:"aaa",count:1},
{name:"bbb",count:1},
{name:"ccc",count:1},
{name:"ddd",count:1}
]
}
}
addNewBook(){
const newBook = {name:"eee",count:1}
this.state.books.push(newBook)
const books = this.state.books
this.setState({
books:books
})
}
  • 如果要想在这种情况下调用render,可以利用books创建一个新的对象,用这个对象去更改state,在这种情况下,即使未更改this.state的,也会调用render
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
constructor(){
super()
this.state ={
books:[
{name:"aaa",count:1},
{name:"bbb",count:1},
{name:"ccc",count:1},
{name:"ddd",count:1}
]
}
}
addNewBook(){
const books = [...this.state.books]
this.setState({
books:books
})
}

函数组件memo

函数由于不能继承于PureComponent,可以使用memo替代,即用memo包裹原来的函数组件

image-20230327131630892

获取原生DOM或获取组件

在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:

  • 管理焦点,文本选择或媒体播放;
  • 触发强制动画;
  • 集成第三方 DOM 库;
  • 我们可以通过refs获取DOM;

通过refs获取DOM的方式:

  • 传入字符串:使用时通过 this.refs.传入的字符串 格式获取对应的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
addNewBook(){
console.log(this.refs.button)
}

render(){
return (
<div>
<button onClick={e=>this.addNewBook()} ref="button">查看DOM</button>
</div>
)
}
}
export default App
  • 传入一个对象:通过React.createRef()方式创建一个对象,使用使用对象的current属性来访问DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.titleRef = React.createRef()
}
addNewBook(){
console.log(this.titleRef.current)

}

render(){
return (
<div>
<button onClick={e=>this.addNewBook()} ref={this.titleRef}>查看DOM</button>
</div>
)
}
}
export default App
  • 传入一个函数:该函数会在DOM被挂载时回调,回调函数会传入一个元素对象,我们可以访问并保存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.titleRef = React.createRef()
}
render(){
return (
<div>
<button ref={(e)=>{console.log(e)}}>查看DOM</button>
</div>
)
}
}
export default App

通过refs获取组件的方式:

针对类组件:

  • 上面的ref同样可以加在子组件上,获取得到子组件的实例,就可以在父组件中获取子组件对子组件的函数、变量进行操作

针对函数组件:

  • 函数组件由于没有实例,是不可能获取到它的
  • 但是在某些情况,想获取得到函数实例中的某个DOM元素,可以使用foreardRef
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
import React from "react";
import { forwardRef } from "react";
//编写组件

const HelloWorld = forwardRef(function(props, ref){
return (
<div>
<h1 ref={ref}>Hello World</h1>
</div>
)
})
class App extends React.PureComponent{
constructor(){
super()
this.HelloWorldRef = React.createRef()
}
render(){
return (
<div>
<HelloWorld ref = {this.HelloWorldRef}/>
<button onClick={()=>{console.log(this.HelloWorldRef.current)}}>查看DOM</button>
</div>
)
}
}
export default App

受控组件与非受控组件

  • HTML中,表单元素(input、textarea、select)通常会自己维护state并根据用户的输入进行更新
  • React中,可变状态通常保存在组件的state属性中,并且只能通过setState来更新
  • 在JSX中,如果给表单元素设置了value属性,显示this.state.value的值,那么表单元素就不会自己维护state,其state来源于React的state,因此当设置了value属性后,input元素输入后输入框里的东西不会变化,相当于未输入
  • 此时只能给input元素添加onChange(e)监听,从而在监听函数中使用输入的值在setState中更改value的值,使得页面刷新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from "react";
import { forwardRef } from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
username:"oww"
}
}
inputChange(e){
this.setState({username:e.target.value})
}
render(){
const {username} = this.state
return (
<div>
<input type="text" name="username" value={username} onChange={(e)=>{this.inputChange(e)}}/>
</div>
)
}
}
export default App

计算属性名

当对多个表单元素变成受控元素时,其更新需要多个监听,此时可用计算属性名将这些监听抽取到一个函数中

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 React from "react";
import { forwardRef } from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
username:"oww",
password:""
}
}
inputChange(e){
this.setState({[e.target.name]:e.target.value})
}
render(){
const {username,password} = this.state
return (
<div>
<input type="text" value={username} name="username" onChange={(e)=>{this.inputChange(e)}}/>
<input type="text" value={password} name="password" onChange={(e)=>{this.inputChange(e)}}/>
</div>
)
}
}
export default App

checkbox表单多选监听

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
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
hobbies:[
{id:"sing",text:"唱",checked:false},
{id:"jump",text:"跳",checked:false},
{id:"rap",text:"rap",checked:false}
]
}
}
inputChange(e,index){
const hobbies = [...this.state.hobbies]
hobbies[index].checked = e.target.checked
this.setState({
hobbies:hobbies
})
}
render(){
const {hobbies} = this.state
return (
<div>
<h2>爱好列表</h2>
{hobbies.map((item, index)=>{
return (
<label htmlFor={item.id} key={item.id}>
<input type="checkbox" id= {item.id} checked={item.checked} onChange={(e)=>this.inputChange(e,index)}/>{item.text}
</label>
)
})}
</div>
)
}
}
export default App

select多选

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
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
fruits:[
{id:"apple",text:"苹果"},
{id:"banana",text:"香蕉"},
{id:"orange",text:"橘子"}
],
checked:["apple"]
}
}
selectChange(e){
let checked = Array.from(e.target.selectedOptions).map(item=>item.value)
this.setState({
checked:checked
})
}
render(){
const {fruits,checked} = this.state
return (
<div>
<h2>爱好列表</h2>
<select multiple value={checked} onChange={e=>this.selectChange(e)}>
{fruits.map(item=>{
return <option value={item.id} key={item.id}>{item.text}</option>
})}
</select>
</div>
)
}
}
export default App

高阶组件

高阶函数:满足下列一个条件

  • 接受一个函数或多个函数作为输入
  • 输出一个函数

高阶组件(Higher-Order Components、HOC):

  • 高阶组件的参数为组件,返回值为新组件的函数
  • 高阶组件本身不是一个组件,而是一个函数

高阶组件不是React的API,而是基于React组合特性而形成的一种设计模式

高阶组件应用——props的增强

HOC代码:高阶函数包裹代码

例如本代码可以将用户信息封装传给子组件,不需要像上面的父向子传值,在子组件中写其他代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { PureComponent } from "react"

function enhanceProps(WrapperCpn, otherProps){
class UserComponent extends PureComponent{
constructor(){
super()
this.state = {
userinfo:{
username:"ouwenwu",
age:22
}
}
}
render(){
return <WrapperCpn {...this.state.userinfo} {...otherProps}/>
}
}
return UserComponent
}
export default enhanceProps

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ChildComponent from "./component/ChildComponent";
import enhanceProps from "./HOC/HOC";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
render(){
const EnhancePropsCpn = enhanceProps(ChildComponent,{color:"blue"})
return (
<div>
<EnhancePropsCpn/>
</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.props.color}}>username:{this.props.username}</h2>
</div>
)
}
}
export default ChildComponent

结果:

image-20230327211035646

高阶组件应用——Context的共享

HOC代码:HOC可以返回类组件也可以返回函数组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { PureComponent } from "react"
import Context1 from "../context/Context1"

function enhanceProps(WrapperCpn){
return props=>{
return (
<Context1.Consumer>
{
value=>{
return <WrapperCpn {...value} {...props}/>
}
}
</Context1.Consumer>
)
}
}
export default enhanceProps

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
import enhanceProps from "./HOC/HOC";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
render(){
const EnhancePropsCpn = enhanceProps(ChildComponent)
return (
<div>
<Context1.Provider value={{color:"red"}}>
<EnhancePropsCpn/>
</Context1.Provider>

</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.props.color}}>username:</h2>
</div>
)
}
}
export default ChildComponent

结果:

image-20230327212419080

注:可以看出对Context不再需要在每一个子组件中写之前提到的获取Context的方法,只需要将所有需要该Context的子组件包裹在高阶组件之中

高阶组件应用——渲染判断定权

与上面的主要差距为,会对传入的参数进行判断来决定返回哪个子组件

image-20230327212646566

高阶组件应用——生命周期劫持

在高阶组件中劫持子组件的生命周期,在自己的生命周期中完成逻辑

image-20230327212743063

总结

高阶组件的主要作用是提高代码的复用性。提高组件之间的复用

缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将产生非常多的嵌套
  • HOC可以劫持props,在不遵从约定的情况下可能造成冲突