0%

prompt-command.js文件位于webapck-cli源码包的bin/utils/目录下,该文件里面的内容主要为运行npm命令相关的一些操作

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
// based on https://github.com/webpack/webpack/blob/master/bin/webpack.js

/**
* @param {string} command 命令
* @param {string[]} args 命令行参数
* @returns {Promise<void>} promise 返回结果的Promise实例
*/
const runCommand = (command, args) => {
// 使用child_process模块,进行shell命令的执行
// 由于js为单线程、额外的shell命令执行与交互
// 需要在另外子线程中执行。
const cp = require("child_process");

// 构建Promise
return new Promise((resolve, reject) => {
// 利用child_process模块的spawn方法
// 该方法实现创建出一个子线程实例
const executedCommand = cp.spawn(command, args, {
stdio: "inherit",
shell: true // 通过系统shell进行执行命令
});

// 注册child_process实例错误事件
// 对应resolve与reject
executedCommand.on("error", error => {
reject(error);
});

// 注册child_process实例退出事件
// 对应resolve与reject
executedCommand.on("exit", code => {
/// 退出状态吗为0,则正常退出
/// shell中的退出状态吗0未正常
/// 非0为对应的其它非正常退出
if (code === 0) {
resolve();
} else {
reject();
}
});
});
};

关于exec与spwan的区别,在于:

  1. exec返回buffer实例

  2. spawn返回stream实例

所以在需要长时间、大量处理数据输入输出,则选择spawn,其它短平快则方式选择exec等

区别参考:https://www.hacksparrow.com/nodejs/difference-between-spawn-and-exec-of-node-js-child-rocess.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取npm的全局包安装路径
// 主要是对命令`npm root -g`的封装
const npmGlobalRoot = () => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
// 通过spawn方法创建对应命令的子线程实例
const command = cp.spawn("npm", ["root", "-g"]);

command.on("error", error => reject(error)); // 注册error事件并reject

// 由于这边的命令没有做持续输出、所以,对于data就简单的调用一下toString()
// 获取终端的执行结果
// 如果在运行其它具有持续输出的命令时候、需要对data事件所返回的数据分片进行整合,
// 并且在close事件中进行resolve,这样可防止丢失返回结果。
command.stdout.on("data", data => resolve(data.toString()));

// stderr的输出数据则为执行的错误消息、该事件非底层的error错误
// 处理方式与stdout的data事件类似、不过在语义上,是reject
command.stderr.on("data", data => reject(data));
});
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 运行`@webpack-cli`相关已安装的包、或者命令
* @param packages 包名称
* @param pathForCmd 包的执行命令路径
* @param args 参数
*/
const runWhenInstalled = (packages, pathForCmd, ...args) => {

// 引入运行命令对应的模块
const currentPackage = require(pathForCmd);

// 约定包默认导出为执行函数
const func = currentPackage.default;

if (typeof func !== "function") {
// 非函数则直接中断
throw new Error(`@webpack-cli/${packages} failed to export a default function`);
}

return func(...args);
};
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
/**
* 运行`@webpack-cli`相关已安装的包、或者命令,如果未安装则提示执行安装
* @param packages 包名称
* @param args 参数
*/
module.exports = function promptForInstallation(packages, ...args) {

// 组合目标包路径
const nameOfPackage = "@webpack-cli/" + packages;

// 默认标志该包是未安装状态、从后面去检测判断
let packageIsInstalled = false;

// 包对应的路径
let pathForCmd;

try {
const path = require("path");
const fs = require("fs");

// 在当前项目node_modules文件夹中的查找对应包
pathForCmd = path.resolve(process.cwd(), "node_modules", "@webpack-cli", packages);

// 当前项目未安装、则进一步找全局
if (!fs.existsSync(pathForCmd)) {
// 获取全局的node_modules路径
const globalModules = require("global-modules");

// 拼接新的路径
pathForCmd = globalModules + "/@webpack-cli/" + packages;

// 利用require.resolve来验证包是否存在
require.resolve(pathForCmd);

// 存在文件
} else {
require.resolve(pathForCmd);
}

// require.resolve正常执行,则包已安装
packageIsInstalled = true;
} catch (err) {
// require.resolve异常,则包未安装
packageIsInstalled = false;
}

/// 如果包未安装则进一步提示安装
if (!packageIsInstalled) {
const path = require("path");
const fs = require("fs");

// 利用原生的readline模块进行询问
const readLine = require("readline");

// 判断是否要通过yarn进行安装,通过yarn.lock来判断
const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));

// 确定npm或者yarn作为安装器
const packageManager = isYarn ? "yarn" : "npm";

// 构建安装命令需要的参数列表,这边默认为npm的参数形式
const options = ["install", "-D", nameOfPackage];

if (isYarn) {
// yarn模式的话,修改参数列表 `install` -> `add`
// --> `yarn add -D`
options[0] = "add";
}

// 初始化安装webapck-cli
if (packages === "init") {
if (isYarn) {
options.splice(1, 1); // remove '-D'
options.splice(0, 0, "global");
// --> `yarn global add`
} else {
options[1] = "-g";
// --> `npm install -g`
}
}


// 拼接需要运行的参数
const commandToBeRun = `${packageManager} ${options.join(" ")}`;

// 问题
const question = `Would you like to install ${packages}? (That will run ${commandToBeRun}) (yes/NO) : `;

console.error(`The command moved into a separate package: ${nameOfPackage}`);

// 通过readline创建终端交互接口
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stdout
});

// 执行提示询问
questionInterface.question(question, answer => {
// 关闭接口
questionInterface.close();

// 根据不同输入执行
switch (answer.toLowerCase()) {
case "y":
case "yes":
case "1": { // 按回车
// 执行安装命令
runCommand(packageManager, options)
.then(_ => {
// 初始化webpack-cli
if (packages === "init") {
npmGlobalRoot()
.then(root => {
const pathtoInit = path.resolve(root.trim(), "@webpack-cli", "init");
return pathtoInit;
})
.then(pathForInit => {
// 找到init相关脚本模块、并执行
return require(pathForInit).default(...args);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
return;
}

// 安装完对应的包后、通过runWhenInstalled执行命令
pathForCmd = path.resolve(process.cwd(), "node_modules", "@webpack-cli", packages);
return runWhenInstalled(packages, pathForCmd, ...args);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
break;
}
default: {
// 放弃安装、提示需要依赖包,退出程序
console.error(`${nameOfPackage} needs to be installed in order to run the command.`);
process.exitCode = 1;
break;
}
}
});
} else {
// 已经对应包已安装、则直接执行
return runWhenInstalled(packages, pathForCmd, ...args);
}
};

1. 文件路径格式

1
2
3
4
5
const files = [
'/Users/god/my_project/a.js',
'/Users/god/my_project/b.js',
'/Users/god/my_project/others/c.js',
]

2. 定义文件树节点

1
2
3
4
5
type FileTreeNode = {
label: string
path: string
children: Array<FileTreeNode>
}

在处理过程中为了方便索引与记录,所以添加一个childMap字段,用于记录children数组中对应的子节点

1
2
3
type CacheFileTreeNode = {
childMap: Record<string, CacheFileTreeNode>
} & FileTreeNode

3. 分割路径、并按节点顺序进行归并

3.1 定义路径分割方法

1
2
3
4
function SplitPath(pathStr: string, sep = '/'): string[] {
/** 根据分隔符sep进行路径字符串分割, 过滤掉空字符串 */
return pathStr.split(sep).filter(Boolean)
}

3.2 定义目录树根节点root

1
2
3
4
5
6
const root: CacheFileTreeNode = {
label: 'root',
path: '',
childMap: {},
children: []
}

3.3 定义文件归并处理

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
function ReducePathParts(parts: string[], root: CacheFileTreeNode): CacheFileTreeNode {

// 进行归并关联
parts.reduce((parent, name) => {

// 查找上一级目录节点中是否存在与当前路径名相同的子节点节点
const hit = parent.childMap[name]
if (hit) {
return hit
}

// 定义当前目录对应的节点
const current = {
label: name,
path: `${parent.path}/${name}`,
childMap: {},
children: []
}

// 将当前目录树节点关联到上一级
parent.childMap[name] = current
parent.children.push(current)

// 返回当前节点作为下一级的父节点
return current

}, root)

return root // 返回根节点
}

4. 合并逻辑

1
2
3
4
5
6
7
8
9
10
11
function generateFileTree(files: string[]): CacheFileTreeNode {

// ...

files.forEach(filePath => {
const parts = SplitPath(filePath) // 分割
ReducePathParts(parts, root) // 关联
})

return root
}

获得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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
label: 'root',
path: '',
cildren: [
{
label: 'Users',
path: '/Users',
children: [
{
label: 'god',
path: '/Users/god',
children: [
{
label: 'my_project',
path: '/Users/god/my_project',
children: [
{
label: 'a.js',
path: '/Users/god/my_project/a.js',
children: []
},
{
label: 'b.js',
path: '/Users/god/my_project/b.js',
children: []
},
{
label: 'others',
path: '/Users/god/my_project/others',
children: [
{
label: 'c.js',
path: '/Users/god/my_project/others/c.js',
children: []
}
]
}
]
}
]
}
]
}
]
}

1. 菜单数据结构

通常,对于菜单的配置,都是单条记录,利用pid等字段进行父级关联,这边默认定义菜单配置项目的数据结构

1
2
3
4
5
type MenuOpt = {
id: nubmer
pid: number
name: string
}

包含最主要的三个字段id,pid,name,其中完成关系嵌套的两个字段为id和pid,这边pid是一对一关系,也就是说一个菜单项目只能归属到一个父级菜单下面,而接下来,我们需要为菜单添加一个额外的字段children用来存放子菜单,这里定义一个新的数据结构:

1
2
3
4
5
6
type MenuItem = {
id: number
pid: number
name: string
children: MenuItem[]
}

由于具有相同的字段id,pid,name,所以我们复用MenuOpt类型进行联合:

1
2
3
type MenuItem = {
children: MenuItem[]
} & MenuOpt

2. 原始菜单配置项数据

通过以上的类型定义,我来生成一个菜单的配置项数据,通常这些数据:

1
2
3
4
5
6
7
8
const menuOpts: MenuOpt[] = [
{ id: 1, pid: 0, name: '节点1' },
{ id: 2, pid: 1, name: '节点1-1' },
{ id: 3, pid: 0, name: '节点2' },
{ id: 4, pid: 3, name: '节点2-1' },
{ id: 5, pid: 2, name: '节点1-1-1' },
{ id: 6, pid: 1, name: '节点1-2' },
]

这边定义pid如果为0则为顶级的菜单项目,则直接添加到最终的顶级菜单数组中。

3. 嵌套菜单

为了方便进行管理,我们需要遍历并记录对应id与配置项的映射,通过Object的引用特性,即可快速完成嵌套关系,具体的步骤为:

  1. 遍历配置项目,拷贝配置项并完成id => opt的映射索引
  2. 遍历配置项目(拷贝完成),根据pid去查找映射并添加到对应的父级配置项中
  3. 完成对菜单项目的排序,根据自定义排序规则进行
  4. 完成菜单关系嵌套

3.1 遍历并建立索引

为了防止引用特性带来的副作用,我们将配置项进行深拷贝,先定义一个深拷贝函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function cloneDeep<
C extends { origin: any, copied: any }
>(origin: any, cache: C[] = []) {

if (typeof origin !== 'object' || origin === null) {
return origin
}

const hit = cache.find(c => c.origin === origin)

if (hit) { return hit.copied }

const copied = Array.isArray(origin) ? [] : {}

cache.push({ origin, copied } as C)

Object.keys(origin).forEach(key => {
copied[key] = cloneDeep(origin[key], cache)
})

return copied
}

定义嵌套处理方法

1
2
3
4
function nestingMenu(menuOpts: MenuOpt[]) {
const menu: MenuItem[] = []
const marked: Record<number, MenuItem> = {}
}

建立索引

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...

// building index mark
const menuItems = menuOpts.map(opt => {
const menuItem: MenuItem = cloneDeep(opt)
// init children
menuItem.children = []
// mark
marked[menuItem.id] = menuItem
return menuItem
})

// ...

3.2 完成嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...

// nesting
menuItems.forEach(item => {
const { pid } = item
const parent = marked[pid]

// nested
if (parent) {
parent.children.push(item)
}
// top level menu item
else {
menu.push(item)
}
})

// ...

3.3 对菜单进行排序保证最终顺序

定义排序方法并完成排序

1
2
3
4
5
6
7
8
9
10
11
// ...

// sorting menu items
function sortMenuItems(menuItems: MenuItem[]) => {
menuItems.sort((a, b) => a.name > b.name ? 1 : -1)
menuItems.forEach(item => sorting(item.children))
}

sortMenuItems(menu)

// ...

4 完整代码

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
type MenuOpt = {
id: number
pid: number
name: string
}

type MenuItem = {
children: MenuItem[]
} & MenuOpt

const menuOpts: MenuOpt[] = [
{ id: 1, pid: 0, name: '节点1' },
{ id: 2, pid: 1, name: '节点1-1' },
{ id: 3, pid: 0, name: '节点2' },
{ id: 4, pid: 3, name: '节点2-1' },
{ id: 5, pid: 2, name: '节点1-1-1' },
{ id: 6, pid: 1, name: '节点1-2' },
]

function cloneDeep<
C extends { origin: any, copied: any }
>(origin: any, cache: C[] = []) {

if (typeof origin !== 'object' || origin === null) {
return origin
}

const hit = cache.find(c => c.origin === origin)

if (hit) { return hit.copied }

const copied = Array.isArray(origin) ? [] : {}

cache.push({ origin, copied } as C)

Object.keys(origin).forEach(key => {
copied[key] = cloneDeep(origin[key], cache)
})

return copied
}

function nestingMenu(menuOpts: MenuOpt[]) {
const menu: MenuItem[] = []

const marked: Record<number, MenuItem> = {}

// sorting menu items
const sorting = (menuItems: MenuItem[]) => {
menuItems.sort((a, b) => a.name > b.name ? 1 : -1)
menuItems.forEach(item => sorting(item.children))
}

// building index mark
const menuItems = menuOpts.map(opt => {
const menuItem: MenuItem = cloneDeep(opt)
// init children
menuItem.children = []
// mark
marked[menuItem.id] = menuItem
return menuItem
})

// nesting
menuItems.forEach(item => {
const { pid } = item
const parent = marked[pid]

// nested
if (parent) {
parent.children.push(item)
}
// top level menu item
else {
menu.push(item)
}
})

sorting(menu) // sorting

return menu
}

测试结果:

1. 基本概念

  • 插件也是JavaScript脚本、改变脚本文件的标示即可
  • 插件的加载与解析是优先于其他普通脚本
  • 插件不提供转码,为实现兼容性,推荐使用ES5进行编写
  • 插件能够获取到cc的全局对象、并且在Web环境中能获取window对象

2. 实现一个简单的Router功能的插件

2.1 功能需求

  • navigateTo
  • back
  • setInitScene

2.2 创建一个普通脚本文件

在资源管理器下,在路径为assets/scripts/plugins下创建router.js文件

然后点击刚刚创建的router.js文件,可以在属性面板中看到刚刚默认创建的脚本内容:

可以在面板找到,一个开关Import As Plugin, 勾选这个开关,即可将该文件更改为插件脚本。勾选后,面板多出来三个可选项,分别确认插件在三个环境中是否加载,对于通用插件,可以勾选native和web,按需选择即可。

2.3 编写脚本内容

由于脚本加载解析是在全局作用域下执行的,为了防止脚本加载造成的变量污染,推荐使用IIFE结构体和’use strict’关键字进行

1
2
3
4
5
6
7
/** 定义基础的IIFE结构体 */
!(function () {
'use strict'

// 代码内容

})()

定义一个globalThis全局上下文变量,方便后面使用:

1
2
3
4
5
6
// ...

/** 定义全局上下文变量globalThis */
var globalThis = this || globalThis|| window || self || cc

// ...

利用ES5语法定义CCRouter类,声明并初始化实例变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...

/**
* @class CCRotuer
* @property {string} initScene
* @property {string[]} routes
*
* @constructor
* @param {string} [initScene]
*/
function CCRotuer(initScene) {
this.initScene = initScene
this.routes = initScene ? [initScene] : []
}

// ...

定义原型方法back,navigateTo

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
// ...

/**
* Router navigate to
* @param {string} scene
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.navigateTo = function (scene, onLaunched) {
this.routes.push(scene)
cc.director.loadScene(scene, onLaunched)
}

/**
* Rotuer back
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.back = function (onLaunched) {
// pop current
var _scene = this.routes.pop()

// pop previous or get init scene or current scene
var previousScene = this.routes.pop() || this.initScene || _scene

this.navigateTo(previousScene, onLaunched)
}

/**
* Router setInitScene
* @param {string} scene
*/
CCRotuer.prototype.setInitScene = function (scene) {
// get original init scene
var _initScene = this.initScene
// reassign init scene
this.initScene = scene
// replace all previous init scene in rotues stack
if (_initScene) {
for (var i = 0; i < this.routes.length; i++) {
if (this.routes[i] === _initScene) {
this.router[i] = scene
}
}
}
}

// ...

初始化router实例并注入到cc全局对象中

1
2
3
4
5
6
7
// ...
// 假设约定默认场景名为index
const router = new CCRouter('index')
// 注入到cc中
cc.router = router

// ...

完成脚本编写后,重新运行游戏,就可以在其他普通脚本中使用cc.router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// in some component script
{
// ...
onLoad: function () {
this.node.on(cc.Node.EventType.TOUCH_START, function () {
cc.router.navigateTo('someSceneName', function () {
console.log('Scene `someSceneName` launched.')
})
})

this.someNode.on(cc.Node.EventType.TOUCH_START, function () {
cc.router.back(function () {
console.log('Back From `someComponentName` component.')
})
})
},
// ...
}

3. 完整插件代码

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
/**
* CCRotuer Plugin
*/
!(function () {
'use strict'

// define globalThis context variable
var globalThis = this || globalThis|| window || self || cc

/**
* CCRotuer
* @param {string} initScene
*/
function CCRotuer(initScene) {
this.initScene = initScene
this.routes = initScene ? [initScene] : []
}

/**
* Router setInitScene
* @param {string} scene
*/
CCRotuer.prototype.setInitScene = function (scene) {
// get original init scene
var _initScene = this.initScene
// reassign init scene
this.initScene = scene
// replace all previous init scene in rotues stack
if (_initScene) {
if (this.routes.length) {
for (var i = 0; i < this.routes.length; i++) {
if (this.routes[i] === _initScene) {
this.router[i] = scene
}
}
}
else {
this.routes = [scene]
}
}
}

/**
* Router navigate to
* @param {string} scene
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.navigateTo = function (scene, onLaunched) {
this.routes.push(scene)
cc.director.loadScene(scene, onLaunched)
}


/**
* Rotuer back
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.back = function (onLaunched) {
// pop current
var _scene = this.routes.pop()

// pop previous or get init scene or current scene
var previousScene = this.routes.pop() || this.initScene || _scene

this.navigateTo(previousScene, onLaunched)
}

// create router instance and inject to cc scope
cc.router = new CCRotuer('index')
})()

Simulating Event Loops

基本要素概念

  1. 宏任务(macro task)
  2. 微任务(micro task)
  3. 栈(stack)

宏任务类型

  • setTimeout
  • setInterval
  • setImmediate

微任务类型

  • Promise
  • Object.observe
  • MutationObserver

Node中的特殊类别

  • process.nextTick

利用代码模拟EventLoops的核心特性

1. 定义队列容器

1
2
3
4
5
6
7
8
9
/**
* 宏任务队列
*/
const macroTaskQueue = []

/**
* 微任务队列
*/
const microTaskQueue = []

2. 定义队列操作方法

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
/**
* 宏任务入列
* @param {Function} callback 回调方法
*/
function enqueueMacroTask(callback) {
macroTaskQueue.push(callback)
}

/**
* 宏任务出列
* @returns {Function}
*/
function dequeueMacroTask() {
return macroTaskQueue.shift()
}

/**
* 微任务入列
* @param {Function} callback 回调方法
*/
function enqueueMicroTask(callback) {
microTaskQueue.push(callback)
}

/**
* 微任务出列
* @returns {Function}
*/
function dequeueMicroTask() {
return microTaskQueue.shift()
}

4. 定义nextTick的执行方法

4.1 分别定义任务的执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 宏任务nextTick
*/
function nextTickMacro() {
const callback = dequeueMacroTask()
callback()
}

/**
* 微任务nextTick
*/
function nextTickMicro() {
const callback = dequeueMicroTask()
callback()
}

4.2 定义总体nextTick执行方法

由于micro task优先级高于macro task,所以在nextTick时候是要优先判断与处理micro task队列信息。

1
2
3
4
5
6
7
8
9
10
11
function nextTick() {
/* 优先判断micro task */
if (microTaskQueue.length) {
nextTickMicro()
return nextTick()
}
if (macroTaskQueue.length) {
nextTickMacro()
return nextTick()
}
}

执行测试

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
// 推入微任务
enqueueMacroTask(() => {

console.log('macro task A', 2)

// 推入微任务
enqueueMicroTask(() => {
console.log('micro task A', 3)
})

// 推入宏任务
enqueueMacroTask(() => {
console.log('macro task B', 5)
})

})

// 推入微任务
enqueueMicroTask(() => {

console.log('micro task B', 1)

// 推入宏任务
enqueueMacroTask(() => {

console.log('macro task C', 4)

})
})

// 模拟主栈为空,进行下一个事件轮训周期
nextTick()

执行的结果为:

1
2
3
4
5
micro task B 1
macro task A 2
micro task A 3
macro task C 4
macro task B 5

对比setTimeout、Promise的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setTimeout(() => {
console.log('setTimeout A', 2)
Promise.resolve().then(() => {
console.log('Promise A', 3)
})
setTimeout(() => {
console.log('setTimeout B', 5)
})
})

Promise.resolve().then(() => {
console.log('Promise B', 1)
setTimeout(() => {
console.log('setTimeout C', 4)
})
})

// 主栈为空,执行原生的event-loop

原生执行结果:

1
2
3
4
5
Promise B 1
setTimeout A 2
Promise A 3
setTimeout C 4
setTimeout B 5

1. 数据类型的拷贝

基本数据类型在heap中是独份存在的,所有拷贝的方式可以直接以赋值形式完成:

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
// number
let a = 1
let aa = a
a = 0
// aa === 1 -> true

// string
let b = 'abc'
let bb = b
b = 'aaa'
// bb === 'abc' -> true

// boolean
let isChecked = true
let checked = isChecked
isChecked = false
// checked === true -> true

// symbol
let s1 = Symbol('S1')
let s2 = s1
s1 = Symbol('SS')
// s2.toString() -> 'S1'

// BigInt
let n1 = BigInt(9999999)
let n2 = n1
n1 = BigInt(12312312312323)
// n2 === 9999999 -> true

// undefined
let u1 = undefined
let u2 = u1
u1 = 0
// u2 === undefined -> true

而对于引用类型如:Object、Array等,其值实际上为引用指针,所以当你以赋值方式进行复制操作时候,修改原始值,则会同步修改,让我们来了解下:

1.1 Array内容存在形式

我们来初始化一个数组arr:

1
const arr = [1, 2, 3]

假设内存中的值形式:

1
2
3
4
|- arr -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003

从上面可知,arr变量指针存放在0x0000的地址中, 其余的基本数据都存放于0x0001-0x0003中,那么,arr数组的内容则变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
arr -> 0x0000 = [e1, e2, e3]

// 等价于
[
e1 -> 0x0001,
e2 -> 0x0002,
e3 -> 0x0003,
]

// 等价于
[1, 2, 3]

// 最终就是我们表达中的 arr = [1, 2, 3]

好,接下来我们定义个新数组,并且将arr复制于新数组变量:

1
const arr2 = arr

此时发生的其实是将arr的引用地址(0x0000)复制与arr2,假设新的内存形式:

1
2
3
4
5
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- arr2 -| - 0x0003

那么,其实arr2所对应的内存值是arr的空间地址(0x0000),也就是:

1
2
3
4
5
6
7
8
9
arr2 -> 0x0003 -> 0x0000

// 等价于

arr2 -> 0x0000

// 等价于

arr2 === arr
1
2
3
4
5
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- 0x0000 -| - 0x0003 // 指针形式

所以可以知道arr和arr2都是同时指向的0x0000这块地址,所以对arr修改内部元素,则arr2也发生了变化,因为是同一个地址内容:

1
2
arr[0] = -1
arr2[0] === -1 // true

经过上面的修改后,新的内存值形式为:

1
2
3
4
5
|- arr  -| - 0x0000
|- -1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- 0x0000 -| - 0x0003 // 指针形式

由上面的分析可以知道引用类型内部子元素其实都为指针形式,所以并并不能直接复制进行拷贝,而是需要新的容器内部重新去添加值:

1
2
3
4
5
const arr2 = []

for(let i = 0; i < arr.length; i++) {
arr2[i] = arr[i]
}

对应的内存形式为:

1
2
3
4
5
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- arr2 -| - 0x0004

其中arr1的内存形式为:

1
2
3
4
5
6
7
arr -> 0x0000 -> [0x0001, 0x0002, 0x0003]
// 等价于
arr: [1, 2, 3]

arr2 -> 0x0003 -> [0x0001, 0x0002, 0x0003]
// 等价于
arr2: [1, 2, 3]

当我们对arr,arr2对任意元素进行修改时候,由于arr和arr2值并非为同一块地址空间,所以互不影响:

1
2
arr[0] = 5
arr2[2] = 5

内存形式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- arr2 -| - 0x0004
|- 5 -| - 0x0005

arr -> 0x0000 -> [0x0005, 0x0002, 0x0003 ]
arr2 -> 0x0004 -> [0x0001, 0x0002, 0x0005 ]

// 等价于

arr ==> [5, 1, 2]
arr2 ==> [1, 2, 5]

2.2 Object内容存在形式

对于Object类型其实道理和Array相同,毕竟Array是特殊的Object,Object中的属性值也是指向地址

1
2
3
4
const o = {
a: 1,
b; 2
}

假设内存形式:

1
2
3
|-  o   -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002

所o的指针形式可以这样来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
o -> 0x0000 = {}
a -> 0x0001 = 1
b -> 0x0002 = 2

// 等价于
o = {
a: -> 0x0001,
b: -> 0x0002,
}

// 等价于
o = {
a: 1,
b: 2,
}

对于引用类型的拷贝基本原则是:

  1. 需要定义新的同类型值来充当容器,从而分离操作地址空间
  2. 将新拷贝的值属性完全指待拷贝的值(仅假设其值为基本数据类型)

2 Array的常用拷贝方式

1
2
const origin = [1, 2, 3]
const copied = []

2.1 基本的遍历方式逐一拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// for
for (let i = 0; i < origin.length; i++) {
copied.push(origin[i])
}

// for-in
for (let idx in origin) {
copied.push(origin[idx])
}

// for-of
for (let item of origin) {
copied.push(item)
}

// forEach
origin.forEach(item => {
copied.push(item)
})

// map
copied = origin.map(item => item)

// filter
copied = origin.filter(() => true)

// 扩展操作符
copied = [...origin]

2.2 快速创建副本

1
2
3
4
5
6
7
8
// concat
copied = origin.concat()

// slice
copied = origin.slice()

// Array.from
copied = Array.form(origin)

3 Object常用拷贝方式

1
2
const origin = { a: 1, b: 'hello' }
const copied = {}

3.1 偷懒的parse、stringify

1
copied = JSON.parse(JSON.stringify(origin))

这种方式虽然简单快速,但是有许多的问题:

  1. 属性必须为enumerable,否则stringify将忽略
  2. 属性必须为非undefined,否则stringify将忽略
  3. 属性必须为非function,否则stringify将忽略
  4. 不支持高级属性名类型,例如Symbol等,stringify将忽略
  5. 不支持循环引用,stringify将报错
  6. 性能问题,parse的解析过程

这个方式在简单、数据结构单纯简单的时候可以轻量使用,从而快速实现对象拷贝

3.2 遍历属性拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// for-in
for (let key in origin) {
copied[key] = origin[key]
}

// for-of
for (pair of origin.entries()) {
const [key, value] = pair
copied[key] = value
}

// Object.keys
Object.keys(origin).forEach(key => copied[key] = origin[key])

// Object.entries
Object.entries(origin).forEach(pair => {
const [key, value] = pair
copied[key] = value
})

// 展开操作符
copied = { ...origin }

注意:以上的遍历方式都将忽略不可枚举及继承的属性

3.3 Object.assign方法进行拷贝

通过Object.assign方式,将origin属性重新赋值到新的对象中,完成拷贝效果,而该方式也会和以上方式一样,忽略不可枚举的属性:

1
copied = Object.assign({}, origin)

3.4 Object.getOwnPropertyNames进行拷贝

对于不可枚举的属性,有时候我们也想要完全拷贝,则使用该方法来实现拷贝:

1
2
3
Object.getOwnPropertyNames(origin).forEach(key => {
copied[key] = origin[key]
})

但该方式会使得原来origin中不可枚举的属性配置丢失,所以需要结合Object.getOwnPropertyDescriptorObject.defineProperty来进行拷贝,对上面代码进行修改:

1
2
3
4
5
6
Object.getOwnPropertyNames(origin).forEach(key => {
// 获取当前key在origin中对属性配置描述
const descriptor = Object.getOwnPropertyDescriptor(origin, key)
// 定义copied的新值
Object.defineProperty(copied, key, descriptor)
})

3.5 利用defineProperties,getOwnPropertyDescriptors拷贝

对于以上的方式,我们可以再利用Object.definePropertiesObject.getOwnPropertyDescriptors进行批量属性拷贝,从而精简代码:

1
2
const originPropDescs = Object.getOwnPropertyDescriptors(origin)
Object.defineProperties(copied, originPropDescs)

该方式于3.4中的效果相同,也是支持获取不可枚举属性的变量名,但此方法去除了遍历添加的步骤,直接批量完成拷贝动作

4 多层级的引用类型拷贝

以上的Array,Object拷贝方式主要实现为仅一层属性的浅拷贝,而当属性的值为引用类型的时候,需要递归以上拷贝步骤进行深层拷贝,所以这边主要是2个关键点:

  1. 基本数据类型、null直接赋值拷贝
  2. Object, Array等数据类型进行递归拷贝

4.1 Array的多层级拷贝

这边先假设Array中存在的引用类型仍为Array,后期对于其他的引用类型拷贝如Object则整合进去就行

1
2
3
4
5
6
7
8
9
10
function deepCloneArray(origin) {
const copied = []
origin.forEach(item => {
copied.push(Array.isArray(item)
? deepCloneArray(item) // 递归
: item
)
})
return copied
}

4.2 Object的多层级拷贝

这边进行4.1一样的假设,先假设Object属性中的引用类型任为Object

1
2
3
4
5
6
7
8
9
10
11
12
const isObject = o => Object.prototype.toString.call(o) === '[object Object]'
function deepCloneObject(origin) {
const res = {}
for (const key in origin) {
const value = origin[key]
res[key] = isObject(value)
? deepCloneObject(value)
: value
}

return res
}

4.3 整合Object和Array深拷贝逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 深拷贝函数
function deepClone(origin) {
if (typeof origin !== 'object' || origin === null)) {
return origin
}

const copied = Array.isArray(origin) ? [] : {}

Object.keys(origin).forEach(key => {
copied[key] = deepClone(origin[key])
})

return copied
}

4.4 关于循环引用的处理

4.3中的深拷贝函数中,当出现循环引用则会发生无限的递归调用,从而发生栈溢出错误,需要添加对循环引用的判断和处理,处理的关键逻辑为:

  1. 创建一个cache数组用来存放引用类型值
  2. 每次递归处理前先查找缓存
  3. 发现缓存直接赋值、未发现缓存则添加缓存,并递归

整理代码为:

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
// 深拷贝函数
function deepClone(origin, cache = []) {
if (typeof origin !== 'object' || origin === null) {
return origin
}

// 查找缓存
const hit = cache.find(c => c.origin === origin)

// 返回缓存中的copied
if (hit) { return hit.copied }

// 根据数据类别创建副本
const copied = Array.isArray(origin) ? [] : {}

// 存入缓存
cache.push({ origin, copied })

// 拷贝属性
Object.keys(origin).forEach(key => {
copied[key] = deepClone(origin[key], cache)
})

return copied
}

1.更新Homebrew源信息:

1
brew update

2.搜索MySQL相关的Homebrew包信息

1
brew search mysql

搜索结果如下:

1
2
3
4
5
6
7
8
9
10
==> Formulae
automysqlbackup mysql-client@5.7 mysql-search-replace mysqltuner
mysql mysql-connector-c++ mysql-utilities
mysql++ mysql-connector-c++@1.1 mysql@5.6
mysql-client mysql-sandbox mysql@5.7

==> Casks
homebrew/cask/mysql-connector-python homebrew/cask/navicat-for-mysql
homebrew/cask/mysql-shell homebrew/cask/sqlpro-for-mysql
homebrew/cask/mysql-utilities

3.安装Homebrew默认的最新版本MySQL

1
brew install mysql

以上命令则直接安装默认的版本,为最新的MySQL

4.安装指定版本,这边以5.7为例:

1
brew install mysql@5.7

在安装非默认的版本时候,在环境变量中是找不到mysql命令的,这边需要手动加入包的bin目录到PATH中,有两种方式实现:

方式1:利用brew link

1
brew link mysql@5.7 --force

方式2:利用.bashrc等方式进行PATH内容增补,这边以zsh为例

1
echo 'export PATH="/usr/local/opt/mysql@5.7/bin:$PATH"' >> ~/.zshrc

5.启动MySQL服务

1
brew services start mysql

最终可以按需配置MySQL,包括用户名、密码、端口等信息。

1. 替换清华update源:

https://mirror.tuna.tsinghua.edu.cn/help/homebrew/

替换:

1
2
3
4
5
6
7
git -C "$(brew --repo)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git

brew update

复原:

1
2
3
4
5
6
7
git -C "$(brew --repo)" remote set-url origin https://github.com/Homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://github.com/Homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://github.com/Homebrew/homebrew-cask.git

brew update

2. 替换二进制源

https://mirrors.tuna.tsinghua.edu.cn/help/homebrew-bottles/

临时替换:

1
export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles

长期替换:

1
2
3
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles' >> ~/.bash_profile

source ~/.bash_profile

1. 时钟数学关系要素

A.模拟时钟就是我们平常生活中所见到的具有指针的那种机械钟表,通常有以下三个要素:

  1. 秒针
  2. 分针
  3. 时针

B.转一周为360°,所以对应的一秒钟、一分钟、一小时的角度为:

  1. 1秒钟:360° / 60秒 => 6°/秒

  2. 1分钟:360° / 60分 => 6°/秒

  3. 1小时:360° / 12时 => 30°/时

C.这三个指针转一周的数值信息为:

  1. 时针转一周12小时
  2. 分针转一周60分钟(1小时)
  3. 秒钟转一周60秒(1分钟)

D.所以这边有个联动关系,当秒针转一秒,分针与时针相对地都会转对应的角度,所以通过以上信息在转动1秒钟后进行的换算规则:

  1. 秒针转动 6°
  2. 分针转动 6°/60 => 0.1°
  3. 时针转动 0.1°/60 => 1/600°

2. 绘制canvas

我们先来完成下绘制前的一些准备工作,初始化canvas等,我们约定HTML内容如下:

1
<canvas id="clock"></canvas>

初始化一下canvas的context等信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义canvas的长、宽
const CLOCK_WIDTH = 800
const CLOCK_HEIGHT = 800

const clockCanvas = document.getElementById('clock')

// 设置canvas长、宽
clockCanvas.width = CLOCK_WIDTH
clockCanvas.height = CLOCK_HEIGHT

const ctx = clockCanvas.getContext('2d')

// 绘制背景色
ctx.save()
ctx.fillStyle = '#ddd'
ctx.fillRect(0, 0, CLOCK_WIDTH, CLOCK_HEIGHT)
ctx.restore()

到目前为止可以看到我们的页面中已经有了一个800x800的灰色画布,准备工作已经差不多了。

2.1 绘制时钟底盘

我们需要先绘制下时钟底盘,包含刻度线和数字信息,大概像这样:

2.1.1 绘制圆盘边界

参照上面时钟图片,我们来画出时钟的边界,其实就是画一个圆,圆心为canvas中心点,半径为定义的时钟大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
const CENTER_X = CLOCK_WIDTH / 2
const CENTER_Y = CLOCK_HEIGHT / 2

const RADIUS = 380
// ...

/* 画边框 */
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 2
ctx.beginPath()
ctx.arc(CENTER_X, CENTER_Y, RADIUS, 0, 2 * Math.PI)
ctx.closePath()
ctx.stroke()
ctx.restore()

2.1.2 绘制圆盘秒钟、分钟刻度

刻度为围绕中心旋转的线段,其中,小时刻度为12条,秒、分刻度都为60条,而其中的12条与小时刻度重合了且被覆盖,所以我们先来画一下秒、分的刻度再去画小时的刻度。根据前面的换算,每一刻度的间隔度数为6°,实现的方式为:

  1. 将canvas原始点移动到圆盘中点 translate(CENTER_X, CENTER_Y)
  2. 从中心点向右水平方向,移动到线段起始点moveTo(RADIUS - 30)
  3. 利用lineTo(RADIUS, 0)进行画线
  4. 循环以上60次,每次旋转递增6°,rotate(i * 6)

上面的moveTolineTo都是在以x轴正方向上进行,所以综合以上的两部其实就是画出一条长度为30的线段。

而1步中的translate操作则是方便我们后续的rotate进行以中心点旋转,因为canvas的起始点也就是(0, 0)坐标点默认对应的是左上角,这样子改变原始点可以方便后续改lineTo的方向,其实可以理解为右手拿着画笔一直从左往右画线段,然后左手同时旋转纸张,这样纸张旋转一周意味着已经画60次的线段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 画秒、分刻度 */
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 1
for(let i = 0; i < 60; i++) {
ctx.save()
ctx.translate(CENTER_X, CENTER_Y) // 原始点移动到中心
ctx.rotate(i * 6 / 180 * Math.PI) // 围绕中心旋转i*6度
ctx.beginPath()
ctx.moveTo(RADIUS - 20, 0) // 移动到坐标(RADIUS - 20, 0)
ctx.lineTo(RADIUS, 0) // 画线到坐标(RADIUS, 0)
ctx.closePath()
ctx.stroke()
ctx.restore()
}
ctx.restore()

2.1.3 绘制小时刻度

小时刻度为12格,每一格相差30°,而小时刻度的宽度更宽,长度更长,所以参考一下上面的流程,实现的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 画时刻度 */
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 2 // 线宽为2
for(let i = 0; i < 12; i++) {
ctx.save()
ctx.translate(CENTER_X, CENTER_Y)
ctx.rotate(i * 30 / 180 * Math.PI)
ctx.beginPath()
ctx.moveTo(RADIUS - 40, 0) // 长度比秒、分刻度多20
ctx.lineTo(RADIUS, 0)
ctx.closePath()
ctx.stroke()
ctx.restore()
}
ctx.restore()

2.1.4 绘制小时数字

从前面的时钟模版图片可以看出,小时数字的方向都是一致的,只是位置的不同,所以此时,我们来分析下每个字体的坐标该怎么去获得,首先我们定义一个向量,其实点为中心,终止点则为数字的坐标,而每一个数字对应的与x轴线的夹角则相差30°,所以利用向量的长度、与x轴线的夹角,在利用正弦、余弦求出对应的x、y分量则为终止点的坐标。

所以我们来实现一个函数,根据角度,和线段长度求出对应终点坐标:

1
2
3
4
5
6
7
8
9
// ...

function genVect(radian, length) {
const x = length * Math.cos(radian)
const y = length * Math.sin(radian)
return [x, y]
}

// ...

利用上面定义的函数来获取每个数字的坐标信息,因为canvas的0°对应是x轴,且逆时针为弧度方向,所以这里添加0.5𝝅的偏移,然后再整体取负数则改变为顺时针旋转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 画数字 */
ctx.save()
ctx.fillStyle = '#000'
ctx.textBaseline = 'middle'
ctx.textAlign = 'center'
ctx.font = '58px Arial'
ctx.translate(CENTER_X, CENTER_Y) // 将原点移动到中心
for (let i = 0; i < 12; i++) {
const rad = -(i * 1 / 6 * Math.PI + 0.5 * Math.PI)
const [x, y] = genVect(rad, RADIUS - 80)
ctx.beginPath()
ctx.fillText(12 - i, x, y)
ctx.closePath()
}
ctx.restore()

到现在为止,我们已经完成了时钟底盘的绘制了,看起来基本没问题了,接下去要开始绘制三个主角,时针、分针和秒针。

2.2 绘制时针

时针、分针和秒针其实都可以看作是向量,我们只要确定向量的终点就能确定当前指针指向的位置,利用前面我们定义的genVect方法,来获取对应的坐标。参考前面的数学关系可以获得具体时刻对应的弧度值,假设现在是12点,约定12点的角度为0°,依此类推1点为30°,2点为60°,3点为90°等等。所以我们定义一个方法,通过小时点数获取对应的指针角度弧度值:

1
2
3
function getRadByHour(hour) {
return hour / 12 * 2 * Math.PI - 0.5 * Math.PI
}

由于分针、秒针的弧度逻辑和时针是一致的,所以我们修改下刚定义的getRadByHour,使之变成更通用的方法:

1
2
3
4
5
6
7
8
9
function createRadGetter(total) {
return function getRadByValue(value) {
return value / total * 2 * Math.PI - 0.5 * Math.PI
}
}

const getRadByHour = createRadGetter(12)
const getRadByMinute = createRadGetter(60)
const getRadBySecond = createRadGetter(60)

利用上面的函数,我们求出对应12点钟的弧度值, 然后利用genVect获得指针向量,再利用这两个数据进行时针的绘制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 画时针 */
const radHour12 = getRadByHour(12)
const [
vectXHour12,
vectYHour12
] = genVect(radHour12, RADIUS - 190) // RADIUS - 190 为时针的宽度

ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 24
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(vectXHour12, vectYHour12)
ctx.closePath()
ctx.stroke()
ctx.restore()

2.3 绘制分针

分针的绘制方法和时针一样,我们来绘制一个时刻为15分的分针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 画分针 */
const radMinute15 = getRadByMinute(15)
const [
vectXMinute15,
vectYMinute15,
] = genVect(radMinute15, RADIUS - 170)
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 18
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(vectXMinute15, vectYMinute15)
ctx.closePath()
ctx.stroke()
ctx.restore()

2.4 绘制秒针

与前面逻辑一样,画一个时刻为35秒的秒针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 画秒针 */
const radSecond35 = getRadBySecond(35)
const [
vectXSecond35,
vectYSecond35,
] = genVect(radSecond35, RADIUS - 100)
ctx.save()
ctx.strokeStyle = '#d84646'
ctx.lineWidth = 8
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(vectXSecond35, vectYSecond35)
ctx.closePath()
ctx.stroke()
ctx.restore()

2.5 绘制中心轴点

我们画一个颜色与秒针一致的中心轴点来美化一下时钟,可以利用arc来实现:

1
2
3
4
5
6
7
8
9
10
/* 画中心轴点 */
ctx.save()
ctx.fillStyle = '#d84646' // 设置填充色
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.arc(0, 0, 16, 0, 2 * Math.PI) // 半径16的圆
ctx.closePath()
ctx.fill() // 填充
ctx.restore()


到目前为止我们已经完成了整个时钟的大体结构和元素,但这只是静态的图像,需要让时钟真正的运行起来,所以接下来就着手指针的动态变化。

3. 动态变化指针

3.1 封装指针绘制方法

从前面可以知道,改变指针的指向位置,只要获得终点的坐标,也就是利用时间值去获取,我们可以利用已经完成的getRandByValuegenVect方法去动态地获取每一时刻指针向量的坐标,为了方便我们的绘制与更新,我们需要整理下前面绘制动作的代码,将绘制动作封装在方法中,这样就可以灵活地去完成每一步绘制动作:

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
/* 画基本要素 */
function drawBasic() {
drawBg()
drawBorder()
drawTick()
drawTimeText()
}

/* 背景 */
function drawBg() {
// ... 代码内容不变,参考前文
}

/* 画边框 */
function drawBorder() {
// ... 代码内容不变,参考前文
}

/* 画刻度 */
function drawTick() {
/* 画秒、分刻度 */
// ... 代码内容不变,参考前文

/* 画小时刻度 */
// ... 代码内容不变,参考前文
}

/* 画时间文本 */
function drawTimeText() {
// ... 代码内容不变,参考前文
}

/* 画时针 */
function drawPointerHour(hour) {
const rad = getRadByHour(hour)
const [ x, y] = genVect(rad, RADIUS - 200)
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 24
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(x, y)
ctx.closePath()
ctx.stroke()
ctx.restore()
}

/* 画分针 */
function drawPointerMinute(minute) {
const rad = getRadByMinute(minute)
const [x, y] = genVect(rad, RADIUS - 170)
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 18
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(x, y)
ctx.closePath()
ctx.stroke()
ctx.restore()
}

/* 画秒针 */
function drawPointerSecond(second) {
const rad = getRadBySecond(second)
const [x, y] = genVect(rad, RADIUS - 100)
ctx.save()
ctx.strokeStyle = '#d84646'
ctx.fillStyle = '#d84646'
ctx.lineWidth = 8
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()

/* 画指针 */
ctx.moveTo(0, 0)
ctx.lineTo(x, y)
ctx.stroke()

/* 画中心轴点 */
ctx.moveTo(0, 0)
ctx.arc(0, 0, 16, 0, 2 * Math.PI)

ctx.closePath()
ctx.fill()
ctx.restore()
}

然后我们调用下画秒针方法,测试一下:

1
drawPointerSecond(45) // 画一个45秒时刻的秒针

3.2 测试自动运行的秒针

前面测试的绘制秒针的方法能够正常运行,我们来实现一个自动运行的秒针,也就是从0秒到60秒的一个转动效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createSecondTester(render) {
let tick = 0
let timer = undefined
const totalTick = 60

const run = function () {
render(tick)
tick = (tick + 1) % totalTick
timer = setTimeout(run, 1000)
}

const stop = function () {
clearTimeout(timer)
}

return { run, stop }
}

const tester = createSecondTester(drawPointerSecond)

tester.run()

可以看时针正常地按照预期的步进弧度进行跳转,但是由于每次执行drawPointerSecond就会叠加一层,这是因为canvas的原理,每次绘制之前的内容都会被保留,在此的解决方案是,在每次绘制新指针时候,需要将之前的内容进行清除,然后重新绘制当前绘制之前的所有信息,在此,我们需要修改下render函数的逻辑:

1
2
3
4
5
6
7
8
9
const testRender = (tick) => {
ctx.clearRect(0, 0, CLOCK_WIDTH, CLOCK_HEIGHT) // 清空canvas所有内容
drawBasic() // 绘制表盘
drawPointerSecond(tick) // 绘制秒针
}

const tester = createSecondTester(testRender)

tester.run()

现在,我们拥有了一个跳动的秒针!

3.3 实现时、分、秒针联动

A.由于我们的指针绘制函数是根据时刻数值来绘制的,所以进行时刻数值的换算:

  1. m = 当前分钟 + (当前秒钟 / 60)
  2. h = 当前小时 + (m / 60)

B.所以整个render函数的逻辑为:

  1. 获取当前时、分、秒的数值
  2. 通过以上换算关系获得时针、分针的时刻值(浮点型)
  3. 使用最终得到的时针、分针、秒针的数值,调用对应绘制方法进行绘制

接下来,我们修改下render函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function render() {
const date = new Date()

const second = date.getSeconds() // 获取当前秒钟
const minute = date.getMinutes() // 获取当前分钟
const hour = date.getHours() % 12 // 获取当前小时(余12后得到12时制的数值)

// 根据换算关系进行换算
const m = minute + second / 60
const h = hour + m / 60
const s = second

ctx.clearRect(0, 0, CLOCK_WIDTH, CLOCK_HEIGHT)

drawBasic() // 画表盘

drawPointerHour(h) // 画时针
drawPointerMinute(m) // 画分针
drawPointerSecond(s) // 画秒针

}

将刚刚的测试运行器的逻辑进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createRunner(render) {
let timer = undefined

const run = function () {
render()
timer = setTimeout(run, 1000)
}

const stop = function () {
clearTimeout(timer)
}

return { run, stop }
}

最终我们创建一个运行器,然后让它跑起来,这样就可以实时根据当前实现来驱动我们的时钟了:

1
2
3
const runner = createRunner(render)

runner.run()

4. 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
const CLOCK_WIDTH = 800
const CLOCK_HEIGHT = 800

const CENTER_X = CLOCK_WIDTH / 2
const CENTER_Y = CLOCK_HEIGHT / 2

const RADIUS = 380

const clockCanvas = document.getElementById('clock')

clockCanvas.width = CLOCK_WIDTH
clockCanvas.height = CLOCK_HEIGHT

/** @type {CanvasRenderingContext2D} */
const ctx = clockCanvas.getContext('2d')

// 向量坐标函数
function genVect(radian, length) {
const x = length * Math.cos(radian)
const y = length * Math.sin(radian)
return [x, y]
}

// 获取弧度值
function createRadGetter(total) {
return function getRadByValue(value) {
return value / total * 2 * Math.PI - 0.5 * Math.PI
}
}

const getRadByHour = createRadGetter(12)
const getRadByMinute = createRadGetter(60)
const getRadBySecond = createRadGetter(60)

/* 画基本要素 */
function drawBasic() {
drawBg()
drawBorder()
drawTick()
drawTimeText()
}

/* 背景 */
function drawBg() {
ctx.save()
ctx.fillStyle = '#ddd'
ctx.fillRect(0, 0, CLOCK_WIDTH, CLOCK_HEIGHT)
ctx.restore()
}

/* 画边框 */
function drawBorder() {
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 2
ctx.beginPath()
ctx.arc(CENTER_X, CENTER_Y, RADIUS, 0, 2 * Math.PI)
ctx.closePath()
ctx.stroke()
ctx.restore()
}

/* 画刻度 */
function drawTick() {
/* 画秒、分刻度 */
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 1
for(let i = 0; i < 60; i++) {
ctx.save()
ctx.translate(CENTER_X, CENTER_Y)
ctx.rotate(i * 6 / 180 * Math.PI)
ctx.beginPath()
ctx.moveTo(RADIUS - 20, 0)
ctx.lineTo(RADIUS, 0)
ctx.closePath()
ctx.stroke()
ctx.restore()
}
ctx.restore()

/* 画时刻度 */
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 2
for(let i = 0; i < 12; i++) {
ctx.save()
ctx.translate(CENTER_X, CENTER_Y)
ctx.rotate(i * 30 / 180 * Math.PI)
ctx.beginPath()
ctx.moveTo(RADIUS - 40, 0)
ctx.lineTo(RADIUS, 0)
ctx.closePath()
ctx.stroke()
ctx.restore()
}
ctx.restore()
}

/* 画时间文本 */
function drawTimeText() {
ctx.save()
ctx.fillStyle = '#000'
ctx.textBaseline = 'middle'
ctx.textAlign = 'center'
ctx.font = '58px Arial'
ctx.translate(CENTER_X, CENTER_Y)
for (let i = 0; i < 12; i++) {
const rad = -(i * 1 / 6 * Math.PI + 0.5 * Math.PI)
const [x, y] = genVect(rad, RADIUS - 80)
ctx.beginPath()
ctx.fillText(12 - i, x, y)
ctx.closePath()
}
ctx.restore()
}

/* 画时针 */
function drawPointerHour(hour) {
const rad = getRadByHour(hour)
const [ x, y] = genVect(rad, RADIUS - 220)
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 24
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(x, y)
ctx.closePath()
ctx.stroke()
ctx.restore()
}

/* 画分针 */
function drawPointerMinute(minute) {
const rad = getRadByMinute(minute)
const [x, y] = genVect(rad, RADIUS - 170)
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = 18
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(x, y)
ctx.closePath()
ctx.stroke()
ctx.restore()
}

/* 画秒针 */
function drawPointerSecond(second) {
const rad = getRadBySecond(second)
const [x, y] = genVect(rad, RADIUS - 100)
ctx.save()
ctx.strokeStyle = '#d84646'
ctx.fillStyle = '#d84646'
ctx.lineWidth = 8
ctx.translate(CENTER_X, CENTER_Y)
ctx.beginPath()

/* 画指针 */
ctx.moveTo(0, 0)
ctx.lineTo(x, y)
ctx.stroke()

/* 画中心轴点 */
ctx.moveTo(0, 0)
ctx.arc(0, 0, 16, 0, 2 * Math.PI)

ctx.closePath()
ctx.fill()
ctx.restore()
}

/* 创建运行器 */
function createRunner(render) {
let timer = undefined

const run = function () {
render() // 执行渲染
timer = setTimeout(run, 1000) // 一秒后调用run,实现循环
}

const stop = function () {
clearTimeout(timer)
}

return { run, stop }
}

function render() {

const date = new Date()

const second = date.getSeconds()
const minute = date.getMinutes()
const hour = date.getHours() % 12

const m = minute + second / 60
const h = hour + m / 60
const s = second

ctx.clearRect(0, 0, CLOCK_WIDTH, CLOCK_HEIGHT)

drawBasic()

drawPointerHour(h)
drawPointerMinute(m)
drawPointerSecond(s)

}

const runner = createRunner(render)

runner.run()

在helpers文件夹中有许多Axios会用到的帮助函数,这些大多数跟特定的流程有关,比如cookie,URL相关的操作,接下来我们一个一个来看下。

1
2
3
4
5
6
7
8
9
10
11
12
13
lib/helpers
├── README.md # 说明
├── bind.js # 绑定函数上下文
├── buildURL.js # 创建URL字符串
├── combineURLs.js # 将绝对URL和相对URL结合
├── cookies.js # cookie操作相关
├── deprecatedMethod.js # 提示Http方法不推荐使用
├── isAbsoluteURL.js # 判断是否是绝对地址
├── isURLSameOrigin.js # 判断是否相同主机
├── isValidXss.js # 判断是否包含XSS的内容
├── normalizeHeaderName.js # 格式化Header的字段名
├── parseHeaders.js # 解析Header字符串,返回解析对象
└── spread.js # 调用函数并传入参数数组

1. bind.js

实现了Function.prototype.bind特性的方法

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = function bind(fn, thisArg) {
// 利用闭包
return function wrap() {
// 将arguments值转换成args数组
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
// 然后利用apply去调用
return fn.apply(thisArg, args);
};
};

2. buildURL.js

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
var utils = require('./../utils');

// 转义URL,但保留部分符号
function encode(val) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, '+').
replace(/%5B/gi, '[').
replace(/%5D/gi, ']');
}

// 将参数追加到URL后面形成新的URL地址
module.exports = function buildURL(url, params, paramsSerializer) {

// 参数对象不存在则直接返回原始URL
if (!params) {
return url;
}

var serializedParams; // 定义参数query的字符串变量

// 1.使用传入自定义的序列化函数进行操作
if (paramsSerializer) {
serializedParams = paramsSerializer(params);

// 2.如果参数是个URLSearchParams事例,则直接调用toString()方法
} else if (utils.isURLSearchParams(params)) {
serializedParams = params.toString();

// 3.按照默认的方式进行参数序列号操作
} else {
var parts = []; // 存放每个参数的序列化字符串

utils.forEach(params, function serialize(val, key) {
// 参数的值为空值,则直接跳过该参数
if (val === null || typeof val === 'undefined') {
return;
}

// 若值是个数组,则修改key的格式,如:list -> list[]
if (utils.isArray(val)) {
key = key + '[]';
} else {
val = [val]; // 构造成数组形式为了方便后续遍历
}

utils.forEach(val, function parseValue(v) {
// 值是Date类型,转换成ISO时间值文本
if (utils.isDate(v)) {
v = v.toISOString();
// 值引用类型直接使用JSON序列化,例如,数组或者对象
} else if (utils.isObject(v)) {
v = JSON.stringify(v);
}

// 存放序列化后的字符串,例如: 'a=123' 或者 'list[]=[1,2,3]'
parts.push(encode(key) + '=' + encode(v));
});
});

// 用`&`连接query字符对
// 例如:['a=1', 'b=2'] => 'a=1&b=2'
serializedParams = parts.join('&');
}

if (serializedParams) {
var hashmarkIndex = url.indexOf('#');
if (hashmarkIndex !== -1) {
// 有hash则去掉hash信息
url = url.slice(0, hashmarkIndex);
}

// 将query字符串拼接到URL后面
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
}

return url;
};

3. combineURL.js

1
2
3
4
5
6
module.exports = function combineURLs(baseURL, relativeURL) {
return relativeURL
// 这步骤为了保证中间连接的只有一个`/`分隔符
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL; // 没有相对URL则直接返回基础URL
};

去除基础URL的尾部/,去除相对URL的开头/, 然后在用/符号进行拼接,形成新的URL,这样的操作是为了

4. isAbsoluteURL.js

1
2
3
4
5
module.exports = function isAbsoluteURL(url) {
// 根据协议头确定是否为绝对URL
// 例如: http://xxx.xxx.xxx, //aaa.aaa.aa
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
};

5. isURLSameOrigin.js

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
var utils = require('./../utils');
var isValidXss = require('./isValidXss');

module.exports = (
utils.isStandardBrowserEnv() ?
(function standardBrowserEnv() {

// IE内核检测
var msie = /(msie|trident)/i.test(navigator.userAgent);

// 创建一个空的<a />标签element,用来解析href
var urlParsingNode = document.createElement('a');
var originURL;

// 通过<a href={url} />标签来解析URL
function resolveURL(url) {
var href = url;

// 检查是否包含XSS脚步攻击信息
if (isValidXss(url)) {
throw new Error('URL contains XSS injection attempt');
}

if (msie) {
// IE 需要设置两次的原生属性
urlParsingNode.setAttribute('href', href);
href = urlParsingNode.href;
}

// 将a标签的href设置为url,则会自动完成解析,
urlParsingNode.setAttribute('href', href);

// 返回的数据对象为URL接口规定的信息
// 其实这边可以利用URL()来解析
// 使用<a />提高兼容性
return {
/* 原始链接 */
href: urlParsingNode.href,
/* 协议 */
protocol: urlParsingNode.protocol
? urlParsingNode.protocol.replace(/:$/, '')
: '',
/* 主机 */
host: urlParsingNode.host,
/* query的字符串 */
search: urlParsingNode.search
? urlParsingNode.search.replace(/^\?/, '')
: '',
/* 锚点 */
hash: urlParsingNode.hash
? urlParsingNode.hash.replace(/^#/, '')
: '',
/* 主机名 */
hostname: urlParsingNode.hostname,
/* 端口 */
port: urlParsingNode.port,
/* 子路径 */
pathname: (urlParsingNode.pathname.charAt(0) === '/')
? urlParsingNode.pathname
: '/' + urlParsingNode.pathname
};
}

originURL = resolveURL(window.location.href);

// 相同主机则协议和主机都得一直
return function isURLSameOrigin(requestURL) {
var parsed = (utils.isString(requestURL))
? resolveURL(requestURL)
: requestURL;

return (parsed.protocol === originURL.protocol &&
parsed.host === originURL.host);
};
})() :

(function nonStandardBrowserEnv() {
return function isURLSameOrigin() {
return true;
};
})()
);

除了这种方式去解析一条url,也可以使用正则去匹配,还有就是利用URL()接口去实现,当然,作者的这种方式会更加取巧,也是看了这部分代码才了解此用法。

6. isValidXss.js

1
2
3
4
5
6
7
8
module.exports = function isValidXss(requestURL) {
// 检测包含XSS脚步攻击内容
// 1. 以on开头的事件回调,如:onclick=
// 2. 包含javascript
// 3. <script 或者 </script
var xssRegex = /(\b)(on\w+)=|javascript|(<\s*)(\/*)script/gi;
return xssRegex.test(requestURL);
};

7. deprecatedMethod.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = function deprecatedMethod(method, instead, docs) {
try {
console.warn(
'DEPRECATED method `' + method + '`.' +
(instead ? ' Use `' + instead + '` instead.' : '') +
' This method will be removed in a future release.');

if (docs) {
console.warn('For more information about usage see ' + docs);
}
} catch (e) { /* Ignore */ }
};

创建提示语专用函数

8. cookies.js

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
var utils = require('./../utils');

module.exports = (
utils.isStandardBrowserEnv() ?
// 标准web环境支持document.cookie
(function standardBrowserEnv() {
return {
// 写入
write: function write(
name, value, expires, path, domain, secure
) {
var cookie = [];

// 一下步骤则是根据key的类别构建 "key=value"形式键值对
cookie.push(name + '=' + encodeURIComponent(value));
if (utils.isNumber(expires)) {
cookie.push('expires=' + new Date(expires).toGMTString());
}

if (utils.isString(path)) {
cookie.push('path=' + path);
}

if (utils.isString(domain)) {
cookie.push('domain=' + domain);
}

if (secure === true) {
cookie.push('secure');
}

// 最后写入到document.cookie中,修改直接赋值替换就行
document.cookie = cookie.join('; ');
},

// 读取
read: function read(name) {
var match = document.cookie.match(
// 在cookie字符串中匹配对应key=value
new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')
);

return (match ? decodeURIComponent(match[3]) : null);
},

// 删除
remove: function remove(name) {
// 设置值为'', 过期时间为24小时前
this.write(name, '', Date.now() - 86400000);
}
};
})() :

// 非标准web环境
(function nonStandardBrowserEnv() {
return {
write: function write() {},
read: function read() { return null; },
remove: function remove() {}
};
})()
);

9. parseHeaders.js

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
var utils = require('./../utils');

// 这些字段的header值如果有多个,则只保留一个值、其他值会被忽略
var ignoreDuplicateOf = [
'age', 'authorization', 'content-length', 'content-type', 'etag',
'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since',
'last-modified', 'location', 'max-forwards', 'proxy-authorization',
'referer', 'retry-after', 'user-agent'
];

// 解析headers文本为Object形式
module.exports = function parseHeaders(headers) {
var parsed = {};
var key;
var val;
var i;

if (!headers) { return parsed; }

// 根据'\n'将分割成独立的行,然后遍历去解析取值
utils.forEach(headers.split('\n'), function parser(line) {
i = line.indexOf(':');

// ':'的左侧为key
key = utils.trim(line.substr(0, i)).toLowerCase();

// ':'的右侧为val
val = utils.trim(line.substr(i + 1));

if (key) {
// 已存在值并且字段为单一值的头部字段,则不进行进一步动作
if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
return;
}

// set-cookie的格式解析, set-cookie多个值则为数组
if (key === 'set-cookie') {
parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);

// 其他格式的解析, 多个值用','隔开
} else {
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
}
}
});

return parsed;
};

10. normalizeHeaderName.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 将headers中的name常规化
module.exports = function normalizeHeaderName(headers, normalizedName) {
utils.forEach(headers, function processHeader(value, name) {

// 数据中的name和传入的normalizedName大小写不一致, 统一转化成大写,
if (name !== normalizedName
&& name.toUpperCase() === normalizedName.toUpperCase()
) {
headers[normalizedName] = value; // 将normalizedName设置成为新key
delete headers[name]; // 删除原始的key
}
});
};

/* 例如 */
const headers = { 'content-type': 'application/json' }
normalizeHeaderName(headers, 'Content-Type')

headers // -> { 'Content-Type': 'application/json' }

// 这样就将'content-type'正常化为'Content-Type'

11. spread.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* function f(x, y, z) {}
* var args = [1, 2, 3];
* f.apply(null, args);
*
* 形式转换
*
* spread(function(x, y, z) {})([1, 2, 3]);
*/
// 这个函数没什么特殊的,就是方便apply调用
module.exports = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};

文档链接:

Axios源码解读「整体代码文件结构说明」
Axios源码解读「帮助方法Helpers」
Axios源码解读「通用工具函数」
Axios源码解读「适配器Adapter」
Axios源码解读「实现请求的取消中断Cancel」
Axios源码解读「默认配置项default」
Axios源码解读「核心逻辑部分Core」
Axios源码解读「静态API的注入」