Tree Shaking 过程分析

什么是 Tree Shaking

Tree Shaking 是 DCE(Dead Code Elimination) 的一种实现,即清除无用代码,这个功能最早是在 Rollup 中实现的,随后 webpack 在 2.0 版本中也实现了此功能

基本使用

1
2
3
4
5
6
module.exports = {
mode: "production",
optimization: {
usedExports: true,
},
};

webpack4 添加了 sideEffects 配置属性,用于声明那些模块是没有副作用的,从而可以安全的移除.

如上面的例子中模块中包含两个纯函数,所以可以设置 sideEffects 为 false, 可以安全的删除 square

1
2
3
4
{
"name": "your-project",
"sideEffects": false
}

sideEffects 可能会比 usedExports 更加有效,因为它是声明式的告诉,那些模块/文件可以跳过.

usedExports 依赖 terser 来检测语句中的副作用。对于如下的模块,默认不会被移除,因为不确定其中是否有副作用.虽然可以使用标注来解决,但是 sideEffect 使用的更多.

1
2
3
4
5
// a.js
class A {}
Array.prototype.slice = () => {};
// 表示一个无副作用的模块,可以删除
export default /*#__PURE__*/ A;

实现原理

Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中
Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用
生成产物时,若变量没有被其它模块使用则删除对应的导出语句

Make 收集阶段

这个阶段需要分析每个文件中有那些模块被导出,把这些导出模块转换为 webpack 内部对象,并添加到当前模块依赖中

1
2
3
4
export const bar = "bar";
export const foo = "foo";

export default "foo-bar";

最终转换为三个内部对象

在编译结束之后会触发回调,这时会遍历 dependencies 数组,找到所有的导出对象转换为 ExportInfo 记录在 ModuleGraph 中,至此 webpack 可以直接访问各模块的导出值.

Seal 标记阶段

标记的主要作用就是删除没有使用模块的导出语句,可以看见对于导出但是没有使用的模块会添加未使用的标记,并且不会被导出,但是标记阶段不会删除代码,删除的过程是 Terser 等压缩工具实现的

1
2
3
4
5
6
7
8
9
10
11
12
/***/ (function (module, __webpack_exports__, __webpack_require__) {
"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
return x * x;
}

function cube(x) {
return x * x * x;
}
});

这一阶段会触发 optimizeDependencies,从 entry 入口文件开始遍 ModuleGraph 所有的模块

接着遍历所有 exportInfo 数组,为每一个 exportInfo 执行 getDependencyReferencedExports 方法,确定其对应的 dependency 对象有否被其它模块使用

被任意模块使用到的导出值,调用 exportInfo.setUsedConditionally 方法将其标记为已被使用,内部修改 exportInfo._usedInRuntime 记录导出如何被使用

这一过程是通过 FlagDependencyUsagePlugin 插件完成的

生成代码

调用 HarmonyExportXXXDependency.Template.apply 生成代码,方法内部会读取 exportsInfo, 分别为使用和未使用的导出值创建不同的 HarmonyExportInitFragment, 保存到 initFragments 数组并循环生成代码.

最终的的无用代码会被压缩工具删除.

对比 Rollup

与 webpack 相同 Rollup 也是基于 ES 模块化做静态分析, 但是由于打包方式不同,Rollup 的打包文件在同一作用域下,所以还可以进行流程分析.

Rollup 可以删除被引用但是没有使用的模块,但也不是所有的副作用 Rollup 都可以处理,包括:

未使用的 class 中的方法(可以通过 enhancement 配置解决)

对全局对象的赋值 window.a = 1

最佳实践

  • 避免无意义的赋值语句
1
2
3
4
5
6
7
// math.js
export const a = 1;
export const b = 2;

// index.js
import { a, b } from "./index";
const f = a;

通过静态分析只能判断:模块导出变量是否被其它模块引用,或引用模块的主体代码中有没有出现这个变量

另外最重要的是因为 JS 这种动态类型语言引用的 a 可能存在副作用

  • 必要的时候使用 /*__PURE__*/ 纯函数标注

  • 禁用 babel 的导入导出语句转换

    当 babel 的 module 配置为 commonjs 的时候,将不能正确识别未使用的模块

    1
    2
    3
    4
    5
    6
    presets: [
    "babel-preset-env",
    {
    modules: "commonjs",
    },
    ];
  • 将”sideEffects”属性添加到项目 package.json 文件中。

  • 使用明确的导出语句

1
2
3
4
5
6
7
8
export default {
bar: "bar",
foo: "foo",
};

// 修改为
export bar;
export foo;
  • 使用支持 Tree Shaking 的工具包

例如:使用 lodash-es 替代 lodash ,或者使用 babel-plugin-lodash 实现类似效果

Rollup 与 Webpack 比较

跟随前端技术的演化

当前端演化到单页应用阶段的时候,对于复杂的单页应用有两个问题需要解决,而 Webpack 就是在这时产生的.

  • 代码分割

这意味着可以按需加载,不用再等待整个应用都被下载并解析完成。

  • 静态资源

图片、CSS 等静态资源可以直接导入到你的 app 中,就和其它的模块、节点一样能够进行依赖管理。可以放在任意的文件夹中, Webpack 会帮助处理文件路径, 包括添加哈希值,最终输出在指定的文件夹中.

包括 Webpack 在内的大多数打包器,都是将模块封装在函数中,通过 Webpack 实现的 require 方法,组织模块间的调用.

而 Rollup 利用了 ES6 的模块设计,将所有的模块的代码都放在同一个位置, 因此更加精简执行速度也会更快. Rollup 交互式解释器(REPL)

但是同时这样也导致 Rollup 失去了代码分割的功能, 也不支持模块热替换(HMR),另一个痛点是通过插件处理大多数 CommonJS 文件的时候,一些代码将无法被翻译为 ES2015。而与之相反 Webpack 可以很好而处理.

如何选择

在开发应用时使用 Webpack,开发库时使用 Rollup

这虽然不是绝对的,但是很多开源项目提供有经验,如果有很多的静态资源,再或者你做的东西深度依赖 CommonJS,毫无疑问选择 Webpack.

如果你的代码基于 ES2015 模块编写,并且你做的东西是准备给他人使用的,你或许可以考虑使用 Rollup。

对于包作者一定使用 pkg.module

对于第三方开源库,在 ES6 模块化规范出现以前,一定要注意模块系统的区别,有人喜欢 Browserify 有人喜欢 AMD,在 UMD 出现之后有了一些改善,但是仍然无法完全信任.

现在给你的库的 package.json 文件增加一个 “module”: “dist/my-library.es.js” 入口,可以让你的库同时支持 UMD 与 ES2015。 Webpack 和 Rollup 都使用了 pkg.module 来尽可能的生成效率更高的代码——在一些情况下,它们都能使用 tree-shake 来精简掉你的库中未使用的部分。

React v16 源码分析 ⑨ 组件生命周期

从横向的结构分析, 生命周期包括 Render phase Pre-commit phase Commit phase 这些生命周期是与代码逻辑相符的,很容易找到类型名称的函数调用

纵向的 Mounting Updating Unmounting 是从 DOM 的表现层面划分的,真正对 DOM 的处理分散在上面的不同阶段之中

下面以横向的结构划分,也就是以代码的执行逻辑分析不同阶段的生命周期方法

Render

Render 阶段也就是 Fiber 树 构建的阶段, 对于首次渲染的节点会创建新的 FiberNode, 对于更新的节点会检查是需要新建还是复用

在构建 Fiber 树的过程中,会在每个节点处理之前调用 createWorkInProgress,当发现此节点还没有创建,会调用 createFiber 创建对应的 FiberNode

将 FiberNode 传入 beginWork 按 Fiber 的 tag 类型,调用不同的方法处理, 如果 Fiber 的类型是 class component 会调用 constructClassInstance 实例化

紧接着会执行 getDerivedStateFromPropscomponentWillMount, 因此这两个生命周期在挂载阶段和更新阶段是都会执行的

1
2
3
4
5
6
7
8
9
10
11
function constructClassInstance(workInProgress, ctor, props) {
var instance = new ctor(props, context);

if (typeof getDerivedStateFromProps === "function") {
getDerivedStateFromProps(nextProps, prevState);
}

if (typeof instance.componentWillMount === "function") {
instance.componentWillMount();
}
}

如果是函数组件则会直接执行

1
var children = Component(props, secondArg);

实例化类组件之后会执行 finishClassComponent, 在这里会执行类组件的 render 方法

1
2
3
function finishClassComponent() {
instance.render();
}

如果节点已经挂载过, 类组件会进入更新的逻辑,这里会执行 shouldComponentUpdate 并返回一个 boolean, 用作是否执行 componentWillUpdate() render() componentDidUpdate() 的依据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function updateClassInstance() {
var shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext
);
if (shouldUpdate) {
if (typeof instance.componentWillUpdate === "function") {
instance.componentWillUpdate(newProps, newState, nextContext);
}
if (typeof instance.componentDidUpdate === "function") {
workInProgress.flags |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === "function") {
workInProgress.flags |= Snapshot;
}
}
}

Pre-commit

Pre-commit 阶段实际就是 commit 阶段 前半部分

其中 getSnapshotBeforeUpdate 将会被执行

Commit

指的就是 commit 阶段 后半部分

在这个阶段会真实的操作 DOM,并且重新绑定 Ref

可以看到 componentDidMount componentDidUpdate 会在这个阶段执行

其中 commitMutationEffects 阶段会对标记删除的节点执行 commitDeletion, componentWillUnmount 会在这个函数中执行

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。有了网关,局域网内的设备才可以通过路由器与其他网段的设备进行通讯。

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信