0%

权限验证分为菜单权限,路由权限,按钮权限,布局模块权限。

菜单权限

以上实现主要是前端层面的设计方案, 我们都知道前端的安全措施永远是不可靠的, 所以我们为了保证系统的安全性, 一般我们会把菜单数据存到后端, 通过接口动态请求权限菜单.。
可以提前和后端做好约定, 让后端根据不同用户返回不同的权限菜单 schema 即可.

递归法

掘金文章介绍
这个参考了 umi 的路由方法,将路由包裹起来
路由包裹掘金介绍
GitHub 源码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { IRoute, routes } from "@/route";
import React, { Suspense, useMemo } from "react";
import { Redirect, Route, Switch, HashRouter } from "react-router-dom";
import styles from "./index.module.less";

export default function App(): React.ReactElement {
const getChildrenComponent = (
route: IRoute,
key: number,
pPath: string = ""
) => {
const path = pPath + route.path;
return route.redirect ? (
<Redirect key={key} to={route.redirect} from={route.path}></Redirect>
) : (
(route.component || route.routes?.length > 0) && (
<Route key={key} path={path} exact={route.exact}>
{route.wrappers?.length > 0
? route.wrappers.reduceRight(
(element: any, wrapper: any) =>
React.createElement(wrapper, {}, element),
React.createElement(
route.component || React.Fragment,
{},
<Switch>
{route?.routes?.map((croute, rindex) =>
getChildrenComponent(croute, rindex, path)
)}
</Switch>
)
)
: React.createElement(
route.component || React.Fragment,
{},
<Switch>
{route?.routes?.map((croute, rindex) =>
getChildrenComponent(croute, rindex, path)
)}
</Switch>
)}
</Route>
)
);
};

const Routes = useMemo(() => {
const _Routes = routes.map((route, rindex) =>
getChildrenComponent(route, rindex)
);
console.log(_Routes, "_Routes");
return _Routes;
}, []);

return (
<Suspense fallback={<div>加载中</div>}>
<HashRouter basename="/">
<Switch>{Routes}</Switch>
</HashRouter>
</Suspense>
);
}

promise 封装法

GitHub 代码展示

Router v6 版本动态路由鉴权

掘金参考文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// router/index.ts
import { lazy } from "react";
import { Navigate } from "react-router-dom";

// React 组件懒加载

// 快速导入工具函数
const lazyLoad = (moduleName: string) => {
const Module = lazy(() => import(`views/${moduleName}`));
return <Module />;
};
// 路由鉴权组件
const Appraisal = ({ children }: any) => {
const token = localStorage.getItem("token");
return token ? children : <Navigate to="/login" />;
};

interface Router {
name?: string;
path: string;
children?: Array<Router>;
element: any;
}

const routes: Array<Router> = [
{
path: "/login",
element: lazyLoad("login"),
},
{
path: "/",
element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>,
children: [
{
path: "",
element: <Navigate to="home" />,
},
{
path: "*",
element: lazyLoad("sand-box/nopermission"),
},
],
},
{
path: "*",
element: lazyLoad("not-found"),
},
];

export default routes;

每次导航列表更新时,再触发路由更新 action
handelFilterRouter 就是根据导航菜单列表 和权限列表 得出路由表的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//redux login/action.ts
import { INITSIDEMENUS, UPDATUSERS, LOGINOUT, UPDATROUTES } from "./contant";
import { getSideMenus } from "services/home";
import { loginUser } from "services/login";
import { patchRights } from "services/right-list";
import { handleSideMenu } from "@/utils/devUtils";
import { handelFilterRouter } from "@/utils/routersFilter";
import { message } from "antd";

// 获取导航菜单列表
export const getSideMenusAction = (): any => {
return (dispatch: any, state: any) => {
getSideMenus().then((res: any) => {
const rights = state().login.users.role.rights;
const newMenus = handleSideMenu(res, rights);
dispatch({ type: INITSIDEMENUS, menus: newMenus });
dispatch(updateRoutesAction()); //import!
});
};
};

// 退出登录
export const loginOutAction = (): any => ({ type: LOGINOUT });

// 更新导航菜单
export const updateMenusAction = (item: any): any => {
return (dispatch: any) => {
patchRights(item).then((res: any) => {
dispatch(getSideMenusAction());
});
};
};

// 路由更新 //import!
export const updateRoutesAction = (): any => {
return (dispatch: any, state: any) => {
const rights = state().login.users.role.rights;
const menus = state().login.menus;
const routes = handelFilterRouter(rights, menus); //import!
dispatch({ type: UPDATROUTES, routes });
};
};

// 登录
export const loginUserAction = (item: any, navigate: any): any => {
return (dispatch: any) => {
loginUser(item).then((res: any) => {
if (res.length === 0) {
message.error("用户名或密码错误");
} else {
localStorage.setItem("token", res[0].username);
dispatch({ type: UPDATUSERS, users: res[0] });
dispatch(getSideMenusAction());
navigate("/home");
}
});
};
};

utils 工具函数处理

说一说我这里为什么要映射 element 成对应组件这步操作,原因是我使用了 redux-persist(redux 持久化),
不熟悉这个插件的可以看看我这篇文章:redux-persist
若是直接转换后存入本地再取出来渲染是会有问题的,所以需要先将 element 保存成映射路径,然后渲染前再进行一次路径映射出对应组件。
每个后台的数据返回格式都不一样,需要自己去转换,我这里的转换仅供参考。
ps:defaulyRoutes 和默认 router/index.ts 导出是一样的,可以做个小优化,复用起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { lazy } from "react";
import { Navigate } from "react-router-dom";
// 快速导入工具函数
const lazyLoad = (moduleName: string) => {
const Module = lazy(() => import(`views/${moduleName}`));
return <Module />;
};
const Appraisal = ({ children }: any) => {
const token = localStorage.getItem("token");
return token ? children : <Navigate to="/login" />;
};

const defaulyRoutes: any = [
{
path: "/login",
element: lazyLoad("login"),
},
{
path: "/",
element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>,
children: [
{
path: "",
element: <Navigate to="home" />,
},
{
path: "*",
element: lazyLoad("sand-box/nopermission"),
},
],
},
{
path: "*",
element: lazyLoad("not-found"),
},
];

// 权限列表 和 导航菜单 得出路由表 element暂用字符串表示 后面渲染前再映射
export const handelFilterRouter = (
rights: any,
menus: any,
routes: any = []
) => {
for (const menu of menus) {
if (menu.pagepermisson) {
let index = rights.findIndex((item: any) => item === menu.key) + 1;
if (!menu.children) {
if (index) {
const obj = {
path: menu.key,
element: `sand-box${menu.key}`,
};
routes.push(obj);
}
} else {
handelFilterRouter(rights, menu.children, routes);
}
}
}
return routes;
};

// 返回最终路由表
export const handelEnd = (routes: any) => {
defaulyRoutes[1].children = [...routes, ...defaulyRoutes[1].children];
return defaulyRoutes;
};

// 映射element 成对应组件
export const handelFilterElement = (routes: any) => {
return routes.map((route: any) => {
route.element = lazyLoad(route.element);
return route;
});
};

主文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import routes from "./router";
import { useRoutes } from "react-router-dom";
import { shallowEqual, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import { handelFilterElement, handelEnd } from "@/utils/routersFilter";
import { deepCopy } from "@/utils/devUtils";

function App() {
console.log("first");
const [rout, setrout] = useState(routes);
const { routs } = useSelector(
(state: any) => ({ routs: state.login.routes }),
shallowEqual
);

const element = useRoutes(rout);
// 监听路由表改变重新渲染
useEffect(() => {
// deepCopy 深拷贝state数据 不能影响到store里的数据!
// handelFilterElement 映射对应组件
// handelEnd 将路由表嵌入默认路由表得到完整路由表
const end = handelEnd(handelFilterElement(deepCopy(routs)));
setrout(end);
}, [routs]);

return <div className="height-all">{element}</div>;
}

export default App;

基本用法

输出

参数可以是字符串,数字,数学表达式,传递若干参数用逗号隔开.

1
2
3
4
5
6
7
8
9
package main
//引入fmt包
import "fmt"

func main() {
//使用fmt中Print函数进行打印
fmt.Print("Tom")
//下面的Println函数有换行功能
fmt.Println("my name is", "Tom")

Printf

第一个参数必须是字符串,其中包含格式化动词,如%v,值由第二个参数决定.

1
2
3
4
5
6
func main() {
fmt.Printf("My %v is 15", "age")
}
//其中格式化动词%v可以指定宽度
%4v,就是向左填充4个空格
&-5v,就是向右填充5个空格

常量和变量

const

声明常量

var

声明变量

1
2
3
4
5
6
7
8
9
var distance = 100
var speed = 30
//or
var distance, speed = 100, 30
/or
var (
distance = 100
speed = 30
)

自增运算

go 中没有++count这种,但是有count++

随机数

使用 math 包中的 rand

1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
"math/rand"
)
func main () {
//Intn(10)是取0-10不包括10的整数.
var num = rand.Intn(10) + 1
fmt.Println(num)
}

Boolean 类型

true 和 false,不会进行类型转换.

字符串

strings.Contains

返回 boolean 类型.

1
2
3
4
5
6
7
8
package main
import (
"string"
)
func main () {
var str = "aaa bbb ccc"
var exit = strings.Contains(str, "aaa")
}

条件判断

if

注意 if 后不适应()

switch

switch 语句中不需要写 break
switch 后也不需要跟传入参数

循环

for

for 语句后也不用(),后面的判断如果没有就是无限循环

作用域

变量的作用域在{}中.

package 作用域

在 main 函数之外声明必须使用 var 声明,不能使用短声明.且作用域存在于 package 中.

短声明

优势:可以在无法使用 var 的地方使用.比如 for 循环中.

1
2
3
4
5
6
var count = 10
count := 10
//for循环中
for count:=10; count > 0; count++ {
fmt.Println(count)
}

声明浮点型变量

1
2
3
days := 365.2425
var days = 365.2425
var days float64 = 365.2425

只要数字带有小数部分,类型就是float64.
如果使用整数声明浮点型,必须使用float64,否则就是整数类型.

单精度浮点类型

go 语言默认是float64,双精度浮点类型.占用 8 字节内存.
也可以使用float32,单精度浮点类型,占用 4 字节内存.
使用场景: 处理大量数据时,可以牺牲精度,节省内存.

显示浮点型

使用PrintPrintln打印时,默认显示所有小数.
如果想控制小数显示位数,可以使用Printf,结合%f格式化动词.

1
2
3
4
5
6
7
third := 1.0 / 3
fmt.Println(third)
fmt.Printf("%f",third) //表示显示浮点型
fmt.Printf("%.3f",third) //表示浮点型有3位数
fmt.Printf("%4.2f",third) //表示浮点型有2位数,整体包括小数点至少有4位数,如果不足,默认空格填充
//如果想使用0填充空格
fmt.Printf("%05.2f",third)

浮点型的精度问题

浮点型不适合做金融计算,如果非要使用,可以先乘后除.

1
2
3
4
5
third : = 1.0 / 3.0
fmt.Println(third + third + third) // 输出1
pigBank := 0.1
pigBank += 0.2
fmt.Pirntln(pigBank) // 0.300000000004

零值

Go 里面每个类型都有一个默认值,即零值.
当声明变量却不初始化时,该值即零值.

1
2
3
4
var price float64
fmt.Println(price)
//相当于
price := 0.0

整数类型

int

有符号类型

uint

无符号类型

uint8 表示颜色

可以用来表示 8 位的颜色(红绿蓝 0-255).
var red, green, blue uint8 = 0, 141, 213;
uint8 的取值范围正合适,而 int 则多出来几十亿不合适的数.

打印数据类型

在 Printf 中使用%T.

十六进制

在数值前加0x即表示十六进制.

打印十六进制

使用Printf%x.

1
2
3
fmt.Printf("%x %x %x", red, green, blue) // 0 8d d5
//指定宽度
fmt.Printf("color: #%02x%02x%02x;",red, green, blue) //color: #008dd5;

整数环绕

当超出超出整数的取值范围,就会发生整数环绕.
原理: 用二进制表示,比如 11111111,加 1 后变成 100000000,但是内存只有 8 位,所以值变为 0.

1
2
3
4
5
6
7
var red uint8 = 255
red++
fmt.Println(red) // 0,因为uint的范围时0-255

var number int8 = 128
number++
fmt.Println(number) //-127,因为int的范围时-127~128

打印二进制

格式化动词%b

1
2
3
4
var green uint8 = 3
fmt.Printf("%08b\n", green) // 00000011
green++
fmt.Printf("%08b\n", green) // 00000100

整数类型的最大值最小值

math 包里,为与架构无关的整数类型,定义了最大值,最小值常量.

1
2
math.MaxInt8
math.MinInt64

而 int 和 uint 可能时 32 位或者 64 位,无最大最小值.

避免时间环绕

unix 系统中,时间从 1970 年开始,到 2038 年会超过 20 亿,超过了 int32 的范围.
方法: 使用 int64 或者 uint64.

数值过大

浮点类型数值大但是精度不高,
整数类型精度高但是取值范围小.
采用方法可以
int64 < uint64 < float64 < 使用 big 包

big 包

对于较大的整数(超过 10 的 18 次方): big.Int
对于任意精度的浮点型: big.Float
对于分数: big.Rat

big.Int

使用了big.Int的等式其他部分也需要使用big.Int.
NewInt()函数可以把int64转化成big.Int类型.

常量和 big.Int 的值不能互换.

字符串

字符串字面值和原始字符串字面值

字符串字面值可以包含转义字符,如\n.
如果想显示\n,而不要换行,则可以使用```,即原始字符串字面值

1
2
fmt.Println("pece you\nwith") //打印有换行
fmt.Println(`pece you \n with`) // 打印出了\n,没有换行,或者直接在这里使用换行会原始换行

code points, runes, bytes

rune 是 int32 的别名,byte 是 int8 的别名.

自定义类型别名

1
2
type byte = uint8
type rune = int32

可以使用%c打印该字符.

字符

字符字面量用''包裹,如果没有指定字符类型,Go 会推断该类型为rune.
字符字面量也可以用byte表示.

字符解码

Go 的函数len可以返回字符的长度,但是有的字符是 16 位或 32 位,那么需要把字符解码成rune类型,再操作.
使用UTF-8包,他提供可以按rune计算字符串长度的方法.
RuneCountInString可以返回转换后的长度.
DecodeRuneInString函数会返回第一个字符,以及字符所占的字节数.
所以 Go 里的函数可以返回多个值.

1
2
3
4
question := '(*Φ皿Φ*)'
fmt.Println(utf8.RuneCountInString(question)
c, size := utf8.DecodeRuneInStirng(question)
fmt.Println("First rune: %c %v bytes", c, size)

遍历方法 range

range 返回两个值,一个 i 是索引,c 就是遍历的元素.

1
2
3
4
5
6
7
func main() {
question := '(*Φ皿Φ*)'
for i, c := range question {
fmt.Printf("%v %c\n", i ,c)
}
}
//如果不想使用索引,可以使用_替代

类型转换

不同类型不能在一起使用,需要进行类型转换.

数值之间转换

1
2
age := 64
marsAge := float64(age) //如果将age转换为浮点型需要进行包裹

浮点型转换为整型,小数点后边会被舍弃.
无符号和有符号整数之间也需要进行转换
不同大小的整数类型之间也需要进行转换.

转换时的数据环绕

因为转换时有可能会超过转换类型的最大值,所以可以使用 math 包中的最大值最小值进行判断.

1
2
3
4
var bh float64 = 32678
if bh < math.MinInt16 || bh >math.MxInt16 {
//handle
}

字符串转换

rune,byte转换成string,

1
2
3
4
var pi rune = 960
var alpha rune = 940

fmt.Println(string(pi), string(alpha)) //转化的结果时code point对应的结果

想把数值类型转换成字符串,那么它的值必须可以转换成code point.

strconv包的Itoa函数可以转换,或者使用Sprintf
strconv包的Atoi函数可以转换,且有两个参数,第二个返回错误信息,如果不为空,则有错误.

1
2
3
4
countdown, err := strconv.Atoi("10")
if err != nil {
//handle
}

函数

  • 函数按值传递
  • 同一个包中的函数调用彼此不需要加上包名.

函数声明

1
2
3
4
5
//rand包的Intl:
func Intn(n int) int
//关键字 函数名(参数 参数类型) 返回值类型
//使用方法
num := rand.Intn(10)

多个返回值

1
2
3
4
func Atio (s string) (i int, err error)
//关键字 函数名 参数名 参数类型 返回值名 返回值类型
//简化写法可以把返回值名省略,只保留返回值类型
func Atio (s string) (int error)

可变参数函数

Println可以接收多个参数.

1
func Println (a ...interface{}) (n int, err error)
  • ...表示函数的参数是可变的
  • 参数 a 的类型为interface{},是一个空接口.
  • ...和空接口组合就可以接收任意数量,类型的参数

一等函数(头等函数,高阶函数)

  • 可以将函数赋值给变量
  • 将函数作为参数传递给函数
  • 将函数作为函数的返回类型

闭包和匿名函数

  • 匿名函数在 go 中被称为函数字面值,函数字面值需要保留外部作用域的变量引用,所以函数字面值都是闭包的.
  • 闭包就是由于匿名函数封闭并包围作用域中 的变量而得名的.

方法

方法也属于函数,只不过函数是独立存在的,方法属于某一个类型,或者与类型有关联.

1
2
func (k kelvin)  celsius() celsius
//关键字 (变量名 类型名)=接收者 方法名 返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"fmt"
"math/rand"
"time"
)
type kelvin float64

func messs(samples int, sensor func() kelvin) {
for i:=0; i < samples; i++ {
k := sensor()
fmt.Printf("%v K\n", k)
time.Sleep(time.Second)
}
}
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func main() {
messs(3, fakeSensor)
}

方法添加行为

可以将方法中同包中声明的任何类型相关联,但不可以是int,float64等预声明的类型进行关联,但是可以通过声明一个新类型的方式曲线救国

1
2
3
func (k kelvin) celsius() celsius
//关键字 接收者 接收者类型 方法名 返回值类型
//表明celsius是类型kevin的一个方法
  • 上例中celsius方法虽然没有参数,但他前面却有一个类型参数的接收者
  • 每个方法可以有多个参数,但只能有一个接收者.
  • 在方法中,接收者的行为和其他函数一样

调用方法

变量.方法()

1
2
3
4
5
6
7
8
9
10
type kevin float64
type celsius float64

func (k kevin) celsius() celsius {
return celsius(k -273.15)
}
func main() {
var k kevin = 294.0
var c = k.celsius()
}

数组

长度len,声明数组时未被声明的元素的值为零值.

1
2
//声明长度为8,元素类型为string的数组
var arr [8]string

数组越界

Go 在检测到对越界元素的访问时会报错
在编译时未发现错误,那么运行时会出现panic

复合字面值初始化数组

可以使用...作为数组的长度,go 会推断数组长度

1
2
3
4
5
var plants := [...]string{
"Earth",
"Mars",
"Venus"
}

遍历数组

for 循环

1
2
3
4
5
6
7
8
9
plants := [...]string{
"Earth",
"Mars",
"Venus"
}
for i := 0;i < len(dwarfs); i++ {
dwarf := dwarfs[i]
fmt.Println(i, dwarf)
}

range 循环

类似 map

1
2
3
for i, dwarf := range dwarfs {
fmt.Println(i, dwarf)
}

数组的复制

无论是复制给新变量还是传递给函数,都会产生一个新数组的副本.
数组也是一种值,通过值传递来接收参数,所以作为函数的参数很低效.
数组的长度也是数组类型的一部分.
函数一般使用slice作为参数.

一个可预测的状态容器.

原则

  • 单一数据源
  • state 是只读的
  • 使用纯函数进行修改

原理

将数据存储在 store 中,一个组件改变了 store 中的内容,其他组件就能知道 store 的变化,再来获取数据,实现了数据传递。

常用概念

state:数据集合.存储状态的值.
action: 改变 state 的指令.或者说行为.
reducer: 加工函数,action 将 state 放到 reducer 中处理,返回新的 state.
store:仓库,有多个 reducer 的总工厂.

核心 API

1
2
3
4
5
6
7
8
9
10
// 创建Store容器
const store = Redux.createStore(reducer);
// 创建用于处理状态的Reducer函数
function reducer(state = initialState, action) {}
// 获取状态
store.getState();
// 订阅状态
store.subscribe(function () {});
// 触发Action
store.dispatch({ type: "description..." });

工作流程

1.组件 Component 通过 dispatch 方法触发 Action
2.Store 接收 Action 并将 Action 分发给 Reducer
3.Reducer 根据 Action 类型对状态进行更改并将更改后的状态返回给 Store
4.组件订阅了 Store 中的状态,Store 中的状态更新会同步到组件
iShot2022-07-27 22.16.54.png

常用方法

store

getState:获取 state.
dispatch(action):更新 state.它内部有 reducer 方法,所以触发 dispatch 就能更新 state
subscribe(listener):注册监听器.
unsubscribe(listener):返回的函数注销监听器.

通过 store.getState()来了解工厂中商品的状态, 使用 store.dispatch 发送 action 指令更新 state.

原则

  • 不变性.
  • state 是不可变的,只是返回一个新的 state.

为什么非要返回一个新的 state 呢?react 的比较机制,Object.is.就是纯纯的比较两个对象是否相等.

避免变化的要点

  • 对数组使用 concat,slice 和…展开运算
  • 对对象使用 Object.assign 和…展开运算

顶级暴露方法

createStore

1
2
3
4
5
// src/js/store/index.js
import { createStore } from 'redux';
import rootReducer from ../reducers/index;
const store = createStore(rootReducers);
export default store;

从上面看出,store 是 createStore 创建的,参数是 rootReducers.

reducer

纯函数,有两个参数,当前 state 和 action.
改变 state 的唯一方法就是派发 action.
reducer 一定是同步的,为了就是保持纯洁性.异步操作有副作用,就是可能会出现不同的结果.比如发送请求,第一次可能成功,第二次可能失败,这就不可预测.这就是副作用.

新产生的 state 是当前 state 添加了新数据后的一个副本,前一个 state 根本没发生变化.

1
2
3
4
5
6
// src/js/reducers/index.js
const initialState = { articles: [] };
function rootReducers(state = initialState, action) {
return state;
}
export default rootReducer;

action

每个 action 需要一个 type 属性来描述状态怎样变化
最佳的实践是在一个函数里包裹每一个 action,这个函数就叫 action creator.

1
2
3
4
// src/js/actions/index.js
export function addArticle(payload){
return {type: "ADD_ARTICE",payload }
}:

因为字符串容易出错或者重复,建议将 action 的 type 声明为常量

1
2
// src/js/constants/action-types.js
export const ADD_ARTICLE = "ADD_ARTICLE";

再使用时引入即可

1
2
3
4
import { ADD_ARTICLE } from "../constants/action-types";
export function addArticle(payload) {
return { type: ADD_ARTICLE, payload };
}

combineReducer

combineReducer 就是将拆分的 render 合并到一起.返回一个可以返回各个 reducer 状态的组合版的 reducer.
然后可以对这个 reducer 调用createStore.
合并后的 reducer 可以调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。 由 combineReducers() 返回的 state 对象,会将传入的每个 reducer 返回的 state 按其传递给 combineReducers() 时对应的 key 进行命名

1
2
3
4
5
6
7
8
import { combineReducers } from "redux";
import todos from "./todos";
import counter from "./counter";

export default combineReducers({
todos,
counter,
});

compose

用来从右到左来组合多个函数.当需要把多个store enhancers依次执行时用到它.
为什么从右到左呢? 因为 middleware 在执行的时候就是从右到左传递 dispatch,然后在从左到右执行.

1
2
3
4
5
6
7
8
9
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import DevTools from "./containers/DevTools";
import reducer from "../reducers";

const store = createStore(
reducer,
compose(applyMiddleware(thunk), DevTools.instrument())
);

辅助函数 bindActionCreators

来自 react,第一个参数要进行的 aciton 操作,第二个是 dispatch.
这个函数的作用就是把 actionCreator(这个就是返回 aciton 对象的函数)和 dispatch 结合起来,形成可以直接触发的 dispatch 操作.
就比如它可以把下面的示例二转化为示例一就明白了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 示例一
const mapDispatchToProps = (dispatch) => ({
increment() {
dispatch({ type: "increment" });
},
decrement() {
dispatch({ type: "decrement" });
},
});

// 示例二
// 将上面的操作提出来使用bindActionCreators
bindActionCreators(
{
increment() {
return { type: "increment" };
},
decrement() {
return { type: "decrement" };
},
},
dispatch
);

将 action 操作提出来

1
2
3
//actions.ts
export const increment = () => ({ type: "increment" });
export const decrement = () => ({ type: "decrement" });

再引入修改

1
2
3
4
5
6
7
8
const mapStateToProps = (state) => ({
count: state.count,
});

const mapDispatchToProps = (dispatch) => ({
...bindActionCreators(counterActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
1
bindActionCreators(mapDispatchToProps, this.store.dispatch);

react-redux

react-redux 主要是用来连接 react 和 redux 的,redux 不仅仅可以用在 react 中,还可以用在别的框架中.
作用: 将 store 中的 state 和组件连接在一起.

核心

  • <Provider store>
  • connect([mapStateToProps],[mapDIspatchToProps],[mergeProps],[options])

理解: Provider 包裹 react 应用,让它可以感知到整个 Redux 的 store.
Provider 中的任何一个组件,只有被 connect 过的组件才可以使用 state 中的数据.
它允许将 store 中的数据作为 props 绑定到组件上.
connect 方法将 store 上的 getState 和 dispatch 包装成组件的 props.

1
2
3
4
5
6
7
8
9
10
11
import { Provider } from 'react-redux';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StricMode>
<Provider store={store}>
<App />
<Provider>
</React.StricMode>
)

connect

四个参数,mapStateToProps,mapDispatchToProps,mergeProps 和 options.
作用分别为:

  • mapStateToProps:将 store 中的数据作为 props 绑定到组件上.
  • mapDispatchToProps(dispatch, ownProps): dispatchProps:将 action 作为 props 绑定到组件上.

1.connect 方法会帮助我们订阅 store,当 store 中的状态发生更改的时候,会帮助我们重新渲染组件
2.connect 方法可以让我们获取 store 中的状态,将状态通过组件的 props 属性映射给组件
3.connect 方法可以让我们获取 dispatch 方法

mapStateToProps

声明这个函数,这个函数是要把 state 绑定到 props 上.就要把相对应的想要传递的 state 中的相关参数传递出来.
这个函数传递出来的参数是和它相连接的函数所需要的参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
import { connect } from "react-redux";
const mapStateToProps = (state) => {
return { articles: state.articles };
};
const ConnectedList = ({ articles }) => (
<ul className="list-group list-group-flush">
{articles.map((el) => (
<li className="list-group-item" key={el.id}>
{el.title}
</li>
))}
</ul>
);
const List = connect(mapStateToProps)(ConnectedList);
export default List;

mapDispatchToProps

下面这个 form 组件本身有 state 可以只进行 action 的 dispatch 操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React, { Compoent } from 'react';
import { connect } from 'react-redux';
import uuidv from 'uuid';
import { addArticle } from "../actions/index";
const mapDispatchToProps = (dispatch) => {
return {
addArticle: article => dispatch(addArticle(article))
}
}
class ConnectedForm extends Component {
constructor(){
super()
this.state = {
titlte: ''
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event){
this.setState({ [event.target.id]: event.target.value });
}
handleSubmit(event){
event.preventDefault();
const { title } = this.state;
const id = uuidv();
this.props.addArticle({ title: id });
this.setState({ title: "" });
}
render(){
const { title } = this.state;
return (
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
className="form-control"
id="title"
value={title}
onChange={this.handleChange}
</div>
<button type="submit" className="btn btn-success btn-bg">SAVE</button>
</form>
);
}
}

const Form = connect(null, mapDispatchToProps)(ConnectedForm);
export default Form;

上面例子中handleSubmit方法触发 action.

Hooks

现在官方默认推荐使用 Hooks 方案替代上面的 connect().因为更简单明了.配合 ts 更是起飞.

useSelector

作用: 在函数组件中获取 state.
替代:mapStateToProps
允许传入一个选择器函数从 redux state 中取需要的状态数据并返回。示例:

1
const data: any = useSelector(selector: Selector, eqaulityFn?: Function)
1
const num = useSelector((state) => state.num);
  • 选择器 Selector 函数可以返回任意类型的数据,而不仅限于 对象

  • 当 dispath 一个 action 的时候,useSelector() 函数将对 Selector 选择器函数的返回值和上一个状态的返回值进行比较,如果 不同就会强制渲染组件,相同则不会

  • Selector 选择器 没有 接收 ownProps 属性

  • useSelector 默认比较更新 方式 为 ===, 如果需要浅层比较可以传入 第二个参数 equalityFn

    Tips: 不要在 Selector 选择器函数中 使用 props 属性, 否则可能会导致错误, 原因如下

  • 选择器函数依赖 props 的 某个数据

  • dispatch action 会导致父组件重新渲染,并传递新的 props

  • 子组件渲染时,选择器函数的执行时机会在 新 props 传入之前先执行,此时就会可能导致选择器函数返回错误数据,甚至报错 Zombile Child 是另一种 旧 props 导致异常的情况, 具体可以查看 常用警告

useDispatch

作用:返回 store 的 dispatch 方法,可以手动调用 dispatch 方法。
替代:mapDispatchToProps

1
const dispatch = useDispatch();

useStore

这个 Hook 返回 redux <Provider>组件的 store 对象的引用。

Tips: 大多数情况用不到这个 api, 取数据使用 useSelector() 代替

1
2
3
4
5
6
7
8
9
10
import React from "react";
import { useStore } from "react-redux";

export const CounterComponent = ({ value }) => {
const store = useStore();

// EXAMPLE ONLY! Do not do this in a real app.
// The component will not automatically update if the store state changes
return <div>{store.getState()}</div>;
};

Hooks 组件与 Connect 组件的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";
import { connect } from "react-redux";
import { addCount } from "./store/counter/actions";
export const Demo = ({ count, addCount }) => {
return (
<div>
      <div>Count: {count}</div>
      <button onClick={addCount}>Count</button>
    
</div>
);
};
const mapStateToProps = (state) => ({
count: state.counter.count,
});
const mapDispatchToProps = { addCount };

// content 链接
export default connect(mapStateToProps, mapDispatchToProps)(Demo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { addCount } from "./store/counter/actions";

export const Demo = () => {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();

return (
<div>
      <div>Count: {count}</div>
      <button onClick={() => dispatch(addCount())}>Count</button>
    
</div>
);
};

中间件 middleware

最最重要不要忘记return next(action)
中间件就是一个函数,带有科里化.是对 dispatch 方法的改造,在发出 action 和执行 reducer 之间,添加的功能。

1
export default (store) => (next) => (action) => {};

例如查询文章标题的禁用词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ADD_ARTICLE } from "../constants/action-types";

const forbiddenWords = ["spam", "money"];

export function forbiddenWordsMiddleware({ dispatch }){
return function(aciton){
if(action.type === ADD_TYPE){
const foundWord = forbiddenWords.filter(word =>
action.payload.title.includes(word)
);
if(foundWord.length){
return dispatch({ type: "FOUND_BAD_WORD"});
}
}
return next(action);
}:
}

开发中间件

中间件接收 store 为参数,返回一个 next 为参数的函数,该函数又返回一个 action 为参数的函数.最后通过 next 将 action 操作返回.
普通同步方法 actionCreator 函数使用 action 返回的是 action 对象,
异步方法返回的是函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// logger.js
export default store => next => action => {
console.log(action)
console.log(store)
next(action)
}

// 开发异步中间件
// 通过判断action的类型,判断是异步还是普通类型
// 异步代码需要放在传递进来的函数中
// 异步代码需要把dispatch传递出去
export default ({dispatch}) => next => action => {
if( typeof action = 'function') {
return action(dispatch)
}
next(action)
}

// 使用这个中间的异步函数也要传入dispatch
export const increment_async = payload => dispatch => {
setTimeout(() => {
dispatch(increment(payload))
},2000)
}

这是编写中间件,不过大多数中间件都有现程的,仅做了解.

工作流程

image.png

使用中间件

和 reducer 一起放到 store 中就行.

1
2
3
4
5
6
7
8
9
10
import { createStore, applyMiddleware } from "redux";
import rootReducer from "../reducers/index";
import { forbiddenWordsMiddleware } form "../middleware";

const store = createStore(
rootReducer,
applyMiddleware(forbiddenWordsMiddleware)
);

export default store;

异步操作中间件

即使不使用中间件,redux 依旧可以使用异步,只是 react-thunk 可以更好的将异步操作的细节隐藏.让书写更加的方便.
异步操作放在 action creator 中.
上面只是写法,真正有更好用的react-thunk.

reudx-thunk

1
2
3
4
5
6
7
8
9
//src/js/store/index.js
import { createStore, applyMiddleware } from "redux";
import rootReducer from "../reducers/index";
import { forbiddenWordsMiddleware } from "../middleware";
import thunk from "redux-thunk";
const store = createStrore(rootReducer,
applyMiddleware(forbiddenWordsMiddleware, thunk))
);
export default store;

重新写一下异步的 action 操作

1
2
3
4
5
6
7
8
9
10
//src/js/actions/index.js
export default getData(){
return function(dispatch){
retrun fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(json => {
dispatch({type: "DATA_LOADED",payload: json});
});
};
}

在 reducer 中添加对应的 type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	//src/js/reducers/index.js
import { ADD_ARTICLE,DATA_LOADED } from '../constants/action-types";
const initialState = {
articles: [],
remoteArticles: []
};
function rootReducer(state = initialState, action){
if(action.type === ADD_ARTICLE){
return Object.assign({}, state, {
articles: state.articles.concat(action.payload)
});
}
if(action.type ==== DATA_LOADED){
return Object.assign({}, state, {
remoteArticles: state.remoteArticles.concat(action.payload)
});
}
return state;
}
export default rootReducer;

redux-saga

这个属于 redux-thunk 的进阶版本,它可以将异步操作从 actionCreator 中提取出来单独放在一个文件中.

使用

1
2
3
4
5
//创建redux-saga中间件
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware();
// 注册 sagaMiddleware
createStore(reducer, createMiddleware(sagaMiddleware));
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用saga接收action执行异步操作
import { takeEvery, put, delay } from "redux-saga";
// takeEvery用来接收action
// put用来触发action
// 迭代器写法
function* increment_async_fn() {
yield delay(2000);
yield put(increment(10));
}
export default function* conuterSaga() {
// 接收action,第一个参数是类型,第二个是调用函数
yield takeEvery(INCREAMENT_ASYNC, increment_async_fn);
}
1
2
3
// 启动 saga
import postSaga from "./store/sagas/post.saga";
sagaMiddleware.run(postSaga);

拆分合并

1
2
3
4
5
6
//将多个saga操作拆分后,使用all合并
import { all } from "redux-saga/effects";

export default function* rootSaga() {
yield all([counterSaga(), modalSaga()]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建reducer
import { handleActions as createReducer } from 'redux-actions'
import { increment_action, decrement_action } from '../actions/counter.action';

const handleAddProductLocalCart = (state, action) => {
// 深拷贝原有数据
const newState = JSON.parse(JSON.stringify(state))
newState.push({action.payload})
// 将修改过的返回
return newState
}
const handleSaveCart = (state, action) => action.payload

const counterReducer = createReducer({
[addProductLocalCart]: handleAddProductLocalCart,
[saveCart]: handleSaveCart
},initialState)
export default counterReducer;

redux-actions

用于简化 reducer 和 action 代码

1
2
3
4
5
6
//创建action
import { createAction } from "redux-actions";
const increment_action = createAction("increment");
// 相当于
// const increment_action = () =>({type: increment})
const decrement_action = createAction("decrement");
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建reducer
import { handleActions as createReducer } from "redux-actions";
import { increment_action, decrement_action } from "../actions/counter.action";

const initialState = { count: 0 };
const counterReducer = createReducer(
{
[increment_action]: (state, action) => ({ count: state.count + 1 }),
[decrement_action]: (state, action) => ({ count: state.count - 1 }),
},
initialState
);
export default counterReducer;

Redux Toolkit

这是一个官方推荐的高效的开箱即用的 redux 工具箱.
它是简化 redux 的开发,无需手动书写大量样板代码.自带很多常用的插件,比如redux thunk,reselect.immer.

安装

1
pnpm add @reduxjs/toolkit

常用 api

configureStore()

封装了 createStore,简化配置项,提供一些现成的默认配置项。它可以自动组合 slice 的 reducer,可以添加任何 Redux 中间件,默认情况下包含 redux-thunk,并开启了 Redux DevTools 扩展。

1
2
3
4
5
6
7
8
9
import { configureStore } from "@reduxjs/toolkit";

export const rootReducer = {
projectList: projectListSlice.reducer,
};

export const store = configureStore({
reducer: rootReducer,
});

createReducer()

帮你将 action type 映射到 reducer 函数,而不是编写 switch…case 语句。另外,它会自动使用 immer 库来让你使用普通的 mutable 代码编写更简单的 immutable 更新,例如 state.todos[3].completed = true。

createAction()

生成给定 action type 字符串的 action creator 函数。该函数本身已定义了 toString(),因此可以代替常量类型使用。

createSlice()

接收一组 reducer 函数的对象,一个 slice 切片名和初始状态 initial state,并自动生成具有相应 action creator 和 action type 的 slice reducer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createSlice } from "@reduxjs/toolkit";

// 或者
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.value++;
},
decrement(state) {
state.value--;
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload;
},
},
});
// 导出
const { reducer: TodosReducer, actions } = createSlice();
export const { addTodo } = actions;
export default TodosReducer;

createAsyncThunk

接收一个 action type 字符串和一个返回值为 promise 的函数, 并生成一个 thunk 函数,这个 thunk 函数可以基于之前那个 promise ,dispatch 一组 type 为 pending/fulfilled/rejected 的 action。

createEntityAdapter

生成一系列可复用的 reducer 和 selector,从而管理 store 中的规范化数据。

createSelector

来源于 Reselect 库,重新 export 出来以方便使用。

工具 RTK Query

类似于 React Query.
Redux Toolkit 更是提供一个新的 RTK Query 数据请求 API。RTK Query 是为 Redux 打造数据请求和缓存的强有力的工具。 它设计出来就是为了 web 应用中加载数据的通用用例,免得手动去写数据请求和缓存的逻辑。

Redux 状态持久化

所谓状态持久化就是将状态与本地存储联系起来,达到刷新或者关闭重新打开后依然能得到保存的状态。

1
2
3
yarn add redux-persist
// 或者
npm i redux-persist

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// store.js
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { persistStore, persistReducer } from "redux-persist"; // **
import storage from "redux-persist/lib/storage"; // **
import reducer from "./reducer";

const persistConfig = {
// **
key: "root", // 储存的标识名
storage, // 储存方式
whitelist: ["persistReducer"], //白名单 模块参与缓存
};

const persistedReducer = persistReducer(persistConfig, reducer); // **

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
persistedReducer,
composeEnhancers(applyMiddleware(thunk))
); // **
const persistor = persistStore(store); // **

export {
// **
store,
persistor,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { PersistGate } from "redux-persist/integration/react"; // **

import { store, persistor } from "./store"; // **
import "antd/dist/antd.min.css";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
{/* 使用PersistGate //** */}
<PersistGate loading={null} persistor={persistor}>
<BrowserRouter>
<App />
</BrowserRouter>
</PersistGate>
</Provider>
</React.StrictMode>
);

注意此时的模块是在白名单之内,这样 persist_reducer 的状态就会进行持久化处理了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// persist_reducer.js
import { DECREMENT } from "./constant";

const defaultState = {
count: 1000,
title: "redux 持久化测试",
};

const reducer = (preState = defaultState, actions) => {
const { type, count } = actions;
switch (type) {
case DECREMENT:
return { ...preState, count: preState.count - count * 1 };
default:
return preState;
}
};

export default reducer;

immutable.js 与状态持久化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// count_reducer.js
import { INCREMENT } from "./constant";
import { Map } from "immutable";
// 简单的结构用Map就行 复杂使用fromJs 读取和设置都可以getIn setIn ...
const defaultState = Map({
// **
count: 0,
title: "计算求和案例",
});

const reducer = (preState = defaultState, actions) => {
const { type, count } = actions;
switch (type) {
case INCREMENT:
// return { ...preState, count: preState.count + count * 1 }
return preState.set("count", preState.get("count") + count * 1); // **
default:
return preState;
}
};

export default reducer;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 函数组件
const dispatch = useDispatch();
const { count, title } = useSelector(
(state) => ({
count: state.countReducer.get("count"),
title: state.countReducer.get("title"),
}),
shallowEqual
);
const handleAdd = () => {
const { value } = inputRef.current;
dispatch(incrementAction(value));
};
const handleAddAsync = () => {
const { value } = inputRef.current;
dispatch(incrementAsyncAction(value, 2000));
};

存在的问题

当我们使用了 redux-persist 它会每次对我们的状态保存到本地并返回给我们,但是如果使用了 immutable 进行处理,把默认状态改成一种它内部定制 Map 结构,此时我们再传给 redux-persist,它倒是不挑食能解析,但是它返回的结构变了,不再是之前那个 Map 结构了而是普通的对象,所以此时我们再在 reducer 操作它时就报错了,如下案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { memo } from "react";
import { useDispatch, useSelector, shallowEqual } from "react-redux";
import { incrementAdd } from "../store/persist_action";

const ReduxPersist = memo(() => {
const dispatch = useDispatch();
const { count, title } = useSelector(
({ persistReducer }) => ({
count: persistReducer.get("count"),
title: persistReducer.get("title"),
}),
shallowEqual
);
return (
<div>
<h2>ReduxPersist----{title}</h2>
<h3>count:{count}</h3>
<button onClick={(e) => dispatch(incrementAdd(10))}>-10</button>
</div>
);
});

export default ReduxPersist;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { DECREMENT } from "./constant";
import { fromJS } from "immutable";

const defaultState = fromJS({
count: 1000,
title: "redux 持久化测试",
});

const reducer = (preState = defaultState, actions) => {
const { type, count } = actions;
switch (type) {
case DECREMENT:
return preState.set("count", preState.get("count") - count * 1);
default:
return preState;
}
};

export default reducer;

此时会出现报错,提示没有 get 方法.

解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { memo } from "react";
import { useDispatch, useSelector, shallowEqual } from "react-redux";
import { incrementAdd } from "../store/persist_action";

const ReduxPersist = memo(() => {
const dispatch = useDispatch();
// **
const { count, title } = useSelector(
({ persistReducer: { count, title } }) => ({
count,
title,
}),
shallowEqual
);

//const { count, title } = useSelector(
// ({ persistReducer }) => ({
// count: persistReducer.get("count"),
// title: persistReducer.get("title"),
// }),
// shallowEqual
// );

return (
<div>
<h2>ReduxPersist----{title}</h2>
<h3>count:{count}</h3>
<button onClick={(e) => dispatch(incrementAdd(10))}>-10</button>
</div>
);
});

export default ReduxPersist;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { DECREMENT } from "./constant";
import { fromJS } from "immutable";

const defaultState = {
// **
count: 1000,
title: "redux 持久化测试",
};

const reducer = (preState = defaultState, actions) => {
const { type, count } = actions;
let mapObj = fromJS(preState); // **
switch (type) {
case DECREMENT:
// return preState.set('count', preState.get('count') - count * 1)
return mapObj.set("count", mapObj.get("count") - count * 1).toJS(); // **
default:
return preState;
}
};

export default reducer;

思路

由于 redux-persist 处理每次会返回普通对象,所以我们只能等要在 reducer 中处理状态时,我们先将其用 immutable 处理成它内部定制 Map 结构,然后我们再进行 set 操作修改,最后我们又将 Map 结构转换为普通对象输出,这样就完美的解决了这个问题。

Redux 和 VueX 的区别

相同点

  • 都是全局共享 state
  • 流程一致: 定义全局 state, 触发修改 state
  • 原句注入 store

不同点

  • 从实现原理上来说:
    • Redux 使用的是不可变数据,而 Vuex 的数据是可变的。Redux 每次都是用新的 state 替换旧的 state,而 Vuex 是直接修改
    • Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而 Vuex 其实和 Vue 的原理一样,是通过 getter/setter 来比较的
  • 从表现层来说:
    • vuex 定义了 state、getter、mutation、action 四个对象;redux 定义了 state、reducer、action。
    • vuex 中 state 统一存放,方便理解;reduxstate 依赖所有 reducer 的初始值
    • vuex 有 getter,目的是快捷得到 state;redux 没有这层,react-redux mapStateToProps 参数做了这个工作。
    • vuex 中 mutation 只是单纯赋值(很浅的一层);redux 中 reducer 只是单纯设置新 state(很浅的一层)。他俩作用类似,但书写方式不同
    • vuex 中 action 有较为复杂的异步 ajax 请求;redux 中 action 中可简单可复杂,简单就直接发送数据对象({type:xxx, your-data}),复杂需要调用异步 ajax(依赖 redux-thunk 插件)。
    • vuex 触发方式有两种 commit 同步和 dispatch 异步;redux 同步和异步都使用 dispatch

通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;

共同思想

  • 单一数据源
  • 变化可以预测

新建项目

1
2
3
4
5
npx create-react-app my-app --template typescript

# or

yarn create react-app my-app --template typescript

安装 react-router

1
2
3
4
5
6
npm install --save react-router-dom
yarn add react-router-dom
//如果要支持ts
npm i -D @types/react-router-dom
//or
yarn add @types/react-router-dom -D

Ts 中别名的设置

很多时候引入模块想把src设置为@.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//vite.config.ts
import { defineConfig } from 'vite'
import path from 'path';
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
}

//tsconfig.json
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}

useState

对于存储string类型的 state,在useState右边加上<string>即可:

1
2
const [input, setInput] = useState<string>("");
//此处初始化是'',则ts会自动判断类型,不写<string>也行

如果要存储数组,就要用interface

1
2
3
4
5
[
{ id: "xxxx", content: "xxxxxxx", isDone: false },
{ id: "xxxx", content: "xxxxxxx", isDone: false },
{ id: "xxxx", content: "xxxxxxx", isDone: false },
];
1
2
3
4
5
6
interface TaskObj {
id: string;
content: string;
isDone: boolean;
}
const [task, setTask] = useState<TaskObj[]>([]);

继承父组件中 state 并有自己的初始值

image.png

父子传参

js 中传参直接在子组件上写,子组件通过props接收

1
<Doing doing={doing} setTask={setTask} />

但是 ts 会报错,因为子组件并没有满足传递参数的接口,需要定义接口

1
2
3
4
5
6
7
8
9
//添加的DoingProps表明该组件接收的参数需要满足
//有doing属性,且类型为数组,数组的元素满足TaskObj接口.
//有setTask属性,且类型是函数.

interface DoingProps {
doing: TaskObj[];
setTask: Function;
}
const Doing: React.FC<DoingProps> = ({doing,setTask}) => {...}

使用 context

尽量使用 context,使用 provider 去替代 props.

类型断言

拿输入框DOM节点的时候

1
2
3
4
5
const inputNode: HTMLInputELement = document.getELementById(`${id}`);
//可能会报错,加上类型断言即可
const inputNode: HTMLInputElement = document.getElementById(
`${id}`
) as HTMLInputElement;

事件

常用的 React 事件类型

  • React.MouseEvent - 点击事件
  • React.KeyboardEvent - 键盘事件
  • React.DragEvent - 拖拽事件
  • React.FocusEvent - 焦点事件
  • React.ChangeEvent - 表单域值变更事件
  • React.FormEvent - 表单提交事件
  • React.WheelEvent - 鼠标滚动事件
  • React.TouchEvent - 触摸事件

注意,有的不能加 react,因为加了 React 是合成事件,不加是原生事件.

键盘事件

1
const addTodo = (e: React.KeyboardEvent<HTMLInputElement>): void => {...}

输入事件

1
2
3
4
5
6
7
8
9
10
11
<input
ref={editInput}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => onBlurEdit(e)}
className="text-edit"
style={{ display: state.onEdit ? "block" : "none" }}
value={todo.content}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleTodoTextEdit(e, todo)
}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => submitEditText(e)}
/>

e 的值可以是(e.target as HTMLInputElement).value.

细节的注意

泛型

泛型在类型或者接口后,是传入参数的约束.也就是说传入的参数是在这个泛型之内的.

返回类型

jsx 的返回类型是ReactElement
node 元素的返回类型是ReactNode

技巧

useState 中设置 setState 中的方法可以将参数解构,简化浅拷贝

1
onChange={e => setParam(...param, e.target.value)}

fetch 方法返回的是异步可以用 async 处理

1
2
3
4
5
fetch("").then(async (res) => {
if (res.ok) {
setList(await res.json());
}
});

修改值时可以先将该值拷贝,修改拷贝值,return 出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const useArray = <T></T>(initialArray: T[]) => {
const [value, setValue] = useState(initialArray)

return {
value,
setValue,
add: (item: V) => {
setValue([...item, value])
},
clear: () => setValue([]),
removeIndex: (index: number) => {
const copy = [...value]
copy.splice(index,1)
setValue(copy)
}
}
}

自定义 http

当一个参数有默认值时,参数内自动变为可选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export const http = (
endpoint: string,
{ data, token, headers, ...customConfig }: Config = {}
) => {
const config = {
method: "GET",
headers: {
Authorization: token ? `Bearer ${token}` : "",
"Content-Type": data ? "applcation/json" : "",
},
...customConfig,
};

if (config.method.toUpperCase() === "GET") {
endpoint += `?${qs.stringify(data)}`;
} else {
config.body = JSON.stringify(data || {});
}

return window.fetch(`${apiUrl}/${endpoint}`, config).then(async (res) => {
if (res.status === 401) {
// 未登录或token失效
await auth.logout();
window.location.reload();
return Promise.reject({ message: "请重新登录" });
}

const data = await res.json();
if (res.ok) {
return data;
} else {
return Promise.reject(data);
}
});
};

export const useHttp = () => {
const { user } = useAuth();
return (...[endpoint, config]: Parameters<typeof http>) =>
http(endpoint, { ...config, token: user?.token });
};

关于 svg 的引入问题

cra 虽然声明了 svg 作为模块可以引入,但是仍会报错.
解决方法:
新建src/react-app-env.d.ts

1
/// <reference types="react-scripts" />

telegram@MaiKaDe666 制作
一、下载注册企业微信
1.搜索下载企业,并使用手机号进行企业微信注册
2.选择成立企业
3.输入企业名称等信息
4.完成后,在通讯录内添加成员
二、企业微信推送
1.打开https://work.weixin.qq.com/,扫码登陆
2.点击我的企业,在最下边找到企业 ID(此为 QYWX_AM 变量第一个参数)
3.点击应用管理,选择创建应用
ip_image002.jpeg
ip_image004.jpeg
4.创建完成后找到 AgentId 和 Secret 记录下来(此为 QYWX_AM 变量第四个和第二个参数)
ip_image006.jpeg
5.点击发消息,找到素材库,点击图片,添加图片
ip_image008.jpeg
6.点击复制图像链接。找到 media_id 后边的 XXXX (此为 QYWX_AM 变量第五个)
7.https://work.weixin.qq.com/wework_admin/material/getOpenMsgBuf?type=image&**media_id**=XXXXXXXXXXXXXXXXXXXX&file_name=XXXXXX
lip_image010.png
8.点击通讯录,找到要推送的人,点击。找到账号。(此为 QYWX_AM 变量第三个)
ip_image012.jpeg
按照 cookie 顺序填写账号信息,如 cookie1 对应的 userid1,cookie2 对应的 userid2 用|隔开
Userid1|userid2|userid3
最终QYWX_AM=”企业 ID,Secret,userid1|userid2|userid3|xxx,AgentId,media_id
例:
QYWX_AM=”wwcff56746d9adwers,B-791548lnzXBE6_BWfxdf3kSTMJr9vFEPKAbh6WERQ,mingcheng|maikade|abcdefg|lxk0301,1000001,2COXgjH2UIfERF2zxrtUOKgQ9XklUqMdGSWLBoW_lSDAdafat”

感谢余温大佬提供的模板,感谢豆豆提供的图片素材
第一个值是企业 id,第二个值是 secret,第三个值@all(或者成员 id),第四个值是 AgentID,第五个值是图片 id   中间以逗号隔开
最终格式例:"ww479cadfqfe8c151f,MPKN9gX97w4e4b4h4u7u4i4i4i4iO6mN_dDedBFzqC5c,@all,1000002,2S8H-JWEdsadFDSFSDDijbrtkldfhjslafhs5zEU1GyUkU"
最终将此代码填入config.sh推送渠道 9 企业微信应用消息推送的值

  1. 扫码进入企业微信后台,获取企业 id
    https://work.weixin.qq.com/wework_admin/loginpage_wx
  2. 截图.png 2.依次进入应用管理-应用-自建-创建应用,自己创建完成后获取 secret 和 AgentID 和成员 id
    前两个直接填,如果给所有人推送填@all(这里的所有人指的是已经添加到可见范围内的成员),如果指定某个接收者直接填 id,多个接收者用‘|’分隔。

截图 (1).png

  1. 点击自建应用下方的发消息,依次选择素材库-图片-添加图片,上传完成后从下载库下载图片复制下载链接,然后图片 id 只需要填写下载链接中红色标出的部分。
    例:https://work.weixin.qq.com/wework_admin/material/getOpenMsgBuf?type=image&media_id=28If78oLQTolSULoVBeKTvSK8-B6LtFJbc_JWwV2JSou6bftKVmpxyWA1kgU8bB2Y&file_name=IMG_20210205_223133_888.jpg&download=1

截图 (2).png
截图 (3).png
截图 (4).png

  1. 补充 1.推送到普通微信,需要扫一下这个

截图 (5).png 2.推送给多人必须邀请其他人也加入企业,加入到应用可见范围中,也需要扫码微信插件。 3.一切以官方 API 为准:https://work.weixin.qq.com/api/doc/90000/90135/90235

Hooks 概览

定义变量
useState:定义普通变量
useReducer:定义有不同类型、参数的变量
组件传值
useContext():定义和接收具有全局性质的属性传值对象,必须配合 React.createContext()使用
对象引用
useRef:获取渲染后的 DOM 元素对象,可调用该对象原生 html 的方法,可能需要配合 React.forwardRef()使用
useImperativeHandle:获取和调用渲染后的 DOM 元素对象拥有的自定义方法,必须配合 React.forwardRef()使用
生命周期
useEffect:挂载或渲染完成后、即将被卸载前,调度

useLayoutEffect:浏览器把内容真正渲染到界面之前,同步调度
性能优化
useCallback:获取某处理函数的引用,必须配合 React.memo()使用
useMemo:获取某处理函数返回值的副本
代码调试
useDebugValue:对 react 开发调试工具中的自定义 hook,增加额外显示信息
自定义 hook
useCustomHook:将 hook 相关逻辑代码从组件中抽离,提高 hook 代码可复用性。

类组件转化为函数组件

  1. 创建一个与重构页面类组件同名的函数。
  2. return 进类组件中的页面结构。
  3. 类中定义的变量,可以使用一个状态变量去表示,使用 useState 钩子我们将把组件重写为一个函数。用 useState 钩子,来处理类中多状态值。
  4. 在函数组件中我们可以使用 useEffect 钩子去处理类组件中的生命周期方法
  5. 注意类组件转为函数组件以后功能的变化。

举例

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

class App extends Component {
state = {
year: 1995,
type: "Mercedes",
used: true,
};
swapCar = () => {
this.setState({
year: 2018,
type: "BMW",
used: false,
});
};

render() {
return (
<div style={{ marginBottom: "50px" }}>
<h2>Challenge 3</h2>
<h3>Car Spec is:</h3>
<ul>
<li>{this.state.type}</li>
<li>{this.state.year}</li>
<li>{this.state.used ? "Used Car" : "Brand New!"}</li>
</ul>
<button onClick={this.swapCar}>Swap Car!</button>
</div>
);
}
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//函数组件重写
import React, { useState } from "react";

function App() {
const [year, changeYear] = useState(1995);
const [type, changeType] = useState("Mercedes");
const [used, changeCondition] = useState(true);

const swapCar = () => {
changeYear(2018);
changeType("BMW");
changeCondition(false);
};

return (
<div style={{ marginBottom: "50px" }}>
<h2>Challenge 3</h2>
<h3>Car Spec is:</h3>
<ul>
<li>{type}</li>
<li>{year}</li>
<li>{used ? "Used Car" : "Brand New!"}</li>
</ul>
<button onClick={swapCar}>Swap Car!</button>
</div>
);
}

export default App;

自定义 hook

相当于将复用逻辑封装到一个 hook 中.在多个函数复用同一个逻辑时使用,
在自定义 hook 的时候可以遵循一个简单原则:当参数大于 2 个的时候返回值的类型返回 object, 否则返回数组

  • 自定义 hook 其实是一个函数,但是要以 use 开头,函数内部可以调用 其他 hook
  • 自定义 hook 复用的是逻辑,而非状态
  • hook 只能用在函数组件中或者自定义 hook 里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const useInputValue = (initialValue) => {
const [value, setValue] = useState(initialValue);
return {
value,
onChange: (e) => setValue(e.target.value),
};
};

const TodoForm = () => {
const text = useInputValue("");
const password = useInputValue("");
return (
<form>
<input type="text" {...text} />
<input type="password" {...password} />
</form>
);
};
export default TodoForm;

独立性

多个函数调用相同的自定义 Hook,他们共用一个 state 吗?
答案是不是同一个,是独立的。
官网上讲:
每次调用 Hook,它都会获取独立的 state。由于我们直接调用了 useFriendStatus,从 React 的角度来看,我们的组件只是调用了 useState 和 useEffect。 正如我们在之前章节中了解到的一样,我们可以在一个组件中多次调用 useState 和 useEffect,它们是完全独立的。

useState

为函数引入状态的钩子.
useState 中可以是对象,数组,函数.
如果是函数,那么函数的返回值就是初始值.而不是保存函数的状态.函数只会被调用一次。这是惰性初始值.
set 方法是异步操作.

函数组件的 useState 修改 state 是直接覆盖。
类组件修改 state 是进行合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//在react中引入
import React,{ useState } from 'react';
//在function中设置
export default () => {
/**
* 使用一个数组包裹
* count: 状态
* setCount: 修改状态
* useState(0): 括号内的值是初始值
*/
const [count, setCount] = useState(0);
return (
<div>
{ count }
<button onClick={()=>{setCount(count+1)}>add</button>
</div>
)
}
// 如果将调用函数抽离,使用setCount时注意参数是count
const handleCount = () => {
setCount(count => count +1)
}

原则

如果一个值可以基于现有的 props 或 state 计算得出,不要把它作为一个 state,而是在渲染期间直接计算这个值.

函数式更新

1
<button onClick={()=> {setCount(count => count+1)}>+</button>
新的state通过上一个state计算得出,可以将函数传给setState. 该函数接收上一个state,返回一个新的state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1)
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
当设置为异步更新,点击按钮延迟到3s之后去调用setCount函数,当快速点击按钮时,也就是说在3s多次去触发更新,但是只有一次生效,因为 count 的值是没有变化的。而当使用函数式更新 state 的时候,这种问题就没有了,因为它可以获取之前的 state 值,也就是代码中的 prevCount 每次都是最新的值。

其实这个特点和类组件中 setState 类似,可以接收一个新的 state 值更新,也可以函数式更新。如果新的 state 需要通过使用先前的 state 计算得出,那么就要使用函数式更新。因为 setState 更新可能是异步,当你在事件绑定中操作 state 的时候,setState 更新就是异步的。

一般操作 state,因为涉及到 state 的状态合并,react 认为当你在事件绑定中操作 state 是非常频繁的,所以为了节约性能 react 会把多次 setState 进行合并为一次,最后在一次性的更新 state,而定时器里面操作 state 是不会把多次合并为一次更新的。

惰性更新

使用函数传入 useState,这样将结果作为初始值,且只执行一次,后续更新也不再执行,节省开销。

1
2
const initialState = Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);

当函数组件更新 re-render 时,函数组件内所有代码都会重新执行一遍。此时 initialState 的初始值是一个相对开销较大的 IO 操作。每次函数组件 re-render 时,第一行代码都会被执行一次,引起不必要的性能损耗。

1
2
const initialState = () => Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);

如何初始化数据?
可以先声明一个变量,用它接收修改后的值,然后把它当做初始值传入 useState.

1
2
3
4
5
let list:IBox[] = []
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef:useRef<HTMLDivElement>(null)})
}
const [boxes, setBoxes] = useState<IBox[]>(list)

useEffect

useEffect 相当于三种状态
分别是挂载后,更新后,卸载前.

  • componentDidMount: useEffect(()=>{},[])
  • componentDidUpdate,componentDidMount: useEffect(()=>{})
  • componentWillUnmount: useEffect(()=>{ return ()=>{ //这里相当于 compoonentWillUnmount }},[])

和 componentDidMount 和 componentDidUpdate 的区别:
ueEffect 是在真实 DOM 构建之后执行,componentDidMount 和 componentDidUpdate 是在真实 DOM 构建之前执行。
原因:useEffect 执行时异步的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//useEffect使用

//class示例
import React, { Component } from "react";
export default class demo extends Component {
state = { count: 0 };
//想要在title上点击更新title
componentDidUpdate() {
document.title = `clicked ${this.state.count} times`;
}
render() {
return (
<div>
count: {`clicked ${this.state.count} times`}
<button
onClick={() => {
this.setState({
count: (this.state.count += 1),
});
}}
>
add
</button>
</div>
);
}
}

//hook示例
import React, { useState, useEffect } from "react";
export default () => {
const [count, setCount] = useState(0);

/**
* useEffect相当于三种状态
* componentDidMount: useEffect(()=>{},[])
* componentDidUpdate,componentDidMount: useEffect(()=>{})
* componentWillUnmount: useEffect(()=>{
* return ()=>{ //这里相当于compoonentWillUnmount }},[])
*/
useEffect(() => {
document.title = `clicked ${count} times`;
});
return (
<div>
count: {`clicked ${count} times`}
<button onClick={() => setCount(count + 1)}>add</button>
</div>
);
};

useEffect 挂载时执行两次

这是 React18 在严格模式做的提醒,注意清除副作用。

useEffect 结合异步函数

需要将异步函数改为自执行的形式,因为 useEffect 返回清理资源的函数,而异步函数返回 Promise,
或者在回调内部创建一个异步函数.

为什么不能直接使用 async/await 呢?
原因: useEffect 的返回值是用来清除副作用的,执行操作应该是可预期的,而不应该是一个异步函数.
所以异步函数应该在回调函数内部.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建异步函数,并在useEffect中执行
useEffect(() =>{
const asyncFn = async () => {}
asyncFn()
},[])


// 使用自执行函数
useEffect(()=> {
(async () => {
await axios.get()
})()
}

useEffect 副作用

纯函数

  • 纯函数的输出和输入值以外的其他信息无关
  • 纯函数不能有可观察的函数副作用

副作用

  1. 引用外部变量
  2. 调用外部函数
  3. 只要不是在组件渲染时用到的变量,所有的操作都是副作用

副作用指函数在执行过程中,除了返回可能的函数值外,还对主调用函数产生附加的影响.

例如:修改了全局变量、修改了传入的参数、甚至是 console.log(), ajax 操作,直接修改 DOM,计时器函数,其他异步操作,其他会对外部产生影响的操作都是算作副作用

我们知道,如果在 useEffect 函数中返回一个函数,这个函数就是清除副作用函数,它会在组件销毁的时候执行,但是其实,它会在组件每次重新渲染时执行,并且先执行清除上一个 effect 的副作用。

规则

当我们编写组件时,应该尽量将组件编写为纯函数。
对于组件中的副作用,首先应该明确:

是**用户行为触发的**还是**视图渲染后主动触发的**?
对于前者,将逻辑放在Event handlers中处理。 对于后者,使用useEffect处理。 具体例子看下面可能不需要使用useEffect

执行时机

  1. 在组件重新 render()后,先执行清除副作用,后执行 useEffect 其他的。
  2. 在组件销毁时执行。
1
2
3
4
5
// 存在清理函数
// 1. render + 清理函数
// => render + 清理函数 + useEffect
// => render + 清理函数 + useEffect
// 组件销毁时执行。

注意上面第一条中,在 render()后执行清除副作用,也就是会先渲染新的 UI,再清除上一次的副作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
假设第一次渲染的时候props是{id: 10},第二次渲染的时候是{id: 20}。
你可能会认为发生了下面的这些事:

React 清除了 {id: 10}的effect。
React 渲染{id: 20}的UI
React 运行{id: 20}的effect。
(事实并不是这样。)

React只会在浏览器绘制后运行effects。
这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。
Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除:

React 渲染{id: 20}的UI
浏览器绘制。我们在屏幕上看到{id: 20}的UI
React 清除{id: 10}的effect。
React 运行{id: 20}的effect。

原因:
组件内的每一个函数(包括事件处理函数,effects,定时器或者API调用等等)
会捕获定义它们的那次渲染中的props和state。

effect的清除并不会读取“最新”的props。它只能读取到定义它的那次渲染中的props值



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { Component } from "react";
const MyAPI = {
count: 0,
subscribe(cb) {
this.intervalId = setInterval(() => {
this.count += 1;
cb(this.count);
}, 1000);
},
unSubscribe() {
clearInterval(this.intervalId);
},
reset() {
this.count = 0;
},
};
//class写法
export default class demo extends Component {
state = {
count: 0,
};
//设置定时器
componentDidMount() {
MyAPI.subscribe((count) => {
this.setState({ count: count });
});
}
//清除定时器
componentWillUnmount() {
MyAPI.unSubscribe();
}
render() {
return <div>{this.state.count}</div>;
}
}
//Hook写法
export default () => {
const [count, setCount] = useState(0);

//设置定时器
useEffect(() => {
MyAPI.subscribe((currentCount) => {
setCount(currentCount);
});
//使用清除副作用函数,清除定时器
return () => {
MyAPI.unSubscribe();
};
}, []);
return <div>{count}</div>;
};

依赖项

  1. 依赖项是数组[],在初次渲染和组件卸载时执行
  2. 有依赖项,且依赖项不一致时执行

依赖的类型

如果是 true,0,null,{},会不会更新?
这个原始类型和引用类型,跟 Object.is 比较有关系。
原始类型是不会变化的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function App() {
const [count, setCount] = useState(0);
const [initState, setInitState] = useState({});
// 通过Object.is比较发现initState是同一个对象引用,所以不更新
useEffect(() => {
console.log("useEffect");
}, [initState]);
// 这里是不同的对象引用,那么就会更新
useEffect(() => {
console.log("useEffect");
}, [{}]);
// 这里值不会变化,所以不更新
useEffect(() => {
console.log("useEffect");
}, [true]);

return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>click</button>
</div>
);
}

useEffect 定时器问题

为什么开发模式下,计时器每次多打印+2?

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

export default function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
function onTick() {
setCount((c) => c + 1);
}

setInterval(onTick, 1000);
}, []);

return <h1>{count}</h1>;
}

开发模式下,为了提示要记得清除副作用,每个组件都会重复挂载一次.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
const [count, setCount] = useState(0);
console.log("render", count);
useEffect(() => {
console.log("useEffect", count);
const id = setInterval(() => {
console.log("interval", count);
setCount(count + 1);
}, 5000);
return () => {
console.log("clear", count);
clearInterval(id)
};
}, []);

return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

// 解释
render => useEffect => interval =>setCount(1)
render => 因为依赖项为空,则不再执行useEffect。
但是内部的定时器还会执行。但是定时器拿到的count是第一次的count0,每次打印都是1
对比count没有变化 不再render




const [count, setCount] = useState(0);
// 使用传入函数的方式,保证拿到上一次的参数
// 这个定时器只有一个
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// 将count放入依赖项,会每次运行生成一个定时器
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
Ï }, [count]);

使用setState的函数形式,实现当前值的+1.

尽管 effect 只运行了一次,第一次渲染中的定时器回调函数可以完美地在每次触发的时候给 React 发送 c => c + 1 更新指令。它不再需要知道当前的 count 值。因为 React 已经知道了。

useLayoutEffect,和 useEffect 区别?在哪个阶段执行? beginWork,commit Work

二者的函数签名是相同的,不同点在于触发的时机不同.
useLayoutEffect是同步处理副作用.useEffect 是异步处理.
useLayoutEffect在 DOM 更新后,浏览器渲染之前,useLayoutEffect内部的更新将会同步刷新.
useEffect是在浏览器渲染后.
useLayoutEffect是在commitWork阶段.

useLayoutEffect

想在 useLayoutEffect 中实现 constructor()的类似方法,也就是在页面渲染前初始化数据,

1
2
3
4
5
6
7
8
9
10
11
12
13
const [boxes, setBoxes] = useState([]);
useLayoutEffect(() => {
let list = [];
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef });
}
setBoxes(list);
}, []);
useEffect(() => {
// 这里获取上面初始化后的boxes,可以修改了
console.log(boxes);
//
}, []);

useHook 与闭包

闭包陷阱

hooks 的闭包陷阱是指 useEffect 等 hook 中用到了某个 state,但是没有把它加到 deps 数组里,导致 state 变了,但是执行的函数依然引用着之前的 state

解决方法

它的解决方式就是正确设置 deps 数组,把用到的 state 放到 deps 数组里,这样每次 state 变了就能执行最新的函数,引用新的 state。同时要清理上次的定时器、事件监听器等。
useRef 能解决闭包陷阱的原因是 useEffect 等 hook 里不直接引用 state,而是引用 ref.current,这样后面只要修改了 ref 中的值,这里取出来的就是最新的。

解决 hooks 的闭包陷阱有两种方式:

  • 设置依赖的 state 到 deps 数组中并添加清理函数
  • 不直接引用 state,把 state 放到 useRef 创建的 ref 对象中再引用

处理定时器的时候,为保证计时的准确,最好使用 useRef 的方式,其余情况两种都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`); // 0,1,2,3...
}, 3000);
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

// 对比class组件
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);// 5,5,5,5,5
}, 3000);
}

上面两个对比,uesEffect 因为有闭包存在,记录的都是当时 count 的值.
class 组件里,React 修改了 class 中的 this.state 使其指向最新状态.所以全都是 5.

**每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state。**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 先点击show按钮,在快速点击增加count,会弹出显示多少?
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
// 弹出 0,handleAlertClick 的运行环境里,state 的 count 是 0,setTimeout 里也是 0,只不过延迟了

````

```jsx
export default function App() {
const [name, setName] = useState("");
const [show, setShow] = useState(false);
// 场景1,更改input,点击button,输出什么?
useEffect(() => {
if (show) {
console.log('name1',name);
}
}, [show]);
// 场景2,修改input内容,5秒后,输出什么?
useEffect(() => {
setTimeout(() => {
console.log('name2',name);
}, 5000);
}, []);

return (
<div className="App">
<input type="text" onChange={(e) => setName(e.target.value)} />
<button onClick={() => setShow(true)}>alert</button>
</div>
);
}
````

总结:
场景 1:依赖项是 show,show 改变时的 state 中,name 是已经改变的值。
场景 2:定时器里使用是 state 是初次挂载时的 state,也就是空。

```tsx
const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
// 挂载前
const oldTitle = document.title;
// 挂载时,传入的title赋给文档的title,而且oldTitle也改变了
useEffect(() => {
document.title = title;
}, []);
// 卸载时
useEffect(() => {
return () => {
if (!keepOnUnmount) {
// 因为这里面是一个闭包, 所以里面的oldTitle还是最初的title
// 如果不指定依赖,这里的闭包就让title是旧title
document.title = oldTitle;
}
};
}, []);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 案例
function App() {
const [count, setCount] = useState(0);
const [countTimeout, setCountTimeout] = useState(0);

useEffect(() => {
setTimeout(() => {
setCountTimeout(count);
}, 3000);
setCount(5);
}, []);

return (
//count发生了变化,但是3s后setTimout的count却还是0
<div>
Count: {count}
setTimeout Count: {countTimeout}
</div>
);
}

执行顺序

mount 顺序:父 => 子
effect 顺序:子 => 父
clean 顺序:子 => 父
如果想 effect 在父组件先执行,可以使用 useLayoutEffect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { useState, useEffect } from "react";

function Child() {
console.log("child mount");
useEffect(() => {
console.log("child effect");
return () => {
console.log("child clean");
};
}, []);

return <p>hello</p>;
}

function Parent() {
console.log("parent mount");
useEffect(() => {
console.log("parent effect");
return () => {
console.log("parent clean");
};
}, []);

return <Child />;
}

function App() {
console.log("App mount");
useEffect(() => {
console.log("App effect");
return () => {
console.log("App clean");
};
}, []);

return <Parent />;
}
export default App;
// 打印顺序
// App mount 2
// parent mount 2
// child mount
// child effect
// parent effect
// App effect
// child clean
// parent clean
// App clean
// child effect
// parent effect
// App effect

为什么会执行两次 effect,因为 React18 的并发模式会强制让组件更新一次,也就是先 clean=>effect。

竞态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import React, { useState, useEffect } from "react";

function Article({ id }) {
const [article, setArticle] = useState(null);

useEffect(() => {
let didCancel = false;
console.log("effect", didCancel);

function fetchData() {
console.log("setArticle begin", didCancel);
new Promise((resolve) => {
setTimeout(() => {
console.log("setTimeout", didCancel);
resolve(id);
}, id);
}).then((article) => {
// 快速点击 Add id 的 button,这里 didCancel 为什么会打印 true
console.log("setArticle end", didCancel, article);
if (!didCancel) {
// 把这一行代码注释就会出现错误覆盖状态值的情况
setArticle(article);
}
});
}
console.log("fetchData begin", didCancel);
fetchData();
console.log("fetchData end", didCancel);

return () => {
didCancel = true;
console.log("clear", didCancel);
};
}, [id]);

return <div>{article}</div>;
}

function App() {
const [id, setId] = useState(5000);
function handleClick() {
setId(id - 1000);
}
return (
<>
<button onClick={handleClick}>add id</button>
<Article id={id} />
</>
);
}

export default App;

快速连点两次,为什么 didCancel 是 true?
自己理解:记住,组件内部的函数每次执行都会捕获当时运行时的 props 和 state。

  1. 第一次运行时,didCancel 开始是 false,执行请求 fetchData,如果请求没来得及返回,还是 false。

但是,

  1. 注意,在还没返回时,又执行了 useEffect,就会先执行上一次的清除副作用。把上一次的 cancel 改为 true。

上一次的 fetchData 请求返回结果时,已经变成了 true。就不在执行 setArticle()了。

解释: 第一次点击,4 秒 setTimeout,页面刷新,渲染 3 秒的 UI,调用 4 秒的移除副作用,设置 4 秒的 didCancel 为 true.setTimeout 中的 didCancel 就是此时的 true
.第二次点击,3 秒 setTimeout,页面没有卸载或更新,didCancel 还是 false.
这里主要是 effect 移除副作用是在第二次 ui 渲染后,再移除上一次的 effect 副作用.因为它会捕获记住那一次的 props 和 state.setTimeout 中记录的也是上一次的.

class 组件解决竞态

1
2
3
4
5
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}

闭包陷阱示例

本质还是 useEffect 内部的 v 还是挂载时捕获的 state,input 改变,事件处理函数拿到的还是第一次的 state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const btn = useRef();
const [v, setV] = useState("");

useEffect(() => {
let clickHandle = () => {
console.log("v:", v);
};
btn.current.addEventListener("click", clickHandle);

return () => {
btn.current.removeEventListener("click", clickHandle);
};
}, [v]);

const inputHandle = (e) => {
setV(e.target.value);
};

return (
<>
<input value={v} onChange={inputHandle} />
<button ref={btn}>测试</button>
</>
);

产生原因

能访问到的自由变量 v 此时还是空值。当点击事件触发时,执行点击回调函数,此时先创建执行上下文,会拷贝作用域链到执行上下文中。

如果未在输入框内输入字符,此时点击拿到的 v 还是原来那个 v
如果在输入框内输入了字符,此时调用了 setV 修改了 state,页面触发 render,组件内部代码会重新执行一遍,重新声明了一个 v,v 就不再是原来那个 v,这里点击事件里作用域中的 v 还是旧的 v,这是两个不同的 v

产生场景

  • 事件绑定。比如示例代码中,在页面最初渲染完成后只绑定一次事件的情况,比如使用 echarts,在 useEffect 中获取 echarts 的实例并绑定事件
  • 定时器。页面加载后注册一个定时器,定时器内的函数也会产生如此的闭包问题。

解决办法

  1. 使用useRef替代useState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const btn = useRef();
const vRef = useRef("");
const [v, setV] = useState("");

useEffect(() => {
let clickHandle = () => {
console.log("v:", vRef.current);
};
btn.current.addEventListener("click", clickHandle);

return () => {
btn.current.removeEventListener("click", clickHandle);
};
}, []);

const inputHandle = (e) => {
let { value } = e.target;
vRef.current = value;
setV(value);
};

return (
<>
<input value={v} onChange={inputHandle} />
<button ref={btn}>测试</button>
</>
);

useRef 的方案之所以有效,是因为每次 input 的 change 修改的是 vRef 这个对象的 current 属性,而 vRef 始终是那个 vRef,即使 rerender,由于 vRef 是对象,所以变量存储在栈内存中的值是该对象在堆内存中的地址,只是一个引用,只修改对象的某个属性,该引用并不会改变。所以点击事件中的作用域链始终访问的都是同一个 vRef

  1. useEffect的依赖加上v.

方法虽然有用,但是每次 v 更新都会重新渲染,重新注册点击事件.只希望注册一次事件,所以也不是非常好.

Hook 规则

不要在循环,条件,嵌套中使用 Hook,确保它在最顶层.
只在 React 函数中调用 Hook,不要在 js 函数中调用.

1
2
3
4
5
6
7
8
9
10
11
12
//错误方式
if (count === 0) {
useEffect(() => {
document.title = `clicked ${count} times`;
});
}
//正确方式
useEffect(() => {
if (count === 0) {
document.title = `clicked ${count} times`;
}
});

Event 和 Effect

事件处理函数内部的逻辑是非响应式的。除非用户又执行了同样的操作(例如点击),否则这段逻辑不会再运行。事件处理函数可以在“不响应”他们变化的情况下读取响应式值。
Effect 内部的逻辑是响应式的。如果 Effect 要读取响应式值,你必须将它指定为依赖项。如果接下来的重新渲染引起那个值变化,React 就会使用新值重新运行 Effect 内的逻辑。

移除 Effect 依赖

每次有新的消息到达时,这个 Effect 会用新创建的数组更新 messages state:
它使用 messages 变量来 创建一个新的数组:从所有现有的消息开始,并在最后添加新的消息。然而,由于 messages 是一个由 Effect 读取的响应式值,它必须是一个依赖:

1
2
3
4
5
6
7
8
9
10
11
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ 所有依赖已声明
// ...

而让 messages 成为依赖会带来问题。

每当你收到一条消息,setMessages() 就会使该组件重新渲染一个新的 messages 数组,其中包括收到的消息。然而,由于该 Effect 现在依赖于 messages,这 也将 重新同步该 Effect。所以每条新消息都会使聊天重新连接。用户不会喜欢这样!

为了解决这个问题,不要在 Effect 里面读取 messages。相反,应该将一个 state 更新函数 传递给 setMessages:

1
2
3
4
5
6
7
8
9
10
11
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ 所有依赖已声明
// ...

可能不需要使用 useEffect

当 props 变化重置 state

ProfilePage 组件接收一个 prop:userId。页面上有一个评论输入框,你用了一个 state:comment 来保存它的值。有一天,你发现了一个问题:当你从一个人的个人资料导航到另一个时,comment 没有被重置。这导致很容易不小心把评论发送到不正确的个人资料。为了解决这个问题,你想在 userId 变化时,清除 comment 变量:
但这是低效的,因为 ProfilePage 和它的子组件首先会用旧值渲染,然后再用新值重新渲染。并且这样做也很复杂,因为你需要在 ProfilePage 里面 所有 具有 state 的组件中都写这样的代码。例如,如果评论区的 UI 是嵌套的,你可能也想清除嵌套的 comment state。

取而代之的是,你可以通过为每个用户的个人资料组件提供一个明确的键来告诉 React 它们原则上是 不同 的个人资料组件。将你的组件拆分为两个组件,并从外部的组件传递一个 key 属性给内部的组件:

1
2
3
4
5
6
7
8
9
export default function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}

function Profile({ userId }) {
// ✅ 当 key 变化时,该组件内的 comment 或其他 state 会自动被重置
const [comment, setComment] = useState("");
// ...
}

通常,当在相同的位置渲染相同的组件时,React 会保留状态。通过将 userId 作为 key 传递给 Profile 组件,使 React 将具有不同 userId 的两个 Profile 组件视为两个不应共享任何状态的不同组件。每当 key(这里是 userId)变化时,React 将重新创建 DOM,并 重置 Profile 组件和它的所有子组件的 state。现在,当在不同的个人资料之间导航时,comment 区域将自动被清空。

当 prop 变化时调整部分 state

有时候,当 prop 变化时,你可能只想重置或调整部分 state ,而不是所有 state。

List 组件接收一个 items 列表作为 prop,然后用 state 变量 selection 来保持已选中的项。当 items 接收到一个不同的数组时,你想将 selection 重置为 null:

1
2
3
4
5
6
7
8
9
10
11
// ❌ 错误示例
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 🔴 避免:当 prop 变化时,在 Effect 中调整 state
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}

这不太理想。每当 items 变化时,List 及其子组件会先使用旧的 selection 值渲染。然后 React 会更新 DOM 并执行 Effect。最后,调用 setSelection(null) 将导致 List 及其子组件重新渲染,重新启动整个流程。

让我们从删除 Effect 开始。取而代之的是在渲染期间直接调整 state:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不太合适的示例
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 好一些:在渲染期间调整 state
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}

像这样 存储前序渲染的信息 可能很难理解,但它比在 Effect 中更新这个 state 要好。上面的例子中,在渲染过程中直接调用了 setSelection。当它执行到 return 语句退出后,React 将 立即 重新渲染 List。此时 React 还没有渲染 List 的子组件或更新 DOM,这使得 List 的子组件可以跳过渲染旧的 selection 值。

在渲染期间更新组件时,React 会丢弃已经返回的 JSX 并立即尝试重新渲染。为了避免非常缓慢的级联重试,React 只允许在渲染期间更新 同一 组件的状态。如果你在渲染期间更新另一个组件的状态,你会看到一条报错信息。条件判断 items !== prevItems 是必要的,它可以避免无限循环。你可以像这样调整 state,但任何其他副作用(比如变化 DOM 或设置的延时)应该留在事件处理函数或 Effect 中,以 保持组件纯粹。

虽然这种方式比 Effect 更高效,但大多数组件也不需要它。无论你怎么做,根据 props 或其他 state 来调整 state 都会使数据流更难理解和调试。总是检查是否可以通过添加 key 来重置所有 state,或者 在渲染期间计算所需内容。例如,你可以存储已选中的 item ID 而不是存储(并重置)已选中的 item:

1
2
3
4
5
6
7
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ 非常好:在渲染期间计算所需内容
const selection = items.find((item) => item.id === selectedId) ?? null;
// ...
}

现在完全不需要 “调整” state 了。如果包含已选中 ID 的项出现在列表中,则仍然保持选中状态。如果没有找到匹配的项,则在渲染期间计算的 selection 将会是 null。行为不同了,但可以说更好了,因为大多数对 items 的更改仍可以保持选中状态。

在事件处理函数共享逻辑

假设你有一个产品页面,上面有两个按钮(购买和付款),都可以让你购买该产品。当用户将产品添加进购物车时,你想显示一个通知。在两个按钮的 click 事件处理函数中都调用 showNotification() 感觉有点重复,所以你可能想把这个逻辑放在一个 Effect 中
这个 Effect 是多余的。而且很可能会导致问题。例如,假设你的应用在页面重新加载之前 “记住” 了购物车中的产品。如果你把一个产品添加到购物车中并刷新页面,通知将再次出现。每次刷新该产品页面时,它都会出现。这是因为 product.isInCart 在页面加载时已经是 true 了,所以上面的 Effect 每次都会调用 showNotification()。

当你不确定某些代码应该放在 Effect 中还是事件处理函数中时,先自问 为什么 要执行这些代码。Effect 只用来执行那些显示给用户时组件 需要执行 的代码
在这个例子中,通知应该在用户 按下按钮 后出现,而不是因为页面显示出来时!删除 Effect 并将共享的逻辑放入一个被两个事件处理程序调用的函数中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
function ProductPage({ product, addToCart }) {
// ✅ 非常好:事件特定的逻辑在事件处理函数中处理
function buyProduct() {
addToCart(product);
showNotification(`已添加 ${product.name} 进购物车!`);
}

function handleBuyClick() {
buyProduct();
}

function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}

````

## 发送post请求
并不是请求必须在effect中.

例子:
这个 Form 组件会发送两种 POST 请求。它在页面加载之际会发送一个分析请求。当你填写表格并点击提交按钮时,它会向 /api/register 接口发送一个 POST 请求:

让我们应用与之前示例相同的准则。
分析请求应该保留在 Effect 中。这是 因为 发送分析请求是表单显示时就需要执行的(在开发环境中它会发送两次,请 参考这里 了解如何处理)。

然而,发送到 /api/register 的 POST 请求并不是由表单 显示 时引起的。你只想在一个特定的时间点发送请求:当用户按下按钮时。它应该只在这个 特定的交互 中发生。删除第二个 Effect,将该 POST 请求移入事件处理函数中:
```jsx
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ 非常好:这个逻辑应该在组件显示时执行
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

function handleSubmit(e) {
e.preventDefault();
// ✅ 非常好:事件特定的逻辑在事件处理函数中处理
post('/api/register', { firstName, lastName });
}
// ...
}
````

当你决定将某些逻辑放入事件处理函数还是 Effect 中时,你需要回答的主要问题是:从用户的角度来看它是 怎样的逻辑。如果这个逻辑是由某个特定的交互引起的,请将它保留在相应的事件处理函数中。如果是由用户在屏幕上 看到 组件时引起的,请将它保留在 Effect 中。

## 链式调用

有时候你可能想链接多个 Effect,每个 Effect 都基于某些 state 来调整其他的 state:
更好的做法是:尽可能在渲染期间进行计算,以及在事件处理函数中调整 state:

```tsx
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);

// ✅ 尽可能在渲染期间进行计算
const isGameOver = round > 5;

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('游戏已经结束了。');
}

// ✅ 在事件处理函数中计算剩下的所有 state
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('游戏结束!');
}
}
}
}

通知父组件有关 state 变化的信息

假设你正在编写一个有具有内部 state isOn 的 Toggle 组件,该 state 可以是 true 或 false。有几种不同的方式来进行切换(通过点击或拖动)。你希望在 Toggle 的 state 变化时通知父组件,因此你暴露了一个 onChange 事件并在 Effect 中调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

function updateToggle(nextIsOn) {
// ✅ 非常好:在触发它们的事件中执行所有更新
setIsOn(nextIsOn);
onChange(nextIsOn);
}

function handleClick() {
updateToggle(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}

// ...
}

通过这种方式,Toggle 组件及其父组件都在事件处理期间更新了各自的 state。React 会 批量 处理来自不同组件的更新,所以只会有一个渲染流程。

你也可以完全移除该 state,并从父组件中接收 isOn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 也很好:该组件完全由它的父组件控制
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}

// ...
}

“状态提升” 允许父组件通过切换自身的 state 来完全控制 Toggle 组件。这意味着父组件会包含更多的逻辑,但整体上需要关心的状态变少了。每当你尝试保持两个不同的 state 变量之间的同步时,试试状态提升

将数据传递给父组件

既然子组件和父组件都需要相同的数据,那么可以让父组件获取那些数据,并将其 向下传递 给子组件:

1

useMemo

useMemo 起作用的前提是,该组件必须是浅比较.
行为类似于 Vue 中的计算属性.防止内部因 js 属性导致无限循环.
另外当 obj 是对象的 state 时,不会无限循环.

1
2
3
4
5
import { useMemo } from "react";
const result = useMemo(() => {
//如果count变化则此函数重新执行
return result;
}, [count]);

案例: 是否能拿到 num1 最新的值?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function App() {
return <Demo1 />;
}

function Demo1() {
const [num1, setNum1] = useState(1);
const [num2, setNum2] = useState(10);

const text = useMemo(() => {
return `num1: ${num1} | num2:${num2}`;
}, [num2]);

function handClick() {
setNum1(2);
setNum2(20);
}

return (
<div>
{text}
<div>
<button onClick={handClick}>click!</button>
</div>
</div>
);
}

useMemo 中依赖的数组,作用是判断是否要执行回调函数。
上面虽然只是依赖了 num2,但是 num2 变化,同样执行了回调,num1 也就同样拿到了最新的值。

memo 方法

用于性能优化,节省开销.,如果本组件内数据未变化,阻止组件更新.
类似于 React.pureComponent,该函数会浅比较 props,state 是否变化,未变化就不重复渲染.
React.memo 有第二个参数。可以比较深层次的 props。表示是否相等, 返回值是布尔值。为 true 就不 re-render.

1
2
3
4
5
function arePropsEqual(prevProps, nextProps) {
return prevPRops === nextProps;
}

export default memo(Button, arePropsEqual);

与 shouldComponentUpdate 不同的是,arePropsEqual 返回 true 时,不会触发 render。返回 false 会触发。
而 shouldComponentUpdate 正好相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { PureComponent } from "react";
import Child from "./child";
export default class demo extends PureComponent {
state = {
time: new Date(),
};
componentDidMount() {
setInterval(() => {
//当time设置为1时,即使渲染还是1,但是log还是会打印render.
//使用PureComponent可以浅比较发现数据没有变化,就不会继续打印.
// this.setState({ time: 1},1000)
this.setState({ time: new Date() }, 1000);
});
}

render() {
console.log("render");
return (
<div>
{/* 加入child组件 */}
{/* 当赋值没有变化时,依旧会渲染log */}
<Child seconds={1} />
{this.state.time.toString()}
</div>
);
}
}

//Child组件
const Child = ({ seconds }) => {
console.log("child");
return <div>current time: {seconds}</div>;
};
//加入memo防止组件重复渲染
export default React.memo(Child);

useCallback

返回一个 memoized 回调函数。
useCallback 只是根据依赖是否变化,返回新的函数或者是之前缓存的函数。
依旧是节省资源.
被 useCallback 包裹的函数是否执行,取决于后面的参数是否变化.
如果后面参数没有执行,而且在第一次的时候时可以执行一次的.

当写自定义 hook 时,返回的函数大概率需要用 useCallback 包裹.返回的函数保证不会每次都变化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState, useCallback } from "react";
export default () => {
const [count1, setCount1] = useState(0);
const [count, setCount] = useState(0);
//useCallback第二个参数决定是否允许第一个参数执行
//如果count发生变化则允许执行
//useCallback第一个参数会先执行一次,之后才判断第二个参数是否变化
return (
<div>
<p>{count1}</p>
<button onClick={() => setCount(count1 + 1)}>click</button>
<p>{count}</p>
<button onClick={useCallback(() => setCount(count + 1), [count])}>
click
</button>
</div>
);
};

useCallback 和 useEffect

复用请求逻辑时,可以 使用 useCallback,或者自定义 hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 1. 把公共逻辑放到组件外

// 你不再需要把它设为依赖,因为它们不在渲染范围内,
// 因此不会被数据流影响。它不可能突然意外地依赖于props或state。
// ✅ Not affected by the data flow
function getFetchUrl(query) {
return "https://hn.algolia.com/api/v1/search?query=" + query;
}

function SearchResults() {
useEffect(() => {
const url = getFetchUrl("react");
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK

useEffect(() => {
const url = getFetchUrl("redux");
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK

// ...
}

// 2. 使用useCallback

// useCallback本质上是添加了一层依赖检查。
// 它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖
function SearchResults() {
// ✅ Preserves identity when its own deps are the same
const getFetchUrl = useCallback(
(query) => {
return "https://hn.algolia.com/api/v1/search?query=" + query;
},
[query]
); // ✅ Callback deps are OK

useEffect(() => {
const url = getFetchUrl("react");
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK

useEffect(() => {
const url = getFetchUrl("redux");
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK

// ...
}

// 3. 请求属性从父级传入

function Parent() {
const [query, setQuery] = useState("react");

// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = "https://hn.algolia.com/api/v1/search?query=" + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK

return <Child fetchData={fetchData} />;
}

function Child({ fetchData }) {
let [data, setData] = useState(null);

useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK

// ...
}

在 class 组件中,函数属性本身并不是数据流的一部分。组件的方法中包含了可变的 this 变量导致我们不能确定无疑地认为它是不变的。因此,即使我们只需要一个函数,我们也必须把一堆数据传递下去仅仅是为了做“diff”。我们无法知道传入的 this.props.fetchData 是否依赖状态,并且不知道它依赖的状态是否改变了。

使用 useCallback,函数完全可以参与到数据流中。我们可以说如果一个函数的输入改变了,这个函数就改变了。如果没有,函数也不会改变。感谢周到的 useCallback,属性比如 props.fetchData 的改变也会自动传递下去。

useCallback 可以用 useMemo 实现

1
2
3
useCallback(fn, []);

useMemo(() => fn, []);

useMemo 和 useCallback 准则

  1. 大部分的 useMemo 和 useCallback 都应该被移除,他们可能没有带来性能上的优化,反而增加首次渲染的负担,增加程序的复杂性。
  2. 使用 useMemo/useCallback 优化子组件 re-render 时,必须满足以下条件才有效。
    1. 子组件已通过 React.memo/useMemo 被缓存
    2. 子组件所有 props 被缓存

如何判断是否应该缓存组件?

  1. react dev tools profiler
  2. useRenderTImes

什么时候应该用 useMemo/useCallback?

  1. 防止不必要的 effect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Component = () => {
const a = useMemo(() => ({ test: 1 }), []);
useEffect(() => {
// 只有当a变化了,这里才触发
doSomething();
}, [a]);
};

const Component = () => {
const fetch = useCallback(() => {
// fetch数据
}, []);

useEffect(() => {
// 仅fetch值被改变时,才会触发
fetch();
}, [fetch]);
};
  1. 防止不必要的 re-render
    1. 组件什么时候会 re-render
      1. 本身的 props 或者 state 改变时
      2. Context value 改变,使用该值的组件会 re-render
      3. 当父组件重新渲染,所有的子组件都会 re-render
1
2
3
4
5
6
7
8
9
10
11
12
// 必须同时缓存组件本身和onClick方法
const PageMemoized = React.memo(Page);

const App = () => {
const [state, setState] = useState(0);

const onClick = useCallback(() => {
//dosomething
}, []);

return <PageMemorized onClick={onClick} />;
};
  1. 防止不必要的重复计算

useReducer

useReducer 是另一种返回状态的方法.可以说是useState的升级版本.和 redux 的差别在于并不能进行全局的状态管理.
useState 适合单个状态,useReducer 适合多个互相有影响的状态.
对于单个状态,可以使用 useState,多个状态,且互相关联,就可以使用 useReducer.

它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

假如层级过深,还可以搭配 context 使用,此时使用 dispatch 代替 callback 优势更明显。因为 dispatch 在 re-render 时不变,不会引起使用 context 的组件执行无意义的更新。
批量更新 react 对事件之外的更新不会批量处理,使用 reducer 可以避免此类问题

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

const initalState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "dcrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
export default () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "dcrement" })}>-</button>
</div>
);
};

useReducer 与 useEffect

使用 useReducer 进行解耦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React, { useReducer, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

// 相比如使用两个state在useEffect中
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
// 上面的方案会在修改step后重新创建定时器,如果想不再创建可以使用useReducer解耦

// 使用useReducer更合适
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, []);

return (
<>
<h1>{count}</h1>
<input
value={step}
onChange={(e) => {
dispatch({
type: "step",
step: Number(e.target.value),
});
}}
/>
</>
);
}

const initialState = {
count: 0,
step: 1,
};

function reducer(state, action) {
const { count, step } = state;
if (action.type === "tick") {
return { count: count + step, step };
} else if (action.type === "step") {
return { count, step: action.step };
} else {
throw new Error();
}
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

useReducer 依赖 props 时

我们可以把 reducer 函数放到组件内去读取 props
这种模式会使一些优化失效,所以你应该避免滥用它,不过如果你需要你完全可以在 reducer 里面访问 props。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);

function reducer(state, action) {
if (action.type === "tick") {
return state + step;
} else {
throw new Error();
}
}

useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);

return <h1>{count}</h1>;
}

即使是在这个例子中,React 也保证 dispatch 在每次渲染中都是一样的。 所以你可以在依赖中去掉它。它不会引起 effect 不必要的重复执行。

你可能会疑惑:这怎么可能?在之前渲染中调用的 reducer 怎么“知道”新的 props?答案是当你 dispatch 的时候,React 只是记住了 action - 它会在下一次渲染中再次调用 reducer。在那个时候,新的 props 就可以被访问到,而且 reducer 调用也不是在 effect 里。

这就是为什么我倾向认为 useReducer 是 Hooks 的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。结果是,这可以帮助我移除不必需的依赖,避免不必要的 effect 调用。

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext providercontext value
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>来为下层组件提供 context

createContext

1
const context = createContext(defaultValue);

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { useContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
//先创建一个Context
const AppContext = React.createContext({});

const Navbar = () => {
const { username } = useContext(AppContext);

return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
};

const Messages = () => {
const { username } = useContext(AppContext);

return (
<div className="messages">
<h1>Messages</h1>
<p>1 message for {username}</p>
<p className="message">useContext is awesome!</p>
</div>
);
};
//用创建的Context包裹组件
function App() {
return (
<AppContext.Provider
value={{
username: "superawesome",
}}
>
<div className="App">
<Navbar />
<Messages />
</div>
</AppContext.Provider>
);
}

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

全局使用

创建 context

使用 createContext 方法创建 context

1
2
3
4
5
6
7
8
9
10
11
12
// 泛型可以约束传入的内容
const AuthContext = createContext<
| {
user: User | null;
login: (form: AuthForm) => Promise<void>;
register: (form: AuthForm) => Promise<void>;
logout: () => void;
}
| undefined
>(undefined);
// context 的名字
AuthContext.displayName = "AuthContext";

创建 Provider 及其附属方法

1
2
3
4
5
6
7
8
9
10
11
12
13
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);

const login = (form: AuthForm) => auth.login(form).then(setUser);
const register = (form: AuthForm) => auth.register(form).then(setUser);
const logout = () => auth.logout();
return (
<AuthContext.Provider
children={children}
value={{ user, login, register, logout }}
/>
);
};

使 Context 在全局生效

包裹 App 组件

1
2
3
4
5
6
7
root.render(
<React.StrictMode>
<AppProviders>
<App />
</AppProviders>
</React.StrictMode>
);

调用 context

使用 useHook 自定义 hook 调用 context

1
2
3
4
5
6
7
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth必须在AuthProvider中使用");
}
return context;
};

页面中使用

1
const { login, user } = useAuth();

useRef

类组件、React 元素用 React.createRef,函数组件使用 useRef
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue).

第二个作用是保存数据
即使组件重新渲染,保存的数据仍然存在.保存的数据被更改不会触发组件渲染.

useRef 是在 memorizedState 链表中放一个对象,current 保存某个值。
初始化的时候创建了一个对象放在 memorizedState 上,后面始终返回这个对象。

这样通过 useRef 保存回调函数,然后在 useEffect 里从 ref.current 来取函数再调用,避免了直接调用,也就没有闭包陷阱的问题了。

何时使用 ref

通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:

  1. 存储 timeout ID
  2. 存储和操作 DOM 元素
  3. 存储不需要被用来计算 JSX 的其他对象。

如果你的组件需要存储一些值,但不影响渲染逻辑,请选择 ref。

注意:
使用 useRef 创建 div 元素时,如果存在循环创建,一定要把创建写在里面,不要在外面创建一个然后引用,因为引用的都是同一个

1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误❌
const boxRef = useRef < HTMLDivElement > null;

let list: IBox[] = [];
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef }); // 这里是10个一样的
}
// 正确✅
let list: IBox[] = [];
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef: useRef < HTMLDivElement > null });
// 这里是10个就是不一样的
}
1
2
3
4
5
6
7
8
9
10
11
12
const fn = () => {
console.log(count);
};
const ref = useRef(fn);

useLayoutEffect(() => {
ref.current = fn;
});

useEffect(() => {
setInterval(() => ref.current(), 500);
}, []);

useEffect 里执行定时器,deps 设置为了 [],所以只会执行一次,回调函数用的是 ref.current,没有直接依赖某个 state,所以不会有闭包陷阱。
用 useRef 创建个 ref 对象,初始值为打印 count 的回调函数,每次 render 都修改下其中的函数为新创建的函数,这个函数里引用的 count 就是最新的。
这里用了 useLayoutEffect 而不是 useEffect 是因为 useLayoutEffect 是在 render 后同步执行的,useEffect 是在 render 后异步执行的,所以用 useLayoutEffect 能保证在 useEffect 之前被调用。
这种方式避免了 useEffect 里直接对 state 的引用,从而避免了闭包问题。
另外,修改 count 的地方,可以用 setCount(count => count + 1) 代替 setCount(count + 1),这样也就避免了闭包问题

1
const refContainer = useRef(initialValue);

useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的 ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
function Parent() {
let [number, setNumber] = useState(0);
return (
<>
<Child />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
</>
);
}
let input;
function Child() {
const inputRef = useRef();
console.log("input===inputRef", input === inputRef);
input = inputRef;
function getFocus() {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={getFocus}>获得焦点</button>
</>
);
}
ReactDOM.render(<Parent />, document.getElementById("root"));

useRef 使用的细节

  1. 函数组件能否使用 createRef?

可以但是最好不要用。createRef 主要是 class 组件访问 dom 的,最佳实践是整个生命周期只用一次,一般在构造函数中。如果在函数组件中使用会造成每次 render 都会调用 createRef。

  1. 每次渲染 useRef 返回值不变
  2. ref.current 发生变化不会造成 re-render。
  3. ref.current 发生变化应该作为 side effect(因为它会影响下次渲染)。所以不应该在 render 阶段更新 current 属性。
  4. 修改 ref.current 的值不能另外赋值再修改,必须修改 ref.current。这和指针有关。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function App() {
const RenderCounter = () => {
const counter = useRef(0);

const handler = () => {
counter.current = counter.current + 1;
console.log(counter.current); // 虽然打印123,但是没有re-render,页面始终是0
//所以应该在useEffect中写
}

return (
<div>
<button onClick={handler}>add</button>
<h1>{`The component has been re-rendered ${counter.current} times`}</h1>
</div>
);
}
return <RenderCounter />

函数回调获取 ref(ref callback)

ref 属性除了接受 ref 对象之外,还可以接受函数也就是 ref callback。在该函数中,DOM 元素作为其唯一参数。
与 effect 函数一样,React 在组件周期中的某些时刻中调用它。当创建 DOM 元素之后会立即执行 ref callback(参数是 DOM 元素),在删除元素时也会再次调用 ref callback,只不过这时的参数是 null。
如果 ref callback 被定义为内联函数,React 将在每次渲染时调用它两次,第一次的参数是 null,第二次的参数是 DOM 元素。
虽然内联 ref callback 被调用两次可能会令人惊讶,如果从 React 的角度来看,我认为这种行为是合理的。它保证了一致性,因为每次渲染都会创建新的函数实例,它可能是一个完全不同的函数。这些函数可能会依赖 props 或 state,而这些 props 或 state 也可能在此期间发生了变化。
因此 React 需要清除旧的 ref callback(参数是 null),然后设置新的回调(参数是 DOM 元素)。这样我们可以根据条件来设置 ref 属性的值,甚至在 React 元素之间交换它们

1
2
3
4
5
6
7

function Child() {
let inputRef;

return (
<>
<input type="text" ref={dom =>{ inputRef = dom}} />
1
2
3
4
5
6
7
const [size, setSize] = useState();

const measureRef = useCallback((node) => {
setSize(node.getBoundingClientRect());
}, []);

return <div ref={measureRef}>{children}</div>;

在这个案例中,没有选择使用 useRef,因为当 ref 是一个对象时,它并不会把当前 ref 值的变化情况通知到我们。使用 callback ref 可以确保即便被测量的节点在子组件延迟显示 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

useRef 案例

案例: 主要利用 ref 保存了计时器的设置函数,因为在 useEffect 中,每次计算都会重新计算,如果不加 count 依赖,就不再重新计时,需要使用 ref 将计时器方法保存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//
import React, { useState, useEffect, useRef } from "react";
import "./style.css";

export default function App() {
const [open, setOpen] = useState(false);
const [count, setCount] = useState(0);
const ref = useRef();

useEffect(() => {
ref.current = () => {
setCount(count + 1);
};
});

useEffect(() => {
let timer = null;
if (open) {
console.log(open, "timer open");
timer = setInterval(() => ref.current(), 1000);
}
if (!open) {
console.log(open);
}
return () => {
clearInterval(timer);
console.log(open, "卸载");
};
}, [open]);

const toggleModal = () => {
if (!open) {
setOpen(true);
} else {
setOpen(false);
}
};
const Modal = () => {
return <div style={{ display: open ? "block" : "none" }}>模态框</div>;
};
return (
<div>
<div>{count}</div>
<button onClick={toggleModal}>打开模态框</button>
<Modal />
</div>
);
}

forwardRef

接收两个参数,一个是 props,一个是 ref.

1
2
3
4
5
6
7
8
9
10
//因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
function Parent() {
return (
<>
// <Child ref={xxx} /> 这样是不行的
<Child />
<button>+</button>
</>
);
}
  • forwardRef 可以在父组件中操作子组件的 ref 对象
  • forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
  • 子组件接受 props 和 ref 作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Child(props, ref) {
return <input type="text" ref={ref} />;
}
Child = React.forwardRef(Child);
function Parent() {
let [number, setNumber] = useState(0);
// 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
// 只有当它被赋给某个元素的 ref 属性时,才会有值
// 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
// 那么父组件就可以操作子组件中的某个元素
// 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
// 所以就需要用到 forwardRef 进行转发
const inputRef = useRef(); //{current:''}
function getFocus() {
inputRef.current.value = "focus";
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef} />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
);
}

useImperativeHandle

  • useImperativeHandle 可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
  • 父组件可以使用操作子组件中的多个 ref
  • 使用场景: 父组件调用子组件方法

作用: 将父级的 ref 拦截,把回调函数返回的值赋给父级的 ref.达到了自定义暴露给父组件的目的.

参数有三个: ref , 回调函数, 依赖项[].

1
2
3
function useImperativeHandle(ref, cb) {
ref.current = cb();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';

function Child(props,parentRef){
// 子组件内部自己创建 ref
let focusRef = useRef();
let inputRef = useRef();
useImperativeHandle(parentRef,()=>{
// 这个函数会返回一个对象
// 该对象会作为父组件 current 属性的值
// 通过这种方式,父组件可以使用操作子组件中的多个 ref
return {
//注意这里是返回对象,必须有{}
focusRef,
inputRef,
name:'计数器',
focus(){
focusRef.current.focus();
},
changeText(text){
inputRef.current.value = text;
}
}
}};
return (
<>
<input ref={focusRef}/>
<input ref={inputRef}/>
</>
)

}
// 子组件需要forwardRef包裹
ForwardChild = forwardRef(Child);

function Parent(){
const parentRef = useRef();//{current:''}
function getFocus(){
parentRef.current.focus();
// 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
parentRef.current.addNumber(666);
parentRef.current.changeText('<script>alert(1)</script>');
console.log(parentRef.current.name);
}
return (
<>
// 子组件上绑定父组件设置的ref
<ForwardChild ref={parentRef}/>
<button onClick={getFocus}>获得焦点</button>
</>
)
}

useTransition

用于提升性能,一般用在大量数据展示时,频繁修改请求参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState, useTransition } from "react";
const Home: React.FC = () => {
const [val, setVal] = useState("");
const [arr, setArr] = useState<number[]>([]);
// pending 就是确定当前状态是否是已经设置的,可搭配loading
const [pending, transition] = useTransition();

const getList = (e: any) => {
setVal(e.target.value);
let arr = Array.from(new Array(10000), (item) => Date.now());
// 这里transition可以将数据减缓,相当于debounce
transition(() => {
setArr(arr);
});
};
return (
<div className={styles.box}>
<input value={val} onChange={getList} />
{pending ? (
<h2>loading....</h2>
) : (
arr.map((item, key) => <div key={key}>{item}</div>)
)}
</div>
);
};

export default Home;

新增 hook

useEffectEvent

试验性特性引入

1
import { experimental_useEffectEvent as useEffectEvent } from "react";

提取非响应式逻辑

例如,假设你想在用户连接到聊天室时展示一个通知。并且通过从 props 中读取当前 theme(dark 或者 light)来展示对应颜色的通知
当 roomId 变化时,聊天会和预期一样重新连接。但是由于 theme 也是一个依赖项,所以每次你在 dark 和 light 主题间切换时,聊天 也会 重连。这不是很好!

1
2
3
4
5
6
7
8
9
10
11
12
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ 声明所有依赖项
// ...

使用 useEffectEvent 这个特殊的 Hook 从 Effect 中提取非响应式逻辑

这里的 onConnected 被称为 Effect Event。它是 Effect 逻辑的一部分,但是其行为更像事件处理函数。它内部的逻辑不是响应式的,而且能一直“看见”最新的 props 和 state。

现在你可以在 Effect 内部调用 onConnected Effect Event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ChatRoom({ roomId, theme }) {
// Effect Event
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 声明所有依赖项
// ...

Effect Event 是非响应式的并且必须从依赖项中删除。

接收参数

effect event 是非响应式的逻辑,接收的参数就是一个事件调用的对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import { useState, useEffect, useRef } from "react";
import { FadeInAnimation } from "./animation.js";
import { experimental_useEffectEvent as useEffectEvent } from "react";

function Welcome({ duration }) {
const ref = useRef(null);

const onAppear = useEffectEvent((animation) => {
animation.start(duration);
});

useEffect(() => {
const animation = new FadeInAnimation(ref.current);
// 接收参数animation
onAppear(animation);
return () => {
animation.stop();
};
}, []);

return (
<h1
ref={ref}
style={{
opacity: 0,
color: "white",
padding: 50,
textAlign: "center",
fontSize: 50,
backgroundImage:
"radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)",
}}
>
欢迎
</h1>
);
}

export default function App() {
const [duration, setDuration] = useState(1000);
const [show, setShow] = useState(false);

return (
<>
<label>
<input
type="range"
min="100"
max="3000"
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
/>
淡入 interval: {duration} ms
</label>
<button onClick={() => setShow(!show)}>{show ? "移除" : "显示"}</button>
<hr />
{show && <Welcome duration={duration} />}
</>
);
}

局限性

Effect Event 的局限性在于你如何使用他们:

只在 Effect 内部调用他们。
永远不要把他们传给其他的组件或者 Hook。

动态数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//view
<div style="width: 100%;height:700rpx;" ref="bar"></div>
//script
data(){
option: {
tooltip: {},
dataset: {
dimensions: ["teamGroup", "oeeRate"],
source: this.monthsData, //主要数据(后台返回值)
},
xAxis: {
type: "category",
},
yAxis: {},
legend: {
show: false, //图例不展示
},
series: [
{
type: "bar",
// data: [5, 20, 36, 10],
itemStyle: { //柱状图上的值
normal: {
label: {
show: true,
position: "top",
textStyle: {
color: "#999",
fontSize: 14,
},
},
color: (params) => {
const colorList = [
"#df6e6a",
"#83bfdb",
"#599f76",
"#ed895d",
];
return colorList[params.dataIndex];
},
},
},
},
],
},
},

watch: {
option: {
handler(newVal, oldVal) {
if (this.monthsData.length > 0) {
this.$nextTick(() => {
const myChart = this.$echarts.init(this.$refs["bar"]);
if (myChart) {
if (newVal) {
myChart.setOption(newVal);
} else {
myChart.setOption(oldVal);
}
}
});
}
},
immediate: true,
deep: true,
}
}

动态数据,包含初始化

适用于多个 div 导致报错问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
  public option: any = {
title: {
text: '',
left: 'center',
x: 'center',
y: 'top',
subtext: '',
},
grid: {
left: '14%',
right: '9%',
},
tooltip: {
show: true,
showContent: true,
},
// legend: {
// show: false,
// },
dataset: {
source: [],
},
series: [
{
type: 'pie',
radius: '50%',
// label: {
// show: true,
// position: 'bottom',
// },
},
],
};
mounted() {
this.init();
this.getData();
}

init() {
this.myChart = echarts.init(document.getElementById(this.id));
this.myChart.setOption(this.option);
}

@Watch('option', { immediate: true, deep: true })
public handlerOption(newVal, oldVal) {
if (this.dataList.length > 0) {
this.$nextTick(() => {
const myChart = this.myChart;
if (myChart) {
if (newVal) {
myChart.setOption(newVal);
} else {
myChart.setOption(oldVal);
}
} else {
this.init();
}
});
}
}

除了基本的嵌套.还有一些常用的.

变量

1
2
3
4
5
6
@width: 10px;
@height: @width + 20px;
#header {
width: @width;
height: @height;
}

混合

将一组属性从一个规则集包含到另一个规则集的方法.
比如将下面的类放到其他类中.

1
2
3
4
.bordered {
border-top: dotted 1px black;
border-bottom: solid 2px black;
}

放到下面中使用

1
2
3
4
#menu a {
color: #111;
.bordered();
}

嵌套

clearfix

1
2
3
4
5
6
7
8
9
10
11
12
.clearfix {
display: block;
zoom: 1;

&::after {
content: '';
display: block;
font-size: 0;
heightL 0;
clear: both;
visibility: hidden;
}

嵌套条件语句

转义

1
2
3
4
5
6
7
8
9
10
11
12
13
@min768: (min-width: 768px);
.element {
@media @min768 {
font-size: 1.2rem;
}
}

//编译为
@media (min-width: 768px) {
.element {
font-size: 1.2rem;
}
}

导入

导入一个.less文件,此文件中的所有变量就可以全部使用.
另外.less可以省略文件名.

1
2
@import "library"; //library.less
@import "typo.css";

stlyelint 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// @see: https://stylelint.io

module.exports = {
extends: [
"stylelint-config-standard", // 配置stylelint拓展插件
"stylelint-config-prettier", // 配置stylelint和prettier兼容
"stylelint-config-recess-order", // 配置stylelint css属性书写顺序插件,
],
plugins: ["stylelint-less"], // 配置stylelint less拓展插件
customSyntax: "postcss-less",
rules: {
indentation: null, // 指定缩进空格
"no-invalid-double-slash-comments": null, // 禁止双斜杠注释
"no-descending-specificity": null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
"function-url-quotes": "always", // 要求或禁止 URL 的引号 "always(必须加上引号)"|"never(没有引号)"
"string-quotes": "double", // 指定字符串使用单引号或双引号
"unit-case": null, // 指定单位的大小写 "lower(全小写)"|"upper(全大写)"
"color-hex-case": "lower", // 指定 16 进制颜色的大小写 "lower(全小写)"|"upper(全大写)"
"color-hex-length": "long", // 指定 16 进制颜色的简写或扩写 "short(16进制简写)"|"long(16进制扩写)"
"rule-empty-line-before": "never", // 要求或禁止在规则之前的空行 "always(规则之前必须始终有一个空行)"|"never(规则前绝不能有空行)"|"always-multi-line(多行规则之前必须始终有一个空行)"|"never-multi-line(多行规则之前绝不能有空行。)"
"font-family-no-missing-generic-family-keyword": null, // 禁止在字体族名称列表中缺少通用字体族关键字
"block-opening-brace-space-before": "always", // 要求在块的开大括号之前必须有一个空格或不能有空白符 "always(大括号前必须始终有一个空格)"|"never(左大括号之前绝不能有空格)"|"always-single-line(在单行块中的左大括号之前必须始终有一个空格)"|"never-single-line(在单行块中的左大括号之前绝不能有空格)"|"always-multi-line(在多行块中,左大括号之前必须始终有一个空格)"|"never-multi-line(多行块中的左大括号之前绝不能有空格)"
"property-no-unknown": null, // 禁止未知的属性(true 为不允许)
"no-empty-source": null, // 禁止空源码
"declaration-block-trailing-semicolon": null, // 要求或不允许在声明块中使用尾随分号 string:"always(必须始终有一个尾随分号)"|"never(不得有尾随分号)"
"selector-class-pattern": null, // 强制选择器类名的格式
"value-no-vendor-prefix": null, // 关闭 vendor-prefix(为了解决多行省略 -webkit-box)
"at-rule-no-unknown": null,
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["global", "v-deep", "deep"],
},
],
},
};

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{
"name": "react-admin-antd",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build:dev": "tsc && vite build --mode development",
"build:test": "tsc && vite build --mode test",
"build:pro": "tsc && vite build --mode production",
"preview": "vite preview",
"lint:eslint": "eslint --fix --ext .js,.ts,.tsx ./src",
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged",
"prepare": "husky install",
"release": "standard-version",
"commit": "git pull && git add -A && git-cz && git push"
},
"dependencies": {
"@ant-design/icons": "^5.0.1",
"antd": "^5.2.1",
"axios": "^1.3.3",
"dotenv": "^16.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.1",
"react-transition-group": "^4.4.5",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"redux-promise": "^0.6.0",
"redux-thunk": "^2.4.2"
},
"devDependencies": {
"@commitlint/cli": "^17.4.3",
"@commitlint/config-conventional": "^17.4.3",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-transition-group": "^4.4.5",
"@types/redux-promise": "^0.5.29",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.2",
"postcss": "^8.4.21",
"postcss-less": "^6.0.0",
"prettier": "^2.8.4",
"rollup-plugin-visualizer": "^5.9.0",
"stylelint": "^15.1.0",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-recess-order": "^4.0.0",
"stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^30.0.1",
"stylelint-less": "^1.0.6",
"typescript": "^4.9.3",
"vite": "^4.1.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-svg-icons": "^2.0.1"
}
}

报错提醒

stylelint 一直报错,

原因

css 中不支持//作为注释前缀,必须是/**/.
而 less 是支持的,而且 stylelint 也支持 less 或 scss 这样,但是就是报错.

解决方法

安装postcss-less,在stylelintrc.js

1
2
// stylelintrc.js
customSyntax: "postcss-less",

原理

通过node-xlsx或者js-xlsx将数据表处理成 data 多维数组输出后自行处理.

安装

1
npm i node-xlsx

新增文件夹,将 xlsx 文件放到excel文件夹下.
iShot2021-11-03 16.42.19.png

文件预览

iShot2021-11-03 16.45.35.png

创建执行文件 index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const fs = require("fs");
const xlsx = require("node-xlsx");

//几列数据,返回name是表单几,data是数组数据
const excelData = xlsx.parse("./excel/card.xlsx");

let arr = [];

// console.log(excelData)
//输出
// [
// {
// name: "Sheet1",
// data: [
// ["goods", "feifan", "jiujiu"],
// ["屈50", 43, 44],
// ["呷50", 36, 33],
// ["德30", 27, 32],
// ["奈30", 20, 31],
// ["本50", 43.5, 44.2],
// ["本50", 43.5, 44.2],
// ],
// }
// ];
//现在要做的就是把这个data,组合成想要的json格式,如下

// [
// {
// goods: '屈50',
// feifan: 43,
// jiujiu: 44
// },
// {
// goods: '呷50',
// feifan: 36,
// jiujiu: 33
// },
// {
// goods: '德30',
// feifan: 27,
// jiujiu: 32
// },
// ...
// ]

函数处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let handleExcel = () => {
excelData.map((item) => {
let xlsData = item.data;
let keyArr = xlsData[0]; //key的数组
let dataArr = xlsData.slice(1); //data的数组,除去第一行

dataArr.map((lineItem) => {
let arrItem = {};
lineItem.map((i, index) =>
Object.assign(arrItem, { [keyArr[index]]: i })
); //再加一层[]是因为keyArr[index]是字符串
arr.push(arrItem);
});
});
};

//文件输出方法
let generatJSON = (fileName, data) => {
fs.writeFile(fileName, data, "UTF-8", (err) => {
if (err) {
console.log(err);
} else {
console.log("success");
}
});
};

handleExcel();
generatJSON("./data/data.json", JSON.stringify(arr, null, "\t"));

执行

1
node inde.js

查看 data 文件夹下的输出文件