SnowKagura‘s blog - 代码
type: channel
link: https://www.snowkagura.com/tag/%E4%BB%A3%E7%A0%81/
items: 数据结构——树状数组小结
items: 函数调用实现原理小结+RISC-V架构学习笔记
items: 浅记编译流程
items: 操作系统学习笔记（4）—— 操作系统的特征
items: 操作系统学习笔记（2）——系统调用接口
items: 操作系统学习笔记（1）——什么是操作系统
items: 从零开始搭建react开发环境（进阶篇）
items: 从零开始构建react+typescript开发环境
items: 如何高效地编写代码

数据结构——树状数组小结
type: item
date: 2022-05-26T16:37:00+08:00
description: 适用场景树状数组是一种适用于多次单点修改统计区间和问题的数据结构。基本思想Binary Indexed Tree 求和的基本思想在于，给定需要求和的位置 i ，例如 13 ，我们可以利用其二进制表示法来进行分段（或者说分层）求和：13 = 2^3 + 2^2 + 2^0，则prefixSum(13) = RANGE(1, 8) + RANGE(9, 12) + RANGE(13, 13) （注意此处的RANGE(x, y)表示数组中第x个位置到第y个位置的所有数字求和）。如下面例子中所示：arr = [1, 7, 3, 0, 5, 8, 3, 2, 6, 2, 1, 1, 4, 5] prefixSum(13) = RANGE(1, 8) + RANGE(9, 12) + RANGE(13, 13) = 29 + 10 + 4 = 43那么只要我们提前计算好这些RANGE(x,y)，便可以对相应的序列进行快速求和。下面是构造树状数组的方法（填坑法）：图中最上面是原数组，中间代表了树状数组各元素的意义，即RANGE(x,y)，而最下面便是原数组对应的树状数组。利用树状数组求区间和利用树状数组，我们可以发现：prefixSum(13) = prefixSum(0b00001101) = BIT[13] + BIT[12] + BIT[8] = BIT[0b00001101] + BIT[0b00001100] + BIT[0b00001000]其中BIT即树状数组，而从二进制位我们可以发现一个很有意思的结论，即我们实际上是在将下标13每次减少一个最低位的1所在位对应的数字，直到小于1为止，将这些下标对应的元素累加起来便是RANGE(1,13)的和。总结起来就像这样：这个结构可以抽象成一颗树一样：寻找父节点的操作便是lowbit ，也即上述所发现的结论，假设当前节点下标为 x ，则 lowbit(x) = x & -x。这样，我们就可以通过树状数组快速求区间和了。更新数组中的元素而关于如何进行单点修改，我们以update(5,2)为例：我们可以发现，单点修改的过程恰好与求区间和的过程相反，在此处，我们需要从5开始，对5进行lowbit的增加，直至超出数组范围，对所经过的下标对应的元素均进行累加，即可完成一次单点修改。树状数组的建立第一种方法很简单，遍历数组中的每一个元素，对其使用update(i + 1, elem)即可，这是一种O(nlog n)时间复杂度的构造方法第二种方法是一种时间复杂度O(n)的方法，其步骤如下(数组下标从0开始)：给定一个长度为n的输入数组list。初始化长度为n + 1的Binary Indexed Tree数组bit，并将list中的数字对应地放在bit[1]到bit[n]的各个位置。对于1到n的每一个i，进行如下操作：令j = i + (i & -i)，若j < n + 1，则bit[j] = bit[j] + bit[i]代码实现class BinaryIndexTree { private: int *tr; inline int lowbit(int x) { return x & -x; } public: BinaryIndexTree(int arr[], int n) { tr = new int[n + 1]; for (int i = 0; i < n; ++i) tr[i + 1] = arr[i]; for (int i = 1; i < n + 1; ++i) { int j = lowbit(i); if (j < n + 1) tr[j] += tr[i]; } } void add(int x, int v) { for(int i = x; i < n + 1; i += lowbit(i)) tr[i] += v; } int query(int x) { int ans = 0; for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i]; return ans; } }参考文章树状数组（Binary Indexed Tree），看这一篇就够了

函数调用实现原理小结+RISC-V架构学习笔记
type: item
date: 2022-04-07T00:55:00+08:00
description: 从汇编语言的视角来看在函数调用的时候，需要有一条指令跳转到被调用函数的位置，这个看起来和其他控制结构没什么不同；但是在被调用函数返回的时候，我们却需要返回那条跳转过来的指令的下一条继续执行。这次用来返回的跳转究竟跳转到何处，在对应的函数调用发生之前是不知道的。比如，我们在两个不同的地方调用同一个函数，显然函数返回之后会回到不同的地址。这是一个很大的不同：其他控制流都只需要跳转到一个 编译期固定下来 的地址，而函数调用的返回跳转是跳转到一个 运行时确定 （确切地说是在函数调用发生的时候）的地址。对此，指令集必须给用于函数调用的跳转指令一些额外的能力，而不只是单纯的跳转。对于8086汇编来说，有一条伪指令符合该特征，也就是 call 伪指令，它会使得 sp 寄存器 减少对应的字节数，然后将当前的 IP 保存到栈顶（在执行当前指令之前，IP 就已经指向下一条指令了），然后使得 IP 指向被调用的函数地址。而 ret 伪指令则是该过程的逆过程，即将保存在栈内的返回地址出栈放入 IP 内。而在 RISC-V 架构上，有两条指令即符合这样的特征：指令指令功能$jal\ rd,\ imm[20:1]$$rd\leftarrow pc+4$$pc\leftarrow pc+imm$$jalr\ rd,\ (imm[11:0])rs$$rd\leftarrow pc+4$$pc\leftarrow rs+imm$[scode type="blue" size=""]RISC-V 指令各部分含义在大多数只与通用寄存器打交道的指令中， rs 表示 源寄存器 (Source Register)， imm 表示 立即数 (Immediate)，是一个常数，二者构成了指令的输入部分；而 rd 表示 目标寄存器 (Destination Register)，它是指令的输出部分。rs 和 rd 可以在 32 个通用寄存器 x0~x31 中选取。但是这三个部分都不是必须的，某些指令只有一种输入类型，另一些指令则没有输出部分。[/scode]从中可以看出，这两条指令在设置 pc 寄存器完成跳转功能之前，还将当前跳转指令的下一条指令地址保存在 rd 寄存器中，即 $rd\leftarrow pc+4$ 这条指令的含义。（这里假设所有指令的长度均为 4 字节）在 RISC-V 架构中，通常使用 ra 寄存器（即 x1 寄存器）作为其中的 rd 对应的具体寄存器，因此在函数返回的时候，只需跳转回 ra 所保存的地址即可。事实上在函数返回的时候我们常常使用一条 伪指令 (Pseudo Instruction) 跳转回调用之前的位置： ret 。它会被汇编器翻译为 jalr x0, 0(x1)，含义为跳转到寄存器 ra 保存的物理地址，由于 x0 是一个恒为 0 的寄存器，在 rd 中保存这一步被省略。总结一下，在进行函数调用的时候，我们通过 jalr 指令保存返回地址并实现跳转；而在函数即将返回的时候，则通过 ret 伪指令回到跳转之前的下一条指令继续执行。这样，RISC-V 的这两条指令就实现了函数调用流程的核心机制。可以发现，汇编指令在实现函数调用流程的核心机制大差不差，其思想都是在调用函数前保存返回地址，在函数调用结束时，恢复到之前的地址。在栈内保存函数地址还好办，但是像 RISC-V 架构这样，指令将地址保存在 ra 寄存器中，我们就得保证 ra 寄存器在函数调用过程中不被篡改了，单次调用还好说，但是在现实应用中，我们离不开函数 多层嵌套调用 的情形。我们很容易想象，如果函数不支持嵌套调用，那么编程将会变得多么复杂。如果我们试图在一个函数 f 中调用一个子函数，在跳转到子函数 g 的同时，ra 会被覆盖成这条跳转指令的下一条的地址，而 ra 之前所保存的函数 f 的返回地址将会 永久丢失 。因此，若想正确实现嵌套函数调用的控制流，我们必须通过某种方式保证：在一个函数调用子函数的前后，ra 寄存器的值不能发生变化。但实际上，这并不仅仅局限于 ra 一个寄存器，而是作用于所有的通用寄存器。这是因为，编译器是独立编译每个函数的，因此一个函数并不能知道它所调用的子函数修改了哪些寄存器。而站在一个函数的视角，在调用子函数的过程中某些寄存器的值被覆盖的确会对它接下来的执行产生影响。因此这是必要的。我们将由于函数调用，在控制流转移前后需要保持不变的寄存器集合称之为 函数调用上下文 (Function Call Context) 。由于每个 CPU 只有一套寄存器，我们若想在子函数调用前后保持函数调用上下文不变，就需要物理内存的帮助。确切的说，在调用子函数之前，我们需要在物理内存中的一个区域 保存 (Save) 函数调用上下文中的寄存器；而在函数执行完毕后，我们会从内存中同样的…

浅记编译流程
type: item
date: 2022-04-01T20:22:00+08:00
description: 最近看了操作系统相关的知识，结合上《程序员的自我修养》这本书所讲的链接装载与库，感觉对于一个程序从编译，到汇编转换成机器码，再到多目标文件与库的链接，是一个非常复杂的过程。所以想要浅显易懂的总结一下，也刚好是找到了一个算是浅显易懂的总结，所以在自己的理解上再进行了一些润色。编译流程从源代码得到可执行文件的编译流程可被细化为多个阶段（虽然输入一条命令便可将它们全部完成）：编译器 (Compiler) 将每个源文件从某种高级编程语言转化为汇编语言，注意此时源文件仍然是一个 ASCII 或其他编码的文本文件；汇编器 (Assembler) 将上一步的每个源文件中的文本格式的汇编指令转化为机器码，得到一个二进制的 目标文件 (Object File)；链接器 (Linker) 将上一步得到的所有目标文件以及一些可能的外部目标文件链接在一起形成一个完整的可执行文件。汇编器输出的每个目标文件都有一个独立的程序内存布局，它描述了目标文件内各段所在的位置。而链接器所做的事情是将所有输入的目标文件整合成一个整体的内存布局。在此期间链接器主要完成两件事情：第一件事情是将来自不同目标文件的段在目标内存布局中重新排布。如下图所示，在链接过程中，分别来自于目标文件 1.o 和 2.o 段被按照段的功能进行分类，相同功能的段被排在一起放在拼装后的目标文件 output.o 中。注意到，目标文件 1.o 和 2.o 的内存布局是存在冲突的，同一个地址在不同的内存布局中存放不同的内容。而在合并后的内存布局中，这些冲突被消除。第二件事情是将符号替换为具体地址。这里的符号指什么呢？我们知道，在我们进行模块化编程的时候，每个模块都会提供一些向其他模块公开的全局变量、函数等供其他模块访问，也会访问其他模块向它公开的内容。要访问一个变量或者调用一个函数，在源代码级别我们只需知道它们的名字即可，这些名字被我们称为符号。取决于符号来自于模块内部还是其他模块，我们还可以进一步将符号分成内部符号和外部符号。然而，在机器码级别（也即在目标文件或可执行文件中）我们并不是通过符号来找到索引我们想要访问的变量或函数，而是直接通过变量或函数的地址。例如，如果想调用一个函数，那么在指令的机器码中我们可以找到函数入口的绝对地址或者相对于当前 PC 的相对地址。[scode type="blue" size=""]PC (Program Counter) 程序计数器，用来存放当前欲执行指令的地址。可自动形成指向下一条指令的地址。一般来讲，这其实是一种行为的代称而不是真实存在的寄存器，在 x86 体系，一般是由 CS:IP （段地址：偏移地址）的组合来构成这个逻辑，而在 ARM 体系中 R15 就是 PC，当然 ARM 和 IA-32、x64 都支持高级内存管理，所以 PC 的内容未必是当前指令在内存中的绝对位置。而且所谓的每次增加1，其实是逻辑上的增加1，并不代表内存地址的增量为1，这受到指令长度的影响。[/scode]那么，符号何时被替换为具体地址呢？因为符号对应的变量或函数都是放在某个段里面的固定位置（如 全局变量往往放在 .bss 或者 .data 段中，而函数则放在 .text 段中），所以我们需要等待符号所在的段确定了它们在内存布局中的位置之后才能知道它们确切的地址。当一个模块被转化为目标文件之后，它的内部符号就已经在目标文件中被转化为具体的地址了，因为目标文件给出了模块的内存布局，也就意味着模块内的各个段的位置已经被确定了。然而，此时模块所用到的外部符号的地址无法确定。我们需要将这些外部符号记录下来，放在目标文件一个名为符号表（Symbol table）的区域内(对于ELF文件一般在.symtab)。由于后续可能还需要重定位，内部符号也同样需要被记录在符号表中。外部符号需要等到链接的时候才能被转化为具体地址。假设模块 1 用到了模块 2 提供的内容，当两个模块的目标文件链接到一起的时候，它们的内存布局会被合并，也就意味着两个模块的各个段的位置均被确定下来。此时，模块 1 用到的来自模块 2 的外部符号可以被转化为具体地址。同时我们还需要注意：两个模块的段在合并后的内存布局中被重新排布，其最终的位置有可能和它们在模块自身的局部内存布局中的位置相比已经发生了变化。因此，每个模块的内部符号的地址也有可能会发生变化，我们也需要进行修正。上面的过程被称为重定位（Relocation），这个过程形象一些来说很像拼图：由于模块 1 用到了模块 2 的内容，因此二者分别相当于一块凹进和凸出一部分的拼图，正因如此我们可以将它们无缝地拼接到一起。

操作系统学习笔记（4）—— 操作系统的特征
type: item
date: 2022-03-24T21:14:00+08:00
description: 操作系统的特征从总体上看，操作系统具有五个方面的特征：虚拟化 (Virtualization)、并发性 (Concurrency)、异步性、共享性和持久性 (Persistency)。操作系统的虚拟化可以理解为它对内存、CPU 的抽象和处理；并发性和共享性可以理解为操作系统支持多个应用程序“同时”运行；异步性可以从操作系统调度、中断处理对应用程序执行造成的影响等几个方面来理解；持久性则可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。虚拟性内存虚拟化程序员在写应用程序的时候，不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置，而是用字符串符号定义了各种变量和函数，直接在代码中便捷地使用这些符号就行了。是由于操作系统建立了一个 地址固定 ， 空间巨大 的虚拟内存给应用程序来运行，这是 内存虚拟化 ，其实是一种 “空间虚拟化” ， 可进一步细分为 内存地址虚拟化 和 内存大小虚拟化 。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么？程序员不用关心。为什么？因为编译器会自动帮我们把这些符号翻译成地址，形成可执行程序。程序使用的内存是否占得太大了？在一般情况下，程序员也不用关心。[scode type="blue" size=""]实际上，编译器 (Compiler，比如 gcc) 和链接器 (linker，比如 ld) 也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以，编译器的一个简单处理办法就是，设定一个固定地址（比如 0x10000）作为起始地址开始存放代码，代码之后是数据，所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。这里的变量指的是全局变量，其地址在编译链接后会确定不变。但局部变量是放在堆栈中的，会随着堆栈大小的动态变化而变化。这里编译器产生的地址就是虚拟地址。这里，编译器和链接器图省事，找了一个适合它们的解决办法。当程序要运行的时候，这个符号所对应的虚拟内存地址到计算机的物理内存地址的映射必须要解决了，于是这个任务便推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到空闲的物理内存中，并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的，这导致虚拟地址到物理地址的映射关系也是动态变化的，需要操作系统来维护好可变的映射关系，确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系，才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址，且编译器只需确定一个固定地址作为程序的起始地址就可以不用考虑将来这个程序要在哪里运行的问题，从而实现了 内存地址虚拟化 。在这里，虚拟地址到物理地址的转换关系由操作系统与 MMU(Memory Management Unit) 进行交互从而处理映射关系。[/scode]应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存，但由于在某些情况下，物理内存的空闲空间可能不多了，这时操作系统通过把物理内存中最近没使用的空间（不是空闲的，只是最近用得少）换出（就是“挪地”）到硬盘上暂时缓存起来，这样空闲空间就大了，就可以满足应用程序的运行时内存需求了，从而实现了 内存大小虚拟化 。[scode type="green" size=""]这里其实就涉及到了内存页的概念，通过将内存按页面大小的映射，从而实现了方便的装载与卸载，等到需要使用时再重新建立映射关系[/scode]CPU虚拟化不同的应用程序可以在内存中并发运行，相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了 CPU 在运行，这是 “CPU虚拟化” ，也是一种 “时间虚拟化” 。操作系统给了运行的应用程序一个幻象，即操作系统把时间分成小段，每个应用程序占用其中一小段时间片运行，用完这一时间片后，操作系统会切换到另外一个应用程序，让它运行。由于时间片很短，操作系统的切换开销也很小，应用程序或使用应用程序的用户基本上是看不出的，反而感觉到多个程序各自在独立“并行”执行，从而实现了 CPU虚拟化 。[scode type="yellow" size=""]并行 (Parallel) 是指两个或者多个事件在同一时刻发生；并发 (Concurrent) 是指两个或多个事件在同一时间间隔内发生。对于基于单 CPU 的计算机而言，各个“同时”运行的程序其实是串行分时复用一个 CPU ，任一个时刻点上只有一个程序在 CPU 上运行。 这些虚拟性的特征给应用程序的开发和执行提供了非常方便的执行环境，但也给操作系统的设计与实现提出了很多挑战。[/scode]并发性操作系统为了能够让 CPU 充…

操作系统学习笔记（2）——系统调用接口
type: item
date: 2022-03-19T19:18:00+08:00
description: 站在用户和应用程序的角度看操作系统操作系统内核是一个需要提供各种服务的软件，其服务对象是应用程序，而用户（这里可以理解为一般使用计算机的人）是通过应用程序的服务间接获得操作系统的服务的，因此操作系统内核藏在一般用户看不到的地方。但应用程序需要访问操作系统获得操作系统的服务，这就需要通过操作系统的接口才能完成。操作系统的接口形式就是应用程序二进制接口 (ABI, Application Binary Interface)。操作系统不能只提供面向单一编程语言的函数库的编程接口 (API, Application Programming Interface) ，它的接口需要考虑对基于各种编程语言的应用支持，以及访问安全等因素，使得应用软件不能像访问函数库一样的直接访问操作系统内部函数，更不能直接读写操作系统内部的地址空间。为此，操作系统设计了一套安全可靠的接口，我们称为系统调用接口 (System Call Interface)。系统调用接口通常面向应用程序提供了API的描述，但在具体实现上，还需要提供ABI的接口描述规范。[scode type="blue"]一些思考：提供ABI规范是便于底层开发人员进行驱动程序的开发，以及进行汇编级别的优化和高级编程语言运行库的开发等操作[/scode]在现代处理器的安全支持（特权级隔离，内存空间隔离等）下，应用程序就不能直接以函数调用的方式访问操作系统的函数，以及直接读写操作系统的数据变量。[scode type="red"]这里的不能直接以函数调用的方式访问操作系统的函数，我认为指的是操作系统内部使用的函数而非暴露出来的系统调用接口[/scode]不同类型的应用程序可以通过符合操作系统规定的ABI规范的系统调用接口，发出系统调用请求，来获得操作系统的服务。操作系统提供完服务后，返回应用程序继续执行。[scode type="share"]API 与 ABI 的区别应用程序二进制接口 ABI 是不同二进制代码片段的连接纽带。ABI 定义了二进制机器代码级别的规则，主要包括基本数据类型、通用寄存器的使用、参数的传递规则、以及堆栈的使用等等。ABI 与处理器和内存地址等硬件架构相关，是用来约束链接器 (Linker) 和汇编器 (Assembler) 的。在同一处理器下，基于不同高级语言编写的应用程序、库和操作系统，如果遵循同样的 ABI 定义，那么它们就能正确链接和执行。应用程序编程接口 API 是不同源代码片段的连接纽带。API 定义了一个源码级（如 C 语言）函数的参数，参数的类型，函数的返回值等。因此 API 是用来约束编译器 (Compiler) 的：一个 API 是给编译器的一些指令，它规定了源代码可以做以及不可以做哪些事。API 与编程语言相关，如 libc 是基于 C 语言编写的标准库，那么基于 C 的应用程序就可以通过编译器建立与 libc 的联系，并能在运行中正确访问 libc 中的函数。简单来说，API是编译器级别的，ABI是汇编级别的。[/scode]系统调用接口的一些比较重要的功能与相应的抽象对于系统调用接口提供的功能，下面列出了一些值得了解的功能：一个运行的程序可以创建另一个程序的实例吗？需要等待另外一个程序执行完成吗？操作系统能停止或恢复另一个正在运行的程序吗？一个运行的程序如何与连接到计算机的设备通信并通过它们与物理世界通信？多个运行的程序如何同步互斥地对共享资源进行访问？一个运行的程序可以要求更多（或更少）的内存空间吗？一个运行的程序如何持久地存储用户数据？一个运行的程序如何能输出字符信息？如何能获得输入字符信息？下面列出了一些相对比较重要的操作系统接口或抽象：进程（即程序运行过程）管理：复制创建进程 fork 、退出进程 exit 、执行进程 exec 等。线程管理：线程（即程序的一个执行流）的创建、执行、调度切换等。线程同步互斥的并发控制：互斥锁 mutex 、信号量 semaphore 、管程 monitor 、条件变量 condition variable 等。进程间通信：管道 pipe 、信号 signal 、事件 event 等。虚存管理：内存空间映射 mmap 、改变数据段地址空间大小 sbrk 、共享内存 shm 等。文件I/O操作：对存储设备中的文件进行读 read 、写 write 、打开 open 、关闭 close 等操作。外设I/O操作：外设包括键盘、显示器、串口、磁盘、时钟 … 但接口均采用了文件 I/O 操作的通用系统调用接口。[scode type="share"]上述表述在某种程度上说明了操作系统对计算机硬件重要组成的抽象和虚拟化，使得应用程序只需基于对简单的抽象概念的访问来达到对计算机系统资源的使用：文件 (File) 是外设的…

操作系统学习笔记（1）——什么是操作系统
type: item
date: 2022-03-19T16:36:00+08:00
description: 前言最近找到一个好文档，关于如何用Rust编写一个操作系统，里面系统的讲述了操作系统的相关知识和源代码等等，所以就打算跟着这个文档开始学习操作系统相关的知识(之后就可以去读CSAPP了从认识来看一个操作系统（OS）是一个软件，它帮助用户和应用程序使用和管理计算机的资源。操作系统可能对最终用户不可见，但控制着嵌入式设备、更通用的系统（如智能手机、台式计算机和服务器）以及巨型机等各种计算机系统。从功能开始操作系统主要做的事情就只有两件：一是向下管理计算机硬件和各种外设，二是向上给应用软件提供各种服务帮助。进一步描述操作系统是一个可以管理CPU、内存和各种外设，并管理和服务应用软件的系统软件。[scode type="share"]系统软件是在一个计算机系统范围内使用的软件，可涉及到整个计算机系统。文本编辑器这样的软件只涉及到对文本文件的编辑，并不能被叫做系统软件。而在计算机中安装的 C 标准库 libc（类似的有 Rust 标准库 等）可以算是一个，因为它提供了与OS交互的系统调用接口，其功能覆盖了整个计算机系统，会被许多不同的软件访问。[/scode]为了完成这些工作，操作系统需要知道如何与硬件打交道，如何更好地给应用软件提供服务，这就有一系列操作系统相关的理论、抽象、设计等来支持如何做和做得好的需求。[scode type="blue"]在不同的应用场景下，操作系统的边界也是不同的，我们可以把运行时库、图形界面支持库等这些可支持不同应用的系统软件 (System Software) 也看成是操作系统的一部分。比如说嵌入式环境，一般操作系统以运行时库的形式与应用程序紧密结合在一起，形成一个可在嵌入式硬件上单一执行的嵌入式应用。而 Android 应用运行在 ARM 处理器上 Android 操作系统的执行环境中。[/scode]简化描述站在应用程序的角度来看，我们可以发现常见的应用程序其实是运行在由硬件、操作系统内核、运行时库、图形界面支持库等所包起来的一个 执行环境 (Execution Environment) 中，应用程序只需根据与系统软件约定好的应用程序二进制接口 (ABI, Application Binary Interface) 来请求执行环境提供的各种服务或功能，从而完成应用程序自己的功能。于是我们可以将操作系统的定义简化成：应用程序的软件执行环境。一直在变化的定义操作系统的内涵和外延随着历史的发展也一直在变化，并没有类似于“1+1=2”这样的明确定义。如今在二十一世纪初期的大众眼中，操作系统就是他们的手机/终端上的软件系统，包括各种应用程序集合，图形界面和网络浏览器是其中重要的组成部分。但无论操作系统的内在实现和具体目标如何变化，其管理计算机硬件，给应用提供服务的核心定位没有变化。

从零开始搭建react开发环境（进阶篇）
type: item
date: 2022-02-14T18:05:00+08:00
description: [scode type="share"]上篇文章写的有些简略，而且有些地方在实际开发中还是有一定问题存在的，所以就有了这个进阶篇，这篇文章也是因为最近在搞前端开发，所以顺便总结一下[/scode]大纲首先我先说明一下这次使用的技术栈：less css预处理器，知名的还有sass、scss、stylus等react 主要的开发依赖库typescript 提供静态类型等诸多好处webpack 打包工具webpack-dev-server 开发用服务器react-router 路由管理库axios 网络请求库babel es新特性等代码的polyfill以这些作为主要的开发工具和生产依赖，除此之外还有一些关于webpack打包的插件，babel的插件之类的都会在后续进行一一说明步骤首先，我们从新建文件夹开始mkdir my-app \ && cd my-app \ && npm init -y然后我们就开始装依赖了npm install react react-dom react-router-dom axios --save-exact以上是几个生产环境所需要的依赖，所以放在一起进行安装，这里的--save-exact是为了把下载下来的依赖库版本固定，从而避免不同人开发时因为版本更新导致的诸多问题现在我们开始安装开发方面的依赖库，这里要使用一个参数--save-dev，说明这些依赖只在开发时使用npm install @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react @babel/runtime @babel/preset-typescript --save-dev \ && npm install webpack webpack-cli webpack-dev-middleware webpack-dev-server webpack-merge --save-dev \ && npm install typescript @types/react @types/react-dom @types/webpack @types/node ts-node --save-dev \ && npm install less less-loader css-loader babel-loader style-loader --save-dev \ && npm install clean-webpack-plugin css-minimizer-webpack-plugin fork-ts-checker-webpack-plugin html-webpack-plugin mini-css-extract-plugin --save-dev这里可以看到，有非常多的开发方面的依赖，在这里我一行一行的进行解释首先第一行，我们可以看到都是关于babel方面的一些库，下面是它们的作用@babel/core babel的核心库@babel/plugin-transform-runtime 和 @babel/runtime 提供对async/await功能的支持@babel/preset-env 提供对ES规范新功能的支持@babel/preset-react 提供对react相关代码规范的支持@babel/preset-typescript 提供对typescript代码的支持babel主要的作用就是为了提供对一些浏览器不支持的语法进行降级转换的作用，使得其可以在浏览器运行然后是第二行，关于webpack的一些功能模块webpack 和 webpack-cli 一个是核心库，一个是命令行支持webpack-dev-middleware 和 webpack-dev-server 对开发环境提供热替换开发服务器功能webpack-merge 用于对配置文件的merge，方便进行配置文件的区分管理（ts原生也可以合并，但是这个比较智能）第三行，关于typescript方面的一些模块声明文件还有typescript自身，这里就不做详细解释了第四行，提供了less支持和一些webpack读取相关文件的相应loader第五行，关于webpack的一些插件clean-webpack-plugin 在打包生成文件时会对之前的文件进行相应的清理操作，主要是为了修复webpack-dev-middleware在启用writeToDisk功能时，webpack原生配置启用自动清理却无法清理的bugcss-minimizer-webpack-plugin 对打包生成的css进行最小化压缩处理fork-ts-checker-webpack-plugin 提供在…

从零开始构建react+typescript开发环境
type: item
date: 2022-02-03T15:55:00+08:00
description: ps：很久没写博客，最近学了一些前端，觉得还是需要沉淀一些东西，于是便有了从零开始构建react开发环境的这篇文章开始初始化项目新建一个my-app文件夹mkdir my-app进入my-app文件夹cd my-app然后在my-app目录下创建src文件夹用来存放React代码mkdir src使用npm初始化项目npm init -y 此时的项目架构如下：安装typescript和react安装typescript全局安装是为了使用tsc命令，开发安装是必须的npm install -g typescript # 全局安装 npm install typescript --save-dev[scode type="blue"]注意：在这里简单介绍一下npm install命令的参数 --save-dev 和 --save的区别，一般来说使用--save-dev参数安装的npm包在最终打包的时候不会被包括到源码里去，所以类似babel和webpack这种进行项目工程构建或者代码编译的库应该用--save-dev来安装，而--save则是安装代码运行必须的库，比如react等。[/scode]配置typescript安装好之后输入tsc --init打开 tsconfig.json，进行以下配置。这将告诉 TypeScript 在哪找到我们的代码。{ "compilerOptions": { "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "lib": ["es5", "es6", "dom"], /* Specify library files to be included in the compilation. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "sourceMap": true, /* Generates corresponding '.map' file. */ "outDir": "./dist/", /* Redirect output structure to the directory. */ "removeComments": true, /* Do not emit comments to output. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "preserveConstEnums": true }, "include": [ "./src/**/*" ] }安装react和react的typescript支持npm install react react-dom --save && npm install @types/react --save-dev && npm install @types/react-dom --save-dev安装webpack在终端输入以下指令npm install webpack --save-dev && npm install webpack-cli --save-dev && npm install webpack-dev-s…

如何高效地编写代码
type: item
date: 2021-07-22T15:49:47+08:00
description: 最近在看js的教程，然后看到里面所讲述的关于提升代码质量的文字，深受所启发，现将之摘抄下来便于回忆：[scode type="green"]很棒的原则（如雷贯耳，听得我想remake）：“如果代码不够清晰以至于需要一个注释，那么或许它应该被重写。”[/scode]关于提高代码质量的两个配方，分别是分解函数和创建函数好的注释应该在什么地方描述架构对组件进行高层次的整体概括，它们如何相互作用、各种情况下的控制流程是什么样的……简而言之 —— 代码的鸟瞰图。记录函数的参数和用法例如：/** * 返回 x 的 n 次幂的值。 * * @param {number} x 要改变的值。 * @param {number} n 幂数，必须是一个自然数。 * @return {number} x 的 n 次幂的值。 */ function pow(x, n) { ... }这种注释可以帮助我们理解函数的目的，并且不需要研究其内部的实现代码，就可以直接正确地使用它。为什么任务以这种方式解决？写了什么代码很重要。但是为什么 不 那样写可能对于理解正在发生什么更重要。为什么任务是通过这种方式解决的？代码并没有给出答案。如果有很多种方法都可以解决这个问题，为什么偏偏是这一种？尤其当它不是最显而易见的那一种的时候。ps:我认为这点是最关键的，当实现一个逻辑的时候，要写下为什么这样写。因为这次解决问题你应该基本上考虑到了所有的情况，而当下一次来review的时候如果这里没写注释，就很有可能理所当然的想到某种更加简单的解决方案，然而这种解决方案很有可能是上次就已经想到的但具有缺陷的解决方法，从而当自己意识到这个解决方案有缺陷的时候，又要重新写回之前的样子，浪费了大量的时间。代码有哪些巧妙的特性？它们被用在了什么地方？如果代码存在任何巧妙和不显而易见的方法，那绝对需要注释。避免注释描述“代码如何工作”和“代码做了什么”。避免在代码已经足够简单或代码有很好的自描述性而不需要注释的情况下，还写些没必要的注释。当然现实总不会那样的完美，例如在一些复杂的算法中，会有一些出于优化的目的而做的一些巧妙的“调整”。但是通常情况下，我们应该尽可能地保持代码的简单和“自我描述”性。记录有感：看完注释的艺术，感觉对自身是一个极大的提升，以后还是要经常学习编程方面的艺术啊！