0%

python中常见错误

opencv相关

最基础的imread、imwrite都不能使用

1
cv2.error: OpenCV(4.2.0) C:\projects\opencv-python\opencv\modules\highgui\src\window.cpp:651: error: (-2:Unspecified error) The function is not implemented.

解决办法:降低python版本为3.7

环境配置

安装 — MMPose 1.1.0 文档

注意:python版本使用3.7的,要不然opencv会出错,什么原因不知道

错误明细

手势识别运行

  • 图片手势识别
1
python demo/topdown_demo_with_mmdet.py demo/mmdetection_cfg/cascade_rcnn_x101_64x4d_fpn_1class.py cascade_rcnn_x101_64x4d_fpn_20e_onehand10k-dac19597_20201030.pth configs/hand_2d_keypoint/topdown_heatmap/onehand10k/td-hm_hrnetv2-w18_8xb64-210e_onehand10k-256x256.py hrnetv2_w18_onehand10k_256x256-30bc9c6b_20210330.pth --input tests/data/onehand10k/9.jpg --show --draw-heatmap

深度学习原理

梯度下降

深度学习简单实现

最基础线性规划实现

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
import tensorflow as tf
from tensorflow.keras import datasets
(x, y), (x_val, y_val) = datasets.mnist.load_data()
# 加载数据
x = tf.convert_to_tensor(x, dtype=tf.float32) / 255.
y = tf.convert_to_tensor(y, dtype=tf.int32)
print(x.shape, y.shape)
# 将x、y转为tensorflow的db
train_dataset = tf.data.Dataset.from_tensor_slices((x, y))
# 设置batch
train_dataset = train_dataset.batch(200)

# [b, 784] => [b, 256] => [b, 128] => [b, 10]
# []
# 这是在计算过程中需要的参数,也是梯度下降时要变化的东西
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1))
b2 = tf.Variable(tf.zeros([128]))
w3 = tf.Variable(tf.random.truncated_normal([128, 10], stddev=0.1))
b3 = tf.Variable(tf.zeros([10]))

lr = 1e-3

for step, (x, y) in enumerate(train_dataset):
# x:[128, 28, 28]
# y:[128]
# [b, 28, 28] => [b, 28*28]
x = tf.reshape(x, [-1, 28*28])

with tf.GradientTape() as tape:
# x:[b, 28*28]
# h1 = x@w1 + b1
# [b, 784]@[784, 256] + [256]=>[b,256] + [256] => [b, 256]
h1 = x@w1 + b1
h1 = tf.nn.relu(h1)
# [b, 256] => [b, 128]
h2 = h1@w2 + b2
h2 = tf.nn.relu(h2)
# [b, 128] => [b, 10]
out = h2@w3 + b3

# compute loss
# out: [b, 10]
# y:[b] => [b, 10]
y = tf.one_hot(y, depth=10)

# mse = mean(sum(y-out)^2)
# [b,10]
loss = tf.square(y-out)
# mean: scalar
loss = tf.reduce_mean(loss)
# 梯度计算
grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
# 梯度更新
# w1 = w1 - lr * grads[0]
w1.assign_sub(lr * grads[0])
b1.assign_sub(lr * grads[1])
w2.assign_sub(lr * grads[2])
b2.assign_sub(lr * grads[3])
w3.assign_sub(lr * grads[4])
b3.assign_sub(lr * grads[5])

if step % 100 == 0:
print(step, 'loss', float(loss))

使用tensorflow的部分接口实现

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 tensorflow as tf
from tensorflow import keras
from tensorflow.keras import datasets, optimizers, Sequential, layers


def data_pre_process(x, y):
x = tf.cast(x, dtype=tf.float32) / 255.
y = tf.cast(y, dtype=tf.int32)

return x, y


def dataLoader():
(x, y), (x_val, y_val) = datasets.fashion_mnist.load_data()
db = tf.data.Dataset.from_tensor_slices((x, y))
db = db.map(data_pre_process).batch(128)

db_val = tf.data.Dataset.from_tensor_slices((x_val, y_val))
db_val = db_val.map(data_pre_process).batch(128)

return db, db_val


if __name__ == "__main__":
# 数据加载
db, db_val = dataLoader()

model = Sequential([
layers.Dense(256, activation=tf.nn.relu),
layers.Dense(128, activation=tf.nn.relu),
layers.Dense(64, activation=tf.nn.relu),
layers.Dense(32, activation=tf.nn.relu),
layers.Dense(10, activation=tf.nn.relu),
])
model.build(input_shape=[None, 28*28])
model.summary()

optimizers = optimizers.Adam(lr=1e-3)
# 迭代
for epoch in range(30):
# 每次迭代过程中会根据batchsize分块计算
for step, (x, y) in enumerate(db):
x = tf.reshape(x, [-1, 28*28])
# 梯度计算
with tf.GradientTape() as tape:
logits = model(x)
y_onehot = tf.one_hot(y, depth=10)
loss_mse = tf.reduce_mean(tf.losses.MSE(y_onehot, logits))
loss_ce = tf.reduce_mean(tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True))

grads = tape.gradient(loss_ce, model.trainable_variables)
# 梯度更新
optimizers.apply_gradients(zip(grads, model.trainable_variables))

if step % 100 == 0:
print(epoch, step, 'loss:', float(loss_ce), float(loss_mse))
# 测试/验证集精度计算
total_correct = 0
total_num = 0
for x, y in db_val:
x = tf.reshape(x, [-1, 28*28])
logits = model(x)
prob = tf.nn.softmax(logits, axis=1)

pred = tf.argmax(prob, axis=1)
pred = tf.cast(pred, dtype=tf.int32)

correct = tf.equal(pred, y)
correct = tf.reduce_sum(tf.cast(correct, dtype=tf.int32))
total_correct += int(correct)
total_num += x.shape[0]

acc = total_correct/total_num
print(epoch, 'test acc:', float(acc))

tensorflow高级api使用

所有的深度学习都包含上面几个步骤

  • 加载数据
  • 迭代
  • 梯度更新
  • 测试/验证精度计算

因此,tensorflow将上面的固定步骤写在同一的接口中方便使用

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
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import datasets, optimizers, Sequential, layers


def data_pre_process(x, y):
x = tf.cast(x, dtype=tf.float32) / 255.
y = tf.cast(y, dtype=tf.int32)

return x, y


def dataLoader():
(x, y), (x_val, y_val) = datasets.fashion_mnist.load_data()
x = x.reshape(60000, 28*28)
y = tf.one_hot(y, depth=10)
db = tf.data.Dataset.from_tensor_slices((x, y))
db = db.map(data_pre_process).batch(128)

db_val = tf.data.Dataset.from_tensor_slices((x_val, y_val))
db_val = db_val.map(data_pre_process).batch(128)

return db, db_val


if __name__ == "__main__":
db, db_val = dataLoader()

model = Sequential([
layers.Dense(256, activation=tf.nn.relu),
layers.Dense(128, activation=tf.nn.relu),
layers.Dense(64, activation=tf.nn.relu),
layers.Dense(32, activation=tf.nn.relu),
layers.Dense(10, activation=tf.nn.relu),
])
model.build(input_shape=[None, 28*28])
model.summary()
# compile中会提供若干参数,如梯度优化方式、误差计算方式、精度评定方式,当然还有更多参数,请阅读文档
model.compile(optimizer=optimizers.Adam(lr=0.0001), loss=tf.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
# fit即开始训练,需要输入db和要迭代的次数,注意db中的x和y要和model的输入与输出维度一致
model.fit(db, epochs=1000)

常见问题

过拟合和欠拟合

过拟合:训练精度很高,测试精度不够(可能)

欠拟合:训练精度不够,测试精度也不够(可能)——模型复杂度不够

过拟合解决

  • 更多数据
  • 更简单的模型
  • 数据增强

Regularization

image-20230702195941412

前面是误差,后面是参数的范式,参数的一范式越小说明参数越接近于0,那么拟合出来的模型就越平滑,出现过拟合的可能性就越小

image-20230702200316610

两张regularization方式,分别是一范数二范数

Momentum

image-20230702200957049

动量设置,参数更改的不仅仅由当前梯度的影像,还与上一次梯度的方向有关

Learning rate

image-20230702201327476

学习率一般刚开始比较大,后续慢慢变小,前期学习率大变化快,后续会较慢

提前取消

如果出现了训练精度还在提高,测试精度不提高了,说明已经过拟合,可以停止训练

Dropout

image-20230703092847561

每一次训练都有一些连线可能中断

tensorflow和pytorch的dropout参数是相反的

dropout在做test时不能使用——要手动取消

为什么要卷积

为什么要卷积?不使用简单的Dense层?

  • 数据存储需求大

视野?滑动窗口?这个窗口是卷积核?cv里面锐化模糊边缘提取的卷积核

卷积核的个数,也就是con2d中的两个参数:

  • 卷积核的大小问题

[c, 3, 3]一个卷积核可以将一张图片卷积到一个channels为1的新层次,c为图片的通道数

image-20230703095053915

如图,这个[3, 3, 3]依次对三个通道做乘法,然后将结果加起来得到一个一通道的数据,这个一通道的数据代表着原图像在某一层次上的特征

有时候我们需要更多的特征,这时候就需要[N, c, 3, 3]这样N个卷积来提取特征,就会得到N个通道的特征

如下面的(64, 3)其中的64就是上面的N代表着64个通道的特征,3是卷积核的大小,也就是[c, 3, 3]中的3,c会默认与图片的通道数相同,所以不需要额外设置

1
conv1 = Conv2D(64, 3, activation='relu', padding='same')(inputs)

image-20230702203217415

为什么要下采样(池化)

不同的程度可以获取到不同层级的特征

image-20230703095836625

GNSS数据处理

  • 探地雷达使用过程中,有一个基站RTK、一个绑定在探地雷达上的移动站RTK

    • 由于要用基站RTK去纠正移动站RTK,因此移动站RTK的数据采集时间应当在基站RTK采集时间包含中

    • 对RTK的数据.GNS文件的处理:使用HGO数据处理软件

    • HGO软件中:工具→RINEX转换工具

image-20230416141026463

image-20230416141327150

基站数据处理

  • 基站RTK数据-1
    • 注意转换基站数据时,前10s和后10s可以不要,防止启动和结束时数据的不稳定
    • 起始时间加10,截止时间减10

image-20230416141439657

  • 基站RTK数据-2

image-20230416141815403

  • 转换后结果
    • 转换后出现.o和.p文件

image-20230416142003716

移动站数据处理

  • 移动站RTK数据-1
    • 可以看出其时间在基站RTK数据-1的范围内

image-20230416142434473

  • 移动站RTK数据-2
    • 可以看出其时间在基站RTK数据-2的范围外
    • 则需要对

image-20230416142546036

RTK数据使用IE解算

  • 创建项目——点击左上角圈中的按钮,然后下一步

image-20230416144253784

  • 导入移动站GNSS数据——刚刚转换后的.o文件,然后一直下一步

image-20230416144409124

  • 导入基站GNSS数据——刚刚转换后的.o文件,然后一直下一步

image-20230416145751122

  • 站点信息设置
    • 坐标选项设置为平均值、投影设置为WGS84——然后下一步

image-20230416145850175

image-20230416145933429

GNSS解算

  • process→Process GNSS

image-20230416150105641

  • 参数设置为如图所示——点击process

image-20230416150201370

image-20230416150507288

解算结果导出

  • Output→Export Wizard

    • 注意选择导出文件格式,这里可以选择cor,如果没有用new添加,格式如图所示,可以点击预览如图所示——然后一直下一步

    • cor的配置

      • Header/Footer Options参数设置

        image-20230504140927212

        image-20230504141124302

      • Field Separator参数设置

        image-20230504141349524

        image-20230504141433882

      • Export Variables内各项参数设置

        image-20230504142058164

        具体参数如下图所示:

        image-20230504142134534

        image-20230504142209097

        image-20230504142230608

        image-20230504142257682

image-20230416161042576

image-20230416161121686

image-20230416161150435

image-20230416161525886

探地雷达数据绑定RTK

  • 探地雷达数据每个项目的文件如下所示——有若干个数据,每个数据都包含一个time文件

image-20230416170214005

  • 利用探地雷达的time与GNSS输出文件中的time比较,就可以得到每个探地雷达数据的经纬度与海拔信息——所以要求GNSS的time比探测雷达的time长
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
timeFilePath = 'GPR/2023041102line_time'
corFilePath = 'GPR/2023041102line_cor'
for file_name in os.listdir(timeFilePath):
file_ie = '0411_2.txt' #GNSS解算的轨迹坐标
file_gpr = os.path.join(timeFilePath, file_name) #time文件
day = '2023-04-11'
write_cor = corFilePath+"/" + file_name[0:-4] + 'cor'
ie_txt = pd.read_csv(file_ie, header=None)
gpr_txt = pd.read_csv(file_gpr, header=None)
gpr_iet = []
gpr_n = []
for i in range(len(gpr_txt[0])):
gpr_t = gpr_txt[0][i].split('\t')
gpr_n.append(gpr_t[0])
gpr_iet.append(utc_to_iet(gpr_t[2]))
gpr_n = np.array(gpr_n)
gpr_iet = np.array(gpr_iet)
condition = (ie_txt[0] >= gpr_iet[0]) & (ie_txt[0] <= gpr_iet[len(gpr_iet)-1])
ie_select = np.extract(condition, ie_txt[0])
cor = []
for i in range(len(ie_select)):
num = find(gpr_iet, ie_select[i])
if num == -1:
continue
j = np.searchsorted(ie_txt[0], ie_select[i])
t_cor = iet_to_utc(ie_select[i])
lon = ie_txt[1][j]
lat = ie_txt[2][j]
hei = ie_txt[3][j]
q = ie_txt[4][j]
# print(type(gpr_n[num]))
cor.append(gpr_n[num] + '\t' + day + '\t' + str(t_cor) + '\t' + str(lon) + '\t' + 'N' + '\t' + str(lat) + '\t' + 'E' + '\t' + str(hei) + '\t' + 'M' + '\t' + str(q))
cor = np.array(cor)
np.savetxt(write_cor, cor, fmt = '%s', delimiter='\t')

image-20230416170431791

  • 将生成的cor文件复制回数据文件夹,替代原有的cor,但还会有一些cor为空,这一部分数据在后面是无法使用的

image-20230523155612770

探地雷达数据的处理

  • 打开CrossPoint

    • 需要插U盘,要不然很多功能无法使用
  • 打开同一个项目中的各项文件

    • 如这里的6和7是同一个地方的项目
    • 点进去这个文件夹就可以出现能打开的文件,注意与刚刚的cor对比,如果该文件的cor大小为0kb,这个文件就不能导入
    • 打开后如图所示

image-20230523160222273

image-20230523160257746

image-20230523160422696

数据处理

  • 过滤器设置
    • 目前我也不会设置,先简单打开这两个选项,那么左边的波形就会更加变化,我们将依据这个波形去判断是否有异常

image-20230523160649796

  • 0m深度位置设置,这个是为了使得到的高度正常
    • 按道理来说,第一天反射横线应该是地面,也就是此次的深度为0,但是图示并不是这样
    • 左键点住红色三角并向下拖动,就可以改变0深度位置,注意上下两个都要设置,上面的图为更细节的,也是我们在处理数据过程中用得更多的

image-20230523160828461

image-20230523161008203

  • 波形图对比度调整(应该是叫做对比度)
    • 分为左右两个,左边的调上面的图,右边的调下面的图,一般也就使用左边的调节上面的图
    • 可以多调整实验一下,调整到可以清晰判断就行

image-20230523161225710

  • 显示文件数据设置
    • 在判断过程中,我们经常需要观察两条相邻带之间有相似的异常
    • 因此,可以在中间的波形图区域设置显示多条带,对比着看异常
    • 在Tools→Settings→Data view中设置,设置一般为偶数,因为对每一条带都要一个细节的和一个不那么细节的数据需要展示
    • 设置为6时如下图所示
    • 因为我们不常使用下面那一条不那么细节的数据,因此可以将其缩小,便于我们观察需要的数据(鼠标放在蓝色条带上往下拉,拉倒两条数据的交界处),结果如图所示

image-20230523161833582

image-20230523161858046

image-20230523162031623

  • 异常点标注
    • 如图中的明显异常点,可以标注出来
    • 可以看出,三条带出现了类似的异常点,那么我们用同一种Marker标记
    • 按照这种逻辑将所有的异常标出来,再导出和正射影像叠加分析,如图所示

image-20230523162454371

image-20230523162638582

对齐图片(投影后)

添加图片

image-20230416114600655

全景图片一张可以投影为若干张普通图片,在PhotoScan中可以将一张全景图片所投影出的图片设置为一个组,对其加以约束,会使得图片对齐效果更好

图片分组

  • 首先选中一组图片,将其添加到一个图片组中

image-20230416115019165

image-20230416115131493

  • 保存后项目文件夹下会有一下红线圈出的内容

image-20230416115256851

进入对应的chunk文件夹下

image-20230416115349659

  • 解压chunk.zip得到doc.xml文件

doc.xml内容如图所示,其中包含了我们预设的group,我们只需要将其他camera也组织成group格式就可以分组成功

image-20230416120430188

  • 现将剩余的camera组织为group格式,并输出到txt中,将txt中的结果复制到doc.xml中替换原来的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def photoScanNewChunk():
trunk_xml_path = "D:/anbo_my/test.files/0/doc.xml"
new_trunk_xml_path = "D:/anbo_my/test.files/0/group.txt"
tree = ET.parse(trunk_xml_path)
root = tree.getroot()
cameras_s = root.findall("cameras")
for cameras in cameras_s:
camera_s = cameras.findall("camera")
with open(new_trunk_xml_path, "w") as f:
for i in range(int(len(camera_s)/6)):
f.write(' <group id="' + str(i+1) + '" label="Group ' + str(i + 2) + '" type="station">' + '\n')
# 这里的6是每一组的图像数目
for j in range(6):
camera = camera_s[i*6+j]
f.write(' <camera id="' + str(camera.attrib.get("id"))+'" sensor_id="0" label="'+camera.attrib.get("label")+'"/>'+ '\n')
f.write(' </group>'+ '\n')

注意更改id、label和type的值

image-20230416120313547

  • 重新打开项目文件

image-20230416120711094

对齐图片

Workflow→Align Photos

根据需求更改精度之类的东西

image-20230416121056281

如果对齐成功,就会像下面一样只有一个component,而如果对齐失败就会出现多个component这时就需要其他方法去改进优化

image-20230416121220687

对齐图片(全景图片直接处理)

  • 除了按上述将投影后的图片进行对齐及后续处理,还可以直接使用全景图片进行操作

    • 全景图片数量更少(一张全景图片会投影为若干张普通图片),因此处理速率更快

    • 出现多个component的概率更小

    • 但可能也会出现其他问题

  • 由于是全景图片,因此不存在分group的情况,当然也可以将比较靠近的若干图片分为一组

  • 注意导入图片后要有一个步骤告诉软件我们正在使用全景图片
    • Tools→Camera Calibration

image-20230416121932890

  • 其余步骤与之前相似,得到align后的结果

image-20230407160308173

一、物理层

物理层所传数据单位为比特。要求发送法发送1/0时,接受方应当准确接受到1/0。因此物理层要考虑用多大的电压代表1或0,以及接受方如何识别出发送法发送的比特。

​ 物理层的作用正是要尽可能的屏蔽掉多种多样的传输媒体和通信手段的差异,使物理层上面的数据链路层感觉不到这些差异。

​ 信道:向某一个方向传送信息的媒体。单工通信、半双工通信、全双工通信

二、数据链路层

​ 两台主机之间的数据传输,总是在一段一段的链路上完成的,这就需要专门的链路层协议。在两个相邻节点之间传输数据时,数据链路层将网络层交下来的IP数据报组装成帧,两个相邻节点在链路中传送帧

​ 每一帧会包括必要的控制信息:同步信息、地址信息、差错控制。

React创建与配置

React创建项目

React脚手架搭建项目

1
create-react-app name

image-20230405102510876

删除默认生成的文件:

image-20230405102711804

项目目录结构配置

image-20230405103045842

项目基本配置

配置项目的icon

直接把需要使用的icon替换public下面的favicon.ico

配置项目的标题

设置html文件下的title属性

配置jsconfig.json文件

配置后编写项目的智能提示会好很多

image-20230405103730430

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*":[
"src/*"
]
},
"jsx":"preserve",
"lib": [
"ESNext",
"DOM",
"DOM.Iterable",
"ScriptHost"
]
}
}

项目的别名配置

image-20230405103925434

采用craco配置

webpack的配置:

  • craco会使项目的配置与webpack的配置融合
1
npm install @craco/craco@alpha -D
  • craco.config.js的配置

image-20230405104646852

1
2
3
4
5
6
7
8
9
10
11
const path = require('path')
const resolve = pathName=>path.resolve(__dirname, pathName)
module.exports = {
webpack:{
alias:{
"@":resolve("src"),
"components":resolve("src/components"),
"utils":resolve("src/utils")
}
}
}

要是craco生效,项目启动必须以craco启动,才能和webpack的配置融合——修改package.json中的scripts

1
2
3
4
5
6
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},

此时就可以用别名去写路径,避免长串的../../

image-20230405104959491

项目的less配置:

1
npm i craco-less@2.1.0-alpha.0 --save -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const path = require('path')
const CraLessPlugin = require('craco-less')
const resolve = pathName=>path.resolve(__dirname, pathName)
module.exports = {
//less
plugins:[
{
plugin:CraLessPlugin
}
],
// webpack
webpack:{
alias:{
"@":resolve("src"),
"components":resolve("src/components"),
"utils":resolve("src/utils")
}
}
}

CSS样式的重置

对默认样式进行重置:

  • normalize.css
  • reset.css

normalize.css:

1
npm install normalize.css --save

在index.js中导入

reset.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 把需要重置的元素名称放在这里 */
body,p,button,input,dd,dl,dt,form{
padding: 0;
margin: 0;
}
a{
color: #484848;
text-decoration: none;
}
img {
vertical-align: top;
}
ul, li {
list-style: none;
}

React全家桶配置

路由配置

1
npm install react-router-dom --save

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import App from '@/App';



import "normalize.css"
import "@/assets/css/reset.css"
import { HashRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
{/* 懒加载是异步的,因此需要在保证异步未完成时提供一个可替换方案,可以是字符串也可以是组件 */}
<Suspense fallback="locding">
<HashRouter>
<App />
</HashRouter>
</Suspense>
</React.StrictMode>
);

router/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

import React from "react"
import { Navigate } from "react-router-dom"
// 懒加载的实现
const Detail = React.lazy(()=>import("@/viewer/detail"))
const Entire = React.lazy(()=>import("@/viewer/entire"))
const Home = React.lazy(()=>import("@/viewer/home"))

const routes = [
{
path:"/",
element:<Navigate to="/home" />
},
{
path:"/home",
element:<Home/>
},
{
path:"/entire",
element:<Entire/>
},
{
path:"/detail",
element:<Detail/>
}
]
export default routes

react中的路由设置

Redux状态管理

  • 普通方式:目前使用效率依然非常高
  • @reduxjs/toolkit方式:推荐方式,未来的趋势
1
npm install @reduxjs/toolkit react-redux --save

普通方式:

image-20230405114121514

  • reducer.js代码:
1
2
3
4
5
6
7
8
9
10
const initialState = {

}
function reducer(state=initialState, action){
switch(action.type){
default:
return state
}
}
export default reducer
  • index.js代码:
1
2
3
import reducer from "./reducer";

export default reducer

@reduxjs/toolkit方式:

image-20230405114538084

  • home.js代码
1
2
3
4
5
6
7
8
9
10
11
12
import { createSlice } from "@reduxjs/toolkit";

const honeSlice = createSlice({
name:"home",
initialState:{

},
reducers:{

}
})
export default honeSlice.reducer
  • index.js代码
1
2
3
4
5
6
7
8
9
10
import { configureStore } from "@reduxjs/toolkit";
import homeReducer from "./modules/home"
import entireReducer from "./modules/entire"
const store = configureStore({
reducer:{
home:homeReducer,
entire:entireReducer
}
})
export default store

React与Redux

网络请求配置

发起网络请求库:

1
npm install axios --save

image-20230405120733930

index.js代码:

1
2
3
import owwRequest from "./request"

export default owwRequest

request/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
import axios from "axios";
import {BASE_URL, TIMEOPUT} from "./config"
class OwwRequest {
constructor(baseURL, timeout){
// 获取axios实例
this.instance = axios.create({
baseURL,
timeout
})
// 发起网络请求后的拦截
this.instance.interceptors.response.use(res=>{
return res.data
},err=>{
return err
})
}
request(config){
return this.instance.request(config)
}
get(config){
return this.request({...config, method:"get"})
}
post(config){
return this.request({...config, method:"post"})
}
}

export default new OwwRequest(BASE_URL, TIMEOPUT)

request/config.js:

1
2
export const BASE_URL = ""
export const TIMEOPUT = 10000

内存管理

内存的管理都会有如下的生命周期:

  • 第一步:分配申请你需要的内存(申请)
  • 第二步:使用分配的内存(存放一些东西,比如对象等)
  • 第三步:不需要使用时,对其进行释放

不同的编程语言对于第一步和第三步会有不同的实现:

  • 手动管理内存:比如C、C++,包括早期的OC,都是需要手动来管理内存的申请和释放的(malloc和free函数)
  • 自动管理内存:比如Java、JavaScript、Python、Swift、Dart等,它们有自动帮助我们管理内存

对于开发者来说,JavaScript 的内存管理是自动的、无形的:

  • 我们创建的原始值、对象、函数……这一切都会占用内存
  • 但是我们并不需要手动来对它们进行管理,JavaScript引擎会帮助我们处理好它

JavaScript内存管理

  • JS对于原始数据类型内存的分配会在执行时, 直接在栈空间进行分配
  • JS对于复杂数据类型内存的分配会在堆内存中 开辟一块空间,并且将这块空间的指针返回值 变量引用

image-20230404202250630

JavaScript的垃圾回收

因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间

在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数:

  • 但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率
  • 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露

垃圾回收的英文是Garbage Collection,简称GC

常见GC算法——引用计数

引用计数:

  • 当一个对象有一个引用指向它时,那么这个对象的引用就+1

  • 当一个对象的引用为0时,这个对象就可以被销毁掉

  • 循环引用不会被销毁

    image-20230404202553182

常见GC算法——标记清除

标记清除:

  • 标记清除的核心思路是可达性(Reachability)
  • 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些 没有引用到的对象,就认为是不可用的对象
  • 这个算法可以很好的解决循环引用的问题

常见GC算法——算法优化

标记整理法

  • 不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化

分代收集法

  • 对象被分成两组:“新的”和“旧的”
  • 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理
  • 那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少

增量收集

  • 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
  • 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的 延迟

闲时收集

  • 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响

image-20230404203527050

JavaScript闭包

JavaScript函数式编程

  • 在JavaScript中,函数是非常重要的,并且是一等公民
    • 那么就意味着函数的使用是非常灵活的
    • 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用
  • avaScript存在很多的高阶函数
    • 自己编写高阶函数
    • 使用内置的高阶函数

闭包

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
  • 闭包让你可以在一个内层函数中访问到其外层函数的作用域
  • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
  • 个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数和周围环境就是一个闭包
  • 从广义的角度来说:JavaScript中的函数都是闭包
  • 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

闭包访问过程:

image-20230404204008476

image-20230404204014899

闭包执行过程:

  • makeAdder函数执行完毕,正常情况下我们的AO对象会被释放

  • 但是因为在0xb00的函数中有作用域引用指向了这个AO对象,所以它不会被释放掉

    image-20230404204409944

闭包的内存泄露问题:

  • 因为在全局作用域下add10变量对0xb00的函数对象有引用,而0xb00的作用域中AO(0x200)有引用,所以最终 会造成这些内存都是无法被释放的
  • 所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的
  • 因为当将add10设置为null时,就不再对函数对象0xb00有引用,那么对应的AO对象0x200也就不可达了
  • 在GC的下一次检测中,它们就会被销毁掉

AO不使用的属性优化:

AO对象不会被销毁时,是否里面的所有属性都不会被释放?

  • 下面这段代码中name属于闭包的父作用域里面的变量

  • 我们知道形成闭包之后count一定不会被销毁掉,那么name是否会被销毁掉呢

    image-20230404204655316

    image-20230404204704016

为什么需要Hook

不编写class的情况下使用state以及其他的React特性

class组件

class组件的优势

  • class组件可以定义自己的state,用来保存组件自己内部的状态
    • 函数式组件不可以,因为函数每次调用都会产生新的临时变量
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑
    • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次
    • 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求
  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate
    • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次

class组件存在的问题

  • 复杂组件变得难以理解

    • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂
    • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在
      componentWillUnmount中移除)
    • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度
  • 难以理解的class

    • 在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this
  • 组件复用状态很难
    • 状态的复用我们需要通过高阶组件
    • 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用
    • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套

Hook的作用

Hook可以解决上述class和函数组件存在的问题

它可以让我们在不编写class的情况下使用state以及其他的React特性

Hook的使用场景

  • Hook的出现基本可以代替我们之前所有使用class组件的地方
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用

Hooks的使用

Hooks只能在函数组件中使用

为什么Hooks中的函数都叫use,不叫create呢?

  • Hooks的一大作用是保存组件的状态,在下一次渲染时,返回当前的state
  • 而create的意思是,每次渲染都重新创建,那么就与state只在组件初次渲染时被创建相反

useState的使用

useState主要在函数组件中定义state

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

function App() {
const [count, setCounter] = useState(0)
console.log("count", count)
console.log("setCounter", setCounter)
return (
<div>
<h2>count:{count}</h2>
{/**
* 注意:onClick传入的是一个函数,setCounter(count+1)是一个函数的调用,所以这里只能使用箭头函数,在函数调用里调用setCounter
*/}
{/* <button onClick={setCounter(count+1)}>+1</button> */}
<button onClick={()=>{setCounter(count+1)}}>+1</button>
</div>
)
}
export default memo(App)
  • useState传入的参数是state中变量的初始值,不传则为默认值undefined
  • useState返回的是一个数组,其中数组中的第一个值是state变量的值,第二个值是设置状态值的函数

  • 只能在最外层调用Hook,不能在循环、条件判断或者子函数中调用

  • 只能在React的函数组件中调用Hook,不能在其他JavaScript函数中调用

useState如何实现状态

useState原理

Effect Hook

  • Effect Hook可以在函数组件中实现类似于生命周期的功能
  • 网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(不是页面渲染)
  • 对于完成这些功能的Hook被称之为 Effect Hook

Effect Hook的基本使用

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

function App() {
const [count, setCounter] = useState(0)
useEffect(()=>{
console.log("监听redux中的数据变换,监听eventBus中的事件")
})
return (
<div>
<h2>count:{count}</h2>
<button onClick={()=>{setCounter(count+1)}}>+1</button>
</div>
)
}
export default memo(App)
  • useEffect传入的参数为一个回调函数,这个回调函数在更新DOM操作之后,就会执行
  • 但是在默认情况下,无论第一次渲染,还是在上面更改count更新后的渲染,在渲染结束后都会执行这个回调函数

  • 由于每次渲染都会执行useEffect中的函数,那么每次都会添加监听,最后会添加若干监听,因此需要在函数组件卸载或渲染前卸载掉之前的监听

Effect Hook的清除

  • 在class组件中,某些副作用的代码,可以在componentWillUnmount中进行清除,比如在这个函数中清除Redux的订阅
  • useEffect通过传入的回调函数1的返回值函数2,在返回值函数2中做清除操作

    • 这样做可以将添加和移除订阅的逻辑放在一起
    • 同属于一个effect的一部分,更好管理
  • React何时清除useEffect

    • 在组件更新和卸载时执行清除操作(装载时不会清除)

多个Effect的使用

  • 使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题

    • 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中
  • 使用Effect Hook,我们可以将它们分离到不同的useEffect中

    • Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样
    • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { memo, useEffect, useState } from 'react'

function App() {
const [count, setCounter] = useState(0)
useEffect(()=>{
console.log("监听redux中的数据变换,监听eventBus中的事件")
return ()=>{
console.log("清除监听redux中的数据变换,清除监听eventBus中的事件")
}
})
useEffect(()=>{
console.log("获取网络请求")
})
return (
<div>
<h2>count:{count}</h2>
<button onClick={()=>{setCounter(count+1)}}>+1</button>
</div>
)
}
export default memo(App)

Effect的性能优化

  • 默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题

    • 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅)
    • 另外,多次执行也会导致一定的性能问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import React, { memo, useEffect, useState } from 'react'

    function App() {
    const [count, setCounter] = useState(0)
    const [message, setMessage] = useState("Hello!")
    useEffect(()=>{
    console.log("监听redux中的数据变换,监听eventBus中的事件")
    return ()=>{
    console.log("清除监听redux中的数据变换,清除监听eventBus中的事件")
    }
    })
    return (
    <div>
    <h2>count:{count}</h2>
    <h2>message:{message}</h2>
    <button onClick={()=>{console.log("加count");setCounter(count+1)}}>+1</button>
    <button onClick={()=>{console.log("修改message");setMessage("你哈个锤子")}}>修改message</button>
    </div>
    )
    }
    export default memo(App)

    image-20230403201741719

  • useEffect的实际上有两个参数

    • 参数一:执行的回调函数
    • 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)——如果不希望受到任何依赖的影响,则传入[],这时候仅会在创建和销毁时分别执行对应回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { memo, useEffect, useState } from 'react'

function App() {
const [count, setCounter] = useState(0)
const [message, setMessage] = useState("Hello!")
useEffect(()=>{
console.log("监听redux中的数据变换,监听eventBus中的事件")
return ()=>{
console.log("清除监听redux中的数据变换,清除监听eventBus中的事件")
}
},[count])
return (
<div>
<h2>count:{count}</h2>
<h2>message:{message}</h2>
<button onClick={()=>{console.log("加count");setCounter(count+1)}}>+1</button>
<button onClick={()=>{console.log("修改message");setMessage("你哈个锤子")}}>修改message</button>
</div>
)
}
export default memo(App)

image-20230403201907764

那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了

useContext的使用

获取Context的方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context

但是多个Context共享时会存在大量的嵌套——Context Hook允许我们通过Hook来直接获取某个Context的值

但是Provider依然会有嵌套

Provider代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { HashRouter } from 'react-router-dom';
import { ColorContext, BgrContext } from './Context';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(

<ColorContext.Provider value={{color:"red"}}>
<BgrContext.Provider value={{fontSize:36}}>
<HashRouter>
<App/>
</HashRouter>
</BgrContext.Provider>
</ColorContext.Provider>
);

useContext代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { memo, useContext, useEffect, useState } from 'react'
import { ColorContext, BgrContext } from './Context'


function App() {
const colorContext = useContext(ColorContext)
const bgrContext = useContext(BgrContext)
return (
<div>
<h2 style={{color:colorContext.color,fontSize:bgrContext.fontSize}}>哈哈哈</h2>
</div>
)
}
export default memo(App)

image-20230403204016535

useReducer的使用

useReducer仅仅是useState的一种替代方案——与Redux关系不大

  • 如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分
  • 或者这次修改的state需要依赖之前的state时,也可以使用
  • 数据是不会共享的,它们只是使用了相同的counterReducer的函数而已
  • useReducer只是useState的一种替代品,并不能替代Redux
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, { memo, useReducer } from 'react'

const counterReducer = function(state, action){
switch(action.type){
case "increment":
return {...state, counter:state.counter+1}
case "decrement":
return {...state, counter:state.counter-1}
case "addNum":
return {...state, counter:state.counter+action.num}
case "decNum":
return {...state, counter:state.counter-action.num}
}
}
function App() {
const [state, dispatch] = useReducer(counterReducer,{message:"Hello!",counter:100})
return (
<div>
<h2>当前计数:{state.counter}</h2>
<button onClick={()=>{dispatch({type:"increment"})}}>+1</button>
<button onClick={()=>{dispatch({type:"decrement"})}}>-1</button>
<button onClick={()=>{dispatch({type:"addNum", num:5})}}>+5</button>
<button onClick={()=>{dispatch({type:"decNum", num:10})}}>-10</button>
</div>
)
}
export default memo(App)

useReducer的参数与返回值:

  • 第一个参数:reducer函数
    • reducer函数的参数一:上一次的state/useReducer的第二个参数
    • reducer函数的参数二:dispatch所传入的参数
    • reducer的返回值:新的状态(…state是为了保障除了这次改变的其余状态依然在新的状态中)
  • 第二个参数:state初始值
  • 第一个返回值:更新后的state值
  • 第二个返回值:用于发起改变state的函数

useCallback的使用

useCallback实际的目的是为了进行性能的优化

  • 函数式组件在每次刷新时都会重新执行函数,那么里面的函数就会重新定义,虽然刷新后,原来定义的函数会被回收,但每次都定义也会影响性能,同时会生成新的foo
  • useCallback就可以实现当满足某些条件时重新定义函数,但得到相同的foo,而如果有子组件依赖这个foo,相同的foo不会引起子组件的刷新

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { memo, useCallback, useState } from 'react'
import ChildComponent from './component/ChildComponent'

function App() {
const [count, setCount] = useState(100)
const [message, setMessage] = useState(22)
const changeCount = ()=>{
console.log("修改count")
setCount(count+1)
}
const changeMessage = ()=>{
console.log("修改message")
setMessage(Math.round())
}
return (
<div>
<h2>当前计数:{count}</h2>
<h2>Message:{message}</h2>
<ChildComponent changeMessage={changeMessage}/>
<button onClick={changeCount}>+1</button>
</div>
)
}
export default memo(App)

子组件代码:

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

function ChildComponent(props) {
const {changeMessage} = props
console.log("子组件刷新")
return (
<div>
<button onClick={changeMessage}>子组件按钮修改Message</button>
</div>
)
}
export default memo(ChildComponent)

image-20230403211929250

可以看出每次修改count都会刷新不依赖count子组件,会造成性能浪费

useCallback的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { memo, useCallback, useState } from 'react'
import ChildComponent from './component/ChildComponent'

function App() {
const [count, setCount] = useState(100)
const [message, setMessage] = useState(22)
const changeCount = useCallback(()=>{
console.log("修改count")
setCount(count+1)
},[count])
const changeMessage = useCallback(()=>{
console.log("修改message")
setMessage(Math.round())
},[])
return (
<div>
<h2>当前计数:{count}</h2>
<h2>Message:{message}</h2>
<ChildComponent changeMessage={changeMessage}/>
<button onClick={changeCount}>+1</button>
</div>
)
}
export default memo(App)

image-20230403212738937

可以看出点击更改count后,子组件并未刷新

useCallback的参数与返回值:

  • 参数一:需要处理的函数
  • 参数二:该函数改变依赖的状态,如果该依赖的状态不变,就返回相同的函数,相同的函数不会引起依赖该函数子组件的刷新
  • 返回值:返回一个函数的 memoized(记忆的) 值

使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存

  • 并不是不会新定义函数,新定义函数是一定的
  • 只针对子组件的渲染次数做优化,在本组件层面是没有优化的

useMemo的使用

useMemo也是为了进行性能的优化:

  • 同样是返回一个memoized(记忆的)值
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • useCallback是在依赖不变的情况下,多次定义的时候,返回的函数是相同的
  • useMemo针对变量进行优化,useCallback针对函数进行优化

useMemo的使用案例:

  • 进行大量的计算操作,是否有必须要每次渲染时都重新计算
  • 对子组件传递相同内容的对象时,使用useMemo进行性能的优化

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { memo, useCallback, useState } from 'react'
import ChildComponent from './component/ChildComponent'

function App() {
const [count, setCount] = useState(100)
function changeCount(){
console.log("修改count")
setCount(count+1)
}
function foo(information){
console.log("生成information")
return {...information,message:"helloWorld"}
}
const information = foo({message:message})
return (
<div>
<h2>当前计数:{count}</h2>
<h2>Message:{information.message}</h2>
<ChildComponent information={information}/>
<button onClick={changeCount}>+1</button>
</div>
)
}
export default memo(App)

子组件代码:

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

function ChildComponent(props) {
const {information} = props
console.log("子组件刷新")
return (
<div>
<h3>子组件:{information.message}</h3>
</div>
)
}
export default memo(ChildComponent)

image-20230404094727924

可以看出:

  • 每次更新count后,message是不变的,那么information,也就不必要每次都去运行foo函数,当foo函数逻辑赋值时会引起性能浪费
  • 子页面仅依赖于message,这里明显每次都是得到的information值是一样的,但由于是不同的对象,子组件每次也会刷新

useMemo的基本使用

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, { memo, useMemo, useState } from 'react'
import ChildComponent from './component/ChildComponent'

function useFoo(message){
const res = useMemo((message)=>{
console.log("生成information")
return {message:message+"修改后"}
},[message])
return res
}
function App() {
const [count, setCount] = useState(100)
const [message, setMessage] = useState("helloWorld")
function changeCount(){
console.log("修改count")
setCount(count+1)
}
let information = useFoo(message)
return (
<div>
<h2>当前计数:{count}</h2>
<h2>Message:{message}</h2>
<ChildComponent information={information}/>
<button onClick={changeCount}>+1</button>
</div>
)
}
export default memo(App)

image-20230404101149326

可以看出:

修改count后,依赖于message的函数不再重新执行,子组件也不再刷新

注意:

  • useMemo这种Hook函数,只能在函数组件和hook函数中使用,自定义hook函数,以use开头,use后的第一个字母为大写
  • useMemo不会传递参数,因为useMemo是由React调用的,我们不知道其在上面时候调用,也就无法传参数进行
  • useMemo的参数与返回值
    • 参数一:useMemo判断是否执行的函数
    • 参数二:useMemo依赖的值,如果这个值没变,就不会执行传入的函数,直接返回上一次的计算值
    • 返回值:返回一个带记忆的值

useRef的使用

useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变

useRef基本使用

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { memo, useRef} from 'react'
function App() {
const inputRef = useRef()
const buttonClick = ()=>{
console.log(inputRef.current)
inputRef.current.focus()
}
return (
<div>
<input type="text" ref={inputRef}/>
<button onClick={buttonClick}>修改</button>
</div>
)
}
export default memo(App)
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变(解决闭包陷阱)
    • 由于函数里的count是获取上层作用域里的,也就是闭包,那么无论页面刷新多少次,函数里的count都是函数定义是的count 0,因此无论多少次点击得到的结果都是1(当然这里可以设置依赖count——仅颜色useRef的作用,所以没有添加)
    • 而使用inputRef所得到的对象每次本身就是一样的,那么有没有闭包陷阱也就无所谓,因为我们本身就是要获取相同的值,而通过修改inptRef这个对象里面的值,从而获得相同的对象不同的值,解决闭包陷阱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { memo, useCallback, useRef, useState} from 'react'
function App() {
const [count,setCount] = useState(0)
const buttonClick = useCallback(function(){
console.log("+1",count+1)
setCount(count+1)
},[])
const inputRef = useRef()
inputRef.count = count
const buttonClick1 = useCallback(function(){
console.log("-1",inputRef.count-1)
setCount(inputRef.count-1)
},[])
return (
<div>
<h2>count:{count}</h2>
<button onClick={buttonClick}>+1</button>
<button onClick={buttonClick1}>-1</button>
</div>
)
}
export default memo(App)

image-20230404105354101

父组件传ref给子组件

  • 方式一:作为参数放到props中传递过去——参数名不能使用ref
  • 方式二:利用forwardRef传递给子组件

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { memo, useRef, useState} from 'react'
import ChildComponent from './component/ChildComponent'
function App() {
const inputRef = useRef()
const buttonClick = ()=>{
console.log(inputRef.current)
inputRef.current.focus()
inputRef.current.value = ""
}
return (
<div>
<ChildComponent ref = {inputRef}/>
<button onClick={buttonClick}>showref</button>
</div>
)
}
export default memo(App)

子组件代码:

1
2
3
4
5
6
7
8
9
10
import React, { forwardRef, memo } from 'react'

function ChildComponent(props, ref) {
return (
<div>
<input type="text" ref={ref}/>
</div>
)
}
export default memo(forwardRef(ChildComponent))

注:要使用ref参数,子组件需用forwardRef包裹,且要使用memo的话,memo在forwardRef外层

useImperativeHandle的使用

  • 上述通过父组件中的ref传递给子组件中的元素,可以在父组件中获得子组件元素,对子组件元素进行操作
  • 将整个元素给到父组件,就容易在父组件中过渡使用,从而在某种情况下产生某种bug,如上述代码,点击showref会在聚焦子组件input的同时,将其值修改为空
  • useImperativeHandle就是为了解决样的问题,不直接把子组件元素给到父组件,而是把子组件元素的某些操作给到父组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { forwardRef, memo, useImperativeHandle, useRef } from 'react'

function ChildComponent(props, ref) {
const inputRef = useRef()
useImperativeHandle(ref, ()=>{
return {
focus:()=>{
inputRef.current.focus()
}
}
})
return (
<div>
<input type="text" ref={inputRef}/>
</div>
)
}
export default memo(forwardRef(ChildComponent))

useImperativeHandle的基本使用

  • 参数一:父组件传给子组件的ref
  • 参数二:回调函数,函数返回结果是传递给父组件的属性值对象,如这里只传了focus函数,那么父组件中就只有focus属性生效,其余属性无效

useLayoutEffect使用

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新

image-20230404143022932

useEffect与useLayoutEffect执行顺序演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { memo, useEffect, useLayoutEffect} from 'react'
function App() {
useEffect(()=>{
console.log("useEffect")
})
useLayoutEffect(()=>{
console.log("useLayoutEffect")
})
console.log("render app")
return (
<div>
<h2>哈哈哈</h2>
</div>
)
}
export default memo(App)

image-20230404150134192

注:先”render app”,等数据更新后,渲染到DOM前执行useLayoutEffect,渲染结束后执行useEffect

useLayoutEffect的作用

在页面即将刷新错误数据前及时拦截

使用useEffect代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { memo, useEffect, useState} from 'react'
function App() {
const [count, setCount] = useState(10)
useEffect(()=>{
if(count===0){
setCount(10)
}
console.log("useEffect",count)
}, [count])
console.log("render app")
function buttonClick(){
setCount(count-1)
}
return (
<div>
<h2>count:{count}</h2>
<button onClick={buttonClick}>-1</button>
</div>
)
}
export default memo(App)

动画

使用useLayoutEffect代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { memo, useLayoutEffect, useState} from 'react'
function App() {
const [count, setCount] = useState(10)
useLayoutEffect(()=>{
if(count===0){
setCount(10)
}
console.log("useEffect",count)
}, [count])
console.log("render app")
function buttonClick(){
setCount(count-1)
}
return (
<div>
<h2>count:{count}</h2>
<button onClick={buttonClick}>-1</button>
</div>
)
}
export default memo(App)

动画

自定义Hook

自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性

注:

  • 自定义Hook以use开头,且use后的字母要大写

自定义Hook——所有的组件在创建和销毁时都进行打印

  • 组件被创建:打印 组件被创建了
  • 组件被销毁:打印 组件被销毁了

image-20230404161751779

image-20230404161758920

自定义Hook——Context的共享

image-20230404161934780

自定义Hook——获取滚动位置

image-20230404162001534

redux hooks的使用

  • 在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect
    • 但是这种方式必须使用高阶函数结合返回的高阶组件
    • 且必须编写:mapStateToProps和 mapDispatchToProps映射的函数
  • useSelector的作用是将state映射到组件中
    • 参数一:将state映射到需要的数据中
    • 参数二:可以进行比较来决定是否组件重新渲染;(后续讲解)
  • useSelector默认会比较我们返回的两个对象是否相等
    • 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染
  • useDispatch非常简单,就是直接获取dispatch函数,之后在组件中直接使用即可
  • 我们还可以通过useStore来获取当前的store对象

redux的常规使用——使用connect函数

整个项目index.js代码:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import store from './stero';
const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
<Provider store={store}>
<App/>
</Provider>
);

countSlice.js代码

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

const countSlice = createSlice({
name:"count",
initialState:{
count:5
},
reducers:{
addCountAction(state, action){
state.count += action.payload
}
}
})

export const {addCountAction} = countSlice.actions
const countReducer = countSlice.reducer
export default countReducer

导出的stero的index.js代码:

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

const store = configureStore({
reducer:{
counter:countReducer
}
})
export default store

stero使用的jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { memo, useLayoutEffect, useState} from 'react'
import { connect } from 'react-redux';
import { addCountAction } from './stero/countSlice';

function App(props) {
const {count, addCount} = props
function buttonCLick(){
addCount(3)
}
return (
<div>
<h2>count:{count}</h2>
<button onClick={buttonCLick}>+3</button>
</div>
)
}

function mapStateToProps(state){
return {
count:state.counter.count
}
}
function mapAction20Props(dispatch){
return {
addCount(count){
dispatch(addCountAction(count))

}
}
}
export default connect(mapStateToProps, mapAction20Props)(memo(App))

注:

  • 这种方式必须使用高阶函数结合返回的高阶组件
  • 并且必须编写:mapStateToProps和 mapDispatchToProps映射的函数

redux的Hook使用

useSelector与useDispatch的使用

App.jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { memo} from 'react'
import { useDispatch, useSelector } from 'react-redux';
import ChildComponent from './component/ChildComponent';
import { addCountAction } from './stero/countSlice';

function App(props) {
console.log("App渲染")
/**
* useSelector的第一个参数为函数,其返回一个对象,会把这个返回对象作为useSelector的值
*/
const {count} = useSelector((state)=>({
count:state.counter.count
}))
/**
* dispatch通过useDispatch直接获取,不再需要像
*/
const dispatch = useDispatch()
function buttonCLick(){
dispatch(addCountAction(4))
}
return (
<div>
<h2>count:{count}</h2>
<ChildComponent/>
<button onClick={buttonCLick}>改变count</button>
</div>
)
}
export default memo(App)

注:

可以看到能够直接使用useSelector对stero中的数据进行映射,直接通过useDispatch获取dispatch

但是上述会出现一些问题:

  • useSelector是监听的是state,也就是state改变后,所有使用useSelector的组件都会刷新

ChildComponent代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, {  memo } from 'react'
import { useSelector } from 'react-redux'

function ChildComponent(props) {
console.log("子组件渲染")
const {message} = useSelector((state)=>({
message:state.counter.message
}))
return (
<div>
<h2>子组件:{message}</h2>
</div>
)
}
export default memo(ChildComponent)

子组件只与state中的message有关,但当count改变时子组件依然会刷新,会引起性能的降低

动画

给useSelector设置shallowEqual就可以避免上述问题,实际上是进行一个浅层比较,如果相同就不更新

1
2
3
4
5
6
7
const {message} = useSelector((state)=>({
message:state.counter.message
}),shallowEqual)

const {count} = useSelector((state)=>({
count:state.counter.count
}),shallowEqual)

动画