0%

如果说flex适合做一维(水平或竖直放向)元素的布局,那么grid就是做二维布局的,如字面意思,grid布局将平面划分成 m*n 的网格,子元素分布其中,以所处/所占的行和列的控制实现页面layout的划分

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
<div class="wrapper">
<div class="one">One</div>
<div class="two">Two</div>
<div class="three">Three</div>
<div class="four">Four</div>
<div class="five">Five</div>
<div class="six">Six</div>
</div>
<style>
.wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
grid-auto-rows: minmax(100px, auto);
}
.one {
grid-column: 1 / 3;
grid-row: 1;
}
.two {
grid-column: 2 / 4;
grid-row: 1 / 3;
}
.three {
grid-row: 2 / 5;
grid-column: 1;
}
.four {
grid-column: 3;
grid-row: 3;
}
.five {
grid-column: 2;
grid-row: 4;
}
.six {
grid-column: 3;
grid-row: 4;
}
</style>

display: grid 或 inline-grid区分整个区域是否作为行内元素插入,不影响区域内的grid

列划分:

1
2
3
grid-template-columns: 200px 500px 100px;
grid-template-columns: repeat(4, 25%);
grid-template-columns: 200px repeat(3, 1fr) 100px;

注:fr是grid的特殊单位,可以从总分列去掉固定宽度列后均分剩余列
行划分grid-template-rows类似,也可以按比例划分剩余宽度

间距:

1
2
3
grid-gap: 5px;
grid-row-gap: 5px;
grid-column-gap: 5px;

填充:

1
grid-template-columns: repeat(auto-fill, 200px);

以200px为一列,根据区域宽度调整列数(响应式)

区间:

1
grid-template-columns: 1fr 1fr minmax(300px, 2fr)

第三个列宽最少也是要 300px,但是最大不能大于第一第二列宽的两倍。

区域定义:(略)

流:

1
2
3
grid-auto-flow: row;
grid-auto-flow: row dense;
grid-auto-flow: column;

填充单元格的横纵顺序,如果遇到尺寸不够而挤到下一行的情况,dense可以使用合适的子元素填充前面的空余单元格

对齐:
justify-item, align-item:父容器配置,分别控制子元素在所处单元格空间的对齐方向,两者默认是stretch撑满空间
可选值:start center end stretch

justify-content, align-content:父容器配置,分别控制整个grid(父容器)在上层容器中的对齐方向,两者默认是start

justify-self, align-self:子元素配置,控制自身在所处单元格空间的对齐方向与*-item一致

隐式网格:
在超出grid-template-columns和grid-template-rows的定义之后应用的网格划分

1
2
3
grid-template-columns: 200px 500px 100px;
grid-template-rows: 100px 100px;
grid-auto-rows: 50px;

指定坐标:

1
2
3
4
5
6
.item {
grid-column-start: 3;
grid-column-end: 4;
grid-row-start: 1;
grid-row-end: 4;
}

svg 可缩放矢量图形(Scalable Vector Graphics), 使用xml格式定义图形,大概长这样子

1
2
3
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
</svg>

嵌入html中可以作为dom操作,在数据可视化入门中曾提到D3数据可视化库即使用svg进行动态渲染的

对于简单的icon响应可以有

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
<style>
.toggle-btn {
display: inline-block;
width: 40px;
height: 40px;
cursor: pointer;
color: #000;
background: #ccc;
}

.toggle-btn>input {
width: 0;
height: 0;
opacity: 0;
}

.toggle-btn>svg {
width: 100%;
height: 100%;
}

input:checked+svg circle {
fill: blue
}
</style>
<label class="toggle-btn">
<input type="checkbox">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="20" cy="20" r="20" stroke="black" stroke-width="2" fill="none" />
</svg>
</label>

svg-react-loader

将.svg文件资源作为组件载入

1
2
3
import MyIcon from '-!svg-react-loader!../../assets/image/icon.svg'
...
return (<> <MyIcon> <>)

作为资源路径
1
2
3
import MyIcon from '../../assets/image/icon.svg'
...
return (<img src={MyIcon} />)

作为inline element(原生React特性)
1
2
3
4
5
import {ReactComponent as MyIcon} from '../../assets/image/icon.svg'
...
return (<div style={{color:'red', cursor: 'pointer'}}>
<MyIcon />
</div>)

ng-inline-svg

shared.module.ts

1
2
3
4
5
6
7
8
9
10
import { InlineSVGModule } from 'ng-inline-svg'

@NgModule({
imports: [...LibModules,
InlineSVGModule.forRoot()],
exports: [...MuiModules],
declarations: [],
providers: [],
})
export class SharedModule { }

mycomponent.html
1
<div [inlineSVG]="'assets/image/icon.svg'"></div>

鼠标响应

Web MDN: svg pointer-events

async await写一个训练过程

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
async function practise() {
console.log('训练开始...')
let result;
result = await warmup(2)
if(result) console.log('热身完成。。')
result = await squat(5)
if(result) console.log('深蹲完成。。')
result = await boating(3)
if(result) console.log('划船完成。。')
}

function warmup(time){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, time*1000);
});
}

function squat(time){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, time*1000);
});
}

function boating(time){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, time*1000);
});
}

// 执行这个‘训练’
practise().then(res=>{
console.log('完了')
})

从输出结果上看,特点是按照顺序逐步完成某动作,似乎本身就是一个generator
于是把每个动作装到一个generator里
1
2
3
4
5
function* practisePlan() {
yield warmup(2)
yield squat(5)
yield boating(3)
}

yield返回的是一个promise,promise ‘resolve’的情况下才会 ‘next’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let practise = function () {
return new Promise((resolve, reject)=>{
const plan = practisePlan()
Promise.resolve(plan.next().value).then(result=>{
if(result) console.log('热身完成。。')
return plan.next().value;
}).then(result=>{
if(result) console.log('深蹲完成。。')
return plan.next().value;
}).then(result=>{
if(result) console.log('划船完成。。')
resolve() // <--完了
})
})
}

再看循环中的async await的栗子 —-> S-为什么说 async/await是generator的语法糖?

Node.js Streams: Everything you need to know(译文)

在 Node.js 中有四种基本的流类型:Readable(可读流),Writable(可写流),Duplex(双向流),Transform(转换流)。

  • 可读流是数据可以被消费的源的抽象。一个例子就是 fs.createReadStream 方法。
  • 可写流是数据可以被写入目标的抽象。一个例子就是 fs.createWriteStream 方法。
  • 双向流即是可读的也是可写的。一个例子是 TCP socket。
  • 转换流是基于双向流的,可以在读或者写的时候被用来更改或者转换数据。一个例子是 zlib.createGzip 使用 gzip 算法压缩数据。你可以将转换流想象成一个函数,它的输入是可写流,输出是可读流。你或许也听过将转换流成为“通过流(through streams)”。

所有的流都是 EventEmitter 的实例。触发它们的事件可以读或者写入数据,然而,我们可以使用 pipe 方法消费流的数据。

从流到流

面试补充(2026-07-22)

本节为后续补充,用于资深软件工程师基础面试复习;上文保留原始笔记。

流解决的是分段处理与速度匹配

Stream 不只是“读取大文件的 API”。它提供一套协议,让数据可以分段产生、转换和消费,并在上下游速度不一致时传递背压。

readFile() 把完整文件读入内存相比,流式管道可以更早地产生首批输出,稳态内存通常与各阶段缓冲和并发量相关,而不是与整个文件大小线性增长。但这不是“流永远只占一个 chunk 的内存”:每个阶段都有缓冲,业务代码也可能缓存 chunk,错误的并发 Transform 仍会扩大内存。

四种流和两种模式

  • Readable 是数据源,例如文件读取、HTTP request 和查询结果游标。
  • Writable 是数据目标,例如文件写入、HTTP response 和压缩包输出。
  • Duplex 同时可读写,读写两侧相互独立,例如 TCP socket。
  • Transform 是一种 Duplex,输出由输入转换而来,例如 gzip、解码和逐行解析。

默认模式处理 BufferUint8Array 或字符串,highWaterMark 按字节相关单位工作。objectMode 可以传任意 JavaScript 值,此时 highWaterMark 计的是对象数量;一个对象可能非常大,所以“缓存 16 个对象”不代表内存很小。

Readable 还可能处于 paused 或 flowing 状态。注册 'data' 监听、调用 resume() 或使用 pipe() 会影响消费方式。不要同时混用 'data''readable'pipe() 和 async iterator 去竞争消费同一条流,除非清楚状态切换和数据所有权。

highWaterMarkwrite()drain

highWaterMark 是内部缓冲达到何种程度后开始施加背压的阈值,不是硬内存上限,也不表示单个 chunk 一定小于该值。

writable.write(chunk) 返回 false 的含义是:当前缓冲已达到或超过阈值,生产者应停止继续写,等待 'drain' 后再恢复。返回值并不表示这次 chunk 写入失败;数据通常已进入内部缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { once } from 'node:events'
import type { Writable } from 'node:stream'

async function writeAll(
writable: Writable,
chunks: AsyncIterable<Buffer>
): Promise<void> {
for await (const chunk of chunks) {
if (!writable.write(chunk)) {
await once(writable, 'drain')
}
}

const finish = once(writable, 'finish')
writable.end()
await finish
}

生产代码还要定义 error 和 abort 策略。若忽略 false 并持续写入,慢消费者前方的缓冲会不断积累,导致 RSS 增长、GC 压力、延迟扩大,最终可能进程退出。这个问题不是通过把 highWaterMark 调得更大就能根治;首先要让生产速度受消费能力约束。

pipe()pipeline()

readable.pipe(writable) 会处理常见的暂停与恢复,但多段管道发生错误时,业务仍需正确销毁其他阶段并等待收尾。pipeline() 把完成、错误传播和销毁组合为一个操作,Promise 版本也便于与 async/await 集成:

1
2
3
4
5
6
7
8
9
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { createGzip } from 'node:zlib'

await pipeline(
createReadStream('telemetry.ndjson'),
createGzip(),
createWriteStream('telemetry.ndjson.gz')
)

如果读取失败、压缩失败或写入失败,返回的 Promise 会 reject,管道中的相关流会被销毁。调用方仍要决定如何记录、重试、清理不完整输出,以及任务是否幂等。对长任务还可以给 pipeline()AbortSignal,但取消后的业务状态仍需显式定义。

不要只写:

1
source.pipe(transform).pipe(destination)

然后只在最后一个流监听 error。错误可能来自任一阶段;手工拼装时必须明确每个阶段的错误传播和资源清理。

Async Iterator 与结束语义

Readable 支持 async iterator,适合用顺序控制流消费数据:

1
2
3
4
5
6
7
import { createReadStream } from 'node:fs'

const input = createReadStream('telemetry.ndjson', { encoding: 'utf8' })

for await (const chunk of input) {
console.log(chunk.length)
}

for await...of 会配合 Readable 的背压逐步拉取数据,异常会从循环抛出。提前 break 时通常会销毁流;若业务要求保留流继续消费,需要使用支持的 iterator 选项并明确后续所有者,而不是依赖偶然行为。

结束语义也必须区分:Readable 的 'end' 表示没有更多数据;Writable 的 'finish' 表示调用 end() 后数据已交给底层系统处理;'close' 表示资源关闭,不等于业务数据已经持久化。网络断开时还可能出现不完整消息,协议层需要帧边界、校验和重连策略。

大文件和实时消息的工程边界

大文件上传或压缩

优先让请求体、校验、压缩和对象存储上传形成受背压约束的管道。限制总大小、单段大小、处理时间和并发数;即使采用流,也不能接受无限输入。失败后清理临时文件或未完成的 multipart upload。

逐行遥测处理

TCP 和文件 chunk 不等于业务消息,一条 NDJSON 或协议帧可能跨多个 chunk,也可能一个 chunk 包含多条消息。解析器要保留不完整尾部、限制单帧长度,并对坏数据定义丢弃、告警或断连策略。

机器人与工业消息

背压策略取决于语义:高频位姿展示可能只保留每台设备最新值;报警、审计和控制命令通常不能静默覆盖。系统需要为不同消息类别定义有界队列、采样、合并、落盘或拒绝策略,而不是只有一个全局缓冲区。

“流为什么更省内存”的量化回答

比较两种实现时,应测量相同文件、并发和处理链路下的峰值 RSS、heapUsed、external memory、吞吐量和尾延迟。流式方案的预期不是内存为常数零,而是内存不随单个完整输入线性膨胀,并在消费者变慢时能限制生产者。

常见追问与回答边界

write() 返回 false 是否表示写入失败?

不是。它表示数据已进入缓冲,但生产者应等待 'drain' 再继续。真正的写入失败通过 error 通道报告。

highWaterMark 调大是否一定提高吞吐量?

不一定。更大的缓冲可能减少部分调度,但会提高内存和排队延迟,也可能掩盖消费端瓶颈。应基于 chunk 大小、下游速度和测量调整。

pipe() 已经有背压,为什么还需要 pipeline()

pipeline() 的主要增量是把整条链路的完成、错误传播和资源销毁收敛成一个可等待结果,而不是发明另一套背压。

使用 Stream 是否就不会内存泄漏?

不会。业务仍可能缓存 chunk、无限并发处理、忘记移除监听器或在错误后不销毁资源。Stream 提供协议,正确性取决于使用者是否遵守背压和生命周期。

Blob Storage存储资源的三个层次

  • storage account 存储实例的顶级层次
  • containers 相当于目录
  • blob 文件实体

Azure 门户点击Storage Account,查看当前tenant的Storage Account,进入其中某个account, 关于存储,提供了一个Explorer工具
左侧工具 Blob Services - Containers 创建容器csd-commom,(意外地发现之前做App Services备份地Deploy Packages在这里)
进入容器,可以直接使用页面提供的上传入口,上传可以填写一个folder,文件上传时自动使用该folder作为子目录
官方教程:使用 Azure 存储在云中上传图像数据
$blobStorageAccount(存储账户)
$blobStorageAccount(容器)
myAppServicePlan(应用服务计划)
myResourceGroup(资源组)
$webapp(App Service)dotNet Blob Uploader

YourStorageAccount — Settings — Access keys 查看账户密钥以及连接字符串

dotNet Blob Client Package: Azure.Storage

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
using Azure.Storage;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

...
public Task<bool> UploadFileToStorage(Stream fileStream){
// Create a URI to the blob
Uri blobUri = new Uri("https://" +
_storageConfig.AccountName +
".blob.core.windows.net/" +
_storageConfig.ImageContainer +
"/" + fileName);
// connection credential
StorageSharedKeyCredential storageCredentials =
new StorageSharedKeyCredential(_storageConfig.AccountName, _storageConfig.AccountKey);

// Create the blob client.
BlobClient blobClient = new BlobClient(blobUri, storageCredentials);

// Upload the file
await blobClient.UploadAsync(fileStream);

return await Task.FromResult(true);
}

public static async Task<List<string>> GetThumbNailUrls(AzureStorageConfig _storageConfig)
{
List<string> thumbnailUrls = new List<string>();

// Create BlobServiceClient from the account URI
BlobContainerClient container = new BlobContainerClient(connectionString, _storageConfig.ThumbnailContainer);

// Get reference to the container
BlobContainerClient container = blobServiceClient.GetBlobContainerClient(_storageConfig.ThumbnailContainer);

if (container.Exists())
{
foreach (BlobItem blobItem in container.GetBlobs())
{
thumbnailUrls.Add(container.Uri + "/" + blobItem.Name);
}
}

return await Task.FromResult(thumbnailUrls);
}

Blob SDK for Node.js: @azure/storage-blob
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 express = require('express')
const bodyParser = require('body-parser')
const multer = require('multer')
const _fs = require('fs')
const { StorageSharedKeyCredential,
BlobServiceClient } = require('@azure/storage-blob')
const {AbortController} = require('@azure/abort-controller')
const app = express();

// init blob client
const STORAGE_ACCOUNT_NAME = 'YourResourceGroupName'
const CONTAINER_NAME = 'BlobStorageContainerName'
const ACCOUNT_ACCESS_KEY ='****************************'
const ONE_MEGABYTE = 1024 * 1024;
const FOUR_MEGABYTES = 4 * ONE_MEGABYTE;
const ONE_MINUTE = 60 * 1000;
const aborter = AbortController.timeout(30 * ONE_MINUTE);
const credentials = new StorageSharedKeyCredential(STORAGE_ACCOUNT_NAME, ACCOUNT_ACCESS_KEY);

const blobServiceClient = new BlobServiceClient(`https://${STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,credentials);

const containerClient = blobServiceClient.getContainerClient(CONTAINER_NAME);
//app.use(bodyParser.urlencoded({ extended: false }));
const upload = multer({dest:'/uploads'})
app.post('/upload',upload.single('file'), async (req,res)=>{
var des_file = __dirname + '/tmp/' +req.file.originalname;
const stream = _fs.createReadStream(req.file.path)
const blobClient = containerClient.getBlobClient(req.file.originalname);
const blockBlobClient = blobClient.getBlockBlobClient();
const uploadOptions = {
bufferSize: FOUR_MEGABYTES,
maxBuffers: 5,
};
const result = await blockBlobClient.uploadStream(
stream,
uploadOptions.bufferSize,
uploadOptions.maxBuffers,
aborter);
res.json(result)
})
const port =process.env.PORT||3000;
app.listen(port,()=>{
console.log('server on port:', port)
})

Azure Blob js SDK

—>自己的栗子

file input

前端文件上传入口一律使用input type=”file”, 关于UI的优化可参考Angular-Tips

图片以base64存放关系数据库

File对象转Uri

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<input type="file" (change)="handleUpload($event)">
————————————————————————————
handleUpload(event) {
const fileInput = event.target;
const imgFile: File = fileInput.files[0];
if (imgFile.type !== 'image/jpeg' && imgFile.type !== 'image/png') {
this.msgService.error('You can only upload JPG file or PNG file.');
return;
}
if (imgFile.size! / 1024 > this.logoSizeLimit) {
this.msgService.error(`Image must smaller than ${this.logoSizeLimit}k.`);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this.logoUri = e.target.result;
}
reader.readAsDataURL(imgFile);
}

logoUri即图片经Bese64编码的字符串,可以直接存入数据库字段,可放入img:src作为上传预览

文件和流

Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。

File继承Blob, File作为特殊的Blob,可以用在任意的 Blob 类型的 context 中。比如FileReader, URL.createObjectURL(), createImageBitmap(), 及 XMLHttpRequest.send()

上一节使用的FileReader.readAsDataURL方法,读取指定的 Blob 或 File 对象。读取操作完成的时候,readyState 会变成已完成DONE,并触发 loadend 事件,同时 result 属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。

URL.createObjectURL(object)返回一个DOMString,其包含object的URL,console中输出的话所谓的DOMString形如

1
blob:https://localhost:44362/fd57b5f3-a3b9-47ae-bd9d-56fc9012fb83

其生命周期与当前document相同,调用URL.revokeObjectURL释放

canvas.toBlob

1
2
3
4
5
6
7
8
9
10
function canvas2file(){
var image = document.querySelector('img');
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height= image.height;
var ctx = canvas.getContext("2d");
ctx.drawImage( image, 0, 0 );
console.log(canvas.toBlob());//转换成bold类型
console.log(canvas.toDataURL());//转换成dataURL类型
}

格物致知:从响应式编程理解‘流’
nodejs_stream

大文件分片上传

受控组件和不受控组件

以封装Html表单控件的组件为例,假设ControlledComponent渲染一个input控件,为使input的value可以通过ControlledComponent进行控制,
会在ControlledComponent的state为input的value创建一个属性,比如this.state.text,这个属性会绑定到input上,同时input修改时会触发onChange事件,于是在onChange的响应方法中setState更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ControlledComponent extends React.Component{
constructor (props) {
super(props);
this.state = {
text: "add your comments"
}
}
onChange (e) {
console.log(e.target.value);
this.setState({
text: e.target.value
})
}
render () {
return <input name="text" value={this.state.text} onChange={(e) => this.onChange(e)} />
}
}

在HTML的表单元素中,它们通常自己维护一套state,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的。而如果将React里的state属性和表单元素的值建立依赖关系,再通过onChange事件与setState()结合更新state属性,就能达到控制用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做受控组件。 掘金:受控和非受控组件真的那么难理解吗?

props

略 见React 子组件传参

context

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Tools  from "./Tools"
const AppContext = React.createContext(null);
const [ var1, var2, var3] =[ "111", "121", "311" ]
const App = () => {
return (
<AppContext.Provider value={
{
var1,
var2,
var3
}
}>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
<Tools />
</AppContext.Provider>
);
};
export { App, AppContext };

Tools.tsx略
ToolButton.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { AppContext } from "./App";
export default () => {
return (
<AppContext.Consumer>
{({ var1, var2, var3 }) => (
<>
<button >{var1}</button>
<button >{var2}</button>
<button >{var3}</button>
</>
)}
</AppContext.Consumer>
);
};

hook

这里不只是组件交互的范畴,React hook是一套新的状态管理API
useState

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

function Example() {
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = useState(0);

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

useState使用闭包定义setXX函数(参考函数式编程看React Hooks),大致如下
1
2
3
4
5
6
7
8
9
let _state;
function useState(initialState) {
_state = _state || initialState; // 如果存在旧值则返回, 使得多次渲染后的依然能保持状态。
function setState(newState) {
_state = newState;
render(); // 重新渲染,将会重新执行 Counter
}
return [_state, setState];
}

useEffect
Effect是指获取数据,订阅,Dom操作等。useEffect跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API,即组件在初始化完成、重新渲染、即将销毁时执行useEffect指定的逻辑。
1
2
3
4
5
6
7
8
9
10
/**
* Accepts a function that contains imperative, possibly effectful code.
*
* @param effect Imperative function that can return a cleanup function
* @param deps If present, effect will only activate if the values in the list change.
*
* @version 16.8.0
* @see https://reactjs.org/docs/hooks-reference.html#useeffect
*/
function useEffect(effect: EffectCallback, deps?: DependencyList): void;

useEffect接受一个‘事件’列表作为可选参数
关于订阅/取消订阅和清除函数
常见于需要在组件初始化后(componentDidMount)设置订阅,在组件销毁前(componentWillUnmount)取消订阅以免内存泄漏,对应于使用Effect钩子的函数式React组件:
1
2
3
4
5
6
7
8
9
10
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

Effectf返回一个函数,该函数会作为Effect钩子的清除函数,React组件自动在销毁前执行清除函数
useContext
上面的ToolBtn经改写变为
1
2
3
4
5
6
7
8
9
10
export default () => {
const {var1, var2, var3} = useContext(AppContext);
return (
<>
<button >{var1}</button>
<button >{var2}</button>
<button >{var3}</button>
</>
)
};

useCallback
useCallback是一个允许在多次渲染中缓存函数的hook

useState不能返回一个方法(读取类型为function的state,会自动执行返回结果),当需要根据若干state的变化得到动态的函数时,使用useCallback, useCallback的定义形如useEffect

1
2
3
4
5
6
7
8
useEffect(()=>{
// do sth with refresh
renderView()
},[refresh])

renderView = useCallback(()=>{
// do render data
}, [renderer, data])

上述renderView方法随renderer, data的变化更新函数体实现,而renderView的执行时机只受refresh状态触发(而不需要在同时监听三个状态的副作用中判断是否需要执行renderView)

但是调用useCallback方法时,函数体可能已因依赖项改变而改变

useMemo
在组件重复渲染中缓存结果
其他hooks

关于函数式组件和Hooks

使用Hooks代替class中的生命周期函数,是函数式组件进行逻辑复用、状态管理的方式

旧的思维:“我在这个生命周期要检查props.A和state.B(props和state),如果改变的话就触发xxx副作用”。这种思维在后续修改逻辑的时候很容易漏掉检查项,造成bug。新的思维:“我的组件有xxx这个副作用,这个副作用依赖的数据是props.A和state.B”。从过去的命令式转变成了声明式编程。
———— csr632 《为什么 React 现在要推行函数式组件,用 class 不好吗?》下的回答

install packages:

1
yarn

execuate project
1
yarn [YourScriptInPackageJSON]

yarn 和 npm

  • yarn 速度更快(并行和离线缓存)
  • lock 版本 (与package-lock.json)

yarn.lock

  • 如package-lock.json 开发过程中不应删除重建 应当及时提交
  • yarn 过程根据yarn.lock的依赖树安装及拷贝package 且对与package.json不一致的依赖进行更新
  • 使用 yarn upgrade 根据package.json对依赖进行升级 并更新yarn.lock

yarn 和 yarn install

yarn install is used to install all dependencies for a project. This is most commonly used when you have just checked out code for a project, or when another developer on the project has added a new dependency that you need to pick up.
If you are used to using npm you might be expecting to use —save or —save-dev. These have been replaced by yarn add and yarn add —dev. For more information, see the yarn add documentation.

Running yarn with no command will run yarn install, passing through any provided flags.

查看包版本 yarn info xxpkg
yarn list xxpkg —depth=0

从私有Repository安装

配置.yarnrc.yml 即yarn resouce configure

1
2
3
4
5
6
7
8
npmRegistries:
//qqstone.jfrog.io/artifactory/api/npm/Viewer/:
npmAlwaysAuth: true
npmAuthIdent: c2hpLnFpdUAlbnZpc3RhY28uY236QUtDcDhuSER6YWo3NDNIekNDOVRxOW1Kb0tGVHVaKU5yZ2N4aU5jaWVRQ0hEb2tNR0ROTE43TGkybV5aRkVzSkxkUzdMYkDudA==

npmScopes:
qqsjfrog:
npmRegistryServer: "https://qqstone.jfrog.io/artifactory/api/npm/Viewer/"

TroubleShooting

  1. cannot be loaded because running scripts is disabled on this system.

  1. Error: This tool requires a Node version compatible with >=18.12.0

    Yarn v4要求nodejs >=18.12.0

    切换Yarn version的命令是 yarn set version 3.0.2
    但是在v4且node imcompatible的情况下任何yarn命令均失效

    只有npm i -g yarn 并没有npm i -g yarn@3 因此使用nvm切到node18再复原是较好的办法

  2. YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.

    1
    yarn install --frozen-lockfile false

    see Github issue

  3. Error: Your application tried to access XXX but it isn’t declared in your dependencies; this makes the require call ambiguous and unsound
    疑因使用nvm导致存在冲突的yarn cache路径,导致程序并没有按yarn.lock确定所需的依赖包,该问题在卸载nvm,删除%USERPROFILE%\AppData\Local\Yarn\Berry文件并重装nodejs后解决