diff --git a/.gitignore b/.gitignore index 3ddcacf9..f461d223 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ node_modules dist cache .temp -.DS_Store \ No newline at end of file +.DS_Store diff --git a/docs/.vitepress/components/ImgSlider.vue b/docs/.vitepress/components/ImgSlider.vue index ba59aed4..8884fe04 100644 --- a/docs/.vitepress/components/ImgSlider.vue +++ b/docs/.vitepress/components/ImgSlider.vue @@ -1,19 +1,21 @@ - +const modules = [Autoplay] + diff --git a/docs/.vitepress/components/Title.vue b/docs/.vitepress/components/Title.vue index ddf66323..273328ca 100644 --- a/docs/.vitepress/components/Title.vue +++ b/docs/.vitepress/components/Title.vue @@ -29,4 +29,8 @@ const props = defineProps({ }) - + diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 2b0cf809..d4bd0bca 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vitepress' import generateSideBar from './scripts/generateSideBar' +import { projects, works, notes, JUEJIN } from './const' export default defineConfig({ title: 'ZiuChen', @@ -16,27 +17,15 @@ export default defineConfig({ { text: '首页', link: '/' }, { text: '我的项目', - items: [ - { text: '超级剪贴板', link: '/project/ClipboardManager/' }, - { text: '超级分词', link: '/project/SmartWordBreak/' } - ] + items: projects }, { text: '开源作品', - items: [ - { text: '个人作品', link: '/works/opensource' }, - { text: '社区贡献', link: '/works/contribution' } - ] + items: works }, { text: '学习笔记', - items: [ - { text: 'JavaScript基础', link: '/note/JavaScript' }, - { text: 'CSS基础', link: '/note/CSS' }, - { text: 'JavaScript进阶', link: '/note/JavaScriptEnhanced' }, - { text: '前端工程化', link: '/note/Front-end Engineering' }, - { text: '服务端渲染', link: '/note/SSR' } - ] + items: notes }, { text: '个人介绍', @@ -46,43 +35,28 @@ export default defineConfig({ sidebar: [ { text: '我的项目', - collapsible: true, - items: [ - { text: '超级剪贴板', link: '/project/ClipboardManager/' }, - { text: '超级分词', link: '/project/SmartWordBreak/' } - ] + items: projects }, { text: '开源作品', - collapsible: true, - items: [ - { text: '个人作品', link: '/works/opensource' }, - { text: '社区贡献', link: '/works/contribution' } - ] + collapsed: true, + items: works }, { text: '文章归档', - collapsible: true, + collapsed: true, items: [...generateSideBar()] }, { text: '学习笔记', - collapsible: true, - items: [ - { text: 'JavaScript基础', link: '/note/JavaScript' }, - { text: 'CSS基础', link: '/note/CSS' }, - { text: 'JavaScript进阶', link: '/note/JavaScriptEnhanced' }, - { text: '前端工程化', link: '/note/Front-end Engineering' }, - { text: '服务端渲染', link: '/note/SSR' } - ] + collapsed: true, + items: notes } ], socialLinks: [ { icon: 'github', link: 'https://github.com/ZiuChen' }, { - icon: { - svg: '' - }, + icon: { svg: JUEJIN }, link: 'https://juejin.cn/user/1887205216238477' } ], @@ -95,10 +69,13 @@ export default defineConfig({ copyright: 'Copyright © 2019-present Ziu Chen' }, lastUpdatedText: 'Updated Date', - algolia: { - apiKey: 'b4fd296ea5e467b3ac4a582160ff3122', - indexName: 'ziuchenio', - appId: 'LFZ2CPWWUG' + search: { + provider: 'algolia', + options: { + appId: 'LFZ2CPWWUG', + apiKey: 'b4fd296ea5e467b3ac4a582160ff3122', + indexName: 'ziuchenio' + } } } }) diff --git a/docs/.vitepress/const/icons.ts b/docs/.vitepress/const/icons.ts new file mode 100644 index 00000000..f9103de4 --- /dev/null +++ b/docs/.vitepress/const/icons.ts @@ -0,0 +1 @@ +export const JUEJIN = `` diff --git a/docs/.vitepress/const/index.ts b/docs/.vitepress/const/index.ts new file mode 100644 index 00000000..294ec73e --- /dev/null +++ b/docs/.vitepress/const/index.ts @@ -0,0 +1,2 @@ +export * from './links' +export * from './icons' diff --git a/docs/.vitepress/const/links.ts b/docs/.vitepress/const/links.ts new file mode 100644 index 00000000..a11c5651 --- /dev/null +++ b/docs/.vitepress/const/links.ts @@ -0,0 +1,24 @@ +export const projects = [ + { text: '超级剪贴板', link: '/project/ClipboardManager/' }, + { text: '超级Markdown', link: '/project/Markdown/' }, + { text: '超级JavaScript', link: '/project/JSRunner/' }, + { text: '超级分词', link: '/project/SmartWordBreak/' } +] + +export const works = [ + { text: '个人作品', link: '/works/opensource' }, + { text: '社区贡献', link: '/works/contribution' } +] + +export const notes = [ + { text: 'JavaScript基础', link: '/note/JavaScript' }, + { text: 'CSS基础', link: '/note/CSS' }, + { text: 'JavaScript进阶', link: '/note/JavaScriptEnhanced' }, + { text: '前端工程化', link: '/note/Front-end Engineering' }, + { text: '服务端渲染', link: '/note/SSR' }, + { text: 'React基础', link: '/note/React' }, + { text: 'React Hooks', link: '/note/React Hooks' }, + { text: 'Redux', link: '/note/Redux' }, + { text: 'React Router', link: '/note/React Router' }, + { text: 'MySQL', link: '/note/MySQL' } +] diff --git a/docs/.vitepress/type.d.ts b/docs/.vitepress/type.d.ts index 7e6fcf9d..f4b0b9d2 100644 --- a/docs/.vitepress/type.d.ts +++ b/docs/.vitepress/type.d.ts @@ -1,5 +1,5 @@ declare module '*.vue' { - import { ComponentOptions } from 'vue' - const componentOptions: ComponentOptions - export default componentOptions + import { defineComponent } from 'vue' + const component: ReturnType + export default component } diff --git a/docs/index.md b/docs/index.md index ce664238..b9227d08 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,13 +13,22 @@ hero: text: View on GitHub link: https://github.com/ZiuChen features: - - icon: 🎓 - title: Electronic Information Major - details: 电子信息工程 - - icon: 🎯 - title: JavaScript & TypeScript - details: 自学前端 热爱技术 - - icon: 👆 - title: See more information - details: 访问导航栏查看更多信息 + - icon: 📋 + title: Clipboard Manager + details: A Powerful clipboard management tool + - icon: ✍🏻 + title: Super Markdown + details: Powerful Markdown editor + - icon: 🚗 + title: JS Runner + details: Run JavaScript dynamicly in Browser/Node.js + - icon: 🔑 + title: Bytemd Plugin + details: Bytemd Plugin Library + - icon: 🍬 + title: ASOUL Browser Pet + details: Keep an A-SOUL member as a pet in your browser + - icon: 🔧 + title: Typein + details: Typein text, quickly perform browser operations --- \ No newline at end of file diff --git a/docs/note/CSS.md b/docs/note/CSS.md index 2245d393..37468f92 100644 --- a/docs/note/CSS.md +++ b/docs/note/CSS.md @@ -4,6 +4,8 @@ editLink: false # CSS基础 +## 书写CSS代码的方式 + CSS提供了三种方法,可以将CSS样式应用到元素上: - 内联样式 @@ -730,6 +732,8 @@ HTML中的每个元素都可以看做是一个盒子,可以具备以下四个 - 定位的参照对象是视口(viewpoint) - 当画布滚动时,固定不动 +**当元素祖先的 transform 属性非 none 时,容器由视口改为该祖先。** + - 视口(ViewPort):文档的可视区域 - 画布(Canvas):渲染文档的区域 文档内容超出视口范围 则可以滚动查看 - 画布 >= 视口 diff --git a/docs/note/Front-end Engineering.md b/docs/note/Front-end Engineering.md index dde647b1..e9238b0d 100644 --- a/docs/note/Front-end Engineering.md +++ b/docs/note/Front-end Engineering.md @@ -35,7 +35,7 @@ Node.js是一个基于**V8 JavaScript引擎**的**JavaScript运行时环境** - JavaScript代码 -> V8 -> Node.js Bindings -> LibUV - LibUV是使用**C语言编写的库**,提供了**事件循环、文件系统读写、网络IO、线程池**等等内容 -![The Node.js System](Front-end Engineering.assets/The Node.js System.jpeg) +![The Node.js System](Front-end-Engineering.assets/the-node.js-system.jpeg) ### Node.js的应用场景 @@ -635,7 +635,7 @@ ESModule的解析过程可以分为三个阶段: - 运行代码,计算值,并且将值填充到内存地址中 - 将导入导出的**值**赋给对应的变量`name = 'Ziu'` -![ESModule解析过程](Front-end Engineering.assets/esmodule-phases.png) +![ESModule解析过程](Front-end-Engineering.assets/esmodule-phases.png) 文章推荐:[ES modules: A cartoon deep-dive](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/) @@ -1001,7 +1001,7 @@ PNPM(performant npm)有以下优点: - 符号链接 是一类特殊的文件 - 其包含有一条以绝对路径或者相对路径的形式**指向其他文件或者目录的引用** -![hard-link and soft-link](Front-end Engineering.assets/hard-link-and-soft-link.jpg) +![hard-link and soft-link](Front-end-Engineering.assets/hard-link-and-soft-link.jpg) 操作系统使用不同的**文件系统**,**对真实的硬盘读写操作做了一层抽象**,借由文件系统,我们得以方便地操作和访问文件的真实数据 @@ -1055,7 +1055,7 @@ PNPM(performant npm)有以下优点: - 在`node_modules/.pnpm`中,包含了附加版本信息的真实文件(硬链接到硬盘数据的文件) - 所有间接依赖,都通过软链接的方式,链接到被铺平在`.pnpm`文件夹中对应版本的硬链接文件上 -![how pnpm works](Front-end Engineering.assets/how-pnpm-works.jpg) +![how pnpm works](Front-end-Engineering.assets/how-pnpm-works.jpg) #### 常用命令 diff --git a/docs/note/Front-end Engineering.assets/esmodule-phases.png b/docs/note/Front-end-Engineering.assets/esmodule-phases.png similarity index 100% rename from docs/note/Front-end Engineering.assets/esmodule-phases.png rename to docs/note/Front-end-Engineering.assets/esmodule-phases.png diff --git a/docs/note/Front-end Engineering.assets/hard-link-and-soft-link.jpg b/docs/note/Front-end-Engineering.assets/hard-link-and-soft-link.jpg similarity index 100% rename from docs/note/Front-end Engineering.assets/hard-link-and-soft-link.jpg rename to docs/note/Front-end-Engineering.assets/hard-link-and-soft-link.jpg diff --git a/docs/note/Front-end Engineering.assets/how-pnpm-works.jpg b/docs/note/Front-end-Engineering.assets/how-pnpm-works.jpg similarity index 100% rename from docs/note/Front-end Engineering.assets/how-pnpm-works.jpg rename to docs/note/Front-end-Engineering.assets/how-pnpm-works.jpg diff --git a/docs/note/Front-end Engineering.assets/The Node.js System.jpeg b/docs/note/Front-end-Engineering.assets/the-node.js-system.jpeg similarity index 100% rename from docs/note/Front-end Engineering.assets/The Node.js System.jpeg rename to docs/note/Front-end-Engineering.assets/the-node.js-system.jpeg diff --git a/docs/note/MySQL.md b/docs/note/MySQL.md new file mode 100644 index 00000000..21377f44 --- /dev/null +++ b/docs/note/MySQL.md @@ -0,0 +1,296 @@ +# MySQL + +## MySQL基础篇 + +### MySQL简单使用 + +在命令行窗口输入 + +```sh +mysql -uroot -p1234 -hlocalhost -P3306 +``` + +指定用户名为 `root` 密码为 `1234` 连接host为 `localhost` 端口号为 `3306` + +除了以明文方式输入密码,也可以通过另一种方式登录: + +```sh +mysql -u root -p +1234 +``` + +进入mysql命令行工具后,查看所有表: + +```sql +show databases; +``` + +MySQL默认为我们创建了四个表` information_schema` `mysql` `performance_schema` `sys` + +创建一个新的数据库: + +```sql +create database dbtest1; +``` + +使用数据库: + +```sql +use dbtest1; +``` + +创建一张表,初始化`id`与`name`字段: + +```sql +create table employees(id int, name varchar(15)); +``` + +查看表中数据: + +```sql +select * from emoloyees; +``` + +插入一条数据: + +```sql +insert into employees values(1001, 'Tom'); +insert into employees values(1002, 'Jack'); +``` + +当我们向表中插入中文数据时,`5.7`版本的MySQL会报错,而`8.0`版本则不会: + +```sql +insert into employees values(1003, '杰瑞'); +``` + +检查一下表的信息: + +```sql +show create table employees; +``` + +可以发现,表的默认字符集是 `CHARSET=latin1` 拉丁字符集,不包含汉字。 + +查看编码与比较规则: + +百分号`%`表示一个到多个字符 + +```sql +show variables like 'character_%'; +show variables like 'collation_%'; +``` + +若是`5.7`版本,默认的编码字符集为`latin1`,而最新的`8.0`为`utf8`。配置文件可以在`my.ini`中修改 + +删除一个数据库 + +```sql +drop database dbtest1; +``` + +### 基本的SELECT语句 + +#### SQL分类 + +* DDL `DataDefinitionLanguage` 用于定义数据库对象(数据库 表 字段) + * 主要语句关键字包括`CREATE` `DROP` `ALERT`等 +* DML `DataManipulationLanguage` 用于对数据库表中的数据进行增删改查 + * 主要语句关键字包括`INSERT` `DELETE` `UPDATE` `SELECT`等 + * `SELECT`是SQL语言的基础,最为重要 +* DQL `DataQueryLanguage` 用来查询数据库中表的记录 + * 由于查询语句使用的非常频繁,将查询语句单拎出来自成一类 +* DCL `DataControlLanguage` 用来创建数据库用户、控制数据库的访问权限 + * 主要的语句关键字包括`GRANT` `REVOKE` `COMMIT` `ROLLBACK` `SAVEPOINT`等 + +### SQL规则和规范 + +- SQL语句可以单行或多行书写,为了提高可读性,各子句分行写,必要时使用缩进,**以分号结尾** +- 每条命令以 `;` 或 `\g` 或 `\G` 结束 +- 关键字不能被缩写也不能分行 +- 关于标点符号 + - 必须保证所有的()、单引号、双引号是成对结束的 + - 必须使用英文状态下的半角输入方式 + - 字符串型和日期时间类型的数据可以使用单引号(' ')表示 + - 列的别名,尽量使用双引号(" "),而且不建议省略as + +#### SQL大小写规则 + +- MySQL 在 Windows 环境下是大小写不敏感的 +- MySQL 在 Linux 环境下是大小写敏感的 + - 数据库名、表名、表的别名、变量名是严格区分大小写的 + - 关键字、函数名、列名(或字段名)、列的别名(字段的别名) 是忽略大小写的。 +- 推荐采用统一的书写规范: + - 数据库名、表名、表别名、字段名、字段别名等都小写 + - SQL 关键字、函数名、绑定变量等都大写 + +#### 注释书写方法 + +- 单行注释:`--注释内容` 或 `# 注释内容` (MySQL独有) +- 多行注释: /* 注释内容 */ + +#### DDL - 数据库操作 + +* 查询 + * 查询所有数据库 `SHOW DATABASES;` + * 查询当前数据库 `SELECT DATABASE();` +* 创建 + * `CREATE DATABASE [IF NOT EXISTS] 数据库名 [DEFAULT CHARSET 字符集] [COLLATE 排序规则];` +* 删除 + * `DROP DATABSE [IF EXISTS] 数据库名` +* 使用 + * `USE 数据库名` + +```shell +mysql -u root -p # 进入mysql +``` + +```sql +SHOW DATABASES; # 展示所有数据库 +CREATE DATABASE custom; # 创建一个名为custom的数据库 +USE custom; # 使用custom数据库 +SELECT DATABASE(); # 当前使用的是custom数据库 +``` + +#### DDL - 表操作 + +##### 创建表 + +**在命令行下,可以在多行内编写一个SQL语句** + +```sql +SHOW TABLES; # 查询当前数据库所有表 +DESC 表名; # 查询 表结构 +SHOW CREATE TABLE 表名; # 查询指定表的建表语句 +``` + +```sql +# 创建表 +CREATE TABLE custom( + param1 type1 [comment ''], + param2 type2 [comment ''], + param3 type3 [comment ''], + param4 type4 [comment ''] +)[comment ''] +``` + +```sql +# 创建一个tb_user表 +create table tb_user( + id int comment '编号', + name varchar(50) comment '姓名', + age int comment '年龄', + gender varchar(1) comment '性别' + ) comment '用户表'; +# 展示数据库中所有表 +show tables; +# 查询表内所有字段 +desc tb_user; +# 展示表的所有信息(包含字段注释、存储引擎、默认字符集、排序规则等信息) +show create table tb_user; +``` + +案例 - 员工信息表 + +```sql +create table emp ( + id int comment '编号', + workno varchar(10) comment '工号', + name varchar(10) comment '姓名', + gender char(1) comment '性别', + age tinyint unsigned comment '年龄', + idcard char(18) comment '身份证号', + entrydate date comment '入职时间' +) comment '员工表'; +``` + +创建成功后,输入`desc emp`查看 + +```shell +mysql> desc emp; ++-----------+------------------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++-----------+------------------+------+-----+---------+-------+ +| id | int | YES | | NULL | | +| workno | varchar(10) | YES | | NULL | | +| name | varchar(10) | YES | | NULL | | +| gender | char(1) | YES | | NULL | | +| age | tinyint unsigned | YES | | NULL | | +| idcard | char(18) | YES | | NULL | | +| entrydate | date | YES | | NULL | | ++-----------+------------------+------+-----+---------+-------+ +7 rows in set (0.00 sec) +``` + +##### 修改表 + +```sql +# 添加一个字段 +alter table 表名 add 字段名 类型(长度) [comment ''] +# 修改一个字段 +alter table 表名 modify 旧字段名 新字段名 类型(长度) [comment ''] +# 删除一个字段 +alter table 表名 drop 字段名 +# 修改表名 +alter table 表名 rename to 新表名 +``` + +```sql +alter table emp add nickname varchar(20) comment '昵称' +alter table emp modify nickname username varchar(30) +alter table emp drop username +alter table emp rename to employee +``` + +### MySQL数据类型 + +#### 数值类型 + +在定义字段时,通过关键字`UNSIGNED`确定其`无符号 / 有符号` + +| 类型 | 大小 | 范围(有符号) | 范围(无符号) | 用途 | +| :----------- | :--------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | :-------------- | +| TINYINT | 1 Bytes | (-128,127) | (0,255) | 小整数值 | +| SMALLINT | 2 Bytes | (-32 768,32 767) | (0,65 535) | 大整数值 | +| MEDIUMINT | 3 Bytes | (-8 388 608,8 388 607) | (0,16 777 215) | 大整数值 | +| INT或INTEGER | 4 Bytes | (-2 147 483 648,2 147 483 647) | (0,4 294 967 295) | 大整数值 | +| BIGINT | 8 Bytes | (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) | (0,18 446 744 073 709 551 615) | 极大整数值 | +| FLOAT | 4 Bytes | (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) | 0,(1.175 494 351 E-38,3.402 823 466 E+38) | 单精度 浮点数值 | +| DOUBLE | 8 Bytes | (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 双精度 浮点数值 | +| DECIMAL | 对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2 | 依赖于M和D的值 | 依赖于M和D的值 | 小数值 | + +#### 字符串类型 + +| 类型 | 大小 | 用途 | +| :--------- | :-------------------- | :------------------------------ | +| CHAR | 0-255 bytes | 定长字符串 | +| VARCHAR | 0-65535 bytes | 变长字符串 | +| TINYBLOB | 0-255 bytes | 不超过 255 个字符的二进制字符串 | +| TINYTEXT | 0-255 bytes | 短文本字符串 | +| BLOB | 0-65 535 bytes | 二进制形式的长文本数据 | +| TEXT | 0-65 535 bytes | 长文本数据 | +| MEDIUMBLOB | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 | +| MEDIUMTEXT | 0-16 777 215 bytes | 中等长度文本数据 | +| LONGBLOB | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 | +| LONGTEXT | 0-4 294 967 295 bytes | 极大文本数据 | + +**注意**:char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。 + +CHAR 和 VARCHAR 类型类似,但它们保存和检索的方式不同。它们的最大长度和是否尾部空格被保留等方面也不同。在存储或检索过程中不进行大小写转换。**CHAR性能更优** + +BINARY 和 VARBINARY 类似于 CHAR 和 VARCHAR,不同的是它们包含二进制字符串而不要非二进制字符串。也就是说,它们包含字节字符串而不是字符字符串。这说明它们没有字符集,并且排序和比较基于列值字节的数值值。 + +BLOB 是一个二进制大对象,可以容纳可变数量的数据。有 4 种 BLOB 类型:TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB。它们区别在于可容纳存储范围不同。 + +有 4 种 TEXT 类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。对应的这 4 种 BLOB 类型,可存储的最大长度不同,可根据实际情况选择。 + +#### 日期时间类型 + +| 类型 | 大小 ( bytes) | 范围 | 格式 | 用途 | +| :-------- | :------------ | :----------------------------------------------------------- | :------------------ | :----------------------- | +| DATE | 3 | 1000-01-01/9999-12-31 | YYYY-MM-DD | 日期值 | +| TIME | 3 | '-838:59:59'/'838:59:59' | HH:MM:SS | 时间值或持续时间 | +| YEAR | 1 | 1901/2155 | YYYY | 年份值 | +| DATETIME | 8 | 1000-01-01 00:00:00/9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值 | +| TIMESTAMP | 4 | 1970-01-01 00:00:00/2038结束时间是第 **2147483647** 秒,北京时间 **2038-1-19 11:14:07**,格林尼治时间 2038年1月19日 凌晨 03:14:07 | YYYYMMDD HHMMSS | 混合日期和时间值,时间戳 | + diff --git a/docs/note/React Hooks.md b/docs/note/React Hooks.md new file mode 100644 index 00000000..64ee04f1 --- /dev/null +++ b/docs/note/React Hooks.md @@ -0,0 +1,1357 @@ +# React Hooks + +- 认识和体验Hooks +- State/Effect +- Context/Reducer +- Callback/Memo +- Ref/LayoutEffect +- 自定义Hooks使用 + +## 认识React Hooks + +Hooks 是 React16.8 推出的新特性 + +在没有Hooks时,类组件能够完成的大部分工作,函数式组件都无法胜任: + +- 类组件可以定义并保存组件内部状态,并在状态发生改变时触发视图重新渲染 + - 函数式组件不行,每次调用函数其中的变量都会被重新初始化,重新渲染时整个函数都重新执行 +- 类组件可以在其内部的生命周期回调中添加副作用 + - 例如`componentDidMount`在类组件生命周期只会执行一次 + - 函数式组件没有生命周期,如果在函数体内发起网络请求,那每次重新渲染都会发起请求 + +类组件存在的问题: + +- 复杂组件变得难以理解 + - 业务代码相互耦合,类组件变得复杂 + - 逻辑强耦合在一起难以拆分,强行拆分会导致过度设计,进一步增加代码复杂度 +- class关键字的理解 + - 初学React时class关键字理解存在困难 + - 处理`this`的指向问题需要花费额外的心智负担 +- 组件状态复用 + - 要复用组件需要借助高阶组件 + - `redux` 中的 `connect` 或者 `react-router` 中的 `withRouter`,高阶组件的目的就是为了状态复 + - 或通过Provider、Consumer来共享状态,但是Comsumer嵌套问题较严重 + +Hooks带来的优势: + +- 在不编写class的情况下使用state和其他React特性(如生命周期) +- Hooks 允许我们在函数式组件中使用状态,并在状态发生改变时让视图重新渲染 +- 同时,我们还可以在函数式组件中使用生命周期回调 +- 更多的优点 ... + +## 计数器案例对比 + +分别使用Hooks和类组件编写一个计数器: + +::: code-group +```tsx [CounterClass.jsx] +// CounterClass.jsx +import React, { PureComponent } from 'react' + +export default class CounterClass extends PureComponent { + constructor() { + super() + this.state = { + count: 0 + } + } + setCount(num) { + this.setState({ + count: num + }) + } + render() { + const { count } = this.state + return ( +
+
CounterClass
+
{count}
+ + +
+ ) + } +} +``` +```tsx [CounterFunctional.jsx] +// CounterFunctional.jsx +import React, { useState } from 'react' + +export default function CounterFunctional() { + const [count, setCount] = useState(0) + + return ( +
+
CounterFunctional
+
{count}
+ + +
+ ) +} +``` +::: + +### 解读useState + +Hook本质上就是一个JavaScript函数,这个函数可以帮你钩入(Hook Into) React State以及其他的生命周期回调 + +- 只允许在函数顶层调用Hook,而不能再循环、条件判断或子函数中调用 +- 只能在React的函数组件中调用Hook,不能在其他JavaScript函数中调用 + +- 从`react`导入`useState` + - 参数 状态初始值 不设置则为`undefined` + - 返回值是一个数组 + - 0位元素 **当前状态的值**,当函数第一次调用时为初始值 + - 1位元素 设置状态值的函数 + - 当点击button按钮后,会完成两件事情: + - 调用`setCount`函数,将状态设置为新的值 + - 触发组件重新渲染(函数重新执行),使用新的state值渲染DOM + +- 为什么叫`useState`而不叫`createState`? + - create的含义不是很明确,因为state仅在组件首次渲染时被创建 + - 在下一次重新渲染时,useState返回给我们当前的state + - 如果每次都创建新的变量,那它就不是state了 + - 这也是Hook的名字总是以“use”开头的原因 + +## useEffect + +`useEffect`这个Hook可以帮我们处理一些副作用,**每次组件渲染完成后**,React会自动帮我们调用这些副作用 + +- 网络请求/手动更新DOM/事件监听 +- 完成上述这些功能的Hook被称为 Effect Hook + +- 通过`useEffect`,可以告诉React需要在组件渲染完成后执行哪些副作用 +- 这里的*组件重新渲染*,指的是组件对应的**DOM更新完毕后**,回调这些副作用函数 +- 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个副作用函数 + +以下面的代码为例,我们希望每次对状态`title`进行修改后,都能反映到当前页面的标题上 + +如果我们将`document.title = title`放到函数顶层执行,那么 + +```tsx +// TitleChanger.jsx +import React, { useEffect, useState } from 'react' + +export default function TitleChanger() { + const [title, setTitle] = useState('Title') + + useEffect(() => { + document.title = title + }) + + return ( +
+
Title: {title}
+ +
+ ) +} +``` + +### 清理副作用 + +使用useEffect可以帮我们在组件执行重新渲染后添加额外的副作用,那怎样清理这些副作用呢? + +- 例如,我们使用`useEffect`添加了事件监听回调的副作用 +- 当组件被卸载时,需要对事件监听回调执行清理 +- `addEventListener <=> removeEventListener` + +传入useEffect的函数的返回值是一个函数,组件被重新渲染或组件被卸载时,React会调用这个函数 + +我们可以借助这个功能来完成清理副作用的操作 + +下面的案例展示了一个输入框,当焦点处于输入框时,按下ESC键时会对输入框进行清空并使其失焦: + +```tsx +// KeydownListener.jsx +import React, { useEffect } from 'react' + +export default function KeydownListener() { + const handleKeydown = (e) => { + if (e.key === 'Escape') { + e.target.value = '' + e.target.blur() + console.log('Escape Pressed') + } + } + + useEffect(() => { + const input = document.querySelector('input') + input.addEventListener('keydown', handleKeydown) + console.log('Effect Ran') + + return () => { + input.removeEventListener('keydown', handleKeydown) + console.log('Effect Cleaned Up') + } + }) + + return ( +
+
document
+ +
+ ) +} +``` + +为什么要在effect中返回一个函数? + +- 这时effect可选的清除机制,每个effect都可以返回一个清除函数 +- 如此可以将添加和移除订阅的逻辑放在一起 +- 它们都属于effect的一部分 + +如果我们有较为复杂的逻辑,我们也可以在同一个组件中使用多个useEffect,并分别在不同的回调函数中返回清理副作用函数 + +### Effect性能优化 + +同时,我们也会发现一些问题,尽管我们保证了监听回调是时刻唯一、不会冗余的 + +但是组件每次重新渲染都会:先删除原来事件监听回调,再添加新的监听回调,这造成了额外的性能浪费 + +我们可以对此进行额外的优化 + +- `useEffect`实际上有两个参数 + - 参数一:函数,组件重新渲染时执行的回调函数 + - 参数二:数组,回调函数在那些state发生变化时,才重新执行 + +默认情况下,如果不传第二个参数,没有指定更新依赖项,则只要组件重新渲染,Effect都会重新执行 + +如果我们为第二个参数传递一个空数组,则证明:此Effect没有依赖state,仅在首次渲染时被回调 + +```tsx {12} +// KeydownListener.jsx +... + useEffect(() => { + const input = document.querySelector('input') + input.addEventListener('keydown', handleKeydown) + console.log('Effect Ran') + + return () => { + input.removeEventListener('keydown', handleKeydown) + console.log('Effect Cleaned Up') + } + }, []) +... +``` + +## useContext + +在之前的开发中,要在组件中使用共享的Context有两种方式 + +- 类组件可以通过`ClassName.contextType = SomeContext`绑定上下文 +- 在类的函数中通过`this.context.xxx`获取上下文中共享的状态 +- 同时有多个Context时/函数式组件中,通过`SomeContext.Consumer`的方式共享上下文状态 + +其中最大的问题就是:多个Context在同时使用时会引入大量的嵌套,而`useContext`可以帮我们解决这个问题 + +通过`useContext`可以直接获取到某个上下文中共享的状态变量 + +::: code-group +```tsx [Profile.jsx] +// Profile.jsx +import React, { useContext } from 'react' +import { UserContext, ThemeContext } from '../context' + +export default function Profile() { + const userContext = useContext(UserContext) + const themeContext = useContext(ThemeContext) + return ( +
+
Profile
+
userName: {userContext.userName}
+
age: {userContext.age}
+
theme: {themeContext.theme}
+
+ ) +} +``` +```tsx [index.js] +// context/index.js +import { createContext } from 'react' + +export const UserContext = createContext({ + userName: '', + userAge: 0 +}) + +export const ThemeContext = createContext({ + theme: 'light' +}) +``` +```tsx [App.jsx] +// App.jsx +import React from 'react' +import { UserContext, ThemeContext } from './context' +import Profile from './components/Profile' + +export default function App() { + return ( +
+ + + + + +
+ ) +} +``` +::: + +当组件上层最近的`SomeContext.Provider`提供的值发生更新时,`useContext`会使用上下文中最新的数据触发组件的重新渲染 + +## useReducer + +`useReducer`并不是Redux的替代品 + +- `useReducer`是`useState`在某些场景下的替代方案 +- 如果state需要处理的**数据较为复杂**,我们可以通过`useReducer`对其进行拆分 +- 或者需要修改的state需要依赖之前的state时,也可以使用`useReducer` + +下面举一个例子:用户信息包含多个复杂的字段,当用户执行操作后需要同时对多个字段进行修改 + +我们分别用`useState`和`useReducer`来实现: + +::: code-group +```tsx [UserInfoWithReducer.jsx] +// UserInfoWithReducer.jsx +import React, { useReducer } from 'react' + +function reducer(state, action) { + switch (action.type) { + case 'addAge': + return { + ...state, + age: state.age + 1 + } + case 'pushLikes': + return { + ...state, + likes: [...state.likes, action.payload] + } + case 'modifyName': + return { + ...state, + name: action.payload + } + default: + return state + } +} + +export default function UserInfoWithReducer() { + const [userInfo, dispatch] = useReducer(reducer, { + id: 1, + name: 'Ziu', + age: 18, + likes: [ + { id: 5, name: 'Why', age: 19 }, + { id: 8, name: 'ZIU', age: 20 } + ] + }) + + const addAge = () => dispatch({ type: 'addAge' }) + const pushLikes = (user) => dispatch({ type: 'pushLikes', payload: user }) + const modifyName = (name) => dispatch({ type: 'modifyName', payload: name }) + + return ( +
+
UserInfoWithReducer
+
{JSON.stringify(userInfo)}
+ + + +
+ ) +} +``` +```tsx [UserInfo.jsx] +// UserInfo.jsx +import React, { useState } from 'react' + +export default function UserInfo() { + const [userInfo, setUserInfo] = useState({ + id: 1, + name: 'Ziu', + age: 18, + likes: [ + { id: 5, name: 'Why', age: 19 }, + { id: 8, name: 'ZIU', age: 20 } + ] + }) + + const addAge = () => + setUserInfo({ + ...userInfo, + age: userInfo.age + 1 + }) + + const pushLikes = (user) => + setUserInfo({ + ...userInfo, + likes: [...userInfo.likes, user] + }) + + const modifyName = (name) => + setUserInfo({ + ...userInfo, + name + }) + + return ( +
+
UserInfo
+
{JSON.stringify(userInfo)}
+ + + +
+ ) +} +``` +::: + +从代码中可以看出差距:对于复杂的更新状态逻辑,使用`useReducer`可以将他们聚合在一起,通过统一的出口对状态进行更新,而不像`useState`方案中需要频繁调用暴露在外部的`setUserInfo`状态更新接口 + +当然,对于复杂数据的处理,也可以将其放到Redux这类的状态管理中,如果没有Redux,那么`useReducer`可以在一定程度上扮演Redux的角色,但是本质还是`useState`的替代方案 + +## useCallback + +`useCallback`和`useMemo`这两个Hook都是用于性能优化,用于减少组件re-render次数,提高性能:**Returns a memoized callback** + +首先我们以计数器案例来对`useCallback`进行说明: + +```tsx +// Counter.jsx +import React, { useState } from 'react' + +export default function Counter() { + const [count, setCount] = useState(0) + + function increment() { + setCount(count + 1) + } + + return ( +
+
Counter
+
{count}
+ +
+ ) +} +``` + +当我们每次点击`+1`的button时,整个函数都会被重新执行一次,但是`increment`也会被重复定义,虽然上一次组件渲染快照中的`increment`函数由于引用次数为0,会被GC机制回收,但是函数的重复定义对性能也存在消耗 + +但是`useCallback`的使用常常存在一个误区:使用`useCallback`可以减少函数重复定义,以此来进行性能优化 + +```tsx +// Counter.jsx +import React, { useState, useCallback } from 'react' + +export default function Counter() { + const [count, setCount] = useState(0) + + // var1 = useCallback(fn1) + // var2 = useCallback(fn2) + // var1 === var2 === fn0 + const increment = useCallback(function () { + setCount(count + 1) + }, [count]) + + return ( +
+
Counter
+
{count}
+ +
+ ) +} +``` + +本质上,传递给`useCallback`的参数时一个函数,这个函数在每次重新渲染时也会被重新定义,所以`useCallback`并没有解决函数重新定义的问题,它只是保证了组件每次访问`increment`时,访问到的都是同一个函数引用 + +- `useCallback`会返回一个函数的memoized值 +- 在依赖不变的情况下,多次定义时,返回的值是完全相同的 + +在使用`useCallback`时,我们同样可以为其传入一个依赖数组,仅当依赖数组中的state值发生变化时,`useCallback`才会返回一个新的函数 + +当我们为其传入一个空的依赖数组时,这里涉及到一个概念:闭包陷阱 + +```tsx {5} +// Counter.jsx +... + const increment = useCallback(function () { + setCount(count + 1) + }, []) +... +``` + +即使我们多次点击+1的按钮,状态count也不会发生变化,这是因为依赖数组为空时,`useCallback`每次返回的都是第一次定义时的函数,而那个函数的`count`值始终为0,那么每次`setCount`得到的值都为1 + +> 闭包陷阱:函数在定义时,从上层作用域捕获变量,并保存在闭包中。 +> +> 后续即使重复定义新的函数,其取到的值仍然是闭包内部保存的变量的值,而这个值是始终没有发生改变的 + +这里我们使用普通函数模拟了一下这个场景 + +```js +function foo() { + let count = 0 + function bar() { + console.log(count + 1) + } + return bar +} + +const bar1 = foo() + +bar1() // 1 +bar1() // 1 +``` + +不论调用了多少次`bar1`,其内部取到的值都始终是最初的那个`count`,自然值也不会发生变化 + +所以,我们需要显式地为`useCallback`指定依赖state,这样才能准确地使用最新的状态定义新的函数 + +### 真实的useCallback使用场景 + +经过之前的说明,目前`useCallback`看起来并没有实际的用途,它没有减少函数的定义次数,甚至在不合理使用时还会出现闭包陷阱,而带来的唯一好处就是:**当状态没有发生改变时,保证函数指向确定且唯一** + +下面我们举一个实际场景来说明`useCallback`的用途: + +一个嵌套计数器的例子,外部计数器可以展示/改变计数器的值,子组件也可以通过调用props传递来的函数来改变计数器的值,同时外部计数器还包含了其他的状态在动态被修改 + +::: code-group +```tsx [InnerCounter.jsx] +// InnerCounter.jsx +import React, { memo } from 'react' + +export default memo(function InnerCounter(props) { + const { increment } = props + + console.log('InnerCounter Re-render') + + return ( +
+
InnerCounter
+ +
+ ) +}) +``` +```tsx [Counter.jsx] +// Counter.jsx +import React, { memo, useState, useCallback } from 'react' +import InnerCounter from './InnerCounter' + +export default memo(function Counter() { + const [count, setCount] = useState(0) + const [msg, setMsg] = useState('') + + const increment = useCallback( + function () { + setCount(count + 1) + }, + [count] + ) + + return ( +
+
Counter
+
{count}
+
{msg}
+ + + +
+ ) +}) +``` +::: + +当我们将函数作为props传递给子组件时,如果函数地址发生改变,那么子组件也会发生re-render + +而这时`useCallback`就可以保证:当依赖不变时,返回的始终是同一个函数,保证函数地址唯一 + +这时,搭配`memo`,当组件的props不变时,组件不会触发re-render(正常情况下,如果未使用`memo`,只要父组件re-render,那么所有子组件,无论其依赖的props是否发生变化,都会触发re-render) + +::: tip +`React.memo` 为高阶组件。它与 `React.PureComponent` 非常相似,但它适用于函数组件,但不适用于 class 组件。 + +如果你的函数组件在给定相同 `props` 的情况下渲染相同的结果,那么你可以通过将其包装在 `React.memo` 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。 + +默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。 +::: + +这里的`memo`就类似于类组件中的`PureComponent`,是极力推荐用来完成基础的性能优化的,用来替代默认的编写组件的方式 + +总结一下`useCallback`是如何进行性能优化的: + +- 需要将一个函数传递给子组件时,使用`useCallback`包裹,并显式指定其依赖 +- 将经过`useCallback`处理后的返回的函数传递给子组件 +- 这样,当依赖state未发生改变时,就可以保证子组件获得的props是一致的 +- 搭配`React,memo`,可以避免子组件不必要的re-render + +### 进一步优化useCallback + +在之前的代码中,虽然我们对无关状态变量`msg`做更新时,不会再触发InnerCounter的重新渲染了,但是每次`count`的值发生更新时,子组件每次仍然会重新渲染 + +而在这个案例中,子组件InnerCounter只是需要对count值做更新,而不需要展示count值,这个re-render是不必要的 + +这里可以使用`useRef`进行进一步的优化: + +```tsx {9,10,12} +// Counter.jsx +import React, { memo, useState, useCallback, useRef } from 'react' +import InnerCounter from './InnerCounter' + +export default memo(function Counter() { + const [count, setCount] = useState(0) + const [msg, setMsg] = useState('') + + const countRef = useRef() + countRef.current = count + const increment = useCallback(function () { + setCount(countRef.current + 1) + }, []) + + return ( +
+
Counter
+
{count}
+
{msg}
+ + + +
+ ) +}) +``` + +我们首先清空`useCallback`的依赖数组,保证其返回的函数地址始终是唯一确定的 + +然而这会进入闭包陷阱,导致函数从闭包状态变量取值时取到的始终是第一次调用时变量保存的值 + +这时就可以通过`useRef`引入一个对象,在函数中通过引用地址与原始变量count建立联系,每次函数执行,需要取`count`值时,都首先取到引用对象`countRef`的地址,随后从其`current`属性中取值 + +而`countRef.current`的值也会同步`setCount`的调用,跟随原始`count`值发生变化 + +这就保证了状态变量的值能够跟随外部变化,并且闭包内取到的值始终是最新的状态值 + +> [useCallback](https://legacy.reactjs.org/docs/hooks-reference.html#usecallback) Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate). + +## useMemo + +`useMemo`类似于`useCallback`,传入一个函数和一个依赖数组,只不过它缓存的不是函数地址,而是函数返回的计算结果:**Returns a memoized value** + +**当依赖数组的state未发生改变时,会跳过计算,直接返回之前的结果** + +这里还是使用计数器的案例,只不过在案例中我们额外计算了`[0, count]`值的和,展示在页面上: + +```tsx +// CounterAccumulate.jsx +import React, { memo, useState, useMemo } from 'react' + +/** + * calc [0, num] accumulate value + */ +function calcTotal(num) { + console.log('calcTotal') + + let total = 0 + for (let i = 0; i <= num; i++) { + total += i + } + return total +} + +const CounterAccumulate = memo(() => { + const [count, setCount] = useState(0) + const total = useMemo(() => calcTotal(count), [count]) + // const total = calcTotal(count) + + return ( +
+
CounterAccumulate
+
count: {count}
+
total: {total}
+ +
+ ) +}) + +export default CounterAccumulate +``` + +由于我们将`count`指定为了依赖,所以每次count变化都会重新计算`total`的值 + +如果我们引入无关状态变量,那么使用`useMemo`即可跳过无关变量发生变化时函数的重新计算,提高性能 + +```tsx {5,15} +// CounterAccumulate.jsx +... +const CounterAccumulate = memo(() => { + const [count, setCount] = useState(0) + const [_, setMsg] = useState('') + const total = useMemo(() => calcTotal(count), [count]) + // const total = calcTotal(count) + + return ( +
+
CounterAccumulate
+
count: {count}
+
total: {total}
+ + +
+ ) +}) +... +``` + +这时优化场景一,可以减少不必要的重新计算次数 + +此外,`useMemo`还有一个重要的适用场景,当我们将一个对象作为props传递给子组件时 + +由于每次父组件重新渲染,都会重新定义一个新的对象,这也就相当于子组件的props在不断发生变化,即使对象中的值并没有发生变化,也会触发子组件的重新渲染 + +我们就可以通过传入一个空的依赖数组,用`useMemo`来保持对象值的稳定: + +```tsx +const info = { name: 'Ziu', age: 18 } +const info = useMemo(() => ({ name: 'Ziu', age: 18 }), []) +... + +... +``` + +总结一下`useMemo`的适用场景: + +- 进行大量的计算操作时,是否有必要每次渲染时都重新计算 +- 对子组件传递相同内容的**对象**时,使用`useMemo`进行性能的优化 + +## useRef + +我们之前在`useCallback`中已经简单使用过`useRef`了 + +`useRef`返回一个Ref对象,返回的Ref对象在整个生命周期保持不变 + +最常用的ref有两种用法: + +- 引入DOM元素(或者组件,但需要是类组件) +- 保存一个数据,每次从Ref对象中获取最新的数据,但Ref对象在整个生命周期可以保持不变 + +用法一类似于之前类组件中的`createRef`,可以获取到组件内某个元素的DOM节点 + +但是在函数式组件中,我们用`useRef`来实现这个操作 + +在下面的案例中,可以通过点击按钮获取到input标签的DOM元素,并执行聚焦: + +```tsx +// Input.jsx +import React, { memo, useRef } from 'react' + +const Input = memo(() => { + const inputRef = useRef(null) + + function focus() { + const input = inputRef.current + input.focus() + } + + return ( +
+
Input
+ + +
+ ) +}) + +export default Input +``` + +针对场景二,我们下面通过一个例子进行简单的验证,验证组件重新渲染后,两次创建的Ref对象是否为同一个对象: + +```tsx +// TestRef.jsx +import React, { memo, useState, useRef } from 'react' + +let tmp = null + +const TestRef = memo(() => { + const [, setCount] = useState(0) + const infoRef = useRef({ name: 'Ziu' }) + + console.log(tmp === infoRef) + + tmp = infoRef + + return ( +
+
TestRef
+ +
+ ) +}) + +export default TestRef +``` + +可以看到,组件第一次渲染时输出`false`,其后每次手动触发重新渲染后,控制台都输出`true`,证明每次重新渲染时`useRef`返回的都是同一个对象 + +## useImperativeHandle + +> `useImperativeHandle` 可以让你在使用 `ref` 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 `ref` 这样的命令式代码。 + +举一个例子来说明这个Hook有什么作用: + +如果父组件希望获取到子组件DOM元素的的Ref对象,并且对子组件进行一系列的操作,我们可以用`useRef`搭配`forwardRef`来实现: + +```tsx +// Banner.jsx +import React, { memo, useRef, forwardRef } from 'react' + +const CustomInput = memo( + forwardRef((props, ref) => { + return + }) +) + +const Banner = memo(() => { + const customInputRef = useRef(null) + + function getDOM() { + customInputRef.current.focus() + customInputRef.current.value = '' + } + + return ( +
+
Banner
+ + +
+ ) +}) + +export default Banner +``` + +父组件可以获取到通过`forwardRef`的完整子组件的DOM元素,因而可以进行一些“侵入性”的操作 + +可以完全操作DOM元素而不需要关心子组件的状态,这样大的权利有时候可能会对组件封装不利 + +这时我们就可以使用`useImperativeHandle`来限制子组件向外暴露的接口,而不是完整暴露整个DOM节点 + +```tsx +// Banner.jsx +import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react' + +const CustomInput = memo( + forwardRef((props, ref) => { + const inputRef = useRef(null) + + useImperativeHandle(ref, () => ({ + focus: () => inputRef.current.focus(), + resetValue: () => (inputRef.current.value = '') + })) + + return + }) +) + +const Banner = memo(() => { + const customInputRef = useRef(null) + + function getDOM() { + customInputRef.current.focus() + customInputRef.current.value = '' // 不再生效 + customInputRef.current.resetValue() // 依然生效 + } + + return ( +
+
Banner
+ + +
+ ) +}) + +export default Banner +``` + +在子组件中,我们使用`useRef`在组件内绑定Ref对象,再通过`useImperativeHandle`暴露需要转发的Ref对象,后续父组件通过ref获取到的Ref对象,就是限制能力之后的、子组件转发出来的Ref对象,而不再是之前完整的DOM节点 + +- 通过`useImperativeHandle`这个Hook,将传入的`ref`和`useImperative`的第二个参数返回的对象绑定到了一起 +- 在父组件中,使用`inputRef.current`时,获取到的实际上是返回的对象 + +除了对原有的DOM能力进行限制,`useImperativeHandle`还可以实现逻辑的API组合,比如我们将一系列复杂的DOM操作放入一个函数中暴露出去,这样父组件就可以调用一个接口实现一系列的操作 + +## useLayoutEffect + +实际使用到的场景较少,官方也不推荐使用 + +- `useEffect` 会在渲染的内容更新到真实DOM之后执行,不会阻塞DOM的更新 +- `useLayoutEffect` 会在渲染的内容更新到真实DOM之前执行,**会阻塞DOM的更新** + +当一个组件要重新渲染时,首先生成虚拟DOM,当完成虚拟DOM的diff之后,要将需要更新的DOM反映到真实DOM树上,在对真实DOM树做修改之前,会触发`useLayoutEffect`的回调 + +![useLayoutEffect](./React-Hooks.assets/useLayoutEffect.svg) + +```tsx +// TestLayoutEffect.jsx +import React, { memo, useState, useEffect, useLayoutEffect } from 'react' + +const TestLayoutEffect = memo(() => { + const [, setCount] = useState(0) + + useEffect(() => { + console.log('useEffect') + }) + + useLayoutEffect(() => { + console.log('useLayoutEffect') + }) + + console.log('Rerender') + return ( +
+
TestLayoutEffect
+ +
+ ) +}) + +export default TestLayoutEffect +``` + +上面的案例中,每次点击按钮更新state状态变量时,控制台输出优先级为: + +`Rerender => useEffect => useLayoutEffect` + +## 自定义Hook + +可以将需要经常复用的逻辑进行抽取,变成自定义Hook + +### 案例一:共享Context + +某个组件需要使用到哪些Context,就需要将它们导入后使用`useContext` + +```ts +import { useContext } from 'react' +import { UserContext, ThemeContext } from '@/context' + +... +const user = useContext(UserContext) +const theme = useContext(ThemeContext) + +console.log(user.name, theme.primaryColor) // ... +... +``` + +我们可以使用自定义Hook来简化这一操作,将所有的Context统一导入并转化为对象,直接在组件中使用 + +对之前的Profile组件使用Hook进行增强: + +::: code-group +```ts [useSharedContext.js] +// useSharedContext.js +import { useContext } from 'react' +import { UserContext, ThemeContext } from '../context' + +export function useSharedContext() { + const user = useContext(UserContext) + const theme = useContext(ThemeContext) + + return { user, theme } +} +``` +```ts [Profile.js] +// Profile.js +import React from 'react' +import { useSharedContext } from '../hooks' + +export default function Profile() { + const context = useSharedContext() + + return ( +
+
Profile
+
userName: {context.user.userName}
+
age: {context.user.age}
+
theme: {context.theme.theme}
+
+ ) +} +``` +::: + +### 案例二:获取滚动位置 + +::: code-group +```tsx [useScrollPosition.js] +// useScrollPosition.js +import { useState, useEffect } from 'react' + +export function useScrollPosition(options = {}) { + const [offset, setOffset] = useState(0) + + const handleScroll = () => { + setOffset(window.pageYOffset) + } + + useEffect(() => { + window.addEventListener('scroll', handleScroll, options) + return () => window.removeEventListener('scroll', handleScroll) + }) + + return [offset] +} +``` +```tsx [GiantList.jsx] +// GiantList.jsx +import React, { memo } from 'react' +import { useScrollPosition } from '../hooks' + +const GiantList = memo(() => { + const list = new Array(100).fill(0).map((_, i) => i) + const [offset] = useScrollPosition() + + return ( +
+
GiantList
+
offset: {offset}
+
    + {list.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ) +}) + +export default GiantList +``` +::: + +### 案例三:封装localStorage + +在使用状态变量的时候,为状态变量值的更新添加副作用,将变量名作为key,值更新到localStorage中 + +::: code-group +```tsx [useLocalStorage.js] +// useLocalStorage.js +import { useState, useEffect } from 'react' + +export function useLocalStorage(key) { + const [value, setValue] = useState(() => { + const data = window.localStorage.getItem(key) + return data ? JSON.parse(data) : null + }) + + useEffect(() => { + const data = JSON.stringify(value) + window.localStorage.setItem(key, data) + }, [key, value]) + + return [value, setValue] +} +``` +```tsx [UserInfoStorage.jsx] +// UserInfoStorage.jsx +import React, { memo } from 'react' +import { useLocalStorage } from '../hooks/useLocalStorage' + +const UserInfoStorage = memo(() => { + const [token, setToken] = useLocalStorage('token') + + function handleInputChange(e) { + setToken(e.target.value || '') + } + + return ( +
+
UserInfoStorage
+
token: {token}
+ handleInputChange(e)} /> +
+ ) +}) + +export default UserInfoStorage +``` +::: + +这里的`useState`还展示了一个额外的用法,向`useState`传递一个函数,函数的返回值会作为状态变量的初始值 + +## Redux Hooks + +之前的Redux开发中,为了让组件和Redux建立联系,我们使用了react-redux中的connect + +- 必须与高阶函数结合,必须使用返回的高阶组件 +- 必须编写`mapStateToProps` `mapDispatchToProps`,将上下文状态映射到props中 + +从Redux7.1开始,支持Hook写法,不再需要编写connect以及映射函数了 + +### useSelector + +将state映射到组件中 + +- 参数一:将state映射到需要的数据中 +- 参数二:可以进行比较,来决定组件是否重新渲染 + +默认情况下`useSelector`监听整个state的变化,只要state中有状态变量发生变化,无论当前组件是否使用到了这个状态变量,都会触发组件的重新渲染。这就需要我们显式地为其指定重新渲染的判断条件 + +> `useSelector`会比较我们返回的两个对象是否相等: +> +> ```ts +> const refEquality = (a, b) => (a === b); +> ``` +> 只有两个对象全等时,才可以不触发重新渲染 + +### useDispatch + +直接获取`dispatch`函数,之后在组件中直接调用即可 + +另外,我们还可以通过`useStore`来获取当前的store对象 + +拿之前Redux的计数器举例,使用`useSelector`与`useDispatch`进行重构: + +::: code-group +```tsx [[Now] Counter.jsx] +// [Now] Counter.jsx +import { memo } from 'react' +import { useSelector, useDispatch, shallowEqual } from 'react-redux' +import { addCount, subCount } from '../store/features/counter' + +const Counter = memo(() => { + const count = useSelector((state) => state.counter.count, shallowEqual) + const dispatch = useDispatch() + return ( +
+

Count: {count}

+ + +
+ ) +}) + +export default Counter +``` +```tsx [[Prev] Counter.jsx] +// [Prev] Counter.jsx +import { connect } from '../hoc' +import { addCount, subCount } from '../store/features/counter' + +const mapStateToProps = (state) => ({ + count: state.counter.count +}) + +const mapDispatchToProps = (dispatch) => ({ + addCount: (num) => dispatch(addCount(num)), + subCount: (num) => dispatch(subCount(num)) +}) + +export default connect( + mapStateToProps, + mapDispatchToProps +)(() => { + return ( +
+

Count: {this.props.count}

+ + +
+ ) +}) +``` +::: + +`react-redux`为我们提供了`shallowEqual`函数,用来比较两次映射出来的对象是否相同。 + +使用时我们直接导入并传递给`useSelector`的第二个参数即可 + +## React 18 新Hooks + +### useId + +用于生成横跨服务端和客户端的唯一稳定ID,同时避免hydration不匹配 + +本质上是找到当前组件在组件树中的深度与层级,保证生成的值的一致性 + +#### SSR + +同构应用 + +- 一套代码,既可以在服务端运行,又可以在客户端运行,这就是同构应用 +- 同构是一种SSR的形态,是现代SSR的一种表现形式 + - 当用户发出请求时,先在服务器通过SSR渲染出首页的内容 + - 但是对应的代码同样可以在客户端被执行 + - 执行的目的包括:绑定事件等,同时切换页面时,也可以在客户端被渲染 + +Hydration + +> When doing SSR our pages are rendered to HTML. But HTML alone is not sufficient to make a page interactive. For example, a page with zero browser-side JavaScript cannot be interactive (there are no JavaScript event handlers to react to user actions such as click on a button.) +> +> To make our page interactive, in addition to render our page to HTML in Node.js, our UI framework (Vue/React/...) also loads and renders the page in the browser. (It creates an internal representation of the page, and then maps the internal representation to the DOM elements of the HTML we rendered in Node.js) +> +> This process is called *hydration*. Informally speaking: it makes our page interactive/alive/hydrated. + +在进行SSR时,我们的页面会呈现为HTML + +但仅仅HTML不足以使页面具有可交互性。例如:浏览器侧的JavaScript为零的页面是无法交互的,没有JavaScript事件处理程序来响应用户操作,例如单击按钮 + +为了使我们的页面具有交互性,除了在Node.js中将页面呈现为HTML,我们的UI框架还在浏览器中加载和呈现页面(它创建页面的内部表示,然后将内部表示映射到我们在Node.js中呈现的HTML的DOM元素) + +这里用一张图简单介绍一下SSR的流程: + +![SSR](./React-Hooks.assets/SSR.svg) + +### useTransition + +并不是做CSS动画的,而是用来完成过渡任务的 + +返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数 + +可以允许我们给React一些提示:某些任务的更新优先级较低,可以稍后再进行更新 + +举一个例子:用输入框内的文本实时筛选万级数据的大列表,我们改造一下之前的GiantList案例: + +```tsx +// GiantList.jsx +import React, { memo, useState } from 'react' + +const list = new Array(10000).fill(0).map((_, i) => i) + +const GiantList = memo(() => { + const [showList, setShowList] = useState(list) + const [, setKeyword] = useState('') + + function handleKeywordChange(e) { + const { value } = e.target + setKeyword(value || '') + value + ? setShowList(list.filter((item) => item.toString().includes(value))) + : setShowList(list) + } + + return ( +
+
GiantList
+ handleKeywordChange(e)} /> +
    + {showList.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ) +}) + +export default GiantList +``` + +用户在向文本框内输入数据时,能够感受到明显卡顿:明明已经按下了键盘,但是输入框内却还没有文本 + +这是因为文本框内的state更新与巨型列表的更新是同步的,二者的变化会同时反映到页面上 + +文本框内的state更新应该优先于筛选列表的展示,无论如何都应该先更新文本框,来获得更好的用户体验 + +这时就可以引入`useTransition`,将巨型列表的更新延后,变成一个“过渡任务” + +```tsx {9,15-19,26} +// GiantList.jsx +import React, { memo, useState, useTransition } from 'react' + +const list = new Array(10000).fill(0).map((_, i) => i) + +const GiantList = memo(() => { + const [showList, setShowList] = useState(list) + const [, setKeyword] = useState('') + const [pending, startTransition] = useTransition() + + function handleKeywordChange(e) { + const { value } = e.target + setKeyword(value || '') + + startTransition(() => { + value + ? setShowList(list.filter((item) => item.toString().includes(value))) + : setShowList(list) + }) + } + + return ( +
+
GiantList
+ handleKeywordChange(e)} /> +
{pending ? 'Loading...' : ''}
+
    + {showList.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ) +}) + +export default GiantList +``` + +将需要延迟更新的操作放入`startTransition`后,可以明显地发现,输入框内的文本先被更新展示到了页面上,而巨型列表的更新则会在自己的筛选操作完成后,展示到页面上 + +这里还利用`pending`做了一下加载中的状态提示 + +### useDeferredValue + +`useDeferredValue`接收一个值,并返回该值的新副本,该副本将推迟到更紧急的更新之后 + +本质上与`useTransition`是相同的目的:为了让DOM更新延迟进行 + +我们沿用之前的例子来说明它的用法 + +```tsx {9,14-16,24} +// GiantList.jsx +import React, { memo, useState, useDeferredValue } from 'react' + +const list = new Array(10000).fill(0).map((_, i) => i) + +const GiantList = memo(() => { + const [showList, setShowList] = useState(list) + const [, setKeyword] = useState('') + const deferredShowList = useDeferredValue(showList) + + function handleKeywordChange(e) { + const { value } = e.target + setKeyword(value || '') + value + ? setShowList(list.filter((item) => item.toString().includes(value))) + : setShowList(list) + } + + return ( +
+
GiantList
+ handleKeywordChange(e)} /> +
    + {deferredShowList.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ) +}) + +export default GiantList +``` + +不需要使用额外的API来显式指定哪些操作作为过渡任务延迟执行更新,只需要将原有的状态变量使用`useDeferredValue`包裹后,使用返回的值进行展示。 + +后续,对原来的状态变量进行的任何操作,当更新反映到真实DOM时都会被延迟执行 + diff --git a/docs/note/React Router.md b/docs/note/React Router.md new file mode 100644 index 00000000..c0e5bb7f --- /dev/null +++ b/docs/note/React Router.md @@ -0,0 +1,484 @@ +# React Router + +## 了解ReactRouter + +三大框架都有各自的路由实现 + +- Angular ngRouter +- React ReactRouter +- Vue VueRouter + +React Router在最近两年的版本更新较快,并且在最新的React Router6发生了较大的变化 + +- Web开发只需要安装`react-router-dom` +- `react-router`包含一些ReactNative的内容 + +```bash +npm i react-router-dom +``` + +从`react-router-dom`中导出`BrowserRouter` 或 `HashRouter`,二者分别对应history模式与哈希模式 + +将App用二者之一包裹,即可启用路由: + +```tsx {5,10,12} +// index.js +import React, { StrictMode } from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import { HashRouter } from 'react-router-dom' + +const root = ReactDOM.createRoot(document.getElementById('root')) +root.render( + + + + + +) +``` + +路由的本质是路径与组件的映射关系(`path <==> component`) + +ReactRouter不像VueRouter,它的路由映射关系是书写在组件中的: + +下面的例子中使用到了几个**组件**:`Routes` `Route` `Navigate` `NavLink` + +- `Routes` `Route`用来描述路径与组件的映射关系 + - 通过为`path`和`element`传入路径和相对应的组件,将其包裹在`Routes`内即可完成路由的描述 +- `Navigate` 导航组件(在react-router5版本中是Redirect) + - 可以帮我们完成重定向操作,将想要重定向的路径传递给组件的`to`属性 + - **当组件出现时,就会自动执行跳转**,属于功能性组件 + - 当访问根路径`/`时就会自动跳转到`/home`页 +- `NavLink`用来实现路由的跳转 + - 特殊组件,其`className` `style`这些属性都可以传递一个函数 + - 可以从函数参数中解构出`isActive`属性来动态绑定样式(实际场景应用较少) + +```tsx +// App.js +import React, { PureComponent } from 'react' +import { Routes, Route, Navigate, NavLink } from 'react-router-dom' +import Home from './views/Home' +import About from './views/About' +import NotFound from './views/NotFound' + +export default class App extends PureComponent { + render() { + return ( +
+

App

+ (isActive ? 'link-active' : '')}> + Home + + ({ color: isActive ? 'red' : '' })}> + About + + + }> + }> + }> + }> + +
+ ) + } +} +``` + +另外,这里还有一个小技巧,在最末一个路由指定一个path为`*`的路由匹配规则,可以为路由匹配添加fallback策略,当未匹配到其之前的任何域名时,会展示NotFound页面 + +## 嵌套路由 + +嵌套路由可以通过在`Route`组件内部嵌套新的`Route`组件来实现 + +再通过`Outlet`组件来指定嵌套路由的占位元素(类似于VueRouter中的router-view) + +我们在之前的例子的基础上,为Home页面添加两个子页面HomeRanking和HomeRecommand + +同时,我们也应该为Home组件添加默认跳转,就像根路径默认重定向到Home组件那样,进入到Home组件后也应该默认重定向一个子页面中,这里我们仍然使用到了Navigate组件 + +::: code-group +```tsx [App.jsx ] +// App.jsx +import React, { PureComponent } from 'react' +import { Routes, Route, Navigate, NavLink } from 'react-router-dom' +import Home from './views/Home' +import HomeRanking from './views/HomeRanking' +import HomeRecommand from './views/HomeRecommand' + +export default class App extends PureComponent { + render() { + return ( +
+ + }> + }> + }> + }> + + +
+ ) + } +} +``` +```tsx [Home.jsx] +// Home.jsx +import React, { PureComponent } from 'react' +import { NavLink, Outlet } from 'react-router-dom' + +export default class Home extends PureComponent { + render() { + return ( +
+
Home
+ Ranking + +
+ ) + } +} +``` +::: + +## 编程式导航(高阶组件) + +之前使用的ReactRouter提供的路由跳转的组件,无论是`Link`还是`NavLink`可定制化能力都比较差,无法实现“点击按钮后跳转路由”这样的需求,那么我们就需要通过编程式导航,使用JS来完成路由的跳转 + +ReactRouter提供了编程式导航的API:`useNavigate` + +自ReactRouter6起,编程式导航的API不再支持ClassComponent,全面拥抱Hooks。 + +我们将在后续的学习中开启Hooks的写法,那么目前如何在类组件中也能使用Hooks呢?答案是高阶组件 + +封装一个高阶组件`withRouter`,经过高阶组件处理的类组件的props将会携带router对象,上面包含一些我们需要的属性和方法: + +::: code-group +```tsx [withRouter.js] +// withRouter.js +import { useNavigate } from 'react-router-dom' + +export function withRouter(WrapperComponent) { + return (props) => { + const navigate = useNavigate() + const router = { navigate } + return + } +} +``` +```tsx [Home.jsx] +// Home.jsx +import React, { PureComponent } from 'react' +import { Outlet } from 'react-router-dom' +import { withRouter } from '../hoc/withRouter' + +export default withRouter( + class Home extends PureComponent { + render() { + return ( +
+
Home
+ + + +
+ ) + } + } +) +``` +::: + +我们使用`withRouter`高阶组件对Home组件进行了增强,可以通过编程式导航来实现二级路由跳转 + +这里只是展示了编程式导航的用法和高阶组件的能力,目前还是尽可能使用Hooks写法编写新项目 + +## 动态路由(路由传参) + +传递参数由两种方式: + +- 动态路由的方式 +- 查询字符串传递参数 + +动态路由是指:路由中的**路径**信息并不会固定 + +- 比如匹配规则为`/detail/:id`时,`/detail/123` `detail/888`都会被匹配上,并将`123/888`作为id参数传递 +- 其中`/detail/:id`这个匹配规则被称为动态路由 + +动态路由常见于嵌套路由跳转,比如:从歌曲列表页面点击后跳转到歌曲详情页,可以通过路由传递歌曲的ID,访问到不同歌曲的详情页 + +我们在之前的HomeRanking榜单中加入列表和点击跳转功能,并编写一个新的组件Detail来接收来自路由的参数 + +同样地,`react-router-dom`为我们提供了从路由获取参数的API:`useParams`,它是一个Hooks,我们将它应用到之前编写的高级组件`withRouter`中 + +- 在使用了`withRouter`的组件中,就可以通过`this.props.router.params.xxx`获取到当前路由中传递的参数 +- 使用动态匹配路由时,传递给Route组件的`path`属性为`:xxx`,这里是`/detail/:id` + +::: code-group +```tsx [withRouter.js] +// withRouter.js +import { useNavigate, useParams } from 'react-router-dom' + +export function withRouter(WrapperComponent) { + return (props) => { + const navigate = useNavigate() + const params = useParams() + const router = { navigate, params } + return + } +} +``` +```tsx [HomeRanking.jsx] +// HomeRanking.jsx +import React, { PureComponent } from 'react' +import { withRouter } from '../hoc/withRouter' + +export default withRouter( + class HomeRanking extends PureComponent { + render() { + const list = Array.from(Array(10), (x, i) => ({ + id: ++i, + name: `Music ${i}` + })) + return ( +
+
HomeRanking
+
    + {list.map((item, index) => ( +
  • this.props.router.navigate(`/detail/${item.id}`)}> + {item.name} +
  • + ))} +
+
+ ) + } + } +) +``` +```tsx [Detail.jsx] +// Detail.jsx +import React, { PureComponent } from 'react' +import { withRouter } from '../hoc/withRouter' + +export default withRouter( + class Detail extends PureComponent { + render() { + return ( +
+
Detail
+ Current Music ID: {this.props.router.params.id} +
+ ) + } + } +) +``` +::: + +## 查询字符串的参数 + +之前传递的是路径参数,那么查询字符串参数应该如何获取? + +可以通过`useLocation`这个Hooks拿到当前地址详细信息: + +```tsx +const location = useLocation() +location.search // ?name=ziu&age=18 +``` + +需要自行完成数据的解析,不太方便 + +还有一个Hooks:`useSearchParams`,可以在获取到查询字符串信息的同时帮我们解析成`URLSearchParams`对象 + +要从`URLSearchParams`类型的对象中取值,需要通过标准方法`get` + +```tsx +const [ searchParams, setSearchParams ] = useSearchParams() +searchParams.get('name') // 'ziu' +searchParams.get('age') // 18 +``` + +当然,我们在实际使用中也可以通过`Object.fromEntries`将它转为普通对象,这样我们使用`useSearchParams`来对之前编写的高阶组件`withRouter`做一次增强: + +```tsx {8,9} +// withRouter.js +import { useNavigate, useParams, useSearchParams } from 'react-router-dom' + +export function withRouter(WrapperComponent) { + return (props) => { + const navigate = useNavigate() + const params = useParams() + const [searchParams] = useSearchParams() + const query = Object.fromEntries(searchParams) + const router = { navigate, params, query } + return + } +} +``` + +::: tip +需要注意的是,这里的`useSearchParams`是一个Hooks的常见形态 + +它返回一个数组,数组的首位为值,数组的次位为改变值的方法 + +与对象解构不同的是,数组结构是对位解构:保证位置一致则值一致,命名随意 + +而对象解构恰恰相反,不必保证位置,而需要保证命名一致 +::: + +## 路由的配置方式 + +至此为止,路由的配置是耦合在`App.jsx`中的,我们可以将Routes这部分代码抽离出单独的组件,也可以通过配置的方式来完成路由映射关系的编写 + +- 在ReactRouter5版本中,我们可以将路由的映射规则写为JS对象,需要引入第三方库`react-router-config` +- 在ReactRouter6版本中,允许我们将其写为配置文件,不需要安装其他内容 + +6版本为我们提供了一个API:`useRoutes`,将我们编写的配置文件传入此函数,可以将其转化为之前编写的组件结构,本质上也是一种语法糖 + +需要注意的是,Hooks只能在函数式组件中使用,这里我们将App组件改用FunctionComponent书写了 + +::: code-group +```tsx [index.js] +// router/index.js +import { Navigate } from 'react-router-dom' +import Home from '../views/Home' +import HomeRanking from '../views/HomeRanking' +import HomeRecommand from '../views/HomeRecommand' +import About from '../views/About' +import Detail from '../views/Detail' +import NotFound from '../views/NotFound' + +export const routes = [ + { + path: '/', + element: + }, + { + path: '/home', + element: , + children: [ + { + path: '', + element: + }, + { + path: 'ranking', + element: + }, + { + path: 'recommand', + element: + } + ] + }, + { + path: '/about', + element: + }, + { + path: '/detail/:id', + element: + }, + { + path: '*', + element: + } +] +``` +```tsx [App.jsx] +import React from 'react' +import { NavLink, useRoutes } from 'react-router-dom' +import { routes } from './router' + +export default function App() { + return ( +
+

App

+ (isActive ? 'link-active' : '')}> + Home + + ({ color: isActive ? 'red' : '' })}> + About + + {useRoutes(routes)} +
+ ) +} +``` +::: + +## 懒加载 + +针对某些场景的首屏优化,我们可以根据路由对代码进行分包,只有需要访问到某些页面时才从服务器请求对应的JS代码块 + +可以使用`React.lazy(() => import( ... ))`对某些代码进行懒加载 + +结合之前使用到的配置式路由映射规则,我们使用懒加载对代码进行分包 + +```tsx {6,7,18,24} +// router/index.js +import { lazy } from 'react' +// import HomeRecommand from '../views/HomeRecommand' +// import About from '../views/About' + +const HomeRecommand = lazy(() => import('../views/HomeRecommand')) +const About = lazy(() => import('../views/About')) + +export const routes = [ + ... + { + path: '/home', + element: , + children: [ + ... + { + path: 'recommand', + element: + } + ] + }, + { + path: '/about', + element: + }, + ... +] +``` + +这时在终端执行`pnpm build`可以发现,构建产物为我们执行了分包,`About`和`HomeRecommand`这两个次级页面被打进了两个单独的包中 + +> 在Vue中默认为我们完成了代码分包,第三方包的代码都被打包到了`vendors`中,业务代码放到了单独的JS文件中 + +> 只有当我们访问到这些页面时,才会发起网络请求,请求这些次级页面的JS代码 + +然而如果你在react-app的构建产物`index.html`开启本地预览服务器,会发现切换到对应页面后项目会crash(本地开发也会crash) + +```bash +# 使用 serve 开启本地预览服务器 +pnpm add serve -g +serve -s build # 将 build 作为根目录 +``` + +这是因为React默认没有为异步组件做额外处理,我们需要使用`Suspense`组件来额外处理懒加载的组件 + +```tsx +// index.js +import React, { StrictMode, Suspense } from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import { HashRouter } from 'react-router-dom' + +const root = ReactDOM.createRoot(document.getElementById('root')) +root.render( + + + Loading...}> + + + + +) +``` + +当根组件内部有组件处于异步加载状态时,都会在页面上展示`Loading...`而不是崩溃掉 diff --git a/docs/note/React-Hooks.assets/SSR.svg b/docs/note/React-Hooks.assets/SSR.svg new file mode 100644 index 00000000..9c46050e --- /dev/null +++ b/docs/note/React-Hooks.assets/SSR.svg @@ -0,0 +1,16 @@ + + + + + + + Userrequest URLNode Servermatch routercomponentrequest Datainject datato HTMLUseraccept HTMLAPIServer \ No newline at end of file diff --git a/docs/note/React-Hooks.assets/useLayoutEffect.svg b/docs/note/React-Hooks.assets/useLayoutEffect.svg new file mode 100644 index 00000000..dee4a255 --- /dev/null +++ b/docs/note/React-Hooks.assets/useLayoutEffect.svg @@ -0,0 +1,16 @@ + + + + + + + ComponentComponentRerendereduseEffectRendered component(DOM)useEffect \ No newline at end of file diff --git a/docs/note/React.assets/immutable.gif b/docs/note/React.assets/immutable.gif new file mode 100644 index 00000000..c3ee60d3 Binary files /dev/null and b/docs/note/React.assets/immutable.gif differ diff --git a/docs/note/React.assets/prototype-setState.png b/docs/note/React.assets/prototype-setState.png new file mode 100644 index 00000000..175930cd Binary files /dev/null and b/docs/note/React.assets/prototype-setState.png differ diff --git a/docs/note/React.assets/react-life-cycle.png b/docs/note/React.assets/react-life-cycle.png new file mode 100644 index 00000000..2d8b6e28 Binary files /dev/null and b/docs/note/React.assets/react-life-cycle.png differ diff --git a/docs/note/React.md b/docs/note/React.md new file mode 100644 index 00000000..f027847a --- /dev/null +++ b/docs/note/React.md @@ -0,0 +1,3728 @@ +# React + +## 邂逅React + +### React开发依赖 + +- `react` 包含React的核心代码 +- `react-dom` 将React渲染到不同平台需要的核心代码 +- `babel` 将JSX转换成React代码的工具 + +为什么要拆分成这么多的包? + +- 不同的库各司其职,让库变得纯粹 +- `react`包中包含了 React Web 和 React Native 共同拥有的**核心代码** +- `react-dom` 针对Web和Native完成的事情不同 + - Web端:`react-dom`会将JSX渲染成真实DOM,展示在浏览器中 + - Native端:`react-dom`会将JSX渲染成原生的控件(如Android中的Button,iOS中的UIButton) + +### Babel与React的关系 + +Babel是什么? + +- Babel又名Babel.js +- 是目前前端使用非常广泛的编译器、转换器(Compiler/Transformer) +- 提供对ES6语法的Polyfill,将ES6语法转为大多数浏览器都支持的ES5语法 + +二者之间的联系 + +- 默认情况下React开发可以不使用Babel +- 但是我们不可能使用React.createElement来编写代码 +- 通过Babel,我们可以直接编写JSX(JavaScript XML),让Babel帮我们转化为React.createElement + +### React初体验 + +我们通过CDN方式引入react、react-dom、babel这三个依赖 + +并且创建`#root`根节点,作为渲染React组件的容器,再新建一个script标签,键入以下内容 + +```html +
+ + + + +``` + +这时,一个内容为`Hello, React!`的div标签就被渲染到页面上了 + +需要注意的是:`ReactDOM.render`这种写法适用于React18之前,在React18之后建议用下面的代码渲染根节点: + +```tsx +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render(

Hello, React!

) +``` + +### 第一个React程序 + +设想我们现在有这样一个需求:点击按钮使文本`Hello, World!`变为`Hello, React!` + +我们很容易就能写出如下代码: + +```tsx +const root = ReactDOM.createRoot(document.querySelector('#root')) +let msg = 'Hello, World!' + +render() // initial render + +function handleChangeClick() { + msg = 'Hello, React!' +} + +root.render( +
+

{msg}

+ +
+) +``` + +在Vue中,如果我们对数据进行了修改,Vue的数据响应式会自动帮我们完成视图的更新 + +然而在React中,当我们修改了数据需要通知React,让React重新渲染视图。在这里,我们可以把渲染的过程封装为一个函数,方便我们重复调用,触发重新渲染 + +```tsx +const root = ReactDOM.createRoot(document.querySelector('#root')) +let msg = 'Hello, World!' + +render() // initial render + +function handleChangeClick() { + msg = 'Hello, React!' + render() // re-render +} + +function render() { + root.render( +
+

{msg}

+ +
+ ) +} +``` + +这个案例中,我们使用`{}`语法,将动态的JS语法嵌入到JSX代码中 + +### 组件化开发 + +React有两种组件:类组件与函数组件,React18+推荐使用函数组件+Hooks + +#### 类组件 + +我们使用类组件来逐步重构上面的案例: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + msg: 'Hello, World!' + } + } + render() { + return

{this.state.msg}

+ } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +- 类组件必须实现render方法,render方法返回值为后续React渲染到页面的内容 +- 组件内数据分为两类 + - 参与页面更新的数据 + - 当数据变化时,需要触发组件重新渲染 + - 不参与页面更新的数据 + - 数据不会变化,或变化后也不需要重新渲染视图 + +- 需要触发视图重新渲染的数据,我们将其成为:**参与数据流** + - 定义在对象的`state`属性中 + - 可以通过在构造函数中通过 `this.state = { name: 'Ziu' }` 来定义状态 + - 当数据发生变化,可以调用 `this.setState` 来更新数据,通知React执行视图更新 + - update操作时,会重新调用render函数,使用最新的数据来渲染界面 + +::: tip +需要注意的是,在constructor中我们调用了`super`,因为App类是继承自React.Component类,调用`super`即调用了其父类的构造函数,让我们的App组件可以继承一些内置属性/方法如`state setState render` +::: + +至此我们完成了数据的迁移,下面我们来完成事件函数的迁移。 + +有了之前的介绍,我们很容易的可以写出下面的代码: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + msg: 'Hello, World!' + } + } + changeText() { + this.setState({ + msg: 'Hello, React!' + }) + } + render() { + return ( +
+

{this.state.msg}

+ +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +我们可以写一个实例方法changeText来修改msg,然而,此时我们点击按钮后发现,案例不能正常工作。 + +如果在changeText中打log,会发现函数被正常触发了,但是状态没有更新 + +为什么this.setState失效了?这和this的绑定有关:绑定的`changeText`在被调用时,向上找`this`找到的是全局的`this`即`undefined` + +这种情况有点类似于下面的例子: + +```ts +const app = new App() +app.changeText() // this => app + +const func = app.changeText +func() // this => undefined +``` + +在非严格模式下,直接调用func时的this指向的是window,严格模式下则为undefined + +为了解决this绑定的问题,我们需要显式把函数调用绑定给当前组件,这时组件就可以正常工作了。 + +```tsx {17} +class App extends React.Component { + constructor() { + super() + this.state = { + msg: 'Hello, World!' + } + } + changeText() { + this.setState({ + msg: 'Hello, React!' + }) + } + render() { + return ( +
+

{this.state.msg}

+ +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +### 提前绑定this + +在render函数中频繁通过`.bind`毕竟不太优雅,好在也有另一种方式:可以在constructor中提前对实例方法进行this绑定: + +```tsx {7,11} +... +constructor() { + super() + this.state = { + msg: 'Hello, World!' + } + this.changeText = this.changeText.bind(this) +} +render() { + ... + + ... +} +... +``` + +### 列表渲染 + +可以通过循环,将数组渲染到视图中 + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + movieList: [ + 'The Shawshank Redemption', + 'The Godfather', + 'The Godfather: Part II', + 'The Dark Knight' + ] + } + } + + render() { + return ( +
    + {this.state.movieList.map((item) => ( +
  • {item}
  • + ))} +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +需要注意的是,这里绑定的key的功能类似于Vue中的特殊属性key,它用来帮助React对列表渲染进行更高效的更新。 + +### 计数器案例 + +结合之前的知识,可以实现一个简单的计数器 + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + count: 0 + } + this.addCount = this.addCount.bind(this) + this.subCount = this.subCount.bind(this) + } + + addCount() { + this.setState({ + count: this.state.count + 1 + }) + } + + subCount() { + this.setState({ + count: this.state.count - 1 + }) + } + + render() { + const { count } = this.state + + return ( +
+

Count: {count}

+ + +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +## 认识JSX语法 + +- 认识JSX语法 +- JSX基本使用 +- JSX事件绑定 +- JSX条件渲染 +- JSX列表渲染 +- JSX的原理与本质 + +是因为我们给script标签添加了`type="text/babel"`属性,浏览器不会对这个script进行解析,当babel被加载完成后,babel会在页面中寻找`type="text/babel"`的script标签进行转义,转义后的代码才会被浏览器执行 + +- JSX: JavaScript Extension / JavaScript XML +- All in JS +- 不同于Vue的模板语法 不需要专门学习模板语法中的指令(v-for/v-if/v-bind) + +### JSX的使用 + +#### 书写JSX的规范与注意事项 + +- JSX的顶层只能有一个根元素 元素必须包裹在单独的闭合标签中 + - 后续会接触到Fragment标签 Vue3也是将元素包裹在了Fragments标签中 +- 为了方便阅读 通常在JSX外层包裹一个小括号`()`方便阅读 + +#### JSX的注释 + +在JSX中编写注释,需要以`{/* ... */}`的形式,在`.jsx/.tsx`文件中,通过快捷键就可以快捷的生成注释内容 + +本质上是通过花括号语法`{}`嵌入了一段JavaScript表达式,在表达式中书写注释 + +```tsx{4} +... +return ( +
+ {/* Some Comment... */} +

Count: {count}

+ + +
+) +... +``` + +#### JSX嵌入变量作为子元素 + +可以通过花括号语法将变量内容嵌入到JSX语法中: + +```tsx +const message = 'Hello, React!' +const arr = ['abc', 'cba', 'nba'] + +return ( +
+

{ message }

+
{ arr }
+
+) +``` + +- 变量类型为number string array类型时,可以直接展示 +- 变量类型为null undefined boolean类型时,内容为空 + - 如果希望可以展示null/undefined/boolean类型,需要通过`.toString()`方法将其转为字符串 + - 空字符串拼接、String构造函数等方式 +- Object对象类型不能作为子元素 (Objects are not valid as a React child) + +下例中,只有number类型会被正常展示,而其余变量则不会展示在视图中 + +```tsx +render() { + const number = 123 + const n = null + const u = undefined + const b = true + + return ( +
+
+ Number: {number} +
+
+ Null: {n} +
+
+ Undefined: {u} +
+
+ Boolean: {b} +
+
+ ) +} +``` + +将对象类型变量嵌入到JSX语法中,React会抛出错误: + +```tsx {6} +... +render() { + const obj = { name: 'Ziu' } + return ( +
+ { obj } +
+ ) +} +... +``` + +#### JSX的属性绑定 + +- 在Vue中我们通过`v-bind`绑定属性 +- 在React中如何绑定元素属性? +- `title` `src` `href` `class` 内联`style`等 + +下例中,我们通过花括号语法对元素的属性进行了动态绑定,点击按钮可以切换className状态 + +同时,动态绑定的内联样式也会发生改变,通过花括号语法动态绑定style属性 + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isActive: false, + title: 'Description' + } + this.changeActive = this.changeActive.bind(this) + } + + changeActive() { + this.setState({ + isActive: !this.state.isActive + }) + } + + render() { + const { isActive, title } = this.state + const classList = ['title', isActive ? 'active' : ''] + + return ( +
+
+ Hello, React! +
+ +
+ ) + } +} +``` + +当我们通过脚手架创建项目时,可以使用第三方库来帮我们完成className的绑定 + +- `classnames`库 `pnpm add classnames` +- 提供了多种创建className的语法 + +### JSX事件绑定 + +先前的例子中,我们已经通过`onClick`给按钮绑定过事件处理函数了,其中涉及了this绑定 + +回顾一下this的四种绑定规则: + +1. 默认绑定 独立执行 foo() this => undefined +2. 隐式绑定 被一个对象执行 obj.foo() this => obj +3. 显式绑定 call/bind/apply foo.call('aaa') this => String('aaa') +4. new绑定 new Foo() 创建一个新对象,并且赋值给this + +除了之前通过`function + bind`绑定事件处理函数的方式,还可以通过箭头函数来帮我们完成处理 + +箭头函数的内部使用this时会自动向上层作用域查找this 实际开发中这种方式并不常用 + +```tsx {2} +... +changeActive = () => { + this.setState({ + isActive: !this.state.isActive + }) +} +... +``` + +相比之下更推荐使用的,是下面这种方式: + +```tsx {2} +... + +... +``` + +这样书写有几种好处: + +- 给事件处理函数传递参数更方便 +- 书写更方便 不必主动考虑this绑定问题 + +它的原理是,我们对外暴露的本质上是一个箭头函数,当调用箭头函数时,本质上是执行`this.changeActive`,这是 一种隐式绑定,找到的this为当前组件实例 + +### 事件绑定参数传递 + +- Event参数传递 +- 额外参数传递 + +事件回调函数的第一个默认参数就是Event对象,这个Event对象是经过React包装后的,但是原生的属性都包含在内,React对其进行了一些扩展 + +```tsx {13} +... +changeActive(ev) { + console.log('Event', ev) +} + +render() { + return ( +
+ {/* event将作为默认入参传递给changeActive */} + + + {/* 通过箭头函数绑定事件监听回调函数时 需要手动透传一下event */} + +
+ ) +} +... +``` + +当我们需要传递额外的参数时,通过箭头函数传递也更容易: + +```tsx {13} +changeActive(ev, name, age) { + console.log('Event', ev) + console.log('Name', name) + console.log('Age', age) +} + +render() { + return ( +
+ {/* NOT Recommand */} + + {/* Recommand */} + +
+ ) +} +``` + +需要注意,当通过`.bind`传递额外参数时,最后一个参数才是默认传递的Event对象,这会导致非预期行为 + +```sh +> Event 'Ziu' +> Name 18 +> Age {Event} +``` + +### JSX事件绑定案例 + +创建一个Tab栏,选中哪个选项,哪个选项被激活切换为红色,同一时间仅有一个激活项目 + +结合之前学习的内容,很容易就可以写出下述 代码: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + activeIndex: 0, + tabList: ['Home', 'Recommend', 'Hot', 'User'] + } + } + + changeActive(index) { + this.setState({ + activeIndex: index + }) + } + + render() { + const { activeIndex, tabList } = this.state + + return ( +
+
+ {tabList.map((item, index) => ( + + ))} +
+
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +### 条件渲染 + +控制元素按照某种条件渲染,以加载状态为例 + +列表未加载出来时,展示`加载中`,加载完毕则渲染完整内容: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isLoading: true + } + } + + changeLoading() { + this.setState({ + isLoading: !this.state.isLoading + }) + } + + render() { + const { isLoading } = this.state + + return ( +
+ {isLoading ? ( +
Loading ...
+ ) : ( +
Some Content
+ )} + +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +常用的条件渲染方式 + +- `if/else/else-if` + - 适合判断逻辑较复杂的情况 将条件渲染抽离出来 +- 三元运算符 `?:` + - 适合判断逻辑简单的情况 +- 逻辑与运算符 `&&` + - 如果条件成立则渲染某个组件,否则什么内容都不渲染 +- 可选链 `user?.info?.name` + +下例中通过逻辑与运算符`&&`决定`VIP`标签是否展示在视图中 + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isVip: false + } + } + + changeVip() { + this.setState({ + isVip: !this.state.isVip + }) + } + + render() { + const { isVip } = this.state + + return ( +
+
+ username: Ziu + {isVip && VIP } +
+ + +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +#### 在React中简单写一个"v-show" + +`v-show`是Vue提供的语法糖,不同于`v-if`,它只切换元素的`display`属性。 + +下面我们在React中简单复现一个`v-show`的效果: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isShow: true + } + } + + changeShow() { + this.setState({ + isShow: !this.state.isShow + }) + } + + render() { + const { isShow } = this.state + + return ( +
+
Target Element
+ +
+ ) + } +} +``` + +实际使用中,将其封装为hooks来调用更具通用性,也更方便管理 + +### 列表渲染中的高阶函数 + +- `filter`函数 过滤器 +- `slice`函数 分页 +- `sorc`函数 排序 +- ... + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + stuList: [ + { name: 'Ziu', age: 18, score: 88 }, + { name: 'Kobe', age: 19, score: 59 }, + { name: 'Why', age: 20, score: 61 }, + { name: 'James', age: 21, score: 99 } + ] + } + } + + render() { + const { stuList } = this.state + + // 及格的学生 + const passStuList = stuList.filter((item) => item.score >= 60) + + // 分数最高的两个学生 + const top2StuList = stuList.sort((a, b) => b.score - a.score).slice(0, 2) + + return ( +
+
+ {stuList.map(({ name, age, score }) => ( +
+ {name} + {age} + {score} +
+ ))} +
+
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +### JSX的本质 + +#### JSX的转换过程 + +假设我们有下面的JSX代码: + +```tsx +class App extends React.Component { + constructor() { + super() + } + + render() { + const page = ( +
+
Header
+
+ Content +
Banner
+
    +
  • Item 1
  • +
  • Item 2
  • +
  • Item 3
  • +
  • Item 4
  • +
  • Item 5
  • +
+
+
Footer
+
+ ) + console.log(page) + return
{page}
+ } +} +``` + +通过JSX语法描述出来的template会经过Babel转化,转化为JavaScript树的数据结构 + +在控制台中我们可以看到,子节点都存放进了父节点的`props.children`中 + +#### 虚拟DOM树 + +JSX仅仅是`React.createElement(component, props, ...children)`的语法糖 + +所有的JSX语法都会被Babel转化为这样的命令式语法 + +.createElement函数的参数 + +- type + - 当前ReactElement的类型 + - 如果是标签元素,值为字符串如:`"div"` + - 如果是组件元素,那么值为组件的名称 +- config + - 所有JSX中绑定的属性都在config中以键值对的形式存储 + - 例如`className` => `class` + +我们借助Babel官网的Playground来检查一下JSX语法的转化 + +```js +import { jsx as _jsx } from "react/jsx-runtime"; +import { jsxs as _jsxs } from "react/jsx-runtime"; +const page = /*#__PURE__*/_jsxs("div", { + className: "page", + children: [/*#__PURE__*/_jsx("div", { + className: "header", + children: "Header" + }), /*#__PURE__*/_jsxs("div", { + className: "content", + children: ["Content", /*#__PURE__*/_jsx("div", { + className: "banner", + children: "Banner" + }), /*#__PURE__*/_jsxs("ul", { + children: [/*#__PURE__*/_jsx("li", { + children: "Item 1" + }), /*#__PURE__*/_jsx("li", { + children: "Item 2" + }), /*#__PURE__*/_jsx("li", { + children: "Item 3" + }), /*#__PURE__*/_jsx("li", { + children: "Item 4" + }), /*#__PURE__*/_jsx("li", { + children: "Item 5" + })] + })] + }), /*#__PURE__*/_jsx("div", { + className: "footer", + children: "Footer" + })] +}); +console.log(page); +``` + +这时经过Babel转义后的纯JS函数,这段函数可以在浏览器中直接运行 + +如果移除了相关JSX代码,并将他们都替换为`React.createElement`函数调用,那么得到的代码也可以直接在浏览器中运行。Babel帮助我们完成了转化,提高了开发效率,相比于通过调用`React.createElement`来描述视图,通过JSX编写的代码更加容易维护 + +这些代码最终形成的就是虚拟DOM树,React可以将虚拟DOM渲染到页面上,形成真实DOM + +虚拟DOM允许React可以通过diff算法,高效地对真实DOM树进行更新 + +### 声明式编程 + +- 虚拟DOM帮我们从命令式编程转到了声明式编程的模式 +- 对虚拟DOM作何处理,如何渲染是由React决定的,由于做了一层抽象,那么同样可以将虚拟DOM渲染成原生组件(React Native) + +### 购物车案例 + +下面写一个经典的购物车案例 + +```tsx +function formatPrice(price) { + return `$ ${price.toFixed(2)}` +} + +class App extends React.Component { + constructor() { + super() + this.state = { + books: [ + { name: 'book1', author: 'author1', price: 100, count: 0 }, + { name: 'book2', author: 'author2', price: 200, count: 0 }, + { name: 'book3', author: 'author3', price: 300, count: 0 }, + { name: 'book4', author: 'author4', price: 400, count: 0 } + ] + } + } + + changeCount(index, count) { + this.setState((state) => { + const books = [...state.books] + books[index].count += count + return { books } + }) + } + + removeItem(index) { + this.setState((state) => { + const books = [...state.books] + books.splice(index, 1) + return { books } + }) + } + + getTotal() { + const { books } = this.state + return books.reduce((acc, { price, count }) => acc + price * count, 0) + } + + renderBookCart() { + const { books } = this.state + const total = this.getTotal() + return ( +
+

Shopping Cart

+
+ {books.map(({ name, author, price, count }, index) => ( +
+ {index + 1} + {name} + {author} + {formatPrice(price)} + + + {count} + + + +
+ ))} +
+
+ Total: {formatPrice(total)} +
+
+ ) + } + + renderEmptyTip() { + return
Shopping Cart is Empty
+ } + + render() { + const isEmpty = this.state.books.length === 0 + + return !isEmpty ? this.renderBookCart() : this.renderEmptyTip() + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +## React项目开发 + +- 认识脚手架工具 +- create-react-app +- 创建React项目 +- Webpack的配置 + +### React脚手架 + +类似于Vue提供的 `pnpm create vite` 创建一个模板,React也可以通过 `create-react-app` 来初始化一个空的React模板 + +```sh +pnpm add create-react-app -g # 全局安装create-react-app +create-react-app react-app # 创建一个名为react-app的React项目 +# 删除node_modules package-lock.json +cd react-app +pnpm i # 使用pnpm重新安装依赖 +``` + +```tsx +// index.js +import ReactDOM from 'react-dom/client' +import App from './App' + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() + +// App.jsx +import { Component } from 'react' + +export default class App extends Component { + render() { + return
Hello, React!
+ } +} +``` + +## React组件化开发 + +- React组件生命周期 +- React组件间通信 +- React组件插槽 +- React非父子的通信 +- setState使用详解 + +组件化是React的核心思想之一,组件化是一个抽象的过程,将大的应用程序抽象为多个小的组件,最终形成组件树 + +分而治之,让代码更容易组织和管理 + +React组件相对于Vue更加灵活多样,按照不同的方式可以分为多种组件 + +- 根据组件定义方式,可以分为:函数组件(Functional Component)与类组件(Class Component) +- 根据组件内部是否有状态需要维护,可以分为:无状态组件(Stateless Component)和有状态组件(Stateful Component) +- 根据组件的不同职责,可以分为:展示型组件(Presentational Component)和容器型组件(Container Component) + +除此之外,还有异步组件、高阶组件等... + +### 类组件 + +- 类组件的定义有以下要求: + - 组件的名称必须为大写(无论是类组件还是函数组件) + - 类组件需要继承自React.Component + - 类组件内必须实现render函数 +- 通过class关键字定义一个组件 + - constructor是可选的,通常需要在constructor中初始化一些数据 + - this.state中维护的数据就是组件内部的数据 + - **render方法是class组件中唯一必须实现的方法** + +#### render函数 + +- render函数在组件第一次渲染时被调用 +- 当`this.props`或`this.state`发生变化时被调用 + +被调用时组件内会检查`this.props`和`this.state`是否发生变化,并且返回下面的返回值之一: + +- React元素 + - 通常通过JSX创建 + - 例如`
`会被React渲染为DOM节点,而``会被React渲染为自定义组件 + - 无论是`
`还是``,他们都为React元素 +- 数组或Fragments组件 + - 允许通过render方法同时返回多个元素 +- 字符串或数字 + - 元素会被渲染 +- boolean/null/undefined类型 + - 元素不会被渲染 + +### 函数组件 + +函数组件不需要继承自任何父类,函数的返回值相当于render函数的返回值,表示组件要渲染的内容 + +修改前文中编写的`App.jsx`即可: + +```tsx +// App.jsx +export default function App() { + return
Hello, React!
+} +``` + +- 函数组件是使用function定义的函数,函数的返回值会返回与render函数相同的内容,表示组件要渲染的内容 +- 函数组件有自己的特点(在无hooks的情况下,引入hooks后函数组件与类组件一样强大) + - 没有生命周期,也会被更新并挂在,但是没有生命周期函数 + - this关键字不能指向组件实例,因为没有组件实例 + - 没有内部状态(state) + +## 组件的生命周期 + +我们需要在组件的不同生命周期中执行不同的操作,比如添加解除监听器、发起网络请求等 + +![React Life Cycle: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/](./React.assets/react-life-cycle.png) + +结合上图,解读一下组件的完整生命周期: + +- 组件挂载后 调用构造方法 constructor +- 执行 render 方法 +- 组件挂载完毕 `componentDidMount` +- 后续,当props发生修改 或调用了setState触发state改变 或调用forceUpdate触发组件更新 + - 重新执行render函数 根据修改后的最新状态更新视图 + - React帮我们更新DOM和refs + - 更新回调 `componentDidUpdate` 被调用 +- 组件卸载 一般是条件渲染切换路由时发生卸载 +- 组件被卸载前 `componentWillUnmount` 被调用 + - 可以用来执行一些清理副作用的操作 + - 如解除监听器等 + +总结一下常用的生命周期钩子: + +- `componentDidMount` 组件挂载后 +- `componentDidUpdate` 组件更新后 +- `componentWillUnmount` 组件卸载前 + +```tsx +// LifeCycle.jsx +import { Component } from 'react' + +export default class LifeCycle extends Component { + constructor() { + super() + this.state = { + count: 0 + } + } + + addCount = () => { + this.setState({ + count: this.state.count + 1 + }) + } + + componentDidMount() { + console.log('LifeCycle componentDidMount') + } + + componentDidUpdate() { + console.log('LifeCycle componentDidUpdate') + } + + componentWillUnmount() { + console.log('LifeCycle componentWillUnmount') + } + + render() { + console.log('LifeCycle render') + return ( +
+

LifeCycle

+

{this.state.count}

+ +
+ ) + } +} +``` + +### constructor + +一般来讲 constructor 中只完成两件事情 + +- 给this.state赋初值 初始化组件内部状态 +- 为事件处理函数绑定实例(.bind(this)) + +如果不初始化state或不进行方法绑定,则不需要为React组件实现构造函数 + +### componentDidMount + +该生命周期钩子会在组件挂载后被立即调用,相当于Vue中的onMounted + +在该生命周期钩子中可以获取到组件的DOM结构,通常在其中完成以下操作: + +- 依赖于DOM的操作 需要操作DOM +- 在此处发送网络请求 (Official Recommend) +- 在此处添加一些订阅监听回调 (在 componentWillUnmount 中取消订阅) + +### componentDidUpdate + +会在组件更新后被立即调用,首次渲染不会执行此方法 + +- 每次组件发生更新后,可以在此回调中对DOM进行操作 +- 如果对更新前后的props进行了比较,也可以选择在此处进行网络请求 + - 比如当props未发生改变,则不执行网络请求 + +### componentWillUnmount + +组件卸载及销毁之前调用 + +- 在此回调中执行必要的清理操作 +- 例如 清除timer 取消网络请求 或取消在 componentDidMount 中创建的订阅等 + +### 不常用的生命周期 + +- static getDeivedStateFromProps + - state的值在任何时候都依赖props时使用,该方法返回一个对象来更新state +- shouldComponentUpdate + - 对外部条件进行显式比较 决定是否需要对组件进行更新 + - 在此生命周期回调中返回false时 不会触发re-render 可以完成一些性能优化 +- getSnapshotBeforeUpdate + - 在更新前获取快照 用于更新DOM前对部分数据进行保存 + - 比如在DOM更新前获取并保存当前滚动位置 + +## 组件间通信 + +组件间通过props通信 + +- 父组件通过直接在子组件上添加属性 `title={someValue}` 传递数据 +- 子组件中通过 props 参数获取父组件传递来的数据 + +需要注意的是,子组件中需要通过 `super(props)` 将props注册给父类,这样才能通过`this.props`获取到props + +但是默认情况下React帮我们完成了这个操作,我们也就不必手动在constructor写了 + +```tsx +// Header.jsx +import React, { Component } from 'react' + +export default class Header extends Component { + // constructor(props) { + // super(props) + // } + + render() { + const { title, count, tabs } = this.props + + return ( +
+

Title: {title}

+

Count: {count}

+
    + {tabs.map((tab, index) => ( +
  • {tab}
  • + ))} +
+
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import Header from './components/Header' + +export default class App extends Component { + render() { + return ( +
+
+
+ ) + } +} +``` + +上文中的例子我们从父组件向子组件传递数据,但是数据都为静态的 + +我们再完成一个动态数据的绑定,用到了axios请求网络数据,并将数据动态传递给子组件 + +在父组件的 componentDidMount 中发起网络请求,获取到 postList 后通过props动态传递给子组件 Content 展示出来 + +```tsx +// Content.jsx +import React, { Component } from 'react' + +export default class Content extends Component { + render() { + const { postList } = this.props + + return ( +
+
    + {postList.map((post) => { + return
  • {post.title}
  • + })} +
+
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import axios from 'axios' +import Content from './components/Content' + +export default class App extends Component { + constructor() { + super() + this.state = { + postList: [] + } + } + + componentDidMount() { + axios.get('https://jsonplaceholder.typicode.com/posts').then((res) => { + this.setState({ + postList: res.data + }) + }) + } + + render() { + const { postList } = this.state + return ( +
+ +
+ ) + } +} +``` + +### 子组件向父组件通信 + +除了父组件向下传递数据,子组件也需要向上传递数据给父组件。 + +在React中是通过父组件提供给子组件一个回调函数,在子组件中调用回调函数,从而达到子组件向父组件通信的目的 + +父组件在提供数据状态 `count` 的同时,也提供了增减 `count` 的回调函数 `addCount` 和 `subCount`,子组件通过调用回调即可修改状态值 + +```tsx +// Counter.jsx +import React, { Component } from 'react' + +export default class Counter extends Component { + render() { + const { count, addCount, subCount } = this.props + return ( +
+ + {count} + +
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import Counter from './components/Counter' + +export default class App extends Component { + constructor() { + super() + this.state = { + count: 0 + } + } + + addCount = () => { + this.setState({ + count: this.state.count + 1 + }) + } + + subCount = () => { + this.setState({ + count: this.state.count - 1 + }) + } + + render() { + const { count } = this.state + return ( +
+ +
+ ) + } +} +``` + +### 参数propTypes + +我们可以对props传递值的类型做限制 (目前官方已不再推荐使用prop-types 建议直接上TypeScript) + +- 如果项目中默认集成了Flow或TypeScript,可以直接进行类型验证 +- 如果没有集成,则可以通过 prop-types 库来进行参数类型验证 +- 从React v15.5起,React.PropTypes独立成为了一个npm包 prop-types 库 + +```sh +pnpm add prop-types +``` + +以之前的类组件 Header 为例,为其添加类型限制: + +```tsx {3,27-31} +// Header.jsx +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class Header extends Component { + // constructor(props) { + // super(props) + // } + + render() { + const { title, count, tabs } = this.props + + return ( +
+

Title: {title}

+

Count: {count}

+
    + {tabs.map((tab, index) => ( +
  • {tab}
  • + ))} +
+
+ ) + } +} + +Header.propTypes = { + title: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + tabs: PropTypes.array.isRequired +} + +Header.defaultProps = { + title: 'Default Title', + count: 0 +} +``` + +- 可以直接在组件类上添加`.propsType`为其添加类型检查 +- 也可以添加`.defaultProps`为其传入默认值 + +需要注意的是,这里的类型限制和Vue做的defineProps类型限制是类似的,如果没有IDE Extension做额外检查,其类型检查都是在运行时进行的 + +如果props类型发生不匹配,在运行时会在控制台抛出错误,而编译是可以正常完成的 + +> Warning: Failed prop type: Invalid prop `title` of type `number` supplied to `Header`, expected `string`. + +相比之下,TypeScript可以完成静态的类型检查,帮助我们更早的发现错误 + +### 组件通信案例 Tab栏切换 + +展示一个Tabs,点击切换页面,并切换不同的Tab激活状态。 + +切换activeIndex后,触发Tabs组件和下方Pages组件的重新渲染 + +这里对className的拼接可以用第三方库 classnames 替换 + +```tsx +// Tabs.jsx +import React, { Component } from 'react' + +export default class Tabs extends Component { + render() { + const { tabs, activeIndex, changeTab } = this.props + + return ( +
+ {tabs.map((tabName, index) => ( +
+ {tabName} +
+ ))} +
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import Tabs from './components/Tabs' + +export default class App extends Component { + constructor() { + super() + this.state = { + tabs: ['Home', 'Hot', 'Category', 'Profile'], + activeIndex: 0 + } + } + + changeTab = (index) => () => { + this.setState({ + activeIndex: index + }) + } + + render() { + const { tabs, activeIndex } = this.state + return ( +
+ + {tabs[activeIndex] === 'Home' &&

Home

} + {tabs[activeIndex] === 'Hot' &&

Hot

} + {tabs[activeIndex] === 'Category' &&

Category

} + {tabs[activeIndex] === 'Profile' &&

Profile

} +
+ ) + } +} +``` + +## React中的插槽 + +React并不存在插槽的概念,但是可以通过`props.children`来实现类似的效果 + +- 可以通过向子组件传递`props.children`子元素来决定子组件内渲染何种内容的标签 +- 我们在子组件标签内书写的内容都会默认作为`props.children`传递给子组件 + +### 通过children实现插槽 + +实现一个导航栏NavBar组件,左中右布局,渲染内容由父组件决定 + +需要注意的是 如果只传入了一个子标签,那么`props.children`不再是一个数组,需要对此做额外判断 + +```tsx +// NavBar.jsx +import React, { Component } from 'react' + +export default class NavBar extends Component { + render() { + const { children } = this.props + + Array.isArray(children) || (children = [children]) + + return ( +
+
{children[0]}
+
{children[1]}
+
{children[2]}
+
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import NavBar from './components/NavBar' + +export default class App extends Component { + render() { + return ( +
+ + Back +
Search
+
Menu
+
+
+ ) + } +} +``` + +### 通过props实现插槽 + +相比于通过`props.children`传递插槽,通过props实现的插槽更具确定性 + +```tsx +// NavBar.jsx +import React, { Component } from 'react' + +export default class NavBar extends Component { + render() { + const { left, center, right } = this.props + + return ( +
+
{left}
+
{center}
+
{right}
+
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import NavBar from './components/NavBar' + +export default class App extends Component { + render() { + const left = Back + const center =
Search
+ const right =
Menu
+ + return ( +
+ +
+ ) + } +} +``` + +### 作用域插槽 + +在Vue中,可以通过作用域插槽,在父组件插槽内容中注入插槽的数据 + +- 标签与结构由父组件决定 +- 数据内容由子组件对外暴露 + +重写之前的Tabs例子,可以将插槽传递的内容由静态的React元素变为一个函数,这样在子组件内部就可以通过函数传参,动态地对外暴露数据 + +之前每个Tab使用`span`标签书写的,通过作用域插槽,我们将它通过`button`标签渲染出来 + +```tsx{6,22,57} +// Tabs.jsx +import React, { Component } from 'react' + +export default class Tabs extends Component { + render() { + const { tabs, activeIndex, changeTab, tabSlot } = this.props + + return ( +
+ {tabs.map((tabName, index) => ( +
+ {tabSlot ? tabSlot(tabName) : tabName} +
+ ))} +
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import Tabs from './components/Tabs' + +export default class App extends Component { + constructor() { + super() + this.state = { + tabs: ['Home', 'Hot', 'Category', 'Profile'], + activeIndex: 0 + } + } + + changeTab = (index) => () => { + this.setState({ + activeIndex: index + }) + } + + render() { + const { tabs, activeIndex } = this.state + return ( +
+ } + > + {tabs[activeIndex] === 'Home' &&

Home

} + {tabs[activeIndex] === 'Hot' &&

Hot

} + {tabs[activeIndex] === 'Category' &&

Category

} + {tabs[activeIndex] === 'Profile' &&

Profile

} +
+ ) + } +} +``` + +## Context跨组件传参 + +非父子组件之间的数据共享 + +- props层层传递 跨组件会很不方便 对于中间那些本不需要这些props数据的组件是冗余的 +- 第三方状态库 外置于React 如Redux (实际开发中较为常用) +- 事件总线 ... + +针对跨组件传参的场景,React提供了一个API名为Context + +- Context 提供了一个在组件之间共享此类值的方式,而不是显式地通过组件树逐层传递props +- 使用 Context 共享那些全局的数据,如主题色、用户登录状态、locales等 + +### 用Context实现跨组件传参 + +假设有App Profile UserCard三个嵌套组件,我们希望App中的 `isDarkMode` 状态能够透传到UserCard组件中 + +- 全局通过 `createContext` 创建一个上下文 +- 根组件通过 `DarkModeContext.Provider` 标签与 `value` 传递值到上下文中 +- 需要使用到该值的子组件通过 `UserCard.contextType = DarkModeContext` 绑定到上下文 +- 随后即可在子组件中通过 `this.context` 获取到此上下文当前绑定的状态值 + +::: code-group +```tsx [context.js] +// context.js +import { createContext } from 'react' + +export const DarkModeContext = createContext() +``` + +```tsx [App.jsx] +// App.jsx +import React, { Component } from 'react' +import Profile from './components/Profile' +import { DarkModeContext } from './context' + +export default class App extends Component { + constructor() { + super() + this.state = { + darkMode: false + } + } + + changeDarkMode = () => { + this.setState({ darkMode: !this.state.darkMode }) + } + + render() { + const { darkMode } = this.state + + return ( + + + + + ) + } +} + +// Profile.jsx +import React, { Component } from 'react' +import UserCard from './UserCard' + +export default class Profile extends Component { + render() { + return ( +
+ +
+ ) + } +} +``` + +```tsx [UserCard.jsx] +// UserCard.jsx +import React, { Component } from 'react' +import { DarkModeContext } from '../context' + +export default class UserCard extends Component { + render() { + return ( +
+

UserCard

+ {this.context ?

Dark Mode

:

Light Mode

} +
+ ) + } +} + +UserCard.contextType = DarkModeContext +``` +::: + +在类组件中可以通过Context共享数据,而函数组件中的this并没有指向组件实例,那么在函数式组件中应当如何使用? + +用函数式组件重写一下 UserCard + +```tsx +// UserCard.jsx +import { DarkModeContext } from '../context' + +export default function UserCard() { + return ( + + {(context) => ( +
+

UserCard

+ {context ?

Dark Mode

:

Light Mode

} +
+ )} +
+ ) +} +``` + +如果同时需要共享多个状态,Provider可以嵌套,那么在子组件中可以通过不同的Context.Consumer获取到不同的全局上下文,执行不同的操作,展示不同的内容 + +### React.createContext + +- 创建一个需要共享的Context对象 +- 如果一个组件订阅了Context,那么这个组件会从自身最近的那个匹配的Provider中读取到当前的context值 +- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值 +- `const SomeContext = React.createContext(defaultValue)` + +### Context.Provider + +- 每个Context对象都会返回一个Provider组件,它允许消费组件订阅Context的变化 +- Provider接收一个value属性,用于将变化的值传递给消费组件Consumer +- 一个Provider可以与多个Consumer创建关系 +- 多个Provider可以嵌套使用,内层数据会覆盖外层数据 +- 当Provider的value发生变化时,其内部的所有Consumer组件都会重新渲染 + +### Class.contextType + +- 挂载在类组件上的 `contextType` 属性会被重新赋值为一个由 `React.createContext` 创建的Context对象 +- 这允许你在类组件中通过 `this.context` 获取到**最近的Context**的值 +- 任何生命周期都能访问到这个值 + +### Context.Consumer + +- 帮你在**函数式组件**中完成订阅context (函数式组件中没有this) +- 当Consumer订阅到context变更,会触发其内部传递的函数 +- 传入Consumer的函数接收当前的context值,返回一个React元素节点 + +### 关于defaultValue + +什么时候会用到创建Context时传入的defaultValue? + +如果子组件通过 `this.context` 向上查找时没有找到相应的Provider,则使用Context的默认值 + +```tsx{10} +... + render() { + const { darkMode } = this.state + + return ( + <> + + + + + + ) + } +... +``` + +### props属性展开 + +如果我们希望将一个对象中的所有属性都作为props传递给子组件,可以在子组件标签上直接展开该对象 + +类似于Vue中的`v-bind="childProps"`,一次绑定所有属性到子组件 + +```tsx{6} +... + render() { + const { childProps } = this.state + return ( +
+ +
+ ) + } +... +``` + +如果你确实希望层层传递props来实现跨组件通信,那么可以在render函数中直接将`this.props`进行属性展开,虽然不推荐这样的做法: + +```tsx +// App.jsx + +// Profile.jsx + +// UserCard.jsx + +// Details.jsx +
+... +``` + +## EventBus跨组件通信 + +很多第三方库实现了时间发布订阅,如 `tiny-emitter` + +可以借助EventBus完成全局状态共享: + +- 在 App.jsx 中点击按钮 触发全局事件并携带payload +- 当 Player 组件挂载时 `componentDidMount` 添加play事件的监听 +- 当 Player 组件卸载时 `componentWillUnmount` 移除play事件的监听 +- 在 Player 组件中展示当前播放的音乐 + +```tsx +// App.jsx +import React, { Component } from 'react' +import Player from './components/Player' +import { emit } from './eventbus' + +export default class App extends Component { + play = () => { + emit('play', { musicName: 'Hello, Music' }) + } + + render() { + return ( + <> + + + + ) + } +} + +// Player.jsx +import React, { Component } from 'react' +import { on, off } from '../eventbus' + +export default class Player extends Component { + constructor() { + super() + this.state = { + musicName: '' + } + } + + playMusic = ({ musicName }) => { + console.log('Music Play: ' + musicName) + this.setState({ musicName }) + } + + componentDidMount() { + on('play', this.playMusic) + } + + componentWillUnmount() { + off('play', this.playMusic) + } + + render() { + return ( +
+

Player

+

Now Playing: {this.state.musicName}

+
+ ) + } +} +``` + +## setState的使用详解 + +不同于Vue的自动追踪依赖,React是通过用户调用`setState`来获知状态的更新,所以开发者要更新状态不能直接`this.state.xxx = 'xxx'`,而必须通过`setState`方法。这样React在内部才能获知状态的更新,继而触发对视图的更新。 + +从React的源码可以看到,setState方法是从Component集继承而来的 + +![setState in source code of React](./React.assets/prototype-setState.png) + +### setState的异步更新 + +当调用 `setState` 时,方法会使用 `Object.assign()` 方法将新旧state合并 + +也可以通过传入回调函数来更新state + +```tsx +// 传入一个state对象 更新state +this.setState({ + count: this.state.count + 1 +}) + +// 传入回调函数 返回值作为将与旧state合并 +this.setState((state, props) => { + return { + count: state.count + 1 + } +}) +``` + +传入回调函数来对state进行更新带来了一些好处: + +- 可以在回调中写心得state的逻辑(代码内聚性更强) +- 回调函数会将之前的state和当前的props传递进来 +- setState在React的事件处理中是被异步调用的 + +**如何获取异步更新的结果?** + +setState的异步更新也带来了一些问题,如果我们希望能在state变化后立即做出一些处理,可以使用到setState的第二个入参: + +第二个参数是一个回调函数,在回调函数中获取到的state为更新后的state最新值 + +`setState((oldState, props) => newState, () => ... )` + +#### 为什么setState被设计为异步执行? + +[Github: RFClarification: why is setState asynchronous?](https://github.com/facebook/react/issues/11527#issuecomment-360199710) + +- 可以显著提升性能,出于性能优化考虑 + - 假设`setState`是同步执行的,假设在调用函数后开发者连续调用了三次`setState` + - render函数会执行三次,创建三份不同的VDOM,执行三次Diff算法,三次更新到DOM上,带来重绘与重排... + - 如果在同一时间段内多次修改了state,那么React会在一段时间内的多次修改合并到一起,统一修改 + - 这样,即使在同一时间多次触发`setState`,那么render函数也只会被调用一次 +- 如果同步更新state,那么render函数中通过props传参的子组件不会被更新,会出现数据不同步的问题 + - 在setState后,可以立即获取到最新的state值,但是此时render函数并没有被执行 + +#### setState一定是异步的吗?(React18之前) + +在React18之后,setState方法调用都为异步的(在生命周期中 或在方法中) + +但是在React18之前,某些情况下是同步的: + +```tsx +// 异步执行 执行setState后当前state并未改变 +changeTitle = () => { + this.setState({ title: 'Hello, React!' }) + console.log(this.state.title) // Hello, World! +} + +// 同步执行 +changeTitle = () => { + setTimeout(() => { + this.setState({ title: 'Hello, React!' }) + console.log(this.state.title) // Hello, React! + }, 0) +} +``` + +- 这是因为setTimeout创建了一个宏任务,脱离了React内部的事件处理队列,不再受React的控制,从而达到了同步执行的效果 +- 同样的,如果是通过DOM监听器触发的回调中执行setState,也会作为宏任务执行,脱离React的事件处理队列 + +在React18之后,即使是setTimeout中的回调也是异步执行的,所有的回调都将被放入React内部维护的队列中,批量更新 + +> New Feature: Automatic Batching +> +> Batching is when React groups multiple state updates into a single re-render for better performance. Without automatic batching, we only batched updates inside React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default. With automatic batching, these updates will be batched automatically: +> +> [What’s New in React 18](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching) + +- 将多个状态更新会放到一次re-render中,为了更好的性能 +- 只在React事件处理函数才会有批处理 +- 之前:在promise/setTimeout/原生事件处理器以及其他的事件默认没有被批处理 +- 现在:都会被做批量处理,收集state改变,统一更新 + +在React18之后,可以通过 `flushSync(() => { ... })` 让 `setState` 实现同步更新: + +```tsx {2} +... +flushSync(() => { + this.setState({ + message: 'Hello, React!' + }) + + this.setState({ + message: 'Hello, React18!' + }) +}) + +console.log(this.state.message) // Hello, React18 +... +``` + +## React性能优化SCU + +### React的更新机制 + +之前我们已经了解了React的渲染流程:JSX => 虚拟DOM => 真实DOM + +React的更新流程: + +- props/state改变 +- render函数重新执行 +- 产生新的虚拟DOM树 +- diff新旧虚拟DOM树 +- 计算出差异执行局部更新 更新真实DOM +- 获取到真实DOM + +### 关于diff算法 + +- React需要基于两棵新旧虚拟DOM树来判断如何更高效地更新UI +- 如果一棵树参考另一棵树完全比较更新,那么即使是最先进的算法,时间复杂度为$O(n^2)$,其中$n$是树中节点的数量 +- 如果React中使用了这样的算法,当节点数量提高,那计算量是巨大的,会造成巨量的性能开销,更新性能较差 + +针对普通diff算法的缺陷,React对其进行了优化,将其时间复杂度优化到了$O(n)$ + +- 同级节点之间互相比较,节点不会跨级比较 +- 不同类型的节点产生不同的树结构 +- 开发中可以通过绑定 `key` 来保证哪些节点在不同的渲染下保持稳定(跳过diff 尽可能复用节点 避免更新) + +这意味着,如果根节点的类型发生变化,即使所有子节点都未发生变化,那整棵树也都将重新渲染,这也是一种取舍 + +### 关于key的优化 + +- 如果我们在渲染列表时没有绑定 `key` 属性,控制台会抛出警告提示 +- key的优化也是分为不同场景的 + - 向列表末位插入数据 + - key的优化意义不大 插入新数据时前面数据不会发生改变 + - 向列表前插入数据 + - 该场景下应当传key 否则列表发生变化时所有列表都会发生re-render +- key必须为唯一的 唯一代表当前节点 +- 不要使用随机数 这脱离了绑定key的初衷 +- 使用index作为key时没有意义 对性能优化没有助益 + +### 引入shouldComponentUpdate + +这里我们首先引入一个例子:在App组件中包含两个纯展示组件Home和Profile。 + +观察控制台输出,当页面第一次渲染时,所有组件的 `render` 函数都会被执行 + +但是当我们点击按钮,修改App中的`state.count`时,实际上只有`h1`标签的内容发生了变化 + +此时观察控制台,Home和Profile的render函数又都被执行了一次,这显然是不合理的,因为这两个组件的内容没有发生变化 + +```tsx +import React, { Component } from 'react' + +export class Home extends Component { + render() { + console.log('Home render') + return

Home

+ } +} + +export class Profile extends Component { + render() { + console.log('Profile render') + return

Profile

+ } +} + +export default class App extends Component { + constructor() { + super() + this.state = { + count: 0 + } + } + + render() { + console.log('App render') + return ( +
+

Count: {this.state.count}

+ + + +
+ ) + } +} +``` + +这样的场景下,可以通过 `shouldComponentUpdate` 生命周期返回 `false` 来决定当前组件是否发生更新 + +判断两次state是否发生改变,只有改变时才触发re-render + +```tsx +... +// nextProps: 修改后最新的Props +// nextState: 修改后最新的State +shouldComponentUpdate(nextProps, nextState) { + // 只有两次不等时 才发生更新 + return this.state.count !== nextState.count +} +... +``` + +在组件内部也可以使用类似的优化手段,自行决定是否更新 + +需要注意的是,`shouldComponentUpdate` 只会进行浅层比较,如果比较的props或state是引用类型的数据,则不适合用这样的方式 + +### PureComponent + +显然,如果每次都通过编写 `shouldComponentUpdate` (SCU) 来决定更新是很繁琐的,React为我们提供了更方便的用法:React.PureComponent + +如果你正在编写类组件,那么你可以使用 PureComponent (纯组件) 包裹你的组件内容,它会来帮你完成跳过更新,它的本质和 `shouldComponentUpdate` 是一样的:相同输入导致相同输出,输入相同时不必重新渲染 + +使用PureComponent对之前Counter的例子进行修改: + +当执行 `changeTitle` 修改父组件状态时,不会触发 Counter 的重新渲染,而只有在修改和 Counter 相关联的状态 count 时,其才会re-render + +```tsx {4} +// Counter.jsx +import React, { PureComponent } from 'react' + +export default class Counter extends PureComponent { + render() { + const { count, addCount, subCount } = this.props + return ( +
+ + {count} + +
+ ) + } +} + +// App.jsx +import React, { Component } from 'react' +import Counter from './components/Counter' + +export default class App extends Component { + constructor() { + super() + this.state = { + count: 0, + title: 'Hello, World!' + } + } + + changeTitle = () => { + this.setState({ + title: new Date().getTime() + }) + } + + ... + + render() { + const { title, count } = this.state + return ( +
+

{title}

+ + +
+ ) + } +} +``` + +### 函数式组件 memo + +我们知道,函数式组件是没有生命周期的,要在函数式组件中使用类似的性能优化手段,可以使用 `memo` 这个API + +```tsx {4} +// Recommand.jsx +import { memo } from 'react' + +export default memo(function Recommand(props) { + console.log('Recommand render') + const { count } = props + return ( +
+

Recommand

+

count: {count}

+
+ ) +}) +``` + +### 不可变数据的力量 + +来自React官方文档,不可变数据指的是稳定的state和props + +我们在这里举一个简单的书籍列表的例子: + +我们首先向state中推入一条新数据,随后使用 `setState` 将当前的状态作为更新源,点击按钮后页面是可以正常更新的 + +```tsx {4,21-22} +// BookList.jsx +import React, { Component } from 'react' + +export default class BookList extends Component { + constructor() { + super() + this.state = { + books: [ + { id: 1, name: 'book1', price: 10 }, + { id: 2, name: 'book2', price: 20 }, + { id: 3, name: 'book3', price: 30 }, + { id: 4, name: 'book4', price: 40 } + ] + } + } + + addBook = () => { + const newBook = { id: new Date().getDate(), name: 'book5', price: 50 } + this.state.books.push(newBook) + this.setState({ books: this.state.books }) + } + + render() { + const { books } = this.state + + return ( +
+
    + {books.map((book) => { + return ( +
  • + {book.name} + {book.price} +
  • + ) + })} +
+ +
+ ) + } +} +``` + +然而,一旦如果我们将 `Component` 替换为 `PureComponent` + +由于 `shouldComponentUpdate` 是**浅层比较**的 + +传入 `setState` 的更新源 `books` 的引用地址和 `this.state.books` 是相同的,**即使内部数据发生了添加,更新也会被跳过** + +最好的方式就是,**保证每次传入 `setState` 的值都是新的**,保证组件能够被正常渲染更新 + +```tsx +... +this.setState({ + books: [ + ...this.state.books, + { id: new Date().getDate(), name: 'book5', price: 50 } + ] +}) +... +``` + +这里的“不可变数据的力量”,指的就是保持state中数据稳定,如果我们希望修改state中的数据,则应当将state.xxx完整替换为一个新的对象 + +从源码层面,在源码内部React实现了一个方法 `checkShouldComponentUpdate`,如果组件内部定义了 `shouldComponentUpdate` 则会通过此方法进行检查 + +如果是 PureComponent,则会从组件原型上检查 `isPureReactComponent`,继而通过 shallowEqual 浅层比较判断 oldState & newState 是否相等 + +## 获取DOM的方式 refs + +### 使用Ref获取到真实DOM + +某些情况下我们需要直接操作DOM,在Vue中可以通过在template中绑定ref获取到DOM元素 + +- 方式1:在ReactElement上绑定ref属性 值为字符串 (已被废弃) +- 方式2:通过 `createRef` 创建一个ref并动态绑定到ReactElement上 +- 方式3:给ref属性传入一个函数,当DOM被创建时将作为参数传递到函数中 + +```tsx +// method 1: bind string to ref attribute +import React, { PureComponent } from 'react' + +export default class Input extends PureComponent { + getNativeDOM = () => { + console.log(this.refs.input) // + } + + render() { + return ( +
+ + +
+ ) + } +} +``` + +```tsx {7,10,16} +// method 2: dynamic bind Ref object to target Element's ref attribute +import React, { PureComponent, createRef } from 'react' + +export default class Input extends PureComponent { + constructor() { + super() + this.inputRef = createRef() + } + getNativeDOM = () => { + console.log(this.inputRef.current) // + } + + render() { + return ( +
+ + +
+ ) + } +} +``` + +```tsx {8} +// method 3: bind a function to ref attribute of target element +import React, { PureComponent } from 'react' + +export default class Input extends PureComponent { + render() { + return ( +
+ console.log(e)} type="text" /> +
+ ) + } +} +``` + +### 获取组件实例 + +通过类似的方法,可以直接获取到组件实例,也可以直接调用组件实例上的方法 + +```tsx +import React, { PureComponent, createRef } from 'react' + +class CustomInput extends PureComponent { + foo = () => { + console.log('CustomInput foo called') + } + + render() { + return + } +} + +export default class Input extends PureComponent { + constructor() { + super() + this.customInputRef = createRef() + } + + getComponent = () => { + this.customInputRef.current.foo() + } + + render() { + return ( +
+ + +
+ ) + } +} +``` + +但是,函数式组件没有实例,更别提直接调用实例方法了。类似于Vue3中通过setup创建的组件,我们需要对函数式组件做额外处理,类似于`defineExpose` + +这时就需要用到新的API:`forwardRef` 和 `useImperativeHandle` + +- `forwardRef` 用于将ref属性传递给函数式组件 + - 父组件传递给子组件的ref属性,会被React自动传递给子组件的第二个参数,即 `forwardRef` 的第二个参数 +- `useImperativeHandle` 用于将函数式组件内部的方法暴露给父组件 + +```tsx +import React, { PureComponent, createRef, forwardRef, useImperativeHandle } from 'react' + +const CustomInput = forwardRef((props, ref) => { + const foo = () => { + console.log('CustomInput foo called') + } + + useImperativeHandle(ref, () => ({ + foo + })) + + return +}) +... +``` + +## 受控和非受控组件 + +在React中,HTML表单的处理方式和普通DOM元素不太一样:表单通常会保存在一些内部的state中,并且根据用户的输入进行更新 + +下例中创建了一个非受控组件,React只能被动从组件接受值并更新到state中,而无法主动更新组件的值 + +```tsx +// Input.jsx +import React, { PureComponent } from 'react' + +export default class Input extends PureComponent { + constructor(props) { + super(props) + this.state = { + value: '' + } + } + + handleInputChange = (ev) => { + console.log(ev.target.value) // 这里的Event对象是合成事件 SyntheticEvent 由React封装的 + this.setState({ + value: ev.target.value + }) + } + + render() { + return ( +
+ + +
+ ) + } +} +``` + +我们对例子稍加改动,将组件的`value`属性设置为state中的值,从而实现受控组件。 + +需要注意的是,绑定`value`属性的同时,我们也要绑定`onChange`事件,供用户输入时对state进行更新 + +```tsx {8,22} +// Input.jsx +import React, { PureComponent } from 'react' + +export default class Input extends PureComponent { + constructor(props) { + super(props) + this.state = { + value: 'default Value' + } + } + + handleInputChange = (ev) => { + this.setState({ + value: ev.target.value + }) + } + + render() { + return ( +
+

currentValue: {this.state.value}

+ +
+ ) + } +} +``` + +this.state.value默认值 => 渲染到Input标签内 => 用户输入 => 触发onChange事件 => 更新state => 渲染到Input标签内 => ... + +React要求我们要么指定`onChange`要么指定`readOnly`,只绑定`value`属性时,控制台会抛出错误 + +### 使用受控组件的几个例子 + +下例中分别使用`input`创建了几个受控组件,文本框、单选、多选 + +```tsx +import React, { PureComponent } from 'react' + +export default class Form extends PureComponent { + constructor(props) { + super(props) + this.state = { + username: 'ziu', + password: '123456', + isAgree: false, + hobbies: [ + { value: 'sing', label: 'Sing', isChecked: false }, + { value: 'dance', label: 'Dance', isChecked: false }, + { value: 'rap', label: 'Rap', isChecked: false }, + { value: 'music', label: 'Music', isChecked: false } + ], + fruits: ['orange'] + } + } + + handleInputChange = (ev, idx) => { + this.setState({ + [ev.target.name]: ev.target.value + }) + } + + handleAgreeChange = (ev) => { + this.setState({ + isAgree: ev.target.checked + }) + } + + handleHobbyChange = (ev, idx) => { + const hobbies = [...this.state.hobbies] // IMPORTANT + hobbies[idx].isChecked = ev.target.checked + this.setState({ + hobbies + }) + } + + handleSelectChange = (ev) => { + this.setState({ + fruits: [...ev.target.selectedOptions].map((opt) => opt.value) + }) + } + + handleSubmitClick = () => { + const { username, password, isAgree, hobbies, fruits } = this.state + console.log( + username, + password, + isAgree, + hobbies.filter((h) => h.isChecked).map((h) => h.value), + fruits + ) + } + + render() { + const { username, password, isAgree, hobbies, fruits } = this.state + return ( +
+ + +
+ +
+
+ Hobby: + {hobbies.map((hobby, idx) => ( + + ))} +
+
+ +
+
+ +
+
+ ) + } +} +``` + +这里有一点小知识,关于可迭代对象,可以通过`Array.from`将可迭代对象转为数组 + +方便我们使用数组的方法来操作选取的DOM列表 + +简单做一下总结,如何在React中绑定受控组件: + +| Element | Value Property | Change Callback | New Value in Callback | +| ------- | -------------- | --------------- | --------------------- | +| `` | value | onChange | event.target.value | +| `` | checked | onChange | event.target.checked | +| `` | checked | onChange | event.target.checked | +| `