抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

React-类组件精简笔记

1. React概述

1.1 概述

React是一个前端开源的用于构建web用户界面的Js库。

1.2 文档

官方文档(英文)官方文档(中文)官方文档(新版、开发中、英文)

1.3 背景

前背景是Facebook内部框架,后背景是Facebook维护,使用者贡献代码,于2013年开源。

2. 创建React项目

使用官方提供的create-react-app(React脚手架)创建react项目

使用npm:(推荐)

1
2
3
4
5
6
7
8
# 1. 全局安装 react 官方提供的脚手架工具 create-react-app (CRA) [推荐]
npm install create-react-app@5.0.1 -g
# 2. 使用 CRA 创建项目,项目名称为 react-basic [默认]
create-react-app react-basic
# 2.1 npm创建react-app
npm init react-app react-basic
# 3. 启动项目
npm run start

使用yarn:

1
2
3
4
5
6
# 1. yarn全局安装 react 官方脚手架
yarn global add create-react-app
# 2. yarn创建react-app
yarn create react-app react-basic
# 3. yarn 启动项目
yarn start

下载安装网络有问题的建议换源,如何换源参考:Node开发相关

项目目录文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 项目结构分析
├── README.md # 项目的说明书
├── package.json # npm 包说明文件、记录项目信息
├── package-lock.json # 跟踪被安装的每个软件包的确切版本
├── public # 本地开发服务器提供的静态资源目录
│   ├── favicon.ico # 网站图标、显示在浏览器的标签栏中
│   ├── index.html # 项目的 HTML 模板
│   ├── logo192.png # react logo 图片 (示例代码中用于设置 IOS 移动端网站图标)
│   ├── logo512.png # react logo 图片
│   ├── manifest.json # web 应用清单如名称, 作者, 图标和描述 (主要用于将 Web 应用程序安装到设备的主屏幕)
│   └── robots.txt # 爬虫协议文件
└── src # 项目源码目录
├── App.css # 示例程序中的根组件样式文件
├── App.js # 示例程序中的根组件文件
├── App.test.js # 示例程序中的根组件测试文件
├── index.css # 示例程序中的全局样式文件
├── index.js # 项目的入口文件
├── logo.svg # 示例程序中根组件中显示的网站图标文件
├── reportWebVitals.js # 测试应用程序的性能
└── setupTests.js # 项目的测试文件

安装编辑器拓展,给项目开发提供支持

Vscode编辑器拓展:VSCODE扩展-ES7+ React/Redux/React-Native snippets

3. React初体验

项目入口文件为index.js文件,接下来以此文件进行初体验

1
2
3
4
5
<!-- public/index.html -->
<!-- 将创建好的 h1 元素渲染到 id 为 root 的 div 中 -->
<div id="root">
<h1 title="Hello React" id="title">Hello React</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* src/index.js */
// react 核心库,包含了构建 web 应用和移动端应用到通用方法
import React from "react";
// react-dom:只包含了构建 web 应用到方法
import ReactDOM from "react-dom/client";

// React.createElement(type, props, children): React 提供的创建元素的方式
const title = React.createElement(
"h1",
{ id: "title", title: "Hello React" },
"Hello React"
)
// 创建 React 应用的根结点
const root = ReactDOM.createRoot(document.getElementById("root"));
// 将h1标签渲染到根节点中:
root.render(title);

React.createElement(type, props, children)

React提供的创建元素的方法

  • type: 标签名称, 字符串类型.

  • props: 元素属性, 对象类型, 无属性填入 null

  • children: 子元素, 普通文本或 createElement 方法返回的元素对象,也可也是react元素数组

  • return: 元素对象

ReactDOM.createRoot(dom)

React提供的构建web应用的创建根结点的方法

dom:webapi获取的DOM元素(document.querySelecter)

return:react根结点元素对象

根结点对象的render方法

根结点对象里的render方法是用于渲染其他虚拟DOM的,相当于webapi中的document.body.appendChild,render方法的参数可以是react的dom对象,也可以是react-dom对象数组,也可也是jsx内容。

1
2
3
4
5
6
7
8
9
10
11
import React from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
// 传递数组会自动展开数组的里的内容进行渲染
[
// react的dom对象
React.createElement("h1",{},"hello"),
// jsx
<p>我是一个段落</p>
])

tips:React通用方法放在React中,web应用的方法放在react-dom包中,移动端应用方法放在react-native-web包中。

4. JSX—JavaScript语法拓展

4.1 JSX概述

JSX语法是另一种在 React 中创建元素的方式,使用它创建元素更简单、直接。

JSX 是 JavaScript XML 的简写,可以理解为在 JavaScript 中书写 XML 格式的代码(XML跨平台)。

1
2
3
4
5
6
7
8
9
const list = (
<div className="list">
<h2>动物</h2>
<ul>
<li>修够</li>
<li>cat</li>
</ul>
</div>
)

JSX不支持在浏览器中直接运行,仅支持在开发时将JSX编译成React项目代码:

image-20230104094156546

问题一:既然 React.createElement 和 JSX 都可以创建元素,那么它们之间有什么关系吗?Babel REPL

虽然我们在编写代码时使用的是 JSX,但是在代码运行之前 babel 会将其转换为 React.createElement 方法的形式,所以它们之间是转换的关系。

image-20230104094337470

问题二:为什么要将 JSX 转换为 React.createElement 方法?

JSX 是 Facebook 为是 JavaScript 添加的语法扩展,它并不是标准的 JavaScript 语法不能直接在浏览器中使用。

4.2 JSX注意事项

(1) JSX 格式美化

如果 JSX 中存在多个标记建议使用小括号进行包裹,否则标签格式看起来不对齐

1
2
3
4
// 美化前
const jsx = <div>
<ul></ul>
</div>
1
2
3
4
5
6
// 美化后
const jsx = (
<div>
<ul></ul>
</div>
)

(2) 在使用 JSX 语法创建元素时,元素的最外层必须要有一个根标记。

1
2
3
4
5
// 错误写法
const jsx = (
<p>Hello</p>
<p>world</p>
)
1
2
3
4
5
6
7
// 正确写法
const jsx = (
<div>
<p>Hello</p>
<p>world</p>
</div>
)

为避免因为要满足规定而出现无意义标记,React 提供了幽灵标记,幽灵标记在渲染后不会产生真实的 DOM 对象。

1
2
3
4
5
6
7
// 幽灵标记的简写语法
const jsx = (
<>
<p>Hello</p>
<p>world</p>
</>
)
1
2
3
4
5
6
7
// 幽灵标记的完整语法
const jsx = (
<React.Fragment>
<p>Hello</p>
<p>world</p>
</React.Fragment>
)

幽灵标记可以使用在遍历的时候加key属性使用

(3) 在 JSX 中使用单标记时单标记必须是闭合状态。

1
2
<input type="text"/>
<img src="" alt="" />

(4) 在JSX 中标记属性使用小驼峰式命名法。

小驼峰式命名法是指如果一个标识符由多个单词组成,第一个个单词的首字母小写、其他单词的首字母大写。

1
<input maxLength="10" readOnly autoFocus />

(5) 在 JSX 中为元素添加属性时使用 className 替代 class、使用 htmlFor 替代 for

因为 JSX 本质上仍然是 JavaScript,而 class、for 是 JavaScript 中的关键字不能直接使用。

1
2
<input type="text" className="todos" />
<label htmlFor="demo"></label>

4.3 JSX嵌入表达式

在JSX中插入JavaScript的数据需要使用花括号{},并且花括号内只能是表达式的结果

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 插入文本内容
const name = '张三';
const jsx = <div>hello, {name}</div>
// 插入属性值
const activeName = "active"
const jsx = <input type="text" className={activeName} />
// 执行计算
const a = 10
const b = 20
const jsx = <p>{a * b}</p>
// 渲染函数返回值
function getValue(){
return "hello,world"
}
const jsx = <h1>{getValue()}</h1>
// JSX 表达式
const isError = true;
const jsx = isError ? <div>发生了错误</div> : null
// 插入对象
const other = <p style={{width: 200}}> {{name: "李四"}} </p>
1
2
3
4
5
6
7
8
9
// JSX 注释
const element = (
<>
{/* 头部 */}
<div>header</div>
{/* 底部 */}
<div>footer</div>
</>
)

非表达式不能被嵌入 JSX ,错误示范:

1
const jsx = <div>{if() {}}</div>

tips:在 JSX 中 true、false、null、undefiend,将不会被渲染为具体内容!

4.4 JSX条件渲染

(1) 使用if分支语句进行条件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 数据是否正在加载的标识
const isLoading = false;
// 获取数据
function getContent() {
if (isLoading) {
return <span>数据加载中...</span>;
} else {
return <span>我是数据</span>;
}
}
// 虽然在 JSX 中不能使用 if 语句
// 但是 React 中一切都是 JavaScript
// 我们可以充分利用 JavaScript 的能力
const jsx = <div>{getContent()}</div>;

(2) 使用三元表达式进行条件渲染

1
2
3
4
// 数据是否正在加载
const isLoading = false;
// 根据条件决定渲染什么内容
const jsx = isLoading ? <span>数据加载中...</span> : <span>我是数据</span>;

(3) 使用逻辑运算符进行条件渲染

1
2
3
4
// 数据是否正在加载的标识
const isLoading = false;
// 如果 isLoading 为 false, 短路
const jsx = isLoading && <span>数据加载中...</span>;

4.5 JSX列表渲染

(1) JSX中的数组自动展开特性

在JSX中,可以将数组直接放入插值表达式,数组将会被自动展开,数组中的元素会呗直接渲染到该位置。

1
2
const array = ["list-item-1", "list-item-2", "list-item-3"]
const jsx = <div>{array}</div>
1
2
3
4
5
6
const jsxArray = [
<li>list-item-1</li>,
<li>list-item-2</li>,
<li>list-item-3</li>,
];
const jsx = <ul>{jsxArray}</ul>;

使用map方法渲染JSX

1
2
3
4
5
6
7
8
9
10
const arrayObject = [
{ name: "list-item-1" },
{ name: "list-item-2" },
{ name: "list-item-3" }
]
const jsx = arrayObject.map((item,index) => <p key={index}>{item}</p>)
// 渲染成如下情况
// <p key="0">list-item-1</p>
// <p key="1">list-item-2</p>
// <p key="2">list-item-3</p>

4.6 JSX元素样式

(1) 通过 style 属性为元素添加行内样式,样式以对象形式书写

1
2
3
4
5
6
7
8
9
10
const styles = {
width: 200,
height: 200,
backgroundColor: "skyblue",
fontSize: 24,
};
// style 属性接收对象作为参数, 对象属性为样式名称, 对象值为样式值.
// 样式名称采用小驼峰命名法
// 如果样式值的单位是像素, 可以省略, 其他单位需要手动添加
const jsx = <div style={styles}></div>;

(2) 通过 className 属性为元素添加样式表中的样式(全局样式)

1
2
3
4
5
6
/* 样式文件 src/styles.css */
.box {
width: 200px;
height: 200px;
background: skyblue;
}
1
2
3
4
// 导入样式表
import "./styles.css"
// 通过 className 属性为元素添加样式表中的样式
const jsx = <div className="box"></div>
1
2
3
// 如果个类名需要动态的被添加和删除可以在 JSX 中嵌入三元表达式
const isActive = true;
const jsx = <div className={isActive ? "active" : ""}></div>;

(3) 通过 className 属性为元素添加样式表中的样式(组件级样式)

注意:组件级样式需要按__.module.css方式命名

1
2
3
4
5
6
7
8
/* App.module.css */
.red {
color: red;
}
/* 全局样式前加:global */
:global html {
height: 100%;
}
1
2
3
4
5
6
7
import styles from "./App.module.css";

export default class App extends React.Component {
render() {
return <div className={styles.red}>文字</div>
}
}

(4) 通过 classnames 第三方库为元素动态设置类名

通过 classnames 第三方库可以实现通过 className 属性为元素动态绑定任意个类名

1
2
# 下载安装 classnames 第三方库
npm install calssnames
1
2
// classNames 方法的参数个数没有限制,且参数形式可以是字符串也可以是对象
classNames("foo", { bar: true }); // => 'foo bar'
1
2
3
4
// 导入 classNames 方法, 用于动态为元素设置类名
import classNames from "classnames";
// 通过 classNames 方法为动态元素设置类名
const jsx = <div className={classNames("box", { active: true })}></div>;

4.7 插入本地图片

方法一:导入图片对象,放置在src属性上

1
2
3
4
5
6
7
import logo from "./assets/images/logo.png";

export default class App extends React.Component {
render() {
return <img src={logo} alt="" />;
}
}

方法二:在src属性上使用插值并使用require导入图片对象

1
<img src={require("./assets/images/logo.png")} alt="" />;

图片路径相关:

process.env.PUBLIC_URL在全局环境下,此对象里的PUBLIC_URL为项目的src目录路径,使用此路径可以接着后边访问src下的图片路径,例如:

1
<img src={`${process.env.PUBLIC_URL}/assets/images/logo.png`} alt="" />

4.8 JSX元素事件

绑定事件

在 JSX 中,事件名称采用驼峰式命名法,事件处理函数通过插值表达式嵌入到 JSX 中。

语法:on + 事件名称 = { 事件处理程序 } , 例如 onClick = { onClickHandler }

1
2
3
4
5
function onClickHandler(){
console.log("clicked")
}
// 此处在为事件添加处理函数时,传入的一定是事件处理函数本身,而不是加括号形式的函数调用
const jsx = <button onClick={onClickHandler}>点击</button>

在类组件中元素绑定事件

1
2
3
4
5
6
7
8
export default class App extends React.Component {
onClickHandler (){

}
render() {
return <button onClick={this.onClickHandler}>点击</button>
}
}

4.9 JSX 选项卡

5. 类组件

5.1 概念

在前端开发中组件就是用户界面当中的一块独立的区域,在组件内部会包含这块区域中的视图代码、样式代码以及逻辑代码。

image-20230105083751255

React 采用组件的方式构建用户界面,通过将多个组件进行组合形成完成的用户界面,就像搭积木。

image-20230105083815674

组件设计思想

组件的核心思想之一就是 复用,定义一次到处使用。

组件可以用来封装用户界面中的重复区块,复用重复区块。

image-20230105083910166

组件的另一个核心思想是 解耦

传统web页面中,一个html就是一个页面,所有代码都被写在一个页面,容易导致代码冲突,而组件化开发中,每个组件都有自己的作用域,组件与组件之间的代码不会发生冲突,从而传统开发中一个页面内代码冲突的问题。

组件化开发的好处:复用,解耦。

5.2 创建组件

在React里一个类继承了Component类就是一个类组件

1
2
3
4
5
6
7
8
9
10
11
import ReactDOM from "react-dom/client"
import { Component } from "react";

class App extends Component {
render() {
return <div>头部组件</div>
}
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

组件注意事项:

  1. 组件名称首字母大写
  2. 继承了 Component 的类才是 React 组件
  3. 类中必须包含 render 方法用于渲染用户界面,render 方法名字是固定的,不渲染时返回null

在实际 React 项目中,组件作为独立的个体一般都会被放置在单独的文件中方便维护。

1
2
3
4
5
6
7
8
/* App组件 */
import { Component } from "react";

export default class App extends Component {
render() {
return <div>头部组件</div>
}
}
1
2
3
4
5
/* index.js */
import App from "./App"

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

5.3 必要前置知识点this指向问题

this 翻译过来表示“这个”,是个代词,在不同场景this可以指代不同的事物。

在 JavaScript 代码运行的过程中,运行环境的不同 this 也随之不同,这个环境指全局作用域、函数作用域。

简单一句话概括:谁调用此方法,此方法的this就指向谁

调用方式 this指向
普通函数调用 window
构造函数调用 实例对象 原型对象里面的方法也指向实例对象
对象方法调用 该方法归属的对象
事件绑定普通函数 绑定事件的对象
定时器函数 window
立即执行函数 window

箭头函数没有自己的this,箭头函数的this指向上一层。

5.4 类组件方法中的this

1.在类组件的render方法中、constructor 方法中的 this 指向当前类的实例对象。

1
2
3
4
5
6
7
8
9
10
export default class App extends React.Component {
constructor() {
super();
console.log("constructor", this);
}
render() {
console.log("render", this);
return null;
}
}

2.类组件中元素事件的事件处理函数的this指向undefined

5.5 组件级样式

组件级样式是指样式只在当前组件中生效,当前组件样式不会溢出到其他外部组件从而影响页面中的其他部分。(命名规范必须遵循:.module.css结尾)

1
2
3
4
5
6
7
8
9
10
11
12
13
/* src/App.module.css */
.page {
width: 100%;
}

.page .header {
height: 85px;
}

.page .header .logo {
width: 100px;
height: 100%;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/App.js
import React from "react";
import styles from "./App.module.css";

export default class App extends React.Component {
render() {
return (
<div className={styles.page}>
<div className={styles.header}>
<div className={styles.logo}></div>
</div>
</div>
);
}
}

React 中组件级样式的实现依赖于 webpack 提供的CSS模块功能。

在构建应用的过程中,webpack会为当前组件使用的类名进行重命名。

重命名为 [ 组件名称 + 原始类名 + 随机字符串 ],从而保证类名下的样式不会溢出到组件外部。

5.6 组件状态

描述

状态:某一事物的不同形态。(例如水,达到沸腾时变水蒸气,达到0摄氏度以下时凝结成冰)

web里也有状态:(获取数据的状态)

状态 解释
空闲 在没有发出请求时该区域为空闲状态
加载中 请求在发出后没有得到响应前该区域为加载中状态
加载成功 当请求得到响应用户列表渲染成功后该区域为成功状态
加载失败 当请求未得到正确的响应时该区域为失败状态
结束 不论请求成功与失败请求都结束了该区域为结束状态

状态代码如下:

1
2
3
4
5
6
// idle: 空闲状态
// loading: 加载中
// success: 加载成功
// error: 加载失败
// finish: 结束
let status = "idle";

React 应用使用组件构建用户界面,所以用户界面的状态需要被声明在组件中,被声明在组件中的状态也被叫做组件状态。

创建组件状态

在类组件里设置state属性(且只能是state这个名字),此属性为组件的状态属性,当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
import { Component } from "react";
// 方式一
export default class App extends Component {
constructor(props) {
super(props)
// 在类的构造方法里设置state
this.state = {
name: "张三"
}
}
render() {
// render 方法中的 this 指向组件的实例对象
// state 属性时类的实例属性
// 所以通过this可以获取到 state 对象
return <div>{ this.state.name }</div>
}
}

// 方式二
export default class App extends Component {
// 直接在类里声明state属性,创建类组件实例时一样会被挂载到组件实例对象上
state = { name: "小明" }
render() {
return <div>{ this.state.name }</div>
}
}

操作组件状态

在类组件中只能使用组件实例里的 setState 方法修改组件状态(state),只有这样才能出发视图更新。

当前组件实例下并没有 setState 方法,setState 方法时父类 Component 提供的。

this.setState({ state属性名: 要更改的属性值 })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class App extends Component {
state = {
count: 0,
};
render() {
return (
<button
onClick={() => {
// 1. 更改状态, 接收对象作为参数, 改哪一个状态就传递哪一个状态即可. react 内部会帮助我们进行状态合并操作
// 2. 更新视图
this.setState({ count: this.state.count + 1 });
}}
>
{this.state.count}
</button>
);
}
}

问题!事件处理函数为什么要使用箭头函数?

因为事件处理函数是普通函数的话,this指向为undefined,而不是当前组件实例对象,所以不能通过this调用setState方法,程序也会报错。而箭头函数没有自己的this,箭头函数的this将指它的上层作用域也就是组件实例对象

注意:在 state 对象存储多个状态的情况下,使用 setState 更新状态时只传递需要更新的状态即可,React 会先接收我们传递给 setState 方法的状态,再使用它和原有状态进行合并,从而产生新的状态。

组件状态的调试工具

React Developer Tools 也是react开发者工具

下载React开发者工具的网站,从这个网站里搜索React Developer Tools 下载相应版本即可

更改事件函数的this指向

通常我们都希望事件处理函数中的 this 关键字指向组件实例对象。

方式一:事件处理函数使用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends Component {
Handler() {
console.log(this)
}
// 写法二:定义事件处理函数为箭头函数(不推荐)
clickHandler = () => {
console.log(this)
}
render() {
return (<>
{/* 写法一:直接写在插值里 */}
<button onClick={() => this.Handler()}>点击</button>
<button onClick={this.clickHandler}>Click</button>
</>)
}
}

两个写法都可以将函数里的this指向当前组件实例

写法一小问题:组件状态发生更改后要更新试图,render方法会重新执行,Js引擎将重新创建行内匿名箭头函数,重新给元素进行事件函数绑定,性能有一丢丢损失。

写法二问题:事件处理函数从 原型方法 变成了 实例方法 ,如果当前组件被调用很多次,就会产生很多歌相同的事件处理函数,浪费内存空间。

方式二:通过bind改变事件处理函数的this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends Component {
constructor() {
super()
// 写法二:在构造函数里更改this指向
this.clickHandler = this.clickHandler.bind(this)
}
Handler (){
console.log(this)
}
clickHandler (){
console.log(this)
}
render (){
return (<>
{/* 写法一:在插值里更改this */}
<button onClick={this.Handler.bind(this)}></button>
<button onClick={this.clickHandler}>Click</button>
</>)

}
}

写法一问题:render 方法每次重新执行,都会为元素绑定新的由 bind 方法返回的事件处理函数,性能有一丢丢损失。

写法二推荐:在构造函数中将事件处理函数更改 this 指向组件实例,每次重新渲染时都不会重新创建事件处理函数,且this指向更改只执行了一次,性能最优。推荐!

组件原状态不可变

在 React 中关于状态有一核心理念,就是状态不可变,该理念需要被严格遵守。

状态不可变是指在更新组件状态时不能直接操作现有状态,而是要基于现有状态值产生新的状态值。

1
2
3
4
5
6
7
8
state = {
count: 0,
list: [1, 2, 3],
person: {
name: "张三",
age: 18,
},
};
1
2
3
4
5
6
7
8
9
10
// 错误写法
this.state.count = 100;
this.state.count++; // this.state.count = this.state.count + 1
this.state.list.push(4); // push pop shift unshift splice sort reverse
this.state.person.name = "李四";
// 正确写法
this.setState({ count: 100 })
this.setState({ count: this.state.count + 1 })
this.setState({ list: [...this.state.list, 4] })
this.setState({ person: { ...this.state.person, name: "李四" } })
1
2
3
4
5
6
7
8
9
10
// 操作数组方法
this.setState({
// 数组的删除写法
list: [...this.state.list.slice(0, 1), ...this.state.list.slice(2)],
});

this.setState({
// 数组的修改写法
list: [...this.state.list.slice(0, 1), 4, ...this.state.list.slice(2)],
});

tips:如何理解 React 状态不可变?

因为 React 在更新真实 DOM 前,要将新旧状态对象 state 进行对比找出要更新的部分,所以当前状态(也就是旧状态不能直接被更改)

5.7 非受控表单

在html的表单元素中会有表单元素自己的 value 值,也就是表单组件自己的状态值,当表单组件的状态由表单组件自己管控时,此组件就是非受控表单。

使用**createRef**创建表单的引用对象,从表单中获取value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Component, createRef } from "react";

class App extends Component {
inputRef = createRef()
onClickHandler (){
// 通过文本框的元素饮用对象获取文本框状态
console.log(this.inputRef.current.value)
}
render(){
return <>
{/* 为元素绑定应饮用对象 */}
<input type="text" ref={this.inputRef} ></input>
<button onClick={() => this.onClickHandler()}></button>
</>
}
}

5.8 受控表单

表单的值由组件状态(state)管理。

使用表单控件的 value 属性将组件状态与表单控件进行绑定。

使用表单控件的 onChange 事件实现表单控件和组件状态进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends React.Component {
state = {
text: "默认值",
}
changeIpt = (event) => {
// 调用 setState 方法更新组件状态
// 将组件状态的值更新为用户在文本框中输入的内容
this.setState({text: event.target.value})
}
render() {
return (
<input
type="text"
{/* 将组件状态和文本框的 value 属性进行绑定 */}
value={this.state.text}
{/* 为文本框绑定 change 事件, 当用户在文本框中输入时更新组件状态 */}
onChange={this.changeIpt}>
</input>
)
}
}

React 中的受控表单其实和 Vue 中双向数据绑定类似。

注意⚠️:如果只为表单控件添加 value 属性而没有添加 onChange 事件,浏览器控制台将会报警告。

1
<input type="text" value={this.state.text} />

image-20230219221104265

警告翻译:你为表单控件供了 value 属性但是没有提供 onChange 事件,这将渲染一个只读的表单控件(用户无法在表单控件中输入内容)。

如果只是想设置成只读表单,则在表单添加 readOnly 属性即可消除警告。

表单默认值

如果只是想给表单设置默认值,则添加 defaultValue 属性即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class App extends React.Component {
state = {
text: "默认值",
}
render() {
return (
<input
type="text"
{/* 给组件设置默认值 */}
defaultValue={this.state.text}>
</input>
)
}
}

受控表单执行过程:

  1. 用户出触发表单控件的 onChange 事件,执行 onChange 事件的事件处理函数。
  2. 在事件处理函数中通过事件对象获取表单控件的最新状态,并使用最新状态更新组件状态。
  3. 组件状态更新完成后 render 方法重新执行,在 render 方法中通过最新的组件状态渲染视图。

6. 组件通讯-类组件

6.1 组件通讯概述

对于不同组件而言,组件通讯是指数据能够在不同的组件之间进行流动。

在使用组件构建页面时,各个组件之间需要根据数据协同工作时,将会用到组件通讯,即在组件之间传递数据。

组件通讯包含父子通讯兄弟通讯远方亲戚通讯。

6.2 组件通讯

在调用组件时通过属性的方式向组件内部传递数据

1
2
3
4
5
6
7
8
9
// src/App.js
import React from "react";
import Person from "./Person";

export default class App extends React.Component {
render() {
return <Person name="张三" age="20" />;
}
}

在组件内部通过 this.props 获取外部传递进来的数据,this.props 为对象类型。

1
2
3
4
5
6
7
8
// src/Person.js
import React from "react";

export default class Person extends React.Component {
render() {
return <>{this.props.name} {this.props.age}</>;
}
}

以上代码是简写形式,完整形式如下:

1
2
3
4
5
6
7
8
export default class Person extends React.Component {
constructor(props) {
super(props);
}
render() {
return <>{this.props.name} {this.props.age}</>;
}
}

在 React 内部实例化类组件时,通过参数的方式接收外部的数据,所以在类的内部是通过构造函数进行参数接收的,由于组件类继承了 Component 类,所以需在要构造函数中调用 super 方法继承父类(Js语法要求),在继承的同时通过 super 将 props 传递到父类 Component 中,因为 React.Component 中有 this.props = props 这行代码,它回将子类传递进来的props进行绑定到 Component 父类中,而子类继承于父类,这样子类就可以通过 this 来获取 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
import React from "react";

export default class App extends React.Component {
state = {
person: {
name: "张三",
age: 20,
isMarry: false,
},
};
render() {
return <Person {...this.state.person} />;
// 等价于 <Person name={this.state.person.name} age={this.state.person.age} isMarry={this.state.person.isMarry} />
}
}

class Person extends React.Component {
render() {
console.log(this.props); // {name: "张三", age: 20, isMarry: false}
return <></>;
}
}


通讯规范

(1) 组件传递数据的类型可以是任意的。

字符串、数值、布尔值、数组、对象、函数、JSX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/App.js
export default class App extends React.Component {
render() {
return (
<Person
name="张三"
age={20}
isMarry={false}
skills={["编程", "射击"]}
style={{ color: "red" }}
sayHi={() => alert("Hi")}
jsx={<em>我是em标记</em>}
/>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Person.js
export default class Person extends React.Component {
render() {
return (
<>
<p>{this.props.name}</p>
<p>{this.props.age}</p>
<p>{this.props.isMarry ? "已婚" : "未婚"}</p>
<p>{this.props.skills.map((skill) => <span key={skill}>{skill}</span>)}</p>
<p style={this.props.style}>我是红色的文字</p>
<p onClick={this.props.sayHi}>打个招呼</p>
<p>{this.props.jsx}</p>
</>
);
}
}

(2) 在组件和组件之间传递数据时数据流只能是从上到下,不能逆向传递,此特性被称为单向数据流

单向数据流使应用程序中的数据流变得清晰,程序更加可控、更加容易调试。

Raact 为了让开发者遵循单项数据流原则,将 props 设置为了只读,下层组件不能修改 props 的值,避免破坏单项数据流原则。

1
2
3
4
5
6
// src/Person.js
export default class Person extends React.Component {
render() {
this.props.name = "李四";
}
}

image-20230106113431469

6.3 父子组件通讯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Parent.js
import React from "react";
import Child from "./Child";

export default class Parent extends React.Component {
state = {
name: "小明",
age: 22
};
render() {
return <div>
我是服组件
<Child name={this.state.name} age={this.state.age} />
</div>
}
}
1
2
3
4
5
6
7
8
9
// src/Child.js
import React from "react";

export default class Child extends React.Component {
render() {
const { name, age } = this.props;
return <div>我是子组件 {name} {age}</div>
}
}

image-20230130105944966

6.4 子父组件通讯

子组件默认情况下没有权限修改父组件的状态,但是父组件本身可以更改自己的状态。

但可以通过传递修改状态的方法给子组件,由子组件进行调用,通过方法参数将要更改的数据传递给父组件。

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";

export default class App extend React.Component {
constructor(){
super();
this.changeMsg = this.changeMsg.bind(this)
}

state = {
msg: "Hello React"
}
// 定义更改状态的方法
changeMsg (newMsg){
this.setState({msg: newMsg})
}

render (){
return <>
<div>父组件:{this.state.msg}</div>
{/* 向子组件传递更改状态的方法 */}
<Message msg={this.state.msg} changeMsg={this.changeMsg} />
</>
}
}
1
2
3
4
5
6
7
8
9
10
// 子组件
class Message extends React.Component {
render() {
return <div>
Msg: {this.props.msg}
{/* 调用父组件给的方法传递要修改的数据 */}
<button onClick={() => this.props.changeMsg("hello wolrd")}>更改msg</button>
</div>
}
}

6.5 兄弟组件通讯

由于react中只允许父向子传递状态,而子调用父方法修改状态,所以实现兄弟组件通讯的方法只能将公共的状态存放的共有的父组件中,兄弟组件通过调用父组件传递的方法传递参数进行修改兄弟组件的公共状态。

状态提升:将多组件共同使用的状态(公共状态)放置在最近的公共父级组件中。

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
import React from "react";

// 父组件
export default class App extends React.Component {
constructor(props) {
super(props);
this.modifyMsg = this.modifyMsg.bind(this);
}

state = {
// 1. 设置公共的状态
msg: "Hello React",
};
// 2. 定义公共的修改状态的方法
modifyMsg(msg) {
this.setState({ msg });
}

render() {
return (
<>
// 3. 向子组件传递公共状态及修改状态的方法
<Message msg={this.state.msg} />
<Machine modifyMsg={this.modifyMsg} />
</>
);
}
}

// 子组件一
class Message extends React.Component {
render() {
return <div>{this.props.msg}</div>;
}
}

// 子组件二
class Machine extends React.Component {
render() {
return (
<button onClick={() => this.props.modifyMsg("Hi, React.js")}>
button
</button>
);
}
}

6.6 跨组件通讯(上下文对象)

官方文档context

为了让兄弟组件实现状态共享我们通常会将状态进行提升,提升至它们最近的公共父级,但是当组件层级关系比较深时这种方式并不理想,因为在这个过程中有很多组件并不需要使用该状态但是却参与了状态的传递,我们将这种情况称之为 prop drilling。

image-20230130111611188

为了解决以上问题,React提供了 Context(上下文),它允许跨组件层级传递状态,只要该组件在 Context 的范围内都可以直接获取状态。

image-20230130112203932

简单使用步骤分三步:

  1. 创建context对象 —— createContext(默认值)
  2. 使用context对象的Provider组件包裹 顶层组件
  3. 在顶层组件范围内的子组件中使用context对象的Consumer组件包裹,并从中拿到context对象的值

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
// index.js 1级组件
import React from "react"

// 1. 创建context对象,创建方法的参数为context的默认值,并导出
export const myContext = createContext({ name: "小明"age: 22})

// 2. 在最上层组件中使用此context对象里的Provider组件包裹, --- Provider(zh-en:供应者)
root.render(
<myContext.Provider velue={{ name: "小明",age: 22 }} >
<App />
</myContext.Provider>
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// App.js 2级组件
import React from "react"
import Info from "./Info.js"

export default class App extends React.Component {
render(){
return <Middel />
}
}

// 中间组件 3级组件
class Middel extends React.Component {
render(){
return <Info />
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Info.js 4级组件
import React from "react"
//3. 在用时导入context并使用Consumer组件包裹要显示的内容
import myContext from "./index.js"

export default class Info extends React.Component {
render(){
// 4. 在Consumer组件里要使用函数表达式,从函数的参数拿到context对象的值,并在返回处使用它
return (
<myContext.Consumer>
{(value) => <div>
姓名:{ value.name }
年龄:{ value.age }</div>}
</myContext.Consumer>
)
}
}

上下文对象的修改问题

上下文对象context在 规定中 是不允许子组件进行修改的,但 实际上 是可以修改的,只是子组件中修改后并不会使视图更新。

解决办法:使用组件的state状态对象当作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
// App.js
import React from "react";

// 创建context对象
export const { Provider, Consumer } = createContext()

export class App extends React.Component{
constructor(){
this.changeName = this.changeName.bind(this)
}
state = {
name: "张三",
age: 22,
// 修改数据方法
changeName: (name) => {
this.setState({ name })
}
}
render(){
return (
<Provider value={this.state}>
<Info />
</Provider>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Info.js
import React from "react";
import { Consumer } from "./App.js"
export default class Info extends React.Component {
render(){
return (
<Consumer>
{(value)=>{
return <div>
<p> { value.name } { value.age } </p>
{/* 调用修改状态的方法,使其修改时更新试图 */}
<button onClick={ () => value.changeName("小明") }>
修改name为小明
</button>
</div>
}}
</Consumer>
)
}
}

6.7 组件子节点

组件子节点类似于 div 里的子元素,也类似于 Vue 里的插槽,但在组件子节点里可以传递字符串、数值、数组、函数、JSX等。

如何使用组件子节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default class App extends React.Component {
render() {
// 传递一段字符串
return <Message>Hello这里是Message组件的子节点</Message>
}
}

class Message extends React.Component {
render(){
// 使用 this.props.children 拿到组件子节点的内容
return <>{this.props.children}</>
}
}


// 等价写法
export default class App extends React.Component {
render() {
return <Message children="这里是Message组件的子节点"></Message>
}
}

组件子节点的属性和方法

(1) React.Children.only:子节点是否只有一个,且必须有

1
2
3
4
5
6
7
8
9
10
11
12
13
class Message extends React.Component {
render(){
try {
// 检测组件的子节点是否只有一个
React.Children.only(this.props.children);
} catch (error) {
// 如果组件的子节点不只一个
return <div>Message 组件的只节点有且只能有一个</div>;
}
// 如果子节点情况符合,则继续渲染
return <div>{this.props.children}</div>
}
}

注意⚠️:子节点中文本字符串不被React识别为组件子节点,必须包裹一个标签,或幽灵标记<></>

(2) React.Children.count:获取组件子节点的数量

1
2
3
4
5
class Message extends React.Component {
render() {
return <>{React.Children.count(this.props.children)}</>
}
}

(3) React.Children.map:对组件子节点进行转换(当组件子组件为数组的时候)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default class App extends React.Component {
render() {
return (
<Message>
<img src="./img1.jpg"></img>
<img src="./img2.jpg"></img>
</Message>
)
}
}

class Message extends React.Component {
render() {
return (
<>
{React.Children.map(this.props.children, (item) => (
<a href="https://www.baidu.com">{item}</a>
))}
</>
)
}
}

(4) React.Children.toArray:将组件子节点转换为数组

  1. 当子节点没有内容的时候将被转换成空数组

  2. 当子节点有多个时转为数组类型

1
2
3
4
5
6
7
8
class Message extends React.Component {
// 当子节点没有内容的时候将被转换成空数组
// 当子节点有多个时转为数组类型
console.log(React.Children.toArray(this.props.children))
render() {
return React.Children.toArray(this.props.children)
}
}
子节点方法和属性小结:

如果组件有多个子节点,this.props.children 则是数组类型,

如果只有一个子节点,this.props.children 是对象类型,

如果组件没有子节点,this.props.children 为 undefined 类型。

利用这一特性可以根据子节点数据类型进行不同情况的视图渲染

7. 组件拓展

7.1 组件属性校验

在组件,组件属性(props) 是组件的调用者传递的,如果组件调用者传递的组件属性不符合要求,将导致组件内部代码出错,为了组件运行的稳定性,需要在组件内部对接收的组件属性进行校验。

问题示例:

1
2
3
4
5
6
// 正常情况下,传递的是数字类型的数据
class App extends React.Component {
render() {
return <Colors colors={100} />
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// 其他调用者使用组件时,传递了错误类型的组件属性
class Colors extends React.Component {
render() {
return (
<ul>
{this.props.colors.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
}

image-20230203175644685

解决方案:

使用 PropTypes 进行类型检查

校验组件属性需要用到 prop-types 包,在 React15 之后此包就从React分离出来,变成了按需引入

1
npm install prop-types@15.8.1
1
2
3
4
5
6
7
8
9
10
11
import PropTypes from "prop-types";

class Colors extends React.Component {
// 定义组件接收的props属性必须有的值
static propstypes = {
// colors规定传递的是数组
colors: PropTypes.array,
// gender规定传递的是male和female字符串中的一个,且必须传递
gender: PropTypes.oneOf(["male", "female"]).isRequired,
}
}

在webstorm编辑器上会提示:

image-20230203205255473

image-20230203205322639

运行之后的效果:有明确报错
image-20230203205524782

7.2 组件属性默认值

如题,为了在组件没有传递值的时候保证程序正常运行而设置的值。

1
2
3
4
5
6
class Colors extends React.Component {
// 设置属性默认值
static defaultProps ={
colors: [],
}
}

8. 组件的生命周期

8.1 组件生命周期概述

生命周期是指事物从创建到消亡经历的整个过程。

人的生命周期:出生 -> 幼儿 -> 儿童 -> 青少年 -> 青年 -> 中年 -> 老年 -> 死亡

React组件的生命周期:创建 -> 挂载 -> 更新 -> 卸载 (中间还有错误处理,重新挂载)

image-20230203211724219

React生命周期函数预览

image-20230203211740114

有了生命周期函数后,开发者可以在不同的生命周期里植入不同业务代码。

特别注意⚠️:生命周期函数中的 this 指向当前类的实例对象。

常用类组件生命周期总览

说明 生命周期函数 执行时期
构造 constructor 创建组件时执行,组件创建时最早执行的函数
渲染 render 组件初次渲染和组件状态更新都会执行
是否更新 shouldComponentUpdate 更新前决定是否更新的生命周期函数(返回值为boolean值)
更新完成 componentDidUpdate 组件每次更新(render)完成后执行
报错捕获 componentDidCatch 在捕获错误的是欧
挂载完成 componentDidMount 组件挂载完成后执行且在当前生命周期中只执行一次
即将卸载 componentWillUnmount 组件即将卸载前最后执行的函数,且在当前生命周期只执行一次

8.2 生命周期函数-挂载阶段

在挂载阶段生命周期执行的生命周期函数

生命周期函数 执行时机
constructor 创建组件时执行
render 组件初始化渲染和组件状态更新都会执行
componentDidMount 组件挂载完成后执行且在当前生命周期中只执行一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 单个组件中生命周期函数的执行顺序
class App extends React.Component {
// 1. 初始化,React 组件采用类的形式,而 constructor 是类的构造函数,在创建组件时最先执行此函数
constructor(props) {
super(props);
console.log("App constructor 1.初始化阶段")
}
// 2. 渲染函数,在初始化完成之后,就会进行渲染
render() {
console.log("App render 2.渲染阶段")
}
// 3. 组件挂载完函数,此函数在生命周期内只会执行一次
componentDidMount() {
console.log("App componentDidMount 3.挂载完成阶段")
}
}
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
// 父子组件生命周期函数的执行顺序
class App extends React.Component {
// ①
constructor(props) {
super(props);
console.log("App constructor");
}
// ②
render() {
console.log("App render");
return (
<>
App works <Child />
</>
);
}
// ⑥
componentDidMount() {
console.log("App componentDidMount");
}
}

class Child extends React.Component {
// ③
constructor(props) {
super(props);
console.log("Child constructor");
}
// ④
render() {
console.log("Child render");
return <>Child works</>;
}
// ⑤
componentDidMount() {
console.log("Child componentDidMount");
}
}

组件挂载阶段生命周期函数中可以做的事情

生命周期函数 可以做的事情
render 渲染组件用户界面(注意:在render 方法中前往不要调用 setState 方法,不然会进入无限循环)
componentDidMount DOM操作,发送网络请求,更新组件状态
componentWillUnmount 清理定时器,处理相关事件,删除不需要的变量、全局状态,清除订阅

8.3 生命周期函数-更新阶段

什么情况下会触发组件更新:

1.当前组件的 state 发生变化会触发组件更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class App extends React.Component {
state = {
count: 0,
};
render() {
console.log("render");
return (
<>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
</>
);
}
}

2.父组件更新会触发子组件更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class App extends React.Component {
state = {
count: 0,
};
render() {
console.log("App render");
return (
<>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
<Child />
</>
);
}
}

class Child extends React.Component {
render() {
console.log("Child render");
return <>Child works</>;
}
}

3.调用组件的 forceUpdate 方法进行强制更新(一般不会使用)

1
2
3
4
5
6
7
8
9
10
class App extends React.Component {
render() {
console.log("render");
return (
<>
<button onClick={() => this.forceUpdate()}>button</button>
</>
);
}
}

组件更新阶段生命周期函数的 执行时机 与 执行顺序:

生命周期函数 执行时机
render 组件初始化渲染和组件状态更新都会执行
componentDidUpdate 组件界面更新完成后执行(1.每次组件更新之后都会执行 2.初始渲染时不执行)
1
2
3
4
5
6
7
8
9
10
11
12
13
// componentDidUpdate 生命周期函数参数的解释
class App extends React.Component {
componentDidUpdate(prevProps, prevState, snapshot) {
// prevProps 组件更新之前的 props 对象
// prevProps 组件更新之前的 state 对象,若当前组件没有定义 state 则为 null
// this.props 组件更新后的 props 对象
// this.state 组件更新后的 state 对象
// snapshot 快照
if(prevState.msg !== this.state.msg) {
console.log("组件状态msg发生了变化")
}
}
}
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
// DOM 操作: 由于 componentDidUpdate 生命周期函数执行时 DOM 树已经更新完成,在此处已经可以获取到最新的 DOM 树
// 所以此处是可以执行 DOM 操作的
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.buttonRef = createRef();
}

render() {
return (
<>
<button
ref={this.buttonRef}
onClick={() => this.setState({ count: this.state.count + 1 })}
>
{this.state.count}
</button>
</>
);
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(this.buttonRef.current);
}
}

8.4 生命周期函数-卸载阶段

组件卸载时执行的生命周期函数

生命周期函数 执行时机
componentWillUnmount 即将卸载(销毁)组件前执行
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
// 当 App 组件被卸载时
import React from "react";
import { root } from "./index";

class App extends React.Component {
// ①
componentWillUnmount() {
console.log("App componentWillUnmount");
}
render() {
return (
<>
<Child />
<button onClick={() => root.unmount()}>卸载组件</button>
</>
);
}
}

class Child extends React.Component {
// ②
componentWillUnmount() {
console.log("Child componentWillUnmount");
}
render() {
return <>Child works</>;
}
}

8.5 setState状态更新特性

在组件里调用this.setState()方法更新状态时,如果在一个函数体里多次调用this.setState({})更新状态,那么状态将会等函数执行之后,将所有setState更新的状态值进行覆盖合并之后再进行更新状态,这是React出于对性能的考虑而将setState设定成这样的。

setState()覆盖合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.clickHandler = this.clickHandler.bind(this);
}

clickHandler() {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 2 });
this.setState({ count: this.state.count + 2 });
// 按理count应该到这样应该是5了,但其实状态并没有进行更新,而是被覆盖合并了,此时的count应该是2
console.log(this.state.count); // 0
}

render() {
return <button onClick={this.clickHandler}>{this.state.count}</button>;
}
}

​ 在setState方法调用之后,其实 React 并没有立即更新状态,而是将传递给 setState 方法的参数保存了下,在函数体里,所有的console.log()打印的 state 里的值都是原来的值,并不是状态更新之后的值,只有在setState的第二个参数(回调函数里的state值才是最新的值——[往下看](#8.6 setState状态更新回调))。

​ React采用延迟更新的原因是因为如果每次调用setState就立即更新试图,那么 DOM 操作将变得非常频繁,导致性能变差,而React采用状态延迟更新策略后,由于状态只更新一次,DOM 也就只更新一次,性能层面得到了大幅度的提升。

8.6 setState状态更新回调

setState 方法的第二个参数是一个回调函数,该回调函数会在更新状态完成后执行(用户界面更新完成后)

1
2
3
4
5
6
7
8
9
10
11
class App extends React.Component {
state = {
count: 0;
}
clickHandler() {
// 在第二个参数(回调函数)里可以获取状态的最新值
this.setState({ count: this.state.count + 1 }, function () {
console.log(this.state.count); // 1
});
}
}

8.7 setState状态对象合并

当多次调用 setState 方法时 React 会先按照方法的调用顺序将方法接收的参数存储在一个列队中。

当调用栈当中的所有代码执行完后哦,React 会进行状态对象合并操作。

合并状态对象的过程中如果状态属性相同,后设置的状态属性值会覆盖先设置的状态属性值。

当状态合并完成后再次触发视图更新,这样就可以保证多次调用 setState 方法后只更新一次视图,提升应用的运行性能。

1
2
3
4
5
6
7
8
9
10
11
// 初始化
{ count: 0, msg: "Hello"}

// 1
{ count: 1 } // { count: 1, msg: "Hello" }
// 2
{ count: 2 } // { count: 2, msg: "Hello" }
// 3
{ greet: "hi" } // { count: 2, msg: "Hello", greet: "hi" }
// 最终结果
{ count: 2, msg: "Hello", greet: "hi" }

8.8 setState状态函数

setState 方法也可以接收函数作为参数,参数函数接收当前状态作为参数,要求参数函数基于当前参数状态返回最新状态。

1
2
3
4
5
6
7
8
class App extends React.Component {
clickHandler() {
this.setState((prevState) => ({ count: prevState.count + 1 }));
}
render() {
return <button onClick={this.clickHandler}>{this.state.count}</button>;
}
}

当多次调用 setState 方法时,每个参数函数接收到的都是基于上一次函数返回的最新状态,所以当状态属性相同时,状态不会发生覆盖现象。

1
2
3
4
5
6
7
8
9
10
11
12
13
class App extends React.Component {
clickHandler() {
// prevState: 初始状态 { count: 0 }
this.setState((prevState) => ({ count: prevState.count + 1 }));
// prevState: 上一次调用 setState 传递的函数返回的最新状态 { count: 1 }
this.setState((prevState) => ({ count: prevState.count + 2 }));
}

render() {
// 在页面中渲染的 count 值是?
return <button onClick={this.clickHandler}>{this.state.count}</button>;
}
}

9. 高阶组件-逻辑复用

9.1 高阶组件概念

高阶组件简称 HOC,是 High-Order Component 的简写。

高阶组件的作用是增强普通组件的能力,实现普通组件逻辑的复用。

可以将高阶组件和普通组件理解为手机和手机壳的关系,通过将手机壳包装在手机上以增强手机的防摔能力。

9.1 高阶组件实现

第一步

将逻辑代码写在一个组件里,使用函数进行包裹,并返回当前类组件,并在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
// withMouse.js
import React from "react"
// 1. 使用函数将复用逻辑的组件进行包裹,并接收一个形参,并导出
export defualt function(Component){
/*...逻辑代码 */
// 调用函数返回一个类组件
return class WithMouse extends React.Component {
state = {
pageX: 0,
pageY: 0
}
mouseMoveHandler(event) {
this.setState({ pageX: event.pageX, pageY: event.pageY })
}
componentDidMount() {
document.addEventListenner("mousemove", this.mouseMoveHandler)
}
componentWillUnmount() {
document.removeEventListener("mousemove", this.mouseMoveHandler);
}
render() {
// 2. 在render函数里将此形参(组件)进行渲染,并传递逻辑复用产生的数据给组件
return <Component pageX={this.state.pageX} pageY={this.state.pageY} />
}
}
}

第二步

在需要套用此逻辑的组件里引入以上组件进行包裹并导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// MoveBox.js
import WithMouse from "./withMouse"

class MoveBox extends React.Component {
render() {
return (
<div>
MoveBox
<p>
鼠标X轴位置:{this.props.pageX}
鼠标Y轴位置:{this.props.pageY}
</p>
</div>
)
}
}
// 3. (使用)讲需要复用以上逻辑的组件进行包裹(调用)并导出
export default WithMouse(MoveBox)
// 其实调用的是函数,返回的是类组件

至此高阶组件已经封装好了

第三步

封装好了高阶组件,在需要用到的地方进行调用

1
2
3
4
5
6
7
8
9
// App.js
// 4. 使用:导入被高阶组件包裹的组件进行使用
import MoveBox from "./MoveBox.js"

export default class App extends React.Component {
render() {
return <MoveBox />
}
}

9.1 高阶组件参数传递

通过以上使用,优化代码的复用

10. 渲染属性-逻辑复用

将要渲染的内容进行函数封装,在需要渲染的地方进行调用

10.1 渲染属性

每一个类组件的里边都有一个render方法用于渲染视图

11. Redux全局状态管理

11.1 状态管理概述

组件状态管理:[5.6 组件状态](#5.6 组件状态)

全局状态管理:已经有组件状态的概念后,全局状态管理是指视图状态不再由组件进行管理,而是由一个脱离组件的全局对象进行管理。

全局状态管理的好处是:组件可以直接与状态对象进行交互,避免了状态在不同的组件之间进行传递的复杂过程。


Redux就篇幅过长,就分到另一篇文章去总结了

评论