Jenkins agent/pipeline

Agent

官方建议使用无论何时都使用 Agent 执行任务,而不是使用内置的节点

停用内置 Agent
  • 在 Manage Jenkins -> Nodes -> Build-In Node 的配置中,配置 Number of executors 为 0。

  • label 填写 don’t use Built-In node

  • Usage 选择 Only build jobs with label expressions matching this node。

  • 点击保存,这样就不会使用内置节点执行任务了。

创建新的 Agent

Agent 节点可以是物理机,也可以是虚拟机。必须装有和 Jenkins controller 相同的 Java 环境, 不需要安装 jenkins, 只用于执行任务。

  • 点击新建节点,选择 Permanent agent

  • Number of executors 填写数量不能大于物理核心数,或是虚拟核心数。

  • Remote root directory 会从 Jenkins controller 中同步 jenkins 相关文件,确保放在登录用户有权限执行的目录下,例如 /home/user/jenkins, user 是 Agent 节点的登录用户名。

  • Labels 可以描述当前 Agent 的用途内置的环境

  • Usage 选择 Use this node as much as possible

  • Launch method 选择 Launch agents via SSH,填写 Host,并创建登录凭证, 凭证的类型一定要选择 username with password

    在使用凭证登录的时候,需要校验相关权限,会去查找 Jenkins controller 上的 ~/.ssh 目录下的 known_hosts 文件,如果没有则会需要手动创建 .ssh 文件夹(确保权限正确,不是 root 用户),在使用以下命令同步认证信息 ssh-keyscan -H your_agent_host >> /home/your_user_name/.ssh/known_hosts

    Host Key Verification Strategy 选择 Known hosts file Verification Strategy 进行验证

  • 执行 docker 命令时报错 permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.48/containers/node-18:alpine/json": dial unix /var/run/docker.sock: connect: permission denied

    Jenkins 会以 jenkins 用户身份运行, 把该用户加入本机 docker 组(例如:docker),以便它可以读写 /var/run/docker.sock

    1
    2
    3
    4
    sudo usermod -aG docker jenkins

    # 如果仍然没有权限,尝试重启系统
    docker run hello-world
  • 点击保存,Jenkins 会自动检查 Agent 链接

Pipeline

workspace

通过在线的方式创建一个简单的 pipeline

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
pipeline {
agent any

stages {
stage('without docker') {
steps {
sh '''
touch without-docker.txt
'''
}
}

stage('with docker') {

agent {
docker {
image 'node:18-alpine'
}
}
steps {
sh '''
npm -v
touch with-docker.txt
'''
}
}
}
}

执行结束后在任务的 workspaces 目录下可以看到生成的多个文件夹,每个文件夹对应一个执行阶段

1
2
/home/jenkins/jenkins-agent/workspace/simple-pipeline on Agent1
/home/jenkins/jenkins-agent/workspace/simple-pipeline@2 on Agent1

如果想要合并这些文件夹,共用上一阶段的产物,添加如下的配置

1
2
3
4
5
6
agent {
docker {
image 'node:18-alpine'
reuseNode true
}
}
Gitlab Plugin Pipeline
  • 测试 Gitlab 对 Jenkins 的认证

    Jenkins 中点击右上角的头像,选择 security, 创建一个 API Token.

    点击任意一个任务,查看浏览器地址栏 https://jenkins.iftrue.me/job/simple-freesyle/,simple-freesyle 即为项目的名称(特殊字符需要转义,所以按地址栏中显示的准), 记下这个项目名称

    在 Gitlab 中进入到一个项目中, setting -> webhooks -> add webhook, 填写 Jenkins 的地址,规则为 https://[Jenkins用户ID]:[Jenkins_Api_Token]@jenkins.iftrue.me/project/[项目名称]

    点击测试通过,在内网环境中可能需要关闭 webhook 的 ssl 验证,才不会报错。(内网环境使用 nginx 作为统一入口,会提示 unable to get local issuer certificate

  • 配置 Jenkins 对 Gitlab 的认证

    此认证配置仅用于访问 GitLab API,以便向 GitLab 发送构建状态。它不用于克隆 git 仓库。用于克隆的凭证(通常是 SSH 凭证)应该在 git 插件中单独配置。

    创建 Gitlab access tokens, setting -> Access Tokens, 选择 api 权限,复制生成的 token

    在 Jenkins 中创建一个凭证,选择 GitLab API Token, 填写生成的 Gitlab Access Token

    jenkins -> System Manage , 找到 GitLab 部分,填写 GitLab host URL 和创建的 GitLab API Token 凭证。(内网环境可能有证书报错的问题,在 高级选项中勾选 Ignore SSL Certificate Errors)

    保存后测试链接是否成功。

  • 任务触发配置

    在一个 pipeline 项目中配置触发器,在 Build Triggers 配置中

    选择 Build when a change is pushed to GitLab,复制 GitLab webhook URL 地址,将项目部分的地址替换到 Gitlab 的 webhook 中,Jenkins 用户名和 Api token 仍然需要保留,点击测试通过。

    继续选择 Push Events,表示在 Gitlab 推送代码时触发 Jenkins 的构建。

    pipeline 中选择 pipeline script from SCM, 选择 GitLab 作为 SCM,填写仓库地址。

    保证在 Jenkins controller 中有 Git 环境,否则校验命令会执行失败。

    如果提示 Host key verification failed., Jenkins 凭证中创建一个 SSH Username with private key 类型的凭证,填写 Gitlab 服务器的用户名,Jenkins controller 中的私钥 (如果私钥有密码也需要填写密码)。

    在 Gitlab SSH Key 管理中添加 Jenkins controller 的公钥,这样可以完成 git 的认证。

    如果任务在 Jenkins agent 中执行,Jenkins 默认使用 known_hosts 文件进行认证,gitlab 的认证信息必须添加到 Jenkins agent 的 known_hosts 文件中。

    1
    ssh-keyscan -H gitlab_host >> /home/jenkins/.ssh/known_hosts

    也可以直接在 System -> Security -> Git Host Key Verification Configuration 中修改为 Accept first connection 会在首次链接后自动保存这个主机指纹。

    这样就可以在拉取代码后,执行项目中的 jenkinsfile 文件

拉取项目

没有编译的过程,当任务被触发时,agent 节点会直接连接上目标服务器,拉取项目并更新并重新执行项目

安装 Publish Over SSH 插件, 进入系统管理 -> Publish over SSH

填写 Jenkins controller 的 SSH Key 密码,添加一个服务器配置,填写服务器的 IP 地址和用户名, 点击测试通过

配置 jenkinsfile

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
pipeline {
agent any
stages {
stage('Deploy') {
steps {
sshPublisher(publishers:
[sshPublisherDesc(
// Publish over SSH 插件配置的服务器名称
configName: 'crawler server',
transfers: [
sshTransfer(
// 远程目录,从 /home/your_username 目录下创建
remoteDirectory: 'crawler',
// 不能使用数组,多类文件可以使用多个 sshTransfer
sourceFiles:'*/**'
),
sshTransfer(
// 由于ssh链接不是使用的交互式命令行所以.bashrc文件不会被执行
// 需要手动执行 source ~/.nvm/nvm.sh 以便于找到npm命令
execCommand: 'cd crawler && source ~/.nvm/nvm.sh && npm ci && npm run build && npm run start'
)
],
// 开启查看详细的报错信息
verbose: true
),
]
)
}
}
}
}

计算机基础

位/字节

特性 位(bit) 字节(byte)
定义 最小信息单位 基本存储和处理单位
大小 1 位 = 1 个二进制值 1 字节 = 8 位(现代标准)
状态数量 2^1 = 2 2^8 = 256
用途 表示简单状态或逻辑值 存储字符、数据或文件
典型应用 网络传输速率、标志位 内存大小、文件大小

单位转换

单位 换算为 Byte 换算为 bit
1 Byte 1 Byte 8 bit
1 KB 1024 Bytes (二进制) 8192 bit (二进制)
1 MB 1024 × 1024 Bytes (二进制) 8,388,608 bit (二进制)
1 GB 1024 × 1024 × 1024 Bytes (二进制) 8,589,934,592 bit (二进制)
1 TB 1024^4 Bytes (二进制) 8,796,093,022,208 bit (二进制)

继电器/MOS 管

  • 继电器

    继电器通过机械开关的闭合和断开,完成逻辑门(如与门、或门、非门)的功能。

    通过组合继电器,可以实现复杂的逻辑电路和算术运算,如加法器和条件判断。

    继电器的输入和输出电路物理隔离,适合处理不同电压和电流的控制任务。

  • MOS 管

    MOS 管作为场效应晶体管(FET)的一个重要分支,是构建逻辑门(如与、或、非等)的基础元件。

    数十亿个 MOS 管可以集成在单个芯片上,构成现代计算机的中央处理器(CPU)、存储器和其他数字电路。

    静态随机存取存储器(SRAM)和动态随机存取存储器(DRAM)都基于 MOS 管设计。DRAM 中,每个存储单元使用一个 MOS 管和一个电容,极大地提高了存储密度。

    静态随机存取存储器(SRAM)和动态随机存取存储器(DRAM)都基于 MOS 管设计。DRAM 中,每个存储单元使用一个 MOS 管和一个电容,极大地提高了存储密度。

    MOS 管非常小,可以集成到硅芯片中,从而实现计算机的小型化。集成电路(IC)和超大规模集成电路(VLSI)都是基于 MOS 技术。

计算机内存原理

MOS 管原理

半加器/全加器

半加器:处理一位二进制运算的逻辑电路,本位值相当于异或运算(00,11 相同的时候为 0,01,10 不同的时候为 1),进位值相当于与运算(相同时发生进位,值为 1)

全加器:相比于半加器的两个输入外,多一个上一次进位的输入,因此用两个半加器可以实现一个全加器,最后通过一个或门,确认进位的输出。

将多个全加器连接起来实现波纹进位加法器,可以实现多位的加法运算。但是由于用到的门过多造成延时过长,因此还有超前进位加法器,分层超前进位加法器。

Javascript 常见问题

柯里化

‌函数柯里化(Currying)是一种将接受多个参数的函数转换为接受单个参数的函数的技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const curry = <T extends (...args: any[]) => any>(fn: T): CurryFunction<T> => {
let args: any[] = [];
return function curryFunction(this: any, arg: any) {
args.push(arg);
if (args.length >= fn.length) return fn.apply(this, args);
return curryFunction;
} as CurryFunction<T>;
};

type CurryFunction<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => infer R
? P extends [infer F, ...infer Rest]
? (arg: F) => CurryFunction<(...args: Rest) => R>
: R
: never;

const a = curry((a: string, b: boolean, c: number): void => {
console.log(a, b, c);
});

反柯里化

1

web中的拦截技巧

代码注入时机

  • 源码注入
  • 构建,推送服务注入
  • 网关注入,nginx 等修改响应文件
  • 浏览器插件注入

重写 api

通过 重写 fetch xhr 原型链,添加额外的功能

1
2
3
4
5
6
7
8
9
10
const _console = window.console;
window.console = new Proxy(_console, {
get(target, props) {
const fn = target[props];
return (...args) => {
fn.apply(null, args);
_console.info("other info");
};
},
});

拦截事件

1
2
3
4
5
6
7
8
9
10
11
12
13
document.addEventListener(
"click",
(e) => {
e.stopPropagation();
e.preventDefault();
},
{
// 捕获阶段执行
capture: true,
// 一个布尔值,设置为 true 时,表示 listener 永远不会调用 preventDefault()
passive: false,
}
);

监听 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
// 选择需要观察变动的节点
const targetNode = document.getElementById("some-id");

// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for (let mutation of mutationsList) {
if (mutation.type === "childList") {
console.log("A child node has been added or removed.");
} else if (mutation.type === "attributes") {
console.log("The " + mutation.attributeName + " attribute was modified.");
}
}
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);

// 之后,可停止观察
observer.disconnect();

监听对象属性变化

1
2
3
4
5
6
7
8
9
10
11
const obj = { a: 1 };

Object.defineProperty(obj, "a", {
get() {},
set(v) {},
});

const newObj = new Proxy(obj, {
get(target, props) {},
set(target, props, value) {},
});

service worker

service worker

1
this.addEventListener("fetch", function (event) {});

web container

webcontainers

整合远程调试方案

根据以上的拦截技巧可以整个一个远程调试的方案,可以实现以下的功能:

  • 实现共享域名的登录态 cookie
  • 在远程设备(手机、测试设备)调试本地开发中服务无需配置 Web 服务的 https 直接使用 https 协议访问开发服务,避免 http 协议导致许多 Web API 不可用仅限于安全上下文的特性 (opens new window)
  • 该服务是一个天然的中间层,可无感注入代码实现效率工具,比如:远程网络抓包、Mock 移动端控制台(eruda)远程代码调试(chii)切换后端接口环境、接口染色

思路:

  • 客户端发起 https 请求,并在请求路径中添加 ip port

  • nginx 拦截指定域名的所有请求
    如果请求的是 html 文件,则 注入客户端的 sdk.js

    1
    2
    3
    4
    5
    location / {
    sub_filter '</body>' '<script src="/sdk.js"/></body>';
    root html;
    index index.html;
    }

    同时把 ip port 写入 cookie

  • 如果是资源请求,直接从 cookie 中读取 ip port

  • sdk 中拦截所有的 a 标签的默认事件,在跳转路径上添加 ip port

  • socket 请求需要重写 socket api,让其携带域名和端口访问

v8引擎相关知识

作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

全局作用域 函数作用域 块级作用域

块级作用域通过 词法环境(Lexical Environment) 实现,通过 const let 声明的变量具有块级作用域,只能在包含他们的代码块中访问。如果在顶级使用 let const,他么们不会被添加到 window 对象上,但是会在全局作用域中

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

作用域链

其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。

JavaScript 引擎首先会在当前的执行上下文中查找该变量,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
foo();
  • 进行词法分析,a,c 加入到变量环境 Variable Environment = {a:undefined,c:undefined}
    b 会加入到词法环境,解析块级作用域中的代码,需要开辟新的词法环境 Lexical Environment = {b:不可达}=> {b:不可达,d:不可达}

    let 只会在执行的时候赋值,在词法分析阶段会被识别,但是不能访问,也就是暂时性死区(TDZ,Temporal Dead Zone)

  • 函数执行,a,b 被赋值,Variable Environment = {a:1,c:undefined}Lexical Environment = {b:2} => {b:3,d:5}
    会先查找词法环境,在查找变量环境

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

执行上下文

执行上下文负责运行时代码的管理,包括作用域,变量,对象,函数生命周期。

  • 创建阶段,确定 this,创建变量环境,创建词法环境。
  • 执行阶段,函数内部代码开始执行,变量赋值,函数引用和执行。
  • 回收阶段,当函数执行完毕后,从相应的栈中弹出,资源被释放。

内存

JavaScript 是一种弱类型的、动态的语言.

  • 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。
  • 动态,意味着你可以使用同一个变量保存不同类型的数据。

数据科技分为原始类型引用类型

原始类型的数据保存在环境变量或词法环境中,包含这两个区域的执行上下文又被压入调用栈中,所以可以说原始类型是保存在栈空间中的.

字符串,symbol,bigint 虽然是原始类型,实际还是存放在堆空间的。

如果是一个引用类型,会单独存放到堆空间中,在分配了引用类型数据之后会拿到一个堆中的地址,在把这个地址保存到环境变量中.

JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了.

所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。

从内存的角度来理解闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
let name = "one";
const obj = {
getName() {
return name;
},
setName(_name) {
name = _name;
},
};
return obj;
}

var bar = foo();
bar.setName("two");
bar.getName();

当 foo 函数执行的时候,首先是编译的过程,当遇到对象的两个方法时还需要对两个方法进行词法分析, 发现引用了 name 变量.

JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 name 变量。并把堆空间的地址保存在 foo 执行上下文中的环境变量中.

垃圾回收

分为栈内存回收,堆内存回收

栈内存回收需要用到一个记录当前执行状态的指针(称为 ESP),指向的就是当前的执行上下文,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。当下移之后如果有新的函数调用,原来的内存位置就会写入新的执行上下文.

回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器,垃圾回收的策略都是建立代际假说的基础之上的

代际假说有以下两个特点:

  • 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
  • 第二个是不死的对象,会活得更久。

需要根据对象变量的不同生命周期长短使用不同的策略:

在 V8 中会把堆分为新生代老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。新生区通常只支持 1 ~ 8M 的容量.副垃圾回收器,主要负责新生代的垃圾回收。主垃圾回收器,主要负责老生代的垃圾回收。

垃圾回收大致都分为一下几个步骤:

  • 标记活动对象和非活动对象
  • 清楚非活动对象
  • 内存整理,并不是所有的回收策略都需要这一步

副垃圾回收器主要负责新生区的垃圾回收. 大多数小的对象都会被分配到新生区,这个区域虽然不大,但是垃圾回收还是比较频繁的。用 Scavenge[ˈskævɪndʒ] 算法来处理.原理是:

把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区.

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。

完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。

由于新生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。

也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

主垃圾回收器 主要负责老生区中的垃圾回收。

除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。

标记阶段就是从一组根元素(调用栈)开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

因为清除部分内存会产生碎片,而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact),这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

全停顿,内存回收过程过长,又因为 JS 为单线程而阻塞 JS 执行,这个问题叫全停顿.

对于新生代的内存回收影响不大,因为内存比较小,可以全停顿等待执行完成.对于老生代会将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法

使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

JS 如何执行

由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。

编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了

字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。解释器 Ignition 除了负责生成字节码之外,它还有另外一个作用,就是解释执行字节码。在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和 Python 的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译(JIT)。

处理新任务

当一个线程在执行任务的时候,如何可以处理新任务,最简单的办法就是通过一个循环,不断检测是否有新的任务产生。

这样可以解决同一个线程中产生的新任务,但是无法解决其他线程中产生的新任务。因为没有办法直接检测其他线程是否有新任务的产生。

通用的模型就是消息队列,息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程

任务类型

任务类型包括, 输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等。

除此之外,消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。

以上这些事件都是在主线程中执行的,所以在编写 Web 应用时,你还需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。

高优先级任务

一个典型的场景是监听 DOM 的改变做一些逻辑处理,如果不加入消息队列选择同步处理,在 DOM 频繁改变的时候,当前任务会被延长,导致后面的任务不能及时处理。

如果加入到消息队尾部,又可能影响效率,因为可能已经有很多任务在排队了。

针对这种情况微任务就产生了,通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。

事件循环

浏览器处理消息队列用到了事件循环系统,但这个事件循环与 nodejs 事件循环没有关系。

  • V8: V8 引擎自己实现了一个事件循环,但是 nodejs 和 浏览器都没有采用
  • 浏览器: 不同的厂商实现可能不同,chrome 浏览器使用 libevent 实现事件循环。
  • nodejs: 使用 libuv 实现事件循环。

setTimeout 如何实现

从使用方式上能感觉到,setTimeout 需要等待指定时间才能执行,而消息队列中的任务是立即执行的,所以 setTimeout 中的回调函数不能立即加入到消息队列中。

所以浏览器还维护着一个延时任务列表,包括定时器和内部一些延时任务,创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。这个回调任务包括,定义的回调函数,发起时间,延时时间。

在处理消息队列的时候,会调用出延时任务的方法 ProcessTimerTask 。这个方法的调用时机是当前事件循环中一个任务处理结束后开始执行。

ProcessDelayTask 函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。

在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,在 5 次调用之后,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。

未被激活的页面中定时器最小值大于 1000 毫秒,也就是说,如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。

Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,那么相当于延时值被设置为 0 了,这导致定时器会被立即执行。

XMLHttpRequest 实现流程

渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数。

宏任务和微任务

前面已经介绍过微任务的由来。

宏任务:

  • 渲染事件(如解析 DOM、计算布局、绘制);
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
  • JavaScript 脚本执行事件;
  • 网络请求完成
  • 文件读写完成事件

WHATWG 规范中定义事件循环机制:

  • 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
  • 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
  • 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
  • 最后统计执行完成的时长等信息。

由于宏任务不能精细的控制执行的时机,因为两个红任务之间可能被插入了很多系统级的任务。

微任务:

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

  • MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • Promise 当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。WHATWG 把执行微任务的时间点称为检查点。

如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。

  • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
  • 微任务的执行时长会影响到当前宏任务的时长。
  • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。

通过异步操作解决了同步操作的性能问题;通过微任务解决了实时性的问题。

W3C 最新解释:

  • 每一个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。

  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行。

因此现在 chrome 中至少有 3 个队列:

  • 延时队列: 用于存放定时器到达后的回调任务,优先级中
  • 交互任务: 用于存放用户交互产生的任务,优先级高
  • 微队列: 用于存放最快需要执行的任务,优先级最高

Promise.then 返回 Promise 的行为

当一个 promise.then 方法返回另一个 Promise 时,thenable 的状态会吸收返回的 promise 的状态,也就是说 thenable 的状态与返回的 promise 状态保持一致

但是这种状态并不是立即吸收的,返回的 promise 会被使用 then 方法,包装成新的 promise 对象,并添加到微队列中

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
Promise.resolve()
.then(() => {
//1 添加到微队列
console.log(0);

//3 打印0
//4 包装成 (Promise.resolve(4).then(res=>res)).then(res=>res) => p4.then(res=>res)添加到微队列
//7 res=>res 添加到微队列
return Promise.resolve(4).then((res) => res);
//10 完成状态
})
.then((res) => {
//11 添加到微队列
console.log(res);
//14 打印4
});

Promise.resolve()
.then(() => {
//2 添加到微队列
console.log(1);
//5 打印1
})
.then(() => {
//6 添加到微队列
console.log(2);
//8 打印2
})
.then(() => {
//9 添加到微队列
console.log(3);
//12 打印3
})
.then(() => {
//13 添加到微队列
console.log(5);
//15 打印3
});

代码美学(摘要)

如何给变量或类型起名字

不要使用以下的命名方式

  • 不要使用单字母:简洁的变量命名是来自数学中的命名习惯,数学家喜欢公式的精简和优美,但是会丢失大量的上下文信息,让你读代码的时间比写代码的时间还要长

  • 不要使用缩写:与单字母的方式一样,无法理解上下文的信息,让代码很难阅读

  • 不要在命名中添加类型说明: 新生的程序工作者可能不会接触到这个命名方式,它来自于匈牙利命名法在类型提示不完善的年代,增加对变量类型的说明 intTotal,strName

  • 不要再类型中添加类型描述: 最典型的就是对interface的定义使用 IBoxProps

  • 不要再类或类型中添加 BaseAbstract: 可能你会习惯性的把基础类命名中添加 Base, 但是这并不能说明他应该被继承或是被实现,只需要直接命名即可,例如 Box
    另外你的命名对子类如何使用并不会有影响,例如 (box:Box)=>void, 并不关心 Box 是不是抽象类
    当你不知道如何给基础类命名的时候,可以考虑是否需要对子类修改命名方式,添加更多信息, 例如子类实现了不同颜色的盒子 YellowBox

推荐的命名方式

  • 在命名中增加单位描述: delaySeconds , 对于一些强类型语言可以使用类型标注,例如 C# TimeSpan, 而对于弱类型语言可以在命名中添加单位描述

你可能不需要Utils文件

可能你正在用一个Utils方法过滤电影列表

1
2
3
class Utils {
filterMove(){}
}

但事实上这应该是电影类中的一部分

1
2
3
class Movie {
filterMove(){}
}

当你准备写一个utils方法的使用请考虑它是否足够抽象, 亦或者考虑将他放到更明确的类或功能模块中去。

使用组合而不是继承

继承最大的问题在于,无法找到一个完美的抽象,随着业务和需求的发展,总是需要调整被继承的类,这将会影响全局。

如果需要使用类,遵循以下的原则:

  • 需要实现的类中有大量重复的接口
  • 避免直接访问受保护的成员变量
  • 显式的为子类创建需要重写的API
  • 每个方法都需要标注行为 final/sealed/private, 避免修改时导致错误

如是使用继承,需要注意以下几点:

  • 组合会导致功能定义时产生大量的重复初始化代码
  • 如果想从类中暴露方法,需要写大量的包装函数
    1
    2
    3
    4
    5
    class Box{
    getName(){
    return this.type.getName()
    }
    }
  • 虽然会导致少量冗余,但是好处远远大于坏处,它可以让你的代码耦合程度更低。

vue/dev-server

demo 介绍

demo 展示在页面中直接引入 esModule 文件,并在文件中引入 vue 组件的渲染流程

渲染流程

sequenceDiagram autonumber participant Client participant Server Client->>Server: /main.js Note left of Server: 返回资源的时候处理node_modules中依赖的路径 Server->>Client: main.js文件内容 Note left of Server: __module/vue 和 test.vue Client->>Server: 浏览器分析分析并请求import资源 Server->>Client: 返回依赖资源或是打包后的js文件
  • 浏览器向服务器请求入口文件 main.js, 请求路径为 import 路径 .main.js

  • 服务端接收到请求,交给 middleware 处理,首先检查缓存,如果缓存存在直接返回缓存结果,如果不存在,通过请求路径读取本地资源文件,处理后加入缓存并返回

  • 如果文件是 js 结尾, 这里表示的是入口文件

    • 将文件内容转为 ast 语法树, 分析依赖模块包括 vue 和 text.vue,将 node_module 中的依赖路径转换为自定义路径用于资源请求时区分资源并可以加入缓存优化,例如__module/vue,将 ast 生成的代码返回给浏览器
    • 浏览器会自动请求 esMoudle 中 import 的文件
      http://localhost:3000/\_\_modules/vue
      http://localhost:3000/test.vue
  • 如果文件路径中包含 \_\_moudles, 则尝试加载 node_modules 中的文件
    通过 require.resolve("vue") 可以获取某个包在 node_modules 中的绝对路径

  • 如果文件路径是以 .vue 结尾, 需要使用 vue 提供的编译模块将文件编译成单个 js 文件

    • 首先通过 vueComplier compileToDescriptor 将 vue 文件处理 template, styles,scripts 的描述对象
    • 在使用 vueCompiler.assemble 将描述文件中的各部分组装成代码,返回给浏览器

middleware.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
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
const vueCompiler = require("@vue/component-compiler");
const fs = require("fs");
const stat = require("util").promisify(fs.stat);
const root = process.cwd();
const path = require("path");
const parseUrl = require("parseurl");
const { transformModuleImports } = require("./transformModuleImports");
const readFile = require("util").promisify(fs.readFile);
const parseUrl = require("parseurl");
const defaultOptions = {
cache: true,
};

async function loadPkg(pkg) {
if (pkg === "vue") {
const dir = path.dirname(require.resolve("vue"));
const filepath = path.join(dir, "vue.esm.browser.js");
return readFile(filepath);
} else {
// TODO
// check if the package has a browser es module that can be used
// otherwise bundle it with rollup on the fly?
throw new Error("npm imports support are not ready yet.");
}
}
async function readSource(req) {
const { pathname } = parseUrl(req);
const filepath = path.resolve(root, pathname.replace(/^\//, ""));
return {
filepath,
source: await readFile(filepath, "utf-8"),
updateTime: (await stat(filepath)).mtime.getTime(),
};
}
function transformModuleImports(code) {
const ast = recast.parse(code);
recast.types.visit(ast, {
visitImportDeclaration(path) {
const source = path.node.source.value;
if (!/^\.\/?/.test(source) && isPkg(source)) {
path.node.source = recast.types.builders.literal(
`/__modules/${source}`
);
}
this.traverse(path);
},
});
return recast.print(ast).code;
}

const vueMiddleware = (options = defaultOptions) => {
let cache;
let time = {};
if (options.cache) {
const LRU = require("lru-cache");

cache = new LRU({
max: 500,
length: function (n, key) {
return n * 2 + key.length;
},
});
}

const compiler = vueCompiler.createDefaultCompiler();

function send(res, source, mime) {
res.setHeader("Content-Type", mime);
res.end(source);
}

function injectSourceMapToBlock(block, lang) {
const map = Base64.toBase64(JSON.stringify(block.map));
let mapInject;

switch (lang) {
case "js":
mapInject = `//# sourceMappingURL=data:application/json;base64,${map}\n`;
break;
case "css":
mapInject = `/*# sourceMappingURL=data:application/json;base64,${map}*/\n`;
break;
default:
break;
}

return {
...block,
code: mapInject + block.code,
};
}

function injectSourceMapToScript(script) {
return injectSourceMapToBlock(script, "js");
}

function injectSourceMapsToStyles(styles) {
return styles.map((style) => injectSourceMapToBlock(style, "css"));
}

async function tryCache(key, checkUpdateTime = true) {
const data = cache.get(key);

if (checkUpdateTime) {
const cacheUpdateTime = time[key];
const fileUpdateTime = (
await stat(path.resolve(root, key.replace(/^\//, "")))
).mtime.getTime();
if (cacheUpdateTime < fileUpdateTime) return null;
}

return data;
}

function cacheData(key, data, updateTime) {
const old = cache.peek(key);

if (old != data) {
cache.set(key, data);
if (updateTime) time[key] = updateTime;
return true;
} else return false;
}

async function bundleSFC(req) {
const { filepath, source, updateTime } = await readSource(req);
const descriptorResult = compiler.compileToDescriptor(filepath, source);
console.log(descriptorResult);
const assembledResult = vueCompiler.assemble(compiler, filepath, {
...descriptorResult,
script: injectSourceMapToScript(descriptorResult.script),
styles: injectSourceMapsToStyles(descriptorResult.styles),
});
return { ...assembledResult, updateTime };
}

return async (req, res, next) => {
if (req.path.endsWith(".vue")) {
const key = parseUrl(req).pathname;
let out = await tryCache(key);

if (!out) {
// Bundle Single-File Component
const result = await bundleSFC(req);
console.log(result);
out = result;
cacheData(key, out, result.updateTime);
}

send(res, out.code, "application/javascript");
} else if (req.path.endsWith(".js")) {
const key = parseUrl(req).pathname;
let out = await tryCache(key);

if (!out) {
// transform import statements
const result = await readSource(req);
out = transformModuleImports(result.source);
console.log(out);
cacheData(key, out, result.updateTime);
}

send(res, out, "application/javascript");
} else if (req.path.startsWith("/__modules/")) {
const key = parseUrl(req).pathname;
const pkg = req.path.replace(/^\/__modules\//, "");

let out = await tryCache(key, false); // Do not outdate modules
if (!out) {
out = (await loadPkg(pkg)).toString();
cacheData(key, out, false); // Do not outdate modules
}

send(res, out, "application/javascript");
} else {
next();
}
};
};

exports.vueMiddleware = vueMiddleware;

隐式转换

一元操作符

一元操作符包括,+, -, ~, !,其中 +, - 需要和加法运算符区分开。

ECMA 规定了 一元操作符 求值过程。

1
2
3
4
console.log(+true); // 1
console.log(+[]); // 0
console.log(+{}); //NaN
console.log(+"123"); // 123

对于 + 一元表达式求值,需要首先对操作数(V)取值(GetValue, 对取值结果进行 ToNumber 操作。

ToNumber:

  1. 如果 V 是 Number,返回 V
  2. 如果 V 是 SymbolBigInt 抛出错误 TypeError
  3. 如果 V undefined, 返回 NaN
  4. 如果 V 是 nullfalse 返回 +0
  5. 如果 V 是 true 返回 1 (案例 1)
  6. 如果 V 是 String 返回 StringToNumber(argument) StringToNumber 规定如果不能转成数字,返回 NaN 如果可以则返回 Number (案例 4)
  7. V 如果是一个 Object
  8. 将 V 转换为原始值 ToPrimitive 数组将会通过 toString 转换为 "", 再次执行第 6 步,最终数组被转换为 0 (案例 2),对象在经过 ToPrimitive 转换为 [Object object], 在执行 StringToNumber 由于不能转换为数字,最终返回 NaN (案例 3)
  9. 原始值不是对象,再次执行 ToNumber

修改 GPT/MBR 分区

添加新的磁盘,或在虚拟机中扩容磁盘,需要修改分区。

查看分区现状

1
2
3
4
5
6
lsblk
# 或者
fdisk -l /dev/sda

# 先使用 sudo apt install parted 安装 parted
parted /dev/sda print

可以看到磁盘容量变大,但是分区没有变化

1
2
3
4
5
6
7
8
9
10
11
12
Disk /dev/sda: 105 GiB, 112742891520 bytes, 220200960 sectors
Disk model: QEMU HARDDISK
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x94a3b99f

Device Boot Start End Sectors Size Id Type
/dev/sda1 * 2048 8484863 8482816 4G 83 Linux
/dev/sda2 8486910 10483711 1996802 975M 5 Extended
/dev/sda5 8486912 10483711 1996800 975M 82 Linux swap / Solaris

manjaro美化

添加 archlinux 源

mirrorlist-repo 官方仓库地址

1
2
3
4
# /etc/pacman.conf

[archlinuxcn]
Server = https://repo.archlinuxcn.org/$arch

设置源

  • 软件仓库中选择 Preferences, use mirrors 选择指定源

  • 通过命令行选择

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 手动选择镜像列表
    sudo pacman-mirrors -i -c China -m rank

    # 自动生成配置列表 -g 表示从活动池中选择镜像列表
    sudo pacman-mirrors -c China -g

    # -c United_States: 指定美国源
    # --fasttrack 5: 自动测速并取最快的 5 个
    sudo pacman-mirrors -c United_States
    sudo pacman-mirrors -f 5

更新系统

1
sudo pacman -Syyu

更新 Manjaro 的 keyring(密钥环)通常是为了解决在更新系统或安装软件时遇到的 GPGME error、invalid or corrupted package 或 signature is unknown trust 等报错。

1
sudo pacman -Sy archlinux-keyring manjaro-keyring

忽略某些安装包更新

1
2
# 修改 IgnorePkgs 列表
sudo nano /etc/pacman.conf

安装 yay 助手

1
sudo pacman -S yay

输入法

Fcitx5

1
sudo pacman -S fcitx5-im fcitx5-chinese-addons fcitx5-material-color

系统设置中添加拼音输入法

配置环境变量,让应用程序可以使用输入法

1
2
3
4
5
# /etc/environment

GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx

配置插件:

  • 点击 configure addons, 选择 classic user interface 勾选 use per screen DPI, 保证输入法随系统缩放。

设置联想:

  • 使用云拼音实现联想功能

    点击 configure addons, 选择 PinYin, 开启云拼音,并选择 Baidu

oh-my-zsh

oh-my-zsh

SSH

GNOME/Keyring

如果安装的是 KDE 桌面等桌面环境的版本,可能无法自动获取 SSH Key 的密码,导致每次使用都会提示输入密钥密码

安装 Cinnamon 它是一个独立的桌面环境,包含了 Keyring 所需要的工具包,首次认证会弹出一个 GUI 窗口填写密码,后续可以免密登录

1
yay cinnamon

开启 gcr 守护进程

1
systemctl --user enable --now gcr-ssh-agent.service

添加环境变量

1
export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/gcr/ssh"

虚拟化环境配置显卡直通

X11 环境中强制 Xorg 使用 NVIDIA GPU, 需要注意 PCI设备不要填错, 通过命令lspci | grep -i nvidia 查看

1
2
sudo rm /etc/X11/xorg.conf
sudo nvidia-xconfig --busid=PCI:1:0:0 --force-generate

设置环境变量

1
2
3
echo 'export __NV_PRIME_RENDER_OFFLOAD=1' >> ~/.zshrc
echo 'export __GLX_VENDOR_LIBRARY_NAME=nvidia' >> ~/.zshrc
source ~/.zshrc

980Ti 删除 video-nvidia 驱动并安装官方驱动

nvidia-prime 是为了处理双显卡切换(通常是笔记本)的工具,它依赖于 nvidia-utils,而 mhwd 在尝试移除旧驱动时无法自动处理这个三方依赖。

1
sudo pacman -R nvidia-prime

移除驱动

1
sudo mhwd -r pci video-nvidia

安装驱动

1
2
3
4
5
# 1. 确保系统是最新的且有头文件(非常重要!)
sudo pacman -Syu $(basename $(pacman -Qq linux | grep "^linux[0-9]*$"))-headers

# 2. 安装指定的驱动
sudo mhwd -a pci nonfree 0300
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信