LeetCode 二叉树展开为链表

二叉树展开为链表

思考

可以很自然想到的是需要一个递归,因为他需要先序遍历
我们会直接进入到每个左边子树的左节点。
但是这经过的右子树是暂时用不到的,所以需要想办法把他们存起来,在稍后使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const flatten = (root) => {
if (root === null) return root;

const stack = [];
const dig = (root) => {
if (root === null) return;
const right = root.right;
root.right = null;
// 暂存右节点
if (right !== null) stack.push(right);

// 将左节点放到有节点上
// 并先序遍历继续递归向下
if (root.left !== null) {
root.right = root.left;
root.left = null;
dig(root.right);
} else {
// 如果已经没有下面的节点
// 从队列中取出最后的右子树遍历
const next = stack.pop();
if (next == undefined) return;
root.right = next;
dig(next);
}
};
dig(root);
return root;
};

还可以思考用 O(1) 的空间复杂度,解决这个问题。既然不能用额外的储存空间,那么可以考虑将用到的信息放在树的节点上操作。

题目要求是以先序遍历的顺序,并且通用右指针相连,对于右指针相连这个问题我们可以弱化它,因为这可以通过节点交换很容易的完成。

而以先序遍历顺序返回,并不意味着一定要先访问根节点,只要处理之后的链表的顺序是先序遍历就可以了,所以对于一个只有三个节点的子树来说,将右节点接在左节点的后面就是先序遍历。

那要如何保证在访问根节点的时候,左右子树都是一个处理好的链表,我们会想到后序遍历,因为在访问某个节点的时候,他的左右子树都已经在后序遍历的代码位置处理成了链表,可以直接拼接。

所以可以写出下面的框架

1
2
3
4
5
6
7
8
9
10
11
12
13
const dig = (root) => {
if (root === null) return;
dig(root.left);
dig(root.right);

// 处理左右节点
};

const flatten = (root) => {
if (root === null) return root;
dig(root);
return root;
};

思考一些具体的细节,当一个节点的左节点不存在的时候,可以直接跳过,因为它只有一个右节点的话不需要处理,或者右节点已经处理成一个链表。

而当一个左节点存在的时候,需要能获取到左节点这个链表中的最后一个,才能和右节点相连,最后还需要交换左右节点。

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
const dig = (root) => {
if (root === null) return;
dig(root.left);
dig(root.right);

let last = root.left;
// 左节点不存在,直接返回
if (last === null) return;

// 获取最后一个节点
while (last.right) {
last = last.right;
}

// 交换左右子树
last.right = root.right;
root.right = root.left;
root.left = null;
};

const flatten = (root) => {
if (root === null) return root;
dig(root);
return root;
};

LeetCode 二叉树直径

二叉树直径

思考

可以观察到两个现象

  • 直径可能并不会经过根节点
  • 所谓的直径就是某个节点的左子树深度加右子树深度最长的一条

那既然是获取最长的子树,是不能能套用一个之前的求 二叉树最大深度 的方法, 思考以下可以使用下面这个框架么

这可能行不通,因为提出了 maxDeep 方法导致调用时,只能作用在根节点的左右子树。替他节点无法调用这个模式递归。

1
2
3
4
5
6
7
const maxDeep = (root) => {
if (root === null) return 0;
return Math.max(maxDeep(root.left), maxDeep(root.right)) + 1;
};
var diameterOfBinaryTree = function (root) {
//...
};

既然这样就放到内部去做, 但是这种框架面临的问题是,返回值只有一个,希望可以知道左右子树的最大深度,又要保留着已经遍历过的子树的最大直径。

1
2
3
4
5
6
7
8
var diameterOfBinaryTree = function (root) {
if (root === null) return 0;

let l = diameterOfBinaryTree(root.left);
let r = diameterOfBinaryTree(root.right);

// return Math.max(l + r, l, r) + 1;
};

那就再引入一个额外的变量去保存,最大直径,让递归可以专注与子树的最大深度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var diameterOfBinaryTree = function (root) {
if (root === null) return 0;

let max = 0;

const dig = (root) => {
if (root === null) return 0;
let left = dig(root.left);
let right = dig(root.right);
max = Math.max(max, left + right);
return Math.max(left, right) + 1;
};

dig(root);

return max;
};

再来看一下,其实用前序遍历也可以实现,但是需要每经历一个节点,就要获取以下当前节点的左右子树的最大深度。

这是因为前序遍历只能获取当前节点的信息,而后序遍历则可以携带上一个节点返回的信息。

Ramda Type 方法源码解析

type

toString 方法是Object内部实现的原型方法,实现方式中规定了某种类型的返回值

1
2
3
4
5
6
7
8
9
var type = _curry1(function type(val) {
return val === null
? 'Null'
: val === undefined
? 'Undefined'
: Object.prototype.toString.call(val).slice(8, -1);
});
export default type;

Ramda List 方法源码分析

adjust

判断索引的合法性

定义开始节点,其实就是抽象了计算真实索引的计算

1
2
3
4
5
6
7
8
9
10
var adjust = _curry3(function adjust(idx, fn, list) {
if (idx >= list.length || idx < -list.length) {
return list;
}
var start = idx < 0 ? list.length : 0;
var _idx = start + idx;
var _list = _concat(list);
_list[_idx] = fn(list[_idx]);
return _list;
});

all

_dispatchable

1
2
3
4
5
6
7
8
9
10
11
var all = _curry2(_dispatchable(['all'], _xall, function all(fn, list) {
var idx = 0;
while (idx < list.length) {
if (!fn(list[idx])) {
return false;
}
idx += 1;
}
return true;
}));
export default all;

LeetCode 二叉树最小/大深度

二叉树最小深度

二叉树最大深度

思考

容易想到的是通过 DFS 遍历携带深度信息,记录深度最大值或最小值。

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
var minDepth = function (root) {
if (root === null) {
return 0;
}

// 记录最小深度(根节点到最近的叶子节点的距离)
let minDepthValue = Infinity;

// 记录当前遍历到的节点深度
let currentDepth = 0;

const traverse = function (root) {
if (root === null) {
return;
}

// 前序位置进入节点时增加当前深度
currentDepth++;

// 如果当前节点是叶子节点,更新最小深度
if (root.left === null && root.right === null) {
minDepthValue = Math.min(minDepthValue, currentDepth);
}

traverse(root.left);
traverse(root.right);

// 后序位置离开节点时减少当前深度
currentDepth--;
};

// 从根节点开始 DFS 遍历
traverse(root);
return minDepthValue;
};

也可以想到为了避免全局定义深度变量,可以把深度放到递归的参数中,下面是最大深度的解法:

1
2
3
4
5
6
7
8
9
10
11
const dig = (root, length) => {
if (root == null) return length;
return Math.max(dig(root.left, length + 1), dig(root.right, length + 1));
};
var maxDepth = function (root) {
if (root === null) return 0;

// 由于退出条件是在叶子节点的左或右的null节点
// 因此深度会被多加1, 所以开始计数从0开始,可以满足条件
return dig(root, 0);
};

但是当用这种思路解决最小深度时,就会遇到问题。

因为是求最小深度,因此递归到叶子节点拿到的高度,是一个无效值,它可能被一个更小的值取代。

但是子树如果为 null,不能参与计算,因此下面的逻辑当遍历到叶子节点时,仍然递归进入空节点,导致空的子树被算作 1 的长度 返回错误的结果。

接下来,如果只有左子树,或者右子树,是不会进入退出逻辑的,这会导致递归无法被正确的处理,当只有其中一个子树的时候,要正确处理子树的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const dig = (root, length) => {
// 避免递归进入空节点
// + if(!node.left && !node.right) return deep;
// - if (root == null) return length;

// 当只有一个子树的时候,给空的子树一个默认值 Infinity
// 因为有值的子树,一定会进入推出逻辑返回一个当前深度值

// + let left = Infinity,right=Infinity;
// + if(node.left) left = dfs(node.left, deep + 1);
// + if(node.right) right = dfs(node.right, deep + 1);
// - return Math.min(dig(root.left, length + 1), dig(root.right, length + 1));

return Math.min(left, right);
};
var maxDepth = function (root) {
if (root === null) return 0;
// 从 1 开始计数
// + return dig(root, 1);
// - return dig(root, 0);
};

优化

观察解题思路,是通过传递一个深度参数进行递归,也就是说这是从根节点开始计数,如果从叶子节点开始计数,就可以避免参数的传递。

最大深度计算:

1
2
3
4
var maxDepth = function (root) {
if (root === null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};

最小深度计算:

1
2
3
4
5
6
7
8
9
10
11
12
var minDepth = function (root) {
if (root === null) return 0;

let left = minDepth(root.left);
let right = minDepth(root.right);

// 解决左右子树其中一个为空的情况
if (left === 0) return right + 1;
if (right === 0) return left + 1;

return Math.min(left, right) + 1;
};

对于最小深度,还可以考虑用 BFS 层序遍历来优化,因为最小深度,是要找到第一个叶子节点所在的深度,而不需要像 DFS 那样,每一个分支走到头才知道哪一条路径是最短的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var minDepth = function (root) {
let deep = 0;
if (root === null) return deep;
let queue = [root];
let len;
while ((len = queue.length)) {
deep++;
const q = queue.concat([]);
queue = [];
while (len--) {
const node = q.pop();
if (node.left === null && node.right === null) return deep;

if (node.right) queue.push(node.right);
if (node.left) queue.push(node.left);
}
}
return deep;
};

二叉树

二叉树可以算是最基础的数据类型。

很多复杂的数据结构都是基于二叉树的,比如 红黑树(二叉搜索树)、多叉树、二叉堆、图、字典树、并查集、s 线段树 等等。

二叉树可以代表一种递归的思维方式,比如 回溯算法、BFS 算法、动态规划 本质上也是把具体问题抽象成树结构。

树的概念

1
2
3
4
5
6
7
    1
/ \
2 3
/ / \
4 5 6
/ \
7 8
  • 每个节点下方直接相连的节点称为子节点,上方直接相连的节点称为父节点。比方说节点 3 的父节点是 1,左子节点是 5,右子节点是 6;节点 5 的父节点是 3,左子节点是 7,没有右子节点。

  • 我们称最上方那个没有父节点的节点 1 为根节点,称最下层没有子节点的节点 4、7、8 为叶子节点

  • 从根节点到最下方叶子节点经过的节点个数为二叉树的最大深度/高度,上面这棵树的最大深度是 4,即从根节点 1 到叶子节点 7 或 8 的路径上的节点个数。

  • 满二叉树: 每一层节点都是满的, 假设深度为 h,那么总节点数就是 2^h - 1

    1
    2
    3
    4
    5
    6
    7
           1
    / \
    2 3
    / \ / \
    4 9 5 6
    / \ / \/ \ / \
    10 11 7 12 13 8 14
  • 完全二叉树: 二叉树的每一层的节点都紧凑靠左排列,且除了最后一层,其他每层都必须是满的, 满二叉树就是特殊的完全二叉树。也可以说 完全二叉树的左右子树也是完全二叉树。完全二叉树的左右子树中,至少有一棵是满二叉树。

    1
    2
    3
    4
    5
    6
    7
           1
    / \
    2 3
    / \ / \
    4 9 5 6
    / \ / \
    10 1 7 2
  • 二叉搜索树:对于树中的每个节点,其左子树的每个节点的值都要小于这个节点的值,右子树的每个节点的值都要大于这个节点的值。为左小右大的特性,可以让我们在 BST 中快速找到某个节点,或者找到某个范围内的所有节点,这是 BST 的优势所在。

    1
    2
    3
    4
    5
        7
    / \
    4 9
    / \ \
    1 8 10

遍历方式

层序遍历

一层一层地遍历二叉树,显然先访问的节点需要先处理,而左右节点暂时用不到需要先存起来,自然想到用队列来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var levelOrderTraverse = function (root) {
if (root === null) {
return;
}
var q = [];
q.push(root);
while (q.length !== 0) {
var cur = q.shift();
// 访问 cur 节点
console.log(cur.val);

// 把 cur 的左右子节点加入队列
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
};

这种方式存在的问题就是不知道是第几层,因此可以使用额外的变量来记录。

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
var levelOrderTraverse = function (root) {
if (root === null) {
return;
}
let q = [];
q.push(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
let depth = 1;

while (q.length !== 0) {
let sz = q.length;
// 使用额外的变量,记录每层的节点个数
// 也可以直接复制q队列,这样可以避免 shift 操作的O(n) 时间复杂度
for (let i = 0; i < sz; i++) {
let cur = q.shift();
// 访问 cur 节点,同时知道它所在的层数
console.log("depth = " + depth + ", val = " + cur.val);

// 把 cur 的左右子节点加入队列
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
depth++;
}
};

同理还可以添加更多的下信息,现在只知道整个树的层数,还可以为每个节点维护他的层数, 一般称作路径的权重,即从根节点到当前的节点。

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
function State(node, depth) {
this.node = node;
this.depth = depth;
}

var levelOrderTraverse = function (root) {
if (root === null) {
return;
}
// @visualize bfs
var q = [];
// 根节点的路径权重和是 1
q.push(new State(root, 1));

while (q.length !== 0) {
var cur = q.shift();
// 访问 cur 节点,同时知道它的路径权重和
console.log("depth = " + cur.depth + ", val = " + cur.node.val);

// 把 cur 的左右子节点加入队列
if (cur.node.left !== null) {
q.push(new State(cur.node.left, cur.depth + 1));
}
if (cur.node.right !== null) {
q.push(new State(cur.node.right, cur.depth + 1));
}
}
};
递归遍历

先深入到一个分支中,在逐层的返回。

1
2
3
4
5
6
7
8
9
10
11
// 二叉树的遍历框架
var traverse = function (root) {
if (root === null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
};

但是在不同的位置写代码,获取到的信息是不同的,所谓的前中后,是相对于根节点的先后。

  • 前序遍历: 根节点 => 左节点 => 右节点, 代码位置写在接入左树之前,因此先访问根节点,在访问左树的根节点。对于一个子树,只有左子树遍历完成后,才回去处理右子树,可以看作是进入一个二叉树节点的时候执行

  • 中序遍历: 左节点 => 根节点 => 右节点,代码会一直递归调用到左子树的左叶子节点,当左叶子节点访问完毕后,会弹出当前的调用栈,而上一个调用栈正是左节点的父节点,也就是根节点,可以看作是二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。

  • 后序遍历: 左节点 => 右节点 => 根节点,代码会一直递归调用到左子树的右叶子节点,当左右子节点访问完毕后,会弹出当前的调用栈,而上一个调用栈正是右节点的父节点,也就是根节点,可以看作是离开一个二叉树节点的时候执行。

而递归遍历常用于寻找最短路径, [二叉树最小/大深度], [二叉树直径],递归和遍历各有优势,遍历的思想是配合外部变量,递归则是将问题分解为子问题。

有一些问题虽然是在二叉树的遍历模型下,但是需要根据条件进入不同的分支处理,并在其中穿插其他的逻辑代码。 [二叉树展开为链表] [填充每个节点的下一个右侧节点指针]

二叉树的序列化

层序遍历

这应该是最容易想到的一种方法,就像 LeetCode 题目给出的可视化数据一样,将每一层的数据依次放入到数组中,就可以对二叉树序列化,但是需要注意的是要保留空节点,这样才能描述每一层节点之间的关系

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
var serialize = function (root) {
if (root === null) return [];
const queue = [root];
const res = [];
while (queue.length) {
const first = queue.shift();
res.push(first === null ? first : first.val);
if (first !== null) queue.push(first.left, first.right);
}
return res;
};

var deserialize = function (data) {
if (data.length === 0) return null;
let index = 0;
let root = new TreeNode(data[index++]);
let queue = [root];

while (data[index] !== undefined) {
const node = queue.shift();
const left = data[index++];
const right = data[index++];

node.left = left === null ? left : new TreeNode(left);
node.right = right === null ? right : new TreeNode(right);
if (left !== null) queue.push(node.left);
if (right !== null) queue.push(node.right);
}
return root;
};
二叉树的唯一性

调试npm包

通过软链接使用第三方包

进入本地npm包文件夹,或通过 git clone拉去的第三方包文件夹

执行 yarn linknpm link 连接到全局(yarn 不会污染全局)

在项目中使用 yarn link [第三方包]npm link [第三方包]

在项目中通过 yarn unlink [第三方包]npm unlink [第三方包] 解除链接

通过一下命令去掉全局安装的包

1
2
npm rm --global foo 
npm ls --global foo // 检查包是否被安装

webpack核心概念

安装

推荐就近安装,即安装在项目中,不要安装在全局中

通过 npx webpack -v 查看项目中 webpack 版本

nrm 镜像源管理

yarn add nrm

查看镜像源列表

nrm ls

测速

nrm test taobao

常用工具

clean-webpack-plugin

https://www.npmjs.com/package/clean-webpack-plugin

source-map

cheap-module-source-map 用于生产环境,不能暴露源码

eval-cheap-module-source-map 开发环境中使用

1
2
3
{
devtool:'cheap-module-source-map'
}

devServer 和热模块更新

安装devServer

1
yarn add -D webpack-dev-server

webpack.config.js 中添加配置项

contentBase 只有需要在访问静态文件时候使用,默认下面三个配置项都可以不写

1
2
3
4
5
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000
}

package.json 中添加启动命令

1
2
3
4
5
{
"scripts":{
"server": "webpack-dev-server --open"
}
}

开启hmr

1.配置webpack-dev-server
2.devServer配置hot:true
3.plugins hotModuleeReplaceMentPlugin
4.js 文件中添加hmr代码

webpack.config.js 中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
const webpack = require('webpack')

module.exports = {
devServer: {
...
hot:true
},
plugins: [
...
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
]
}

index.js 增加代码

1
2
3
4
5
6
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}

entry

string : 所有的资源打包成一个chunk,输出一个bundle文件,默认的名称是main.js

array: 多入口,所有的文件也只会被打包成一个chunk,通常只在配置html的HMR时使用

object 多入口,有几个入口文件就可以形成几个chunk,输出几个bundle文件,文件的名称时对象中的key,每个key后面可以写一个数组,可以将数组中的文件打包成一个bundle,(参照dll的用法)

output

filename 输出的文件名称,可以指定目录和文件名 js/[name].js

publicpath 所有资源引入时候的公共路径,会拼在资源路径的前面作为基础路径, publicpath:/,img/a.png => /img/a.png, 注意:不是资源的输出路径

chunkFilename 非入口chunk的名称,通过动态import引入的文件名称通过id的形式命名,从0开始,依次递增,通常会使用webpackchunkname来重命名

library 会将chunk文件用一个变量接受,暴露给全局使用

libraryTarget 指明以那种方式引入,window把输入的变量添加到浏览器全局环境window[name]=xxx, global把输入的变量添加到node全局环境global[name]=xxx,commonjs以commonjs模块化规范引入,通常配合dll使用

与 devserver 中的 publicpath 区别

output 中的 publicpath

这是一个在使用按需加载和引入外部资源(图片,文件等)非常重要的属性,如果设置了一个错误的值,当加载这些资源时会报404错误

这个配置项指定了输出目录在浏览器中引用时的公共路径(publicpath),一个相对路径被解析为相对于HTML页面或<base>标签

标签为页面上的所有链接规定默认地址或默认目标。

相对服务器的路径,相对与协议的路径,或绝对路径都是有可能的甚至有时是必须的,换句话说,在CDN 托管静态资源

在运行时或loader处理时,每一个URL的前缀都会被色设置配置项中的值,这就是为什么在很多例子中这个配置项被设置为 / 的原因

webpack-dev-server 也需要从publicPath获取信息,使用它来确定从何处提供输出文件。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
//...
output: {
// One of the below
publicPath: 'https://cdn.example.com/assets/', // CDN (always HTTPS)
publicPath: '//cdn.example.com/assets/', // CDN (same protocol)
publicPath: '/assets/', // server-relative
publicPath: 'assets/', // relative to HTML page
publicPath: '../assets/', // relative to HTML page
publicPath: '', // relative to HTML page (same directory)
}
};

devServer 中的 publicpath

打包的文件可以在浏览器的这个目录下面得到

如果服务跑在 http://localhost:8080,打包的文件为bundle.js,publicPath为 /, 可以在 http://localhost:8080/bundle.js访问到打包文件

如果 publicPath 改为 /assets/, 那么可以在 http://localhost:8080/assets/bundle.js访问,也可以把 publicPath 改为 http://localhost:8080/assets/

这说明了 devServer.publicPath 与 output.publicPath 是一致的

@babel/polyfill @babel/plugin-transform-runtime @babel/runtime-corejs2

@babel/preset-env 只会转换新语法,但是不会转换新的api,比如 Array.from

需要 @babel/polyfill 转换新的api,但是 @babel/polyfill 会全量引入,不能按需引入

可以通过 babel.rc 配置文件来实现

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/preset-env",
// This option was removed in v7 by just making it the default.在新版本中已经移除,无需添加
// {
// "useBuiltIns": "usage"
// }
]
]
}

但是@babel/preset-env也存在问题,虽然会按需引入但是每个文件如果有重复的方法,都会被编译成相同的代码引入,文件多的时候会让冗余的代码越来越多

@babel/runtime-corejs2 是一个随着 @babel/plugin-transform-runtime 一起时使用的运行时依赖,会把重复的函数覆盖为 @babel/runtime-corejs2 中的引用

@babel/runtime-corejs2 仅仅是一个包含着函数的包,把函数以模块化的形式引入, 要安装到生产依赖中

.babelrc

1
2
3
4
5
6
7
8
9
10
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}

treeshaking

webpack4 production 默认开启,需要引入的库使用commonjs 模块化规范

如 loadsh-es

全局引入

provide-plugin

1
2
3
4
5
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
})
]

多入口文件的每一个都会被引入jquery,所以需要提取公共代码

动态加载

@babel/plugin-syntax-dynamic-import

Dynamic Imports

需要指明webpackChunkName才能被单独打包

1
2
3
4
5
6
7
8
import(
/* webpackChunkName: "my-jquery" */
'jquery'
)
.then(({ default: $ }) => {
console.log($)
return $('body');
})

代码分割

SplitChunksPlugin 代替原来的 commonChunksPlugin

  • splitChunks.chunks

async表示只从异步加载得模块(动态加载import())里面进行拆分
initial表示只从入口模块进行拆分
all表示以上两者都包括

  • splitChunks.maxInitialRequests

每个入口的并发请求数, 如果拆出的包的个数大于maxInitialRequests,则不会把较小的包单独拆出

  • splitChunks.maxInitialRequests

动态引入的模块,最多拆分的数量

css分割

css-minimizer-webpack-plugin

压缩css代码

MiniCssExtractPlugin

可视化分析

webpack-bundle-analyzer

Ramda Object方法源码分析

keys

Safari 可能存在 argument.length 可枚举的bug

1
2
3
4
var hasArgsEnumBug = (function() {
'use strict';
return arguments.propertyIsEnumerable('length');
}());

IE9一下toString方法存在可以枚举的bug

1
2
3
4
5
6
// cover IE < 9 keys issues
var hasEnumBug = !({toString: null}).propertyIsEnumerable('toString');
var nonEnumerableProps = [
'constructor', 'valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'
];
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
// 如果有原生 Object.keys 实现且Safari不存在bug
var keys = typeof Object.keys === 'function' && !hasArgsEnumBug ?
_curry1(function keys(obj) {
// 参数错误处理
return Object(obj) !== obj ? [] : Object.keys(obj);
}) :
_curry1(function keys(obj) {
if (Object(obj) !== obj) {
return [];
}
var prop, nIdx;
var ks = [];
// 如果传入的对象时argument,而且有bug
var checkArgsLength = hasArgsEnumBug && _isArguments(obj);
// 循环对象每一项检查
for (prop in obj) {
if (_has(prop, obj) && (!checkArgsLength || prop !== 'length')) {
ks[ks.length] = prop;
}
}
// 如果有toString可枚举的bug
if (hasEnumBug) {
nIdx = nonEnumerableProps.length - 1;
// 排除不可枚举的属性
while (nIdx >= 0) {
prop = nonEnumerableProps[nIdx];
if (_has(prop, obj) && !contains(ks, prop)) {
ks[ks.length] = prop;
}
nIdx -= 1;
}
}
return ks;
});

Ramda 私有方法源码分析

_objectIs

Object.is 用于判断两值是否项等

1
2
3
4
5
6
7
8
9
10
11
12
function _objectIs(a, b) {
// MDN polyfill
if (a === b) {
// Steps 6.b-6.e: +0 != -0
return a !== 0 || 1 / a === 1 / b;
} else {
// Step 6.a: NaN == NaN
return a !== a && b !== b;
}
}
export default typeof Object.is === 'function' ? Object.is : _objectIs;

_arrayFromIterator

1
2
3
4
5
6
7
8
export default function _arrayFromIterator(iter) {
var list = [];
var next;
while (!(next = iter.next()).done) {
list.push(next.value);
}
return list;
}

_functionName

function.name 可能被压缩工具修改,不能依赖函数名来判断函数

作者通过正则方式判断

1
2
3
4
5
6
export default function _functionName(f) {
// String(x => x) evaluates to "x => x", so the pattern may not match.
var match = String(f).match(/^function (\w*)/);
return match == null ? '' : match[1];
}

_has

判断属性在对象的实例上,而不是在原型上

1
2
3
export default function _has(prop, obj) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}

等价于

1
Reflect.has(obj, prop)

_isArguments

实现了 callee 方法数组也可以当作是形式参数

1
2
3
4
5
6
var toString = Object.prototype.toString;
var _isArguments = (function() {
return toString.call(arguments) === '[object Arguments]' ?
function _isArguments(x) { return toString.call(x) === '[object Arguments]'; } :
function _isArguments(x) { return _has('callee', x); };
}());

_concat

通过双循环,涵盖了array-like类型,

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
/**
* Private `concat` function to merge two array-like objects.
*
* @private
* @param {Array|Arguments} [set1=[]] An array-like object.
* @param {Array|Arguments} [set2=[]] An array-like object.
* @return {Array} A new, merged array.
* @example
*
* _concat([4, 5, 6], [1, 2, 3]); //=> [4, 5, 6, 1, 2, 3]
*/
export default function _concat(set1, set2) {
set1 = set1 || [];
set2 = set2 || [];
var idx;
var len1 = set1.length;
var len2 = set2.length;
var result = [];

idx = 0;
while (idx < len1) {
result[result.length] = set1[idx];
idx += 1;
}
idx = 0;
while (idx < len2) {
result[result.length] = set2[idx];
idx += 1;
}
return result;
}

_dispatchable

dispatchable翻译为调度单元,这个方法用在了类似 R.all 遍历每一项的方法上

如果如参是一个数组会按照常规循环方式处理,如果不是数组但是对象携带了传入的方法s数组([all])其中的一个,将会调用对象携带的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default function _dispatchable(methodNames, transducerCreator, fn) {
return function() {
if (arguments.length === 0) {
return fn();
}
var obj = arguments[arguments.length - 1];
if (!_isArray(obj)) {
var idx = 0;
while (idx < methodNames.length) {
if (typeof obj[methodNames[idx]] === 'function') {
return obj[methodNames[idx]].apply(obj, Array.prototype.slice.call(arguments, 0, -1));
}
idx += 1;
}
if (_isTransformer(obj)) {
var transducer = transducerCreator.apply(null, Array.prototype.slice.call(arguments, 0, -1));
return transducer(obj);
}
}
return fn.apply(this, arguments);
};
}

_arrayFromIterator

1
2
3
4
5
6
7
8
export default function _arrayFromIterator(iter) {
var list = [];
var next;
while (!(next = iter.next()).done) {
list.push(next.value);
}
return list;
}

_equals

用在了涉及到比较的方法上,例如 R.equals

_objectIs

type

_arrayFromIterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150

// 处理map set 集合

function _uniqContentEquals(aIterator, bIterator, stackA, stackB) {
var a = _arrayFromIterator(aIterator);
var b = _arrayFromIterator(bIterator);

function eq(_a, _b) {
return _equals(_a, _b, stackA.slice(), stackB.slice());
}

// if *a* array contains any element that is not included in *b*
return !_includesWith(function(b, aItem) {
return !_includesWith(eq, aItem, b);
}, b, a);
}

export default function _equals(a, b, stackA, stackB) {
// 引用相同,基本类型的值相同
if (_objectIs(a, b)) {
return true;
}

// 类型不同
var typeA = type(a);

if (typeA !== type(b)) {
return false;
}

// 引用不同的情况
// ??????
if (typeof a['fantasy-land/equals'] === 'function' || typeof b['fantasy-land/equals'] === 'function') {
return typeof a['fantasy-land/equals'] === 'function' && a['fantasy-land/equals'](b) &&
typeof b['fantasy-land/equals'] === 'function' && b['fantasy-land/equals'](a);
}

// 如果自身实现了equals方法
if (typeof a.equals === 'function' || typeof b.equals === 'function') {
return typeof a.equals === 'function' && a.equals(b) &&
typeof b.equals === 'function' && b.equals(a);
}

// 其他引用类型
switch (typeA) {
case 'Arguments':
case 'Array':
case 'Object':
// 引用类型如果为Promise实例,看作是项等的
if (typeof a.constructor === 'function' &&
_functionName(a.constructor) === 'Promise') {
return a === b;
}
break;
// 基本类型的装箱对象
case 'Boolean':
case 'Number':
case 'String':
if (!(typeof a === typeof b && _objectIs(a.valueOf(), b.valueOf()))) {
return false;
}
break;
case 'Date':
// 时间类型转为事件戳
if (!_objectIs(a.valueOf(), b.valueOf())) {
return false;
}
break;
case 'Error':
return a.name === b.name && a.message === b.message;
case 'RegExp':
// 正则类型的所有实例方法的实现相同
if (!(a.source === b.source &&
a.global === b.global &&
a.ignoreCase === b.ignoreCase &&
a.multiline === b.multiline &&
a.sticky === b.sticky &&
a.unicode === b.unicode)) {
return false;
}
break;
}
// 处理map set集合
var idx = stackA.length - 1;
while (idx >= 0) {
if (stackA[idx] === a) {
return stackB[idx] === b;
}
idx -= 1;
}

switch (typeA) {
case 'Map':
if (a.size !== b.size) {
return false;
}

return _uniqContentEquals(a.entries(), b.entries(), stackA.concat([a]), stackB.concat([b]));
case 'Set':
if (a.size !== b.size) {
return false;
}

return _uniqContentEquals(a.values(), b.values(), stackA.concat([a]), stackB.concat([b]));
case 'Arguments':
case 'Array':
case 'Object':
case 'Boolean':
case 'Number':
case 'String':
case 'Date':
case 'Error':
case 'RegExp':
case 'Int8Array':
case 'Uint8Array':
case 'Uint8ClampedArray':
case 'Int16Array':
case 'Uint16Array':
case 'Int32Array':
case 'Uint32Array':
case 'Float32Array':
case 'Float64Array':
case 'ArrayBuffer':
break;
default:
// Values of other types are only equal if identical.
// 其他类型只有引用相同才算项等
return false;
}

// 处理数组或对象的每一个值是否项等
var keysA = keys(a);
if (keysA.length !== keys(b).length) {
return false;
}

var extendedStackA = stackA.concat([a]);
var extendedStackB = stackB.concat([b]);

idx = keysA.length - 1;
while (idx >= 0) {
var key = keysA[idx];
if (!(_has(key, b) && _equals(b[key], a[key], extendedStackA, extendedStackB))) {
return false;
}
idx -= 1;
}
return true;
}

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信