wordcloud2 源码分析

wordcloud2 是一个词云工具,根据文字的不同权重,铺满整个图形.

与 jquery 类似,通过立即执行函数,根据不同的模块化规范导出 WordCloud 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(() => {
// ...

// Expose the library as an AMD module
if (typeof define === "function" && define.amd) {
global.WordCloud = WordCloud;
define("wordcloud", [], function () {
return WordCloud;
});
} else if (typeof module !== "undefined" && module.exports) {
module.exports = WordCloud;
} else {
global.WordCloud = WordCloud;
}
})();

事件设计

实现 setImmediate , 为了保证每次绘制一个单词,需要在每次绘制之后,重新调用绘制方法, 并传入下一个单词.

源码中使用 postMessage 事件模拟, 回比 setTimeout(fn,0) 执行时机提前一些,如果不支持则回退到 setTimeout

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
window.setImmediate = (function setupSetImmediate () {
return window.msSetImmediate ||
window.webkitSetImmediate ||
window.mozSetImmediate ||
window.oSetImmediate ||
(function setupSetZeroTimeout () {
if (!window.postMessage || !window.addEventListener) {
return null
}

var callbacks = [undefined]
var message = 'zero-timeout-message'

// Like setTimeout, but only takes a function argument. There's
// no time argument (always zero) and no arguments (you have to
// use a closure).
var setZeroTimeout = function setZeroTimeout (callback) {
var id = callbacks.length
callbacks.push(callback)
window.postMessage(message + id.toString(36), '*')

return id
}

window.addEventListener('message', function setZeroTimeoutMessage (evt) {
// Skipping checking event source, retarded IE confused this window
// object with another in the presence of iframe
if (typeof evt.data !== 'string' ||
evt.data.substr(0, message.length) !== message/* ||
evt.source !== window */) {
return
}

evt.stopImmediatePropagation()

var id = parseInt(evt.data.substr(message.length), 36)
if (!callbacks[id]) {
return
}

callbacks[id]()
callbacks[id] = undefined
}, true)

/* specify clearImmediate() here since we need the scope */
window.clearImmediate = function clearZeroTimeout (id) {
if (!callbacks[id]) {
return
}

callbacks[id] = undefined
}

return setZeroTimeout
})()

list 是需要渲染词条的数组, 事件绑定在 canvas 元素上, 如果还有词条需要渲染就递归执行渲染方法. 超出长度或超时则停止执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function start() {
addEventListener("wordcloudstart", anotherWordCloudStart);
timer[timerId] = loopingFunction(function loop() {
if (i >= settings.list.length) {
stoppingFunction(timer[timerId]);
removeEventListener("wordcloudstart", anotherWordCloudStart);
delete timer[timerId];
return;
}
var drawn = putWord(settings.list[i]);

if (exceedTime() || canceled) {
stoppingFunction(timer[timerId]);
removeEventListener("wordcloudstart", anotherWordCloudStart);
delete timer[timerId];
return;
}
i++;
timer[timerId] = loopingFunction(loop, settings.wait);
}, settings.wait);
}

start();

源码设计

  • 合并初始化和默认参数,判断元素合法性

  • 定义默认图形的函数表达式,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    switch (settings.shape) {
    case "cardioid":
    settings.shape = function shapeCardioid(theta) {
    return 1 - Math.sin(theta);
    };
    }

    // 如果不是极坐标表示的方式,需要自行转换 \
    // http://timdream.org/wordcloud2.js/shape-generator.html 用于生成图形坐标
    const shape = () => {
    const max = 1026;
    const leng = [
    290, 296, 299, 301, 305, 309, 311, 313, 315, 316, 318, 321, 325, 326, 327,
    328, 330, 330, 331, 334, 335, 338, 340, 343, 343, 343, 346, 349, 353, 356,
    360, 365, 378, 380, 381, 381,
    ];
    return leng[((theta / (2 * Math.PI)) * leng.length) | 0] / max;
    };
  • 随机颜色方法 randomHslColor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function randomHslColor(min, max) {
    return (
    "hsl(" +
    (Math.random() * 360).toFixed() +
    "," +
    (Math.random() * 30 + 70).toFixed() +
    "%," +
    (Math.random() * (max - min) + min).toFixed() +
    "%)"
    );
    }
  • 获取随机角度 getRotateDeg

    有几个变量可以控制这个值:

    rotateRatio 旋转角度的概率
    maxRotation 最大值 Math.PI / 2
    minRotation 最小值 -Math.PI / 2
    rotationRange 旋转角度区间默认在 [-Math.PI / 2 , Math.PI / 2]
    rotationSteps 固定递进旋转角度, 如果是 3 旋转角度只会是 [-90deg,-30deg,30deg,90deg]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function getRotateDeg() {
    // 最大角度和最小角度相同
    if (rotationRange === 0) {
    return minRotation;
    }
    // 概率以外不旋转
    if (Math.random() > settings.rotateRatio) {
    return 0;
    }
    //固定角度区随机值
    if (rotationSteps > 0) {
    return (
    minRotation +
    (Math.floor(Math.random() * rotationSteps) * rotationRange) /
    (rotationSteps - 1)
    );
    } else {
    return minRotation + Math.random() * rotationRange;
    }
    }
  • 获取渲染相关数据 getTextInfo

    传入需要渲染的 词条,权重,旋转角度

    采用双缓存的方法,创建另一个不可见的 canvas 画布,将词条绘制在另一个画布上,将画布转成图片,并分析像素点信息,最终返回文字信息

    定义一个网格大小,将画布分成若干个格子,一旦一个有效像素点落在格子中,那么这个格子中的其他像素点无需在判断,从而优化性能

    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
    function getTextInfo() {
    // 根据权重计算字体大小 weightFactor 指定权重基数
    var fontSize = weight * weightFactor;
    // 不绘制
    if (fontSize <= settings.minSize) {
    return false;
    }
    var mu = 1;
    // 网格大小
    var g = settings.gridSize;
    // 创建新画布
    var fcanvas = document.createElement("canvas");

    // 绘制词条前获取词条的宽高
    var fw = fctx.measureText(word).width / mu;
    var fh =
    Math.max(
    fontSize * mu,
    fctx.measureText("m").width,
    fctx.measureText("\uFF37").width
    ) / mu;
    // 创建一个包围盒, 让他足够容纳文字
    var boxWidth = fw + fh * 2;
    var boxHeight = fh * 3;

    // 计算网格的长宽个数
    var fgw = Math.ceil(boxWidth / g);
    var fgh = Math.ceil(boxHeight / g);
    boxWidth = fgw * g;
    boxHeight = fgh * g;
    // 绘制文字时候的偏移量
    var fillTextOffsetX = -fw / 2;
    // 希腊字母在 0.4 高度的位置,视觉效果会在中间位置
    var fillTextOffsetY = -fh * 0.4;

    // 计算考虑到旋转角度后的盒子大小
    var cgh = Math.ceil(
    (boxWidth * Math.abs(Math.sin(rotateDeg)) +
    boxHeight * Math.abs(Math.cos(rotateDeg))) /
    g
    );
    var cgw = Math.ceil(
    (boxWidth * Math.abs(Math.cos(rotateDeg)) +
    boxHeight * Math.abs(Math.sin(rotateDeg))) /
    g
    );
    var width = cgw * g;
    var height = cgh * g;

    // 把文字绘制到临时画布上
    fctx.fillStyle = "#000";
    fctx.textBaseline = "middle";
    fctx.fillText(
    word,
    fillTextOffsetX * mu,
    (fillTextOffsetY + fontSize * 0.5) * mu
    );

    // 将画布转为 像素点

    var imageData = fctx.getImageData(0, 0, width, height).data;

    // 计算像素占据的网格
    var occupied = [];
    var gx = cgw;
    var gy, x, y;
    // 文字占据区域的网格坐标 [x1,x2,y1,y2]
    var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2];
    while (gx--) {
    gy = cgh;
    while (gy--) {
    y = g;
    singleGridLoop: while (y--) {
    x = g;
    while (x--) {
    if (imageData[((gy * g + y) * width + (gx * g + x)) * 4 + 3]) {
    // 如果像素点落在格子中则添加到数列中
    occupied.push([gx, gy]);

    if (gx < bounds[3]) {
    bounds[3] = gx;
    }
    if (gx > bounds[1]) {
    bounds[1] = gx;
    }
    if (gy < bounds[0]) {
    bounds[0] = gy;
    }
    if (gy > bounds[2]) {
    bounds[2] = gy;
    }

    break singleGridLoop;
    }
    }
    }
    }
    }
    return {
    mu: mu,
    occupied: occupied,
    bounds: bounds,
    gw: cgw,
    gh: cgh,
    fillTextOffsetX: fillTextOffsetX,
    fillTextOffsetY: fillTextOffsetY,
    fillTextWidth: fw,
    fillTextHeight: fh,
    fontSize: fontSize,
    };
    }


  • 绘制策略

    现在有了文字包围盒的尺寸和坐标,需要利用这些信息将图形填充满

    首先拿到一个词条, 以中心点为圆心,这个中心点可能是用户自定义的中心点,所以不一定在图形的中心. 以指定的半径画圆 (也可能是其他图形的极坐标表达式产生的图形),在这个圆上平均取若干个采样点,半径越大采样点越多.

    从圆心开始,初始半径为 0,也就表示词条放在中心点. 下一个词条进来的时候,因为半径为 0 的圆上已经有了一个词条,所以扩大半径画圆,产生采样点,循环这些采样点,并检测词条是否发生碰撞.

    如果不能放下就继续循环采样点,如果所有采样点都不符合条件,则扩大半径重新读取采样点,重新遍历

    如果可以放下,则跳出所有循环,传入下一个词条,重复以上过程

  • 计算绘制点

    最终绘制的过程,是以中心点为原点,指定半径长度画圆, 在圆周上平均取八个点作为绘制点,半径每增加一次,绘制点增加 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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    // 绘制的中心点
    center = settings.origin
    ? [settings.origin[0] / g, settings.origin[1] / g]
    : [ngx / 2, ngy / 2];

    // 最大绘制半径,当中心点在包围盒的四个顶点时,绘制最大半径就是对角线的长度
    //
    maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy));

    var r = maxRadius + 1;

    while (r--) {
    // 获取不同半径圆周上的点的坐标
    var points = getPointsAtRadius(maxRadius - r);

    // 检查是否可以绘制
    var drawn = points.some(tryToPutWordAtPoint);

    if (drawn) {
    // leave putWord() and return true
    return true;
    }
    }

    var getPointsAtRadius = function getPointsAtRadius(radius) {
    // 采样点的个数随半径的增加增大
    var T = radius * 8;

    // Getting all the points at this radius
    var t = T;
    var points = [];

    // 半径为 0 时,词条放在中心点
    if (radius === 0) {
    points.push([center[0], center[1], 0]);
    }

    // 半径不为 0 时获取采样点的坐标,共有t个采样点
    while (t--) {
    var rx = 1;
    // 图形的数学表达式建立在极坐标系中, x 的取值范围 [0, 2*Math.PI] 值域为 [0,1]
    // 这个值会和半径相乘,得到半径的实际长度
    // 这个过程可以想象乘等比放大一个图形的过程,而在放大的过程中,图形上的采样点也越来越多
    if (settings.shape !== "circle") {
    rx = settings.shape((t / T) * 2 * Math.PI); // 0 to 1
    }

    // Push [x, y, t] t is used solely for getTextColor()
    points.push([
    center[0] + radius * rx * Math.cos((-t / T) * 2 * Math.PI),
    center[1] +
    radius * rx * Math.sin((-t / T) * 2 * Math.PI) * settings.ellipticity,
    (t / T) * 2 * Math.PI,
    ]);
    }

    pointsAtRadius[radius] = points;
    return points;
    };
  • 碰撞检测

    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
    var tryToPutWordAtPoint = function (gxy) {
    // gxy [x1,y1] 采样点坐标

    // info.gw info.gh 旋转周的包围和宽度和高度
    // 将包围盒中心和采样点对其
    var gx = Math.floor(gxy[0] - info.gw / 2);
    var gy = Math.floor(gxy[1] - info.gh / 2);
    var gw = info.gw;
    var gh = info.gh;

    // occupied 所有包含有效像素的格子的坐标
    // ngx ngy 整个画布被分成的格子数
    // grid 所有格子的二维数组
    var canFitText = function canFitText(gx, gy, gw, gh, occupied) {
    var i = occupied.length;
    while (i--) {
    // 从偏移后的采样点位置,每次加上格子的坐标
    var px = gx + occupied[i][0];
    var py = gy + occupied[i][1];

    // 是否超出画布的情况
    if (px >= ngx || py >= ngy || px < 0 || py < 0) {
    if (!settings.drawOutOfBound) {
    return false;
    }
    continue;
    }

    // 只要有一个格子放不下就是重叠的情况直接跳出
    if (!grid[px][py]) {
    return false;
    }
    }
    return true;
    };
    // If we cannot fit the text at this position, return false
    // and go to the next position.
    if (!canFitText(gx, gy, gw, gh, info.occupied)) {
    return false;
    }

    // Actually put the text on the canvas
    drawText(
    gx,
    gy,
    info,
    word,
    weight,
    maxRadius - r,
    gxy[2],
    rotateDeg,
    attributes,
    extraDataArray
    );

    // Mark the spaces on the grid as filled
    updateGrid(gx, gy, gw, gh, info, item);
    // Return true so some() will stop and also return true.
    return true;
    };

其他实现思路

填充过程可以使用 阿基米德螺线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const points = []; // 所有放置点

let dxdy,
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), // 最大半径
t = 1, // 阿基米德弧度
index = 0, // 当前位置序号
dx, // x坐标
dy; // y坐标

// 通过每次增加的步长固定为1,实际步长为 step * 1,来获取下一个放置点

while ((dxdy = getPosition((t += 1)))) {
dx = dxdy[0];

dy = dxdy[1];

if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break; // (dx, dy)距离中心超过maxDelta,跳出螺旋返回false

points.push([dx, dy, index++]);
}

d3-dispatch 源码分析

d3-dispatch 是一个基于 观察者模式 的事件处理工具.

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注册事件
const d = dispatch("start", "end");

//帮顶事件方法
d.on('start.a',()=>{})
d.on('start.*b.c',()=>{}) // *b.c作为方法名
d.on('start.d start.e',()=>{}) // 帮顶d,e两个方法
d.on('start.d',null) // 删除d绑定事件
d.on('start.e') // 获取e绑定事件

// 执行方法
d.fire('start') // 按照帮顶顺序执行每一个方法

dispatch

用于添加一个或多个事件名称,事件对应着一个队列,可以包含多个子方法

1
const d = dispatch("start", "end");
  • 尝试把方法名转换为字符串
  • 不能转换为字符串 或 已经注册过 或 包含空格. 则不合法
1
2
3
4
5
6
7
8
9
10
11
const dispatch = (...args: string[]) => {
const types: Types = {};
for (let i = 0, l = args.length, t; i < l; i += 1) {
t = `${args[i]}`;
if (!t || t in types || /[\s.]/.test(t)) throw new Error();
types[t] = [];
}
return new Dispatch(types);
};

export default dispatch;

Dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface TypeItem {
name: string;
value: (...args: unknown[]) => unknown;
}

interface Types {
[k: string]: TypeItem[];
}

// 工具方法空函数
const noop = { value: () => {} } as TypeItem;

class Dispatch {
private types: Types;

constructor(types: Types) {
this.types = types;
}

on() {}

copy() {}

fire() {}
}

一个取值的工具方法
事件对象中取出回调方法的数组

1
2
3
4
5
6
7
8
9
const get = (type: TypeItem[], name: string): TypeItem["value"] | undefined => {
for (let i = 0, n = type.length, c; i < n; i += 1) {
c = type[i];
if (c.name === name) {
return c.value;
}
}
return undefined;
};

设置值的工具方法
如果存在同名方法,从事件队列中删除,并置空同名方法
如果传递了回调函数则在队列最后重新添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const set = (
_type: TypeItem[],
name: string,
callback: TypeItem["value"] | null
): TypeItem[] => {
let type = _type;
for (let i = 0, n = type.length; i < n; i += 1) {
if (type[i].name === name) {
type[i] = noop;
type = type.slice(0, i).concat(type.slice(i + 1));
break;
}
}
if (callback != null) type.push({ name, value: callback });
return type;
};

解析事件方法名
方法名尝试转为字符串,并删除头尾空格预处理
a.b a.c 会添加两个回调方法, a.b.c 会添加 b.c 作为方法名称
用空格来切割,因为可能存在多个绑定事件
查找 . 的第一个匹配位置,后面的无论是什么都看作是一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const parseTypenames = (
typenames: string,
types: Types
): { type: string; name: string }[] => {
return typenames
.trim()
.split(/^|\s+/)
.map(function map(_t) {
let t = _t;
let name = "";
const i = t.indexOf(".");
if (i >= 0) {
name = t.slice(i + 1);
t = t.slice(0, i);
}
const has = Object.prototype.hasOwnProperty.call(types, t);
if (t && !has) throw new Error(`unknown type: ${t}`);
return { type: t, name };
});
};

on

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
public on(
_typename: string | { name: string; type: string },
callback: TypeItem['value'] | null
): this | TypeItem['value'] | undefined {
const { types } = this;
let typename = _typename;
const T = parseTypenames(`${typename}`, types);
let t: string | TypeItem['value'] | undefined;
let i = 0;
const n = T.length;

// 如果不存在回调函数,把他作为取值操作,返回最先匹配到的方法
if (callback === undefined) {
while (i < n) {
typename = T[i];
t = typename.type;
if (!t) return undefined;
t = get(types[t], typename.name);
if (t) return t;
}
i += 1;
return undefined;
}
// 如果回调函数不是方法,直接报错

if (callback != null && typeof callback !== 'function')
throw new Error(`invalid callback: ${callback}`);

// 添加到 type 事件对象中
while (i < n) {
// 插入
typename = T[i];
t = typename.type;
const ct = typename;

if (t) types[t] = set(types[t], typename.name, callback);
// 删除
// 所有监听队列中的同名方法都将被删除

else if (callback == null)
Object.entries(types).forEach(([k, v]) => {
types[k] = set(v, ct.name, null);
});

i += 1;
}

return this;
}

copy

1
2
3
4
5
6
7
8
public copy() {
const copy: Types = {};
const { types } = this;
Object.entries(types).forEach(([k, v]) => {
copy[k] = v.slice();
});
return new Dispatch(copy);
}

fire

1
2
3
4
5
6
7
8
public fire(type: string, that?: unknown, ...args: unknown[]) {
const has = Object.prototype.hasOwnProperty.call(this.types, type);
if (!has) throw new Error(`unknown type: ${type}`);
const t = this.types[type];
let i = 0;
const n = t.length;
for (; i < n; i += 1) t[i].value.apply(that, args);
}

React v16 源码分析 ⑧ commit阶段

预备工作

flushPassiveEffects 执行所有还没执行的副作用,因为执行的时候还可能产生额外的副作用,所以需要 while 判断. rootWithPendingPassiveEffect 表示是否有副作用标记

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
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);

// 第一步执行所有的副作用销毁函数
// 一定要保证在执行副作用函数前,所有的销毁函数已经执行完成,但也有例外的情况
// 例如在兄弟组件中,一个组件的副作用销毁函数,是在兄弟组件的副作用创建函数中定义的
// 并且被添加在Ref 属性上通过引用的方式使用.

var unmountEffects = pendingPassiveHookEffectsUnmount;
for (var i = 0; i < unmountEffects.length; i += 2) {
var destroy = _effect.destroy;
destroy();
}

// 执行所有的副作用创建函数
var mountEffects = pendingPassiveHookEffectsMount;

for (var _i = 0; _i < mountEffects.length; _i += 2) {
var _effect2 = mountEffects[_i];
var create = effect.create;
effect.destroy = create();
}

// 在副作用链表上 删除, 用于内存回收
while (effect !== null) {
var nextNextEffect = effect.nextEffect;
effect.nextEffect = null;
effect = nextNextEffect;
}

// 如果与额外的副作产生则重新发起调度
flushSyncCallbackQueue();

将 rootFiber 添加到 effect 链表中, completeWork 中构建的 effect 链表只包涵它的子元素, 如果 rootFiber 有副作用需要把他添加到链表的最后. 最终的 effect 链表属于 rootFiber 的父元素

1
2
3
4
5
6
7
8
9
10
if (finishedWork.flags > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}

commitBeforeMutationEffects

尽可能早的去调度 mutation effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
while (nextEffect !== null) {
var current = nextEffect.alternate;

var flags = nextEffect.flags;

// getSnapshotBeforeUpdate 将会执行
if ((flags & Snapshot) !== NoFlags) {
commitBeforeMutationLifeCycles(current, nextEffect);
}

if ((flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalPriority$1, function () {
flushPassiveEffects();
return null;
});
}
}

nextEffect = nextEffect.nextEffect;
}

scheduleCallback

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
// 获取当前时间
var startTime = getCurrentTime();
// 计算不同优先级任务的过期时间

var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY = 1073741823;

var expirationTime;
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
// ...
}

var expirationTime = startTime + timeout;

var newTask = {
id: taskIdCounter++, // 任务id
callback, //任务内容
priorityLevel, // 任务优先级
startTime, // 任务开始时间
expirationTime, //任务过期时间
sortIndex: -1, // 小顶堆,始终从任务队列中拿出最优先的任务
};

// 开始时间>当前时间说明是一个延时任务
if (startTime > currentTime) {
newTask.sortIndex = startTime;

// 放入到延迟队列中,小顶堆
push(timerQueue, newTask);
// 目前所有的任务都是延时任务,取出一个最早的延时任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 调度一个延时任务
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
// 放入普通任务队列,小顶堆
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// 调度一个普通任务
requestHostCallback(flushWork);
}
}

return newTask;

调度普通任务

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
// 异步通知需要处理任务
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function (callback) {
// 接受传入的flushWork
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};

const performWorkUntilDeadline = () => {
// flushWork
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 每次执行任务只有5ms,意味着每一次渲染周期中有多次处理用户响应的机会
// 也不需要和浏览器的刷新对其,也就是说一次更新的内容不一定会在浏览器的下一帧渲染。
// let yieldInterval = 5;
// let deadline = 0;
deadline = currentTime + yieldInterval;
// 默认还有剩余时间
const hasTimeRemaining = true;
try {
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 还有没处理完的工作继续安排下一次执行
port.postMessage(null);
}
} catch (error) {
// 抛出错误退出执行
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
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
function flushWork(hasTimeRemaining, initialTime) {
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
return workLoop(hasTimeRemaining, initialTime);
} finally {
}
}

function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// 遍历timerQueue如果有到期的任务就放入到taskQueue中
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// currentTask.expirationTime > currentTime 任务没有过期
// hasTimeRemaining || shouldYieldToHost() 但是已经没有剩余时间,任务需要暂停
// 跳出并归还主线程
break;
}
// 到了过期时间,且有时间执行
const callback = currentTask.callback;
if (typeof callback === "function") {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
// 任务执行
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === "function") {
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
// 不是函数直接丢弃
pop(taskQueue);
}
// 拿一个新任务
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
// 如果taskQueue没有任务了,就处理timerQueue
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}

调度延迟任务

1
2
3
4
5
6
7
8
// callback 任务
// ms 延迟时间

requestHostTimeout = function (callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
};

commitMutationEffects

为什么需要解绑 Ref ?

如果 Ref 被定义为一个函数,在更新阶段会执行两次,第一次为 null,第二次会绑定 DOM 元素,这是因为每次渲染都会创建新的函数实例,所以 React 会先删除旧的 Ref 回收内存,再创建一个新的.

1
2
3
4
5
6
7
8
9
10
11
12
13
if (flags & Ref) {
var current = nextEffect.alternate;
if (current !== null) {
var currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === "function") {
currentRef(null);
} else {
currentRef.current = null;
}
}
}
}

不同的 Tag 对应不同的 DOM 操作

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
var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);

switch (primaryFlags) {
case Placement:
{
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
break;
}

case PlacementAndUpdate:
{
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
var _current = nextEffect.alternate;
commitWork(_current, nextEffect);
break;
}

case Hydrating:
{
nextEffect.flags &= ~Hydrating;
break;
}

case HydratingAndUpdate:
{
nextEffect.flags &= ~Hydrating;

var _current2 = nextEffect.alternate;
commitWork(_current2, nextEffect);
break;
}

case Update:
{
var _current3 = nextEffect.alternate;
commitWork(_current3, nextEffect);
break;
}

case Deletion:
{
commitDeletion(root, nextEffect);
break;
}
}

resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
commitPlacement 插入
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
function commitPlacement(finishedWork) {
// 找到当前插入节点的上级节点
var parentFiber = getHostParentFiber(finishedWork); // Note: these two variables *must* always be updated together.

var parent;
var isContainer;
var parentStateNode = parentFiber.stateNode;

// 判断是不是跟节点
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;

case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;

case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
}

// 如果上级节点标记了清除内容的副作用,则先清空文本
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);

parentFiber.flags &= ~ContentReset;
}
// 插入有两种情况,直接插入一个元素中,或插入兄弟节点前面,这里需要获取兄弟节点
var before = getHostSibling(finishedWork);

if (isContainer) {
如果兄弟节点存在;
/*
如果兄弟节点存在
if (container.nodeType === COMMENT_NODE) {
container.parentNode.insertBefore(child, beforeChild);
} else {
container.insertBefore(child, beforeChild);
}

如果兄弟节点不存在
if (container.nodeType === COMMENT_NODE) {
parentNode = container.parentNode;
parentNode.insertBefore(child, container);
} else {
parentNode = container;
parentNode.appendChild(child);
}
*/
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
commitWork 更新
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
function commitWork(current, finishedWork) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 有可能兄弟组件间的副作用会相互影响,例如在同一次 commit 阶段中
// 一个组件的副作用销毁函数,不应该依赖与另一个组件的副作用创建函数,通过ref传递,因为执行时机问题造成不会执行
{
commitHookEffectListUnmount(Layout | HasEffect, finishedWork);
}

return;
}

case ClassComponent: {
return;
}

case HostComponent: {
var instance = finishedWork.stateNode;

if (instance != null) {
// Commit the work prepared earlier.
var newProps = finishedWork.memoizedProps; // For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.

var oldProps = current !== null ? current.memoizedProps : newProps;
var type = finishedWork.type; // TODO: Type the updateQueue to be specific to host components.

var updatePayload = finishedWork.updateQueue;
finishedWork.updateQueue = null;

/*
最终会调用原生节点上的设置属性
if (value === null) {
node.removeAttribute(_attributeName);
} else {
node.setAttribute(_attributeName, '' + value);
}
*/
if (updatePayload !== null) {
commitUpdate(instance, updatePayload, type, oldProps, newProps);
}
}

return;
}

case HostText: {
if (!(finishedWork.stateNode !== null)) {

var textInstance = finishedWork.stateNode;
var newText = finishedWork.memoizedProps;

var oldText = current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
}
commitDeletion 删除

虽然只有当前的 Fiber 节点删除,但仍然需要遍历所有的子节点,因为可能子节点会执行 componentWillUnmount

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
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
// 执行 commitUnmount
// 遍历所有子节点,删除 Ref,或执行 componentWillUnmount
// HostPortal 需要删除挂载元素
// 执行 useEffect销毁函数
commitNestedUnmounts(finishedRoot, node);

//所有子节点卸载后才能安全的从 DOM 树中删除当前节点
if (currentParentIsContainer) {
removeChildFromContainer(currentParent, node.stateNode);
} else {
removeChild(currentParent, node.stateNode);
} // Don't visit children because we already visited them.
} else if (node.tag === HostPortal) {
if (node.child !== null) {
// When we go into a portal, it becomes the parent to remove from.
// We will reassign it back when we pop the portal on the way up.
currentParent = node.stateNode.containerInfo;
currentParentIsContainer = true; // Visit children because portals might contain host components.

node.child.return = node;
node = node.child;
continue;
}
} else {
// Visit children because we may find more host components below.
commitUnmount(finishedRoot, node);

if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}

if (node === current) {
return;
}

while (node.sibling === null) {
if (node.return === null || node.return === current) {
return;
}

node = node.return;

if (node.tag === HostPortal) {
// When we go out of the portal, we need to restore the parent.
// Since we don't keep a stack of them, we will search for it.
currentParentIsValid = false;
}
}

node.sibling.return = node.return;
node = node.sibling;
}

commitLayoutEffects

执行前会将 current 指向 finishedWork, 在 componentDidMount/Update. 执行結束后, work-in-progress tree 已经变为最终的状态了,可以安全的指向 current

1
root.current = finishedWork;

下一个阶段是 layout 节点,会在已经改变的树上读取 effect 执行,这也是为什么此时能获取到 DOM 的最新状态

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
{
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 同步执行 layoutEffect
{
commitHookEffectListMount(Layout | HasEffect, finishedWork);
}
// 将 mutation effects 创建函数和销毁函数添加到队列中,并执行调度
schedulePassiveEffects(finishedWork);
return;
}

case ClassComponent:
{
var instance = finishedWork.stateNode;

if (finishedWork.flags & Update) {
if (current === null) {
// current 不存在表示首次挂载
instance.componentDidMount();
} else {
var prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
var prevState = current.memoizedState; // We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.

// 组件更新
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
);
}
}
commitUpdateQueue(finishedWork, updateQueue, instance);
}

return;
}

// 重新绑定 Ref
if (flags & Ref) {
var ref = finishedWork.ref;

if (ref !== null) {
var instance = finishedWork.stateNode;
var instanceToUse;

switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;

default:
instanceToUse = instance;
} // Moved outside to ensure DCE works with this flag

if (typeof ref === "function") {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
}

requestPaint

在上面三个阶段执行結束后,会执行请求绘制的方法, 通过 Scheduler 模块调度, 这也是 useLayoutEffect 会在 DOM 绘制前执行的原因

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
requestPaint();

var rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

if (rootDoesHavePassiveEffects) {
// 在所有阶段执行结束后 root 节点上还有副作用, 先保存一个引用,在 layout 结束后再去调度
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {
// We are done with the effect chain at this point so let's clear the
// nextEffect pointers to assist with GC. If we have passive effects, we'll
// clear this in flushPassiveEffects.
nextEffect = firstEffect;

while (nextEffect !== null) {
var nextNextEffect = nextEffect.nextEffect;
nextEffect.nextEffect = null;

if (nextEffect.flags & Deletion) {
detachFiberAfterEffects(nextEffect);
}

nextEffect = nextNextEffect;
}
} // Read this again, since an effect might have updated it

TS extends 使用技巧

extends 关键字在 TS 中的两种用法,即接口继承和条件判断。

接口继承

和 class 中的继承类似,但是类型中可以使用多继承

1
2
3
interface T3 extends T1, T2 {
age: number;
}

条件语句

extends 可以当作类型中的 if 语句使用,当时理念上与 if 略有不同

1
2
type T = string;
type A = T extends string ? true : false;

上面的语句可以简单理解为, T 的类型是否是 string 类型. 但是,更准确的说法是 T 的类型能否分配给 string 类型.

因为类型系统中不能像 if 一样,通过 ===== 来判断, 例如下面的接口或对象类型

1
2
3
4
5
6
7
8
9
interface A1 {
name: string;
}

interface A2 {
name: string;
age: number;
}
type A = A2 extends A1 ? string : number; //string

这两个类型并不是完全相同,但是 A2 的类型可以分配给 A1 使用,因为 A2 中 完全包括了 A1 中的类型,也就是说可以把 A2 当做 A1 使用.

反过来 A1 不能当作 A2 使用,应为 A1 中没有 age 类型,可能导致类型错误.

条件分配类型

看一个例子

1
2
3
4
type A2 = "x" | "y" extends "x" ? string : number; // number

type P<T> = T extends "x" ? string : number;
type A3 = P<"x" | "y">; // string | number

A3 并不会和 A2 相同,造成这个的原因就是 分配条件类型(Distributive Conditional Types)

When conditional types act on a generic type, they become distributive when given a union type

当条件类型作用在泛型上时, 当传入一个联合类型这个类型是可分配的.

换句话说,当在使用泛型做条件判断的时候, 而且这个泛型传入的是一个联合类型,就会像数学中的分配率一样,把联合类型中的没没一项分别进行条件判断,最终返回一个联合类型

所以上面 A3 类型,等价于

1
2
3
type A3 =
| ("x" extends "x" ? string : number)
| ("y" extends "x" ? string : number);

分配条件类型最终会返回一个不同分支返回结果的联合类型,如果返回的结果是一个包装过的类型,那么就是不同分支包装类型的联合类型

1
2
3
4
5
6
7
type Test<T, T2 = T> = T extends T2 ? {t: T} : never;

type a = Test<string|number> // {t:string} | {t:number}

type Test2<T, T2 = T> = T extends T2 ? [T] : never;

type a = Test<string|number> // [string] | [number]

注意 never

never 在条件语句中的行为可能和想象的不一样,这也是条件类型在对其约束, never 相当于空的联合类型,所以没有判断直接返回

1
2
3
4
type A1 = never extends "x" ? string : number; // string

type P<T> = T extends "x" ? string : number;
type A2 = P<never>; // never

防止条件类型分配

如果不想让 never 解析成空的联合类型,而是当作一个 never 类型传入,实际上就是阻止类型系统对联合类型自动分配,可以使用一个 []

1
2
3
type P<T> = [T] extends ["x"] ? string : number;
type A1 = P<"x" | "y">; // number
type A2 = P<never>; // string

Highcharts wrapper for React

简介

一个非常精简的包装工具,可以在 React 项目中使用 highcharts

源码分析

服务端渲染的时候 useLayoutEffect 会抛出警告,所以需要按条件使用,如果是浏览器环境使用 useLayoutEffect,如果是服务器环境使用 useEffect

使用 useLayoutEffect 可以保证在布局阶段, ref 所指向的挂载元素是可以使用的,也可以用在一个父组件的 componentDidMount

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

// 区分浏览器环境还是
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;


// 使用 forwardRef 转发 ref, 将 ref 传递到子组件
// 如果有需要可以通过传入 ref 属性,获取到挂载节点的真是DOM元素
// 也可以配合 useImperativeHandle 使用
const HighchartsReact = forwardRef(
function HighchartsReact(props, ref) {

const containerRef = useRef();
const chartRef = useRef();

useIsomorphicLayoutEffect(() => {
function createChart() {
const H = props.highcharts || (
typeof window === 'object' && window.Highcharts
);

// 暴露参数,用于表明实例化图表类型
const constructorType = props.constructorType || 'chart';

if (!H) {
console.warn('The "highcharts" property was not passed.');

} else if (!H[constructorType]) {
console.warn(
'The "constructorType" property is incorrect or some ' +
'required module is not imported.'
);

// options 必填参数,如果没有提示警告
} else if (!props.options) {
console.warn('The "options" property was not passed.');

} else {
// 创建图表实例
chartRef.current = H[constructorType](
containerRef.current,
props.options,
props.callback ? props.callback : undefined
);
}
}

if (!chartRef.current) {
createChart();
} else {
// 是否允许图表更新,如果为假,在接受到新的 props 之后会直接忽略掉
if (props.allowChartUpdate !== false) {
// immutable 用于指定是否使用不可变数据
// 本质就是不会再原有的图表上更新,而会直接创建新图表的实例
if (!props.immutable && chartRef.current) {
chartRef.current.update(
props.options,
// 与用指定原生的更新参数,由 highcharts 自己提供
...(props.updateArgs || [true, true])
);
} else {
createChart();
}
}
}
});

useIsomorphicLayoutEffect(() => {
return () => {
// 组件卸载的时候注销实例
if (chartRef.current) {
chartRef.current.destroy();
chartRef.current = null;
}
};
}, []);



// 一般配合 forwardRef 使用
// 可以用于暴露封装组件内部的状态
// 通过 ref.current.chart 或 ref.current.chart 可以在外部获取到组件实例,以及挂载节点
useImperativeHandle(
ref,
() => ({
get chart() {
return chartRef.current;
},
container: containerRef
}),
[]
);

// Create container for the chart
return <div { ...props.containerProps } ref={ containerRef } />;
}
);

export default memo(HighchartsReact);

React+TS 实战文档

Function Components

React.FunctionComponent 或 React.FC 与普通函数有什么不同?

React.FC 是隐式的返回值类型, 不同函数是显示的返回值类型

React.FC 提供了一些静态属性类型检查 displayName,propTypes, defaultProps

React.FC 隐式的定义了 children 子元素类型, 但某些场景自定义这个类型会更好

第二点中,当在 React.FC 中使用 defaultProps 的时候可能会造成错误

1
2
3
4
5
6
7
8
9
10
11
interface Props {
text: string;
}

const BackButton: React.FC<Props> = (props) => {
return <div />;
};
BackButton.defaultProps = {
text: "Go Back",
};
let a = <BackButton />; // error: text is missing in type {}

这个问题有很多种方法可以解决, 可以使用普通函数来定义,而不是使用函数类型

1
2
3
4
5
6
7
8
9
10
interface Props {
text: string;
}
function BackButton(props: Props) {
return <div />;
}
BackButton.defaultProps = {
text: "Go Back",
};
let a = <BackButton />; // it's OK

或者显示的声明 defaultProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Props {
text: string;
}

const BackButton: React.FC<Props> & {
defaultProps: {
text: string;
};
} = (props) => {
return <div />;
};
BackButton.defaultProps = {
text: "Go Back",
};
let a = <BackButton />; // it's OK

另一个问题是 children 类型的问题, 有时组件返回的 children 类型并不是 React.FC 默认的 children 类型

1
2
// Type 'string' is not assignable to type 'ReactElement<any, any>'.
const Title: React.FC = () => "123";

你可以选择者重新为返回值赋类型,这个处理方法同样适用于条件语句中的子元素,或者通过数组或对象生成的子元素

1
const Title: React.FC = () => "123" as unknown as React.ReactElement;

正因为有这种问题, @type/react@^18.0.0 将不再把 children 作为默认的属性,而是需要显示声明

1
2
3
4
5
6
7
8
9
10
11
type T1 = { name: string };
type T2 = { age: number };

// 返回值逆变 因此返回的是联合类型
type UnionToIntersection<T> = T extends {
a: (x: T1) => infer U;
b: (x: T2) => infer U;
}
? U
: never;
type T3 = UnionToIntersection<{ a: (x: T1) => string; b: (x: T2) => void }>; // string|void

类型模块查找过程

  • 检查 package.json 的 exports 字段

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "exports": {
    "./es": {
    "types": "./es/index.d.ts",
    "import": "./es/index.js"
    }
    }
    }

    但是不支持动态路径映射,必须是指定路径下的文件, 也就是说类型文件必须存在于 /es 目录下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "exports": {
    "./es": {
    // 使用方会提示找不到对应的类型文件
    "types": "./index.d.ts",
    "import": "./es/index.js"
    }
    }
    }
  • 检查 package.json 的 types

    1
    2
    3
    4
    {
    "name": "lodash",
    "types": "index.d.ts"
    }

    如果导入了 lodash/es 类似的二级路径,但 types 只指向 index.d.ts,TypeScript 不会自动识别子路径,类型文件必须存在于 /es 目录下,或者使用 tsconfig 文件明确指明路径

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "compilerOptions": {
    "baseUrl": ".",
    "paths": {
    "lodash/es": ["node_modules/@types/lodash/index.d.ts"]
    }
    }
    }
  • 直接检查文件路径
    尝试直接从导入路径中解析类型文件

  • 在 node_modules/@types 中查找类型
    同样不支持动态路径映射,必须指定路径

映射类型

  • 边界情况,映射类型在遇到非对象类型(例如 undefined 或 null)时,直接保留原始类型。

    1
    2
    3
    4
    5
    6
    type NonNullableFlat<O> = {
    [K in keyof O]: NonNullable<O[K]>;
    } & {};

    type c = NonNullableFlat<undefined>; // undefined
    type d = NonNullableFlat<null>; // null

条件类型

  • 条件类型会引入局部作用域。

    1
    2
    3
    type z = [1, 2, 3, 4, 5];
    // B 的定义仅在 true 分支中存在。
    type V = z extends [any, ...infer B] ? 1 : B;

IP地址、子网掩码、网关

什么是IP地址?

假如有一群人在一个密闭房间里面,用什么方式能够快速的叫到某一个人呢?没错,一个简单的方式,就是每个人编一个号码,例如,叫到一号,就知道是谁了。

在网络世界中也是一样的,要想快速访问某一台设备,就需要每台设备有一个编号,而这个编号就是网络设备的IP地址。在这个房间里面,如果有两个人的编号相同,那么会怎么样,肯定就是当叫到这个编号的时候,不知道叫的是谁,所以一个房间里面不允许有两个编号相同的人,在一个局域网里面不允许有两个IP地址相同的设备,如果有就被称为IP冲突,会严重危害到网络的稳定。

扩展一下,在一栋大楼里面,有好多个这样的密闭房间,每个密闭的房间也都有一群人,那么要怎么定位到某一个房间里面的一个人呢?

答案肯定也还是编号,给每个房间编号,例如1号房间里面的1号,这样就能定位到特定的那一个人了,这时候我们把房间号也加入到人的编号当中去,房间号和人的编号用一个”.”间隔开来,例如1.1号,说明就这个人就是1号房间里面的1号人。

网络设备中的IP地址也是如此,例如192.168.1.100,我们可以这样理解,192.168.1号房间,也就是我们会提到的网段,100就是在这个网段里面的编号100的设备。

什么是子网掩码?

根据上面提出的编号:192.168.1.100,会引发一个新的疑问,为什么房间号是:192.168.1,而人的编号是100,可不可以把房间号设置成为192.168,人的编号设置成为1.100呢?

答案当然是可以的,但是这样设置会引发一个问题,同样192.168.1.100这个编号就会有歧义,可以表示192.168.1房间里面的100号人,也可以表示192.168号房间里面的1.100号人,这时候就要引入另外一个规则,告诉人们多少就是房间编号,多少就是人的编号,而这个规则就是子网掩码。

都知道网络时间就是数字世界,所以这个规则设计得很讨巧,长度设置和编号一样长,通过和编号的于运算,最后告诉人们那些是房间号,那些事是人的编号。

举个例子:子网掩码是255.255.255.0这个最常用的规则意思是255.255.255这前三位是房间号,后面0那一位是人编号,再比如192.168.1.100这个IP地址和255.255.0.0这个子网掩码,说明192.168是房间号,也就是网段,而1.100是人的编号,也就是设备在这个网段的编号。

换句话说:子网掩码的作用就是确定设备出于哪个网段。子网掩码 255.255.0.0 表示前两位是网络部分,后两位是主机部分. 所以子网掩码相同且前两位相同的IP (例如:192.168.0.12 与 192.168.1.13) 在同一网段.

什么是网关?

接着上面的问题,一群人在一个密闭房间里面,已经每一个人都有了一个编号,就是网络设备中的IP地址,那么这时候需求升级了,房间里面的人需要和房间外面的人们进行沟通对话,这时候怎么办呢,就需要一个会穿墙术的超能力者当传话筒,在这个房间里面穿梭,把房间里面的人的话传到外面去,把外面的人回应传回给房间里面的人。

这个有超能力的人就是网络世界中的网关,他负责把内部网络的讯息传递到外网, 把外网信息传递回来,对于一个家庭网络而言,这个角色不正是我们的路由器吗?

路由器是唯一一个和宽带连接的设备,家里所有的设备都要经过路由器才能连接到宽带,进行上网冲浪。所以网关也就是我们家庭宽带网络中的路由器,如果网关设置错误,就好像你把要传递出去的话语传给了一个没有超能力的人,自然也就无法把话语传达到房间外面。

所以这里所说的穿墙只的就是两个设备不在同一网段,如果在同一网段就可以利用交换机通信,如果不是就需要路由器,也就会用到网关的概念.

网关就是跨网段通讯的“关口”。当数据包从本网段设备传输到路由器时,首先要保证网关一致。网关可以使用本网段中任何一个地址,但是有一个不成文的规定,一般网关使用本网段中第一个可用的地址,这是为了避免与局域网中其他设备IP产生冲突。所以上面例子中的局域网1中,所有设备的网关都应该是10.0.0.1。局域网2中所有设备的网关都应该是12.0.0.1。有了网关,局域网内的设备才可以通过路由器与其他网段的设备进行通讯。

VMWare 使用SSH链接

SSH客户端与服务端

openssh-client 客户端,如果想通过 ssh 链接其他服务器需要安装客户端

ssh-server 服务端,如果想让其他机器通过 ssh 链接本机,需要在本机开机 ssh 服务,即安装服务端

Ubuntu server版 默认没有安装 ssh-client
Ubuntu 桌面版 默认没有安装 ssh-server

Ubuntu 安装 ssh 客户端或服务端

1
2
3
4
$ sudo apt-get update //更新软件源
$ sudo apt-get install openssh-client //安装openssh-client
$ sudo apt-get install openssh-server //安装openssh-server
$ sudo service ssh start //启动ssh服务

centOS 安装 ssh

1
2
3
4
5
# 搜索 ssh 包名
yum search openssh

yum install openssh-clients.x86_64
yum install openssh-server.x86_64

检查虚拟机是否安装了 ssh-server

1
2
ps -e | grep ssh
# 1512 00:00:00 sshd

VMWare 配置

查询 IP

查询宿主机和虚拟机的 ip 备用

宿主机IP

虚拟机IP

建立映射

接下来就需要将宿主机和虚拟机的IP映射起来。

打开VMware的虚拟网络编辑器(编辑>虚拟网络编辑器):

检查子网Ip(Subnet IP) 和 子网掩码(Subnet mask), 正常情况下无需修改

如果保存时报错 子网ip和子网掩码不匹配,请检查子网IP, 格式为 xxx.xxx.xxx.0 他与子网掩码 255.255.255.0 对应

不可以写为 xxx.xxx.xxx.120 等其他数字,这表示具体子网中的一个网络设备,并不是子网IP

配置完成后可以使用 ssh 工具链接

1
ssh root@192.168.255.128

MySQL 练习题

创建表结构

  • 学生表 Student
1
2
3
4
5
6
7
8
9
create table Student(Sid varchar(6), Sname varchar(10), Sage datetime, Ssex varchar(10));
insert into Student values('01' , '赵雷' , '1990-01-01' , '男');
insert into Student values('02' , '钱电' , '1990-12-21' , '男');
insert into Student values('03' , '孙风' , '1990-05-20' , '男');
insert into Student values('04' , '李云' , '1990-08-06' , '男');
insert into Student values('05' , '周梅' , '1991-12-01' , '女');
insert into Student values('06' , '吴兰' , '1992-03-01' , '女');
insert into Student values('07' , '郑竹' , '1989-07-01' , '女');
insert into Student values('08' , '王菊' , '1990-01-20' , '女')
  • 成绩表 SC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create table SC(Sid varchar(10), Cid varchar(10), score decimal(18,1));
insert into SC values('01' , '01' , 80);
insert into SC values('01' , '02' , 90);
insert into SC values('01' , '03' , 99);
insert into SC values('02' , '01' , 70);
insert into SC values('02' , '02' , 60);
insert into SC values('02' , '03' , 80);
insert into SC values('03' , '01' , 80);
insert into SC values('03' , '02' , 80);
insert into SC values('03' , '03' , 80);
insert into SC values('04' , '01' , 50);
insert into SC values('04' , '02' , 30);
insert into SC values('04' , '03' , 20);
insert into SC values('05' , '01' , 76);
insert into SC values('05' , '02' , 87);
insert into SC values('06' , '01' , 31);
insert into SC values('06' , '03' , 34);
insert into SC values('07' , '02' , 89);
insert into SC values('07' , '03' , 98)
  • 课程表 Course
1
2
3
4
create table Course(Cid varchar(10),Cname varchar(10),Tid varchar(10));
insert into Course values('01' , '语文' , '02');
insert into Course values('02' , '数学' , '01');
insert into Course values('03' , '英语' , '03')
  • 教师表 Teacher
1
2
3
4
create table Teacher(Tid varchar(10),Tname varchar(10));
insert into Teacher values('01' , '张三');
insert into Teacher values('02' , '李四');
insert into Teacher values('03' , '王五')

练习题

查询” 01 “课程比” 02 “课程成绩高的学生的信息及课程分数
查询平均成绩大于等于 60 分的同学的学生编号和学生姓名和平均成绩
查询在 SC 表存在成绩的学生信息
查询所有同学的学生编号、学生姓名、选课总数、所有课程的总成绩(没成绩的显示为 null )
查询姓李的老师数量
学过”张三”老师授课的同学的信息
没有学过”张三”老师授课的同学的信息
查询学过编号为”01”并且也学过编号为”02”的课程的同学的信息
查询学过编号为”01”但是没有学过编号为”02”的课程的同学的信息
查询没有学全所有课程的同学的信息
查询至少有一门课与学号为 01 同学所学相同的同学的信息
查询和 01 号的同学学习的课程,完全相同的其他同学的信息
查询所有同学最高分对应的学科名称

查询”01“课程比”02”课程成绩高的学生的信息及课程分数
1
2
3
4
5
6
7
8
9
10
11
12
13
select Sname as 姓名,t1.score as "语文" , t2.score as '数学'
from
(select SId ,score from SC as sc1 where sc1.CId='01') as t1,
(select SId ,score from SC as sc2 where sc2.CId='02') as t2,
(Select * from Student) as t3
where t1.SId=t2.SId and t2.SId =t3.SId and t1.score>t2.score;


select st.*, sc1.score as '语文',sc2.score as '数学'
from Student as st
left join SC as sc1 on st.SId = sc1.SId and sc1.CId ='01'
left join SC as sc2 on st.SId = sc2.SId and sc2.CId ='02'
where sc1.score > sc2.score;
查询平均成绩大于等于 60 分的同学的学生编号和学生姓名和平均成绩
1
2
3
4
5
select s.SId ,s.Sname,AVG(s2.score) avg_score
from Student s
left join SC s2 on s2.SId = s.SId
group by s.SId
having avg_score >= 60;

需要注意 where 和 having 的区别, where 是分组前筛选,所以一定写在 group by 的前面, having 是分组后筛选,这道题是想查找求完平均分数之后的结果

查询在 SC 表存在成绩的学生信息

第一种方法:将学生表和成绩表关联,过滤出没有成绩的条目,再用学生 id 分组

1
2
3
4
5
select s.*
from Student s
left join SC s2 on s2.SId = s.SId
where s2.score is not NULL
group by s.SId ;

第二种方法: 先将成绩表按学生 id 分组,再查询学生 id 在分组后的临时表中的学生信息

1
2
3
4
5
6
7
select s.*
from Student s
where s.SId in (
SELECT s.SId
from SC s
group by s.SId
);
查询所有同学的学生编号、学生姓名、选课总数、所有课程的总成绩(没成绩的显示为 null )
1
2
3
4
select s.SId ,s.Sname, COUNT(s2.CId),sum(s2.score)
from Student s
left join SC s2 on s2.SId = s.SId
group by s.SId ;
查询姓李的老师数量
1
2
3
select COUNT(*) as 数量
from Teacher t
where t.Tname LIKE "李%";
学过”张三”老师授课的同学的信息
1
2
3
4
5
select s.*,t.Tname  from Student s
inner join SC s2 on s.SId = s2.SId
inner join Course c on s2.CId = c.CId
inner join Teacher t on t.TId = c.TId
where t.Tname = "张三";
没有学过”张三”老师授课的同学的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

-- 排除学过张三课的同学,剩下的就是没学过张三课的同学
select s.* from Student s
WHERE s.SId not in (
select s.SId from Student s
inner join SC s2 on s.SId = s2.SId
inner join Course c on s2.CId = c.CId
inner join Teacher t on t.TId = c.TId
where t.Tname = "张三"
);

-- 查找每条成绩对应的课程信息,查找这些信息中是张三老师课的信息
-- 在查找这些信息对应的学生信息,并排除

SELECT s.* FROM Student s
WHERE s.SId NOT IN (
SELECT s2.SId from SC s2
inner JOIN Course c2 on c2.CId = s2.CId
WHERE c2.TId = (
SELECT t.TId from Teacher t
WHERE t.Tname = "张三"
)
);

查询学过编号为”01”并且也学过编号为”02”的课程的同学的信息
1
2
3
4
5
6
7
8
9
10
-- 利用inner join 过滤没有匹配结果的条目
SELECT * from Student s
inner join SC s2 on s2.SId =s.SId and s2.CId = "01"
inner join SC s3 on s3.SId =s.SId and s3.CId = "02";

-- 先分组在查询个数
select s.* from Student s
inner join SC s2 on s2.SId = s.SId
GROUP BY s.SId
HAVING sum(IF(s2.CId="01" or s2.CId="02",1,0)) >1;
查询学过编号为”01”但是没有学过编号为”02”的课程的同学的信息
1
2
3
4
5
6
SELECT  * from Student s
inner join SC s2 on s2.SId =s.SId and s2.CId = "01"
WHERE s.SId not in (
SELECT s.SId from Student s
inner join SC s2 on s2.SId =s.SId and s2.CId = "02"
);
查询没有学全所有课程的同学的信息
1
2
3
4
5
6
select s.* from Student s
left join SC s2 on s2.SId = s.SId
GROUP by s.SId
HAVING COUNT(*) < (
SELECT COUNT(*) from Course c
);
查询至少有一门课与学号为 01 同学所学相同的同学的信息
  1. 查找学号 01 同学学过的科目
  2. 查找每个同学学过的科目,排除 01 同学自己
  3. 查找每个同学学过的科目在 01 同学学过的科目中的同学
  4. 去除重复的同学信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
select s.* from Student s
left join SC s2 on s2.SId = s.SId
where s2.CId in (
select s2.CId from Student s
inner join SC s2 on s2.SId = s.SId and s2.SId ='01'
) and s.SId <> '01'
group by s.SId;

-- 优化查询01同学学过的科目,直接从成绩表中查
-- 使用distinct去重
Select distinct sc.SId, st.* from SC as sc
Join Student st
On sc.SId = st.SId and st.SId <> '01'
Where sc.CId in (Select CId from SC where SId = '01')
查询和 01 号的同学学习的课程,完全相同的其他同学的信息
  1. 查找所有同学学过的科目是在 01 同学学过的科目中的同学
  2. 学过和 01 同学相同课程的科目数时候和 01 同学的科目数相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
select s.* from Student s
join SC s2 on s2.SId = s.SId and s.SId <> '01' and s2.CId in (select s3.CId from SC s3 where s3.SId = '01')
group by s.SId
HAVING count(s.SId) = (
select count(*) from SC s3 where s3.SId = '01'
)


-- 也可以使用group concat 对比课程id
-- join的时候无需确认是否课程是01学过的课程


select s.* from Student s
join SC s2 on s2.SId = s.SId and s.SId <> '01'
group by s.SId
HAVING GROUP_CONCAT(s2.CId order by s2.CId desc) = (
select GROUP_CONCAT(s3.CId order by s3.CId desc) from SC s3 where s3.SId ='01'
)
查询所有同学最高分对应的学科名称
1
2
3
4
5
6
7
8
9
select * from (
select s.SId ,s.Sname ,t.CId,t.score,t.row from Student s
left join (
select s2.*,
row_number () over (partition by s2.SId order by s2.score desc) as row
from SC s2
) as t on s.SId = t.SId
left join Course c on c.CId = t.CId
) as t where t.row = '1'

TypeScript 练习题

实现 Pick
实现 Readonly
元组转换为对象
第一个元素
实现 Exclude
Promise 返回值类型
实现 Array.Concat
实现 Array.includes
实现 Parameters
实现 ReturnType
实现 Omit
Pick Readonly
Deep Readonly
链式调用的类型
Promise.all
Type Lookup
Trim
Type Replace
追加参数
Flatten
AppendToObject
数字转字符串
StringToUnion
MergeKey
CamelCase & KebabCase
Diff
anyOf
isUnion

实现 Pick
1
2
3
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

利用 keyof 将对象类型转换成键值的联合类型

利用 extends 进行泛型约束, K 可以分配给 T, 表示 K 是 T 的子集.

利用 in 运算符,遍历联合类型

实现 Readonly
1
2
3
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
元组转换为对象
1
2
3
4
5
6
7
8
const tuple = ["tesla", "model 3", "model X", "model Y"] as const;

type TupleToObject<T extends readonly any[]> = {
[K in T[number]]: K;
};

type result = TupleToObject<typeof tuple>;
// expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

因为 in 运算符可以遍历联合类型,所以把元组 T 转换成联合类型,在进行遍历

第一个元素

实现一个通用 First,它接受一个数组 T 并返回它的第一个元素的类型。

1
2
3
4
5
6
7
8
9
type arr2 = [3, 2, 1];

type First<T extends readonly any[]> = T[0];

type First<T extends readonly any[]> = T extends [infer F, ...infer R]
? F
: never;

type head1 = First<arr1>; // expected to be 'a'

利用条件语句中 infer 类型推断,返回第一个元素所代表的类型

实现 Exclude

Exclude 的用法是从联合类型中,排除指定的属性

1
type Exclude<T, U> = T extends U ? never : T;

extends 条件类型, T 是否能分配给 U, 会去拿 T 中的每一项与 U 进行匹配, 如果当前项可以分配,表示 U 中存在这种类型,需要排除,所以返回 never. 如果不存在则返回这一项的类型.

Promise 返回值类型
1
type Awaited<T extends Promise<any>> = T extends Promise<infer R> ? R : T;
实现 Array.Concat
1
type Concat<T extends any[], U extends any[]> = [...T, ...U];
实现 Array.includes
1
type Includes<T extends any[], U> = U extends T[number] ? true : false;

利用 extends 条件类型可以进行联合类型的判断,, 首先吧元组转换为联合类型, 如果类型可分配表示 U 存在与元组中.

实现 Parameters

Parameters 作用是用于获得函数的参数类型组成的元组类型。

1
2
3
4
5
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
实现 ReturnType
1
2
3
4
5
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
实现 Omit
1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Pick Readonly

指定属性 ReadOnly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type PickReadonly<T, K extends keyof T = keyof T> = {
[Key in Exclude<keyof T, K>]: T[Key];
} & {
readonly [Key in K]: T[Key];
};

interface Todo {
title: string;
description: string;
completed: boolean;
}

const todo: MyReadonly2<Todo, "title" | "description"> = {
title: "Hey",
description: "foobar",
completed: false,
};

todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property
todo.completed = true; // OK
Deep Readonly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type X = {
x: {
a: 1;
b: "hi";
};
y: "hey";
};

type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

type Todo = DeepReadonly<X>; // should be same as `Expected`

type Expected = {
readonly x: {
readonly a: 1;
readonly b: "hi";
};
readonly y: "hey";
};
链式调用的类型

假设 key 只接受字符串而 value 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 key 只会被使用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Chainable<T = {}> = {
option: <K extends string, V>(k: K, v: V) => Chainable<T & { [P in K]: V }>;
get: () => T;
};

declare const config: Chainable;

const result = config
.option("foo", 123)
.option("name", "type-challenges")
.option("bar", { value: "Hello World" })
.get();

// 期望 result 的类型是:
interface Result {
foo: number;
name: string;
bar: {
value: string;
};
}

实现 Promise.all

ts 允许像遍历一个对象一样遍历类数组

1
2
3
4
5
6
7
8
9
10
11
12
13
type Awaited<T> =
T extends null | undefined ? T :
// special case for `null | undefined` when not in `--strictNullChecks` mode
T extends object & { then(onfulfilled: infer F): any } ?
// `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
F extends ((value: infer V, ...args: any) => any) ? // if the argument to `then` is callable, extracts the first argument
Awaited<V> :
// recursively unwrap the value
never :
// the argument to `then` was not callable
T;
// non-object or non-thenable
type PromiseAll<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;

Type Lookup

1
2
3
type LookUp<U, T extends string> = {
[K in T]: U extends { type: T } ? U : never;
}[T];

实现 Trim

类型推断可以用于字符串

1
2
3
4
5
type Trim<T extends string> = T extends ` ${infer R}`
? Trim<R>
: T extends `${infer R} `
? Trim<R>
: T;

Type Replace

ts 没有 indexOf 的能力, 通过条件类型判断两个类型是否匹配

1
2
3
4
5
type Replace<
T extends string,
P extends string,
U extends string
> = T extends `${infer F}${P}${infer R}` ? `${F}${U}${R}` : T;

追加参数

1
2
3
4
5
type AppendArgument<F extends (...args: any[]) => any, P> = F extends (
...args: infer R
) => infer L
? (...args: [...R, P]) => L
: F;

Flatten

1
2
3
4
5
6
7
8
type Flatten<T extends any[], A extends any[] = []> = T extends [
infer F,
...infer R
]
? F extends any[]
? Flatten<R, Flatten<F, A>>
: Flatten<R, [...A, F]>
: A;

AppendToObject

对象 Key 的类型约束,有两种方式

1
2
3
4
5
6
// K extends PropertyKey 其中 PropertyKey 为内置属性
// K extends keyof any

type AppendToObject<T, K extends PropertyKey, V> = {
[Key in keyof T | K]: Key extends keyof T ? T[Key] : V;
};

另一种是现实是重新遍历一次组合后的对象

1
2
3
4
5
6
7
type MapKey<T> = { [K in keyof T]: T[K] };

type AppendToObject<T, K extends keyof any, V> = MapKey<
T & {
[K1 in K]: V;
}
>;

数子转字符串

1
type Test<T extends number> = `${T}`;

可以把非字符串类型转为字符串,利用类型系统处理

1
2
3
4
5
type Test<T extends number | string | bigint> = `${T}` extends `-${infer R}`
? R
: T;

type dd = Test<"-123">; //123

StringToUnion

1
2
3
4
5
6
7
8
9
10
11
type StringToUnion<
T extends string,
A extends string[] = []
> = T extends `${infer F}${infer R}` ? StringToUnion<R, [...A, F]> : A[number];

type StringToUnion<
T extends string,
A = never
> = T extends `${infer F}${infer R}` ? StringToUnion<R, A | F> : A;

type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"

MergeKey

1
2
3
4
5
6
7
8
9
10
11
12
13
type Merge<T, P extends { [k in PropertyKey]: any }> = {
[K in keyof foo | keyof coo]: (foo & coo)[K] extends never
? P[K]
: (foo & coo)[K];
};

type Merge<F, S> = {
[K in keyof F | keyof S]: K extends keyof S
? S[K]
: K extends keyof F
? F[K]
: never;
};

CamelCase & KebabCase

aa-bb-cc => aaBbCc

1
2
3
4
5
6
7
type CamelCase<T extends string> = T extends `${infer F}-${infer D}${infer R}`
? CamelCase<`${F}${Uppercase<D>}${R}`>
: T;

type CamelCase<T extends string> = T extends `${infer F}-${infer D}`
? CamelCase<`${F}${Capitalize<D>}`>
: T;

AaBbCc => aa-bb-cc

1
2
3
4
5
6
7
8
9
10
type KebabCase<
T extends string,
P extends string = ""
> = T extends `${infer F}${infer R}`
? Lowercase<F> extends F
? KebabCase<R, `${P}${F}`>
: KebabCase<R, `${P}-${Lowercase<F>}`>
: P extends `-${infer R}`
? R
: never;

Diff

1
2
3
4
5
6
7
8
9
type Diff<T extends object, P extends object> = {
[K in
| Exclude<keyof T, keyof P>
| Exclude<keyof P, keyof T>]: K extends keyof T
? T[K]
: K extends keyof P
? P[K]
: never;
};

anyOf

实现一个类型,接受一个元组,如果元组中的每一个都为 false,返回 false,有一个为 true 则返回 true

需要注意联合类型是通过 T[number] 得到的,并不会进行条件类型分配,所以当联合类型可以分配给指定联合类型, 也就是联合类型中的每一个都在指定的联合类型中的时候返回 false

1
2
3
4
5
6
7
8
type AnyOf<T extends any[]> = T[number] extends
| ""
| 0
| false
| []
| Record<any, never>
? false
: true;

isUnion

利用 分配条件类型,联合类型在条件分配后会被转换为复合联合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type IsUnion<T, O = T> = T extends O ? ([O] extends [T] ? false : true) : never;

type case1 = IsUnion<string>;
type case2 = IsUnion<string | number>; // true
type case3 = IsUnion<[string | number]>; // false

// 利用分配条件类型把基本类型的联合类型转换为符合类型的联合类型

type Test<T, O = T> = T extends O ? [T] : never;

type case1 = Test<string>; //[string]
type case2 = Test<string | number>; // [string] | [number]
type case3 = Test<[string | number]>; // [[string|number]]

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信