lodash之debounce实现

防抖,节流 这都是一些老生常谈的问题,作为一个前端也算是必备的知识了.

通常工具类的函数,我们一般都是直接使用一个库 lodash. 当然了,自己写一个简易版本的也不是不能,不过参考 lodash 源码之后,就发现人家写的考虑的相当周全.

话不多说,最简易版本走一个

1
2
3
4
5
6
7
8
9
10
11
12
13
// v1.0
function debounce(func, wait) {
let timer = undefined;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(func, wait);
};
}
const fn = debounce(() => {
console.log("1s 后执行");
}, 1000);

如你的要求没那么多,这个就已经能满足你的需求了, 但如果你在使用中发现 this 怎么都指向 window or globalThis, 这个时候,就需要稍微升级一下了.

1
2
3
4
5
6
7
8
9
10
11
12
// v2.0
function debounce(func, wait) {
let timer = undefined;
return function () {
const that = this,
args = arguments;
timer && clearTimeout(timer);
timer = setTimeout(() => {
func.apply(that, args);
}, wait);
};
}

这个时候,就能保持 this 不变了.

有时候,我们需要至少执行一次,之后再进行防抖. 那么就需要小小的升级一下. 加上一个参数 配置项(opt.immediately),来判断是否需要立即执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//v2.1 改进一下, 增加立即执行判断
function debounce(func, wait, opt = { immediately: false }) {
let timer = undefined;
return function () {
let that = this,
args = arguments;
if (timer === undefined && opt.immediately) {
func.apply(that, args);
}
timer && clearTimeout(timer);
timer = setTimeout(() => {
func.apply(that, args);
}, wait);
};
}

const run = debounce(
() => {
console.log("OK");
},
1000,
{ immediately: true }
);

lodash 相比,这个防抖函数,还有好多功能未实现, 我把源码整理了一下, 把debounce 以及依赖的函数整理了一下,贴了出来. 如果算上依赖的函数和属性等,将近有 250 行代码了. 核心函数 debounce 差不都 100 多行, 占一半左右吧, 最后还有一个 节流函数, 其实就是调用的 debounce, 改了下参数.

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// debounce配置项如下
// const options = {
// leading: boolean, leading表示在wait时间开始时 立即执行回调
// maxWait: number, 表示func可以延迟执行的最大时间,若超过了maxWait,则必须要执行回调函数func。
// trailing: boolean, trailing表示在wait时间结束后 立即执行回调
// };

var nullTag = "[object Null]",
symbolTag = "[object Symbol]",
undefinedTag = "[object Undefined]";

var reIsBinary = /^0b[01]+$/i;
var reIsOctal = /^0o[0-7]+$/i;
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
var nativeMax = Math.max;

var freeParseInt = parseInt;

var freeGlobal =
typeof global == "object" && global && global.Object === Object && global;

var freeSelf =
typeof self == "object" && self && self.Object === Object && self;

var objectProto = Object.prototype;

var root = freeGlobal || freeSelf || Function("return this")();

var FUNC_ERROR_TEXT = "Expected a function";
var ctxNow = Date && Date.now !== root.Date.now && Date.now;
var nativeObjectToString = objectProto.toString;

var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
var reTrim = /^\s+|\s+$/g;

var now =
ctxNow ||
function () {
return root.Date.now();
};

function objectToString(value) {
return nativeObjectToString.call(value);
}

function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];

try {
value[symToStringTag] = undefined;
var unmasked = true;
} catch (e) {}

var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}

function baseGetTag(value) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return symToStringTag && symToStringTag in Object(value)
? getRawTag(value)
: objectToString(value);
}

function isSymbol(value) {
return (
typeof value == "symbol" ||
(isObjectLike(value) && baseGetTag(value) == symbolTag)
);
}

function toNumber(value) {
if (typeof value == "number") {
return value;
}
if (isSymbol(value)) {
return NAN;
}
if (isObject(value)) {
var other = typeof value.valueOf == "function" ? value.valueOf() : value;
value = isObject(other) ? other + "" : other;
}
if (typeof value != "string") {
return value === 0 ? value : +value;
}
value = value.replace(reTrim, "");
var isBinary = reIsBinary.test(value);
return isBinary || reIsOctal.test(value)
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: reIsBadHex.test(value)
? NAN
: +value;
}

function isObjectLike(value) {
return value != null && typeof value == "object";
}

function isObject(value) {
var type = typeof value;
return value != null && (type == "object" || type == "function");
}

function debounce(func, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;

if (typeof func != "function") {
throw new TypeError(FUNC_ERROR_TEXT);
}

wait = toNumber(wait) || 0;

if (isObject(options)) {
leading = !!options.leading;
maxing = "maxWait" in options;
maxWait = maxing
? nativeMax(toNumber(options.maxWait) || 0, wait)
: maxWait;
trailing = "trailing" in options ? !!options.trailing : trailing;
}

// 执行回调函数, 清理上次的 this 和 参数
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;

lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}

// leading 检查判断 同时开启定时器 ,记录上次执行时间 lastInvokeTime
// 如果 leading = true, 立即执行, 否则 返回结果,这个时候 result 可能还是 undefined.

function leadingEdge(time) {
// Reset any `maxWait` timer.
// 这里并 不一定会执行 回调函数(func), 但是也设置了一个值,
// 就是为了判断 最大间隔 执行, lastInvokeTime 只会再真正调用 回调函数的时候设置
// 但是 如果 防抖函数 一直触发的情况, 实际回调函数(func) 并不会执行, 所以 lastInvokeTime 一定是空的.
// 如果设置maxWait, 没有上次执行时间, 就无法判断回调函数执行间隔时间,
// 其实这里 就是针对首次的情况, 初始化第一次时间未当前时间

// 如果 不设置 maxWait ,其实没有必要添加,

lastInvokeTime = time;

// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}

// 计算下一次执行的间隔时间(剩余时间)
// 这里就是重启定时器时候,需要重新计算一下,
// 针对与 wait maxWait 不同场景,执行下一次定时器需要的时间

// wait - (当前时间 - debounced 上一次执行时间)
// maxWait - (当前时间 - func 上一次执行时间)

// 然后取最小

function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
timeWaiting = wait - timeSinceLastCall;

return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
/**
* 这里对与时间的判断 是否应该调用 回调函数
* 这里的判断很巧妙
*/
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;

// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.

// 1. 函数还未被调用过
// 2. 函数(debounce)距离上次 调用 间隔时间 >= wait 间隔时间
// 3. 函数(debounce)距离上次 调用 间隔时间 < 0 ??这个不知道为啥要这样判断, 按理来说, 当前时间 time 一定会大于上次执行时间
// 4. 设置了最大间隔时间, 而且 回调函数间隔执行时间 大于 最大间隔时间
return (
lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
);
}

// 进入到这个函数(定时器的回调函数)只有两个场景
// 1. 没有定时器
// 2. 定时器被清了,那不是相当于还是没有定时器
function timerExpired() {
var time = now();
// 再根据时间判断一下,能不能执行,
if (shouldInvoke(time)) {
return trailingEdge(time);
}

// 不能执行,重启定时器,不过时间需要重新计算一下
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}

// trailing 的逻辑处理
// 其实这里的英文注释系的很明白了, func 至少被节流过一次,才会触发, 也就是 wait之后,触发一次
// 节流过的话, 会把 参数保存起来 lastArgs

function trailingEdge(time) {
timerId = undefined;

// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}

function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}

function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
/**
* 最终返回的经过防抖动处理的函数
*
* debounced 函数的执行,无非是为了判断 func 是否能执行,因此按照这个分类,可能存在的场景:
*
* - func 可以执行
* 1. 第一次执行 (leading = true)
* 2. maxWait 存在 && 超过了 maxWait 时间
* 3. 定时器到达了wait, debounced 并未触发
*
* - func 不可以执行
* 1. 时间 wait 不满足
* 1.1. 定时器不存在, 初始化一个即可
* 1.2. 定时器已经存在 ,重置定时器, 延迟wait执行.
*
* 2. 不存在 wait 满足这种场景, 如果 wait 满足, func 必定执行
*/

/**
* 如果按照 debounced 的逻辑来判断
*
* - 满足 >wait || >maxWait || 第一次. 前两种立即执行, 第三种可能会立即执行,可能延迟执行,
*
* 1. 没有定时器
* 无非两种情况 立即执行, 定时延期执行
*
* 2. 有定时器, (说明func 已经被加入到执行的 "队列里面", 并非第一次,)
*
* 2.1 如果设置了最大间隔 maxWait,这里结合上一层的条件(> wait , > maxWait). 立即执行,并添加一个定时器,
*
* 2.2 如果没有,无需处理,等定时器执行即可
*
* - 不满足, func 一定不会立即执行
* 1. 有定时器, 已经触发了防抖, 无需处理,到了时间肯定会执行
* 2. 没有定时器, 创建即可
*
*
*/
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);

lastArgs = arguments;
lastThis = this;
lastCallTime = time;

if (isInvoking) {
// 如果可执行,( 通过时间判断, 满足执行条件)

// 如果没有 定时器, 则检查 leading,
// 可能是第一次 (或者已经执行过 func, timerId 被 清理了 , 也可以认为是新的开始,第一次)
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}

// 如果 有定时器 ( 说明已经触发过了 debounced )

// 如果 设置了最大 间隔时间,
if (maxing) {
// Handle invocations in a tight loop.
// 先把定时器 clear 掉, 再添加一个, 并立即执行 func
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}

function throttle(func, wait, options) {
var leading = true,
trailing = true;

if (typeof func != "function") {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = "leading" in options ? !!options.leading : leading;
trailing = "trailing" in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
leading: leading,
maxWait: wait,
trailing: trailing,
});
}

这其中有一半 依赖的代码,是考虑并处理了 N 多的场景,用来提高代码健壮性.这就是一个通用库的开发与业务开发的区别之一吧。

例如: 对 wait 的类型判断和转换, 获取当前时间的 now 方法(Date 的 now 是可修改的,也就是说,如果有人修改了 now,如果你直接使用 Date.now() 就无法返回正确的时间). 全局对象的获取 , 类型判断, 转换等等.

我研究了一下,感觉他写的不错的地方就是针对于时间的判断与控制,

  1. 通过间隔时间判断, 避免频繁调用和清理定时器,而是放在了 每个定时器执行的时候,再去判断,这个大大的减少了 clearTimeout 的执行.

  2. 增加了函数结果存储, 如果未达到执行时间,可以返回上次的执行结果,

  3. 最大间隔时间选项, 这不就是把 防抖 转成 节流了么???

  4. leading trailing 配置项,可以灵活设置执行回调的时机

看完大佬写的,再看看自己实现的简直天差地别。

后话:虽然大佬写的确实牛,但是在自己项目中,可能还需要实际考量一下,例如:在自己的项目,对于参数类型是可以控制把握的 或者性能要求没有那么高,如果只是为了 使用其中 1,2 个函数,引用一个 lodash,着实没有太大必要, 当然现在有 lodasg-es 版本,能够 treeShaking 按需引用. 即使 treeShaking 之后(我贴出来的代码是用 rollup treeShaking 之后的),依然有 250 多行。比起 10 多行 简易版本仍然大了不少。

当然有轮子的情况下,为啥不用轮子呢?毕竟人家写的是经过时间考验的

努力学习,共勉!!!

作者

Fat Dong

发布于

2023-12-28

更新于

2023-12-28

许可协议

七牛云文件hash计算浏览器环境实现

最近要实现一个文件唯一性判断的功能,在网上找了一大圈,基本上都是使用一些摘要算法,根据文件内容生成一串字符串。

想起来之前用过七牛云,当时也没太在意他的计算方式,于是就去研究一下他是怎么实现的,没想到人家已经把计算方式开源,遂去 Github 上看了一下,虽然说提供了 JS 版本的实现,但是只有 NodeJS 版本,没有浏览器版本,于是乎参照着 NodeJS 版本,实现了一个浏览器环境的版本.

以下是 readme, 我直接复制过来了,具体地址:https://github.com/qiniu/qetag/tree/master

qetag 是一个计算文件在七牛云存储上的 hash 值(也是文件下载时的 etag 值)的实用程序。
七牛的 hash/etag 算法是公开的。算法大体如下:
如果你能够确认文件 <= 4M,那么 hash = UrlsafeBase64([0x16, sha1(FileContent)])。也就是,文件的内容的 sha1 值(20 个字节),前面加一个 byte(值为 0x16),构成 21 字节的二进制数据,然后对这 21 字节的数据做 urlsafe 的 base64 编码。
如果文件 > 4M,则 hash = UrlsafeBase64([0x96, sha1([sha1(Block1), sha1(Block2), …])]),其中 Block 是把文件内容切分为 4M 为单位的一个个块,也就是 BlockI = FileContent[I*4M:(I+1)*4M]。
为何需要公开 hash/etag 算法?这个和 “消重” 问题有关,详细见:
https://developer.qiniu.com/kodo/kb/1365/how-to-avoid-the-users-to-upload-files-with-the-same-key > http://segmentfault.com/q/1010000000315810
为何在 sha1 值前面加一个 byte 的标记位(0x16 或 0x96)?
0x16 = 22,而 2^22 = 4M。所以前面的 0x16 其实是文件按 4M 分块的意思。
0x96 = 0x80 | 0x16。其中的 0x80 表示这个文件是大文件(有多个分块),hash 值也经过了 2 重的 sha1 计算。

有了前辈的实现过程,再配合 ChatGPT,写起来就比较顺利了。 其实这里还有一个点,就是对于大文件的分片,尤其是前端上传大文件的时候,基本上都是分片之后,一段一段的上传。

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
85
86
const cryptoName = "SHA-1";
const bit = 22;
const baseSize = 1 << bit; // 4MB 每个块大小

// 计算sha1,
function sha1(data, hex) {
return new Promise((reslove, reject) => {
crypto.subtle
.digest(cryptoName, data)
.then((buf) => reslove(hex ? arrayBufferToHex(buf) : buf))
.catch(reject);
});
}

function countBlock(fileSize) {
return (fileSize + (baseSize - 1)) >> bit;
}

// 计算文件hash
async function fileHash(file) {
const fileSize = file.size;
const chunkCount = countBlock(fileSize);

let allBuff;
let prefix;
// 大于 4MB 的文件
if (chunkCount > 1) {
// const sha1ListBuff
prefix = new Uint8Array([0x96]).buffer;
let start = 0;
const sah1BuffList = [];
while (start < fileSize) {
const bf = file.slice(start, start + baseSize);
const currSha1Buff = await sha1(await bf.arrayBuffer());
start += baseSize;
sah1BuffList.push(currSha1Buff);
}
allBuff = await sha1(mergeToU8Arr(...sah1BuffList));
} else {
prefix = new Uint8Array([0x16]).buffer;
allBuff = await sha1(await file.arrayBuffer());
}

allBuff = mergeToU8Arr(prefix, allBuff);
// base64
console.log(base64URLEncode(allBuff));
// hex
console.log(arrayBufferToHex(allBuff));
}

function mergeToU8Arr(...buf) {
const l = buf.length;
if (l === 1) {
return buf;
}
let totalLen = 0;
for (let i = 0; i < l; i++) {
totalLen += buf[i].byteLength;
}
const tmpBuf = new ArrayBuffer(totalLen);
const u8Buf = new Uint8Array(tmpBuf);
let offset = 0;
for (const bf of buf) {
const curBuf = new Uint8Array(bf);
u8Buf.set(curBuf, offset);
offset += curBuf.length;
}
return u8Buf;
}

function base64URLEncode(input) {
// 使用 btoa 进行 Base64 编码
let base64Encoded = btoa(
String.fromCharCode.apply(null, new Uint8Array(input))
);
// 替换标准 Base64 字符集中的一些字符,以获得 URL 安全的编码
return base64Encoded
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
// 将 ArrayBuffer 转换为十六进制字符串
function arrayBufferToHex(buffer) {
const hashArray = Array.from(new Uint8Array(buffer));
return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
作者

Fat Dong

发布于

2023-12-04

更新于

2023-12-04

许可协议

FLIP 动画入门了解

最近看到一个词 “FLIP 动画”,我还以为是什么新奇的技术,点击去看了一眼,原来是 CSS3 的相关技术的应用。

具体解释,我就从其他地方 Copy 的一段:

CSS FLIP 动画是一种使用 CSS 过渡和变换属性来创建流畅动画效果的技术。FLIP 是 “First, Last, Invert, Play” 的缩写,它描述了一种优化动画性能的方法。

FLIP 动画的基本原理是在动画开始和结束时,通过获取元素的初始状态和最终状态的位置、大小和其他属性,然后通过 CSS 变换和过渡来实现平滑的动画效果。

具体步骤如下:

  • First(首次):获取元素的初始状态(位置、大小、样式等)。
  • Last(最后):获取元素的最终状态(位置、大小、样式等)。
  • Invert(反转):通过 CSS 变换将元素的最终状态反转到初始状态。
  • Play(播放):应用 CSS 过渡,将元素从初始状态平滑地过渡到最终状态。

通过使用 FLIP 技术,可以减少动画期间的重排和重绘操作,提高动画的性能和流畅度。这种技术特别适用于需要在页面布局改变时进行动画的情况,如元素的位置变化、大小变化或样式变化等。

请注意,FLIP 动画需要一些 JavaScript 代码来计算元素的初始状态和最终状态,并应用相应的 CSS 类和样式

简而言之,言而简之!就是利用 CSS3 的动画特性,通常只要把起始、结束两个状态告诉浏览器,渲染引擎(浏览器)会自动控制动画序列,从而实现动画,无需额外 JS

FLIP 动画 为了得到起始、结束的状态需要 JS 配合,初始状态 容易得到,但是为了得到 结束状态 ,就得让元素放置到 结束状态 的位置,正常情况下,直接放置是不会有任何动画,而且元素直接到了 结束状态

怎么让元素其实已经放置到 结束状态 ,但是看起来还是初始状态呢?这就是其中一个关键点,其实实现起来也很简答,利用 CSS3 位移属性,计算两种状态的 偏移距离, 然后利用 transform: translate(X px, Y px) 在移动回去, 让视觉上看起来实际没有移动。(这其实就是所谓的 “初始状态”)

接下来就是让动画动起来,就是要设置 结束动画属性,当前位置不就是结束位置么?压根不用动,直接把 位移值设置为 0 - transform: translate(0px, 0px) 即可. 开始-结束 都有了,动画不就自动开始了。

这个时候你如过直接设置,可能会发现毫无效果,为啥呢??你这样直接设置,肯定会在同一帧内渲染,根本看不到效果(具体为啥,去了解 JS 阻塞渲染 相关知识),我们可以利用一些异步方式,或者强制渲染一次,然后在第二次渲染的时候 设置结束属性,例如: requestAnimationFrame, setTimeout 等都可以。

具体 demo 如下:

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flip 动画实现</title>
<style>
body {
font-family: -apple-system, "Helvetica Neue", "Arial", "PingFang SC",
"Hiragino Sans GB", "STHeiti", "Microsoft YaHei",
"Microsoft JhengHei", "Source Han Sans SC", "Noto Sans CJK SC",
"Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC",
"Noto Sans CJK TC", "WenQuanYi Micro Hei", SimSun, sans-serif;
}

.container {
width: 320px;
height: 320px;
background-color: blanchedalmond;
}

.item {
width: 80px;
height: 80px;
line-height: 80px;
text-align: center;
font-size: 30px;
}

.item[data-key="1"],
.item[data-key="6"],
.item[data-key="11"],
.item[data-key="14"] {
background-color: tomato;
color: #fff;
}

.item[data-key="2"],
.item[data-key="7"],
.item[data-key="16"] {
background-color: #999933;
color: #fff;
}

.item[data-key="3"],
.item[data-key="10"],
.item[data-key="12"] {
background-color: #ff0033;
color: #fff;
}

.item[data-key="4"],
.item[data-key="9"],
.item[data-key="15"] {
background-color: #003366;
color: #fff;
}

.item[data-key="8"],
.item[data-key="13"] {
background-color: #0099cc;
color: #fff;
}

.item[data-key="8"],
.item[data-key="15"],
.item[data-key="5"] {
background-color: #996600;
color: #fff;
}

/* grid 布局 */
.grid-wrap {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
</style>
</head>

<body>
<h1>
最新看来一个新东西(对我而言是新东西)Flip
动画,秉着好奇的心理,打开看了一眼,原来是这样的!
</h1>
<p></p>
<hr />
<button id="change1">变化</button>
<div class="container normal-wrap">
<div class="item" data-key="1">1</div>
<div class="item" data-key="2">2</div>
<div class="item" data-key="3">3</div>
<div class="item" data-key="4">4</div>
</div>

<hr />
<button id="change2">变化</button>
<div class="container grid-wrap">
<div class="item" data-key="1">1</div>
<div class="item" data-key="2">2</div>
<div class="item" data-key="3">3</div>
<div class="item" data-key="4">4</div>
<div class="item" data-key="5">5</div>
<div class="item" data-key="6">6</div>
<div class="item" data-key="7">7</div>
<div class="item" data-key="8">8</div>
<div class="item" data-key="9">9</div>
<div class="item" data-key="10">10</div>
<div class="item" data-key="11">11</div>
<div class="item" data-key="12">12</div>
<div class="item" data-key="13">13</div>
<div class="item" data-key="14">14</div>
<div class="item" data-key="15">15</div>
<div class="item" data-key="16">16</div>
</div>
</body>
<script>
// const item = document.querySelector('.item[data-key="1"]')
const wrap = document.querySelector(".normal-wrap");
const btn = document.getElementById("change1");

const itemsCache = new Map();

btn.addEventListener("click", () => {
raffle();
});

function delay(d = 1000) {
const n = Date.now();
while (Date.now() - n < d) {}
}

function calcPos(dom) {
return dom.getBoundingClientRect();
}

function item(wrap, index) {
if (!itemsCache[index]) {
itemsCache[index] = wrap.querySelector(`.item[data-key="${index}"]`);
}
return itemsCache[index];
}

function itemV2(dom) {
return (k) => {
return dom.querySelector(`.item[data-key="${k}"]`);
};
}

let index = [1, 2, 3, 4];
function raffle() {
const posBegin = {};
const postLast = {};

// 初始位置
index.forEach((k) => {
posBegin[k] = calcPos(item(wrap, k));
});

// 移动元素
index = index.sort(() => Math.random() - 0.5);
index.forEach((k) => {
wrap.append(item(wrap, k));
});

// Invert 计算位移
const diff = {};
index.forEach((k) => {
// 计算移动后位置
postLast[k] = calcPos(item(wrap, k));

// 获取偏移量
diff[k] = {
x: postLast[k].left - posBegin[k].left,
y: postLast[k].top - posBegin[k].top,
};
});

Object.keys(diff).forEach((k) => {
// 设置偏移样式
item(wrap, k).style.transform = `translate(${-diff[k].x}px, ${-diff[k]
.y}px) scale(0.9)`;
item(wrap, k).style.transition = "none";
});

window.requestAnimationFrame(() => {
index.forEach((k) => {
item(wrap, k).style.transform = `translate(0,0)`;
item(wrap, k).style.transition = "transform .5s";
});
});
}

// v2 grid 布局
let index2 = Array.from({ length: 16 })
.fill(0)
.map((v, i) => i + 1);
const wrap2 = document.querySelector(".grid-wrap");
const btn2 = document.querySelector("#change2");

btn2.addEventListener("click", () => {
raffle2();
});

function raffle2() {
const _item = itemV2(wrap2);
const posBegin = {};
const postLast = {};

// 初始位置
index2.forEach((k) => {
posBegin[k] = calcPos(_item(k));
});

// 移动元素
index2 = index2.sort(() => Math.random() - 0.5);
index2.forEach((k) => {
wrap2.append(_item(k));
});

// Invert 计算位移
const diff = {};
index2.forEach((k) => {
// 计算移动后位置
postLast[k] = calcPos(_item(k));

// 获取偏移量
diff[k] = {
x: postLast[k].left - posBegin[k].left,
y: postLast[k].top - posBegin[k].top,
};
});

Object.keys(diff).forEach((k) => {
// 设置偏移样式
_item(k).style.transform = `translate(${-diff[k].x}px, ${-diff[k]
.y}px) scale(0.9)`;
_item(k).style.transition = "none";
});

window.requestAnimationFrame(() => {
index2.forEach((k) => {
_item(k).style.transform = `translate(0,0)`;
_item(k).style.transition = "transform .5s";
});
});

console.log("a");
}
</script>
</html>

这是一个简易 demo,没有通用性,只是为了演示具体原理。好多大佬封装了一些通用库来实现这个功能,可去 Github 上找找。

作者

Fat Dong

发布于

2023-10-12

更新于

2023-10-12

许可协议

TypesSript 模块化浅谈

最近在学习 TS,一直在看官方的 handbook, 看到模块化这一章节,记录一下学习心得。其实大部分内容还是把文档的翻译直接拿过来,所以说最好的教程还是看官方的文档

什么是模块化

模块化是一种将代码按照一定规则组织成独立、可复用的单元的方式。模块化可以将一个复杂的系统分解为多个独立的模块,每个模块只关注自己的功能,与其他模块之间的耦合度低,从而提高代码的可维护性、复用性和扩展性。

其实重点就是:低耦合,高内聚, 高复用,高维护 , 通过将一些复杂的功能拆解,化整为零到多个模块(具体体现可能是分解成多个文件),尤其是最新的开发模式,借助于一些现代化的构建工具,可以很大程度提高项目的维护性。

在远古时期的浏览器端开发,大家可能都是在一个或多个文件中写代码,然后在引入到一个页面中。这个时期基本上谈不上模块化,在多人协作的时非常难。早期大家为了解决冲突问题,常见方式就是使用“命名空间模式(Namespace Pattern)”,通过创建全局对象,并将相关函数、变量添加到该对象中来实现模块化。

随着技术逐渐发展,出现了多种模块化的方案,例如: CommonJSAMDCMDUMDES6 Module 等。由于早期没有明确的规范,导致出现了多种模块化的方案, 关于每种方式的具体实现可自行查找资料了解。

JS 的模块化标准的推进比较缓慢,2015 年 6 月, ECMAScript6 标准正式发布,其中 ES 模块化规范的提出目标是整合 CommonJS、AMD 等已有模块方案,在语言标准层面实现模块化,成为浏览器和服务器通用的模块解决方案。到 2020 年,大多数 Web 浏览器和 JavaScript 运行时都得到了广泛支持。

TS 中的模块

在开始之前,了解 TypeScript 将模块视为什么非常重要。JavaScript 规范声明任何没有 import 声明、export 或顶级的 JavaScript 文件 await 都应被视为脚本而不是模块。

TypesScript 不仅支持 ES6 Module 也支持 CommonJS.

TS 中比较特别一点就是: 文件中没有 exportimport,又想将当前文件视为模块,可以添加 一行 export {} 那么当前文件不会导出任何内容,但会被认为是一个模块

还有一点就是,TS 还支持导出 类型声明. 可参照示下面例。

  • 通用语法示例(ES6 Module):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// a.ts
export default function helloWorld() {
console.log("Hello, world!");
}

// b.ts
import helloWorld from "./a.ts";

// 导出多个
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;

import { pi, phi, absolute } from "./a.ts";

// 重命名
import { pi as PI } from "./a.ts";

// 混合导出
import helloWorld, { pi } from "./a.ts";

// 使用命名空间导出
import * as A from "./a.ts";
  • TS 中特定(特殊)的语法,支持导出 Type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// @filename: a.ts
export type A = { name: string };

export interface IA {
name: any;
}

// b.ts
import { A, IA } from "./a.ts";

// import type TS 独有
import type { A, IA } from "a.ts";

// 内联方式导入
// TypeScript 4.5 还允许单个导入添加前缀,type以指示导入的引用是一种类型:
import { type A, type IA } from "a.ts";
1
2
3
4
5
6
//  当然也可以使用commonJS的语法

import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");

// 有关于commonJS的更多语法,可参考 Node 相关教程

关于 CommonJS 和 ES modules 的 互操作性,由于两者在导入、导出规则的区别,有一个配置项 esModuleInterop

TS 模块解析 配置项

主要是指 import 等语句引用的文件的时候,根据 import语句中的字符串,来确定是那个文件的过程
有两种策略: 经典模式 (Classic), Node 经典模式(Node Classic),

能够影响解析策略的配置项有好几个:moduleResolution, baseUrl, paths, rootDirs.

其实相对文件地址还好,主要是一些安装的第三方的库。

TS 模块输出 配置项

共有 2 个配置项,影响编译生成 JS,

  • target
    决定了那些 JS 功能会被降级(能够兼容以前的 JS 环境,保持正常运行),那些功能保持不变.
    这个很容易理解了,由于不同的浏览器厂商对规范实现的程度不一样(不同版本支持的 JS 版本不一样),如果当前浏览器版本较低,不支持一个新功能语法等,通过设置 target 可以把这类功能降低成支持的语法。
    例如:我们把 target 设置成 ES5, 以下代码就是被转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 之前
    class Animals {
    constructor() {}
    }

    // 之后
    ("use strict");
    var Animals = /** @class */ (function () {
    function Animals() {}
    return Animals;
    })();

    不过目前大部分浏览器基本上都支持了 ES6

  • module
    决定了模块之间用什么代码交互,听起来不太容易理解,简单的说,就是最终生成的代码是什么模块风格的。
    你可以自己尝试去修改 module 的值,然后生成 JS 文件,去对比区别。这里就不一一罗列了。

综上所述, target 和 module 都会影响最后生成 JS 代码的内容。

TS 的命名空间

这里说的命名空间,和之前讲的 JS 的命名空间不是同一个东西。虽然使用起来有一些相似之处。

TS 有自己的模块格式,成为命名空间,比 ES 的标准要早出现,目的也是:方便组织代码,跟踪类型,避免命名之间的冲突。类似与 JS 一样,把对象包装到一个命名空间,而不是放到全局中

这是官方的 Demo:

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
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidValidationator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(
`"${s}" - ${
validators[name].isAcceptable(s) ? "matches" : "does not match"
} ${name}`
);
}
}

  • 注 1: 什么是互操作, 英文原文为:CommonJS and ES Modules interop, 直译过来就是 CommonJS 和 ES Modules 互操作。

    这个翻译很不好理解。

    其实所谓的互操作,其实简答的讲,就是可以在 CommonJS 模块导入 ES 模块, 或者 ES 模块导入 CommonJS, 他们之间相互导入的操作。

    通常 ES Modules 是 使用 import export 语法导入导出, CommonJS 使用 module.exports 语法导出,ES Modules 有 default 这个概念, 但是 CommonJS 没有,它是直接导出了一个“命名空间”。为了解决他们呢之间的相互导入的时候造成的不统一:
    TS 做了如下处理:

    • 对于 export default 的变量,TS 会将其放在 module.exports 的 default 属性上
    • 对于 export 的变量,TS 会将其放在 module.exports 对应变量名的属性上
    • 额外给 module.exports 增加一个 __esModule: true 的属性,用来告诉编译器,这本来是一个 esm 模块

    TS 配置文件中有一项 esModuleInterop 配置,设置成 true ,TS 的编译器会自动兼容两个模式的导入导出。也就是支持 export 方式导出。

作者

Fat Dong

发布于

2023-10-10

更新于

2023-10-11

许可协议

leetcode-640-表达式解析-解题思路

这个提其实不太难,第一眼却没做出来,看了题解之后才思路清晰了, 有思路了很容易做出来
所有的表达式,经过计算最总都会变成, n\*x + val = 0 , n val 都是数值,我们要做的就是把 n ,val 计算出来,

  • 考虑等号(“=”)的问题,和做算术题一样,把右边挪到左边, 相当于就是减去左边, 也就是 减一下(-)
    设置一个标记,把之后的值都取个反, 对于代码的话, 不就是 a = -a 么?
  • 符号判断, 加号, 减号 ,等于号 , 加号 不用管 ,减号 取个反, 还有一个考虑点就是 = 这个标记之后的情况, 结合判断
    例如: 如果是 = 右边的, 本来是 - , 现在就变成了 +
  • 解析 x 之前的数字,这个就是一个数字字符串 转数字的算法
    可能是 100x, 5x, 18x 等等这种情况, 其实就是正常解析数字字符串一样, 每次 数值*10 + 当前值 ,直到 不是数字 停止, 保存并累加
  • 正常数值就没啥考虑的了,直接累加即可.

最后根据结果,进行计算, 需要判断表达式是否有效的情况.

如果 n*xn0 的时候,x 对表达式无影响, 这时候,判断 val 即可:

  • 如果val = 00 = 0, 有 无数解,任意值都可以,
  • 如果是 val != 0 ,那么就是无解.

剩下情况就是正常解, 返回 -val / n 即可. 下面是官方的 golang 题解。

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
func solveEquation(equation string) string {
factor, val := 0, 0
i, n, sign := 0, len(equation), 1 // 等式左边默认系数为正
for i < n {
if equation[i] == '=' {
sign = -1 // 等式右边默认系数为负
i++
continue
}

s := sign
if equation[i] == '+' { // 去掉前面的符号
i++
} else if equation[i] == '-' {
s = -s
i++
}

num, valid := 0, false
for i < n && unicode.IsDigit(rune(equation[i])) {
valid = true
num = num*10 + int(equation[i]-'0')
i++
}

if i < n && equation[i] == 'x' { // 变量
if valid {
s *= num
}
factor += s
i++
} else { // 数值
val += s * num
}
}

if factor == 0 {
if val == 0 {
return "Infinite solutions"
}
return "No solution"
}
return "x=" + strconv.Itoa(-val/factor)
}

leetcode-640-表达式解析-解题思路

https://www.md7.top/2023/06/08/力扣640-leetcode-640解题/

作者

Fat Dong

发布于

2023-06-08

更新于

2023-06-08

许可协议

flex布局应用案例-左对齐

最近在做一个类似于京东商城首页 banner 图下面分类图标展示效果, 一行四个,分两行展示. 通常情况下如果数量固定,本来是很简单的flex布局即可搞定。却高的很麻烦。

jd 使用了float属性 来布局。

实际UI效果要求(不上图了,涉及公司隐私)

  1. 每行最多四个,四个元素,中间距离一样,两端对齐,就是 justify-content:space-between 效果,
  2. 数量不固定, 可能是 1 - 12个。例如:只有两个的时候, 也要保持两端对齐的情况下,前两个元素同样的位置

使用flex布局, 不考虑两端对齐

这个是最常见的情况,使用 flex布局, 子元素固定宽度之后,会自动平铺换行。但是这种不一定会符合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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* 公共css */
* {
margin: 0;
padding: 0;
}
ul,
li {
list-style: none;
}

.page {
padding: 0 20px;
}

.page h5{
margin-top: 10px;
margin-bottom: 10px;
}

.wrap {
background-color: #f3f4f8;
border-radius: 7px;
margin-bottom: 18px;
padding-bottom: 20px;
}

.icon {
width: 27px;
height: 27px;
object-fit: cover;
line-height: 0;
font-size: 0;
}
.name {
font-size: 12px;
font-weight: 500;
margin-top: 10px;
}

.item {
text-align: center;
margin-top: 20px;
}
1
2
3
4
5
6
7
8
9
/* 第一种方式 v1 */
.list {
display: flex;
flex-wrap: wrap;
}

.list .item {
flex: 0 0 25%;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="page">
<div class="wrap">
<ul class="list">
<li class="item">
<img
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
class="icon">
<p class="name">
测试
</p>
</li>
<!-- 省略其余7个 li -->
</ul>
</div>
</div>

效果图:

问题点:容器两侧保留空间太大,与UI不符合,

使用flex ,两端对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 第二种方式 */
.list2 {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}

.list2 .item {
flex: 0 0 25%;
}
.list2 .item-inner{
width: 30px;
margin: 0 auto;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="wrap">
<ul class="list2">
<li class="item">
<div class="item-inner">
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" class="icon">
<p class="name">
测试
</p>
</div>
</li>
<!-- 省略其余7个 li -->
</ul>
</div>

方式2中,多了一层div,效果和1 一样,但是 方式2 有一个弊端: 如果数量无法保持 4n 的话,最后一行就无法左对齐。也没有实现两端对齐。

flex 动态计算 marginleft

这个是看完 软老师 博客学习,一般很少使用calc这个属性,相比较性能差点。而且做的大部分功能偏后台的,不太用得着。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.list3 {
display: flex;
flex-wrap: wrap;
padding: 0 22px;
}

.list3 .item {
flex: 0 0 30px;
}
.list3 .item:nth-child(4n+1) {
margin-left: 0;
}
.list3 .item:not(:nth-child(4n+1)) {
margin-left: calc((100% - 120px) / 3);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="wrap">
<ul class="list3">
<li class="item">
<div class="item-inner">
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" class="icon">
<p class="name">
测试
</p>
</div>
</li>
<!-- 省略其余7个 li -->
</ul>
</div>

第三种方式,配合 nth-child 选择器,不用考虑数量问题,任意都可以,保持两端左对齐,但是需要根据实际需求去调整 css 属性列数 即可。

对于容器两边距离固定, 又要保持子元素两端对齐的方式,也是一种解决方式。

如果你有更好的,欢迎交流

作者

Fat Dong

发布于

2023-04-06

更新于

2023-04-06

许可协议

MacOs 关闭IPV6

MacOS 禁用IPV6

打开系统设置 -> 网络设置,如果看到以下设置,说明IPV6已经关闭
网络设置

如果没有显示关闭,下拉菜单中也没有关闭选项,就需要执行如下命令:

1
2
3
4
5
6
7
sudo networksetup -listallnetworkservices // 列出网络接口、名称

# 找到当前自己的使用 wifi 或者 有线连接 的网口

# 继续执行以下命令
sudo networksetup -setv6off Wi-Fi-New // Wi-Fi-New 换成你自己的

正常情况下,执行之后就会显示上图的结果,然后就可以在设置中修改了。

作者

Fat Dong

发布于

2023-02-07

更新于

2023-02-07

许可协议

TypeScript的内置类型

使用TS开发项目有好久了,经常使用到TS一些内置的的工具类型,非常实用。对于一些不是经常使用的类型,每次得看文档,今天就记录一下,加深记忆
参考地址: https://www.typescriptlang.org/docs/handbook/utility-types.html

类型后面的Vx.x 表示是那个版本发布此类型

Awaited V4.5

这个工具类型比较有用, 可以返回 异步函数(async)/ promise的.then 方法的结果值类型,尤其是可以递归解包。

1
2
3
4
5
6
7
8
9
10
11
type A = Awaited<Promise<string>>;

type A = string

type B = Awaited<Promise<Promise<number>>>;

type B = number

type C = Awaited<boolean | Promise<number>>;

type C = number | boolean

如果不考虑递归,可以利用 infer 实现一个简单的 Awaited

1
2
3
4
type MyAwaited<T> = T extends Promise<infer R> ? R : T;

type A = MyAwaited<Promise<string>>;
// type A = string

Partial V2.1

将所有属性变为 可选 的,这个比较实用,尤其在复用一些 type/interface 的时候,

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
name: string
age: number
address: string
}

type Person2 = Partial<Person>
// 相当于
interface Person2 {
name?: string
age?: number
address?: string
}

Required V2.8

这个和 Partial 相反,是把所有属性变为必选

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
name?: string
age?: number
address?: string
}

type Person2 = Required<Person>
// 相当于
interface Person2 {
name: string
age: number
address: string
}

Readonly V2.1

将某个类型里的属性全部变为只读, 这个貌似日常开发不怎用,可以参考 Object.freeze 方法,

1
2
// Object.freeze
freeze<T>(o: T): Readonly<T>;

Record<Keys, Type> V2.1

这个也比较实用,能构造一个具有指定类型(Type)的一组属性(Keys)的类型,尤其是将一个类型的属性映射到另一个类型的属性是,很方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface EmployeeType {
id: number
fullname: string
role: string
}

let employees: Record<number, EmployeeType> = {
0: { id: 1, fullname: "John Doe", role: "Designer" },
1: { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
2: { id: 3, fullname: "Sara Duckson", role: "Developer" },
}

// 或者创建一个类型
type A = Record<string,number>
// 相当于
type A = {
[k: string]: number;
}

Pick<Type, Keys> V2.1

选择某个类型中 指定的一组属性(keys),返回一个相当于这个类型(type)的子类型,平时用的也比较多吧

1
2
3
4
5
6
7
8
9
10
11
12
interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
title: "Clean room",
completed: false,
};

Omit<Type, Keys> V3.5

从某个类型中,移除(忽略)某些属性,返回剩余的属性组成新的类型,行为和Pick 刚好相反。也经常使用

1
2
3
4
5
6
7
8
9
10
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 查看源码,就是 Pick ,Exclude 配合使用

interface Person {
name: string
age: number
addr: string
}
type PurPersion1 = Omit<Person,"addr"> // {name, age}
type PurPersion2 = Omit<Person,"addr"|"age"> // {name}

Exclude<UnionType, ExcludedMembers> V2.8

排除某个集合中的类型,

1
2
3
4
5
6
7
type UnionList = string | number | (() => void) | Array<any>

// 排除了函数类型
type ExFuncList = Exclude<UnionList, Function> // string | number | Array<any>

// 排除了 "a"
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"

Extract<Type, Union> V2.8

Type 中 提取 满足(存在)Union中的类型取出,去掉不存在的,就像是取 两个类型的交集 平时业务开发很少用到,一些工具类库,框架用的比较多

1
2
3
4
5
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// type T0 = "a"

type T1 = Extract<string | number | (() => void), Function>;
// type T1 = () => void

NonNullable V2.8

排除 null undefined 属性

1
2
3
4
5
6
type T0 = NonNullable<string | number | undefined>;

type T0 = string | number
type T1 = NonNullable<string[] | null | undefined>;

type T1 = string[]

Parameters V3.1

这个类型可以用来提取函数中的参数,返回一个元组,这里贴出了官方的例子,

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
declare function f1(arg: { a: number; b: string }): void;

type T0 = Parameters<() => string>;

type T0 = []
type T1 = Parameters<(s: string) => void>;

type T1 = [s: string]
type T2 = Parameters<<T>(arg: T) => T>;

type T2 = [arg: unknown]
type T3 = Parameters<typeof f1>;

type T3 = [arg: {
a: number;
b: string;
}]

type T4 = Parameters<any>;

type T4 = unknown[]
type T5 = Parameters<never>;

type T5 = never
type T6 = Parameters<string>;
// Error Type 'string' does not satisfy the constraint '(...args: any) => any'.

type T6 = never
type T7 = Parameters<Function>;
// Error Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Error Type 'Function' provides no match for the signature '(...args: any): any'.

type T7 = never

ConstructorParameters

用来提取构造函数的参数,返回一个元组类型, 查看源码可以看到,有两个点需要注意:

  • abstract关键字修饰的函数叫抽象方法,而抽象方法只能出现在抽象类中
  • new (…args: any) => any,这就是对构造函数的定义

所以,这个工具类型只是针对抽象类发挥效果,never和any我们不需要关心。 平时基本很少用

1
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

ReturnType V2.8

获取函数返回参数类型

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
declare function f1(): { a: number; b: string };

type T0 = ReturnType<() => string>;
type T0 = string

type T1 = ReturnType<(s: string) => void>;
type T1 = void

type T2 = ReturnType<<T>() => T>;
type T2 = unknown

type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
type T3 = number[]

type T4 = ReturnType<typeof f1>;
type T4 = {
a: number;
b: string;
}
type T5 = ReturnType<any>;

type T5 = any

type T6 = ReturnType<never>;
type T6 = never

type T7 = ReturnType<string>;
// Error Type 'string' does not satisfy the constraint '(...args: any) => any'. l
type T7 = any


type T8 = ReturnType<Function>;
// Error Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Error Type 'Function' provides no match for the signature '(...args: any): any'.
type T8 = any

InstanceType V2.8

返回基于构造函数返回值的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class C {
x = 0;
y = 0;
}

type T0 = InstanceType<typeof C>;
type T0 = C

type T1 = InstanceType<any>;
type T1 = any

type T2 = InstanceType<never>;
type T2 = never

type T3 = InstanceType<string>;
// Error Type 'string' does not satisfy the constraint 'abstract new (...args: any) => any'.
type T3 = any

type T4 = InstanceType<Function>;
// Error Type 'Function' does not satisfy the constraint 'abstract new (...args: any) => any'.
// Error Type 'Function' provides no match for the signature 'new (...args: any): any'.
type T4 = any

ThisParameterType V3.3

获取函数 this 参数的类型, 如果没有则返回 unkown, 这个貌似在平时业务开发很少使用。

官方demo:

1
2
3
4
5
6
7
function toHex(this: Number) {
return this.toString(16);
}

function numberToString(n: ThisParameterType<typeof toHex>) {
return toHex.apply(n);
}

前置知识点:this

  • this参数只能叫 this,且必须在参数列表的第一个位置
  • this 必须是显式定义的
  • 这个 this 参数在函数实际被调用的时候不存在,不需要显式作为参数传入,而是通过 call、apply或者是 bind 等方法指定

OmitThisParameter V3.3

如果定义了 this 参数类型,就返回一个仅是去掉了 this 参数类型的新函数类型。对于没有,直接返回这个函数类型,

1
2
3
4
5
6
7
function toHex(this: Number) {
return this.toString(16);
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

console.log(fiveToHex());

ThisType V2.3

这个类型比较特殊,查看源码可以发现,定义了一个空接口. interface ThisType<T> { }

按照文档说法: 这个工具并不返回一个转换后的类型。相反,它作为一个上下文的this类型的标记。注意,必须启用 noImplicitThis 标志才能使用这个工具类型

直接从翻译结果来看,比较晦涩难懂,结合文档的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}

let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // Strongly typed this
this.y += dy; // Strongly typed this
},
},
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

把 上面的demo 直接复制到 typescript playgroud 中去,把鼠标移到this,可以看看提示的类型,你会发现 this 的类型为

1
2
3
4
5
6
{
x: number;
y: number;
} & {
moveBy(dx: number, dy: number): void;
}

先从简单的一点开始, 先声明一个对象

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
let obj = {
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // Property 'x' does not exist on type '{ moveBy(dx: number, dy: number): void; }'
this.y += dy; // Property 'y' does not exist on type '{ moveBy(dx: number, dy: number): void; }'
},
},
};

// 可以看到 this 默认推导的类型是 { moveBy(dx: number, dy: number): void; } , 也就是它的上一层对象 , 并没有x,y 属性

// 注意区别 :如果是箭头函数 就是 gloablThis (浏览器环境默认的上下文就是 window)

// 这个问题也可以通过显式的指明 this,
let obj = {
data: { x: 0, y: 0 },
methods: {
moveBy(this: { x: 0; y: 0 }, dx: number, dy: number) {
this.x += dx;
this.y += dy;
},
},
};

正常情况下,这种简单场景显式指定 this 就能满足使用

回头看看官方demo 的结构,非常类似 vue2.0 API 的, data ,methods ,…等结构,我们可以定义 多个data/methods ,之前的方法 就无法满足 这种可变未知的情况。
ts 是通过静态代码分析推断出的类型,但是在实际运行阶段,this是可能变化的,
所以仅仅靠依赖代码分析是无预测 this 类型的,

假设只考虑 data 、methods 两个属性, 于是就可以利用 泛型 ObjectDescriptor 和 一个工具方法 makeObject

1
2
3
4
5
6
7
8
9
10
11
12
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M ,重点是这里,ThisType 来标记(重)一个 this 的类型
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {}; // methods 已经变成了 M & ThisType<D & M>
return { ...data, ...methods } as D & M; // 使用断言 指定一个类型
}
// 这样 desc 类型 已经是类型安全的了。 然后在 methods 中 this 就变为了 <data & methods>,

Intrinsic String Manipulation Types 字符串操作类型

以下几个类型都是操作字符串
Uppercase // 大写
Lowercase // 小写
Capitalize // 首字母大写
Uncapitalize // 首字母小写

作者

Fat Dong

发布于

2023-02-01

更新于

2023-02-01

许可协议

React useState理解及使用

前情提要:最近在写一个功能的时候,犯了一个关于 React Hooks 的低级错误。 目前项目的React版本还是16.12.0

最近在开发一个功能,比较简单,就是请求一个接口返回数据,渲染一个列表,在这个过程有一些 loading(加载中),finish(加载结束),empty(接口没有返回数据) 等状态来控制显示一些交互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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import {getListData} from "@/api/list"


const List = () => {
const [list,setList] = useState([])
const [loading,setLoading] = useState(false)
const [finish,setFinish] = useState(false)
const [empty,setEmpty] = useState(false)
const [page,setPage] = useState(1)

const fetchData = ()=> {
setLoading(true)
getListData({page}).then(res=>{
if(res.code == 200 && res.data){
setList(res.data)
if(page == 1){
setEmpty(!res.data.length)
}
}else{
setList([])
}

}).finally(()=> setLoading(false))

}

return (
<div>
{loading
? <div>loading....</div>
: <>
{
list.map(ele=> <div key={ele.id}></div>)
}
</>
}
{
(finish && !empty) && <div>没有更多了</div>
}
{
empty && <div>暂无数据</div>
}
</div>
)

}

可以注意到,所有state是分离到多个了,

由于该页面有多个tab下,大体都是同样的list, 每个list 展示的 item 结构不一样,这样就会有 4 * 4 个 useState 分别控制每个list的data/loading 等状态。

于是我把每个list相关的数据给合并到一起了,于是就变成了:

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

// const [list,setList] = useState([])
// const [loading,setLoading] = useState(false)
// const [finish,setFinish] = useState(false)
// const [empty,setEmpty] = useState(false)
// const [page,setPage] = useState(1)

const [listData,setListData]= useState({
list:[],
loading: false,
finish: false,
empty: false,
page: 1,
})

// const fetchData = ()=> {
// setLoading(true)
// getListData({page}).then(res=>{
// if(res.code == 200 && res.data){
// setList(res.data)
// if(page == 1){
// setEmpty(!res.data.length)
// }
// }else{
// setList([])
// }

// }).finally(()=> setLoading(false))

// }
const fetchData = ()=> {
setListData({...listData, loading:true})
getListData({page}).then(res=>{
if(res.code == 200 && res.data){
setListData({
...listData,
list:res.data,
empty: page == 1 &&!res.data.length
})
}else{
setListData({
...listData,
list:[],
})
}
}).finally(()=> {
setListData({
...listData,
loading: false
})
})
}

这个时候有的人就能看出问题来了,loading,empty,finish 的状态出现了问题,无法渲染出正确的结构。

这里涉及到 useState 异步属性,首先确认一点 useState是异步的。也就是说执行 setXXX 之后,并不能立刻更新state 的状态,

异步的 setXXX 优点就是用户体验和性能,在只有一个 useState的情况下,并不明显,若是有多个的情况,如果每更新一个 state 就去渲染一次页面,一方面是性能问题,另一方面如果页面结构比较复杂,就是耗费时间,影响用户体验,

而且setState 是分批执行的,即时同时有多个 setXXX 的话或者是同一个 setXXX 执行了多次 ,都会被放去一个队列,等下次渲染前合并更新。

尤其在上面的例子中,第一种不会出现问题,是因为每个状态是分离,各自的 setXXX 互不影响。

而在第二种中,每次设置新值是建立在旧值(上一次状态值)之上的,也就说现在如果按照 第二种方案,在 finally 中设置值的时候,无法获取到 then 执行到时候设置的值。但是 then 中执行 setListData 之后,并未更新 state 状态,到了 finally 的时候,拿到的 listData 还是旧的,那么 then中的 setListData 就相当于被 “覆盖” 了, 而我们预期的目的是要 “合并”

useState 提供了两种方式更新 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
const fetchData = ()=> {
setListData({...listData, loading:true})
getListData({page}).then(res=>{
if(res.code == 200 && res.data){
setListData((prev)=>(
{
...prev,
list:res.data,
empty: page == 1 &&!res.data.length
}
))
}else{
setListData((prev)=>(
{
...prev,
list:[],
}
))
}
}).finally(()=> {
setListData((prev)=>(
{
...prev,
loading: false
}
))
})
}

也可以从这个例子可以看出,setXXX 并不会立即去执行更新state,渲染结构,

虽然明知 useState 是异步,还是偶尔脑子抽抽,犯一些低级错误,

作者

Fat Dong

发布于

2023-01-04

更新于

2023-01-04

许可协议

http缓存机制-原理

缓存在web开发中是相当重要的,对网络请求缓存,不仅可以提高用户的访问体验,一定程度上也能减轻服务器的压力

以下一些理论文字都是从 MDN COPY 了一些内容,当然这些知识都是从官方RFC文档来的,

类型

分为:私有缓存共享缓存

私有缓存

私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。

在请求头上则必须指定 private 指令。

1
Cache-Control: private

共享缓存

共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存和托管缓存。

在浏览器环境中,http缓存大部分情况是根据请求头的一些些属性来控制缓存的状态,常用的属性例如有:Cache-Control , Expires, Etag , If-None-Match,Last-Modified, If-Modified-Since 等等

属性 可取值

附录:规范RFC

作者

Fat Dong

发布于

2022-12-23

更新于

2023-02-07

许可协议