<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Snofly</title><description>Blog Site</description><link>https://0110wdj.github.io/</link><language>en</language><item><title>多米诺骨牌分治法求最大合</title><link>https://0110wdj.github.io/snofly-blog/posts/algo/dominoes-divide/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/algo/dominoes-divide/</guid><pubDate>Tue, 14 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;去年遇到的一道题目，不过当时还不能完全解决。经过这段时间对算法的理解，再来试试整个解题过程。&lt;/p&gt;
&lt;h1&gt;正文&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./question.jpg&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;用分治法求解问题，标准步骤为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分解&lt;/li&gt;
&lt;li&gt;解决&lt;/li&gt;
&lt;li&gt;合并&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;显然，问题规模 n 小于 2 时，问题无意义。&lt;/p&gt;
&lt;p&gt;问题规模 n 等于 2 时为最小子问题，可以简单求解。（解决）&lt;/p&gt;
&lt;p&gt;对 n 大于 2 的问题，可以分解为规模为 2 的问题进行求解。（分解）&lt;/p&gt;
&lt;p&gt;对于两个子问题的解进行合并，这是一个重点考虑的事情。（合并）&lt;/p&gt;
&lt;p&gt;对于合并步骤，我们假设有个函数 merge 来实现，具体实现可以先忽略。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function maxSumOfProducts(L, R) {
  function helper(L, R, left, right) {
    if (left === right) {
      // 只有一块骨牌的情况
      return { noRotate: 0, rotate: 0 }; // { 不旋转的最大值, 旋转的最大值 }
    }

    const mid = Math.floor((left + right) / 2);

    // 递归处理左右两部分
    const leftResult = helper(L, R, left, mid);
    const rightResult = helper(L, R, mid + 1, right);

    // 左右两部分不旋转和旋转的结果
    const maxLeftNoRotate = leftResult.noRotate;
    const maxLeftRotate = leftResult.rotate;
    const maxRightNoRotate = rightResult.noRotate;
    const maxRightRotate = rightResult.rotate;

    // 合并结果，计算跨越边界的最大值
    const maxNoRotate = Math.max(
      maxLeftNoRotate + R[mid] * L[mid + 1], // 左不旋转，右不旋转
      maxLeftRotate + L[mid] * L[mid + 1] // 左旋转，右不旋转
    );
    const maxRotate = Math.max(
      maxLeftNoRotate + R[mid] * R[mid + 1], // 左不旋转，右旋转
      maxLeftRotate + L[mid] * R[mid + 1] // 左旋转，右旋转
    );

    return {
      noRotate: maxNoRotate + maxRightNoRotate,
      rotate: maxRotate + maxRightRotate,
    };
  }

  const n = L.length;
  const result = helper(L, R, 0, n - 1);
  return Math.max(result.noRotate, result.rotate);
}

// 示例输入
const L = [5, 8, 4, 9, 3, 11];
const R = [8, 4, 2, 6, 9, 10];

const result = maxSumOfProducts(L, R);
console.log(&quot;最大乘积和:&quot;, result);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>社区提问注意事项</title><link>https://0110wdj.github.io/snofly-blog/posts/common/how-to-ask/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/common/how-to-ask/</guid><pubDate>Sat, 01 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;译者述&lt;/h2&gt;
&lt;p&gt;在技术领域，常用的检索信息的方式有（按照笔者的使用频率排序）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索引擎&lt;/li&gt;
&lt;li&gt;AI 问答&lt;/li&gt;
&lt;li&gt;社区提问&lt;/li&gt;
&lt;li&gt;查阅文献&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多模态的 ai 对话模型真的令人惊艳，而传统搜索引擎则显得呆板和不智能。&lt;/p&gt;
&lt;p&gt;值得注意的是，这两种工具的侧重点是不同的。&lt;/p&gt;
&lt;p&gt;搜索引擎的优势在于关键字的检索，往往一两个词语，就能得到用户想要的结果。&lt;/p&gt;
&lt;p&gt;ai 模型的优势在于，对完善的 prompt 能有非常出色的解答。&lt;/p&gt;
&lt;p&gt;社区提问存在延迟性，可能过很长时间也没有回答。而且，往往到不了这一步，就可以在前两步得到答案。&lt;/p&gt;
&lt;p&gt;查阅文献的时候，只有在大概知道答案在什么地方，才能有一点点效率，否则就是大海捞针。好消息是，知识是有来源的，我们总能找到问题的出处。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关于下文&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、原文来自 &lt;a href=&quot;https://stackoverflow.com/users/148870/amber&quot;&gt;Amber&lt;/a&gt; 个人网站上的一篇文章：&lt;a href=&quot;https://codingkilledthecat.wordpress.com/2012/06/26/how-to-ask-for-programming-help/&quot;&gt;How to Ask for Programming Help&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2、这是一篇针对社区提问的一些建议, 发布于 2012 年 6 月 26 日。&lt;/p&gt;
&lt;p&gt;3、翻译中会尽量遵照原意, 也会加入译者的技术注释, 以及选择更符合汉语文法的译句。&lt;/p&gt;
&lt;p&gt;4、欢迎指正，非常感谢。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;译文&lt;/h1&gt;
&lt;p&gt;先声明一下，在编程问题中我&lt;a href=&quot;http://stackoverflow.com/users/148870/amber&quot;&gt;乐于助人&lt;/a&gt;。当别人学习编程时，如果能给予他们帮助，我会觉得很有趣、很开心。这个过程中，提供帮助的难易程度会受到多方面因素的影响。这里有一些建议可以帮助你提高问题的质量和你得到帮助的效率。&lt;/p&gt;
&lt;h2&gt;先自己尝试解决&lt;/h2&gt;
&lt;p&gt;简单来说，如果你在寻求帮助，你应该是&quot;寻求帮助&quot;，而不是&quot;获得解决方案&quot;。大多数人都乐意提供帮助建议，但是不会为你的工作或爱好负责。如果你有一个问题，先试试看你自己能做什么。如果你完全不会也没有关系，至少可以帮助你的上司给你安排工作时排除某些不适合的内容。&lt;/p&gt;
&lt;p&gt;如果你遇到的问题有文档或手册，从那里去寻找答案。即使最后没有找到，你也能够找到很多其他问题的答案。&lt;/p&gt;
&lt;p&gt;如果你的问题已经存在解决方案了，谷歌搜索、FAQs 等都可以提供帮助。&lt;/p&gt;
&lt;h2&gt;缩小问题范围&lt;/h2&gt;
&lt;p&gt;一旦你对这个问题有了一些思路，试着缩小你需要帮助的范围。一个路人可能不会想要遍历你的数千行代码来找出遇到的问题。如果可能的话，尝试创建问题的简要版本，只用最少的代码来显示遇到的问题(参见:&lt;a href=&quot;http://sscce.org/&quot;&gt;SSCCE&lt;/a&gt;)。&lt;/p&gt;
&lt;h2&gt;提供足够的细节&lt;/h2&gt;
&lt;p&gt;例如，当你在询问解决 error 时，一定要包含错误信息的关键文本——最好直接从包含它的日志、控制台或网页中复制粘贴。通常，错误消息中的一个小&lt;strong&gt;细节&lt;/strong&gt;可能是帮助其他人找出代码问题的关键，这样他们就可以指出如何修复它。&lt;/p&gt;
&lt;p&gt;如果你是在请求对特定代码段的帮助，请务必提供&lt;strong&gt;代码示例&lt;/strong&gt;。试图在没有真正看到代码的情况下回答有关代码的问题是很困难的。能够运行的代码可以更容易地理解所问的问题，也可以更容易地回答问题，因为答案可以在实际代码的上下文中调试出来。&lt;/p&gt;
&lt;p&gt;类似地，当你提供代码样例时，尽量不要删除太多——理想情况下，代码样例应该能够独立运行，至少在&lt;strong&gt;语义&lt;/strong&gt;上不应该与实际代码不同。例如，在代码中隐藏特定的敏感值是可以的，但不应该更改循环条件之类的东西。&lt;/p&gt;
&lt;p&gt;描述所展示的代码的&lt;strong&gt;上下文&lt;/strong&gt;通常也很有用，即使只展示了一小段代码。有的时候，你会得到一些建议，使用完全不同的方法来实现你的目标，这些方法完全绕过了最初的问题(并且通常更有效)。&lt;/p&gt;
&lt;h2&gt;别懒&lt;/h2&gt;
&lt;p&gt;如果你在寻求帮助，你其实是在要求别人用自己的时间来解决别人的问题。不要因为愚蠢的事情而浪费他们的时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查你的问题是否有错别字&lt;/li&gt;
&lt;li&gt;确保你发布的任何链接都有效&lt;/li&gt;
&lt;li&gt;如果可以的话，尽量把内容格式化得合理而美观&lt;/li&gt;
&lt;li&gt;如果你在没有内置代码共享功能的地方提问，可以使用&lt;a href=&quot;https://pastebin.com/&quot;&gt;Pastebin.com&lt;/a&gt;这样的服务来提供你的代码示例(并打开适当的语法高亮显示)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：代码分享的话，我比较常用的是 &lt;a href=&quot;https://codepen.io/&quot;&gt;codepen&lt;/a&gt; ）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;有耐心&lt;/h2&gt;
&lt;p&gt;除非你为别人的时间付钱，否则他们没有义务帮助你。有很多人愿意回答问题，但他们往往不喜欢被纠缠。一旦你在某处陈述了你的问题或疑问，给它一些时间。一般来说，愿意帮助别人的人会在他们有能力的时候帮助别人。如果你在第一个地方没有得到回应，你可以在其他地方问问(但如果你在其他地方得到了回应，要有礼貌，让你问的其他地方知道——最好贴出答案)。&lt;/p&gt;
</content:encoded></item><item><title>计算机组成原理知识体系</title><link>https://0110wdj.github.io/snofly-blog/posts/computer-basic/computer-part/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/computer-basic/computer-part/</guid><pubDate>Sat, 25 May 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;一、计算机系统概述&lt;/h1&gt;
&lt;h2&gt;（一）组成部分&lt;/h2&gt;
&lt;p&gt;1.计算机硬件：CPU + MM + I/O&lt;/p&gt;
&lt;p&gt;1）==中央处理器==（CPU，Central Processing Unit）是一块超大规模的集成电路，是一台计算机的运算核心（Core）和控制核心（ Control Unit）。
2）MM 是计算机硬件中的==主存储器==，即 Main Memory。
3）I/O==输入/输出==(Input/Output)，分为 IO 设备和 IO 接口两个部分。&lt;/p&gt;
&lt;p&gt;2.计算机软件：系统软件+应用软件&lt;/p&gt;
&lt;h2&gt;（二）冯·诺依曼结构&lt;/h2&gt;
&lt;p&gt;1.计算机有运算器、控制器、存储器、输入和输出设备。 2. 各基本部件的功能是：
==存储器==存放数据和指令，形式上两者没有区别，但计算机应
能区分数据还是指令；
==控制器==应能自动执行指令；
==运算器==应能进行加/减/乘/除四种算术运算和一些逻辑运算；
操作人员通过==输入、输出设备==和主机进行通信。 3. 内部以二进制表示==指令和数据==。每条指令由==操作码和地址码==两
部分组成。操作码指出操作类型，地址码指出操作数的地址。
由一串指令组成程序。 4. 采用“==存储程序==”工作方式。 将事先编好的程序和原始数据送
入主存中，然后启动执行。计算机能自动完成逐条取出指令和
执行指令的任务。&lt;/p&gt;
&lt;h2&gt;（三）计算机发展时间节点&lt;/h2&gt;
&lt;p&gt;第一代：真空管（电子管 Vacuum Tube ）1946 ～ 57 年
第二代：晶体管 1958 ～ 64 年
第三代：SSI/MSI 1965 ～ 71 年
第四代：LSI/VLSI/ULSI 1972 ～至今&lt;/p&gt;
&lt;h2&gt;（四）典型计算机的硬件组成&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;d0aa3afd96e760bc67e67e8b302c6f66.png&quot; alt=&quot;大体上看一下&quot; /&gt;
&lt;img src=&quot;03d74e9c4de0eace055bbbc3322a1bca.png&quot; alt=&quot;以后回来看看&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（五）三种程序&lt;/h2&gt;
&lt;p&gt;汇编程序(Assembler)：汇编语言源程序 → 机器目标程序
编译程序(Complier)：高级语言源程序 → 汇编/机器目标程序
解释程序(Interpreter )：将高级语言语句逐条翻译成机器指令并立即执行,不生成目标文件。&lt;/p&gt;
&lt;h1&gt;二、计算机性能评价&lt;/h1&gt;
&lt;h2&gt;（一）评价指标&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;两大分类&lt;/strong&gt;
Tasks per day, hour, sec, ns..
– 吞吐率（throughput）
– 带宽（bandwidth ）
Time to do the task
– 响应时间(response time)
– 执行时间、等待时间(execution time， latency)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;各项指标&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1.==CPU 执行时间==：执行程序中每条指令的时间。
意义：
一般不考虑主存磁盘输入输出开销，只考虑 CPU 的执行时间。
比较计算机的性能时，多用 CPU 执行时间来衡量。&lt;/p&gt;
&lt;p&gt;2.==CPU 时间==：指 CPU 花在程序执行上的时间。
包括 2 部分：
==用户 CPU 时间==：运行用户代码的时间。
==系统 CPU 时间==：为了执行用户程序而需要运行操作系统程序的时间。&lt;/p&gt;
&lt;p&gt;3.==CPI (Cycles Per Instruction)==：
CPI = 时钟周期数 /指令 (每条指令单周期或多周期)
意义：
CPI 用来衡量以下各方面的综合结果:
指令集、程序、计算机系统的设计&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其他指标&lt;/strong&gt;
1.==MIPS==
Million Instructions Per Second （定点指令执行速度）&lt;/p&gt;
&lt;p&gt;2.==MFLOPS==
Million Floating-point Operations Per Second（浮点操作速度）&lt;/p&gt;
&lt;p&gt;3.==性能评价基准程序==（Benchmarks）
• 基准测试程序是专门用来进行性能评价的一组程序
• 基准程序通过运行实际负载来反映计算机的性能
• 最好的基准程序是用户实际使用的程序或典型的简单程序&lt;/p&gt;
&lt;h2&gt;（二）部分数值计算&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;计算式&lt;/strong&gt;
CPI = (CPU 时间 × 时钟频率) / 指令条数 = 总时钟周期数 / 指令条数
CPU 执行时间 = CPU 时钟周期数 × 时钟周期
= CPU 时钟周期数 / 时钟频率
= 指令条数 × CPI × 时钟周期&lt;/p&gt;
&lt;h1&gt;三、数据的机器级表示&lt;/h1&gt;
&lt;h2&gt;（一）数值数据表示&lt;/h2&gt;
&lt;p&gt;==数值数据==：无符号整数、带符号整数、浮点数（实数）、十进制数
数值数据表示三要素：进位计数制(进制)，定点浮点表示，编码规则&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定点数的表示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• 进位计数制
二、八、十六、十&lt;/p&gt;
&lt;p&gt;• 定点数的二进制编码
==原码==
......
==补码==
假定补码有 n 位，则 X 的补码为：
定点整数：[X]补= 2^n^ + X （-2^n^≤X ＜ 2^n^，mod 2^n^）；
定点小数：[X]补= 2 + X （-1≤X ＜ 1，mod 2）；&lt;/p&gt;
&lt;p&gt;一个负数的补码等于模减去该负数的绝对值。&lt;/p&gt;
&lt;p&gt;便捷计算：
正数：符号位为 0，数值部分不变 
负数：符号位为 1，数值部分“各位取反，末位加 1”
......
==反码==
正数的反码与原码相同
负数的反码符号不变，数值位取反
......
==移码==
定义：将每一个补码数值加上一个偏置常数得到移码 。
作用：主要用来表示浮点数阶码。
• 定点整数的表示&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无符号整数、带符号整数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;浮点数的规格化&lt;/strong&gt;
 ==IEEE754 浮点数标准==
&lt;img src=&quot;df2b5124b4bb30aec01ce0ec9962d558.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
非规格化参照表格
&lt;img src=&quot;550494cd546429a88de5214628f9d96d.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;浮点数计算注意问题&lt;/strong&gt;
很大的数与很小的数进行运算时，小的数将会被抹掉。
（原因，浮点数计算第一步，阶码计算时，小数移位运算，全部变成 0 了）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;十进制数表示&lt;/strong&gt;
用 ASCII 码或 BCD 码表示。&lt;/p&gt;
&lt;h2&gt;（二）非数值数据表示&lt;/h2&gt;
&lt;p&gt;==非数值数据==：逻辑数（包括位串）、西文字符和汉字
&lt;strong&gt;逻辑数据&lt;/strong&gt;
用一位表示 。
逻辑数据和数值数据在形式上并无差别，也是一串 0/1 序列，机器靠指令来别。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;西文字符&lt;/strong&gt;
拼音文字，用有限几个字母可拼写出所有单词；
只对有限个字母和数学符号、标点符号等辅助字符编码；
所有字符总数不超过 256 个，使用 7(或 8)个二进位可表示；
常用编码为 ASCII 码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;汉字&lt;/strong&gt;
有以下几种汉字代码：
==输入码==：对汉字用按键进行编码，用于输入；&lt;/p&gt;
&lt;p&gt;==内码==：用于在系统内存储、查找、传送等处理；&lt;/p&gt;
&lt;p&gt;变化过程：
码表区位码(加 20H)---&amp;gt; GB2312 国标码(高位变 1)---&amp;gt;内码&lt;/p&gt;
&lt;p&gt;解释：
码表由 94 行、94 列组成，行号为区号，列号为位号，各占 7 位；
区号和位号各自加上 32（20H），得到其“国标码”；
国标码中区号和位号各占 7 位，前面添一个 0，构成一个字节；
为与 ASCII 码区别，将国标码的两个字节的第一位置“1”得到汉字内码。&lt;/p&gt;
&lt;p&gt;==字模点阵或轮廓描述==: 描述汉字点阵或轮廓，用于显示/打印。&lt;/p&gt;
&lt;h2&gt;（三）数据的宽度、存储排列、纠/检错&lt;/h2&gt;
&lt;p&gt;==比特==（bit）是计算机中处理、存储、传输信息的最小单位；
==字节==(Byte)，也称“位组”。
存储器按字节编址；
字节是最小可寻址单位 (addressable unit )。
==字长==：定点运算数据通路的宽度。
字长等于 CPU 内部总线的宽度、运算器的位数、通用寄存器的宽度等。
==字==用来度量数据类型的宽度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;储存&lt;/strong&gt;
ISA 设计时要考虑的两个问题：
• 大小端问题
大端方式: MSB 的地址是数的地址；
大端方式: MSB 的地址是数的地址。
• 字的边界对齐问题
按边界对齐 （假定字的宽度为 32 位，按字节编址）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字地址：4 的倍数(低两位为 0)&lt;/li&gt;
&lt;li&gt;半字地址：2 的倍数(低位为 0)&lt;/li&gt;
&lt;li&gt;字节地址：任意
不按边界对齐
好处：节约空间；
坏处：可能会增加访存次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;纠错、检错&lt;/strong&gt;
元器件故障或噪音会使数据出现差错。
措施：
(1) 通过电路、布线等方面提高的抗干扰能力；
(2) 进行数据检错和校正，发现并纠正错误。&lt;/p&gt;
&lt;p&gt;例如：
增大码距可提高检错纠错能力。&lt;/p&gt;
&lt;p&gt;==码距==：
若干位代码组成的一个字叫==码字==（数据校验中“码字”指数据位和校验位排列得到的代码）
两个码字中不同位的个数叫它们的==距离==；
码制中所有码字间最小距离为码距，即码制的距离。

常用的数据校验码有：
==奇偶校验码==、==海明校验码==、==循环冗余校验码==。
• 奇偶校验：适应于一字节长数据的校验，如内存
• 海明校验：各组内用奇偶校验，用于内存储器数据的校验
• 循环冗余校验：用在通信和外存中，适合于大批量数据校验&lt;/p&gt;
&lt;h1&gt;四、运算方法及部件&lt;/h1&gt;
&lt;h2&gt;（一）高级语言和机器指令中的运算&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;算术运算&lt;/strong&gt;
整数算术运算、浮点数算术运算
&lt;strong&gt;按位运算&lt;/strong&gt;
用途：
对位串实现“掩码”（mask）操作或相应的其他处理
&lt;strong&gt;逻辑运算&lt;/strong&gt;
与、或、非
与按位运算的差别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;符号表示不同：&amp;amp; - &amp;amp;&amp;amp; ......&lt;/li&gt;
&lt;li&gt;运算过程不同：按位 - 整体&lt;/li&gt;
&lt;li&gt;结果类型不同：位串 - 逻辑值
&lt;strong&gt;移位运算&lt;/strong&gt;
• 用途&lt;/li&gt;
&lt;li&gt;提取部分信息&lt;/li&gt;
&lt;li&gt;扩大或缩小数值的 2、4、8…倍
• 操作&lt;/li&gt;
&lt;li&gt;左移：x&amp;lt;&amp;lt;k; 右移： x&amp;gt;&amp;gt;k&lt;/li&gt;
&lt;li&gt;由 x 的类型决定移位的类型&lt;/li&gt;
&lt;li&gt;无符号数：==逻辑移位==（逻辑移位:空位补 0；）&lt;/li&gt;
&lt;li&gt;带符号整数：==算术移位==（算术左移:低位补 0;算术右移: 高位补符号。）
右移：低位移出，高位补符号，可能发生有效数据丢失。
&lt;strong&gt;位扩展和位截断运算&lt;/strong&gt;
• 用途&lt;/li&gt;
&lt;li&gt;类型转换时可能需要数据扩展或截断
• 操作&lt;/li&gt;
&lt;li&gt;无专门运算符，由类型转换前后数据长短确定是扩展还是截断&lt;/li&gt;
&lt;li&gt;==扩展==：短转长
无符号数：0 扩展，前面补 0
带符号整数：符号扩展，前面补符&lt;/li&gt;
&lt;li&gt;==截断==：长转短
强行将高位丢弃，故可能发生“溢出”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实现 MIPS 定点和浮点运算指令的思路&lt;/strong&gt;：
先实现一个能进行基本算术运算（加/减）和基本逻辑运算的 ALU；
再由 ALU 和移位器实现乘、除、浮点运算器。&lt;/p&gt;
&lt;h2&gt;（二）ALU（划重点）&lt;/h2&gt;
&lt;p&gt;话不多说，先上图：
&lt;img src=&quot;0bb01e0dee78d3ee7f43da0c85378c78.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
==算术逻辑单元==(arithmetic and logic unit)，简称==ALU==。
ALU 可进行加/减算术运算和逻辑运算。其核心部件是加法器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;加法器&lt;/strong&gt;
半加器；
全加器；
串行进位加法器；
并行进位加法器。&lt;/p&gt;
&lt;p&gt;==超前进位加法器==(Carry Look-Ahead)，简称==CLA==加法器。
CLA 加法器由进位生成/传递部件、CLA 部件和 求和部件 构成。
实现 Si=Pi⊕Ci 的电路称为==求和部件==&lt;/p&gt;
&lt;p&gt;==局部（单级）CLA 加法器==
&lt;img src=&quot;81935d8c6854e3a33bd1faa34196c75f.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
==带标志加法器==
加法器需要增加逻辑门生成标志信息。
溢出标志 OF= Cn⊕Cn-1
符号标志 SF=Fn-1
零标志 ZF
进位借位标志 CF=Cout ⊕ Cin
&lt;img src=&quot;5e91b2e7d716a4eb109948e5af22ec55.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
==ALU 的核心部件==是带标志位加法器。
A、B 为 n 位输入端。ALUop 操作控制端。下图中可进行与、或、加运算。
&lt;img src=&quot;f2745c1eaf4c0c021f2859b6a702aa77.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（三） 定点数运算及其运算部件&lt;/h2&gt;
&lt;p&gt;无符号数的按位逻辑运算可用逻辑门实现；
逻辑移位可用移位器实现；
带符号数的算术移位、无符号和带符号数的位扩展和截断也可用简单电路实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;加减公式&lt;/strong&gt;
[A+B]补 = [A]补 + [B]补 ( mod 2n )
[A–B]补 = [A]补 + [–B]补 ( mod 2n )&lt;/p&gt;
&lt;p&gt;[–B]补=[B]补取反+1
(取反时含符号位)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;标志位作用&lt;/strong&gt;
零标志 ZF、符号标志 SF、进/借位标志 CF、溢出标志 OF 称为==条件标志==。
条件标志在运算电路中产生，记录到专门寄存器。称为程序状态字寄存器或==标志寄存器==。&lt;/p&gt;
&lt;p&gt;零标志 ZF=1 当 F=0；
有符号数的符号标志 SF：SF=Fn-1
(无符号数的符号位没有意义）
有符号数溢出标志 OF：OF=Cn 异或 Cn-1
(不必关心无符号数溢出，模 2n）
无符号数进位/借位标志 CF：CF=Cout 异或 Cin
(不必关心有符号数进位/借位）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对加减运算电路认识&lt;/strong&gt; 1.所有算术运算都基于加法器实现； 2.加法器不知道所算的是带符号数还是无符号数； 3.加法器不判定对错，总是取低 n 位作为结果，并生成标志信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原码乘法&lt;/strong&gt;
可用 ALU 和移位器来实现乘法。&lt;/p&gt;
&lt;p&gt;手工乘法的特点：
① 每步计算：X×y~i~，若 y~i~ = 0，则得 0；若 y~i~ = 1，则得 X
② 把 ① 求得的各项结果 X× yi 逐次左移，表示为 X× y~i~×2^-i^
③ 对 ② 中结果求和，即 (X× y~i~×2^-i^)求和 即为两数的乘积

计算机稍作改进：
① 每次得 X×y~i~后，与前面的结果累加得到 P~i~，称之为部分积，减少了保存各次相乘结果的开销。
② 每次得 X×y~i~后，不是左移与 P~i~相加，而将 P~i~右移后与 X×y~i~相加。
因为加法运算始终对部分积中高 n 位进行，故用 n 位加法器可实现二个 n 位数相乘。
③ 对乘数中为“1”的位加并右移，对为“0”的位只右移，不加。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;补码乘法&lt;/strong&gt;
校正法
......
布斯算法
......&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;快速乘法器&lt;/strong&gt;
设计快速乘法部件的必要性 ：
• 乘法运算耗时多
• 比例大，大约 1/3 是乘法运算
==快速乘法器==的实现（由特定功能的组合逻辑单元构成）
如：阵列乘法器
==阵列乘法器==是原码乘去掉符号位，即为无符号数乘法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;除法运算&lt;/strong&gt;
手算除法的基本要点
① 被除数与除数相减，够减则上商为 1；不够减则上商为 0。
② 每次得到的差为中间余数，将除数右移后与上次的中间余数比较。够减则上商为 1；不够减则上商为 0。
③ 重复执行 ②，直到商的位数足够。
&lt;strong&gt;带符号除法&lt;/strong&gt;
==原码除法==：
==恢复余数法==；
==不恢复余数法==。&lt;/p&gt;
&lt;p&gt;==补码除法==：
==补码恢复余数法==；
==补码不恢复余数法==。&lt;/p&gt;
&lt;h2&gt;（四） 浮点数运算及其运算部件&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;浮点数运算&lt;/strong&gt;
&lt;img src=&quot;a888be8d73a6f9c1fc91ab51d6450397.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;加减法要点&lt;/strong&gt;
基本要点：
&lt;img src=&quot;d1a18cc182af3b1ca4647bb271353264.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;乘除法要点&lt;/strong&gt;
基本要点
(1) 求阶： Xe + Ye - 127 或者 Xe - Ye + 127
(2) 尾数相乘除： Xm */Ym （原码乘/除）
(3) 两数符号相同结果为正；相异为负；
(4) 结果的尾数高位为 0 需左规；最高位有进位需右规。
(5) 若尾数比规定的长，则需舍入。
(6) 若尾数是 0，则阶码也置 0，结果为 0。
(7) 阶码溢出判断&lt;/p&gt;
&lt;h1&gt;五、指令系统&lt;/h1&gt;
&lt;h2&gt;指令&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;一条指令须包含的信息&lt;/strong&gt;
操作码：操作类型
(操作码长度：固定／可变)
源操作数：一个或多个源操作数的地址
(操作数来源：指令本身/寄存器/主存/I/O 端口）
目的操作数：结果存放何处
(结果地址：寄存器/主存/I/O 端口)
下一条指令地址：
(正常隐含在 PC 中，跳转时由指令给出）&lt;/p&gt;
&lt;h2&gt;（二）指令类型和格式&lt;/h2&gt;
&lt;p&gt;指令格式的基本原则
1 应尽量短
2 操作码位数要足够
3 有唯一的解释
4 指令长度应是字节的整数倍
5 地址字段的个数应合理
6 指令尽量规整&lt;/p&gt;
&lt;h2&gt;（三）数据类型&lt;/h2&gt;
&lt;p&gt;操作数的基本类型：
==地址==（指针）
无符号整数，用来确定主(虚)存地址
==数值数据==
定点数(整数)：用二进制补码表示
浮点数(实数)：IEEE754
==位、位串、字符和字符串==
用来表示文本、声音和图像等
==逻辑(布尔)数据==
按位操作（0-假／1-真）&lt;/p&gt;
&lt;p&gt;这些操作数都存放在寄存器或内存单元中&lt;/p&gt;
&lt;h2&gt;（四）操作数的寻址方式&lt;/h2&gt;
&lt;p&gt;– 立即 / 寄存器 / 寄存器间接 / 直接 / 间接 / 堆栈 / 偏移&lt;img src=&quot;b84d23e896445cec948b7a9bb0b647be.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
顾名思义。&lt;/p&gt;
&lt;h2&gt;（五）操作码的编码&lt;/h2&gt;
&lt;p&gt;操作码的编码有两种方式
-- 定长操作码
-- 扩展操作码&lt;/p&gt;
&lt;p&gt;指令字也可定长或变长
--定长指令字 -- 变长指令字&lt;/p&gt;
&lt;p&gt;• 代码长度更重要时：变长指令字、变长操作码，存储更紧凑；
• 性能更重要时：定长指令字、定长操作码，访问和译码更快速。&lt;/p&gt;
&lt;p&gt;指令长度与操作码长度是否可变没有绝对关系。
操作码定长不一定指令字定长；操作码变长指令字一般变长。&lt;/p&gt;
&lt;h2&gt;（六）指令系统&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;指令设计风格&lt;/strong&gt;
– 按操作数地址指定方式来分：
累加器型 、堆栈型 、通用寄存器型、load/store 型
– 按指令的复杂度来分：
复杂指令集计算机 CISC (Complex Instruction Set Computer)
精简指令集计算机 RISC (Reduced Instruction Set Computer)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;指令系统举例&lt;/strong&gt;
MIPS 特点概述：
• 所有指令都是 32 位；
• 有些指令有 26 位目标地址；有些只有 16 位。
• 所有动作在 1 个时钟周期内完成；
• 所有运算都是 32 位的，没有字节和半字的运算；
• MIPS 指令固定 4 个字节，x86 平均长度 3 个字节，所以一般二进制文件比 x86 大；
• 寻址方式：只有一种内存寻址方式，即基地址加 16 位偏移地址；
• 没有栈指令 PUSH 和 POP，对栈的操作都是统一的内存访问方式。
• 子函数的返回地址不存到栈中，而是存到$31 寄存器。&lt;/p&gt;
&lt;h1&gt;六、CPU&lt;/h1&gt;
&lt;h2&gt;（一）单周期数据通路的设计&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CPU 的功能&lt;/strong&gt;
CPU 执行指令。&lt;/p&gt;
&lt;p&gt;每条指令都由四种基本操作实现：
主 存 ---&amp;gt;寄存器（取指， 取数）
寄存器 ---&amp;gt;主存（存结果）
寄存器 ---&amp;gt; 寄存器或者 ALU（取数，存结果）
算术或逻辑运算（运算，计算地址，PC 计算）&lt;/p&gt;
&lt;p&gt;操作功能用==RTL(Register Transfer Language)==描述
RTL 通常规定：
（1）用 R[r]表示寄存器 r 的内容；
（2）用 M[addr]表示主存单元 addr 的内容；
（3）传送方向用“←”表示，传送源在右，传送目的在左；
（4）直接用 PC 表示其内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据通路的位置&lt;/strong&gt;
==数据通路==：
指令执行过程中数据经过的路径，包括路径中的部件，是执行部件。
（数据通路是由操作元件和存储元件通过总线方式或分散方式连接而成的进行数据存储、运算和传送的路径。）
==数据通路组成==：
• 组合逻辑元件（也称操作元件）
• 时序逻辑元件（也称状态元件，存储元件）
数据通路中的状态元件有两种：寄存器(组) + 存储器
==数据通路的功能==：
• 进行数据传送、运算、存储&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单周期数据通路的设计&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;单总线一个时钟只传一个数据，效率低。
多总线同时传送数据，提高效率&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;选择 MIPS 指令集的一个子集作为 CPU 的实现目标&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MIPS 的三种指令类型：
°R: ADD and SUB
• add rd, rs, rt
• sub rd, rs, rt
°I: OR Immediate:
• ori rt, rs, imm16
°I: LOAD and STORE
• lw rt, rs, imm16
• sw rt, rs, imm16
°I: BRANCH:
• beq rs, rt, imm16
°J: JUMP:
• j target&lt;/p&gt;
&lt;p&gt;• 下条指令地址计算与取指令部件
30 位 PC，三路选择：顺序、Branch（结合标志 Zero）、Jump
• R 型指令的数据通路
ALU 两个操作数来自 rs 和 rt，结果写到 rd
• 访存指令的数据通路
符号扩展，数据在 rt 和主存单元中交换
• 立即数运算指令的数据通路
0 扩展后的操作数送到 ALU 的一个输入端
• 分支和跳转指令的数据通路&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;综合所有指令的数据通路&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个单周期数据路径：
&lt;img src=&quot;89bdd4e89c31172e5d2672ba3f47e66b.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（二）单周期控制器的设计&lt;/h2&gt;
&lt;p&gt;控制器的功能：
指令译码，生成控制信号，控制数据通路的动作，是控制部件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考察每条指令的执行过程和涉及到的控制信号的取值&lt;/strong&gt;
• 公共操作：取指令和计算下址 PC
• R-Type 指令（add / sub）
• 立即数指令 （ori）
• 访存指令（lw / sw）
• 分支指令 （beq）
• 跳转指令 （j）
&lt;strong&gt;汇总各指令的控制信号取值&lt;/strong&gt;
• 分两类控制信号：直接送往数据通路 / 送往局部控制单元
&lt;strong&gt;分析 ALU 操作对应的控制信号与 func 字段之间的关系&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设计 ALU 局部控制单元&lt;/strong&gt;
局控的另一支路是 R 型指令，ALUctr 由 func 决定，需要根据 func 写出 ALUctr 的表达式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设计主控制单元&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;（三）微程序控制器设计与异常处理&lt;/h2&gt;
&lt;p&gt;**硬连线路控制器设计的优点和缺点 **
优点：速度快，适合于简单或规整的指令系统，例如，MIPS 指令集。
缺点：结构庞杂，实现困难；修改、维护不易；灵活性差。&lt;/p&gt;
&lt;p&gt;**微程序设计控制器的基本思想 **
基本思想：
• 仿照程序设计，为每个指令编制微程序；
• 每个微程序由若干条微指令构成，各微指令包含若干条微命令
（一条微指令相当于一个状态，一个微命令就是状态中的控制信号）
• 执行时依次取出微指令，译码产生微命令(即控制信号)。
• 存储器称为控制存储器(Control Storage)，简称控存 CS 。
特点：灵活，但速度慢。适用于复杂指令集。&lt;/p&gt;
&lt;p&gt;**微程序、微指令、微操作和微命令的概念及其关系 **
==微程序==是一个微指令序列；
每条==微指令==是一个 0/1 序列，其中包含若干个微命令（即：控制信号）；
==微命令==控制数据通路的执行&lt;/p&gt;
&lt;p&gt;**微指令格式设计 **
• ==微操作码==字段&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;水平微程序：不译法、字段直接编译法、字段间接编译法&lt;/li&gt;
&lt;li&gt;垂直微程序：垂直编译法
• 下条微指令地址确定方式&lt;/li&gt;
&lt;li&gt;增量法（计数器法）&lt;/li&gt;
&lt;li&gt;断定法（下址字段法）
下条微指令有四种情况：
• 取指微程序：每条指令一定先执行取指微程序。
• 第一条微指令：取指后转到该指令的第一条微指令。
• 顺序执行时：顺序取下条微指令。
• 分支执行时：根据不同指令选择下条微指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;MIPS 指令子集的微程序控制器设计&lt;/strong&gt;
......&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;异常和中断的处理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;° 程序执行被 “中断” 的事件有两类
• ==内部“异常”==：在 CPU 内部发生的意外事件或特殊事件
按发生原因分为硬故障中断和程序性中断两类
硬故障中断：如电源掉电、硬件线路故障等
程序性中断：执行某条指令时发生的“例外(Exception)”，如溢出、缺页、越界、非法指令、除数为 0、堆栈溢出、访问超时、断点设置、单步、系统调用等
• ==外部“中断”==：在 CPU 外部发生的特殊事件，外部中断是一种 I/O 方式，
通过“中断请求”信号向 CPU 请求处理。如实时钟、控制台、打印机缺纸、外设准备好、采样计时到、DMA 传输结束等。&lt;/p&gt;
&lt;p&gt;内部“异常” 按处理方式分为故障、自陷和终止三类：
==故障(fault)== ：执行指令引起的异常事件，如溢出、缺页、堆栈溢出、访问超时等。
==自陷(Trap)== ：预先安排的事件，如单步跟踪、系统调用(执行访管指令)等。是一种自愿中断。
==终止系统(Abort)== ：硬故障事件，机器将“终止”，调出中断服务程序来重启操作系统。&lt;/p&gt;
&lt;p&gt;外部中断处理： 1.关中断； 2.保护断点和程序状态； 3.识别异常事件。（软件识别（MIPS 采用）、硬件识别（中断向量）（80x86 采用））&lt;/p&gt;
&lt;h2&gt;（四）多周期处理器&lt;/h2&gt;
&lt;p&gt;特点:
• 时钟周期短
• 不同指令所用周期数可以不同，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load： 5 cycles&lt;/li&gt;
&lt;li&gt;Jump： 3 cycles（前两个都一样）
• 在一条指令中功能部件可以重复使用。如：&lt;/li&gt;
&lt;li&gt;Adder + ALU（多周期时只用一个 ALU，在不同周期可重复使用）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>软件体系结构</title><link>https://0110wdj.github.io/snofly-blog/posts/computer-basic/software-architecture/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/computer-basic/software-architecture/</guid><pubDate>Fri, 06 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;@[toc]软件体系结构&lt;/p&gt;
&lt;h1&gt;一、绪论&lt;/h1&gt;
&lt;h2&gt;1、发展史&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;18206d82c25931fbb763e2f0a0dd77b1.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2、定义(组成派)&lt;/h2&gt;
&lt;p&gt;1993 年， D Garlan, M Shaw 提出软件架构包括
component、connector 和 constraint 三大要素。
&lt;strong&gt;软件设计过程中的一个层次&lt;/strong&gt;，超越了计算过程中的算法设计和数据结构设计。
&lt;strong&gt;组件 component&lt;/strong&gt;可以是一组代码，也可以是独立的程序；
&lt;strong&gt;连接件 connector&lt;/strong&gt;用于表示组件之间的相互关系，可以是过程调用、管道
和消息等；
&lt;strong&gt;约束 constraint&lt;/strong&gt;为组件连接时的条件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;270d3a7dfc5bcea937ebbf983fd47e13.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;二、软件体系结构风格&lt;/h1&gt;
&lt;h2&gt;1、体系结构风格&lt;/h2&gt;
&lt;p&gt;“&lt;strong&gt;风格&lt;/strong&gt;”——经过长时间的实践，被证明具有良好的工艺可行性、性能与实用性，并可直接用来遵循与模仿 (复用)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件体系结构风格定义&lt;/strong&gt;
描述特定领域中软件系统家族的&lt;strong&gt;组织方式的惯用模式&lt;/strong&gt;(idiomatic paradigm)，反映了领域中众多系统所共有的&lt;strong&gt;结构和语义特性&lt;/strong&gt;，并指导如何将各个模块和子系统有效地组织成一个完整的系统。&lt;/p&gt;
&lt;h2&gt;2、经典的体系结构风格&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;8edbda62bcc546b920b0b63359320ede.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;三、数据流体系结构风格&lt;/h1&gt;
&lt;h2&gt;1、理解&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;7e28ad31971c09a257e6426e68ce2d02.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;7fcdeb3126f1b847d47422f87485ecdb.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;0651c343098beb190e25036c17b68c02.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2、批处理体系结构风格&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;
➢ Components (processing steps) are independent programs (基本构件：独立的应用程序)
➢ Connectors are some type of media - traditionally tape (连接件：某种类型的媒质)
➢ Topology: Connectors define data flow graph (连接件定义了相应的数据流图，表达拓扑结构)
➢ Processing steps are independent programs（每个处理步骤是一个独立的程序）
➢ Each step runs to completion before next step starts（每一步必须在前一步结束后才能开始）
➢ Data transmitted as a whole between steps（数据必须是完整的，以整体的方式传递）&lt;/p&gt;
&lt;h2&gt;3、管道-过滤体系结构风格&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;直观理解&lt;/strong&gt;
&lt;img src=&quot;43b2894bdf0cf14282df22bb718b066b.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;275e5405559c4f28b3a75847f7bef759.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;strong&gt;变换&lt;/strong&gt;
&lt;img src=&quot;1506b222239cac607c1baa249e4438c6.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;strong&gt;优点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;➢ 使构件具有良好的隐蔽性和高内聚、低耦合的特点，可将整个系统的输入/输出行为看成多个过滤器的行为的简单合成；
➢ 支持软件复用，只要提供适合在两个过滤器之间传送的数据，任何两个过滤器都可被连接起来；
➢ 系统维护和增强系统性能简单，新的过滤器可以添加到现有系统中来，旧的可以被改进的过滤器替换掉；
➢ 允许对一些如吞吐量、死锁等属性的分析;
➢ 支持并行执行：每个过滤器是作为一个单独的任务完成，因此可与其它任务并行执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不足&lt;/strong&gt;
➢ 不适合处理交互的应用
✓ 在早期对交互应用需求不高的情况下提出
✓ 当需要增量地显示改变时，这个问题尤为严重
➢ 系统性能不高，并增加了编写过滤器的复杂性
✓ 数据传输缺乏通用标准，每个过滤器都增加了解析和合成数据的工作
✓ 绝大部分处理时间消耗在格式转换上
✓ 不适用于需要大量共享数据的应用设置&lt;/p&gt;
</content:encoded></item><item><title>CSS 学习笔记</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/css-snack/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/css-snack/</guid><pubDate>Thu, 26 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;CSS 语法&lt;/h1&gt;
&lt;h2&gt;CSS 结构&lt;/h2&gt;
&lt;p&gt;格式：
&lt;img src=&quot;c9c77de3f00f327b50ae6fa4bfc80bc5.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;镶嵌：
&lt;img src=&quot;639961ad15bedc202e4cd77adc169c00.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;CSS 属性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;background-color&lt;/strong&gt;指定背景色&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;border&lt;/strong&gt;段落边框&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;color&lt;/strong&gt;改变字体颜色属性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;font-family&lt;/strong&gt;设置字体&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;border-bottom&lt;/strong&gt;设置下边框&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;font-size&lt;/strong&gt;字体大小&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;font-weight&lt;/strong&gt;字体粗细&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;text-decoration&lt;/strong&gt;更多样式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;line-height&lt;/strong&gt;行距&lt;/p&gt;
&lt;h2&gt;高级用法&lt;/h2&gt;
&lt;h3&gt;link 外部链接&lt;/h3&gt;
&lt;p&gt;link 元素用来引入外部样式表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;26e83effb905a20a6b3f44189bbdf513.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;创建 first.css 文件&lt;/h3&gt;
&lt;p&gt;独立出 html，方便使用。&lt;/p&gt;
&lt;h3&gt;使用 class&lt;/h3&gt;
&lt;p&gt;用 class 属性将一个元素加入类中。
使用“.”符号来选择类里的某个元素。&lt;/p&gt;
&lt;h3&gt;text-decoration&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;none&lt;/strong&gt;取消文本修饰&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;line-through&lt;/strong&gt;中划线&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;underline&lt;/strong&gt;下划线&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;overline&lt;/strong&gt;上划线&lt;/p&gt;
&lt;p&gt;两个包含语义和文本修饰的 HTML 元素&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ins&lt;/strong&gt;插入内容，下划线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;del&lt;/strong&gt;删除内容，中划线。&lt;/p&gt;
&lt;h3&gt;字体设置&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;aee19a324126ff6f653c261e0488ccfb.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;盒模式&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;b7ddc1ef6c9ace3e1eec975f0aa87879.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;盒模式：CSS 控制元素显示的方式
内容区、补白、边框、外边距&lt;/p&gt;
&lt;p&gt;背景图片可以在内容和补白底下显示
但是不能越过边框&lt;/p&gt;
&lt;p&gt;设置补白、边框和外边距时
可以分别设置各个侧面的属性
left, right, top, bottom&lt;/p&gt;
&lt;p&gt;id 元素给元素设置页面唯一的名字
使用“#”号选择 id 中的元素&lt;/p&gt;
&lt;h2&gt;页面&lt;/h2&gt;
&lt;p&gt;页面元素的结构化：&lt;strong&gt;div&lt;/strong&gt;和&lt;strong&gt;span&lt;/strong&gt;
div 和 span 可以构建一些严格的支持结构
&lt;strong&gt;text-align&lt;/strong&gt;：center 居中对齐&lt;/p&gt;
&lt;h2&gt;浮动布局设计&lt;/h2&gt;
&lt;h3&gt;流&lt;/h3&gt;
&lt;p&gt;浏览器会一边走读文档，一边把路过的元素从左向右、从上向下放到合适位置。&lt;/p&gt;
&lt;p&gt;内联元素：从左至右
块元素：从上至下&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;display&lt;/strong&gt;:none 把元素隐藏&lt;/p&gt;
</content:encoded></item><item><title>CSS 基础</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/css/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/css/</guid><pubDate>Sat, 07 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;众所周知啊，前端三件套是 HTML、CSS、JavaScript，浏览器只识别这三种规范，其他的任何高级技术都是先翻译（或编译）成这基础三样，再给浏览器处理的。&lt;/p&gt;
&lt;p&gt;所以，只要把握住这三种规范，就能自然地理解新技术中到底节约了怎样的成本，也就能使用、改进、创造新的衍生技术，甚至为三件套新标准的制定建言献策。&lt;/p&gt;
&lt;p&gt;加油吧~&lt;/p&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;了解 HTML、CSS、JavaScript 基本语法&lt;/li&gt;
&lt;li&gt;能用 CSS 实现简单样式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文适合有一定使用经验的读者——能够在 web DevTools 中调整样式，但只是“能用而不能理解”的状态；或者能够实现并理解常用的代码功能，却对复杂要求和陌生（新规范）代码感到困惑。&lt;/p&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;p&gt;从历史宏观到技术细节的微观，全面提高理解力。我们将会学到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSS 版本区别与历史&lt;/li&gt;
&lt;li&gt;代码编写规范与技巧&lt;/li&gt;
&lt;li&gt;应用实例&lt;/li&gt;
&lt;li&gt;衍生技术&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;CSS 历史版本&lt;/h1&gt;
&lt;h2&gt;关于 web 标准&lt;/h2&gt;
&lt;p&gt;万维网联盟（W3C）创建于 1994 年，是 Web 技术领域最具权威和影响力的国际中立性&lt;strong&gt;技术标准机构&lt;/strong&gt;。它以工作组的方式，把某项技术的相关各方聚集起来，最终由他们来产出标准。&lt;/p&gt;
&lt;p&gt;其中，CSS 规范通常是由 CSS 工作组的成员来编写的。该组的成员，九成左右是来自 W3C 会员公司的成员，余下小部分是“特邀专家”和“W3C 工作人员”。&lt;/p&gt;
&lt;p&gt;web 标准中，每项规范从最初启动到最终成熟，都会经过以下阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编辑草案（ED）&lt;/li&gt;
&lt;li&gt;首个公开工作草案（FPWD）&lt;/li&gt;
&lt;li&gt;工作草案（WD）&lt;/li&gt;
&lt;li&gt;候选推荐草案（CR）&lt;/li&gt;
&lt;li&gt;提名推荐草案（PR）&lt;/li&gt;
&lt;li&gt;正式推荐规范（REC）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.w3.org/TR/&quot;&gt; W3C 官网&lt;/a&gt;中可以查看到全部规范，以及他们的所处的阶段。比如 CSS 的各项规范，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cssTR.png&quot; alt=&quot;CSS 规范&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关于 CSS 版本&lt;/h2&gt;
&lt;p&gt;CSS 1 的规范发表于 1996 年，大约能打印 68 页 A4 纸。&lt;/p&gt;
&lt;p&gt;CSS 2 的规范发表于 1998 年，大约能打印 480 页 A4 纸。&lt;/p&gt;
&lt;p&gt;CSS 2.1 纠正了 CSS 2 中的一些错误，并添加了一些已经被广泛实现的特性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;此时，已经不能用人脑全部记下来了。于是，查阅规范或文档就是常规操作了，这里推荐 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS&quot;&gt;MDN 的 CSS 文档&lt;/a&gt;，因为支持中文。如果没有解决疑问，仍建议到官网&lt;a href=&quot;https://www.w3.org/TR/?tag=css&quot;&gt;查看规范原文&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;重大转折！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于这门语言非常庞大，不再适合放在单个规范中，于是 CSS 工作组决定将 CSS 打散到多个不同的规范中，&lt;strong&gt;每个规范独立更新版本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 CSS 2.1 已有的特性，直接升级到 Level 3 这个版本号，而新添加的规范则从 Level 1 这个版本号开始。下面通过一些例子，来感受一下规范的拆分：&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-syntax-3&quot;&gt;CSS 语法（https://www.w3.org/TR/css-syntax-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-cascade-3&quot;&gt;CSS 层叠与继承（https://www.w3.org/TR/css-cascade-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-color-3/&quot;&gt;CSS 颜色（https://www.w3.org/TR/css-color-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/selectors-3/&quot;&gt;选择符（https://www.w3.org/TR/selectors-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-backgrounds-3/&quot;&gt;CSS 背景与边框（https://www.w3.org/TR/css-backgrounds-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-values-3/&quot;&gt;CSS 值与单位（https://www.w3.org/TR/css-values-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-text-3/&quot;&gt;CSS 文本排版（https://www.w3.org/TR/css-text-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-text-decor-3/&quot;&gt;CSS 文本装饰效果（https://www.w3.org/TR/css-text-decor-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-fonts-3/&quot;&gt;CSS 字体（https://www.w3.org/TR/css-fonts-3）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-ui-3/&quot;&gt;CSS 基本 UI 特性（https://www.w3.org/TR/css-ui-3）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-transforms-1/&quot;&gt;CSS 变形（https://www.w3.org/TR/css-transforms-1）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.w3.org/TR/compositing-1/&quot;&gt;图像混合效果（http://www.w3.org/TR/compositing-1）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/filter-effects-1/&quot;&gt;滤镜效果（https://www.w3.org/TR/filter-effects-1）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-masking-1/&quot;&gt;CSS 遮罩（https://www.w3.org/TR/css-masking-1）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-flexbox-1/&quot;&gt;CSS 伸缩盒布局（https://www.w3.org/TR/css-flexbox-1）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-grid-1&quot;&gt;CSS 网格布局（https://www.w3.org/TR/css-grid-1）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;虽然 “CSS 3” 这种说法很流行，但是它并没有在任何规范中定义过。我们提到 “CSS 3”，指的是上面这些规范（还有更多没提及的）的集合。&lt;/p&gt;
&lt;p&gt;由于独立更新，规范的版本号从 Level 1 到 Level 3 都存在，甚至后来还有 Level 4、Level 5 的版本。不过由于约定俗成，我们并没有 CSS 4、CSS 5 这种说法了，而是用 “CSS 3” 来指代一切新的 CSS 规范。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在的 CSS 3 规范（其实是各个小的规范）仍在不断更新中，“浏览器支持 CSS 3”是指支持其中的 REC 阶段的规范，能用官方写法来编写代码。
对某些实验阶段的规范（仍是草案阶段），浏览器想要支持，但又为了不和定稿的新规范冲突，所以采用加前缀的方法。比如 Firefox 的 -moz-，Opera 的 -o-，Safari 和 Chrome 的 -webkit-。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;CSS 代码编写&lt;/h1&gt;
&lt;h2&gt;关于编程理念&lt;/h2&gt;
&lt;p&gt;DRY（Don&apos;t Repeat Yourself）译为“不要重复已经做过的事”。&lt;/p&gt;
&lt;p&gt;它是一种编程理念，旨在提高代码某方面的可维护性：在改变某个参数时，做到只改尽量少的地方，最好是一处。&lt;/p&gt;
&lt;p&gt;在使用 CSS 代码时，通常需要考虑 DRY、可维护性、灵活性、轻量级、尽量符合标准。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 视觉上，这两个总是一起改变的
font-size: 20px;
line-height: 30px;

// 新的写法，减少了修改的变量
font-size: 20px;
line-height: 1.5;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于 @ 规则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./rule@.png&quot; alt=&quot;atRule&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;应用举例：
@media （媒体查询）可以针对不同的屏幕尺寸设置不同的样式。当你重置浏览器大小的过程中，页面也会根据浏览器的宽度和高度重新渲染页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  /* 如果浏览器窗口小于 800px, 背景将变为浅蓝色： */
  @media screen and (max-width: 800px) {
    body {
      background-color: aqua;
    }
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于预处理器&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;CSS 预处理器定义了一种新的语言，主要是通过用一种专门的编程语言，为 CSS 添加一些编程特性，再编译生成 CSS 文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;现有流行库有 &lt;a href=&quot;https://sass-lang.com/&quot;&gt;Sass(Scss)&lt;/a&gt;、&lt;a href=&quot;https://lesscss.org/&quot;&gt;Less&lt;/a&gt;、&lt;a href=&quot;https://stylus-lang.com/&quot;&gt;Stylus&lt;/a&gt; 等，目前，广泛使用的是 Less 和 Sass。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;关于三种 CSS 预处理器的简介，除了官网文档，还推荐查阅&lt;a href=&quot;https://blog.csdn.net/HH18700418030/article/details/122886403&quot;&gt;&lt;em&gt;这篇文章&lt;/em&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;h1&gt;CSS 应用实例&lt;/h1&gt;
&lt;p&gt;有一个容易忽略的事情：CSS (Cascading Style Sheets)——层叠样式表，这名字怎么翻译的？该怎么去理解？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;层叠就是浏览器对多个样式来源进行叠加，最终确定结果的过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;层叠是 CSS 的核心机制，理解了它才能以最经济的方式写出最容易改动的 CSS，让文档外观在达到设计要求的同时，也给用户留下一些空间，让他们根据需要更改文档的显示效果，例如调整字号。——摘自《CSS 设计指南》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;肯定有读者会说：&quot;talk is cheap, show me the code。&quot;那么，我们用两个例子来理解这个“层叠”。&lt;/p&gt;
&lt;h2&gt;梯形标签页&lt;/h2&gt;
&lt;p&gt;一般的标签可能是这样的方盒子样式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./tabs.png&quot; alt=&quot;default&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果想要更多的表现力，一般是修改边框，比如梯形的标签。&lt;/p&gt;
&lt;p&gt;在这个&lt;a href=&quot;https://dabblet.com/gist/1345dc9399dc8e794502&quot;&gt;在线示例&lt;/a&gt;中，有梯形标签页的实现参考。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./tabsCss.png&quot; alt=&quot;梯形&quot; /&gt;&lt;/p&gt;
&lt;p&gt;先想想这样的样式怎样实现？&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;思路是：利用伪元素的 3D 旋转。&lt;/p&gt;
&lt;p&gt;下面是逐步递进的样式实现例子，直接运行看看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  html {
    background-color: rgb(151 90 219);
    display: flex;
    justify-content: space-around;
  }

  div {
    background: #58a;
    margin: 50px;
    float: left;
  }
&amp;lt;/style&amp;gt;

&amp;lt;style&amp;gt;
  .div1 {
    transform: perspective(10px) rotateX(5deg);
  }

  .div2 {
    position: relative;
    display: inline-block;
    padding: 0.5em 1em 0.35em;
    color: white;
    background: blueviolet;
  }

  .div2::before {
    content: &quot;&quot;;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: -1;
    background: #58a;
    transform: perspective(0.5em) rotateX(5deg);
  }

  .div3 {
    position: relative;
    display: inline-block;
    padding: 0.5em 1em 0.35em;
    color: white;
    background: bottom;
  }

  .div3::before {
    content: &quot;&quot;;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: -1;
    background: #58a;
    transform: scaleY(1.3) perspective(0.5em) rotateX(5deg);
    transform-origin: bottom;
  }

  .div4 {
    position: relative;
    display: inline-block;
    padding: 0.5em 1em 0.35em;
    background: bottom;
  }

  .div4::before {
    content: &quot;&quot;;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: -1;
    background: #ccc;
    border: 1px solid rgba(0, 0, 0, 0.4);
    border-bottom: none;
    border-radius: 0.5em 0.5em 0 0;
    box-shadow: 0 0.15em white inset;
    background-image: linear-gradient(
      hsl(327deg 96% 60% / 73%),
      hsl(232deg 61% 49% / 81%)
    );
    transform: scaleY(1.3) perspective(0.5em) rotateX(5deg);
    transform-origin: bottom;
  }
&amp;lt;/style&amp;gt;

&amp;lt;!-- 如果直接3D旋转元素，则会导致内容非预期地变化 --&amp;gt;
&amp;lt;div class=&quot;div1&quot;&amp;gt;DIV1SNOFLY&amp;lt;/div&amp;gt;
&amp;lt;!-- 文本没有变形了，只是背景的旋转轴在中心，导致上面凹陷、下面突出 --&amp;gt;
&amp;lt;div class=&quot;div2&quot;&amp;gt;DIV2SNOFLY&amp;lt;/div&amp;gt;
&amp;lt;!-- 改变后不再突出，已经符合基本要求了 --&amp;gt;
&amp;lt;div class=&quot;div3&quot;&amp;gt;SNOFLYDIV3&amp;lt;/div&amp;gt;
&amp;lt;!-- 添加更多样式 --&amp;gt;
&amp;lt;div class=&quot;div4&quot;&amp;gt;SNOFLYDIV4&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用伪元素作为背景的梯形，留下了很大的灵活性，因为方便继续修改元素本身的样式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;各自负责一部分现在，最后再合起来一起达到目标效果。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;简单饼图&lt;/h2&gt;
&lt;p&gt;先看看这个图，想想要怎么去实现：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./div4.png&quot; alt=&quot;实现效果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;直接&lt;a href=&quot;https://github.com/0110wdj/Stark-Mansion-Lab-One/blob/master/books/CSSSecrets/%E7%AC%AC3%E7%AB%A0%20%E5%BD%A2%E7%8A%B6/14_pieChart.html&quot;&gt;打开代码&lt;/a&gt;运行看看，它也是分步骤去实现的，理解变化过程即可。&lt;/p&gt;
&lt;p&gt;这里说一下思路：先用渐变画一个左绿右黑的圆，然后用&lt;strong&gt;绿色伪元素挡板&lt;/strong&gt;绕着圆心旋转。&lt;/p&gt;
&lt;p&gt;变个挡板颜色，看看实际的遮挡：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./div4Css.png&quot; alt=&quot;遮挡效果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不一定要控制扇形图像本身，只要多层叠加起来的效果合理即可。&lt;/p&gt;
&lt;h2&gt;其他效果&lt;/h2&gt;
&lt;p&gt;问题很多，解决问题的办法也很多。&lt;/p&gt;
&lt;p&gt;如果没有体会到层叠的好处，继续处理问题就好了。&lt;/p&gt;
&lt;p&gt;推荐一个&lt;a href=&quot;https://css-tricks.com/&quot;&gt;网站&lt;/a&gt;，有使用技巧讲解和实现效果。&lt;/p&gt;
&lt;h1&gt;CSS 衍生技术&lt;/h1&gt;
&lt;p&gt;前文提到的 CSS 预处理器，属于 CSS 的衍生技术之一，它是因为使用者遇到了某种困难而出现的。同样的，因为有其他困难需要面对，所以有其他衍生技术的出现。&lt;/p&gt;
&lt;h2&gt;主要问题&lt;/h2&gt;
&lt;p&gt;1、默认样式很丑，重新设计一套又很花时间，于是有了 &lt;strong&gt;UI 框架&lt;/strong&gt;，基本上改改就能用。比如：bootstrap、Foundation、Less Framework、Pure、Skeleton、99lime HTML KickStart、Kube 等。&lt;/p&gt;
&lt;p&gt;更进一步，结合 HTML 和 JS，干脆把功能封装，于是有了&lt;strong&gt;UI 组件库&lt;/strong&gt;。比如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./UI.png&quot; alt=&quot;UI组件库&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2、当 CSS 代码难以维护时，可以通过编程的方式，减少代码、增加可维护性和复用性。&lt;/p&gt;
&lt;p&gt;用 &lt;strong&gt;CSS 预处理器&lt;/strong&gt;来写，就像用 react 来写 js 一样，易读易写，它们通常都支持“层级，mixin， 变量，循环， 函数”。&lt;/p&gt;
&lt;p&gt;常见 CSS 预处理器：Sass（Scss）, &lt;strong&gt;Less&lt;/strong&gt;, Stylus, Turbine, Swithch css, CSS Cacheer, DT Css。&lt;/p&gt;
&lt;p&gt;3、为了不用手动排查 CSS 代码的浏览器兼容性问题和性能问题，于是有了自动排查和优化 CSS 代码的 &lt;strong&gt;CSS 后处理器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;后处理器例如：PostCSS，通常被视为在完成的样式表中根据 CSS 规范处理 CSS，让其更有效；目前最常做的是给 CSS 属性添加浏览器私有前缀，实现跨浏览器兼容性的问题。&lt;/p&gt;
&lt;p&gt;另外还有 CSS 压缩工具，比如 clean-css，它可以删除 CSS 中一些无用的空格、评论以及每个选择器后面的分号等。&lt;/p&gt;
&lt;h2&gt;LESS 介绍&lt;/h2&gt;
&lt;p&gt;以 LESS 为例，看看预处理器的使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lesscss.org&quot;&gt;官网文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://less.bootcss.com&quot;&gt;中文文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;中文文档首页的概述&lt;strong&gt;非常建议阅读&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里节选了两个功能：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;导入（Importing）&lt;/strong&gt;：
“导入”的工作方式和你预期的一样。你可以导入一个 .less 文件，此文件中的所有变量就可以全部使用了。如果导入的文件是 .less 扩展名，则可以将扩展名省略掉：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@import &quot;library&quot;; // library.less
@import &quot;typo.css&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;嵌套（Nesting）&lt;/strong&gt;:
Less 提供了使用嵌套（nesting）代替层叠或与层叠结合使用的能力。假设我们有以下 CSS 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#header {
  color: black;
}
#header .navigation {
  font-size: 12px;
}
#header .logo {
  width: 300px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 Less 语言我们可以这样书写代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#header {
  color: black;
  .navigation {
    font-size: 12px;
  }
  .logo {
    width: 300px;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 Less 书写的代码更加简洁，并且模仿了 HTML 的组织结构。&lt;/p&gt;
&lt;p&gt;你还可以使用此方法将伪选择器（pseudo-selectors）与混合（mixins）一同使用。下面是一个经典的 clearfix 技巧，重写为一个混合（mixin） (&amp;amp; 表示当前选择器的父级）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.clearfix {
  display: block;
  zoom: 1;

  &amp;amp;::after {
    content: &quot; &quot;;
    display: block;
    font-size: 0;
    height: 0;
    clear: both;
    visibility: hidden;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有很多特性和使用技巧...&lt;/p&gt;
&lt;h1&gt;结束语&lt;/h1&gt;
&lt;p&gt;本文很少提及 HTML 和 JS 相关的内容，为了降耦合，非必要不要用 JS 去控制 CSS 代码。但是一味降耦合，会导致 CSS 代码难以维护。在实际应用中，需要权衡取舍 JS 代码的使用。&lt;/p&gt;
</content:encoded></item><item><title>JavaScript 事件循环深入</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/event-loop/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/event-loop/</guid><pubDate>Tue, 21 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;笔者看了一个关于 JS 事件循环的视频——&lt;a href=&quot;https://www.youtube.com/watch?v=8aGhZQkoFbQ&quot;&gt;菲利普·罗伯茨：到底什么是 Event Loop 呢？ | 欧洲 JSConf 2014&lt;/a&gt; 。观后深有感悟，结合另一篇文章——&lt;a href=&quot;https://segmentfault.com/a/1190000037435824&quot;&gt;浏览器是如何工作的：Chrome V8 让你更懂 JavaScript&lt;/a&gt;，整理思路、输出本文。&lt;/p&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;具备 JS 基础理解&lt;/li&gt;
&lt;li&gt;了解 JS 异步队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：本文不适合零基础的读者。&lt;/p&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;p&gt;通过观察 JavaScript 代码在浏览器中的执行过程，理解 event loop ，顺便了解一下 JS 引擎的设计思路。&lt;/p&gt;
&lt;h1&gt;JS 引擎与 JS 代码&lt;/h1&gt;
&lt;h2&gt;JS 引擎&lt;/h2&gt;
&lt;p&gt;众所周知啊，计算机的 CPU 只能执行其指令集所包含的机器指令或汇编指令，所以高级语言到机器语言的编译过程是不能省去的。&lt;/p&gt;
&lt;p&gt;指令集的差异是客观存在的，为了使 JS 代码在不同环境中执行，就有了 JS 引擎去兼容各种指令集，然后编译出相应的汇编代码。&lt;/p&gt;
&lt;h2&gt;Google V8&lt;/h2&gt;
&lt;p&gt;浏览器中包含了渲染引擎和 JS 引擎，因为浏览器厂商很多，所以 JS 引擎也多种多样。&lt;/p&gt;
&lt;p&gt;当然，优秀的技术和设计总是趋同的，这里介绍一款 JS 引擎：Google V8 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;V8 (Google)，用 C++ 编写，开放源代码，由 Google 丹麦开发，是 Google Chrome 的一部分，也用于 Node.js。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中，第一个版本随着第一版 Chrome 于 2008 年 9 月 2 日发布。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;V8 率先引入了即时编译（JIT）的双轮驱动的设计（混合使用编译器和解释器的技术），这是一种权衡策略，混合编译执行和解释执行这两种手段，给 JavaScript 的执行速度带来了极大的提升。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了优化 JS 代码的执行，V8 中还设计了更多的机制。感兴趣可以查看本文首段的那篇文章。&lt;/p&gt;
&lt;h2&gt;解释型语言&lt;/h2&gt;
&lt;p&gt;先引用《百度百科》的一段话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;解释型语言：程序不需要编译，程序在运行时才翻译成机器语言，每执行一次都要翻译一次。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;相对于编译型语言存在的，源代码不是直接翻译成机器语言，而是先翻译成中间代码，再由解释器对中间代码进行解释运行。比如 Python/JavaScript / Perl /Shell 等都是解释型语言。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这种说法是很模糊的，举个例子：&lt;/p&gt;
&lt;p&gt;对 JS 代码中的函数，V8 只会在函数调用时，编译一次函数然后执行。如果这个函数被多次调用，那么 V8 会缓存编译结果，以后就不用再编译了。&lt;/p&gt;
&lt;p&gt;可以说，现代的 JS 均衡地混合了解释型语言和编译型语言的特点。&lt;/p&gt;
&lt;h1&gt;event loop&lt;/h1&gt;
&lt;h2&gt;单线程&lt;/h2&gt;
&lt;p&gt;JavaScript 是单线程语言，它是不支持多线程并行执行代码的。&lt;/p&gt;
&lt;p&gt;我们看到的异步代码的执行，比如处理 http 请求的同时执行其他代码，这个过程似乎是多线程的并行执行。&lt;/p&gt;
&lt;p&gt;实际上不是的，因为支持多线程的是浏览器，而不是 JS 引擎。&lt;/p&gt;
&lt;p&gt;JS 引擎只是依次处理执行栈中的帧，这个栈帧可能来自 JS 代码，也可能来自任务队列。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./eventLoop.webp&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(图片来自&lt;a href=&quot;https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5&quot;&gt;另一篇文章&lt;/a&gt;，其描述的事件循环过程和前文视频中相同。也可以看看&lt;a href=&quot;https://juejin.cn/post/6844903518319411207&quot;&gt;这篇译文&lt;/a&gt;。)&lt;/p&gt;
&lt;h2&gt;执行过程&lt;/h2&gt;
&lt;p&gt;一个有趣的事情：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;setTimeout() 方法不是 ecmascript 规范定义的内容，而是属于 BOM 提供的功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以在浏览器控制台中直接使用 setTimeout() 。&lt;/p&gt;
&lt;p&gt;下面描述了一段示例代码的执行过程：（建议查看&lt;a href=&quot;https://www.youtube.com/watch?v=8aGhZQkoFbQ&quot;&gt;视频&lt;/a&gt;的第 12 分 40 秒）&lt;/p&gt;
&lt;p&gt;1、开始执行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2、入栈当前任务(假设是 main )：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3、入栈第一句代码，执行、出栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;4、入栈第二句代码，执行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;5、第二句代码主体执行完成，定时器给 webapi 执行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;6、第二句代码出栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;7、第三句代码入栈，执行、出栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;8、当前任务执行完成，main() 出栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;9、计时器执行完成，回调函数转入任务队列：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./9.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;10、计时器函数清除：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./10.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;11、当前任务——回调函数 cb 入栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./11.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;12、第一句代码入栈，执行、出栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./12.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;13、当前任务 cb 执行完成，出栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./13.PNG&quot; alt=&quot;eventLoop&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;优先级考虑&lt;/h2&gt;
&lt;p&gt;上面的步骤就是这段代码的执行过程了。其中，值得注意的地方有：&lt;/p&gt;
&lt;p&gt;1、定时器是 webapi 处理的，也就是说，定时器的等待时间是指 cb 在 webapi 中的等待时间，而 cb 再回到栈中执行的时间是不确定的。&lt;/p&gt;
&lt;p&gt;2、当 main() 执行完成时，先查询任务队列。此时任务队列为空，于是 JS 引擎会再去取新的任务(比如 main2())执行。&lt;/p&gt;
&lt;p&gt;3、当栈为空，且任务队列中有等待执行的任务时，会取任务队列中的任务入栈执行。&lt;/p&gt;
&lt;p&gt;4、任务队列内部的任务，存在优先级（也就是插队）的机制。&lt;/p&gt;
&lt;h2&gt;宏任务和微任务&lt;/h2&gt;
&lt;h3&gt;理论&lt;/h3&gt;
&lt;p&gt;JS 中的任务可以分为同步和异步。按照上图，同步代码会先于异步代码执行，栈空之后，才有可能执行异步代码。&lt;/p&gt;
&lt;p&gt;这里有个问题：如果有段异步代码在 webpai 中处理很慢，但是它很重要，处理完后需要优先执行，于是就需要插队。&lt;/p&gt;
&lt;p&gt;目前的设计是：将任务队列就分为两个子队列，分别存放需要优先执行的任务(微任务)，和不需要优先执行的任务(宏任务)。&lt;/p&gt;
&lt;p&gt;这两个队列就是微任务队列和宏任务队列，只有微任务队列清空时，才会执行宏任务队列。&lt;/p&gt;
&lt;p&gt;这样，就能更细粒度地控制任务队列的执行顺序。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;常见的划分：
宏任务:常见的定时器,用户交互事件等。
微任务:Promise 相关任务, MutationObserver 等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;p&gt;结合上面的理论，下面看一段代码的执行顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function app() {
  setTimeout(() =&amp;gt; {
    console.log(&quot;1-1&quot;);
    Promise.resolve().then(() =&amp;gt; {
      console.log(&quot;2-1&quot;);
    });
  });
  console.log(&quot;1-2&quot;);
  Promise.resolve().then(() =&amp;gt; {
    console.log(&quot;1-3&quot;);
    setTimeout(() =&amp;gt; {
      console.log(&quot;3-1&quot;);
    });
  });
}
app();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的输出是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1-2
1-3
1-1
2-1
3-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以自己推导一下。&lt;/p&gt;
&lt;p&gt;下面笔者推导一下执行过程：&lt;/p&gt;
&lt;p&gt;1、执行同步代码，打印 1-2；&lt;/p&gt;
&lt;p&gt;2、检查到同步代码全部执行完毕，开始访问任务队列。&lt;/p&gt;
&lt;p&gt;3、优先访问微任务队列，发现有一个任务。&lt;/p&gt;
&lt;p&gt;4、微任务入栈，开始执行。&lt;/p&gt;
&lt;p&gt;5、微任务执行第一句，打印 1-3；&lt;/p&gt;
&lt;p&gt;6、微任务执行第二句，创建计时器，给 webapi；&lt;/p&gt;
&lt;p&gt;7、微任务执行完成，再次访问任务队列。&lt;/p&gt;
&lt;p&gt;8、优先访问微任务队列，为空。&lt;/p&gt;
&lt;p&gt;9、最后访问宏任务队列，存在两个宏任务。&lt;/p&gt;
&lt;p&gt;10、第一个宏任务入栈，开始执行。&lt;/p&gt;
&lt;p&gt;11、执行宏任务第一句，打印 1-1；&lt;/p&gt;
&lt;p&gt;12、执行宏任务第二句，创建一个微任务给 webapi 。&lt;/p&gt;
&lt;p&gt;13、该宏任务执行完成再次访问任务队列。&lt;/p&gt;
&lt;p&gt;14、优先访问微任务队列，发现有一个任务。&lt;/p&gt;
&lt;p&gt;15、微任务入栈，开始执行。&lt;/p&gt;
&lt;p&gt;16、执行微任务第一句，打印 2-1 。&lt;/p&gt;
&lt;p&gt;17、该微任务执行完成，再次访问任务队列。&lt;/p&gt;
&lt;p&gt;18、优先访问微任务队列，为空。&lt;/p&gt;
&lt;p&gt;19、最后访问宏任务队列，发现有一个任务。&lt;/p&gt;
&lt;p&gt;20、该宏任务入栈，开始执行。&lt;/p&gt;
&lt;p&gt;21、执行宏任务第一句，打印 3-1 。&lt;/p&gt;
&lt;p&gt;22、该宏任务执行完成，再次访问任务队列。&lt;/p&gt;
&lt;p&gt;23、任务队列为空，持续等待新任务。&lt;/p&gt;
&lt;p&gt;到此，当前代码全部执行完成。&lt;/p&gt;
&lt;h1&gt;结束语&lt;/h1&gt;
&lt;p&gt;技术更迭日新月异，希望本文能对你有所帮助。&lt;/p&gt;
&lt;p&gt;如果谬误，欢迎指正。&lt;/p&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;h2&gt;额外的讨论（2024.06.20 补充）&lt;/h2&gt;
&lt;p&gt;下面是一段是未经实际验证的问答（符合我的理解和预期）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者提问：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;关于宏任务与微任务的执行顺序，现在有个场景需要明确。当浏览器 API 执行 io 任务完成后，生成了一个新的微任务，假设为任务 3，此时调用栈中在不断产生新的微任务，假设为任务 1、2、4、5。这五个微任务产生的时间依次为：1、2、3、4、5；那么请问，实际微任务的执行顺序是 1、2、4、5、3 还是 1、2、3、4、5 ？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;chatGPT 3.5 答：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 JavaScript 中，事件循环（Event Loop）处理宏任务（macro task）和微任务（micro task）时，遵循一定的顺序。以下是详细的解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宏任务包括：setTimeout、setInterval、I/O 操作等。&lt;/li&gt;
&lt;li&gt;微任务包括：Promise 的 .then 回调、MutationObserver 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事件循环的执行顺序是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从宏任务队列中取出第一个宏任务执行。&lt;/li&gt;
&lt;li&gt;执行完当前宏任务后，处理所有的微任务队列，直到微任务队列为空。&lt;/li&gt;
&lt;li&gt;然后，再从宏任务队列中取出下一个宏任务执行。&lt;/li&gt;
&lt;li&gt;重复上述步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在你描述的场景中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务 1 和 任务 2 在 I/O 操作完成前产生。&lt;/li&gt;
&lt;li&gt;浏览器 API 执行 I/O 任务完成后生成任务 3（这是一个新的微任务）。&lt;/li&gt;
&lt;li&gt;随后，调用栈中又产生了任务 4 和 任务 5。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据事件循环的机制和微任务的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;I/O 操作完成生成任务 3 后，该任务会立即进入微任务队列，并且会在下一个事件循环开始前的微任务队列中排队。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为任务 4 和 任务 5 是在任务 3 之后产生的，所以它们会排在任务 3 之后。
具体执行顺序如下：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I/O 操作完成后，任务 3 进入微任务队列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于任务 1 和 任务 2 已经在微任务队列中，因此先执行任务 1 和 任务 2。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来执行任务 3。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后执行任务 4 和 任务 5。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，微任务的执行顺序是 1、2、3、4、5。&lt;/p&gt;
&lt;p&gt;总结来说，在这个场景下，微任务的执行顺序是 1、2、3、4、5。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;回答完毕&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;验证思路大概如下（&lt;strong&gt;这个没有成功验证&lt;/strong&gt;，还需要控制浏览器 API 将微任务推到微任务队列的时机）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&quot;node:fs&quot;);

function app() {
  fs.readFile(&quot;./test.txt&quot;, (err, file) =&amp;gt; {
    Promise.resolve().then(() =&amp;gt; {
      console.log(&quot;3&quot;);
    });
    if (err) {
      console.error(err);
    } else {
      console.log(&quot;read complete:&quot;, file.toString());
    }
  });

  Promise.resolve().then(() =&amp;gt; {
    console.log(&quot;1&quot;);
    Promise.resolve().then(() =&amp;gt; {
      console.log(&quot;2&quot;);
      Promise.resolve().then(() =&amp;gt; {
        console.log(&quot;4&quot;);
        Promise.resolve().then(() =&amp;gt; {
          console.log(&quot;5&quot;);
        });
      });
    });
  });
}

app();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>HTML 学习笔记</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/html/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/html/</guid><pubDate>Sun, 22 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;HTML 语法&lt;/h1&gt;
&lt;h2&gt;HTML 结构&lt;/h2&gt;
&lt;h3&gt;理解 1&lt;/h3&gt;
&lt;p&gt;用包围文本的成对标记告诉浏览器网页的结构。&lt;/p&gt;
&lt;p&gt;整个语句称为一个元素。&lt;/p&gt;
&lt;p&gt;我们也称一对开始标记和结束标记为匹配标记。&lt;/p&gt;
&lt;p&gt;元素=开始标记+内容+结束标记&lt;/p&gt;
&lt;h3&gt;理解 2&lt;/h3&gt;
&lt;p&gt;有的网页上只有开始标记，没有结束标记&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;drinks.gif&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是只用一个标记作为速记符号的元素&lt;/p&gt;
&lt;h3&gt;理解 3&lt;/h3&gt;
&lt;p&gt;CSS 指层叠样式表 (Cascading Style Sheets)
样式定义如何显示 HTML 元素
把样式添加到 HTML 4.0 中，是为了解决内容与表现分离的问题&lt;/p&gt;
&lt;h3&gt;理解 4&lt;/h3&gt;
&lt;p&gt;HTML 和 CSS 是用来创建网页的语言&lt;/p&gt;
&lt;p&gt;HTML 用来标记网页结构，CSS 指定显示样式&lt;/p&gt;
&lt;p&gt;网页通常应该有一个 html 元素，下面有一个 head 和一个 body 元素&lt;/p&gt;
&lt;h3&gt;理解 5&lt;/h3&gt;
&lt;p&gt;用块元素来创建网页，然后用内联元素修饰。&lt;/p&gt;
&lt;h3&gt;理解 6&lt;/h3&gt;
&lt;p&gt;使用 bgsound 标记只能添加声音文件，而且也不能显示和调整播放软件的控制面板。
使用 embed 标记则可以插入各种各样的多媒体。&lt;/p&gt;
&lt;h2&gt;HTML 标签&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注释&amp;lt;!--...--&amp;gt; 标签&lt;/strong&gt; !- - 内容 - -&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;head&lt;/strong&gt;标签用于定义文档的头部，它是所有头部元素的容器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;body&lt;/strong&gt; 元素定义文档的主体。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;title&lt;/strong&gt; 元素可定义文档的标题。
浏览器通常把它放置在浏览器窗口的标题栏或状态栏上。
当把文档加入用户的链接列表或者收藏夹或书签列表时，标题将成为该文档链接的默认名称。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;h1- h6&lt;/strong&gt; 标签可定义标题。
h1 定义最大的标题。
h6 定义最小的标题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p&lt;/strong&gt; 标签定义段落。
p 元素会自动在其前后创建一些空白。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;img&lt;/strong&gt; 元素向网页中嵌入一幅图像。
img 标签从网页上链接图像。
img 标签创建的是被引用图像的占位空间。
src 属性的值是文件的路径，即 URL。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;style&lt;/strong&gt; 标签用于为 HTML 文档定义样式信息。
在 style 中，您可以规定在浏览器中如何呈现 HTML 文档。
style 元素位于 head 元素中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;style&lt;/strong&gt;标签可选的属性，定义 style 元素的内容。唯一可能的值是 &quot;text/css&quot;，表示 CSS。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;a&lt;/strong&gt;元素的内容是链接的标签，浏览器也有默认样式。
内容可以是文字或图像。
“两点”表示父目录，一般站内链接用相对路径，站外用网址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;0l&lt;/strong&gt;或者&lt;strong&gt;ul&lt;/strong&gt;里面嵌套&lt;strong&gt;li&lt;/strong&gt;标签可以表示分句
ordered list 有序列表
unordered list 无序列表&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;br&lt;/strong&gt;表示换行，可以单独使用，不需要对应的斜线标签。
概念：空元素(&quot;void&quot;)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;q&lt;/strong&gt;元素，用来标记网页中的“引用”&lt;/p&gt;
&lt;p&gt;**blockquote&amp;gt;**块引用
注意：块引用也会被看成一个段落，所以，要放在 p 的外面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dl&lt;/strong&gt; &lt;strong&gt;dt&lt;/strong&gt;和&lt;strong&gt;dd&lt;/strong&gt;标签
自定义列表&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;bgsound&lt;/strong&gt;背景音乐--设置背景音乐的标记
语法格式 ：
bgsound src=&quot;url&quot; loop=&quot; &quot;
src 指定声音文件的 URL 来源，即其路径，为必选属性；
loop 指定声音文件的循环播放次数，其值为正整数可指定播放次数，值为-1 或 infinite 指定循环播放无限次&lt;/p&gt;
&lt;p&gt;对比
&lt;strong&gt;embed&lt;/strong&gt;
embed src=&quot; url&quot; loop=&quot;&quot; width=&quot; &quot; height=&quot; &quot; autostart=&quot;&quot; type=&quot;&quot; pluginspage=&quot;&quot;
&lt;strong&gt;src&lt;/strong&gt;：指定多媒体文件的 URL 来源，即其路径，为必选属性；
&lt;strong&gt;loop&lt;/strong&gt;指定声音文件的循环播放次数，值为 true 可循环播放无限次，值为 false 只播放一次，false 为默认值
&lt;strong&gt;autostart&lt;/strong&gt;指定对媒体文件在下载完后是否自动播放；
&lt;strong&gt;width&lt;/strong&gt;和&lt;strong&gt;height&lt;/strong&gt;分别指定多媒体窗口的宽和高。值都为 0 时，类似背景音乐可隐藏面板；
&lt;strong&gt;type&lt;/strong&gt;指定多媒体的播放类型；
&lt;strong&gt;pluginspage&lt;/strong&gt;指定插件页面。&lt;/p&gt;
&lt;h2&gt;HTML 标签属性&lt;/h2&gt;
&lt;p&gt;HTML 标签可以拥有属性。属性提供了有关 HTML 元素的更多的信息。&lt;/p&gt;
&lt;p&gt;属性总是以名称/值对的形式出现，比如：name=&quot;value&quot;。&lt;/p&gt;
&lt;p&gt;属性总是在 HTML 元素的开始标签中规定。&lt;/p&gt;
&lt;h2&gt;HTML 超链接&lt;/h2&gt;
&lt;p&gt;&quot;Markup&quot;
标记。
“hypertext”
超链接。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;eefdb7b061145db0573fd9c1277f4ab4.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
属性：关于内容的附加信息
也叫元信息（meta-information）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;d37a5bd912763de6c3127577bbbcfa72.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;网页中的字符表示&lt;/h2&gt;
&lt;p&gt;如 &amp;lt;
表示为 &amp;amp;/lt;(同样，中间的斜线不要在意)&lt;/p&gt;
&lt;h1&gt;HTML 协议&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;99ba11732af34692ee1f47a808f06ab9.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;理解 1&lt;/h2&gt;
&lt;p&gt;一个域名下可以有多个网站&lt;/p&gt;
&lt;h2&gt;URL&lt;/h2&gt;
&lt;p&gt;URL: Uniform Resource Locators
统一资源定位符
&lt;img src=&quot;524a62f888afc1193f533a6ac1563a6a.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;http 协议&lt;/h2&gt;
&lt;p&gt;HTTP: HyperText Transfer Protocol
超文本传输协议&lt;/p&gt;
&lt;p&gt;HTTP 是一个简单的 “请求—响应” 协议就是通过 URL 帮助你寻找服务器上的资源，响应完成就立即断开连接，不保持连接状态。
资源：如 HTML 文件、图片等。&lt;/p&gt;
&lt;p&gt;使用 a 元素创建登陆点：三步曲
找到网页中需要创建登陆点的地方
用 a 元素把他围起来
使用 a 元素的 id 属性定义登陆点的标识名&lt;/p&gt;
&lt;p&gt;链接到一个新窗口
使用&lt;strong&gt;target&lt;/strong&gt;属性
取值为&quot;_blank&quot;：每次都打开新窗口。&lt;/p&gt;
&lt;h1&gt;严格的 HTML&lt;/h1&gt;
&lt;p&gt;HTML：必不可少的元素！&lt;/p&gt;
&lt;p&gt;记得使用 head 和 body&lt;/p&gt;
&lt;p&gt;head 里没有 title&lt;/p&gt;
&lt;p&gt;body 里只有块元素&lt;/p&gt;
&lt;p&gt;块元素不嵌入内联元素
块元素不嵌入 p 元素&lt;/p&gt;
&lt;p&gt;列表只能包括列表项
列表项可以是任何内容&lt;/p&gt;
&lt;p&gt;blockquote 里面必须是块元素&lt;/p&gt;
</content:encoded></item><item><title>使用 JavaScript 调用 API</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/js-call-api/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/js-call-api/</guid><pubDate>Wed, 04 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;译者述&lt;/h2&gt;
&lt;p&gt;1、原文来自 &lt;a href=&quot;https://www.taniarascia.com/&quot;&gt;Tania Rascia&lt;/a&gt; 个人网站上的一篇文章：&lt;a href=&quot;https://www.taniarascia.com/how-to-connect-to-an-api-with-javascript/&quot;&gt;How to Connect to an API with JavaScript&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2、这是一篇较易的入门介绍文章，发布于 2017 年 12 月 7 日。&lt;/p&gt;
&lt;p&gt;3、翻译中会尽量遵照原意，也会加入译者的技术注释，以及选择更符合汉语文法的译句。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;众所周知啊，使用 JavaScript 的大部分工作都是调用 API 。作为一个菜鸡，你可能被告知：多用用 API 就能知道用法了。如果你看到 API 的文档，但仍举手无措，那么这篇文章正好适合你。&lt;/p&gt;
&lt;p&gt;我们将使用纯粹的 javaScript 编写一个非常简单的 web app，它从 API 中检索信息并在页面上展示。不需要服务端、依赖、构建工具或者其他复杂的内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://taniarascia.github.io/sandbox/ghibli/&quot;&gt;在线示例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/taniarascia/sandbox/tree/master/ghibli&quot;&gt;GitHub 源码&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基本了解 &lt;a href=&quot;https://www.internetingishard.com/html-and-css/&quot;&gt;HTML 和 CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;基本了解 &lt;a href=&quot;https://www.sitepoint.com/beginners-guide-javascript-variables-and-datatypes/&quot;&gt;JS 语法和数据类型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;基本了解 &lt;a href=&quot;https://www.taniarascia.com/how-to-use-json-data-with-php-or-javascript/&quot;&gt;JSON 和 JS 对象&lt;/a&gt; 工作原理（这篇链接的文章有完全的介绍）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他知识下文会提及。&lt;/p&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;p&gt;我们将从头开始编写&lt;a href=&quot;https://taniarascia.github.io/sandbox/ghibli/&quot;&gt;这个&lt;/a&gt;链接到&lt;a href=&quot;https://ghibliapi.herokuapp.com/&quot;&gt;吉卜力工作室 API &lt;/a&gt;的应用，检索数据并在网站前端展示。这并不意味着需要用到 APIs 或 REST 的大量资源——只需要最少可用的数据来构建应用即可。我们将会学到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么是 API 。&lt;/li&gt;
&lt;li&gt;学会使用 JavaScript 发出 HTTP GET 请求。&lt;/li&gt;
&lt;li&gt;学会使用 JavaScript 创建一个用于展示的 HTML 元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;表现的效果如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./demo.png&quot; alt=&quot;网页展示图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下面就开始了。&lt;/p&gt;
&lt;h1&gt;快速浏览&lt;/h1&gt;
&lt;p&gt;API 全称为 Application Program Interface，可以定义为：各种软件组件之间的一组通信方法。换句话说，一个 API 允许一个软件和另一个软件通信。&lt;/p&gt;
&lt;p&gt;我们将特别关注 Web API，它能允许 Web 服务器与第三方软件交互。在这种情况下，Web 服务器使用 HTTP 请求与包含 JSON 数据的公开可用 URL 端点通信。如果现在对此感到困惑，那么读完本文就会明白了。&lt;/p&gt;
&lt;p&gt;你可能很熟悉 CRUD app 的概念，它代表 Create、Read、Update、Delete（创建、读取、更新、删除）。任何编程语言都可以用各种方法来实现 CRUD app。一个 Web API 使用的 HTTP 请求对应着 CRUD 的动词。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;HTTP Method&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Creates a new resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Retrieves a resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;td&gt;PUT/PATCH&lt;/td&gt;
&lt;td&gt;Updates an existing resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;Deletes a resource&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;假如你听说过 REST 和 REST 风格 API，它其实只是指一组遵循特定的架构风格的标准。大多数 web 应用都是这样，或者以符合 REST 标准为目标。总而言之，有很多术语、缩写、概念需要理解：HTTP、API、REST，所以感到困惑和受挫是正常的，特别是当 API 文档默认你已经知道这些知识的时候。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;配置&lt;/h1&gt;
&lt;p&gt;我们的目标是什么？得到所有吉卜力工作室电影的数据，然后在网格中展示标题和描述信息。这里有个背景知识，吉卜力工作室是一家日本动画工作室，制作了几部电影，例如《千与千寻》——我的朋友克雷格启发我用它作为例子。&lt;/p&gt;
&lt;p&gt;我们将从在新目录中创建 index.html 文件开始。项目最后只会包含 index.html 、 style.css 和 scripts.js 。这个 HTML 结构只是链接到 CSS 和 JavaScript 文件，加载字体、包含一个 id=&quot;root&quot; 的 div 元素。它是完整的，不用再更改。我们将使用 JavaScript 添加所有要出现的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;

    &amp;lt;title&amp;gt;Ghibli App&amp;lt;/title&amp;gt;

    &amp;lt;link
      href=&quot;https://fonts.googleapis.com/css?family=Dosis:400,700&quot;
      rel=&quot;stylesheet&quot;
    /&amp;gt;
    &amp;lt;link href=&quot;style.css&quot; rel=&quot;stylesheet&quot; /&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;scripts.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于本文关注的是 APIs 和 JavaScript 的概念，所以我不会解释 CSS 的工作原理。我们将新建一个 style.css 来创建网格。为了简洁处理，我只在下面列出了最相关的 CSS 结构，但你可以在&lt;a href=&quot;https://raw.githubusercontent.com/taniarascia/sandbox/master/ghibli/style.css&quot;&gt;这里复制完整的 CSS 代码&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#root {
  max-width: 1200px;
  margin: 0 auto;
}

.container {
  display: flex;
  flex-wrap: wrap;
}

.card {
  margin: 1rem;
  border: 1px solid gray;
}

@media screen and (min-width: 600px) {
  .card {
    flex: 1 1 calc(50% - 2rem);
  }
}

@media screen and (min-width: 900px) {
  .card {
    flex: 1 1 calc(33% - 2rem);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们已经配置了 HTML 和 CSS，然后你可以创建 scripts.js，我们将从那一步继续。&lt;/p&gt;
&lt;h1&gt;连接 API&lt;/h1&gt;
&lt;p&gt;先来看看&lt;a href=&quot;https://ghibliapi.herokuapp.com/&quot;&gt;吉卜力工作室 API 文档&lt;/a&gt;，他可以帮助开发人员了解怎样使用 HTTP 请求进行资源交互，这对我们来说是非常有帮助的。由于 API 可以通过许多不同的方法访问—— JavaScript, PHP, Ruby, Python 等等 —— 大多数 API 的文档往往不会给出如何连接的具体说明。&lt;/p&gt;
&lt;p&gt;从文档中得知，我们可以通过 curl 和常规 REST 调用发出请求，只是目前可能还不理解怎么操作。&lt;/p&gt;
&lt;h2&gt;获得 API 端点&lt;/h2&gt;
&lt;p&gt;首先，让我们转到&lt;a href=&quot;https://ghibliapi.herokuapp.com/#tag/Films&quot;&gt;电影部分&lt;/a&gt;。在右边你能看到 GET /films，它会展示 API 端点的 url，&lt;a href=&quot;https://ghibliapi.herokuapp.com/films&quot;&gt;https://ghibliapi.herokuapp.com/films&lt;/a&gt;。点击该链接将显示一个 JSON 格式的对象数组。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你的浏览器上没有用于查看 JSON 文件的扩展，那么现在就添加一个，例如 &lt;a href=&quot;https://chrome.google.com/webstore/category/extensions?hl=en&quot;&gt;JSON View&lt;/a&gt;。这将使读取 JSON 变得非常非常容易。请记住，如果你从未使用过 JSON，先阅读&lt;a href=&quot;https://www.taniarascia.com/how-to-use-json-data-with-php-or-javascript/&quot;&gt;这篇必备的文章&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;通过 HTTP 请求检索数据&lt;/h2&gt;
&lt;p&gt;在数据放到网站前端之前，先打开一个 API 连接。我们使用 XMLHttpRequest 对象，它可以打开文件、创建 HTTP 请求。&lt;/p&gt;
&lt;p&gt;创建一个 request 并分配一个 XMLHttpRequest 对象给它。然后用 open() 方法打开一个新的连接，其中的参数需要指定请求类型为 GET，以及设置 API 端的 URL。请求完成后，我们可以访问 onload 函数中的数据。做完这些后，就可以发送请求了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建一个 request 并分配一个 XMLHttpRequest 对象给它
var request = new XMLHttpRequest();

// 用 open() 方法打开一个新的连接，其中的参数需要指定请求类型为 GET，以及设置 API 端的 URL
request.open(&quot;GET&quot;, &quot;https://ghibliapi.herokuapp.com/films&quot;, true);

request.onload = function () {
  // 访问 onload 函数中的数据
};

// 发送请求
request.send();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，也可以使用 fetch API 和 async/await 来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getData() {
  const response = await fetch(&apos;https://ghibliapi.herokuapp.com/films&apos;)
  const data = await response.json()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;处理 JSON 响应&lt;/h2&gt;
&lt;p&gt;现在我们接收到了从 HTTP 请求返回的响应，然后就可以处理它了。但是，响应是 JSON 格式的，我们需要将 JSON 转换为 JavaScript 对象才能使用。&lt;/p&gt;
&lt;p&gt;使用 JSON.parse() 来序列化响应，然后创建 data 变量 —— JavaScript 数组对象，包含全部 JSON。再使用 forEach() 打印出全部的电影标题，验证处理操作的正确性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 JSON.parse() 来序列化响应
var data = JSON.parse(this.response);

data.forEach((movie) =&amp;gt; {
  // 打印出全部的电影标题
  console.log(movie.title);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看控制台，可以看到 20 条电影数据，这说明成功了。&lt;/p&gt;
&lt;p&gt;还有一件事：缺少一些错误处理的方法。如果使用了错误的 URL，或者资源本身有问题，导致没有显示任何内容，该怎么办？当发出 HTTP 请求时，返回响应中会带有 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status&quot;&gt;HTTP 状态码&lt;/a&gt;。“404”是最常见的响应，表示资源未找到，“200 OK” 是表示请求成功。&lt;/p&gt;
&lt;p&gt;让我们将代码包装在 if 语句中，对 [200,399) 范围内的任何响应进行处理，并在请求失败时打印错误。你可以把 URL 弄乱测试看看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 开始访问 JSON 数据
var data = JSON.parse(this.response);

if (request.status &amp;gt;= 200 &amp;amp;&amp;amp; request.status &amp;lt; 400) {
  data.forEach((movie) =&amp;gt; {
    console.log(movie.title);
  });
} else {
  console.log(&quot;error&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是到目前为止的全部代码，scripts.js 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var request = new XMLHttpRequest();

request.open(&quot;GET&quot;, &quot;https://ghibliapi.herokuapp.com/films&quot;, true);
request.onload = function () {
  // Begin accessing JSON data here
  var data = JSON.parse(this.response);

  if (request.status &amp;gt;= 200 &amp;amp;&amp;amp; request.status &amp;lt; 400) {
    data.forEach((movie) =&amp;gt; {
      console.log(movie.title);
    });
  } else {
    console.log(&quot;error&quot;);
  }
};

request.send();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们已经成功地使用 GET HTTP 请求来检索(或使用) API 端点，该 API 端点由 JSON 格式的数据组成。但是，我们仍然停留在控制台上——想要在网站的前端显示这些数据，需要通过修改 DOM 来实现。&lt;/p&gt;
&lt;h1&gt;展示数据&lt;/h1&gt;
&lt;p&gt;为了在网站前端展示信息，我们将使用 DOM，它实际上是一种 API，允许 JavaScript 与 HTML 通信。如果你没有用过 DOM，我为 DigitalOcean 写了&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/introduction-to-the-dom&quot;&gt;《用 JavaScript 理解和修改 DOM 》&lt;/a&gt;，阐明了 DOM 是什么以及 DOM 与 HTML 源代码的区别。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：DigitalOcean 是一家成立于 2012 年的总部设置在纽约的云主机商家。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后，我们的页面将包含一个 logo 和多个卡片——每张卡片是一部电影，有一个标题和一个段落，包含了电影名称和简介。下面是它的样子，只加载了必要的 CSS:
&lt;img src=&quot;./result.png&quot; alt=&quot;数据展示&quot; /&gt;
还记得吗？我们的 index.html 现在只有一个 root div —— &amp;lt; div id=&quot;root&quot; / &amp;gt;，用 getElementById() 来访问它。我们现在可以简单地删除之前编写的所有代码，稍后将重新添加这些代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const app = document.getElementById(&quot;root&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你不是 100%确定 getElementById() 做了什么，在上面的代码中打印 app: console.log(app)，有助于理解过程中发生的事情。网站首要的是 logo，这是一个 img 元素。我们将使用 createElement() 创建图像元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const logo = document.createElement(&quot;img&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个空的 img 没什么用，所以我们用 src 属性设置图片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logo.src = &quot;logo.png&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将创建另一个 div 元素并将其 class 属性设置为 container。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const container = document.createElement(&quot;div&quot;);
container.setAttribute(&quot;class&quot;, &quot;container&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们有了一个 img 和一个 div，我们只需要把它们放在网站上。我们将使用 appendChild() 方法将 logo img 和容器 div 附加到 app root。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.appendChild(logo);
app.appendChild(container);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是完整的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const app = document.getElementById(&quot;root&quot;);

const logo = document.createElement(&quot;img&quot;);
logo.src = &quot;logo.png&quot;;

const container = document.createElement(&quot;div&quot;);
container.setAttribute(&quot;class&quot;, &quot;container&quot;);

app.appendChild(logo);
app.appendChild(container);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存后，在网站的前端，你会看到以下内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;root&quot;&amp;gt;
  &amp;lt;img src=&quot;logo.png&quot; /&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 Elements 中显示的，而不是在 HTML 源代码中存在——正如我链接的 DOM 文章中所解释的那样。
现在我们要把之前的代码粘贴回去。最后要根据之前控制台打印出来的东西创建卡片元素。
虽然粘贴全部代码，但是我们只关注 forEach() 中的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data.forEach((movie) =&amp;gt; {
  console.log(movie.title);
  console.log(movie.description);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用 textContent 将 API 返回数据中的文本设置到 HTML 元素中。为了保持卡片元素宽度一致，我们使用 substring() 来限制 p 元素中的段落长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data.forEach((movie) =&amp;gt; {
  // Create a div with a card class
  const card = document.createElement(&quot;div&quot;);
  card.setAttribute(&quot;class&quot;, &quot;card&quot;);

  // Create an h1 and set the text content to the film&apos;s title
  const h1 = document.createElement(&quot;h1&quot;);
  h1.textContent = movie.title;

  // Create a p and set the text content to the film&apos;s description
  const p = document.createElement(&quot;p&quot;);
  movie.description = movie.description.substring(0, 300); // Limit to 300 chars
  p.textContent = `${movie.description}...`; // End with an ellipses

  // Append the cards to the container element
  container.appendChild(card);

  // Each card will contain an h1 and a p
  card.appendChild(h1);
  card.appendChild(p);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理，前端 error 打印部分也将被替换为 marquee 元素！（我这样做只是为了好玩和演示，不要在任何实际应用中使用 marquee，也不要把我的话当真。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const errorMessage = document.createElement(&quot;marquee&quot;);
errorMessage.textContent = `Gah, it&apos;s not working!`;
app.appendChild(errorMessage);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们做完了!下面是最终的 scripts.js 代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const app = document.getElementById(&quot;root&quot;);

const logo = document.createElement(&quot;img&quot;);
logo.src = &quot;logo.png&quot;;

const container = document.createElement(&quot;div&quot;);
container.setAttribute(&quot;class&quot;, &quot;container&quot;);

app.appendChild(logo);
app.appendChild(container);

var request = new XMLHttpRequest();
request.open(&quot;GET&quot;, &quot;https://ghibliapi.herokuapp.com/films&quot;, true);
request.onload = function () {
  // Begin accessing JSON data here
  var data = JSON.parse(this.response);
  if (request.status &amp;gt;= 200 &amp;amp;&amp;amp; request.status &amp;lt; 400) {
    data.forEach((movie) =&amp;gt; {
      const card = document.createElement(&quot;div&quot;);
      card.setAttribute(&quot;class&quot;, &quot;card&quot;);

      const h1 = document.createElement(&quot;h1&quot;);
      h1.textContent = movie.title;

      const p = document.createElement(&quot;p&quot;);
      movie.description = movie.description.substring(0, 300);
      p.textContent = `${movie.description}...`;

      container.appendChild(card);
      card.appendChild(h1);
      card.appendChild(p);
    });
  } else {
    const errorMessage = document.createElement(&quot;marquee&quot;);
    errorMessage.textContent = `Gah, it&apos;s not working!`;
    app.appendChild(errorMessage);
  }
};

request.send();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后使用完整的 CSS 样式，这里是最终产品的样子。
&lt;img src=&quot;./page.png&quot; alt=&quot;结果图&quot; /&gt;
同样，这里有链接可以查看在线 app 和源代码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://taniarascia.github.io/sandbox/ghibli/&quot;&gt;查看在线实例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/taniarascia/sandbox/tree/master/ghibli&quot;&gt;源代码&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;恭喜，你使用纯 JavaScript 通过 HTTP 请求连接到 API。希望你能够更好地理解什么是 API 端点，浏览器如何通过请求和响应与第三方 API 数据通信，如何将 JSON 解析为 JavaScript 能够理解的数组和对象，以及如何只用 JavaScript 构建前端。&lt;/p&gt;
&lt;p&gt;我们做到了这一切，而不用担心 Node.js、npm、Webpack、React、Angular、构建工具、jQuery、Axios 和其他流行的开发术语、依赖项和框架，这些东西可能会让你对最简单的形式下发生的事情感到困惑。&lt;/p&gt;
&lt;p&gt;我希望这篇文章对你有用，也欢迎分享。&lt;/p&gt;
</content:encoded></item><item><title>从兴趣和收益的角度优化 javascript 代码</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/js-opt-fun/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/js-opt-fun/</guid><pubDate>Mon, 18 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;译者述&lt;/h2&gt;
&lt;p&gt;1、原文来自 romgrk 个人网站上的一篇文章： &lt;a href=&quot;https://romgrk.com/posts/optimizing-javascript&quot;&gt;Optimizing Javascript for fun and for profit&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2、这是一篇关于 js 编码优化的文章, 发布于 2024 年 3 月 21 日。&lt;/p&gt;
&lt;p&gt;3、翻译中会尽量遵照原意, 也会加入译者的技术注释, 以及选择更符合汉语文法的译句。&lt;/p&gt;
&lt;p&gt;4、感谢阅读，欢迎指正。&lt;/p&gt;
&lt;h1&gt;正文&lt;/h1&gt;
&lt;h2&gt;关于基准测试&lt;/h2&gt;
&lt;p&gt;我经常觉得 js 代码的执行速度远远不及最佳速度，而这仅仅是因为缺少适当的优化。本文是我收集的一些常用优化技术的总结。&lt;/p&gt;
&lt;p&gt;注意，性能和可读性总是难以兼顾的，而至于怎么权衡，则是留给读者自己的问题。&lt;/p&gt;
&lt;p&gt;我也注意到，要讨论优化问题，必须要先讨论&lt;strong&gt;基准测试(benchmarking)&lt;/strong&gt;。如果一个函数只占实际总运行时间的一小部分，那么即使花几个小时对该函数进行微优化(micro-optimizing)——使其运行速度提高 100 倍，也是没有意义的。如果要进行优化，最先最重要的一步就是基准测试。我将在后文的优化要点中讨论这个主题。&lt;/p&gt;
&lt;p&gt;还要注意，微基准测试(micro-benchmarks)通常是有缺陷的，可能后文中提到的微基准测试也存在缺陷。我已经尽了最大努力避免这些陷阱，但不要在没有基准测试的情况下盲目地应用后文提出的任何优化要点。&lt;/p&gt;
&lt;h2&gt;关于可运行示例&lt;/h2&gt;
&lt;p&gt;我尽可能在每个部分都留下了可运行的示例。它们默认显示我在我的机器上得到的结果（在 archlinux 上是 brave 122），但你也可以自己运行它们试试。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者注：这些示例包含一个初始值（作者机器上跑出来的结果），还包含一个可在线运行的脚本，本译文不处理改脚本。如果读者想运行程序看看效果，可以复制代码本地运行，或者去原文运行在线示例。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然我不想这么说，但火狐浏览器在优化方面有点落后，而且只占市场份额的很小一部分，所以我不建议使用火狐浏览器的结果作为衡量的指标。&lt;/p&gt;
&lt;h2&gt;0 避免工作&lt;/h2&gt;
&lt;p&gt;这听起来很简单，但却是最优先的优化步骤：如果你试图优化，你应该首先考虑&lt;strong&gt;避免工作&lt;/strong&gt;。这里有一些概念：缓存(memoization)、惰性计算(laziness)、增量计算(incremental computation)。它们将根据上下文以合适的方式应用。比如说在 React 中，就是使用 memo 、useMemo 和其他合适的语法。&lt;/p&gt;
&lt;h2&gt;1 避免字符串比较&lt;/h2&gt;
&lt;p&gt;Javascript 悄无声息地隐藏了字符串比较的实际开销。&lt;/p&gt;
&lt;p&gt;如果你需要在 C 语言中比较字符串，你需要使用 strcmp 函数。而在 Javascript 中，使用 &lt;code&gt;===&lt;/code&gt; 运算符，这样就看不见 strcmp 函数了。但它是存在的，字符串比较通常需要将字符串中的每个字符与另一个字符串中的字符进行比较，时间复杂度为 Ο(n)。&lt;/p&gt;
&lt;p&gt;要避免的一种常见写法是&lt;strong&gt;字符串作为枚举&lt;/strong&gt;。但随着 TypeScript 的出现，这应该很容易避免，因为 enum 默认是整数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// No
enum Position {
  TOP = &quot;TOP&quot;,
  BOTTOM = &quot;BOTTOM&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Yes
enum Position {
  TOP, // = 0
  BOTTOM, // = 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是一个实际开销的比较：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. string compare
const Position = {
  TOP: &quot;TOP&quot;,
  BOTTOM: &quot;BOTTOM&quot;,
};

let _ = 0;
for (let i = 0; i &amp;lt; 1000000; i++) {
  let current = i % 2 === 0 ? Position.TOP : Position.BOTTOM;
  if (current === Position.TOP) _ += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. int compare
const Position = {
  TOP: 0,
  BOTTOM: 1,
};

let _ = 0;
for (let i = 0; i &amp;lt; 1000000; i++) {
  let current = i % 2 === 0 ? Position.TOP : Position.BOTTOM;
  if (current === Position.TOP) _ += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、string 50.35%&lt;/li&gt;
&lt;li&gt;2、int 100%&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;关于基准(benchmarks):
百分比结果表示：两段程序在 1 秒内的操作次数，除以两个操作数中的最大值。百分比更大，表示性能更好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如上所示，两者差距显著。这种差异不一定是使用 strcmp 函数的开销，因为 JS 引擎有时会使用&lt;strong&gt;字符串池&lt;/strong&gt;，然后通过引用来比较。&lt;/p&gt;
&lt;p&gt;在 JS 引擎中，整数通常是按值传递的，字符串则是按引用（指针）传递的。内存访问的代价是昂贵的（见第五节），在字符串很多的代码中，这会产生很严重的影响。&lt;/p&gt;
&lt;p&gt;举个现实的例子，我能够使&lt;a href=&quot;https://github.com/json5/json5/pull/278&quot;&gt;这个 JSON5 js 解析器&lt;/a&gt;（遗憾的是，这个改动并没有被合并，但这就是开源的魅力之处）运行速度提高两倍，而改变仅仅是将字符串常量替换为数字。&lt;/p&gt;
&lt;h2&gt;2 避免不同的结构/形状&lt;/h2&gt;
&lt;p&gt;JS 引擎有一种默认的优化：先假设对象具有相特定的结构，且假设函数的参数对象具有相同的结构，然后就只需要存储一次对象的 keys，然后单独用一个数据存储对象的 values。&lt;/p&gt;
&lt;p&gt;如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 比如这些对象的结构是一致的
const objects = [
  {
    name: &quot;Anthony&quot;,
    age: 36,
  },
  {
    name: &quot;Eckhart&quot;,
    age: 42,
  },
  {
    name: &quot;a&quot;,
    age: 1,
  },
  {
    name: &quot;b&quot;,
    age: 2,
  },
  {
    name: &quot;c&quot;,
    age: 3,
  },
  {
    name: &quot;d&quot;,
    age: 4,
  },
  {
    name: &quot;e&quot;,
    age: 5,
  },
  {
    name: &quot;f&quot;,
    age: 6,
  },
  {
    name: &quot;g&quot;,
    age: 7,
  },
  {
    name: &quot;h&quot;,
    age: 8,
  },
  {
    name: &quot;i&quot;,
    age: 9,
  },
  {
    name: &quot;j&quot;,
    age: 10,
  },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 于是，使用这样的存储结构，显然能够减少很多重复字符
const shape = [
  { name: &quot;name&quot;, type: &quot;string&quot; },
  { name: &quot;age&quot;, type: &quot;integer&quot; },
];

const objects = [
  [&quot;Anthony&quot;, 36],
  [&quot;Eckhart&quot;, 42],
  [&quot;a&quot;, 1],
  [&quot;b&quot;, 2],
  [&quot;c&quot;, 3],
  [&quot;d&quot;, 4],
  [&quot;e&quot;, 5],
  [&quot;f&quot;, 6],
  [&quot;g&quot;, 7],
  [&quot;h&quot;, 8],
  [&quot;i&quot;, 9],
  [&quot;j&quot;, 10],
];
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;关于术语的说明：
我使用了 shap 这个词来描述这个概念，但要注意，你也可以使用 hidden class 或 map 来描述它，这取决于引擎。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者注：为了增加对比性，译者增加了示例的内容。另外，原文使用 shap 这个单词，但是翻译不是“形状”，而是“结构”，因为“结构”这个词更能说明数量和类型。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;举个例子，在运行时，如果下面这个函数，接收到的两个参数的结构都是 {x:number,y:number}, 那么引擎将推测未来的参数都是这样的结构，然后就能生成针对性的机器代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function add(a, b) {
  return {
    x: a.x + b.x,
    y: a.y + b.y,
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一旦传递的对象结构不再是 {x,y} —— 比如改为 {y,x}, 那么引擎就会撤销它的推测（相当于放弃这种优化），然后函数就会突然变得相当慢了。&lt;/p&gt;
&lt;p&gt;我尽量减少相关的解释，如果你想了解更多细节，你应该去看看 &lt;a href=&quot;https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html&quot;&gt;mraleph 文章&lt;/a&gt; 的解释。&lt;/p&gt;
&lt;p&gt;这里我要强调的是，V8 中有 3 种特别的模式，以对应不同的访问方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单态 —— 只有一种结构&lt;/li&gt;
&lt;li&gt;多态 —— 有 2-4 种结构&lt;/li&gt;
&lt;li&gt;亚态 —— 5 种以上的结构&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者述：原文描述为 monomorphic (1 shape), polymorphic (2-4 shapes), and megamorphic (5+ shapes))&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;要我说，你就应该保持单态模式，因为它的效率最高。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup
let _ = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. monomorphic
const o1 = { a: 1, b: _, c: _, d: _, e: _ };
const o2 = { a: 1, b: _, c: _, d: _, e: _ };
const o3 = { a: 1, b: _, c: _, d: _, e: _ };
const o4 = { a: 1, b: _, c: _, d: _, e: _ };
const o5 = { a: 1, b: _, c: _, d: _, e: _ }; // all shapes are equal
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. polymorphic
const o1 = { a: 1, b: _, c: _, d: _, e: _ };
const o2 = { a: 1, b: _, c: _, d: _, e: _ };
const o3 = { a: 1, b: _, c: _, d: _, e: _ };
const o4 = { a: 1, b: _, c: _, d: _, e: _ };
const o5 = { b: _, a: 1, c: _, d: _, e: _ }; // this shape is different
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 3. megamorphic
const o1 = { a: 1, b: _, c: _, d: _, e: _ };
const o2 = { b: _, a: 1, c: _, d: _, e: _ };
const o3 = { b: _, c: _, a: 1, d: _, e: _ };
const o4 = { b: _, c: _, d: _, a: 1, e: _ };
const o5 = { b: _, c: _, d: _, e: _, a: 1 }; // all shapes are different
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// test case
function add(a1, b1) {
  return a1.a + a1.b + a1.c + a1.d + a1.e + b1.a + b1.b + b1.c + b1.d + b1.e;
}

let result = 0;
for (let i = 0; i &amp;lt; 1000000; i++) {
  result += add(o1, o2);
  result += add(o3, o4);
  result += add(o4, o5);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;monomorphic: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;polymorphic: 13.07%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;megamorphic: 4.09%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;对此，我到底应该怎么办？&lt;/h3&gt;
&lt;p&gt;只需要保证你的对象都有相同的结构就行了。&lt;/p&gt;
&lt;p&gt;但是说起来容易做起来难，即使是编写 recat 组件 props 顺序不同这样的事情，也会导致退出单态模式。&lt;/p&gt;
&lt;p&gt;举个例子：
我在 React 的代码库中发现了一个 &lt;a href=&quot;https://github.com/facebook/react/pull/28569&quot;&gt;简单的例子&lt;/a&gt;，但在几年前他们有一个 &lt;a href=&quot;https://v8.dev/blog/react-cliff&quot;&gt;更严重的类似的问题&lt;/a&gt; —— 他们用整数初始化了一个对象，但是又存储浮点数。&lt;/p&gt;
&lt;p&gt;没错，改变 number 类型也会改变结构，number 隐含着 integer 和 float 两种类型。这是需要处理的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 number 的表示：
引擎通常可以将整数编码为值。例如，V8 以 32 位表示值，小整数作为紧凑的 &lt;a href=&quot;https://medium.com/fhinkel/v8-internals-how-small-is-a-small-integer-e0badc18b6da&quot;&gt;Smi&lt;/a&gt;值，但浮点数和大整数像字符串和对象一样作为指针传递。
JSC 使用 64 位编码，&lt;a href=&quot;https://ktln2.org/2020/08/25/javascriptcore/&quot;&gt;双标记&lt;/a&gt;，按值传递所有数字，其余的作为指针传递。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3 避免数组或对象方法&lt;/h2&gt;
&lt;p&gt;我和大多数人一样喜欢函数式(functional)编程，但除非你在 Haskell/OCaml/Rust 这种能将“函数式代码”高效编译成“机器代码”的环境中工作，否则函数式总是慢于命令式(imperative)的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const result = [1.5, 3.5, 5.0]
  .map((n) =&amp;gt; Math.round(n))
  .filter((n) =&amp;gt; n % 2 === 0)
  .reduce((a, n) =&amp;gt; a + n, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些方法的问题在于：&lt;/p&gt;
&lt;p&gt;1、它们需要生成数组的完整副本，这些副本稍后需要由 gc 释放。（我们在第五节会讨论内存 I/O 的更多细节）
2、它们对 N 个操作遍历 N 次，而 for 循环只需要遍历一次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const numbers = Array.from({ length: 10_000 }).map(() =&amp;gt; Math.random());
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. functional
const result = numbers
  .map((n) =&amp;gt; Math.round(n * 10))
  .filter((n) =&amp;gt; n % 2 === 0)
  .reduce((a, n) =&amp;gt; a + n, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. imperative
let result = 0;
for (let i = 0; i &amp;lt; numbers.length; i++) {
  let n = Math.round(numbers[i] * 10);
  if (n % 2 !== 0) continue;
  result = result + n;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;functional: 36.51%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;imperative: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如 Object.values(), Object.keys() 和 Object.entries() 等方法也存在类似的问题，因为它们也会申请分配更多的数据空间，而内存访问是所有性能问题的根源。&lt;/p&gt;
&lt;p&gt;No really I swear，我会在第五节讨论内存问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者注：原文是：“No really I swear, I’ll show you in section 5.”，总感觉作者很皮。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;4 避免间接访问(Avoid indirection)&lt;/h2&gt;
&lt;p&gt;另一个优化收益的点是减少使用&lt;strong&gt;间接源&lt;/strong&gt;(source of indirection) —— 主要是这三种间接源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用代理&lt;/li&gt;
&lt;li&gt;多层嵌套访问&lt;/li&gt;
&lt;li&gt;多层函数调用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const point = { x: 10, y: 20 };

// 1.
// Proxy 对象难以被优化，因为它们的 get/set 函数可能会运行自定义逻辑，所以引擎无法做出他们的一般假设。
const proxy = new Proxy(point, {
  get: (t, k) =&amp;gt; {
    return t[k];
  },
});
// 有些引擎可以使代理成本消失，但这些优化是昂贵的，并且容易出错。
const x = proxy.x;

// 2.
// 通常被忽视，但通过 `.` 或 `[]` 访问对象也是一个间接源。在简单的情况下，引擎可能会优化掉这些成本：
const x = point.x;
// 但是，每个额外的访问都将增加成本，并且使引擎难以做出关于 point 状态的一般假设：
const x = this.state.circle.center.point.x;

// 3.
// 最后，函数调用也可能有成本。引擎通常能够内联这些函数：
function getX(p) {
  return p.x;
}
const x = getX(p);
// 但是，这并不能确保优化。例如，如果这个函数调用不是来自静态函数：
function Component({ point, getX }) {
  return getX(point);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前，Proxy 的基准测试结果在 V8 上非常糟糕。上次我检查时，代理对象总是从 JIT 回退到解释器，从这些结果来看，情况可能仍然如此。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. proxy access
const point = new Proxy({ x: 10, y: 20 }, { get: (t, k) =&amp;gt; t[k] });

for (let _ = 0, i = 0; i &amp;lt; 100_000; i++) {
  _ += point.x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. direct access
const point = { x: 10, y: 20 };
const x = point.x;

for (let _ = 0, i = 0; i &amp;lt; 100_000; i++) {
  _ += x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;proxy access: 2.8%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;direct access: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我还想展示访问深度嵌套对象与直接访问的对比，但是当存在热循环(hot loop)和常量对象时，引擎非常擅长&lt;a href=&quot;https://www.youtube.com/watch?v=KiWEWLwQ3oI&amp;amp;t=1055s&quot;&gt;通过 escape 分析优化对象访问&lt;/a&gt;。于是我插入了一点间接的东西来防止这种优化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. nested access
const a = { state: { center: { point: { x: 10, y: 20 } } } };
const b = { state: { center: { point: { x: 10, y: 20 } } } };
const get = (i) =&amp;gt; (i % 2 ? a : b);

let result = 0;
for (let i = 0; i &amp;lt; 100_000; i++) {
  result = result + get(i).state.center.point.x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. direct access
const a = { x: 10, y: 20 }.x;
const b = { x: 10, y: 20 }.x;
const get = (i) =&amp;gt; (i % 2 ? a : b);

let result = 0;
for (let i = 0; i &amp;lt; 100_000; i++) {
  result = result + get(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;nested access: 42.08%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;direct access: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5 避免缓存未命中&lt;/h2&gt;
&lt;p&gt;This point requires a bit of low-level knowledge, but has implications even in javascript, so I’ll explain. From the CPU point of view, retrieving memory from RAM is slow. To speed things up, it uses mainly two optimizations.&lt;/p&gt;
&lt;p&gt;虽然这一节讨论的是底层知识，但在 javascript 中也有同样的影响，所以我会解释一下。从 CPU 的角度来看，从 RAM 中检索内存是缓慢的。为了加速，它主要使用两种优化。&lt;/p&gt;
&lt;h3&gt;5.1 预取(refetching)&lt;/h3&gt;
&lt;p&gt;第一种是&lt;strong&gt;预取(refetching)&lt;/strong&gt;：它提前获取更多的内存，并希望这是你感兴趣的内存。&lt;/p&gt;
&lt;p&gt;它总是猜测，如果你请求一个内存地址，那么你将对紧随其后的内存区域感兴趣。所以&lt;strong&gt;按顺序访问数据&lt;/strong&gt;是关键。&lt;/p&gt;
&lt;p&gt;在下面的示例中，我们可以观察到随机访问内存的影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const K = 1024;
const length = 1 * K * K;

// 这些点是一个接一个创建的，所以它们在内存中是按顺序分配的。
const points = new Array(length);
for (let i = 0; i &amp;lt; points.length; i++) {
  points[i] = { x: 42, y: 0 };
}

// 这个数组包含与上面相同的数据，但以随机顺序排列。
const shuffledPoints = shuffle(points.slice());
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. sequential
let _ = 0;
for (let i = 0; i &amp;lt; points.length; i++) {
  _ += points[i].x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. random
let _ = 0;
for (let i = 0; i &amp;lt; shuffledPoints.length; i++) {
  _ += shuffledPoints[i].x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;sequential: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;random: 26.22%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;我应该怎么办？&lt;/h4&gt;
&lt;p&gt;这方面可能是最难付诸实践的，因为 javascript 没有在内存中放置对象的方法。&lt;/p&gt;
&lt;p&gt;但是你可以使用上文例子中的知识去优化：例如在顺序改变之前对数据进行操作。&lt;/p&gt;
&lt;p&gt;你不能假设顺序创建的对象在一段时间后会保持在同一位置，因为 gc 可能会移动它们。&lt;/p&gt;
&lt;p&gt;有一个例外，它是数字数组，最好是 TypedArray 实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// from this
const points = [
  { x: 0, y: 5 },
  { x: 0, y: 10 },
];

// to this
const points = new Int64Array([0, 5, 0, 10]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看更多详细的例子，查看&lt;a href=&quot;https://mrale.ph/blog/2018/02/03/maybe-you-dont-need-rust-to-speed-up-your-js.html#optimizing-parsing---reducing-gc-pressure&quot;&gt;这个链接&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;请注意，它包含一些现在已经过时的优化，但总体上仍然是准确的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5.2 L1/2/3 缓存(Caching in L1/2/3)&lt;/h3&gt;
&lt;p&gt;第二种 CUP 使用的优化是 L1/L2/L3 缓存：它们好比更快的 RAM，但也更昂贵，所以也更小。它们包含 RAM 数据，但充当 LRU 缓存。数据在工作时进入，并在新的工作数据需要空间时写回主 RAM。因此，这里的关键是&lt;strong&gt;使用尽可能少的数据&lt;/strong&gt;来将工作数据集保存在快速缓存中。&lt;/p&gt;
&lt;p&gt;在下面的示例中，我们可以观察破坏每个连续缓存后的影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const KB = 1024;
const MB = 1024 * KB;

const L1 = 256 * KB;
const L2 = 5 * MB;
const L3 = 18 * MB;
const RAM = 32 * MB;

// 对于所有测试用例，我们将访问相同的缓冲区。
// 但是不同的测试用例，只会访问部分区间。比如第一个用例只会访问到 buffer 的 0 - L1 区间，第二个用例只会访问到 0 - L2 区间，以此类推。
const buffer = new Int8Array(RAM);
buffer.fill(42);

const random = (max) =&amp;gt; Math.floor(Math.random() * max);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. L1
let r = 0;
for (let i = 0; i &amp;lt; 100000; i++) {
  r += buffer[random(L1)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. L2
let r = 0;
for (let i = 0; i &amp;lt; 100000; i++) {
  r += buffer[random(L2)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 3. L3
let r = 0;
for (let i = 0; i &amp;lt; 100000; i++) {
  r += buffer[random(L3)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 4. RAM
let r = 0;
for (let i = 0; i &amp;lt; 100000; i++) {
  r += buffer[random(RAM)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;L1: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;L2: 78.06%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;L3: 65.89%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;RAM: 29.33%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;我应该怎么办？&lt;/h4&gt;
&lt;p&gt;消除每一个可以消除的数据或内存分配。数据集越小，程序的运行速度就越快。内存 I/O 是 95% 的程序的瓶颈。另一个好的策略是将你的工作拆分，确保你每次只处理一个小的数据集。&lt;/p&gt;
&lt;p&gt;更多 CPU 和内存的细节信息，可以&lt;a href=&quot;https://people.freebsd.org/~lstewart/articles/cpumemory.pdf&quot;&gt;查看这个&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于不可变数据结构
对于清晰度和正确性来说，不可变性是很好的，但是在性能方面，更新不可变数据结构意味着创建容器的副本，这就需要更多的内存 I/O 来刷新缓存。你应该尽可能避免不可变数据结构。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;关于解构运算 {...rest}
它非常方便，但每次使用它时都会在内存中创建一个新对象。更多的内存 I/O，更慢的缓存！（More memory I/O, slower caches!）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;6 避免大型对象&lt;/h2&gt;
&lt;p&gt;Hashmaps are prone to this because their data is usually randomly &amp;amp; evenly distributed over the memory region they occupy. Let’s see how it behaves with this map of some users indexed by their ID.&lt;/p&gt;
&lt;p&gt;如第 2 节所述，引擎使用结构(shapes)来优化对象。&lt;/p&gt;
&lt;p&gt;但是，当结构变得太大时，引擎别无选择，只能使用常规的 hashmap（如 Map 对象）。&lt;/p&gt;
&lt;p&gt;正如我们在第 5 节中看到的，缓存缺失会显著降低性能。&lt;/p&gt;
&lt;p&gt;hashmap 很容易出现这种情况，因为它们的数据通常是随机且均匀地分布在它们所占用的内存区域上。&lt;/p&gt;
&lt;p&gt;让我们看看它是如何处理这个由用户 ID 索引的用户映射的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const USERS_LENGTH = 1_000;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const byId = {};
Array.from({ length: USERS_LENGTH }).forEach((_, id) =&amp;gt; {
  byId[id] = { id, name: &quot;John&quot; };
});
let _ = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. [] access
Object.keys(byId).forEach((id) =&amp;gt; {
  _ += byId[id].id;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. direct access
Object.values(byId).forEach((user) =&amp;gt; {
  _ += user.id;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;[ ] access: 43.18%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;direct access: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们还可以观察到，随着对象大小的增长，性能是如何不断下降的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const USERS_LENGTH = 100_000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;[ ] access: 20.67%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;direct access: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;我应该怎么办？&lt;/h4&gt;
&lt;p&gt;如上所示，避免频繁索引大型对象。最好事先将对象转换为数组。组织数据以获得模型上的 ID 会有所帮助，因为可以使用 Object.values()，而不必引用键映射来获取 ID。&lt;/p&gt;
&lt;h2&gt;7 使用 eval&lt;/h2&gt;
&lt;p&gt;有些 javascript 模式(patterns)很难针对引擎进行优化，通过使用 eval() 或其衍生物，可以使这些模式消失。&lt;/p&gt;
&lt;p&gt;在这个例子中，我们可以看到 eval() 是如何避免“创建带有动态对象键的对象”的开销的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const key = &quot;requestId&quot;;
const values = Array.from({ length: 100_000 }).fill(42);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. without eval
function createMessages(key, values) {
  const messages = [];
  for (let i = 0; i &amp;lt; values.length; i++) {
    messages.push({ [key]: values[i] });
  }
  return messages;
}

createMessages(key, values);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. with eval
function createMessages(key, values) {
  const messages = [];
  const createMessage = new Function(
    &quot;value&quot;,
    `return { ${JSON.stringify(key)}: value }`
  );
  for (let i = 0; i &amp;lt; values.length; i++) {
    messages.push(createMessage(values[i]));
  }
  return messages;
}

createMessages(key, values);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;without eval: 53.2%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;with eval: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;eval 的另一个好的用例可能是编译一个过滤器谓词函数，在该函数中丢弃你知道永远不会使用的分支。&lt;/p&gt;
&lt;p&gt;一般来说，任何将在热循环中运行的函数都是这种优化的良好候选者。&lt;/p&gt;
&lt;p&gt;显然，关于 eval() 的常见警告仍然适用：不要相信用户输入，对传递到 eval() 代码中的任何内容进行消毒，并且不要创建任何 XSS 的可能性。还要注意，有些环境不允许访问 eval()，例如带有 CSP 的浏览器页面。&lt;/p&gt;
&lt;h2&gt;8 谨慎使用字符串&lt;/h2&gt;
&lt;p&gt;我们已经在前文看到字符串比它们表面上开销更大。&lt;/p&gt;
&lt;p&gt;现在我有一个好消息和一个坏消息，总所周知，当然是先说坏消息：字符串比它们看起来更复杂。然后是好消息：但它们也可以非常有效地被使用。&lt;/p&gt;
&lt;p&gt;基于上下文关系，字符串的操作是 JavaScript 的核心部分。&lt;/p&gt;
&lt;p&gt;为了优化存在大量字符串的代码，JS 引擎的作法必须具有创造性。&lt;/p&gt;
&lt;p&gt;我的意思是，他们必须根据实际情况，使用 c++ 中的&quot;多字符串形式&quot;(multiple string representation)来表示 String 对象。&lt;/p&gt;
&lt;p&gt;这里有两种一般情况值得关注，因为它们适用于 V8(到目前为止最常见的引擎)，通常也适用于其他引擎。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先&lt;/strong&gt;，用 + 连接字符串的操作，不会创建两个输入字符串的副本，而是创建一个指向每个子字符串的指针。&lt;/p&gt;
&lt;p&gt;如果是 typescript 中的话，应该是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class String {
  abstract value(): char[] {}
}

class BytesString {
  constructor(bytes: char[]) {
    this.bytes = bytes
  }
  value() {
    return this.bytes
  }
}

class ConcatenatedString {
  constructor(left: String, right: String) {
    this.left = left
    this.right = right
  }
  value() {
    return [...this.left.value(), ...this.right.value()]
  }
}

function concat(left, right) {
  return new ConcatenatedString(left, right)
}

const first = new BytesString([&apos;H&apos;, &apos;e&apos;, &apos;l&apos;, &apos;l&apos;, &apos;o&apos;, &apos; &apos;])
const second = new BytesString([&apos;w&apos;, &apos;o&apos;, &apos;r&apos;, &apos;l&apos;, &apos;d&apos;])

// 看嘛，没有真的复制!
const message = concat(first, second)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;其次&lt;/strong&gt;，字符串切片的操作，也不需要创建副本：它们可以简单地指向另一个字符串中的范围。&lt;/p&gt;
&lt;p&gt;继续用上面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SlicedString {
  constructor(source: String, start: number, end: number) {
    this.source = source;
    this.start = start;
    this.end = end;
  }
  value() {
    return this.source.value().slice(this.start, this.end);
  }
}

function substring(source, start, end) {
  return new SlicedString(source, start, end);
}

// 这能表示 &quot;He&quot;，但它仍然不包含任何数据副本。
const firstTwoLetters = substring(message, 0, 2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这里有一个问题：一旦你需要去改变这些 bytes，那么就是开销复制成本的时刻。&lt;/p&gt;
&lt;p&gt;让我们回顾 String 类，然后尝试添加一个 .trimend 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class String {
  abstract value(): char[] {}

  trimEnd() {
    const bytes = this.value()
    const result = bytes.slice()
    while (result[result.length - 1] === &apos; &apos;){
      result.pop()
    }
    return new BytesString(result)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么让我们来看一个例子，在这个例子中，我们比较了&lt;strong&gt;使用突变操作&lt;/strong&gt;和&lt;strong&gt;只使用连接操作&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const classNames = [&quot;primary&quot;, &quot;selected&quot;, &quot;active&quot;, &quot;medium&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. mutation
const result = classNames.map((c) =&amp;gt; `button--${c}`).join(&quot; &quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. concatenation
const result = classNames
  .map((c) =&amp;gt; &quot;button--&quot; + c)
  .reduce((acc, c) =&amp;gt; acc + &quot; &quot; + c, &quot;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;mutation: 37.43%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;concatenation: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;我应该怎么办？&lt;/h4&gt;
&lt;p&gt;一般来说，&lt;strong&gt;尽量避免突变&lt;/strong&gt;。这包括 .trim() .replace() 等方法。考虑一下如何避免这些方法。在某些引擎中，字符串模板也可能比 + 操作慢。目前在 V8 中是这样的，但将来可能不会，所以还得靠基准测试。&lt;/p&gt;
&lt;p&gt;关于上面的 SlicedString 的注意事项：如果我们使用的字符串，是内存中的一个非常大的字符串的子字符串，它可能会阻止 gc 收集大字符串！&lt;/p&gt;
&lt;p&gt;如果你正在处理大文本并从中提取小字符串，则可能会泄漏大量内存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const large = Array.from({ length: 10_000 })
  .map(() =&amp;gt; &quot;string&quot;)
  .join(&quot;&quot;);
const small = large.slice(0, 50);
//    ^ will keep `large` alive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的解决方案是使用对我们有利的变异方法。&lt;/p&gt;
&lt;p&gt;如果我们改变小字符串的一个字符，那么小字符串将被强制复制，并且原来指向大字符串的指针将丢失：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// replace a token that doesn&apos;t exist
const small = small.replace(&quot;#&quot;.repeat(small.length + 1), &quot;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有关更多细节，请参阅 &lt;a href=&quot;https://github.com/v8/v8/blob/main/src/objects/string.h&quot;&gt;V8 中的 string.h&lt;/a&gt; 或 &lt;a href=&quot;https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/JSString.h&quot;&gt;JavaScriptCore 中的 JSString.h&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于字符串复杂性:
我很快浏览了一遍，但是有很多实现细节增加了字符串的复杂性。每种字符串表示通常都有最小长度。例如，连接字符串可能不适用于非常小的字符串。或者有时存在限制，例如避免指向子字符串的子字符串。阅读上面链接的 c++ 文件可以很好地了解实现细节 —— 即使只是阅读注释。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;9 使用专业化(specialization)&lt;/h2&gt;
&lt;p&gt;性能优化中的一个重要概念是专业化：调整代码逻辑以适应特定用例的约束。这通常意味着弄清楚哪些条件可能适合你的例子，并针对这些条件进行编码。&lt;/p&gt;
&lt;p&gt;假设我们是一个商家，有时需要将标签添加到产品列表中。根据经验，我们知道标签通常是空的。知道了这些信息，我们就可以针对这种情况对函数进行专门的优化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const descriptions = [&quot;apples&quot;, &quot;oranges&quot;, &quot;bananas&quot;, &quot;seven&quot;];
const someTags = {
  apples: &quot;::promotion::&quot;,
};
const noTags = {};

// 将产品转换成字符串，再加上可能存在的标签
function productsToString(description, tags) {
  let result = &quot;&quot;;
  description.forEach((product) =&amp;gt; {
    result += product;
    if (tags[product]) result += tags[product];
    result += &quot;, &quot;;
  });
  return result;
}

// 现在来优化一下啊
function productsToStringSpecialized(description, tags) {
  // 我们知道 `tags` 很可能是空的，所以我们提前检查一次，然后我们就可以从内部循环中删除 `if` 检查
  if (isEmpty(tags)) {
    let result = &quot;&quot;;
    description.forEach((product) =&amp;gt; {
      result += product + &quot;, &quot;;
    });
    return result;
  } else {
    let result = &quot;&quot;;
    description.forEach((product) =&amp;gt; {
      result += product;
      if (tags[product]) result += tags[product];
      result += &quot;, &quot;;
    });
    return result;
  }
}
function isEmpty(o) {
  for (let _ in o) {
    return false;
  }
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. not specialized
for (let i = 0; i &amp;lt; 100; i++) {
  productsToString(descriptions, someTags);
  productsToString(descriptions, noTags);
  productsToString(descriptions, noTags);
  productsToString(descriptions, noTags);
  productsToString(descriptions, noTags);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. specialized
for (let i = 0; i &amp;lt; 100; i++) {
  productsToStringSpecialized(descriptions, someTags);
  productsToStringSpecialized(descriptions, noTags);
  productsToStringSpecialized(descriptions, noTags);
  productsToStringSpecialized(descriptions, noTags);
  productsToStringSpecialized(descriptions, noTags);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;not specialized: 85.71%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;specialized: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种类型的优化可以只给你适度的改进，但这些会累积起来。它们是对更关键的优化(如形状和内存 I/O)的一个很好的补充。&lt;/p&gt;
&lt;p&gt;但请注意，一旦条件发生变化，反而可能造成负面效果，因此在应用此方法时要小心。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;分支预测和无分支代码:
从代码中删除分支可以非常有效地提高性能。有关分支预测器的更多细节，请阅读经典的堆栈溢出回答&lt;a href=&quot;https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array&quot;&gt;为什么处理排序数组更快&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;10 数据结构&lt;/h2&gt;
&lt;p&gt;我不会详细介绍数据结构，因为它们需要单开一章。&lt;/p&gt;
&lt;p&gt;但是请注意，如果你的用例使用了不正确的数据结构，那么可能会产生&lt;strong&gt;比上述任何优化更大的负面效果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我建议你熟悉一下原生的 Map 和 Set ，然后了解一下链表、优先级队列、树（RB 和 B+），最后尝试用一下。&lt;/p&gt;
&lt;p&gt;但是作为一个简单的例子，让我们比较一下 Array.includes 和 Set.has 在一个小列表中的表现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// setup:
const userIds = Array.from({ length: 1_000 }).map((_, i) =&amp;gt; i);
const adminIdsArray = userIds.slice(0, 10);
const adminIdsSet = new Set(adminIdsArray);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. Array
let _ = 0;
for (let i = 0; i &amp;lt; userIds.length; i++) {
  if (adminIdsArray.includes(userIds[i])) {
    _ += 1;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. Set
let _ = 0;
for (let i = 0; i &amp;lt; userIds.length; i++) {
  if (adminIdsSet.has(userIds[i])) {
    _ += 1;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基准测试结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;Array: 34.27%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;Set: 100%&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如你所见，数据结构的选择会产生非常大的影响。&lt;/p&gt;
&lt;p&gt;我有一个真实的例子：将数组切换为一个链表，&lt;a href=&quot;https://github.com/mui/mui-x/pull/9200&quot;&gt;函数的运行时间从 5 秒减少到 22 毫秒&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;11 基准测试(Benchmarking)&lt;/h2&gt;
&lt;p&gt;把这一节留到最后的原因是，我需要通过前面有趣的章节建立可信度。&lt;/p&gt;
&lt;p&gt;现在我（希望）已经掌握了它，我要告诉你的是：基准测试是优化中最重要的部分。而且不仅是最重要的，也是最难的。&lt;/p&gt;
&lt;p&gt;即使有着 20 年的经验，我有时仍然会创建有缺陷的基准，或者错误地使用分析工具。&lt;/p&gt;
&lt;p&gt;所以无论你想做什么，请先&lt;strong&gt;尽力做正确的基准测试&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;11.0 从最关键的开始(Start with the top)&lt;/h3&gt;
&lt;p&gt;你的首要优化任务，始终是针对运行时占比最大的函数或代码片段。&lt;/p&gt;
&lt;p&gt;如果你花时间优化这些之外的任何东西，那么你就是在浪费时间。&lt;/p&gt;
&lt;h3&gt;11.1 避免微基准测试&lt;/h3&gt;
&lt;p&gt;在生产模式下运行你的代码，然后基于观察的结果进行优化。&lt;/p&gt;
&lt;p&gt;JS 引擎非常复杂，在微基准测试(micro-benchmark)中的表现通常与在真实场景中的表现不同。&lt;/p&gt;
&lt;p&gt;例如，以这个微基准测试为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a = { type: &quot;div&quot;, count: 5 };
const b = { type: &quot;span&quot;, count: 10 };

function typeEquals(a, b) {
  return a.type === b.type;
}

for (let i = 0; i &amp;lt; 100_000; i++) {
  typeEquals(a, b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你前文有注意看的话，就会意识到，这里 JS 引擎会专门为这个结构({type: string， count: number})优化函数。&lt;/p&gt;
&lt;p&gt;但这个说法在实际用例中还能成立呢？a 和 b 总是那种结构吗，还是你会得到任何其他结构？&lt;/p&gt;
&lt;p&gt;如果你的函数，在生产中接收到很多种结构，那么该函数的行为将有所不同。&lt;/p&gt;
&lt;h3&gt;11.2 怀疑结果&lt;/h3&gt;
&lt;p&gt;如果你刚刚优化了一个函数，现在它的运行速度提高了 100 倍，先怀疑一下。&lt;/p&gt;
&lt;p&gt;尝试推翻你的结果，在生产模式下测试，给它增加负载。&lt;/p&gt;
&lt;p&gt;同样地，也要对你的工具保持怀疑。&lt;/p&gt;
&lt;p&gt;仅仅通过 devtools 观察一个基准测试就可能会改变它的行为。&lt;/p&gt;
&lt;h3&gt;11.3 选择你的目标&lt;/h3&gt;
&lt;p&gt;不同的引擎对某些模式的优化效果可能会有所不同。你应该针对与你相关的引擎进行基准测试，并优先考虑哪个引擎对你更重要。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/babel/babel/pull/16357&quot;&gt;这是 Babel 中的一个真实例子&lt;/a&gt;，其中改进 V8 意味着降低 JSC 的性能。&lt;/p&gt;
&lt;h2&gt;12 分析和工具&lt;/h2&gt;
&lt;p&gt;关于性能分析和开发工具的各种说明。&lt;/p&gt;
&lt;h3&gt;12.1 浏览器陷阱&lt;/h3&gt;
&lt;p&gt;如果你在浏览器中进行性能分析，请确保使用空白的浏览器配置文件。我甚至使用一个单独的浏览器来进行这项工作。&lt;/p&gt;
&lt;p&gt;如果你在进行性能分析时启用了浏览器扩展，那么它们可能会干扰测量结果。尤其是 React devtools 会显著影响结果，渲染代码的速度可能比用户看到的更慢。&lt;/p&gt;
&lt;h3&gt;12.2 样品分析 vs 结构分析&lt;/h3&gt;
&lt;p&gt;浏览器分析工具(profiling tools)是基于样本的分析工具，它定期对你的堆栈进行采样。&lt;/p&gt;
&lt;p&gt;这有一个很大的缺点：在这些样本之间可能会调用非常小但非常频繁的函数，并且可能在你得到的堆栈图中被严重低估。&lt;/p&gt;
&lt;p&gt;可以使用 Firefox 开发者工具并设置自定义采样间隔，或使用 Chrome 开发者工具的 CPU 降速功能来缓解这个问题。&lt;/p&gt;
&lt;h3&gt;12.3 业内工具&lt;/h3&gt;
&lt;p&gt;除了常规的浏览器开发工具，了解这些选项可能会有所帮助：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Chrome 开发工具有很多&lt;a href=&quot;https://github.com/iamakulov/devtools-perf-features&quot;&gt;实验性选项&lt;/a&gt;，可以帮助你找出性能缓慢的原因。样式失效跟踪器在需要调试浏览器中的样式或布局重新计算时非常有用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/microsoft/deoptexplorer-vscode&quot;&gt;deoptexplorer-vscode&lt;/a&gt; 扩展允许你加载 V8/chromium 日志文件以了解代码何时触发去优化，比如当你将不同的结构传递给函数的时候。虽然你不用该扩展也可以读取日志文件，但它让体验更加友好。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你可以为每个 JS 引擎编译 debug shell，以此更详细地了解其工作原理。这样可以运行 perf 等底层工具，还可以检查每个引擎生成的字节码和机器代。
&lt;a href=&quot;https://mrale.ph/blog/2018/02/03/maybe-you-dont-need-rust-to-speed-up-your-js.html#getting-the-code&quot;&gt;V8 示例&lt;/a&gt;|&lt;a href=&quot;https://zon8.re/posts/jsc-internals-part1-tracing-js-source-to-bytecode/&quot;&gt;示例 JSC 示例&lt;/a&gt;|示例 SpiderMonkey&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;希望你学到了一些有用的技巧。如果你有任何评论、更正或问题，请在页脚发电子邮件。我总是很高兴收到读者的反馈或问题。&lt;/p&gt;
</content:encoded></item><item><title>JavaScript学习笔记</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/js-snack/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/js-snack/</guid><pubDate>Tue, 01 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;零散笔记&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;web 前端三层&lt;/strong&gt;：（w3c 规范）
结构层 HTML 从语义的角度，描述页面结构。
样式层 CSS 从审美的角度，美化页面。
行为层 JavaScript 从交互的角度，提升用户体验。&lt;/p&gt;
&lt;p&gt;加入 script 属性后，网页会产生弹窗。&lt;/p&gt;
&lt;h2&gt;script 属性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;alter(&quot;&quot;);&lt;/strong&gt;
显示警告弹窗信息，双引号内信息即为弹窗内信息。
如果页面上出现弹框，此时页面位置不能改变，并且不能关闭，进程被阻塞。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;type=&lt;/strong&gt;&quot;text/javascript&quot;
可以省略。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sync=&lt;/strong&gt;&quot;sync&quot;
同步：理解为单线程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;async=&lt;/strong&gt;&quot;async&quot;
异步：理解为多线程
立即异步下载外部 js ，不影响其他页面操作，js 下载完毕立即执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;defer=&lt;/strong&gt;&quot;defer&quot;
异步：区别于 async
脚本延迟到文档完全被解析和显示后再执行；
只有外部脚本可以使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;console.log(&quot;&quot;);&lt;/strong&gt;
在浏览器控制台中输出一句话。
作用：进行代码的调试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;prompt(&quot;&quot;);&lt;/strong&gt;
在页面上弹出一个提示输入框，提示信息为双引号间内容。&lt;/p&gt;
&lt;h2&gt;JavaScript 语法&lt;/h2&gt;
&lt;p&gt;** 变量申明与赋值**
JavaScript 是弱类型脚本语言。
var a = &quot;123&quot;;
var b = 123;
变量的类型由内部的数据的类型决定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;变量规则与规范&lt;/strong&gt;
规则： 1.由字母、数字、下划线、$组成 2.不能是关键字和保留字 3.区分大小写
规范：
驼峰命名法，第一个单词首字母小写，之后单词首字母大写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;string
注意，字符串的不可变性。
var str = &quot;one&quot;
var str = str + &quot;two&quot;
//第二句会分配一个新的栈空间&lt;/p&gt;
&lt;p&gt;number
数值范围：
最小值：Number.MIN_VALUE 5e-324
最大值：Number.MAX_VALUE
1.7976931348623157e+308
无穷大：infinity
无穷小：-infinity&lt;/p&gt;
&lt;p&gt;boolean
取值范围：
true and false&lt;/p&gt;
&lt;p&gt;undefined，变量声明但未赋值&lt;/p&gt;
&lt;p&gt;null&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数值检测&lt;/strong&gt;
Number.NaN 是一个特殊值，说明某些算术运算（如求负数的平方根）的结果不是数字。
函数 isNan() 返回值 boolean 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型转换&lt;/strong&gt;
转换成字符串方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数 toString()&lt;/li&gt;
&lt;li&gt;函数 String&lt;/li&gt;
&lt;li&gt;字符串拼接：string + number 得到 string 类型&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;转换成数字：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数 Number()&lt;/li&gt;
&lt;li&gt;函数 parseInt()&lt;/li&gt;
&lt;li&gt;函数 parseFloat()&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;表达式&lt;/strong&gt;
由数据、变量、运算符组成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;运算符&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;赋值运算符&lt;/li&gt;
&lt;li&gt;算数运算符&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;转义符&lt;/strong&gt;
右斜杠
换行 \n&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;函数&lt;/strong&gt;
实例：
function function_name(parmeter1,parameter2){
var variable = parmeter1 + parameter2
return variable
}
注意：没有 return 时，默认返回 undefined。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对象&lt;/strong&gt;
封装、继承、多态
定义：
var object_name = {
name:&quot;Tim&quot;,
age: 18,
height: 180
};
属性用键值对表示。&lt;/p&gt;
&lt;h2&gt;动态交互&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;按钮&lt;/strong&gt;：input 或者 button 标签
&lt;strong&gt;点击事件&lt;/strong&gt;：onclick
&lt;strong&gt;获取用户输入&lt;/strong&gt;：prompt()
&lt;strong&gt;内容检测&lt;/strong&gt;：if..else
&lt;strong&gt;提示操作&lt;/strong&gt;：alert()&lt;/p&gt;
&lt;h2&gt;JavaScript 补充&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;button&quot; value=&quot;变&quot; id=&quot;btn&quot; /&amp;gt;
//创建一个按钮
&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
  var btn = document.getElementById(&quot;btn&quot;);
  //获取id
  btn.onclick = function () {
    alert(&quot;点击时弹出此框！&quot;);
  };
  //点击按钮时触发此框
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;单行注释&lt;/strong&gt;
用双斜杠表示
添加或删除注释的快捷键一般为 Ctrl+/
&lt;strong&gt;多行注释&lt;/strong&gt;
/**/ 中间包含注释内容&lt;/p&gt;
&lt;h2&gt;浏览器工作模式&lt;/h2&gt;
&lt;p&gt;很有深度的文章，不求一次看懂。
（链接失效）&lt;/p&gt;
&lt;h2&gt;JavaScript 存放位置&lt;/h2&gt;
&lt;p&gt;如果没有 style，代码放在 title 标签下，反之，放在 style 下。&lt;/p&gt;
</content:encoded></item><item><title>flex 实现时间轴</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/timeline-flexbox/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/timeline-flexbox/</guid><pubDate>Sat, 12 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;译者述&lt;/h2&gt;
&lt;p&gt;1、原文来自 Jonas Hietala 个人网站上的一篇文章： &lt;a href=&quot;https://www.jonashietala.se/blog/2024/08/25/a_simple_timeline_using_css_flexbox/&quot;&gt;A simple timeline using CSS flexbox&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2、这是一篇关于 CSS flex 布局的具体应用, 发布于 2024 年 8 月 25 日。&lt;/p&gt;
&lt;p&gt;3、翻译中会尽量遵照原意, 也会加入译者的技术注释, 以及选择更符合汉语文法的译句。&lt;/p&gt;
&lt;p&gt;4、欢迎指正，非常感谢。&lt;/p&gt;
&lt;h1&gt;正文&lt;/h1&gt;
&lt;p&gt;当我在个人网站中添加 now 页面时，我也觉得应该更新一下我的 about 页面。它应该有一个简洁的时间轴来表示我人生中的大事件。&lt;/p&gt;
&lt;p&gt;令人惊喜的是，这并不是一件困难的事情，甚至能够很优雅的实现 —— CSS 的 flexbox 真是一个很棒的特性。在本文中，我会引导你来实现这样的时间轴：&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;180px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-codepart.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：如果你使用屏幕阅读器来阅读本文，那么 HTML 示例（如上）将不能正常展示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者注：这里是 iframe 的例子，里面就有完整的代码，直接 F12 查看接口就能获取 html 文件。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HTML 结构&lt;/h2&gt;
&lt;p&gt;我喜欢先写 html，再引入样式。我用两层容器（timeline 和 events）来包装多种事件（event），每个事件包含一个记号（svg）和一份详情（content），每个详情包含了时间和文字。&lt;/p&gt;
&lt;p&gt;如下结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;timeline&quot;&amp;gt;
  &amp;lt;div class=&quot;events&quot;&amp;gt;
    &amp;lt;!-- The first `1989` event --&amp;gt;
    &amp;lt;div class=&quot;event life&quot;&amp;gt;
      &amp;lt;!-- The circle is an svg --&amp;gt;
      &amp;lt;svg
        class=&quot;marker&quot;
        xmlns=&quot;http://www.w3.org/2000/svg&quot;
        width=&quot;12&quot;
        height=&quot;12&quot;
      &amp;gt;
        &amp;lt;circle cx=&quot;6&quot; cy=&quot;6&quot; r=&quot;6&quot;&amp;gt;&amp;lt;/circle&amp;gt;
      &amp;lt;/svg&amp;gt;
      &amp;lt;!-- The event info --&amp;gt;
      &amp;lt;div class=&quot;content&quot;&amp;gt;
        &amp;lt;time&amp;gt;1989&amp;lt;/time&amp;gt;
        &amp;lt;div class=&quot;text&quot;&amp;gt;
          &amp;lt;p&amp;gt;I was born in the north of Sweden&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- etc ... --&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一条简单的线&lt;/h2&gt;
&lt;p&gt;让我们来看看时间轴中实际的线。我选择使用 events::before 伪元素，设置一下宽高，然后模拟一条线。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.events::before {
  /* // We need some content for the element to show up. */
  content: &quot;&quot;;
  /* // Use absolute positioning to place the timeline at the very top. */
  position: absolute;
  top: 0;
  /* // With a height and with the timeline will be a tall and thin box. */
  height: 100%;
  width: 1px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：我们需要设置容器 events 的相对定位，否则时间轴会相对页面来定位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.events {
  position: relative;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我还添加一些样式，这样更好看一点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* // For the tutorial I use slightly different colors, */
/* // but you get the idea. */
.events::before {
  background: white;
}
/* // Events use different classes to differentiate them. */
.event.life .marker {
  fill: yellow;
}
.event.programming .marker {
  fill: magenta;
}
.event.family .marker {
  fill: red;
}
/* // Make the time stand out */
.content time {
  font-family: concourse_4, Helvetica, sans-serif;
  font-weight: bold;
}
/* // Just some extra spacing to make the timeline not merge */
/* // with the surrounding text. */
.events {
  margin: 0.5em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们就得到了一个时间轴：&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;270px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-linepart.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;h2&gt;对齐&lt;/h2&gt;
&lt;p&gt;圆圈和事件信息没有对齐，我们来修复它。&lt;/p&gt;
&lt;p&gt;通过使用 flexbox 布局，事件信息可以水平展示（圆圈在左，事件信息在右）。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;240px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-alignment-1.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event {
  display: flex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更接近了，但是圆圈似乎偏移了。注意，圆圈是一个宽高 12px 的 svg，默认是 (0,0)的定位。&lt;/p&gt;
&lt;p&gt;通过相对定位，可以将圆心更好的对齐：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event .marker {
  position: relative;
  left: -6px;
  top: 6px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;240px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-alignment-2.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;p&gt;但如果你仔细观察，上面扔没有对齐。事实证明，&lt;a href=&quot;https://tonsky.me/blog/centering/&quot;&gt;确定中心问题&lt;/a&gt;是计算机科学中最难的问题，所以不必气馁。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我发现 align-items: baseline 比微调 top 位置更有效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event .marker {
  position: relative;
  left: -6px;
  top: 0px;
}
.event {
  align-items: baseline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;240px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-alignment-3.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;p&gt;(现在的对齐效果就差强人意了，至少这个字体上看起来是这样的。)&lt;/p&gt;
&lt;h2&gt;垂直间距&lt;/h2&gt;
&lt;p&gt;如果感觉有点狭窄，我们让间距更大一点。一种办法是简单地添加 margin-bottom:1em; 但是这会在最后一个事件消息后面加上无用的间距（并且还不能去除）。&lt;/p&gt;
&lt;p&gt;我想到一个简洁的办法就是利用 flexbox 和 row-gap 去指定元素之间的间距。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.timeline-5 {
  .events {
    display: flex;
    /* // Lay out events column-wise instead of row-wise. */
    flex-direction: column;
    /* // Set some spacing between elements. */
    row-gap: 1em;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;280px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-verticalspacing-1.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;h2&gt;设置响应式布局&lt;/h2&gt;
&lt;p&gt;我们现在的样式对于小屏幕来说很友好，但是对于大屏幕来说却不同。对于大屏幕，我想把这条线放在中间，把一些事件移到左边，另一些移到右边。&lt;/p&gt;
&lt;p&gt;我使用媒体查询设置边界：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (min-width: 700px) {
  /* // Styling for wider screens goes here. */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though I won&apos;t include the media query in the following code snippets the media query should wrap them all.&lt;/p&gt;
&lt;p&gt;下文我就不再重复写媒体查询的代码片段了，但是你应该知道，这些代码是被包含在媒体查询结构代码里面的。&lt;/p&gt;
&lt;h3&gt;事件移动靠左&lt;/h3&gt;
&lt;p&gt;首先我想要做的就是将时间轴移动到中间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.events::before {
  /* // This centers the line horizontally. */
  /* // Remember that we used absolute positioning before. */
  left: 50%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;280px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-making-responsive-1.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;p&gt;（可以试试使用更宽的屏幕来查看显示效果）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者注：手动拉伸一下，对比效果。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, let&apos;s move the marker to the timeline. First lets move the marker to be after the content in the layout ordering:&lt;/p&gt;
&lt;p&gt;现在，让我们把圈圈移动到时间轴上。&lt;/p&gt;
&lt;p&gt;第一步，将圈圈移动到文本的后面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event .marker {
  order: 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;280px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-making-responsive-2.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;p&gt;第二步，我们会让内容占据左边的所有空间，这样圈圈就会被推到中间去：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event .content {
  width: 50%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;280px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-making-responsive-3.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;p&gt;第三步，我们让内容向右对齐。注意增加内边距，防止和圈圈重合:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event .content {
  text-align: right;
  padding-inline: 1em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;280px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-making-responsive-4.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;h3&gt;事件移动到右侧&lt;/h3&gt;
&lt;p&gt;为了将事件移动到时间轴的右侧，我们需要修改 flexbox 布局，用从右到左的方式来布局元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* // Use `nth-child(even)` to target every other event. */
.event:nth-child(even) {
  /* // Layout elements from right to left. */
  flex-direction: row-reverse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者注：这里的 nth-child 是指选择第几个元素，偶数就是 even，奇数就是 odd。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;小提示：对于 blog 我不喜欢按照奇偶来分两侧，而是按照内容来分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event:is(.programming, .work, .projects) {
  flex-direction: row-reverse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了显示好看，右侧的事件内容需要坐对齐：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.event:nth-child(even) {
  .content {
    text-align: left;
  }
  /* // The marker used to be offset -6px, but now we */
  /* // move from the right. */
  .marker {
    left: 6px;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
width=&quot;100%&quot;
height=&quot;280px&quot;
src=&quot;/snofly-blog/files/timeline/timeline-making-responsive-5.html&quot;
title=&quot;A simple timeline using CSS flexbox&quot;
frameborder=&quot;0&quot;
allowfullscreen=&quot;allowfullscreen&quot;
/&amp;gt;&amp;lt;br/ &amp;gt;&lt;/p&gt;
&lt;h2&gt;完成&lt;/h2&gt;
&lt;p&gt;这就是我所使用的时间轴啦。当然，你可以进一步修改和扩展它，只是我个人非常喜欢这种简单的样式。&lt;/p&gt;
&lt;p&gt;使用 flexbox，创建一个基本的时间轴是相当简单的。flex 是我最喜欢的 CSS 特性之一，它解决了许多以前难以处理的问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;下面是全部的样式文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 略 */
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者注：完整的样式文件从上面的示例（iframe 引入的 html 文件）中获取即可。）&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>由虚拟列表引发的思考</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/virtul-list/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/virtul-list/</guid><pubDate>Wed, 25 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;一般认为：超过 1000 行 + 多列（10+），或者含复杂内容的表格，就可以开始视为“大数据量”，需要进行优化考虑。&lt;/p&gt;
&lt;p&gt;对于大数据量的表格页面，我们一般考虑使用虚拟列表去渲染可视区域的行列 dom 元素，这样在性能上能够得到很大的提升。&lt;/p&gt;
&lt;p&gt;本文将给出虚拟列表的实现原理，然后讨论在哪些方面提升了性能，以及到底提升了多少（量化）。&lt;/p&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;前端三件套基础知识&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文使用 html、css、js 编写 demo 代码，专注研究虚拟列表的实现原理。&lt;/p&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;p&gt;理解虚拟滚动原理，提高对浏览器的认知。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能够实现虚拟列表代码&lt;/li&gt;
&lt;li&gt;了解浏览器的渲染机制&lt;/li&gt;
&lt;li&gt;了解性能测量方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;rc-virtual-list 中的实现原理&lt;/h1&gt;
&lt;p&gt;本文考虑两种情况下的虚拟列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列表项高度固定&lt;/li&gt;
&lt;li&gt;列表项高度不固定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;针对不同的情况，我们会采取不同的策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列表项高度固定：我们会根据可视区域的高度，计算出可视区域内最多能展示多少个列表项。然后我们只渲染这部分列表项，超出可视区域的列表项不渲染。&lt;/li&gt;
&lt;li&gt;列表项高度不固定：在这种情况下，我们无法直接根据可视区域的高度来计算出最多能展示多少个列表项，还需要增加额外的处理逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先看看常用工具库中，是怎么设计的。(本文使用 rc-virtual-list@3.19.1 版本，不同版本可能存在差异，建议使用同一版本，便于查看源代码)&lt;/p&gt;
&lt;h2&gt;列表项目高度固定&lt;/h2&gt;
&lt;p&gt;在 rc-virtual-list 中，做了这三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算可见区域索引&lt;/li&gt;
&lt;li&gt;设置偏移量和虚拟容器总高度&lt;/li&gt;
&lt;li&gt;只渲染 visible + buffer 的数据项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对&lt;strong&gt;列表项高度固定&lt;/strong&gt;的情况，根据滚动条位置，它能很容易计算到需要显示的列表项的索引范围。&lt;/p&gt;
&lt;p&gt;我们查看相关源代码：&lt;/p&gt;
&lt;p&gt;1、从 /lib/index.js 打开工具包，看到实际引用的工具：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var _List = _interopRequireDefault(require(&quot;./List&quot;));
var _default = (exports.default = _List.default);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、查看 List 代码，会看到 .d.ts 中参数定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export interface ListProps&amp;lt;T&amp;gt;
  extends Omit&amp;lt;React.HTMLAttributes&amp;lt;any&amp;gt;, &quot;children&quot;&amp;gt; {
  // ... 其他代码
  height?: number;
  itemHeight?: number;
  // ... 其他代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们关注 &lt;code&gt;height&lt;/code&gt; 和 &lt;code&gt;itemHeight&lt;/code&gt; 这两个参数。&lt;/p&gt;
&lt;p&gt;如果读者有使用经验，就知道 itemHeight 就是我们的列表项高度。&lt;/p&gt;
&lt;p&gt;3、我们继续查看 List.js 代码：&lt;/p&gt;
&lt;p&gt;有 500 多行，好在我们现在可以通过 itemHeight 参数快速了解代码流程，所以我们可以直接检索 itemHeight 的相关用法即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里列出找到的关键代码 1 —— 虚拟滚动的开关：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var useVirtual = !!(virtual !== false &amp;amp;&amp;amp; height &amp;amp;&amp;amp; itemHeight);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 virtual 参数我们不传递，就是 undefined ，默认是开启的。height 和 itemHeight 是开启虚拟滚动的必要参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;然后是相关变量的使用，这里给出关键代码 2 —— 计算显示的数据的区间：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var _React$useMemo = React.useMemo(function () {
      // ... 其他代码
      var itemTop = 0;
      var startIndex;
      var startOffset;
      var endIndex;
      var dataLen = mergedData.length;
      for (var i = 0; i &amp;lt; dataLen; i += 1) {
        var _item = mergedData[i];
        var key = getKey(_item);
        var cacheHeight = heights.get(key);
        var currentItemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);

        // Check item top in the range
        if (currentItemBottom &amp;gt;= offsetTop &amp;amp;&amp;amp; startIndex === undefined) {
          startIndex = i;
          startOffset = itemTop;
        }

        // Check item bottom in the range. We will render additional one item for motion usage
        if (currentItemBottom &amp;gt; offsetTop + height &amp;amp;&amp;amp; endIndex === undefined) {
          endIndex = i;
        }
        itemTop = currentItemBottom;
      }
      // ... 其他代码
      return {
        scrollHeight: itemTop,
        start: startIndex,
        end: endIndex,
        offset: startOffset
      };
    }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，这里有个关键的触发逻辑：当滚动条高度变化的时候，重新计算需要显示的列表项的范围。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后是对获取到的变量的渲染，这里是关键代码 3 —— 渲染：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;(scrollHeight = _React$useMemo.scrollHeight),
  (start = _React$useMemo.start),
  (end = _React$useMemo.end),
  (fillerOffset = _React$useMemo.offset);
// ================================ Render ================================
var listChildren = (0, _useChildren.default)(
  mergedData,
  start,
  end,
  scrollWidth,
  offsetLeft,
  setInstanceRef,
  children,
  sharedConfig
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;了解这三段代码就能知道 rc-virtual-list 整体设计了，然后可以进一步阅读源代码。&lt;/p&gt;
&lt;h2&gt;列表项目高度不固定&lt;/h2&gt;
&lt;p&gt;对于不确定的高度的情况，仍然需要传递 itemHeight 参数，用于开启虚拟渲染。itemHeight 还有一个作用，作为初始化渲染时估算的高度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var useVirtual = !!(virtual !== false &amp;amp;&amp;amp; height &amp;amp;&amp;amp; itemHeight);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们查看源代码 .d.ts 中 List 的参数，并没有看到“标记是否是等高列表项”的参数。&lt;/p&gt;
&lt;p&gt;所以 rc-virtual-list 并没有专门区分列表项是等高还是不等高，而是统一通过一套逻辑来处理。&lt;/p&gt;
&lt;p&gt;于是，很容易想到，对于不确定的高度的情况，还有额外的逻辑处理。&lt;/p&gt;
&lt;p&gt;从上面的“关键代码 2”中，我们关注这两句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var cacheHeight = heights.get(key);
var currentItemBottom =
  itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，比 itemHeight 优先级更高的是 cacheHeight ，显然这是最终的列表项的高度。&lt;/p&gt;
&lt;p&gt;我们可以在 List.js 中查找 heights 这个变量相关代码，就能找到对于不等高列表项的处理逻辑。&lt;/p&gt;
&lt;p&gt;关键代码 4 —— 列表项最终高度的计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ================================ Height ================================
var _useHeights = (0, _useHeights3.default)(getKey, null, null),
  _useHeights2 = (0, _slicedToArray2.default)(_useHeights, 4),
  setInstanceRef = _useHeights2[0],
  collectHeight = _useHeights2[1],
  heights = _useHeights2[2],
  heightUpdatedMark = _useHeights2[3];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进一步查看 _useHeights3 和 _slicedToArray2 的定义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var _slicedToArray2 = _interopRequireDefault(
  require(&quot;@babel/runtime/helpers/slicedToArray&quot;)
);
var _useHeights3 = _interopRequireDefault(require(&quot;./hooks/useHeights&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查阅网上资料：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;_slicedToArray 是一个帮助函数，用于将一个可迭代对象（比如数组或类数组对象）转化为一个数组，并且允许你通过解构赋值的方式获取其中的元素。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;显然，关键代码在 useHeights.js 文件中，它负责动态记录并更新每个列表项的实际高度。&lt;/p&gt;
&lt;p&gt;关键代码 5 —— 缓存高度的更新方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ... 其他代码
var instanceRef = (0, _react.useRef)(new Map());
var heightsRef = (0, _react.useRef)(new _CacheMap.default());
// ... 其他代码
function collectHeight() {
  var sync =
    arguments.length &amp;gt; 0 &amp;amp;&amp;amp; arguments[0] !== undefined ? arguments[0] : false;
  cancelRaf();
  var doCollect = function doCollect() {
    var changed = false;
    instanceRef.current.forEach(function (element, key) {
      if (element &amp;amp;&amp;amp; element.offsetParent) {
        var offsetHeight = element.offsetHeight;
        var _getComputedStyle = getComputedStyle(element),
          marginTop = _getComputedStyle.marginTop,
          marginBottom = _getComputedStyle.marginBottom;
        var marginTopNum = parseNumber(marginTop);
        var marginBottomNum = parseNumber(marginBottom);
        var totalHeight = offsetHeight + marginTopNum + marginBottomNum;
        if (heightsRef.current.get(key) !== totalHeight) {
          heightsRef.current.set(key, totalHeight);
          changed = true;
        }
      }
    });

    // Always trigger update mark to tell parent that should re-calculate heights when resized
    if (changed) {
      setUpdatedMark(function (c) {
        return c + 1;
      });
    }
  };
  if (sync) {
    doCollect();
  } else {
    promiseIdRef.current += 1;
    var id = promiseIdRef.current;
    Promise.resolve().then(function () {
      if (id === promiseIdRef.current) {
        doCollect();
      }
    });
  }
}
// ... 其他代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里，逻辑就很明确了：（朴实无华的操作）获取列表项元素的高度，然后缓存起来。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;我们查阅了 rc-virtual-list 源代码，根据 itemHeight 参数一路追踪相关代码逻辑，最终梳理得到了它的实现原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1、初始化计算可见区域
根据传入的 height（容器高度）与 itemHeight（预估项高）计算出可视区域最多可展示多少项（visible count），并结合缓冲区（buffer）确定初始渲染范围。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2、设置容器偏移与占位高度
通过 transform: translateY(...) 设置可见项的起始偏移位置，通过一个填充元素 (Filler) 设置虚拟列表总高度，确保滚动条正常。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3、监听滚动事件，实时计算渲染范围
滚动时，根据 scrollTop 和（预估或真实）高度缓存，遍历计算可视区域的起始和结束索引，更新渲染项。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4、收集并缓存真实 DOM 高度
利用 useHeights 钩子，监听每个 item 的 DOM 元素变化（包括高度和 margin），并缓存其实际高度，如果高度发生变化会触发重新计算。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5、动态同步滚动偏移，修正误差
如果发现首项真实高度与 itemHeight 不一致，会在下一帧中自动调整 scrollTop，防止滚动跳动或错位。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;6、默认开启虚拟滚动模式
除非手动设置 virtual={false}，只要设置了 height 与 itemHeight，就会默认启用虚拟渲染逻辑。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，1、3、4 步骤就是我们主要追踪的源码的逻辑。&lt;/p&gt;
&lt;h1&gt;编码实现 demo&lt;/h1&gt;
&lt;p&gt;参考了别人的代码，我们可以自己尝试编写代码了。（不是 cv 不是 cv 不是 cv）&lt;/p&gt;
&lt;p&gt;这是一份基于 &lt;strong&gt;原生 JavaScript 和 DOM 操作&lt;/strong&gt; 实现的「虚拟列表」示例，支持 &lt;strong&gt;不等高列表项&lt;/strong&gt; 和 &lt;strong&gt;平滑滚动体验&lt;/strong&gt;，整体思路参考了 &lt;code&gt;rc-virtual-list&lt;/code&gt; 的核心设计。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Virtual List - Smooth Scrolling with Better Buffer&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      #container {
        height: 400px;
        overflow-y: auto;
        border: 1px solid #ccc;
        position: relative;
      }

      .item {
        box-sizing: border-box;
        padding: 10px;
        margin: 4px 0;
        background: #f9f9f9;
        border: 1px solid #ddd;
        position: absolute;
        width: calc(100% - 8px);
        left: 4px;
      }

      #spacer {
        position: absolute;
        width: 100%;
        left: 0;
        top: 0;
        z-index: -1;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;container&quot;&amp;gt;
      &amp;lt;div id=&quot;spacer&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;div id=&quot;list&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
      // 总数据量
      const TOTAL = 10000;

      // 估算每项平均高度（未测量前使用）
      const ESTIMATED_HEIGHT = 50;

      // 向上向下缓冲渲染多少个额外元素（增加缓冲区以提高平滑度）
      const BUFFER = 15;

      // DOM 元素引用
      const container = document.getElementById(&quot;container&quot;);
      const list = document.getElementById(&quot;list&quot;);
      const spacer = document.getElementById(&quot;spacer&quot;);

      // 模拟不等高数据
      const data = Array.from({ length: TOTAL }, (_, i) =&amp;gt; ({
        id: i,
        text: `Item ${i} - 这是一个测试项目，内容长度不同以模拟真实场景`,
        height: 30 + Math.floor(Math.random() * 50), // 高度 30~80 之间
      }));

      // 高度缓存（id =&amp;gt; height）
      const heights = new Map();

      // 每一项的偏移位置（计算 top）
      const positions = [];

      // 当前渲染的元素范围
      let currentRange = { start: 0, end: 0 };

      // 已渲染的DOM元素缓存
      const renderedElements = new Map();

      // 计算所有项的 top 偏移位置
      function calcPositions() {
        positions.length = 0;
        let top = 0;
        for (let i = 0; i &amp;lt; data.length; i++) {
          const h = heights.get(i) || ESTIMATED_HEIGHT;
          positions.push(top);
          top += h;
        }
        spacer.style.height = top + &quot;px&quot;; // 设置撑高容器高度
      }

      // 使用二分查找快速定位当前 scrollTop 对应的可视起始项
      function findStartIndex(scrollTop) {
        let low = 0;
        let high = data.length - 1;
        while (low &amp;lt;= high) {
          const mid = Math.floor((low + high) / 2);
          const midTop = positions[mid];
          const midBottom = midTop + (heights.get(mid) || ESTIMATED_HEIGHT);
          if (midBottom &amp;lt;= scrollTop) {
            low = mid + 1;
          } else {
            high = mid - 1;
          }
        }
        return Math.max(0, low - BUFFER); // 加 buffer
      }

      // 创建单个列表项元素
      function createElement(index) {
        const item = data[index];
        const div = document.createElement(&quot;div&quot;);
        div.className = &quot;item&quot;;
        div.innerText = item.text;
        div.style.height = item.height + &quot;px&quot;;
        div.dataset.index = index;
        return div;
      }

      // 渲染可见范围内的项
      function render() {
        const scrollTop = container.scrollTop;
        const containerHeight = container.clientHeight;

        calcPositions();

        const start = findStartIndex(scrollTop);
        const visibleEnd = scrollTop + containerHeight;

        // 计算结束索引，增加更多缓冲
        let end = start;
        while (
          end &amp;lt; data.length &amp;amp;&amp;amp;
          positions[end] &amp;lt; visibleEnd + BUFFER * ESTIMATED_HEIGHT
        ) {
          end++;
        }
        end = Math.min(end + BUFFER, data.length);

        // 如果渲染范围变化不大，避免重新渲染
        if (
          Math.abs(currentRange.start - start) &amp;lt; BUFFER / 2 &amp;amp;&amp;amp;
          Math.abs(currentRange.end - end) &amp;lt; BUFFER / 2
        ) {
          return;
        }

        // 移除不再需要的元素
        const toRemove = [];
        renderedElements.forEach((element, index) =&amp;gt; {
          if (index &amp;lt; start || index &amp;gt;= end) {
            toRemove.push(index);
            list.removeChild(element);
          }
        });
        toRemove.forEach((index) =&amp;gt; renderedElements.delete(index));

        // 添加新需要的元素
        for (let i = start; i &amp;lt; end; i++) {
          if (!renderedElements.has(i)) {
            const element = createElement(i);
            element.style.transform = `translateY(${positions[i]}px)`;
            renderedElements.set(i, element);
            list.appendChild(element);
          } else {
            // 更新已存在元素的位置
            const element = renderedElements.get(i);
            element.style.transform = `translateY(${positions[i]}px)`;
          }
        }

        currentRange = { start, end };

        // 测量新添加元素的真实高度
        measureHeights(start, end);
      }

      // 获取真实 DOM 高度，更新缓存
      function measureHeights(start, end) {
        let needsRecalc = false;

        for (let i = start; i &amp;lt; end; i++) {
          const element = renderedElements.get(i);
          if (element) {
            const realHeight = element.offsetHeight;
            if (heights.get(i) !== realHeight) {
              heights.set(i, realHeight);
              needsRecalc = true;
            }
          }
        }

        // 如果高度有变化，重新计算位置
        if (needsRecalc) {
          calcPositions();
          // 更新所有已渲染元素的位置
          renderedElements.forEach((element, index) =&amp;gt; {
            element.style.transform = `translateY(${positions[index]}px)`;
          });
        }
      }

      // 滚动监听
      // 滚动事件可能非常频繁触发（比如每毫秒多次）
      container.addEventListener(&quot;scroll&quot;, () =&amp;gt; {
        // 但渲染只会在浏览器准备重绘时执行
        requestAnimationFrame(render); // 不会少于一帧的时间执行
      });

      // 窗口大小变化时重新渲染
      window.addEventListener(&quot;resize&quot;, () =&amp;gt; {
        requestAnimationFrame(render);
      });

      // 初始渲染
      calcPositions();
      render();
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接运行上面的代码，打开开发者工具 Elements ，可以查看到这个 demo 的显示效果。&lt;/p&gt;
&lt;h2&gt;DOM 结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;container&quot;&amp;gt;
  &amp;lt;div id=&quot;spacer&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;!-- 撑起总高度，使滚动条正常显示 --&amp;gt;
  &amp;lt;div id=&quot;list&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;!-- 实际渲染的列表项区域 --&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#container&lt;/code&gt; 是设置固定高度的滚动容器；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#spacer&lt;/code&gt; 设置整个虚拟列表高度；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#list&lt;/code&gt; 中渲染真实可见的列表项。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高度预估与测量&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始使用 &lt;code&gt;ESTIMATED_HEIGHT&lt;/code&gt; 作为每项高度；&lt;/li&gt;
&lt;li&gt;渲染后获取真实 DOM 高度并更新缓存；&lt;/li&gt;
&lt;li&gt;每次变化后重新计算各项的偏移 &lt;code&gt;top&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可见区域渲染&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用二分查找定位当前 &lt;code&gt;scrollTop&lt;/code&gt; 对应的可视起始项；&lt;/li&gt;
&lt;li&gt;加入上下 &lt;code&gt;BUFFER&lt;/code&gt; 缓冲区，避免频繁增删导致的跳动；&lt;/li&gt;
&lt;li&gt;对于不可见项及时卸载 DOM，提升性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;渲染与更新&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据可见范围创建/更新 DOM 元素；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;transform: translateY(...)&lt;/code&gt; 设置 Y 轴偏移；&lt;/li&gt;
&lt;li&gt;缓存已渲染元素，避免重复插入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能优化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;Map&lt;/code&gt; 存储已渲染 DOM；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 控制渲染频率；&lt;/li&gt;
&lt;li&gt;如果滚动范围变化不大，则跳过更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;关键函数说明&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数名&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calcPositions()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;根据缓存高度计算每项的偏移 top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findStartIndex(scrollTop)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;二分查找滚动位置对应的起始项&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;render()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制渲染可见范围元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;measureHeights(start, end)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更新高度缓存并修正偏移位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createElement(index)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建单项 DOM 节点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;到此为止，我们已经能够理解并实现虚拟滚动列表了。&lt;/p&gt;
&lt;p&gt;如果你的目标是学习虚拟列表，那么到此就可以结束了。&lt;/p&gt;
&lt;p&gt;后文会介绍浏览器的渲染机制，然后进一步讨论虚拟列表带来的收益的量化测评。&lt;/p&gt;
&lt;h1&gt;浏览器的部分渲染机制&lt;/h1&gt;
&lt;p&gt;简单来说，就是地址栏输入地址后敲下回车，之后发生的一系列变化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器根据地址找到对应的资源；&lt;/li&gt;
&lt;li&gt;解析 HTML 代码，构建 DOM 树；&lt;/li&gt;
&lt;li&gt;解析 CSS 代码，构建 CSSOM 树；&lt;/li&gt;
&lt;li&gt;合并 DOM 树和 CSSOM 树，生成渲染树；&lt;/li&gt;
&lt;li&gt;布局渲染树，确定每个节点的位置和大小；&lt;/li&gt;
&lt;li&gt;绘制渲染树，将每个节点转换为屏幕上的像素；&lt;/li&gt;
&lt;li&gt;浏览器将像素发送到显卡，显示在屏幕上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我们关注这一步骤：&lt;strong&gt;渲染树&lt;/strong&gt;的生成和使用。&lt;/p&gt;
&lt;h2&gt;渲染树的生成&lt;/h2&gt;
&lt;h2&gt;渲染树的使用&lt;/h2&gt;
&lt;h1&gt;性能评测&lt;/h1&gt;
&lt;p&gt;接下来我们将对比使用虚拟列表和不使用虚拟列表的渲染树的差异。&lt;/p&gt;
&lt;h2&gt;渲染树直观上的比较&lt;/h2&gt;
&lt;h2&gt;渲染树量化比较&lt;/h2&gt;
&lt;h2&gt;性能优化量化指标&lt;/h2&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;文章谬误之处欢迎指正。&lt;/p&gt;
&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/rc-virtual-list&quot;&gt;rc-virtual-list&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Web 性能优化权威指南&lt;/li&gt;
&lt;li&gt;chatGpt&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Webpack 入门</title><link>https://0110wdj.github.io/snofly-blog/posts/frontend-basic/webpack/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/frontend-basic/webpack/</guid><pubDate>Sat, 04 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;随着技术的发展，新的工具不断出现，老的工具依然不过时。说到底，设计思想并没有革命性的变化。而工具，够用就行了。&lt;/p&gt;
&lt;p&gt;本文通过 Webpack 来聊聊打包上的一些事情，结合实例来看看 Webpack 的使用和优化，最后做到能够按需自定义配置。&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.Webpackjs.com/&quot;&gt;官网&lt;/a&gt;有完整的说明，强烈建议之后去看一下。&lt;/p&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;前端基础知识&lt;/li&gt;
&lt;li&gt;了解项目部署基本知识&lt;/li&gt;
&lt;li&gt;了解 npm 使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文适合有一定使用经验的读者 —— 能够创建一个简单的 app, 完成简单的模块代码开发。&lt;/p&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;p&gt;提高整体理解，对“打包”这件事和“打包工具 Webpack” 的认识。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;了解到 Webpack 的历史发展&lt;/li&gt;
&lt;li&gt;使用基本配置完成打包&lt;/li&gt;
&lt;li&gt;了解常用的配置项及其含义&lt;/li&gt;
&lt;li&gt;常用的配置项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最主要的是，在实际项目中用到 Webpack 时，能够用更高的视角来看待问题。&lt;/p&gt;
&lt;h1&gt;历史发展&lt;/h1&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;p&gt;先介绍下三个概念的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打包&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;打包&lt;/strong&gt;是一个动作，是指处理某些⽂件并将其输出为其他⽂件的过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块打包工具(module bundler)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模块打包工具&lt;/strong&gt; 帮助生成用于部署的 js 脚本和样式表，可以通过配置提高代码性能和浏览器兼容性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务执行工具(task runner)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;任务执行工具&lt;/strong&gt; 用来自动化处理常见的开发任务，例如，lint(代码检测)、build(构建)、test(测试)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相比模块打包工具（例如：Webpack、Browserify、Brunch），任务执行工具（例如：Make、Grunt、Gulp ）则聚焦在偏重上层的问题上面。&lt;/p&gt;
&lt;h2&gt;打包过程与模块开发&lt;/h2&gt;
&lt;p&gt;这个过程，随着时间的发展，不断出现新的问题和解决方案。&lt;/p&gt;
&lt;h3&gt;手动处理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在浏览器中运⾏ JavaScript 有两种⽅法。第⼀种⽅式，引⽤⼀些脚本来存放每个功能；此解决⽅案很难扩展，因为加载太多脚本会导致⽹络瓶颈。第⼆种⽅式，使⽤⼀个包含所有项⽬代码的⼤型 .js ⽂件，但是这会导致作⽤域、⽂件⼤⼩、可读性和可维护性⽅⾯的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那时候还是 var 定义变量，在项目逐渐复杂之后，一系列问题凸显出来。&lt;/p&gt;
&lt;h3&gt;作用域冲突&lt;/h3&gt;
&lt;p&gt;为了解决作用域冲突问题，用立即执行函数 (IIFE - Immediately invoked
function expressions) 来封装单个函数功能。&lt;/p&gt;
&lt;p&gt;这个做法虽然不是很好看，但确实很有用。当然，现在基本不这么写了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// foo.js
((param) =&amp;gt; {
  var onlyThisFun = 0;
  console.log({ param });
  // other handle
  return &quot;result&quot;;
})(param);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 IIFE 的方法成功避免了作用域冲突，但是代码在 &amp;lt;script /&amp;gt; 中的加载仍然复杂且存在冗余代码。&lt;/p&gt;
&lt;h3&gt;精简引入&lt;/h3&gt;
&lt;p&gt;CommonJS 问世并引⼊了 require 机制，它允许你在当前⽂件中加载和使⽤某个模块。而 Node.js 中很好的实现了这一点。&lt;/p&gt;
&lt;p&gt;npm + Node.js + modules 的方式几乎和现在的写法一致了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// bar.js
module.exports = () =&amp;gt; {
  // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const bar = require(&apos;./bar.js&apos;)

bar()

exports.foo = &apos;foo&apos;

exports.fn = () =&amp;gt; { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以只引入库中需要的函数，减少了非必要代码。&lt;/p&gt;
&lt;p&gt;当然，这里有个关键的问题：CommonJS 在 Node.js 中被原生支持，而浏览器原生是不支持 CommonJS 的。&lt;/p&gt;
&lt;h3&gt;ECMA 模块&lt;/h3&gt;
&lt;p&gt;来⾃ Web 项⽬的好消息是，模块正在成为 ECMAScript 标准的官⽅功能，浏览器正在逐步⽀持中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NumCalc.js
const addCalc = (a, b) =&amp;gt; a + b;
const delCac = (a, b) =&amp;gt; a - b;
export { addCalc, delCac };
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// index.js
import { addCalc, delCac } from &quot;NumCalc&quot;;
console.log(addCalc(1, 2));
console.log(delCac(1, 2));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种写法就很现代化了。&lt;/p&gt;
&lt;p&gt;但是，ECMAScript 规范只是规定了最基本的事情需要怎么做，这里还有很多值得优化的地方。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// index.js
import { addCalc, delCac } from &quot;NumCalc&quot;;
setTimeOut(() =&amp;gt; {
  console.log(addCalc(1, 2)); // 1、提前很久引入很久以后才执行、甚至可能不执行的代码
}, 200000);
// console.log(delCac(1, 2)) // 2、引入不用的代码
// 3、图片等资源怎么处理
// 4、代码的浏览器兼容问题
// 5、import export 太多，眼睛疼，怎么提高开发效率
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能会说我在吹毛求疵，眼睛疼关 Webpack 什么事？&lt;/p&gt;
&lt;p&gt;我想说，Webpack 经过这么多年的发展，已经是很完善且稳定的打包工具了。它就是考虑到了各种情况 —— 虽然这样可能会让人觉得配置起来好麻烦。&lt;/p&gt;
&lt;p&gt;如果开发者就用默认配置，当然也能完成开发工作，只是不能得心应手的话难免会有些许遗憾。&lt;/p&gt;
&lt;h2&gt;Webpack 发展史&lt;/h2&gt;
&lt;p&gt;此时从 Webpack github 上看到，最新的 release 版本是 v5.89.0 ，Webpack-cli 的版本是 v5.1.4 。&lt;/p&gt;
&lt;p&gt;本文不关注版本的差别（一般都用新版本），只是稍稍了解一下 Webpack 是怎么出现的。&lt;/p&gt;
&lt;h3&gt;创作者&lt;/h3&gt;
&lt;p&gt;从 github 的 Webpack 上可以看到核心团队的信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./coreTeam.png&quot; alt=&quot;core team&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中，第一位，Tobias Koppers 就是这个仓库创建者，也就是最早的创作者。&lt;/p&gt;
&lt;p&gt;Tobias 是一个德国人，网络昵称叫 sokra 。&lt;/p&gt;
&lt;p&gt;sokra 之前是 Java 开发者。在 Java 里面有个很出名的技术 GWT（Google Web Toolkit）。GWT 里面有个 feature 叫「code splitting」。&lt;/p&gt;
&lt;p&gt;2012 年 2 月 24 号，sokra 给 modules-webmake（前端项目打包的库）提了一个 issue ，希望他们能实现这个 feature。但是 modules-webmake 的维护者一直没有实现这个功能。&lt;/p&gt;
&lt;p&gt;2012 年 3 月 10 号，sokra 去 follow 了一份 modules-webmake 代码，在 github 上开了一个新的项目 Webpack 。&lt;/p&gt;
&lt;p&gt;「code splitting」这个 feature 就是 Webpack 现在提供的主要功能，也是当代前端的基石。&lt;/p&gt;
&lt;p&gt;Webpack 的设计思想并不是独创的，但在当时的前端模块打包工具，它是最先进的 —— 特别是性能优化和热更新。&lt;/p&gt;
&lt;h3&gt;影响力扩大&lt;/h3&gt;
&lt;p&gt;2013 年，React 开源。&lt;/p&gt;
&lt;p&gt;2014 OSCON 大会（OSCON 是动物书 O&apos;Reilly 组织的），Instagram （React 开发的图片社交网站） 的前端团队分享了他们对前端页面加载性能优化，其中很重要的一件就是用到的 Webpack 的 featere「code splitting」。&lt;/p&gt;
&lt;p&gt;当时引起了很大的轰动，之后大家纷纷使用 Webpack，并给 Webpack 贡献了无数的 plugins ，loader。&lt;/p&gt;
&lt;p&gt;2014 年后 Webpack 发展非常迅猛，版本更新非常快。&lt;/p&gt;
&lt;p&gt;至今，Webpack 仍是主流之一。&lt;/p&gt;
&lt;h1&gt;示例讲解&lt;/h1&gt;
&lt;h2&gt;专业术语&lt;/h2&gt;
&lt;p&gt;拉高视角 up up up ~~&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;在项目开发过程中，我们经常会用到很多工具，webpack 只是好用的工具之一。&lt;/p&gt;
&lt;p&gt;实际项目中使用 webpack 时，常提到一些名词或操作：代理、打包、热更新、开发环境、生产环境、依赖等。&lt;/p&gt;
&lt;p&gt;还有一些不常提及（因为不怎么修改）但一直使用的概念：打包入口、loader、输出目录、插件、懒加载等。&lt;/p&gt;
&lt;p&gt;后文会结合配置文件内容解读来说明一下这些术语，现在不理解也没关系。&lt;/p&gt;
&lt;p&gt;这里提出来是为了说明一下本文的&lt;strong&gt;核心思想&lt;/strong&gt;，也就是前文这句：&lt;strong&gt;&lt;em&gt;“这个过程，随着时间的发展，不断出现新的问题和解决方案。”&lt;/em&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;上面这些既是术语概念也是解决方案，为了相继出现的问题而存在。&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;webpack 以 npm 包的形式存在，所以像正常使用一个 npm 包一样就行了。&lt;/p&gt;
&lt;p&gt;比如在一个简单的 npm 管理的项目中安装 webpack :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install webpack webpack-cli --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx webpack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ok, 完成，结束，你已经会了。🤪&lt;/p&gt;
&lt;p&gt;虽然但是，到这一步，只使用默认配置，确实可以完成项目的 build 了。&lt;/p&gt;
&lt;p&gt;对于前端初级工程师来说，一般是做项目模块的业务代码开发，是不会去做项目结构上的配置和优化的。比如项目技术栈选型、目录结构设置、项目流程优化（其中构建部分如果选用 webpack 工具，那么也涉及到 webpack 配置优化）等等，都是更有经验的高级工程师来做的。&lt;/p&gt;
&lt;p&gt;本文到这里，后面的内容是从官网选了一部分，结合我的个人理解总结的。如果读者时间充裕的话，建议直接去官网阅读。&lt;/p&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;p&gt;和大多数工具一样，传递配置信息的入口无外乎两种：命令行参数、配置文件。&lt;/p&gt;
&lt;p&gt;webpack 首先获取并合并命令行参数和配置文件内的参数。&lt;/p&gt;
&lt;p&gt;为了便于管理，webpack 不使用全局安装，一个项目用一个专属的 webpack 配置文件。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我们先来看看最基本的配置都有什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// {项目根目录}/webpack.config.js
const path = require(&quot;path&quot;);

module.exports = {
  entry: &quot;./src/index.js&quot;,
  output: {
    filename: &quot;main.js&quot;,
    path: path.resolve(__dirname, &quot;dist&quot;),
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个程序最基本的当然是输入和输出啦，webpack 也一样。&lt;/p&gt;
&lt;p&gt;其中 entry 是打包的入口，也就是主程序的入口。对于现在常用的&lt;strong&gt;单页应用程序（SPA: Single Page Application）&lt;/strong&gt; 来说，通常只有一个 entry 入口文件。&lt;/p&gt;
&lt;p&gt;而 output 就是打包结果输出的地方，一般都放在主目录的 dist 文件夹下面。&lt;/p&gt;
&lt;p&gt;诶？有没有发现什么问题？&lt;/p&gt;
&lt;p&gt;1、这个配置文件为什么是用 CommonJS 的模块写法？很多项目里也是这样的写法。
2、我们开发的时候不是 npm start 就能跑程序了吗？和 dist 文件夹有什么关系？&lt;/p&gt;
&lt;p&gt;下面先回答一下这两个问题。&lt;/p&gt;
&lt;h3&gt;配置文件写法问题&lt;/h3&gt;
&lt;p&gt;先说结论：这个问题不重要，现在新版 webpack 对两种写法都是支持的了。&lt;/p&gt;
&lt;p&gt;第一个原因是：webpack 在 es6（ECMAScript 6 约等于 JavaScript 2015，也就是 2015 年发布的）之前出现，所以一开始就是这么写的，后来就习惯了懒得改。&lt;/p&gt;
&lt;p&gt;第二个原因是：webpack 在 node.js 环境中运行，而 node.js 就是实现的 CommonJs ，运行没问题没必要改。&lt;/p&gt;
&lt;h3&gt;热更新和 dist 文件问题&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;热更新：热更新通常用于实时预览代码更改的效果，无需手动刷新浏览器。这种机制可以加速开发过程，提高开发效率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;npm start 其实也会生成类似 dist 的文件，只是这个文件不像放在项目根目录的 dist 文件那样静态、精简和具体。&lt;/p&gt;
&lt;p&gt;开发的时候关注点在代码更新上，也就是热更新和错误定位，这需要很快的读写速度。&lt;/p&gt;
&lt;p&gt;npm start 启动的开发服务器在内存中运行，当进行代码更改并保存时，热更新机制会检测到这些更改，并实时更新正在运行的应用程序，相关页面自动刷新。&lt;/p&gt;
&lt;p&gt;用热更新的方式去检测打包输出的 dist 文件，也可以做到不用重启 app 就更新模块的效果。但是一般不这么做（除非想观察和调整资源路径，比如图片放哪里比较好这种问题），因为磁盘读写比内存读写要慢。&lt;/p&gt;
&lt;p&gt;因为关注点不同，这里就有两个概念：开发环境和生产环境，也是两种工作模式，需要相应的配置。&lt;/p&gt;
&lt;h3&gt;开发环境和生产环境&lt;/h3&gt;
&lt;p&gt;我们基于在上面的配置，添加一行配置，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// {项目根目录}/webpack.config.js
const path = require(&quot;path&quot;);

module.exports = {
  entry: &quot;./src/index.js&quot;,
  output: {
    filename: &quot;main.js&quot;,
    path: path.resolve(__dirname, &quot;dist&quot;),
  },
  mode: &quot;development&quot;, // 或者 mode: &apos;production&apos;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 mode 有两个取值：development 或 production。用于指明当前 webpack 的一系列默认操作，可以简单而好用的帮助开发者应对不同场景。&lt;/p&gt;
&lt;p&gt;还是之前一样，很多默认配置不关注也足够使用。也可以人为地控制各种细节功能应该怎么处理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;题外话：&lt;/p&gt;
&lt;p&gt;在一个空白目录中执行 npm init，会生成 package.json 文件。&lt;/p&gt;
&lt;p&gt;为了防止不必要的资源打包入最后的代码，依赖项也分为开发环境依赖和生产环境依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
{
  // ......
  &quot;devDependencies&quot;: { // 开发环境依赖项
    &quot;webpack&quot;: &quot;^5.88.2&quot;,
    &quot;webpack-cli&quot;: &quot;^5.1.4&quot;
  },
  &quot;dependencies&quot;: { // 生产环境依赖项
    &quot;lodash&quot;: &quot;^4.17.21&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅用于开发的工具资源就放在 devDependencies 就行，生产环境中需要用到的工具资源就放在 dependencies 里面。这样也能优化打包过程，较少打包工具的负担。&lt;/p&gt;
&lt;p&gt;添加包时，用参数指明添加到哪个下面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev webpack-merge // 开发
npm install --save lodash // 生产
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;更多配置，详见官网。&lt;/p&gt;
&lt;h2&gt;回答问题&lt;/h2&gt;
&lt;p&gt;之前有五个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、提前很久引入很久以后才执行、甚至可能不执行的代码&lt;/li&gt;
&lt;li&gt;2、引入不用的代码&lt;/li&gt;
&lt;li&gt;3、图片等资源怎么处理&lt;/li&gt;
&lt;li&gt;4、代码的浏览器兼容问题&lt;/li&gt;
&lt;li&gt;5、import export 太多，眼睛疼，怎么提高开发效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据实际项目经验，这里在补充两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6、开发环境时 api 请求的主机地址不在本机，怎么处理？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;!-- 代理、loader、插件。 --&amp;gt;&lt;/p&gt;
&lt;h3&gt;懒加载&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;问题：1、提前很久引入很久以后才执行、甚至可能不执行的代码&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;有时候下载了资源，但是全程没有使用，那么这时候所占用的下载时间和网络资源就浪费了，特别是资源本身很大的时候。&lt;/p&gt;
&lt;p&gt;举个栗子 🌰：有一种情况，就是有个按钮的事件非常复杂，相应的 script 代码就很多，但是我们实际使用的时候只有很小的概率去点击它。&lt;/p&gt;
&lt;p&gt;再举个栗子 🌰：一个网页的后半部分有很多不小的图片，但是用户可能根本不往后翻就切换了，这个时候仍然下载图片的话，也是一种浪费。&lt;/p&gt;
&lt;p&gt;解决办法就是懒加载，达到某个条件才去加载相关资源，而不是一开始就去下载。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import _ from &quot;lodash&quot;;

function component() {
  const button = document.createElement(&quot;button&quot;);

  button.innerHTML = &quot;import while click&quot;;

  button.onclick = (e) =&amp;gt;
    import(&quot;./print&quot;).then((module) =&amp;gt; {
      const print = module.default;
      print();
    });

  return element;
}

document.body.appendChild(component());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;懒加载的特点就是，只在需要的时刻（或提前一点点）去获取资源。这会下降少数时候的一些用户体验，但是能提升多数时候的用户体验。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 4G 基建完成、网速大幅度提升之后，懒加载仍然是优化用户体验不可或缺的手段之一。&lt;/p&gt;
&lt;p&gt;在短视频 app 中，为了兼顾流畅的用户体验和减少网络负担，在用户查看第一个视频时，app 就开始加载下几个视频的前几秒钟的内容，当用户切换到某个视频时，再下载该视频的后续内容。这样用户体验上完全没有卡顿，但是也不会花占用太多网络资源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;tree shaking&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;问题：2、引入不用的代码&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { foo } from &quot;foo.js&quot;;
import { bar } from &quot;bar.js&quot;;
const res = foo();
console.log({ res });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里没有使用 bar 函数，看起来最好的解决办法就是修改代码，不引入 bar 就行了。&lt;/p&gt;
&lt;p&gt;但是注意这里是有坑的。&lt;/p&gt;
&lt;p&gt;如果 bar.js 中有副作用，那么还需要更多的考虑。比如这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// bar.js
window.isBarLoaded = true;
window.isBarCalled = false;

bar = () =&amp;gt; {
  window.isBarCalled = true;
  console.log(&quot;did something&quot;);
};

export { bar };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这个时候，webpack 不能很容易的判断是否应该忽略 bar.js 这个文件，从而减少打包输出的文件体积。&lt;/p&gt;
&lt;p&gt;当然，在 webpack 也中有相关的机制去处理，但是更需要手动配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;your-project&quot;,
  &quot;sideEffects&quot;: [&quot;./src/some-side-effectful-file.js&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tree shaking 是一个术语，通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。webpack 通过 sideEffects 来标记存在副作用的文件，然后他就能轻松地 tree shaking 了。&lt;/p&gt;
&lt;h3&gt;loader&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;3、图片等资源怎么处理&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;比如图片、字体这些静态资源，只要指明路径就可以加载了。但是 less、scss、ts 这种资源是需要翻译一下的，浏览器只支持他们翻译过来的 css、js 文件。&lt;/p&gt;
&lt;p&gt;这个时候明显就需要打包之前执行某些翻译程序了。&lt;/p&gt;
&lt;p&gt;回顾一下， webpack 是模块打包工具的一种。它处理以模块为单位的资源，这里的翻译程序就是将各种资源转化为 webpack 可以处理的模块的加载器 —— loader。&lt;/p&gt;
&lt;p&gt;关于 loader 的配置，在 module.rules 中配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const path = require(&quot;path&quot;);

module.exports = {
  entry: &quot;./src/index.js&quot;,
  output: {
    filename: &quot;bundle.js&quot;,
    path: path.resolve(__dirname, &quot;dist&quot;),
  },
  module: {
    rules: [
      {
        test: /\.less$/i,
        use: [&quot;style-loader&quot;, &quot;css-loader&quot;, &quot;less-loader&quot;],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: &quot;asset/resource&quot;,
      },
    ],
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 type: &quot;asset/resource&quot; 表示图片资源会被作为模块类型进行处理，最终会生成一个资源模块。在 dist 中可以看到文件名被改为 uuid 保证唯一性的图片资源。&lt;/p&gt;
&lt;p&gt;对于 less 这种非标准的文件，需要专门的工具(例如 less-loader)去转化为 css 文件，然后再有 css-loader 解析 CSS 中的 @import 和 url() 等引用。最后，style-loader 将 CSS 样式通过 JS 模块的形式嵌入到 HTML 文档中。&lt;/p&gt;
&lt;p&gt;得到模块之后，就是 webpack 的主场了。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;4、代码的浏览器兼容问题&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;正如之前 less 翻译为 css 一样，es6 代码也可以用 hack 的写法翻译为 es5 代码。&lt;/p&gt;
&lt;p&gt;我们常在项目中看到的 babel-loader 就是调用了 babel 工具来做翻译的事情。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: &quot;babel-loader&quot;,
        options: {
          presets: [&quot;@babel/preset-env&quot;],
        },
      },
    },
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Plugin (插件)&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;5、import export 太多，眼睛疼，怎么提高开发效率&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;地点眼药水吧&lt;/s&gt; 合理使用插件。&lt;/p&gt;
&lt;p&gt;插件是扩展 webpack 功能的工具，它们并不直接操作文件，而是基于事件机制工作，监听 webpack 打包过程中的某些节点，执行广泛的任务。&lt;/p&gt;
&lt;p&gt;这个非常广泛和庞杂，等用到了自行查询即可。也可以看看推荐较多的 webpack 插件。&lt;/p&gt;
&lt;h3&gt;代理 proxy&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;6、开发环境时 api 请求的主机地址不在本机，怎么处理？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;使用 js 调用 API 时，我们一般用 http 访问。&lt;/p&gt;
&lt;p&gt;如果前后端部署在同一个服务器上，那么自然使用 http://loacalhost:port/urlPath 来访问。&lt;/p&gt;
&lt;p&gt;我们在代码中一般这么写访问路径，fetch(&apos;/api/urlPath&apos;),这段代码由 webpack 先转换一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  //...
  devServer: {
    proxy: {
      &quot;/api&quot;: &quot;http://localhost:3000&quot;,
    },
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后由 http-proxy-middleware 提供的代理功能，请求先到本地前端开发服务器，再转到后端，这样就能解决前端直接访问后端 API 的跨域问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;理解一下，前端开发时，往往前端程序在本地运行，而后端是已经部署的服务。那么前端直接访问外部资源的话，浏览器从安全因素考虑直接拒绝了。这个时候，本地有个受信任的服务器，就能让前端的请求交给该服务器帮忙处理发送和接受数据。&lt;/p&gt;
&lt;p&gt;前端程序和后端程序一般部署在同一个机器上，自然没有问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;以上是笔者整理的 webpack 相关内容，希望能帮到你，不至于毫无头绪。&lt;/p&gt;
&lt;p&gt;我们回顾一下目标，感受一下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;了解到 Webpack 的历史发展&lt;/li&gt;
&lt;li&gt;使用基本配置完成打包&lt;/li&gt;
&lt;li&gt;了解常用的配置项及其含义&lt;/li&gt;
&lt;li&gt;常用的配置项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文章谬误之处欢迎指正。&lt;/p&gt;
&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.Webpackjs.com/guides/integrations/&quot;&gt;Webpack 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/71640308&quot;&gt;Webpack 诞生记&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844904042246881293&quot;&gt;webpack proxy 使用（代理的使用）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Java学习笔记 一 课件缝合怪</title><link>https://0110wdj.github.io/snofly-blog/posts/java/java-basic-1/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/java/java-basic-1/</guid><pubDate>Thu, 24 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;词语理解记录&lt;/h1&gt;
&lt;h2&gt;JVM&lt;/h2&gt;
&lt;p&gt;2019/2/22
Java 虚拟机，为 Java 程序在各个平台使用运行提供一个中介。
2022/7/28
JVM: Java Virtual Machine, Java 虚拟机。对 java 程序员屏蔽了不同结构计算机之间的差异，使得 java 代码的编译和运行，经过 JVM 的转换，再到具体机器上。&lt;/p&gt;
&lt;h2&gt;计算机&lt;/h2&gt;
&lt;p&gt;2019/9/6
计算机是纯粹的工具，来源于生活，需要和其他东西结合起来才有用。
2022/7/28
(上文说的好，他和算盘没什么本质区别。)&lt;/p&gt;
&lt;h2&gt;框架&lt;/h2&gt;
&lt;p&gt;2020/9/24
为实际应用开发带来方便和规范的辅助程序。
2022/7/28
框架是指一个可用的应用，它几乎只有打开和关闭这样的简单功能，但是开发者可以方便地在其上添加需要的业务功能，而不用从零开始配置整个技术链。&lt;/p&gt;
&lt;h2&gt;数据类型&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;标识符&lt;/em&gt;
&lt;em&gt;保留字（reserved word）&lt;/em&gt;
&lt;em&gt;基本类型&lt;/em&gt;
······整数类型：byte，short，int，long；（分别占位 8,16,32,64 位；都是有符号类型）
······浮点类型：float，double；
······字符类型：char（16 位，使用 Unicode 字符集）；
······布尔类型：boolean；
&lt;em&gt;复合数据类型&lt;/em&gt;
······class（类）；
······interface（接口）；
······数组；
&lt;em&gt;常量&lt;/em&gt;
······用保留字 final 来实现；
&lt;em&gt;变量&lt;/em&gt;
······作用域；
变量的作用域可分为：局部变量、类属性变量、方法参数和异常处理参数。
在一个确定的域中，变量名应该是唯一的。&lt;/p&gt;
&lt;h2&gt;数据类型注意点&lt;/h2&gt;
&lt;p&gt;1.Java 中不允许将数字转化为布尔值，所以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(x = y) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译报错。
2.Unicode 编码中的胆英豪必须使用半角输入法中的引号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\\ 	&apos;\b&apos; 对应 &apos;\u0008&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.两个整数相加，结果会默认的转换为 int 型变量，注意使用 byte 和 short 进行运算时的类型转换问题。还有，注意加法溢出问题。&lt;/p&gt;
&lt;p&gt;4.在十进制和科学计数法常熟后面可以跟 F 或 f 标识 float 类型，跟 D 或 d 标识 double 类型；如果没有跟任何修饰，那么默认为为双精度类型。&lt;/p&gt;
&lt;p&gt;5.字符串变量用 String 标识，而 String 是类（class）。&lt;/p&gt;
&lt;p&gt;6.关于包装类：
&lt;em&gt;Java 提供与基本数据类型对应的包装类；&lt;/em&gt;
&lt;em&gt;引入包装类的目的：&lt;/em&gt;
&lt;em&gt;基本数据类型不是类，在一些场合不能直接使用；&lt;/em&gt;
&lt;em&gt;包装类的一些静态方法可以实现不同数据类型的转化；&lt;/em&gt;
&lt;em&gt;包装类的静态属性中含有相应数据类型的范围。&lt;/em&gt;
API Doc：http://docs.oracle.com/javase/6/docs/api/overview-summary.html&lt;/p&gt;
&lt;h2&gt;表达式&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;1.算术表达式&lt;/em&gt;
+ - * / % ++ -- -(取反)
补充：
整数和浮点数都能进行取余运算，9%2=1,3.14%2=1.14000001；
&lt;em&gt;2.关系表达式&lt;/em&gt;
** != &amp;gt; &amp;lt; &amp;gt;= &amp;lt;=
&lt;em&gt;3.逻辑表达式&lt;/em&gt;
&amp;amp; | ! ^(异或) &amp;amp;&amp;amp; ||
&lt;em&gt;4.赋值表达式&lt;/em&gt;
= += -= *= /= %= ^= &amp;amp;= |= &amp;lt;&amp;lt;= &amp;gt;&amp;gt;= &amp;gt;&amp;gt;&amp;gt;=
&amp;lt;&amp;lt;(左移，右补 0) &amp;gt;&amp;gt;(右移，左按符号位补 0 或 1)
&amp;gt;&amp;gt;&amp;gt;(右移，左补 1)
&lt;em&gt;5.条件表达式&lt;/em&gt;
布尔表达式 1 ? 表达式 2 : 表达式 3
补充：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class b2_6 {
    public static void main(String[] args){
        float sum=1.5f;
        int num=2;
        System.out.println((sum&amp;lt;2 ? 1 : num/sum));
		int num1=3,sum1=2;
		System.out.println((sum1&amp;lt;3 ? 1 : num1/sum1));
		int num12=3,sum12=2;
        System.out.println((sum12&amp;lt;3 ? 1.0 : num12/sum12));
    }
}
发生隐式类型转换，最后结果为
1.0
1
1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;6.运算符优先级&lt;/em&gt;
域运算&quot;.&quot; &amp;gt; 数组下记“[]” &amp;gt; 分组括号“()” &amp;gt; 单目 &amp;gt; 双目 &amp;gt; 三目 &amp;gt; 赋值&lt;/p&gt;
&lt;h2&gt;控制语句&lt;/h2&gt;
&lt;p&gt;1.分支语句
if-else
注意 if 括号中应为布尔值，这里与 C 语言的区别。
switch
注意 switch(expression) 中的表达式 expression 返回值类型只能是 byte、shout、int、char 之一。 2.循环语句
while
do-while
for
同样注意条件表达式中的结果应为布尔值。 3.跳转语句
break;
break lab;
countinue;
countnue lab;
标号 lab 的用意是完成多重循环的跳转。
return;
return expression;
注意 return 语句后面的语句可能成为死鱼(永远无法执行)，然后编译器就会报错。 4.异常处理语句
try-catch-finally
throw 5.注释语句：//，/* */， /_** _/&lt;/p&gt;
&lt;h2&gt;数组&lt;/h2&gt;
&lt;p&gt;分类 1：
基本数据类型数组、对象数组。
分类 2：
一维数组声明、定义、初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a[10];a=new int[10];
int[10] b=new int[10];
int c[]={3,4,5};
String[] s=new String[]{&quot;ab&quot;,&quot;cd&quot;};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二维数组声明、定义、初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int arr[][];	arr = new int[3][4];
int[][] brr;
brr = new int[2][];
brr[0] = new int[2];
brr[1] = new int[5];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小思考&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Java for 循环简便写法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] a = {1,2,3,4};
for (int i : a) {
System.out.println(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;基本数据类型相对包装类的优点&lt;/strong&gt;
占空间小。&lt;/p&gt;
&lt;h1&gt;Java 程序工程规范&lt;/h1&gt;
&lt;h2&gt;Java 程序编程规范&lt;/h2&gt;
&lt;p&gt;1.有多个 import 语句时，先写 Java 包（都是 Java 包时，依照字母先后顺序排序），后写 javax，最后写其他公司的包和自己定义的包。 2.命名规则：
&lt;strong&gt;包名&lt;/strong&gt;中字母一律小写，如 xxxyyyzzz。
&lt;strong&gt;类名、接口名&lt;/strong&gt;每个单词首字母大写，如 XxxYyyZzz。（接口名加上大写“i”前缀）
&lt;strong&gt;方法名&lt;/strong&gt;第一个单词的字母小写，后面每个单词的首字母大写，如 xxxYyyZzz。
&lt;strong&gt;常量&lt;/strong&gt;中的每个字母大写，如 XXXYYYZZZ。 3.程序{}强调匹配的同时，要保持适当的缩进，以便于阅读。 4.必要时要有一定的程序注释量（20%~50%），注释内容有：
程序头说明，属性说明，方法说明。&lt;/p&gt;
&lt;h2&gt;关于帮助文档&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;帮助文档一般的格式信息：&lt;/strong&gt;
类的继承层次；
类和类的一般目的描述；
成员变量列表；
构造方法列表；
方法列表；
变量详细列表及目的和用途的描述；
构造方法详细列表及描述；
方法详细列表及描述。&lt;/p&gt;
&lt;h1&gt;面向对象基础&lt;/h1&gt;
&lt;h2&gt;抽象&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;书上的原话：&lt;/strong&gt;
抽象是从被研究对象中舍弃个别的、非本质的或与研究主旨无关的次要特征，而抽取与研究有关的共性内容加以考察，形成对研究问题正确、简明扼要的认识。
&lt;strong&gt;我的理解&lt;/strong&gt;
抽象是动词；抽象是舍末逐本的方法；抽象是相对的。&lt;/p&gt;
&lt;h2&gt;类与对象&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;定义：&lt;/strong&gt;
对象是对客观事物的抽象，类是对对象的抽象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class 类名{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关系：&lt;/strong&gt;
对象是类的实例，类是对象的模板。&lt;/p&gt;
&lt;h2&gt;类的域和方法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;定义：&lt;/strong&gt;
域（也称属性或数据员）是对事物静态特征的抽象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;类型 域变量名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法（也成操作或成员方法）是事物动态特征的抽象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[修饰] 返回值类型 方法名(参数类型 参数1，[参数类型 参数2]...){方法体}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关系：&lt;/strong&gt; 1.类中定义的域变量可以被类中的所有方法访问。 2.方法中的形式参数和定义的局部变量的作用域仅限于方法；
局部变量在使用前必须初始化；
如果局部变量和类中的域变量重名，则在方法中对同名变量的改变只作用在局部变量上； 3.类中定义的方法可以进行递归调用。&lt;/p&gt;
&lt;h2&gt;对象详解&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;对象的创建&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FighterPlane fp;//声明对象引用
fp = new FighterPlane();//创建对象，并与对象引用fp关联
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;static 关键字&lt;/strong&gt;
类中被 static 修饰的方法属于类，不被复制到对象中；
可以说，static 修饰的方法（属性）是类方法（属性）。
&lt;strong&gt;堆栈内存分布&lt;/strong&gt;
堆在应用程序生命周期内一直存在，栈在方法调用完毕后就释放；
类和对象被分配到堆中，方法执行时用得局部变量和参数放到栈中；
&lt;strong&gt;所以，方法调用中产生的对象不会因调用结束而被释放掉。&lt;/strong&gt;
声明能引用对象的实质是对象引用在栈中保留了堆中对象的实际地址。
&lt;strong&gt;对象参数&lt;/strong&gt;
对象是引用传递，当对象作为参数传递时，传递的是对象的地址。
&lt;strong&gt;也就是说，对象只有一个，不生成副本&lt;/strong&gt;
基本数据类型作为参数在方法中的传递是值传递。
&lt;strong&gt;对象数组&lt;/strong&gt;
建立&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MyClass [] mc = new MyClass[10];//产生一个MyClass数组对象mc
//mc可以容纳10个MyClass对象引用，每个单元初始值都为NULL
for(int i = 0 ; i &amp;lt; mc.length ; i++){
	mc[i] = new MyClass();
}//为每个单元赋值，即关联对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float f1[],f2[];
f1=new float[10];
f2 = f1;//f1和f2拥有相同的数组成员
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一维数组
数组名.length 可以得到单元个数
二位数组
数组名.length 可以得到行数
数组名[0].length 可以得到第一行的列数
n 维数组
......类推&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;简便方法&lt;/strong&gt;
System.arraycopy()方法可以复制数组；
Java 集合类可以调用相应的方法返回对象数组引用（见 JDK 文档）；
Java 中有的类的方法可以针对某些基本数据类型进行排序（见 JDK 文档）；&lt;/p&gt;
&lt;h2&gt;构造方法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;概念与特征&lt;/strong&gt;
构造方法属于类，不属于任何对象；
构造方法与类同名，且不能指定类型（其隐含的返回值由系统内部使用）；
构造方法具有多态性；
系统有一个默认的无参数的构造方法；
&lt;strong&gt;用 this 区分同名变量&lt;/strong&gt;
当构造方法中的参数与域变量名相同时，此时在构造方法中需要使用 this 关键字来加以区分；
&lt;strong&gt;垃圾回收机制&lt;/strong&gt;
垃圾定义：没有任何引用的对象；
析构：finalize 方法 ；
回收机制：系统在资源不够的情况下可能进行释放；
　　　　　可以用 System.gc()方法“提醒”系统进行垃圾释放；
　　　　　进行垃圾回收的线程级别很低，只有系统不忙时才会理睬提醒；
何时调用析构函数？
堆栈上的对象（系统自动）
堆上的对象（delete 语句）
&lt;strong&gt;创建对象时的初始化顺序&lt;/strong&gt; 1.系统会对数据成员进行默认初始化 2.执行数据成员定义处的初始化语句 3.调用构造方法为数据成员指定初值&lt;/p&gt;
&lt;h2&gt;类成员属性与方法的非访问修饰符&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;static&lt;/strong&gt;
用&lt;strong&gt;static 修饰的域变量&lt;/strong&gt;专属于类，保存在类的内存区（堆中）的公共存储单元中。因此,一个类的任何对象访问它时，存取到的都是相同的数值。
用&lt;strong&gt;static 修饰的方法&lt;/strong&gt;称为静态方法，属于类方法，不属于类的任何对象。（main 方法属于特殊的静态方法）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
A[static方法] --允许访问 --&amp;gt; a(static属性)
A --允许访问--&amp;gt; C(static方法)
C --不能访问--&amp;gt; D(非static属性)
D --允许访问--&amp;gt; C
D --允许访问--&amp;gt; a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;静态代码块
当类被装载时，静态代码块被执行，且仅执行一次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class StaticCodeBlock{
  static int value ;
  static{
    value = 3;
    System.out.println(&quot;value=&quot;+value);
  }
  public static void main(String[] args){
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;abstract&lt;/strong&gt;
（以后补充）
&lt;strong&gt;final&lt;/strong&gt;
final 修饰的类属性为常量；
final 修饰的类方法为最终方法，在子类当中不能被覆盖。
&lt;strong&gt;native&lt;/strong&gt;
native 修饰的方法称为本地方法，此方法的使用的目的是调用其他语言编写的代码块。
缺点：Java 程序的跨平台性能受到限制或破坏。
（暂时不明白工作原理）&lt;/p&gt;
&lt;h2&gt;包（package）&lt;/h2&gt;
&lt;p&gt;包是对 Java 类进行组织与管理的一种机制。&lt;/p&gt;
&lt;h1&gt;面向对象之封装、继承与多态&lt;/h1&gt;
&lt;p&gt;封装 （encapsulation）： 类与对象
继承 （inheritance） ： 基类与派生类
多态 （Polymorphism）： 抽象类与接口&lt;/p&gt;
&lt;h2&gt;封装&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：利用抽象数据类型将数据和基于数据的操作绑定在一起，数据被保存在抽象数据类型内部，系统只有通过被授权的操作方法才能够访问数据。
&lt;strong&gt;特点&lt;/strong&gt;：（1）数据和基于数据的方法构成一个统一体；
（2）隐藏实现细节，只通过操作接口名称进行调用，操作内部的变动不会影响接口的使用。
&lt;strong&gt;访问控制符&lt;/strong&gt;：
public（接口访问权限）
protected（包访问权限、继承访问权限）
“默认”（包访问权限）
private（无法直接访问）
&lt;strong&gt;这些修饰符可以用于&lt;/strong&gt;
修饰类&lt;br /&gt;
修饰类的属性成员（域）
修饰类的成员方法
&lt;strong&gt;访问权限&lt;/strong&gt;：
&lt;img src=&quot;921595aed62551f045b25c832ac2e844.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;e9002da41499c756a536f69a5c6c0cf1.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
访问权限首先看类前的修饰符，当类的修饰符为 public 时，
则类可以被所有的类所访问（可以 import），当为默认时，
则只能为包中的类所访问。
在此前提下，再看属性和方法前的修饰符。
类前修饰符为 public 时，
当属性和方法前的修饰符为 public 时，则该属性可以被所有类访问；
当属性和方法前为 protected 修饰时，对于类定义而言，可以被同包中的类和所有子类访问（子类和父类可以不在同一个包中），但对于对象而言，只能被同一个包的类对象所访问；
当为默认时，只能被同包中的类所访问（比 protected 访问权限小）；当为 private 时，只能被本类内部所访问 。
&lt;strong&gt;面向对象程序 = 对象+ 消息&lt;/strong&gt;
消息的实质就是引用向对象发出的服务请求，是对数据成员和成员方法的调用。
&lt;strong&gt;能否发送消息取决于&lt;/strong&gt;
引用必须引用了特定的对象，否则会在运行时抛出 NullPointerException 异常。
对象必须定义了相应的属性或方法，否则编译不会通过。
被访问的属性或方法必须具有可访问的权限。&lt;/p&gt;
&lt;h2&gt;继承&lt;/h2&gt;
&lt;p&gt;1.通过 extends 关键字实现； 2.单一继承； 3.子类继承了父类的所有属性和方法， 但只有 public、protected 的属性和方法在子类是可见的；
4.Object 类是所有类的共同祖先，即使定义类时没有写 extents Object 。
&lt;img src=&quot;51f70958f29e55fb1a7d0cb728e1722d.png&quot; alt=&quot;没有查JDk&quot; /&gt; 5.继承设计用途：功能复用；为引用带来新特点。
组合设计用途：功能复用，同时不增加类之间的耦合性。
6.final 为最终类，不能有子类。&lt;/p&gt;
&lt;h2&gt;多态&lt;/h2&gt;
&lt;p&gt;1.多态是指一个程序中同名的不同方法共存的情况；
2.Java 中提供两种多态的机制：重载（overloading）与覆盖（overriding）； 3.覆盖与重载的区别：
覆盖：方法名称相同，参数相同，返回类型相同；
重载：方法名称相同，参数类型或个数不同，返回类型相同； 4.覆盖注意：
1）子类的访问修饰符权限应等于或大于父类；
2）同名的 static 方法和非 static 方法不能相互覆盖；
3）方法前有 final 修饰符，此方法不能在子类方法中进行覆盖；
4）在 JDK 中，很多父类的方法被子类重新覆盖，赋予了不同的含义，如 Object 类中的 boolean equals(Object obj)方法；
5）抽象类中如果存在抽象方法，则具体子类必须对抽象方法进行覆盖； 5.多态设计用途：
功能复用，功能扩充。&lt;/p&gt;
&lt;h1&gt;面向对象之抽象类与接口思想&lt;/h1&gt;
&lt;h2&gt;this，super 用法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;this 用法：&lt;/strong&gt; 1.表示当前对象引用，使用 this.成员名表示当前对象的成员；（常用于形参或局部变量与类的成员变量同名的情形） 2.指代当前对象； 3.调用当前类的构造方法；
&lt;strong&gt;super&lt;/strong&gt;
super 访问当前类的直接父类。
主要应用场合：
1.super.数据成员；super.成员方法
2.super(参数) , 表示调用父类构造方法&lt;/p&gt;
&lt;h2&gt;构造方法的多态&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;构造方法的重载&lt;/strong&gt; 1.一个类的若干个重载的构造方法之间可以相互调用。 2.当一个构造方法需要调用另一个构造方法时，可以使用关键字 this，同时这个调用语句应该（必须？）是整个构造方法的第一个可执行语句。 3.使用关键字 this 来调用同类的其它构造方法，优点同样是以最大限度地提高对已有代码的利用程度，减少程序的维护工作量。 4.父类含参构造方法，子类可以在自己的构造方法中使用 super 来显式调用，但必须是子类构造方法的第一个可执行语句。 5.若子类构造方法中没有显式调用父类构造方法，则在产生子类的对象时，系统在调用子类构造方法的同时，默认调用父类无参构造方法。 4.子类的构造方法必定调用父类的构造方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;子类对象实例化过程&lt;/strong&gt; 1.为对象分配内存空间，对成员变量进行默认的初始化 ； 2.绑定构造方法，将 new 中的参数传递给构造方法的形式参数； 3.调用 this 或 super 语句(二者必居其一,不能同时存在)，执行流程如图所示:
&lt;img src=&quot;e613c10bfad4557b7c65265eb27a17f3.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt; 4.进行实例变量的显式初始化操作， 如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     public class A{
         int value=4;       //显式初始化
         B b = new B();   //显式初始化，注：B为另一个类
      }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5.执行当前构造方法的方法体中的程序代码。&lt;/p&gt;
&lt;h2&gt;抽象类&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;抽象类形成的思维过程：客观事物—&amp;gt;对象—&amp;gt;类—&amp;gt;抽象类。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;抽象类的概念：&lt;/strong&gt;
用 abstract 修饰的类称为抽象类，用 abstract 修饰的成员方法称为抽象方法。
&lt;strong&gt;理解抽象类&lt;/strong&gt; 1.抽象类和具体类的关系就是一般类和特殊类之间的关系，是继承和被继承的关系。 2.抽象类中可以有零个或多个抽象方法，也可以包含非抽象方法。只要有一个抽象方法，类前就必须有 abstract 修饰。 3.抽象类不能创建对象，创建对象由具体子类来实现，但可以有声明，声明能引用所有具体子类的对象。&lt;br /&gt;
4.对于抽象方法，在抽象类中只指定方法名及类型，而不写实现代码。抽象类必定要派生子类，若派生的子类是具体类，则具体子类中必须实现抽象类中定义的所有抽象方法（覆盖）。 5.在抽象类中，非抽象方法可以调用抽象方法。
6.abstract 不能与 final 并列修饰同一个类（产生逻辑矛盾）；abstract 不能与 private ,static（因为 static 修饰的方法必然被直接调用）,final 或 native 并列修饰同一个方法。
&lt;strong&gt;抽象类设计用途&lt;/strong&gt; 1.在继承方面：
简化子类定义，增加系统结构关系的清晰度。 2.在引用具体子类对象方面：
其声明可以引用所有具体子类的对象。
&lt;strong&gt;设计注意事项&lt;/strong&gt; 1.在设计抽象类的具体子类时，追求它们在实现抽象方法方面所表现的不同性。 2.在确定抽象类的抽象方法时，一定要确保这些抽象方法就是同类对象共同行为的抽取。 3.抽象类中的信息应尽可能和研究对象的本质相关。&lt;/p&gt;
&lt;h2&gt;接口（interface）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;接口扮演将实现者和使用者有机联系到一起的角色&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接口的两种含义：&lt;/strong&gt; 1.可以被引用调用的方法（public 方法或同包中的 protected 方法或默认方法）； 2.另一种是同“类”概念地位相当的专有概念 interface， interface 是方法说明的集合。
&lt;strong&gt;interface 声明&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [public] interface 接口名［extends 父接口名列表］
  {
     //静态常量数据成员声明
     [public][static][final] 域类型 域名 = 常量值
　 //抽象方法声明
     [public][abstract]返回值 方法名(参数列表)[throw 异常列表]
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;定义接口注意事项&lt;/strong&gt; 1.接口定义用关键字 interface，而不是用 class，interface 前的修饰符要么为 public，要么为缺省。 2.接口定义的数据成员全是 public final static(静态常量)。即使没有修饰符。 3.接口中没有构造方法；所有成员方法都是 public abstract 方法（与抽象类有所不同）。即使没有修饰符，其效果完全等效。注：方法前不能修饰为 final。 4.接口具有继承性，可通过 extends 关键字声明接口的父接口。 5.不能有实例对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接口的设计用途：&lt;/strong&gt;
通用可扩展代码，体现多态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现接口注意事项&lt;/strong&gt; 1.在类中，用 implements 关键字来实现接口。一个类可以实现多个接口，在 implements 后用逗号隔开多个接口的名字。一个接口也可被多个类来实现。 2.接口中的字段（域）的值存储在该接口的静态存储区域内，使用接口名.字段或实现类.字段均可访问。 3.如果实现某接口的类不是 abstract 修饰的抽象类，则在类的定义部分必须实现接口的所有抽象方法，而且方法头部分应该与接口中的定义完全一致。 4.如果实现接口的类是 abstract 类，则它可以不实现该接口的所有方法。但对于抽象类的任何一个非抽象的子类而言，接口中的所有抽象方法都必须实现。 5.类在实现接口的抽象方法时，必须显式使用 public 修饰符，否则将被警告为缩小了接口中定义的方法的访问控制范围。&lt;/p&gt;
&lt;h2&gt;抽象类与接口的比较&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;43d7e6de840cd92c058d97f5a3b50d3f.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;引用形成&lt;/strong&gt;
先声明，后初始化（将对象赋给引用）
//先声明，赋予对象后声明变为引用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;抽象类声明引用的特点&lt;/strong&gt;
抽象类声明可以引用所有具体子类对象，此概念可以推广到所有具有父子关系的类，以及接口和实现类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引用替规则&lt;/strong&gt;
父类引用可以指向子类对象，子类声明不能引用平行级别的其它类的对象,也不能引用父类对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;父类声明和子类声明都引用同一个子类对象的区别&lt;/strong&gt;
父类声明是从父类的角度去引用对象，子类声明是从子类角度引用对象。
父类引用子类对象时，可以经过显式的转化赋给子类的声明，但子类的引用赋值给父类的声明则不需要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引用比较&lt;/strong&gt;
1.equals 方法比较
equals 方法是 Object 类的方法，比较本引用和参数指明的某个引用是否相等，即是否指向同一对象。返回 true 或 false。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{} class B{}
A a=new A();
B b=new B();
if( a.equals(b) ) {System.out.println(&quot;a与b相同&quot;);}
else {System.out.println(&quot;a与b不相&quot;);}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.使用 &quot; ** &quot; 进行比较
如果两边是对象引用则比较的是它们的引用是否相同；
如果两边是数值，则比较的是它们的值(如果值类型不同，有可能发生类型转化，例如 10**10.0 将返回 true)；
如果一边是引用，一边是值，则编译错误。&lt;/p&gt;
&lt;p&gt;3.使用 instanceof 比较引用类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{} class B{}
A a=new A();
B b=new B();
boolen result = a instanceof A;//result为true
boolen result = b instanceof A;//result为false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类的其他相关内容&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;类的完整定义形式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[修饰符] class 类名 [extends 父类]
                               [implements 接口名1,接口2]
{  类成员变量；
   类方法；}
注：其中修饰符为：public、缺省、abstract、final
                                外部类不能被private、protected所修饰。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;内部类定义&lt;/strong&gt;
定义在某个类 A 中的类 B，（可以是抽象类，也可以是静态类）。
相应的，A 就称为外部类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内部类相关要点&lt;/strong&gt; 1.内部类可以直接访问外部类中的所有属性；
外部内无法直接访问内部类。 2.内部类对象产生方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Outer{public class Inner{}}//public修饰内部类
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Outer{
	private class Inner{}//不是public修饰内部类
	Inner getInner(){return new Inner();}
}
Outer outer = new Outer();
Outer.Inner inner = outer.getInner();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Outer{static class Inner{}}//static修饰内部类
Outer.Inner inner = new Outer.Inner();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.static 修饰的内部类：
相当于外部类的一个静态属性，其内部可以有静态方法或属性。
不能使用外部类的非静态属性或方法。&lt;/p&gt;
&lt;p&gt;4.内部类定义在方法体中时
只能访问方法体内的常量，不能访问方法体内的局部变量；
此时内部类前不能有修饰符。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;匿名内部类与匿名对象&lt;/strong&gt; 1.匿名对象就是在产生对象时没有显式地为这个对象建立引用。 2.匿名对象在两种情况下经常使用：
如果对一个对象只需要进行一次方法调用。
将匿名对象作为参数传递给一个函数调用。 3.匿名内部类使用规则：
不能有构造方法；
不能定以任何静态成员和方法；
不能被 public、protected、private、static 修饰；
只能创建一个实例。&lt;/p&gt;
&lt;h2&gt;Class 类&lt;/h2&gt;
&lt;p&gt;1.在 java.lang 包中有一个特殊的类——Class(注：同关键字 class 区别的地方是首字母大写)，它也继承 Object 类。 2.通过调用对象方法 getClass 而得到 Class 对象，进而我们可以知道类的相关的一些特性，如：类的名称、类的包、类的方法、类的父类等待。&lt;/p&gt;
&lt;h1&gt;异常&lt;/h1&gt;
&lt;h2&gt;异常含义&lt;/h2&gt;
&lt;p&gt;正常情况以外的事件，具有不确定性。遇到异常后程序不能正常向下执行。&lt;/p&gt;
&lt;p&gt;例如：用户输入错误、除数为 0，需要的文件不存在或打不开、数组下标越界、传入参数为空或不符合指定范围。&lt;/p&gt;
&lt;h2&gt;异常分类&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;9f5b760845363b990d5e137adf09a719.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
（暂时也看不懂）&lt;/p&gt;
&lt;h2&gt;异常处理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;graph LR
A(异常处理) --&amp;gt; B(声明抛出处理)
A --&amp;gt; C(程序捕获处理)
B --&amp;gt; D(隐式声明抛出)
B --&amp;gt; G(显式声明抛出)
C --&amp;gt; E(非嵌套捕获)
C --&amp;gt; F(嵌套捕获)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;隐式异常抛出&lt;/strong&gt;
特点：
异常类型是 RuntimeException 或是子类时，程序方法可对异常不作任何声明抛出或处理，直接交给调用该方法的地方处理；
程序能编译通过，不会给可能产生错误的代码行给出提示。
&lt;strong&gt;显式声明抛出&lt;/strong&gt;
通过 throws 实现
例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class A{
	//public static void main(Sting[] args){}改为如下：
	public static void main(Sting[] args) throws IOException{}
	//发生IO异常时，main不负责处理，由调用main方法的地方处理异常
	//即Java虚拟机。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;
当子类覆盖父类的方法时，子类抛出的异常类应和父类抛出的相同，或为其子类，不能为其父类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;捕获处理&lt;/strong&gt; 1.异常处理是由 try －catch 组成的一个异常处理块结构；
最为完整的方式是 try-catch-finally 语句&lt;/p&gt;
&lt;p&gt;2.多 catch 情况：
异常类由上到下排布规则：由子到父，由具体到抽象，或为并列关系。
执行过程类似于 switch case（但一个执行，其它不执行）。
若无异常发生，则都不执行。
有时不需要对异常进行细致分类处理，则可统一为 catch(Exception e){}。&lt;/p&gt;
&lt;p&gt;3.finally：
是这个组合语句的统一出口，一般用来进行一些“善后”操作，例如释放资源、关闭文件等。它是可选的部分，但一旦选定，必定执行。&lt;/p&gt;
&lt;p&gt;4.显式异常抛出和程序异常捕获处理的关系
（不好描述）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;嵌套处理&lt;/strong&gt;
（顾名思义）
注意：最内层的 try 产生的异常对象，如果不能被对应的 catch 异常类声明引用，则转交给外层的 catch 去处理。&lt;/p&gt;
&lt;h2&gt;自定义异常&lt;/h2&gt;
&lt;p&gt;异常对象大部分由系统产生；
在编程时也可以主动产生异常，相应的语句为：
　　　 throw new 异常类；
new 后面的异常类可以是 JDK 中定义好的异常，也可以是自定义异常（继承某个异常类的自制异常类）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用途&lt;/strong&gt;
根据程序的需要而定义流程的跳转。
（汇编语言中的断点）&lt;/p&gt;
&lt;h1&gt;Java 常用类库与工具&lt;/h1&gt;
&lt;h2&gt;Java 类库概述&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;d7c564ff800fd06d7507126d43787ac6.png&quot; alt=&quot;duole&quot; /&gt;
java.lang 包中的类不需要 import 就可以使用。&lt;/p&gt;
&lt;h2&gt;String 与 StringBuffer&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Stirng&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;定义：
String 不是简单类型，而是一个类，它被用来表示字符序列。
字符本身符合 Unicode 标准，其基本的初始化方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;方式一：String s1 = “abc&quot;;
       等价于
             char data[] = {&apos;a&apos;, &apos;b&apos;, &apos;c&apos;};  String s1 = new String(data);

方式二: String s2 = new String(“abc&quot;);
        等价于
       String s = “abc&quot;; String s2 = new String(s);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象特点：
String 对象是不可变的，在 String 类中每一个看起来会修改 String 对象内容的方法，实质都是创建了一个全新的 String 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//字符串比较
    String s1 = &quot;abc&quot;;
    String s3 = &quot;abc&quot;;
    System.out.println(s1**s3); // true
    String s2 = new String(“abc&quot;);
    String s4 = new String(“abc&quot;);
    System.out.println(s2===s4); // false
    System.out.println(s2.equals(s4)); // true
//String类对eqauls方法进行了覆盖，比较两个字符串对象的内容是否相同
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作为参数传递的特点：
尽管是传引用，但通过形参引用并未改变实参指向的字符串内容。
优点：
兼顾效率与安全&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;StirngBuffer&lt;/strong&gt;
StringBuffer 是一个具有对象引用传递特点的字符串对象。&lt;/p&gt;
&lt;p&gt;StringBuffer 对象的值是可变的，对字符串的增加、插入、修改、删除等操作比 String 高效（不需多次创建新的对象）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;String、StringBuffer、StringBuilder 不同之处&lt;/strong&gt;
1、String 内部实现基于常量字符数组，内容不可变；
StringBuffer、StringBuilder 基于普通字符数组，数组 大小可根据&lt;br /&gt;
字符串的实际长度自动扩容，内容可变
2、性能方面，对于字符串的处理，相对来说
StringBuilder &amp;gt;StringBuffer&amp;gt;String
3、StringBuffer 线程安全；StringBuilder 非线程安全&lt;/p&gt;
&lt;h2&gt;系统类与时间类&lt;/h2&gt;
&lt;p&gt;System 和 Runtime 两个类封装了对系统进行的一些操作；
Date 和 Calendar 封装了对日期和时间进行的操作。
&lt;strong&gt;System&lt;/strong&gt;
不能实例化；有 in，out，err 三个域；类成员方法都是静态的。
&lt;strong&gt;Runtime&lt;/strong&gt;
每一个 Java 应用程序都有一个 Runtime 类的实例，允许应用程序与其运行的环境进行交互。
要得到该类的实例，不能用 new 的方法产生，只能调用该类的 getRuntime 方法。
&lt;strong&gt;Date&lt;/strong&gt;
JDK 当中有两个同名的 Date，一个存在与 java.util 包中，一个存在于 java.sql 包中。前者在 JDK1.0 中就出现，但是逐渐被弃用（被 Calendar 所取代）；而后者是前者的子类，用来描述数据库中的时间字段。
&lt;strong&gt;Calendar&lt;/strong&gt;
Calendar 类是对时间操作的主要类。要得到其对象引用，不能使用 new，而要调用其静态方法 getInstance，之后利用相应的对象方法。&lt;/p&gt;
&lt;h2&gt;格式化类&lt;/h2&gt;
&lt;p&gt;格式化数字：NumberFormat 和 DecimalFormat
格式化日期：SimpleDateFormat 对象的 format 方法是将 Date 转为 String，而 parse 方法是将 String 转为 Date&lt;/p&gt;
&lt;h1&gt;Java 高级特性之线程&lt;/h1&gt;
&lt;h2&gt;线程概念&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;进程&lt;/strong&gt;
一个进程就是一个执行中的程序；
每一个进程都有自己独立的一块内存空间、一组系统资源；
每一个进程的内部数据和状态都是完全独立的。
&lt;strong&gt;线程&lt;/strong&gt;
Java 程序执行中的单个顺序的流控制称为线程；
多线程则指一个进程中可以同时运行多个不同的线程，执行不同的任务。
&lt;strong&gt;区别比较&lt;/strong&gt;
同类的多个线程共享一块内存空间和一组系统资源，而线程本身的数据通常只有微处理器的寄存器数据，以及一个供程序执行时使用的堆栈。
所以系统在产生一个线程，或者在各个线程之间切换时，负担要比进程小的多 。
&lt;strong&gt;线程意义&lt;/strong&gt;
提高 CPU 利用率，即提高系统性能。&lt;/p&gt;
&lt;h2&gt;线程创建&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;方式一&lt;/strong&gt;
通过继承 Thread 类创建线程，类 MyThread 声明为 Thread 的子类 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyThread extends Thread {
    public static void main ( String args[] ) {
        Thread a = new MyThread();
        a.start();
        System.out.println(&quot;This is main thread.&quot;);
    }
    public void run() {
        System.out.println(&quot;This is another thread.&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式二&lt;/strong&gt;
利用 Runnable 接口产生线程 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyThread implements Runnable {
    public static void main ( String args[] ) {
        MyThread my = new MyThread();
        Thread a = new Thread(my);
        a.start();
        System.out.println(&quot;This is main thread.&quot;);
    }
    public void run() {
        System.out.println(&quot;This is another thread.&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt; 1.要用 start() 来启动线程.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new MyThread().run();//没有新建线程
/*private Runnable target;
 *public void run() {
 *        if (target != null) {
 *            target.run();
 *        }
 *    }
 */
new MyThread().start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.子类应该覆盖 run()方法。&lt;/p&gt;
&lt;h2&gt;线程控制与调度&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;并发特性：可见性；有序性；原子性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.线程的生命周期和状态的改变&lt;/strong&gt;
&lt;img src=&quot;1be24120e9b3b9139f32fae48eb8a6eb.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;2.常用方法&lt;/strong&gt;
&lt;em&gt;join&lt;/em&gt;
thread.Join 把指定的线程加入到当前线程，可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程 B 中调用了线程 A 的 Join()方法，直到线程 A 执行完毕后，才会继续执行线程 B。
t.join(); //调用 join 方法，等待线程 t 执行完毕
t.join(1000); //等待 t 线程，等待时间是 1000 毫秒。
&lt;strong&gt;3.调度&lt;/strong&gt;
线程调度：
当有多个线程处于就绪状态时，线程调度程序根据调度策略安排线程的执行。
调度策略：
抢占式调度(preemptive): 支持基于固定优先级的调度算法，高优先级线程抢占低优先级线程执行;
时间片轮转调度(round-robin)：对于同等优先级的线程，轮流分享 CPU 时间运行。
&lt;strong&gt;4.优先级&lt;/strong&gt;
线程优先级：
public static final int NORM_PRIORITY = 5;
public static final int MIN_PRIORITY = 1;
public static final int MAX_PRIORITY = 10;
可以用下面方法设置和返回线程的优先级。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   public final void setPriority(int newPriority); // 设置线程的优先级。
   public final int getPriority();  // 返回线程的优先级。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5.局限&lt;/strong&gt;
Java 中，优先级高的线程只是比优先级低的线程更有机会执行，具体的执行情况还依赖于操作系统的支持，执行顺序并不是确定的。
&lt;strong&gt;6.volatile&lt;/strong&gt;
用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volatile  public int i;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用：为解决缓存不一致，禁止编译优化，只读写主存。
问题：仍不能解决多线程并发中的安全问题。&lt;/p&gt;
&lt;h2&gt;线程同步机制&lt;/h2&gt;
&lt;p&gt;未解决同步问题，设置对象监视器，即锁。
&lt;strong&gt;一个对象只有一个锁&lt;/strong&gt;
对象监视器（monitor）与关键字 synchronized&lt;/p&gt;
&lt;p&gt;Synchronized 的三种用法：&lt;/p&gt;
&lt;p&gt;synchronized 代码块：监视器就是指定的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
	void put(int i){
	 	while(ture){
	 		synchronized (this){
	 			if(ture) {...}
	 		}
	 	}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;synchronized 方法：监视器就是 this 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A { synchronized void put(int i){...} }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;synchronized 静态方法：监视器就是相应的 Class 对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;死锁（deadlock）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;方法一：{
synchronized(A) {
     ….
    synchronized(B) {
        ….
    }
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;方法二：{
synchronized(B) {
     ….
    synchronized(A) {
        ….
    }
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般解决办法：CUP 老大站出来，kill 掉一个。&lt;/p&gt;
&lt;h2&gt;线程间的同步通信&lt;/h2&gt;
&lt;p&gt;使用 wait 和 notifyAll 实现线程间同步通信&lt;/p&gt;
&lt;p&gt;《notify 和 notifyAll 的区别》
原文：https://blog.csdn.net/u014658905/article/details/81035870&lt;/p&gt;
&lt;p&gt;调用 notify 时，只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒，这取决于线程调度器。虽然如果你调用 notifyAll 方法，那么等待该锁的所有线程都会被唤醒，但是在执行剩余的代码之前，所有被唤醒的线程都将争夺锁定，这就是为什么在循环上调用 wait，因为如果多个线程被唤醒，那么线程是将获得锁定将首先执行，它可能会重置等待条件，这将迫使后续线程等待。因此，notify 和 notifyAll 之间的关键区别在于 notify（）只会唤醒一个线程，而 notifyAll 方法将唤醒所有线程。&lt;/p&gt;
</content:encoded></item><item><title>Java学习笔记二（温故知新）</title><link>https://0110wdj.github.io/snofly-blog/posts/java/java-basic-2/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/java/java-basic-2/</guid><pubDate>Sun, 21 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;说明&lt;/h1&gt;
&lt;p&gt;本文章记录的内容是零散的。
我看书时候，遇到觉得有意思的地方、值得记录一下的知识点、被我以前忽略的地方，就写下来。&lt;/p&gt;
&lt;h1&gt;第四章 面向对象 上&lt;/h1&gt;
&lt;p&gt;1、类中局部变量在&lt;strong&gt;使用前&lt;/strong&gt;必须进行赋值初始化。&lt;/p&gt;
&lt;p&gt;2、this 用于区分局部变量和域变量。&lt;/p&gt;
&lt;p&gt;3、方法在调用时，系统会开辟一个栈空间，存放局部变量和形式参数。
执行时，访问复制到对象中的属性。
方法执行完毕后，栈空间被释放。&lt;/p&gt;
&lt;p&gt;4、static 修饰的属性专属于类，不复制到对象中；static 方法
调用时，同样的开辟栈空间。
执行时，只能访问专属于类的 static 的属性。
执行后，栈空间释放。&lt;/p&gt;
&lt;p&gt;5、ClassName cn = new ClassName();代码执行过程是，先产生对象，之后将对象赋予声明 fp。&lt;/p&gt;
&lt;p&gt;6、内存空间分为堆和栈。
堆在应用程序生命周期内一直存在；
栈在方法调用完毕后被释放。
类和对象分配在堆当中；
方法执行时使用的局部变量和形式参数则放在栈空间中。&lt;/p&gt;
&lt;p&gt;7、ClassName cn = new ClassName();语句在某个方法中执行时，声明 cn 存在于方法栈中，cn 相关的对象存在于堆中。方法栈空间释放后，cn 相关的对象仍会存在一段时间。&lt;/p&gt;
&lt;p&gt;意义：本方法方法执行完毕后，产生的对象可能在其他地方仍然被使用。于是，可能有多个声明都和这个对象相关。如果每释放一个声明，就全部检索一遍是否仍有相关，这将是非常浪费资源的行为。当然，无用的对象在堆中也会消耗空间资源。没有十全十美。&lt;/p&gt;
&lt;p&gt;8、数组名.length 中分 length 是属性，不是方法。&lt;/p&gt;
&lt;p&gt;9、void 返回与 没有返回的区别。void 只是用于标记该方法没有返回，与有返回的方法作区分。而构造方法与析构方法本身有隐含的返回值，该值由系统内部使用，所以就不写 void 标记，当然其他标记也不可以。&lt;/p&gt;
&lt;p&gt;10、构造方法被系统自动调用，不能通过对象引用来调用。&lt;/p&gt;
&lt;p&gt;11、System.gc() 方法提醒系统进行一次垃圾释放，但不一定释放，因为垃圾回收的系统线程级别很低。&lt;/p&gt;
&lt;p&gt;根据 Java 文档，finalize() 是一个用于释放非 Java 资源的方法。例如本地方法——在 java 中调用的非 java 代码。&lt;/p&gt;
&lt;p&gt;不重写 finalize 方法：
使用 try...finally...代码块，将要执行的后续操作放入到 finally 块中。&lt;/p&gt;
&lt;p&gt;12、static 方法只能访问到 static 属性或方法，非 static 方法可以访问全部。&lt;/p&gt;
&lt;p&gt;13、如果 main 方法中缺少 static ，虽然编译能够通过，但它不能作为程序的入口。
原理一：当类被装载时，静态代码块被执行，且只被执行一次。&lt;/p&gt;
&lt;p&gt;14、final 修饰的类属性，相对于常量的值，有助记符。&lt;/p&gt;
&lt;p&gt;15、包的特殊存在形式——默认包&lt;/p&gt;
&lt;p&gt;16、package 是 Java 产生包路径的关键字，且应放在程序的第一句。&lt;/p&gt;
&lt;h1&gt;第五章 面向对象 中&lt;/h1&gt;
&lt;p&gt;1、面向对象的基本特征&lt;strong&gt;主要&lt;/strong&gt;有封装、继承和多态。&lt;/p&gt;
&lt;p&gt;2、封装内部的 set 方法可以加上数据合法性检测。例如，String.trim() 方法用于删除字符串的头尾空白符。&lt;/p&gt;
&lt;p&gt;3、访问控制权限有四种，第四种是“默认”（无任何修饰符）。修饰类的有两种：public 和默认。修饰类属性成员和方法的有四种。&lt;/p&gt;
&lt;p&gt;4、类的构造方法的修饰符为 private 时，外部无法直接产生其对象。&lt;/p&gt;
&lt;p&gt;5、public 类的 protected 属性和方法并不安全，因为可以通过子类对象直接访问其属性和方法。但是又可以加强子类的安全性，从而都变得安全。&lt;/p&gt;
&lt;p&gt;6、程序=数据结构+算法；程序=对象+消息&lt;/p&gt;
&lt;p&gt;7、生产子类对象时，只将父类和子类的非静态属性复制到子类对象中。&lt;/p&gt;
&lt;p&gt;8、父类中存在 private 属性时，子类对象是否复制 private 属性，区分点在于，父类中是否存在可被继承的可以访问 private 属性的方法。分两种情况：其一，不存在，不复制，不能访问；其二，存在，复制，但是子类内部甚至都不能直接访问。&lt;/p&gt;
&lt;p&gt;9、jdk11 中，Object.finalize() 被弃用，理由是 The finalization mechanism is inherently problematic.&lt;/p&gt;
&lt;p&gt;10、java 中，同名同参数不同返回值类型的方法，不是重载，也不是子类中的覆盖。编译不能通过。&lt;/p&gt;
&lt;p&gt;11、子类方法覆盖父类方法时，子类的访问修饰符权限应等于或大于父类的。（public 最大）&lt;/p&gt;
&lt;p&gt;12、静态和非静态方法不能互相覆盖。&lt;/p&gt;
&lt;p&gt;13、抽象类中如果存在抽象方法，则具体子类必须对抽象方法进行覆盖。&lt;/p&gt;
&lt;h1&gt;第六章 面向对象 下&lt;/h1&gt;
&lt;p&gt;1、注意 this 指代当前对象这种用法。&lt;/p&gt;
&lt;p&gt;2、构造方法可以通过 this 调用重载的其他构造方法。注意，这个调用语句放在第一个可执行语句位置。&lt;/p&gt;
&lt;p&gt;3、注意语句执行顺序：
System.ooutprintln(&quot;a + b + c =&quot; + p1.add());
p1.add()完全执行完之后，才会打印前面的字符串&quot;a+b+c=&quot;。&lt;/p&gt;
&lt;p&gt;4、子类的构造方法必定调用父类的构造方法。至少会隐含调用 super() 。所以要注意：当父类只含有有参数的构造方法，子类自动调用时，可能出错。&lt;/p&gt;
&lt;p&gt;5、思考：因为 super 的存在，那么子类在一定程度上，是可以控制父类属性的变化的。&lt;/p&gt;
&lt;p&gt;6、构造方法中的代码要在显示初始化后执行。&lt;/p&gt;
&lt;p&gt;7、抽象类的子类如果仍是抽象类，则不能定义和父类同名的抽象方法。&lt;/p&gt;
&lt;p&gt;8、Java 中的借口有两种含义：
其一，可以被引用调用的方法；
其二，专有概念——interface 。&lt;/p&gt;
&lt;p&gt;9、接口中的数据成员全部是 static final ，无论是否有修饰；
接口中的成员方法全部是抽象方法，无论是否有修饰；
接口没有构造方法，也并不能有自己的实例对象。&lt;/p&gt;
&lt;p&gt;10、接口的作用：使功能的实现和使用以弱耦合的方式连接起来。&lt;/p&gt;
&lt;p&gt;11、如果实现某接口的类不是抽象类，那么必须实现
接口中的全部抽象方法；这里的实现，可以来自是实现类本身；也可以来自他的抽象父类中实现了一部分的方法。&lt;/p&gt;
&lt;p&gt;12、如果接口的抽象方法的访问控制符是默认或 public，那么实现时必须显示地使用 public 修饰符。&lt;/p&gt;
&lt;p&gt;13、面向抽象类和接口编程，这是各种设计模式的核心思想之一。&lt;/p&gt;
&lt;p&gt;14、JDK1.1 之后，Java 事件实现机制，从层次模型改为委托模型，即监听者模式。&lt;/p&gt;
&lt;p&gt;15、注意，Java 事件处理机制。一般有两种方式，但是都是相同的：
1）事物委托型（监听者模式）
2）观察者模式
原理想通的。&lt;/p&gt;
&lt;p&gt;16、灵活应用构造方法 ，来实现两个对象之间的互相链接。这是实现监听或观察的基本步骤。&lt;/p&gt;
&lt;p&gt;17、当接口需要增加新功能方法时，完全可以通过接口的继承，将变化体现在子接口中，从而保证父接口的稳定。&lt;/p&gt;
&lt;p&gt;18、父类申明看似比子类申明要更宽泛，因为父类声明所引用的子类对象在赋值给子类申明时，需要显示转换。这类似于，double 赋值给 float 时，要显示转换，防止溢出；反之，隐式转换。&lt;/p&gt;
&lt;p&gt;在理解父类申明的使用时，不难发现，他不能调用子类新创的属性和方法，实际作用范围是小于子类申明的。&lt;/p&gt;
&lt;p&gt;19、语句 a instanceof A 有三种结果：
a 是 A 或其子类的实例，true；
a 是 A 父类的实例，false；
若无关系，编译不能通过。&lt;/p&gt;
&lt;p&gt;20、上述 19 条中，a instanceof A 中的子类，可以是指多代之后的子类，子、孙、玄孙，也都是 true。&lt;/p&gt;
&lt;p&gt;21、数据成员隐藏，父类声明引用子类对象，这时，父类引用访问的对象属性默认是父类的那个被隐藏的数据。而方法覆盖时，父类引用访问的对象方法，是子类的那个方法。（Java 方法没有隐藏的概念。）&lt;/p&gt;
&lt;p&gt;22、内部类存在于两个位置：
1）外部类域——可以被全部 6 种修饰符修饰。
2）方法内部——不能被任何修饰符修饰。&lt;/p&gt;
&lt;p&gt;23、static 内部类不能再使用外部类的非 static 属性和方法。这符合之前对 static 的理解。&lt;/p&gt;
&lt;p&gt;24、注意匿名内部类的多种情形和写法。&lt;/p&gt;
&lt;p&gt;25、匿名内部类继承了只含有有参构造方法的父类时，需要在创建时的括号类带上相应的参数。当然，这样只是统一了形式，其内部仍没有构造方法。&lt;/p&gt;
&lt;p&gt;26、Object 中的 getClass() 获得 Class 对象。主要作用是获得一些周边信息。&lt;/p&gt;
&lt;h1&gt;第七章 异常&lt;/h1&gt;
&lt;p&gt;1、稍稍注意异常 Exception 和错误 Error 的区分。
JDK 中关于 Error 的描述：
An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.&lt;/p&gt;
&lt;p&gt;举例：运行算式时除 0，是异常，IO 找不到文件，是异常；
Java 虚拟机损坏或耗尽了继续运行所需的资源，是错误。&lt;/p&gt;
&lt;p&gt;2、&lt;strong&gt;虚拟机系统&lt;/strong&gt;产生异常对象。&lt;/p&gt;
&lt;p&gt;3、异常隐式抛出的前提条件是，异常类型为 RuntimeException 或其子类。&lt;/p&gt;
&lt;p&gt;4、语句 void functionName() throws IOException; 含义：
此处的异常不由本方法处理，而是由调用本方法的地方处理。&lt;/p&gt;
&lt;p&gt;5、子类覆盖父类的方法时，异常类应该一致或为其异常子类。&lt;/p&gt;
&lt;p&gt;6、try 后的 catch 语句可能有多个，因为 try 中的异常可能抛出多个。这类似于 switch case 语句的作用，因此，catch 中异常类的排序一般是按照 F 从子类到父类顺序。&lt;/p&gt;
&lt;p&gt;7、Finally 语句块一旦选中，必定执行。例如：rty{return;}finally{System.out.println(&quot;字符串&quot;);}仍会打印字符串。&lt;/p&gt;
&lt;p&gt;8、异常嵌套似乎一直存在，比如第一个 try catch 和 虚拟机之间就存在嵌套。&lt;/p&gt;
&lt;p&gt;9、应用逻辑中的异常，不一定等于语言语法上的异常。于是，自定义异常，用来控制流程的跳转，就很有帮助了。&lt;/p&gt;
&lt;h1&gt;第八章 Java 常用类库与工具&lt;/h1&gt;
&lt;p&gt;1、稍稍注意顶层类和顶层包的概念。
这里也解释了为什么文件同名类总是 pubic 开头。&lt;/p&gt;
&lt;p&gt;2、java.lang 包中的类无须 import 。&lt;/p&gt;
&lt;p&gt;3、注意理解集合类、反射包类。水很深啊。&lt;/p&gt;
&lt;p&gt;4、String 定义时，编译器会优化代码（编译器的优化都是可以通过参数控制开关是），导致 String str1 = &quot;s&quot;; String str2 = &quot;s&quot; ; 中，str1 和 str2 指向同一个对象，而不会产生新的对象。&lt;/p&gt;
&lt;p&gt;5、字符串方法使用时，本身不能为 null。即使调用的是增加后缀的方法。&lt;/p&gt;
&lt;p&gt;6、返回新字符串的方法，不能改变原来的字符串。例如
b = b.newString();
c = b.newString();
上述中，b 不改变，但是 c 会改变。&lt;/p&gt;
&lt;p&gt;7、String 与 StringBuffer 的相互转化：
StringBuffer sb = new StringBuffer(new String());
String s = new String(new StringBuffer());// 或者使用 toString()方法。&lt;/p&gt;
&lt;p&gt;意义：String 转化为 StringBuffer 便于字符串的处理，同时不会增加过多的 String 对象。&lt;/p&gt;
&lt;p&gt;8、System 类不能被实例化。它的类成员方法都是静态的。（估计设计出来就是避免 Java 虚拟机和外部操作系统直接相关的。）&lt;/p&gt;
&lt;p&gt;9、每一个 Java 应用程序只允许有一个 Runtime 类实例。（它涉及到关键的控制点，进程）&lt;/p&gt;
&lt;h1&gt;第九章 线程&lt;/h1&gt;
&lt;p&gt;1、线程本身的数据通常来自两个地方：
1）微处理器的寄存器；
2）程序运行时的堆栈。&lt;/p&gt;
&lt;p&gt;2、Java 的产生线程的方法：
1）继承 Thread 类，并覆盖其 run 方法；
2）实现 Runnable 接口，并并将实现类对象传参到 Thread 类的构造方法。&lt;/p&gt;
&lt;p&gt;3、Runnable 接口只有唯一的抽象方法 run();
上述两种方法原理是相通的，都是手动覆盖或者实现 run()方法，再由 Thread 的 start() 方法启动线程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Thread 中的部分代码
private Runnable target;
public void run() {
        if (target != null) {
            target.run();
        }
    }
// ------------------------
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or &quot;system&quot;
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state &quot;NEW&quot;.
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group&apos;s list of threads
         * and the group&apos;s unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

private native void start0();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;start() 的 JDK11 描述：
Causes this thread to begin execution;
the Java Virtual Machine calls the run method of this thread.&lt;/p&gt;
&lt;p&gt;4、两种方法的比较：
1）由于 Java 单继承的特性，所以通过继承覆盖 run() 的方式会有不小的局限性；
2）通过接口实现的方式，多线程可共享实现类对象的资源。&lt;/p&gt;
&lt;p&gt;（其中第二条存疑，保留，暂时不做解释）&lt;/p&gt;
&lt;p&gt;解释共享：
观察语句
RunnableImplement m = new RunnableImplement(参数);
Thread thread1 = new Thread(m);
Thread thread1 = new Thread(m);
这里都是传递的 m 到 Thread 构造方法，那么之后这两个线程自然都是围绕着 m 在运行，也就是访问的对象是同一个，同时控制对象内部的数据成员变化。&lt;/p&gt;
&lt;p&gt;对比继承覆盖的方式，观察语句：
Thread a = new MyThread();
a.start();
这里如果出现多次声明创建 MyThread 对象，这些对象之间毫无交叉，彼此独立。&lt;/p&gt;
&lt;p&gt;5、注意 volatile 关键字的作用：
让 JVM 总是从主存中读取数据，&lt;strong&gt;一定程度上&lt;/strong&gt;防止共享的数据不一致。&lt;/p&gt;
&lt;p&gt;6、一个具有生命的线程总是在五个态之一：
创建、可运行（就绪）、运行中、阻塞、死亡。&lt;/p&gt;
&lt;p&gt;这里的“具有生命”和生物上的理解有些不同，包括了创建和死亡状态。&lt;/p&gt;
&lt;p&gt;7、Java 中，如果一个程序只有后台线程，那么这个程序就会立即结束。&lt;/p&gt;
&lt;p&gt;（前台线程和后台线程的区分与联系）&lt;/p&gt;
&lt;p&gt;1）后台线程不会阻止进程的终止。属于某个进程的所有前台线程都终止后，该进程就会被终止。所有剩余的后台线程都会停止且不会完成。
2）可以在任何时候将前台线程修改为后台线程，方式是设置 Thread.IsBackground 属性。
3）不管是前台线程还是后台线程，如果线程内出现了异常，都会导致进程的终止。
4）托管线程池中的线程都是后台线程，使用 new Thread 方式创建的线程默认都是前台线程。&lt;/p&gt;
&lt;p&gt;用法：
一般前台线程用于需要长时间等待的任务，比如监听客户端的请求；
后台线程一般用于处理时间较短的任务，比如处理客户端发过来的请求信息。&lt;/p&gt;
&lt;p&gt;8、阻塞状态的线程，仍可能继续占用同就绪状态一样的资源。&lt;/p&gt;
&lt;p&gt;9、yield() 暂停线程时，系统是选择其他同优先级线程执行，最后再选用自己。
这种设置，可以用来作为一种信息处理的手段。&lt;/p&gt;
&lt;p&gt;10、Thread a; a.join(参数); 暂停的是这句话所在的线程（即当前线程），直到 a 线程执行完毕（或者一段时间，取决于参数）。&lt;/p&gt;
&lt;p&gt;11、高优先级线程执行时，采用独占的方式调度。&lt;/p&gt;
&lt;p&gt;12、多线程中程序执行顺序是多变的。&lt;/p&gt;
&lt;p&gt;13、synchronized 工作原理、用法、区别。
（之后补充）&lt;/p&gt;
&lt;p&gt;基本解释：
Java 语言为了解决并发编程中存在的原子性、可见性和有序性问题，提供了一系列和并发处理相关的关键字，比如 synchronized、volatile、final、concurren 包等。&lt;/p&gt;
&lt;p&gt;用法：
主要有两种用法，分别是同步方法和同步代码块。也就是说，synchronized 既可以修饰方法也可以修饰代码块。&lt;/p&gt;
&lt;p&gt;原理：
对于同步方法，JVM 采用 ACC_SYNCHRONIZED 标记符来实现同步。
对于同步代码块。JVM 采用 monitorenter、monitorexit 两个指令来实现同步。&lt;/p&gt;
&lt;p&gt;无论是 ACC_SYNCHRONIZED 还是 monitorenter、monitorexit 都是基于 Monitor 实现的，在 Java 虚拟机(HotSpot)中，Monitor 是基于 C++实现的，由 ObjectMonitor 实现。&lt;/p&gt;
&lt;p&gt;ObjectMonitor 类中提供了几个方法，如 enter、exit、wait、notify、notifyAll 等。&lt;/p&gt;
&lt;p&gt;（大坑！目前填不上了。）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.hollischuang.com/archives/2637&quot;&gt;参考文章&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;14、Java 死锁相关。
Java 不能发现和避免死锁问题，这需要靠程序员自己注意......
（之后补充）&lt;/p&gt;
&lt;p&gt;死锁是这样一种情形：多个线程同时被阻塞，它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞，因此程序不可能正常终止。&lt;/p&gt;
&lt;p&gt;java 死锁产生的四个必要条件：&lt;/p&gt;
&lt;p&gt;1、互斥使用，即当资源被一个线程使用(占有)时，别的线程不能使用
2、不可抢占，资源请求者不能强制从资源占有者手中夺取资源，资源只能由资源占有者主动释放。
3、请求和保持，即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待，即存在一个等待队列：P1 占有 P2 的资源，P2 占有 P3 的资源，P3 占有 P1 的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候，便形成死锁。当然，死锁的情况下如果打破上述任何一个条件，便可让死锁消失。&lt;/p&gt;
&lt;p&gt;15、wait 暂停当前线程，并释放该对象监视权；
（注意区分）sleep 暂停当前线程，但是不释放该对象监视权。&lt;/p&gt;
&lt;p&gt;16、notify() 释放该对象监视权，并唤醒队列中和它拥有同类型对象监视器标签的等待线程。&lt;/p&gt;
&lt;p&gt;17、线程同步通信由三方面配合实现：
1）方法前的 synchronized
2）方法中的 wait 和 notify
3）一个布尔型的变量指示器&lt;/p&gt;
&lt;p&gt;18、notifyAll() 和 notify 理解。
（之后补充）&lt;/p&gt;
&lt;p&gt;notify() 方法随机唤醒对象的等待池中的一个线程，进入锁池；notifyAll() 唤醒对象的等待池中的所有线程，进入锁池。&lt;/p&gt;
&lt;p&gt;等待池：假设一个线程 A 调用了某个对象的 wait()方法，线程 A 就会释放该对象的锁后，进入到了该对象的等待池，等待池中的线程不会去竞争该对象的锁。&lt;/p&gt;
&lt;p&gt;锁池：只有获取了对象的锁，线程才能执行对象的 synchronized 代码，对象的锁每次只有一个线程可以获得，其他线程只能在锁池中等待&lt;/p&gt;
&lt;h1&gt;第十章 集合类&lt;/h1&gt;
&lt;p&gt;1、集合类和集合接口类似与对象数组，但是其中的元素类型可以不一致。这好像 C 语言中的联合体。（具体区别需要另外去查资料，这里只是方便理解。）&lt;/p&gt;
&lt;p&gt;2、对集合类对象中的对象元素进行遍历常用 Iterator 迭代器接口，而枚举接口 Enumeration 仅限于特定类，如 Vector、Hashtable 等。&lt;/p&gt;
&lt;p&gt;3、for 循环的增强写法竟然有规定的读法：
写作：for (MyObject m: c) {}
读作：for each MyObject in c.&lt;/p&gt;
&lt;p&gt;（有点 Python 的味道了）&lt;/p&gt;
&lt;p&gt;4、注意五个类的关系：
子接口 ListIterator 继承了接口 Iterator；
子接口 List 和子接口 Set 继承了接口 Collection；
Collection 中可以调用方法产生 Iterator 对象引用；
List 中有额外的方法来生产 ListIterator。&lt;/p&gt;
&lt;p&gt;5、ListIterator 可以说是 Iterator 的功能拓展，具有更多更强大的功能；但是相应的，他的使用范围被局限在 List 接口这一个地方。（这应当是列表和集合的不同特性决定的。）&lt;/p&gt;
&lt;p&gt;6、基本数据类型的包装类都默认实现了 Collection 。&lt;/p&gt;
&lt;p&gt;7、造型和泛型：
造型：在取出对象时进行类型转化，不匹配时抛出异常；
泛型：在定义时就加入类型标志，编译时自动检测类型匹配；&lt;/p&gt;
&lt;p&gt;泛型在 JDK1.5 之后被引入，替代造型的使用。&lt;/p&gt;
&lt;p&gt;8、Bitset 类是二进制类构成的集合，其中的元素都是 flalse 或 true 。于是，可以虚拟一个电脑了。&lt;/p&gt;
&lt;p&gt;9、如果考虑类的性能问题，就一定要设置好集合对象的初始大小。（注意，它们自动增加容量的时候，是当前容量使用到一定程度——载入因子——的时候）&lt;/p&gt;
&lt;p&gt;10、HashMap 对象遍历顺序与加入对象元素的顺序并不一致。而 TreeMap 的映像以关键字的字母顺序存储，单这却造成了额外的开销。&lt;/p&gt;
&lt;p&gt;这两个配合使用，能够有效的坚固兼顾性能和排序需求。&lt;/p&gt;
&lt;h1&gt;第十一章 Applet 程序&lt;/h1&gt;
&lt;p&gt;虽然 Applet 不用了，但是学习一点基本概念也可以了解设计思想，触类旁通。&lt;/p&gt;
&lt;p&gt;1、Applet 生命周期的四个状态：
初始态、运行态、停止态、消亡态。&lt;/p&gt;
&lt;p&gt;2、状态转化涉及的四个方法：
init()、start()、stop()、destroy()&lt;/p&gt;
&lt;h1&gt;第十二章 AWT 图形界面&lt;/h1&gt;
&lt;p&gt;这一章也是简单了解基本概念即可。&lt;/p&gt;
&lt;p&gt;1、容器（Container）继承自 Component ，所以其本身也是组件。但是他的主要功能是容纳其他组件和容器。&lt;/p&gt;
&lt;p&gt;2、面板类（Panel ）是 Container 的子类，他实例化后必须装入到 Window 对象中。&lt;/p&gt;
&lt;p&gt;3、Frame 对象创建后，如果要其可视，则必须调用 setVisible() 方法。&lt;/p&gt;
&lt;p&gt;4、Frame 对象产生时，创建了一个前台线程——AWT 线程。&lt;/p&gt;
&lt;p&gt;5、FileDialog 用于对文件读取和存储时指定路径和文件名。&lt;/p&gt;
&lt;p&gt;6、组件是 Java 用户图形界面的基本单元。组件不能独立显示，必须放到特定的容器中。&lt;/p&gt;
&lt;p&gt;7、组件文本框 Textfield 单行的文本输入框。&lt;/p&gt;
&lt;p&gt;8、组件文本域 FileArea 是一个多行文本框。&lt;/p&gt;
&lt;p&gt;9、单选按钮必须属于一个条目组，一个条目组中的按钮具有互斥关系。&lt;/p&gt;
&lt;p&gt;10、列表与下拉列表的区别：
列表条目的数量超过列表显示的行数时会自送出现滚动条；
列表既可以多选也可以单选。&lt;/p&gt;
&lt;p&gt;11、组件在容器中的位置可采用两种方式确定：
1）使用容器坐标系确定；
2）使用容器布局管理器确定。&lt;/p&gt;
&lt;p&gt;容器默认采用的是布局管理器来进行组件布局。
如果要使用 1），则需要执行 setLayout(null) 语句。&lt;/p&gt;
&lt;p&gt;12、布局管理器的类型主要有 5 种：&lt;/p&gt;
&lt;p&gt;1）FlowLayout
Panel、Applet 容器的默认布局管理器；
布局规律：上下左右。&lt;/p&gt;
&lt;p&gt;2）BorderLayout （Border 边境）
Window、Frame、Dialog 的默认布局管理器；
布局规律：把容器分为东西南北中 5 各区域。&lt;/p&gt;
&lt;p&gt;3）GridLayout （Grid 网格）
布局规律：使容器中的各个组件呈网格状布局，平均占据容器的空间。&lt;/p&gt;
&lt;p&gt;4）CardLayout
布局规律：把容器分为多层，每层显示空间占据整个容器大小，每层只放置一个组件。&lt;/p&gt;
&lt;p&gt;5）GridBagLayout
布局规律：具有极强的灵活性。（自定义）
每个组件都有一个 GridBagConstraints 对象来给出他的大小和希望摆放的位置。&lt;/p&gt;
&lt;p&gt;13、Java 程序运行时，用户在界面上进行的操作会被&lt;strong&gt;系统捕捉&lt;/strong&gt;，然后产生相应的事件对象 event 交由事件程序处理。&lt;/p&gt;
&lt;p&gt;14、Java 事件处理模型有两种：
1）JDK1.0 的层次事件模型
当事件产生时，它先被送往产生该事件的组件 (component), 如事件在这里未被处理，它就会被自动送往该组件的 Container, 如 Container 也未对事件进行处理，则还会递交给该 Container 的上一层 Container（如有的话）。&lt;/p&gt;
&lt;p&gt;2）JDK1.1 以后的委托事件模型
当事件产生时，该事件被送到产生该事件的组件去处理，而要能够处理这个事件，该组件必须登记(register)有与该事件有关的一个或多个被称为 listeners 的类，这些类包含了相应的方法能接受事件并对事件进行处理。在这种模式中，事件的产生者和事件的处理者分离开来了，它们可以是不同的对象。事件的处理者，即那些 listeners，是一些实施了 Listener 接口的类。当事件传到登记的 listener 时，该 listener 中必须有相应的方法来接受这种类型的事件并对它进行处理。&lt;/p&gt;
&lt;p&gt;14、AWT 监听接口的四种实现方式：
1）在实现者之中给出接口的每个方法实现；
2）采用接口形式产生匿名对象给出实现；
3）采用事件适配器方式；
4）采用适配器形式产生匿名对象给出实现；&lt;/p&gt;
&lt;p&gt;15、可利用事件对象提供的方法得到事件源对象信息：
new ActionEvent().getSource() 返回事件源对象的引用。&lt;/p&gt;
&lt;p&gt;16、书中原话：（当然 Swing 也凉凉了）
虽然 AWt 在 Java 界面设计中的地位被 Swing 逐渐取代，但&lt;strong&gt;作为 Swing 理解和应用的基础&lt;/strong&gt;，AWT 仍然很&lt;strong&gt;重要&lt;/strong&gt;。&lt;/p&gt;
&lt;h1&gt;第十三章 Swing 图形用户界面&lt;/h1&gt;
&lt;p&gt;1、AWT 的组件都有 peer 组件，peer 与特定操作系统相关的内容；Swing 的大部分组件都有由纯 Java 实现的委托组件 ComponentUI 。&lt;/p&gt;
&lt;p&gt;2、Swing 采用 MVC 设计模式。
Model，View，Controller。&lt;/p&gt;
&lt;p&gt;3、大多数 AWT 组件名称前面加上“J”就是 Swing 中对应的组件。&lt;/p&gt;
&lt;p&gt;4、Swing 的容器&lt;strong&gt;大致&lt;/strong&gt;分为三类：
顶层容器、中间容器、特殊容器。&lt;/p&gt;
&lt;p&gt;5、编写 Swing 程序的步骤：
引入 Swing 包；
选择外观和感觉；
设置顶层容器；
设置 Swing 组件；
进行实践处理。&lt;/p&gt;
&lt;h1&gt;第十四章 I/O 输入/输出&lt;/h1&gt;
&lt;p&gt;1、流的分类：&lt;/p&gt;
&lt;p&gt;1）流动方向
输入流、输出流&lt;/p&gt;
&lt;p&gt;2）读取类型
字节流：InputStream 和 OutputStream 派生的一系列类
字符流：Reader 和 Writer 派生出的一系列类&lt;/p&gt;
&lt;p&gt;3）发生的源头
节点流：直接操作目标设备的流
过滤流：程序可以通过过滤流去操作节点流&lt;/p&gt;
&lt;p&gt;2、Java 标准输入/输出流
标准输入：对象是键盘，Java 对应的类是 System.in 。
标准输出：对象是屏幕，Java 对应的类是 System.out 。
标准错误输出：对象也是屏幕，Java 对应的类是 System.err 。&lt;/p&gt;
&lt;p&gt;3、输入输出流调用 close 的作用是释放占用的系统资源，不仅仅是释放一个文件引用。&lt;/p&gt;
&lt;p&gt;4、常用字符流：
1）文件流 FileInputStream、FileOutputStream
2）字节数组流 ByteArrayInputStream、ByteArrayOutputStream
3）管道流 PipedInputStream、PipedOutputStream
4）对象流 ObjectInputStream、ObjectOutputStream
5）过滤流 FilterInputStream、FilterOutputStream
6）缓冲流 BufferedInputStream、BufferedOutputStream
7）数据流 DataInputStream、DataOutputStream
8）打印流 PrintStream&lt;/p&gt;
&lt;p&gt;5、字符流和字节流的相互转化的过程叫做流的&lt;strong&gt;装配过程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;6、File 类不仅指系统中的文件，也指目录。因为目录也是特殊的文件。&lt;/p&gt;
&lt;p&gt;7、File 的含义和提供的方法使其很容易应用于递归之中。&lt;/p&gt;
&lt;p&gt;8、写入文件时，有覆盖和追加两种方式，在 FileOutputStream 的构造方法中指出。&lt;/p&gt;
&lt;p&gt;9、调用 flush 方法一次性将缓冲区的数据写入到 FileOutputStream 中。如果用 PrintStream，则调用 println 方法或者自动检测换行符，都会自动调用 flush 方法。&lt;/p&gt;
&lt;p&gt;10、流装配套路固定，但是可以灵活使用。&lt;/p&gt;
&lt;p&gt;11、对象串行化的目的是，便于网络传输和介质传输。&lt;/p&gt;
&lt;p&gt;12、对象持久化：内存中的对象保存在硬盘介质上。&lt;/p&gt;
&lt;p&gt;13、编列（Marshaling）就是将参数（不同机器之间调用方法时的参数传输比本地要复杂一点）转化为可以传输的形式。串行化之后编列。&lt;/p&gt;
&lt;p&gt;14、Interface Serializable 中没有任何方法。他是对象串行化的一个标志，表明一个实现类加入对象串行化协议。&lt;/p&gt;
&lt;h1&gt;第十五章 Java 网络通信&lt;/h1&gt;
&lt;p&gt;1、Java 网络编程从传输层开始，并分为高层次网络编程（基于应用层）和低层次网络编程（基于传输层）。&lt;/p&gt;
&lt;p&gt;数据链路层的编程和通信与设备紧密相关；网络层编程和操作系统紧密相关。这都不适合具有平台移植性的 Java 。&lt;/p&gt;
&lt;p&gt;2、URL 最后可精确到一个文件名，或者文件内部的一个锚点。&lt;/p&gt;
&lt;p&gt;加上锚点可以指定到页面上的某个元素，例如网站登录之后，回到登录前点选中的某个标签上，可以使用锚点来跳转回来。&lt;/p&gt;
&lt;p&gt;3、一个 URL 对象生成之后，其属性不能改变，只能访问（通过其提供的方法）。&lt;/p&gt;
&lt;p&gt;4、Java 下 Socket 编程主要指基于 TCP/IP 协议的网络编程。
注意：Socket 支持的协议种类不止 TCP/IP 协议一种，而可以操作 TCP/IP 协议的接口不止 Socket 一个。两者没有必然联系。&lt;/p&gt;
&lt;p&gt;5、Socket 和 URL 应用场景不同。
Socket 面向内部网络通信；
URL 适用于面向 Internet 通信。&lt;/p&gt;
&lt;p&gt;6、在关闭 Socket 之前，还应该关闭与 Socket 相关的所有输入/输出流。&lt;/p&gt;
&lt;p&gt;7、bind() 方法没有调用时，系统会自动绑定本地地址和一个可用的端口。&lt;/p&gt;
&lt;p&gt;8、DatagramSocket 用于在程序之间建立传送数据报的通信连接，DataGramPacket 则用来表示一个数据报。&lt;/p&gt;
&lt;p&gt;9、Socket 中的地址和端口号是目的机器的地址和端口号，而 DatagramSocket 中的则是本机的。&lt;/p&gt;
&lt;h1&gt;第十六章 JDBC&lt;/h1&gt;
&lt;p&gt;1、JDBC 全称为：Java DataBase Connectivity
他是一种规范，统一 Java 应用程序对&lt;strong&gt;关系数据库&lt;/strong&gt;的访问。&lt;/p&gt;
&lt;p&gt;2、数据库的种类：（6 种）&lt;/p&gt;
&lt;p&gt;层次数据库（Hierarchical Database，HDB）&lt;/p&gt;
&lt;p&gt;关系型数据库（Relational Database，RDB）&lt;/p&gt;
&lt;p&gt;面向文档（Document-Oriented）数据库&lt;/p&gt;
&lt;p&gt;列存储（Column-oriented）数据库&lt;/p&gt;
&lt;p&gt;XML 数据库（XML Database，XMLDB）&lt;/p&gt;
&lt;p&gt;键值存储数据库（Key-Value Store，KVS）&lt;/p&gt;
&lt;p&gt;3、数据库的作用是存储数据，同一个应用程序对存储到数据库中的数据使用逻辑往往都是一样的。&lt;/p&gt;
&lt;p&gt;4、先有 Java.sql 中的各种接口的定义，再由数据库厂商按照接口定义来做它们的驱动程序。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;（好家伙！一次工作转嫁，一辈子编程吃香。）&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;5、JDBC 驱动类型有三类：
1）纯 Java 驱动
2）JDBC 调用客户端
3）JDBC/ODBC 桥&lt;/p&gt;
&lt;p&gt;6、使用 JDBC 操作数据库的步骤：（五个步骤）&lt;/p&gt;
&lt;p&gt;1）载入 JDBC driver
// 类型不同，会有不同的载入操作。&lt;/p&gt;
&lt;p&gt;2）得到数据库的 Connection 对象
// 注意，Connection、Statement、ResultSet 都是接口，要用特定的方式产生对象。&lt;/p&gt;
&lt;p&gt;3）根据连接对象得到 Statement 进行查询或更新数据。&lt;/p&gt;
&lt;p&gt;4）如果执行查询，则对记录集 ResultSet 进行遍历操；如果执行更新，则根据成功与否的返回结果进行数据库事务操作。
// 这里有很多细节操作值得注意，能够让运用更加灵活。&lt;/p&gt;
&lt;p&gt;5）操作结束后，&lt;strong&gt;依次&lt;/strong&gt;对 ResultSet、Statement、Connection 执行关闭操作。
// 注意，依次关闭，且由于容易发生异常，应该放在 finally 中执行。&lt;/p&gt;
&lt;p&gt;7、在 finally 当中对接口引用对象关闭时，其接口声明应该在 try-catch-finally 之外。并且，finally 中也应该要进行异常捕获。&lt;/p&gt;
&lt;p&gt;8、虽然 JDBC 面对不同数据库的代码不完全相同，但是已经做到了核心功能代码部分能够重用了。&lt;/p&gt;
&lt;h1&gt;第十七章 UML 简介&lt;/h1&gt;
&lt;p&gt;粗略的看了看，没什么记录。&lt;/p&gt;
&lt;h1&gt;第十八章 设计模式&lt;/h1&gt;
&lt;p&gt;1、GoF 模式
Gang of Four 四人组
系统的整理和描述了 23 种个精选的设计模式。&lt;/p&gt;
&lt;p&gt;2、面向对象语言学习是一种规则学习，而设计模式学习则是进行面向对象设计思想的学习。&lt;/p&gt;
&lt;p&gt;3、每个 GoF 模式都有四个要素：
模式名称、问题、解决方案、效果。&lt;/p&gt;
&lt;p&gt;4、GoF 模式根据范围可分为类模式和对象模式两种，根据目的可分为创建型、结构型、行为型三种。&lt;/p&gt;
&lt;p&gt;5、模式原则：&lt;/p&gt;
&lt;p&gt;1）开闭原则
开闭原则是指软件系统对拓展开放，对修改关闭。
开闭原则的具体实现方法是，依赖接口编程而不是依赖实现编程。&lt;/p&gt;
&lt;p&gt;2）组合 /聚合复用原则
继承是白箱复用，而组合聚合则是黑箱复用。
在复用选择时应该优先选择黑箱复用，有四个优点。&lt;/p&gt;
&lt;p&gt;3）高内聚松耦合原则
耦合是指两个对象相互依赖对方的程度，内聚是指一个对象内部功能相联的程度。
（注意两点：类的单一职责，以及接口隔离）&lt;/p&gt;
&lt;h1&gt;第十九章 软件框架&lt;/h1&gt;
&lt;p&gt;1、软件框架（Software Framework）是对通用功能操作进行抽象的软件。&lt;/p&gt;
&lt;p&gt;2、软件框架的四个特点：
1）基于框架的应用程序的流程控制在框架内部；
2）框架针对通用功能给出了默认实现；
3）框架内部代码不可被用户修改；
4）框架可以被应用程序有选择的覆盖和细化来实现功能。&lt;/p&gt;
&lt;p&gt;3、框架是软件，设计模式是软件的知识。&lt;/p&gt;
&lt;p&gt;4、常见的框架技术路线主要有两类：
1）微软的 .NET Framework；
2）基于 J2EE 的 Java 应用框架。&lt;/p&gt;
&lt;p&gt;5、Srtuts 是实现 MVC （Model-View-Controller）架构的框架，分为 Struts1 和 Struts2。&lt;/p&gt;
&lt;p&gt;6、Spring 是全方位的应用程序框架，功能强大。&lt;/p&gt;
&lt;p&gt;7、AOP （Aspect-Oriented Programming） 是相对于面向过程、面向对象编程而言一种新的编程思想。Spring 提供面向对象编程的 AOP 子框架。&lt;/p&gt;
&lt;p&gt;8、应用过于依赖框架不是件好事。如果能做到应用不依赖于框架，控制关系就会反向（IoC，Inversion of Control）。&lt;/p&gt;
&lt;p&gt;9、Struts1 框架负责为 Model、View、Controller 三者之间的关系调用提供一个基本的环境。&lt;/p&gt;
&lt;p&gt;10、Struts1 有四个不足之处：
1）框架同应用是正向控制关系，即应用依赖于框架；
2）易产生线程安全问题；
3）ActionForm 的冗余问题；
4）表现层单一：只支持 JSP。&lt;/p&gt;
&lt;p&gt;11、Struts2 源于 WebWork，可以看做是 WebWork 的升级版，而不是 Struts1 的升级版。&lt;/p&gt;
&lt;p&gt;12、Struts1 和 Struts2 的编程思维截然不同。&lt;/p&gt;
&lt;p&gt;13、模板技术是指将视图显示和业务规则代码分离的一种技术。&lt;/p&gt;
&lt;p&gt;14、Struts2 的 Controller 室友核心控制器 FilterDispatcher 和 Action 组成的。&lt;/p&gt;
&lt;p&gt;15、一个 Action 的拦截器数量无要求。&lt;/p&gt;
&lt;p&gt;16、Struts2 核心功能放在拦截器（Interceptor）上。&lt;/p&gt;
&lt;p&gt;17、Struts2 与 Struts1 的对比：
1）Struts2 是个 IoC 反向控制容器，大大降低两类框架和应用的耦合；
2）Struts2 为每一个请求单独建立一个 Action 对象，从而不会产生线程安全问题；
3）Struts2 视图通过支持模板技术拓展了表现类型；
等等...&lt;/p&gt;
&lt;h1&gt;第二十章 软件体系结构与分布式对象技术&lt;/h1&gt;
&lt;p&gt;1、软件体系结构：
两种成分的集合：其一，具有一定形式的软件结构化元素；其二，他们之间的关系。&lt;/p&gt;
&lt;p&gt;2、软件系统的分类：
1）从构成角度划分
构件、他们之间的调用关系
2）从开发过程角度划分
概念结构、物理结构
3）从运行角度划分
静态结构、动态结构
4）从部署的角度
集中式结构、&lt;strong&gt;分布式结构&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;3、常见的分布式系统结构：
C/S 以及 B/S&lt;/p&gt;
&lt;p&gt;4、C/S 的特点是必须在客户端安装数据库客户端。&lt;/p&gt;
&lt;p&gt;5、客户端三种类型：
胖客户端：C/S 结构中的客户端；
瘦客户端：B/S 结构中的浏览器端；
富客户端：介于二者之间。&lt;/p&gt;
&lt;p&gt;6、富客户端又称富互联网应用程序（Rich Internet Application，RIA），其主要解决&lt;strong&gt;通信&lt;/strong&gt;和&lt;strong&gt;界面表示&lt;/strong&gt;两个问题。&lt;/p&gt;
&lt;p&gt;7、RIA 技术主要有两类：其一是内嵌于浏览器中的 Ajax；其二是在浏览器中安装插件。&lt;/p&gt;
&lt;p&gt;8、Ajax（Asynchronous JavaScript And XML，动态 JavaScript 和 XML）不是新的编程语言，而是一种使用现有标准的新方法。
Ajax 最大的优点是在不重新加载整个页面的情况下，可以与服务器交换数据并更新部分网页内容。&lt;/p&gt;
&lt;p&gt;9、分布式软件系统就是将&lt;strong&gt;物理上分散&lt;/strong&gt;的独立构件或系统，在&lt;strong&gt;使用逻辑上&lt;/strong&gt;统一起来，相互合作来共同完成任务。&lt;/p&gt;
&lt;p&gt;10、分布式软件系统需要解决的问题：
1）网络通信协议和通信方式的选择；
2）提供远程过程调用（Remote Procedure Call，RPC）接口；
3）名称查找，快速在网络上找到需要调用服务的计算机名称、地址、端口等；
4）安全机制，分布式软件系统彼此进行通信时需要确保数据传输和身份认证的安全性；
5）事务管理。&lt;/p&gt;
&lt;p&gt;11、中间件是将不同软件构件或者操作者与多种应用程序连接起来的软件。&lt;/p&gt;
&lt;p&gt;12、中间件的技术等级处于操作系统和应用程序“中间”。同框架相比，中间件是可以独立运行的成品软件。同操作系统相比，中间件只解决分布式软件系统某一类共性问题。&lt;/p&gt;
&lt;p&gt;13、中间件的划分多种多样。&lt;/p&gt;
&lt;p&gt;从中间件的&lt;strong&gt;应用类型&lt;/strong&gt;上分 10 类：
数据访问中间件、远程过程调用中间件、交易中间件、&lt;strong&gt;消息中间件&lt;/strong&gt;、对象中间件、应用服务器中间件、工作流中间件、门户中间件、安全中间件、企业应用集成中间件。&lt;/p&gt;
&lt;p&gt;从中间件的通信实现机制上分 3 类：
远程过程调用（RPC）、利用消息进行通信、对象请求代理（ORB）。&lt;/p&gt;
&lt;p&gt;14、消息是需要传递的数据。&lt;/p&gt;
&lt;p&gt;15、消息分类：
1）按照参与者数量划分：点到点、发布/订阅；
2）按照传递方向：推、拉；
3）按照通信方式：同步、异步。&lt;/p&gt;
&lt;p&gt;16、JMS （Java Message Service）是一组 Java 应用程序接口，它规定了创建、发送、接受、读取消息的一系列标准。&lt;/p&gt;
&lt;p&gt;17、JMS 的应用的构成：
1）JMS 提供者
2）JMS 客户端
3）消息对象
4）管理对象&lt;/p&gt;
&lt;p&gt;18、分布式对象技术是一种面向对象技术。作用是，将分布在网络上的资源按照对象的概念来组织。其有两方面内容：其一，分布式对象的标准规范；其二，对标准规范的实现。&lt;/p&gt;
&lt;p&gt;19、OMG 的 CORBA 主要是对标准进行规定。
JavaRMI、JNDI、COM+ 既是标准也是实现。
WbeService 采用 XL 格式规定了一系列由 W3C 国际组织确认的标准规范。&lt;/p&gt;
&lt;p&gt;20、CORBA（Common Object Request Broker Architecture）标准内容主要包括四个部分：对象模型、语言映射、互操作协议、对象服务。&lt;/p&gt;
&lt;p&gt;21、CORBA 体系结构及对象模型主要由四部分组成：
ORB 内核、对象适配器、客户端存根（Syub）、服务器端骨架（Skeleton）组成。&lt;/p&gt;
&lt;p&gt;22、ORB（Object request Broker）是一种能将客户端对象请求转化为服务端对象调用的机制。&lt;/p&gt;
&lt;p&gt;23、微软的分布式对象技术经历了 COM、DCOM、MTS、COM+ 几个主要阶段。&lt;/p&gt;
&lt;p&gt;24、Java 2 平台提供了两种方法来构造分布式应用系统：
Java RMI（Remote Method Invocation）
Java IDL（Interface Definition Language）&lt;/p&gt;
&lt;p&gt;25、JNDI （Java Namig and Directory Interface）是为 Java 应用程序提供目录命令和命名服务功能的接口，负责对 Java 分布式对象进行管理和检索。&lt;/p&gt;
&lt;p&gt;26、RMI 可以说是 Java 的 RPC 技术，即一个 JVM 内存空间对象可以获得另一个 JVM 内存空间对象的远程引用，并向其发送消息。&lt;/p&gt;
&lt;p&gt;27、WebService 结构层次有四层：
发现服务——UDDI
服务描述——WSDL
服务调用——SOAP
传送协议——HTTP&lt;/p&gt;
&lt;p&gt;28、WSDL（Web Services Description Language）是基于 XML 格式被 W3C 组织确认的标准协议，用于对 Web 服务接口和服务位置进行描述。&lt;/p&gt;
&lt;p&gt;29、UDDI（Universal Description，Discovery and Integration）是一种独立于平台、基于 XML 格式、用于在互联网上描述 Web 服务的协议。&lt;/p&gt;
&lt;p&gt;30、SOAP（Simple Object Access Protocol）是有微软、IBM 等公司发起由 W3C 组织确认的标准协议。它采用 XML 格式定义，可以将本地方法调用转为适合网络传输的形式（SOAP 请求），同时将 Web 服务经网络传输返回的调用结果转化为本地方法的返回值（SOAP 响应）。&lt;/p&gt;
</content:encoded></item><item><title>Java 部分课后题及答案</title><link>https://0110wdj.github.io/snofly-blog/posts/java/java-practice/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/java/java-practice/</guid><pubDate>Wed, 20 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;版本&lt;/h1&gt;
&lt;p&gt;《Java 面向对象程序设计》（第 2 版）袁绍欣 安毅生 赵祥模 葛玮 编著
清华大学出版社&lt;/p&gt;
&lt;h1&gt;声明&lt;/h1&gt;
&lt;p&gt;我自己写的答案，公布出来的用意在于方便与同学，与大家交流学习。我不希望这里出现那种只会抄作业的人，抄作业行为没有长远意义。&lt;/p&gt;
&lt;p&gt;2021.02.20 回顾补充：
没想到原来没完成的文章又有了动力去完成，只感叹时光如梭啊。这次重新学习，温故知新，或许有更多的体会，以及对细节的把握、对思想的理解。&lt;/p&gt;
&lt;p&gt;2022.05.21 回顾补充：
如果读者是学生，这篇文章可能会对你产生误导，因为这是一个缺乏工程应用经验的学生写的，过于偏重理论知识，但又说不清楚某个技术存在的意义是什么。建议对 Java 感兴趣的人去找经典书籍阅读。&lt;/p&gt;
&lt;h1&gt;第一章&lt;/h1&gt;
&lt;h2&gt;1.Java 语言有哪些特点？&lt;/h2&gt;
&lt;p&gt;答：简单、面向对象、分布式、解释型、健壮、安全、与体系结构无关、可移植、高性能、多线程和动态执行。&lt;/p&gt;
&lt;p&gt;Sun 公司对 Java 的定义：
A simple, object-oriented, distributed, interpreted, robust, secure, architecture-neutral, portable, high-performance, multi-threaded, and dynamic language.&lt;/p&gt;
&lt;h2&gt;2.下载一个 JDK 和一个 JRE 版本，进行安装，并配置环境变量，建立编译和运行环境。&lt;/h2&gt;
&lt;p&gt;我遇到了问题，之后写了个问题总结。(链接失效)&lt;/p&gt;
&lt;p&gt;（我直接下载的 jdk-11.0.2 版本，自带 jre。）&lt;/p&gt;
&lt;h2&gt;3.Java 平台分成几类？他们的适用范围各是什么？&lt;/h2&gt;
&lt;p&gt;答：目前 Java 平台分为 J2EE、J2SE、J2ME 三个平台。
J2EE 主要目的是为==企业==计算提供一个应用服务运行和开发平台。
J2SE 主要目的是为==台式机和工作站==提供一个开发和运行的平台。
J2ME 主要面向==消费电子产品==，为消费电子产品提供一个 Java 的运行平台。&lt;/p&gt;
&lt;h2&gt;4.分别编写 Application 和 Applet 程序，输出字符串“My frist Java!”。请记下编译和执行过程出现的问题。&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Appliction 的 Java 程序&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyJava {
	public static void main(String args[]){
		system.out.println(&quot;My frist Java!&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Applet 的 Java 程序&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.awt.*;
//java.awt包含用于创建用户界面和绘制图形图像的所有分类
import java.applet.*;
public class MyJavaApplet extends Applet {
	public void init(){
	}
    public void paint(Graphics g){
		g.drawString(&quot;My frist Java!&quot;,25,25);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对应的 html 代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;applet code=&quot;MyJavaApplet&quot; width=&quot;150&quot; height=&quot;100&quot;&amp;gt; &amp;lt;/applet&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主要问题&lt;/strong&gt;
文件命名是的统一关系；
java 环境配置的相关问题；
不会用浏览器运行小程序。&lt;/p&gt;
&lt;p&gt;补充：因为历史原因，applet 在各种浏览器中支持是不同的，所以新手容易产生疑惑。背后的故事很有趣，可以了解一下 Applet 和 ActiveX 的发展历史。&lt;/p&gt;
&lt;h2&gt;5.改造第 4 题的程序，将两程序合为一个程序，既能作为 Application 执行，又能作为 Applet 程序执行。&lt;/h2&gt;
&lt;p&gt;目前不会。
更新补充：
成功合并且运行的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.awt.*;
import java.applet.*;
public class HelloWorldApplet extends Applet{
	public void init(){
	}
	public static void main(String args[]){
		System.out.println(&quot;Hello World!&quot;);
	}
	public void paint(Graphics g){
		g.drawString(&quot;Hello World!&quot;,25,25);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;问题笔记&lt;/em&gt;
将 main()那一部分合并在同一个 class 类里就可以了（外层只保留了一个类），注意名称。&lt;/p&gt;
&lt;h1&gt;第二章&lt;/h1&gt;
&lt;h2&gt;1.下面那些标识符是合法的？ （D）。&lt;/h2&gt;
&lt;p&gt;A. 8ID 　　 S#160 　　 B. -MU 　　 C. SY#　　 D. _S9&lt;/p&gt;
&lt;p&gt;（标识符是以字母、下划线 “_” 、美元符 “$” 开始的一个字符序列，后面可以接字母、下划线 “_” 、美元符 “$” 、数字。）&lt;/p&gt;
&lt;h2&gt;2.那些数据类型可以充当 swith 语句的条件？&lt;/h2&gt;
&lt;p&gt;必须是一下四种基本数据类型（不能是包装类）：
byte 、short 、int 、char 。&lt;/p&gt;
&lt;h2&gt;3.下面片段的执行结果是**__**。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int x = 3;
int y = 1;
if(x = y){
	System.out.println(&quot;x = &quot; + x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A. x = 1 　　 B. x = 3 　　 C. 编译失败　　 D. 无输出&lt;/p&gt;
&lt;p&gt;选 C，我猜的。&lt;/p&gt;
&lt;p&gt;补充：答案选择 C。if 括号中只能是布尔型数据。&lt;/p&gt;
&lt;p&gt;再补充：x = y 是赋值语句，结果类型是 int 类型，不是 boolean 类型。如果 x 和 y 本身就是 boolean 类型，那么 x=y 结果就为 boolean 类型，可以编译通过。&lt;/p&gt;
&lt;h2&gt;4.定义一个浮点变量 s ，写成“Float s = 2.3;” ，错在什么地方？如何修改正确？&lt;/h2&gt;
&lt;p&gt;用 2.3 赋值变量 s 时，默认产生的应该是 double 类型变量，于是类型错误。改正方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float s = 2.3f;//增加后缀
float s2 =(float)2.3;//类型强制转换

补充：
double s3 = 2.3f 这种“小变量赋值大变量“写法是允许的。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5.下面程序片段的执行结果是**__**。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int i = 0, j = 1;
if((i++ == 1) &amp;amp;&amp;amp; (j++ == 2)){
	i = 42;
}
System.out.println(&quot;i = &quot; + i + &quot;,j = &quot; + j);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A. i = 1, j = 2 　　 B. i = 1, j = 1
C. i = 42, j = 2 　 D. i = 42, j = 1
我选 A，就是个前后++问题。&lt;/p&gt;
&lt;p&gt;补充：
这里考虑的有两点：++运算符的前后缀；&amp;amp;&amp;amp;的判断机制与&amp;amp;的判断机制不同。&lt;/p&gt;
&lt;h2&gt;6.定义一个一维数组有几种方法？&lt;/h2&gt;
&lt;p&gt;一维数组声明有两种形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//类型标识符 数组名[];
type name [];
//类型标识符[] 数组名;
type [] name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一维数组初始化有三种方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1、使用关键字new进行定义//未手动赋值，系统默认赋值
type name[] = new type[length];
type[] name = new type[length];
2、直接在声明的时候初始化//注意只能写在同一行代码中
String[] s = {&quot;ab&quot;,&quot;bc&quot;,&quot;cd&quot;};
int[] a = {3,4,5,6};
3、结合的方法
float f4[] = new float[]{1.0f,2.0f,3.2f};
String[] dogs = new String[]{
	new String(&quot;大黄&quot;), new String(&quot;二黑&quot;)};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特别补充：
声明和定义的区别在于是否建立变量空间：
把建立空间的称为“定义”；
把不需要建立存储空间的称为“声明”。&lt;/p&gt;
&lt;p&gt;基本数据类型变量的声明和定义(默认初始化)是同时产生的；
而对于类对象来说，声明和定义是分开的。&lt;/p&gt;
&lt;h2&gt;7.编写 JavaApplication，求出 e=1+1/1!+1/2!+1/3!+···+1/n!···的近似值，要求误差小于 0.00001。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class fore(){
	public static void main(String[] args){
        double e=1.0,t=1.0;
        int i;
        //指数-6、-7也可，但没必要
        for(i=1;t&amp;gt;1e-5;++i){
        	t*=1.0/i;
            e+=t;
        }
        System.out.print(e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8.利用可变列数组实现乘法口诀打印。&lt;/h2&gt;
&lt;p&gt;样例：
1 X 1 = 1
2 X 1 = 2 　 2 X 2 = 4
... ...
&lt;em&gt;运行成功的代码&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MultiplicationTable{
    public static void main(String[] args) {
        //确定每一行的列数
        String [][] arra;
        arra = new String[9][];
        for(int i=0;i &amp;lt; 9; i++) {
            arra[i]=new String[i+1];
        }
        //根据数组下标长度，直接打印，没必要存储
        for(int i = 0 ; i &amp;lt; 9 ; i++) {
            for(int j = 0 ; j &amp;lt; i + 1 ;j++) {
                int ji;
                ji = ++i*++j;//注意数组下标从0开始
                System.out.print(i + &quot; &quot; + &quot;x&quot; + &quot; &quot; + j + &quot; = &quot; + ji + &quot;\t&quot;);
                i--;j--;
            }
            System.out.println(&quot;&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9.编程输出。&lt;/h2&gt;
&lt;p&gt;样例： *
*.*.
*. .*. .*. .
*...*...*...*...
(各个符号之间没有空格，求输出八阶图形，即共有 8 行输出)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;运行成功的代码&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class mystart {
    public static void main(String[] args){
        int n = 8;//阶数控制
        System.out.println(&quot;*&quot;);
        for(int i = 1 ; i &amp;lt; n ; i++) {
            for(int k = i+1;k &amp;gt; 0;k--){
                System.out.print(&quot;*&quot;);
                for(int j = 1 ; j &amp;lt; i + 1 ;j++) {
                    System.out.print(&quot;.&quot;);
                }
            }
            System.out.println(&quot;&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补充说明：
这里对 i=0 的情况单独处理的，逻辑上没问题，但是代码不够美观。&lt;/p&gt;
&lt;p&gt;精修一下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class mystart {
    public static void main(String[] args){
        int n = 8;//阶数控制
        for(int i = 0 ; i &amp;lt; n ; i++) {
            for(int k = i+1;k &amp;gt; 0;k--){
                System.out.print(&quot;*&quot;);
                for(int j = 1 ; j &amp;lt; i + 1 ;j++) {
                    System.out.print(&quot;.&quot;);
                }
            }
            System.out.println(&quot;&quot;);
        }
    }
}//原来我以前写的代码还是挺好的嘛
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第三章&lt;/h1&gt;
&lt;h2&gt;1.编写程序为什么要写注释？&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;因为我高兴呀&lt;/s&gt;
提高程序的可读性以及管理上的方便。&lt;/p&gt;
&lt;p&gt;（2021 的我：“19 年的我挺皮的。”）&lt;/p&gt;
&lt;h2&gt;2.Java 都有哪些命名规范？&lt;/h2&gt;
&lt;p&gt;1.包名中的字母一律小写； 2.类名、接口名的每一个单词的首字母大写； 3.方法名中第一个单词首字母小写，其他单词首字母大写； 4.常量中的每一个字母都大写。&lt;/p&gt;
&lt;p&gt;注：这些要求过于基础，我自己想跟着阿里的 java 开放规范来写程序。&lt;/p&gt;
&lt;h2&gt;3.Javadoc 生成帮助文档的时候，都有哪些命令行开关，他们的作用是什么？&lt;/h2&gt;
&lt;p&gt;以 javadoc 开头 :
-d 文件存储位置&lt;br /&gt;
-head 文件头部名称&lt;br /&gt;
-version 显示文件版本&lt;br /&gt;
-author 显示作者&lt;br /&gt;
xxx.java 处理的 java 源文件&lt;/p&gt;
&lt;h2&gt;4.选择前两章的一道练习题，加上适当的注释，并帮助其产生文档。&lt;/h2&gt;
&lt;p&gt;第二章第 9 题，改写后得到的效果：
&lt;img src=&quot;dd47ffb0abaa2174da4def382b381a4e.png&quot; alt=&quot;第一页&quot; /&gt;
&lt;img src=&quot;00c678a139ebfa468427edf46cc5b28d.png&quot; alt=&quot;第二页&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;第四章&lt;/h1&gt;
&lt;h2&gt;1.名词解释：构造方法、抽象。&lt;/h2&gt;
&lt;p&gt;1.构造方法是一个与类名相同的类方法。
构造方法属于类，不属于任何对象；
构造方法与类同名，且不能指定任何返回值类型（其隐含的返回值由系统内部使用），包括 void；
系统有一个默认的无参数的构造方法，可以覆盖。&lt;/p&gt;
&lt;p&gt;用途是，每当使用 new 关键字创建一个对象时，为新建对象开辟了内存空间之后，Java 系统将自动调用构造方法初始化这个对象。（不管有没有对象引用）&lt;/p&gt;
&lt;p&gt;2.抽象：抽象是从被研究对象中舍弃个别的、非本质的或与研究主旨无关的次要特征，而抽取与研究有关的共性内容加以考察，形成对研究问题正确、简明扼要的认识。&lt;/p&gt;
&lt;h2&gt;2.对象位于内存何处？声明能引用对象的实质是什么？&lt;/h2&gt;
&lt;p&gt;对象在堆内存上；
实质：对象引用在方法栈中保留了堆中对象的实际地址。&lt;/p&gt;
&lt;h2&gt;3.对象和基本数据类型作为参数传递时，有什么不同？&lt;/h2&gt;
&lt;p&gt;对象作为参数在方法中的传递是是引用传递，传递的是对象的地址；
基本数据类型作为参数在方法中的传递是值传递，传递的是拷贝的值。&lt;/p&gt;
&lt;h2&gt;4.在自定义对象中写 finalize 方法，看看什么情况下 finalize 被调用。&lt;/h2&gt;
&lt;p&gt;当该对象成为垃圾之后，且系统 GC 机制触发，调用 finalize 方法释放资源。&lt;/p&gt;
&lt;h2&gt;5.对象在什么条件下成为垃圾？什么情况下释放垃圾对象，如何证明一个对象被释放了？&lt;/h2&gt;
&lt;p&gt;1.对象在没有任何引用的时候成为垃圾； 2.系统在资源不够的情况下可能进行释放，也可以用 System.gc()方法“提醒”系统进行垃圾释放，但是进行垃圾回收的线程级别很低，只有系统不忙时才会理睬提醒； 3.该对象占用的资源（对象在堆中占用的空间、对其他设备等资源的占用）被释放时。&lt;/p&gt;
&lt;h2&gt;6.final 修饰符都有什么作用？&lt;/h2&gt;
&lt;p&gt;final 修饰的类属性为常量，相比于直接使用常量数值，起着助记符的作用；
final 修饰的类方法在子类当中不能被覆盖，作为最终方法，可以保证程序的安全性和正确性。&lt;/p&gt;
&lt;h2&gt;7.static 修饰的属性和方法有什么特点？&lt;/h2&gt;
&lt;p&gt;用 static 修饰的域变量专属于类，保存在类的内存区（堆中）的公共存储单元中。
用 static 修饰的方法称为静态方法，属于类方法，不属于类的任何对象。&lt;/p&gt;
&lt;h2&gt;8.Application 程序执行时为什么不能带后缀名？&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;因为这是规定&lt;/s&gt;
&lt;em&gt;网络引用答案如下：&lt;/em&gt;
若写 java xxx.yyy
是代表要运行 package xxx 中的 class yyy 里面的 main(String[])
所以当你写 java xxx.class 时, 它会以为要找一个叫 xxx 的 package 里面的一个叫 class 的 class 。&lt;/p&gt;
&lt;h2&gt;9.下面的代码中，Vector 是 java.util 包中的一个类，关于此类哪个叙述是正确的？（）&lt;/h2&gt;
&lt;p&gt;1）public void create(){
2）Vector myVect;
3）myVect = new Vector();
4）}
A. 第二行的声明不会为变量 myVector 分配内存空间。
B. 第二行的声明分配了一个 Vector 对象内存空间。
C. 第二行语句创建一个 Vector 类对象。
D. 第三行语句创建一个 Vector 类对象。
E. 第三行语句为一个 Vector 类对象分配内存空间。&lt;/p&gt;
&lt;p&gt;我选 D，暂时解释不清。&lt;/p&gt;
&lt;p&gt;补充：DE 仍有不清晰的地方。
补充解析：
A 选项，变量 myVector 是有一定大小的，因为里面要存放将来引用的对象在堆中的地址。
B 选项，未分配。Java 中，需要 new 关键字来分配。
C 选项，只是声明，还不存在对象。
D 选项，我的理解第三行作用，创建 Vector 对象并将对象赋予声明 myVector。
E 选项，对象在创建的同时就在堆上分配空间了。&lt;/p&gt;
&lt;h2&gt;10.请在 display 函数中用递归方式输出如下图形。&lt;/h2&gt;
&lt;p&gt;n n n … n
…
3 3 3
2 2
1
void display(n){…}
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class display{
    static void display(int n){
        if (n &amp;gt;= 1){
            for (int i = 0; i &amp;lt; n; i++){
                System.out.print(n + &quot; &quot;);
            }
            System.out.println();
            display(n - 1);
        }else{
            return ;
        }
    }
    public static void main(String[] args){
        display(5);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;11.象棋跳马。&lt;/h2&gt;
&lt;p&gt;国际象棋中，马的规则是每步棋先横走或直走一格，然后再斜走一格。在下图中根据马的当前位置，其可以走的位置共有 8 种，分别用 1 到 8 来表示。如果马以 0 方格为起点开始起跳，则跳满棋盘中所有位置为一趟，请问共有多少趟（种）跳法？请分别显示每趟跳法的轨迹。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;58c916fd5b728c57e46d58ed783dba23.jpg&quot; alt=&quot;其实可以手绘来着&quot; /&gt;
这一题的代码，是我参考了同学的思路，简单改改得到的。
最后没有完全解决问题。&lt;/p&gt;
&lt;p&gt;文章链接
https://blog.csdn.net/weixin_44728197/article/details/88624747&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//格式控制
import java.text.DecimalFormat;
public class test {
    static DecimalFormat df = new DecimalFormat(&quot;00&quot;);
    void printchess(test my)
    {
        int a,b;
        for(a=2;a&amp;lt;=9;a++)
        {
            for(b=2;b&amp;lt;=9;b++)
            {
                if(my.chessboard[a][b]!=0)
                System.out.print( df.format(my.chessboard[a][b])+&quot; &quot;);
                else{
                    System.out.print( &quot;   &quot;);
                }
            }
            System.out.println();
        }
    }
    int horse(test my,int x,int y)
    {
        int i;
        for(i=0;i&amp;lt;8;i++)
        {
            if(my.chessboard[x+my.move[i][0]][y+my.move[i][1]]==0)
            {
                if(my.chessboard[4][3]!=0 &amp;amp;&amp;amp; my.chessboard[3][4]!=0
                &amp;amp;&amp;amp; my.chessboard[6][3]!=0&amp;amp;&amp;amp; my.chessboard[3][6]!=0
                &amp;amp;&amp;amp; my.chessboard[4][7]!=0&amp;amp;&amp;amp; my.chessboard[7][4]!=0
                &amp;amp;&amp;amp; my.chessboard[7][6]!=0&amp;amp;&amp;amp; my.chessboard[6][7]!=0 )
                {
                    return 1;//八个格子跳完，返回1，sum加1
                }
                my.chessboard[x+my.move[i][0]][y+my.move[i][1]]=++my.cnt;//标记走了这一格
                my.horse(my,x+my.move[i][0],y+my.move[i][1]);//递归
            }
        }
        return 0;//中途断掉，sum不加
    }
    int[][] chessboard = new int[12][12];
    int cnt = 0;//标记马已走的方格数
    //int sum = 0;//标记马走完全程的具体方案数
    int[][] move = {{2,1},{1,2},{-1,2},{-2,1},{-2,-1},{-1,-2},{1,-2},{2,-1},};
    //每一种走法都可以中途变化，而不是move中按照固定顺序来，直到第一种走不通才会走下一步
    //每个马有8个位置可以跳，这是8中情况下x,y的偏移量
    public static void main(String[] args) {
        int sum=0;
        int control=0;
        while(control!=-1){
            test my = new test();
            int a,b;
            for(a=0;a&amp;lt;12;a++)
            {
                for(b=0;b&amp;lt;12;b++)
                {
                    my.chessboard[a][b]=0;
                }
            }
            for(a=0;a&amp;lt;12;a++)
            {
                for(b=0;b&amp;lt;12;b++)
                {
                    if(a==0||a==1||a==10||a==11||b==0||b==1||b==10||b==11)
                    {
                        my.chessboard[a][b]=-1;
                        //在8x8棋盘外面加两层，使得每个位置都有8种跳法，只不过这些多出来的位置标记为已跳
                    }
                }
            }
            my.chessboard[2][2]=++my.cnt;//开始位置为第一个已跳位置
            sum+=my.horse(my,2, 2);
            my.printchess(my);

            //while(control&amp;lt;(8*8*8*8*8*8*8*8))
            //一个简单但是效率极低的思路：
            //改变move数组，循环得到全部的解，中途每得到一个解，就对sum加1
            //control控制循环次数，即move排列组合的情况,循环次数为16777216次
            //然后就不用写了，我的计算机说它还想多活两年
            control=-1;//通过简单转变，控制循环次数
        }
        //System.out.println(sum);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补充：
这题目条件没有表达清楚。
1、能否跳在已经跳过的格子？
只能默认为不能，这样问题的解才有限。&lt;/p&gt;
&lt;p&gt;2、跳满所有位置，是指 64 个格子还是 8 个格子？
“8 个位置”这句话和“跳满”这句话是分开的两句话，前者更像是用于说明马的走法的。我选择默认为要求 8 个格子，反正解法都相当的。&lt;/p&gt;
&lt;p&gt;3、跳马的规则描述是，先直走再斜着走，这个和中国象棋的走法是否不同？国际象棋跳一次马是经过了一个还是两个格子？
我默认为只经过一个格子。&lt;/p&gt;
&lt;p&gt;补充的（我认为）完整的解法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 沿着上述代码的单次结题思路，这里增加多次控制即可
coding
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第五章&lt;/h1&gt;
&lt;h2&gt;1.面向对象的主要特征是什么？&lt;/h2&gt;
&lt;p&gt;（抽象、）封装、继承和多态。&lt;/p&gt;
&lt;p&gt;补充：注意这里的表述，是&lt;strong&gt;主要特征&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;2.封装是如何实现的？&lt;/h2&gt;
&lt;p&gt;java 封装类通过三个步骤实现：
（1）修改属性的可见性，限制访问。
（2）设置属性的读取方法。
（3）在读取属性的方法中，添加对属性读取的限制。&lt;/p&gt;
&lt;p&gt;补充：书中原文为，“封装是通过访问控制符实现的。”&lt;/p&gt;
&lt;h2&gt;3.对象之间如何相互作用？作用的条件是什么？&lt;/h2&gt;
&lt;p&gt;通过对象引用、继承访问成员属性和方法；&lt;/p&gt;
&lt;p&gt;作用条件：拥有合法访问权限。&lt;/p&gt;
&lt;h2&gt;4.protected 修饰符有何特点？&lt;/h2&gt;
&lt;p&gt;对 public 类中的 protected 属性和方法：
1）可以被子类（复制属性）直接访问；
2）可以被包内的所有类访问；&lt;/p&gt;
&lt;p&gt;对默认（即 default，省却）类中的 protected 属性和方法：
只能被包内的所有类访问。&lt;/p&gt;
&lt;p&gt;（原答案有误，已删除。）&lt;/p&gt;
&lt;h2&gt;5.Object 都有哪些方法？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 1 这个方法涉及到底层思想了，比较复杂。下面给出参考文章地址 https://blog.csdn.net/Saintyyu/article/details/90452826#commentBox
registerNatives()
// 2 返回此 Object 的运行类。
getClass()
// 3 用于获取对象的哈希值，对象的唯一编号，类似于人的身份证号。
hashCode()
// 4 用于确认两个对象是否“相同”。（String、包装类、URL、File 类中覆盖本方法后，多用于比较值而不是对象的异同）
equals(Object obj)
// 5 创建并返回此对象的一个副本。
clone()
// 6 返回该对象的字符串表示。
toString()
// 7 唤醒在此对象监视器上等待的单个线程。
notify()
// 8 唤醒在此对象监视器上等待的所有线程。
notifyAll()
// 9 直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法，或者超过指定的时间量前，导致当前线程等待。
wait(long timeoutMillis)
// 10 直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法，或者其他某个线程中断当前线程，或者已超过某个实际时间量前，导致当前线程等待。
wait(long timeoutMillis, int nanos)
// 11 用于让当前线程失去操作权限，当前线程进入等待序列
wait()
// 12 当垃圾回收器确定不存在对该对象的更多引用时，由对象的垃圾回收器调用此方法。
finalize()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6.重载的方法之间一般有什么关系？&lt;/h2&gt;
&lt;p&gt;外在表现上：
返回值类型相同，方法名称相同，参数类型或个数不同。&lt;/p&gt;
&lt;p&gt;方法的实现上：
最终功能和目的相同或相似，根据具体情况其实现不同；
多个方法之间往往存在调用关系。（体现程序共享的思想）&lt;/p&gt;
&lt;h2&gt;7.子类覆盖父类方法需要什么条件？子类中定义与父类同名的方法一定是覆盖吗？&lt;/h2&gt;
&lt;p&gt;条件：
1）子类的访问修饰符权限应等于或大于父类；（public 最大）
2）同名的 static 方法和非 static 方法不能相互覆盖；
3）方法前有 final 修饰符，此方法不能在子类方法中进行覆盖；&lt;/p&gt;
&lt;p&gt;不一定：
若名称相同，参数不同，则是重载；
若名称相同，参数相同，而返回值类型不同，则编译不能通过。&lt;/p&gt;
&lt;h2&gt;8.封装、继承和多态在面向对象程序设计中的用途是什么？&lt;/h2&gt;
&lt;p&gt;封装与组合：实现类之间的高内聚低耦合。&lt;/p&gt;
&lt;p&gt;继承：功能复用；为引用带来新特点。&lt;/p&gt;
&lt;p&gt;多态：功能复用与扩充。&lt;/p&gt;
&lt;h2&gt;9.消息发送。&lt;/h2&gt;
&lt;p&gt;设计 Src 和 Dis 两个类，Src 中有一个被封装的属性，类型为 int（要求为非负值），每当通过特定的方法更改 Src 对象中的这个属性后，Dis 对象都能得到通知，并向 Src 发消息获得此属性值。
（没读懂题目，以后再看。）&lt;/p&gt;
&lt;p&gt;（2021.3.7 的我就是“以后”了。现在看懂了，反倒觉得这题目又没有表述清楚了。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.company;

public class Src {
    private static int nonNegativeNumber = 0;
    public static void printNonNegativeNumber (){
        System.out.println(nonNegativeNumber);
    }
    Src (int nonNegativeNumber){
        this.nonNegativeNumber = nonNegativeNumber;
    }
    Src (){
        this.nonNegativeNumber = 0;
    }
    public void setNonNegativeNumber (int nonNegativeNumber) {
        if (nonNegativeNumber &amp;lt;0 ) {
            System.out.println(&quot;不能设置为负数&quot;);
        }else {
            new Src(nonNegativeNumber);
            Dis.flag = 1;
            Dis.checkFlag();
        }
    }
    public int getNonNegativeNumber() {
        return nonNegativeNumber;
    }
    public static void main(String[] args){
        Src src = new Src();
        src.setNonNegativeNumber(5);
    }
}

// ---------------------

package com.company;

public class Dis {
    public static int flag = 0;
    public static void checkFlag(){
        if (flag == 1) {
            Src.printNonNegativeNumber();
            flag = 0;
        }else {
            System.out.println(&quot;未变化&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上文代码存在的问题：
1）总是 new（），浪费空间；
2）只用到了 static 的特点。&lt;/p&gt;
&lt;h1&gt;第六章&lt;/h1&gt;
&lt;h2&gt;1.this 和 super 各有几种用法？&lt;/h2&gt;
&lt;p&gt;this 三种用法：
1）指代本类对象。
2）this.域变量 以及 this.成员方法 特指本类对象的属性和方法。用以区分同名的局部变量。
3）this(参数)，调用本类重载的构造方法。&lt;/p&gt;
&lt;p&gt;super 两种用法：
1）super.域变量，表示被隐藏的父类变量；super.成员方法，表示被覆盖的父类方法。
2）super(参数) ，调用父类构造方法。&lt;/p&gt;
&lt;p&gt;注：this 和 super 的使用有很多值得注意的细节，灵活使用他们，能够写出简洁且高效的程序。&lt;/p&gt;
&lt;h2&gt;2.子类对象实例化的具体过程是什么？&lt;/h2&gt;
&lt;p&gt;实例化过程（简略）：
1）分配对象空间，域变量默认初始化；
2）绑定构造方法，传递参数；
3）调用 this 或 super；
4）实例变量显式初始化；
5）执行其他程序代码。&lt;/p&gt;
&lt;p&gt;实例化过程（详细）：
0）装载类到堆中，执行一次静态代码块；
1）产生一个类的声明，声明存在于方法栈中；
2）产生该类对象——在堆上为其分配空间，并默认初始化域变量；
3）根据参数数量和类型，绑定（选择）构造方法；
4）传递参数，new ClassName(参数) 中的参数 传到栈中构造方法的形式参数上；
5）执行构造方法内部代码第一句：根据 this、super、默认三种句子，选择跳转执行的其他构造方法，最后返回。（中间可能存在多次跳转执行）
6）进行全部或部分对象变量的显式初始化；（可能存在创建并引用类对象的情形，那么又会发生程序跳转执行）
7）执行构造方法内部的其他代码。&lt;/p&gt;
&lt;p&gt;注意：默认初始化和显式初始化的时机不同、产生的影响（程序跳转执行）不同，注意区别。&lt;/p&gt;
&lt;h2&gt;3.类的域变量和方法中定义的局部变量在初始化上有何区别？&lt;/h2&gt;
&lt;p&gt;类的域变量有三次初始化机会：
1、类装载时，执行的静态代码块可能会初始化类的域变量；
2、类对象实例化，为对象分配空间时，会进行默认初始化；
3、类对象实例化，执行构造函数时，可能会进行显式的初始化。&lt;/p&gt;
&lt;p&gt;方法中定义的局部变量初始化分两种：
非 static 方法：
1、方法加载时，是在方法栈中分配空间，此时进行默认初始化；
2、之后执行方法内部代码，可能存在显式初始化；
3、当方法执行完毕后，释放其在栈中的全部空间，局部变量消失；
4、再次调用方法时，重新加载。
static 方法：
1、方法本身代码存在于堆中；
2、局部变量在调用时创建并初始化，结束后释放。
补充：static 中创建 static 变量是非法的。&lt;/p&gt;
&lt;h2&gt;4.模仿形成抽象类的过程，自选角度，形成一个自己的抽象类，并在程序的类继承和引用中体现抽象类的作用。&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;上香不是&lt;/s&gt;上代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.company;

class CharmvOfSnofly {
    String charmTypes ;
    int charmValue ;
    public CharmvOfSnofly(String kindName, int value) {
        this.charmTypes = kindName;
        this.charmValue = value;
    }
    public CharmvOfSnofly(CharmvOfSnofly charmvOfSnofly) {
        this.charmTypes = charmvOfSnofly.charmTypes;
        this.charmValue = charmvOfSnofly.charmValue;
    }
    public void beGoodFriend(CharmvOfSnofly charmvOfSnofly) {}
}

class AppearanceGirlCharmvOfSnofly extends CharmvOfSnofly {
    public AppearanceGirlCharmvOfSnofly(CharmvOfSnofly charmvOfSnofly){
        super(charmvOfSnofly);
    }
    public void beGoodFriend() {
        System.out.print(&quot;关注 Appearance 女孩：&quot;);
        if (charmTypes != &quot;Appearance&quot;) {
            System.out.println(&quot;除了你的颜值我都不关心。&quot;);
        }else {
            if (charmValue &amp;gt; 80) {
                System.out.println(&quot;太好了，我们做朋友吧！&quot;);
            }else {
                System.out.println(&quot;你的颜值有待提高。&quot;);
            }
        }
    }
}

class TemperamentGirlCharmvOfSnofly extends CharmvOfSnofly {
    public TemperamentGirlCharmvOfSnofly(CharmvOfSnofly charmvOfSnofly){
        super(charmvOfSnofly);
    }
    public void beGoodFriend() {
        System.out.print(&quot;关注 Temperament 女孩：&quot;);
        if (charmTypes != &quot;Temperament&quot;) {
            System.out.println(&quot;除了你的气质我都不关心。&quot;);
        }else {
            if (charmValue &amp;gt; 80) {
                System.out.println(&quot;太好了，我们做朋友吧！&quot;);
            }else {
                System.out.println(&quot;你的气质有待提高。&quot;);
            }
        }
    }
}

public class GirlFriendOfSnofly {
    public static void main(String[] args) {
        CharmvOfSnofly charmvOfSnofly = new CharmvOfSnofly(&quot;Temperament&quot;,90);
        AppearanceGirlCharmvOfSnofly appearanceGirlCharmvOfSnofly = new AppearanceGirlCharmvOfSnofly(charmvOfSnofly);
        TemperamentGirlCharmvOfSnofly temperamentGirlCharmvOfSnofly = new TemperamentGirlCharmvOfSnofly(charmvOfSnofly);
        appearanceGirlCharmvOfSnofly.beGoodFriend();
        temperamentGirlCharmvOfSnofly.beGoodFriend();
    }
}

// ps:我也不知道我在写什么......
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5.接口有什么作用？自己定义一个接口，并给出实现类和使用类。&lt;/h2&gt;
&lt;p&gt;接口的主要作用有两个：
1）分离功能的使用者和实现者，形成低耦合。
2）保留扩展性的同时维持系统稳定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 接口
package com.service;

public interface CallToHomePhone {
    void call1();
}

// 实现类
package com.implement;

import com.service.CallToHomePhone;

public class CallToHomePhoneImpl implements CallToHomePhone {
    public void call1() {
        System.out.println(&quot;完成打电话操作。&quot;);
    }
}

// 使用类
package com.user;

import com.implement.CallToHomePhoneImpl;
import com.service.CallToHomePhone;

public class useCall {
    public static void main(String[] args) {
        CallToHomePhone callToHomePhone = new CallToHomePhoneImpl();
        callToHomePhone.call1();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6.抽象类与接口的异同点是什么？&lt;/h2&gt;
&lt;p&gt;相同点：
1）都含有抽象方法；
2）都需要实现类；&lt;/p&gt;
&lt;p&gt;不通点：
1）前者可以有域变量等数据成员，后者最多只能存在静态常量；
2）前者可以有具体成员方法，后者全部为抽象方法；
3）前者扩展性较弱，后者较强。&lt;/p&gt;
&lt;p&gt;补充：
这两者看似很相似，包括写法、引用，但是有个根本性的区别：
抽象类中涉及到的思想，只有抽象和继承，他的使用方式被局限在继承特性上了，特别是 Java 中的继承是单一继承；
而接口中涉及到的思想，包括抽象、继承、实现，他比抽象类多出一整个能力，通过继承和实现的配合使用，接口的局限性将大大降低，他能有更广阔的天地。&lt;/p&gt;
&lt;h2&gt;7.引用比较方法有哪些？&lt;/h2&gt;
&lt;p&gt;引用比较有三种：
1）Object 类中的 equals() 方法，默认比较引用是否指向同一对象；
2）比较运算符中的连等符号 == ，具有重载功能，既可以比较引用的对象，也可以比较数值；
3）使用关键字 instanceof，主要作用是判断引用和类之间的亲属关系的辈分问题。&lt;/p&gt;
&lt;h2&gt;8.内部类的作用是什么？什么情况下使用匿名内部类？&lt;/h2&gt;
&lt;p&gt;内部类的作用：
1）作为封装好的类中的后门。因为内部类能够直接访问外部类的所有属性，包括 private 属性，那么这里就可以大做文章了。
2）简化代码，方便书写。&lt;/p&gt;
&lt;p&gt;匿名内部类的使用，一般在这三个情况下使用：
1）只用到类的一个实例；
2）类在定以后马上用到；
3）类非常小（官方推荐是在 4 行代码以下）。&lt;/p&gt;
&lt;h2&gt;9.不上机，判断下面程序的输出结果。&lt;/h2&gt;
&lt;p&gt;答案见代码注释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class X{
    Y b = new Y();
    X() {
        System.out.println(&quot;X&quot;);
    }
}
class Y{
    Y() {
        System.out.println(&quot;Y&quot;);
    }
}
public class Z extends X {
    Y y = new Y();
    Z() {
        System.out.println(&quot;Z&quot;);
    }
    public static void main(String [] args) {
        new Z();
    }
}

/**
 * 选项：A.Z　B.YZ　C.XYZ　D.YXYZ
 * 答案选D,输出为 YXYZ
 * 程序执行步骤：
 * 0) static void main 作为程序入口；
 * 1）new Z() 新建对象，执行属性默认初始化，无
 * 2）执行默认 supper(),即 X()
 * 3）执行默认 X() 中的 super(),无影响
 * 4）执行 X 类的显式初始化，这里只有一句 Y b = new Y();
 * 5）new Y() 新建对象，无其他变化，直接打印 Y
 * 6）返回到 X 类中，打印 X
 * 7）返回到 Z 类中，此时 Z 的 super()执行完毕
 * 8）显式初始化 Z ，只有一句 Y b = new Y();
 * 9）new Y() 新建对象，无其他变化，直接打印 Y
 * 10）返回到 Z 类中，打印 Z
 * 11）程序结束
 *
 * 注意：按执行步骤顺序分析
 * 1、默认初始化（给所有不确定变量赋初值，程序不应跳转）
 * 2、参数传递 (new ClassName(参数) 中的参数，传递给构造方法的形式参数，
 *     此时构造方法在方法栈上，准备执行。注意，这个参数可以大做文章。)
 * 3、调用构造方法中的this或super语句（注意默认的super()）
 * 4、对象变量显式初始化（可能会有程序跳转，注意与默认初始化区别）
 * 5、执行当前构造方法之后的其他代码
 * 补充：程序多次跳转执行，容易出错（特别是默认super()和显示初始化），一定要按着步骤来。
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10.什么是数据隐藏？如何证明子类对父类同名方法进行重新定义，只能是方法的覆盖而不是方法的隐藏？&lt;/h2&gt;
&lt;p&gt;数据隐藏：当子类定义的变量与其父类中定义的变量同名时，在子类中直接用该变量名访问到的只能是子类自己定义的变量，也就相当于，其从父类继承得到的同名变量隐藏了起来。（注意，这两个变量都客观存在。）&lt;/p&gt;
&lt;p&gt;证明思路：
1、覆盖与隐藏决定性的区别是：
（以下用“内容”这个词语来泛指属性与方法，便于说明。）
隐藏的内容存在三份，父类中一份、子类中继承的一份、子类中自定义的同名内容一份。
覆盖的内容存在两份，父类中一份、子类中自定义的同名内容一份。（注意，此时子类根本没有继承同名内容，子类中只有一份！）&lt;/p&gt;
&lt;p&gt;2、易知，子类对象调用的 functionName() 方法必然来自子类，而子类对象调用的 super.function() 方法必然来自其父类。&lt;/p&gt;
&lt;p&gt;3、正经内容：在 function() 方法中增加输出当前类名的功能，即可证明。
（口胡内容：在 function() 方法中设置自爆功能——释放本类的所有对象空间，到时候观察一下是谁爆炸了就行。(ಡωಡ)）&lt;/p&gt;
&lt;h2&gt;11.A1、A2 分别是具体类 A 的子类，A3 为 A1 的子类，A1、A2 之间的关系为平行类。下面代码为连续的程序片段，请问哪些是正确的？&lt;/h2&gt;
&lt;p&gt;答案见代码注释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A a = new A();
a = new A1();
a = new A2();
a = new A3();
A1 a1 = new A3();
A3 a3 = a1; // 错误，这里需要显示转换引用类型。
A2 a2 = new A1(); // 错误，A1 和 A2 平行，完全不能互相引用
a3 = new A2(); // 错误，a3 是 A2 的侄子，完全不能引用
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;12.借助 JDK 帮助，编写程序实现这样的功能：Applet 当中的 TextField，每输入任一字符，在一个 label 当中都能动态跟踪刷新。&lt;/h2&gt;
&lt;p&gt;本题答案来自网上：
（我自己想学深入一点，但是太花时间——各种调用、继承、接口实现。如果这里查询着学习，难成知识系统。后面的章节学了之后再自己手动写一个吧。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package applet.language.test.java;

import java.applet.Applet;
import java.awt.Label;
import java.awt.TextField;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class Test extends Applet implements KeyListener{
    // 10列大小的空白TextField
    TextField tf = new TextField(10);
    // 一大串空白的Label组件
    Label l = new Label(&quot;                    &quot;);
    public void init() {
        // 监听键盘按键
        tf.addKeyListener(this);
        // 添加组件TextField
        add(tf);
        // 添加组件Label
        add(l);
        // 设置TextField大小（长x高）
        tf.setSize(500, 100);
        // 设置Label大小（长x高）
        l.setSize(500, 100);
    }
    public void keyPressed(KeyEvent arg0) {}
    public void keyReleased(KeyEvent arg0) {}
    public void keyTyped(KeyEvent ke) {
        // Label中字符串随着TextField中动态变化
        l.setText(tf.getText() + ke.getKeyChar());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第七章&lt;/h1&gt;
&lt;h2&gt;1、“程序中凡是可能出现异常的地方必须进行捕捉或抛出”，这句话对吗？&lt;/h2&gt;
&lt;p&gt;（这个问句有疑问，这句话的主语没有明确。
对隐式声明抛出，程序方法本身不作任何声明或处理。
但是对虚拟机来说，只要检测到异常，一定是要处理的。）&lt;/p&gt;
&lt;h2&gt;2、自定义一个异常类，并在程序中主动产生这个异常对象。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package high.order.usage.exception;

/**
 * public Exception(String message) {
 *         super(message);
 *     }
 *
 * public class Exception extends Throwable {}
 *
 * public Throwable(String message) {
 *         fillInStackTrace();
 *         detailMessage = message;
 *     }
 */

public class SelfGenerateException extends Exception
{
	SelfGenerateException(String msg){
		super(msg);
	}

	static void throwOne() throws SelfGenerateException
	{
		int a = 1;
		if (a==1)
		{throw new SelfGenerateException(&quot;检测到异常，显示详细信息&quot;);}
	}

	public static void main(String args[])
	{
		try
		{throwOne();}
		catch(SelfGenerateException e)
		{e.printStackTrace();}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3、借助 JDK 帮助，请举例发生 NullPointerException 异常的一些情况。&lt;/h2&gt;
&lt;p&gt;JDK 原文：&lt;/p&gt;
&lt;p&gt;Thrown when an application attempts to use null in a case where an object is required. These include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Calling the instance method of a null object.&lt;/li&gt;
&lt;li&gt;Accessing or modifying the field of a null object.&lt;/li&gt;
&lt;li&gt;Taking the length of null as if it were an array.&lt;/li&gt;
&lt;li&gt;Accessing or modifying the slots of null as if it were an array.&lt;/li&gt;
&lt;li&gt;Throwing null as if it were a Throwable value&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（不翻译了，英文将就着看，毕竟是原文。）&lt;/p&gt;
&lt;h2&gt;4、不执行程序，指出下面程序的输出结果；如果将黑体代码去掉，写出输出结果；如果再将斜体代码去掉，写出结果。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class TestSevenPointFour {
    public static void aMethod() throws Exception {
        try {
            throw new Exception();
        }
        // 黑体代码开始位置
        catch (Exception e) {
            System.out.println(&quot;exception000&quot;);
        }
        // 黑体代码结束位置

        // 斜体代码开始位置
        finally {
            System.out.println(&quot;finally111&quot;);
        }
        // 斜体代码结束位置
    }

    public static void main(String[] args) {
        try {
            aMethod();
        }
        catch (Exception e) {
            System.out.println(&quot;exception&quot;);
        }
        System.out.println(&quot;finished&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：&lt;/p&gt;
&lt;p&gt;1、直接执行：
exception000
finally111
finished&lt;/p&gt;
&lt;p&gt;2、去黑执行：
finally111
exception
finished&lt;/p&gt;
&lt;p&gt;3、去黑去斜执行：
报错，因为只有 try 不完整。&lt;/p&gt;
&lt;p&gt;（实际运行效果：
编译不能通过——找不到 class 文件；
系统提示：java: &apos;try&apos; 不带有 &apos;catch&apos;, &apos;finally&apos; 或资源声明）&lt;/p&gt;
&lt;p&gt;补充：应该注意三点，其一，捕获的异常不向上层返回；其二，finally 中的代码必定执行；其三，注意 try-catch-finally 异常处理块的合法结构。&lt;/p&gt;
&lt;h2&gt;5、不执行程序，指出下面程序的输出结果。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class TestSevenPointFive {
    public static String output = &quot;&quot;;
    public static void foo(int i) {
        try {
            if (i == 1) {
                throw new Exception();
            }
            output += &quot;1&quot;;
        }
        catch (Exception e) {
            output += &quot;2&quot;;
        }
        finally {
            output +=&quot;3&quot;;
        }
        output += &quot;4&quot;;
    }

    public static void main(String[] args) {
        foo(0);
        foo(1);
        System.out.println(TestSevenPointFive.output);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：
134234&lt;/p&gt;
&lt;p&gt;注意：try 中异常出现点之后的代码不执行。&lt;/p&gt;
&lt;p&gt;==有趣的扩展思考==
补充：如果 try 中异常隐式抛出，返回上层，那么 finally 之后的代码不执行。
题目代码修改如下,以证明上述补充：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestSevenPointFive {
    public static String output = &quot;&quot;;
    public static void foo(int i) {
        try {
            if (i == 1) {
                throw new RuntimeException();
            }
            output += &quot;1&quot;;
        }
        finally {
            output +=&quot;3&quot;;
        }
        output += &quot;4&quot;;
    }

    public static void main(String[] args) {
        try {
            foo(0);
            foo(1);
            System.out.println(TestSevenPointFive.output);
        }
        catch (RuntimeException e){
            System.out.println(TestSevenPointFive.output);
        }
    }
}

// 输出 1343
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6、编写一个程序方法，对空指针异常、除数为零异常给出出错的中文提示。当有新异常发生时，可拓展该方法中的代码进行统一处理。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class TestSevenPointSix {

    public static void foo() {
        int a = 1/0;
    }

    public static void main(String[] args) {
        try {
            foo();
        }
        catch (NullPointerException e){
            System.out.println(&quot;捕获到空指针异常&quot;);
        }
        catch (ArithmeticException e){
            System.out.println(&quot;捕获到算术异常&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（PS：又没看明白题目在问什么，出题人是“甲方”吗？）&lt;/p&gt;
&lt;h2&gt;7、从屏幕输入 10 个数，在输入错误的情况下，给出相应的提示，并继续输入。在输入完成的情况下，找到最大最小数。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.util.InputMismatchException;
import java.util.Scanner;

public class TestSevenPointSeven {
    public static void main(String[] args) {
        Scanner sn = new Scanner(System.in);
        int intVal, maxIntVal = 0, minIntVal = 0;
        int i = 0;
        while (i &amp;lt; 10) {
            try {
                // 不推荐用异常处理来控制程序结构
                intVal = sn.nextInt();
                i++;
                if (i == 1) {
                    maxIntVal = intVal;
                    minIntVal = intVal;
                }
                if (intVal &amp;gt; maxIntVal) {
                    maxIntVal = intVal;
                }
                if (intVal &amp;lt; minIntVal) {
                    minIntVal = intVal;
                }
            }
            catch(InputMismatchException e){
                System.out.println(&quot;这次输入了非整数，继续：&quot;);
                // 清理一下非数字字符
                sn.next();
            }
        }
        System.out.println(&quot;maxIntVal &quot; + maxIntVal + &quot;\nminIntVal &quot; +minIntVal);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：数据输入方式有很多，在没看 IO 之前都不清楚原理，就随便找了个输入方式。&lt;/p&gt;
&lt;h2&gt;8、阅读下面程序，TimeOutException 为自定义异常，完成指定方法后面的部分。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public void method()____
    { success = connect();
        if (success==-1) {
            throw new TimeoutException();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（原题目代码如上，甚至格式都没有变化，共 6 行）&lt;/p&gt;
&lt;p&gt;解题：（代码扩充）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestSevenPointEight {
    public void method() {
        int success = connect();
        if (success == -1) {
            new TimeoutException();
        }
    }
    public int connect() {
        // 链接操作（鬼知道是什么！）
        return -1;
    }
    class TimeoutException {
        public TimeoutException() {
            System.out.println(&quot;超时异常&quot;);
        }
    }
    public static void main(String[] args) {
        new TestSevenPointEight().method();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（PS：又来了又来了，又没看懂题目。）&lt;/p&gt;
&lt;h1&gt;第八章&lt;/h1&gt;
&lt;h2&gt;1、Java 常用类库有哪些？其基本功能是什么？&lt;/h2&gt;
&lt;p&gt;（这里写的是常用的，但是常用这个词语又是相对的，是否常用取决于实际的应用环境。）&lt;/p&gt;
&lt;p&gt;按层次记忆：
等层包： com、java、javax、org
java 中常用的子包九个：java.lang, java.util, java.awt, javaapplet, java.text, java.io, java.net, java.rmi, java.sql.&lt;/p&gt;
&lt;p&gt;功能：顾名思义了。&lt;/p&gt;
&lt;h2&gt;2、JDK 中那些包有 Date 类，他们的区别是什么？&lt;/h2&gt;
&lt;p&gt;共有两个 Date 类：java.util.Date 以及 java.sql.Date&lt;/p&gt;
&lt;p&gt;区别：
前者用于时间操作，后者用来描述数据库中的时间字段；
后者是前者的子类；
前者逐渐被 java.util.Calendar 类取代，后者仍常使用。&lt;/p&gt;
&lt;h2&gt;3、String 类型有什么特点？&lt;/h2&gt;
&lt;p&gt;相比与一般的类，String 类型有如下特点：&lt;/p&gt;
&lt;p&gt;1、申明和对象之间的引用关系很不可靠。一旦更改对象中的值，那么就会生产新的对象被原来的声明引用，而原来的对象就舍弃一旁了。（String 类型存储在常量池中）&lt;/p&gt;
&lt;p&gt;2、初始化时，存在默认编译器优化。导致两个声明引用了同一个对象，即使代码书写上不是想表达这个用意。&lt;/p&gt;
&lt;p&gt;3、参数传递时，是按对象引用传递，具有引用传递的一般特点。但不同的是，由于 String 改变值的时候生成新的对象（基于第一条），所以就会导致形式参数的指向发生变化，指向的地址发生变化。
（简单来说，就是不能改变内存空间中 String 对象的值，当常量用就行。）&lt;/p&gt;
&lt;p&gt;注：主要基于第一条，造成了一些 String 不同于其他类的变化。&lt;/p&gt;
&lt;h2&gt;4、String 什么时候进行值比较，什么时候进行引用比较？&lt;/h2&gt;
&lt;p&gt;解题思路：
String 再特殊，他也是一个类，那么要按照类的一般特征去思考。&lt;/p&gt;
&lt;p&gt;使用 == 自然是进行引用比较；
使用 str1.equals(str2) 则是进行值比较。&lt;/p&gt;
&lt;h2&gt;5、String 与 StrinBuffer 的区别是什么？如何相互转化？&lt;/h2&gt;
&lt;p&gt;来自网上的说法，比较通俗和详细：&lt;/p&gt;
&lt;p&gt;String 类型和 StringBuffer 的主要性能区别：String 是不可变的对象, 因此在每次对 String 类型进行改变的时候，都会生成一个新的 String 对象，然后将指针指向新的 String 对象，所以经常改变内容的字符串最好不要用 String ，因为每次生成对象都会对系统性能产生影响，特别当内存中无引用对象多了以后， JVM 的 GC 就会开始工作，性能就会降低。&lt;/p&gt;
&lt;p&gt;使用 StringBuffer 类时，每次都会对 StringBuffer 对象本身进行操作，而不是生成新的对象并改变对象引用。所以多数情况下推荐使用 StringBuffer ，特别是字符串对象经常改变的情况下。&lt;/p&gt;
&lt;p&gt;我的理解：&lt;/p&gt;
&lt;p&gt;SringBuffer 才像 String，String 就很不合群。
（虚假的字符串类型：String；真正的字符串类型：StringBuffer）&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;第二问&lt;/p&gt;
&lt;p&gt;String 与 StringBuffer 的相互转化：
StringBuffer sb = new StringBuffer(new String());
String s = new String(new StringBuffer());// 或者使用 toString()方法。&lt;/p&gt;
&lt;h2&gt;6、如果要在 Java 程序中启动另一个程序，什么包中的类能够完成此功能？&lt;/h2&gt;
&lt;p&gt;java.lang.Runtime&lt;/p&gt;
&lt;h2&gt;7、Calendaer 如何得到自己的一个实例？&lt;/h2&gt;
&lt;p&gt;只能通过其静态方法 getInstance()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// getInstance() 有四个重载，其中的无参方法如下：
public static Calendar getInstance(){
    return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8、格式化类的作用是什么？格式化数字的类有哪些？格式化日期的类又有哪些？&lt;/h2&gt;
&lt;p&gt;作用：获得原数据想要的格式，便于观察和提取信息。&lt;/p&gt;
&lt;p&gt;格式化数字的类&lt;strong&gt;主要&lt;/strong&gt;有：
java.util.NumberFormat，java.util.DecimalFormat&lt;/p&gt;
&lt;p&gt;格式化日期的类：
（主要使用）java.text.SimpleDateFormat&lt;/p&gt;
&lt;h1&gt;第九章&lt;/h1&gt;
&lt;h2&gt;1、线程和进程的联系和区别是什么？&lt;/h2&gt;
&lt;p&gt;1）概念
线程是程序执行中的单个顺序流程；
进程是一个执行中的程序。&lt;/p&gt;
&lt;p&gt;2）空间
每一个进程都有自己独立的一块内存空间、一组系统资源，其内部数据和状态完全独立；
同类的多个线程共享一块内存空间和一组系统资源，其本身的数据通常来自寄存器和堆栈。&lt;/p&gt;
&lt;p&gt;3）包含：
一个进程可以运行多个线程。&lt;/p&gt;
&lt;h2&gt;2、什么是前台线程，什么是后台线程？&lt;/h2&gt;
&lt;p&gt;通俗的解释：
后台线程就是 main 同生共死，当 main 退出，它将终止，而前台线程是在任务执行结束才停止。&lt;/p&gt;
&lt;p&gt;详细的解释（以及拓展）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;后台线程会随着主线程(main 线程)的结束而结束，但是前台进程则不会(如果 main 线程先于前台进程结束，前台进程仍然会执行)；或者说，只要有一个前台线程未退出，进程就不会终止。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认情况下，程序员创建的线程是用户线程；用 setDaemon(true)可以设置线程为后台线程；而用 isDaemon( )则可以判断一个线程是前台线程还是后台线程；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jvm 的垃圾回收器其实就是一个后台线程；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;setDaemon 函数必须在 start 函数之前设定，否则会抛出 IllegalThreadStateException 异常;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有的“非后台线程”结束时，程序也就终止了，同时会杀死进程中所有后台线程：main 就是一个非后台线程；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后台线程创建的子线程也是后台线程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3、创建线程有几种方法？他们之间的区别是什么？&lt;/h2&gt;
&lt;p&gt;Java 的产生线程的两种方法：
1）继承 Thread 类，并覆盖其 run 方法；
2）实现 Runnable 接口，并并将实现类对象传参到 Thread 类的构造方法。&lt;/p&gt;
&lt;p&gt;上述两种方法原理是相通的，都是手动覆盖或者实现 run()方法，再由 Thread 的 start() 方法启动线程。（Runnable 接口只有唯一的抽象方法 run();）&lt;/p&gt;
&lt;p&gt;两种方法的比较：
1）由于 Java 单继承的特性，所以通过继承覆盖 run() 的方式会有不小的局限性；
2）通过接口实现的方式，多线程可共享实现类对象的资源。&lt;/p&gt;
&lt;h2&gt;4、线程的生命周期有哪些状态？哪些方法可以改变这些状态？&lt;/h2&gt;
&lt;p&gt;一共有五种状态：
1）创建状态：只是 new 创建对象，还没有分配活动线程的资源给它；
2）可运行（就绪）状态：除 CPU 之外的资源已经分配完成；
3）运行中状态：分配 CPU ，正在执行；
4）阻塞状态：因为某些原因退出执行状态，此时仍可能占用除 CPU 外的各种资源；
5）死亡状态：线程执行完 run() 方法，或者抛出未捕获的异常或错误。&lt;/p&gt;
&lt;p&gt;状态转化示意：
1-&amp;gt;2 start()
2-&amp;gt;3 分配 CPU 及内存
3-&amp;gt;2 yield() 或超过配额
3-&amp;gt;4 sleep(), wait(), join()
3-&amp;gt;5 完成或意外终止
4-&amp;gt;2 引起阻塞原因消除&lt;/p&gt;
&lt;h2&gt;5、什么是线程安全？为什么会产生线程安全问题？如何解决线程安全问题？&lt;/h2&gt;
&lt;p&gt;线程安全问题：
多线程共享资源时引起意料之外的数据变化。&lt;/p&gt;
&lt;p&gt;如何解决：
使用 synchronized 等关键字或包，解决程序的原子性、有序性、可见性问题，就能解决线程安全问题。&lt;/p&gt;
&lt;p&gt;线程安全问题产生原因：
原子性、有序性、可见性三个问题没有处理好。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;补充拓展：
这三种说法是对应于 Java 内存模型的，他们其实来自操作系统中内存模型原本就存在的问题或机制。&lt;/p&gt;
&lt;p&gt;1）缓存一致性问题：
在 CPU 和主存之间增加缓存，在多线程场景下会存在缓存一致性问题。
2）处理器优化：
为了使处理器内部的运算单元能够尽量的被充分利用，处理器可能会对输入代码进行乱序执行处理。
3）指令重排：
除了现在很多流行的处理器会对代码进行优化乱序处理，很多编程语言的编译器也会有类似的优化，比如 Java 虚拟机的即时编译器（JIT）也会做指令重排。&lt;/p&gt;
&lt;p&gt;关联：
缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。&lt;/p&gt;
&lt;h2&gt;6、什么是线程同步通信？同步通信又是如何实现的？&lt;/h2&gt;
&lt;p&gt;线程同步通信概念：
使隶属于同一进程的线程协调一致的工作就是线程间的==同步==。
线程间==通信==：由于多线程共享地址空间和数据空间，所以&lt;strong&gt;多个线程间的通信是一个线程的数据可以直接提供给其他线程使用&lt;/strong&gt;，而不必通过操作系统（也就是内核的调度）。&lt;/p&gt;
&lt;p&gt;同步通信的实现：
线程间的通信方式：使用全局变量、使用消息、使用事件 CEvent 类
线程间的同步方式：临界区、互斥量、信号量、事件。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;补充扩展：
进程间的通信方式：管道（pipe）、有名管道 (named pipe) 、信号量( semophore ) 、消息队列( message queue ) 、信号（signal）、共享内存（shared memory）、套接字( socket )&lt;/p&gt;
&lt;h2&gt;7、什么是死锁？&lt;/h2&gt;
&lt;p&gt;死锁是这样一种情形：多个线程同时被阻塞，它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞，因此程序不可能正常终止。&lt;/p&gt;
&lt;p&gt;java 死锁产生的四个必要条件：&lt;/p&gt;
&lt;p&gt;1、互斥使用，即当资源被一个线程使用(占有)时，别的线程不能使用
2、不可抢占，资源请求者不能强制从资源占有者手中夺取资源，资源只能由资源占有者主动释放。
3、请求和保持，即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待，即存在一个等待队列：P1 占有 P2 的资源，P2 占有 P3 的资源，P3 占有 P1 的资源。这样就形成了一个等待环路。&lt;/p&gt;
&lt;p&gt;当上述四个条件都成立的时候，便形成死锁。当然，死锁的情况下如果打破上述任何一个条件，便可让死锁消失。&lt;/p&gt;
&lt;h2&gt;8、如何让某个对象的 A 方法内的一个代码块和另一个方法 B 实现同步？&lt;/h2&gt;
&lt;p&gt;在 A、B 方法前使用 synchronized，代码块前后使用 wait() 和 notify() ，再配合变量指示器使用。&lt;/p&gt;
&lt;h2&gt;9、设计一个程序产生两个线程 A 和 B， B 线程执行 10 秒后，被 A 线程终止。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package high.order.usage.thread;

class AB {
	volatile private int runTime = 0;
	volatile private boolean Enough = false;
	public void waitTimeUntilEnough () throws RuntimeException{
		if(Enough == true) {
			runTime = 1/0;
		}
		try {
			Thread.sleep(100);
		}
		catch(Exception e) {}

		runTime ++;
		System.out.println(&quot;线程&quot; +
				Thread.currentThread().getName() +
				&quot;执行了&quot;+ runTime + &quot;秒&quot;);
	}

	public void checkRunTimeIsEnough () throws RuntimeException {
		if(runTime == 10) {
			Enough = true;
			runTime = 1/0;
		}
	}
}

class RunA implements Runnable {
	private AB ab;
	public RunA (AB ab) {
		this.ab = ab ;
	}
	public void run()
	{
		while (true) {
			try{
				ab.waitTimeUntilEnough();
			}
			catch (RuntimeException e) {
				System.out.println(&quot;运行线程&quot; +
						Thread.currentThread().getName() +
						&quot;结束运行&quot;);
				break;
			}
		}
	}
}

class OverseerB implements Runnable {
	private AB ab;
	public OverseerB (AB ab) {
		this.ab = ab ;
	}
	public void run()
	{
		while (true) {
			try{
				ab.checkRunTimeIsEnough();
			}
			catch (RuntimeException e) {
				System.out.println(&quot;观察者线程&quot; +
						Thread.currentThread().getName() +
						&quot;结束运行&quot;);
				break;
			}
		}
	}
}

public class TestCommunicate{
	public static void main(String[] args){
		AB ab = new AB();
		new Thread(new RunA(ab)).start();
		new Thread(new OverseerB(ab)).start();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;备注：由于代码写的不熟练，所以看着有点傻瓜。我一时间只想到用异常来控制程序跳转了，总所周知，这是很不合规范的事情，自我批评 ing...&lt;/p&gt;
&lt;h1&gt;第十章&lt;/h1&gt;
&lt;h2&gt;1、Collection、List、Set 之间的联系和区别是什么？&lt;/h2&gt;
&lt;p&gt;1、List 和 Set 都是接口 Collection 的子接口，他们的父接口都是这两个：Collection 和 Iterable 。&lt;/p&gt;
&lt;p&gt;2、List 中的元素要求具有可控的顺序；Set 中的元素要求不能重复。（这符合其名称在数据结构与数学中的意义。）&lt;/p&gt;
&lt;p&gt;3、Collection 能通过 iterator() 获得 iterator 对象，List 和 Set 自然也可以。
但值得注意的是，iterator 的子接口 ListIterator 具有更丰富的功能，只有 List 中的 listIterator(参数) 方法才能获得 ListIterator 对象。&lt;/p&gt;
&lt;h2&gt;2、遍历一个集合对象有哪些方法？&lt;/h2&gt;
&lt;p&gt;有三种方法：
1）非一般化的、局限很多的下标遍历法；
2）使用遍历接口 Enumeration；
3）使用遍历接口 Iterator 。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (Iterator it = c.iterator(); it.hasNext(); ) {
    MyObject o = (MyObject)it.next();
    // 对获取到的对象o 操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3、同数组相比，Vector 有何特点？&lt;/h2&gt;
&lt;p&gt;Vector 可以自动增加容量来容纳不定数量的元素。而
数组长度是确定的。&lt;/p&gt;
&lt;h2&gt;4、Vector 与 ArrayList、LinkedList 与 ArrayList、Hashtable 与 HashMap、TreeMap 与 HashMap 之间的共同点和区别是什么？&lt;/h2&gt;
&lt;p&gt;1）Vector 与 ArrayList
同：
都是存储可变数量对象元素的集合。
异：
Vector 方法同步，线程安全；ArrayList 方法不同步，性能更好。&lt;/p&gt;
&lt;p&gt;2）LinkedList 与 ArrayList
同：
都是存储可变数量对象元素的集合，都是线程不安全。
异：
LinkedList 内部实现基于链表方式；ArrayList 内部实现基于数组方式。&lt;/p&gt;
&lt;p&gt;3）Hashtable 与 HashMap
同：
都可以实现多组 key 到 value 之间的映射。
异：
Hashtable 方法同步，线程安全；HashMap 方法不同步，效率更高。&lt;/p&gt;
&lt;p&gt;4）TreeMap 与 HashMap
同：
都可以实现多组 key 到 value 之间的映射，都是线程不安全。
异：
TreeMap 基于红黑树实现，HashMap 基于哈希表实现。&lt;/p&gt;
&lt;h2&gt;5、Map、Collection、Iterator 之间的关系如何？&lt;/h2&gt;
&lt;p&gt;Collection 能有方法够产生 Iterator；
Map 能够有方法产生 Collection，也能间接产生 Iterator 。&lt;/p&gt;
&lt;h2&gt;6、Collection 和 Collections 各自的功能是什么？&lt;/h2&gt;
&lt;p&gt;Collection 是接口，用来说明作为一个集合类应有的结构特征属性和共性操作方法。
Collections 是类，有一些集合类的操作功能。&lt;/p&gt;
&lt;h2&gt;7、现需要选择集合类，它存储的对象可以被多个线程维护（增加、删除），请问应该选择什么样的集合类，为什么？如果多线程只是读取，而不是维护，应该选择什么样的集合类，为什么？&lt;/h2&gt;
&lt;p&gt;存在多线程，就需要线程安全；存在维护就优先选择链表方式实现的集合；如果是只是查看，那么优先选择数组方式实现的集合。&lt;/p&gt;
&lt;p&gt;1）Hashtable 。
2）Vector 。&lt;/p&gt;
&lt;h2&gt;8、如何实现集合对象排序？定义一个复数类并按照复数的实部大小对复数对象进行排序。&lt;/h2&gt;
&lt;p&gt;使用 List 接口，因为 List 接口中，对象之间有指定的顺序。
还有 TreeMap 修改时就自带排序功能。&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.usage.Collections;

import java.util.*;

class complete {
   int realValue;
   int virtualValue;
   public complete (int realValue, int virtualValue) {
      this.realValue = realValue;
      this.virtualValue = virtualValue;
   }
}

public class ExampleTreeMap {
   Map calendar = new TreeMap();
   public ExampleTreeMap(List &amp;lt;complete&amp;gt; c){
         for (int i = 0; i &amp;lt; c.size(); i++) {
            calendar.put(c.get(i).realValue, c.get(i).virtualValue);
         }
   }
   public static void main(String args[]) {
      List &amp;lt;complete&amp;gt; c = new ArrayList &amp;lt;complete&amp;gt; ();
      c.add(new complete(1,165));
      c.add(new complete(9,165));
      c.add(new complete(5,165));
      System.out.println(&quot;输入的复数：&quot;);
      for (int i = 0; i &amp;lt; c.size(); i++) {
         System.out.println(&quot;实部：&quot; + c.get(i).realValue + &quot;虚部：&quot;+ c.get(i).virtualValue);
      }
      ExampleTreeMap example = new ExampleTreeMap(c);
      System.out.println(&quot;TreeMap 自带排序: &quot; + example.calendar);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9、集合类对象调用 remove 方法将某个对象删除，这个对象是否就一定是垃圾对象了？&lt;/h2&gt;
&lt;p&gt;不一定。&lt;/p&gt;
&lt;p&gt;remove 删除的仅仅是集合类对象中保存的引用副本，而原引用的情况是无从得知的。所以 remove 执行后，该对象仍可能存在其他引用。&lt;/p&gt;
&lt;h2&gt;10、对第 7 章第 6 题进行适当改造，将异常类型与中文提示存储在一种集合类当中，从而实现相应的功能。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package high.order.usage.Collections;

import java.util.HashMap;
import java.util.Map;

public class TestTenPointTen {
    public static void foo() {
        int a = 1/0;
    }
    public static void main(String[] args) {
        Map calendar = new HashMap();
        calendar.put(&quot;java.lang.NullPointerException&quot;, &quot;捕获到空指针异常&quot;);
        calendar.put(&quot;java.lang.ArithmeticException&quot;, &quot;捕获到算术异常&quot;);
        try {
            foo();
        }
        catch (ArithmeticException e){
            System.out.println(calendar.get(e.getClass().getName()));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第十一章&lt;/h1&gt;
&lt;p&gt;题外话：虽然 Applet 凉了，但是触类旁通，简单学习一下也不错。&lt;/p&gt;
&lt;h2&gt;1、为什么要对 Applet 的安全性作限制？&lt;/h2&gt;
&lt;p&gt;这是 Applet 的运行机制决定的：
客户端浏览器解释执行从服务器下载的 Applet 字节码，而这个字节码可能包含恶意程序，于是就存在安全隐患，需要进行安全性限制。&lt;/p&gt;
&lt;h2&gt;2、Applet 有哪几种状态，状态切换需要什么条件？&lt;/h2&gt;
&lt;p&gt;四种状态：
初始态、运行态、停止态、消亡态。&lt;/p&gt;
&lt;p&gt;切换条件：
运行这四个方法；init()、start()、stop()、destroy()&lt;/p&gt;
&lt;h2&gt;3、如何刷新一个 Applet？&lt;/h2&gt;
&lt;p&gt;Appplet 的显式和刷新由一个独立的 AWT 线程控制。&lt;/p&gt;
&lt;p&gt;有两种刷新的情况：
1）自动执行 paint 方法绘图；
2）调用 repaint 方法重新绘图。&lt;/p&gt;
&lt;h2&gt;4、Applet 如何获得 Image 资源，如何显示图像？&lt;/h2&gt;
&lt;p&gt;1）装载图像文件
public Image getImage(URL url)
public Image getImage(URL url, String name)&lt;/p&gt;
&lt;p&gt;2）显示图像
通过类 Graphics 的方法来实现，6 个重载的 drawImage 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abstract boolean	drawImage​(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer)

abstract boolean	drawImage​(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer)

abstract boolean	drawImage​(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer)

abstract boolean	drawImage​(Image img, int x, int y, int width, int height, ImageObserver observer)

abstract boolean	drawImage​(Image img, int x, int y, Color bgcolor, ImageObserver observer)

abstract boolean	drawImage​(Image img, int x, int y, ImageObserver observer)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5、Applet 程序当中，如果书写了一个含参数的构造方法，将会发生什么现象，如何解释？&lt;/h2&gt;
&lt;p&gt;无法实例化。&lt;/p&gt;
&lt;p&gt;Applet 的实例化由系统决定，系统主动调用 Applet 的无参构造方法，但是没有无参构造方法。&lt;/p&gt;
&lt;h1&gt;第十二章&lt;/h1&gt;
&lt;p&gt;题外话：虽然 AWT 凉了，但是触类旁通，简单学习一下也不错。
（这句话好像在前面说过...）&lt;/p&gt;
&lt;h2&gt;1、AWT 常用容器有哪些？如何将一个组件加入到容器当中？&lt;/h2&gt;
&lt;p&gt;常用容器：
panel、Frame、Dialog、ScrollPanel&lt;/p&gt;
&lt;p&gt;通过从 Component 继承得到的 add 方法。&lt;/p&gt;
&lt;h2&gt;2、AWT 容器对象或组件对象产生后，是否能立即显示？&lt;/h2&gt;
&lt;p&gt;不能。
还必须要调用 Component 类中的 setVisible(true) 方法。&lt;/p&gt;
&lt;h2&gt;3、AWT 中如何产生单选按钮和复选框？&lt;/h2&gt;
&lt;p&gt;单选按钮：
CheckboxGroup cbg = new CheckboxGroup();
add(new Checkbox(&quot;select1&quot;, cbg, true));
add(new Checkbox(&quot;select2&quot;, cbg, true));&lt;/p&gt;
&lt;p&gt;(注意，他们必须属于一个条目组——一个条目组中的选项具有互斥关系。)&lt;/p&gt;
&lt;p&gt;复选框：
不规定在某个组中的多个单选按钮即为复选框，形如：
add(new Checkbox(&quot;select2&quot;, null, true));
add(new Checkbox(&quot;select1&quot;, null, true));&lt;/p&gt;
&lt;h2&gt;4、如何实现在界面中让一组组件的位置相对固定？&lt;/h2&gt;
&lt;p&gt;使用 BorderLayout 布局管理器。&lt;/p&gt;
&lt;h2&gt;5、Menu 类中可以加入 MenuItem，但为什么 Menu 是 MenuItem 的子类？&lt;/h2&gt;
&lt;p&gt;因为 Menu 中包含的方法：
public menuItem add(MenuItem mi);&lt;/p&gt;
&lt;h2&gt;6、AWT 都有几种布局管理器？Panel、Applet 默认的布局管理器是什么？Window、Frame 和 Dialog 默认的布局管理是什么？使用什么方法设置容器的布局管理器？&lt;/h2&gt;
&lt;p&gt;AWT 共有 5 种布局管理器：
FlowLayout、BorderLayout、GridLayout、CardLayout、GridBagLayout&lt;/p&gt;
&lt;p&gt;Panel、Applet 容器的默认布局管理器 FlowLayout；
Window、Frame、Dialog 的默认布局管理器 BorderLayout 。&lt;/p&gt;
&lt;p&gt;容器可设置布局管理器，管理容器中组件的布局：
container.setLayout(new XxxLayout()) 。&lt;/p&gt;
&lt;h2&gt;7、容器尺寸变化时，不同的布局管理器对组件的大小、位置有何影响？&lt;/h2&gt;
&lt;p&gt;FlowLayout：大小不变，位置依次流动；&lt;/p&gt;
&lt;p&gt;BorderLayout：大小改变，相对位置不变；&lt;/p&gt;
&lt;p&gt;GridLayout：大小改变，相对位置不变；&lt;/p&gt;
&lt;p&gt;CardLayout：大小改变，相对位置不变；&lt;/p&gt;
&lt;p&gt;GridBagLayout：取决于具体的设置。&lt;/p&gt;
&lt;h2&gt;8、AWT 中都有什么方法设置组件在容中的位置？&lt;/h2&gt;
&lt;p&gt;两种方法：
1）绝对坐标确定
2）使用布局管理器设置&lt;/p&gt;
&lt;h2&gt;9、Java 委托事件模型比层次型模型有哪些优点？&lt;/h2&gt;
&lt;p&gt;事件源和监听者彼此独立，这样就可以更灵活的处理事件。&lt;/p&gt;
&lt;h2&gt;10、事件对象在编程中的作用是什么？&lt;/h2&gt;
&lt;p&gt;传递消息。&lt;/p&gt;
&lt;h2&gt;11、应用程序如何得到一个图像的引用？&lt;/h2&gt;
&lt;p&gt;Applet 直接通过 URL 得到图像引用。&lt;/p&gt;
&lt;p&gt;Application 得到引用的方式有两个：
Image img1 = Toolkit.getDefaultToolKit().getImage(URL 及名称)
Image img2 = 组件引用.getToolKit().getImage(URL 及名称)&lt;/p&gt;
&lt;h2&gt;12、当产生一个 AWT 界面时，main 主程序已经完成，界面为什么不消失？&lt;/h2&gt;
&lt;p&gt;因为容器对象产生时，创建了一个 AWT 线程，他是一个前台线程。&lt;/p&gt;
&lt;h2&gt;13、组件重绘有哪些方式？&lt;/h2&gt;
&lt;p&gt;需要调用 paint 方法，分两种调用：
1）AWT 线程自动调用；
2）程序调用。&lt;/p&gt;
&lt;p&gt;注意，程序不能直接调用 paint 方法，但是可以统计过调用 repaint 方法间接调用。&lt;/p&gt;
&lt;h2&gt;14、Graphics 对象都能进行什么类型的绘制？&lt;/h2&gt;
&lt;p&gt;三个方面：文字绘制、图形绘制、图像绘制 。&lt;/p&gt;
&lt;h2&gt;15、双缓存技术解决的问题是什么？解决的方法是什么？&lt;/h2&gt;
&lt;p&gt;解决的问题：
在 Component 及其子类的 paint 方法中进行图形图像绘制时，如果绘制内容复杂且频繁时，往往比较费时，常常出现绘制界面的抖动。&lt;/p&gt;
&lt;p&gt;解决方法：
利用额外的空间，将绘制好的图像保留在 Image 中，这样再次需要显示这个图像时，就不需要再计算一次了。&lt;/p&gt;
&lt;h1&gt;第十三章&lt;/h1&gt;
&lt;h2&gt;1、Swing 与 AWT 有什么区别？&lt;/h2&gt;
&lt;p&gt;1）Swing 是轻量级组件，AWT 是重量级组件。
2）Swing 采用了 MVC 的设计模式。
3）Swing 保留了 AWT 中大多数对应名称的组件，此外还提供了更多新的组件。
4）Swing 的功能有所提高。&lt;/p&gt;
&lt;h2&gt;2、Swing 容器有几类？&lt;/h2&gt;
&lt;p&gt;Swing 容器大致可分为三类：
顶层容器、中间容器、特殊容器。&lt;/p&gt;
&lt;h2&gt;3、组件加入 Swing 顶层容器与加入 AWT 容器有什么不同？&lt;/h2&gt;
&lt;p&gt;不能直接调用顶层容器的 add 方法添加 Swing 组件。&lt;/p&gt;
&lt;h2&gt;4、Swing 中的许多容器和组件都是继承于 JComponent 的，有哪些例外？&lt;/h2&gt;
&lt;p&gt;顶层容器例外：JFrame、JApplet、JDialog、JWindow 。&lt;/p&gt;
&lt;h2&gt;5、模仿例 13.1，编写 Swing 程序，并让外观呈现 Motif 风格。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package high.order.usage.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.UIManager.LookAndFeelInfo;

public class JFrameDemo extends JFrame {
    Container ct;
    JButton jButton1 = new JButton();
    public JFrameDemo() {
        jbInit();
    }
    private void jbInit() {
        // 得到内容面板添加组件
        ct=getContentPane();
        // 设定Frame的宽度为200,高度为100
        this.setSize(new Dimension(200,100));
        // 设定Frame的标题
        this.setTitle(&quot;Frame Title&quot;);
        // 设定按钮的caption
        jButton1.setText(&quot;connect&quot;);
        Icon ic = new ImageIcon(&quot;local.gif&quot;);
        jButton1.setIcon(ic);
        // jButton1.setBorder(BorderFactory.createLineBorder(Color.red));
        // 将按钮加入Frame
        ct.add(jButton1,&quot;Center&quot;);
        // 显示Frame
        this.setVisible(true) ;
    }
    public static void main (String []arg) throws ClassNotFoundException,
        InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException {
        // 打印默认风格
        System.out.println(&quot;默认风格为： &quot; + UIManager.getLookAndFeel().getName());
        LookAndFeelInfo[] lAndF = UIManager.getInstalledLookAndFeels();
        // 遍历风格种类
        for (int j = 0; j &amp;lt; lAndF.length; j++) {
            System.out.println(&quot;本机器第&quot;+ (j + 1) + &quot;种风格：&quot; + lAndF[j].getClassName());
        }
        System.out.println(&quot;选择表现的风格为：motif &quot;);
        // UIManager.setLookAndFeel(&quot;javax.swing.plaf.metal.MetalLookAndFeel&quot;);
        // UIManager.setLookAndFeel(&quot;javax.swing.plaf.nimbus.NimbusLookAndFeel&quot;);
        UIManager.setLookAndFeel(&quot;com.sun.java.swing.plaf.motif.MotifLookAndFeel&quot;);
        // UIManager.setLookAndFeel(&quot;com.sun.java.swing.plaf.gtk.GTKLookAndFeel&quot;);

        JFrameDemo	jf =  new JFrameDemo();
        jf.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第十四章&lt;/h1&gt;
&lt;h2&gt;1、流有哪些分类？&lt;/h2&gt;
&lt;p&gt;三种分类方式：
1）按照&lt;strong&gt;流动方向&lt;/strong&gt;分类：
输入流、输出流
2）按照&lt;strong&gt;读取类型&lt;/strong&gt;分类：
字节流、字符流
3）按照&lt;strong&gt;发生源头&lt;/strong&gt;分类：
节点流、过滤流&lt;/p&gt;
&lt;h2&gt;2、常见的文件类和接口有哪些？&lt;/h2&gt;
&lt;p&gt;接口：
FileFilter、FilenameFilter
类：
File、FileDescription、
FileInputStream、FileReader、
FileOutputStream、FileWriter、
RandomAccessFile 。&lt;/p&gt;
&lt;h2&gt;3、完成下面方法中的代码，要求建立一个缓冲区，将字节输入流中的内容转为字符串。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static String loadStream (InputStream in) throws IOException {
	// ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.io.java;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class QuesionFourteenPointThree {
    static String loadStream (InputStream in) throws IOException {
        // ...
        StringBuffer returnString = new StringBuffer();
        int n = 512, c = 0;
        byte buffer[] = new byte[n];
        while ((c = in.read(buffer,0,n)) != -1) {
            String tempString = new String(buffer,0,c);
            System.out.print(tempString);
            returnString.append(tempString);
        }
        // 关闭输入流
        in.close();
        return returnString.toString();
    }
    public static void main (String[] args) {
        try {
            InputStream fileInputStream = new FileInputStream(&quot;Write1.txt&quot;);
            QuesionFourteenPointThree.loadStream(fileInputStream);
            System.out.println(&quot;完成&quot;);
        }
        catch (Exception e) {
            System.out.println(&quot;报告异常&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4、编写程序，将一个字符串转为字节数组输入流，将这个流中所有小写字母换成大写字母并写入字节数组输出流中，再将数组输出流转为字符串。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package high.order.io.java;

import java.io.*;

public class QuestionFourteenPointFour {
    public static void main (String[] args) {
        try {
            String testText = &quot;Hi Guys! Good night!&quot;;
            System.out.println(&quot;原字符串：&quot; + testText);
            byte[] bytes = new byte[30];
            ByteArrayInputStream byteArrayInputStream =
                new ByteArrayInputStream(testText.getBytes());
            byteArrayInputStream.read(bytes);
            System.out.println(&quot;字节显式：&quot;);
            for (int i = 0; i &amp;lt; bytes.length; i++) {
                System.out.print(bytes[i] + &quot; &quot;);
                if (bytes[i] &amp;gt;= 97 &amp;amp;&amp;amp; bytes[i] &amp;lt;= 122) {
                    bytes[i] -= 32;
                }
            }
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(bytes);
            String a = byteArrayOutputStream.toString();
            System.out.println(&quot;\n&quot; + &quot;改变后字符：&quot; + a);
            byteArrayInputStream.close();
            byteArrayOutputStream.close();
        }
        catch (Exception e) {
            System.out.println(&quot;报告异常&quot;);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5、完成下面方法中的代码，方法中的参数为一个文本文件的名称，要求将文件中的内容转为字符串。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static public String loadFile (String filename) {
	// ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.io.java;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class QuestionFourteenPointFive {
    static public String loadFile (String filename) {
        try {
            FileInputStream fileInputStream = new FileInputStream(filename);
            byte[] bytes = new byte[30];
            fileInputStream.read(bytes);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(bytes);
            return byteArrayOutputStream.toString();
        }
        catch (FileNotFoundException e) {
            System.out.println(&quot;文件未找到&quot;);
        }
        catch (IOException e) {
            System.out.println(&quot;IO异常&quot;);
        }
        finally {
            System.out.println(&quot;方法调用完成&quot;);
        }
        return &quot;我就是个凑数的~QAQ&quot;;
    }
    public static void main (String[] args) {
        String s = QuestionFourteenPointFive.loadFile(&quot;Write1.txt&quot;);
        System.out.println(&quot;显示文件内容：&quot;);
        System.out.println(s);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6、完成下面方法中的代码，将字符串 contents 中的内容写入文件 filename 中。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static public boolean saveFile(String filename,String contents){}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.io.java;

import java.io.*;

public class QuestionFourteenPointSix {
    static public boolean saveFile (String filename,String contents) {
        boolean saveFileResult = true;
        try {
            FileOutputStream wf = new FileOutputStream(filename,true);
            wf.write(contents.getBytes(),0,contents.getBytes().length);
            wf.close();
        }
        catch (FileNotFoundException e){
            System.out.println(&quot;Not found the file!&quot;);
            saveFileResult = false;
        }
        catch (IOException e){
            System.out.println(&quot;IO Exception!&quot;);
            saveFileResult = false;
        }
        return saveFileResult;
    }
    public static void main(String[] args) {
        String filename = &quot;Write1.txt&quot;;
        String contents = &quot;The contents about something.&quot;;
        if (saveFile(filename, contents)) {
            System.out.println(&quot;Save File Success!&quot;);
        }else {
            System.out.println(&quot;Save File Failure!&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7、socket 套接字有一个方法 getInputStream() ，其含义是得到从网络上传过来的数据流。现要求编写一段程序，将接收的数据存入文件。&lt;/h2&gt;
&lt;p&gt;（绝对超纲了啊，网络通信明明是第十五章的内容。如果只是写文件的话，就和前面的题目重复了。socket 还没学呢...）&lt;/p&gt;
&lt;p&gt;参考了网上内容：（好像是十五章的例子，早先看的了，现在不记得了）&lt;/p&gt;
&lt;p&gt;服务器端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package temp.text;

// 服务器端
import java.io.*;
import java.net.*;

public class MultiTalkServer {
    public static void main (String[] args) throws IOException {
        // 打开服务器制定端口, 等待客户端连接
        ServerSocket server = new ServerSocket(5000);
        // 获得与服务器相连的套接字对象——套接字:绑定ip地址和端口号的网络对象
        Socket socket = server.accept();
        // 查看该地址文件夹是否存在,如果不存在,创建一个
        String name = &quot;Write1.txt&quot;;
        File file = new File(name);
        if (!file.exists()) {
            file.createNewFile();
        }
        // 套接字的字节输入流, 读取客户端传过来的数据
        InputStream in = socket.getInputStream();
        // 创建输出流对象
        FileOutputStream fos = new FileOutputStream(name);
        // 一次读取5字节，方便阅读，因为客户端传的就是5个字节
        byte[] data = new byte[5];
        int len = 0;
        // 如果客户端没有关闭socket输出流,这里的read方法会一直读取
        while ((len = in.read(data)) != -1) {
            fos.write(data, 0, len);
            System.out.println(&quot;客户端接受信息：&quot; + fos.toString());
            // 写入一个系统通用换行符
            fos.write(System.lineSeparator().getBytes());
        }
        System.out.println(&quot;信息上传到文件!&quot;);
        server.close();
        fos.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package temp.text;

// 客户端
import java.io.*;
import java.net.Socket;

public class TalkClient {
    public static void main (String[] args) throws IOException {
        Socket socket = new Socket(&quot;127.0.0.1&quot;,5000);
        OutputStream out = socket.getOutputStream();
        int i = 10;
        try {
            while(i-- &amp;gt; 0) {
                out.write((&quot;HELLO&quot; + i).getBytes());
                out.flush();
                System.out.println(&quot;客户端发送消息：HELLO&quot; + i);
                Thread.sleep(1000);
            }
        }
        catch (InterruptedException e) {}

        System.out.println(&quot;发送消息完毕！&quot;);
        out.close();
        socket.close();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8、编写程序实现文件查找功能。提供两个参数，第一个参数为查找的起始路径，第二个参数为要查找的文件名称。如果找到，则给出文件的完整路径，否则提示文件不存在。&lt;/h2&gt;
&lt;p&gt;这里查找的内容为：其实路径目录及其子目录下的所有文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.system.IO;

import java.io.File;

public class SearchFileByName {
    public static StringBuffer fileAbsoluteFile;
    public static void main (String[] args) {
        String path = &quot;/home/snofly/IdeaProjects/test/src/high/order/system/IO&quot;;
        String filename = &quot;Input1.java&quot;;
        boolean searchResult = new SearchFileByName().search(path, filename);
        if (searchResult == true) {
            System.out.println(&quot;查找成功，路径为：&quot; + fileAbsoluteFile);
        } else {
            System.out.println(&quot;查找失败&quot;);
        }
    }
    public boolean search (String path, String fileName) {
        File file = new File(path);
        if (file.isFile()) {
            System.out.println(&quot;当前文件路径：&quot; + file.getAbsoluteFile());
            System.out.println(&quot;当前文件名称：&quot; + file.getName());
            if (file.getName().equals(fileName)) {
                fileAbsoluteFile = new StringBuffer(file.getAbsoluteFile().toString());
                return true;
            } else {
                return false;
            }
        }
        if (file.isDirectory()) {
            System.out.println(&quot;当前目录路径：&quot; + file.getAbsoluteFile());
            System.out.println(&quot;当前目录名称：&quot; + file.getName());
            if (file.list().length == 0) {
                System.out.println(&quot;目录为空&quot;);
                return false;
            } else {
                for (int i = 0; i &amp;lt; file.list().length; i++) {
                    System.out.println(&quot;目录递归：&quot; +  file.list()[i]);
                    if (search((file + &quot;/&quot; + file.list()[i]), fileName)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写的有点不好看，但确实废了些心力——好久没写过递归了——类方法都不清楚，刚查的。&lt;/p&gt;
&lt;h1&gt;第十五章&lt;/h1&gt;
&lt;h2&gt;1、网络协议分几层？各层解决的问题是什么？&lt;/h2&gt;
&lt;p&gt;网络协议层次的划分是多重的，因为历史原因并没有完全统一起来。不过，目前使用最广泛的、也是未来标准的划分，就是七层协议五层划分。&lt;/p&gt;
&lt;p&gt;最底层：物理层
通过电气设备，实现比特形式的信息传输。&lt;/p&gt;
&lt;p&gt;第二层：数据链路层
点到点通信，对数据进行检错和纠错。&lt;/p&gt;
&lt;p&gt;第三层：网络层
端到端通信。&lt;/p&gt;
&lt;p&gt;第四层：传输层
通信连接与管理。&lt;/p&gt;
&lt;p&gt;第五层：应用、表示、会话层
面向用户提供应用服务。&lt;/p&gt;
&lt;h2&gt;2、TCP 和 UDP 协议有什么不同，为什么称 TCP 是面向连接的可靠的协议？&lt;/h2&gt;
&lt;p&gt;有三个方面不同：
1）通信方式
前者传输数据之前需要先建立连接；后者直接传输。&lt;/p&gt;
&lt;p&gt;2）传输数量不同
前者能够传输更大量的数据，后者只能传输单元很小。&lt;/p&gt;
&lt;p&gt;3）可靠性不同
前者保证连接和数据正确，后者只保证快速。&lt;/p&gt;
&lt;p&gt;因为 TCP 先连接再传输数据，而且保证数据的正确与完整，所以说 TCP 是面向连接的可靠的协议。&lt;/p&gt;
&lt;h2&gt;3、在 Java 语言当中，网络编程是从协议的什么层次开始的？程序设计时，什么情况下选择 Java 高层次网络编程，什么情况下选择低层次网络编程？&lt;/h2&gt;
&lt;p&gt;Java 网络编程从传输层开始，并分为高层次网络编程（基于应用层）和低层次网络编程（基于传输层）。
（数据链路层的编程和通信与设备紧密相关；网络层编程和操作系统紧密相关。这都不适合具有平台移植性的 Java 。）&lt;/p&gt;
&lt;p&gt;Socket 和 URL 应用场景不同：Socket 面向内部网络通信；URL 适用于面向 Internet 通信。这也是选择 Java 编程方式的依据。&lt;/p&gt;
&lt;h2&gt;4、Socket 编程时，目的地址和端口号需要在什么地方指出？使用数据报时，又在什么地方指出？&lt;/h2&gt;
&lt;p&gt;Socket 构造方法参数中的地址和端口号是目的机器的，而 DatagramSocket 构造方法参数中的则是本机的。使用数据包时，应该在 DatagramPacket 构造方法中指出目的地址和端口号。&lt;/p&gt;
&lt;h2&gt;5、利用 URLconnetction 对象编写程序返回某网站的首页，并将首页内容存放到文件当中。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package high.order.net;
import java.net.*;
import java.io.*;

public class URLReader {
	public static void main(String[] args) throws Exception {
		URL tirc = new URL(&quot;https://www.chd.edu.cn&quot;);
		URLConnection urlConnection = tirc.openConnection();
		InputStream inputStream = new DataInputStream(urlConnection.getInputStream());
		BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
		FileOutputStream fileOutputStream = new FileOutputStream(&quot;Write1.txt&quot;, true);
		while (in.readLine() != null) {
			fileOutputStream.write(in.readLine().getBytes());
			fileOutputStream.write(System.getProperty(&quot;line.separator&quot;).getBytes());
		}
		inputStream.close();
		in.close();
		fileOutputStream.close();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PS：我也不记得这几个流怎么连接装配的了，反正随便 new，有错误再解决，最后能实现功能。&lt;/p&gt;
&lt;h2&gt;6、利用串行化技术和 Socket 通信，将一个客户端构造的对象传送到服务器端，并输出该对象的属性值。&lt;/h2&gt;
&lt;p&gt;服务器端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.net;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class QuestionFifteenPointSixTalkServer{
    public static void main(String args[]) {
        try{
            ServerSocket server = null;
            // 创建一个ServerSocket在端口4700监听客户请求
            server = new ServerSocket(4700);
            Socket socket = null;
            socket = server.accept();
            // 由Socket对象得到输入流
            ObjectInputStream os = new ObjectInputStream(socket.getInputStream());
            Photo photo = (Photo)os.readObject();
            System.out.println(&quot;photoId:&quot; + photo.photoId + &quot; photoSize:&quot; + photo.photoSize);
            os.close();
            socket.close();
            server.close();
        }
        catch (Exception e) {
            System.out.println(&quot;Error:&quot;+e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.net;

import java.io.*;
import java.net.Socket;

class Photo implements Serializable {
    public int photoId;
    public int photoSize;
    public Photo (int photoId, int photoSize) {
        this.photoId = photoId;
        this.photoSize = photoSize;
    }
}

public class QuestionFifteenPointSixTalkClient {
    public static void main(String args[]) {
        try{
            // 向本机的4700端口发出客户请求
            Socket socket = new Socket(&quot;127.0.0.1&quot;,4700);
            // 由Socket对象得到输出流，并构造ObjectOutputStream 对象
            ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream());
            // 创建并串行化对象
            Photo photo = new Photo(1,512);
            os.writeObject(photo);
            // 刷新输出流
            os.flush();
            os.close();
            socket.close();
        }
        catch(Exception e) {
            System.out.println(&quot;Error: &quot;+e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PS：这里只是表示一下对象串行化的使用，以及对象传输，所以程序比较简略。&lt;/p&gt;
&lt;h1&gt;第十六章&lt;/h1&gt;
&lt;h2&gt;1、使用 JDBC 来操作数据库通常包含哪几个步骤？&lt;/h2&gt;
&lt;p&gt;使用 JDBC 操作数据库的步骤：（五个步骤）&lt;/p&gt;
&lt;p&gt;1）载入 JDBC driver
// 类型不同，会有不同的载入操作。&lt;/p&gt;
&lt;p&gt;2）得到数据库的 Connection 对象
// 注意，Connection、Statement、ResultSet 都是接口，要用特定的方式产生对象。&lt;/p&gt;
&lt;p&gt;3）根据连接对象得到 Statement 进行查询或更新数据。&lt;/p&gt;
&lt;p&gt;4）如果执行查询，则对记录集 ResultSet 进行遍历操；如果执行更新，则根据成功与否的返回结果进行数据库事务操作。
// 这里有很多细节操作值得注意，能够让运用更加灵活。&lt;/p&gt;
&lt;p&gt;5）操作结束后，&lt;strong&gt;依次&lt;/strong&gt;对 ResultSet、Statement、Connection 执行关闭操作。
// 注意，依次关闭，且由于容易发生异常，应该放在 finally 中执行。&lt;/p&gt;
&lt;h2&gt;2、如何载入 JDBC driver？&lt;/h2&gt;
&lt;p&gt;分两种情形：
1）对类型三（JDBC/ODBC 桥这种情形）
安装数据库客户端并配置 ODBC 数据源，驱动程序在 rt.jar 文件中，其路径由默认环境指出，无须在 classpath 中设置。&lt;/p&gt;
&lt;p&gt;2）对类型一（纯 Java 驱动）和类型二（JDBC 调用客户端）
首先从数据库厂商那里获取相应数据库的 JDBC driver 类库，再载入相应驱动。&lt;/p&gt;
&lt;h2&gt;3、java.sql 包中的主要接口 Connection、Statement 和 ResultSet 之间是什么关系？&lt;/h2&gt;
&lt;p&gt;ResultSet 实例只能由 Statement 实例的方法得到；
Statement 实例只能由 Connection 实例的方法得到；
Connection 实例只能通过 DriveManager 的方法得到。&lt;/p&gt;
&lt;h2&gt;4、数据库使用完毕之后，要进行哪些关闭操作？&lt;/h2&gt;
&lt;p&gt;操作结束后，&lt;strong&gt;依次&lt;/strong&gt;对 ResultSet、Statement（或者 prepareStatement）、Connection 执行关闭操作。&lt;/p&gt;
&lt;p&gt;（注意，依次关闭，且由于容易发生异常，应该放在 finally 中执行。）&lt;/p&gt;
&lt;h2&gt;5、对第 10 章第 10 题进行改造，使 HashTable 中的异常类型与中文提示均来源于数据库。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package high.order.jdbc;

import java.sql.*;
import java.util.ConcurrentModificationException;

public class TestSixteenPointFive {
    // MySQL 8.0 以下版本 - JDBC 驱动名及数据库 URL
    // static final String JDBC_DRIVER = &quot;com.mysql.jdbc.Driver&quot;;
    // static final String DB_URL = &quot;jdbc:mysql://localhost:3306/DBTest&quot;;

    // MySQL 8.0 以上版本 - JDBC 驱动名及数据库 URL
    static final String JDBC_DRIVER = &quot;com.mysql.cj.jdbc.Driver&quot;;
    static final String DB_URL = &quot;jdbc:mysql://127.0.0.1:3306/DBTest?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;serverTimezone=UTC&quot;;

    // 数据库的用户名与密码，需要根据自己的设置
    static final String USER = &quot;root&quot;;
    static final String PASS = &quot;root123456&quot;;

    public static void foo() {
        String str=null;
        str.length();
    }

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try{
            // 注册 JDBC 驱动
            System.out.println(&quot;注册 JDBC 驱动...&quot;);
            Class.forName(JDBC_DRIVER);

            // 打开链接
            System.out.println(&quot;连接数据库...&quot;);
            conn = DriverManager.getConnection(DB_URL,USER,PASS);

            // 执行查询
            System.out.println(&quot; 实例化Statement对象...&quot;);
            stmt = conn.createStatement();
            String sqlFrist = &quot;SELECT chinese_text FROM HashTableException WHERE hashtable_exception = &apos;&quot;;
            String sqlLast = &quot;&apos;&quot;;

            try {
                System.out.println(&quot;运行功能程序主体...&quot;);
                foo();
            }
            catch (ConcurrentModificationException e){
                rs = stmt.executeQuery(sqlFrist + &quot;ConcurrentModificationException&quot; + sqlLast);
                System.out.println(&quot;出现异常：&quot; + rs.getString(&quot;chinese_text&quot;));
            }
            catch (IllegalArgumentException e){
                rs = stmt.executeQuery(sqlFrist + &quot;IllegalArgumentException&quot; + sqlLast);
                System.out.println(&quot;出现异常：&quot; + rs.getString(&quot;chinese_text&quot;));
            }
            catch (NullPointerException e){
                rs = stmt.executeQuery(sqlFrist + &quot;NullPointerException&quot; + sqlLast);
                rs.next();
                System.out.println(&quot;出现异常：&quot; + rs.getString(&quot;chinese_text&quot;));
            }
            finally{
                // 关闭资源
                try{
                    if(rs != null)  rs.close();
                }catch(SQLException se2){
                    System.out.println(&quot;不正常，结果集为空！&quot;);
                }
            }
        }
        catch (SQLException se) {
            // 处理 JDBC 错误
            se.printStackTrace();
        }
        catch (Exception e) {
            // 处理 Class.forName 错误
            e.printStackTrace();
        }
        finally {
            // 关闭资源
            try {
                if (stmt != null) stmt.close();
            }
            catch (SQLException se2) {}
            try{
                if(conn != null) conn.close();
            }catch(SQLException se){
                se.printStackTrace();
            }
        }
        System.out.println(&quot;结束&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有相应数据库的话，这个程序运行不起来。但是作为一个实例，还是挺规范的。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;不信你看，我在 finally 里面关闭连接，你看他关得严不严实？&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;6、实现两个方法。&lt;/h2&gt;
&lt;p&gt;原题目：
现有代码如下，ExeSql 类当中有三个方法，其中 getConnection 方法是得到数据库连接对象，本方法假定已经实现。现编写两个重载的方法 execSql，第一个方法的参数为字符串，用来执行数据库操作语句（插入、删除、修改），如果执行成功则返回 true，失败返回 false；第二个方法为一个字符串数组，每个单元存放一条数据操作语句，如果所有语句执行成功，则返回 true，有一条失败，返回 false，并输出出错语句。请将这两个方法给出相应的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ExeSql{
	public boolean execSql(String sqlStr){
	}
	public boolean execSql(String[] sql){
	}
	private Connection getConnection(){
	// 得到同数据库相连的各种方法
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package high.order.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Random;

public class ExeSql {
    static final String JDBC_DRIVER = &quot;com.mysql.cj.jdbc.Driver&quot;;
    static final String DB_URL = &quot;jdbc:mysql://127.0.0.1:3306/DBTest?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;serverTimezone=UTC&quot;;
    static final String USER = &quot;root&quot;;
    static final String PASS = &quot;root123456&quot;;
    private Connection getConnection(){
        Connection conn = null;
        try {
            Class.forName(JDBC_DRIVER);
            conn = DriverManager.getConnection(DB_URL, USER, PASS);
        }
        catch (SQLException se) {
            se.printStackTrace();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        return conn;
    }

    public boolean execSql(String sqlStr){
        boolean executeResult = true;
        Connection connection = new ExeSql().getConnection();
        Statement stmt = null;
        try {
            stmt = connection.createStatement();
            String randown = new Random().nextInt(100) + &quot;&quot;;
            stmt.execute(&quot;&quot; +
                    &quot;insert into HashTableException(id,chinese_text) values(&quot; +
                            randown +
                            &quot;,&apos;&quot; +
                            sqlStr +
                            &quot;&apos;)&quot;
                    );
            executeResult = true;
        }
        catch (SQLException se) {
            // 处理 JDBC 错误
            // se.printStackTrace();
            executeResult = false;
        }
        catch (Exception e) {
            // 处理 Class.forName 错误
            // e.printStackTrace();
            executeResult = false;
        }
        finally{
            try{
                if(connection != null) {
                    // System.out.println(&quot;connection yes&quot;);
                    connection.close();
                }
            }
            catch (SQLException se2) {
                System.out.println(&quot;不正常，connection 为空！&quot;);
            }
            try{
                if(stmt != null)  stmt.close();
            }
            catch (SQLException se2) {
            }
            return executeResult;
        }
    }

    public boolean execSql(String[] sql){
        boolean executeResult = true;
        Connection connection = new ExeSql().getConnection();
        Statement stmt = null;
        try {
            stmt = connection.createStatement();
            for (int i = 0; i &amp;lt; sql.length; i++) {
                stmt.execute(sql[i]);
            }
            executeResult = true;
        }
        catch (SQLException se) {
            // 处理 JDBC 错误
            // se.printStackTrace();
            executeResult = false;
        }
        catch (Exception e) {
            // 处理 Class.forName 错误
            // e.printStackTrace();
            executeResult = false;
        }
        finally{
            try{
                if(connection != null) {
                    // System.out.println(&quot;connection yes&quot;);
                    connection.close();
                }
            }
            catch (SQLException se2) {
                System.out.println(&quot;不正常，connection 为空！&quot;);
            }
            try{
                if(stmt != null)  stmt.close();
            }
            catch (SQLException se2) {
            }
            return executeResult;
        }
    }

    public static void main(String[] args) {
        boolean bString = new ExeSql().execSql(&quot;str&quot;);
        System.out.println(&quot;String 调用返回：&quot; + bString);

        String[] stringArray= {
                &quot;insert into HashTableException(id,chinese_text) values(99,&apos;sdg&apos;)&quot;,
                &quot;UPDATE HashTableException SET chinese_text = &apos;change&apos; WHERE id = 3&quot;};

        boolean bStringArray = new ExeSql().execSql(stringArray);

        System.out.println(&quot;String[] 调用返回：&quot; + bStringArray);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PS：我好像没有理解清楚题意，多谢了一部分。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;我才不会承认，是我懒得改了。&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;第十七章&lt;/h1&gt;
&lt;h2&gt;1、简要回答 UML 的统一、建模、语言的三层含义。&lt;/h2&gt;
&lt;p&gt;统一：建模符号的统一。
建模：捕捉、描述系统的核心。
语言：可视化、拥有独特语法和语义的建模语言。&lt;/p&gt;
&lt;h2&gt;2、UML 的主要特点有哪些？&lt;/h2&gt;
&lt;p&gt;三个特点：
1）建模设计上的标准化与可视化的结合。
2）建模设计上的语言无关性。
3）建模设计上标准性与拓展性的统一。&lt;/p&gt;
&lt;h2&gt;3、UML 的建模工具有哪些？请选择一种安装和应用。&lt;/h2&gt;
&lt;p&gt;建模工具有非常多，比较流行的是 IBM 公司的 Rational Rose 和 Sybase 公司的 PowerDesigner。&lt;/p&gt;
&lt;p&gt;（我选的是比较轻量的工具 DIA，因为我是想在 Ubuntu 系统下开发软件。我暂时用不到太好的功能。）&lt;/p&gt;
&lt;h2&gt;4、UML 的视图和图有哪些？它们的对应关系是什么？图都有哪些主要的分类？这些图在设计过程中的演化关系是什么？&lt;/h2&gt;
&lt;p&gt;4.1.1 UML 视图有 5 种，也被称为 4+1 结构的视图：
逻辑视图、进程视图、构建视图、部署视图，再加上用例视图。&lt;/p&gt;
&lt;p&gt;4.1.2 UML 图有 10 种：
用例图、类图、对象图、包图、构件图、部署图、顺序图、协作图、状态图、活动图。&lt;/p&gt;
&lt;p&gt;4.2 UML 视图由若干图进行描述，他们的对应关系为：
用例图---用例视图
对象图---用例视图、逻辑视图
顺序图---用例视图、逻辑视图
协作图---用例视图、逻辑视图
活动图---用例视图、逻辑视图
类图---逻辑视图
包图---逻辑视图
状态图---逻辑视图
构件图---构件视图
部署图---部署视图&lt;/p&gt;
&lt;p&gt;4.3 图主要的分类：
分为静态模型图和动态模型图两类。&lt;/p&gt;
&lt;p&gt;4.4 演化逻辑关系：
软件设计是一个由表及里、由浅入深、从外到内的一个逐渐深化的过程。
UML 的各类图反应了人们对软件系统的认识规律。&lt;/p&gt;
&lt;h2&gt;5、用例图都有哪些模型元素，它们的含义是什么？&lt;/h2&gt;
&lt;p&gt;用例图有 6 种模型元素：
1）用例
用例是必须被驱动的功能，功能应完整，且外部可见、相互独立。
2）参与者（角色）
参与者是驱动用例的角色，可以是人也可以是物品。
3）关联关系
参与者和用例之间的沟通渠道。
4）包含关系
用例与用例的一种依赖关系。
5）拓展关系
用例与用例的一种依赖关系。
6）继承关系
用例与用例，参与者与参与者之间的一种继承关系。&lt;/p&gt;
&lt;h2&gt;6、类图和对象图有哪些相同点和不同点？&lt;/h2&gt;
&lt;p&gt;类图用于描述类以及类之间的关系，对象图用于描述对象及对象之间的关系。&lt;/p&gt;
&lt;h2&gt;7、简要画出类图的类、泛化、关联、接口、依赖等模型元素的建模符号。&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;708b683e0dac887da7c30f3d5bff031b.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（都有，随便展示一下。）&lt;/p&gt;
&lt;h2&gt;8、默写出第 17.5.4 节中给出的关联对应的代码表示。&lt;/h2&gt;
&lt;p&gt;代码实现的基本模式：（相互引用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class A {
    B b;
}

Class B {
    A a;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9、关联的类型有哪些？四种特殊的关联各是什么？&lt;/h2&gt;
&lt;p&gt;关联的类型有四种：（按照相互关联的类的个数划分）
一元关联（自关联或递归关联）、二元关联、三元关联、多元关联。&lt;/p&gt;
&lt;p&gt;四种特殊的关联：
关联类、受限关联、聚合关联、组合关联。&lt;/p&gt;
&lt;h2&gt;10、依赖的含义有哪些？对应的代码又是什么？&lt;/h2&gt;
&lt;p&gt;依赖的含义：
1）类和对象之间的依赖；
2）对接口的使用；
3）类之间的依赖。&lt;/p&gt;
&lt;p&gt;代码实现：
一个类中声明依赖目标的引用，然后使用其内，这就形成依赖关系了。&lt;/p&gt;
&lt;h2&gt;11、顺序图和协作图中的消息有哪些类型？请给出一个顺序图中的消息和代码的对应关系。&lt;/h2&gt;
&lt;p&gt;顺序图和协作图的消息类型有四种：
简单消息、同步消息、异步消息、返回消息。&lt;/p&gt;
&lt;p&gt;比如，同步消息就是调用方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class A {
	// 调用之前的内容
    B b = new B().function();
    // 调用之后的内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;12、活动图和状态图的相同点和不同点是什么？&lt;/h2&gt;
&lt;p&gt;相同点：
1）图中的建模元素有相似之处；
2）对系统或对象在生存周期的状态和行为进行描述。&lt;/p&gt;
&lt;p&gt;不同点：
1）状态图中迁移发生时，必须有事件发生或者条件满足；
而活动图中迁移发生时，可以无条件或者没有事件发生。
2）状态图对于系统状态的描述可以横跨多个用例；
而活动图则是针对同一用例内部的动作执行给出描述。&lt;/p&gt;
&lt;h2&gt;13、案例 2 的用例图中，图形的选择和图形的删除是包含关系，而图形的移动和改变是拓展关系，为什么？&lt;/h2&gt;
&lt;p&gt;删除图形之前必须选择图形，因为没有其他办法；而移动和改变图形则有其他办法。
比如：通过整体缩放、软件移动，即可实现上述功能。&lt;/p&gt;
&lt;h1&gt;第十八章&lt;/h1&gt;
&lt;h2&gt;1、设计模式的含义是什么？&lt;/h2&gt;
&lt;p&gt;设计模式的含义：
设计模式描述了软件开发过程中若干重复出现问题的解决方案。&lt;/p&gt;
&lt;h2&gt;2、设计模式的原则有哪些？各自的含义是什么？&lt;/h2&gt;
&lt;p&gt;三个原则：&lt;/p&gt;
&lt;p&gt;1）开闭原则
开闭原则是指软件系统对拓展开放，对修改关闭。&lt;/p&gt;
&lt;p&gt;2）组合 /聚合复用原则
继承是白箱复用，而组合聚合则是黑箱复用。
在复用选择时应该优先选择黑箱复用，有四个优点。&lt;/p&gt;
&lt;p&gt;3）高内聚松耦合原则
耦合是指两个对象相互依赖对方的程度，内聚是指一个对象内部功能相联的程度。&lt;/p&gt;
&lt;h2&gt;3、设计模式的分类依据是什么？模式构成的主要要素都有哪些？&lt;/h2&gt;
&lt;p&gt;设计模式依据范围或目的进行分类。&lt;/p&gt;
&lt;p&gt;模式构成主要有四个要素：
模式名称、问题、解决方案、效果。&lt;/p&gt;
&lt;h2&gt;4、结合第 17.12 节和第 17.13 节的代码，讨论都应用了哪些设计模式？&lt;/h2&gt;
&lt;p&gt;（这 23 种设计模式没有细看，无法回答。）&lt;/p&gt;
&lt;h1&gt;第十九章&lt;/h1&gt;
&lt;h2&gt;1、软件框架的含义是什么？&lt;/h2&gt;
&lt;p&gt;软件框架（Software Framework）是对通用功能操作进行抽象的软件。&lt;/p&gt;
&lt;h2&gt;2、软件框架与设计模式的关系是什么？&lt;/h2&gt;
&lt;p&gt;框架是软件，设计模式是软件的知识。&lt;/p&gt;
&lt;h2&gt;3、主要的 Java 应用框架有哪些？&lt;/h2&gt;
&lt;p&gt;常见的框架技术路线主要有两类：
1）微软的 .NET Framework；
2）基于 J2EE 的 Java 应用框架。&lt;/p&gt;
&lt;h2&gt;4、软件框架与应用的正向控制和反向控制的含义是什么？&lt;/h2&gt;
&lt;p&gt;表示应用和软件框架之间的依赖关系：
应用依赖于框架，控制关系就为反向；
若应用不依赖于框架，控制关系就会反向。&lt;/p&gt;
&lt;h2&gt;5、MVC 的含义是什么？&lt;/h2&gt;
&lt;p&gt;MVC （Model-View-Controller）架构：
应用程序通常需要处理数据及其界面表示这样的功能需求，需要处理的数据就是 Model，而数据的表现就是 View 。&lt;/p&gt;
&lt;h2&gt;6、简要介绍一下 Struts1 的框架结构。&lt;/h2&gt;
&lt;p&gt;Struts1 框架负责为 Model、View、Controller 三者之间的关系调用提供一个基本的环境。&lt;/p&gt;
&lt;p&gt;Struts1 有四个不足之处：
1）框架同应用是正向控制关系，即应用依赖于框架；
2）易产生线程安全问题；
3）ActionForm 的冗余问题；
4）表现层单一：只支持 JSP。&lt;/p&gt;
&lt;h2&gt;7、Struts1 如何体现 MVC 的结构设计，设计和部署一个 Struts1 的主要步骤有哪些？&lt;/h2&gt;
&lt;p&gt;服务器端结构，由 Servlet（Controller）、JSP（View）、JavaBean（Model）构成。&lt;/p&gt;
&lt;p&gt;步骤：
Tomcat 配置；安装 JDK；Struts1 应用部署；应用类部署。&lt;/p&gt;
&lt;h2&gt;8、简要介绍一个 Struts2 的框架结构。&lt;/h2&gt;
&lt;p&gt;Struts2 源于 WebWork，可以看做是 WebWork 的升级版，而不是 Struts1 的升级版。Struts1 和 Struts2 的编程思维截然不同。&lt;/p&gt;
&lt;p&gt;Struts2 的 Controller 室友核心控制器 FilterDispatcher 和 Action 组成的。&lt;/p&gt;
&lt;h2&gt;9、Struts1 和 Struts2 的区别点是什么？&lt;/h2&gt;
&lt;p&gt;Struts2 与 Struts1 的对比：
1）Struts2 是个 IoC 反向控制容器，大大降低两类框架和应用的耦合；
2）Struts2 为每一个请求单独建立一个 Action 对象，从而不会产生线程安全问题；
3）Struts2 视图通过支持模板技术拓展了表现类型；
等等...&lt;/p&gt;
&lt;h1&gt;第二十章&lt;/h1&gt;
&lt;h2&gt;1、软件体系结构的含义和主要类型有哪些？&lt;/h2&gt;
&lt;p&gt;软件体系结构：
两种成分的集合：其一，具有一定形式的软件结构化元素；其二，他们之间的关系。&lt;/p&gt;
&lt;p&gt;软件系统的分类：
1）从构成角度划分
构件、他们之间的调用关系
2）从开发过程角度划分
概念结构、物理结构
3）从运行角度划分
静态结构、动态结构
4）从部署的角度
集中式结构、分布式结构**&lt;/p&gt;
&lt;h2&gt;2、C/S 和 B/S 各自都有哪些优点和缺点？&lt;/h2&gt;
&lt;p&gt;C/S 的优缺点：
优点是开发容易，部署简单，执行速度块，界面表现形式丰富。
缺点是维护困难。&lt;/p&gt;
&lt;p&gt;B/S 的优缺点：
优点是维护拓展方便。
缺点是开发和部署相对复杂。&lt;/p&gt;
&lt;h2&gt;3、软件客户端都有哪些主要类型？&lt;/h2&gt;
&lt;p&gt;客户端三种类型：
胖客户端：C/S 结构中的客户端；
瘦客户端：B/S 结构中的浏览器端；
富客户端：介于二者之间。&lt;/p&gt;
&lt;h2&gt;4、分布式软件系统需要处理哪些共性问题？&lt;/h2&gt;
&lt;p&gt;1）网络通信协议和通信方式的选择；
2）提供远程过程调用（Remote Procedure Call，RPC）接口；
3）名称查找，快速在网络上找到需要调用服务的计算机名称、地址、端口等；
4）安全机制，分布式软件系统彼此进行通信时需要确保数据传输和身份认证的安全性；
5）事务管理。&lt;/p&gt;
&lt;h2&gt;5、中间件的含义是什么？消息模型的类型有哪些？&lt;/h2&gt;
&lt;p&gt;中间件是将不同软件构件或者操作者与多种应用程序连接起来的软件。&lt;/p&gt;
&lt;p&gt;消息分类：
1）按照参与者数量划分：点到点、发布/订阅；
2）按照传递方向：推、拉；
3）按照通信方式：同步、异步。&lt;/p&gt;
&lt;h2&gt;6、JMS 的含义是什么？&lt;/h2&gt;
&lt;p&gt;JMS （Java Message Service）是一组 Java 应用程序接口，它规定了创建、发送、接受、读取消息的一系列标准。&lt;/p&gt;
&lt;h2&gt;7、主要的分布式对象技术有哪些？&lt;/h2&gt;
&lt;p&gt;OMG 的 CORBA 主要是对标准进行规定。
JavaRMI、JNDI、COM+ 既是标准也是实现。
WbeService 采用 XL 格式规定了一系列由 W3C 国际组织确认的标准规范。&lt;/p&gt;
&lt;h2&gt;8、RMI 通信原理是什么？结合书上的案例给出你的一个关于数据库操作的 RMI 设计。（提示：可将对数据库的查询和操作放到 RMI 的服务器端，客户端仅提交请求和得到返回结果）&lt;/h2&gt;
&lt;p&gt;RMI 可以说是 Java 的 RPC 技术，即一个 JVM 内存空间对象可以获得另一个 JVM 内存空间对象的远程引用，并向其发送消息。&lt;/p&gt;
&lt;p&gt;（设计略）&lt;/p&gt;
&lt;h2&gt;9、JNDI 的含义是什么？&lt;/h2&gt;
&lt;p&gt;JNDI （Java Namig and Directory Interface）是为 Java 应用程序提供目录命令和命名服务功能的接口，负责对 Java 分布式对象进行管理和检索。&lt;/p&gt;
&lt;h2&gt;10、WebService、结构层次是什么？&lt;/h2&gt;
&lt;p&gt;WebService 结构层次有四层：
发现服务——UDDI
服务描述——WSDL
服务调用——SOAP
传送协议——HTTP&lt;/p&gt;
&lt;h2&gt;11、简要介绍 WSDL、UDDI、SOAP 的含义。&lt;/h2&gt;
&lt;p&gt;WSDL（Web Services Description Language）是基于 XML 格式被 W3C 组织确认的标准协议，用于对 Web 服务接口和服务位置进行描述。&lt;/p&gt;
&lt;p&gt;UDDI（Universal Description，Discovery and Integration）是一种独立于平台、基于 XML 格式、用于在互联网上描述 Web 服务的协议。&lt;/p&gt;
&lt;p&gt;SOAP（Simple Object Access Protocol）是有微软、IBM 等公司发起由 W3C 组织确认的标准协议。它采用 XML 格式定义，可以将本地方法调用转为适合网络传输的形式（SOAP 请求），同时将 Web 服务经网络传输返回的调用结果转化为本地方法的返回值（SOAP 响应）。&lt;/p&gt;
&lt;h2&gt;12、参考本章的案例结合 XFire 设计一个你的 WebService 应用。&lt;/h2&gt;
&lt;p&gt;（设计略）&lt;/p&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;p&gt;卧槽，终于写完了（吧？）。
出来混迟早要还的。
还是珍惜当下，免得以后难受。&lt;/p&gt;
</content:encoded></item><item><title>NLP-KD 课件学习笔记</title><link>https://0110wdj.github.io/snofly-blog/posts/ml/machine-learning/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/ml/machine-learning/</guid><pubDate>Wed, 18 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;一、基础理论&lt;/h1&gt;
&lt;h2&gt;（一）基本概念&lt;/h2&gt;
&lt;p&gt;1、Natural Language(&lt;strong&gt;自然语言&lt;/strong&gt;)
2、Artificial Language(&lt;strong&gt;人工语言&lt;/strong&gt;)
3、Natural Language Processing(&lt;strong&gt;自然语言处理&lt;/strong&gt;)：
从应用系统实现和语言工程的角度开展语言信息处理方法的研究。
4、专门针对中文的语言信息技术研究称为&lt;strong&gt;中文信息处理&lt;/strong&gt;。
5、Computation linguistic(&lt;strong&gt;计算语言学&lt;/strong&gt;)：
侧重于计算方法和语言学理论方面的研究。
6、BioNLP(Biomedical NLP,&lt;strong&gt;生物医学文本挖掘&lt;/strong&gt;)&lt;/p&gt;
&lt;h2&gt;（二）NLP 的行业应用&lt;/h2&gt;
&lt;p&gt;1、Spell and Grammar Checking
其一：Checking spelling and grammar.
其二：Suggesting alternatives(替代选择) for the errors.&lt;/p&gt;
&lt;p&gt;2、Word Prediction
Predicting the next word that is highly probable to be typed(输入) by the user.&lt;/p&gt;
&lt;p&gt;3、Information Retrieval(检索)
Finding relevant information to the user’s query.&lt;/p&gt;
&lt;p&gt;4、Text Categorization(分类)
Assigning(分配) one (or more) pre-defined category to a text.&lt;/p&gt;
&lt;p&gt;5、Summarization(摘要)
Generating(生成) a short summary from one or more documents, sometimes based on a given query.&lt;/p&gt;
&lt;p&gt;6.Question answering&lt;/p&gt;
&lt;p&gt;7.Information Extraction(提取)
Extracting important concepts(概念) from texts and assigning them to slot(位置) in a certain template(模板); named-entity recognition.&lt;/p&gt;
&lt;p&gt;8.Machine Translation
Translating a text from one language to another.&lt;/p&gt;
&lt;p&gt;9.Sentiment Analysis&lt;/p&gt;
&lt;p&gt;10.Speech recognition
Recognizing a spoken language and transforming it into a text.&lt;/p&gt;
&lt;p&gt;11.Speech synthesis( synthesis)&lt;/p&gt;
&lt;p&gt;12.Spoken dialog(对话框) systems&lt;/p&gt;
&lt;h2&gt;（三）NLP 基础技术/研究内容&lt;/h2&gt;
&lt;p&gt;1、层次划分
2、Part-of-speech(词性) tagging
3、Syntatic Parsing(解析)
4、Dependency(依赖) Parsing
5、Named-entity recognition
6、Word sense disambiguation(消歧)
7、Semantic(语义) role labeling&lt;/p&gt;
&lt;h2&gt;（四）NLP 的语言学知识&lt;/h2&gt;
&lt;p&gt;NLP 结合了现代语音学/音系学、词法学、句法学、语义学、语用学的知识。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;词法学(Morphology)&lt;/strong&gt;
研究词(word) 由有意义的基本单位－词素(morphemes) 的构成问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;句法学(Syntax)&lt;/strong&gt;
研究句子结构成分之间的相互关系和组成句子序列的规则。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语义学(Semantics)&lt;/strong&gt;
研究如何从一个语句中词的意义，以及这些词在该语句中句法结构中的作用来推导出该语句的意义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语用学(Pragmatics)&lt;/strong&gt;
研究通过语言达到目标，以及上下文对语义理解所产生的影响。&lt;/p&gt;
&lt;h2&gt;（五）NLP 的困难和挑战&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;歧义现象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、词法歧义：词素组合成词产生歧义
词素：词根、前缀、后缀、词尾
2、词法歧义(词性歧义)
3、句法歧义(结构歧义)
词(或短语)的组合导致的歧义
4、句法歧义(结构歧义)
5、语义歧义
6、语用层面(Discourse analysis) 和语境、上下文有关&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;未知语言现象&lt;/strong&gt;
1、新词、人名、地名、术语等
2、旧词新意
3、新用法新句型&lt;/p&gt;
&lt;h2&gt;（六）NLP 研究方法&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;一般技术线路&lt;/s&gt;
1、基于规则的方法
&lt;s&gt;实际技术路线&lt;/s&gt;
2、基于统计的方法&lt;/p&gt;
&lt;h1&gt;二、NDarrays&lt;/h1&gt;
&lt;p&gt;优点：速度快！&lt;/p&gt;
&lt;h2&gt;（一）numpy 语句&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;numpy.array()&lt;/strong&gt;
直接对应运算，例如哈达吗积。
&lt;img src=&quot;cf24fa4986866eecb0abd71541f1e5e1.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;6e1c5f0a67d4dbf0781f11afb5be4b58.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;strong&gt;numpy.dot()&lt;/strong&gt;
&lt;img src=&quot;75630c1a4787bd9f5e707dad0751014e.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;ec6a0e22480562b4e925b7b38617c423.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;np.matmul()&lt;/strong&gt;
(暂时没发现有什么用)
&lt;img src=&quot;9dcce860db04a945b881381d1c3acfa3.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（二）Broadcasting&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;一般规则&lt;/strong&gt;
&lt;img src=&quot;c370f41277d02ff2d847477893129241.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;数组+数字
&lt;img src=&quot;8fc949abf406b10fcc25ebdc5835fc2c.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
二维数组+一维数组&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;3cff3318fae836d3fc4bf269c9cc71d0.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（三）Mathematical Functions&lt;/h2&gt;
&lt;p&gt;维度合并
&lt;img src=&quot;b021e47b30a695ae08841001c682809a.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（四）最常用法合集&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;scipy.linalg.inv&lt;/strong&gt;
Inverse of matrix (numpy as equivalent)
矩阵的逆(与 numpy 等价)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;scipy.linalg.ei&lt;/strong&gt;
Get eigen value (Read documentation on eigh and numpy equivalent)
获取特征值(阅读等效 eigh 和 numpy 的文档)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;scipy.spatial.distance&lt;/strong&gt;
Compute pairwise distance
计算两两距离&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;np.matmul&lt;/strong&gt;
Matrix multiply
矩阵相乘&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;np.zeros&lt;/strong&gt;
Create a matrix filled with zeros (Read on np.ones)
创建一个满是 0 的矩阵(读 np.ones)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;np.arange&lt;/strong&gt;
Start, stop, step size (Read on np.linspace)
启动、停止、步长(在 np.linspace 上阅读)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;np.identity&lt;/strong&gt;
Create an identity matrix
创建一个单位矩阵&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;np.vstack&lt;/strong&gt;
Vertically stack 2 arrays (Read on np.hstack)
垂直堆栈 2 个数组(在 np.hstack 上读取)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;array.shape&lt;/strong&gt;
Get shape of numpy array
获取 numpy 数组的形状&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;array.dtype&lt;/strong&gt;
Check data type of array (for precision, for weird behavior)
检查数组的数据类型(为了精确，为了古怪的行为)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;type(stuff)&lt;/strong&gt;
Get type of a variable
获取变量的类型&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;import pdb; pdb.set_trace()&lt;/strong&gt;
Set a breakpoint (https://docs.python.org/3/library/pdb.html)
设置一个断点&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;print(f’My name is {name}’)&lt;/strong&gt;
Easy way to construct a message
构造消息的简单方法
f = &quot;韩梅梅&quot;
print(f&apos;My name is {f}&apos;)&lt;/p&gt;
&lt;p&gt;&lt;s&gt;Python scripting in the terminal is great for this!&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;三、隐马尔科夫模型&lt;/h1&gt;
&lt;h2&gt;（一）Markov 模型&lt;/h2&gt;
&lt;p&gt;存在一类重要的随机过程
一个系统有 N 个 状态 S1, S2,…, SN，随着&lt;strong&gt;时间&lt;/strong&gt;的推移，该系统从某一&lt;strong&gt;状态&lt;/strong&gt;转移到另一状态。&lt;/p&gt;
&lt;p&gt;如果用 𝑞𝑡 表示系统在时间 t 的&lt;strong&gt;状态变量&lt;/strong&gt;，那么，t 时刻的&lt;strong&gt;状态取值&lt;/strong&gt;为 𝑆𝑗(1 ≤ 𝑗 ≤ 𝑁)的概率取决于前 t-1 个时刻 (1, 2, …, t-1) 的状态，该概率为：
𝑝(𝑞𝑡 = 𝑆𝑗|𝑞𝑡−1 = 𝑆𝑖, 𝑞𝑡−2 = 𝑆𝑘, …)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;703cac6fc8020a995df08dc2b0b02cf4.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;f10be959fbbe85f7f6b9db8a0572320a.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;根据 makov 模型(满足两个假设)
状态序列 S1, …, ST 的概率(乘法公式)：
&lt;img src=&quot;04d3abc2d8b9d475b1051a395fc9a15f.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;这样就可以判断某个序列出现的概率了。&lt;/p&gt;
&lt;h2&gt;（二）隐马尔科夫模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;68c3ff629826064542286c3b56526ce0.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;strong&gt;HMM 三(五)要素&lt;/strong&gt;
在一个岛上，这里没有天气预报(隐藏状态)，只有一片片的海藻(观察状态)。
&lt;img src=&quot;3b690d3aa35fb95116901b7b74998bf3.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;S：隐藏状态集合。
O：观察状态集合。
&lt;img src=&quot;6e6a9d4f47dacb555c039b524a268fb8.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;c0bce02fa7b8f84fb0339d11ed46a3a8.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（三）HMM 的三个问题和相应算法&lt;/h2&gt;
&lt;p&gt;1、评估：
根据已知的 HMM 找出一个观察序列的概率 。
解决：前向算法，后向算法。&lt;/p&gt;
&lt;p&gt;2、解码：
根据观察序列 O 找到最可能的隐藏序列 Q。
解决：Viterbi 算法。&lt;/p&gt;
&lt;p&gt;3、参数估计/学习：&lt;img src=&quot;a9c53fb2044a0190669fc8a19f6d9b9a.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
解决：Baum-Welch 算法。&lt;/p&gt;
&lt;h1&gt;四、词法分析(Lexical)-分词&amp;amp;PoS&lt;/h1&gt;
&lt;h2&gt;（一）词法分析概述&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;词&lt;/strong&gt;是自然语言中能够独立运用的最小单位， 是自然语言处理的基本单位。
&lt;strong&gt;自动词法分析&lt;/strong&gt;就是利用计算机对自然语言的形态(morphology) 进行分析，判断词的结构和类别等。
&lt;strong&gt;词性或称词类(Part-of-Speech, PoS&lt;/strong&gt;)是词汇最重要的特性，是连接词汇到句法的桥梁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;句法研究&lt;/strong&gt;的是句子的结构，以词为单位。
&lt;strong&gt;词法研究&lt;/strong&gt;的是词的结构，以词素为单位。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;0a7c262851731ec3951cdbaaa37b6ee6.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（二）英语词法分析&lt;/h2&gt;
&lt;p&gt;1.单词识别 2.形态还原
a.规律变化词还原
b.不规则词形态还原
c.数字形态还原
d.合成词的形态还原&lt;/p&gt;
&lt;h2&gt;（三）汉语分词&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;： 1.规范性问题 2.分词歧义问题
1）交集型歧义：词的交叉组合产生 2) 组合型歧义：覆盖歧义，多义歧义 3) 混合型歧义 3.未登录词的识别&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法&lt;/strong&gt;： 1.基于词典（字符串匹配）的分词方法
1）最大匹配
正向最大匹配算法 (Forward MM, FMM)
逆向最大匹配算法 (Backward MM, BMM)
双向最大匹配算法 (Bi-directional MM)
2）最小匹配 2.基于统计的方法
LM, HMM, CRF 3.基于规则
需要领域专家来定义规则。&lt;/p&gt;
&lt;h2&gt;（四）词性标注&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;词性标注&lt;/strong&gt;
词性(part-of-speech, POS)标注(tagging)
主要任务是消除词性兼类歧义。
&lt;strong&gt;词性兼类&lt;/strong&gt;
在任何一种自然语言中，词性兼类问题都普遍存在。&lt;/p&gt;
</content:encoded></item><item><title>机器学习入门实践</title><link>https://0110wdj.github.io/snofly-blog/posts/ml/nlp/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/ml/nlp/</guid><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;0 前言&lt;/h1&gt;
&lt;p&gt;本文是一个&lt;strong&gt;机器学习&lt;/strong&gt;(Machine Learning, ML)的应用实践，适合初级编码水平的读者。&lt;/p&gt;
&lt;p&gt;本文选择的实践内容是&lt;strong&gt;基于统计的自然语言处理(Natural Language Processing, NLP)&lt;/strong&gt;，具体来说，是基于&lt;strong&gt;朴素贝叶斯算法&lt;/strong&gt;训练的一个二分类器 —— 用于判断收到的邮件是否为垃圾邮件。&lt;/p&gt;
&lt;p&gt;阅读完成之后，应该达到的目标:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 理解&lt;strong&gt;朴素贝叶斯算法&lt;/strong&gt;原理&lt;/li&gt;
&lt;li&gt;2 了解&lt;strong&gt;基于统计的自然语言处理&lt;/strong&gt;的原理&lt;/li&gt;
&lt;li&gt;3 理解&lt;strong&gt;机器学习模型&lt;/strong&gt;的原理&lt;/li&gt;
&lt;li&gt;4 成功完成训练&lt;strong&gt;机器学习模型&lt;/strong&gt;的实践&lt;/li&gt;
&lt;li&gt;5 理解并运用&lt;strong&gt;模型评价指标&lt;/strong&gt;(略)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1 算法原理&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者：算法原理了解一下就行，不用担心现在看不懂，在后文中，会进一步结合代码理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.1 贝叶斯定理&lt;/h2&gt;
&lt;p&gt;贝叶斯定理的数学表达式如下：&lt;/p&gt;
&lt;p&gt;$$
P(A \mid B) = \frac{P(B \mid A) P(A)}{P(B)}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P(A|B) 表示在事件 B 发生的条件下，事件 A 发生的概率（后验概率）。&lt;/li&gt;
&lt;li&gt;P(B|A) 表示在事件 A 发生的条件下，事件 B 发生的概率（似然）。&lt;/li&gt;
&lt;li&gt;P(A) 是事件 A 的先验概率。&lt;/li&gt;
&lt;li&gt;P(B) 是事件 B 的全概率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.2 定理的应用&lt;/h2&gt;
&lt;p&gt;如果我们得到 P(A|B) 的值，那么就能根据一些条件来预测某些事情的发生了。&lt;/p&gt;
&lt;p&gt;举例 1， P(“I&apos;m fine.” | “How are you ?”) ，根据经验，这两句话先后出现概率挺高的，其值假设为的 0.8 , 那么可以这样表述：当一个人对你说 “How are you ?” 时，你有 80% 的概率会回复他 “I′m fine.”&lt;/p&gt;
&lt;p&gt;举例 2, P(“垃圾邮件” | “期待您的光临，电话号码：153xxxx1234”) = 0.7&lt;/p&gt;
&lt;p&gt;举例 3, P(“垃圾邮件” | “我有朋友在那里工作。这个是个个人信箱亚，难道骗简历？”) = 0.01&lt;/p&gt;
&lt;p&gt;总之，我们将 P 看作是一个函数（也就是模型），输入是字符串 B，输出是 P(A|B) 的值。&lt;/p&gt;
&lt;h2&gt;1.3 朴素贝叶斯&lt;/h2&gt;
&lt;p&gt;朴素贝叶斯是贝叶斯定理的一个&lt;strong&gt;特例&lt;/strong&gt;，它简化了问题的假设前提，从而简化问题的解决过程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;简化的假设前提&lt;/strong&gt;：假设特征之间是条件独立的，不考虑特征之间的关系（这也是“朴素”的含义）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特征&lt;/strong&gt;：事件 B 可能是由多个变量组成的一个集合，称为特征集。这些变量都能够影响最终的结果，被称为特征。&lt;/p&gt;
&lt;h1&gt;2 基于统计的 NLP&lt;/h1&gt;
&lt;p&gt;在 NLP 领域有两个研究方法：&lt;strong&gt;基于规则的方法&lt;/strong&gt;和&lt;strong&gt;基于统计的方法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本文对这两种方法的优劣不作评价，当然我们选择后者。&lt;/p&gt;
&lt;h2&gt;2.1 常规的处理流程&lt;/h2&gt;
&lt;p&gt;标准化的操作步骤是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、数据获取&lt;/li&gt;
&lt;li&gt;2、数据预处理&lt;/li&gt;
&lt;li&gt;3、特征提取&lt;/li&gt;
&lt;li&gt;4、模型训练&lt;/li&gt;
&lt;li&gt;5、模型评估&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，第一步的数据获取，一般是用爬虫技术爬取数据，或者购买数据。我们这里准备好了&amp;lt;a href=&quot;/snofly-blog/files/nlp/data.zip&quot; target=&quot;_blank&quot;&amp;gt;训练数据&amp;lt;/a&amp;gt;，直接下载即可。&lt;strong&gt;严郑声明，仅用于学习用途！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第二步的数据预处理，我们的训练数据也已经做好了。我们拿到的数据就是&lt;strong&gt;有标记的&lt;/strong&gt;数据。&lt;/p&gt;
&lt;p&gt;第五步的模型评估，不是本文的重点，直接略过。感兴趣的读者可以查阅&lt;strong&gt;准确率、召回率、F1-score&lt;/strong&gt; 等概念。&lt;/p&gt;
&lt;p&gt;代码实践部分，我们只做第三步和第四步。&lt;/p&gt;
&lt;h2&gt;2.2 特征提取&lt;/h2&gt;
&lt;h3&gt;2.2.1 观察数据&lt;/h3&gt;
&lt;p&gt;在 2.1 节下载训练数据 data.zip 后，解压查看内部结构，应当是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  data tree -L 1
.
├── normal
├── spam
├── test
└── 中文停用词表.txt

4 directories, 1 file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，normal、spam、test 三个文件夹下都有若干 txt 文件，每个 txt 文件的内容是爬取的文本信息。&lt;/p&gt;
&lt;p&gt;normal 表示其内都是正常邮件（个人笔记、好友消息等），spam 表示其内都是垃圾邮件（广告邮件之类的），test 用于测试。&lt;/p&gt;
&lt;p&gt;从 normal 和 spam 中各取一份文件展示一下：&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;data/normal 文件 274.txt&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Return-Path: &amp;lt;chi@rbl.ccert.edu.cn&amp;gt;
Received: from rbl.ccert.edu.cn (rbl.ccert.edu.cn [202.112.57.71])
	by sea.net.edu.cn (8.12.11/8.12.11) with ESMTP id i9G08rJt015597
	for &amp;lt;han@ccert.edu.cn&amp;gt;; Sat, 16 Oct 2004 08:08:53 +0800 (CST)
Received: from rbl.ccert.edu.cn (localhost.localdomain [127.0.0.1])
	by rbl.ccert.edu.cn (8.12.11/8.12.11) with ESMTP id i9G0EVlm030752
	for &amp;lt;han@ccert.edu.cn&amp;gt;; Sat, 16 Oct 2004 08:14:32 +0800
Received: (from qa@localhost)
	by rbl.ccert.edu.cn (8.12.11/8.12.11/Submit) id i9G0EVSi030751;
	Sat, 16 Oct 2004 08:14:31 +0800
Date: Sat, 16 Oct 2004 08:14:31 +0800
Message-Id: &amp;lt;200410160014.i9G0EVSi030751@rbl.ccert.edu.cn&amp;gt;
Subject: =?gb2312?B?ofEgztK1xMXz09Eg0uy12MG1x+kg0KHM7Lbsyq608w==?=
To: han@ccert.edu.cn
From: Tran Quang Anh &amp;lt;chi@cernet.edu.cn&amp;gt;
Content-Type: multipart/mixed; boundary=&quot;----------=_1097885672-2811-72&quot;
MIME-Version: 1.0
X-UIDL: )+[!!?A)&quot;!%/l&quot;!~XO&quot;!

大三了,我们的恋情维系了三年.
   可是,越来越不知道自己在坚持什么.
   坚信他是最适合我的人，因为在一起的时候很快乐.
   可是一旦开学,被分开----距离拉开了，美没了
   因为相信地久天长,因为拿不起,放不下
   身为射手女生,是不是有点名不副实呢?
   吵架的时间居多,怀疑是否相爱,是否在乎对方,
   有时候嘲笑自己的幼稚,可是脾气上来了,就忍不住去伤害
   有时候甚至佩服起自己来,竟然这么久!~
   身边出现了一些人,可是最终还是选择他.因为一份执着吧.
   那些可以天天在一起的恋人们,你们真的很幸福哦.要珍惜呢.
   那些和我一样,仍在坚持着的&quot;单身恋人&quot;,你们怎么想呢?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;data/spam 文件 793.txt&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Return-Path: &amp;lt;qian@163.com&amp;gt;
Received: from 163.com ([61.144.65.194])
	by spam-gw.ccert.edu.cn (MIMEDefang) with ESMTP id j6ALQwsR008055
	for &amp;lt;han@ccert.edu.cn&amp;gt;; Mon, 11 Jul 2005 05:26:59 +0800 (CST)
Message-ID: &amp;lt;200507110526.j6ALQwsR008055@spam-gw.ccert.edu.cn&amp;gt;
From: =?GB2312?B?wda6ow==?= &amp;lt;qian@163.com&amp;gt;
Subject: =?gb2312?B?1sKjqL6twO0vssbO8aOpytWjug==?=
To: han@ccert.edu.cn
Content-Type: text/plain;charset=&quot;GB2312&quot;
Reply-To: qian@163.com
Date: Mon, 11 Jul 2005 05:40:52 +0800
X-Priority: 3
X-Mailer: Foxmail 4.2 [cn]

*******************广州市兴地贸易有限公司*********************

(负责人/经理/财务）您好：
    本公司代理多家公司操作帐务，具有丰富财务经验.可向贵司提供：
（普通销售/服务行业/广告行业/建筑行业/运输行业）发票，只收2%
的税点。若有意，请联系！

            联 系 人：林海
            联系电话：020-31946660    13824444525

承诺：发票一律通过正规手续从税务局办理领购，保证随时查询验证。

本邮件属群发邮件对你造成不便敬请原谅！！

************************欢迎来电咨询****************************
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;他们的区别很明显，一种是个人记录的，一种是推销产品的。&lt;/p&gt;
&lt;p&gt;最后，还有一个“中文停用词表”，这里“停用词”的含义是：语句中的介词、转折词、语气词等，无法表达语句主要含义的词语。&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;中文停用词表节选：&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;,
?
、
。
“
”
《
》
！
，
：
；
？
.
(
)
{
}
[
]
&amp;lt;
&amp;gt;
@
+
-
=
*
末##末
啊
阿
哎
哎呀
哎哟
唉
俺
俺们
按
按照
吧
吧哒
把
罢了
被
本
本着
比
比方
比如
鄙人
彼
彼此
边
别
别的
别说
并
并且
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;通过中文停用词表，可以对&lt;strong&gt;分词&lt;/strong&gt;之后的文本进行过滤，最后得到&lt;strong&gt;有意义的部分&lt;/strong&gt;，也就是能代表整个语句的&lt;strong&gt;特征&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;分词释义&lt;/strong&gt;: 将文本按照预设的语法规则进行分割。举例：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;原文&lt;/th&gt;
&lt;th&gt;正向最长匹配&lt;/th&gt;
&lt;th&gt;逆向最长匹配&lt;/th&gt;
&lt;th&gt;双向最长匹配&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;项目的研究&lt;/td&gt;
&lt;td&gt;[&apos;项目&apos;, &apos;的&apos;, &apos;研究&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;项&apos;, &apos;目的&apos;, &apos;研究&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;项&apos;, &apos;目的&apos;, &apos;研究&apos;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;商品和服务&lt;/td&gt;
&lt;td&gt;[&apos;商品&apos;, &apos;和服&apos;, &apos;务&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;商品&apos;, &apos;和&apos;, &apos;服务&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;商品&apos;, &apos;和&apos;, &apos;服务&apos;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;研究生命起源&lt;/td&gt;
&lt;td&gt;[&apos;研究生&apos;, &apos;命&apos;, &apos;起源&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;研究&apos;, &apos;生命&apos;, &apos;起源&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;研究&apos;, &apos;生命&apos;, &apos;起源&apos;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;当下雨天地面积水&lt;/td&gt;
&lt;td&gt;[&apos;当下&apos;, &apos;雨天&apos;, &apos;地面&apos;, &apos;积水&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;当&apos;, &apos;下雨天&apos;, &apos;地面&apos;, &apos;积水&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;当下&apos;, &apos;雨天&apos;, &apos;地面&apos;, &apos;积水&apos;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;结婚的和尚未结婚的&lt;/td&gt;
&lt;td&gt;[&apos;结婚&apos;, &apos;的&apos;, &apos;和尚&apos;, &apos;未&apos;, &apos;结婚&apos;, &apos;的&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;结婚&apos;, &apos;的&apos;, &apos;和&apos;, &apos;尚未&apos;, &apos;结婚&apos;, &apos;的&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;结婚&apos;, &apos;的&apos;, &apos;和&apos;, &apos;尚未&apos;, &apos;结婚&apos;, &apos;的&apos;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;欢迎新老师生前来就餐&lt;/td&gt;
&lt;td&gt;[&apos;欢迎&apos;, &apos;新&apos;, &apos;老师&apos;, &apos;生前&apos;, &apos;来&apos;, &apos;就餐&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;欢&apos;, &apos;迎新&apos;, &apos;老&apos;, &apos;师生&apos;, &apos;前来&apos;, &apos;就餐&apos;]&lt;/td&gt;
&lt;td&gt;[&apos;欢&apos;, &apos;迎新&apos;, &apos;老&apos;, &apos;师生&apos;, &apos;前来&apos;, &apos;就餐&apos;]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;当某个文本拥有类似的特征时，也就能类似的认为，该文本具有相同的结论——比如都是垃圾邮件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.2.2 编码实现&lt;/h3&gt;
&lt;p&gt;代码步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、读取中文停用词表，构造一个过滤字符的字符串数组；&lt;/li&gt;
&lt;li&gt;2、读取某个文件，排除文件的非中文部分；&lt;/li&gt;
&lt;li&gt;3、对中文文本进行分词，得到一个字符串数组（这就是描述该文件的特征）；&lt;/li&gt;
&lt;li&gt;4、生成一个新的文件，存储特征字符串；&lt;/li&gt;
&lt;li&gt;5、重复 2 ～ 4 步骤，得到全部文件的特征文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者原本打算使用 python 代码来表示，但是&amp;lt;a href=&quot;/snofly-blog/files/nlp/pythonCode.zip&quot; target=&quot;_blank&quot;&amp;gt;源代码&amp;lt;/a&amp;gt;是 2019 年写，既没有输出中间结果(特征放内存里面了)，又多了模型评估的部分，不太适合本文的节奏，所以现在改用 nodejs 重写一遍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 data 文件夹的同级目录下创建 vector 文件夹，并在其中创建 spam 和 normal 文件夹。&lt;/p&gt;
&lt;p&gt;在 data 文件夹的同级目录下创建 vector.js 文件。&lt;/p&gt;
&lt;p&gt;此时的文件结构是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  emailCode tree -L 2
.
├── data
│   ├── normal
│   ├── spam
│   ├── test
│   └── 中文停用词表.txt
├── vector
│   ├── normal
│   └── spam
└── vector.js

8 directories, 2 files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 vector.js 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vector.js
const fs = require(&quot;fs&quot;);
const iconv = require(&quot;iconv-lite&quot;);
const path = require(&quot;path&quot;);
const nodejieba = require(&quot;nodejieba&quot;);

function filterNonChinese(str) {
  return str.replace(/[^\u4e00-\u9fa5]/g, &quot;&quot;);
}

function stringToVector(fileContent) {
  // 构造一个分割字符的字符串数组
  const buffer = fs.readFileSync(&quot;./data/中文停用词表.txt&quot;);
  const fileText = iconv.decode(buffer, &quot;gbk&quot;);
  const stpoWords = fileText.split(&quot;\n&quot;);

  // 匹配非中文字符并删除
  const fileChineseContent = filterNonChinese(fileContent);
  // 根据 nodejieba 进行分词
  const tokens = nodejieba.cut(fileChineseContent);
  // 根据停用词表进行过滤
  const filteredData = tokens.filter((item) =&amp;gt; !stpoWords.includes(item));
  return filteredData;
}

function generateVector(sourceDirPath, outDirPath, stpoWords) {
  const files = fs.readdirSync(sourceDirPath);
  // 遍历每个文件并读取它们的内容

  files.forEach((file, index) =&amp;gt; {
    const filePath = path.join(sourceDirPath, file);
    // 读取文件内容
    const buffer = fs.readFileSync(filePath);
    const fileContent = iconv.decode(buffer, &quot;gbk&quot;);
    // 获得特征向量
    const data = stringToVector(fileContent);
    // 将结果写入文件中保存
    fs.writeFileSync(path.join(outDirPath, file), data.join(&quot;\n&quot;), &quot;utf8&quot;);
    console.log(`处理进度：${index + 1}/${files.length}`);
  });
}

function main(params) {
  // 开始分割，输出在 vector 中
  generateVector(&quot;./data/normal&quot;, &quot;./vector/normal&quot;);
  generateVector(&quot;./data/spam&quot;, &quot;./vector/spam&quot;);
}

// 执行一次这个
// main();

module.exports = {
  stringToVector,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2.3 结果展示&lt;/h3&gt;
&lt;p&gt;经过处理之后，以 2.2.1 节中的两个文件为例，展示一下输出结果：&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;vector/normal 文件 274.txt&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  &quot;大三&quot;,
  &quot;恋情&quot;,
  &quot;维系&quot;,
  &quot;三年&quot;,
  &quot;越来越&quot;,
  &quot;知道&quot;,
  &quot;坚持&quot;,
  &quot;坚信&quot;,
  &quot;最&quot;,
  &quot;适合&quot;,
  &quot;人&quot;,
  &quot;一起&quot;,
  &quot;快乐&quot;,
  &quot;开学&quot;,
  &quot;分开&quot;,
  &quot;距离&quot;,
  &quot;拉开&quot;,
  &quot;美&quot;,
  &quot;相信&quot;,
  &quot;地久天长&quot;,
  &quot;放不下&quot;,
  &quot;身为&quot;,
  &quot;射手&quot;,
  &quot;女生&quot;,
  &quot;是不是&quot;,
  &quot;有点&quot;,
  &quot;名不副实&quot;,
  &quot;吵架&quot;,
  &quot;时间&quot;,
  &quot;居多&quot;,
  &quot;怀疑&quot;,
  &quot;是否&quot;,
  &quot;相爱&quot;,
  &quot;是否&quot;,
  &quot;在乎&quot;,
  &quot;对方&quot;,
  &quot;有时候&quot;,
  &quot;嘲笑&quot;,
  &quot;幼稚&quot;,
  &quot;脾气&quot;,
  &quot;忍不住&quot;,
  &quot;伤害&quot;,
  &quot;有时候&quot;,
  &quot;佩服&quot;,
  &quot;这么久&quot;,
  &quot;身边&quot;,
  &quot;出现&quot;,
  &quot;一些&quot;,
  &quot;人&quot;,
  &quot;最终&quot;,
  &quot;选择&quot;,
  &quot;一份&quot;,
  &quot;执着&quot;,
  &quot;天天&quot;,
  &quot;一起&quot;,
  &quot;恋人们&quot;,
  &quot;真的&quot;,
  &quot;幸福&quot;,
  &quot;珍惜&quot;,
  &quot;坚持&quot;,
  &quot;单身&quot;,
  &quot;恋人&quot;,
  &quot;想&quot;,
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;vector/spam 文件 793.txt&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  &quot;广州市&quot;,
  &quot;兴&quot;,
  &quot;贸易&quot;,
  &quot;有限公司&quot;,
  &quot;负责人&quot;,
  &quot;经理&quot;,
  &quot;财务&quot;,
  &quot;您好&quot;,
  &quot;公司&quot;,
  &quot;代理&quot;,
  &quot;多家&quot;,
  &quot;公司&quot;,
  &quot;操作&quot;,
  &quot;帐务&quot;,
  &quot;具有&quot;,
  &quot;丰富&quot;,
  &quot;财务&quot;,
  &quot;经验&quot;,
  &quot;贵&quot;,
  &quot;司&quot;,
  &quot;提供&quot;,
  &quot;普通&quot;,
  &quot;销售&quot;,
  &quot;服务行业&quot;,
  &quot;广告行业&quot;,
  &quot;建筑行业&quot;,
  &quot;运输&quot;,
  &quot;行业&quot;,
  &quot;发票&quot;,
  &quot;只&quot;,
  &quot;收&quot;,
  &quot;税&quot;,
  &quot;点&quot;,
  &quot;有意&quot;,
  &quot;请&quot;,
  &quot;联系&quot;,
  &quot;联系人&quot;,
  &quot;林海&quot;,
  &quot;联系电话&quot;,
  &quot;承诺&quot;,
  &quot;发票&quot;,
  &quot;一律&quot;,
  &quot;正规&quot;,
  &quot;手续&quot;,
  &quot;税务局&quot;,
  &quot;办理&quot;,
  &quot;领购&quot;,
  &quot;保证&quot;,
  &quot;随时&quot;,
  &quot;查询&quot;,
  &quot;验证&quot;,
  &quot;邮件&quot;,
  &quot;属&quot;,
  &quot;群&quot;,
  &quot;发邮件&quot;,
  &quot;造成&quot;,
  &quot;不便&quot;,
  &quot;敬请原谅&quot;,
  &quot;欢迎&quot;,
  &quot;来电&quot;,
  &quot;咨询&quot;,
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;2.3 模型训练&lt;/h2&gt;
&lt;h3&gt;2.3.1 算法原理回顾&lt;/h3&gt;
&lt;p&gt;回顾 2.2.3 节中的特征，可以得到一个初步结论：当某个邮件中中，大量含有 spam 中的词语，那么可以认为这个邮件大概率是垃圾邮件；反之同理。&lt;/p&gt;
&lt;p&gt;这里引出的一个问题是：这个“概率”该怎么去量化？&lt;/p&gt;
&lt;p&gt;我们很自然的想到，邮件中的词语对结果影响的权重是不同的，也就是影响的力度和方向是不同的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者：回顾“向量”的定义，同时具有方向和大小的量。在这个场景中，方向只有两个：是或不是垃圾邮件；而大小则表示对结果影响的权重。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;进而想到，我们需要计算&lt;strong&gt;特征向量&lt;/strong&gt;的具体值，从而能够判断输入的邮件最终具有怎样的结果倾向。&lt;/p&gt;
&lt;p&gt;那么计算方法是什么？&lt;/p&gt;
&lt;p&gt;再回顾 1.1~1.3 节中关于贝叶斯定理的描述。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们能够基于已有的事实反过来推测它发生的概率：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
P(A \mid B) = \frac{P(B \mid A) P(A)}{P(B)}
$$&lt;/p&gt;
&lt;p&gt;这里直接变换为：&lt;/p&gt;
&lt;p&gt;$$
P(是 \mid 特征1) = \frac{P(特征1 \mid 是) P(是)}{P(特征1)} \
P(是 \mid 特征2) = \frac{P(特征2 \mid 是) P(是)}{P(特征2)} \
... \
... \
... \
P(是 \mid 特征n) = \frac{P(特征n \mid 是) P(是)}{P(特征n)} \
$$&lt;/p&gt;
&lt;p&gt;于是我们需要计算等式右侧的变量。&lt;/p&gt;
&lt;p&gt;再回顾 2 节介绍部分，在 NLP 领域有两个研究方法：基于规则的方法和基于统计的方法。&lt;/p&gt;
&lt;p&gt;我们选择&lt;strong&gt;基于统计&lt;/strong&gt;的方法，于是能够“通过频率来模拟概率”。&lt;/p&gt;
&lt;p&gt;例如 P(特征 1 | 是) 的值，可以这样假设：&lt;/p&gt;
&lt;p&gt;垃圾邮件中，假设全部特征词语有 m 个，特征 1 这个词语有 n 个，也就是出现了 n 次，那么它出现的频率就是 n/m。&lt;/p&gt;
&lt;p&gt;所以 P(特征 1 | 是) = n/m 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据量越丰富，这个值也就越准确。从这里也可以感受一下大数据的含义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.3.2 算法实现&lt;/h3&gt;
&lt;p&gt;在 data 文件夹的同级目录下创建 train.js 文件。&lt;/p&gt;
&lt;p&gt;此时的文件结构是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  emailCode tree -L 2
.
├── data
│   ├── normal
│   ├── spam
│   ├── test
│   └── 中文停用词表.txt
├── train.js
├── vector
│   ├── normal
│   └── spam
└── vector.js

8 directories, 3 files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 train.js 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// train.js
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);
const { stringToVector } = require(&quot;./vector&quot;);
const iconv = require(&quot;iconv-lite&quot;);

/**
 *
 * @param {string} sourceDirPath
 * @param {Map} map
 * @returns
 */
function addMap(sourceDirPath, map) {
  let count = 0;
  const files = fs.readdirSync(sourceDirPath);
  files.forEach((file) =&amp;gt; {
    const filePath = path.join(sourceDirPath, file);
    // 读取文件内容
    const fileContent = fs.readFileSync(filePath, &quot;utf-8&quot;);
    const words = fileContent.split(&quot;\n&quot;);
    count += words.length;
    words.map((word) =&amp;gt; {
      map.set(word, (map.get(word) ?? 0) + 1);
    });
  });
  return count;
}

// 1、统计得到每个词语出现的次数、总词语个数
const normalWords = new Map();
const spamWords = new Map();
const normalWordsCount = addMap(&quot;./vector/normal&quot;, normalWords);
const spamWordsCount = addMap(&quot;./vector/spam&quot;, spamWords);

// 2、朴素贝叶斯算法——计算概率值
function calProb(word) {
  if (!normalWords.has(word) &amp;amp;&amp;amp; !spamWords.has(word)) {
    return 0.47;
  }
  const pw_n = normalWords.has(word)
    ? normalWords.get(word) / normalWordsCount
    : 0.01;
  const pw_s = spamWords.has(word)
    ? spamWords.get(word) / spamWordsCount
    : 0.01;
  ps_w = pw_s / (pw_s + pw_n);
  return ps_w;
}

function calBayes(testVector) {
  let ps_w = 1;
  let ps_n = 1;
  testVector.forEach((word) =&amp;gt; {
    const prob = calProb(word);
    ps_w *= prob;
    ps_n *= 1 - prob;
  });
  const p = ps_w / (ps_w + ps_n);
  return p;
}

// 3、简单测试
const testVector1 = stringToVector(
  iconv.decode(fs.readFileSync(&quot;./data/normal/274.txt&quot;), &quot;gbk&quot;)
);
const testVector2 = stringToVector(
  iconv.decode(fs.readFileSync(&quot;./data/spam/793.txt&quot;), &quot;gbk&quot;)
);
const testVector3 = stringToVector(
  iconv.decode(fs.readFileSync(&quot;./data/test/1.txt&quot;), &quot;gbk&quot;)
);
const testVector4 = stringToVector(
  iconv.decode(fs.readFileSync(&quot;./data/test/8000.txt&quot;), &quot;gbk&quot;)
);

console.log(calBayes(testVector1.slice(0, 15))); // 0.00327150967855315
console.log(calBayes(testVector2.slice(0, 15))); // 1
console.log(calBayes(testVector3.slice(0, 15))); // 9.756882688122813e-9
console.log(calBayes(testVector4.slice(0, 15))); // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3.3 代码解释&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;函数 calProb&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传入参数是一个词语，返回值是一个概率值，表示“这个词语存在时，该邮件更倾向于垃圾邮件，或者更倾向于正常邮件”。&lt;/p&gt;
&lt;p&gt;pw_n 表示该词在所有 normal 邮件中出现的频率，如果不存在，设置经验值 0.01；
pw_s 表示该词在所有 spam 邮件中出现的频率，如果不存在，设置经验值 0.01；
ps_w 表示了一个倾向，经验值为 0.47；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数 calBayes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传入值是需要判断的邮件的特征向量，返回值是一个概率值，表示“综合来看，该邮件是垃圾邮件的概率”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试代码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为数字计算精度问题，所以只取了一部分看看效果。&lt;/p&gt;
&lt;h1&gt;3 总结&lt;/h1&gt;
&lt;p&gt;好啦，到这里告一段落，可以简单总结下。&lt;/p&gt;
&lt;p&gt;回顾本文目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 理解&lt;strong&gt;朴素贝叶斯算法&lt;/strong&gt;原理&lt;/li&gt;
&lt;li&gt;2 了解&lt;strong&gt;基于统计的自然语言处理&lt;/strong&gt;的原理&lt;/li&gt;
&lt;li&gt;3 理解&lt;strong&gt;机器学习模型&lt;/strong&gt;的原理&lt;/li&gt;
&lt;li&gt;4 成功完成训练&lt;strong&gt;机器学习模型&lt;/strong&gt;的实践&lt;/li&gt;
&lt;li&gt;5 理解并运用&lt;strong&gt;模型评价指标&lt;/strong&gt;(略)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主要内容都已经涉及到了。第 5 部分，可以使用 data 中 test 的数据去测试，它们的文件名称就是标记。&lt;/p&gt;
&lt;p&gt;我们可以看到，算法实现部分的代码是相对很少的，更多的是对数据的处理，构造更好的输入。这也侧面说明了，很多人选择使用 python 的原因就是它有非常多好用的数据处理的库。为了说明朴素贝叶斯算法的作用，我们就没有直接调用“贝叶斯库”这样的 API 。&lt;/p&gt;
&lt;p&gt;于是，我们可以基于现有的资料，向上包装一些业务代码，构建一个可执行文件。它的输入是邮件文本信息，输出是一个概率值。&lt;/p&gt;
&lt;p&gt;最后，我们可以得到一个简单的二分类器（判断是或否），也就是一个机器学习模型了。&lt;/p&gt;
</content:encoded></item><item><title>Python 基本语法</title><link>https://0110wdj.github.io/snofly-blog/posts/python/python-basic/imdex/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/python/python-basic/imdex/</guid><pubDate>Wed, 29 May 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言随笔&lt;/h1&gt;
&lt;p&gt;2019/5/29/20:25 1.做垃圾邮件分类，老师推荐语言是 Python, 因为书写简单。我们就决定先找资料学会 Python 基本语法，然后学习相关库使用，这里结合学习算法思想，最后综合起来，贯通知识。 2.预期最后效果，小组每人都能独立完成分类器设计与实现。三人分别用三种模型训练，比较效果，有能力改进最好。 3.学习步骤，一，Python 基本语法学习。学习资料目前有：中国大学 mooc《Python 语言程序设计》北京理工大学。&lt;/p&gt;
&lt;p&gt;2019/7/6
经过昨天的努力，找到了路径去完成，我原计划明天出结果来着，老师要求今天就出结果，啊，想想就好累。&lt;/p&gt;
&lt;p&gt;2022/7/28
当时做完之后好累的，然后文章就没有更新了。
关于数据集、算法、模型和评价报告，资料都保留了的，以后有机会做个机器学习的入门教程吧。
本文标题也从“综合实训--垃圾邮件分类”改为“Python 基本语法”了。&lt;/p&gt;
&lt;h1&gt;Python 语言学习笔记&lt;/h1&gt;
&lt;h2&gt;Python 基础认识&lt;/h2&gt;
&lt;p&gt;==编译==：
将源代码一次性转换成目标代码的过程；
一次性翻译，之后不再需要源代码（类似英文翻译）。
==解释==：
将源代码逐条转换成目标代码同时逐条运行的过程；
每次程序运行时随翻译随执行（类似实时的同声传译）。&lt;/p&gt;
&lt;p&gt;执行编译过程的程序叫作==编译器==。
执行解释过程的程序叫作==解释器==。&lt;/p&gt;
&lt;p&gt;==静态语言==：使用编译执行的编程语言
C/C++语言、Java 语言
优势：编译器一次性生成目标代码，优化更充分，程序运行速度更快。
==脚本语言==：使用解释执行的编程语言
&lt;em&gt;Python 语言&lt;/em&gt;、JavaScript 语言、PHP 语言
优势：执行程序时需要源代码，维护更灵活；源代码可以跨多个操作系统平台运行。&lt;/p&gt;
&lt;p&gt;Python 的两种编程方式
==交互式==：对每个输入语句即时运行结果，适合语法练习
==文件式==：批量执行一组语句并运行结果，编程的主要方式&lt;/p&gt;
&lt;h2&gt;Python 语法元素概述&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;程序的格式框架&lt;/strong&gt;
==缩进==
类似 C 语言中的{}作用，表示一个代码块。
人为约定（或规定）缩进格式，一般为一个 Tab。
==注释==
单行注释：以#开头，其后内容为注释。
多行注释：以&apos;&apos;&apos;(三个单引号)开头和结尾。
&lt;strong&gt;命名与保留字&lt;/strong&gt;
变量采用标识符(名字) 来表示，关联标识符的过程叫==命名==。
命名规则与 C 语言类似，但是不用定义，直接用。
==保留字==：类似 C 语言中的关键字。
（红色表示更有深度）
&lt;img src=&quot;9dd8e0c257ba78efb3d13a220eff9568.png&quot; alt=&quot;26/33&quot; /&gt;
&lt;strong&gt;数据类型&lt;/strong&gt;
数据类型：字符串、整数、浮点数、列表。
&lt;strong&gt;语句与函数&lt;/strong&gt;
赋值语句 分支语句 函数
&lt;strong&gt;Python 程序的输入输出&lt;/strong&gt;
==输入函数 input()==使用格式：&amp;lt;变量&amp;gt; = input(&amp;lt;提示信息字符串&amp;gt;)
==输出函数 print()==：以字符形式向控制台输出结果的函数。
==评估函数 eval()==：去掉参数最外侧引号并执行余下语句的函数。&lt;/p&gt;
&lt;h2&gt;Python 数值数据类型&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;整数类型&lt;/strong&gt; 1.可正可负，没有取值范围限制；
2.pow(x,y)函数：计算 x^y^，想算多大算多大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;浮点数类型&lt;/strong&gt; 1.浮点数间运算存在不确定尾数，不是 bug； 2.取值范围数量级约-10307 至 10308，精度数量级 10-16。 3.科学计数法 &amp;lt;a&amp;gt;e&amp;lt;b&amp;gt; 表示 a*10^b^
4.round(x, d)：对 x 四舍五入，d 是小数截取位数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复数类型&lt;/strong&gt;
……&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数值运算操作符&lt;/strong&gt;
&lt;img src=&quot;ac4198eec9c63b568317fb2342f3a4d4.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
增强操作符及使用
用 x op= y 来代替 x = x op y ；可以少写一个 x 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数值运算函数&lt;/strong&gt;
==abs(x)==
绝对值，x 的绝对值；
==divmod(x,y)==
商余，(x//y, x%y)，同时输出商和余数；
==pow(x, y[, z])==
幂余，(x**y)%z，[..]表示参数 z 可省略
==round(x[, d])==
四舍五入，d 是保留小数位数，默认值为 0；
==max(x1,x2, … ,xn)==
最大值，返回 x1,x2, … ,xn 中的最大值，n 不限
==min(x1,x2, … ,xn)==
最大值，返回 x1,x2, … ,xn 中的最小值，n 不限
==int(x)==
将 x 变成整数，舍弃小数部分
==float(x)==
将 x 变成浮点数，增加小数部分
==complex(x)==
将 x 变成复数，增加虚数部分&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关系&lt;/strong&gt;
三种类型存在一种逐渐&quot;扩展&quot;或&quot;变宽&quot;的关系：
整数 ---&amp;gt; 浮点数 ---&amp;gt; 复数&lt;/p&gt;
&lt;h2&gt;Python 字符串数据类型&lt;/h2&gt;
&lt;p&gt;==字符串==
由 0 个或多个字符组成的有序字符序列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串表示方法&lt;/strong&gt;：
由一对单引号或双引号表示，仅表示单行字符串；
由一对三单引号或三双引号表示，可表示多行字符串。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串序号&lt;/strong&gt;
正向递增序号
0，1，2，3，，，N
反向递减序号 -（N+1），，，-4，-3，-2，-1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串使用&lt;/strong&gt;
索引：返回字符串中单个字符 &amp;lt;字符串&amp;gt;[M]
切片：返回字符串中一段字符子串 &amp;lt;字符串&amp;gt;[M: N]
高级：
&amp;lt;字符串&amp;gt;[M: N]，M 缺失表示至开头，N 缺失表示至结尾；
&amp;lt;字符串&amp;gt;[M: N: K]，根据步长 K 对字符串切片。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串的特殊字符&lt;/strong&gt;
转义符 \&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串操作符&lt;/strong&gt;
&lt;img src=&quot;94932c799fe75b9e0bdc6212f4d22abc.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;字符串处理函数&lt;/strong&gt;
&lt;img src=&quot;2ac0f5434ed50ee89673a1a041c0ed5d.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;3da79a32f21c0a80807090e2e25fad00.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;字符串处理方法&lt;/strong&gt;&lt;img src=&quot;c000962ccc7c460bdfd3dc6bab8c69b3.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;25f1861fb34ef39450551381f01c11b0.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;52c29f2280165f88693e9675b60c4b06.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串类型的格式化&lt;/strong&gt;
&amp;lt;模板字符串&amp;gt;.format(&amp;lt;逗号分隔的参数&amp;gt;)&lt;/p&gt;
&lt;p&gt;槽的概念：（镶嵌对应内容）
&lt;img src=&quot;6c008f17a774a6259a5db381bf686508.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
更精确的控制：
&lt;img src=&quot;156dce5d66c5b7f14103e767f7cc39f0.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Python 控制结构&lt;/h2&gt;
&lt;p&gt;==顺序结构==&lt;/p&gt;
&lt;p&gt;==分支结构==&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单分支结构
if &amp;lt;条件&amp;gt; : &amp;lt;语句块&amp;gt;&lt;/li&gt;
&lt;li&gt;二分支结构
一般形式
if &amp;lt;条件&amp;gt; : &amp;lt;语句块 1&amp;gt;
else :&amp;lt;语句块 2&amp;gt;
紧凑形式
&amp;lt;表达式 1&amp;gt; if &amp;lt;条件&amp;gt; else &amp;lt;表达式 2&amp;gt;&lt;/li&gt;
&lt;li&gt;多分支结构
if &amp;lt;条件 1&amp;gt; : &amp;lt;语句块 1&amp;gt;
elif &amp;lt;条件 2&amp;gt; : &amp;lt;语句块 2&amp;gt;
……
else :&amp;lt;语句块 N&amp;gt;&lt;/li&gt;
&lt;li&gt;条件判断及组合
条件判断
小于，小于等于，大于等于，大于，等于，不等于。
条件组合
逻辑与，逻辑或，逻辑非。and,or,not.&lt;/li&gt;
&lt;li&gt;程序的异常处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;try :&amp;lt;语句块 1&amp;gt;
except : &amp;lt;语句块 2&amp;gt;&lt;/p&gt;
&lt;p&gt;try :&amp;lt;语句块 1&amp;gt;
except &amp;lt;异常类型&amp;gt; : &amp;lt;语句块 2&amp;gt;
（只响应对应类型异常）&lt;/p&gt;
&lt;p&gt;try :
&amp;lt;语句块 1&amp;gt;
except :
&amp;lt;语句块 2&amp;gt;
else :
&amp;lt;语句块 3&amp;gt;
finally :
&amp;lt;语句块 4&amp;gt;
（ finally 对应语句块 4 一定执行）
（else 对应语句块 3 在不发生异常时执行）&lt;/p&gt;
&lt;p&gt;==循环结构==&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遍历循环&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;for &amp;lt;循环变量&amp;gt; in &amp;lt;遍历结构&amp;gt; : &amp;lt;语句块&amp;gt;
（每次循环，所获得元素放入循环变量，并执行一次语句块）&lt;/p&gt;
&lt;p&gt;for i in range(N) : &amp;lt;语句块&amp;gt;
（遍历由 range()函数产生的数字序列，产生循环）&lt;/p&gt;
&lt;p&gt;for i in range(M,N,K) : &amp;lt;语句块&amp;gt;
（M，N 是截取始尾，K 是截取步长。）&lt;/p&gt;
&lt;p&gt;应用：
计数循环(N 次)
计数循环(特定次)
字符串遍历循环
列表遍历循环
文件遍历循环
等等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无限循环
while &amp;lt;条件&amp;gt; : &amp;lt;语句块&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;循环控制保留字
==break==跳出并结束当前整个循环，执行循环后的语句
==continue==结束当次循环，继续执行后续次数循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;循环的高级用法
==循环与 else==
当循环没有被 break 语句退出时，执行 else 语句块
else 语句块作为&quot;正常&quot;完成循环的奖励
这里 else 的用法与异常处理中 else 用法相似&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Python 函数使用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;函数的理解与==定义==&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;def &amp;lt;函数名&amp;gt;(&amp;lt;参数(0 个或多个)&amp;gt;) :
&amp;lt;函数体&amp;gt;
return &amp;lt;返回值&amp;gt;
（函数定义时，所指定的参数是一种占位符）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数的==参数传递==&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;def &amp;lt;函数名&amp;gt;() : &amp;lt;函数体&amp;gt;
return &amp;lt;返回值&amp;gt;
（函数可以有参数，也可以没有，但必须保留括号）&lt;/p&gt;
&lt;p&gt;def &amp;lt;函数名&amp;gt;(&amp;lt;非可选参数&amp;gt;, &amp;lt;可选参数&amp;gt;) : &amp;lt;函数体&amp;gt;
return &amp;lt;返回值&amp;gt;
（函数定义时可以为某些参数指定默认值，构成可选参数）&lt;/p&gt;
&lt;p&gt;def &amp;lt;函数名&amp;gt;(&amp;lt;参数&amp;gt;, *b ) : &amp;lt;函数体&amp;gt;
return &amp;lt;返回值&amp;gt;
（函数定义时可以设计可变数量参数，既不确定参数总数量）&lt;/p&gt;
&lt;p&gt;def fact(n, m) :
fact( 10,5 )
fact( m=5,n=10 )
（函数调用时，参数可以按照位置或名称方式传递）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数的==返回值==&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数可以有返回值，也可以没有，可以有 return，也可以没有；
return 可以传递 0 个返回值，也可以传递任意多个返回值。（元组类型。）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;局部变量和全局变量
可以使用 global 保留字在函数内部使用全局变量；
局部变量为组合数据类型且未创建，等同于全局变量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;==lambda 函数==
&amp;lt;函数名&amp;gt; = lambda &amp;lt;参数&amp;gt;: &amp;lt;表达式&amp;gt;
lambda 函数是一种匿名函数，即没有名字的函数；
使用 lambda 保留字定义，函数名是返回结果；
lambda 函数用于定义简单的、能够在一行内表示的函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Python 组合数据类型&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;集合类型及操作&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集合类型定义
集合用大括号 {} 表示，元素间用逗号分隔
建立集合类型用 {} 或 set()
建立空集合类型，必须使用 set()&lt;/li&gt;
&lt;li&gt;集合操作符
&lt;img src=&quot;fd365b01b61da0e18da130875c536763.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/li&gt;
&lt;li&gt;集合处理方法
&lt;img src=&quot;329a3fee2804deb82b8e3660581ac56f.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;43d8c427b216c921cc9f36431f8871b3.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/li&gt;
&lt;li&gt;集合类型应用场景
包含关系比较
数据去重：集合类型所有元素无重复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;序列类型及操作&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;序列类型定义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;序列是具有先后关系的一组元素
序列是一维元素向量，==元素类型可以不同==
类似数学元素序列： s0, s1, … , sn-1
元素间由序号引导，通过下标访问序列的特定元素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;序列处理函数及方法
&lt;img src=&quot;bb44718ee8398a7bdba9bcb03d226754.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;0cc7d652210f7223f20a666ff0c45db1.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;元组类型及操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;元组是一种序列类型，一旦创建就不能被修改
使用小括号 () 或 tuple() 创建，元素间用逗号 , 分隔
可以使用或不使用小括号
def func(): return 1,2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列表类型及操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;列表是一种序列类型，创建后可以随意被修改
使用方括号 [] 或 list() 创建，元素间用逗号 , 分隔
列表中各元素类型可以不同，无长度限制
&lt;img src=&quot;952cc08b681c627ecc2f686a98cb40fa.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;03dbe7455c0e2b34d765a9f5706077d8.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;序列类型应用场景
数据表示：元组 和 列表&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Python 字典类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;字典类型定义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;==映射==是一种键(索引)和值(数据)的对应
==键值对==：键是数据索引的扩展
字典是键值对的集合，键值对之间无序&lt;/p&gt;
&lt;p&gt;采用大括号{}和 dict()创建，键值对用冒号: 表示
&amp;lt;字典变量&amp;gt; = {&amp;lt;键 1&amp;gt;:&amp;lt;值 1&amp;gt;, … , &amp;lt;键 n&amp;gt;:&amp;lt;值 n&amp;gt;}
&amp;lt;值&amp;gt; = &amp;lt;字典变量&amp;gt;[&amp;lt;键&amp;gt;]
&amp;lt;字典变量&amp;gt;[&amp;lt;键&amp;gt;] = &amp;lt;值&amp;gt;
[ ] 用来向字典变量中索引或增加元素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字典处理函数及方法
&lt;img src=&quot;6c80c64351494720143caf4141e8bbb5.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;a8e1d913bcebd5aba3bdf3a122bf5c02.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/li&gt;
&lt;li&gt;字典类型应用场景
统计数据出现的次数，数据是键，次数是值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Python 文件使用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;文件的类型&lt;/strong&gt;
文件是数据的抽象和集合
文件展现形态：文本文件和二进制文件&lt;/p&gt;
&lt;p&gt;==文本文件==
由单一特定编码组成的文件，如 UTF-8 编码
由于存在编码，也被看成是存储着的长字符串
适用于例如：.txt 文件、.py 文件等&lt;/p&gt;
&lt;p&gt;==二进制文件==
直接由比特 0 和 1 组成，没有统一字符编码
一般存在二进制 0 和 1 的组织结构，即文件格式
适用于例如：.png 文件、.avi 文件等&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件的打开和关闭&lt;/strong&gt;
文件处理的步骤: 打开-操作-关闭&lt;/p&gt;
&lt;p&gt;&amp;lt;变量名&amp;gt; = open(&amp;lt;文件名&amp;gt;, &amp;lt;打开模式&amp;gt;)
&amp;lt;变量名&amp;gt;.close()&lt;/p&gt;
&lt;p&gt;&amp;lt;变量名&amp;gt; ：文件句柄
&amp;lt;文件名&amp;gt; ：文件路径和名称（源文件同目录可省路径）
&amp;lt;打开模式&amp;gt; ：文本 or 二进制，读 or 写
&lt;img src=&quot;3c286c794a67012f6dac1be0830c19a0.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;文件内容的读取&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;f&amp;gt;.read(size=-1)
#读入全部内容，如果给出参数，读入前size长度
&amp;gt;&amp;gt;&amp;gt;s = f.read(2)

&amp;lt;f&amp;gt;.readline(size=-1)
#读入一行内容，如果给出参数，读入该行前size长度
&amp;gt;&amp;gt;&amp;gt;s = f.readline()

&amp;lt;f&amp;gt;.readlines(hint=-1)
#读入文件所有行，以每行为元素形成列表,如果给出参数，读入前hint行
&amp;gt;&amp;gt;&amp;gt;s = f.readlines()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;数据的文件写入&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;f&amp;gt;.write(s)
#向文件写入一个字符串或字节流
&amp;gt;&amp;gt;&amp;gt;f.write(&quot;中国是一个伟大的国家!&quot;)

&amp;lt;f&amp;gt;.writelines(lines)
#将一个元素全为字符串的列表写入文件
&amp;gt;&amp;gt;&amp;gt;ls = [&quot;中国&quot;, &quot;法国&quot;, &quot;美国&quot;]
&amp;gt;&amp;gt;&amp;gt;f.writelines(ls)
#中国法国美国

&amp;lt;f&amp;gt;.seek(offset)
#改变当前文件操作指针的位置，offset含义如下：
#0 – 文件开头； 1 – 当前位置； 2 – 文件结尾
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;涉及到的一些第三方库&lt;/h2&gt;
&lt;p&gt;==jieba 库==
原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用一个中文词库，确定中文字符之间的关联概率&lt;/li&gt;
&lt;li&gt;中文字符间概率大的组成词组，形成分词结果&lt;/li&gt;
&lt;li&gt;除了分词，用户还可以添加自定义的词组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;jieba 分词的三种模式
精确模式、全模式、搜索引擎模式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;精确模式：把文本精确的切分开，不存在冗余单词&lt;/li&gt;
&lt;li&gt;全模式：把文本中所有可能的词语都扫描出来，有冗余&lt;/li&gt;
&lt;li&gt;搜索引擎模式：在精确模式基础上，对长词再次切分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jieba.lcut(s)
#精确模式，返回一个列表类型的分词结果
&amp;gt;&amp;gt;&amp;gt;jieba.lcut(&quot;中国是一个伟大的国家&quot;)
[&apos;中国&apos;, &apos;是&apos;, &apos;一个&apos;, &apos;伟大&apos;, &apos;的&apos;, &apos;国家&apos;]

jieba.lcut(s,cut_all=True)
#全模式，返回一个列表类型的分词结果，存在冗余
&amp;gt;&amp;gt;&amp;gt;jieba.lcut(&quot;中国是一个伟大的国家&quot;,cut_all=True)
[&apos;中国&apos;, &apos;国是&apos;, &apos;一个&apos;, &apos;伟大&apos;, &apos;的&apos;, &apos;国家&apos;]

jieba.lcut_for_search(s)
#搜索引擎模式，返回一个列表类型的分词结果，存在冗余
&amp;gt;&amp;gt;&amp;gt;jieba.lcut_for_search(“中华人民共和国是伟大的&quot;)
[&apos;中华&apos;, &apos;华人&apos;, &apos;人民&apos;, &apos;共和&apos;, &apos;共和国&apos;, &apos;中华人民共和国&apos;, &apos;是&apos;, &apos;伟大&apos;, &apos;的&apos;]

jieba.add_word(w)
#向分词词典增加新词w
&amp;gt;&amp;gt;&amp;gt;jieba.add_word(&quot;蟒蛇语言&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;基于 NB 算法的中文垃圾邮件分类&lt;/h1&gt;
&lt;h1&gt;模型评估&lt;/h1&gt;
&lt;p&gt;混淆矩阵：
T/F(True/False)表示预测是否正确，P/N(Positive/Negative)表示 预测的 label 而不是实际的 label。&lt;/p&gt;
</content:encoded></item><item><title>React 设计原理导论</title><link>https://0110wdj.github.io/snofly-blog/posts/react/react-design/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/react/react-design/</guid><pubDate>Fri, 23 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;0 前言&lt;/h1&gt;
&lt;p&gt;本文适合具有 react 编码经验的读者，或者熟悉原生 js 的读者。&lt;/p&gt;
&lt;p&gt;本文基于 react 18 版本，以“函数组件 + hooks”的形式，介绍 react 的设计思路。&lt;/p&gt;
&lt;p&gt;本文是一个初步引导，更多详细的内容，可以参阅书籍《React 设计原理（卡颂）》，也可以进一步阅读源码。&lt;/p&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1、了解 react 整体设计思路&lt;/li&gt;
&lt;li&gt;2、了解主要模块的设计思路&lt;/li&gt;
&lt;li&gt;3、了解 react 源码结构&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终期望：读者能够深入对 react 设计的理解，并为后续学习打下基础。&lt;/p&gt;
&lt;h1&gt;1 正文&lt;/h1&gt;
&lt;h2&gt;1.1 react 整体设计思路&lt;/h2&gt;
&lt;h3&gt;1.1.1 简单的 jsx 代码 🌰&lt;/h3&gt;
&lt;p&gt;先看一段经典的计数器代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useState } from &quot;react&quot;;

const App = () =&amp;gt; {
  const [count, setCount] = useState(0);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Count: {count}&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，这会在页面上显示一个按钮，点击按钮后，数字会加 1。&lt;/p&gt;
&lt;p&gt;这里我们可能会回到的一些工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组件模块的导入导出&lt;/li&gt;
&lt;li&gt;jsx 语法到 js 语法到转换&lt;/li&gt;
&lt;li&gt;状态变量的定义与更新&lt;/li&gt;
&lt;li&gt;element 的生成、事件加载、dom 挂载、重新渲染&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，这个组件是能用了。&lt;/p&gt;
&lt;h3&gt;1.1.2 原生 js 代码 🌰&lt;/h3&gt;
&lt;p&gt;思考上面的计数器代码，如果用原生 js 代码实现，我们会怎么去实现？&lt;/p&gt;
&lt;p&gt;随即我们能够想到，类似这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const renderButton = () =&amp;gt; {
  let count = 0;
  const button = document.createElement(&quot;button&quot;);
  button.textContent = `Count: ${count}`;
  button.addEventListener(&quot;click&quot;, () =&amp;gt; {
    count++;
    button.textContent = `Count: ${count}`;
  });
};

const buttonElement = renderButton();
// appendChild(buttonElement)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本思路是这样的，在点击按钮后，修改按钮的 textContent，从而实现数字的变化。&lt;/p&gt;
&lt;p&gt;但是这里存在一些问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、业务逻辑和 dom 操作耦合在一起，无法复用。&lt;/li&gt;
&lt;li&gt;2、随着组件的规模的增加，代码会变得非常复杂，难以维护。&lt;/li&gt;
&lt;li&gt;3、组件间的状态管理和交互逻辑不易共享和复用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;显然，这样的代码在实际的工程项目中，是不可取的。&lt;/p&gt;
&lt;h3&gt;1.1.3 react 的工作&lt;/h3&gt;
&lt;p&gt;我们做这样的实验（可以不用实际编码）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1、基于 create-react-app 构建一个简单的 react 项目。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2、使用 1.1.1 中的计数器代码，引入到页面中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3、编译运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4、点击按钮，页面中数字变化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这个过程中，react 帮我们做了什么？&lt;/p&gt;
&lt;p&gt;这里需要分两种情况来讨论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行之前 —— 编码打包阶段&lt;/li&gt;
&lt;li&gt;运行时阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.1.3.1 运行之前&lt;/h4&gt;
&lt;p&gt;在运行之前，我们按照 react 的编码规范，完成 jsx 代码的编写（可以是 cra 工具生成的代码框架，也可以是手动修改的代码）。&lt;/p&gt;
&lt;p&gt;这时，&lt;strong&gt;react 提供了一种 JSX 规范&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;接下来，在打包流程中，使用 Babel 工具将 JSX 代码转换为 JavaScript 代码，然后就能在浏览器中运行。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 原始 JSX
&amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Click me&amp;lt;/button&amp;gt;;

// Babel 编译后（简化）
React.createElement(
  &quot;button&quot;,
  { onClick: () =&amp;gt; setCount(count + 1) },
  &quot;Click me&quot;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个转换过程中，由 &lt;strong&gt;react 负责定义规则&lt;/strong&gt;，并&lt;strong&gt;提供相关 API&lt;/strong&gt;，例如 React.createElement 。&lt;/p&gt;
&lt;h4&gt;1.1.3.2 运行时阶段&lt;/h4&gt;
&lt;p&gt;转换后的 js 代码就可以直接运行在浏览器的 js 引擎中了。&lt;/p&gt;
&lt;p&gt;这些 js 代码，除了业务相关代码，就是工具代码了，例如 react 、 lodash 等。&lt;/p&gt;
&lt;p&gt;react 的核心模块代码执行之后，一些函数和对象会被加载到内存里面，在运行时发挥重要作用。&lt;/p&gt;
&lt;p&gt;运行时分为两个时期来看待 react 的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、&lt;strong&gt;代码初始化时&lt;/strong&gt; —— 负责建立组件层级体系&lt;/li&gt;
&lt;li&gt;2、&lt;strong&gt;状态更新时&lt;/strong&gt; —— 负责响应交互，更新 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们常说的 react 设计原理，就是指这两个时期的代码逻辑。&lt;/p&gt;
&lt;p&gt;其中包括了我们常见的一些概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fiber&lt;/li&gt;
&lt;li&gt;hooks&lt;/li&gt;
&lt;li&gt;虚拟 DOM&lt;/li&gt;
&lt;li&gt;diff 算法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些概念设计，在后续章节中会介绍到。&lt;/p&gt;
&lt;h4&gt;1.1.3.3 初始化时期&lt;/h4&gt;
&lt;p&gt;浏览器下载 js 代码，在执行 react 的工具代码之后，且在在状态更新之前，这段时间，react 会进行一系列的初始化操作。&lt;/p&gt;
&lt;p&gt;这里有两个重要的变化：&lt;/p&gt;
&lt;p&gt;1、通过 ReactDom.render() 方法，将组件渲染到页面上。&lt;/p&gt;
&lt;p&gt;例如，将一个 App 挂载到指定的 root 元素上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;
const App = () =&amp;gt; {
  return &amp;lt;div&amp;gt;Hello World&amp;lt;/div&amp;gt;;
};
ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&quot;root&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 App 挂载之后，页面上就能看到 Hello World 了，开发者工具中 Element 里面也能看到这个 div 元素。&lt;/p&gt;
&lt;p&gt;2、在内存中加载了很多函数、对象、标记，等待之后使用。&lt;/p&gt;
&lt;p&gt;这里比较关键的就是 Fiber Node。&lt;/p&gt;
&lt;p&gt;此时，实际上一共会存在三个 App DOM 对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;其中一个是真实 DOM, 也就是页面中的 root 元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另外两个是虚拟 DOM (也称为 Fiber node), 其实本质是相似于真实 DOM 的两个 js 对象，它们只会存在于内存中参与计算。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在真实的 App DOM 生成的时候，会有很多事项要处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于 react 的事件加载机制，为其绑定事件。&lt;/li&gt;
&lt;li&gt;如果有状态变量的话，会基于 react 的 hooks 机制，为其生成状态变量。&lt;/li&gt;
&lt;li&gt;如果有副作用函数，需要订阅相关的状态变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至此，基本的 js 代码执行完成，页面进入一个待更新的状态。&lt;/p&gt;
&lt;h4&gt;1.1.3.4 更新时期&lt;/h4&gt;
&lt;p&gt;在状态更新时期， react 会不断推动工作流程的循环：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;触发任务——&amp;gt;计算更新——&amp;gt;更新 DOM——&amp;gt;触发任务——&amp;gt;计算更新——&amp;gt;更新 DOM——&amp;gt;循环...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以用户点击计数器按钮为例，触发任务的过程大致如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户点击按钮之后，触发事件&lt;/li&gt;
&lt;li&gt;触发 onClick 回调函数&lt;/li&gt;
&lt;li&gt;回调函数执行&lt;/li&gt;
&lt;li&gt;函数内执行到 setCount() 函数&lt;/li&gt;
&lt;li&gt;创建更新任务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到这里，点击事件的同步代码执行过程就结束了，后续是 react 的调度器的工作了。&lt;/p&gt;
&lt;p&gt;后续工作流程是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调度阶段：调度器，根据优先级给出这一帧要执行的任务，给到渲染中心。&lt;/li&gt;
&lt;li&gt;渲染阶段：渲染中心，根据当前的虚拟 DOM ，执行更新代码，得到下一帧要更新的虚拟 DOM 。&lt;/li&gt;
&lt;li&gt;提交阶段：切换两个虚拟 DOM，使用 diff 算法确定 UI 更新的内容，然后执行 UI 更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再然后就是循环了，等待下一次用户操作，或者执行其他低优先级的更新任务。&lt;/p&gt;
&lt;h3&gt;1.1.4 小结&lt;/h3&gt;
&lt;p&gt;react 的设计原理，就是指它在运行时是怎么发挥作用的，主要包括初始化和更新两个时期。&lt;/p&gt;
&lt;p&gt;我们了解了这些作用后，就能很好的理解我们写的计数器组件代码了。&lt;/p&gt;
&lt;p&gt;下面重点介绍&lt;strong&gt;运行时阶段&lt;/strong&gt;的&lt;strong&gt;状态更新时期&lt;/strong&gt;，结合实例，了解 react 在工作流程中的每一步做了什么。&lt;/p&gt;
&lt;p&gt;这是一个多模块协同的工作流程，数据在各个模块之间传递、共享。&lt;/p&gt;
&lt;h2&gt;1.2 初始化时期简介&lt;/h2&gt;
&lt;p&gt;js 中，一个工具函数被加载到调用栈中执行，它通常会做两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、进行一些运算；&lt;/li&gt;
&lt;li&gt;2、申请一些内存空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 react 的初始化时期，也相当于执行了一个函数，函数中同样会做这两件事。&lt;/p&gt;
&lt;p&gt;在申请到的内存空间中，会加载各种模块 —— 一些函数和变量，然后等待更新。&lt;/p&gt;
&lt;p&gt;在触发更新时期，这些模块和变量将发挥重要作用。&lt;/p&gt;
&lt;p&gt;后文我们以计数器点击过程为例，逐步讲解涉及到的模块，及其相关功能。&lt;/p&gt;
&lt;p&gt;我们会主要介绍这些模块的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调度中心&lt;/li&gt;
&lt;li&gt;渲染中心&lt;/li&gt;
&lt;li&gt;提交中心&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这之前，先简要介绍一些初始化过程中建立的一些变量，因为更新时期会用到它们。&lt;/p&gt;
&lt;h3&gt;1.2.1 状态注册&lt;/h3&gt;
&lt;p&gt;初始化的过程中，这句代码做了什么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [count, setCount] = useState(0);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;1、调用 useState(0)&lt;/li&gt;
&lt;li&gt;2、创建一个 Hook 节点&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const hook = {
  memoizedState: 0,     // 当前 state
  baseState: 0,         // 用于追踪上次的 state
  queue: {
    pending: null,      // 这里挂的是 Update 链表（setState 生成的 update 就在这里）
    dispatch: ...,      // 就是 setCount 本身
  },
  next: null            // 指向下一个 Hook（useState、useEffect 等）
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;3、把这个 Hook 节点加入当前函数组件对应的 fiber 的 Hook 链表中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 Hook 链表不存在，则会创建一个。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4、创建 setCount 函数（dispatch）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function dispatchSetState(action) {
  // 创建 update 对象
  const update = { action, lane, next };

  // 将 update 加入 queue.pending（环形链表）
  // 调用 scheduleUpdateOnFiber(fiber, lane)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面提到的 fiber 是一个树结构的对象，这里面包含了很多属性，其中就有一个 Hook 链表。&lt;/p&gt;
&lt;p&gt;fiber 在形式上，就是 DOM 节点在内存中的表示，也称为 Fiber Node，它和真实的 DOM 节点一一对应。&lt;/p&gt;
&lt;h3&gt;1.2.2 引申的问题：为什么不能在 if 中使用 hook&lt;/h3&gt;
&lt;p&gt;在函数组件中，不能使用这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [count, setCount] = useState(0);
if (condition) {
  const [num, setNum] = useState(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的代码会导致 Hook 链表的顺序不一致，从而导致状态管理的错误，所以 react 会直接报错。&lt;/p&gt;
&lt;p&gt;更深层的原因是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;useState 本身没有“名字”概念，React 不知道你用的是什么变量名；&lt;/li&gt;
&lt;li&gt;函数组件每次执行时会重新声明这些变量，变量名只是 JS 层的临时绑定；&lt;/li&gt;
&lt;li&gt;链表和顺序，可以准确找到对应的状态；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.3 更新时期的工作流程&lt;/h2&gt;
&lt;h3&gt;1.3.1 触发更新&lt;/h3&gt;
&lt;p&gt;用户点击按钮，触发事件。我们来看这个过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function onClick() {
  setCount(count + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中只有一句代码，调用了 setCount() 函数，这个函数是 react 的 hooks 机制提供的。&lt;/p&gt;
&lt;p&gt;源代码中的类型定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function useState&amp;lt;S&amp;gt;(
  initialState: S | (() =&amp;gt; S)
): [S, Dispatch&amp;lt;SetStateAction&amp;lt;S&amp;gt;&amp;gt;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从名称也可以看出，setCount 做的事情是发送(Dispatch)更新操作，而不是直接 change state。&lt;/p&gt;
&lt;p&gt;执行这个函数之后，本质上是做了这三件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个更新任务对象（Update）&lt;/li&gt;
&lt;li&gt;把任务放入组件的更新队列（Update Queue）&lt;/li&gt;
&lt;li&gt;交给调度器（Scheduler）安排“重新渲染一次组件”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以到这一步执行之后，内存中会多出一个 Update 对象，会由调度器去进一步处理。&lt;/p&gt;
&lt;p&gt;Update 对象的结构大概是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  action: newState,      // 或者是一个函数
  next: null,            // 链表结构中的下一个 update
  lane: 优先级标记,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面的 action 就是用于计算新的 state 的函数。&lt;/p&gt;
&lt;p&gt;调度时，如果存在多个更新，那么可能会进行合并。&lt;/p&gt;
&lt;p&gt;总之，如果有这样的操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setCount(1);
setCount((prev) =&amp;gt; prev + 2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么会产生这样的更新队列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Update1: {
  action: 1,
  next: Update2,
  lane: ...
}

Update2: {
  action: prev =&amp;gt; prev + 2,
  next: null,
  lane: ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个更新队列会被放在这个地方：&lt;/p&gt;
&lt;p&gt;当前函数组件所对应的 fiber 节点的 memoizedState 属性，所指向的 hook 链表中的第一个 hook 节点的 queue 属性中的 pending 数组中。&lt;/p&gt;
&lt;p&gt;举例说明：&lt;/p&gt;
&lt;p&gt;如果有这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function App() {
  const [count, setCount] = useState(0); // Hook 1
  const [name, setName] = useState(&quot;React&quot;); // Hook 2

  return (
    &amp;lt;button onClick={() =&amp;gt; setName(&quot;Vue&quot;)}&amp;gt;
      {count} - {name}
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在执行到两个 hook 声明时，fiber 中会产生这样的变化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fiber.memoizedState → Hook(count) → Hook(name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化了 Hook 1 和 Hook 2，然后执行了 onClick 函数，产生了更新任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fiber.memoizedState → Hook(count=0, queue=null)
                    → Hook(name=&quot;React&quot;, queue.pending=[update:&quot;Vue&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有新的 UpDate 对象产生，它会被插入到 queue.pending 中。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fiber.memoizedState → Hook(count=0, queue=null)
                    → Hook(name=&quot;React&quot;,
                        queue.pending=[
                          update:&quot;Vue&quot;，
                          update:&quot;js&quot;,
                          update:(pre)=&amp;gt;{pre+&quot;something&quot;}
                          ]
                        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，把更新任务交给调度器，等待执行进一步处理。&lt;/p&gt;
&lt;h3&gt;1.3.2 调度器&lt;/h3&gt;
&lt;p&gt;调度器的核心目标是：按照优先级合理安排组件重新渲染的时间和方式。&lt;/p&gt;
&lt;p&gt;如果把调度器理解为一个复杂函数，它的输入是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fiber&lt;/li&gt;
&lt;li&gt;Lane&lt;/li&gt;
&lt;li&gt;UpdateQueue&lt;/li&gt;
&lt;li&gt;当前调度状态&lt;/li&gt;
&lt;li&gt;当前时间戳&lt;/li&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它没有直接的输出，而是产生一些副作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标记组件为「有更新」&lt;/li&gt;
&lt;li&gt;计算优先级，注册调度任务&lt;/li&gt;
&lt;li&gt;启动渲染阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回到上面的一个例子，调度中心收到了更新队列，其中包含了两个 Update 对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UpdateQueue: Update1 -&amp;gt; Update2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面看看调度器具体的工作。&lt;/p&gt;
&lt;h4&gt;1.3.2.1 将组件标记为待更新状态&lt;/h4&gt;
&lt;p&gt;我们之前提到了，我们在内存中有两个对应于真实 dom 的虚拟 dom。&lt;/p&gt;
&lt;p&gt;这两个虚拟 dom 本质就是树结构的对象，我们也可以称其为两个 Fiber Node 。&lt;/p&gt;
&lt;p&gt;我们常说的 Fiber 架构，就是基于这种树结构的设计。&lt;/p&gt;
&lt;p&gt;首先，看第一个问题：为什么使用两个对象？&lt;/p&gt;
&lt;p&gt;答案是：参考双缓存机制，获得极大的性能优化。&lt;/p&gt;
&lt;p&gt;进一步，我们可以区分这两个对象：称一个为 current node，另一个为 workInProgress node。&lt;/p&gt;
&lt;p&gt;current node 对应于当前的真实 dom ，它作用有两个：
1、渲染阶段，基于它生成 workInProgress node；
2、和真实 dom 作 diff 计算。&lt;/p&gt;
&lt;p&gt;workInProgress node 是一个对象，react 将下一帧需要更新的内容，首先应用到这个对象上。&lt;/p&gt;
&lt;p&gt;需要实际应用更新的时候，切换一下这两个 node 即可。&lt;/p&gt;
&lt;p&gt;基于 Update 对象，调度器能够知道并标记需要更新的组件，然后会向上级组件递归传递，直到 root 节点。&lt;/p&gt;
&lt;h4&gt;1.3.2.2 计算优先级（Lane 模型）&lt;/h4&gt;
&lt;p&gt;我们知道，浏览器进程中的 js 线程和 UI 线程是互斥的。&lt;/p&gt;
&lt;p&gt;为了防止 js 线程阻塞 UI 线程，在任务量很大时，只能分批执行，于是用到了 react 的优先级系统。&lt;/p&gt;
&lt;p&gt;这里介绍一下 Lane 模型：它的本质是许多二进制数字的处理机制。&lt;/p&gt;
&lt;p&gt;包括两部分：1、优先级定义——数字的定义；2、合并批量处理——优先级逻辑操作。&lt;/p&gt;
&lt;p&gt;比如，不同的操作，优先级是不同的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const SyncLane = 0b0000000000000000000000000000001; // 同步优先级
const InputContinuousLane = 0b0000000000000000000000000000010; // 连续输入
const DefaultLane = 0b0000000000000000000000000000100; // 默认优先级
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  action: (count) =&amp;gt; count + 1,
  lane: SyncLane, // 代表是“同步更新”，比如点击事件触发的
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就是简单的标记了这个操作的优先级。&lt;/p&gt;
&lt;p&gt;回到举例中，调度器标记了更新状态后，还会进一步计算和标记优先级状态。&lt;/p&gt;
&lt;h4&gt;1.3.2.3 合并更新&lt;/h4&gt;
&lt;p&gt;如果存在这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setCount(1);
setCount(2);
setCount(3);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，在调度器中，会合并为一次更新操作，也就只会触发一次下一阶段的处理流程。&lt;/p&gt;
&lt;p&gt;更复杂的情况，setCount(p=&amp;gt;p+1)，则会有更多处理的逻辑，本文略过。&lt;/p&gt;
&lt;h4&gt;小结&lt;/h4&gt;
&lt;p&gt;至此，调度器的大概工作就介绍完成了。&lt;/p&gt;
&lt;p&gt;简单来说，调度器更新了 Fiber node，准备好了全部信息之后，等待下一阶段的执行。&lt;/p&gt;
&lt;p&gt;下一阶段，在 react 中称为 render 阶段，中文翻译为渲染阶段，这里的渲染和浏览器中基于 cssom 和 dom 生成渲染树的过程类似。&lt;/p&gt;
&lt;p&gt;注意：调度器在当前调度工作完成后，可能还有部分任务没有处理。这些任务，将会等待下一次调度的处理。&lt;/p&gt;
&lt;h3&gt;1.3.3 render 阶段&lt;/h3&gt;
&lt;p&gt;renderRoot 是一个纯计算过程，会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据旧的 fiber tree；&lt;/li&gt;
&lt;li&gt;按顺序遍历每个 Hook&lt;/li&gt;
&lt;li&gt;应用 queue.pending 中的 update，生成新的状态&lt;/li&gt;
&lt;li&gt;得出新的虚拟 DOM fiber 树（workInProgress node）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至此，调度器的任务完成了，后续就是渲染中心的事。&lt;/p&gt;
&lt;p&gt;注意：调度器代码执行期间，浏览器页面并不会发生变化，此时的操作都是内存中对象的修改。&lt;/p&gt;
&lt;h3&gt;1.3.4 commit 阶段&lt;/h3&gt;
&lt;p&gt;commit 阶段的整体目标是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将渲染阶段生成的 fiber 树（workInProgress）中的变更，真正「提交」到浏览器的真实 DOM 中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;渲染阶段只是「算出」要干什么&lt;/li&gt;
&lt;li&gt;提交阶段才「真正执行」这些事情：修改 DOM、触发副作用、调用生命周期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时，内存中存在两个 fiber 树，我们之前提到过，一个是 current node，一个是 workInProgress node。&lt;/p&gt;
&lt;p&gt;显然，current node 中的 dom 结构和真实 dom 一致，而 workInProgress node 中的 dom 结构就是下一帧会真实 DOM。&lt;/p&gt;
&lt;p&gt;commit 阶段会把 workInProgress node 和 current node 做 diff 计算，得到变化的部分。
然后，应用到真实 DOM 中，完成更新。&lt;/p&gt;
&lt;p&gt;之后会根据变化的部分，执行生命周期函数和副作用函数。&lt;/p&gt;
&lt;h4&gt;1.3.4.1 diff 算法&lt;/h4&gt;
&lt;p&gt;注意之前提到的标记，diff 时能够更具变化标记，支剪大量遍历工作。&lt;/p&gt;
&lt;p&gt;diff 算法会检查逐层变了 node，如果没有变化标记，就不再处理。&lt;/p&gt;
&lt;p&gt;如果需要修改，则会确定修改的方式：新增、删除、修改。&lt;/p&gt;
&lt;p&gt;在这个过程中，react 会根据 key 来选择复用某些节点，减少不必要的操作。&lt;/p&gt;
&lt;p&gt;这里有个常见的问题：为什么不能推荐使用数组的 index 来做 key ?&lt;/p&gt;
&lt;p&gt;答案是：React 在「列表渲染」中使用 key 来识别每个列表项，区分哪些元素被插入、删除、移动或复用。当数组发生变化（特别是中间插入/删除）时，会导致「错误的复用」，造成页面状态错乱！&lt;/p&gt;
&lt;h3&gt;1.3.5 小结&lt;/h3&gt;
&lt;p&gt;commit 之后，页面中的真实 DOM 就会被更新。&lt;/p&gt;
&lt;p&gt;这一帧的更新就完成了，之后会等待下一次更新。&lt;/p&gt;
&lt;p&gt;值得一提的是：虚拟 DOM 的设计，可以支持跨端。&lt;/p&gt;
&lt;p&gt;在 commit 阶段之前的工作，都是在内存中进行的，不涉及具体的环境 —— pc 浏览器或手机屏幕。&lt;/p&gt;
&lt;p&gt;于是，利用不同的 commit 工具（react native），既可以把 react 代码应用到其他终端上。&lt;/p&gt;
&lt;h2&gt;1.4 总结&lt;/h2&gt;
&lt;p&gt;react 设计原理的核心，是调度器、渲染中心和提交中心这三个核心模块的协同工作。&lt;/p&gt;
&lt;p&gt;调度器负责管理更新任务，渲染中心负责计算更新，提交中心负责更新 DOM。&lt;/p&gt;
&lt;p&gt;在这个过程中，react 会根据优先级，合并更新任务，提高更新效率。&lt;/p&gt;
&lt;p&gt;这就是 react 的设计原理，它是一个复杂的系统，需要不断地学习和探索。&lt;/p&gt;
&lt;h2&gt;1.5 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;《react 设计原理(卡颂)》&lt;/li&gt;
&lt;li&gt;React 官网&lt;/li&gt;
&lt;li&gt;chatGpt&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>浅谈 React 发展史</title><link>https://0110wdj.github.io/snofly-blog/posts/react/react-history/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/react/react-history/</guid><pubDate>Fri, 24 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;对了，事情的经过大概是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./showTime.jpg&quot; alt=&quot;showTime&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以，为了能够继续 &lt;s&gt;水群&lt;/s&gt; 助人为乐，需要整理一下 React 的发展史。&lt;/p&gt;
&lt;p&gt;下面从年份版本、背景故事、特性需求来讲讲 React 是怎么走到今天的。&lt;/p&gt;
&lt;h1&gt;React 编年史&lt;/h1&gt;
&lt;p&gt;最完整的历史，当然是在&lt;a href=&quot;https://github.com/facebook/react/&quot;&gt;源代码&lt;/a&gt;中查看提交记录，这里也有文档说明各个&lt;a href=&quot;https://github.com/facebook/react/releases&quot;&gt;释放版本&lt;/a&gt;的更新内容。&lt;/p&gt;
&lt;p&gt;下面整理一个表格，列出年份和版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 版本号                        | 版本更新时间   |
| ---------------------------- | ------------ |
| v0.3.0                       | Jul 03, 2013 |
| v0.3.3                       | Jul 03, 2013 |
| v0.4.0                       | Jul 20, 2013 |
| v0.4.1                       | Jul 30, 2013 |
| v0.5.0                       | Oct 20, 2013 |
| v0.5.1                       | Oct 30, 2013 |
| v0.4.2                       | Dec 20, 2013 |
| v0.5.2                       | Dec 20, 2013 |
| v0.8.0                       | Dec 20, 2013 |
| v0.9.0-rc1                   | Apr 19, 2015 |
| v0.9.0                       | Mar 05, 2014 |
| v0.10.0-rc1                  | Apr 19, 2015 |
| v0.10.0                      | Nov 21, 2014 |
| v0.11.0-rc1                  | Apr 19, 2015 |
| v0.11.0                      | Nov 21, 2014 |
| v0.11.1                      | Nov 21, 2014 |
| v0.11.2                      | Nov 21, 2014 |
| v0.12.0-rc1                  | Apr 19, 2015 |
| v0.12.0                      | Nov 21, 2014 |
| v0.12.1                      | Nov 21, 2014 |
| v0.12.2                      | Jan 06, 2015 |
| v0.13.0-rc1                  | Apr 19, 2015 |
| v0.13.0-rc2                  | Apr 19, 2015 |
| v0.13.0                      | Apr 19, 2015 |
| v0.13.1                      | Apr 19, 2015 |
| v0.13.2                      | Apr 19, 2015 |
| v0.13.3                      | Jun 18, 2015 |
| v0.14.0                      | Oct 09, 2015 |
| v0.14.1                      | Oct 29, 2015 |
| v0.14.2                      | Nov 03, 2015 |
| v0.14.3                      | Nov 25, 2015 |
| v0.14.4                      | Jan 07, 2016 |
| v0.14.5                      | Jan 07, 2016 |
| v0.14.6                      | Jan 07, 2016 |
| v0.14.7                      | Jan 29, 2016 |
| v0.14.8                      | Mar 30, 2016 |
| v15.0.0                      | Apr 09, 2016 |
| v15.0.1                      | Apr 09, 2016 |
| v15.0.2                      | Apr 30, 2016 |
| v15.1.0                      | May 21, 2016 |
| v15.2.0                      | Jul 02, 2016 |
| v15.2.1                      | Jul 09, 2016 |
| v15.3.0                      | Jul 30, 2016 |
| v15.3.1                      | Aug 20, 2016 |
| v15.3.2                      | Sep 20, 2016 |
| v15.4.0                      | Nov 16, 2016 |
| v15.4.1                      | Nov 23, 2016 |
| v15.4.2                      | Jan 07, 2017 |
| v15.5.0                      | Apr 08, 2017 |
| v15.5.1                      | May 02, 2017 |
| v15.5.2                      | May 02, 2017 |
| v15.5.3                      | May 02, 2017 |
| v15.5.4                      | May 02, 2017 |
| v15.6.0                      | Jun 14, 2017 |
| v15.6.1                      | Jun 15, 2017 |
| v15.6.2                      | Nov 15, 2017 |
| v16.0.0                      | Sep 27, 2017 |
| v16.1.0                      | Nov 09, 2017 |
| v16.1.1                      | Nov 14, 2017 |
| v16.2.0                      | Nov 29, 2017 |
| v16.3.0                      | Mar 30, 2018 |
| v16.3.1                      | Apr 04, 2018 |
| v16.3.2                      | Apr 16, 2018 |
| v16.4.0                      | May 24, 2018 |
| v16.4.1                      | Jun 14, 2018 |
| v16.4.2                      | Aug 02, 2018 |
| v16.5.0                      | Sep 07, 2018 |
| v16.5.1                      | Sep 14, 2018 |
| v16.5.2                      | Sep 19, 2018 |
| v16.6.0                      | Oct 24, 2018 |
| v16.6.1                      | Nov 07, 2018 |
| v16.6.3                      | Nov 14, 2018 |
| v16.7.0                      | Dec 20, 2018 |
| v16.8.0                      | Feb 06, 2019 |
| v16.8.1                      | Feb 09, 2019 |
| v16.8.2                      | Feb 15, 2019 |
| v16.8.3                      | Feb 22, 2019 |
| v16.8.4                      | Mar 06, 2019 |
| v16.8.5                      | Mar 23, 2019 |
| v16.8.6                      | Mar 28, 2019 |
| v16.9.0-alpha.0              | Apr 04, 2019 |
| v16.9.0-rc.0                 | Aug 06, 2019 |
| 16.9.0 (August 8, 2019)      | Aug 09, 2019 |
| 16.10.0 (September 27, 2019) | Sep 28, 2019 |
| 16.10.1 (September 28, 2019) | Sep 29, 2019 |
| 16.10.2 (October 3, 2019)    | Oct 04, 2019 |
| 16.11.0 (October 22, 2019)   | Oct 23, 2019 |
| 16.12.0 (November 14, 2019)  | Nov 15, 2019 |
| 16.13.0 (February 26, 2020)  | Feb 27, 2020 |
| 16.13.1 (March 19, 2020)     | Mar 20, 2020 |
| 0.14.10 (October 14, 2020)   | Oct 15, 2020 |
| 15.7.0 (October 14, 2020)    | Oct 15, 2020 |
| 16.14.0 (October 14, 2020)   | Oct 15, 2020 |
| 17.0.0 (October 20, 2020)    | Oct 21, 2020 |
| 17.0.1 (October 22, 2020)    | Mar 23, 2021 |
| 17.0.2 (March 22, 2021)      | Mar 23, 2021 |
| 18.0.0 (March 29, 2022)      | Mar 30, 2022 |
| 18.1.0 (April 26, 2022)      | Apr 27, 2022 |
| 18.2.0 (June 14, 2022)       | Jun 15, 2022 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，某些版本需要特别注意，下文会指出。&lt;/p&gt;
&lt;h2&gt;背景故事&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐两个视频，其一：&lt;a href=&quot;https://www.youtube.com/watch?v=Wm_xI7KntDs&quot;&gt;《The Story of React》&lt;/a&gt;，从宏观上描述了前端技术的发展变化过程，包括 React 的出现。（我觉得值得另写一篇 blog 了，或者补充在这里。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;其二：&lt;a href=&quot;https://www.youtube.com/watch?v=J-2buiB1uGQ&quot;&gt;《React 出现的历史背景及特性介绍》&lt;/a&gt;，大致介绍了 React 的出现解决的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;React 起源于 Facebook 的内部项目，于 2013 年 5 月开源，之后持续发展。&lt;/p&gt;
&lt;p&gt;那么发生了什么事情呢？Facebook 干了啥？&lt;/p&gt;
&lt;h3&gt;问题的产生&lt;/h3&gt;
&lt;p&gt;一个小问题：为什么简单的功能也总是出现 BUG？即使 Facebook 的工程师也常遇到。&lt;/p&gt;
&lt;p&gt;例如：显示未读消息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./sentMessage.png&quot; alt=&quot;question&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当 BUG 被解决之后，一般人就认为结束了，但是 Facebook 的工程师会思考更多。&lt;/p&gt;
&lt;p&gt;问题出现的根源是什么？两个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传统 UI 操作关注太多细节。&lt;/li&gt;
&lt;li&gt;应用程序状态分散在各处，难以追踪和维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第一个问题：传统 DOM API 关注了太多细节。&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./domApi.png&quot; alt=&quot;question&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决思路：React 始终整体“刷新”页面，无需关心细节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 情景：原始数据新增时 */
{text:&apos;message1&apos;}，
{text:&apos;message2&apos;}，
------------------
{text:&apos;message3&apos;}，
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/** 局部刷新 */
&amp;lt;ul&amp;gt;
  &amp;lt;li&amp;gt;message1&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;message2&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
-------------------
// Append:
&amp;lt;li&amp;gt;message2&amp;lt;/li&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/** React 整体刷新 */
// 第一步：
{text:&apos;message1&apos;}，
{text:&apos;message2&apos;}，
{text:&apos;message3&apos;}，
// 第二步：
&amp;lt;ul&amp;gt;
  &amp;lt;li&amp;gt;message1&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;message2&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;message3&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;状态变化时， React 自动刷新，于是就不需要关心细节实现了，而只需要关心状态和最终 UI 。&lt;/p&gt;
&lt;h3&gt;第二个问题：数据模型如何解决？&lt;/h3&gt;
&lt;p&gt;传统的 MVC 模型，view 层和 model 层关系复杂。当应用程序变大之后，就难以扩展和维护了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./MVC.png&quot; alt=&quot;MVC&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决思路：Flux 架构——单线数据流，只关心状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Flux.png&quot; alt=&quot;Flux&quot; /&gt;&lt;/p&gt;
&lt;p&gt;基于 Flux 出现的衍生项目：Redux、MobX ，都是优秀的状态管理框架。&lt;/p&gt;
&lt;h2&gt;著名版本&lt;/h2&gt;
&lt;h3&gt;React 14&lt;/h3&gt;
&lt;p&gt;当时 React 的版本号是 0.14.X，被称为 “React 14” 。&lt;/p&gt;
&lt;p&gt;在 React 0.14 前，浏览器端实现对 jsx 的编译依赖 jsxtransformer.js 。&lt;/p&gt;
&lt;p&gt;在 React 0.14 后，这个依赖的库改为 browser.js 。&lt;/p&gt;
&lt;p&gt;同时，页面 script 标签的 type 也由 text/jsx 改为 text/babel 。&lt;/p&gt;
&lt;h3&gt;React 15&lt;/h3&gt;
&lt;p&gt;Facebook 将 React 的版本号从 0.14 直接跳到 15 ，希望借此提升这个迅猛发展的项目的认可度。&lt;/p&gt;
&lt;p&gt;React 15 架构可以分为两层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reconciler（协调器）—— 负责找出变化的组件。&lt;/li&gt;
&lt;li&gt;Renderer（渲染器）—— 负责将变化的组件渲染到页面上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特点：React 15 的更新流程是同步执行的，一旦开始更新直到页面渲染前都不能中断。&lt;/p&gt;
&lt;h3&gt;React 16&lt;/h3&gt;
&lt;p&gt;React 16 架构可以分为三层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scheduler（调度器）—— 负责调度任务的优先级，高优任务优先进入 Reconciler 。&lt;/li&gt;
&lt;li&gt;Reconciler（协调器）—— 负责找出需要更新的组件。&lt;/li&gt;
&lt;li&gt;Renderer（渲染器）—— 负责将需要更新的组件渲染到页面上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fiber：将原本的同步更新过程碎片化，避免主线程的长时间阻塞，使应用的渲染更加流畅。&lt;/li&gt;
&lt;li&gt;Hooks：解决状态逻辑复用问题——妙极了！（永远的神！）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;React 17&lt;/h3&gt;
&lt;p&gt;作为一个过渡版本，承上启下，作为渐进式框架的首版本，在后续的 18、19 等版本中会进行渐进升级而不是强制进行硬切换。&lt;/p&gt;
&lt;p&gt;特点：启发式更新算法。&lt;/p&gt;
&lt;p&gt;React16 的 expirationTimes 模型只能区分是否 &amp;gt;=expirationTimes 决定节点是否更新。&lt;/p&gt;
&lt;p&gt;React17 的 lanes 模型可以选定一个更新区间，并且动态的向区间中增减优先级，可以处理更细粒度的更新。&lt;/p&gt;
&lt;h3&gt;React 18&lt;/h3&gt;
&lt;p&gt;React 18 中的重大更改仅限于几个简单的 API 更改，以及对 React 中多个行为的稳定性和一致性的一些改进，比较重要的一点是，不再支持 IE 浏览器。&lt;/p&gt;
&lt;p&gt;新特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端渲染 API：带有 createRoot() 的 root API，替换现有的 render() 函数，提供更好的人体工程学并启用新的并发渲染特性。&lt;/li&gt;
&lt;li&gt;自动批处理：任何情况下都可以合并渲染，如果仍然希望 setState 之后立即重新渲染，只需要使用 flushSync 包裹，或者 async/awit 。&lt;/li&gt;
&lt;li&gt;并发渲染器：允许并发渲染，即同时在后台准备多个版本的 UI，这意味着更好的性能和更平滑的状态转换。它提供了三个 API：startTransition()、useDeferredValue()、useTransition()。&lt;/li&gt;
&lt;li&gt;服务端渲染：Suspense 可以运行在服务器端，Server Rendering 的性能不再受制于性能最差的组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;前端技术发展史——React 的故事&lt;/h1&gt;
&lt;p&gt;对于没有经历过技术衍变（出生较晚或入行较晚）的人来说，入门时，摆在眼前的就是大量的新技术和新名词。&lt;/p&gt;
&lt;p&gt;这常常使人感到疑惑：这玩意儿怎么冒出来的？&lt;/p&gt;
&lt;p&gt;虽然只要学会运用最新的工具，就足以胜任工作了，但是笔者认为了解历史衍变是非常有必要的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;温故知新：新技术能取代旧技术，必然有其优点。相对的，那些旧技术的出现，也是因为比更早的技术有优势。&lt;/li&gt;
&lt;li&gt;推陈出新：技术发展的规律是可以通过历史观察的，那么未来的新技术发展方向也是可以预测的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面让我们一起来看看，那过去的故事。&lt;/p&gt;
&lt;h2&gt;排名现状&lt;/h2&gt;
&lt;p&gt;先看看关于前端框架的一些统计数据。(数据来源：&lt;a href=&quot;https://2020.stateofjs.com/zh-Hans/technologies/front-end-frameworks/&quot;&gt;stateofjs&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;1、2022 年前端框架用户调查&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./PN.png&quot; alt=&quot;PN&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2、近几年前端框架的满意度、关注度、 使用率、和认知率的排行。&lt;/p&gt;
&lt;p&gt;不包含认知率低于 10％ 的技术。每个百分比定义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;满意率：会再次使用 / (会再次使用 + 不会再次使用)&lt;/li&gt;
&lt;li&gt;关注率：想学习 / (想学习 + 不感兴趣)&lt;/li&gt;
&lt;li&gt;使用率：(将再次使用 + 将不再使用) / 总计&lt;/li&gt;
&lt;li&gt;认知率：(总计 - 从未听说过) / 总计&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;1&quot; /&gt;
&lt;img src=&quot;./2.png&quot; alt=&quot;2&quot; /&gt;
&lt;img src=&quot;./3.png&quot; alt=&quot;3&quot; /&gt;
&lt;img src=&quot;./4.png&quot; alt=&quot;4&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;设计思想的变化&lt;/h2&gt;
&lt;p&gt;React 的诞生不是一蹴而就的，在更早之前存在着一些非常优秀的 JS 框架或库。&lt;/p&gt;
&lt;p&gt;在设计思想的不断革新中，React 作为其中一环，出现了。&lt;/p&gt;
&lt;p&gt;当热，React 之后也有更新的技术出现。(不断流的思想源泉，真是一件美事啊！)&lt;/p&gt;
&lt;h3&gt;React 之前&lt;/h3&gt;
&lt;p&gt;2011 年到 2013 年， jQuery、Backbone、Angular 占领了社区讨论空间。&lt;/p&gt;
&lt;p&gt;而 jQuery 则是当之无愧的“天下第一”，是最流行的构建 web 网站的选择。&lt;/p&gt;
&lt;p&gt;jQuery 便于操作真实 DOM 树，能够在任意 node 改变另一个 node 的状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jquery.png&quot; alt=&quot;jQuery&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;John Resig (jQuery 创始人) 在 2011 年 6 月接受采访时被问到：
对于 jQuery 你最初的目标是什么？&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
John 回答到：“......我想在 Firefox 开发我的 App，然后它也能在 IE 上运行......我想专注于改自己的 BUG，而不想处理浏览器的差异问题......我写了一点简洁的 API 来支持我写代码......嗯哼......”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是后来发现 jQuery 最大的祝福也是它最大的诅咒：&lt;/p&gt;
&lt;p&gt;它的是依赖共享可变状态的，更新 DOM 状态的简单方法，通常会导致难以预测和追踪的代码出现。&lt;/p&gt;
&lt;p&gt;比如，多个 node 都去改变某一个 node 时，那么这段代码就难以维护了。&lt;/p&gt;
&lt;p&gt;于是，backbone 闪亮登场!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;backbone 引入了 MVC 模式，它是第一个流行的用于 web 构建的 JavaScript 框架。&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
backbone 允许将应用程序状态和 dom 分离，只要 model 改变，所有关心该 model 的 view 将重新呈现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然， MVC 的问题同样严重。&lt;/p&gt;
&lt;p&gt;接下来登场的，就是大名鼎鼎的 Angular ！(看图片就有一种随性的感觉了——注意右下脚)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./angular.png&quot; alt=&quot;angular&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Angular 是用于构建 web app 的一体式框架，号称可以做任何事情，包括双向数据绑定、数据过滤器、路由控制器、依赖注入模板等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 2011 年，一个纯粹的前端工程师是稀有物种，因为大多做前端工作的还是悲催的后端工程师。&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
所以说，Angular 的强大是对于后端工程师来说的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看看这个“双向数据绑定”，写法简单明了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./angularForm.png&quot; alt=&quot;angularForm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是 Angular 最大的问题也在于“双向数据绑定”（model 改变时 view 改变，view 改变时 model 改变）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在实践中，自己手动操作隐式状态改变时，由于双向数据绑定的存在，会导致代码逻辑复杂、难以维护。也会导致新能问题，因为 Angular 要不算扫描代码来寻找状态变化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;React 的出现&lt;/h3&gt;
&lt;p&gt;前文提到的 jQuery、Backbone、Angular，还有没提到的 canjs、stapes.js、BATMAN.js、Knockout.js、soma.js、Spine.js、Chaplin、Knockback.js 等等，都是基于模型的修改和视图的修改来完成功能的，需要开发人员保证模型和视图的同步。&lt;/p&gt;
&lt;p&gt;那么有新的解决思路吗？答案是 React 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2013 年 7 月，React 团队的工程师经理 Tom Ochino 在 React 介绍会上说道：
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;我们发现构建视图的最简单的方法是：完全避免突变（直接 dom 操作修改内容），在需要数据变化的时候，直接摧毁视图，再重新渲染一个新的。对开发者来说，这会简单很多，因为只需要关心数据状态的变化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;React 的核心创新在于，使视图成为处理应用程序状态的函数，开发者只需要关心状态的更改，而由 React 来处理其余部分（视图更新等）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“......没有更多明确的 dom 操作；一切都是申明性的。”—— Pete Hunt (React Core Team, Sep 2013)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用过 React 的人也能感觉到，组件的使用就像搭积木一样：看起来像积木，用起来像积木，那他就是 React 。&lt;/p&gt;
&lt;h3&gt;React 之后&lt;/h3&gt;
&lt;p&gt;技术在发展，React 并非完美无缺。&lt;/p&gt;
&lt;p&gt;更新的技术：Next.js, Remix 出现了。&lt;/p&gt;
&lt;p&gt;感兴趣可以自行了解。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;由于 React 的易用性（吸引新人），以及各种第三方组件带来的强大生态圈（留住老人），目前 React 的地位是难以撼动的。&lt;/p&gt;
&lt;p&gt;每个版本都有它的优缺点，新版本不断优化或解决旧版本的问题，这让人对未来感到非常期待！&lt;/p&gt;
</content:encoded></item><item><title>React Router：单页应用程序实例</title><link>https://0110wdj.github.io/snofly-blog/posts/react/react-router/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/react/react-router/</guid><pubDate>Fri, 30 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;译者述&lt;/h2&gt;
&lt;p&gt;1、原文来自&lt;a href=&quot;https://www.taniarascia.com/&quot;&gt;Tania Rascia&lt;/a&gt;个人网站上的一篇文章：&lt;a href=&quot;https://www.taniarascia.com/using-react-router-spa/&quot;&gt;Using React Router for a Single Page Application&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2、这是一篇较易的入门介绍文章，发布于 2019 年 4 月 25 日。&lt;/p&gt;
&lt;p&gt;3、翻译中会尽量遵照原意，也会加入译者的技术注释，以及选择更符合汉语文法的译句。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;React 没有内置的路由器，但是我们很容易通过 react-router-dom 库实现路由。&lt;strong&gt;路由&lt;/strong&gt;是指 web 应用怎样定位资源路径。（如果你知道路由是什么，可以跳过这一段。）&lt;/p&gt;
&lt;p&gt;举例：对 Tania 的个人网站，如果访问 &lt;a href=&quot;https://www.taniarascia.com/&quot;&gt;taniarascia.com&lt;/a&gt; 那么会到达首页，如果访问 &lt;a href=&quot;https://www.taniarascia.com/me&quot;&gt;taniarascia.com/me&lt;/a&gt; 那么会重定向到个人简介。如果再访问 &lt;a href=&quot;https://www.taniarascia.com/categories/java-script&quot;&gt;taniarascia.com/categories/java-script&lt;/a&gt; 或者 &lt;a href=&quot;https://www.taniarascia.com/categories/front-end&quot;&gt;taniarascia.com/categories/front-end&lt;/a&gt; 那么会出现文章列表。这些文章的路由就像这样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;/ 根目录&lt;/li&gt;
&lt;li&gt;/:page-id 文章&lt;/li&gt;
&lt;li&gt;/categories/:category_id 类别&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上没有 categorie 文件夹，也没有 java-script.html 或者 front-end.html 这样的文件。这里只有一个模板，而路由器知道怎么选择合适的文章。当获取到模板，它就可以从 URL 中提取相关信息，从而知道要显示的内容——例如，java-script 或 front-end 相关的文章。&lt;/p&gt;
&lt;p&gt;Tania 的个人网站正好也是单页应用(Single Page Application (SPA))——只有一个页面，每次点击新页面链接会加载额外的 JSON 数据，但是不会重新请求页面资源，比如请求 index.html 或者 about-me.html。&lt;/p&gt;
&lt;p&gt;我将向你展示如何使用 react-router-dom 在 React 中设置一个简单的 SPA，并通过 URL 动态获取数据。 下面是项目的全部源程序，如果你之后感到迷惑，可以查看。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/taniarascia/router-example&quot;&gt;查看项目源程序&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;阅读 &lt;a href=&quot;https://www.taniarascia.com/getting-started-with-react/&quot;&gt;Getting Started with React&lt;/a&gt; 或者 &lt;a href=&quot;https://www.taniarascia.com/crud-app-in-react-with-hooks/&quot;&gt;Build a React App with Hooks&lt;/a&gt;，如果你不知道 React 或者 React Hooks.&lt;/li&gt;
&lt;li&gt;阅读 &lt;a href=&quot;https://www.taniarascia.com/how-to-connect-to-an-api-with-javascript/&quot;&gt;How to Connect to an API in JavaScript&lt;/a&gt;，如果你一点也不知道 API 的使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;创建&lt;/h1&gt;
&lt;p&gt;创建一个新的 React App。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx create-react-app router-example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的项目有两个依赖——路由：react-router-dom，API 调用：axios。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：记得 cd 先进文件，再执行安装依赖的命令。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;npm install react-router-dom axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yarn add react-router-dom axios
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;浏览器路由器&lt;/h1&gt;
&lt;p&gt;为了使用 react-router-dom，我们需要将整个 App 组件包裹在 &lt;a href=&quot;https://reacttraining.com/react-router/web/api/BrowserRouter&quot;&gt;BrowserRouter&lt;/a&gt; 中。这里有两种路由器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BrowserRouter —— 制作漂亮的 URL，例如 example.com/about 。&lt;/li&gt;
&lt;li&gt;HashRouter —— 使用 octothorpe（或 hashtag）制作 URL，例如 example.com/#about 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我们选择使用 BrowserRouter 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/index.js
import React from &quot;react&quot;;
import { render } from &quot;react-dom&quot;;
import { BrowserRouter } from &quot;react-router-dom&quot;;
import App from &quot;./App&quot;;

render(
  &amp;lt;BrowserRouter&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/BrowserRouter&amp;gt;,
  document.querySelector(&quot;#root&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：结合之前下载的项目，参看并修改。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;路由选择&lt;/h1&gt;
&lt;p&gt;现在在 App.js 中，我们可以决定想要使用的路由并相应地引导。这次我们要使用 Route 和 Switch 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Switch - 将所有路由组合在一起，并确保它们从上到下的优先级关系。&lt;/li&gt;
&lt;li&gt;Route - 每个独立的路由。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：react-router-dom&apos; 从第 6 版开始已经没有 Switch 组件了，改用 Routes 组件，详细&lt;a href=&quot;https://reactrouter.com/en/main/components/routes&quot;&gt;参考官方文档&lt;/a&gt;。下面的代码除了原文的写法，还有译者测试当前可用的写法。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// App.js 原文写法
import React from &quot;react&quot;;
import { Route, Switch } from &quot;react-router-dom&quot;;
// 一会儿要创建这两个页面
import HomePage from &quot;./pages/HomePage&quot;;
import UserPage from &quot;./pages/UserPage&quot;;

export default function App() {
  return (
    &amp;lt;Switch&amp;gt;
      &amp;lt;Route exact path=&quot;/&quot; component={HomePage} /&amp;gt;
      &amp;lt;Route path=&quot;/:id&quot; component={UserPage} /&amp;gt;
    &amp;lt;/Switch&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其他写法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// App.js 新的写法
import React from &quot;react&quot;;
import { Route, Routes } from &quot;react-router-dom&quot;;
// 一会儿要创建这两个页面
import HomePage from &quot;./pages/HomePage.js&quot;;
import UserPage from &quot;./pages/UserPage&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Routes&amp;gt;
        &amp;lt;Route path=&quot;/&quot; element={&amp;lt;HomePage /&amp;gt;}&amp;gt;
          &amp;lt;Route path=&quot;:id&quot; element={&amp;lt;UserPage /&amp;gt;} /&amp;gt;
        &amp;lt;/Route&amp;gt;
      &amp;lt;/Routes&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把根路由 / 匹配给 HomePage，把其他页面动态匹配给 UserPage 。对于这个简单的例子，只有一条路由，当然也可以配置更多:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 原文写法
&amp;lt;Switch&amp;gt;
  &amp;lt;Route exact path=&quot;/&quot; component={HomePage} /&amp;gt;
  &amp;lt;Route path=&quot;/:id&quot; component={UserPage} /&amp;gt;
  &amp;lt;Route path=&quot;/categories&quot; component={CategoriesPage} /&amp;gt;
  &amp;lt;Route path=&quot;/categories/:id&quot; component={IndividualCategoryPage} /&amp;gt;
&amp;lt;/Switch&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其他写法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 新的写法
&amp;lt;Routes&amp;gt;
  &amp;lt;Route path=&quot;/&quot; element={&amp;lt;HomePage /&amp;gt;}&amp;gt;
    &amp;lt;Route path=&quot;:id&quot; element={&amp;lt;UserPage /&amp;gt;} /&amp;gt;
    &amp;lt;Route path=&quot;categories&quot; component={CategoriesPage}&amp;gt;
      &amp;lt;Route path=&quot;:id&quot; element={&amp;lt;IndividualCategoryPage /&amp;gt;} /&amp;gt;
    &amp;lt;/Route&amp;gt;
  &amp;lt;/Route&amp;gt;
&amp;lt;/Routes&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就能让 taniarascia.com/categories 去访问类别列表，让 taniarascia.com/categories/javascript 去访问另一种模板的页面。&lt;/p&gt;
&lt;h1&gt;Link（链接）&lt;/h1&gt;
&lt;p&gt;为了链接到 SPA 中的页面，我们使用 Link 。如果我们使用传统的 a 标签指定路由，它将发出一个全新的请求并重新加载页面，因此我们使用 Link 来解决问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/pages/HomePage.js
import React from &quot;react&quot;;
import { Link } from &quot;react-router-dom&quot;;

export default function HomePage() {
  return (
    &amp;lt;div className=&quot;container&quot;&amp;gt;
      &amp;lt;h1&amp;gt;首页&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;
        本站中的&amp;lt;Link to=&quot;/test&quot;&amp;gt; test &amp;lt;/Link&amp;gt;页面链接
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们进入第一个路由，也就是加载首页的 / 根路径，然后就可以看到首页的文本和链接。
&lt;img src=&quot;./home.png&quot; alt=&quot;首页&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;动态路由参数&lt;/h1&gt;
&lt;p&gt;上面的链接指向 /test ，它在路由中匹配 :id 参数。这种情况下，为了动态获取 URL 中的内容，我们使用 props 中的 match.params.id 参数。&lt;/p&gt;
&lt;p&gt;我将使用 id 参数来调用 GitHub 接口，以获取相关数据。本例中会用到 Hooks，如果你不熟悉，请参看这篇文章：&lt;a href=&quot;https://www.taniarascia.com/crud-app-in-react-with-hooks/&quot;&gt;Building a CRUD App with Hooks&lt;/a&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/pages/UserPage.js
import React, { useState, useEffect } from &quot;react&quot;;
import axios from &quot;axios&quot;;

export default function UserPage(props) {
  // Setting initial state
  const initialUserState = {
    user: {},
    loading: true,
  };

  // Getter and setter for user state
  const [user, setUser] = useState(initialUserState);

  // Using useEffect to retrieve data from an API (similar to componentDidMount in a class)
  useEffect(() =&amp;gt; {
    const getUser = async () =&amp;gt; {
      // Pass our param (:id) to the API call
      const { data } = await axios(
        `https://api.github.com/users/${props.match.params.id}`
      );

      // Update state
      setUser(data);
    };

    // Invoke the async function
    getUser();
  }, []); // Don&apos;t forget the `[]`, which will prevent useEffect from running in an infinite loop

  // Return a table with some data from the API.
  return user.loading ? (
    &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;
  ) : (
    &amp;lt;div className=&quot;container&quot;&amp;gt;
      &amp;lt;h1&amp;gt;{props.match.params.id}&amp;lt;/h1&amp;gt;

      &amp;lt;table&amp;gt;
        &amp;lt;thead&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;Name&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Location&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Website&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Followers&amp;lt;/th&amp;gt;
          &amp;lt;/tr&amp;gt;
        &amp;lt;/thead&amp;gt;
        &amp;lt;tbody&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;{user.name}&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;{user.location}&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;
              &amp;lt;a href={user.blog}&amp;gt;{user.blog}&amp;lt;/a&amp;gt;
            &amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;{user.followers}&amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
        &amp;lt;/tbody&amp;gt;
      &amp;lt;/table&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：这里的结构有变化，暂时不能运行，只做参考，以后有空再改写为符合新版本的代码。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;如果你感到迷惑，可以看看源代码整体结构。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/taniarascia/router-example&quot;&gt;查看源代码&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，请注意：如果你想在 GitHub 上使用这个页面，只知道从 you.github.io/repo/:id 导向 /:id 是不够的。如果你想把主机放到在 GitHub 上，并且遇到了问题，那么这篇文章能够帮助到你：&lt;a href=&quot;https://itnext.io/so-you-want-to-host-your-single-age-react-app-on-github-pages-a826ab01e48&quot;&gt;hosting your single page app on GitHub Pages&lt;/a&gt; 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：翻译不合理之处，欢迎指正。）&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>React 入门引导：理论概述与示例演练</title><link>https://0110wdj.github.io/snofly-blog/posts/react/react-to/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/react/react-to/</guid><pubDate>Tue, 31 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;译者述&lt;/h2&gt;
&lt;p&gt;1、原文来自 &lt;a href=&quot;https://www.taniarascia.com/&quot;&gt;Tania Rascia&lt;/a&gt; 个人网站上的一篇文章：&lt;a href=&quot;https://www.taniarascia.com/getting-started-with-react/&quot;&gt;React Tutorial: An Overview and Walkthrough&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2、这篇文章受到 &lt;a href=&quot;https://react.docschina.org/docs/getting-started.html&quot;&gt;React 官网&lt;/a&gt; 的推荐，是一篇较易的入门介绍文章，发布于 2018 年 8 月 20 日。&lt;/p&gt;
&lt;p&gt;3、翻译中会尽量遵照原意，也会加入译者的技术注释，以及选择更符合汉语文法的译句。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;我刚开始学习 JavaScript 的时候就听说 React 了，但只看了一眼，就觉得惊讶无比。因为我看见它把 HTML 代码和 JavaScript 代码混合在一起，而这不正是我们一直以来需要尽量去避免的吗？所以说，React 到底有什么了不起的地方呢？&lt;/p&gt;
&lt;p&gt;一直以来，我专注于学习原生 JS（&lt;a href=&quot;https://blog.csdn.net/HANZY72/article/details/121734116&quot;&gt;vanilla JavaScript&lt;/a&gt;），并且在职业环境中使用 jQuery 。在经过数次失败挫折之后，我终于成功入门 React ，然后就明白了为什么我会更愿意使用 React ，而不是原生 JS 或者 jQuery 。&lt;/p&gt;
&lt;p&gt;下面，我将尽量精简我所学会的知识，汇成一个优质简介，分享给大家。&lt;/p&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;p&gt;在学习使用 React 之前，你应当知道下面列举的知识。如果你之前从未使用过 JavaScript 或者 DOM 操作，额，我个人建议提前熟悉它们。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要的知识&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;熟悉基本的 HTML 和 CSS&lt;/li&gt;
&lt;li&gt;知道基本的 JavaScript 和编程&lt;/li&gt;
&lt;li&gt;基本理解 DOM&lt;/li&gt;
&lt;li&gt;熟悉 ES6 语法及其特性&lt;/li&gt;
&lt;li&gt;了解 Node.js 和 npm 全局安装&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;学习 React 的重要概念和相关术语，例如 &lt;strong&gt;Babel, Webpack, JSX, components, props, state, lifecycle 。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;创建一个简单地 React 应用，并演示上面的相关概念。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是实例应用的源代码和网页实际效果。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/taniarascia/react-tutorial&quot;&gt;GitHub 源代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://taniarascia.github.io/react-tutorial/&quot;&gt;网页 Demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;什么是 React ？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;React 是一个 JavaScript 库，也是最受欢迎的库之一，他在 GitHub 上拥有超过十万的收藏！&lt;/li&gt;
&lt;li&gt;React 不是一个框架（与“固执己见”的 Angular 大为不同）。&lt;/li&gt;
&lt;li&gt;React 是 Facebook 创建的一个开源项目。&lt;/li&gt;
&lt;li&gt;React 被用于在前端构建用户界面(UI)。&lt;/li&gt;
&lt;li&gt;React 是 MVC 应用程序中的视图层(MVC: Model View Controller)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事实上，React 最重要的一点就是，你可以创建组件(components) —— 类似于自定义的、可复用的 HTML 元素，然后借此高效地实现 UI。另一方面，React 通过 state 和 props 大大地简化了数据的存储和处理过程。&lt;/p&gt;
&lt;p&gt;我们将在下文讨论这些内容，以及更多其他相关知识。那么，让我们开始吧！&lt;/p&gt;
&lt;h1&gt;Setup and Installation&lt;/h1&gt;
&lt;p&gt;有多种使用 React 的方法，这里我将展示其中的两种，以便于你理解到 React 的工作原理。&lt;/p&gt;
&lt;h2&gt;静态 HTML 文件&lt;/h2&gt;
&lt;p&gt;这是第一种方法，不过它是一种不受欢迎的写法，而且之后的内容重点也不是它。但是，它可以让你很容易地熟悉和理解 React ——即使你从未使用过 JS 库，比如 jQuery。甚至可以说，如果你一点也不熟悉 Webpack、Babel、Node.js，那么这将是最简单的入门方法。&lt;/p&gt;
&lt;p&gt;我们先要创建一个 index.html 文件，然后在 head 元素中加载三个 &lt;a href=&quot;https://zhuanlan.zhihu.com/p/51842336&quot;&gt;CDN&lt;/a&gt; ：React、React DOM、Babel。之后要写个 div 元素，设置其 id 为 root，最后创建一个 script 标签用来写你自己的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;

    &amp;lt;title&amp;gt;Hello React!&amp;lt;/title&amp;gt;

    &amp;lt;script src=&quot;https://unpkg.com/react@^16/umd/react.production.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/react-dom@16.13.0/umd/react-dom.production.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/babel-standalone@6.26.0/babel.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script type=&quot;text/babel&quot;&amp;gt;
      // React code will go here
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在撰写本文时，我正在加载这些库的最新稳定版本。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：作者写文时，2018 年 React 是 16 版本，现在 2022 年是 18 版本。对于版本差异，读者暂时不必关心，React
核心思想是不变的。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://reactjs.org/docs/react-api.html&quot;&gt;React&lt;/a&gt; - React 顶层 API
&lt;a href=&quot;https://reactjs.org/docs/react-dom.html&quot;&gt;React DOM&lt;/a&gt; - 提供了一些 DOM 方法
&lt;a href=&quot;https://babeljs.io/&quot;&gt;Babel&lt;/a&gt; - 一种 JavaScript 编译器，它能让 ES6+ 代码在旧浏览器上运行。&lt;/p&gt;
&lt;p&gt;我们的程序入口是 id 为 root 的 div 元素，这是按照规范约定的命名。同时，你也要注意到 test/babel 这种类型，这是使用 Babel 必须的写法。&lt;/p&gt;
&lt;p&gt;现在，让我们编写第一个 React 代码块。它将使用 ES6 的类来创建 React 组件，命名为 APP。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class App extends React.Component {
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们添加 &lt;a href=&quot;https://reactjs.org/docs/react-component.html#render&quot;&gt;render() 方法&lt;/a&gt;，这是类组件中惟一强制要求实现的方法，它将用于渲染 DOM 节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class App extends React.Component {
  render() {
      return (
          //...
      );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 return 中，我们将放入一个好像 HTML 元素的东西。注意，这里并不是返回的字符串，所以不用引号来包裹元素。这种写法就被称为 JSX，后续我们将学习更多的相关知识。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：传统 JS 代码，是基于字符串拼接来生成 HTML 代码，相当于一直在做字符串处理。JSX 中，直接在 JS
代码中插入有意义的 HTML 元素，这样的写法理解起来更直观。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class App extends React.Component {
  render() {
    return &amp;lt;h1&amp;gt;Hello world!&amp;lt;/h1&amp;gt;;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们使用 ReactDOM.render() 方法，将我们创建的 App 类渲染到 HTML 的 root div 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&quot;root&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 index.html 的完整代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;

    &amp;lt;title&amp;gt;Hello React!&amp;lt;/title&amp;gt;

    &amp;lt;script src=&quot;https://unpkg.com/react@16/umd/react.development.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/react-dom@16/umd/react-dom.development.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/babel-standalone@6.26.0/babel.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script type=&quot;text/babel&quot;&amp;gt;
      class App extends React.Component {
        render() {
          return &amp;lt;h1&amp;gt;Hello world!&amp;lt;/h1&amp;gt;;
        }
      }

      ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&quot;root&quot;));
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在在浏览器中浏览 index.html，你将看到我们创建的 h1 标签被渲染到了 DOM 中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./hello.png&quot; alt=&quot;hello world&quot; /&gt;&lt;/p&gt;
&lt;p&gt;妙啊！&lt;/p&gt;
&lt;p&gt;现在你完成这个代码，就能发现 React 入门并不是一样难以做到的事。它只是加载了一些 JS 库到我们的 HTML 中嘛。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：可能有人觉得上面的代码很奇怪：在 HTML 代码里面放 JS 代码，再在 JS 代码里面放 HTML 代码，那为什么不直接写个
HTML 代码呢？想必聪明的你已经体会到了，当然是为了兼顾灵活性和可读性。到最后，一般的程序开发，就能像搭积木一样简单。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;到此为止的内容，都是为了简单演示。从下面开始，我们将使用另一种方法：Create React App。&lt;/p&gt;
&lt;h2&gt;Create React APP&lt;/h2&gt;
&lt;p&gt;上面使用的方法——将 JavaScript 库加载到静态 HTML 页面并动态渲染 React 和 Babel，效率不高且难以维护。&lt;/p&gt;
&lt;p&gt;幸运的是， Facebook 做了一个好东西：&lt;a href=&quot;https://github.com/facebook/create-react-app&quot;&gt;Create React APP&lt;/a&gt;，它是一个预先配置完善的 React app 构建环境。它构建了一个热更新的开发环境服务器；通过 Webpack 来自动编译 React、JSX、ES6 代码；自动增加 CSS 文件的前缀；使用 ESLint 来测试代码和警告其中的问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：这里可以简单理解为，有一个程序，自动帮你配置了各种依赖，减少了手动配置的麻烦。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在你的终端运行以下命令，将会在当前目录下生成一个新的文件夹 react-tutorial，其中就是项目内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npx create-react-app react-tutorial
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待完成之后，进去到新的文件夹中，开始启动项目。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd react-tutorial &amp;amp;&amp;amp; npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一会儿之后，你的浏览器就会弹出新窗口，现实如下页面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./init.png&quot; alt=&quot;运行成功啦&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create React App is very good for getting started for beginners as well as large-scale enterprise applications, but it&apos;s not perfect for every workflow. You can also create your own Webpack setup for React.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;(译)Create React App 非常适合初学者的入门，以及大型企业应用程序的开始，但它并不适合每个工作情况。 所以，你还可以为 React 自定义 Webpack 设置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;观察项目结构，你可以看到 /public 目录和 /src 目录，还有常规的 node_modules、.gitignore、README.md、package.json。&lt;/p&gt;
&lt;p&gt;在 /public 中，最重要的文件是 index.html，它和前文中提到的静态 index.html 文件很像，都是一个 root div。不过这次，没有在文件中加载任何的依赖库和 script 标签。&lt;/p&gt;
&lt;p&gt;在 /src 中，将会包含全部的 React 代码。&lt;/p&gt;
&lt;p&gt;为了观察 React 代码的自动化编译与更新，在 /src/App.js 文件中找到这句话:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Edit &amp;lt;code&amp;gt;src/App.js&amp;lt;/code&amp;gt; and save to reload.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后随便写点字替换它，再然后保存文件，你就能看到 localhost:3000 中刷新了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充，React 16 中的这句话是：To get started, edit &lt;code&gt;src/App.js&lt;/code&gt; and save to reload. 而在 React 18 中，这句话是：Edit &amp;lt;code&amp;gt;src/App.js&amp;lt;/code&amp;gt; and save to reload.）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在，让我们删除 /src 目录下的全部文件，因为我们将要创建自定义的模板——非常简洁，只有 index.css 和 index.js 文件。&lt;/p&gt;
&lt;p&gt;对于 index.css 文件，我就直接复制粘贴来自 &lt;a href=&quot;https://taniarascia.github.io/primitive/css/main.css&quot;&gt;Primitive CSS&lt;/a&gt; 的内容。当然，如果你想，也可以使用 Bootstrap 或者别的什么 CSS 框架，或者干脆什么都不写。我只是觉得这个更容易使用。&lt;/p&gt;
&lt;p&gt;对于 index.js 文件，我们导入 React、ReactDOM，以及 CSS 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;
import &quot;./index.css&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们再来创建 App 组件。之前，我们只是写了一个 h1 标签，现在添加一个 div 元素包裹 h1 标签，并设置 div className=“App”。值得注意的是，我们使用 className 代替了 class。这是一个小提醒，说明这里的代码本质上是 JavaScript，而不是单纯的 HTML。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class App extends React.Component {
  render() {
    return (
      &amp;lt;div className=&quot;App&quot;&amp;gt;
        &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，像之前一样，我们渲染 App 组件到 root div 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&quot;root&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是 index.js 的完整代码。这次我们直接导入了 Component，所以就不需要再写 React.Component 了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { Component } from &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;
import &quot;./index.css&quot;;

class App extends Component {
  render() {
    return (
      &amp;lt;div className=&quot;App&quot;&amp;gt;
        &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}

ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&quot;root&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 localhost:3000 网页上，你就能看到&quot;Hello, React!&quot; 了。就像之前一样，我们实现了一个简单的 React app。&lt;/p&gt;
&lt;h2&gt;React Developer Tools&lt;/h2&gt;
&lt;p&gt;有一个浏览器扩展，叫做 React Developer Tools，它能让你更轻松地使用 React。下载 &lt;a href=&quot;https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot;&gt;React DevTools for Chrome&lt;/a&gt; ，或者在其他你更喜欢的浏览器里面下载。&lt;/p&gt;
&lt;p&gt;安装之后，打开浏览器开发工具 F12，你能在 tab 列中看到 React。点击一下，你就能够在编写组件时进行检查。你也可以在 Elements 中查看实际的 DOM 输出。现在看起来用处不大，但是当 app 变得越来越复杂的时候，它就越来越有必要去使用了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./test1.png&quot; alt=&quot;原文原图&quot; /&gt;
&lt;img src=&quot;./test2.png&quot; alt=&quot;译者本地测试图片&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：React Developer Tools 版本可能出现了大的变化，安装插件后，在 F12 的 tab 列中，没有 React ，而是 Component 以及 Profiler。所以这里图片有两张，一张是原文原图，一张是译者测试的图片。
另外需要注意的是， React 18 中 render 用法发生改变，旧的写法会引发警告。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时此刻，我们已经拥有了真正开始使用 React 所需的全部工具和设置。&lt;/p&gt;
&lt;h1&gt;JSX: JavaScript + XML&lt;/h1&gt;
&lt;p&gt;如你所见，我们在 React 代码中编写的好像是 HTML，但它并不是正经的 HTML，而是 JSX，即 JavaScript XML。&lt;/p&gt;
&lt;p&gt;使用 JSX，我们可以编写类似 HTML 的内容，还可以创建和使用自定义的类 XML 便签。下面是 JSX 变量赋值语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const heading = &amp;lt;h1 className=&quot;site-heading&quot;&amp;gt;Hello, React&amp;lt;/h1&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写 React 代码并不一定要使用 JSX。引擎底层的实现，是通过 createElement 方法来创建标签、属性、子组件的，之后渲染它们。下面的代码具有相同的输出结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const heading = React.createElement(
  &quot;h1&quot;,
  { className: &quot;site-heading&quot; },
  &quot;Hello, React!&quot;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上，JSX 更像 JavaScript 而不是 HTML，这里有一些编写代码时候需要注意的关键性差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 CSS 类名指定时，className 被用来替换 class ，因为在 JavaScript 中 class 是关键字。&lt;/li&gt;
&lt;li&gt;在 JSX 中的属性和方法采用小驼峰命名法，比如 onclick 变为 onClick。&lt;/li&gt;
&lt;li&gt;自闭合标签必须使用斜杠结尾，例如 &amp;lt; img /&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JavaScript 代码自然也能被嵌入 JSX 中，使用花括号{}就能嵌入变量、函数、属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const name = &quot;Tania&quot;;
const heading = &amp;lt;h1&amp;gt;Hello, {name}&amp;lt;/h1&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSX 创建和添加元素（大部分）比原生 JS 更容易理解和使用，这也是人们喜欢他的理由之一。&lt;/p&gt;
&lt;h1&gt;组件 (Components)&lt;/h1&gt;
&lt;p&gt;目前为止，我们只创建了一个组件：APP。几乎所有 React 组件可以分为两种：类组件（class components）和简单组件（simple components）。&lt;/p&gt;
&lt;p&gt;大多数 React app 都包含了很多小组件，并且他们都会在 App 组件中被加载。小组件们拥有它们自己的文件，下面让我们一起来升级改造我们的项目吧。&lt;/p&gt;
&lt;p&gt;jiang index.js 中的 App，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;
import App from &quot;./App&quot;;
import &quot;./index.css&quot;;

ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&quot;root&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着新建一个文件 App.js，将组件放到这里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { Component } from &quot;react&quot;;

class App extends Component {
  render() {
    return (
      &amp;lt;div className=&quot;App&quot;&amp;gt;
        &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们导出 App 组件，然后在 index.js 中加载。将组件分离到其他文件里面，这并不是强制性的，但是如果你不这样做，一个应用程序就会变得非常臃肿且难以维护。&lt;/p&gt;
&lt;h2&gt;类组件（class components）&lt;/h2&gt;
&lt;p&gt;现在创建另一个组件，计划是做个表格组件。现新建一个 Table.js 文件，然后把下面的代码贴上去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { Component } from &quot;react&quot;;

class Table extends Component {
  render() {
    return (
      &amp;lt;table&amp;gt;
        &amp;lt;thead&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;Name&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Job&amp;lt;/th&amp;gt;
          &amp;lt;/tr&amp;gt;
        &amp;lt;/thead&amp;gt;
        &amp;lt;tbody&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;Charlie&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;Janitor&amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;Mac&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;Bouncer&amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;Dee&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;Aspiring actress&amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;Dennis&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;Bartender&amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
        &amp;lt;/tbody&amp;gt;
      &amp;lt;/table&amp;gt;
    );
  }
}

export default Table;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是我们创建的自定义类组件，它用大写字母开始，以区别于一般的 html 元素。回到 App.js 中，我们加载 Table 到里面。第一步是引入 Table ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Table from &quot;./Table&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是在 App 的 render() 方法中加载它，就是之前写 “Hello, React!” 的地方。当然，也可以把 className 改为 container。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { Component } from &quot;react&quot;;
import Table from &quot;./Table&quot;;

class App extends Component {
  render() {
    return (
      &amp;lt;div className=&quot;container&quot;&amp;gt;
        &amp;lt;Table /&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后回到运行环境中，在浏览器中就可以看到 Table 被加载了出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./table.png&quot; alt=&quot;Table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在，我们已经见识了自定义类组件是什么东西了。我们可以多次复用这个组件，但是，因为里面的数据是写死的，所以其实也没什么用。&lt;/p&gt;
&lt;h2&gt;简单组件（simple components）&lt;/h2&gt;
&lt;p&gt;另一种 React 组件就是简单组件，其本质是一个函数，所以这种组件不需要使用关键字 class。现在让我们改造 Table，在里面做两个简单组件：一个表格标题、一个表格内容。&lt;/p&gt;
&lt;p&gt;我们使用 ES6 的箭头函数来创建这些简单组件。首先，是表格标题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const TableHeader = () =&amp;gt; {
  return (
    &amp;lt;thead&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;th&amp;gt;Name&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;Job&amp;lt;/th&amp;gt;
      &amp;lt;/tr&amp;gt;
    &amp;lt;/thead&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是表格内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const TableBody = () =&amp;gt; {
  return (
    &amp;lt;tbody&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;Charlie&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Janitor&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;Mac&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Bouncer&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;Dee&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Aspiring actress&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;Dennis&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Bartender&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
    &amp;lt;/tbody&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们的 Table 如下所示。注意，TableHeader 和 TableBody 组件在同一个文件中，都是被 Table 类组件使用的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const TableHeader = () =&amp;gt; { ... }
const TableBody = () =&amp;gt; { ... }

class Table extends Component {
  render() {
    return (
      &amp;lt;table&amp;gt;
        &amp;lt;TableHeader /&amp;gt;
        &amp;lt;TableBody /&amp;gt;
      &amp;lt;/table&amp;gt;
    )
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后结果应该和之前一样。注意观察，组件是可以被嵌套在其他组件中的，并且简单组件和类组件可以混合使用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A class component must include render(), and the return can only return one parent element.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;(译) 一个类组件必须包含 render() 方法，并且 return 返回的最外层父元素只能有一个。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：React 18 版本有较大变化，不用 render() 方法)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后总结一下类组件和函数组件的区别。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** Simple Component */
const SimpleComponent = () =&amp;gt; {
  return &amp;lt;div&amp;gt;Example&amp;lt;/div&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/** Class Component */
class ClassComponent extends Component {
  render() {
    return &amp;lt;div&amp;gt;Example&amp;lt;/div&amp;gt;;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，如果 return 只有一行，就可以不用括号。&lt;/p&gt;
&lt;h1&gt;Props&lt;/h1&gt;
&lt;p&gt;现在，我们有了一个漂亮的 Table 组件，但是它的数据是硬编码的。React 的一大优化就是数据的处理，通过属性 props 和状态 state 来实现。接下来，我们将重点关注 props 中的数据处理。&lt;/p&gt;
&lt;p&gt;首先，将 TableBody 中的数据移除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Table.js */
const TableBody = () =&amp;gt; {
  return &amp;lt;tbody /&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把数据都放到一个数组对象里面，就像自定义了一个 JSON 格式参数的 API。这个数组对象需要放在 render() 方法里面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/App.js */
class App extends Component {
  render() {
    const characters = [
      {
        name: &quot;Charlie&quot;,
        job: &quot;Janitor&quot;,
      },
      {
        name: &quot;Mac&quot;,
        job: &quot;Bouncer&quot;,
      },
      {
        name: &quot;Dee&quot;,
        job: &quot;Aspring actress&quot;,
      },
      {
        name: &quot;Dennis&quot;,
        job: &quot;Bartender&quot;,
      },
    ];

    return (
      &amp;lt;div className=&quot;container&quot;&amp;gt;
        &amp;lt;Table /&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们通过 Table 组件的元素特性将数据传到 Table 组件里面，这种传数据的用法类似 HTML5 中的 data-* 属性。&lt;/p&gt;
&lt;p&gt;这个元素特性的命名是任意的，只要不与关键字重复即可，所以这里使用自定义的 characterData。&lt;/p&gt;
&lt;p&gt;传递的数据是一个变量 characters，所以需要用花括号包裹起来——其本质上是作为一个 JavaScript 表达式存在的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/App.js */
return (
  &amp;lt;div className=&quot;container&quot;&amp;gt;
    &amp;lt;Table characterData={characters} /&amp;gt;
  &amp;lt;/div&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在数据已经从外面开始传入 Table，那么就需要从里面接收数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Table.js */
class Table extends Component {
  render() {
    const { characterData } = this.props;

    return (
      &amp;lt;table&amp;gt;
        &amp;lt;TableHeader /&amp;gt;
        &amp;lt;TableBody characterData={characterData} /&amp;gt;
      &amp;lt;/table&amp;gt;
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时打开 React 开发工具检查 Table 组件，你就能在其 props 中看到数组数据。这些数据是被存储在虚拟 DOM(virtual DOM) 中，这时一种快速同步数据到实体 DOM 方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./f12.png&quot; alt=&quot;开发工具虚拟DOM数据&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时数据还没有放到实际的 DOM 中。在 Table 中，我们可以通过 this.props 访问全部的属性。当然，这里只用到一个属性：characterData，所以我们使用 this.props.characterData 来取得那个数据。&lt;/p&gt;
&lt;p&gt;我将使用 ES6 的结构方式来创建一个变量，用于包含 this.props.characterData 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { characterData } = this.props;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 Table 组件包含两个更小的简单组件，所以需要再次通过 props 将数据传递到 TableBody 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Table.js */
class Table extends Component {
  render() {
    const { characterData } = this.props;

    return (
      &amp;lt;table&amp;gt;
        &amp;lt;TableHeader /&amp;gt;
        &amp;lt;TableBody characterData={characterData} /&amp;gt;
      &amp;lt;/table&amp;gt;
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，在 TableBody 中没有参数，并且它也只返回了一个标签。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Table.js */
const TableBody = () =&amp;gt; {
  return &amp;lt;tbody /&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将通过函数参数来传递 props，然后通过数组映射来返回构造&lt;/p&gt;
&lt;p&gt;我们将 props 作为函数参数传递，并通过&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map&quot;&gt;数组映射&lt;/a&gt;以返回数组中每个对象的表行。该映射将包含在 rows 变量中，我们将其作为表达式返回。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：如果不知道 map 语法，建议先点击“数组映射”链接看看。这里相当于简化了一个循环拼接 HTML 元素的过程。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/** src/Table.js */
const TableBody = (props) =&amp;gt; {
  const rows = props.characterData.map((row, index) =&amp;gt; {
    return (
      &amp;lt;tr key={index}&amp;gt;
        &amp;lt;td&amp;gt;{row.name}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{row.job}&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
    );
  });

  return &amp;lt;tbody&amp;gt;{rows}&amp;lt;/tbody&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在去查看 app 的前端运行效果，就能看到数据加载了。&lt;/p&gt;
&lt;p&gt;值得注意的是，我在每个表行中都加了 key。你也应该在生成 React 的列表时使用 &lt;a href=&quot;https://reactjs.org/docs/lists-and-keys.html#keys&quot;&gt;keys&lt;/a&gt;，以便于区分每个列表条目。当我们想要操作列表条目时，key 就是必要的了。&lt;/p&gt;
&lt;p&gt;在 React 组件中传递存在的数据，props 是一种高效的方式，然而组件不能修改只可读的 props。在下个章节，我们将学习使用状态(state) 来进一步控制 React 中的数据处理。&lt;/p&gt;
&lt;h1&gt;State&lt;/h1&gt;
&lt;p&gt;现在，我们已经将数据存储在一个数组变量中，并通过 props 传参。这样很容易使用，但是试想一种场景：如果我们想要删除数组中的条目呢？通过 props 我们得到了一个单向数据流；而通过 state 我们可以更新组件内部的私有数据。&lt;/p&gt;
&lt;p&gt;你可以将 state 想象成这数据：能够被保存和修改，但是又不存入数据库。举个例子，在购物车中添加或删除物品，还没有进行结算提交。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：简单理解为一种临时变量。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了说明入门，我们现创建一个 state 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class App extends Component {
  state = {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个对象中要包含所有你想要存储的变量，这里就是 characters。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class App extends Component {
  state = {
    characters: [],
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;移动实际的数据对象到 state.characters 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class App extends Component {
  state = {
    characters: [
      {
        name: &quot;Charlie&quot;,
        // the rest of the data
      },
    ],
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样数据就被包含在 state 中。因为我们想能够移除表格中的数据，所以在 App 中创建一个 removeCharacter 方法。&lt;/p&gt;
&lt;p&gt;在 removeCharacter 中，为了取得相关数据，我们依然使用结构的方式，获得 this.state.characters。为了更新 state，我们使用 this.setState()，它是一种操作 state 的内置方法。最后基于传入的索引参数&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter&quot;&gt;过滤数组&lt;/a&gt;，获取并返回一个新的数组。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You must use this.setState() to modify an array. Simply applying a new value to this.state.property will not work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（译）你必须使用 this.setState() 来修改数组。简单地为 this.state.property 赋新值是无效的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：简单来说：通过内置方法来改值，不能直接写等号赋值。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/** src/App.js */
removeCharacter = (index) =&amp;gt; {
  const { characters } = this.state;

  this.setState({
    characters: characters.filter((character, i) =&amp;gt; {
      return i !== index;
    }),
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;filter 方法不会改变原来的数组，而是返回一个新的数组，它是 JavaScript 中修改数组的首选方法。上面这个特定的方法中，检测比较 index 和数组中的所有索引，然后返回了所有符合条件的索引。&lt;/p&gt;
&lt;p&gt;现在，我们需要通过组件传递函数，然后在每行的后面渲染一个按钮，点击它可以调用这个函数。这里将 removeCharacter 作为 prop 传递到 Table 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/App.js */
render() {
  const { characters } = this.state

  return (
    &amp;lt;div className=&quot;container&quot;&amp;gt;
      &amp;lt;Table characterData={characters} removeCharacter={this.removeCharacter} /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为还需要将参数从 Table 传递到 TableBody 中，所以再来一次通过 prop 传值，和前文一样。&lt;/p&gt;
&lt;p&gt;另外，在我们的项目中，只有 App 和 Form 两个组件拥有自己的 state，所以 Table 类组件最好转为简单组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Table.js */
const Table = (props) =&amp;gt; {
  const { characterData, removeCharacter } = props;

  return (
    &amp;lt;table&amp;gt;
      &amp;lt;TableHeader /&amp;gt;
      &amp;lt;TableBody
        characterData={characterData}
        removeCharacter={removeCharacter}
      /&amp;gt;
    &amp;lt;/table&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面就是 removeCharacter() 方法中我们定义的索引的来源。在 TableBody 组件中，我们传递 key/index 参数，所以 filter 函数知道那个条目是需要移除的。我们创建一个按钮 onClick 来传递这个参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Table.js */
&amp;lt;tr key={index}&amp;gt;
  &amp;lt;td&amp;gt;{row.name}&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;{row.job}&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;
    &amp;lt;button onClick={() =&amp;gt; props.removeCharacter(index)}&amp;gt;Delete&amp;lt;/button&amp;gt;
  &amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;The onClick function must pass through a function that returns the removeCharacter() method, otherwise it will try to run automatically.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（意译）onClick 函数必须传入一个函数作为参数，且这个函数是返回的 removeCharacter() 方法，否则，如果直接传入 removeCharacter() 方法给 onClick，则会自动执行一次。
观察下面两行代码的区别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button onClick={() =&amp;gt; props.removeCharacter(index)}&amp;gt;Delete&amp;lt;/button&amp;gt;
&amp;lt;button onClick={props.removeCharacter(index)}&amp;gt;Delete&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后者在项目运行之后，会自动执行一次；前者只有在点击按钮的时候才会执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;妙极了，现在我们有了 delete 按钮，然后就可以修改 state 来删除数据了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./table2.png&quot; alt=&quot;Remove Mac.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;删除 Mac. 试试。&lt;/p&gt;
&lt;p&gt;现在你应该理解 state 的初始化和修改了吧。&lt;/p&gt;
&lt;h1&gt;表单数据提交&lt;/h1&gt;
&lt;p&gt;现在我们将数据存储在了 state 中，并且可以移除其中的任意条目。然而，如果我们想要添加新的条目呢？在一个实际的项目中，大概率是从一个空的 state 开始，然后往里面添加数据，例如任务手账或者购物车。&lt;/p&gt;
&lt;p&gt;首先，移除 state.characters 中的硬编码数据，然后通过表单来更新数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class App extends Component {
  state = {
    characters: [],
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在新文件 From.js 中创建一个 Form 组件。&lt;/p&gt;
&lt;p&gt;我们将 Form 的初始值设置为空属性对象 initialState，并将其配给 this.state。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { Component } from &quot;react&quot;;

class Form extends Component {
  initialState = {
    name: &quot;&quot;,
    job: &quot;&quot;,
  };

  state = this.initialState;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Previously, it was necessary to include a constructor() on React class components, but it&apos;s not required anymore.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（译）以前在 React 类组件中，必须包含 constructor() 方法，但是现在不用了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：这个可能是版本不一样，但是构造函数还是挺有用的）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们这个表单的目标是，每当一个表单中的字段发生变化时，能够更新 Form 的 state，然后在提交时，所有数据都要传到 App state 中并更新 Table。&lt;/p&gt;
&lt;p&gt;首先，创建一个函数，一旦发生了输入改变，它就会执行。这个事件被传递，然后通过设置 Form state 来获得输入的 name(key) 和 value。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Form.js */
handleChange = (event) =&amp;gt; {
  const { name, value } = event.target;

  this.setState({
    [name]: value,
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在提交表单之前，需要完成下面的事情。在 render 中，获取 state 中的两个属性，并将他们作为 value 分配给对应的表单 key。将 handleChange() 作为 input 的 onChange，最后导出 Form 组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;render() {
  const { name, job } = this.state;

  return (
    &amp;lt;form&amp;gt;
      &amp;lt;label htmlFor=&quot;name&quot;&amp;gt;Name&amp;lt;/label&amp;gt;
      &amp;lt;input
        type=&quot;text&quot;
        name=&quot;name&quot;
        id=&quot;name&quot;
        value={name}
        onChange={this.handleChange} /&amp;gt;
      &amp;lt;label htmlFor=&quot;job&quot;&amp;gt;Job&amp;lt;/label&amp;gt;
      &amp;lt;input
        type=&quot;text&quot;
        name=&quot;job&quot;
        id=&quot;job&quot;
        value={job}
        onChange={this.handleChange} /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}

export default Form;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 App.js 中，我们可以在 表格的下方渲染表单。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Form from &quot;./Form&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;return (
  &amp;lt;div className=&quot;container&quot;&amp;gt;
    &amp;lt;Table characterData={characters} removeCharacter={this.removeCharacter} /&amp;gt;
    &amp;lt;Form /&amp;gt;
  &amp;lt;/div&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察前端运行效果，会发现此时还没有提交按钮。更新表单字段，你会发现本地的 From 也被更新。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./f12-2.png&quot; alt=&quot;Form change&quot; /&gt;&lt;/p&gt;
&lt;p&gt;好极了！最后一步就是提交表单，然后更新父组件状态。我们在 App 组件中创建一个 handleSubmit() 方法，用来更新 App 的 state，数据来自原有的 this.state.characters 以及新添加 character 参数，这里用到了 ES6 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax&quot;&gt;扩展运算符&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;handleSubmit = (character) =&amp;gt; {
  this.setState({ characters: [...this.state.characters, character] });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后确保把这个函数参数传递到了 Form 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Form handleSubmit={this.handleSubmit} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Form 组件中，创建一个 submitForm() 方法，由它来调用 handleSubmit() 并传参，实参就是 Form 中的 state ，形参就是前面定义的 character。 之后，它会重置 state 为初始值，相当于表单重置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;submitForm = () =&amp;gt; {
  this.props.handleSubmit(this.state);
  this.setState(this.initialState);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们添加一个表单提交按钮。注意，这里使用 onClick 而不是 onSubmit，因为我们并不使用标准的提交函数。这个点击事件会调用自定义的 submitForm 函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;button&quot; value=&quot;Submit&quot; onClick={this.submitForm} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大功告成！这样 app 就已经完成啦，我们可以创建和删除表格中的数据。因为 Table 和 TableBody 已经获取到了数据，所以会直接展示出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./result.png&quot; alt=&quot;Last&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你不知所措，没有跟上，可以查看&lt;a href=&quot;https://github.com/taniarascia/react-tutorial&quot;&gt;Github 中的源代码&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：强烈建议初学者，一步一步修改到最终版，而不是直接使用最后的代码运行。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Pulling in API Data&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：这一节看不懂，不用纠结，可以略过。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;React 有一个很常见的用法，就是从 API 中提取数据。如果你不熟悉 API 的概念或者不知道怎么链接 API，我推荐你阅读这篇文章：&lt;a href=&quot;https://www.taniarascia.com/how-to-connect-to-an-api-with-javascript/&quot;&gt;How to Connect to an API with JavaScript&lt;/a&gt;，它会让你知道什么是 API 以及怎样用原生 JavaScript 调用它。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：关于 API，网上随便找几篇文章对照着看，简单了解下就行了。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;做个小测试，我们新建一个 Api.js 文件，然后创建一个新的 App。&lt;a href=&quot;https://en.wikipedia.org/w/api.php&quot;&gt;Wikipedia API&lt;/a&gt; 是一个公共接口，我们可以用来测试。这里有个 &lt;a href=&quot;https://en.wikipedia.org/w/api.php?action=opensearch&amp;amp;search=Seona+Dancing&amp;amp;format=json&amp;amp;origin=*&quot;&gt;URL&lt;/a&gt; 可以用来做测试，直接点击这个链接，看看效果，如果用 &lt;a href=&quot;https://chrome.google.com/webstore/detail/jsonvue/chklaanhfefbnpoihckbnefhakgolnmc&quot;&gt;JSONView&lt;/a&gt; 插件可以好看一点。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：维基百科是不能直接打开的，嗯。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们计划使用 JavaScript 内置的 &lt;a href=&quot;https://www.taniarascia.com/how-to-use-the-javascript-fetch-api-to-get-json-data/&quot;&gt;Fetch&lt;/a&gt; 方法从 URL 终端获取数据并展示。通过改变 index.js 中的 App 引入路径 （例如：import App from &apos;./Api&apos;），你可以在我们之前创建的 app 和这个测试的 app 之间切换。&lt;/p&gt;
&lt;p&gt;我不会逐行解释这段代码，因为我们前面学过了通过 state 数组创建组件、渲染和映射。这里新的知识点是 componentDidMount()，它是 React 生命周期里面的方法，而生命周期是指 React 中的某些特定方法的执行顺序。挂载（Mount）是指某个条目被插入到 DOM。&lt;/p&gt;
&lt;p&gt;当我们获取 API 的数据时，我们会选择使用 componentDidMount，因为我们想要确保组件在获取数据之前就被渲染出来。下文中，你将看到如何从 Wikipedia API 中引入数据，并将其显示在页面上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** src/Api.js */
import React, { Component } from &quot;react&quot;;

class App extends Component {
  state = {
    data: [],
  };

  // Code is invoked after the component is mounted/inserted into the DOM tree.
  componentDidMount() {
    const url =
      &quot;https://en.wikipedia.org/w/api.php?action=opensearch&amp;amp;search=Seona+Dancing&amp;amp;format=json&amp;amp;origin=*&quot;;

    fetch(url)
      .then((result) =&amp;gt; result.json())
      .then((result) =&amp;gt; {
        this.setState({
          data: result,
        });
      });
  }

  render() {
    const { data } = this.state;

    const result = data.map((entry, index) =&amp;gt; {
      return &amp;lt;li key={index}&amp;gt;{entry}&amp;lt;/li&amp;gt;;
    });

    return &amp;lt;ul&amp;gt;{result}&amp;lt;/ul&amp;gt;;
  }
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在本地环境中运行，你就能看到 API 的数据被展示在了 DOM 中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./local.png&quot; alt=&quot;API&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有其他生命周期方法，但对它们的介绍超出了本文的范围。 您可以&lt;a href=&quot;https://reactjs.org/docs/react-component.html&quot;&gt;在此处阅读有关 React 组件的更多信息&lt;/a&gt;。&lt;/p&gt;
&lt;h1&gt;构建和部署 React 应用&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：如果这一节看不懂，不必纠结，略过即可。这些内容，重要但不急迫，需要各种基础知识和经验，才能更好地理解这一部分内容。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;到目前为止，我们所做的一切都是在开发环境中进行的：编译、热加载、实时更新。 在实际生产中，我们将希望加载的是最后的静态文件，也就是不需要源代码来编译。 这个可以通过构建和部署来实现。&lt;/p&gt;
&lt;p&gt;现在，如果你想要编译所有的 React 代码，然后把它放到根目录的某个地方，你需要运行这个命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会创建一个 build 文件夹来存放你的 app，其中的内容可以放到任何地方，然后就完成了！&lt;/p&gt;
&lt;p&gt;我们还可以更进一步，用 npm 将它部署到某处。我们计划创建一个 GitHub 网页，所以你需要&lt;a href=&quot;https://www.taniarascia.com/getting-started-with-git/&quot;&gt;熟悉 Git &lt;/a&gt;，然后将你的代码上传到 GitHub 中。&lt;/p&gt;
&lt;p&gt;确保你已经退出本地运行环境，也就是代码没在运行。首先，在 package.json 中添加 homepage 字段，这个 URL 是我们想让这个 app 运行的地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** package.json */
&quot;homepage&quot;: &quot;https://taniarascia.github.io/react-tutorial&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后添加这两行到 package.json 的属性 scripts 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  // ...
  &quot;predeploy&quot;: &quot;npm run build&quot;,
  &quot;deploy&quot;: &quot;gh-pages -d build&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在项目中，添加 gh-pages 包来到开发依赖中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev gh-pages
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们创建 build 静态文件，它能完成全部的编译。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后部署到 gh-pages 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成！在 &lt;a href=&quot;https://taniarascia.github.io/react-tutoria&quot;&gt;https://taniarascia.github.io/react-tutoria&lt;/a&gt; 中就能查看 app 了。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;本文应该很好地介绍了 React、简单组件、类组件、state、props、使用表单数据、从 API 中提取数据、部署应用程序。关于 React 的学习和使用，还有很多很多内容，但我希望你现在已经有一定信心，继续研究和运用 React 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/taniarascia/react-tutorial&quot;&gt;查看 GitHub 源代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://taniarascia.github.io/react-tutorial/&quot;&gt;查看项目运行&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果有任何不清楚的地方，或者想在这篇文章或后续文章中看到什么，请告诉我。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（译者补充：翻译不合理之处，欢迎指正。）&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Redux 入门引导：理论概述与示例演练</title><link>https://0110wdj.github.io/snofly-blog/posts/react/redux-to/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/react/redux-to/</guid><pubDate>Tue, 08 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;h2&gt;译者述&lt;/h2&gt;
&lt;p&gt;1、原文来自 &lt;a href=&quot;https://www.taniarascia.com/&quot;&gt;Tania Rascia&lt;/a&gt; 个人网站上的一篇文章：&lt;a href=&quot;https://www.taniarascia.com/redux-react-guide/&quot;&gt;Redux Tutorial: An Overview and Walkthrough&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2、这是一篇较易的入门介绍文章, 发布于 2020 年 3 月 9 日。&lt;/p&gt;
&lt;p&gt;3、翻译中会尽量遵照原意, 也会加入译者的技术注释, 以及选择更符合汉语文法的译句。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;正文&lt;/h1&gt;
&lt;h2&gt;作者述&lt;/h2&gt;
&lt;p&gt;你有使用 &lt;a href=&quot;https://reactjs.org/&quot;&gt;React&lt;/a&gt; 的经验吗?你是否听说过 Redux, 但是因为它看起来很复杂且没有合适的入门指南而推迟学习？如果这是你的情况, 那么这篇文章正适合你。放下恐惧, 和我一起开始这段不算痛苦的学习旅程吧。&lt;/p&gt;
&lt;h2&gt;预备知识&lt;/h2&gt;
&lt;p&gt;对本文教程, 你必须已知如何使用 React, 我不会解释任何 React 方面的知识。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;熟悉 &lt;a href=&quot;https://internetingishard.com/&quot;&gt;HTML 和 CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;熟悉 &lt;a href=&quot;https://www.taniarascia.com/es6-syntax-and-feature-overview/&quot;&gt;ES6 语法和特性&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;了解 React 术语：&lt;a href=&quot;https://www.taniarascia.com/getting-started-with-react/&quot;&gt;JSX, State(状态), Components(组件), Props, Lifecycle(生命周期)&lt;/a&gt; 以及 &lt;a href=&quot;https://www.taniarascia.com/crud-app-in-react-with-hooks&quot;&gt;Hooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;了解 &lt;a href=&quot;https://www.taniarascia.com/using-react-router-spa/&quot;&gt;React Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;了解异步 JS 和&lt;a href=&quot;https://www.taniarascia.com/how-to-connect-to-an-api-with-javascript&quot;&gt;创建 API 调用&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外, 可以下载 Redux 开发工具：&lt;a href=&quot;https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en&quot;&gt;谷歌浏览器插件&lt;/a&gt;或者&lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/&quot;&gt;火狐浏览器插件&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;本文目标&lt;/h2&gt;
&lt;p&gt;在本文中, 我们会新建一个小的 blog app, 它会从一个 API 中获取文章和评论。我曾经通过普通版本的 Redux 和&lt;a href=&quot;https://redux-toolkit.js.org/&quot;&gt;Redux Toolkit&lt;/a&gt; (RTK, 官方认可的 Redux 工具集) 创建过一个相同的 app。以下是普通版和 RTK 版的源代码和演示的链接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;React + Redux Application (Plain Redux)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://codesandbox.io/s/react-redux-application-hewdb&quot;&gt;源代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hewdb.csb.app/&quot;&gt;Demo 链接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;React + Redux Toolkit Application&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://codesandbox.io/s/react-redux-toolkit-application-cbb6s&quot;&gt;源代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cbb6s.csb.app/&quot;&gt;Demo 链接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意: 此 app 是通过 “&lt;a href=&quot;https://jsonplaceholder.typicode.com/&quot;&gt;JSON 占位符 API&lt;/a&gt;” 从真正的 API 中获取数据的的。由于 CodeSandbox 上的速率限制，API 可能会显得很慢，但这与 Redux app 本身无关。当然，还可以直接在本地克隆数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们将学到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;什么是 Redux 以及为什么使用它&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redux 的术语：actions, reducers, store, dispatch, connect, 和 container&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用 &lt;a href=&quot;https://github.com/reduxjs/redux-thunk&quot;&gt;Redux Thunk&lt;/a&gt; 发起异步 API 请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;学会使用 React 和 Redux 制作轻巧、真实的 app&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;学会使用 Redux 工具简化 Redux app 的开发&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;什么是 Redux&lt;/h1&gt;
&lt;p&gt;Redux 是一个 JS app 的状态管理器。使用 React, 可以在组件级别管理状态, 并通过 props 传递状态。而使用 Redux, 整个 app 的状态都会在一个不可变对象中管理。对 Redux 状态的每次更新, 其结果是一个新的对象, 它复制了原对象中的部分状态, 并添加了改动状态。&lt;/p&gt;
&lt;p&gt;Redux 的初创人是 &lt;a href=&quot;https://overreacted.io/&quot;&gt;Dan Abramov&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/acdlite&quot;&gt;Andrew Clark&lt;/a&gt;。&lt;/p&gt;
&lt;h1&gt;为什么使用 Redux&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;便于管理全局状态 —— 访问或更新任何 Redux 连接组件的状态的任意部分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;便于追踪状态更改(通过 Redux 开发工具) —— 任何 action 或状态改变都可以被轻松追踪。实际上，每次更改，app 的全部状态都会被记录，这意味着我们可以轻松的进行 time-travel debugging, 以便于在更改历史之间切换。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redux 的缺点在于它有很多初始模板( initial boilerplate )需要设置和维护(特别是单纯使用 Redux 而没有使用 Redux 工具的时候)。因此，对一个简单的 app 而言，或许&lt;a href=&quot;https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367&quot;&gt;不需要使用 Redux&lt;/a&gt;, 而只需要简单使用 &lt;a href=&quot;https://www.taniarascia.com/using-context-api-in-react/&quot;&gt;Context API&lt;/a&gt; 即可管理好全局状态。&lt;/p&gt;
&lt;p&gt;根据我的个人经验，之前是只使用 Context 建立 app, 后来将所有内容转换为 Redux 以便于维护和组织。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：这个状态追踪很像 git 的历史记录)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;术语&lt;/h1&gt;
&lt;p&gt;我通常不喜欢单列出术语及其定义列表，但是 Redux 中有些概念就是很陌生的。所以我将在这部分简单描述一下其定义，便于在后文中使用。虽然读者也可以跳过这部分内容，但我认为最好先阅读所有的定义，以便了解它们并在脑海中形成一个概念。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Actions&lt;/li&gt;
&lt;li&gt;Reducers&lt;/li&gt;
&lt;li&gt;Store&lt;/li&gt;
&lt;li&gt;Dispatch&lt;/li&gt;
&lt;li&gt;Connect&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我将使用经典的 todo app (待办事项) 来举例说明。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：这几个术语单词就不翻译了，读写时都使用英文原文，以免造成误解。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Actions&lt;/h2&gt;
&lt;p&gt;一个 action 从 app 将数据发送到 Redux store。一个 action 通常是一个对象，它有两个属性：type 和 payload(可选)。type 通常是一个大写的字符串，描述了 action (动作/事件)。payload 是可能被传输的附加数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Action Type
const DELETE_TODO = &quot;posts/deleteTodo&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Action
{
  type: DELETE_TODO,
  payload: id,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Action creators&lt;/h2&gt;
&lt;p&gt;一个 action creator 是一个函数，他返回一个 action 。&lt;/p&gt;
&lt;h2&gt;Reducers&lt;/h2&gt;
&lt;p&gt;一个 reducer 是一个函数，有两个入参：state 和 action 。&lt;/p&gt;
&lt;p&gt;一个 reducer 是不可变的，它的返回值是整个 state 的复制版本。&lt;/p&gt;
&lt;p&gt;一个 reducer 通常由一个 switch 语句组成，该语句遍历所有可能的 action type 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Reducer
const initialState = {
  todos: [
    { id: 1, text: &quot;Eat&quot; },
    { id: 2, text: &quot;Sleep&quot; },
  ],
  loading: false,
  hasErrors: false,
};

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter((todo) =&amp;gt; todo.id !== action.payload),
      };
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：通过不同的 action 触发特定功能的运行，于是 state 的更新逻辑可以不对外暴露，在业务逻辑中，只需要调用 action 即可。而 action 一般是多个子 action 的集合，通过 type 指定具体的值，这样便于以后增加新的 action。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Store&lt;/h2&gt;
&lt;p&gt;Redux app 的 state 是存在于 store 中的，而 store 是由 reducer 初始化得到的。当和 React 一起使用时，用 Provider 标签包裹 app, 然后在 Provider 内部的任何东西就都可以访问 Redux 了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Store
import { createStore } from &quot;redux&quot;;
import { Provider } from &quot;react-redux&quot;;
import reducer from &quot;./reducers&quot;;

const store = createStore(reducer);

render(
  &amp;lt;Provider store={store}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/Provider&amp;gt;,
  document.getElementById(&quot;root&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dispatch&lt;/h2&gt;
&lt;p&gt;dispatch 是一个函数，在 store 对象中。dispatch 接受一个对象参数：该对象用于更新 Redux state 。通常，此对象是调用 action creator 得到的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Component = ({ dispatch }) =&amp;gt; {
  useEffect(() =&amp;gt; {
    dispatch(deleteTodo());
  }, [dispatch]);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Connect&lt;/h2&gt;
&lt;p&gt;connect 是一个函数，它是连接 React 到 Redux 的一种典经方法，连接的组件有时被称为容器。&lt;/p&gt;
&lt;p&gt;好了，这差不多涵盖了 Redux 的主要项。在没有任何上下文的情况下阅读术语可能会让人不知所措，所以让我们开始吧。&lt;/p&gt;
&lt;h1&gt;快速开始&lt;/h1&gt;
&lt;p&gt;为了简单，我们使用 create-react-app 来设置环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx create-react-app redux-tutorial
cd redux-tutorial
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redux 需要一些依赖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reduxjs/redux&quot;&gt;Redux&lt;/a&gt; ———— 核心库&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reduxjs/react-redux&quot;&gt;React Redux&lt;/a&gt; ———— 绑定 React 和 Redux&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reduxjs/redux-thunk&quot;&gt;Redux Thunk&lt;/a&gt; ———— Redux 异步中间件&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reduxjs/redux-devtools-extension&quot;&gt;Redux DevTools Extension&lt;/a&gt; ———— 连接 Redux app 和 Redux DevTools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以使用 yarn add 或者 npm i 安装这些依赖，我自己也会使用 react-router-dom 这个额外的依赖。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：在不同 OS 中，读者注意下命令的变化。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;(由于本文原文是 2020 年写的，有些写法在新版本中可能不兼容，需要时切换到旧版本即可，或者直接使用译者的 packge.json 配置：)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;dependencies&quot;: {
   &quot;@testing-library/jest-dom&quot;: &quot;^5.16.5&quot;,
   &quot;@testing-library/react&quot;: &quot;^13.4.0&quot;,
   &quot;@testing-library/user-event&quot;: &quot;^13.5.0&quot;,
   &quot;react&quot;: &quot;^18.2.0&quot;,
   &quot;react-dom&quot;: &quot;^18.2.0&quot;,
   &quot;react-redux&quot;: &quot;^8.0.5&quot;,
   &quot;react-router-dom&quot;: &quot;^5.0.0&quot;, // 这一个要注意，后文中的写法，不兼容 6 及以上版本
   &quot;react-scripts&quot;: &quot;5.0.1&quot;,
   &quot;redux&quot;: &quot;^4.2.1&quot;,
   &quot;redux-devtools-extension&quot;: &quot;^2.13.9&quot;,
   &quot;redux-thunk&quot;: &quot;^2.4.2&quot;,
   &quot;web-vitals&quot;: &quot;^2.1.4&quot;
 },
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;npm i \
redux \
react-redux \
redux-thunk \
redux-devtools-extension \
react-router-dom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下一步，删除所有模板文件。我们将从头开始添加我们需要的所有东西。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd src &amp;amp;&amp;amp; rm * # 进入 src 文件，然后删除里面的所有文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下一步，创建四个目录: Redux 中的 reducers 和 actions，还有 React 中的页面目录 pages 和组件目录 components 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir actions components pages reducers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下一步，把 index.js, App.js 和 index.css 重建回来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch index.js index.css App.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们的工程目录树如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;└── src/
    ├── actions/
    ├── components/
    ├── pages/
    ├── reducers/
    ├── App.js
    ├── index.css
    └── index.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 index.css 文件，只需复制&lt;a href=&quot;https://gist.githubusercontent.com/taniarascia/b0ca2e2c165390f8678e1346514e143d/raw/bed873301fd1c71261a05ec02ca152d908f49463/index.css&quot;&gt;这个网站&lt;/a&gt;的内容。我只打算讨论功能而不是样式，所以我只写了一些非常基本的样式，以确保网站看起来不太糟糕。&lt;/p&gt;
&lt;p&gt;现在我们有了足够的模板文件，让我们开始吧。&lt;/p&gt;
&lt;h1&gt;设置 Redux Store&lt;/h1&gt;
&lt;p&gt;当我第一次学习 Redux 时，感到非常茫然，因为我看到的每个 app 的 index.js 设置都有点不同。在观察了很多最新的 app 并对它们的共性进行分析之后，我能够很好地识别 Redux app 中真正需要包含的内容，或者人们为了与众不同而别出心裁的操作。&lt;/p&gt;
&lt;p&gt;网上有很多教程展示了“如何使用 todo 设置一个非常基本的 Redux 存储”，但我认为这些教程对于了解“如何进行生产级设置”没有多大帮助，所以我将从一开始就使用完整项（你需要的所有东西）来设置它。尽管 Redux 非常灵活多变，但还是会有一些固定不变化的方面。&lt;/p&gt;
&lt;p&gt;在 index.js 中，我们会引入一些东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;createStore&lt;/strong&gt;：创建 store 以维护 Redux 状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;applyMiddleware&lt;/strong&gt;：能够使用中间件，这里中间件指 thunk&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider&lt;/strong&gt;：将整个应用程序封装在 Redux 中&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;thunk&lt;/strong&gt;：一个中间件，允许我们在 Redux 中进行异步操作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;composeWithDevTools&lt;/strong&gt;：将应用程序连接到 Redux DevTools 的代码&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// index.js
import React from &quot;react&quot;;
import { render } from &quot;react-dom&quot;;
import { createStore, applyMiddleware } from &quot;redux&quot;;
import { Provider } from &quot;react-redux&quot;;
import thunk from &quot;redux-thunk&quot;;
import { composeWithDevTools } from &quot;redux-devtools-extension&quot;;

import App from &quot;./App&quot;;
import rootReducer from &quot;./reducers&quot;;

import &quot;./index.css&quot;;

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

render(
  &amp;lt;Provider store={store}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/Provider&amp;gt;,
  document.getElementById(&quot;root&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：在 index.js 中的 Provider 写法，和 React 中提供的 useContext 写法相似)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 App.js 中添加一个组件。我们稍后会修改它，但我们现在只想让应用程序启动并运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// App.js

import React from &quot;react&quot;;

const App = () =&amp;gt; {
  return &amp;lt;div&amp;gt;Hello, Redux&amp;lt;/div&amp;gt;;
};

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;引入 reducers&lt;/h2&gt;
&lt;p&gt;最后一件事是引入 reducers —— reducer 是一个决定 Redux 状态变化的函数，它是一个纯函数，返回更新后的状态的复制。&lt;/p&gt;
&lt;p&gt;Redux 的一个简洁特性是，我们可以有许多 reducer，并使用 combineReducers 将它们组合成一个给 store 使用的根 reducer。这使我们能够轻松地组织代码，同时仍然将所有内容放在一个根状态树中。&lt;/p&gt;
&lt;p&gt;由于这个应用程序会类似一个博客，所以它会有一个文章列表，我们把列表放在 postsReducer 中。有了这个 combineReducers 方法，我们就可以引入任何我们想要的东西 —— commentsReducer 、authReducer 等。&lt;/p&gt;
&lt;p&gt;在 reducers/index.js 中，创建一个文件来组合所有的 reducers。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// reducers/index.js

import { combineReducers } from &quot;redux&quot;;

import postsReducer from &quot;./postsReducer&quot;;

const rootReducer = combineReducers({
  posts: postsReducer,
});

export default rootReducer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们将创建 postsReducer，它有一个初始状态。就像你对普通 React 组件的期望一样，我们将有一个 loading 和 hasErrors 状态，以及一个 posts 数组，文章列表将保存在 posts 里。在 switch 中，还没有设置 action，而只有一个返回整个 state 的 default case。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// reducers/postsReducer.js

export const initialState = {
  posts: [],
  loading: false,
  hasErrors: false,
};

export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们至少有了足够的配置，应用程序能够加载而不会崩溃了。&lt;/p&gt;
&lt;h2&gt;Redux 插件&lt;/h2&gt;
&lt;p&gt;加载完应用程序并设置好 Redux Provider 后，我们可以看一下 Redux DevTools。下载插件后，它将成为开发人员工具(F12)中的一个 tab。单击 State，能观察到到目前为止应用程序的整个状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./reduxDevtools.png&quot; alt=&quot;dev&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里内容比较少看不太明显，但当你获得了很多 reducers 和 actions 时， Redux DevTools 就会显得很神奇。它可以跟踪应用程序的所有更改，与普通 React 相比，它使调试变得轻而易举。&lt;/p&gt;
&lt;h1&gt;设置 Redux Actions&lt;/h1&gt;
&lt;p&gt;现在我们有一个 reducer，但我们没有任何 actions。这时 reducer 只会返回状态，但没办法修改状态。actions 是我们与 Redux 存储进行通信的方式。对于这个博客 app，我们将从 API 获取文章数据，然后把它们置于 Redux 状态中。&lt;/p&gt;
&lt;p&gt;由于获取文章是一个异步操作，因此需要使用 Redux thunk。幸运的是，使用 thunk，我们只需要在 store 中设置 thunk （我们已经这样做了），而不必做任何特殊处理。&lt;/p&gt;
&lt;p&gt;创建一个 postsActions.js 文件。首先，我们将 action type 定义为常量 —— 这不是强制要求的，但这是一种常见的约定，便于导出 action 和防止拼写错误。我们想做三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;getPosts - 开始告诉 Redux 我们将从 API 获取帖子&lt;/li&gt;
&lt;li&gt;getPostsSuccess - 在成功的 API 调用时将帖子传递给 Redux&lt;/li&gt;
&lt;li&gt;getPostsFailure - 通知 Redux 在调用期间遇到错误，API 调用失败&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// actions/postsActions.js
// 创建 Redux action types
export const GET_POSTS = &quot;GET_POSTS&quot;;
export const GET_POSTS_SUCCESS = &quot;GET_POSTS_SUCCESS&quot;;
export const GET_POSTS_FAILURE = &quot;GET_POSTS_FAILURE&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建 action creators —— 返回 action 的函数。该 action 由 type 和 payload 组成（payload 是可选字段，它包含了数据）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// actions/postsActions.js
// 创建 Redux action creators ，它返回一个 action
export const getPosts = () =&amp;gt; ({
  type: GET_POSTS,
});

export const getPostsSuccess = (posts) =&amp;gt; ({
  type: GET_POSTS_SUCCESS,
  payload: posts,
});

export const getPostsFailure = () =&amp;gt; ({
  type: GET_POSTS_FAILURE,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，创建将上述所有三个 action 组合在一起的异步 thunk action。调用时，它将 dispatch(调度) 初始的 action (即 getPosts())以通知 Redux 准备 API 调用，然后在 try/catch 中执行所有必要的操作以获取数据，最后 dispatch 成功或失败的 action。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// actions/postsActions.js
// Combine them all in an asynchronous thunk
export function fetchPosts() {
  return async (dispatch) =&amp;gt; {
    dispatch(getPosts());

    try {
      const response = await fetch(
        &quot;https://jsonplaceholder.typicode.com/posts&quot;
      );
      const data = await response.json();

      dispatch(getPostsSuccess(data));
    } catch (error) {
      dispatch(getPostsFailure());
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;太好了，我们现在都完成了创建 actions！剩下要做的就是告诉 reducer 如何处理每个 action 的状态。&lt;/p&gt;
&lt;h2&gt;响应 actions&lt;/h2&gt;
&lt;p&gt;回到我们的 postsReducers，其中有一个 switch，它还没有发挥作用呢。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// reducers/postsReducer.js
export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们有了 actions，我们可以从 postsActions 页面引入它们。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Import all actions
import * as actions from &quot;../actions/postsActions&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于每个 action，我们将创建一个对应的 case ，它返回整个 state 以及我们对它所做的任何更改。例如 GET_POSTS ，我们要做的就是告诉应用程序将 loading 设置为 true， 因为我们马上要进行 API 调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case actions.GET_POSTS:
  return { ...state, loading: true }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;GET_POSTS - 开始加载&lt;/li&gt;
&lt;li&gt;GET_POSTS_SUCCESS - 应用程序没有错误，应该停止加载&lt;/li&gt;
&lt;li&gt;GET_POSTS_FAILURE - 应用程序有错误，应该停止加载&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是整个 reducer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// reducers/postsReducer.js
import * as actions from &quot;../actions/postsActions&quot;;

export const initialState = {
  posts: [],
  loading: false,
  hasErrors: false,
};

export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    case actions.GET_POSTS:
      return { ...state, loading: true };
    case actions.GET_POSTS_SUCCESS:
      return { posts: action.payload, loading: false, hasErrors: false };
    case actions.GET_POSTS_FAILURE:
      return { ...state, loading: false, hasErrors: true };
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们的 actions 和 reducers 已经准备就绪，所以剩下要做的就是将所有内容连接到 React 应用程序。&lt;/p&gt;
&lt;h1&gt;将 Redux 连接到 React 组件&lt;/h1&gt;
&lt;p&gt;由于我创建的 demo 使用 React Router 来提供一些路由，比如（仪表板）首页、文章列表页、单文章页面等。我现在将引入 React Router。此 demo 中，我将只引入首页和文章列表页。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：注意 react-router-dom 版本，Switch 在 6 及以上版本无效，所以最好使用 5 版本。当然，你也可以自己修改代码，使用新版的 Routers 写法。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect,
} from &quot;react-router-dom&quot;;

import DashboardPage from &quot;./pages/DashboardPage&quot;;
import PostsPage from &quot;./pages/PostsPage&quot;;

const App = () =&amp;gt; {
  return (
    &amp;lt;Router&amp;gt;
      &amp;lt;Switch&amp;gt;
        &amp;lt;Route exact path=&quot;/&quot; component={DashboardPage} /&amp;gt;
        &amp;lt;Route exact path=&quot;/posts&quot; component={PostsPage} /&amp;gt;
        &amp;lt;Redirect to=&quot;/&quot; /&amp;gt;
      &amp;lt;/Switch&amp;gt;
    &amp;lt;/Router&amp;gt;
  );
};

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以创建首页 —— 常规的 React 组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// pages/DashboardPage.js
import React from &quot;react&quot;;
import { Link } from &quot;react-router-dom&quot;;

const DashboardPage = () =&amp;gt; (
  &amp;lt;section&amp;gt;
    &amp;lt;h1&amp;gt;Dashboard&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;This is the dashboard.&amp;lt;/p&amp;gt;

    &amp;lt;Link to=&quot;/posts&quot; className=&quot;button&quot;&amp;gt;
      View Posts
    &amp;lt;/Link&amp;gt;
  &amp;lt;/section&amp;gt;
);

export default DashboardPage;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于每个文章，让我们创建一个 Post 组件来显示文章的标题和文本摘录。在 components 子目录中创建 Post.js 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// components/Post.js
import React from &quot;react&quot;;

export const Post = ({ post }) =&amp;gt; (
  &amp;lt;article className=&quot;post-excerpt&quot;&amp;gt;
    &amp;lt;h2&amp;gt;{post.title}&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;{post.body.substring(0, 100)}&amp;lt;/p&amp;gt;
  &amp;lt;/article&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;未连接到 Redux 的组件仍然很重要且实用，特别是在较小的、可重用的领域，例如此处的 Post 组件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在，有趣的部分出现在文章页面 —— 将 Redux 引入 React。为此，我们将使用 react-redux 中的 connect。首先，我们将为页面创建一个常规组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// pages/PostsPage.js
import React from &quot;react&quot;;

const PostsPage = () =&amp;gt; {
  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;/section&amp;gt;
  );
};

export default PostsPage;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，我们将引入 connect —— 它是一个高阶函数，可以把 Redux store 连接到 React 组件。这个顾名思义的函数将从 Redux store 中获取任何 state，并将其传递给 React 组件的 props。对 connect 的参数 mapStateToProps，就是引入的 postsReducer 中的 loading 、 posts 和 hasErrors 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充:&lt;/p&gt;
&lt;p&gt;1、高阶函数：简单理解为，生成函数的函数。&lt;/p&gt;
&lt;p&gt;2、注意 connect 的写法。connect(mapStateToProps) 的 return 是一个新的函数，它的参数是 PostsPage。&lt;/p&gt;
&lt;p&gt;3、数据从哪里来的？还记得 Provider 的 store 属性吗？下面 mapStateToProps 中的参数 state 就是指数据流上游中的 store 中的 state。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// pages/PostsPage.js
import React from &quot;react&quot;;
import { connect } from &quot;react-redux&quot;;

// Redux state is now in the props of the component
const PostsPage = ({ loading, posts, hasErrors }) =&amp;gt; {
  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;/section&amp;gt;
  );
};

// Map Redux state to React component props
const mapStateToProps = (state) =&amp;gt; ({
  loading: state.posts.loading,
  posts: state.posts.posts,
  hasErrors: state.posts.hasErrors,
});
// Connect Redux to React
export default connect(mapStateToProps)(PostsPage);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;由于此组件使用来自同一 reducer 的状态，因此我们也可以简写为 state =&amp;gt; state.posts 。但是，使用更长的写法也是有意义的：如果需要将多个 reducers 引入同一个组件中，可以明确其内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后，我们将从 actions 中引入异步 fetchPosts ，它将“获取所有文章的整个生命周期”（译者注：就是 fetch 从开始到成功或失败的全过程）合并为一个 action。使用 Reat 的 useEffecct，在组件挂载时调用 dispatch(fetchPosts()) 来更新文章。在已连接的组件上，dispatch 将自动可用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// pages/PostsPage.js
import React, { useEffect } from &quot;react&quot;;
import { connect } from &quot;react-redux&quot;;

// Bring in the asynchronous fetchPosts action
import { fetchPosts } from &quot;../actions/postsActions&quot;;

const PostsPage = ({ dispatch, loading, posts, hasErrors }) =&amp;gt; {
  useEffect(() =&amp;gt; {
    dispatch(fetchPosts());
  }, [dispatch]);

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;/section&amp;gt;
  );
};

const mapStateToProps = (state) =&amp;gt; ({
  loading: state.posts.loading,
  posts: state.posts.posts,
  hasErrors: state.posts.hasErrors,
});

export default connect(mapStateToProps)(PostsPage);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时剩下要做的就是显示页面的所有三种可能状态：无论是正在加载、出现错误还是从 API 成功检索文章。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// pages/PostsPage.js
import React, { useEffect } from &quot;react&quot;;
import { connect } from &quot;react-redux&quot;;

import { fetchPosts } from &quot;../actions/postsActions&quot;;
import { Post } from &quot;../components/Post&quot;;

const PostsPage = ({ dispatch, loading, posts, hasErrors }) =&amp;gt; {
  useEffect(() =&amp;gt; {
    dispatch(fetchPosts());
  }, [dispatch]);

  // Show loading, error, or success state
  const renderPosts = () =&amp;gt; {
    if (loading) return &amp;lt;p&amp;gt;Loading posts...&amp;lt;/p&amp;gt;;
    if (hasErrors) return &amp;lt;p&amp;gt;Unable to display posts.&amp;lt;/p&amp;gt;;
    return posts.map((post) =&amp;gt; &amp;lt;Post key={post.id} post={post} /&amp;gt;);
  };

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
      {renderPosts()}
    &amp;lt;/section&amp;gt;
  );
};

const mapStateToProps = (state) =&amp;gt; ({
  loading: state.posts.loading,
  posts: state.posts.posts,
  hasErrors: state.posts.hasErrors,
});

export default connect(mapStateToProps)(PostsPage);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是全部的内容啦 - 我们现在有一个已连接的组件，并将数据从 API 引入我们的 Redux store。使用 Redux DevTools，我们可以看到每个 action 的记录，以及每个状态更改后的变化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./demo.png&quot; alt=&quot;demo&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;结束语&lt;/h1&gt;
&lt;p&gt;使用纯 Redux 创建 app 的教程到此结束。如果查看 demo 的&lt;a href=&quot;https://codesandbox.io/s/react-redux-application-hewdb&quot;&gt;源代码&lt;/a&gt;，你会发现添加了很多内容 —— 单个文章以及评论的 reducer 和 actions。&lt;/p&gt;
&lt;p&gt;我建议你逐步完善你的 project，以便它能和 demo app 对得上。后文没有新的概念需要学习，只是更灵活的运用技巧。你将创建两个甚至更多的 reducers 和 actions，并了解如何将两个 state 传入一个单文章页面组件中，该组件包含文章数据以及该文章的评论数据。&lt;/p&gt;
&lt;h1&gt;Redux 工具包&lt;/h1&gt;
&lt;p&gt;我还想介绍一件事 —— Redux 工具包。Redux Toolkit (RTK)是一种更新、更简单的官方使用 Redux 的方式。你可能会注意到 Redux 有很多用于设置的模板文件，并且需要比普通 React 更多的文件夹和文件。现在已经出现了一些模式试图轻量化配置，例如 &lt;a href=&quot;https://github.com/erikras/ducks-modular-redux&quot;&gt;Redux ducks 模式&lt;/a&gt;。但其实我们可以进一步简化它。&lt;/p&gt;
&lt;p&gt;查看 &lt;a href=&quot;https://codesandbox.io/s/react-redux-toolkit-application-cbb6s&quot;&gt;Redux Toolkit demo app&lt;/a&gt; 的源代码，这与我们刚刚使用 Redux 创建的 app 相同，但使用的是 RTK。它要简单得多，所有相同功能的代码行数都大大减少。&lt;/p&gt;
&lt;p&gt;使用 RTK 只需要一个依赖项，@reduxjs/toolkit。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i @reduxjs/toolkit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且不再需要您安装 redux-thunk 或 redux-devtools-extension 依赖项。&lt;/p&gt;
&lt;h2&gt;Redux 工具包的优势&lt;/h2&gt;
&lt;p&gt;使用 RTK 的主要优点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更易于设置（更少的依赖）&lt;/li&gt;
&lt;li&gt;减少模板代码（一个 slice vs. 多文件的 actions and reducers）&lt;/li&gt;
&lt;li&gt;明智的默认值（ 内置 Redux Thunk 和 Redux DevTools）&lt;/li&gt;
&lt;li&gt;更小粒度的 state 更新，这意味着不再需要在每个 reducer 中都 return { ...state } 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：第四个优点的原文不太好翻译，这里贴一下原文：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The ability to use &lt;a href=&quot;https://redux-toolkit.js.org/api/createreducer#direct-state-mutation&quot;&gt;direct state mutation&lt;/a&gt;, since RTK uses &lt;a href=&quot;https://github.com/immerjs/immer&quot;&gt;immer&lt;/a&gt; under the hood. This means you no longer need to return { ...state } with every reducer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Store&lt;/h2&gt;
&lt;p&gt;由于 Redux Toolkit 已经内置了很多功能，比如 Redux DevTools 和 Redux Thunk，所以我们不再需要将它们放入 index.js 文件中。现在我们只需要 configureStore ，而不是 createStore 了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// index.js
import React from &quot;react&quot;;
import { render } from &quot;react-dom&quot;;
import { configureStore } from &quot;@reduxjs/toolkit&quot;;
import { Provider } from &quot;react-redux&quot;;

import App from &quot;./App&quot;;
import rootReducer from &quot;./slices&quot;;

import &quot;./index.css&quot;;

const store = configureStore({ reducer: rootReducer });

render(
  &amp;lt;Provider store={store}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/Provider&amp;gt;,
  document.getElementById(&quot;root&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Slices&lt;/h2&gt;
&lt;p&gt;RTK 不是将 “reducers、actions 和所有文件” 作为单独的文件处理并单独创建所有 action types，而是为我们提供了 slices 的概念。slices 自动生成 reducers、action types 和 action creators。因此，您只需创建一个文件夹 - slices 。&lt;/p&gt;
&lt;p&gt;initialState 看起来是一样的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// slices/posts.js
import { createSlice } from &quot;@reduxjs/toolkit&quot;;

export const initialState = {
  loading: false,
  hasErrors: false,
  posts: [],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slices 中 reducers 的名称也将相同 —— getPosts 、 getPostsSuccess 和 getPostsFailure 。我们将进行所有相同的更改，但请注意，我们不再返回整个 state —— 我们只是改变状态。它在 hood 下仍然是不可变的，但这种方法对某些人来说可能更容易、更快捷。如果愿意，你仍然可以将整个 state 作为对象返回。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;译者补充：这里 hood 的理解，和前文第四个优点一样，取查看更多资料吧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// slices/posts.js
// A slice for posts with our three reducers
const postsSlice = createSlice({
  name: &quot;posts&quot;,
  initialState,
  reducers: {
    getPosts: (state) =&amp;gt; {
      state.loading = true;
    },
    getPostsSuccess: (state, { payload }) =&amp;gt; {
      state.posts = payload;
      state.loading = false;
      state.hasErrors = false;
    },
    getPostsFailure: (state) =&amp;gt; {
      state.loading = false;
      state.hasErrors = true;
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的 actions 是相同的，我们只是不必再单独写出它们。从同一个文件中，我们可以导出所有 actions、reducer、异步 thunk 和一个新东西 —— selector。我们将使用 selector 来访问 React 组件中的任何状态，而不是使用 connect 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// slices/posts.js
// Three actions generated from the slice
export const { getPosts, getPostsSuccess, getPostsFailure } =
  postsSlice.actions;

// A selector
export const postsSelector = (state) =&amp;gt; state.posts;

// The reducer
export default postsSlice.reducer;

// Asynchronous thunk action
export function fetchPosts() {
  return async (dispatch) =&amp;gt; {
    dispatch(getPosts());

    try {
      const response = await fetch(
        &quot;https://jsonplaceholder.typicode.com/posts&quot;
      );
      const data = await response.json();

      dispatch(getPostsSuccess(data));
    } catch (error) {
      dispatch(getPostsFailure());
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;译者补充:为了最低运行，还需要加上这个文件。完整代码地址在前文查看，可能需要重新 npm start 一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// slices/index.js
import { combineReducers } from &quot;redux&quot;;

import postsReducer from &quot;./posts&quot;;

const rootReducer = combineReducers({
  posts: postsReducer,
});

export default rootReducer;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;在 React 组件中选择 Redux 状态&lt;/h2&gt;
&lt;p&gt;正如我们刚刚了解到的，传统的方法是与 connect() 函数一起使用 mapStateToProps ，这在代码库中仍然很常见，因此值得了解这种写法。你仍然可以在 RTK 中使用此方法，但较新的 React Hooks 写法是使用 react-redux 提供的 useDispatch 和 useSelector ，这种方法需要的代码较少。&lt;/p&gt;
&lt;p&gt;正如下面的 PostsPage.js 文件中所示，Redux 状态不再连接到组件的 props 上，而是由我们在 slice 中导出的 selector(选择器)提供。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// pages/PostsPage.js
import React, { useEffect } from &quot;react&quot;;
import { useDispatch, useSelector } from &quot;react-redux&quot;;

import { fetchPosts, postsSelector } from &quot;../slices/posts&quot;;

import { Post } from &quot;../components/Post&quot;;

const PostsPage = () =&amp;gt; {
  const dispatch = useDispatch();
  const { posts, loading, hasErrors } = useSelector(postsSelector);

  useEffect(() =&amp;gt; {
    dispatch(fetchPosts());
  }, [dispatch]);

  const renderPosts = () =&amp;gt; {
    if (loading) return &amp;lt;p&amp;gt;Loading posts...&amp;lt;/p&amp;gt;;
    if (hasErrors) return &amp;lt;p&amp;gt;Unable to display posts.&amp;lt;/p&amp;gt;;

    return posts.map((post) =&amp;gt; &amp;lt;Post key={post.id} post={post} excerpt /&amp;gt;);
  };

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
      {renderPosts()}
    &amp;lt;/section&amp;gt;
  );
};

export default PostsPage;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们有和以前一样的应用程序，从 Redux Toolkit 进行了一些更新，并且需要维护的代码少了很多。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;我们成功了！如果你跟我一起读完了整个教程，你现在应该对 Redux 有一个不错的体会，无论是旧式写法还是使用 Redux Toolkit 来简化代码。&lt;/p&gt;
&lt;p&gt;总而言之，Redux 允许我们轻松地在 React 应用程序中管理全局状态。我们可以从任何地方访问和更新状态，并使用 Redux Devtools 轻松调试应用程序的整个状态。&lt;/p&gt;
&lt;p&gt;你可以将应用程序的大部分状态放在 Redux 中，但应用程序的某些区域（例如正在更新的表单）保持 React 组件状态本身仍然有意义。&lt;/p&gt;
&lt;p&gt;希望你喜欢这篇文章！将两个完整的 demo 放在一起并在这里运行整个事情需要做很多工作，这篇文章很长，但希望这是你学习所有初级和中级 Redux 概念的一站式商店。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(译者补充：原文这里是捐款和订阅链接，感兴趣的读者可用前文地址自行访问。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;请让我知道你的想法，如果对你有所帮助，请分享这篇文章，并且随时欢迎捐款！&lt;/p&gt;
</content:encoded></item><item><title>2023 into 2024</title><link>https://0110wdj.github.io/snofly-blog/posts/self-live/23into24/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/self-live/23into24/</guid><pubDate>Sat, 30 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前看到过一个有意思的言论，大意是：人们往往更喜欢做新一年的计划，而不是做旧一年的总结，因为往事不堪回首。&lt;/p&gt;
&lt;p&gt;我寻思了一下，这一年还是可堪回首的。&lt;/p&gt;
&lt;h2&gt;时间线&lt;/h2&gt;
&lt;p&gt;去年十二月可谓是承上启下了，毕竟疫情宣布结束，过年相聚时大家都会聊聊各自的“感冒”经历。&lt;/p&gt;
&lt;p&gt;这次过年没什么不同，吐槽发泄一番，交换一下信息，结束。&lt;/p&gt;
&lt;p&gt;对了，去年十月的“转岗风波”并未结束，我年初就未雨绸缪，开始准备找新工作了。&lt;/p&gt;
&lt;p&gt;二月面试了几家，地理上限定在北京，最后三月初确定了新的工作。&lt;/p&gt;
&lt;p&gt;紧接着就从武汉到北京了。有意思的是，我是在赶飞机的路上，才确定新工作的。&lt;/p&gt;
&lt;p&gt;很开心，北京也有认识的人。&lt;/p&gt;
&lt;p&gt;三月到十二月就是平平无奇的工作了，当然是从超级难受到游刃有余这样的。&lt;/p&gt;
&lt;p&gt;十月份带女友回了趟重庆，感觉任重道远。&lt;/p&gt;
&lt;p&gt;技术上，看了很多资料，获得了极大进步，现在一个吊打一年前的我两个。&lt;/p&gt;
&lt;p&gt;另外补全了 css 基础，因为通读了 《css 权威指南》。&lt;/p&gt;
&lt;p&gt;自认为升级到中级水平了。&lt;/p&gt;
&lt;h2&gt;记忆深刻的事情&lt;/h2&gt;
&lt;h3&gt;换工作&lt;/h3&gt;
&lt;p&gt;毕竟是第一次换工作诶，没想到过程意外的顺利。&lt;/p&gt;
&lt;p&gt;二月陆续面试几家工作，第一次几乎裸面，回想起来真不好意思浪费了别人的时间。&lt;/p&gt;
&lt;p&gt;另一个意外的是，我竟然不是我们小组第一个跑路的。&lt;/p&gt;
&lt;p&gt;还有一个意外的是，赶飞机的下午去公司总部（离机场更近）找同事吃饭，接到了一个 offer。（我原计划想先过去再找工作的）&lt;/p&gt;
&lt;p&gt;陆续走了不少人，后来我才知道有很多做法是严重违反劳动法的。&lt;/p&gt;
&lt;p&gt;大学生又菜又好骗。&lt;/p&gt;
&lt;h3&gt;塑料友谊&lt;/h3&gt;
&lt;p&gt;和在北京的同学一起玩，说好很快下次，结果三月初见了一次面，下次见面竟然在九月底了。&lt;/p&gt;
&lt;p&gt;唔，应该是我太宅了。&lt;/p&gt;
&lt;p&gt;年底了再聚一聚。&lt;/p&gt;
&lt;h3&gt;规范性&lt;/h3&gt;
&lt;p&gt;我在武汉两次换房，都没有退押金，所以我会觉得押金退不了是正常的，于是我选了在线租房平台。&lt;/p&gt;
&lt;p&gt;不得不说，北京这边对规范的遵守确实好一点。&lt;/p&gt;
&lt;p&gt;十一月公司裁员，虽然有点突然，一下走了好多同事，但真的有赔偿啊。&lt;/p&gt;
&lt;h2&gt;新的计划&lt;/h2&gt;
&lt;p&gt;我经常在 QQ 空间发阶段性总结，主要是维护一个关于专业知识学习的任务主线。&lt;/p&gt;
&lt;p&gt;明年要做的事情大概有了计划。&lt;/p&gt;
&lt;h3&gt;关于算法和数学基础&lt;/h3&gt;
&lt;p&gt;一年的时间不一定够。&lt;/p&gt;
&lt;p&gt;本科的时候关注点没在这个上面，导致现在需要挤时间出来。&lt;/p&gt;
&lt;p&gt;总之看看算法导论，刷刷算法题，从计算机的角度去拓展一下视野。&lt;/p&gt;
&lt;h3&gt;关于工作&lt;/h3&gt;
&lt;p&gt;“良禽择木而栖，岂能郁郁久居人下。”&lt;/p&gt;
&lt;p&gt;其实我觉得生活挺好的，没什么追求，能看书进步一点点就很开心了。&lt;/p&gt;
&lt;p&gt;可惜有各种压力，还要为社会做贡献，所以只能尽量发挥自己的价值了。&lt;/p&gt;
&lt;p&gt;“学成文武艺，货与帝王家。”&lt;/p&gt;
&lt;p&gt;能安心看书当然是最好的了。&lt;/p&gt;
&lt;h3&gt;关于身体健康、婚姻&lt;/h3&gt;
&lt;p&gt;有信心就有希望，或者是，有希望就有信心。&lt;/p&gt;
&lt;h2&gt;结束语&lt;/h2&gt;
&lt;p&gt;明年年底再来看看这篇文章吧。&lt;/p&gt;
</content:encoded></item><item><title>2024 into 2025</title><link>https://0110wdj.github.io/snofly-blog/posts/self-live/24into25/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/self-live/24into25/</guid><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;0 前言&lt;/h2&gt;
&lt;p&gt;总的来说，今年是奋斗的一年。&lt;/p&gt;
&lt;p&gt;虽然看起来和去年的处境相当，但是内在积累了更多的力量，各种意义上的。&lt;/p&gt;
&lt;h2&gt;1 时间线&lt;/h2&gt;
&lt;p&gt;年初，春节照旧。&lt;/p&gt;
&lt;p&gt;3 月，前端同事离职，新入职同事补上。&lt;/p&gt;
&lt;p&gt;4 月，开始了解市场环境，总结了大约 25 次经验，并修改了主线学习计划。&lt;/p&gt;
&lt;p&gt;这期间，看了几本书，值得推荐的，列举一下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《Hello 算法》&lt;/li&gt;
&lt;li&gt;《图解 HTTP》&lt;/li&gt;
&lt;li&gt;《深入浅出 Node.js》&lt;/li&gt;
&lt;li&gt;《JavaScript 设计模式与开发实践》&lt;/li&gt;
&lt;li&gt;《Webpack 实战 入门、进阶、调优 第二版》&lt;/li&gt;
&lt;li&gt;《遥远的救世主》&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最忙的时间段在 3-6 月，其他时候就陆陆续续的了。&lt;/p&gt;
&lt;p&gt;6 月，去湖北襄阳参加了一场婚宴，干了点帮忙贴纸的活，顺便思考了下自己的未来。&lt;/p&gt;
&lt;p&gt;7 月，奶奶、姐姐、外甥女来北京，主要陪同参观游玩了天安门、颐和园、环球影城。&lt;/p&gt;
&lt;p&gt;8 月，上涨 18.75% 月薪。&lt;/p&gt;
&lt;p&gt;9 月，和小舅舅见面，了解一下别人的打工人生活。&lt;/p&gt;
&lt;p&gt;10 月，国庆初，老妈五十大寿。老家装修、父母养老等问题，参与计划决策。&lt;/p&gt;
&lt;p&gt;国庆中后期，游览成都、四姑娘山，确似人间仙境。&lt;/p&gt;
&lt;p&gt;10 月 - 11 月，参加健身房私教锻炼。&lt;/p&gt;
&lt;p&gt;12 月，开始摆烂。&lt;/p&gt;
&lt;h2&gt;2 散碎的思考&lt;/h2&gt;
&lt;h3&gt;2.1 关于工作&lt;/h3&gt;
&lt;p&gt;这是今年最重要的事情了，甚至今年的学习计划都是根据业内实践经验来制定的。&lt;/p&gt;
&lt;p&gt;花了大约半年时间，去探索新工作、新学习计划、新的市场行情，虽然最后没有换工作，但是这个经历就是非常值得的积累了。&lt;/p&gt;
&lt;p&gt;目前看来，前端欠缺的只有工程化和性能优化相关的系统性知识了，然后就是服务端的入门了。&lt;/p&gt;
&lt;p&gt;这大概也是明年要完成的部分学习任务。&lt;/p&gt;
&lt;p&gt;春节前还有一项工作内容需要完成，其他随缘。&lt;/p&gt;
&lt;p&gt;技术水平的提高主要体现在，思维角度的拔高，用俯视的角度去设计完成工作、指导学习路线。&lt;/p&gt;
&lt;p&gt;高级工程师，指日可待！&lt;/p&gt;
&lt;h3&gt;2.2 关于出行&lt;/h3&gt;
&lt;p&gt;今年主动出行挺多的，北京香山、植物园，河北大境门长城、张北麻田岭；天津；四川成都、四姑姑娘山、都江堰；湖北襄阳唐城。&lt;/p&gt;
&lt;p&gt;另外还办理了护照，今年没时间了，预计明年会出国游玩。&lt;/p&gt;
&lt;p&gt;在这些游览参观中，印象最深刻的莫过于，成都的悠闲、川西的美景，让人能剥离出俗世烦恼中。&lt;/p&gt;
&lt;p&gt;奶奶来北京那次，可惜的是，没有完全完成“去北京见毛主席”的年初笑谈承诺————排队太久，没赶上毛主席纪念堂展出时间。&lt;/p&gt;
&lt;p&gt;我现在感觉，旅行对我的意义是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看看不一样地方的人的生活方式，对照自己的生活方式，思考有没有值得借鉴的地方；&lt;/li&gt;
&lt;li&gt;欣赏别处的美景，增广见闻、谈资；&lt;/li&gt;
&lt;li&gt;提高眼界高度，真正地俯瞰地图，做到顶天立地。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 关于新事物的探索&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1）积木&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在一次公司活动中得了一套乐高积木，然后就对拼积木产生了兴趣。&lt;/p&gt;
&lt;p&gt;后面陆续玩过一些，快乐点在于：内心的平静、小伙伴快乐传递。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2）switch&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;年中的时候，买了 switch oled 玩游戏，《塞尔达传说》确实很有创意，双人游戏也能促进交流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3）社交技能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我去看了一些视频，感觉自己会变得很社牛。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4）摆地摊&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;冬天到了，我买个一些手套，去摆地摊，但是我太内向了，一双也没卖出去。&lt;/p&gt;
&lt;p&gt;好消息是，成功和路人搭话聊天。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5）兼职媒婆&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;好多单身的同龄人，于是，有机会的时候就会很努力地去推荐——手动吃瓜。&lt;/p&gt;
&lt;h2&gt;3 新一年的计划&lt;/h2&gt;
&lt;p&gt;今年没做完的事情，明年继续。&lt;/p&gt;
&lt;p&gt;明年估计也做不完，得后年去了。&lt;/p&gt;
&lt;p&gt;根据学习计划，得 2026 年到 2027 年才能完成。&lt;/p&gt;
&lt;p&gt;工作的话，暂时是稳定的，且稳中向好。&lt;/p&gt;
&lt;h2&gt;4 总结&lt;/h2&gt;
&lt;p&gt;今年上半年，最辛苦的那三个月，真就是没日没夜的学呀，就像第二天就要考试了一样 —— 从紧迫感的角度来看，确实是一样的。&lt;/p&gt;
&lt;p&gt;下半年就比较平缓了，应该是因为涨薪，导致很多事情都不那么急迫了。&lt;/p&gt;
&lt;p&gt;还有个没说的事情，12 月摆烂了两三周，现在不好分析原因，只能说表象是“逃避现实”。&lt;/p&gt;
&lt;p&gt;瑕不掩瑜，总的来说，今年还是奋斗的一年。&lt;/p&gt;
</content:encoded></item><item><title>2023 前端面试经验</title><link>https://0110wdj.github.io/snofly-blog/posts/self-live/lookjob-2023/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/self-live/lookjob-2023/</guid><pubDate>Mon, 06 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;大约来说，就是《关于毕业不到两年却有两年 &lt;s&gt;划水&lt;/s&gt; 工作经验的卑微初级 web 前端程序员在一个月内完成从武汉跳槽到北京继续认真 &lt;s&gt;划水&lt;/s&gt; 工作这件事》。&lt;/p&gt;
&lt;p&gt;咳咳。&lt;/p&gt;
&lt;p&gt;就是说一下关于找工作的整个过程，以及给迷茫的读者一点点建议。&lt;/p&gt;
&lt;h1&gt;正文（正经的文）&lt;/h1&gt;
&lt;p&gt;找工作的几个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编写简历&lt;/li&gt;
&lt;li&gt;投递简历&lt;/li&gt;
&lt;li&gt;笔试、面试&lt;/li&gt;
&lt;li&gt;谈 offer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，我自己写简历、投简历和谈 offer 的过程都比较简洁，主要时间花费在笔试和面试上了。&lt;/p&gt;
&lt;h2&gt;编写简历&lt;/h2&gt;
&lt;p&gt;招聘相关的 APP 都会提供简历编写模板，自己挑一个好看的就行。&lt;/p&gt;
&lt;p&gt;至于具体写什么内容，我觉得能“让自己感觉到自信”就行了——（哟！我其实还不错嘛！）。&lt;/p&gt;
&lt;p&gt;这里推荐一篇关于写简历的文章，阮一峰的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2020/01/technical-resume.html&quot;&gt;《如何写一份有效的技术简历？》&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这篇文章的问题在于，初级工程师就一两个项目经历。如果写不出实践经验，那就多写点自己能干啥吧。&lt;/p&gt;
&lt;h2&gt;投递简历&lt;/h2&gt;
&lt;p&gt;部分企业可以从官网投递简历，部分认识的人可以内推一下，其他公司就从招聘软件里面看就行了。&lt;/p&gt;
&lt;p&gt;主要看看工作环境、工资待遇、工作内容，自己或公司的晋升前途。&lt;/p&gt;
&lt;p&gt;不要怕“已读不回”，换一家就行了。&lt;/p&gt;
&lt;h2&gt;笔试面试&lt;/h2&gt;
&lt;p&gt;这个我强烈建议，开始的时候一周两次面试就足够了。&lt;/p&gt;
&lt;p&gt;我们要“以战养战”，从面试过程中吸取经验。&lt;/p&gt;
&lt;p&gt;逐渐熟悉之后，就能不慌不忙了。&lt;/p&gt;
&lt;p&gt;与一般人说的不同，我认为预约面试的公司都应该是自己想去的公司，这样能让自己更加重视。&lt;/p&gt;
&lt;p&gt;如果面试结束后后悔没准备好，那么这种遗憾情绪能促进自己去好好准备下一场面试。&lt;/p&gt;
&lt;p&gt;当然，也是不浪费大家的时间。&lt;/p&gt;
&lt;h2&gt;谈 offer&lt;/h2&gt;
&lt;p&gt;我自己不懂怎么谈，也不会讨价还价。&lt;/p&gt;
&lt;p&gt;反正我投递简历的时候就设定了薪资待遇的下限，低于下限直接 pass 掉换一家。&lt;/p&gt;
&lt;p&gt;总之，我谈 offer 的时候大概就是 hr 说得噼里啪啦，我回答嗯嗯啊啊。&lt;/p&gt;
&lt;h1&gt;正文其二&lt;/h1&gt;
&lt;p&gt;从我经历来说，2 月份找工作，一共联系面试或笔试了 6 家公司。&lt;/p&gt;
&lt;p&gt;第一周 1 家，第二周 2 家，第三周 3 家。&lt;/p&gt;
&lt;p&gt;第一次是没有准备就去面试的，因为我之前研究了很久的基础理论，有点自信了，同时也不知道要准备什么。&lt;/p&gt;
&lt;p&gt;然后被打击得很惨，就像苍蝇被拍在墙上，就像身体被火箭射出地球。&lt;/p&gt;
&lt;p&gt;不过这也让我拿到了一份“历年真题”，以及现在面试问题的主流倾向。&lt;/p&gt;
&lt;p&gt;之后就是复盘一下面试问题，然后解决，最后看看出题思路。&lt;/p&gt;
&lt;p&gt;到第二周的时候，重复如此。&lt;/p&gt;
&lt;p&gt;到第三周的时候，已经能够侃侃而谈、滔滔不绝了。&lt;/p&gt;
&lt;p&gt;当然，这里有我平时积累的功劳，所以才能短时间内理解大量原来不知道的知识。&lt;/p&gt;
&lt;p&gt;然后新工作的事情就水到渠成了。&lt;/p&gt;
&lt;h1&gt;正文其三&lt;/h1&gt;
&lt;p&gt;这里是我之前记录下来的面试或笔试题目，可以参考一下。&lt;/p&gt;
&lt;p&gt;投递的岗位都是 北京 React 前端开发 1~3 年 ** k。&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;/snofly-blog/files/2023.docx&quot; target=&quot;_blank&quot;&amp;gt;下载 2023 面试题目&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;关于题目的答案，这里推荐两个网站：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vue3js.cn/interview/&quot;&gt;常见面试题集合&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bigfrontend.dev/zh&quot;&gt;大前端面试和笔试&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个网站是总结得比较好的，然后其他的没收集的问题，在网上检索就行。&lt;/p&gt;
&lt;p&gt;注意：问题都是冰上一角，还要有知识底蕴才好去理解。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;很多人遇到的问题就是：简历没有亮点、投了简历也没有约面试、约了面试也答不出来问题、开始谈 offer 不敢要价。&lt;/p&gt;
&lt;p&gt;我想说的是，“把握自己的核心需求”，想想到底想要什么，然后坚持学习，不管是不是面试时期。&lt;/p&gt;
</content:encoded></item><item><title>软件测试与质量保证笔记</title><link>https://0110wdj.github.io/snofly-blog/posts/test/basic/</link><guid isPermaLink="true">https://0110wdj.github.io/snofly-blog/posts/test/basic/</guid><pubDate>Fri, 15 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;一、概述引论&lt;/h1&gt;
&lt;h2&gt;（一）缺陷&lt;/h2&gt;
&lt;h3&gt;1、缺陷定义&lt;/h3&gt;
&lt;p&gt;缺陷：最终产品同用户的期望不一致。
1、功能错误
2、功能遗漏
3、超出需求的部分
4、性能不符合要求&lt;/p&gt;
&lt;h3&gt;2、缺陷产生原因&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;732f10ea9099a16fbb63ee352438f726.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3、解决办法&lt;/h3&gt;
&lt;p&gt;通过&lt;strong&gt;软件测试&lt;/strong&gt;尽可能减少缺陷。&lt;/p&gt;
&lt;h2&gt;（二）软件测试&lt;/h2&gt;
&lt;h3&gt;1、软件测试定义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;传统意义上的测试定义&lt;/strong&gt;，是狭义的。
1979 年 Glenford J. Myers 在《软件测试艺术》（The art of software testing）书中给出的定义：
软件测试是为了发现软件缺陷而执行程序或系统的过程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1983 年 IEEE 的定义&lt;/strong&gt; ：
使用人工或自动手段运行或测定某个系统的过程，其目的在于检验它是否满足规定的需求或是弄清预期结果与实际结果之间的差别。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件测试也可以定义&lt;/strong&gt;为是“&lt;strong&gt;验证（Verification）&lt;/strong&gt;”和“&lt;strong&gt;有效性确认（Validation）&lt;/strong&gt;”活动构成的整体。
&lt;strong&gt;验证&lt;/strong&gt;：是检验软件是否已正确地实现了产品规格书所定义的系统功能和特性。
&lt;strong&gt;有效性确认&lt;/strong&gt;：是确认所开发的软件是否满足用户真正需求的活动。&lt;/p&gt;
&lt;h3&gt;2、区分缺陷、故障、失效&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;1e4806cd95bed2fc4fb9d724b345509b.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;二、软件测试基础&lt;/h1&gt;
&lt;h2&gt;（一）软件测试的原则&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1.所有的测试都应追溯到用户的需求&lt;/strong&gt;
系统中最严重的错误是那些导致程序无法满足用户需求的错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.尽早地和不断地进行软件测试&lt;/strong&gt;
问题发现越早，解决问题的代价就越小。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.不可能完全的测试&lt;/strong&gt;
输入量太大
执行路径太多
软件测试最致命的缺陷就是：不能进行彻底的测试&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4 .Pareto 原则&lt;/strong&gt;
Pareto 原则暗示着测试发现的错误中的 80%很可能起源于程序模块中的 20%。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5.软件缺陷的寄生虫性&lt;/strong&gt;
找到的软件缺陷越多，就说明软件缺陷越多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6.避免测试自己的程序&lt;/strong&gt;
程序员轻易不会承认自己写的程序有错误；
程序员的测试思路有局限性，做测试时很容易受到编程思路的影响；
程序员测试不具有典型性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7.设计周密的测试用例&lt;/strong&gt;
软件测试的本质就是针对要测试的内容确定一组测试用例。
测试用例至少应包括：
a 执行测试用例前，应满足的前提条件
b 输入
c 预期输出
设计测试用例时，应当包括合理的输入条件和不合理的输入条件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8.回归测试&lt;/strong&gt;
程序修改后必须进行回归测试，避免引入新的错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9.严格执行测试计划，排除测试的随意性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10.确认 BUG 的有效性&lt;/strong&gt;
对测试错误结果一定要有一个确认的过程。
有时候测试人员提交的 BUG 并不是真正的 BUG。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;11.妥善保存测试计划，测试用例，出错统计和最终分析报告，为维护提供方便。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;（二）软件质量保证&lt;/h2&gt;
&lt;h3&gt;1、质量定义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ISO 9000&lt;/strong&gt; ：质量是一组固有特性符合需求的程度。
&lt;strong&gt;IEEE 的定义&lt;/strong&gt;：
系统、部件或过程满足明确需求的程度。
系统、部件或过程满足客户或用户需要或期望的程度。
&lt;strong&gt;六标准差（6σ）&lt;/strong&gt;：10^6^次测试中的错误次数。
&lt;strong&gt;RUP 的定义&lt;/strong&gt;：满足或超出认定的一组需求，并使用经过认可的评测方法和标准来评估，还使用认定的流程来生产。&lt;/p&gt;
&lt;h3&gt;2、软件质量的定义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ISO 14598-1999 定义&lt;/strong&gt;：软件特性的总和，软件满足规定或潜在用户需求的能力。
&lt;strong&gt;ISO 9126-2001 定义&lt;/strong&gt;：软件满足用户规定或潜在用户需求的能力，要从软件在内部，外部和使用过程中的表现来衡量，包含内部质量、外部质量、和使用质量。&lt;/p&gt;
&lt;h3&gt;3、质量的不同层面&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;符合性质量&lt;/strong&gt;：能够满足国家或行业标准、产品规范的要求，最初的质量观念。
&lt;strong&gt;适用性质量&lt;/strong&gt;：让客户满意，不仅满足标准、规范的要求，而且满足客户的其他要求。
&lt;strong&gt;广义质量&lt;/strong&gt;：不仅要让客户满意，还要让客户愉快，也就是，想在客户的前面，超出客户的希望。&lt;/p&gt;
&lt;p&gt;### 4、软件质量属性（特性）的定义
定义：软件产品、过程或与质量要求有关的固有属性 。
1、软件质量是许多质量属性的综合体现，各种质量属性反映了软件质量的方方面面。
2、质量要求只有转化为有指标的质量特性，质量才可以度量。
3、通过改善软件的各种质量属性，从而提高软件的整体质量。&lt;/p&gt;
&lt;h3&gt;5、软件质量保证（SQA）的定义&lt;/h3&gt;
&lt;p&gt;建立一套有计划，有系统的方法，来向管理层保证拟定出的标准、步骤、实践和方法能够正确地被所有项目所采用。
1、它使软件开发过程对于管理人员来说是可见的。
1、它通过对软件产品和活动进行评审来验证软件是合乎标准的。&lt;/p&gt;
&lt;h3&gt;6、软件质量保证方法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;产品质量&lt;/strong&gt;：
工作成果产生时(马上)进行质量检查，及时找出并消除工作成果中的缺陷&lt;br /&gt;
1、技术评审
2、软件测试&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;过程质量&lt;/strong&gt;：
开发过程中有效地预防工作成果产生缺陷，将高质量内建于开发过程。&lt;br /&gt;
过程规范建立
1、过程检查
2、软件过程改进&lt;/p&gt;
&lt;h2&gt;（三）测试模型与基础&lt;/h2&gt;
&lt;h3&gt;1、测试层次的传统观点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;a73ad66b16a9023e244c97b54c1e9c08.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;2f2bdb074695145dff6984e2590a78f4.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、软件测试模型&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;8d366106f83bb09c58d0fb7018345261.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;6363b587aab91a50faec0f205d169a4f.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;a6eea2592e52925d0a9eedd44d6fbaeb.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;a1af90d1faedf5e64e303c348cad7d40.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3、软件测试流程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1、测试计划&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试计划&lt;/strong&gt;是对每个产品，或是对各个开发阶段的产品开展测试的策略。
一个完整的测试计划应该包含以下几个方面：
a 对测试范围的界定；
b 风险的确定；
c 资源的规划；
d 时间表的制定；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、测试设计&lt;/strong&gt;
&lt;strong&gt;测试设计&lt;/strong&gt;阶段要设计测试用例和测试过程，要保证测试用例完全覆盖测试需求。
测试设计阶段最重要的是如何将测试需求分解，如何设计测试用例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、测试开发&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(1).准备测试环境
a 测试技术准备
b 配置软件、硬件环境
c 人员&lt;/p&gt;
&lt;p&gt;(2).获得测试数据&lt;br /&gt;
a 正常事务的测试
b 使用无效数据的测试&lt;/p&gt;
&lt;p&gt;(3).测试脚本
所谓脚本，是完整的一系列相关终端的活动。
一般测试脚本有 5 个级别：
a 单元脚本
b 并发脚本
c 集成脚本
d 回归脚本
e 强度/性能脚本&lt;/p&gt;
&lt;p&gt;(4).辅助测试工具
a 优秀的办公处理软件
b 错误跟踪系统
c 自动测试工具
d 软件分析工具
e 好的操作系统
f 多样化平台&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4、测试执行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试执行&lt;/strong&gt;的过程由 4 个部分组成：
输入：要完成工作所必须的入口标准或可交付的结果。
执行过程：从输入到输出的过程或工作任务。
检查过程：确定输出是否满足标准的处理过程。
输出：推出标准或工作流程产生的可交付的结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5、评估测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;软件测试的主要评测方法包括：
(1) 覆盖评测
最常用的覆盖评测是基于需求的测试覆盖和基于代码的测试覆盖。&lt;/p&gt;
&lt;p&gt;(2)质量评测
在测试过程中，已发现缺陷的评估提供了最佳的软件质量指标。&lt;/p&gt;
&lt;p&gt;(3)性能评测
评估测试对象的性能时，侧重于获取与行为相关的数据，如响应时间、事务处理数、内存占用率、操作可靠性等。&lt;/p&gt;
&lt;h1&gt;三、软件测试方法与技术&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;主流测试技术&lt;/strong&gt;
&lt;img src=&quot;4a391c02972f8e69c0f1eae5d15adb12.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（一）黑盒测试之等价类划分&lt;/h2&gt;
&lt;h3&gt;1、技术概述&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;7bca939434f68865dae22dbf812a3e81.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、黑盒测试的目标&lt;/h3&gt;
&lt;p&gt;黒盒测试试图发现以下类型的错误：
1、功能错误或遗漏；
2、界面错误，不美观；
3、外部信息访问错误；
4、初始化和终止错误；
5、在接口上，能否正确地接受输入数据，能否产生正确地输出信息；
6、性能错误；&lt;/p&gt;
&lt;h3&gt;3、等价类划分 A&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;等价类划分法&lt;/strong&gt;：
依据需求对输入的范围进行细分，然后再分出的每一个区域内选取一个有代表性的测试数据开展测试。
&lt;strong&gt;有效等价类&lt;/strong&gt;：
符合需求说明，合理地输入数据集合。
&lt;strong&gt;无效等价类&lt;/strong&gt;：
不符合需求说明，无意义地输入数据的集合。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;
&lt;img src=&quot;2cb13ce56b87aef42efe3ad68753ce93.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4、等价类划分 B&lt;/h3&gt;
&lt;p&gt;单缺陷与多缺陷假设产生弱等价类与强等价类测试之分；
是否进行无效数据的处理产生健壮与一般等价类测试之分。&lt;/p&gt;
&lt;p&gt;于是有：&lt;/p&gt;
&lt;p&gt;1、弱一般等价类测试
测试用例设计：
选取的测试用例覆盖所有的有效等价类（只有一个缺陷）。&lt;/p&gt;
&lt;p&gt;2、强一般等价类测试
测试用例设计：
将每个变量的有效等价类做笛卡尔积，设计测试用例覆盖笛卡尔积的每个元素。&lt;/p&gt;
&lt;p&gt;3、弱健壮等价类测试
测试用例设计：
对有效输入，使用每个有效等价类的一个值。
对无效输入，测试用例将拥有一个无效值，并保持其余的值都是有效的。&lt;/p&gt;
&lt;p&gt;4、强健壮等价类测试
从所有等价类的笛卡尔积的每一个元素中获得测试用例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“单缺陷”假设&lt;/strong&gt;
失效极少是由两个或多个缺陷的同时发生引起的。&lt;/p&gt;
&lt;h3&gt;5、等价类测试指导方针和观察&lt;/h3&gt;
&lt;p&gt;1）弱形式不如对应的强形式的测试全面。
2）如果实现语言是强类型，则没有必要使用健壮形式的测试。
3）如果错误条件非常重要，则进行健壮形式的测试。
4）强等价类测试假设变量是独立的，相应的测试用例相乘会引起冗余问题。如果存在依赖关系，则常常会生成错误测试用例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;1f5169ee71fe3042576f6fcb238852e2.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（二）黑盒测试之边界值测试&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;功能性测试的基本观点&lt;/strong&gt;是：
任何程序都可以看作是将输入定义域取值映射到输出值域的函数。&lt;/p&gt;
&lt;h3&gt;1、边界值分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;边界值分析(Boundary Value Analysis)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本原理&lt;/strong&gt;：
边界值分析关注输入空间的边界，并从中标识测试用例。
错误更可能出现在输入变量的极值附近。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本思想&lt;/strong&gt;
在最小值、略高于最小值、正常值、略低于最大值和最大值处取输入变量值；表示方法为 min、min+、nom、max-、和 max。
单缺陷”假设是边界值分析的关键假设。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;获得测试用例&lt;/strong&gt;的方法：
使所有变量取正常值，只使一个变量取极值。&lt;/p&gt;
&lt;p&gt;归纳基本边界值分析方法
a 通过变量的个数
b 通过变量的取值范围（类型）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;/strong&gt;
&lt;img src=&quot;ecffd031a6ecdc65a8dea317f5069887.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、健壮性测试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;健壮性&lt;/strong&gt;是指在异常情况下，软件还能正常运行的能力。
健壮性有两层含义：
a 容错能力
b 恢复能力&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本思想&lt;/strong&gt;
健壮性测试是边界值分析的一种简单扩展，除了使用五个边界值分别取值，还要采用:
一个略超过最大值(max+)的取值
一个略小于最小值(min-)的取值&lt;/p&gt;
&lt;p&gt;健壮性测试的&lt;strong&gt;主要价值&lt;/strong&gt;是观察异常情况的处理。
软件质量要素的衡量标准：软件的容错性；
软件容错性的度量:从非法输入中恢复。&lt;/p&gt;
&lt;h3&gt;3、最坏情况测试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;基本思想&lt;/strong&gt;
边界值测试分析采用了可靠性理论的单缺陷假设；
最坏情况测试&lt;strong&gt;拒绝&lt;/strong&gt;这种假设，关心当多个变量取极值时会出现的情况。&lt;/p&gt;
&lt;p&gt;对每一个变量首先进行包含最小值、略高于最小值、正常值、略低于最大值、最大值五个元素集合的测试，然后对这些集合进行笛卡尔积计算，以生成测试用例。&lt;/p&gt;
&lt;h3&gt;4、随机测试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;基本思想&lt;/strong&gt;
不是永远选取有界变量的最小值、略高于最小值、正常值、略低于最大值和最大值，而是使用随机数生成器选出测试用例值。&lt;/p&gt;
&lt;h3&gt;5、边界值分析应遵循的原则&lt;/h3&gt;
&lt;p&gt;1）如果输入条件规定了取值范围，或规定了值的个数，测试用例选择：范围的边界内，刚刚超出范围的边界外的值；或者说：最小值、稍高于最小值、正常值、稍低于最大值、最大值。
2）针对规约的每个输出条件使用 1）原则。
3）如果规约中提到的输入输出是个有序的集合（如顺序文件、有序表等），就应注意选取有序集中的第一个和最后一个元素作为测试用例。
4）分析软件规约，找出其它的可能边界条件。&lt;/p&gt;
&lt;h2&gt;（三）黑盒测试之决策表法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;/strong&gt;
决策表法能把&lt;strong&gt;复杂逻辑关系&lt;/strong&gt;和&lt;strong&gt;多条件组合&lt;/strong&gt;情况表达得较明确。
适用：在程序中，若输入输出较多，且相互制约的条件较多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;决策表组成&lt;/strong&gt;
&lt;img src=&quot;bf0d346d36a1acf8b20a14619db526e8.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;任何一个条件组合的特定取值及其相应要执行的操作称为规则；
在判定表中贯穿条件项和动作项的一列就是一条规则；
判定表中列出多少组条件取值，即有多少条规则，即条件项和动作项有多少列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤总结&lt;/strong&gt;
&lt;img src=&quot;d0fedfec20c6a07addf9c85b1ab48eeb.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;strong&gt;优缺点&lt;/strong&gt;.
优点：它能把复杂的问题按各种可能的情况一一列举出来，简明而易于理解，也可避免遗漏。
缺点：不能表达重复执行的动作，例如循环结构。&lt;/p&gt;
&lt;h2&gt;（四）黑盒测试之因果图法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;概述&lt;/strong&gt;
&lt;img src=&quot;6620d80f30f4e2e7a765c7d5167c6836.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;b80c1fcc3e1930edd411b029f091b4ff.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;img src=&quot;2ec18cabf29fd081b72bfdf7def27753.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算步骤&lt;/strong&gt;
&lt;img src=&quot;86af0213a2e416efca6e702a0373946c.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
应用场合：
当软件的输入条件过多时，可以考虑输入的所有排列组合情况，考虑条件之间和条件结果之间关系，防止遗漏。
局限性：
测试用例数目可能会很大，不便于维护。&lt;/p&gt;
&lt;h2&gt;（五）白盒测试&lt;/h2&gt;
&lt;h3&gt;1、白盒测试概念&lt;/h3&gt;
&lt;p&gt;白盒测试又叫&lt;strong&gt;结构测试&lt;/strong&gt;，逻辑驱动测试或基于程序本身的测试。&lt;/p&gt;
&lt;h3&gt;2、基于控制流的测试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;控制流图：&lt;/strong&gt;
控制流图是退化的程序流程图，图中每个处理都退化成一个结点，流线变成连接不同结点的有向弧。
控制流图将程序流程图中结构化构件改用一般有向图的形式表示。
在控制流图中用圆“○”表示节点，一个圆代表一条或多条语句。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;3e74147fe7b0fefef3adf53e6219a136.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;圈复杂度：是一种为程序逻辑复杂性提供定量测度的软件度量，将该度量用于计算程序的基本的独立路径数目。
独立路径必须包含一条在定义之前不曾用到的边。（每一条新的路径都包含了一条新边）&lt;/p&gt;
&lt;p&gt;程序的环路复杂性即 McCabe 复杂性度量，简单的定义为控制流图的区域数。
程序环路复杂性又叫圈复杂度。
&lt;img src=&quot;1d3fb8912424849dee0dab9dbca70726.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;逻辑覆盖&lt;/strong&gt;
&lt;strong&gt;路径测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;b56a8ca68aa7a12a314cc392302e2f14.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;路径覆盖的困难：太复杂。&lt;/p&gt;
&lt;p&gt;如果把覆盖的路径数压缩到一定限度内，例如程序中的循环体只执行零次和一次，就成为基本路径测试。&lt;/p&gt;
&lt;p&gt;通过分析程序控制流图的环路的复杂性，导出基本路径集合，从而设计测试用例，保证这些路径至少通过一次。&lt;/p&gt;
&lt;h3&gt;3、循环测试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;简单循环测试&lt;/strong&gt;
&lt;img src=&quot;dced59cf6e3887f8a68857a5139bf897.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;嵌套循环测试&lt;/strong&gt;
&lt;img src=&quot;d6b3831ab1e6ca5ef9ac54ccd8fa059a.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;串接循环&lt;/strong&gt;
&lt;img src=&quot;32bb7e53dd3eca4c74f7cb72e2a39ce7.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;非结构循环&lt;/strong&gt;
不能测试，尽量重新设计给结构化的程序结构后再进行测试。&lt;/p&gt;
&lt;h3&gt;4、基于数据流的测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;1d80bff0ec6cc75e6404c8e5387d2526.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
DD 路径：决策对决策路径（Miller）。从决策语句的“出路”开始，到下一个决策语句的“入路”结束。
DD 链：有向图中一条起始和终止节点不同点路径。
&lt;img src=&quot;c47c24b2dc0c1c4583a517bdb96a41b9.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（六）单元测试&lt;/h2&gt;
&lt;h3&gt;1、基本概念&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;单元测试&lt;/strong&gt;：是对软件基本组成单元进行的测试。是检验程序最小单位，即检查模块有无错误，它是在编码完成后必须进行的测试工作。&lt;/p&gt;
&lt;p&gt;地位：
&lt;img src=&quot;08008333bd577fede18f7803f68790a6.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、主要任务&lt;/h3&gt;
&lt;p&gt;单元测试针对每个程序的模块，主要测试如下 5 方面：&lt;br /&gt;
&lt;strong&gt;模块接口&lt;/strong&gt;
对模块接口进行测试，检查进出程序单元的数据流是否正确。须在其它测试之前进行。
&lt;strong&gt;局部数据结构&lt;/strong&gt;
测试模块内部的数据能否保持完整性，包括内部数据的内容、形式及相互关系不发生错误。
&lt;strong&gt;边界条件&lt;/strong&gt;
采用边界值分析方法设计测试用例，重点关注程序边界处。
&lt;strong&gt;独立的路径（路径测试）&lt;/strong&gt;
针对程序路径进行测试，尤为重要。重点关注由于计算错误、不正确的判定或不正常的控制流而产生的错误。
&lt;strong&gt;错误处理（出错处理）&lt;/strong&gt;
重点关注模块在工作中发生错误时，出错处理设施是否有效。&lt;/p&gt;
&lt;h3&gt;3、执行过程&lt;/h3&gt;
&lt;p&gt;模拟一个大环境中的情况
&lt;img src=&quot;aeec846cf760e307c8622eec47228598.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;996fa0d46d8a71f90772b41efe7e9ce5.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;（七）集成测试&lt;/h2&gt;
&lt;h3&gt;1、概述&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;集成测试(Integration test)&lt;/strong&gt;
也叫组装测试或联合测试，是在单元测试的基础上，将所有模块按照设计要求集成为系统或子系统，并进行测试。用于验证程序和概要设计说明的一致性。&lt;/p&gt;
&lt;h3&gt;2、基于分解的集成&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;非渐增式集成&lt;/strong&gt;
&lt;strong&gt;定义&lt;/strong&gt;：
又叫大爆炸集成(Big-bang Integration)，把所有通过了单元测试的模块按设计要求，一次全部组装起来，然后进行整体测试。
&lt;strong&gt;目的&lt;/strong&gt;：
尽可能缩短测试时间，使用最少的测试用例验证系统。
&lt;strong&gt;特点&lt;/strong&gt;：
急于求成。
&lt;strong&gt;具体方法&lt;/strong&gt;：
假设要对某个系统的部分功能进行测试，其功能分解如图所示：
&lt;img src=&quot;cd9702ce7839878df8dce5957f2e94a7.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;渐增式集成&lt;/strong&gt;
&lt;strong&gt;定义&lt;/strong&gt;：
从一个模块开始，测一次添加一个模块，边组装边测试，以发现与接口相联系的问题。
&lt;strong&gt;集成方式&lt;/strong&gt;：
1、自顶向下集成
&lt;img src=&quot;4ab471c83ed29d6afae77b65410f1168.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;8862d15d124f06470f3a3c8612b60414.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
2、自底向上集成
&lt;img src=&quot;712f1fa9b97dd675af3681261be6eade.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;71dfc98e7da1114de27aeeda529f21da.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
适用范围：
底层模块接口比较稳定的产品；
高层模块接口变更比较频繁的产品；
底层模块开发和单元测试工作完成较早的产品。&lt;/p&gt;
&lt;p&gt;3、三明治集成
&lt;img src=&quot;cbdf716c87c3fce3ba767d9dd5566a43.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
一般对软件结构的上层使用自顶向下结合的方法；
对下层使用自底向上结合的方法；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;a76e855f0e252905ad31847fc3b75b52.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3、基于调用图的集成&lt;/h3&gt;
&lt;p&gt;基于调用图的集成有两种：
&lt;strong&gt;成对集成&lt;/strong&gt;
&lt;strong&gt;相邻集成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、&lt;strong&gt;成对集成&lt;/strong&gt;
成对集成的&lt;strong&gt;思想&lt;/strong&gt;就是免除桩/驱动器开发工作，使用实际代码来代替桩/驱动器。
成对集成的&lt;strong&gt;方法&lt;/strong&gt;就是对应调用图的每一个边建立并执行一个集成测试会话。
2、&lt;strong&gt;相邻集成&lt;/strong&gt;
相邻节点：有向图中，节点邻居包括所有直接前驱节点和直接后继节点 。
相邻集成：
可大大降低集成测试的会话数量，并可避免桩和驱动器的开发。
相邻集成本质上是三明治集成。
相邻集成具有“中爆炸”集成的缺陷：隔离困难。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;b3dccddb0433bdac8827ba3a8c13a82b.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;4、基于路径的集成&lt;/h4&gt;
&lt;p&gt;1、基本概念
MM-路径
Paul C.Jorgensen 提出，Message Method；
MM-路径是穿插出现&lt;strong&gt;模块执行路径&lt;/strong&gt;和&lt;strong&gt;消息&lt;/strong&gt;的序列；
给定一组单元，其 MM-路径图是一种有向图，其中节点表示&lt;strong&gt;模块执行路径&lt;/strong&gt;，边表示&lt;strong&gt;消息和单元之间的返回&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;注意：MM-路径&lt;strong&gt;不是可执行路径&lt;/strong&gt;，描述单元之间的控制转移。
对于传统软件来说，MM-路径永远是从主程序开始，在主程序中结束。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;b44f6c69a9de65b7609da84479b6cd27.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;ba1f2ea39752f4128e592741cf770f75.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;img src=&quot;29d4985f1a08d3a64ca0c35d06e8bc0d.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
2、MM-路径复杂度&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;521ba6d126246e5b12935161d07bf12d.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
MM-路径复杂度：
V（G）= e – n + 2p
V（G）= e – n + 2
（对于结构化过程代码，p=1）&lt;/p&gt;
&lt;p&gt;优点：
MM-路径是功能性测试和结构性测试的一种混合。
基于路径的集成测试既适用于采用传统瀑布过程开发的软件，也适用于采用基于合成可选的生命周期模型开发的软件。
缺点：
需要更多的工作量标识 MM-路径。&lt;/p&gt;
&lt;h2&gt;（八）系统测试&lt;/h2&gt;
&lt;h3&gt;1、基本概念&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;系统测试&lt;/strong&gt;：
将整个软件系统看做一个整体进行测试，包括对功能、性能等，以及将计算机硬件、某些支持软件、 数据和人员等系统元素结合起来，在实际运行环境下对软件进行测试。&lt;/p&gt;
&lt;h3&gt;2、测试内容&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;3db59d06174b080a6b51c23ba66e8ccf.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
1、&lt;strong&gt;功能测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;功能测试&lt;/strong&gt;主要是对产品的各功能点进行验证。根据&lt;strong&gt;需求规格说明书&lt;/strong&gt;和&lt;strong&gt;功能测试用例&lt;/strong&gt;，逐项测试以检查产品是否达到用户的要求。
2、&lt;strong&gt;界面测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思考范围
风格（主色调、背景）
正确性（标志、文字、图片、弹出的提示信息）
一致性（单一页面、多个页面）
合理性（布局、工具栏图标、放大缩放时）
美观协调
等等&lt;/p&gt;
&lt;p&gt;3、&lt;strong&gt;易用性测试&lt;/strong&gt;
&lt;strong&gt;易用性测试&lt;/strong&gt;：从软件的&lt;strong&gt;使用合理性&lt;/strong&gt;和&lt;strong&gt;方便性&lt;/strong&gt;等角度对软件系统进行检查，来发现软件不方便用户使用的地方。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;易见 Easy to discover&lt;/strong&gt;：单单凭观察，用户就应知道设备的状态，该设备供选择可以采取的行动。
&lt;strong&gt;易学 Easy to learn&lt;/strong&gt;：不通过帮助文件或通过简单的帮助文件，用户就能对一个陌生的产品有清晰的认识。
&lt;strong&gt;易用 Easy to use&lt;/strong&gt;：用户不翻阅手册就能使用软件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;bdc8160c37b7fb28057ce21c93d20ddc.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
4、&lt;strong&gt;兼容性测试&lt;/strong&gt;
&lt;img src=&quot;9c6e12df2b0e2173a687638e4f141304.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;软件兼容性&lt;/strong&gt;：
一般来说，兼容性指能同时容纳多个方面，在计算机术语上兼容是指几个硬件之间、几个软件之间或是软硬件之间的相互配合程度。
&lt;strong&gt;兼容性测试&lt;/strong&gt;：
指测试软件在特定的硬件平台上、不同的应用软件之间、不同的操作系统平台上、不同的网络等环境中是否能很好地运行的测试。
&lt;strong&gt;配置测试&lt;/strong&gt;：
配置测试是指验证在不同的硬件配置和软件配置下，应用程序能否正常工作。&lt;/p&gt;
&lt;h2&gt;（九）Web 系统测试&lt;/h2&gt;
&lt;h3&gt;Web 系统的测试范围&lt;/h3&gt;
&lt;p&gt;功能测试
性能测试（负载/压力）
用户界面测试/易用性测试
安全性测试
兼容性测试
接口测试&lt;/p&gt;
&lt;h4&gt;功能测试：&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;页面内容测试&lt;/strong&gt;
内容测试用来检测 Web 应用系统提供信息：正确性、准确性、相关性
** 页面链接测试**
链接是 Web 应用系统的一个主要特征。
链接将作为一个独立的项目进行测试。
&lt;strong&gt;检查内容&lt;/strong&gt;：
测试所有链接是否能正确链接；
测试所链接的页面是否存在；
保证 Web 应用系统上没有孤立的页面，所谓孤立页面是指没有链接指向该页面。
&lt;strong&gt;表单测试&lt;/strong&gt;
当用户注册、登录、信息提交等，必须测试提交操作的完整性，以校验提交给服务器的信息的正确性。
** Cookies 测试、Session 测试**
Cookies 通常用来存储用户信息和用户在某个应用系统的操作。
PHP 从 4.1 开始支持 Session 管理。
&lt;strong&gt;设计语言测试&lt;/strong&gt;
Web 设计语言版本的差异可以引起客户端或服务器端严重的问题，例如使用哪种版本的 HTML 等。
&lt;strong&gt;数据库测试&lt;/strong&gt;
在使用了数据库的 Web 应用系统中，可能发生两种错误：
&lt;strong&gt;数据一致性错误&lt;/strong&gt;：由于用户提交的表单信息不正确而造成的。
&lt;strong&gt;输出错误&lt;/strong&gt;：由于网络速度或程序设计问题等引起的。&lt;/p&gt;
&lt;h4&gt;安全测试&lt;/h4&gt;
&lt;p&gt;安全测试：检查系统对非法侵入的防范能力。
目的：验证安装在系统内的保护机制能否在实际中保护系统且不受非法入侵，不受非法干扰。（应对入侵、自我防护和应变）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试内容&lt;/strong&gt;
&lt;strong&gt;目录&lt;/strong&gt;
正确设置目录。
不能通过简单的 URL 替换和推测暴露整个 Web 目录。
严格设置 Web 服务器的目录访问权限，不会显示该目录下的所有内容。
&lt;strong&gt;登录&lt;/strong&gt;
验证用户身份，阻止非法用户登录。
&lt;strong&gt;日志文件&lt;/strong&gt;
日志文件对保证 Web 应用系统的安全性是至关重要的。
需要测试相关信息是否写进了日志文件、是否可追踪。
在后台，要注意验证服务器日志工作正常。
&lt;strong&gt;数据加密&lt;/strong&gt;
当使用了安全套接字时，还要测试加密是否正确，检查信息的完整性。
&lt;strong&gt;脚本语言&lt;/strong&gt;
跨站点脚本执行过程
&lt;img src=&quot;1869b15b2f25c0a976cdda202cfc7740.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;SSL&lt;/strong&gt;
使用 SSL（Security Socket Layer）安全协议进行传送用户信息。
使用 SSL 在 HTTP 层和 TCP 层之间建立用户和服务器之间的加密通信，利用公开密钥/私有密钥的加密技术，确保所传送信息的安全性。&lt;/p&gt;
&lt;h4&gt;易用性测试&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;导航测试&lt;/strong&gt;
导航是否直观？
Web 系统的主要部分是否通过主页存取。
Web 系统的页面结构、导航、菜单、连接的风格是否一致。
Web 应用系统导航帮助要尽可能的准确。
&lt;strong&gt;图形测试&lt;/strong&gt;
一个 Web 应用系统的图形可以包括图片、动画、边框、颜色、字体、背景、按钮等。
&lt;strong&gt;内容测试&lt;/strong&gt;
检验 Web 提供信息的正确性；
检验 Web 提供信息的准确性；
检验 Web 提供信息的相关性。
&lt;strong&gt;整体界面测试&lt;/strong&gt;
整体页面结构设计
界面内容测试
是否能给用户一个整体感&lt;/p&gt;
&lt;h4&gt;兼容性测试&lt;/h4&gt;
&lt;p&gt;1、跨平台测试
2、浏览器测试
3、分辨率测试
4、连接速率测试
5、组合测试&lt;/p&gt;
&lt;h4&gt;接口测试&lt;/h4&gt;
&lt;p&gt;1、服务器接口
2、外部接口
3、错误处理&lt;/p&gt;
&lt;h4&gt;性能测试&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;连接速度测试&lt;/strong&gt;
Web 系统的响应时间
超时的限制
&lt;strong&gt;负载测试&lt;/strong&gt;
性能测试是通过自动化的测试工具模拟多种&lt;strong&gt;正常&lt;/strong&gt;、&lt;strong&gt;峰值&lt;/strong&gt;以及&lt;strong&gt;异常负载&lt;/strong&gt;条件来对系统的&lt;strong&gt;各项性能指标&lt;/strong&gt;进行测试。
&lt;strong&gt;时间性能&lt;/strong&gt;：软件的一个具体事务的响应时间
&lt;strong&gt;空间性能&lt;/strong&gt;：软件运行时所消耗的系统资源&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般性能测试&lt;/strong&gt;：验证软件在正常环境和系统条件下重复使用时是否还能满足性能指标，如运行速度、响应时间、占有系统资源等，不施加任何压力。
&lt;strong&gt;可靠性测试&lt;/strong&gt;：从验证的角度出发，检验系统的可靠性是否达到预期的目标，同时给出当前系统可能的可靠性增长情况。
&lt;strong&gt;负载测试&lt;/strong&gt;
让被测系统在其能忍受的压力的极限范围之内连续运行，来测试系统的可靠性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;压力测试&lt;/strong&gt;
概念：
压力测试是&lt;strong&gt;持续不断&lt;/strong&gt;地给被测系统&lt;strong&gt;增加压力&lt;/strong&gt;，直到将系统压垮为止，用来测试系统所能承受的&lt;strong&gt;最大压力&lt;/strong&gt;。
实际破坏一个 Web 应用系统。
压力测试是测试系统的限制和故障恢复能力，也就是测试 Web 应用系统会不会&lt;strong&gt;崩溃&lt;/strong&gt;，在什么情况下会崩溃。&lt;/p&gt;
&lt;p&gt;关注点：
瞬间访问高峰
每个用户传送大量数据
长时间使用&lt;/p&gt;
&lt;p&gt;内容：
&lt;strong&gt;重复测试&lt;/strong&gt;
一遍又一遍地执行某个操作或功能。这将确定一个操作能否正常执行，并且能否继续在每次执行时都表现正常。
&lt;strong&gt;并发测试&lt;/strong&gt;
就是在同一时间内执行多个操作。由并发引起的错误只能通过执行多个代码示例才能测出来，测试时要同时遍历多条代码路径。
&lt;strong&gt;量级增加&lt;/strong&gt;
&lt;strong&gt;随机变化&lt;/strong&gt;
改变数据的大小、改变时间延迟的长度、输入速度及变化.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试工具 LoadRunner&lt;/strong&gt;
性能测试工具原理：录制+回放模拟用户实际操作场景，监控并分析运行结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;网页性能 Firefox 插件&lt;/strong&gt;：Yslow，Findbug，PageSpeed
&lt;strong&gt;Dynatrace 检查网页性能&lt;/strong&gt;
前端性能检测工具 dynaTrace 是一个运行在 IE 下的免费的性能分析工具，可以分析页面渲染时间、DOM 方法执行时间、js 的解析时间等。&lt;/p&gt;
&lt;h2&gt;（十）自动化测试&lt;/h2&gt;
&lt;h3&gt;1、基本概念与认识&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;概念&lt;/strong&gt;：
通过测试工具或其他手段来部分替代手工测试，并按照测试工程师预定计划进行自动测试的活动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目的&lt;/strong&gt;：
提高测试质量；
减少测试过程中的重复劳动；
实现测试自动化；&lt;/p&gt;
&lt;p&gt;自动化测试用于发现老缺陷；
手工测试用于发现新缺陷。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;/strong&gt;：
&lt;strong&gt;测试自动化能&lt;/strong&gt;：
显著降低重复手工测试的时间
建立可靠、重复的测试，减少认为错误
增强测试质量和覆盖率
&lt;strong&gt;测试自动化不能&lt;/strong&gt;：
完全替代手工测试和手工测试工程师
保证 100%的测试覆盖率
弥补测试实践的不足&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自动测试技术的发展&lt;/strong&gt;
&lt;img src=&quot;8cf9e00c987e516bd027985365b070f3.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、测试工具分类&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;白盒测试工具&lt;/strong&gt;
&lt;img src=&quot;19d204fb54cba4bc1dca9bd8ae5cd2a5.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;黑盒测试工具&lt;/strong&gt;
&lt;img src=&quot;de1cc3a3d4cb987b92ac18f8af0a7968.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;性能测试工具&lt;/strong&gt;
&lt;img src=&quot;3f5dfa31292511f1bb7c97254c8e5aac.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;
&lt;strong&gt;用于测试管理（测试流程管理、缺陷跟踪管理、测试用例管理）的工具&lt;/strong&gt;
&lt;img src=&quot;a26aa53c62071f60eaf33b3c951a4180.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3、自动化测试的基本原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;录制&lt;/strong&gt;
在测试者运行应用程序的时，把所有动作，包括键盘操作、鼠标点击等捕获下来，生成一个脚本文件。
&lt;strong&gt;回放&lt;/strong&gt;
录制的脚本可以被“回放”。
&lt;strong&gt;脚本修改机制&lt;/strong&gt;
根据测试需求，对脚本进行一些必要的修改。&lt;/p&gt;
&lt;h3&gt;4、适于自动化测试场景&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;259b9540da6dcef7dee338dfb86dc0fa.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>