后端性能指标优化

网站性能优化

  1. 响应时间
  2. 并发数量
  3. 吞吐量 ①TPS(每秒事务数)、②HPS(每秒Http请求数)、③QPS(每秒查询数,)

性能计数器

常用的性能计数器有:
System Load、对象和线程数、CPU使用、内存使用、磁盘和网络IO等指标。

性能测试的几个参考点:

  • 负载测试:系统的某项或者多想性能指标达到安全临界值时的并发数
  • 压力测试
  • 稳定性测试 PS:稳定性测试主要是长时间给系统一定的压力,看系统是否正常运行。

缓存

  1. 缓存的本质
    缓存的本质就是一个内存Hash表,数据以一对KeyValue键值对存储在内存Hash表中。主要用于存放读写比很高、很少变化的数据,网站数据通常遵循“二八定律”,即80%的访问落在20%的数据上,因此,将这20%的数据缓存起来,可以很好的改善系统性能。

  2. 合理使用缓存
    合理的使用缓存对提高系统性能有很多好处,但是不合理的使用缓存反而会成为系统的累赘甚至风险。滥用缓存的三种情况如下:

    • 频繁修改的数据:数据的读写比至少应该是2:1以上,即写入一次缓存,在数据更新前至少读写两次,缓存才有意义。真正实践中这个比例可能会更高。
    • 没有热点的访问:如果应用系统访问数据没有热点,不遵循二八定律,即大部分数据访问并没有集中在小部分数据中,那么缓存也没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。
    • 数据的不一致与脏读:写入缓存的数据最好能容忍一定时间的数据不一致,一般情况下最好对缓存的数据设置失效时间(固定值+一定范围的随机值)。如果不能容忍数据的不一致,必须在数据更新时,删除对应的缓存(思考:为什么不是更新缓存)可以参考这个文档,但是这种情况只针对读写比非常高的情况。
  3. 缓存雪崩
    缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
    该类问题的解决方式主要有三种:

①加锁排队。大概原理是在去数据库取数据的时候加锁排队,该方法仅仅适用于并发量不高的情况。

②在原有失效时间基础上加一个合理的随机值(0-5分钟)。分布式场景下最常见的方式(单机也可以)。

③给缓存加标记,在缓存失效之后更新缓存数据。

  1. 缓存穿透
      缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。

该类问题的主要解决方式。

①使用布隆过滤器做过滤。该方法仅仅用于查询一个不可能存在的数据。

②把不存在的数据也缓存起来。最佳实践:单独设置比较短的过期时间,比如说五分钟。

  1. 缓存预热
      缓存中存放的是热点数据,热点数据又是缓存系统利用某种算法对不断访问的数据筛选淘汰出来的,在重建缓存数据的过程中,系统的性能和数据库负载都不太好,那么多好的方式就是在缓存系统启动的时候就把热点数据加载好,这个缓存预加载的手段叫做缓存预热。对于一些元数据如省市区列表,类目信息,就可以在启动的加载数据库中的全部数据。

  2. 分布式缓存架构
    分布式缓存是指缓存部署在多个服务器组成的集群中,以集群方式提供缓存服务,其架构方式有两种:

①以JBosss Cache为代表的需要更新同步的分布式缓存(在所有服务器中保存相同的缓存数据)。

②以Memcache为代表的互不通信的分布式缓存(应用程序通过一致性Hash等路由算法选择缓存服务器远程访问远程数据,可以会容易的扩容,具有良好的可伸缩性)。

异步

使用异步操作,可以大幅度改善网站的性能,使用异步的两种场景,高并发、微服务;

①高并发,在不使用消息队列的情况下,用户的请求数据直接写入数据库,在高并发的情况下会对数据库造成一定的压力,同时也使得响应延迟加剧。使用消息队列具有很好的削峰作用,在电子商务网站促销活动中,使用消息队列是常见的技术手段。

②微服务之间调用,在微服务流行的当下,有时候我们调用其他系统的微服务接口,只是为了通知其他系统,我们不关心结果,这个时候我们可以使用单独的线程池异步调用其他系统的微服务,这样可以减少程序的响应时间。

任何可以晚点的事情都应该晚点再做。

集群

在网站高并发访问的场景洗下,使用负载均衡技术为一个应用构建一个由多台服务器组成的服务器集群,可以避免单一服务器因负载压力过大而响应缓慢。常用的负载均衡技术有以下几种:

①HTTP重定向负载均衡,不利于SEO,不推荐。

②DNS域名解析负载均衡,许多DNS服务器还支持基于地理位置的域名解析,会将域名解析成距离用户地理最近的一个服务器地址,这样可以加快访问速度。大公司常用的手段。

③反向代理负载均衡(应用层负载均衡),常见产品:Nginx,反向代理服务器的性能可能会成为瓶颈。

④IP负载均衡,在内核进程完成数据分发,叫反向代理负载均衡有更好的处理性能,网卡和带宽会成为主要的瓶颈。

⑤数据链路层负载均衡(三角传输模式),又名DR(直接路由模式),也是大型网站昌运宫的负载均衡手段,在Linux平台上最好的链路层负载均衡产品是LVS。

代码优化

网站的业务逻辑实现代码主要部署在应用服务器上,合理的优化代码也可以很好的改善网站性能。几种常用的几种代码优化方式:

①合理使用多线程,服务器的启动的线程数参考值:[任务执行时间/(任务执行时间-IO等待时间)]CPU内核数。

②资源复用,要尽量减少那些开销很大的系统资源的创建和销毁,比如数据库连接,网络通信连接、线程、复杂对象,从编程角度,资源复用主要有两种方式,单例、对象池。

③数据结构,前面缓存部分就已经提到了Hash表的基本原理,Hash表的读写性能在很大程度上依赖于HashCode的随机性,即HashCode越散列,Hash表的冲突就越少,目前比较好的Hash散列算法是Time33算法,算法原型为:hash(i) = hash(i-1)33+str[i]。

④垃圾回收,比如说在JVM里,合理设置Young Generation和Old Generation的大小,尽量减少Full GC,如果设置合理的话,可以在整个运行期间做到从不进行Full GC。

存储优化

在网站应用中,海量是的数据读写对磁盘访问会造成一定的压力,虽然可以通过Cache解决一部分数据读压力,但是很多时候,磁仍然是系统最严重的瓶颈。

机械硬盘VS固态硬盘
  这两个的区别我相信大家都知道了吧,机械硬盘是通过马达驱动磁头臂带动磁头到指定的磁盘位置访问数据,这个效率我就不用多说了吧,相反,固态硬盘的数据是存储在可以持久记忆的硅晶体上,因此可以像内存一样随机访问,而且功耗更小。

B+树 VS LSM树
  B+树是一种专门针对磁盘存储而优化的N叉排序树,以树节点为单位存储在磁盘中,从根开始查找所需的节点编号和磁盘位置,将其加载到内存中,然后继续查找,知道找到所需数据,目前大部分关系型数据库多采用两级索引的B+树,树的层次最多为3层。

目前很多NoSQL产品采用LSM树作为主要的数据结构,LSM树可以看做是一个N阶合并树,数据的写操作都在内存中完成,并且都会创建一个新记录,这些数据在内存中仍然还是一颗排序树。在需要读的时候,总是从内存中的排序树开始搜索,如果没有找到,就从磁盘的排序树中查找。

在LSM树上进行一次数据更新不需要磁盘访问,在内存中即可完成,速度远快于B+树,当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用LSM树可以极大程度的减少磁盘的访问次数,加快访问速度。

docker常用命令

容器使用

官方可用容器地址

dockerhub

docker 命令

1
2
3
4
5
6
7
8
9
10
11
# 载入镜像文件
docker pull nginx

# 启动容器
docker run -itd --name ubuntu-test ubuntu /bin/bash
# -i: 交互式操作。
# -t: 终端。
# -d: 参数默认不会进入容器,容器启动后会进入后台
# --name: 设置容器名字为ubuntu-test
# ubuntu: ubuntu 镜像。
# /bin/bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 /bin/bash。

容器的查看/关闭/停止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看所有的容器
docker ps -a

# 重启应用
docker restart 容器id

# 关闭容器
docker kill 容器id

# 启动容器
docker start 容器id

# 停止容器
docker stop 容器id

复制本地文件到容器目录

1
docker cp /usr/local/vendor.js  容器id:/usr/src/app/_site

进入容器

1
2
3
4
5
6
7
8
9
10
11
# 进入了容器,并且退出不会导致容器停止
docker exec -it 容器id /bin/bash

# 导出容器快照到本地文件
docker export 容器id > ubuntu.tar

# 导入容器快照
cat docker/ubuntu.tar | docker import - test/ubuntu:v1

# 此外,也可以指定url来导入
docker import http://example.com/image.tgz example/imagerepo

删除容器

1
docker rm -f 容器id

镜像使用

列出镜像列表

1
2
3
4
5
6
7
8
9
10
11
12
docker images

docker run -t -i ubuntu:15.10 /bin/bash

# 获取指定版本的镜像
docker pull ubuntu:13.10

# 查找镜像
docker search httpd

# 删除镜像
docker rmi hello-world

例子:docker创建mongo本地数据目录和配置文件

1
2
3
4
5
6
7
8
9
10
11
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
storage:
dbPath: /data/db
net:
port: 27037
bindIp: 0.0.0.0
#security:
#authorization: enabled
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
docker run -itd --name attack-i-mongo  -v /Users/attacki/mongo:/data/db -v /Users/attacki/mongo/db_config/mongo.conf:/data/configdb/mongo.conf -p 27037:27037 -d mongo -f /data/configdb/mongo.conf

# -v /Users/zhangzhi/mongodb:/data/db
# 宿主机 /Users/attacki/mongo 数据库目录
# 映射到容器 /data/db 目录

# -v /Users/attacki/mongo/db_config/mongo.conf:/data/configdb/mongo.conf
# 宿主机mongodb配置文件 /Users/attacki/mongo/db_config/mongo.conf
# 映射到容器 /data/configdb/mongo.conf

# -p 27037:27037 容器的27037端口,映射到宿主机的27037端口

# 进入运行容器的终端
docker exec -it attack-i-mongo mongo --host 127.0.0.1 --port 27037 admin

# 使用管理员
use admin

# 创建用户
db.createUser({user:'root',pwd:'root',roles:['root']})
db.createUser({ user: 'admin', pwd: 'admin', roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] });
# Successfully added user: {
# "user" : "admin",
# "roles" : [
# {
# "role" : "userAdminAnyDatabase",
# "db" : "admin"
# }
# ]
# }

# 授权admin
db.auth('admin','admin')
# 1

# 添加数据库操作员
db.createUser({ user: "attack-i", pwd: "attack-i", roles: [ { role: "readWrite", db: "attack-i" } ] });

# nodejs链接示例: mongodb://admin:admin@localhost:27037/attack-i?authSource=admin

React的hooks

针对react的函数式组件的渲染过程,需要了解一下,不然设置函数变量时,会导致一些莫名其妙的bug。

react Hook的使用

  1. 如何在组件加载时发起异步任务
  2. 如何在组件交互时发起异步任务
  3. 其他陷阱

一、react Hooks 发起异步请求

使用 useEffect 发起异步任务,第二个参数使用空数组可实现组件加载时执行方法体,返回值函数在组件卸载时执行一次,用来清理一些东西,例如计时器。
使用 AbortController 或者某些库自带的信号量 ( axios.CancelToken) 来控制中止请求,更加优雅地退出。
当需要在其他地方(例如点击处理函数中)设定计时器,在 useEffect 返回值中清理时,使用局部变量或者 useRef 来记录这个 timer。不要使用 useState。
组件中出现 setTimeout 等闭包时,尽量在闭包内部引用 ref 而不是 state,否则容易出现读取到旧值的情况。
useState 返回的更新状态方法是异步的,要在下次重绘才能获取新值。不要试图在更改状态之后立马获取状态。

二、如何在组件加载时发起异步任务

这类需求非常常见,典型的例子是在列表组件加载时发送请求到后端,获取列表后展现。

发送请求也属于 React 定义的副作用之一,因此应当使用 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
import React, { useState, useEffect } from 'react';
const SOME_API = '/api/get/value';
export const MyComponent: React.FC<{}> = => {
const [loading, setLoading] = useState(true);
const [value, setValue] = useState(0);
useEffect( => {
(async => {
const res = await fetch(SOME_API);
const data = await res.json;
setValue(data.value);
setLoading(false);
});
}, []);
return (
<>
{
loading ? (
<h2>Loading...</h2>
) : (
<h2>value is {value}</h2>
)r
}
</>
);
}

如上是一个基础的带 Loading 功能的组件,会发送异步请求到后端获取一个值并显示到页面上。如果以示例的标准来说已经足够,但要实际运用到项目中,还不得不考虑几个问题。

三、如果在响应回来之前组件被销毁了会怎样?

React 会报一个 Warning

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subions and asynchronous tasks in a useEffect cleanup http://function.in Notification
大意是说在一个组件卸载了之后不应该再修改它的状态。虽然不影响运行,但作为完美主义者代表的程序员群体是无法容忍这种情况发生的,那么如何解决呢?

问题的核心在于,在组件卸载后依然调用了 setValue(data.value)和 setLoading(false)来更改状态。因此一个简单的办法是标记一下组件有没有被卸载,可以利用 useEffect的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 省略组件其他内容,只列出 diff
useEffect( => {
let isUnmounted = false;
(async => {
const res = await fetch(SOME_API);
const data = await res.json;
if (!isUnmounted) {
setValue(data.value);
setLoading(false);
}
});
return => {
isUnmounted = true;
}
}, []);

这样可以顺利避免这个 Warning。

有没有更加优雅的解法?

上述做法是在收到响应时进行判断,即无论如何需要等响应完成,略显被动。一个更加主动的方式是探知到卸载时直接中断请求,自然也不必再等待响应了。这种主动方案需要用到 AbortController。

AbortController 是一个浏览器的实验接口,它可以返回一个信号量(singal),从而中止发送的请求。这个接口的兼容性不错,除了 IE 之外全都兼容(如 Chrome, Edge, FF 和绝大部分移动浏览器,包括 Safari)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
useEffect( => {
let isUnmounted = false;
const abortController = new AbortController; // 创建
(async => {
const res = await fetch(SOME_API, {
singal: abortController.singal, // 当做信号量传入
});
const data = await res.json;
if (!isUnmounted) {
setValue(data.value);
setLoading(false);
}
});
return => {
isUnmounted = true;
abortController.abort; // 在组件卸载时中断
}
}, []);

singal 的实现依赖于实际发送请求使用的方法,如上述例子的 fetch方法接受 singal属性。如果使用的是 axios,它的内部已经包含了 axios.CancelToken,可以直接使用,例子在这里。

四、如何在组件交互时发起异步任务

另一种常见的需求是要在组件交互(比如点击某个按钮)时发送请求或者开启计时器,待收到响应后修改数据进而影响页面。这里和上面一节(组件加载时)最大的差异在于 React Hooks 只能在组件级别编写,不能在方法( dealClick)或者控制逻辑( if, for等)内部编写,所以不能在点击的响应函数中再去调用 useEffect。但我们依然要利用 useEffect的返回函数来做清理工作。

以计时器为例,假设我们想做一个组件,点击按钮后开启一个计时器(5s),计时器结束后修改状态。但如果在计时未到就销毁组件时,我们想停止这个计时器,避免内存泄露。用代码实现的话,会发现开启计时器和清理计时器会在不同的地方,因此就必须记录这个 timer。看如下的例子:

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, { useState, useEffect } from 'react';
export const MyComponent: React.FC<{}> = => {
const [value, setValue] = useState(0);
let timer: number;
useEffect( => {
// timer 需要在点击时建立,因此这里只做清理使用
return => {
console.log('in useEffect return', timer); // <- 正确的值
window.clearTimeout(timer);
}
}, []);
function dealClick {
timer = window.setTimeout( => {
setValue(100);
}, 5000);
}
return (
<>
<span>Value is {value}</span>
<button onClick={dealClick}>Click Me!</button>
</>
);
}

既然要记录 timer,自然是用一个内部变量来存储即可(暂不考虑连续点击按钮导致多个 timer 出现,假设只点一次。因为实际情况下点了按钮还会触发其他状态变化,继而界面变化,也就点不到了)。

这里需要注意的是,如果把 timer升级为状态(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
import React, { useState, useEffect } from 'react';
export const MyComponent: React.FC<{}> = => {
const [value, setValue] = useState(0);
const [timer, setTimer] = useState(0); // 把 timer 升级为状态
useEffect( => {
// timer 需要在点击时建立,因此这里只做清理使用
return => {
console.log('in useEffect return', timer); // <- 0
window.clearTimeout(timer);
}
}, []);
function dealClick {
let tmp = window.setTimeout( => {
setValue(100);
}, 5000);
setTimer(tmp);
}
return (
<>
<span>Value is {value}</span>
<button onClick={dealClick}>Click Me!</button>
</>
);
}

有关语义上 timer到底算不算作组件的状态我们先抛开不谈,仅就代码层面来看。利用 useState来记住 timer状态,利用 setTimer去更改状态,看似合理。但实际运行下来,在 useEffect返回的清理函数中,得到的 timer却是初始值,即 0。

为什么两种写法会有差异呢?

其核心在于写入的变量和读取的变量是否是同一个变量。

第一种写法代码是把 timer作为组件内的局部变量使用。在初次渲染组件时, useEffect返回的闭包函数中指向了这个局部变量 timer。在 dealClick中设置计时器时返回值依旧写给了这个局部变量(即读和写都是同一个变量),因此在后续卸载时,虽然组件重新运行导致出现一个新的局部变量 timer,但这不影响闭包内老的 timer,所以结果是正确的。

第二种写法, timer是一个 useState的返回值,并不是一个简单的变量。从 React Hooks 的源码来看,它返回的是 [hook.memorizedState,dispatch],对应我们接的值和变更方法。当调用 setTimer和 setValue时,分别触发两次重绘,使得 hook.memorizedState指向了 newState(注意:不是修改,而是重新指向)。但 useEffect返回闭包中的 timer依然指向旧的状态,从而得不到新的值。(即读的是旧值,但写的是新值,不是同一个)

如果觉得阅读 Hooks 源码有困难,可以从另一个角度去理解:虽然 React 在 16.8 推出了 Hooks,但实际上只是加强了函数式组件的写法,使之拥有状态,用来作为类组件的一种替代,但 React 状态的内部机制没有变化。在 React 中 setState内部是通过 merge 操作将新状态和老状态合并后,重新返回一个新的状态对象。不论 Hooks 写法如何,这条原理没有变化。现在闭包内指向了旧的状态对象,而 setTimer和 setValue重新生成并指向了新的状态对象,并不影响闭包,导致了闭包读不到新的状态。

我们注意到 React 还提供给我们一个 useRef, 它的定义是:

useRef 返回一个可变的 ref 对象,其 current属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
ref 对象可以确保在整个生命周期中值不变,且同步更新,是因为 ref 的返回值始终只有一个实例,所有读写都指向它自己。所以也可以用来解决这里的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent: React.FC<{}> = => {
const [value, setValue] = useState(0);
const timer = useRef(0);
useEffect( => {
// timer 需要在点击时建立,因此这里只做清理使用
return => {
window.clearTimeout(timer.current);
}
}, []);
function dealClick {
timer.current = window.setTimeout( => {
setValue(100);
}, 5000);
}
return (
<>
<span>Value is {value}</span>
<button onClick={dealClick}>Click Me!</button>
</>
);
}

事实上我们后面会看到, useRef和异步任务配合更加安全稳妥。

其他陷阱 修改状态是异步的

这个其实比较基础了。

1
2
3
4
5
6
7
8
9
10
11
import React, { useState } from 'react';
export const MyComponent: React.FC<{}> = => {
const [value, setValue] = useState(0);
function dealClick {
setValue(100);
console.log(value); // <- 0
}
return (
<span>Value is {value}, AnotherValue is {anotherValue}</span>
);
}

useState返回的修改函数是异步的,调用后并不会直接生效,因此立马读取 value获取到的是旧值( 0)。

React 这样设计的目的是为了性能考虑,争取把所有状态改变后只重绘一次就能解决更新问题,而不是改一次重绘一次,也是很容易理解的。

在 timeout 中读不到其他状态的新值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from 'react';
export const MyComponent: React.FC<{}> = => {
const [value, setValue] = useState(0);
const [anotherValue, setAnotherValue] = useState(0);
useEffect( => {
window.setTimeout( => {
console.log('setAnotherValue', value) // <- 0
setAnotherValue(value);
}, 1000);
setValue(100);
}, []);
return (
<span>Value is {value}, AnotherValue is {anotherValue}</span>
);
}

这个问题和上面使用 useState去记录 timer类似,在生成 timeout 闭包时,value 的值是 0。虽然之后通过 setValue修改了状态,但 React 内部已经指向了新的变量,而旧的变量仍被闭包引用,所以闭包拿到的依然是旧的初始值,也就是 0。

要修正这个问题,也依然是使用 useRef,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent: React.FC<{}> = => {
const [value, setValue] = useState(0);
const [anotherValue, setAnotherValue] = useState(0);
const valueRef = useRef(value);
valueRef.current = value;
useEffect( => {
window.setTimeout( => {
console.log('setAnotherValue', valueRef.current) // <- 100
setAnotherValue(valueRef.current);
}, 1000);
setValue(100);
}, []);
return (
<span>Value is {value}, AnotherValue is {anotherValue}</span>
);
}

还是 timeout 的问题

假设我们要实现一个按钮,默认显示 false。当点击后更改为 true,但两秒后变回 false( true 和 false 可以互换)。考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { useState } from 'react';
export const MyComponent: React.FC<{}> = => {
const [flag, setFlag] = useState(false);
function dealClick {
setFlag(!flag);
setTimeout( => {
setFlag(!flag);
}, 2000);
}
return (
<button onClick={dealClick}>{flag ? "true" : "false"}</button>
);
}

我们会发现点击时能够正常切换,但是两秒后并不会变回来。究其原因,依然在于 useState的更新是重新指向新值,但 timeout 的闭包依然指向了旧值。所以在例子中, flag一直是 false,虽然后续 setFlag(!flag),但依然没有影响到 timeout 里面的 flag。

解决方法有二。

第一个还是利用 useRef:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useRef } from 'react';
export const MyComponent: React.FC<{}> = => {
const [flag, setFlag] = useState(false);
const flagRef = useRef(flag);
flagRef.current = flag;
function dealClick {
setFlag(!flagRef.current);
setTimeout( => {
setFlag(!flagRef.current);
}, 2000);
}
return (
<button onClick={dealClick}>{flag ? "true" : "false"}</button>
);
}

第二个是利用 setFlag可以接收函数作为参数,并利用闭包和参数来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { useState } from 'react';
export const MyComponent: React.FC<{}> = => {
const [flag, setFlag] = useState(false);
function dealClick {
setFlag(!flag);
setTimeout( => {
setFlag(flag => !flag);
}, 2000);
}
return (
<button onClick={dealClick}>{flag ? "true" : "false"}</button>
);
}

当 setFlag参数为函数类型时,这个函数的意义是告诉 React 如何从当前状态产生出新的状态(类似于 redux 的 reducer,不过是只针对一个状态的子 reducer)。既然是当前状态,因此返回值取反,就能够实现效果。

总结

在 Hook 中出现异步任务尤其是 timeout 的时候,我们要格外注意。useState只能保证多次重绘之间的状态值是一样的,但不保证它们就是同一个对象,因此出现闭包引用的时候,尽量使用 useRef而不是直接使用 state 本身,否则就容易踩坑。反之如果的确碰到了设置了新值但读取到旧值的情况,也可以往这个方向想想,可能就是这个原因所致。

git常用命令

本地仓库与远端仓库链接

1
2
3
4
5
6
7
8
9
10
11
12
13

# 查看远端仓库
git remote -v

# 查看当前配置的用户名和邮箱 ,如果没有设置需要进行设置
git config --global --list

# 需要注意需提前将本机生成的ssh公钥保存在远端仓库个人账户中
# 将本地仓库与远端仓库建立链接
git remote add origin git@gitee.com:attacki/attack-i.git

# 取消git跟踪node_debug.log文件
git checkout -- node_debug.log

更改远程仓库地址

1
2
# 修改命令
git remote set-url origin https://github.com/Attacki/attacki.git

git常见用法

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
# 在dev分支
git add .

# 代码提交
git commit -m 'fix: 修复服务监控mettingId丢失'

# 代码推送远端
git push

# 切换到release分支
git checkout release

# 新建并切换到一个bugfix分支,为的是将dev分支的修改,追加到bugfix分支上,然后合并到release上
git checkout -b bugfix_monitor_0715

git reflog

# 将dev分支的提交的那次修改添加到bugfix_monitor_0715上面
git cherry-pick 1d72444b0d6fe707d2835c01fec0a125fad93ff6

# 将bugfix分支推送远端部署,并验证bug
git push --set-upstream origin bugfix_monitor_0715

# 将验证过的bugfix分支合入到release
git merge bugfix_monitor_0715

# 合并
git merge dev

git代码暂存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 保存当前未commit的代码
git stash

# 保存当前未commit的代码并添加备注
git stash save "备注的内容"

# 列出stash的所有记录
git stash list

# 删除stash的所有记录
git stash clear

# 应用最近一次的stash
git stash apply

# 应用最近一次的stash,随后删除该记录
git stash pop

# 删除最近的一次stash
git stash drop

cherry-pick用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 一次转移多个提交:
git cherry-pick commit1 commit2

# 多个连续的commit,也可区间复制:
git cherry-pick commit1^..commit2

# 在cherry-pick有冲突时, 需要解决代码冲突,重新提交到暂存区, 然后使用下面命令
gits cherry-pick --continue

# 放弃 cherry-pick, 恢复到cherry-pick之前的样子
gits cherry-pick --abort

# 退出 cherry-pick, 保留已经 cherry-pick 成功的 commit,并退出 cherry-pick 流程
gits cherry-pick --quit

git分支代码 merge

1
2
3
4
5
6
7
8
9
10

# 最基本的合并方式, 包含完整的提交历史记录。
git checkout master
git merge dev



# 会把差异暂存本地, 需要自己commit, 会丢失历史记录, 因为改动变成自己提交的了
git checkout master
git merge --squash dev

rebase merge 可以保留提交的作者信息,同时可以合并commit历史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

git checkout dev
# 将master上面所有dev分支所没有的提交, 都给拉过来
git rebase master

# 上面这一步可能会有冲突(如果没有,可省略这一步), 解决冲突后, add到暂存区
git add .

# 继续rebase的完成
git rebase --continue

# 放弃rebase操作
git rebase --abort

# 回到 master 分支, 然后直接 merge dev
git checkout master
git merge dev