16 |
17 |
18 |
19 | ## 功能实现
20 |
21 | 首先,插件是一个接收 `APlayer` 类型的参数的函数,它会在 `APlayer` 上注入一些新的方法。我们可以通过 TypeScript 的泛型来表示这个插件:
22 |
23 | ```typescript
24 | type Plugin = (player: APlayer) => void;
25 | export const APlayerFixedModePlugin: Plugin;
26 | export const APlayerHlsPlugin: Plugin;
27 | export const addMusicPlugin: Plugin<{
28 | list: {
29 | add: (audios: Audio[] | Audio) => void;
30 | }
31 | }>;
32 | ```
33 |
34 | 不难想到 `APlayer` 类型也应该是一个泛型:
35 |
36 | ```typescript
37 | export type APlayer = {
38 | init(options: APlayerOptions): APlayer;
39 | use(plugin: Plugin
): APlayer;
40 | play(): void;
41 | // ... other methods
42 | } & T
43 | ```
44 |
45 | 每次 `use` 操作都将插件提供的类型 `P` 与当前 `APlayer` 的泛型参数 `T` 进行合并。通过 `APlayer = {...} & T` 的方式,我们将 `APlayer` 自带方法的类型与插件提供方法的类型进行了合并。
46 |
47 | 这里功能已经实现了,但还遗留了一些问题:
48 |
49 | - 这里泛型的默认值为什么选择了 `unknown`?
50 | - `APlayer` 的泛型参数 `T` 与 `P` 的合并操作是什么意思?
51 |
52 | 后文继续讨论。
53 |
54 | ## TypeScript 中的特殊“空”类型
55 |
56 | ### 概念:AnyScript
57 |
58 | 相信大家都看过那张 JavaScript 的各种 falsy 值相互之间是否 `==` 相等的表格 meme,现在 TypeScript 出现了,“空”类型更多了(x
59 |
60 | 先明确一些重要的概念:
61 |
62 | - 子类型几乎可以看成是一种 `assignable to` 的关系,即 `A` 是 `B` 的子类型几乎等价任何需要 `B` 类型值的地方都可以使用 `A` 类型的值。
63 | - `any` 类型是特例,可以赋值给任何类型(除 `never`)或者被任何类型赋值。相当于 TypeScript 开的一个后门,后文不再考虑。
64 |
65 | 嘛,毕竟赋值/隐式类型转换就是我们最常见的操作。在不考虑 `any` 之后,子类型关系就形成了一个链条,如果 `A` 是 `B` 的子类型,`B` 是 `C` 的子类型,那么 `A` 也是 `C` 的子类型。这种关系叫做偏序关系。
66 |
67 | ### 类型即集合
68 |
69 | 我们可以进一步形式化这一偏序关系:
70 |
71 | - 考虑每个类型都对应一个由它的所有可取值组成的集合,那么 `A` 是 `B` 的子类型,就意味着 `A` 的集合是 `B` 的集合的子集。
72 | - 这等价于 `assignable to` 关系成立。
73 | - 类型的 Intersection 和 Union 操作,就是对应可取值集合的交和并操作。
74 |
75 | 听起来还是有点抽象,我们举几个例子:
76 |
77 | ```typescript
78 | // 第一种情况:B 是 A 的子类型
79 | type A = { a: number };
80 | type B = { a: number, b: string };
81 |
82 | // 第二种情况:A 是 B 的子类型
83 | type A = 'a';
84 | type B = 'a' | 'b';
85 | ```
86 |
87 | 好像看起来更抽象了:为什么同样是 `B` 的范围看起来比 `A` 更广,但是一种情况下 `B` 是 `A` 的子类型,另一种情况下 `A` 是 `B` 的子类型呢?
88 |
89 | 一方面可以用最基本的 `assignable to` 概念判断。第一种情况中,任何需要 `A` 类型的地方都可以用 `B` 类型的值来代替,因为 `A` 要求具备 `a: number` 属性,而 `B` 满足这一要求。第二种情况同理判断任何需要 `B` 类型的地方都可以用 `A` 类型的值来代替。
90 |
91 | 另一方面,我们也可以用集合的观点来看待问题。第二种情况此时就变得非常显然了,`type B = A | 'b'`,这是集合的并运算,所以 `A` 是 `B` 的子集(子类型)。问题是怎么理解第一种情况中 `B` 是 `A` 的子集(子类型)呢?我们重新表示第一种情况的代码:
92 |
93 | ```typescript
94 | type A = {
95 | a: number;
96 | b?: any;
97 | c?: any;
98 | [any_string_here]?: any
99 | ...
100 | }
101 |
102 | type B = {
103 | a: number;
104 | b: string;
105 | c?: any;
106 | [any_string_here]?: any
107 | ...
108 | }
109 | ```
110 |
111 | 注意 TypeScript/JavaScript 的结构体本身是一个对象,所以其实隐含了所有其他未被注明的属性都是可选的。这么一看,`B` 可取值集合显然是 `A` 的子集了,因为 `A` 没有对 `b` 属性做出要求:`A` 的可取值相当于 `[number, any, any, ...]`,而 `B` 的可取值相当于 `[number, string, any, ...]`。可以想象,不同结构类型的 Intersection 运算就是将所有注明的属性取交集,所得结果也会是任意原来类型的子类型。到这里,上文遗留的第二个问题已经解决了。
112 |
113 | ### unknown, never, void
114 |
115 | 理解了类型和集合的对应之后,我们终于可以解决遗留的第一个问题,开始讨论 TypeScript 中的 `unknown` 和 `never` 了。
116 |
117 | - 空集是所有集合的子集。对应 `never` 是所有类型的子类型。因此 `never` 又叫底类型。
118 | - 任何类型都是 `unknown` 的子类型,因此 `unknown` 又叫顶类型。
119 |
120 | `unknown` 类型相当于没有任何约束,任何值都是 `unknown` 类型。所以,在我们对插件一无所知的时候,可以使用 `APlayer` 类型。随着增加新的插件 `Plugin`,我们会有 `APlayer = APlayer`(`unknown & P = P` 是顶类型,或者说全集具有的性质)。
121 |
122 | 从集合的角度,我们还可以发现,空集的大小为 0,所以 `never` 没有实例。因此,它可以表示一个函数永远不会返回值(死循环或异常),或者一个变量永远不会被赋值。
123 |
124 | ```typescript
125 | let bar: never = (() => {
126 | throw new Error('Throw my hands in the air like I just dont care');
127 | })();
128 | ```
129 |
130 | 一个函数除了正常情况(有特定返回值)和永远不返回(`never`)之外,还可能我们并不关心它的返回值。通常这可以通过什么都不返回实现(其实是返回 `undefined`)。
131 |
132 | ```typescript
133 | type f = () => void;
134 | let a: f = () => {};
135 | let b: f = () => 1;
136 | ```
137 |
138 | 注意不关心(不使用)返回值不等于没有返回值。这就是 `void` 类型的特殊性,在一个标注为需要返回值 `void` 的函数的地方,我们使用返回值为任意值的函数都是可以的(如上面的 `b`)。
139 |
--------------------------------------------------------------------------------
/posts/digital-lab.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 数电计组实验 Vscode 配置指南
3 | date: 2022-03-19 21:18:29
4 | tags: [笔记]
5 | category: 笔记
6 | tocbot: true
7 | ---
8 |
9 | 本篇博客将带来优雅的 Vscode 编写数字电路实验 / 计算机组成原理实验的 Verilog 一键式配置方案,让你编写代码全程远离 Vivado(~~新建工程还是要见一面的~~)
10 |
11 | 主要还是介绍一些名词和工具,读者看上去了哪些可以自己挑
12 |
13 | 
14 |
15 |
16 |
17 | ## Vscode 插件
18 |
19 | 首先可能需要连接远程的 Vlab 服务器,这个时候需要使用 Vscode-remote 插件
20 |
21 | 
22 |
23 | 这个插件主要作用就是能在本地通过 SSH 连接操作 Vlab 虚拟机
24 |
25 | 具体配置过程不再赘述,配置完之后可以很方便的浏览远程文件
26 |
27 | 
28 |
29 | 这个时候已经能愉快的在 Vscode 上写 Verilog 了,但是仅仅能写肯定不够,我们还需要一个 linter(检查语法) 还有一个 formatter(代码格式化)
30 |
31 | 这里推荐一个插件:Digital IDE
32 |
33 | 
34 |
35 | 插件来自 [ysy-phoenix](https://github.com/ysy-phoenix) 的推荐,在此感谢
36 |
37 | 配置完之后应该已经默认有代码格式化和端口的各种提示信息了,上图的代码就是这个插件格式后的成果
38 |
39 | 但 linter 还需要额外配置,这里我们打开插件的设置:
40 |
41 | 
42 |
43 | 个人感觉 Verilator 更好用,当然 Vlab 虚拟机上默认是没有自带的,不过 Ubuntu 通过包管理器安装还是很方便的:
44 |
45 | ```bash
46 | apt update // 这一步为了确保软件包是最新的
47 | apt install verilator // 安装
48 | ```
49 |
50 | 两下就好了,之后写代码时一保存文件就可以自动帮你检查语法错误了:
51 |
52 | 
53 |
54 | (这里报错会显示在一行上是因为装了另一个插件 Error Lens,很美观,亲测好用)
55 |
56 | ## 干掉 Vivado 之仿真篇
57 |
58 | 仿真需要看波形,这个时候一个推荐是 `gtkwave`,Ubuntu 下同样通过包管理器安装即可
59 |
60 | 因为这个不能在没有图形界面的 Vlab 上运行,所以下文给出的仿真推荐都是配在本地环境的:
61 |
62 | ### Icarus Verilog
63 |
64 | 大名鼎鼎的一个仿真工具,简称 iverilog, 如果你用 Ubuntu 的包管理器装它还会自动帮你装上 gtkwave(~~捆绑消费~~)
65 |
66 | Digital IDE 自带了对于 Icarus Verilog 的快速仿真支持,见 [它的文档](https://zhuanlan.zhihu.com/p/365805011)
67 |
68 | ### Verilator
69 |
70 | 这里还是要安利 Verilator,因为用它做仿真可以 **不写 Verilog** ,Verilator 会生成高层次的 C++ 代码模拟模块的行为,然后你只需要用 C++ 编写 top 模块进行测试就行了
71 |
72 | 很大的好处在于 C++ 作为高级语言很明显比 Verilog 的 testbench 写起来灵活
73 |
74 | Ubuntu 包管理器装的版本比较旧,如果想要最新版可以自己下载它的 GitHub 仓库按照说明编译,不过这样就麻烦一点了
75 |
76 | 具体使用方式可以参考 [这篇文章](http://www.sunnychen.top/2019/07/25/%E8%B7%A8%E8%AF%AD%E8%A8%80%E7%9A%84Verilator%E4%BB%BF%E7%9C%9F%EF%BC%9A%E4%BD%BF%E7%94%A8%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1/) ,~~对着复制粘贴就行~~
77 |
78 | 如果你发现 Vscode 找不到头文件 `verilated.h` ,那就找到这个设置(直接在 Vscode 的设置里搜索即可),路径添加以下两个中的一个(根据你自己的情况而定,自己看哪个目录能进,如果是包管理器装的应该是下面那个路径)
79 |
80 | 
81 |
82 | 然后就可以愉悦的编写 C++ 代码仿真了,以下是一个对 ALU 模块 100% 覆盖率的测试示例:
83 |
84 | 
85 |
86 | 根据上面那篇链接里教程的步骤,即可判断仿真结果是否正确,并生成波形
87 |
88 | ## 干掉 Vivado 之比特流
89 |
90 | 写完代码我们还需要 Vivado 这个~~工具人~~帮我们生成比特流
91 |
92 | 但每次打开 VNC 操作 Vivado 显然太过笨重,这个时候我们可以利用 Vivado 自带的 TCL 命令行工具来构建项目
93 |
94 | 这里直接贴一个 Vlab 能用的脚本(如果在本地跑可能需要一些修改)[作者链接](https://github.com/WuTianming)
95 |
96 | ```bash
97 | #!/bin/bash
98 |
99 | echo "Initiating project build ..."
100 |
101 | ProjName=$(find . -type f -iname "*.xpr")
102 | if [ -z "$ProjName" ]; then
103 | echo "xpr file not found. Exiting"
104 | exit 1
105 | fi
106 | echo "Found project file ${ProjName}."
107 |
108 | echo "Creating build script ..."
109 | cat - > automation_genbitstream.tcl << EOF
110 | set_param general.maxThreads 2
111 | open_project ${ProjName}
112 |
113 | reset_run synth_1
114 | launch_run synth_1
115 | wait_on_run synth_1
116 | open_run synth_1
117 | # report_timing_summary
118 | launch_run -to_step write_bitstream impl_1
119 | wait_on_run impl_1
120 | # open_run impl_1
121 | # report_timing_summary
122 | # report_utilization > utilization.txt
123 |
124 | quit
125 | EOF
126 |
127 | cat - > wrapper.tcl << EOF
128 | if {[catch {source automation_genbitstream.tcl} errorstring]} {
129 | puts "Error - $errorstring"
130 | exit 1
131 | }
132 | quit
133 | EOF
134 |
135 | cat - > build_remote.sh << EOF
136 | #!/bin/bash
137 |
138 | # source /extra/vivado2016/Vivado/2016.3/settings64.sh
139 | source /opt/vlab/path.sh
140 |
141 | if ! time vivado2019 -mode tcl -source wrapper.tcl; then
142 | grep --color=always "ERROR" vivado.log
143 | fi
144 | rm -f *.log *.jou
145 | printf "\a"
146 | EOF
147 |
148 | echo "Running job ..."
149 | bash build_remote.sh
150 | echo "Build done."
151 |
152 | rm automation_genbitstream.tcl wrapper.tcl build_remote.sh
153 | ```
154 |
155 | 放在和 xpr 文件同一个目录(也就是项目目录下即可)
156 |
157 | 怎么运行脚本这里不再赘述
158 |
159 | 
160 |
161 | 可以很漂亮的在命令行里得到结果并看到各种 report
162 |
163 | 具体设置可以自己看脚本被注释掉的一些部分
164 |
165 | 至此我们成功构建了一套还算趁手的 Verilog 开发工具链
166 |
--------------------------------------------------------------------------------
/posts/github-actions-ci.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: GitHub Actions 持续集成体验
3 | date: 2022-08-23 19:10:20
4 | tags: [笔记]
5 | category: web
6 | ---
7 |
8 | 咕了快小半年,今天更新一期关于 GitHub Actions 在项目部署测试中的应用
9 |
10 | ## 介绍
11 |
12 | 官方介绍:
13 |
14 | > GitHub Actions 是一个持续集成和持续交付 (CI/CD) 平台,可用于自动执行构建、测试和部署管道。您可以创建工作流程来构建和测试存储库的每个拉取请求,或将合并的拉取请求部署到生产环境。
15 | GitHub Actions 不仅仅是 DevOps,还允许您在存储库中发生其他事件时运行工作流程。例如,您可以运行工作流程,以便在有人在您的存储库中创建新问题时自动添加相应的标签。
16 | GitHub 提供 Linux、Windows 和 macOS 虚拟机来运行工作流程,或者您可以在自己的数据中心或云基础架构中托管自己的自托管运行器。
17 |
18 |
19 |
20 | 概括一下其实就是 GitHub 给你送了台虚拟机的免费使用权,你可以利用它在项目有新 push, pull request 时或者定时执行某些构建,测试,部署的任务
21 |
22 | - 构建:如果项目需要构建出 release 版本,可以使用它自动发布
23 | - 测试:运行测试脚本,监测项目的可用性及正确性
24 | - 部署:例如与 GitHub Pages 配合使用,可以自动将静态网页部署到 GitHub Pages 上
25 |
26 | ## 构建与部署
27 |
28 | 首先介绍下 GitHub Actions 的 YAML 格式,需要包含:
29 |
30 | - 触发事件,决定什么时候运行虚拟机,可选 push/pull request/手动运行等
31 | - 运行流程,决定具体虚拟机内执行的程序
32 |
33 | 一个项目可能需要多个 actions,每个 actions 都是 `.github/workflows` 下的一个文件,触发事件和运行流程都彼此独立
34 |
35 | ```yaml
36 | name: build
37 |
38 | on:
39 | push:
40 | branches:
41 | - posts
42 |
43 | jobs:
44 | build:
45 | runs-on: ubuntu-latest
46 |
47 | steps:
48 | - uses: pnpm/action-setup@v2.2.2
49 | with:
50 | version: latest
51 |
52 | - uses: actions/checkout@v2
53 |
54 | - name: Set up Python 3.8
55 | uses: actions/setup-python@v2
56 | with:
57 | python-version: 3.8
58 |
59 | - name: Run build script
60 | run: |
61 | bash build.sh
62 |
63 | - name: Push
64 | uses: s0/git-publish-subdir-action@develop
65 | env:
66 | REPO: self
67 | BRANCH: main
68 | FOLDER: Q-Blog/dist
69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70 |
71 | ```
72 |
73 | 例如以上是一个 workflow 文件,该 actions 的功能是在仓库内的博客 md 文件更新后,自动构建生成静态网页并 push 到 main 分支(随后这一 push 会由 GitHub Pages 负责自动更新静态网页)
74 |
75 | - 触发事件:posts 分支的 push 请求
76 | - 运行流程:一个 actions 可以由几个 job 组成,这些 job 可以根据需求并行或者串行,这里只有一个 job
77 |
78 | 具体来说,一个 job 由多个 step 串行组成,以下具体分析
79 |
80 | ```yaml
81 | - uses: pnpm/action-setup@v2.2.2
82 | with:
83 | version: latest
84 | ```
85 |
86 | 通过 uses, 可以方便地使用别人编写的用于实现特定功能的 actions
87 |
88 | with 相当于传入需要的参数
89 |
90 | 别人的 actions 可以在 GitHub 提供的 [marketplace](https://github.com/marketplace?type=actions) 找到
91 |
92 | 例如这一 step 是配置 node 和 pnpm 环境
93 |
94 | 同理
95 |
96 | ```yaml
97 | - uses: actions/checkout@v2
98 |
99 | - name: Set up Python 3.8
100 | uses: actions/setup-python@v2
101 | with:
102 | python-version: 3.8
103 | ```
104 |
105 | 分别使得 actions 可以访问本仓库文件以及设置起 python 和 pip 的环境
106 |
107 | 之后的 steps 则是构建出静态文件并 push 到 main 分支
108 |
109 | ## 测试
110 |
111 | GitHub Actions 的另一大常见用途便是用于测试,我们经常能看见别人的开源项目会有一个  的标识,甚至还有测试覆盖率,这都可以通过 GitHub Actions 实现,例如上面的图标表示的就是某个 workflow 上次运行的测试是否通过
112 |
113 | 例如这个暑假摸了一个 QQ 机器人的插件 (js 写的),就尝试配合 JS 的 mocha 测试框架玩了下 GitHub Actions 用于 push 后自动测试
114 |
115 | 只需要将测试脚本作为一个 step 执行即可:
116 |
117 | ```yaml
118 | - name: run test script
119 | run: docker exec yunzai-bot sh -c "cd plugins/l-plugin/ && pnpm run test"
120 | ```
121 |
122 | mocha 框架可以指定测试样例(可以嵌套),并最终生成报告
123 |
124 | ```js
125 | describe('骰子', function () {
126 | describe('#r', function () {
127 | it('应该返回 114514 和 1919810 之间的一个随机数', async function () {
128 | const res = await command.run('r 114514 1919810')
129 | const num = Number(res.pop().split(':').pop())
130 | assert(Number.isInteger(num) && num >= 114514 && num <= 1919810)
131 | })
132 | })
133 | describe('#roll', function () {
134 | it('应该返回 a 或 b', async function () {
135 | const res = await command.run('roll a b')
136 | const choice = res.pop().pop().split(':').pop()
137 | assert(choice === 'a' || choice == 'b')
138 | })
139 | })
140 | })
141 | ```
142 |
143 | 通过 assert 断言来具体测试每个样例,通过全部样例就会这样显示
144 |
145 | ```plaintext
146 | 塔罗牌
147 | ✔ 应该返回一条牌面信息和对应图片 (232ms)
148 |
149 | 每日一题
150 | ✔ 应该返回一张图片和对应 url (3909ms)
151 |
152 | 求签
153 | ✔ 应该返回一条或两条签文消息 (72ms)
154 |
155 | 骰子
156 | #r
157 | ✔ 应该返回 114514 和 1919810 之间的一个随机数 (51ms)
158 | #roll
159 | ✔ 应该返回 a 或 b (47ms)
160 |
161 | 吃什么
162 | #今天吃什么
163 | ✔ 应该返回随机五个食物 (49ms)
164 | #咱今天吃什么
165 | ✔ 应该返回加入的特色菜 (105ms)
166 |
167 | Markdown
168 | ✔ 应该返回一张 Markdown 图片 (145ms)
169 |
170 | Tex
171 | ✔ 应该返回一张 Tex 图片 (967ms)
172 |
173 |
174 | 9 passing (6s)
175 | ```
176 |
177 | 非常方便(~~而且挺好看的~~
178 |
179 | 可以在 Actions 界面选择 workflow 后再选择获取对应的图标 URL,实时显示该 workflow 的通过状态,再贴到自己的 `README.md` 里就大功告成了
--------------------------------------------------------------------------------
/posts/bangumi_anime_list.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Anime List!
3 | date: 2024-02-23 22:43:15
4 | tags: [杂谈, anime]
5 | category: web
6 | ---
7 |
8 | 想做 Anime List 很久了,之前也考虑过 Notion 的方案,但是本人平常也不用 Notion,不想为了这个事情多用一个平台。最近发现自己使用的 [bangumi](https://bgm.tv) 有 API,于是就想到了用它来做 Anime List。
9 |
10 |
11 |
12 | 目前在博客导航栏选择动画即可进入 Anime List 页面。
13 |
14 | 
15 |
16 | 编码过程得到了 windicss 这一原子 CSS 框架和 Copilot 的大力帮助。
17 |
18 | ## Copilot
19 |
20 | 你可以直接往代码里粘贴 [bangumi 接口](https://bangumi.github.io/api/) 的返回数据示例,然后敲一个 `interface Anime`,后面就是 Copilot 发挥了。
21 |
22 | ```typescript
23 | interface Anime {
24 | updated_at: string
25 | comment: string
26 | tags: { name: string; count: number }[]
27 | subject: {
28 | date: string
29 | images: {
30 | small: string
31 | grid: string
32 | large: string
33 | medium: string
34 | common: string
35 | }
36 | name: string
37 | name_cn: string
38 | short_summary: string
39 | tags: { name: string; count: number }[]
40 | score: number
41 | type: number
42 | id: number
43 | eps: number
44 | volumes: number
45 | collection_total: number
46 | rank: number
47 | }
48 | subject_id: number
49 | vol_status: number
50 | ep_status: number
51 | subject_type: number
52 | type: number
53 | rate: number
54 | private: boolean
55 | }
56 | ```
57 |
58 | ## windicss
59 |
60 | ```html
61 |
67 |
68 |
73 |
74 |
75 | {{ anime.subject.name_cn || anime.subject.name }}
76 |
77 |
78 | {{ anime.subject.short_summary }}……
79 |
80 |
81 |
82 |
83 |
84 |
85 | {{ anime.comment }}
86 |
87 |
88 |
89 |
90 | {{ timeToDate(anime.updated_at) }}
91 |
92 |
93 |
94 |
95 | ```
96 |
97 | 写个样式非常方便,而且也可以 Copilot 生成类名(
98 |
99 | ## 分页
100 |
101 | bangumi 的这个 API 是分页的,出于性能考虑可以写一个滚动加载。
102 |
103 | ```typescript
104 | const animeList = ref([] as Anime[])
105 |
106 | const loading = ref(true)
107 | const pageSize = 12
108 | let page = 0
109 |
110 | const fetchAnimeList = async () => {
111 | if (!loading.value) return
112 | const offset = page * pageSize
113 | try {
114 | const res = await fetch(
115 | `https://api.bgm.tv/v0/users/undef_baka/collections?subject_type=2&type=2&limit=${pageSize}&offset=${offset}`,
116 | )
117 | if (!res.ok) {
118 | throw new Error('Network response was not ok')
119 | }
120 | const data = await res.json()
121 | const totalSize = data.total
122 | animeList.value = animeList.value.concat(data.data)
123 | if (offset + data.data.length >= totalSize) {
124 | loading.value = false
125 | }
126 | page++
127 | } catch (error) {
128 | loading.value = false
129 | }
130 | }
131 |
132 | // 节流
133 | let ticking = false
134 |
135 | async function updateOnScroll(event: Event) {
136 | if (ticking) return
137 | ticking = true
138 | const element = event.target as HTMLElement
139 | // 这里在处理兼容问题,正常情况元素滚动用元素,页面滚动用 document 就行
140 | const isBottom = document.documentElement.scrollTop ? document.documentElement.scrollHeight - document.documentElement.scrollTop <= document.documentElement.clientHeight + 100 :
141 | element.scrollHeight - element.scrollTop <= element.clientHeight + 100
142 | if (isBottom) {
143 | await fetchAnimeList()
144 | }
145 | ticking = false
146 | }
147 |
148 | onMounted(async () => {
149 | await fetchAnimeList()
150 | nextTick(() => {
151 | const element = document.querySelector('.n-layout-content .n-scrollbar-container')
152 | element?.addEventListener('scroll', updateOnScroll)
153 | document.addEventListener('scroll', updateOnScroll)
154 | })
155 | })
156 |
157 | onUnmounted(() => {
158 | const element = document.querySelector('.n-layout-content .n-scrollbar-container')
159 | element?.removeEventListener('scroll', updateOnScroll)
160 | document.removeEventListener('scroll', updateOnScroll)
161 | })
162 | ```
--------------------------------------------------------------------------------
/partial-evaluate/replacement.ts:
--------------------------------------------------------------------------------
1 | import type { NodePath } from '@babel/traverse'
2 | import type { MemberExpression, ObjectProperty, RestElement, VariableDeclarator } from '@babel/types'
3 | import babel from '@babel/core'
4 | import t from '@babel/types'
5 | import logger from './log'
6 |
7 | function isValidMemberExpression(node: NodePath, constObjectName: string) {
8 | if (constObjectName === 'this')
9 | return node.get('object').isThisExpression()
10 | return node.get('object').isIdentifier({ name: constObjectName })
11 | }
12 |
13 | function isValidDestructure(node: NodePath, constObjectName: string) {
14 | if (constObjectName === 'this')
15 | return node.get('init').isThisExpression()
16 | return node.get('init').isIdentifier({ name: constObjectName })
17 | }
18 |
19 | /**
20 | * Replace `props.prop` with given const
21 | * @param node Root node of function body needs to be optimized
22 | * @param constObjectName Name of the object that contains the constants
23 | * @param consts Map of constants
24 | */
25 | export function replaceMemberExpression(node: NodePath, constObjectName: string, consts: Record) {
26 | const memberExpressions: NodePath[] = []
27 | const potentialOptimizeProps: Set = new Set()
28 | node.traverse({
29 | MemberExpression(nodePath) {
30 | if (isValidMemberExpression(nodePath, constObjectName)) {
31 | memberExpressions.push(nodePath)
32 | potentialOptimizeProps.add(nodePath.toString().split('.')[1])
33 | }
34 | },
35 | })
36 | memberExpressions.forEach((nodePath) => {
37 | const propNode = nodePath.get('property').node
38 | if (propNode.type !== 'Identifier')
39 | return
40 | const propName = propNode.name
41 | if (propName in consts) {
42 | nodePath.replaceWithSourceString(`${consts[propName]}`)
43 | potentialOptimizeProps.delete(propName)
44 | }
45 | })
46 | logger.log(`Potential optimize member expression: ${Array.from(potentialOptimizeProps).join(', ')}`)
47 | }
48 |
49 | function replaceDestructureWithOneConst(
50 | root: NodePath,
51 | property: NodePath,
52 | consts: Record,
53 | potentialOptimizeProps: Set,
54 | ) {
55 | if (!property.isObjectProperty())
56 | return
57 | const key = property.get('key')
58 | if (!key.isIdentifier())
59 | return
60 | const propName = key.node.name
61 | if (propName in consts) {
62 | const value = babel.template.expression.ast(`${consts[propName]}`)
63 | const newVariableDeclarator = t.variableDeclarator(t.identifier(propName), value)
64 | root.replaceWith(newVariableDeclarator)
65 | }
66 | else {
67 | potentialOptimizeProps.add(propName)
68 | }
69 | }
70 |
71 | function replaceDestructureWithMultiDeclarations(
72 | root: NodePath,
73 | properties: NodePath[],
74 | consts: Record,
75 | potentialOptimizeProps: Set,
76 | ) {
77 | const deleteProperties: NodePath[] = []
78 | properties.forEach((property) => {
79 | if (property.isObjectProperty()) {
80 | const key = property.get('key')
81 | if (key.isIdentifier()) {
82 | const propName = key.node.name
83 | if (propName in consts)
84 | deleteProperties.push(property)
85 | else
86 | potentialOptimizeProps.add(propName)
87 | }
88 | }
89 | })
90 | const newVariableDeclarators: VariableDeclarator[] = deleteProperties.map((property) => {
91 | const key = property.get('key')
92 | if (!key.isIdentifier())
93 | throw new Error('Unexpected property key type')
94 | const propName = key.node.name
95 | const value = babel.template.expression.ast(`${consts[propName]}`)
96 | return t.variableDeclarator(t.identifier(propName), value)
97 | })
98 | deleteProperties.forEach(property => property.remove())
99 | root.insertAfter(newVariableDeclarators)
100 | }
101 |
102 | /**
103 | * replace `const { prop } = props` with given const
104 | * @param node Root node of function body needs to be optimized
105 | * @param constObjectName Name of the object that contains the constants
106 | * @param consts Map of constants
107 | */
108 | export function replaceDestructure(node: NodePath, constObjectName: string, consts: Record) {
109 | const potentialOptimizeProps: Set = new Set()
110 | node.traverse({
111 | VariableDeclarator(nodePath) {
112 | if (!isValidDestructure(nodePath, constObjectName))
113 | return
114 | const id = nodePath.get('id')
115 | if (!id.isObjectPattern())
116 | return
117 | const properties = id.get('properties')
118 | if (properties.length === 1)
119 | replaceDestructureWithOneConst(nodePath, properties[0], consts, potentialOptimizeProps)
120 | else
121 | replaceDestructureWithMultiDeclarations(nodePath, properties, consts, potentialOptimizeProps)
122 | },
123 | })
124 | logger.log(`Potential optimize destructure: ${Array.from(potentialOptimizeProps).join(', ')}`)
125 | }
126 |
--------------------------------------------------------------------------------
/posts/re-deriv.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 正则表达式求导
3 | date: 2025-01-24 13:08:18
4 | tags: [笔记, 算法]
5 | category: 笔记
6 | ---
7 |
8 | 是的,正则表达式也可以有求导!这一技巧在生成正则表达式(或拓展正则表达式)对应的 DFA/scanner 时非常有用。本文作为学习笔记,记录了正则表达式求导的定义和作用。
9 |
10 | 参考:
11 |
12 | - [Regular-expression derivatives reexamined](https://www.khoury.northeastern.edu/home/turon/re-deriv.pdf), Scott Owens, John Reppy, Aaron Turon, 2009
13 |
14 |
15 |
16 | ## 正则表达式
17 |
18 | 在编程上,正则表达式首先是一个 `(s: string) -> bool` 的函数,即判断字符串是否符合某种模式。我们把这件事形式化一下:
19 |
20 | - 字母表 $\Sigma$
21 | - 字母表上的字符 $a, b, c, \ldots$ 可以构成字符串,如 $abc, bca, \ldots$,字符串记作 $u, v, w, \ldots$
22 | - 特别地,空字符串记作 $\varepsilon$,长度为 0
23 | - $\Sigma$ 上所有长度有限的字符串构成的集合 $\Sigma^*$
24 | - 一个 *语言* 是一个 $\Sigma^*$ 的子集,即 $L \subseteq \Sigma^*$
25 |
26 | 那么正则表达式的函数签名转换成数学语言:
27 |
28 | - 给定一个正则表达式 $r$,它所有 *接受*(即返回 `true`,表示匹配)的字符串构成的语言记作 $L(r)$
29 | - $L(r) = \{ s \in \Sigma^* \mid r(s) = \text{true} \}$
30 |
31 | 上面的 $L(r)$ 定义对所有语言都适用,也就是从数学上形式化了 `(s: string) -> bool` 这件事。那么正则语言又特殊在哪里呢?
32 |
33 | 这就需要了解正则表达式的构造过程。因为一个正则表达式对应一个正则语言,对应的正则语言也被构造。本文采用一种拓展正则表达式(增加了布尔运算)的定义,读者在下文中只需要将正则表达式理解成一般的代数表达式即可。
34 |
35 | $$
36 | \begin{align*}
37 | L(\emptyset) &= \emptyset \\
38 | L(\varepsilon) &= \{ \varepsilon \} \\
39 | L(a) &= \{ a \} \\
40 | L(r_1 \cdot r_2) &= \{ u\cdot v \mid u \in L(r_1), v \in L(r_2)\} \\
41 | L(r^*) &= \{ u_1 \cdot u_2 \cdot \ldots \cdot u_n \mid n \ge 0, u_i \in L(r)\} \\
42 | L(r_1 + r_2) &= L(r_1) \cup L(r_2) \\
43 | L(r_1\ \&\ r_2) &= L(r_1) \cap L(r_2) \\
44 | L(\lnot r_1) &= \Sigma^* - L(r_1)
45 | \end{align*}
46 | $$
47 |
48 | 等号左边是正则表达式,右边是对应的正则语言。$r_1 \cdot r_2$ 是字符串拼接;等号左侧的其他运算依次称为闭包、并、交、补;等号右侧的运算就是集合运算。
49 |
50 | 可以发现,正则语言必然从 $\emptyset, \{\varepsilon\}, \{a\}$ 出发,通过(集合的)拼接、闭包、并、交、补等操作递归构造出来。
51 |
52 | ## 语言的导数
53 |
54 | 正则表达式求导的核心思想是:对于一个正则表达式 $r$,我们可以定义一个 *导数*,表示 $r$ 在字符(串)$u$ 上的变化。这个变化是指,如果 $r$ 匹配的字符串中,第一个字符(串)是 $u$,那么求导后匹配的字符串是去掉第一个字符(串)后的剩余部分。这里的求导实际就是状态转移,我们希望通过求导得到所有可能的状态,从而构造出 DFA。
55 |
56 | 换言之,求导操作相当于尝试“消耗”一个字符后,求剩余部分需要满足的正则表达式。
57 |
58 | 为了形式化这件事,我们先定义语言的导数:对于语言 $L$ 和字符串 $u \in \Sigma^*$:
59 |
60 | $$\partial_u L = \{ v \mid u\cdot v \in L \}$$
61 |
62 | 这个定义的意思是,$L$ 中所有以 $u$ 开头的字符串,去除 $u$ 后剩下的部分构成的集合。这个定义是可以对任意语言成立的。
63 |
64 | 举例说明,$S=\{apple,banana,apricot,cherry\}$,那么 $\partial_{ap} S = \{ple,ricot\}$。
65 |
66 | 我们让 $\partial_u$ 可以作用在正则表达式 $r$ 上,即:
67 |
68 | $$
69 | L(\partial_u r) = \partial_u L(r)
70 | $$
71 |
72 | (这里其实是个同构,但我们不深究这个问题,只要知道正则表达式的导数由语言的导数定义即可)
73 |
74 | 那么根据这个定义,我们来推导下正则表达式的求导吧。
75 |
76 | ## 正则表达式的导数
77 |
78 | 首先是对某个字符求导。这里空集表示不接受任何字符串的正则表达式(和接受空字符串的 $\varepsilon$ 不同),对应正则语言的 $\emptyset$。
79 |
80 | (所以对正则表达式 $r, s$,如果 $r=\emptyset$,那么 $r \cdot s = \emptyset$,其他运算同理)
81 |
82 | 另外引入记号
83 |
84 | $$
85 | \nu (r)= \begin{cases}
86 | \varepsilon & if\ \varepsilon \in L(r) \\
87 | \emptyset & otherwise.\end{cases}
88 | $$
89 |
90 | 例如:
91 |
92 | $$
93 | \begin{align*}
94 | \nu(a) &= \emptyset \\
95 | \nu(a^*) &= \varepsilon \\
96 | \end{align*}
97 | $$
98 |
99 | 不难发现 $\nu$ 可以简单递归定义。那么可以验证正则表达式的导数(对字符):
100 |
101 | $$
102 | \begin{align*}
103 | \partial_a \varepsilon &= \emptyset \\
104 | \partial_a a &= \varepsilon \\
105 | \partial_a b &= \emptyset, \text{ if } a \ne b \\
106 | \partial_a \emptyset &= \emptyset \\
107 | \partial_a (r_1 \cdot r_2) &= \partial_a r_1 \cdot r_2 + \nu(r_1) \cdot \partial_a r_2 \\
108 | \partial_a (r^*) &= \partial_a r \cdot r^* \\
109 | \partial_a (r_1 + r_2) &= \partial_a r_1 + \partial_a r_2 \\
110 | \partial_a (r_1\ \&\ r_2) &= \partial_a r_1\ \&\ \partial_a r_2\\
111 | \partial_a (\lnot r) &= \lnot (\partial_a r)
112 | \end{align*}
113 | $$
114 |
115 | 对字符串求导是递归定义的:
116 |
117 | $$
118 | \begin{align*}
119 | \partial_{\varepsilon} r &= r \\
120 | \partial_{ua} r &= \partial_a (\partial_u r)
121 | \end{align*}
122 | $$
123 |
124 | ## DFA 构造
125 |
126 | ### 朴素算法
127 |
128 | 考虑字母表 $\Sigma = \{a, b, c\}$,正则表达式 $r = a\cdot b + a\cdot c$。
129 |
130 | 
131 |
132 | 上图是完整的构造过程。概括来说,就是从原正则表达式对应的初始状态出发,对每个字符求导,得到下一个状态,直到没有新的状态产生。
133 |
134 | 这里状态(正则表达式)相等的定义是,两个正则表达式对应的语言相等。这个定义是合理的,因为我们的目标是构造 DFA,而 DFA 的状态是根据语言的不同而不同的。
135 |
136 | 单纯从状态转移的角度,我们可以看出这一定是最简 DFA,但是这个简单的算法也存在一些问题,足以让它不可用:
137 |
138 | - Unicode 有超过 100 万个字符(code point),对每个字符求导是不实际的;
139 | - 判定正则表达式相等复杂度甚至是 nonelementary 的(超过指数级);
140 | - 只能将一个正则表达式转换为 DFA
141 |
142 | ### 正则表达式弱相等
143 |
144 | 为了解决上述问题,我们引入 *正则表达式弱相等* 的概念。两个正则表达式 $r, s$ 弱相等,记作 $r \approx s$,我们希望判断弱相等的开销较低,从而把它用于状态检查和合并。
145 |
146 | 为了保证算法正确,我们仍然希望 $r \not= s \Rightarrow r \not\approx s$,即如果两个状态不相等,我们不会把它们合并。另一个方向则无所谓。
147 |
148 | 写成逆否命题就是:$r \approx s \Rightarrow r = s$。
149 |
150 | 一个直观的想法就是,我们定义一套规则,能快速判断 $r \approx s$。这里给出一套规则:
151 |
152 | 
153 |
154 | Brzozowski 证明了仅用 (*) 标记的规则就足以保证状态数有限。而根据实践,采用完整的规则集合,可以在实际应用中保证大多数时候都是最简 DFA。
155 |
156 | 一个实践中的做法是,对每个正则表达式,都保存它的 *标准形式*,即按照规则集合化简后的形式。这样,我们就可以用标准形式来判断弱相等。
157 |
158 | 这可以在构造函数中实现,示例:`mkNegate(r)` 会检查 $r$ 是否为 $¬s$,若是则返回 $s$(应用双重否定律 $¬¬s ≈ s$)。
159 |
160 | 为了使用交换律和结合律,则可以规定词法排序,例如对于 $r + s$ 还是 $s + r$,规定使用字典序。
161 |
162 | 其他有效的加速策略包括将正则表达式作为 key 映射到 DFA 状态等。
163 |
164 | ### 字符集
165 |
166 | 传统的 DFA 构造和基于求导的 DFA 构造都会遇到字母表太大,完整迭代效率太低的问题:一般情况状态数都远小于字母表大小(Unicode 字母表大小大于 100 万),所以我们希望能够对字母表进行压缩。这可以通过引入 *字符集合* 来实现。
167 |
168 | 简单来说就是基本字符不再是 $a, b, c$,而是某个集合。之后一些运算会相应地变成集合运算。
169 |
170 | ### 正则向量
171 |
172 | 原文是说,为了让 scanner generator 能并行处理多个正则表达式,我们可以引入 *正则向量* 的概念。正则向量是一个正则表达式的集合,我们可以对正则向量进行求导,得到一个新的正则向量。
173 |
174 | $$
175 | \partial_u (r_1, r_2, \ldots, r_n) = (\partial_u r_1, \partial_u r_2, \ldots, \partial_u r_n)
176 | $$
177 |
178 | 接受状态的定义是,$r$ 中的某个正则表达式接受空字符串;反之,拒绝状态是所有正则表达式都拒绝。
179 |
180 | 但我感觉这里就是处理几个正则表达式并起来,好像实践中很少有并行这么干的情况。
181 |
182 | ## 总结
183 |
184 | 挺好玩。
185 |
--------------------------------------------------------------------------------
/posts/sudoko-astar.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 数独的 A* 算法及其实现
3 | date: 2021-06-05 22:19:04
4 | tags: [算法, C/C++]
5 | category: 算法
6 | mathjax: true
7 | tocbot: true
8 | ---
9 |
10 | 接上一篇博客:[数独的模拟逻辑解法的实现](http://home.ustc.edu.cn/~liuly0322/blog/2021/06/03/sudoko-iddfs/),本篇博客将介绍数独的 A\* 算法求解。
11 |
12 | ## A\* 算法
13 |
14 | ### 原理分析
15 |
16 | A\* 算法是游戏中寻找路径的一种常见解法,它能在保证找到最短路径的同时,以一种较为节省计算资源开销的方式达到这一目的。关于理论分析,可以见斯坦福计算机系写的一篇 [算法介绍](https://blog.csdn.net/denghecsdn/article/details/78778769),或者见它的 [中文译文](https://dev.gameres.com/Program/Abstract/Arithmetic/AmitAStar.mht),这个网页似乎有点兼容性问题,所以你也可以查看 [csdn 的转载](https://blog.csdn.net/b2b160/article/details/4057781)。
17 |
18 |
19 |
20 | ~~大概只有在这个时候我才能感觉到中文互联网 csdn 无限套娃的一点好处~~
21 |
22 | 概括一点来说,A\* 算法结合了 **Dijkstra 算法** 和 **最佳优先搜索算法** 的优点。二者都从普通的广度优先搜索演化而来,其中前者是按照离起始点的有权路径距离来进行搜索的,能确保找到最短路径,而后者采取了贪心策略,估计了当前点和目标点的距离,一般能较快的找到终点。这种贪心策略被称为 **启发式方法**。
23 |
24 | 可以认为,Dijkstra 算法按照点的 $g(n)$ 值作为依据进行搜索,$g(n)$ 是距离原点距离(可以带权)。
25 |
26 | 而最佳优先搜索算法按照点的 $h(n)$ 值作为搜索依据,$h(n)$ 是从结点到目标点的距离(只能估计)。
27 |
28 | 而 A\* 算法就是对这二者进行综合,它进行搜索的依据是点的 $f(n)=g(n)+h(n)$ 值。这样一来,可以在保证找到最短路径的同时尽可能减少计算开销。
29 |
30 | 以上,在进行寻路算法时,我们使用了 **距离**一词,由于不同结点之间的“距离”可能是不同的,也就是这些点可以被抽象成有权图,因此,有时,也会使用用 **代价** 一词来表示有权的距离。此外,对于简单的二维平面迷宫问题,选取 $f(n)=g(n)+h(n)$ 是既合理又比较快速的,但是更一般情况,只需要 $f(n)$ 与 $g(n)$ 和 $h(n)$ 都具有正相关性即可,根据 $g(n)$ 和 $h(n)$ 的权重的大小,A\* 算法会表现出结果更精准或运算更快,也就是更向 Dijkstra 算法或最佳优先搜索算法退化的特性。
31 |
32 | ### 具体实现
33 |
34 | 这里引用 Red Blob Games[这篇文章](https://www.redblobgames.com/pathfinding/a-star/introduction.html) 里的 Python 代码(~~为什么,因为他写的实在是太好了~~)。
35 |
36 | 顺便一提,作者的博客有很多很有意思的可视化内容,有兴趣可以自己去玩玩。
37 |
38 | ```python
39 | frontier = PriorityQueue()
40 | frontier.put(start, 0)
41 | came_from = dict()
42 | cost_so_far = dict()
43 | came_from[start] = None
44 | cost_so_far[start] = 0
45 |
46 | while not frontier.empty():
47 | current = frontier.get()
48 |
49 | if current == goal:
50 | break
51 |
52 | for next in graph.neighbors(current):
53 | new_cost = cost_so_far[current] + graph.cost(current, next)
54 | if next not in cost_so_far or new_cost n2.cost;
102 | }
103 | //构造函数
104 | SudokuNode();
105 | SudokuNode(char src[][9]);
106 | //运算函数
107 | static void mark_cell(int cell, bool* mark, char* num); //标记 cell 可放数字
108 | int fill(int cell, int num_fill); //填数,返回 0 代表无解
109 | void cal_cost(int increase); //计算 cost_to_end 和 cost 总
110 | void get_current_num(char to_num[][9]); //将当前结点信息输出
111 | };
112 | ```
113 |
114 | 部分变量意义参见 [上一篇博客](http://home.ustc.edu.cn/~liuly0322/blog/2021/06/03/sudoko-iddfs/)。具体函数实现此处不表。
115 |
116 | A\* 搜索函数如下所示:
117 |
118 | ```cpp
119 | int Sudoku::search_astar() {
120 | SudokuNode current(sudoku_num), next;
121 | std::priority_queue frontier;
122 | frontier.push(current);
123 | while (!frontier.empty()) {
124 | current = frontier.top();
125 | frontier.pop();
126 | if (current.num_now == 81) {
127 | current.get_current_num(sudoku_solve);
128 | return 1;
129 | }
130 | // 接下来需要遍历所有可能节点。由于相互的连通性,只要从最少可能的找即可
131 | int min = 127, min_index;
132 | for (int i = 0; i < 81; i++) {
133 | if (current.num_can_put[i] < min) { // 记录最小值
134 | min = current.num_can_put[i];
135 | min_index = i;
136 | }
137 | }
138 | // 对这个格子生成所有可能的新节点,并加入优先队列
139 | for (int j = 1; j <= 9; j++) {
140 | if (!current.mark[min_index][j]) {
141 | next = current;
142 | if (next.fill(min_index, j)) {
143 | next.cal_cost(min);
144 | frontier.push(next);
145 | }
146 | }
147 | }
148 | }
149 | return 0;
150 | }
151 | ```
152 |
--------------------------------------------------------------------------------
/posts/pwa.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: PWA 补完计划
3 | date: 2024-02-26 19:59:23
4 | tags: [vite, pwa]
5 | category: web
6 | ---
7 |
8 | 这篇文章记录下对博客进行的 PWA(Progressive-Web-App)改造!因为博客是基于 Vite 的,所以理论上来说安装配置一下 [VitePWA](https://github.com/vite-pwa/vite-plugin-pwa) 就 ok 了。
9 |
10 | ## PWA
11 |
12 | PWA 这个词听上去可能很让人陌生,但在国内我们早就熟悉一个很类似的概念了——小程序。
13 |
14 |
15 |
16 | > PWA 是 Progressive Web App 的缩写,是一种 Web App 的新模式,可以让网站具备类似原生 App 的体验。
17 |
18 | 好吧,还是很抽象,那么来看看 PWA 好处都有啥?最重要的一点,PWA 可以让网页自行管理某个作用域(例如 `/`)的所有请求,例如第一次请求后在本地储存一份缓存下来的版本,之后每次访问就不需要再联网了:
19 |
20 | - 允许用户在离线状态下使用,甚至可以作为 app 被安装到多端桌面
21 | - 允许预取资源,只需要预先提供一份网站的资源列表
22 |
23 | 其中预取资源对我们的博客网站来说是非常有用的,可以大大加载访问速度。当然,PWA 还有很多其他特性,比如推送通知等,但这些对于一个静态博客网站来说就不是那么重要了。
24 |
25 | 那么 PWA 有没有什么坏处呢?其实也有,相信大家都遇到过微信小程序提示更新后才能打开的情况,这是因为小程序的缓存机制导致的。PWA 也有类似的问题,如果我们的博客(程序)更新了,用户需要手动/自动刷新才能看到最新内容。本篇文章会具体解释这一更新机制。
26 |
27 | ## VitePWA
28 |
29 | ```shell
30 | pnpm add -D vite-plugin-pwa
31 | ```
32 |
33 | 配置过程参考了 VitePWA 的 [PWA Minimal Requirements](https://vite-pwa-org.netlify.app/guide/pwa-minimal-requirements.html)、[Automatic reload](https://vite-pwa-org.netlify.app/guide/auto-update.html) 和 [Static assets handling](https://vite-pwa-org.netlify.app/guide/static-assets.html)。此外,[Periodic Service Worker Updates](https://vite-pwa-org.netlify.app/guide/periodic-sw-updates.html) 解释了如何以指定间隔检查更新,[Unregister Service Worker](https://vite-pwa-org.netlify.app/guide/unregister-service-worker.html) 解释了如何在启用后禁用 PWA 功能,也值得看看。
34 |
35 | `index.html` 中需要补充一些元信息:
36 |
37 | ```html
38 |
39 | llyのblog
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | ```
50 |
51 | `vite.config.ts` 中配置:
52 |
53 | ```typescript
54 | import { defineConfig } from 'vite'
55 | import { VitePWA } from 'vite-plugin-pwa'
56 |
57 | export default defineConfig({
58 | plugins: [VitePWA({
59 | // 配置自动更新
60 | registerType: 'autoUpdate',
61 | workbox: {
62 | // 配置缓存列表
63 | globPatterns: ['**/*.{js,css,ico,svg}', 'index.html'],
64 | // https://github.com/vite-pwa/vite-plugin-pwa/issues/120
65 | navigateFallback: null,
66 | },
67 | includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
68 | manifest: {
69 | name: 'llyのblog',
70 | short_name: 'llyのblog',
71 | description: '我的个人博客,写点想写的',
72 | lang: 'zh-CN',
73 | theme_color: '#ffffff',
74 | icons: [
75 | {
76 | src: 'pwa-192x192.png',
77 | sizes: '192x192',
78 | type: 'image/png',
79 | },
80 | {
81 | src: 'pwa-512x512.png',
82 | sizes: '512x512',
83 | type: 'image/png',
84 | },
85 | ],
86 | },
87 | })],
88 | })
89 | ```
90 |
91 | - `registerType: 'autoUpdate'` 配置了 PWA 自动更新;
92 | - `workbox.globPatterns` 配置了所有需要缓存(预取)的文件;
93 | - `includeAssets` 和 `manifest` 是一些元信息和图标相关的配置。
94 |
95 | 图标可以在 [Favicon InBrowser.App](https://favicon.inbrowser.app/tools/favicon-generator) 生成。
96 |
97 | 在 `main.ts` 里需要引入 `registerSW` 以实现自动更新:
98 |
99 | ```typescript
100 | import { registerSW } from 'virtual:pwa-register'
101 |
102 | registerSW({ immediate: true })
103 | ```
104 |
105 | 如果不引入这个虚拟模块,每次打开页面后也会检查 `sw.js` 的更新,但是后台静默安装完更新(包括下载新资源并删除旧资源)后并不会与页面产生交互(这里就是刷新页面)。旧 JavaScript 资源的删除会导致我们无法在 SPA 应用中导航去新的页面(因为新的页面的动态路由区块的 JavaScript 资源被删除了),所以**一般情况**需要引入它来实现更新后刷新页面(最后会介绍一种不需要自动刷新页面的更新策略)。
106 |
107 | 此时可以编译出来测测了:
108 |
109 | ```shell
110 | pnpm build
111 | pnpm preview
112 | ```
113 |
114 | 可以使用 [Lighthouse](https://github.com/GoogleChrome/lighthouse) 的 PWA 测试,也可以关闭 pnpm 的 preview 后测试离线访问。DevTools 的 Network 和 Application 面板对调试也会很有帮助。
115 |
116 | ## PWA 的更新
117 |
118 | 上文提到:
119 |
120 | > 如果我们的博客(程序)更新了,用户需要手动/自动刷新才能看到最新内容。
121 |
122 | 现在可以整理一下更新逻辑了。首先,博客(程序)是指 `sw.js` 和在 `sw.js` 缓存列表(就是 `vite.config.ts` 中配置的 `workbox.globPatterns`)中的文件(而不是所有的静态文件!注意未被缓存的静态文件的更新不必引起 PWA 的更新)。缓存列表中的文件的更新会引起 `sw.js` 的更新,因为 `sw.js` 中保存了所有缓存列表的文件的 hash 值。
123 |
124 | 随后,随着 `index.html` 的打开和 JavaScript 的加载,客户端会去请求 `sw.js`,当发现有更新后会后台静默安装好更新并刷新页面(这里是指上面提到的 `registerSW({ immediate: true })` 配置)。
125 |
126 | 但是,对于我们的博客网页来说,这样的更新存在下列问题:
127 |
128 | - 打开网页可能看到的是旧版内容,比如新更新的博客没刷新出来;
129 | - 强制刷新的体验并不好,包括额外的加载时间和元素滚动位置不被记录等。
130 |
131 | 一个更新策略是应用框架与数据分离。可以把博客想象成一个论坛 app,UI 和动态的帖子数据(这里就是我们的每一篇博客内容)是分离的。只有当 UI 更新时才需要强制刷新,而帖子数据可以做成动态的数据请求,比如请求一个 JSON 文件。帖子的更新通过 JSON 文件的更新来实现,只有 UI 的更新会更改 `sw.js`。
132 |
133 | 为此,我们需要指定这个 JSON 文件不被缓存。也就是不要把它包含在 `workbox.globPatterns` 中。这可以通过检查最后生成的 `sw.js` 文件来确认。
134 |
135 | 上面的策略可以确保看到的是最新博客内容,并且一定程度上缓解了强制刷新的问题。那么,有没有更到位的解决方案呢?确实是有的,但需要牺牲 PWA 的离线能力。
136 |
137 | 道理很简单,我们所加载的所有 JavaScript 文件都是入口的 `index.html` 指定的,所以只要不把 `index.html` 缓存起来,就可以保证每次打开页面都是最新的。分类讨论:
138 |
139 | - 某次更新后第一次打开页面:打包的 JavaScript 和 CSS 的文件名哈希会改变,所以 `index.html` 会去请求未被缓存的新文件,这样请求就通过网络发生。与此同时,后台安装新的 service worker,缓存新版本的资源,并删除旧版本的资源。因为我们现在就处在新版本当中了,所以不再需要刷新页面,旧版本的资源也可以安全删除。
140 | - 某次更新后第二次打开页面:`index.html` 请求的资源都没有更新,可以直接从缓存中加载。
141 |
142 | 同理,对于一些 SSR/SSG 场景,也不需要 HTML 文件被缓存,可以参考这个 [issue](https://github.com/vite-pwa/vite-plugin-pwa/issues/120) 的讨论。
143 |
144 | 需要注意 workbox 的配置:
145 |
146 | ```javascript
147 | workbox: {
148 | // 资源文件或不需要保证实时性的文件可以放在缓存列表中
149 | // 不需要包含 HTML 文件
150 | globPatterns: ['**/*.{js,css,ico,svg}', 'friends.json'],
151 | // https://github.com/vite-pwa/vite-plugin-pwa/issues/120
152 | navigateFallback: null,
153 | },
154 | ```
155 |
156 | 再移除 `registerSW({ immediate: true })` 就愉快的收工了。
157 |
--------------------------------------------------------------------------------
/posts/set-union.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 集合间并集个数计算
3 | date: 2022-09-09 16:00:49
4 | tags: [C/C++, 算法]
5 | category: 算法
6 | ---
7 |
8 | 来源于西瓜书习题 1.2, 计算若 n 个集合中最多取 k 个集合做并集,则有多少种可能的并集结果
9 |
10 | 题目内容:
11 |
12 | 与使用单个合取式来进行假设表示相比,使用“析合范式”将使得假设空间具有更强的表达能力
13 |
14 |
15 |
16 | 例如:
17 |
18 | 好瓜 $\iff$ ((色泽=\*)$∧$(根蒂=蜷缩)$∧$(敲声=\*))$∨$((色泽=乌黑)$∧$(根蒂=\*)$∧$(敲声=沉闷))
19 |
20 | 会把“(色泽=青绿)∧(根蒂=蜷缩)∧(敲声=清脆)”以及“(色泽=青绿)∧(根蒂=蜷缩)∧(敲声=清脆)”都分类为“好瓜”。若使用最多包含 k 个合取式的析合范式来表达下表西瓜分类问题的假设空间,试估算共有多少中可能的假设
21 |
22 | 给定的数据:西瓜的色泽有 2 种,根蒂和敲声都有 3 种
23 |
24 | ## 分析
25 |
26 | 显然总并集数的上界:$2^{2*3*3}=262144$,我们只需要枚举 $k$ 使得最后总并集数达到这一上限即可
27 |
28 | 首先明确,当 $k = 1$ 时,合法的单个合取式一共有多少种呢?
29 |
30 | 注意到色泽可以是两种颜色或者通配符,根蒂(敲声)可以是三种根蒂(敲声)或者通配符,因此总数是 $3 * 4 * 4 + 1 = 49$(空集也可以纳入考虑范围)
31 |
32 | 那么,当 $k=n$ 时,我们理论上只需要遍历 $\sum_{i=1}^nC_{48}^{i}$ 种集合的组合,随后去重即可,但这个数字是非常庞大的:$n=9$ 时,仅
33 |
34 | $$C_{48}^{9}=1677106640 = 1.68 \times 10^9$$
35 |
36 | [知乎上有一篇文章](https://zhuanlan.zhihu.com/p/355235881),作者穷举了所有可能,`用时:8899.29s`,这显然不是一个能够轻易接受的时间
37 |
38 | 因此,我们必须考虑优化
39 |
40 | ## 状态压缩
41 |
42 | 实际上,每一种可能,如(色泽=青绿)$∧$(根蒂=蜷缩)$∧$(敲声=清脆)可以看成一个点,不妨对点进行标号并状态压缩为 0(x) 00(y) 00(z) 形式
43 | - x, y, z 分别代表色泽(取值 0, 1),根蒂(取值 0, 1, 2),敲声(取值 0, 1, 2)
44 | - 所有点的集合就是 $\{0, 1, ..., 26(16+8+2)\}$
45 | - 每一个合取式对应一个点集
46 |
47 | 同时,对于合取式对应的点集又可以状态压缩:注意到点集中最大编号也只有 26,因此一个点集可以通过移位合并成一个整数,我们最后只要统计有多少个不同的整数(并集)就可以了
48 |
49 | ## 剪枝
50 |
51 | 有些合取式(包含通配符)包含了另外一些合取式,因此如果选取了这种合取式,它的子合取式就没必要再选取了
52 |
53 | 这可以通过状态压缩后的位运算来实现,如果合取式 $a$ 包含了合取式 $b$,有 `a | b = a`,因为 b 不会贡献新的假设
54 |
55 | ## Python 代码
56 |
57 | Python 标准库就对多处理(并行计算)有了比较好的支持,因此这里采用并行计算加速
58 |
59 | ```python
60 | from itertools import product, repeat
61 | from functools import reduce
62 | from multiprocessing import Pool
63 |
64 | def task(sets: list[int], n: int):
65 | """
66 | 从给定的可选点集列表 sets 中选出 n 个进行取并,返回值是一个集合,包含所有可能得到的点集
67 | 这里的点集都是指点集状态压缩后对应的整数
68 | """
69 | if len(sets) == n:
70 | return (reduce(int.__or__, sets),)
71 |
72 | chosen = sets[-1]
73 | if (n := (n - 1)) == 0:
74 | return (chosen,)
75 |
76 | sets = [s for s in sets if s | chosen != chosen]
77 | res = set()
78 | for _ in range(len(sets) - n + 1):
79 | res = res.union(ans | chosen for ans in task(sets, n))
80 | sets.pop()
81 | return res
82 |
83 | def main(k: int, sets: list[int]):
84 | """
85 | 对于给定的 k,拆分任务,多线程执行
86 | """
87 | all_ans = set()
88 | # 遍历所有可能的 n <= k,让 n 个点集组合
89 | for n in range(1, k + 1):
90 | p = Pool(15)
91 | paras = zip((sets[:i] for i in range(len(sets), n - 1, -1)), repeat(n))
92 | values = p.starmap(task, paras)
93 | p.close(); p.join()
94 |
95 | all_ans = set.union(all_ans, *values)
96 | print(f'k = {n} finished... Ans is {len(all_ans)}')
97 |
98 | if __name__ == '__main__':
99 | # 初始化,先建立所有合取式的列表
100 | # 如开头所述,每个合取式被位压缩成整数
101 | state = lambda x, y, z: (x << 4) + (y << 2) + z
102 | sets = []
103 | for x, y, z in product(range(3), range(4), range(4)):
104 | if x == 2:
105 | sets.append(sets[state(0, y, z)] | sets[state(1, y, z)])
106 | elif y == 3:
107 | sets.append(sets[state(x, 0, z)] | sets[state(x, 1, z)] | sets[state(x, 2, z)])
108 | elif z == 3:
109 | sets.append(sets[state(x, y, 0)] | sets[state(x, y, 1)] | sets[state(x, y, 2)])
110 | else:
111 | sets.append(1 << state(x, y, z))
112 | # 添加空集
113 | sets = [0] + sets
114 |
115 | # 接受输入
116 | num = int(input())
117 | main(num, sets)
118 | ```
119 |
120 | 10 秒左右就可以输出结果,~~比知乎上的答案快了 900 倍~~
121 |
122 | ## C++ 代码
123 |
124 | C++ 代码虽然没有并行,但本身编译型语言的性能就比较好,在我的设备上大约比上述 15 线程并行的 Python 代码快了一倍
125 |
126 | ```cpp
127 | #include
128 | #include
129 | #include
130 |
131 | using Set = std::unordered_set;
132 | using Vec = std::vector;
133 |
134 | Set task(Vec& sets, int n) {
135 | Set ans;
136 |
137 | if (sets.size() == n) {
138 | int or_acc = 0;
139 | for (auto const& set : sets) {
140 | or_acc |= set;
141 | }
142 | return ans.emplace(or_acc), ans;
143 | }
144 |
145 | int chosen = sets.back();
146 | if (!--n)
147 | return ans.emplace(chosen), ans;
148 |
149 | // shadowing sets, _sets is a new vec with only useful point sets
150 | Vec _sets;
151 | for (auto const& set : sets) {
152 | if ((set | chosen) != chosen) {
153 | _sets.push_back(set);
154 | }
155 | }
156 |
157 | Set rets;
158 | for (int i = _sets.size(); i >= n; i--) {
159 | rets = task(_sets, n);
160 | for (auto const& ret : rets) {
161 | ans.emplace(ret | chosen);
162 | }
163 | _sets.pop_back();
164 | }
165 |
166 | return ans;
167 | }
168 |
169 | int main() {
170 | Vec sets;
171 | Set ans;
172 | auto state = [](int x, int y, int z) { return (x << 4) + (y << 2) + z; };
173 | for (int x = 0; x < 3; x++) {
174 | for (int y = 0; y < 4; y++) {
175 | for (int z = 0; z < 4; z++) {
176 | if (x == 2) {
177 | sets.push_back(sets[state(0, y, z)] | sets[state(1, y, z)]);
178 | } else if (y == 3) {
179 | sets.push_back(sets[state(x, 0, z)] | sets[state(x, 1, z)] |
180 | sets[state(x, 2, z)]);
181 | } else if (z == 3) {
182 | sets.push_back(sets[state(x, y, 0)] | sets[state(x, y, 1)] |
183 | sets[state(x, y, 2)]);
184 | } else {
185 | sets.push_back(1 << state(x, y, z));
186 | }
187 | }
188 | }
189 | }
190 | sets.push_back(0);
191 |
192 | int k;
193 | std::cin >> k;
194 |
195 | Set rets;
196 | for (int n = 0; n <= k; n++) {
197 | rets = task(sets, n + 1);
198 | for (auto& ret : rets) {
199 | ans.insert(ret);
200 | }
201 | if (n) {
202 | std::cout << "length: " << n << " nums: " << ans.size()
203 | << std::endl;
204 | }
205 | }
206 | }
207 | ```
--------------------------------------------------------------------------------
/posts/specialization.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 部分求值与程序特化
3 | date: 2024-05-18 00:25:40
4 | tags: [部分求值, 编译]
5 | category: web
6 | ---
7 |
8 | 整理自前段时间做的一个技术分享。还是承接两个月前我写的打包体积优化的文章,最后留了一个小坑:
9 |
10 | > ESM 时期这类项目应该是有机会有后续进展的,因为对 ESM 模块而言,静态的导入导出声明使得可以更简单的获取到模块的精确调用模型。值得期待。
11 |
12 | 其实这里说的不太准确,ESM 的好处主要是限制了顶层变量的作用域,再配合模块导入导出声明才能达到追踪变量使用的效果。总之后面自己尝试填了一点这方面的坑,给 Rollup 提的 [PR](https://github.com/rollup/rollup/pull/5443) 也是在干这件事,这个链接里有 Rollup 的 maintainer([@lukastaegert](https://github.com/lukastaegert))整理的这个 PR 的作用和合并过程(其实还挺折磨的,因为一开始没太看懂 Rollup 的算法思想)。
13 |
14 | 所以本篇文章补一下这个 PR 实现背后的部分理论基础,主要是部分求值,和在 JavaScript 打包体积优化的应用。关于 Tree-Shaking 算法细节,和算法中比较重要的,变量的状态的格(代数结构里的格)表示,可能会另开一篇(写在毕设里了,不过未必会整理到博客上),也可能咕咕咕(
15 |
16 |
17 |
18 | > POPL '86 [“Compilers and staging transformations”](https://dl.acm.org/doi/abs/10.1145/512644.512652):
19 | >
20 | > 计算通常可以分为多个阶段,这些阶段通过执行频率或数据可用性来区分。预计算 和 循环不变代码外提(Frequency Reduction) 涉及在程序的不同阶段完成计算,以便尽早完成计算(因此后续步骤需要更少的时间)并且尽可能不要重复计算(以减少总体时间)。
21 |
22 | TL;DR: 编译期和运行时只是程序执行的不同的阶段,编译的过程也是一个**部分执行**程序的过程。熟知 Linux 上编译安装某个程序的常见步骤:
23 |
24 | ```shell
25 | ./configure
26 | make
27 | make install
28 | ```
29 |
30 | `./configure` 命令用于编译前配置一些选项,例如是否包含某个功能。考虑一段简单的矩阵乘法例子:
31 |
32 | ```c
33 | #include
34 |
35 | #define ROWS 3
36 | #define COLS 3
37 |
38 | void matrix_multiply(int A[ROWS][COLS], int B[COLS][ROWS], int result[ROWS][ROWS]) {
39 | for (int i = 0; i < ROWS; i++) {
40 | for (int j = 0; j < ROWS; j++) {
41 | result[i][j] = 0;
42 | for (int k = 0; k < COLS; k++) {
43 | result[i][j] += A[i][k] * B[k][j];
44 | }
45 | }
46 | }
47 | }
48 | ```
49 |
50 | 如果 `./configure` 配置了 `ROWS` 和 `COLS` 为常量,那么 `make` 时编译器就可以进行更针对的优化,例如当 `ROWS` 和 `COLS` 较小时循环展开。在这个例子中,`ROWS` 和 `COLS` 不仅是**配置**,也可以看成程序的**输入**,只是这部分输入在编译期已经确定,或者说被特化。
51 |
52 | ## 部分求值器
53 |
54 | 这一部分说明通过程序编译期输入进行程序优化的潜力。
55 |
56 | 我们把程序的输入分为两部分,一部分是运行时才知道的输入,一部分是编译期就可以知道的输入。
57 |
58 | $$\verb|INPUT| = \verb|INPUT|_{compiletime} \cup \verb|INPUT|_{runtime}$$
59 |
60 | 存在一个部分求值器,接收一个程序和该程序的编译期输入,据此输出另一个优化后的程序。
61 |
62 | $$P(\verb|program|, \verb|INPUT|_{compiletime}) = \verb|program_optimized|$$
63 |
64 | 优化前后程序功能等价。
65 |
66 | $$\verb|program|(\verb|INPUT|_{runtime}) = \verb|program_optimized|(\verb|INPUT|_{runtime})$$
67 |
68 | 假设我们现在有一个语言的解释器 Interpreter 和它要执行的一段脚本 script。解释器本身也是一个程序,它接收脚本 script 和 执行脚本时用户的输入 这两个输入。这两个输入处在不同的阶段,我们把前者看成编译期输入:
69 |
70 | $$P(\verb|Interpreter|, \verb|script|) = \verb|program_optimized|$$
71 |
72 | 当然需要保证对运行时输入表现相同:
73 |
74 | $$\verb|Interpreter|(\verb|INPUT|_{runtime}) = \verb|program_optimized|(\verb|INPUT|_{runtime})$$
75 |
76 | 等式左边是用解释器执行这段脚本,接收执行脚本时用户输入。它等价于等式右边的另一个优化后的程序接收执行脚本时用户的输入。相当于我们得到了一个脚本编译后的程序。也就是 **只需要实现一个语言的解释器,它的编译器自然存在**。然而通用的效果良好的部分求值器难以实现,因此还没有实用的“给定一个语言的解释器,得到一个该语言的**高效**的编译器”的方法。但换言之,只要我们将尽可能多的计算提前到编译期,就能提高运行时的性能。如现在模版引擎常常将模版解析部分放在编译期。
77 |
78 | 这里未必需要严格的「编译期」,「运行时」的概念,只要能将 **计算的阶段(Staging)** 提前即可。例如 SSG (Static Site Generation) 和 SSR (Server Side Render) 都是提前渲染了页面,但是 SSG 是在「编译期」,SSR 是服务器实时完成。
79 |
80 | ### 实际应用
81 |
82 | 什么东东能接收一个程序和(一段时间)输入数据,输出一个更高效的程序?
83 |
84 | - JIT 编译器
85 | - PGO
86 |
87 | PGO 听上去不是很部分求值,但其实挺 staging 的。
88 |
89 | ## Tree-Shaking 算法
90 |
91 | 第三方库,如 jQuery,lodash,vue-router 等,可以完全被放到运行时加载(比如以 CDN 形式提供)。然而,随着 ES6 模块机制的提出和打包工具的 Tree-Shaking 优化算法的发展,现在一般会有一个打包的步骤以减小体积。打包也是一种形式的编译,Tree-Shaking 就是在编译期特化(只保留使用到的函数/类/对象)代码,伪代码:
92 |
93 | ```javascript
94 | const included = new Set();
95 | let needTreeShakingPass = true;
96 | // 迭代直到算法收敛
97 | // 算法收敛意味着一轮迭代中,所有结点的状态都不变,且没有新的结点被包含
98 | While (changed) {
99 | needTreeShakingPass = false;
100 | markTopLevelSideEffectNodesIncluded(graph, included);
101 | for (const node of included) {
102 | // 更新结点状态。例如 if (a) {} 中,如果 a 在上一轮迭代中被标记为有修改,那就要置这里的条件值为 UNKNOWN
103 | const isNodeStateUpdated = UpdateNodeState(node);
104 | // 结点状态更新可能会带来新的结点被包含。
105 | // 若 if (a) {} 中 a 的值从 false 变为 UNKNOWN,就要新包含 块语句 结点
106 | const isNewNodeIncluded = MarkNodeUsedByCurrentNodeIncluded(node, included);
107 | if (isNodeStateUpdated || isNewNodeIncluded) {
108 | needTreeShakingPass = true;
109 | }
110 | }
111 | }
112 | ```
113 |
114 | Tree-Shaking 实现还有一些细节,例如每个表达式的值都会在判断副作用阶段就被首先尽可能的求出;表达式的值只有已知到未知的变化路径,最多变化一次,以保证算法收敛等,这里不再展开。
115 |
116 | ## 特化第三方库函数
117 |
118 | 然而,Tree-Shaking 算法只能精确到是否包含**某个函数/类/对象**,无法做到对内部属性的精确分析。一部分原因也是因为 JavaScript 中这些都是一等对象(first-class objects),由于 JavaScript 的动态特性很难追踪它们的所有使用。所以 UglifyJS 等工具一般也没办法对类的方法重命名,或者去除对象的某个没有被使用到的属性。但有的时候在包含某个函数/类/对象的粒度并不足够我们使用。例如,Vue 很多第三方组件库都提供了丰富的配置选项,但一个 Vue 组件是整体作为一个对象,因此我们打包时只能选择包含或不包含某个组件,无法部分包含:
119 |
120 | 
121 |
122 | 上图是 Naive UI 的[分页组件](https://www.naiveui.com/zh-CN/os-theme/components/pagination),即使我们只需要使用「简单分页」这一功能,也不得不去包含一大堆无用的渲染代码。如果某个第三方函数/类/对象的体积成为了打包产物体积的瓶颈,有以下几种可能的解决方法
123 |
124 | ### 拆分单独的函数/功能对象
125 |
126 | 暴露不同的函数好理解(直接不同功能提供不同函数),也可以选择暴露不同的对象以方便组合 e.g.
127 |
128 | ```javascript
129 | export PluginA;
130 | export PluginB;
131 |
132 | export SomeLibraryFunction() {
133 | // ...
134 | const self = {
135 | // 实际执行
136 | run() {
137 | // ...
138 | }
139 | installPlugin(plugin) {
140 | // ...
141 | return self;
142 | }
143 | };
144 | return self;
145 | }
146 |
147 | // 用户调用
148 | SomeLibraryFunction().installPlugin(PluginA).run();
149 | ```
150 |
151 | 此时 PluginB 因为没有被使用,自然不会被包含。
152 |
153 | ### 打包工具静态分析
154 |
155 | 除了本文一开始提到的 PR,Rollup 当前还有另一个 [PR](https://github.com/rollup/rollup/pull/5420) 在推进,是关于消除对象的未使用属性的(2018 年被[提出](https://github.com/rollup/rollup/issues/2201),一直未得到解决)。
156 |
157 | ### Babel 插件
158 |
159 | 对 Vue 组件而言,暂时没有一个通用的静态分析工具自动替换所有已知调用参数(需要处理默认参数等问题)。然而,对于体积瓶颈的组件选项我们可以简单使用 babel 转译,来帮助打包时消除无用分支(其实也可以自己维护一份删除了不需要分支的组件,但这样可能不利于后续功能需求变更)。最后可以达到的效果是作为 Rollup/其他打包工具的插件提供,可以在配置文件中手动指定已知属性的值:
160 |
161 | ```typescript
162 | // 让插件把组件 setup 函数内的 props.disabled 和 render 函数内的 this.disabled 等都替换成 false
163 | // 则对应逻辑分支会被消除
164 | PartialEvaluator({
165 | components: {
166 | Tag: {
167 | disabled: false,
168 | checkable: false,
169 | closable: false,
170 | },
171 | },
172 | })
173 | ```
174 |
175 | 比如若引入了一个第三方 Tag 组件,只需要样式而不需要使用它提供的「关闭 Tag」,「选择 Tag」,「禁用 Tag」等功能,就如上配置。
176 |
--------------------------------------------------------------------------------
/posts/lc3-bench.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: lc3 评测姬
3 | date: 2021-12-10 22:17:59
4 | tags: [编译]
5 | category: web
6 | mathjax: false
7 | tocbot: true
8 | ---
9 |
10 | 总算把各种期中混杂着复变期末全部结束了,~~摸鱼一会~~
11 |
12 | 本文将安利一个开源的纯 JS 的 lc-3 模拟器,并介绍自己改的一个 lc-3 评测姬
13 |
14 | 
15 |
16 |
17 |
18 | 源项目地址:https://github.com/wchargin/lc3web
19 |
20 | 并有 github-page:https://wchargin.github.io/lc3web/
21 |
22 | ~~感觉比某大学某 ICS 课程主页安利的某模拟器好用~~
23 |
24 | ~~JavaScript 是世界上最好的语言~~
25 |
26 | Web 版的好处就在于甚至不需要安装(好文明),真正的全平台通用
27 |
28 | 大概由以下这些这些 JS 文件组成:
29 |
30 | ```shell
31 | js/
32 | ├── bootstrap.js
33 | ├── bootstrap.min.js
34 | ├── jquery-1.11.1.min.js
35 | ├── lc3_as.js
36 | ├── lc3_core.js
37 | ├── lc3_hexbin.js
38 | ├── lc3_os.js
39 | ├── lc3_ui.js
40 | ├── lc3_util.js
41 | ├── npm.js
42 | └── Queue.js
43 | ```
44 |
45 | 上面三个是用到的库,主体逻辑是 `lc3_core.js` ,主体界面交互放在了 `lc3_os.js` ,其余是一些供这两个 JS 调用的函数
46 |
47 | ~~这命名简洁清晰爱了爱了~~
48 |
49 | 所以阅读源代码主要也就看上面提到的两个 JS 文件。
50 |
51 | ## lc-3 评测姬
52 |
53 | ### 开发动机
54 |
55 | ~~采用一次性 Judge 却不提供提前检验正确性工具的 lab 就好像只能提交一次的 acm, 没有灵魂~~
56 |
57 | ### 具体实现
58 |
59 | #### 页面组件
60 |
61 | 不想破坏原来页面的美感(?),直接看着写就行了
62 |
63 | ```html
64 |
65 |
lab 评测
66 |
67 | 说明:本程序基于 GitHub 项目
68 | lc3web
69 | 修改而来,旨在满足修读 2021 USTC CS1002A 计算系统概论的同学们的可能的需要。
70 |
71 |
fork 后仓库: lc3web
72 |
73 | 请先在本模拟器中载入汇编代码或者机器码(在左侧的 Assemble 和 Raw
74 | 选项中载入),机器码需要 0011 0000 0000 0000 开头。
75 |
76 |
77 | 然后即可点击下方选择需要评测的实验,目前支持显示测试点正确性以及总执行指令数。评测要求基本参照助教文档。数据是自己随便给的,尽量全一点
78 |
79 |
132 |
133 | ```
134 |
135 | 按照原来的格式写好组件,然后加点允许用户自由设置的输入框即可。评测分别采用三个函数 `bench1(), bench2(), bench3()`
136 |
137 | 主要是因为一共没几个实验,而且输入输出不固定(直接指定寄存器),所以就直接这样高耦合低内聚的写了()
138 |
139 | #### 脚本逻辑
140 |
141 | 不想破坏原来框架的美感,另开一个 `lc3_bench.js`
142 |
143 | 核心是判题逻辑:
144 |
145 | 在 bench 函数预先处理好寄存器数据后,就要运行判题逻辑了,需要:
146 |
147 | 1. 获取设置的单个 case 最多指令数
148 | 2. pc 归位 x3000
149 | 3. 总执行指令数清零
150 |
151 | 随后就逐条指令执行。终止标志是当前即将执行的指令是 Trap 或者是 x0000(与\_\_\_助教的判题逻辑相同)
152 |
153 | ```javascript
154 | function benchTest(f) {
155 | const limit = document.querySelector("#cycleLimit").value;
156 | lc3.pc = 0x3000;
157 | lc3.totalInstruction = 0;
158 | var cnt = 0;
159 | while (true) {
160 | var op = lc3.decode(lc3.getMemory(lc3.pc));
161 | if ((op.raw >= 61440 && op.raw <= 61695) || op.raw === 0) {
162 | var str = f();
163 | break;
164 | }
165 | cnt++;
166 | if (cnt > limit) {
167 | alert("有测试样例超过单次最高执行指令数,请检查!");
168 | return;
169 | }
170 | lc3.nextInstruction();
171 | }
172 | return str;
173 | }
174 | ```
175 |
176 | 当评测结束时,传入的回调函数用于处理结果(lc-3 模拟器是一个全局变量,回调函数通过读取这些全局变量的值判断结果是否正确)
177 |
178 | 下面是每个题目判题脚本的编写。以 lab2&3 为例:
179 |
180 | ```javascript
181 | function bench2() {
182 | // r0 是给定的 n, 其余寄存器初始化为 0, 结果存在 r7,
183 | // 计算数列:f(0)=1,f(1)=1,f(2)=2,f(n)=f(n-1)+2*f(n-3) 的第 n 项
184 |
185 | // 设置批处理模式,不更新用户显示的界面(见 lc3_ui.js)
186 | window.batchMode = true;
187 |
188 | // 获取正确答案的函数(因为测试样例没有很多,直接每次 O(n) 算了)
189 | function fib(x) {
190 | var arr = [1, 1, 2];
191 | for (var i = 3; i <= x; i++) {
192 | arr[i] = (arr[i - 1] + 2 * arr[i - 3]) % 1024;
193 | }
194 | return arr[x];
195 | }
196 |
197 | // 获取用户填入的测试样例
198 | const testcase = document
199 | .querySelector("#testcase1")
200 | .value.replace(/\s*/g, "")
201 | .split(",")
202 | .map(Number);
203 |
204 | var str = ""; // 最终将显示的结果
205 | var sumInstruction = 0; // 总计命令数(用于计算平均值)
206 |
207 | for (var i = 0; i < testcase.length; i++) {
208 | // 每次测试先初始化寄存器
209 | window.lc3.r = [0, 0, 0, 0, 0, 0, 0, 0];
210 | window.lc3.r[0] = testcase[i];
211 |
212 | var ans = fib(testcase[i]); // 获取正确答案
213 | str += `测试数据 F(${testcase[i]}) = ${ans} `; // 理论答案
214 | str += benchTest(bench_res); // 实际测试
215 | }
216 |
217 | // 所有样例评测完毕
218 | str += "平均指令数 " + sumInstruction / testcase.length; // 显示平均指令
219 | alert(str); // 显示最终将显示的结果
220 | window.gExitBatchMode(); // 刷新界面,退出批处理模式
221 | return;
222 |
223 | // 判断结果正确性的函数
224 | function bench_res() {
225 | // 判断结果
226 | var lc3res = window.lc3.r[7];
227 | sumInstruction += window.lc3.totalInstruction;
228 | if (lc3res == ans) {
229 | return `你的回答正确,指令数 ${window.lc3.totalInstruction} \n`;
230 | } else {
231 | return `你的答案是 ${lc3res} \n`;
232 | }
233 | }
234 | }
235 | ```
236 |
237 | 大概还可以抽象优化一下,摸了
238 |
239 | Have fun playing
240 |
241 | 不知道下一届还需不需要用到这个(希望人没事)
242 |
--------------------------------------------------------------------------------
/posts/csapp-shell.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: CSAPP 之 Shell Lab
3 | date: 2022-03-19 22:27:56
4 | tags: [C/C++, os]
5 | category: 笔记
6 | mathjax: true
7 | tocbot: true
8 | ---
9 |
10 | 博客摘几篇自己做的 CSAPP 发发(自认为可能有点参考价值)
11 |
12 | 全部代码可以见 [GitHub 仓库](https://github.com/liuly0322/CSAPP-LABS)
13 |
14 | 本篇是 Shell Lab
15 |
16 |
17 |
18 | 本次实验的要求是实现一个支持任务控制的 Unix shell 程序
19 |
20 | 程序的框架已经给出,只需要补充一些功能性的函数
21 |
22 | 由于整体是一个编程性质的实验,所以这里只在贴上最后结果后,讲一些实验中值得注意的函数
23 |
24 | ## 评测
25 |
26 | 可能由于每次运行的 pid 都有所不同,并且也无法保证 `/bin/ps` 行为相同,本实验没有给出一键测试 shell 正确性的程序
27 |
28 | 但对于每个评测点,都给出了 `tsh` 和 `tshref` 生成运行结果的程序
29 |
30 | 所以只要批量生成结果后手动比较即可
31 |
32 | `tshref` 的结果已经给出,在 `tshref.out` 文件中,下面我们写一个脚本批量生成 `tsh` 的运行结果 (fish 脚本,语法与 posix 有所不同)
33 |
34 | 
35 |
36 | 然后手动比较文件
37 |
38 | (因为 pid 都在括号内,所以首先用正则表达式把 pid 统一替换成 10000)
39 |
40 | 
41 |
42 | 随后比较:
43 |
44 | ```bash
45 | diff 1.out tshref.out > out.diff
46 | ```
47 |
48 | 查看发现只有 `ps` 运行结果不同,而运行行为达到预期
49 |
50 | 
51 |
52 | 因此实验完成
53 |
54 | ## eval
55 |
56 | 补全如下:
57 |
58 | ```cpp
59 | void eval(char* cmdline) {
60 | char* argv[MAXARGS];
61 | int bg;
62 | pid_t pid;
63 |
64 | bg = parseline(cmdline, argv);
65 | if (argv[0] == NULL)
66 | return;
67 |
68 | if (!builtin_cmd(argv)) {
69 | if ((pid = fork()) == 0) {
70 | setpgid(0, 0);
71 | if (execve(argv[0], argv, environ) < 0) {
72 | printf("%s: Command not found\n", argv[0]);
73 | exit(0);
74 | }
75 | }
76 |
77 | // 此处是父进程
78 | addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
79 | if (!bg) {
80 | waitfg(pid);
81 | } else {
82 | printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
83 | }
84 | }
85 |
86 | return;
87 | }
88 | ```
89 |
90 | 和书上给出的例程差不多,主要区别如下:
91 |
92 | - `setpgid(0, 0);` 使得能正常接受别的 `shell` 发送的终止信号,否则自己的所有子进程都会被终止
93 | - 前台进程的阻塞用的是 `waitfg`,后面实现 `fg` 命令也需要用到这一函数
94 |
95 | 理论上来说这里需要考虑屏蔽信号,实际上,现代计算机多核 CPU 并行运行各个进程,在进程数小的情况下很难因为并发遇到执行时序的问题,不过严谨考虑还是加锁为好,这里作为一个 toy 程序就没加了
96 |
97 | ## do_bgfg
98 |
99 | 这里的 `do_xx` 似乎是这种解释器程序普遍的命名习惯,代表执行什么什么内置指令
100 |
101 | `bg` 和 `fg` 要实现的是对进程运行状态的转换
102 |
103 | ```cpp
104 | void do_bgfg(char** argv) {
105 | int id;
106 | struct job_t* job;
107 |
108 | // 这里根据参数判断合法性,获取 job
109 | ......
110 |
111 | job->state = (argv[0][0] == 'b' ? BG : FG);
112 | kill(-job->pid, SIGCONT);
113 | if (argv[0][0] == 'b')
114 | printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
115 | else
116 | waitfg(job->pid);
117 |
118 | return;
119 | }
120 | ```
121 |
122 | 注意前台进程需要等待即可
123 |
124 | ## waitfg
125 |
126 | 很实用的 `helper` 函数
127 |
128 | ```cpp
129 | void waitfg(pid_t pid) {
130 | struct job_t* job = getjobpid(jobs, pid);
131 | while (job->state == FG) {
132 | sleep(1);
133 | }
134 | return;
135 | }
136 | ```
137 |
138 | 按照实验说明的推荐,采用轮询加上休眠的方式即可,这样对这一进程的负担也比较小
139 |
140 | ## sigint & sigtstp
141 |
142 | 接下来是几个信号处理时的异步回调函数
143 |
144 | ```cpp
145 | void sigint_handler(int sig) {
146 | pid_t f_pid = fgpid(jobs);
147 | if (f_pid) {
148 | kill(-f_pid, sig);
149 | }
150 | return;
151 | }
152 | ```
153 |
154 | ```cpp
155 | void sigtstp_handler(int sig) {
156 | pid_t f_pid = fgpid(jobs);
157 | if (f_pid) {
158 | kill(-f_pid, sig);
159 | }
160 | return;
161 | }
162 | ```
163 |
164 | 这两个函数比较类似,接收到键盘的终止 / 暂停信号后发送给前台进程的进程组,所以用 `-f_pid`
165 |
166 | ## sigchld
167 |
168 | 这一部分用于处理子进程的中断 / 暂停信号
169 |
170 | ```cpp
171 | void sigchld_handler(int sig) {
172 | pid_t pid;
173 | int status;
174 |
175 | while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
176 | if (WIFSTOPPED(status)) { // 暂停信号
177 | printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid,
178 | WSTOPSIG(status));
179 | getjobpid(jobs, pid)->state = ST;
180 | } else {
181 | if (WIFSIGNALED(status)) { // 退出信号
182 | printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid),
183 | pid, WTERMSIG(status));
184 | }
185 | // 子进程退出信号,以及正常运行结束
186 | deletejob(jobs, pid);
187 | }
188 | }
189 | return;
190 | }
191 | ```
192 |
193 | 注意由于 unix 信号的阻塞机制,这里需要用 `while` 处理所有的僵死进程
194 |
195 | ## debug 细节
196 |
197 | 由于是编程实验,这一部分记录实验过程中遇到的有意思的问题以及解决方案
198 |
199 | ### 信号处理
200 |
201 | 在 `sigchld_handler` 函数中,一开始仿照书本 (注:第二版书),采用的条件是
202 |
203 | ```cpp
204 | while ((pid = waitpid(-1, &status, 0)) > 0) {
205 | ......
206 | }
207 | ```
208 |
209 | 但是会出现奇怪的 bug,终止进程输出的提示均为 `[0] (0)`
210 |
211 | 实际上书本的写法是有些问题的:这样做虽然会回收所有的僵死进程,但是 `waitpid` 的默认行为会不断等待活跃进程,直到活跃进程僵死才会返回
212 |
213 | 而我们虽然希望信号处理函数能够回收所有的进程,但我们也不希望信号处理处理函数会一直阻塞,直到所有进程运行完毕才继续执行,这样的话之后连前台进程都不存在了,自然也就获取不到前台进程的 `pid` 了,故 shell 的显示就会出现问题。理想情况下,应该是一次信号处理函数处理完当前所有僵死进程后就退出
214 |
215 | 换言之,这里的 `waitpid` 应该是一个同步函数而非异步函数
216 |
217 | (吐槽一下,高级语言的同步异步函数写多了再看 C 语言这种面向底层的语言确实有点小头疼)
218 |
219 | 为了修正这一问题,可以通过设置 `waitpid` 的 `options` 参数改变 `waitpid` 的默认行为
220 |
221 | 修改后如下:
222 |
223 | ```cpp
224 | while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
225 | ......
226 | }
227 | ```
228 |
229 | `WNOHANG` 选项使得这一函数变为同步函数,如果没有僵死进程就立刻返回
230 |
231 | `WUNTRACED` 选项使得这一函数能够处理暂停的进程
232 |
233 | ### printf
234 |
235 | 可能在阅读上面代码时,读者会觉得 `sigchld_handler` 的信号处理很不优雅, `printf` 这种根据信号类型来打印的函数为什么不放在具体的信号处理函数里,而是选择放在一个大的回收子进程的函数内呢?
236 |
237 | 从功能上来说,是因为如果 shell 中运行的子进程收到终止 / 暂停信号而终止,我们希望 shell 程序也能提示用户。而信号处理函数只能处理 shell 进程自身收到的信号,适用范围就窄了。如果把打印写在信号处理函数内,信号依旧会得到处理,但是会在 `test16` 中因为没有子进程终止信号的提示,输出与 `tshref` 不一致
238 |
239 | 值得一提的是,这里有一个很有意思的安全问题,由信号处理函数中的 `printf` 引发。我们现在所写的 `sigchld_handler` 事实上也是不安全的,C 语言中信号处理函数中能安全调用的函数是有限的,可以在 [这个网站](https://man7.org/linux/man-pages/man7/signal-safety.7.html) 查阅
240 |
241 | 而 `printf` 函数并不在此列,这是因为 `printf` 为了确保线程安全会在写入到文件(这里是写入到标准输出这一文件描述符)时给文件加一个锁,但是注意:这个 `shell` 程序在主体控制流中也有 `printf` 函数的调用(例如打印进程的提示消息),考虑现在发生了这样的一个调用,并且在从打印提示消息到给文件描述符解锁的过程中,恰好程序收到一个终止信号,于是信号处理函数被调用,进程会阻塞在给文件描述符解锁之前,转而去执行信号处理函数中的 `printf` ,而这次的调用在执行到准备打印时,却会发现标准输出被加锁了(因为还没能成功解锁),故会停下来等候。但信号处理函数已经阻塞了进程执行正常控制流,自然也就一直等不到谁能给标准输出解锁了:这就造成了程序的死锁
242 |
243 | 严格来说,我们这个 "toy shell" 目前还是一个非常不健壮的状态。这种类似的信号处理更合理也更普遍的方式是使用管道来完成,不过这就大大超出了本实验的范畴,故不在此介绍
244 |
245 | 至此,Shell Lab 的实验圆满结束
246 |
--------------------------------------------------------------------------------
/posts/sudoko-iddfs.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 数独的模拟逻辑解法的实现
3 | date: 2021-06-03 23:38:03
4 | tags: [算法, C/C++]
5 | category: 算法
6 | mathjax: true
7 | tocbot: true
8 | ---
9 |
10 | 本文将主要介绍人工解数独时采用的唯余法的算法实现,并给出将其改造成迭代加深算法时,解所在最低层数的确定。
11 |
12 | ## 唯余法
13 |
14 | > 当数独谜题中的某一个宫格,因为所处的列、行及九宫格中,合计已出现过不同的 8 个数字,使得这个宫格所能填入 的数字,就只剩下那个还没出现过的数字时,我们称这个宫格有唯余解。
15 |
16 | 可以看到,唯余法就是利用排除法得出数独某个位置可能的唯一解。但在人工用这种方法解数独时,可能会出现以下问题:
17 |
18 | 1. 只剩唯一解的数独格子难以直接观察得到。采用唯余法观察时,行,列,宫格的重复性都要考虑,~~对人的视力提出了巨大的挑战~~。
19 | 2. 可能,而且很可能不存在能直接确定答案的数独格子。比如可能排除到最后,这个格子还剩 4 和 6 能填,确定不了具体需要填哪一个数。
20 |
21 |
22 |
23 | 但以上两点在计算机算法实现时却可以比较好的解决。对于第一点,计算机可以穷举每一个格子,对于第二点,计算机可以穷举每一个可能性,这就为我们采用唯余法编写程序提供了可行性。
24 |
25 | ## 算法实现
26 |
27 | ### 基本思路
28 |
29 | 采用递归的思路编写程序。这里采用 DFS 遍历所有格子。递归函数 int 类型,有解返回 1,无解返回 0
30 |
31 | 
32 |
33 | 此外,内层每次递归时也要对操作进行回溯,此处不表。
34 |
35 | 这里列出流程所必需的变量:
36 |
37 | - `char sudoko_solve[9][9]`:记录着当前的解
38 | - `char num_can_input[81]`:记录着每个格子还可以放多少数字。0 表示无解,取最大值 127 表示格子已经被填入数字
39 | - `bool mark[81][10]`:记录每个格子是否允许放 1 到 9 的数字。0 空出来,是为了代码简洁性。
40 |
41 | 在进入正式流程之前,先要对这些数字预处理。这里略去过程。
42 |
43 | `sudoko_solve[9][9]` 对于已有数字为该数字,否则为 0。
44 |
45 | 可能会注意到,这里存在 $9*9$ 和 81 两种数据记录的方法。这是由笔者别的模块的函数写法决定的。
46 |
47 | 转换关系:对于坐标 $(x,y)$,对应的后者数字为 $9*x+y$。反之,$x = cell / 9, y = cell \% 9$。
48 |
49 | ### 代码实现
50 |
51 | 根据上面的说明写出代码:
52 |
53 | ```cpp
54 | int dfs_ID(bool mark[][10], char* num_can_put, int depth) {
55 | if (depth> 90) {
56 | return 0; //迭代加深,退出递归,目前深度待定
57 | }
58 | struct IDDFS_change* queue[81]; //记录填入唯一解数字时造成的改变
59 | int change_num = 0; //对应上面的 queue,记录有多少唯一解
60 | int min = 127, min_index, flag = 0;
61 | do {
62 | min = 127; flag = 0;
63 | for (int i = 0; i < 81; i++) {
64 | if (num_can_put[i] == 0) { //无解,回溯后退出
65 | solve_res = 1;
66 | for (int i = change_num - 1; i>= 0; i--) {
67 | dfs_ID_recall(mark, num_can_put, queue[i]);
68 | }
69 | return 0;
70 | }
71 | else if (num_can_put[i] == 1) { //唯一解,填入并记录
72 | flag = 1;
73 | queue[change_num] = new IDDFS_change; change_num++;
74 | for (int j = 1; j <= 9; j++) {
75 | if (!mark[i][j]) {
76 | dfs_ID_fill(mark, num_can_put, i, j, queue[change_num - 1]);
77 | break;
78 | }
79 | }
80 | continue; //填入唯一解后,再次进入循环
81 | }
82 | else if (num_can_put[i] < min) { //记录最小值
83 | min = num_can_put[i];
84 | min_index = i;
85 | }
86 | }
87 | } while (flag == 1); //填入了唯一解,需要再搜
88 | if (min == 127) { //最小值是 127,意味着每个格子都是唯一解的,也就是找到了数独的解
89 | solve_res = 2;
90 | return 1;
91 | }
92 | //对 min_index 进入下一层
93 | int cell = min_index;
94 | for (int j = 1; j <= 9; j++) {
95 | if (!mark[cell][j]) {
96 | struct IDDFS_change* nextlay = new struct IDDFS_change; //记录改变
97 | dfs_ID_fill(mark, num_can_put, cell, j, nextlay);
98 | if (dfs_ID(mark, num_can_put, depth + 1)) { //填入
99 | return 1;
100 | }
101 | dfs_ID_recall(mark, num_can_put, nextlay); //无解,回溯
102 | }
103 | }
104 | for (int i = change_num - 1; i>= 0; i--) { //回溯填入的唯一解
105 | dfs_ID_recall(mark, num_can_put, queue[i]);
106 | }
107 | return 0;
108 | }
109 | ```
110 |
111 | 这里用 `flag` 记录是否有只有唯一解的格子。
112 |
113 | 填入和回溯函数借助了一个结构体:`struct IDDFS_change`。
114 |
115 | ```cpp
116 | struct IDDFS_change {
117 | int cell_fill,num_fill;
118 | int cell_num_can_put;
119 | int queue[30], change_num;
120 | };
121 | ```
122 |
123 | 这里仅仅是用于储存需要回溯的一些数据,所以不需要用类,结构体足矣。
124 |
125 | `dfs_ID_fill` 用于填入数字,并记录信息到 p 指针,便于回溯。
126 |
127 | ```cpp
128 | void dfs_ID_fill(bool mark[][10], char* num_can_put, int cell, int num, struct IDDFS_change* p) {
129 | p->cell_fill = cell; p->num_fill = num;
130 | p->cell_num_can_put = num_can_put[cell]; p->change_num = 0; //记录回溯需要信息
131 | int x = cell / 9, y = cell % 9;
132 | sudoko_solve[x][y] = num; //数字填入该格子
133 | num_can_put[cell] = 127;
134 | for (int k = 0; k < 9; k++) {
135 | if (!mark[9 * x + k][num] && !sudoko_solve[x][k]) {
136 | p->queue[p->change_num] = 9 * x + k; p->change_num++; //记录改变
137 | mark[9 * x + k][num] = true;
138 | num_can_put[9 * x + k]--;
139 | }
140 | }
141 | for (int k = 0; k < 9; k++) {
142 | if (!mark[9 * k + y][num] && !sudoko_solve[k][y]) {
143 | p->queue[p->change_num] = 9 * k + y; p->change_num++;
144 | mark[9 * k + y][num] = true;
145 | num_can_put[9 * k + y]--;
146 | }
147 | }
148 | int i_start = x / 3 * 3, j_start = y / 3 * 3;
149 | for (int dx = 0; dx < 3; dx++) {
150 | for (int dy = 0; dy < 3; dy++) {
151 | if (!mark[(i_start + dx) * 9 + (j_start + dy)][num] &&
152 | !sudoko_solve[(i_start + dx)][(j_start + dy)]) {
153 | p->queue[p->change_num] = (i_start + dx) * 9 + (j_start + dy);
154 | p->change_num++;
155 | mark[(i_start + dx) * 9 + (j_start + dy)][num] = true;
156 | num_can_put[(i_start + dx) * 9 + (j_start + dy)]--;
157 | }
158 | }
159 | }
160 | }
161 | ```
162 |
163 | `dfs_ID_recall` 用于回溯。
164 |
165 | ```cpp
166 | void dfs_ID_recall(bool mark[][10], char* num_can_put, struct IDDFS_change* p) {
167 | int x = p->cell_fill / 9, y = p->cell_fill % 9;
168 | num_can_put[9 * x + y] = p->cell_num_can_put;
169 | sudoko_solve[x][y] = 0;
170 | for (int i = 0; i < p->change_num; i++) {
171 | num_can_put[p->queue[i]]++;
172 | mark[p->queue[i]][p->num_fill] = false;
173 | }
174 | delete p;
175 | }
176 | ```
177 |
178 | ## 算法的迭代加深改进
179 |
180 | 这里可以考虑随机生成数独并利用该算法求解,记录:出现解时最低到达深度。
181 |
182 | 随机生成数独是指在 $9*9$ 的格子上随机放入指定数量的数字(确保不矛盾,但未必有解)
183 |
184 | 因此,这里需要更改放入数字数量,反复实验,实验结果如下所示:
185 |
186 | - 初始 17 数字:
187 |
188 | 21,20,20,21,24,20,21,27,21,27,21,24,24,21,19,21,20,23,28,24,25,23。
189 |
190 | - 初始 25 数字:
191 |
192 | 12,10,16,9,12,9,13,6,13,7,12,11,10,8,12,10,5,3,17,7。
193 |
194 | - 初始 30 数字:
195 |
196 | 3,3,5,4,5,3,3,3,3,2,8,11,6,2,10,5,4,4,2,3。
197 |
198 | 可以看出,大部分情况,设置搜索深度为 24 已经能够找到目标,这样的搜索深度并不大。如果没找到目标,全部遍历即可。
199 |
200 | 本算法在复杂度上和朴素 dfs 都是 $O(c^n)$ 级别,$n=81$,但是相比朴素 dfs 大幅降低了 c,所以即便穷举所耗时间也比较优秀。
201 |
202 | 故可以这样将算法改进为迭代加深版本:
203 |
204 | ```cpp
205 | int Sudoko::dfs_ID(bool mark[][10], char* num_can_put, int depth, bool full_search) {
206 | if (depth> 24 && !full_search) {
207 | solve_res = 1;
208 | return 0; //迭代加深,退出递归。
209 | }
210 | ...
211 | }
212 | ```
213 |
214 | 增加了一个参数 `bool full_search` 用于表示当前是否需要全部搜索。至此,就完成了本次实验题关于实现迭代加深 DFS 算法的要求。
215 |
--------------------------------------------------------------------------------
/posts/lifemanual.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 写于 2021 高考前
3 | category:
4 | - 日志
5 | date: 2021-05-21 20:16:47
6 | mathjax: true
7 | tags: [杂谈]
8 | tocbot: true
9 | ---
10 |
11 | ## 前言
12 |
13 | 本文写于 2021 高考临近前,旨在给即将进入大学的各位提供一些微小的帮助。
14 |
15 | 考虑到本人能力,视野有限,故选校 / 学习部分更多是站在一个 985 理工科的视角上,若不符合个人情况,仅供参考。
16 |
17 |
18 |
19 | ---
20 |
21 | ## 选校篇
22 |
23 | 基本原则:
24 |
25 | 1. “兴趣”优于“热门”(前提真的是兴趣
26 |
27 | 2. “择专”优于“择校”
28 |
29 | ### 如何得知专业信息
30 |
31 | ---
32 |
33 | 其实高考后留给各位选专业的时间并不是很多,因此如何在短时间内搜集自己想选择的专业的内容是很重要的。下面提供几个了解专业信息时可以侧重考虑的方面:
34 |
35 | #### 学什么
36 |
37 | “顾名思义”并不总能奏效,比如 USTC 有信息学院和计算机学院两个平行的院系,对于大部分没有搜集过相关信息的同学,如果我问他们这两个学院之间有什么区别,相信大多数人都是答不上来的。
38 |
39 | 此外,就算是学院的名字你很熟悉,比如物理学院,但是大学阶段的物理真的和诸位在中学阶段的物理学到的是一回事吗?这些问题都是很值得思考的。当然,高考后到填报志愿之间的时间并不长,因此到也不必真的需要对每个专业如数家珍,但是对自己感兴趣的专业应该多少做到心里有数。
40 |
41 | 这里提供一些了解专业学习内容的比较好的办法:
42 |
43 | > 在网络上搜索本专业的一些通用介绍,可以参考百度百科、学院官网的介绍(包括师资、培养计划、专业排名等)、官方的各种就业深造统计数据、知乎等问答性网站,但这些内容往往缺少针对性,而且不是可交互的,无法及时针对你的疑问做出相应回答。
44 |
45 | 本部分引用自上海交通大学生存手册,文末可获得详细链接。补充一点,百度搜索“你考虑的学校 + 教务系统”,一般排在前列会出现该学校的教务系统或者教务处界面,在这里通常可以公开查询到不同专业的培养方案(也就是必修什么课)。
46 |
47 | #### 未来发展
48 |
49 | 就业 or 科研?就业前景如何,科研收入如何,辛不辛苦?科研可能并不像大家想象的那么美好,从硕士到博士到博士能顺利毕业到评奖,评教职,以至于更上,每一步都会淘汰不计其数的人,如果没有对科研的真心热爱,这会是一条很辛苦的路。
50 |
51 | ### 新兴专业,交叉学科与强基计划
52 |
53 | ---
54 |
55 | 这一条要单独列出来讲,是因为这些名词看上去很高端很唬人,但大部分这些所谓的名词并不是真的就这么美好:新兴专业,很多时候,连同其他一些热门专业一样,属于是顺应了时代的需求或者被其他因素推动捧成了热门专业(这里点名“21 世纪是生物的世纪”),并不是说这些热门专业就不好,而是说没必要冲着它们的名字就作出选择,还是要想清楚自己喜欢的是什么。
56 |
57 | 此外,对于强基计划,还是要慎重考虑,一方面这些强基计划覆盖的专业多为基础专业,且理论上不允许转强基计划之外的专业;另一方面,对于强基计划对应的特殊培养方案以及可能宣传的教授指导:特殊培养方案未必对你是好事,永远不要太高估自己的抗压能力;而教授辅导这件事情,事实上只要你对该学科有兴趣,哪怕你甚至不是这个专业,在大学你往往也可以通过邮件联系导师进行本科科研等,强基计划只能是提供一个平台,具体个人的发展还是得靠自己的奋斗(((
58 |
59 | 既然说到了这些比较现实的因素,那大家可以看看这份 [清华大学 2021 转专业结果](https://www.zhihu.com/question/455564234),以供参考。
60 |
61 | ### 其他影响择校因素
62 |
63 | ---
64 |
65 | 列举一些以供参考:
66 |
67 | 1. 转专业方便程度
68 |
69 | 2. 宿舍条件:24 小时热水,上床下桌,卫生间条件,晚上是否断网断电,空调是否自行决定开关
70 |
71 | 3. 地理位置:是否方便,离家远近等
72 |
73 | 4. ~~男女比例~~
74 |
75 | 5. 规章制度是否严格,是否自由(比如点不点名)
76 |
77 | 以上信息有条件的可以找认识的问问,没条件的话可以多看看想报的学校的贴吧以及知乎评价。
78 |
79 | ---
80 |
81 | ## 开学过渡篇
82 |
83 | 本篇内容将简要带来大学的生活概要,以便各位提前准备,适应。
84 |
85 | ### 暑假
86 |
87 | ---
88 |
89 | 暑假主要活动当然就是玩,不过如果你**非要**想学习的话,这里推荐几个可以考虑的:
90 |
91 | 1. 数学:数列极限的 $\varepsilon-N$ 语言,函数极限的 $\varepsilon-\delta$ 语言,如果你开学后第一学期要学线性代数,可以提前看一下 3blue1brown 做的 [线性代数的本质](https://www.bilibili.com/video/BV1ys411472E),或者 MIT 的 [线代课程](https://www.bilibili.com/video/BV1zx411g7gq?p=3),国内的线性代数教材普遍忽视了一些几何的东西,可能理解起来有点难受。
92 |
93 | 2. 程序设计:不同学校通修的语言可能不太一样,不过也没必要提前学太多,大概看看 B 站网课有个基本的理解就行了。如果你想对计算机科学,而不仅是编程,有更深的理解,这里推荐一下哈佛的 [CS50](https://www.bilibili.com/video/BV1Rb411378V) 课程,是一门类似于讲座性质的公开课,这视频有点老了,不过还是挺经典。
94 |
95 | ### 军训
96 |
97 | ---
98 |
99 | 这应该是大部分同学与大学的第一次接触了,一般时间也就两周左右,基本内容从晒太阳到练习军体拳到聊天到展示才艺不等,几乎等价于一场大型体育课,以下几点注意一下:
100 |
101 | 1. 军训可能采用的是那种平底鞋,站起军姿属实又麻又累,强烈建议提前准备一次性鞋垫,而且一般一次可以多垫几张,如果没有准备的话,~~反正我校每年都是有男生军训几天受不了去超市买卫生巾的~~
102 |
103 | 2. 防晒:站在大太阳底下两周对皮肤造成的~~荼毒~~和平时完全是不能比的,因此强烈建议提前准备防晒霜之类的,~~男孩子也要爱护好自己的皮肤~~,本人当时头铁,结果袖子内外的皮肤晒出了明显的分界线,过了好几个月才逐渐模糊
104 |
105 | 3. 降温:可选,不是很必要,因为假如训练量大了,教官一般还是比较好心的,会有休息时间的,而且别人都在训练,你一个人贴清凉贴吹电风扇似乎也有点奇怪(((
106 |
107 | 以上主要是提醒一下对自己好一点,相信到时候你们妈妈应该也会跟你们说这些的,这里是稍微再强调一下。
108 |
109 | ### 寝室
110 |
111 | ---
112 |
113 | 大概会成为各位在大学待最久的地方?(不过如果你天天去图书馆卷或者在实验室搞科研那确实例外)宿舍条件这玩意选完学校就已经定下来了,是否有 24h 热水,是否上床下桌,独立卫浴... 本篇主要安利一些(可能有用)小东西。
114 |
115 | 1. 小风扇:推荐选那种可以手持又可以放桌上的那种,风量基本够用,反正可以往近了放。
116 |
117 | 2. 插线板:提醒一下买大一点的,以免不够用。
118 |
119 | 3. 理线器:那种可以粘在桌子后面桌子底下的,建议桌上各种数据线鼠标线电线如果过长了都可以绕到桌子后面走一下线,让桌子整洁一点。
120 |
121 | 4. 键盘鼠标:如果没买的话,建议买无线的,绝对比有线的方便了不少,然后如果不打游戏,对键盘鼠标手感没太高追求的话可以买那种宣传静音的,去图书馆学习时不会打扰别人。
122 |
123 | ---
124 |
125 | ## 开学学习篇
126 |
127 | ### 电子产品选购与使用
128 |
129 | ---
130 |
131 | 这里介绍学习时可能会需要的一些电子产品。由于苹果的产品比较特殊,推荐:
132 |
133 | 1. 要么买全套(iPhone,iPad,MacBook)中的至少两件(但考虑大家有打游戏等需求,可能笔记本更适合买 Windows 系统的),以感受苹果生态带来的联动
134 |
135 | 2. 要么就分开选购:手机平板选择安卓,电脑选择 Windows,或者如果电脑没有打游戏的需求,专业也不需要特别强的电脑配置的话,可以考虑买带有触屏的轻薄本,同时作为电脑和平板。(当然,对于学习而言,平板电脑不是必须的)
136 |
137 | #### 笔记本电脑
138 |
139 | ---
140 |
141 | 由于近来挖矿逐渐盛行的原因,电脑显卡价格暴增,这对笔记本市场也产生了一定的影响。
142 |
143 | 这里建议如果家里有电脑的话就先用着吧,现在买笔记本大部分溢价严重,如果是买轻薄本可以考虑,买一些游戏性能比较好的独显笔记本实在不太值得。
144 |
145 | 考虑到大家选购笔记本的时间可能并不统一,为了保证本文时效性,建议有购买需求的同学可以去 b 站关注 up 主 [笔吧评测室](https://space.bilibili.com/367877)
146 |
147 | #### 平板电脑
148 |
149 | ---
150 |
151 | 平板电脑对于学习不是必选项,如果要买的话建议首先考虑自己手机品牌,苹果就买苹果,华为就买华为,别的随意(((
152 |
153 | 平板电脑如果用来学习,最主要的应用场景大概就是手写记笔记了:iPad 上会有很多好用的笔记软件,goodnotes,notability 等,华为在这方面的软件生态会稍微差一点。
154 |
155 | 其次就是看很多电子书以及论文(pdf),有个平板会方便很多。如果觉得自己是那种学一门科目就一定会去找参考书看的,那买个平板应该很能方便你:对于大部分主课,助教及老师往往都会给出推荐书目,这些书目大部分都是可以在学校图书馆网站或者别的地方找到电子版下载的,或者助教会直接提供电子版。
156 |
157 | 最后再提醒一下,平板电脑对于学习不是必须的,买之前建议考虑清楚你买的平板会不会变成“买前生产力,买后爱奇艺”(((
158 |
159 | ### 学习规划
160 |
161 | ---
162 |
163 | #### 名词介绍
164 |
165 | ---
166 |
167 | **绩点**:
168 |
169 | 你所修读的每一门课程都会按照期末总评(一般参考考试成绩和平时分)给出一个对应的成绩档位,比如采用 95-100 分对应 4.3, 90-94 对应 4.0, 85-89 对应 3.7......
170 |
171 | 那么对于所有你所修读的要计算成绩的课程,按照学分对每门课所取得的成绩档位进行加权平均,即得到你的绩点(GPA)。计算公式:
172 | $$\frac{\sum score_i * credit_i}{\sum credit_i}$$
173 | 对于校内评比,往往 GPA 是很重要的一个因素。或者有的学校会单纯将分数按照学分加权平均(略去转换每门课绩点的过程),以总加权平均分作为校内评比依据。
174 |
175 | **通修课,专业课,选修课**:
176 |
177 | 基本上你要学习的课程可以分为两类,一类是培养计划内的,统称为必修课,另一类是培养计划要求之外的,统称为选修课。顺利毕业除了要求修完必修学分之外,往往还要求选修学分达到一定数目。
178 |
179 | 必修课中有一部分是数理基础课,体育课,英语课,思政课等通修课,还有一部分则是与你专业有关的专业课。
180 |
181 | #### 选课
182 |
183 | ---
184 |
185 | 这里建议第一学期默认置课基本没啥必要再动了,以适应为主,选课往往专业课看讲的怎么样,一些选修课看有没有趣,给分好不好。这些东西怎么判断建议询问对应学校学长学姐经验(进校再问肯定不迟,或者这些东西在学校的一些 qq 大群中询问一般都能得到回答)。
186 |
187 | #### 一些时间节点
188 |
189 | ---
190 |
191 | 一般来说规划是大一大二打好专业基础,大二大三有兴趣可以发邮件找导师做点科研,考虑工作实习的话大三也要开始准备了。
192 |
193 | 以上规划也需要具体专业具体分析,比如数学专业本科学到的内容大概是不足以接触一些前沿的东西的,所以~~主要就是学~~。
194 |
195 | 如果想出国考虑学英语的话,视自己英语水平而定,一般来说这些英语考试(托福,雅思,GRE)证书有效期有 2 年,所以往往稳妥打算,在大二的寒假及春季学期可以考虑针对性学习,考试(因为要申请国外学校的话可能大四一开始甚至更早就要着手申请)。平时的话能多积累点词汇,听听听力自然更好。
196 |
197 | ### 学习心态
198 |
199 | ---
200 |
201 | 诗云:
202 |
203 | > 晚上熬夜冲浪,早上不想起床。
204 | > 完全不敢逃课,前排上网很忙。
205 | > 知乎微博豆瓣,B 站白嫖之王。
206 | > 催利老师营业,建议爽子改行。
207 | > 中午想吃什么,西区芳华食堂。
208 | > 回寝开始游戏,作业完全不慌。
209 | > 三点开始午觉,四点上课匆忙。
210 | > 课上继续游戏,输到头昏脑涨。
211 | > 晚上大物实验,做到直接骂娘。
212 | > 回寝先买夜宵,冰粉不加红糖。
213 | > 抹嘴拿出手机,数据扔在一旁。
214 | > ddl 马上截止,自习室装模作样。
215 | > 微积分学习指导,看懂的不过几行。
216 | > 遇题不会求群友,群友个个是卷王。
217 | > 一般方法就不讲,wolfram 它不香?
218 | > 实验报告不会写,祖传张力帮大忙。
219 | > 凑完作业连诉苦,享受网抑云时光。
220 | > 生而为人很抱歉,失去梦想不应当。
221 | > 复习一天练习生,头脑空空上考场。
222 | > 考完快乐把号上,成绩下来涕泗淌。
223 | > 痛心疾首狂饮醉,愧对自己愧爹娘。
224 | > 明日立志当勤学,闻鸡起舞好儿郎。
225 | > 闹钟六点开始响,愣是九点没起床。
226 | > 和大多数人一样,三十没牵过姑娘。
227 | > 隔壁寝室的兄弟,和 npy 得郭奖。
228 | > 游戏水准直线升,英语能力指数降。
229 | > 能力不如田舍郎,幻想能登天子堂。
230 | > 问我所得有几何?肥西路上划水王。
231 | > 故表情包有云:
232 | > 读书人,混子人,看完忘一半是基本。学到一半群聊见,手机三分钟瞄一眼,装模作样读书人!
233 | > 中国科大学生星云诗社 - 刚体小咸鱼 投稿
234 |
235 | 各位大学的学习生活其实理论上来说是相当自由的:不会有班主任天天盯着你学习,甚至上不上课往往都看你个人意愿(~~本人已经好几节近现代史纲要课没去上了~~)。
236 |
237 | 不过出于种种原因,不管是想获得一个好看的成绩,还是周围同学都付出了许多努力,往往会有很多因素会推动你去“应试性的学习”。然而就算获得一个好成绩,也不代表真正掌握了一门课的内容。且如果这种学习不是出于你的本意,那想必你也不会很快乐。本部分内容主要是希望各位能够不要太应试性的学习。
238 |
239 | 出于不再重复造轮子的原则,这里仅提供一篇文章以供拓展阅读:
240 | [上海交通大学生存手册](https://survivesjtu.gitbook.io/survivesjtumanual/)
241 |
--------------------------------------------------------------------------------
/posts/vue3-fastapi.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: vue3-fastapi 简易开发体验
3 | date: 2022-03-01 14:20:02
4 | tags: [vue, fastapi, 异步]
5 | category: web
6 | mathjax: true
7 | tocbot: true
8 | ---
9 |
10 | 温故而知新,本文将借助比较现代化的开发流程(vue-cli, vue3, fastapi)重构之前的一篇简易备忘系统
11 |
12 |
13 |
14 | ## 前端
15 |
16 | 前端借助 Vue CLI 搭建起 Vue 项目,并对原先内容进行迁移
17 |
18 | ```bash
19 | vue create hello-world
20 | ```
21 |
22 | 后即可修改 Vue 项目
23 |
24 | ### Vue
25 |
26 | 在介绍 Vue3 之前,首先需要介绍一下 Vue 框架的基本思想。
27 |
28 | 原先,DOM (html 文档) 中显示的数据和 JS 中的变量并没有绑定关系,因此,每次变量改变(包括从后端获取数据)都需要重新操作 DOM, 更新数据
29 |
30 | Vue 对此进行了简化,这是怎么做到的呢?
31 |
32 | 从逻辑上来说,设 State 是当前所有应用(网页)中所有数据的集合,View 是用户看到的 ui 界面,它们之间应该具备一个单向的函数关系 $View = f(state)$
33 |
34 | Vue 所做的工作即为自动描述了这一函数关系,使得 HTML 文档中显示的元素可以通过 Vue 提供的模板语法 `{{ }}` 与 State 中的变量进行绑定,比如如果我想在页面某处显示脚本中的值 `x`,那 HTML 对应位置直接写 `{{ x }}` 即可。加上 Vue 提供的 `v-if` 和 `v-for` 之类的模板控制流,使得用户可以专注于数据的操作,而无需担心这些数据怎样更新到页面上
35 |
36 | 具体实现上,Vue2 采用 `data、computed、methods、watch` 等组件,被称为响应式 API:
37 |
38 | - data 即为 $State$ 集合,包含了所有该页面需要用到的数据
39 | - methods 是一些方法(函数)的集合,可以用于处理页面点击事件,更新数据等
40 | - computed 为计算属性,可以理解一个语法糖(当然,具体实现上不是语法糖)。如果页面上一个元素的内容 $a$ 依赖数据 $x$, 具体关系为 $a = f(x)$, $f$ 是一个很复杂的函数,直接写模板 `{{ f(x) }}` 既麻烦又表意不明,这个时候可以设置计算属性 $y = f(x)$,即可通过 `{{ y }}` 达到自己想要的效果
41 | - watch 用于自动检测页面上元素的变化,可以在检测到用户的操作之后调用相应的 methods 中的函数
42 |
43 | ### 组合式 API
44 |
45 | Vue2 在功能上已经很完善,但是一个很大的弊端是如果不注意拆分组件(页面),一个组件文件可能会非常长,甚至上千行。试想一个界面内有很多元素,每个元素都有对应的数据和用户操作界面的方法,那么 `data、computed、methods、watch` 中会有页面里不同模块的内容混杂,一方面可读性较差,另外一方面太长的文件也不方便编辑。
46 |
47 | 对此,Vue3 相对于原先的响应式 API,引入了组合式 API,目的就是为了将操作页面中同一模块的 JS 逻辑整合到一起。
48 |
49 | `data、computed、methods、watch` 在这一改变后被统合到了一个 `setup` 组件。
50 |
51 | 先看一下重构之后的备忘系统前端逻辑:
52 |
53 | ```html
54 |
84 | ```
85 |
86 | 再来逐个部分解析。
87 |
88 | #### 前后端通信
89 |
90 | 首先是三个前后端通信的接口,这里先忽略 `async` 只要知道它们能请求数据即可。
91 |
92 | ```html
93 |
119 | ```
120 |
121 | #### 数据绑定
122 |
123 | 接下来讨论一下 $View = f(state)$ 如何实现。
124 |
125 | 主要实现方法是 `ref`,这是为了给指定的数据创建引用。为什么要创建引用以及引用的使用可以参见官方 [组合式 API 文档](https://v3.cn.vuejs.org/api/composition-api.html), 这里注意组合式 API 是兼容原先响应式 API 的, `
152 | ```
153 |
154 | #### 钩子
155 |
156 | 再来看 `onMounted(getNotes);` 这一行。Vue 提供了一些特殊的钩子,`onMounted` 代表组件加载完毕后会执行的语句。这里我们希望组件加载完成后直接加载所有笔记数据。关于钩子,是 Vue 的一个重要特性,本文不在此讨论。
157 |
158 | ```html
159 |
183 | ```
184 |
185 | #### 页面模板
186 |
187 | 至此,前端的逻辑就基本分析完毕。下面考虑页面模板的编写:
188 |
189 | ```html
190 |
204 | ```
205 |
206 | 可以看到 `v-if` 和 `v-for` 的方便之处。
207 |
208 | #### 补充:异步
209 |
210 | 异步名字看起来很高大上,但原理没有这么复杂。本质就是因为浏览器对某个接口的请求可能会耗费较长的时间(比如我们用 JS 下载一个文件,可能需要好几秒),这个期间我们希望 JS 能继续执行。因此,我们需要一个 **回调函数** ,在接口请求完成后继续执行这个回调函数,来完成与后端接口通信结束之后的处理。
211 |
212 | 举例来说,逻辑大致是这样:
213 |
214 | ```javascript
215 | func1 = ...
216 | func2 = ...
217 | func3 = ...
218 | func4 = ...
219 |
220 | func1()
221 |
222 | fuc2(请求的 url 和相关参数,func3)
223 |
224 | func4()
225 | ```
226 |
227 | 实际执行中,func1 执行完后来到了一个请求后端接口的函数 func2,func2 在请求后端数据的同时,JS 的运行并不会阻塞,而是会继续从 func4 往后执行。直到请求完成,才会执行 func3(比如用于处理得到的数据)
228 |
229 | async/await 是对以上的异步过程的简化。这里我们不去阐述 JS 的 Promise 机制,而单从使用上理解:对于一个异步函数(比如调用后端接口),我们可以用 await 来 "等待" 这一函数调用完毕。await 之后的内容起到了与回调函数类似的作用,会在异步函数调用完后再执行。
230 |
231 | 比如以下代码(这并不实际生效,下面再解释)
232 |
233 | ```javascript
234 | function foo() {
235 | const resp = await axios.get(url); // GET 请求的结果会被存在 resp 中
236 | // 接下来可以处理 resp
237 | }
238 | ```
239 |
240 | 但注意的是,`foo` 调用了一个异步函数,所以 `foo` 的执行会消耗较长时间,于是它也变成了一个异步函数。为了标识这一点,我们给 `foo` 注明 `async`
241 |
242 | ```javascript
243 | async function foo() {
244 | const resp = await axios.get(url); // GET 请求的结果会被存在 resp 中
245 | // 接下来可以处理 resp
246 | }
247 | ```
248 |
249 | `foo` 是一个异步函数,所以也可以再写一个异步函数处理 `foo` 返回的数据:
250 |
251 | ```javascript
252 | async function bar() {
253 | const res = await foo();
254 | // 接下来处理 res
255 | }
256 | ```
257 |
258 | 若调用 `bar`,实际会依次执行 `axios`, `foo`, `bar` 中的逻辑,但是我们的代码中并没有出现嵌套,使得异步代码看起来与同步代码类似,很清爽
259 |
260 | ## 后端
261 |
262 | 本文采用 uvicorn + fastapi 在服务器上部署。部署具体可以参考 fastapi 文档。
263 |
264 | ```bash
265 | pip install fastapi
266 | pip install uvicorn[standard]
267 | vim main.py
268 | uvicorn main:app --host 0.0.0.0 --port 80 # for example
269 | ```
270 |
271 | fastapi 官方文档的说明非常清楚,直接贴代码:
272 |
273 | ```python
274 | from fastapi import FastAPI
275 | from fastapi.middleware.cors import CORSMiddleware
276 |
277 | from pydantic import BaseModel
278 |
279 |
280 | class Note(BaseModel):
281 | content: str
282 | create_time: str
283 |
284 |
285 | app = FastAPI()
286 | app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"],
287 | allow_headers=["*"],)
288 |
289 | notes = []
290 |
291 |
292 | @app.get("/notes")
293 | def read_notes():
294 | return notes
295 |
296 |
297 | @app.post("/notes")
298 | def append_note(note: Note):
299 | notes.append(note)
300 | return notes[-1]
301 |
302 |
303 | @app.delete("/notes/{id}")
304 | def delete_note(id: int):
305 | return notes.pop(id)
306 |
307 | ```
308 |
309 | 这一行是用来设置跨域
310 |
311 | ```python
312 | app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"],
313 | allow_headers=["*"],)
314 | ```
315 |
316 | `@app.get("/notes")` 代表使用 `GET` 访问 `/notes` 接口时会调用的函数。这里介绍几个常见的访问接口的方式:
317 |
318 | - GET: 获取信息
319 | - POST: 添加信息
320 | - PUT: 添加/更新信息,需要保证调用 n 次和调用 1 次的结果相同,因此常用于更新数据
321 | - DELETE:删除数据
322 |
323 | ## 画饼
324 |
325 | 啥时候用 Vue3 + fastapi 把自己博客重构一遍()
326 |
--------------------------------------------------------------------------------