{"version":"https://jsonfeed.org/version/1.1","title":"YieldRay's blog","home_page_url":"https://yieldray.fun/","description":"A personal blog powered by Next.js","favicon":"https://yieldray.fun/profile.png","feed_url":"https://yieldray.fun/feed.json","items":[{"id":"how-lit-html-works","url":"https://yieldray.fun/posts/how-lit-html-works","title":"lit","date_published":"2026-04-05T16:04:53.000Z","date_modified":"2026-04-05T16:04:53.000Z","content_text":"<blockquote>\n<p>翻译自： &lt;<a href=\"https://github.com/lit/lit/blob/main/dev-docs/design/how-lit-html-works.md?plain=1\">https://github.com/lit/lit/blob/main/dev-docs/design/how-lit-html-works.md?plain=1</a></p>\n</blockquote>\n<h1>lit-html 渲染的生命周期</h1>\n<h2>关于本文档</h2>\n<p>这是一篇关于 lit-html 如何工作的概览，介绍它为什么快，以及代码是如何组织的。目标读者是贡献者，或任何对 lit-html 内部机制感兴趣的人。它不是 Lit 的教程或入门文档。若你需要入门材料，请参阅 <a href=\"https://lit.dev/tutorials/intro-to-lit/\">Intro to Lit Tutorial</a> 或 <a href=\"https://lit.dev/docs/libraries/standalone-templates/\">Using lit-html standalone</a> 文档。</p>\n<p>本文档描述的所有源码都在 <a href=\"https://github.com/lit/lit/blob/main/packages/lit-html/src/lit-html.ts\"><code>lit-html.ts</code></a> 中。</p>\n<p>如果你有不清楚的地方或问题，可以在我们的 <a href=\"https://discord.com/invite/buildWithLit\">Discord channel</a> 联系我们，或在 <a href=\"https://github.com/lit/lit/discussions\">GitHub Discussions</a> 提问。所有社区入口见 <a href=\"https://lit.dev/docs/resources/community/\">Lit Community page</a>。</p>\n<h1>什么是 lit-html？</h1>\n<p>lit-html 是一个 HTML 模板库。模板通过 JavaScript 的模板字面量来编写，把静态 HTML 字符串和动态 JavaScript 值混合在一起。lit-html 让你可以把 UI 写成应用状态的函数，具有快速的首次渲染和快速更新能力，并且在状态变化时只最小化更新 DOM。</p>\n<p>lit-html 的性能非常好，可参考 <a href=\"https://krausest.github.io/js-framework-benchmark/\">JS Frameworks Benchmark</a>。这篇文档的一个很好的补充材料是 <a href=\"https://youtu.be/Io6JjgckHbg\">Justin Fagnani 的 lit-html 演讲</a>。</p>\n<h1>基础构件</h1>\n<p>lit-html 的设计自然来源于两个强大的 Web 原语的结合：<a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates\">JavaScript Tagged Template Literals</a>（TTL）和 HTML 的 <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template\"><code>&lt;template&gt;</code></a> 元素。</p>\n<p>TTL 提供了显式语法，可将静态 HTML 字符串与动态 JavaScript 值分离。静态字符串是不可变的，并且在同一个标签模板字面量多次求值时保留身份（引用稳定），因此非常适合作为缓存键。</p>\n<p><code>&lt;template&gt;</code> 元素提供了一个 HTML 内容容器，之后可克隆到文档中。在 template 元素里：脚本不会执行，样式不会生效，自定义元素不会升级，等等。在渲染时，template 的内容会被克隆或导入到 Document 中，从而使克隆出的节点变为活动节点。</p>\n<h1>渲染</h1>\n<h2>lit-html 渲染阶段总结</h2>\n<p>lit-html 模板只是 UI 的描述。它们必须通过 <code>render()</code> 才会影响 DOM。<strong>Define</strong> 与 <strong>Render</strong> 阶段由 Lit 使用者控制，而 <strong>Prepare</strong>、<strong>Create</strong>、<strong>Update</strong> 是内部阶段。渲染被拆成这些内部阶段，是为了缓存并复用 Prepare 与 Create 已完成的工作。</p>\n<ol>\n<li>\n\n<p><strong>Define</strong>: <code>const ui = (title) =&gt; html`&lt;h1&gt;${title}&lt;/h1&gt;`</code></p>\n</li>\n<li>\n\n<p><strong>Render</strong>: <code>render(ui(&quot;Example Title&quot;), document.body)</code></p>\n</li>\n<ol type=\"a\">\n<li>\n\n<p><strong>Prepare</strong>：首次对某个唯一 TTL 调用 <code>render()</code> 时，创建一个 <code>&lt;template&gt;</code> 元素，包含静态 HTML 和 Lit 的 <code>TemplatePart</code>。<code>TemplatePart</code> 编码了每个动态 JavaScript 值到静态 HTML 中位置的映射。</p>\n</li>\n<li>\n\n<p><strong>Create</strong>：首次在某个节点上渲染某个模板时，将 <code>&lt;template&gt;</code> 内容克隆进 DOM，并实例化管理动态内容的 Lit <code>Part</code>。</p>\n</li>\n\n<li>\n\n<p><strong>Update</strong>：遍历动态 JS 值与对应的 Lit <code>Part</code>，只把变化的值提交到 DOM。这是每次模板渲染都会执行的唯一阶段。</p>\n</li>\n</ol>\n</ol>\n\n<p>最终结果是在 <code>&lt;body&gt;</code> 中渲染 <code>&lt;h1&gt;Example Title&lt;/h1&gt;</code>。如果用户随后执行 <code>render(ui(&quot;Updated&quot;), document.body)</code>，则只会运行 <strong>Update</strong> 阶段，<strong>Prepare</strong> 与 <strong>Create</strong> 会被跳过。</p>\n<h2>动机示例</h2>\n<p>接下来文档会反复引用这个示例，来具体展示 lit-html 在每个内部渲染阶段做了什么。</p>\n<pre><code class=\"language-html\">&lt;!-- File: index.html --&gt;\n&lt;script type=&quot;module&quot; src=&quot;./example.js&quot;&gt;&lt;/script&gt;\n&lt;style&gt;\n    .odd {\n        color: green;\n        text-decoration: underline;\n    }\n&lt;/style&gt;\n\n&lt;div id=&quot;container&quot;&gt;&lt;/div&gt;\n</code></pre>\n<pre><code class=\"language-js\">// File: example.js\nimport { html, render } from &quot;lit&quot;;\n\nconst container = document.querySelector(&quot;#container&quot;);\n\nconst counterUi = (count) =&gt;\n    html` &lt;span class=&quot;${count % 2 == 1 ? &quot;odd&quot; : &quot;&quot;}&quot;&gt; ${count} &lt;/span&gt;\n        &lt;button @click=${() =&gt; render(counterUi(count + 1), container)}&gt;Increment&lt;/button&gt;`;\n\nrender(counterUi(0), container);\n</code></pre>\n<p><a href=\"https://lit.dev/playground/#gist=a1058bae08d79928fc2fafaf02b04581\">在 playground 里查看在线示例。</a></p>\n<p>这段示例代码会得到一个计数器，点击 &quot;Increment&quot; 按钮时数值递增：</p>\n<p><img src=\"../images/how-lit-html-works/how-lit-works-demo.gif\" alt=\"Gif showing a counter and increment button. Clicking the button increments the counter. Odd numbers are green and underlined.\"></p>\n<h2>1. Define</h2>\n<p>模板通过 <code>html</code> 标签函数定义。这个标签函数几乎不做额外工作，它只捕获当前值和 <code>strings</code> 对象引用，并将它们作为 <code>TemplateResult</code> 返回。</p>\n<p>lit-html 还提供了一个 <code>svg</code> 函数，与 <code>html</code> 类似，但用于定义 <a href=\"https://developer.mozilla.org/en-US/docs/Web/SVG\">SVG</a> 片段而不是 HTML 片段。渲染时，lit-html 会确保由 <code>svg</code> 创建的元素在 SVG 命名空间中创建。</p>\n<p>默认的 <code>html</code> 和 <code>svg</code> 标签非常简单：把静态字符串和动态值收集到一个对象字面量中。</p>\n<pre><code class=\"language-js\">const tag =\n    (type) =&gt;\n    (strings, ...values) =&gt; ({\n        _$litType$: type,\n        strings,\n        values,\n    });\nconst html = tag(/* HTML_RESULT type:*/ 1);\nconst svg = tag(/* SVG_RESULT type:*/ 2);\n</code></pre>\n<p>示例中的第一次 <code>render()</code> 调用会把初始 UI 状态定义为 <code>counterUi(0)</code>。把它内联展开后：</p>\n<pre><code class=\"language-js\">// `counterUi(0)` 表达式的求值结果：\nhtml`\n  &lt;span class=&quot;${&quot;&quot;}&quot;&gt;\n   ${0}\n  &lt;/span&gt;\n  &lt;button @click=${\n    () =&gt; render(counterUi(1), container)\n  }&gt;\n    Increment\n  &lt;/button&gt;`;\n\n// 然后 `html` 标签函数表达式会求值为以下 TemplateResult：\n{\n  _$litType$: 1,\n  strings: [\n    &#39;&lt;span class=&quot;&#39;,\n    &#39;&quot;&gt;&#39;,\n    &#39;&lt;/span&gt;&lt;button @click=&#39;,\n    &#39;&gt;Increment&lt;/button&gt;&#39;\n  ],\n  values: [\n    &quot;&quot;,\n    0,\n    () =&gt; render(counterUi(1), container)\n  ]\n}\n</code></pre>\n<p>模板表达式求值非常快。它的开销基本等于模板中 JavaScript 表达式本身，再加一次函数调用和一次对象分配。</p>\n<h2>2. Render</h2>\n<p>由调用 lit-html 的 <code>render()</code> 函数触发，例如示例里的 <code>render(counterUi(0), container)</code>。</p>\n<p><code>render()</code> 会检查容器元素上是否有 <code>_$litPart$</code> 字段。如果 Lit 之前渲染过这个容器，那么该字段会是一个 Lit <code>ChildPart</code> 对象。若存在，就直接调用它的 <code>_$setValue</code> 来渲染模板；否则就创建新的 <code>ChildPart</code>，挂到容器的 <code>_$litPart$</code> 上，再调用 <code>_$setValue</code>。后文会详细介绍 Part（包括 ChildPart）。</p>\n<h3>2.i. Prepare</h3>\n<p>源码位置：<a href=\"https://github.com/lit/lit/blob/5659f6eec2894f1534be1a367c8c93427d387a1a/packages/lit-html/src/lit-html.ts#L1562-L1568\">包含并缓存 <strong>Prepare</strong> 阶段的 <code>_$getTemplate</code> 方法</a>。</p>\n<p>当页面上某个唯一的 <code>html</code> 标签模板字面量第一次被渲染时，必须先进行 prepare。Prepare 阶段的结果是 Lit <code>Template</code> 类的一个实例：它包含一个 <code>&lt;template&gt;</code> 元素和一组元数据对象（称为 <code>TemplatePart</code>），用于记录每个动态 JavaScript 值在 DOM 上应该设置的位置。这个 Lit <code>Template</code> 对象会以 <code>TemplateResult</code> 唯一的 <code>strings</code> 模板字符串数组作为 key 进行缓存，因此后续再遇到来自同一个标签模板字面量的 <code>TemplateResult</code> 时，会跳过此阶段。</p>\n<p>Prepare 阶段只看 <code>TemplateResult</code> 的静态字符串，不看动态值，所以每个源码里唯一的标签模板字面量只会执行一次。</p>\n<p>在示例代码中，<strong>Prepare</strong> 完成后，<code>ChildPart._$getTemplate</code> 的返回值如下：</p>\n<pre><code class=\"language-js\">// Prepare 阶段的伪代码：\nlet preparedTemplate = ChildPart._$getTemplate(counterUi(0));\n\n// Prepare 阶段结果：\nconsole.log(preparedTemplate.el);\n// &lt;template&gt;&lt;span&gt;&lt;!--?lit$1234$--&gt;&lt;/span&gt;&lt;button&gt;Increment&lt;/button&gt;&lt;/template&gt;\n\nconsole.log(preparedTemplate.parts);\n/*\n[\n  { type: 1, index: 0, name: &quot;class&quot; },\n  { type: 2, index: 1 },\n  { type: 1, index: 2, name: &quot;click&quot; }\n]\n*/\n</code></pre>\n<p>下面这些小节详细说明：当某个 <code>TemplateResult</code> 第一次进入 prepare 时，<code>Template</code> 类构造函数里发生了什么。</p>\n<h4>用标记拼接 TemplateResult 的模板字符串数组</h4>\n<p>我们手里有一组静态 HTML 字符串，但还不能直接放进 <code>&lt;template&gt;</code> 元素。这个步骤会把不可变的模板字符串数组转换成带注释标记的 HTML 字符串，在动态值“空洞”处打标：</p>\n<ul>\n<li>文本位置表达式（例如 <code>&lt;span&gt;${}&lt;/span&gt;</code>）会被标记成注释节点：<code>&lt;span&gt;&lt;!--?lit$random$--&gt;&lt;/span&gt;</code>。</li>\n<li>属性位置表达式（例如 <code>&lt;p class=&quot;${}&quot;&gt;&lt;/p&gt;</code>）会被标记成哨兵字符串：<code>&lt;p class$lit$=&quot;lit$random$&quot;&gt;&lt;/p&gt;</code>，并且属性名会追加 <code>$lit$</code> 后缀。</li>\n</ul>\n<p>之所以给绑定属性追加 <code>$lit$</code> 后缀，是为了避免某些特殊属性（例如 <code>style</code>、<code>class</code> 以及许多 SVG 属性）在仍包含 marker 表达式时被浏览器提前处理。例如，IE 与 Edge 会解析 <code>style</code> 属性值，若 CSS 无效就会直接丢弃。</p>\n<p>示例模板字符串数组对应的预处理 HTML 字符串如下：</p>\n<pre><code class=\"language-js\">getTemplateHtml([&#39;&lt;span class=&quot;&#39;, &#39;&quot;&gt;&#39;, &quot;&lt;/span&gt;&lt;button @click=&quot;, &quot;&gt;Increment&lt;/button&gt;&quot;]);\n\n// 返回以下带标记的 HTML（为便于阅读添加了空格）：\n`&lt;span class$lit$=&quot;lit$1234$&quot;&gt;\n  &lt;?lit$1234$&gt;\n&lt;/span&gt;\n&lt;button @click$lit$=lit$1234$&gt;\n  Increment\n&lt;/button&gt;`;\n</code></pre>\n<p>注意这里有三个 marker，与示例 <code>TemplateResult</code> 的 <code>values</code> 数组中的三个动态值一一对应。</p>\n<blockquote>\n<p><strong>注意</strong>\n在上面的预处理 HTML 字符串中，<code>&lt;?lit$1234$&gt;</code> 语法表示一个 <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/ProcessingInstruction\"><code>ProcessingInstruction</code></a>。它在插入 <code>&lt;template&gt;</code> 元素并被解析后，会变成注释节点：<code>&lt;!--?lit$1234$--&gt;</code>。这是一个减少 marker 字节数的优化。</p>\n</blockquote>\n<h4>创建 <code>&lt;template&gt;</code> 元素</h4>\n<p>预处理后的 HTML 字符串会用于设置新建 <code>&lt;template&gt;</code> 元素的 <code>innerHTML</code>，这会触发浏览器解析模板 HTML。这个 <code>&lt;template&gt;</code> 元素会被赋给 <code>Template</code> 的 <code>el</code> 字段。</p>\n<p>这一步不会导致 XSS 或其他恶意输入风险，因为只使用了静态字符串，不会插入任何动态值。</p>\n<h4>创建 <code>TemplatePart</code></h4>\n<p><code>TemplatePart</code> 是一个元数据对象，记录动态 JavaScript 值应该设置到 DOM 的位置。每个 <code>TemplatePart</code> 包含：它所关联节点的深度优先索引、表达式类型（<code>text</code> 或 <code>attribute</code>）以及属性名。</p>\n<p><code>TemplatePart</code> 的发现方式是：遍历 <code>&lt;template&gt;</code> 元素中的节点树并查找 marker 标记。如果在属性、marker 注释节点、或文本内容中找到 marker，就会创建 <code>TemplatePart</code> 并保存到 <code>parts</code> 字段。</p>\n<p>在遍历节点树时，不同 Node 类型会执行以下逻辑：</p>\n<h5>Element</h5>\n<p>对于元素上的每个属性，如果属性名带 <code>$lit$</code> 后缀，则该表达式对应 <code>PropertyPart</code>、<code>BooleanAttributePart</code>、<code>EventPart</code> 或 <code>AttributePart</code> 之一。可通过属性名首字符区分（见附录的 <a href=\"#parts\">Parts 表</a>）。</p>\n<p>元素上的属性也可能表示 <code>ElementPart</code>，当属性名以 marker 哨兵值开头时会出现。比如 <code>&lt;div ${} ${}&gt;</code> 会被标记为 <code>&lt;div lit$random$0=&quot;&quot; lit$random$1=&quot;&quot;&gt;</code>。</p>\n<p>对于这些识别出的绑定属性，会从元素上移除，并创建 <code>TemplatePart</code> 记录其位置、名称和类型。</p>\n<h5>Comment</h5>\n<p>匹配 marker 标记的注释节点，表示一个 ChildPart 位置的 <code>TemplatePart</code>。</p>\n<h5>Text</h5>\n<p>通常文本位置 marker 会是注释节点，但在 <code>&lt;style&gt;</code>、<code>&lt;script&gt;</code>、<code>&lt;textarea&gt;</code>、<code>&lt;title&gt;</code> 标签内部，看起来像注释的标记（<code>&lt;!-- lit --&gt;</code>）会作为文本插入。因此我们还必须在 Text 节点中查找 marker。若找到，就拆分 Text 节点并插入注释 marker，同时在 ChildPart 位置记录一个 <code>TemplatePart</code>，其索引指向该注释 marker 节点。</p>\n<h3>2.ii. Create</h3>\n<p>Create 阶段发生在 ChildPart 正在渲染的模板与它上次使用的模板不同时。</p>\n<p>在 <code>lit-html</code> 源码里，这一阶段对应 <a href=\"https://github.com/lit/lit/blob/64fb960246e8f1eae982c2f09fca9759001756af/packages/lit-html/src/lit-html.ts#L1534-L1535\">实例化并克隆</a> <code>TemplateInstance</code>。</p>\n<p>在示例中，这个阶段会把 <code>&lt;template&gt;</code> 中的 DOM 节点克隆到一个 <code>fragment</code>，准备插入文档，同时创建一组 <code>Part</code> 列表，供下一阶段更新时设置 fragment 里的动态 JavaScript 值。</p>\n<pre><code class=\"language-js\">const instance = new TemplateInstance(preparedTemplate);\n// 创建出的 DOM fragment 将被插入到 DOM。\nconst fragment = instance._clone();\n\nconsole.log(instance._$parts);\n// [AttributePart, ChildPart, EventPart]\n</code></pre>\n<p>在 update 阶段后，这个 fragment 会被插入 DOM。</p>\n<p>下面详细说明创建并克隆 <code>TemplateInstance</code> 时发生了什么。</p>\n<h4>创建 <code>TemplateInstance</code> 并实例化 <code>Part</code></h4>\n<p><code>TemplateInstance</code> 负责创建初始 DOM 和更新这份 DOM。它是一个 <code>Template</code> 在特定 DOM 位置上的实例。<code>TemplateInstance</code> 持有用于更新 DOM 的 <code>Part</code> 引用。</p>\n<p>在 <code>TemplateInstance._clone()</code> 中，首先会把 <code>&lt;template&gt;</code> 元素克隆为 document fragment。</p>\n<p>克隆后，会遍历 fragment 的节点树，通过深度优先索引把节点与 <code>TemplatePart</code> 对齐。当某个节点索引与 <code>TemplatePart</code> 上记录的索引匹配时，就会为该节点创建一个 <code>Part</code> 实例。这个实例持有节点引用，便于后续更新时直接访问。根据 <code>TemplatePart</code> 的 <code>type</code> 字段，会实例化以下之一：<code>ChildPart</code>、<code>AttributePart</code>、<code>PropertyPart</code>、<code>EventPart</code>、<code>BooleanAttributePart</code>、<code>ElementPart</code>。</p>\n<p>这些 <code>Part</code> 实例会存到 <code>TemplateInstance</code> 上。</p>\n<p>随后，<code>TemplateInstance</code>（连同其 <code>Part</code>）会存到根 <code>ChildPart</code> 上，供后续渲染请求直接进入 update 阶段。</p>\n<h3>2.iii. Update</h3>\n<p>Update 阶段每次渲染都会执行，由 <a href=\"https://github.com/lit/lit/blob/64fb960246e8f1eae982c2f09fca9759001756af/packages/lit-html/src/lit-html.ts#L1532\"><code>TemplateInstance._update()</code></a> 触发。这里才真正写入 DOM。此前所有步骤都在构建可缓存、可复用的对象，update 则执行实际写入。</p>\n<p>更新时会遍历 parts 与 values 数组，并调用 <code>part._$setValue(value)</code>。</p>\n<p>每种 Part 在 <code>_$setValue</code> 中如何处理值，由该 Part 自身决定。</p>\n<p>回顾一下，示例在首次渲染时的 <code>TemplateResult</code> 动态值为：</p>\n<pre><code class=\"language-js\">[&quot;&quot;, 0, () =&gt; render(counterUi(1), container)];\n</code></pre>\n<p>这会导致 <code>TemplateInstance</code> 上的三个 part 分别接收对应值。为了演示，把它内联写出来是：</p>\n<pre><code class=\"language-js\">AttributePart._$setValue(&quot;&quot;);\nChildPart._$setValue(0);\nEventPart._$setValue(() =&gt; render(counterUi(1), container));\n</code></pre>\n<p>在 <code>_$setValue</code> 内，每一种 Part 类型也都会先解析 directive，并把 <code>value</code> 改为 directive 的返回值。这就是 <a href=\"https://lit.dev/docs/templates/directives/\">directive 可以自定义并扩展表达式渲染方式</a> 的机制。</p>\n<p><code>_$setValue</code> 引发的 DOM 变更会因 <code>Part</code> 类型而异：</p>\n<h4>ChildPart._$setValue</h4>\n<p>当模板作者在子节点位置写动态表达式时，就会对 <code>ChildPart</code> 设值；传入值就是该模板表达式计算得到的值。</p>\n<p>以下这些模板最终都会通过 <code>ChildPart</code> 渲染出 <code>&lt;div&gt;hi&lt;/div&gt;</code>：</p>\n<ul>\n<li><code>html`&lt;div&gt;${&quot;hi&quot;}&lt;/div&gt;`</code>：对子 part 设置字符串字面量 <code>&quot;hi&quot;</code>。</li>\n<li><code>html`&lt;div&gt;${html`hi`}&lt;/div&gt;`</code>：对子 part 设置 TemplateResult 值 <code>html`Hi`</code>。</li>\n<li><code>html`&lt;div&gt;${[&#39;h&#39;, &#39;i&#39;]}&lt;/div&gt;`</code>：对子 part 设置可迭代对象。</li>\n<li><code>html`&lt;div&gt;${document.createTextNode(&#39;hi&#39;)}&lt;/div&gt;`</code>：对子 part 设置 Text 节点。</li>\n</ul>\n<p>高层来说，<code>ChildPart</code> 通过 <code>_$committedValue</code> 记录上次提交值；若本次传入值与上次相等，则跳过工作。这就是 <code>ChildPart</code> 在值级别做 diff 的方式。\n如果本次值与 <code>_$committedValue</code> 不同，就会根据值类型进行处理，转为 Node 或多个 Node 并提交到 DOM。随后把本次值写入 <code>_$committedValue</code>，供后续 <code>_$setValue</code> 在值不变时省去额外工作。</p>\n<p>对 <code>ChildPart</code> 设置原始值（<code>null</code>、<code>undefined</code>、<code>boolean</code>、<code>number</code>、<code>string</code>、<code>symbol</code>、<code>bigint</code>）会创建或更新 Text 节点。文本提交发生在 <code>_commitText</code> 中，文本净化也在这里进行。</p>\n<p>如果值是 <code>TemplateResult</code>，ChildPart 会执行 <code>_commitTemplateResult</code>，把该 <code>TemplateResult</code> 渲染到当前 <code>ChildPart</code>，并递归地重复本文描述的 <a href=\"#2i-prepare\">prepare</a>、<a href=\"#2ii-create\">create</a>、<a href=\"#2iii-update\">update</a> 流程。</p>\n<p>如果值是 Node，<code>ChildPart</code> 会清除之前渲染的节点并直接插入该 Node。</p>\n<h4>AttributePart._$setValue</h4>\n<p>在单个绑定属性值场景（如 <code>html`&lt;input value=${...}&gt;`</code>）中，<code>AttributePart</code> 同样使用 <code>_$committedValue</code> 与上次值做 diff，避免无意义更新。提交时会调用 <code>this.element.setAttribute(name, value)</code>。</p>\n<p><code>AttributePart</code> 也处理属性值中有多个绑定的复杂场景：<code>html`&lt;div class=&quot;${...} static-class ${...}&quot;&gt;&lt;/div&gt;`</code>。多个值会先求值并拼接，再通过一次 <code>setAttribute</code> 提交。</p>\n<h4>PropertyPart._$setValue</h4>\n<p><code>PropertyPart</code> 继承自 <code>AttributePart</code>，区别仅在于提交方式：不是 <code>setAttribute(name, value)</code>，而是通过属性赋值 <code>this.element[name] = value</code>。例如 <code>html`&lt;input .value=${&#39;hi&#39;}&gt;`</code> 会通过 <code>inputEl.value = &#39;hi&#39;</code> 提交值 <code>&#39;hi&#39;</code>。</p>\n<h4>BooleanAttributePart._$setValue</h4>\n<p><code>BooleanAttributePart</code> 也继承自 <code>AttributePart</code>，并重写 <code>_commitValue</code>。提交到 <code>BooleanAttributePart</code> 时，falsy 值或 <code>nothing</code> 会移除该属性；truthy 值会把属性设为空字符串值。</p>\n<h4>EventPart._$setValue</h4>\n<p><code>EventPart</code> 同样继承自 <code>AttributePart</code>，并重写 <code>_$setValue</code>：接收用户提供的事件监听器后调用 <code>this.element.addEventListener(attributeName, value)</code>。EventPart 使用监听器对象，使得跨多次重渲染时即使传入函数变化，也可更新监听器而无需每次都移除再添加。仅当提供了选项不同的监听器对象时，才会移除并添加新监听器。若值为 <code>null</code>、<code>undefined</code> 或 <code>nothing</code>，则会移除已有监听器且不添加新监听器。</p>\n<p>它还会确保事件监听回调中的 <code>this</code> 指向 host 对象，通常是渲染该模板的宿主组件。</p>\n<h4>ElementPart._$setValue</h4>\n<p><code>ElementPart</code> 不能直接提交任何值。它只调用 <code>resolveDirective</code>，让 directive 可以挂接在 <code>ElementPart</code> 位置。</p>\n<p>例如 <a href=\"https://lit.dev/playground/#sample=examples/motion-simple\"><code>@lit-labs/motion</code> 提供了 <code>animate</code> directive</a>，可应用在元素的 <code>ElementPart</code> 位置，由它来管理元素动画。</p>\n<h1>高效更新</h1>\n<p>前面我们讲的是首次渲染：示例中的 <code>render(counterUi(0), container)</code>。那当点击 &quot;Increment&quot; 并触发 <code>render(counterUi(1), container)</code> 时会怎样？</p>\n<p><code>render()</code> 会在容器上查找 <code>_$litPart$</code> 属性，并找到一个 <code>ChildPart</code>。随后调用该 <code>ChildPart</code> 的 <code>_$setValue</code>，传入 <code>counterUi(1)</code> 返回的 <code>TemplateResult</code>。</p>\n<p><code>counterUi(1)</code> 的 <code>TemplateResult</code> 为：</p>\n<pre><code class=\"language-js\">{\n  _$litType$: 1,\n  strings: [\n    &#39;&lt;span class=&quot;&#39;,\n    &#39;&quot;&gt;&#39;,\n    &#39;&lt;/span&gt;&lt;button @click=&#39;,\n    &#39;&gt;Increment&lt;/button&gt;&#39;\n  ],\n  values: [\n    &quot;odd&quot;,\n    1,\n    () =&gt; render(counterUi(2), container)\n  ]\n}\n</code></pre>\n<p>此时不会重复 <strong>Prepare</strong> 阶段，而是用模板的 <code>strings</code> 模板字符串数组引用从缓存取回 Lit <code>Template</code>。正如 <a href=\"#%E5%9F%BA%E7%A1%80%E6%9E%84%E4%BB%B6\">基础构件</a> 中所说，这个引用在同一标签函数多次求值间保持稳定。</p>\n<p>容器 <code>ChildPart</code> 保存着上次提交值，在这个例子中是一个 <code>TemplateInstance</code>，其引用的 <code>Template</code> 正是缓存中取回的那个。因为当前渲染到这个 <code>ChildPart</code> 的还是同一个 <code>Template</code>，所以 <strong>Create</strong> 阶段也会被跳过。</p>\n<p>剩下的就是遍历先前创建好的 <code>Part</code> 并用新值调用 <code>_$setValue</code>。演示内联如下：</p>\n<pre><code class=\"language-js\">AttributePart._$setValue(&quot;odd&quot;);\nChildPart._$setValue(1);\nEventPart._$setValue(() =&gt; render(counterUi(2), container));\n</code></pre>\n<p>结果就是 DOM 更新为绿色带下划线的数字 1，且事件监听器替换为新监听函数。</p>\n<p>如果某个要设置的值与之前相同（<code>===</code> 比较），该 Part 就不会进行 DOM 操作。</p>\n<h1>未来工作</h1>\n<p>lit-html 已经很快了，但还能更快吗？</p>\n<ul>\n<li><a href=\"https://github.com/WICG/webcomponents/issues/990\">用于创建 DOM Parts 的声明式语法</a>。这可以去掉 lit-html 遍历 <code>&lt;template&gt;</code> 树并手动实例化 Part 的需求，从而减少 <strong>Create</strong> 阶段树遍历开销。</li>\n<li>预编译 lit-html 的 <code>html</code> 标签函数（跟踪于 <a href=\"https://github.com/lit/lit/issues/189%EF%BC%89%E3%80%82%E8%BF%99%E5%8F%AF%E4%BB%A5%E6%8A%8A\">https://github.com/lit/lit/issues/189）。这可以把</a> <strong>Prepare</strong> 阶段前移到构建期，运行时将不再需要 <strong>Prepare</strong>。</li>\n<li><a href=\"https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md#html-template-instantiation\">Template instantiation</a> 也有望减少 prepare 阶段工作，并建立在 DOM parts 提案之上。</li>\n</ul>\n<h1>附录</h1>\n<h2>Parts</h2>\n<p><code>Part</code> 是 lit-html 的概念，表示 <code>html</code> 标签模板字面量中表达式所处的位置：</p>\n<table>\n<thead>\n<tr>\n<th>Part</th>\n<th>描述</th>\n<th>作者写法</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>ChildPart</code></td>\n<td>HTML 子节点位置中的表达式</td>\n<td><code>html`&lt;div&gt;${...}&lt;/div&gt;` </code></td>\n</tr>\n<tr>\n<td><code>AttributePart</code></td>\n<td>HTML 属性值位置中的表达式</td>\n<td><code>html`&lt;input id=&quot;${...}&quot;&gt;` </code></td>\n</tr>\n<tr>\n<td><code>BooleanAttributePart</code></td>\n<td>布尔属性值中的表达式（名称前缀 <code>?</code>）</td>\n<td><code>html`&lt;input ?checked=&quot;${...}&quot;&gt;` </code></td>\n</tr>\n<tr>\n<td><code>PropertyPart</code></td>\n<td>属性（property）值位置中的表达式（前缀 <code>.</code>）</td>\n<td><code>html`&lt;input .value=${...}&gt;` </code></td>\n</tr>\n<tr>\n<td><code>EventPart</code></td>\n<td>事件监听位置中的表达式（前缀 <code>@</code>）</td>\n<td><code>html`&lt;button @click=${...}&gt;&lt;/button&gt;` </code></td>\n</tr>\n<tr>\n<td><code>ElementPart</code></td>\n<td>元素标签上的表达式</td>\n<td><code>html`&lt;input ${...}&gt;` </code></td>\n</tr>\n</tbody></table>\n<p>在上述所有情形中，作者都在 <code>${...}</code> 中传入一个表达式，表示模板的动态绑定。不同 Part 类型决定该值如何提交到 DOM。比如在 <code>html`&lt;button @click=${() =&gt; console.log(&#39;clicked&#39;)}&gt;&lt;/button&gt;` </code> 中，<code>EventPart</code> 会接管用户提供函数，并管理 DOM 上的 <code>addEventListener</code> 与 <code>removeEventListener</code>，从而在触发点击事件时调用该函数。</p>\n<p>理解 Parts 对 <a href=\"https://lit.dev/docs/templates/custom-directives/#parts\">编写自定义 directives</a> 很有帮助。</p>\n<h2>具体示例：复用同一个模板</h2>\n<p>这个示例会在多个位置复用同一个模板，能更清晰地说明多个类之间的关系。</p>\n<p>考虑以下代码：</p>\n<pre><code class=\"language-ts\">const imgTemplate = (url) =&gt; html`&lt;img src=${url} /&gt;`;\n\nrender(html`${imgTemplate(&quot;1.jpg&quot;)} ${imgTemplate(&quot;2.jpg&quot;)} ${imgTemplate(&quot;3.jpg&quot;)}`, document.body);\n</code></pre>\n<p>首次渲染时：</p>\n<pre><code class=\"language-mermaid\">flowchart TD\n    A[&amp;#40url&amp;#41 =&amp;gt html`&amp;ltimg src=$&amp;#123url&amp;#125&amp;gt`] --&gt;|Called with &#39;1.jpg&#39;| C1(TemplateResult\\n&amp;#123strings, values: &amp;#91&#39;1.jpg&#39;&amp;#93&amp;#125)\n    A --&gt;|Called with &#39;2.jpg&#39;| C2(TemplateResult\\n&amp;#123strings, values: &amp;#91&#39;2.jpg&#39;&amp;#93&amp;#125)\n    A --&gt;|Called with &#39;3.jpg&#39;| C3(TemplateResult\\n&amp;#123strings, values: &amp;#91&#39;3.jpg&#39;&amp;#93&amp;#125)\n    C1 --&gt; |Prepare| Template(&amp;lttemplate&amp;gt)\n    C2 --&gt; |Cache lookup| Template\n    C3 --&gt; |Cache lookup| Template\n    Template --&gt; |Create| TI1(Template Instance 1)\n    Template --&gt; |Create| TI2(Template Instance 2)\n    Template --&gt; |Create| TI3(Template Instance 3)\n    TI1 --&gt; |Update| Up1(Render &amp;ltimg src=&#39;1.jpg&#39;&amp;gt)\n    TI2 --&gt; |Update| Up2(Render &amp;ltimg src=&#39;2.jpg&#39;&amp;gt)\n    TI3 --&gt; |Update| Up3(Render &amp;ltimg src=&#39;3.jpg&#39;&amp;gt)\n</code></pre>\n<p>然后若执行以下代码：</p>\n<pre><code class=\"language-ts\">render(html`${imgTemplate(&quot;4.jpg&quot;)} ${imgTemplate(&quot;5.jpg&quot;)} ${imgTemplate(&quot;6.jpg&quot;)}`, document.body);\n</code></pre>\n<p>对应更新流程为：</p>\n<pre><code class=\"language-mermaid\">flowchart TD\n    A[&amp;#40url&amp;#41 =&amp;gt html`&amp;ltimg src=$&amp;#123url&amp;#125&amp;gt`] --&gt;|Called with &#39;4.jpg&#39;| C1(TemplateResult\\n&amp;#123strings, values: &amp;#91&#39;4.jpg&#39;&amp;#93&amp;#125)\n    A --&gt;|Called with &#39;5.jpg&#39;| C2(TemplateResult\\n&amp;#123strings, values: &amp;#91&#39;5.jpg&#39;&amp;#93&amp;#125)\n    A --&gt;|Called with &#39;6.jpg&#39;| C3(TemplateResult\\n&amp;#123strings, values: &amp;#91&#39;6.jpg&#39;&amp;#93&amp;#125)\n    C1 --&gt; |Cache lookup| TI1(Template Instance 1)\n    C2 --&gt; |Cache lookup| TI2(Template Instance 2)\n    C3 --&gt; |Cache lookup| TI3(Template Instance 3)\n    TI1 --&gt; |Update| Up1(setAttribute&amp;#40imgElem1, &#39;4.jpg&#39;&amp;#41)\n    TI2 --&gt; |Update| Up2(setAttribute&amp;#40imgElem2, &#39;5.jpg&#39;&amp;#41)\n    TI3 --&gt; |Update| Up3(setAttribute&amp;#40imgElem3, &#39;6.jpg&#39;&amp;#41)\n</code></pre>\n","tags":["lit"]},{"id":"js-signal","url":"https://yieldray.fun/posts/js-signal","title":"实现 Signal","date_published":"2026-01-13T23:22:58.000Z","date_modified":"2026-01-13T23:22:58.000Z","content_text":"<blockquote>\n<p>摘自 <a href=\"https://github.com/tc39/proposal-signals\">tc39/proposal-signals</a></p>\n</blockquote>\n<p>为了开发复杂的用户界面（UI），JavaScript 应用开发者需要以一种高效的方式<strong>存储、计算、使失效、同步并将状态推送</strong>到应用的视图层。</p>\n<p>用户界面通常不仅仅涉及管理简单的数值，往往还经常涉及渲染<strong>计算状态（computed state）</strong>，而这些状态又依赖于一个复杂的其它数值树，或者依赖于本身也是由计算得出的状态。</p>\n<p>**Signals（信号）**的目标就是提供管理此类应用状态的基础设施，从而让开发者能够专注于业务逻辑，而无需分心于这些重复的细节。</p>\n<p><strong>类 Signal 的机制</strong>也被独立地发现在非 UI 场景中非常有用，特别是在<strong>构建系统</strong>中，用于避免不必要的重复构建。</p>\n<p>在<strong>响应式编程</strong>中，使用 Signals 可以免除手动管理应用更新的需求。</p>\n<blockquote>\n<p>一种基于状态变化进行更新的<strong>声明式编程模型</strong>。</p>\n</blockquote>\n<h1>极简模型</h1>\n<p>最基础的响应式系统核心在于“发布-订阅”模式。需要一个容器来存储值，当值变化时通知订阅者。</p>\n<pre><code class=\"language-ts\">/**\n * 指向当前正在执行的副作用函数 (依赖收集的核心)\n */\nlet activeEffect: VoidFunction | null = null;\n\n/**\n * 创建响应式状态容器（信号）\n */\nexport function signal&lt;T&gt;(initialValue: T) {\n    let value = initialValue;\n    const subscribers = new Set&lt;VoidFunction&gt;();\n\n    const read = () =&gt; {\n        // 依赖收集：如果有副作用函数正在执行，说明它读取了这个信号\n        if (activeEffect) {\n            subscribers.add(activeEffect);\n        }\n        return value;\n    };\n\n    const write = (newValue: T) =&gt; {\n        if (value !== newValue) {\n            value = newValue;\n            // 派发更新：信号的变化会通知所有订阅者\n            [...subscribers].forEach((fn) =&gt; fn());\n        }\n    };\n\n    return [read, write] as const;\n}\n\n/**\n * 注册副作用函数（订阅者）\n */\nexport function effect(fn: VoidFunction) {\n    const execute = () =&gt; {\n        activeEffect = execute;\n        try {\n            fn();\n        } finally {\n            activeEffect = null;\n        }\n    };\n\n    execute(); // 立即执行以建立初始依赖\n}\n</code></pre>\n<p><strong>存在问题：</strong></p>\n<ol>\n<li><strong>无法处理条件分支（分支切换）</strong>：如果 <code>effect(() =&gt; show ? data : null)</code>，当 <code>show</code> 变为 false 后，<code>data</code> 的变化不应再触发 effect。目前的实现会导致内存泄漏和无效执行。</li>\n<li><strong>没有清理机制</strong>：<code>subscribers</code> 会无限膨胀。</li>\n</ol>\n<h1>动态依赖与自动清理</h1>\n<p>为解决分支切换问题，需要在每次 effect 重新执行前，<strong>清除之前的订阅关系</strong>，重新收集依赖。<br>这就要求 effect 知道它依赖了谁，signal 也知道谁依赖了它（双向记录）。</p>\n<pre><code class=\"language-ts\">interface ReactiveEffect {\n    (): void;\n    // 反向存储：effect 知道哪些 Signal 的订阅者集合里包含了自己\n    deps: Set&lt;Set&lt;ReactiveEffect&gt;&gt;;\n}\n\nlet activeEffect: ReactiveEffect | null = null;\n\nfunction cleanup(effect: ReactiveEffect) {\n    // 从所有依赖的 Signal 中移除当前 effect\n    for (const dep of effect.deps) {\n        dep.delete(effect);\n    }\n    effect.deps.clear();\n}\n\nexport function signal&lt;T&gt;(initialValue: T) {\n    let value = initialValue;\n    const subscribers = new Set&lt;ReactiveEffect&gt;();\n\n    const read = () =&gt; {\n        if (activeEffect) {\n            // 1. Signal 记录 Effect\n            subscribers.add(activeEffect);\n            // 2. Effect 记录 Signal (的 subscribers 集合)\n            activeEffect.deps.add(subscribers);\n        }\n        return value;\n    };\n\n    const write = (newValue: T) =&gt; {\n        if (value !== newValue) {\n            value = newValue;\n            // 必须创建快照！因为 cleanup 会在执行过程中修改 subscribers，\n            // 导致遍历过程中的 add/delete 操作引发死循环\n            const effectsToRun = new Set(subscribers);\n            effectsToRun.forEach((fn) =&gt; fn());\n        }\n    };\n\n    return [read, write] as const;\n}\n\nexport function effect(fn: VoidFunction) {\n    const effect: ReactiveEffect = () =&gt; {\n        // 核心：每次运行前先断开所有旧连接\n        cleanup(effect);\n\n        const prevEffect = activeEffect;\n        activeEffect = effect;\n\n        try {\n            fn();\n        } finally {\n            activeEffect = prevEffect;\n        }\n    };\n\n    effect.deps = new Set();\n    effect();\n}\n</code></pre>\n<h1>解决“菱形依赖”与 Computed</h1>\n<p>在复杂的依赖图中，我们经常遇到<strong>菱形依赖（Diamond Problem）</strong>：</p>\n<pre><code class=\"language-mermaid\">graph TD\n    A[Signal A] --&gt; B[Computed B]\n    A --&gt; C[Computed C]\n    B --&gt; D[Effect D]\n    C --&gt; D\n    style A fill:#f9f,stroke:#333\n    style D fill:#ff9,stroke:#333\n</code></pre>\n<p>如果不加控制，修改 <code>A</code> 会导致 <code>D</code> 运行两次（A-&gt;B-&gt;D 和 A-&gt;C-&gt;D），且第一次运行时可能处于中间态（Glitch）。</p>\n<p>解决方案包含两个部分：</p>\n<ol>\n<li><strong>调度器（Scheduler）</strong>：使用微任务批处理 Effect，避免同步重复执行。</li>\n<li><strong>计算属性（Computed）</strong>：引入 <strong>Push-Pull 模型</strong> 和 <strong>脏检查（Dirty Marking）</strong>。</li>\n</ol>\n<h2>核心实现：Push-Pull + Lazy Evaluation</h2>\n<pre><code class=\"language-ts\">interface ReactiveEffect {\n    (): void;\n    deps: Set&lt;Set&lt;ReactiveEffect&gt;&gt;;\n    options?: { scheduler?: (fn: VoidFunction) =&gt; void };\n}\n\nlet activeEffect: ReactiveEffect | null = null;\n\n// --- 调度器逻辑 ---\nconst effectQueue = new Set&lt;VoidFunction&gt;();\nlet isFlushPending = false;\n\nfunction flushQueue() {\n    const tasks = [...effectQueue];\n    effectQueue.clear();\n    isFlushPending = false;\n    tasks.forEach((fn) =&gt; fn());\n}\n\nfunction queueJob(job: VoidFunction) {\n    effectQueue.add(job);\n    if (!isFlushPending) {\n        isFlushPending = true;\n        queueMicrotask(flushQueue);\n    }\n}\n\n// --- 清理逻辑 ---\nfunction cleanup(effect: ReactiveEffect) {\n    for (const dep of effect.deps) {\n        dep.delete(effect);\n    }\n    effect.deps.clear();\n}\n\n// --- Signal 实现 ---\nexport function signal&lt;T&gt;(initialValue: T) {\n    let value = initialValue;\n    const subscribers = new Set&lt;ReactiveEffect&gt;();\n\n    const read = () =&gt; {\n        if (activeEffect) {\n            subscribers.add(activeEffect);\n            activeEffect.deps.add(subscribers);\n        }\n        return value;\n    };\n\n    const write = (newValue: T) =&gt; {\n        if (value !== newValue) {\n            value = newValue;\n            const effectsToRun = new Set(subscribers);\n            effectsToRun.forEach((effect) =&gt; {\n                // 如果有调度器（用户 effect），放入队列；\n                // 否则立即执行（主要用于 computed 内部传递脏标记）\n                if (effect.options?.scheduler) {\n                    effect.options.scheduler(effect);\n                } else {\n                    effect();\n                }\n            });\n        }\n    };\n\n    return [read, write] as const;\n}\n\n// --- Computed 实现 (Push-Pull) ---\nexport function computed&lt;T&gt;(fn: () =&gt; T) {\n    let value: T;\n    let dirty = true; // 懒计算的核心标志\n\n    // Computed 本身既是 Signal (被别人订阅)，也是 Effect (订阅别人)\n\n    // 1. 作为 Effect 的行为：依赖变了，我不马上算，我只标记自己脏了\n    const runner = () =&gt; {\n        if (!dirty) {\n            dirty = true;\n            // 2. 作为 Signal 的行为：告诉我的订阅者“我可能变了” (Push 阶段)\n            trigger(subscribers);\n        }\n    };\n\n    const internalEffect: ReactiveEffect = runner as ReactiveEffect;\n    internalEffect.deps = new Set();\n\n    const subscribers = new Set&lt;ReactiveEffect&gt;();\n\n    // 辅助触发函数\n    const trigger = (subs: Set&lt;ReactiveEffect&gt;) =&gt; {\n        const effectsToRun = new Set(subs);\n        effectsToRun.forEach((effect) =&gt; {\n            if (effect.options?.scheduler) {\n                effect.options.scheduler(effect);\n            } else {\n                effect();\n            }\n        });\n    };\n\n    const read = () =&gt; {\n        // 1. 依赖收集 (如果有其它 effect 读取了这个 computed)\n        if (activeEffect) {\n            subscribers.add(activeEffect);\n            activeEffect.deps.add(subscribers);\n        }\n\n        // 2. 懒计算 (Pull 阶段)\n        if (dirty) {\n            const prevEffect = activeEffect;\n            activeEffect = internalEffect;\n            cleanup(internalEffect);\n            try {\n                value = fn();\n                dirty = false;\n            } finally {\n                activeEffect = prevEffect;\n            }\n        }\n        return value;\n    };\n\n    return read;\n}\n\nexport function effect(fn: VoidFunction) {\n    const effect: ReactiveEffect = () =&gt; {\n        cleanup(effect);\n        // 栈式结构，允许嵌套 effect\n        const prevEffect = activeEffect;\n        activeEffect = effect;\n        try {\n            fn();\n        } finally {\n            activeEffect = prevEffect;\n        }\n    };\n    effect.deps = new Set();\n    effect.options = { scheduler: queueJob }; // 默认走微任务调度\n    effect();\n}\n</code></pre>\n<h1>工程化与生命周期</h1>\n<p>实际应用中，通常会在组件挂载时创建多个 Effect。如果组件销毁时不清理，这些 Effect 仍会响应信号变化，导致内存泄漏。</p>\n<p>手动保存每个 Effect 的返回值并在 <code>onUnmount</code> 中逐个调用是不现实的。我们引入 <code>effectScope</code> 来批量收集和处理副作用的停止。</p>\n<h2>核心实现：作用域容器</h2>\n<p>全局变量标记“当前所在的作用域”，并让 <code>effect</code> 函数感知到它。</p>\n<pre><code class=\"language-ts\">// 1. 定义 Scope 类\nexport class EffectScope {\n    private effects: VoidFunction[] = [];\n    private active = true;\n\n    run&lt;T&gt;(fn: () =&gt; T): T | undefined {\n        if (!this.active) return;\n\n        const prevScope = activeScope;\n        activeScope = this; // 开启当前作用域\n        try {\n            return fn();\n        } finally {\n            activeScope = prevScope; // 恢复上层作用域\n        }\n    }\n\n    // 收集停止函数\n    add(stopFn: VoidFunction) {\n        if (this.active) {\n            this.effects.push(stopFn);\n        } else {\n            // 如果 Scope 已经停止，新加入的 effect 应该立即销毁\n            stopFn();\n        }\n    }\n\n    stop() {\n        if (this.active) {\n            this.effects.forEach((stop) =&gt; stop());\n            this.effects = [];\n            this.active = false;\n        }\n    }\n}\n\n// 指向当前活跃的 Scope\nlet activeScope: EffectScope | null = null;\n\nexport function effectScope(fn?: VoidFunction) {\n    const scope = new EffectScope();\n    if (fn) scope.run(fn);\n    return () =&gt; scope.stop();\n}\n</code></pre>\n<h2>改造 Effect 以支持 Scope</h2>\n<p>修改之前的 <code>effect</code> 函数，使其：</p>\n<ol>\n<li>返回一个停止函数（Disposer）。</li>\n<li>自动将这个停止函数注册到当前的 <code>activeScope</code> 中。</li>\n</ol>\n<pre><code class=\"language-ts\">export function effect(fn: VoidFunction) {\n    const effect: ReactiveEffect = () =&gt; {\n        cleanup(effect);\n        const prevEffect = activeEffect;\n        activeEffect = effect;\n        try {\n            fn();\n        } finally {\n            activeEffect = prevEffect;\n        }\n    };\n    effect.deps = new Set();\n    effect.options = { scheduler: queueJob };\n\n    // 首次执行\n    effect();\n\n    // --- 新增逻辑 ---\n\n    // 定义停止函数：彻底移除该 effect 的所有依赖\n    const stop = () =&gt; {\n        cleanup(effect);\n        // 如果有调度器中的任务，也应该移除（此处简化略过）\n    };\n\n    // 如果当前处于某个 Scope 中，自动注册\n    if (activeScope) {\n        activeScope.add(stop);\n    }\n\n    return stop;\n}\n</code></pre>\n<h1>性能优化 (Alien Signals)</h1>\n<p>目前的实现虽然功能完备，但在大规模图结构下存在性能瓶颈：</p>\n<ol>\n<li><strong>Set 的开销</strong>：<code>Set</code> 的创建、迭代和垃圾回收（GC）在极高频更新下开销显著。</li>\n<li><strong>过度遍历</strong>：Pull 模型中，每次读取 computed 都需要检查脏标记，依赖链很长时，需要层层回溯。</li>\n</ol>\n<p><a href=\"https://www.npmjs.com/package/alien-signals\">alien-signals</a> 是目前基准测试中性能最强的实现之一，核心优化思路如下：</p>\n<h2>消除 Set，使用链表</h2>\n<p>Alien Signals 不使用 <code>Set</code> 来存储依赖，而是使用<strong>双向链表</strong>直接存储节点关系。这极大地减少了内存分配和 GC 压力。</p>\n<h2>版本号机制 (Versioning / Epochs)</h2>\n<p>这是性能的关键。它不依赖简单的 <code>dirty</code> 布尔值，而是引入了全局版本号。</p>\n<ul>\n<li><strong>全局版本 (Global Version)</strong>：每次任何 Signal 更新，全局版本号 +1。</li>\n<li><strong>节点版本 (Node Version)</strong>：Signal 记录自己最后一次更新时的版本号。</li>\n<li><strong>依赖检查</strong>：\nComputed 不需要每次都去问上游 &quot;你变了吗？&quot;，它只需要检查：\n<code>LastSeenVersion &lt; GlobalVersion</code> ?\n如果全局版本没变，说明整个系统没有任何状态更新，直接返回缓存，<strong>O(1) 复杂度</strong>。</li>\n</ul>\n<h2>信号增强 (Signal Boosting)</h2>\n<p>在某些情况下，Computed 可以跳过依赖图的遍历。如果一个 Computed 的所有依赖都没有变化（通过比较版本号），它可以直接更新自己的版本号到最新，而不需要执行计算函数。</p>\n<h2>简化版源码示意 (概念模型)</h2>\n<pre><code class=\"language-ts\">let globalEpoch = 1;\n\nclass Signal&lt;T&gt; {\n    epoch = 0; // 最后更新的时间戳\n    value: T;\n\n    write(newVal: T) {\n        this.value = newVal;\n        this.epoch = ++globalEpoch; // 更新时推进全局时间\n    }\n}\n\nclass Computed&lt;T&gt; {\n    lastCheckEpoch = 0; // 上次检查的时间\n    cachedValue: T;\n\n    read() {\n        // 如果上次检查后，全局时间都没动过，说明肯定没变\n        if (this.lastCheckEpoch === globalEpoch) {\n            return this.cachedValue;\n        }\n\n        // ... 这里再进行细粒度的依赖版本对比 ...\n    }\n}\n</code></pre>\n<p>这种设计使得 Alien Signals 在只有少量信号变更、但计算链路很深的大型应用中，性能表现远超传统的“脏标记+递归通知”模式。</p>\n","tags":["js"]},{"id":"windows-setup","url":"https://yieldray.fun/posts/windows-setup","title":"windows setup","date_published":"2026-01-02T13:50:11.000Z","date_modified":"2026-01-02T13:50:11.000Z","content_text":"<p>下载 ISO 镜像：<br><a href=\"https://www.microsoft.com/zh-cn/software-download/windows11\">https://www.microsoft.com/zh-cn/software-download/windows11</a><br><a href=\"https://msdn.itellyou.cn/\">https://msdn.itellyou.cn/</a><br><a href=\"https://uupdump.net/\">https://uupdump.net/</a></p>\n<p>制作启动盘工具：<br><a href=\"https://rufus.ie/zh/\">https://rufus.ie/zh/</a><br><a href=\"https://www.ventoy.net/\">https://www.ventoy.net/</a><br><a href=\"https://www.firpe.cn/\">https://www.firpe.cn/</a></p>\n<p>激活脚本：<br><a href=\"https://github.com/massgravel/Microsoft-Activation-Scripts\">https://github.com/massgravel/Microsoft-Activation-Scripts</a></p>\n<p>安装 Office：<br><a href=\"https://github.com/YerongAI/Office-Tool\">https://github.com/YerongAI/Office-Tool</a></p>\n<p>系统清理优化：<br><a href=\"https://github.com/zoicware/RemoveWindowsAI\">https://github.com/zoicware/RemoveWindowsAI</a><br><a href=\"https://github.com/Raphire/Win11Debloat\">https://github.com/Raphire/Win11Debloat</a><br><a href=\"https://github.com/hellzerg/optimizer\">https://github.com/hellzerg/optimizer</a><br><a href=\"https://github.com/flick9000/winscript\">https://github.com/flick9000/winscript</a></p>\n<p>图吧工具箱：<br><a href=\"https://www.tbtool.cn/\">https://www.tbtool.cn/</a></p>\n<p>系统工具：<br><a href=\"https://learn.microsoft.com/zh-cn/sysinternals/\">https://learn.microsoft.com/zh-cn/sysinternals/</a><br><a href=\"https://www.winpenpack.com/\">https://www.winpenpack.com/</a><br><a href=\"http://dimio.altervista.org/eng/\">http://dimio.altervista.org/eng/</a><br><a href=\"https://litemonitor.cn/\">https://litemonitor.cn/</a></p>\n<p>包管理器：<br><a href=\"https://scoop.sh/\">https://scoop.sh/</a><br><a href=\"https://gitee.com/scoop-installer\">https://gitee.com/scoop-installer</a><br><a href=\"https://chocolatey.org/\">https://chocolatey.org/</a><br><a href=\"https://learn.microsoft.com/zh-cn/windows/package-manager/winget/\">https://learn.microsoft.com/zh-cn/windows/package-manager/winget/</a></p>\n<p>DirectX 修复工具：<br><a href=\"https://www.microsoft.com/zh-cn/download/details.aspx?id=35\">https://www.microsoft.com/zh-cn/download/details.aspx?id=35</a><br><a href=\"https://blog.csdn.net/vbcom/article/details/7245186\">https://blog.csdn.net/vbcom/article/details/7245186</a></p>\n<p>右键菜单：<br><a href=\"https://github.com/moudey/Shell\">https://github.com/moudey/Shell</a></p>\n<p>开发通用：<br><a href=\"https://github.com/RubyMetric/chsrc\">https://github.com/RubyMetric/chsrc</a><br><a href=\"https://zed.dev/\">https://zed.dev/</a><br><a href=\"https://reqable.com/\">https://reqable.com/</a></p>\n","tags":["windows"]},{"id":"box-shadow-is-outline","url":"https://yieldray.fun/posts/box-shadow-is-outline","title":"box-shadow是outline的替代品","date_published":"2025-12-20T07:53:10.000Z","date_modified":"2025-12-20T07:53:10.000Z","content_text":"<p>使用 outline 而非 border 的原因在于 border 需要实际布局空间（盒模型的一部分）。<br>outline 不属于盒模型，不占空间，因此不会造成布局抖动。<br>例如动画场景，border 从无到有，为了避免布局抖动，使用 border 时需要预留空间，比较麻烦。</p>\n<p>当只需要实心 outline 时，即 <code>outline-style: solid</code> 时，box-shadow 可以作为 outline 的替代品。<br>box-shadow 也和 outline 一样遵循 border-radius。</p>\n<p>总的来说，除了 outline-style，outline-color outline-offset outline-width 都可通过 box-shadow 实现。</p>\n<h1>例子</h1>\n<p>outline-offset</p>\n<pre><code class=\"language-css\">button {\n    /* 相当于 outline-width: calc(4px - 2px) */\n    box-shadow:\n        0 0 0 2px white /* 需要与背景色一致，伪 outline-offset */,\n        0 0 0 4px indigo /* box-shadow 伪边框 */;\n}\n</code></pre>\n<p>outline 的最大缺点是不能仅在指定方向绘制，box-shadow 可以做到。<br>仅在指定方向有边框，下面的例子在水平方向均有边框（相当于 border-inline）</p>\n<pre><code class=\"language-css\">button {\n    /* 相当于 outline-width: 2px */\n    box-shadow:\n        2px 0 0 0 indigo,\n        -2px 0 0 0 indigo;\n}\n</code></pre>\n<p>还可以在元素内部绘制（相当于 outline-offset 为负数）</p>\n<pre><code class=\"language-css\">button {\n    box-shadow: inset 0 0 0 2px #3498db;\n}\n</code></pre>\n<h1>缺点</h1>\n<p>box-shadow 不在 border-box 内绘制，因此可能不适合与 border 一起使用。</p>\n","tags":["css"]},{"id":"c-simple-epoll-http-server","url":"https://yieldray.fun/posts/c-simple-epoll-http-server","title":"实现一个使用 epoll 的极简 HTTP 服务器","date_published":"2025-11-09T20:26:40.000Z","date_modified":"2025-11-09T20:26:40.000Z","content_text":"<p>实现一个使用 epoll 的极简 HTTP 服务器 (仅支持 GET/POST, 简单路由)。</p>\n<blockquote>\n<p>Source: <a href=\"https://github.com/flouthoc/envelop.c\">https://github.com/flouthoc/envelop.c</a><br>Author: <a href=\"mailto:flouthoc@gmail.com\">flouthoc@gmail.com</a>\nMIT LICENSE</p>\n</blockquote>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\n#include &lt;string.h&gt;\n#include &lt;sys/types.h&gt;\n#include &lt;sys/socket.h&gt;\n#include &lt;unistd.h&gt;\n#include &lt;stdlib.h&gt;\n#include &lt;errno.h&gt;\n#include &lt;arpa/inet.h&gt;\n#include &lt;sys/epoll.h&gt;\n#include &lt;fcntl.h&gt;\n\n/* 命名约定简述:\n    MACROS 全大写; ENUMS 首字母大写其余小写; STRUCTS 全小写下划线; 变量全小写。\n*/\n\n// 服务器监听地址 0.0.0.0:SERVER_PORT (对所有本地接口开放)\n#define SERVER_PORT 3000\n\n/* 支持的 HTTP 版本标识 */\n#define VERSION_11 &quot;1.1&quot;\n#define VERSION_10 &quot;1.0&quot;\n/* 事件数组最大容量 (epoll) */\n#define MAXEVENTS 64\n\n\n/* 全局临时缓冲区 (复用以减少栈分配) */\nchar global_buffer[1024];\n\n/* 仅实现需要支持的请求方法 */\nenum Method {\n  GET,\n  POST\n};\n\n/* 常用 HTTP 状态码 (只列出当前用到的) */\nenum Http_status {\n  HTTP_STATUS_OK = 200,\n  HTTP_STATUS_CREATED = 201,\n  HTTP_STATUS_ACCEPTED = 202,\n  HTTP_STATUS_BAD_REQUEST = 400,\n  HTTP_STATUS_NOT_FOUND = 404,\n};\n\n/* 解析后的请求行数据 (仅保留必要字段) */\nstruct http_request {\n  char version[4];\n  enum Method method;\n  char * uri;\n};\n\n/* 构造用的响应头及可选正文 */\nstruct http_response {\n  char * header;\n  size_t length;\n  int is_content; // 如果此响应中有消息体，则为1；如果没有，则为0，这只是为了在回复时额外添加换行\n};\n\n// 打印响应头 (调试用)，保留 \\r\\n 可视化\nvoid print_rn(char * str) {\n  while ( * str) {\n    if ( * str == &#39;\\n&#39;) {\n      fputs(&quot;\\n&quot;, stdout);\n    } else if ( * str == &#39;\\r&#39;) {\n      fputs(&quot;\\r&quot;, stdout);\n    } else {\n      fputc( * str, stdout);\n    }\n    str++;\n  }\n  fputs(&quot;\\n&quot;, stdout);\n}\n\n// 扩展响应缓冲区并追加字符串 (header + 内容)\nstatic inline void extend_header(struct http_response * handle,\n  const char * extension, size_t length) {\n  struct http_response * h = handle;\n  h -&gt; length += length;\n  h -&gt; header = realloc(h -&gt; header, h -&gt; length);\n  strcat(h -&gt; header, extension);\n}\n\n// 发送响应并 free 结构体\nstatic void reply(struct http_response * handle, int connectionfd) {\n\n  int written, n;\n  char * buf;\n  if (handle -&gt; is_content == 0) extend_header(handle, &quot;\\r\\n&quot;, 2);\n  n = handle -&gt; length;\n  buf = handle -&gt; header;\n\n  print_rn(buf);\n\n  while (n &gt; 0) {\n    if ((written = write(connectionfd, buf, (handle -&gt; length + 1))) &gt; 0) {\n      buf += written;\n      n -= written;\n    } else {\n      perror(&quot;When replying to file descriptor&quot;);\n    }\n  }\n\n  free(handle -&gt; header);\n  free(handle);\n\n}\n\n/* TODO: 可扩展为最简模板解析器 (占位) */\nstatic char * readTemplate(char * filename) {\n  if (filename != NULL) {\n    /* 预留: 读取文件并返回动态内容 */\n  }\n  return NULL;\n}\n\n// 构造状态行 (HTTP/X.X &lt;code&gt;) 并初始化响应结构\nstatic inline void prepare_status_line(struct http_response ** response_handle, char * version, enum Http_status status) {\n  struct http_response * r = malloc(sizeof(struct http_response));\n  /* Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF */\n  /* I wont use Reason-Phrase cause its not important for me now */\n  r -&gt; header = malloc(16 * sizeof(char)); // hard-coding length for HTTP/X.X(8) SP(1) STATUS-CODE(3) SP(1) CRLF(2)\n  r -&gt; length = sprintf(r -&gt; header, &quot;HTTP/%s %d \\r\\n&quot;, version, status);\n  r -&gt; is_content = 0;\n  * response_handle = r;\n}\n\n// 添加单个 header 行\nvoid setHeader(struct http_response * handle, char * header, char * value) {\n  size_t len;\n  char local_buffer[1024];\n  strcpy(local_buffer, value);\n  len = sprintf(global_buffer, &quot;%s: %s\\r\\n&quot;, header, local_buffer);\n  extend_header(handle, global_buffer, len);\n}\n\n// 设置正文相关头并发送带内容的响应\nvoid replywithContent(struct http_response * response_handle, int connectionfd,\n  const char * content) {\n  size_t content_length = strlen(content);\n  sprintf(global_buffer, &quot;%d&quot;, (int) content_length);\n  setHeader(response_handle, &quot;Content-Length&quot;, global_buffer);\n  setHeader(response_handle, &quot;Content-Type&quot;, &quot;text/html&quot;);\n  extend_header(response_handle, &quot;\\r\\n&quot;, 2);\n  response_handle -&gt; is_content = 1;\n  extend_header(response_handle, content, content_length);\n  reply(response_handle, connectionfd);\n}\n\n/* 路由: 根据 URI 返回不同内容 (简易示例) */\nvoid router(struct http_request * request_handle, int connectionfd, struct http_response ** response_handle) {\n  if (!strcmp(request_handle -&gt; uri, &quot;/hello&quot;)) {\n    prepare_status_line(response_handle, request_handle -&gt; version, HTTP_STATUS_OK);\n    replywithContent( * response_handle, connectionfd, &quot;Hello&quot;);\n  } else if (!strcmp(request_handle -&gt; uri, &quot;/hey&quot;)) {\n    prepare_status_line(response_handle, request_handle -&gt; version, HTTP_STATUS_OK);\n    replywithContent( * response_handle, connectionfd, &quot;Hey hi this is a test&quot;);\n  } else {\n    prepare_status_line(response_handle, request_handle -&gt; version, HTTP_STATUS_NOT_FOUND);\n    replywithContent( * response_handle, connectionfd, &quot;Not Found&quot;);\n  }\n}\n\n// 解析请求行 (方法 URI 版本)，仅支持 GET/POST + HTTP/1.0/1.1\nstatic int parse_http_request(struct http_request ** req, char * request_buffer) /* 仅解析运行所需部分 */ {\n  int length;\n  char * uri;\n  char * token_handler;\n  struct http_request * p = (struct http_request * )(malloc(sizeof(struct http_request)));\n  token_handler = strtok(request_buffer, &quot; &quot;);\n\n  if (!strcmp(token_handler, &quot;GET&quot;)) p -&gt; method = GET;\n  else if (!strcmp(token_handler, &quot;POST&quot;)) p -&gt; method = POST;\n  else return 0;\n\n  token_handler = strtok(NULL, &quot; &quot;);\n  length = strlen(token_handler);\n  uri = malloc(sizeof(char) * (length + 1));\n  strcpy(uri, token_handler);\n  p -&gt; uri = uri;\n\n  token_handler = strtok(NULL, &quot; &quot;);\n  if (!strncmp(token_handler, &quot;HTTP/1.1\\r\\n&quot;, 10)) strcpy(p -&gt; version, VERSION_11);\n  else if (!strncmp(token_handler, &quot;HTTP/1.0\\r\\n&quot;, 10)) strcpy(p -&gt; version, VERSION_10);\n  else return 0;\n\n  * req = p;\n  return 1;\n}\n\n// 根据解析结果分发响应 (目前只区分 GET)\nvoid respond(struct http_request * request_handle, int connectionfd) {\n  struct http_response * response_handle;\n  if (request_handle -&gt; method == GET) router(request_handle, connectionfd, &amp; response_handle);\n  else goto error;\n  return;\n\n  error:\n    prepare_status_line( &amp; response_handle, request_handle -&gt; version, HTTP_STATUS_BAD_REQUEST);\n  reply(response_handle, connectionfd);\n}\n\nint main() {\n  int n, i;\n  int listenfd;\n  int listenfd_flags;\n  int epoll_fd;\n  char msg_buffer[1024];\n  char temp_buffer[1024];\n  int connectionfd;\n  int client_length;\n\n  /* 请求结构体指针 (解析成功后填充) */\n  struct http_request * request_handle;\n\n  /* sockaddr_in: 保存服务器与客户端地址信息 */\n  struct sockaddr_in server_address, client_address;\n\n  /* epoll_event: 事件数据; 仅在支持 epoll 的系统运行 (Linux) */\n  struct epoll_event event;\n  struct epoll_event * events;\n\n  char greet[] = &quot;Hello&quot;;\n\n  /* 创建监听套接字 */\n  listenfd = socket(AF_INET, SOCK_STREAM, 0);\n\n  if (listenfd == -1) {\n    perror(&quot;Socket&quot;);\n    exit(1);\n  }\n\n  /* 清零地址结构 */\n  memset(&amp;server_address, 0, sizeof(server_address));\n\n  /* 设置地址族/地址/端口 */\n  server_address.sin_family = AF_INET;\n  server_address.sin_addr.s_addr = inet_addr(&quot;0.0.0.0&quot;);\n  /* 端口号定义见顶部宏 */\n  server_address.sin_port = htons(SERVER_PORT);\n\n  /* 绑定监听地址与端口 */\n  if (bind(listenfd, (struct sockaddr * ) &amp; server_address, sizeof(server_address)) == -1) {\n    perror(&quot;Bind&quot;);\n    exit(1);\n  }\n\n  if ((listenfd_flags = fcntl(listenfd, F_GETFL, 0)) == -1) {\n    perror(&quot;While trying to get listenfd flags&quot;);\n    exit(1);\n  }\n\n  /* 设置非阻塞 (事件驱动需要) */\n  listenfd_flags |= O_NONBLOCK;\n\n  if (fcntl(listenfd, F_SETFL, listenfd_flags) == -1) {\n    perror(&quot;While trying to set flags&quot;);\n    exit(1);\n  }\n\n  if (listen(listenfd, 10) == -1) {\n    perror(&quot;Listen&quot;);\n    exit(1);\n  }\n\n  if ((epoll_fd = epoll_create1(0)) == -1) {\n    perror(&quot;epoll_create&quot;);\n    exit(1);\n  }\n\n  event.data.fd = listenfd;\n  event.events = EPOLLIN;\n  if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listenfd, &amp; event) == -1) {\n    perror(&quot;Epoll context&quot;);\n    exit(1);\n  }\n\n  events = calloc(MAXEVENTS, sizeof event);\n\n  /* 主事件循环 (epoll_wait) */\n  while (1) {\n\n    /* 等待就绪事件 */\n    n = epoll_wait(epoll_fd, events, MAXEVENTS, -1);\n    for (i = 0; i &lt; n; i++) {\n\n      /* 若事件包含错误/挂断或非可读则关闭并跳过 */\n      if ((events[i].events &amp; EPOLLERR) || (events[i].events &amp; EPOLLHUP) || (!(events[i].events &amp; EPOLLIN))) {\n        /* 文件描述符尚未连接用于传输，可以在不关闭的情况下关闭 */\n        close(events[i].data.fd);\n        continue;\n      } else if (listenfd == events[i].data.fd) {\n\n        // 新连接到来: 循环接受所有就绪连接\n        while (1) {\n\n          /* 接受连接 */\n          int connectionfd_flags;\n          connectionfd = accept(listenfd, (struct sockaddr * ) &amp; client_address, &amp; client_length);\n          if (connectionfd == -1) {\n\n            if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {\n              /* 已处理所有传入连接 */\n              break;\n            } else {\n              perror(&quot;accept&quot;);\n              break;\n            }\n          }\n\n          /* 设置客户端套接字非阻塞 */\n          if ((connectionfd_flags = fcntl(connectionfd, F_GETFL, 0)) == -1) {\n            perror(&quot;While trying to get listenfd flags&quot;);\n            exit(1);\n          }\n\n          connectionfd_flags |= O_NONBLOCK;\n\n          if (fcntl(connectionfd, F_SETFL, connectionfd_flags) == -1) {\n            perror(&quot;While trying to set flags&quot;);\n            exit(1);\n          }\n\n          // 将客户端套接字加入 epoll 监听\n          event.data.fd = connectionfd;\n          event.events = EPOLLIN;\n          if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, connectionfd, &amp; event) == -1) {\n            perror(&quot;epoll_ctl&quot;);\n            exit(1);\n          }\n        }\n        continue;\n      } else {\n        // 已有连接: 读取并处理请求\n        memset(msg_buffer, 0, sizeof(msg_buffer));\n        memset(temp_buffer, 0, sizeof(temp_buffer));\n\n        // 读取请求 (简单循环, 遇到 -1 退出)\n        while ((n = read(events[i].data.fd, msg_buffer, 1024))) {\n\n          if (n == -1) {\n            //\tprintf(&quot;Might be EAGAIN\\n&quot;);\n            break;\n          }\n\n        }\n\n        // 解析 HTTP 请求\n        strcpy(temp_buffer, msg_buffer);\n        if (!parse_http_request( &amp; request_handle, temp_buffer)) {\n          printf(&quot;Request Unparsablen\\n&quot;);\n          // 解析失败: 关闭连接\n          if (shutdown(events[i].data.fd, SHUT_RDWR) == -1) {\n            perror(&quot;shutdown&quot;);\n          }\n\n          continue;\n        }\n\n        // 构造并发送响应\n        respond(request_handle, events[i].data.fd);\n        // 关闭连接并释放资源\n        if (shutdown(events[i].data.fd, SHUT_RDWR) == -1) {\n          perror(&quot;shutdown&quot;);\n        }\n\n        free(request_handle -&gt; uri);\n        free(request_handle);\n        //printf(&quot;%s\\n&quot;, temp_buffer);\n      }\n    }\n  }\n\n\n  /*\n\n  // This was some rough loop so i&#39;m commenting it\n  while(1){\n      client_length = sizeof(client_address);\n      memset(msg_buffer, 0, sizeof(msg_buffer));\n      memset(temp_buffer, 0, sizeof(temp_buffer));\n      connectionfd = accept(listenfd, (struct sockaddr*)&amp;client_address, &amp;client_length);\n      while( n = recv(connectionfd, msg_buffer, 1024, MSG_DONTWAIT)){\n\n          //printf(&quot;%sn&quot;, msg_buffer);\n          //printf(&quot;%zdn&quot;, strlen(msg_buffer));\n          if(strstr(msg_buffer, &quot;rnrn&quot;))\n          break;\n\n\n          //if( n &lt; 0)\n          //    if(errno == EWOULDBLOCK)\n          //        break;\n      }\n      strcpy(temp_buffer, msg_buffer);\n      if(!parse_http_request(&amp;request_handle, temp_buffer)){\n          printf(&quot;Request Unparsable&quot;);\n          close(connectionfd);\n          continue;\n      }\n      respond(request_handle, connectionfd);\n      close(connectionfd);\n      free(request_handle-&gt;uri);\n      free(request_handle);\n  }\n\n  */\n\n  exit(0);\n}\n</code></pre>\n","tags":[]},{"id":"web-encode-uri","url":"https://yieldray.fun/posts/web-encode-uri","title":"Web URI 编码","date_published":"2025-10-19T22:47:56.000Z","date_modified":"2025-10-19T22:47:56.000Z","content_text":"<h1>前言</h1>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent\"><code>encodeURIComponent()</code></a> 和 <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams\"><code>URLSearchParams</code></a> 都可用于 URI 组件编码。目前，前者由 ECMA262 定义，后者由 HTML 标准定义。</p>\n<p>RFC3986 定义的 <a href=\"https://datatracker.ietf.org/doc/html/rfc3986#section-3\">URI 组件</a>如下：</p>\n<pre><code>  foo://example.com:8042/over/there?name=ferret#nose\n  \\_/   \\______________/\\_________/ \\_________/ \\__/\n   |           |            |            |        |\nscheme     authority       path        query   fragment\n   |   _____________________|__\n  / \\ /                        \\\n  urn:example:animal:ferret:nose\n</code></pre>\n<p>备注：某些 scheme 的组件 在 <a href=\"https://url.spec.whatwg.org/\">URL 对象</a>中会被<a href=\"https://url.spec.whatwg.org/#url-miscellaneous\">特殊处理</a> （scheme 为：ftp/file/http/https/ws/wss）。</p>\n<pre><code class=\"language-js\">new URL(&quot;http://domain域.域&quot;).toString();\n// =&gt; &quot;http://xn--domain-u21k.xn--cjs/&quot;\n\nnew URL(&quot;ws://domain域.域&quot;).toString();\n// =&gt; &quot;ws://xn--domain-u21k.xn--cjs/&quot;\n\nnew URL(&quot;file://domain域.域&quot;).toString();\n// =&gt; &quot;file://xn--domain-u21k.xn--cjs/&quot;\n\nnew URL(&quot;ftp://domain域.域&quot;).toString();\n// =&gt; &quot;ftp://xn--domain-u21k.xn--cjs/&quot;\n\nnew URL(&quot;git://domain域.域&quot;).toString();\n// =&gt; &quot;git://domain%E5%9F%9F.%E5%9F%9F&quot;\n</code></pre>\n<h1>encodeURIComponent</h1>\n<p>encodeURIComponent 遵循 <a href=\"https://datatracker.ietf.org/doc/html/rfc2396#section-2.2\">RFC2396</a>，</p>\n<pre><code>reserved    = &quot;;&quot; | &quot;/&quot; | &quot;?&quot; | &quot;:&quot; | &quot;@&quot; | &quot;&amp;&quot; | &quot;=&quot; | &quot;+&quot; |\n            &quot;$&quot; | &quot;,&quot;\n\nunreserved  = alphanum | mark\n\nmark        = &quot;-&quot; | &quot;_&quot; | &quot;.&quot; | &quot;!&quot; | &quot;~&quot; | &quot;*&quot; | &quot;&#39;&quot; | &quot;(&quot; | &quot;)&quot;\n</code></pre>\n<p>而非 <a href=\"https://datatracker.ietf.org/doc/html/rfc3986#section-2.1\">RFC3986</a>。</p>\n<pre><code>reserved    = gen-delims / sub-delims\n\ngen-delims  = &quot;:&quot; / &quot;/&quot; / &quot;?&quot; / &quot;#&quot; / &quot;[&quot; / &quot;]&quot; / &quot;@&quot;\n\nsub-delims  = &quot;!&quot; / &quot;$&quot; / &quot;&amp;&quot; / &quot;&#39;&quot; / &quot;(&quot; / &quot;)&quot;\n            / &quot;*&quot; / &quot;+&quot; / &quot;,&quot; / &quot;;&quot; / &quot;=&quot;\n\nunreserved  = ALPHA / DIGIT / &quot;-&quot; / &quot;.&quot; / &quot;_&quot; / &quot;~&quot;\n</code></pre>\n<blockquote>\n<p>因为 RFC3986 支持了 IPv6。</p>\n</blockquote>\n<p>在 encodeURIComponent 中，大小写、数字和 <code>-_.!~*&#39;()</code> 不会被转义，即： <code>abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*&#39;()</code> 不会被转义。</p>\n<p>要支持 RFC3986，还需要转义 <code>!&#39;()*</code>。<br>要支持 RFC5987，还需要转义 <code>[&#39;()*]</code>，无需转义 <code>|`^</code>， （用于 Content-Disposition 和 Link 标头）</p>\n<pre><code class=\"language-js\">function encodeRFC3986URIComponent(str) {\n    return encodeURIComponent(str).replace(/[!&#39;()*]/g, (c) =&gt; `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n\nfunction encodeRFC5987ValueChars(str) {\n    return encodeURIComponent(str)\n        .replace(/[&#39;()*]/g, (c) =&gt; `%${c.charCodeAt(0).toString(16).toUpperCase()}`)\n        .replace(/%(7C|60|5E)/g, (str, hex) =&gt; String.fromCharCode(parseInt(hex, 16)));\n}\n</code></pre>\n<hr>\n<p>下为 <a href=\"https://github.com/engine262/engine262/blob/f915a64e03381af90c5b1da46f9c7cf464e68799/src/intrinsics/URIHandling.mts#L303\">engine262</a> 实现：</p>\n<pre><code class=\"language-ts\">const uriReserved = &quot;;/?:@&amp;=+$,&quot;;\nconst uriAlpha = &quot;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;;\nconst uriMark = &quot;-_.!~*&#39;()&quot;;\nconst DecimalDigit = &quot;0123456789&quot;;\nconst uriUnescaped = uriAlpha + DecimalDigit + uriMark;\n\n/** https://tc39.es/ecma262/#sec-encodeuricomponent-uricomponent */\nfunction* encodeURIComponent([uriComponent = Value.undefined]: Arguments): ValueEvaluator {\n    // 1. Let componentString be ? ToString(uriComponent).\n    const componentString = Q(yield* ToString(uriComponent));\n    // 2. Let unescapedURIComponentSet be a String containing one instance of each code unit valid in uriUnescaped.\n    const unescapedURIComponentSet = uriUnescaped;\n    // 3. Return ? Encode(componentString, unescapedURIComponentSet).\n    return Q(Encode(componentString, unescapedURIComponentSet));\n}\n\n/** https://tc39.es/ecma262/#sec-encodeuri-uri */\nfunction* encodeURI([uri = Value.undefined]: Arguments): ValueEvaluator {\n    // 1. Let uriString be ? ToString(uri).\n    const uriString = Q(yield* ToString(uri));\n    // 2. Let unescapedURISet be a String containing one instance of each code unit valid in uriReserved and uriUnescaped plus &quot;#&quot;.\n    const unescapedURISet = `${uriReserved}${uriUnescaped}#`;\n    // 3. Return ? Encode(uriString, unescapedURISet).\n    return Q(Encode(uriString, unescapedURISet));\n}\n</code></pre>\n<blockquote>\n<p>不难看出，encodeURI 除了不会转义 <code>abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*&#39;()</code> 以外，还不会转义 <code>;/?:@&amp;=+$,#</code>。</p>\n</blockquote>\n<p>下为 <a href=\"https://github.com/bellard/quickjs/blob/eb2c89087def1829ed99630cb14b549d7a98408c/quickjs.c#L52945\">quickjs</a> 实现：</p>\n<pre><code class=\"language-c\">static JSValue js_global_encodeURI(JSContext *ctx, JSValueConst this_val,\n                                   int argc, JSValueConst *argv,\n                                   int isComponent)\n{\n    JSValue str;\n    StringBuffer b_s, *b = &amp;b_s;\n    JSString *p;\n    int k, c, c1;\n\n    str = JS_ToString(ctx, argv[0]);\n    if (JS_IsException(str))\n        return str;\n\n    p = JS_VALUE_GET_STRING(str);\n    string_buffer_init(ctx, b, p-&gt;len);\n    for (k = 0; k &lt; p-&gt;len;) {\n        c = string_get(p, k);\n        k++;\n        if (isURIUnescaped(c, isComponent)) {\n            string_buffer_putc16(b, c);\n        } else {\n            if (is_lo_surrogate(c)) {\n                js_throw_URIError(ctx, &quot;invalid character&quot;);\n                goto fail;\n            } else if (is_hi_surrogate(c)) {\n                if (k &gt;= p-&gt;len) {\n                    js_throw_URIError(ctx, &quot;expecting surrogate pair&quot;);\n                    goto fail;\n                }\n                c1 = string_get(p, k);\n                k++;\n                if (!is_lo_surrogate(c1)) {\n                    js_throw_URIError(ctx, &quot;expecting surrogate pair&quot;);\n                    goto fail;\n                }\n                c = from_surrogate(c, c1);\n            }\n            if (c &lt; 0x80) {\n                encodeURI_hex(b, c);\n            } else {\n                /* XXX: use C UTF-8 conversion ? */\n                if (c &lt; 0x800) {\n                    encodeURI_hex(b, (c &gt;&gt; 6) | 0xc0);\n                } else {\n                    if (c &lt; 0x10000) {\n                        encodeURI_hex(b, (c &gt;&gt; 12) | 0xe0);\n                    } else {\n                        encodeURI_hex(b, (c &gt;&gt; 18) | 0xf0);\n                        encodeURI_hex(b, ((c &gt;&gt; 12) &amp; 0x3f) | 0x80);\n                    }\n                    encodeURI_hex(b, ((c &gt;&gt; 6) &amp; 0x3f) | 0x80);\n                }\n                encodeURI_hex(b, (c &amp; 0x3f) | 0x80);\n            }\n        }\n    }\n    JS_FreeValue(ctx, str);\n    return string_buffer_end(b);\n\nfail:\n    JS_FreeValue(ctx, str);\n    string_buffer_free(b);\n    return JS_EXCEPTION;\n}\n</code></pre>\n<h1>URLSearchParams</h1>\n<p><a href=\"https://datatracker.ietf.org/doc/html/rfc1866#section-8.2.1\">RFC1866</a> 和 <a href=\"https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm\">HTML 标准</a> 均定义过 <code>application/x-www-form-urlencoded</code>。</p>\n<p><a href=\"https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior\">URLSearchParams.prototype.toString()</a> 遵循 <code>application/x-www-form-urlencoded</code>。<br>在 URLSearchParams 中，空格会被编码为 <code>+</code> 而不是 <code>%20</code>。</p>\n<p>大小写、数字和 <code>-_.*</code> 不会被转义，空格被编码为 <code>+</code>，其余字符按<a href=\"https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding\">百分号编码</a>。</p>\n<h1>总结</h1>\n<table>\n<thead>\n<tr>\n<th>规范</th>\n<th>不编码字符</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><strong>RFC2396</strong></td>\n<td><code>abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*&#39;()</code></td>\n</tr>\n<tr>\n<td><strong>RFC3986</strong></td>\n<td><code>abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~</code></td>\n</tr>\n<tr>\n<td><strong>application/x-www-form-urlencoded</strong></td>\n<td><code>abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.*</code></td>\n</tr>\n</tbody></table>\n<pre><code class=\"language-js\">const encodeQueryParam = (s) =&gt; new URLSearchParams({ &quot;&quot;: s }).toString().slice(1);\n\nencodeQueryParam(&quot;~&quot;); // =&gt; %7E\n\n// decodeURIComponent 会解码所有百分号编码串，但是注意查询字符串中空格会被编码为加号\nconst decodeQueryParam = (s) =&gt; decodeURIComponent(s.replace(/\\+/g, &quot; &quot;));\n\ndecodeQueryParam(&quot;%7E&quot;); // =&gt; ~\n</code></pre>\n","tags":["js"]},{"id":"frontend-news","url":"https://yieldray.fun/posts/frontend-news","title":"frontend news source","date_published":"2025-10-04T12:00:00.000Z","date_modified":"2025-10-04T12:00:00.000Z","content_text":"<h1>Browser</h1>\n<p><a href=\"https://web.dev/blog\">https://web.dev/blog</a></p>\n<p><a href=\"https://developer.chrome.com/blog\">https://developer.chrome.com/blog</a></p>\n<p><a href=\"https://blog.chromium.org/\">https://blog.chromium.org/</a></p>\n<p><a href=\"https://webkit.org/blog/\">https://webkit.org/blog/</a></p>\n<p><a href=\"https://blog.nightly.mozilla.org/\">https://blog.nightly.mozilla.org/</a></p>\n<p><a href=\"https://blogs.windows.com/msedgedev/posts/\">https://blogs.windows.com/msedgedev/posts/</a></p>\n<hr>\n<p><a href=\"https://developer.chrome.com/release-notes\">https://developer.chrome.com/release-notes</a></p>\n<p><a href=\"https://developer.apple.com/documentation/safari-release-notes\">https://developer.apple.com/documentation/safari-release-notes</a></p>\n<p><a href=\"https://www.mozilla.org/en-US/firefox/releases/\">https://www.mozilla.org/en-US/firefox/releases/</a></p>\n<h1>JS</h1>\n<p><a href=\"https://v8.dev/blog\">https://v8.dev/blog</a></p>\n<p><a href=\"https://spidermonkey.dev/blog/\">https://spidermonkey.dev/blog/</a></p>\n<p><a href=\"https://2ality.com/\">https://2ality.com/</a></p>\n<h1>CSS</h1>\n<p><a href=\"https://css-tricks.com/\">https://css-tricks.com/</a></p>\n<p><a href=\"https://www.smashingmagazine.com/category/css/\">https://www.smashingmagazine.com/category/css/</a></p>\n<p><a href=\"https://frontendmasters.com/blog/\">https://frontendmasters.com/blog/</a></p>\n<p><a href=\"https://piccalil.li/\">https://piccalil.li/</a></p>\n<p><a href=\"https://css-tip.com/\">https://css-tip.com/</a></p>\n<p><a href=\"https://www.w3.org/groups/wg/css/publications/\">https://www.w3.org/groups/wg/css/publications/</a></p>\n<h1>Feature</h1>\n<p><a href=\"https://web-platform-dx.github.io/web-features-explorer/\">https://web-platform-dx.github.io/web-features-explorer/</a></p>\n<p><a href=\"https://webstatus.dev/\">https://webstatus.dev/</a></p>\n<p><a href=\"https://chromestatus.com/\">https://chromestatus.com/</a></p>\n<p><a href=\"https://polypane.app/experimental-web-platform-features/\">https://polypane.app/experimental-web-platform-features/</a></p>\n<p><a href=\"https://bcd-watch.igalia.com/\">https://bcd-watch.igalia.com/</a></p>\n<h1>Other</h1>\n<p><a href=\"https://frontenddogma.com/\">https://frontenddogma.com/</a></p>\n","tags":[]},{"id":"what-you-need-to-know-about-modern-css-2025-edition","url":"https://yieldray.fun/posts/what-you-need-to-know-about-modern-css-2025-edition","title":"你需要了解的现代 CSS（2025 版）","date_published":"2025-10-01T20:25:00.000Z","date_modified":"2025-10-01T20:25:00.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-2025-edition/\">https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-2025-edition/</a></p>\n</blockquote>\n<p>我们去年（2024）发布了<a href=\"/posts/what-you-need-to-know-about-modern-css-spring-2024-edition\">《你需要了解的现代 CSS》</a>，当时我还不确定一年后是否有足够的新内容值得再写一版。但时间和 CSS 都在进步，结果呢？今年的新内容比去年还多。至少在这个有点主观的“Chris 认为值得了解的新鲜或获得了浏览器支持提升的东西”列表里是这样。</p>\n<hr>\n<h2>动画到 auto</h2>\n<h3>这是什么？</h3>\n<p>我们通常不会为包含任意内容的元素设置 <code>height</code>，而是让它们根据内容自适应高度。问题在于，我们一直无法将高度从一个固定值（如 0）动画到其本身的高度（或反过来）。换句话说，无法动画到 <code>auto</code>（或其他类似的尺寸关键字，如 <code>min-content</code> 等）。</p>\n<p>现在，我们可以选择性地允许动画到这些关键字，例如：</p>\n<pre><code class=\"language-css\">html {\n    interpolate-size: allow-keywords;\n    /* 现在如果我们在任何地方 transition\n     &quot;height: 0;&quot; 到 &quot;height: auto;&quot;\n     都能生效 */\n}\n</code></pre>\n<p>如果不想全局开启，也可以用 <code>calc-size()</code> 函数实现 transition，无需 <code>interpolate-size</code>：</p>\n<pre><code class=\"language-css\">.content {\n    height: 3lh;\n    overflow: hidden;\n    transition: height 0.2s;\n\n    &amp;.expanded {\n        height: calc-size(auto, size);\n    }\n}\n</code></pre>\n<h3>为什么要关心？</h3>\n<p>这是 CSS 首次原生支持这种动画。实际开发中这种需求很常见，现在可以非常自然地实现，而不会破坏行为。</p>\n<p>而且不仅仅是 <code>height</code>，任何接受尺寸的属性都可以；也不仅仅是 <code>auto</code>，其他尺寸关键字也可以。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>仅 Chrome</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>可以！通常这种动画不是硬性需求，只是锦上添花</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>基本没有。以前的做法是动画 <code>max-height</code> 到一个很大的值，或者用 JS 计算尺寸再动画，体验都不好</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<iframe src=\"//codepen.io/editor/anon/embed/01992602-6209-7ff6-9aec-9157440104cc?height=1300&theme-id=1&slug-hash=01992602-6209-7ff6-9aec-9157440104cc&default-tab=css,result\" height=\"1300\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 01992602-6209-7ff6-9aec-9157440104cc\" title=\"CodePen Embed 01992602-6209-7ff6-9aec-9157440104cc\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<hr>\n<h2>Popovers &amp; Invokers</h2>\n<p>这些是独立且实用的功能，主要偏向 HTML，但它们互补，值得一起介绍。</p>\n<h3>这是什么？</h3>\n<p><code>popover</code> 是你可以加在任何 HTML 元素上的属性，赋予其打开/关闭的功能，并有 JS API 控制。它和模态框类似但不同，更像是 tooltip 或可以同时打开多个的东西。</p>\n<p>Invoker 也是 HTML 属性，让我们可以用声明式的方式在标记中调用这些 JS API。</p>\n<h3>为什么要关心？</h3>\n<p>在 HTML 层实现功能非常强大。无需 JS 就能工作，且可访问性好，用户体验细节也更容易做对。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>Popover 各浏览器都支持，但 invokers 目前仅 Chrome。子功能如 <code>popover=&quot;hint&quot;</code> 支持略少。</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>不太适用。这类功能通常必须“能用”，建议用 polyfill 保证一致性。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>有！<a href=\"https://github.com/oddbird/popover-polyfill\">Popovers Polyfill</a> / <a href=\"https://github.com/keithamus/invokers-polyfill\">Invokers Polyfill</a></td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<p>记住 popover 也有 JS API，如 <code>myPopover.showPopover()</code> 和 <code>secondPopover.hidePopover()</code>，但这里展示的是 HTML 的 invoker 控制。还有其他 HTML 控制方式（如 <code>popovertarget=&quot;mypopover&quot; popovertargetaction=&quot;show&quot;</code>），但我更喜欢通用的 command invokers 方法。</p>\n<iframe src=\"//codepen.io/editor/anon/embed/019948c7-09d1-7f29-b676-2b57fdec4f8f?height=450&theme-id=1&slug-hash=019948c7-09d1-7f29-b676-2b57fdec4f8f&default-tab=html,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 019948c7-09d1-7f29-b676-2b57fdec4f8f\" title=\"CodePen Embed 019948c7-09d1-7f29-b676-2b57fdec4f8f\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<p><em>另外</em> —— popover 和锚点定位（anchor positioning）配合特别好，这也是现代 CSS 的奇迹之一。</p>\n<hr>\n<h2>@function</h2>\n<h3>这是什么？</h3>\n<p>CSS 已经有很多函数，比如 <code>calc()</code>、<code>attr()</code>、<code>clamp()</code>，<a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Values_and_Units/CSS_Value_Functions\">还有很多</a>。它们其实叫 CSS <em>值函数</em>，总是返回一个值。</p>\n<p><code>@function</code> 的魔力在于——<em>你可以自己写函数了</em>。</p>\n<pre><code class=\"language-css\">@function --titleBuilder(--name) {\n    result: var(--name) &quot; is cool.&quot;;\n}\n</code></pre>\n<h3>为什么要关心？</h3>\n<p>将逻辑抽象为函数是计算机编程的基本思想。把代码和逻辑集中在一个地方而不是重复写，能让 CSS 更简洁、可维护。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>仅 Chrome</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>取决于你用它做什么。如果可以接受回退值，可以这样写：<br><br><code>property: fallback; property: --function();</code></td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>没有。Sass 有函数但不是同一规范，不能直接用。</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<iframe src=\"//codepen.io/editor/anon/embed/019930ad-8e97-7b37-9330-e5cae3d2c938?height=450&theme-id=1&slug-hash=019930ad-8e97-7b37-9330-e5cae3d2c938&default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 019930ad-8e97-7b37-9330-e5cae3d2c938\" title=\"CodePen Embed 019930ad-8e97-7b37-9330-e5cae3d2c938\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<h3>其他资源</h3>\n<ul>\n<li>Una Kravets: <a href=\"https://una.im/5-css-functions/\">5 个有用的 CSS @function 用法</a></li>\n<li>Bramus Van Damme: <a href=\"https://www.bram.us/2025/02/18/css-at-function-and-css-if/\">CSS @function + CSS if() = 🤯</a></li>\n<li>Juan Diego Rodríguez: <a href=\"https://css-tricks.com/functions-in-css/\">CSS 里的函数？！</a></li>\n<li>草案: <a href=\"https://www.w3.org/TR/css-mixins-1/\">CSS Functions and Mixins Module</a></li>\n</ul>\n<hr>\n<h2>if()</h2>\n<h3>这是什么？</h3>\n<p>从概念上说，CSS 里已经有很多条件逻辑。选择器本身就是“如果”匹配就应用样式，媒体查询也是“如果”条件成立就生效。</p>\n<p>但 <a href=\"https://developer.chrome.com/blog/if-article\"><code>if()</code> 函数</a> 是第一个专门用于逻辑分支的 CSS 构造。</p>\n<h3>为什么要关心？</h3>\n<p>和所有函数一样，包括上面自定义的 @function，<code>if()</code> 返回一个值。它的语法让代码更易读，也能减少重复。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>仅 Chrome</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>取决于你用它做什么。如果可以接受回退值，可以这样写：<br><br><code>property: fallback; property: if( style(--x: true): value; else: fallback; );</code></td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>没有。CSS 预处理器有类似逻辑，但不会基于动态值和 DOM 重新计算。</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<p>将逻辑写进一个值里很酷！</p>\n<pre><code class=\"language-css\">.grid {\n    display: grid;\n    grid-template-columns: if(\n        media(max-width &gt; 300px): repeat(2, 1fr) ; media(max-width &gt; 600px): repeat(3, 1fr) ;\n            media(max-width &gt; 900px): repeat(auto-fit, minmax(250px, 1fr)) ; else: 1fr;\n    );\n}\n</code></pre>\n<p>语法很像 switch 语句，可以有任意多个条件，匹配到第一个就用。</p>\n<pre><code class=\"language-css\">if(\n  condition: value;\n  condition: value;\n  else: value;\n)\n</code></pre>\n<p>条件可以是：</p>\n<ul>\n<li><code>media()</code></li>\n<li><code>supports()</code></li>\n<li><code>style()</code></li>\n</ul>\n<hr>\n<h2>field-sizing</h2>\n<h3>这是什么？</h3>\n<p>CSS 新增的 <code>field-sizing</code> 属性可以让表单字段（或任何可编辑元素）根据内容自动增长。</p>\n<h3>为什么要关心？</h3>\n<p>开发者一直用 JS 实现这个需求，最典型的例子是 <code>&lt;textarea&gt;</code>，让它根据用户输入自动变大，无需手动调整（尤其在小屏幕上很难操作）。当然，内联调整大小<a href=\"https://bsky.app/profile/eva.town/post/3lynv7jniys2u\">也不错</a>。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>Chrome，Safari 很快也会支持</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>可以！通常只是提升用户体验，不是硬性需求</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>有<a href=\"https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/\">非常轻量的 JS 实现</a></td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<iframe src=\"//codepen.io/editor/anon/embed/01993ac3-0511-7ca2-b400-43dac665e755?height=450&theme-id=1&slug-hash=01993ac3-0511-7ca2-b400-43dac665e755&default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 01993ac3-0511-7ca2-b400-43dac665e755\" title=\"CodePen Embed 01993ac3-0511-7ca2-b400-43dac665e755\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<hr>\n<h2>自定义 Select</h2>\n<h3>这是什么？</h3>\n<p>给 <code>&lt;select&gt;</code> 外观做样式已经可以实现，但下拉菜单内容一直是操作系统自带的样式。现在你可以选择完全自定义 select 菜单的样式。</p>\n<h3>为什么要关心？</h3>\n<p>（原文未写，略）</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>仅 Chrome</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>100%。会回退为未自定义样式的 <code>&lt;select&gt;</code>，也没问题</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>以前 <code>&lt;selectlist&gt;</code> 时代有 <a href=\"https://github.com/luwes/selectlist-polyfill\">polyfill</a>，但现在渐进增强很好，不需要</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<p><a href=\"https://developer.chrome.com/blog/rfc-customizable-select\">先 opt-in</a>，然后<a href=\"https://frontendmasters.com/blog/custom-select-that-comes-up-from-the-bottom-on-mobile/\">随意发挥</a>。</p>\n<pre><code class=\"language-css\">select,\n::picker(select) {\n    appearance: base-select;\n}\n</code></pre>\n<iframe src=\"//codepen.io/anon/embed/eYojgZw?height=450&theme-id=1&slug-hash=eYojgZw&default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed eYojgZw\" title=\"CodePen Embed eYojgZw\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<hr>\n<h2>text-wrap</h2>\n<h3>这是什么？</h3>\n<p>CSS 的 <code>text-wrap</code> 属性可以让浏览器以不同方式换行。例如，<code>text-wrap: balance;</code> 会让每行长度尽量接近。</p>\n<h3>为什么要关心？</h3>\n<p>这对大字号元素（如标题）来说是更好的默认值。它也能避免单词孤零零地出现在下一行（孤儿），而 <code>text-wrap: pretty;</code> 也能做到，并且<a href=\"https://webkit.org/blog/16547/better-typography-with-text-wrap-pretty/\">专为长文本设计</a>，让文本更易读。总之：免费获得更好的排版。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td><code>balance</code> 各浏览器都支持，<code>pretty</code> 目前仅 Chrome 和 Safari</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>绝对可以。即使没有这些增强，文本依然可读可访问</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://arc.net/l/quote/ulftlzvc\"><code>balance</code> 有 polyfill</a></td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<iframe src=\"//codepen.io/editor/anon/embed/01996388-9b45-7cdc-9c2e-089f052527c4?height=650&theme-id=1&slug-hash=01996388-9b45-7cdc-9c2e-089f052527c4&default-tab=result\" height=\"650\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 01996388-9b45-7cdc-9c2e-089f052527c4\" title=\"CodePen Embed 01996388-9b45-7cdc-9c2e-089f052527c4\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<h3>资源</h3>\n<ul>\n<li>Jen Simmons: <a href=\"https://webkit.org/blog/16547/better-typography-with-text-wrap-pretty/\">Better typography with text-wrap pretty</a></li>\n<li>Stephanie Stimac: <a href=\"https://blog.stephaniestimac.com/posts/2023/10/css-text-wrap/\">When to use CSS text-wrap: balance; vs text-wrap: pretty;</a></li>\n</ul>\n<hr>\n<h2>linear() 缓动</h2>\n<h3>这是什么？</h3>\n<p>这个有点让人困惑，因为 <code>linear</code> 作为 <code>transition-timing-function</code> 或 <code>animation-timing-function</code> 的关键字，意味着“线性无变化”（有时正是你想要的，比如透明度变化）。但 <a href=\"https://developer.chrome.com/docs/css-ui/css-linear-easing-function#a_tool_to_help\"><code>linear()</code> 函数</a> 实际上可以实现非常花哨的缓动，比如“弹跳”效果。</p>\n<h3>为什么要关心？</h3>\n<p>即使是 <code>cubic-bezier()</code> 也只能做有限的弹跳动画，而 <code>linear()</code> 可以接受无限个点，效果更丰富。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>可以！可以回退到命名缓动值或 <code>cubic-bezier()</code></td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>没有，但如果你很在意 fancy 缓动，<a href=\"https://gsap.com/docs/v3/Eases/\">GSAP</a> 这类 JS 库都能实现</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<pre><code class=\"language-css\">.bounce {\n    animation-timing-function: linear(\n        0,\n        0.004,\n        0.016,\n        0.035,\n        0.063,\n        0.098,\n        0.141 13.6%,\n        0.25,\n        0.391,\n        0.563,\n        0.765,\n        1,\n        0.891 40.9%,\n        0.848,\n        0.813,\n        0.785,\n        0.766,\n        0.754,\n        0.75,\n        0.754,\n        0.766,\n        0.785,\n        0.813,\n        0.848,\n        0.891 68.2%,\n        1 72.7%,\n        0.973,\n        0.953,\n        0.941,\n        0.938,\n        0.941,\n        0.953,\n        0.973,\n        1,\n        0.988,\n        0.984,\n        0.988,\n        1\n    );\n}\n</code></pre>\n<iframe src=\"//codepen.io/editor/anon/embed/01993fdd-5fd1-751d-bf26-f43dd3140396?height=450&theme-id=1&slug-hash=01993fdd-5fd1-751d-bf26-f43dd3140396&default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 01993fdd-5fd1-751d-bf26-f43dd3140396\" title=\"CodePen Embed 01993fdd-5fd1-751d-bf26-f43dd3140396\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<h3>资源</h3>\n<ul>\n<li>Jake Archibald: <a href=\"https://linear-easing-generator.netlify.app/\">linear() easing generator</a></li>\n<li>Matthias Martin: <a href=\"https://easingwizard.com/\">Easing Wizard</a></li>\n</ul>\n<hr>\n<h2>shape()</h2>\n<h3>这是什么？</h3>\n<p>CSS 早有 <code>path()</code> 函数，但它只能 1:1 复制 SVG <code>&lt;path&gt;</code> 的 <code>d</code> 属性，只能用像素单位，语法也<a href=\"https://css-tricks.com/svg-path-syntax-illustrated-guide/\">不太友好</a>。<a href=\"https://drafts.csswg.org/css-shapes-2/#shape-function\"><code>shape()</code> 函数</a> 则是为 CSS 量身定制的升级版。</p>\n<h3>为什么要关心？</h3>\n<p><code>shape()</code> 函数几乎可以画任何东西。可以用在 <code>clip-path</code>，让元素变任意形状，且支持响应式、CSS 单位、自定义属性、媒体查询等。也可以用在 <code>offset-path()</code>，让元素沿任意路径运动。未来还会支持 <code>shape-outside</code>。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>Chrome、Safari 已支持，Firefox 也在开发，很快就全面了</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>基本可以！裁剪和路径动画通常是美化，回退到普通样式也没问题</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>没有，建议做好回退方案</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<p><a href=\"https://path-to-shape.netlify.app/\">任何 SVG path</a> 都能转成 <code>shape()</code>。</p>\n<pre><code class=\"language-css\">.arrow {\n    clip-path: shape(\n        evenodd from 97.788201% 41.50201%,\n        line by -30.839077% -41.50201%,\n        curve by -10.419412% 0% with -2.841275% -3.823154% / -7.578137% -3.823154%,\n        smooth by 0% 14.020119% with -2.841275% 10.196965%,\n        line by 18.207445% 24.648236%,\n        hline by -67.368705%,\n        curve by -7.368452% 9.914818% with -4.103596% 0% / -7.368452% 4.393114%,\n        smooth by 7.368452% 9.914818% with 3.264856% 9.914818%,\n        hline by 67.368705%,\n        line by -18.211656% 24.50518%,\n        curve by 0% 14.020119% with -2.841275% 3.823154% / -2.841275% 10.196965%,\n        curve by 5.26318% 2.976712% with 1.472006% 1.980697% / 3.367593% 2.976712%,\n        smooth by 5.26318% -2.976712% with 3.791174% -0.990377%,\n        line by 30.735919% -41.357537%,\n        curve by 2.21222% -7.082013% with 1.369269% -1.842456% / 2.21222% -4.393114%,\n        smooth by -2.21222% -7.082013% with -0.736024% -5.239556%,\n        close\n    );\n}\n</code></pre>\n<p>天然支持缩放，语法也比 <code>path()</code> 更易读：</p>\n<iframe src=\"//codepen.io/editor/anon/embed/0199597b-d364-76f2-a843-0e434ebaaac8?height=450&theme-id=1&slug-hash=0199597b-d364-76f2-a843-0e434ebaaac8&default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 0199597b-d364-76f2-a843-0e434ebaaac8\" title=\"CodePen Embed 0199597b-d364-76f2-a843-0e434ebaaac8\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<hr>\n<h2>更强大的 attr()</h2>\n<h3>这是什么？</h3>\n<p>CSS 的 <code>attr()</code> 函数可以获取匹配元素的字符串值。例如 <code>&lt;div data-name=&quot;Chris&quot;&gt;</code> 可以用 <code>div::before { content: attr(data-name); }</code> 得到 &quot;Chris&quot;。现在，你可以为获取的值指定<em>类型</em>，更实用。</p>\n<h3>为什么要关心？</h3>\n<p>数字和颜色等类型比字符串更有用，可以直接用于样式。</p>\n<pre><code class=\"language-xml\">attr(data-count type(&lt;number&gt;))\n</code></pre>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>仅 Chrome</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>取决于你用它做什么。如果只是美化，没问题；如果是布局核心信息，则不建议依赖</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>没有</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<iframe src=\"//codepen.io/editor/anon/embed/0199599c-caf7-7245-b445-bde7a0d43fe9?height=1000&theme-id=1&slug-hash=0199599c-caf7-7245-b445-bde7a0d43fe9&default-tab=result\" height=\"1000\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed 0199599c-caf7-7245-b445-bde7a0d43fe9\" title=\"CodePen Embed 0199599c-caf7-7245-b445-bde7a0d43fe9\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<hr>\n<h2>reading-flow</h2>\n<h3>这是什么？</h3>\n<p>有多种方式可以让视觉顺序和源码顺序不一致。<a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/reading-flow\">新的 <code>reading-order</code> 属性</a> 允许我们在改变布局顺序时，让 Tab 键导航顺序也能合理。</p>\n<h3>为什么要关心？</h3>\n<p>长期以来我们被告知：不要随意重排布局！源码顺序应尽量和视觉顺序一致，否则 Tab 导航会变得混乱甚至导致滚动，影响可访问性。现在我们可以告诉浏览器按布局顺序导航。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th>支持情况</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>仅 Chrome</td>\n</tr>\n<tr>\n<td>渐进增强</td>\n<td>不太适用。建议等所有浏览器都支持后再广泛使用</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>没有，但可以用 JS 动态设置 <code>tabindex</code> 实现</td>\n</tr>\n</tbody></table>\n<h3>使用示例</h3>\n<pre><code class=\"language-css\">.grid {\n    reading-flow: grid-rows;\n}\n</code></pre>\n<p>重排 grid 布局是最常见的需求，让 Tab 顺序跟随行顺序很合理。你需要根据实际布局设置对应值，比如 flexbox 用 <code>flex-flow</code>。<a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/reading-flow#value\">MDN 有完整值列表</a>。</p>\n<iframe src=\"//codepen.io/anon/embed/jEEdewP?height=450&theme-id=1&slug-hash=jEEdewP&default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" allowpaymentrequest=\"\" name=\"CodePen Embed jEEdewP\" title=\"CodePen Embed jEEdewP\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback</iframe>\n\n<h3>资源</h3>\n<ul>\n<li>Rachel Andrew: <a href=\"https://rachelandrew.co.uk/archives/2025/05/02/reading-flow-ships-in-chrome-137/\">Reading flow ships in Chrome 137</a></li>\n<li>Daniel Schwarz: <a href=\"https://css-tricks.com/what-we-know-so-far-about-css-reading-order/\">What We Know (So Far) About CSS Reading Order</a></li>\n<li>Di Zhang: <a href=\"https://developer.chrome.com/blog/reading-flow\">Use CSS reading-flow for logical sequential focus navigation</a></li>\n</ul>\n<hr>\n<h2>值得关注的新特性</h2>\n<ul>\n<li>“Masonry” 布局虽然有多种初步实现，但还未最终定稿，预计明年会有进展。最有趣的是 <a href=\"https://webkit.org/blog/17219/item-flow-part-2-next-steps-for-masonry/\">item-flow 的提案</a>，不仅能帮助 Masonry，还能为 grid 以外的布局带来新可能。</li>\n<li>CSS 函数 <code>random()</code> 已在 Safari 支持，<a href=\"https://frontendmasters.com/blog/very-early-playing-with-random-in-css/\">非常酷</a>。</li>\n<li>CSS 属性 <a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/margin-trim\"><code>margin-trim</code></a> 很实用，期待能在更多浏览器用上。</li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-index\"><code>sibling-index()</code></a> 和 <a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-count\"><code>sibling-count()</code></a> 已在 Chrome 支持，非常适合做错位动画。</li>\n<li>View Transitions 里的 <code>view-transition-name: match-element;</code> 很方便，无需为每个元素生成唯一名。Firefox 也在开发 View Transitions，意义重大。</li>\n<li>很快我们就能用 <code>calc()</code> 进行带单位的乘除（不再要求第二个参数无单位），<a href=\"https://wpt.fyi/results/css/css-values/getComputedStyle-calc-mixed-units-003.html?label=experimental&label=master&aligned\">不再需要 hack</a>。</li>\n<li>我们没等到“<a href=\"https://github.com/w3c/csswg-drafts/issues/4770\">CSS4</a>”（<a href=\"https://www.youtube.com/watch?v=j4mOm1qic7k\">Zoran 解释得很好</a>），但我认为有个命名版本系统对大家都有好处。</li>\n<li>如果你想看近 5 年“新 CSS 特性”清单，<a href=\"https://nerdy.dev/cascading-secret-sauce\">Adam Argyle 有个很棒的列表</a>。</li>\n</ul>\n<hr>\n<h2>值得记住的好东西</h2>\n<ul>\n<li><a href=\"https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/#container-queries-size\">容器查询</a>（及单位）依然是 CSS 继媒体查询之后最棒的特性。</li>\n<li><a href=\"https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/#the-has-pseudo-selector\"><code>:has()</code> 伪类</a> 非常适合选择有特定子元素或状态的元素。</li>\n<li>超酷的现代 CSS 特性如 <a href=\"https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/#view-transitions\">View Transitions</a>、<a href=\"https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/#anchor-positioning\">Anchor Positioning</a>、<a href=\"https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/#scroll-driven-animations\">Scroll-Driven Animations</a> 都已登陆 Safari。</li>\n<li>所有有用的额外视口单位（比如 <code>dvh</code>）<a href=\"https://web.dev/blog/viewport-units\">现在都已成为基线</a>。</li>\n</ul>\n","tags":["css"]},{"id":"what-you-need-to-know-about-modern-css-spring-2024-edition","url":"https://yieldray.fun/posts/what-you-need-to-know-about-modern-css-spring-2024-edition","title":"你需要了解的现代 CSS（2024 版）","date_published":"2025-10-01T20:24:00.000Z","date_modified":"2025-10-01T20:24:00.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/\">https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/</a></p>\n</blockquote>\n<p>我的目标是通过这份可收藏的指南，列出最近 CSS 的（坦率地说：令人惊叹的）新特性。这个列表没有严格的标准，只要这些东西都<em>相当</em>新，而且我感觉很多人还不了解。即使知道，也未必真正理解它们，或许需要一个<strong>通俗易懂</strong>的解释，说明它是什么、<em>为什么要关心</em>，以及一些参考代码。也许你就是这样的人。</p>\n<p>我希望大家能对这些特性形成肌肉记忆。<a href=\"https://twitter.com/chriscoyier/status/1757826338722058444\">正如我说的</a>，“即使你<em>知道</em>这些东西，形成肌肉记忆也需要时间。”</p>\n<p>这些特性的语法、细节和细微之处远比我在这里展示的要多。我希望你知道它们的可能性，能查到最基础的用法和语法，之后需要时再深入研究。</p>\n<hr>\n<h2>容器查询（尺寸）</h2>\n<h3>什么是尺寸容器查询？</h3>\n<p>容器查询允许你为容器元素的子元素编写样式，当该容器满足某些媒体条件（通常是宽度）时生效。</p>\n<pre><code class=\"language-html\">&lt;div class=&quot;element-wrap&quot;&gt;\n    &lt;div class=&quot;element&quot;&gt;&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<pre><code class=\"language-css\">.element-wrap {\n    container: element / inline-size;\n}\n@container element (min-inline-size: 300px) {\n    .element {\n        display: flex;\n        gap: 1rem;\n    }\n}\n</code></pre>\n<h3>为什么要关心？</h3>\n<p>如果你曾想过：我希望能根据<em>这个</em>元素的尺寸做样式决策，而不是像 <code>@media</code> 查询那样只能针对整个页面。那么 <code>@container</code> 查询就是为你准备的！做设计系统或组件化网站的人，通常会用容器查询根据尺寸来调整样式，因为页面整体尺寸并不能代表组件的实际尺寸。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-container-queries\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>视情况而定——如果不是关键样式，可以渐进增强。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://github.com/GoogleChromeLabs/container-query-polyfill\">有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<p>中间有个拖动条，可以看到日历根据空间大小切换布局。它有三个断点。</p>\n<hr>\n<h2>容器查询（样式）</h2>\n<h3>什么是样式容器查询？</h3>\n<p>样式容器查询允许你在某个自定义属性有特定值时应用样式。</p>\n<pre><code class=\"language-css\">.container {\n    --variant: 1;\n\n    &amp;.variant2 {\n        --variant: 2;\n    }\n}\n\n@container style(--variant: 1) {\n    button {\n    } /* 不能选 .container，但能选内部元素 */\n    .other-things {\n    }\n}\n\n@container style(--variant: 2) {\n    button {\n    }\n    .whatever {\n    }\n}\n</code></pre>\n<h3>为什么要关心？</h3>\n<p>你是否想过在 CSS 里用 mixin？比如设置一个属性却能影响多个属性。<a href=\"https://sass-lang.com/documentation/at-rules/mixin/\">Sass 的 mixin</a> 很流行。用样式容器查询也能做到。而且就像 CSS 变量比 Sass 变量更强大一样，样式容器查询也更强大，因为它遵循层叠、可计算等。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-container-queries-style\">浏览器支持</a></td>\n<td>✅ Chrome 及相关浏览器，Safari 即将支持，Firefox 暂不支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>视你怎么用样式而定，但一般来说<em>不太适合</em>渐进增强。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>无</td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>容器单位</h2>\n<h3>什么是容器单位？</h3>\n<p>容器单位（如 <code>px</code>、<code>rem</code>、<code>vw</code>）允许你根据容器元素的当前尺寸设置元素大小。类似于视口单位 <code>1vw</code> 是浏览器宽度的 1%，<code>1cqw</code> 是容器宽度的 1%（推荐用 <code>cqi</code>，即“逻辑内联方向”）。</p>\n<p>单位有：<code>cqw</code>（容器宽度）、<code>cqh</code>（容器高度）、<code>cqi</code>（容器内联）、<code>cqb</code>（容器块方向）、<code>cqmin</code>（<code>cqi</code> 和 <code>cqb</code> 中较小者）、<code>cqmax</code>（较大者）。</p>\n<h3>为什么要关心？</h3>\n<p>如果你觉得某个元素的尺寸应该基于容器当前尺寸，容器单位几乎是唯一选择。比如排版，一个卡片组件在变大时标题也应变大，无需加类名控制。<a href=\"https://codepen.io/scottkellum/details/jOwmOZE\">这个演示很棒</a>。即使用容器查询也没这么灵活。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-container-query-units\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以——可以先写回退单位，再写容器单位。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://github.com/GoogleChromeLabs/container-query-polyfill\">有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<p>这个元素不仅 <code>font-size</code>，连 <code>padding</code>、<code>border</code>、<code>margin</code> 都用容器单位。</p>\n<hr>\n<h2>:has() 伪选择器</h2>\n<h3>什么是 :has() 选择器？</h3>\n<p><code>:has()</code> 允许你有条件地选择某个元素，当其 DOM 树中更深层的元素匹配 <code>:has()</code> 内的选择器时生效。</p>\n<pre><code class=\"language-css\">figure:has(figcaption) {\n    border: 1px solid black;\n    padding: 0.5rem;\n}\n</code></pre>\n<h3>为什么要关心？</h3>\n<p>如果你想要 CSS 的“父选择器”，<code>:has()</code> 就能实现，而且更强大。选中父元素后还能继续向下选。Jhey Tompkins 称它为<a href=\"https://developer.chrome.com/blog/has-m105/\">“家族选择器”</a>。还能和 <code>:not()</code> 结合，选中<em>没有</em>某元素的情况。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-has\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>视你怎么用样式而定，但一般来说<em>不太适合</em>渐进增强。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://github.com/jplhomer/polyfill-css-has\">仅 JS 侧有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>视图过渡（View Transitions）</h2>\n<h3>什么是视图过渡？</h3>\n<p>视图过渡有两种：</p>\n<ol>\n<li>同页过渡（需 JS）</li>\n<li>跨页过渡（仅 CSS）</li>\n</ol>\n<p>两者都很有用。同页过渡指 DOM 变化但页面不变时的动画，比如列表排序。跨页过渡是页面切换时元素动画，比如视频缩略图变成视频。下面是同页过渡的基本语法：</p>\n<pre><code class=\"language-js\">if (!document.startViewTransition) {\n    updateTheDOM();\n} else {\n    document.startViewTransition(() =&gt; updateTheDOM());\n}\n</code></pre>\n<p>跨页过渡需要加 meta 标签：</p>\n<pre><code class=\"language-html\">&lt;meta name=&quot;view-transition&quot; content=&quot;same-origin&quot; /&gt;\n</code></pre>\n<p>然后需要确保要过渡的元素在新旧页面都有唯一的 <code>view-transition-name</code>。</p>\n<h3>为什么要关心？</h3>\n<p>如果元素移动到新位置而不是瞬间出现，用户更容易理解界面。这就是动画里的<em>tweening</em>，即根据起止状态自动生成动画。视图过渡本质上就是 tweening。你可以控制动画细节，但大部分动画都是根据 DOM 的起止状态自动生成，无需手动指定每个细节。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/view-transitions\">浏览器支持</a></td>\n<td>✅ Chrome 及相关浏览器 ❌ Safari ❌ Firefox</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以——动画可以不执行，或提供回退动画。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>无</td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<p>这是同页视图过渡的例子：</p>\n<hr>\n<h2>嵌套（Nesting）</h2>\n<h3>什么是嵌套？</h3>\n<p>嵌套允许你在已有<strong>规则集</strong>内写更多选择器。</p>\n<pre><code class=\"language-css\">.card {\n    padding: 1rem;\n\n    &gt; h2:first-child {\n        margin-block-start: 0;\n    }\n\n    footer {\n        border-block-start: 1px solid black;\n    }\n}\n</code></pre>\n<h3>为什么要关心？</h3>\n<p>嵌套主要是 CSS 编写的便利性。它能把相关样式组织在一起，避免重复写选择器，减少错误，让 CSS 更易读。但嵌套也可能导致写出和 HTML 结构过度耦合的 CSS，增加特异性，降低复用性。</p>\n<pre><code class=\"language-css\">.card {\n    container: card / inline-size;\n\n    display: grid;\n    gap: 1rem;\n\n    @container (min-inline-size: 250px) {\n        gap: 2rem;\n    }\n}\n</code></pre>\n<p>和 Sass 风格嵌套的唯一区别是不能直接用 <code>&amp;</code> 拼接：</p>\n<pre><code class=\"language-css\">.card {\n    body.home &amp; {\n        /* 可以 */\n    }\n    &amp; .footer {\n        /* 可以，甚至不需要 &amp; */\n    }\n    &amp;__big {\n        /* 不行，不能这样用 */\n    }\n}\n</code></pre>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-nesting\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>不支持</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>可用 LightningCSS、Sass、Less 等预处理器</td>\n</tr>\n</tbody></table>\n<hr>\n<h2>滚动驱动动画</h2>\n<h3>什么是滚动驱动动画？</h3>\n<p>任何与元素滚动（通常是页面本身）相关的动画，现在都能用 CSS 实现，无需 JS 绑定滚动事件。分两种：</p>\n<ul>\n<li>元素的滚动进度（<code>animation-timeline: scroll()</code>）</li>\n<li>元素在可视区域内的位置（<code>animation-timeline: view()</code>）</li>\n</ul>\n<h3>为什么要关心？</h3>\n<p>比如阅读进度条，随着页面滚动从 0% 到 100%。可以用动画移动元素的 <code>background-position</code>，与页面滚动同步。用 CSS 实现<a href=\"https://developer.chrome.com/blog/scroll-animation-performance-case-study/\">性能更好</a>。</p>\n<p>另一个主要用例是元素进入（或离开）视口时触发动画。你可以精细控制动画何时开始、结束。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/mdn-css_properties_animation-timeline_scroll\">浏览器支持</a></td>\n<td>✅ Chrome 及相关浏览器 ❌ Safari 🔜 Firefox</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以——这些效果通常是视觉增强，不是必需功能。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://github.com/flackr/scroll-timeline\">有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<p>这是<a href=\"https://frontendmasters.com/blog/background-size-zooming-with-scroll-driven-animations/\">图片缩放与页面滚动</a>的演示。</p>\n<hr>\n<h2>锚点定位（Anchor Positioning）</h2>\n<h3>什么是锚点定位？</h3>\n<p>锚点定位允许你将元素相对于另一个元素定位。你声明一个锚点元素并命名，然后可以将其他元素定位到锚点的上/右/下/左（或中心、逻辑等价位置）。</p>\n<h3>为什么要关心？</h3>\n<p>一旦能自由使用，你就不用太关心元素的精确 DOM 位置（可访问性除外）。现在想相对定位，通常要求目标元素是<em>子元素</em>，并且有定位上下文。这会影响 DOM 结构是否合理。</p>\n<p>主要用例是 tooltip 和自定义右键菜单。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-anchor-positioning\">浏览器支持</a></td>\n<td>🔜 Chrome 及相关浏览器 ❌ Safari ❌ Firefox</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可能——如果能接受元素位置完全不同。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://github.com/oddbird/css-anchor-positioning\">有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<p>截至发稿时，仅在 Chrome Canary 开启“Experimental Web Platform Features”后可用。</p>\n<hr>\n<h2>作用域（Scoping）</h2>\n<h3>什么是作用域 CSS？</h3>\n<p>CSS 的作用域通过 <code>@scope</code> at-rule 实现，声明一块 CSS 只应用于指定选择器。还可以指定<em>停止</em>应用的选择器。</p>\n<h3>为什么要关心？</h3>\n<p>你也可以通过加类名并嵌套实现作用域。但 <code>@scope</code> 有一些独特用法，比如“甜甜圈作用域”：</p>\n<pre><code class=\"language-css\">@scope (.card) to (.markdown-output) {\n    h2 {\n        background: tan; /* 到 Markdown 时停止应用 */\n    }\n}\n</code></pre>\n<p>更合理的就近样式也是一大优点。比如主题切换，如果 <code>.dark</code> 和 <code>.light</code> 并存，后定义的会覆盖前面的，即使不合理。<a href=\"https://codepen.io/web-dot-dev/pen/MWZqazx\">用 <code>@scope</code> 可以解决</a>。</p>\n<p>我最喜欢的是可以直接在 DOM 里插入 <code>&lt;style&gt;</code>，只作用于那一块，无需命名：</p>\n<pre><code class=\"language-html\">&lt;div class=&quot;my-cool-component&quot;&gt;\n    &lt;style&gt;\n        @scope {\n            :scope {\n                /* 选中上面的 div，无需类名 */\n            }\n            .card {\n            }\n        }\n    &lt;/style&gt;\n\n    &lt;article class=&quot;card&quot;&gt;&lt;/article&gt;\n&lt;/div&gt;\n</code></pre>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-cascade-scope\">浏览器支持</a></td>\n<td>✅ Chrome ✅ Safari ❌ Firefox</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>不支持</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>无</td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>层叠层（Cascade Layers）</h2>\n<h3>什么是层？</h3>\n<p>CSS 层叠层是一种极其强大的语法，影响一组样式的优先级。你可以给层命名和排序（不排序则按源码顺序）。高层自动覆盖低层，无论选择器强度如何。<em>未</em>在层内的样式最强。</p>\n<pre><code class=\"language-html\">&lt;body id=&quot;home&quot;&gt;&lt;/body&gt;\n</code></pre>\n<pre><code class=\"language-css\">@layer base {\n    body#home {\n        margin: 0;\n        background: #eee;\n    }\n}\n\nbody {\n    background: white;\n}\n</code></pre>\n<p>我们习惯认为 <code>body#home</code> 选择器很强，背景会是 <code>#eee</code>。但因为有未分层样式，最终背景是 <code>white</code>。</p>\n<p>你可以有任意多层，并提前排序。新项目建议这样分层：</p>\n<pre><code class=\"language-css\">@layer reset, default, themes, patterns, layouts, components, utilities;\n</code></pre>\n<p>注意：<em>低层</em>的 <code>!important</code> 实际上<em>更强</em>。</p>\n<h3>为什么要关心？</h3>\n<p>如果你用第三方样式库，CSS 层能让你把库放在低层，你写的样式在高层，永远不用担心选择器强度“打架”。你的样式总能覆盖库，CSS 更干净、可维护。</p>\n<p>比如把 Bootstrap 放在低层：</p>\n<pre><code class=\"language-css\">@import url(&quot;https://cdn.com/bootstrap.css&quot;) layer;\n\nh5 {\n    margin-bottom: 2rem;\n}\n</code></pre>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-cascade-layers\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>不支持</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://www.oddbird.net/2022/06/21/cascade-layers-polyfill/\">有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>逻辑属性</h2>\n<h3>什么是逻辑属性？</h3>\n<p>逻辑属性是带方向的属性的替代方案。例如在英文（LTR）中，<code>inline</code> 方向是水平，<code>block</code> 方向是垂直，所以 <code>margin-right</code> 等价于 <code>margin-inline-end</code>，<code>margin-top</code> 等价于 <code>margin-block-start</code>。在阿拉伯语（RTL）中，<code>margin-inline-end</code> 就等价于 <code>margin-left</code>。很多 CSS 属性都有方向性，关键是理解 <code>inline</code> 和 <code>block</code> 流，以及 <code>start</code>/<code>end</code> 的用法。</p>\n<h3>为什么要关心？</h3>\n<p>很多时候你写方向信息，其实是想表达“文本的内联方向”。比如按钮图标和文字之间的间距，如果用 <code>margin-right</code>，RTL 语言下就错了。你真正想要的是 <code>margin-inline-end</code>。用逻辑属性，<strong>自动适配多语言</strong>，无需额外判断。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-logical-props\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>需要用 <code>@supports</code> 和 <code>unset</code> 移除回退值再用逻辑属性，<a href=\"https://adactio.com/journal/19487\">可行</a>。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://github.com/erickskrauch/postcss-logical-properties-polyfill\">有处理器</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>P3 色彩</h2>\n<h3>什么是 Display P3 色彩空间？</h3>\n<p>我们习惯用 sRGB 色彩空间（hex、<code>rgb()</code>、<code>hsl()</code>、<code>hsb()</code>）。但现在很多显示器能显示比 sRGB 更广的色域，<a href=\"https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/\">Display P3</a> 比 sRGB 宽 50%，颜色更丰富。新 CSS 函数还能用不同色彩模型，声明更丰富的颜色。</p>\n<h3>为什么要关心？</h3>\n<p>想用更鲜艳的颜色，就要用 P3 色彩空间。新色彩模型（和函数）很有用。</p>\n<p>比如 <code>oklch()</code>（<a href=\"https://oklch.com/#61.88,0.286,342.4,100\">OKLCH 色彩模型</a>）能显示所有颜色（包括 P3），和 <code>hsl()</code> 一样易读，且“亮度感知一致”，第一个参数（亮度）比 <code>hsl()</code> 更可控。对网页配色很友好。<code>oklab</code> 也很适合做渐变。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td>浏览器支持</td>\n<td>全面支持（如 <a href=\"https://caniuse.com/mdn-css_types_color_oklab\">oklab</a>）</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以——可声明回退色，无法显示的设备会自动降级。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://www.npmjs.com/package/@csstools/postcss-oklab-function?activeTab=readme\">有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<p>你可以编辑这些 <code>&lt;style&gt;</code> 块，因为我设置了 <code>display: block;</code> 和 <code>contenteditable</code>：</p>\n<hr>\n<h2>颜色混合</h2>\n<h3>什么是 color-mix()？</h3>\n<p><code>color-mix()</code> 允许你在 CSS 里<em>直接</em>混合颜色。以前只能用预处理器，现在原生 CSS 更强大。</p>\n<h3>为什么要关心？</h3>\n<p>想随时变暗/变亮已有颜色？<code>color-mix()</code> 就能做到。还能指定色彩模型，比如 OKLCH 适合调整亮度。<a href=\"https://developer.mozilla.org/en-US/blog/color-palettes-css-color-mix/\">还能做整套色板</a>。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/?search=color-mix\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以，声明回退色即可。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>理论可行，但我没见过。</td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>margin-trim</h2>\n<h3>什么是 margin-trim？</h3>\n<p><code>margin-trim</code> 属性会移除选中容器在指定方向上的<em>末尾</em>的 margin。比如一行有五个块，每个都有右 margin，通常要选 <code>:last-child</code> 去掉最后一个。用 <code>margin-trim</code> 可以直接让父元素自动去掉多余 margin。</p>\n<pre><code class=\"language-css\">.container {\n    /* 防止元素末尾有“多余” margin */\n    margin-trim: block-end;\n\n    /* 可能的“罪魁祸首”，但也可能是其他元素 */\n    &gt; p {\n        margin-block-end: 1rem;\n    }\n}\n</code></pre>\n<h3>为什么要关心？</h3>\n<p>你知道 flexbox 和 grid 的 <code>gap</code> 有多好用吧？只在元素<em>之间</em>加间距。如果不能用 <code>gap</code>，<code>margin-trim</code> 就很棒，可以给所有子元素加方向 margin，无需再选第一个/最后一个去掉多余 margin。<a href=\"https://chriscoyier.net/2023/06/12/margin-trim-as-a-best-practice/\">可能会成为最佳实践</a>。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/mdn-css_properties_margin-trim\">浏览器支持</a></td>\n<td>✅ Safari ❌ Chrome ❌ Firefox</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以。多一点空白通常不是大问题。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>无</td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<p>最后一个段落常因底部 margin 造成多余空白。用 <code>margin-trim</code> 可以自动去掉，无需手动选最后一个段落。</p>\n<hr>\n<h2>文本换行</h2>\n<h3>什么是 text-wrap？</h3>\n<p><code>text-wrap</code> 可能不在你的长期 CSS 记忆里。它能做 <code>text-wrap: nowrap;</code>，但我们一般用 <code>white-space: nowrap;</code>。现在，<code>text-wrap</code> 有两个新用法：</p>\n<ul>\n<li><code>text-wrap: balance;</code> —— 多行文本尽量等宽</li>\n<li><code>text-wrap: pretty;</code> —— 避免孤儿行</li>\n</ul>\n<h3>为什么要关心？</h3>\n<p>标题最后一个词单独一行很难看，也算排版失误。以前只能用 <code>&amp;nbsp;</code> 等 hack。<em>平衡</em>标题能避免这种情况，还能让多行文本更均匀。<code>pretty</code> 更专注于避免孤儿，适合正文。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-text-wrap-balance\">浏览器支持</a></td>\n<td>取决于值。<code>balance</code> 支持较好，<code>pretty</code> 支持较少。</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以。美观略差，但孤儿行不是大问题，没生效也无妨。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td><a href=\"https://github.com/adobe/balance-text\">有</a></td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>子网格（Subgrid）</h2>\n<h3>什么是 Subgrid？</h3>\n<p>Subgrid 是 CSS grid 的可选部分，适用于嵌套 grid 元素。设置 <code>grid-template-columns: subgrid;</code> 或 <code>grid-template-rows: subgrid;</code>，表示“继承父 grid 的列或行”。</p>\n<h3>为什么要关心？</h3>\n<p>用 grid 布局的核心是<em>对齐</em>。没有 subgrid，子元素无法访问父 grid 的线，难以对齐。Subgrid 填补了这个空白。比如在 <code>&lt;form&gt;</code> 里，DOM 嵌套很重要，subgrid 能<a href=\"https://codepen.io/chriscoyier/pen/YzxqJap\">保证对齐</a>。</p>\n<h3>支持情况</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://caniuse.com/css-subgrid\">浏览器支持</a></td>\n<td>全面支持</td>\n</tr>\n<tr>\n<td>渐进增强？</td>\n<td>可以。可回退为自定义 grid 线，虽然不完美但可用。</td>\n</tr>\n<tr>\n<td>Polyfill</td>\n<td>无。有 <a href=\"https://github.com/FremyCompany/css-grid-polyfill\">grid polyfill</a> 但不支持 subgrid。</td>\n</tr>\n</tbody></table>\n<h3>基本用法演示</h3>\n<hr>\n<h2>还值得关注的内容…</h2>\n<p>CSS 的发展速度并未放缓，还有很多值得期待的新特性。</p>\n<ul>\n<li><a href=\"https://css.oddbird.net/sasslike/mixins-functions/\">CSS Mixins &amp; Functions</a> —— 真正的 mixin 和带参数的函数</li>\n<li><a href=\"https://developer.chrome.com/blog/css-relative-color-syntax\">相对色彩语法</a> —— 以直观强大的方式操作颜色各部分</li>\n<li><a href=\"https://webkit.org/blog/14955/the-web-just-gets-better-with-interop/\">Interop 2024</a> —— 很多特性很快就会跨浏览器兼容，包括上面的相对色彩语法</li>\n<li><a href=\"https://developer.chrome.com/docs/css-ui/css-field-sizing\">CSS 属性 <code>field-sizing</code></a> 将解决表单元素（如 textarea、input）自动适应内容的老问题</li>\n<li><a href=\"https://open-ui.org/prototypes/selectmenu/\">HTML 的 <code>&lt;selectmenu&gt;</code></a> 本质上是完全可 CSS 样式化的 <code>&lt;select&gt;</code>，非常酷</li>\n</ul>\n<p>这只是值得关注的一部分。你可以<a href=\"https://frontendmasters.com/blog/feed/\">订阅我们的 feed</a>，我们会帮你持续关注，下一期你就不会错过。</p>\n<p>你有喜欢的新特性我没提到吗？欢迎告诉我。</p>\n","tags":["css"]},{"id":"cli-standard","url":"https://yieldray.fun/posts/cli-standard","title":"环境变量约定","date_published":"2025-09-27T18:19:38.000Z","date_modified":"2025-09-27T18:19:38.000Z","content_text":"<h1>NO_COLOR</h1>\n<p><a href=\"https://no-color.org/\">https://no-color.org/</a></p>\n<h1>FORCE_COLOR</h1>\n<p><a href=\"https://force-color.org/\">https://force-color.org/</a></p>\n<h1>Standard for ANSI Colors in Terminals</h1>\n<p><a href=\"https://bixense.com/clicolors/\">https://bixense.com/clicolors/</a></p>\n<h1>http_proxy</h1>\n<p><a href=\"https://curl.se/docs/manpage.html#ENVIRONMENT\">https://curl.se/docs/manpage.html#ENVIRONMENT</a></p>\n<p><a href=\"https://about.gitlab.com/blog/we-need-to-talk-no-proxy/\">https://about.gitlab.com/blog/we-need-to-talk-no-proxy/</a></p>\n<h1>XDG Base Directory Specification</h1>\n<p><a href=\"https://specifications.freedesktop.org/basedir-spec/latest/\">https://specifications.freedesktop.org/basedir-spec/latest/</a></p>\n<h1>IEEE Std 1003.1-2017</h1>\n<p><a href=\"https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html\">https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html</a></p>\n<h1>ld.so</h1>\n<p><a href=\"https://man7.org/linux/man-pages/man8/ld.so.8.html\">https://man7.org/linux/man-pages/man8/ld.so.8.html</a></p>\n<h1>SSH &amp; GIT</h1>\n<p><a href=\"https://man.openbsd.org/ssh#ENVIRONMENT\">https://man.openbsd.org/ssh#ENVIRONMENT</a></p>\n<p><a href=\"https://git-scm.com/docs/git#_environment_variables\">https://git-scm.com/docs/git#_environment_variables</a></p>\n","tags":["linux"]},{"id":"music","url":"https://yieldray.fun/posts/music","title":"Music for Web","date_published":"2025-09-13T21:14:22.000Z","date_modified":"2025-09-13T21:14:22.000Z","content_text":"<h1><a href=\"https://github.com/spessasus/SpessaSynth\">SpessaSynth</a></h1>\n<p>一个基于 SoundFont2 的实时合成器和 MIDI 播放/编辑器，完全用 JavaScript 编写，支持在线和本地运行。它允许用户加载、播放和编辑 MIDI 文件，并支持 SoundFont（SF2）、DLS 音色库的管理与转换。</p>\n<h3>MIDI</h3>\n<p>MIDI（Musical Instrument Digital Interface）是一种数字音乐协议，常用于描述音乐演奏的指令（如音符、力度、乐器类型等），本身不包含实际音频数据。可以理解为“数字乐谱”，需要通过合成器和音源进行音频渲染。</p>\n<h3>SoundFont（SF2）</h3>\n<p>SoundFont（.sf2）是一种音色库文件格式，内部包含了多种乐器的采样数据。合成器在播放 MIDI 时，会根据 MIDI 指令调用 SoundFont 中的采样，合成出最终的音频。</p>\n<h3>DLS</h3>\n<p>DLS（Downloadable Sounds）是一种与 SoundFont 类似的音色库标准，常见于部分音频硬件和软件。其作用与 SoundFont 相同，均为合成器提供乐器采样。</p>\n<h3>合成器（Synthesizer）</h3>\n<p>在本项目中，合成器指的是将 MIDI 文件与音色库（SoundFont/DLS）结合，实时生成音频输出的软件模块。SpessaSynth 即为基于 JavaScript 的软件合成器。</p>\n<h3>Web MIDI API</h3>\n<p>Web MIDI API 是浏览器提供的接口，允许网页与外部 MIDI 设备（如电子琴、MIDI 控制器）进行数据交互，实现 MIDI 信号的输入与输出。</p>\n<h3>常见文件格式</h3>\n<ul>\n<li><strong>.wav</strong>：标准无损音频文件格式。</li>\n<li><strong>.mid</strong>：MIDI 文件，存储演奏指令。</li>\n<li><strong>.sf2</strong>：SoundFont 2 音色库文件。</li>\n<li><strong>.dls</strong>：DLS 音色库文件。</li>\n<li><strong>.rmi</strong>：带有元数据的 MIDI 文件格式。</li>\n<li><strong>.sf3</strong>：压缩版 SoundFont 文件，体积更小。</li>\n</ul>\n<h1><a href=\"https://github.com/CoderLine/alphaTab\">AlphaTab</a></h1>\n<p>跨平台乐谱与吉他六线谱渲染库</p>\n<h3>Guitar Pro</h3>\n<p>Guitar Pro 是一款流行的乐谱编辑和播放软件，广泛用于吉他、贝斯等乐器的曲谱制作。其文件格式（如 .gp3、.gp4、.gp5、.gpx、.gp）分别对应不同版本，包含了乐谱、六线谱、和弦、歌词、演奏技巧等详细音乐信息。AlphaTab 支持直接解析这些格式的数据。</p>\n<h3>AlphaTex</h3>\n<p>AlphaTex 是 AlphaTab 项目自定义的一种轻量级乐谱标记语言。它以文本方式描述乐谱内容，便于手工编辑和版本管理，适合在代码环境中直接嵌入和处理。</p>\n<h3>MusicXML</h3>\n<p>MusicXML 是一种开放的、基于 XML 的音乐记谱交换标准，旨在不同乐谱软件之间实现乐谱数据的互操作。它能够描述丰富的音乐结构和表现细节，被广泛用于主流乐谱编辑器和数字音乐应用中。</p>\n<h1><a href=\"https://github.com/DrSnuggles/chiptune\">Chiptune.js</a></h1>\n<p><a href=\"https://lib.openmpt.org/libopenmpt/\">libopenmpt</a> 的 <a href=\"https://github.com/emscripten-core/emscripten\">emscripten</a> 编译</p>\n<p>libopenmpt 是一个跨平台 C/C++ 库，用于将 音乐<a href=\"https://zh.wikipedia.org/wiki/%E6%A8%A1%E5%9D%97%E6%96%87%E4%BB%B6\">模块文件</a>(MOD) 解码为原始 PCM 音频流。</p>\n<h3>MOD</h3>\n<p>MOD（Module） 是一种早期的数字音乐文件格式，最初流行于 Amiga 电脑和 Demo 场景（Demoscene）社区。MOD 文件不仅包含乐曲的音符信息，还包含用于合成音乐的采样（Sample）数据，因此它既像乐谱，又像一个小型的音源库。</p>\n<h3>PCM</h3>\n<p>PCM（Pulse Code Modulation，脉冲编码调制）是一种最常见的数字音频编码方式。</p>\n<p>PCM 是将模拟音频信号（如麦克风采集到的声音）按一定的采样率和量化精度，定期采样并转换为数字信号（即一系列二进制数字）的过程。PCM 数据本身不经过压缩，保留了原始音频的全部细节，因此常用于高保真音频处理和存储。</p>\n<p>常见的 PCM 格式：</p>\n<ul>\n<li>WAV（Windows 常用的音频文件格式，通常内部存储的就是 PCM 数据）</li>\n<li>AIFF（Mac 常用的音频文件格式）</li>\n</ul>\n<h1><a href=\"https://tonejs.github.io/\">Tone.js</a></h1>\n<p>Tone.js 是一个 Web Audio 的框架，用于在浏览器中创建交互式音乐。<br>Tone.js 的架构旨在让音乐家和音频程序员在创建基于网页的音频应用时都能感到熟悉。<br>高层次上，Tone 提供了常见的 DAW（数字音频工作站）功能，例如用于同步和调度事件的全局传输，以及预制的合成器和效果。</p>\n<h1><a href=\"https://github.com/jazz-soft/JZZ\">JZZ</a></h1>\n<p>发送、接收和播放 MIDI 消息。</p>\n<h1><a href=\"https://codeberg.org/uzu/strudel\">strudel</a></h1>\n<p>strudel 是 <a href=\"https://tidalcycles.org/\">Tidal Cycles</a> 模式语言的 JavaScript 移植。</p>\n<h3>Tidal Cycles</h3>\n<p>Tidal Cycles（简称 “Tidal”）是一款用于用代码制作模式的软件，无论是在演算法现场编码音乐，还是在工作室作曲。<br>它包括一个简单而灵活的节奏序列符号，以及一个用于组合和转换它们的大量模式函数库。<br>默认情况下，声音由功能丰富的 SuperDirt 合成器/采样器制作，也可以使用开放声音控制 (OSC) 或 MIDI 控制其他合成器。</p>\n","tags":["music"]},{"id":"otel","url":"https://yieldray.fun/posts/otel","title":"OpenTelemetry","date_published":"2025-08-13T22:15:53.000Z","date_modified":"2025-08-13T22:15:53.000Z","content_text":"<h1>概念</h1>\n<p>OpenTelemetry 提供一组框架和工具包，用于可观测性。要让系统可观测，主要分为三个步骤：</p>\n<ul>\n<li>生成：通过 <a href=\"https://opentelemetry.io/docs/concepts/instrumentation/\">Instrumentation</a>（插桩），使系统发出 <a href=\"https://opentelemetry.io/docs/concepts/signals/\">Signals</a>（信号）。信号承载可观测系统输出，主要类型包括：<ul>\n<li><a href=\"https://opentelemetry.io/docs/concepts/signals/traces\">Trace</a>（链路）：一组相关的 Span，用于描述一次请求的完整生命周期。<ul>\n<li>Span：Trace 中的操作单元，表示一次具体操作（如 HTTP 请求、数据库查询）。</li>\n<li>Context Propagation：在服务间传递 Trace 和 Span 信息，保证链路完整性。</li>\n</ul>\n</li>\n<li><a href=\"https://opentelemetry.io/docs/concepts/signals/metrics\">Metric</a>（指标）：在运行时捕获数值度量，如延迟、吞吐量、内存使用等。</li>\n<li><a href=\"https://opentelemetry.io/docs/concepts/signals/logs\">Log</a>（日志）：对事件的记录，可与 Traces 和 Metrics 关联。</li>\n<li><a href=\"https://opentelemetry.io/docs/concepts/signals/baggage\">Baggage</a>（行李）：在多个信号间传递的上下文信息。</li>\n<li>Resource &amp; Semantic Conventions：资源用于标识生成信号的实体（如主机、服务、K8s 集群），语义约定定义常用属性名称。</li>\n</ul>\n</li>\n<li>导出：使用 Exporters 将捕获的遥测数据（Trace、Metric、Log）发送到收集层或后端系统。常见导出协议和格式包括 OTLP、Jaeger、Zipkin、Prometheus 等。SDK 内置多种导出器，也可自定义实现以适配不同目标。</li>\n<li>收集：使用 OpenTelemetry Collector 或第三方收集器接收、处理和转发遥测数据。Collector 管道有：<ul>\n<li>Receiver：接收来自应用或其他 Collector 的数据输入。</li>\n<li>Processor：对数据进行批量、采样、过滤、聚合等处理。</li>\n<li>Exporter：将处理后的数据发送到后端（如 Observability 平台）。</li>\n</ul>\n</li>\n</ul>\n<pre><code class=\"language-mermaid\">flowchart TD\n    %% 左侧：Capture Telemetry\n    subgraph Capture_Telemetry [Capture Telemetry]\n        A1[Application Code]\n        A2[OpenTelemetry API]\n        A3[OpenTelemetry SDK]\n        A4[Providers]\n        A5[Meters/Tracers]\n        A6[Telemetry Data]\n        A1 --&gt;|Uses| A2\n        A2 --&gt;|Implemented by| A3\n        A3 --&gt;|Configures Resources with| A4\n        A4 --&gt;|Create| A5\n        A5 --&gt;|Capture| A6\n    end\n\n    %% 右侧：Process and Save\n    subgraph Process_and_Save [Process and Save]\n        B1[Telemetry Data]\n        B2[Readers, Samplers, Etc.]\n        B3[Processed Telemetry Data]\n        B4[Exporters]\n        B5[Collectors]\n        B6[Backend Services]\n        B1 --&gt;|Processed by| B2\n        B2 --&gt;|Outputs| B3\n        B3 --&gt;|Exported via| B4\n        B3 --&gt;|Pulled by| B5\n        B4 --&gt;|Sends to| B6\n        B5 --&gt;|Sends to| B6\n    end\n</code></pre>\n<h1>自动监测</h1>\n<p><a href=\"https://opentelemetry.io/docs/zero-code/js/\">https://opentelemetry.io/docs/zero-code/js/</a></p>\n<pre><code class=\"language-sh\">npm i @opentelemetry/api @opentelemetry/auto-instrumentations-node\n\nexport OTEL_RESOURCE_ATTRIBUTES=&quot;host.name=localhost&quot;\nexport OTEL_TRACES_EXPORTER=&quot;otlp&quot;\nexport OTEL_EXPORTER_OTLP_ENDPOINT=&quot;your-endpoint&quot;\nexport OTEL_NODE_RESOURCE_DETECTORS=&quot;env,host,os&quot;\nexport OTEL_SERVICE_NAME=&quot;your-service-name&quot;\nexport NODE_OPTIONS=&quot;--require @opentelemetry/auto-instrumentations-node/register&quot;\nnode app.js\n</code></pre>\n<h1>手动配置</h1>\n<p>手动配置需要在应用中引入 OpenTelemetry SDK 并在应用启动时初始化。下面以 Node.js 为例。</p>\n<h2>安装依赖</h2>\n<pre><code class=\"language-sh\">npm install @opentelemetry/api @opentelemetry/sdk-node \\\n  @opentelemetry/resources \\\n  @opentelemetry/semantic-conventions \\\n  @opentelemetry/exporter-trace-otlp-http\n</code></pre>\n<h2>初始化 SDK</h2>\n<p>在应用入口（如 <code>tracing.js</code>）中添加：</p>\n<pre><code class=\"language-js\">const { NodeSDK } = require(&quot;@opentelemetry/sdk-node&quot;);\nconst { Resource } = require(&quot;@opentelemetry/resources&quot;);\nconst { SemanticResourceAttributes } = require(&quot;@opentelemetry/semantic-conventions&quot;);\nconst { OTLPTraceExporter } = require(&quot;@opentelemetry/exporter-trace-otlp-http&quot;);\nconst { OTLPMetricExporter } = require(&quot;@opentelemetry/exporter-metrics-otlp-http&quot;);\nconst { PeriodicExportingMetricReader } = require(&quot;@opentelemetry/sdk-metrics&quot;);\nconst { ParentBasedSampler, AlwaysOnSampler } = require(&quot;@opentelemetry/sdk-trace-base&quot;);\nconst { W3CTraceContextPropagator } = require(&quot;@opentelemetry/core&quot;);\n\nconst sdk = new NodeSDK({\n    // 资源（Resource）：标识生成信号的实体，如服务名称与版本\n    resource: new Resource({\n        [SemanticResourceAttributes.SERVICE_NAME]: &quot;your-service-name&quot;,\n        [SemanticResourceAttributes.SERVICE_VERSION]: &quot;1.0.0&quot;,\n    }),\n    // 采样（Sampler）：决定哪些 Traces 被导出\n    sampler: new ParentBasedSampler({\n        root: new AlwaysOnSampler(), // 始终采样所有 Traces\n    }),\n    // 传播（Propagator）：使用 W3C Trace Context 规范在服务间传递上下文\n    textMapPropagator: new W3CTraceContextPropagator(),\n    // 导出器（Exporter）：将 Trace 发送到 Collector\n    traceExporter: new OTLPTraceExporter({\n        url: &quot;http://your-collector:4318/v1/traces&quot;,\n    }),\n    // 收集（Metric Reader）：周期性导出 Metric 数据\n    metricReader: new PeriodicExportingMetricReader({\n        exporter: new OTLPMetricExporter({\n            url: &quot;http://your-collector:4318/v1/metrics&quot;,\n        }),\n        exportIntervalMillis: 60000, // 每 60 秒导出一次\n    }),\n});\n\nsdk.start()\n    .then(() =&gt; console.log(&quot;Tracing &amp; Metrics initialized&quot;))\n    .catch((error) =&gt; console.error(&quot;Error initializing SDK&quot;, error));\n</code></pre>\n<p>通过 <code>--require</code> 在启动应用时加载该文件：</p>\n<pre><code class=\"language-sh\">node --require ./tracing.js app.js\n</code></pre>\n<h2>获取并使用 Tracer</h2>\n<p>在业务代码中获取 <code>Tracer</code> 并创建 <code>Span</code>：</p>\n<pre><code class=\"language-js\">const { trace } = require(&quot;@opentelemetry/api&quot;);\nconst tracer = trace.getTracer(&quot;example-tracer&quot;);\n\nfunction main() {\n    const span = tracer.startSpan(&quot;main&quot;);\n    // 业务逻辑...\n    span.end();\n}\n\nmain();\n</code></pre>\n<h3>概念演示示例</h3>\n<pre><code class=\"language-js\">/** 生成（Generation）: 使用 Tracer 生成 Trace 信号 */\nconst { trace, metrics, propagation, context } = require(&quot;@opentelemetry/api&quot;);\n\n// Trace 示例\nconst tracer = trace.getTracer(&quot;demo-tracer&quot;, &quot;1.0.0&quot;);\nconst span = tracer.startSpan(&quot;processOrder&quot;, {\n    kind: trace.SpanKind.INTERNAL,\n    attributes: { &quot;order.id&quot;: &quot;1234&quot; }, // Resource &amp; Semantic Conventions\n});\n// 在上下文中激活此 Span\nspan.setAttribute(&quot;processing.step&quot;, &quot;validate&quot;);\nspan.end();\n\n/** 生成（Generation）: 使用 Meter 生成 Metric 信号 */\nconst meter = metrics.getMeter(&quot;demo-meter&quot;, &quot;1.0.0&quot;);\nconst requestCount = meter.createCounter(&quot;requests.count&quot;, {\n    description: &quot;示例请求计数&quot;,\n});\n// 记录指标\nrequestCount.add(1, { route: &quot;/placeOrder&quot; });\n\n/** 生成（Generation）: 使用 Baggage 传递上下文信息 */\nconst userBaggage = propagation.createBaggage({\n    &quot;user-id&quot;: { value: &quot;alice&quot; },\n});\nconst ctxWithBaggage = propagation.setBaggage(context.active(), userBaggage);\n// 在后续调用中传递 ctxWithBaggage\n\n/** 导出（Export）与收集（Collect）由 SDK 初始化时的 Exporter 和 Collector 管道自动处理 */\n</code></pre>\n<h2>注册 Instrumentations</h2>\n<p>手动配置应用自身的 Instrumentation 之外，可以使用社区提供的 Instrumentations 来自动生成常用库的遥测数据：</p>\n<pre><code class=\"language-js\">const { registerInstrumentations } = require(&quot;@opentelemetry/instrumentation&quot;);\nconst { HttpInstrumentation } = require(&quot;@opentelemetry/instrumentation-http&quot;);\nconst { ExpressInstrumentation } = require(&quot;@opentelemetry/instrumentation-express&quot;);\n\nregisterInstrumentations({\n    instrumentations: [\n        new HttpInstrumentation({\n            // 配置 HTTP Span 属性\n            ignoreIncomingPaths: [&quot;/health&quot;],\n        }),\n        new ExpressInstrumentation({\n            requestHook: (span, req) =&gt; {\n                span.setAttribute(&quot;express.route&quot;, req.route.path);\n            },\n        }),\n    ],\n});\n</code></pre>\n<h2>日志导出与调试</h2>\n<p>默认情况下，SDK 不会打印调试信息。可通过环境变量开启诊断日志：</p>\n<pre><code class=\"language-sh\">export OTEL_LOG_LEVEL=debug\n</code></pre>\n<p>可以将 Span 导出到控制台，帮助本地调试：</p>\n<pre><code class=\"language-js\">const { ConsoleSpanExporter, SimpleSpanProcessor } = require(&quot;@opentelemetry/sdk-trace-base&quot;);\n// 在 SDK 初始化后或自定义 TracerProvider 时：\nsdk.configureTracerProvider((provider) =&gt; {\n    provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));\n});\n</code></pre>\n<h2>关闭 SDK</h2>\n<p>在应用关闭时调用：</p>\n<pre><code class=\"language-js\">sdk.shutdown()\n    .then(() =&gt; console.log(&quot;Tracing terminated&quot;))\n    .catch((error) =&gt; console.error(&quot;Error terminating tracing&quot;, error));\n</code></pre>\n","tags":["apm","observability"]},{"id":"makefile-tutorial","url":"https://yieldray.fun/posts/makefile-tutorial","title":"Makefile Tutorial","date_published":"2025-06-21T16:38:33.000Z","date_modified":"2025-06-21T16:38:33.000Z","content_text":"<blockquote>\n<p><a href=\"https://makefiletutorial.com/\">Makefile Tutorial</a>（<a href=\"https://github.com/chaselambda/makefiletutorial/blob/gh-pages/src/index.md\">源码</a>），通过 Gemini 2.5 Flash <a href=\"https://rentry.org/8v9mmzan\">翻译</a></p>\n</blockquote>\n<p><b>我创建本指南是因为我一直无法完全理解 Makefiles。</b>它们似乎充满了隐藏的规则和深奥的符号，而提出简单的问题却得不到简单的答案。为了解决这个问题，我花了几个周末阅读了所有关于 Makefiles 的资料。我将最关键的知识浓缩到本指南中。每个主题都有一个简短的描述和一个可以自行运行的独立示例。</p>\n<p>如果您对 Make 有大致的了解，可以查看 <a href=\"#makefile-cookbook\">Makefile Cookbook</a>，其中包含一个适用于中型项目的模板，并对 Makefile 的每个部分的作用进行了充分的注释。</p>\n<p>祝您好运，希望您能征服 Makefiles 令人困惑的世界！</p>\n<h1>入门</h1>\n<h2>为什么存在 Makefiles？</h2>\n<p>Makefiles 用于帮助决定大型程序的哪些部分需要重新编译。在绝大多数情况下，C 或 C++ 文件会被编译。其他语言通常有自己的工具，其作用与 Make 类似。当您需要根据文件更改运行一系列指令时，Make 也可以用于编译之外的用途。本教程将重点介绍 C/C++ 编译用例。</p>\n<p>这是一个您可以使用 Make 构建的依赖关系图示例。如果任何文件的依赖关系发生更改，则该文件将被重新编译：</p>\n<div class='center'>\n<img src=\"https://makefiletutorial.com/assets/dependency_graph.png\"/>\n</div>\n\n<h2>Make 有哪些替代方案？</h2>\n<p>流行的 C/C++ 替代构建系统有 <a href=\"https://scons.org/\">SCons</a>、<a href=\"https://cmake.org/\">CMake</a>、<a href=\"https://bazel.build/\">Bazel</a> 和 <a href=\"https://ninja-build.org/\">Ninja</a>。一些代码编辑器，如 <a href=\"https://visualstudio.microsoft.com/\">Microsoft Visual Studio</a>，有自己的内置构建工具。对于 Java，有 <a href=\"https://ant.apache.org/\">Ant</a>、<a href=\"https://maven.apache.org/what-is-maven.html\">Maven</a> 和 <a href=\"https://gradle.org/\">Gradle</a>。其他语言，如 Go、Rust 和 TypeScript，有自己的构建工具。</p>\n<p>像 Python、Ruby 和原始 Javascript 这样的解释型语言不需要 Makefiles 的类似物。Makefiles 的目标是根据文件更改编译需要编译的任何文件。但是当解释型语言中的文件更改时，不需要重新编译任何东西。当程序运行时，使用文件的最新版本。</p>\n<h2>Make 的版本和类型</h2>\n<p>Make 有多种实现，但本指南的大部分内容适用于您正在使用的任何版本。但是，它专门为 GNU Make 编写，这是 Linux 和 MacOS 上的标准实现。所有示例都适用于 Make 版本 3 和 4，除了某些深奥的差异外，它们几乎是等效的。</p>\n<h2>运行示例</h2>\n<p>要运行这些示例，您需要一个终端并安装“make”。对于每个示例，将内容放入名为 <code>Makefile</code> 的文件中，并在该目录中运行命令 <code>make</code>。让我们从最简单的 Makefile 开始：</p>\n<pre><code class=\"language-makefile\">hello:\n    echo &quot;Hello, World&quot;\n</code></pre>\n<blockquote>\n<p>注意：Makefiles <strong>必须</strong>使用 TAB 缩进，而不是空格，否则 <code>make</code> 将失败。</p>\n</blockquote>\n<p>以下是运行上述示例的输出：</p>\n<pre><code class=\"language-shell\">$ make\necho &quot;Hello, World&quot;\nHello, World\n</code></pre>\n<p>就是这样！如果您有点困惑，这里有一个视频，它将逐步介绍这些步骤，并描述 Makefiles 的基本结构。</p>\n<div class='yt-video'>\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/zeEMISsjO38\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</div>\n\n<h2>Makefile 语法</h2>\n<p>Makefile 由一组<em>规则</em>组成。规则通常如下所示：</p>\n<pre><code class=\"language-makefile\">targets: prerequisites\n    command\n    command\n    command\n</code></pre>\n<ul>\n<li><em>目标</em>是文件名，用空格分隔。通常，每个规则只有一个目标。</li>\n<li><em>命令</em>是一系列步骤，通常用于生成目标。这些<em>需要以制表符开头</em>，而不是空格。</li>\n<li><em>先决条件</em>也是文件名，用空格分隔。这些文件需要在运行目标的命令之前存在。这些也称为<em>依赖项</em>。</li>\n</ul>\n<h2>Make 的精髓</h2>\n<p>让我们从一个 hello world 示例开始：</p>\n<pre><code class=\"language-makefile\">hello:\n    echo &quot;Hello, World&quot;\n    echo &quot;This line will print if the file hello does not exist.&quot;\n</code></pre>\n<p>这里已经有很多内容了。让我们分解一下：</p>\n<ul>\n<li>我们有一个名为 <code>hello</code> 的<em>目标</em></li>\n<li>此目标有两个<em>命令</em></li>\n<li>此目标没有<em>先决条件</em></li>\n</ul>\n<p>然后我们将运行 <code>make hello</code>。只要 <code>hello</code> 文件不存在，命令就会运行。如果 <code>hello</code> 存在，则不会运行任何命令。</p>\n<p>重要的是要意识到我将 <code>hello</code> 既称为<em>目标</em>又称为<em>文件</em>。这是因为两者直接相关。通常，当运行目标（即运行目标的命令）时，命令将创建一个与目标同名的文件。在这种情况下，<code>hello</code> <em>目标</em>不会创建 <code>hello</code> <em>文件</em>。</p>\n<p>让我们创建一个更典型的 Makefile——一个编译单个 C 文件的 Makefile。但在我们这样做之前，创建一个名为 <code>blah.c</code> 的文件，其内容如下：</p>\n<pre><code class=\"language-c\">// blah.c\nint main() { return 0; }\n</code></pre>\n<p>然后创建 Makefile（一如既往地命名为 <code>Makefile</code>）：</p>\n<pre><code class=\"language-makefile\">blah:\n    cc blah.c -o blah\n</code></pre>\n<p>这次，尝试简单地运行 <code>make</code>。由于没有将目标作为参数提供给 <code>make</code> 命令，因此将运行第一个目标。在这种情况下，只有一个目标 (<code>blah</code>)。第一次运行此命令时，将创建 <code>blah</code>。第二次，您将看到 <code>make: &#39;blah&#39; is up to date</code>。这是因为 <code>blah</code> 文件已经存在。但有一个问题：如果我们修改 <code>blah.c</code> 然后运行 <code>make</code>，则不会重新编译任何内容。</p>\n<p>我们通过添加一个先决条件来解决这个问题：</p>\n<pre><code class=\"language-makefile\">blah: blah.c\n    cc blah.c -o blah\n</code></pre>\n<p>当我们再次运行 <code>make</code> 时，会发生以下一系列步骤：</p>\n<ul>\n<li>选择第一个目标，因为第一个目标是默认目标</li>\n<li>这有一个 <code>blah.c</code> 的先决条件</li>\n<li>Make 决定是否应该运行 <code>blah</code> 目标。它只会在 <code>blah</code> 不存在，或者 <code>blah.c</code> <em>比</em> <code>blah</code> <em>新</em>时运行</li>\n</ul>\n<p>最后一步至关重要，也是 <strong>make 的精髓</strong>。它试图做的是决定 <code>blah</code> 的先决条件自 <code>blah</code> 上次编译以来是否已更改。也就是说，如果 <code>blah.c</code> 被修改，运行 <code>make</code> 应该重新编译该文件。反之，如果 <code>blah.c</code> 没有更改，则不应重新编译。</p>\n<p>为了实现这一点，它使用文件系统时间戳作为代理来确定是否发生了更改。这是一个合理的启发式方法，因为文件时间戳通常只会在文件被修改时才会更改。但重要的是要意识到情况并非总是如此。例如，您可以修改一个文件，然后将该文件的修改时间戳更改为旧的。如果您这样做，Make 将错误地猜测该文件没有更改，因此可以忽略。</p>\n<p>哎呀，真是一大堆。<strong>确保您理解这一点。这是 Makefiles 的核心，可能需要几分钟才能正确理解</strong>。如果仍然感到困惑，请尝试上述示例或观看上面的视频。</p>\n<h2>更多快速示例</h2>\n<p>以下 Makefile 最终运行所有三个目标。当您在终端中运行 <code>make</code> 时，它将通过一系列步骤构建一个名为 <code>blah</code> 的程序：</p>\n<ul>\n<li>Make 选择目标 <code>blah</code>，因为第一个目标是默认目标</li>\n<li><code>blah</code> 需要 <code>blah.o</code>，因此 make 搜索 <code>blah.o</code> 目标</li>\n<li><code>blah.o</code> 需要 <code>blah.c</code>，因此 make 搜索 <code>blah.c</code> 目标</li>\n<li><code>blah.c</code> 没有依赖项，因此运行 <code>echo</code> 命令</li>\n<li>然后运行 <code>cc -c</code> 命令，因为 <code>blah.o</code> 的所有依赖项都已完成</li>\n<li>运行顶部的 <code>cc</code> 命令，因为 <code>blah</code> 的所有依赖项都已完成</li>\n<li>就是这样：<code>blah</code> 是一个编译好的 C 程序</li>\n</ul>\n<pre><code class=\"language-makefile\">blah: blah.o\n    cc blah.o -o blah # Runs third\n\nblah.o: blah.c\n    cc -c blah.c -o blah.o # Runs second\n\n# Typically blah.c would already exist, but I want to limit any additional required files\nblah.c:\n    echo &quot;int main() { return 0; }&quot; &gt; blah.c # Runs first\n</code></pre>\n<p>如果您删除 <code>blah.c</code>，所有三个目标都将重新运行。如果您编辑它（从而将时间戳更改为比 <code>blah.o</code> 新），则前两个目标将运行。如果您运行 <code>touch blah.o</code>（从而将时间戳更改为比 <code>blah</code> 新），则只有第一个目标将运行。如果您不更改任何内容，则不会运行任何目标。试试看！</p>\n<p>下一个示例没有做任何新的事情，但仍然是一个很好的附加示例。它将始终运行两个目标，因为 <code>some_file</code> 依赖于 <code>other_file</code>，而 <code>other_file</code> 从未创建。</p>\n<pre><code class=\"language-makefile\">some_file: other_file\n    echo &quot;This will always run, and runs second&quot;\n    touch some_file\n\nother_file:\n    echo &quot;This will always run, and runs first&quot;\n</code></pre>\n<h2>Make clean</h2>\n<p><code>clean</code> 通常用作删除其他目标输出的目标，但它在 Make 中不是一个特殊词。您可以在此上运行 <code>make</code> 和 <code>make clean</code> 来创建和删除 <code>some_file</code>。</p>\n<p>请注意，<code>clean</code> 在这里做了两件新事情：</p>\n<ul>\n<li>它不是第一个（默认）目标，也不是先决条件。这意味着除非您明确调用 <code>make clean</code>，否则它永远不会运行</li>\n<li>它不打算成为文件名。如果您碰巧有一个名为 <code>clean</code> 的文件，此目标将不会运行，这不是我们想要的。请参阅本教程后面的 <code>.PHONY</code>，了解如何解决此问题</li>\n</ul>\n<pre><code class=\"language-makefile\">some_file:\n    touch some_file\n\nclean:\n    rm -f some_file\n</code></pre>\n<h2>变量</h2>\n<p>变量只能是字符串。您通常会希望使用 <code>:=</code>，但 <code>=</code> 也可以。请参阅 <a href=\"#%E5%8F%98%E9%87%8F%E7%AC%AC-2-%E9%83%A8%E5%88%86\">变量第 2 部分</a>。</p>\n<p>以下是使用变量的示例：</p>\n<pre><code class=\"language-makefile\">files := file1 file2\nsome_file: $(files)\n    echo &quot;Look at this variable: &quot; $(files)\n    touch some_file\n\nfile1:\n    touch file1\nfile2:\n    touch file2\n\nclean:\n    rm -f file1 file2 some_file\n</code></pre>\n<p>单引号或双引号对 Make 没有意义。它们只是分配给变量的字符。但是，引号<em>对</em> shell/bash 有用，您在 <code>printf</code> 等命令中需要它们。在此示例中，这两个命令的行为相同：</p>\n<pre><code class=\"language-makefile\">a := one two# a is set to the string &quot;one two&quot;\nb := &#39;one two&#39; # Not recommended. b is set to the string &quot;&#39;one two&#39;&quot;\nall:\n    printf &#39;$a&#39;\n    printf $b\n</code></pre>\n<p>使用 <code>${}</code> 或 <code>$()</code> 引用变量</p>\n<pre><code class=\"language-makefile\">x := dude\n\nall:\n    echo $(x)\n    echo ${x}\n\n    # Bad practice, but works\n    echo $x\n</code></pre>\n<h1>目标</h1>\n<h2>all 目标</h2>\n<!--  (Section 4.4) -->\n\n<p>要制作多个目标并希望它们全部运行？创建一个 <code>all</code> 目标。\n由于这是列出的第一个规则，如果调用 <code>make</code> 时未指定目标，它将默认运行。</p>\n<pre><code class=\"language-makefile\">all: one two three\n\none:\n    touch one\ntwo:\n    touch two\nthree:\n    touch three\n\nclean:\n    rm -f one two three\n</code></pre>\n<h2>多个目标</h2>\n<!--  (Section 4.8) -->\n\n<p>当一个规则有多个目标时，将为每个目标运行命令。<code>$@</code> 是一个 <a href=\"#%E8%87%AA%E5%8A%A8%E5%8F%98%E9%87%8F\">自动变量</a>，它包含目标名称。</p>\n<pre><code class=\"language-makefile\">all: f1.o f2.o\n\nf1.o f2.o:\n    echo $@\n# Equivalent to:\n# f1.o:\n# \techo f1.o\n# f2.o:\n# \techo f2.o\n</code></pre>\n<h1>自动变量和通配符</h1>\n<h2>* 通配符</h2>\n<!--  (Section 4.2) -->\n\n<p><code>*</code> 和 <code>%</code> 在 Make 中都称为通配符，但它们的意思完全不同。<code>*</code> 在您的文件系统中搜索匹配的文件名。我建议您始终将其包装在 <code>wildcard</code> 函数中，否则您可能会陷入下面描述的常见陷阱。</p>\n<pre><code class=\"language-makefile\"># Print out file information about every .c file\nprint: $(wildcard *.c)\n    ls -la  $?\n</code></pre>\n<p><code>*</code> 可用于目标、先决条件或 <code>wildcard</code> 函数中。</p>\n<p>危险：<code>*</code> 不能直接用于变量定义中</p>\n<p>危险：当 <code>*</code> 不匹配任何文件时，它会保持原样（除非在 <code>wildcard</code> 函数中运行）</p>\n<pre><code class=\"language-makefile\">thing_wrong := *.o # Don&#39;t do this! &#39;*&#39; will not get expanded\nthing_right := $(wildcard *.o)\n\nall: one two three four\n\n# Fails, because $(thing_wrong) is the string &quot;*.o&quot;\none: $(thing_wrong)\n\n# Stays as *.o if there are no files that match this pattern :(\ntwo: *.o\n\n# Works as you would expect! In this case, it does nothing.\nthree: $(thing_right)\n\n# Same as rule three\nfour: $(wildcard *.o)\n</code></pre>\n<h2>% 通配符</h2>\n<p><code>%</code> 非常有用，但由于其在各种情况下的使用方式而有些令人困惑。</p>\n<ul>\n<li>在“匹配”模式下使用时，它匹配字符串中的一个或多个字符。此匹配称为词干。</li>\n<li>在“替换”模式下使用时，它获取匹配的词干并将其替换到字符串中。</li>\n<li><code>%</code> 最常用于规则定义和某些特定函数中。</li>\n</ul>\n<p>请参阅以下部分，了解其使用示例：</p>\n<ul>\n<li><a href=\"#%E9%9D%99%E6%80%81%E6%A8%A1%E5%BC%8F%E8%A7%84%E5%88%99\">静态模式规则</a></li>\n<li><a href=\"#%E6%A8%A1%E5%BC%8F%E8%A7%84%E5%88%99\">模式规则</a></li>\n<li><a href=\"#%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%9B%BF%E6%8D%A2\">字符串替换</a></li>\n<li><a href=\"#vpath-%E6%8C%87%E4%BB%A4\">vpath 指令</a></li>\n</ul>\n<h2>自动变量</h2>\n<!--  (Section 10.5) -->\n\n<p>有许多 <a href=\"https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html\">自动变量</a>，但通常只出现几个：</p>\n<pre><code class=\"language-makefile\">hey: one two\n    # Outputs &quot;hey&quot;, since this is the target name\n    echo $@\n\n    # Outputs all prerequisites newer than the target\n    echo $?\n\n    # Outputs all prerequisites\n    echo $^\n\n    # Outputs the first prerequisite\n    echo $&lt;\n\n    touch hey\n\none:\n    touch one\n\ntwo:\n    touch two\n\nclean:\n    rm -f hey one two\n</code></pre>\n<h1>高级规则</h1>\n<h2>隐式规则</h2>\n<!--  (Section 10) -->\n\n<p>Make 喜欢 C 编译。每次它表达它的爱时，事情都会变得混乱。Make 最令人困惑的部分可能是它所做的魔法/自动规则。Make 将这些称为“隐式”规则。我个人不同意这种设计决策，我不建议使用它们，但它们经常被使用，因此了解它们很有用。以下是隐式规则的列表：</p>\n<ul>\n<li>编译 C 程序：<code>n.o</code> 自动从 <code>n.c</code> 生成，命令形式为 <code>$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@</code></li>\n<li>编译 C++ 程序：<code>n.o</code> 自动从 <code>n.cc</code> 或 <code>n.cpp</code> 生成，命令形式为 <code>$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@</code></li>\n<li>链接单个目标文件：<code>n</code> 自动从 <code>n.o</code> 生成，通过运行命令 <code>$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@</code></li>\n</ul>\n<p>隐式规则使用的重要变量是：</p>\n<ul>\n<li><code>CC</code>：用于编译 C 程序的程序；默认 <code>cc</code></li>\n<li><code>CXX</code>：用于编译 C++ 程序的程序；默认 <code>g++</code></li>\n<li><code>CFLAGS</code>：提供给 C 编译器的额外标志</li>\n<li><code>CXXFLAGS</code>：提供给 C++ 编译器的额外标志</li>\n<li><code>CPPFLAGS</code>：提供给 C 预处理器的额外标志</li>\n<li><code>LDFLAGS</code>：当编译器需要调用链接器时提供给编译器的额外标志</li>\n</ul>\n<p>让我们看看现在如何构建一个 C 程序，而无需明确告诉 Make 如何进行编译：</p>\n<pre><code class=\"language-makefile\">CC = gcc # Flag for implicit rules\nCFLAGS = -g # Flag for implicit rules. Turn on debug info\n\n# Implicit rule #1: blah is built via the C linker implicit rule\n# Implicit rule #2: blah.o is built via the C compilation implicit rule, because blah.c exists\nblah: blah.o\n\nblah.c:\n    echo &quot;int main() { return 0; }&quot; &gt; blah.c\n\nclean:\n    rm -f blah*\n</code></pre>\n<h2>静态模式规则</h2>\n<!--  (Section 4.10) -->\n\n<p>静态模式规则是 Makefile 中减少编写的另一种方式。以下是它们的语法：</p>\n<pre><code class=\"language-makefile\">targets...: target-pattern: prereq-patterns ...\n   commands\n</code></pre>\n<p>其本质是给定的 <code>target</code> 通过 <code>target-pattern</code>（通过 <code>%</code> 通配符）匹配。匹配到的内容称为<em>词干</em>。然后将词干替换到 <code>prereq-pattern</code> 中，以生成目标的先决条件。</p>\n<p>一个典型的用例是将 <code>.c</code> 文件编译成 <code>.o</code> 文件。以下是<em>手动方式</em>：</p>\n<pre><code class=\"language-makefile\">objects = foo.o bar.o all.o\nall: $(objects)\n    $(CC) $^ -o all\n\nfoo.o: foo.c\n    $(CC) -c foo.c -o foo.o\n\nbar.o: bar.c\n    $(CC) -c bar.c -o bar.o\n\nall.o: all.c\n    $(CC) -c all.c -o all.o\n\nall.c:\n    echo &quot;int main() { return 0; }&quot; &gt; all.c\n\n# Note: all.c does not use this rule because Make prioritizes more specific matches when there is more than one match.\n%.c:\n    touch $@\n\nclean:\n    rm -f *.c *.o all\n</code></pre>\n<p>以下是更<em>高效的方式</em>，使用静态模式规则：</p>\n<pre><code class=\"language-makefile\">objects = foo.o bar.o all.o\nall: $(objects)\n    $(CC) $^ -o all\n\n# Syntax - targets ...: target-pattern: prereq-patterns ...\n# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the &quot;stem&quot; to be &quot;foo&quot;.\n# It then replaces the &#39;%&#39; in prereq-patterns with that stem\n$(objects): %.o: %.c\n    $(CC) -c $^ -o $@\n\nall.c:\n    echo &quot;int main() { return 0; }&quot; &gt; all.c\n\n# Note: all.c does not use this rule because Make prioritizes more specific matches when there is more than one match.\n%.c:\n    touch $@\n\nclean:\n    rm -f *.c *.o all\n</code></pre>\n<h2>静态模式规则和过滤器</h2>\n<!--  (Section 4.10) -->\n\n<p>虽然我稍后会介绍 <a href=\"#filter-%E5%87%BD%E6%95%B0\">filter 函数</a>，但它在静态模式规则中很常用，所以我会在这里提及。<code>filter</code> 函数可以在静态模式规则中使用，以匹配正确的文件。在此示例中，我创建了 <code>.raw</code> 和 <code>.result</code> 扩展名。</p>\n<pre><code class=\"language-makefile\">obj_files = foo.result bar.o lose.o\nsrc_files = foo.raw bar.c lose.c\n\nall: $(obj_files)\n# Note: PHONY is important here. Without it, implicit rules will try to build the executable &quot;all&quot;, since the prereqs are &quot;.o&quot; files.\n.PHONY: all\n\n# Ex 1: .o files depend on .c files. Though we don&#39;t actually make the .o file.\n$(filter %.o,$(obj_files)): %.o: %.c\n    echo &quot;target: $@ prereq: $&lt;&quot;\n\n# Ex 2: .result files depend on .raw files. Though we don&#39;t actually make the .result file.\n$(filter %.result,$(obj_files)): %.result: %.raw\n    echo &quot;target: $@ prereq: $&lt;&quot;\n\n%.c %.raw:\n    touch $@\n\nclean:\n    rm -f $(src_files)\n</code></pre>\n<h2>模式规则</h2>\n<p>模式规则经常使用，但相当令人困惑。您可以从两个方面来看待它们：</p>\n<ul>\n<li>定义自己的隐式规则的一种方式</li>\n<li>静态模式规则的更简单形式</li>\n</ul>\n<p>让我们先看一个例子：</p>\n<pre><code class=\"language-makefile\"># Define a pattern rule that compiles every .c file into a .o file\n%.o : %.c\n        $(CC) -c $(CFLAGS) $(CPPFLAGS) $&lt; -o $@\n</code></pre>\n<p>模式规则在目标中包含一个 &#39;%&#39;。这个 &#39;%&#39; 匹配任何非空字符串，而其他字符匹配它们自己。模式规则的先决条件中的 &#39;%&#39; 代表与目标中的 &#39;%&#39; 匹配的相同词干。</p>\n<p>这是另一个例子：</p>\n<pre><code class=\"language-makefile\"># Define a pattern rule that has no pattern in the prerequisites.\n# This just creates empty .c files when needed.\n%.c:\n   touch $@\n</code></pre>\n<h2>双冒号规则</h2>\n<!--  (Section 4.11) -->\n\n<p>双冒号规则很少使用，但允许为同一目标定义多个规则。如果这些是单冒号，则会打印警告，并且只有第二组命令会运行。</p>\n<pre><code class=\"language-makefile\">all: blah\n\nblah::\n    echo &quot;hello&quot;\n\nblah::\n    echo &quot;hello again&quot;\n</code></pre>\n<h1>命令和执行</h1>\n<h2>命令回显/静默</h2>\n<!--  (Section 5.1) -->\n\n<p>在命令前添加 <code>@</code> 以阻止其打印\n您还可以使用 <code>-s</code> 运行 make，以便在每行前添加 <code>@</code></p>\n<pre><code class=\"language-makefile\">all:\n    @echo &quot;This make line will not be printed&quot;\n    echo &quot;But this will&quot;\n</code></pre>\n<h2>命令执行</h2>\n<!--  (Section 5.2) -->\n\n<p>每个命令都在一个新的 shell 中运行（或者至少效果是如此）</p>\n<pre><code class=\"language-makefile\">all:\n    cd ..\n    # The cd above does not affect this line, because each command is effectively run in a new shell\n    echo `pwd`\n\n    # This cd command affects the next because they are on the same line\n    cd ..;echo `pwd`\n\n    # Same as above\n    cd ..; \\\n    echo `pwd`\n</code></pre>\n<h2>默认 Shell</h2>\n<!--  (Section 5.2) -->\n\n<p>默认 shell 是 <code>/bin/sh</code>。您可以通过更改变量 SHELL 来更改它：</p>\n<pre><code class=\"language-makefile\">SHELL=/bin/bash\n\ncool:\n    echo &quot;Hello from bash&quot;\n</code></pre>\n<h2>双美元符号</h2>\n<p>如果您希望字符串中包含美元符号，可以使用 <code>$$</code>。这是在 <code>bash</code> 或 <code>sh</code> 中使用 shell 变量的方法。</p>\n<p>请注意下一个示例中 Makefile 变量和 Shell 变量之间的区别。</p>\n<pre><code class=\"language-makefile\">make_var = I am a make variable\nall:\n    # Same as running &quot;sh_var=&#39;I am a shell variable&#39;; echo $sh_var&quot; in the shell\n    sh_var=&#39;I am a shell variable&#39;; echo $$sh_var\n\n    # Same as running &quot;echo I am a make variable&quot; in the shell\n    echo $(make_var)\n</code></pre>\n<h2>错误处理与 <code>-k</code>、<code>-i</code> 和 <code>-</code></h2>\n<!--  (Section 5.4) -->\n\n<p>运行 make 时添加 <code>-k</code>，即使出现错误也继续运行。如果您想一次性查看 Make 的所有错误，这很有帮助。\n在命令前添加 <code>-</code> 以抑制错误\n向 make 添加 <code>-i</code> 以使每个命令都发生这种情况。</p>\n<!--  (Section 5.4) -->\n\n<pre><code class=\"language-makefile\">one:\n    # This error will be printed but ignored, and make will continue to run\n    -false\n    touch one\n</code></pre>\n<h2>中断或终止 make</h2>\n<!--  (Section 5.5) -->\n\n<p>注意：如果您 <code>ctrl+c</code> make，它将删除刚刚创建的较新目标。</p>\n<h2>make 的递归使用</h2>\n<!--  (Section 5.6) -->\n\n<p>要递归调用 makefile，请使用特殊的 <code>$(MAKE)</code> 而不是 <code>make</code>，因为它会为您传递 make 标志，并且本身不会受其影响。</p>\n<pre><code class=\"language-makefile\">new_contents = &quot;hello:\\n\\ttouch inside_file&quot;\nall:\n    mkdir -p subdir\n    printf $(new_contents) | sed -e &#39;s/^ //&#39; &gt; subdir/makefile\n    cd subdir &amp;&amp; $(MAKE)\n\nclean:\n    rm -rf subdir\n</code></pre>\n<h2>导出、环境和递归 make</h2>\n<!--  (Section 5.6) -->\n\n<p>当 Make 启动时，它会自动从执行时设置的所有环境变量中创建 Make 变量。</p>\n<pre><code class=\"language-makefile\"># Run this with &quot;export shell_env_var=&#39;I am an environment variable&#39;; make&quot;\nall:\n    # Print out the Shell variable\n    echo $$shell_env_var\n\n    # Print out the Make variable\n    echo $(shell_env_var)\n</code></pre>\n<p><code>export</code> 指令获取一个变量并将其设置为所有配方中所有 shell 命令的环境：</p>\n<pre><code class=\"language-makefile\">shell_env_var=Shell env var, created inside of Make\nexport shell_env_var\nall:\n    echo $(shell_env_var)\n    echo $$shell_env_var\n</code></pre>\n<p>因此，当您在 make 中运行 <code>make</code> 命令时，您可以使用 <code>export</code> 指令使其可供子 make 命令访问。在此示例中，<code>cooly</code> 被导出，以便 subdir 中的 makefile 可以使用它。</p>\n<pre><code class=\"language-makefile\">new_contents = &quot;hello:\\n\\techo \\$$(cooly)&quot;\n\nall:\n    mkdir -p subdir\n    printf $(new_contents) | sed -e &#39;s/^ //&#39; &gt; subdir/makefile\n    @echo &quot;---MAKEFILE CONTENTS---&quot;\n    @cd subdir &amp;&amp; cat makefile\n    @echo &quot;---END MAKEFILE CONTENTS---&quot;\n    cd subdir &amp;&amp; $(MAKE)\n\n# Note that variables and exports. They are set/affected globally.\ncooly = &quot;The subdirectory can see me!&quot;\nexport cooly\n# This would nullify the line above: unexport cooly\n\nclean:\n    rm -rf subdir\n</code></pre>\n<!--  (Section 5.6) -->\n\n<p>您需要导出变量才能在 shell 中运行它们。</p>\n<pre><code class=\"language-makefile\">one=this will only work locally\nexport two=we can run subcommands with this\n\nall:\n    @echo $(one)\n    @echo $$one\n    @echo $(two)\n    @echo $$two\n</code></pre>\n<!--  (Section 5.6) -->\n\n<p><code>.EXPORT_ALL_VARIABLES</code> 为您导出所有变量。</p>\n<pre><code class=\"language-makefile\">.EXPORT_ALL_VARIABLES:\nnew_contents = &quot;hello:\\n\\techo \\$$(cooly)&quot;\n\ncooly = &quot;The subdirectory can see me!&quot;\n# This would nullify the line above: unexport cooly\n\nall:\n    mkdir -p subdir\n    printf $(new_contents) | sed -e &#39;s/^ //&#39; &gt; subdir/makefile\n    @echo &quot;---MAKEFILE CONTENTS---&quot;\n    @cd subdir &amp;&amp; cat makefile\n    @echo &quot;---END MAKEFILE CONTENTS---&quot;\n    cd subdir &amp;&amp; $(MAKE)\n\nclean:\n    rm -rf subdir\n</code></pre>\n<h2>make 的参数</h2>\n<!--  (Section 9) -->\n\n<p>有一个很好的 <a href=\"http://www.gnu.org/software/make/manual/make.html#Options-Summary\">选项列表</a> 可以从 make 运行。查看 <code>--dry-run</code>、<code>--touch</code>、<code>--old-file</code>。</p>\n<p>您可以有多个目标要制作，即 <code>make clean run test</code> 运行 <code>clean</code> 目标，然后是 <code>run</code>，然后是 <code>test</code>。</p>\n<h1>变量第 2 部分</h1>\n<h2>风格和修改</h2>\n<!-- (6.1, 6.2, 6.3) -->\n\n<p>变量有两种风格：</p>\n<ul>\n<li>递归（使用 <code>=</code>） - 仅在命令<em>使用</em>时查找变量，而不是在命令<em>定义</em>时查找。</li>\n<li>简单扩展（使用 <code>:=</code>） - 像正常的命令式编程一样 -- 只有到目前为止定义的变量才会被扩展</li>\n</ul>\n<pre><code class=\"language-makefile\"># Recursive variable. This will print &quot;later&quot; below\none = one ${later_variable}\n# Simply expanded variable. This will not print &quot;later&quot; below\ntwo := two ${later_variable}\n\nlater_variable = later\n\nall:\n    echo $(one)\n    echo $(two)\n</code></pre>\n<p>简单扩展（使用 <code>:=</code>）允许您向变量追加。递归定义将导致无限循环错误。</p>\n<pre><code class=\"language-makefile\">one = hello\n# one gets defined as a simply expanded variable (:=) and thus can handle appending\none := ${one} there\n\nall:\n    echo $(one)\n</code></pre>\n<p><code>?=</code> 仅在变量尚未设置时才设置变量</p>\n<pre><code class=\"language-makefile\">one = hello\none ?= will not be set\ntwo ?= will be set\n\nall:\n    echo $(one)\n    echo $(two)\n</code></pre>\n<p>行尾的空格不会被删除，但开头的空格会被删除。要创建一个包含单个空格的变量，请使用 <code>$(nullstring)</code></p>\n<pre><code class=\"language-makefile\">with_spaces = hello   # with_spaces has many spaces after &quot;hello&quot;\nafter = $(with_spaces)there\n\nnullstring =\nspace = $(nullstring) # Make a variable with a single space.\n\nall:\n    echo &quot;$(after)&quot;\n    echo start&quot;$(space)&quot;end\n</code></pre>\n<p>未定义的变量实际上是一个空字符串！</p>\n<pre><code class=\"language-makefile\">all:\n    # Undefined variables are just empty strings!\n    echo $(nowhere)\n</code></pre>\n<p>使用 <code>+=</code> 追加</p>\n<pre><code class=\"language-makefile\">foo := start\nfoo += more\n\nall:\n    echo $(foo)\n</code></pre>\n<p><a href=\"#%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%9B%BF%E6%8D%A2\">字符串替换</a> 也是一种非常常见且有用的修改变量的方法。另请查看 <a href=\"https://www.gnu.org/software/make/manual/html_node/Text-Functions.html#Text-Functions\">文本函数</a> 和 <a href=\"https://www.gnu.org/software/make/manual/html_node/File-Name-Functions.html#File-Name-Functions\">文件名函数</a>。</p>\n<h2>命令行参数和覆盖</h2>\n<!--  (Section 6.7) -->\n\n<p>您可以使用 <code>override</code> 覆盖来自命令行的变量。\n这里我们使用 <code>make option_one=hi</code> 运行 make</p>\n<pre><code class=\"language-makefile\"># Overrides command line arguments\noverride option_one = did_override\n# Does not override command line arguments\noption_two = not_override\nall:\n    echo $(option_one)\n    echo $(option_two)\n</code></pre>\n<h2>命令列表和定义</h2>\n<!--  (Section 6.8) -->\n\n<p><a href=\"https://www.gnu.org/software/make/manual/html_node/Multi_002dLine.html\">define 指令</a> 不是一个函数，尽管它可能看起来像那样。我很少看到它被使用，所以我不会详细介绍，但它主要用于定义 <a href=\"https://www.gnu.org/software/make/manual/html_node/Canned-Recipes.html#Canned-Recipes\">预设配方</a>，并且与 <a href=\"https://www.gnu.org/software/make/manual/html_node/Eval-Function.html#Eval-Function\">eval 函数</a> 配合得很好。</p>\n<p><code>define</code>/<code>endef</code> 只是创建一个变量，该变量设置为命令列表。请注意，这与命令之间用分号分隔略有不同，因为每个命令都在单独的 shell 中运行，正如预期的那样。</p>\n<pre><code class=\"language-makefile\">one = export blah=&quot;I was set!&quot;; echo $$blah\n\ndefine two\nexport blah=&quot;I was set!&quot;\necho $$blah\nendef\n\nall:\n    @echo &quot;This prints &#39;I was set&#39;&quot;\n    @$(one)\n    @echo &quot;This does not print &#39;I was set&#39; because each command runs in a separate shell&quot;\n    @$(two)\n</code></pre>\n<h2>目标特定变量</h2>\n<!--  (Section 6.10) -->\n\n<p>可以为特定目标设置变量</p>\n<pre><code class=\"language-makefile\">all: one = cool\n\nall:\n    echo one is defined: $(one)\n\nother:\n    echo one is nothing: $(one)\n</code></pre>\n<h2>模式特定变量</h2>\n<!--  (Section 6.11) -->\n\n<p>您可以为特定目标<em>模式</em>设置变量</p>\n<pre><code class=\"language-makefile\">%.c: one = cool\n\nblah.c:\n    echo one is defined: $(one)\n\nother:\n    echo one is nothing: $(one)\n</code></pre>\n<h1>Makefiles 的条件部分</h1>\n<h2>条件 if/else</h2>\n<!--  (Section 7.1) -->\n\n<pre><code class=\"language-makefile\">foo = ok\n\nall:\nifeq ($(foo), ok)\n    echo &quot;foo equals ok&quot;\nelse\n    echo &quot;nope&quot;\nendif\n</code></pre>\n<h2>检查变量是否为空</h2>\n<!--  (Section 7.2) -->\n\n<pre><code class=\"language-makefile\">nullstring =\nfoo = $(nullstring) # end of line; there is a space here\n\nall:\nifeq ($(strip $(foo)),)\n    echo &quot;foo is empty after being stripped&quot;\nendif\nifeq ($(nullstring),)\n    echo &quot;nullstring doesn&#39;t even have spaces&quot;\nendif\n</code></pre>\n<h2>检查变量是否已定义</h2>\n<!--  (Section 7.2) -->\n\n<p>ifdef 不会扩展变量引用；它只是查看是否定义了任何内容</p>\n<pre><code class=\"language-makefile\">bar =\nfoo = $(bar)\n\nall:\nifdef foo\n    echo &quot;foo is defined&quot;\nendif\nifndef bar\n    echo &quot;but bar is not&quot;\nendif\n</code></pre>\n<h2>$(MAKEFLAGS)</h2>\n<!-- `(Section 7.3) -->\n\n<p>此示例向您展示如何使用 <code>findstring</code> 和 <code>MAKEFLAGS</code> 测试 make 标志。使用 <code>make -i</code> 运行此示例，以查看它打印 echo 语句。</p>\n<pre><code class=\"language-makefile\">all:\n# Search for the &quot;-i&quot; flag. MAKEFLAGS is just a list of single characters, one per flag. So look for &quot;i&quot; in this case.\nifneq (,$(findstring i, $(MAKEFLAGS)))\n    echo &quot;i was passed to MAKEFLAGS&quot;\nendif\n</code></pre>\n<h1>函数</h1>\n<h2>第一个函数</h2>\n<!--  (Section 8.1) -->\n\n<p><em>函数</em>主要用于文本处理。使用 <code>$(fn, arguments)</code> 或 <code>${fn, arguments}</code> 调用函数。Make 有相当多的 <a href=\"https://www.gnu.org/software/make/manual/html_node/Functions.html\">内置函数</a>。</p>\n<pre><code class=\"language-makefile\">bar := ${subst not,&quot;totally&quot;, &quot;I am not superman&quot;}\nall:\n    @echo $(bar)\n</code></pre>\n<p>如果要替换空格或逗号，请使用变量</p>\n<pre><code class=\"language-makefile\">comma := ,\nempty:=\nspace := $(empty) $(empty)\nfoo := a b c\nbar := $(subst $(space),$(comma),$(foo))\n\nall:\n    @echo $(bar)\n</code></pre>\n<p>第一个参数之后不要包含空格。那将被视为字符串的一部分。</p>\n<pre><code class=\"language-makefile\">comma := ,\nempty:=\nspace := $(empty) $(empty)\nfoo := a b c\nbar := $(subst $(space), $(comma) , $(foo)) # Watch out!\n\nall:\n    # Output is &quot;, a , b , c&quot;. Notice the spaces introduced\n    @echo $(bar)\n</code></pre>\n<!-- # 8.2, 8.3, 8.9 TODO do something about the fns\n# TODO 8.7 origin fn? Better in documentation?\n-->\n\n<h2>字符串替换</h2>\n<p><code>$(patsubst pattern,replacement,text)</code> 执行以下操作：</p>\n<p>“在文本中查找与模式匹配的以空格分隔的单词，并将其替换为 replacement。这里的模式可以包含一个 &#39;%&#39;，它充当通配符，匹配单词中的任意数量的任意字符。如果 replacement 也包含一个 &#39;%&#39;，则该 &#39;%&#39; 将被模式中匹配 &#39;%&#39; 的文本替换。只有模式和 replacement 中的第一个 &#39;%&#39; 以这种方式处理；任何后续的 &#39;%&#39; 保持不变。”（<a href=\"https://www.gnu.org/software/make/manual/html_node/Text-Functions.html#Text-Functions\">GNU 文档</a>）</p>\n<p>替换引用 <code>$(text:pattern=replacement)</code> 是上述的简写。</p>\n<p>还有另一种简写，只替换后缀：<code>$(text:suffix=replacement)</code>。这里不使用 <code>%</code> 通配符。</p>\n<p>注意：不要为此简写添加额外的空格。它将被视为搜索或替换项。</p>\n<pre><code class=\"language-makefile\">foo := a.o b.o l.a c.o\none := $(patsubst %.o,%.c,$(foo))\n# This is a shorthand for the above\ntwo := $(foo:%.o=%.c)\n# This is the suffix-only shorthand, and is also equivalent to the above.\nthree := $(foo:.o=.c)\n\nall:\n    echo $(one)\n    echo $(two)\n    echo $(three)\n</code></pre>\n<h2>foreach 函数</h2>\n<!--  (Section 8.4) -->\n\n<p>foreach 函数如下所示：<code>$(foreach var,list,text)</code>。它将一个单词列表（以空格分隔）转换为另一个单词列表。<code>var</code> 被设置为列表中的每个单词，并且 <code>text</code> 为每个单词展开。\n这会在每个单词后附加一个感叹号：</p>\n<pre><code class=\"language-makefile\">foo := who are you\n# For each &quot;word&quot; in foo, output that same word with an exclamation after\nbar := $(foreach wrd,$(foo),$(wrd)!)\n\nall:\n    # Output is &quot;who! are! you!&quot;\n    @echo $(bar)\n</code></pre>\n<h2>if 函数</h2>\n<!--  (Section 8.5) -->\n\n<p><code>if</code> 检查第一个参数是否非空。如果非空，则运行第二个参数，否则运行第三个参数。</p>\n<pre><code class=\"language-makefile\">foo := $(if this-is-not-empty,then!,else!)\nempty :=\nbar := $(if $(empty),then!,else!)\n\nall:\n    @echo $(foo)\n    @echo $(bar)\n</code></pre>\n<h2>call 函数</h2>\n<!--  (Section 8.6) -->\n\n<p>Make 支持创建基本函数。您只需通过创建变量来“定义”函数，但使用参数 <code>$(0)</code>、<code>$(1)</code> 等。然后使用特殊的 <a href=\"https://www.gnu.org/software/make/manual/html_node/Call-Function.html#Call-Function\"><code>call</code></a> 内置函数调用该函数。语法是 <code>$(call variable,param,param)</code>。<code>$(0)</code> 是变量，而 <code>$(1)</code>、<code>$(2)</code> 等是参数。</p>\n<pre><code class=\"language-makefile\">sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)\n\nall:\n    # Outputs &quot;Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:&quot;\n    @echo $(call sweet_new_fn, go, tigers)\n</code></pre>\n<h2>shell 函数</h2>\n<!--  (Section 8.8) -->\n\n<p>shell - 这会调用 shell，但它会将换行符替换为空格！</p>\n<pre><code class=\"language-makefile\">all:\n    @echo $(shell ls -la) # Very ugly because the newlines are gone!\n</code></pre>\n<h2>filter 函数</h2>\n<p><code>filter</code> 函数用于从列表中选择与特定模式匹配的某些元素。例如，这将选择 <code>obj_files</code> 中所有以 <code>.o</code> 结尾的元素。</p>\n<pre><code class=\"language-makefile\">obj_files = foo.result bar.o lose.o\nfiltered_files = $(filter %.o,$(obj_files))\n\nall:\n    @echo $(filtered_files)\n</code></pre>\n<p>Filter 也可以以更复杂的方式使用：</p>\n<ol>\n<li><p><strong>过滤多个模式</strong>：您可以一次过滤多个模式。例如，<code>$(filter %.c %.h, $(files))</code> 将从文件列表中选择所有 <code>.c</code> 和 <code>.h</code> 文件。</p>\n</li>\n<li><p><strong>否定</strong>：如果您想选择所有不匹配模式的元素，可以使用 <code>filter-out</code>。例如，<code>$(filter-out %.h, $(files))</code> 将选择所有不是 <code>.h</code> 文件的文件。</p>\n</li>\n<li><p><strong>嵌套过滤器</strong>：您可以嵌套过滤器函数以应用多个过滤器。例如，<code>$(filter %.o, $(filter-out test%, $(objects)))</code> 将选择所有以 <code>.o</code> 结尾但不以 <code>test</code> 开头的对象文件。</p>\n</li>\n</ol>\n<h1>其他功能</h1>\n<h2>包含 Makefiles</h2>\n<p>include 指令告诉 make 读取一个或多个其他 makefile。它是 makefile 中的一行，看起来像这样：</p>\n<pre><code class=\"language-makefile\">include filenames...\n</code></pre>\n<p>当您使用 <code>-M</code> 等编译器标志根据源文件创建 Makefiles 时，这尤其有用。例如，如果某些 C 文件包含一个头文件，该头文件将被添加到由 gcc 编写的 Makefile 中。我在 <a href=\"#makefile-cookbook\">Makefile Cookbook</a> 中对此进行了更多讨论。</p>\n<h2>vpath 指令</h2>\n<!--  (Section 4.3.2) -->\n\n<p>使用 vpath 指定某些先决条件存在的位置。格式为 <code>vpath &lt;pattern&gt; &lt;directories, space/colon separated&gt;</code>\n<code>&lt;pattern&gt;</code> 可以包含一个 <code>%</code>，它匹配任意零个或多个字符。\n您也可以使用变量 VPATH 全局地执行此操作</p>\n<pre><code class=\"language-makefile\">vpath %.h ../headers ../other-directory\n\n# Note: vpath allows blah.h to be found even though blah.h is never in the current directory\nsome_binary: ../headers blah.h\n    touch some_binary\n\n../headers:\n    mkdir ../headers\n\n# We call the target blah.h instead of ../headers/blah.h, because that&#39;s the prereq that some_binary is looking for\n# Typically, blah.h would already exist and you wouldn&#39;t need this.\nblah.h:\n    touch ../headers/blah.h\n\nclean:\n    rm -rf ../headers\n    rm -f some_binary\n</code></pre>\n<h2>多行</h2>\n<p>反斜杠 (&quot;\\&quot;) 字符使我们能够在命令过长时使用多行</p>\n<pre><code class=\"language-makefile\">some_file:\n    echo This line is too long, so \\\n        it is broken up into multiple lines\n</code></pre>\n<h2>.phony</h2>\n<p>将 <code>.PHONY</code> 添加到目标将防止 Make 将虚假目标与文件名混淆。在此示例中，如果创建了文件 <code>clean</code>，<code>make clean</code> 仍将运行。从技术上讲，我应该在每个带有 <code>all</code> 或 <code>clean</code> 的示例中使用它，但我想保持示例的简洁。此外，“虚假”目标的名称通常很少是文件名，实际上许多人会跳过此步骤。</p>\n<pre><code class=\"language-makefile\">some_file:\n    touch some_file\n    touch clean\n\n.PHONY: clean\nclean:\n    rm -f some_file\n    rm -f clean\n</code></pre>\n<h2>.delete_on_error</h2>\n<!-- (Section 5.4) -->\n\n<p>如果命令返回非零退出状态，make 工具将停止运行规则（并将传播回先决条件）。\n<code>DELETE_ON_ERROR</code> 将在规则以这种方式失败时删除规则的目标。这将适用于所有目标，而不仅仅是它之前的目标，例如 PHONY。始终使用此功能是个好主意，尽管出于历史原因 make 没有这样做。</p>\n<pre><code class=\"language-makefile\">.DELETE_ON_ERROR:\nall: one two\n\none:\n    touch one\n    false\n\ntwo:\n    touch two\n    false\n</code></pre>\n<h1>Makefile Cookbook</h1>\n<p>让我们来看一个非常棒的 Make 示例，它非常适用于中型项目。</p>\n<p>这个 makefile 的巧妙之处在于它会自动为您确定依赖关系。您所要做的就是将 C/C++ 文件放入 <code>src/</code> 文件夹中。</p>\n<pre><code class=\"language-makefile\"># Thanks to Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/)\nTARGET_EXEC := final_program\n\nBUILD_DIR := ./build\nSRC_DIRS := ./src\n\n# Find all the C and C++ files we want to compile\n# Note the single quotes around the * expressions. The shell will incorrectly expand these otherwise, but we want to send the * directly to the find command.\nSRCS := $(shell find $(SRC_DIRS) -name &#39;*.cpp&#39; -or -name &#39;*.c&#39; -or -name &#39;*.s&#39;)\n\n# Prepends BUILD_DIR and appends .o to every src file\n# As an example, ./your_dir/hello.cpp turns into ./build/./your_dir/hello.cpp.o\nOBJS := $(SRCS:%=$(BUILD_DIR)/%.o)\n\n# String substitution (suffix version without %).\n# As an example, ./build/hello.cpp.o turns into ./build/hello.cpp.d\nDEPS := $(OBJS:.o=.d)\n\n# Every folder in ./src will need to be passed to GCC so that it can find header files\nINC_DIRS := $(shell find $(SRC_DIRS) -type d)\n# Add a prefix to INC_DIRS. So moduleA would become -ImoduleA. GCC understands this -I flag\nINC_FLAGS := $(addprefix -I,$(INC_DIRS))\n\n# The -MMD and -MP flags together generate Makefiles for us!\n# These files will have .d instead of .o as the output.\nCPPFLAGS := $(INC_FLAGS) -MMD -MP\n\n# The final build step.\n$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)\n    $(CXX) $(OBJS) -o $@ $(LDFLAGS)\n\n# Build step for C source\n$(BUILD_DIR)/%.c.o: %.c\n    mkdir -p $(dir $@)\n    $(CC) $(CPPFLAGS) $(CFLAGS) -c $&lt; -o $@\n\n# Build step for C++ source\n$(BUILD_DIR)/%.cpp.o: %.cpp\n    mkdir -p $(dir $@)\n    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $&lt; -o $@\n\n\n.PHONY: clean\nclean:\n    rm -r $(BUILD_DIR)\n\n# Include the .d makefiles. The - at the front suppresses the errors of missing\n# Makefiles. Initially, all the .d files will be missing, and we don&#39;t want those\n# errors to show up.\n-include $(DEPS)\n</code></pre>\n<!--\nTODO: This example fails initially because blah.d doesn't exist. I'm not sure how to fix this example, there are probably better ones out there..\n\n# Generating Prerequisites Automatically (Section 4.12)\nExample requires: blah.c\nGenerating prereqs automatically\nThis makes one small makefile per source file\nNotes:\n1) $$ is the current process id in bash. $$$$ is just $$, with escaping. We use it to make a temporary file, that doesn't interfere with others if there is some parallel builds going on.\n2) cc -MM outputs a makefile line. This is the magic that generates prereqs automatically, by looking at the code itself\n3) The purpose of the sed command is to translate (for example):\n    main.o : main.c defs.h\n    into:\n    main.o main.d : main.c defs.h\n4) Running `make clean` will rerun the rm -f ... rule because the include line wants to include an up to date version of the file. There is such a target that updates it, so it runs that rule before including the file.\n```makefile\n# Run make init first, then run make\n# This outputs\nall: blah.d\n\nclean:\n    rm -f blah.d blah.c blah.h blah.o blah\n\n%.d: %.c\n    rm -f $@; \\\n     $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \\\n     sed 's,\\($*\\)\\.o[ :]*,\\1.o $@ : ,g' < $@.$$$$ > $@; \\\n     rm -f $@.$$$$\n\ninit:\n    echo \"#include \\\"blah.h\\\"; int main() { return 0; }\" > blah.c\n    touch blah.h\n\nsources = blah.c\n\ninclude $(sources:.c=.d)\n```\n-->\n","tags":["linux"]},{"id":"nodejs-vm","url":"https://yieldray.fun/posts/nodejs-vm","title":"Node.js VM","date_published":"2025-05-05T23:00:00.000Z","date_modified":"2025-05-05T23:00:00.000Z","content_text":"<h1>CommonJS: <a href=\"https://nodejs.org/docs/latest/api/vm.html#class-vmscript\">vm.Script</a></h1>\n<p>从源码运行一个普通脚本：首先创建上下文对象，然后创建 vm.Script 对象，最后执行之。</p>\n<p>上下文对象是通过使一个普通对象 “contextify” 来创建的，可通过 <code>vm.isContext</code> 判断一个对象是否是 contextified 对象。</p>\n<pre><code class=\"language-ts\">import vm from &quot;node:vm&quot;;\n\nconst script = new vm.Script(\n    /*js*/ `\n    function add(a, b) {\n      // console 实际上是 globalThis.console \n      console.log(&quot;Called times: &quot; + globalThis.count++);\n      return a + b;\n    }\n    \n    const x = add(1, 2);\n    const y = add(3, 4);\n    &quot;eXpression&quot;;\n`,\n    {\n        // 所有选项都是可选的\n        filename: &quot;evalmachine.&lt;anonymous&gt;&quot;,\n        lineOffset: 0,\n        columnOffset: 0,\n    },\n);\n\nconst context: vm.Context = vm.createContext(\n    { count: 666, console },\n    {\n        name: &quot;VM Context 0&quot;,\n        origin: &quot;file://test.js&quot;,\n        codeGeneration: {\n            strings: true,\n            wasm: true,\n        },\n        microtaskMode: &quot;afterEvaluate&quot;,\n    },\n);\n\n// 返回表达式的值\nconst result = script.runInContext(context, {\n    displayErrors: true,\n    timeout: 1234,\n});\nconsole.log(result); // =&gt; &quot;eXpression&quot;\n</code></pre>\n<p>也可以从源码编译一个函数。</p>\n<pre><code class=\"language-ts\">const parsingContext: vm.Context = vm.createContext(vm.constants.DONT_CONTEXTIFY);\nparsingContext[&quot;console&quot;] = console;\nconst fn: Function = vm.compileFunction(`return console.log(a + b)`, [&quot;a&quot;, &quot;b&quot;] as const, {\n    parsingContext, // 编译该函数的上下文对象\n    contextExtensions: [],\n    produceCachedData: false,\n    cachedData: Buffer.from(&quot;nope&quot;),\n});\n\nfn(&quot;Hello, &quot;, &quot;World!&quot;); // =&gt; Hello, World!\n</code></pre>\n<p>vm.runInThisContext() 与 [<code>eval()</code>][<a href=\"https://tc39.es/ecma262/#sec-eval-x%5D\">https://tc39.es/ecma262/#sec-eval-x]</a> 的区别在于 vm.runInThisContext() 无权访问本地作用域，但 eval() 则可以。<br>因此 vm.runInThisContext() 类似于 <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#direct_and_indirect_eval\">indirect <code>eval()</code> call</a></p>\n<pre><code class=\"language-js\">const vm = require(&quot;node:vm&quot;);\nlet localVar = &quot;initial value&quot;;\n\nconst vmResult = vm.runInThisContext(&#39;localVar = &quot;vm&quot;;&#39;);\nconsole.log(`vmResult: &#39;${vmResult}&#39;, localVar: &#39;${localVar}&#39;`);\n// =&gt;        vmResult: &#39;vm&#39;, localVar: &#39;initial value&#39;\n\nconst evalResult = eval(&#39;localVar = &quot;eval&quot;;&#39;);\nconsole.log(`evalResult: &#39;${evalResult}&#39;, localVar: &#39;${localVar}&#39;`);\n// =&gt;        evalResult: &#39;eval&#39;, localVar: &#39;eval&#39;\n</code></pre>\n<h1>上下文对象 “contextify”</h1>\n<p>所有在 Node.js 中执行的 JavaScript 代码都在“上下文”的范围内运行。根据 <a href=\"https://v8.dev/docs/embed#contexts\">V8 嵌入器指南</a>：</p>\n<pre><code>在 V8 中，上下文是一个执行环境，它允许独立的、不相关的 JavaScript 应用程序在单个 V8 实例中运行。必须显式指定要运行任何 JavaScript 代码的上下文。\n</code></pre>\n<p>当传入对象来调用方法 <code>vm.createContext()</code> 时，<code>contextObject</code> 参数将用于包装 V8 上下文新实例的全局对象（如果 <code>contextObject</code> 是 <code>undefined</code>，则将在 contextified 之前从当前上下文创建一个新对象）。<br>此 V8 上下文为使用 node:vm 模块的方法运行的代码提供了一个<strong>隔离的全局环境</strong>，代码可以在其中运行。创建 V8 上下文并将其与外部上下文中的 <code>contextObject</code> 关联的过程就是“contextifying”对象。</p>\n<p>contextifying 会给上下文中的 <code>globalThis</code> 值引入一些怪异之处。例如，它不能被冻结，并且它与外部上下文中的 <code>contextObject</code> 的引用不相等。</p>\n<pre><code class=\"language-js\">const vm = require(&quot;node:vm&quot;);\n\n// `contextObject` 选项指定为 undefined，来使全局对象 contextified。\nconst context = vm.createContext();\nconsole.log(vm.runInContext(&quot;globalThis&quot;, context) === context); // false\n// 一个 contextified 的全局对象不能被冻结。\ntry {\n    vm.runInContext(&quot;Object.freeze(globalThis);&quot;, context);\n} catch (e) {\n    console.log(e); // TypeError: Cannot freeze\n}\nconsole.log(vm.runInContext(&quot;globalThis.foo = 1; foo;&quot;, context)); // 1\n</code></pre>\n<p>要创建一个具有普通全局对象的上下文，并在外部上下文中访问一个怪异之处较少的全局代理，可将 <code>contextObject</code> 参数指定为 <code>vm.constants.DONT_CONTEXTIFY</code>。</p>\n<h1>ESModule: <a href=\"https://nodejs.org/docs/latest/api/vm.html#class-vmmodule\">vm.Module</a></h1>\n<ul>\n<li><code>vm.Module</code> 表示 <a href=\"https://262.ecma-international.org/14.0/#sec-abstract-module-records\">Module Record</a></li>\n<li><code>vm.SourceTextModule</code> <a href=\"https://tc39.es/ecma262/#sec-source-text-module-records\">Source Text Module Record</a></li>\n<li><code>vm.SyntheticModule</code> <a href=\"https://heycam.github.io/webidl/#synthetic-module-records\">Synthetic Module Record</a></li>\n</ul>\n<pre><code class=\"language-mermaid\">classDiagram\n    vm.Module &lt;|-- vm.SourceTextModule\n    vm.Module &lt;|-- vm.SyntheticModule\n</code></pre>\n<p>执行流程如下，与 <a href=\"https://tc39.es/ecma262/#sec-cyclic-module-records\">ECMA262</a> 一致。</p>\n<pre><code class=\"language-mermaid\">stateDiagram-v2\n    [*] --&gt; unlinked\n    unlinked --&gt; linking : module.link()\n    linking --&gt; linked : All linker Promises resolved\n    linked --&gt; evaluating : module.evaluate()\n    evaluating --&gt; evaluated : Successfully evaluated\n    evaluating --&gt; errored : Exception thrown\n    evaluated --&gt; [*]\n    errored --&gt; [*]\n</code></pre>\n<blockquote>\n<p>备注：通过 <code>node --experimental-vm-modules --experimental-strip-types demo.ts</code> 运行下面的代码</p>\n</blockquote>\n<pre><code class=\"language-ts\">import vm from &quot;node:vm&quot;;\nimport type { ImportAttributes } from &quot;node:module&quot;;\n\nconst contextifiedObject = vm.createContext({\n    secret: 42,\n    print: console.log,\n});\n\n// Step 1\n//\n// 通过构造一个新的 `vm.SourceTextModule` 对象来创建一个模块。\n// 这会解析提供的源代码文本，如果出现任何问题，则抛出一个 `SyntaxError`。\n// 默认情况下，模块是在顶层上下文中创建的。但在这里，我们指定 `contextifiedObject` 作为此模块所属的上下文。\n//\n// 这里，我们尝试从模块 &quot;foo&quot; 中获取默认导出，并将其放入本地绑定 &quot;secret&quot; 中。\nconst bar: vm.Module = new vm.SourceTextModule(\n    /*js*/ `\n    import s from &#39;foo&#39;;\n    s;\n    print(s);\n    export let abc = import.meta[&#39;abc&#39;]; // -&gt; 123\n    import(&quot;s&quot;)\n`,\n    {\n        // 所有选项都是可选的\n        context: contextifiedObject,\n        identifier: &quot;vm:module(0)&quot;,\n        initializeImportMeta(meta, module: vm.SourceTextModule): void {\n            meta[&quot;abc&quot;] = 123; // &lt;-\n            console.assert(module === bar);\n        },\n        async importModuleDynamically(specifier: string, script, importAttributes): Promise&lt;vm.Module&gt; {\n            console.log(specifier, script, importAttributes);\n            throw new Error(`Dynamic import not supported: ${specifier}`);\n        },\n    },\n);\n// Cyclic Module Record [[Status]]\nconsole.assert(bar.status === &quot;unlinked&quot;);\nconsole.assert(bar.context === contextifiedObject);\nconsole.assert(bar.identifier === &quot;vm:module(0)&quot;);\n// Cyclic Module Record [[RequestedModules]]\nconsole.log(bar.dependencySpecifiers); // =&gt; [&quot;foo&quot;]\n\n// Step 2\n//\n// 将此模块导入的依赖项“链接”到它。\n//\n// 提供的链接回调（“linker”）接受两个参数：父模块（在本例中为 `bar`）和作为导入模块标识符的字符串。\n// 回调应返回与提供的标识符相对应的模块，并满足 `module.link()` 中记录的某些要求。\n//\n// 如果返回的模块尚未开始链接，则将在返回的模块上调用相同的 linker 回调。\n//\n// 即使是没有依赖项的顶层模块也必须显式链接。但是，提供的回调永远不会被调用。\n//\n// link() 方法返回一个 Promise，当 linker 返回的所有 Promise 都解析时，该 Promise 将被解析。\n//\n// 注意：这是一个人为的例子，因为 linker 函数每次调用时都会创建一个新的 &quot;foo&quot; 模块。\n// 在一个成熟的模块系统中，可能会使用缓存来避免重复的模块。\n\n// Cyclic Module Record Link()\nasync function linker(\n    specifier: string,\n    referencingModule: vm.Module,\n    extra: {\n        attributes: ImportAttributes;\n    },\n): Promise&lt;vm.Module&gt; {\n    if (specifier === &quot;foo&quot;) {\n        return new vm.SourceTextModule(\n            /*js*/ `\n            // &quot;secret&quot; 变量指的是我们在创建上下文时添加到 &quot;contextifiedObject&quot; 的全局变量。\n            export default secret;`,\n            // 这里使用 `contextifiedObject` 代替 `referencingModule.context` 也可以。\n            { context: referencingModule.context },\n        );\n    }\n    throw new Error(`Unable to resolve dependency: ${specifier}`);\n}\nawait bar.link(linker);\nconsole.assert(bar.status === &quot;linked&quot;);\n\n// Step 3\n//\n// 评估模块。evaluate() 方法返回一个 promise，该 promise 将在模块完成评估后 resolve。\n\n// 打印 42。\nawait bar.evaluate();\nconsole.assert(bar.status === &quot;evaluated&quot;);\n// GetModuleNamespace 返回 Module Namespace Object：包含所有导出绑定\nconsole.log(bar.namespace); // =&gt; { abc: 123 }\n\nif (bar.status === &quot;errored&quot;) {\n    // Cyclic Module Record [[EvaluationError]]\n    console.error(bar.error);\n}\n</code></pre>\n<p>合成模块（vm.SyntheticModule）的作用是提供一个通用接口，使非 JavaScript 源代码能够暴露于 ECMAScript 模块图中。</p>\n<pre><code class=\"language-ts\">import vm from &quot;node:vm&quot;;\nconst source = `{ &quot;a&quot;: 1 }`;\nconst module = new vm.SyntheticModule([&quot;default&quot;], function (this: vm.SyntheticModule) {\n    const obj = JSON.parse(source);\n    this.setExport(&quot;default&quot;, obj);\n});\n\nawait module.link((specifier, referencingModule, { attributes }) =&gt; {});\nawait module.evaluate();\nconsole.log(module.namespace); // =&gt; [Module: null prototype] { default: { a: 1 } }\n</code></pre>\n<blockquote>\n<p>下面纯翻译 <a href=\"https://github.com/nodejs/node/blob/main/doc/api/vm.md\">https://github.com/nodejs/node/blob/main/doc/api/vm.md</a></p>\n</blockquote>\n<h2>异步任务和 Promise 的超时交互</h2>\n<p><code>Promise</code> 和 <code>async function</code> 可以异步地调度由 JavaScript 引擎运行的任务。默认情况下，这些任务在当前堆栈上的所有 JavaScript 函数执行完毕后运行。这允许绕过 <code>timeout</code> 和 <code>breakOnSigint</code> 选项的功能。</p>\n<p>例如，以下代码由 <code>vm.runInNewContext()</code> 执行，超时时间为 5 毫秒，它调度一个无限循环在 promise resolve 之后运行。 计划的循环永远不会被超时中断：</p>\n<pre><code class=\"language-js\">const vm = require(&quot;node:vm&quot;);\n\nfunction loop() {\n    console.log(&quot;entering loop&quot;);\n    while (1) console.log(Date.now());\n}\n\nvm.runInNewContext(&quot;Promise.resolve().then(() =&gt; loop());&quot;, { loop, console }, { timeout: 5 });\n// 这会在 &#39;entering loop&#39; *之前* 打印 (!)\nconsole.log(&quot;done executing&quot;);\n</code></pre>\n<p>可以通过将 <code>microtaskMode: &#39;afterEvaluate&#39;</code> 传递给创建 <code>Context</code> 的代码来解决此问题：</p>\n<pre><code class=\"language-js\">const vm = require(&quot;node:vm&quot;);\n\nfunction loop() {\n    while (1) console.log(Date.now());\n}\n\nvm.runInNewContext(\n    &quot;Promise.resolve().then(() =&gt; loop());&quot;,\n    { loop, console },\n    { timeout: 5, microtaskMode: &quot;afterEvaluate&quot; },\n);\n</code></pre>\n<p>在这种情况下，通过 <code>promise.then()</code> 调度的微任务将在从 <code>vm.runInNewContext()</code> 返回之前运行，并且将被 <code>timeout</code> 功能中断。 这仅适用于在 <code>vm.Context</code> 中运行的代码，因此例如 <a href=\"https://nodejs.org/docs/latest/api/vm.html#vmruninthiscontextcode-options\"><code>vm.runInThisContext()</code></a> 不采用此选项。</p>\n<p>Promise 回调被输入到创建它们的上下文的微任务队列中。 例如，如果在上面的示例中将 <code>() =&gt; loop()</code> 替换为 <code>loop</code>，则 <code>loop</code> 将被推送到全局微任务队列中，因为它是一个来自外部（主）上下文的函数，因此也能够逃避超时。</p>\n<p>如果异步调度函数（例如 <code>process.nextTick()</code>、<code>queueMicrotask()</code>、<code>setTimeout()</code>、<code>setImmediate()</code> 等）在 <code>vm.Context</code> 中可用，则传递给它们的函数将被添加到全局队列中，这些队列由所有上下文共享。 因此，传递给这些函数的回调也不能通过超时来控制。</p>\n<h2>编译 API 中对动态 <code>import()</code> 的支持</h2>\n<p>以下 API 支持 <code>importModuleDynamically</code> 选项，以在 vm 模块编译的代码中启用动态 <code>import()</code>。</p>\n<ul>\n<li><code>new vm.Script</code></li>\n<li><code>vm.compileFunction()</code></li>\n<li><code>new vm.SourceTextModule</code></li>\n<li><code>vm.runInThisContext()</code></li>\n<li><code>vm.runInContext()</code></li>\n<li><code>vm.runInNewContext()</code></li>\n<li><code>vm.createContext()</code></li>\n</ul>\n<p>此选项仍然是实验性模块 API 的一部分。 我们不建议在生产环境中使用它。</p>\n<h3>当未指定或未定义 <code>importModuleDynamically</code> 选项时</h3>\n<p>如果未指定此选项，或者如果它是 <code>undefined</code>，则包含 <code>import()</code> 的代码仍然可以由 vm API 编译，但是当编译后的代码被执行并且它实际调用 <code>import()</code> 时，结果将拒绝并返回 <a href=\"https://nodejs.org/api/errors.html#err_vm_dynamic_import_callback_missing\"><code>ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING</code></a>。</p>\n<h3>当 <code>importModuleDynamically</code> 为 <code>vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER</code> 时</h3>\n<p><code>vm.SourceTextModule</code> 当前不支持此选项。</p>\n<p>使用此选项，当在编译后的代码中启动 <code>import()</code> 时，Node.js 将使用主上下文中的默认 ESM 加载器来加载请求的模块并将其返回给正在执行的代码。</p>\n<p>这使编译后的代码可以访问 Node.js 内置模块，例如 <code>fs</code> 或 <code>http</code>。 如果代码在不同的上下文中执行，请注意，从主上下文加载的模块创建的对象仍然来自主上下文，而不是新上下文中内置类的 <code>instanceof</code>。</p>\n<pre><code class=\"language-js\">import { Script, constants } from &quot;node:vm&quot;;\n\nconst script = new Script(&#39;import(&quot;node:fs&quot;).then(({readFile}) =&gt; readFile instanceof Function)&#39;, {\n    importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,\n});\n\n// false: 从主上下文加载的 URL 不是新上下文中 Function 类的实例。\nscript.runInNewContext().then(console.log);\n</code></pre>\n<p>此选项还允许脚本或函数加载用户模块：</p>\n<pre><code class=\"language-js\">import { Script, constants } from &quot;node:vm&quot;;\nimport { resolve } from &quot;node:path&quot;;\nimport { writeFileSync } from &quot;node:fs&quot;;\n\n// 将 test.js 和 test.txt 写入当前脚本正在运行的目录。\nwriteFileSync(resolve(import.meta.dirname, &quot;test.mjs&quot;), &#39;export const filename = &quot;./test.json&quot;;&#39;);\nwriteFileSync(resolve(import.meta.dirname, &quot;test.json&quot;), &#39;{&quot;hello&quot;: &quot;world&quot;}&#39;);\n\n// 编译一个加载 test.mjs 然后加载 test.json 的脚本\n// 就像脚本放置在同一目录中一样。\nconst script = new Script(\n    `(async function() {\n    const { filename } = await import(&#39;./test.mjs&#39;);\n    return import(filename, { with: { type: &#39;json&#39; } })\n  })();`,\n    {\n        filename: resolve(import.meta.dirname, &quot;test-with-default.js&quot;),\n        importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,\n    },\n);\n\n// { default: { hello: &#39;world&#39; } }\nscript.runInThisContext().then(console.log);\n</code></pre>\n<p>使用主上下文中的默认加载器加载用户模块存在一些注意事项：</p>\n<ol>\n<li>要解析的模块将相对于传递给 <code>vm.Script</code> 或 <code>vm.compileFunction()</code> 的 <code>filename</code> 选项。 该解析可以使用 <code>filename</code>，该 <code>filename</code> 是绝对路径或 URL 字符串。 如果 <code>filename</code> 是既不是绝对路径也不是 URL 的字符串，或者如果它是未定义的，则解析将相对于进程的当前工作目录。 在 <code>vm.createContext()</code> 的情况下，解析始终相对于当前工作目录，因为此选项仅在没有引用脚本或模块时使用。</li>\n<li>对于解析为特定路径的任何给定 <code>filename</code>，一旦进程设法从该路径加载特定模块，结果可能会被缓存，并且随后从同一路径加载同一模块将返回相同的内容。 如果 <code>filename</code> 是 URL 字符串，则如果它具有不同的搜索参数，则不会命中缓存。 对于不是 URL 字符串的 <code>filename</code>，目前没有办法绕过缓存行为。</li>\n</ol>\n<h3>当 <code>importModuleDynamically</code> 是一个函数时</h3>\n<p>当 <code>importModuleDynamically</code> 是一个函数时，当在编译后的代码中调用 <code>import()</code> 时，将调用它，以便用户可以自定义应如何编译和评估请求的模块。 目前，必须使用 <code>--experimental-vm-modules</code> 标志启动 Node.js 实例，此选项才能工作。 如果未设置该标志，则将忽略此回调。 如果评估的代码实际调用 <code>import()</code>，则结果将拒绝并返回 <a href=\"https://nodejs.org/api/errors.html#err_vm_dynamic_import_callback_missing_flag\"><code>ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG</code></a>。</p>\n<p>回调 <code>importModuleDynamically(specifier, referrer, importAttributes)</code> 具有以下签名：</p>\n<ul>\n<li><code>specifier</code> {string} 传递给 <code>import()</code> 的说明符</li>\n<li><code>referrer</code> {vm.Script|Function|vm.SourceTextModule|Object}\n对于 <code>new vm.Script</code>、<code>vm.runInThisContext</code>、<code>vm.runInContext</code> 和 <code>vm.runInNewContext</code>，referrer 是编译后的 <code>vm.Script</code>。 对于 <code>vm.compileFunction</code>，它是编译后的 <code>Function</code>，对于 <code>new vm.SourceTextModule</code>，它是编译后的 <code>vm.SourceTextModule</code>，对于 <code>vm.createContext()</code>，它是上下文 <code>Object</code>。</li>\n<li><code>importAttributes</code> {Object} 传递给 [<code>optionsExpression</code>][<a href=\"https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call%5D\">https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call]</a> 可选参数的 <code>&quot;with&quot;</code> 值，如果没有提供值，则为空对象。</li>\n<li><code>phase</code> {string} 动态导入的阶段（<code>&quot;source&quot;</code> 或 <code>&quot;evaluation&quot;</code>）。</li>\n<li>返回值：{Module Namespace Object|vm.Module} 建议返回 <code>vm.Module</code>，以便利用错误跟踪，并避免包含 <code>then</code> 函数导出的命名空间的问题。</li>\n</ul>\n<pre><code class=\"language-js\">import { Script, SyntheticModule } from &quot;node:vm&quot;;\n\nconst script = new Script(&#39;import(&quot;foo.json&quot;, { with: { type: &quot;json&quot; } })&#39;, {\n    async importModuleDynamically(specifier, referrer, importAttributes) {\n        console.log(specifier); // &#39;foo.json&#39;\n        console.log(referrer); // 编译后的脚本\n        console.log(importAttributes); // { type: &#39;json&#39; }\n        const m = new SyntheticModule([&quot;bar&quot;], () =&gt; {});\n        await m.link(() =&gt; {});\n        m.setExport(&quot;bar&quot;, { hello: &quot;world&quot; });\n        return m;\n    },\n});\nconst result = await script.runInThisContext();\nconsole.log(result); //  { bar: { hello: &#39;world&#39; } }\n</code></pre>\n","tags":["node.js"]},{"id":"ts-declare","url":"https://yieldray.fun/posts/ts-declare","title":"理解TypeScript之declare","date_published":"2025-05-02T15:35:04.000Z","date_modified":"2025-05-02T15:35:04.000Z","content_text":"<p>不妨从 <code>@type/node</code> 定义入手。</p>\n<p>观察 <code>@type/node</code> 包的 <code>package.json</code>，可以发现其入口文件为 <a href=\"https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/index.d.ts\">index.d.ts</a>。</p>\n<pre><code class=\"language-json\">{\n    &quot;name&quot;: &quot;@types/node&quot;,\n    &quot;types&quot;: &quot;index&quot;\n}\n</code></pre>\n<p>入口文件的部分如下：</p>\n<pre><code class=\"language-ts\">/// &lt;reference lib=&quot;es2020&quot; /&gt;\n/// &lt;reference path=&quot;globals.d.ts&quot; /&gt;\n/// &lt;reference path=&quot;fs.d.ts&quot; /&gt;\n/// &lt;reference path=&quot;fs/promises.d.ts&quot; /&gt;\n/// &lt;reference path=&quot;process.d.ts&quot; /&gt;\n</code></pre>\n<p>显然，<code>reference path</code> 语法的作用是从指定文件模块中引入类型。<br><code>reference lib</code> 则是引入内置库。</p>\n<blockquote>\n<p>注：使用 <code>/// &lt;reference types=&quot;vite/client&quot; /&gt;</code> 语法则可以从指定包中引入类型。</p>\n</blockquote>\n<p>内置库的特点就是其定义的类型会变成 <code>globalThis</code> 全局模块中的类型，细节这里先按下不表。</p>\n<pre><code class=\"language-ts\">/// &lt;reference no-default-lib=&quot;true&quot;/&gt;\n\n/// &lt;reference lib=&quot;es2019&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.bigint&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.date&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.number&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.promise&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.sharedmemory&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.string&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.symbol.wellknown&quot; /&gt;\n/// &lt;reference lib=&quot;es2020.intl&quot; /&gt;\n</code></pre>\n<pre><code class=\"language-ts\">/// &lt;reference no-default-lib=&quot;true&quot;/&gt;\n\n/// &lt;reference lib=&quot;es2020&quot; /&gt;\n/// &lt;reference lib=&quot;dom&quot; /&gt;\n/// &lt;reference lib=&quot;webworker.importscripts&quot; /&gt;\n/// &lt;reference lib=&quot;scripthost&quot; /&gt;\n/// &lt;reference lib=&quot;dom.iterable&quot; /&gt;\n/// &lt;reference lib=&quot;dom.asynciterable&quot; /&gt;\n</code></pre>\n<pre><code class=\"language-ts\">/// &lt;reference no-default-lib=&quot;true&quot;/&gt;\n\ninterface Window {\n    readonly window: Window &amp; typeof globalThis;\n}\n\ndeclare var Window: {\n    prototype: Window;\n    new (): Window;\n};\n\ndeclare var window: Window &amp; typeof globalThis;\n\ntype WindowProxy = Window;\n</code></pre>\n<hr>\n<p><code>@types/node</code> 定义全局空间的方法如下：</p>\n<pre><code class=\"language-ts\">export {}; // Make this a module\ndeclare global {\n    var global: typeof globalThis;\n    var process: NodeJS.Process;\n    var console: Console;\n}\n</code></pre>\n<p><code>@types/node</code> 也定义了各个内置模块：</p>\n<pre><code class=\"language-ts\">declare module &quot;fs&quot; {\n    import { URL } from &quot;node:url&quot;;\n    export type PathLike = string | Buffer | URL;\n    import * as promises from &quot;node:fs/promises&quot;;\n    export { promises };\n}\ndeclare module &quot;node:fs&quot; {\n    export * from &quot;fs&quot;;\n}\n</code></pre>\n<pre><code class=\"language-ts\">declare module &quot;fs/promises&quot; {}\n</code></pre>\n<pre><code class=\"language-ts\">declare module &quot;process&quot; {\n    global {\n        var process: NodeJS.Process;\n        namespace NodeJS {\n            interface Process extends EventEmitter {}\n        }\n    }\n    export = process;\n    // 过时语法，等价于\n    // export default process\n}\ndeclare module &quot;node:process&quot; {\n    import process = require(&quot;process&quot;);\n    export = process;\n    // 过时语法，等价于\n    // export * from &quot;process&quot;\n}\n</code></pre>\n<hr>\n<p><code>declare module</code> 语法仅能在 <code>.d.ts</code> 文件中使用，不能在 <code>.ts</code> 文件中使用：</p>\n<pre><code class=\"language-ts\">declare module &quot;module-name&quot; {\n    global {\n        interface PromiseConstructor {\n            deferred&lt;T&gt;(): PromiseWithResolvers&lt;T&gt;;\n        }\n    }\n}\n</code></pre>\n<p>下面的语法可以在 <code>.d.ts</code> 中使用，也可以在 <code>.ts</code> 文件中使用：</p>\n<pre><code class=\"language-ts\">declare global {\n    interface PromiseConstructor {\n        deferred&lt;T&gt;(): PromiseWithResolvers&lt;T&gt;;\n    }\n}\nexport {}; // This is required to make this file a module\n</code></pre>\n<hr>\n<p>注意到 <code>typeof globalThis</code> 等价于 <code>module globalThis</code>。<br>如果我们手动定义 <code>globalThis</code> 模块，实际上是定义 <code>module &quot;globalThis&quot;</code> 模块，<br>而非全局空间 <code>module globalThis</code> 模块（注意多余的引号）。</p>\n<pre><code class=\"language-ts\">declare module &quot;globalThis&quot; {}\n</code></pre>\n<pre><code class=\"language-ts\">import globalThis2 from &quot;globalThis&quot;;\ntype UserDefined = typeof globalThis2; // (alias) module &quot;globalThis&quot;\ntype BuiltIn = typeof globalThis; // module globalThis\n</code></pre>\n<p>直接通过 <code>interface</code> 关键字定义的接口不会与全局空间的同名接口合并（而是表现为覆盖同名全局标识符）。</p>\n<pre><code class=\"language-ts\">type SimpleEqual&lt;X, Y&gt; = [X, Y] extends [Y, X] ? true : false;\n\n// prettier-ignore\ninterface ObjectConstructor { // [!code ++]\n    foo: &quot;bar&quot;;               // [!code ++]\n} // [!code ++]\n\ntype Test = SimpleEqual&lt;ObjectConstructor, { foo: &quot;bar&quot; }&gt;; // false // [!code --]\ntype Test = SimpleEqual&lt;ObjectConstructor, { foo: &quot;bar&quot; }&gt;; // true  // [!code ++]\n</code></pre>\n<p>因此可以简单的认为，<code>declare</code> 关键字的作用就是用于向全局空间添加类型。</p>\n<p>然而，<code>declare</code> 与 <code>declare global</code> 是不等价的！<br>简单来说：</p>\n<ul>\n<li><code>declare</code> 的语义是向当前模块空间内添加变量类型</li>\n<li><code>declare global</code> 的语义是向 <code>module globalThis</code> 模块中添加变量类型</li>\n</ul>\n<p>我们看看下面的情况：</p>\n<pre><code class=\"language-ts\">// 这里无论有没有 declare 效果相同\n/** declare */ interface PromiseConstructor {\n    deferred&lt;T&gt;(): PromiseWithResolvers&lt;T&gt;;\n}\ntype PromiseDeferred = PromiseConstructor[&quot;deferred&quot;]; // OK\nconst promiseDeferred = Promise.deferred; // Property &#39;deferred&#39; does not exist on type &#39;PromiseConstructor&#39;.\ntype PromiseAll = PromiseConstructor[&quot;all&quot;]; // Property &#39;all&#39; does not exist on type &#39;PromiseConstructor&#39;.\nconst promiseAll = Promise.all; // OK\n</code></pre>\n<p>上述情况容易理解，因为 <code>Promise</code> 的类型为 <code>globalThis.PromiseConstructor</code>，而非我们定义的 <code>interface PromiseConstructor</code>。<br>因此，要修改 <code>globalThis.PromiseConstructor</code>，需使用 <code>declare global</code> 语法</p>\n<pre><code class=\"language-ts\">declare global {\n    interface PromiseConstructor {\n        deferred&lt;T&gt;(): PromiseWithResolvers&lt;T&gt;;\n    }\n}\ntype PromiseDeferred = PromiseConstructor[&quot;deferred&quot;]; // OK\nconst promiseDeferred = Promise.deferred; // OK\n</code></pre>\n<p>要说明 <code>declare</code> 与 <code>declare global</code> 的区别，给出如下示例：</p>\n<blockquote>\n<p>注意 TypeScript 的这些概念与目标模块环境（compilerOptions.module）相关，本文模块环境统一为 ES 模块</p>\n</blockquote>\n<pre><code class=\"language-ts\">declare const declareVar: &quot;declareVar&quot;;\ndeclare function declareFunction(): &quot;declareFunction&quot;;\ndeclare class DeclareClass {\n    static declareClass: &quot;declareClass&quot;;\n}\ndeclare global {\n    const declareGlobalVar: &quot;declareGlobalVar&quot;;\n    function declareGlobalFunction(): &quot;declareGlobalFunction&quot;;\n    class DeclareGlobalClass {\n        static declareGlobalClass: &quot;declareGlobalClass&quot;;\n    }\n}\n\ntype A = Equal&lt;typeof declareVar, &quot;declareVar&quot;&gt;;\ntype B = Equal&lt;typeof declareFunction, () =&gt; &quot;declareFunction&quot;&gt;;\ntype C = Equal&lt;typeof DeclareClass.declareClass, &quot;declareClass&quot;&gt;;\n\ntype D = Equal&lt;typeof declareGlobalVar, &quot;declareGlobalVar&quot;&gt;;\ntype E = Equal&lt;typeof declareGlobalFunction, () =&gt; &quot;declareGlobalFunction&quot;&gt;;\ntype F = Equal&lt;typeof DeclareGlobalClass.declareGlobalClass, &quot;declareGlobalClass&quot;&gt;;\n\ntype _ = Equal&lt;typeof globalThis.foobar, any&gt;; // 类型为 any，不会报错\ntype G = Equal&lt;typeof globalThis.declareVar, any&gt;; // 类型为 any，而不是 &quot;declareVar&quot;\ntype H = Equal&lt;typeof globalThis.declareFunction, any&gt;; // 类型为 any，而不是 () =&gt; &quot;declareFunction&quot;\ntype I = Equal&lt;typeof globalThis.DeclareClass.declareClass, any&gt;; // 类型为 any，而不是 () =&gt; &quot;declareFunction&quot;\n// @ts-expect-error: Property &#39;declareGlobalVar&#39; does not exist on type &#39;typeof globalThis&#39;.\ntype J = Equal&lt;typeof globalThis.declareGlobalVar, &quot;declareGlobalVar&quot;&gt;; // 报错\ntype K = Equal&lt;typeof globalThis.declareGlobalFunction, () =&gt; &quot;declareGlobalFunction&quot;&gt;;\n//@ts-expect-error: Property &#39;DeclareGlobalClass&#39; does not exist on type &#39;typeof globalThis&#39;.\ntype L = Equal&lt;typeof globalThis.DeclareGlobalClass.declareGlobalClass, &quot;declareGlobalClass&quot;&gt;; // 报错\n\ntype Equal&lt;X, Y&gt; = (&lt;T&gt;() =&gt; T extends X ? 1 : 2) extends &lt;T&gt;() =&gt; T extends Y ? 1 : 2 ? true : false;\n</code></pre>\n<p>观察上例，我们可以得到一个不符合直觉（但确实是事实）的结论：<br><code>globalThis</code> 变量上访问任意属性，得到的类型为 <code>any</code>，但使用 <code>declare global</code> 定义类型后，该属性则无法找到（报错），然而 <code>declare global function</code> 是一个特例。</p>\n","tags":["typescript"]},{"id":"understanding-mcp","url":"https://yieldray.fun/posts/understanding-mcp","title":"理解MCP","date_published":"2025-04-13T20:30:00.000Z","date_modified":"2025-04-13T20:30:00.000Z","content_text":"<h1>前置：LLMs 接口抽象</h1>\n<pre><code class=\"language-ts\">interface Message {\n    role: &quot;system&quot; | &quot;user&quot; | &quot;assistant&quot;;\n    content: string;\n}\n\ninterface Context {\n    messages: Message[];\n}\n\ndeclare function chatCompletion(context: Context): string;\n</code></pre>\n<h1>理解 RAG</h1>\n<p>RAG 和 Function Call 都是（在无需重新训练模型的情况下）实现 Agent (智能体) 的方式。</p>\n<p>由于 LLM 主要依赖其训练数据来生成回答。这会导致：</p>\n<ul>\n<li>知识截止 (Knowledge Cutoff): LLM 的知识停留在其训练数据最后更新的时间点，无法了解最新事件或信息。</li>\n<li>幻觉 (Hallucination): LLM 可能编造听起来合理但实际上错误的信息，尤其是在其知识库之外或不确定的领域。</li>\n<li>领域特定知识缺乏: 对于非常专业或私有的领域（如公司内部文档），通用 LLM 缺乏深入了解。</li>\n<li>缺乏溯源性: LLM 通常不会说明信息来源，难以核实。</li>\n<li>等等问题。</li>\n</ul>\n<p><a href=\"https://cloud.google.com/use-cases/retrieval-augmented-generation\">RAG</a>（Retrieval-Augmented Generation）做的工作显然是通过知识库（Knowledge Base）插入额外的上下文。它做的工作可以抽象为：</p>\n<pre><code class=\"language-ts\">// 通过知识库，增强给定上下文\ndeclare function rag(db: Database, context: Context): Context;\n\ninterface Database {\n    // ?\n}\n</code></pre>\n<p>问题其实在于 RAG 是如何插入文本的？关键字是<strong>向量嵌入</strong> (Vector Embeddings) 。</p>\n<p>将文本转换为向量，这就将文本相似度问题转换为向量相似度问题。<br>根据下面的演示代码可以发现，关键的<strong>文本转换为向量</strong>这一过程也是由 LLM 完成的。</p>\n<pre><code class=\"language-ts\">import OpenAI from &quot;openai&quot;;\nconst openai = new OpenAI();\nconst EMBEDDING_MODEL = &quot;text-embedding-3-small&quot;; // 选择嵌入模型\nconst KNOWLEDGE_BASE = [\n    &quot;RAG（Retrieval-Augmented Generation）结合了检索系统和生成模型，以提高回答的准确性和时效性。&quot;,\n    &quot;微调（Fine-tuning）是指在一个预训练模型的基础上，使用特定任务的数据继续训练模型，使其更适应特定领域。&quot;,\n    &quot;向量数据库（Vector Database）专门用于存储和高效查询高维向量数据，常用于相似性搜索。&quot;,\n    &quot;在RAG流程中，首先根据用户查询从知识库检索相关文档片段，然后将这些片段作为上下文提供给LLM生成最终答案。&quot;,\n    &quot;与微调相比，RAG的主要优势在于能够轻松更新知识库以反映最新信息，且无需重新训练大模型。&quot;,\n    &quot;常见的向量相似度计算方法包括余弦相似度、点积和欧氏距离。&quot;,\n];\nconst TOP_K = 2; // 我们想要检索的最相关的 K 个文档块\n\n// 计算余弦相似度\nfunction cosineSimilarity(vecA: number[], vecB: number[]) {\n    let dotProduct = 0;\n    let magnitudeA = 0;\n    let magnitudeB = 0;\n    for (let i = 0; i &lt; vecA.length; i++) {\n        dotProduct += vecA[i] * vecB[i];\n        magnitudeA += vecA[i] * vecA[i];\n        magnitudeB += vecB[i] * vecB[i];\n    }\n    magnitudeA = Math.sqrt(magnitudeA);\n    magnitudeB = Math.sqrt(magnitudeB);\n    if (magnitudeA === 0 || magnitudeB === 0) return 0;\n    else return dotProduct / (magnitudeA * magnitudeB);\n}\n\n// 获取文本嵌入\nasync function getEmbedding(text: string) {\n    const response = await openai.embeddings.create({\n        model: EMBEDDING_MODEL,\n        input: text,\n    });\n    return response.data[0].embedding;\n}\n\nasync function ragExample(userQuery: string) {\n    // 在实际应用中，这一步通常是预处理并存储在向量数据库中\n    const knowledgeBaseEmbeddings = await Promise.all(\n        KNOWLEDGE_BASE.map(async (text) =&gt; {\n            const embedding = await getEmbedding(text); // 生成嵌入向量\n            return { text, embedding };\n        }),\n    );\n\n    const userQuery = &quot;RAG 和微调有什么不同？&quot;;\n    const queryEmbedding = await getEmbedding(userQuery);\n\n    //  计算查询向量与知识库向量的相似度\n    const similarities = knowledgeBaseEmbeddings.map((kbItem) =&gt; ({\n        text: kbItem.text,\n        similarity: cosineSimilarity(queryEmbedding, kbItem.embedding),\n    }));\n\n    // 按相似度降序排序\n    similarities.sort((a, b) =&gt; b.similarity - a.similarity);\n\n    // 获取 Top-K 结果\n    const relevantChunks = similarities.slice(0, TOP_K);\n\n    // 返回最相关的文档片段\n    return relevantChunks.map((item) =&gt; item.text).join(&quot;\\n&quot;);\n}\n</code></pre>\n<p>生产环境下往往可以考虑使用向量数据库优化索引和检索。这些基础设施不在本文考虑的范围之内。</p>\n<h1>理解 Function Calling</h1>\n<p>Function Calling 允许 LLM 调用外部函数。这使得 LLM 不仅可以生成文本，还可以与外部世界交互，执行特定任务。</p>\n<p>可以抽象为：</p>\n<pre><code class=\"language-ts\">interface FunctionDefinition {\n    name: string;\n    description: string;\n    parameters: {\n        type: &quot;object&quot;;\n        properties: { [name: string]: ParameterDefinition };\n        required: string[];\n    };\n}\n\ninterface ParameterDefinition {\n    type: &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot; | &quot;array&quot; | &quot;object&quot;;\n    description: string;\n    enum?: string[];\n}\n\ninterface FunctionCallContext extends Context {\n    availableFunctions?: { [name: string]: (...args: any[]) =&gt; any };\n    type?: &quot;function&quot;;\n    function_call?: {\n        name: string;\n        arguments: string; // JSON string\n    };\n}\n\ndeclare function chatCompletionWithFunctionCall(context: FunctionCallContext): FunctionCallContext;\n</code></pre>\n<p><a href=\"https://platform.openai.com/docs/guides/function-calling?api-mode=responses&lang=javascript\">OpenAI 的 Function calling 机制</a>很显然需要 LLM 的 API\n支持。</p>\n<pre><code class=\"language-ts\">import { OpenAI } from &quot;openai&quot;;\n\nconst openai = new OpenAI();\nconst model = &quot;gpt-4.1&quot;;\n\nconst tools = [\n    {\n        type: &quot;function&quot;,\n        function: {\n            name: getWeather.name,\n            description: &quot;Get current temperature for provided coordinates in celsius.&quot;,\n            parameters: {\n                type: &quot;object&quot;,\n                properties: {\n                    latitude: { type: &quot;number&quot; },\n                    longitude: { type: &quot;number&quot; },\n                },\n                required: [&quot;latitude&quot;, &quot;longitude&quot;],\n                additionalProperties: false,\n            },\n            strict: true,\n        },\n    },\n];\n\nasync function getWeather(latitude: number, longitude: number) {\n    const response = await fetch(\n        `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&amp;longitude=${longitude}&amp;current=temperature_2m,wind_speed_10m&amp;hourly=temperature_2m,relative_humidity_2m,wind_speed_10m`,\n    );\n    const data = await response.json();\n    return data.current.temperature_2m;\n}\n\nconst messages = [\n    {\n        role: &quot;user&quot;,\n        content: &quot;What&#39;s the weather like in Paris today?&quot;,\n    },\n];\n\nconst completion = await openai.chat.completions.create({\n    model,\n    messages,\n    tools,\n    store: true,\n});\nmessages.push(completion.choices[0].message);\n\nconst toolCall = completion.choices[0].message.tool_calls[0];\n/*{\n    &quot;id&quot;: &quot;call_12345xyz&quot;,\n    &quot;type&quot;: &quot;function&quot;,\n    &quot;function&quot;: {\n      &quot;name&quot;: &quot;getWeather&quot;,\n      &quot;arguments&quot;: &quot;{\\&quot;latitude\\&quot;:48.8566,\\&quot;longitude\\&quot;:2.3522}&quot;\n    }\n}*/\nconst args = JSON.parse(toolCall.function.arguments);\nconst result = await getWeather(args.latitude, args.longitude);\nmessages.push({\n    role: &quot;tool&quot;,\n    tool_call_id: toolCall.id,\n    content: result.toString(),\n});\n\nconst completion2 = await openai.chat.completions.create({\n    model,\n    messages,\n    tools,\n    store: true,\n});\n\nconsole.log(completion2.choices[0].message.content);\n// -&gt; &quot;The current temperature in Paris is 14°C (57.2°F).&quot;\n</code></pre>\n<p>显然，为了执行 Function Calling，必须额外调用一次 API。</p>\n<h1>MCP</h1>\n<p><a href=\"https://modelcontextprotocol.io\">Model Context Protocol (MCP)</a> 是 anthropic 开源的协议。它是 客户端-主机-服务器架构。</p>\n<p>每个主机可运行多个客户端实例。</p>\n<p>服务器为客户端提供：</p>\n<ul>\n<li>资源：上下文和数据，供用户或 AI 模型使用（例如：访问文件系统）</li>\n<li>提示词：为用户提供模板消息和工作流（例如：返回生成单元测试的提示词）</li>\n<li>工具：AI 模型执行的函数（例如：返回获取今天天气的函数）</li>\n</ul>\n<p>客户端可向服务器提供：</p>\n<ul>\n<li>取样：服务器启动的代理行为和递归 LLM 交互</li>\n</ul>\n<blockquote>\n<p>取样（Sampling）指客户端有能力与 AI 模型交互，而服务端请求在客户端上执行 AI 功能。（AI 模型由客户端维护，服务端无需了解 AI 模型）</p>\n</blockquote>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant Server\n    participant Client\n    participant User\n    participant LLM\n\n    Note over Server,Client: 服务器发起采样\n    Server-&gt;&gt;Client: sampling/createMessage\n\n    Note over Client,User: 人工参与循环审查\n    Client-&gt;&gt;User: 呈现请求以供批准\n    User--&gt;&gt;Client: 审查并批准/修改\n\n    Note over Client,LLM: 模型交互\n    Client-&gt;&gt;LLM: 转发已批准的请求\n    LLM--&gt;&gt;Client: 返回生成结果\n\n    Note over Client,User: 响应审查\n    Client-&gt;&gt;User: 呈现响应以供批准\n    User--&gt;&gt;Client: 审查并批准/修改\n\n    Note over Server,Client: 完成请求\n    Client--&gt;&gt;Server: 返回已批准的响应\n</code></pre>\n<p>MCP 以 JSON-RPC 为消息格式，以 stdio（此时主机上同时运行客户端和服务器）或流式 HTTP（通用）通信，<br>提供了一个有状态会话协议，重点关注客户端和服务器之间的上下文交换和采样协商。</p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant Host\n    participant Client\n    participant Server\n\n    Host-&gt;&gt;+Client: 初始化客户端\n    Client-&gt;&gt;+Server: 使用指定功能初始化会话\n    Server--&gt;&gt;Client: 响应支持的功能\n\n    Note over Host,Server: 具有协商功能的活动会话\n\n    loop 客户端请求\n        Host-&gt;&gt;Client: 用户或模型发起的动作\n        Client-&gt;&gt;Server: 请求 (工具/资源)\n        Server--&gt;&gt;Client: 响应\n        Client--&gt;&gt;Host: 更新UI或响应模型\n    end\n\n    loop 服务器请求\n        Server-&gt;&gt;Client: 请求 (采样)\n        Client-&gt;&gt;Host: 转发给AI\n        Host--&gt;&gt;Client: AI响应\n        Client--&gt;&gt;Server: 响应\n    end\n\n    loop 通知\n        Server--)Client: 资源更新\n        Client--)Server: 状态改变\n    end\n\n    Host-&gt;&gt;Client: 终止\n    Client-&gt;&gt;-Server: 结束会话\n    deactivate Server\n</code></pre>\n<p>使用 stdio 通信时，MCP 服务器进程从 stdin 读取 JSON-RPC 消息，响应消息到 stdout。消息以换行符分隔。</p>\n<blockquote>\n<p>此时主机上同时运行客户端和服务器，无需身份验证。</p>\n</blockquote>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant Client\n    participant Server Process\n\n    Client-&gt;&gt;+Server Process: 启动子进程\n    loop 消息交换\n        Client-&gt;&gt;Server Process: 写入 stdin\n        Server Process-&gt;&gt;Client: 写入 stdout\n        Server Process--)Client: stderr 上的可选日志\n    end\n    Client-&gt;&gt;Server Process: 关闭 stdin，终止子进程\n    deactivate Server Process\n</code></pre>\n<p>使用流式 HTTP 通信时，客户端与服务器之间建立有状态的 SSE 连接。</p>\n<blockquote>\n<p>使用 OAuth 2.0 鉴权，具体见<a href=\"https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization\">specification</a></p>\n</blockquote>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant Client\n    participant Server\n\n    note over Client, Server: 初始化\n\n    Client-&gt;&gt;+Server: POST InitializeRequest\n    Server-&gt;&gt;-Client: InitializeResponse&lt;br&gt;Mcp-Session-Id: 1868a90c...\n\n    Client-&gt;&gt;+Server: POST InitializedNotification&lt;br&gt;Mcp-Session-Id: 1868a90c...\n    Server-&gt;&gt;-Client: 202 Accepted\n\n    note over Client, Server: 客户端请求\n    Client-&gt;&gt;+Server: POST ... 请求 ...&lt;br&gt;Mcp-Session-Id: 1868a90c...\n\n    alt 单个 HTTP 响应\n      Server-&gt;&gt;Client: ... 响应 ...\n    else 服务器打开 SSE 流\n      loop 当连接保持打开时\n          Server-)Client: ... 来自服务器的 SSE 消息 ...\n      end\n      Server-)Client: SSE event: ... 响应 ...\n    end\n    deactivate Server\n\n    note over Client, Server: 客户端通知/响应\n    Client-&gt;&gt;+Server: POST ... 通知/响应 ...&lt;br&gt;Mcp-Session-Id: 1868a90c...\n    Server-&gt;&gt;-Client: 202 Accepted\n\n    note over Client, Server: 服务器请求\n    Client-&gt;&gt;+Server: GET&lt;br&gt;Mcp-Session-Id: 1868a90c...\n    loop 当连接保持打开时\n        Server-)Client: ... 来自服务器的 SSE 消息 ...\n    end\n    deactivate Server\n</code></pre>\n<p>了解规范后容易发现，MCP 规范不处理客户端与 AI 模型的交互。例如，MCP 也提供函数调用功能，但客户端 AI 模型如何调用函数则需要自己实现。</p>\n<p>关于具体代码则参见 <a href=\"https://github.com/modelcontextprotocol/typescript-sdk\">MCP SDK</a>。</p>\n","tags":["ai"]},{"id":"type-vs-interface","url":"https://yieldray.fun/posts/type-vs-interface","title":"Type vs interface","date_published":"2025-03-31T14:27:21.000Z","date_modified":"2025-03-31T14:27:21.000Z","content_text":"<h1>类型理论</h1>\n<p>接口本质是<a href=\"https://zh.wikipedia.org/wiki/%E7%B1%BB%E5%9E%8B%E8%AE%BA\">类型</a>的一种，这就像原始类型、对象类型、联合类型、交叉类型、条件类型也属于类型一样。</p>\n<p>接口，是面向对象观点的一种类型，它本质上还是在表达对象类型。从类型论的观点看，它们应该可以是等价的。</p>\n<p>我们知道：接口的特性是继承，而继承可以通过交叉对象类型来表达。</p>\n<p>总之，讨论这之间的区别实际上是讨论两个问题：</p>\n<ul>\n<li>interface type 关键字的区别</li>\n<li>接口 extends 与交叉对象类型的区别</li>\n</ul>\n<h1>接口特性</h1>\n<p>接口具有<a href=\"https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces\">声明合并</a>的特性。允许在任意处声明同名接口，使用时使用的是合并后的结果。<br>这个性质实际上很关键，正因为要实现此特性：类型具有隐式索引签名，而接口类型没有。</p>\n<pre><code class=\"language-ts\">interface Interface {\n    k1: string;\n    k2: number;\n}\n\nconst obj1: Interface = {\n    k1: &quot;hello&quot;,\n    k2: 42,\n    k3: true,\n};\n\nconst obj2: Obj = {\n    k1: &quot;hello&quot;,\n    k2: 42,\n    k3: true,\n};\n\ninterface Interface {\n    k3: boolean;\n}\n\ntype Obj = {\n    [key: string]: string | number | boolean;\n};\n\n// Type &#39;Interface&#39; is not assignable to type &#39;Obj&#39;.\n//   Index signature for type &#39;string&#39; is missing in type &#39;Interface&#39;. ts(2322)\nconst obj3: Obj = obj1; // 💥\n\n// interface Interface {\n//     [key: string]: string | number | boolean;\n// }\n</code></pre>\n<p>根据上面的例子可知，由于接口 Interface 可在任意时刻增加属性，它不具有隐式的\n<code>{[key: string]: string | number | boolean}</code> 签名。因此解决方法是像最后的注释代码一样，手动添加索引约束。</p>\n<h1>interface type 关键字的区别</h1>\n<p>type 关键字用于定义任意类型，interface 关键字<strong>专门</strong>用于表达对象类型。<br>interface 可通过 extends 表示对象类型的扩展关系。显然从类型本身的角度来说只有交叉关系，没有继承和扩展关系。这是面向对象的观点。</p>\n<p>注意： 从 TS 2.1 起，类可以 implements 接口类型和对象类型。此前只能 implements 接口类型。<br>由于现在使用的版本远超过 2.1，因此现在类不能 implements 对象类型的说法是错误的。<br>同理，接口也可以同时 extends 接口和对象类型。</p>\n<h1>接口 extends 与交叉对象类型的区别</h1>\n<p>直观上来看，我们期望下面的方式表达同样的类型：</p>\n<pre><code class=\"language-ts\">interface Interface$Partial {\n    k1: string;\n    k2: number;\n}\n\ninterface Interface extends Interface$Partial {\n    k3: boolean;\n}\n\ntype Obj = {\n    k1: string;\n    k2: number;\n    k3: boolean;\n};\n\ntype Intersection = {\n    k1: string;\n    k2: number;\n} &amp; {\n    k3: boolean;\n};\n</code></pre>\n<p>如果做过 <a href=\"https://github.com/type-challenges/type-challenges/blob/main/questions/02757-medium-partialbykeys/README.md\">type-challenges</a> <a href=\"https://typehero.dev/challenge/partialbykeys\">2757 PartialByKeys</a>，就会发现，对象类型的交叉类型不会直接被类型系统认为是对象类型。</p>\n<pre><code class=\"language-ts\">type PartialByKeys&lt;T, K = any&gt; = {\n    [P in keyof T as P extends K ? P : never]?: T[P];\n} &amp; {\n    [P in keyof T as P extends K ? never : P]: T[P];\n}; // 💥\n\ntype IntersectionToObj&lt;T&gt; = { [K in keyof T]: T[K] };\ntype PartialByKeys&lt;T, K = any&gt; = IntersectionToObj&lt;\n    {\n        [P in keyof T as P extends K ? P : never]?: T[P];\n    } &amp; {\n        [P in keyof T as P extends K ? never : P]: T[P];\n    }\n&gt;; // ✅\n</code></pre>\n<p>因此解法是手动告知类型系统，将交叉类型转换为对象类型，如上面的解法。<br>不过，这是类型系统类型层面的问题，在实例化时实际上是没有区别的。</p>\n<hr>\n<p>此外，当然接口还受到上面提到的 没有隐式索引类型 的限制：</p>\n<pre><code class=\"language-ts\">const x: Interface =\n    // 💥 可以是 Obj 和 Intersection 类型，但不能是 Interface 类型\n    {\n        k1: &quot;string&quot;,\n        k2: 123,\n        k3: true,\n    };\n\nconst _: Record&lt;string, string | number | boolean&gt; = x;\n</code></pre>\n<h1>另见</h1>\n<p><a href=\"https://www.totaltypescript.com/type-vs-interface-which-should-you-use\">https://www.totaltypescript.com/type-vs-interface-which-should-you-use</a><br><a href=\"https://www.alvseven.com/type-vs-interface\">https://www.alvseven.com/type-vs-interface</a></p>\n","tags":["typescript"]},{"id":"web-performance","url":"https://yieldray.fun/posts/web-performance","title":"Web性能速览","date_published":"2025-03-24T16:44:44.000Z","date_modified":"2025-03-24T16:44:44.000Z","content_text":"<h1>相关 API 规范</h1>\n<p><a href=\"https://www.w3.org/webperf/\">https://www.w3.org/webperf/</a></p>\n<blockquote>\n<p>浏览器渲染机制，参见：<a href=\"https://yieldray.fun/posts/how-browser-render\">https://yieldray.fun/posts/how-browser-render</a></p>\n</blockquote>\n<h1>主动缓存</h1>\n<p>HTTP 缓存机制</p>\n<ul>\n<li>Cache-Control</li>\n<li>Etag/Last-Modified</li>\n</ul>\n<p><a href=\"https://developer.chrome.com/docs/workbox/caching-strategies-overview\">Service-Worker 缓存</a>（建议使用库，代码仅演示）</p>\n<pre><code class=\"language-js\">const addResourcesToCache = async (resources) =&gt; {\n    const cache = await caches.open(&quot;v1&quot;);\n    await cache.addAll(resources);\n};\n\nself.addEventListener(&quot;install&quot;, (event) =&gt; {\n    event.waitUntil(addResourcesToCache([&quot;/&quot;, &quot;/index.html&quot;, new Request(&quot;/style.css&quot;)]));\n});\n\nself.addEventListener(&quot;fetch&quot;, (event) =&gt; {\n    event.respondWith(caches.match(event.request));\n});\n</code></pre>\n<blockquote>\n<p>Service Worker 具备很多缓存<strong>高级特性</strong>，超出本文范畴</p>\n</blockquote>\n<h1>TTFB，速度</h1>\n<p><img src=\"https://wsrv.nl/?url=https://web.dev/static/articles/ttfb/image/performance-navigation-timing-timestamp-diagram.svg\" alt=\"performance-navigation-timing-timestamp-diagram\"></p>\n<p>避免重定向（如 HSTS）</p>\n<p>使用 CDN，边缘服务器</p>\n<p>使用 HTTP/2 HTTP/3 （队头阻塞问题）</p>\n<p>服务器使用流式响应，无论是否是 SSR</p>\n<p><img src=\"https://s2.loli.net/2025/03/24/UPVRo1Bf2TituWA.png\" alt=\"http2-and-http3-stacks-comparison.png\"></p>\n<h1>预请求/预缓存，优化请求发现/资源提示</h1>\n<pre><code>&lt;link rel=&quot;dns-prefetch&quot; href=&quot;https://fonts.googleapis.com&quot;&gt;\n&lt;link rel=&quot;preconnect&quot; href=&quot;https://example.com&quot;&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;/lcp-image.jpg&quot; as=&quot;image&quot;&gt;\n&lt;link rel=&quot;modulepreload&quot; href=&quot;script.mjs&quot; /&gt;\n</code></pre>\n<p>下面属于预渲染，非首次请求</p>\n<pre><code>&lt;link rel=&quot;prefetch&quot; href=&quot;/next-page.css&quot; as=&quot;style&quot;&gt;\n&lt;script type=&quot;speculationrules&quot;&gt;\n    {\n        &quot;prerender&quot;: [],\n        &quot;prefetch&quot;: []\n    }\n&lt;/script&gt;\n</code></pre>\n<p>HTTP/2 资源推送（成本可能较高）</p>\n<p>避免导致无法资源发现，如：CSS @import，动态创建 script 标签等</p>\n<h1>资源大小/请求数量</h1>\n<p>使用 HTTP 压缩机制（Content-Encoding）：gzip / deflate / br / zstd</p>\n<p>压缩代码，图片等资源。使用压缩效果更好的格式，如 webp</p>\n<p>代码拆分，避免请求首屏不需要的代码：</p>\n<ul>\n<li>按路由拆分</li>\n<li>按组件拆分</li>\n<li>Vendor 拆分</li>\n</ul>\n<p>树摇，移除未使用的脚本和样式（对于 Webpack/Rollup 的脚本，必须使用 ESM）</p>\n<p>减少请求数量：考虑内联图片，内联关键 CSS；使用 bundler 合并代码（即使是 HTTP/2），减少对不同源的请求</p>\n<p>正确使用 polyfill（渐进式增强），可使用 <a href=\"https://browsersl.ist/\">browserslist</a> 工具确定目标浏览器</p>\n<h1>FCP</h1>\n<p>优化渲染关键路径，减少阻塞（主线程）渲染的资源，尽快呈现内容（减少白屏时间）</p>\n<p><img src=\"https://wsrv.nl/?url=https://web.dev/learn/performance/optimize-resource-loading/image/fig-2.svg\" alt=\"optimize-resource-loading\"></p>\n<ul>\n<li>CSS 加载不会阻塞 DOM 解析，但会阻塞 DOM 树的生成与渲染；阻塞 JS 的执行（当然不阻塞 JS 解析）</li>\n<li>同步 JS 会阻塞 DOM 和 CSSOM 解析</li>\n</ul>\n<p>避免 DOM 规模过大，降低 CSS 选择器的复杂性，减少要设置样式的元素数量</p>\n<p>考虑 SSR，预渲染（指构建 HTML 骨架屏）等</p>\n<h1>LCP</h1>\n<p>懒加载/延迟加载非关键资源 <code>&lt;img loading=&quot;lazy&quot;&gt; &lt;iframe loading=&quot;lazy&quot;&gt;</code>（Intersection Observer 或相关 JS 库）</p>\n<p>请求优先级，合理使用 <code>fetchPriority</code></p>\n<p>避免 DOM 规模过大（无论是 SPA 还是 SSR/MPA，可考虑分页）</p>\n<h1>TTI</h1>\n<p><img src=\"https://wsrv.nl/?url=https://web.dev/static/articles/tti/image/a-page-load-timeline-show-762f93f25ad4b.svg\" alt=\"a-page-load-timeline-show\"></p>\n<blockquote>\n<p>SSR 等技术可能会导致网页出现以下情况：看上去可互动（即屏幕上会显示链接和按钮），但实际上不是可交互的，因为主线程处于阻塞或因为控制这些元素的 JavaScript 代码尚未加载。</p>\n</blockquote>\n<p>SSR 水合 (Hydration) 优化：</p>\n<ul>\n<li>减少水合的代码量： 只水合必要的组件，并避免水合整个应用程序。</li>\n<li>渐进式水合： 按需水合组件，而不是一次性水合所有组件。</li>\n<li>并行水合： 并行水合多个组件，加快水合速度。</li>\n</ul>\n<h1>CLS</h1>\n<p>避免由于图片等资源未正确分配空间导致的<strong>布局偏移（Layout Shift）</strong></p>\n<p>避免 FOUC，保证样式、字体资源加载顺序合理（<code>font-display: swap</code>）</p>\n<p>使用<a href=\"https://web.dev/articles/animations-guide\">高性能动画</a>（即提升渲染到合成器层），避免使用会触发布局或绘制（回流重绘）的属性</p>\n<h1>INP</h1>\n<blockquote>\n<p>INP 指标已替代 FID 指标</p>\n</blockquote>\n<p>优先使用 CSS 动画而不是 JS 动画</p>\n<p>避免在同一帧中多次变更 DOM 并查询布局（强制回流），这会导致破坏布局快速路径，导致<strong>布局抖动（Layout Thrashing）</strong>（方法：考虑使用 requestAnimationFrame）</p>\n<p>避免使用 <code>useLayoutEffect</code>，防止阻塞当前帧渲染</p>\n<p>将 DOM 无关的高昂计算操作卸载到 Web Worker</p>\n<p>考虑使用防抖与节流</p>\n<p>使用 KeepAlive 缓存 DOM（优化路由切换），对于 React 则正确使用 useMono，useCallback 等机制</p>\n<p>考虑使用虚拟滚动（避免 DOM 规模过大）</p>\n<p>任务调度/拆解，使用 <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API\">Prioritized Task Scheduling API</a>（或类似 <a href=\"https://github.com/facebook/react/tree/main/packages/scheduler\">React Scheduler</a> 的调度机制）</p>\n<h1>性能采集</h1>\n<p><a href=\"https://github.com/GoogleChrome/web-vitals\">web-vitals</a>：LCP CLS INP (TTFB FCP)</p>\n<p><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/Performance_API\">Performance API</a></p>\n<p>Lighthouse、WebPageTest 工具<br>第三方 Real User Monitoring (RUM) 工具监控</p>\n<h1>参考</h1>\n<p><a href=\"https://web.dev/learn/performance\">https://web.dev/learn/performance</a><br><a href=\"https://developer.chrome.com/docs/lighthouse/performance/\">https://developer.chrome.com/docs/lighthouse/performance/</a></p>\n<p>案例研究：<br><a href=\"https://largeapps.dev/case-studies/advanced/\">https://largeapps.dev/case-studies/advanced/</a></p>\n","tags":["web"]},{"id":"micro-frontend-compare","url":"https://yieldray.fun/posts/micro-frontend-compare","title":"微前端实现比较","date_published":"2025-03-04T19:44:23.000Z","date_modified":"2025-03-04T19:44:23.000Z","content_text":"<h1>前言</h1>\n<p>微前端可用于实现多前端应用的集成。应用分为主应用（基座应用）和多个子应用（微应用），主应用一般相当于底座，控制哪个或哪些应用的呈现。</p>\n<p>何谓微前端应用？在微前端中，应用都是 SPA，因此可以简单的认为应用就是一个 HTML 页面。<br>页面为这个单独的应用提供独立的渲染上下文，就像进程之于操作系统。<br>应用指的是 SPA 的这个 A。</p>\n<p>应当注意微前端和微服务是很大程度上不同的。在很多场景下，微服务的最小维度是服务，微前端的最小维度是前端应用。<br>这也就是说，微前端中各个应用往往更具独立性，微服务往往则需要注册中心。</p>\n<p>微前端提供什么优势？我觉得主要提供的功能在于<strong>独立构建部署</strong>和<strong>独立页面上下文</strong>。<br>事实上，直接构建多个独立应用也可以提供这些功能。我们想要的其实就是拿回这些功能。</p>\n<p>此外，我们可能还希望：</p>\n<ul>\n<li>多应用之间的路由就像是在普通 SPA 里路由一样</li>\n<li>无需重复维护页面的公共部分，如导航栏；应用的公共部分，如鉴权</li>\n<li>可以同时加载多个应用，甚至还可以嵌套</li>\n</ul>\n<p>因此，微前端解决方案（框架）都会提供应用隔离、路由与渲染生命周期、共享状态管理等功能。</p>\n<p>此外，微前端的很多场景都是管理系统，额外的请求延迟和渲染延迟是可以接受的。<br>并且我们只考虑 SPA，完全不考虑 SSR。</p>\n<h1>文档与浏览上下文</h1>\n<p>在很多情况下我们使用“页面”一词，这有时不够精确。因为某些微前端中我们可能会考虑 iframe。</p>\n<p>根据 HTML 规范：</p>\n<ul>\n<li>文档（Document）是<a href=\"https://html.spec.whatwg.org/multipage/document-sequences.html#navigable\">可导航对象</a>，即导航发生在文档之间。（标签页，窗口或iframe）</li>\n<li><a href=\"https://html.spec.whatwg.org/multipage/document-sequences.html#browsing-context\">浏览上下文</a> 是文档的编程表示。它与 WindowProxy 对象一一对应。多个文档可存在于单个可导航上下文中。</li>\n</ul>\n<p>DocumentOrShadowRoot &lt;- <a href=\"https://developer.mozilla.org/docs/Web/API/ShadowRoot\">ShadowRoot</a></p>\n<h1>一个最小实现</h1>\n<p>不同微前端框架提供的运行时方案可能是不同的。</p>\n<p>例如，wujie 可直接通过 iframe 访问子应用的 window，那么子应用就可以通过自己的 window 向主应用暴露生命周期。</p>\n<p>qiankun/Single-SPA 不使用 iframe，通过模块系统暴露生命周期。</p>\n<p>（本文只是以 wujie 和 qiankun 两种不同原理的实现方式举例说明）</p>\n<p>显然，目标其实是一样的，即需要让主应用通过路由控制子应用的生命周期。</p>\n<hr>\n<p>下面是一个非常简单的例子，使用 ES 模块，可以方便的暴露生命周期。</p>\n<blockquote>\n<p><a href=\"https://super-simple-mfe.naoh.eu.org/\">Demo</a></p>\n</blockquote>\n<pre><code class=\"language-js\">let microApp = undefined;\n\nexport function mount(target) {\n    microApp = target;\n    addEventListener(&quot;hashchange&quot;, onHashChange);\n    microApp.innerHTML = `&lt;h1&gt;I&#39;m microApp&lt;/h1&gt;`;\n}\n\nexport function unmount(target) {\n    target.innerHTML = &quot;&quot;;\n    removeEventListener(&quot;hashchange&quot;, onHashChange);\n}\n\nfunction onHashChange() {\n    let hash = location.hash.slice(1); // 去除前导 # 字符\n    // 假设路由前缀为 microApp/，若为子应用则去除前缀\n    if (window.__IS_MICRO_FRONT_END__) hash = hash.replace(/^microApp\\//, &quot;&quot;);\n    microApp.innerHTML = `microApp is at location of &quot;${hash}&quot;`;\n}\n\nif (!window.__IS_MICRO_FRONT_END__) {\n    mount(document.body);\n}\n</code></pre>\n<p>子应用必须对路由做处理，并且与主应用约定一致。</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n    window.__IS_MICRO_FRONT_END__ = true;\n&lt;/script&gt;\n&lt;script type=&quot;module&quot;&gt;\n    import { mount as mountMicroApp, unmount as unmountMicroApp } from &quot;./microApp.js&quot;;\n\n    document.body.innerHTML = /*html*/ `\\\n&lt;nav&gt;\n    &lt;a href=&quot;#/&quot;&gt;Home&lt;/a&gt;\n    &lt;a href=&quot;#microApp/&quot;&gt;microApp&lt;/a&gt;\n&lt;/nav&gt;\n\n&lt;div id=&quot;microApp&quot;&gt;&lt;/div&gt;\n`;\n\n    const registry = {\n        &quot;microApp/&quot; /* &lt;- 约定的路由前缀 */: {\n            isMounted: false,\n            mount: mountMicroApp,\n            unmount: unmountMicroApp,\n            container: document.getElementById(&quot;microApp&quot;),\n        },\n    };\n\n    function onHashChange() {\n        let hash = location.hash.slice(1); // 去除前导 # 字符\n\n        for (const [prefix, app] of Object.entries(registry)) {\n            if (!hash.startsWith(prefix) &amp;&amp; app.isMounted) {\n                app.unmount(app.container);\n                app.isMounted = false;\n            }\n        }\n\n        for (const [prefix, app] of Object.entries(registry)) {\n            if (hash.startsWith(prefix) &amp;&amp; !app.isMounted) {\n                app.mount(app.container);\n                app.isMounted = true;\n            }\n        }\n    }\n\n    onHashChange();\n    addEventListener(&quot;hashchange&quot;, onHashChange);\n&lt;/script&gt;\n</code></pre>\n<p>microApp 也可以作为一个独立应用。</p>\n<pre><code class=\"language-html\">&lt;script type=&quot;module&quot;&gt;\n    import &quot;./microApp.js&quot;;\n&lt;/script&gt;\n</code></pre>\n<p>可以看出：在这个架构中，实际只存在一个文档。</p>\n<h1>使用 iframe</h1>\n<p>如果想利用 iframe 做微前端，很容易想到两个缺点：</p>\n<ul>\n<li>iframe 的路由导航是分离的</li>\n<li>iframe 内的文档渲染在 iframe 元素下，体验上很割裂</li>\n</ul>\n<p>wujie 文档上说其利用了 web components，实际上它只需要利用的是 <a href=\"https://developer.mozilla.org/docs/Web/API/ShadowRoot\">ShadowRoot</a>。</p>\n<p>隐式根（ShadowRoot）是一个类似于 Document 的对象，如果我们不将子应用渲染在 iframe 下，而是渲染在隐式根下，<br>则能够同时享受到<em>隐式根</em>提供的<em>样式隔离</em> 和 <em>iframe</em> 提供的<em>全局对象隔离</em>。</p>\n<p>对于路由问题，我们可以通过 iframe.contentWindow.history.pushState 等方法将主应用的路由同步到 iframe 上。<br>不妨直接看看其源码中是怎么做处理的：<a href=\"https://github.com/Tencent/wujie/blob/master/packages/wujie-core/src/sync.ts\">https://github.com/Tencent/wujie/blob/master/packages/wujie-core/src/sync.ts</a></p>\n<p>使用影子 DOM（ShadowDOM）时，当然需要关注一些特定问题，例如<a href=\"https://zh.javascript.info/shadow-dom-events\">事件边界</a>。<br>qiankun 的 strictStyleIsolation 样式隔离方案也是基于影子 DOM 的，这里的问题都是类似的。</p>\n<p>剩下的问题其实很显然了，即我们希望在 iframe 下使用的 document location 等对象是主应用提供的，<br>wujie 通过 Proxy 替换 iframe 下的对象：<a href=\"https://github.com/Tencent/wujie/blob/master/packages/wujie-core/src/proxy.ts\">https://github.com/Tencent/wujie/blob/master/packages/wujie-core/src/proxy.ts</a></p>\n<p>当然，wujie 还有一套降级方案，这里就不讨论了。<br>在不考虑降级的情况下，wujie 的核心解决方案是很简单的，可以发现它的很多代码都是在做代理的边界处理。</p>\n<h1>样式，全局环境隔离</h1>\n<p>默认情况下 qiankun 使用基于 Proxy 的沙盒，沙盒用于隔离全局环境。观察源码可以发现其做的操作基本为共享全局对象的边界处理。<br>源码：<a href=\"https://github.com/umijs/qiankun/blob/master/src/sandbox/proxySandbox.ts\">https://github.com/umijs/qiankun/blob/master/src/sandbox/proxySandbox.ts</a></p>\n<p>若不支持 Proxy，则降级到快照沙箱（本质上就是记录修改了哪些属性，然后可以恢复，因此只支持单实例）。<br>源码：<a href=\"https://github.com/umijs/qiankun/blob/master/src/sandbox/snapshotSandbox.ts\">https://github.com/umijs/qiankun/blob/master/src/sandbox/snapshotSandbox.ts</a></p>\n<hr>\n<p>单实例情况下，只需保证激活时挂载样式，并在卸载时移除样式，即可解决大部分样式问题，这也是 qiankun 的默认行为。<br>此外，qiankun 使用 strictStyleIsolation 配置，将样式注入影子 DOM 内部实现隔离。<br>使用 experimentalStyleIsolation 配置时，qiankun 会在运行时改写CSS选择器，<br>即 <code>.selector {}</code> 变为 <code>[data-qiankun-react16] .selector {}</code></p>\n<p>实现方法很简单，修改已有的 <a href=\"https://developer.mozilla.org/docs/Web/API/StyleSheet\">CSSOM</a> 即可。<br>源码：<a href=\"https://github.com/umijs/qiankun/blob/master/src/sandbox/patchers/css.ts\">https://github.com/umijs/qiankun/blob/master/src/sandbox/patchers/css.ts</a></p>\n<p>如果子应用构建时使用 Scoped CSS 或 <a href=\"https://github.com/topaxi/postcss-selector-namespace\">selector-namespace</a>，那么无需微前端框架做运行时处理。</p>\n<p>对于加载入口，qiankun 抽象了 npm 包 <a href=\"https://github.com/kuitos/import-html-entry\">import-html-entry</a>，用于从 HTML 字符串中提取脚本和样式入口。</p>\n<h1>其它优化</h1>\n<blockquote>\n<p>TODO\n预加载，子应用保活。。。</p>\n</blockquote>\n<p>瀑布式请求：主应用启动之后，才开始启动子应用，子应用才能发起请求，加大了请求瀑布问题。<br>对于此问题，往往可以采用很多简单有效的方法，比如 prefetch</p>\n<h1>依赖复用</h1>\n<p>应用间依赖复用：Module Federation？微前端的主要优势是解耦，依赖复用的本质是耦合，这实际上增大了维护难度。<br>需要依赖复用则说明公共逻辑比较多。因为如果只是共享公共依赖，其实使用直接使用公共 CDN 即可实现。<br>Module Federation 反而给工程化带来很多挑战（当然框架已解决了部分问题）。</p>\n<h1>是否要使用</h1>\n<p>现代基于模块化的前端开发中，全局污染、样式冲突都比较可控。使用微前端，同时实现独立部署和微前端部署确实是优势。</p>\n<p>如果无需考虑同时显示多个子应用，牺牲一些模块共享和路由一致性，构建为多页应用也不失为一种稳妥的方案。</p>\n","tags":[]},{"id":"js-modules","url":"https://yieldray.fun/posts/js-modules","title":"js模块机制","date_published":"2025-02-28T12:00:00.000Z","date_modified":"2025-02-28T12:00:00.000Z","content_text":"<p>假设读者已熟悉 esm/cjs 语义，若不了解，参考：</p>\n<ul>\n<li><a href=\"https://exploringjs.com/es6/ch_modules.html\">https://exploringjs.com/es6/ch_modules.html</a></li>\n<li><a href=\"https://2ality.com/2015/07/es6-module-exports.html\">https://2ality.com/2015/07/es6-module-exports.html</a></li>\n</ul>\n<p>服务器环境只以 Node.js 为例。</p>\n<p>本文不涉及具体运行时实现，仅考虑标准。关于 Node.js 的模块实现，见<a href=\"/nodejs-modules\">Node.js模块处理机制</a>。</p>\n<p>对于 bundler 部分，只以 webpack（对于 cjs→cjs） 和 rollup（对于 esm→esm） 为例，这是它们优先支持的模块系统。</p>\n<h1>ES Module</h1>\n<p>概况性地说，es 模块流程如下：</p>\n<ul>\n<li>构建（construction）：解析（resolution），获取（fetch），语法解析（Parse）</li>\n<li>实例化（instantiation）</li>\n<li>执行（evaluation）</li>\n</ul>\n<p>其中，宿主负责语法解析前的操作，引擎负责之后的工作。</p>\n<hr>\n<p>esm 的特点是静态模块发现（生成模块依赖图）可以在语法解析时完成（而无需在执行时）。<br>若处理动态导入，只需再创建一个单独的模块依赖图即可（这一点非常重要）。</p>\n<blockquote>\n<p>cjs 是为本地文件系统设计的模块系统，而 esm 则需要单独考虑模块获取流程。</p>\n</blockquote>\n<p>esm 通过语言规范引入了新的语义，该语义为静态分析（即语法解析）暴露出模块系统。<br>这种静态特性不仅优化了请求瀑布（实现并发加载），对构建系统也很有帮助。</p>\n<blockquote>\n<p>不要误解解释器和ES代码的同步或异步执行。对于ES代码，简单来说，只有立即推入执行上下文堆栈执行才是同步。<br>而解释器的异步流程很可能根本无法在ES代码中观测到，换言之，解释器的异步流程对ES脚本来说可以是同步。对并发问题也是如此。</p>\n</blockquote>\n<h2>规范</h2>\n<p>timeline：</p>\n<ul>\n<li>ES2015 支持模块</li>\n<li>ES2020 支持 <code>import()</code> <code>export * as ns from &#39;module&#39;</code> <code>import.meta</code></li>\n<li>ES2022 支持 top-level await</li>\n</ul>\n<blockquote>\n<p>有一些异步执行的提案还在推进中，如<a href=\"https://github.com/tc39/proposal-defer-import-eval\">defer-import-eval</a></p>\n</blockquote>\n<p>see also <a href=\"https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/\">https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/</a></p>\n<h3>模块记录</h3>\n<p><a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-abstract-module-records\">Module Record</a></p>\n<table>\n<thead>\n<tr>\n<th>字段名称</th>\n<th>值类型</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>[[Realm]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#realm-record\">Realm Record</a></td>\n<td>创建此模块的 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#realm\">Realm</a>。</td>\n</tr>\n<tr>\n<td><code>[[Environment]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-module-environment-records\">Module Environment Record</a> 或 empty</td>\n<td>包含此模块的顶层绑定的 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-environment-records\">Environment Record</a>。 此字段在模块链接时设置。</td>\n</tr>\n<tr>\n<td><code>[[Namespace]]</code></td>\n<td>一个 Object 或 empty</td>\n<td>如果已为此模块创建模块命名空间对象（<a href=\"https://tc39.es/ecma262/multipage/reflection.html#sec-module-namespace-objects\">28.3</a>），则为该对象。</td>\n</tr>\n<tr>\n<td><code>[[HostDefined]]</code></td>\n<td>任何类型（默认值为 undefined）</td>\n<td>保留字段，供需要将附加信息与模块关联的<a href=\"https://tc39.es/ecma262/multipage/overview.html#host-environment\">宿主环境</a>使用。</td>\n</tr>\n</tbody></table>\n<hr>\n<p><a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#cyclic-module-record\">Cyclic Module Records</a></p>\n<table>\n<thead>\n<tr>\n<th>字段名称</th>\n<th>值类型</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>[[Status]]</code></td>\n<td>new, unlinked, linking, linked, evaluating, evaluating-async, 或 evaluated</td>\n<td>初始状态为 new。随着模块在其生命周期中的进展，依次转换为 unlinked, linking, linked, evaluating，可能为 evaluating-async，以及 evaluated。 evaluating-async 表示此模块已排队等待其异步依赖项完成后执行，或者它是一个 <code>[[HasTLA]]</code> 字段为 true 的模块，该模块已执行并正在等待顶层完成。</td>\n</tr>\n<tr>\n<td><code>[[EvaluationError]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-completion-record-specification-type\">throw completion</a> 或 empty</td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-completion-record-specification-type\">throw completion</a>，表示求值期间发生的异常。如果未发生异常或 <code>[[Status]]</code> 不是 evaluated，则为 undefined。</td>\n</tr>\n<tr>\n<td><code>[[DFSIndex]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#integer\">integer</a> 或 empty</td>\n<td>仅在 Link 和 Evaluate 期间使用的辅助字段。如果 <code>[[Status]]</code> 是 linking 或 evaluating，则此非负数记录了在深度优先遍历依赖关系图期间首次访问该模块的点。</td>\n</tr>\n<tr>\n<td><code>[[DFSAncestorIndex]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#integer\">integer</a> 或 empty</td>\n<td>仅在 Link 和 Evaluate 期间使用的辅助字段。如果 <code>[[Status]]</code> 是 linking 或 evaluating，则这要么是模块自身的 <code>[[DFSIndex]]</code>，要么是同一强连通分量中“更早”模块的 <code>[[DFSIndex]]</code>。</td>\n</tr>\n<tr>\n<td><code>[[RequestedModules]]</code></td>\n<td>一个由字符串组成的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">List</a></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">List</a>，包含此记录所代表的模块用于请求导入模块的所有 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ModuleSpecifier\">ModuleSpecifier</a> 字符串。<a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">List</a> 按照源代码文本出现的顺序排列。</td>\n</tr>\n<tr>\n<td><code>[[LoadedModules]]</code></td>\n<td>一个由 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">Records</a> 组成的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">List</a>，其中包含字段 <code>[[Specifier]]</code> (一个字符串) 和 <code>[[Module]]</code> (一个 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-abstract-module-records\">Module Record</a>)</td>\n<td>一个从该记录所代表的模块用于请求导入模块的 specifier 字符串到已解析的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-abstract-module-records\">Module Record</a> 的映射。该列表不包含两个具有相同 <code>[[Specifier]]</code> 的不同 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">Records</a>。</td>\n</tr>\n<tr>\n<td><code>[[CycleRoot]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#cyclic-module-record\">Cyclic Module Record</a> 或 empty</td>\n<td>循环中第一个被访问的模块，即强连通分量的根 DFS 祖先。对于不在循环中的模块，这将是模块本身。一旦 Evaluate 完成，模块的 <code>[[DFSAncestorIndex]]</code> 就是其 <code>[[CycleRoot]]</code> 的 <code>[[DFSIndex]]</code>。</td>\n</tr>\n<tr>\n<td><code>[[HasTLA]]</code></td>\n<td>一个 Boolean</td>\n<td>此模块是否是单独异步的（例如，如果它是一个包含顶层 await 的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sourctextmodule-record\">Source Text Module Record</a>）。具有异步依赖项并不意味着此字段为 true。此字段在模块解析后不得更改。</td>\n</tr>\n<tr>\n<td><code>[[AsyncEvaluation]]</code></td>\n<td>一个 Boolean</td>\n<td>此模块本身是否是异步的，或者是否具有异步依赖项。注意：设置此字段的顺序用于对排队的执行进行排序，请参见 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-async-module-execution-fulfilled\">16.2.1.5.3.4</a>。</td>\n</tr>\n<tr>\n<td><code>[[TopLevelCapability]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promisecapability-records\">PromiseCapability Record</a> 或 empty</td>\n<td>如果此模块是某个循环的 <code>[[CycleRoot]]</code>，并且已对该循环中的某个模块调用了 Evaluate()，则此字段包含该整个求值的 <a href=\"https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promisecapability-records\">PromiseCapability Record</a>。它用于解决从 Evaluate() 抽象方法返回的 Promise 对象。对于该模块的任何依赖项，此字段将为空，除非已为其中一些依赖项启动了顶层 Evaluate()。</td>\n</tr>\n<tr>\n<td><code>[[AsyncParentModules]]</code></td>\n<td>一个由 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#cyclic-module-record\">Cyclic Module Records</a> 组成的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">List</a></td>\n<td>如果此模块或依赖项的 <code>[[HasTLA]]</code> 为 true，并且执行正在进行中，则这会跟踪此模块的顶层执行作业的父导入器。这些父模块不会在此模块成功完成执行之前开始执行。</td>\n</tr>\n<tr>\n<td><code>[[PendingAsyncDependencies]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#integer\">integer</a> 或 empty</td>\n<td>如果此模块有任何异步依赖项，则这会跟踪此模块剩余要执行的异步依赖模块的数量。当此字段达到 0 并且没有执行错误时，将执行具有异步依赖项的模块。</td>\n</tr>\n</tbody></table>\n<h3>模块命名空间</h3>\n<p>动态导入有单独的<a href=\"https://tc39.es/ecma262/multipage/reflection.html#sec-module-namespace-objects\">运行时表示</a>，这正确的保证了命名空间是密封、可枚举的。</p>\n<table>\n<thead>\n<tr>\n<th>内部插槽</th>\n<th>类型</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>[[Module]]</code></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-abstract-module-records\">Module Record</a></td>\n<td>此命名空间公开其导出的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-abstract-module-records\">Module Record</a>。</td>\n</tr>\n<tr>\n<td><code>[[Exports]]</code></td>\n<td>一个由字符串组成的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">List</a></td>\n<td>一个 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-list-and-record-specification-type\">List</a>，其元素是作为此对象自身属性公开的导出名称的字符串值。该列表按照<a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#lexicographic-code-unit-order\">词典编码单元顺序</a>排序。</td>\n</tr>\n</tbody></table>\n<h2>模块处理流程</h2>\n<p>这里以 HTML标准 和 ECMA262 加以说明，浏览器环境：</p>\n<p><a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#scripting-processing-model\">https://html.spec.whatwg.org/multipage/webappapis.html#scripting-processing-model</a></p>\n<p><a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-example-cyclic-module-record-graphs\">https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-example-cyclic-module-record-graphs</a></p>\n<p>根据规范，只考虑静态导入导出，简单来说：</p>\n<ul>\n<li>递归地加载模块，构建模块依赖图（注意：由浏览器网络请求模块，一旦失败，例如跨源错误和模块解析失败，<strong>整个过程将终止</strong>）</li>\n<li>递归地连接模块，检查导入导出的标识符是否正确，对每个模块 InitializeEnvironment（一旦失败，例如导入了不存在的标识符，<strong>整个过程将终止</strong>）</li>\n<li>按照依赖图执行所有模块（只有这个步骤有副作用，因此不允许重复执行）</li>\n</ul>\n<h2>Bundler</h2>\n<blockquote>\n<p>对于浏览器来说，<a href=\"https://v8.dev/features/modules#bundle\">bundler 还是很有必要的</a>。使用 <a href=\"https://web.developers.google.cn/articles/modulepreload\"><code>&lt;link rel=&quot;modulepreload&quot;&gt;</code></a>虽然可以优化请求瀑布问题，还是 bundler 来实现移除死代码和代码压缩。</p>\n</blockquote>\n<hr>\n<p><a href=\"https://rollupjs.org/repl/\">以 rollup 为例</a>，rollup 自身实现了语法解析器，AST 格式遵循 <a href=\"https://github.com/estree/estree\">estree 规范</a>。</p>\n<p><a href=\"https://stackblitz.com/edit/rollup-bundle-demo-ed8ygxp9?file=src%2Findex.js\">https://stackblitz.com/edit/rollup-bundle-demo-ed8ygxp9?file=src%2Findex.js</a></p>\n<p>bundler 的特点在于它不需要执行模块，但需要合并和生成模块。<br>因此，显然也需要记录模块依赖图、检查导入导出的标识符。无需执行。<br>bundler 也需要考虑解释器无需处理的问题，例如，如何生成模块文件，如何处理多入口模块。</p>\n<hr>\n<p>下面我们考虑实现一个简单的 esm bundler，给定（可以是多个）入口文件，首先递归解析出所有模块。模块表示如下：</p>\n<pre><code class=\"language-ts\">interface Module {\n    /** 文件系统绝对路径 */\n    path: string;\n    /** AST */\n    ast: any;\n    /** 表示是否是入口文件，入口文件必须生成对应输出模块 */\n    isEntry: boolean;\n}\n</code></pre>\n<p>首先考虑最简单的情况：</p>\n<pre><code class=\"language-js\">import &quot;./entry.js&quot;;\nimport entry from &quot;./entry.js&quot;;\nimport * as a from &quot;./entry.js&quot;;\nimport { a, b } from &quot;./entry.js&quot;;\n</code></pre>\n<p>由于 entry.js 是入口文件，打包后也存在 entry.js，因此 import 语句可以保留。这种情况下无需处理。</p>\n<p>若不是入口文件，假设 not_entry.js <strong>只被此模块引入</strong>，则 not_entry.js 不存在对应的输出模块。</p>\n<pre><code class=\"language-js\">import &quot;./not_entry.js&quot;;\nimport { a, b } from &quot;./not_entry.js&quot;;\nimport * as a from &quot;./not_entry.js&quot;;\nimport not_entry from &quot;./not_entry.js&quot;;\n</code></pre>\n<p>考虑 not_entry.js 中的 export 语句如下：</p>\n<pre><code class=\"language-js\">const str = &quot;not_entry.js&quot;;\nconsole.log(str);\nexport { str };\nexport default str;\n</code></pre>\n<p>若<strong>不考虑支持 tree-shaking</strong>，可以将 import 改写为：</p>\n<pre><code class=\"language-js\">const bundled$not_entry = ((exports = {}) =&gt; {\n    const str = &quot;not_entry.js&quot;;\n    console.log(str);\n    exports.str = str;\n    exports.default = str;\n    return exports;\n})();\n\nconst { a, b } = bundled$not_entry;\nconst a = bundled$not_entry;\nconst { default: not_entry } = bundled$not_entry;\n</code></pre>\n<p>not_entry.js 模块中也可以包含 import，若导入的是入口文件，直接将 import 提升即可，否则可以递归处理。</p>\n<p>这种方式相当于 webpack <strong>模块中心</strong>实现，可以实现模块只执行一次的预期效果。<br>然而，这种改写方法有局限性：无法实现 esm 的 live-binding 效果，因为导出变成了赋值行为，而非预期的绑定行为。</p>\n<p>要实现 tree-shaking，语法解析时，需要确定何处使用了导入的变量，然后改写变量名称即可。<br>对于未使用的标识符（未被导入，且不被模块副作用引用），其声明和赋值语句都可以直接删除。</p>\n<p>假设 not_entry.js <strong>被多个入口模块引入</strong>，则必须为该模块生成单独的 chunk 模块，而不能采取替换方法。<br>因为需要保证导入方导入的是同一个模块。</p>\n<p>综上可以发现，这些 chunk 生成的模式最终都可以通过一种有向图算法来描述。</p>\n<hr>\n<p>esm 中，同一个模块标识符的模块只运行一次。由于模块可以有副作用，因此 Bundler 需要考虑模块执行顺序。<br>bundler 相比于实际执行，只是少了实例化和执行阶段，因此它也是从入口模块出发，深度优先。<br>bundler 也是保守的，因为需要最大程度保证产物与输入的执行结果一致（特别是实际执行顺序）。<br>由于语法限制加上 bundler语义 本身的局限，并非所有输入情况都可以编译出运行结果一致的产物。</p>\n<h6>循环依赖</h6>\n<p>esm bundler 天生可以处理循环依赖，因为同标识符模块仅执行一次。<br>由于仅执行一次，非入口模块被替换时，代码顺序自然在更上方，这同时满足了执行顺序和声明作用域问题。</p>\n<p>不过存在一些模块输入情况，解释器会报错，然而此时 bundler 根本无法完成代码生成：</p>\n<pre><code class=\"language-js\">import { a } from &quot;./a.js&quot;; // SyntaxError: Detected cycle while resolving name &#39;a&#39; in &#39;./a.js&#39;\nexport { a };\n</code></pre>\n<pre><code class=\"language-js\">import { a } from &quot;./a.js&quot;;\nconst aPlusOne = a + 1; // ReferenceError: Cannot access &#39;a&#39; before initialization\nexport { aPlusOne as a };\n</code></pre>\n<h6>静态 import 语句必须为顶部声明</h6>\n<p>此语法限制 + 外部依赖可能出现问题，例如：</p>\n<pre><code class=\"language-js\">// main.js\nimport &quot;./polyfill.js&quot;;\nimport &quot;external&quot;;\nconsole.log(&quot;main&quot;);\n\n// polyfill.js\nconsole.log(&quot;polyfill&quot;);\n</code></pre>\n<p>esm 执行是先构建模块依赖图，然后按依赖图执行。<br>换言之，是语法解析-&gt;确定依赖关系-&gt;按依赖顺序执行。而非等遇见 import 语句再执行。</p>\n<p>这相当于，import 语句会提升到顶部！</p>\n<blockquote>\n<p>注意：这与 var/函数声明提升原理完全不同，因为 import/export 模块依赖图解析发生在<strong>代码执行之前</strong>，然后按图顺序执行，只是表现上看起来像是提升了。</p>\n</blockquote>\n<pre><code class=\"language-js\">// output.js\nconsole.log(&quot;polyfill&quot;);\nimport &quot;external&quot;; // 由于 import 语句会提升到顶部，因此 polyfill 在 external 模块后执行\nconsole.log(&quot;main&quot;);\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTML/Element/script/type/importmap\">导入映射</a></h2>\n<pre><code class=\"language-html\">&lt;script type=&quot;importmap&quot;&gt;\n    {\n        &quot;imports&quot;: {\n            &quot;square&quot;: &quot;./module/shapes/square.js&quot;,\n            &quot;shapes/&quot;: &quot;./module/shapes/&quot;,\n            &quot;modules/shapes/&quot;: &quot;./module/src/shapes/&quot;,\n            &quot;modules/square&quot;: &quot;./module/src/other/shapes/square.js&quot;,\n            &quot;https://example.com/modules/square.js&quot;: &quot;./module/src/other/shapes/square.js&quot;,\n            &quot;../modules/shapes/&quot;: &quot;/modules/shapes/&quot;\n        },\n        &quot;scopes&quot;: {\n            &quot;/modules/customshapes/&quot;: {\n                &quot;square&quot;: &quot;https://example.com/modules/shapes/square.js&quot;\n            }\n        }\n    }\n&lt;/script&gt;\n</code></pre>\n<h1>CommonJS</h1>\n<p>cjs 是在无需 ecma262 改动下支持的，换言之，它没有改变语言本身的任何语义。这也意味着运行时本身必须对其做一些支持。</p>\n<h2>Runtime</h2>\n<p>cjs 导出单个值，而 esm 导出一个<strong>不可变</strong>命名空间，其中包含一组绑定（因此需要语义支持）。<br>由于 esm 导入不可变绑定，其对循环依赖的处理是很自然的。</p>\n<p>cjs 对循环依赖的处理简单粗暴，但由于只支持导出单个值，导入/导出方必须对共享值做特殊支持。</p>\n<pre><code class=\"language-js\">//------ a.js ------\nconsole.log(&#39;a: before require(&quot;./b&quot;)&#39;);\nconst b = require(&quot;./b&quot;);\nconsole.log(&#39;a: after require(&quot;./b&quot;)&#39;);\n\nfunction foo() {\n    b.bar();\n}\n\nmodule.exports.foo = foo;\nconsole.log(&quot;a: end&quot;);\n\n//------ b.js ------\nconsole.log(&#39;b: before require(&quot;./a&quot;)&#39;);\nconst a = require(&quot;./a&quot;);\nconsole.log(&#39;b: after require(&quot;./a&quot;)&#39;);\n\nfunction bar() {\n    if (Math.random()) {\n        a.foo();\n    }\n}\n\nmodule.exports.bar = bar;\nconsole.log(&quot;b: end&quot;);\n</code></pre>\n<p>若先执行 a.js，实际流程是：</p>\n<pre><code>a: before require(&quot;./b&quot;)\n    b: before require(&quot;./a&quot;)\n    b: after require(&quot;./a&quot;)\n    b: end\na: after require(&quot;./b&quot;)\na: end\n</code></pre>\n<p>因此 cjs 模块导入时，导出方可能还未完成导出过程（还没运行完）。<br>这也是为什么在循环导入时有必要导入（引用）整个导出值。</p>\n<p>简单来说：cjs 模块导出默认是空对象（这很重要，这里我们称为早期绑定），require 导入的值是导出方当前导出值的拷贝。</p>\n<h2>Bundler</h2>\n<p>webpack 源码：<a href=\"https://github.com/webpack/webpack/blob/main/lib/javascript/JavascriptModulesPlugin.js\">https://github.com/webpack/webpack/blob/main/lib/javascript/JavascriptModulesPlugin.js</a></p>\n<p>例子：<a href=\"https://stackblitz.com/edit/webpack-bundle-demo?file=src%2Findex.js\">https://stackblitz.com/edit/webpack-bundle-demo?file=src%2Findex.js</a></p>\n<pre><code class=\"language-js\">//prettier-ignore\n/******/ (() =&gt; { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ &quot;./src/a.js&quot;:\n/*!******************!*\\\n  !*** ./src/a.js ***!\n  \\******************/\n/***/ ((module, __unused_webpack_exports, __webpack_require__) =&gt; {\n\nconsole.log(&#39;a: before require(&quot;./b&quot;)&#39;);\nconst b = __webpack_require__(/*! ./b */ &quot;./src/b.js&quot;);\nconsole.log(&#39;a: after require(&quot;./b&quot;)&#39;);\n\nfunction foo() {\n    b.bar();\n}\n\nmodule.exports.foo = foo;\nconsole.log(&quot;a: end&quot;);\n\n/***/ }),\n\n/***/ &quot;./src/b.js&quot;:\n/*!******************!*\\\n  !*** ./src/b.js ***!\n  \\******************/\n/***/ ((module, __unused_webpack_exports, __webpack_require__) =&gt; {\n\nconsole.log(&#39;b: before require(&quot;./a&quot;)&#39;);\nconst a = __webpack_require__(/*! ./a */ &quot;./src/a.js&quot;);\nconsole.log(&#39;b: after require(&quot;./a&quot;)&#39;);\n\nfunction bar() {\n  if (!Math.random()) {\n    a.foo();\n  }\n}\n\nmodule.exports.bar = bar;\nconsole.log(&#39;b: end&#39;);\n\n\n/***/ }),\n\n/***/ &quot;./src/index.js&quot;:\n/*!**********************!*\\\n  !*** ./src/index.js ***!\n  \\**********************/\n/***/ ((module, __unused_webpack_exports, __webpack_require__) =&gt; {\n\nmodule.exports.index = function () {\n  (__webpack_require__(/*! ./a */ &quot;./src/a.js&quot;).foo)();\n};\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tvar cachedModule = __webpack_module_cache__[moduleId];\n/******/ \t\tif (cachedModule !== undefined) {\n/******/ \t\t\treturn cachedModule.exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \t// This entry module used &#39;module&#39; so it can&#39;t be inlined\n/******/ \tvar __webpack_exports__ = __webpack_require__(&quot;./src/index.js&quot;);\n/******/ \tvar __webpack_export_target__ = exports;\n/******/ \tfor(var __webpack_i__ in __webpack_exports__) __webpack_export_target__[__webpack_i__] = __webpack_exports__[__webpack_i__];\n/******/ \tif(__webpack_exports__.__esModule) Object.defineProperty(__webpack_export_target__, &quot;__esModule&quot;, { value: true });\n/******/ \t\n/******/ })()\n;\n</code></pre>\n<p>原理很简单，模拟 node 运行时逻辑。</p>\n<ul>\n<li><strong><code>__webpack_modules__</code></strong>: 一个对象，包含了所有模块定义。键为模块 ID，值为模块包装函数</li>\n<li><strong><code>__webpack_module_cache__</code></strong>: 一个对象，作为模块缓存。键为模块 ID，值是已加载的模块对象</li>\n<li><strong><code>__webpack_require__</code></strong>: 模块加载器函数。负责加载、缓存和执行模块</li>\n<li><strong>启动代码</strong>: 加载入口模块，并将入口模块的导出赋值给全局 <code>exports</code> 对象</li>\n</ul>\n<pre><code class=\"language-js\">function __webpack_require__(moduleId) {\n    // 检查模块是否在缓存中\n    var cachedModule = __webpack_module_cache__[moduleId];\n    if (cachedModule !== undefined) {\n        // 若已缓存，则直接返回\n        return cachedModule.exports;\n    }\n    // 执行模块并放入缓存\n    var module = (__webpack_module_cache__[moduleId] = {\n        exports: {},\n    });\n\n    // 此处执行\n    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n    // 返回模块导出\n    return module.exports;\n}\n</code></pre>\n<h1>Bundler 高级部分</h1>\n<h2>Tree Shaking / Dead Code</h2>\n<p>优化产物体积就在于消除一些保守的部分（如副作用）。</p>\n<p>显然，esm 天生适合 tree-shaking，因为导出和（静态）导入只能发生在顶级作用域，因此所有导入导出的标识符都是确定的，并且不能动态修改。</p>\n<p>rollup 以 tree-shaking 而闻名，在 webpack 中支持 tree-shaking 也需要保证输入是 es 模块。<br>并且支持一些<a href=\"https://webpack.js.org/guides/tree-shaking/\">约定</a>，以进行优化。</p>\n<p>注意，cjs 只导出单个值（esm 为多值），可在任意时刻修改导出（esm 仅允许静态导出），并在导入时是复制行为（esm 为绑定行为）。<br>理论上说，cjs 也能够一定程度支持 tree-shaking，因为导入行为可以看作是静态的。</p>\n<h2>Lazy Loading / 多入口</h2>\n<p>由于 esm 支持<strong>动态导入</strong>，lazy loading 就非常简单了。对于 bundler，将被动态导入的模块也视为入口模块即可。</p>\n<p>另一种场景则是<a href=\"https://web.developers.google.cn/articles/use-long-term-caching?#extract_dependencies_and_runtime_into_a_separate_file\">为 bundler 手动配置分块</a>，处理方式也是类似的。</p>\n<p>see also: <a href=\"https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting\">https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting</a></p>\n<p><a href=\"https://web.developers.google.cn/articles/use-long-term-caching?hl=zh-cn#lazy-loading\">https://web.developers.google.cn/articles/use-long-term-caching?hl=zh-cn#lazy-loading</a></p>\n<h2>Hot Module Replacement</h2>\n<p>热模块替换比热重载的优点就是能够保留应用的状态，这需要应用本身做支持，bundler 只是通知应用程序模块发生了变更。</p>\n<p>显然，HMR 并非 bundler 的本职工作，而是开发服务器实现的。</p>\n<p>理论上来说可以很简单，开发服务器和浏览器建立单向连接（SSE/WS都可以实现），开发服务器记录依赖状态并在依赖变更时发送通知即可。<br>开发服务器还需要为框架做支持，例如如果是 react 组件，为保留组件状态，可以创建代理组件保证实际的组件，状态提升到代理组件上，从而避免状态丢失。</p>\n<p>see also: <a href=\"https://webpack.js.org/concepts/hot-module-replacement/\">https://webpack.js.org/concepts/hot-module-replacement/</a></p>\n<h1>转换</h1>\n<p>see also <a href=\"https://github.com/vite-plugin/vite-plugin-commonjs/blob/main/commonjs.zh-CN.md\">https://github.com/vite-plugin/vite-plugin-commonjs/blob/main/commonjs.zh-CN.md</a></p>\n<h2>esm 到 cjs</h2>\n<pre><code class=\"language-js\">export var foo = 1;\n\nexport function bar() {\n    return foo++;\n}\n\nexport default function baz() {\n    return bar();\n}\n</code></pre>\n<p>注意到 <a href=\"https://rollupjs.org/configuration-options/#output-interop\"><code>__esModule</code></a>，这标记 esm 使用了默认导出。</p>\n<pre><code class=\"language-js\">&quot;use strict&quot;;\n\nObject.defineProperty(exports, &quot;__esModule&quot;, { value: true });\n\nexports.foo = 1;\n\nfunction bar() {\n    return exports.foo++;\n}\n\nfunction baz() {\n    return bar();\n}\n\nexports.bar = bar;\nexports.default = baz;\n</code></pre>\n<p>导入的转换也可以很简单</p>\n<pre><code class=\"language-js\">import foo from &quot;./foo&quot;; // 假设 foo 是 esm 编译出的 cjs 模块\n// ↓↓↓↓\nconst { default: foo } = require(&quot;./foo&quot;);\n\nimport * as foo from &quot;./foo&quot;;\n// ↓↓↓↓\nconst foo = require(&quot;./foo&quot;);\n\nimport { bar, baz } from &quot;./foo&quot;;\n//↓↓↓↓\nconst { bar, baz } = require(&quot;./foo&quot;);\n\nfunction func(name) {\n    const foo = import(`./${name}`);\n    //↓↓↓↓\n    const foo = Promise.resolve(require(`./${name}`));\n}\n</code></pre>\n<h2>cjs 到 esm</h2>\n<p>强烈建议阅读 <a href=\"https://github.com/rollup/plugins/tree/master/packages/commonjs/\">@rollup/plugin-commonjs</a> 文档。文档中讨论了 rollup 的 cjs 转换 esm 实现。<br>该插件能够配置处理各种依赖情况，包括循环依赖和混合依赖。</p>\n<p>下面我们以 <a href=\"https://vite.dev/guide/dep-pre-bundling.html#dependency-pre-bundling\">vite Pre-Bundling</a> 为例，它是一种简单但有效的方法。做如下转换：</p>\n<pre><code class=\"language-js\">// add.js\nexports.add = (a, b) =&gt; a + b;\n\n// minus.js\nexports.minus = (a, b) =&gt; a - b;\n\n// math.js\nexports.calc = (a, b, operate) =&gt; {\n    const calc =\n        operate === &quot;+&quot; // condition require\n            ? require(&quot;./add&quot;).add(a, b)\n            : require(&quot;./minus&quot;).minus(a, b);\n    return calc(a, b);\n};\n</code></pre>\n<p>转换为</p>\n<pre><code class=\"language-js\">var __commonJS = (cb, module = { exports: {} }) =&gt;\n    function __require2() {\n        const cjs_wrapper = Object.values(cb)[0]; // wrapper function\n        cjs_wrapper(module.exports, module); // inject exports, module\n        return module.exports;\n    };\n\n// add.js\nvar require_add = __commonJS({\n    &quot;add.js&quot;(exports, module) {\n        exports.add = (a, b) =&gt; a + b;\n    },\n});\n\n// minus.js\nvar require_minus = __commonJS({\n    &quot;minus.js&quot;(exports, module) {\n        exports.minus = (a, b) =&gt; a - b;\n    },\n});\n\n// math.js\nvar require_math = __commonJS({\n    &quot;math.js&quot;(exports, module) {\n        exports.calc = (a, b, operate) =&gt; {\n            const calc =\n                operate === &quot;+&quot; // condition require\n                    ? require_add().add(a, b)\n                    : require_minus().minus(a, b);\n            return calc(a, b);\n        };\n    },\n});\n\nexport default require_math(); // -&gt; { calc: Function }\n</code></pre>\n<p>这样即可利用 cjs 唯一导出的特性。可以看出，打包后的 esm 也导出单值（默认导出）。</p>\n<pre><code class=\"language-js\">const math = require(&quot;./math&quot;);\n//↓↓↓↓\nimport math from &quot;./math&quot;;\n\nconst { calc } = require(&quot;./math&quot;);\n//↓↓↓↓\nimport math from &quot;./math&quot;;\nconst { calc } = math;\n</code></pre>\n<p>cjs 本身可能是 esm 的编译结果，此时 <code>__esModule</code> 就起作用了。</p>\n<pre><code class=\"language-js\">// mod.cjs\nexports.default = 3;\n// mod-compiled.cjs\nexports.__esModule = true;\nexports.default = 3;\n\n// main1.mjs\nimport foo from &quot;./mod.cjs&quot;;\nimport bar from &quot;./mod-compiled.cjs&quot;;\nconsole.log(foo); // -&gt; { default: 3 }\nconsole.log(bar); // -&gt; 3\n\n// main2.mjs\nimport * as foo from &quot;./mod.cjs&quot;;\nimport * as bar from &quot;./mod-compiled.cjs&quot;;\nconsole.log(foo); // -&gt; { default: 3 }\nconsole.log(bar); // -&gt; { default: 3, __esModule: true }\n</code></pre>\n","tags":["js"]},{"id":"synchronization","url":"https://yieldray.fun/posts/synchronization","title":"同步","date_published":"2025-02-10T14:43:20.000Z","date_modified":"2025-02-10T14:43:20.000Z","content_text":"<h1>预定义</h1>\n<p>本文的目标是对一些<a href=\"https://zh.m.wikipedia.org/wiki/%E5%90%8C%E6%AD%A5\">同步</a>机制做一下高层次表述，附上一些抽象的实现。<br>如果从比较理论的角度来看待这些话题，一般就以资源为抽象视角，但这里的视角是<strong>物理内存</strong>。</p>\n<p>我们首先希望考虑在<strong>裸机</strong>上编程（以 risc-v 为例），所以是不存在操作系统的。</p>\n<p>因此这里定义一个类Linux的非常简单的模型：线程是调度的最小单位；进程只是共享内存的线程组。</p>\n<p><strong>调度</strong>简单来说，分为主动放弃CPU（暂时没有任务可以做了）和被动放弃CPU（如时间片耗尽）。<br>好的调度很复杂，还需要考虑不同的设计目标（比如实时/公平性），所以这里就从略了（包括比如锁唤醒等等调度场景也从略）。</p>\n<h2>目标</h2>\n<p>同步考虑的是一个<a href=\"https://zh.m.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E4%B8%80%E8%87%B4%E6%80%A7%E6%A8%A1%E5%9E%8B\">内存一致性</a>问题。</p>\n<p>把任务的执行抽象为两种：</p>\n<ul>\n<li>单核：任务可以被中断/继续（可以等价为<a href=\"https://zh.wikipedia.org/wiki/%E5%B9%B6%E5%8F%91%E8%AE%A1%E7%AE%97\"><strong>并发</strong></a>）</li>\n<li>多核：多个任务可以<a href=\"https://zh.m.wikipedia.org/wiki/%E4%BB%BB%E5%8A%A1%E5%B9%B6%E8%A1%8C\"><strong>并行</strong></a>，并允许访问同一块内存</li>\n</ul>\n<blockquote>\n<p>注：物理CPU有更复杂的机制，如：<a href=\"https://zh.wikipedia.org/wiki/%E5%8D%95%E6%8C%87%E4%BB%A4%E6%B5%81%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%B5%81\">SIMD</a>、<a href=\"https://zh.wikipedia.org/wiki/%E8%B6%85%E5%9F%B7%E8%A1%8C%E7%B7%92\">超线程</a>。这里只是简单探讨。</p>\n</blockquote>\n<p>目标是任务完成前，避免任务需要的某些内存被意外修改了。（任务一旦开始，往往说任务“持有”资源）</p>\n<h1>原语</h1>\n<h2>原子操作</h2>\n<pre><code class=\"language-c\">#include &lt;stdint.h&gt;\n\n// 原子加\nuint32_t atomic_add(volatile uint32_t *addr, uint32_t value) {\n  uint32_t result;\n  asm volatile (\n    &quot;amoadd.w %[result], %[value], (%[addr])&quot;\n    : [result] &quot;=r&quot; (result)\n    : [value] &quot;r&quot; (value), [addr] &quot;r&quot; (addr)\n    : &quot;memory&quot;\n  );\n  return result;\n}\n\n// 原子交换\nuint32_t atomic_swap(volatile uint32_t *addr, uint32_t value) {\n    uint32_t result;\n    asm volatile (\n        &quot;amoswap.w %[result], %[value], (%[addr])&quot;\n        : [result] &quot;=r&quot; (result)\n        : [value] &quot;r&quot; (value), [addr] &quot;r&quot; (addr)\n        : &quot;memory&quot;\n    );\n    return result;\n}\n\n// 原子比较并交换 (Compare and Swap - CAS)\nuint32_t atomic_compare_and_swap(volatile uint32_t *addr, uint32_t expected, uint32_t new_value) {\n    uint32_t result;\n    asm volatile (\n        &quot;1:  lr.w  %[result], (%[addr])\\n&quot;\n        &quot;    bne   %[result], %[expected], 2f\\n&quot;\n        &quot;    sc.w  %[result], %[new_value], (%[addr])\\n&quot;\n        &quot;    bnez  %[result], 1b\\n&quot;\n        &quot;2:&quot;\n        : [result] &quot;=&amp;r&quot; (result)\n        : [new_value] &quot;r&quot; (new_value), [expected] &quot;r&quot; (expected), [addr] &quot;r&quot; (addr)\n        : &quot;memory&quot;\n    );\n    return result; // 返回0表示成功，非0表示失败\n}\n</code></pre>\n<h2>互斥锁</h2>\n<pre><code class=\"language-c\">#include &lt;stdbool.h&gt;\n\ntypedef struct {\n  volatile uint32_t locked; // 0: unlocked, 1: locked\n} mutex_t;\n\nvoid mutex_init(mutex_t *mutex) {\n  mutex-&gt;locked = 0;\n}\n\nvoid mutex_lock(mutex_t *mutex) {\n  uint32_t expected = 0;\n  uint32_t new_value = 1;\n\n  // 自旋等待\n  while (atomic_compare_and_swap(&amp;mutex-&gt;locked, expected, new_value) != 0) {\n    yield(); // 切换到其它进程/线程\n  }\n}\n\nvoid mutex_unlock(mutex_t *mutex) {\n  mutex-&gt;locked = 0; // 直接赋值，因为解锁时我们知道它是锁定的\n  // 可选：加入内存屏障，确保解锁操作对其它核心可见\n  asm volatile (&quot;fence rw,w&quot; ::: &quot;memory&quot;);\n}\n</code></pre>\n<h2>信号量</h2>\n<pre><code class=\"language-c\">typedef struct {\n  volatile int32_t count;\n} semaphore_t;\n\nvoid semaphore_init(semaphore_t *sem, int32_t initial_count) {\n  sem-&gt;count = initial_count;\n}\n\nvoid semaphore_wait(semaphore_t *sem) {\n  while (1) {\n    int32_t current_count = sem-&gt;count;\n    if (current_count &gt; 0) {\n      if (atomic_compare_and_swap(&amp;sem-&gt;count, current_count, current_count - 1) == 0) {\n        break; // 获取信号量成功\n      }\n    } else {\n      // 信号量为0，需要阻塞当前线程\n      // ... (实现线程阻塞和加入等待队列的逻辑) ...\n      // 例如：\n      // block_current_thread(sem-&gt;wait_queue);\n      // yield(); // 切换到其它线程\n    }\n  }\n}\n\nvoid semaphore_signal(semaphore_t *sem) {\n  atomic_add(&amp;sem-&gt;count, 1);\n  // ... (唤醒等待队列中的一个线程) ...\n  // 例如：\n  // unblock_thread(sem-&gt;wait_queue);\n  // yield(); // 考虑是否需要切换线程\n}\n</code></pre>\n<h2>条件变量</h2>\n<pre><code class=\"language-c\">typedef struct {\n  // ... (等待队列等数据结构) ...\n} condvar_t;\n\nvoid condvar_init(condvar_t *cv) {\n  // ... (初始化等待队列) ...\n}\n\nvoid condvar_wait(condvar_t *cv, mutex_t *mutex) {\n  // 1. 释放互斥锁\n  mutex_unlock(mutex);\n\n  // 2. 将当前线程加入条件变量的等待队列\n  // ... (将当前线程加入 cv-&gt;wait_queue) ...\n  // block_current_thread(cv-&gt;wait_queue);\n\n  // 3. 调度到其它线程\n  // yield();\n\n  // 4. 重新获取互斥锁 (在线程被唤醒后)\n  mutex_lock(mutex);\n}\n\nvoid condvar_signal(condvar_t *cv) {\n  // 唤醒等待队列中的一个线程\n  // ... (从 cv-&gt;wait_queue 中移除一个线程并唤醒) ...\n  // unblock_thread(cv-&gt;wait_queue);\n}\n\nvoid condvar_broadcast(condvar_t *cv) {\n  // 唤醒等待队列中的所有线程\n  // ... (唤醒 cv-&gt;wait_queue 中的所有线程) ...\n}\n</code></pre>\n<h2>屏障</h2>\n<pre><code class=\"language-c\">// 假设的线程结构体 (根据操作系统实现定义)\ntypedef struct {\n    uint32_t thread_id;\n    // ... 其它线程相关信息 ...\n} thread_t;\n\ntypedef struct {\n    volatile uint32_t threshold; // 需要等待的线程数量\n    volatile uint32_t count;     // 当前已到达屏障的线程数量\n    volatile uint32_t phase;     // 屏障的阶段 (用于唤醒线程)\n    // 等待队列 (用于存放被阻塞的线程)\n    // ... (需要实现一个线程等待队列) ...\n} barrier_t;\n\nvoid barrier_init(barrier_t *barrier, uint32_t threshold) {\n    barrier-&gt;threshold = threshold;\n    barrier-&gt;count = 0;\n    barrier-&gt;phase = 0;\n    // 初始化等待队列\n    // ... (barrier-&gt;wait_queue = create_wait_queue();) ...\n}\n\nvoid barrier_wait(barrier_t *barrier) {\n    uint32_t current_phase = barrier-&gt;phase;\n\n    // 原子性地增加计数器\n    uint32_t new_count = atomic_add(&amp;barrier-&gt;count, 1) + 1; // atomic_add 返回的是旧值，所以要 + 1\n\n    if (new_count == barrier-&gt;threshold) {\n        // 最后一个到达的线程负责重置屏障并唤醒所有等待的线程\n\n        // 重置计数器\n        barrier-&gt;count = 0;\n\n        // 增加屏障阶段 (用于唤醒等待的线程)\n        atomic_add(&amp;barrier-&gt;phase, 1);\n\n        // 唤醒所有等待的线程\n        // ... unblock_all_threads(barrier-&gt;wait_queue); ...\n        // ... yield(); ... // 考虑是否需要切换线程\n        // 实际情况可能需要遍历等待队列，逐个唤醒线程\n    } else {\n        // 其它线程需要等待\n\n        // 将当前线程加入等待队列\n        // ... block_current_thread(barrier-&gt;wait_queue); ...\n\n        // 调度到其它线程\n        // ... yield(); ...\n\n        // 当线程被唤醒时，它会从这里继续执行\n        // 确保线程是在正确的阶段被唤醒\n        while (barrier-&gt;phase == current_phase) {\n            // 自旋等待，直到屏障进入下一个阶段\n            // 这种情况是为了防止假唤醒 (spurious wakeups)\n            // 可以加入 yield() 或其它让步操作，避免过度占用CPU\n            // 例如： asm volatile (&quot;pause&quot; ::: &quot;memory&quot;);\n        }\n    }\n}\n</code></pre>\n<h2>读写锁</h2>\n<pre><code class=\"language-c\">typedef struct {\n  volatile uint32_t writer;   // 0: no writer, 1: writer present\n  volatile uint32_t readers;  // Number of active readers\n  // ... (等待队列，用于阻塞写线程和读线程) ...\n} rwlock_t;\n\nvoid rwlock_init(rwlock_t *rwlock) {\n  rwlock-&gt;writer = 0;\n  rwlock-&gt;readers = 0;\n  // ... (初始化等待队列) ...\n}\n\nvoid rwlock_read_lock(rwlock_t *rwlock) {\n  while (1) {\n    // 检查是否有写线程正在写\n    if (rwlock-&gt;writer == 0) {\n      uint32_t current_readers = rwlock-&gt;readers;\n      if (atomic_compare_and_swap(&amp;rwlock-&gt;readers, current_readers, current_readers + 1) == 0) {\n        // 获取读锁成功\n        break;\n      }\n    } else {\n      // 有写线程，需要阻塞当前读线程\n      // ... (将当前线程加入读等待队列) ...\n      // block_current_thread(rwlock-&gt;read_wait_queue);\n      // yield();\n    }\n  }\n}\n\nvoid rwlock_read_unlock(rwlock_t *rwlock) {\n  atomic_add(&amp;rwlock-&gt;readers, -1);\n  // ... (唤醒写等待队列中的一个线程，如果队列不为空) ...\n  // if (rwlock-&gt;write_wait_queue is not empty) {\n  //   unblock_thread(rwlock-&gt;write_wait_queue);\n  //   yield(); // 考虑是否需要切换线程\n  // }\n}\n\nvoid rwlock_write_lock(rwlock_t *rwlock) {\n  while (1) {\n    // 检查是否有读线程或写线程正在访问\n    if (rwlock-&gt;writer == 0 &amp;&amp; rwlock-&gt;readers == 0) {\n      if (atomic_compare_and_swap(&amp;rwlock-&gt;writer, 0, 1) == 0) {\n        // 获取写锁成功\n        break;\n      }\n    } else {\n      // 有读线程或写线程，需要阻塞当前写线程\n      // ... (将当前线程加入写等待队列) ...\n      // block_current_thread(rwlock-&gt;write_wait_queue);\n      // yield();\n    }\n  }\n}\n\nvoid rwlock_write_unlock(rwlock_t *rwlock) {\n  rwlock-&gt;writer = 0;\n  // ... (唤醒读等待队列中的所有线程，如果队列不为空) ...\n  // unblock_all_threads(rwlock-&gt;read_wait_queue);\n  // ... (唤醒写等待队列中的一个线程，如果队列不为空) ...\n  // if (rwlock-&gt;write_wait_queue is not empty) {\n  //   unblock_thread(rwlock-&gt;write_wait_queue);\n  // }\n  // yield(); // 考虑是否需要切换线程\n}\n</code></pre>\n<h2>自旋锁</h2>\n<pre><code class=\"language-c\">typedef struct {\n  volatile uint32_t locked; // 0: unlocked, 1: locked\n} spinlock_t;\n\nvoid spinlock_init(spinlock_t *lock) {\n  lock-&gt;locked = 0;\n}\n\nvoid spinlock_lock(spinlock_t *lock) {\n  uint32_t expected = 0;\n  uint32_t new_value = 1;\n\n  while (atomic_compare_and_swap(&amp;lock-&gt;locked, expected, new_value) != 0) {\n    // 自旋等待\n    expected = 0; // 每次失败后重置expected\n    // 可以考虑加入 yield() 或其它让步操作，避免过度占用CPU\n    // 例如： asm volatile (&quot;pause&quot; ::: &quot;memory&quot;); // RISC-V 的 pause 指令\n  }\n}\n\nvoid spinlock_unlock(spinlock_t *lock) {\n  lock-&gt;locked = 0;\n  asm volatile (&quot;fence rw,w&quot; ::: &quot;memory&quot;); // 确保解锁操作对其它核心可见\n}\n</code></pre>\n<h2>可重入锁</h2>\n<pre><code class=\"language-c\">#include &lt;stdint.h&gt;\n#include &lt;stdbool.h&gt;\n\n// 假设的线程结构体 (需要根据你的操作系统实现定义)\ntypedef struct {\n    uint32_t thread_id;\n    // ... 其他线程相关信息 ...\n} thread_t;\n\ntypedef struct {\n    volatile uint32_t owner_thread_id; // 持有锁的线程 ID (0 表示未持有)\n    volatile uint32_t recursion_count;   // 递归计数器\n    // 等待队列 (用于存放被阻塞的线程)\n    // ... (需要实现一个线程等待队列) ...\n} reentrant_lock_t;\n\nvoid reentrant_lock_init(reentrant_lock_t *lock) {\n    lock-&gt;owner_thread_id = 0;\n    lock-&gt;recursion_count = 0;\n    // 初始化等待队列\n    // ... lock-&gt;wait_queue = create_wait_queue(); ...\n}\n\nvoid reentrant_lock_lock(reentrant_lock_t *lock) {\n    uint32_t current_thread_id = get_current_thread_id(); // 获取当前线程 ID\n\n    // 循环尝试获取锁，直到成功\n    while (1) {\n        // 检查锁是否被持有\n        if (lock-&gt;owner_thread_id == 0) {\n            // 锁未被持有，尝试获取\n            if (atomic_compare_and_swap(&amp;lock-&gt;owner_thread_id, 0, current_thread_id) == 0) {\n                // 获取锁成功\n                lock-&gt;recursion_count = 1;\n                return;\n            }\n            // 获取锁失败，重试\n        } else if (lock-&gt;owner_thread_id == current_thread_id) {\n            // 锁已被当前线程持有，增加递归计数器\n            lock-&gt;recursion_count++;\n            return;\n        } else {\n            // 锁已被其他线程持有，阻塞等待\n            // ... block_current_thread(lock-&gt;wait_queue); ...\n            // ... yield(); ...\n        }\n    }\n}\n\nvoid reentrant_lock_unlock(reentrant_lock_t *lock) {\n    uint32_t current_thread_id = get_current_thread_id(); // 获取当前线程 ID\n\n    // 检查当前线程是否持有锁\n    if (lock-&gt;owner_thread_id != current_thread_id) {\n        // 当前线程不持有锁，报错\n        // ... error_handler(&quot;Current thread does not own the lock&quot;); ...\n        return;\n    }\n\n    // 减少递归计数器\n    lock-&gt;recursion_count--;\n\n    // 检查递归计数器是否为 0\n    if (lock-&gt;recursion_count == 0) {\n        // 释放锁\n        lock-&gt;owner_thread_id = 0;\n\n        // 唤醒等待线程\n        // ... unblock_all_threads(lock-&gt;wait_queue); ...\n        // ... yield(); ...\n    }\n}\n</code></pre>\n<h1>概念</h1>\n<h2>死锁</h2>\n<p>同时满足：互斥 请求与保持 不可剥夺 循环等待</p>\n<ul>\n<li><strong>预防死锁：</strong> 破坏死锁产生的四个必要条件中的一个或多个。<ul>\n<li><strong>破坏互斥条件：</strong> 尽量减少对独占资源的使用，例如使用可重入锁。 但很多资源本身就是互斥的，所以这个条件很难破坏。</li>\n<li><strong>破坏请求与保持条件：</strong> 线程在请求资源时，要么一次性获得所有需要的资源，要么不请求任何资源。 实现起来比较困难，因为线程可能无法预知需要哪些资源。</li>\n<li><strong>破坏不可剥夺条件：</strong> 当线程请求新的资源无法满足时，主动释放已持有的资源。 这种方法可能会导致线程频繁释放和获取资源，增加开销。</li>\n<li><strong>破坏循环等待条件：</strong> 对所有资源进行排序，线程按照固定的顺序请求资源。 需要对资源进行统一管理</li>\n</ul>\n</li>\n<li><strong>避免死锁：</strong> 使用算法来动态地检测和避免死锁的发生，例如银行家算法。<ul>\n<li><strong>银行家算法：</strong> 模拟操作系统分配资源的过程，在分配资源之前，判断分配后系统是否处于安全状态（即所有线程都能完成）。 算法复杂度较高，需要维护资源分配信息。</li>\n</ul>\n</li>\n<li><strong>检测死锁：</strong> 定期检测系统中是否存在死锁，如果发现死锁，则采取措施解除死锁。<ul>\n<li><strong>资源分配图：</strong> 通过资源分配图来检测是否存在环路，如果存在环路，则说明存在死锁。 适用于资源数量较少情况。</li>\n<li><strong>超时机制：</strong> 设置锁的超时时间，当线程获取锁的时间超过超时时间时，认为发生了死锁。 可能会误判，因为线程可能只是因为某些原因阻塞了。</li>\n</ul>\n</li>\n<li><strong>解除死锁：</strong> 可能会导致数据丢失或不一致。<ul>\n<li><strong>终止线程：</strong> 终止一个或多个死锁中的线程，释放其占用的资源</li>\n<li><strong>资源剥夺：</strong> 强制剥夺一个或多个线程占用的资源，分配给其他线程</li>\n</ul>\n</li>\n</ul>\n<h2>活锁</h2>\n<p>线程不断尝试获取资源，但由于其他线程也在不断尝试，导致所有线程都无法取得进展。</p>\n<ul>\n<li>不合理的重试机制： 线程在请求资源失败后，立即重试，而没有引入任何延迟或随机性，导致它们始终在竞争同一资源。</li>\n<li>优先级反转： 高优先级线程不断被低优先级线程抢占，导致高优先级线程无法获得资源，从而陷入活锁。</li>\n</ul>\n<p>解决：</p>\n<ul>\n<li>引入随机性： 在重试之前引入随机的延迟，避免所有线程同时重试。</li>\n<li>优先级调整： 调整线程的优先级，让某些线程优先获得资源。</li>\n<li>退避算法： 使用退避算法，例如指数退避，逐渐增加重试的延迟时间。</li>\n</ul>\n<h2>饥饿</h2>\n<p>一个或多个线程长时间无法获得所需资源，导致无法执行。</p>\n<p>解决：</p>\n<ul>\n<li>公平锁： 使用公平锁，保证线程按照请求资源的顺序获得资源。</li>\n<li>优先级反转控制： 解决高优先级线程被低优先级线程阻塞的问题。 例如，当高优先级线程等待低优先级线程释放资源时，临时提升低优先级线程的优先级。</li>\n<li>资源预留： 为某些线程预留资源，保证它们能够及时获得所需资源。</li>\n<li>避免长时间占用资源： 线程在使用资源时，尽量避免长时间占用，及时释放资源。</li>\n</ul>\n<h2>乐观锁/悲观锁</h2>\n<table>\n<thead>\n<tr>\n<th>特性</th>\n<th>乐观锁</th>\n<th>悲观锁</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>假设</td>\n<td>数据很少被修改</td>\n<td>数据总是会被修改</td>\n</tr>\n<tr>\n<td>加锁时机</td>\n<td>更新时检查数据是否被修改</td>\n<td>读取时加锁，防止其他线程修改数据</td>\n</tr>\n<tr>\n<td>冲突处理</td>\n<td>重试 (自旋)</td>\n<td>阻塞等待锁释放</td>\n</tr>\n<tr>\n<td>适用场景</td>\n<td>读多写少，冲突概率低</td>\n<td>写多读少，冲突概率高</td>\n</tr>\n<tr>\n<td>优点</td>\n<td>并发性能高，锁开销小</td>\n<td>保证数据一致性</td>\n</tr>\n<tr>\n<td>缺点</td>\n<td>存在 ABA 问题，自旋开销，冲突概率高</td>\n<td>并发性能低，锁开销大</td>\n</tr>\n</tbody></table>\n<h1>异步</h1>\n<blockquote>\n<p>同步机制之后有必要简单考虑一下异步机制</p>\n</blockquote>\n<p>上述同步机制中，任务在运行时获取的是所需资源的原始内存。若改成获取资源内存的拷贝，即消息机制（或事件通信），则本文不称为同步。</p>\n<p>因此我们可以再对同步加以定义：即同步时，任务需锁定（或者说持有）资源的原始内存。</p>\n<p>异步机制总是需要需要额外的内存，这存在以下情况：</p>\n<ol>\n<li>拷贝资源：拷贝资源的原始内存，内存开销较大，后续还需考虑写回原始内存。</li>\n<li>传递消息：需要通过不同的标识符区分事件，至少需要额外线性内存空间。</li>\n</ol>\n<p>同步机制虽然也需要额外内存，但可以发现，所需的都是常数级别内存，内存开销小。</p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>同步</th>\n<th>异步</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>资源排布</td>\n<td>场景越复杂就需要做越复杂的资源安排</td>\n<td>资源安排较为简单</td>\n</tr>\n<tr>\n<td>内存开销</td>\n<td>内存开销小</td>\n<td>内存开销做不到最小</td>\n</tr>\n</tbody></table>\n<p>至于时间开销，则根据不同场景和异步机制而不同，本文略。</p>\n<h1>分布式</h1>\n<p>如果我们的观察维度是任务，那么<a href=\"https://zh.wikipedia.org/wiki/%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%A1%E7%AE%97\">分布式（计算）</a>，即联机（而非单机）计算任务结果，也与同步有关。</p>\n<p>联机情况下需要考虑的有单点故障（物理机器故障，如物理磁盘和内存损坏），网络故障（影响通信，如数据包丢失、延迟、乱序、重复），负载均衡，弹性扩容等。 此外，由于分布式系统中的各个节点拥有独立的时钟，因此还存在时钟不同步的问题，这可能导致事件顺序判断错误。 分布式同步需要处理这些额外的复杂性，常用的技术包括：</p>\n<ul>\n<li><strong>分布式锁:</strong> 用于协调多个节点对共享资源的访问。 实现方式有基于 ZooKeeper, etcd, Redis 等。</li>\n<li><strong>分布式共识算法:</strong> 确保多个节点对某个值或操作顺序达成一致。 常见的算法有 Paxos, Raft, Zab 等。</li>\n<li><strong>两阶段提交 (2PC) / 三阶段提交 (3PC):</strong> 用于实现分布式事务，保证多个节点上的操作要么全部成功，要么全部失败。</li>\n<li><strong>向量时钟 / Lamport 时间戳:</strong> 用于在分布式系统中确定事件的偏序关系，解决时钟不同步问题。</li>\n<li><strong>消息队列:</strong> 通过异步消息传递来解耦各个节点，提高系统的可用性和可伸缩性. 常见的消息队列有 RabbitMQ, Kafka, ActiveMQ 等。</li>\n</ul>\n<p>分布式计算与单机上的编程必然是完全不同的。可以说，很多解决方案是在通过协调协议来模拟非分布式系统，对分布式本身的控制减少了。</p>\n<h1><a href=\"https://zh.m.wikipedia.org/wiki/%E6%97%A0%E5%86%B2%E7%AA%81%E5%A4%8D%E5%88%B6%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B\">CRDT</a></h1>\n<p>在分布式计算中，同时更新数据的多个副本（复制数据的并发更新）而不进行协调可能导致不一致和冲突。分布式的一些通用解决方案的开销可能比较大。</p>\n<p>无冲突复制数据类型（CRDT）是一种特殊的数据结构，其数学特性保证了并发更新的无冲突合并，从而确保最终一致性，无需协调。基于此，乐观复制允许所有更新，并在稍后解决潜在的不一致。这种模式直接针对数据冲突本身，因此可以说对是否分布式都适用，前提是数据能通过此种结构表示。（解决同步的方法就是不同步？）</p>\n","tags":["os"]},{"id":"react-some-patterns-and-optimization","url":"https://yieldray.fun/posts/react-some-patterns-and-optimization","title":"React：一些模式、优化及其实现","date_published":"2025-01-05T19:02:45.000Z","date_modified":"2025-01-05T19:02:45.000Z","content_text":"<h1>控制反转：Render Props 与 React Hooks</h1>\n<blockquote>\n<p>在 vue3 中，Render Props 可以通过 <a href=\"https://cn.vuejs.org/guide/components/slots.html#scoped-slots\">作用域插槽</a> 模拟。<br>vue3 的 作用域插槽 与 组合式API 和这里探讨的 Render Props 与 React Hooks 是类似的。</p>\n</blockquote>\n<p>Render Props 是在<a href=\"https://legacy.reactjs.org/docs/render-props.html\">旧版 React 文档</a>就已经存在的一种模式。<br>其本质可以说是一种<a href=\"https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC\">控制反转（IoC）</a>，因为在这种模式中，渲染权交给了使用方。</p>\n<pre><code class=\"language-tsx\">function Toggle({\n    render,\n    initialOn = false,\n}: {\n    render: (props: { toggle: VoidFunction; on: boolean }) =&gt; React.ReactNode;\n}) {\n    const [on, setOn] = useState(initialOn);\n    return render({\n        toggle: () =&gt; setOn(!on),\n        on,\n    });\n}\n\nfunction App() {\n    return (\n        &lt;Toggle\n            render={({ toggle, on }) =&gt; {\n                return &lt;button onClick={toggle}&gt;{on ? &quot;on&quot; : &quot;off&quot;}&lt;/button&gt;;\n            }}\n        /&gt;\n    );\n}\n</code></pre>\n<p>在只有 <em>类组件</em> 的时代，上例也可以使用类组件实现。</p>\n<p>另一种方式是抽象为 hooks，hooks 的特点是完全没有 <em>渲染权</em>。</p>\n<pre><code class=\"language-js\">function useToggle(initialOn = false) {\n    const [on, setOn] = useState(initialOn);\n    const toggle = () =&gt; setOn(!on);\n    return { on, toggle };\n}\n</code></pre>\n<p>此时 render props 又可以看成是对 hooks 的抽象：</p>\n<pre><code class=\"language-tsx\">function Toggle({\n    render,\n    initialOn = false,\n}: {\n    render: (props: { toggle: VoidFunction; on: boolean }) =&gt; React.ReactNode;\n}) {\n    const props = useToggle(initialOn); // 1. 调用 hooks 得到 props\n    return render(props); // 2. 在 jsx 中消费 props\n}\n</code></pre>\n<p>Custom hooks 确实可以替代 Render props，但使用 hooks 时需要两步：</p>\n<ol>\n<li>调用 hooks 得到 props</li>\n<li>在 jsx 中消费 props</li>\n</ol>\n<p>此时，状态和 UI 是分离的。而 render props 由于拥有渲染权，只需要在 jsx 中消费状态就可以了。<br>这可以说是使用 render props 的优势，实际使用时权衡利弊即可。</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-light-bulb mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>\n<p>新版 React 文档中也写了一些逻辑复用方法，参见<a href=\"https://zh-hans.react.dev/reference/react/cloneElement#alternatives\">这里</a></p>\n</div>\n<h1>通过 Radix UI 得到一些启发</h1>\n<p>Radix UI 是现在很流行的组件库，其使用方式是非常组合式的，即组件由多个子组件组合而成。</p>\n<p>以 Dialog/Modal 组件为例：</p>\n<pre><code class=\"language-jsx\">import React from &quot;react&quot;;\nimport * as Dialog from &quot;@radix-ui/react-dialog&quot;;\nimport { Cross2Icon } from &quot;@radix-ui/react-icons&quot;;\nimport &quot;./styles.css&quot;;\n\nconst DialogDemo = () =&gt; (\n    &lt;Dialog.Root&gt;\n        &lt;Dialog.Trigger asChild&gt;\n            &lt;button&gt;Open Dialog&lt;/button&gt;\n        &lt;/Dialog.Trigger&gt;\n        &lt;Dialog.Portal&gt;\n            &lt;Dialog.Overlay /&gt;\n            &lt;Dialog.Content&gt;\n                &lt;Dialog.Title&gt;Title&lt;/Dialog.Title&gt;\n                &lt;Dialog.Description&gt;Description&lt;/Dialog.Description&gt;\n                &lt;Dialog.Close asChild&gt;\n                    &lt;button&gt;Close Dialog&lt;/button&gt;\n                &lt;/Dialog.Close&gt;\n            &lt;/Dialog.Content&gt;\n        &lt;/Dialog.Portal&gt;\n    &lt;/Dialog.Root&gt;\n);\n</code></pre>\n<blockquote>\n<p>也可以看出 Radix UI 默认情况是 Uncontrolled 的，本文对此探讨略。</p>\n</blockquote>\n<p>在不使用 context 的情况下，组件设计者可能将组件的各个部分使用 props 承载，这样只有一个组件：</p>\n<pre><code class=\"language-tsx\">import React, { useState } from &quot;react&quot;;\nimport { Button, Modal } from &quot;antd&quot;;\n\nconst App = () =&gt; {\n    const [open, setOpen] = useState(false);\n    const showModal = () =&gt; setOpen(true);\n    const handleCancel = () =&gt; setOpen(false);\n\n    return (\n        &lt;&gt;\n            &lt;Button type=&quot;primary&quot; onClick={showModal}&gt;\n                Open Modal\n            &lt;/Button&gt;\n            &lt;Modal\n                open={open}\n                onCancel={handleCancel}\n                title=&quot;Title&quot;\n                footer={[\n                    &lt;Button key=&quot;back&quot; onClick={handleCancel}&gt;\n                        Close\n                    &lt;/Button&gt;,\n                    &lt;Button key=&quot;submit&quot; type=&quot;primary&quot;&gt;\n                        Submit\n                    &lt;/Button&gt;,\n                ]}\n            &gt;\n                Description\n            &lt;/Modal&gt;\n        &lt;/&gt;\n    );\n};\n</code></pre>\n<p>Radix UI 这种方式在于，其根组件用于<strong>创建状态并作为 context 提供</strong>：</p>\n<p>Source: <a href=\"https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/src/Dialog.tsx\">https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/src/Dialog.tsx</a></p>\n<pre><code class=\"language-tsx\">const Dialog: React.FC&lt;DialogProps&gt; = (props: ScopedProps&lt;DialogProps&gt;) =&gt; {\n    const { __scopeDialog, children, open: openProp, defaultOpen, onOpenChange, modal = true } = props;\n    const triggerRef = React.useRef&lt;HTMLButtonElement&gt;(null);\n    const contentRef = React.useRef&lt;DialogContentElement&gt;(null);\n    const [open = false, setOpen] = useControllableState({\n        prop: openProp,\n        defaultProp: defaultOpen,\n        onChange: onOpenChange,\n    });\n\n    return (\n        &lt;DialogProvider\n            scope={__scopeDialog}\n            triggerRef={triggerRef}\n            contentRef={contentRef}\n            contentId={useId()}\n            titleId={useId()}\n            descriptionId={useId()}\n            open={open}\n            onOpenChange={setOpen}\n            onOpenToggle={React.useCallback(() =&gt; setOpen((prevOpen) =&gt; !prevOpen), [setOpen])}\n            modal={modal}\n        &gt;\n            {children}\n        &lt;/DialogProvider&gt;\n    );\n};\n</code></pre>\n<p>观察其实现可以发现，radix ui 每个组件是通过 <code>createContextScope</code> 来创建 context。<br>这种“作用域”机制是为了保证根组件创建的 context 只被自身类型的组件消费。</p>\n<p>Source：<a href=\"https://github.com/radix-ui/primitives/blob/main/packages/react/context/src/createContext.tsx\">https://github.com/radix-ui/primitives/blob/main/packages/react/context/src/createContext.tsx</a></p>\n<pre><code class=\"language-tsx\">import * as React from &quot;react&quot;;\n\nfunction createContext&lt;ContextValueType extends object | null&gt;() {\n    // ...\n    return [Provider, useContext] as const;\n}\n\ntype Scope&lt;C = any&gt; = { [scopeName: string]: React.Context&lt;C&gt;[] } | undefined;\ntype ScopeHook = (scope: Scope) =&gt; { [__scopeProp: string]: Scope };\ninterface CreateScope {\n    scopeName: string;\n    (): ScopeHook;\n}\n\nfunction createContextScope(scopeName: string, createContextScopeDeps: CreateScope[] = []) {\n    // ...\n    return [createContext, composeContextScopes(createScope, ...createContextScopeDeps)] as const;\n}\n\nfunction composeContextScopes(...scopes: CreateScope[]) {\n    // ...\n    return createScope;\n}\n\nexport { createContext, createContextScope };\nexport type { CreateScope, Scope };\n</code></pre>\n<p>另一个模式是 asChild，这是通过 <a href=\"https://www.radix-ui.com/primitives/docs/utilities/slot\">Slot</a> 组件实现的。它将传给 Slot 的 props 都转发给其直接子元素。</p>\n<p>其实现实际上很简单，但要注意正确合并 event handlers 和 ref，另外还对 className 和 style 的合并有特殊处理。<br>Source: <a href=\"https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\">https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx</a></p>\n<p>下面给出一个简单实现：</p>\n<pre><code class=\"language-tsx\">function Slot({\n    children,\n    ...props\n}: React.HTMLAttributes&lt;HTMLElement&gt; &amp; {\n    children?: React.ReactNode;\n}) {\n    if (React.isValidElement(children)) {\n        return React.cloneElement(children, {\n            ...props,\n            ...children.props,\n        });\n    }\n    if (React.Children.count(children) &gt; 1) {\n        React.Children.only(null);\n    }\n    return null;\n}\n</code></pre>\n<h1>状态粒度化：避免不必要重渲染</h1>\n<p>以 <a href=\"https://github.com/pmndrs/zustand#selecting-multiple-state-slices\">zustand</a> 为例，它使状态粒度变小的方法是传入一个状态选择器函数：</p>\n<pre><code class=\"language-ts\">// 粒度大\nconst state = useBearStore();\n\n// 粒度小\nconst nuts = useBearStore((state) =&gt; state.nuts);\nconst honey = useBearStore((state) =&gt; state.honey);\n</code></pre>\n<p>Source（见 createImpl、useStore 实现）：<a href=\"https://github.com/pmndrs/zustand/blob/main/src/react.ts\">https://github.com/pmndrs/zustand/blob/main/src/react.ts</a><br>Source（见 useStore 实现）：<a href=\"https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts\">https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts</a></p>\n<p>下面给出一个简单实现：</p>\n<pre><code class=\"language-ts\">function createStore&lt;T&gt;(initialState: T) {\n    let state = initialState;\n    const listeners = new Set&lt;VoidFunction&gt;();\n\n    const subscribe = (listener: VoidFunction) =&gt; {\n        listeners.add(listener);\n        return () =&gt; listeners.delete(listener);\n    };\n\n    const setState = (newState: T) =&gt; {\n        state = newState;\n        listeners.forEach((listener) =&gt; listener());\n    };\n\n    const getState = () =&gt; state;\n\n    const useStoreWithSelector = &lt;U&gt;(selector: (state: T) =&gt; U) =&gt; {\n        return React.useSyncExternalStore(\n            subscribe,\n            () =&gt; selector(getState()),\n            () =&gt; selector(initialState),\n        );\n    };\n\n    return { useStoreWithSelector, setState, getState };\n}\n</code></pre>\n<p>使用选择器函数还是有些繁琐，类似 @vue/reactivity 或 <a href=\"https://preactjs.com/guide/v10/signals/\">signal</a> 机制可能更简单。\n对此 react 生态中也有 <a href=\"https://valtio.dev/\">valtio</a> 这样基于 Proxy 的状态管理。</p>\n<p>不妨借助 Proxy 来减小状态粒度，下面修改自<a href=\"https://github.com/pacocoursey/state\">这里</a>：</p>\n<pre><code class=\"language-ts\">function create&lt;T extends object&gt;(initial: T) {\n    const state = initial;\n    type Listener = (state: T, key: keyof T) =&gt; void;\n    const listeners = new Set&lt;Listener&gt;();\n\n    function subscribe(listener: Listener) {\n        listeners.add(listener);\n        return () =&gt; listeners.delete(listener);\n    }\n\n    function setState&lt;K extends keyof T&gt;(key: K, value: T[K]) {\n        if (!Object.is(state[key], value)) state[key] = value;\n        listeners.forEach((listener) =&gt; listener(state, key));\n    }\n\n    function useStore() {\n        const rerender = useState()[1];\n        const tracked = useRef({} as Record&lt;keyof T, boolean&gt;);\n        const stateRef = useRef(state);\n\n        useLayoutEffect(\n            subscribe((_, key) =&gt; {\n                if (tracked.current[key]) rerender({});\n            }),\n            [],\n        );\n\n        return useMemo(() =&gt; {\n            stateRef.current = state;\n\n            return new Proxy({} as T, {\n                get(_, property) {\n                    tracked.current[property] = true;\n                    return stateRef.current[property];\n                },\n                set(_, property, value) {\n                    setState(property as keyof T, value);\n                    return true;\n                },\n            });\n        }, []);\n    }\n\n    return useStore;\n}\n</code></pre>\n<blockquote>\n<p>WIP：本章待改进</p>\n</blockquote>\n","tags":["react"]},{"id":"boot-process-android-vs-linux","url":"https://yieldray.fun/posts/boot-process-android-vs-linux","title":"BOOT PROCESS: ANDROID vs. LINUX\n","date_published":"2024-12-28T19:49:35.000Z","date_modified":"2024-12-28T19:49:35.000Z","content_text":"<blockquote>\n<p>翻译自： <a href=\"https://xdaforums.com/t/info-boot-process-android-vs-linux.3785254/\">https://xdaforums.com/t/info-boot-process-android-vs-linux.3785254/</a></p>\n</blockquote>\n<p><strong>注意：</strong>\n<strong><em>我并非开发人员或 Android 专家。此处提供的所有信息均来自不同的互联网来源，并且是尽我所知悉的。对于因使用本文信息而对您或您的设备造成的任何损害，我概不负责。</em></strong></p>\n<h1>1. PC 启动过程</h1>\n<p>在深入了解 Android 的启动过程之前，我们先来看看 Linux PC 的情况。</p>\n<ul>\n<li>按下电源按钮</li>\n<li>开机自检（POST）：识别存在的设备并报告任何问题</li>\n<li>BIOS / UEFI</li>\n<li>必要的硬件初始化（键盘、磁盘等）</li>\n<li>磁盘（MBR）</li>\n<li>DOS 兼容区域代码（可选）</li>\n<li>引导加载程序</li>\n<li>活动/启动分区（引导扇区）</li>\n<li>内核</li>\n<li>Initrd / initramfs (init)</li>\n<li>服务/守护进程/进程</li>\n</ul>\n<p>BIOS / UEFI 是硬编码在主板上的第一个软件代码，在我们按下电源按钮后运行。BIOS 在处理器的实模式（16 位）下运行，因此它无法寻址超过 2^20 字节的 RAM，即例程无法访问超过 1 MiB 的 RAM，这是一个严格的限制和主要的不便之处。</p>\n<p>创建分区时，MBR 保存在 LBA0 中，GPT 标头保存在 LBA1 中，主 GPT 保存在 LBA2-33 中，LBA34（第 35 个）是第一个可用的扇区。备份或辅助 GPT 保存在最后 33 个 LBA 中，操作系统最后一个可用的扇区是（总 LBA 数 - 33）。分区软件将 GPT 分区对齐在较大的边界上，例如在 2,048 的倍数的 LBA 上对齐到 1,048,576 字节（512 字节 * 2048 = 1 MiB）的边界。因此，第一个分区的第一个扇区是 LBA 2048，以此类推。</p>\n<p>当系统启动时，必须将文件系统的驱动程序加载到 RAM 中才能使用该文件系统，但驱动程序本身是一个文件，位于某个文件系统内部。这就像一个鸡生蛋蛋生鸡的场景。因此，解决方案是始终加载（作为 BIOS/UEFI 标准）可启动存储器上的第一个扇区（在较旧的方案中为 0/0/1 C/H/S，在较新的方案中为 LBA0），它是（传统的或保护性的）MBR。BIOS/UEFI 和存储介质之间的这种通信是通过特定于主机控制器的命令进行的，例如 PC 上具有 SATA/AHCI 接口的设备的 ATA 命令。</p>\n<p><strong>主引导记录（MBR）</strong></p>\n<ul>\n<li>第一个有效磁盘起始处的第一个 512 字节（1 个扇区）</li>\n<li>引导代码（446 字节）+ 分区表（64 字节）</li>\n<li>可执行代码：引导加载程序的第一阶段扫描分区表并找到活动分区的第一个扇区（或可能指向中间阶段）</li>\n<li>分区表提供有关活动/可启动分区（以及所有其他分区）的信息</li>\n<li>64 字节的小尺寸将最大（主）分区数限制为 4 个</li>\n<li>由于引导加载程序尚无法理解文件系统（inode 等），因此 MBR 本身是可执行的</li>\n<li>最后 2 个字节是引导签名，即立即查找磁盘/驱动器是否可启动，从而切换到下一个</li>\n</ul>\n<p><strong>DOS 兼容区域</strong></p>\n<ul>\n<li>此阶段特定于传统的 GRUB，GRUB 2（大多数现代 Linux 发行版上的默认引导加载程序）将此阶段分为第 2 阶段和第 3 阶段</li>\n<li>MBR 旁边的 31.5 KiB / 63 个扇区，包含文件系统实用程序</li>\n<li>仍然由 BIOS 例程加载（或引导加载程序可以使用其自己的驱动程序）</li>\n<li>某些硬件需要此阶段，或者如果 &quot;/boot&quot; 分区（包含第 2 阶段的扇区）位于磁盘的 1024 个柱面头之上，或者如果使用 LBA 模式</li>\n</ul>\n<p><strong>卷引导记录（VBR）/分区引导记录（PBR）</strong></p>\n<ul>\n<li>扇区号 63（第 64 个扇区）及以上可能包含卷引导记录或分区 BR，与 MBR 非常相似</li>\n<li>也称为卷引导扇区，它可能是任何分区上的第一个引导扇区</li>\n<li>NTFS 将 VBR 保存为元数据文件名称 $Boot（位于第一个簇上），其中还包含文件 $MFT 的簇号。$MFT 描述卷上的所有文件；文件名、时间戳、流名称、数据流所在的簇号列表、索引、安全标识符 (SID) 和文件属性，如“只读”、“压缩”、“加密”等。</li>\n<li>如果磁盘未分区，则是磁盘的第一个引导扇区</li>\n</ul>\n<p><strong>引导分区（如果存在）</strong></p>\n<ul>\n<li><p>在 MBR 方案中，可以使用标志将分区标记为可启动/活动，通常是磁盘的第一个分区</p>\n</li>\n<li><p>Windows 第一阶段引导加载程序仅从 MBR 分区表中读取和加载“活动分区”</p>\n</li>\n<li><p>引导扇区或 VBR/PBR 由第一阶段或 1.5 阶段（GRUB2 上为 2 或 3）引导加载程序读取，该引导加载程序加载第二阶段（GRUB2 上为 4）或实际的引导加载程序</p>\n</li>\n<li><p>MBR / VBR 包含：</p>\n<ul>\n<li>跳转指令（前 3 个字节），即“goto 引导代码”命令</li>\n<li>文件系统标头</li>\n<li>可执行引导代码，通常包含用于跳转到包含第二阶段引导加载程序的下一个相邻扇区的跳转指令</li>\n<li>扇区末尾（类似于引导签名）</li>\n</ul>\n</li>\n<li><p>第一阶段或 1.5 阶段（或 GRUB2 上的 3）引导加载程序读取分区上的文件系统表（如 MFT / FAT），并将实际引导加载程序加载为常规文件</p>\n</li>\n</ul>\n<p><strong>引导加载程序（实际）</strong></p>\n<ul>\n<li><p>由前一个引导加载程序从同一分区的文件系统中加载</p>\n</li>\n<li><p>加载所有必要的文件系统驱动程序（如果还需要）</p>\n</li>\n<li><p>配置是从数据库中读取的，例如 Linux 上的 /boot/grub/ (GRUB) 和 Windows 上的 &lt;“系统保留”分区&gt;/Boot/BCD (BOOTMGR)</p>\n</li>\n<li><p>Windows:</p>\n<ul>\n<li>BCD 是二进制文件，可以使用命令行工具 bcdedit.exe 或 GUI 工具 EasyBCD 读取和修改</li>\n<li>XP 上的 NTLDR 只是将 C:\\ 用作活动分区，读取 C:\\Boot.ini</li>\n</ul>\n</li>\n<li><p>Linux：</p>\n<ul>\n<li>GRUB 利用模块为复杂的启动过程提供额外的功能</li>\n<li>如果需要或配置，它可以向用户显示引导菜单，例如用于多重启动或在安全/恢复模式下启动或从 USB/网络启动等</li>\n<li>在 RAM 中定位并加载所需操作系统的内核和 ramdisk</li>\n<li>如果 GRUB 无法处理 Windows 等操作系统的内核，可以将其配置为链式加载，即读取并执行包含 Windows 引导加载程序的分区的引导扇区</li>\n<li>&#39;os-prober&#39; 通过读取该分区中的引导加载程序配置来帮助 &#39;grub-install&#39; 和 &#39;grub-update&#39; 查找 Windows 引导分区（系统保留）</li>\n<li>内核</li>\n<li>引导加载程序以读取模式将同一分区 (/boot) 中的内核的第一个 MB 加载到 RAM 中，然后切换到保护模式 (32 位)，并将 1MB 前移清除第一个 MB</li>\n<li>然后切换回实模式，并对 initrd 执行相同的操作（如果它与内核分开）</li>\n<li>内核包含 ramfs 驱动程序，以从 initrd 读取 rootfs 并进行挂载</li>\n</ul>\n</li>\n</ul>\n<p><strong>Initramfs</strong></p>\n<ul>\n<li>包含访问真实 rootfs（硬盘驱动器、NFS 等）所需的最小文件系统和模块（内核未携带的所需驱动程序）</li>\n<li>udev 或特定的脚本加载所需的模块</li>\n<li>&lt;ramdisk&gt;/init 通常是一个脚本，它加载必要的驱动程序并挂载真实的 rootfs</li>\n<li>最后，init switch_root 到真实的 rootfs 并执行 <real rootfs>/sbin/init；sysV（传统的）、upstart（Ubuntu 的倡议）或 systemD（最新被广泛接受的）</li>\n</ul>\n<p><strong>init &gt; getty（在虚拟终端上）&gt; login（程序）&gt; motd &gt; 登录 shell &gt; bashrc / bash_profile</strong></p>\n<p>阅读更多关于<a href=\"https://xdaforums.com/android/general/info-terminal-shell-display-server-t3756163/post75710069\"><strong>LINUX 控制台和虚拟终端</strong></a> 的信息</p>\n<p><strong>UEFI</strong></p>\n<ul>\n<li>与 BIOS 相反，UEFI 可以理解文件系统，因此不受 MBR 代码（446 字节）的限制</li>\n<li>需要一个 EFI 系统分区 (ESP)，最好至少 550MB</li>\n<li>ESP 分区格式化为 FAT32，但可以理解其他文件系统，如 FAT12（软盘）、FAT16、ISO9660（CD/DVD）、UDF 等</li>\n<li>EFI 固件直接读取 &lt;ESP_Partition&gt;/EFI/&lt;vendor&gt;/&lt;boot_programs&gt;，如引导管理器中配置的那样（哪个磁盘、哪个分区、哪个程序）</li>\n<li>引导程序使用 EFI 固件或 EFI shell 或 GUI <a href=\"https://software.intel.com/en-us/articles/uefi-boot-manager-1\">引导管理器</a> 加载内核</li>\n<li>如果引导程序只是磁盘（没有配置分区和程序），则执行后备程序 &lt;disk&gt;/&lt;ESP partition&gt;/BOOT/BOOTX64.EFI</li>\n<li>安全启动功能在加载前验证引导程序的签名</li>\n<li>多重引导很容易，只需从 ESP 分区读取不同的条目，而不是依赖单个引导加载程序来链式加载所有可用的操作系统</li>\n<li>Linux 内核的 EFISTUB 功能允许直接将内核作为 boot_program 启动</li>\n<li>UEFI 比 MBR 更适合 GPT</li>\n</ul>\n<p>必读：</p>\n<p><a href=\"https://xdaforums.com/android/general/info-android-device-partitions-basic-t3586565\"><strong>Android 分区和文件系统</strong></a></p>\n<h1>2. Android 启动顺序</h1>\n<p>可能存在一个或多个引导加载程序（以提供有关如何启动的指示）。对于典型的 Android 设备（最常见的 Qualcomm SoC / ARM 处理器），启动顺序如下：</p>\n<ul>\n<li>BootROM（类似于 PC 上的 BIOS）。它与 SoC 集成在一起。</li>\n<li>处理器、引导加载程序</li>\n<li>开机自检 (POST)</li>\n<li>SBL<ul>\n<li>并行加载来自不同分区的内容。</li>\n</ul>\n</li>\n<li>应用程序引导加载程序 (aboot)<ul>\n<li>主引导模式（如果未检测到内核或如果应用了引导加载程序/下载模式键组合）<ul>\n<li>引导加载程序/下载模式</li>\n</ul>\n</li>\n<li>辅助引导<ul>\n<li>内核（硬件检测和填充 /sys、/dev/ 和 /proc 目录，随着进程的启动）和 initramfs（在 rootfs 上创建 rootfs 和其他伪文件系统）<ul>\n<li>Init（PID 为“1”的第一个进程。它启动进程和守护进程的进一步加载）</li>\n<li>系统 / 操作系统 (ROM)</li>\n</ul>\n</li>\n<li>恢复模式（如果应用了恢复模式键组合。它是一个带有 UI 的内核，用于执行基本的故障排除操作）</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n<h1>3. 引导加载程序</h1>\n<p>引导加载程序通过从 SoC 接管控制、执行必要的检查、加载所需的组件，然后将启动控制权移交给内核来促进设备的初始启动。在第一阶段检测到 RAM，以开始在其中加载其他硬件（如键盘、显示器等）的配置。\n存在（曾存在）多个由不同处理器、在具有不同（分区）名称的不同设备上执行的引导加载程序，如 RPM (PBL)、DBL（设备引导加载程序；CFG_DATA 或 sbl1）、SBL2、SBL3 (QCSBL) 和 OSBL（操作系统引导加载程序）等。</p>\n<p>简而言之，在现代 ARM 设备（Qualcomm SoC）上：</p>\n<p><strong>BootROM / iROM 和 PBL</strong></p>\n<p>iROM 在按下电源按钮时由 CPU0 运行，加载到 iRAM 中（在 RAM 初始化之前）\n它可以设置 RAM 并执行 RAM 中的 PBL，或将此留给 SBL。iROM/PBL 在 SoC 上硬编码，在 CPU 生产过程中写入，并且是闭源的。\n在支持从多个来源（如 eMMC/sdcard/USB/UART/网络）启动的设备（如开放板或某些平板电脑）上，与 PC BIOS 一样，在 iROM 和 PBL 之间有一个额外的阶段：</p>\n<p><strong>IBL（初始 BL）</strong>\n它也被加载到 iRAM 中。根据 iROM 通知的 CPU 引脚设置（隐藏和焊接或暴露以进行手动切换），IBL 将引导模式选择传递给 PBL，并且如果本身由 iROM 进行电子签名，则可以选择检查 PBL 的完整性。</p>\n<p><strong>SBL 或 XBL（预加载程序）</strong>\nIBL 从 eMMC/SDCard 调用 SBL，该 SBL 支持 LCD 输出。SBL 初始化 DDR RAM，加载可信固件 (TZ) 和 RPM 固件（如果 BootROM 未加载）。SBL 在自检设备后调用最终的引导加载程序。\nUboot 是用于嵌入式设备的开源辅助引导加载程序。但是，也可以从 Qualcomm 获取 SBL 的源代码。</p>\n<p><strong>ABOOT</strong>（APPSBL；Little Kernel 的前身）</p>\n<p>ABOOT 加载分区表、内核、启动画面（徽标）和调制解调器。它还负责充电模式和快速启动模式。RAM 中用于启动/恢复分区的内存地址在 aboot 中硬编码。\n最终（即在内核之前）引导加载程序的其他示例包括 uboot（用于嵌入式设备的传统 Linux 引导加载程序）或制造商开发的 BL，如 hboot（由 HTC 使用）和 redboot 等。</p>\n<p>制造商在此阶段设置其限制（例如网络运营商，即 SIM 卡锁和其他限制）。USB 协议不足以破解此类限制，与引导加载程序通信以破解此类限制需要特殊设备（通常称为刷机盒或维修盒），有时甚至需要像 JTAG 这样的协议，即直接与微处理器对话。\n按照惯例，所有这些阶段 1、2、3... 引导加载程序都简称为引导加载程序。而在某些设备上，根本没有引导加载程序分区，并且引导加载程序驻留在 SoC 上。</p>\n<p>回到启动过程，在初始化启动过程后，引导加载程序（如果已锁定）检查 boot.img（正常启动）或 recovery.img（恢复启动）的完整性，将其加载到 RAM 中，并将控制权移交给内核，为它提供压缩（cpio，gzipped）initramfs 的“phys_initrd_start”地址。</p>\n<h1>4. 内核和 INITRAMFS</h1>\n<p>一旦内核被引导加载程序加载并解压到 RAM 中以及参数，内核就开始执行。内核实际上是一个自包含的（静态）可执行二进制文件，由在编译时链接在一起的许多目标文件 (.o) 组成。一旦确定了架构和 CPU，就会根据从引导加载程序传递的参数执行依赖于架构的代码。然后执行独立于架构的阶段，其中包括设置驱动程序（显示、触摸等）、文件系统（如 rootfs、tmpfs、proc、ext4 等）以及初始化控制台（如果已配置）。在这里，内核空间结束，用户空间开始（他们称之为）。</p>\n<p>内核在 rootfs（其本身是 ramfs 或 tmpfs）中解压压缩的 initramfs，并执行 /init 二进制文件，该二进制文件随后读取其配置文件 /init.rc 和其他以 Android 特定 init 语言编写的 /*.rc 文件。在内核的帮助下，init 挂载伪文件系统 /sys 和 /proc，并填充包含设备节点文件的 /dev 目录。然后，它挂载 /system 和所有其他分区（包括 /data，如果已加密，则也会解密），并设置（SELinux 安全）策略、系统属性和环境变量（PATH、EXTERNAL_STORAGE 等）。此外，init 还负责处理任何硬件更改 (ueventd) 和动态发生的启动服务更改（看门狗）。</p>\n<p>最后，init 启动位于系统分区上的运行时。init 启动的主要最后一个进程之一是 Zygote（Java 虚拟机），它会编译应用程序以在特定的架构（主要是 arm / arm64）上运行。</p>\n<p><strong>设备树 Blob</strong></p>\n<p><a href=\"https://elinux.org/Device_Tree_What_It_Is\">设备树</a> Blob (DTB) - 由 DT 编译器 (DTC) 从 DT 源 (DTS) 文本创建 - 是板/SoC 上硬件组件的映射，通常是内核源代码的一部分。</p>\n<p>PC 硬件通常支持通过 ACPI 进行硬件枚举，即内核可以查询（探测）总线 - PCI（内部设备）、USB（外部设备）、SCSI（存储设备）、HDMI/DVI/VGA（显示设备）等 - 哪些设备连接到它。</p>\n<p>嵌入式设备（包括 Android 设备）上的总线大多不支持枚举（硬件发现），因为通常存在固定的一组设备，并且无法选择在设备上加载不同的操作系统。因此，需要通知操作系统所有连接的设备，这是通过向内核提供标准的 DTB 来完成的。DTB 由 SoC/主板供应商提供，通常是内核源代码的一部分。在启动过程中，DTB 由引导加载程序在启动时加载并传递给内核，以便内核可以发现硬件并相应地创建节点点。</p>\n<p>我们可以通过以下方式查看 Adroid 设备上的设备树：</p>\n<pre><code>~# ls /sys/firmware/devicetree/base\n~# ls /proc/device-tree\n</code></pre>\n<p>DTB 可以像 AOSP 指定的那样位于单独的 dtb/odm 分区上（并且是 Android 诞生之前针对基于 ARM 的嵌入式 Linux 设备提出的解决方案），但这并没有得到广泛的应用。通常，DTB 会附加到内核 zImage/Image.gz 或放置在 boot.img 内的第二阶段。</p>\n<p><strong>验证/安全启动</strong></p>\n<p>确保从开机到加载内核的信任链属于 SoC 供应商（Qualcomm、Intel 等）和 OEM 的领域。在启动过程中的任何时候注入一些恶意或有害的代码都会变得非常困难，以至于不可能实现。</p>\n<p>为了确保安全的启动链，PBL 验证 SBL 的真实性，SBL 随后验证引导加载程序（TZ、RPM、DSP、HYP 和 aboot）的完整性，以避免加载未签名的映像（引导、恢复、系统等）。TZ 在由 SBL 加载后，还使用基于硬件的根证书验证 ABOOT。</p>\n<p>具有验证/安全启动实施的引导加载程序通过将其签名与存储在“OEM 密钥库”（某些分区，如 CMNLIB、KEYMASTER 或其他名称）中的密钥进行匹配来验证 boot.img 或 recovery.img（内核、initramfs 和附加到内核或 boot.img 第二阶段的 DTB），该密钥库本身由 OEM 签名。一些供应商允许用自定义密钥库替换/附加此密钥库，以便可以刷写自定义签名映像，然后重新锁定引导加载程序。<a href=\"https://blog.omitol.com/2017/09/30/Bypass-QCOM-Secure-Boot/\">此处</a>给出了一个简单的详细信息。</p>\n<p>在此阶段，信任链被移交给存储在引导映像 initramfs 中的“dm-verity”密钥，该密钥负责 Google/AOSP 的“验证启动”过程。Dm-verity（Google 的验证启动的一部分，它实现<a href=\"https://www.kernel.org/doc/Documentation/device-mapper/verity.txt\">Linux 设备映射器</a>）是一项内核功能，即它在 boot 映像（内核和 ramdisk）加载到 RAM 中后生效。它会验证随后加载的块设备；/system、（如果存在，则为 /vendor）和可选的其他设备。</p>\n<p>有关详细信息，请参见<a href=\"https://www.timesys.com/security/secure-boot-snapdragon-410/\">此处</a>、<a href=\"https://www.xda-developers.com/qualcomm-maintains-its-dedication-to-security-with-secure-boot/\">此处</a> 和 <a href=\"https://lwn.net/Articles/638627/\">此处</a>。</p>\n<p>Google 建议从 <a href=\"https://source.android.com/security/verifiedboot/avb\">验证启动 2</a> 开始在引导加载程序中集成 libavb（用于验证 boot.img 完整性的本机代码）。</p>\n<p><strong>解锁引导加载程序</strong></p>\n<p><em>阅读<a href=\"https://xdaforums.com/android/help/info-android-device-security-privacy-t3637290\">此处</a>以了解 BL 解锁的风险。</em></p>\n<p>除非解锁引导加载程序，否则无法加载未签名的内核或恢复。要对操作系统进行任何修改，一个关键的过程是禁用 Android 引导加载程序 (aboot) 中内置的安全系统，该系统可保护只读分区免受意外（或故意）修改，以保护隐私、安全和 DRM。这就是所谓的“解锁 NAND”或“解锁引导加载程序”。您必须首先解锁引导加载程序才能修改分区“boot”或“recovery”并在 /system 上获得 root 访问权限。如果引导加载程序已锁定，则您只能对 /cache 和 /data 分区具有写入访问权限。设备上的其他所有内容都是只读的，并且引导加载程序会阻止将未签名的映像刷写到手机。解锁的引导加载程序会忽略 BootROM 启动的签名验证检查，然后转移到“SBL”，然后转移到“ABOOT”，同时加载内核或恢复。</p>\n<p>一些较新的设备不允许在没有制造商许可的情况下直接解锁引导加载程序 (FRP)，以确保更高的安全性，即<a href=\"https://xdaforums.com/android/general/info-android-device-partitions-basic-t3586565\">分区内容</a>“devinfo”由 OEM 签名，未经其批准不得修改。获得许可后，提供了一种使用 PC 解锁 BL 的官方方法。尽管如此，由于<a href=\"https://developer.sony.com/develop/open-devices/get-started/unlock-bootloader/\">解锁引导加载程序</a>，某些与<a href=\"https://xdaforums.com/android/general/info-to-instal-windows-ios-linux-t3763961\"><strong>专有内容</strong></a>相关的功能可能会丢失。</p>\n<p>DRM 用于保护内容不被复制。</p>\n<blockquote>\n<p>由于移除了 DRM 安全密钥，设备上的某些预加载内容也可能无法访问。</p>\n</blockquote>\n<p><strong>Android Root</strong></p>\n<p><strong>必读：<a href=\"https://xdaforums.com/showpost.php?p=77437874&postcount=6\">Root 用户和 Linux 功能：Linux 与 Android</a>\n注意：解锁引导加载程序和 Root 会破坏“验证启动”。这可能是<a href=\"https://copperhead.co/android/docs/verified_boot\">危险的</a>。</strong></p>\n<p>为了在 Android 上执行某些特权任务，我们需要首先“root”该设备。由于无法从正在运行的 Android 操作系统中启动具有提升权限的进程，因此 root 通常涉及从具有所有功能的启动中运行 root 进程（su-守护进程）。超级用户请求由任何非特权程序通过执行“su”二进制文件提出，权限由应用程序管理。</p>\n<p>在早期，root 通常涉及启动到自定义恢复模式，这反过来又挂载和修改 /system 文件。通常，某些守护进程的可执行二进制文件被替换为自定义脚本。为了解决因提高安全功能（SELinux、验证启动、SafetyNet 等）而导致的 OTA 和其他问题，引入了无系统 root 方法，该方法被最新的应用程序（如 Magisk）使用。它涉及修改 /boot 映像并将一些文件放在 /data 上。因此，注入了一个新的 init 服务，以满足新安全机制的所有必要要求。</p>\n<p>在这两种情况下，锁定的引导加载程序都不会启动自定义恢复模式或修改后的内核 (boot.img)。请参阅<a href=\"https://source.android.com/security/verifiedboot\">验证启动</a>。因此，需要解锁引导加载程序才能进行 root。\n但是，有时有可能在没有解锁引导加载程序的情况下获得 root 权限，但并非总是如此。</p>\n<p>使用某种一键式 root 解决方案（KingRoot、Z4Root、KingoRoot 等）从正在运行的 ROM 中 root 手机的其他方法依赖于 Android 操作系统中的某些漏洞或漏洞利用。随着 Android 的每次新版本发布和改进的防御机制，制造此类安全漏洞变得越来越困难，尽管对于不同的供应商而言，情况也有所不同。最突出的是随着 Lollipop 和 Marshmallow 的发布，<a href=\"https://www.xda-developers.com/a-look-at-marshmallow-root-verity-complications/\">无系统方法</a>不得不被引入，因为以前的方法无法工作。当使用这些不正确的 root 方法之一 root 手机时，很可能会在某个时候遇到“root 不完整”之类的消息。如果这样的 root 方法适用于您的设备，则令人震惊。此漏洞利用也是恶意软件进入您设备的一种方式。例如，请参阅 <a href=\"https://topjohnwu.github.io/Magisk/install.html#Exploits\">Magisk 安装 - 漏洞</a>、<a href=\"http://usenix.org/system/files/conference/usenixsecurity17/sec17-gasparis.pdf\">此处</a> 和 <a href=\"https://blog.trendmicro.com/trendlabs-security-intelligence/new-androrat-exploits-dated-permanent-rooting-vulnerability-allows-privilege-escalation/\">此处</a>。一个非常流行的漏洞 <a href=\"https://github.com/timwr/CVE-2016-5195\">dirty cow</a> 后来被<a href=\"https://source.android.com/security/bulletin/2016-11-01#2016-11-01-details\">修补</a>。</p>\n<p>此外，对于某些设备，有一些技巧可以在不解锁引导加载程序的情况下刷写自定义恢复模式，这使用某种固件刷写工具（SPFlasher、MiFlasher 等）在下载模式下完成，因为下载模式甚至可以在加载引导加载程序/fastboot 之前访问设备。或者，如果您是编码专家，则可以模仿自定义恢复映像，使其看起来像工厂签名的固件，并通过库存恢复模式刷写它。但是，此漏洞利用也不是通用的解决方案。</p>\n<p>因此，不需要任何漏洞利用的正确 root 方法是通过解锁的引导加载程序。在购买新手机时必须考虑这一点。让您远离 root 访问和解锁的引导加载程序有利于供应商。通过强迫您使用他们的 ROM（捆绑了一堆无用的膨胀软件应用程序），他们通过收集数据、显示广告和使用许多其他策略从您那里赚取大量资金（以及被迫的忠诚度）。选择提供内核源代码和解锁引导加载程序能力的品牌（当然是在客户承担责任且保修失效的情况下）。</p>\n<p><strong>固件更新协议（引导加载程序模式）</strong></p>\n<p>与 BL 类似，在每个设备上，可能存在一个或多个具有不同名称的 BL 模式，如引导加载程序模式、下载模式、紧急模式 (EDL)、ODIN (Samsung)、nvFlash 工具等。当我们在 BL 模式下启动时，设备卡在启动徽标上。一些工厂刷写器在这些模式下工作，例如 MiFlasher (Xiaomi) 和 SP Flash Tool（用于 MTK 设备）。即使设备软砖了，即如果无法访问恢复模式和/或 ROM，也可以访问引导加载程序或下载模式。</p>\n<p><strong>下载模式</strong></p>\n<p>下载模式（在设备开机时组合某些按钮；通常是音量上 + 音量下或音量下持续较长时间 + 电源）是许多供应商使用刷写器（软件）刷写工厂固件/更新的官方方法。紧急下载模式 (EDL)，正如它在小米设备上所称的那样，也可以通过 fastboot/adb 命令或使用某些跳线/跳线器访问。但是，为了确保更高的安全性，EDL 在某些较新的设备上被禁用。</p>\n<p>下载模式是引导加载程序模式（在 PBL 或 SBL 阶段）的主要模式，可以在不解锁引导加载程序的情况下使用。\nOdin (Samsung)、QPST/QFIL 在下载模式（Qualcomm HS-USB QDloader 9008）下工作。\n当我们在下载模式下启动时，设备卡在空白屏幕上。</p>\n<p><strong>快速启动模式</strong></p>\n<p>Fastboot - 由 ABOOT 提供 - 是一种软件开发工具和用于 Android 引导加载程序的标准通信协议。它是恢复刷写的替代方案，它在引导加载程序模式 (aboot) 下工作，并且捆绑在大多数最新的 ARM Qualcomm 设备上。它是一个通过命令行与设备进行交互的最小 UI，以防发生故障或修改/刷写分区。一些 OEM 提供的 fastboot 功能有限，例如 &#39;fastboot oem&#39; 命令不起作用，并且有些设备根本没有 fastboot。这取决于手机供应商的决定。</p>\n<p>当设备通过 USB 连接到 PC 时，可以使用快速启动模式通过命令执行操作。即使手机未在恢复模式或 ROM 中打开，或者即使手机上未安装 Android，它也可以工作。您可以在<a href=\"https://xdaforums.com/showpost.php?p=72055561&postcount=1\">此处</a>阅读我们可以通过快速启动模式执行哪些操作。</p>\n<p>在此阶段仅激活 NAND (eMMC) 和 USB 模块（驱动程序）。</p>\n<p><a href=\"https://xdaforums.com/showpost.php?p=75905976&postcount=4\">INIT 进程和服务：Android 与 Linux</a></p>\n<p><a href=\"https://xdaforums.com/showpost.php?p=75905976&postcount=3\">由 INIT 挂载的文件系统树：Android 与 Linux</a></p>\n<h1>资源</h1>\n<ul>\n<li><a href=\"https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-1.html\">从引导加载程序到内核</a></li>\n</ul>\n","tags":["android"]},{"id":"how-v8-works","url":"https://yieldray.fun/posts/how-v8-works","title":"高层次理解 V8","date_published":"2024-12-25T15:34:52.000Z","date_modified":"2025-04-01T14:34:52.000Z","content_text":"<h1>Prerequisites</h1>\n<p>本节用于快速说明理解程序执行所需的前置知识</p>\n<pre><code class=\"language-mermaid\">graph TB\n    %% 定义节点\n    SourceCode[&quot;Source Code (源代码)&quot;]\n    Tokens[&quot;Tokens (词法单元)&quot;]\n    SyntaxTree[&quot;Syntax Tree (语法树)&quot;]\n    IR[&quot;Intermediate Representation (中间表示)&quot;]\n    ByteCode[&quot;Byte Code (字节码)&quot;]\n    MachineCode[&quot;Machine Code (机器码)&quot;]\n    HighLevelLanguage[&quot;High Level Language (高级语言)&quot;]\n    Evaluate[&quot;Evaluate (执行结果)&quot;]\n\n    %% 定义连接关系\n    SourceCode --&quot;Scanning (词法分析)&quot;--&gt; Tokens --&quot;Parsing (语法分析)&quot;--&gt; SyntaxTree\n\n    SyntaxTree --&quot;Analysis (静态分析)&quot;--&gt; IR\n    SyntaxTree --&quot;Transpiling (转译)&quot;--&gt; HighLevelLanguage\n\n    IR --&quot;Optimizing (优化)&quot;--&gt; IR\n    IR --&quot;Code Generation (代码生成)&quot;--&gt; ByteCode\n    IR --&quot;Code Generation (代码生成)&quot;--&gt; MachineCode\n    IR --&quot;Transpiling (转译)&quot;--&gt; HighLevelLanguage\n\n    SyntaxTree --&quot;Tree Walk Interpreter (树遍历解释器)&quot;--&gt; Evaluate\n    ByteCode --&quot;Virtual Machine (虚拟机执行)&quot;--&gt; Evaluate\n    MachineCode --&gt; Evaluate\n</code></pre>\n<p>简而言之：前端针对代码（语义），后端关注（物理）运行</p>\n<ul>\n<li>single-pass compilers</li>\n<li>tree-walk interpreter</li>\n<li>transcompiler</li>\n<li>just-in-time(JIT) compilation</li>\n</ul>\n<p>根据局部性原理，树遍历解释器需要在树结构中（通过指针）来回跳转，造成缓存丢失。<br>字节码是一个理想化的幻想指令集。可以把字节码看作是AST的一种紧凑的序列化，因此对CPU缓存更友好。</p>\n<h4>See Also</h4>\n<ul>\n<li><a href=\"https://www.youtube.com/watch?v=veYjbF1rt5o\">https://www.youtube.com/watch?v=veYjbF1rt5o</a></li>\n<li><a href=\"https://loora1n.github.io/2024/08/14/%E3%80%90V8%E3%80%91%E6%B7%B1%E5%85%A5turbofan/\">https://loora1n.github.io/2024/08/14/%E3%80%90V8%E3%80%91%E6%B7%B1%E5%85%A5turbofan/</a></li>\n<li><a href=\"https://geekdaxue.co/books/lishengshidiwen@itk711\">https://geekdaxue.co/books/lishengshidiwen@itk711</a></li>\n<li><a href=\"https://blog.shi1011.cn/learn/2412\">https://blog.shi1011.cn/learn/2412</a></li>\n<li><a href=\"https://www.royalbhati.com/posts/why-js-is-fast\">https://www.royalbhati.com/posts/why-js-is-fast</a></li>\n<li><a href=\"https://trustfoundry.net/2025/01/14/a-mere-mortals-introduction-to-jit-vulnerabilities-in-javascript-engines/\">https://trustfoundry.net/2025/01/14/a-mere-mortals-introduction-to-jit-vulnerabilities-in-javascript-engines/</a></li>\n</ul>\n<div class=\"markdown-alert markdown-alert-caution\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-stop mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>\n<p>v8 架构并非一成不变，目的在于参考其思路。</p>\n</div>\n<h1>JavaScript</h1>\n<p>一个 v8 实例称为 Isolation，隔离 js 堆。<br>Isolate 对应于一个物理线程。Blink 中的 Isolate：物理线程 = 1：1。<br>主线程有自己的 Isolate。工作线程有自己的 Isolate。</p>\n<p>Context 对应于一个全局对象（在 Frame 的情况下，它是 Frame 的 window 对象）。<br>由于每个框架都有自己的 window 对象，因此一个渲染器进程中有多个 Context。</p>\n<hr>\n<p>ECMA262 规范中包含一个“执行上下文”的定义。和上述 Context 概念比较接近。<br>每个<a href=\"https://tc39.es/ecma262/#sec-execution-contexts\">上下文</a>包含 Realm，因此每个上下文的内置对象都是不同的。</p>\n<hr>\n<p><a href=\"https://source.chromium.org/chromium/chromium/src/+/main:gin/\">gin</a>用于简化 Chromium 与 v8 交互，还用于 ChromeExtension/PAC/PDFium。<br>Blink Bindings 用于 WebIDL 自动生成 v8 bindings。<br>Node.js 则直接使用 v8。</p>\n<hr>\n<p>v8 整体流程如下</p>\n<pre><code class=\"language-mermaid\">graph TB\n    NetworkCache[&quot;Network/Cache&quot;] --&gt; ScriptText[&quot;Script Text&quot;]\n    ScriptText --&gt; Parser\n    Parser --&gt; AST\n    AST --&gt; Ignition\n    Ignition --&gt; Bytecode\n    Bytecode --&gt; Execution\n    Execution &lt;--&gt; TypeFeedback[&quot;Type Feedback&quot;]\n    ObjectShapes[&quot;Object Shapes&quot;] --&gt; TypeFeedback\n    Bytecode -.-&gt; TurboFan\n    TypeFeedback -.-&gt; TurboFan\n    TurboFan --&gt; OptimizedCode[&quot;Optimized Code&quot;]\n    OptimizedCode -- deopt --&gt; Bytecode\n\n    subgraph Loading\n        NetworkCache\n        ScriptText\n    end\n\n    subgraph Parsing\n        Parser\n        AST\n    end\n\n    subgraph Interpreter\n        Ignition\n        Bytecode\n    end\n\n    subgraph Execution Phase\n        Execution\n        TypeFeedback\n        ObjectShapes\n    end\n\n    subgraph Optimization\n        TurboFan\n        OptimizedCode\n    end\n</code></pre>\n<h2>解析</h2>\n<p>流式解析（后台线程）。字符流解析为 Token 流。</p>\n<p>v8 使用手写递归下降解析，提供更好的错误处理。</p>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/scanner\">https://v8.dev/blog/scanner</a></p>\n</blockquote>\n<h3>预解析</h3>\n<p>解析但不生成 AST。用于快速执行，跳过无需立即执行的内容（函数，因为可以惰性解析），从而加速和节约内存。<br>解析过程是必要的，因为语法检查是必要的。</p>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/preparser\">https://v8.dev/blog/preparser</a></p>\n</blockquote>\n<h3>解析</h3>\n<p>为了实际执行，则必须生成 AST（注意解析是流式的）。</p>\n<p>因为 js 变量的语义（例如变量可以在声明前就被引用），还需做范围分析（即：变量提升）。</p>\n<p>那么 v8 解析器实现没有使用单遍算法，因此做范围分析则需要多扫描一遍，以确定变量引用。</p>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/cost-of-javascript-2019\">https://v8.dev/blog/cost-of-javascript-2019</a></p>\n</blockquote>\n<h2>执行</h2>\n<h3><a href=\"https://v8.dev/docs/ignition\">Ignition</a></h3>\n<p>Ignition 生成 High-Level 字节码，其语义与 JavaScript 类似。</p>\n<p>字节码生成器生成字节码流，该字节码可以直接解释。<br>采用线程解释器（Threaded Interpreter）实现方式，每条指令直接跳转到下一条指令的实现代码，减少了查表和分派的开销。</p>\n<p>不基于栈式虚拟机，而是<strong>虚拟寄存器</strong>。其优势在于寄存器在字节码中可以是隐含的，因此能够减小字节码体积<br>注意 JS 是解释型的，因此所有 IR 都存在内存，因此减小 IR 体积就是减小内存占用。</p>\n<hr>\n<p>下面以这个简单的代码为例：</p>\n<pre><code class=\"language-js\">function foo(a) {\n    if (a === 0) {\n        let b = 1;\n        return a;\n    }\n}\n\n// 让 foo 变成热函数，否则不会 jit\nfor (let i = 0; i &lt; 1e6; i++) foo();\n</code></pre>\n<p>为了方便，使用 node.js 来输出函数 foo 的字节码。<code>node --print-bytecode demo.js</code></p>\n<blockquote>\n<p>a 表示隐式寄存器 accumulator。所以 Lda 表示 load accumulator</p>\n</blockquote>\n<pre><code>   22 S&gt; 0x3810d58db0e8 @    0 : 0c                LdaZero\n   28 E&gt; 0x3810d58db0e9 @    1 : 6c 03 00          TestEqualStrict a0, [0]\n         0x3810d58db0ec @    4 : 99 08             JumpIfFalse [8] (0x3810d58db0f4 @ 12)\n   53 S&gt; 0x3810d58db0ee @    6 : 0d 01             LdaSmi [1]\n         0x3810d58db0f0 @    8 : c4                Star0\n   63 S&gt; 0x3810d58db0f1 @    9 : 0b 03             Ldar a0\n   71 S&gt; 0x3810d58db0f3 @   11 : a9                Return\n         0x3810d58db0f4 @   12 : 0e                LdaUndefined\n   78 S&gt; 0x3810d58db0f5 @   13 : a9                Return\n</code></pre>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/ignition-interpreter\">https://v8.dev/blog/ignition-interpreter</a></p>\n</blockquote>\n<h4><a href=\"https://v8.dev/docs/hidden-classes\">Hidden Classes</a> (a.k.a. Maps)</h4>\n<p>使用隐藏类是因为它比哈希表更快。</p>\n<p>隐藏类记录对象的形状（对象有哪些字段），并当形状变更时，过渡为新形状。</p>\n<p>用于（字节码）内联缓存（IC）。在隐藏类中查询字段则只需查询偏移量，无需哈希函数开销。</p>\n<h3><a href=\"https://v8.dev/docs/turbofan\">TurboFan</a></h3>\n<p>TurboFan 是基于 <a href=\"https://darksi.de/d.sea-of-nodes/\">Sea of Nodes (SoN)</a> 基于图的优化编译器。</p>\n<div class=\"markdown-alert markdown-alert-caution\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-stop mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>\n<p>v8 正在使用更传统的控制流图（CFG）中间表示法（IR）<a href=\"https://v8.dev/blog/leaving-the-sea-of-nodes\">替代 SoN</a>，并将其命名为 Turboshaft</p>\n</div>\n<p>数据/控制流/副作用被表示为节点，用于其它节点的输入。<br>复用 Ignition 字节码，函数独立编译，目标也是编译热函数。</p>\n<p>JIT，通过<strong>推断的类型反馈</strong>生成内联优化代码。生成低级 IR，并编译为平台相关机器码。<br>由于 JS 语义是动态的，类型不匹配时（JIT 失效，就）需降级为字节码解释。</p>\n<hr>\n<p>为了方便，下面使用 node.js 来输出函数 foo 的机器码。<code>node --print-opt-code demo.js</code><br>注意只有<strong>热函数</strong>代码才会被 JIT（下例为 ARM64 平台）</p>\n<pre><code>0x1086cb700     0  f85c0050       ldur x16, [x2, #-64]\n0x1086cb704     4  b840f210       ldur w16, [x16, #15]\n0x1086cb708     8  36000070       tbz w16, #0, #+0xc (addr 0x1086cb714)\n0x1086cb70c     c  580006b1       ldr x17, pc+212 (addr 0x00000001086cb7e0)\n0x1086cb710    10  d61f0220       br x17\n0x1086cb714    14  a9bf7bfd       stp fp, lr, [sp, #-16]!\n0x1086cb718    18  910003fd       mov fp, sp\n0x1086cb71c    1c  a9be03ff       stp xzr, x0, [sp, #-32]!\n0x1086cb720    20  a9016fe1       stp x1, cp, [sp, #16]\n0x1086cb724    24  f8520344       ldur x4, [x26, #-224]\n0x1086cb728    28  eb2463ff       cmp sp, x4\n0x1086cb72c    2c  54000309       b.ls #+0x60 (addr 0x1086cb78c)\n0x1086cb730    30  d2800001       movz x1, #0x0\n0x1086cb734    34  580004e3       ldr x3, pc+156 (addr 0x00000001086cb7d0)    ;; object: 0x011791a5b131 &lt;FeedbackVector[1]&gt;\n0x1086cb738    38  d280001b       movz cp, #0x0\n0x1086cb73c    3c  f9401fe0       ldr x0, [sp, #56]\n0x1086cb740    40  aa0103e2       mov x2, x1\n0x1086cb744    44  58000530       ldr x16, pc+164 (addr 0x00000001086cb7e8)\n0x1086cb748    48  d63f0200       blr x16\n0x1086cb74c    4c  f9402f44       ldr x4, [x26, #88]\n0x1086cb750    50  eb04001f       cmp x0, x4\n0x1086cb754    54  54000180       b.eq #+0x30 (addr 0x1086cb784)\n0x1086cb758    58  f9402340       ldr x0, [x26, #64]\n0x1086cb75c    5c  f85e83a3       ldur x3, [fp, #-24]\n0x1086cb760    60  910003bf       mov sp, fp\n0x1086cb764    64  a8c17bfd       ldp fp, lr, [sp], #16\n0x1086cb768    68  f100087f       cmp x3, #0x2 (2)\n0x1086cb76c    6c  5400004a       b.ge #+0x8 (addr 0x1086cb774)\n0x1086cb770    70  d2800043       movz x3, #0x2\n0x1086cb774    74  91000470       add x16, x3, #0x1 (1)\n0x1086cb778    78  927ffa10       and x16, x16, #0xfffffffffffffffe\n0x1086cb77c    7c  8b306fff       add sp, sp, x16, lsl #3\n0x1086cb780    80  d65f03c0       ret\n0x1086cb784    84  f9401fe0       ldr x0, [sp, #56]\n0x1086cb788    88  17fffff5       b #-0x2c (addr 0x1086cb75c)\n0x1086cb78c    8c  d2c00604       movz x4, #0x3000000000\n0x1086cb790    90  d10043ff       sub sp, sp, #0x10 (16)\n0x1086cb794    94  f90007ff       str xzr, [sp, #8]\n0x1086cb798    98  f90003e4       str x4, [sp]\n0x1086cb79c    9c  f9000bfb       str cp, [sp, #16]\n0x1086cb7a0    a0  d297c101       movz x1, #0xbe08\n0x1086cb7a4    a4  f2a06bc1       movk x1, #0x35e, lsl #16\n0x1086cb7a8    a8  f2c00021       movk x1, #0x1, lsl #32\n0x1086cb7ac    ac  d2800020       movz x0, #0x1\n0x1086cb7b0    b0  58000144       ldr x4, pc+40 (addr 0x00000001086cb7d8)    ;; object: 0x1bd96a0c1151 &lt;NativeContext[271]&gt;\n0x1086cb7b4    b4  aa0403fb       mov cp, x4\n0x1086cb7b8    b8  580001d0       ldr x16, pc+56 (addr 0x00000001086cb7f0)\n0x1086cb7bc    bc  d63f0200       blr x16\n0x1086cb7c0    c0  17ffffdc       b #-0x90 (addr 0x1086cb730)\n0x1086cb7c4    c4  d503201f       nop\n0x1086cb7c8    c8  5800017f       constant pool begin (num_const = 11)    ;; constant pool\n0x1086cb7cc    cc  d63f03e0       constant\n0x1086cb7d0    d0  91a5b131       constant\n0x1086cb7d4    d4  00000117       constant\n0x1086cb7d8    d8  6a0c1151       constant\n0x1086cb7dc    dc  00001bd9       constant\n0x1086cb7e0    e0  038c55e0       constant\n0x1086cb7e4    e4  00000001       constant\n0x1086cb7e8    e8  0391a0e0       constant\n0x1086cb7ec    ec  00000001       constant\n0x1086cb7f0    f0  039390a0       constant\n0x1086cb7f4    f4  00000001       constant\n0x1086cb7f8    f8  f8548350       ldur x16, [x26, #-184]\n0x1086cb7fc    fc  d61f0200       br x16\n0x1086cb800   100  97fffffe       bl #-0x8 (addr 0x1086cb7f8)    ;; debug: deopt position, script offset &#39;c&#39;\n                                                             ;; debug: deopt position, inlining id &#39;ffffffff&#39;\n                                                             ;; debug: deopt reason &#39;(unknown)&#39;\n                                                             ;; debug: deopt index 0\n</code></pre>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/turbofan-jit\">https://v8.dev/blog/turbofan-jit</a><br><a href=\"https://v8.dev/blog/launching-ignition-and-turbofan\">https://v8.dev/blog/launching-ignition-and-turbofan</a></p>\n</blockquote>\n<h2>内存管理</h2>\n<h3>Orinoco</h3>\n<p>跟踪式 GC，分代堆布局。基于块的增量式、并发回收。</p>\n<ul>\n<li><em>新生代（new-space）</em> (最多 32MB) 存放新分配的对象</li>\n<li><em>老生代（old-space）</em> (最多 2048MB) 存放长期存在的对象</li>\n<li>独立的 <em>代码空间</em>（可执行）</li>\n</ul>\n<hr>\n<ul>\n<li><p>&quot;Minor GC&quot;（新生代垃圾回收）：仅新生代空间</p>\n<ul>\n<li>将存活对象移动到其他半空间</li>\n<li>存活两次的对象晋升到老年代空间</li>\n<li>典型暂停时间：≤ 1 毫秒</li>\n</ul>\n</li>\n<li><p>&quot;Major GC&quot;（老年代垃圾回收）：所有空间</p>\n<ul>\n<li>标记：查找存活对象（增量式 + 并发 + 并行）</li>\n<li>压缩：疏散几乎空闲的页面（并行）</li>\n<li>更新引用</li>\n<li>清理：将非存活区域添加到空闲列表（并发 + 并行）</li>\n<li>运行 Blink 的 Oilpan GC</li>\n<li>典型暂停时间：≤ 10 毫秒</li>\n</ul>\n</li>\n</ul>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/free-garbage-collection\">https://v8.dev/blog/free-garbage-collection</a><br><a href=\"https://v8.dev/blog/orinoco\">https://v8.dev/blog/orinoco</a><br><a href=\"https://v8.dev/blog/orinoco-parallel-scavenger\">https://v8.dev/blog/orinoco-parallel-scavenger</a><br><a href=\"https://v8.dev/blog/concurrent-marking\">https://v8.dev/blog/concurrent-marking</a><br><a href=\"https://v8.dev/blog/trash-talk\">https://v8.dev/blog/trash-talk</a></p>\n</blockquote>\n<h3>Oilpan</h3>\n<p>与 blink 垃圾回收配合，可以处理 Oilpan 对象和 v8 对象的循环引用。</p>\n<p>实现跨 JS 和 C++ 边界回收（可达性分析）</p>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/tracing-js-dom\">https://v8.dev/blog/tracing-js-dom</a></p>\n</blockquote>\n<h2>语言实现</h2>\n<h3>值的表示，装箱</h3>\n<p>小整数（Smi）内联表示，其它值装箱，通过指针引用。</p>\n<h3>64位指针压缩</h3>\n<p>特性：</p>\n<ul>\n<li>典型的 JS 堆中有许多标记值（高达 70%！）</li>\n<li>不需要每个堆都占用完整的 64 位地址空间</li>\n<li>构建时选项可以将 64 位架构上的 JS 堆指针压缩到 32 位</li>\n<li>在 Chrome 中已启用</li>\n</ul>\n<p>区分 Pointer 和 Smi</p>\n<pre><code>Pointer:\n[offset (30 bits)][01]\n\nSmall integer (Smi):\n[int31 value (31 bits)][0]\n</code></pre>\n<ul>\n<li>Recall each Isolate has a separate heap</li>\n<li>...so each Isolate can have its own base</li>\n</ul>\n<pre><code>                   V8 instance data     Allocated memory chunks\n                          ↓                     ↓\n┌───┬──┬────────────────────────┬──┬───┬──┬───┬──┐\n│   │  │                        │  │   │  │   │  │\n└───┴──┴────────────────────────┴──┴───┴──┴───┴──┘\n    ←―――――― ~2GB ――――――→        ↑    ←―― ~2GB ――→\n                            base (4GB aligned)\n\n</code></pre>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>该图显示了内存布局，其中 V8 实例数据和分配的内存块分布在一个 4GB 对齐的基址上，两侧大约各有 2GB 的区域。</p>\n</div>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/pointer-compression\">https://v8.dev/blog/pointer-compression</a><br><a href=\"https://v8.dev/blog/oilpan-pointer-compression\">https://v8.dev/blog/oilpan-pointer-compression</a></p>\n</blockquote>\n<h3><a href=\"https://v8.dev/docs/torque\">Torque</a></h3>\n<p>Torque 是一种语言，目标是实现 v8 内置函数。Torque 编译器使用 <a href=\"https://v8.dev/blog/csa\">CodeStubAssembler</a> 将这些片段转换为高效的汇编代码。</p>\n<p>下面举 <code>Reflect.get</code> 实现为例子：</p>\n<pre><code class=\"language-ts\">namespace Reflect {\n// ES6 section 26.1.6 Reflect.get\ntransitioning javascript builtin\nReflectGet(js-implicit context: NativeContext)(...arguments): JSAny {\n  const object: JSAny = arguments[0];\n  const objectJSReceiver = Cast&lt;JSReceiver&gt;(object)\n      otherwise ThrowTypeError(MessageTemplate::kCalledOnNonObject, &#39;Reflect.get&#39;);\n  const propertyKey: JSAny = arguments[1];\n  const name: AnyName = ToName(propertyKey);\n  const receiver: JSAny =\n      arguments.length &gt; 2 ? arguments[2] : objectJSReceiver;\n  return GetPropertyWithReceiver(\n      objectJSReceiver, name, receiver, SmiConstant(kReturnUndefined));\n  }\n}\n</code></pre>\n<h1><a href=\"https://v8.dev/docs/wasm-compilation-pipeline\">WebAssembly</a></h1>\n<p>栈式虚拟机，一种架构无关的低级编译目标（但也不那么低层，因为基于抽象堆栈而非寄存器）。<br>数据类型很简单：i32/i64/f32/f64/v128（128位SIMD）</p>\n<p>WASM 文件都是模块，包含头（function types, imports, function declaration, tables, memories, global varaiables, exports, start function and elements for table initialization），代码段和数据段</p>\n<p>特点在于，（wasm）内存，表，全局变量都可通过外部控制。</p>\n<blockquote>\n<p>另见：<br><a href=\"https://v8.dev/blog/tags/webassembly\">https://v8.dev/blog/tags/webassembly</a></p>\n</blockquote>\n<h2><a href=\"https://v8.dev/blog/liftoff\">Liftoff</a></h2>\n<blockquote>\n<p>注意：函数级别编译，因此可在不同优化级别编译器中过渡</p>\n</blockquote>\n<p>流式基线编译器</p>\n<ul>\n<li>用于快速完成编译（10x，快速启动）（解码函数体-&gt;代码生成）</li>\n<li>无优化，只做简单的寄存器分配</li>\n<li>用于 debugging，不用于 profiling</li>\n</ul>\n<h2>TurboFan</h2>\n<ul>\n<li>用于快速执行（～½x体积，～2x速度）</li>\n<li>智能寄存器分配（SeaOfNodes，和 TurboFan 对 JS 做的一致），安排指令顺序</li>\n</ul>\n<h2>多线程</h2>\n<p><img src=\"/media/v8-wasm.svg\" alt=\"\"></p>\n","tags":["v8"]},{"id":"the-semantics-of-all-js-class-elements","url":"https://yieldray.fun/posts/the-semantics-of-all-js-class-elements","title":"The Semantics of All JS Class Elements","date_published":"2024-12-09T22:40:00.000Z","date_modified":"2024-12-09T22:40:00.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://rfrn.org/~shu/2018/05/02/the-semantics-of-all-js-class-elements.html\">https://rfrn.org/~shu/2018/05/02/the-semantics-of-all-js-class-elements.html</a><br>过时警告：原文编写于 2018/05/02</p>\n</blockquote>\n<p>本文总结了当前和提议的类字段和方法语义。在撰写本文时，除了私有静态字段和方法外，所有描述的语义都已获得 TC39 的一致意见。</p>\n<p>我将讨论下表中的特性。完整的特性集是各列的乘积。</p>\n<table>\n<thead>\n<tr>\n<th>可见性</th>\n<th>位置</th>\n<th>类元素</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>公共</td>\n<td>实例</td>\n<td>字段</td>\n</tr>\n<tr>\n<td>私有</td>\n<td>静态</td>\n<td>方法</td>\n</tr>\n</tbody></table>\n<p>我将详细描述每一列的语义，以及它们彼此组合以及与现有 JS 语言特性的组合结果。</p>\n<p>ES6 已经提供了公共实例方法和静态方法。上述特性矩阵中的其余部分正在由三个独立的提案推进：</p>\n<ul>\n<li><a href=\"https://github.com/tc39/proposal-class-fields/\">公共和私有实例字段</a> 处于第 3 阶段</li>\n<li><a href=\"https://github.com/tc39/proposal-private-methods/\">私有实例方法</a> 处于第 3 阶段</li>\n<li><a href=\"https://github.com/tc39/proposal-static-class-features/\">公共和私有静态字段、私有静态方法</a> 处于第 3 阶段</li>\n</ul>\n<p>Chrome 72 已经提供了公共实例字段和公共静态字段。字段的实现工作正在 Firefox 和 Safari 中进行中。</p>\n<h2>心智模型</h2>\n<p>以下是上述各列的心智模型。下一节将通过具体的示例深入探讨语义，从而强化这些直觉。</p>\n<h3>可见性</h3>\n<p><strong>公共</strong>元素是可写且可配置的属性。作为属性，公共元素参与原型继承。</p>\n<p><strong>私有</strong>元素仅可以通过词法作用域名称在具有特定来源的对象上访问。由于它们不是属性，因此它们也不参与原型继承。</p>\n<p>换一种说法，私有的行为是公共行为的严格子集。封装正是通过词法作用域与对象分派来源限制的组合实现的。JS 中的私有概念是为 JS 设计的，与其他语言中的“私有”不同。</p>\n<h3>位置</h3>\n<p><strong>实例</strong>元素在类的实例上访问。</p>\n<p><strong>静态</strong>元素在类构造函数上访问。</p>\n<h3>类元素</h3>\n<p><strong>字段</strong>是与类关联的状态，具有良好的声明式语法。实例字段是每个实例的状态。静态字段是每个类构造函数的状态。</p>\n<p><strong>方法</strong>是与类关联的某种行为，具有良好的声明式语法。实例方法和静态方法每个类评估只有一个标识。实例方法位于原型上，静态方法位于类构造函数上。</p>\n<h2>通过示例具体说明语义</h2>\n<p>我希望通过以下示例阐明所有类元素的语义。每个示例之后都会简要解释突出显示的语义。</p>\n<h3>公共实例字段</h3>\n<p>类可以声明公共字段，这些字段可以在实例上作为属性访问。</p>\n<p>公共实例字段是由 <code>Object.defineProperty</code> 添加的属性。它们在对象的基本类构造时间，在构造函数体运行之前添加。</p>\n<pre><code class=\"language-javascript\">class Ex1 {\n    publicField;\n\n    constructor() {\n        let desc = Object.getOwnPropertyDescriptor(this, &quot;publicField&quot;);\n        assert(desc.value === undefined);\n        assert(desc.writable);\n        assert(desc.enumerable);\n        assert(desc.configurable);\n    }\n}\n\nnew Ex1();\n</code></pre>\n<hr>\n<p>对于子类，如果在调用 <code>super()</code> 之前访问 <code>this</code>，则会抛出 <code>ReferenceError</code>。[(*)] 因此，公共实例字段是在 <code>super()</code> 返回时添加的。</p>\n<pre><code class=\"language-javascript\">class Ex2_Base {\n    basePublicField;\n}\n\nclass Ex2_Sub extends Ex2_Base {\n    subPublicField;\n\n    constructor() {\n        super();\n\n        assert(this.hasOwnProperty(&quot;basePublicField&quot;));\n        assert(this.hasOwnProperty(&quot;subPublicField&quot;));\n    }\n}\n\nnew Ex2_Sub();\n</code></pre>\n<hr>\n<p>JavaScript 中的所有构造函数都可以返回不同的对象，从而覆盖 <code>new</code> 的结果以及来自 <code>super()</code> 的新绑定的 <code>this</code> 值。对于实例字段，如果超类构造函数返回不同的内容，则子类的字段仍然会被添加。</p>\n<pre><code class=\"language-javascript\">class Ex3_Base {\n    basePublicField;\n}\n\nclass Ex3_ReturnTrickBase {\n    trickyBasePublicField;\n\n    constructor() {\n        return new Ex3_Base();\n    }\n}\n\nclass Ex3_ReturnTrickSub extends Ex3_ReturnTrickBase {\n    trickySubPublicField;\n\n    constructor() {\n        super();\n\n        assert(!this.hasOwnProperty(&quot;trickyBasePublicField&quot;));\n        assert(this.hasOwnProperty(&quot;basePublicField&quot;));\n        assert(this.hasOwnProperty(&quot;trickySubPublicField&quot;));\n    }\n}\n\nnew Ex3_ReturnTrickSub();\n</code></pre>\n<hr>\n<p>公共字段名称可以像属性一样计算出来。它们在每个类评估时只计算一次。</p>\n<pre><code class=\"language-javascript\">let count = 0;\nfunction makeEx4(sym) {\n    return class Ex4 {\n        [(count++, sym)];\n    };\n}\n\nlet key = Symbol(&quot;key&quot;);\nlet Ex4 = makeEx4(key);\nassert(count === 0);\nlet ex4a = new Ex4();\nassert(ex4a.hasOwnProperty(key));\nassert(count === 1);\nlet ex4b = new Ex4();\nassert(ex4b.hasOwnProperty(key));\nassert(count === 1);\n</code></pre>\n<h3>公共实例方法</h3>\n<p>类可以声明公共方法，这些方法可以通过原型在实例上访问。</p>\n<p>公共实例方法在类评估时使用 <code>Object.defineProperty</code> 添加到类原型中。它们是可写的、不可枚举的和可配置的。</p>\n<pre><code class=\"language-javascript\">class Ex5 {\n    publicMethod() {\n        return 42;\n    }\n}\n\nlet desc = Object.getOwnPropertyDescriptor(Ex5.prototype, &quot;publicMethod&quot;);\nassert(desc.value === Ex5.prototype.publicMethod);\nassert(desc.writable);\nassert(!desc.enumerable);\nassert(desc.configurable);\n</code></pre>\n<hr>\n<p>生成器函数、异步函数和异步生成器函数形式也可以是公共实例方法。</p>\n<pre><code class=\"language-javascript\">class Ex6 {\n    *publicGeneratorMethod() {}\n    async publicAsyncMethod() {}\n    async *publicAsyncGeneratorMethod() {}\n}\n</code></pre>\n<hr>\n<p>getter 和 setter 也是可以的。没有生成器、异步或异步生成器 getter 和 setter 形式。</p>\n<pre><code class=\"language-javascript\">class Ex7 {\n    get publicAccessor() {\n        return 42;\n    }\n    set publicAccessor(x) {}\n}\n\nlet desc = Object.getOwnPropertyDescriptor(Ex7.prototype, &quot;publicAccessor&quot;);\nassert(desc.value === undefined);\nassert(!desc.enumerable);\nassert(desc.configurable);\n</code></pre>\n<hr>\n<p>在实例方法中，<code>super</code> 引用超类的 <code>prototype</code> 属性。以下是调用 <code>Base.prototype.basePublicMethod</code>。[^(†)]</p>\n<pre><code class=\"language-javascript\">class Ex8_Base {\n    basePublicMethod() {\n        return 42;\n    }\n}\n\nclass Ex8_Sub extends Ex8_Base {\n    subPublicMethod() {\n        return super.basePublicMethod();\n    }\n}\n\nassert(new Ex8_Sub().subPublicMethod() === 42);\n</code></pre>\n<h3>私有实例字段</h3>\n<p>类可以声明私有字段，这些字段可以在类声明本身的词法作用域内从基类或子类的实例中访问。</p>\n<p>私有实例字段用 <strong><code>#</code> 名称</strong>（称为“哈希名称”）声明，这些标识符以 <code>#</code> 为前缀。虽然字符不同，但这遵循了使用 <code>_</code> 前缀属性名称来表示私有的约定。</p>\n<p><code>#</code> 是新的 <code>_</code>，</p>\n<p>封装由语言而不是约定强制执行。<code>#</code> 是名称本身的一部分，在声明和访问中都使用。</p>\n<pre><code class=\"language-javascript\">class Ex9 {\n    #privateField;\n\n    constructor() {\n        this.#privateField = 42;\n    }\n}\n\nnew Ex9();\n</code></pre>\n<hr>\n<p><code>#</code> 名称的词法作用域规则比标识符名称的词法作用域规则更严格。引用不在作用域内的 <code>#</code> 名称是语法错误。</p>\n<pre><code class=\"language-javascript\">class Ex10_A {\n    #privateField;\n\n    constructor() {\n        this.#nonExistentField = 42; // 语法错误\n    }\n}\n</code></pre>\n<pre><code class=\"language-javascript\">class Ex10_B {\n    #privateField;\n}\n\nnew Ex10_B().#privateField; // 语法错误\n</code></pre>\n<hr>\n<p>与词法绑定一样，在相同作用域（即类声明）中具有多个同名 <code>#</code> 名称是语法错误，而通过嵌套作用域进行隐藏是允许的。</p>\n<p>请注意，由于所有 <code>#</code> 名称都以 <code>#</code> 开头，而属性名称不能以 <code>#</code> 开头，因此两者不会冲突。</p>\n<pre><code class=\"language-javascript\">class Ex11_A {\n  #privateField;\n  #privateField; // 语法错误\n}\n</code></pre>\n<pre><code class=\"language-javascript\">class Ex11_Outer {\n    #privateField;\n\n    constructor() {\n        class Ex11_Inner {\n            #privateField;\n\n            privateFieldValue() {\n                return this.#privateField;\n            }\n\n            constructor() {\n                this.#privateField = 42;\n            }\n        }\n\n        assert(new Ex11_Inner().privateFieldValue() === 42);\n        assert(this.#privateField === undefined);\n    }\n}\n\nnew Ex11_Outer();\n</code></pre>\n<hr>\n<p><code>delete</code> <code>#</code> 名称也是语法错误。</p>\n<pre><code class=\"language-javascript\">class Ex12 {\n  #privateField;\n  constructor() {\n    delete this.#privateField; // 语法错误\n  }\n}\n</code></pre>\n<hr>\n<p>私有字段访问严格比公共字段访问更严格。在没有具有该名称的私有字段的对象上获取 <code>#</code> 名称将引发 <code>TypeError</code>。设置 <code>#</code> 名称也是如此。私有字段不是属性，不参与原型继承之类的属性查找机制。这是为了启用封装，因为属性查找机制是可观察的，例如通过 <code>Proxy</code>。</p>\n<p>私有字段将词法作用域与分派来源限制相结合。对于私有实例字段，来源是通过声明私有实例字段的类构造的，无论是作为基类还是子类。</p>\n<pre><code class=\"language-javascript\">class Ex13 {\n    #privateField;\n\n    getField() {\n        return this.#privateField;\n    }\n    setField(v) {\n        this.#privateField = v;\n    }\n}\n\nlet ex13 = new Ex13();\nlet onProto = Object.create(ex13);\nassertThrows(() =&gt; ex13.getField.call({}), TypeError);\nassertThrows(() =&gt; ex13.setField.call({}, 42), TypeError);\nassertThrows(() =&gt; ex13.getField.call(onProto), TypeError);\n</code></pre>\n<hr>\n<p><code>#</code> 名称可以通过直接 <code>eval</code> 访问，就像其他词法作用域的内容一样。</p>\n<pre><code class=\"language-javascript\">class Ex14 {\n    #privateField;\n\n    constructor() {\n        eval(&quot;this.#privateField = 42&quot;);\n    }\n}\n\nnew Ex14();\n</code></pre>\n<hr>\n<p>私有字段与公共字段同时添加，在基类中在构造时间添加，或在子类中 <code>super()</code> 返回后添加。</p>\n<pre><code class=\"language-javascript\">class Ex15_Base {\n    #basePrivateField;\n\n    setBasePrivateField() {\n        this.#basePrivateField = 42;\n    }\n}\n\nclass Ex15_Sub extends Ex15_Base {\n    #subPrivateField;\n\n    constructor() {\n        super();\n\n        this.setBasePrivateField();\n        this.#subPrivateField = 84;\n    }\n}\n\nnew Ex15_Sub();\n</code></pre>\n<hr>\n<p>与公共实例字段一样，如果 <code>super()</code> 覆盖返回值，则子类的私有字段仍将被添加。对于实现者而言，这意味着私有字段可以添加到任意对象中。</p>\n<pre><code class=\"language-javascript\">class Ex16_ReturnTrickBase {\n    constructor() {\n        return new Proxy({}, {});\n    }\n}\n\nclass Ex16_ReturnTrickSub extends Ex16_ReturnTrickBase {\n    #subPrivateField;\n\n    constructor() {\n        super();\n\n        this.#subPrivateField = 42;\n    }\n}\n\nnew Ex16_ReturnTrickSub();\n</code></pre>\n<hr>\n<p>虽然 <code>#</code> 名称在语言中不是一等公民，但它们具有可观察的不同的每个类评估标识。一个类声明的评估不能访问同一声明的另一个评估的私有字段。</p>\n<pre><code class=\"language-javascript\">function makeEx17() {\n    return class Ex17 {\n        #privateField;\n\n        getField() {\n            return this.#privateField;\n        }\n    };\n}\n\nlet ex17a = new makeEx17();\nlet ex17b = new makeEx17();\nassertThrows(() =&gt; ex17a.getField.call(ex17b), TypeError);\nassertThrows(() =&gt; ex17b.getField.call(ex17a), TypeError);\n</code></pre>\n<p>最后，请注意，目前没有访问 <code>#</code> 名称的简写表示法。它们的访问需要接收者。</p>\n<h3>私有实例方法</h3>\n<p>私有实例方法类似于公共实例方法。它们的访问方式与私有实例字段相同。</p>\n<p>这些方法被指定为类实例的不可写私有字段。与私有实例字段一样，它们在基类的构造时间添加，并在子类的 <code>super()</code> 返回后添加。</p>\n<pre><code class=\"language-javascript\">class Ex18 {\n    #privateMethod() {\n        return 42;\n    }\n\n    constructor() {\n        assert(this.#privateMethod() === 42);\n        assertThrows(() =&gt; (this.#privateMethod = null), TypeError);\n    }\n}\n\nnew Ex18();\n</code></pre>\n<hr>\n<p>生成器函数、异步函数和异步生成器函数形式也可以是私有实例方法。</p>\n<pre><code class=\"language-javascript\">class Ex19 {\n    *#privateGeneratorMethod() {}\n    async #privateAsyncMethod() {}\n    async *#privateAsyncGeneratorMethod() {}\n}\n</code></pre>\n<hr>\n<p>getter 和 setter 也是可以的。对于私有实例方法，没有生成器、异步或异步生成器 getter 和 setter 形式。</p>\n<p>私有方法被指定为一个每个实例的 <code>#</code> 名称到属性描述符的映射列表。这保留了与公共实例方法的可表达形式的对称性，并强制执行私有字段附带的限制。</p>\n<pre><code class=\"language-javascript\">class Ex20 {\n    get #privateAccessor() {\n        return 42;\n    }\n    set #privateAccessor(x) {}\n\n    constructor() {\n        assert(this.#privateAccessor === 42);\n        this.#privateAccessor = &quot;ignored&quot;;\n    }\n}\n\nnew Ex20();\n</code></pre>\n<hr>\n<p>每个类评估只有一个私有实例方法的函数标识。即使它们被指定为每个实例的私有字段，实例方法也会在所有实例之间共享。</p>\n<pre><code class=\"language-javascript\">let exfiltrated;\nclass Ex21 {\n    #privateMethod() {}\n\n    constructor() {\n        if (exfiltrated === undefined) {\n            exfiltrated = this.#privateMethod;\n        }\n        assert(exfiltrated === this.#privateMethod);\n    }\n}\n\nnew Ex21();\n</code></pre>\n<hr>\n<p>在私有实例方法中，<code>super</code> 和 <code>this</code> 遵循与公共实例方法相同的语义。由于私有字段和方法不会添加到原型中，因此下面的 <code>#privateMethod()</code> 会抛出异常。类似地，当将不兼容来源的对象作为接收者传递给实例私有方法时，私有字段查找会抛出异常。</p>\n<pre><code class=\"language-javascript\">class Ex22_Base {\n  #privateMethod() { return 42; }\n}\n\nclass Ex22_Sub extends Ex22_Base {\n  #privateMethod() {\n    assertThrows(() =&gt; super.#privateMethod(), TypeError);\n  }\n\n  #privateMethodTwo() {\n    this.#privateMethod();\n  }\n\n  publicMethod() {\n    this.#privateMethodTwo();\n  }\n\n  constructor() {\n    this.#privateMethod();\n  }\n}\n\nassertThrows(() =&gt; (new Ex22_Sub).publicMethod.call({}), TypeError);\n</code></pre>\n<h3>实例字段初始化器</h3>\n<p>所有字段都可以使用就地声明的初始化器表达式进行初始化。初始化器按声明顺序运行。实例字段初始化器表达式被指定为不可观察实例方法的主体，这决定了 <code>this</code>、<code>new.target</code> 和 <code>super</code> 的值。</p>\n<p>没有初始化器的字段将初始化为 <code>undefined</code>。</p>\n<pre><code class=\"language-javascript\">class Ex23 {\n    #privateField = 42;\n    publicFieldOne = 84;\n    publicFieldTwo;\n\n    constructor() {\n        assert(this.#privateField === 42);\n        assert(this.publicFieldOne === 84);\n        assert(this.publicFieldTwo === undefined);\n    }\n}\n\nnew Ex23();\n</code></pre>\n<hr>\n<p>字段初始化器在添加字段时按声明顺序运行，并且此顺序可由初始化器观察到。在评估 <code>#privateField</code> 的初始化器时，由于 <code>publicField</code> 尚未添加，因此 <code>#privateField</code> 为 <code>false</code>。由于 <code>#privateField</code> 之后是 <code>publicField</code>，因此使用 <code>this.#privateField</code> 初始化 <code>publicField</code> 也不会出错。</p>\n<p>这些初始化器被指定为返回初始化器表达式结果的方法。在实现中不必对方法进行具体化，但这种规范的虚拟方法决定了 <code>this</code> 的值。在实例字段初始化器中，<code>this</code> 是正在构造的对象。在实例字段初始化器运行时，<code>this</code> 是可访问的。</p>\n<pre><code class=\"language-javascript\">class Ex24 {\n    #privateField = this.hasOwnProperty(&quot;publicField&quot;);\n    publicField = this.#privateField;\n\n    constructor() {\n        assert(!this.#privateField);\n        assert(!this.publicField);\n    }\n}\n\nnew Ex24();\n</code></pre>\n<hr>\n<p>在同一个类声明中允许使用多个同名的公共字段和方法，并且它们的初始化器按顺序运行。由于公共方法和字段是使用 <code>Object.defineProperty</code> 添加的属性，因此最后一个字段或方法会覆盖所有之前的字段或方法。</p>\n<p>对于私有字段和方法，这种情况不会发生，因为在相同作用域中使用多个同名的 <code>#</code> 名称是语法错误。</p>\n<pre><code class=\"language-javascript\">let log = &quot;&quot;;\nclass Ex25 {\n    publicField = ((log += &quot;1&quot;), 42);\n    publicField = ((log += &quot;2&quot;), 84);\n    publicField() {}\n}\n\nassert(typeof new Ex25().publicField === &quot;function&quot;);\nassert(log === &quot;12&quot;);\n</code></pre>\n<hr>\n<p>实例方法在运行任何初始化器之前添加，因此所有实例方法都可以在实例字段初始化器中使用。</p>\n<pre><code class=\"language-javascript\">class Ex26 {\n    #privateField = this.#privateMethod();\n    publicField = this.#privateField;\n\n    #privateMethod() {\n        return 42;\n    }\n}\n\nassert(new Ex26().publicField === 42);\n</code></pre>\n<hr>\n<p>在字段初始化器中，<code>new.target</code> 为 <code>undefined</code>。</p>\n<pre><code class=\"language-javascript\">class Ex27 {\n    publicField = new.target;\n}\n\nassert(new Ex27().publicField === undefined);\n</code></pre>\n<hr>\n<p>与实例方法一样，<code>super</code> 在实例字段初始化器中引用超类的原型。</p>\n<pre><code class=\"language-javascript\">class Ex28_Base {\n    baseMethod() {\n        return 42;\n    }\n}\n\nclass Ex28_Sub extends Ex28_Base {\n    subPublicField = super.baseMethod();\n}\n\nassert(new Ex28_Sub().subPublicField === 42);\n</code></pre>\n<hr>\n<p>类体始终是严格代码，因此初始化器不能通过直接 <code>eval</code> 泄漏任何新的绑定。</p>\n<pre><code class=\"language-javascript\">class Ex29 {\n    publicField = eval(&quot;var x = 42;&quot;);\n}\n\nnew Ex29();\nassertThrows(() =&gt; x, ReferenceError);\n</code></pre>\n<hr>\n<p>在字段初始化器中使用 <code>arguments</code> 是语法错误。</p>\n<pre><code class=\"language-javascript\">// 语法错误\nclass Ex30 {\n  publicField = arguments;\n}\n</code></pre>\n<h3>公共静态字段</h3>\n<p>类可以声明公共静态字段，这些字段可以在类构造函数上作为属性访问。</p>\n<p>公共静态字段在类评估时使用 <code>Object.defineProperty</code> 添加到类构造函数中。除此之外，它们的语义与公共实例字段相同。</p>\n<pre><code class=\"language-javascript\">class Ex31 {\n    static PUBLIC_STATIC_FIELD;\n}\n\nlet desc = Object.getOwnPropertyDescriptor(Ex31, &quot;PUBLIC_STATIC_FIELD&quot;);\nassert(desc.value === undefined);\nassert(desc.writable);\nassert(desc.enumerable);\nassert(desc.configurable);\n</code></pre>\n<hr>\n<p>公共静态字段仅在其定义的类上初始化，而不是在子类上重新初始化。子类构造函数具有其超类作为其原型。超类的公共静态字段通过原型链访问。</p>\n<pre><code class=\"language-javascript\">class Ex32_Base {\n    static BASE_PUBLIC_STATIC_FIELD;\n}\n\nclass Ex32_Sub extends Ex32_Base {\n    static SUB_PUBLIC_STATIC_FIELD;\n}\n\nassert(Ex32_Base.hasOwnProperty(&quot;BASE_PUBLIC_STATIC_FIELD&quot;));\nassert(Ex32_Sub.hasOwnProperty(&quot;SUB_PUBLIC_STATIC_FIELD&quot;));\nassert(Object.getPrototypeOf(Ex32_Sub) === Ex32_Base);\n</code></pre>\n<h3>公共静态方法</h3>\n<p>公共静态方法声明为函数形式，并且像公共静态字段一样，也可以作为类构造函数上的属性访问。与公共实例方法一样，也接受生成器函数、异步函数、异步生成器函数、getter 和 setter 形式。</p>\n<p>这些方法在类评估时使用 <code>Object.defineProperty</code> 添加到类构造函数中。与公共实例方法一样，它们是可写的、不可枚举的和可配置的。</p>\n<pre><code class=\"language-javascript\">class Ex33 {\n    static publicStaticMethod() {\n        return 42;\n    }\n}\n\nlet desc = Object.getOwnPropertyDescriptor(Ex33, &quot;publicStaticMethod&quot;);\nassert(desc.value === Ex33.publicMethod);\nassert(desc.writable);\nassert(!desc.enumerable);\nassert(desc.configurable);\n</code></pre>\n<hr>\n<p>在静态方法中，<code>super</code> 引用超类构造函数，并且可以访问超类的公共静态方法。[^(‡)]</p>\n<pre><code class=\"language-javascript\">class Ex34_Base {\n    static basePublicStaticMethod() {\n        return 42;\n    }\n}\n\nclass Ex34_Sub extends Ex34_Base {\n    static subPublicStaticMethod() {\n        return super.basePublicStaticMethod();\n    }\n}\n\nassert(Ex34_Sub.subPublicStaticMethod() === 42);\n</code></pre>\n<h3>私有静态字段</h3>\n<p>类可以声明私有字段，这些字段可以在类声明本身的词法作用域内从类构造函数中访问。</p>\n<p>私有静态字段在类评估时添加到类构造函数中。</p>\n<pre><code class=\"language-javascript\">class Ex35 {\n    static #PRIVATE_STATIC_FIELD;\n\n    static publicStaticMethod() {\n        Ex35.#PRIVATE_STATIC_FIELD = 42;\n        return Ex35.#PRIVATE_STATIC_FIELD;\n    }\n}\n\nassert(Ex35.publicStaticMethod() === 42);\n</code></pre>\n<hr>\n<p>私有静态字段的来源限制将访问限制为类构造函数。以下代码会抛出异常，因为 <code>basePublicStaticMethod</code> 的 <code>this</code> 值是子类构造函数，它没有 <code>#BASE_PRIVATE_STATIC_FIELD</code> 字段。这是私有字段来源限制与 <code>this</code> 动态性的组合的自然结果，尽管对某些人来说出乎意料。</p>\n<p>可以通过始终使用类构造函数作为接收者来访问私有静态元素来避免此类错误。</p>\n<pre><code class=\"language-javascript\">class Ex36_Base {\n    static #BASE_PRIVATE_STATIC_FIELD;\n\n    static basePublicStaticMethod() {\n        return this.#BASE_PRIVATE_STATIC_FIELD;\n    }\n}\n\nclass Ex36_Sub extends Ex36_Base {}\n\nassertThrows(() =&gt; Ex36_Sub.basePublicStaticMethod(), TypeError);\n</code></pre>\n<h3>私有静态方法</h3>\n<p>私有静态方法类似于公共静态方法。它们的访问方式与私有静态字段相同。</p>\n<p>这些方法被指定为类构造函数的不可写私有字段。与私有静态字段一样，它们在类评估时添加。</p>\n<p>与所有方法一样，也接受生成器函数、异步函数、异步生成器函数、getter 和 setter 形式。</p>\n<pre><code class=\"language-javascript\">class Ex37 {\n    static #privateStaticMethod() {\n        return 42;\n    }\n\n    constructor() {\n        assertThrows(() =&gt; (Ex37.#privateStaticMethod = null), TypeError);\n        assert(Ex37.#privateStaticMethod() === 42);\n    }\n}\n\nnew Ex37();\n</code></pre>\n<hr>\n<p>与公共静态方法一样，<code>super</code> 引用超类构造函数。</p>\n<pre><code class=\"language-javascript\">class Ex38_Base {\n    static basePublicStaticMethod() {\n        return 42;\n    }\n}\n\nclass Ex38_Sub extends Ex38_Base {\n    static #subStaticPrivateMethod() {\n        assert(super.basePublicStaticMethod() === 42);\n    }\n\n    static check() {\n        Ex38_Sub.#subStaticPrivateMethod();\n    }\n}\n\nEx38_Sub.check();\n</code></pre>\n<h3>静态字段初始化器</h3>\n<p>静态字段可以具有就地初始化器。与实例字段初始化器一样，它们也按声明顺序运行，并且表达式被指定为不可观察静态方法的主体。</p>\n<p>与实例字段初始化器一样，静态方法在运行任何初始化器之前添加，因此所有静态方法都可以在静态字段初始化器中使用。</p>\n<p>由于初始化器表达式被指定为静态方法主体，因此 <code>super</code> 引用超类构造函数，<code>this</code> 引用类构造函数。</p>\n<pre><code class=\"language-javascript\">class Ex39_Base {\n    static BASE_PUBLIC_STATIC_FIELD = this;\n    static basePublicStaticMethod() {\n        return 42;\n    }\n}\n\nclass Ex39_Sub extends Ex39_Base {\n    static SUB_PUBLIC_STATIC_FIELD = super.basePublicStaticMethod();\n}\n\nassert(Ex39_Sub.BASE_PUBLIC_STATIC_FIELD === Ex39_Base);\nassert(Ex39_Sub.SUB_PUBLIC_STATIC_FIELD === 42);\n</code></pre>\n<hr>\n<p>在评估静态字段初始化器时，类作用域内的类名绑定已初始化（即可以访问并且不会抛出 <code>ReferenceError</code>）。</p>\n<pre><code class=\"language-javascript\">class Ex40 {\n    static #PRIVATE_STATIC_FIELD = 42;\n    static PUBLIC_STATIC_FIELD = Ex40.#PRIVATE_STATIC_FIELD;\n}\n\nassert(Ex40.PUBLIC_STATIC_FIELD === 42);\n</code></pre>\n<h2>一些要点</h2>\n<p>我们已经探讨了所有 JavaScript 类元素的语义。也许有些语义令人惊讶，而另一些则在意料之中。我相信私有字段不匹配直觉的一个常见原因是 <code>#</code> 名称的来源限制，我希望本文有助于更清楚地说明这一点。最后，我提供这两句格言：</p>\n<p><code>#</code> 是新的 <code>_</code></p>\n<p><em>并且</em></p>\n<p>私有字段具有来源限制</p>\n<h2>致谢</h2>\n<p>我要感谢 <a href=\"https://twitter.com/littledan\">Dan Ehrenberg</a> 为类特性所做的不懈努力和对本文编辑的帮助，以及 <a href=\"https://twitter.com/robpalmer2\">Rob Palmer</a> 的校对和建议。</p>\n<h2>编辑</h2>\n<ol>\n<li>2018 年 5 月 2 日 — 更正了示例 38；重新格式化以在移动设备上更好地阅读。</li>\n<li>2018 年 5 月 3 日 — 将 <code>this</code> 语义添加到示例 39。</li>\n<li>2018 年 5 月 4 日 — 更正了示例 3。（来自 <a href=\"https://twitter.com/TChetwin\">Thomas Chetwin</a>）</li>\n<li>2018 年 11 月 6 日 — 将静态字段提案从第 2 阶段更新到第 3 阶段。</li>\n<li>2019 年 2 月 1 日 — 更正了示例 15。（来自 <a href=\"https://twitter.com/WomanCorn\">@WomanCorn</a>）</li>\n</ol>\n<p>[^(*)]: 在子类构造函数中，<code>this</code> 处于 TDZ（时间死区），直到 <code>super()</code> 返回。</p>\n<p>[^(†)]: 在实例方法中，[[HomeObject]] 是类原型。</p>\n<p>[^(‡)]: 在静态方法中，[[HomeObject]] 是类构造函数。</p>\n","tags":["ecma262"]},{"id":"whats-new-in-react19","url":"https://yieldray.fun/posts/whats-new-in-react19","title":"React19.0已发布","date_published":"2024-12-09T00:20:20.000Z","date_modified":"2024-12-09T00:20:20.000Z","content_text":"<p><a href=\"https://react.dev/blog/2024/12/05/react-19\">https://react.dev/blog/2024/12/05/react-19</a><br><a href=\"https://www.epicreact.dev/react-19-cheatsheet\">https://www.epicreact.dev/react-19-cheatsheet</a></p>\n<p>React 18 引入 transitions，即低优先级 UI 变更。<br><code>startTransition</code> 中传入的函数称为 transitions。</p>\n<pre><code class=\"language-jsx\">function Comp({ onClick, children }) {\n    const [isPending, startTransition] = useTransition(); // [!code highlight]\n    if (isPending) return &lt;button disabled&gt;rendering...&lt;/button&gt;;\n\n    const onClick = () =&gt; {\n        startTransition(() =&gt; {\n            onClick();\n        });\n    };\n    return &lt;button onClick={onClick}&gt;{children}&lt;/button&gt;;\n}\n</code></pre>\n<p>现在 React 19 支持异步 transitions， ，<strong>异步 transitions</strong> 称为 <strong>actions</strong>。<br>也就是说，startTransition 可以传入异步函数，而 useTransition 现在会考虑 Promise 的状态。</p>\n<pre><code class=\"language-jsx\">// Using pending state from Actions\nfunction UpdateName({}) {\n    const [name, setName] = useState(&quot;&quot;);\n    const [error, setError] = useState(null);\n    const [isPending, startTransition] = useTransition(); // [!code highlight]\n\n    const handleSubmit = () =&gt; {\n        startTransition(async () =&gt; {\n            const error = await updateName(name);\n            if (error) {\n                setError(error);\n                return;\n            }\n            redirect(&quot;/path&quot;);\n        });\n    };\n\n    return (\n        &lt;div&gt;\n            &lt;input value={name} onChange={(event) =&gt; setName(event.target.value)} /&gt;\n            &lt;button onClick={handleSubmit} disabled={isPending}&gt;\n                Update\n            &lt;/button&gt;\n            {error &amp;&amp; &lt;p&gt;{error}&lt;/p&gt;}\n        &lt;/div&gt;\n    );\n}\n</code></pre>\n<p>新增 useActionState 用于管理 <a href=\"https://react.dev/reference/react-dom/components/form\"><code>&lt;form&gt;</code></a> 的状态。</p>\n<p>useActionState 接受一个函数（Action），并返回一个包装后的 Action 供调用。<br>调用封装后的 Action 时，useActionState 将返回 Action 的最后结果 data 和 Action 的待定状态 pending 。</p>\n<blockquote>\n<p>可以看出，React 正在内置一些功能，避免什么事情都交给 useEffect</p>\n</blockquote>\n<pre><code class=\"language-jsx\">// Using &lt;form&gt; Actions and useActionState\nfunction ChangeName({ name, setName }) {\n    const [error, submitAction, isPending] = useActionState(async (previousState, formData) =&gt; {\n        const error = await updateName(formData.get(&quot;name&quot;));\n        if (error) {\n            return error;\n        }\n        redirect(&quot;/path&quot;);\n        return null;\n    }, null);\n\n    return (\n        &lt;form action={submitAction}&gt;\n            &lt;input type=&quot;text&quot; name=&quot;name&quot; /&gt;\n            &lt;button type=&quot;submit&quot; disabled={isPending}&gt;\n                Update\n            &lt;/button&gt;\n            {error &amp;&amp; &lt;p&gt;{error}&lt;/p&gt;}\n        &lt;/form&gt;\n    );\n}\n</code></pre>\n<p>引入 useFormStatus，可以读取父级 form 上次提交的状态（类似于 context）。</p>\n<pre><code class=\"language-jsx\">function Submit() {\n    const { pending, data, method, action } = useFormStatus(); // [!code highlight]\n    return &lt;button disabled={pending}&gt;Submit&lt;/button&gt;;\n}\n\nexport default function App() {\n    return (\n        &lt;form action={action}&gt;\n            &lt;Submit /&gt;\n        &lt;/form&gt;\n    );\n}\n</code></pre>\n<p><a href=\"https://react.dev/reference/react/useOptimistic\">useOptimistic</a> 已稳定</p>\n<pre><code class=\"language-jsx\">function ChangeName({ currentName, onUpdateName }) {\n    const [optimisticName, setOptimisticName] = useOptimistic(currentName); // [!code highlight]\n\n    const submitAction = async (formData) =&gt; {\n        const newName = formData.get(&quot;name&quot;);\n        setOptimisticName(newName); // [!code highlight]\n        const updatedName = await updateName(newName);\n        onUpdateName(updatedName);\n    };\n\n    return (\n        &lt;form action={submitAction}&gt;\n            &lt;p&gt;Your name is: {optimisticName}&lt;/p&gt; // [!code highlight]\n            &lt;p&gt;\n                &lt;label&gt;Change Name:&lt;/label&gt;\n                &lt;input\n                    type=&quot;text&quot;\n                    name=&quot;name&quot;\n                    disabled={currentName !== optimisticName} // [!code highlight]\n                /&gt;\n            &lt;/p&gt;\n        &lt;/form&gt;\n    );\n}\n</code></pre>\n<p><a href=\"https://react.dev/reference/react/use\">use</a> 已稳定。注意它<strong>不是</strong>一个 hook，无需<a href=\"https://react.dev/reference/rules/rules-of-hooks\">保证</a>每次渲染的调用数量一致。</p>\n<p>可用于读取：</p>\n<ul>\n<li>Promise：配合 <a href=\"https://react.dev/reference/react/Suspense\">Suspense</a> 使用</li>\n<li>Context：替代 <a href=\"https://react.dev/reference/react/useContext\">useContext</a></li>\n</ul>\n<blockquote>\n<p>可以发现，在一次性数据获取（异步请求）时，可以避免使用 useEffect</p>\n</blockquote>\n<pre><code class=\"language-jsx\">import { use } from &#39;react&#39;;\n\nfunction MessageComponent({ messagePromise }) {\n  const message = use(messagePromise);\n  const theme = use(ThemeContext);\n  // ...\n</code></pre>\n<p>新增 react-dom/static 用于 SSG，而 react-dom/server 用于 SSR</p>\n<ul>\n<li>SSR：支持在内容加载时进行流式处理</li>\n<li>SSG：等待所有数据加载完毕</li>\n</ul>\n<pre><code class=\"language-jsx\">import { prerender } from &quot;react-dom/static&quot;;\n\nasync function handler(request) {\n    const { prelude } = await prerender(&lt;App /&gt;, {\n        bootstrapScripts: [&quot;/main.js&quot;],\n    });\n    return new Response(prelude, {\n        headers: { &quot;content-type&quot;: &quot;text/html&quot; },\n    });\n}\n</code></pre>\n<p><a href=\"https://react.dev/reference/rsc/server-components\">RSC</a> 已稳定<br>另外新增 Server Functions，可以直接在 RSC 调用（实际上就是自动生成了后端接口）</p>\n<pre><code class=\"language-js\">async function updateName(name) {\n    &quot;use server&quot;;\n    if (!name) {\n        return { error: &quot;Name is required&quot; };\n    }\n    await db.users.updateName(name);\n}\n</code></pre>\n<p>不需要 forwardRef 了，很方便</p>\n<pre><code class=\"language-jsx\">function MyInput({ placeholder, ref }) {\n    return &lt;input placeholder={placeholder} ref={ref} /&gt;;\n}\n\n//...\n&lt;MyInput ref={ref} /&gt;;\n</code></pre>\n<p>无需 .Provider 了</p>\n<pre><code class=\"language-jsx\">//@@prettier-ignore\nconst ThemeContext = createContext(&quot;&quot;);\n\nfunction App({ children }) {\n    return &lt;ThemeContext value=&quot;dark&quot;&gt;{children}&lt;/ThemeContext&gt;;\n    // before: &lt;ThemeContext.Provider&gt;\n}\n</code></pre>\n<p>ref 支持返回 cleanup 函数</p>\n<blockquote>\n<p>BreakingChanges：而不是通过 null 调用 ref 函数</p>\n</blockquote>\n<pre><code class=\"language-jsx\">&lt;input\n    ref={(ref) =&gt; {\n        // ref 创建时调用\n\n        return () =&gt; {\n            // ref 从 DOM 移除后调用\n        };\n    }}\n/&gt;\n</code></pre>\n<p><a href=\"https://react.dev/reference/react/useDeferredValue\">useDeferredValue</a> 支持初始值参数</p>\n<blockquote>\n<p>useDeferredValue 本身会导致渲染两次，上一次的值用于呈现，当前值用于后台（指主线程低优先级）渲染</p>\n</blockquote>\n<pre><code class=\"language-jsx\">function Search({ deferredValue }) {\n    // On initial render the value is &#39;&#39;.\n    // Then a re-render is scheduled with the deferredValue.\n    const value = useDeferredValue(deferredValue, &quot;&quot;);\n    return &lt;Results query={value} /&gt;;\n}\n</code></pre>\n<p>支持在 CSR/SSR 环境下组件中使用元数据标签，会自动调整到文档的 head</p>\n<pre><code class=\"language-jsx\">function BlogPost({ post }) {\n    return (\n        &lt;article&gt;\n            &lt;h1&gt;{post.title}&lt;/h1&gt;\n            &lt;title&gt;{post.title}&lt;/title&gt;\n            &lt;meta name=&quot;author&quot; content=&quot;Josh&quot; /&gt;\n            &lt;link rel=&quot;author&quot; href=&quot;https://twitter.com/joshcstory/&quot; /&gt;\n            &lt;meta name=&quot;keywords&quot; content={post.keywords} /&gt;\n            &lt;p&gt;Eee equals em-see-squared...&lt;/p&gt;\n        &lt;/article&gt;\n    );\n}\n\nfunction ComponentOne() {\n  return (\n    &lt;Suspense fallback=&quot;loading...&quot;&gt;\n      &lt;link rel=&quot;stylesheet&quot; href=&quot;foo&quot; precedence=&quot;default&quot; /&gt;\n      &lt;link rel=&quot;stylesheet&quot; href=&quot;bar&quot; precedence=&quot;high&quot; /&gt;\n      &lt;article class=&quot;foo-class bar-class&quot;&gt;\n        {...}\n      &lt;/article&gt;\n    &lt;/Suspense&gt;\n  )\n}\n\nfunction ComponentTwo() {\n  return (\n    &lt;div&gt;\n      &lt;p&gt;{...}&lt;/p&gt;\n      &lt;link rel=&quot;stylesheet&quot; href=&quot;baz&quot; precedence=&quot;default&quot; /&gt;  &lt;-- will be inserted between foo &amp; bar\n    &lt;/div&gt;\n  )\n}\n\n\nfunction MyComponent() {\n  return (\n    &lt;div&gt;\n      &lt;script async={true} src=&quot;...&quot; /&gt;\n      Hello World\n    &lt;/div&gt;\n  )\n}\n</code></pre>\n<p>内置了资源预加载的支持</p>\n<pre><code class=\"language-jsx\">import { prefetchDNS, preconnect, preload, preinit } from &quot;react-dom&quot;;\nfunction MyComponent() {\n    preinit(&quot;https://.../path/to/some/script.js&quot;, { as: &quot;script&quot; }); // loads and executes this script eagerly\n    preload(&quot;https://.../path/to/font.woff&quot;, { as: &quot;font&quot; }); // preloads this font\n    preload(&quot;https://.../path/to/stylesheet.css&quot;, { as: &quot;style&quot; }); // preloads this stylesheet\n    prefetchDNS(&quot;https://...&quot;); // when you may not actually request anything from this host\n    preconnect(&quot;https://...&quot;); // when you will request something but aren&#39;t sure what\n}\n</code></pre>\n<p>支持 Custom Element，不过这部分的<a href=\"https://react.dev/reference/react-dom/components#custom-html-elements\">文档</a>还没更新</p>\n<blockquote>\n<p>注：不支持像 <a href=\"https://cn.vuejs.org/guide/extras/web-components\">vue</a> 这种编译为 WebComponent</p>\n</blockquote>\n<p>与 Custom Element 实例上的属性匹配的项将被指定为 properties，否则它们为 attributes</p>\n<blockquote>\n<p>TODO：还没写完</p>\n</blockquote>\n","tags":["react"]},{"id":"how-browser-render","url":"https://yieldray.fun/posts/how-browser-render","title":"浏览器：渲染管线","date_published":"2024-12-06T09:24:07.000Z","date_modified":"2024-12-12T12:12:12.000Z","content_text":"<h1>Prerequisites</h1>\n<p>本节用于快速说明理解渲染所需的前置知识</p>\n<blockquote>\n<p>全文以 Chromium/Blink 系为例</p>\n</blockquote>\n<div class=\"markdown-alert markdown-alert-caution\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-stop mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>\n<p>标准和规范是向后兼容的（Don&#39;t break the Web），但实现并非一成不变。<br>因此实现可能最终会过时，但思路是值得参考的。</p>\n</div>\n<h4><a href=\"https://webperf.tips/tip/browser-process-model/\">多进程/线程模型</a></h4>\n<ul>\n<li><p>1 个浏览器进程</p>\n</li>\n<li><p>N 个 sandbox 渲染器进程（实现<a href=\"https://www.chromium.org/Home/chromium-security/site-isolation/\">Site Isolation</a>）</p>\n<ul>\n<li>一个主线程：JavaScript（除了 worker）、DOM、CSS、样式和布局</li>\n<li>多个工作线程：运行 Web Workers、ServiceWorker 和 Worklets</li>\n<li>几个内部线程：处理 webaudio、数据库、GC</li>\n</ul>\n</li>\n<li><p>1 个 GPU 进程</p>\n</li>\n<li><p>1 个网络进程，实现网络堆栈</p>\n</li>\n<li><p>多个插件进程</p>\n</li>\n</ul>\n<p>浏览器与渲染器进程之间的通信是通过 <a href=\"https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md\">Mojo</a> 实现的</p>\n<p>跨线程通信：基于 (blink) PostTask API 进行消息传递，很少共享内存</p>\n<h4><a href=\"https://webperf.tips/tip/event-loop/\">事件循环</a></h4>\n<p>事件循环决定如何在<strong>主线程</strong>上调度任务。<strong>任务</strong>是不可打断（暂停）的。</p>\n<p>任务队列实际是特殊的优先队列，用于排队任务。</p>\n<p>绘制一帧之前（某些任务导致 UI 变更），必须优先运行渲染步骤任务（Style→Layout→Paint）。</p>\n<h4>See Also</h4>\n<ul>\n<li><a href=\"http://bit.ly/lifeofapixel\">http://bit.ly/lifeofapixel</a></li>\n<li><a href=\"https://developer.chrome.com/docs/chromium/renderingng-architecture\">https://developer.chrome.com/docs/chromium/renderingng-architecture</a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work\">https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work</a></li>\n<li><a href=\"https://taligarsiel.com/Projects/howbrowserswork1.htm\">https://taligarsiel.com/Projects/howbrowserswork1.htm</a></li>\n<li><a href=\"https://aerotwist.com/blog/the-anatomy-of-a-frame/\">https://aerotwist.com/blog/the-anatomy-of-a-frame/</a></li>\n<li><a href=\"https://chromium.googlesource.com/chromium/src/+/HEAD/docs/life_of_a_frame.md\">https://chromium.googlesource.com/chromium/src/+/HEAD/docs/life_of_a_frame.md</a></li>\n<li><a href=\"https://bit.ly/3nkZH4B\">https://bit.ly/3nkZH4B</a></li>\n<li><a href=\"https://developer.chrome.com/blog/inside-browser-part3\">https://developer.chrome.com/blog/inside-browser-part3</a></li>\n</ul>\n<h1>Render</h1>\n<style>object,img {max-width:100%}</style>\n\n<p>渲染的终极目标是将标记语言/样式定义/脚本操作渲染为屏幕上的像素。</p>\n<p><img src=\"/media/loap-review.svg\" alt=\"\"></p>\n<p>渲染管线中有很多中间态，使重渲染（更新）更高效。</p>\n<hr>\n<ol>\n<li><p>解析 HTML 为 DOM</p>\n</li>\n<li><p><a href=\"https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/resolver/style_resolver.cc;drc=42112cd2b5164c7410121180d173556d9c03ffdb\">解析并处理样式表</a>，计算出用于渲染的最终值（<a href=\"https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/style/computed_style.h;l=153-207;drc=f7f2dcfbd24f7ee74a0b306043bc757da65f64a6\">ComputedStyle</a>），并将样式附加到对应 DOM<br>规则：<a href=\"https://www.w3.org/TR/css-cascade-3/\">级联（cascading）与继承</a><br><strong>对于任一CSS属性</strong>：<br><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/initial_value\"><em>初始值</em></a>：每个属性有自己规范定义的初始值<br><em>声明值</em>：所有应用到当前元素的属性值<br><em>级联值</em>：根据级联规则得到的获胜声明<br><em>指定值</em>：若存在级联值，则是级联值；否则根据继承规则得到<br><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/computed_value\"><em>计算值</em></a>：将指定值绝对化，为继承做准备（注意：此时没有进行布局，所以可能是auto）<br><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/used_value\"><em>应用值</em></a>：（完成布局，得到可以用于渲染的绝对值）若计算值无效，则为空<br><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/actual_value\"><em>实际值</em></a>：实际渲染使用的值（此时可能考虑到物理设备做一些舍入）</p>\n<p> 层叠顺序（省略未实际实现的样式来源）：<br>  <code>过渡</code> → <code>作者样式!important</code> → <code>动画</code> → <code>作者样式</code> → <code>用户代理样式</code></p>\n</li>\n<li><p>这一步的目标是根据 CSS 声明的<a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/Layout_mode\">布局</a>，<a href=\"https://www.w3.org/TR/css-display-3/\">遵循 CSS 规范</a>地计算出每个布局矩形的坐标和尺寸，最终生成一颗 LayoutTree</p>\n<blockquote>\n<p>参见 <a href=\"https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_object.cc;l=329\">blink 源码</a>。另：在 blink 中我们称为 LayoutTree，某些文章会称为 RenderTree</p>\n</blockquote>\n<p> 每个 DOM 节点可能：</p>\n<ul>\n<li><p>没有对应 LayoutObject：<code>display:none</code></p>\n</li>\n<li><p>对应一个 LayoutObject：大部分情况</p>\n</li>\n<li><p>对应多个 LayoutObject：一个 LayoutObject 可以拥有块级子元素或内联子元素，但不能同时拥有两者。因此会生成匿名 LayoutObject 以满足规则。</p>\n<blockquote>\n<p>个人理解这里主要是为一组相连的内联盒子创建包含块。</p>\n</blockquote>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flow_layout/Block_and_inline_layout_in_normal_flow\">常规流</a>：常规流中的盒子属于格式化上下文，可以是块级或内联级，但不能同时是两者。块级盒子参与块级格式化上下文，内联级盒子参与内联级格式化上下文。</p>\n</div>\n</li>\n</ul>\n<p> <object type=\"image/svg+xml\" data=\"/media/loap-layout.svg\"></object></p>\n<p> 遍历 LayoutTree，对 LayoutObject 进行布局操作：计算多种边界矩形的坐标（例如：若内容溢出，则需要计算滚动边界，滚动条等）</p>\n</li>\n<li><p>绘制 LayoutObject。遵循层叠（stacking）顺序。这一步实际上是记录一系列绘制指令</p>\n<p> <object type=\"image/svg+xml\" data=\"/media/loap-paint.svg\"></object></p>\n</li>\n<li><p>光栅化：Skia 将绘制指令转换为 OpenGL/vulkan 调用（把高层指令转换为底层指令），最终生成 bitmap<br>（GPU 进行光栅化，将生成的位图置于 GPU 内存，通过 OpenGL 纹理对象引用）</p>\n<p> <object type=\"image/svg+xml\" data=\"/media/loap-raster.svg\"></object></p>\n</li>\n</ol>\n<h1>Dynamic Render</h1>\n<p>运行整个渲染管线来渲染帧是昂贵的。流水线的每个阶段跟踪细粒度的异步无效操作，尽可能的复用前一帧的中间态。（双缓冲）</p>\n<p><object type=\"image/svg+xml\" data=\"/media/loap-invalidation.svg\"></object></p>\n<p>如果一个大区域进行变换（例如滚动一个容器），绘制(Paint) + 光栅化(raster)操作仍然是昂贵的...<br>由于 JS 是阻塞且不可中断的，一切发生在主线程的操作还要与 JS 竞争。总之主线程是很繁忙的。</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-light-bulb mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>\n<p>DevTools 的<a href=\"https://webperf.tips/tip/collect-a-trace/\">性能</a>工具可用于分析任务</p>\n<p><img src=\"https://s2.loli.net/2024/12/06/5ktKyqWiuaz79TY.png\" alt=\"性能.png\" style=\"width:520px\"></p></div>\n<p>主线程将页面分层（layer），层可以独立光栅化。每个合成器线程可独立绘制层，分担主线程压力</p>\n<p><img src=\"/media/loap-composing.svg\" alt=\"\"></p>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-alert mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<p>不要与层叠上下文混淆！</p>\n</div>\n<p>合成器线程可以（虽然并非总可以）处理浏览器线程的输入事件，而无需主线程参与</p>\n<blockquote>\n<p>若用户输入会触发 DOM 已设置的监听器，则需要主线程来运行 JS，此时当然就不能只靠合成器线程处理了</p>\n<p>题外话：<br>不过现代浏览器对此也有一些默认优化。例如对于 <code>touchstart</code>, <code>touchmove</code>, <code>touchend</code>, <code>wheel</code>（不含 <code>scroll</code>）事件（默认行为会产生滚动）\n它们在 <code>Window</code> <code>Document</code> 和 <code>Document.body</code> 上默认为 passive，即浏览器默认不等待监听器执行完毕</p>\n<p>Related:<br><a href=\"https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener#using_passive_listeners\">Using passive listeners</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/touch-action\">css <code>touch-action</code></a><br><a href=\"https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/\">Smoother scrolling in Firefox 46 with APZ</a></p>\n</blockquote>\n<p><img src=\"/media/loap-threaded.svg\" alt=\"\"></p>\n<p>根据什么规则分层？（参见此处 <a href=\"https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/renderer/platform/graphics/compositing_reasons.h;l=18;drc=4e8e81f6eeb6969973f3ec97132d80339b92d227\">Blink 源码</a>）</p>\n<ul>\n<li>某些样式属性会触发布局对象生成一个层，特别是所有 3D 变换</li>\n<li>可滚动区域需要生成多个层 <code>overflow: scroll</code></li>\n<li>某些替换元素会生成层 <code>&lt;video&gt;</code> <code>&lt;canvas&gt;</code></li>\n<li>要求浏览器生成层，例如 <code>will-change: transform</code></li>\n</ul>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-alert mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<ul>\n<li>z-index 控制的是层叠顺序，即 paint 的先后顺序，与 layer 没有直接关联。</li>\n<li>一般来说，层（layer）也会创建自己的层叠上下文。毕竟它们都需要独立渲染了。（<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/paint/README.md\">参见 blink 文档</a>）</li>\n<li>根据 blink 源码，静态（即非动画）2D transform 不会创建层。<br>既然不会创建层，可以说不会 GPU 加速。简单的方法是加上 <code>translateZ(0)</code>。</li>\n<li>个人认为最佳实践是不要滥用 GPU 加速。可以认为现代浏览器对静态变换的优化是足够的，并且对动态变换（动画）已经做了 GPU 加速。<br>因此一般需要手动开启的场景为：非动画场景，且变换会经常发生</li>\n</ul>\n<p>see also: <a href=\"https://stackoverflow.com/questions/16148007/which-css-properties-create-a-stacking-context\">which-css-properties-create-a-stacking-context</a></p>\n</div>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>position: absolute</th>\n<th>transition :transform(&lt;2d&gt;)</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>渲染阶段</td>\n<td>Layout</td>\n<td>Composite</td>\n</tr>\n<tr>\n<td>BFC</td>\n<td>创建BFC</td>\n<td>不创建BFC</td>\n</tr>\n<tr>\n<td>创建层叠上下文</td>\n<td>Yes</td>\n<td>Yes</td>\n</tr>\n<tr>\n<td>文档流</td>\n<td>脱离文档流</td>\n<td>保持在文档流中</td>\n</tr>\n<tr>\n<td>定位参考</td>\n<td>相对于最近的定位祖先元素</td>\n<td>相对于元素自身</td>\n</tr>\n</tbody></table>\n<p>布局完成后，主线程将图层任务分配给多个合成器线程</p>\n<p><object type=\"image/svg+xml\" data=\"/media/loap-assign.svg\"></object></p>\n<p>具体来说，composer 向图层（layer）应用不同的属性（移动，缩放，裁剪，etc），构建一棵属性树</p>\n<p>当所有 paint 工作完成后，主线程阻塞一段时间以接收所有合成器线程的层和属性树</p>\n<p>图层可以很大，而视口可能只显示层的一部分，因此完整地光栅化整个层是昂贵的</p>\n<p>合成器线程将层分割为图块（tiles），图块是光栅化的最小单元，越接近视口的图块优先级越高<br>合成器将图块加入（GPU）线程池来进行光栅化</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-light-bulb mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>\n<p>DevTools 的<a href=\"https://webperf.tips/tip/layers-and-compositing/\">渲染</a>工具可以用于检查层和绘制</p>\n<p><img src=\"https://s2.loli.net/2024/12/06/ugKiVT56SDJhYRy.png\" alt=\"绘制.png\" style=\"width:580px\"></p></div>\n<p><object type=\"image/svg+xml\" data=\"/media/loap-tiles.svg\"></object></p>\n<p>一旦所有图块都完成光栅化，合成线程就会生成<em>绘制四边形</em>（draw quads）<br>四边形类似于一个命令，用于在屏幕的特定位置绘制图块，同时需要考虑图层树应用的所有变换<br>每个四边形都会引用内存中对应图块的光栅化输出（此时像素还未显示在屏幕上）<br>这些四边形被封装在一个合成帧（compositor frame）对象中，然后提交给 GPU 进程</p>\n<p>合成帧来自多个渲染器，也来自浏览器（浏览器有自己的用于UI的合成器<br>合成帧与表面（surface）相关联，表示它们将在屏幕上出现的位置<br>表面之间可以相互嵌入。浏览器UI可以嵌入渲染器。渲染器可以为跨源iframe嵌入其他渲染器<br>显示合成器在 GPU 进程的 Viz 合成线程上运行。它是 viz 服务（visuals 的简写）的一部分<br>显示合成器负责同步传入的帧，并理解嵌入表面之间的依赖关系（表面聚合）</p>\n<p><object type=\"image/svg+xml\" data=\"/media/loap-viz.svg\"></object></p>\n<p>在大多数平台上，显示合成器的输出是双缓冲的<br>图块绘制操作会绘制到后缓冲区，通过<em>交换</em>缓冲区来使其可见<br>最终，我们的像素显示在屏幕上了。:)</p>\n<h1>题外话</h1>\n<h2>回流并重绘</h2>\n<p>下面我们使用这个简单的模型，而非完整的 Style→Layout→Pre-Paint→Paint→Layerize→Commit（主线程）流程</p>\n<p><img src=\"https://s2.loli.net/2024/12/10/UEySZvphk35n28l.png\" alt=\"Render Steps Task.png\"></p>\n<blockquote>\n<p>回流（reflow）是 Gecko（Firefox）术语。上文讨论 blink 时我们使用布局（layout）一词，但下文会按惯例使用“回流”。</p>\n</blockquote>\n<p>正常情况下，浏览器每帧只需执行一次渲染步骤任务，称为<strong>正常回流</strong>。</p>\n<p>当 JavaScript 代码路径强制浏览器以<a href=\"https://gist.github.com/paulirish/5d52fb081b3570c81e3a\">超出其通常节奏的方式执行回流</a>过程时，称为<strong>强制回流</strong>。</p>\n<blockquote>\n<p>例如，<code>Element.getBoundingClientRect()</code> 需要布局引擎同步获取元素精确坐标信息，浏览器必须立即回流（强制同步布局）以计算出当前帧（指即将生成的帧）的信息（不能使用上一帧）。<br>参见 <a href=\"https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/dom/range.cc;l=1722;drc=38321ee39cd73ac2d9d4400c56b90613dee5fe29\">blink 源码</a>。立即回流就是运行 style 和 layout 步骤，但注意这些步骤本身也有优化策略。下面会提到如何防止破坏优化路径。</p>\n</blockquote>\n<p>当代码路径在一帧内快速连续多次执行强制回流时，称为<strong>布局抖动</strong>。</p>\n<blockquote>\n<p>参见：<a href=\"https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing\">https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing</a><br><a href=\"https://devhints.io/layout-thrashing\">https://devhints.io/layout-thrashing</a></p>\n</blockquote>\n<blockquote>\n<p>CSS 属性也可触发渲染步骤任务，见：<a href=\"https://csstriggers.com/\">https://csstriggers.com/</a></p>\n</blockquote>\n<hr>\n<p>初次渲染时，通过静态 HTML 得到静态 DOM 结构，此时渲染是增量的：遍历渲染树时，可以根据固定的上下文布局得到当前节点布局。</p>\n<p>每次 JavaScript 需要查询当前帧布局信息时，都必须回流。多次查询就会导致多次回流。</p>\n<p><img src=\"https://s2.loli.net/2024/12/11/bN61zISHaYvdkte.png\" alt=\"\"></p>\n<p>当（JavaScript 操作导致）DOM 变更时，若变动的渲染节点层级越高，则需要重新计算的子树也就越多；</p>\n<p>反之，若渲染树没有变更，则回流可以使用上一次的布局信息（快速路径）。</p>\n<p>若同时变更 DOM 并立即查询其布局，则需要立即回流，且回流必须完成重新布局。</p>\n<hr>\n<p>这就揭示出优化策略：避免在一帧中多次变更 DOM 并查询布局，这会导致破坏布局快速路径（布局抖动）</p>\n<pre><code class=\"language-js\">const elements = [...document.querySelectorAll(&quot;.some-class&quot;)];\n\n// Bad\nfor (const element of elements) {\n    element.classList.add(&quot;width-adjust&quot;); // invalidate Layout Tree\n    element.getBoundingClientRect(); //   force a reflow\n}\n\n// Improved\nconst rects = elements.map((element) =&gt; element.getBoundingClientRect());\nelements.forEach((element) =&gt; element.classList.add(&quot;width-adjust&quot;));\n</code></pre>\n<pre><code class=\"language-js\">// Bad\nfor (let i = 0; i &lt; 10; i++) {\n    elements[i].style.width = `${container.offsetWidth + 10}px`;\n}\n\n// Improved\nconst containerWidth = container.offsetWidth;\nfor (let i = 0; i &lt; 10; i++) {\n    elements[i].style.width = `${containerWidth + 10}px`;\n}\n</code></pre>\n<p>或变更 DOM 后（通过 requestAnimationFrame）在下一帧读取其布局，避免强制回流。</p>\n<h2>React useLayoutEffect</h2>\n<p>这也是 React 的 <a href=\"https://zh-hans.react.dev/reference/react/useLayoutEffect\"><code>useLayoutEffect</code></a> 会影响性能的原因。它在提交 DOM 之前运行，阻塞当前帧渲染。</p>\n<p>也可以使用 useEffect 或 ResizeObserver 来避免阻塞当前帧，但需要考虑布局偏移。</p>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-alert mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<p>**布局抖动（Layout Thrashing）<strong>是在一帧内发生的。而</strong>布局偏移（Layout Shift）**是多次实际渲染导致的。<br>关于布局抖动，参见 <a href=\"https://web.dev/articles/inp\">INP</a> 指标；关于布局偏移，参见 <a href=\"https://web.dev/articles/cls\">CLS</a> 指标</p>\n</div>\n<pre><code class=\"language-tsx\">let i = 0;\n\nfunction MeasureExample() {\n    console.log(++i); // =&gt; 1 2 3\n    const [height, setHeight] = useState(0);\n\n    const measuredRef = (node: HTMLElement) =&gt; {\n        const observer = new ResizeObserver(([entry]) =&gt; {\n            setHeight(entry.contentRect.height);\n        });\n\n        observer.observe(node);\n        return () =&gt; observer.disconnect();\n    };\n\n    return (\n        &lt;&gt;\n            &lt;h1 ref={measuredRef}&gt;Hello, world ({i})&lt;/h1&gt;\n            &lt;h2&gt;The above header is {Math.round(height)}px tall&lt;/h2&gt;\n        &lt;/&gt;\n    );\n}\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/Window/requestAnimationFrame\">requestAnimationFrame</a></h2>\n<pre><code class=\"language-ts\">/**\n * 通过 raf 调度的任务在 下一次重绘（帧）之前 运行\n */\ndeclare function requestAnimationFrame(callback: FrameRequestCallback): number;\n\ninterface FrameRequestCallback {\n    /** 当前所处主线程帧的开始时间 */\n    (time: DOMHighResTimeStamp): void;\n}\n</code></pre>\n<p><img src=\"https://s2.loli.net/2024/12/30/EYSbiu6Q1UFXKRg.png\" alt=\"life-of-a-frame.png\">\n<img src=\"https://s2.loli.net/2024/12/30/dcDFUNyBeTfH4wl.png\" alt=\"event-dispatch-diagram.png\"></p>\n<blockquote>\n<p>图片引用自：<a href=\"https://medium.com/@paul_irish/requestanimationframe-scheduling-for-nerds-9c57f7438ef4\">https://medium.com/@paul_irish/requestanimationframe-scheduling-for-nerds-9c57f7438ef4</a></p>\n</blockquote>\n<p>MDN 上的说明目前比较简略，具体参见 HTML 标准 <a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering\">update the rendering</a> 一节。</p>\n<p>根据规范（<a href=\"https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#run-the-animation-frame-callbacks\">run the animation frame callbacks</a>），排队在同一帧的所有 raf 将会在实际调度 raf 阶段一口气同步执行。</p>\n<p>举个例子：</p>\n<pre><code class=\"language-html\">&lt;style&gt;\n    body {\n        margin: 0;\n        height: 100vh;\n        background-size: 200% 200%;\n        background-image: linear-gradient(135deg, #ff7c7c, #ffeb3b, #26c6da, #7e57c2);\n        animation: glow 10s ease infinite;\n        /* 背景颜色持续变化，因此浏览器会持续重绘 */\n    }\n\n    @keyframes glow {\n        0%,\n        100% {\n            background-position: 0% 50%;\n        }\n        50% {\n            background-position: 100% 50%;\n        }\n    }\n&lt;/style&gt;\n\n&lt;script&gt;\n    function loopRaf() {\n        for (\n            let i = 0;\n            i &lt; 50000; // 把这个值调大，效果更明显\n            i++\n        ) {\n            requestAnimationFrame((currentFrame) =&gt; {\n                console.log(currentFrame); // 当前帧开始时间，因此每次打印的值相同\n                console.log(performance.now()); // 实际执行时间\n            });\n        }\n    }\n\n    setTimeout(loopRaf, 2000);\n&lt;/script&gt;\n</code></pre>\n<p>上例中，在同一帧中安排了非常多 raf，因为同一帧的所有 raf 会同步顺序执行，运行完后才能重绘，因此 raf 阶段会观测到明显卡顿。</p>\n<hr>\n<p>当前帧排队的 raf 在下一帧之前运行，即当前帧的 ref 在当前帧运行。但 css 过渡则需要至少两帧才能过渡。<br>因此常见的模式是嵌套两个 raf：</p>\n<pre><code class=\"language-ts\">const doubleRAF = (cb: () =&gt; unknown) =&gt; {\n    requestAnimationFrame(() =&gt; requestAnimationFrame(() =&gt; cb()));\n};\n</code></pre>\n","tags":["chromium"]},{"id":"js-prioritized-task-scheduling-api","url":"https://yieldray.fun/posts/js-prioritized-task-scheduling-api","title":"Web 优先任务调度 API","date_published":"2024-12-01T01:23:45.000Z","date_modified":"2024-12-01T01:23:45.000Z","content_text":"<p><img src=\"https://bcd.deno.dev/api.Scheduler\" alt=\"bcd\"></p>\n<h1>定义</h1>\n<pre><code class=\"language-ts\">declare global {\n    /**  用于使用优先级任务调度 API 的全局 {@link Scheduler} 入口点。*/\n    const scheduler: Scheduler;\n\n    interface Scheduler {\n        /**\n         * 将任务作为回调添加到调度程序，可选择指定优先级、延迟和/或用于中止任务的信号。\n         * @param callback 实现任务的回调函数。回调的返回值用于 resolve 此函数返回的 Promise。\n         * @param options {@link SchedulerPostTaskOptions} 选项。\n         */\n        postTask&lt;T extends unknown&gt;(callback: () =&gt; T, options?: SchedulerPostTaskOptions): Promise&lt;T&gt;;\n        /** 返回一个 Promise，当 await 时，会让出事件循环，允许在新任务中继续执行。*/\n        yield(): Promise&lt;void&gt;;\n    }\n}\n\n/** 决定任务运行顺序的任务优先级。*/\ntype TaskPriority = &quot;user-blocking&quot; | &quot;user-visible&quot; | &quot;background&quot;;\n\ntype SchedulerPostTaskOptions = {\n    /**\n     * 任务的不可变 {@link TaskPriority}。值为 `&quot;user-blocking&quot;`、`&quot;user-visible&quot;` 或 `&quot;background&quot;` 之一。\n     * 若设置，则此优先级将用于任务的整个生命周期，并将忽略在 `signal` 上设置的优先级。\n     */\n    priority?: TaskPriority;\n    /** 一个 {@link AbortSignal} 或 {@link TaskSignal}，可用于中止或重新确定任务的优先级（通过其关联的控制器）。若设置了 `priority`，则信号的优先级将被忽略。*/\n    signal?: AbortSignal | TaskSignal;\n    /**\n     * 将任务添加到调度程序队列后的最短时间（以毫秒为单位）。\n     * 实际延迟可能高于指定值，但不会低于指定值。默认延迟为 0。\n     */\n    delay?: number;\n};\n</code></pre>\n<pre><code class=\"language-ts\">declare global {\n    /** 扩展 AbortController，允许设置和更改关联 TaskSignal 的优先级。*/\n    class TaskController extends AbortController {\n        constructor(options?: TaskControllerOptions);\n        /** 与此控制器关联的 TaskSignal。*/\n        readonly signal: TaskSignal;\n        /** 设置关联 TaskSignal 的优先级。*/\n        setPriority(priority: TaskPriority): void;\n    }\n\n    /** 当 TaskSignal 的优先级更改时触发的事件。*/\n    class TaskPriorityChangeEvent extends Event {\n        constructor(type: string, init: TaskPriorityChangeEventInit);\n        /** 更改前的优先级。*/\n        readonly previousPriority: TaskPriority;\n    }\n\n    /** 扩展 AbortSignal 以包含优先级信息和 prioritychange 事件。*/\n    class TaskSignal extends AbortSignal {\n        /** 此信号的当前优先级。*/\n        readonly priority: TaskPriority;\n        /** 当此信号的优先级更改时触发的事件处理程序。*/\n        onprioritychange: (event: TaskPriorityChangeEvent) =&gt; void;\n    }\n}\n\ntype TaskControllerOptions = {\n    /** TaskController 创建时 TaskSignal 的初始优先级。*/\n    priority?: TaskPriority;\n};\n\ninterface TaskPriorityChangeEventInit extends EventInit {\n    /** 更改前的优先级。*/\n    previousPriority: TaskPriority;\n}\n</code></pre>\n<h1>可中断任务</h1>\n<p><a href=\"https://web.dev/articles/optimize-long-tasks\">优化耗时长的任务</a>的本质是<strong>防止主线程阻塞</strong>。</p>\n<blockquote>\n<p>主线程一次只能处理一个任务。任何用时超过 50 毫秒的任务都是耗时较长的任务。对于耗时超过 50 毫秒的任务，任务的总时间减去 50 毫秒称为任务的<strong>阻塞时段</strong>。</p>\n</blockquote>\n<p>scheduler.yield 提供一种简洁的机制，允许我们显式将主线程让出给（浏览器）事件循环。</p>\n<pre><code class=\"language-ts\">async function slowTask() {\n    firstHalfOfWork();\n    await scheduler.yield(); // 让出主线程\n    secondHalfOfWork();\n}\n</code></pre>\n<p>scheduler.yield 可以简单的看作是 scheduler.postTask 的封装</p>\n<pre><code class=\"language-ts\">// from https://github.com/GoogleChromeLabs/scheduler-polyfill\nfunction schedulerYield() {\n    // Use &#39;user-blocking&#39; priority to get similar scheduling behavior as\n    // scheduler.yield(). Note: we can&#39;t reliably inherit priority and abort since\n    // we lose context if async functions are spread across multiple tasks.\n    return self.scheduler.postTask(() =&gt; {}, { priority: &quot;user-blocking&quot; });\n}\n</code></pre>\n<hr>\n<p>scheduler.postTask 允许我们向某些（浏览器实现的）任务队列提交任务，并允许指定任务优先级为以下三个之一：</p>\n<ul>\n<li><p>user-blocking<br>阻止用户与页面交互的任务。这包括将页面渲染到可以使用的程度，或响应用户输入。</p>\n</li>\n<li><p>user-visible（默认值）<br>用户可见但不一定会阻止用户操作的任务。这可能包括呈现页面的非必要部分，如非必要的图像或动画。</p>\n</li>\n<li><p>background<br>时间不紧迫的任务。这可能包括日志处理或初始化渲染不需要的第三方库。</p>\n</li>\n</ul>\n<p>还可以通过 TaskController/TaskSignal 实现可变优先级。</p>\n<pre><code class=\"language-js\">function saveSettings() {\n    // Validate the form at high priority\n    scheduler.postTask(validateForm, { priority: &quot;user-blocking&quot; });\n\n    // Show the spinner at high priority:\n    scheduler.postTask(showSpinner, { priority: &quot;user-blocking&quot; });\n\n    // Update the database in the background:\n    scheduler.postTask(saveToDatabase, { priority: &quot;background&quot; });\n\n    // Update the user interface at high priority:\n    scheduler.postTask(updateUI, { priority: &quot;user-blocking&quot; });\n\n    // Send analytics data in the background:\n    scheduler.postTask(sendAnalytics, { priority: &quot;background&quot; });\n}\n</code></pre>\n<p><img src=\"https://s2.loli.net/2024/12/01/ersvlyJ2RK3QEgM.png\" alt=\"\"></p>\n<p>可以简单的将三种优先级视为下面三种队列：</p>\n<p><code>user-blocking</code>: MessageChannel.postMessage\n<code>user-visible</code>: setTimeout\n<code>background</code>: requestIdleCallback</p>\n<h1>题外话：React scheduler</h1>\n<p>对于库来说，可能需要更粒度化的优先级，<a href=\"https://github.com/facebook/react/blob/main/packages/scheduler/src/SchedulerPriorities.js\">React scheduler</a> 中包含\nNoPriority ImmediatePriority UserBlockingPriority NormalPriority LowPriority IdlePriority 优先级。实现所谓“时间切片”。</p>\n<p>任务调度的<a href=\"https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js\">入口函数</a>如下：</p>\n<pre><code class=\"language-ts\">// taskQueue: 最小堆，存储所有准备好执行的任务，按过期时间排序\n// timerQueue: 最小堆，存储延迟执行的任务，按开始时间排序\n\nfunction unstable_scheduleCallback(\n    priorityLevel: PriorityLevel,\n    callback: Callback,\n    options?: { delay: number },\n): Task {\n    var currentTime = getCurrentTime();\n\n    var startTime;\n    if (typeof options === &quot;object&quot; &amp;&amp; options !== null) {\n        var delay = options.delay;\n        if (typeof delay === &quot;number&quot; &amp;&amp; delay &gt; 0) {\n            startTime = currentTime + delay;\n        } else {\n            startTime = currentTime;\n        }\n    } else {\n        startTime = currentTime;\n    }\n\n    var timeout;\n    switch (priorityLevel) {\n        case ImmediatePriority:\n            // Times out immediately\n            timeout = -1;\n            break;\n        case UserBlockingPriority:\n            // Eventually times out\n            timeout = userBlockingPriorityTimeout;\n            break;\n        case IdlePriority:\n            // Never times out\n            timeout = maxSigned31BitInt;\n            break;\n        case LowPriority:\n            // Eventually times out\n            timeout = lowPriorityTimeout;\n            break;\n        case NormalPriority:\n        default:\n            // Eventually times out\n            timeout = normalPriorityTimeout;\n            break;\n    }\n\n    var expirationTime = startTime + timeout;\n\n    var newTask: Task = {\n        id: taskIdCounter++,\n        callback,\n        priorityLevel,\n        startTime,\n        expirationTime,\n        sortIndex: -1,\n    };\n\n    if (startTime &gt; currentTime) {\n        // This is a delayed task.\n        newTask.sortIndex = startTime;\n        push(timerQueue, newTask);\n        if (peek(taskQueue) === null &amp;&amp; newTask === peek(timerQueue)) {\n            // All tasks are delayed, and this is the task with the earliest delay.\n            if (isHostTimeoutScheduled) {\n                // Cancel an existing timeout.\n                cancelHostTimeout();\n            } else {\n                isHostTimeoutScheduled = true;\n            }\n            // Schedule a timeout.\n            requestHostTimeout(handleTimeout, startTime - currentTime);\n        }\n    } else {\n        newTask.sortIndex = expirationTime;\n        push(taskQueue, newTask);\n        // Schedule a host callback, if needed. If we&#39;re already performing work,\n        // wait until the next time we yield.\n        if (!isHostCallbackScheduled &amp;&amp; !isPerformingWork) {\n            isHostCallbackScheduled = true;\n            requestHostCallback();\n        }\n    }\n\n    return newTask;\n}\n</code></pre>\n<p>任务调度过程如下：</p>\n<ol>\n<li>调用 <code>unstable_scheduleCallback</code> 函数，传入任务的优先级、回调函数和可选的延迟时间</li>\n<li><code>unstable_scheduleCallback</code> 函数会创建一个 <code>Task</code> 对象，包含任务的优先级、回调函数、开始时间、过期时间等信息</li>\n<li>根据任务的 <code>startTime</code> (当前时间 + 延迟时间)，判断任务是否需要延迟执行</li>\n<li>如需延迟执行，将任务添加到 <code>timerQueue</code> 中；否则添加到 <code>taskQueue</code> 中</li>\n<li>如果调度器当前空闲（<code>isHostCallbackScheduled</code> 为 <code>false</code> 且 <code>isPerformingWork</code> 为 <code>false</code>），则调用 <code>requestHostCallback</code> 函数</li>\n<li><code>requestHostCallback</code> 函数会将 <code>performWorkUntilDeadline</code> 函数添加到主线程的消息队列中</li>\n<li>当主线程空闲时，会执行 <code>performWorkUntilDeadline</code> 函数</li>\n<li><code>performWorkUntilDeadline</code> 函数会调用 <code>flushWork</code> 函数</li>\n<li><code>flushWork</code> 函数会调用 <code>workLoop</code> 函数，循环执行 <code>taskQueue</code> 中的任务，直到队列为空或时间切片耗尽</li>\n<li>在 <code>workLoop</code> 的循环中，会调用 <code>advanceTimers</code> 函数，将 <code>timerQueue</code> 中到期的任务转移到 <code>taskQueue</code> 中</li>\n</ol>\n<blockquote>\n<p>在浏览器中，调度器基于 <a href=\"https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-postmessage-dev\"><code>MessageChannel.postMessage</code></a> 实现。它可以看作是入队优先级较高的宏任务，而 <a href=\"https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers\">setTimeout</a> 会有（HTML 标准定义的）4ms 延迟时间问题。<br>另外，注意微任务执行完毕后，不会将控制权返回给事件循环。</p>\n</blockquote>\n<p>另外可以发现，<a href=\"https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/SchedulerPostTask.js\">React schedular 源码</a> 也在实验性使用 Scheduler postTask API。</p>\n","tags":["web"]},{"id":"whats-new-in-node22","url":"https://yieldray.fun/posts/whats-new-in-node22","title":"Node.js 22 新特性","date_published":"2024-11-29T22:22:22.000Z","date_modified":"2024-11-29T22:22:22.000Z","content_text":"<p><a href=\"https://nodejs.org/en/blog/announcements/v22-release-announce\">Node.js 22</a> 已成为 LTS 版本了。让我们快速过一下 Node22 相比 Node20 新增的实验特性和已稳定的实验级特性！</p>\n<blockquote>\n<p>截止编写日期，下面以 Node 20.18.1 和 Node 22.11.0 为例</p>\n</blockquote>\n<h1>稳定功能</h1>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--run\">--run</a></h2>\n<p>可以看作是精简版的 <a href=\"https://docs.npmjs.com/cli/v10/commands/npm-run-script\">npm run</a>，支持搜寻上层目录的 <code>package.json</code> 和添加 <code>./node_modules/.bin</code> 到 <code>PATH</code>。<br>不支持 <code>pre</code> 和 <code>post</code> hook。</p>\n<p>另外，在传递额外参数时需要：</p>\n<pre><code class=\"language-sh\">node --run test -- --verbose\n</code></pre>\n<p>对于 npm，则只对 <code>-</code> 开头的参数要求必须使用额外的 <code>--</code>。</p>\n<blockquote>\n<p>简单起见，总是使用额外的 <code>--</code> 就好。node 和 npm 解析命令行参数策略不同，本文就不深入了。</p>\n</blockquote>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--watch\">--watch</a></h2>\n<p><code>--watch</code> 已稳定！</p>\n<h1>不稳定功能</h1>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--experimental-require-module\">--experimental-require-module</a></h2>\n<p>启用后，支持在 CommonJS 模块中使用 <code>require()</code> 导入<strong>同步 ES 模块</strong></p>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--experimental-strip-types\">--experimental-strip-types</a></h2>\n<p>Node 实现了一个叫 <a href=\"https://github.com/nodejs/amaro\">amaro</a> 的 <code>@swc/wasm-typescript</code> 包装层，实现了类型擦除。</p>\n<p>启用此选项后支持导入和运行 <code>.ts</code> <code>.mts</code> <code>.cts</code> 脚本，<strong>仅做类型擦除</strong>（可以理解为将类型部分替换为空格）。<br>不支持 tsconfig.json，当然也不支持 <code>.tsx</code>。</p>\n<hr>\n<p>TypeScript 5.7 之前需要启用 <a href=\"https://www.typescriptlang.org/tsconfig/#allowImportingTsExtensions\">allowImportingTsExtensions</a> 才支持导入 <code>.ts</code> 后缀。<br>并且 tsc 编译器实际上避免重写任何路径，因此开启 allowImportingTsExtensions 的同时必须同时启用 <a href=\"https://www.typescriptlang.org/tsconfig/#noEmit\">noEmit</a></p>\n<p>TypeScript 5.7 支持 <a href=\"https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/#path-rewriting-for-relative-paths\">rewriteRelativeImportExtensions </a> 来启用编译器对 <code>.ts</code> <code>.tsx</code> <code>.cts</code> <code>.mts</code> 后缀的相对导入路径改写。</p>\n<p>必须满足<strong>ts 文件后缀</strong>和<strong>相对路径</strong>，才会改写！否则会保留原样。以下情况不会改写：</p>\n<pre><code class=\"language-ts\">import &quot;some-package/file.ts&quot;;\n\nimport &quot;#root/file.ts&quot;;\n// package.json\n{\n    &quot;name&quot;: &quot;my-package&quot;,\n    &quot;imports&quot;: {\n        &quot;#root/*&quot;: &quot;./dist/*&quot;\n    }\n}\n\nimport &quot;@/file.ts&quot;;\n// tsconfig.json\n{\n    &quot;compilerOptions&quot;: {\n        &quot;module&quot;: &quot;nodenext&quot;,\n        // ...\n        &quot;paths&quot;: {\n            &quot;@/*&quot;: [&quot;./src/*&quot;]\n        }\n    }\n}\n</code></pre>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--experimental-transform-types\">--experimental-transform-types</a></h2>\n<p>等价于同时启用 --experimental-strip-types 和 --enable-source-maps</p>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--experimental-websocket\">--experimental-websocket</a></h2>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--experimental-sqlite\">--experimental-sqlite</a></h2>\n<p>启用后，提供 <code>node:sqlite3</code> 模块。</p>\n<blockquote>\n<p>看起来可能是 Node 内部之前用 sqlite3 支持了 WebStorage，现在正好做一个 sqlite3 的 binding。</p>\n</blockquote>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--experimental-test-coverage\">--experimental-test-coverage</a></h2>\n<p>继支持 --test 后，也支持生成测试覆盖率了。另外 Node 也新增一些测试模块工具，这里不展开了。</p>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/fs.html#fsglobpattern-options-callback\">fs.{glob, globSync}</a></h2>\n<p>原生支持 glob 函数</p>\n<blockquote>\n<p>希望将来可以不用在 glob/fast-glob/picomatch 等一堆 glob 库里面做选择了。</p>\n</blockquote>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/util.html#utilgetcallsiteframes\">util.getCallSite</a></h2>\n<p>可以获取当前所有调用帧信息，很实用的工具函数</p>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/module.html#moduleenablecompilecachecachedir\">module.enableCompileCache</a></h2>\n<p>启用后，每当 Node.js 编译 CommonJS 或 ECMAScript 模块时，都会使用指定目录中的磁盘 V8 代码缓存来加快编译速度。<br>这可能会降低模块图的首次加载速度，但如果模块内容不变，同一模块图的后续加载速度可能会显著提高。</p>\n<h1>其它功能</h1>\n<h2>默认启用 --experimental-detect-module</h2>\n<p>在此之前，除非显式指定（&quot;type&quot;: &quot;module&quot; 或 .mjs），否则 Node 将脚本视为 CommonJS。<br>现在默认启用对未知脚本，自动检测是否是 CJS 或 ESM。</p>\n<blockquote>\n<p>注：由于 Node 的策略是先将脚本视为 CJS，因此对于 CJS 应当无性能影响，对 ESM 则会稍微降低性能。</p>\n</blockquote>\n<h2>package.json export 字段新增 module-sync 条件</h2>\n<p>既然实现了 --experimental-require-module。那么这个条件就可以让支持 <em>CJS 导入同步 ESM</em> 的运行时直接导入同一个脚本。</p>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--no-experimental-websocket\">默认启用全局对象 WebSocket</a></h2>\n<p>改进了与 Web API 的兼容性</p>\n<blockquote>\n<p>不过这是提供一个客户端，没法 cover 服务端场景</p>\n</blockquote>\n<h2><a href=\"https://nodejs.org/docs/v22.11.0/api/cli.html#--no-experimental-global-navigator\">默认启用全局对象 navigator</a></h2>\n<p>实验性支持 HTML 标准定义的 Navigator 接口。</p>\n<blockquote>\n<p>看来 Node 比 <a href=\"https://docs.deno.com/api/web/\">Deno</a> 还先实现了这个 Web API<br>不过对于服务端 js，还没有任何从 os 模块切换过来的理由</p>\n</blockquote>\n","tags":["node.js"]},{"id":"vue-mount","url":"https://yieldray.fun/posts/vue-mount","title":"vue mount 琐谈","date_published":"2024-11-26T09:09:09.000Z","date_modified":"2024-11-26T09:09:09.000Z","content_text":"<p>本文讨论仅 vue 3.5 使用 setup 时，mount 之前的生命周期。<br>注意 beforeCreate 和 create 不属于组合式 API 的生命周期钩子，因此下文不会探讨。</p>\n<p>官方图例如下：</p>\n<p><img src=\"https://wsrv.nl/?url=https://raw.githubusercontent.com/vuejs-translations/docs-zh-cn/refs/heads/main/src/guide/essentials/images/lifecycle_zh-CN.png\" alt=\"Lifecycle Hooks\"></p>\n<p>本文仅考虑 mount。从 mountComponent 函数开始（源码：<a href=\"https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts\"><code>packages/runtime-core/src/renderer.ts</code></a>）</p>\n<pre><code class=\"language-mermaid\">graph TB\n    %% 主要组件挂载流程\n    M[&quot;mountComponent&quot;] --&gt; SC[&quot;setupComponent&quot;]\n    SC --&gt; SSC[&quot;setupStatefulComponent&quot;]\n    SSC --&gt; CS[&quot;component.setup&quot;]\n    CS --&gt; SRE[&quot;setupRenderEffect&quot;]\n    SRE --&gt; CUF[&quot;componentUpdateFn&quot;]\n\n    %% 首次挂载流程\n    CUF --&gt; NM{{&quot;是否已挂载?&quot;}}\n    NM --&gt;|未挂载| BM[&quot;beforeMount&quot;]\n    BM --&gt; VNBM[&quot;onVnodeBeforeMount&quot;]\n    VNBM --&gt; HBM[&quot;hook:beforeMount&quot;]\n    HBM --&gt; RCR[&quot;renderComponentRoot&quot;]\n    RCR --&gt; M1[&quot;mounted&quot;]\n    M1 --&gt; VNM[&quot;onVnodeMounted&quot;]\n    VNM --&gt; HM[&quot;hook:mounted&quot;]\n    HM --&gt; IM[&quot;instance.isMounted = true&quot;]\n\n    %% 更新流程\n    NM --&gt;|已挂载| BU[&quot;beforeUpdate&quot;]\n    BU --&gt; VNBU[&quot;onVnodeBeforeUpdate&quot;]\n    VNBU --&gt; HBU[&quot;hook:beforeUpdate&quot;]\n    HBU --&gt; RCR2[&quot;renderComponentRoot&quot;]\n    RCR2 --&gt; U[&quot;updated&quot;]\n    U --&gt; VNU[&quot;onVnodeUpdated&quot;]\n    VNU --&gt; HU[&quot;hook:updated&quot;]\n\n    %% 标注异步队列的钩子\n    subgraph &quot;queuePostRenderEffect&quot;\n        M1\n        VNM\n        HM\n        U\n        VNU\n        HU\n    end\n\n    %% 样式定义\n    classDef default fill:#E6F3FF,stroke:#65C8FF,stroke-width:2px\n    classDef async fill:#F2E6FF,stroke:#A585FF,stroke-width:2px\n    classDef condition fill:#E6FFFA,stroke:#65E8FF,stroke-width:2px\n\n    %% 应用样式\n    class M1,VNM,HM,U,VNU,HU async\n    class NM condition\n</code></pre>\n<p>我们知道，组件实际 DOM 节点在 renderComponentRoot 中创建。<br>因此 setup 和 onBeforeMount 时，DOM 节点并未创建。<br>而当 onMounted 时，DOM 节点已经添加到文档。</p>\n<p>简单验证：</p>\n<pre><code class=\"language-vue\">&lt;script setup&gt;\nimport { getCurrentInstance, onBeforeMount, onMounted } from &quot;vue&quot;;\nconst inDocument = document.contains.bind(document);\nconst instance = getCurrentInstance();\nconst log = (x) =&gt;\n    console.log(performance.now(), x, instance.isMounted, instance.vnode.el, inDocument(instance.vnode.el));\n\nlog(&quot;setup&quot;); // false null false\nonBeforeMount(() =&gt; log(&quot;onBeforeMount&quot;)); // false null false\nonMounted(() =&gt; log(&quot;onMounted&quot;)); // true &lt;h1&gt;​Hi​&lt;/h1&gt;​ true\n&lt;/script&gt;\n\n&lt;template&gt;\n    &lt;h1 ref=&quot;h1&quot;&gt;Hi&lt;/h1&gt;\n&lt;/template&gt;\n</code></pre>\n<pre><code class=\"language-vue\">&lt;script setup&gt;\nimport Comp from &quot;./Comp.vue&quot;;\nconst log = (x) =&gt; console.log(performance.now(), x);\n&lt;/script&gt;\n\n&lt;template&gt;\n    &lt;Comp\n        @vue:before-mount=&quot;log(&#39;vnode-before-mount&#39;)&quot;\n        @vue:before-unmount=&quot;log(&#39;vnode-before-unmount&#39;)&quot;\n        @vue:before-update=&quot;log(&#39;vnode-before-update&#39;)&quot;\n        @vue:mounted=&quot;log(&#39;vnode-mounted&#39;)&quot;\n        @vue:unmounted=&quot;log(&#39;vnode-unmounted&#39;)&quot;\n        @vue:updated=&quot;log(&#39;vnode-updated&#39;)&quot;\n    /&gt;\n&lt;/template&gt;\n</code></pre>\n<p>可以发现，setup 完成（组件已经完成了其响应式状态的设置）后几乎立即同步调用 onBeforeMount。<br>setup/onBeforeMount 与 onMount 的区别显而易见：只有 onMount 后，DOM 才被创建并挂载。</p>\n<p>那么使用 setup 和 onBeforeMount 有什么区别？<br>个人认为主要的区别是 SSR 期间，onBeforeMount 不会被调用。那么区分 setup 和 onBeforeMount 则可以区分 SSR 和 CSR。</p>\n<blockquote>\n<p>setup 执行时间一般很短。那么这里除了可以忽略不计的先后顺序问题，其它问题暂未想到，待补充。。。</p>\n</blockquote>\n<p>setup 和 onBeforeMount 差异很小，考虑 CSR 数据获取的场景，如何选择在 setup 还是 onMounted 中发起请求？<br>需要获取 DOM 时，答案不言而喻。但绝大部分发起网络请求的场景不涉及 DOM，或者说与 DOM 无关。<br>当然，onMounted 不会在 SSR 期间调用，SSR 数据获取可参考 <a href=\"https://nuxt.com/docs/getting-started/data-fetching\">Nuxt.js 方案</a>。下面只讨论 CSR。</p>\n<pre><code class=\"language-vue\">&lt;script setup&gt;\nimport { onMounted } from &quot;vue&quot;;\nimport { ref } from &quot;vue&quot;;\nconst data = ref(null);\nconst callApi = () =&gt;\n    fetch(&quot;https://dummyjson.com/test&quot;)\n        .then((res) =&gt; res.json())\n        .then((d) =&gt; (data.value = d));\n\ncallApi();\n// OR\nonMounted(callApi);\n&lt;/script&gt;\n\n&lt;template&gt;\n    &lt;h1&gt;{{ data ? data.status : &quot;loading...&quot; }}&lt;/h1&gt;\n&lt;/template&gt;\n</code></pre>\n<p><a href=\"https://router.vuejs.org/zh/guide/advanced/data-fetching.html\">https://router.vuejs.org/zh/guide/advanced/data-fetching.html</a></p>\n<p>不妨观察某些第三方库实现：<br><a href=\"https://vueuse.org/core/useFetch/\">vue-use</a> 的 <a href=\"https://github.com/vueuse/vueuse/blob/main/packages/core/useFetch/index.ts#L321\">useFetch 实现</a>。</p>\n<p><a href=\"https://github.com/TanStack/query/blob/main/packages/vue-query/src/useBaseQuery.ts#L57\">@tanstack/vue-query</a> 实现在这方面也是一样的。</p>\n<p>响应式数据需在 setup 中创建，因此它们实际发起异步请求都是在 setup 中，而不是 onMounted。</p>\n<p>比较疑惑的点在于，一些博客教程坚持将请求放在 onMounted 发起，给出的说法是在生命周期中更直观/错误处理更好。<br>当然，使用回调函数确实有优点，如可以在函数作用域中创建临时变量。<br>总的来说，在无需 DOM 的场景下，坚持使用 onMounted 除了考虑代码风格外，很难有说服力。</p>\n<p>由于 setup 发生在 onMounted 之前，且在这之间 vue 是同步创建 DOM 的，这要花一点时间。<br>因此使用 setup 能够更快的入队请求。虽然收益不会很高，但确实要快一些。</p>\n<p>以下例为证：</p>\n<pre><code class=\"language-vue\">&lt;script setup lang=&quot;ts&quot;&gt;\nimport { onMounted } from &quot;vue&quot;;\nimport { ref } from &quot;vue&quot;;\nconst data = ref(null);\nconst callApi = () =&gt;\n    fetch(&quot;https://worldtimeapi.org/api/timezone/Asia/Hong_Kong&quot;)\n        .then((res) =&gt; res.json())\n        .then(({ datetime }) =&gt; (data.value = datetime));\n\nconst props = defineProps&lt;{\n    on: &quot;setup&quot; | &quot;mounted&quot;;\n}&gt;();\n\nswitch (props.on) {\n    case &quot;setup&quot;:\n        callApi();\n        break;\n    case &quot;mounted&quot;:\n        onMounted(callApi);\n        break;\n}\n&lt;/script&gt;\n\n&lt;template&gt;\n    &lt;h1&gt;{{ data ?? &quot;loading...&quot; }}&lt;/h1&gt;\n&lt;/template&gt;\n</code></pre>\n<pre><code class=\"language-vue\">&lt;script setup&gt;\nimport Comp from &quot;./Comp.vue&quot;;\n&lt;/script&gt;\n\n&lt;template&gt;\n    &lt;Comp on=&quot;setup&quot; /&gt;\n    &lt;Comp on=&quot;mounted&quot; /&gt;\n    &lt;Comp on=&quot;mounted&quot; /&gt;\n    &lt;Comp on=&quot;setup&quot; /&gt;\n&lt;/template&gt;\n</code></pre>\n","tags":["vue"]},{"id":"js-cannot-polyfill-apply-call-bind","url":"https://yieldray.fun/posts/js-cannot-polyfill-apply-call-bind","title":"Function.prototype.{apply,call,bind} 是无法正确polyfill的","date_published":"2024-11-23T19:20:30.000Z","date_modified":"2024-11-23T19:20:30.000Z","content_text":"<p>我们以 <a href=\"https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-function.prototype.call\"><code>Function.prototype.call</code></a> 为例，因为它的步骤最简单。</p>\n<p>一个典型的 <code>Function.prototype.call</code> 的 polyfill 实现可以是如下：</p>\n<pre><code class=\"language-ts\">function functionPrototypeCall(func: (...args: any[]) =&gt; any, thisArg: any, ...args: any[]) {\n    // 计算出上下文对象\n    const thisValue = ordinaryCallGetThis(thisArg);\n    // 将函数设置为上下文对象的方法并调用\n    return withTempKeyOf(thisValue, (thisValue, key) =&gt; thisValue[key](...args), func);\n}\n\nfunction ordinaryCallGetThis(thisArg: any) {\n    if (thisArg === null || thisArg === undefined) {\n        return globalThis;\n    } else {\n        return thisArg;\n    }\n}\n\nfunction withTempKeyOf&lt;R&gt;(obj: any, cb: (thisValue: any, key: PropertyKey) =&gt; R, value?: any): R {\n    const UNIQUE_KEY = Symbol();\n    const thisValue = Object(obj);\n    Object.defineProperty(thisValue, UNIQUE_KEY, { enumerable: false, value, configurable: true });\n    const result = cb(thisValue, UNIQUE_KEY);\n    delete thisValue[UNIQUE_KEY];\n    return result;\n}\n</code></pre>\n<p>问题在于， <code>ordinaryCallGetThis</code> 和 <code>withTempKeyOf</code> 函数的实现都不完美。</p>\n<h1><code>withTempKeyOf</code></h1>\n<p><code>withTempKeyOf</code> 函数的目的是正确调用 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-call\"><code>Call(func, thisValue, argList)</code></a>。<br>当执行 <code>withTempKeyOf(thisValue, (thisValue, key) =&gt; thisValue[key](...args), func)</code> 时，Call 抽象操作的所有参数都被正确传入了。</p>\n<p>通过设置临时 Symbol 键，并使之不可枚举，已经将该临时键的其可见性降低到最小。<br>不过不可枚举当然不意味着无法列出，显然，<code>Reflect.ownKeys</code> 就会受影响。</p>\n<h1><code>ordinaryCallGetThis</code></h1>\n<p>该 polyfill 的最大 miss 就是 <code>ordinaryCallGetThis</code> 的实现，即使它看起来很简单。</p>\n<p>可以看出，当 thisArg 为 null 或 undefined 时，它返回 globalThis；否则原样返回。</p>\n<p>这一步部分模拟了 <a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinarycallbindthis\">OrdinaryCallBindThis(F, calleeContext, thisArgument)</a> 抽象操作。<br>当函数 F 是非严格模式下才是此行为；对于严格函数，不会为 thisArg 为 null 或 undefined 做特殊处理。</p>\n<p>但是，关键在于我们<strong>无法知道传入的函数 func 是否是严格模式</strong>！</p>\n<pre><code class=\"language-js\">// 此脚本必须在全局环境是非严格模式下运行\n\nfunction strict() {\n    &quot;use strict&quot;;\n    console.log(this === globalThis);\n}\n\nfunction normal() {\n    console.log(this === globalThis);\n}\n\nstrict(); // =&gt; false\nstrict.call(undefined); // =&gt; false\nfunctionPrototypeCall(strict, undefined); // =&gt; true\n\nnormal(); // =&gt; true\nnormal.call(undefined); // =&gt; true\nfunctionPrototypeCall(normal, undefined); // =&gt; true\n</code></pre>\n<p>另外，假设已知函数 func 必然是严格函数，<code>ordinaryCallGetThis</code> 的调用能否省略呢？</p>\n<p>显然也是不行的，因为 polyfill 是通过<strong>将函数设置为上下文对象的方法并调用</strong>来处理 this 的，<br>但是 <code>Function.prototype.call</code> 也能够正确处理严格模式下 this 不是对象的情况。</p>\n<pre><code class=\"language-ts\">function strict2() {\n    &quot;use strict&quot;;\n    console.log(this);\n}\n\nfunction functionPrototypeCall2(func: (...args: any[]) =&gt; any, thisArg: any, ...args: any[]) {\n    return withTempKeyOf(thisArg, (key) =&gt; thisArg[key](...args), func);\n}\n\nstrict2.call(null);\nfunctionPrototypeCall2(strict2, null); // 报错\n</code></pre>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-light-bulb mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>\n<p>小知识：<br><a href=\"https://tc39.es/ecma262/multipage/reflection.html#sec-reflect.apply\"><code>Reflect.apply</code></a> 和 <a href=\"https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-function.prototype.call\"><code>Function.prototype.call</code></a> 不一样。<br>前者要求传入的参数列表必须是类数组对象，而后者允许使用 null 和 undefined 替代空数组。<br>v8 的报错信息为 <code>CreateListFromArrayLike called on non-object</code>，可以看出，v8 内部函数命名与规范一致</p>\n</div>\n<h1>后话</h1>\n<p>实际上，<a href=\"https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/function-apply.js\">core-js</a> 也没有提供 Function.prototype.call 函数的实现，毕竟它确实无法 polyfill。</p>\n","tags":["js"]},{"id":"js-structured-clone","url":"https://yieldray.fun/posts/js-structured-clone","title":"js深拷贝的另类实现","date_published":"2024-11-21T07:57:38.000Z","date_modified":"2024-11-21T07:57:38.000Z","content_text":"<h1>结构化克隆</h1>\n<p><code>Window</code> 接口上的 <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone\"><code>structuredClone()</code></a> 方法根据<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm\">结构化克隆算法</a>，为指定值创建<a href=\"https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy\">深拷贝</a>。</p>\n<p>这个函数可以说是深拷贝的模版，<a href=\"https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/web.structured-clone.js\">core-js</a> 对之有一个遵循规范的 polyfill 实现。</p>\n<p>根据 HTML 标准，<a href=\"https://html.spec.whatwg.org/multipage/structured-data.html#structured-cloning\"><code>structuredClone(value, options)</code></a> 的操作步骤很简单：</p>\n<ol>\n<li>Let <code>serialized</code> be ? <code>StructuredSerializeWithTransfer(value, options[&quot;transfer&quot;])</code>.</li>\n<li>Let <code>deserializeRecord</code> be ? <code>StructuredDeserializeWithTransfer(serialized, this&#39;s relevant realm)</code>.</li>\n<li>Return <code>deserializeRecord.[[Deserialized]]</code>.</li>\n</ol>\n<p>本文不打算探讨 Transfer 对象，那么简单来说，忽略 options 参数，structuredClone 所做操作无非：先序列化 value，再将结果反序列化，最后返回。</p>\n<blockquote>\n<p>注1：通过 <a href=\"https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal\">StructuredSerializeInternal(value, forStorage [,memory])</a> 抽象操作<br>注2：下文都不考虑 SharedArrayBuffer</p>\n</blockquote>\n<p>在浏览器环境中，注意到 <a href=\"https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps\">history.replaceState</a> 函数的第一个参数实际上会被序列化。\n而 <a href=\"https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-history-state\"><code>history.state</code></a> 则返回反序列化的结果。</p>\n<p>根据 HTML 标准，其序列化使用 <a href=\"https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeforstorage\">StructuredSerializeForStorage(value)</a> 抽象操作。</p>\n<p>观察其步骤可以发现，在忽略 Transfer 对象的情况下，<code>StructuredSerializeForStorage</code> 等价于 <code>StructuredSerializeWithTransfer</code>。</p>\n<p>这就给我们一个实现 polyfill 的思路，即通过 StructuredSerializeForStorage 来把浏览器实现的结构化克隆算法暴露出来。</p>\n<pre><code class=\"language-ts\">function structuredClone(x: unknown) {\n    const UNUSED = &quot;&quot;;\n    const saved = history.state;\n    history.replaceState(x, UNUSED); // 序列化\n    const cloned = history.state; // 反序列化\n    history.replaceState(saved, UNUSED);\n    return cloned;\n}\n</code></pre>\n<h1>此外</h1>\n<p>在 ECMA262 中，实现深拷贝实际上需要做一些取舍，即区分哪些对象无法拷贝 和 对象的那些部分无法拷贝。</p>\n<p>结构化克隆算法所做取舍如下：</p>\n<ul>\n<li>无法克隆 Function 对象（规范角度来说叫可调用对象，因为函数可以是闭包）</li>\n<li>无法克隆 Symbol 值</li>\n<li>无法克隆 DOM 对象（是无法序列化的 <a href=\"https://webidl.spec.whatwg.org/#dfn-platform-object\">platform</a>对象）</li>\n<li>只克隆对象的可枚举键（Object.keys），且不克隆属性描述符</li>\n<li>不克隆对象的原型</li>\n<li>不克隆类私有属性</li>\n<li>特殊处理的对象为：\n原始值对应对象 Boolean/Number/BigInt/String\n内置对象 Date/RegExp/SharedArrayBuffer/GrowableSharedArrayBuffer/ArrayBuffer/ResizableArrayBuffer/ArrayBufferView/\nMap/Set/Array/Error</li>\n<li>无法克隆包含除 [[Prototype]] [[Extensible]] [[PrivateElements]] 外内部槽的对象（Promise/WeakMap）</li>\n<li>无法克隆原型不属于当前 realm 的 exotic 对象（Proxy）</li>\n</ul>\n","tags":[]},{"id":"js-async-await-downgrade","url":"https://yieldray.fun/posts/js-async-await-downgrade","title":"async/await 语法降级","date_published":"2024-11-03T14:09:37.000Z","date_modified":"2024-11-03T14:09:37.000Z","content_text":"<p>ES2015/ES2016 已支持 Generator，但不支持 async/await。<br>Typescript 编译器使用如下辅助函数降级 async/await 操作符。</p>\n<pre><code class=\"language-js\">var __awaiter =\n    (this &amp;&amp; this.__awaiter) || // 检查 this.__awaiter 是否已存在，若存在则直接使用，避免重复定义\n    function (thisArg, _arguments, P, generator) {\n        // thisArg:  绑定 Generator 函数的 this 上下文\n        // _arguments:  Generator 函数的参数 (通常不使用)\n        // P:  Promise 构造函数 (默认为全局 Promise)\n        // generator:  Generator 函数\n\n        function adopt(value) {\n            // 此函数完全等价于 Promise.resolve 静态方法\n            return value instanceof P\n                ? value\n                : new P(function (resolve) {\n                      resolve(value);\n                  });\n        }\n\n        // 返回一个新的 Promise，用于包装 Generator 函数的执行\n        return new (P || (P = Promise))(function (resolve, reject) {\n            // resolve:  用于解决外部 Promise\n            // reject:  用于拒绝外部 Promise\n\n            function fulfilled(value) {\n                // 处理 Promise 的 resolve 结果\n                try {\n                    step(generator.next(value)); // 将 Promise 的 resolve 值传递给 Generator 的 next 方法\n                } catch (e) {\n                    reject(e); // 如果 Generator 执行过程中出现错误，则拒绝外部 Promise\n                }\n            }\n\n            function rejected(value) {\n                // 处理 Promise 的 reject 结果\n                try {\n                    step(generator[&quot;throw&quot;](value)); // 将 Promise 的 reject 值传递给 Generator 的 throw 方法\n                } catch (e) {\n                    reject(e); // 如果 Generator 执行过程中出现错误，则拒绝外部 Promise\n                }\n            }\n\n            function step(result) {\n                // 驱动 Generator 函数的执行\n                result.done // 检查 Generator 是否执行完毕\n                    ? resolve(result.value) // 如果执行完毕，则用 Generator 的返回值解决外部 Promise\n                    : adopt(result.value).then(fulfilled, rejected); // 如果未执行完毕，则等待 yield 后面的 Promise 完成，然后继续执行 Generator\n            }\n\n            // 开始执行 Generator 函数\n            step((generator = generator.apply(thisArg, _arguments || [])).next());\n        });\n    };\n</code></pre>\n<p>降级过程如下：</p>\n<pre><code class=\"language-js\">async function foo() {\n    const p1 = new Promise((resolve) =&gt; setTimeout(() =&gt; resolve(&quot;1&quot;), 1000));\n    const p2 = new Promise((_, reject) =&gt; setTimeout(() =&gt; reject(&quot;2&quot;), 500));\n    const results = [await p1, await p2]; // 不要这么写！请使用 Promise.all 或 Promise.allSettled\n}\n</code></pre>\n<p><code>async function foo() {/*BODY*/}</code> 会被转换为 <code>function foo() { return __awaiter(this, void 0, void 0, function* () {/*BODY*/}) }</code></p>\n<p>可以发现，<code>await</code> 都转为了 <code>yield</code></p>\n<pre><code class=\"language-js\">function foo() {\n    return __awaiter(this, void 0, void 0, function* () {\n        const p1 = new Promise((resolve) =&gt; setTimeout(() =&gt; resolve(&quot;1&quot;), 1000));\n        const p2 = new Promise((_, reject) =&gt; setTimeout(() =&gt; reject(&quot;2&quot;), 500));\n        const results = [yield p1, yield p2]; // 不要这么写！请使用 Promise.all 或 Promise.allSettled\n    });\n}\n</code></pre>\n<p>对于 ES5，由于不支持 Generator，编译器还将使用 <code>__generator</code> 辅助函数转换 Generator</p>\n<p>Generator 的本质是一个状态机，其 Polyfill 就比较复杂了</p>\n<p>由于它不属于本文的核心部分，说明略</p>\n<pre><code class=\"language-js\">// prettier-ignore\nvar __generator = (this &amp;&amp; this.__generator) || function (thisArg, body) {\n    // thisArg: Generator 函数的 this 上下文\n    // body: Generator 函数的主体逻辑\n\n    var _ = { \n        label: 0, // 当前 label，用于跳转\n        sent: function() { \n            // 用于 yield* 表达式，获取 yield* 后 Generator 的返回值\n            if (t[0] &amp; 1) throw t[1]; // 如果 Generator 抛出异常，则重新抛出\n            return t[1]; // 返回 Generator 的返回值\n        },\n        trys: [], // try 块的栈\n        ops: []  // 操作的栈，用于循环、switch 和 finally 等\n    },\n    f, // 标记 Generator 是否正在执行\n    y, // yield 后表达式的结果，或者 yield* 的 Generator 对象\n    t, // 临时变量\n    g = Object.create((typeof Iterator === &quot;function&quot; ? Iterator : Object).prototype); // 创建一个迭代器对象\n\n\n    // 定义 next、throw 和 return 方法\n    return g.next = verb(0), \n           g[&quot;throw&quot;] = verb(1), \n           g[&quot;return&quot;] = verb(2), \n           typeof Symbol === &quot;function&quot; &amp;&amp; (g[Symbol.iterator] = function() { return this; }), // 实现 Symbol.iterator 接口\n           g; // 返回生成的迭代器对象\n\n\n    function verb(n) { return function (v) { return step([n, v]); }; }\n    // verb 函数用于创建 next、throw 和 return 方法。\n    // n 表示操作类型：0 为 next，1 为 throw，2 为 return。\n    // v 表示传入的值。\n\n    function step(op) {\n        // 驱动 Generator 执行的核心函数\n        if (f) throw new TypeError(&quot;Generator is already executing.&quot;); // 如果 Generator 正在执行，则抛出错误\n        while (g &amp;&amp; (g = 0, op[0] &amp;&amp; (_ = 0)), _) try { // g 为迭代器对象，_ 为状态对象\n            if (f = 1, y &amp;&amp; (t = op[0] &amp; 2 ? y[&quot;return&quot;] : op[0] ? y[&quot;throw&quot;] || ((t = y[&quot;return&quot;]) &amp;&amp; t.call(y), 0) : y.next) &amp;&amp; !(t = t.call(y, op[1])).done) return t;\n            // 处理 yield* 表达式，如果 yield* 后面的 Generator 还没有执行完毕，则返回 yield* 后 Generator 的返回值\n\n            if (y = 0, t) op = [op[0] &amp; 2, t.value]; // 将 yield 后表达式的结果赋值给 op[1]\n            switch (op[0]) {\n                case 0: case 1: t = op; break; // next 和 throw 操作\n                case 4: _.label++; return { value: op[1], done: false }; // yield 表达式\n                case 5: _.label++; y = op[1]; op = [0]; continue; // yield* 表达式\n                case 7: op = _.ops.pop(); _.trys.pop(); continue; // try 块结束\n                default:\n                    // 处理 try...catch...finally 块\n                    if (!(t = _.trys, t = t.length &gt; 0 &amp;&amp; t[t.length - 1]) &amp;&amp; (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n                    // 如果不是 try 块，并且是 catch 或 finally 块，则跳出循环\n                    if (op[0] === 3 &amp;&amp; (!t || (op[1] &gt; t[0] &amp;&amp; op[1] &lt; t[3]))) { _.label = op[1]; break; }\n                    // try 块中的跳转\n                    if (op[0] === 6 &amp;&amp; _.label &lt; t[1]) { _.label = t[1]; t = op; break; }\n                    // catch 块中的跳转\n                    if (t &amp;&amp; _.label &lt; t[2]) { _.label = t[2]; _.ops.push(op); break; }\n                    // finally 块中的跳转\n                    if (t[2]) _.ops.pop();\n                    _.trys.pop(); continue; // 恢复 try 块之前的状态\n            }\n            op = body.call(thisArg, _); // 执行 Generator 函数的主体逻辑\n        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } // 处理异常和 finally 块\n        if (op[0] &amp; 5) throw op[1]; // 重新抛出异常\n        return { value: op[0] ? op[1] : void 0, done: true }; // 返回 Generator 的最终结果\n    }\n};\n</code></pre>\n","tags":["js"]},{"id":"chromium-how-blink-works","url":"https://yieldray.fun/posts/chromium-how-blink-works","title":"Blink 工作原理","date_published":"2024-10-27T00:00:00.000Z","date_modified":"2024-10-27T00:00:00.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"http://bit.ly/how-blink-works\">http://bit.ly/how-blink-works</a></p>\n</blockquote>\n<h1>Blink 工作原理</h1>\n<p>Blink 的开发工作并非易事。对于 Blink 新手开发者而言，Blink 引入了许多特有的概念和编码规范，旨在实现一个极速的渲染引擎，这需要一定的学习成本。即使是经验丰富的 Blink 开发者，由于 Blink 体量庞大，并且对性能、内存和安全极其敏感，开发工作也充满挑战。</p>\n<p>本文旨在<strong>从宏观角度概述 Blink 的工作原理</strong>，希望能帮助 Blink 开发者快速熟悉其架构：</p>\n<ul>\n<li>本文<strong>并非</strong> Blink 详细架构和编码规则的完整教程（这些规则可能会发生变化，并很快过时）。相反，本文将简明扼要地描述 Blink 的基本原理，这些原理在短期内不太可能发生变化，并指出你可以阅读的资源，以便更深入地学习。</li>\n<li>本文<strong>不会</strong>解释具体的功能（e.g., ServiceWorkers, editing）。而是解释一些代码库中广泛使用的基础功能（e.g., 内存管理、V8 API）。</li>\n</ul>\n<p>有关 Blink 开发的更多常规信息，请参阅 <a href=\"https://www.chromium.org/blink\">Chromium wiki 页面</a>。</p>\n<h1>Blink 的功能</h1>\n<p><a href=\"https://www.chromium.org/blink\">Blink</a> 是 Web 平台的渲染引擎。概括地说，Blink 实现了浏览器选项卡内所有内容的渲染：</p>\n<ul>\n<li>实现 Web 平台规范（例如，<a href=\"https://html.spec.whatwg.org/multipage/\">HTML 标准</a>），包括 DOM、CSS 和 Web IDL</li>\n<li>嵌入 V8 并运行 JavaScript</li>\n<li>从底层网络栈请求资源</li>\n<li>构建 DOM 树</li>\n<li>计算样式和布局</li>\n<li>嵌入 <a href=\"https://chromium.googlesource.com/chromium/src/+/HEAD/cc/README.md\">Chrome 合成器</a> 并绘制图形</li>\n</ul>\n<p>许多客户端（例如 Chromium、Android WebView 和 Opera）都通过 <a href=\"https://chromium.googlesource.com/chromium/src/+/HEAD/content/public/README.md\">content public APIs</a> 嵌入了 Blink。</p>\n<p><img src=\"https://s2.loli.net/2024/11/26/A18lyuSNcE6QxZz.png\" alt=\"image1.png\"></p>\n<p>从代码库的角度来看，“Blink”通常指 <code>//third_party/blink/</code>。从项目的角度来看，“Blink”通常指实现 Web 平台功能的项目。实现 Web 平台功能的代码涵盖了 <code>//third_party/blink/</code>、<code>//content/renderer/</code>、<code>//content/browser/</code> 和其他位置。</p>\n<h1>进程/线程架构</h1>\n<h2>进程</h2>\n<p>Chromium 采用 <a href=\"https://www.chromium.org/developers/design-documents/multi-process-architecture\">多进程架构</a>。Chromium 有一个浏览器进程和 N 个沙盒化的渲染器进程。Blink 运行在渲染器进程中。</p>\n<p>渲染器进程是如何创建的？出于安全考虑，在跨站点文档之间隔离内存地址区域至关重要（这称为 <a href=\"https://www.chromium.org/Home/chromium-security/site-isolation\">站点隔离</a>）。理论上，每个渲染器进程最多只能专用于一个站点。然而，实际上，当用户打开过多的选项卡或设备没有足够的内存时，将每个渲染器进程限制在一个站点有时会占用过多的资源。这时，一个渲染器进程可能会被来自不同站点的多个 iframe 或选项卡共享。这意味着一个选项卡中的 iframe 可能由不同的渲染器进程托管，而不同选项卡中的 iframe 可能由同一个渲染器进程托管。<strong>渲染器进程、iframe 和选项卡之间不存在 1:1 的映射关系</strong>。</p>\n<p>由于渲染器进程在沙盒中运行，Blink 需要请求浏览器进程来分派系统调用（例如，文件访问、播放音频）和访问用户配置文件数据（例如，Cookie、密码）。这种浏览器与渲染器进程之间的通信是通过 <a href=\"https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md\">Mojo</a> 实现的。（注意：过去我们使用的是 <a href=\"https://www.chromium.org/developers/design-documents/inter-process-communication\">Chromium IPC</a>，并且许多地方仍在使用它。但是，它已被弃用，并在底层使用了 Mojo）在 Chromium 方面，<a href=\"https://www.chromium.org/servicification\">服务化</a> 正在进行中，并将浏览器进程抽象为一组“服务”。从 Blink 的角度来看，Blink 可以直接使用 Mojo 与服务和浏览器进程进行交互。</p>\n<p><img src=\"https://s2.loli.net/2024/11/26/mXvHOcLkg5G8UKt.png\" alt=\"image2.png\"></p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li><a href=\"https://www.chromium.org/developers/design-documents/multi-process-architecture\">多进程架构</a></li>\n<li>Blink 中的 Mojo 编程：platform/mojo/MojoProgrammingInBlink.md</li>\n</ul>\n<h2>线程</h2>\n<p>一个渲染器进程中会创建多少个线程？</p>\n<p>Blink 有一个主线程、N 个工作线程和几个内部线程。</p>\n<p>几乎所有重要的事情都发生在主线程上。所有 JavaScript（除了 worker）、DOM、CSS、样式和布局计算都在主线程上运行。Blink 经过高度优化，可以在假设大多是单线程架构的情况下最大限度地提高主线程的性能。</p>\n<p>Blink 可以创建多个工作线程来运行 <a href=\"https://html.spec.whatwg.org/multipage/workers.html#workers\">Web Workers</a>、<a href=\"https://w3c.github.io/ServiceWorker/\">ServiceWorker</a> 和 <a href=\"https://html.spec.whatwg.org/multipage/worklets.html\">Worklets</a>。</p>\n<p>Blink 和 V8 可能会创建几个内部线程来处理 webaudio、数据库、GC 等。</p>\n<p>对于跨线程通信，你必须使用 PostTask API 进行消息传递。不鼓励使用共享内存编程，除非某些地方确实需要出于性能原因使用它。这就是为什么你在 Blink 代码库中看不到很多 MutexLocks 的原因。</p>\n<p><img src=\"https://s2.loli.net/2024/11/26/Y8G1EO4wnVgUFxb.png\" alt=\"image3.png\"></p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>Blink 中的线程编程：platform/wtf/ThreadProgrammingInBlink.md</li>\n<li>Workers：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/workers/README.md\">core/workers/README.md</a></li>\n</ul>\n<h2>Blink 的初始化和终止</h2>\n<p>Blink 通过 <a href=\"https://cs.chromium.org/chromium/src/third_party/blink/renderer/controller/blink_initializer.cc?sq=package:chromium&dr=C&g=0&l=86\"><code>BlinkInitializer::Initialize()</code></a> 初始化。在执行任何 Blink 代码之前，必须调用此方法。</p>\n<p>另一方面，Blink 永远不会被终止；也就是说，渲染器进程会被强制退出而不会被清理。其中一个原因是性能。另一个原因是，通常很难以优雅有序的方式清理渲染器进程中的所有内容（而且这样做不值得）。</p>\n<h1>目录结构</h1>\n<h2>Content public APIs 和 Blink public APIs</h2>\n<p><a href=\"https://cs.chromium.org/chromium/src/content/public/\">Content public APIs</a> 是使嵌入器能够嵌入渲染引擎的 API 层。Content public APIs 必须仔细维护，因为它们暴露给嵌入器。</p>\n<p><a href=\"https://cs.chromium.org/chromium/src/third_party/blink/public/?q=blink/public&sq=package:chromium&dr\">Blink public APIs</a> 是将 <code>//third_party/blink/</code> 中的功能暴露给 Chromium 的 API 层。该 API 层只是从 WebKit 继承的历史遗留物。在 WebKit 时代，Chromium 和 Safari 共享 WebKit 的实现，因此需要 API 层将 WebKit 的功能暴露给 Chromium 和 Safari。现在 Chromium 是 <code>//third_party/blink/</code> 的唯一嵌入器，API 层已经没有意义了。我们正在积极减少 Blink public APIs 的数量，方法是将 Web 平台代码从 Chromium 移至 Blink（该项目称为 Onion Soup）。</p>\n<p><img src=\"https://s2.loli.net/2024/11/26/LgtTDluOs7rpo3A.png\" alt=\"image4.png\"></p>\n<h2>目录结构和依赖关系</h2>\n<p><code>//third_party/blink/</code> 包含以下目录。有关这些目录的更详细定义，请参阅<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/README.md\">此文档</a>：</p>\n<ul>\n<li><code>platform/</code><ul>\n<li>Blink 底层功能的集合，这些功能是从单体 <code>core/</code> 中分离出来的。例如，几何和图形实用程序。</li>\n</ul>\n</li>\n<li><code>core/</code> 和 <code>modules/</code><ul>\n<li>规范中定义的所有 Web 平台功能的实现。<code>core/</code> 实现与 DOM 紧密耦合的功能。<code>modules/</code> 实现更独立的功能。例如 webaudio、indexeddb。</li>\n</ul>\n</li>\n<li><code>bindings/core/</code> 和 <code>bindings/modules/</code><ul>\n<li>从概念上讲，<code>bindings/core/</code> 是 <code>core/</code> 的一部分，<code>bindings/modules/</code> 是 <code>modules/</code> 的一部分。大量使用 V8 API 的文件放在 <code>bindings/{core,modules}</code> 中。</li>\n</ul>\n</li>\n<li><code>controller/</code><ul>\n<li>一组使用 <code>core/</code> 和 <code>modules/</code> 的高级库。例如，devtools 前端。</li>\n</ul>\n</li>\n</ul>\n<p>依赖关系按以下顺序流动：</p>\n<ul>\n<li>Chromium =&gt; controller/ =&gt; <code>modules/</code> 和 <code>bindings/modules/</code> =&gt; <code>core/</code> 和 <code>bindings/core/</code> =&gt; <code>platform/</code> =&gt; 低级原语，例如 <code>//base</code>、<code>//v8</code> 和 <code>//cc</code></li>\n</ul>\n<p>Blink 仔细维护暴露给 <code>//third_party/blink/</code> 的低级原语列表。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>目录结构和依赖关系：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/README.md\">blink/renderer/README.md</a></li>\n</ul>\n<h2>WTF</h2>\n<p>WTF 是一个“Blink 特定的基础”库，位于 <code>platform/wtf/</code>。我们正试图尽可能地统一 Chromium 和 Blink 之间的编码原语，因此 WTF 应该很小。需要这个库是因为有许多类型、容器和宏确实需要针对 Blink 的工作负载和 Oilpan（Blink GC）进行优化。如果类型是在 WTF 中定义的，则 Blink 必须使用 WTF 类型而不是在 <code>//base</code> 或 std 库中定义的类型。最常用的类型是向量、哈希集、哈希映射和字符串。Blink 应使用 <code>WTF::Vector</code>、<code>WTF::HashSet</code>、<code>WTF::HashMap</code>、<code>WTF::String</code> 和 <code>WTF::AtomicString</code>，而不是 <code>std::vector</code>、<code>std::*set</code>、<code>std::*map</code> 和 <code>std::string</code>。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>如何使用 WTF：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/wtf/README.md\">platform/wtf/README.md</a></li>\n</ul>\n<h1>内存管理</h1>\n<p>就 Blink 而言，你需要注意三种内存分配器：</p>\n<ul>\n<li><a href=\"https://chromium.googlesource.com/chromium/src/+/master/base/allocator/partition_allocator/PartitionAlloc.md\">PartitionAlloc</a></li>\n<li><a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/heap/BlinkGCAPIReference.md\">Oil Pan</a> （又名 Blink GC）</li>\n<li>malloc/free 或 new/delete（已禁用）</li>\n</ul>\n<p>你可以使用 <code>USING_FAST_MALLOC()</code> 在 PartitionAlloc 的堆上分配对象：</p>\n<pre><code class=\"language-c++\">class SomeObject {\n  USING_FAST_MALLOC(SomeObject);\n  static std::unique_ptr&lt;SomeObject&gt; Create() {\n    return std::make_unique&lt;SomeObject&gt;();  // 在 PartitionAlloc 的堆上分配\n  }\n};\n</code></pre>\n<p>由 PartitionAlloc 分配的对象的生命周期应由 <code>scoped_refptr&lt;&gt;</code> 或 <code>std::unique_ptr&lt;&gt;</code> 管理。强烈建议不要手动管理生命周期。Blink 中禁止手动删除。</p>\n<p>你可以使用 <code>GarbageCollected</code> 在 Oilpan 的堆上分配对象：</p>\n<pre><code class=\"language-c++\">class SomeObject : public GarbageCollected&lt;SomeObject&gt; {\n  static SomeObject* Create() {\n    return new SomeObject;  // 在 Oilpan 的堆上分配\n  }\n};\n</code></pre>\n<p>由 Oilpan 分配的对象的生命周期由垃圾收集器自动管理。你必须使用特殊指针（例如，<code>Member&lt;&gt;</code>、<code>Persistent&lt;&gt;</code>）来持有 Oilpan 堆上的对象。请参阅<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/heap/BlinkGCAPIReference.md\">此 API 参考</a> 以熟悉有关 Oilpan 的编程限制。最重要的限制是，你不允许在 Oilpan 对象的析构函数中触及任何其他 Oilpan 对象（因为不能保证销毁顺序）。</p>\n<p>如果你既不使用 <code>USING_FAST_MALLOC()</code> 也不使用 <code>GarbageCollected</code>，则对象将在系统 malloc 的堆上分配。强烈建议不要在 Blink 中这样做。所有 Blink 对象都应由 PartitionAlloc 或 Oilpan 分配，如下所示：</p>\n<ul>\n<li>默认使用 Oilpan。</li>\n<li>仅当 1）对象的生命周期非常清晰并且 <code>std::unique_ptr&lt;&gt;</code> 或 <code>scoped_refptr&lt;&gt;</code> 足够时，2）在 Oilpan 上分配对象会引入很多复杂性时，或者 3）在 Oilpan 上分配对象会给垃圾收集运行时带来很多不必要的压力时，才使用 PartitionAlloc。</li>\n</ul>\n<p>无论你使用 PartitionAlloc 还是 Oilpan，都必须非常小心，不要创建悬空指针（注意：强烈建议不要使用原始指针）或内存泄漏。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>如何使用 PartitionAlloc：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/wtf/allocator/Allocator.md\">platform/wtf/allocator/Allocator.md</a></li>\n<li>如何使用 Oilpan：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/heap/BlinkGCAPIReference.md\">platform/heap/BlinkGCAPIReference.md</a></li>\n<li>Oilpan GC 设计：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/heap/BlinkGCDesign.md\">platform/heap/BlinkGCDesign.md</a></li>\n</ul>\n<h1>任务调度</h1>\n<p>为了提高渲染引擎的响应速度，Blink 中的任务应尽可能异步执行。不鼓励使用同步 IPC/Mojo 和任何其他可能需要几毫秒的操作（尽管有些是不可避免的，例如用户的 JavaScript 执行）。</p>\n<p>渲染器进程中的所有任务都应使用正确的任务类型发布到 <a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/scheduler/README.md\">Blink 调度器</a>，如下所示：</p>\n<pre><code class=\"language-c++\">// 使用 kNetworking 的任务类型将任务发布到帧的调度器\nframe-&gt;GetTaskRunner(TaskType::kNetworking)-&gt;PostTask(..., WTF::Bind(&amp;Function));\n</code></pre>\n<p>Blink 调度器维护多个任务队列，并智能地确定任务的优先级，以最大限度地提高用户感知的性能。指定<a href=\"https://cs.chromium.org/chromium/src/third_party/blink/public/platform/task_type.h?q=blink+tasktype&sq=package:chromium&dr=CSs&l=5\">正确的任务类型</a> 非常重要，这样 Blink 调度器才能正确且智能地调度任务。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>如何发布任务：<a href=\"https://chromium.googlesource.com/chromium/src/+/HEAD/third_party/blink/renderer/platform/scheduler/TaskSchedulingInBlink.md\">third_party/blink/renderer/platform/scheduler/TaskSchedulingInBlink.md</a></li>\n</ul>\n<h1>页面、框架、文档、DOMWindow 等</h1>\n<h2>概念</h2>\n<p>页面（Page）、框架（Frame）、文档（Document）、ExecutionContext 和 DOMWindow 是以下概念：</p>\n<ul>\n<li>页面对应于选项卡的概念（如果没有启用下文解释的 OOPIF）。每个渲染器进程可能包含多个选项卡。</li>\n<li>框架对应于框架的概念（主框架或 iframe）。每个页面可能包含一个或多个按树状层次结构排列的框架。</li>\n<li>DOMWindow 对应于 JavaScript 中的 window 对象。每个框架都有一个 DOMWindow。</li>\n<li>文档对应于 JavaScript 中的 window.document 对象。每个框架都有一个文档。</li>\n<li>ExecutionContext 是一个抽象文档（对于主线程）和 WorkerGlobalScope（对于工作线程）的概念。</li>\n</ul>\n<p>渲染器进程：Page = 1：N。</p>\n<p>Page：Frame = 1：M。</p>\n<p>Frame：DOMWindow：Document（或 ExecutionContext）= 1：1：1 在任何时间点，但映射可能会随着时间而改变。例如，考虑以下代码：</p>\n<pre><code class=\"language-javascript\">iframe.contentWindow.location.href = &quot;https://example.com&quot;;\n</code></pre>\n<p>在这种情况下，将为 <a href=\"https://example.com\">https://example.com</a> 创建一个新的 DOMWindow 和一个新的文档。但是，可以重复使用该框架。</p>\n<p>（注意：准确地说，在某些情况下会创建一个新文档，但会重复使用 DOMWindow 和框架。还有<a href=\"https://docs.google.com/presentation/d/1pHjF3TNCX--j0ss3SK09pXlVOFK0Cdq6HkMcOzcov1o/edit#slide=id.g4983c55b2d55fcc7_42\">更复杂的情况</a>）</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>core/frame/FrameLifecycle.md</li>\n</ul>\n<h2>进程外 iframe (OOPIF)</h2>\n<p><a href=\"https://www.chromium.org/developers/design-documents/site-isolation\">站点隔离</a> 使事情更安全，但也更复杂。:) 站点隔离的思想是每个站点创建一个渲染器进程。（<em>站点</em> 是页面的可注册域 + 1 个标签及其 URL 方案。例如，<a href=\"https://mail.example.com\">https://mail.example.com</a> 和 <a href=\"https://chat.example.com\">https://chat.example.com</a> 位于同一站点，但 <a href=\"https://noodles.com\">https://noodles.com</a> 和 <a href=\"https://pumpkins.com\">https://pumpkins.com</a> 不位于同一站点）如果一个页面包含一个跨站点 iframe，则该页面可能由两个渲染器进程托管。考虑以下页面：</p>\n<pre><code class=\"language-html\">&lt;!-- https://example.com --&gt;\n&lt;body&gt;\n    &lt;iframe src=&quot;https://example2.com&quot;&gt;&lt;/iframe&gt;\n&lt;/body&gt;\n</code></pre>\n<p>主框架和 <code>&lt;iframe&gt;</code> 可能由不同的渲染器进程托管。渲染器进程本地的框架由 LocalFrame 表示，而非渲染器进程本地的框架由 RemoteFrame 表示。</p>\n<p>从主框架的角度来看，主框架是一个 LocalFrame，而 <code>&lt;iframe&gt;</code> 是一个 RemoteFrame。从 <code>&lt;iframe&gt;</code> 的角度来看，主框架是一个 RemoteFrame，而 <code>&lt;iframe&gt;</code> 是一个 LocalFrame。</p>\n<p>LocalFrame 和 RemoteFrame（可能存在于不同的渲染器进程中）之间的通信通过浏览器进程处理。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>设计文档：<a href=\"https://www.chromium.org/developers/design-documents/site-isolation\">站点隔离设计文档</a></li>\n<li>如何使用站点隔离编写代码：core/frame/SiteIsolation.md</li>\n</ul>\n<h2>分离的框架/文档</h2>\n<p>框架/文档可能处于分离状态。考虑以下情况：</p>\n<pre><code class=\"language-javascript\">doc = iframe.contentDocument;\niframe.remove(); // iframe 从 DOM 树中分离\ndoc.createElement(&quot;div&quot;); // 但是你仍然可以在分离的框架上运行脚本\n</code></pre>\n<p>棘手的事实是，你仍然可以在分离的框架上运行脚本或 DOM 操作。由于框架已被分离，大多数 DOM 操作将失败并引发错误。不幸的是，分离框架的行为在浏览器之间并没有真正的互操作性，在规范中也没有明确定义。基本上，期望是 JavaScript 应该继续运行，但大多数 DOM 操作应该失败并抛出一些适当的异常，如下所示：</p>\n<pre><code class=\"language-c++\">void someDOMOperation(...) {\n  if (!script_state_-&gt;ContextIsValid()) { // 框架已分离\n    …;  // 设置异常等\n    return;\n  }\n}\n</code></pre>\n<p>这意味着在通常情况下，当框架分离时，Blink 需要执行许多清理操作。你可以通过继承 <a href=\"https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/dom/context_lifecycle_observer.h?type=cs&q=contextlifecycleobserver&sq=package:chromium&g=0&l=95\">ContextLifecycleObserver</a> 来做到这一点，如下所示：</p>\n<pre><code class=\"language-c++\">class SomeObject : public GarbageCollected&lt;SomeObject&gt;, public ContextLifecycleObserver {\n  void ContextDestroyed() override {\n    // 在这里执行清理操作\n  }\n  ~SomeObject() {\n    // 在这里执行清理操作不是一个好主意，因为这已经太晚了。此外，析构函数不允许触及 Oilpan 堆上的任何其他对象\n  }\n};\n</code></pre>\n<h1>Web IDL 绑定</h1>\n<p>当 JavaScript 访问 <code>node.firstChild</code> 时，将调用 <code>node.h</code> 中的 <code>Node::firstChild()</code>。它是如何工作的？让我们来看看 <code>node.firstChild</code> 是如何工作的。</p>\n<p>首先，你需要根据规范定义一个 IDL 文件：</p>\n<pre><code class=\"language-webidl\">interface Node : EventTarget {\n  [...] readonly attribute Node? firstChild;\n};\n</code></pre>\n<p>Web IDL 的语法在 <a href=\"https://heycam.github.io/webidl/\">Web IDL 规范</a> 中定义。<code>[...]</code> 称为 IDL 扩展属性。一些 IDL 扩展属性在 Web IDL 规范中定义，其他是 <a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/IDLExtendedAttributes.md\">Blink 特定的 IDL 扩展属性</a>。除了 Blink 特定的 IDL 扩展属性外，IDL 文件应以符合规范的方式编写（即，只需从规范中复制和粘贴）。</p>\n<p>其次，你需要为 Node 定义一个 C++ 类并为 <code>firstChild</code> 实现一个 C++ getter：</p>\n<pre><code class=\"language-c++\">class EventTarget : public ScriptWrappable {  // 所有暴露给 JavaScript 的类都必须继承自 ScriptWrappable\n  ...;\n};\n\nclass Node : public EventTarget {\n  DEFINE_WRAPPERTYPEINFO();  // 所有具有 IDL 文件的类都必须具有此宏\n  Node* firstChild() const { return first_child_; }\n};\n</code></pre>\n<p>通常情况下，就是这样。当你构建 <code>node.idl</code> 时，<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/IDLCompiler.md\">IDL 编译器</a> 会自动为 Node 接口和 <code>Node.firstChild</code> 生成 Blink-V8 绑定。自动生成的绑定在 <a href=\"https://cs.chromium.org/chromium/src/out/Debug/gen/third_party/blink/renderer/bindings/core/v8/v8_node.h?q=v8node&sq=package:chromium&dr=CSs&l=11\">//src/out/{Debug,Release}/gen/third_party/blink/renderer/bindings/core/v8/v8_node.h</a> 中生成。当 JavaScript 调用 <code>node.firstChild</code> 时，V8 调用 <code>v8_node.h</code> 中的 <code>V8Node::firstChildAttributeGetterCallback()</code>，然后调用你在上面定义的 <code>Node::firstChild()</code>。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>如何添加 Web IDL 绑定：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/IDLCompiler.md\">bindings/IDLCompiler.md</a></li>\n<li>如何使用 IDL 扩展属性：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/IDLExtendedAttributes.md\">bindings/IDLExtendedAttributes.md</a></li>\n<li>规范：<a href=\"https://heycam.github.io/webidl/\">Web IDL 规范</a></li>\n</ul>\n<h1>V8 和 Blink</h1>\n<h2>Isolate、Context、World</h2>\n<p>当你编写涉及 V8 API 的代码时，了解 Isolate、Context 和 World 的概念非常重要。它们在代码库中分别由 <code>v8::Isolate</code>、<code>v8::Context</code> 和 <code>DOMWrapperWorld</code> 表示。</p>\n<p>Isolate 对应于一个物理线程。Blink 中的 Isolate：物理线程 = 1：1。主线程有自己的 Isolate。工作线程有自己的 Isolate。</p>\n<p>Context 对应于一个全局对象（在 Frame 的情况下，它是 Frame 的 window 对象）。由于每个框架都有自己的 window 对象，因此一个渲染器进程中有多个 Context。当你调用 V8 API 时，必须确保你位于正确的上下文中。否则，<code>v8::Isolate::GetCurrentContext()</code> 将返回错误的上下文，在最坏的情况下，最终会导致对象泄漏并导致安全问题。</p>\n<p>World 是一个支持 Chrome 扩展程序的内容脚本的概念。World 不对应于 Web 标准中的任何内容。内容脚本希望与网页共享 DOM，但出于安全原因，内容脚本的 JavaScript 对象必须与网页的 JavaScript 堆隔离。（此外，一个内容脚本的 JavaScript 堆必须与另一个内容脚本的 JavaScript 堆隔离）为了实现隔离，主线程为网页创建一个主 World，并为每个内容脚本创建一个隔离的 World。主 World 和隔离的 World 可以访问相同的 C++ DOM 对象，但它们的 JavaScript 对象是隔离的。这种隔离是通过为一个 C++ DOM 对象创建多个 V8 包装器来实现的；即，每个 World 一个 V8 包装器。</p>\n<p><img src=\"https://s2.loli.net/2024/11/26/bsE4YnaDWzPvgek.png\" alt=\"image5.png\"></p>\n<p>Context、World 和 Frame 之间的关系是什么？</p>\n<p>假设主线程上有 N 个 World（一个主 World + (N - 1) 个隔离的 World）。那么一个 Frame 应该有 N 个 window 对象，每个 window 对象用于一个 World。Context 是对应于 window 对象的概念。这意味着当我们有 M 个 Frame 和 N 个 World 时，我们有 M * N 个 Context（但 Context 是延迟创建的）。</p>\n<p>在 worker 的情况下，只有一个 World 和一个全局对象。因此只有一个 Context。</p>\n<p>再次强调，当你使用 V8 API 时，你应该非常小心使用正确的上下文。否则，最终会导致 JavaScript 对象在隔离的 World 之间泄漏，并导致安全灾难（例如，来自 A.com 的扩展程序可以操纵来自 B.com 的扩展程序）。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li><a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md\">bindings/core/v8/V8BindingDesign.md</a></li>\n</ul>\n<h2>V8 API</h2>\n<p>在 <a href=\"https://cs.chromium.org/chromium/src/v8/include/v8.h?q=v8.h&sq=package:chromium&dr=CSs&l=10\">//v8/include/v8.h</a> 中定义了许多 V8 API。由于 V8 API 是低级的并且难以正确使用，因此 <a href=\"https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/bindings/?q=platform/bindings&sq=package:chromium&dr\">platform/bindings/</a> 提供了许多包装 V8 API 的辅助类。你应该尽可能多地使用这些辅助类。如果你的代码必须大量使用 V8 API，则文件应放在 <code>bindings/{core,modules}</code> 中。</p>\n<p>V8 使用句柄指向 V8 对象。最常见的句柄是 <code>v8::Local&lt;&gt;</code>，它用于从机器堆栈指向 V8 对象。<code>v8::Local&lt;&gt;</code> 必须在机器堆栈上分配 <code>v8::HandleScope</code> 后使用。<code>v8::Local&lt;&gt;</code> 不应在机器堆栈外使用：</p>\n<pre><code class=\"language-c++\">void function() {\n  v8::HandleScope scope;\n  v8::Local&lt;v8::Object&gt; object = ...;  // 这是正确的\n}\n\nclass SomeObject : public GarbageCollected&lt;SomeObject&gt; {\n  v8::Local&lt;v8::Object&gt; object_;  // 这是错误的\n};\n</code></pre>\n<p>如果要从机器堆栈外指向 V8 对象，则需要使用 <a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/bindings/TraceWrapperReference.md\">包装器跟踪</a>。但是，你必须非常小心，不要用它创建引用循环。通常，V8 API 很难使用。如果你不确定自己在做什么，请咨询 <a href=\"https://groups.google.com/a/chromium.org/forum/#!forum/blink-reviews-bindings\">blink-review-bindings@</a>。</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>如何使用 V8 API 和辅助类：platform/bindings/HowToUseV8FromBlink.md</li>\n</ul>\n<h2>V8 包装器</h2>\n<p>每个 C++ DOM 对象（例如，Node）都有其对应的 V8 包装器。准确地说，每个 C++ DOM 对象每个 World 都有其对应的 V8 包装器。</p>\n<p>V8 包装器对其对应的 C++ DOM 对象具有强引用。但是，C++ DOM 对象对其 V8 包装器只有弱引用。因此，如果你想将 V8 包装器保持活动状态一段时间，则必须明确地执行此操作。否则，V8 包装器将被过早收集，并且 V8 包装器上的 JS 属性将丢失...</p>\n<pre><code class=\"language-javascript\">div = document.getElementById(&quot;div&quot;);\nchild = div.firstChild;\nchild.foo = &quot;bar&quot;;\nchild = null;\ngc(); // 如果我们什么都不做，|firstChild| 的 V8 包装器将被 GC 收集\nassert(div.firstChild.foo === &quot;bar&quot;); //...这将失败\n</code></pre>\n<p>如果我们什么都不做，<code>child</code> 将被 GC 收集，因此 <code>child.foo</code> 将丢失。为了使 <code>div.firstChild</code> 的 V8 包装器保持活动状态，我们必须添加一种机制，“只要 <code>div</code> 所属的 DOM 树可以从 V8 访问，就使 <code>div.firstChild</code> 的 V8 包装器保持活动状态”。</p>\n<p>有两种方法可以使 V8 包装器保持活动状态：<a href=\"https://cs.chromium.org/chromium/src/third_party/blink/renderer/bindings/core/v8/active_script_wrappable.h?q=activescriptwrappable&sq=package:chromium&dr=CSs&l=16\">ActiveScriptWrappable</a> 和 <a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/bindings/TraceWrapperReference.md\">包装器跟踪</a>.</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>如何管理 V8 包装器的生命周期：bindings/core/v8/V8Wrapper.md</li>\n<li>如何使用包装器跟踪：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/platform/bindings/TraceWrapperReference.md\">platform/bindings/TraceWrapperReference.md</a></li>\n</ul>\n<h1>渲染流水线</h1>\n<p>从 HTML 文件交付给 Blink 到像素显示在屏幕上，这是一个漫长的旅程。渲染流水线的架构如下。</p>\n<p><img src=\"https://s2.loli.net/2024/11/26/HTajLiDvwlrZFBk.png\" alt=\"image6.png\"></p>\n<p>阅读<a href=\"https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit#slide=id.p\">这篇精彩的幻灯片</a> 以了解渲染流水线的每个阶段都做了什么。（我认为我无法写出比这更好的解释了 :-)）</p>\n<p>如果你想了解更多信息：</p>\n<ul>\n<li>概述：<a href=\"https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit#slide=id.p\">像素的生命周期</a></li>\n<li>DOM：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/dom/README.md\">core/dom/README.md</a></li>\n<li>样式：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/css/README.md\">core/css/README.md</a></li>\n<li>布局：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/layout/README.md\">core/layout/README.md</a></li>\n<li>绘制：<a href=\"https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/paint/README.md\">core/paint/README.md</a></li>\n<li>合成器线程：<a href=\"https://www.chromium.org/developers/design-documents/chromium-graphics\">Chromium 图形</a></li>\n</ul>\n<h1>有问题吗？</h1>\n<p>你可以将任何问题提交至 <a href=\"https://groups.google.com/a/chromium.org/forum/#!forum/blink-dev\">blink-dev@chromium.org</a>（一般问题）或 <a href=\"https://groups.google.com/a/chromium.org/forum/#!forum/platform-architecture-dev\">platform-architecture-dev@chromium.org</a>（与架构相关的问题）。我们随时乐意提供帮助！:D</p>\n","tags":["chromium"]},{"id":"chromium-multi-process-architecture","url":"https://yieldray.fun/posts/chromium-multi-process-architecture","title":"Chromium 的多进程架构","date_published":"2024-10-26T00:00:00.000Z","date_modified":"2024-10-26T00:00:00.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://www.chromium.org/developers/design-documents/multi-process-architecture/\">https://www.chromium.org/developers/design-documents/multi-process-architecture/</a></p>\n</blockquote>\n<h2>Chromium 的多进程架构</h2>\n<p>本文档描述了 Chromium 的高层架构以及它如何在多种进程类型之间划分。</p>\n<h2>问题</h2>\n<p>构建一个永不崩溃或永不卡死的渲染引擎几乎是不可能的。构建一个绝对安全的渲染引擎也几乎是不可能的。</p>\n<p>在某些方面，2006 年左右的网络浏览器状态类似于过去的单用户协作式多任务操作系统。正如在这样的操作系统中，一个行为不当的应用程序可能会导致整个系统崩溃，一个行为不当的网页也可能会导致网络浏览器崩溃。只需要一个渲染引擎或插件错误就可以导致整个浏览器及其所有当前运行的标签页崩溃。</p>\n<p>现代操作系统更加健壮，因为它们将应用程序放入彼此隔离的独立进程中。一个应用程序的崩溃通常不会损害其他应用程序或操作系统的完整性，并且每个用户对其他用户数据的访问受到限制。Chromium 的架构旨在实现这种更健壮的设计。</p>\n<h2>架构概述</h2>\n<p>Chromium 使用多个进程来保护整个应用程序免受渲染引擎或其他组件中的错误和故障的影响。它还限制了每个渲染引擎进程对其他进程和系统其余部分的访问。在某些方面，这为网络浏览带来了内存保护和访问控制为操作系统带来的好处。</p>\n<p>我们将运行 UI 并管理渲染器和其他进程的主进程称为“<strong>浏览器进程</strong>”或“浏览器”。同样，处理网页内容的进程称为“<strong>渲染器进程</strong>”或“渲染器”。渲染器使用 <a href=\"https://www.chromium.org/blink\">Blink</a> 开源布局引擎来解释和布局 HTML。</p>\n<p><a href=\"https://www.chromium.org/developers/design-documents/multi-process-architecture/arch.png\"><img src=\"https://i1.wp.com/www.chromium.org/developers/design-documents/multi-process-architecture/arch.png\" alt=\"\"></a></p>\n<h3>管理渲染器进程</h3>\n<p>每个渲染器进程都有一个全局 <code>RenderProcess</code> 对象，用于管理与父浏览器进程的通信并维护全局状态。浏览器为每个渲染器进程维护一个对应的 <code>RenderProcessHost</code>，用于管理渲染器的浏览器状态和通信。浏览器和渲染器使用 <a href=\"https://chromium.googlesource.com/chromium/src/+/HEAD/mojo/README.md\">Mojo</a> 或 <a href=\"https://www.chromium.org/developers/design-documents/inter-process-communication\">Chromium 的传统 IPC 系统</a> 进行通信。</p>\n<h3>管理框架和文档</h3>\n<p>每个渲染器进程都有一个或多个 <code>RenderFrame</code> 对象，它们对应于包含内容的文档的框架。浏览器进程中相应的 <code>RenderFrameHost</code> 管理与该文档关联的状态。每个 <code>RenderFrame</code> 都被赋予一个路由 ID，用于区分同一渲染器中的多个文档或框架。这些 ID 在一个渲染器内部是唯一的，但在浏览器中不是唯一的，因此识别一个框架需要 <code>RenderProcessHost</code> 和路由 ID。浏览器与渲染器中特定文档之间的通信是通过这些 <code>RenderFrameHost</code> 对象完成的，这些对象知道如何通过 Mojo 或传统 IPC 发送消息。</p>\n<h2>组件和接口</h2>\n<p>在渲染器进程中：</p>\n<ul>\n<li><code>RenderProcess</code> 处理与浏览器中相应 <code>RenderProcessHost</code> 的 Mojo 设置和传统 IPC。每个渲染器进程只有一个 <code>RenderProcess</code> 对象。</li>\n<li><code>RenderFrame</code> 对象与其在浏览器进程中相应的 <code>RenderFrameHost</code>（通过 Mojo）和 Blink 层进行通信。此对象表示标签页或子框架中一个网页文档的内容。</li>\n</ul>\n<p>在浏览器进程中：</p>\n<ul>\n<li><code>Browser</code> 对象表示一个顶级浏览器窗口。</li>\n<li><code>RenderProcessHost</code> 对象表示单个浏览器 ↔ 渲染器 IPC 连接的浏览器端。浏览器进程中每个渲染器进程都有一个 <code>RenderProcessHost</code>。</li>\n<li><code>RenderFrameHost</code> 对象封装了与 <code>RenderFrame</code> 的通信，而 <code>RenderWidgetHost</code> 处理浏览器中 <code>RenderWidget</code> 的输入和绘制。</li>\n</ul>\n<p>有关此嵌入工作原理的更多详细信息，请参阅<a href=\"https://www.chromium.org/developers/design-documents/displaying-a-web-page-in-chrome\">Chromium 如何显示网页</a>设计文档。</p>\n<h2>共享渲染器进程</h2>\n<p>通常，每个新窗口或标签页都会在一个新进程中打开。浏览器将生成一个新进程并指示它创建一个 <code>RenderFrame</code>，该 <code>RenderFrame</code> 可能会在页面中创建更多 iframe（可能在不同的进程中）。</p>\n<p>有时，在标签页或窗口之间共享渲染器进程是必要或可取的。例如，Web 应用程序可以使用 <code>window.open</code> 创建另一个窗口，如果新文档属于同一来源，则它必须共享同一进程。如果进程总数过大，Chromium 也有策略将新标签页分配给现有进程。这些考虑因素和策略在<a href=\"https://chromium.googlesource.com/chromium/src/+/main/docs/process_model_and_site_isolation.md\">进程模型</a> 中进行了描述。</p>\n<h2>检测崩溃或行为不当的渲染器</h2>\n<p>与浏览器进程的每个 Mojo 或 IPC 连接都会监视进程句柄。如果这些句柄发出信号，则渲染器进程已崩溃，受影响的标签页和框架将收到崩溃通知。Chromium 会显示一个“悲伤标签页”或“悲伤框架”图像，通知用户渲染器已崩溃。可以通过按下重新加载按钮或启动新的导航来重新加载页面。发生这种情况时，Chromium 会注意到没有渲染器进程，并创建一个新的渲染器进程。</p>\n<h2>沙盒化渲染器</h2>\n<p>鉴于渲染器在单独的进程中运行，我们有机会通过 <a href=\"https://chromium.googlesource.com/chromium/src/+/HEAD/docs/design/sandbox.md\">沙盒化</a> 限制其对系统资源的访问。例如，我们可以确保渲染器只能通过 Chromium 的网络服务访问网络。同样，我们可以使用主机操作系统的内置权限限制其对文件系统的访问，或限制其对用户显示和输入的访问。这些限制极大地限制了受损渲染器进程所能完成的操作。</p>\n<h2>释放内存</h2>\n<p>由于渲染器在单独的进程中运行，因此将隐藏的标签页视为低优先级变得很简单。通常，Windows 上最小化的进程会将其内存自动放入“可用内存”池中。在内存不足的情况下，Windows 会将此内存交换到磁盘，然后再交换出更高优先级的内存，从而帮助保持用户可见程序的响应速度。我们可以将相同的原则应用于隐藏的标签页。当渲染器进程没有顶级标签页时，我们可以释放该进程的“工作集”大小，作为提示系统在必要时首先将该内存交换到磁盘的提示。因为我们发现减少工作集大小也会降低用户在两个标签页之间切换时的标签页切换性能，所以我们逐渐释放此内存。这意味着，如果用户切换回最近使用的标签页，则该标签页的内存比最近未使用过的标签页更有可能被分页。拥有足够内存来运行所有程序的用户根本不会注意到此过程：Windows 只会在需要时实际回收此类数据，因此在内存充足时不会出现性能损失。</p>\n<p>这有助于我们在内存不足的情况下获得更佳的内存占用。与很少使用的后台标签页关联的内存可以完全交换出去，而前台标签页的数据可以完全加载到内存中。相比之下，单进程浏览器会将其所有标签页的数据随机分布在其内存中，并且无法如此清晰地分离已使用和未使用的的数据，从而浪费内存和性能。</p>\n<h2>其他进程类型</h2>\n<p>Chromium 还将许多其他组件拆分到单独的进程中，有时以特定于平台的方式进行。例如，它现在具有单独的 GPU 进程、网络服务和存储服务。沙盒化的实用程序进程也可用于小型或高风险任务，作为满足安全<a href=\"https://chromium.googlesource.com/chromium/src/+/master/docs/security/rule-of-2.md\">双重规则</a> 的一种方法。</p>\n","tags":["chromium"]},{"id":"http-protocols","url":"https://yieldray.fun/posts/http-protocols","title":"HTTP 规范解读","date_published":"2024-10-23T21:43:23.000Z","date_modified":"2024-11-02T21:37:00.000Z","content_text":"<p>省略 HTTP/0.9，因为它没有被 RFC 标准化。</p>\n<h1>HTTP/1.0</h1>\n<blockquote>\n<p><a href=\"https://www.rfc-editor.org/rfc/rfc1945\">RFC 1945</a></p>\n</blockquote>\n<p>HTTP/1.0 是一个基于 TCP 的简单请求-响应式协议。</p>\n<p>RFC 规定每个连接都由客户端在请求之前建立，并在发送响应后由服务器关闭。</p>\n<p>注意 RFC 全文中并<strong>没有表述 <code>Connection: keep-alive</code> 标头，也没有规定长连接机制</strong>。（因此网络上的说法是无迹可寻的）<br>不过根据 <a href=\"https://www.rfc-editor.org/rfc/rfc9112.html#name-keep-alive-connections\">RFC9112 C.2.2</a>，这确实属于实验性功能。</p>\n<p>RFC 正文中仅<a href=\"https://www.rfc-editor.org/rfc/rfc1945#section-8\">定义了 HEAD/GET/POST 方法</a>。\n注意 HTTP/1.0 已支持正文编码（即 <a href=\"https://www.rfc-editor.org/rfc/rfc1945#section-10.3\">Content-Encoding</a>，用于对内容编码以压缩数据），但不支持响应分块。</p>\n<p>另外注意 RFC 中已经对代理/网关等角色做了表述，不过在 HTTP/1.1 增加了更多标头来做精细化控制。<br>此外，RFC 附录中包含一些方法和标头的扩展。</p>\n<h1>HTTP/1.1</h1>\n<blockquote>\n<p>1997 RFC2068<br>1999 RFC2616<br>2014 RFC7230-7235<br>2022 RFC9110-9112<br>下文基于现行 RFC9110-9112<br>读者可参考：<a href=\"https://yieldray.github.io/rfc-http/\">https://yieldray.github.io/rfc-http/</a></p>\n</blockquote>\n<p>HTTP/1.1 强制要求客户端发送 Host 标头。</p>\n<p>HTTP/1.1 通过 <a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Transfer-Encoding\">Transfer-Encoding</a> 支持响应分块。<br>与 Content-Encoding 不同，Transfer-Encoding 指定的是 HTTP 消息本身如何在网络上逐跳（单次连接）传输（而代理不应重传或缓存）。<br>特殊点在于通过 <code>Transfer-Encoding: chunked</code> 能够支持分块传输。</p>\n<p>相对于 HTTP/1 主要依赖于 Expires 头部字段，HTTP/1.1 引入了更精细的缓存控制机制，<br>包括 Cache-Control 头部字段，支持更多缓存指令，如 max-age、no-cache、private 等，可更灵活地控制缓存行为。</p>\n<p>默认情况下 HTTP/1.1 使用<a href=\"https://www.rfc-editor.org/rfc/rfc9112.html#section-9.3\">持久化连接</a>，无需 keep-alive，但允许指定 <code>Connection: close</code> 来关闭。<br>注意 RFC 没有表述 HTTP 是如何通过底层的传输协议<a href=\"https://www.rfc-editor.org/rfc/rfc9112.html#section-9.1\">连接</a>的。</p>\n<p>由于发生在一个连接上， HTTP/1.1 是串行的（即请求流水线）。规范要求请求和响应必须相关联，因此对于非法响应，客户端必须断开连接。</p>\n<h1>HTTP/2</h1>\n<blockquote>\n<p>2015 RFC7540<br>2020 RFC8740<br>2022 RFC9113</p>\n</blockquote>\n<p>HTTP/2 引入<em>头部压缩</em>（<a href=\"https://www.rfc-editor.org/rfc/rfc9113.html#section-4.3\">HPACK</a>）和<a href=\"https://www.rfc-editor.org/rfc/rfc9113.html#name-streams-and-multiplexing\"><em>多路复用</em></a>（多流并发），可更有效地利用网络资源（减少 TCP 连接数）并减少延迟。<br>多路复用即将每个 HTTP 请求/响应交换与流关联起来，多流并发自然又带来了<a href=\"https://www.rfc-editor.org/rfc/rfc9113.html#name-flow-control\"><em>流量控制</em></a>（<a href=\"https://www.rfc-editor.org/rfc/rfc9113.html#WINDOW_UPDATE\">WINDOW_UPDATE</a>）和<a href=\"https://www.rfc-editor.org/rfc/rfc9113.html#name-prioritization\"><em>请求优先级</em></a>的问题。</p>\n<p>如何建立 HTTP/2 连接呢？<br><a href=\"https://www.rfc-editor.org/rfc/rfc9113.html#section-3.2\">对于 HTTPS</a>，客户端发送的 TLS 请求带有应用层协议协商（<a href=\"https://developer.mozilla.org/en-US/docs/Glossary/ALPN\">ALPN</a>）扩展（使用 h2 为标识符）。<br>对于 HTTP，对 HTTP/2 的服务发现实际上比较困难，规范弃用了 <code>Upgrade: h2c</code> 标头的做法。<br>另外注意，所有主流浏览器都只实现了 HTTP/2 over HTTPS。</p>\n<h2>帧</h2>\n<p>HTTP/2 帧是 HTTP/2 协议的基本单位，所有在客户端和服务器之间传输的数据都被封装在帧中。帧提供了在单个 TCP 连接上多路复用多个请求和响应的功能。</p>\n<p>一个 HTTP/2 帧由一个 9 字节的帧头和可变长度的有效载荷组成。</p>\n<pre><code class=\"language-js\">DATA Frame {\n  Length (24),\n  Type (8) = 0x00,\n\n  Unused Flags (4),\n  PADDED Flag (1),\n  Unused Flags (2),\n  END_STREAM Flag (1),\n\n  Reserved (1),\n  Stream Identifier (31),\n\n  [Pad Length (8)],\n  Data (..),\n  Padding (..2040),\n}\n</code></pre>\n<pre><code class=\"language-js\">HEADERS Frame {\n  Length (24),\n  Type (8) = 0x01,\n\n  Unused Flags (2),\n  PRIORITY Flag (1),\n  Unused Flag (1),\n  PADDED Flag (1),\n  END_HEADERS Flag (1),\n  Unused Flag (1),\n  END_STREAM Flag (1),\n\n  Reserved (1),\n  Stream Identifier (31),\n\n  [Pad Length (8)],\n  [Exclusive (1)],\n  [Stream Dependency (31)],\n  [Weight (8)],\n  Field Block Fragment (..),\n  Padding (..2040),\n}\n</code></pre>\n<pre><code class=\"language-js\">PRIORITY Frame {\n  Length (24) = 0x05,\n  Type (8) = 0x02,\n\n  Unused Flags (8),\n\n  Reserved (1),\n  Stream Identifier (31),\n\n  Exclusive (1),\n  Stream Dependency (31),\n  Weight (8),\n}\n</code></pre>\n<pre><code class=\"language-js\">RST_STREAM Frame {\n  Length (24) = 0x04,\n  Type (8) = 0x03,\n\n  Unused Flags (8),\n\n  Reserved (1),\n  Stream Identifier (31),\n\n  Error Code (32),\n}\n</code></pre>\n<pre><code class=\"language-js\">SETTINGS Frame {\n  Length (24),\n  Type (8) = 0x04,\n\n  Unused Flags (7),\n  ACK Flag (1),\n\n  Reserved (1),\n  Stream Identifier (31) = 0,\n\n  Setting (48) ...,\n}\n\nSetting {\n  Identifier (16),\n  Value (32),\n}\n</code></pre>\n<pre><code class=\"language-js\">PUSH_PROMISE Frame {\n  Length (24),\n  Type (8) = 0x05,\n\n  Unused Flags (4),\n  PADDED Flag (1),\n  END_HEADERS Flag (1),\n  Unused Flags (2),\n\n  Reserved (1),\n  Stream Identifier (31),\n\n  [Pad Length (8)],\n  Reserved (1),\n  Promised Stream ID (31),\n  Field Block Fragment (..),\n  Padding (..2040),\n}\n</code></pre>\n<pre><code class=\"language-js\">PING Frame {\n  Length (24) = 0x08,\n  Type (8) = 0x06,\n\n  Unused Flags (7),\n  ACK Flag (1),\n\n  Reserved (1),\n  Stream Identifier (31) = 0,\n\n  Opaque Data (64),\n}\n</code></pre>\n<pre><code class=\"language-js\">GOAWAY Frame {\n  Length (24),\n  Type (8) = 0x07,\n\n  Unused Flags (8),\n\n  Reserved (1),\n  Stream Identifier (31) = 0,\n\n  Reserved (1),\n  Last-Stream-ID (31),\n  Error Code (32),\n  Additional Debug Data (..),\n}\n</code></pre>\n<pre><code class=\"language-js\">WINDOW_UPDATE Frame {\n  Length (24) = 0x04,\n  Type (8) = 0x08,\n\n  Unused Flags (8),\n\n  Reserved (1),\n  Stream Identifier (31),\n\n  Reserved (1),\n  Window Size Increment (31),\n}\n</code></pre>\n<pre><code class=\"language-js\">CONTINUATION Frame {\n  Length (24),\n  Type (8) = 0x09,\n\n  Unused Flags (5),\n  END_HEADERS Flag (1),\n  Unused Flags (2),\n\n  Reserved (1),\n  Stream Identifier (31),\n\n  Field Block Fragment (..),\n}\n</code></pre>\n<h1>HTTP/3</h1>\n<blockquote>\n<p>2022 <a href=\"https://autumnquiche.github.io/RFC9114_Chinese_Simplified/\">RFC9114</a><br>参考：<a href=\"https://www.infoq.cn/article/Lddlsa5f21StY04LI3hP\">https://www.infoq.cn/article/Lddlsa5f21StY04LI3hP</a></p>\n</blockquote>\n<p>HTTP/3 通过 QUIC 协议承载。QUIC 基于 UDP，这样就可以完成在用户态对协议栈的实现（相对于 TCP 集成在了内核态实现）。</p>\n<p>QUIC 提供协议协商、基于流的复用和流量控制。</p>\n<hr>\n<p>对于传统的 HTTPS，实际上是 TLS over TCP，因此需要做 TCP 和 TLS 两次握手。</p>\n<p><img src=\"https://www.plantuml.com/plantuml/svg/XP9DIZD148RtVOeYg--3ACI52s4aSH24H0bEN0XkYaubQTAfQQiw_Xn1H3o3ft2LwuGSmypFGYKJTpNrF5KlnNHN8qfSBhpHkUE0WWjGkbyIM-g9v8-ZcRDp9bNeD2bfxdE1CuzslaCPeaHdNK2EiEGnYNTCC46ljCE-95MSKi-xAI-88mne4eq9tvQqh7TRqW9KIClo9rQI1v8IPsLfJ9B0A1t2-lrZ_V8AIMAgEJWu_TSv_bzhS0BPxPMfFsfKYHLgGo_jDuqMg8WPPD0vFFepgMvVaFTvGsGjLquRKfBOlkEgsQVdywVzkqjbEpP3dbBgmema2sGRmdvxnIsRjy_DQhNdY1ib0Id-dCQclTUa8MSb66D4qoKU5u_j2m00\" alt=\"\"></p>\n<p>QUIC 建立在 TLS&gt;=1.3 之上，并将这两个握手合并。</p>\n<p><img src=\"https://www.plantuml.com/plantuml/svg/ZO-nIiDG58Rt-nI7JZeqsCMX46cjGmI5edc1GtBQNd9o4kzDTBMJ8AWLr3TGdAohxsEYleN1P51inErml-_xuGviXYRBOrOsqfAYmHWi7UKa0GsDIKxycGuY8MkXrwQC4auCR3C6qGfbNEthGASe6UGIac4j11DaIoliVR8MfpHaFPMOgcIF9fbIwRUHpeXrHJg05khp9puaSqpc2zUdKhK6tQsrtkPwymfzs1MTQUJo_BwyE-itOnun9wfncg8clulQ-hCehgu_JkVlo_dhoqERuw64TeOHmKuYf9JhmaRtmFUBnVBjvh5ON8Bhjdx2y_rnUVjSNZor6OxZ_2DM0v8mZrbz0W00\" alt=\"\"></p>\n<p>另外，HTTP/2 虽解决了 HTTP 队头阻塞，但没有解决 TCP 队头阻塞。使用 UDP 也能够解决此问题。</p>\n<p>如何建立 HTTP/3 连接？<br>服务端通过 HTTP/2 ALTSVC 帧 或 响应头 <code>Alt-Svc: h3=&quot;:端口号&quot;</code>，可通告 HTTP/3 端点可用。</p>\n<hr>\n<p>HTTP/3 使用 QPACK（HPACK 的一种变体）实现头部压缩。<br>HTTP/3 为请求定义了以下伪标头字段：<code>:method</code> <code>:scheme</code> <code>:authority</code> <code>:path</code>\n为响应定义了 <code>:status</code> 伪标头。</p>\n<p>HTTP 帧由 QUIC 流承载。HTTP/3 帧与 HTTP/2 类似，但也<a href=\"https://www.rfc-editor.org/rfc/rfc9114.html#name-http-frame-types\">存在区别</a>。</p>\n<h1>See Also</h1>\n<p><a href=\"https://httpwg.org/specs/\">https://httpwg.org/specs/</a></p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Evolution_of_HTTP\">https://developer.mozilla.org/docs/Web/HTTP/Evolution_of_HTTP</a></p>\n<p><a href=\"https://www.ruanyifeng.com/blog/2016/08/http.html\">https://www.ruanyifeng.com/blog/2016/08/http.html</a></p>\n","tags":["http"]},{"id":"build-your-own-sqlite","url":"https://yieldray.fun/posts/build-your-own-sqlite","title":"Build your own SQLite","date_published":"2024-10-19T17:22:33.000Z","date_modified":"2024-10-19T17:22:33.000Z","content_text":"<h1>Bootstrapping</h1>\n<p>让我们从建立一个简单的是数据库文件开始（通过 sqlite3）</p>\n<pre><code class=\"language-sh\">sqlite3 minimal_test.db\nsqlite&gt; create table table1(id integer);\nsqlite&gt; create table table2(id integer);\nsqlite&gt; .exit\n</code></pre>\n<p>这将创建两个表，只含 id 列。</p>\n<pre><code class=\"language-sh\">sqlite3 minimal_test.db\nsqlite&gt; .tables\ntable1  table2\nsqlite&gt; .exit\n</code></pre>\n<p>我们创建一个 rust 项目，并添加 anyhow 为依赖：</p>\n<pre><code class=\"language-sh\">cargo new rsqlite\ncd rsqlite\ncargo add anyhow\n</code></pre>\n<h1>SQLite File Format</h1>\n<p>SQLite 的文件格式定义在 <a href=\"https://www.sqlite.org/fileformat.html\">SQlite3 文档：Database File Format</a> 中。</p>\n<pre><code class=\"language-mermaid\">graph TD\n    subgraph &quot;SQLite Database File&quot;\n        subgraph &quot;Database Header (100 bytes)&quot;\n            A1[&quot;Magic Header String (16 bytes)&quot;]\n            A2[&quot;Page Size (2 bytes)&quot;]\n            A3[&quot;File Format Write Version (1 byte)&quot;]\n            A4[&quot;File Format Read Version (1 byte)&quot;]\n            A5[&quot;Reserved Bytes Per Page (1 byte)&quot;]\n            A6[&quot;Payload Fractions (3 bytes)&quot;]\n            A7[&quot;File Change Counter (4 bytes)&quot;]\n            A8[&quot;In-Header Database Size (4 bytes)&quot;]\n            A9[&quot;Free Page List (8 bytes)&quot;]\n            A10[&quot;Schema Cookie (4 bytes)&quot;]\n            A11[&quot;Schema Format Number (4 bytes)&quot;]\n            A12[&quot;Suggested Cache Size (4 bytes)&quot;]\n            A13[&quot;Incremental Vacuum Settings (8 bytes)&quot;]\n            A14[&quot;Text Encoding (4 bytes)&quot;]\n            A15[&quot;User Version Number (4 bytes)&quot;]\n            A16[&quot;Application ID (4 bytes)&quot;]\n            A17[&quot;Reserved for Expansion (20 bytes)&quot;]\n            A18[&quot;Version-Valid-For Number (4 bytes)&quot;]\n            A19[&quot;SQLite Version Number (4 bytes)&quot;]\n        end\n\n        subgraph &quot;Lock-Byte Page (512 bytes)&quot;\n            B1[&quot;Reserved for VFS Locking&quot;]\n        end\n\n        subgraph &quot;Page 1 (Table B-Tree Root Page)&quot;\n            C1[&quot;Database Header&quot;]\n            C2[&quot;B-Tree Page Header&quot;]\n            C3[&quot;Cell Pointer Array&quot;]\n            C4[&quot;Unallocated Space&quot;]\n            C5[&quot;Cell Content Area&quot;]\n            C6[&quot;Reserved Region&quot;]\n        end\n\n        subgraph &quot;Other Pages&quot;\n            subgraph &quot;B-Tree Page&quot;\n                D1[&quot;B-Tree Page Header&quot;]\n                D2[&quot;Cell Pointer Array&quot;]\n                D3[&quot;Unallocated Space&quot;]\n                D4[&quot;Cell Content Area&quot;]\n                D5[&quot;Reserved Region&quot;]\n            end\n            subgraph &quot;Freelist Trunk Page&quot;\n                E1[&quot;Next Trunk Page Number (4 bytes)&quot;]\n                E2[&quot;Number of Leaf Pages (4 bytes)&quot;]\n                E3[&quot;Leaf Page Numbers (4 bytes each)&quot;]\n            end\n            subgraph &quot;Freelist Leaf Page&quot;\n                F1[&quot;No Content&quot;]\n            end\n            subgraph &quot;Overflow Page&quot;\n                G1[&quot;Next Overflow Page Number (4 bytes)&quot;]\n                G2[&quot;Overflow Content&quot;]\n            end\n            subgraph &quot;Ptrmap Page&quot;\n                H1[&quot;Ptrmap Entries (5 bytes each)&quot;]\n            end\n        end\n    end\n\n    C1 --&gt; A2\n    A2 --&gt; D1\n    A2 --&gt; E1\n    A2 --&gt; F1\n    A2 --&gt; G1\n    A2 --&gt; H1\n    A9 --&gt; E1\n    D1 --&gt; G1\n    D1 --&gt; H1\n    H1 --&gt; D1\n</code></pre>\n<p><strong>简单来说：</strong></p>\n<p>SQLite 数据库文件是单个文件，包含数据库的所有数据和元数据。<br>这个文件被组织成一系列的页 (page)，每个页的大小通常是 4KB (4096 字节)，但也可以是 512 字节到 64KB 之间的任何 2 次幂。</p>\n<p><strong>页 (Page) 类型</strong></p>\n<ul>\n<li>数据库头页 (Database Header Page): 数据库文件的第一页。包含了数据库的元数据，例如页大小、数据库格式版本、数据库 schema 等。</li>\n<li>空闲页列表页 (Freelist Page): 这些页跟踪数据库中所有未使用的页。</li>\n<li>B 树页 (B-tree Page): 这些页存储数据库中的实际数据，并使用 B 树为数据结构进行组织。</li>\n<li>溢出页 (Overflow Page): 当 B 树页中的数据太大而无法容纳在一个页中时，可使用溢出页来存储额外的数据。</li>\n</ul>\n<p><strong>B 树页结构</strong></p>\n<p>B 树页是 SQLite 数据库文件中最常见的页类型，用于存储数据库中的表数据。每个 B 树页包含：</p>\n<ul>\n<li>页头 (Page Header): 包含页码、页类型、指向父页和子页的指针等信息。</li>\n<li>单元指针数组 (Cell Pointer Array): 指向页中存储的每个数据单元 (cell) 的指针数组。</li>\n<li>单元内容区 (Cell Content Area): 存储实际数据单元 (cell) 的区域。</li>\n</ul>\n<p><strong>数据单元 (Cell)</strong></p>\n<p>每个数据单元 (cell) 代表数据库表中的一行数据，包含：</p>\n<ul>\n<li>单元头 (Cell Header): 包含单元的大小、数据类型等信息。</li>\n<li>数据 (Data): 实际的数据值。</li>\n</ul>\n<pre><code class=\"language-mermaid\">graph TD\n    A[&quot;SQLite 数据库文件 (.db)&quot;] --&gt; B[&quot;页(Page)&quot;]\n    B --&gt; B1[&quot;数据库头页\n    (Database Header Page)&quot;]\n    B --&gt; B2[&quot;空闲页列表页\n    (Freelist Page)&quot;]\n    B --&gt; B3[&quot;B 树页\n    (B-tree Page)&quot;]\n    B --&gt; B4[&quot;溢出页\n    (Overflow Page)&quot;]\n\n    B3 --&gt; C1[&quot;页头\n    (Page Header)&quot;]\n    B3 --&gt; C2[&quot;单元指针数组\n    (Cell Pointer Array)&quot;]\n    B3 --&gt; C3[&quot;单元内容区\n    (Cell Content Area)&quot;]\n\n    C3 --&gt; D1[&quot;数据单元\n    (Cell)&quot;]\n    D1 --&gt; E1[&quot;单元头\n    (Cell Header)&quot;]\n    D1 --&gt; E2[&quot;数据\n    (Data)&quot;]\n</code></pre>\n<p>这里我们将只对 table btree leaf 页面（B 树页）感兴趣，因为它存储的是实际的表数据。</p>\n<pre><code class=\"language-mermaid\">graph TD\n    subgraph &quot;根节点 (Root Node)&quot;\n        A[&quot;键1 | 指针1 | 键2 | 指针2 | 键3 | 指针3&quot;]\n    end\n    subgraph &quot;中间节点 (Intermediate Node)&quot;\n        B[&quot;键4 | 指针4 | 键5 | 指针5&quot;]\n        C[&quot;键6 | 指针6 | 键7 | 指针7&quot;]\n        D[&quot;键8 | 指针8 | 键9 | 指针9&quot;]\n    end\n    subgraph &quot;叶子节点 (Leaf Node)&quot;\n        E[&quot;数据记录1&quot;]\n        F[&quot;数据记录2&quot;]\n        G[&quot;数据记录3&quot;]\n        H[&quot;数据记录4&quot;]\n        I[&quot;数据记录5&quot;]\n        J[&quot;数据记录6&quot;]\n        K[&quot;数据记录7&quot;]\n        L[&quot;数据记录8&quot;]\n        M[&quot;数据记录9&quot;]\n    end\n    A --&gt; B\n    A --&gt; C\n    A --&gt; D\n    B --&gt; E\n    B --&gt; F\n    C --&gt; G\n    C --&gt; H\n    C --&gt; I\n    D --&gt; J\n    D --&gt; K\n    D --&gt; L\n    D --&gt; M\n</code></pre>\n<h1>实现</h1>\n<p>SQLite 数据库存储在单个文件中，文件分为多页，每页大小相同：2 的幂次，介于 512 和 65536 字节之间。<br>第一页的前 100 个字节包含数据库头，其中包括页面大小和文件格式版本等信息。</p>\n<p>我们的首要任务是实现一个 Pager 结构，用于读取和缓存数据库文件中的页面。<br>但在此之前，我们必须从数据库头中读取页面大小。</p>\n<p>我们定义这些需要的结构如下：</p>\n<pre><code class=\"language-rs\">// 数据库头，可读取页面大小\n#[derive(Debug, Copy, Clone)]\npub struct DbHeader {\n    pub page_size: u32,\n}\n\n// 页面可以有多种类型，这里我们只对 table btree leaf 页面感兴趣，因为它存储的是实际的表数据。\n#[derive(Debug, Copy, Clone)]\npub enum PageType {\n    TableLeaf,\n}\n\n#[derive(Debug, Copy, Clone)]\npub struct PageHeader {\n    // 表示页面类型的字节。对于 table btree-leaf 页面，它是 0x0D。\n    pub page_type: PageType,\n    // 2 字节整数，代表页面中第一个空闲区块的偏移量，如果没有空闲区块，则为 0。\n    pub first_freeblock: u16,\n    // 2 字节整数，代表页面中的单元格数。\n    pub cell_count: u16,\n    // 2 字节整数，代表第一个单元格的偏移量。\n    pub cell_content_offset: u32,\n    // 1 字节整数，代表页面中零散的空闲字节数（我们暂时不会使用它）。\n    pub fragmented_bytes_count: u8,\n}\n\n#[derive(Debug, Clone)]\npub enum Page {\n    TableLeaf(TableLeafPage),\n}\n\n// table btree-leaf 页以一个 8 字节的 header 开始，\n// 随后是一串 &quot;单元格指针&quot;，其中包含页面中每个单元格的偏移量。\n// 单元格包含表数据，我们可以将其视为键值对，\n// 其中键是一个以 varint (rowid) （即可变长度）编码的 64 位整数，\n// 值是代表行数据的任意字节序列。\n#[derive(Debug, Clone)]\npub struct TableLeafPage {\n    pub header: PageHeader,\n    pub cell_pointers: Vec&lt;u16&gt;,\n    pub cells: Vec&lt;TableLeafCell&gt;,\n}\n\n// 每个单元格包含表中一行的值，使用 SQLite 记录格式编码。\n#[derive(Debug, Clone)]\npub struct TableLeafCell {\n    pub size: i64,\n    pub row_id: i64,\n    pub payload: Vec&lt;u8&gt;,\n}\n</code></pre>\n<p>我们实现一系列解析函数，最终组装出目标结构。</p>\n<pre><code class=\"language-rs\">use std::{\n    collections::HashMap,\n    io::{Read, Seek, SeekFrom},\n};\n\nuse anyhow::Context;\n\nuse crate::page;\n\npub const HEADER_SIZE: usize = 100;\nconst HEADER_PREFIX: &amp;[u8] = b&quot;SQLite format 3\\0&quot;;\nconst HEADER_PAGE_SIZE_OFFSET: usize = 16;\n\nconst PAGE_MAX_SIZE: u32 = 65536;\nconst PAGE_LEAF_HEADER_SIZE: usize = 8;\n\nconst PAGE_LEAF_TABLE_ID: u8 = 13;\n\nconst PAGE_FIRST_FREEBLOCK_OFFSET: usize = 1;\nconst PAGE_CELL_COUNT_OFFSET: usize = 3;\nconst PAGE_CELL_CONTENT_OFFSET: usize = 5;\nconst PAGE_FRAGMENTED_BYTES_COUNT_OFFSET: usize = 7;\n\npub fn parse_header(buffer: &amp;[u8]) -&gt; anyhow::Result&lt;page::DbHeader&gt; {\n    // 数据库头以 magic string `SQLite format 3\\0` 开头\n    if !buffer.starts_with(HEADER_PREFIX) {\n        let prefix = String::from_utf8_lossy(&amp;buffer[..HEADER_PREFIX.len()]);\n        anyhow::bail!(&quot;invalid header prefix: {prefix}&quot;);\n    }\n\n    // 紧跟着一个 2 字节大端整数，表示页面大小（是2的幂次）\n    let page_size_raw = read_be_word_at(buffer, HEADER_PAGE_SIZE_OFFSET);\n    let page_size = match page_size_raw {\n        1 =&gt; PAGE_MAX_SIZE, // 由于最大页面大小无法用 2 字节整数表示，因此使用页面大小为 1 来表示最大页面大小\n        n if n.is_power_of_two() =&gt; n as u32,\n        _ =&gt; anyhow::bail!(&quot;page size is not a power of 2: {}&quot;, page_size_raw),\n    };\n\n    Ok(page::DbHeader { page_size })\n}\n\nfn parse_page(buffer: &amp;[u8], page_num: usize) -&gt; anyhow::Result&lt;page::Page&gt; {\n    // 第一页包含数据库头，此时从偏移量 100 开始解析页面\n    let ptr_offset = if page_num == 1 { HEADER_SIZE as u16 } else { 0 };\n\n    match buffer[0] {\n        PAGE_LEAF_TABLE_ID =&gt; parse_table_leaf_page(buffer, ptr_offset),\n        _ =&gt; Err(anyhow::anyhow!(&quot;unknown page type: {}&quot;, buffer[0])),\n    }\n}\n\nfn parse_table_leaf_page(buffer: &amp;[u8], ptr_offset: u16) -&gt; anyhow::Result&lt;page::Page&gt; {\n    let header = parse_page_header(buffer)?;\n\n    let content_buffer = &amp;buffer[PAGE_LEAF_HEADER_SIZE..];\n    let cell_pointers = parse_cell_pointers(content_buffer, header.cell_count as usize, ptr_offset);\n\n    let cells = cell_pointers\n        .iter()\n        .map(|&amp;ptr| parse_table_leaf_cell(&amp;buffer[ptr as usize..]))\n        .collect::&lt;anyhow::Result&lt;Vec&lt;page::TableLeafCell&gt;&gt;&gt;()?;\n\n    Ok(page::Page::TableLeaf(page::TableLeafPage {\n        header,\n        cell_pointers,\n        cells,\n    }))\n}\n\nfn parse_table_leaf_cell(mut buffer: &amp;[u8]) -&gt; anyhow::Result&lt;page::TableLeafCell&gt; {\n    let (n, size) = read_varint_at(buffer, 0);\n    buffer = &amp;buffer[n as usize..];\n\n    let (n, row_id) = read_varint_at(buffer, 0);\n    buffer = &amp;buffer[n as usize..];\n\n    let payload = buffer[..size as usize].to_vec();\n\n    Ok(page::TableLeafCell {\n        size,\n        row_id,\n        payload,\n    })\n}\n\nfn parse_page_header(buffer: &amp;[u8]) -&gt; anyhow::Result&lt;page::PageHeader&gt; {\n    let page_type = match buffer[0] {\n        0x0d =&gt; page::PageType::TableLeaf,\n        _ =&gt; anyhow::bail!(&quot;unknown page type: {}&quot;, buffer[0]),\n    };\n\n    let first_freeblock = read_be_word_at(buffer, PAGE_FIRST_FREEBLOCK_OFFSET);\n    let cell_count = read_be_word_at(buffer, PAGE_CELL_COUNT_OFFSET);\n    let cell_content_offset = match read_be_word_at(buffer, PAGE_CELL_CONTENT_OFFSET) {\n        0 =&gt; 65536,\n        n =&gt; n as u32,\n    };\n    let fragmented_bytes_count = buffer[PAGE_FRAGMENTED_BYTES_COUNT_OFFSET];\n\n    Ok(page::PageHeader {\n        page_type,\n        first_freeblock,\n        cell_count,\n        cell_content_offset,\n        fragmented_bytes_count,\n    })\n}\n\nfn parse_cell_pointers(buffer: &amp;[u8], n: usize, ptr_offset: u16) -&gt; Vec&lt;u16&gt; {\n    let mut pointers = Vec::with_capacity(n);\n    for i in 0..n {\n        pointers.push(read_be_word_at(buffer, 2 * i) - ptr_offset);\n    }\n    pointers\n}\n\n// Varint 的编码规则是：\n// 每个字节的最高位 (MSB) 用作标志位：\n// 1 表示还有后续字节\n// 0 表示这是最后一个字节\n// 每个字节的低 7 位存储整数的一部分，从低位到高位排列\npub fn read_varint_at(buffer: &amp;[u8], mut offset: usize) -&gt; (u8, i64) {\n    let mut size = 0;\n    let mut result = 0;\n\n    // varint 最多占用 8 个字节。当前字节的最高位为 0，表示这是最后一个字节\n    while size &lt; 8 &amp;&amp; buffer[offset] &gt;= 0b1000_0000 {\n        result |= ((buffer[offset] as i64) &amp; 0b0111_1111) &lt;&lt; (7 * size);\n        offset += 1;\n        size += 1;\n    }\n\n    result |= (buffer[offset] as i64) &lt;&lt; (7 * size);\n\n    (size + 1, result)\n}\n\nfn read_be_double_at(input: &amp;[u8], offset: usize) -&gt; u32 {\n    u32::from_be_bytes(input[offset..offset + 4].try_into().unwrap())\n}\n\nfn read_be_word_at(input: &amp;[u8], offset: usize) -&gt; u16 {\n    u16::from_be_bytes(input[offset..offset + 2].try_into().unwrap())\n}\n\n#[derive(Debug, Clone)]\npub struct Pager&lt;I: Read + Seek = std::fs::File&gt; {\n    input: I,\n    page_size: usize,\n    pages: HashMap&lt;usize, page::Page&gt;,\n}\n\nimpl&lt;I: Read + Seek&gt; Pager&lt;I&gt; {\n    pub fn new(input: I, page_size: usize) -&gt; Self {\n        Self {\n            input,\n            page_size,\n            pages: HashMap::new(),\n        }\n    }\n\n    pub fn read_page(&amp;mut self, n: usize) -&gt; anyhow::Result&lt;&amp;page::Page&gt; {\n        if self.pages.contains_key(&amp;n) {\n            return Ok(self.pages.get(&amp;n).unwrap());\n        }\n\n        let page = self.load_page(n)?;\n        self.pages.insert(n, page);\n        Ok(self.pages.get(&amp;n).unwrap())\n    }\n\n    fn load_page(&amp;mut self, n: usize) -&gt; anyhow::Result&lt;page::Page&gt; {\n        let offset = HEADER_SIZE + n.saturating_sub(1) * self.page_size;\n\n        self.input\n            .seek(SeekFrom::Start(offset as u64))\n            .context(&quot;seek to page start&quot;)?;\n\n        let mut buffer = vec![0; self.page_size];\n        self.input.read_exact(&amp;mut buffer).context(&quot;read page&quot;)?;\n\n        parse_page(&amp;buffer, n)\n    }\n}\n</code></pre>\n<p>我们现在有了读取页面和访问页面单元格的方法。但如何解码单元格的值呢？</p>\n<p>每个单元格包含表中一行的值，使用 <a href=\"https://www.sqlite.org/fileformat2.html#record_format\">SQLite 记录（Record）格式</a>编码。<br>记录格式非常简单：记录由标题和字段值序列组成。<br>header 以一个代表其字节大小的 varint 开始，后面是一个 varint 序列（每列一个）用于表示类型。</p>\n<pre><code class=\"language-rs\">use std::borrow::Cow;\n\nuse crate::{page::Page, pager::Pager, value::Value};\n\n#[derive(Debug, Copy, Clone)]\npub enum RecordFieldType {\n    Null,\n    I8,\n    I16,\n    I24,\n    I32,\n    I48,\n    I64,\n    Float,\n    Zero,\n    One,\n    String(usize),\n    Blob(usize),\n}\n\n#[derive(Debug, Clone)]\npub struct RecordField {\n    pub offset: usize,\n    pub field_type: RecordFieldType,\n}\n\n#[derive(Debug, Clone)]\npub struct RecordHeader {\n    pub fields: Vec&lt;RecordField&gt;,\n}\n\nfn parse_record_header(mut buffer: &amp;[u8]) -&gt; anyhow::Result&lt;RecordHeader&gt; {\n    // header 以一个代表其字节大小的 varint 开始，后面是一个 varint 序列\n    let (varint_size, header_length) = crate::pager::read_varint_at(buffer, 0);\n    buffer = &amp;buffer[varint_size as usize..header_length as usize];\n\n    let mut fields = Vec::new();\n    let mut current_offset = header_length as usize;\n\n    while !buffer.is_empty() {\n        let (discriminant_size, discriminant) = crate::pager::read_varint_at(buffer, 0);\n        buffer = &amp;buffer[discriminant_size as usize..];\n\n        // 确定每一列的类型\n        let (field_type, field_size) = match discriminant {\n            0 =&gt; (RecordFieldType::Null, 0),\n            1 =&gt; (RecordFieldType::I8, 1),\n            2 =&gt; (RecordFieldType::I16, 2),\n            3 =&gt; (RecordFieldType::I24, 3),\n            4 =&gt; (RecordFieldType::I32, 4),\n            5 =&gt; (RecordFieldType::I48, 6),\n            6 =&gt; (RecordFieldType::I64, 8),\n            7 =&gt; (RecordFieldType::Float, 8), // 64 位 IEEE 浮点数\n            8 =&gt; (RecordFieldType::Zero, 0),  // 整数 0\n            9 =&gt; (RecordFieldType::One, 0),   // 整数 1\n            // 10 和 11：保留给内部使用\n            n if n &gt;= 12 &amp;&amp; n % 2 == 0 =&gt; {\n                // n 偶数且 n &gt; 12：大小为 (n - 12) / 2 的 BLOB\n                let size = ((n - 12) / 2) as usize;\n                (RecordFieldType::Blob(size), size)\n            }\n            n if n &gt;= 13 &amp;&amp; n % 2 == 1 =&gt; {\n                // n 为奇数且 n &gt; 13：大小为 (n - 13) / 2 的 TEXT\n                let size = ((n - 13) / 2) as usize;\n                (RecordFieldType::String(size), size)\n            }\n            n =&gt; anyhow::bail!(&quot;unsupported field type: {}&quot;, n),\n        };\n\n        fields.push(RecordField {\n            offset: current_offset,\n            field_type,\n        });\n\n        current_offset += field_size;\n    }\n\n    Ok(RecordHeader { fields })\n}\n</code></pre>\n<p>为了更方便地处理记录，我们将定义一个 Value 类型（表示字段值）和一个 Cursor 结构（唯一标识数据库文件中的记录）。<br>Cursor 将暴露一个 field 方法，返回记录第 n 个字段的值：</p>\n<pre><code class=\"language-rs\">#[derive(Debug)]\npub struct Cursor&lt;&#39;p&gt; {\n    header: RecordHeader,\n    pager: &amp;&#39;p mut Pager,\n    page_index: usize,\n    page_cell: usize,\n}\n\nimpl&lt;&#39;p&gt; Cursor&lt;&#39;p&gt; {\n    pub fn field(&amp;mut self, n: usize) -&gt; Option&lt;Value&gt; {\n        let record_field = self.header.fields.get(n)?;\n\n        let payload = match self.pager.read_page(self.page_index) {\n            Ok(Page::TableLeaf(leaf)) =&gt; &amp;leaf.cells[self.page_cell].payload,\n            _ =&gt; return None,\n        };\n\n        match record_field.field_type {\n            RecordFieldType::Null =&gt; Some(Value::Null),\n            RecordFieldType::I8 =&gt; Some(Value::Int(read_i8_at(payload, record_field.offset))),\n            RecordFieldType::I16 =&gt; Some(Value::Int(read_i16_at(payload, record_field.offset))),\n            RecordFieldType::I24 =&gt; Some(Value::Int(read_i24_at(payload, record_field.offset))),\n            RecordFieldType::I32 =&gt; Some(Value::Int(read_i32_at(payload, record_field.offset))),\n            RecordFieldType::I48 =&gt; Some(Value::Int(read_i48_at(payload, record_field.offset))),\n            RecordFieldType::I64 =&gt; Some(Value::Int(read_i64_at(payload, record_field.offset))),\n            RecordFieldType::Float =&gt; Some(Value::Float(read_f64_at(payload, record_field.offset))),\n            RecordFieldType::String(length) =&gt; {\n                let value = std::str::from_utf8(\n                    &amp;payload[record_field.offset..record_field.offset + length],\n                )\n                .expect(&quot;invalid utf8&quot;);\n                Some(Value::String(Cow::Borrowed(value)))\n            }\n            RecordFieldType::Blob(length) =&gt; {\n                let value = &amp;payload[record_field.offset..record_field.offset + length];\n                Some(Value::Blob(Cow::Borrowed(value)))\n            }\n            _ =&gt; panic!(&quot;unimplemented&quot;),\n        }\n    }\n}\n\nfn read_i8_at(input: &amp;[u8], offset: usize) -&gt; i64 {\n    input[offset] as i64\n}\n\nfn read_i16_at(input: &amp;[u8], offset: usize) -&gt; i64 {\n    i16::from_be_bytes(input[offset..offset + 2].try_into().unwrap()) as i64\n}\n\nfn read_i24_at(input: &amp;[u8], offset: usize) -&gt; i64 {\n    (i32::from_be_bytes(input[offset..offset + 3].try_into().unwrap()) &amp; 0x00FFFFFF) as i64\n}\n\nfn read_i32_at(input: &amp;[u8], offset: usize) -&gt; i64 {\n    i32::from_be_bytes(input[offset..offset + 4].try_into().unwrap()) as i64\n}\n\nfn read_i48_at(input: &amp;[u8], offset: usize) -&gt; i64 {\n    i64::from_be_bytes(input[offset..offset + 6].try_into().unwrap()) &amp; 0x0000FFFFFFFFFFFF\n}\n\nfn read_i64_at(input: &amp;[u8], offset: usize) -&gt; i64 {\n    i64::from_be_bytes(input[offset..offset + 8].try_into().unwrap())\n}\n\nfn read_f64_at(input: &amp;[u8], offset: usize) -&gt; f64 {\n    f64::from_be_bytes(input[offset..offset + 8].try_into().unwrap())\n}\n</code></pre>\n<pre><code class=\"language-rs\">use std::borrow::Cow;\n\n#[derive(Debug, Clone)]\npub enum Value&lt;&#39;p&gt; {\n    Null,\n    String(Cow&lt;&#39;p, str&gt;),\n    Blob(Cow&lt;&#39;p, [u8]&gt;),\n    Int(i64),\n    Float(f64),\n}\n\nimpl&lt;&#39;p&gt; Value&lt;&#39;p&gt; {\n    pub fn as_str(&amp;self) -&gt; Option&lt;&amp;str&gt; {\n        if let Value::String(s) = self {\n            Some(s.as_ref())\n        } else {\n            None\n        }\n    }\n}\n</code></pre>\n<p>为了简化对页面记录的迭代，我们还将实现一个 Scanner 结构，它可以封装页面，并允许我们为每条记录获取一个 Cursor 结构：</p>\n<pre><code class=\"language-rs\">\n#[derive(Debug)]\npub struct Scanner&lt;&#39;p&gt; {\n    pager: &amp;&#39;p mut Pager,\n    page: usize,\n    cell: usize,\n}\n\nimpl&lt;&#39;p&gt; Scanner&lt;&#39;p&gt; {\n    pub fn new(pager: &amp;&#39;p mut Pager, page: usize) -&gt; Scanner&lt;&#39;p&gt; {\n        Scanner {\n            pager,\n            page,\n            cell: 0,\n        }\n    }\n    pub fn next_record(&amp;mut self) -&gt; Option&lt;anyhow::Result&lt;Cursor&gt;&gt; {\n        let page = match self.pager.read_page(self.page) {\n            Ok(page) =&gt; page,\n            Err(e) =&gt; return Some(Err(e)),\n        };\n\n        match page {\n            Page::TableLeaf(leaf) =&gt; {\n                let cell = leaf.cells.get(self.cell)?;\n\n                let header = match parse_record_header(&amp;cell.payload) {\n                    Ok(header) =&gt; header,\n                    Err(e) =&gt; return Some(Err(e)),\n                };\n\n                let record = Cursor {\n                    header,\n                    pager: self.pager,\n                    page_index: self.page,\n                    page_cell: self.cell,\n                };\n\n                self.cell += 1;\n\n                Some(Ok(record))\n            }\n        }\n    }\n}\n</code></pre>\n<p>在完成了大部分前期工作后，我们就可以回到最初的目标：列出表。<br>SQLite 将数据库的模式（Schema）存储在一个名为 <a href=\"https://sqlite.org/schematab.html\">sqlite_schema</a> 的特殊表中。<br>该表用于表示 schema 对象。</p>\n<pre><code class=\"language-sql\">CREATE TABLE sqlite_schema(\n  type text,\n  name text,\n  tbl_name text,\n  rootpage integer,\n  sql text\n);\n</code></pre>\n<p>表字段的含义如下：</p>\n<ul>\n<li><code>type</code>：可为 <code>table</code>，<code>index</code>，<code>view</code> 或 <code>trigger</code>。我们这里只有表，因此只会是 <code>table</code></li>\n<li><code>name</code>：schema 对象的名称</li>\n<li><code>tbl_name</code>：schema 对象关联的表名称，如果是表，则与 <code>name</code> 相同</li>\n<li><code>rootpage</code>：表的根页面，稍后我们将使用它来读取表的内容</li>\n<li><code>sql</code>：用于创建表的 SQL 语句</li>\n</ul>\n<pre><code class=\"language-sh\">sqlite&gt; select * from sqlite_schema;\ntable|table1|table1|2|CREATE TABLE table1(id integer)\ntable|table2|table2|3|CREATE TABLE table2(id integer)\n</code></pre>\n<p>由于我们的简单数据库目前只处理基本 schema，因此我们可以假定整个 schema 都包含在数据库文件的第一页中。<br>为了列出数据库中的表，我们需要：</p>\n<ul>\n<li>用数据库文件初始化 Pager</li>\n<li>为第一页创建 Scanner</li>\n<li>遍历 records，并打印其 name 字段的值（位于索引 1）</li>\n</ul>\n<pre><code class=\"language-rs\">use std::{io::Read, path::Path};\n\nuse anyhow::Context;\n\nuse crate::{cursor::Scanner, page::DbHeader, pager, pager::Pager};\n\npub struct Db {\n    pub header: DbHeader,\n    pager: Pager,\n}\n\nimpl Db {\n    pub fn from_file(filename: impl AsRef&lt;Path&gt;) -&gt; anyhow::Result&lt;Db&gt; {\n        let mut file = std::fs::File::open(filename.as_ref()).context(&quot;open db file&quot;)?;\n\n        let mut header_buffer = [0; pager::HEADER_SIZE];\n        file.read_exact(&amp;mut header_buffer)\n            .context(&quot;read db header&quot;)?;\n\n        let header = pager::parse_header(&amp;header_buffer).context(&quot;parse db header&quot;)?;\n\n        let pager = Pager::new(file, header.page_size as usize);\n\n        Ok(Db { header, pager })\n    }\n\n    pub fn scanner(&amp;mut self, page: usize) -&gt; Scanner {\n        Scanner::new(&amp;mut self.pager, page)\n    }\n}\n</code></pre>\n<p>实现支持 tables 命令的基本 REPL 非常简单：</p>\n<pre><code class=\"language-rs\">use std::io::{stdin, BufRead, Write};\n\nuse anyhow::Context;\n\nmod cursor;\nmod db;\nmod page;\nmod pager;\nmod value;\n\nfn main() -&gt; anyhow::Result&lt;()&gt; {\n    let database = db::Db::from_file(std::env::args().nth(1).context(&quot;missing db file&quot;)?)?;\n    cli(database)\n}\n\nfn cli(mut db: db::Db) -&gt; anyhow::Result&lt;()&gt; {\n    print_flushed(&quot;rqlite&gt; &quot;)?;\n\n    let mut line_buffer = String::new();\n\n    while stdin().lock().read_line(&amp;mut line_buffer).is_ok() {\n        match line_buffer.trim() {\n            &quot;.exit&quot; =&gt; break,\n            &quot;.tables&quot; =&gt; display_tables(&amp;mut db)?,\n            _ =&gt; {\n                println!(&quot;Unrecognized command &#39;{}&#39;&quot;, line_buffer.trim());\n            }\n        }\n\n        print_flushed(&quot;\\nrqlite&gt; &quot;)?;\n\n        line_buffer.clear();\n    }\n\n    Ok(())\n}\n\nfn display_tables(db: &amp;mut db::Db) -&gt; anyhow::Result&lt;()&gt; {\n    let mut scanner = db.scanner(1);\n\n    while let Some(Ok(mut record)) = scanner.next_record() {\n        let type_value = record\n            .field(0)\n            .context(&quot;missing type field&quot;)\n            .context(&quot;invalid type field&quot;)?;\n\n        if type_value.as_str() == Some(&quot;table&quot;) {\n            let name_value = record\n                .field(1)\n                .context(&quot;missing name field&quot;)\n                .context(&quot;invalid name field&quot;)?;\n\n            print!(&quot;{} &quot;, name_value.as_str().unwrap());\n        }\n    }\n\n    Ok(())\n}\n\nfn print_flushed(s: &amp;str) -&gt; anyhow::Result&lt;()&gt; {\n    print!(&quot;{}&quot;, s);\n    std::io::stdout().flush().context(&quot;flush stdout&quot;)\n}\n</code></pre>\n<blockquote>\n<p>改编自：<a href=\"https://blog.sylver.dev/build-your-own-sqlite-part-1-listing-tables\">https://blog.sylver.dev/build-your-own-sqlite-part-1-listing-tables</a><br>另见：<a href=\"https://blog.jabid.in/2024/11/24/sqlite.html\">https://blog.jabid.in/2024/11/24/sqlite.html</a>\nto be continued...</p>\n</blockquote>\n","tags":["db"]},{"id":"react-closures","url":"https://yieldray.fun/posts/react-closures","title":"Sneaky React Memory Leaks: How `useCallback` and closures can bite you\n","date_published":"2024-10-18T00:26:19.000Z","date_modified":"2024-10-18T00:26:19.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://www.schiener.io/2024-03-03/react-closures\">https://www.schiener.io/2024-03-03/react-closures</a><br>原文包含一节 <a href=\"https://www.schiener.io/2024-03-03/react-closures#the-problem\">关于闭包的简要回顾</a>，为保持简略此处没有保留。</p>\n</blockquote>\n<h2>闭包与 React</h2>\n<p>在 React 中，我们重度依赖闭包来实现所有函数组件、hooks 和事件处理程序。<br>每当创建一个新函数，需要访问组件作用域内的变量（如状态或属性）时，实际上就是在创建闭包。</p>\n<p>以下是一个示例：</p>\n<pre><code class=\"language-jsx\">import { useState, useEffect } from &quot;react&quot;;\n\nfunction App({ id }) {\n    const [count, setCount] = useState(0);\n\n    const handleClick = () =&gt; {\n        setCount(count + 1); // 这是一个对 count 变量的闭包\n    };\n\n    useEffect(() =&gt; {\n        console.log(id); // 这是一个对 id 属性的闭包\n    }, [id]);\n\n    return (\n        &lt;div&gt;\n            &lt;p&gt;{count}&lt;/p&gt;\n            &lt;button onClick={handleClick}&gt;Increment&lt;/button&gt;\n        &lt;/div&gt;\n    );\n}\n</code></pre>\n<p>在大多数情况下，这本身并不是问题。在上面的例子中，每次 <code>App</code> 渲染时都会重新创建闭包，旧的闭包会被垃圾回收。<br>这可能意味着一些不必要的分配和释放操作，但这些操作通常都非常快。</p>\n<p>然而，当我们的应用程序变得庞大，并开始使用 <code>useMemo</code> 和 <code>useCallback</code> 等记忆化技术来避免不必要的重新渲染时，就需要格外注意了。</p>\n<h2>闭包与 <code>useCallback</code></h2>\n<p>使用记忆化 hooks，我们以增加内存使用量为代价来换取更好的渲染性能。只要依赖项没有改变，<code>useCallback</code> 就会一直持有对函数的引用。让我们来看一个例子：</p>\n<pre><code class=\"language-tsx\">import React, { useState, useCallback } from &quot;react&quot;;\n\nfunction App() {\n    const [count, setCount] = useState(0);\n\n    const handleEvent = useCallback(() =&gt; {\n        setCount(count + 1);\n    }, [count]);\n\n    return (\n        &lt;div&gt;\n            &lt;p&gt;{count}&lt;/p&gt;\n            &lt;ExpensiveChildComponent onMyEvent={handleEvent} /&gt;\n        &lt;/div&gt;\n    );\n}\n</code></pre>\n<p>在这个例子中，我们希望避免 <code>ExpensiveChildComponent</code> 的重新渲染。我们可以尝试保持 <code>handleEvent()</code> 函数引用的稳定性来实现这一点。<br>我们使用 <code>useCallback</code> 记忆 <code>handleEvent()</code>，以便仅在 <code>count</code> 状态发生变化时才重新分配新值。<br>然后，我们可以将 <code>ExpensiveChildComponent</code> 包裹在 <code>React.memo()</code> 中，以避免在父组件 <code>App</code> 渲染时重新渲染。<br>到目前为止，一切顺利。</p>\n<p>但是，让我们稍微修改一下这个例子：</p>\n<pre><code class=\"language-tsx\">import { useState, useCallback } from &quot;react&quot;;\n\nclass BigObject {\n    public readonly data = new Uint8Array(1024 * 1024 * 10); // 10MB 的数据\n}\n\nfunction App() {\n    const [count, setCount] = useState(0);\n    const bigData = new BigObject();\n\n    const handleEvent = useCallback(() =&gt; {\n        setCount(count + 1);\n    }, [count]);\n\n    const handleClick = () =&gt; {\n        console.log(bigData.data.length);\n    };\n\n    return (\n        &lt;div&gt;\n            &lt;button onClick={handleClick} /&gt;\n            &lt;ExpensiveChildComponent2 onMyEvent={handleEvent} /&gt;\n        &lt;/div&gt;\n    );\n}\n</code></pre>\n<p><strong>你能猜到会发生什么吗？</strong></p>\n<p>由于 <code>handleEvent()</code> 创建了一个对 <code>count</code> 变量的闭包，它将持有对<strong>组件上下文对象</strong>的引用。<br>而且，即使我们<em>从未在 <code>handleEvent()</code> 函数中访问 <code>bigData</code></em>，<code>handleEvent()</code> 仍然会通过组件的上下文对象持有对 <code>bigData</code> 的引用。</p>\n<p>所有闭包都共享一个从创建时就存在的共同上下文对象。由于 <code>handleClick()</code> 闭包捕获了 <code>bigData</code>，因此 <code>bigData</code> 将被此上下文对象引用。<br>这意味着，只要 <code>handleEvent()</code> 被引用，<code>bigData</code> 就永远不会被垃圾回收。这个引用将一直保持到 <code>count</code> 发生变化并且 <code>handleEvent()</code> 被重新创建。</p>\n<p><img src=\"https://s2.loli.net/2024/10/18/NwFQ6LrhPH5MgEo.png\" alt=\"捕获大型对象\"></p>\n<blockquote>\n<p>译注：组件上下文对象简单来说就是 React Fiber 对象<br>读者可参考 <a href=\"https://github.com/facebook/react/blob/bf7e210cb5672685bfe992a3b253880f5a3d47f5/packages/react-reconciler/src/ReactFiberHooks.js#L2836\">useCallback 相关源码</a> 和 <a href=\"https://github.com/facebook/react/blob/bf7e210cb5672685bfe992a3b253880f5a3d47f5/packages/react-reconciler/src/ReactFiber.js#L136\">React Fiber 类源码</a></p>\n</blockquote>\n<h2><code>useCallback</code> + 闭包 + 大型对象的无限内存泄漏</h2>\n<p>让我们看最后一个例子，它将上述所有内容发挥到了极致。这个例子是我在我们的应用程序中遇到的一个简化版本。<br>因此，尽管这个例子可能看起来有些刻意，但它很好地展示了普遍存在的问题。</p>\n<pre><code class=\"language-jsx\">import { useState, useCallback } from &#39;react&#39;;\n\nclass BigObject {\n  public readonly data = new Uint8Array(1024 * 1024 * 10);\n}\n\nexport const App = () =&gt; {\n  const [countA, setCountA] = useState(0);\n  const [countB, setCountB] = useState(0);\n  const bigData = new BigObject(); // 10MB 的数据\n\n  const handleClickA = useCallback(() =&gt; {\n    setCountA(countA + 1);\n  }, [countA]);\n\n  const handleClickB = useCallback(() =&gt; {\n    setCountB(countB + 1);\n  }, [countB]);\n\n  // 这段代码仅仅是为了演示问题\n  const handleClickBoth = () =&gt; {\n    handleClickA();\n    handleClickB();\n    console.log(bigData.data.length);\n  };\n\n  return (\n    &lt;div&gt;\n      &lt;button onClick={handleClickA}&gt;Increment A&lt;/button&gt;\n      &lt;button onClick={handleClickB}&gt;Increment B&lt;/button&gt;\n      &lt;button onClick={handleClickBoth}&gt;Increment Both&lt;/button&gt;\n      &lt;p&gt;\n        A: {countA}, B: {countB}\n      &lt;/p&gt;\n    &lt;/div&gt;\n  );\n};\n</code></pre>\n<p>在这个例子中，我们有两个记忆化的事件处理程序 <code>handleClickA()</code> 和 <code>handleClickB()</code>。<br>我们还有一个函数 <code>handleClickBoth()</code>，它调用这两个事件处理程序并记录 <code>bigData</code> 的长度。</p>\n<p><strong>你能猜到当我们交替点击“Increment A”和“Increment B”按钮时会发生什么吗？</strong></p>\n<p>让我们来看看在 Chrome 开发者工具中，点击每个按钮 5 次后的内存使用情况：</p>\n<p><img src=\"https://s2.loli.net/2024/10/18/VIDHmqkjtYTKuLz.png\" alt=\"BigObject 泄漏\"></p>\n<p>看起来 <code>bigData</code> 从未被垃圾回收。每次点击都会导致内存使用量持续增长。<br>在我们的例子中，应用程序持有对 11 个 <code>BigObject</code> 实例的引用，每个实例大小为 10MB。<br>一个用于初始渲染，其余的每次点击都会创建一个。</p>\n<p>保留树为我们提供了一些线索。看起来我们正在创建一个不断重复的引用链。让我们一步一步地分析它。</p>\n<p><strong>0. 首次渲染：</strong></p>\n<p>当 <code>App</code> 首次渲染时，它会创建一个<em>闭包作用域</em>，该作用域持有对所有变量的引用，因为我们在至少一个闭包中使用了所有这些变量。<br>这包括 <code>bigData</code>、<code>handleClickA()</code> 和 <code>handleClickB()</code>。我们在 <code>handleClickBoth()</code> 中引用了它们。我们将这个闭包作用域称为 <code>AppScope#0</code>。</p>\n<p><img src=\"https://s2.loli.net/2024/10/18/3UqpM4zPwsDRrSt.png\" alt=\"闭包链 0\"></p>\n<p><strong>1. 点击“Increment A”：</strong></p>\n<ul>\n<li>第一次点击“Increment A”会导致 <code>handleClickA()</code> 被重新创建，因为我们改变了 <code>countA</code> - 我们将新的闭包称为 <code>handleClickA()#1</code>。</li>\n<li><code>handleClickB()#0</code> <em>不会</em> 被重新创建，因为 <code>countB</code> 没有改变。</li>\n<li>然而，这意味着 <code>handleClickB()#0</code> 仍然会持有对先前 <code>AppScope#0</code> 的引用。</li>\n<li>新的 <code>handleClickA()#1</code> 将持有对 <code>AppScope#1</code> 的引用，<code>AppScope#1</code> 又持有对 <code>handleClickB()#0</code> 的引用。</li>\n</ul>\n<p><img src=\"https://s2.loli.net/2024/10/18/rOpeiX4Hl63NoTc.png\" alt=\"闭包链 1\"></p>\n<p><strong>2. 点击“Increment B”：</strong></p>\n<ul>\n<li>第一次点击“Increment B”会导致 <code>handleClickB()</code> 被重新创建，因为我们改变了 <code>countB</code>，从而创建了 <code>handleClickB()#1</code>。</li>\n<li>React <em>不会</em> 重新创建 <code>handleClickA()</code>，因为 <code>countA</code> 没有改变。</li>\n<li><code>handleClickB()#1</code> 将持有对 <code>AppScope#2</code> 的引用，<code>AppScope#2</code> 持有对 <code>handleClickA()#1</code> 的引用，<code>handleClickA()#1</code> 持有对 <code>AppScope#1</code> 的引用，<code>AppScope#1</code> 持有对 <code>handleClickB()#0</code> 的引用。</li>\n</ul>\n<p><img src=\"https://s2.loli.net/2024/10/18/H1iTrChXwG4LB65.png\" alt=\"闭包链 2\"></p>\n<p><strong>3. 第二次点击“Increment A”：</strong></p>\n<p>以此类推，我们可以创建一个无休止的闭包链，它们相互引用并且永远不会被垃圾回收，同时还拖着一个独立的 10MB <code>bigData</code> 对象，因为它在每次渲染时都会被重新创建。</p>\n<p><img src=\"https://s2.loli.net/2024/10/18/kB6om48Xy5gfGWT.png\" alt=\"闭包链\"></p>\n<h2>问题的本质</h2>\n<p>问题的本质在于，单个组件中的不同 <code>useCallback</code> hooks 可能会通过闭包作用域相互引用，以及引用其它占用大量内存的数据。<br>然后，这些闭包会一直保存在内存中，直到 <code>useCallback</code> hooks 被重新创建。<br>在一个组件中拥有多个 <code>useCallback</code> hook 会使得我们很难推断出哪些内容被保存在内存中，以及何时会被释放。<br>拥有的回调越多，就越有可能遇到这个问题。</p>\n<h2>你是否会遇到这个问题？</h2>\n<p>以下是一些让你更容易遇到这个问题的因素：</p>\n<ol>\n<li>你有一些很少被重新创建的大型组件，例如，你将很多状态提升到应用程序外壳组件中。<blockquote>\n<p>译注：组件存活时间越长，就越有时间从其它闭包中捕获各种变量（比如一些需要在组件树中提升的状态）。这通常是组件树顶端的问题。</p>\n</blockquote>\n</li>\n<li>你依赖 <code>useCallback</code> 来最小化重新渲染。</li>\n<li>你从记忆化的函数中调用其它函数。</li>\n<li>你处理大型对象，例如图像数据或大型数组。</li>\n</ol>\n<p>如果你不需要处理任何大型对象，那么引用几个额外的字符串或数字可能不是问题。<br>大多数情况下，这些闭包交叉引用会在足够多的属性发生变化后被清除。<br>只是要注意，你的应用程序占用的内存可能会比你预期的要多。</p>\n<h2>如何避免闭包和 <code>useCallback</code> 导致的内存泄漏？</h2>\n<p>以下是我可以提供的一些避免此问题的技巧：</p>\n<p><em>技巧 1：尽可能缩小闭包作用域。</em></p>\n<p>JavaScript 中难以发现所有被捕获的变量。避免持有过多变量的最佳方法是减少闭包周围的函数大小。这意味着：</p>\n<ol>\n<li><em>编写更小的组件</em>。这将减少创建新闭包时作用域内的变量数量。</li>\n<li><em>编写自定义 hooks</em>。因为这样任何回调都只能捕获 hook 函数作用域内的变量。这通常只意味着函数参数。</li>\n</ol>\n<p><em>技巧 2：避免捕获其它闭包，尤其是记忆化的闭包。</em></p>\n<p>尽管这看起来很明显，但 React 很容易让人掉入这个陷阱。如果你编写了相互调用的较小函数，那么一旦你添加了第一个 <code>useCallback</code>，组件作用域内的所有被调用函数都会发生连锁反应，从而被记忆化。</p>\n<p><em>技巧 3：在没有必要的情况下避免记忆化。</em></p>\n<p><code>useCallback</code> 和 <code>useMemo</code> 是避免不必要重新渲染的好工具，但它们是有代价的。只有当你注意到由于渲染而导致性能问题时才使用它们。</p>\n<p><em>技巧 4（紧急出口）：对大型对象使用 <code>useRef</code>。</em></p>\n<p>这可能意味着你需要自己处理对象的声明周期并正确地清理它。这不是最佳选择，但总比内存泄漏要好。</p>\n<h2>结论</h2>\n<p>闭包是 React 中一种常用的模式，它们允许我们的函数记住组件上次渲染时作用域内的属性和状态。<br>当与 <code>useCallback</code> 等记忆化技术结合使用时，这可能会导致意外的内存泄漏，尤其是在处理大型对象时。<br>为了避免这些内存泄漏，请尽可能缩小闭包作用域，在非必要情况下避免使用记忆化，并可以考虑对大型对象使用 <code>useRef</code>。</p>\n<p>Big thanks to David Glasser for his 2013 article <a href=\"http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html\">A surprising JavaScript memory leak found at Meteor</a> that pointed me in the right direction.</p>\n<h2>Feedback?</h2>\n<p>Do you think I missed something or got something wrong? Maybe you have a better solution to the problem or never encountered it in the first place.</p>\n<p>If you have any questions or comments, please feel free to reach out to me on <a href=\"https://www.linkedin.com/in/kevin-schiener?lipi=urn%3Ali%3Apage%3Ad_flagship3_profile_view_base_contact_details%3BgPhiPd6iQU2G3nTikyAtIA%3D%3D\">LinkedIn</a> or <a href=\"https://twitter.com/KevinSchiener\">X/Twitter</a>.</p>\n<p>Happy debugging!</p>\n<ul>\n<li><em>Follow-up article for React Query users:</em> <a href=\"https://www.schiener.io/2024-05-29/react-query-leaks\">Sneaky React Memory Leaks II: Closures Vs. React Query</a>.</li>\n<li><em>Interested how the React compiler will handle this?:</em> <a href=\"https://www.schiener.io/2024-07-07/react-closures-compiler\">Sneaky React Memory Leaks: How the React compiler won&#39;t save you</a>.</li>\n</ul>\n","tags":["react"]},{"id":"css-at-property","url":"https://yieldray.fun/posts/css-at-property","title":"css @property","date_published":"2024-10-13T15:00:00.000Z","date_modified":"2024-10-13T15:00:00.000Z","content_text":"<p>兼容性：<a href=\"https://caniuse.com/mdn-css_at-rules_property\">https://caniuse.com/mdn-css_at-rules_property</a></p>\n<hr>\n<p>@property 用于注册自定义属性。注册后的自定义属性称为<em>已注册自定义属性</em>（registered custom property）。区分与在此特性之前的自定义属性被称为<em>未注册自定义属性</em>（unregistered custom properties）</p>\n<p>已注册自定义属性有一个实用特性就是它在<a href=\"https://drafts.css-houdini.org/css-properties-values-api/#substitution\">使用 <code>var()</code> 函数后会被替换为<em>计算值</em></a>，而非用于生成该值的原始标记序列（对于未注册自定义属性）。</p>\n<h1><a href=\"https://drafts.css-houdini.org/css-properties-values-api/#at-property-rule\">Syntax</a></h1>\n<pre><code class=\"language-css\">@property &lt;custom-property-name&gt; {\n  &lt;declaration-list&gt;\n}\n</code></pre>\n<p>与普通选择器一样，包含一组声明列表。不过列表中不是样式声明（<a href=\"https://drafts.csswg.org/css-syntax-3/#qualified-rule\">qualified rule</a>），而是只允许 <code>syntax</code> <code>inherits</code> <code>initial-value</code> 三个描述符。</p>\n<p>其中 syntax 可指定为字符串表示的语法规则，<a href=\"https://drafts.css-houdini.org/css-properties-values-api/#supported-names\">参见规范</a>。<br>可以简单理解为<a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_Types\">数据类型</a>语法。</p>\n<pre><code class=\"language-ts\">declare namespace CSS {\n    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CSS/registerProperty_static) */\n    function registerProperty(definition: PropertyDefinition): void;\n}\ninterface PropertyDefinition {\n    /** A string representing the name of the property being defined. */\n    name: string;\n    /** A string representing the expected syntax of the defined property. Defaults to &quot;*&quot;. */\n    syntax?: string;\n    /** A boolean value defining whether the defined property should be inherited (`true`), or not (`false`). Defaults to `false`. */\n    inherits: boolean;\n    /** A string representing the initial value of the defined property. */\n    initialValue?: string;\n}\n</code></pre>\n<h1>示例</h1>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"at-property\" src=\"https://codepen.io/YieldRay/embed/bGXgQNX?default-tab=css%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/bGXgQNX\">\n  at-property</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_animated_properties\">动画性</a></h1>\n<p>CSS 动画和过渡依赖于<em>动画性</em>（animatable）属性这一概念。</p>\n<ul>\n<li>对于<em>已注册自定义属性</em>，其动画类型为<em>按计算值</em>，且其计算值类型由此属性的语法定义所决定。</li>\n<li>对于<em>未注册自定义属性</em>，其动画类型为<em>离散</em>。</li>\n</ul>\n","tags":["css"]},{"id":"windows-nt-vs-unix-design","url":"https://yieldray.fun/posts/windows-nt-vs-unix-design","title":"Windows NT vs. Unix: A design comparison","date_published":"2024-09-27T00:41:42.000Z","date_modified":"2024-09-27T00:41:42.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://blogsystem5.substack.com/p/windows-nt-vs-unix-design\">https://blogsystem5.substack.com/p/windows-nt-vs-unix-design</a><br>Windows NT vs. Unix: A design comparison — by Julio Merino</p>\n</blockquote>\n<blockquote>\n<p>NT 经常被吹捧为“非常先进”的操作系统。这是为什么呢？是什么让 NT 比 Unix 更好？现在还是这样吗？</p>\n</blockquote>\n<hr>\n<p>多年来，我反复听说 Windows NT 是一个非常先进的操作系统，作为一名 Unix 用户，我一直被不知道 <em>为什么</em> （它很先进）而困扰。多年来，我一直想回答这个问题，现在我可以回答了，这意味着我想向你们介绍我的发现。</p>\n<p>我对 NT 内部结构的渴望始于 2006 年，当时我申请了 Google Summer of Code 计划来开发 Boost.Process。我需要为 ATF 开发这样一个库，但我也将该项目视为学习 Win32 API 的机会。然后，这段旅程在 2020 年继续，我在 Google 工作了很长时间后<a href=\"https://jmmv.dev/2020/10/bye-google-hi-microsoft.html\">选择加入 Microsoft</a>，并在 2021 年购买了 <a href=\"https://www.amazon.com/Windows-Internals-Part-architecture-management/dp/0735684189\">Windows Internals</a> 第 5 版书（由于其令人难以置信的细节和篇幅，我从未完全阅读过这本书）。然而，这些都没有让我学到我想要的东西：NT 与 Unix 的根本区别（如果有的话）。</p>\n<p><img src=\"https://s2.loli.net/2024/09/26/h5KmzSNRLf8EBFq.jpg\" alt=\"\"></p>\n<p>然后，在 2023 年底，<a href=\"https://blogsystem5.substack.com/p/windows-nt-peeking-into-the-cradle\">Showstopper</a> 这本书再次激发了（我的）这种好奇心。很快，一个新的想法浮现在脑海中：Windows Internals 第 5 版这本书太晦涩了，但是……第一版呢？当然，这本书肯定更容易消化，因为上世纪 90 年代初的系统要简单得多。于是，我搜索到了这一版本，并在标题 <a href=\"https://www.amazon.com/Inside-Windows-NT-Helen-Custer/dp/155615481X\">Inside Windows NT</a> 下找到了它，从头到尾读了一遍，并做了笔记，来评估 NT 与 Unix 的对比。</p>\n<p>这就是我写这篇文章的初衷 —— 将 NT 的设计（1993 年 7 月）与当代 Unix 系统（如 4.4BSD 系统（1994 年 6 月）或 Linux 1.0 系统（1994 年 3 月））进行比较的一些想法。请注意，由于我的背景，这篇文章是从一个 Unix “专家”和一个 NT “无知者”的角度来写的，因此它侧重于描述 NT 的不同之处。</p>\n<h2>任务</h2>\n<p><a href=\"https://en.wikipedia.org/wiki/Unix\">Unix</a> 的历史很长 —— 比 NT 的历史要长得多。Unix 的开发始于 1969 年，其主要目标是为程序员提供一个方便的平台。Unix 的灵感来自 <a href=\"https://en.wikipedia.org/wiki/Multics\">Multics</a>，但与其它系统相比，Unix 专注于简单性，这是它战胜 Multics 的一个特质。不过，可移植性和多任务处理并不是 Unix 设计的最初目标：这些功能在几年后 Unix 的许多 “分叉” 和重塑中得到了改造。</p>\n<p>反观 Microsoft 这边，MS-DOS 的第一个版本于 1981 年 8 月推出，“旧版 Windows”（基于 DOS 的版本）的第一个版本于 1985 年 11 月推出。虽然 MS-DOS 取得了广泛的成功，但直到 1990 年 5 月的 <a href=\"https://en.wikipedia.org/wiki/Windows_3.0\">Windows 3.0</a> 才真正开始变得重要。Windows NT 构思于 1989 年，并在 1993 年 7 月问世发布 NT 3.1。</p>\n<p>这个时间线给了 Microsoft 一个优势：NT 的设计比 Unix 晚了 20 年，凭借 MS-DOS 和旧版 Windows，Microsoft 已经拥有庞大的用户群。设计 NT 的 Microsoft 团队对这些发展有后见之明，以前有开发其它操作系统的经验，并且可以使用更现代的技术，因此他们可以在创建 NT 时“一步登天”。</p>\n<p>特别是，作为其使命的一部分，NT 从以下设计目标开始，这些目标与 Unix 的形成鲜明对比：</p>\n<ol>\n<li>可移植性；</li>\n<li>支持多处理器系统（SMP）；</li>\n<li>与 DOS、旧版 Windows、OS/2 和 POSIX 兼容。</li>\n</ol>\n<p>这些目标不容小觑，这意味着 NT 从一开始就有着坚实的设计准则。换句话说：这些功能从第一天起便已存在，而不像许多 Unix 系统那样在后期才被添加进去。</p>\n<h2>内核</h2>\n<p>现在我们知道了其中一些设计目标和约束，让我们看看它们实现的细节。</p>\n<p>除了 <a href=\"https://www.minix3.org/\">Minix</a> 或 <a href=\"https://www.gnu.org/software/hurd/\">GNU Hurd</a> 等少数例外，Unix 是作为宏内核实现的，它暴露一系列系统调用来与操作系统提供的功能进行交互。另一方面，NT 是宏内核和微内核的混合体：其特权组件，被称为 <em>执行体</em>（executive），以模块化组件的集合形式呈现给用户空间 <em>子系统</em>（subsystems）。用户空间子系统是特殊的进程，它将应用程序使用的 API（POSIX、OS/2 等）“翻译”为执行体的系统调用。</p>\n<p>NT 执行体的一个重要部分是硬件抽象层（HAL），这是一个提供抽象原语以访问机器硬件的模块，也是内核其余部分的基础。这一层是让 NT 能在各种架构上运行的关键，包括 i386、Alpha 和 PowerPC。要理解 HAL 的重要性，我们可以看看同时期的 Unix：是的，Unix 作为一个概念是可移植的，因为存在许多针对不同机器的不同变体，但其实现并不可移植。 SunOS 最初只支持 Motorola 68000；386BSD 是首个移植到 Intel 架构的 BSD；IRIX 是为 Silicon Graphic 基于 MIPS 的工作站设计的 Unix 变体；诸如此类。这解释了为什么 NetBSD 主要关注通过在硬件之上提供最小 shim 来实现可移植性，在当时是如此有趣：除了 NT ，其它操作系统内部都没有这种清晰的设计，而 NT 在这方面早已有了领先。</p>\n<p>NT 执行体的另一个重要组成部分是它对多处理系统的支持和它的抢占式内核。内核有各种中断级别（在 BSD 术语中称为 <a href=\"https://en.wikipedia.org/wiki/Spl_(Unix)\">SPLs</a> ）来决定什么可以中断什么（例如，时钟中断比磁盘中断有更高的优先级），但更重要的是，内核线程可以被其它内核线程抢占。这“当然”是如今每个高性能 Unix 系统所做的，但这并不是许多 Unix 系统最初的样子：最初的内核不支持抢占也不支持多处理；然后添加了对用户空间多处理的支持；最后才添加了内核抢占。后者是所有步骤中最困难的，这解释了 <a href=\"https://en.wikipedia.org/wiki/FreeBSD_version_history#FreeBSD_5\">FreeBSD 5.0</a> 的惨痛经历。因此，看到 NT 从一开始就有正确的基础是很有趣的。</p>\n<h2>对象</h2>\n<p>NT 是面向对象的内核。你可能会认为 Unix 也是：毕竟，进程是由结构体定义的，文件系统实现处理的是 vnode（“虚拟节点”，不要与 inode 混淆，后者是特定于文件系统的实现细节）。但这与 NT 所做的并不完全相同：NT 强制所有这些不同的对象在系统中有一个共同的表示方式。</p>\n<p>你可能会对此持怀疑态度，因为……如何能对进程和文件句柄这样截然不同的东西提供一个有意义的抽象呢？实际上是很难做到的，但 NT 强制所有这些都继承自一个共同的对象类型，令人惊讶的是，这带来了一些不错的特性：</p>\n<ul>\n<li><p><strong>集中式访问控制</strong>：对象只能由 <em>对象管理器</em> 创建，这意味着在代码中只有一个地方来执行策略。这很强大，因为例如权限检查的语义可以只在一个位置定义，并在整个系统中统一应用。NetBSD 也得出这是个好主意的结论，但直到 2001 年才获得其<a href=\"https://man.netbsd.org/NetBSD-9.3/kauth.9\">内核授权（kauth）</a>框架。</p>\n</li>\n<li><p><strong>共同身份</strong>：对象有身份，它们都在一个单一的树中表示。这意味着所有对象都有一个唯一的命名空间，无论我们谈论的是进程、文件句柄还是管道。树中的对象可以通过名称（路径）来寻址，树的不同部分可以由不同的子系统拥有。例如，树的一部分可以代表一个挂载的文件系统，因此遍历该子树的根节点将导致文件系统解析路径的剩余部分。这类似于 Unix 系统的 VFS 层，区别在于 VFS 专门用于文件系统，而对象树是关于每一个内核对象的。诚然，Unix 试图通过 <code>/proc/</code>、<code>/sys/</code> 等将其它类型的非文件对象塞进文件系统，但与 NT 提供的相比，这些感觉像是事后想法。</p>\n</li>\n<li><p><strong>统一的事件处理</strong>：所有对象类型都有一个“信号状态”，其语义对每种对象类型都是特定的。例如，当进程退出时，进程对象进入信号状态；当 I/O 请求完成时，文件句柄对象进入信号状态。这使得在用户空间编写事件驱动代码（咳咳，异步代码）变得很简单，因为单个等待式系统调用可以等待一组对象改变它们的状态 — 无论它们是什么类型。试图在 Unix 系统上同时等待 I/O 和进程完成是非常痛苦的。</p>\n</li>\n</ul>\n<p>对象是 NT 特有的构造，然而它们并不能很好地泛化到 NT 打算支持的所有 API。POSIX 子系统就是一个例子：POSIX 没有与 NT 相同的对象概念，但 NT 必须为 POSIX 应用程序提供某种兼容性。因此，虽然 POSIX 子系统从执行体分配对象，但这个子系统必须保持自己的簿记（译注：bookkeeping）来表示相应的 POSIX 实体，并在运行时进行两者之间的逻辑转换。另一方面，Win32 子系统则直接将对象交给客户端，没有中间层。</p>\n<h2>进程</h2>\n<p>进程在 NT 和 Unix 中都是常见的实体，但它们并不完全相同。在 Unix 中，进程以树状结构表示，这意味着每个进程都有一个父进程，且一个进程可以有零个或多个子进程。然而在 NT 中，并不存在这种关系：进程可以从其创建者“继承”资源 — 基本上是任何类型的对象 — 但在创建后它们就是独立的实体。</p>\n<p>在 NT 设计时，线程并不常见：Mach 在 1985 年成为第一个集成线程的类 Unix 内核，这意味着其它 Unix 系统后来才采用这个概念，并不得不将其改造到现有的设计中。例如，Linux 在 1996 年 6 月发布的 2.0 版本中选择将线程表示为进程，每个线程都有自己的 PID；而 NetBSD 直到 2004 年的 2.0 版本才引入线程，将其表示为与进程分离的实体。与 Unix 相反，NT 从一开始就选择支持线程，因为知道线程是 SMP 机器（译注：多处理器系统）上高性能计算的必要条件。</p>\n<p>NT 在传统 Unix 意义上并没有信号。然而，它确实有 <em>alert</em>（警报），并且可以分为内核态和用户态。用户态警报像其它对象一样必须被等待，而内核态警报对进程是不可见的。POSIX 子系统使用内核态警报来模拟信号。值得注意的是，信号在 Unix 中常被称为一种缺陷，因为它们会干扰进程的执行：正确处理信号是一项非常困难的任务，所以 NT 的替代方案看起来更优雅。</p>\n<p>在 NT 领域中最近一个有趣的发展是引入了 <a href=\"https://www.microsoft.com/en-us/research/project/drawbridge/\">picoprocess（微进程）</a>。在添加这个功能之前，NT 中的进程是相当笨重的：新进程在启动时会将一大堆 NT 运行库映射到它们的地址空间中。在微进程中，进程与 Windows 架构的联系极少，这种设计被用来在 WSL 1 中实现与 Linux 兼容的进程。某种意义上，微进程比原生 Windows 进程更接近 Unix 进程，但由于向 WSL 2 的过渡，微进程目前的应用已经不多了，尽管它们自 2016 年 8 月才存在。</p>\n<p>最后，尽管我们经常批评 Windows 的安全问题，NT 起初在早期互联网标准方面就采用了先进的安全设计，因为该系统基本上是一个基于能力（capability-based）的系统。登录后启动的第一个用户进程会从内核中获得一个访问令牌，代表用户会话的权限，该进程及其子进程必须向内核提供此令牌以断言它们的权限。这与 Unix 不同，Unix 进程只具有标识符，内核需要在进程表中跟踪每个进程的权限。</p>\n<h2>兼容性</h2>\n<p>正如简介中提到的，NT 的一个主要目标是与为旧版 Windows、DOS、OS/2 和 POSIX 编写的应用程序兼容。其中一个原因是技术上的，因为这迫使系统具有优雅的设计；另一个原因是政治上的，因为 NT 是与 IBM 的联合开发，NT 必须支持 OS/2 应用程序，即使 NT 最终成为 Windows。</p>\n<p>这种对兼容性的需求迫使 NT 的设计与 Unix 的设计截然不同。在 Unix 中，用户空间应用程序通过其系统调用接口直接与内核通信，这个接口<em>就是</em>Unix 接口。通常（ <a href=\"https://utcc.utoronto.ca/~cks/space/blog/programming/Go116OpenBSDUsesLibc\">但并非总是</a> ），C 库提供了调用内核的粘合剂（胶水代码），而应用程序本身从不发出系统调用，但这只是一个小细节。</p>\n<p>与此相反，在 NT 中，应用程序<em>不</em>直接与执行体（内核）通信。相反，每个应用程序都与一个特定的受保护子系统通信，而这些子系统是实现 NT 希望与之兼容的各种操作系统的 API 的子系统。这些子系统作为用户空间服务器实现（它们不在 NT “微内核”中）。对 Windows 应用程序的支持来自 Win32 服务器，这很特殊，因为它是唯一一个用户可以直接看到的服务器：它控制控制台程序和 DOS 终端，并且出于性能原因具有某些权限。</p>\n<p>与传统的 Unix 相比，NT 的设计非常不同，因为 BSD 和 Linux 具有宏内核。这些内核通过系统调用接口向用户空间应用程序暴露，使它们可以直接与系统交互。然而，BSD 系统长期以来提供了运行不同<em>二进制文件</em>的支持，所有这些都在宏内核中完成：其工作原理是根据正在运行的二进制文件向用户空间暴露不同的系统调用表，然后将这些“外来的”系统调用翻译成内核可以理解的形式。Linux 也通过 <em><a href=\"https://man7.org/linux/man-pages/man2/personality.2.html\">personality</a></em> 提供了有限的支持。</p>\n<p>尽管 BSD 的方法与 NT 处理其它系统的方式大不相同，WSL 1 的设计却非常相似，并不符合子系统的原始定义。在 WSL 1 中，NT 内核将 Linux 进程标记为微进程（picoprocess），然后向它们暴露不同的系统调用接口。在 NT 内核中，这些特定于 Linux 的系统调用被转换为 NT 操作，并在同一内核中提供，就像 BSD 的 Linux 兼容性一样。唯一的问题是，NT 不是 Unix，它对 Linux 的“模拟”很棘手，而且比 BSD 所能提供的要慢得多。遗憾的是，<a href=\"https://jmmv.dev/2020/11/wsl-lost-potential.html\">WSL 2 失去了这种设计的精髓</a>，转而采用了全面的 VM 设计……</p>\n<p>在结束这一节之前，还有两个有趣的细节：NT 设计的一个目标是允许在单个 shell 中实现子系统之间的无缝 I/O 重定向；子系统通过 <em>port</em> 暴露给应用程序，这些 <em>port</em> 当然是 NT 对象，类似于 Mach 允许进程和服务器之间通信的方式。</p>\n<h2>虚拟内存</h2>\n<p>NT 和 Unix 一样，依赖于内存管理单元（MMU）和分页来提供跨进程的保护和虚拟内存。在用户空间进程中进行分页是一种常见机制，可以使它们拥有比机器上的物理内存更大的地址空间。但是，NT 提前于当代 Unix 系统的一件事是内核本身也可以被分页到磁盘上。显然不是整个内核 —— 如果所有内容都可以分页，那么当解析内核页错误时，可能需要从已分页的文件系统驱动程序中获取代码 —— 但其中的大部分是可以分页的。这在当今并不特别有趣，因为与机器上典型的安装内存相比，内核很小，但在过去，每个字节都是宝贵的，这当然会产生很大的差异。</p>\n<p>此外，虽然我们现在认为虚拟内存和分页的工作方式是理所当然的，但在设计 NT 时，这是一个重要的研究领域。旧的 Unix 实现为文件系统和虚拟内存分别设置了内存缓存，直到 1987 年 <a href=\"https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=a36b01c8fd5071f64b981dd3ffc66a6bce56736d\">SunOS 才实现了统一的虚拟内存架构</a>，以减少这种旧设计的开销。</p>\n<p>相比之下，NT 从一开始就采用了统一的内存架构。你可能会说这很容易做到，因为他们可以看到 Unix 中发现的低效率，并且在 NT 设计开始之前就可以看到 SunOS 实现的解决方案。但是不管怎样，这使得 NT 在当时比许多其它操作系统“更先进”，必须指出的是，其它系统如 NetBSD 直到 2002 年才在 NetBSD 1.6 中实现了 <a href=\"https://www.usenix.org/legacy/publications/library/proceedings/usenix2000/freenix/full_papers/silvers/silvers.pdf\">统一缓冲区缓存（UBC）</a>。</p>\n<p>NT 和 Unix 之间一个有趣的区别是它们如何管理和表示共享内存。在 NT 中，共享内存段是（令人惊讶的）对象，因此受到与任何其它对象相同的访问验证检查的影响。此外，它们可以像任何其它对象一样寻址，因为它们是单个对象树的一部分。与之相反，Unix 中的这个特性是后来添加的：共享内存对象有一个<a href=\"https://man7.org/linux/man-pages/man3/shm_open.3.html\">不同的命名空间</a>。</p>\n<h2>I/O 子系统</h2>\n<p>Unix 的早期版本仅支持一种文件系统。例如，直到 1990 年的 4.3BSD 版本，BSD 才获得了虚拟文件系统 （VFS） 抽象，以支持 UFS 之外的其它文件系统。另一方面，NT 一开始就采用了允许多种文件系统的设计。</p>\n<p>为了支持多种文件系统，内核必须以某种方式公开它们的命名空间。Unix 通过挂载点将文件系统组合在一个文件层次结构下：VFS 层提供了识别哪些节点对应于文件系统根目录的机制，并在遍历路径时将请求重定向到这些文件系统驱动程序。NT 有类似的设计，即使从标准用户界面来看，文件系统看起来像是不连续的驱动器：在内部，执行体将文件系统表示为对象树中的对象，每个对象负责解析路径的其余部分。这些文件系统对象被重新映射为 DOS 驱动器，以便用户空间可以访问它们。而且，你猜怎么着？DOS 驱动器也是一个单独子树下的对象，它将 I/O 重定向到它们引用的文件系统。</p>\n<p>对于文件系统，NT 最终使用了 NTFS。NTFS 在当时是一个非常先进的文件系统，即使我们喜欢因其性能差而抨击它（其实这是 <a href=\"https://www.youtube.com/watch?v=qbKGw8MQ0i8\">一个错误的说法</a>）。NT 的 I/O 子系统与 NTFS 相结合，带来了 64 位寻址、日志记录，甚至是 Unicode 文件名。Linux 直到 1990 年代末才获得了 64 位文件支持，直到 2001 年 ext3 的推出才获得了日志记录。FreeBSD 直到 1998 年才出现了另一种容错机制 <a href=\"https://www.usenix.org/conference/1999-usenix-annual-technical-conference/soft-updates-technique-eliminating-most\">Soft updates</a>。并且 Unix 将文件名表示为 <a href=\"https://blogsystem5.substack.com/p/strings-encodings-nuls-and-bazel\">nul 结尾的字节数组</a>，而非 Unicode。</p>\n<p>NT 在发布时包含的其它功能包括磁盘分割和镜像（我们今天所知道的 RAID）和设备热插拔。鉴于 SunOS 自 1990 年代初以来就确实包括 RAID 支持，这些功能并不是什么新鲜事，但有趣的是，这些功能都是作为 NT 原始设计的一部分考虑的。</p>\n<p>在更高层次上，使 NT 的 I/O 子系统比 Unix 的 I/O 子系统先进得多的是，它的接口是异步的，并且从一开始就是这样。从这方面来看，FreeBSD 直到 1998 年的 FreeBSD 3.0 才支持 <a href=\"https://man7.org/linux/man-pages/man7/aio.7.html\">aio(7)</a>，Linux 也是直到 2002 年的 Linux 2.5 才支持。即使异步 I/O 的支持在 Unix 系统中已经存在了 20 多年，它仍然不是普遍的：很少有人知道这些 API，绝大多数应用程序不使用它们，且它们的性能很差。Linux 的 <a href=\"https://en.wikipedia.org/wiki/Io_uring\">io_uring</a> 是一个相对较新的功能，它改进了异步 I/O，但它一直是安全漏洞的一个重要来源，而且并没有得到广泛使用。</p>\n<h2>联网</h2>\n<p>今天互联网无处不在，但在设计 NT 时，情况并非如此。回顾微软生态系统，DOS 3.1（1987 年）包含了 FAT 文件系统中的文件共享基础，但“操作系统”本身并未提供任何网络功能：一个名为 Microsoft Networks（MS-NET）的单独产品提供了这些功能。Windows 3.0（1990 年）包含了对 NetBIOS 的支持，这允许在本地网络上进行原始的打印机和文件共享，但是对 TCP/IP 的支持却无处可寻。</p>\n<p>相比之下，Unix 简直<em>就是</em>互联网：所有基础互联网协议都是为它编写、用它编写的。因此，在设计 NT 时，考虑到良好的网络支持至关重要，事实上 NT 确实带有网络功能。因此，NT 支持互联网协议和传统的 LAN 协议，这使其在企业环境中领先于 Unix。</p>\n<p>以 NT 的网络域为例。在 Unix 中，网络管理员通常手动跨计算机同步用户帐户，他们可能会使用 X.500 目录协议（1988 年）和 Kerberos（1980 年代）进行用户身份验证，像 SunOS 这样的系统实现了这些技术，但这些技术并不特别简单。相反，NT 从一开始就提供了<em>域</em>，它集成了目录和身份验证功能，我认为这些在公司网络中“赢得了胜利”，因为它们更容易设置，并且内置在系统中。</p>\n<p>同步用户帐户的目标是跨计算机共享资源，主要是文件，当共享时，表示权限的方式很重要。很长一段时间以来，Unix 只为每个文件提供了简单的读/写/执行权限集。另一方面，NT 从一开始就提供了高级 ACL — 这在 Unix 上仍然是一个痛点。尽管 Linux 和 BSD 现在也有 ACL，但它们的接口在系统之间不一致，感觉像是对系统设计的外来附加组件。在 NT 上，ACL 在对象级别工作，这意味着它们在所有内核功能中始终保持一致。</p>\n<p>说到共享文件，我们必须谈论网络文件系统。在 Unix 中，事实上的文件系统是 NFS，而在 NT 上是 SMB。SMB 是从 MS-NET 和 LAN Manager 继承而来的，并通过一个名为 <em>重定向器</em>（redirector） 的组件在内核中实现。本质上，重定向器“只是”另一个文件系统，就像 NFS 在 Unix 上一样，它会捕获文件操作并将其发送到网络上，这使我们可以比较 RPC 系统。</p>\n<p>尽管 protobuf 和 gRPC 可能由于广泛使用而看起来像新颖的想法，但它们是基于旧想法的。在 Unix 上，我们从 1980 年代初就有了 Sun RPC，主要用于支持 NFS。同样，NT 通过自己的 DSL（称为 MIDL）提供了内置的 RPC 支持，用于指定接口定义并为远程过程生成代码，以及实现 RPC 客户端和服务器的自己的工具。</p>\n<p>从堆栈中往下看，Unix 系统从来都不擅长支持任意驱动程序：请记住 Unix 系统通常与特定的机器和供应商耦合。另一方面，NT 旨在成为“任何”机器的操作系统，并由软件公司出售，因此支持他人编写的驱动程序至关重要。因此，NT 配备了网络驱动程序接口规范（NDIS），这是一个轻松支持网卡驱动程序的抽象层。直到今天，制造商提供的驱动程序在 Linux 上依然很少见，这带来了像 <a href=\"https://ndiswrapper.sourceforge.net/wiki/index.php/Main_Page\">ndiswrapper</a> 这样的有趣解决方案，这是 2000 年代初非常流行的一个 shim，可以在 Linux 上重用 Windows 驱动程序的 WiFi 卡。</p>\n<p>最后，NT 和 Unix 之间的另一个区别在于它们对命名管道的实现。在 Unix 中，命名管道是一个本地构造：它们为同一台机器上的两个进程提供了一种机制，使它们可以通过磁盘上的持久文件名相互通信。NT 具有相同的功能，但其命名管道可以通过网络操作。通过将命名管道放在共享文件系统上，不同计算机上的两个应用程序可以相互通信，而无需担心网络细节。</p>\n<h2>用户空间</h2>\n<p>我保证，我们已经接近尾声了。这里仅简单介绍几个用户空间的主题：</p>\n<ul>\n<li><p><strong>配置</strong>：NT 将系统和应用程序配置集中在一个名为“注册表”的数据库中，摆脱了旧的 <code>CONFIG.SYS</code>、<code>AUTOEXEC.BAT</code> 和传统 Windows 使用的无数 INI 文件。这让一些人很生气，但最终，统一的配置接口对每个人都有好处：应用程序更容易编写，因为只有一个基础需要支持，用户更容易调整系统，因为只有一个地方需要查看。</p>\n<p>  另一方面，Unix 仍然受到数十种 DSL 和不一致的文件位置的困扰。每个支持配置文件的程序都有自己编制的语法，知道程序读取的位置很困难，而且并不总是有很好的文档。Linux 生态系统通过 XDG 和 dconf（以前是 GConf）推动了一种更像 NT 的方法，但是……这是一场艰苦的战斗：虽然桌面组件专门使用这些技术，但系统的基础组件可能永远不会采用它们，留下不一致的混乱。</p>\n</li>\n<li><p><strong>国际化</strong>：作为一家已经在全球范围内交付 Windows 3.x 的大公司，微软理解本地化的重要性，并从一开始就使 NT 支持这一功能。相比之下，Unix 直到 1990 年代末才开始支持 UTF ，支持不同语言是通过可选的 <code>gettext</code> 附加组件实现的。</p>\n</li>\n<li><p><strong>C 语言</strong>：FreeBSD 和 NetBSD 等 Unix 系统一直梦想自己创造一种 C 语言方言，以<a href=\"https://cs.rochester.edu/u/jzhou41/papers/freebsd_checkedc.pdf\">更安全地实现内核</a>。然而，除了 Linux 依赖的 GCC 专有扩展，这种设想从未取得过实质性进展。另一方面，微软拥有 C 编译器的特权，因此他们确实在 NT 中使用了这种方法，NT 是用 Microsoft C 编写的。例如，NT 依赖于结构化异常处理（SEH），这是一种添加 try/except 子句以处理软件和硬件异常的功能。我不会说这是一个很大的优点，但这确实是一个区别。</p>\n</li>\n</ul>\n<h2>结论</h2>\n<p>NT 在推出时是一项开创性的技术。正如我上面所介绍的，自 NT 诞生以来，许多我们今天在系统设计中认为理所当然的功能都已经存在，而几乎所有其它 Unix 系统都必须随着时间的推移慢慢获得这些功能。因此，这些功能并不总是与 Unix 的理念无缝集成。</p>\n<p>然而，今天，我不清楚 NT 是否真的比 Linux 或 FreeBSD 等更“先进”。的确，NT 在开始时具有更可靠的设计原则和更多功能，而且比其同时代的操作系统更多，但是现在……差异变得模糊。是的，NT 是先进的，但并不比现代 Unix 先进得多。</p>\n<p>我发现令人失望的是，尽管 NT 已经制定了所有这些可靠的设计原则……UI 中的臃肿不会让设计大放异彩。即使在超级强大的机器上，操作系统的迟缓也是<a href=\"https://jmmv.dev/2023/06/fast-machines-slow-machines.html\">令人痛苦的</a>，甚至可能导致该操作系统的消亡。</p>\n<p>我将为您留下用于撰写本文的书籍，以便您可以了解我的学习之旅。正如您可以想象的那样，我不得不跳过大量有趣的细节，因此这些值得一读：</p>\n<ul>\n<li><a href=\"https://www.amazon.com/Inside-Windows-NT-Helen-Custer/dp/155615481X\">Inside Windows NT, 1st edition</a>.</li>\n<li><a href=\"https://www.amazon.com/Design-Implementation-4-4-Operating-System/dp/0201549794\">The Design and Implementation of the BSD 4.4 operating system</a>.</li>\n</ul>\n<p>如果您想<em>继续</em>我的旅程并真正深入研究<em>现代</em> NT 和 Unix 的每一部分是如何工作的，那么新版是必读的：</p>\n<ul>\n<li><a href=\"https://www.amazon.com/Windows-Internals-Part-architecture-management/dp/0735684189\">Windows Internals, part 1, 7th edition</a>.</li>\n<li><a href=\"https://www.amazon.com/Windows-Internals-Part-2-7th/dp/0135462401\">Windows Internals, part 2, 7th edition</a>.</li>\n<li><a href=\"https://www.amazon.com/Design-Implementation-FreeBSD-Operating-System/dp/0321968972\">The Design and Implementation of the FreeBSD operating system, 2nd edition</a>.</li>\n</ul>\n","tags":["os"]},{"id":"go-work-stealing-scheduler","url":"https://yieldray.fun/posts/go-work-stealing-scheduler","title":"Go's work-stealing scheduler","date_published":"2024-09-16T22:45:46.000Z","date_modified":"2024-09-16T22:45:46.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://rakyll.org/scheduler/\">https://rakyll.org/scheduler/</a></p>\n</blockquote>\n<p>Go 调度器的职责是在运行在一个或多个处理器上的多个工作 OS 线程之间分配可运行的 goroutine。在多线程计算中，出现了两种调度范式：<strong>工作共享</strong>和<strong>工作窃取</strong>。</p>\n<ul>\n<li><strong>工作共享</strong>：当一个处理器生成新的线程时，它会尝试将其中一些线程迁移到其他处理器，希望它们能够被闲置或未充分利用的处理器利用。</li>\n<li><strong>工作窃取</strong>：未充分利用的处理器会主动寻找其他处理器的线程并“窃取”一些。</li>\n</ul>\n<p>与工作共享相比，工作窃取的线程迁移频率更低。当所有处理器都有工作要运行时，就不会进行线程迁移。一旦出现空闲处理器，就会考虑进行迁移。</p>\n<p>自 1.1 版本以来，Go 就采用了由 Dmitry Vyukov 贡献的工作窃取调度器。本文将深入解释什么是工作窃取调度器，以及 Go 是如何实现它的。</p>\n<h2>调度基础</h2>\n<p>Go 有一个 M:N 调度器，它也可以利用多个处理器。任何时候，都需要在最多 GOMAXPROCS 个处理器上运行的 N 个操作系统线程上调度 M 个 goroutine。Go 调度器对 goroutine、线程和处理器使用以下术语：</p>\n<ul>\n<li>G：goroutine</li>\n<li>M：操作系统线程（机器）</li>\n<li>P：处理器</li>\n</ul>\n<p>每个 P 都有一个本地 goroutine 队列和一个全局 goroutine 队列。每个 M 都应该分配给一个 P。如果 P 被阻塞或处于系统调用中，则它可能没有 M。任何时候，P 的数量最多为 GOMAXPROCS 个。任何时候，每个 P 上只能运行一个 M。如果需要，调度器可以创建更多 M。</p>\n<p><img src=\"https://s2.loli.net/2024/09/16/fzDNovlcSgQE467.png\" alt=\"调度器基础\"></p>\n<p>每一轮调度只是找到一个可运行的 goroutine 并执行它。在每一轮调度中，搜索按以下顺序进行：</p>\n<pre><code class=\"language-go\">runtime.schedule() {\n    // 只有 1/61 的时间会检查全局可运行队列中是否有 G。\n    // 如果没有找到，则检查本地队列。\n    // 如果还没有找到，\n    //     尝试从其他 P 窃取。\n    //     如果仍然没有找到，则再次检查全局可运行队列。\n    //     如果还是没有找到，则轮询网络。\n}\n</code></pre>\n<p>一旦找到可运行的 G，就会一直执行它，直到它被阻塞。</p>\n<p>注意：看起来全局队列比本地队列更有优势，但偶尔检查全局队列对于避免 M 只从本地队列调度直到没有本地排队的 goroutine 非常重要。</p>\n<h2>窃取</h2>\n<p>当创建一个新的 G 或一个现有的 G 变为可运行状态时，它会被推送到当前 P 的可运行 goroutine 列表中。当 P 完成执行 G 后，它会尝试从自己的可运行 goroutine 列表中弹出一个 G。如果列表现在为空，P 会随机选择另一个处理器 (P)，并尝试从其队列中窃取一半的可运行 goroutine。</p>\n<p><img src=\"https://s2.loli.net/2024/09/16/F9ARXBgMnbf15Uj.png\" alt=\"P2 从 P1 窃取\"></p>\n<p>在上面的例子中，P2 找不到任何可运行的 goroutine。因此，它随机选择另一个处理器 (P1)，并将三个 goroutine 窃取到自己的本地队列中。P2 将能够运行这些 goroutine，并且调度器的工作将在多个处理器之间更公平地分配。</p>\n<h2>自旋线程</h2>\n<p>调度器总是希望将尽可能多的可运行 goroutine 分配给 M，以充分利用处理器，但与此同时，我们需要停放过多的工作以节省 CPU 和电源。与此相反，调度器也应该能够扩展到高吞吐量和 CPU 密集型程序。</p>\n<p>持续的抢占既昂贵，如果性能至关重要，对高吞吐量程序来说也是一个问题。操作系统线程不应该频繁地在彼此之间传递可运行的 goroutine，因为这会导致延迟增加。此外，在存在系统调用的情况下，操作系统线程需要不断地被阻塞和解除阻塞。这是很昂贵的，并且会增加很多开销。</p>\n<blockquote>\n<p>译注：</p>\n<ul>\n<li><strong>注重速度:</strong> 如果目标是快速响应（考虑占用），那么低延迟更重要。</li>\n<li><strong>注重效率:</strong> 如果目标是完成尽可能多的工作，那么高吞吐量更重要。</li>\n</ul>\n</blockquote>\n<p>为了最大限度地减少传递，Go 调度器实现了“自旋线程”。自旋线程会消耗一点额外的 CPU 资源，但它们最大限度地减少了操作系统线程的抢占。在以下情况下，线程处于自旋状态：</p>\n<ul>\n<li>具有 P 分配的 M 正在寻找可运行的 goroutine。</li>\n<li>没有 P 分配的 M 正在寻找可用的 P。</li>\n<li>如果有空闲的 P 并且没有其他自旋线程，调度器还会在准备 goroutine 时解除对另一个线程的阻塞，并使其自旋。</li>\n</ul>\n<p>任何时候最多只能有 GOMAXPROCS 个自旋的 M。当一个自旋线程找到工作时，它就会退出自旋状态。</p>\n<p>如果有没有 P 分配的空闲 M，则具有 P 分配的空闲线程不会阻塞。当创建新的 goroutine 或 M 被阻塞时，调度器会确保至少有一个自旋的 M。这确保了没有可以运行的可运行 goroutine；并避免了过多的 M 阻塞/解除阻塞。</p>\n<h2>结论</h2>\n<p>Go 调度器通过窃取将它们调度到正确和未充分利用的处理器，以及实现“自旋”线程以避免频繁发生阻塞/解除阻塞转换，从而做了很多工作来避免过度抢占操作系统线程。</p>\n<p>调度事件可以通过 <a href=\"https://golang.org/cmd/trace/\">执行跟踪器</a> 进行跟踪。如果你碰巧认为你的处理器利用率很低，你可以调查一下发生了什么。</p>\n<h3>References</h3>\n<ul>\n<li><a href=\"https://github.com/golang/go/blob/master/src/runtime/proc.go\">The Go runtime scheduler source</a></li>\n<li><a href=\"https://golang.org/s/go11sched\">Scalable Go Scheduler design document</a></li>\n<li><a href=\"https://morsmachine.dk/go-scheduler\">The Go scheduler by Daniel Morsing</a></li>\n</ul>\n<p><a href=\"https://goguide.ryansu.tech/guide/concepts/golang/20-gmp.html\">https://goguide.ryansu.tech/guide/concepts/golang/20-gmp.html</a></p>\n","tags":["go"]},{"id":"coroutine-and-fiber","url":"https://yieldray.fun/posts/coroutine-and-fiber","title":"协程与纤程","date_published":"2024-09-16T14:09:26.000Z","date_modified":"2024-09-16T14:09:26.000Z","content_text":"<h1>概念</h1>\n<p><strong>纤程 (Fiber)</strong> 和 <strong>协程 (Coroutine)</strong> 可用于实现并发编程（这又可实现异步 I/O）。它们之间的共同点在于：</p>\n<ul>\n<li><strong>轻量级</strong>: 与操作系统线程相比，纤程和协程更加轻量级，创建和切换的开销更低。</li>\n<li><strong>用户空间</strong>: 纤程和协程的调度发生在用户空间，而非内核空间，减少了系统调用的开销。</li>\n<li><strong>并发性</strong>: 虽然不是真正的并行，但纤程和协程可以通过交替执行任务来模拟并发执行效果。</li>\n</ul>\n<p><strong>区别:</strong></p>\n<p><strong>调度控制:</strong></p>\n<blockquote>\n<p>注：由于这里是概念性叙述，不太能对底层堆栈加以描述</p>\n</blockquote>\n<ul>\n<li><strong>纤程:</strong> 概念上来说是一种<strong>轻量级（用户态）线程</strong>。因此调度完全由程序员控制。需显式指定何时挂起和恢复纤程。<br> 纤程重点解决线程的问题。传统线程往往用于执行单个 I/O 操作，这时阻塞会导致整个线程挂起。<br> 线程是时间片的最小单位。通过将多个纤程派发给单个（或多个）线程，由纤程执行 I/O 操作，能够提高线程时间片利用率。</li>\n<li><strong>协程:</strong> 调度由程序员和编程语言的运行时系统共同控制。协程可以在特定操作（如 I/O 操作）阻塞时自动挂起，并在操作完成后恢复，无需程序员显式指定。<br> 这种自动挂起显然不是用户控制的，而是低层库提供。但也可手动挂起（主动让出控制流）和恢复来控制执行的流程。<br> 其优势在于避免进程可能的的竞争态，因此无需关心是否是单线程还是多线程。</li>\n</ul>\n<blockquote>\n<p>协程可能更接近异步编程，因为无需（一般也不允许手动）管理上下文（切换），低层库提供的 API 背后可能使用多个线程（比如线程池），也可能包装了操作系统提供的非阻塞 I/O 接口<br>异步很大程度上是通过事件驱动，而这又可以基于状态机。协程天然带有这种状态转移的特性。</p>\n</blockquote>\n<p><strong>实现方式:</strong></p>\n<ul>\n<li><strong>纤程:</strong> 通常在用户空间实现，作为更轻量的线程替代方案。</li>\n<li><strong>协程:</strong> 可以是基于栈式 (stackful) 或非栈式 (stackless) 的。<ul>\n<li><p><strong>栈式协程:</strong> 拥有自己的调用栈，类似于轻量级线程，可以在函数调用之间保存状态。</p>\n</li>\n<li><p><strong>非栈式协程:</strong> 没有自己的调用栈，状态保存在编译器生成的协程对象中。</p>\n</li>\n<li><p><strong>非对称纤程：</strong> 纤程只能将控制权让给调用方，因为纤程需主动让出控制权。</p>\n</li>\n<li><p><strong>对称纤程：</strong> 纤程启动后，调用方获得控制器，用于控制纤程的暂停和恢复。</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>一句话来说，纤程在于轻量化线程，而协程在于子程序的自动调度。</p>\n<p>参考：<br><a href=\"https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf\">https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf</a></p>\n<h1><a href=\"https://go.dev/tour/concurrency/1\">Goroutine</a></h1>\n<p><a href=\"https://go.dev/doc/effective_go#goroutines\">Goroutine</a> 的模型可能类似于纤程，但 goroutine 栈（创建，销毁和自动增长）和 goroutine 切换是 runtime 自动调度的。</p>\n<blockquote>\n<p>线程栈需要手动控制：<a href=\"https://www.man7.org/linux/man-pages/man3/pthread_attr_setstack.3.html\">pthread_attr_setstack</a></p>\n</blockquote>\n<p>其特点在于没有共享内存，个人认为其通过信道通信的方式很大程度上模拟了协程的挂起和恢复。</p>\n<h1>以 C/C++ 为例</h1>\n<p><a href=\"https://www.man7.org/linux/man-pages/man2/getcontext.2.html\">ucontext</a> 库提供用户进程内部的 context 控制，可视为更先进的 setjmp/longjmp。（<a href=\"https://zyy.rs/post/ucontext-usage-and-coroutine/\">参考</a>）</p>\n<pre><code class=\"language-c\">// in ucontext.h\n\n/* Userlevel context.  */\ntypedef struct ucontext_t\n  {\n    unsigned long __ctx(uc_flags);\n    struct ucontext_t *uc_link;     // uc_link指向当前context 结束时待恢复的上下文\n    stack_t uc_stack;               // context 使用的栈\n    sigset_t uc_sigmask;            // context 要阻塞的信号合集\n    mcontext_t uc_mcontext;         // 机器特定的 context 表示\n  } ucontext_t;\n</code></pre>\n<pre><code class=\"language-cpp\">#include &lt;ucontext.h&gt;\n#include &lt;iostream&gt;\n\nucontext_t mainContext, fiberContext;\n\nvoid FiberFunc() {\n  for (int i = 0; i &lt; 5; ++i) {\n    std::cout &lt;&lt; &quot;Fiber: &quot; &lt;&lt; i &lt;&lt; std::endl;\n    swapcontext(&amp;fiberContext, &amp;mainContext); // 切换回主上下文\n  }\n}\n\nint main() {\n  // 初始化纤程上下文\n  getcontext(&amp;fiberContext);\n  fiberContext.uc_stack.ss_sp = malloc(64 * 1024);\n  fiberContext.uc_stack.ss_size = 64 * 1024;\n  fiberContext.uc_link = &amp;mainContext;\n  makecontext(&amp;fiberContext, FiberFunc, 0);\n\n  // 切换到纤程上下文\n  swapcontext(&amp;mainContext, &amp;fiberContext);\n\n  // 主上下文逻辑\n  for (int i = 0; i &lt; 3; ++i) {\n    std::cout &lt;&lt; &quot;Main: &quot; &lt;&lt; i &lt;&lt; std::endl;\n    swapcontext(&amp;mainContext, &amp;fiberContext);\n  }\n\n  // 释放内存\n  free(fiberContext.uc_stack.ss_sp);\n\n  return 0;\n}\n</code></pre>\n<p>Win32 提供 <a href=\"https://learn.microsoft.com/en-us/windows/win32/procthread/fibers\">Fiber API</a></p>\n<pre><code class=\"language-cpp\">#include &lt;Windows.h&gt;\n#include &lt;iostream&gt;\n\nvoid FiberProc(void* parameter) {\n  for (int i = 0; i &lt; 5; ++i) {\n    std::cout &lt;&lt; &quot;Fiber: &quot; &lt;&lt; i &lt;&lt; std::endl;\n    SwitchToFiber(parameter); // 切换回主纤程\n  }\n}\n\nint main() {\n  // 创建纤程栈\n  void* fiberStack = VirtualAlloc(nullptr, 64 * 1024, MEM_COMMIT, PAGE_READWRITE);\n\n  // 创建纤程\n  void* fiber = CreateFiber(0, FiberProc, nullptr);\n\n  // 切换到纤程\n  SwitchToFiber(fiber);\n\n  // 主纤程逻辑\n  for (int i = 0; i &lt; 3; ++i) {\n    std::cout &lt;&lt; &quot;Main: &quot; &lt;&lt; i &lt;&lt; std::endl;\n    SwitchToFiber(fiber);\n  }\n\n  // 清理纤程\n  DeleteFiber(fiber);\n  VirtualFree(fiberStack, 0, MEM_RELEASE);\n\n  return 0;\n}\n</code></pre>\n<p>Boost.Fiber</p>\n<pre><code class=\"language-cpp\">#include &lt;boost/fiber/all.hpp&gt;\n#include &lt;iostream&gt;\n\nvoid fiber_function() {\n  for (int i = 0; i &lt; 5; ++i) {\n    std::cout &lt;&lt; &quot;Fiber: &quot; &lt;&lt; i &lt;&lt; std::endl;\n    boost::this_fiber::yield(); // 切换到其他纤程\n  }\n}\n\nint main() {\n  boost::fibers::fiber f(fiber_function);\n  for (int i = 0; i &lt; 3; ++i) {\n    std::cout &lt;&lt; &quot;Main: &quot; &lt;&lt; i &lt;&lt; std::endl;\n    f.join(); // 等待纤程 f 结束\n  }\n  return 0;\n}\n</code></pre>\n<p>C++20 Coroutines / Boost.Coroutine</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;coroutine&gt;\n\nstruct Task {\n  struct promise_type {\n    std::suspend_always initial_suspend() { return {}; }\n    std::suspend_always final_suspend() noexcept { return {}; }\n    void return_void() {}\n    void unhandled_exception() {}\n    Task get_return_object() { return {}; }\n  };\n};\n\nTask foo() {\n    std::cout &lt;&lt; &quot;foo() begin&quot; &lt;&lt; std::endl;\n    co_await std::suspend_always{};\n    std::cout &lt;&lt; &quot;foo() end&quot; &lt;&lt; std::endl;\n}\n\nint main() {\n    auto f = foo();\n    std::cout &lt;&lt; &quot;main() continues&quot; &lt;&lt; std::endl;\n    return 0;\n}\n</code></pre>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;coroutine&gt;\n\nstruct CountingCoroutine {\n    struct promise_type {\n        int current_value = 0;\n        CountingCoroutine get_return_object() { return CountingCoroutine(std::coroutine_handle&lt;promise_type&gt;::from_promise(*this));  }\n        std::suspend_always initial_suspend() { return {}; }\n        std::suspend_always final_suspend() noexcept { return {}; }\n        void return_void() {}\n        void unhandled_exception() {}\n        std::suspend_always yield_value(int value) {\n            current_value = value;\n            return {};\n        }\n    };\n    std::coroutine_handle&lt;promise_type&gt; coroutine;\n    CountingCoroutine(std::coroutine_handle&lt;promise_type&gt; handle) : coroutine(handle) {}\n    ~CountingCoroutine() {\n        if (coroutine)\n            coroutine.destroy();\n    }\n    int getValue() const {\n        return coroutine.promise().current_value;\n    }\n    void resume() {\n        coroutine.resume();\n    }\n};\n\nCountingCoroutine generateNumbers() {\n    for (int i = 0; i &lt; 10; ++i) {\n        //移交控制权\n        co_yield i;\n    }\n}\n\nint main() {\n    CountingCoroutine counter = generateNumbers();\n    while (counter.coroutine) {\n        std::cout &lt;&lt; &quot;Value: &quot; &lt;&lt; counter.getValue() &lt;&lt; std::endl;\n        //回到 co_yield 继续执行\n        counter.resume();\n    }\n    return 0;\n}\n</code></pre>\n","tags":["os"]},{"id":"web-event-interface","url":"https://yieldray.fun/posts/web-event-interface","title":"Web事件接口","date_published":"2024-09-15T00:15:52.000Z","date_modified":"2024-09-15T00:15:52.000Z","content_text":"<h1>原理</h1>\n<p>事件机制为发布订阅模式，属于<a href=\"https://refactoringguru.cn/design-patterns/observer\">观察者模式</a>的变种。<br><strong>观察者模式</strong>，简单来说，存在两类：目标（发布者）和观察者（订阅者），目标持有其所有<em>观察者的引用</em>和<em>状态</em>，观察者提供一个接受目标的接口（以消费目标的状态）。<br><strong>发布订阅模式</strong>在目标和观察者之间引入了一个<em>消息代理 (Message Broker)<em>或</em>事件总线 (Event Bus)</em>（事件=消息）。事件总线负责接收和分发事件，发布和订阅者双方都完全<em>不需要知道对方的存在</em>。</p>\n<h1>库实现</h1>\n<p>这里展示几个轻量级的库作为演示</p>\n<h2><a href=\"https://github.com/ai/nanoevents/blob/main/index.js\">nanoevents</a></h2>\n<blockquote>\n<p>源码中包含 <a href=\"https://github.com/ai/nanoevents/blob/main/index.d.ts\">d.ts</a> 类型</p>\n</blockquote>\n<p>nanoevents 库只能作为一个实现示例，但我个人不建议在生产中使用它。</p>\n<pre><code class=\"language-js\">//prettier-ignore\nexport let createNanoEvents = () =&gt; ({\n  emit(event, ...args) {\n    for (\n      let i = 0,\n        callbacks = this.events[event] || [],\n        length = callbacks.length;\n      i &lt; length;\n      i++\n    ) {\n      callbacks[i](...args)\n    }\n  },\n  events: {},\n  on(event, cb) {\n    ;(this.events[event] ||= []).push(cb)\n    return () =&gt; {\n      this.events[event] = this.events[event]?.filter(i =&gt; cb !== i)\n    }\n  }\n})\n</code></pre>\n<h2><a href=\"https://github.com/developit/mitt/blob/main/src/index.ts\">mitt</a></h2>\n<blockquote>\n<p>源码为 ts 实现，为缩短篇幅转换为 js</p>\n</blockquote>\n<p>mitt 库支持通配符，类型支持完善，适用于普通场景。</p>\n<pre><code class=\"language-ts\">export default function mitt(all) {\n    all = all || new Map();\n    return {\n        all,\n        on(type, handler) {\n            const handlers = all.get(type);\n            if (handlers) {\n                handlers.push(handler);\n            } else {\n                all.set(type, [handler]);\n            }\n        },\n\n        off(type, handler) {\n            const handlers = all.get(type);\n            if (handlers) {\n                if (handler) {\n                    // n &gt;&gt;&gt; 0 这里的有符号右移 0 位对非负数肯定是无影响的\n                    // 对于负数，这里只可能是 -1\n                    // -1 &gt;&gt;&gt; 0 = 4294967295\n                    handlers.splice(handlers.indexOf(handler) &gt;&gt;&gt; 0, 1);\n                } else {\n                    all.set(type, []);\n                }\n            }\n        },\n\n        emit(type, evt) {\n            let handlers = all.get(type);\n            if (handlers) {\n                // .slice() 防止直接在原数组上迭代\n                // 处理在事件处理过程中修改 handlers 数组情况\n                handlers.slice().map((handler) =&gt; {\n                    handler(evt);\n                });\n            }\n            handlers = all.get(&quot;*&quot;);\n            if (handlers) {\n                handlers.slice().map((handler) =&gt; {\n                    handler(type, evt);\n                });\n            }\n        },\n    };\n}\n</code></pre>\n<h1>Web Events</h1>\n<p>可以看出，实现基础的发布订阅机制非常简单</p>\n<p>核心在于两个基类 <a href=\"https://developer.mozilla.org/docs/Web/API/EventTarget\">EventTarget</a> <a href=\"https://developer.mozilla.org/docs/Web/API/Event\">Event</a><br>注意它们曾经只是接口，但现在（在现代浏览器中）可直接通过 new 构造，特别是 Event 的子类<br>为了区分性，提供一个 <a href=\"https://developer.mozilla.org/docs/Web/API/CustomEvent\">CustomEvent</a>，以表示任意应用程序事件</p>\n<blockquote>\n<p>WHATWG Reference<br><a href=\"https://dom.spec.whatwg.org/#interface-event\">Event</a> <a href=\"https://dom.spec.whatwg.org/#eventtarget\">EventTarget</a></p>\n</blockquote>\n<pre><code class=\"language-ts\">class EventTarget {\n    /**\n     * 为类型属性值为 `type` 的事件添加事件监听器。`callback` 参数设置事件分发时将被调用的回调函数。\n     *\n     * `options` 参数设置监听器特定的选项。为了兼容性，它可以是一个布尔值，在这种情况下，方法的行为与将值指定为 `options` 的 `capture` 相同。\n     *\n     * `options` 的 `capture` 为 `true` 表示阻止事件的 `eventPhase` 属性值为 `BUBBLING_PHASE` 时调用 `callback`。\n     * 为 `false`（或不存在）时，`callback` 不会在事件的 `eventPhase` 属性值为 `CAPTURING_PHASE` 时被调用。\n     * 无论哪种方式，如果事件的 `eventPhase` 属性值为 `AT_TARGET`，`callback` 将被调用。\n     *\n     * `options` 的 `passive` 为 `true` 表示回调函数不会通过调用 `preventDefault()` 来取消事件。这是用于启用 [§ 2.8 观察事件监听器](https://dom.spec.whatwg.org/#observing-event-listeners)中描述的性能优化。\n     *\n     * `options` 的 `once` 为 `true` 表示回调函数只会被调用一次，之后事件监听器就会被移除。\n     *\n     * 如果为 `options` 的 `signal` 传递了 `AbortSignal`，则当 `signal` 被中止时，事件监听器将被移除。\n     *\n     * 事件监听器被追加到 `target` 的事件监听器列表中，如果它具有相同的类型、回调函数和捕获方式，则不会被追加。\n     */\n    addEventListener(\n        type: string,\n        callback: EventListenerOrEventListenerObject | null,\n        options?: AddEventListenerOptions | boolean,\n    ): void;\n    /**\n     * 将合成事件 `event` 分发到 `target`，如果事件的 `cancelable` 属性值为 `false` 或其 `preventDefault()` 方法没有被调用，则返回 `true`，否则返回 `false`。\n     */\n    dispatchEvent(event: Event): boolean;\n    /**\n     * 从 `target` 的事件监听器列表中移除具有相同类型、回调函数和选项的事件监听器。\n     */\n    removeEventListener(\n        type: string,\n        callback: EventListenerOrEventListenerObject | null,\n        options?: EventListenerOptions | boolean,\n    ): void;\n}\n\ntype EventListenerOrEventListenerObject = EventListener | EventListenerObject;\ninterface EventListener {\n    (evt: Event): void;\n}\ninterface EventListenerObject {\n    handleEvent(object: Event): void;\n}\ninterface EventListenerOptions {\n    capture?: boolean;\n}\n</code></pre>\n<p>与事件相关的 Web API 基本都使用它们，但最具代表性的还是 <a href=\"https://dom.spec.whatwg.org/#events\">DOM Events</a>。</p>\n<p>观察 <code>Event</code> 基类，事件具有 eventPhase 属性，可表示事件处于捕获或冒泡阶段（NONE/CAPTURING_PHASE/AT_TARGET/BUBBLING_PHASE）<br>这是为了实现事件委托机制：DOM 事件大部分都是先<strong>捕获阶段</strong>，然后才是<strong>目标阶段</strong>，最后是<strong>冒泡阶段</strong>。</p>\n<p>我们只需考虑反例即可：</p>\n<blockquote>\n<ul>\n<li><strong>一些较老的，非标准化的事件：</strong> 例如 <code>focus</code>, <code>blur</code>, <code>change</code> 等，这些事件在某些浏览器中可能不支持捕获阶段，或者行为不一致。</li>\n<li><strong>一些特定类型的事件：</strong><ul>\n<li><code>load</code>: 该事件在页面加载完成后触发，只会在目标阶段触发一次，没有捕获和冒泡阶段。</li>\n<li><code>unload</code>: 该事件在页面卸载时触发，类似于 <code>load</code> 事件，也只会在目标阶段触发一次。</li>\n<li><code>scroll</code>: 该事件在滚动条滚动时触发，通常只在目标阶段触发。</li>\n<li><code>resize</code>: 该事件在窗口或元素大小发生改变时触发，通常只在目标阶段触发。</li>\n</ul>\n</li>\n<li><strong>直接在元素上触发的鼠标事件：</strong> 例如使用 <code>element.click()</code> 触发的 <code>click</code> 事件，只会执行目标阶段和冒泡阶段，没有捕获阶段。</li>\n<li><strong>自定义事件：</strong> 默认情况下，使用 <code>dispatchEvent</code> 触发的自定义事件<strong>只可能有冒泡阶段</strong>。<br>可以通过设置 <code>Event</code> 构造函数的 <code>bubbles</code> 参数为 <code>true</code> 来启用冒泡，但是无法启用捕获阶段。</li>\n</ul>\n</blockquote>\n<p>根据 DOM 规范，每个 EventTarget 对象都关联一个 <a href=\"https://dom.spec.whatwg.org/#get-the-parent\">get the parent</a> 算法，接受一个 Event 并返回 EventTarget。<br>显然，<a href=\"https://dom.spec.whatwg.org/#node\">DOM 节点（Node）</a> 等接口覆写了该算法。<br>而手动构造 EventTarget 的 get the parent 算法返回 null，并且不关联 <a href=\"https://dom.spec.whatwg.org/#concept-event-dispatch\">activation</a> 行为。<br>DOM 是树形结构，因此事件会参与这种树形结构。目前手动构造的 EventTarget 不参与树形结构，但规范也提到未来可能会允许自定义 get the parent 算法。</p>\n<pre><code class=\"language-ts\">interface EventInit {\n    bubbles?: boolean;\n    cancelable?: boolean;\n    composed?: boolean;\n}\n\nclass Event {\n    constructor(type: string, eventInitDict?: EventInit);\n    /**\n     * 返回 true 或 false，具体取决于事件的初始化方式。如果事件按照逆向树序遍历其目标的祖先，则为 true，否则为 false。\n     */\n    readonly bubbles: boolean;\n    /**\n     * @deprecated\n     */\n    cancelBubble: boolean;\n    /**\n     * 返回 true 或 false，具体取决于事件的初始化方式。其返回值并不总是具有意义，但 true 可能表明事件分发过程中的一部分操作可以通过调用 preventDefault() 方法取消。\n     */\n    readonly cancelable: boolean;\n    /**\n     * 返回 true 或 false，具体取决于事件的初始化方式。如果事件调用其目标根节点的 ShadowRoot 节点以外的监听器，则为 true，否则为 false。\n     */\n    readonly composed: boolean;\n    /**\n     * 返回当前正在调用其事件监听器回调的函数的对象。\n     */\n    readonly currentTarget: EventTarget | null;\n    /**\n     * 如果成功调用 preventDefault() 以指示取消，则返回 true，否则返回 false。\n     */\n    readonly defaultPrevented: boolean;\n    /**\n     * 返回事件的阶段，可以是 NONE、CAPTURING_PHASE、AT_TARGET 和 BUBBLING_PHASE 之一。\n     */\n    readonly eventPhase: number;\n    /**\n     * 如果事件是由用户代理分发的，则返回 true，否则返回 false。\n     */\n    readonly isTrusted: boolean;\n    /**\n     * @deprecated\n     */\n    returnValue: boolean;\n    /**\n     * @deprecated\n     */\n    readonly srcElement: EventTarget | null;\n    /**\n     * 返回事件分发到的对象（其目标）。\n     */\n    readonly target: EventTarget | null;\n    /**\n     * 返回事件的时间戳，以相对于时间原点的毫秒数表示。\n     */\n    readonly timeStamp: DOMHighResTimeStamp;\n    /**\n     * 返回事件的类型，例如 &quot;click&quot;、&quot;hashchange&quot; 或 &quot;submit&quot;。\n     */\n    readonly type: string;\n    /**\n     * 返回事件路径的调用目标对象（将调用监听器的对象），除了 shadow 树中 shadow 根节点模式为 &quot;closed&quot; 的任何节点，这些节点无法从事件的 currentTarget 访问。\n     */\n    composedPath(): EventTarget[];\n    /**\n     * @deprecated\n     */\n    initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void;\n    /**\n     * 如果在 cancelable 属性值为 true 时调用，并且在执行事件的监听器时 passive 设置为 false，则向导致事件分发操作发出信号，表明需要取消该操作。\n     */\n    preventDefault(): void;\n    /**\n     * 调用此方法可阻止事件到达当前事件运行完成后注册的任何事件监听器，并且当在树中分发时，还可阻止事件到达任何其他对象。\n     */\n    stopImmediatePropagation(): void;\n    /**\n     * 当在树中分发时，调用此方法可阻止事件到达除当前对象以外的任何对象。\n     */\n    stopPropagation(): void;\n    readonly NONE: 0;\n    readonly CAPTURING_PHASE: 1;\n    readonly AT_TARGET: 2;\n    readonly BUBBLING_PHASE: 3;\n}\n</code></pre>\n<p>当通过编程方式触发 DOM 事件时，事件处理器是<strong>同步</strong>被调用的：</p>\n<pre><code class=\"language-js\">const et = new EventTarget();\net.addEventListener(&quot;test&quot;, console.log);\net.dispatchEvent(new Event(&quot;test&quot;));\net.dispatchEvent(new CustomEvent(&quot;test&quot;, { detail: &quot;detail&quot; }));\nconsole.log(&quot;main_thread&quot;);\n// Event -&gt; CustomEvent -&gt; main_thread\n\nconst el = document.createElement(&quot;a&quot;);\nel.addEventListener(&quot;click&quot;, console.log);\nel.click();\nel.dispatchEvent(new PointerEvent(&quot;click&quot;));\nconsole.log(&quot;main_thread&quot;);\n// PointerEvent -&gt; PointerEvent -&gt; main_thread\n\nel.addEventListener(&quot;test&quot;, console.log);\nel.dispatchEvent(new Event(&quot;test&quot;));\n// 可以监听和派发任意事件\n</code></pre>\n<h1><a href=\"https://nodejs.org/api/events.html\">Node.js events</a></h1>\n<h2><a href=\"https://nodejs.org/api/events.html#eventtarget-and-event-api\">Event Web API</a></h2>\n<p><a href=\"https://nodejs.org/api/events.html#eventtarget-and-event-api\">Node.js</a> 也实现了 Web EventTarget 和 Event API，毕竟存在 Node.js 实现的其它 Web API 依赖于此。</p>\n<p>关于 Node.js EventTarget 与 DOM EventTarget 的区别，参见<a href=\"https://nodejs.org/api/events.html#nodejs-eventtarget-vs-dom-eventtarget\">官方文档</a>即可。</p>\n<h2>EventEmitter</h2>\n<p>本文的主题是 Web Events，因此会省略 <a href=\"https://nodejs.org/api/events.html#events\">Node.js 的事件机制</a>。</p>\n<p>Node.js 的 events 模块提供 EventEmitters 类对应于 EventTarget，但没有 Event 对应类。</p>\n","tags":["web-api","dom"]},{"id":"sec-headers","url":"https://yieldray.fun/posts/sec-headers","title":"Sec Headers","date_published":"2024-09-11T23:54:47.000Z","date_modified":"2024-09-11T23:54:47.000Z","content_text":"<h1>Overview</h1>\n<p>Sec = <strong>Sec</strong>urity</p>\n<h1>Sec-Fetch-*</h1>\n<p><a href=\"https://www.w3.org/TR/fetch-metadata/\">Specs(Fetch Metadata Request Headers)</a> <a href=\"https://developer.mozilla.org/docs/Glossary/Fetch_metadata_request_header\">MDN Reference</a> <a href=\"https://web.dev/articles/fetch-metadata\">Web.dev Article</a></p>\n<h1>Sec-CH-*</h1>\n<p>CH = Client Hints</p>\n<p><a href=\"https://wicg.github.io/ua-client-hints/\">Specs(User-Agent Client Hints)</a> <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints\">MDN Reference</a> <a href=\"https://developer.chrome.com/docs/privacy-security/user-agent-client-hints\">Chrome Dev Blog</a></p>\n","tags":["http","web-api"]},{"id":"js-equality","url":"https://yieldray.fun/posts/js-equality","title":"ECMA262比较操作","date_published":"2024-09-10T00:00:00.000Z","date_modified":"2024-11-03T15:00:00.000Z","content_text":"<blockquote>\n<p>截止编写日期，下面引用来源为：ECMAScript® 2025 Language Specification</p>\n</blockquote>\n<h1><a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-equality-operators\">等式运算符</a></h1>\n<p>等式运算符作为二元运算符，但注意等式两边<strong>不满足</strong>等价关系。因为其满足自反性、对称性，但<strong>不满足</strong>传递性。</p>\n<p>根据 ECMA262 给出的 4 个注释，我们可以再加以解释：</p>\n<ol>\n<li>以下命题为真<br>凭此可强制使用字符串比较：<code>`${a}` == `${b}`</code><br>凭此可强制使用数值比较：<code>+a == +b</code><br>凭此可强制使用布尔值比较： <code>!a == !b</code></li>\n<li>以下命题为真<br><code>A != B</code> 等价于 <code>!(A == B)</code><br><code>A == B</code> 等价于 <code>B == A</code> ，当然， A 与 B 的执行顺序除外</li>\n<li>不满足传递性是因为存在特例，例如<br><code>new String(&quot;a&quot;) == &quot;a&quot;</code> 和 <code>&quot;a&quot; == new String(&quot;a&quot;)</code> 都为 <code>true</code><br><code>new String(&quot;a&quot;) == new String(&quot;a&quot;)</code> 为 <code>false</code></li>\n<li>字符串比较对代码单元值序列使用简单的相等性测试。没有尝试使用 Unicode 规范中定义的更复杂、面向语义的字符或字符串相等性和排序顺序定义。\n因此，根据 Unicode 标准，规范上相等的字符串值可能被测试为不相等。实际上，该算法假设两个字符串都已处于规范化形式。</li>\n</ol>\n<h2><a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-isstrictlyequal\">IsStrictlyEqual(x, y)</a></h2>\n<ol>\n<li>若 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-sametype\">SameType</a>(<code>x</code>, <code>y</code>) 为 false，返回 false。<blockquote>\n<p>即按顺序判断是否都是 undefined, null, Boolean, Number, BigInt, Symbol, String, Object 类型</p>\n</blockquote>\n</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type\">是 Number</a>，则<blockquote>\n<p>注意 NaN 也属于 Number</p>\n</blockquote>\n<ol>\n<li>返回 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-equal\">Number::equal</a>(<code>x</code>, <code>y</code>)。<blockquote>\n<p>与 IEEE754 一致，即 NaN!=NaN，+0=-0</p>\n</blockquote>\n</li>\n</ol>\n</li>\n<li>返回 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluenonnumber\">SameValueNonNumber</a>(<code>x</code>, <code>y</code>)。<blockquote>\n<p>此操作实际上就是将比较操作委托到具体类型。但注意由于输入参数已经是同类型，当 x 为 undefined 或 null 时直接返回 true</p>\n</blockquote>\n</li>\n</ol>\n<h2><a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual(x, y)</a></h2>\n<ol>\n<li>若 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-sametype\">SameType</a>(<code>x</code>, <code>y</code>) 为 true, 则 1. 返回 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-isstrictlyequal\">IsStrictlyEqual</a>(<code>x</code>, <code>y</code>).<blockquote>\n<p>类型相同时，完全等同于严格比较</p>\n</blockquote>\n</li>\n<li>若 <code>x</code> 为 null 且 <code>y</code> 为 undefined, 返回 true.</li>\n<li>若 <code>x</code> 为 undefined 且 <code>y</code> 为 null, 返回 true.<blockquote>\n<p>这两步是对称的，表示 null 与 undefined 松散相等</p>\n</blockquote>\n</li>\n<li>NOTE: This step is replaced in section <a href=\"https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-IsHTMLDDA-internal-slot-aec\">B.3.6.2</a>.</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type\">是一个 Number</a> 且 <code>y</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-string-type\">是一个 String</a>, 返回 ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(<code>x</code>, ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber\">ToNumber</a>(<code>y</code>)).</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-string-type\">是一个 String</a> 且 <code>y</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type\">是一个 Number</a>, 返回 ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber\">ToNumber</a>(<code>x</code>), <code>y</code>).<blockquote>\n<p>这两步是对称的，对于字符串与数字的比较，字符串总是转为数字</p>\n</blockquote>\n</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-bigint-type\">是一个 BigInt</a> 且 <code>y</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-string-type\">是一个 String</a>, 则<ol>\n<li>令 <code>n</code> 为 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-stringtobigint\">StringToBigInt</a>(<code>y</code>).</li>\n<li>若 <code>n</code> 是 undefined, 返回 false.</li>\n<li>Return ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(<code>x</code>, <code>n</code>).</li>\n</ol>\n</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-string-type\">是一个 String</a> 且 <code>y</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-bigint-type\">是一个 BigInt</a>, 返回 ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(<code>y</code>, <code>x</code>).<blockquote>\n<p>这两步和上两步一致</p>\n</blockquote>\n</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-boolean-type\">是一个 Boolean</a>, 返回 ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber\">ToNumber</a>(<code>x</code>), <code>y</code>).</li>\n<li>若 <code>y</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-boolean-type\">是一个 Boolean</a>, 返回 ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(<code>x</code>, ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber\">ToNumber</a>(<code>y</code>)).<blockquote>\n<p>这两步是对称的，对于与布尔值的比较，总是转为数字再与布尔值比较</p>\n</blockquote>\n</li>\n<li>若 <code>x</code> 为 String, Number, BigInt, 或 Symbol 中的一个且 <code>y</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-object-type\">是一个 Object</a>, 返回 ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(<code>x</code>, ? <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toprimitive\">ToPrimitive</a>(<code>y</code>)).</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-object-type\">是一个 Object</a> 且 <code>y</code> 是 String, Number, BigInt, 或 Symbol 中的一个, 返回 ! <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal\">IsLooselyEqual</a>(? <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toprimitive\">ToPrimitive</a>(<code>x</code>), <code>y</code>).<blockquote>\n<p>这两步是对称的，对于与对象的比较，对象总是会通过 ToPrimitive 抽象操作转为原始值<br>在不考虑 Symbol.toPrimitive 的情况下相当于 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-ordinarytoprimitive\">OrdinaryToPrimitive(input, NUMBER)</a>，即依次调用（包括在原型上的）valueOf 和 toString 方法</p>\n</blockquote>\n</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-bigint-type\">是一个 BigInt</a> 且 <code>y</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type\">是一个 Number</a>, 或若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type\">是一个 Number</a> 且 <code>y</code> <a href=\"ecmascript-data-types-and-values.html#sec-ecmascript-language-types-bigint-type\">是一个 BigInt</a>, 则<ol>\n<li>若 <code>x</code> 不是 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#finite\">finite</a> 或 <code>y</code> 不是 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#finite\">finite</a>, 返回 false.</li>\n<li>若 <a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#%E2%84%9D\">ℝ</a>(<code>x</code>) = <a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#%E2%84%9D\">ℝ</a>(<code>y</code>), 返回 true; 否则 返回 false.</li>\n</ol>\n</li>\n</ol>\n<p>可以发现，原始值之间的比较几乎就是将双方转为数字（ToNumber）再比较。<br>若包含对象，对象也是先转为原始值（ToPrimitive），最后其实还是要转为数字来比较。</p>\n<h3><a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toprimitive\">ToPrimitive(input [,preferredType])</a></h3>\n<ol>\n<li>If <code>input</code> <a href=\"ecmascript-data-types-and-values.html#sec-object-type\">is an Object</a>, then<ol>\n<li>Let <code>exoticToPrim</code> be ? <a href=\"abstract-operations.html#sec-getmethod\">GetMethod</a>(<code>input</code>, <a href=\"ecmascript-data-types-and-values.html#sec-well-known-symbols\">%Symbol.toPrimitive%</a>).</li>\n<li>If <code>exoticToPrim</code> is not undefined, then<ol>\n<li>If <code>preferredType</code> is not present, then<ol>\n<li>Let <code>hint</code> be &quot;default&quot;.</li>\n</ol>\n</li>\n<li>Else if <code>preferredType</code> is string, then<ol>\n<li>Let <code>hint</code> be &quot;string&quot;.</li>\n</ol>\n</li>\n<li>Else,<ol>\n<li><a href=\"notational-conventions.html#assert\">Assert</a>: <code>preferredType</code> is number.</li>\n<li>Let <code>hint</code> be &quot;number&quot;.</li>\n</ol>\n</li>\n<li>Let <code>result</code> be ? <a href=\"abstract-operations.html#sec-call\">Call</a>(<code>exoticToPrim</code>, <code>input</code>, « <code>hint</code> »).</li>\n<li>If <code>result</code> <a href=\"ecmascript-data-types-and-values.html#sec-object-type\">is not an Object</a>, return <code>result</code>.</li>\n<li>Throw a TypeError exception.</li>\n</ol>\n</li>\n<li>If <code>preferredType</code> is not present, let <code>preferredType</code> be number.</li>\n<li>Return ? <a href=\"abstract-operations.html#sec-ordinarytoprimitive\">OrdinaryToPrimitive</a>(<code>input</code>, <code>preferredType</code>).</li>\n</ol>\n</li>\n<li>Return <code>input</code>.</li>\n</ol>\n<h1><a href=\"https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.is\">Object.is(value1, value2)</a></h1>\n<ol>\n<li>返回 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevalue\">SameValue</a>(<code>value1</code>, <code>value2</code>).</li>\n</ol>\n<blockquote>\n<p><code>Object.is</code> 就是暴露了 SameValue 抽象操作，见下</p>\n</blockquote>\n<h2>SameValue(x, y)</h2>\n<p>本质上就是为了区别于 <a href=\"#isstrictlyequalx-y\">IsStrictlyEqual(x, y)</a> 抽象操作，它视 <code>NaN</code> 与 <code>NaN</code> 相等，<code>+0</code> 与 <code>-0</code> 不相等（这与 IsStrictlyEqual 得到的结果正好相反）。</p>\n<p>可以看出，唯一区别就是对 Number 类型的处理有差异，其余步骤是完全一致的。</p>\n<ol>\n<li>若 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-sametype\">SameType</a>(<code>x</code>, <code>y</code>) 是 false, 返回 false.</li>\n<li>若 <code>x</code> <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type\">是 Number</a>, 则</li>\n<li>返回 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-sameValue\">Number::sameValue</a>(<code>x</code>, <code>y</code>).</li>\n<li>返回 <a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluenonnumber\">SameValueNonNumber</a>(<code>x</code>, <code>y</code>).</li>\n</ol>\n<h1>数值比较</h1>\n<p>数值有 Number（IEEE 754-2019 双精度浮点数， 𝔽）和 BigInt（任意整形，表示数学值，不含正负无穷和NaN，ℤ）两种类型。<br>Number 和 BigInt 之间不能直接比较（不会隐式转换），因为 Number 比较可能会丢失精度。</p>\n<h2><a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-equal\"><code>Number::equal(x, y)</code></a></h2>\n<ol>\n<li>If x is NaN, return false.</li>\n<li>If y is NaN, return false.</li>\n<li>If x is y, return true.</li>\n<li>If x is +0𝔽 and y is -0𝔽, return true.</li>\n<li>If x is -0𝔽 and y is +0𝔽, return true.</li>\n<li>Return false.</li>\n</ol>\n<h2><a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-sameValue\"><code>Number::sameValue(x, y)</code></a></h2>\n<ol>\n<li>If x is NaN and y is NaN, return true.</li>\n<li>If x is +0𝔽 and y is -0𝔽, return false.</li>\n<li>If x is -0𝔽 and y is +0𝔽, return false.</li>\n<li>If x is y, return true.</li>\n<li>Return false.</li>\n</ol>\n<h2><a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-sameValueZero\"><code>Number::sameValueZero(x, y)</code></a></h2>\n<ol>\n<li>If x is NaN and y is NaN, return true.</li>\n<li>If x is +0𝔽 and y is -0𝔽, return true.</li>\n<li>If x is -0𝔽 and y is +0𝔽, return true.</li>\n<li>If x is y, return true.</li>\n<li>Return false.</li>\n</ol>\n<h2><a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-bigint-equal\"><code>BigInt::equal(x, y)</code></a></h2>\n<ol>\n<li>If <a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#%E2%84%9D\">ℝ</a>(x) = <a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#%E2%84%9D\">ℝ</a>(y), return true; otherwise return false.</li>\n</ol>\n<h2><a href=\"https://tc39.es/ecma262/multipage/notational-conventions.html#%E2%84%9D\"><code>ℝ(x)</code></a></h2>\n<p>表示 x 的数学值（在数学意义上的值，而非计算机表示）。<br>+0𝔽 和 -0𝔽 视为数学值 0。<br>+∞𝔽 和 -∞𝔽 和 NaN 的行为未定义。</p>\n<h1>See Also</h1>\n<p><a href=\"https://evanhahn.com/re-implementing-javascript-double-equals-in-javascript/\">https://evanhahn.com/re-implementing-javascript-double-equals-in-javascript/</a></p>\n","tags":["ecma262"]},{"id":"saml-oauth-oidc","url":"https://yieldray.fun/posts/saml-oauth-oidc","title":"SAML/OAuth/OIDC","date_published":"2024-09-07T19:15:44.000Z","date_modified":"2024-09-07T19:15:44.000Z","content_text":"<h1>SSO</h1>\n<p>单点登录 <a href=\"https://en.wikipedia.org/wiki/Single_sign-on\">Single sign-on (SSO)</a> 简单来说，就是通过一次登录，授权访问多个应用程序。</p>\n<p>单点登录的好处显而易见：用户能够集中管理其身份。也可通过 MFA 加强安全性。</p>\n<blockquote>\n<p>另见：<br><a href=\"https://www.cloudflare.com/zh-cn/learning/access-management/what-is-sso/\">https://www.cloudflare.com/zh-cn/learning/access-management/what-is-sso/</a></p>\n</blockquote>\n<h1>SAML</h1>\n<p><a href=\"https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language\">SAML (Security Assertion Markup Language)</a> 基于 XML/HTTP/SOAP （现 SAML2.0），主要用于浏览器 SSO。</p>\n<p><strong>工作原理</strong></p>\n<ul>\n<li>身份提供者（Identity Provider，IdP）： 通常是企业内部的目录服务，负责验证用户的身份</li>\n<li>服务提供商（Service Provider，SP）： 需要用户登录的应用程序或服务</li>\n<li>SAML 协议： IdP 和 SP 之间通过 SAML 协议交换信息。当用户试图访问 SP 时，SP 会重定向用户到 IdP 进行身份验证。\n如果验证成功，IdP 会生成一个 SAML 断言，其中包含用户的身份信息，并将其发送给 SP。SP 收到断言后，验证其有效性，并允许用户访问</li>\n</ul>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant ServiceProvider\n    participant UserAgent\n    participant IdentityProvider\n\n    UserAgent-&gt;&gt;ServiceProvider: Request target resource\n    ServiceProvider--&gt;&gt;UserAgent: (Discover the IdP)\n    ServiceProvider-&gt;&gt;UserAgent: Redirect to SSO Service\n    UserAgent-&gt;&gt;IdentityProvider: Request SSO Service\n    IdentityProvider--&gt;&gt;UserAgent: (Identify the user)\n    IdentityProvider-&gt;&gt;UserAgent: Respond with XHTML form\n    UserAgent-&gt;&gt;ServiceProvider: Request Assertion Consumer Service\n    ServiceProvider-&gt;&gt;UserAgent: Redirect to target resource\n    UserAgent-&gt;&gt;ServiceProvider: Request target resource\n    ServiceProvider-&gt;&gt;UserAgent: Respond with requested resource\n</code></pre>\n<blockquote>\n<p>另见：\n<a href=\"https://www.sheshbabu.com/posts/visual-explanation-of-saml-authentication/\">https://www.sheshbabu.com/posts/visual-explanation-of-saml-authentication/</a><br><a href=\"https://www.cloudflare.com/zh-cn/learning/access-management/what-is-saml/\">https://www.cloudflare.com/zh-cn/learning/access-management/what-is-saml/</a><br><a href=\"https://www.microsoft.com/zh-cn/security/business/security-101/what-is-security-assertion-markup-language-saml\">https://www.microsoft.com/zh-cn/security/business/security-101/what-is-security-assertion-markup-language-saml</a><br><a href=\"https://ssoready.com/docs/saml/saml-technical-primer\">https://ssoready.com/docs/saml/saml-technical-primer</a></p>\n</blockquote>\n<h1>OAuth</h1>\n<p><a href=\"https://en.wikipedia.org/wiki/OAuth\">OAuth (Open Auth)</a> 主要用于<strong>授权</strong>第三方应用程序（通过令牌，代表用户来）访问用户的资源。<br>因此 OAuth 相比于 SAML，就会区分 Client（第三方应用程序） 和 ResourceOwner（拥有资源的人，即用户）。</p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant Client\n    participant ResourceOwner\n    participant AuthorizationServer\n    participant ResourceServer\n\n    Client-&gt;&gt;ResourceOwner: Authorization Request\n    ResourceOwner--&gt;&gt;Client: Authorization Grant\n    Client-&gt;&gt;AuthorizationServer: Authorization Grant\n    AuthorizationServer--&gt;&gt;Client: Access Token\n    Client-&gt;&gt;ResourceServer: Access Token\n    ResourceServer--&gt;&gt;Client: Protected Resource\n</code></pre>\n<blockquote>\n<p>另见：<br><a href=\"https://www.cloudflare-cn.com/learning/access-management/what-is-oauth/\">https://www.cloudflare-cn.com/learning/access-management/what-is-oauth/</a><br><a href=\"https://www.microsoft.com/zh-cn/security/business/security-101/what-is-oauth\">https://www.microsoft.com/zh-cn/security/business/security-101/what-is-oauth</a></p>\n</blockquote>\n<h1>OIDC</h1>\n<p>OpenID 是一种分布式的<strong>身份验证</strong>协议，它允许用户使用一个唯一的标识符（通常是一个URL）在不同的网站上登录，而无需在每个网站上重新注册。<br>OpenID 的核心在于提供一个可信赖的第三方身份验证服务（IdP），用户可以在这个服务上验证自己的身份，然后将这个验证结果带到其他网站上使用。</p>\n<p><a href=\"https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)\">OpenID Connect（OIDC）</a>是建立在OAuth 2.0 授权框架之上的一个身份认证层。它利用 OAuth 2.0 的授权机制，为客户端提供了一种更简单、更安全的方法（REST/JSON 消息流）来验证用户身份。OIDC 通过在 OAuth 2.0 的基础上增加了一个 ID Token，来明确表示用户的身份信息。</p>\n<h1>Summary</h1>\n<h3>SAML 流程</h3>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant User as 用户\n    participant SP as 服务提供商 (SP)\n    participant IdP as 身份提供商 (IdP)\n\n    User -&gt;&gt; SP: 访问资源\n    SP -&gt;&gt; User: 重定向到IdP进行身份验证\n    User -&gt;&gt; IdP: 提交认证信息\n    IdP -&gt;&gt; User: 返回SAML断言\n    User -&gt;&gt; SP: 使用SAML断言访问资源\n    SP -&gt;&gt; User: 提供访问权限\n</code></pre>\n<h3>OAuth2 流程</h3>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant User as 用户\n    participant Client as 客户端 (第三方应用)\n    participant AuthServer as 授权服务器\n    participant ResourceServer as 资源服务器\n\n    User -&gt;&gt; Client: 请求访问资源\n    Client -&gt;&gt; AuthServer: 重定向用户进行授权 (授权请求)\n    AuthServer -&gt;&gt; User: 询问用户授权同意\n    User -&gt;&gt; AuthServer: 同意授权\n    AuthServer -&gt;&gt; Client: 返回授权码\n    Client -&gt;&gt; AuthServer: 用授权码换取访问令牌\n    AuthServer -&gt;&gt; Client: 返回访问令牌\n    Client -&gt;&gt; ResourceServer: 使用访问令牌请求资源\n    ResourceServer -&gt;&gt; Client: 返回资源\n</code></pre>\n<h3>OIDC 流程</h3>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant User as 用户\n    participant Client as 客户端 (第三方应用)\n    participant OIDCServer as OIDC 服务器\n    participant ResourceServer as 资源服务器\n\n    User -&gt;&gt; Client: 请求访问资源\n    Client -&gt;&gt; OIDCServer: 重定向用户进行身份验证 (授权请求)\n    OIDCServer -&gt;&gt; User: 询问用户同意\n    User -&gt;&gt; OIDCServer: 同意并提交身份验证信息\n    OIDCServer -&gt;&gt; Client: 返回授权码和ID Token\n    Client -&gt;&gt; OIDCServer: 用授权码换取访问令牌\n    OIDCServer -&gt;&gt; Client: 返回访问令牌\n    Client -&gt;&gt; ResourceServer: 使用访问令牌请求资源\n    ResourceServer -&gt;&gt; Client: 返回资源\n</code></pre>\n","tags":["auth"]},{"id":"styling-tables","url":"https://yieldray.fun/posts/styling-tables","title":"样式化表格","date_published":"2024-09-05T00:00:00.000Z","date_modified":"2024-09-05T00:00:00.000Z","content_text":"<p>从 HTML 开始</p>\n<pre><code class=\"language-html\">&lt;table&gt;\n    &lt;!-- caption 元素用于总结表格的内容，若存在则必须为表格的第一个元素 --&gt;\n    &lt;caption&gt;\n        Annual surface temperature change in 2022\n    &lt;/caption&gt;\n    &lt;!-- thread 和 tbody 分别包含表格的头部和主体行 --&gt;\n    &lt;!-- tr 形成行，其中包含 th（头部）和 td（主体）单元格 --&gt;\n    &lt;thead&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;column&quot;&gt;Country&lt;/th&gt;\n            &lt;th scope=&quot;column&quot;&gt;Mean temperature change (°C)&lt;/th&gt;\n        &lt;/tr&gt;\n    &lt;/thead&gt;\n    &lt;!-- 对于 th，可以存在可选的 scope 属性，值为 row 或 column，以推断标题所属的轴 --&gt;\n    &lt;tbody&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;row&quot;&gt;United Kingdom&lt;/th&gt;\n            &lt;td&gt;1.912&lt;/td&gt;\n        &lt;/tr&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;row&quot;&gt;Afghanistan&lt;/th&gt;\n            &lt;td&gt;2.154&lt;/td&gt;\n        &lt;/tr&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;row&quot;&gt;Australia&lt;/th&gt;\n            &lt;td&gt;0.681&lt;/td&gt;\n        &lt;/tr&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;row&quot;&gt;Kenya&lt;/th&gt;\n            &lt;td&gt;1.162&lt;/td&gt;\n        &lt;/tr&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;row&quot;&gt;Honduras&lt;/th&gt;\n            &lt;td&gt;0.945&lt;/td&gt;\n        &lt;/tr&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;row&quot;&gt;Canada&lt;/th&gt;\n            &lt;td&gt;1.284&lt;/td&gt;\n        &lt;/tr&gt;\n    &lt;/tbody&gt;\n    &lt;tfoot&gt;\n        &lt;tr&gt;\n            &lt;th scope=&quot;row&quot;&gt;Global average&lt;/th&gt;\n            &lt;td&gt;1.4&lt;/td&gt;\n        &lt;/tr&gt;\n    &lt;/tfoot&gt;\n&lt;/table&gt;\n</code></pre>\n<p>不要忘记浏览器默认样式如下</p>\n<pre><code class=\"language-css\">table {\n    display: table;\n    border-collapse: separate;\n    box-sizing: border-box;\n    text-indent: initial;\n    unicode-bidi: isolate;\n    border-spacing: 2px;\n    border-color: gray;\n}\n\ncaption {\n    display: table-caption;\n    text-align: -webkit-center;\n    unicode-bidi: isolate;\n}\n\nthead {\n    display: table-header-group;\n    vertical-align: middle;\n    unicode-bidi: isolate;\n    border-color: inherit;\n}\n\ntbody {\n    display: table-row-group;\n    vertical-align: middle;\n    unicode-bidi: isolate;\n    border-color: inherit;\n}\n\ntfoot {\n    display: table-footer-group;\n    vertical-align: middle;\n    unicode-bidi: isolate;\n    border-color: inherit;\n}\n\ntr {\n    display: table-row;\n    vertical-align: inherit;\n    unicode-bidi: isolate;\n    border-color: inherit;\n}\n\nth {\n    display: table-cell;\n    vertical-align: inherit;\n    font-weight: bold;\n    text-align: -internal-center;\n    unicode-bidi: isolate;\n}\n\ntd {\n    display: table-cell;\n    vertical-align: inherit;\n    unicode-bidi: isolate;\n}\n</code></pre>\n<blockquote>\n<p><strong>FYI</strong><br>把 table 的 display 设置为 grid，就因为能让样式更容易？<br>这会破坏浏览器免费提供的内置辅助功能，使得通过辅助技术访问的用户难以访问表格。</p>\n</blockquote>\n<p>现在加入基本的样式（不要忘记浏览器的默认 CSS）</p>\n<pre><code class=\"language-css\">body {\n    font-family: &quot;Open Sans&quot;, sans-serif;\n    line-height: 1.5;\n}\n\ntable {\n    text-align: left;\n    border-collapse: collapse;\n}\n\nth,\ntd {\n    border: 1px solid;\n}\n</code></pre>\n<iframe height=\"350\" style=\"width: 100%;\" scrolling=\"no\" title=\"styling-tables-1\" src=\"https://codepen.io/YieldRay/embed/KKjrZgZ?default-tab=css%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/KKjrZgZ\">\n  styling-tables-1</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>加入装饰性的样式</p>\n<pre><code class=\"language-css\">/* Text alignment */\nth,\ncaption {\n    text-align: start;\n}\n\nthead th:not(:first-child),\ntd {\n    text-align: end;\n}\n\n/* Headers and footers */\nthead {\n    border-block-end: 2px solid;\n    background: whitesmoke;\n}\n\ntfoot {\n    border-block: 2px solid;\n    background: whitesmoke;\n}\n\nth,\ntd {\n    border: 1px solid lightgrey;\n    padding: 0.25rem 0.75rem;\n}\n\n/* Colouring rows and columns */\ntable {\n    --color: #d0d0f5;\n}\n\nthead,\ntfoot {\n    background: var(--color);\n}\n\ntbody tr:nth-child(even) {\n    background: color-mix(in srgb, var(--color), transparent 60%);\n}\n</code></pre>\n<iframe height=\"350\" style=\"width: 100%;\" scrolling=\"no\" title=\"styling-tables-2\" src=\"https://codepen.io/YieldRay/embed/gONQoWw?default-tab=css%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/gONQoWw\">\n  styling-tables-2</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>对于一个过宽的表格，其在水平方向可能超出视口。让用户移动整个视口往往不够好，解决办法也很简单：<br>通过一个 <code>&lt;div class=&quot;wrapper&quot;&gt; &lt;table/&gt; &lt;/div&gt;</code> wrapper 元素包裹表格，并设置 wrapper 元素的 overflow 即可。</p>\n<iframe height=\"400\" style=\"width: 100%;\" scrolling=\"no\" title=\"styling-tables\" src=\"https://codepen.io/YieldRay/embed/YzoRYYw?default-tab=css%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/YzoRYYw\">\n  styling-tables</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>固定第一列</p>\n<iframe height=\"400\" style=\"width: 100%;\" scrolling=\"no\" title=\"styling-tables-3\" src=\"https://codepen.io/YieldRay/embed/zYVMpWX?default-tab=css%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/zYVMpWX\">\n  styling-tables-3</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>表格的 table-layout 默认是 auto，浏览器会自动为我们调整每个列的宽度（<a href=\"https://css-tricks.com/almanac/properties/t/table-layout/\">CSS Tricks</a>）</p>\n<iframe height=\"400\" style=\"width: 100%;\" scrolling=\"no\" title=\"styling-tables-4\" src=\"https://codepen.io/YieldRay/embed/wvLQpOy?default-tab=css%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/wvLQpOy\">\n  styling-tables-4</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>改编自：<br><a href=\"https://piccalil.li/blog/styling-tables-the-modern-css-way/\">Styling Tables the Modern CSS Way</a></p>\n<p>引用：<br><a href=\"https://lea.verou.me/blog/2012/04/background-attachment-local/\">Pure CSS scrolling shadows with background-attachment: local</a><br><a href=\"https://adrianroselli.com/2020/01/fixed-table-headers.html\">Fixed Table Headers</a></p>\n","tags":["css"]},{"id":"js-iterator","url":"https://yieldray.fun/posts/js-iterator","title":"js迭代对象Iterator","date_published":"2024-09-01T22:59:53.000Z","date_modified":"2024-09-01T22:59:53.000Z","content_text":"<h1>JavaScript</h1>\n<p>JavaScript 的世界已存在<a href=\"/posts/js-iteration\">迭代协议</a>（ES2015: <code>Symbol.iterator</code>），内置对象的 <code>Symbol.iterator</code> 直接继承 <a href=\"https://tc39.es/ecma262/#sec-%iteratorprototype%-object\"><code>%IteratorPrototype%</code></a>，但该对象没有直接暴露为全局对象。</p>\n<p><a href=\"https://github.com/tc39/proposal-iterator-helpers\">Iterator Helpers</a> 提案（<a href=\"https://tc39.es/proposal-iterator-helpers/\">stage3</a>，截止目前 <a href=\"https://v8.dev/features/iterator-helpers\">Chrome112</a> 已实现）将该对象暴露为 <a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Iterator\">Iterator 全局对象</a>，并在该对象的原型上提供一系列辅助函数。</p>\n<p>因为 Iterator 类的原型提供 <code>[Symbol.iterator]()</code> 方法（即 Iterator 实例就是可迭代对象），通过继承 Iterator（并覆写其上的一些方法），就能方便地实现可迭代对象。</p>\n<p>以一些<a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Iterator#description\">内置迭代器</a>为例，它们往往继承 Iterator 并覆写 <code>next()</code> 和 <code>Symbol.toStringTag</code>。</p>\n<pre><code class=\"language-ts\">// 示例\nclass RangeIterator extends Iterator {\n    private current: number;\n    constructor(\n        private start: number,\n        private end: number,\n    ) {\n        super();\n        this.current = start;\n    }\n\n    next() {\n        if (this.current &lt; this.end) {\n            const value = this.current;\n            this.current++;\n            return { value, done: false };\n        } else {\n            return { done: true };\n        }\n    }\n}\n\nfor (const number of new RangeIterator(1, 5)) {\n    console.log(number); // =&gt; 1 2 3 4\n}\n</code></pre>\n<p>迭代器辅助方法类似于数组方法，但提供了一个很好的、也是标准库中曾缺失的惰性执行能力。<br>迭代器本身又是与流等其它 Web API 相关的，因此现在可以在不引入第三方库的情况下更容易实现流式/响应式编程。(个人认为引入 <a href=\"https://rxjs.dev/\">RxJS</a> 这种库在各种方面都开销大)</p>\n<hr>\n<p>我们知道，规范中已有 <a href=\"https://tc39.es/ecma262/#sec-asynciteratorprototype\"><code>%AsyncIteratorPrototype%</code></a>，但是不存在全局对象 <code>AsyncIterator</code> 被暴露，目前也没有提案给该原型上提供辅助函数。</p>\n<p>这也很容易理解，因为迭代器迭代本来就是惰性的，因此无需区分同步和异步。</p>\n<h1>TypeScript</h1>\n<blockquote>\n<p>update 2024/9/11<br><a href=\"https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/#iterator-helper-methods2\">TypeScript 5.6</a> 已为迭代器辅助方法提供类型，包括内置迭代器子类。</p>\n</blockquote>\n","tags":["js"]},{"id":"musttail-efficient-interpreters","url":"https://yieldray.fun/posts/musttail-efficient-interpreters","title":"Parsing Protobuf at 2+GB/s: How I Learned To Love Tail Calls in C\n","date_published":"2024-08-31T15:21:15.000Z","date_modified":"2024-08-31T15:21:15.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://blog.reverberate.org/2021/04/21/musttail-efficient-interpreters.html\">https://blog.reverberate.org/2021/04/21/musttail-efficient-interpreters.html</a></p>\n</blockquote>\n<h2>使用 <code>musttail</code> 优化解释器循环</h2>\n<p>一个令人兴奋的新特性<a href=\"https://reviews.llvm.org/D99517\">刚刚合并到 Clang 编译器的主分支</a>。使用 <code>[[clang::musttail]]</code> 或 <code>__attribute__((musttail))</code> 语句<a href=\"https://zh.cppreference.com/w/c/language/attributes\">属性</a>，现在可以在 C、C++ 和 Objective-C 中获得保证的尾调用。</p>\n<iframe width=\"800px\" height=\"200px\" src=\"https://godbolt.org/e?hideEditorToolbars=true#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,selection:(endColumn:1,endLineNumber:2,positionColumn:1,positionLineNumber:2,selectionStartColumn:1,selectionStartLineNumber:2,startColumn:1,startLineNumber:2),source:'int+g(int)%3B%0A%0Aint+f(int+x)+%7B%0A++++__attribute__((musttail))+return+g(x)%3B%0A%7D'),l:'5',n:'1',o:'C+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cclang_trunk,filters:(b:'0',binary:'1',binaryObject:'1',commentOnly:'0',debugCalls:'1',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'0',trim:'1',verboseDemangling:'0'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:2,lang:___c,libs:!(),options:'-O2',overrides:!(),selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1),l:'5',n:'0',o:'+x86-64+clang+(trunk)+(Editor+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4\"></iframe>\n\n<p>虽然尾调用通常与函数式编程风格相关联，但我对它们的兴趣纯粹出于性能原因。事实证明，在某些情况下，我们可以使用尾调用从编译器中获得比其他方法更好的代码 - 至少在当前的编译器技术下 - 并且无需降级到汇编语言。</p>\n<p>将这种技术应用于 protobuf 解析已经产生了惊人的结果：<a href=\"https://github.com/protocolbuffers/upb/pull/310\">我们已经成功地展示了超过 2GB/s 的 protobuf 解析速度</a>，是之前最先进技术的两倍多。有多种技术促成了这种加速，因此“尾调用 == 2 倍加速”是一个错误的结论。但尾调用是使这种加速成为可能的关键部分。</p>\n<p>在这篇博文中，我将描述为什么尾调用是一种如此强大的技术，我们是如何将其应用于 protobuf 解析的，以及这种技术如何推广到解释器。我认为，所有用 C 语言编写的主要语言解释器（Python、Ruby、PHP、Lua 等）都可以通过采用这种技术来获得显著的性能提升。主要缺点是可移植性：目前 <code>musttail</code> 是一个非标准的编译器扩展，虽然我希望它能流行起来，但要过一段时间才能广泛传播到足以让你系统的 C 编译器支持它。也就是说，在构建时，如果检测到 <code>musttail</code> 不可用，可以牺牲一些效率来换取可移植性。</p>\n<h2>尾调用基础</h2>\n<p>尾调用是任何位于函数尾部的调用，即在函数返回之前要执行的最后一个动作。当进行 <em>尾调用优化</em> 时，编译器为尾调用发出一个 <code>jmp</code> 指令，而非 <code>call</code> 指令。这跳过了通常允许被调用者 <code>g()</code> 返回到调用者 <code>f()</code> 的 bookkeeping 操作，例如创建新的堆栈帧或压入返回地址。相反，<code>f()</code> 直接跳转到 <code>g()</code>，就好像它是同一个函数的一部分一样，而 <code>g()</code> 直接返回到调用 <code>f()</code> 的任何函数。这种优化是安全的，因为一旦尾调用开始，<code>f()</code> 的堆栈帧就不再需要了，因为它不再可能访问 <code>f()</code> 的任何局部变量。</p>\n<p>虽然这看起来像是一个普通的优化，但它具有两个非常重要的特性，为我们能够编写的算法类型打开了新的可能性。首先，它将连续进行 <strong>n</strong> 次尾调用时的堆栈内存从 <strong>O(n)</strong> 降低到 <strong>O(1)</strong>，这很重要，因为堆栈内存是有限的，而且堆栈溢出会导致程序崩溃。这意味着某些算法实际上只有在执行了这种优化后才安全写入。其次，<code>jmp</code> 消除了 <code>call</code> 的性能开销，使得函数调用与任何其它分支一样高效。这两个特性使我们能够使用尾调用作为正常迭代控制结构（如 <code>for</code> 或 <code>while</code>）的有效替代方案。</p>\n<p>这绝不是一个新想法，实际上它至少可以追溯到 1977 年，当时 Guy Steele <a href=\"http://dspace.mit.edu/handle/1721.1/5753\">发表了一篇完整的论文</a>，论证了过程调用比 <code>GOTO</code> 具有更简洁的设计，而尾调用优化可以使它们一样快。这是 <a href=\"https://en.wikipedia.org/wiki/History_of_the_Scheme_programming_language#The_Lambda_Papers\">“Lambda Papers”</a> 之一，这些论文是在 1975 年到 1980 年间撰写的，期间发展了许多 Lisp 和 Scheme 的基础思想。</p>\n<p>尾调用优化甚至对 Clang 来说也不是什么新鲜事：像 GCC 和许多其它编译器一样，Clang 已经能够优化尾调用。实际上，我们第一个示例中的 <code>musttail</code> 属性根本没有改变编译器的输出：Clang 已经可以在 <code>-O2</code> 下优化尾调用。</p>\n<p>新的是 <em>保证性</em>。虽然编译器通常能够成功地优化尾调用，但这只是尽力而为，而不是可以依赖的东西。特别是，这种优化很可能不会在非优化构建中发生：</p>\n<iframe width=\"800px\" height=\"200px\" src=\"https://godbolt.org/e?hideEditorToolbars=true#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,selection:(endColumn:2,endLineNumber:5,positionColumn:2,positionLineNumber:5,selectionStartColumn:2,selectionStartLineNumber:5,startColumn:2,startLineNumber:5),source:'void+g(int)%3B%0A%0Avoid+f(int+x)+%7B%0A++++return+g(x)%3B%0A%7D'),l:'5',n:'1',o:'C+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cclang_trunk,filters:(b:'0',binary:'1',binaryObject:'1',commentOnly:'0',debugCalls:'1',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'0',trim:'1',verboseDemangling:'0'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:2,lang:___c,libs:!(),options:'',overrides:!(),selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1),l:'5',n:'0',o:'+x86-64+clang+(trunk)+(Editor+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4\"></iframe>\n\n<p>此处，尾调用被编译成真正的 <code>call</code>，所以我们又回到了 <strong>O(n)</strong> 堆栈空间。这就是我们需要 <code>musttail</code> 的原因：除非我们能从编译器那里得到保证，确保我们的尾调用将在所有构建模式下 <em>始终</em> 被优化，否则使用尾调用进行迭代的算法是不安全的。如果代码只有在启用优化时才能正常工作，那将是一个非常严重的限制。</p>\n<h2>解释器循环的麻烦</h2>\n<p>编译器是令人难以置信的技术，但它们并不完美。LuaJIT 的作者 Mike Pall 决定用汇编语言而不是 C 语言编写 LuaJIT 2.x 的解释器，他将此决定视为解释 <a href=\"https://www.reddit.com/r/programming/comments/badl2/luajit_2_beta_3_is_out_support_both_x32_x64/c0lrus0/\">为什么 LuaJIT 的解释器如此快</a> 的主要因素。他后来详细介绍了 <a href=\"http://lua-users.org/lists/lua-l/2011-02/msg00742.html\">为什么 C 编译器难以处理解释器主循环</a>。他最核心的两点是：</p>\n<ul>\n<li>函数越大，其控制流越复杂和相互关联，编译器的寄存器分配器就越难将最重要的数据保存在寄存器中。</li>\n<li>当快速路径和慢速路径混合在同一个函数中时，慢速路径的存在会影响快速路径的代码质量。</li>\n</ul>\n<p>这些观察结果与我们在优化 protobuf 解析时的经验非常吻合。好消息是尾调用可以帮助解决这两个问题。</p>\n<p>将解释器循环与 protobuf 解析器进行比较可能看起来很奇怪，但 protobuf 的线格式的性质使它们比你想象的更相似。protobuf 的线格式是一系列标签/值对，其中标签包含字段编号和线类型。这个标签类似于解释器的操作码：它告诉我们需要执行什么操作来解析该字段的数据。与解释器操作码一样，protobuf 字段编号可以以任何顺序出现，因此我们必须准备好随时调度到代码的任何部分。</p>\n<p>编写这种解析器的自然方法是让一个 <code>while</code> 循环围绕一个 <code>switch</code> 语句，实际上，这基本上是 protobuf 解析的最新技术，从 protobuf 存在的那一刻起就是如此。例如，<a href=\"https://github.com/protocolbuffers/protobuf/blob/f763a2a86084371fd0da95f3eeb879c2ff26b06d/src/google/protobuf/descriptor.pb.cc#L2175-L2227\">以下是一些来自 protobuf 当前 C++ 版本的解析代码</a>。如果我们以图形方式表示控制流，我们会得到类似于以下内容：</p>\n<p><img src=\"https://s2.loli.net/2024/08/31/TaEyUWIC14Vz8Lc.png\" alt=\"\"></p>\n<p>但这是不完整的，因为几乎在每个阶段都可能出现问题。线类型可能错误，或者我们可能看到一些损坏的数据，或者我们可能只是碰巧到达了当前缓冲区的末尾。所以完整的控制流图看起来更像这样。</p>\n<p><img src=\"https://s2.loli.net/2024/08/31/a6RCkDr3VbePpLK.png\" alt=\"\"></p>\n<p>我们希望尽可能地停留在快速路径（蓝色）上，但当我们遇到一个困难的情况时，我们必须执行一些回退代码来处理它。这些回退路径通常比快速路径更大、更复杂，涉及更多数据，并且通常甚至进行对其他函数的越界调用来处理更复杂的情况。</p>\n<p>理论上，这种控制流图与配置文件相结合，应该能为编译器提供生成最优代码所需的所有信息。在实践中，当一个函数如此庞大且相互关联时，我们经常发现自己与编译器作斗争。在我们需要它将重要变量保存在寄存器中时，它却溢出了那个变量。它提升了我们想缩放到回退函数调用的堆栈帧操作。它合并了我们希望为了分支预测原因而保持分离的相同代码路径。这种体验最终可能会让人感觉像戴着手套弹钢琴一样。</p>\n<h2>使用尾调用改进解释器循环</h2>\n<p>上面的分析主要只是对 Mike 关于 <a href=\"http://lua-users.org/lists/lua-l/2011-02/msg00742.html\">解释器主循环的观察</a> 的复述。但是，我们并没有像 Mike 使用 LuaJIT 2.x 那样降低到汇编语言，而是发现面向尾调用的设计可以为我们提供从 C 语言获得几乎最佳代码所需的控制。我和我的同事 Gerben Stavenga 一起研究了这个问题，他提出了大部分的设计。我们的方法类似于 <a href=\"https://github.com/wasm3/wasm3\">wasm3 WebAssembly 解释器</a> 的设计，该解释器将这种模式描述为一个 <a href=\"https://github.com/wasm3/wasm3/blob/main/docs/Interpreter.md#m3-massey-meta-machine\">“元机器”</a>。</p>\n<p>我们 2+GB/s protobuf 解析器的代码提交给了 <a href=\"https://github.com/protocolbuffers/upb\">upb</a>，这是一个用 C 语言编写的 小型 protobuf 库，在 <a href=\"https://github.com/protocolbuffers/upb/pull/310\">pull/310</a> 中提交。虽然它完全可以工作，并且通过了所有 protobuf 兼容性测试，但它还没有在任何地方推出，并且该设计也没有在 protobuf 的 C++ 版本中实现。但现在 <code>musttail</code> 在 Clang 中可用（并且 <a href=\"https://github.com/protocolbuffers/upb/pull/390\">upb 已更新为使用它</a>），完全生产化快速解析器的最大障碍之一已经消除。</p>\n<p>我们的设计不再使用单个大型解析函数，而是为每个操作提供一个自己的小型函数。每个函数都尾调用下一个操作序列。例如，以下是一个用于解析单个定长字段的函数。（这段代码是从 upb 中的实际代码简化而来的；有很多设计细节我在这篇文章中没有提到，但希望在未来的文章中涵盖）。</p>\n<pre><code class=\"language-c\">#include &lt;stdint.h&gt;\n#include &lt;stddef.h&gt;\n#include &lt;string.h&gt;\n\ntypedef void *upb_msg;\nstruct upb_decstate;\ntypedef struct upb_decstate upb_decstate;\n\n// 传递给每个解析函数的标准参数集。\n// 由于 x86-64 的调用约定，这些参数将通过寄存器传递。\n#define UPB_PARSE_PARAMS                                          \\\n  upb_decstate *d, const char *ptr, upb_msg *msg, intptr_t table, \\\n      uint64_t hasbits, uint64_t data\n#define UPB_PARSE_ARGS d, ptr, msg, table, hasbits, data\n\n#define UNLIKELY(x) __builtin_expect(x, 0)\n#define MUSTTAIL __attribute__((musttail))\n\nconst char *fallback(UPB_PARSE_PARAMS);\nconst char *dispatch(UPB_PARSE_PARAMS);\n\n// 解析使用 1 字节标签（字段 1-15）的 4 字节定长字段的代码\nconst char *upb_pf32_1bt(UPB_PARSE_PARAMS) {\n  // 解码“data”，其中包含关于该字段的信息\n  uint8_t hasbit_index = data &gt;&gt; 24;\n  size_t ofs = data &gt;&gt; 48;\n\n  if (UNLIKELY(data &amp; 0xff)) {\n    // 线类型不匹配（dispatch 函数将预期的线类型与实际的线类型进行异或运算，因此 data &amp; 0xff == 0 表示匹配）\n    MUSTTAIL return fallback(UPB_PARSE_ARGS);\n  }\n\n  ptr += 1;  // 跳过标签\n\n  // 将数据存储到消息中\n  hasbits |= 1ull &lt;&lt; hasbit_index;\n  memcpy((char*)msg + ofs, ptr, 4);\n\n  ptr += 4;  // 跳过数据\n\n  // 调用 dispatch 函数，该函数将读取下一个标签，并分支到正确的字段解析函数。\n  MUSTTAIL return dispatch(UPB_PARSE_ARGS);\n}\n</code></pre>\n<p>对于这样一个小型而简单的函数，Clang 为我们提供了几乎无法超越的代码。</p>\n<pre><code>upb_pf32_1bt:                           # @upb_pf32_1bt\n        mov     rax, r9\n        shr     rax, 24\n        bts     r8, rax\n        test    r9b, r9b\n        jne     .LBB0_1\n        mov     r10, r9\n        shr     r10, 48\n        mov     eax, dword ptr [rsi + 1]\n        mov     dword ptr [rdx + r10], eax\n        add     rsi, 5\n        jmp     dispatch                        # TAILCALL\n.LBB0_1:\n        jmp     fallback                        # TAILCALL\n</code></pre>\n<p>请注意，没有序言或尾声指令，没有寄存器溢出，实际上根本没有使用堆栈。唯一的出口是来自两个尾调用的 <code>jmp</code>，但不需要任何代码来转发参数，因为参数已经位于正确的寄存器中。我们几乎唯一希望的改进是获得尾调用的条件跳转，<code>jne fallback</code>，而不是 <code>jne</code> 后面跟着 <code>jmp</code>。</p>\n<blockquote>\n<p><strong>译注：</strong><br>序言（prologue）指令位于函数体的开头，用于保存上下文并创建栈帧，例如：</p>\n<pre><code>push ebp ; 保存调用者的栈帧指针\nmov ebp, esp ; 将当前栈指针设置为新的栈帧指针\nsub esp, N ; 为局部变量保留N字节的栈空间\npush ebx ; 保存需要使用的寄存器（比如ebx）\npush esi\npush edi\n</code></pre>\n<p>尾声（epilogue）指令位于函数体的末尾，用于清理和恢复在 prologue 中保存的状态，例如：</p>\n<pre><code>pop edi       ; 恢复寄存器\npop esi\npop ebx\nmov esp, ebp  ; 恢复栈指针\npop ebp       ; 恢复栈帧指针\nret           ; 返回调用点\n</code></pre>\n<p>另见：<a href=\"https://zh.wikipedia.org/wiki/X86%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A\">X86调用约定</a></p>\n</blockquote>\n<p>如果你查看这段代码的汇编指令，而没有符号信息，你将没有理由知道这是一个完整的函数。它也可能是来自更大函数的基本块。而这本质上正是我们正在做的事情。我们正在将一个在概念上是一个大型复杂函数的解释器循环分解成块，并通过尾调用将控制流从一个块转移到另一个块。我们在每个块边界（至少对于六个寄存器）都完全控制寄存器分配，并且通过对每个函数使用相同的一组参数，我们消除了在一次调用到下一次调用之间来回移动任何值的必要性。只要函数足够简单，不会溢出这六个寄存器，我们就实现了将最重要的状态始终保存在所有快速路径中的寄存器中的目标。</p>\n<p>我们可以独立地优化每个指令序列，而且至关重要的是，编译器也会将每个序列视为独立的，因为它们位于不同的函数中（如果需要，我们可以使用 <code>noinline</code> 阻止内联）。这解决了我们之前描述的问题，即回退路径中的代码会降低快速路径的代码质量。如果我们将慢速路径放在与快速路径完全不同的函数中，我们可以保证快速路径不会受到影响。我们上面看到的漂亮的汇编序列实际上是冻结的，不受我们在解析器其他部分所做的任何更改的影响。</p>\n<p>如果我们将这种模式应用于 <a href=\"https://www.reddit.com/r/programming/comments/badl2/luajit_2_beta_3_is_out_support_both_x32_x64/c0lrus0/\">Mike 来自 LuaJIT 的示例</a>，我们或多或少可以 <a href=\"https://godbolt.org/z/K8Mo6hcGa\">用只有轻微代码质量缺陷的手写汇编指令来匹配他的代码</a>：</p>\n<pre><code class=\"language-c\">#define PARAMS unsigned RA, void *table, unsigned inst, \\\n               int *op_p, double *consts, double *regs\n#define ARGS RA, table, inst, op_p, consts, regs\ntypedef void (*op_func)(PARAMS);\nvoid fallback(PARAMS);\n\n#define UNLIKELY(x) __builtin_expect(x, 0)\n#define MUSTTAIL __attribute__((musttail))\n\nvoid ADDVN(PARAMS) {\n    op_func *op_table = table;\n    unsigned RC = inst &amp; 0xff;\n    unsigned RB = (inst &gt;&gt; 8) &amp; 0xff;\n    unsigned type;\n    memcpy(&amp;type, (char*)&amp;regs[RB] + 4, 4);\n    if (UNLIKELY(type &gt; -13)) {\n        return fallback(ARGS);\n    }\n    regs[RA] += consts[RC];\n    inst = *op_p++;\n    unsigned op = inst &amp; 0xff;\n    RA = (inst &gt;&gt; 8) &amp; 0xff;\n    inst &gt;&gt;= 16;\n    MUSTTAIL return op_table[op](ARGS);\n}\n</code></pre>\n<p>生成的汇编指令是：</p>\n<pre><code>ADDVN:                                  # @ADDVN\n        movzx   eax, dh\n        cmp     dword ptr [r9 + 8*rax + 4], -12\n        jae     .LBB0_1\n        movzx   eax, dl\n        movsd   xmm0, qword ptr [r8 + 8*rax]    # xmm0 = mem[0],zero\n        mov     eax, edi\n        addsd   xmm0, qword ptr [r9 + 8*rax]\n        movsd   qword ptr [r9 + 8*rax], xmm0\n        mov     edx, dword ptr [rcx]\n        add     rcx, 4\n        movzx   eax, dl\n        movzx   edi, dh\n        shr     edx, 16\n        mov     rax, qword ptr [rsi + 8*rax]\n        jmp     rax                             # TAILCALL\n.LBB0_1:\n        jmp     fallback\n</code></pre>\n<p>我在这里看到的唯一改进机会，除了前面提到的 <code>jne fallback</code> 问题之外，是编译器出于某种原因不想生成 <code>jmp qword ptr [rsi + 8*rax]</code>。相反，它更喜欢加载到 <code>rax</code> 中，然后跟随 <code>jmp rax</code>。这些是轻微的代码生成问题，希望可以在 Clang 中通过并不太大的工作量来解决。</p>\n<h2>限制</h2>\n<p>这种方法最大的问题之一是，如果存在任何非尾调用，这些漂亮的汇编序列就会被灾难性地悲观化。任何非尾调用都会导致创建堆栈帧，并且大量数据会溢出到堆栈中。</p>\n<pre><code class=\"language-c\">#define PARAMS unsigned RA, void *table, unsigned inst, \\\n               int *op_p, double *consts, double *regs\n#define ARGS RA, table, inst, op_p, consts, regs\ntypedef void (*op_func)(PARAMS);\nvoid fallback(PARAMS);\n\n#define UNLIKELY(x) __builtin_expect(x, 0)\n#define MUSTTAIL __attribute__((musttail))\n\nvoid ADDVN(PARAMS) {\n    op_func *op_table = table;\n    unsigned RC = inst &amp; 0xff;\n    unsigned RB = (inst &gt;&gt; 8) &amp; 0xff;\n    unsigned type;\n    memcpy(&amp;type, (char*)&amp;regs[RB] + 4, 4);\n    if (UNLIKELY(type &gt; -13)) {\n        // 当我们省略 &quot;return&quot; 时，事情会变得非常糟糕。\n        fallback(ARGS);\n    }\n    regs[RA] += consts[RC];\n    inst = *op_p++;\n    unsigned op = inst &amp; 0xff;\n    RA = (inst &gt;&gt; 8) &amp; 0xff;\n    inst &gt;&gt;= 16;\n    MUSTTAIL return op_table[op](ARGS);\n}\n</code></pre>\n<p>这导致了非常不幸的结果：</p>\n<pre><code>ADDVN:                                  # @ADDVN\n        push    rbp\n        push    r15\n        push    r14\n        push    r13\n        push    r12\n        push    rbx\n        push    rax\n        mov     r15, r9\n        mov     r14, r8\n        mov     rbx, rcx\n        mov     r12, rsi\n        mov     ebp, edi\n        movzx   eax, dh\n        cmp     dword ptr [r9 + 8*rax + 4], -12\n        jae     .LBB0_1\n.LBB0_2:\n        movzx   eax, dl\n        movsd   xmm0, qword ptr [r14 + 8*rax]   # xmm0 = mem[0],zero\n        mov     eax, ebp\n        addsd   xmm0, qword ptr [r15 + 8*rax]\n        movsd   qword ptr [r15 + 8*rax], xmm0\n        mov     edx, dword ptr [rbx]\n        add     rbx, 4\n        movzx   eax, dl\n        movzx   edi, dh\n        shr     edx, 16\n        mov     rax, qword ptr [r12 + 8*rax]\n        mov     rsi, r12\n        mov     rcx, rbx\n        mov     r8, r14\n        mov     r9, r15\n        add     rsp, 8\n        pop     rbx\n        pop     r12\n        pop     r13\n        pop     r14\n        pop     r15\n        pop     rbp\n        jmp     rax                             # TAILCALL\n.LBB0_1:\n        mov     edi, ebp\n        mov     rsi, r12\n        mov     r13d, edx\n        mov     rcx, rbx\n        mov     r8, r14\n        mov     r9, r15\n        call    fallback\n        mov     edx, r13d\n        jmp     .LBB0_2\n</code></pre>\n<p>为了避免这种情况，我们尝试遵循一个规则，即只通过内联或尾调用来调用其他函数。如果一个操作有多个点可能发生不寻常的情况，而这些情况不是错误，这可能会变得很烦人。例如，当我们解析 protobuf 时，快速而常见的情况是 varint 只有一个字节长，但更长的 varint 不是错误。如果回退代码太复杂，那么在内联中处理不寻常的情况可能会影响快速路径的质量。但尾调用到回退函数没有简单的方法在处理完不寻常情况后恢复操作，因此回退函数必须能够向前推进并完成操作。这会导致代码重复和复杂性。</p>\n<p>理想情况下，这个问题可以通过在回退函数中添加 <a href=\"https://clang.llvm.org/docs/AttributeReference.html#preserve-most\"><code>__attribute__((preserve_most))</code></a>，然后正常调用它们，而不进行尾调用来解决。<code>preserve_most</code> 属性使被调用者负责几乎所有寄存器的保存，这将寄存器溢出的成本转移到我们想要的回退函数中。我们对此属性进行了一些实验，但遇到了一些神秘的问题，我们无法弄清楚是什么原因造成的。这可能是我们的错误；重新审视这一点是未来的工作。<em>[<strong>更新：2023-03-14:</strong> 事实上，这是一个 Clang 中的 bug，<a href=\"https://reviews.llvm.org/D141020\">两个月前已修复</a>]</em></p>\n<p>另一个主要限制是 <code>musttail</code> 不可移植。我非常希望这个属性能流行起来，传播到 GCC、Visual C++ 和其他流行的编译器，甚至有一天能被标准化。但那一天还很遥远，那么在此期间该怎么办？</p>\n<p>当 <code>musttail</code> 不可用时，我们需要为每个概念上的循环迭代执行至少一个真正的 <code>return</code>，而不进行尾调用。我们还没有在 upb 中实现这个回退，但我预计它将涉及一个宏，该宏根据 <code>musttail</code> 的可用性，要么尾调用到 dispatch，要么只是返回。</p>\n","tags":["c"]},{"id":"css-vertical-center","url":"https://yieldray.fun/posts/css-vertical-center","title":"CSS finally adds vertical centering in 2024","date_published":"2024-08-24T22:27:43.000Z","date_modified":"2024-08-24T22:27:43.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://build-your-own.org/blog/20240813_css_vertical_center/\">https://build-your-own.org/blog/20240813_css_vertical_center/</a></p>\n</blockquote>\n<p><code>align-content</code> 在 2024 年的默认布局中起作用，允许使用<strong>一个 CSS 属性</strong>进行垂直居中。</p>\n<pre><code>&lt;div style=&quot;align-content: center; height: 100px;&quot;&gt;\n  &lt;code&gt;align-content&lt;/code&gt; just works!\n&lt;/div&gt;\n</code></pre>\n<p><code>align-content</code> 就是这么好用！</p>\n<p><a href=\"https://caniuse.com/mdn-css_properties_align-content_block_context\">自何时起支持</a>:<br><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACaklEQVR42nWTA6xsVxhG17/3mXm2bds2w96oRlgzqo34IqxtG1GNsDbmPtvG+b/OHtRdx1jfduAflGaOXfrrrHHNpTkTSq1zJ2rD/Em+YcGUzzctmvrQlrlTh/IPrPTnu66etbkZ7MoQAsFM6WQhYGaVI6SD2HwknLwV2Atgn1cDunYMxXeDhSkWLFGXFGLSUOU5BQcD48sj8mUpJAB0DIVU8lSBUcEQUEmo3lBNxAQGYWrHLCs7yXqqYeglb2wuLf1uX7XaZlYYMIBeN93Ftp6jiJ06MSB/C//5anS0JENIMsMl1/IswM3Pze/FrF8P0PG4rNB/AN7yHGe/KT799RhwjEHd5vLc+R/S75fFpqOtYE6FQEPwnKnbOxV4Y1p3Ej2uuppTHz9Zlo9TZ8OenKueN+K4uzGLYBkQMcIpAWmqy/XalK5s71xg9+RlbCwL/yQFbjwxHiXRgkRmsjA0uDtyt4NF4/4FPeQHD/B/dOnYDUJERCNUaxIkb1XCnc+GtbdthfXMG1Hkn0zon9Hp8DsYGVZrAha/SH3wslwmCbm47v1GbmuIFaFGObBA85kF8l9vRRYQAbOIEb+wifetXZrDuxIgIaB/h95cN/9CZvedx4a9BzjoJT79/EYu6/oVIKQcQ8Tch9mY+9YANMl1BQJJhoRLAiHHBhRP8tiobQwsngAc5JjUDFwVAI4dPHarXF/Wm+IuSPd59fm83ocY1FbCIiKi1PY83gpgQ5uWUqNrzOItolYThMoMLLq9N2UXIMCR5y3ZiRO31BdToEbtxZV+0oe59IhcX0jisgFHwGKrCC0ylqV/6nLid9CxMOX4sHSyAAAAAElFTkSuQmCC\" alt=\"支持情况\"> Chrome: 123 | <img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAZ0lEQVR42q2SAQbAMBRD/2VztF1p18lsiCJ7Nhqiqt7ztZ2aw659izVeQWuqwOc8Z1UAMEoiWEpwkRQBiALf61eYJQj3BraKgECeoIuyxwmg++/ACmwLXgJh0T/gdgFLGAYJwpw/4AWWCw/0v3nNDwAAAABJRU5ErkJggg==\" alt=\"支持情况\"> Firefox: 125 | <img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJ1BMVEUAAAAQfd0fhvkSrvH/UVAGwudtnampc33RpKe23ubx8fGQyt1mq9Ygasa0AAAAAXRSTlMAQObYZgAAAGdJREFUeNpjYGBgFBQUYADRQkpKiiCWoBIQCIIFjI2VHAUYGJWMQ0NTlAQYhJRMQ8PclBQZhJRDQ0tSjcCMCPdQCGNWK4QRvTIUzFA6tTU0VEkRqH1NaGgwUDujlBJIQABmBcJSuDMAytIVRxPqOxAAAAAASUVORK5CYII=\" alt=\"支持情况\"> Safari: 17.4</p>\n<h3>新功能</h3>\n<p>CSS 对齐的现状是 <em>flexbox</em> 或 <em>grid</em>，因为 <code>align-content</code> 在默认布局（<em>流</em>）中不起作用。在 2024 年，浏览器已经<a href=\"https://web.dev/blog/align-content-block\">实现</a>了 <em>流布局</em> 的 <code>align-content</code>。这有一些优势：</p>\n<ul>\n<li>你不需要 flexbox 或 grid，只需要一个 CSS 属性来对齐。</li>\n<li>因此，内容不需要包装在 div 中。</li>\n</ul>\n<pre><code>&lt;!-- Works --&gt;\n&lt;div style=&quot;display: grid; align-content: center;&quot;&gt;\n  Content.\n&lt;/div&gt;\n</code></pre>\n<pre><code>&lt;!-- FAIL! --&gt;\n&lt;div style=&quot;display: grid; align-content: center;&quot;&gt;\n  Content with *multiple* nodes.\n&lt;/div&gt;\n</code></pre>\n<pre><code>&lt;!-- Works with the content wrapper --&gt;\n&lt;div style=&quot;display: grid; align-content: center;&quot;&gt;\n  &lt;div&gt; &lt;!-- The extra wrapper --&gt;\n    Content with *multiple* nodes.\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<pre><code>&lt;!-- Works without the content wrapper --&gt;\n&lt;div style=&quot;align-content: center;&quot;&gt;\n  Content with *multiple* nodes.\n&lt;/div&gt;\n</code></pre>\n<p>令人惊叹的是，CSS 经过几十年的发展，终于拥有了一个<strong>单个属性</strong>来控制垂直对齐！</p>\n<h3>垂直居中——历史</h3>\n<p>浏览器很有趣，像对齐这样的基本需求长期以来都没有简单的答案。以下是在 LibreOffice 中如何居中内容：</p>\n<p><img src=\"https://s2.loli.net/2024/08/24/xvCuYR9fcz5ae8J.png\" alt=\"LibreOffice 居中\"></p>\n<p>以下是在浏览器中如何进行<em>垂直</em>居中（<em>水平</em>居中是另一个主题）：</p>\n<h3>方法 1：表格单元格</h3>\n<p>心智：⭐️⭐️⭐️☆☆</p>\n<p>有四种主要布局：流（默认）、表格、flexbox、grid。如何对齐内容取决于容器的布局。flexbox 和 grid 是比较晚才添加的，因此表格是第一个方法。</p>\n<pre><code>&lt;div style=&quot;display: table;&quot;&gt;\n  &lt;div style=&quot;display: table-cell; vertical-align: middle;&quot;&gt;\n    Content.\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<p>表格可以完全通过 CSS 创建，但很可惜需要这样的间接的方式。</p>\n<h3>方法 2：绝对定位</h3>\n<p>心智：☆☆☆☆☆</p>\n<p>我很不理解。人们总是不断发明更多间接的方法。</p>\n<pre><code>&lt;div style=&quot;position: relative;&quot;&gt;\n  &lt;div style=&quot;position: absolute; top: 50%; transform: translateY(-50%);&quot;&gt;\n    Content.\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<p>此方法使用绝对定位来绕过布局，因为流布局对我们没有帮助：</p>\n<ol>\n<li>用 <code>position: relative</code> 标记参考容器。</li>\n<li>用 <code>position: absolute; top: 50%</code> 将内容的边缘放置在中心。</li>\n<li>用 <code>transform: translateY(-50%)</code> 将内容中心偏移到边缘。</li>\n</ol>\n<h3>方法 3：内联内容</h3>\n<p>心智：☆☆☆☆☆</p>\n<p>虽然流布局对内容对齐没有帮助，但它允许在<em>一行</em>内进行垂直对齐。那么为什么不将一行设置得与容器一样高呢？</p>\n<pre><code>&lt;div class=&quot;container&quot;&gt;\n  ::before\n  &lt;div class=&quot;content&quot;&gt;Content.&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<pre><code>.container::before {\n  content: &#39;&#39;;\n  height: 100%;\n  display: inline-block;\n  vertical-align: middle;\n}\n.content {\n  display: inline-block;\n  vertical-align: middle;\n}\n</code></pre>\n<p>这种方法有一些缺陷：除了牺牲一个伪元素之外，开头还有一个零宽度<a href=\"https://christopheraue.net/design/vertical-align#%3A%3Atext%3Dstrut\">“撑杆”字符</a>，可能会搞砸一些东西。</p>\n<h3>方法 4：单行 flexbox</h3>\n<p>心智：⭐️⭐️⭐️☆☆</p>\n<p>flexbox 在 Web 兴起后 20 年才广泛可用。它有两种模式：单行和多行。在单行模式（默认）下，一行会填满垂直空间，并且 <code>align-items</code> 会对齐行中的内容。</p>\n<pre><code>&lt;div style=&quot;display: flex; align-items: center;&quot;&gt;\n  &lt;div&gt;Content.&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<p>或者，将一行设置为列状，并使用 <code>justify-content</code> 对齐 items。</p>\n<pre><code>&lt;div style=&quot;display: flex; flex-flow: column; justify-content: center;&quot;&gt;\n  &lt;div&gt;Content.&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<h3>方法 5：多行 flexbox</h3>\n<p>心智：⭐️⭐️⭐️☆☆</p>\n<p>在多行 flexbox 中，一行不再填满垂直空间，因此一行（只有一项）可以使用 <code>align-content</code> 对齐。</p>\n<pre><code>&lt;div style=&quot;display: flex; flex-flow: row wrap; align-content: center;&quot;&gt;\n  &lt;div&gt;Content.&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<h3>方法 6：网格内容</h3>\n<p>心智：⭐️⭐️⭐️⭐️☆</p>\n<p>网格出现得更晚。对齐变得更简单。</p>\n<pre><code>&lt;div style=&quot;display: grid; align-content: center;&quot;&gt;\n  &lt;div&gt;Content.&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<h3>方法 7：网格单元格</h3>\n<p>心智：⭐️⭐️⭐️⭐️☆</p>\n<p>请注意与上一个方法的细微差别：</p>\n<ul>\n<li><code>align-content</code> 将<em>单元格</em>居中到<em>容器</em>。</li>\n<li><code>align-items</code> 将<em>内容</em>居中到<em>单元格</em>，而<em>单元格</em>会拉伸以适应<em>容器</em>。</li>\n</ul>\n<pre><code>&lt;div style=&quot;display: grid; align-items: center;&quot;&gt;\n  &lt;div&gt;Content.&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<p>似乎有许多方法可以做同一件事。</p>\n<h3>方法 8：自动边距</h3>\n<p>心智：⭐️⭐️⭐️☆☆</p>\n<p>在流布局中，<code>margin: auto</code> 可以水平居中，但不能垂直居中。flexbox 和 grid 没有这种缺陷。</p>\n<pre><code>&lt;div style=&quot;display: grid;&quot;&gt;\n  &lt;div style=&quot;margin-block: auto;&quot;&gt;\n    Content.\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<p>尽管如此，边距也被设计用于控制对齐，这一点让我感到困惑。</p>\n<h3>方法 9：本文中的方法（2024 年）</h3>\n<p>心智：⭐️⭐️⭐️⭐️⭐️</p>\n<p>为什么浏览器一开始没有添加这个功能？</p>\n<pre><code>&lt;div style=&quot;align-content: center;&quot;&gt;\n  &lt;code&gt;align-content&lt;/code&gt; just works!\n&lt;/div&gt;\n</code></pre>\n<p>表格单元格（<a href=\"#%E6%96%B9%E6%B3%95-1%E8%A1%A8%E6%A0%BC%E5%8D%95%E5%85%83%E6%A0%BC\">方法 1</a>），就像这种方法一样，也不需要内容包装器（尽管它需要表格包装器）。我们又回到了起点！</p>\n<h3>总结</h3>\n<p>所有垂直居中方法都可以在这个<a href=\"https://codepen.io/byo-books/pen/Porpmab?editors=1000\">Codepen</a> 中找到。</p>\n<h3>进入二维</h3>\n<p>是否有一个<strong>单个属性</strong>用于<em>水平</em>对齐？<code>align-content</code> 的对应项是什么？让我们看看各种对齐属性：</p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>flow</th>\n<th>flexbox</th>\n<th>grid</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>align-content</code></td>\n<td>block axis</td>\n<td>cross axis (line)</td>\n<td>block axis (grid)</td>\n</tr>\n<tr>\n<td><code>justify-content</code></td>\n<td>no effect</td>\n<td>main axis</td>\n<td>inline axis (grid)</td>\n</tr>\n<tr>\n<td><code>align-items</code></td>\n<td>no effect</td>\n<td>cross axis (item)</td>\n<td>block axis (cell)</td>\n</tr>\n<tr>\n<td><code>justify-items</code></td>\n<td>no effect</td>\n<td>no effect</td>\n<td>inline axis (cell)</td>\n</tr>\n</tbody></table>\n<h3>背景：CSS 轴术语</h3>\n<p><em>block 轴</em> 通常是<em>垂直</em>的，而 <em>inline 轴</em> 是<em>水平</em>的。需要这些术语是因为<a href=\"https://build-your-own.org/visual_css/2p40_box_model#:~:text=Logical%20properties\">vertical <code>writing-mode</code></a> 是存在的，因此<strong>block 轴和 inline 轴相对于文本方向</strong>。这类似于<em>主轴</em>和<em>交叉轴</em>相对于 flexbox items 方向。</p>\n<style>#svg1{max-width:500px;display:block;margin:0 auto}</style>\n<svg version=\"1.1\" id=\"svg1\" viewBox=\"0 0 385.51181 143.62206\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:svg=\"http://www.w3.org/2000/svg\">\n  <defs id=\"defs1\">\n    <clipPath clipPathUnits=\"userSpaceOnUse\" id=\"clipPath3\">\n      <path d=\"M 0,0.028 H 289.105 V 107.716 L 0,107.716 Z\" clip-rule=\"evenodd\" id=\"path3\"></path>\n    </clipPath>\n  </defs>\n  <g id=\"g1\">\n    <g id=\"g2\">\n      <path id=\"path2\" d=\"M 0,107.716 H 289.106 V 0.028 L 0,0.028 Z\" style=\"fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\" clip-path=\"url(#clipPath3)\"></path>\n    </g>\n    <g id=\"g3\">\n      <path id=\"path4\" d=\"M 41.102,19.842 H 19.843 v 42.52 h 42.519 v -42.52 z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g4\">\n      <path id=\"path5\" d=\"M 19.843,70.866 H 68.4\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path6\" d=\"m 68.031,73.7 5.67,-2.834 -5.67,-2.835 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g6\">\n      <text id=\"text6\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,26.457333,39.346047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan6\">inline axis</tspan></text>\n    </g>\n    <g id=\"g7\">\n      <path id=\"path7\" d=\"M 70.866,62.362 V 13.804\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path8\" d=\"m 73.701,14.173 -2.835,-5.67 -2.835,5.67 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g8\">\n      <text id=\"text8\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,60.472714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan8\">b</tspan></text>\n      <text id=\"text9\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,67.200714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan9\">l</tspan></text>\n      <text id=\"text10\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,73.928714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan10\">o</tspan></text>\n      <text id=\"text11\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,80.618047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan11\">c</tspan></text>\n      <text id=\"text12\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,87.346047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan12\">k</tspan></text>\n      <text id=\"text13\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,100.80071)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan13\">a</tspan></text>\n      <text id=\"text14\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,107.52871)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan14\">x</tspan></text>\n      <text id=\"text15\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,114.21805)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan15\">i</tspan></text>\n      <text id=\"text16\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,104.27733,120.94605)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan16\">s</tspan></text>\n    </g>\n    <g id=\"g16\">\n      <path id=\"path16\" d=\"M 25.512,53.858 H 56.693\" style=\"fill:none;stroke:#666666;stroke-width:2.8346;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g17\">\n      <path id=\"path17\" d=\"M 25.512,45.354 H 56.693\" style=\"fill:none;stroke:#666666;stroke-width:2.8346;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g18\">\n      <path id=\"path18\" d=\"M 25.512,36.85 H 43.228\" style=\"fill:none;stroke:#666666;stroke-width:2.8346;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path19\" d=\"M 42.661,41.102 48.189,36.85 42.661,32.598 Z\" style=\"fill:#666666;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g19\">\n      <path id=\"path20\" d=\"M 120.472,19.842 H 99.213 v 42.52 h 42.519 v -42.52 z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g20\">\n      <path id=\"path21\" d=\"m 101.679,70.866 h 48.557\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path22\" d=\"m 102.047,68.031 -5.669,2.835 5.669,2.834 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g22\">\n      <text id=\"text22\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,132.284,39.346047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan22\">block axis</tspan></text>\n    </g>\n    <g id=\"g23\">\n      <path id=\"path23\" d=\"M 150.236,62.362 V 13.804\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path24\" d=\"m 153.071,14.173 -2.835,-5.67 -2.834,5.67 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g24\">\n      <text id=\"text24\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,60.472714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan24\">i</tspan></text>\n      <text id=\"text25\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,67.200714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan25\">n</tspan></text>\n      <text id=\"text26\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,73.928714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan26\">l</tspan></text>\n      <text id=\"text27\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,80.618047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan27\">i</tspan></text>\n      <text id=\"text28\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,87.346047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan28\">n</tspan></text>\n      <text id=\"text29\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,94.072714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan29\">e</tspan></text>\n      <text id=\"text30\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,107.52871)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan30\">a</tspan></text>\n      <text id=\"text31\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,114.21805)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan31\">x</tspan></text>\n      <text id=\"text32\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,120.94605)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan32\">i</tspan></text>\n      <text id=\"text33\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,210.104,127.67271)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan33\">s</tspan></text>\n    </g>\n    <g id=\"g33\">\n      <path id=\"path33\" d=\"M 133.228,25.511 V 56.692\" style=\"fill:none;stroke:#666666;stroke-width:2.8346;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g34\">\n      <path id=\"path34\" d=\"M 124.724,56.692 V 25.511\" style=\"fill:none;stroke:#666666;stroke-width:2.8346;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g35\">\n      <path id=\"path35\" d=\"M 116.22,56.692 V 41.81\" style=\"fill:none;stroke:#666666;stroke-width:2.8346;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path36\" d=\"m 120.472,42.377 -4.252,-5.527 -4.251,5.527 z\" style=\"fill:#666666;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g36\">\n      <path id=\"path37\" d=\"m 188.957,59.499 h 0.029 c -0.34,0 -0.68,-0.085 -0.964,-0.255 -0.283,-0.171 -0.51,-0.397 -0.68,-0.681 -0.17,-0.283 -0.255,-0.623 -0.255,-0.964 v -10.403 0 c 0,-0.34 0.085,-0.68 0.255,-0.963 0.17,-0.284 0.397,-0.511 0.68,-0.681 0.284,-0.17 0.624,-0.255 0.964,-0.255 h 7.54 v 0 c 0.34,0 0.68,0.085 0.964,0.255 0.283,0.17 0.51,0.397 0.68,0.681 0.17,0.283 0.255,0.623 0.255,0.963 v 10.432 -0.029 0 c 0,0.341 -0.085,0.681 -0.255,0.964 -0.17,0.284 -0.397,0.51 -0.68,0.681 -0.284,0.17 -0.624,0.255 -0.964,0.255 z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g37\">\n      <text id=\"text37\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,253.60667,78.123381)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan37\">1</tspan></text>\n    </g>\n    <g id=\"g38\">\n      <path id=\"path38\" d=\"m 226.29,59.527 h 0.028 c -0.425,0 -0.85,-0.113 -1.19,-0.312 -0.369,-0.227 -0.652,-0.51 -0.879,-0.879 -0.199,-0.34 -0.312,-0.765 -0.312,-1.19 v -9.439 -0.029 c 0,-0.397 0.113,-0.822 0.312,-1.162 0.227,-0.369 0.51,-0.652 0.879,-0.879 0.34,-0.198 0.765,-0.312 1.19,-0.312 h 9.439 0.029 c 0.397,0 0.822,0.114 1.162,0.312 0.369,0.227 0.652,0.51 0.879,0.879 0.198,0.34 0.312,0.765 0.312,1.162 v 9.496 -0.028 0 c 0,0.425 -0.114,0.85 -0.312,1.19 -0.227,0.369 -0.51,0.652 -0.879,0.879 -0.34,0.199 -0.765,0.312 -1.162,0.312 z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g39\">\n      <text id=\"text39\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,304.62933,78.086047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan39\">3</tspan></text>\n    </g>\n    <g id=\"g40\">\n      <path id=\"path40\" d=\"m 203.613,59.527 h 0.028 c -0.425,0 -0.85,-0.113 -1.191,-0.312 -0.368,-0.227 -0.652,-0.51 -0.878,-0.879 -0.199,-0.34 -0.312,-0.765 -0.312,-1.19 v -9.439 -0.029 c 0,-0.397 0.113,-0.822 0.312,-1.162 0.226,-0.369 0.51,-0.652 0.878,-0.879 0.341,-0.198 0.766,-0.312 1.191,-0.312 h 15.08 0.029 c 0.396,0 0.822,0.114 1.162,0.312 0.368,0.227 0.652,0.51 0.879,0.879 0.198,0.34 0.311,0.765 0.311,1.162 l -0.028,9.496 0.028,-0.028 v 0 c 0,0.425 -0.113,0.85 -0.311,1.19 -0.227,0.369 -0.511,0.652 -0.879,0.879 -0.34,0.199 -0.766,0.312 -1.162,0.312 z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g41\">\n      <text id=\"text41\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,278.17333,78.086047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan41\">2</tspan></text>\n    </g>\n    <g id=\"g42\">\n      <path id=\"path42\" d=\"m 184.252,70.866 h 65.565\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path43\" d=\"m 249.449,73.7 5.669,-2.834 -5.669,-2.835 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g43\">\n      <text id=\"text43\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,245.66933,39.346047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan43\">main axis</tspan></text>\n    </g>\n    <g id=\"g44\">\n      <path id=\"path44\" d=\"m 189.439,42.519 h 0.029 c -0.425,0 -0.851,-0.113 -1.191,-0.312 -0.368,-0.226 -0.652,-0.51 -0.879,-0.878 -0.198,-0.341 -0.311,-0.766 -0.311,-1.191 V 30.699 30.67 c 0,-0.397 0.113,-0.822 0.311,-1.162 0.227,-0.368 0.511,-0.652 0.879,-0.879 0.34,-0.198 0.766,-0.311 1.191,-0.311 h 12.274 0.028 c 0.397,0 0.822,0.113 1.162,0.311 0.369,0.227 0.652,0.511 0.879,0.879 0.198,0.34 0.312,0.765 0.312,1.162 v 9.496 -0.028 0 c 0,0.425 -0.114,0.85 -0.312,1.191 -0.227,0.368 -0.51,0.652 -0.879,0.878 -0.34,0.199 -0.765,0.312 -1.162,0.312 z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g45\">\n      <text id=\"text45\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,257.38533,100.76338)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan45\">4</tspan></text>\n    </g>\n    <g id=\"g46\">\n      <path id=\"path46\" d=\"m 215.433,19.842 h -31.181 v 42.52 h 62.362 v -42.52 z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:3.40152, 2.55114;stroke-dashoffset:0;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g47\">\n      <path id=\"path47\" d=\"M 255.118,62.362 V 13.804\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n      <path id=\"path48\" d=\"m 257.953,14.173 -2.835,-5.67 -2.835,5.67 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g48\">\n      <text id=\"text48\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,60.472714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan48\">c</tspan></text>\n      <text id=\"text49\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,67.200714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan49\">r</tspan></text>\n      <text id=\"text50\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,73.928714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan50\">o</tspan></text>\n      <text id=\"text51\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,80.618047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan51\">s</tspan></text>\n      <text id=\"text52\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,87.346047)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan52\">s</tspan></text>\n      <text id=\"text53\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,100.80071)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan53\">a</tspan></text>\n      <text id=\"text54\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,107.52871)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan54\">x</tspan></text>\n      <text id=\"text55\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,114.21805)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan55\">i</tspan></text>\n      <text id=\"text56\" xml:space=\"preserve\" transform=\"matrix(0,1.3333333,-1.3333333,0,349.94667,120.94605)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan56\">s</tspan></text>\n    </g>\n    <g id=\"g56\">\n      <text id=\"text57\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,26.457333,14.816714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan57\">text direction (writing-mode)</tspan></text>\n    </g>\n    <g id=\"g57\">\n      <text id=\"text58\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,245.66933,14.816714)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan58\">flexbox direction</tspan></text>\n    </g>\n    <g id=\"g58\">\n      <path id=\"path58\" d=\"M 14.173,90.708 H 274.961\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n    <g id=\"g59\">\n      <path id=\"path59\" d=\"M 172.913,104.881 V 5.669\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,143.62205)\"></path>\n    </g>\n  </g>\n</svg>\n\n<h3>关于命名事物</h3>\n<p>从属性名称可以推断出 CSS 的设计方式：</p>\n<ul>\n<li><code>align-*</code> 通常是垂直的，而 <code>justify-*</code> 通常是水平的。</li>\n<li><code>*-content</code> 和 <code>*-items</code> 控制不同级别的对象？</li>\n</ul>\n<p><code>justify-content</code> 是 <code>align-content</code> 的对应项，在网格布局中很方便，但在流布局中没有效果。<a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/place-content\"><code>place-content</code> 简写</a> 可以同时设置这两个属性。</p>\n<h3>“Align”与“justify”</h3>\n<p>为什么“Align”和“justify”会在 CSS 中指的是轴？<code>justify-*</code> 是受文本对齐的启发吗？这很混乱，考虑到还有 <code>text-align: justify</code>。</p>\n<p>通常情况下，当人们说“Align”时，他们的意思是单个对象的<em>放置</em>，而“justify”是指多个对象的<em>分布</em>。</p>\n<p>虽然在 CSS 中，<code>justify-*</code> 和 <code>align-*</code> 都类似于文本对齐，因为它们接受诸如 <code>space-between</code> 之类的值；但它们只是表示不同的轴！</p>\n<p><strong>如何记忆</strong>：文本对齐是水平的，<code>justify-*</code> 也是如此。</p>\n<h3>“Content”与“items”</h3>\n<p>在 flexbox 中，“Content”和“items”令人困惑：</p>\n<ul>\n<li>主轴：<code>justify-content</code> 控制 items，而 <code>justify-items</code> 没有效果。</li>\n<li>交叉轴：单行模式和多行模式之间的差异。</li>\n</ul>\n<svg version=\"1.1\" id=\"svg1\" viewBox=\"0 0 385.51181 128.50394\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:svg=\"http://www.w3.org/2000/svg\">\n  <defs id=\"defs1\">\n    <clipPath clipPathUnits=\"userSpaceOnUse\" id=\"clipPath3\">\n      <path d=\"M 0,0.028 H 289.105 V 96.377 L 0,96.377 Z\" clip-rule=\"evenodd\" id=\"path3\"></path>\n    </clipPath>\n  </defs>\n  <g id=\"g1\">\n    <g id=\"g2\">\n      <path id=\"path2\" d=\"M 0,96.377 H 289.106 V 0.027 H 0 Z\" style=\"fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\" clip-path=\"url(#clipPath3)\"></path>\n    </g>\n    <g id=\"g3\">\n      <path id=\"path4\" d=\"M 187.087,96.377 V -0.001\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g4\">\n      <path id=\"path5\" d=\"m 214.016,45.353 h -21.26 V 73.7 h 42.52 V 45.353 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:3.40152, 2.55114;stroke-dashoffset:0;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g5\">\n      <path id=\"path6\" d=\"m 209.764,48.188 h -14.173 v 22.677 h 28.346 V 48.188 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g6\">\n      <path id=\"path7\" d=\"M 201.26,70.865 V 48.188\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g7\">\n      <path id=\"path8\" d=\"M 209.764,70.865 V 48.188\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g8\">\n      <path id=\"path9\" d=\"m 195.591,59.527 h 28.346\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g9\">\n      <path id=\"path10\" d=\"m 262.205,45.353 h -21.26 V 73.7 h 42.52 V 45.353 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:3.40152, 2.55114;stroke-dashoffset:0;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g10\">\n      <path id=\"path11\" d=\"M 266.457,48.188 H 252.283 V 70.865 H 280.63 V 48.188 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g11\">\n      <path id=\"path12\" d=\"M 257.953,70.865 V 48.188\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g12\">\n      <path id=\"path13\" d=\"M 266.457,70.865 V 48.188\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g13\">\n      <path id=\"path14\" d=\"M 252.283,59.527 H 280.63\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g14\">\n      <path id=\"path15\" d=\"m 214.016,5.668 h -21.26 v 28.347 h 42.52 V 5.668 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:3.40152, 2.55114;stroke-dashoffset:0;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g15\">\n      <path id=\"path16\" d=\"M 214.016,8.503 H 195.591 V 31.18 h 36.85 V 8.503 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g16\">\n      <path id=\"path17\" d=\"M 215.433,31.18 V 8.503\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g17\">\n      <path id=\"path18\" d=\"m 195.591,19.842 h 36.85\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g18\">\n      <path id=\"path19\" d=\"m 262.205,5.668 h -21.26 v 28.347 h 42.52 V 5.668 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:3.40152, 2.55114;stroke-dashoffset:0;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g19\">\n      <path id=\"path20\" d=\"M 262.205,8.503 H 243.78 V 31.18 h 36.85 V 8.503 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g20\">\n      <path id=\"path21\" d=\"M 263.622,31.18 V 8.503\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g21\">\n      <path id=\"path22\" d=\"m 243.78,19.842 h 36.85\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g22\">\n      <path id=\"path23\" d=\"m 199.361,28.346 h 0.028 c -0.17,0 -0.34,-0.057 -0.482,-0.114 -0.142,-0.085 -0.283,-0.227 -0.368,-0.368 -0.057,-0.142 -0.114,-0.312 -0.114,-0.482 v -3.77 -0.029 c 0,-0.141 0.057,-0.312 0.114,-0.453 0.085,-0.142 0.226,-0.284 0.368,-0.369 0.142,-0.056 0.312,-0.113 0.482,-0.113 l 9.439,0.028 0.029,-0.028 c 0.141,0 0.312,0.057 0.453,0.113 0.142,0.085 0.284,0.227 0.369,0.369 0.056,0.141 0.113,0.312 0.113,0.453 v 3.827 -0.028 0 c 0,0.17 -0.057,0.34 -0.113,0.482 -0.085,0.141 -0.227,0.283 -0.369,0.368 -0.141,0.057 -0.312,0.114 -0.453,0.114 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g23\">\n      <path id=\"path24\" d=\"m 199.361,17.007 h 0.028 c -0.17,0 -0.34,-0.057 -0.482,-0.113 -0.142,-0.086 -0.283,-0.227 -0.368,-0.369 -0.057,-0.142 -0.114,-0.312 -0.114,-0.482 v -3.77 -0.028 c 0,-0.142 0.057,-0.312 0.114,-0.454 0.085,-0.142 0.226,-0.283 0.368,-0.368 0.142,-0.057 0.312,-0.114 0.482,-0.114 l 6.605,0.029 0.028,-0.029 c 0.142,0 0.312,0.057 0.454,0.114 0.141,0.085 0.283,0.226 0.368,0.368 0.057,0.142 0.113,0.312 0.113,0.454 v 3.826 -0.028 0 c 0,0.17 -0.056,0.34 -0.113,0.482 -0.085,0.142 -0.227,0.283 -0.368,0.369 -0.142,0.056 -0.312,0.113 -0.454,0.113 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g24\">\n      <path id=\"path25\" d=\"m 218.721,28.346 h 0.029 c -0.085,0 -0.17,-0.029 -0.256,-0.057 -0.056,-0.057 -0.113,-0.114 -0.17,-0.17 -0.028,-0.085 -0.056,-0.17 -0.056,-0.255 v -4.734 0 c 0,-0.085 0.028,-0.17 0.056,-0.255 0.057,-0.057 0.114,-0.114 0.17,-0.17 0.086,-0.029 0.171,-0.057 0.256,-0.057 h 1.899 v 0 c 0.085,0 0.17,0.028 0.255,0.057 0.057,0.056 0.113,0.113 0.17,0.17 0.028,0.085 0.057,0.17 0.057,0.255 v 4.762 -0.028 0 c 0,0.085 -0.029,0.17 -0.057,0.255 -0.057,0.056 -0.113,0.113 -0.17,0.17 -0.085,0.028 -0.17,0.057 -0.255,0.057 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g25\">\n      <path id=\"path26\" d=\"m 219.203,17.007 h 0.028 c -0.17,0 -0.34,-0.057 -0.481,-0.113 -0.142,-0.086 -0.284,-0.227 -0.369,-0.369 -0.057,-0.142 -0.113,-0.312 -0.113,-0.482 v -3.77 -0.028 c 0,-0.142 0.056,-0.312 0.113,-0.454 0.085,-0.142 0.227,-0.283 0.369,-0.368 0.141,-0.057 0.311,-0.114 0.481,-0.114 l 6.605,0.029 0.029,-0.029 c 0.141,0 0.311,0.057 0.453,0.114 0.142,0.085 0.284,0.226 0.369,0.368 0.056,0.142 0.113,0.312 0.113,0.454 v 3.826 -0.028 0 c 0,0.17 -0.057,0.34 -0.113,0.482 -0.085,0.142 -0.227,0.283 -0.369,0.369 -0.142,0.056 -0.312,0.113 -0.453,0.113 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g26\">\n      <path id=\"path27\" d=\"m 250.384,28.346 v 0 c -0.141,0 -0.312,-0.057 -0.453,-0.114 -0.142,-0.085 -0.284,-0.227 -0.369,-0.368 -0.056,-0.142 -0.113,-0.312 -0.113,-0.482 v -3.77 -0.029 c 0,-0.141 0.057,-0.312 0.113,-0.453 0.085,-0.142 0.227,-0.284 0.369,-0.369 0.141,-0.056 0.312,-0.113 0.453,-0.113 l 9.468,0.028 0.028,-0.028 c 0.142,0 0.312,0.057 0.454,0.113 0.142,0.085 0.283,0.227 0.368,0.369 0.057,0.141 0.114,0.312 0.114,0.453 v 3.827 -0.028 0 c 0,0.17 -0.057,0.34 -0.114,0.482 -0.085,0.141 -0.226,0.283 -0.368,0.368 -0.142,0.057 -0.312,0.114 -0.454,0.114 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g27\">\n      <path id=\"path28\" d=\"m 275.414,28.346 h 0.029 c -0.086,0 -0.171,-0.029 -0.256,-0.057 -0.056,-0.057 -0.113,-0.114 -0.17,-0.17 -0.028,-0.085 -0.056,-0.17 -0.056,-0.255 v -4.734 0 c 0,-0.085 0.028,-0.17 0.056,-0.255 0.057,-0.057 0.114,-0.114 0.17,-0.17 0.085,-0.029 0.17,-0.057 0.256,-0.057 h 1.899 v 0 c 0.085,0 0.17,0.028 0.255,0.057 0.057,0.056 0.113,0.113 0.17,0.17 0.028,0.085 0.057,0.17 0.057,0.255 v 4.762 -0.028 0 c 0,0.085 -0.029,0.17 -0.057,0.255 -0.057,0.056 -0.113,0.113 -0.17,0.17 -0.085,0.028 -0.17,0.057 -0.255,0.057 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g28\">\n      <path id=\"path29\" d=\"m 270.227,17.007 h 0.028 c -0.17,0 -0.34,-0.057 -0.482,-0.113 -0.142,-0.086 -0.283,-0.227 -0.368,-0.369 -0.057,-0.142 -0.114,-0.312 -0.114,-0.482 v -3.77 -0.028 c 0,-0.142 0.057,-0.312 0.114,-0.454 0.085,-0.142 0.226,-0.283 0.368,-0.368 0.142,-0.057 0.312,-0.114 0.482,-0.114 l 6.605,0.029 0.028,-0.029 c 0.142,0 0.312,0.057 0.454,0.114 0.141,0.085 0.283,0.226 0.368,0.368 0.057,0.142 0.114,0.312 0.114,0.454 v 3.826 -0.028 0 c 0,0.17 -0.057,0.34 -0.114,0.482 -0.085,0.142 -0.227,0.283 -0.368,0.369 -0.142,0.056 -0.312,0.113 -0.454,0.113 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g29\">\n      <path id=\"path30\" d=\"m 253.219,17.007 h 0.028 c -0.17,0 -0.34,-0.057 -0.482,-0.113 -0.141,-0.086 -0.283,-0.227 -0.368,-0.369 -0.057,-0.142 -0.114,-0.312 -0.114,-0.482 v -3.77 -0.028 c 0,-0.142 0.057,-0.312 0.114,-0.454 0.085,-0.142 0.227,-0.283 0.368,-0.368 0.142,-0.057 0.312,-0.114 0.482,-0.114 l 6.605,0.029 0.028,-0.029 c 0.142,0 0.312,0.057 0.454,0.114 0.142,0.085 0.283,0.226 0.368,0.368 0.057,0.142 0.114,0.312 0.114,0.454 v 3.826 -0.028 0 c 0,0.17 -0.057,0.34 -0.114,0.482 -0.085,0.142 -0.226,0.283 -0.368,0.369 -0.142,0.056 -0.312,0.113 -0.454,0.113 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g30\">\n      <path id=\"path31\" d=\"M 111.969,45.353 H 90.709 V 73.7 h 42.519 V 45.353 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:3.40152, 2.55114;stroke-dashoffset:0;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g31\">\n      <path id=\"path32\" d=\"m 94.961,70.865 v 0 c -0.255,0 -0.482,-0.057 -0.709,-0.198 -0.227,-0.114 -0.397,-0.284 -0.51,-0.51 C 93.6,69.93 93.543,69.703 93.543,69.448 V 63.779 63.75 c 0,-0.255 0.057,-0.482 0.199,-0.708 0.113,-0.227 0.283,-0.397 0.51,-0.511 0.227,-0.141 0.454,-0.198 0.709,-0.198 h 5.669 0.028 c 0.255,0 0.482,0.057 0.709,0.198 0.227,0.114 0.397,0.284 0.51,0.511 0.142,0.226 0.199,0.453 0.199,0.708 v 5.698 0 0 c 0,0.255 -0.057,0.482 -0.199,0.709 -0.113,0.226 -0.283,0.396 -0.51,0.51 -0.227,0.141 -0.454,0.198 -0.709,0.198 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g32\">\n      <path id=\"path33\" d=\"m 106.299,70.865 v 0 c -0.255,0 -0.482,-0.057 -0.708,-0.198 -0.227,-0.114 -0.397,-0.284 -0.511,-0.51 -0.141,-0.227 -0.198,-0.454 -0.198,-0.709 V 63.779 63.75 c 0,-0.255 0.057,-0.482 0.198,-0.708 0.114,-0.227 0.284,-0.397 0.511,-0.511 0.226,-0.141 0.453,-0.198 0.708,-0.198 h 14.173 0.029 c 0.255,0 0.482,0.057 0.708,0.198 0.227,0.114 0.397,0.284 0.511,0.511 0.141,0.226 0.198,0.453 0.198,0.708 v 5.698 0 0 c 0,0.255 -0.057,0.482 -0.198,0.709 -0.114,0.226 -0.284,0.396 -0.511,0.51 -0.226,0.141 -0.453,0.198 -0.708,0.198 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g33\">\n      <path id=\"path34\" d=\"m 95.414,59.527 h 0.029 c -0.341,0 -0.652,-0.085 -0.964,-0.256 -0.284,-0.17 -0.51,-0.396 -0.681,-0.68 -0.17,-0.312 -0.255,-0.623 -0.255,-0.964 v -7.568 0 c 0,-0.34 0.085,-0.652 0.255,-0.964 0.171,-0.283 0.397,-0.51 0.681,-0.68 0.312,-0.17 0.623,-0.255 0.964,-0.255 h 18.907 v 0 c 0.34,0 0.652,0.085 0.963,0.255 0.284,0.17 0.511,0.397 0.681,0.68 0.17,0.312 0.255,0.624 0.255,0.964 v 7.597 -0.029 0 c 0,0.341 -0.085,0.652 -0.255,0.964 -0.17,0.284 -0.397,0.51 -0.681,0.68 -0.311,0.171 -0.623,0.256 -0.963,0.256 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g34\">\n      <path id=\"path35\" d=\"M 160.157,45.353 H 138.898 V 73.7 h 42.519 V 45.353 Z\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:3.40152, 2.55114;stroke-dashoffset:0;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g35\">\n      <path id=\"path36\" d=\"m 151.654,70.865 v 0 c -0.256,0 -0.482,-0.057 -0.709,-0.198 -0.227,-0.114 -0.397,-0.284 -0.51,-0.51 -0.142,-0.227 -0.199,-0.454 -0.199,-0.709 V 63.779 63.75 c 0,-0.255 0.057,-0.482 0.199,-0.708 0.113,-0.227 0.283,-0.397 0.51,-0.511 0.227,-0.141 0.453,-0.198 0.709,-0.198 h 5.669 0.028 c 0.255,0 0.482,0.057 0.709,0.198 0.227,0.114 0.397,0.284 0.51,0.511 0.142,0.226 0.199,0.453 0.199,0.708 v 5.698 0 0 c 0,0.255 -0.057,0.482 -0.199,0.709 -0.113,0.226 -0.283,0.396 -0.51,0.51 -0.227,0.141 -0.454,0.198 -0.709,0.198 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g36\">\n      <path id=\"path37\" d=\"m 162.992,70.865 v 0 c -0.255,0 -0.482,-0.057 -0.709,-0.198 -0.226,-0.114 -0.396,-0.284 -0.51,-0.51 -0.142,-0.227 -0.198,-0.454 -0.198,-0.709 V 63.779 63.75 c 0,-0.255 0.056,-0.482 0.198,-0.708 0.114,-0.227 0.284,-0.397 0.51,-0.511 0.227,-0.141 0.454,-0.198 0.709,-0.198 h 14.173 0.029 c 0.255,0 0.482,0.057 0.708,0.198 0.227,0.114 0.397,0.284 0.511,0.511 0.141,0.226 0.198,0.453 0.198,0.708 v 5.698 0 0 c 0,0.255 -0.057,0.482 -0.198,0.709 -0.114,0.226 -0.284,0.396 -0.511,0.51 -0.226,0.141 -0.453,0.198 -0.708,0.198 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g37\">\n      <path id=\"path38\" d=\"m 157.776,59.527 h 0.029 c -0.34,0 -0.652,-0.085 -0.964,-0.256 -0.284,-0.17 -0.51,-0.396 -0.68,-0.68 -0.17,-0.312 -0.255,-0.623 -0.255,-0.964 v -7.568 0 c 0,-0.34 0.085,-0.652 0.255,-0.964 0.17,-0.283 0.396,-0.51 0.68,-0.68 0.312,-0.17 0.624,-0.255 0.964,-0.255 h 18.907 v 0 c 0.34,0 0.652,0.085 0.964,0.255 0.283,0.17 0.51,0.397 0.68,0.68 0.17,0.312 0.255,0.624 0.255,0.964 v 7.597 -0.029 0 c 0,0.341 -0.085,0.652 -0.255,0.964 -0.17,0.284 -0.397,0.51 -0.68,0.68 -0.312,0.171 -0.624,0.256 -0.964,0.256 z\" style=\"fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g38\">\n      <path id=\"path39\" d=\"M 85.039,96.377 V -0.001\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g39\">\n      <path id=\"path40\" d=\"M 0,79.369 H 289.134\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g40\">\n      <path id=\"path41\" d=\"M 0,39.684 H 289.134\" style=\"fill:none;stroke:#000000;stroke-width:0.85038;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1\" transform=\"matrix(1.3333333,0,0,-1.3333333,0,128.50394)\"></path>\n    </g>\n    <g id=\"g41\">\n      <text id=\"text41\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,120.94533,14.81727)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan41\">flexbox</tspan></text>\n    </g>\n    <g id=\"g42\">\n      <text id=\"text42\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,4.9893333,53.519937)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan42\">justify-content</tspan></text>\n    </g>\n    <g id=\"g43\">\n      <text id=\"text43\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,4.9893333,106.43327)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan43\">justify-items</tspan></text>\n    </g>\n    <g id=\"g44\">\n      <text id=\"text44\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,257.008,14.81727)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:10.006px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" y=\"0\" id=\"tspan44\">grid</tspan></text>\n    </g>\n    <g id=\"g45\">\n      <text id=\"text45\" xml:space=\"preserve\" transform=\"matrix(1.3333333,0,0,1.3333333,172.68667,113.4626)\"><tspan style=\"font-variant:normal;font-weight:normal;font-size:25.994px;font-family:'Ubuntu Mono';writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none\" x=\"0\" y=\"0\" id=\"tspan45\">?</tspan></text>\n    </g>\n  </g>\n</svg>\n\n<p><strong>结论</strong>：“items”用于可以<em>单独</em>对齐的东西。在主轴上，flex items 不能单独对齐，因此它是“Content”。</p>\n<h3>为什么 CSS 如此混乱？</h3>\n<p>即使忽略历史遗留问题，CSS 对于我们中的大多数人来说仍然过于混乱。它有数百种命名不佳的属性，每个属性都可能以不直观的方式影响结果。</p>\n<h3>软件项目失控</h3>\n<p>这是一个关于软件设计范例的案例研究：</p>\n<ul>\n<li>Unix：<strong>正交、可组合的基本元素</strong>，可以独立地进行推理。</li>\n<li>CSS（<a href=\"https://news.ycombinator.com/item?id=40130549\"><strong>C</strong>entral <strong>S</strong>oftware <strong>S</strong>ystem</a>）：只需用<strong>更多旋钮</strong>来修改软件。</li>\n</ul>\n<p>早期的万维网只是链接文档。CSS 被用来<em>样式化</em>文档，而不考虑<em>布局</em>。随着时间的推移，CSS 获得了一些随机的布局功能，但没有连贯的愿景。</p>\n<p>通常情况下，在 CSS 中有很多方法可以做某事，但却没有正确的功能来理智地完成它。这篇整个文章都是关于一个新功能，用于更理智地进行垂直对齐，而水平轴仍然不同。</p>\n<p>相比之下，LibreOffice 遵循正交、可组合基本元素的范例：</p>\n<ul>\n<li>对齐是统一的。垂直方向与水平方向没有区别。</li>\n<li>统一的对齐之所以可能，是因为“Align”和“justify”是正交的，而不是混合在一起的。<ul>\n<li>“Align”是<em>容器</em>的属性。</li>\n<li>“justify”是<em>段落</em>的属性。</li>\n</ul>\n</li>\n<li>“Align”和“justify”可以以任何方式组合。</li>\n</ul>\n","tags":["css"]},{"id":"portable-browsers","url":"https://yieldray.fun/posts/portable-browsers","title":"portable-browsers","date_published":"2024-08-20T23:31:33.000Z","date_modified":"2024-08-20T23:31:33.000Z","content_text":"<h1>ungoogled-chromium</h1>\n<p><a href=\"https://ungoogled-software.github.io/\">https://ungoogled-software.github.io/</a></p>\n<p>Google Chromium 去谷歌服务构建版本</p>\n<p>见官方<a href=\"https://github.com/ungoogled-software/ungoogled-chromium#downloads\">下载指引</a>，Windows 直接<a href=\"https://ungoogled-software.github.io/ungoogled-chromium-binaries/\">点击此处</a>即可</p>\n<p>不过数据默认会在 <code>%USERPROFILE%\\AppData\\Local\\Chromium</code> 目录下写入（只测试过 Windows）</p>\n<h1>Chromium</h1>\n<p><a href=\"https://www.chromium.org/Home/\">https://www.chromium.org/Home/</a></p>\n<p>见官方<a href=\"https://www.chromium.org/getting-involved/download-chromium/\">下载指引</a>，或者直接<a href=\"https://download-chromium.appspot.com/\">下载最新版</a></p>\n<p><a href=\"https://www.chromium.org/developers/creating-and-using-profiles/\">命令行选项</a>可改变数据目录<br>参见<a href=\"https://developer.chrome.com/docs/web-platform/chrome-flags\">官方博客</a>、<a href=\"https://chromium.googlesource.com/playground/chromium-org-site/+/master/developers/how-tos/run-chromium-with-flags.md\">文档</a> 和 <a href=\"https://peter.sh/experiments/chromium-command-line-switches/\">命令行 flags 列表</a></p>\n<h1>LibreWolf</h1>\n<p><a href=\"https://librewolf.net/\">https://librewolf.net/</a></p>\n<p>Firefox 隐私增强版</p>\n<p>数据会写入到同目录下的 <code>Profiles</code> 目录</p>\n<p><a href=\"https://librewolf.net/installation/\">下载地址</a></p>\n<h1>QtWeb</h1>\n<p><a href=\"https://www.qtweb.net/\">https://www.qtweb.net/</a></p>\n<p>基于 Qt 和 Webkit 的浏览器</p>\n<p><a href=\"https://www.qtweb.net/download.html\">下载地址</a></p>\n<p>最后 commit 在 2013 年，实际上已经无法使用</p>\n<h1>Vivaldi</h1>\n<p><a href=\"https://help.vivaldi.com/desktop/install-update/standalone-version-of-vivaldi/\">https://help.vivaldi.com/desktop/install-update/standalone-version-of-vivaldi/</a></p>\n<p>Windows 安装时可指定独立安装</p>\n<h1>Brave</h1>\n<p><a href=\"https://github.com/brave/brave-browser/releases\">https://github.com/brave/brave-browser/releases</a></p>\n<p>官网未提供，但 Release 有提供</p>\n<h1>其它</h1>\n<p><a href=\"https://portableapps.com/apps/internet\">https://portableapps.com/apps/internet</a><br><a href=\"https://portapps.io/apps/\">https://portapps.io/apps/</a></p>\n","tags":["note"]},{"id":"micro-frontend","url":"https://yieldray.fun/posts/micro-frontend","title":"微前端技术概览","date_published":"2024-08-17T21:55:55.000Z","date_modified":"2024-08-17T21:55:55.000Z","content_text":"<p><a href=\"https://micro-frontends.org/\">微前端</a>是一种前端网页开发模式，允许将单个应用程序构建自不同的独立构建模块。<br>它类似于微服务架构，但适用于用 JavaScript 编写的客户端单页应用（SPA）。它是针对多个前端应用程序<strong>分解</strong>和<strong>路由</strong>的解决方案。</p>\n<p>可以想到，一个复杂的单体前端应用（当然，后端应用也是如此），代码上要基于一套代码去支持多个业务，这限制了新业务的技术栈，还会困于已有的技术债。</p>\n<p>微前端和微服务的软件工程思路大致一致，将应用根据团队或业务模块做拆解（即<strong>单体架构</strong>→<strong>垂直架构</strong>），不过前端应用本质上是一个单独的 GUI 应用，技术上实现子应用的独立（解耦）并不容易。</p>\n<p>此外，对于这样一个复杂的单体前端应用，其背后的后端应用也是数量庞大的微服务集群。微前端可以对应微服务，最终团队实现从UI实现到数据库设计这样<em>端到端</em>的<em>跨职能人员</em>构成。</p>\n<hr>\n<p><a href=\"https://micro-frontends.org/\">Micro Frontends - extending the microservice idea to frontend development</a> 一文探讨了微前端架构下如何实现不同团队开发的页面模块的组合和交互，并提出了使用 Custom Elements、DOM 事件和 Server Side Includes 等技术方案，以解决页面渲染、数据获取、跨团队通信等技术难题。</p>\n<blockquote>\n<p>抛开<strong>前后端</strong>如何集成来自于<strong>不同开发框架</strong>的代码这个问题本身，还有很多其他值得讨论的话题：用来<strong>隔离js作用域</strong>的机制，避免<strong>css样式冲突</strong>，按需<strong>加载资源</strong>，团队之间<strong>共用资源的共享</strong>，<strong>处理获取数据的流程</strong>以及因此产生的如何通过<strong>更好的加载状态管理</strong>来为用户带来更好的体验。</p>\n</blockquote>\n<p>微前端（MFE）可以通过以下方法实现：</p>\n<ul>\n<li>使用<a href=\"https://webpack.js.org/concepts/module-federation/\">模块联邦（module federation）</a>或原生联邦（native federation）（例如 <a href=\"https://developer.mozilla.org/docs/Web/API/Web_components\">Web Components</a> 和 <a href=\"https://developer.mozilla.org/docs/Web/HTML/Element/iframe\">iframe</a>）</li>\n<li>使用微前端框架：<a href=\"https://single-spa.js.org/\">Single-SPA</a>, <a href=\"https://www.piral.io/\">Piral</a>\n, <a href=\"https://qiankun.umijs.org/zh\">qiankun</a>, <a href=\"https://micro-frontends.ice.work/\">icestark</a>, <a href=\"https://alfajs.xconsole.cloud/\">Alfa</a>, <a href=\"https://frint.js.org/\">FrintJS</a>, <a href=\"https://ara-framework.github.io/website/\">Ara Framework</a>, <a href=\"https://github.com/puzzle-js/puzzle-js\">PuzzleJs</a>, <a href=\"https://micro-zoe.github.io/micro-app/\">MicroApp</a>, <a href=\"https://opencomponents.github.io/\">OpenComponents</a>, <a href=\"https://luigi-project.io/\">Luigi</a>, <a href=\"https://wujie-micro.github.io/doc/\">wujie</a>, <a href=\"https://empjs.dev/\">EMP</a>, <a href=\"https://www.garfishjs.org/\">Garfish</a>, <a href=\"https://bit.dev/\">bit</a></li>\n</ul>\n<blockquote>\n<p>注：<a href=\"https://github.com/module-federation/core\">Module Federation</a> 已<a href=\"https://module-federation.io/blog/announcement.html\">发布 2.0</a></p>\n</blockquote>\n<p>注册中心/主应用：整体布局，子应用的注册和加载（生命周期管理）</p>\n<p>隔离 JavaScript：隔离全局环境/副作用（例如通过 Proxy/new Function），例如劫持 DOM，全局事件，样式，localStorage 等，不妥善处理将导致内存泄露</p>\n<p>隔离样式：传统 CSS modules，Shadow DOM 引入的 CSS Scoping 等</p>\n<hr>\n<p>虽然微前端得到了<strong>独立部署</strong>的好处（例如通过模块联邦，子应用只需将自身打包成模块，无需考虑其它应用），但相比于微服务，微前端所有应用共享一个浏览器页面的资源。虽然某些微前端框架/库为我们做了隔离处理，<em>本质上来说</em>所有应用还是在共享资源。<br>与微服务不同，我们必须面对这些问题，导致微前端应用不太能像普通单体应用一样自由。</p>\n<p>此外，微前端可能没有解决代码膨胀的问题（相比于 MPA），因为应用间仍有可能需要 数据传输 和 依赖共享，而运行在浏览器中的微前端根本就不如微服务一般自由（例如通信和隔离）。<br>因此，必须权衡其利弊：例如，需要微前端提供的哪些<strong>必要</strong>能力？使用后收益真的高于多页应用吗？</p>\n<hr>\n<p>iframe有利点：</p>\n<ul>\n<li>真正的环境/DOM隔离，确保页面组件间完全独立</li>\n</ul>\n<p>iframe不利点：</p>\n<ul>\n<li>无法跨 iframe 传递 DOM 事件，需要使用其它方式做事件处理（见下文 iframe 通信方法）</li>\n<li>独立环境需占用更多性能资源，且路由控制可能存在延迟</li>\n</ul>\n<blockquote>\n<p><strong>iframe 通信方法</strong>\n直接调用：</p>\n<ul>\n<li><a href=\"https://developer.mozilla.org/docs/Web/API/Window/frames\">window.frames</a> 或 <a href=\"https://developer.mozilla.org/docs/Web/API/HTMLIFrameElement\">HTMLIFrameElement</a>.contentWindow/contentDocument</li>\n</ul>\n<p>事件通信：</p>\n<ul>\n<li>跨源通信 <a href=\"https://developer.mozilla.org/docs/Web/API/Window/postMessage\">postMessage</a> 和 <a href=\"https://developer.mozilla.org/docs/Web/API/Window/message_event\">Window: message event</a></li>\n<li>利用 localStorage 或 sessionStorage 对应的 <a href=\"https://developer.mozilla.org/docs/Web/API/Window/storage_event\">Window: storage event</a></li>\n<li><a href=\"https://developer.mozilla.org/docs/Web/Events/Creating_and_triggering_events\">创建和触发自定义事件</a></li>\n</ul>\n</blockquote>\n<p>Shadow DOM 有利点：</p>\n<ul>\n<li>主流框架对 Web Components 的积极支持</li>\n<li>CSS scoping 隔离，随标准的推进，能够实现更多复杂的需求（不过需考虑新 CSS 语法）</li>\n<li>使用 WebComponents 就可以直接实现组件跨框架复用</li>\n</ul>\n<p>Shadow DOM 不利点：</p>\n<ul>\n<li>兼容性、少量学习成本</li>\n<li>元素的事件处理上需考虑事件边界（与已有的事件埋点可能存在兼容性问题）</li>\n</ul>\n<hr>\n<p>另见：</p>\n<p><a href=\"https://lianpf.github.io/posts/frontend-develop/microfrontend_framework_compare/\">https://lianpf.github.io/posts/frontend-develop/microfrontend_framework_compare/</a><br><a href=\"https://github.com/empjs/emp/wiki/%E3%80%8A%E4%BB%80%E4%B9%88%E6%98%AF%E5%BE%AE%E5%89%8D%E7%AB%AF%E3%80%8B\">https://github.com/empjs/emp/wiki/%E3%80%8A%E4%BB%80%E4%B9%88%E6%98%AF%E5%BE%AE%E5%89%8D%E7%AB%AF%E3%80%8B</a><br><a href=\"https://www.cnblogs.com/everfind/p/microfrontend.html\">https://www.cnblogs.com/everfind/p/microfrontend.html</a></p>\n","tags":["frontend"]},{"id":"frontend-build-systems","url":"https://yieldray.fun/posts/frontend-build-systems","title":"Exposition of Frontend Build Systems","date_published":"2024-08-04T18:55:55.000Z","date_modified":"2024-08-04T18:55:55.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://sunsetglow.net/posts/frontend-build-systems.html\">https://sunsetglow.net/posts/frontend-build-systems.html</a><br>其它翻译：<a href=\"https://juejin.cn/post/7398480670445895743\">https://juejin.cn/post/7398480670445895743</a></p>\n</blockquote>\n<p>开发者编写 JavaScript；浏览器运行 JavaScript。从根本上说，前端开发无需构建步骤。那么，为何在现代前端开发中我们需要构建步骤呢？</p>\n<p>随着前端代码库的不断扩大，同时开发者的使用体验变得越来越重要，若直接将 JavaScript 源代码发送到客户端会导致两个主要问题：</p>\n<ol>\n<li><p><strong>不支持的语言特性：</strong> 由于 JavaScript 运行在浏览器中，并且市面上存在各种版本的浏览器，每使用一个新的语言特性都会减少能够执行你的 JavaScript 的客户数量。<br>此外，像 JSX 这样的语言扩展并不是有效的 JavaScript，在任何浏览器中都无法运行。</p>\n</li>\n<li><p><strong>性能问题：</strong> 浏览器必须单独请求每个 JavaScript 文件。在一个大型代码库中，这可能会导致数千次 HTTP 请求来渲染一个页面。<br>在过去，在 HTTP/2 出现之前，这也会导致数千次 TLS 握手。</p>\n<blockquote>\n<p>译注：这里的表述有误。在 HTTP/1.1 中，浏览器可以通过持久连接重用同一个 TCP 连接进行多个请求，但仍然会有一定数量的握手。HTTP/2 进一步优化了这种情况，通过单个连接并行传送多个请求，显著减少了握手的开销。</p>\n</blockquote>\n<p> 此外，在所有 JavaScript 都加载完之前，可能需要几次连续的网络往返。例如，如果 <code>index.js</code> 导入 <code>page.js</code>，而 <code>page.js</code> 导入 <code>button.js</code>，那么需要三次连续的网络往返才能完全加载 JavaScript。这被称为瀑布问题。</p>\n<p> 源文件的体积也可能由于长变量名和空白缩进字符而变得不必要的大，增加了带宽使用和网络加载时间。</p>\n</li>\n</ol>\n<p>前端构建系统处理源代码，并生成一个或多个优化后可发送到浏览器的 JavaScript 文件。最终的<em>构建产物</em>通常对人类来说是难以阅读的。</p>\n<h1>构建步骤</h1>\n<p>前端构建系统通常包括三个步骤：转译（transpilation）、打包（bundling）和压缩（minification）。</p>\n<p>有些应用程序可能无需所有的三个步骤。例如，较小的代码库可能不需要打包或压缩，而开发服务器可能为了性能而跳过打包和/或压缩。也可以添加一些自定义步骤。</p>\n<blockquote>\n<p>译注：<br>例如，vite 的开发服务器基于原生 ESM 模块，它通过 esbuild 做<a href=\"https://cn.vitejs.dev/guide/dep-pre-bundling.html\">依赖预打包</a>以减少浏览器请求数量，并进行 CommonJS 到 ESM 的转换。<br>webpack 需要自行配置<a href=\"https://webpack.js.org/guides/development/\">开发模式</a>，若选择 <a href=\"https://webpack.js.org/configuration/dev-server/\">webpack-dev-server</a>，它通过将构建产物置于内存而非硬盘来优化访问速度。</p>\n</blockquote>\n<p>有些工具实现了多个构建步骤。特别是，打包工具通常会实现所有三个步骤，单一的打包工具可能足以构建简单的应用程序。复杂的应用程序可能需要专门的工具来完成每个构建步骤，这些工具提供更强大的功能集。</p>\n<h2>转译（Transpilation）</h2>\n<p>转译通过将使用现代 JavaScript 版本编写的代码转换为较旧的 JavaScript 版本，解决了不支持的语言特性问题。目前，ES6/ES2015 是一个常见的目标。</p>\n<p>框架和工具也可能引入转译步骤。例如，JSX 语法必须转译成 JavaScript。如果某个库提供了 Babel 插件，这通常意味着它需要一个转译步骤。此外，像 TypeScript、CoffeeScript 和 Elm 这样的语言必须转译成 JavaScript。</p>\n<p><a href=\"https://wiki.commonjs.org/wiki/Modules\">CommonJS 模块</a> (CJS) 也必须转译成浏览器兼容的模块系统。在 2018 年浏览器普遍支持 <a href=\"https://exploringjs.com/es6/ch_modules.html\">ES6 Modules</a> (ESM) 之后，转译为 ESM 一般是推荐的。由于 ESM 的导入和导出是静态定义的，因此更容易优化和<a href=\"https://sunsetglow.net/posts/frontend-build-systems.html#tree-shaking\">树摇</a>。</p>\n<p>目前常用的转译器有 Babel、SWC 和 TypeScript 编译器。</p>\n<ol>\n<li><p><strong><a href=\"https://babeljs.io/\">Babel</a></strong>（2014）是一个标准的转译器：使用 JavaScript 编写的单线程转译器。许多需要转译的框架和库通过 Babel 插件完成转译，这就需要 Babel 成为构建过程中不可或缺的一部分。然而，Babel 难以调试，并且常常令人困惑。</p>\n</li>\n<li><p><strong><a href=\"https://swc.rs/\">SWC</a></strong>（2020）是一个使用 Rust 编写的快速多线程转译器。它声称比 Babel 快 20 倍，因此被较新的框架和构建工具所使用。它支持转译 TypeScript 和 JSX。如果你的应用程序不依赖 Babel，SWC 是一个更好的选择。</p>\n</li>\n<li><p><strong><a href=\"https://github.com/microsoft/TypeScript\">TypeScript 编译器 (tsc)</a></strong> 也支持转译 TypeScript 和 JSX。它是 TypeScript 的参考实现，也是唯一全功能的 TypeScript 类型检查器。然而，它非常慢。尽管 TypeScript 应用程序必须通过 TypeScript 编译器类型检查，但对于构建步骤，使用替代的转译器会更高效。</p>\n</li>\n</ol>\n<p>如果你的代码是纯 JavaScript 且使用 ES6 模块，则可能可以跳过转译步骤。</p>\n<p>另一种解决不支持语言特性的方法是使用 polyfill。polyfill 在运行时执行，在执行主应用程序逻辑之前实现缺失的语言特性。然而，这增加了运行时开销，而且某些语言特性无法通过 polyfill 实现。参见 <a href=\"https://github.com/zloirock/core-js\">core-js</a>。</p>\n<p>所有打包工具本质上也是转译器，因为它们解析多个 JavaScript 源文件并生成一个新的打包的 JavaScript 文件。在此过程中，它们可以选择在其生成的 JavaScript 文件中使用哪些语言特性。有些打包工具还能够解析 TypeScript 和 JSX 源文件。如果你的应用程序有简单的转译需求，你可能不需要单独的转译器。</p>\n<h2>打包（Bundling）</h2>\n<p>打包解决了需要许多网络请求和瀑布问题。打包工具将多个 JavaScript 源文件拼接为一个单一的 JavaScript 输出文件，称为 bundle，而不改变应用程序行为。这样，浏览器可以通过一次往返网络请求高效加载 bundle。</p>\n<p>当前常用的打包工具有 Webpack、Parcel、Rollup、esbuild 和 Turbopack。</p>\n<blockquote>\n<p>译注：vite 基于 rollup，<a href=\"https://swc.rs/\">swc</a> 也实现了打包功能。另外还有 <a href=\"https://rolldown.rs/\">rolldown</a>、<a href=\"https://rspack.dev/\">rspack</a>、<a href=\"https://www.farmfe.org/\">farm</a> 等等</p>\n</blockquote>\n<ol>\n<li><p><strong><a href=\"https://webpack.js.org/\">Webpack</a></strong>（2014）在 2016 年左右变得非常流行，后来成为标准的打包工具。与当时常用的 Browserify 不同，Webpack 开创了可以在导入时转换源码文件的“loaders”，使得 Webpack 能够协同整个构建流水线。</p>\n<p> Loaders 允许开发者在 JavaScript 文件中透明地导入静态资源，将所有的源码文件和静态资源组合成一个单一的依赖图。而在使用 Gulp 时，每种类型的静态资源都必须作为一个单独的任务构建。Webpack 还开箱即用的支持代码拆分，简化了其设置和配置。</p>\n<p> Webpack 速度慢且是单线程的，用 JavaScript 编写。它高度可配置，但许多配置选项可能让人困惑。</p>\n</li>\n<li><p><strong><a href=\"https://rollupjs.org/\">Rollup</a></strong>（2016）利用 ES6 模块的广泛浏览器支持及其优化能力（如树摇），生成的 bundle 比 Webpack 小得多，导致 Webpack 也后来采用了类似的优化。Rollup 是一个单线程的打包工具，用 JavaScript 编写，性能仅略优于 Webpack。</p>\n</li>\n<li><p><strong><a href=\"https://parceljs.org/\">Parcel</a></strong>（2018）是一个少配置的打包工具，旨在“开箱即用”，为构建过程的所有步骤和开发者工具需求提供合理的默认配置。它是多线程的，比 Webpack 和 Rollup 快得多。Parcel 2 在内部使用 SWC。</p>\n</li>\n<li><p><strong><a href=\"https://esbuild.github.io/\">Esbuild</a></strong>（2020）是一个为并行处理和优化性能而设计的打包工具，用 Go 编写。它的性能比 Webpack、Rollup 和 Parcel 快数倍。Esbuild 实现了一个基本的转译器和一个压缩器，但功能不如其它打包工具，它提供了一个有限的插件 API，不能直接修改 AST。与其使用 esbuild 插件修改源文件，不如在传给 esbuild 之前先对文件进行转换。</p>\n</li>\n<li><p><strong><a href=\"https://turbo.build/pack\">Turbopack</a></strong>（2022）是一个支持增量重建的快速 Rust 打包工具。该项目由 Vercel 开发，Webpack 的创建者领导。目前处于 Beta 版，可在 Next.js 中可选使用。</p>\n</li>\n</ol>\n<p>如果你有非常少的模块或网络延迟非常低（如在 localhost 上），跳过打包步骤是合理的。有些开发服务器也选择不在开发时打包模块。</p>\n<h3>代码拆分（Code Splitting）</h3>\n<p>默认情况下，客户端 React 应用程序被转化为一个单一的 bundle 文件。对于具有许多页面和功能的大型应用程序，bundle 文件可能非常大，抵消了打包的原始性能优势。</p>\n<p>将 bundle 文件划分为几个较小的 bundle 文件，即代码拆分，可以解决这个问题。一种常见的方法是将每个页面拆分为一个单独的 bundle 文件。使用 HTTP/2，可以将共享的依赖项也分离成独立的 bundle 文件，从而避免重复且几乎没有成本。此外，大型模块可以拆分为单独的 bundle 文件并按需懒加载。</p>\n<p>代码拆分后，每个 bundle 文件的大小大大减少，但会需要额外的网络往返，这可能重新引入瀑布问题。因此代码拆分是一种权衡。</p>\n<p>被 Next.js 普及的文件系统路由优化了代码拆分的权衡。Next.js 为每个页面创建单独的 bundle 文件，只在 bundle 文件中包含该页面导入的代码。加载一个页面时并行预加载该页面使用的所有 bundle 文件。这优化了 bundle 文件大小，而不会重新引入瀑布问题。文件系统路由通过为每个页面创建一个入口点（<code>pages/**/*.jsx</code>）来实现这一点，而不是传统客户端 React 应用程序的单一入口点（<code>index.jsx</code>）。</p>\n<h3>树摇（Tree Shaking）</h3>\n<p>一个 bundle 文件由多个模块组成，每个模块包含一个或多个导出项。通常，一个给定的 bundle 文件只会使用其导入模块的一部分导出项。打包工具可以在一个称为树摇的过程中移除其模块中未使用的导出项。这优化了 bundle 文件大小，提高了加载和解析时间。</p>\n<p>树摇依赖于对源文件的静态分析，因此当静态分析变得更具挑战性时会受到阻碍。两个主要因素影响树摇的效率：</p>\n<ol>\n<li><p><strong>模块系统：</strong> ES6 模块是静态导出和导入，而 CommonJS 模块是动态导出和导入。因此，打包工具在对 ES6 模块进行树摇时能够更加激进和高效。</p>\n</li>\n<li><p><strong>副作用：</strong> <code>package.json</code> 的 <code>sideEffects</code> 属性声明模块在导入时是否有副作用。当存在副作用时，由于静态分析的限制，未使用的模块和导出项可能无法被树摇掉。</p>\n</li>\n</ol>\n<h3>静态资源（Static Assets）</h3>\n<p>静态资源，如 CSS、图片和字体，通常在打包步骤中添加到产物中。它们也可以在压缩步骤中进行文件大小优化。</p>\n<p>在 Webpack 出现之前，静态资源作为独立的构建任务从源码中单独构建。为了加载静态资源，应用程序必须通过其产物中的最终路径引用它们。因此，常常会根据 URL 约定来仔细组织资源（如 <code>/assets/css/banner.jpg</code> 和 <code>/assets/fonts/Inter.woff2</code>）。</p>\n<p>Webpack 的“loaders”允许从 JavaScript 中导入静态资源，将代码和静态资源统一到一个依赖图中。在打包过程中，Webpack 会用其在产物中的最终路径替换静态资源导入。这一特性使静态资源可以与其相关组件一起在源码中组织，并创造了新的静态分析可能性，如检测不存在的资源。</p>\n<p>需要注意的是，导入静态资源（非 JavaScript 或转译成 JavaScript 的文件）不是 JavaScript 语言的一部分。这需要配置支持该资源类型的打包工具。幸运的是，跟随 Webpack 的打包工具也采用了“loaders”模式，使这一特性普遍化。</p>\n<h2>压缩（Minification）</h2>\n<p>压缩解决了文件不必要大的问题。压缩工具在不影响文件行为的前提下减小文件大小。对于 JavaScript 代码和 CSS 资源，压缩工具可以缩短变量、消除空白和注释、消除无用代码，并优化语言特性的使用。对于其它静态资源，压缩工具可以进行文件大小优化。压缩工具通常在构建流程结束时对 bundle 文件运行。</p>\n<p>目前常用的 JavaScript 压缩工具有 Terser、esbuild 和 SWC。<br><strong><a href=\"https://terser.org/\">Terser</a></strong> 是 fork 自不维护的 uglify-es ，用 JavaScript 编写，速度比较慢。<strong>Esbuild</strong> 和 <strong>SWC</strong> 在前文提到，这两者除了其它功能外，还实现了压缩器，比 Terser 更快。</p>\n<p>常用的 CSS 压缩工具有 cssnano、csso 和 Lightning CSS。<br><strong><a href=\"https://cssnano.github.io/cssnano/\">Cssnano</a></strong> 和 <strong><a href=\"https://github.com/css/csso\">csso</a></strong> 是用 JavaScript 编写的纯 CSS 压缩工具，速度较慢。<strong><a href=\"https://lightningcss.dev/\">Lightning CSS</a></strong> 是用 Rust 编写的，声称比 cssnano 快 100 倍。Lightning CSS 还支持 CSS 转换和打包。</p>\n<h1>开发者工具（Developer Tooling）</h1>\n<p>上述基本的前端构建流水线足以创建一个优化的生产构建产物。还有几类工具可以增强基本的构建流水线并改善开发者体验。</p>\n<h2>元框架（Meta-Frameworks）</h2>\n<p>前端领域以选择“正确”包的挑战而著称。例如，上述五个打包工具中，你该选择哪一个？</p>\n<p>元框架提供了一套已经选好的包组合，包括构建工具，这些包协同工作以实现特定的应用模式。例如，<strong><a href=\"https://nextjs.org/\">Next.js</a></strong> 专注于服务器端渲染 (SSR)，<strong><a href=\"https://remix.run/\">Remix</a></strong> 专注于渐进增强。</p>\n<p>元框架通常提供预配置的构建系统，免去了你需要自己去拼装一个的麻烦。它们的构建系统为生产环境和开发服务器提供配置。</p>\n<p>与元框架类似，构建工具如 <strong><a href=\"https://vitejs.dev/\">Vite</a></strong> 提供用于生产和开发的预配置构建系统。然而，它们不强制采用特定的应用模式，适用于通用的前端应用程序。</p>\n<h2>源映射（Sourcemaps）</h2>\n<p>构建流水线输出的产物可以说是人类不可读的。这使得发生错误时调试很困难，因为其错误堆栈指向难以理解的代码。</p>\n<p><a href=\"https://developer.chrome.com/blog/sourcemaps/\">源映射</a>解决了这个问题，通过将产物中的代码映射回其源码中的原始位置。浏览器和诊断工具（如 Sentry）使用源映射恢复并显示原始源码。在生产环境中，源映射通常隐藏在浏览器中，仅上传到诊断工具以避免公开源码。</p>\n<p>构建流水线的每一步都可以生成一个源映射。如果使用多个构建工具来构建流水线，源映射将形成一个链（如 <code>source.js</code> -&gt; <code>transpiler.map</code> -&gt; <code>bundler.map</code> -&gt; <code>minifier.map</code>）。要识别压缩代码对应的源代码，必须遍历源映射链。</p>\n<p>然而，大多数工具无法解释源映射链；它们期望每个产物中的文件最多只有一个源映射。必须将源映射链展平成一个单一的源映射。预配置的构建系统会解决这个问题（参见 Vite 的 <a href=\"https://github.com/vitejs/vite/blob/feae09fdfab505e58950c915fe5d8dd103d5ffb9/packages/vite/src/node/utils.ts#L831\">combineSourcemaps</a> 函数）。</p>\n<h2>热重载（Hot Reload）</h2>\n<p>开发服务器通常提供热重载功能，当源代码改变时自动重建一个新的 bundle 文件并重新加载浏览器。尽管比手动重建和重载要好许多，但它仍然有些慢，并且重新加载时所有客户端状态都会丢失。</p>\n<p><a href=\"https://webpack.js.org/concepts/hot-module-replacement/\">热模块替换（Hot Module Replacement，HMR）</a>改进了热重载（Hot Reload），通过在运行中的应用程序中就地替换更改的 bundle 文件。这样保留了未改变模块的客户端状态，并减少了代码变更和应用程序更新之间的延迟时间。</p>\n<p>然而，每次代码更改都会触发所有导入它的 bundle 文件的重建。这在 bundle 文件大小上具有线性时间复杂度。因此，在大型应用程序中，热模块替换可能因为重建成本的增长而变得缓慢。</p>\n<p>当前由 Vite 推崇的<a href=\"https://vitejs.dev/guide/why.html\">无打包范式（no-bundle paradigm）</a>，通过不在开发服务器中打包来应对这一问题。取而代之的是 Vite 将 ESM 模块，每个模块对应一个源文件，直接服务于浏览器。在这种范式下，每次代码更改都会触发前端单个模块的替换。这使得相对于应用程序大小，刷新时间复杂度几乎保持不变。然而，如果你有许多模块，初始页面加载可能会更长。</p>\n<h2>单一代码库（Monorepos）</h2>\n<p>在有多个团队或多个应用程序的组织中，前端可能分成多个 JavaScript 包，但保持在一个代码库中。在这样的架构中，每个包都有自己的构建步骤，它们一起形成一个包的依赖图。应用程序位于依赖图的根部。</p>\n<p>Monorepo 工具协调依赖图的构建。它们通常提供增量重建、并行处理和远程缓存等特性。借助这些特性，大型代码库可以享受小型代码库的构建时间。</p>\n<p>业界标准的更广泛的 monorepo 工具，如 <strong><a href=\"https://bazel.build/\">Bazel</a></strong>，支持广泛的语言、复杂的构建图（complicated build graphs）和透明的执行（hermetic execution）。然而，JavaScript 对前端来说是最难完全集成到这些工具中的生态系统之一，目前几乎没有现成的案例。</p>\n<p>幸运的是，存在专门为前端设计的若干 monorepo 工具。不幸的是，它们缺乏 Bazel 等工具的灵活性和稳健性，尤其是透明执行。</p>\n<p>目前常用的前端特定 monorepo 工具是 <strong><a href=\"https://nx.dev/\">Nx</a></strong> 和 <strong><a href=\"https://turbo.build/repo\">Turborepo</a></strong>。Nx 更成熟且功能更强，而 Turborepo 是 Vercel 生态系统的一部分。过去，<strong><a href=\"https://lerna.js.org/\">Lerna</a></strong> 是将多个 JavaScript 包链接在一起并发布到 NPM 的标准工具。2022 年，Nx 团队接管了 Lerna，现在 Lerna 在内部使用 Nx 来驱动构建。</p>\n<blockquote>\n<p>另有：<a href=\"https://bit.dev/\">Bit</a></p>\n</blockquote>\n<h1>趋势</h1>\n<p>新的构建工具是用编译语言编写的，强调性能。2019 年的前端构建速度非常慢，但现代工具已经大大加快了速度。然而，现代工具功能集较小，有时与库不兼容，因此旧代码库通常无法轻易切换到它们。</p>\n<p>服务器端渲染 (SSR) 在 Next.js 兴起后变得更加流行。SSR 并不会给前端构建系统带来任何根本性的不同。SSR 应用程序也必须向浏览器提供 JavaScript，因此它们执行相同的构建步骤。</p>\n<h1>toolchain</h1>\n<p>State of JavaScript 2024 构建工具: <a href=\"https://2024.stateofjs.com/zh-Hans/libraries/build_tools/\">https://2024.stateofjs.com/zh-Hans/libraries/build_tools/</a></p>\n<blockquote>\n<p>非原文部分</p>\n</blockquote>\n<ul>\n<li><p><a href=\"https://webpack.js.org/\">Webpack</a>: bundler, dev_server</p>\n</li>\n<li><p><a href=\"https://parceljs.org/\">Parcel</a>: dev_server</p>\n</li>\n<li><p><a href=\"https://rollupjs.org/\">Rollup</a>: bundler</p>\n</li>\n<li><p><a href=\"https://vitejs.dev/\">Vite</a>: dev_server</p>\n</li>\n<li><p><a href=\"https://prettier.io/\">Prettier</a>: formatter</p>\n</li>\n<li><p><a href=\"https://eslint.org/\">ESLint</a>: linter</p>\n</li>\n<li><p><a href=\"https://babeljs.io/\">Babel</a>: transformer</p>\n</li>\n<li><p><a href=\"https://terser.org/\">terser</a>: minifier</p>\n</li>\n<li><p><a href=\"https://postcss.org/\">PostCSS</a>: css</p>\n<ul>\n<li><a href=\"https://cssnano.github.io/cssnano/\">cssnano</a>: minifier</li>\n</ul>\n</li>\n<li><p><a href=\"https://github.com/mishoo/UglifyJS\">UglifyJS</a>: minifier</p>\n</li>\n<li><p><a href=\"https://esbuild.github.io/\">Esbuild</a>: transformer, bundler, minifier</p>\n</li>\n<li><p><a href=\"https://swc.rs/\">swc</a>: transformer, bundler, minifier</p>\n</li>\n<li><p><a href=\"https://biomejs.dev/\">Biome</a>: formatter, linter</p>\n</li>\n<li><p><a href=\"https://oxc.rs/\">Oxc</a>: parser, linter, transformer, formatter, minifier</p>\n<ul>\n<li><a href=\"https://rolldown.rs/\">Rolldown</a> bundler</li>\n</ul>\n</li>\n<li><p><a href=\"https://dprint.dev/\">dprint</a>: formatter</p>\n</li>\n<li><p><a href=\"https://lightningcss.dev/\">Lightning CSS</a>: css</p>\n</li>\n<li><p><a href=\"https://rspack.dev/\">Rspack</a>: webpack</p>\n</li>\n<li><p><a href=\"https://rsbuild.dev/\">Rsbuild</a>: dev_server</p>\n</li>\n<li><p><a href=\"https://www.farmfe.org/\">Farm</a>: vite</p>\n</li>\n<li><p><a href=\"https://makojs.dev/\">Mako</a>: bundler</p>\n</li>\n</ul>\n","tags":["fe"]},{"id":"js-this","url":"https://yieldray.fun/posts/js-this","title":"通过ECMA262理解this","date_published":"2024-07-26T22:22:22.000Z","date_modified":"2024-12-29T20:00:00.000Z","content_text":"<blockquote>\n<p><a href=\"/posts/understanding-ecmascript-part-1\">Read this first if you are new to ECMA262</a></p>\n</blockquote>\n<h1>调用函数</h1>\n<p>从解释器实现角度看，运行时<a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-function-calls\">调用一个函数</a>的本质是执行以下抽象操作：</p>\n<ol>\n<li><a href=\"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-call\"><code>Call(F, V [,argumentsList])</code></a>：</li>\n</ol>\n<ul>\n<li>F 为函数对象，V 为 this 值</li>\n<li>此操作调用 F 的 <code>[[Call]]</code> 方法</li>\n</ul>\n<blockquote>\n<p>可以发现：除了要提供参数列表，还需要提供 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-this-keyword-runtime-semantics-evaluation\">this</a> 参数</p>\n</blockquote>\n<ol start=\"2\">\n<li><a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ecmascript-function-objects-call-thisargument-argumentslist\"><code>[[Call]](thisArgument, argumentsList)</code></a></li>\n</ol>\n<ul>\n<li>该抽象操作内部又委托 <code>OrdinaryCallBindThis(F, calleeContext, thisArgument)</code> 抽象操作，来指定当前执行环境的 this 值</li>\n<li>此操作主要是根据函数的 <code>[[ThisMode]]</code> 内部槽来决定如何绑定 this。分为严格模式、非严格模式和箭头函数情况</li>\n<li>（简单来说，绑定 this 即设置当前 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-function-environment-records\">Function Environment Record</a> 的 <code>[[ThisValue]]</code>）</li>\n<li>据此可知，传入的 thisArgument 不一定就是最终的 this 值，详细见下面 <a href=\"#%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0%E5%92%8C%E4%B8%A5%E6%A0%BC%E6%A8%A1%E5%BC%8F\">箭头函数和严格模式</a> 一节</li>\n</ul>\n<ol start=\"3\">\n<li><p>最后通过 <code>OrdinaryCallEvaluateBody(F, argumentsList)</code> 抽象操作来执行代码（委托 <a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-runtime-semantics-evaluatebody\"><code>EvaluateCall</code></a> 抽象操作）</p>\n</li>\n<li><p>当执行函数体时，this 只不过是一个关键字，解释器对其求值即调用 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-resolvethisbinding\"><code>? ResolveThisBinding()</code></a> 抽象操作<br>该抽象操作会在上层 Environment Record 中寻找已保存的 this 值（规范术语 this binding），详细参见 <a href=\"#%E8%A1%A5%E5%85%85\">补充</a>一节</p>\n</li>\n</ol>\n<hr>\n<p>那么上述抽象操作中的 this 是怎么得来的？关键在于最后的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-evaluatecall\"><code>EvaluateCall(func, ref, arguments, tailPosition)</code></a> 抽象操作。</p>\n<p>首先看看该抽象操作最初是如何被调用的，从语法解析完毕开始，即我们执行一个 <code>CallExpression : CallExpression Arguments</code> 表达式：</p>\n<ol>\n<li>令 <code>ref</code> 为 ? 对 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-CallExpression\">CallExpression</a> 的<a href=\"https://tc39.es/ecma262/multipage/syntax-directed-operations.html#sec-evaluation\">求值</a>结果。</li>\n<li>令 <code>func</code> 为 ? <a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-getvalue\">GetValue</a>(ref)。</li>\n<li>令 <code>thisCall</code> 为当前的 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-CallExpression\">CallExpression</a>。</li>\n<li>令 <code>tailCall</code> 为 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-functions-and-classes.html#sec-isintailposition\">IsInTailPosition</a>(<code>thisCall</code>)。</li>\n<li>返回 ? <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-evaluatecall\">EvaluateCall</a>(<code>func</code>, <code>ref</code>, Arguments, <code>tailCall</code>)。</li>\n</ol>\n<p>现在执行 EvaluateCall 抽象操作：</p>\n<ol>\n<li>若 ref 为 Reference Record, 则<ol>\n<li>若 IsPropertyReference(ref) 是 true, 则<ol>\n<li>令 thisValue 为 GetThisValue(ref).</li>\n</ol>\n</li>\n<li>否则,<ol>\n<li>令 refEnv 为 ref.[[Base]].</li>\n<li>断言: refEnv 是 Environment Record.</li>\n<li>令 thisValue 为 refEnv.WithBaseObject().</li>\n</ol>\n</li>\n</ol>\n</li>\n<li>否则,<ol>\n<li>令 thisValue 为 undefined.</li>\n</ol>\n</li>\n<li>令 argList 为 ? ArgumentListEvaluation of arguments.</li>\n<li>若 func 不是 Object, 抛出 TypeError 异常.</li>\n<li>若 IsCallable(func) 是 false, 抛出 TypeError 异常.</li>\n<li>若 tailPosition 是 true, 执行 PrepareForTailCall().</li>\n<li>返回 ? Call(func, thisValue, argList).</li>\n</ol>\n<p>结合上面的信息，可知（我们忽略 with 表达式的情况）：</p>\n<p>如果执行 CallExpression 得到 Reference Record 且它还是 PropertyReference（即其 <code>[[Base]]</code> 是 Environment Record），\n在此情况下：若它是 SuperReference（即含 <code>[[ThisValue]]</code>）则返回它的 <code>[[ThisValue]]</code>，否则返回它的 <code>[[Base]]</code></p>\n<p>这里，核心问题在于 <strong>ref 是不是一个 Reference Record</strong>：<br>因为如果 ref 不是 Reference Record，那么 thisValue 就会是 undefined<br>事实是 ref 既可以是包含了函数的 Reference Record，也可以是直接是函数本身</p>\n<p>例如，在下面的示例中，<code>obj.fn</code> 是 包含了函数的 Reference Record，而 <code>(true &amp;&amp; obj.fn)</code> （求值后）是函数本身：</p>\n<pre><code class=\"language-js\">var fn = function () {\n    // &quot;use strict&quot;;\n    return `this is ${this}`;\n};\nvar fn2 = () =&gt; `this is ${this}`;\n\nvar obj = { fn, fn2 };\n\nfn(); // this is [object global]\nobj.fn(); // this is [object Object]\nobj.fn2(); // this is [object global]\n(true &amp;&amp; obj.fn)(); // this is [object global]\n(false || obj.fn)(); // this is [object global]\n</code></pre>\n<h1>箭头函数和严格模式</h1>\n<p>箭头函数时，解释器算法传入 thisValue 也和普通情况别无二致，也就是说这时是不区分是否是箭头函数的。<br>但我们知道，箭头函数的内部槽 <code>[[ThisMode]]</code> 与其它函数不一样：它是 LEXICAL，表示 this 引用词法上包围的函数的 this 值。</p>\n<p>还记得上面提到的 <a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinarycallbindthis\"><code>OrdinaryCallBindThis(F, calleeContext, thisArgument)</code></a> 抽象操作吗？<br>这个操作非常重要：正是它最终为 Function Environment Record 绑定 thisValue（调用 <code>BindThisValue(thisValue)</code>）。</p>\n<p>箭头函数的 <code>[[ThisMode]]</code> 是 LEXICAL，此时 OrdinaryCallBindThis 抽象操作中不会为其（指这个箭头函数的 Function Environment Record）绑定 this。<br>这就是为什么规范称<strong>箭头函数的 Function Environment Record 不会绑定 this</strong>。<br>因此在 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-getthisenvironment\"><code>GetThisEnvironment()</code></a> 抽象操作中，会向上层 Environment Record 寻找，直至找到 this （规范术语 this binding）。</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-light-bulb mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>\n<p>因为在此抽象操作中，必然会跳过当前 Function Environment Record，因此即使调用 Function.prototype.bind 来绑定当前 Function Environment Record 的 this 也是没有意义的<br>这就是为什么对箭头函数调用 <code>.bind()</code> 没有效果</p>\n</div>\n<p>非严格模式下，函数的 <code>[[ThisMode]]</code> 是 GLOBAL，这时如果 thisArgument 是 undefined 或 null，<br>thisValue 就为 <code>globalEnv.[[GlobalThisValue]]</code>。</p>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-alert mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<p>最顶层 EnvironmentRecord 分两种情况：模块环境（ModuleEnvironmentRecord）和非模块环境（GlobalEnvironmentRecord） （注：模块总是严格模式）<br><a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-global-environment-records-getthisbinding\"><code>GlobalEnvironmentRecord.GetThisBinding()</code></a>: 返回当前 <code>[[GlobalThisValue]]</code> 内部槽\n<code>GlobalEnvironmentRecord.[[GlobalThisValue]]</code> : The value returned by this in global scope. Hosts may provide any ECMAScript Object value.<br><a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-module-environment-records-getthisbinding\"><code>ModuleEnvironmentRecord.GetThisBinding()</code></a>: 返回 undefined</p>\n<p>根据规范，非模块环境的 <code>[[GlobalThisValue]]</code> 值由实现定义<br>在浏览器中，它是 <code>window</code>；<strong>在 Node.js 中，它<a href=\"https://nodejs.org/docs/latest/api/deprecations.html#dep0092-top-level-this-bound-to-moduleexports\">指向 <code>module.exports</code> 的初始值</a>，而不是 <code>global</code></strong><br>注意这与 <a href=\"https://tc39.es/ecma262/multipage/global-object.html#sec-globalthis\"><code>globalThis</code></a> 的机制不同</p>\n<p>Node.js 注意：cjs 模块代码本质是运行在一个包装函数内，其 <a href=\"https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js\">this 被指定为 cjsModuleInstance</a> 并通过 <a href=\"https://github.com/nodejs/node/blob/main/lib/internal/vm.js#L208\">runScriptInThisContext</a> 运行</p>\n<pre><code class=\"language-js\">let wrap = function (script) {\n    return Module.wrapper[0] + script + Module.wrapper[1];\n};\n\nconst wrapper = [&quot;(function (exports, require, module, __filename, __dirname) { &quot;, &quot;\\n});&quot;];\n</code></pre>\n<p>Node.js 模块细节不在本文探讨范围内，见<a href=\"https://yieldray.fun/posts/nodejs-modules\">https://yieldray.fun/posts/nodejs-modules</a></p>\n<p>示例：<br>创建两个文件，<code>test.cjs</code> 和 <code>test.mjs</code>，内容均为 <code>&quot;use strict&quot;; console.log(this)</code><br>执行 <code>node test.cjs</code>，结果为 <code>{}</code><br>执行 <code>node test.mjs</code>，结果为 <code>undefined</code><br>创建 <code>index.html</code>，内容为 <code>&lt;script src=&quot;./test.cjs&quot;&gt;&lt;/script&gt;&lt;script src=&quot;./test.mjs&quot; type=&quot;module&quot;&gt;&lt;/script&gt;</code><br>打开 <code>index.html</code>，结果为 <code>window</code> 和 <code>undefined</code></p>\n</div>\n<p>严格模式下，函数的 <code>[[ThisMode]]</code> 是 STRICT，这时不会因为 thisArgument 是 undefined 或 null 而做特殊处理，<br>即 thisArgument 就是 CallExpression 那一步得到的 thisCall，否则就是 undefined 了。</p>\n<h1>apply call bind</h1>\n<p>现在我们来看，<code>Function.prototype.apply</code> 和 <code>Function.prototype.call</code> 就非常简单了：<br>这两个函数都接受 thisArg，这个参数传递给 <code>Call(func, thisArg, argList)</code>，最终被设置为 Function Environment Record 的 <code>[[ThisValue]]</code> 内部槽。</p>\n<hr>\n<p>比较特殊的是 <a href=\"https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-function.prototype.bind\">Function.prototype.bind(thisArg, ...args)</a>：<br>它委托 <code>BoundFunctionCreate(targetFunction, boundThis, boundArgs)</code> 抽象操作，<br>我们可以简单理解为该抽象操作将 <code>[[BoundThis]]</code> 内部槽设为 boundThis，将 <code>[[BoundArguments]]</code> 内部槽设为 boundArgs。</p>\n<p>其实就是创建了一个 <a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-bound-function-exotic-objects\">Bound Function Exotic Objects</a>，它是一个特殊的 Function Objects。<br>这个 Bound Function Exotic Objects 的 <code>[[Call]]</code> 内部方法的 this 必定是它 <code>[[BoundThis]]</code> 内部槽的值，因此也就实现了绑定的效果。</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>简单来说：<a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-bound-function-exotic-objects\">bound function exotic object</a>，包装了 <a href=\"https://tc39.es/ecma262/#function-object\">function object</a>，并覆写了 <a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-bound-function-exotic-objects-call-thisargument-argumentslist\"><code>[[Call]]</code> 内部方法</a></p>\n</div>\n<h1>new</h1>\n<p>当使用 <a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-new-operator\">new</a> 调用构造函数时，解释器调用 <a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ecmascript-function-objects-construct-argumentslist-newtarget\"><code>[[Construct]]</code></a> 内部方法而非 <code>[[Call]]</code> 内部方法。<br>（这里省略 <code>[[ConstructorKind]]</code> 为 derived，也就是构造函数是派生构造函数的情况，不过过程也是类似的）</p>\n<p>OrdinaryCallBindThis 绑定 this 的值就是 <code>? OrdinaryCreateFromConstructor(newTarget, &quot;%Object.prototype%&quot;)</code> 的返回值，注意这里的 newTarget 就是指构造函数本身。</p>\n<p>也就是说 this 就是新创建的一个对象，对象的 <code>[[Prototype]]</code> 为构造函数的 prototype 属性。</p>\n<h1>前置知识</h1>\n<p>首先了解普通对象的<strong>必要内部方法</strong></p>\n<table>\n<thead>\n<tr>\n<th>内部方法</th>\n<th>签名</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>[[GetPrototypeOf]]</code></td>\n<td>( ) <strong>→</strong> Object | Null</td>\n<td>确定提供此对象继承属性的对象。null值表示没有继承属性。</td>\n</tr>\n<tr>\n<td><code>[[SetPrototypeOf]]</code></td>\n<td>(Object | Null) <strong>→</strong> Boolean</td>\n<td>将此对象与提供继承属性的另一个对象关联。传递null表示没有继承属性。返回true表示操作成功完成，返回false表示操作未成功。</td>\n</tr>\n<tr>\n<td><code>[[IsExtensible]]</code></td>\n<td>( ) <strong>→</strong> Boolean</td>\n<td>确定是否允许向此对象添加其它属性。</td>\n</tr>\n<tr>\n<td><code>[[PreventExtensions]]</code></td>\n<td>( ) <strong>→</strong> Boolean</td>\n<td>控制是否可以向此对象添加新属性。返回true表示操作成功，返回false表示操作未成功。</td>\n</tr>\n<tr>\n<td><code>[[GetOwnProperty]]</code></td>\n<td>(<code>propertyKey</code>) <strong>→</strong> Undefined | Property Descriptor</td>\n<td>返回此对象自有属性的属性描述符，其键为<code>propertyKey</code>，如果不存在此类属性，则返回undefined。</td>\n</tr>\n<tr>\n<td><code>[[DefineOwnProperty]]</code></td>\n<td>(<code>propertyKey</code>, <code>PropertyDescriptor</code>) <strong>→</strong> Boolean</td>\n<td>创建或更改自有属性，其键为<code>propertyKey</code>，使其具有<code>PropertyDescriptor</code>描述的状态。如果该属性成功创建/更新，则返回true；如果无法创建或更新该属性，则返回false。</td>\n</tr>\n<tr>\n<td><code>[[HasProperty]]</code></td>\n<td>(<code>propertyKey</code>) <strong>→</strong> Boolean</td>\n<td>返回一个布尔值，指示此对象是否已经具有键为<code>propertyKey</code>的自有或继承属性。</td>\n</tr>\n<tr>\n<td><code>[[Get]]</code></td>\n<td>(<code>propertyKey</code>, <code>Receiver</code>) <strong>→</strong> <em>any</em></td>\n<td>返回此对象中键为<code>propertyKey</code>的属性值。如果需要执行任何ECMAScript代码来获取属性值，则在评估代码时使用<code>Receiver</code>作为this值。</td>\n</tr>\n<tr>\n<td><code>[[Set]]</code></td>\n<td>(<code>propertyKey</code>, <code>value</code>, <code>Receiver</code>) <strong>→</strong> Boolean</td>\n<td>将键为<code>propertyKey</code>的属性值设置为<code>value</code>。如果需要执行任何ECMAScript代码来设置属性值，则在评估代码时使用<code>Receiver</code>作为this值。如果属性值已设置，则返回true；如果无法设置，则返回false。</td>\n</tr>\n<tr>\n<td><code>[[Delete]]</code></td>\n<td>(<code>propertyKey</code>) <strong>→</strong> Boolean</td>\n<td>从此对象中删除键为<code>propertyKey</code>的自有属性。如果属性未删除且仍然存在，则返回false。如果属性已删除或不存在，则返回true。</td>\n</tr>\n<tr>\n<td><code>[[OwnPropertyKeys]]</code></td>\n<td>( ) <strong>→</strong> List of property keys</td>\n<td>返回一个列表，其元素是此对象的所有自有属性键。</td>\n</tr>\n</tbody></table>\n<pre><code class=\"language-ts\">interface ProxyHandler&lt;T extends object&gt; {\n    apply?(target: T, thisArg: any, argArray: any[]): any;\n    construct?(target: T, argArray: any[], newTarget: Function): object;\n    defineProperty?(target: T, property: string | symbol, attributes: PropertyDescriptor): boolean;\n    deleteProperty?(target: T, p: string | symbol): boolean;\n    get?(target: T, p: string | symbol, receiver: any): any;\n    getOwnPropertyDescriptor?(target: T, p: string | symbol): PropertyDescriptor | undefined;\n    getPrototypeOf?(target: T): object | null;\n    has?(target: T, p: string | symbol): boolean;\n    isExtensible?(target: T): boolean;\n    ownKeys?(target: T): ArrayLike&lt;string | symbol&gt;;\n    preventExtensions?(target: T): boolean;\n    set?(target: T, p: string | symbol, newValue: any, receiver: any): boolean;\n    setPrototypeOf?(target: T, v: object | null): boolean;\n}\n</code></pre>\n<hr>\n<p>ECMAScript 函数对象封装了带参数的 ECMAScript 代码，这些代码在词法环境中闭合，并支持动态求值。<br>ECMAScript 函数对象是普通对象，具有与其它普通对象相同的内部槽和内部方法。<br>ECMAScript函数对象的代码可以是严格模式代码或非严格模式代码。<br>代码为严格模式的 ECMAScript 函数对象称为严格函数，代码为非严格模式的则称为非严格函数。</p>\n<p>除了 <a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions\"><code>[[Extensible]]</code></a> 和 <code>[[Prototype]]</code>，ECMAScript函数对象还拥有表中列出的内部槽。</p>\n<table>\n<thead>\n<tr>\n<th>内部槽</th>\n<th>类型</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>[[Environment]]</code></td>\n<td>an Environment Record</td>\n<td>函数闭合时的环境记录。在求值函数代码时用作外部环境。</td>\n</tr>\n<tr>\n<td><code>[[PrivateEnvironment]]</code></td>\n<td>a PrivateEnvironment Record or null</td>\n<td>函数闭合时的私有环境记录。如果该函数在语法上不包含在类中，则为null。在求值函数代码时，用作内部类的外部私有环境。</td>\n</tr>\n<tr>\n<td><code>[[FormalParameters]]</code></td>\n<td>a Parse Node</td>\n<td>定义函数形式参数列表的源文本的根解析节点。</td>\n</tr>\n<tr>\n<td><code>[[ECMAScriptCode]]</code></td>\n<td>a Parse Node</td>\n<td>定义函数主体的源文本的根解析节点。</td>\n</tr>\n<tr>\n<td><code>[[ConstructorKind]]</code></td>\n<td>base or derived</td>\n<td>函数是否为派生类构造函数。</td>\n</tr>\n<tr>\n<td><code>[[Realm]]</code></td>\n<td>a Realm Record</td>\n<td>创建函数的 Realm，并在求值函数时提供任何访问的内在对象。</td>\n</tr>\n<tr>\n<td><code>[[ScriptOrModule]]</code></td>\n<td>a Script Record or a Module Record</td>\n<td>创建函数的脚本或模块。</td>\n</tr>\n<tr>\n<td><code>[[ThisMode]]</code></td>\n<td>lexical, strict, or global</td>\n<td>定义在函数的形式参数和代码主体中如何解释<code>this</code>引用。lexical意味着<code>this</code>引用词法上包围的函数的this值。strict意味着this值完全按照函数调用时提供的值使用。global意味着undefined或null的this值被解释为全局对象的引用，其它this值首先传递给ToObject。</td>\n</tr>\n<tr>\n<td><code>[[Strict]]</code></td>\n<td>a Boolean</td>\n<td>如果这是一个严格函数，则为true；如果这是一个非严格函数，则为false。</td>\n</tr>\n<tr>\n<td><code>[[HomeObject]]</code></td>\n<td>an Object</td>\n<td>如果函数使用<code>super</code>，这是一个对象，其<code>[[GetPrototypeOf]]</code>提供了<code>super</code>属性查找开始的对象。</td>\n</tr>\n<tr>\n<td><code>[[SourceText]]</code></td>\n<td>a sequence of Unicode code points</td>\n<td>定义函数的源文本。</td>\n</tr>\n<tr>\n<td><code>[[Fields]]</code></td>\n<td>a List of ClassFieldDefinition Records</td>\n<td>如果函数是一个类，这是一个表示类的非静态字段及其相应初始化器的记录列表。</td>\n</tr>\n<tr>\n<td><code>[[PrivateMethods]]</code></td>\n<td>a List of PrivateElements</td>\n<td>如果函数是一个类，这是一个表示类的非静态私有方法和访问器的列表。</td>\n</tr>\n<tr>\n<td><code>[[ClassFieldInitializerName]]</code></td>\n<td>a String, a Symbol, a Private Name, or empty</td>\n<td>如果函数是作为类字段的初始化器创建的，用于字段命名求值的名称；否则为空。</td>\n</tr>\n<tr>\n<td><code>[[IsClassConstructor]]</code></td>\n<td>a Boolean</td>\n<td>指示函数是否为类构造函数。（如果为true，调用函数的<code>[[Call]]</code>将立即抛出TypeError异常。）</td>\n</tr>\n</tbody></table>\n<h1>补充</h1>\n<h2>Environment Record</h2>\n<p><a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-function-environment-records\">函数环境记录（Function Environment Record）</a>是一种声明性环境记录，用于表示函数的顶级作用域。<br>如果该函数不是箭头函数，还提供一个 this 绑定。<br>如果一个函数不是箭头函数并引用了 super，其函数环境记录还包含在函数内部执行 super 方法调用所需的状态。</p>\n<pre><code class=\"language-mermaid\">classDiagram\n    class EnvironmentRecord {\n        &lt;&lt;abstract&gt;&gt;\n        EnvironmentRecord [[OuterEnv]]\n        HasBinding(N) Boolean\n        CreateMutableBinding(N, D) UNUSED\n        CreateImmutableBinding(N, S) UNUSED\n        InitializeBinding(N, V) UNUSED\n        SetMutableBinding(N, V, S) UNUSED\n        GetBindingValue(N, S) any\n        DeleteBinding(N) Boolean\n        HasThisBinding() false\n        HasSuperBinding() false\n        WithBaseObject() undefined\n    }\n\n    class DeclarativeEnvironmentRecord {\n    }\n\n    class FunctionEnvironmentRecord {\n        any [[ThisValue]]\n        LEXICAL|INITIALIZED|UNINITIALIZED [[ThisBindingStatus]]\n        function [[FunctionObject]]\n        object|undefined [[NewTarget]]\n        BindThisValue(V) V\n        GetThisBinding() any\n        GetSuperBase() Object|null|undefined\n    }\n\n    class ModuleEnvironmentRecord {\n        CreateImportBinding(N, M, N2)\n        GetThisBinding() any\n    }\n\n    class ObjectEnvironmentRecord {\n        Object [[BindingObject]]\n        Boolean [[IsWithEnvironment]]\n    }\n\n    class GlobalEnvironmentRecord {\n        ObjectEnvironmentRecord [[ObjectRecord]]\n        Object [[GlobalThisValue]]\n        DeclarativeEnvironmentRecord [[DeclarativeRecord]]\n        List~String~ [[VarNames]]\n        GetThisBinding() any\n        HasVarDeclaration(N) Boolean\n        HasLexicalDeclaration(N) Boolean\n        HasRestrictedGlobalProperty(N) Boolean\n        CanDeclareGlobalVar(N) Boolean\n        CanDeclareGlobalFunction(N) Boolean\n        CreateGlobalVarBinding(N, D)\n        CreateGlobalFunctionBinding(N, V, D)\n    }\n\n    EnvironmentRecord &lt;|-- DeclarativeEnvironmentRecord\n    EnvironmentRecord &lt;|-- ObjectEnvironmentRecord\n    EnvironmentRecord &lt;|-- GlobalEnvironmentRecord\n    DeclarativeEnvironmentRecord &lt;|-- FunctionEnvironmentRecord\n    DeclarativeEnvironmentRecord &lt;|-- ModuleEnvironmentRecord\n</code></pre>\n<h2>执行上下文</h2>\n<p>一个 agent 由一组 ECMAScript 执行上下文、一个执行上下文栈、一个正在运行的执行上下文、一个 Agent Record 以及一个执行线程组成。除执行线程外，agent 的各个组成部分是该 agent 独有的。</p>\n<p>agent 的执行线程在该agent的执行上下文中独立执行算法步骤，不受其它 agent 的影响，除非一个执行线程被多个 agent 共享，且这些 agent 的 Agent Record 中的 <code>[[CanBlock]]</code> 字段均为false。</p>\n<p><em>执行上下文</em>是一种规范装置，用于跟踪 ECMAScript 实现对代码的运行时执行。<br>在任何时间点，每个 agent 最多只有一个实际执行代码的<em>执行上下文</em>，这被称为 agent 的<em>运行执行上下文</em>。</p>\n<p><em>执行上下文栈</em>用于跟踪<em>执行上下文</em>。<em>运行执行上下文</em>始终是此栈的顶部元素。<br>每当控制从当前<em>运行执行上下文</em>关联的可执行代码转移到与该执行上下文无关的可执行代码时，便会创建一个新的<em>执行上下文</em>。<br>新创建的<em>执行上下文</em>会被推入栈顶并成为<em>运行执行上下文</em>。</p>\n<p><em>执行上下文</em>包含跟踪其关联代码执行进度所需的任何特定于实现的状态。</p>\n<p>在执行之前，所有的 ECMAScript 代码都必须与一个 <a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-code-realms\">Realm</a> 相关联。<br>Realm 包含一个 Global Environment Record。</p>\n<h2>TypeScript</h2>\n<pre><code class=\"language-ts\">const $ = document.querySelector;\n$(&quot;#foo&quot;); // TypeError: Illegal invocation\n\nfunction mustHaveRightThis(this: HTMLElement) {\n    return this.remove();\n}\n\n// The &#39;this&#39; context of type &#39;void&#39; is not assignable to method&#39;s &#39;this&#39; of type &#39;HTMLElement&#39;\nmustHaveRightThis();\n\nconst rightThis: ThisType&lt;Console&gt; = {\n    callback: function () {\n        this.log(666);\n        this.foo; // Property &#39;foo&#39; does not exist on type &#39;Console&#39;.\n    },\n};\n</code></pre>\n<h1>规范总结</h1>\n<p>排除几种特殊情况</p>\n<ul>\n<li><code>[[ThisMode]]</code> 为 LEXCIAL，即箭头函数：\n此时会向上层 Environment Record 寻找 this binding</li>\n<li><code>Function.prototype.bind</code>：\n会创建 Bound Function Exotic Objects</li>\n<li><code>Function.prototype.{call,apply}</code>：\n会跳过 EvaluateCall 操作来绑定 this 并调用函数</li>\n</ul>\n<p>普通情况</p>\n<ul>\n<li>使用 new，即构造函数：\n会调用 <code>[[Construct]]</code> 而不是 <code>[[Call]]</code>，此时 this 是新创建的对象</li>\n<li>直接通过括号运算符调用：\n语法解析后执行 EvaluateCall，括号前面的表达式解析结果如果是 PropertyReference 则 this 被设置为函数所在的对象；\n否则表达式解析结果就是 ECMA262 语言值，而不是规范值，此时 this 设置为 undefined</li>\n</ul>\n<p>是否严格模式</p>\n<ul>\n<li>非严格模式下，<code>[[ThisMode]]</code> 是 GLOBAL，这时会对 thisArgument 是 undefined 或 null 的情况做特殊处理，即 this 设置为 globalThis</li>\n<li>严格模式下，函数 <code>[[ThisMode]]</code> 是 STRICT，不会做特殊处理</li>\n</ul>\n<pre><code class=\"language-mermaid\">flowchart TD\n    Start[&quot;开始: this 绑定判断&quot;]\n\n    %% 主要分支\n    Start --&gt; Special[&quot;是否为特殊情况?&quot;]\n    Start --&gt; Normal[&quot;普通情况&quot;]\n\n    %% 特殊情况分支\n    Special --&gt; Arrow[&quot;箭头函数&quot;]\n    Special --&gt; Bind[&quot;bind 方法&quot;]\n    Special --&gt; CallApply[&quot;call/apply 方法&quot;]\n\n    Arrow --&gt; ArrowResult[&quot;继承上层环境的 this&quot;]\n    Bind --&gt; BindResult[&quot;创建 Bound Function Exotic Objects&quot;]\n    CallApply --&gt; CallApplyResult[&quot;跳过 EvaluateCall 直接绑定&quot;]\n\n    %% 普通情况分支\n    Normal --&gt; IsNew{&quot;是否 new 调用?&quot;}\n    IsNew --&gt;|是| NewThis[&quot;this 为新创建的对象&quot;]\n\n    IsNew --&gt;|否| IsProp{&quot;括号前是否为 PropertyReference?&quot;}\n    IsProp --&gt;|是| PropThis[&quot;this 为函数所在对象&quot;]\n    IsProp --&gt;|否| IsStrict{&quot;是否严格模式?&quot;}\n\n    IsStrict --&gt;|是| StrictThis[&quot;this 为 undefined&quot;]\n    IsStrict --&gt;|否| GlobalThis[&quot;this 为 globalThis&quot;]\n\n    %% 样式\n    classDef decision fill:#f9f,stroke:#333\n    class IsNew,IsProp,IsStrict decision\n</code></pre>\n<h1>简单总结</h1>\n<ol>\n<li>函数通过 new 调用（<code>new FuncClass()</code>）时，this 为创建（new）的对象</li>\n<li>this 由 bind|call|apply 指定，并且不是 undefined || null</li>\n<li>函数在对象上下文中调用，this 为上下文对象（注：此处表述不严谨）</li>\n<li>非上述情况：严格模式（<a href=\"https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/\">模块上下文</a>）下 this === undefined，否则 this === globalThis</li>\n<li>bind|call|apply 指定 undefined || null 时，this === globalThis</li>\n<li>箭头函数没有自己的 this， this 即是上下文中的 this</li>\n</ol>\n","tags":["ecma262"]},{"id":"js-event-loop","url":"https://yieldray.fun/posts/js-event-loop","title":"JavaScript事件循环与异步机制","date_published":"2024-07-25T20:00:00.000Z","date_modified":"2024-12-25T22:22:22.000Z","content_text":"<p>本文主要说明现代浏览器以及 Node.js 环境下的 Event Loop</p>\n<h1>异步任务</h1>\n<p>异步任务在事件循环中调度执行。ECMA262 规范不规定“何时调度”异步任务（规范称之为 <a href=\"https://tc39.es/ecma262/#sec-jobs\">Job</a>），与运行时相关（<a href=\"https://tc39.es/ecma262/#host-defined\">host-defined</a>）。</p>\n<blockquote>\n<p>注：没有定义调度，但定义了 hooks 通知宿主环境来调度，例如 HostEnqueuePromiseJob</p>\n</blockquote>\n<p>Job 是一种无参数的<a href=\"https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-abstract-closure\">抽象闭包</a>，当没有其它 ECMAScript 计算正在进行时，它会启动一个 ECMAScript 计算。<br>在未来某个时间点，当 Job 所属 <a href=\"https://tc39.es/ecma262/#agent\">agent</a> 中<b>没有运行中的上下文</b>且该 agent 的<b><a href=\"https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#execution-context-stack\">执行上下文堆栈</a>为空</b>时，必须：</p>\n<ol>\n<li>执行任何宿主定义的准备步骤。</li>\n<li>调用 Job 抽象闭包。（注：可以是将 Job 的执行上下文推入执行上下文堆栈）</li>\n<li>执行任何宿主定义的清理步骤，之后<b>执行上下文堆栈必须为空</b>。（注：即 Job 的执行上下文必须弹出。这里实际上隐含地告诉我们 Job 必然是在 Global code 执行后再执行）</li>\n</ol>\n<blockquote>\n<p>执行上下文来源：<a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-scripts\">Global code</a>，Eval code，Function code，<a href=\"https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-modules\">Module code</a></p>\n</blockquote>\n<p>代码由 <a href=\"https://tc39.es/ecma262/#agent\">agent</a> 执行，执行在 agent 的执行上下文中。<br>agent 包括一组 ECMAScript 执行上下文、执行上下文堆栈、运行中的执行上下文、Agent Record 和执行线程。<br>除了执行线程之外，代理的组成部分只属于该代理。</p>\n<pre><code class=\"language-mermaid\">graph TD\n    A[Agent] --&gt; B[一组ECMAScript执行上下文]\n    A --&gt; C[执行上下文堆栈]\n    A --&gt;|正在实际执行代码| D[运行中的执行上下文]\n    A --&gt;|管理锁的状态| E[Agent Record]\n\n    D --&gt;|指向执行上下文堆栈顶部| C\n\n    subgraph Agent\n        B\n        C\n        D\n        E\n    end\n\n    F[执行线程] &lt;-.-|执行算法步骤| A\n</code></pre>\n<p>规定：</p>\n<ul>\n<li>同一时间内，agent 中只能执行单个 job （job 不能并行）</li>\n<li>job 一旦开始执行，就不能被打断（抢占）（job 不能并发）</li>\n</ul>\n<blockquote>\n<p>这与 C 语言不同。例如，如果函数在线程中运行，它可能在任何位置被终止，然后在另一个线程中运行其它代码。</p>\n</blockquote>\n<p>注意：job 本身不能并行/并发，但如果是调用宿主环境的函数，宿主函数和当前 ECMAScript 执行当然不禁止并行。<br>比如，浏览器中执行 fetch 函数时，网络请求并非由 ECMAScript 发出，网络请求和脚本执行可以并行，只是请求完成的回调会成为 job。<br>job 之间不并行实际上防止了脚本之间发生可观察到的数据竞争。</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Event_loop\">事件循环</a></h1>\n<p>除了 ECMA262 定义的：</p>\n<ul>\n<li>执行上下文堆（对象被分配在堆中）</li>\n<li>执行上下文栈（函数调用形成了一个由若干帧组成的栈，下面可能简称为调用栈）</li>\n</ul>\n<p>运行时内部实现任务<strong>队列</strong>，往往分为（下面使用 v8 术语，微任务之外都称为宏任务）：</p>\n<ul>\n<li>宏任务队列（存储稍后要执行的其它任务）</li>\n<li>微任务队列（存储应该在最近的将来调用的任务）</li>\n</ul>\n<p>一旦<em>调用栈为空</em>（<a href=\"https://tc39.es/ecma262/#execution-context-stack\">JavaScript 执行上下文栈</a> 清空之后），<del>事件循环从任务队列中取出任务来执行</del><br>解释器从任务队列中取出任务执行（更精确的说法是宿主将一些任务推入执行上下文堆栈），而事件循环接受任务并将任务加入任务队列</p>\n<blockquote>\n<p>运行任务的具体步骤是实现定义的，例如：宿主环境可以通过将执行上下文推入执行上下文堆栈来准备执行。</p>\n</blockquote>\n<p>事件循环能够协调事件、用户交互、脚本、渲染、网络等。每个 agent 有唯一关联的事件循环。</p>\n<hr>\n<p>HTML 标准中定义了如何调度异步任务（注意不要与 v8 术语混淆）。<br>根据 <a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#event-loop\">HTML 标准</a>，<a href=\"https://gist.github.com/YieldRay/258504dbb9869cc864239bf46b8136bb\">事件循环</a>中可以有一个或多个任务队列，而<strong>微任务队列不是任务队列</strong>。</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>本文只讨论主线程上的事件循环，即标准所称的<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#window-event-loop\">窗口事件循环</a></p>\n</div>\n<p>事件循环包含：</p>\n<ul>\n<li>一个或多个任务队列。每个任务是一个结构，包含任务步骤、任务来源（用于对相关任务进行分组和序列化）、关联文档和执行环境。</li>\n<li>一个微任务队列。</li>\n</ul>\n<p>事件循环过程任务执行过程具体见<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model\">标准 8.1.7.3节</a>。</p>\n<p>简单来说：</p>\n<p>对于每个<em>事件循环</em>，每个<em>任务源</em>必须与特定的<em>任务队列</em>相关联。<br><em>事件循环</em>从具有至少一个<em>可运行任务</em>的<em>任务队列</em>中选取一个<em>任务队列</em>（微任务队列不是任务队列）<br>每运行完一个<em>任务</em>，就将该任务从<em>任务队列</em>中移除，并执行<em>微任务检查点</em>。<br>执行<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint\"><em>微任务检查点</em></a>会运行<em>微任务队列</em>中的所有任务，直至<em>微任务队列</em>为空。</p>\n<p>此外，浏览器将并行页面渲染（<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering\">update the rendering:</a>）。但每次事件循环不一定能够完成一次渲染。</p>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-alert mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<p>水平有限，本文仅作参考，必须以<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model\">规范</a>为准。</p>\n</div>\n<p>实际情况中我们可以发现，当脚本本身运行结束后，是先运行了微任务再是宏任务。<br>这是因为规范中指明了<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop\">此时</a>将微任务移动到常规任务队列，也就是说这个微任务变成了宏任务。</p>\n<blockquote>\n<p>在 Chrome 中，事件循环由 <a href=\"https://libevent.org/\">libevent</a> 提供<br>在 Nodejs 中，事件循环由 <a href=\"https://libuv.org/\">libuv</a> 提供</p>\n</blockquote>\n<blockquote>\n<p>一个演示 WebApp：<a href=\"https://github.com/Hopding/js-visualizer-9000-client\">https://github.com/Hopding/js-visualizer-9000-client</a></p>\n</blockquote>\n<p>在下面的演示代码中需要注意：浏览器的 console.log 为同步，Node.js 为异步。</p>\n<h1>浏览器</h1>\n<blockquote>\n<p>注意：HTML 标准中只存在 <em>任务</em> 和 <em>微任务</em>。“宏任务”并没有精确定义，下面通俗地用“宏任务”代替所有非微任务</p>\n</blockquote>\n<p>在浏览器中，<a href=\"https://developer.mozilla.org/docs/Web/API/queueMicrotask\"><code>queueMicrotask</code></a> <a href=\"https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promise-constructor\"><code>Promise.prototype.then</code></a> 和 <a href=\"https://developer.mozilla.org/docs/Web/API/MutationObserver\">Mutation Observer API</a> 的回调函数使用微任务队列。</p>\n<p>宏任务队列可以是一个或多个队列，具体实现方式与浏览器有关。<br><code>setTimeout</code> <code>setInterval</code> <code>requestAnimationFrame</code> <code>requestIdleCallback</code> 以及其它事件 API 都使用宏任务。</p>\n<blockquote>\n<p>现代浏览器遵循 <a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#event-loop\">HTML 标准</a></p>\n</blockquote>\n<p>规范定义了如下几种<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#generic-task-sources\">通用任务源</a>：</p>\n<ul>\n<li>DOM 操作任务源</li>\n<li>用户交互任务源</li>\n<li>网络任务源</li>\n<li>导航和（历史）遍历任务源</li>\n<li>渲染任务源</li>\n</ul>\n<hr>\n<p>根据事件循环的特性，微任务队列中的所有微任务会依次出队直至队列为空。<br>任务也可以在队列中新增任务，这就可能导致队列中永远有任务。<br>由于在<strong>现代浏览器</strong>中，微任务队列<em>优于</em>宏任务队列运行。<br>引擎会调度微任务队列中的所有任务，直至队列为空。但宏任务则不是。<br>（即，一个宏任务，在宏任务队列中新增宏任务，再新增微任务，微任务先执行）<br>因此如果微任务队列不为空，宏任务将<em>无法</em>得到执行。反之则<strong>不会</strong>。</p>\n<pre><code class=\"language-js\">setTimeout(() =&gt; {\n    console.log(&quot;macro&quot;); // 永远不会运行\n});\nfunction recursiveMicroTask() {\n    queueMicrotask(() =&gt; {\n        console.log(&quot;micro&quot;);\n        recursiveMicroTask();\n    });\n}\nrecursiveMicroTask();\n</code></pre>\n<p>此外，在现代浏览器中，冒泡中的所有事件是按冒泡顺序先添加至宏任务队列中的，然后再执行之。</p>\n<blockquote>\n<p>摘自 <a href=\"https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/\">https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/</a></p>\n</blockquote>\n<pre><code class=\"language-html\">&lt;div class=&quot;outer&quot;&gt;\n    &lt;div class=&quot;inner&quot;&gt;&lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<pre><code class=\"language-js\">const outer = document.querySelector(&quot;.outer&quot;);\nconst inner = document.querySelector(&quot;.inner&quot;);\n\n// 监听元素属性变化\nnew MutationObserver(function () {\n    console.log(&quot;mutate&quot;);\n}).observe(outer, {\n    attributes: true,\n});\n\nfunction onClick() {\n    console.log(&quot;click&quot;);\n\n    setTimeout(function () {\n        console.log(&quot;timeout&quot;);\n    }, 0);\n\n    Promise.resolve().then(function () {\n        console.log(&quot;promise&quot;);\n    });\n\n    outer.setAttribute(&quot;data-random&quot;, Math.random());\n}\n\ninner.addEventListener(&quot;click&quot;, onClick);\nouter.addEventListener(&quot;click&quot;, onClick);\n</code></pre>\n<p>现代浏览器的结果都是：</p>\n<pre><code>click\npromise\nmutate\nclick\npromise\nmutate\ntimeout\ntimeout\n</code></pre>\n<iframe height=\"539.2\" style=\"width: 100%;\" scrolling=\"no\" title=\"event-loop\" src=\"https://codepen.io/YieldRay/embed/NWmKBmb?default-tab=js%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/NWmKBmb\">\n  event-loop</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<blockquote>\n<p>注： <a href=\"https://developer.mozilla.org/docs/Web/API/Window/setImmediate\"><code>setImmediate</code></a> 在现代浏览器中不存在（废弃），被 Node.js 实现（未废弃），参见下面的 Node.js 部分</p>\n</blockquote>\n<hr>\n<p>根据 HTML 标准，<a href=\"https://html.spec.whatwg.org/multipage/workers.html#workers\">Web workers</a> 在专用的<em>单独线程</em>上运行，其全局环境为 <a href=\"https://html.spec.whatwg.org/multipage/workers.html#workerglobalscope\">WorkerGlobalScope</a>。但 Javascript 中上下文之间是隔离的，因此只能通过事件通信，只有 Transferable 对象允许共享内存。<br>由于不共享内存，消息必须经过序列化（通过 <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm\">结构化克隆算法</a>）。我们手动创建的值，对象以及二进制数组都是可克隆的。但是 WebAPI 获取的对象，例如 DOM 就无法克隆了。（特别是在早期的 IE 中，其本质可能是一个 C++ 对象）</p>\n<blockquote>\n<p>Transferable 对象的所有权问题，超出了本文的范畴</p>\n</blockquote>\n<p>根据 HTML 标准，<a href=\"https://html.spec.whatwg.org/multipage/worklets.html#worklets\">Worklets</a> 可独立于主 Javascript 环境运行脚本，其全局环境为 <a href=\"https://html.spec.whatwg.org/multipage/worklets.html#workletglobalscope\">WorkletGlobalScope</a>。Worklets 与线程无关（可以运行在任何线程，包括主线程）。既然独立，那就可以实现与其它环境并行。<br>目前在 <a href=\"https://html.spec.whatwg.org/multipage/references.html#refsCSSPAINT\">CSS Painting API</a> 和 <a href=\"https://html.spec.whatwg.org/multipage/references.html#refsWEBAUDIO\">Web Audio API</a> 中提供。</p>\n<h1>Node.js</h1>\n<p><img src=\"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F6b288555862049b4b5cd7f19e2ae909f?format=webp&width=2000\" alt=\"\"></p>\n<blockquote>\n<p>摘自：<a href=\"https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick\">https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick</a></p>\n</blockquote>\n<p>在 Node.js 中每个队列称为一个阶段（phase）。当事件循环进入给定阶段时，将执行特定于该阶段的任何操作，然后执行该阶段队列中的回调，直到队列耗尽或当最大回调数已执行。<br>当队列耗尽或达到回调限制时，事件循环将进入下一阶段，依此类推。</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>Node.js 事件循环负责从 libuv 的队列中取出回调函数，并（调用 v8）在 JavaScript 环境中执行它们。<br>libuv 事件循环向操作系统轮询事件，当事件发生时，libuv 将对应回调函数添加到其内部的队列中。</p>\n</div>\n<ul>\n<li><strong>计时器 (timers)</strong>：此阶段执行被 <code>setTimeout()</code> 和 <code>setInterval()</code> 调度的回调。</li>\n<li><strong>挂起的回调 (pending callbacks)</strong>：执行延迟到下一个循环迭代的 I/O 回调。</li>\n<li><strong>空闲、准备 (idle, prepare)</strong>：仅在内部使用。</li>\n<li><strong>轮询 (poll)</strong>：检索新的 I/O 事件；执行与 I/O 相关的回调（几乎所有回调都是如此，除了关闭回调、计时器调度的回调和 <a href=\"https://nodejs.org/zh-cn/learn/asynchronous-work/understanding-setimmediate\"><code>setImmediate()</code></a> 调度的回调）；适当时，Node.js 将在此处阻塞。</li>\n<li><strong>检查 (check)</strong>：在此处调用 <a href=\"https://nodejs.org/api/timers.html#setimmediatecallback-args\"><code>setImmediate()</code></a> 回调。</li>\n<li><strong>关闭回调 (close callbacks)</strong>：一些关闭回调，例如 <code>socket.on(&#39;close&#39;, ...)</code>.</li>\n</ul>\n<pre><code>   ┌───────────────────────────┐\n┌─&gt;│           timers          │\n│  └─────────────┬─────────────┘\n│  ┌─────────────┴─────────────┐\n│  │     pending callbacks     │\n│  └─────────────┬─────────────┘\n│  ┌─────────────┴─────────────┐\n│  │       idle, prepare       │\n│  └─────────────┬─────────────┘      ┌───────────────┐\n│  ┌─────────────┴─────────────┐      │   incoming:   │\n│  │           poll            │&lt;─────┤  connections, │\n│  └─────────────┬─────────────┘      │   data, etc.  │\n│  ┌─────────────┴─────────────┐      └───────────────┘\n│  │           check           │\n│  └─────────────┬─────────────┘\n│  ┌─────────────┴─────────────┐\n└──┤      close callbacks      │\n   └───────────────────────────┘\n</code></pre>\n<p>Node.js 提供了 <a href=\"https://nodejs.org/api/process.html#processnexttickcallback-args\"><code>process.nextTick(callback[, ...args])</code></a>，而 <a href=\"https://nodejs.org/api/globals.html#queuemicrotaskcallback\"><code>queueMicrotask(callback)</code></a>（<a href=\"https://github.com/nodejs/node/blob/ae8280c95df2ef7d02b37df6bc0c5b307147a5e5/lib/internal/process/task_queues.js#L160\">源码</a>）用于运行 v8 微任务队列。</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>微任务队列由 v8 维护（MicrotasksQueue），并在执行完一个宏任务（例如一个阶段的事件循环）后，会检查并执行微任务队列中的所有任务，直到队列为空</p>\n</div>\n<p>Node.js 还实现了 <code>process.nextTick</code>，next tick 即事件循环继续之前。<br>可以将 <code>process.nextTick</code> 看作是 Node.js 自己实现的 <code>queueMicrotask</code> 版本，并且它<strong>运行在 v8 微任务之前</strong>。<br>即 <code>process.nextTick()</code> 队列在 Promise 微任务队列之前被排空（具体信息，Node.js 文档中已经很详细说明了）</p>\n<p>当我们实现一个函数执行回调时，保证回调函数异步执行往往很重要。不过如果使用 Promise 则没有这些烦恼。</p>\n<pre><code class=\"language-js\">function definitelyAsync(arg, cb) {\n    if (arg) {\n        // 为了互操性建议使用 queueMicrotask 替代\n        process.nextTick(cb);\n        return;\n    }\n\n    fs.stat(&quot;file&quot;, cb);\n}\n</code></pre>\n<hr>\n<p>Node.js 同样支持 Worker 多线程（通过 <a href=\"https://nodejs.org/api/worker_threads.html\">worker_threads</a>，注意并非是 Web Workers，不过接口确实也仿照其实现）。<br>众所周知，Node.js 的异步 I/O 机制非常适合 I/O 敏感型任务（并发），对于 CPU 敏感型任务则需要考虑多线程（并行）支持。<br>排除 Worker 线程，JavaScript 几乎可以说是单线程。为了实现异步 I/O，Node.js 显然要通过 libuv 的多个线程（线程池）来实际调用系统调用，并在操作完成时通知 JavaScript 线程。</p>\n<p>下面以 <a href=\"https://github.com/nodejs/node/blob/7f0b80525acec4ef582ab59b3cbce4e82338c93c/lib/fs.js#L548\"><code>fs.open()</code></a> 为例</p>\n<pre><code class=\"language-js\">/**\n * Asynchronously opens a file.\n * @param {string | Buffer | URL} path\n * @param {string | number} [flags]\n * @param {string | number} [mode]\n * @param {(err?: Error, fd?: number) =&gt; any} callback\n * @returns {void}\n */\nfunction open(path, flags, mode, callback) {\n    path = getValidatedPath(path);\n    if (arguments.length &lt; 3) {\n        callback = flags;\n        flags = &quot;r&quot;;\n        mode = 0o666;\n    } else if (typeof mode === &quot;function&quot;) {\n        callback = mode;\n        mode = 0o666;\n    } else {\n        mode = parseFileMode(mode, &quot;mode&quot;, 0o666);\n    }\n    const flagsNumber = stringToFlags(flags);\n    callback = makeCallback(callback); // 包装回调函数\n\n    const req = new FSReqCallback(); // 创建请求\n    req.oncomplete = callback; // 请求对象上附加回调函数\n    // 将请求对象发送给 libuv\n    binding.open(pathModule.toNamespacedPath(path), flagsNumber, mode, req);\n}\n</code></pre>\n<p><code>fs.open()</code> 函数最终通过 <a href=\"https://github.com/nodejs/node/blob/7f0b80525acec4ef582ab59b3cbce4e82338c93c/src/node_file.cc#L1902\">node_file.cc</a> 调用 libuv 的 <code>uv_fs_open()</code> 方法</p>\n<p>以 unix 系统为例，实现非阻塞 I/O 的背后可能是 <code>read</code> <code>select</code> <code>poll</code> <code>epoll</code> <code>pselect</code> <code>kqueue</code> <code>evport</code> 等等系统调用。</p>\n<p><a href=\"https://docs.libuv.org/en/v1.x/\">libuv</a> 作为一个跨平台的 C 库，在 unix 下使用 epoll，在 BSD 下使用 kqueue，在 windows 下使用 IOCP，在 SunOS 下使用 event ports 实现。</p>\n<p><img src=\"https://docs.libuv.org/en/v1.x/_images/architecture.png\" alt=\"libuv architecture\"></p>\n<p>网络异步 I/O 可以通过对应系统调用实现，因此只需要单线程（事件循环线程）；而文件 I/O 则是通过线程池调用多个同步的系统调用模拟实现异步。</p>\n<blockquote>\n<p>具体设计参见 <a href=\"https://docs.libuv.org/en/v1.x/design.html\">libuv 文档</a></p>\n</blockquote>\n<h1>参见</h1>\n<p><a href=\"https://stackoverflow.com/questions/25915634\">https://stackoverflow.com/questions/25915634</a><br><a href=\"https://zh.javascript.info/event-loop\">https://zh.javascript.info/event-loop</a><br><a href=\"https://v8.dev/blog/fast-async\">https://v8.dev/blog/fast-async</a><br><a href=\"https://limeii.github.io/2019/05/js-eventloop/\">https://limeii.github.io/2019/05/js-eventloop/</a><br><a href=\"https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif\">https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif</a><br><a href=\"https://dev.to/tdo95/javascript-event-loop-everything-you-need-to-know-explained-in-simple-terms-fg0\">https://dev.to/tdo95/javascript-event-loop-everything-you-need-to-know-explained-in-simple-terms-fg0</a><br><a href=\"https://www.bbss.dev/posts/eventloop/\">https://www.bbss.dev/posts/eventloop/</a><br><a href=\"https://www.thisdot.co/blog/deep-dive-into-node-js-with-james-snell\">https://www.thisdot.co/blog/deep-dive-into-node-js-with-james-snell</a></p>\n","tags":["js","node.js"]},{"id":"shiki-demo","url":"https://yieldray.fun/posts/shiki-demo","title":"shiki demo","date_published":"2024-07-16T00:00:00.000Z","date_modified":"2024-07-16T00:00:00.000Z","content_text":"<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>Highlights information that users should take into account, even when skimming.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-light-bulb mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>\n<p>Optional information to help a user be more successful.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-report mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Important</p>\n<p>Crucial information necessary for users to succeed.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-alert mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<p>Critical content demanding immediate user attention due to potential risks.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-caution\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-stop mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>\n<p>Negative potential consequences of an action.</p>\n</div>\n<pre><code class=\"language-ts\">console.log(&quot;hewwo&quot;); // [!code --]\nconsole.log(&quot;hello&quot;); // [!code ++]\nconsole.log(&quot;goodbye&quot;);\n</code></pre>\n<pre><code class=\"language-ts\">console.log(&quot;Not highlighted&quot;);\nconsole.log(&quot;Highlighted&quot;); // [!code highlight]\nconsole.log(&quot;Not highlighted&quot;);\n</code></pre>\n<pre><code class=\"language-ts\">console.log(&quot;Not focused&quot;);\nconsole.log(&quot;Focused&quot;); // [!code focus]\nconsole.log(&quot;Not focused&quot;);\n</code></pre>\n<pre><code class=\"language-ts\">// [!code word:Hello]\nconst message = &quot;Hello World&quot;;\nconsole.log(message); // prints Hello World\n</code></pre>\n<pre><code class=\"language-ts\">console.log(&quot;No errors or warnings&quot;);\nconsole.error(&quot;Error&quot;); // [!code error]\nconsole.warn(&quot;Warning&quot;); // [!code warning]\n</code></pre>\n","tags":["demo"]},{"id":"how-to-read-an-rfc","url":"https://yieldray.fun/posts/how-to-read-an-rfc","title":"如何阅读RFC","date_published":"2024-07-14T22:22:22.000Z","date_modified":"2024-07-14T22:22:22.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://www.mnot.net/blog/2018/07/31/read_rfc\">How to Read an RFC</a> by.Mark Nottingham<br>此文也被 <a href=\"https://www.ietf.org/blog/how-read-rfc/\">IETF 官方博客</a> 刊载</p>\n</blockquote>\n<p>无论好坏，RFC 是我们指定许多互联网协议的方式。这些文件有时被开发者视为圣典，仔细解析寻找隐藏的意义；而有时又被忽视，因为它们难以理解。<br>这常常令人沮丧，更重要的是，导致互操作性和安全问题。</p>\n<p>然而，如果对它们的构建和发布有一些了解，就更容易理解你在看什么。以下是我的看法，基于我在 HTTP 和一些<a href=\"https://datatracker.ietf.org/person/Mark%20Nottingham\">其它事情</a>上的经验。</p>\n<h3>从哪里开始？</h3>\n<p>找到 RFC 的标准位置是 <a href=\"https://rfc-editor.org/\">RFC Editor 网站</a>。然而，正如我们将在下面看到的，它缺少一些重要信息，所以大多数人使用 <a href=\"https://tools.ietf.org/\">tools.ietf.org</a>。</p>\n<p>找到正确的 RFC 可能很困难，因为数量很多（目前接近 9000 个！）。显然，你可以使用通用的网络搜索引擎找到它们，RFC Editor 网站上也有一个优秀的搜索功能。</p>\n<p>另一个选择是我创建的 <a href=\"https://rfc.fyi/\">rfc.fyi</a>，可通过标题和关键词搜索 RFC，并通过标签进行探索。</p>\n<p>众所周知，纯文本的 RFC 难以阅读，甚至可以说很难看，但情况即将改善；RFC Editor 正在完成一个<a href=\"https://www.rfc-editor.org/rse/format-faq/\">新的 RFC 格式</a>，呈现更加美观，同时还有自定义选项。同时，如果你想要更易用的 RFC，可以使用第三方存储库；例如，<a href=\"https://greenbytes.de/tech/webdav/\">greenbytes</a> 保留了一份与 WebDAV 相关的 RFC 列表，[HTTP 工作组] (<a href=\"https://httpwg.org/specs/)%E7%BB%B4%E6%8A%A4%E4%BA%86%E4%B8%80%E4%BA%9B%E4%B8%8E\">https://httpwg.org/specs/)维护了一些与</a> HTTP 相关的 RFC。</p>\n<h3>这是什么类型的 RFC？</h3>\n<p>所有 RFC 的顶部都有一个横幅，看起来像这样：</p>\n<pre><code class=\"language-plain\">Internet Engineering Task Force (IETF)                  R. Fielding, Ed.\nRequest for Comments: 7230                              Adobe\nObsoletes: 2145, 2616                                   J. Reschke, Ed.\nUpdates: 2817, 2818                                     greenbytes\nCategory: Standards Track                               June 2014\nISSN: 2070-1721\n</code></pre>\n<p>在左上角，这里写着“Internet Engineering Task Force (IETF, 互联网工程特别工作组)”。这表明这是 IETF 的产品；尽管不广为人知，还有其它不需要 IETF 共识就可以发布 RFC 的方式，例如<a href=\"https://www.rfc-editor.org/about/independent/\">独立流</a>。</p>\n<p>实际上，有许多“流”可以发布文档。<strong>只有 IETF 流表明整个 IETF 已经审查并对协议的规范达成共识</strong>。</p>\n<p>较旧的文档（大约在 RFC5705 之前）在那里写着“网络工作组 (Network Working Group)”，所以你需要更多地挖掘才能确定它们是否代表 IETF 共识；首先查看“备忘录状态”部分，以及<a href=\"https://www.rfc-editor.org/\">RFC 编辑器网站</a>。</p>\n<p>在那之下是“Request for Comments”编号。<strong>如果此处写着的是“Internet-Draft (互联网草案)”，那么它就不是 RFC</strong>；它只是一个提案，<em>任何人</em>都可以<a href=\"https://datatracker.ietf.org/submit/\">写一个</a>。若某物仅仅是互联网草案并不意味着它会被 IETF 采纳。</p>\n<p>**Category (类别)**可以是“Standards Track”、“Informational”、“Experimental”或“Best Current Practice”之一。这些之间的区别有时是模糊的，但如果是由 IETF 生产的（见上文），那么它已经经过了足够量的审查。然而，请注意，Informational 和 Experimental <em>不是</em>标准，即使由 IETF 共识发布。</p>\n<p>最后，**authors (作者)**列在标题的右侧。与学术界不同，这不是一份全面的贡献者名单；通常，这在文档底部的“Acknowledgments (致谢)”部分完成。在 RFC 中，这字面意思是“谁写了这篇文档”。通常，你会看到“Ed. (编辑)”附在后面，这表明他们是以编辑身份行事，通常是因为文本是预先存在的（例如，当 RFC 被修订时）。</p>\n<h3>它是最新的吗？</h3>\n<p>RFC 是一系列存档文档；它们不能改变，甚至一个字符都不能（以 <a href=\"https://tools.ietf.org/rfcdiff?url1=rfc7158&url2=rfc7159\">RFC7158 和 RFC7159 之间的差异</a> 作为极端示例；它们把年份弄错了。</p>\n<p>因此，知道你正在查看正确的文档很重要。标题包含一些有助于此的元数据：</p>\n<ul>\n<li><p>**Obsoletes (废止)**列出该文档完全取代的 RFC；即，你应该使用这个文档，而不是那个。注意，当较新的版本出现时，旧版本的协议不一定被废止；例如，HTTP/2 没有废止 HTTP/1.1，因为实现旧协议仍然是合法的（也是必要的）。然而，RFC7230 确实废止了 RFC2616，因为它是该协议的参考。</p>\n</li>\n<li><p>**Updates (更新)**列出该文档对其进行了实质性更改的 RFC；换句话说，如果你在阅读那个其它文档，你也应该阅读这个。</p>\n</li>\n</ul>\n<p>不幸的是，ASCII 文本 RFC（例如，在 RFC 编辑器网站上）不会告诉你更新或废止了你当前查看的文档。这就是为什么大多数人使用 tools.ietf.org 上的 RFC 存储库，它将此信息放在一个<a href=\"https://datatracker.ietf.org/doc/html/rfc2616\">横幅中</a>：</p>\n<pre><code class=\"language-plain\">[Docs] [txt|pdf] [draft-ietf-http...] [Tracker] [Diff1] [Diff2] [Errata]\n\nObsoleted by: 7230, 7231, 7232, 7233, 7234, 7235          DRAFT STANDARD\nUpdated by: 2817, 5785, 6266, 6585                          Errata Exist\n</code></pre>\n<p>tools 页面上的每个数字都是一个链接，所以你可以轻松找到当前的文档。</p>\n<p>即使是最新的 RFC 也常常有问题。在 tools 横幅上，你还会看到右侧有一个警告“勘误存在”，以及上面的勘误链接。</p>\n<p>**Errata (勘误)**是对文档的更正和澄清，不值得发布新的 RFC。有时它们会对 RFC 的实现产生重大影响（例如，如果规范中的错误导致了重大误解），所以它们值得一读。</p>\n<p>例如，这里是 <a href=\"https://www.rfc-editor.org/errata_search.php?rfc=7230\">RFC7230 的勘误</a>。阅读勘误时，请记住它们的状态；许多被拒绝，因为有人误读了规范。</p>\n<h3>理解上下文</h3>\n<p>比你想象的更常见的是，开发者看了 RFC 中的一句话，按照他们看到的实现，结果做了与作者意图相反的事情。</p>\n<p>这是因为很难以一种当被选择性阅读时不会被误解的方式编写规范（就像任何圣典一样）。</p>\n<p>因此，有必要不仅阅读直接相关的文本，还要（至少）阅读它引用的任何内容，无论是在同一规范中还是在不同的规范中。必要时，阅读任何可能相关的部分会有很大帮助，如果你不能阅读整个文档的话。</p>\n<p>例如，HTTP 消息头被<a href=\"https://httpwg.org/specs/rfc7230.html#http.message\">定义</a>为用 CRLF 分隔，但如果你向下跳到<a href=\"https://httpwg.org/specs/rfc7230.html#message.robustness\">这里</a>，你会看到“接收者可以将单个 LF 识别为行终止符，并忽略任何前面的 CR。”很明显，对吧？</p>\n<p>还要记住，许多协议设置了<a href=\"https://www.iana.org/protocols\">IANA 注册表</a>来管理它们的扩展点；这些，而不是规范，是事实的来源。例如，HTTP 方法的规范列表在<a href=\"https://www.iana.org/assignments/http-methods/http-methods.xhtml\">这个注册表</a>中，而不是任何 HTTP 规范。</p>\n<h3>解释要求</h3>\n<p>几乎所有的 RFC 在顶部附近都有类似这样的样板：</p>\n<pre><code class=\"language-plain\">本文件中的关键字 &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;, &quot;SHALL NOT&quot;,\n&quot;SHOULD&quot;, &quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;, &quot;NOT RECOMMENDED&quot;, &quot;MAY&quot;, 和\n&quot;OPTIONAL&quot; 应按照 BCP 14 [RFC2119] [RFC8174] 中的描述进行解释，当且仅当\n它们以全大写形式出现时，如此处所示。\n</code></pre>\n<p>这些 <a href=\"https://datatracker.ietf.org/doc/html/rfc2119\">RFC2119</a> 关键字有助于定义互操作性，但它们有时也会让开发者感到困惑。很常见的是，规范会说：</p>\n<pre><code class=\"language-plain\">Foo 消息 不得(MUST NOT) 包含 Bar 标头。\n</code></pre>\n<p>这个要求是针对协议工件“Foo 消息”的。如果你发送一个，很明显它需要<em>不包含</em> Bar 标头；如果你包括了一个，它就<em>不是</em>一个符合规范的消息。</p>\n<p>然而，接收者的行为就不那么清晰了；如果你看到一个包含 Bar 标头的 Foo 消息，你该怎么办？</p>\n<p>有些开发者会拒绝包含它的消息，即使规范没有说要这样做。其他人仍会处理消息，但会去掉 Bar 标头，或者忽略它——即使规范明确说所有头都需要处理。</p>\n<p>所有这些都可能无意中导致互操作性问题。正确的做法是按照正常的标头处理流程，除非有具体要求相反。</p>\n<p>这是因为一般来说，规范是这样编写的：行为是明确规定的；换句话说，所有没有明确禁止的行为都是允许的。因此，过度解读规范可能会无意中造成伤害，因为你会引入新的行为，其他人不得不绕过。</p>\n<p>在理想的世界中，规范将以处理消息的人的行为为基础定义，如下所示：</p>\n<pre><code class=\"language-plain\">发送 Foo 消息的发送者 不得(MUST NOT) 包含 Bar 标头。\n接收包含 Bar 标头的 Foo 消息的接收者 必须(MUST) 忽略 Bar 标头，但不得删除它。\n</code></pre>\n<p>在没有这种情况的情况下，最好在规范的其它地方寻找关于错误处理的更一般的建议（例如，HTTP 的<a href=\"https://httpwg.org/specs/rfc7230.html#conformance\">一致性和错误处理</a>部分）。</p>\n<p>此外，记住要求的<em>目标</em>；大多数规范都有一套高度发达的术语，用于区分协议中的不同角色。</p>\n<p>例如，HTTP 有<a href=\"https://httpwg.org/specs/rfc7230.html#intermediaries\">代理</a>，它们是一种中介，既实现了客户端又实现了服务器（但不是用户代理或源服务器）；它们需要注意针对所有这些角色的要求。</p>\n<p>同样，HTTP 在某些要求中区分了“生成”消息和仅仅“转发”消息，具体情况而定。注意这种特定术语可以节省你很多猜测。</p>\n<h4>SHOULD</h4>\n<p>是的，SHOULD 值得有自己的部分。这个模棱两可的术语困扰着许多 RFC，尽管有努力在消除它。RFC2119 将其描述为：</p>\n<pre><code class=\"language-plain\">SHOULD 这个词，或形容词 &quot;RECOMMENDED (推荐)&quot;，意味着在特定情况下可能存在忽略某个条目的有效理由，\n       但必须理解并仔细权衡所有影响，然后选择不同的做法。\n</code></pre>\n<p>在实践中，作者经常使用 SHOULD 和 SHOULD NOT 来表示 “我们希望你这样做，但我们知道不能总是要求这样做。”</p>\n<p>例如，在<a href=\"https://httpwg.org/specs/rfc7231.html#method.overview\">HTTP 方法概述</a>中，我们看到：</p>\n<pre><code class=\"language-plain\">当收到一个未被识别或未由源服务器实现的请求方法时，源服务器 应(SHOULD) 响应 501 (未实现) 状态码。\n当收到一个源服务器知道但不允许用于目标资源的请求方法时，源服务器 应(SHOULD) 响应 405 (方法不允许) 状态码。\n</code></pre>\n<p>这些是 SHOULD 而不是 MUST，因为服务器可能会合理地决定采取其它行动；如果请求来自被认为是攻击者的客户端，它可能会断开连接，或者如果资源需要 HTTP 身份验证，它可能会在到达 405 之前用 401 (未认证) 强制执行。</p>\n<p>SHOULD <em>不</em>意味着服务器可以随意忽略一个要求，仅因为它不想遵守。</p>\n<p>有时，我们会看到一个 SHOULD，像这样：</p>\n<pre><code class=\"language-plain\">生成包含有效负载主体的消息的发送者 应(SHOULD) 生成该消息中的 Content-Type 标头字段，除非发送者不知道所包含表示的预期媒体类型。\n</code></pre>\n<p>注意“除非”一词，它指定了 SHOULD 允许的“特定情况”。可以说，这可以指定为 MUST，因为除非条款仍然适用，但这种规范风格相当常见。</p>\n<h3>阅读示例</h3>\n<p>另一个非常常见的陷阱是浏览规范中的示例，并按照它们所做的实现。</p>\n<p>不幸的是，示例通常是作者最少关注的部分，因为它们需要随着协议的每次更改而更新。</p>\n<p>因此，它们通常是规范中最不可靠的部分。是的，作者在发布前应该绝对仔细检查示例，但错误确实会溜进去。</p>\n<p>此外，即使是完美的示例也可能不是为了说明你正在寻找的协议方面；它们通常为了简洁而被截断，或者在解码步骤之后显示。</p>\n<p>尽管需要花费更多时间，但最好阅读实际文本；示例不是规范。</p>\n<h3>关于 ABNF</h3>\n<p><a href=\"https://datatracker.ietf.org/doc/html/rfc5234\">增强 BNF</a> 经常用于定义协议工件。例如：</p>\n<pre><code class=\"language-plain\">FooHeader = 1#foo\nfoo       = 1*9DIGIT [ &quot;;&quot; &quot;bar&quot; ]\n</code></pre>\n<p>一旦习惯了，ABNF 提供了一种易于理解的协议元素的草图。</p>\n<p>然而，ABNF 是“理想化的”——它识别了消息的理想形式，而你生成的那些消息确实需要匹配它。它没有指定如何处理不匹配的接收消息。事实上，许多规范根本没有说明 ABNF 与处理要求的关系。</p>\n<p>如果你试图严格执行 ABNF，大多数协议会严重失败，但有时这很重要。在上面的例子中，空格不允许出现在分号周围，但你可以打赌，有些人会把它放在那里，有些实现会接受它。</p>\n<p>所以，一定要阅读 ABNF 周围的文本以获取附加要求或上下文，并意识到在没有直接要求的情况下，你可能需要调整解析以比 ABNF 暗示的更宽容地接受输入。</p>\n<p>一些规范开始承认 ABNF 的理想化性质，并指定包含错误处理的显式解析算法。当指定时，应严格遵循这些算法，以确保互操作性。</p>\n<h3>安全考虑</h3>\n<p>自从 <a href=\"https://datatracker.ietf.org/doc/html/rfc3552\">RFC3552</a> 以来，RFC 样板中包含了“Security Considerations (安全考虑)”部分。</p>\n<p>因此，很少有 RFC 在没有大量安全部分的情况下发布；审查过程不允许草案仅仅说“该协议没有安全考虑”。</p>\n<p>所以，无论你是在实现还是部署协议，都值得阅读并确保理解安全考虑部分；如果你不这样做，很可能会在未来的某个时候遇到问题。</p>\n<p>跟随其引用（如果有的话）也是一个好主意。如果没有，尝试查找使用的一些术语，以了解所讨论的问题。</p>\n<h3>了解更多</h3>\n<p>如果 RFC 没有回答你的问题，或者你不确定其文本的意图，最好的做法是找到最相关的<a href=\"https://datatracker.ietf.org/wg/\">工作组</a>，并在他们的邮件列表中提问。如果没有活跃的工作组覆盖相关主题，尝试适当的<a href=\"https://ietf.org/topics/areas/\">领域</a>的邮件列表。</p>\n<p>提交勘误通常不是你应该采取的第一步——先和某人谈谈。</p>\n<p>许多工作组现在使用 Github 来管理他们的规范；如果你对一个活跃的规范有问题，可以提出一个问题。如果它已经是 RFC，通常最好使用邮件列表，除非你找到相反的指示。</p>\n<p>我相信还有更多关于如何阅读 RFC 的内容可以写，有些人会质疑我在这里写的内容，但这是我对它们的看法。希望它对你有用。</p>\n","tags":["rfc"]},{"id":"maglev","url":"https://yieldray.fun/posts/maglev","title":"Maglev - V8’s Fastest Optimizing JIT","date_published":"2024-07-08T15:58:30.000Z","date_modified":"2024-07-08T15:58:30.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://v8.dev/blog/maglev\">https://v8.dev/blog/maglev</a><br>备注：<a href=\"https://nodejs.org/en/blog/announcements/v22-release-announce\">Node.js 22</a> 已默认启用 Maglev 编译器</p>\n</blockquote>\n<p>在Chrome M117中，我们引入了一种新的优化编译器：Maglev。Maglev位于现有的Sparkplug和TurboFan编译器之间，肩负着快速生成足够优秀代码的任务。</p>\n<h2>背景</h2>\n<p>直到2021年，V8有两个主要的执行层次：Ignition（解释器）和<a href=\"https://v8.dev/docs/turbofan\">TurboFan</a>，V8的优化编译器，专注于峰值性能。所有JavaScript代码首先被编译为Ignition字节码，并通过解释执行。在执行过程中，V8会跟踪程序的行为，包括对象形状和类型的跟踪。运行时执行元数据和字节码都被传递给优化编译器，以生成高性能、通常是投机性的机器代码，这些代码的运行速度远快于解释器。</p>\n<p>这些改进在<a href=\"https://browserbench.org/JetStream2.1/\">JetStream</a>等基准测试中清晰可见，JetStream是一组传统的纯JavaScript基准测试，测量启动、延迟和峰值性能。TurboFan使V8运行该套件的速度提高了4.35倍！JetStream对稳态性能的强调比过去的基准测试（如<a href=\"https://v8.dev/blog/retiring-octane\">已退休的Octane基准测试</a>）有所减少，但由于许多项目的简单性，优化后的代码仍然占据了大部分时间。</p>\n<p><a href=\"https://browserbench.org/Speedometer2.1/\">Speedometer</a>是一种与JetStream不同的基准测试套件。它旨在通过计时模拟用户交互来衡量Web应用的响应能力。与较小的静态独立JavaScript应用不同，该套件由完整的网页组成，其中大多数是使用流行框架构建的。与大多数网页加载期间一样，Speedometer的项目在运行紧密的JavaScript循环上花费的时间要少得多，而更多地执行与浏览器其他部分交互的大量代码。</p>\n<p>TurboFan在Speedometer上仍然有很大的影响：它的运行速度提高了超过1.5倍！但其影响显然比在JetStream上要小得多。这部分是因为完整的网页<a href=\"https://v8.dev/blog/real-world-performance#making-a-real-difference\">在纯JavaScript上的花费时间较少</a>。但部分原因是基准测试花费了大量时间在没有足够热的函数上，这些函数无法被TurboFan优化。</p>\n<p><img src=\"https://v8.dev/_img/maglev/I-IT.svg\" alt=\"\"></p>\n<p>未优化和优化执行的Web性能基准测试</p>\n<p>本文中的所有基准测试分数都是在13英寸M2 Macbook Air上使用Chrome 117.0.5897.3测量的。</p>\n<p>由于Ignition和TurboFan之间的执行速度和编译时间差异如此之大，我们在2021年引入了一个新的基线JIT，名为<a href=\"https://v8.dev/blog/sparkplug\">Sparkplug</a>。它旨在几乎瞬间将字节码编译为等效的机器代码。</p>\n<p>在JetStream上，与Ignition相比，Sparkplug显著提高了性能（+45%）。即使在有TurboFan的情况下，我们仍然看到性能有了显著提高（+8%）。在Speedometer上，我们看到相对于Ignition的41%的改进，使其接近TurboFan的性能，并且相对于Ignition + TurboFan有22%的改进！由于Sparkplug非常快，我们可以很容易地广泛部署它并获得一致的加速。如果代码不完全依赖于容易优化的长时间运行的紧密JavaScript循环，它是一个很好的补充。</p>\n<p><img src=\"https://v8.dev/_img/maglev/I-IS-IT-IST.svg\" alt=\"\"></p>\n<p>添加了Sparkplug的Web性能基准测试</p>\n<p>然而，Sparkplug的简单性对其能提供的加速有一个相对较低的上限。这从Ignition + Sparkplug和Ignition + TurboFan之间的巨大差距中可以清楚地看出。</p>\n<p>这就是Maglev的用武之地，我们的新优化JIT，它生成的代码比Sparkplug代码快得多，但生成速度比TurboFan快得多。</p>\n<h2>Maglev：一个简单的SSA基础JIT编译器</h2>\n<p>当我们开始这个项目时，我们看到了两条前进的道路，以弥补Sparkplug和TurboFan之间的差距：要么尝试使用Sparkplug的单遍方法生成更好的代码，要么构建一个具有中间表示（IR）的JIT。由于我们认为在编译过程中完全没有IR可能会严重限制编译器，因此我们决定采用一种传统的静态单赋值（SSA）基础的方法，使用CFG（控制流图），而不是TurboFan更灵活但缓存不友好的节点海表示。</p>\n<p>编译器本身设计得快速且易于工作。它有一组最小的遍和一个简单的单一IR，编码了专门的JavaScript语义。</p>\n<h2>预处理</h2>\n<p>首先，Maglev对字节码进行预处理，以找到分支目标，包括循环和循环中的变量赋值。此遍还收集活动信息，编码哪些变量中的值在哪些表达式中仍然需要。这些信息可以减少编译器稍后需要跟踪的状态量。</p>\n<h2>SSA</h2>\n<p><img src=\"https://v8.dev/_img/maglev/graph.svg\" alt=\"\"></p>\n<p>命令行上的Maglev SSA图打印输出</p>\n<p>Maglev对帧状态进行抽象解释，创建表示表达式求值结果的SSA节点。变量赋值通过将这些SSA节点存储在相应的抽象解释器寄存器中来模拟。在分支和切换的情况下，所有路径都被评估。</p>\n<p>当多个路径合并时，抽象解释器寄存器中的值通过插入所谓的Phi节点来合并：这些值节点知道在运行时根据选择的路径选择哪个值。</p>\n<p>循环可以“回到过去”合并变量值，当变量在循环体内被赋值时，数据从循环结束向循环头部反向流动。这就是预处理数据派上用场的地方：由于我们已经知道哪些变量在循环内被赋值，我们可以在开始处理循环体之前预先创建循环Phi。在循环结束时，我们可以用正确的SSA节点填充Phi输入。这使得SSA图生成成为单向遍，而不需要“修复”循环变量，同时最小化需要分配的Phi节点数量。</p>\n<h2>已知节点信息</h2>\n<p>为了尽可能快，Maglev尽可能多地一次性完成。与其在后期优化阶段构建通用JavaScript图并降低其复杂性，这是一种理论上干净但计算上昂贵的方法，Maglev在图构建过程中尽可能多地完成。</p>\n<p>在图构建过程中，Maglev会查看在未优化执行期间收集的运行时反馈元数据，并为观察到的类型生成专门的SSA节点。如果Maglev看到<code>o.x</code>并从运行时反馈中知道<code>o</code>总是具有一个特定的形状，它将生成一个SSA节点，在运行时检查<code>o</code>是否仍然具有预期的形状，然后是一个廉价的<code>LoadField</code>节点，它通过偏移进行简单访问。</p>\n<p>此外，Maglev会生成一个旁节点，现在它知道了<code>o</code>的形状，使得以后不再需要检查形状。如果Maglev后来遇到一个没有反馈的操作，这种在编译期间学到的信息可以作为第二个反馈来源。</p>\n<p>运行时信息可以有多种形式。有些信息需要在运行时检查，比如之前描述的形状检查。其他信息可以通过注册对运行时的依赖而无需运行时检查。实际上不变的全局变量（在初始化和Maglev看到其值之间未更改）属于这一类：Maglev不需要生成代码来动态加载和检查它们的身份。Maglev可以在编译时加载值并将其直接嵌入机器代码中；如果运行时更改了该全局变量，它也会负责使该机器代码失效并进行去优化。</p>\n<p>某些形式的信息是不稳定的。这种信息只能在编译器确定它不会更改的情况下使用。例如，如果我们刚刚分配了一个对象，我们知道它是一个新对象，可以完全跳过昂贵的写屏障。一旦有了另一个潜在的分配，垃圾收集器可能已经移动了对象，我们现在需要发出这样的检查。其他信息是稳定的：如果我们从未见过任何对象从具有某个特定形状过渡出去，那么我们可以注册对此事件的依赖（任何对象从该特定形状过渡出去），并且不需要重新检查对象的形状，即使是在调用具有未知副作用的未知函数之后。</p>\n<h2>去优化</h2>\n<p>鉴于Maglev可以使用在运行时检查的投机信息，Maglev代码需要能够进行去优化。为实现这一点，Maglev将抽象解释器帧状态附加到可以去优化的节点。此状态将解释器寄存器映射到SSA值。这种状态在代码生成期间变成元数据，提供从优化状态到未优化状态的映射。去优化器解释这些数据，从解释器帧和机器寄存器中读取值并将它们放入解释所需的位置。这建立在与TurboFan使用的相同的去优化机制之上，使我们能够共享大部分逻辑并利用现有系统的测试。</p>\n<h2>表示选择</h2>\n<p>根据<a href=\"https://tc39.es/ecma262/#sec-ecmascript-language-types-number-type\">规范</a>，JavaScript数字表示64位浮点值。这并不意味着引擎必须总是将它们存储为64位浮点值，特别是因为实际上许多数字是小整数（例如数组索引）。V8尝试将数字编码为31位标记整数（内部称为“小整数”或“Smi”），以节省内存（由于<a href=\"https://v8.dev/blog/pointer-compression\">指针压缩</a>为32位），并提高性能（整数操作比浮点操作快）。</p>\n<p>为了使数值密集的JavaScript代码运行得更快，选择值节点的最佳表示非常重要。与解释器和Sparkplug不同，优化编译器一旦知道类型，就可以取消值的装箱，操作原始数字而不是表示数字的JavaScript值，并且只有在绝对必要时才重新装箱。浮点数可以直接在浮点寄存器中传递，而不是分配包含浮点数的堆对象。</p>\n<p>Maglev主要通过查看运行时反馈（例如二元操作）来了解SSA节点的表示，并通过已知节点信息机制向前传播这些信息。当具有特定表示的SSA值流入Phi节点时，需要选择支持所有输入的正确表示。循环Phi再次很棘手，因为循环内的输入在应为Phi选择表示之后看到——与图构建相同的“回到过去”问题。这就是为什么Maglev在图构建之后有一个单独的阶段来对循环Phi进行表示选择。</p>\n<h2>寄存器分配</h2>\n<p>在图构建和表示选择之后，Maglev大致知道它想生成什么样的代码，并且从经典的优化角度来看已经“完成”。然而，为了能够生成代码，我们需要选择SSA值在执行机器代码时的实际位置；它们何时在机器寄存器中，何时在堆栈上保存。这通过寄存器分配来完成。</p>\n<p>每个Maglev节点都有输入和输出要求，包括对临时变量的要求。寄存器分配器对图进行单次前向遍历，维护一个与图构建期间维护的抽象解释状态非常相似的抽象机器寄存器状态，并满足这些要求，用实际位置替换节点的要求。这些位置随后可用于代码生成。</p>\n<p>首先，预处理遍历图以找到节点的线性生命周期范围，以便我们可以在不再需要SSA节点时释放寄存器。此预处理还跟踪使用链。知道一个值在未来需要多长时间可以帮助我们决定优先考虑哪些值，以及在寄存器用完时放弃哪些值。</p>\n<p>在预处理之后，寄存器分配运行。寄存器分配遵循一些简单的局部规则：如果一个值已经在寄存器中，则尽可能使用该寄存器。节点在图遍历过程中跟踪它们存储到的寄存器。如果节点尚未有寄存器，但有一个空闲寄存器，则选择它。节点会更新以指示它在寄存器中，并且抽象寄存器状态会更新以知道它包含节点。如果没有空闲寄存器但需要寄存器，则另一个值会被推出寄存器。理想情况下，我们有一个已经在不同寄存器中的节点，可以“免费”丢弃它；否则，我们选择一个不需要很长时间的值，并将其溢出到堆栈上。</p>\n<p>在分支合并时，来自传入分支的抽象寄存器状态会合并。我们尽量保持尽可能多的值在寄存器中。这可能意味着我们需要引入寄存器到寄存器的移动，或者可能需要从堆栈中取消溢出值，使用称为“间隙移动”的移动。如果分支合并有一个Phi节点，寄存器分配将为Phi分配输出寄存器。Maglev倾向于将Phi的输出分配给与其输入相同的寄存器，以最小化移动。</p>\n<p>如果Maglev帧中的SSA值比寄存器更多，我们将需要将一些值溢出到堆栈上，并在以后取消溢出。遵循Maglev的精神，我们保持简单：如果一个值需要溢出，它会被追溯告知在定义时立即溢出（在值创建之后），代码生成将处理发出溢出代码。定义保证“支配”所有值的使用（到达使用点必须经过定义点，因此经过溢出代码）。这也意味着溢出的值在代码的整个持续时间内将只有一个溢出槽；具有重叠生命周期的值将因此具有不重叠的分配溢出槽。</p>\n<p>由于表示选择，Maglev帧中的某些值将是标记指针，V8的GC理解并需要考虑的指针；而某些值将是未标记的，GC不应查看的值。TurboFan通过精确跟踪哪些堆栈槽包含标记值，哪些包含未标记值来处理这一点，这在执行过程中会发生变化，因为槽被重新用于不同的值。对于Maglev，我们决定保持简单，以减少跟踪所需的内存：我们将堆栈帧分为标记和未标记区域，并且只存储这个分割点。</p>\n<h2>代码生成</h2>\n<p>一旦我们知道要为哪些表达式生成代码，以及要将它们的输出和输入放在哪里，Maglev就准备生成代码了。</p>\n<p>Maglev节点直接知道如何使用“宏汇编器”生成汇编代码。例如，一个<code>CheckMap</code>节点知道如何发出汇编指令，将输入对象的形状（内部称为“映射”）与已知值进行比较，如果对象具有错误的形状，则进行去优化。</p>\n<p>处理间隙移动的代码有些棘手：寄存器分配器创建的请求移动知道一个值在某处需要去另一个地方。然而，如果有一系列这样的移动，前面的移动可能会覆盖后续移动所需的输入。并行移动解析器计算如何安全地执行这些移动，以便所有值都能到达正确的位置。</p>\n<h2>结果</h2>\n<p>那么，我们刚刚介绍的编译器显然比Sparkplug复杂得多，但比TurboFan简单得多。它表现如何？</p>\n<p>在编译速度方面，我们成功地构建了一个比Sparkplug慢约10倍，比TurboFan快约10倍的JIT。</p>\n<p><img src=\"https://v8.dev/_img/maglev/compile-time.svg\" alt=\"\"></p>\n<p>JetStream中所有编译函数的编译时间比较</p>\n<p>这使我们能够比想要部署TurboFan更早地部署Maglev。如果它依赖的反馈最终不太稳定，去优化和重新编译的成本并不高。这也使我们可以稍后使用TurboFan：我们运行的速度远快于使用Sparkplug。</p>\n<p>将Maglev插入Sparkplug和TurboFan之间，带来了显著的基准测试改进：</p>\n<p><img src=\"https://v8.dev/_img/maglev/I-IS-IT-IST-ISTM.svg\" alt=\"\"></p>\n<p>包含Maglev的Web性能基准测试</p>\n<p>我们还在实际数据上验证了Maglev，并在<a href=\"https://web.dev/vitals/\">核心Web指标</a>上看到了良好的改进。</p>\n<p>由于Maglev编译速度更快，并且我们现在可以在编译TurboFan函数之前等待更长时间，这带来了一个不太明显的次要好处。基准测试关注主线程延迟，但Maglev也显著减少了V8的整体资源消耗，使用更少的线程外CPU时间。可以在基于M1或M2的Macbook上使用<code>taskinfo</code>轻松测量进程的能量消耗。</p>\n<table>\n<thead>\n<tr>\n<th>基准测试</th>\n<th>能量消耗</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>JetStream</td>\n<td>-3.5%</td>\n</tr>\n<tr>\n<td>Speedometer</td>\n<td>-10%</td>\n</tr>\n</tbody></table>\n<p>Maglev还远未完成。我们还有很多工作要做，还有更多的想法要尝试，还有更多的低垂果实要采摘——随着Maglev的完善，我们预计会看到更高的分数和更多的能量消耗减少。</p>\n<p>Maglev现在已经可以在桌面版Chrome上使用，并将很快推广到移动设备。</p>\n","tags":["v8"]},{"id":"libuv-overview","url":"https://yieldray.fun/posts/libuv-overview","title":"libuv 概览","date_published":"2024-07-01T14:14:14.000Z","date_modified":"2024-07-01T14:14:14.000Z","content_text":"<p>总结：libuv 是围绕事件驱动的异步 I/O 模型设计的 跨平台 C 库。</p>\n<p>libuv 的主要组件包括：</p>\n<ul>\n<li><strong>I/O 循环（事件循环）</strong>：libuv 的核心部分。为所有 I/O 操作建立上下文（即管理所有 I/O 操作），并且应与单个线程绑定（非线程安全）。</li>\n<li><strong>Handles</strong>：表示能够在活动状态下执行某些操作的长生命周期对象，例如 TCP 服务器 handle。</li>\n<li><strong>Requests</strong>：通常表示短生命周期的操作，例如写请求。</li>\n</ul>\n<blockquote>\n<p>使用说明，另见 <a href=\"https://riife.github.io/libuv-guide/\">UserGuide 翻译</a></p>\n</blockquote>\n<h1>Design overview</h1>\n<p><strong>libuv 是一个跨平台支持库，最初是为 <a href=\"https://nodejs.org/\">Node.js</a> 编写的。它围绕事件驱动的异步 I/O 模型设计。</strong></p>\n<p>该库不仅仅是对不同 I/O 轮询机制的简单抽象：&#39;handles&#39; 和 &#39;streams&#39; 提供了对套接字和其他实体的高级抽象；还提供了跨平台的文件 I/O 和线程功能等。</p>\n<p>下图展示了 libuv 的不同组成部分及其相关的子系统：</p>\n<p><img src=\"https://docs.libuv.org/en/v1.x/_images/architecture.png\" alt=\"_images/architecture.png\"></p>\n<h2>Handles 和 Requests</h2>\n<p>libuv 提供了两种与事件循环配合使用的抽象：handles 和 requests。</p>\n<p>Handles 表示能够在活动状态下执行某些操作的长生命周期对象。例如：</p>\n<ul>\n<li>一个 prepare handle 在每次循环迭代时调用其回调函数。</li>\n<li>一个 TCP 服务器 handle 在每次有新连接时调用其连接回调函数。</li>\n</ul>\n<p>Requests 通常表示短生命周期的操作。这些操作可以在 handle 上执行：写请求用于在 handle 上写入数据；也可以独立执行：getaddrinfo 请求不需要 handle，它们直接在循环上运行。</p>\n<h2>I/O 循环</h2>\n<p>I/O（或事件）循环是 libuv 的核心部分。它为所有 I/O 操作建立了上下文，并且应该与单个线程绑定。可以在不同的线程中运行多个事件循环。libuv 事件循环（或任何涉及循环或 handles 的 API）<strong>不是线程安全的</strong>，除非另有说明。</p>\n<p>事件循环遵循常见的单线程异步 I/O 方法：所有（网络）I/O 都在非阻塞套接字上执行，使用给定平台上最好的机制进行轮询：Linux 上的 epoll，OSX 和其他 BSD 上的 kqueue，SunOS 上的事件端口和 Windows 上的 IOCP。作为循环迭代的一部分，循环将阻塞等待已添加到轮询器的套接字上的 I/O 活动，并触发回调以指示套接字状态（可读、可写、挂起），以便 handles 可以读取、写入或执行所需的 I/O 操作。</p>\n<p>为了更好地理解事件循环的操作，以下图示说明了循环迭代的所有阶段：</p>\n<p><img src=\"https://docs.libuv.org/en/v1.x/_images/loop_iteration.png\" alt=\"_images/loop_iteration.png\"></p>\n<ol>\n<li><p>循环的“现在”概念最初被设置。</p>\n</li>\n<li><p>如果循环是用 <code>UV_RUN_DEFAULT</code> 运行的，则运行到期的定时器。所有计划在循环的<em>现在</em>之前的活动定时器都会调用其回调函数。</p>\n</li>\n<li><p>如果循环是<em>活跃</em>的，则开始一次迭代，否则循环会立即退出。那么，什么时候循环被认为是<em>活跃</em>的呢？如果循环有活动且引用的 handles、活动请求或正在关闭的 handles，则被认为是<em>活跃</em>的。</p>\n</li>\n<li><p>调用挂起的回调。所有 I/O 回调在大多数情况下在轮询 I/O 后立即调用。但是，在某些情况下，调用此类回调会被推迟到下一次循环迭代。如果前一次迭代推迟了任何 I/O 回调，它将在此时运行。</p>\n</li>\n<li><p>调用 idle handle 回调。尽管名称不幸，idle handles 在每次循环迭代时都会运行，如果它们是活动的。</p>\n</li>\n<li><p>调用 prepare handle 回调。Prepare handles 在循环即将阻塞 I/O 之前调用其回调函数。</p>\n</li>\n<li><p>计算轮询超时。在阻塞 I/O 之前，循环会计算应阻塞多长时间。计算超时的规则如下：</p>\n<blockquote>\n<ul>\n<li>如果循环是用 <code>UV_RUN_NOWAIT</code> 标志运行的，超时为 0。</li>\n<li>如果循环即将停止（调用了 <a href=\"https://docs.libuv.org/en/v1.x/loop.html#c.uv_stop\" title=\"uv_stop\"><code>uv_stop()</code></a>），超时为 0。</li>\n<li>如果没有活动的 handles 或请求，超时为 0。</li>\n<li>如果有任何活动的 idle handles，超时为 0。</li>\n<li>如果有任何待关闭的 handles，超时为 0。</li>\n<li>如果以上情况都不符合，则取最近的定时器的超时，如果没有活动的定时器，则为无限大。</li>\n</ul>\n</blockquote>\n</li>\n<li><p>循环阻塞 I/O。此时，循环将阻塞 I/O，持续时间为上一步计算的时间。所有监视给定文件描述符的读或写操作的 I/O 相关 handles 在此时调用其回调函数。</p>\n</li>\n<li><p>调用 check handle 回调。Check handles 在循环阻塞 I/O 后立即调用其回调函数。Check handles 本质上是 prepare handles 的对应物。</p>\n</li>\n<li><p>调用关闭回调。如果通过调用 <a href=\"https://docs.libuv.org/en/v1.x/handle.html#c.uv_close\" title=\"uv_close\"><code>uv_close()</code></a> 关闭了一个 handle，它将调用关闭回调。</p>\n</li>\n<li><p>更新循环的“现在”概念。</p>\n</li>\n<li><p>运行到期的定时器。请注意，“现在”不会在下一个循环迭代之前再次更新。因此，如果在处理其他定时器时有一个定时器到期，它将在下一个事件循环迭代之前不会运行。</p>\n</li>\n<li><p>迭代结束。如果循环是用 <code>UV_RUN_NOWAIT</code> 或 <code>UV_RUN_ONCE</code> 模式运行的，迭代结束并且 <a href=\"https://docs.libuv.org/en/v1.x/loop.html#c.uv_run\" title=\"uv_run\"><code>uv_run()</code></a> 将返回。如果循环是用 <code>UV_RUN_DEFAULT</code> 运行的，并且仍然<em>活跃</em>，则它将从头开始继续，否则它也将结束。</p>\n</li>\n</ol>\n<p><strong>重要提示</strong></p>\n<p>libuv 使用线程池使异步文件 I/O 操作成为可能，但网络 I/O <strong>始终</strong>在单个线程中执行，即每个循环的线程。</p>\n<p><strong>注意</strong></p>\n<p>尽管轮询机制不同，libuv 使得在 Unix 系统和 Windows 上的执行模型保持一致。</p>\n<h2>文件 I/O</h2>\n<p>与网络 I/O 不同，libuv 没有可以依赖的平台特定文件 I/O 原语，因此当前的方法是在线程池中运行阻塞的文件 I/O 操作。</p>\n<p>有关跨平台文件 I/O 详细解释，请查看<a href=\"https://blog.libtorrent.org/2012/10/asynchronous-disk-io/\">这篇文章</a>。</p>\n<p>libuv 目前使用全局线程池，所有循环都可以在此池中排队工作。目前在此池中运行 3 种类型的操作：</p>\n<blockquote>\n<ul>\n<li>文件系统操作</li>\n<li>DNS 函数（getaddrinfo 和 getnameinfo）</li>\n<li>用户指定的代码通过 <a href=\"https://docs.libuv.org/en/v1.x/threadpool.html#c.uv_queue_work\" title=\"uv_queue_work\"><code>uv_queue_work()</code></a> 运行</li>\n</ul>\n</blockquote>\n<blockquote>\n<p><a href=\"https://docs.libuv.org/en/v1.x/fs.html#file-system-operations\">补充说明</a>：<br>上面提到的文章主要说明为何文件操作不使用操作系统异步IO（如 AIO）而是使用线程池。不过新版本 libuv 也开始使用 io_uring 了。<br>从 libuv v1.45.0 开始，Linux上的一些文件操作在可能的情况下将转移到 <a href=\"https://en.wikipedia.org/wiki/Io_uring\">io_uring</a>。除了吞吐量有时会显著增加外，观察到的行为没有变化。当必要的内核特性不可用或不合适时，Libuv 会回退到使用其线程池。</p>\n</blockquote>\n","tags":["c","lib","node.js"]},{"id":"v8-trash-talk","url":"https://yieldray.fun/posts/v8-trash-talk","title":"v8 垃圾回收：Orinoco 垃圾回收器","date_published":"2024-06-30T14:14:14.000Z","date_modified":"2024-06-30T14:14:14.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://v8.dev/blog/trash-talk\">https://v8.dev/blog/trash-talk</a><br>延申阅读：<a href=\"https://www.jayconrod.com/posts/55/a-tour-of-v8--garbage-collection\">https://www.jayconrod.com/posts/55/a-tour-of-v8--garbage-collection</a></p>\n</blockquote>\n<p>过去几年里，V8垃圾回收器（GC）发生了很大变化。Orinoco 项目将一个顺序的、全停顿的垃圾回收器转变为一个主要并行和并发的回收器，并具有增量回退功能。</p>\n<p>任何垃圾回收器都有一些必须定期执行的基本任务：</p>\n<ol>\n<li>识别存活/死亡对象</li>\n<li>回收/重用死亡对象占用的内存</li>\n<li>压缩/整理内存（可选）</li>\n</ol>\n<p>这些任务可以按顺序执行，也可以任意交错。一个简单的方法是暂停JavaScript执行，并在主线程上按顺序执行每个任务。这可能会导致主线程上的卡顿和延迟问题，我们在<a href=\"https://v8.dev/blog/jank-busters\">之前</a>的<a href=\"https://v8.dev/blog/orinoco\">博客文章</a>中谈到过，还会减少程序的吞吐量。</p>\n<h2>主要GC（全标记-压缩）</h2>\n<p>主要GC从整个堆中收集垃圾。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/01.svg\" alt=\"\"></p>\n<p>主要GC分为三个阶段：标记、清扫和压缩。</p>\n<h3>标记</h3>\n<p>确定哪些对象可以被回收是垃圾回收的一个重要部分。垃圾回收器通过使用可达性作为“存活性”的代理来实现这一点。这意味着任何当前在运行时可达的对象都必须保留，而任何不可达的对象都可以被回收。</p>\n<p>标记是找到可达对象的过程。GC从一组已知的对象指针（称为根集）开始。这包括执行堆栈和全局对象。然后它跟踪每个指向JavaScript对象的指针，并将该对象标记为可达。GC跟踪该对象中的每个指针，并递归地继续这一过程，直到找到并标记所有在运行时可达的对象。</p>\n<h3>清扫</h3>\n<p>清扫是将死亡对象留下的内存空隙添加到一个称为空闲列表的数据结构中的过程。标记完成后，GC找到不可达对象留下的连续空隙，并将它们添加到适当的空闲列表中。空闲列表按内存块的大小分开，以便快速查找。将来当我们想要分配内存时，只需查看空闲列表并找到一个合适大小的内存块。</p>\n<h3>压缩</h3>\n<p>主要GC还根据碎片化启发式选择一些页面进行清空/压缩。你可以将压缩类比为旧PC上的硬盘碎片整理。我们将存活的对象复制到未被压缩的其他页面（使用该页面的空闲列表）。这样，我们可以利用死亡对象留下的内存中的小而分散的空隙。</p>\n<p>复制存活对象的垃圾回收器的一个潜在弱点是，当我们分配大量长寿命对象时，我们需要付出高昂的复制这些对象的成本。这就是为什么我们选择只压缩一些高度碎片化的页面，而对其他页面只进行清扫，不复制存活对象。</p>\n<h2>分代布局</h2>\n<p>V8中的堆被分为不同的区域，称为<a href=\"https://v8.dev/blog/orinoco-parallel-scavenger\">代</a>。有一个年轻代（进一步分为“新生代”和“中生代”），还有一个老年代。对象首先分配到新生代。如果它们在下一次GC中存活下来，它们仍然留在年轻代，但被视为“中生代”。如果它们再一次GC中存活下来，它们会被移动到老年代。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/02.svg\" alt=\"\"></p>\n<p>V8堆被分为代。对象在GC存活后会在代之间移动。</p>\n<p>在垃圾回收中有一个重要的术语：“分代假说”。这基本上是指大多数对象很快就会死亡。换句话说，从GC的角度来看，大多数对象在分配后几乎立即变得不可达。这不仅适用于V8或JavaScript，还适用于大多数动态语言。</p>\n<p>V8的分代堆布局旨在利用这一对象寿命的事实。GC是一个压缩/移动GC，这意味着它会复制在垃圾回收中存活的对象。这看起来是反直觉的：在GC时复制对象是昂贵的。但我们知道，根据分代假说，只有很小一部分对象实际上会在垃圾回收中存活。通过只移动存活的对象，其他所有分配都变成了“隐式”垃圾。这意味着我们只需要支付与存活对象数量成比例的成本（用于复制），而不是分配的数量。</p>\n<h2>次要GC（清扫器）</h2>\n<p>V8中有两个垃圾回收器。 <a href=\"https://v8.dev/blog/trash-talk#major-gc\"><strong>主要GC（标记-压缩）</strong></a>从整个堆中收集垃圾。**次要GC（清扫器）**在年轻代中收集垃圾。主要GC在从整个堆中收集垃圾方面非常有效，但分代假说告诉我们，新分配的对象非常可能需要垃圾回收。</p>\n<p>在清扫器中，它只在年轻代中收集垃圾，存活的对象总是被转移到一个新页面。V8对年轻代使用“半空间”设计。这意味着总有一半的空间是空的，以便进行这个转移步骤。在清扫期间，这个最初为空的区域称为“目标空间”。我们从中复制的区域称为“源空间”。在最坏的情况下，每个对象都可能在清扫中存活，我们需要复制每个对象。</p>\n<p>对于清扫，我们有一组额外的根，这些根是从老代到新代的引用。这些是老空间中指向年轻代对象的指针。与其为每次清扫跟踪整个堆图，我们使用<a href=\"https://www.memorymanagement.org/glossary/w.html#term-write-barrier\">写屏障</a>来维护一个从老到新的引用列表。结合堆栈和全局对象，我们知道每个指向年轻代的引用，而不需要跟踪整个老代。</p>\n<p>转移步骤将所有存活的对象移动到一个连续的内存块（在一个页面内）。这具有完全消除碎片化的优点，即死亡对象留下的空隙。然后我们交换两个空间，即目标空间变为源空间，反之亦然。一旦GC完成，新分配就会发生在源空间中的下一个空闲地址。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/03.svg\" alt=\"\"></p>\n<p>清扫器将存活的对象转移到一个新的页面。</p>\n<p>仅靠这种策略，我们很快就会在年轻代中耗尽空间。第二次GC中存活的对象被转移到老年代，而不是目标空间。</p>\n<p>清扫的最后一步是更新引用原始对象的指针，这些对象已被移动。每个复制的对象留下一个转发地址，用于更新原始指针以指向新位置。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/04.svg\" alt=\"\"></p>\n<p>清扫器将“中生代”对象转移到老年代，将“新生代”对象转移到一个新的页面。</p>\n<p>在清扫中，我们实际上将这三个步骤——标记、转移和指针更新——交错进行，而不是分阶段进行。</p>\n<p>这些算法和优化大多在垃圾回收文献中很常见，并且可以在许多垃圾回收语言中找到。但最先进的垃圾回收已经走了很长的路。衡量垃圾回收时间的一个重要指标是主线程在执行GC时暂停的时间。对于传统的“全停顿”垃圾回收器，这段时间可能会累积，这段时间直接影响用户体验，表现为页面卡顿和渲染和延迟不佳。</p>\n<p><img src=\"https://v8.dev/_img/v8-orinoco.svg\" alt=\"\"></p>\n<p>Orinoco是V8垃圾回收器项目的代号，旨在利用最新和最先进的并行、增量和并发技术进行垃圾回收，以释放主线程。有些术语在GC上下文中有特定的含义，值得详细定义。</p>\n<h3>并行</h3>\n<p>并行是指主线程和辅助线程在同一时间做大致相同的工作。这仍然是一种“全停顿”的方法，但总暂停时间现在被参与线程的数量（加上一些同步开销）除以。这是三种技术中最简单的一种。JavaScript堆暂停，因为没有JavaScript在运行，所以每个辅助线程只需要确保它同步访问任何其他辅助线程可能也想访问的对象。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/05.svg\" alt=\"\"></p>\n<p>主线程和辅助线程同时处理相同的任务。</p>\n<h3>增量</h3>\n<p>增量是指主线程间歇性地做少量工作。我们不会在增量暂停中完成整个GC，只是完成GC所需总工作量的一小部分。这更困难，因为在每个增量工作段之间JavaScript会执行，这意味着堆的状态发生了变化，可能会使之前的增量工作无效。从图中可以看出，这并没有减少主线程上花费的时间（实际上通常会略微增加），只是将其分散在一段时间内。这仍然是解决我们最初问题的一个好技术：主线程延迟。通过允许JavaScript间歇性运行，同时继续进行垃圾回收任务，应用程序仍然可以响应用户输入并在动画上取得进展。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/06.svg\" alt=\"\"></p>\n<p>GC任务的小块与主线程执行交错进行。</p>\n<h3>并发</h3>\n<p>并发是指主线程不断执行JavaScript，而辅助线程完全在后台进行GC工作。这是三种技术中最困难的一种：JavaScript堆上的任何东西都可能随时改变，使我们之前所做的工作无效。除此之外，还有读取/写入竞争的问题，因为辅助线程和主线程同时读取或修改相同的对象。这里的优点是主线程完全可以自由地执行JavaScript——尽管由于与辅助线程的一些同步，存在一些小的开销。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/07.svg\" alt=\"\"></p>\n<p>GC任务完全在后台进行。主线程可以自由运行JavaScript。</p>\n<h2>V8中的GC状态</h2>\n<h3>清扫</h3>\n<p>今天，V8使用并行清扫将年轻代GC的工作分配到辅助线程中。每个线程接收若干指针，跟踪这些指针，积极地将任何存活的对象转移到目标空间。清扫任务在尝试转移对象时必须通过原子读取/写入/比较并交换操作进行同步；另一个清扫任务可能通过不同路径找到相同的对象并也尝试移动它。成功移动对象的辅助线程然后返回并更新指针。它留下一个转发指针，以便其他工作线程找到对象时可以更新其他指针。为了快速无同步地分配存活对象，清扫任务使用线程本地分配缓冲区。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/08.svg\" alt=\"\"></p>\n<p>并行清扫将清扫工作分配到多个辅助线程和主线程中。</p>\n<h3>主要GC</h3>\n<p>V8中的主要GC从并发标记开始。当堆接近动态计算的限制时，启动并发标记任务。每个辅助线程接收若干指针，跟踪这些指针，并在跟踪从发现的对象的所有引用时标记每个找到的对象。并发标记完全在后台进行，而JavaScript在主线程上执行。<a href=\"https://dl.acm.org/citation.cfm?id=2025255\">写屏障</a>用于跟踪JavaScript在辅助线程并发标记时创建的对象之间的新引用。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/09.svg\" alt=\"\"></p>\n<p>主要GC使用并发标记和清扫，并行压缩和指针更新。</p>\n<p>当并发标记完成或达到动态分配限制时，主线程执行快速标记完成步骤。主线程暂停在此阶段开始。这代表了主要GC的总暂停时间。主线程再次扫描根，以确保所有存活对象都被标记，然后与若干辅助线程一起，开始并行压缩和指针更新。老空间中的所有页面都不适合压缩——那些不适合的页面将使用前面提到的空闲列表进行清扫。主线程在暂停期间启动并发清扫任务。这些任务与并行压缩任务和主线程本身并发运行——即使JavaScript在主线程上运行，它们也可以继续。</p>\n<h2>空闲时间GC</h2>\n<p>JavaScript用户无法直接访问垃圾回收器；它完全是实现定义的。然而，V8确实提供了一种机制，允许嵌入器触发垃圾回收，即使JavaScript程序本身不能。GC可以发布“空闲任务”，这些任务是最终会被触发的可选工作。像Chrome这样的嵌入器可能对空闲时间有一些概念。例如，在Chrome中，以60帧每秒的速度，浏览器大约有16.6毫秒来渲染动画的每一帧。如果动画工作提前完成，Chrome可以选择在下一帧之前的空闲时间运行一些GC创建的空闲任务。</p>\n<p><img src=\"https://v8.dev/_img/trash-talk/10.svg\" alt=\"\"></p>\n<p>空闲GC利用主线程的空闲时间主动执行GC工作。</p>\n<p>欲了解更多详细信息，请参阅<a href=\"https://queue.acm.org/detail.cfm?id=2977741\">我们关于空闲时间GC的深入出版物</a>。</p>\n<h2>总结</h2>\n<p>V8中的垃圾回收器自其诞生以来已经走了很长的路。将并行、增量和并发技术添加到现有GC中是一个多年的努力，但它已经取得了回报，将大量工作移到了后台任务中。它大大改善了暂停时间、延迟和页面加载，使动画、滚动和用户交互更加流畅。<a href=\"https://v8.dev/blog/orinoco-parallel-scavenger\">并行清扫器</a>减少了主线程年轻代垃圾回收总时间约20%–50%，具体取决于工作负载。<a href=\"https://v8.dev/blog/free-garbage-collection\">空闲时间GC</a>可以在Gmail空闲时减少45%的JavaScript堆内存。<a href=\"https://v8.dev/blog/jank-busters\">并发标记和清扫</a>在重度WebGL游戏中将暂停时间减少了多达50%。</p>\n<p>但工作还没有结束。减少垃圾回收暂停时间对于为用户提供最佳的网络体验仍然很重要，我们正在研究更先进的技术。除此之外，Chrome中的渲染器Blink也有一个垃圾回收器（称为Oilpan），我们正在努力改进两个回收器之间的<a href=\"https://dl.acm.org/citation.cfm?doid=3288538.3276521\">协作</a>，并将Orinoco的一些新技术移植到Oilpan中。</p>\n<p>大多数开发者在开发JavaScript程序时不需要考虑GC，但了解一些内部机制可以帮助你思考内存使用和有益的编程模式。例如，V8堆的分代结构中，从垃圾回收器的角度来看，短命对象实际上非常便宜，因为我们只为存活的对象付费。这种模式在许多垃圾回收语言中都很有效，不仅仅是JavaScript。</p>\n","tags":["v8"]},{"id":"linux-ipc","url":"https://yieldray.fun/posts/linux-ipc","title":"Linux IPC","date_published":"2024-06-18T17:00:00.000Z","date_modified":"2024-10-20T17:00:00.000Z","content_text":"<h1>仅使用 C 标准库，无 POSIX 扩展 API 情况</h1>\n<p>C 标准库提供的程序支持工具，与 IPC 相关的主要有以下：（只讨论平台无关的手段）</p>\n<p><a href=\"https://zh.cppreference.com/w/c/program/system\"><code>int system( const char *command );</code></a> 用于向宿主 Shell 发送命令 （注意 popen 等函数是 POSIX 函数，所有这里无法访问子进程 I/O）<br><a href=\"https://zh.cppreference.com/w/c/program/getenv\"><code>char *getenv( const char *name );</code></a> 用于读取环境变量 （注意 setenv 等函数是 POSIX 函数，所以这里无法传递环境变量）</p>\n<p>另外，信号是无需考虑的，因为唯一的信号相关函数 <a href=\"https://zh.cppreference.com/w/c/program/raise\">raise</a> 只能向进程自身发送信号。<br>不过，仅使用标准库是可以实现创建文件（<a href=\"https://zh.cppreference.com/w/c/io/fopen\">fopen</a>）、读写文件、删除文件（<a href=\"https://zh.cppreference.com/w/c/io/remove\">remove</a>）的。（理论上可以完成 IPC，不过没有同步机制）</p>\n<hr>\n<p>因此，仅有的 IPC 方式有：</p>\n<ul>\n<li>程序参数（父 -&gt; 子，单向）</li>\n<li>文件 I/O（双向，但必须事先确定路径）</li>\n</ul>\n<p>并且无法实现同步。</p>\n<h1>Unix/Linux IPC</h1>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-alert mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<p>这里仅以 Linux 为例，不讨论实现的内核版本、不讨论达到系统限制（如超出某些缓冲区）的情况。<br>细节方面，例如 C 标准库 I/O 缓冲，不在讨论范围之内。<br>也不关心：某个系统调用是否是基于其它系统调用，因为它们可能是平台相关的。<br>本文不是一个 Linux 系统调用的教程。</p>\n</div>\n<p>首先在 Linux 上，上面提到的 C 标准库扩展 POSIX API 当然是可用的。<br>不过注意 <a href=\"https://www.man7.org/linux/man-pages/man3/popen.3.html\"><code>FILE *popen(const char *command, const char *type);</code></a> 的 type 参数只能指定只读或只写，因此无法实现双向通信。</p>\n<p>下面我们就不再局限于 C 标准库（及其扩展），放眼 Linux 原语。</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-light-bulb mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>\n<p>文件描述符可通过 fork 传递给子进程；在 exec 之后，文件描述符是否保留取决于是否设置了 Close-on-exec。<br>因此这里可以实现父子进程读写同一文件。另外 pipe/socketpair 等通信方式就是基于此机制的，此下文再讨论。<br>理论上来说，甚至可以通过监控文件事件（<a href=\"https://www.man7.org/linux/man-pages/man7/inotify.7.html\">inotify</a>，Linux API）来实现通用 IPC，不过这显然没有什么实用性。</p>\n</div>\n<h2>概览</h2>\n<p>TLPI 将 IPC 分为以下 3 类（但是本文不考虑同步）：<br><img src=\"https://s2.loli.net/2024/06/18/lLGKo7csJhrnzmA.png\" alt=\"ipc.png\"></p>\n<table>\n<thead>\n<tr>\n<th>工具类型</th>\n<th>用于识别对象的名称</th>\n<th>用于在程序中引用对象的句柄</th>\n<th>数据传输模型</th>\n<th>传输形式</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>管道</td>\n<td>没有名称</td>\n<td>文件描述符</td>\n<td>读写流控</td>\n<td>字节流</td>\n</tr>\n<tr>\n<td>FIFO</td>\n<td>路径名</td>\n<td>文件描述符</td>\n<td>读写流控</td>\n<td>字节流</td>\n</tr>\n<tr>\n<td>UNIX domain socket</td>\n<td>路径名</td>\n<td>文件描述符</td>\n<td>读写流控</td>\n<td>字节流/消息</td>\n</tr>\n<tr>\n<td>Internet domain socket</td>\n<td>IP 地址+端口号</td>\n<td>文件描述符</td>\n<td>读写流控</td>\n<td>字节流/消息</td>\n</tr>\n<tr>\n<td>System V 消息队列</td>\n<td>System V IPC 键</td>\n<td>System V IPC 标识符</td>\n<td>读写流控</td>\n<td>消息</td>\n</tr>\n<tr>\n<td>System V 信号量</td>\n<td>System V IPC 键</td>\n<td>System V IPC 标识符</td>\n<td>同步</td>\n<td>无</td>\n</tr>\n<tr>\n<td>System V 共享内存</td>\n<td>System V IPC 键</td>\n<td>System V IPC 标识符</td>\n<td>共享内存</td>\n<td>无</td>\n</tr>\n<tr>\n<td>POSIX 消息队列</td>\n<td>POSIX IPC 路径名</td>\n<td><code>mqd_t</code> (消息队列描述符)</td>\n<td>读写流控</td>\n<td>消息</td>\n</tr>\n<tr>\n<td>POSIX 命名信号量</td>\n<td>POSIX IPC 路径名</td>\n<td><code>sem_t *</code> (信号量指针)</td>\n<td>同步</td>\n<td>无</td>\n</tr>\n<tr>\n<td>POSIX 无名信号量</td>\n<td>没有名称</td>\n<td><code>sem_t *</code> (信号量指针)</td>\n<td>同步</td>\n<td>无</td>\n</tr>\n<tr>\n<td>POSIX 共享内存</td>\n<td>POSIX IPC 路径名</td>\n<td>文件描述符</td>\n<td>共享内存</td>\n<td>无</td>\n</tr>\n<tr>\n<td>匿名映射</td>\n<td>无</td>\n<td>文件描述符</td>\n<td>共享内存</td>\n<td>无</td>\n</tr>\n<tr>\n<td>内存映射文件</td>\n<td>路径名</td>\n<td>文件描述符</td>\n<td>共享内存</td>\n<td>无</td>\n</tr>\n<tr>\n<td><code>flock()</code> 文件锁</td>\n<td>路径名</td>\n<td>文件描述符</td>\n<td>同步</td>\n<td>无</td>\n</tr>\n<tr>\n<td><code>fcntl()</code> 文件锁</td>\n<td>路径名</td>\n<td>文件描述符</td>\n<td>同步</td>\n<td>无</td>\n</tr>\n<tr>\n<td>终端/伪终端</td>\n<td>路径名</td>\n<td>文件描述符</td>\n<td>读写流控</td>\n<td>字节流</td>\n</tr>\n</tbody></table>\n<h2>信号</h2>\n<p><a href=\"https://www.man7.org/linux/man-pages/man7/signal.7.html\">信号</a>分为两种：</p>\n<ul>\n<li>（POSIX reliable signals）标准信号不会对遭阻塞的信号进行排队处理，如果在信号处理器程序执行过程中重复产生这些信号中的任何信号，（稍后）对信号的传递将是一次性的。</li>\n<li>（POSIX real-time signals）实时信号则会排队处理（通过 <a href=\"https://www.man7.org/linux/man-pages/man3/sigqueue.3.html\">sigqueue(3)</a>），并多次传递。</li>\n</ul>\n<blockquote>\n<p>前提：<br>对于普通进程来说，发送信号（kill）的进程的 RUID 或 EUID 需要匹配于接受者的 RUID 或 SetUID。</p>\n</blockquote>\n<blockquote>\n<p>从实际角度看：<br>标准信号中可供应用随意使用的信号仅有两个：SIGUSR1 和 SIGUSR2；实时信号的信号范围有所扩大，可应用于应用程序自定义的目的。<br>此外，实时信号可为信号指定伴随数据（一整型数或者指针值），并且对于等待中的信号有优先级机制（编号越小优先级越高）。</p>\n</blockquote>\n<p>由于信号能够打断程序的正常运行，因此信号处理器中调用的函数（显然也包括处理器函数本身）需要是<a href=\"https://www.man7.org/linux/man-pages/man7/signal-safety.7.html\">异步信号安全的（AS-Safe）</a>。<br>即满足：函数是<strong>可重入的</strong>，或者信号处理器函数<strong>无法将该函数中断</strong>（如果该函数不会导致信号发生，那么当然就不可能会“重入”）。<br>从实践的角度看，基本不可能禁止主程序去调用不可中断函数，因此为了保证信号处理函数是异步信号安全的，<strong>必须保证信号处理函数中不调用不安全的函数</strong>。简单来说，最好保持信号处理程序尽可能简单。</p>\n<blockquote>\n<p>考虑实践性：<br>信号在 Unix 中会被认为是一种比较原始和存在缺陷的 IPC 和进程控制机制。<br>我们需要考虑：</p>\n<ul>\n<li>异步性（信号可能在任何时候中断进程的正常执行流程）与竞争条件（信号处理函数与主程序逻辑上是并发的）</li>\n<li>信号处理程序的限制和信号丢失的可能（正如上面讨论过的）</li>\n<li>难以调试，因为它是异步的</li>\n</ul>\n</blockquote>\n<p>总的来说：信号是任意向的（具有适当的进程操纵凭证）、只能携带很少数据（信号编号 + 实时信号额外的 <code>union sigval</code>）。<br>因此只适合传递简单的信息（消息），例如通知某事件已发生。</p>\n<h2>管道</h2>\n<h4>匿名管道（<a href=\"https://www.man7.org/linux/man-pages/man2/pipe.2.html\">pipe(2)</a>）</h4>\n<p>匿名管道（简称：管道）实际上是很常见的，Shell 管道（<code>|</code>）和 <code>popen()</code> 都是基于它实现的。</p>\n<p>管道具有以下性质：</p>\n<ul>\n<li>管道是连续、顺序且无法定位的字节流（无边界）</li>\n<li>每个管道是单向的，若需要双向管道机制，可使用 <a href=\"https://www.man7.org/linux/man-pages/man2/socketpair.2.html\">socketpair(2)</a> 替代</li>\n<li>可确保写入不超过 PIPE_BUF 字节的操作是原子的</li>\n<li>管道实际上是在内核内存中维护的缓冲器，因而容量是有限的，如果填满了就会阻塞</li>\n</ul>\n<h4>命名管道（<a href=\"https://www.man7.org/linux/man-pages/man7/fifo.7.html\">fifo(7)</a>）</h4>\n<p>命名管道（简称：FIFO）的“命名”指在文件系统有一个标识（即路径）。</p>\n<p>通过 <a href=\"https://www.man7.org/linux/man-pages/man3/mkfifo.3.html\">mkfifo(3)</a> 在文件系统中创建一个命名管道，它可以被拥有合适的权限的任意进程打开。<br>除此之外，其运作方式与管道完全一样。<br>在默认情况下，为读取数据（O_RDONLY）而打开一个 FIFO 会被阻塞直到另一个进程为写入数据（O_WRONLY）而打开了该 FIFO，反之亦然。</p>\n<h2>内存映射 <a href=\"https://www.man7.org/linux/man-pages/man2/mmap.2.html\">mmap(2)</a></h2>\n<p>要通过 <code>void *mmap(void addr[.length], size_t length, int prot, int flags, int fd, off_t offset);</code> 来 IPC，那必然要使用共享映射（MAP_SHARED）。<br>对于匿名映射（MAP_ANONYMOUS），因为不存在对应的文件，要求两个进程是父子关系：<code>fork()</code> 的子进程会继承映射，因此父子进程就共享了同样的内存物理分页（不过执行 <code>exec()</code> 时映射会丢失）。</p>\n<p>共享映射时，任意两个进程间打开（open）同一个文件即可。简单来说，两个进程<em>映射区域的页表项</em>指向物理内存的<em>同一个映射页面</em>，而内核负责管理物理内存映射页面于被映射文件区域的 I/O。<br>显然，共享内存的优势之一就是内核<strong>无需在内核态内存中维护额外的缓冲区</strong>，用于与<em>用户态内存</em>之间交换数据。</p>\n<h2>套接字 <a href=\"https://www.man7.org/linux/man-pages/man7/socket.7.html\">socket(7)</a></h2>\n<p>套接字可以说是唯一跨（网络间的）主机的 ICP 方式（Internet Domain Socket），当然用于本机进程间也是可以的（<a href=\"https://www.man7.org/linux/man-pages/man7/unix.7.html\">Unix Domain Socket</a>）。</p>\n<p><a href=\"https://www.man7.org/linux/man-pages/man2/socket.2.html\"><code>int socket(int domain, int type, int protocol);</code></a></p>\n<table>\n<thead>\n<tr>\n<th>域（domain）</th>\n<th>执行的通信</th>\n<th>应用程序间的通信</th>\n<th>地址格式</th>\n<th>地址结构</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>AF_UNIX</td>\n<td>内核中</td>\n<td>同一主机</td>\n<td>路径名</td>\n<td>sockaddr_un</td>\n</tr>\n<tr>\n<td>AF_INET</td>\n<td>通过 IPv4</td>\n<td>通过 IPv4 网络连接起来的主机</td>\n<td>32 位 IPv4 地址 + 16 位端口号</td>\n<td>sockaddr_in</td>\n</tr>\n<tr>\n<td>AF_INET6</td>\n<td>通过 IPv6</td>\n<td>通过 IPv6 网络连接起来的主机</td>\n<td>128 位 IPv6 地址 + 16 位端口号</td>\n<td>sockaddr_in6</td>\n</tr>\n</tbody></table>\n<table>\n<thead>\n<tr>\n<th>类型（type）</th>\n<th>描述</th>\n<th>默认协议</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>SOCK_STREAM</td>\n<td>有序的、可靠的、双向的、面向连接的字节流</td>\n<td>TCP (Transmission Control Protocol)</td>\n</tr>\n<tr>\n<td>SOCK_DGRAM</td>\n<td>固定长度的、无连接的、不可靠的报文传递</td>\n<td>UDP (User Datagram Protocol)</td>\n</tr>\n<tr>\n<td>SOCK_RAW</td>\n<td>IP协议的数据报接口（在POSIX.1中为可选）</td>\n<td>IP (Internet Protocol)</td>\n</tr>\n<tr>\n<td>SOCK_SEQPACKET</td>\n<td>固定长度的、有序的、可靠的、面向连接的报文传递</td>\n<td>SCTP (Stream Control Transmission Protocol)</td>\n</tr>\n</tbody></table>\n<table>\n<thead>\n<tr>\n<th>协议</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>0</td>\n<td>为给定的域和套接字类型选择默认协议</td>\n</tr>\n<tr>\n<td>IPPROTO_IP</td>\n<td>IPv4 网际协议</td>\n</tr>\n<tr>\n<td>IPPROTO_IPV6</td>\n<td>IPv6 网际协议（在 POSX.1 中为可选）</td>\n</tr>\n<tr>\n<td>IPPROTO_ICMP</td>\n<td>因特网控制报文协议（Internet Control Message Protocol）</td>\n</tr>\n<tr>\n<td>IPPROTO_RAW</td>\n<td>原始 IP 数据包协议（在 POSX.1 中为可选）</td>\n</tr>\n<tr>\n<td>IPPROTO_TCP</td>\n<td>传输控制协议</td>\n</tr>\n<tr>\n<td>IPPROTO_UDP</td>\n<td>用户数据报协议（User Datagram Protocol）</td>\n</tr>\n</tbody></table>\n<p>详细参见 man 手册。另外，也可查看<a href=\"https://man.archlinux.org/man/extra/man-pages-zh_cn/socket.7.zh_CN\">中文版 man 手册的 socket(7) 页面</a>。</p>\n<h2>System V IPC（XSI IPC）</h2>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>仅概述</p>\n</div>\n<p>SystemV IPC 使用<strong>不同于文件描述符</strong>的机制，内核内部维护一组 SystemV IPC 对象。</p>\n<table>\n<thead>\n<tr>\n<th>特征</th>\n<th>消息队列</th>\n<th>信号量</th>\n<th>共享内存</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>头文件</td>\n<td>&lt;sys/msg.h&gt;</td>\n<td>&lt;sys/sem.h&gt;</td>\n<td>&lt;sys/shm.h&gt;</td>\n</tr>\n<tr>\n<td>关联数据结构</td>\n<td><code>msqid_ds</code></td>\n<td><code>semid_ds</code></td>\n<td><code>shmid_ds</code></td>\n</tr>\n<tr>\n<td>创建/打开对象</td>\n<td>msgget()</td>\n<td>semget()</td>\n<td>shmget(), shmat()</td>\n</tr>\n<tr>\n<td>关闭对象</td>\n<td>（无）</td>\n<td>（无）</td>\n<td>shmdt()</td>\n</tr>\n<tr>\n<td>控制操作</td>\n<td>msgctl()</td>\n<td>semctl()</td>\n<td>shmctl()</td>\n</tr>\n<tr>\n<td>执行IPC</td>\n<td>msgsnd(), msgrcv()</td>\n<td>semop()</td>\n<td>（无）</td>\n</tr>\n</tbody></table>\n<h2>POSIX IPC</h2>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>仅概述</p>\n</div>\n<p>相比于System V IPC，POSIX IPC与 Linux 系统 I/O 模型更一致。</p>\n<table>\n<thead>\n<tr>\n<th>特征</th>\n<th>消息队列</th>\n<th>信号量</th>\n<th>共享内存</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>头文件</td>\n<td>&lt;mqueue.h&gt;</td>\n<td>&lt;semaphore.h&gt;</td>\n<td>&lt;sys/mman.h&gt;</td>\n</tr>\n<tr>\n<td>对象句柄</td>\n<td><code>mqd_t</code></td>\n<td><code>sem_t *</code></td>\n<td><code>int</code>（文件描述符）</td>\n</tr>\n<tr>\n<td>创建/打开</td>\n<td>mq_open()</td>\n<td>sem_open()</td>\n<td>shm_open() + mmap()</td>\n</tr>\n<tr>\n<td>关闭</td>\n<td>mq_close()</td>\n<td>sem_close()</td>\n<td>munmap()</td>\n</tr>\n<tr>\n<td>断开链接</td>\n<td>mq_unlink()</td>\n<td>sem_unlink()</td>\n<td>shm_unlink()</td>\n</tr>\n<tr>\n<td>执行IPC</td>\n<td>mq_send(), mq_receive()</td>\n<td>sem_post(), sem_wait()</td>\n<td>在共享区域中的位置上操作</td>\n</tr>\n<tr>\n<td>其他操作</td>\n<td>mq_setattr() 设置特性</td>\n<td>sem_getvalue()</td>\n<td>-</td>\n</tr>\n<tr>\n<td></td>\n<td>mq_getattr() 获取特性</td>\n<td>sem_init() 初始化未命名信号量</td>\n<td>-</td>\n</tr>\n<tr>\n<td></td>\n<td>mq_notify() 请求通知</td>\n<td>sem_destroy() 销毁未命名信号量</td>\n<td>-</td>\n</tr>\n</tbody></table>\n<p>每个 socket 实现都至少提供了两种 socket 类型</p>\n<table>\n<thead>\n<tr>\n<th>特征</th>\n<th>流</th>\n<th>数据报</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>可靠地传递</td>\n<td>✔</td>\n<td>✘</td>\n</tr>\n<tr>\n<td>消息边界保留</td>\n<td>✘</td>\n<td>✔</td>\n</tr>\n<tr>\n<td>面向连接</td>\n<td>✔</td>\n<td>✘</td>\n</tr>\n</tbody></table>\n<h2>终端/伪终端</h2>\n<p>伪终端机制可实现父进程控制子进程的标准输入/输出/错误。具体来说：<br>子进程令<strong>伪终端的从设备成为其控制终端</strong>，父进程通过伪终端主设备读取（子进程标准输出/错误）和写入数据（子进程标准输入）。</p>\n<p>一对伪终端（主从设备）同一个双向管道很相似（它们在内核空间中传输数据），因此这里也将其纳入 IPC 方法中。<br>不过这显然不是一种具有实践性的 IPC 手段。</p>\n","tags":["linux","os"]},{"id":"git-proxy","url":"https://yieldray.fun/posts/git-proxy","title":"Git代理","date_published":"2024-06-15T15:30:00.000Z","date_modified":"2024-06-15T15:30:00.000Z","content_text":"<p>我们知道，常用的 <a href=\"https://git-scm.com/book/zh/v2/%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E7%9A%84-Git-%E5%8D%8F%E8%AE%AE\">Git 远程协议</a>有 HTTP 协议 和 SSH 协议。</p>\n<p>要设置代理，实际上就是分别为这两种协议配置代理。</p>\n<h1>HTTP 协议</h1>\n<p><a href=\"https://git-scm.com/docs/git-config\">git config</a> 命令用于修改 Git 本身的配置文件，包含全局配置（<code>~/.gitconfig</code>）和本地（当前仓库 <code>.git/config</code>）配置。</p>\n<p>Git 本身是支持 <code>http_proxy</code> <code>https_proxy</code> <code>all_proxy</code> 环境变量的。</p>\n<p><a href=\"https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy\"><code>http.proxy</code></a> 键用于强制设置 HTTP 代理的服务器地址，支持 HTTP 和 HTTPS 代理。<br>（有关更多高级配置，参见<a href=\"https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy\">文档</a>）</p>\n<pre><code class=\"language-sh\">git config [--global] http.proxy [protocol://][user[:password]@]proxyhost[:port]\n# e.g. git config --global http.proxy http://127.0.0.1:7890\n</code></pre>\n<p>对应配置如下</p>\n<pre><code class=\"language-ini\">[http]\n    proxy = [protocol://][user[:password]@]proxyhost[:port]\n</code></pre>\n<p>Git 允许使用特殊的配置键 <a href=\"https://git-scm.com/docs/git-config#Documentation/git-config.txt-httplturlgt\"><code>http.&lt;url&gt;.*</code></a> 为指定 url 模式设置代理</p>\n<pre><code class=\"language-sh\">git config [--global] http.&lt;url&gt;.proxy [protocol://][user[:password]@]proxyhost[:port]\n# &lt;url&gt; 的模式示例（https://username@git-server.domain:8080/repo.git）:\n# https\n# git-server.domain\n# 8080\n# repo.git\n# username\n</code></pre>\n<p>对应配置如下</p>\n<pre><code class=\"language-ini\">[http &quot;&lt;url&gt;&quot;]\n    proxy = [protocol://][user[:password]@]proxyhost[:port]\n</code></pre>\n<h1>SSH 协议</h1>\n<p>对于 SSH 协议，Git 实际上是通过本机的 ssh 命令来进行通信的，因此实际上是要配置 ssh 而非 git。</p>\n<blockquote>\n<p>对于 Git for Windows，默认安装选项是使用其捆绑的 ssh，不过 Windows 本身也提供 ssh。\n<img src=\"https://s2.loli.net/2024/06/15/6JwpfdDBbctzE5h.png\" alt=\"git-for-windows.png\"></p>\n</blockquote>\n<p>ssh 的用户配置文件位于 <code>~/.ssh/config</code></p>\n<p><a href=\"https://www.man7.org/linux/man-pages/man5/ssh_config.5.html\">ssh_config</a> 的 ProxyCommand 项，用于指定一个命令来处理对指定 Host （占位符 <code>%h</code>）和 Port （占位符 <code>$p</code>） 的连接。</p>\n<p>Unix 一般使用 nc（netcat） 命令（这里不会说明 nc 的细节，见具体 man 手册），Git for Windows 则可以使用捆绑的 connect 命令。</p>\n<hr>\n<p><a href=\"https://man.freebsd.org/cgi/man.cgi?nc\">BSD nc</a> 命令</p>\n<pre><code class=\"language-sh\">usage: nc [-46CDdFhklNnrStUuvZz] [-I length] [-i interval] [-M ttl]\n          [-m minttl] [-O length] [-P proxy_username] [-p source_port]\n          [-q seconds] [-s source] [-T keyword] [-V rtable] [-W recvlimit] [-w timeout]\n          [-X proxy_protocol] [-x proxy_address[:port]]           [destination] [port]\n\n       -X proxy_protocol\n           Requests\tthat nc\tshould use the specified protocol when talking\n           to the proxy server.  Supported protocols are &quot;4&quot; (SOCKS\t v.4),\n           &quot;5&quot;  (SOCKS  v.5) and &quot;connect&quot; (HTTPS proxy).  If the protocol\n           is not specified, SOCKS version 5 is used.\n\n       -x proxy_address[:port]\n           Requests\tthat nc\tshould connect to destination using a proxy at\n           proxy_address and port.\tIf port\tis not\tspecified,  the\t well-\n           known port for the proxy\tprotocol is used (1080 for SOCKS, 3128\n           for HTTPS).\n</code></pre>\n<pre><code class=\"language-sh\">Host github.com\n    User git\n  #  -X connect 为 http 代理\n    ProxyCommand nc -X connect -x 127.0.0.1:7890 %h %p\n  #  -X 5 为 socks5 代理（默认）\n  # ProxyCommand nc -X 5 -x 127.0.0.1:7891 %h %p\n</code></pre>\n<hr>\n<p><a href=\"https://www.man7.org/linux/man-pages/man1/ncat.1.html\">Nmap ncat</a> 命令</p>\n<pre><code class=\"language-sh\">Ncat 7.80 ( https://nmap.org/ncat )\nUsage: ncat [options] [hostname] [port]\n</code></pre>\n<pre><code class=\"language-sh\">Host github.com\n    User git\n  #  --proxy-type http 为 http 代理\n    ProxyCommand ncat --proxy 127.0.0.1:7890 --proxy-type http %h %p\n  #  --proxy-type socks5 为 socks5 代理\n  # ProxyCommand ncat --proxy 127.0.0.1:7891 --proxy-type socks5 %h %p\n</code></pre>\n<hr>\n<p>Windows 下，connect 命令</p>\n<pre><code class=\"language-sh\">connect --- simple relaying command via proxy.\nVersion 1.105\nusage: C:\\Program Files\\Git\\mingw64\\bin\\connect.exe [-dnhst45] [-p local-port]\n          [-H proxy-server[:port]] [-S [user@]socks-server[:port]]\n          [-T proxy-server[:port]]\n          [-c telnet-proxy-command]\n          host port\n</code></pre>\n<pre><code class=\"language-sh\">Host github.com\n    User git\n  # -H 设置 http 代理\n    ProxyCommand connect -H 127.0.0.1:7890 %h %p\n  # -S 设置 socks 代理\n  # ProxyCommand connect -S 127.0.0.1:7891 %h %p\n</code></pre>\n<hr>\n<p>Git 还允许通过<a href=\"https://git-scm.com/docs/git/zh_HANS-CN#git-codeGITSSHcode\">环境变量</a>来指定其调用的 ssh</p>\n<p>例如：</p>\n<pre><code class=\"language-sh\">export GIT_SSH_COMMAND=&#39;ssh -o ProxyCommand=&quot;nc -x 127.0.0.1:7891 %h %p&quot;&#39;\n</code></pre>\n<p>对于 Linux，也可以使用通用的工具来应用代理，如 <a href=\"https://tsocks.sourceforge.net/\">tsocks</a> <a href=\"https://proxychains.sourceforge.net/\">proxychains</a> <a href=\"https://github.com/mzz2017/gg\">go-graft</a></p>\n","tags":["git"]},{"id":"go-buildmode","url":"https://yieldray.fun/posts/go-buildmode","title":"Go构建库","date_published":"2024-06-14T20:30:00.000Z","date_modified":"2024-06-14T20:30:00.000Z","content_text":"<blockquote>\n<p>相关文档：<a href=\"https://go.dev/doc/\">Go Documentation</a> &gt; <a href=\"https://go.dev/doc/cmd\">Command</a> &gt; <a href=\"https://pkg.go.dev/cmd/go\">go</a> &gt; <a href=\"https://pkg.go.dev/cmd/go#hdr-Build_modes\">Build modes</a><br>gcc选项：<a href=\"https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html\">https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html</a></p>\n</blockquote>\n<pre><code class=\"language-go\">package main // cgo 要求包名为 main\n\nimport &quot;C&quot;\n\n//export Add\nfunc Add(a, b int) int { // or: func Add(a, b C.int) C.int\n    return a + b\n}\n\n// 必须有一个空的 main 函数\nfunc main() {}\n</code></pre>\n<p>下面的命令生成 <code>mylib.a</code> 静态库文件和 <code>mylib.h</code> 头文件。</p>\n<blockquote>\n<p>即使是在 Windows 下，也是生成兼容 GCC ABI 的 <code>.a</code> 静态库文件（ar 格式，含 <code>.o</code> 文件），而非兼容 MSVC ABI 的 <code>.dll</code> 静态库文件（ar 格式，含 <code>.obj</code> 文件）。</p>\n</blockquote>\n<pre><code class=\"language-sh\">go build -buildmode=c-archive -o mylib.a mylib.go\n\nfile mylib.a\n# current ar archive\n</code></pre>\n<p>下面的命令生成 <code>mylib.so</code> 共享库文件和 <code>mylib.h</code> 头文件。</p>\n<blockquote>\n<p>生成的共享库类型与本机操作系统一致，与扩展名无关联</p>\n</blockquote>\n<pre><code class=\"language-sh\"># Linux\ngo build -buildmode=c-shared -o mylib.so mylib.go\nfile mylib.so\n# ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=xxx, with debug_info, not stripped\n\n# Windows\ngo build -buildmode=c-shared -o mylib.dll mylib.go\nfile mylib.dll\n# PE32+ executable (DLL) (console) x86-64, for MS Windows\n</code></pre>\n<hr>\n<p>以下面的 C 代码为例</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\n#include &quot;mylib.h&quot;\n\nint main() {\n    int result = Add(2, 3);\n    printf(&quot;Result: %d\\n&quot;, result);\n    return 0;\n}\n</code></pre>\n<p>使用静态库，直接一起编译即可链接</p>\n<pre><code class=\"language-sh\">gcc main.c mylib.a -o main\n# 注意：linux 需要加上 -pthread\n\nfile main\n#linux#   ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=xxx, for GNU/Linux 3.2.0, with debug_info, not stripped\n#windows# PE32+ executable (console) x86-64, for MS Windows\n</code></pre>\n<p>使用共享库，可使用 cc 的链接选项</p>\n<pre><code class=\"language-sh\">gcc main.c -o main -L. -l:mylib.so\n# OR:\ngcc main.c mylib.so -o main\n\n# 对于linux，运行时需要确保 mylib.so 在 $LD_LIBRARY_PATH 目录下： LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main\nLD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ldd ./main\n#        linux-vdso.so.1 (0x00007fffd5347000)\n#        mylib.so =&gt; ./mylib.so (0x00007ff2fd6d5000)\n#        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff2fd4e0000)\n#        libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ff2fd4bd000)\n#        /lib64/ld-linux-x86-64.so.2 (0x00007ff2fd83f000)\n</code></pre>\n<p>或者将库嵌入可执行文件，则无需动态链接</p>\n<pre><code class=\"language-sh\">gcc main.c -o main -L. -l:mylib.so -Wl,-rpath=.\n</code></pre>\n<p>See Also: <a href=\"https://yian.me/blog/coding/cgo-examples.html\">https://yian.me/blog/coding/cgo-examples.html</a></p>\n","tags":["go"]},{"id":"go-wasm","url":"https://yieldray.fun/posts/go-wasm","title":"Go WASM/WASI","date_published":"2024-06-10T19:59:20.000Z","date_modified":"2024-06-10T19:59:20.000Z","content_text":"<h1>WASM</h1>\n<p>对于 WASM，谷歌的 <a href=\"https://go.dev/blog/go1.11\">Go&gt;=1.11</a> 编译器目前是通过 <code>syscall/js</code> 包，与 JavaScript 环境（而且主要是浏览器）交互，而并非直接将 Go 函数编译为 Wasm 函数。<br>具体来说：通过 Go 侧的 <code>syscall/js</code> 和必须的 <code>func main()</code> 函数，以及在 <a href=\"https://github.com/golang/go/wiki/WebAssembly\">官方 wiki</a> 上说明的 <code>cp &quot;$(go env GOROOT)/misc/wasm/wasm_exec.js&quot; .</code> 提供的一个 JavaScript 侧的对 JavaScript 环境的包装脚本，才能够实现操纵 JavaScript 环境，因此也就能够间接实现“导出”函数到 JavaScript（准确来说是添加函数到 JavaScript 环境）。</p>\n<blockquote>\n<p>注：GOOS=js GOARCH=wasm</p>\n</blockquote>\n<p>很明显上面的方法并非针对 Wasm 本身，而实际上是 JavaScript 了。但如果我们的目标只是编译 WASM 函数（并且我们的 Go 侧可以根本无需 main 函数），那么只能寻求 tinygo 这样的编译器了。<br>本文不会说明 tinygo 对 Wasm 目标的编译，具体参见 <a href=\"https://tinygo.org/docs/guides/webassembly/wasm/\">https://tinygo.org/docs/guides/webassembly/wasm/</a></p>\n<pre><code class=\"language-go\">package main\n\n// 从外部命名空间导入\n//\n//export add\nfunc add(x, y int) int\n\n\n// 导出到外部命名空间\n//\n//export multiply\nfunc multiply(x, y int) int {\n    return x * y;\n}\n</code></pre>\n<h1>WASI</h1>\n<p><a href=\"https://go.dev/blog/go1.21\">Go 1.21</a> 实现了 WASI preview 1 syscall API （wasi_snapshot_preview1）编译目标。对应的 GOOS 值为 wasip1。<br>通过这个新特性，终于可以脱离 JavaScript 环境执行 Wasm 了（当然也就无需 <code>syscall/js</code> 了）。</p>\n<blockquote>\n<p>注：GOOS=wasip1 GOARCH=wasm</p>\n</blockquote>\n<p>要实现 WASI，就需要实现从外部命名空间导入函数，这里摘抄一个 <a href=\"https://go.dev/blog/wasi\">Go 博客</a> 中给出的示例：</p>\n<pre><code class=\"language-go\">//go:wasmimport wasi_snapshot_preview1 random_get\n//go:noescape\nfunc random_get(buf unsafe.Pointer, bufLen size) errno\n</code></pre>\n<p>另外下面从官方博客中摘抄目前实现中存在的限制：</p>\n<p>虽然 <code>wasip1</code> 端口通过了所有标准库测试，但 Wasm 架构的一些显著基本限制可能会让用户感到惊讶。</p>\n<p>Wasm 是一个单线程架构，没有并行性。调度器仍然可以调度 goroutine 并发运行，标准输入/输出/错误是非阻塞的，因此一个 goroutine 可以在另一个 goroutine 读取或写入时执行，但任何主机函数调用（例如使用上述示例请求随机数据）将导致所有 goroutine 阻塞，直到主机函数调用返回。</p>\n<p><code>wasip1</code> API 中一个显著缺失的功能是网络套接字的完整实现。<code>wasip1</code> 仅定义对已打开的套接字进行操作的函数，这使得支持 Go 标准库的一些最流行功能（如 HTTP 服务器）成为不可能。像 Wasmer 和 WasmEdge 这样的 Host 实现了 <code>wasip1</code> API 的扩展，允许打开网络套接字。虽然这些扩展未由 Go 编译器实现，但存在一个第三方库，<a href=\"https://github.com/stealthrocket/net\"><code>github.com/stealthrocket/net</code></a>，它使用 <code>go:wasmimport</code> 允许在支持的 Wasm 主机上使用 <code>net.Dial</code> 和 <code>net.Listen</code>。这使得在使用此包时可以创建 <code>net/http</code> 服务器和其他网络相关功能。</p>\n","tags":["go","wasm"]},{"id":"web-push-notifications","url":"https://yieldray.fun/posts/web-push-notifications","title":"Demystifying Web Push Notifications","date_published":"2024-05-29T10:35:59.000Z","date_modified":"2024-05-29T10:35:59.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://pqvst.com/2023/11/21/web-push-notifications/\">https://pqvst.com/2023/11/21/web-push-notifications/</a><br>另见：<a href=\"https://web.dev/articles/push-notifications-web-push-protocol\">https://web.dev/articles/push-notifications-web-push-protocol</a></p>\n</blockquote>\n<p>对于我最近的 <a href=\"https://pqvst.com/2023/10/23/one-day-build-expense-tracking/\">One Day Build: Expense Tracking</a> 项目，我希望在渐进式 Web 应用中启用通知。ChatGPT 难以生成任何有用的代码，我也很难在网上找到任何简洁明了的解释。</p>\n<p>本博客文章旨在全面介绍实现 Web Push 通知所需的所有步骤。我还创建了一个完整的工作示例，其中包含一个 node.js 后端，供那些只想查看代码而不是阅读本文的人参考：</p>\n<p><a href=\"https://github.com/pqvst/minimal-web-push\">https://github.com/pqvst/minimal-web-push</a></p>\n<h1>Web Push 如何运作？</h1>\n<p>简而言之，Web Push 的运作方式是，你的应用会与浏览器供应商提供的“推送服务”进行交互。此过程包含三个主要步骤，如下图所示：</p>\n<p><img src=\"https://i1.wp.com/pqvst.com/assets/img/web-push/overview.png\" alt=\"web push overview\"></p>\n<h2>1. 创建订阅</h2>\n<p>你的客户端代码创建一个 Web Push 订阅，并将订阅发送到你的后端。订阅只是一些 JSON，其中包含唯一的（特定于浏览器的）端点和一些加密密钥。以下是 Firefox 订阅示例（因此端点是 mozilla.com）：</p>\n<pre><code class=\"language-json\">{\n    &quot;endpoint&quot;: &quot;https://updates.push.services.mozilla.com/wpush/v2/...&quot;,\n    &quot;expirationTime&quot;: null,\n    &quot;keys&quot;: {\n        &quot;auth&quot;: &quot;...&quot;,\n        &quot;p256dh&quot;: &quot;...&quot;\n    }\n}\n</code></pre>\n<p>使用 Safari，你将收到 Apple 端点 (<code>https://web.push.apple.com/...</code>)，在 Chrome 中，你将获得 Google 端点 (<code>https://fcm.googleapis.com/fcm/send/...</code>)。</p>\n<h2>2. 发送通知</h2>\n<p>你的后端代码使用订阅详细信息向浏览器供应商托管的推送服务发送推送通知。然后，推送服务确保将通知发送回你的浏览器。</p>\n<h2>3. 处理通知</h2>\n<p>你的浏览器收到推送通知并触发你 Service Worker 中的回调。然后，你的 Service Worker 可选择显示通知或执行你希望执行的任何其他操作。</p>\n<h1>先决条件：VAPID 密钥</h1>\n<p>VAPID 密钥对于确保 Web Push 在所有主流浏览器上正常运行是必需的。VAPID 代表 <a href=\"https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid\"><strong>V</strong>oluntary <strong>Ap</strong>plication <strong>S</strong>erver <strong>Id</strong>entification (VAPID)</a> ，本质上只是如何生成一组公钥-私钥的规范。尽管名称中带有“自愿”，但事实并非如此，*因为 Chrome 和 Safari 都要求你提供 VAPID 密钥。*我测试过的唯一不需要 VAPID 密钥的浏览器是 Firefox。</p>\n<p>如果你尝试在没有 VAPID 密钥的情况下在 Safari 中订阅推送通知，你将收到以下错误：</p>\n<pre><code>Subscribing for push requires an applicationServerKey\n</code></pre>\n<p>在 Chrome 中，你将收到此错误：</p>\n<pre><code>DOMException: Registration failed - missing applicationServerKey, and gcm_sender_id not found in manifest\n</code></pre>\n<p>虽然你可以在技术上自己生成 VAPID 密钥，但使用生成器（如 <a href=\"https://vapidkeys.com/\">vapidkeys.com</a>）要容易得多，该生成器会为你生成一组密钥。</p>\n<h1>服务端实现</h1>\n<p>为了从你的后端应用程序服务器发送 Web Push 通知，你必须正确构造、编码和加密消息。根据你使用的编程语言，你可能会找到一个可以帮助你完成此操作的库。</p>\n<p>如果你使用 node.js 后端，那么添加 Web Push 支持非常简单。有一个很好的库 <a href=\"https://www.npmjs.com/package/web-push\">叫做 <code>web-push</code></a> ，它可以为你定制 Web Push 通知。</p>\n<h2>1. 导入并配置 web-push</h2>\n<p>要开始使用 <code>web-push</code>，只需调用该函数来设置你的 VAPID 密钥。务必同时包含一个电子邮件地址（用 <code>mailto:</code> 开头）。</p>\n<pre><code class=\"language-js\">import webPush from &quot;web-push&quot;;\n\n// TODO: 生成 VAPID 密钥 (如：https://vapidkeys.com/)\nconst vapid = {\n    publicKey: &quot;...&quot;,\n    privateKey: &quot;...&quot;,\n};\n\nwebPush.setVapidDetails(&quot;mailto:&lt;email-address&gt;&quot;, vapid.publicKey, vapid.privateKey);\n</code></pre>\n<h2>2. 存储订阅</h2>\n<p>Web Push 订阅是在客户端生成的，因此你很可能需要某种方式将订阅从前端传递到后端。你可能还想以某种方式保存它们，例如将它们存储在你的数据库中或将它们保存在持久化 JSON 文件中。如果你不保存订阅数据，那么在你的服务器重新启动时，你将丢失所有现有的订阅！</p>\n<pre><code class=\"language-js\">app.post(&quot;/subscribe&quot;, authenticateRequest, (req, res) =&gt; {\n    const sub = req.body;\n    // TODO: 持久化订阅 (如：存入数据库)\n    res.status(200).end();\n});\n</code></pre>\n<h2>广播通知</h2>\n<p>你只需要实现的另一件事就是实际创建和发送新通知的方式。如果我们向所有订阅广播通知，那么我们只需遍历我们保存的订阅数组并使用 web-push 库调用 <code>sendNotification</code>。</p>\n<p>如果你收到错误，表示用户已撤销你的页面上通知权限（或订阅已过期）。你可以捕获这些错误并删除无效订阅。</p>\n<pre><code class=\"language-js\">async function pushNotification(payload) {\n    await Promise.all(\n        subscriptions.map(async (sub) =&gt; {\n            try {\n                await webPush.sendNotification(sub, payload); // 不成功会抛出异常\n            } catch (err) {\n                console.log(sub.endpoint, &quot;-&gt;&quot;, err.message);\n                // TODO: 删除订阅 (如：从数据库中)\n            }\n        }),\n    );\n}\n\n// 发送测试通知\npushNotification(&quot;This is a test notification!&quot;);\n</code></pre>\n<h1>客户端</h1>\n<p>客户端实现稍微复杂一些。你需要两个文件：一个用于你的 Service Worker，另一个用于你的主客户端应用程序。我们只需要将 Service Worker 的回调用于传入通知的处理。</p>\n<h2>Service Worker： <code>/sw.js</code></h2>\n<pre><code class=\"language-js\">self.addEventListener(&quot;push&quot;, (event) =&gt; {\n    const options = {\n        body: event.data.text(),\n        icon: &quot;/apple-touch-icon.png&quot;,\n        badge: &quot;/badge.png&quot;,\n    };\n    event.waitUntil(self.registration.showNotification(&quot;My App&quot;, options));\n});\n</code></pre>\n<blockquote>\n<p>**为什么我们将我们的调用包装在 <code>event.waitUntil</code> 中？**由于 Service Worker 作为后台进程运行，因此可能被暂停/终止。通过将 promise 包装在 <code>waitUntil</code> 中，我们告诉浏览器工作正在进行中，并且它不应该在工作完成之前终止我们的 Service Worker。</p>\n</blockquote>\n<h2>客户端应用程序： <code>/client.js</code></h2>\n<p>在我们的主应用程序脚本中，我们必须负责请求通知权限、注册我们的 Service Worker 以及使用浏览器的 <code>pushManager</code> API 实际创建推送通知订阅。</p>\n<h3>1. 请求通知权限</h3>\n<p>首先，我们需要注意已确保我们有权推送通知（如果没有这些权限，我们的通知毫无用处）。在你页面的某个位置，你可能会希望显示一个链接或按钮，供用户单击以启用通知。</p>\n<pre><code class=\"language-html\">&lt;a id=&quot;promptLink&quot; onclick=&quot;onPromptClick()&quot;&gt;启用通知&lt;/a&gt;\n</code></pre>\n<p>如果已授予（或拒绝）通知，我们可以隐藏链接或相应地更新 UI。</p>\n<pre><code class=\"language-js\">function updatePrompt() {\n    if (&quot;Notification&quot; in window) {\n        if (Notification.permission === &quot;granted&quot; || Notification.permission === &quot;denied&quot;) {\n            promptLink.style.display = &quot;none&quot;;\n        } else {\n            promptLink.style.display = &quot;block&quot;;\n        }\n    }\n}\n\nfunction onPromptClick() {\n    if (&quot;Notification&quot; in window) {\n        Notification.requestPermission().then((permission) =&gt; {\n            updatePrompt();\n            if (permission === &quot;granted&quot;) {\n                console.log(&quot;Notification permission granted.&quot;);\n                init();\n            } else if (permission === &quot;denied&quot;) {\n                console.warn(&quot;Notification permission denied.&quot;);\n            }\n        });\n    }\n}\n</code></pre>\n<h3>2. 注册Service Worker并启用推送通知</h3>\n<p>接下来，我们确保支持 Service Worker 并注册我们的 Service Worker，以便我们可以接收通知。最后，我们将使用浏览器 <code>pushManager</code> API 请求推送通知订阅，然后将其发送到我们的后端服务器。</p>\n<p>对于此步骤，你需要你的 VAPID 公钥（确保只在客户端代码中包含你的 <em>公钥</em>，并将你的私钥保密）。</p>\n<p>这个过程不言自明。确保支持 Service Worker，注册 Service Worker，然后检查我们是否已经拥有有效的推送通知订阅，否则创建新的订阅。</p>\n<p>在两种情况下，我们都会将订阅数据发送到我们的后端以确保它被存储。</p>\n<pre><code class=\"language-js\">const vapidPublicKey = &quot;...&quot;;\n\nasync function initServiceWorker() {\n    if (&quot;serviceWorker&quot; in navigator) {\n        const swRegistration = await navigator.serviceWorker.register(&quot;sw.js&quot;);\n        const subscription = await swRegistration.pushManager.getSubscription();\n        if (subscription) {\n            console.log(&quot;用户已订阅：&quot;, subscription);\n            sendSubscriptionToServer(subscription);\n        } else {\n            const subscription = await swRegistration.pushManager.subscribe({\n                userVisibleOnly: true,\n                applicationServerKey: vapidPublicKey,\n            });\n            console.log(&quot;用户已订阅：&quot;, subscription);\n            sendSubscriptionToServer(subscription);\n        }\n    } else {\n        console.warn(&quot;不支持 Service Worker&quot;);\n    }\n}\n\nfunction sendSubscriptionToServer(subscription) {\n    fetch(&quot;/subscribe&quot;, {\n        method: &quot;post&quot;,\n        body: JSON.stringify(subscription),\n        headers: { &quot;content-type&quot;: &quot;application/json&quot; },\n    });\n}\n\nwindow.addEventListener(&quot;load&quot;, () =&gt; {\n    initServiceWorker();\n    updatePrompt();\n});\n</code></pre>\n<h1>调试提示：重新加载 Service Worker</h1>\n<p>请注意，Service Worker 在你重新加载页面时<em>不会</em>自动重新加载。如果你在本地进行开发并对 Service Worker 进行修改，你需要在浏览器的开发工具中手动重新加载 Service Worker <em>或者</em>启用页面重新加载时自动重新加载 Service Worker 的选项。</p>\n<p><img src=\"https://i1.wp.com/pqvst.com/assets/img/web-push/devtools.png\" alt=\"\"></p>\n<h1>额外功能：可点击的通知</h1>\n<p>另一个你可能想要实现的功能是使你的通知可点击。起初，我以为点击通知会自动打开相关页面。然而，事实并非如此。你需要在你的 Service Worker 中自己实现这一功能。</p>\n<p>实现这个功能的代码比我预想的要复杂一些。以下是我在线上找到的最佳示例，它确保在点击通知后清除通知，并且如果目标页面已经打开，则聚焦该页面，否则在新窗口/标签页中打开目标页面。</p>\n<pre><code class=\"language-js\">const targetUrl = &quot;...&quot;;\n\nself.addEventListener(&quot;notificationclick&quot;, (event) =&gt; {\n    self.console.log(&quot;notificationclick&quot;);\n    event.notification.close(); // Android 需要显式关闭\n    event.waitUntil(\n        clients.matchAll({ type: &quot;window&quot; }).then((windowClients) =&gt; {\n            // 检查是否已经有一个窗口/标签页打开了目标 URL\n            for (var i = 0; i &lt; windowClients.length; i++) {\n                var client = windowClients[i];\n                // 如果是，则聚焦它\n                if (client.url === targetUrl &amp;&amp; &quot;focus&quot; in client) {\n                    return client.focus();\n                }\n            }\n            // 如果没有，则在新窗口/标签页中打开目标 URL\n            if (clients.openWindow) {\n                return clients.openWindow(targetUrl);\n            }\n        }),\n    );\n});\n</code></pre>\n<p>根据我的测试，这在所有浏览器（Firefox、Chrome、Safari、Android、iOS）上似乎都能很好地工作。</p>\n","tags":["web-api"]},{"id":"web-assembly","url":"https://yieldray.fun/posts/web-assembly","title":"Web Assembly","date_published":"2024-05-26T12:12:12.000Z","date_modified":"2024-05-26T12:12:12.000Z","content_text":"<p><a href=\"https://webassembly.org/\">WebAssembly</a>（简称 Wasm）是一种面向<strong>堆栈虚拟机</strong>的<em>二进制指令格式</em>（这就比使用寄存器的汇编简单很多，但也可看作是一门低级的类汇编语言）。<br>Wasm 被设计为编程语言的便携式编译目标，使其能够在网络上部署客户端和服务器端应用程序。</p>\n<blockquote>\n<p>本篇不涉及 <a href=\"https://wasi.dev/\">WASI</a>：WebAssembly 系统接口（WASI）是一组针对编译为 W3C WebAssembly（Wasm）标准的软件的标准 API 规范。WASI 旨在为应用程序提供一个安全的标准接口，这些应用程序可以从任何语言编译为 Wasm，并能够在任何环境中运行——从浏览器到云端再到嵌入式设备。</p>\n</blockquote>\n<h1><a href=\"https://developer.mozilla.org/docs/WebAssembly\">API 概览</a></h1>\n<blockquote>\n<p>参考性地观察 Typescript 类型声明：</p>\n<ul>\n<li><a href=\"https://github.com/microsoft/TypeScript/blob/af3a61fe4487a92d59f9479aa4249d897b91af14/src/lib/dom.generated.d.ts#L26942\">浏览器</a></li>\n<li><a href=\"https://github.com/denoland/deno/blob/16ed81f62cac68f1ef0cfa925fffbf2d208eed61/cli/tsc/dts/lib.deno.shared_globals.d.ts#L18\">Deno</a></li>\n</ul>\n</blockquote>\n<pre><code class=\"language-mermaid\">flowchart TB\n    Module -- 实例化为 --&gt; Instance\n    Instance -- 访问 --&gt; Memory\n    Instance -- 访问 --&gt; Table\n    Instance -- 访问 --&gt; Global\n    Module -- 共享 --&gt; Global\n    WebAssembly代码 -- instantiate() instantiateStreaming() --&gt; Module\n    WebAssembly代码 -- instantiate() instantiateStreaming() --&gt; Instance\n    WebAssembly代码 -- compile() compileStreaming() --&gt; Module\n</code></pre>\n<table>\n<thead>\n<tr>\n<th>名称</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Global</td>\n<td>表示一个全局变量实例，可以从 JavaScript 访问，并且可以在一个或多个 WebAssembly.Module 实例之间导入/导出。这允许动态链接多个模块。</td>\n</tr>\n<tr>\n<td>Module</td>\n<td>包含了已经被浏览器编译的无状态 WebAssembly 代码。该代码能够高效地与 Worker 共享，并多次实例化。</td>\n</tr>\n<tr>\n<td>Instance</td>\n<td>一个有状态的、可执行的模块（Module）的实例。实例对象包含了所有能够从 JavaScript 调用 WebAssembly 代码的导出的 WebAssembly 函数。</td>\n</tr>\n<tr>\n<td>compile()</td>\n<td>将 WebAssembly 二进制代码编译为 WebAssembly.Module 对象。</td>\n</tr>\n<tr>\n<td>compileStreaming()</td>\n<td>直接从流式底层源编译 WebAssembly.Module。</td>\n</tr>\n<tr>\n<td>instantiate()</td>\n<td>允许你编译和实例化 WebAssembly 代码。</td>\n</tr>\n<tr>\n<td>instantiateStreaming()</td>\n<td>编译和实例化 WebAssembly 代码的主要 API，它返回 Module 及其第一个实例。</td>\n</tr>\n<tr>\n<td>Memory</td>\n<td>一个存储了可被实例（Instance）访问的内存原始字节的可变长 ArrayBuffer。</td>\n</tr>\n<tr>\n<td>Table</td>\n<td>一个用于存储可被实例（Instance）访问的不透明值（例如函数引用）的可变长类型化数组。</td>\n</tr>\n</tbody></table>\n<h1>理解 WebAssembly 文本格式和二进制格式</h1>\n<h2>概述</h2>\n<p><a href=\"https://webassembly.github.io/spec/core/binary/\">Wasm 二进制格式</a> 是对 Wasm 模块的抽象语法树的紧凑线性编码，格式通过属性文法定义。\n模块的二进制编码被组织成多个<a href=\"https://webassembly.github.io/spec/core/binary/modules.html#sections\">段</a>。大多数段对应于模块记录的一个组成部分，唯一的例外是函数定义被分成了两个段：函数段和代码段。函数段包含函数的类型声明，而代码段包含函数的主体。</p>\n<p><a href=\"https://webassembly.github.io/spec/core/text/\">Wasm 文本格式</a>（WAT，WebAssembly Text format）是一种人类可读的表示形式。本质上来说，它类似于汇编指令。语法上类似于 Lisp，将抽象语法树表示为 S- 表达式，格式同样由属性文法定义。</p>\n<p>下面以一个非常简单的 Wasm 模块举例，模块包含 add 函数，并可在 C 语言中表示如下：</p>\n<pre><code class=\"language-c\">int32_t add(int32_t lhs, int32_t rhs) {\n    return lhs + rhs;\n}\n</code></pre>\n<pre><code class=\"language-wasm\">(module\n  (func $add (param $lhs i32) (param $rhs i32) (result i32)\n    local.get $lhs\n    local.get $rhs\n    i32.add\n  )\n  (export &quot;add&quot; (func $add))\n)\n</code></pre>\n<pre><code>00 61 73 6D  &quot;\\0asm&quot;\n01 00 00 00  版本 1\n\n\n注：下面是多个段，每个段的第一个字节是段号\n附：段长可以置0，表示解码者需要推测段长，但此时段尾需要添加 FIXUP\n\n01        段号为1，类型（type）段\n07        长度为7字节\n01        类型数量为1\n60        当前为函数类型\n02        参数数量为2\n7F 7F     参数类型为 (i32, i32)\n01        返回值数量为 1\n7F        返回类型为 (i32)\n\n03        段号为3，函数（function）段\n02        长度为2字节\n01        包含1个函数\n00        索引为1\n\n07        段号为7，导出（export）段\n07        长度为7字节\n01        导出数量为1\n03        导出的符号名称长度为3字节\n61 64 64  &quot;add&quot;\n00        导出类型为函数\n00        导出的索引为0\n\n0A        段号为10，代码（code）段\n09        段长为9字节\n01        包含1个函数体\n07        函数体长度为7字节\n00        包含0个本地（local）变量\n20 00     local.get 0\n20 01     local.get 1\n6A        i32.add\n0B        end\n</code></pre>\n<p>显然可以看出，WAT 格式虽然比汇编语言要“高级”一些，但也很接近编译后的二进制表示。因此下面只以 WAT 格式来说明。</p>\n<blockquote>\n<p><a href=\"https://github.com/WebAssembly/wabt\">wabt</a> 工具包提供命令行程序，可实现 WAT 与 WASM 格式的双向转换。 （<a href=\"https://webassembly.github.io/wabt/demo/\">wabt demos</a>）</p>\n<p>当然也可以在 npm 中找到一些库来验证：</p>\n<pre><code class=\"language-js\">import { compile } from &quot;watr&quot;;\n\nconst u8a = compile(`(module\n  (func $add (param $lhs i32) (param $rhs i32) (result i32)\n    local.get $lhs\n    local.get $rhs\n    i32.add\n  )\n  (export &quot;add&quot; (func $add))\n)`);\n// OR:\nconst u8a = Buffer.from(&quot;0061736d0100000001070160027f7f017f030201000707010361646400000a09010700200020016a0b&quot;, &quot;hex&quot;);\n\nconst module = new WebAssembly.Module(u8a);\nconst instance = new WebAssembly.Instance(module);\nconst { add } = instance.exports;\n\nadd(128, 256); // 384\n</code></pre>\n</blockquote>\n<h2>模块</h2>\n<blockquote>\n<p>本篇主要关注 Wasm 的 MVP（Minimum Viable Product）版本，更新的特性很可能被省略。<br>有关各执行环境的支持程度，参考<a href=\"https://webassembly.org/features/\">此处</a>。</p>\n</blockquote>\n<p>Wasm 含且仅含一个模块。在文本格式中，一个模块被表示为一个大的 S- 表达式，其 EBNF 语法参见<a href=\"https://github.com/WebAssembly/spec/blob/master/interpreter/README.md#s-expression-syntax\">此处</a>。</p>\n<p>在最开始的示例中，使用了别名来引用变量。我们可能会见到使用索引来引用变量的代码，因此加以说明：</p>\n<pre><code class=\"language-wasm\">(func (param i32) (param f32) (local f64)\n  local.get 0    ;; 将0号局部变量（i32类型的参数）入栈\n  local.get 1    ;; 将1号局部变量（f32类型的参数）入栈\n  local.get 2    ;; 将2号局部变量（类型为f64）入栈\n  drop drop drop ;; 无返回值，因此丢弃栈中的所有值\n)\n\n;; 使用索引来获取局部变量很不友好，更清晰的做法是使用别名\n;; 在类型声明的前面添加一个使用美元符号（$）作为前缀的名字\n\n(func (param $p1 i32) (param $p2 f32) (local $loc i32)\n  local.get $p1\n  local.get $p2\n  local.get $loc\n  drop drop drop\n)\n</code></pre>\n<p>Wasm 的目标之一是安全，因此函数调用会检查类型。</p>\n<pre><code class=\"language-wasm\">(func $add (param $lhs i32) (param $rhs i32)\n  (result i32) ;; 返回值，执行后栈顶会多出一个i32类型的值\n  (i32.add (local.get $lhs) (local.get $rhs))\n)\n\n;; 下面是错误示例\n\n(func (export &quot;test&quot;)\n  (call $add (f32.const 3.14) (i64.const 15926)) ;; CompileError，参数类型不匹配\n  i32.const 8080 ;; CompileError，因为没有声明返回值，执行后栈应为空\n)\n</code></pre>\n<p>下面是一个更复杂的示例，阶乘函数（循环实现）：</p>\n<pre><code class=\"language-wasm\">(func $factorial (param $n i32) (result i32)\n  (local $result i32) ;; 定义局部变量 $result\n  (local $counter i32) ;; 定义局部变量 $counter\n\n  local.get $n\n  local.set $counter ;; 等价于 (local.set $counter (local.get $n)) 即 $counter = $n\n  i32.const 1\n  local.set $result ;; 等价于 (local.set $result (i32.const 1)) 即 $result = 1\n\n  block\n    loop\n      local.get $counter\n      i32.const 1\n      i32.le_s ;; 等价于 (i32.le_s (local.get $counter) (i32.const 1))\n      br_if 1  ;; 如果 $counter &lt;= 1，则跳出循环\n\n      local.get $result\n      local.get $counter\n      i32.mul           ;; 等价于 (i32.mul (local.get $result) (local.get $counter))\n      local.set $result ;; $result *= $counter\n\n      local.get $counter\n      i32.const 1\n      i32.sub            ;; 等价于 (i32.sub (local.get $counter) (i32.const 1))\n      local.set $counter ;; $counter -= 1\n\n      br 0 ;; 跳回循环的开始\n    end\n  end\n\n  local.get $result ;; 返回 $result\n)\n</code></pre>\n<p>递归实现则更简洁（非尾递归）：</p>\n<pre><code class=\"language-wasm\">(func $factorial (param $n i32) (result i32)\n  (local $result i32)\n\n  ;; 递归基准情况：如果 $n &lt;= 1，返回 1\n  local.get $n\n  i32.const 1\n  i32.le_s\n  if (result i32)\n    i32.const 1\n  ;; 递归情况：$n * factorial($n - 1)\n  else\n    local.get $n\n    local.get $n\n    i32.const 1\n    i32.sub\n    call $factorial\n    i32.mul\n  end\n)\n</code></pre>\n<h2>通过 WebAssembly API 与 Javascript 交互</h2>\n<h3>导入导出</h3>\n<p>导出函数（到外部）</p>\n<pre><code class=\"language-wasm\">;; 声明后导出\n(export &quot;exportName&quot; (func $funcName))\n\n;; 声明即导出\n(func (export &quot;exportName&quot;) (param $n i32) (result i32)\n  (local.set $n)\n)\n</code></pre>\n<p>导入函数（从外部）</p>\n<pre><code class=\"language-wasm\">(module\n  (import &quot;console&quot; &quot;log&quot; (func $log (param i32)))\n  (func (export &quot;log666&quot;)\n    i32.const 666\n    call $log\n  )\n)\n</code></pre>\n<p>通过 JavaScript 提供函数，以及获取 Wasm 函数，可以通过以下方法：</p>\n<pre><code class=\"language-js\">const importObject = {\n    console: {\n        log: (arg) =&gt; {\n            console.log(arg);\n        },\n    },\n};\n\nconst { instance } = await WebAssembly.instantiateStreaming(fetch(&quot;xxx.wasm&quot;), importObject);\n\nconst { log666 } = instance.exports;\n\nlog666();\n</code></pre>\n<p>模块之间还可以导入和导出全局变量</p>\n<pre><code class=\"language-wasm\">(module\n  ;; 定义全局变量\n  (global $myGlobal (mut i32) (i32.const 42))\n\n  ;; 导出全局变量\n  (export &quot;myGlobal&quot; (global $myGlobal))\n\n  ;; 导入全局变量\n  (global $g (import &quot;js&quot; &quot;global&quot;) (mut i32))\n\n  ;; 获取全局变量\n  (func (export &quot;getGlobal&quot;) (result i32)\n    (global.get $g)\n  )\n\n  ;; 修改全局变量\n  (func (export &quot;incGlobal&quot;)\n    (global.set $g (i32.add (global.get $g) (i32.const 1)))\n  )\n)\n</code></pre>\n<p>可以在多个 Wasm 以及 JavaScript 之间导入和导出，下面只演示了从 JavaScript 导入</p>\n<pre><code class=\"language-js\">const global = new WebAssembly.Global({ value: &quot;i32&quot;, mutable: true }, 0);\nconst importObject = { js: { global } };\nglobal.value = 32; // 可修改\n</code></pre>\n<h3>内存</h3>\n<p>显然，仅通过栈，我们只能传递多个 i32,i64,f32,f64 这样的值。如果要传递数组（比如字符串，本质上来说就是指针），则是行不通的，我们需要内存（WebAssembly.Memory）。</p>\n<p>内存也可在多个 Wasm 以及 JavaScript 之间创建、导入和导出（但 MVP 版本的 Wasm 模块只支持一个内存）。</p>\n<pre><code class=\"language-wasm\">(import &quot;js&quot; &quot;mem&quot;\n  (memory 1) ;; 表示导入的内存必须至少有 1 页 (64KB)\n)\n\n(import &quot;console&quot; &quot;log&quot; (\n  func $log (param i32 i32)) ;; 打印字符串：两个参数表示字符串，起始地址和长度\n)\n\n(data\n  (i32.const 0) &quot;Hello, world!&quot; ;; 因为是静态字符串，因此直接在数据段声明\n)\n\n(func (export &quot;helloWorld&quot;)\n  i32.const 0  ;; 起始地址为 0\n  i32.const 13 ;; 长度为 13\n  call $log\n\n  ;; 写入内存当然需要其它指令，如：\n  ;; (i32.store (i32.const 0) (i32.const 123))\n)\n</code></pre>\n<pre><code class=\"language-js\">const mem = new WebAssembly.Memory({\n    initial: 1,\n    // 可增长 .grow() 但初始页数为 1\n});\n\nconst importObject = {\n    console: {\n        log: (offset, length) =&gt; {\n            const bytes = new Uint8Array(mem.buffer, offset, length);\n            const string = new TextDecoder(&quot;utf8&quot;).decode(bytes);\n            console.log(string);\n        },\n    },\n    js: { mem },\n};\n</code></pre>\n<p>下面的示例覆盖了上例没有演示的部分：</p>\n<pre><code class=\"language-wasm\">;; 从 Wasm 创建多个内存\n(memory $mem0 1)\n(memory $mem1 1)\n;; 默认写入0号内存\n(data (i32.const 0) &quot;Hello from mem0&quot;)\n;; 写入指定内存\n(data (memory $mem1) (i32.const 1) &quot;Hello from mem1&quot;)\n;; 从 Wasm 导出内存\n(export &quot;memory1&quot; (memory $mem1))\n;; 创建初始页面大小为1、最大为2的共享内存\n(memory $sharedMem 1 2 shared)\n</code></pre>\n<p>内存支持如下指令（<a href=\"https://developer.mozilla.org/docs/WebAssembly/Reference/Memory\">MDN Reference</a>）：</p>\n<ul>\n<li>Grow: 增加内存实例的大小</li>\n<li>Size: 获取内存实例的大小</li>\n<li>Load: 从内存中加载一个数字</li>\n<li>Store: 将一个数字存储到内存中</li>\n<li>Copy: 将数据从内存的一个区域复制到另一个区域</li>\n<li>Fill: 将一个区域的所有值设置为特定的字节</li>\n</ul>\n<p>当然也都有完全等效的 <a href=\"https://developer.mozilla.org/docs/WebAssembly/JavaScript_interface/Memory\">JavaScript API</a></p>\n<pre><code class=\"language-ts\">class Memory {\n    constructor(descriptor: {\n        initial: number;\n        maximum?: number;\n        /**\n         * shared 指定底层的 buffer 是 ArrayBuffer 还是 SharedArrayBuffer\n         */\n        shared?: boolean;\n    });\n\n    readonly buffer: ArrayBuffer | SharedArrayBuffer;\n\n    /**\n     * 增长多少页（每页 64KB）\n     */\n    grow(delta: number): number;\n}\n</code></pre>\n<p>对于规范中新的共享内存，很显然涉及到多线程（当然是指运行环境的多线程，毕竟 Wasm 只是提供模块而已）。<br>我们都以浏览器环境为例，那么 JavaScript 对多线程的支持是通过 <a href=\"https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Using_web_workers\">Web Workers API</a> 实现的。<br>规范也新增内存原子性访问指令，此处就不展开了。</p>\n<h3>表格</h3>\n<p>为保证内存安全，Wasm 不允许随意解释内存（例如，不允许将一块内存解释为函数指针），因为内存可随时被动态修改。</p>\n<p>作为替代，可以使用表格（WebAssembly.Table），表格也由环境提供。目前，表格只能存储 anyfunc，即任意类型的函数引用。表格还可以在多个实例中共享，实现动态链接。</p>\n<pre><code class=\"language-wasm\">(module\n  (table 2 funcref)\n  (elem (i32.const 0) $f1 $f2)\n  (func $f1 (result i32) i32.const 123)\n  (func $f2 (result i32) i32.const 456)\n\n  (type $return_i32 (func (result i32))) ;; 类型段，调用函数需要提供类型\n  (func (export &quot;callAnyfunc0and1&quot;)\n    (call_indirect (type $return_i32) (i32.const 0)) ;; 通过索引调用函数\n    (call_indirect (type $return_i32) (i32.const 1))\n  )\n)\n\n;; 注：上面因为只有一个表格，故 call_indirect 无需指定表格\n;; 也可创建命名表格并指定之\n(table $tableName 1 funcref)\n(func $callTableNameFn0\n  i32.const 0\n  call_indirect $tableName (type $return_i32)\n)\n</code></pre>\n<p>注：由于 JavaScript 是弱类型的，所以在 JavaScript 调用 Wasm 表格的函数是很随意的。<br>但 Wasm 通过 call_indirect 调用函数则需要提供类型，并且会检查类型。<br>不匹配的类型将导致 WebAssembly.RuntimeError</p>\n<p>也可以在 JavaScript 中修改表格：</p>\n<pre><code class=\"language-js\">const table = new WebAssembly.Table({ initial: 2, element: &quot;anyfunc&quot; });\nconst f0 = table.get(0);\nconst f1 = table.get(1);\ntable.set(0, () =&gt; {});\ntable.set(1, () =&gt; {});\n</code></pre>\n<h3>异常</h3>\n<p>Wasm 和 JavaScript 都可以抛出和捕获异常。对于 JavaScript，使用的是 WebAssembly 命名空间下的异常。</p>\n<p>Wasm 异常的本质是一个标签：</p>\n<pre><code class=\"language-wasm\">(tag $my_exception (param i32))\n\n(func $throw_exception\n  $my_exception (i32.const 42)\n)\n\n(func $try_catch\n  (try\n    (do\n      ;; 可能抛出异常的代码\n    )\n    (catch $my_exception\n      ;; 异常处理代码\n      ;; 或使用 catch_all 捕获所有异常\n    )\n  )\n)\n\n(func $handle_exception (result i32)\n  (local $exception_param i32)\n  (try (result i32)\n    (do\n      (call $throw_exception)\n      (i32.const 0) ;; 返回 0\n    )\n    (catch $my_exception\n      (local.set $exception_param)\n      (local.get $exception_param) ;; 返回异常标签携带的参数\n    )\n  )\n)\n</code></pre>\n<p>从 JavaScript 导入异常</p>\n<pre><code class=\"language-wasm\">(module\n  (import &quot;extmod&quot; &quot;exttag&quot; (tag $tagname (param i32)))\n\n  (func $throwException (param $errorValueArg i32)\n    local.get $errorValueArg\n    throw $tagname\n  )\n\n  (func (export &quot;run&quot;)\n    i32.const 42\n    call $throwException\n  )\n)\n</code></pre>\n<pre><code class=\"language-ts\">const exttag = new WebAssembly.Tag({ parameters: [&quot;i32&quot;] });\n\nconst importObject = { extmod: { exttag } };\n\nWebAssembly.instantiateStreaming(fetch(&quot;example.wasm&quot;), importObject)\n    .then(({ instance }) =&gt; {\n        console.log(instance.exports.run());\n    })\n    .catch((e) =&gt; {\n        const err = e as WebAssembly.Exception;\n        console.error(err);\n\n        if (err.is(exttag)) {\n            console.log(`getArg 0 : ${err.getArg(exttag, 0)}`);\n        }\n    });\n</code></pre>\n<h1>Useful links</h1>\n<ul>\n<li><p><a href=\"https://github.com/EmNudge/watlings\">watlings</a> – learn Wasm text by examples.</p>\n</li>\n<li><p><a href=\"https://developer.mozilla.org/docs/WebAssembly/Reference/Control_flow\">MDN: Control Flow</a></p>\n</li>\n<li><p><a href=\"https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#loop\">WASM reference manual</a></p>\n</li>\n<li><p><a href=\"https://github.com/WebAssembly/design/blob/main/BinaryEncoding.md\">WASM binary encoding</a></p>\n</li>\n<li><p><a href=\"https://web.dev/explore/webassembly\">web.dev WebAssembly</a></p>\n</li>\n<li><p><a href=\"https://webassembly.org/specs/\">WebAssembly Specifications</a></p>\n</li>\n<li><p><a href=\"https://www.w3.org/TR/wasm-core-1/\">W3C WebAssembly Core Specification</a></p>\n</li>\n<li><p><a href=\"https://www.w3.org/TR/wasm-js-api-1/\">W3C WebAssembly JavaScript Interface</a></p>\n</li>\n<li><p><a href=\"https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/WebAssembly%E5%85%A5%E9%97%A8%E8%AF%BE/\">WebAssembly入门课</a></p>\n</li>\n<li><p><a href=\"https://jimmysong.io/book/wasm-definitive-guide/\">WebAssembly权威指南</a></p>\n</li>\n<li><p><a href=\"https://github.com/wasmlang/awesome-wasm-zh\">WebAssembly标准入门</a></p>\n</li>\n</ul>\n","tags":["web-api","wasm"]},{"id":"web-crypto-api","url":"https://yieldray.fun/posts/web-crypto-api","title":"Web Crypto API","date_published":"2024-05-18T18:00:00.000Z","date_modified":"2024-05-18T18:00:00.000Z","content_text":"<p>Web Crypto API 提供了 <a href=\"https://developer.mozilla.org/docs/Web/API/SubtleCrypto\"><code>SubtleCrypto</code> 接口</a>，该接口提供一系列加密相关的静态函数。<br>这个接口通过 <code>globalThis.crypto.subtle</code> 暴露。</p>\n<p>需要注意的是，这些函数是面向过程的：虽然密钥通过 <code>CryptoKey</code> 对象表示，但该对象只含描述该密钥的只读字段，不包含方法。<br>要使用密钥，需要将密钥通过参数提供给 SubtleCrypto 接口上的函数。</p>\n<p>在下文中，为了简洁，对于 SubtleCrypto 接口上的静态函数，我们将做一些省略表示，即：<br><code>crypto.subtle.generateKey()</code> 将表示为 <code>generateKey()</code><br>此外，它们都是异步函数，即返回值都是 Promise，但下面将做一些省略。</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-info mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>\n<p>本文不涉及 Proposal <a href=\"https://github.com/WICG/webcrypto-secure-curves\">webcrypto-secure-curves</a> 中的算法。</p>\n</div>\n<h1>生成 <code>CryptoKey</code> 对象</h1>\n<p>下面只考虑<strong>加密算法</strong>，因此密钥是必须的。</p>\n<p>若没有已导出的密钥，则首先需要生成之：<code>generateKey(algorithm, extractable, keyUsages)</code>。</p>\n<ul>\n<li><p><code>algorithm</code>：若为对称加密，返回值是 <code>CryptoKey</code>；若为非对称加密，返回值是 <code>CryptoKeyPair</code>（只不过是包装了 <code>privateKey</code> 和 <code>publicKey</code> 属性的对象而已）<br>（详细参见 <a href=\"https://w3c.github.io/webcrypto/#algorithm-overview\">W3C 规范</a>，哈希算法不包含在下表中。另外，这个表格的分类可能不够准确，另有分类参见后文）</p>\n<table>\n<tr>\n<th>加密类型</th>\n<th>参数类型</th>\n<th>算法名称</th>\n<th>描述</th>\n</tr>\n<tr>\n<td rowspan=\"5\">非对称加密</td>\n<td rowspan=\"3\">RsaHashedKeyGenParams</td>\n<td>RSASSA-PKCS1-v1_5</td>\n<td>RSA Signature Scheme with Appendix - Public Key Cryptography Standards #1 version 1.5</td>\n</tr>\n<tr>\n<td>RSA-PSS</td>\n<td>RSA - Probabilistic Signature Scheme</td>\n</tr>\n<tr>\n<td>RSA-OAEP</td>\n<td>RSA - Optimal Asymmetric Encryption Padding</td>\n</tr>\n<tr>\n<td rowspan=\"2\">EcKeyGenParams</td>\n<td>ECDSA</td>\n<td>Elliptic Curve Digital Signature Algorithm</td>\n</tr>\n<tr>\n<td>ECDH</td>\n<td>Elliptic Curve Diffie-Hellman</td>\n</tr>\n<tr>\n<td rowspan=\"5\">对称加密</td>\n<td rowspan=\"4\">AesKeyGenParams</td>\n<td>AES-CTR</td>\n<td>AES Counter Mode</td>\n</tr>\n<tr>\n<td>AES-CBC</td>\n<td>AES Cipher Block Chaining</td>\n</tr>\n<tr>\n<td>AES-GCM</td>\n<td>AES Galois/Counter Mode</td>\n</tr>\n<tr>\n<td>AES-KW</td>\n<td>AES Key Wrap</td>\n</tr>\n<tr>\n<td>HmacKeyGenParams</td>\n<td>HMAC</td>\n<td>Hash-based Message Authentication Code</td>\n</tr>\n</table>\n</li>\n<li><p><code>extractable</code>：布尔值，指定该密钥是否允许通过 <code>exportKey()</code> 或 <code>wrapKey()</code> 导出</p>\n</li>\n<li><p><code>keyUsages</code>：字符串数组，指定该密钥是否可以执行以下操作：</p>\n<ul>\n<li><code>&quot;encrypt&quot;</code>: 使用 <code>encrypt(algorithm, key, data)</code> 对数据进行加密</li>\n<li><code>&quot;decrypt&quot;</code>: 使用 <code>decrypt(algorithm, key, data)</code> 对数据进行解密</li>\n<li><code>&quot;sign&quot;</code>: 使用 <code>sign(algorithm, key, data)</code> 对数据进行签名</li>\n<li><code>&quot;verify&quot;</code>: 使用 <code>verify(algorithm, key, signature, data)</code> 验证签名的有效性</li>\n<li><code>&quot;deriveKey&quot;</code>: 使用 <code>deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages)</code> 从现有密钥派生出新密钥</li>\n<li><code>&quot;deriveBits&quot;</code>: 使用 <code>deriveBits(algorithm, baseKey, length)</code> 从现有密钥派生出指定长度的位数据</li>\n<li><code>&quot;wrapKey&quot;</code>: 使用 <code>wrapKey(format, key, wrappingKey, wrapAlgorithm)</code> 将密钥包装成可导出格式</li>\n<li><code>&quot;unwrapKey&quot;</code>: 使用 <code>unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages)</code> 将包装好的密钥解包成可用的 <code>CryptoKey</code></li>\n</ul>\n</li>\n</ul>\n<h1>导出 <code>CryptoKey</code> 对象</h1>\n<p>为了能够使用 importKey() 函数导入密钥，首先还需要通过 exportKey() 将 generateKey() 生成的密钥导出：</p>\n<p><code>exportKey(format, key)</code> 函数将一个 <code>CryptoKey</code> 对象导出为指定格式的密钥数据</p>\n<ul>\n<li><p><code>format</code>：字符串，指定导出密钥的格式，即返回值取决于此参数。支持的格式包括：</p>\n<ul>\n<li><code>&quot;raw&quot;</code>：用于导出对称密钥的原始字节数据</li>\n<li><code>&quot;pkcs8&quot;</code>：Public-Key Cryptography Standards，用于导出非对称密钥对中的私钥，以 <strong>PKCS #8 格式</strong>表示</li>\n<li><code>&quot;spki&quot;</code>：Simple Public Key Infrastructure，用于导出非对称密钥对中的公钥，以 <strong>X.509 Subject Public Key Info 格式</strong>表示</li>\n<li><code>&quot;jwk&quot;</code>：用于导出密钥为 JSON Web Key 格式</li>\n</ul>\n</li>\n<li><p><code>key</code>：一个 <code>CryptoKey</code> 对象，表示要导出的密钥</p>\n</li>\n</ul>\n<p>密钥本身只是一个字节数组，即 raw 格式为原始密钥的 ArrayBuffer。<br><a href=\"https://www.rfc-editor.org/rfc/rfc7517\">jwk 格式</a>可将<em>密钥</em>或<em>密钥对</em>表示为 JSON 文本格式（在 SubtleCrypto 接口中则是 <code>JSONWebKey</code> 对象）。<br>对称密钥没有其它包装的格式，即直接使用 raw 或通用的 jwk 格式即可。<br>对于非对称密钥：pkcs8 格式用于编码私钥，spki 格式用于编码公钥，它们都是 DER 编码的二进制格式（即 ArrayBuffer）。</p>\n<h1>导入 <code>CryptoKey</code> 对象</h1>\n<p>要将导出的密钥数据导入为 <code>CryptoKey</code> 对象，可以使用 <code>importKey(format, keyData, algorithm, extractable, keyUsages)</code> 函数。<br>该函数接收以下参数：</p>\n<ul>\n<li><code>format</code>：字符串，对应于 exportKey() 的 format 参数</li>\n<li><code>keyData</code>：对应于 exportKey() 的返回值</li>\n<li><code>algorithm</code>：对应于 generateKey() 的 algorithm 的参数</li>\n<li><code>extractable</code>：对应于 generateKey() 的 extractable 的参数</li>\n<li><code>keyUsages</code>：对应于 generateKey() 的 keyUsages 的参数</li>\n</ul>\n<h1>哈希算法</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest\"><code>digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise&lt;ArrayBuffer&gt;</code> 函数</a>用于哈希。<br>algorithm 可以是 &quot;SHA-1&quot; &quot;SHA-256&quot; &quot;SHA-384&quot; 和 &quot;SHA-512&quot;</p>\n<h1>派生算法</h1>\n<p>派生指的是派生密钥（<a href=\"https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey\"><code>deriveKey()</code></a>）和派生密码（<a href=\"https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits\"><code>deriveBits()</code></a>）。</p>\n<p>HKDF 和 PBKDF2 算法用于通过<em>密码</em>实现派生。<br>这两种算法虽然不是密钥，但是还是被抽象为了 CryptoKey，这样我们就有一个统一的 API。</p>\n<pre><code class=\"language-ts\">const key: CryptoKey = await importKey(\n    &quot;raw&quot;,\n    passwordBuffer, // 密码 // [!code highlight]\n    { name: &quot;HKDF&quot; || &quot;PBKDF2&quot; },\n    false,\n    [&quot;deriveBits&quot;, &quot;deriveKey&quot;],\n);\n</code></pre>\n<p>ECDH 本身则是专门用于派生的非对称密钥（只能用于 importKey 和 exportKey），所以它可以被生成（<code>generateKey()</code>）和导入（<code>importKey()</code>）</p>\n<h1>附：其它非对称密钥封装格式</h1>\n<h2><a href=\"https://letsencrypt.org/zh-cn/docs/a-warm-welcome-to-asn1-and-der/\">DER</a></h2>\n<p>SPKI（Subject Public Key Info）和 PKCS#8 都是基于 ASN.1（Abstract Syntax Notation One）定义的结构。</p>\n<p>ASN.1 结构本身只是<em>结构</em>而非二进制，而 DER（Distinguished Encoding Rules） 是一种用于编码 ASN.1 结构的规则（二进制格式）。</p>\n<h2>PEM</h2>\n<p>PEM（Privacy-Enhanced Message）则是文本格式，它非常简单，只是通过 base64 编码二进制密钥而已，并不关心输入的密钥是原始格式还是其它二进制格式，但往往是 DER 格式。</p>\n<p>该格式通常如下：</p>\n<pre><code>-----BEGIN XXX-----\nbase64字符串，可以换行但仅出于可读性\n-----END XXX-----\n</code></pre>\n<h1>其它参考</h1>\n<h2>示例代码</h2>\n<p><a href=\"https://github.com/diafygi/webcrypto-examples\">https://github.com/diafygi/webcrypto-examples</a><br><a href=\"https://github.com/mdn/dom-examples/tree/main/web-crypto\">https://github.com/mdn/dom-examples/tree/main/web-crypto</a></p>\n<h2>兼容性</h2>\n<p>Node.js 见：<a href=\"https://nodejs.org/api/webcrypto.html\">https://nodejs.org/api/webcrypto.html</a></p>\n<p>Deno 见：<a href=\"https://deno.land/api?s=SubtleCrypto\">https://deno.land/api?s=SubtleCrypto</a></p>\n<h2>分类</h2>\n<pre><code class=\"language-mermaid\">graph TD\n    窃听-秘密泄露 --&gt; 机密性\n    机密性 --&gt; 对称加密\n    机密性 --&gt; 非对称加密\n\n    对称加密 --&gt; AES-CTR --&gt; encrypt/decrypt\n    对称加密 --&gt; AES-CBC --&gt; encrypt/decrypt\n    对称加密 --&gt; AES-GCM --&gt; encrypt/decrypt\n\n    非对称加密 --&gt; RSASSA-PKCS1-v1_5 --&gt; sign/verify\n    非对称加密 --&gt; RSA-PSS --&gt; sign/verify\n    非对称加密 --&gt; RSA-OAEP --&gt; encrypt/decrypt\n    非对称加密 --&gt; ECDSA --&gt; sign/verify\n\n    篡改-信息被修改 --&gt; 完整性\n    完整性 --&gt; 数字签名\n    完整性 --&gt; 消息认证码 --&gt; HMAC --&gt; sign/verify\n    完整性 --&gt; 单向散列 --&gt; SHA-1,SHA-256,SHA-384,SHA-512 --&gt; digest\n\n    数字签名 --&gt; 非对称加密\n\n    伪装-中间人攻击 --&gt; 认证\n    认证 --&gt; 消息认证码\n    认证 --&gt; 数字签名\n\n    否认-事后否认事实 --&gt; 不可否认性\n    不可否认性 --&gt; 数字签名\n\n    伪随机数 --&gt; getRandomValues[&quot;getRandomValues&lt;T extends ArrayBufferView | null&gt;(array: T): T&quot;]\n\n    非对称加密 --&gt; ECDH\n    密钥派生 --&gt; ECDH --&gt; deriveKey/deriveBits\n    密钥派生 --&gt; HKDF --&gt; deriveKey/deriveBits\n    密钥派生 --&gt; PBKDF2 --&gt; deriveKey/deriveBits\n\n    密钥包装 --&gt; RSA-OAEP --&gt; wrapKey/unwrapKey\n    密钥包装 --&gt; AES-CTR --&gt; wrapKey/unwrapKey\n    密钥包装 --&gt; AES-CBC --&gt; wrapKey/unwrapKey\n    密钥包装 --&gt; AES-GCM --&gt; wrapKey/unwrapKey\n</code></pre>\n","tags":["web-api","crypto"]},{"id":"functional-composite-simple-example","url":"https://yieldray.fun/posts/functional-composite-simple-example","title":"函数式/组合式 简单原理示例","date_published":"2024-05-07T18:48:48.000Z","date_modified":"2024-05-07T18:48:48.000Z","content_text":"<p>名词定义：</p>\n<ul>\n<li>函数式：这里指的是类似 React&gt;=16.8 函数式组件（React Hooks）</li>\n<li>组合式：Vue&gt;=2.7,&gt;=3 组合式 API</li>\n</ul>\n<p>基本原理很简单，因此先代码，再说明优劣</p>\n<h1>简单原理示例</h1>\n<pre><code class=\"language-ts\">// 这个简单示例使用 Person 为例，真实的库中可能就是一个 Component 类（而且往往是仅库内部使用的）\nclass Person {\n    name: string = &quot;unnamed&quot;;\n    age: number = 0;\n    onRender?: VoidFunction;\n}\n\n/**\n * 函数式组件（组合式 API）风格：例如 React 中的 useState/useEffect 等，或 vue 的生命周期钩子如 onMounted\n * 本质上是利用闭包来捕获当前的组件的实例\n * 显然，由于是闭包，下面这个 API 函数不是纯函数\n */\nexport function setName(name: string) {\n    if (!currentInstance) throw new Error(&quot;setName 只能在函数式组件内部调用&quot;);\n    currentInstance.name = name;\n}\nexport function setAge(age: number) {\n    if (!currentInstance) throw new Error(&quot;setAge 只能在函数式组件内部调用&quot;);\n    currentInstance.age = age;\n}\nexport function onRender(cb: VoidFunction) {\n    if (!currentInstance) throw new Error(&quot;onRender 只能在函数式组件内部调用&quot;);\n    currentInstance.onRender = cb;\n}\n\n// 在这个示例中，函数式组件只是一个简单的函数\nexport type FunctionalComponent = (context?: unknown) =&gt; void;\n\n/**\n * 一般来说，当前实例的引用是仅对库内部公开的\n * 例如，在 vue 中是这样的：\n * export let currentInstance: ComponentInternalInstance | null = null\n * @see https://github.com/vuejs/core/blob/main/packages/runtime-core/src/component.ts#L644\n */\nlet currentInstance: Person | null = null;\n\n/**\n * 在这里，渲染函数只是指代真正执行任务的函数（这里和真正的渲染没有任何关系）\n */\nexport async function render(fcs: FunctionalComponent[]) {\n    const result: Person[] = [];\n    for (const fc of fcs) {\n        currentInstance = new Person();\n        fc(/* 提供一个上下文，只是为了演示 */ { timestamp: Date.now() });\n        currentInstance.onRender?.();\n\n        // 只是为了说明渲染函数真正的渲染工作可以是异步调度的\n        await new Promise((resolve) =&gt; setTimeout(resolve, 500));\n        result.push(currentInstance);\n    }\n    currentInstance = null;\n    return result;\n}\n</code></pre>\n<p>函数式组件（即一个组件的实现方式是一个函数）本身是无状态的（即不依赖外部状态）。\n而函数具体会在何时，通过什么参数被调用，也是由库决定的。</p>\n<p>React 则建议函数式组件为纯函数。</p>\n<pre><code class=\"language-ts\">import { onRender, render, setAge, setName } from &quot;./lib&quot;;\n\nconst Mike: FunctionalComponent = (ctx: unknown) =&gt; {\n    setName(&quot;Mike&quot;);\n    onRender(() =&gt; {\n        console.log(&quot;Mike is being rendered! And context is :&quot;, ctx);\n    });\n};\n\nconst Alice: FunctionalComponent = () =&gt; {\n    const name = &quot;Alice&quot;;\n    setName(name);\n    setAge(20);\n    onRender(() =&gt; {\n        console.log(`${name} is being rendered!`);\n    });\n};\n\nconst result = await render([Mike, Alice]);\nconsole.log(result);\n</code></pre>\n<h1>优点和缺点</h1>\n<p><a href=\"https://cn.vuejs.org/guide/extras/composition-api-faq.html#why-composition-api\">Vue.js 的文档</a>中其实已经说明了这种实现方式的优点，不过它是仅对于\nvue 做说明的。</p>\n<p>如果我们将这种设计抽象为一种与具体实现无关的设计模式，可以总结其优点如下：</p>\n<ul>\n<li>函数式组件本身是无状态的，这有利于代码复用（因为它不依赖上下文）</li>\n<li>库本身的 API 是通过多个函数暴露的，因此使用者只需关心自己调用的函数。未使用的\nAPI 函数还可以利用 TreeShaking 减小打包体积（不过一般收益不明显，因为这种 API\n函数往往并非实际功能的主要实现者）</li>\n</ul>\n<p>可能的缺点：</p>\n<ul>\n<li>依赖于闭包，库本身注定不是 side-effect free 的</li>\n<li>一旦使用库提供的组合式\nAPI，则对库产生强依赖，几乎不可移植，不过对于基础库来说并不算很大的缺点</li>\n</ul>\n","tags":["js"]},{"id":"vue-reactivity","url":"https://yieldray.fun/posts/vue-reactivity","title":"vue3响应式原理","date_published":"2024-05-06T17:00:00.000Z","date_modified":"2024-05-06T17:00:00.000Z","content_text":"<h1>前文</h1>\n<blockquote>\n<p>参考：《Vue.js设计与实现》，Vue.js 源码实现 <a href=\"https://github.com/vuejs/core/tree/main/packages/reactivity\">@vue/reactivity</a></p>\n</blockquote>\n<p>如何理解响应式系统？参考 Vue.js 官方文档：<a href=\"https://cn.vuejs.org/guide/extras/reactivity-in-depth\">《深入响应式系统》</a></p>\n<p>对于何谓响应式，借助 <a href=\"https://github.com/tc39/proposal-signals\">signals</a> 这一概念可能有助于理解。（如 <a href=\"https://angular.dev/guide/signals\">Angular</a> <a href=\"https://docs.solidjs.com/concepts/signals\">Solid</a> <a href=\"https://preactjs.com/guide/v10/signals/\">Preact</a>。参见<a href=\"https://github.com/transitive-bullshit/ts-reactive-comparison\">这个响应式对比</a>）</p>\n<blockquote>\n<p>目前 <a href=\"https://github.com/tc39/proposal-signals\">stage1 的这篇 proposal</a> 已经对 Signals 做了细致的研究和比较严格的定义，也包括很多相关解释性引用，本问就不重复引用了。</p>\n</blockquote>\n<p>简单来说：“signal” 是状态管理的最小单元，用于存储和更新状态，并可以订阅其状态变更。</p>\n<p>下面我们实现一个最精简的 signals，可以发现这里的核心是发布订阅机制。</p>\n<pre><code class=\"language-ts\">class Signal&lt;T&gt; extends EventTarget {\n    #value: T;\n    constructor(value: T) {\n        super();\n        this.#value = value;\n    }\n\n    get value(): T {\n        return this.#value;\n    }\n\n    set value(value: T) {\n        if (Object.is(value, this.#value)) return;\n        this.#value = value;\n        this.dispatchEvent(new CustomEvent(&quot;change&quot;));\n    }\n\n    effect(fn: VoidFunction) {\n        fn();\n        this.addEventListener(&quot;change&quot;, fn);\n        return () =&gt; this.removeEventListener(&quot;change&quot;, fn);\n    }\n}\n\nconst signal = &lt;T&gt;(val: T) =&gt; new Signal(val);\n\nclass Computed&lt;T&gt; extends Signal&lt;T&gt; {\n    // 注意这个简单实现中需要手动指明依赖。下面我们会知道 vue 是如何实现自动追踪依赖的\n    constructor(fn: (...deps: Signal&lt;any&gt;[]) =&gt; T, deps: Signal&lt;any&gt;[]) {\n        super(fn(...deps)); // 注意这里立即执行一次，得到初始值\n        for (const dep of deps) {\n            if (!(dep instanceof Signal)) continue;\n            dep.addEventListener(&quot;change&quot;, () =&gt; {\n                this.value = fn(...deps);\n            });\n        }\n    }\n}\n\nconst computed = &lt;T&gt;(fn: (...deps: Signal&lt;any&gt;[]) =&gt; T, deps: Signal&lt;any&gt;[]) =&gt; new Computed(fn, deps);\n</code></pre>\n<p>这个简单示例可以这样使用：</p>\n<pre><code class=\"language-js\">const name = signal(&quot;Jane&quot;);\nconst surname = signal(&quot;Doe&quot;);\nconst fullName = computed(() =&gt; `${name} ${surname}`, [name, surname]);\n\nfullName.effect(() =&gt; console.log(fullName.value));\n// -&gt; Jane Doe\n\nname.value = &quot;John&quot;;\n// -&gt; John Doe\n</code></pre>\n<p>作为 vue 的用户，我们会发现 vue 的响应式系统默认是深响应的（ref/reactive），可以看作是 signals 用 Proxy 包装的递归版本。</p>\n<h1>基于对象的基本响应式系统</h1>\n<blockquote>\n<p>应当注意 Vue.js 对 TypeScript 具有完善的支持，下面的代码虽然使用 TypeScript 编写，但仅做类型示意</p>\n</blockquote>\n<pre><code class=\"language-ts\">// 存储副作用函数的桶\nconst bucket = new WeakMap&lt;typeof data, Map&lt;keyof typeof data, Set&lt;EffectFunction&gt;&gt;&gt;();\n\n// 原始数据\nconst data = {\n    foo: 1,\n    bar: 2,\n    // this key is for computed()\n    value: undefined,\n};\n\n// 对原始数据的代理\nexport const obj = new Proxy(data, {\n    // 拦截读取操作\n    get(target, key) {\n        // 将副作用函数 activeEffect 添加到存储副作用函数的桶中\n        track(target, key as keyof typeof data);\n        // 返回属性值\n        return Reflect.get(target, key);\n    },\n    // 拦截设置操作\n    set(target, key, newVal) {\n        // 设置属性值\n        Reflect.set(target, key, newVal);\n        // 把副作用函数从桶里取出并执行\n        trigger(target, key as keyof typeof data);\n        return true;\n    },\n});\n\n// effect 副作用函数内部需要触发代理对象 get 操作\n// 注意：这个实现是缺陷的，因为如果在副作用函数内部只是读取代理对象本身，那么并不会被 track\nfunction track(target: typeof data, key: keyof typeof data) {\n    if (!activeEffect) return;\n    let depsMap = bucket.get(target);\n    if (!depsMap) {\n        bucket.set(target, (depsMap = new Map()));\n    }\n    // 当前 key 关联的所有副作用\n    let deps: Set&lt;EffectFunction&gt; | undefined = depsMap.get(key);\n    if (!deps) {\n        depsMap.set(key, (deps = new Set()));\n    }\n    deps.add(activeEffect);\n    // 保持包装后的副作用函数，其持有其对应 key 的所有副作用函数集合\n    // 用于实现重建依赖关系（之后会有 cleanup 逻辑）\n    activeEffect.deps.push(deps);\n}\n\n// set\nfunction trigger(target: typeof data, key: keyof typeof data) {\n    const depsMap = bucket.get(target);\n    if (!depsMap) return;\n    // 指定 key 的所有 effects\n    const effects: Set&lt;EffectFunction&lt;void&gt;&gt; | undefined = depsMap.get(key);\n\n    const effectsToRun = new Set&lt;EffectFunction&gt;();\n    effects?.forEach((effectFn) =&gt; {\n        // 如果副作用是由于在副作用函数内部触发 set 导致的，则忽略（防止无限循环）\n        if (effectFn !== activeEffect) {\n            effectsToRun.add(effectFn);\n        }\n    });\n    effectsToRun.forEach((effectFn) =&gt; {\n        if (effectFn.options?.scheduler) {\n            effectFn.options.scheduler(effectFn);\n        } else {\n            effectFn();\n        }\n    });\n    // effects?.forEach(effectFn =&gt; effectFn())\n}\n\ninterface EffectFunction&lt;Returns = void&gt; {\n    /** 包装后的副作用函数 */\n    (): Returns;\n    /** 对应 options */\n    options?: {\n        lazy?: boolean;\n        scheduler?: (fn: VoidFunction) =&gt; void;\n    };\n    /** 与该副作用函数相关的依赖集合 */\n    deps: Set&lt;EffectFunction&gt;[];\n}\n\n// 用一个全局变量存储当前激活的 effect 函数\nlet activeEffect: EffectFunction;\n// effect 栈\nconst effectStack: EffectFunction[] = [];\n\n// 注意，除非指定 options.lazy 为 true，否则副作用函数将立即（通过可选的 options.scheduler）执行一次\nexport function effect&lt;T&gt;(fn: () =&gt; T /* 要注册的副作用函数 */, options: EffectFunction&lt;T&gt;[&quot;options&quot;] = {}) {\n    // 实际包装后的副作用函数，包装函数的附加功能是：\n    // 生成 effect 嵌套栈（允许 effect 嵌套），并保持 activeEffect 指向当前激活的 effect 函数\n    const effectFn: EffectFunction&lt;T&gt; = () =&gt; {\n        // 清理依赖，依赖是包含当前副作用的所有 key 关联的所有副作用集合\n        for (const deps of effectFn.deps) {\n            deps.delete(effectFn);\n        }\n        effectFn.deps = []; // 重置，之后将通过 track 函数重建依赖\n        /**\n         * 清理工作实现下面的代码不会造成无意义的副作用函数执行\n         * effect(() =&gt; {\n         *     document.body.innerText = obj.ok ? obj.text : &#39;not&#39;\n         * })\n         * obj.ok = false\n         * 副作用终将导致 track() 函数被调用，track 函数通过 Set 重建依赖（因此无需去重操作）\n         */\n\n        // 当调用 effect 注册副作用函数时，将副作用函数复制给 activeEffect\n        activeEffect = effectFn;\n        // 在调用副作用函数之前将当前副作用函数压栈\n        effectStack.push(effectFn);\n        const res = fn();\n        // 在当前副作用函数执行完毕后，将当前副作用函数弹出栈，并还原 activeEffect 为之前的值\n        effectStack.pop();\n        activeEffect = effectStack.at(-1)!;\n\n        return res;\n    };\n    // 将 options 挂在到 effectFn 上\n    effectFn.options = options;\n    // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合\n    effectFn.deps = [];\n    // 执行副作用函数\n    if (!options.lazy) {\n        effectFn(); // 副作用函数内部需要触发 set 操作，导致调用 track 函数\n    }\n\n    return effectFn;\n}\n\n// =========================\n\nexport function computed&lt;T&gt;(getter: () =&gt; T) {\n    let value: T;\n    let dirty = true;\n\n    const effectFn = effect(getter, {\n        lazy: true,\n        scheduler() {\n            if (!dirty) {\n                dirty = true;\n                trigger(obj as any, &quot;value&quot;);\n            }\n        },\n    });\n\n    const obj = {\n        get value() {\n            if (dirty) {\n                value = effectFn();\n                dirty = false;\n            }\n            track(obj as any, &quot;value&quot;);\n            return value;\n        },\n    };\n\n    return obj;\n}\n\n// =========================\n\nfunction traverse(value: Record&lt;keyof any, any&gt;, seen = new Set()) {\n    if (typeof value !== &quot;object&quot; || value === null || seen.has(value)) return;\n    seen.add(value);\n    for (const k in value) {\n        traverse(value[k], seen);\n    }\n\n    return value;\n}\n\nexport function watch&lt;\n    Source = Record&lt;keyof any, any&gt; /* the proxied data */,\n    Getter = () =&gt; Source /* getter returning the key of proxied data */,\n&gt;(\n    source: Source | Getter,\n    cb: (oldValue?: Source, newValue?: Source, onInvalidate?: (fn: VoidFunction) =&gt; void) =&gt; VoidFunction,\n    options: { immediate?: boolean; flush?: &quot;pre&quot; | &quot;post&quot; } = {},\n) {\n    let getter: Getter;\n    if (typeof source === &quot;function&quot;) {\n        getter = source as Getter;\n    } else {\n        getter = (() =&gt; traverse(source as any)) as Getter;\n    }\n\n    let oldValue: Source, newValue: Source;\n\n    let cleanup: VoidFunction;\n    function onInvalidate(fn: VoidFunction) {\n        cleanup = fn;\n    }\n\n    const job = () =&gt; {\n        newValue = effectFn();\n        if (cleanup) {\n            cleanup();\n        }\n        cb(oldValue, newValue, onInvalidate);\n        oldValue = newValue;\n    };\n\n    const effectFn = effect(\n        // 执行 getter\n        () =&gt; (getter as () =&gt; Source)(),\n        {\n            lazy: true,\n            scheduler: () =&gt; {\n                if (options.flush === &quot;post&quot;) {\n                    const p = Promise.resolve();\n                    p.then(job);\n                } else {\n                    job();\n                }\n            },\n        },\n    );\n\n    if (options.immediate) {\n        job();\n    } else {\n        oldValue = effectFn();\n    }\n}\n</code></pre>\n<h1>实现对语言内置对象的响应式</h1>\n<p>语言内置对象包括：Array, Map, WeakMap, Set, WeakSet</p>\n<pre><code class=\"language-ts\">/**\n * 为了简单起见，下面的类型是不完整的，特别是一些嵌套对象，注意分辨\n */\n\n// 存储副作用函数的桶\nconst bucket = new WeakMap&lt;object, Map&lt;keyof any, Set&lt;EffectFunction&gt;&gt;&gt;();\nconst ITERATE_KEY: keyof any = Symbol(); // 虚拟的对象键，用于关联 for...in 副作用\n\nconst reactiveMap = new Map&lt;object, any&gt;();\n\nexport function reactive&lt;T extends object&gt;(obj: T): T /** vue中的实际类型是 UnwrapNestedRefs&lt;T&gt; */ {\n    const proxy = createReactive(obj);\n\n    const existionProxy = reactiveMap.get(obj);\n    if (existionProxy) return existionProxy;\n\n    reactiveMap.set(obj, proxy);\n    return proxy;\n}\n\nexport declare const ShallowReactiveMarker: unique symbol; // 注意：类似这样的symbol键仅为类型系统使用，并不会产生运行时开销\nexport function shallowReactive&lt;T extends object&gt;(obj: T): T &amp; { [ShallowReactiveMarker]?: true } {\n    return createReactive(obj, true);\n}\n\nexport function readonly&lt;T extends object&gt;(\n    obj: T,\n): Readonly&lt;T&gt; /** vue中的实际类型是 DeepReadonly&lt;UnwrapNestedRefs&lt;T&gt;&gt; */ {\n    return createReactive(obj, false, true);\n}\n\nexport function shallowReadonly&lt;T extends object&gt;(obj: T): Readonly&lt;T&gt; /** vue中的实际类型是 Readonly&lt;T&gt; */ {\n    return createReactive(obj, true, true);\n}\n\nconst arrayInstrumentations: Record&lt;string, Function&gt; = {};\n\n([&quot;includes&quot;, &quot;indexOf&quot;, &quot;lastIndexOf&quot;] as const).forEach((method) =&gt; {\n    const originMethod = Array.prototype[method] as Function;\n    arrayInstrumentations[method] = function (...args: any[]) {\n        // this 是代理对象，先在代理对象中查找，将结果存储到 res 中\n        let res = originMethod.apply(this, args);\n\n        if (res === false) {\n            // res 为 false 说明没找到，在通过 this.RAW 拿到原始数组，再去原始数组中查找，并更新 res 值\n            res = originMethod.apply(this.RAW, args);\n        }\n        // 返回最终的结果\n        return res;\n    };\n});\n\nlet shouldTrack = true;\n([&quot;push&quot;] as const).forEach((method) =&gt; {\n    const originMethod = Array.prototype[method];\n    arrayInstrumentations[method] = function (...args: any[]) {\n        shouldTrack = false;\n        let res = originMethod.apply(this, args);\n        shouldTrack = true;\n        return res;\n    };\n});\n\nconst RAW: unique symbol = Symbol();\nfunction createReactive&lt;T extends object&gt;(obj: T, isShallow = false, isReadonly = false) {\n    return new Proxy(obj, {\n        // 拦截读取操作\n        get(target, key, receiver) {\n            console.log(&quot;get: &quot;, key);\n            if (key === RAW) {\n                // 代理对象使用一个特殊的 RAW 键访问原始对象\n                return target;\n            }\n\n            if (Array.isArray(target) &amp;&amp; arrayInstrumentations.hasOwnProperty(key)) {\n                return Reflect.get(arrayInstrumentations, key, receiver);\n            }\n\n            // 非只读的时候才需要建立响应联系\n            if (!isReadonly &amp;&amp; typeof key !== &quot;symbol&quot;) {\n                track(target, key);\n            }\n\n            const res = Reflect.get(target, key, receiver);\n\n            if (isShallow) {\n                return res;\n            }\n\n            if (typeof res === &quot;object&quot; &amp;&amp; res !== null) {\n                // 深只读/响应\n                return isReadonly ? readonly(res) : reactive(res);\n            }\n\n            return res;\n        },\n        // 拦截设置操作\n        set(target, key, newVal, receiver) {\n            console.log(&quot;set: &quot;, key);\n            if (isReadonly) {\n                console.warn(`属性 ${String(key)} 是只读的`);\n                return true;\n            }\n            const oldVal = target[key as keyof T];\n            // 对 trigger 函数标记当前 set 操作是新增还是修改，实现正确处理 for...in （只遍历对象键）副作用\n            // 注：如果只是修改某个对象值，那么对象的键集合实际上没有变更，因此不需要触发副作用\n            // 如果属性不存在，则说明是在添加新的属性，否则是设置已存在的属性\n            const type = Array.isArray(target)\n                ? // 如果代理目标是数组，则检测被设置的索引值是否小于数组长度\n                  Number(key) &lt; target.length\n                    ? // 如果是，则视作 SET 操作，否则是 ADD 操作\n                      &quot;SET&quot;\n                    : &quot;ADD&quot;\n                : Object.prototype.hasOwnProperty.call(target, key)\n                  ? &quot;SET&quot;\n                  : &quot;ADD&quot;;\n            // 设置属性值\n            const res = Reflect.set(target, key, newVal, receiver);\n            if (target === receiver.RAW) {\n                if (oldVal !== newVal &amp;&amp; (oldVal === oldVal || newVal === newVal)) {\n                    trigger(target, key, type, newVal);\n                }\n            }\n\n            return res;\n        },\n        has(target, key) {\n            track(target, key);\n            return Reflect.has(target, key);\n        },\n        ownKeys(target) {\n            // ownKeys 处理函数实现正确绑定代理对象的与对象键相关副作用\n            console.log(&quot;ownkeys: &quot;);\n            track(target, Array.isArray(target) ? &quot;length&quot; : ITERATE_KEY);\n            return Reflect.ownKeys(target);\n        },\n        deleteProperty(target, key) {\n            if (isReadonly) {\n                console.warn(`属性 ${String(key)} 是只读的`);\n                return true;\n            }\n            const hadKey = Object.prototype.hasOwnProperty.call(target, key);\n            const res = Reflect.deleteProperty(target, key);\n\n            if (res &amp;&amp; hadKey) {\n                trigger(target, key, &quot;DELETE&quot;);\n            }\n\n            return res;\n        },\n    });\n}\n\nfunction track(target: object, key: keyof any) {\n    if (!activeEffect || !shouldTrack) return;\n    let depsMap = bucket.get(target);\n    if (!depsMap) {\n        bucket.set(target, (depsMap = new Map()));\n    }\n    let deps = depsMap.get(key);\n    if (!deps) {\n        depsMap.set(key, (deps = new Set()));\n    }\n    deps.add(activeEffect);\n    activeEffect.deps.push(deps);\n}\n\nfunction trigger(target: object, key: keyof any, type: &quot;ADD&quot; | &quot;DELETE&quot; | &quot;SET&quot;, newVal?: any) {\n    console.log(&quot;trigger&quot;, key);\n    const depsMap = bucket.get(target);\n    if (!depsMap) return;\n    const effects = depsMap.get(key);\n\n    const effectsToRun = new Set&lt;EffectFunction&gt;();\n    effects?.forEach((effectFn) =&gt; {\n        if (effectFn !== activeEffect) {\n            effectsToRun.add(effectFn);\n        }\n    });\n\n    if (type === &quot;ADD&quot; || type === &quot;DELETE&quot;) {\n        const iterateEffects = depsMap.get(ITERATE_KEY);\n        iterateEffects?.forEach((effectFn) =&gt; {\n            if (effectFn !== activeEffect) {\n                effectsToRun.add(effectFn);\n            }\n        });\n    }\n\n    // 当操作类型为 ADD 并且目标对象是数组时，应该取出并执行那些与 length 属性相关联的副作用函数\n    if (type === &quot;ADD&quot; &amp;&amp; Array.isArray(target)) {\n        const lengthEffects = depsMap.get(&quot;length&quot;);\n        lengthEffects?.forEach((effectFn) =&gt; {\n            if (effectFn !== activeEffect) {\n                effectsToRun.add(effectFn);\n            }\n        });\n    }\n\n    // 如果操作目标是数组，并且修改了数组的 length 属性\n    if (Array.isArray(target) &amp;&amp; key === &quot;length&quot;) {\n        // 对于索引大于或等于新的 length 值的元素，需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行\n        depsMap.forEach((effects, key) =&gt; {\n            if ((key as number) &gt;= newVal) {\n                effects.forEach((effectFn) =&gt; {\n                    if (effectFn !== activeEffect) {\n                        effectsToRun.add(effectFn);\n                    }\n                });\n            }\n        });\n    }\n\n    effectsToRun.forEach((effectFn) =&gt; {\n        if (effectFn.options?.scheduler) {\n            effectFn.options.scheduler(effectFn);\n        } else {\n            effectFn();\n        }\n    });\n}\n\ninterface EffectFunction&lt;Returns = void&gt; {\n    /** 包装后的副作用函数 */\n    (): Returns;\n    /** 对应 options */\n    options?: {\n        lazy?: boolean;\n        scheduler?: (fn: VoidFunction) =&gt; void;\n    };\n    /** 与该副作用函数相关的依赖集合 */\n    deps: Set&lt;EffectFunction&gt;[];\n}\n\n// 用一个全局变量存储当前激活的 effect 函数\nlet activeEffect: EffectFunction;\n// effect 栈\nconst effectStack: EffectFunction[] = [];\n\nexport function effect&lt;T = any&gt;(fn: () =&gt; T, options: { lazy?: boolean; scheduler?: (fn: VoidFunction) =&gt; void } = {}) {\n    const effectFn: EffectFunction = () =&gt; {\n        cleanup(effectFn);\n        // 当调用 effect 注册副作用函数时，将副作用函数复制给 activeEffect\n        activeEffect = effectFn;\n        // 在调用副作用函数之前将当前副作用函数压栈\n        effectStack.push(effectFn);\n        const res = fn();\n        // 在当前副作用函数执行完毕后，将当前副作用函数弹出栈，并还原 activeEffect 为之前的值\n        effectStack.pop();\n        activeEffect = effectStack[effectStack.length - 1];\n\n        return res;\n    };\n    // 将 options 挂在到 effectFn 上\n    effectFn.options = options;\n    // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合\n    effectFn.deps = [];\n    // 执行副作用函数\n    if (!options.lazy) {\n        effectFn();\n    }\n\n    return effectFn;\n}\n\nfunction cleanup(effectFn: EffectFunction) {\n    // 清理依赖，依赖是包含当前副作用的所有 key 关联的所有副作用集合\n    for (const deps of effectFn.deps) {\n        deps.delete(effectFn);\n    }\n    effectFn.deps = []; // 重置，之后将通过 track 函数重建依赖\n    /**\n     * 清理工作实现下面的代码不会造成无意义的副作用函数执行\n     * effect(()=&gt;{\n     *     document.body.innerText = obj.ok ? obj.text : &#39;not&#39;\n     * })\n     * obj.ok = false\n     * 副作用终将导致 track() 函数被调用，track 函数通过 Set 重建依赖（因此无需去重操作）\n     */\n}\n\n// =================================================================\n\n// const obj = reactive({ foo: 1, bar: 2 });\n\n// const newObj = {\n//   ...obj\n// }\n\n// effect(() =&gt; {\n//   console.log(newObj.foo)\n// })\n\n// obj.foo = 100\n\ndeclare const RefSymbol: unique symbol;\nexport declare const RawSymbol: unique symbol;\nexport interface Ref&lt;T = any&gt; {\n    value: T;\n    /**\n     * Type differentiator only.\n     * We need this to be in public d.ts but don&#39;t want it to show up in IDE\n     * autocomplete, so we use a private Symbol instead.\n     */\n    [RefSymbol]: true;\n}\n\nexport function ref&lt;T = any&gt;(val: T): Ref&lt;T&gt; {\n    const wrapper = {\n        value: val,\n    };\n    // 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef，并且值为 true\n    Object.defineProperty(wrapper, &quot;__v_isRef&quot;, {\n        value: true,\n    });\n    return reactive(wrapper) as Ref&lt;T&gt;;\n}\n\nexport function toRefs&lt;T extends object&gt;(obj: T) {\n    const ret = {} as { [K in keyof T]: Ref&lt;T[K]&gt; };\n    for (const key in obj) {\n        ret[key] = toRef(obj, key);\n    }\n    return ret;\n}\n\nexport function toRef&lt;T extends object, K extends keyof T&gt;(\n    obj: T,\n    key: K,\n): Ref&lt;T[K]&gt; /** vue的实际类型可能是 ToRef&lt;T[K]&gt; */ {\n    const wrapper = {\n        get value() {\n            return obj[key];\n        },\n        set value(val) {\n            obj[key] = val;\n        },\n    };\n\n    Object.defineProperty(wrapper, &quot;__v_isRef&quot;, {\n        value: true,\n    });\n\n    return wrapper as Ref&lt;T[K]&gt;;\n}\n\nexport function proxyRefs(target: any) {\n    return new Proxy(target, {\n        get(target, key, receiver) {\n            const value = Reflect.get(target, key, receiver);\n            return value.__v_isRef ? value.value : value;\n        },\n        set(target, key, newValue, receiver) {\n            const value = target[key];\n            if (value.__v_isRef) {\n                value.value = newValue;\n                return true;\n            }\n            return Reflect.set(target, key, newValue, receiver);\n        },\n    });\n}\n\n/*\nconst newObj = proxyRefs({ ...toRefs(obj) });\n\nconsole.log(newObj.foo);\nconsole.log(newObj.bar);\n\nnewObj.foo = 100;\nconsole.log(newObj.foo);\n */\n</code></pre>\n","tags":["vue"]},{"id":"tsx-config","url":"https://yieldray.fun/posts/tsx-config","title":"tsx 配置","date_published":"2024-04-29T18:00:00.000Z","date_modified":"2024-04-29T18:00:00.000Z","content_text":"<p>本篇说明 Typescript 环境下编译 JSX 的配置</p>\n<p>对于 jsx 工厂函数：</p>\n<ul>\n<li>在 React 17 及之后，React 提供一个 jsx-runtime 子模块，导出 jsx 函数<pre><code class=\"language-jsx\">import { Fragment, jsx } from &quot;react/jsx-runtime&quot;;\njsx(Fragment, { children: &quot;Hello world&quot; });\n</code></pre>\n  对应于 tsconfig.json 的 compilerOptions.jsx 设置为 react-jsx</li>\n<li>在 React 17 之前，使用的是 createElement 函数，此函数现已标记为废弃<pre><code class=\"language-jsx\">import { createElement, Fragment } from &quot;react&quot;;\ncreateElement(Fragment, null, &quot;Hello world&quot;);\n</code></pre>\n  对应于 tsconfig.json 的 compilerOptions.jsx 设置为 react</li>\n</ul>\n<h1>tsconfig.json</h1>\n<pre><code class=\"language-jsonc\">{\n    &quot;compilerOptions&quot;: {\n        /**\n         * @docs https://www.typescriptlang.org/tsconfig/#jsx\n         * @default &quot;react&quot;\n         * 此选项控制 jsx 工厂函数的导入方式\n         *\n         * react: React&lt;17 transform，对于第三方库需指定 jsxFactory\n         * 要求 jsxFactory 指定一个函数名称（细节参见下面具体选项）\n         * import { jsxFactory } from &quot;jsxFactoryModule&quot;\n         * export const HelloWorld = () =&gt; jsxFactory(&quot;h1&quot;, null, &quot;Hello world&quot;)\n         *\n         * react-jsx: React&gt;=17 transform，对于第三方库需指定 jsxImportSource\n         * 要求 jsxImportSource 提供 jsx-runtime 子模块并导出 jsx 函数\n         * import { jsx as _jsx, Fragment as _Fragment } from &quot;jsxImportSource/jsx-runtime&quot;\n         * export const HelloWorld = () =&gt; _jsx(_Fragment, { children: &quot;Hello world&quot; })\n         */\n        &quot;jsx&quot;: &quot;react-jsx&quot;,\n\n        /**\n         * @docs https://www.typescriptlang.org/tsconfig/#jsxFactory\n         * @default React.createElement\n         * 此选项指定一个函数名称 或 模块.函数名称，例如 h 或 preact.h\n         * 支持 Babel 的 [Customizing the Classic Runtime Import](https://babeljs.io/docs/babel-plugin-transform-react-jsx#customizing-the-classic-runtime-import)\n         *\n         * 附：The factory chosen will also affect where the JSX namespace is looked up (for type checking information) before falling back to the global one.\n         * If the factory is defined as React.createElement (the default), the compiler will check for React.JSX before checking for a global JSX. If the factory is defined as h, it will check for h.JSX before a global JSX.\n         */\n        &quot;jsxFactory&quot;: &quot;h&quot;,\n        /**\n         * @docs https://www.typescriptlang.org/tsconfig/#jsxFactory\n         * 同理 jsxFactory，从略\n         */\n        &quot;jsxFragmentFactory&quot;: &quot;Fragment&quot;,\n\n        /**\n         * @docs https://www.typescriptlang.org/tsconfig/#jsxImportSource\n         * @default React\n         * 此选项指定 jsx 和 Fragment 函数从哪个模块的 jsx-runtime 子模块中导入\n         * 支持 Babel 的 [Customizing the Automatic Runtime Import](https://babeljs.io/docs/babel-plugin-transform-react-jsx#customizing-the-automatic-runtime-import)\n         */\n        &quot;jsxImportSource&quot;: &quot;React&quot;,\n    },\n}\n</code></pre>\n<h1>第三方库示例</h1>\n<p>下面的示例配置使用 compilerOptions.jsx = &quot;react-jsx&quot;<br>我们将手动实现一个 JSX runtime 并实现将 jsx 转换为 HTML 字符串</p>\n<pre><code>.\n├── src\n│   ├── RayAct\n│   │   └── jsx-runtime.ts // 需要导出 jsx 函数，和可选的 Fragment 符号\n│   │   ├── index.ts       // 不做要求\n│   └── demo.tsx\n└── tsconfig.json\n</code></pre>\n<p>jsxImportSource 使用我们自定义的 RayAct 模块</p>\n<pre><code class=\"language-json\">{\n    &quot;compilerOptions&quot;: {\n        &quot;jsx&quot;: &quot;react-jsx&quot;,\n        &quot;jsxImportSource&quot;: &quot;./RayAct&quot;,\n\n        &quot;module&quot;: &quot;ESNext&quot;,\n        &quot;outDir&quot;: &quot;dist&quot;\n    },\n    &quot;include&quot;: [&quot;src/**/*&quot;]\n}\n</code></pre>\n<pre><code class=\"language-tsx\">import { render } from &quot;./RayAct&quot;;\n\nconst jsx = (\n    &lt;h1 foo=&quot;bar&quot;&gt;\n        Hello, &lt;&gt;world&lt;/&gt;!\n    &lt;/h1&gt;\n);\n\nconsole.log(jsx);\nconsole.log(render(jsx)); // =&gt; &lt;h1 foo=&quot;bar&quot;&gt;Hello, world!&lt;/h1&gt;\n</code></pre>\n<pre><code class=\"language-tsx\">import { createElement, type RayActNode } from &quot;./index&quot;;\n\nexport namespace JSX {\n    export interface IntrinsicElements {\n        [elemName: string]: any;\n    }\n}\nexport { Fragment } from &quot;./index&quot;;\n\nexport function jsx(type: string, { children, ...props }: { children?: RayActNode[]; [prop: string]: any }) {\n    return createElement(type, props, children);\n}\n</code></pre>\n<p>下面的实现兼容 compilerOptions.jsx = &quot;react&quot;</p>\n<pre><code class=\"language-tsx\">export type RayActNode = RayActElement | Array&lt;RayActNode&gt; | string | number | boolean | null | undefined;\n\nexport interface RayActElement&lt;P = any, T extends string = string&gt; {\n    type: T | Function;\n    props: P &amp; { children?: Array&lt;RayActElement | RayActElement[]&gt; };\n}\n\nexport function createElement(\n    type: string,\n    props?: Record&lt;string, any&gt; | null,\n    ...children: RayActNode[]\n): RayActElement {\n    function nodeToElement(node: RayActNode): RayActElement | RayActElement[] {\n        if (node === null) return createNullElement();\n        if (Array.isArray(node)) return node.map(nodeToElement) as RayActElement[];\n        if (typeof node === &quot;object&quot;) return node;\n        if (typeof node === &quot;string&quot;) return createTextElement(node);\n        if (typeof node === &quot;number&quot;) return createTextElement(String(node));\n        if (Boolean(node) === false) return createNullElement();\n        return createTextElement(String(node));\n    }\n    return {\n        type,\n        props: {\n            ...props,\n            children: children.map(nodeToElement),\n        },\n    };\n}\n\nfunction createTextElement(text: string): RayActElement {\n    return {\n        type: &quot;TEXT_ELEMENT&quot;,\n        props: {\n            nodeValue: text,\n            children: [],\n        },\n    };\n}\n\nfunction createNullElement(): RayActElement {\n    return {\n        type: &quot;NULL_ELEMENT&quot;,\n        props: {\n            children: [],\n        },\n    };\n}\n\nexport function Fragment(props: { children?: RayActNode[] }): RayActNode {\n    return {\n        type: &quot;FRAGMENT_ELEMENT&quot;,\n        props: {\n            children: props.children,\n        },\n    };\n}\n\nexport function render(element: RayActElement): string {\n    if (element.type === &quot;NULL_ELEMENT&quot;) return &quot;&quot;;\n    if (element.type === &quot;TEXT_ELEMENT&quot;) return element.props.nodeValue as string;\n    if (element.type === &quot;FRAGMENT_ELEMENT&quot;) {\n        return element.props.children?.map(render).join(&quot;&quot;) || &quot;&quot;;\n    }\n    if (typeof element.type === &quot;function&quot;) {\n        return render(element.type(element.props));\n    }\n\n    let sb = `&lt;${element.type}`;\n\n    Object.keys(element.props)\n        .filter((key) =&gt; key !== &quot;children&quot;)\n        .forEach((name) =&gt; {\n            const value = element.props[name];\n            if (!value) return;\n            sb += ` ${name}=&quot;${value}&quot;`;\n        });\n\n    sb += &quot;&gt;&quot;;\n\n    element.props.children.forEach((child: RayActElement) =&gt; {\n        sb += render(child);\n    });\n\n    sb += `&lt;/${element.type}&gt;`;\n\n    return sb;\n}\n</code></pre>\n","tags":["react"]},{"id":"js-string","url":"https://yieldray.fun/posts/js-string","title":"JavaScript字符串","date_published":"2024-04-17T15:15:15.000Z","date_modified":"2024-04-17T15:15:15.000Z","content_text":"<h1><a href=\"https://262.ecma-international.org/14.0/#sec-ecmascript-language-types-string-type\">string</a></h1>\n<p>字符串是原始类型，一旦字符串被初始化，字符串对象自身得到一个不变的 <a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/length\">length</a> 属性（<code>{ [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }</code>）<br>表示字符串中码元（而非字符）的数量（但字符串上的迭代器按字符迭代）</p>\n<h1><a href=\"https://datatracker.ietf.org/doc/html/rfc2781\">UTF-16</a></h1>\n<p>UTF-16 (16-bit Unicode Transformation Format) 是一种用于表示 Unicode 字符的字符编码。UTF-16 是 Unicode 标准的一部分，并且由 Unicode Consortium 设计。</p>\n<p>UTF-16 使用单个 16 位单元或一对这样的单元来表示每个字符。在 Unicode 标准中，这些单元被称为代码单元（码元）。<br>UTF-16 可以表示 Unicode 标准中的所有字符，包括那些无法用单个 16 位单元表示的字符。</p>\n<p>具体来说，UTF-16 的工作方式如下：</p>\n<p>对于 U+0000 到 U+FFFF 范围内的字符（基本多语言平面，BMP），UTF-16 使用一个 16 位代码单元来表示。这个代码单元就是字符的 Unicode 码点值。</p>\n<p>对于超出这个范围的字符（辅助平面字符），UTF-16 使用一对 16 位代码单元来表示。这对代码单元被称为代理对。</p>\n<p><img src=\"https://mermaid.ink/svg/pako:eNp9kEFLAlEUhf_K424sHOXN02b0LQJ1dFWbwk2Oi4czpuDMyDRCpoIVUhDSpqhdRSESha3CDOvPzDAu-wk9x6ho0V3dezjf4XJaULI0HShs26xeQWsbqpkqeI8X_sOgiCKR1XY-jPkg7_gJ5cM5Pm2ULrjTd_9s6PXP3fE9EiXkTvvu661_vc8lr3c4uxn6d5MijwoiZs8972jyjWe-cHfc9UYv_-HpOY6Uggru24k3OKAolAqhpflLcXEZeaejhdUdT9BcUoFDmQDK_oY-ri67ASfmJIz_gEoipiAli3FAgwCGbhusqvFOWqqJkApORTd0FShfNb3MGjVHBdXscCtrONZm0ywBdeyGLkCjrjFHV6qMt2kALbPaDld1repY9vqi56BuAerM3LKsHw-_gbZgF6iEowlCiIwJJlJMkgVoAo2QlWhMjotJmcQxJklJlDoC7AUJYucTWq-zTA\" alt=\"\"></p>\n<p>显然 UTF-16 是不定长编码的，编解码过程如下：</p>\n<p><strong>编码过程：</strong></p>\n<ol>\n<li><p>对于 U+0000 到 U+FFFF 范围内的字符（基本多语言平面，BMP），直接使用该字符的 Unicode 码点值作为其 UTF-16 编码。</p>\n</li>\n<li><p>对于超出 U+FFFF 的字符（辅助平面的字符），使用以下步骤进行编码：</p>\n<ul>\n<li>从字符的 Unicode 码点值中减去 0x10000，得到一个在 0x00000 到 0xFFFFF 范围内的值</li>\n<li>把这个值分成高位 10 位和低位 10 位两部分</li>\n<li>把高位 10 位的值加上 0xD800，得到第一个 16 位代码单元</li>\n<li>把低位 10 位的值加上 0xDC00，得到第二个 16 位代码单元</li>\n</ul>\n</li>\n</ol>\n<p><strong>解码过程：</strong></p>\n<ol>\n<li><p>如果一个 16 位代码单元在 0xD800 到 0xDFFF 之间，那么这个代码单元是一个代理对的一部分。<br>如果它在 0xD800 到 0xDBFF 之间，那么它是代理对的高位；<br>如果它在 0xDC00 到 0xDFFF 之间，那么它是代理对的低位。<br>将高位减去 0xD800，得到一个 10 位的数；将低位减去 0xDC00，得到另一个 10 位的数。<br>将这两个数合并为一个 20 位的数，然后加上 0x10000，得到字符的 Unicode 码点。</p>\n</li>\n<li><p>如果一个 16 位代码单元不在 0xD800 到 0xDFFF 之间，那么这个代码单元就是字符的 Unicode 码点。</p>\n</li>\n</ol>\n<p><strong>字节序：</strong></p>\n<ol>\n<li>大端：高位字节在前，低位字节在后。例如，Unicode 字符 U+1234 在大端字节序中表示为 12 34。</li>\n<li>小端：低位字节在前，高位字节在后。例如，Unicode 字符 U+1234 在小端字节序中表示为 34 12。</li>\n<li>字节序标记（BOM，Byte Order Mark）：如果实现了字节序，那么在文本开始处使用一个特殊的字符为字节序标记。在大端字节序中，BOM 是 U+FEFF；在小端字节序中，BOM 是 U+FFFE。（Linux 默认小端）</li>\n</ol>\n<h1><a href=\"https://262.ecma-international.org/14.0/#sec-string.fromcharcode\">ECMA262</a></h1>\n<h2><code>String.fromCharCode ( ...codeUnits )</code></h2>\n<p>This function may be called with any number of arguments which form the rest parameter codeUnits.</p>\n<p>It performs the following steps when called:</p>\n<ol>\n<li>Let <em>result</em> be the empty String.</li>\n<li>For each element <em>next</em> of <em>codeUnits</em>, do<br>a. Let <em>nextCU</em> be the code unit whose numeric value is <code>ℝ(? ToUint16(next))</code>.<br>b. Set <em>result</em> to the string-concatenation of <em>result</em> and <em>nextCU</em>.</li>\n<li>Return <em>result</em>.</li>\n</ol>\n<p>The <em>&quot;length&quot;</em> property of this function is 1<sub>𝔽</sub>.</p>\n<h2><code>String.fromCodePoint ( ...codePoints )</code></h2>\n<p>This function may be called with any number of arguments which form the rest parameter codePoints.</p>\n<p>It performs the following steps when called:</p>\n<ol>\n<li>Let <em>result</em> be the empty String.</li>\n<li>For each element next of <em>codePoints</em>, do\na. Let <em>nextCP</em> be ? ToNumber(next).<br>b. If <code>IsIntegralNumber(nextCP)</code> is <code>false</code>, throw a <strong>RangeError</strong> exception.<br>c. If <code>ℝ(nextCP) &lt; 0</code> or <code>ℝ(nextCP) &gt; 0x10FFFF</code>, throw a <strong>RangeError</strong> exception.<br>d. Set <em>result</em> to the string-concatenation of <em>result</em> and <code>UTF16EncodeCodePoint(ℝ(nextCP))</code>.</li>\n<li>Assert: If <em>codePoints</em> is empty, then <em>result</em> is the empty String.</li>\n<li>Return <em>result</em>.</li>\n</ol>\n<p>The &quot;length&quot; property of this function is 1<sub>𝔽</sub>.</p>\n<h1>附: UTF-8</h1>\n<p><a href=\"https://iamvishnu.com/posts/utf8-is-brilliant-design\">https://iamvishnu.com/posts/utf8-is-brilliant-design</a></p>\n<p><a href=\"https://github.com/vishnuharidas/utf8-playground\">https://github.com/vishnuharidas/utf8-playground</a></p>\n","tags":["js"]},{"id":"web-cache","url":"https://yieldray.fun/posts/web-cache","title":"Web缓存速查","date_published":"2024-04-04T15:51:15.000Z","date_modified":"2025-03-14T00:00:00.000Z","content_text":"<h1>规范和参考</h1>\n<p><strong>浏览器默认行为</strong><br><a href=\"https://web.dev/articles/http-cache\">https://web.dev/articles/http-cache</a><br><a href=\"https://html.spec.whatwg.org/multipage/browsing-the-web.html\">https://html.spec.whatwg.org/multipage/browsing-the-web.html</a></p>\n<blockquote>\n<p>备注：<br>如非手动 fetch，浏览器可能自动添加 If-None-Match 和 If-Modified-Since 标头<br>但此行为目前尚未在 HTML 标准中规范</p>\n</blockquote>\n<p><strong>fetch 标准</strong><br><a href=\"https://fetch.spec.whatwg.org/#http-network-or-cache-fetch\">https://fetch.spec.whatwg.org/#http-network-or-cache-fetch</a><br><a href=\"https://fetch.spec.whatwg.org/#concept-request-cache-mode\">https://fetch.spec.whatwg.org/#concept-request-cache-mode</a></p>\n<p><strong>条件请求与缓存</strong><br><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html#name-conditional-requests\">https://www.rfc-editor.org/rfc/rfc9110.html#name-conditional-requests</a><br><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html\">https://www.rfc-editor.org/rfc/rfc9111.html</a></p>\n<p><strong>参考</strong><br><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching\">https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching</a><br><a href=\"https://www.jonoalderson.com/performance/http-caching/\">https://www.jonoalderson.com/performance/http-caching/</a><br><a href=\"https://zh.wikipedia.org/wiki/Web%E7%BC%93%E5%AD%98\">https://zh.wikipedia.org/wiki/Web%E7%BC%93%E5%AD%98</a></p>\n<h1>标头</h1>\n<table>\n<thead>\n<tr>\n<th align=\"left\">类型</th>\n<th align=\"left\">消息报头</th>\n<th align=\"left\">值/示例</th>\n<th align=\"left\">作用</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control\">Cache-Control</a></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-max-age-2\"><code>max-age=&lt;seconds&gt;</code></a></td>\n<td align=\"left\">指明缓存副本的有效时长，从请求时间开始到过期时间之间的秒数</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-must-revalidate\"><code>must-revalidate</code></a></td>\n<td align=\"left\">一旦响应过时，缓存必须向源服务器验证响应的有效性后才能使用</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-must-understand\"><code>must-understand</code></a></td>\n<td align=\"left\">限制可缓存该响应的缓存为理解并符合该响应状态码要求的缓存。 包含 <code>must-understand</code> 指令的响应 <em>应该</em> 也包含 <code>no-store</code> 指令</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2\"><code>no-cache</code></a></td>\n<td align=\"left\">缓存必须先向源服务器验证后才能使用响应</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-no-store-2\"><code>no-store</code></a></td>\n<td align=\"left\">缓存不得存储任何关于请求或响应的部分，且不得使用此响应去满足其他请求</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-no-transform-2\"><code>no-transform</code></a></td>\n<td align=\"left\">中介（无论其是否实现缓存）不得转换内容</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-private\"><code>private</code></a></td>\n<td align=\"left\">共享缓存不得存储响应（即，响应仅供单个用户使用）。私有缓存可以存储响应</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-proxy-revalidate\"><code>proxy-revalidate</code></a></td>\n<td align=\"left\">类似于 <code>must-revalidate</code>，但不适用于私有缓存。共享缓存一旦响应过时，必须向源服务器验证响应的有效性后才能使用</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-public\"><code>public</code></a></td>\n<td align=\"left\">即使存在其他禁止缓存的限制，缓存也可以存储响应</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-s-maxage\"><code>s-maxage=&lt;seconds&gt;</code></a></td>\n<td align=\"left\">对于共享缓存，此指令指定的最大生存期将覆盖 <code>max-age</code> 指令或 <code>Expires</code> 头字段指定的最大生存期。并包含 proxy-revalidate 语义</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://httpwg.org/specs/rfc5861.html#n-the-stale-while-revalidate-cache-control-extension\"><code>stale-while-revalidate=&lt;seconds&gt;</code></a></td>\n<td align=\"left\">在指定的秒数内可使用过时的缓存，同时缓存在后台应尝试重新验证它</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://httpwg.org/specs/rfc5861.html#n-the-stale-if-error-cache-control-extension\"><code>stale-if-error=&lt;seconds&gt;</code></a></td>\n<td align=\"left\">如果在尝试重新验证时遇到错误，允许缓存在指定的秒数内使用过期的响应</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control\">Cache-Control</a></td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control#immutable\">immutable</a></td>\n<td align=\"left\">表示响应正文不会随时间而改变, 这是响应中的指令，不是请求指令</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-max-age\"><code>max-age=&lt;seconds&gt;</code></a></td>\n<td align=\"left\">客户端希望获取一个 age 小于或等于指定秒数的响应。 如果没有 <code>max-stale</code>，客户端不希望收到过期的响应</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale\"><code>max-stale[=&lt;seconds&gt;]</code></a></td>\n<td align=\"left\">客户端愿意接受已经过期的响应。可选的秒数表示客户端愿意接受的响应超过其新鲜生存期的最大秒数。如果不指定秒数，则客户端愿意接受任何年龄的过期响应。</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-min-fresh\"><code>min-fresh=&lt;seconds&gt;</code></a></td>\n<td align=\"left\">客户端希望获取一个在未来指定秒数内仍然保持新鲜的响应</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache\"><code>no-cache</code></a></td>\n<td align=\"left\">客户端要求缓存必须在使用存储的响应之前，先与源服务器进行成功验证</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-no-store\"><code>no-store</code></a></td>\n<td align=\"left\">客户端要求缓存不得存储此请求或任何响应的任何部分</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-no-transform\"><code>no-transform</code></a></td>\n<td align=\"left\">客户端要求中间件不要转换响应的内容（例如，不要压缩或更改内容编码）</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"></td>\n<td align=\"left\"><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached\"><code>only-if-cached</code></a></td>\n<td align=\"left\">客户端只希望获取已存储的响应。如果缓存没有可用的响应，它应返回一个 504 (Gateway Timeout) 状态码</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Expires\">Expires</a></td>\n<td align=\"left\"><code>Sun, 16 Oct 2016 05:43:02 GMT</code></td>\n<td align=\"left\">告知浏览器在过期时间前可以使用副本 (存在时间不一致问题)</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Pragma\">Pragma</a></td>\n<td align=\"left\"><code>no-cache</code></td>\n<td align=\"left\">告知浏览器忽略资源的缓存副本（兼容 HTTP1.0，HTTP1.1 使用 Cache-Control 代替）</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Last-Modified\">Last-Modified</a></td>\n<td align=\"left\"><code>Sun, 16 Oct 2016 05:43:02 GMT</code></td>\n<td align=\"left\">告知浏览器当前资源的最后修改时间</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/If-Modified-Since\">If-Modified-Since</a></td>\n<td align=\"left\"><code>Sun, 16 Oct 2016 05:43:02 GMT</code></td>\n<td align=\"left\">如果浏览器第一次请求时响应中 Last-Modified 非空，第二次请求同一资源时，会把它作为该项的值发给服务器</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag\">ETag</a></td>\n<td align=\"left\"><code>&quot;33a64df551425fcc55e4d42a148795d9f25f89d4&quot;</code> <code>W/&quot;0815&quot;</code></td>\n<td align=\"left\">告知浏览器当前资源在服务器的唯一标识符（生成规则有服务器决定）</td>\n</tr>\n<tr>\n<td align=\"left\">请求</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/If-None-Match\">If-None-Match</a></td>\n<td align=\"left\"><code>&quot;bfc13a64729c4290ef5b2c2730249c88ca92d82d&quot;</code> <code>W/&quot;67ab43&quot;, &quot;54ed21&quot;, &quot;7892dd&quot;</code></td>\n<td align=\"left\">如果浏览器第一次请求时响应中 Etag 非空，第二次请求同一资源时，会把它作为该项的值发给服务器</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Vary\">Vary</a></td>\n<td align=\"left\"><code>Accept-Encoding</code></td>\n<td align=\"left\">辅助从多个缓存副本中筛选合适的版本（不同压缩算法产生的副本）</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Age\">Age</a></td>\n<td align=\"left\"><code>24</code></td>\n<td align=\"left\">表示对象在代理缓存中停留的时间，以秒为单位</td>\n</tr>\n<tr>\n<td align=\"left\">响应</td>\n<td align=\"left\"><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Clear-Site-Data\">Clear-Site-Data</a></td>\n<td align=\"left\"><code>&quot;cache&quot;, &quot;cookies&quot;, &quot;storage&quot;, &quot;executionContexts&quot;</code> <code>&quot;*&quot;</code></td>\n<td align=\"left\">清除当前请求网站有关的浏览器数据</td>\n</tr>\n</tbody></table>\n<blockquote>\n<ul>\n<li><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/toUTCString\">UTCString</a> <code>Mon, 03 Jul 2006 21:44:38 GMT</code>（RFC 1123）</li>\n<li><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString\">ISOString</a> <code>1970-01-01T00:00:00.000Z</code>（ISO 8601）</li>\n</ul>\n</blockquote>\n<pre><code class=\"language-mermaid\">graph LR\n  A(是否可以被重用) --&gt; B(no)\n  A --&gt; C(yes)\n  C --&gt; D(每次请求是否量新验证)\n  D --&gt; E(yes)\n  D --&gt; F(no)\n  F --&gt; G(no-store)\n  E --&gt; H(no-cache)\n  H --&gt; I(yes)\n  H --&gt; J(no)\n  J --&gt; K(是否允许被中介缓存)\n  K --&gt; L(Public)\n  K --&gt; M(Private)\n  L --&gt; N(max-age)\n  M --&gt; O(添加 ETag)\n  O --&gt; P(最长的缓存时间是多少秒)\n</code></pre>\n<h1>缓存流程</h1>\n<pre><code class=\"language-mermaid\">graph TB\nA(用户请求资源) --&gt; B(是否存在缓存)\nB --&gt; C(判断缓存是否过期)\nC --&gt; D(直接使用缓存)\nD --&gt; E(返回展示资源)\nB --&gt; F(向服务器请求)\nF --&gt; G(判断ETag)\nG --&gt; H(向服务器请求)\nG --&gt; I(服务端返回304,读取本地缓存)\nH --&gt; J(判断Last-Modified)\nJ --&gt; K(向服务器请求)\nJ --&gt; L(服务端返回304,读取本地缓存)\nK --&gt; M(服务器返回200还是304)\nM --&gt; N(有更新200,向服务器请求)\nM --&gt; O(304无更新)\nN --&gt; P(请求响应,缓存协商)\nP --&gt; E\nL --&gt; E\nO --&gt; E\n</code></pre>\n<h1>用户操作</h1>\n<blockquote>\n<p>此行为目前尚未在 HTML 标准中规范，仅供参考</p>\n</blockquote>\n<table>\n<thead>\n<tr>\n<th>用户操作</th>\n<th>Expires/Cache-Control</th>\n<th>Last-Modified/Etag</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>地址栏回车</td>\n<td>有效</td>\n<td>有效</td>\n</tr>\n<tr>\n<td>页面链接跳转</td>\n<td>有效</td>\n<td>有效</td>\n</tr>\n<tr>\n<td>新开窗口</td>\n<td>有效</td>\n<td>有效</td>\n</tr>\n<tr>\n<td>前进后退</td>\n<td>有效</td>\n<td>有效</td>\n</tr>\n<tr>\n<td>F5刷新（刷新按钮）</td>\n<td>无效</td>\n<td>有效</td>\n</tr>\n<tr>\n<td>Ctrl+F5强制刷新</td>\n<td>无效</td>\n<td>无效</td>\n</tr>\n</tbody></table>\n<p>刷新请求也会被当前激活的 ServiceWorker 捕获；强制刷新或重新打开标签页则不会</p>\n<h1>预请求/加载</h1>\n<blockquote>\n<p><code>memory cache</code> <code>disk cache</code> <code>service worker cache</code> <code>push cache</code><br><a href=\"https://web.dev/learn/performance/resource-hints\">https://web.dev/learn/performance/resource-hints</a><br><a href=\"https://web.dev/learn/performance/prefetching-prerendering-precaching\">https://web.dev/learn/performance/prefetching-prerendering-precaching</a><br><a href=\"https://developer.mozilla.org/zh-CN/docs/Glossary/Prefetch\">https://developer.mozilla.org/zh-CN/docs/Glossary/Prefetch</a><br><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/Performance/Guides/Speculative_loading\">https://developer.mozilla.org/zh-CN/docs/Web/Performance/Guides/Speculative_loading</a><br><a href=\"https://developer.chrome.google.cn/docs/web-platform/prerender-pages\">https://developer.chrome.google.cn/docs/web-platform/prerender-pages</a></p>\n</blockquote>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/dns-prefetch\">预解析 DNS</a></p>\n<pre><code class=\"language-html\">&lt;link rel=&quot;dns-prefetch&quot; href=&quot;https://fonts.googleapis.com&quot; /&gt;\n&lt;link rel=&quot;dns-prefetch&quot; href=&quot;https://fonts.gstatic.com&quot; /&gt;\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes/rel/preconnect\">预先与源建立连接</a></p>\n<blockquote>\n<p>crossorigin 属性必须对跨源请求设置，否则不会重复利用连接。</p>\n</blockquote>\n<pre><code class=\"language-html\">&lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.googleapis.com&quot; /&gt;\n&lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.gstatic.com&quot; crossorigin /&gt;\n</code></pre>\n<p>或使用<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Link\">Link响应头</a>或<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Status/103\">103状态码</a></p>\n<pre><code>Link: &lt;https://example.com&gt;; rel=&quot;preconnect&quot;\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes/rel/preload\">预请求当前网页所需资源</a></p>\n<pre><code class=\"language-html\">&lt;!-- prettier-ignore --&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;/lcp-image.jpg&quot; as=&quot;image&quot; /&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;style.css&quot; as=&quot;style&quot; /&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;main.js&quot; as=&quot;script&quot; /&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;bg-narrow.png&quot; as=&quot;image&quot; media=&quot;(max-width: 600px)&quot; /&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;bg-wide.png&quot; as=&quot;image&quot; media=&quot;(min-width: 601px)&quot; /&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;sintel-short.mp4&quot; as=&quot;video&quot; type=&quot;video/mp4&quot; /&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;/font.woff2&quot; as=&quot;font&quot; type=&quot;font/woff2&quot; crossorigin /&gt;\n&lt;link rel=&quot;preload&quot; href=&quot;/data.json&quot; as=&quot;fetch&quot; crossorigin /&gt;\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload\">模块预加载</a></p>\n<pre><code class=\"language-html\">&lt;link rel=&quot;modulepreload&quot; href=&quot;script.mjs&quot; /&gt;\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/zh-CN/docs/Glossary/Prefetch\">预请求未来导航的资源（文档预取）</a></p>\n<blockquote>\n<p>注：<code>&lt;link rel=&quot;prerender&quot;&gt;</code> 已弃用</p>\n</blockquote>\n<pre><code class=\"language-html\">&lt;link rel=&quot;prefetch&quot; href=&quot;/next-page.css&quot; as=&quot;style&quot; /&gt;\n</code></pre>\n<p>或使用<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/Speculation_Rules_API\">推测规则 API</a></p>\n<pre><code class=\"language-html\">&lt;script type=&quot;speculationrules&quot;&gt;\n    {\n        &quot;prerender&quot;: [\n            {\n                &quot;where&quot;: {\n                    &quot;and&quot;: [\n                        { &quot;href_matches&quot;: &quot;/*&quot; },\n                        { &quot;not&quot;: { &quot;href_matches&quot;: &quot;/logout&quot; } },\n                        { &quot;not&quot;: { &quot;href_matches&quot;: &quot;/*\\\\?*(^|&amp;)add-to-cart=*&quot; } },\n                        { &quot;not&quot;: { &quot;selector_matches&quot;: &quot;.no-prerender&quot; } },\n                        { &quot;not&quot;: { &quot;selector_matches&quot;: &quot;[rel~=nofollow]&quot; } }\n                    ]\n                }\n            }\n        ],\n        &quot;prefetch&quot;: [\n            {\n                &quot;urls&quot;: [&quot;next.html&quot;, &quot;next2.html&quot;],\n                &quot;requires&quot;: [&quot;anonymous-client-ip-when-cross-origin&quot;],\n                &quot;referrer_policy&quot;: &quot;no-referrer&quot;\n            }\n        ]\n    }\n&lt;/script&gt;\n</code></pre>\n<p>HTTP/2 服务器推送</p>\n<pre><code class=\"language-js\">import http2 from &quot;node:http2&quot;;\nimport fs from &quot;node:fs/promises&quot;;\nimport path from &quot;node:path&quot;;\nimport { fileURLToPath } from &quot;node:url&quot;;\n\nconst { HTTP2_HEADER_PATH, HTTP2_HEADER_METHOD, HTTP2_HEADER_STATUS, HTTP_STATUS_OK, HTTP_STATUS_NOT_FOUND } =\n    http2.constants;\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst server = http2.createSecureServer({\n    key: fs.readFile(path.join(__dirname, &quot;server.key&quot;)),\n    cert: fs.readFile(path.join(__dirname, &quot;server.crt&quot;)),\n});\n\nserver.on(\n    &quot;stream&quot;,\n    async (\n        /** https://docs.deno.com/api/node/http2/~/ServerHttp2Stream */\n        stream,\n        headers,\n    ) =&gt; {\n        const reqPath = headers[HTTP2_HEADER_PATH];\n        const reqMethod = headers[HTTP2_HEADER_METHOD];\n\n        const [html, css, js] = await Promise.all([\n            fs.readFile(path.join(__dirname, &quot;index.html&quot;)),\n            fs.readFile(path.join(__dirname, &quot;style.css&quot;)),\n            fs.readFile(path.join(__dirname, &quot;script.js&quot;)),\n        ]);\n\n        if (reqMethod === &quot;GET&quot; &amp;&amp; reqPath === &quot;/&quot;) {\n            if (stream.pushAllowed) {\n                // 推送 style.css\n                stream.pushStream({ [HTTP2_HEADER_PATH]: &quot;/style.css&quot; }, (err, pushStream) =&gt; {\n                    pushStream.respond({\n                        [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,\n                        &quot;content-type&quot;: &quot;text/css&quot;,\n                    });\n                    pushStream.end(css);\n                });\n\n                // 推送 script.js\n                stream.pushStream({ [HTTP2_HEADER_PATH]: &quot;/script.js&quot; }, (err, pushStream) =&gt; {\n                    pushStream.respond({\n                        [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,\n                        &quot;content-type&quot;: &quot;application/javascript&quot;,\n                    });\n                    pushStream.end(js);\n                });\n            }\n\n            // 响应 index.html\n            stream.respond({\n                [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,\n                &quot;content-type&quot;: &quot;text/html&quot;,\n            });\n            stream.end(html);\n        } else {\n            switch (reqPath) {\n                // 响应直接请求\n                case &quot;/style.css&quot;:\n                    stream.respond({\n                        [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,\n                        &quot;content-type&quot;: &quot;text/css&quot;,\n                    });\n                    stream.end(css);\n                    break;\n                case &quot;/script.js&quot;:\n                    stream.respond({\n                        [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,\n                        &quot;content-type&quot;: &quot;application/javascript&quot;,\n                    });\n                    stream.end(js);\n                    break;\n                // 404\n                default:\n                    stream.respond({ [HTTP2_HEADER_STATUS]: HTTP_STATUS_NOT_FOUND });\n                    stream.end();\n                    break;\n            }\n        }\n    },\n);\n\nserver.listen(8443, () =&gt; {\n    console.log(&quot;HTTP/2 server listening on port 8443&quot;);\n});\n</code></pre>\n<h1>Service Worker</h1>\n<p>navigation.serviceWorker : interface <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer\">ServiceWorkerContainer</a></p>\n<pre><code class=\"language-html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=&quot;en&quot;&gt;\n    &lt;head&gt;\n        &lt;meta charset=&quot;UTF-8&quot; /&gt;\n        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;\n    &lt;/head&gt;\n    &lt;body&gt;\n        &lt;script type=&quot;module&quot;&gt;\n            const { serviceWorker } = navigator;\n\n            serviceWorker.register(&quot;/sw.js&quot;, { scope: &quot;/&quot; }).then((registration) =&gt; {\n                console.log(&quot;register&quot;, { registration });\n            });\n\n            serviceWorker.getRegistrations().then((registrations) =&gt; {\n                console.log(&quot;getRegistrations&quot;, { registrations });\n            });\n\n            if (serviceWorker.controller) {\n                console.log(&quot;controller&quot;, serviceWorker.controller);\n            }\n\n            serviceWorker.addEventListener(&quot;controllerchange&quot;, () =&gt; {\n                console.log(&quot;controllerchange&quot;, serviceWorker.controller);\n            });\n\n            serviceWorker.addEventListener(&quot;message&quot;, (event) =&gt; {\n                console.log(&quot;message&quot;, event.data);\n            });\n\n            serviceWorker.ready.then((registration) =&gt; {\n                console.log(&quot;ready&quot;, { registration });\n                registration.active.postMessage(&quot;Hello from the page!&quot;);\n            });\n        &lt;/script&gt;\n    &lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker\">ServiceWorker</a>&#39;s global is <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope\">ServiceWorkerGlobalScope</a></p>\n<pre><code class=\"language-js\">console.log(&quot;ServiceWorker&quot;, serviceWorker);\n\nconst addResourcesToCache = async (resources) =&gt; {\n    const cache = await caches.open(&quot;v1&quot;);\n    await cache.addAll(resources);\n};\n\nself.addEventListener(&quot;install&quot;, (event) =&gt; {\n    event.waitUntil(addResourcesToCache([&quot;/&quot;, &quot;/index.html&quot;, new Request(&quot;/style.css&quot;)]));\n});\n\nself.addEventListener(&quot;fetch&quot;, (event) =&gt; {\n    event.respondWith(caches.match(event.request));\n});\n</code></pre>\n<p><a href=\"https://developer.chrome.com/docs/workbox/service-worker-overview\">ServiceWorker</a> 相当于网络中间人，出于安全相比于普通 Worker 有更多限制：</p>\n<ul>\n<li>必须通过同源http脚本创建，<a href=\"https://github.com/w3c/ServiceWorker/issues/578\">不能使用 blob url</a></li>\n<li>register 默认 scope 为 ServiceWorker 脚本所在目录，且 scope 只能在此基础上具体化。（<a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Service-Worker-Allowed\">Service-Worker-Allowed</a>）</li>\n</ul>\n","tags":["web-api","http"]},{"id":"web-csp","url":"https://yieldray.fun/posts/web-csp","title":"Content Security Policy (CSP)","date_published":"2024-03-29T19:19:19.000Z","date_modified":"2024-03-29T19:19:19.000Z","content_text":"<h1>前言</h1>\n<blockquote>\n<p>TL;DR:<br>简而言之，<a href=\"https://developer.mozilla.org/docs/web/http/csp\">内容安全策略（CSP）</a>类似于白名单机制，限制当前网页中允许执行的脚本及样式。<br>通过 HTTP 标头指定，用于缓解<a href=\"https://developer.mozilla.org/docs/Glossary/Cross-site_scripting\">跨站脚本攻击（XSS）</a>（显然对 SQL 注入无能为力）</p>\n</blockquote>\n<p>现代 Web 应用的工作方式往往是从后端数据库获取数据，并渲染数据到 HTML 模板。<br>攻击者可能通过向表单提交一段构造的 HTML 字符串，最终导致恶意脚本在浏览器中执行。</p>\n<p>在 CSP 之前，通常的方法是转义（Escape） HTML 字符</p>\n<pre><code class=\"language-js\">const html = str\n    .replace(/&amp;/g, &quot;&amp;amp;&quot;)\n    .replace(/&lt;/g, &quot;&amp;lt;&quot;)\n    .replace(/&gt;/g, &quot;&amp;gt;&quot;)\n    .replace(/&quot;/g, &quot;&amp;quot;&quot;)\n    .replace(/&#39;/g, &quot;&amp;#039;&quot;);\n</code></pre>\n<p>或 <a href=\"https://github.com/cure53/DOMPurify\">DOMPurify</a>。此外， HTML 标准甚至已规定 <a href=\"https://developer.mozilla.org/docs/Web/API/Sanitizer\">Web Sanitizer API</a>（<a href=\"https://web.dev/articles/sanitizer\">另见</a>）</p>\n<pre><code class=\"language-js\">const $div = document.querySelector(&quot;div&quot;);\nconst user_input = `&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot; onerror=alert(0)&gt;`;\n$div.setHTML(user_input, { sanitizer: new Sanitizer() }); // &lt;div&gt;&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot;&gt;&lt;/div&gt; // [!code highlight]\n</code></pre>\n<p>仅作文本渲染时，则可以使用 <a href=\"https://developer.mozilla.org/docs/Web/API/Node/textContent\"><code>Node</code> 接口的 <code>textContent</code> 属性</a> 设置文本内容</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy\">Content Security Policy</a></h1>\n<p><a href=\"https://www.w3.org/TR/CSP/\">CSP</a> <a href=\"https://www.w3.org/TR/CSP/#csp-header\">响应头</a>包括 Content-Security-Policy 和 Content-Security-Policy-Report-Only。</p>\n<ul>\n<li><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy\">Content-Security-Policy</a> 指明用户代理能够为指定的页面加载哪些资源，以及页面能够 fetch 哪些资源（当然，也遵循<a href=\"https://developer.mozilla.org/docs/Web/Security/Same-origin_policy\">同源策略</a>）。</li>\n<li><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only\">Content-Security-Policy-Report-Only</a> 如其名，仅报告，可对 Content-Security-Policy 进行实验和渐进式应用。</li>\n</ul>\n<blockquote>\n<p>生产环境当然需要考虑浏览器兼容性，和渐进式应用的策略。<br>此处仅根据 CSP Level3 进行说明，略过 Content-Security-Policy-Report-Only。</p>\n</blockquote>\n<p>形式化语法</p>\n<pre><code>Content-Security-Policy = serialized-policy-list\n\nserialized-policy-list = 1#serialized-policy\n\nserialized-policy =\n    serialized-directive *( optional-ascii-whitespace &quot;;&quot; [ optional-ascii-whitespace serialized-directive ] )\n\nserialized-directive = directive-name [ required-ascii-whitespace directive-value ] // [!code highlight]\ndirective-name       = 1*( ALPHA / DIGIT / &quot;-&quot; )\ndirective-value      = *( required-ascii-whitespace / ( %x21-%x2B / %x2D-%x3A / %x3C-%x7E ) )\n                       ; Directive values may contain whitespace and VCHAR characters,\n                       ; excluding &quot;;&quot; and &quot;,&quot;. The second half of the definition\n                       ; above represents all VCHAR characters (%x21-%x7E)\n                       ; without &quot;;&quot; and &quot;,&quot; (%x3B and %x2C respectively)\n</code></pre>\n<p>可以看出，CSP 策略是一个指令列表，每个<a href=\"https://www.w3.org/TR/CSP/#csp-%E6%8C%87%E4%BB%A4\">指令</a>由名称和可选的值构成，通过分号分隔。</p>\n<p>具体指令可参见 <a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy#directives\">MDN</a> 或 <a href=\"https://www.w3.org/TR/CSP/#csp-directives\">规范</a></p>\n<p>举例（注意指令值的格式）：</p>\n<pre><code>Content-Security-Policy: script-src https://safe-external-site.com *.trusted.com; default-src &#39;self&#39;\n</code></pre>\n<p>或通过 <code>&lt;meta http-equiv&gt;</code>（不支持 Report 指令）：</p>\n<pre><code class=\"language-html\">&lt;meta\n    http-equiv=&quot;Content-Security-Policy&quot;\n    content=&quot;script-src https://safe-external-site.com *.trusted.com; default-src &#39;self&#39;&quot;\n/&gt;\n</code></pre>\n<blockquote>\n<p>正如上文所述，CSP 类似于<strong>白名单</strong>，指令未声明的资源则不被允许加载。<br>因此 <code>default-src</code> 指令非常有用</p>\n</blockquote>\n<p><code>&#39;unsafe-inline&#39;</code> 值允许内联资源（此处即内联 <code>&lt;script&gt;</code> 脚本），但这也给 XSS 留下可乘之机。</p>\n<pre><code>Content-Security-Policy: script-src &#39;self&#39; &#39;unsafe-inline&#39; my.trusted.domain\n</code></pre>\n<p>因此应避免使用 <code>&#39;unsafe-*&#39;</code> 值，可使用特殊的 <code>&#39;nonce-*&#39;</code> 值<br>该值可由工具生成，例如 <a href=\"https://webpack.js.org/guides/csp/\">Webpack</a></p>\n<pre><code>Content-Security-Policy: script-src &#39;nonce-dGhpcyBpcyBhIG5v==&#39;; style-src &#39;nonce-dGhpcyBpcyBhIG5v==&#39;\n</code></pre>\n<p>只有对应元素允许被执行</p>\n<pre><code class=\"language-html\">&lt;script nonce=&quot;dGhpcyBpcyBhIG5v==&quot;&gt;\n    ...\n&lt;/script&gt;\n\n&lt;style nonce=&quot;dGhpcyBpcyBhIG5v==&quot;&gt;\n    ...\n&lt;/style&gt;\n</code></pre>\n","tags":["web-api","http"]},{"id":"clang-flexible-array-member","url":"https://yieldray.fun/posts/clang-flexible-array-member","title":"C语言 灵活数组成员","date_published":"2024-03-22T16:30:00.000Z","date_modified":"2024-03-22T16:30:00.000Z","content_text":"<blockquote>\n<p>摘自 C99 §6.7.2.1, item 16：<a href=\"https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf\">https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf</a></p>\n</blockquote>\n<p>As a special case, the last element of a structure with more than one named member may<br>have an incomplete array type; this is called a <em>flexible array member</em>. In most situations,<br>the <em>flexible array member</em> is ignored. In particular, the size of the structure is as if the<br><em>flexible array member</em> were omitted except that it may have more trailing padding than<br>the omission would imply. However, when a <code>.</code> (or <code>-&gt;</code>) operator has a left operand that is<br>(a pointer to) a structure with a <em>flexible array member</em> and the right operand names that<br>member, it behaves as if that member were replaced with the longest array (with the same<br>element type) that would not make the structure larger than the object being accessed; the<br>offset of the array shall remain that of the <em>flexible array member</em>, even if this would differ<br>from that of the replacement array. If this array would have no elements, it behaves as if<br>it had one element but the behavior is undefined if any attempt is made to access that<br>element or to generate a pointer one past it.</p>\n<hr>\n<p>根据规范，一个拥有至少两个命名成员的结构体，允许其最后一个成员是一个数组，且没有写出长度（或者长度是 0，因此<em>灵活数组成员</em>也称<em>零长度数组成员</em>）</p>\n<p>当然，还有可能有一些字节对齐的情况，此处省略，具体参见规范。</p>\n<p>下面的示例实现了在堆上创建指定长度的数组（这有点类似与在栈上创建的<em>变长数组</em>）<br>此方法实现了数组的长度和数据都分配到一块连续内存，效果优于定义一个指针指向数据。</p>\n<pre><code class=\"language-c\">// preview: https://ideone.com/hWjI9C\n#include &lt;stdio.h&gt;\n#include &lt;stdlib.h&gt;\n\ntypedef struct\n{\n    int len;\n    double array[]; // 注意这与定义一个指针的不同之处\n} Vectord;\n\nVectord *newVectord(int len)\n{\n    Vectord *vec = (Vectord *)malloc(sizeof(int) + sizeof(double) * len);\n    vec-&gt;len = len;\n    for (int i = 0; i &lt; len; i++)\n        vec-&gt;array[i] = 0;\n    return vec;\n}\n\nvoid printVectord(Vectord *vec)\n{\n    for (int i = 0; i &lt; vec-&gt;len; i++)\n        printf(&quot;%f &quot;, vec-&gt;array[i]);\n    puts(&quot;&quot;);\n}\n\nint main()\n{\n    Vectord *vec = newVectord(3);\n    printVectord(vec); // -&gt; 0.000000 0.000000 0.000000\n\n    vec-&gt;array[0] = 2;\n    vec-&gt;array[1] = 3;\n    vec-&gt;array[2] = 3;\n\n    printVectord(vec); // -&gt; 2.000000 3.000000 3.000000\n}\n</code></pre>\n<p>要实现能获取数组长度，且在内存上连续，还有一种弯弯绕绕的方法就是类似于 C语言的 <code>\\0</code> 结尾字符串。<br>不过这样显然在很多情况下都不可取，因此需具体问题具体分析。</p>\n<hr>\n<p>参见：<br><a href=\"https://en.wikipedia.org/wiki/Flexible_array_member\">https://en.wikipedia.org/wiki/Flexible_array_member</a></p>\n<p>另见：<br><a href=\"https://en.wikipedia.org/wiki/Variable-length_array\">https://en.wikipedia.org/wiki/Variable-length_array</a></p>\n","tags":["c"]},{"id":"clang-struct-align","url":"https://yieldray.fun/posts/clang-struct-align","title":"C语言 结构体对齐","date_published":"2024-03-21T15:00:00.000Z","date_modified":"2024-03-21T15:00:00.000Z","content_text":"<blockquote>\n<p>摘自 C99 §6.7.2.1, item 13：<a href=\"https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf\">https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf</a></p>\n</blockquote>\n<p>Within a structure object, the non-bit-field members and the units in which bit-fields<br>reside have addresses that increase in the order in which they are declared. A pointer to a<br>structure object, suitably converted, points to its initial member (or if that member is a<br>bit-field, then to the unit in which it resides), and vice versa. There may be unnamed<br>padding within a structure object, but not at its beginning.</p>\n<hr>\n<p>根据规范，结构体的第一个字段保证对齐这段内存的第一个字节。（当然，后面的字段就可能发生字节填充）</p>\n<p>因此：可以接受一个指向结构体的指针，并安全地将其转换为指向其第一个字段的指针，反之亦然。</p>\n<p>这有点像继承：下面的例子中，结构体 A 和 B 的第一个字段是结构体 Base，<br>因此保证可以安全地将 <code>A*</code> 或 <code>B*</code> “向下转换”为 <code>Base*</code></p>\n<pre><code class=\"language-c\">// preview: https://ideone.com/rro44a\n#include &lt;stdio.h&gt;\n#include &lt;inttypes.h&gt;\n#include &lt;stdlib.h&gt;\n\ntypedef enum\n{\n    a = &#39;a&#39;,\n    b = &#39;b&#39;,\n} BaseType;\n\ntypedef struct Base\n{\n    BaseType type;\n} Base;\n\ntypedef struct A\n{\n    struct Base base;\n    int a;\n} A;\n\nA *newA(int a)\n{\n    A *p = malloc(sizeof(A));\n    p-&gt;base.type = &#39;a&#39;;\n    p-&gt;a = a;\n    return p;\n}\n\ntypedef struct B\n{\n    struct Base base;\n    int64_t b;\n} B;\n\nB *newB(int64_t b)\n{\n    B *p = malloc(sizeof(B));\n    p-&gt;base.type = &#39;b&#39;;\n    p-&gt;b = b;\n    return p;\n}\n\nint main()\n{\n    A *a = newA(123);\n    B *b = newB(456);\n\n    Base *base;\n    base = (Base *)a;\n    printf(&quot;%c\\n&quot;, base-&gt;type); // -&gt; a\n\n    base = (Base *)b;\n    printf(&quot;%c\\n&quot;, base-&gt;type); // -&gt; b\n}\n</code></pre>\n<p>另见：<br><a href=\"https://stackoverflow.com/questions/53578631\">https://stackoverflow.com/questions/53578631</a><br><a href=\"https://en.wikipedia.org/wiki/Type_punning\">https://en.wikipedia.org/wiki/Type_punning</a><br><a href=\"https://hackmd.io/@sysprog/c-memory\">https://hackmd.io/@sysprog/c-memory</a></p>\n","tags":["c"]},{"id":"understanding-ecmascript-part-4","url":"https://yieldray.fun/posts/understanding-ecmascript-part-4","title":"理解ECMAScript规范（Part 4）","date_published":"2024-03-10T10:00:00.000Z","date_modified":"2024-03-10T10:00:00.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://v8.dev/blog/understanding-ecmascript-part-4\">https://v8.dev/blog/understanding-ecmascript-part-4</a></p>\n</blockquote>\n<h1>网络上的其它文章</h1>\n<p>来自 Mozilla 的 <a href=\"https://github.com/jorendorff\">Jason Orendorff</a> 发表了 <a href=\"https://github.com/mozilla-spidermonkey/jsparagus/blob/master/js-quirks.md#readme\">一篇深入分析 JS 怪异语法的精彩文章</a>。虽然实现细节有所不同，但每个 JS 引擎都会在这些怪异上遇到相同的问题。</p>\n<h1>覆盖语法</h1>\n<p>在本章节中，我们将更深入地了解 <em>覆盖语法</em>。它们是一种指定语法结构的语法的方法，乍一看这些语法结构似乎有歧义。</p>\n<p>同样，为了简洁，这里将跳过 <code>[In, Yield, Await]</code> 中的下标，因为它们对于本文不重要。<br>有关其含义和用法的解释，请参阅 <a href=\"/posts/understanding-ecmascript-part-3\">第 3 部分</a>。</p>\n<h1>有限前瞻</h1>\n<p>通常，解析器根据有限前瞻（固定数量的后续标记）来决定使用哪个生成。</p>\n<p>在某些情况下，下一个标记会明确确定要使用的生成。<a href=\"https://tc39.es/ecma262/#prod-UpdateExpression\">例如</a>：</p>\n<pre><code>UpdateExpression :\n  LeftHandSideExpression\n  LeftHandSideExpression ++\n  LeftHandSideExpression --\n  ++ UnaryExpression\n  -- UnaryExpression\n</code></pre>\n<p>如果我们正在解析一个 <code>UpdateExpression</code> 和下一个标记是 <code>++</code> 或 <code>--</code>，我们马上就知道要使用的生成。<br>如果下一个标记不是，也不算坏：我们可以从我们所在的位置开始解析一个 <code>LeftHandSideExpression</code>，并在解析完它之后弄清楚接下来该怎么做。</p>\n<p>如果 <code>LeftHandSideExpression</code> 后的标记是 <code>++</code>，那么要使用的生成就是 <code>UpdateExpression: LeftHandSideExpression ++</code>。对于 <code>--</code> 的情况也是类似的。<br>并且，如果 <code>LeftHandSideExpression</code> 后的标记既不是 <code>++</code> 也不是 <code>--</code>，那么我们使用生成 <code>UpdateExpression : LeftHandSideExpression</code>。</p>\n<h2>箭头函数参数列表还是带圆括号的表达式？</h2>\n<p>区分箭头函数参数列表和带圆括号的表达式更复杂。</p>\n<p>例如：</p>\n<pre><code>let x = (a,\n</code></pre>\n<p>这是否是箭头函数的开头，就像这样？</p>\n<pre><code>let x = (a, b) =&gt; { return a + b };\n</code></pre>\n<p>或者，它可能是带圆括号的表达式，就像这样？</p>\n<pre><code>let x = (a, 3);\n</code></pre>\n<p>带圆括号的任何东西都可以任意长 - 我们无法根据有限数量的标记知道它的内容。</p>\n<p>让我们想象一下，我们有以下简单的生成：</p>\n<pre><code>AssignmentExpression :\n  ...\n  ArrowFunction\n  ParenthesizedExpression\n\nArrowFunction :\n  ArrowParameterList =&gt; ConciseBody\n</code></pre>\n<p>现在我们无法使用有限前瞻来选择要使用的生成。如果我们必须解析一个 <code>AssignmentExpression</code> 且下一个标记是 <code>(</code>，我们如何决定接下来解析什么？<br>我们可以解析一个 <code>ArrowParameterList</code> 也可以解析一个 <code>ParenthesizedExpression</code>，但我们的猜测可能会出错。</p>\n<h2>非常宽松的新符号：<code>CPEAAPL</code></h2>\n<p>规范通过引入符号 <code>CoverParenthesizedExpressionAndArrowParameterList</code>（覆盖带括号的表达式和箭头参数列表，简称 <code>CPEAAPL</code>）解决了这个问题。<code>CPEAAPL</code> 实际上是一个 <code>ParenthesizedExpression</code> 或一个 <code>ArrowParameterList</code>，但我们现在还不知道哪一个。</p>\n<p><code>CPEAAPL</code> 的 <a href=\"https://tc39.es/ecma262/#prod-CoverParenthesizedExpressionAndArrowParameterList\">生成</a> 非常宽松，允许所有可以在 <code>ParenthesizedExpression</code> 和 <code>ArrowParameterList</code> 中出现的结构：</p>\n<pre><code>CPEAAPL :\n  ( Expression )\n  ( Expression , )\n  ( )\n  ( ... BindingIdentifier )\n  ( ... BindingPattern )\n  ( Expression , ... BindingIdentifier )\n  ( Expression , ... BindingPattern )\n</code></pre>\n<p>例如，以下表达式是有效的 <code>CPEAAPL</code>：</p>\n<pre><code>// 有效的 ParenthesizedExpression 和 ArrowParameterList：\n(a, b)\n(a, b = 1)\n\n// 有效的 ParenthesizedExpression：\n(1, 2, 3)\n(function foo() { })\n\n// 有效的 ArrowParameterList：\n()\n(a, b,)\n(a, ...b)\n(a = 1, ...b)\n\n// 既无效也不有效，但仍然是 CPEAAPL：\n(1, ...b)\n(1, )\n</code></pre>\n<p>尾随逗号和 <code>...</code> 只能出现在 <code>ArrowParameterList</code> 中。某些结构，如 <code>b = 1</code> 可以在两者中出现，但它们的含义不同：<br>在 <code>ParenthesizedExpression</code> 内是一个赋值，在 <code>ArrowParameterList</code> 内是一个有默认值的参数。<br>不是有效参数名称（或参数解构模式）的数字和其他 <code>PrimaryExpressions</code> 只能出现在 <code>ParenthesizedExpression</code> 中。<br>但它们都可以在 <code>CPEAAPL</code> 中出现。</p>\n<h2>在生成中使用 <code>CPEAAPL</code></h2>\n<p>现在，我们可以在 <a href=\"https://tc39.es/ecma262/#prod-AssignmentExpression\"><code>AssignmentExpression</code> 生成</a> 中使用非常宽松的 <code>CPEAAPL</code>。<br>（注意：<code>ConditionalExpression</code> 通过一个这里未显示的长生成链导致 <code>PrimaryExpression</code>。）</p>\n<pre><code>AssignmentExpression :\n  ConditionalExpression\n  ArrowFunction\n  ...\n\nArrowFunction :\n  ArrowParameters =&gt; ConciseBody\n\nArrowParameters :\n  BindingIdentifier\n  CPEAAPL\n\nPrimaryExpression :\n  ...\n  CPEAAPL\n</code></pre>\n<p>试想我们再次遇到这种情况，我们需要解析一个 <code>AssignmentExpression</code> 且下一个标记是 <code>(</code>。<br>现在，我们可以解析一个 <code>CPEAAPL</code>，并在稍后弄清楚要使用哪个生成。<br>无论我们要解析一个 <code>ArrowFunction</code> 还是一个 <code>ConditionalExpression</code> 都没关系，无论如何，下一个要解析的符号都在 <code>CPEAAPL</code> 中！</p>\n<p>在解析完 <code>CPEAAPL</code> 之后，我们可以决定针对原始 <code>AssignmentExpression</code>（包含 <code>CPEAAPL</code> 的那个）使用哪个生成。该决定是根据 <code>CPEAAPL</code> 后的标记做出的。</p>\n<p>如果标记是 <code>=&gt;</code>，我们使用生成：</p>\n<pre><code>AssignmentExpression :\n  ArrowFunction\n</code></pre>\n<p>如果标记是其他东西，我们使用生成：</p>\n<pre><code>AssignmentExpression :\n  ConditionalExpression\n</code></pre>\n<p>例如：</p>\n<pre><code>let x = (a, b) =&gt; { return a + b; };\n//      ^^^^^^\n//     CPEAAPL\n//             ^^\n//             CPEAAPL 后的标记\n\nlet x = (a, 3);\n//      ^^^^^^\n//     CPEAAPL\n//            ^\n//            CPEAAPL 后的标记\n</code></pre>\n<p>此时，我们可以将 <code>CPEAAPL</code> 保留原样，并继续解析程序的其余部分。<br>例如，如果 <code>CPEAAPL</code> 位于 <code>ArrowFunction</code> 中，我们暂时不必查看它是否是有效的箭头函数参数列表 - 稍后可以完成此操作。<br>（现实中的解析器可能会选择立即执行有效性检查，但从规范的角度来看，我们无需这样做。）</p>\n<h2>限制 CPEAAPL</h2>\n<p>正如我们之前看到的，<code>CPEAAPL</code> 的语法生成非常宽松，并允许无效的结构（例如 <code>(1, ...a)</code>）。<br>在根据语法解析完程序之后，我们需要禁止相应的非法结构。</p>\n<p>规范通过添加以下限制来实现此目的：</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#sec-grouping-operator-static-semantics-early-errors\">静态语义：早期错误</a></p>\n<p><code>PrimaryExpression : CPEAAPL</code></p>\n<p>如果 <code>CPEAAPL</code> 没有覆盖 <code>ParenthesizedExpression</code>，则这是语法错误。</p>\n</blockquote>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#sec-primary-expression\">补充语法</a></p>\n<p>在处理生成</p>\n<p><code>PrimaryExpression : CPEAAPL</code></p>\n<p>的实例时，使用以下语法来精炼（refine）<code>CPEAAPL</code> 的解释：</p>\n<p><code>ParenthesizedExpression : ( Expression )</code></p>\n</blockquote>\n<p>这意味着：如果 <code>CPEAAPL</code> 出现在语法树中 <code>PrimaryExpression</code> 的位置，它实际上是一个 <code>ParenthesizedExpression</code>，并且这是其唯一有效生成。</p>\n<p><code>Expression</code> 永远不能为空，所以 <code>( )</code> 不是有效的 <code>ParenthesizedExpression</code>。<br>像 <code>(1, 2, 3)</code> 这样的逗号分隔列表是由 <a href=\"https://tc39.es/ecma262/#sec-comma-operator\">逗号操作符</a> 创建的：</p>\n<pre><code>Expression :\n  AssignmentExpression\n  Expression , AssignmentExpression\n</code></pre>\n<p>类似地，如果 <code>CPEAAPL</code> 出现 <code>ArrowParameters</code> 的位置，则适用以下限制：</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#sec-arrow-function-definitions-static-semantics-early-errors\">静态语义：早期错误</a></p>\n<p><code>ArrowParameters : CPEAAPL</code></p>\n<p>如果 <code>CPEAAPL</code> 没有覆盖 <code>ArrowFormalParameters</code>，则这是语法错误。</p>\n</blockquote>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#sec-arrow-function-definitions\">补充语法</a></p>\n<p>当识别生成</p>\n<p><code>ArrowParameters</code> : <code>CPEAAPL</code></p>\n<p>时，使用以下语法来精炼（refine）<code>CPEAAPL</code> 的解释：</p>\n<p><code>ArrowFormalParameters :</code><br><code>( UniqueFormalParameters )</code></p>\n</blockquote>\n<h2>其他覆盖语法</h2>\n<p>除了 <code>CPEAAPL</code> 之外，规范还将覆盖语法用于其他看起来模棱两可的结构。</p>\n<p><code>ObjectLiteral</code> 被用作覆盖语法，用于出现在箭头函数参数列表中的 <code>ObjectAssignmentPattern</code>。<br>这意味着 <code>ObjectLiteral</code> 允许不能出现在实际对象文字中的结构。</p>\n<pre><code>ObjectLiteral :\n  ...\n  { PropertyDefinitionList }\n\nPropertyDefinition :\n  ...\n  CoverInitializedName\n\nCoverInitializedName :\n  IdentifierReference Initializer\n\nInitializer :\n  = AssignmentExpression\n</code></pre>\n<p>例如：</p>\n<pre><code>let o = { a = 1 }; // 语法错误\n\n// 具有具有默认值的解构参数的箭头函数：\nlet f = ({ a = 1 }) =&gt; { return a; };\nf({}); // 返回 1\nf({a : 6}); // 返回 6\n</code></pre>\n<p>异步箭头函数在有限前瞻下看起来也很模棱两可：</p>\n<pre><code>let x = async(a,\n</code></pre>\n<p>这是调用名为 <code>async</code> 的函数还是异步箭头函数？</p>\n<pre><code>let x1 = async(a, b);\nlet x2 = async();\nfunction async() { }\n\nlet x3 = async(a, b) =&gt; {};\nlet x4 = async();\n</code></pre>\n<p>为此，语法定义了一个覆盖语法符号 <code>CoverCallExpressionAndAsyncArrowHead</code>，其工作方式与 <code>CPEAAPL</code> 类似。</p>\n<h1>总结</h1>\n<p>在本集中，我们研究了规范如何定义覆盖语法，以及在无法根据有限前瞻识别当前语法结构的情况下如何使用它们。</p>\n<p>具体来说，我们研究了区分箭头函数参数列表与括号表达式以及规范如何通过在解析阶段使用覆盖语法来宽松地解析模棱两可的结构，并在稍后使用静态语义规则对其进行限制。</p>\n","tags":["ecma262"]},{"id":"understanding-ecmascript-part-3","url":"https://yieldray.fun/posts/understanding-ecmascript-part-3","title":"理解ECMAScript规范（Part 3）","date_published":"2024-03-09T10:00:00.000Z","date_modified":"2024-03-09T10:00:00.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://v8.dev/blog/understanding-ecmascript-part-3\">https://v8.dev/blog/understanding-ecmascript-part-3</a></p>\n</blockquote>\n<p>本节中，我们将深入了解 ECMAScript 语言及其语法定义。如果你不熟悉<em>上下文无关文法</em>，现在是查阅基础知识的好时机，因为规范使用<em>上下文无关文法</em>来定义语言。<br>参见 <a href=\"https://craftinginterpreters.com/representing-code.html#context-free-grammars\">《Crafting Interpreters》中有关<em>上下文无关文法</em>的章节</a> 以了解通俗易懂的介绍，或参见 <a href=\"https://en.wikipedia.org/wiki/Context-free_grammar\">维基百科页面</a> 以了解更数学化的定义。</p>\n<h1>ECMAScript 语法</h1>\n<p>ECMAScript 规范定义了四种语法：</p>\n<p><a href=\"https://tc39.es/ecma262/#sec-ecmascript-language-lexical-grammar\">词法语法</a> 描述了如何将 <a href=\"https://en.wikipedia.org/wiki/Unicode#Architecture_and_terminology\">Unicode 代码点</a> 转化为一系列 <strong>输入元素</strong>（标记、行终止符、注释、空白）。</p>\n<p><a href=\"https://tc39.es/ecma262/#sec-syntactic-grammar\">句法语法</a> 定义了语法正确的程序如何由标记组成。</p>\n<p><a href=\"https://tc39.es/ecma262/#sec-patterns\">正则表达式语法</a> 描述了如何将 Unicode 代码点转换为正则表达式。</p>\n<p><a href=\"https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type\">数字字符串语法</a> 描述了如何将字符串转换为数字值。</p>\n<p>每个语法都定义为一个<em>上下文无关文法</em>，由一组产生式组成。</p>\n<p>语法使用稍微不同的表示法：句法语法使用 <code>LeftHandSideSymbol :</code> ，而词法语法和正则表达式语法使用 <code>LeftHandSideSymbol ::</code> ，数字字符串语法使用 <code>LeftHandSideSymbol :::</code> 。</p>\n<p>接下来，我们将更详细地研究<em>词法语法</em>和<em>句法语法</em>。</p>\n<h1>词法语法</h1>\n<p>规范将 ECMAScript 源文本定义为 Unicode 代码点的序列。例如，变量名不仅限于 ASCII 字符，还可以包括其他 Unicode 字符。<br>规范并未讨论实际编码（例如 UTF-8 或 UTF-16）。它假定根据源代码所在的编码，源代码已经转换为 Unicode 代码点序列。</p>\n<p>无法预先标记 ECMAScript 源代码，这使得词法语法的定义变得稍微复杂一些。</p>\n<p>例如，如果不对出现 <code>/</code> 的上下文进行更宏观的观察，我们就无法确定 <code>/</code> 是除法运算符还是正则表达式的开始：</p>\n<pre><code>const x = 10 / 5;\n</code></pre>\n<p>这里，<code>/</code> 是 <code>DivPunctuator</code>。</p>\n<pre><code>const r = /foo/;\n</code></pre>\n<p>这里，第一个 <code>/</code> 是 <code>RegularExpressionLiteral</code> 的开始。</p>\n<p>模板引入了类似的歧义—— <code>}` </code>的解释取决于其出现的上下文：</p>\n<pre><code>const what1 = &#39;temp&#39;;\nconst what2 = &#39;late&#39;;\nconst t = `I am a ${ what1 + what2 }`;\n</code></pre>\n<p>这里，<code>`I am a ${</code> 是 <code>TemplateHead</code> ，而 <code>}`</code> 是 <code>TemplateTail</code> 。</p>\n<pre><code>if (0 == 1) {\n}`not very useful`;\n</code></pre>\n<p>这里，<code>} </code>是 <code>RightBracePunctuator</code> ，而 <code>`</code> 是 <code>NoSubstitutionTemplate</code> 的开始。</p>\n<p>即使 <code>/</code> 和 <code>}` </code> 的解释取决于它们的“上下文”（它们在代码语法结构中的位置），但我们接下来描述的语法仍然是上下文无关的。</p>\n<p>词法语法使用几个目标符号来区分允许使用某些输入元素或不允许使用某些输入元素的上下文。<br>例如，目标符号 <code>InputElementDiv</code> 用于 <code>/</code> 表示除法和 <code>/=</code> 表示除法赋值的上下文。<br><a href=\"https://tc39.es/ecma262/#prod-InputElementDiv\"><code>InputElementDiv</code></a> 产生式列出了可以在此上下文中产生的可能的标记：</p>\n<pre><code>InputElementDiv ::\n  WhiteSpace\n  LineTerminator\n  Comment\n  CommonToken\n  DivPunctuator\n  RightBracePunctuator\n</code></pre>\n<p>在此上下文中，遇到 <code>/</code> 会产生 <code>DivPunctuator</code> 输入元素。在此处生成 <code>RegularExpressionLiteral</code> 不是一个可选项。</p>\n<p>另一方面，<a href=\"https://tc39.es/ecma262/#prod-InputElementRegExp\"><code>InputElementRegExp</code></a> 是<code> /</code> 是正则表达式开头的上下文的的目标符号：</p>\n<pre><code>InputElementRegExp ::\n  WhiteSpace\n  LineTerminator\n  Comment\n  CommonToken\n  RightBracePunctuator\n  RegularExpressionLiteral\n</code></pre>\n<p>从产生式中可以看出，有可能产生 <code>RegularExpressionLiteral</code> 输入元素，但生成 <code>DivPunctuator</code> 是不可能的。</p>\n<p>类似地，还有另一个目标符号 <code>InputElementRegExpOrTemplateTail</code> ，用于允许 <code>TemplateMiddle</code> 和 <code>TemplateTail</code> 的上下文，以及 <code>RegularExpressionLiteral</code> 。<br>最后，<code>InputElementTemplateTail</code> 是只允许 <code>TemplateMiddle</code> 和 <code>TemplateTail</code> 但不允许 <code>RegularExpressionLiteral</code> 的上下文的目标符号。</p>\n<p>在实现中，句法语法分析器（“解析器”）可以调用词法语法分析器（“标记器”或“词法分析器”），将目标符号作为参数传递，并请求下一个适合该目标符号的输入元素。</p>\n<h1>句法语法</h1>\n<p>我们研究了词法语法，它定义了如何从 Unicode 代码点构造标记。句法语法基于它：它定义了语法正确的程序如何由标记组成。</p>\n<h2>示例：允许旧标识符</h2>\n<p>向语法中引入新关键字是一个有可能会破坏现有代码的更改——如果现有代码已经将关键字用作标识符怎么办？</p>\n<p>例如，在 <code>await</code> 成为一个关键字之前，某人可能编写了以下代码：</p>\n<pre><code>function old() {\n  var await;\n}\n</code></pre>\n<p>ECMAScript 语法仔细地添加了 <code>await</code> 关键字，以便此代码继续工作。在异步函数内，<code>await</code> 是一个关键字，所以这是不起作用的：</p>\n<pre><code>async function modern() {\n  var await; // 语法错误\n}\n</code></pre>\n<p>在非生成器中允许 <code>yield</code> 作为标识符，而在生成器中不允许 <code>yield</code> 的工作方式类似。</p>\n<p>了解 <code>await</code> 如何作为标识符被允许需要了解 ECMAScript 特定的句法语法符号。让我们直接深入了解一下！</p>\n<h2>产生式和速记符号</h2>\n<p>让我们看看 <a href=\"https://tc39.es/ecma262/#prod-VariableStatement\"><code>VariableStatement</code></a> 的产生式是如何定义的。对语法的一瞥可能会让人有点望而生畏：</p>\n<pre><code>VariableStatement[Yield, Await] :\n  var VariableDeclarationList[+In, ?Yield, ?Await] ;\n</code></pre>\n<p>下标（<code>[Yield, Await]</code>）和前缀（<code>+</code> 在 <code>+In</code> 中以及 <code>?</code> 在 <code>?Async</code> 中）是什么意思？</p>\n<p>符号在 [语法符号] 部分（<a href=\"https://tc39.es/ecma262/#sec-grammar-notation%EF%BC%89%E4%B8%AD%E8%BF%9B%E8%A1%8C%E4%BA%86%E8%A7%A3%E9%87%8A%E3%80%82\">https://tc39.es/ecma262/#sec-grammar-notation）中进行了解释。</a></p>\n<p>下标都是一次用于表示一组左侧符号的产生式集的速记符号。左侧符号有两个参数，这两个参数展开成四个“真正的”左侧符号：<code>VariableStatement</code>、<code>VariableStatement_Yield</code>、<code>VariableStatement_Await</code> 和 <code>VariableStatement_Yield_Await</code>。</p>\n<p>请注意，这里的普通 <code>VariableStatement</code> 表示“没有 <code>_Await</code> 和 <code>_Yield</code> 的 <code>VariableStatement</code>”。它不应与 <code>VariableStatement[Yield, Await]</code> 混淆。</p>\n<p>在产生式的右侧，我们看到速记符号 <code>+In</code>，表示“使用带有 <code>_In</code> 的版本”，和 <code>?Await</code>，表示“当且仅当左侧符号有 <code>_Await</code> 时，使用带有 <code>_Await</code> 的版本”（对于 <code>?Yield</code> 类似）。</p>\n<p>第三个速记符号 <code>~Foo</code>，表示“使用没有 <code>_Foo</code> 的版本”，未在此产生式中使用。</p>\n<p>有了这些信息，我们可以这样展开产生式：</p>\n<pre><code>VariableStatement :\n  var VariableDeclarationList_In ;\n\nVariableStatement_Yield :\n  var VariableDeclarationList_In_Yield ;\n\nVariableStatement_Await :\n  var VariableDeclarationList_In_Await ;\n\nVariableStatement_Yield_Await :\n  var VariableDeclarationList_In_Yield_Await ;\n</code></pre>\n<p>最终，我们需要找出两件事：</p>\n<ol>\n<li>在何处决定我们是使用带有 <code>_Await</code> 还是不带有 <code>_Await</code> 的情况？</li>\n<li>在何处产生差异——<code>Something_Await</code> 和 <code>Something</code>（没有 <code>_Await</code>）的产生式在何处出现分歧？</li>\n</ol>\n<h2><code>_Await</code> 或没有 <code>_Await</code>？</h2>\n<p>我们先来解决第一个问题。不难猜到，非异步函数和异步函数在我们为函数体选择参数 <code>_Await</code> 或不选择参数 <code>_Await</code> 方面有所不同。<br>阅读异步函数声明的产生式时，我们发现了 <a href=\"https://tc39.es/ecma262/#prod-AsyncFunctionBody\">此内容</a>：</p>\n<pre><code>AsyncFunctionBody :\n  FunctionBody[~Yield, +Await]\n</code></pre>\n<p>请注意，<code>AsyncFunctionBody</code> 没有参数——它们会被添加到右侧的 <code>FunctionBody</code> 中。</p>\n<p>如果我们展开此产生式，我们将得到：</p>\n<pre><code>AsyncFunctionBody :\n  FunctionBody_Await\n</code></pre>\n<p>换句话说，异步函数有 <code>FunctionBody_Await</code>，这意味着函数体将 <code>await</code> 视为一个关键字。</p>\n<p>另一方面，如果我们在一个非异步函数内，<a href=\"https://tc39.es/ecma262/#prod-FunctionDeclaration\">相关的产生式</a> 是：</p>\n<pre><code>FunctionDeclaration[Yield, Await, Default] :\n  function BindingIdentifier[?Yield, ?Await] ( FormalParameters[~Yield, ~Await] ) { FunctionBody[~Yield, ~Await] }\n</code></pre>\n<p>（<code>FunctionDeclaration</code> 有另一个产生式，但与我们的代码示例无关。）</p>\n<p>为了避免组合展开，让我们忽略此特定产生式中不使用的 <code>Default</code> 参数。</p>\n<p>产生式展开后的形式是：</p>\n<pre><code>FunctionDeclaration :\n  function BindingIdentifier ( FormalParameters ) { FunctionBody }\n\nFunctionDeclaration_Yield :\n  function BindingIdentifier_Yield ( FormalParameters ) { FunctionBody }\n\nFunctionDeclaration_Await :\n  function BindingIdentifier_Await ( FormalParameters ) { FunctionBody }\n\nFunctionDeclaration_Yield_Await :\n  function BindingIdentifier_Yield_Await ( FormalParameters ) { FunctionBody }\n</code></pre>\n<p>在此产生式中，我们始终得到 <code>FunctionBody</code> 和 <code>FormalParameters</code>（没有 <code>_Yield</code> 和没有 <code>_Await</code>），因为它们在未展开的产生式中使用 <code>[~Yield, ~Await]</code> 进行参数化。</p>\n<p>函数名称的处理不同：如果左侧符号具有 <code>_Await</code> 和 <code>_Yield</code>，则函数名称将获得参数 <code>_Await</code> 和 <code>_Yield</code>。</p>\n<p>总结一下：异步函数具有 <code>FunctionBody_Await</code>，非异步函数具有 <code>FunctionBody</code>（没有 <code>_Await</code>）。<br>由于我们讨论的是非生成器函数，因此我们的异步示例函数和非异步示例都使用不带 <code>_Yield</code> 的参数。</p>\n<p>也许很难记住哪一个是 <code>FunctionBody</code>，哪一个是 <code>FunctionBody_Await</code>。<br><code>FunctionBody_Await</code> 是用于 <code>await</code> 是标识符的函数，还是用于 <code>await</code> 是关键字的函数？</p>\n<p>你可以将 <code>_Await</code> 参数理解为“<code>await</code> 是一个关键字”。这种方法还可以面向未来。想象一下，添加了一个新关键字 <code>blob</code>，但仅在“blob 状”函数内。<br>非 blob 状的非异步非生成器仍会具有 <code>FunctionBody</code>（没有 <code>_Await</code>、<code>_Yield</code> 或 <code>_Blob</code>），就像它们现在一样。<br>Blob 状函数将具有 <code>FunctionBody_Blob</code>，异步 blob 状函数将具有 <code>FunctionBody_Await_Blob</code>，依此类推。<br>我们仍然需要向产生式添加 <code>Blob</code> 下标，但已经存在的函数的 <code>FunctionBody</code> 的展开形式保持不变。</p>\n<h2>不允许 <code>await</code> 作为标识符</h2>\n<p>接下来，我们需要找出如何在 <code>FunctionBody_Await</code> 内部将 <code>await</code> 不允许作为一个标识符。</p>\n<p>我们可以进一步跟踪产生式，以了解 <code>_Await</code> 参数如何从 <code>FunctionBody</code> 原封不动地传递到我们先前看到的 <code>VariableStatement</code> 产生式。</p>\n<p>因此，在异步函数内，我们将有一个 <code>VariableStatement_Await</code>，在非异步函数内，我们将有一个 <code>VariableStatement</code>。</p>\n<p>我们可以进一步跟踪产生式，并跟踪参数。我们已经看到了 <a href=\"https://tc39.es/ecma262/#prod-VariableStatement\"><code>VariableStatement</code></a> 的产生式：</p>\n<pre><code>VariableStatement[Yield, Await] :\n  var VariableDeclarationList[+In, ?Yield, ?Await] ;\n</code></pre>\n<p><a href=\"https://tc39.es/ecma262/#prod-VariableDeclarationList\"><code>VariableDeclarationList</code></a> 的所有产生式都会原封不动地传递参数：</p>\n<pre><code>VariableDeclarationList[In, Yield, Await] :\n  VariableDeclaration[?In, ?Yield, ?Await]\n</code></pre>\n<p>（这里我们只展示与我们的示例相关的 <a href=\"https://tc39.es/ecma262/#prod-VariableDeclaration\">产生式</a>。）</p>\n<pre><code>VariableDeclaration[In, Yield, Await] :\n  BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt\n</code></pre>\n<p>速记符号 <code>opt</code> 表示右侧符号是可选的；实际上有两个产生式，一个带有可选符号，一个不带有可选符号。</p>\n<p>在与我们的示例相关的简单情况下，<code>VariableStatement</code> 由关键字 <code>var</code> 组成，后面跟着一个没有初始值设定项的 <code>BindingIdentifier</code>，最后以分号结尾。</p>\n<p>为了不允许或允许 <code>await</code> 作为 <code>BindingIdentifier</code>，我们希望最终得到类似这样的内容：</p>\n<pre><code>BindingIdentifier_Await :\n  Identifier\n  yield\n\nBindingIdentifier :\n  Identifier\n  yield\n  await\n</code></pre>\n<p>这将不允许在异步函数内将 <code>await</code> 作为标识符，并在非异步函数内允许将其作为标识符。</p>\n<p>但规范并不是这样定义的，相反我们发现了 <a href=\"https://tc39.es/ecma262/#prod-BindingIdentifier\">此产生式</a>：</p>\n<pre><code>BindingIdentifier[Yield, Await] :\n  Identifier\n  yield\n  await\n</code></pre>\n<p>展开后，这意味着以下产生式：</p>\n<pre><code>BindingIdentifier_Await :\n  Identifier\n  yield\n  await\n\nBindingIdentifier :\n  Identifier\n  yield\n  await\n</code></pre>\n<p>（我们省略了在我们的示例中不需要的 <code>BindingIdentifier_Yield</code> 和 <code>BindingIdentifier_Yield_Await</code> 的产生式。）</p>\n<p>这看起来像 <code>await</code> 和 <code>yield</code> 将始终被允许用作标识符。怎么回事？整篇博文都白写了？</p>\n<h2>静态语义来救援</h2>\n<p>事实证明，禁止将 <code>await</code> 作为异步函数内的标识符需要 <strong>静态语义</strong>。</p>\n<p>静态语义描述静态规则——也就是说，在程序运行之前检查的规则。</p>\n<p>在这种情况下，<code>BindingIdentifier</code> 的 <a href=\"https://tc39.es/ecma262/#sec-identifiers-static-semantics-early-errors\">静态语义</a> 定义了以下语法导向规则：</p>\n<blockquote>\n<pre><code>BindingIdentifier[Yield, Await] : await\n</code></pre>\n<p>如果此产生式具有 <code>[Await]</code> 参数，则这是 Syntax Error。</p>\n</blockquote>\n<p>实际上，这禁止了 <code>BindingIdentifier_Await : await</code> 产生式。</p>\n<p>规范解释说，之所以有此产生式但通过静态语义将其定义为 Syntax Error，是因为与自动分号插入（ASI）发生干扰。</p>\n<p>记住，当我们无法根据语法产生式解析代码行时，ASI 就会发挥作用。ASI 会尝试添加分号以满足语句和声明必须以分号结尾的要求。（我们将在以后的章节中详细描述 ASI。）</p>\n<p>考虑以下代码（规范中的示例）：</p>\n<pre><code>async function too_few_semicolons() {\n  let\n  await 0;\n}\n</code></pre>\n<p>如果语法不允许 <code>await</code> 作为标识符，ASI 会发挥作用并将代码转换成以下语法正确的代码，该代码也使用 <code>let</code> 作为标识符：</p>\n<pre><code>async function too_few_semicolons() {\n  let;\n  await 0;\n}\n</code></pre>\n<p>由于这种类型的与 ASI 的干扰被认为过于混乱，因此静态语义被用于禁止将 <code>await</code> 作为标识符。</p>\n<h2>标识符的不允许 <code>StringValue</code></h2>\n<p>还有一条相关的规则：</p>\n<blockquote>\n<pre><code>BindingIdentifier : Identifier\n</code></pre>\n<p>如果此产生式具有 <code>[Await]</code> 参数并且 <code>StringValue</code> of <code>Identifier</code> 是 <code>&quot;await&quot;</code>，则这是 Syntax Error。</p>\n</blockquote>\n<p>这起初可能会令人困惑。<a href=\"https://tc39.es/ecma262/#prod-Identifier\"><code>Identifier</code></a> 定义如下：</p>\n<pre><code>Identifier :\n  IdentifierName but not ReservedWord\n</code></pre>\n<p><code>await</code> 是 <code>ReservedWord</code>，所以 <code>Identifier</code> 怎么会是 <code>await</code> 呢？</p>\n<p>事实证明，<code>Identifier</code> 不能是 <code>await</code>，但它可以是 <code>StringValue</code> 为 <code>&quot;await&quot;</code> 的其他东西——字符序列 <code>await</code> 的不同表示形式。</p>\n<p>标识符名称的 <a href=\"https://tc39.es/ecma262/#sec-identifier-names-static-semantics-stringvalue\">静态语义</a> 定义了如何计算标识符名称的 <code>StringValue</code>。<br>例如，<code>a</code> 的 Unicode 转义序列是 <code>\\u0061</code>，所以 <code>\\u0061wait</code> 的 <code>StringValue</code> 是 <code>&quot;await&quot;</code>。<br><code>\\u0061wait</code> 不会被词法语法识别为关键字，而将是一个 <code>Identifier</code>。<br>静态语义禁止将其用作异步函数内的变量名。</p>\n<p>因此，这是可行的：</p>\n<pre><code>function old() {\n  var \\u0061wait;\n}\n</code></pre>\n<p>而这不行：</p>\n<pre><code>async function modern() {\n  var \\u0061wait; // Syntax error\n}\n</code></pre>\n<h1>总结</h1>\n<p>在本节中，我们熟悉了词法语法、句法语法以及用于定义句法语法的速记符号。<br>作为一个示例，我们研究了禁止在异步函数内将 <code>await</code> 用作标识符，但在非异步函数内允许将其用作标识符。</p>\n<p>句法语法的其他有趣部分，例如自动分号插入和覆盖语法，将在以后的章节中介绍。敬请期待！</p>\n","tags":["ecma262"]},{"id":"understanding-ecmascript-part-2","url":"https://yieldray.fun/posts/understanding-ecmascript-part-2","title":"理解ECMAScript规范（Part 2）","date_published":"2024-03-08T10:00:00.000Z","date_modified":"2024-03-08T10:00:00.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://v8.dev/blog/understanding-ecmascript-part-2\">https://v8.dev/blog/understanding-ecmascript-part-2</a></p>\n</blockquote>\n<h1>准备好第 2 部分了吗？</h1>\n<p>了解规范的一个有趣方法是从我们已知的 JavaScript 特性开始，找出其规范化方式。</p>\n<blockquote>\n<p>警告！本章节包含从 <a href=\"https://tc39.es/ecma262/\">ECMAScript 规范</a>（截至 2020 年 2 月）复制粘贴的算法。它们最终会过时。</p>\n</blockquote>\n<p>我们知道属性是在原型链中查找的：如果一个对象没有我们要读取属性，我们会向上遍历原型链直到找到它（或找到不再有原型的对象）。</p>\n<p>例如：</p>\n<pre><code>const o1 = { foo: 99 };\nconst o2 = {};\nObject.setPrototypeOf(o2, o1);\no2.foo;\n// → 99\n</code></pre>\n<h1>原型遍历在何处定义？</h1>\n<p>让我们尝试找出此行为的定义位置。一个好的出发点是 <a href=\"https://tc39.es/ecma262/#sec-object-internal-methods-and-internal-slots\">对象内部方法</a> 列表。</p>\n<p>这里既有 <code>[[GetOwnProperty]]</code> 也有 <code>[[Get]]</code> — 我们对不受限于<em>自身</em>属性的版本感兴趣，所以我们会使用 <code>[[Get]]</code>。</p>\n<p>遗憾的是，<a href=\"https://tc39.es/ecma262/#sec-property-descriptor-specification-type\">属性描述器规范类型</a> 也有一个名为 <code>[[Get]]</code> 的字段，因此在规范中浏览 <code>[[Get]]</code> 时，我们需要仔细区分两个独立的使用情况。</p>\n<p><code>[[Get]]</code> 是一种<strong>必要内部方法</strong>。<strong>普通对象</strong>实现必要内部方法的默认行为。<strong>异类对象</strong>可以定义自己的内部方法 <code>[[Get]]</code>，与默认行为不同。在这篇文章中，我们重点关注普通对象。</p>\n<p><code>[[Get]]</code> 的默认实现委托给 <code>OrdinaryGet</code>：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-get-p-receiver\"><code>[[Get]] ( P, Receiver )</code></a></strong></p>\n<p>当 <code>O</code> 的 <code>[[Get]]</code> 内部方法使用属性键 <code>P</code> 和 ECMAScript 语言值 <code>Receiver</code> 被调用时，执行以下步骤：</p>\n<ol>\n<li>返回 <code>? OrdinaryGet(O, P, Receiver)</code>。</li>\n</ol>\n</blockquote>\n<p>我们很快就会看到 <code>Receiver</code> 是在调用访问器属性的 getter 函数时用作<strong>this value</strong>的值。</p>\n<p><code>OrdinaryGet</code> 定义如下：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-ordinaryget\"><code>OrdinaryGet ( O, P, Receiver )</code></a></strong></p>\n<p>当抽象操作 <code>OrdinaryGet</code> 使用对象 <code>O</code>、属性键 <code>P</code> 和 ECMAScript 语言值 <code>Receiver</code> 被调用时，执行以下步骤：</p>\n<ol>\n<li>断言：<code>IsPropertyKey(P)</code> 为 <code>true</code>。</li>\n<li>令 <code>desc</code> 为 <code>? O.[[GetOwnProperty]](P)</code>。</li>\n<li>若 <code>desc</code> 为 <code>undefined</code>，则<ol>\n<li>令 <code>parent</code> 为 <code>? O.[[GetPrototypeOf]]()</code>。</li>\n<li>若 <code>parent</code> 为 <code>null</code>，则返回 <code>undefined</code>。</li>\n<li>返回 <code>? parent.[[Get]](P, Receiver)</code>。</li>\n</ol>\n</li>\n<li>若 <code>IsDataDescriptor(desc)</code> 为 <code>true</code>，则返回 <code>desc.[[Value]]</code>。</li>\n<li>断言：<code>IsAccessorDescriptor(desc)</code> 为 <code>true</code>。</li>\n<li>令 <code>getter</code> 为 <code>desc.[[Get]]</code>。</li>\n<li>若 <code>getter</code> 为 <code>undefined</code>，则返回 <code>undefined</code>。</li>\n<li>返回 <code>? Call(getter, Receiver)</code>。</li>\n</ol>\n</blockquote>\n<p>原型链遍历位于步骤 3 中：如果我们未找到属性作为自身属性，我们调用原型的 <code>[[Get]]</code> 方法，该方法再次委托给 <code>OrdinaryGet</code>。<br>如果我们仍然找不到属性，我们调用其原型的 <code>[[Get]]</code> 方法，该方法再次委托给 <code>OrdinaryGet</code>，依此类推，直至我们找到该属性或到达没有原型的对象。</p>\n<p>让我们看看当我们访问 <code>o2.foo</code> 时此算法如何工作。首先，我们用 <code>O</code> 为 <code>o2</code> 且 <code>P</code> 为 <code>&quot;foo&quot;</code> 的值调用 <code>OrdinaryGet</code>。<br><code>O.[[GetOwnProperty]](&quot;foo&quot;)</code> 返回 <code>undefined</code>，因为 <code>o2</code> 没有名为 <code>&quot;foo&quot;</code> 的自身属性，因此我们使用步骤 3 中的分支。<br>在步骤 3.a 中，我们将 <code>parent</code> 设置为 <code>o2</code> 的原型，即 <code>o1</code>。<br><code>parent</code> 不为 <code>null</code>，因此我们不会在步骤 3.b 中返回。<br>在步骤 3.c 中，我们使用属性键 <code>&quot;foo&quot;</code> 调用父级的 <code>[[Get]]</code> 方法，并返回任何它返回的内容。</p>\n<p>父级 (<code>o1</code>) 是一个普通对象，因此其 <code>[[Get]]</code> 方法再次调用 <code>OrdinaryGet</code>，这次 <code>O</code> 为 <code>o1</code> 且 <code>P</code> 为 <code>&quot;foo&quot;</code>。<br><code>o1</code> 有一个名为 <code>&quot;foo&quot;</code> 的自身属性，因此在步骤 2 中，<code>O.[[GetOwnProperty]](&quot;foo&quot;)</code> 返回关联的属性描述符，我们将它存储在 <code>desc</code> 中。</p>\n<p><a href=\"https://tc39.es/ecma262/#sec-property-descriptor-specification-type\">属性描述符</a> 是一种规范类型。<br>数据属性描述符直接将属性的值存储在 <code>[[Value]]</code> 字段中。<br>访问器属性描述符将访问器函数存储在字段 <code>[[Get]]</code> 和/或 <code>[[Set]]</code> 中。<br>在本例中，与 <code>&quot;foo&quot;</code> 关联的属性描述符是数据属性描述符。</p>\n<p>我们第 2 步中存储在 <code>desc</code> 中的数据属性描述符不是 <code>undefined</code>，因此我们不会在步骤 3 中使用 <code>if</code> 分支。接下来，我们执行步骤 4。<br>属性描述符是数据属性描述符，因此我们返回其 <code>[[Value]]</code> 字段，在步骤 4 中返回 <code>99</code>，我们完成了。</p>\n<h1><code>Receiver</code> 是什么，它从何而来？</h1>\n<p><code>Receiver</code> 参数仅在第 8 步访问器属性的情况下使用。它在调用访问器属性的 getter 函数时作为<strong>this value</strong>传递。</p>\n<p><code>OrdinaryGet</code> 在整个递归过程中传递原始 <code>Receiver</code>，保持不变（步骤 3.c）。让我们找出 <code>Receiver</code> 最初来自哪里！</p>\n<p>搜索调用 <code>[[Get]]</code> 的地方，我们发现一个操作在 Reference 上操作的抽象操作 <code>GetValue</code>。Reference 是一种规范类型，由基值、引用名称和严格引用标志组成。在 <code>o2.foo</code> 的情况下，基值为对象 <code>o2</code>，引用名称为字符串 <code>&quot;foo&quot;</code>，严格引用标志为 <code>false</code>，因为示例代码是松散的。</p>\n<h2>附属内容：为什么 Reference 不是 Record？</h2>\n<p>附属内容：Reference 不是 Record，即使它看起来像那样。<br>它包含三个组件，这些组件同样可以表示为三个命名字段。Reference 不是 Record，仅仅是因为历史原因。</p>\n<h2>返回 <code>GetValue</code></h2>\n<p>让我们看看 <code>GetValue</code> 的定义：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-getvalue\"><code>GetValue ( V )</code></a></strong></p>\n<ol>\n<li><code>ReturnIfAbrupt(V)</code>。</li>\n<li>若 <code>Type(V)</code> 不是 <code>Reference</code>，则返回 <code>V</code>。</li>\n<li>令 <code>base</code> 为 <code>GetBase(V)</code>。</li>\n<li>若 <code>IsUnresolvableReference(V)</code> 为 <code>true</code>，则抛出 <code>ReferenceError</code> 异常。</li>\n<li>若 <code>IsPropertyReference(V)</code> 为 <code>true</code>，则<ol>\n<li>若 <code>HasPrimitiveBase(V)</code> 为 <code>true</code>，则<ol>\n<li>断言：在这种情况下，<code>base</code> 绝不会是 <code>undefined</code> 或 <code>null</code>。</li>\n<li>将 <code>base</code> 设置为 <code>! ToObject(base)</code>。</li>\n</ol>\n</li>\n<li>返回 <code>? base.[[Get]](GetReferencedName(V), GetThisValue(V))</code>。</li>\n</ol>\n</li>\n<li>否则，<ol>\n<li>断言：<code>base</code> 是一个 Environment Record。</li>\n<li>返回 <code>? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))</code></li>\n</ol>\n</li>\n</ol>\n</blockquote>\n<p>在我们示例中，引用为 <code>o2.foo</code>，这是一个属性引用（property reference）。因此，我们使用分支 5。我们不使用 5.a 中的分支，因为基(<code>o2</code>)不是 <a href=\"https://v8.dev/blog/react-cliff#javascript-types\">原始值</a>（Number, String, Symbol, BigInt, Boolean, Undefined, or Null）。</p>\n<p>然后我们在步骤 5.b 中调用 <code>[[Get]]</code>。我们传递的 <code>Receiver</code> 是 <code>GetThisValue(V)</code>。在本例中，它只是 Reference 的基值（base value）：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-getthisvalue\"><code>GetThisValue( V )</code></a></strong></p>\n<ol>\n<li>断言：<code>IsPropertyReference(V)</code> 为 <code>true</code>。</li>\n<li>若 <code>IsSuperReference(V)</code> 为 <code>true</code>，则<ol>\n<li>返回 Reference <code>V</code> 的 <code>thisValue</code> 组件的值。</li>\n</ol>\n</li>\n<li>返回 <code>GetBase(V)</code>。</li>\n</ol>\n</blockquote>\n<p>对于 <code>o2.foo</code>，我们不使用步骤 2 中的分支，因为它不是 Super Reference（例如 <code>super.foo</code>），但我们使用步骤 3 并返回 Reference 的基值，即 <code>o2</code>。</p>\n<p>将所有内容拼凑在一起，我们发现将 <code>Receiver</code> 设置为原始 Reference 的基，然后我们在原型链遍历期间保持不变。最后，如果我们找到的属性是访问器属性，我们使用 <code>Receiver</code> 作为<strong>this value</strong>来调用它。</p>\n<p>特别是，getter 内部的<strong>this value</strong>引用的是我们尝试从中获取该属性的原始对象，而不是我们在原型链遍历期间找到该属性的对象。</p>\n<p>让我们试一试！</p>\n<pre><code>const o1 = { x: 10, get foo() { return this.x; } };\nconst o2 = { x: 50 };\nObject.setPrototypeOf(o2, o1);\no2.foo;\n// → 50\n</code></pre>\n<p>在此示例中，我们有一个名为 <code>foo</code> 的访问器属性，并且为它定义了一个 getter。getter 返回 <code>this.x</code>。</p>\n<p>然后，我们访问 <code>o2.foo</code> - getter 返回什么？</p>\n<p>我们发现，当我们调用 getter 时，<strong>this value</strong>是我们最初尝试从中获取属性的对象，而不是我们在原型链遍历期间找到该属性的对象。在本例中，<strong>this value</strong> 是 <code>o2</code>，而不是 <code>o1</code>。我们可以通过检查 getter 是否返回 <code>o2.x</code> 或 <code>o1.x</code> 来验证这一点，实际上，它返回 <code>o2.x</code>。</p>\n<p>有效！我们根据在规范中阅读的内容预测了此代码片段的行为。</p>\n<h1>访问属性 - 为何它调用 <code>[[Get]]</code>？</h1>\n<p>规范在哪指出访问属性（例如 <code>o2.foo</code>）时将调用对象内部方法 <code>[[Get]]</code>？这肯定在某个地方有定义。不要只相信我的话！</p>\n<p>我们发现对象内部方法 <code>[[Get]]</code> 从操作在 Reference 上的抽象操作 <code>GetValue</code> 调用。但是 <code>GetValue</code> 从何处调用？</p>\n<h2><code>MemberExpression</code> 的运行时语义</h2>\n<p>规范的语法规则定义了语言的语法。<a href=\"https://tc39.es/ecma262/#sec-runtime-semantics\">运行时语义</a> 定义语法结构“表示的意思”（即在运行时如何评估它们）。</p>\n<p>如果你不熟悉 <a href=\"https://en.wikipedia.org/wiki/Context-free_grammar\">上下文无关文法</a>，现在了解一下是个好时机！</p>\n<p>我们将在之后的章节中深入研究语法规则，现在先简单点！特别是，我们可以忽略本章中的下标（<code>Yield</code>、<code>Await</code> 等）。</p>\n<p>以下生成描述了 <a href=\"https://tc39.es/ecma262/#prod-MemberExpression\"><code>MemberExpression</code></a> 是什么样子：</p>\n<pre><code>MemberExpression :\n  PrimaryExpression\n  MemberExpression [ Expression ]\n  MemberExpression . IdentifierName\n  MemberExpression TemplateLiteral\n  SuperProperty\n  MetaProperty\n  new MemberExpression Arguments\n</code></pre>\n<p>这里有 7 个 <code>MemberExpression</code> 的生成（productions）。<code>MemberExpression</code> 仅能是 <code>PrimaryExpression</code>。<br>或者，<code>MemberExpression</code> 可以通过拼接另一个 <code>MemberExpression</code> 和 <code>Expression</code> 来构建：<br>例如，<code>MemberExpression [ Expression ]</code>，<code>o2[&#39;foo&#39;]</code>。<br>或者，它可以是 <code>MemberExpression . IdentifierName</code>，例如 <code>o2.foo</code> - 这是我们示例中相关的生成。</p>\n<p>生成 <code>MemberExpression : MemberExpression . IdentifierName</code> 的运行时语义定义了在评估它时要执行的一组步骤：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-property-accessors-runtime-semantics-evaluation\">运行时语义：<code>MemberExpression : MemberExpression . IdentifierName</code> 的评估</a></strong></p>\n<ol>\n<li>令 <code>baseReference</code> 为评估 <code>MemberExpression</code> 的结果。</li>\n<li>令 <code>baseValue</code> 为 <code>? GetValue(baseReference)</code>。</li>\n<li>若由 <code>MemberExpression</code> 匹配的代码是严格模式代码，则令 <code>strict</code> 为 <code>true</code>；否则 <code>strict</code> 为 <code>false</code>。</li>\n<li>返回 <code>? EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict)</code>。</li>\n</ol>\n</blockquote>\n<p>该算法委托给抽象操作 <code>EvaluatePropertyAccessWithIdentifierKey</code>，所以我们也需要阅读它：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-evaluate-property-access-with-identifier-key\"><code>EvaluatePropertyAccessWithIdentifierKey( baseValue, identifierName, strict )</code></a></strong></p>\n<p>抽象操作 <code>EvaluatePropertyAccessWithIdentifierKey</code> 带有参数值 <code>baseValue</code>、解析节点 <code>identifierName</code> 和布尔参数 <code>strict</code>。它执行以下步骤：</p>\n<ol>\n<li>断言：<code>identifierName</code> 为 <code>IdentifierName</code></li>\n<li>令 <code>bv</code> 为 <code>? RequireObjectCoercible(baseValue)</code>。</li>\n<li>令 <code>propertyNameString</code> 为 <code>identifierName</code> 的 <code>StringValue</code>。</li>\n<li>返回类型为 Reference 的值，其基值组件（base value component）为 <code>bv</code>，引用名称组件（referenced name component）为 <code>propertyNameString</code>，严格引用标志为 <code>strict</code>。</li>\n</ol>\n</blockquote>\n<p>即：<code>EvaluatePropertyAccessWithIdentifierKey</code> 构造一个 Reference，使用提供的 <code>baseValue</code> 作为基，<code>identifierName</code> 的字符串值作为属性名称，<code>strict</code> 作为严格模式标志。</p>\n<p>最终，此 Reference 将传给 <code>GetValue</code>。这在规范的多个地方有定义，具体取决于 Reference 最终如何使用。</p>\n<h2><code>MemberExpression</code> 作为参数</h2>\n<p>在我们的示例中，我们使用属性访问作为参数：</p>\n<pre><code>console.log(o2.foo);\n</code></pre>\n<p>在这种情况下，行为在 <code>ArgumentList</code> 生成运行时语义中定义，该运行时语义对参数调用 <code>GetValue</code>：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-argument-lists-runtime-semantics-argumentlistevaluation\">运行时语义：<code>ArgumentListEvaluation</code></a></strong></p>\n<p><code>ArgumentList : AssignmentExpression</code></p>\n<ol>\n<li>将 <code>ref</code> 设置为评估 <code>AssignmentExpression</code> 的结果。</li>\n<li>将 <code>arg</code> 设置为 <code>? GetValue(ref)</code>。</li>\n<li>返回唯一项目为 <code>arg</code> 的列表。</li>\n</ol>\n</blockquote>\n<p><code>o2.foo</code> 不会像 <code>AssignmentExpression</code> 一样，但这个生成是适用的。要了解原因，你可以查看此 <a href=\"/posts/understanding-ecmascript-part-2-extra\">附加内容</a>，但在这一点上并不是绝对必要的。</p>\n<p>步骤 1 中的 <code>AssignmentExpression</code> 是 <code>o2.foo</code>。<code>ref</code>，评估 <code>o2.foo</code> 的结果，就是上面提到的 Reference。在步骤 2 中，我们对其调用 <code>GetValue</code>。因此，我们知道对象内部方法 <code>[[Get]]</code> 将被调用，并且将发生原型链遍历。</p>\n<h1>总结</h1>\n<p>在本章节中，我们研究了规范如何跨所有不同层定义语言特性（在本例中为原型查找）：触发该特性的语法结构以及定义该特性的算法。</p>\n","tags":["ecma262"]},{"id":"understanding-ecmascript-part-2-extra","url":"https://yieldray.fun/posts/understanding-ecmascript-part-2-extra","title":"理解ECMAScript规范（Part 2）额外内容","date_published":"2024-03-07T10:00:00.000Z","date_modified":"2024-03-07T10:00:00.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://v8.dev/blog/extras/understanding-ecmascript-part-2-extra\">https://v8.dev/blog/extras/understanding-ecmascript-part-2-extra</a></p>\n</blockquote>\n<h1>为什么 <code>o2.foo</code> 是 <code>AssignmentExpression</code>？</h1>\n<p><code>o2.foo</code> 看起来不像是 <code>AssignmentExpression</code>，因为它没有赋值。<br>那么为什么它是一个 <code>AssignmentExpression</code>？</p>\n<p>实际上，规范允许 <code>AssignmentExpression</code> 同时作为<em>参数</em>和<em>赋值的右侧</em>。例如：</p>\n<pre><code>function simple(a) {\n  console.log(&#39;The argument was &#39; + a);\n}\nsimple(x = 1);\n// → Logs “The argument was 1”。\nx;\n// → 1\n</code></pre>\n<p>…此外…</p>\n<pre><code>x = y = 5;\nx; // 5\ny; // 5\n</code></pre>\n<p><code>o2.foo</code> 是一个不赋值的 <code>AssignmentExpression</code>。<br>这遵循以下语法规则，每个规则取最“简单”的情況，直到最后一条规则：</p>\n<p>一个 <code>AssignmentExpression</code> 不一定需要赋值，它也可以只是一个 <code>ConditionalExpression</code>：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-assignment-operators\"><code>AssignmentExpression : ConditionalExpression</code></a></strong></p>\n</blockquote>\n<p>（还有其他的规则，我们这里只展示相关的。）</p>\n<p>一个 <code>ConditionalExpression</code> 不一定需要条件（<code>a == b ? c : d</code>），它也可以只是一个 <code>ShortcircuitExpression</code>：</p>\n<blockquote>\n<p><strong><a href=\"https://tc39.es/ecma262/#sec-conditional-operator\"><code>ConditionalExpression : ShortCircuitExpression</code></a></strong></p>\n</blockquote>\n<p>以此类推：</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#prod-ShortCircuitExpression\"><code>ShortCircuitExpression : LogicalORExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-LogicalORExpression\"><code>LogicalORExpression : LogicalANDExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-LogicalANDExpression\"><code>LogicalANDExpression : BitwiseORExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-BitwiseORExpression\"><code>BitwiseORExpression : BitwiseXORExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-BitwiseXORExpression\"><code>BitwiseXORExpression : BitwiseANDExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-BitwiseANDExpression\"><code>BitwiseANDExpression : EqualityExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#sec-equality-operators\"><code>EqualityExpression : RelationalExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-RelationalExpression\"><code>RelationalExpression : ShiftExpression</code></a></p>\n</blockquote>\n<p>差不多就这些…</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#prod-ShiftExpression\"><code>ShiftExpression : AdditiveExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-AdditiveExpression\"><code>AdditiveExpression : MultiplicativeExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-MultiplicativeExpression\"><code>MultiplicativeExpression : ExponentialExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-ExponentiationExpression\"><code>ExponentialExpression : UnaryExpression</code></a></p>\n</blockquote>\n<p>别绝望！再看几条规则……</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#prod-UnaryExpression\"><code>UnaryExpression : UpdateExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-UpdateExpression\"><code>UpdateExpression : LeftHandSideExpression</code></a></p>\n</blockquote>\n<p>然后我们到达了 <code>LeftHandSideExpression</code> 的规则：</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#prod-LeftHandSideExpression\"><code>LeftHandSideExpression :</code></a><br><code>NewExpression</code><br><code>CallExpression</code><br><code>OptionalExpression</code></p>\n</blockquote>\n<p>不清楚哪个规则可能适用于 <code>o2.foo</code>。<br>我们只需要知道（或找出）<code>NewExpression</code> 实际上不必有 <code>new</code> 关键词。</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#prod-NewExpression\"><code>NewExpression : MemberExpression</code></a></p>\n</blockquote>\n<p><code>MemberExpression</code> 看起来像是我们正在找的东西，所以现在我们取这条规则</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#prod-MemberExpression\"><code>MemberExpression : MemberExpression . IdentifierName</code></a></p>\n</blockquote>\n<p>因此，如果 <code>o2</code> 是一个有效的 <code>MemberExpression</code>，那么 <code>o2.foo</code> 就是一个 <code>MemberExpression</code>。幸运的是，这很容易看出：</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#prod-MemberExpression\"><code>MemberExpression : PrimaryExpression</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-PrimaryExpression\"><code>PrimaryExpression : IdentifierReference</code></a></p>\n<p><a href=\"https://tc39.es/ecma262/#prod-IdentifierReference\"><code>IdentifierReference : Identifier</code></a></p>\n</blockquote>\n<p><code>o2</code> 肯定是一个 <code>Identifier</code>，所以很不错的。<br><code>o2</code> 是一个 <code>MemberExpression</code>，所以 <code>o2.foo</code> 也是一个 <code>MemberExpression</code>。<br>一个 <code>MemberExpression</code> 是一个有效的 <code>AssignmentExpression</code>，所以 <code>o2.foo</code> 也是一个 <code>AssignmentExpression</code>。</p>\n","tags":["ecma262"]},{"id":"understanding-ecmascript-part-1","url":"https://yieldray.fun/posts/understanding-ecmascript-part-1","title":"理解ECMAScript规范（Part 1）","date_published":"2024-03-06T10:00:00.000Z","date_modified":"2024-03-06T10:00:00.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://v8.dev/blog/understanding-ecmascript-part-1\">https://v8.dev/blog/understanding-ecmascript-part-1</a><br>亦见：<a href=\"https://v8.js.cn/blog/understanding-ecmascript-part-1\">https://v8.js.cn/blog/understanding-ecmascript-part-1</a></p>\n</blockquote>\n<h1>序言</h1>\n<p>ECMAScript 语言规范（<a href=\"https://tc39.es/ecma262/\">ECMA262</a>），下面简称语言规范，或规范。</p>\n<p>让我们通过一个实例走进并理解规范。<br>下面的代码说明了如何使用 <code>Object.prototype.hasOwnProperty</code>：</p>\n<pre><code class=\"language-js\">const o = { foo: 1 };\no.hasOwnProperty(&quot;foo&quot;); // true\no.hasOwnProperty(&quot;bar&quot;); // false\n</code></pre>\n<p>在上面的示例中，对象 <code>o</code> 并没有 <code>hasOwnProperty</code> 属性，因此要在原型链上找到它。<br>对象 <code>o</code> 的原型是 <code>Object.prototype</code>。</p>\n<p>为了描述 <code>Object.prototype.hasOwnProperty</code> 是如何工作的，规范使用如下伪代码来描述：</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262#sec-object.prototype.hasownproperty\"><code>Object.prototype.hasOwnProperty(V)</code></a></p>\n<p>给定参数 <code>V</code>，调用 <code>hasOwnProperty</code> 方法，将执行如下步骤：</p>\n<ol>\n<li>令 <code>P</code> 为 <code>? ToPropertyKey(V)</code></li>\n<li>令 <code>O</code> 为 <code>? ToObject(this value)</code></li>\n<li>返回 <code>? HasOwnProperty(O, P)</code></li>\n</ol>\n</blockquote>\n<p>…此外…</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262#sec-hasownproperty\"><code>HasOwnProperty(O, P)</code></a></p>\n<p>抽象操作 <code>HasOwnProperty</code> 用于确定一个对象上是否存在指定属性键，返回一个布尔值。<br>该操作接受参数 <code>O</code> 和 <code>P</code>，且 <code>O</code> 是 对象，<code>P</code> 是属性键。此抽象操作执行如下步骤：</p>\n<p>断言：<code>Type(O)</code> 为 <code>Object</code><br>断言：<code>IsPropertyKey(P)</code> 为 <code>true</code><br>令 <code>desc</code> 为 <code>? O.[[GetOwnProperty]](P)</code><br>若 <code>desc</code> 为 <code>undefined</code>，返回 <code>false</code><br>返回 <code>true</code></p>\n</blockquote>\n<p>但，什么是 “抽象操作”（abstract operations）？<code>[[ ]]</code> 符号里面的东西又是什么？为什么函数前有一个 <code>?</code> 符号？断言（assert）又起什么作用？</p>\n<p>让我们一起看看！</p>\n<h1>语言类型与规范类型</h1>\n<blockquote>\n<p>注：此处类型都指数据类型（data type）</p>\n</blockquote>\n<p>规范中使用的值，例如 <code>undefined</code>、<code>true</code> 和 <code>false</code> 都是我们已了解的 Javascript 值。<br>它们也都是<a href=\"https://tc39.es/ecma262/#sec-ecmascript-language-types\"><strong>语言值</strong>（language values）</a>，即规范定义的语言类型（language types）的值。</p>\n<p>同时，规范内部也会使用语言值。例如，一个内部数据类型可能包含某个可以为 <code>true</code> 或 <code>false</code> 的字段。<br>与之相反，JavaScript 引擎（即规范实现者）往往不在内部使用语言值。<br>例如，如果引擎使用 C++ 编写，则它往往使用 C++ 的 <code>true</code> 和 <code>false</code>（而不是引擎内部表示出来的 JavaScript 的 <code>true</code> 和 <code>false</code>）</p>\n<p>除了语言类型，规范同时使用<a href=\"https://tc39.es/ecma262/#sec-ecmascript-specification-types\">规范类型（specification types）</a>。规范类型是仅在规范中使用的，但不存在与 JavaScript 语言的类型。JavaScript 引擎不需要（但也可以）实现它们。<br>在本篇博客中，我们会了解规范类型之一 —— Record（以及其子类型 Completion Record）。</p>\n<h1>抽象操作</h1>\n<p><a href=\"https://tc39.es/ecma262/#sec-abstract-operations\">抽象操作</a>是语言规范中定义的函数。定义它们使得规范更简洁。Javascript 引擎并非必须在引擎中实现它们，它们也不能直接通过 Javascript 调用。</p>\n<h1>内部槽与内部方法</h1>\n<p><a href=\"https://tc39.es/ecma262/#sec-object-internal-methods-and-internal-slots\">内部槽与内部方法</a> 的名称会通过 <code>[[ ]]</code> 符号围住。</p>\n<p>内部槽（internal slots）是 JavaScript 对象的数据成员，或者是一个规范类型。它们用于存储一个对象的状态。<br>内部方法（internal methods）是 JavaScript 对象的成员函数。</p>\n<p>例如，每个 JavaScript 对象都有内部槽 <code>[[Prototype]]</code> 和内部方法 <code>[[GetOwnProperty]]</code>。</p>\n<p>内部槽和内部方法无法通过 JavaScript 访问。例如，你无法访问 <code>o.[[Prototype]]</code> 或调用 <code>o.[[GetOwnProperty]]</code>。JavaScript 引擎可以（但不是必须）实现它们以备内部使用。</p>\n<p>有时一些内部方法会直接委托同名抽象操作，例如普通对象的 <code>[[GetOwnProperty]]</code>：</p>\n<blockquote>\n<p><a href=\"https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-getownproperty-p\"><code>[[GetOwnProperty]](P)</code></a></p>\n<p>当给定属性键 <code>P</code>，调用 <code>O</code> 的内部方法 <code>[[GetOwnProperty]]</code>，将执行如下操作：</p>\n<ol>\n<li>返回 <code>! OrdinaryGetOwnProperty(O, P)</code></li>\n</ol>\n</blockquote>\n<p>（此处感叹号标记下节再谈）</p>\n<p><code>OrdinaryGetOwnProperty</code> 不是内部方法，因为它不是对象关联的方法；反之，它要操作的对象作为参数传入。</p>\n<p><code>OrdinaryGetOwnProperty</code> 被称为 “普通”（ordinary）因为它操作的是普通对象。ECMAScript 对象可以是<strong>普通的</strong>（ordinary）或<strong>异类的</strong>（exotic）。普通对象必须拥有含一系列默认行为方法的<strong>必要内部方法</strong>。一个对象如果违背这些默认行为，则它是异类的。</p>\n<p>我们最熟知的异类对象就是 <code>Array</code>，因为它的 <code>length</code> 属性违反默认行为：设置 length 属性可以从 <code>Array</code> 中移除元素。</p>\n<p>必要内部方法列于<a href=\"https://tc39.es/ecma262/#table-5\">此处</a>。</p>\n<h1>Completion records</h1>\n<p>那么上面提到的问号和感叹号是什么含义呢？为了理解它们，我们来看看 <a href=\"https://tc39.es/ecma262/#sec-completion-record-specification-type\">Completion Records</a>。</p>\n<p>Completion Record 是<a href=\"https://tc39.es/ecma262/#sec-ecmascript-specification-types\">规范类型</a>（仅在规范中定义）。JavaScript 引擎并非必须实现对应数据类型。</p>\n<p>Completion Record 是 “record” ——— 一种具有固定命名字段的数据类型。 Completion Record 则有三个字段：</p>\n<table>\n<thead>\n<tr>\n<th>名称</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>[[Type]]</code></td>\n<td>取 <code>normal</code>, <code>break</code>, <code>continue</code>, <code>return</code>, 或 <code>throw</code> 之一。 除 <code>normal</code> 外，其余皆为 <strong>abrupt completions</strong>.</td>\n</tr>\n<tr>\n<td><code>[[Value]]</code></td>\n<td>The value that was produced when the completion occurred, for example, the return value of a function or the exception (if one is thrown).</td>\n</tr>\n<tr>\n<td><code>[[Target]]</code></td>\n<td>Used for directed control transfers (not relevant for this blog post).</td>\n</tr>\n</tbody></table>\n<p>每个抽象操作隐式返回 Completion Record。即使其看起来似乎只是返回一个简单类型如 Boolean，实际上是隐式包装为一个 <code>[[Type]]</code> 为 <code>normal</code> 的 Completion Record。（见 <a href=\"https://tc39.es/ecma262/#sec-implicit-completion-values\">Implicit Completion Values</a>）</p>\n<p><strong>注意 1：</strong> 规范在此方面并不完全一致；某些辅助函数返回裸值，并且其返回值按原样使用，在提取 Completion Record 中的值时也不例外。这通常在上下文中显而易见。</p>\n<p><strong>注意 2：</strong> 规范作者正研究如何使 Completion Record 的处理更显式。</p>\n<p>如果算法抛出异常，则表示返回 <code>[[Type]]</code> 为 <code>throw</code> 的 Completion Record，其 <code>[[Value]]</code> 为异常对象。我们暂且忽略 <code>break</code>、<code>continue</code> 和 <code>return</code> 类型。</p>\n<blockquote>\n<p>译注：Completion Record 简称为 Completion，<code>[[Type]]</code> 为 <code>throw</code> 的 Completion Record 称为 Throw Completion，诸如此类。</p>\n</blockquote>\n<p><a href=\"https://tc39.es/ecma262/#sec-returnifabrupt\"><code>ReturnIfAbrupt(argument)</code></a> 意味着执行以下步骤：</p>\n<blockquote>\n<ol>\n<li>若 <code>argument</code> 是 abrupt，则返回 <code>argument</code></li>\n<li>将 <code>argument</code> 设置为 <code>argument.[[Value]]</code></li>\n</ol>\n</blockquote>\n<p>也就是说，我们检查一个 Completion Record；如果这是 abrupt completion，则我们立即返回。否则，我们从 Completion Record 中提取出值。</p>\n<p><code>ReturnIfAbrupt</code> 看似为函数调用，但实际上并非如此。它将导致出现了 <code>ReturnIfAbrupt()</code> 的函数返回，而不是 <code>ReturnIfAbrupt</code> 函数本身。其行为更像 C 语言中的宏。</p>\n<p>可按如下方式使用 <code>ReturnIfAbrupt</code>：</p>\n<blockquote>\n<ol>\n<li>令 <code>obj</code> 为 <code>Foo()</code>。（<code>obj</code> 是一个 Completion Record。）</li>\n<li><code>ReturnIfAbrupt(obj)</code></li>\n<li><code>Bar(obj)</code>。（如果我们仍在此处，则说明 <code>obj</code> 是从 Completion Record 中提取的值。）</li>\n</ol>\n</blockquote>\n<p>现在，<a href=\"https://tc39.es/ecma262/#sec-returnifabrupt-shorthands\">问号</a> 发挥了作用：<code>? Foo()</code> 等效于 <code>ReturnIfAbrupt(Foo())</code>。使用简写非常实用：我们无需每次都显式编写错误处理代码。</p>\n<p>同样，<code>Let val be ! Foo()</code> 等效于：</p>\n<blockquote>\n<ol>\n<li>令 <code>val</code> 为 <code>Foo()</code>。</li>\n<li>断言：<code>val</code> 不是 abrupt completion。</li>\n<li>将 <code>val</code> 设置为 <code>val.[[Value]]</code>。</li>\n</ol>\n</blockquote>\n<p>利用此知识，我们可以按如下方式重写 <code>Object.prototype.hasOwnProperty</code>：</p>\n<blockquote>\n<p><strong><code>Object.prototype.hasOwnProperty(V)</code></strong></p>\n<ol>\n<li>令 <code>P</code> 为 <code>ToPropertyKey(V)</code>。</li>\n<li>若 <code>P</code> 是 abrupt completion，则返回 <code>P</code></li>\n<li>将 <code>P</code> 设置为 <code>P.[[Value]]</code></li>\n<li>令 <code>O</code> 为 <code>ToObject(this value)</code>。</li>\n<li>若 <code>O</code> 是 abrupt completion，则返回 <code>O</code></li>\n<li>将 <code>O</code> 设置为 <code>O.[[Value]]</code></li>\n<li>令 <code>temp</code> 为 <code>HasOwnProperty(O, P)</code>。</li>\n<li>若 <code>temp</code> 是 abrupt completion，则返回 <code>temp</code></li>\n<li>令 <code>temp</code> 为 <code>temp.[[Value]]</code></li>\n<li>返回 <code>NormalCompletion(temp)</code></li>\n</ol>\n</blockquote>\n<p>…我们也可按如下方式重写 <code>HasOwnProperty</code>：</p>\n<blockquote>\n<p><strong><code>HasOwnProperty(O, P)</code></strong></p>\n<ol>\n<li>断言：<code>Type(O)</code> 是 <code>Object</code>。</li>\n<li>断言：<code>IsPropertyKey(P)</code> 为 <code>true</code>。</li>\n<li>令 <code>desc</code> 为 <code>O.[[GetOwnProperty]](P)</code>。</li>\n<li>如果 <code>desc</code> 是 abrupt completion，则返回 <code>desc</code></li>\n<li>将 <code>desc</code> 设置为 <code>desc.[[Value]]</code></li>\n<li>若 <code>desc</code> 是 <code>undefined</code>，则返回 <code>NormalCompletion(false)</code>。</li>\n<li>返回 <code>NormalCompletion(true)</code>。</li>\n</ol>\n</blockquote>\n<p>我们还可以不带感叹号，重写 <code>[[GetOwnProperty]]</code> 内部方法：</p>\n<blockquote>\n<p><strong><code>O.[[GetOwnProperty]]</code></strong></p>\n<ol>\n<li>令 <code>temp</code> 为 <code>OrdinaryGetOwnProperty(O, P)</code>。</li>\n<li>断言：<code>temp</code> 不是 abrupt completion。</li>\n<li>令 <code>temp</code> 为 <code>temp.[[Value]]</code>。</li>\n<li>返回 <code>NormalCompletion(temp)</code>。</li>\n</ol>\n</blockquote>\n<p>此处，我们假定 <code>temp</code> 是一个全新的临时变量，不会与任何其它变量冲突。</p>\n<p>我们还利用这样一个知识：当 <code>return</code> 语句返回除 Completion Record 以外的其他内容时，它会隐式包装在 <code>NormalCompletion</code> 中。</p>\n<h2>离题：<code>Return ? Foo()</code></h2>\n<p>规范使用此表示法：<code>Return ? Foo()</code> —— 此处问号是什么？</p>\n<p><code>Return ? Foo()</code> 展开为：</p>\n<blockquote>\n<ol>\n<li>令 <code>temp</code> 为 <code>Foo()</code>。</li>\n<li>如果 <code>temp</code> 是 abrupt completion，则返回 <code>temp</code>。</li>\n<li>将 <code>temp</code> 设置为 <code>temp.[[Value]]</code>。</li>\n<li>返回 <code>NormalCompletion(temp)</code>。</li>\n</ol>\n</blockquote>\n<p>这与 <code>Return Foo()</code> 相同；对于 abrupt 和 normal completions，其表现相同。</p>\n<p><code>Return ? Foo()</code> 仅出于规范编辑目的，以更明确地说明 <code>Foo</code> 返回一个 Completion Record。</p>\n<h1>断言（Asserts）</h1>\n<p>规范中的断言用于断言算法中的不变量。添加它们只是用于澄清，实现者并不需要添加 —— 即实现者无需检查断言。</p>\n<blockquote>\n<p>译注：这里的断言有点像标注类型。</p>\n</blockquote>\n<h1>Moving on</h1>\n<p>抽象操作委托给其它抽象操作（见下图），但是根据这篇博文，我们应该能够弄清楚他们做了什么。<br>我们将遇到 Property Descriptors，它仅仅是另一个规范类型。</p>\n<p><img src=\"https://wsrv.nl/?url=https://v8.dev/_img/understanding-ecmascript-part-1/call-graph.svg\" alt=\"\"></p>\n<p>从 <code>Object.prototype.hasOwnProperty</code> 开始的函数调用图</p>\n<h1>总结</h1>\n<p>We read through a simple method — <code>Object.prototype.hasOwnProperty</code> — and <strong>abstract operations</strong> it invokes. We familiarized ourselves with the shorthands <code>?</code> and <code>!</code> related to error handling. We encountered <strong>language types</strong>, <strong>specification types</strong>, <strong>internal slots</strong>, and <strong>internal methods</strong>.</p>\n<h1>Useful links</h1>\n<blockquote>\n<p><a href=\"https://timothygu.me/es-howto/\">How to Read the ECMAScript Specification</a>：从一个稍微不同的角度出发，包含了本篇中的大部分材料。<a href=\"https://github.com/Pines-Cheng/blog/issues/63\">中文翻译</a></p>\n</blockquote>\n","tags":["ecma262"]},{"id":"nodejs-express-next","url":"https://yieldray.fun/posts/nodejs-express-next","title":"Express之next()","date_published":"2024-03-04T18:44:44.000Z","date_modified":"2024-03-04T18:44:44.000Z","content_text":"<h1>定义</h1>\n<p>express 的 <code>next()</code> 函数类型如下</p>\n<pre><code class=\"language-ts\">export interface NextFunction {\n    (err?: any): void;\n    /**\n     * &quot;Break-out&quot; of a router by calling {next(&#39;router&#39;)};\n     * @see {https://expressjs.com/en/guide/using-middleware.html#middleware.router}\n     */\n    (deferToNext: &quot;router&quot;): void;\n    /**\n     * &quot;Break-out&quot; of a route by calling {next(&#39;route&#39;)};\n     * @see {https://expressjs.com/en/guide/using-middleware.html#middleware.application}\n     */\n    (deferToNext: &quot;route&quot;): void;\n}\n</code></pre>\n<h1>实现</h1>\n<p>express 的 <code>next()</code> 函数可能稍显复杂，不妨先看看 koa 的 <code>next()</code> 函数，通过 <a href=\"https://github.com/koajs/compose/blob/master/index.js\">koa-compose</a> 包实现（另见<a href=\"https://github.com/koajs/koa/blob/5f159415e58c42b54cb0703d7cfb10870d33d65f/lib/application.js#L154\">此处</a>），其代码比 express 简洁，并且更好地支持异步中间件</p>\n<pre><code class=\"language-js\">function (context, next) {\n    // last called middleware #\n    let index = -1\n    return dispatch(0)\n    function dispatch (i) {\n      if (i &lt;= index) return Promise.reject(new Error(&#39;next() called multiple times&#39;))\n      index = i\n      let fn = middleware[i]\n      if (i === middleware.length) fn = next // 若当前已运行所有中间件\n      if (!fn) return Promise.resolve() // 如果没有调用 next 函数，停止运行\n      try {\n        return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) // 运行下一个中间件\n      } catch (err) {\n        return Promise.reject(err)\n      }\n    }\n}\n</code></pre>\n<p>再简化（<code>next()</code> 函数无 err 参数）</p>\n<pre><code class=\"language-js\">const next = /* async */ () =&gt; {\n    let mw;\n    while (!mw &amp;&amp; mwArray.length &gt; 0) {\n        mw = mwArray.shift(); // 添加时 push()\n    }\n    /* await */ mw?.(req, res, next);\n};\n</code></pre>\n<p>注意： express 实际继承自 <code>EventEmitter</code>，就和 Node.js 中其它异步机制一样，不关心回调函数是否异步<br>对于 express，缺点在于中间件内部抛出的异常必须通过 <code>next()</code> 函数手动捕获</p>\n<p>express 的 <code>next()</code> 函数如下：<br><a href=\"https://github.com/expressjs/express/blob/master/lib/router/route.js\">https://github.com/expressjs/express/blob/master/lib/router/route.js</a></p>\n<pre><code class=\"language-js\">Route.prototype.dispatch = function dispatch(req, res, done) {\n    var idx = 0;\n    var stack = this.stack;\n    var sync = 0;\n\n    if (stack.length === 0) {\n        return done();\n    }\n    var method = typeof req.method === &quot;string&quot; ? req.method.toLowerCase() : req.method;\n\n    if (method === &quot;head&quot; &amp;&amp; !this.methods[&quot;head&quot;]) {\n        method = &quot;get&quot;;\n    }\n\n    req.route = this;\n\n    next();\n\n    function next(err) {\n        // signal to exit route\n        if (err &amp;&amp; err === &quot;route&quot;) {\n            return done();\n        }\n\n        // signal to exit router\n        if (err &amp;&amp; err === &quot;router&quot;) {\n            return done(err);\n        }\n\n        // max sync stack\n        if (++sync &gt; 100) {\n            return setImmediate(next, err);\n        }\n\n        var layer = stack[idx++];\n\n        // end of layers\n        if (!layer) {\n            return done(err);\n        }\n\n        if (layer.method &amp;&amp; layer.method !== method) {\n            next(err);\n        } else if (err) {\n            layer.handle_error(err, req, res, next);\n        } else {\n            layer.handle_request(req, res, next);\n        }\n\n        sync = 0;\n    }\n};\n</code></pre>\n","tags":["lib","node.js"]},{"id":"react-state-mgmt","url":"https://yieldray.fun/posts/react-state-mgmt","title":"React状态管理库","date_published":"2024-02-17T16:30:00.000Z","date_modified":"2024-02-17T16:30:00.000Z","content_text":"<p>在本文中，React 状态管理指的是管理 React 组件外部的状态。</p>\n<p>“状态”往往就是外部的数据源，状态管理库管理的数据源主要是（手动创建的）普通的 JavaScript 值（例如 WebSocket 就不包含了）。<br>状态管理库要做的就是当数据源变更时，触发组件重渲染，以达到同步内外数据的状态。</p>\n<p>状态管理库的原理，简单来说就是：要触发重渲染，调用 setState 函数；要同步外部数据，调用 useEffect 函数。（另见：<a href=\"https://www.joshwcomeau.com/react/why-react-re-renders/\">Why React Re-Renders</a>）</p>\n<p>现代库往往会顾虑到 SSR 的情况，使用 <a href=\"https://zh-hans.react.dev/reference/react/useSyncExternalStore\">useSyncExternalStore</a> 订阅数据源是一个很好的选择。</p>\n<p><del>TODO: 一些状态管理库，例如 <a href=\"https://react-redux.js.org\">React Redux</a> 会用到 <a href=\"https://zh-hans.react.dev/reference/react/createContext#provider\">Provider</a>。</del></p>\n<p>以下状态管理库不依赖 Context</p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>可变性</th>\n<th>原子性</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>zustand</td>\n<td>❎</td>\n<td>❎</td>\n</tr>\n<tr>\n<td>jotai</td>\n<td>❎</td>\n<td>✅</td>\n</tr>\n<tr>\n<td>valtio</td>\n<td>✅</td>\n<td>❎</td>\n</tr>\n</tbody></table>\n<blockquote>\n<p>下面简单介绍一下它们的核心 API<br>以下 TS 类型为了演示方便，<strong>可能是修改和简化过的</strong></p>\n</blockquote>\n<h1><a href=\"https://docs.pmnd.rs/zustand\">zustand</a></h1>\n<pre><code class=\"language-ts\">import React from &quot;react&quot;;\nimport { create } from &quot;zustand&quot;;\n\ninterface BearState {\n    bears: number;\n    increase: (by: number) =&gt; void;\n}\n\nconst useBearStore: UseBoundStore&lt;BearState&gt; = create&lt;BearState&gt;()(\n    (\n        setState: StoreApi&lt;BearState&gt;[&quot;setState&quot;],\n        getState: StoreApi&lt;BearState&gt;[&quot;getState&quot;],\n        storeApi: StoreApi&lt;BearState&gt;,\n    ) =&gt; ({\n        bears: 0,\n        increase: (by) =&gt; setState((state) =&gt; ({ bears: state.bears + by })),\n        removeAll: () =&gt; setState({ bears: 0 }),\n    }),\n);\n\nfunction BearCounter() {\n    const bears = useBearStore((state) =&gt; state.bears);\n    return React.createElement(&quot;h1&quot;, {}, `${bears} around here ...`);\n}\n\nfunction Controls() {\n    const increasePopulation = useBearStore((state) =&gt; state.increase);\n    return React.createElement(&quot;button&quot;, { onClick: increasePopulation }, &quot;one up&quot;);\n}\n\n// 类型\n\ntype UseBoundStore&lt;T&gt; = StoreApi&lt;T&gt; &amp; {\n    (): T;\n    &lt;U&gt;(selector: (state: T) =&gt; U): U;\n};\n\ninterface StoreApi&lt;T&gt; {\n    setState: (partial: MaybePartial&lt;T&gt; | ((state: T) =&gt; MaybePartial&lt;T&gt;), replace?: boolean | undefined) =&gt; void;\n    getState: () =&gt; T;\n    getInitialState: () =&gt; T;\n    subscribe: (listener: (state: T, prevState: T) =&gt; void) =&gt; VoidFunction;\n}\n\ntype MaybePartial&lt;T&gt; = T | Partial&lt;T&gt;;\n</code></pre>\n<p>另见：<a href=\"https://docs-zustand.surge.sh/modules.html\">https://docs-zustand.surge.sh/modules.html</a></p>\n<h1><a href=\"https://jotai.org\">jotai</a></h1>\n<pre><code class=\"language-ts\">// primitive atom\nfunction atom&lt;Value&gt;(initialValue: Value): PrimitiveAtom&lt;Value&gt; &amp; WithInitialValue&lt;Value&gt;;\n\n// read-only derived atom\nfunction atom&lt;Value&gt;(read: Read&lt;Value&gt;): Atom&lt;Value&gt;;\n\n// write-only derived atom\nfunction atom&lt;Value, Args extends unknown[], Result&gt;(\n    initialValue: Value, // it&#39;s a convention to pass `null` for the first argument\n    write: Write&lt;Args, Result&gt;,\n): WritableAtom&lt;Value, Args, Result&gt; &amp; WithInitialValue&lt;Value&gt;;\n\n// writable derived atom\nfunction atom&lt;Value, Args extends unknown[], Result&gt;(\n    read: Read&lt;Value&gt;,\n    write: Write&lt;Args, Result&gt;,\n): WritableAtom&lt;Value, Args, Result&gt;;\n\n// functions（简化）\ntype Read&lt;Value&gt; = (get: Getter) =&gt; Value;\n\ntype Write&lt;Args extends unknown[], Result&gt; = (get: Getter, set: Setter, ...args: Args) =&gt; Result;\n\ntype Getter = &lt;Value&gt;(atom: Value) =&gt; Value;\n\ntype Setter = &lt;Value, Args extends unknown[], Result&gt;(atom: Value, ...args: Args) =&gt; Result;\n</code></pre>\n<pre><code class=\"language-ts\">// primitive or writable derived atom\nfunction useAtom&lt;Value, Args extends unknown[], Result&gt;(\n    atom: WritableAtom&lt;Value, Args, Result&gt;,\n    options?: { store?: Store },\n): [Value, SetAtom&lt;Args, Result&gt;];\n\n// read-only atom\nfunction useAtom&lt;Value&gt;(atom: Atom&lt;Value&gt;, options?: { store?: Store }): [Value, never];\n\ntype SetAtom&lt;Args extends unknown[], Result&gt; = &lt;A extends Args&gt;(...args: A) =&gt; Result;\n</code></pre>\n<p>另见：<a href=\"https://docs-jotai.surge.sh/modules.html\">https://docs-jotai.surge.sh/modules.html</a></p>\n<h1><a href=\"https://valtio.pmnd.rs\">valtio</a></h1>\n<p>valtio 基于 Proxy，很容易让人联想到 <a href=\"https://github.com/vuejs/core/tree/main/packages/reactivity\">@vue/reactivity</a> <a href=\"https://paka.dev/npm/@vue/reactivity\">API</a><br>实际上是有很多区别的，只是心智模型类似</p>\n<p>注意：因为基于 Proxy，因此只能监听对象</p>\n<pre><code class=\"language-tsx\">import { proxy, useSnapshot } from &quot;valtio&quot;;\n\nconst state = proxy({ count: 0, meta: { text: &quot;hello&quot; } });\n\n// mutate from anywhere\nsetInterval(() =&gt; {\n    ++state.count;\n}, 1000);\n\nfunction Counter() {\n    const snap = useSnapshot(state);\n    const meta = useSnapshot(state.meta);\n    return (\n        &lt;div&gt;\n            &lt;span&gt;{meta.text}&lt;/span&gt;\n            &lt;span&gt;{snap.count}&lt;/span&gt;\n            &lt;button\n                onClick={() =&gt; {\n                    // also read from the state proxy in callbacks\n                    if (state.count &lt; 10) ++state.count;\n                }}\n            &gt;\n                +1\n            &lt;/button&gt;\n        &lt;/div&gt;\n    );\n}\n\n// 类型\n\nfunction proxy&lt;T extends object&gt;(initialObject?: T): T;\nfunction useSnapshot&lt;T extends object&gt;(\n    proxyObject: T,\n    options?: {\n        sync?: boolean;\n    },\n): DeepReadonly&lt;T&gt;;\n</code></pre>\n<p>另见：<a href=\"https://docs-valtio.surge.sh/modules.html\">https://docs-valtio.surge.sh/modules.html</a></p>\n","tags":["js","react"]},{"id":"nodejs-modules","url":"https://yieldray.fun/posts/nodejs-modules","title":"Node.js模块处理机制","date_published":"2024-02-10T23:00:00.000Z","date_modified":"2025-04-06T18:00:00.000Z","content_text":"<p>本文不讨论具体的模块解析识和加载步骤，<a href=\"https://nodejs.org/docs/latest/api/packages.html\">文档</a>对此有详细描述。</p>\n<h1><a href=\"https://nodejs.org/docs/latest/api/modules.html\">CommonJS modules</a></h1>\n<blockquote>\n<p><code>require()</code> <a href=\"https://nodejs.org/docs/latest/api/modules.html#all-together\">模块解析算法</a></p>\n</blockquote>\n<p>CommonJS 模块执行前会被如下函数包装：（<a href=\"https://github.com/nodejs/node/blob/852a1a6742cc7a941276a4def7960b3a9b9b098b/lib/internal/modules/cjs/loader.js#L328\">参见此处 Node.js 源码</a>）</p>\n<pre><code class=\"language-ts\">(function (\n    exports: Module[&quot;exports&quot;], // 指向 module.exports\n    require: Require,\n    module: Module, // 指向当前模块\n    __filename: string,\n    __dirname: string,\n) {\n    // 对 module.exports *同步*赋值，将值导出\n    // module.exports 默认值是一个空对象，exports 仅当包装模块时是对这个对象的引用\n    // 最终实际导出值是 module.exports，而不是 exports\n});\n</code></pre>\n<p>为了验证之，测试 arguments 变量即可</p>\n<pre><code class=\"language-js\">// test.cjs\nconsole.log(arguments);\n</code></pre>\n<hr>\n<p><code>require()</code> 函数定义如下</p>\n<pre><code class=\"language-ts\">interface Require {\n    (id: string): any;\n    resolve: RequireResolve;\n    cache: Dict&lt;NodeModule&gt;;\n    main: Module | undefined;\n}\n\ninterface RequireResolve {\n    (id: string, options?: { paths?: string[] | undefined }): string;\n    paths(request: string): string[] | null;\n}\n\ninterface Module {\n    isPreloading: boolean;\n    exports: any;\n    require: Require;\n    id: string;\n    filename: string;\n    loaded: boolean;\n    children: Module[];\n    path: string;\n    paths: string[];\n}\n</code></pre>\n<p><code>require()</code> 函数对于相同输入 id，仅导入（执行）一次（在第一次时缓存，缓存到 <code>require.cache</code> 对象中），重复导入将得到<em>相同的引用</em>（在缓存 <code>require.cache</code> 中读取）。<br>注意 cjs 导入的结果是普通的 JavaScript 值，即导出和导入得到的值是相同的。（esm 则有不同）<br>此外 Node.js 内置模块都是<em>可写的对象</em>，这就允许它们被替换。而由于导入的是相同的引用，因此修改是立即可见的。</p>\n<p>对于内置模块，如果不带 <code>node:</code> 前缀，也会尝试从 <code>require.cache</code> 中读取。<br>但若加上 <code>node:</code> 前缀则不会。因此 <code>node:</code> 前缀将保证能够获取到内置模块，但注意内置模块本身还是允许被修改的。</p>\n<pre><code class=\"language-js\">require(&quot;fs&quot;).write = (fd, str, cb) =&gt; {\n    console.log(str + &quot; &lt;patched&gt;&quot;);\n};\n\nrequire.cache.fs = {\n    exports: {\n        write() {\n            console.log(&quot;fake!&quot;);\n        },\n    },\n};\n\nrequire.cache[&quot;node:fs&quot;] = {\n    exports: {\n        write() {\n            console.log(&quot;fake!&quot;);\n        },\n    },\n};\n\n// 确保得到内置模块，但内置模块本身允许被修改\nconst fs1 = require(&quot;node:fs&quot;);\nfs1.write(1, &quot;Hi, fs1&quot;, (err) =&gt; {}); // =&gt; Hi, fs1 &lt;patched&gt;\n\n// 会先从缓存中读取\nconst fs2 = require(&quot;fs&quot;);\nfs2.write(1, &quot;Hi, fs2&quot;, (err) =&gt; {}); // =&gt; fake!\n</code></pre>\n<p>删除 <code>require.cache</code> 的键就可以令对应 id 重新加载导入</p>\n<h1><a href=\"https://nodejs.org/docs/latest/api/esm.html\">ECMAScript modules</a></h1>\n<blockquote>\n<p>ESM 模块除遵循语言规范外，还有 Node.js 的<a href=\"https://nodejs.org/docs/latest/api/esm.html#resolution-and-loading-algorithm\">模块解析算法</a><br>另：<a href=\"https://github.com/unjs/mlly\">mlly</a> 库可能会有帮助</p>\n</blockquote>\n<p><code>require()</code> 导入的值与对应 <code>module.exports</code> 导出的值完全相同。<br>但在 ECMAScript 标准模块中，当导入整个模块作为一个对象时，得到是一个 Module 对象（module namespace object）。<br>该对象是普通的 JavaScript 对象，但其原型为 <code>null</code>，其 <code>Symbol.toStringTag</code> 键对应值为字符串 <code>Module</code>。<br>export 导出的对应键，在该对象中都是不可写的。</p>\n<pre><code class=\"language-js\">import * as m from &quot;./esm.js&quot;;\nconst m = await import(&quot;./esm.js&quot;);\n\nObject.getPrototypeOf(m) // =&gt; null\n\n// m 相当于如下\n{\n    [Symbol.toStringTag]: &#39;Module&#39;\n    // ... 包含其它 export 导出的对应键\n}\n</code></pre>\n<p>Node.js 模块的 ESM 导出，是命名导出所有 API，并将 CommonJS 导出值以 default 导出。<br>这种方式导出方式就很方便了，因为：</p>\n<pre><code class=\"language-js\">// 无需（当然也可以）\nimport * as fs from &quot;fs&quot;;\n// 只需（因为有 default 导出）\nimport fs from &quot;fs&quot;;\n</code></pre>\n<p>以第一种导入方式时，根据 ESM 规范，该对象的键当然是不可写的。<br>第二种方式导入的相当于 <code>require()</code> 返回值，因此其键可写。</p>\n<p>default 导出值就是 CommonJS 导出值。因此只有修改 default 导出值，才能在 ESM 环境中修改内置模块，<br>但需调用特殊的 <a href=\"https://nodejs.org/docs/latest/api/module.html#modulesyncbuiltinesmexports\"><code>syncBuiltinESMExports</code></a> 函数，才能传播修改。</p>\n<pre><code class=\"language-js\">import fs, { readFileSync } from &quot;node:fs&quot;;\nimport { syncBuiltinESMExports } from &quot;node:module&quot;;\nimport { Buffer } from &quot;node:buffer&quot;;\n\nfs.readFileSync = () =&gt; Buffer.from(&quot;Hello, ESM&quot;);\nsyncBuiltinESMExports();\n\nfs.readFileSync === readFileSync; // =&gt; true\n</code></pre>\n<p>对于我们自己编写的 CJS 模块，根据<a href=\"https://nodejs.org/api/esm.html#interoperability-with-commonjs\">文档</a>：<br>ESM 环境导入 CJS 模块时，module.exports 对象提供为 default 导出。为了更好的兼容性，会<a href=\"https://github.com/nodejs/cjs-module-lexer\">静态分析</a>来提供命名导出。</p>\n<h1><a href=\"https://nodejs.org/api/esm.html#interoperability-with-commonjs\">ESM 与 CJS 互操性</a></h1>\n<p>Node.js 需要确定脚本是 ESM 还是 CJS，参见<a href=\"https://nodejs.org/api/esm.html#enabling\">此处</a>。需要注意 Node.js 对这些特性的支持版本。</p>\n<hr>\n<p>ESM 环境没有 require 函数，通过以下方法得到：</p>\n<pre><code class=\"language-js\">import { createRequire } from &quot;node:module&quot;;\nconst require = createRequire(import.meta.url);\n</code></pre>\n<hr>\n<p>目前，Node.js 已实验性支持<a href=\"https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require\">通过 require() 加载 ES 模块</a><br>模块加载结果与 <code>await import()</code> 一致。限制条件为 ES 模块必须完全同步，即不含 top-level await</p>\n<h1><a href=\"https://www.typescriptlang.org/docs/handbook/modules/reference\">Typescript</a></h1>\n<h2><a href=\"https://www.typescriptlang.org/tsconfig#moduleResolution\">moduleResolution</a> 选项</h2>\n<p><a href=\"https://www.typescriptlang.org/tsconfig#module\">module</a> 选项决定编译器输出的模块类型，moduleResolution 决定如何解析输入的模块。</p>\n<p>对于现代 Node.js 应用，使用 tsc 编译时，指定为 <code>nodenext</code> 即可。此时会仿照 Node.js 模块解析方式。<br>对于浏览器环境，往往真正是由其它 bundler 来解析依赖，因此指定为 <code>bundler</code>。此时允许省略文件名后缀。</p>\n<h2><a href=\"https://www.typescriptlang.org/tsconfig#allowSyntheticDefaultImports\">allowSyntheticDefaultImports</a> 和 <a href=\"https://www.typescriptlang.org/tsconfig#esModuleInterop\">esModuleInterop</a> 选项</h2>\n<p>typescript 本身是根据 declaration 来识别导出的，这就可能导致 default 导入出现问题。</p>\n<p>这些选项可处理此问题，具体参见<a href=\"https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html\">handbook</a>和对应文档。</p>\n<h1>bundler</h1>\n<p>在 ESM 规范中，import 不能省略文件名后缀（毕竟浏览器是网络请求，而不是本地文件系统），但很多 bundler 在解析 import 时并不会这么严格。</p>\n<h2>rollup</h2>\n<p>见 <code>@rollup/plugin-commonjs</code> 包 <a href=\"https://github.com/rollup/plugins/tree/master/packages/commonjs/#extensions\">extensions</a> 选项</p>\n<h2>webpack</h2>\n<p>见 <a href=\"https://webpack.js.org/configuration/resolve/#resolveextensions\">resolve.extensions</a></p>\n<h2>esbuild</h2>\n<p>见 <a href=\"https://esbuild.github.io/api/#resolve-extensions\">resolve extensions</a></p>\n<h1>CommonJS modules 实现</h1>\n<blockquote>\n<p>WIP</p>\n</blockquote>\n<pre><code class=\"language-js\">/**\n * Create a new module instance.\n * @param {string} id\n * @param {Module} parent\n */\nfunction Module(id = &quot;&quot;, parent) {\n    this.id = id;\n    this.path = path.dirname(id);\n    setOwnProperty(this, &quot;exports&quot;, {});\n    this[kModuleParent] = parent;\n    updateChildren(parent, this, false);\n    this.filename = null;\n    this.loaded = false;\n    this.children = [];\n}\n\n/** @type {Record&lt;string, Module&gt;} */\nModule._cache = { __proto__: null };\n/** @type {Record&lt;string, string&gt;} */\nModule._pathCache = { __proto__: null };\n/** @type {Record&lt;string, (module: Module, filename: string) =&gt; void&gt;} */\nModule._extensions = { __proto__: null };\n/** @type {string[]} */\nlet modulePaths = [];\n/** @type {string[]} */\nModule.globalPaths = [];\n\nlet patched = false;\n\n/**\n * Add the CommonJS wrapper around a module&#39;s source code.\n * @param {string} script Module source code\n */\nlet wrap = function (script) {\n    // eslint-disable-line func-style\n    return Module.wrapper[0] + script + Module.wrapper[1];\n};\n\nconst wrapper = [&quot;(function (exports, require, module, __filename, __dirname) { &quot;, &quot;\\n});&quot;];\n\nlet wrapperProxy = new Proxy(wrapper, {\n    __proto__: null,\n\n    set(target, property, value, receiver) {\n        patched = true;\n        return ReflectSet(target, property, value, receiver);\n    },\n\n    defineProperty(target, property, descriptor) {\n        patched = true;\n        return ObjectDefineProperty(target, property, descriptor);\n    },\n});\n\nObjectDefineProperty(Module, &quot;wrap&quot;, {\n    __proto__: null,\n    get() {\n        return wrap;\n    },\n\n    set(value) {\n        patched = true;\n        wrap = value;\n    },\n});\n\nObjectDefineProperty(Module, &quot;wrapper&quot;, {\n    __proto__: null,\n    get() {\n        return wrapperProxy;\n    },\n\n    set(value) {\n        patched = true;\n        wrapperProxy = value;\n    },\n});\n</code></pre>\n<pre><code class=\"language-js\">/**\n * Wraps the given content in a script and runs it in a new context.\n * @param {string} filename The name of the file being loaded\n * @param {string} content The content of the file being loaded\n * @param {Module|undefined} cjsModuleInstance The CommonJS loader instance\n * @param {&#39;commonjs&#39;|undefined} format Intended format of the module.\n */\nfunction wrapSafe(filename, content, cjsModuleInstance, format) {\n    assert(format !== &quot;module&quot;, &quot;ESM should be handled in loadESMFromCJS()&quot;);\n    const hostDefinedOptionId = vm_dynamic_import_default_internal;\n    const importModuleDynamically = vm_dynamic_import_default_internal;\n    if (patched) {\n        const wrapped = Module.wrap(content);\n        const script = makeContextifyScript(\n            wrapped, // code\n            filename, // filename\n            0, // lineOffset\n            0, // columnOffset\n            undefined, // cachedData\n            false, // produceCachedData\n            undefined, // parsingContext\n            hostDefinedOptionId, // hostDefinedOptionId\n            importModuleDynamically, // importModuleDynamically\n        );\n\n        // Cache the source map for the module if present.\n        const { sourceMapURL } = script;\n        if (sourceMapURL) {\n            maybeCacheSourceMap(filename, content, cjsModuleInstance, false, undefined, sourceMapURL);\n        }\n\n        return {\n            __proto__: null,\n            function: runScriptInThisContext(script, true, false), // 关键函数\n            sourceMapURL,\n        };\n    }\n\n    let shouldDetectModule = false;\n    if (format !== &quot;commonjs&quot;) {\n        if (cjsModuleInstance?.[kIsMainSymbol]) {\n            // For entry points, format detection is used unless explicitly disabled.\n            shouldDetectModule = getOptionValue(&quot;--experimental-detect-module&quot;);\n        } else {\n            // For modules being loaded by `require()`, if require(esm) is disabled,\n            // don&#39;t try to reparse to detect format and just throw for ESM syntax.\n            shouldDetectModule = getOptionValue(&quot;--experimental-require-module&quot;);\n        }\n    }\n    const result = compileFunctionForCJSLoader(content, filename, false /* is_sea_main */, shouldDetectModule);\n\n    // Cache the source map for the module if present.\n    if (result.sourceMapURL) {\n        maybeCacheSourceMap(filename, content, cjsModuleInstance, false, undefined, result.sourceMapURL);\n    }\n\n    return result;\n}\n\n/**\n * Run the file contents in the correct scope or sandbox. Expose the correct helper variables (`require`, `module`,\n * `exports`) to the file. Returns exception, if any.\n * @param {string} content The source code of the module\n * @param {string} filename The file path of the module\n * @param {\n *    &#39;module&#39;|&#39;commonjs&#39;|&#39;commonjs-typescript&#39;|&#39;module-typescript&#39;\n * } format Intended format of the module.\n */\nModule.prototype._compile = function (content, filename, format) {\n    // 注：node22 通过 @swc/wasm-typescript 支持typescript，这里做类型擦除\n    if (format === &quot;commonjs-typescript&quot; || format === &quot;module-typescript&quot; || format === &quot;typescript&quot;) {\n        content = stripTypeScriptModuleTypes(content, filename);\n        switch (format) {\n            case &quot;commonjs-typescript&quot;: {\n                format = &quot;commonjs&quot;;\n                break;\n            }\n            case &quot;module-typescript&quot;: {\n                format = &quot;module&quot;;\n                break;\n            }\n            // If the format is still unknown i.e. &#39;typescript&#39;, detect it in\n            // wrapSafe using the type-stripped source.\n            default:\n                format = undefined;\n                break;\n        }\n    }\n\n    let redirects;\n\n    let compiledWrapper;\n    if (format !== &quot;module&quot;) {\n        // 默认当作 commonjs 执行\n        // 通过 wrapSafe，将给定源码生成函数\n        const result = wrapSafe(filename, content, this, format);\n        compiledWrapper = result.function;\n        if (result.canParseAsESM) {\n            format = &quot;module&quot;;\n        }\n    }\n\n    if (format === &quot;module&quot;) {\n        loadESMFromCJS(this, filename, format, content);\n        return;\n    }\n\n    // 创建包装函数所需参数\n    const dirname = path.dirname(filename);\n    const require = makeRequireFunction(this, redirects);\n    let result;\n    const exports = this.exports;\n    const thisValue = exports;\n    const module = this;\n    if (requireDepth === 0) {\n        statCache = new SafeMap();\n    }\n    setHasStartedUserCJSExecution();\n    this[kIsExecuting] = true;\n    if (this[kIsMainSymbol] &amp;&amp; getOptionValue(&quot;--inspect-brk&quot;)) {\n        const { callAndPauseOnStart } = internalBinding(&quot;inspector&quot;);\n        result = callAndPauseOnStart(compiledWrapper, thisValue, exports, require, module, filename, dirname);\n    } else {\n        result = ReflectApply(compiledWrapper, thisValue, [exports, require, module, filename, dirname]);\n    }\n    this[kIsExecuting] = false;\n    if (requireDepth === 0) {\n        statCache = null;\n    }\n    return result;\n};\n</code></pre>\n<pre><code class=\"language-js\">/**\n * Runs a script in the current context.\n * Internal version of `vm.Script.prototype.runInThisContext()` which skips argument validation.\n * @param {ReturnType&lt;makeContextifyScript&gt;} script - The script to run.\n * @param {boolean} displayErrors - Whether to display errors.\n * @param {boolean} breakOnFirstLine - Whether to break on the first line.\n */\nfunction runScriptInThisContext(script, displayErrors, breakOnFirstLine) {\n    return ReflectApply(runInContext, script, [\n        null, // sandbox - use current context\n        -1, // timeout\n        displayErrors, // displayErrors\n        false, // breakOnSigint\n        breakOnFirstLine, // breakOnFirstLine\n    ]);\n}\n</code></pre>\n<pre><code class=\"language-js\">function runInContext(code, contextifiedObject, options) {\n    validateContext(contextifiedObject);\n    if (typeof options === &quot;string&quot;) {\n        options = {\n            filename: options,\n            [kParsingContext]: contextifiedObject,\n        };\n    } else {\n        options = { ...options, [kParsingContext]: contextifiedObject };\n    }\n    return createScript(code, options).runInContext(contextifiedObject, options);\n}\n\nfunction createScript(code, options) {\n    return new Script(code, options);\n}\n\nclass Script extends ContextifyScript {\n    // 省略\n}\n</code></pre>\n<p>ContextifyScript 本质是一个 c++ 对象</p>\n<pre><code class=\"language-cpp\">class ContextifyScript final : CPPGC_MIXIN(ContextifyScript) {\n public:\n  SET_CPPGC_NAME(ContextifyScript)\n  void Trace(cppgc::Visitor* visitor) const final;\n\n  ContextifyScript(Environment* env, v8::Local&lt;v8::Object&gt; object);\n  ~ContextifyScript() override;\n\n  v8::Local&lt;v8::UnboundScript&gt; unbound_script() const;\n  void set_unbound_script(v8::Local&lt;v8::UnboundScript&gt;);\n\n  static void CreatePerIsolateProperties(IsolateData* isolate_data,\n                                         v8::Local&lt;v8::ObjectTemplate&gt; target);\n  static void RegisterExternalReferences(ExternalReferenceRegistry* registry);\n  static ContextifyScript* New(Environment* env, v8::Local&lt;v8::Object&gt; object);\n  static void New(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static bool InstanceOf(Environment* env, const v8::Local&lt;v8::Value&gt;&amp; args);\n  static void CreateCachedData(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void RunInContext(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static bool EvalMachine(v8::Local&lt;v8::Context&gt; context,\n                          Environment* env,\n                          const int64_t timeout,\n                          const bool display_errors,\n                          const bool break_on_sigint,\n                          const bool break_on_first_line,\n                          v8::MicrotaskQueue* microtask_queue,\n                          const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n\n private:\n  v8::TracedReference&lt;v8::UnboundScript&gt; script_;\n};\n</code></pre>\n<h1>ECMAScript modules 实现</h1>\n<pre><code class=\"language-js\">function createModuleLoader() {\n    let customizations = null;\n    // Don&#39;t spawn a new worker if custom loaders are disabled. For instance, if\n    // we&#39;re already in a worker thread created by instantiating\n    // CustomizedModuleLoader; doing so would cause an infinite loop.\n    if (!forceDefaultLoader()) {\n        const userLoaderPaths = getOptionValue(&quot;--experimental-loader&quot;);\n        if (userLoaderPaths.length &gt; 0) {\n            if (!emittedLoaderFlagWarning) {\n                const readableURIEncode = (string) =&gt;\n                    ArrayPrototypeReduce(\n                        [\n                            [/&#39;/g, &quot;%27&quot;], // We need to URL-encode the single quote as it&#39;s the delimiter for the --import flag.\n                            [/%22/g, &#39;&quot;&#39;], // We can decode the double quotes to improve readability.\n                            [/%2F/gi, &quot;/&quot;], // We can decode the slashes to improve readability.\n                        ],\n                        (str, { 0: regex, 1: replacement }) =&gt;\n                            RegExpPrototypeSymbolReplace(hardenRegExp(regex), str, replacement),\n                        encodeURIComponent(string),\n                    );\n                process.emitWarning(\n                    &quot;`--experimental-loader` may be removed in the future; instead use `register()`:\\n&quot; +\n                        `--import &#39;data:text/javascript,import { register } from &quot;node:module&quot;; import { pathToFileURL } from &quot;node:url&quot;; ${ArrayPrototypeJoin(\n                            ArrayPrototypeMap(\n                                userLoaderPaths,\n                                (loader) =&gt;\n                                    `register(${readableURIEncode(JSONStringify(loader))}, pathToFileURL(&quot;./&quot;))`,\n                            ),\n                            &quot;; &quot;,\n                        )};&#39;`,\n                    &quot;ExperimentalWarning&quot;,\n                );\n                emittedLoaderFlagWarning = true;\n            }\n            customizations = new CustomizedModuleLoader();\n        }\n    }\n\n    return new ModuleLoader(customizations);\n}\n</code></pre>\n<ol>\n<li><p><strong>注册(Register):</strong> 可选步骤，用于注册自定义加载器。通过 <code>module.exports.register</code> 或命令行参数 <code>--experimental-loader</code> 实现。自定义加载器可以拦截和修改默认的加载流程。</p>\n</li>\n<li><p><strong>解析(Resolve):</strong> 将模块标识符（specifier）解析为模块的 URL。核心函数包括：</p>\n<ul>\n<li><code>ModuleLoader.resolve(specifier, parentURL, importAttributes)</code>: 入口函数，处理自定义 <code>resolve</code> 钩子。</li>\n<li><code>ModuleLoader.resolveSync(specifier, parentURL, importAttributes)</code>: 同步版本，用于 <code>import.meta.resolve</code> 和在 CJS 中 <code>require</code> ESM。</li>\n<li><code>ModuleLoader.defaultResolve(originalSpecifier, parentURL, importAttributes)</code>: 默认的同步解析逻辑，如果没有自定义钩子则使用它。内部使用 <code>internal/modules/esm/resolve.defaultResolve</code>。</li>\n<li><code>ModuleLoader.#resolveAndMaybeBlockOnLoaderThread</code>: 在同步解析中处理异步钩子，必要时阻塞主线程。</li>\n</ul>\n</li>\n<li><p><strong>加载(Load):</strong> 根据解析得到的 URL 加载模块的源代码。核心函数包括：</p>\n<ul>\n<li><code>ModuleLoader.load(url, context)</code>: 入口函数，处理自定义 <code>load</code> 钩子。</li>\n<li><code>ModuleLoader.#loadSync(url, context)</code>: 同步版本，用于在 CJS 中 <code>require</code> ESM。</li>\n<li><code>ModuleLoader.#loadAndMaybeBlockOnLoaderThread</code>: 在同步加载中处理异步钩子，必要时阻塞主线程。</li>\n<li>默认加载函数：<code>internal/modules/esm/load</code> <code>defaultLoad</code> (异步) 和 <code>internal/modules/esm/load</code> <code>defaultLoadSync</code> (同步)。</li>\n</ul>\n</li>\n<li><p><strong>翻译(Translate):</strong> 将加载的源代码转换为可执行的模块包装对象（ModuleWrap）。核心函数：</p>\n<ul>\n<li><code>ModuleLoader.#translate(url, format, source, isMain)</code>: 根据模块格式选择合适的翻译器进行转换。翻译器定义在 <code>internal/modules/esm/translators</code> 中。</li>\n</ul>\n</li>\n<li><p><strong>实例化(Instantiate)和求值(Evaluate):</strong> 创建模块实例，执行模块代码，并将导出的值存储在命名空间对象中。核心类：</p>\n<ul>\n<li><code>ModuleJob</code>: 异步加载和执行模块。</li>\n<li><code>ModuleJobSync</code>: 同步加载和执行模块，用于在 CJS 中 <code>require</code> ESM。</li>\n</ul>\n<blockquote>\n<p>定义在 <code>/lib/internal/modules/esm/module_job.js</code></p>\n</blockquote>\n</li>\n<li><p><strong>缓存(Cache):</strong> Node.js 会缓存已解析和加载的模块。<code>ModuleLoader</code> 的 <code>resolveCache</code> 和 <code>loadCache</code> 属性用于存储缓存结果。</p>\n</li>\n</ol>\n<div class=\"markdown-alert markdown-alert-caution\">\n<p class=\"markdown-alert-title\"><svg class=\"octicon octicon-stop mr-2\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>\n<p>以最新源码为准，贴出来的可能过时了</p>\n</div>\n<pre><code class=\"language-js\">/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of\n * its dependencies, over time. */\nclass ModuleJob extends ModuleJobBase {\n    #loader = null;\n\n    /**\n     * @param {ModuleLoader} loader The ESM loader.\n     * @param {string} url URL of the module to be wrapped in ModuleJob.\n     * @param {ImportAttributes} importAttributes Import attributes from the import statement.\n     * @param {ModuleWrap|Promise&lt;ModuleWrap&gt;} moduleOrModulePromise Translated ModuleWrap for the module.\n     * @param {boolean} isMain Whether the module is the entry point.\n     * @param {boolean} inspectBrk Whether this module should be evaluated with the\n     *                             first line paused in the debugger (because --inspect-brk is passed).\n     * @param {boolean} isForRequireInImportedCJS Whether this is created for require() in imported CJS.\n     */\n    constructor(\n        loader,\n        url,\n        importAttributes = { __proto__: null },\n        moduleOrModulePromise,\n        isMain,\n        inspectBrk,\n        isForRequireInImportedCJS = false,\n    ) {\n        super(url, importAttributes, isMain, inspectBrk);\n        this.#loader = loader;\n\n        // Expose the promise to the ModuleWrap directly for linking below.\n        if (isForRequireInImportedCJS) {\n            this.module = moduleOrModulePromise;\n            assert(this.module instanceof ModuleWrap);\n            this.modulePromise = PromiseResolve(this.module);\n        } else {\n            this.modulePromise = moduleOrModulePromise;\n        }\n        //! 注意：ModuleWrap 由 C++ 实现，参见下文\n        //! this.module 指向一个 C++ 对象\n\n        // Promise for the list of all dependencyJobs.\n        this.linked = this._link();\n        // This promise is awaited later anyway, so silence\n        // &#39;unhandled rejection&#39; warnings.\n        PromisePrototypeThen(this.linked, undefined, noop);\n\n        // instantiated == deep dependency jobs wrappers are instantiated,\n        // and module wrapper is instantiated.\n        this.instantiated = undefined;\n    }\n\n    /**\n     * Iterates the module requests and links with the loader.\n     * @returns {Promise&lt;ModuleJob[]&gt;} Dependency module jobs.\n     */\n    async _link() {\n        this.module = await this.modulePromise;\n        assert(this.module instanceof ModuleWrap);\n\n        const moduleRequests = this.module.getModuleRequests();\n        // Explicitly keeping track of dependency jobs is needed in order\n        // to flatten out the dependency graph below in `_instantiate()`,\n        // so that circular dependencies can&#39;t cause a deadlock by two of\n        // these `link` callbacks depending on each other.\n        // Create an ArrayLike to avoid calling into userspace with `.then`\n        // when returned from the async function.\n        const dependencyJobs = Array(moduleRequests.length);\n        ObjectSetPrototypeOf(dependencyJobs, null);\n\n        // Specifiers should be aligned with the moduleRequests array in order.\n        const specifiers = Array(moduleRequests.length);\n        const modulePromises = Array(moduleRequests.length);\n        // Iterate with index to avoid calling into userspace with `Symbol.iterator`.\n        for (let idx = 0; idx &lt; moduleRequests.length; idx++) {\n            const { specifier, attributes } = moduleRequests[idx];\n            // TODO(joyeecheung): resolve all requests first, then load them in another\n            // loop so that hooks can pre-fetch sources off-thread.\n            const dependencyJobPromise = this.#loader.getModuleJobForImport(specifier, this.url, attributes);\n            const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) =&gt; {\n                debug(`async link() ${this.url} -&gt; ${specifier}`, job);\n                dependencyJobs[idx] = job;\n                return job.modulePromise;\n            });\n            modulePromises[idx] = modulePromise;\n            specifiers[idx] = specifier;\n        }\n\n        const modules = await SafePromiseAllReturnArrayLike(modulePromises);\n        this.module.link(specifiers, modules);\n\n        return dependencyJobs;\n    }\n\n    instantiate() {\n        if (this.instantiated === undefined) {\n            this.instantiated = this._instantiate();\n        }\n        return this.instantiated;\n    }\n\n    async _instantiate() {\n        const jobsInGraph = new SafeSet();\n        const addJobsToDependencyGraph = async (moduleJob) =&gt; {\n            debug(`async addJobsToDependencyGraph() ${this.url}`, moduleJob);\n\n            if (jobsInGraph.has(moduleJob)) {\n                return;\n            }\n            jobsInGraph.add(moduleJob);\n            const dependencyJobs = await moduleJob.linked;\n            return SafePromiseAllReturnVoid(dependencyJobs, addJobsToDependencyGraph);\n        };\n        await addJobsToDependencyGraph(this);\n\n        try {\n            if (!hasPausedEntry &amp;&amp; this.inspectBrk) {\n                hasPausedEntry = true;\n                const initWrapper = internalBinding(&quot;inspector&quot;).callAndPauseOnStart;\n                initWrapper(this.module.instantiate, this.module);\n            } else {\n                this.module.instantiate();\n            }\n        } catch (e) {\n            decorateErrorStack(e);\n            // TODO(@bcoe): Add source map support to exception that occurs as result\n            // of missing named export. This is currently not possible because\n            // stack trace originates in module_job, not the file itself. A hidden\n            // symbol with filename could be set in node_errors.cc to facilitate this.\n            if (\n                !getSourceMapsSupport().enabled &amp;&amp;\n                StringPrototypeIncludes(e.message, &quot; does not provide an export named&quot;)\n            ) {\n                const splitStack = StringPrototypeSplit(e.stack, &quot;\\n&quot;);\n                const parentFileUrl = RegExpPrototypeSymbolReplace(/:\\d+$/, splitStack[0], &quot;&quot;);\n                const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(\n                    /module &#39;(.*)&#39; does not provide an export named &#39;(.+)&#39;/,\n                    e.message,\n                );\n                const { url: childFileURL } = await this.#loader.resolve(childSpecifier, parentFileUrl, kEmptyObject);\n                let format;\n                try {\n                    // This might throw for non-CommonJS modules because we aren&#39;t passing\n                    // in the import attributes and some formats require them; but we only\n                    // care about CommonJS for the purposes of this error message.\n                    ({ format } = await this.#loader.load(childFileURL));\n                } catch {\n                    // Continue regardless of error.\n                }\n\n                if (format === &quot;commonjs&quot;) {\n                    const importStatement = splitStack[1];\n                    // TODO(@ctavan): The original error stack only provides the single\n                    // line which causes the error. For multi-line import statements we\n                    // cannot generate an equivalent object destructuring assignment by\n                    // just parsing the error stack.\n                    const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement);\n                    const destructuringAssignment =\n                        oneLineNamedImports &amp;&amp; RegExpPrototypeSymbolReplace(/\\s+as\\s+/g, oneLineNamedImports, &quot;: &quot;);\n                    e.message =\n                        `Named export &#39;${name}&#39; not found. The requested module` +\n                        ` &#39;${childSpecifier}&#39; is a CommonJS module, which may not support` +\n                        &quot; all module.exports as named exports.\\nCommonJS modules can &quot; +\n                        &quot;always be imported via the default export, for example using:&quot; +\n                        `\\n\\nimport pkg from &#39;${childSpecifier}&#39;;\\n${\n                            destructuringAssignment ? `const ${destructuringAssignment} = pkg;\\n` : &quot;&quot;\n                        }`;\n                    const newStack = StringPrototypeSplit(e.stack, &quot;\\n&quot;);\n                    newStack[3] = `SyntaxError: ${e.message}`;\n                    e.stack = ArrayPrototypeJoin(newStack, &quot;\\n&quot;);\n                }\n            }\n            throw e;\n        }\n\n        for (const dependencyJob of jobsInGraph) {\n            // Calling `this.module.instantiate()` instantiates not only the\n            // ModuleWrap in this module, but all modules in the graph.\n            dependencyJob.instantiated = resolvedPromise;\n        }\n    }\n\n    runSync() {\n        assert(this.module instanceof ModuleWrap);\n        if (this.instantiated !== undefined) {\n            return { __proto__: null, module: this.module };\n        }\n\n        this.module.instantiate();\n        this.instantiated = PromiseResolve();\n        const timeout = -1;\n        const breakOnSigint = false;\n        setHasStartedUserESMExecution();\n        this.module.evaluate(timeout, breakOnSigint);\n        return { __proto__: null, module: this.module };\n    }\n\n    async run(isEntryPoint = false) {\n        await this.instantiate();\n        if (isEntryPoint) {\n            globalThis[entry_point_module_private_symbol] = this.module;\n        }\n        const timeout = -1;\n        const breakOnSigint = false;\n        setHasStartedUserESMExecution();\n        try {\n            await this.module.evaluate(timeout, breakOnSigint);\n        } catch (e) {\n            if (e?.name === &quot;ReferenceError&quot; &amp;&amp; isCommonJSGlobalLikeNotDefinedError(e.message)) {\n                e.message += &quot; in ES module scope&quot;;\n\n                if (StringPrototypeStartsWith(e.message, &quot;require &quot;)) {\n                    e.message += &quot;, you can use import instead&quot;;\n                }\n\n                const packageConfig =\n                    StringPrototypeStartsWith(this.module.url, &quot;file://&quot;) &amp;&amp;\n                    RegExpPrototypeExec(/\\.js(\\?[^#]*)?(#.*)?$/, this.module.url) !== null &amp;&amp;\n                    require(&quot;internal/modules/package_json_reader&quot;).getPackageScopeConfig(this.module.url);\n                if (packageConfig.type === &quot;module&quot;) {\n                    e.message +=\n                        &quot;\\nThis file is being treated as an ES module because it has a &quot; +\n                        `&#39;.js&#39; file extension and &#39;${packageConfig.pjsonPath}&#39; contains ` +\n                        &#39;&quot;type&quot;: &quot;module&quot;. To treat it as a CommonJS script, rename it &#39; +\n                        &quot;to use the &#39;.cjs&#39; file extension.&quot;;\n                }\n            }\n            throw e;\n        }\n        return { __proto__: null, module: this.module };\n    }\n}\n</code></pre>\n<p>js 侧实现的 <code>ModuleJob</code> 类的主要职责是：</p>\n<ul>\n<li>链接 (Linking): 解析模块的依赖项，创建 <code>ModuleJob</code> 实例来加载这些依赖项，并将它们与当前模块链接起来。</li>\n<li>实例化 (Instantiation): 准备模块及其依赖项的执行环境。</li>\n<li>执行 (Execution): 实际运行模块的代码。</li>\n</ul>\n<hr>\n<p>观察源码易知，<code>ModuleWrap</code> 是真正由 v8 执行模块的类，本质是一个 c++ 对象。（下面只给出头文件源码）</p>\n<p>它在 Node.js 内部表示一个 V8 模块。 它是一个包装器，用于封装 V8 引擎中的模块对象。\n它提供了与 V8 交互的接口，允许 Node.js 控制模块的生命周期。</p>\n<p>关键方法：</p>\n<ul>\n<li>getModuleRequests(): 返回模块的导入请求（依赖项）。</li>\n<li>link(specifiers, modules): 将模块的依赖项（其他 <code>ModuleWrap</code> 实例）链接到当前模块。这在 V8 内部创建模块之间的依赖关系图。</li>\n<li>instantiate(): 在 V8 中实例化模块。 这会准备模块的执行环境，例如创建作用域和绑定变量。</li>\n<li>evaluate(timeout, breakOnSigint): 在 V8 中执行模块的代码。</li>\n</ul>\n<pre><code class=\"language-cpp\">class ModuleWrap : public BaseObject {\n public:\n  enum InternalFields {\n    kModuleSlot = BaseObject::kInternalFieldCount,\n    kURLSlot,\n    kSyntheticEvaluationStepsSlot,\n    kContextObjectSlot,  // Object whose creation context is the target Context\n    kInternalFieldCount\n  };\n\n  static void CreatePerIsolateProperties(IsolateData* isolate_data,\n                                         v8::Local&lt;v8::ObjectTemplate&gt; target);\n  static void CreatePerContextProperties(v8::Local&lt;v8::Object&gt; target,\n                                         v8::Local&lt;v8::Value&gt; unused,\n                                         v8::Local&lt;v8::Context&gt; context,\n                                         void* priv);\n  static void RegisterExternalReferences(ExternalReferenceRegistry* registry);\n  static void HostInitializeImportMetaObjectCallback(\n      v8::Local&lt;v8::Context&gt; context,\n      v8::Local&lt;v8::Module&gt; module,\n      v8::Local&lt;v8::Object&gt; meta);\n\n  void MemoryInfo(MemoryTracker* tracker) const override {\n    tracker-&gt;TrackField(&quot;resolve_cache&quot;, resolve_cache_);\n  }\n\n  v8::Local&lt;v8::Context&gt; context() const;\n  v8::Maybe&lt;bool&gt; CheckUnsettledTopLevelAwait();\n\n  SET_MEMORY_INFO_NAME(ModuleWrap)\n  SET_SELF_SIZE(ModuleWrap)\n\n  bool IsNotIndicativeOfMemoryLeakAtExit() const override {\n    // XXX: The garbage collection rules for ModuleWrap are *super* unclear.\n    // Do these objects ever get GC&#39;d? Are we just okay with leaking them?\n    return true;\n  }\n\n  static v8::Local&lt;v8::PrimitiveArray&gt; GetHostDefinedOptions(\n      v8::Isolate* isolate, v8::Local&lt;v8::Symbol&gt; symbol);\n\n  // When user_cached_data is not std::nullopt, use the code cache if it&#39;s not\n  // nullptr, otherwise don&#39;t use code cache.\n  // TODO(joyeecheung): when it is std::nullopt, use on-disk cache\n  // See: https://github.com/nodejs/node/issues/47472\n  static v8::MaybeLocal&lt;v8::Module&gt; CompileSourceTextModule(\n      Realm* realm,\n      v8::Local&lt;v8::String&gt; source_text,\n      v8::Local&lt;v8::String&gt; url,\n      int line_offset,\n      int column_offset,\n      v8::Local&lt;v8::PrimitiveArray&gt; host_defined_options,\n      std::optional&lt;v8::ScriptCompiler::CachedData*&gt; user_cached_data,\n      bool* cache_rejected);\n\n  static void CreateRequiredModuleFacade(\n      const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n\n private:\n  ModuleWrap(Realm* realm,\n             v8::Local&lt;v8::Object&gt; object,\n             v8::Local&lt;v8::Module&gt; module,\n             v8::Local&lt;v8::String&gt; url,\n             v8::Local&lt;v8::Object&gt; context_object,\n             v8::Local&lt;v8::Value&gt; synthetic_evaluation_step);\n  ~ModuleWrap() override;\n\n  static void New(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void GetModuleRequests(\n      const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void InstantiateSync(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void EvaluateSync(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void GetNamespaceSync(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n\n  static void Link(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void Instantiate(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void Evaluate(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void GetNamespace(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void GetStatus(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void GetError(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n\n  static void SetImportModuleDynamicallyCallback(\n      const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void SetInitializeImportMetaObjectCallback(\n      const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static v8::MaybeLocal&lt;v8::Value&gt; SyntheticModuleEvaluationStepsCallback(\n      v8::Local&lt;v8::Context&gt; context, v8::Local&lt;v8::Module&gt; module);\n  static void SetSyntheticExport(\n      const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n  static void CreateCachedData(const v8::FunctionCallbackInfo&lt;v8::Value&gt;&amp; args);\n\n  static v8::MaybeLocal&lt;v8::Module&gt; ResolveModuleCallback(\n      v8::Local&lt;v8::Context&gt; context,\n      v8::Local&lt;v8::String&gt; specifier,\n      v8::Local&lt;v8::FixedArray&gt; import_attributes,\n      v8::Local&lt;v8::Module&gt; referrer);\n  static ModuleWrap* GetFromModule(node::Environment*, v8::Local&lt;v8::Module&gt;);\n\n  v8::Global&lt;v8::Module&gt; module_;\n  std::unordered_map&lt;std::string, v8::Global&lt;v8::Object&gt;&gt; resolve_cache_;\n  contextify::ContextifyContext* contextify_context_ = nullptr;\n  bool synthetic_ = false;\n  int module_hash_;\n};\n</code></pre>\n<h1>内部模块机制</h1>\n<p>这里首先快速介绍一下 C++ API，具体<a href=\"https://github.com/nodejs/node/blob/main/src/README.md\">Node.js 相关文档</a></p>\n<ul>\n<li><code>v8::Isolate</code> 隔离是一个 v8 虚拟机实例，有自己的堆</li>\n<li><code>v8::Context</code> 表示 JavaScript 执行上下文，允许全局和内置对象存放在同一个堆中</li>\n<li><code>v8::Local&lt;v8::Value&gt;</code> 句柄提供了 JavaScript 对象在堆中位置的引用（因为垃圾回收等机制可以改变对象在堆中的位置，此处省略其它句柄类型）</li>\n<li><code>v8::HandleScope</code> 句柄作用域，用于自动管理句柄的生命周期</li>\n<li><code>v8::Value</code> 表示 JavaScript 值类型，子类包括 <code>v8::Number</code>、<code>v8::Object</code> 等等</li>\n<li><code>uv_loop_t</code> 表示事件循环。使用 libuv 时两个核心概念是句柄（<code>HandleWrap</code> 子类）和请求（<code>ReqWrap</code> 子类）。</li>\n<li><code>Environment</code> 表示一个 Node.js 实例，包含一个 事件循环、<code>v8::Isolate</code>、<code>v8::Realm</code></li>\n<li><code>Realm</code> Node.js <code>Realm</code> 对象，用于表示 ECMAScript <code>Realm.[[HostDefined]]</code> 字段（不是指语言的 <code>Realm</code> 本身）</li>\n</ul>\n<p>启动 Node.js</p>\n<pre><code class=\"language-cpp\">int main(int argc, char* argv[]) {\n  return node::Start(argc, argv);\n}\n</code></pre>\n<p>省略非核心代码</p>\n<pre><code class=\"language-cpp\">static ExitCode StartInternal(int argc, char** argv) {\n  CHECK_GT(argc, 0);\n  argv = uv_setup_args(argc, argv);\n  std::shared_ptr&lt;InitializationResultImpl&gt; result =\n      InitializeOncePerProcessInternal(\n          std::vector&lt;std::string&gt;(argv, argv + argc));\n\n  uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);\n  NodeMainInstance main_instance(snapshot_data,\n                                 uv_default_loop(),\n                                 per_process::v8_platform.Platform(),\n                                 result-&gt;args(),\n                                 result-&gt;exec_args());\n  return main_instance.Run();\n}\n</code></pre>\n<p>启动 node 主实例</p>\n<pre><code class=\"language-cpp\">NodeMainInstance::NodeMainInstance(const SnapshotData* snapshot_data,\n                                   uv_loop_t* event_loop,\n                                   MultiIsolatePlatform* platform,\n                                   const std::vector&lt;std::string&gt;&amp; args,\n                                   const std::vector&lt;std::string&gt;&amp; exec_args)\n    : args_(args),\n      exec_args_(exec_args),\n      array_buffer_allocator_(ArrayBufferAllocator::Create()),\n      isolate_(nullptr),\n      platform_(platform),\n      isolate_data_(),\n      isolate_params_(std::make_unique&lt;Isolate::CreateParams&gt;()),\n      snapshot_data_(snapshot_data) {\n  isolate_params_-&gt;array_buffer_allocator = array_buffer_allocator_.get();\n  // 创建 v8 isolate\n  isolate_ = NewIsolate(isolate_params_.get(), event_loop, platform, snapshot_data);\n}\n\nExitCode NodeMainInstance::Run() {\n  Locker locker(isolate_);\n  Isolate::Scope isolate_scope(isolate_);\n  HandleScope handle_scope(isolate_);\n\n  ExitCode exit_code = ExitCode::kNoFailure;\n  // 创建 Node.js 环境\n  DeleteFnPtr&lt;Environment, FreeEnvironment&gt; env = CreateMainEnvironment(&amp;exit_code);\n\n  Context::Scope context_scope(env-&gt;context());\n  Run(&amp;exit_code, env.get());\n  return exit_code;\n}\n\nvoid NodeMainInstance::Run(ExitCode* exit_code, Environment* env) {\n  if (*exit_code == ExitCode::kNoFailure) {\n    // 运行 Node.js 的本质就是轮转事件循环\n    *exit_code = SpinEventLoopInternal(env).FromMaybe(ExitCode::kGenericUserError);\n  }\n}\n\n\nDeleteFnPtr&lt;Environment, FreeEnvironment&gt;\nNodeMainInstance::CreateMainEnvironment(ExitCode* exit_code) {\n  *exit_code = ExitCode::kNoFailure;  // 重置退出码为 0\n\n  HandleScope handle_scope(isolate_);\n\n  Local&lt;Context&gt; context;\n  DeleteFnPtr&lt;Environment, FreeEnvironment&gt; env;\n\n  context = NewContext(isolate_);\n  CHECK(!context.IsEmpty());\n  Context::Scope context_scope(context);\n  env.reset(\n      // 创建 Node.js 环境\n      // 背后是创建 node::Environment 对象\n      // 负责管理和维护 JavaScript 代码执行所需的各种资源\n      // 例如：V8 隔离/上下文，事件循环、异步操作、主进程状态\n      CreateEnvironment(isolate_data_.get(), context, args_, exec_args_));\n\n  return env;\n}\n</code></pre>\n<p>CreateEnvironment 函数还会调用启动函数 <code>node::Realm::RunBootstrapping</code></p>\n<pre><code class=\"language-cpp\">MaybeLocal&lt;Value&gt; Realm::RunBootstrapping() {\n  EscapableHandleScope scope(isolate_);\n\n  CHECK(!has_run_bootstrapping_code());\n\n  Local&lt;Value&gt; result;\n  if (!ExecuteBootstrapper(&quot;internal/bootstrap/realm&quot;).ToLocal(&amp;result) ||\n      !BootstrapRealm().ToLocal(&amp;result)) {\n    return MaybeLocal&lt;Value&gt;();\n  }\n\n  DoneBootstrapping();\n\n  return scope.Escape(result);\n}\n</code></pre>\n<p>可以发现，<strong>内部 js 模块</strong>通过 <code>node_builtins.cc</code> 来编译运行。<br>该函数负责通过 realm，传递全局 process primordials internalBinding 等全局对象。<br>因此，相当于内部模块可以访问额外的 c++ 绑定。</p>\n<pre><code class=\"language-cpp\">MaybeLocal&lt;Value&gt; BuiltinLoader::CompileAndCall(Local&lt;Context&gt; context,\n                                                const char* id,\n                                                Realm* realm) {\n  Isolate* isolate = context-&gt;GetIsolate();\n  // Detects parameters of the scripts based on module ids.\n  // internal/bootstrap/realm: process, getLinkedBinding,\n  //                           getInternalBinding, primordials\n  if (strcmp(id, &quot;internal/bootstrap/realm&quot;) == 0) {\n    Local&lt;Value&gt; get_linked_binding;\n    Local&lt;Value&gt; get_internal_binding;\n    if (!NewFunctionTemplate(isolate, binding::GetLinkedBinding)\n             -&gt;GetFunction(context)\n             .ToLocal(&amp;get_linked_binding) ||\n        !NewFunctionTemplate(isolate, binding::GetInternalBinding)\n             -&gt;GetFunction(context)\n             .ToLocal(&amp;get_internal_binding)) {\n      return MaybeLocal&lt;Value&gt;();\n    }\n    Local&lt;Value&gt; arguments[] = {realm-&gt;process_object(),\n                                get_linked_binding,\n                                get_internal_binding,\n                                realm-&gt;primordials()};\n    return CompileAndCall(\n        context, id, arraysize(arguments), &amp;arguments[0], realm);\n  } else if (strncmp(id, &quot;internal/main/&quot;, strlen(&quot;internal/main/&quot;)) == 0 ||\n             strncmp(id,\n                     &quot;internal/bootstrap/&quot;,\n                     strlen(&quot;internal/bootstrap/&quot;)) == 0) {\n    // internal/main/*, internal/bootstrap/*: process, require,\n    //                                        internalBinding, primordials\n    Local&lt;Value&gt; arguments[] = {realm-&gt;process_object(),\n                                realm-&gt;builtin_module_require(),\n                                realm-&gt;internal_binding_loader(),\n                                realm-&gt;primordials()};\n    return CompileAndCall(\n        context, id, arraysize(arguments), &amp;arguments[0], realm);\n  }\n\n  // This should be invoked with the other CompileAndCall() methods, as\n  // we are unable to generate the arguments.\n  // Currently there are two cases:\n  // internal/per_context/*: the arguments are generated in\n  //                         InitializePrimordials()\n  // all the other cases: the arguments are generated in the JS-land loader.\n  UNREACHABLE();\n}\n</code></pre>\n<h2>模块 Bootstrapping</h2>\n<p>C++ 侧 <code>RunBootstrapping</code> 函数首先执行的是 <code>internal/bootstrap/realm.js</code> 模块。</p>\n<p>该文件在 Node.js 创建的每个 realm 中执行，包括主线程、工作线程和 ShadowRealm 的上下文。只有每个 realm 的内部状态和绑定应该在此文件中进行引导，并且不应将全局变量暴露给用户代码。</p>\n<p>该文件创建内置模块使用的内部模块和绑定加载器。相比之下，用户态模块使用 <code>lib/internal/modules/cjs/loader.js</code>（CommonJS 模块）或 <code>lib/internal/modules/esm/*</code>（ES 模块）加载。</p>\n<p>该文件由 <code>node.cc</code> 编译并在调用 <code>bootstrap/node.js</code> 之前运行，因此加载器在实际启动 Node.js 之前被引导。它创建以下对象：</p>\n<p><strong>C++ 绑定加载器：</strong></p>\n<ul>\n<li><code>process.binding()</code>：传统的 C++ 绑定加载器，可以从用户态访问，因为它是一个附加到全局 process 对象的对象。这些 C++ 绑定使用 <code>NODE_BUILTIN_MODULE_CONTEXT_AWARE()</code> 创建，并且其 <code>nm_flags</code> 设置为 <code>NM_F_BUILTIN</code>。</li>\n<li><code>process._linkedBinding()</code>：旨在供嵌入器在其应用程序中添加额外的 C++ 绑定使用。这些 C++ 绑定可以使用带有标志 <code>NM_F_LINKED</code> 的 <code>NODE_BINDING_CONTEXT_AWARE_CPP()</code> 创建。</li>\n<li><code>internalBinding()</code>：私有内部 C++ 绑定加载器，除非通过 <code>require(&#39;internal/test/binding&#39;)</code>，否则无法从用户态访问。这些 C++ 绑定使用 <code>NODE_BINDING_CONTEXT_AWARE_INTERNAL()</code> 创建，并且其 <code>nm_flags</code> 设置为 <code>NM_F_INTERNAL</code>。</li>\n</ul>\n<p><strong>内部 JavaScript 模块加载器：</strong></p>\n<ul>\n<li><code>BuiltinModule</code>：一个最小的模块系统，用于加载位于 <code>lib/**/*.js</code> 和 <code>deps/**/*.js</code> 中的 JavaScript 核心模块。所有核心模块都通过 <code>js2c.cc</code> 生成的 <code>node_javascript.cc</code> 编译到 node 二进制文件中，因此可以更快地加载它们，而无需 I/O 开销。该类默认使 <code>lib/internal/*</code>、<code>deps/internal/*</code> 模块和 <code>internalBinding()</code> 可用于核心模块，并允许核心模块通过 <code>require(&#39;internal/bootstrap/realm&#39;)</code> 甚至在该文件不是用 CommonJS 风格编写时也可以 require 自身。</li>\n</ul>\n<p><strong>其他对象：</strong></p>\n<ul>\n<li><code>process.moduleLoadList</code>：一个数组，记录进程中加载的绑定和模块以及它们的加载顺序。</li>\n</ul>\n<p>该文件被编译为好像它被包装在一个函数中，参数由 <code>node::RunBootstrapping()</code> 传递。</p>\n<h2><code>NODE_BINDING_*</code></h2>\n<p>这里举个非常简单的例子</p>\n<pre><code class=\"language-cpp\">void ExampleAdd(const FunctionCallbackInfo&lt;Value&gt;&amp; args) {\n  Isolate* isolate = args.GetIsolate();\n  if (args.Length() &lt; 2) {\n    isolate-&gt;ThrowException(Exception::TypeError(\n        String::NewFromUtf8(isolate, &quot;Expected two arguments&quot;)));\n    return;\n  }\n  CHECK(args[0]-&gt;IsNumber());\n  CHECK(args[1]-&gt;IsNumber());\n  double num1 = args[0]-&gt;NumberValue(isolate-&gt;GetCurrentContext()).FromMaybe(0.0);\n  double num2 = args[1]-&gt;NumberValue(isolate-&gt;GetCurrentContext()).FromMaybe(0.0);\n  double sum = num1 + num2;\n  args.GetReturnValue().Set(Number::New(isolate, sum));\n}\n\nvoid Initialize(Local&lt;Object&gt; target,\n                Local&lt;Value&gt; unused,\n                Local&lt;Context&gt; context,\n                void* priv) {\n  Environment* env = Environment::GetCurrent(context);\n  SetMethod(context, target, &quot;exampleAdd&quot;, ExampleAdd);\n}\n\n// Run the `Initialize` function when loading this binding through\n// `internalBinding(&#39;cares_wrap&#39;)` in Node.js&#39;s built-in JavaScript code:\nNODE_BINDING_CONTEXT_AWARE_INTERNAL(cares_wrap, Initialize)\n</code></pre>\n<p>使用 <code>NODE_BINDING_CONTEXT_AWARE_INTERNAL</code> 注册用于 <code>internalBinding()</code> 查找</p>\n<pre><code class=\"language-cpp\">void GetInternalBinding(const FunctionCallbackInfo&lt;Value&gt;&amp; args) {\n  Realm* realm = Realm::GetCurrent(args);\n  Isolate* isolate = realm-&gt;isolate();\n  HandleScope scope(isolate);\n\n  CHECK(args[0]-&gt;IsString());\n\n  Local&lt;String&gt; module = args[0].As&lt;String&gt;();\n  node::Utf8Value module_v(isolate, module);\n  Local&lt;Object&gt; exports;\n\n  node_module* mod = FindModule(modlist_internal, *module_v, NM_F_INTERNAL);\n  if (mod != nullptr) {\n    exports = InitInternalBinding(realm, mod);\n    realm-&gt;internal_bindings.insert(mod);\n  } else {\n    return THROW_ERR_INVALID_MODULE(isolate, &quot;No such binding: %s&quot;, *module_v);\n  }\n\n  args.GetReturnValue().Set(exports);\n}\n</code></pre>\n<p>观察 <code>NODE_BINDING_*</code> 宏可以发现，这个宏为当前的 C++ 文件导出 <code>static node::node_module _module</code> 变量。<br>定义的内部绑定可以通过 <code>node::Environment</code> 的上下文进行加载。</p>\n","tags":["npm","node.js"]},{"id":"nodejs-async-hooks","url":"https://yieldray.fun/posts/nodejs-async-hooks","title":"Node.js之async_hooks","date_published":"2024-02-08T15:00:00.000Z","date_modified":"2024-02-08T15:00:00.000Z","content_text":"<blockquote>\n<p>async_hooks 不稳定，<a href=\"https://nodejs.org/api/async_hooks.html#async-hooks\">文档</a>中也不建议使用。不过该包下提供 AsyncLocalStorage 类是稳定的，<br>其中的稳定方法可以使用。AsyncLocalStorage 是基于 async_hooks 实现的高层接口，因此有必要做一些了解。</p>\n</blockquote>\n<h1>async_hooks</h1>\n<p>async_hooks 用于跟踪异步资源，即，为原本的异步操作加上一些生命周期钩子。<br>异步资源表示具有关联（异步）回调的<em>对象</em>。<br>该回调可能被多次调用，例如 <code>net.createServer()</code> 的 <code>&#39;connection&#39;</code> 事件；<br>也可能只调用一次，例如 <code>fs.open()</code>。<br>资源也可以在（异步）回调被调用之前关闭。<code>AsyncHook</code> 没有明确区分这些不同的情况，而是将它们表示为抽象的概念，即资源。</p>\n<p>在 <a href=\"https://nodejs.org/api/worker_threads.html#class-worker\"><code>Worker</code></a> 线程中运行的代码拥有独立的 async_hooks 接口，每个线程都会使用一组新的 <code>asyncID</code>。</p>\n<p>回调在异步作用域（async scope）中执行（即：执行上下文 execution context）。</p>\n<p>注意：回调当然必须是异步执行。注意 Javascript 本身提供的异步回调实际上非常有限，<br>例如 <code>setTimeout</code> <code>setInterval</code> <code>queueMicrotask</code> 和 <code>Promise</code> 构造函数等<br>除了这些与事件循环有关的回调（事件循环也是由运行环境提供的）<br>其它异步回调则是由特定运行环境提供的，例如浏览器和 Node.js 提供的异步 API</p>\n<hr>\n<pre><code class=\"language-ts\">import async_hooks from &quot;node:async_hooks&quot;;\n\n// 返回当前执行（execution）上下文 ID。执行上下文往往就是某个异步回调\nconst eid = async_hooks.executionAsyncId(); // =&gt; 0\n\n// 返回触发（trigger）当前异步回调的上下文 ID。即当前回调是由哪个回调触发的\nconst tid = async_hooks.triggerAsyncId(); // =&gt; 0\n\n// 通过提供一系列生命周期钩子（都是可选的），创建 AsyncHook 实例\nconst asyncHook = async_hooks.createHook({\n    init,\n    before,\n    after,\n    destroy,\n    promiseResolve,\n});\n\n// 还需要调用此方法启用\nasyncHook.enable();\n\n// 还允许停止监听\nasyncHook.disable();\n</code></pre>\n<p><code>AsyncHook</code> 可以捕获 Node.js 提供的异步回调（即所谓异步资源）和 <code>Promise</code><br>因为 <code>Promise</code> 也是异步资源，所以钩子不应该是异步函数（即返回 <code>Promise</code>的函数，防止无限递归）</p>\n<p>注：使用同步方法即可，例如 <code>fs.writeFileSync()</code><br>注意 Node.js 的 <code>console.log()</code> 是异步操作（可以想象是 <code>fs.write(process.stdout.fd, xxx, () =&gt; {})</code>）\n可用 <code>process.stdout.write(xxx)</code> 或 <code>fs.writeSync(process.stdout.fd, xxx)</code> 代替</p>\n<p>我们自己创造的异步资源，则需要通过 <code>AsyncResource</code> 包装才能被捕获</p>\n<pre><code class=\"language-ts\">import async_hooks from &quot;node:async_hooks&quot;;\nimport { writeFileSync } from &quot;node:fs&quot;;\nimport { format } from &quot;node:util&quot;;\n\nfunction debug(...args) {\n    writeFileSync(&quot;log.out&quot;, `${format(...args)}\\n`, { flag: &quot;a&quot; });\n}\n\nconst asyncHook = async_hooks.createHook({\n    init: debug,\n    before: debug,\n    after: debug,\n    destroy: debug,\n    promiseResolve: debug,\n});\n\nasyncHook.enable();\n</code></pre>\n<table>\n<thead>\n<tr>\n<th>方法名</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>init(asyncId: number, type: string, triggerAsyncId: number, resource: object)</code></td>\n<td>在对象构造期间调用。当此回调运行时，资源可能尚未完成构造。因此，asyncId 引用的资源的所有字段可能尚未填充。</td>\n</tr>\n<tr>\n<td><code>before(asyncId: number)</code></td>\n<td>在资源的回调函数调用之前调用。对于事件监听器（如 TCPWrap），可以调用多次；对于请求（如 FSReqCallback），将仅调用一次。</td>\n</tr>\n<tr>\n<td><code>after(asyncId: number)</code></td>\n<td>在资源的回调函数完成后调用</td>\n</tr>\n<tr>\n<td><code>destroy(asyncId: number)</code></td>\n<td>当销毁资源时调用</td>\n</tr>\n<tr>\n<td><code>promiseResolve(asyncId: number)</code></td>\n<td>仅针对 Promise 调用。发生在当传递给 Promise 构造函数的 <code>resolve()</code> 函数被调用时</td>\n</tr>\n</tbody></table>\n<p>详细描述参见 <a href=\"https://nodejs.org/api/async_hooks.html#hook-callbacks\">文档</a>，这里说明一下 init 钩子的 type 参数：</p>\n<p>type 是一个描述异步资源的字符串，通常对应于资源构造函数的名称。<br>由 Node.js 本身创建的资源类型可能在任何 Node.js 版本中更改。有效值包括 TLSWRAP、TCPWRAP、TCPSERVERWRAP、GETADDRINFOREQWRAP、FSREQCALLBACK、Microtask 和 Timeout。<br>由 AsyncResource 创建的与 Node.js 本身无关的异步资源，AsyncResource 的构造函数可提供 type。<br>还存在 PROMISE 资源类型，用于跟踪 Promise 实例和由它们调度的异步操作。</p>\n<h1><a href=\"https://nodejs.org/api/async_context.html#class-asyncresource\">AsyncResource</a></h1>\n<p>完整类型见 <a href=\"https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/async_hooks.d.ts#L261\">@types/node</a></p>\n<pre><code class=\"language-ts\">class AsyncResource {\n    constructor(type: string, triggerAsyncId?: number | AsyncResourceOptions);\n\n    static bind&lt;Func extends (this: ThisArg, ...args: any[]) =&gt; any, ThisArg&gt;(\n        fn: Func,\n        type?: string,\n        thisArg?: ThisArg,\n    ): Func;\n\n    bind&lt;Func extends (...args: any[]) =&gt; any&gt;(fn: Func): Func;\n\n    runInAsyncScope&lt;This, Result&gt;(fn: (this: This, ...args: any[]) =&gt; Result, thisArg?: This, ...args: any[]): Result;\n\n    emitDestroy(): this;\n\n    asyncId(): number;\n\n    triggerAsyncId(): number;\n}\n</code></pre>\n<h1><a href=\"https://nodejs.org/api/async_context.html#class-asynclocalstorage\">AsyncLocalStorage</a></h1>\n<p>下面只列出了稳定 API，完整类型见 <a href=\"https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/async_hooks.d.ts#L369\">@types/node</a></p>\n<p>AsyncLocalStorage 下文简称为 als</p>\n<pre><code class=\"language-ts\">class AsyncLocalStorage&lt;T&gt; {\n    getStore(): T | undefined;\n\n    run&lt;R&gt;(store: T, callback: () =&gt; R): R;\n    run&lt;R, TArgs extends any[]&gt;(store: T, callback: (...args: TArgs) =&gt; R, ...args: TArgs): R;\n}\n</code></pre>\n<p>使用 als 可以实现隐式传递上下文</p>\n<pre><code class=\"language-ts\">// ref: https://docs.adonisjs.com/guides/async-local-storage\nimport { AsyncLocalStorage } from &quot;node:async_hooks&quot;;\nimport { setTimeout } from &quot;node:timers/promises&quot;;\n\ninterface User {\n    id: number;\n}\n\nconst storage = new AsyncLocalStorage&lt;{ user: User }&gt;();\n\nclass UserService {\n    async get() {\n        // als 消费者与上下文无关，即无需显式持有上下文\n        // 传统方式中可能要求将上下文作为参数传递\n        // 换言之，上下文由 storage.run 隐式传递\n        const state = storage.getStore(); // similar to thread-local storage\n        const { user } = state!;\n        console.log(`The user id is ${user.id}`);\n        return user;\n    }\n}\n\nasync function run(user: User) {\n    const state = { user };\n\n    return storage.run(state, async () =&gt; {\n        await setTimeout(100); // executing async function\n        const userService = new UserService();\n        return await userService.get(); // executing async function\n    });\n}\n\nrun({ id: 1 });\nrun({ id: 2 });\nrun({ id: 3 });\n</code></pre>\n<hr>\n<p>观察源码可以发现，实现也不复杂。<br>原理就是将上下文存储到资源中，每当新建资源时，让上下文从父资源传递到当前新建的资源。</p>\n<pre><code class=\"language-ts\">const storageList: Array&lt;AsyncLocalStorage&gt; = [];\n\nconst storageHook = createHook({\n    init(asyncId, type, triggerAsyncId, resource: object) {\n        // init 钩子从参数中得到当前（子）资源\n        const currentResource: object = executionAsyncResource(); // 获取父资源\n        // Value of currentResource is always a non null object\n        for (let i = 0; i &lt; storageList.length; ++i) {\n            // storageList 数组引用了所有 als 实例\n            // 这里只是触发每个实例的 _propagate 方法\n            storageList[i]._propagate(resource, currentResource, type);\n        }\n    },\n});\n\nclass AsyncLocalStorage {\n    constructor() {\n        // 为异步资源关联一个 store\n        // 这个 store 就是所谓的上下文\n        this.kResourceStore = Symbol(&quot;kResourceStore&quot;);\n        this.enabled = false;\n    }\n\n    _enable() {\n        // 懒启动，对于每个 als 实例，都保存在 storageList 一次\n        if (!this.enabled) {\n            this.enabled = true;\n            ArrayPrototypePush(storageList, this);\n            storageHook.enable();\n        }\n    }\n\n    /** 将父资源的 store 传递到 子资源的 store */\n    _propagate(resource: object, triggerResource: object, type: string) {\n        const store = triggerResource[this.kResourceStore];\n        if (this.enabled) {\n            resource[this.kResourceStore] = store;\n        }\n    }\n\n    run(store, callback, ...args) {\n        // Avoid creation of an AsyncResource if store is already active\n        if (ObjectIs(store, this.getStore())) {\n            return ReflectApply(callback, null, args);\n        }\n\n        this._enable();\n\n        const resource = executionAsyncResource();\n        const oldStore = resource[this.kResourceStore];\n\n        resource[this.kResourceStore] = store;\n\n        try {\n            // run 方法运行的回调，回调中获取的 store 就是 run 方法传递的 store\n            return ReflectApply(callback, null, args);\n        } finally {\n            // 还原 store\n            resource[this.kResourceStore] = oldStore;\n        }\n    }\n\n    getStore() {\n        if (this.enabled) {\n            const resource: object = executionAsyncResource();\n            return resource[this.kResourceStore]; // 从当前资源中获得当前 store\n        }\n    }\n}\n</code></pre>\n","tags":["node.js"]},{"id":"template-from-github","url":"https://yieldray.fun/posts/template-from-github","title":"从Github下载模板","date_published":"2024-02-06T22:40:48.000Z","date_modified":"2024-02-06T22:40:48.000Z","content_text":"<p>由于本地使用了 WattToolkit，通过中间人代理了 Github，导致其它不走系统证书的请求会报证书错误。<br>例如 <code>npm create astro</code> 下载模板时，它<a href=\"https://github.com/withastro/astro/blob/6e30bef652b23c5ae2e097f003bcc9f7a4f1e5c3/packages/create-astro/src/actions/template.ts#L74\">内部</a>使用 <a href=\"https://github.com/unjs/giget/\">giget</a> 请求 <a href=\"https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#download-a-repository-archive-tar\">Github API</a> 来下载模板。<a href=\"https://github.com/unjs/giget/blob/83eb734bb6df993958023e19a363d93d60568248/src/providers.ts#L63\">giget</a> 内部又是直接使用 fetch 函数发起请求，最终导致证书错误而失败。</p>\n<p>giget 原理是从 Github 下载 tar 包，然后解压其中子目录。因为 Github 是不支持直接下载子目录的。</p>\n<pre><code class=\"language-ts\">/**\n * @param input &quot;withastro/starlight#main&quot;\n */\nasync function getDlLink(input: string) {\n    const [owner_repo, ref = &quot;&quot;] = input.split(&quot;#&quot;);\n    const res = await fetch(`https://api.github.com/repos/${owner_repo}/tarball/${ref}`, {\n        headers: {\n            Accept: &quot;application/vnd.github+json&quot;,\n            &quot;X-GitHub-Api-Version&quot;: &quot;2022-11-28&quot;,\n        },\n        redirect: &quot;manual&quot;,\n    });\n    if (res.status !== 302) throw new Error(&quot;unexpected status code&quot;);\n    return res.headers.get(&quot;location&quot;)!;\n}\n\ngetDlLink(&quot;withastro/starlight&quot;); // =&gt; https://codeload.github.com/withastro/starlight/legacy.tar.gz/refs/heads/main\n</code></pre>\n<hr>\n<p>下面为了简单，直接通过 <code>git clone --depth</code> 实现相同效果，<code>git clone</code> 不会出现证书错误。<br>给出一段简单的 Bash 脚本</p>\n<pre><code class=\"language-bash\">#!/usr/bin/env bash\nset -e\n\nthis_name=$(basename &quot;${BASH_SOURCE[0]}&quot;)\n\nusage() {\n    cat &lt;&lt;EOF\nUsage:\n  $this_name &lt;USER/REPO&gt; -p &lt;project&gt; [-d &lt;sub_dir&gt;]\n\nExample:\n  $this_name withastro/astro     -p proj -d examples/basics\n  $this_name withastro/starlight -p proj -d examples/basics\n  $this_name vitejs/vite         -p proj -d packages/create-vite/template-vanilla-ts\n  $this_name hexojs/hexo-starter -p blog -d / -y\n\nAvailable options:\n -p   Project, the local directory to create\n -d   Sub dir, optional, sub directory of the repository\n -y   Yes    , no prompt for y/n\n -h   Help   , print this help and exit\n -v   Verbose, print script debug info\nEOF\n    exit\n}\n\nmsg() {\n    echo &gt;&amp;2 -e &quot;\\033[0;34m${1-}\\033[0m&quot;\n}\n\nwarn() {\n    echo &gt;&amp;2 -e &quot;\\033[1;33m${1-}\\033[0m&quot;\n}\n\nif [ -n &quot;$1&quot; ] &amp;&amp; [[ ! &quot;$1&quot; =~ ^- ]]; then\n    repo=&quot;$1&quot;\n    shift\nfi\n\nwhile getopts &quot;:p:d:yhv&quot; opt; do\n    case ${opt} in\n    p) project_name=$OPTARG ;;\n    d) sub_dir=$OPTARG ;;\n    y) no_confirm=true ;;\n    h) usage ;;\n    v) set -x ;;\n    \\?) warn &quot;Invalid option: $OPTARG&quot; &amp;&amp; usage ;;\n    :) warn &quot;Option -$OPTARG requires an argument&quot; &amp;&amp; usage ;;\n    esac\ndone\nshift $((OPTIND - 1))\n\nif [ -z &quot;$repo&quot; ]; then\n    msg &quot;Enter USER/REPO     (e.g. withastro/astro):&quot;\n    read -r repo\nfi\n\nif [ -z &quot;$sub_dir&quot; ]; then\n    msg &#39;Enter sub directory (e.g. examples/basics) enter to skip:&#39;\n    read -r sub_dir\nfi\n\n[ &quot;$sub_dir&quot; = &quot;/&quot; ] &amp;&amp; sub_dir=&quot;&quot;\n\nif [ -z &quot;$project_name&quot; ]; then\n    msg &#39;Enter new directory (e.g. my-project):&#39;\n    read -r project_name\nfi\n\nconfirm() {\n    if [ -n &quot;$no_confirm&quot; ]; then\n        return\n    fi\n\n    while true; do\n        msg &quot;$1 (y/n):&quot;\n        read -r yn\n        case $yn in\n        [Yy]*) return 0 ;;\n        [Nn]*) return 1 ;;\n        *) warn &quot;Please enter y OR n&quot; ;;\n        esac\n    done\n}\n\nrandom_string=$(tr -dc &#39;a-zA-Z0-9&#39; &lt;/dev/urandom | head -c 4)\ntmp_dir=&quot;tmp_${random_string}_$(echo &quot;$repo/$sub_dir&quot; | sed &#39;s/\\//_/g&#39;)&quot;\n\nconfirm &quot;Will copy from \\&quot;https://github.com/$repo/tree/main/$sub_dir\\&quot;\\nTo \\&quot;$project_name\\&quot;, Is it correct?&quot; || (warn &quot;Aborted&quot; &amp;&amp; exit 1)\n\ngit clone --depth=1 &quot;https://mirror.ghproxy.com/https://github.com/$repo&quot; &quot;$tmp_dir&quot;\ncp -r &quot;$tmp_dir/$sub_dir&quot; &quot;$project_name&quot;\nrm -rf &quot;$project_name/.git&quot;\nrm -rf &quot;$tmp_dir&quot;\n</code></pre>\n<p>保存为 <code>gitget.sh</code>，直接运行 <code>./gitget.sh withastro/starlight -p proj -d examples/basics -y</code> 即可复制模板</p>\n","tags":["bash"]},{"id":"wsl","url":"https://yieldray.fun/posts/wsl","title":"WSL小记","date_published":"2024-01-30T17:17:17.000Z","date_modified":"2024-01-30T17:17:17.000Z","content_text":"<p>安装 WSL 本身： <a href=\"https://learn.microsoft.com/zh-cn/windows/wsl/install-on-server\">https://learn.microsoft.com/zh-cn/windows/wsl/install-on-server</a><br><a href=\"https://github.com/microsoft/WSL/releases\">https://github.com/microsoft/WSL/releases</a></p>\n<p>see also: <a href=\"https://www.sysgeek.cn/install-wsl-2-windows/\">https://www.sysgeek.cn/install-wsl-2-windows/</a></p>\n<p>下面以 Debian 为例，支持安装到任意路径。</p>\n<p>首先<a href=\"https://learn.microsoft.com/zh-cn/windows/wsl/install-manual#downloading-distributions\">下载任意发行版</a>，并直接安装 AppxBundle 安装包，安装后<a href=\"https://learn.microsoft.com/zh-cn/windows/wsl/setup/environment#set-up-your-linux-username-and-password\">设置账号密码</a>。</p>\n<p>下面的命令完成 导出 + 卸载 + 安装到新路径。</p>\n<pre><code class=\"language-sh\"># [Example]\n# Distro:           debian\n# FileName:         D:\\wsl\\debian.tar\n# InstallLocation:  D:\\wsl\\debian\n\nwsl -l -v\nwsl --shutdown\nwsl --export &lt;Distro&gt; &lt;FileName&gt;\nwsl --unregister &lt;Distro&gt;\nwsl  --import &lt;Distro&gt; &lt;InstallLocation&gt; &lt;FileName&gt;\ndebian config --default-user &lt;UserName&gt;\n# 设置默认用户为安装后设置的用户，否则为 root\n# 若为 ubuntu，则对应命令为 ubuntu，其它同理\n</code></pre>\n<p>使用阿里云镜像：<a href=\"https://developer.aliyun.com/mirror/debian\">https://developer.aliyun.com/mirror/debian</a></p>\n<p>注意使用 https 源时需要先安装证书：</p>\n<pre><code class=\"language-sh\">apt install apt-transport-https ca-certificates\n</code></pre>\n<p>wsl2 可通过 windows 系统上的 <code>%UserProfile%\\.wslconfig</code> 文件<a href=\"https://learn.microsoft.com/zh-cn/windows/wsl/wsl-config\">配置</a>。</p>\n<pre><code class=\"language-toml\">[experimental]\nnetworkingMode=mirrored   # 启用镜像网络模式，而不是默认的NAT模式。\nautoProxy=true            # 让WSL使用Windows的HTTP代理\nfirewall=true             # 让Windows防火墙规可筛选WSL网络流量\n</code></pre>\n","tags":["windows"]},{"id":"alpine-linux","url":"https://yieldray.fun/posts/alpine-linux","title":"Alpine Linux","date_published":"2024-01-24T18:32:55.000Z","date_modified":"2024-01-24T18:32:55.000Z","content_text":"<p><a href=\"https://www.alpinelinux.org/\">Alpine</a> 是一个轻量级的 Linux 发行版，基于 <a href=\"https://www.musl-libc.org/\">musl libc</a> 和 <a href=\"https://www.busybox.net/\">busybox</a>，自带 <a href=\"https://docs.alpinelinux.org/user-handbook/0.1a/Working/apk.html\">apk</a> 包管理器。</p>\n<p>Alpine 适合作为 <a href=\"https://hub.docker.com/_/alpine\">docker 的基础镜像</a>，不过如果不需要包管理器或切换 libc 的需求，也可以使用 <a href=\"https://hub.docker.com/_/busybox\">busybox 镜像</a>。<br>无需 UNIX utilities 和其它依赖时，也可以直接 <a href=\"https://hub.docker.com/_/scratch/\"><code>FROM scratch</code></a> （但应用程序往往或多或少有某些依赖）</p>\n<p>Alpine 自带 <a href=\"https://docs.alpinelinux.org/user-handbook/0.1a/Working/apk.html\">apk</a>（Alpine Package Keeper）包管理器，主页为 <a href=\"https://pkgs.alpinelinux.org\">https://pkgs.alpinelinux.org</a>，包的扩展名为 <code>.apk</code></p>\n<p>国内可能需要镜像，参考：<a href=\"https://mirror.tuna.tsinghua.edu.cn/help/alpine/\">https://mirror.tuna.tsinghua.edu.cn/help/alpine/</a></p>\n<blockquote>\n<p>Debian 系的 <a href=\"https://man.archlinux.org/man/apt\"><code>apt</code></a> 基于 <a href=\"https://man.archlinux.org/man/dpkg\"><code>dpkg</code></a><br>Redhat 系的 <a href=\"https://man.archlinux.org/man/dnf\"><code>dnf</code></a> （原 <code>yum</code>） 基于 <code>rpm</code></p>\n</blockquote>\n<p>通过 <a href=\"https://man.archlinux.org/man/apk\"><code>apk(8)</code></a> 安装的顶层包称为 <a href=\"https://man.archlinux.org/man/apk-world\">world</a>，数据保存在 <code>/etc/apk/world</code> 文件中</p>\n<pre><code class=\"language-sh\">apk update                  # 更新索引\napk upgrade                 # 更新\napk search [-v] nano        # 搜索\napk del --simulate nano     # 模拟删除\napk del --purge vim         # 同时删除配置文件\napk add -u nano             # 未安装则安装，已安装则更新（包括依赖）\napk info -e                 # 查看已安装的包信息\napk info -L nano            # 查看包包含的文件\napk info -R nano            # 查看包依赖\napk info -s nano            # 查看安装某个包将要占据的大小\napk info -W /etc/rc.conf    # 查询哪个包拥有此文件 -W (–-who-owns)\napk fetch nano -o nano-apk  # 仅下载安装包到指定目录\napk cache clean             # 清理缓存\ncat /etc/apk/world\napk add --allow-untrusted /tmp/package_name.apk\n</code></pre>\n","tags":["linux"]},{"id":"nginx-intro","url":"https://yieldray.fun/posts/nginx-intro","title":"nginx入门","date_published":"2024-01-17T18:18:18.000Z","date_modified":"2024-01-17T18:18:18.000Z","content_text":"<h1>安装</h1>\n<p>非 Windows 参见：<a href=\"https://nginx.org/en/docs/install.html\">https://nginx.org/en/docs/install.html</a></p>\n<p>Windows 可以直接下载编译好的二进制：<a href=\"https://nginx.org/en/download.html\">https://nginx.org/en/download.html</a></p>\n<p>下以 linux 为例。</p>\n<h1>启动</h1>\n<p>nginx 有一个主进程和一些工作进程。主进程主要用于读取和执行配置文件，以及管理工作进程。<br>工作进程用于处理实际的请求。nginx 使用基于事件的模型和对应 OS 的机制，将请求分发给各个工作进程</p>\n<p>首先，启动 nginx。nginx 的默认配置文件为 <code>/etc/nginx/nginx.conf</code></p>\n<pre><code class=\"language-sh\"># 启动\nnginx\n</code></pre>\n<p>启动后 nginx 会在后台运行，使用 <code>nginx -s &lt;signal&gt;</code> 向 nginx 主进程发送信号<br>（也可以直接用 kill 命令发送 Unix 信号，此处略）</p>\n<pre><code class=\"language-sh\">nginx -s stop   # fast shutdown\nnginx -s quit   # graceful shutdown\nnginx -s reopen # reloading the configuration file\nnginx -s reload # reopening the log files\n</code></pre>\n<p>一旦主进程接收到重载配置文件的信号，nginx 就会检查新配置文件的语法并尝试应用之。<br>如果成功，主进程就生成新的工作进程，并通知旧的工作进程关闭。<br>否则，主进程回滚变更并继续保持旧配置。<br>对于被通知关闭的旧工作进程，停止处理新请求，但继续保持服务的请求直至已处理完所有请求。</p>\n<h1>配置文件</h1>\n<p>nginx 本身由模块构成，而 nginx 的配置文件由 <em>指令</em> 组成，来控制这些模块。</p>\n<p>简单指令由 <em>名称</em> 和 <em>参数</em> 组成，以 <em>空格</em> 分隔，以 <em>分号</em> <code>;</code> 结尾。</p>\n<p>块指令和简单指令基本一致，但不以分号结尾，而是用花括号 <code>{}</code> 来包含一组其它指令。</p>\n<p>若块指令的花括号内可以包含其它指令，则称之为上下文（context）。（上下文可以用来继承一些指令）</p>\n<p>若指令不被其它上下文包含，则其位于 main 上下文。</p>\n<p>注释使用 <code>#</code>。</p>\n<pre><code class=\"language-nginx\"># ~/nginx.conf\n\nevents {\n  worker_connections 1024;\n}\n\nhttp {\n  server {\n    listen 8080;\n    location / {\n      root ./data/www;\n    }\n    location /images/ {\n      root ./data;\n    }\n  }\n}\n</code></pre>\n<p>配置文件的 <code>-c</code> 选项需要使用完整路径，否则会自动添加 <code>-p &lt;prefix&gt;</code> 选项指定的前缀（默认值 <code>/etc/nginx/</code>）<br>包括在配置文件中使用的相对路径，也是并非相对于工作路径，而是自动添加前缀</p>\n<p>下面的命令停止 nginx 进程，并设置前缀为用户主目录，配置文件位于 <code>~/nginx.conf</code></p>\n<pre><code class=\"language-sh\">nginx -s stop\nnginx -p ~/ -c nginx.conf\n</code></pre>\n<hr>\n<p>注意到 <code>/etc/nginx</code> 目录有如下结构</p>\n<pre><code>/etc/nginx\n├── conf.d/\n│   └── default.conf\n├── fastcgi_params\n├── mime.types\n├── modules -&gt; /usr/lib/nginx/modules\n├── nginx.conf\n├── scgi_params\n└── uwsgi_params\n</code></pre>\n<p>默认配置文件如下</p>\n<pre><code class=\"language-nginx\">user  nginx;\nworker_processes  auto;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include       /etc/nginx/mime.types;\n    default_type  application/octet-stream;\n    log_format  main  &#39;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#39;\n                      &#39;$status $body_bytes_sent &quot;$http_referer&quot; &#39;\n                      &#39;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#39;;\n    access_log  /var/log/nginx/access.log  main;\n    sendfile        on;\n    #tcp_nopush     on;\n    keepalive_timeout  65;\n    #gzip  on;\n    include /etc/nginx/conf.d/*.conf;\n}\n</code></pre>\n<p>注意 <code>include /etc/nginx/conf.d/*.conf;</code> 指令，<br>该指令读取 <code>/etc/nginx/conf.d</code> 目录下的所有 <code>.conf</code> 文件，<br>并用其内容填充当前 <code>include</code> 指令的位置，因此可以将配置文件分散到多个文件</p>\n<p>例如 <code>/etc/nginx/conf.d/default.conf</code> 文件内容大致如下：</p>\n<pre><code class=\"language-nginx\">server {\n    listen       80;\n    server_name  localhost;\n\n    location / {\n        root   /usr/share/nginx/html;\n        index  index.html index.htm;\n    }\n}\n</code></pre>\n<p>可以看出，nginx 是推荐使用这种方法分割多个 server 指令的。</p>\n<p>下面给出一些简单的示例。</p>\n<hr>\n<p>花括号内部的指令可以继承上下文中的指令</p>\n<pre><code class=\"language-nginx\">server {\n    listen 8080;\n    root /data;\n\n    location / {\n        # 此处继承： root /data;\n    }\n}\n</code></pre>\n<p>下面的例子使用 <a href=\"https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass\">proxy_pass</a> 指令</p>\n<pre><code class=\"language-nginx\">server {\n    listen 8080 default_server;\n    server_name _;\n\n    location / {\n        proxy_pass http://localhost:8081/;\n    }\n\n    location ~ \\.(gif|jpg|png)$ {\n        root /data/images;\n    }\n}\n</code></pre>\n<p>下面的例子启用文件列表</p>\n<pre><code class=\"language-nginx\">autoindex on;\nautoindex_exact_size on;\nautoindex_localtime on;\n\nserver {\n    listen 8080;\n    root /path/to/list;\n}\n</code></pre>\n<p>下面的例子可部署 HTTPS 静态服务器</p>\n<pre><code class=\"language-nginx\">server {\n    listen 443 ssl;\n    server_name your_domain.com;\n\n    ssl_certificate /path/to/ssl_certificate.crt;\n    ssl_certificate_key /path/to/ssl_certificate_key.key;\n\n    root /path/to/static/files;\n    index index.html;\n\n    location / {\n        try_files $uri $uri/ =404;\n    }\n}\n</code></pre>\n<h1>其它资源</h1>\n<p>官方文档：<a href=\"https://nginx.org/en/docs/\">https://nginx.org/en/docs/</a></p>\n<p>nginx playground：<a href=\"https://nginx-playground.wizardzines.com/\">https://nginx-playground.wizardzines.com/</a></p>\n<p>配置生成：<a href=\"https://www.digitalocean.com/community/tools/nginx\">https://www.digitalocean.com/community/tools/nginx</a></p>\n<p>nginx tutorial：<br><a href=\"https://dunwu.github.io/nginx-tutorial/\">https://dunwu.github.io/nginx-tutorial/</a><br><a href=\"https://wangchujiang.com/nginx-tutorial/\">https://wangchujiang.com/nginx-tutorial/</a></p>\n","tags":["cli"]},{"id":"http-proxy-and-tls","url":"https://yieldray.fun/posts/http-proxy-and-tls","title":"HTTP代理原理与浅析TLS","date_published":"2024-01-15T13:00:00.000Z","date_modified":"2024-01-15T13:00:00.000Z","content_text":"<h1>HTTP 代理</h1>\n<p>可以简单地使用下面的代码来观察请求内容</p>\n<pre><code class=\"language-js\">require(&quot;net&quot;)\n    .createServer((socket) =&gt; {\n        socket.pipe(process.stdout);\n        socket.end();\n    })\n    .listen(9980);\n</code></pre>\n<p>注意 HTTP 协议自身的换行符是 CRLF，即 <code>\\r\\n</code></p>\n<hr>\n<p>对于 HTTP 流量，要求代理直接解析并转发 HTTP 请求（<a href=\"https://datatracker.ietf.org/doc/html/rfc7230\">RFC7230</a>）</p>\n<pre><code class=\"language-sh\">curl -x 127.0.0.1:9980 http://example.net/pathname -d &quot;post_body&quot;\n</code></pre>\n<p>与普通的 HTTP 请求不同的是，路径处是完整的 URL，这样代理才知道要请求的 URL<br>有一些请求头是发送给代理的，例如下面的 Proxy-Connection</p>\n<pre><code class=\"language-sh\">POST http://example.net/pathname HTTP/1.1\nHost: example.net\nUser-Agent: curl/8.4.0\nAccept: */*\nProxy-Connection: Keep-Alive\nContent-Length: 9\nContent-Type: application/x-www-form-urlencoded\n\npost_body\n</code></pre>\n<hr>\n<p>对于非 HTTP 流量，HTTP 代理可承载任意 TCP 流量（<a href=\"https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.6\">RFC7231#section-4.3.6</a>）</p>\n<p>下以 HTTPS 举例，但注意此时相当于隧道代理，转发的流量对代理来说是透明的（即代理只管原样转发流量）</p>\n<pre><code class=\"language-sh\">curl -x 127.0.0.1:9980 https://example.net/pathname -d &quot;post_body&quot;\n</code></pre>\n<p>客户端首先向代理发送 CONNECT 请求，路径处指定的是目标 TCP 的主机地址（域名:端口）</p>\n<p>此时的请求头都是发送给代理的指令。CONNECT 请求是普通的 HTTP 请求，可以复用已有的 HTTP 机制，<br>比如需要认证的 HTTP 代理可以是 <a href=\"https://datatracker.ietf.org/doc/html/rfc7235\">RFC7235</a> 描述的 HTTP 身份验证实现的。</p>\n<pre><code class=\"language-sh\">CONNECT example.net:443 HTTP/1.1\nHost: example.net:443\nUser-Agent: curl/8.4.0\nProxy-Connection: Keep-Alive\n</code></pre>\n<p>代理需要与目标 TCP 服务器建立连接，并向客户端返回</p>\n<pre><code class=\"language-sh\">HTTP/1.1 200 Connection established\n（这一行是空行，再次强调换行符是 \\r\\n 即 0x0D0A）\n</code></pre>\n<p>（注意上面的空行，这是因为响应头发送完毕，接下来的内容作为响应体传输）</p>\n<p>表示已成功建立连接，可以开始转发 TCP 流量<br>注意此时代理保持了与目标服务器和客户端的 TCP 连接</p>\n<p>此后代理只需要双向原样转发 TCP 流量即可</p>\n<p>代理与客户端建立的 HTTP 连接，实际上只是借用了 HTTP 的原语发送代理指令而已，<br>一旦代理指令发送完毕，此时的 HTTP 连接直接视为 TCP 连接，因此是双向 TCP 连接（而 HTTP 是单向的）<br>只不过从报文的顺序来说，对于建立的 HTTP 连接，转发的流量处于 HTTP 的请求体</p>\n<hr>\n<p>简单实现如下：</p>\n<pre><code class=\"language-ts\">import http from &quot;node:http&quot;;\nimport url from &quot;node:url&quot;;\nimport net from &quot;node:net&quot;;\n\nhttp.createServer((req, res) =&gt; {\n    console.log(&quot;转发 HTTP 流量. 简单起见，不识别和添加请求头&quot;, req.url);\n    http.request(req.url!, { headers: req.headers }, (hRes) =&gt; {\n        res.writeHead(hRes.statusCode!, hRes.headers);\n        hRes.pipe(res);\n    }).on(&quot;error&quot;, () =&gt; res.end());\n})\n    .on(&quot;connect&quot;, (req, clientSocket, head) =&gt; {\n        console.log(&quot;转发任意 TCP 流量. 简单起见，省略错误处理&quot;, req.url);\n        const { hostname, port } = url.parse(&quot;//&quot; + req.url, false, true);\n        const serverSocket = net\n            .connect({ host: hostname!, port: Number(port) }, () =&gt; {\n                clientSocket.write(&quot;HTTP/1.1 200 Connection Established\\r\\n&quot;);\n                clientSocket.write(&quot;\\r\\n&quot;);\n                serverSocket.pipe(clientSocket);\n            })\n            .on(&quot;error&quot;, () =&gt; clientSocket.end());\n        clientSocket.pipe(serverSocket);\n    })\n    .listen(9980, () =&gt; console.log(&quot;Proxy listen at http://127.0.0.1:9980&quot;));\n</code></pre>\n<p>一个更完善的版本，但仅处理 CONNECT</p>\n<pre><code class=\"language-ts\">#!/usr/bin/env node\nimport http from &quot;node:http&quot;;\nimport net from &quot;node:net&quot;;\nimport url from &quot;node:url&quot;;\nimport console from &quot;node:console&quot;;\nimport process from &quot;node:process&quot;;\n\nconst PORT = Number(process.env.PORT) || 7891;\n\nconst server = http.createServer((req, res) =&gt; {\n    // Deny all requests other than CONNECT for HTTPS\n    console.log(`[PROXY] Denying non-CONNECT request for: ${req.method} ${req.url}`);\n    res.writeHead(405, { &quot;Content-Type&quot;: &quot;text/plain&quot; });\n    res.end(&quot;Method Not Allowed&quot;);\n});\n\nserver.on(&quot;connect&quot;, (req, clientSocket, head) =&gt; {\n    // req.url will be in the format &quot;hostname:port&quot; for a CONNECT request.\n    const { hostname, port } = url.parse(`http://${req.url}`, false);\n\n    console.log(`[PROXY] Intercepted CONNECT request for: ${hostname}:${port}`);\n\n    if (\n        true // Allow all connections for simplicity, but you can add your own logic here\n    ) {\n        console.log(`[PROXY] Allowing connection to ${hostname}:${port}`);\n\n        // Establish a TCP connection to the original destination.\n        const serverSocket = net.connect(Number(port!), hostname!, () =&gt; {\n            clientSocket.write(&quot;HTTP/1.1 200 Connection Established\\r\\n\\r\\n&quot;);\n            // Create a tunnel by piping data between the client and the destination server.\n            serverSocket.write(head);\n            serverSocket.pipe(clientSocket);\n            clientSocket.pipe(serverSocket);\n        });\n\n        serverSocket.on(&quot;error&quot;, (err) =&gt; {\n            console.error(`[PROXY] Error connecting to destination: ${err.message}`);\n            clientSocket.end(`HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n`);\n        });\n    } else {\n        console.log(`[PROXY] Denying connection to ${hostname}:${port}`);\n        clientSocket.end(&quot;HTTP/1.1 403 Forbidden\\r\\n\\r\\n&quot;);\n    }\n\n    clientSocket.on(&quot;error&quot;, (err) =&gt; {\n        // This can happen if the client hangs up.\n        console.error(`[PROXY] Client socket error: ${err.message}`);\n    });\n});\n\nserver.listen(PORT, () =&gt; {\n    const address = server.address() as net.AddressInfo;\n    console.log(`[PROXY] Proxy listening on ${address.address}:${address.port}`);\n});\n</code></pre>\n<h1>TLS</h1>\n<p>TLS流程如下：</p>\n<p>图解：<a href=\"https://tls13.xargs.org/\">https://tls13.xargs.org/</a></p>\n<ol>\n<li>客户端 C 选定一组加密算法（密钥交换算法），并生成客户端不重数（随机数）发送给服务器 S（不重数用于防止重放攻击）<ul>\n<li>TLS1.3 时，C 可以猜测 S 支持的加密算法（例如 C 先前已与 S 建立过 TLS 连接），并直接发送一种加密算法待 S 确认（若成功，则减少一次 RTT 开销）</li>\n<li>对于 HTTPS，还会根据 <a href=\"https://www.cloudflare.com/zh-cn/learning/ssl/what-is-sni/\">TLS 的 SNI 扩展</a>发送目标服务器域名</li>\n</ul>\n</li>\n<li>服务器 S 选择支持的加密算法和生成的服务端不重数，连同 S 的证书，发送给 C</li>\n<li>C 使用 CA 公钥鉴别 S 的证书。C 生成预主密钥 PMS，用 S 的公钥加密发送给 S</li>\n<li>S 使用私钥解密得到预主密钥 PMS（此时 C 和 S 均持有相同的 PMS）。</li>\n<li>C 和 S 使用已商定的算法，均使用 PMS+客户端不重数+服务端不重数，生成主密钥 MS</li>\n<li>主密钥 MS 划分为四个密钥，之后 C 和 S 会分别使用其中两个（C/S 的会话密钥、C/S 的 MAC 密钥）（MAC 密钥用于计算 MAC 散列）</li>\n<li>C 向 S 发送全部握手阶段报文的 MAC 散列</li>\n<li>S 向 C 发送全部握手阶段报文的 MAC 散列</li>\n<li>分别确认 MAC 一致（没有被中间人篡改）后完成握手</li>\n<li>TLS 中，C 或 S 发送的每一块数据称为记录（record），实际发送的数据块算法如下<ul>\n<li><code>MAC 散列 = MAC 密钥加密(明文记录 + MAC 密钥 + 记录的序号)</code></li>\n<li><code>实际发送数据块 = 类型 + 版本 + 长度 + 会话密钥加密(明文记录 + MAC 散列)</code></li>\n</ul>\n</li>\n<li>通过上面的类型字段来关闭 TLS 连接（而不是直接通过 TCP 的 FIN 报文，防止截断攻击）</li>\n</ol>\n<p>注意到 HTTPS 相当于 TLS 承载 HTTP，浏览器还会验证服务器证书声称的 FQDN(Fully Qualified Domain Name) 是否与 SNI 目标域名一致</p>\n","tags":["tls","http"]},{"id":"openssl-quick-reference","url":"https://yieldray.fun/posts/openssl-quick-reference","title":"常用OpenSSL命令","date_published":"2024-01-14T22:22:22.000Z","date_modified":"2024-01-14T22:22:22.000Z","content_text":"<blockquote>\n<p>If you&#39;re looking for a more in-depth and comprehensive look at OpenSSL,<br>we recommend you check out the <a href=\"https://www.feistyduck.com/library/openssl-cookbook/\">OpenSSL Cookbook</a> by Ivan Ristić.</p>\n</blockquote>\n<h1>检查版本</h1>\n<p>目前 OpenSSL 主要有 1.x 和 3.x 的版本，主要区别在于 1.x 只支持 TLS&lt;=1.2</p>\n<p>某些 linux 发行版可能自带 LibreSSL （即 openssl 命令实际调用 libressl）而不是 OpenSSL</p>\n<p>windows可下参见<a href=\"https://learn.microsoft.com/zh-cn/windows-server/administration/openssh/openssh_install_firstuse\">微软的文档</a>安装，或安装<a href=\"https://github.com/PowerShell/Win32-OpenSSH/releases\">微软预编译的二进制文件</a>\n或者也使用 git for windows 自带的 openssl，一般位于 <code>C:\\Program Files\\Git\\usr\\bin\\openssl.exe</code></p>\n<pre><code>openssl version -a\n</code></pre>\n<p>输出如下</p>\n<pre><code>OpenSSL 版本\nbuilt on: 编译时间\nplatform: 平台\noptions:  编译选项\ncompiler: 编译器命令\nOPENSSLDIR: 配置目录\n</code></pre>\n<h1>生成私钥</h1>\n<p>本文只包括最广泛使用的 RSA 算法，长度为 2048</p>\n<pre><code class=\"language-sh\"># 生成 RSA 私钥\nopenssl genrsa -out private.key 2048\n\n# 查看该密钥内容\nopenssl rsa -text -in private.key -noout\n</code></pre>\n<p>上述私钥实际上包含了私钥和公钥，使用如下命令提取出公钥：</p>\n<pre><code class=\"language-sh\">openssl rsa -in private.pem -outform PEM -pubout -out public.pem\n</code></pre>\n<h1>生成 CSR</h1>\n<pre><code class=\"language-sh\">openssl req -new -key public.key -out yourdomain.csr\n</code></pre>\n<p>该命令会交互式地提示使用者回答问题，如下：<br>可直接回车置空，缺失值也会从 OPENSSLDIR 目录的配置文件读取。</p>\n<table>\n<thead>\n<tr>\n<th>prompt</th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Country Name (2 letter code)</td>\n<td>公司所在地的两字母国家/地区代码</td>\n</tr>\n<tr>\n<td>State or Province Name (full name)</td>\n<td>公司所在地的州/省全名</td>\n</tr>\n<tr>\n<td>Locality Name (e.g., city)</td>\n<td>公司所在地的城市名</td>\n</tr>\n<tr>\n<td>Organization Name (e.g., company)</td>\n<td>公司的合法注册名称（例如 YourCompany, Inc.）</td>\n</tr>\n<tr>\n<td>Organizational Unit Name (e.g., section)</td>\n<td>组织内部部门的名称</td>\n</tr>\n<tr>\n<td>Common Name (e.g., server FQDN)</td>\n<td>fully-qualified domain name（FQDN）（例如 <a href=\"http://www.example.com%EF%BC%89\">www.example.com）</a></td>\n</tr>\n<tr>\n<td>Email Address</td>\n<td>电子邮件地址</td>\n</tr>\n<tr>\n<td>A challenge password</td>\n<td></td>\n</tr>\n<tr>\n<td>An optional company name</td>\n<td></td>\n</tr>\n</tbody></table>\n<p>或使用 <code>-subj</code> 选项输入，而不是交互式：</p>\n<pre><code class=\"language-sh\">openssl req -new -key public.key -out yourdomain.csr \\\n-subj &quot;/C=US/ST=Utah/L=Lehi/O=Your Company, Inc./OU=IT/CN=yourdomain.com&quot;\n</code></pre>\n<p>亦或一步到位生成私钥和 CSR：</p>\n<pre><code class=\"language-sh\">openssl req -new \\\n-newkey rsa:2048 -nodes -keyout private.key \\\n-out yourdomain.csr \\\n-subj &quot;/C=US/ST=Utah/L=Lehi/O=Your Company, Inc./OU=IT/CN=yourdomain.com&quot;\n</code></pre>\n<p>最后验证 CSR 文件：</p>\n<pre><code class=\"language-sh\">openssl req -text -in yourdomain.csr -noout -verify\n</code></pre>\n<h1>签名证书</h1>\n<p>下面我们使用私钥，接受 CSR 请求，生成自签名证书（365 天有效期）。</p>\n<pre><code class=\"language-sh\">openssl x509 -req -days 365 -in yourdomain.csr -signkey private.key -out server.crt\n</code></pre>\n<p>有效的正式证书需要我们发送 CSR 文件，交由 CA 签名，总之最终得到 PEM 格式的证书。</p>\n<p>得到证书后，查看证书信息：</p>\n<pre><code class=\"language-sh\">openssl x509 -text -in yourdomain.crt -noout\n</code></pre>\n<h1>验证密钥是否匹配</h1>\n<pre><code class=\"language-sh\">openssl pkey -pubout -in private.key | openssl sha256\nopenssl req -pubkey -in yourdomain.csr -noout | openssl sha256\nopenssl x509 -pubkey -in server.crt -noout | openssl sha256\n</code></pre>\n<p>得到的值应都相同</p>\n<h1>格式转换</h1>\n<pre><code class=\"language-sh\"># PEM to PKCS#12\nopenssl pkcs12 -export -name &quot;yourdomain-digicert-(expiration date)&quot; \\\n-out yourdomain.pfx -inkey yourdomain.key -in yourdomain.crt\n\n# PKCS#12 to PEM(private key)\nopenssl pkcs12 -in yourdomain.pfx -nocerts -out yourdomain.key -nodes\n\n# PKCS#12 to PEM(certificate)\nopenssl pkcs12 -in yourdomain.pfx -nokeys -clcerts -out yourdomain.crt\n\n# PEM(private key) to DER(private key)\nopenssl rsa -inform PEM -in yourdomain.key -outform DER -out yourdomain_key.der\n\n# PEM(certificate) to DER(certificate)\nopenssl x509 -inform PEM -in yourdomain.crt -outform DER -out yourdomain.der\n\n# DER(private key) to PEM(private key)\nopenssl rsa -inform DER -in yourdomain_key.der -outform PEM -out yourdomain.key\n\n# DER(certificate) to PEM(certificate)\nopenssl x509 -inform DER -in yourdomain.der -outform PEM -out yourdomain.crt\n</code></pre>\n<p><a href=\"https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm\">参见</a></p>\n","tags":["tls","crypto"]},{"id":"vercel-configuration","url":"https://yieldray.fun/posts/vercel-configuration","title":"vercel 配置小记","date_published":"2024-01-05T15:13:24.000Z","date_modified":"2024-01-05T15:13:24.000Z","content_text":"<h1>零配置</h1>\n<p>2025.9.5 开始，vercel 支持零配置<a href=\"https://vercel.com/docs/frameworks/backend\">部署</a> 后端框架。<br>自动识别 <a href=\"https://vercel.com/changelog/zero-configuration-express-backends\">express</a> 和 hono 框架，无需配置 <code>vercel.json</code> 和使用 <code>/api</code> 目录。</p>\n<h1>package.json</h1>\n<p><code>package.json</code> 定义的 node 版本优先级高于 Project Settings。</p>\n<pre><code class=\"language-json\">{\n    &quot;engines&quot;: {\n        &quot;node&quot;: &quot;&gt;=18&quot;\n    },\n    &quot;packageManager&quot;: &quot;pnpm&quot;\n}\n</code></pre>\n<p>vercel 会自动识别项目并构建，或者在 Build &amp; Development Settings 手动配置。</p>\n<h1>vercel.json</h1>\n<p>默认情况下 vercel 匹配 <code>/api</code> 目录下以 <code>.js</code> <code>.mjs</code> <code>.ts</code> 为后缀的文件作为 <a href=\"https://vercel.com/docs/functions/serverless-functions/runtimes/node-js\">Serverless Functions</a>。</p>\n<p>对于 node 以外的，官方支持的<a href=\"https://vercel.com/docs/functions/configuring-functions/runtime\">运行时</a>，对应的后缀也会自动匹配。</p>\n<p>对于<a href=\"https://vercel.com/docs/functions/configuring-functions/runtime#other-runtimes\">其它运行时</a>，需要在 <code>vercel.json</code> 中额外配置，例如：</p>\n<pre><code class=\"language-json\">{\n    &quot;functions&quot;: {\n        &quot;api/**/*.php&quot;: {\n            &quot;runtime&quot;: &quot;vercel-php@0.7.3&quot;\n        },\n        &quot;api/**/*.[jt]s&quot;: {\n            &quot;runtime&quot;: &quot;vercel-deno@3.1.0&quot;\n        }\n    }\n}\n</code></pre>\n<p>识别 <code>/api</code> 目录为 Serverless Functions 实际上是 vercel 的<em>默认构建行为</em>。</p>\n<p>修改 <a href=\"https://vercel.com/docs/projects/project-configuration#builds\">builds</a> 字段可以自定义此行为。（尽管 vercel 文档不推荐，不过这个比较方便，用 functions 字段定义则更细致一些）</p>\n<p>但注意，配置 builds 字段后，Build &amp; Development Settings 将<a href=\"https://vercel.com/docs/errors/error-list#unused-build-and-development-settings\">被忽略</a>，<br>也就是 vercel 不会执行其它配置的构建步骤，此时我们必须在手动构建后再推送到 vercel。<br>这对需要构建步骤的应用来说很不方便，特别是 typescript，因为本来 vercel 是会自动编译 <code>/api</code> 目录下的 <code>.ts</code> 文件的。</p>\n<p>当使用 functions 配置而非 builds 字段配置时，也可以考虑使用 <a href=\"https://vercel.com/docs/functions/runtimes/node-js/advanced-node-configuration#custom-build-step-for-node.js\">vercel-build 脚本构建</a>。然而，使用 functions 配置时要求构建后存在 public 目录。</p>\n<blockquote>\n<p><strong>使用 esbuild 编译</strong></p>\n<pre><code class=\"language-sh\"> esbuild index.ts --outfile=index.js --platform=node --packages=external --format=esm\n</code></pre>\n<p>注：无需使用 <code>--bundle</code> 构建，因为 vercel 会安装 package.json 的依赖。</p>\n</blockquote>\n<p>例如，我们想部署一个纯 nodejs 的 http 应用（并且<strong>阻止文件式路由</strong>），则可以有如下 <code>vercel.json</code>。</p>\n<pre><code class=\"language-json\">{\n    &quot;$schema&quot;: &quot;https://openapi.vercel.sh/vercel.json&quot;,\n    &quot;builds&quot;: [{ &quot;src&quot;: &quot;index.js&quot;, &quot;use&quot;: &quot;@vercel/node&quot; }],\n    &quot;rewrites&quot;: [{ &quot;source&quot;: &quot;/(.*)&quot;, &quot;destination&quot;: &quot;index.js&quot; }],\n    &quot;env&quot;: { &quot;NODEJS_HELPERS&quot;: &quot;0&quot; }\n}\n</code></pre>\n<p>根目录的 <code>index.js</code> 如下：（<a href=\"https://vercel.com/docs/functions/runtimes/node-js\">文档</a>），支持的导出实际上就是 Next.js API 路由的 <a href=\"https://nextjs.org/docs/pages/building-your-application/routing/api-routes\">pages 风格</a> 和 <a href=\"https://nextjs.org/docs/app/getting-started/route-handlers-and-middleware\">app 风格</a></p>\n<pre><code class=\"language-js\">/**\n * @param {import(&#39;node:http&#39;).IncomingMessage} req\n * @param {import(&#39;node:http&#39;).ServerResponse} res\n */\nexport default async function handler(req, res) {\n    res.write(`method: ${req.method}\\n`);\n    res.write(`url: ${req.url}\\n`);\n    res.write(`headers: ${JSON.stringify(req.headers, null, 4)}`);\n    res.end();\n}\n</code></pre>\n<p>可以将环境变量 <code>NODEJS_HELPERS</code> 设置为 <code>0</code> 以关闭 vercel 在 req 和 res 上添加的<a href=\"https://vercel.com/docs/functions/serverless-functions/runtimes/node-js#node.js-helpers\">辅助方法</a>，这样与标准的 nodejs 环境更一致。</p>\n<hr>\n<p>要让 vercel 将文件识别为 <a href=\"https://vercel.com/docs/functions/edge-functions/edge-runtime\">Edge Function</a>，需要在代码中多加几行：</p>\n<blockquote>\n<p>Edge Function 的优点在于其支持流式传输，流式传输没有<a href=\"https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration\">时长限制</a>，而普通的 Serverless Function 必须在有限的时间内发送完所有响应。</p>\n</blockquote>\n<pre><code class=\"language-ts\">export const config = {\n    runtime: &quot;edge&quot;,\n};\n\nexport async function GET(req: Request) {\n    return Response.json({ hello: &quot;world&quot; });\n}\n\n// 或导出\nimport type { VercelRequest, VercelResponse } from &quot;@vercel/node&quot;;\nexport default function handler(request: VercelRequest, response: VercelResponse) {\n    return response.status(200).json({ hello: &quot;world&quot; });\n}\n</code></pre>\n<p>对于 next.js（app router）：</p>\n<pre><code class=\"language-ts\">import { NextResponse } from &quot;next/server&quot;;\nimport type { NextRequest } from &quot;next/server&quot;;\n\nexport const runtime = &quot;edge&quot;; // &#39;nodejs&#39; is the default\n\nexport function GET(request: NextRequest) {\n    return NextResponse.json({ hello: &quot;world&quot; });\n}\n</code></pre>\n","tags":["vercel"]},{"id":"how-to-implement-a-basic-activitypub-server","url":"https://yieldray.fun/posts/how-to-implement-a-basic-activitypub-server","title":"如何实现基本的 Activity Pub 服务器","date_published":"2024-01-04T16:08:15.000Z","date_modified":"2024-01-04T16:08:15.000Z","content_text":"<blockquote>\n<p>这篇文章主要参考了 <a href=\"https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/\">mastodon 的官方博客上的一篇文章</a><br>代码以 ruby 为例。本文仅使用标准的 Web API，以及少量 Deno 代码为例。</p>\n</blockquote>\n<h1>生成密钥对</h1>\n<p>Activity Pub 通过密钥机制（WebFinger）保证数据不被篡改，因此我们首先需要准备密钥对（公钥和私钥）<br>我们要通过 Web Crypto API 生成 RSA 密钥对，并保存（导出）为 <code>.pem</code> 格式的文本文件</p>\n<blockquote>\n<p>仅使用 Web Crypto API，无需直接使用 openssl 命令行！</p>\n</blockquote>\n<pre><code class=\"language-ts\">// 生成 RSA 密钥对\nasync function generateRSAKeyPair() {\n    const keyPair = await crypto.subtle.generateKey(\n        {\n            name: &quot;RSASSA-PKCS1-v1_5&quot;,\n            modulusLength: 2048,\n            publicExponent: new Uint8Array([0x01, 0x00, 0x01]),\n            hash: { name: &quot;SHA-256&quot; },\n        },\n        true,\n        [&quot;sign&quot;, &quot;verify&quot;],\n    );\n\n    return keyPair;\n}\n\n// 导出私钥为 PEM 格式\nasync function exportPrivateKey(key: CryptoKey) {\n    const exportedKey = await crypto.subtle.exportKey(&quot;pkcs8&quot;, key);\n    const exportedPem = arrayBufferToPem(exportedKey, &quot;PRIVATE KEY&quot;);\n    return exportedPem;\n}\n\n// 导出公钥为 PEM 格式\nasync function exportPublicKey(key: CryptoKey) {\n    const exportedKey = await crypto.subtle.exportKey(&quot;spki&quot;, key);\n    const exportedPem = arrayBufferToPem(exportedKey, &quot;PUBLIC KEY&quot;);\n    return exportedPem;\n}\n\n// 将 ArrayBuffer 转换为 PEM 格式\nfunction arrayBufferToPem(buffer: ArrayBuffer, label: &quot;PRIVATE KEY&quot; | &quot;PUBLIC KEY&quot;) {\n    const pemHeader = `-----BEGIN ${label}-----`;\n    const pemFooter = `-----END ${label}-----`;\n\n    const binary = String.fromCharCode.apply(null, new Uint8Array(buffer) as any as number[]);\n    const base64 = btoa(binary);\n\n    let pem = pemHeader + &quot;\\n&quot;;\n    let offset = 0;\n\n    while (offset &lt; base64.length) {\n        const line = base64.substr(offset, 64);\n        pem += line + &quot;\\n&quot;;\n        offset += 64;\n    }\n    pem += pemFooter + &quot;\\n&quot;;\n    return pem;\n}\n\n// 生成 RSA 密钥对\nconst keyPair = await generateRSAKeyPair();\n\n// 导出私钥和公钥\nconst privateKeyPem: string = await exportPrivateKey(keyPair.privateKey);\nconst publicKeyPemL: string = await exportPublicKey(keyPair.publicKey);\n\n// 写入文件（这里使用 Deno，nodejs 同理）\nDeno.writeTextFileSync(&quot;private.pem&quot;, privateKeyPem);\nDeno.writeTextFileSync(&quot;public.pem&quot;, publicKeyPem);\n</code></pre>\n<p>现在我们得到了 <code>private.pem</code> 和 <code>public.pem</code> 两个文本文件</p>\n<blockquote>\n<p>注：等效 openssl 命令</p>\n<pre><code>openssl genrsa -out private.pem 2048\nopenssl rsa -in private.pem -outform PEM -pubout -out public.pem\n</code></pre>\n</blockquote>\n<h1>在公网上暴露我们的服务器</h1>\n<p>我们需要实现 <a href=\"https://webfinger.net/\">Webfinger</a>，将公钥暴露在公网，以便其它服务器获取</p>\n<p>其它服务器会 GET 请求例如 <code>/.well-known/webfinger?resource=acct:bob@my-example.com</code><br>来查询我们的服务器是否存在指定的 resource（这里则是查询我们的服务器是否存在指定用户）</p>\n<p>我们简单地返回下面的 JSON（注意，下文演示中的 <code>my-example.com</code> 都需要修改为<strong>真实的公网 URL</strong>）</p>\n<p>其中 <code>https://my-example.com/actor</code> 是公钥将要存在的 url</p>\n<pre><code class=\"language-json\">{\n    &quot;subject&quot;: &quot;acct:alice@my-example.com&quot;,\n\n    &quot;links&quot;: [\n        {\n            &quot;rel&quot;: &quot;self&quot;,\n            &quot;type&quot;: &quot;application/activity+json&quot;,\n            &quot;href&quot;: &quot;https://my-example.com/actor&quot;\n        }\n    ]\n}\n</code></pre>\n<p>下面使用 Deno 简单实现</p>\n<pre><code class=\"language-ts\">import { ServeRouter } from &quot;npm:serve-router&quot;;\n\nconst app = ServeRouter();\n\napp.all(&quot;/.well-known/webfinger&quot;, (req: Request) =&gt; {\n    const url = new URL(req.url);\n\n    return Response.json(\n        {\n            subject: url.searchParams.get(&quot;resource&quot;),\n            links: [\n                {\n                    rel: &quot;self&quot;,\n                    type: &quot;application/activity+json&quot;,\n                    href: `${url.origin}/actor`,\n                },\n            ],\n        },\n        {\n            headers: { &quot;Content-Type&quot;: &quot;application/jrd+json&quot; },\n        },\n    );\n});\n</code></pre>\n<p>之后，其它的服务器还会通过查询 <code>https://my-example.com/actor</code> 来获取这个用户的公钥（我们的服务器只有我们一个人）</p>\n<p>我们返回如下 JSON（<code>publicKeyPem</code> 就是我们之前生成的 <code>public.pem</code> 文件的内容）</p>\n<pre><code class=\"language-json\">{\n    &quot;@context&quot;: [&quot;https://www.w3.org/ns/activitystreams&quot;, &quot;https://w3id.org/security/v1&quot;],\n\n    &quot;id&quot;: &quot;https://my-example.com/actor&quot;,\n    &quot;type&quot;: &quot;Person&quot;,\n    &quot;preferredUsername&quot;: &quot;alice&quot;,\n    &quot;inbox&quot;: &quot;https://my-example.com/inbox&quot;,\n\n    &quot;publicKey&quot;: {\n        &quot;id&quot;: &quot;https://my-example.com/actor#main-key&quot;,\n        &quot;owner&quot;: &quot;https://my-example.com/actor&quot;,\n        &quot;publicKeyPem&quot;: &quot;-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----&quot;\n    }\n}\n</code></pre>\n<pre><code class=\"language-ts\">/**\n * 公钥 pem 文件内容如下：\n * -----BEGIN PUBLIC KEY-----\n * xxxx\n * -----END PUBLIC KEY-----\n */\nconst publicKeyPem = Deno.readTextFileSync(&quot;./public.pem&quot;);\n\napp.all(&quot;/actor&quot;, (req: Request) =&gt; {\n    // 我们还可以仅当请求明确指明需要返回 json 时，才返回，例如如下请求头：\n    // accept: application/activity+json, application/ld+json\n    // 换言之，当我们请求其它服务器时，也应当加上上述请求头\n    const url = new URL(req.url);\n\n    return Response.json(\n        {\n            &quot;@context&quot;: [&quot;https://www.w3.org/ns/activitystreams&quot;, &quot;https://w3id.org/security/v1&quot;],\n\n            id: `${url.origin}/actor`,\n            type: &quot;Person&quot;,\n            preferredUsername: &quot;alice&quot;,\n            inbox: `${url.origin}/inbox`,\n\n            publicKey: {\n                id: `${url.origin}/actor#main-key`,\n                owner: `${url.origin}/actor`,\n                publicKeyPem,\n            },\n        },\n        {\n            headers: { &quot;Content-Type&quot;: &quot;application/jrd+json&quot; },\n        },\n    );\n});\n</code></pre>\n<p>顺便简单地实现我们的收件箱（inbox）<br>这里只是简单地打印出请求的 JSON，返回值我们不去处理</p>\n<pre><code class=\"language-ts\">app.post(&quot;/inbox&quot;, async (req: Request) =&gt; {\n    console.log(JSON.stringify(await req.json(), null, 4));\n\n    return new Response(`I&#39;m inbox!`);\n});\n</code></pre>\n<h1>发出我们的第一个请求</h1>\n<p>现在我们想要关注（Follow）Mastodon 的官方账号</p>\n<p>我们向 <code>https://mastodon.social/inbox</code> POST 请求发送如下 JSON</p>\n<pre><code class=\"language-json\">{\n    &quot;@context&quot;: &quot;https://www.w3.org/ns/activitystreams&quot;,\n    &quot;id&quot;: &quot;https://my-example.com/my-first-follow&quot;,\n    &quot;type&quot;: &quot;Follow&quot;,\n    &quot;actor&quot;: &quot;https://my-example.com/actor&quot;,\n    &quot;object&quot;: &quot;https://mastodon.social/users/Mastodon&quot;\n}\n</code></pre>\n<p>不过在发送之前，我们需要<a href=\"https://docs.joinmastodon.org/spec/security/#http\">对请求头签名</a>，确保是由 <code>https://my-example.com/actor</code> 代表的用户（也就是我们）发出的请求（通过 <code>Signature:</code> 请求头）</p>\n<blockquote>\n<p><a href=\"https://datatracker.ietf.org/doc/rfc9421/\">RFC 9421</a> <a href=\"https://oauth.net/http-signatures/\">HTTP Signatures</a></p>\n</blockquote>\n<pre><code>Signature: keyId=&quot;https://my-example.com/actor#main-key&quot;,headers=&quot;(request-target) host date&quot;,signature=&quot;...&quot;\n</code></pre>\n<p>需要签名的请求头内容如下：\n（注意 <code>(request-target)</code> 请求头并不会实际发送，因为它相当于 HTTP 请求的第一行，在此仅作签名需要）<br>（<code>post</code> 是小写而不是大写，<code>date</code> 必须是当前 UTC 时间，只允许 30 秒上下的时间误差，否则视签名无效）</p>\n<pre><code>(request-target): post /inbox\nhost: mastodon.social\ndate: Sun, 06 Nov 1994 08:49:37 GMT\n</code></pre>\n<p>但注意，对于 POST 请求，mastodon 还要求发送 <code>Digest:</code> 请求头，值是请求体的 RSA-SHA256 摘要（digest） <a href=\"https://docs.joinmastodon.org/spec/security/#digest\">参见此处</a></p>\n<p>首先构造一些辅助函数，用于导入密钥和 base64 编码（对于 <code>Digest:</code> 请求头）</p>\n<pre><code class=\"language-ts\">// 导入私钥\nasync function importPrivateKey(pem: string) {\n    const pemHeader = &quot;-----BEGIN PRIVATE KEY-----&quot;;\n    const pemFooter = &quot;-----END PRIVATE KEY-----&quot;;\n\n    const pemContents = pem.replace(pemHeader, &quot;&quot;).replace(pemFooter, &quot;&quot;).replace(/\\s/g, &quot;&quot;);\n\n    const binaryString = atob(pemContents);\n    const binaryBuffer = new Uint8Array(binaryString.length);\n\n    for (let i = 0; i &lt; binaryString.length; i++) {\n        binaryBuffer[i] = binaryString.charCodeAt(i);\n    }\n\n    const privateKey = await crypto.subtle.importKey(\n        &quot;pkcs8&quot;,\n        binaryBuffer,\n        {\n            name: &quot;RSASSA-PKCS1-v1_5&quot;,\n            hash: &quot;SHA-256&quot;,\n        },\n        true,\n        [&quot;sign&quot;],\n    );\n\n    return privateKey;\n}\n\n// 使用密钥对一段文本签名\nfunction signText(key: CryptoKey, text: string) {\n    return crypto.subtle.sign(\n        {\n            name: &quot;RSASSA-PKCS1-v1_5&quot;,\n            hash: { name: &quot;SHA-256&quot; },\n        },\n        key,\n        new TextEncoder().encode(text),\n    );\n}\n\n// 编码为 base64 字符串\nfunction encodeBase64(buf: ArrayBufferLike): string {\n    let binary = &quot;&quot;;\n    const bytes = new Uint8Array(buf);\n    for (let i = 0; i &lt; bytes.length; i++) {\n        binary += String.fromCharCode(bytes[i]);\n    }\n    return btoa(binary);\n}\n</code></pre>\n<p>终于可以发出第一个请求了</p>\n<pre><code class=\"language-ts\">// 读取私钥文件\nconst privateKeyPem = Deno.readTextFileSync(&quot;./private.pem&quot;);\nconst privateKey = await importPrivateKey(privateKeyPem);\n\n// 修改为公网域名\nconst origin = &quot;https://my-example.com&quot;;\n\n// 请求体\nconst requestBody = JSON.stringify({\n    &quot;@context&quot;: &quot;https://www.w3.org/ns/activitystreams&quot;,\n    id: `${origin}/my-first-follow`,\n    type: &quot;Follow&quot;,\n    actor: `${origin}/actor`,\n    object: &quot;https://mastodon.social/users/Mastodon&quot;,\n});\n\nconst date = new Date().toUTCString();\nconst digest = &quot;sha-256=&quot; + encodeBase64(await crypto.subtle.digest(&quot;SHA-256&quot;, new TextEncoder().encode(requestBody)));\n\nconst signedString = encodeBase64(\n    await signText(\n        privateKey,\n        `(request-target): post /inbox\nhost: mastodon.social\ndate: ${date}\ndigest: ${digest}`,\n    ),\n);\n\nconst signature = `keyId=&quot;${origin}/actor#main-key&quot;,headers=&quot;(request-target) host date digest&quot;,signature=&quot;${signedString}&quot;`;\n\nconst res: Response = await fetch(&quot;https://mastodon.social/inbox&quot;, {\n    method: &quot;POST&quot;,\n    headers: {\n        date,\n        digest,\n        signature,\n        &quot;content-type&quot;: &#39;application/ld+json; profile=&quot;http://www.w3.org/ns/activitystreams&quot;&#39;,\n    },\n    body: requestBody,\n});\n\nawait inspectResponse(res);\n\n// 打印 HTTP 响应\nasync function inspectResponse(res: Response) {\n    console.log(res.status, res.statusText);\n    console.log(Object.fromEntries(res.headers.entries()));\n    const text = await res.text();\n    console.log(text);\n}\n</code></pre>\n<p>现在， mastodon.social 将返回 <code>202 Accepted</code></p>\n<p>然后，mastodon.social 将向我们的收件箱 <code>https://my-example.com/inbox</code> 发送如下 JSON</p>\n<p>（当然，请求也是经过签名的，但这里就不演示<a href=\"https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/\">如何验证请求头</a>了）</p>\n<pre><code class=\"language-json\">{\n    &quot;@context&quot;: &quot;https://www.w3.org/ns/activitystreams&quot;,\n    &quot;id&quot;: &quot;https://mastodon.social/users/Mastodon#accepts/follows/&quot;,\n    &quot;type&quot;: &quot;Accept&quot;,\n    &quot;actor&quot;: &quot;https://mastodon.social/users/Mastodon&quot;,\n    &quot;object&quot;: {\n        &quot;id&quot;: &quot;https://my-example.com/my-first-follow&quot;,\n        &quot;type&quot;: &quot;Follow&quot;,\n        &quot;actor&quot;: &quot;https://my-example.com/actor&quot;,\n        &quot;object&quot;: &quot;https://mastodon.social/users/Mastodon&quot;\n    }\n}\n</code></pre>\n<p>我们可以在 <a href=\"https://mastodon.social/@Mastodon/followers\">Mastodon 的 followers 页面</a> 看到，我们已经成功关注了 <code>@Mastodon</code></p>\n","tags":["activitypub","rfc","http","crypto"]},{"id":"css-display","url":"https://yieldray.fun/posts/css-display","title":"css display","date_published":"2023-12-03T19:00:00.000Z","date_modified":"2023-12-03T19:00:00.000Z","content_text":"<h1><a href=\"https://drafts.csswg.org/css-display/\">CSS Display Module</a></h1>\n<p>依据最新草案，<code>display</code> 属性由外部和内部两个基本特质构成：</p>\n<ul>\n<li><a href=\"https://drafts.csswg.org/css-display/#outer-display-type\">外部 display 类型</a><br>决定 <a href=\"https://drafts.csswg.org/css-display/#principal-box\">主盒</a> 如何参与 <a href=\"https://drafts.csswg.org/css-display/#flow-layout\">流布局</a><br>主要是两个值：<code>block</code> 和 <code>inline</code>（<a href=\"https://css-tricks.com/run-in/\"><code>run-in</code></a> 的浏览器支持差，实用性不高）</li>\n<li><a href=\"https://drafts.csswg.org/css-display/#inner-display-type\">内部 display 类型</a><br>决定 非替换元素 如何生成 <a href=\"https://drafts.csswg.org/css-display/#formatting-context\">格式化上下文</a> （替换元素的内部 display 类型超出了 CSS 的范畴）<br>最基础的是 <code>flow</code> 和 <code>flow-root</code>。其它的例如：<code>flex</code> <code>grid</code> <code>table</code></li>\n</ul>\n<p>除上述两个特质外，还有一些特殊的 display 值能够创建副作用。<br>比如 <code>list-item</code> 可生成 <code>::marker</code> 伪元素，<code>none</code> 可以将整个元素从 box-tree 中移除。</p>\n<p>形式化语法：</p>\n<pre><code class=\"language-css\">display =\n    [ &lt;display-outside&gt; || &lt;display-inside&gt; ]\n    | &lt;display-listitem&gt; | &lt;display-internal&gt; | &lt;display-box&gt; | &lt;display-legacy&gt;\n\n&lt;display-outside&gt;  = block | inline | run-in\n&lt;display-inside&gt;   = flow | flow-root | table | flex | grid | ruby\n&lt;display-listitem&gt; = &lt;display-outside&gt;? &amp;&amp; [ flow | flow-root ]? &amp;&amp; list-item\n&lt;display-internal&gt; = table-row-group | table-header-group |\n                     table-footer-group | table-row | table-cell |\n                     table-column-group | table-column | table-caption |\n                     ruby-base | ruby-text | ruby-base-container |\n                     ruby-text-container\n&lt;display-box&gt;      = contents | none\n&lt;display-legacy&gt;   = inline-block | inline-table | inline-flex | inline-grid\n</code></pre>\n<p>注意到已有的单值 display 属性可转换为完整格式，举例如下：</p>\n<table>\n<thead>\n<tr>\n<th>short</th>\n<th>full</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>block</td>\n<td>block flow</td>\n</tr>\n<tr>\n<td>inline</td>\n<td>inline flow</td>\n</tr>\n<tr>\n<td>inline-block</td>\n<td>inline flow-root</td>\n</tr>\n<tr>\n<td>flow-root</td>\n<td>block flow-root</td>\n</tr>\n<tr>\n<td>flex</td>\n<td>block flex</td>\n</tr>\n<tr>\n<td>inline-flex</td>\n<td>inline flex</td>\n</tr>\n<tr>\n<td>list-item</td>\n<td>block flow list-item</td>\n</tr>\n</tbody></table>\n<p>可以发现，完整格式对非常清晰地表达了元素的表现方式。</p>\n<p>总结：外部主盒参与流布局，内部生成格式化上下文。</p>\n<h1><a href=\"https://drafts.csswg.org/css-sizing/\">CSS Box Sizing Module</a></h1>\n<p>CSS Box Sizing 模块包含 外在尺寸（<a href=\"https://drafts.csswg.org/css-sizing/#extrinsic\">extrinsic size</a>） 和 内在尺寸（<a href=\"https://drafts.csswg.org/css-sizing/#intrinsic-sizes\">intrinsic size</a>） 的概念。看起来也是一外一内，不过该模块表述的是盒模型尺寸的生成。</p>\n<iframe height=\"500\" style=\"width: 100%;\" scrolling=\"no\" title=\"content\" src=\"https://codepen.io/YieldRay/embed/BaMGRLK?default-tab=js%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/BaMGRLK\">\n  content</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<ul>\n<li>外部尺寸基于上下文确定元素的尺寸，而无需考虑其内容。</li>\n<li>内部尺寸基于元素内容确定其尺寸，而无需考虑上下文。</li>\n</ul>\n","tags":["css"]},{"id":"web-popover-api","url":"https://yieldray.fun/posts/web-popover-api","title":"Popover API","date_published":"2023-12-02T16:00:00.000Z","date_modified":"2025-10-13T23:12:00.000Z","content_text":"<p>在网页上非常常见的一种模式是在其它内容上显示内容，吸引用户注意力到特定的重要信息或需要执行的操作。<br>这种内容可以取多种不同的名字：overlay、popup、popover、dialog 等。规范称为 popover。<br>总的来说，它们可以分为：</p>\n<p><code>modal</code>，popover 显示时，其他页面部分将处于非交互状态，直到 popover 被通过某种方式进行操作（例如选择一个重要的选项）。</p>\n<p><code>non-modal</code>，popover 显示时，用户可以与其他页面部分进行交互。</p>\n<p>使用 Popover API 创建的 popover 始终为 <code>non-modal</code>。<br>如果希望创建一个 <code>modal</code> 的 popover，可以使用 <code>&lt;dialog&gt;</code> 元素（需要通过 <code>HTMLDialogElement.showModal()</code> 实现，否则其不会创建 top layer；popover 则始终创建 top layer）</p>\n<p>HTML popover 具有以下特性：</p>\n<ul>\n<li>提升至 <a href=\"https://developer.chrome.com/blog/what-is-the-top-layer/\">top layer</a>。从而无需考虑 z-index（只需考虑在 top layer 的顺序，无需考虑 DOM 层级的层叠上下文）</li>\n<li>Light-dismiss 功能。点击 popover 外部区域即可关闭 popover 并返还 focus （回到打开 popover 之前的 focus）</li>\n<li>默认的 focus 管理。打开 popover 则 tab 键局限在 popover 内部</li>\n<li>无障碍键盘绑定。按下 Esc 键即可关闭 popover 并返还 focus</li>\n<li>无障碍组件绑定。可以从语义上将一个 popover 元素连接到一个触发者元素</li>\n</ul>\n<h1>HTML 属性</h1>\n<p>popover 可以是任何 HTML 元素（即 HTMLElement 而不是 SVGElement），popover 的触发者只能是 button 或 input 元素（下面主要以按钮举例）</p>\n<pre><code class=\"language-html\">&lt;div id=&quot;div&quot; popover&gt;[popover]&lt;/div&gt;\n&lt;button popovertarget=&quot;div&quot;&gt;toggle&lt;/button&gt;\n&lt;button popovertarget=&quot;div&quot; popovertargetaction=&quot;show&quot;&gt;show&lt;/button&gt;\n&lt;button popovertarget=&quot;div&quot; popovertargetaction=&quot;hide&quot;&gt;hide&lt;/button&gt;\n</code></pre>\n<ul>\n<li><p><code>popover=auto</code> （空字符串相当于 auto）自动隐藏 popover 的元素（比如点击页面的其它部分或按 <code>Esc</code> 键）。因此只会同时出现一个 auto 的 popover（即所谓 <code>&quot;light dismissed&quot;</code>）</p>\n</li>\n<li><p><code>popover=manual</code> 手动隐藏，需通过 js 或对应 popovertarget 元素来操纵。因此可以出现多个 popover</p>\n</li>\n<li><p><code>popovertarget</code> 在 button 或 input 元素上设置，值为对应 popover 的 id，用于控制该 popover</p>\n</li>\n<li><p><code>popovertargetaction=toggle</code> （空字符串或未设置相当于 toggle）相当于点击该按钮调用 popover 的 <code>HTMLElement.togglePopover()</code> 方法</p>\n</li>\n<li><p><code>popovertargetaction=show</code> 相当于 popover 隐藏时，点击该按钮调用 popover 的 <code>HTMLElement.showPopover()</code> 方法</p>\n</li>\n<li><p><code>popovertargetaction=hide</code> 相当于 popover 显示时，点击该按钮调用 popover 的 <code>HTMLElement.hidePopover()</code> 方法</p>\n</li>\n</ul>\n<h1>CSS</h1>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/CSS/:popover-open\">:popover-open</a></h2>\n<p>注意 close 状态没有对应的伪类。要只匹配 close 状态，使用 <code>:not()</code> 即可</p>\n<pre><code class=\"language-css\">:popover-open {\n}\n:not(:popover-open) {\n}\n</code></pre>\n<p>popover 的默认样式如下</p>\n<pre><code class=\"language-css\">[popover]:popover-open:not(dialog) {\n    overlay: auto !important;\n}\n[popover] {\n    position: fixed;\n    width: fit-content;\n    height: fit-content;\n    color: canvastext;\n    background-color: canvas;\n    inset: 0px;\n    margin: auto;\n    border-width: initial;\n    border-style: solid;\n    border-color: initial;\n    border-image: initial;\n    padding: 0.25em;\n    overflow: auto;\n}\n</code></pre>\n<p>js 可利用此伪类判断是 open 还是 close 状态</p>\n<pre><code class=\"language-js\">popover.matches(&quot;:popover-open&quot;);\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/CSS/::backdrop\">::backdrop</a></h2>\n<p><code>:popover-open</code> 状态的 popover 会创建 <a href=\"https://developer.mozilla.org/docs/Glossary/Top_layer\">top-layer</a> 层，<code>::backdrop</code> 伪类可匹配该层的 backdrop</p>\n<blockquote>\n<p>注：每一个提升至 top-layer 的元素都关联一个 <code>::backdrop</code>，且在层叠顺序上 <code>::backdrop</code> 在该元素下。</p>\n</blockquote>\n<p>注意到 popover 的 <code>::backdrop</code> 默认是透明且屏蔽指针事件的</p>\n<pre><code class=\"language-css\">[popover]:-internal-popover-in-top-layer::backdrop {\n    position: fixed;\n    background-color: transparent;\n    pointer-events: none !important;\n    inset: 0px;\n}\n</code></pre>\n<p>相比于 <a href=\"https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/showModal\"><code>HTMLDialogElement.showModal()</code></a> 产生的默认 <code>::backdrop</code>，则默认阴影且响应指针事件</p>\n<pre><code class=\"language-css\">dialog:-internal-dialog-in-top-layer::backdrop {\n    position: fixed;\n    inset: 0px;\n    background: rgba(0, 0, 0, 0.1);\n}\n</code></pre>\n<h2>DOM API</h2>\n<pre><code class=\"language-ts\">// 仅作示意\ninterface HTMLButtonElement /* 以及 HTMLInputElement */ {\n    //  html 属性 popovertarget 只能设置为 id\n    popoverTargetElement: Element | null;\n    // 反映 html 属性 popovertargetaction，即修改此值也会反映到 html 上\n    // 注意 popovertargetaction 为空字符串或未设置相当于 toggle\n    popoverTargetAction: &quot;hide&quot; | &quot;show&quot; | &quot;toggle&quot;;\n}\n\ninterface HTMLElement {\n    // 反映 html 属性 popover\n    popover: &quot;auto&quot; | &quot;manual&quot; | null;\n\n    // 若 popover 已隐藏，会抛出 DOMException\n    hidePopover(): void;\n    // 若 popover 已显示，会抛出 DOMException\n    showPopover(): void;\n\n    // 不传 force 时，则切换显隐状态\n    // force 为 true 时，强制显示 popover。若已显示，也不会抛出异常\n    // force 为 false 时，强制隐藏 popover。若已隐藏，也不会抛出异常\n    // 返回切换后 popover 是否是 open 状态（目前Chrome120暂不支持，会返回undefined）\n    togglePopover(force?: boolean): boolean;\n\n    // 状态切换之前触发，ToggleEvent 包含当前状态（oldState）和切换之后的状态（newState）\n    // 注意：即使在一次事件循环中多次修改 popover 状态，也只会触发一次 beforetoggle 事件\n    onbeforetoggle: ((this: HTMLElement, ev: ToggleEvent) =&gt; any) | null;\n\n    // 状态切换之后触发，ToggleEvent 包含切换之前的状态（oldState）和当前状态（newState）\n    // 注意：HTMLDetailsElement 也会触发此事件，与 HTMLElement 触发的事件略有不同\n    ontoggle: ((this: HTMLElement, ev: ToggleEvent) =&gt; any) | null;\n\n    addEventListener(\n        type: &quot;beforetoggle&quot; | &quot;toggle&quot;,\n        listener: (this: HTMLElement, ev: ToggleEvent) =&gt; any,\n        options?: boolean | AddEventListenerOptions,\n    ): void;\n\n    removeEventListener(\n        type: &quot;beforetoggle&quot; | &quot;toggle&quot;,\n        listener: (this: HTMLElement, ev: ToggleEvent) =&gt; any,\n        options?: boolean | EventListenerOptions,\n    ): void;\n}\n\n// 注意是 open 或 closed，而不是 close 或者 show/hide\ninterface ToggleEvent extends Event {\n    readonly newState: &quot;open&quot; | &quot;closed&quot;;\n    readonly oldState: &quot;open&quot; | &quot;closed&quot;;\n}\n</code></pre>\n<h1>popover 嵌套</h1>\n<p>略。参见：<a href=\"https://developer.mozilla.org/docs/Web/API/Popover_API/Using#nested_popovers\">https://developer.mozilla.org/docs/Web/API/Popover_API/Using#nested_popovers</a></p>\n<p><a href=\"https://developer.chrome.com/blog/tether-elements-to-each-other-with-css-anchor-positioning/\">https://developer.chrome.com/blog/tether-elements-to-each-other-with-css-anchor-positioning/</a></p>\n<h1>CSS 扩展</h1>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"popover css example\" src=\"https://codepen.io/YieldRay/embed/zYeMYrg?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/zYeMYrg\">\n  popover css example</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>注意 CSS 属性 <a href=\"https://developer.mozilla.org/docs/Web/CSS/overlay\">overlay</a> 只能由浏览器设置，该属性作用在 <code>::backdrop</code> 伪类之前的那个元素（比如 popover 和 dialog，作用相当于 <code>::backdrop</code> 的 display:block 到 display:none）。我们只能设置 <code>transition-property: overlay</code>，且仅当 <code>transition-behavior: allow-discrete</code> 时才有实际效果。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/CSS/transition-behavior\">transition-behavior: allow-discrete</a></h2>\n<p>允许 CSS 属性从离散值切换时，仍进行过渡（默认行为是直接不发生过渡）。此时待连续值过渡完成后，再进行离散值的切换。</p>\n<p>这里主要是应用在 overlay 和 display 属性上（即元素隐藏时），使得 display:block 到 display:none 时仍发生过渡（推迟 display:none 的发生）。</p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"allow-discrete\" src=\"https://codepen.io/YieldRay/embed/mdvgaNB?default-tab=js%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/mdvgaNB\">\n  allow-discrete</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h2><a href=\"https://developer.mozilla.org/docs/Web/CSS/@starting-style\">@starting-style</a></h2>\n<p>允许指定过渡的前一个状态，使得可以立即发生一次过渡。此 At Rule 支持嵌套。</p>\n<p>这里主要是应用在元素显示时。注意这里与 display:none 到 display:block 无关，因为过渡只是发生在 display:block 的选择器上。（所以无需 <code>transition-behavior: allow-discrete</code>）</p>\n<p>所以将 <code>@starting-style</code> 应用在 display:block 的选择器上，而不是 display:none 的选择器。（即完成 display:none 到 display:block 之后，再根据 <code>@starting-style</code> 进行过渡）</p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"@starting-style\" src=\"https://codepen.io/YieldRay/embed/OJdaJPx?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/OJdaJPx\">\n  @starting-style</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h2><a href=\"https://developer.mozilla.org/docs/Web/CSS/interpolate-size\">interpolate-size: allow-keywords</a></h2>\n<p>允许在 <code>&lt;length-percentage&gt;</code> 和 固有尺寸值（intrinsic size）之间进行动画和过渡。</p>\n<p><code>interpolate-size</code> 是继承属性，默认值是 <code>numeric-only</code>。<br>下面的例子使得 <code>height: 0</code> 到 <code>height: max-content</code> 之间发生过渡。</p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" src=\"https://codepen.io/YieldRay/embed/OPMjama?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/OPMjama\">\n  Untitled</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h2>一个综合示例</h2>\n<p><a href=\"https://gist.github.com/YieldRay/3d965bf568332a25c38f8c00fb79285e\">gist</a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" src=\"https://codepen.io/YieldRay/embed/wBMqQqO?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/wBMqQqO\">\n  Untitled</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>参见</h1>\n<p><a href=\"https://open-ui.org/components/popover.research.explainer/\">https://open-ui.org/components/popover.research.explainer/</a></p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Popover_API\">https://developer.mozilla.org/docs/Web/API/Popover_API</a></p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Popover_API/Using\">https://developer.mozilla.org/docs/Web/API/Popover_API/Using</a></p>\n<p><a href=\"https://developer.chrome.com/blog/introducing-popover-api/\">https://developer.chrome.com/blog/introducing-popover-api/</a></p>\n","tags":["web-api","dom"]},{"id":"css-nesting-and-layer","url":"https://yieldray.fun/posts/css-nesting-and-layer","title":"css嵌套与@layer","date_published":"2023-11-11T18:30:00.000Z","date_modified":"2023-11-11T18:30:00.000Z","content_text":"<p><a href=\"https://web.dev/blog/learn-css-refresh\">https://web.dev/blog/learn-css-refresh</a></p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_nesting\">CSS nesting</a></h1>\n<blockquote>\n<p>注意：firefox117 完整支持。chrome112<del>119 部分支持（历史规范问题）chrome120 支持最新规范<br>由于历史规范原因，在 chrome112</del>119 下不能省略 <code>&amp;</code> 符号，否则会出现不符合现行规范的奇奇怪怪的问题。<br>scss 在线编译：<a href=\"https://sass-lang.com/playground/\">https://sass-lang.com/playground/</a></p>\n</blockquote>\n<p>现行规范中 <code>&amp;</code> 嵌套选择器的语法可以说基本与 scss 表现一致。<br>当然某些 css 无法实现的功能除外，比如 <code>&amp;</code> 是不支持 scss 的字符串连接的，即：</p>\n<pre><code class=\"language-css\">.component {\n    &amp;__child-element {\n        /* css 不支持这样，会导致这个选择器块失效（因为css无法解析） */\n        /* 但是外层选择器还是有效的，即嵌套中的选择器块失效不影响外部选择器块有效 */\n    }\n}\n/* scss 会编译成这样 */\n.component__child-element {\n}\n</code></pre>\n<p>此外，css 允许无需嵌套，直接使用 <code>&amp;</code> 选择器（scss 不支持）<br>此时 <code>&amp;</code> 相当于 <code>:scope</code> （注：在 css 中相当于 <code>:root</code>，此伪类主要在 js 中使用）</p>\n<pre><code class=\"language-css\">&amp; {\n    background: wheat;\n}\n/* 相当于 */\n:scope {\n    background: wheat;\n}\n/* 相当于 */\n:root {\n    background: wheat;\n}\n</code></pre>\n<p>css 嵌套的优先级与 <code>:is()</code> 的优先级完全一致，即优先级由选择器列表中的最高优先级相同</p>\n<pre><code class=\"language-css\">//@prettier-ignore\n#a, b {\n    &amp; c {\n        color: blue;\n    }\n}\n/* 相当于，优先级相当于 #a 选择器的优先级 */\n:is(#a, b) {\n    &amp; c {\n        color: blue;\n    }\n}\n</code></pre>\n<p>css 嵌套和 at-rules 一同使用时也与 scss 基本一致，这里从略。</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_nesting/Nesting_at-rules\">https://developer.mozilla.org/docs/Web/CSS/CSS_nesting/Nesting_at-rules</a></p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/@layer\">@layer</a></h1>\n<p><code>@layer</code> 用于创建一个级联层，即属于同一个 <code>@layer</code> 的 css 具有相同的级联</p>\n<p>创建级联层的方法有三种：</p>\n<pre><code class=\"language-css\">/* 直接创建级联层 */\n@layer cascade {\n    display-none {\n        display: none;\n    }\n}\n\n/* 引入 css 并将其导入到一个级联层中 */\n@import (cascade.css) layer(cascade);\n\n/* 仅创建级联层，但不指定任何样式。可以同时声明多个 */\n@layer cascade1, cascade2, cascade3;\n</code></pre>\n<p>当我们多次声明一个同名的级联层时，后面的声明会被合并到第一个声明中。</p>\n<pre><code class=\"language-css\">@layer cascade;\n\n.foo {\n}\n\n@layer cascade {\n    display-none {\n        display: none;\n    }\n}\n\n.bar {\n}\n\n@layer cascade {\n    display-block {\n        display: block;\n    }\n}\n\n/* 相当于 */\n@layer cascade {\n    display-none {\n        display: none;\n    }\n    display-block {\n        display: block;\n    }\n}\n\n.foo {\n}\n\n.bar {\n}\n</code></pre>\n<p>不同级联层的优先级按它们<strong>第一次</strong>在 css 出现的顺序依次递增。（高层覆盖低层，<strong>越后出现的级联层优先级越高</strong>，符合CSS一般认知中的后来居上原则）</p>\n<blockquote>\n<p>级联层的好处在于它是超脱于选择器的存在。即无论选择器本身的优先级，<br>优先级更高的级联层中的样式覆盖优先级更低的样式。</p>\n</blockquote>\n<pre><code class=\"language-css\">/* 优先级低 --------------------&gt; 优先级高 */\n@layer theme, base, components, utilities;\n</code></pre>\n<p>其它不属于任何级联层的样式将被集中到同一匿名层，并置于所有层的<strong>后部</strong>，<br>即 不属于级联层的样式 <em>覆盖</em> 级联层中声明的样式。</p>\n<p>级联层允许嵌套，父子级联层用 <code>.</code> 连接</p>\n<pre><code class=\"language-css\">@layer framework {\n    @layer base;\n    @layer layout {\n    }\n}\n\n@layer framework.layout {\n    p {\n        margin-block: 1rem;\n    }\n}\n</code></pre>\n<p>级联层可以不指定名称，即允许匿名级联层。<br>匿名级联层因为没有名字，自然无法在创建之后再次添加规则。</p>\n<pre><code class=\"language-css\">:scope {\n    /* 非级联层，优先级高于任何级联层 */\n    background: hotpink;\n}\n\n@layer {\n    /* 匿名级联层 */\n    :scope {\n        /* 被覆盖 */\n        background: skyblue;\n    }\n}\n\n@layer; /* 语法正确，但没有任何意义 */\n</code></pre>\n","tags":["css"]},{"id":"css-scroll-snap","url":"https://yieldray.fun/posts/css-scroll-snap","title":"css滚动吸附","date_published":"2023-11-06T22:22:22.000Z","date_modified":"2023-11-06T22:22:22.000Z","content_text":"<h1>演示</h1>\n<p>下面的演示只考虑纵向滚动</p>\n<iframe src=\"https://codesandbox.io/embed/scroll-snap-km9p7f?autoresize=1&fontsize=14&hidenavigation=1&theme=dark\"\n     style=\"width:100%; height:520px; border:0; border-radius: 4px; overflow:hidden;\"\n     title=\"scroll-snap\"\n     allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n     sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n   ></iframe>\n\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"scroll-snap-1\" src=\"https://codepen.io/YieldRay/embed/wvNgMjx?default-tab=js%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/wvNgMjx\">\n  scroll-snap-1</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_scroll_snap\">Scroll Snap</a></h1>\n<p>要开启滚动吸附，至少需要在父（滚动）容器上设置 <code>scroll-snap-type</code> 属性（一般还需正确设置 <code>overflow</code>）和在需要吸附的子孙元素上设置 <code>scroll-snap-align</code> 属性</p>\n<p>一个简单的示例如下（摘自规范文档）<br>强调：只有在滚动容器上才能发生滚动吸附效果，最简单的就是 html 元素（只需设置 <code>scroll-snap-type</code>）<br>设置 <code>scroll-snap-align</code> 的滚动锚点可以是滚动容器的 直接子元素 和 间接子元素（即子孙元素）</p>\n<pre><code class=\"language-css\">html {\n    scroll-snap-type: block; /* applied to main document scroller */\n}\n/*prettier-ignore*/\nh1, h2, h3, h4, h5, h6 {\n    scroll-snap-align: start;  /* snap to the start (top) of the viewport */\n}\n</code></pre>\n<p><code>scroll-snap-type</code> 属性可以声明开启滚动吸附的方向：（默认值 <code>none</code>）可以是 <code>x</code> <code>y</code> 或逻辑方向 <code>inline</code> <code>block</code> 或 <code>both</code><br>还可以声明吸附的严格程度：<code>proximity</code>（默认值，让浏览器决定）或 <code>mandatory</code>（滚动停止时，必须吸附到可见视口的吸附位置）</p>\n<p><video style=\"max-width:100%\" autoplay loop muted playsinline src=\"https://md-cors.deno.dev/https://web.dev/articles/css-scroll-snap/video/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/U9rWoMBMwhKMo6dJZE8v.mp4\"></video></p>\n<p><code>scroll-snap-align</code> 属性声明吸附元素吸附的位置，可取 <code>none</code> <code>start</code> <code>end</code> <code>center</code>，指定双值时分别应用在 block 和 inline 方向</p>\n<p><code>scroll-snap-stop</code> 属性声明滚动容器是否可以越过吸附位置，主要针对触摸屏。<code>normal</code>（默认值）为允许，<code>always</code> 为不允许</p>\n<p><code>scroll-padding</code> 属性声明在滚动容器上，设置滚动内边距。（比如可以用于为导航栏留出空间）</p>\n<p><code>scroll-margin</code> 属性声明在吸附子元素上，设置滚动外边距。</p>\n<h1>参考</h1>\n<p>推荐阅读相关文章，有更详细的示例</p>\n<p><a href=\"https://web.dev/articles/css-scroll-snap\">https://web.dev/articles/css-scroll-snap</a></p>\n<p><a href=\"https://css-tricks.com/practical-css-scroll-snapping/\">https://css-tricks.com/practical-css-scroll-snapping/</a></p>\n<p><a href=\"https://12daysofweb.dev/2022/css-scroll-snap/\">https://12daysofweb.dev/2022/css-scroll-snap/</a></p>\n<p><a href=\"https://codepen.io/collection/KpqBGW\">https://codepen.io/collection/KpqBGW</a></p>\n","tags":["css"]},{"id":"css-theory","url":"https://yieldray.fun/posts/css-theory","title":"css理论基础","date_published":"2023-11-05T11:11:11.000Z","date_modified":"2024-11-11T11:11:11.000Z","content_text":"<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/Containing_block\">包含块（containing block）</a></h1>\n<p><a href=\"https://www.w3.org/TR/css-display-3/#containing-block\">包含块</a>是一个矩形区域，它为与其关联的（后代）盒子提供了<strong>尺寸</strong>和<strong>定位</strong>的基础。</p>\n<h2>确定一个元素的包含块</h2>\n<blockquote>\n<p>CSS2 定义：<br><a href=\"https://www.w3.org/TR/CSS2/visuren.html#containing-block\">9.1.2 Containing blocks</a><br><a href=\"https://www.w3.org/TR/CSS2/visudet.html#containing-block-details\">10.1 Definition of &quot;containing block&quot;</a></p>\n</blockquote>\n<p>确定包含块的流程完全取决于元素 <code>position</code> 属性的值：</p>\n<blockquote>\n<p>以下为 <a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\">MDN 描述</a>，严格定义参见<a href=\"https://www.w3.org/TR/css-position-3/#def-cb\">规范</a></p>\n</blockquote>\n<ol>\n<li>若 <code>position</code> 属性值为 <code>static</code>、<code>relative</code> 或 <code>sticky</code>，则包含块由最近的祖先元素的内容框边缘形成，该祖先元素要么是<strong>块级容器</strong>（例如 inline-block、block 或 list-item 元素），要么<strong>建立了格式化上下文</strong>（例如 table 容器、flex 容器、grid 容器或块级容器本身）。</li>\n<li>若 <code>position</code> 属性值为 <code>absolute</code>，则包含块由最近的 position 值不是 static（fixed、absolute、relative 或 sticky）的祖先元素的内边距框边缘形成。</li>\n<li>若 <code>position</code> 属性值为 <code>fixed</code>，则包含块由 <a href=\"https://developer.mozilla.org/docs/Glossary/Viewport\">viewport</a>（对于连续媒体，<a href=\"https://developer.mozilla.org/docs/Glossary/Continuous_Media\">continuous media</a>，往往是屏幕上的网页）或页面区域（对于分页媒体，<a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_paged_media\">paged media</a>，往往是打印机打印的文档）建立。</li>\n<li>若 <code>position</code> 属性值为 <code>absolute</code> 或 <code>fixed</code>，包含块也可能由最近的具有以下任意属性的祖先元素的内边距框边缘形成：<ul>\n<li><code>filter</code>、<code>backdrop-filter</code>、<code>transform</code> 或 <code>perspective</code> 的值不是 <code>none</code>。</li>\n<li><code>contain</code> 的值为 <code>layout</code>、<code>paint</code>、<code>strict</code> 或 <code>content</code>（例如 <code>contain: paint;</code>）。</li>\n<li><code>container-type</code> 的值不是 <code>normal</code>。</li>\n<li><code>will-change</code> 的值包含一个属性，该属性的非初始值会形成一个包含块（例如 <code>filter</code> 或 <code>transform</code>）。</li>\n<li><code>content-visibility</code> 的值为 <code>auto</code>。</li>\n</ul>\n</li>\n</ol>\n<h2>百分比是如何计算的</h2>\n<ul>\n<li><code>height</code> <code>top</code> <code>bottom</code> 属性根据包含块的 <code>height</code> 计算百分比值</li>\n<li><code>width</code> <code>left</code> <code>right</code> <code>padding</code> <code>margin</code> 属性根据包含块的 <code>width</code> 计算百分比值</li>\n</ul>\n<blockquote>\n<p><strong>注意：</strong> <code>padding</code> 和 <code>margin</code> 及它们的完整形式都是根据 <code>width</code> 计算的，包括 <code>padding-top</code> <code>padding-bottom</code> 这些。</p>\n</blockquote>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_flow_layout/Block_and_inline_layout_in_normal_flow\">常规流 （Normal flow）</a></h1>\n<blockquote>\n<p>具体参见 CSS2.1 规范 9.4 节：<a href=\"https://www.w3.org/TR/CSS2/visuren.html#normal-flow\">https://www.w3.org/TR/CSS2/visuren.html#normal-flow</a>。下面只是摘要，有省略。<br><strong>注意：</strong> 这是 CSS2.1 的规范，所以不涉及 CSS3 的新属性或值。</p>\n</blockquote>\n<p>常规流中的盒子属于格式化上下文，可以是块级或内联级，但不能同时是两者。块级盒子参与块级格式化上下文，内联级盒子参与内联级格式化上下文。</p>\n<h2>块级格式化上下文 BFC</h2>\n<p>浮动元素、绝对定位元素、非块级盒子的块级容器（如内联块、表格单元格和表格标题）以及 <code>overflow</code> 属性不是 <code>visible</code> （默认值）的块级盒子（除非该值已传播到视口）为其内容建立新的块级格式化上下文。</p>\n<blockquote>\n<p>注：CSS3 的 <a href=\"https://developer.mozilla.org/docs/Web/CSS/overflow#clip\"><code>overflow: clip</code></a> <strong>不会创建</strong>新的块级格式化上下文。<br>关于 BFC 的细节与使用参见下文 BFC 部分，而不是本节。</p>\n</blockquote>\n<p>在块级格式化上下文中，盒子按垂直方向一个接一个地布局，从包含块的顶部开始。两个兄弟盒子之间的垂直距离由 <code>margin</code> 属性确定。块级格式化上下文中相邻的块级盒子之间的垂直外边距会发生折叠。</p>\n<p>在块级格式化上下文中，每个盒子的左外边缘与包含块的左边缘接触（对于从右到左的格式化，右边缘接触）。即使存在浮动元素（尽管由于浮动元素的存在，盒子的行盒可能会收缩），这仍然成立，除非盒子本身建立了一个新的块级格式化上下文（在这种情况下，由于浮动元素的存在，盒子本身可能变窄）。</p>\n<h2>内联级格式化上下文 IFC</h2>\n<p>在内联级格式化上下文中，盒子按水平方向一个接一个地布局，从包含块的顶部开始。这些盒子之间会考虑水平外边距、边框和内边距。这些盒子可以以不同的方式在垂直方向上对齐。包含形成一行的盒子的矩形区域称为行盒。</p>\n<p>行盒的宽度由包含块和浮动元素的存在确定。行盒的高度由 <code>line-height</code> 计算部分中给出的规则确定。</p>\n<p>行盒始终足够高以容纳其包含的所有盒子。但是，它可能比其包含的最高盒子更高（例如，盒子以 <code>baseline</code> 对齐）。当盒子 B 的高度小于包含它的行盒的高度时，B 在行盒中的垂直对齐由 <code>vertical-align</code> 属性确定。当几个内联级盒子无法在单个行盒中水平放置时，它们会分布在两个或多个垂直堆叠的行盒中。因此，段落是一堆垂直堆叠的行盒。行盒堆叠时没有垂直间隔（除非另有规定），并且它们永远不会重叠。</p>\n<p>通常情况下，行盒的左边缘与其包含块的左边缘接触，右边缘与其包含块的右边缘接触。然而，浮动盒子可能位于包含块边缘和行盒边缘之间。因此，尽管同一内联级格式化上下文中的行盒通常具有相同的宽度（即包含块的宽度），但如果由于浮动元素而减少了可用的水平空间，它们的宽度可能会有所不同。同一内联级格式化上下文中的行盒通常在高度上有所不同（例如，一行可能包含一个高的图像，而其他行只包含文本）。</p>\n<p>当一行上的内联级盒子的总宽度小于包含它们的行盒的宽度时，它们在行盒内的水平分布由 <code>text-align</code> 属性确定。如果该属性的值为 <code>justify</code>，用户代理可拉伸内联盒子中的空格和单词（但不包括内联表格和内联块盒子）。</p>\n<p>当内联级盒子的宽度超过行盒的宽度时，它会被分割成多个盒子，并在多个行盒中分布。如果无法分割内联级盒子（例如，如果内联级盒子只包含一个字符，或者语言特定的断词规则不允许在内联级盒子内进行断词，或者内联级盒子受 <code>white-space</code> 属性值为 <code>nowrap</code> 或 <code>pre</code> 的影响），那么内联级盒子将溢出行盒。</p>\n<p>当内联级盒子被分割时，边距、边框和内边距在分割发生的地方（或者在多个分割时，当有多个分割时）没有视觉效果。</p>\n<p>由于双向文本处理，内联级盒子也可能在同一行盒中分割为多个盒子。</p>\n<p>在内联级格式化上下文中，根据需要创建行盒以容纳内联级内容。不包含文本、无保留空格、没有具有非零外边距、内边距或边框的内联元素以及没有其他流动内容（如图像、内联块或内联表格）的行盒，并且不以保留的换行符结尾的行盒，在确定其中任何元素位置时必须被视为零高度行盒，并且在其他任何情况下都被视为不存在。</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_flow_layout/In_flow_and_out_of_flow\">In flow and out of flow</a></h1>\n<p>如果一个盒子被从其预期位置中提取出来，并且在父级格式化上下文中以不同的方式布局，与周围内容的交互也不同于正常内容流程，那么该盒子就是 <a href=\"https://www.w3.org/TR/css-display-3/#out-of-flow\">&quot;out-of-flow&quot;</a>（脱离文档流）的。<br>这种情况发生在盒子被浮动（通过 <code>float</code> 属性）或绝对定位（通过 <code>position</code> 属性）时。如果一个盒子不是 &quot;out-of-flow&quot; 的，那么它就是 <a href=\"https://www.w3.org/TR/css-display-3/#in-flow\">&quot;in-flow&quot;</a>（在文档流中）。</p>\n<h1><a href=\"https://www.w3.org/TR/css-display-3/#formatting-context\">格式化上下文</a></h1>\n<p>格式化上下文是提供内部盒子布局的环境，不同的格式化上下文根据不同的规则来布局其内部的盒子。<br>每个格式化上下文具有自己独特的布局规则（内部元素的布局不受外部元素影响），用于确定其<em>子元素</em>如何排列和显示。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/Guide/CSS/Block_formatting_context\">块级格式化上下文（Block Formatting Context，BFC）</a></h2>\n<p><a href=\"https://www.w3.org/TR/css-display-3/#inline-formatting-context\">内联级格式化上下文</a>存在于<a href=\"https://www.w3.org/TR/css-display-3/#block-formatting-context\">块级格式化上下文</a>中（是其包含的一部分）。例如，属于 <em>内联级格式化上下文的行盒</em> 与 <em>属于块级格式化上下文的浮动元素</em> 相互作用。</p>\n<p>块级布局是在块级格式化上下文内执行的布局，用于处理块级盒子。</p>\n<p>块级格式化上下文根（Block Formatting Context Root）是建立新的块级格式化上下文的块级容器。</p>\n<p>表现为：</p>\n<ol>\n<li><strong>包含内部浮动</strong> :BFC 使得让浮动内容和周围的内容等高。（防止当一个元素包含浮动元素时发生高度坍塌）</li>\n<li><strong>排除外部浮动</strong> ：在正常流中建立新的 BFC 的元素不会与位于相同 BFC 中的浮动元素的外边距框重叠。（BFC 中的浮动元素不会覆盖 BFC 中的普通流元素）</li>\n<li><strong>阻止外边距重叠</strong> ：不同的 BFC 直接不会发生外边距重叠。（防止外边距重叠）</li>\n</ol>\n<blockquote>\n<p>简单来说：BFC 内部子元素不影响 BFC 外部元素</p>\n</blockquote>\n<h3>满足以下条件时，创建 BFC</h3>\n<blockquote>\n<p>翻译自 MDN：<a href=\"https://developer.mozilla.org/docs/Web/Guide/CSS/Block_formatting_context\">https://developer.mozilla.org/docs/Web/Guide/CSS/Block_formatting_context</a></p>\n</blockquote>\n<ul>\n<li>文档的根元素（<code>&lt;html&gt;</code>）</li>\n<li>浮动元素（<code>float</code>属性值不为<code>none</code>的元素）</li>\n<li>绝对定位元素（<code>position</code>属性值为<code>absolute</code>或<code>fixed</code>的元素）</li>\n<li>内联块元素（<code>display</code>属性值为<code>inline-block</code>的元素）</li>\n<li>表格单元格（<code>display</code>属性值为<code>table-cell</code>的元素。对于 HTML 表格单元格来说，这是默认值）</li>\n<li>表格标题（<code>display</code>属性值为<code>table-caption</code>的元素。对于 HTML 表格标题来说，这是默认值）</li>\n<li>由元素隐式创建的匿名表格单元格，这些元素的<code>display</code>属性值为<code>table</code>、<code>table-row</code>、<code>table-row-group</code>、<code>table-header-group</code>、<code>table-footer-group</code>（分别对应 HTML 表格、表格行、表格主体、表格头部和表格底部），或者<code>inline-table</code></li>\n<li><code>overflow</code>属性值不为<code>visible</code>和<code>clip</code>的块级元素</li>\n<li><code>display</code>属性值为<code>flow-root</code>的元素（<code>display: flow-root</code>）</li>\n<li><code>contain</code>属性值为<code>layout</code>、<code>content</code>或<code>paint</code>的元素</li>\n<li><code>flex</code>或<code>inline-flex</code>的直接子元素，如果它们本身既不是<code>flex</code>容器，也不是<code>grid</code>容器或<code>table</code>容器</li>\n<li><code>grid</code>或<code>inline-grid</code>的直接子元素，如果它们本身既不是<code>flex</code>容器，也不是<code>grid</code>容器或<code>inline-grid</code>容器</li>\n<li>多列容器（<code>column-count</code>或<code>column-width</code>属性值不为<code>auto</code>的元素，包括<code>column-count: 1</code>的元素）</li>\n<li><code>column-span</code>属性值为<code>all</code>的元素应始终创建一个新的格式化上下文，即使<code>column-span: all</code>的元素不包含在多列容器中（<a href=\"https://github.com/w3c/csswg-drafts/commit/a8634b96900279916bd6c505fda88dda71d8ec51\">规范更改</a>，<a href=\"https://crbug.com/709362\">Chrome bug</a>）</li>\n</ul>\n<blockquote>\n<p><code>clear: left</code> 声明：不允许浮动元素浮动到当前元素的左边，那么当前元素就会被移动到浮动元素的 <code>margin-box</code> 的下面<br><code>clear: right</code> <code>clear: both</code> 同理。参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/clear\">https://developer.mozilla.org/docs/Web/CSS/clear</a><br><a href=\"https://developer.mozilla.org/docs/Learn/CSS/CSS_layout/Floats\"><code>float</code></a> <a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_overflow\"><code>overflow</code></a></p>\n</blockquote>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/Visual_formatting_model\">视觉格式化模型 （Visual formatting model）</a></h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_display\"><code>display</code></a> 属性定义了元素的显示类型，它包括两个基本特性，决定了元素如何生成盒子。</p>\n<ol>\n<li>外部显示类型（outer display type）：决定了主要盒子本身在流式布局中的参与方式。</li>\n<li>内部显示类型（inner display type）：如果元素是非替换元素，它定义了元素生成的格式化上下文类型，决定了其后代盒子的布局方式。（替换元素的内部显示类型不在 CSS 的范围之内）</li>\n</ol>\n<p><a href=\"https://drafts.csswg.org/css-display/#the-display-properties\">display</a> 属性形式化语法如下：</p>\n<pre><code class=\"language-css\">display =\n  [ &lt;display-outside&gt; || &lt;display-inside&gt; ]         |\n  &lt;display-listitem&gt;                                |\n  &lt;display-internal&gt;                                |\n  &lt;display-box&gt;                                     |\n  &lt;display-legacy&gt;                                  |\n  &lt;display-outside&gt; || [ &lt;display-inside&gt; | math ]\n\n&lt;display-outside&gt;  = block | inline | run-in\n&lt;display-inside&gt;   = flow | flow-root | table | flex | grid | ruby\n&lt;display-listitem&gt; = &lt;display-outside&gt;? &amp;&amp; [ flow | flow-root ]? &amp;&amp; list-item\n&lt;display-internal&gt; = table-row-group | table-header-group |\n                     table-footer-group | table-row | table-cell |\n                     table-column-group | table-column | table-caption |\n                     ruby-base | ruby-text | ruby-base-container |\n                     ruby-text-container\n&lt;display-box&gt;      = contents | none\n&lt;display-legacy&gt;   = inline-block | inline-table | inline-flex | inline-grid\n</code></pre>\n<blockquote>\n<p>注：多值语法（<code>&lt;display-outside&gt; &lt;display-inside&gt;</code>）兼容性为 Chrome&gt;=115，考虑兼容性应使用 <code>&lt;display-legacy&gt;</code> 语法。</p>\n</blockquote>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context\">层叠上下文</a></h1>\n<blockquote>\n<p>翻译自 MDN 英文文档</p>\n</blockquote>\n<ul>\n<li><p>文档的根元素（<code>&lt;html&gt;</code>）</p>\n</li>\n<li><p><code>position</code> 属性值为 <code>absolute</code> 或 <code>relative</code>，且 <code>z-index</code> 属性值不为 <code>auto</code> 的元素</p>\n</li>\n<li><p><code>position</code> 属性值为 <code>fixed</code> 或 <code>sticky</code>（对于所有移动浏览器都适用，但不适用于旧的桌面浏览器）的元素</p>\n</li>\n<li><p><code>container-type</code> 属性值为 <code>size</code> 或 <code>inline-size</code>，用于<a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_container_queries\">容器查询</a>的元素</p>\n</li>\n<li><p>是 <code>flex</code> 容器的子元素，且 <code>z-index</code> 属性值不为 <code>auto</code></p>\n</li>\n<li><p>是 <code>grid</code> 容器的子元素，且 <code>z-index</code> 属性值不为 <code>auto</code></p>\n</li>\n<li><p><code>opacity</code> 属性值小于 <code>1</code> 的元素（参见<a href=\"https://www.w3.org/TR/css-color-3/#transparency\">透明度规范</a>）</p>\n</li>\n<li><p><code>mix-blend-mode</code> 属性值不为 <code>normal</code> 的元素</p>\n</li>\n<li><p>具有以下任一属性且属性值不为 <code>none</code> 的元素：</p>\n<ul>\n<li><code>transform</code></li>\n<li><code>filter</code></li>\n<li><code>backdrop-filter</code></li>\n<li><code>perspective</code></li>\n<li><code>clip-path</code></li>\n<li><code>mask</code> / <code>mask-image</code> / <code>mask-border</code></li>\n</ul>\n</li>\n<li><p><code>isolation</code> 属性值为 <code>isolate</code> 的元素</p>\n</li>\n<li><p><code>will-change</code> 属性值指定任何属性，该属性会在非初始值时创建一个层叠上下文（参见<a href=\"https://dev.opera.com/articles/css-will-change-property/\">此文章</a>）。</p>\n</li>\n<li><p><code>contain</code> 属性值为 <code>layout</code>、<code>paint</code> 或包含它们的组合值（例如 <code>contain: strict</code>、<code>contain: content</code>）的元素</p>\n</li>\n<li><p>放置在 <a href=\"https://developer.mozilla.org/docs/Glossary/Top_layer\">top layer</a> 的元素及其对应的 <code>::backdrop</code>。例如 <a href=\"https://developer.mozilla.org/docs/Web/API/Fullscreen_API\">fullscreen</a> 元素和 <a href=\"https://developer.mozilla.org/docs/Web/API/Popover_API\">popover</a> 元素</p>\n</li>\n</ul>\n<p>在一个层叠上下文中，子元素根据上述相同的规则进行层叠。重要的是，<strong>子层叠上下文的 <code>z-index</code> 值只在父层叠上下文中有意义</strong>。层叠上下文在父层叠上下文中被作为一个单独的单元对待。</p>\n<p>总结一下：</p>\n<ul>\n<li>层叠上下文可以嵌套在其他层叠上下文中，共同创建一个层叠上下文的层次结构</li>\n<li>每个层叠上下文完全独立于其兄弟元素：在处理层叠（层叠顺序变化或渲染）时，只考虑后代元素</li>\n<li>每个层叠上下文是自包含的：在元素内容进行层叠后，整个元素被视为父层叠上下文中的层叠顺序的一部分</li>\n</ul>\n<blockquote>\n<p><strong>注意：<strong>层叠上下文的层次结构是 HTML 元素层次结构的子集，因为只有特定的元素会创建层叠上下文。我们可以说，不创建自己的层叠上下文的元素被父层叠上下文所</strong>同化</strong>。</p>\n</blockquote>\n<p><img src=\"https://s2.loli.net/2023/11/05/EfjbQGsCJgnziPe.png\" alt=\"stacking context\"><br>图片<a href=\"https://www.zhangxinxu.com/wordpress/2016/01/understand-css-stacking-context-order-z-index/\">来源</a>，此处仅考虑 CSS2.1，忽略 CSS3 属性</p>\n<p>指定 <a href=\"https://developer.mozilla.org/docs/Web/CSS/isolation\"><code>isolation</code></a> 属性的值为 isolate 的元素会创建一个新的层叠上下文。主要配合 background-blend-mode 使用，用于把当前元素从当前层叠上下文隔离开来。</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_containment\">CSS containment</a></h1>\n<p>CSS <a href=\"https://developer.mozilla.org/docs/Web/CSS/contain\"><code>contain</code></a> 属性用于标识元素的子树是否独立于页面中的其他部分（允许浏览器自动性能优化）</p>\n<p>取值如下：</p>\n<ul>\n<li><code>none</code></li>\n<li><code>size</code>：开启<a href=\"https://drafts.csswg.org/css-contain/#size-containment\">size containment</a>。意味着此元素的子元素尺寸无法影响元素本身的尺寸，计算其尺寸时视为无子元素（包括伪元素，如 <code>::before</code> <code>::after</code> <code>::marker</code>）。可使用 <a href=\"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-size\"><code>contain-intrinsic-size</code></a> 指定应用此值的元素的尺寸。若不手动给予其尺寸，则多数时候元素尺寸将变为零。替换元素视为有自然宽度，无自然高度，无自然横纵比（可以有 <code>aspect-ratio</code>）</li>\n<li><code>inline-size</code>。仅在 inline 方向应用 size containment</li>\n<li><code>layout</code>：开启<a href=\"https://drafts.csswg.org/css-contain/#layout-containment\">layout containment</a>。建立独立的格式化上下文。外部元素不能影响内部布局，反之亦然</li>\n<li><code>style</code>：开启<a href=\"https://drafts.csswg.org/css-contain/#style-containment\">style containment</a>。确保由 <code>counter-increment</code> 和 <code>counter-set</code> 属性所创建的新计数器的作用域被限制为此子树。</li>\n<li><code>paint</code>：开启<a href=\"https://drafts.csswg.org/css-contain/#paint-containment\">paint containment</a>。与 <code>overflow: clip</code> 类似，不会创建格式化上下文，但会创建层叠上下文。</li>\n<li><code>strict</code>：相当于 size layout paint style。即开启所有局限。尽可能使用此值来获得优化。</li>\n<li><code>content</code>：相当于 layout paint style。相对于 strict 保险一些。因为没有应用尺寸局限，元素仍然可以响应其内容的尺寸。</li>\n</ul>\n<p>CSS <a href=\"https://developer.mozilla.org/docs/Web/CSS/content-visibility\"><code>content-visibility</code></a> 属性</p>\n<ul>\n<li>visible：默认值</li>\n<li>hidden：跳过渲染其 content-box 内容。并且不能被页内查找和 tab 键顺序导航等用户代理特性访问，亦不可被选中或获得焦点（这点类似于 <code>display: none</code> 和 <code>visibility: hidden</code>）。</li>\n<li>auto： 相当于 <code>contain: content</code></li>\n</ul>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_counter_styles\">CSS Counter</a></h1>\n<p>CSS 计数器可以说是 CSS 维护的变量，counter-increment 属性在 CSS 渲染时读取。</p>\n<p>这里只摘抄一下其形式化语法，使用方法参见<a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters\">MDN</a>即可。</p>\n<pre><code class=\"language-css\">counter-reset =\n  [ &lt;counter-name&gt; &lt;integer&gt;? | reversed(&lt;counter-name&gt;) &lt;integer&gt;? ]+  |\n  none\n\ncounter-increment =\n  [ &lt;counter-name&gt; &lt;integer&gt;? ]+  |\n  none\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSSOM_view/Coordinate_systems\">CSS 坐标系</a></h1>\n<p>Javascript 接口参见<a href=\"/posts/web-coordinate/\">此处</a></p>\n<ul>\n<li><code>offset</code>：相对于当前元素（padding-box）的左上角（即所谓偏移）</li>\n<li><code>client</code>：相对于视图（viewport）的左上角</li>\n<li><code>page</code>：相对于文档（即 Document.documentElement）的左上角</li>\n<li><code>screen</code>：相当于屏幕（显示器）的左上角</li>\n</ul>\n","tags":["css"]},{"id":"web-animation-api","url":"https://yieldray.fun/posts/web-animation-api","title":"Web Animation API","date_published":"2023-10-31T18:08:59.000Z","date_modified":"2023-10-31T18:08:59.000Z","content_text":"<h1><a href=\"https://developer.mozilla.org/docs/Web/API/Web_Animations_API\">Web Animation API</a></h1>\n<p>这里主要写一下在 Javascript 中操纵 Web Animation API，CSS 的部分有所省略。</p>\n<blockquote>\n<p>Useful links to learn CSS animation itself:<br><a href=\"https://www.joshwcomeau.com/animation/keyframe-animations/\">https://www.joshwcomeau.com/animation/keyframe-animations/</a><br><a href=\"https://www.cnblogs.com/coco1s/p/15796478.html\">https://www.cnblogs.com/coco1s/p/15796478.html</a></p>\n</blockquote>\n<p><code>Animation()</code> 构造函数用于创建 <code>Animation</code> 实例，该实例是一个 <code>EventTarget</code>，可用于监听动画事件和控制动画。<br>正如构造函数定义的，一个 <code>Animation</code> 由 <code>AnimationEffect</code> 和 <code>AnimationTimeline</code> 构成。<br>effect 声明动画效果，timeline 声明动画进行到哪一步。</p>\n<p><code>KeyframeEffect</code> 实现了 <code>AnimationEffect</code> 接口，是目前唯一的动画效果。它可以给指定元素（或伪元素）声明关键帧动画。<br><code>Element.animate()</code> 方法是一个快捷函数，可以直接在某个元素上创建 <code>KeyframeEffect</code> 动画并开始运行动画。</p>\n<pre><code class=\"language-ts\">const element = document.querySelector(&quot;.target&quot;);\n\nconst keyframes = [{ rotate: &quot;0deg&quot; }, { rotate: &quot;360deg&quot; }];\n// or:  const keyframes = { rotate: [&quot;0deg&quot;, &quot;360deg&quot;] };\nconst options = { duration: 3000, fill: &quot;forwards&quot; as const };\n// or: const options = 3000;\nconst effect = new KeyframeEffect(element, keyframes, options);\n\nconst timeline = document.timeline; // 默认时间轴\n\nconst animation = new Animation(effect, timeline);\n\nanimation.play();\n\n// or:\nelement.animate(keyframes, options);\n</code></pre>\n<p><img src=\"https://www.plantuml.com/plantuml/svg/VLDDImCn4BtlhyXHXVq38bB1dXGKBDxJTBQ4vaEIj5BmnvlTc4vcT-mjC-_DIoPlPXMJ3EbeJQUCZ56i3LXmgTCkGHYa0l7aj9L9RWrKoYaJDZBi8JMPUYvqxTqyNWy3gCfwWVCGf0MIH2gdzsxaUIlLeO3ulYhLf8hgHbljzWopm6zVesgSNvEhm6XNcttswZX-I4sZt0AYWXygU6EOCWhGeayDtqm9fh6WQsmG3pzzVx4E3IDvqG9z_uWjYtiXfpFSt5uZvPikOQHKt_UNxytCmzynBb-9yq4dey3pmQ0XkOQucE_qvTIn-FyFfFIfvuON8TLexBWTBEwzrHpyjRksvKAYA3DvYAC3TIl6HfsIJe6f3bnZ1h3-11oJBbMjaBlTT7pLCO63a2iBHBsRSYn2jtJHMs4lbcgU49vXBQtcvUUhHqFXAOCLHXnLoMwsWV0KPVHMu7P7Qtu1\" alt=\"classes.svg\"></p>\n<h1>Typescript 定义</h1>\n<iframe src=\"https://codesandbox.io/embed/animation-types-t25p7r?fontsize=14&hidenavigation=1&theme=dark\"\n     style=\"width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;\"\n     title=\"animation-types\"\n     allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n     sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n   ></iframe>\n\n<h1><a href=\"https://developer.mozilla.org/docs/Web/API/AnimationEffect\">AnimationEffect</a> 动画效果</h1>\n<p>AnimationEffect &lt;|-- KeyframeEffect</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/KeyframeEffect\">KeyframeEffect</a></h2>\n<p>目前只有 KeyframeEffect，即关键帧效果</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/API/AnimationTimeline\">AnimationTimeline</a> 动画时间轴</h1>\n<p>AnimationTimeline &lt;|-- DocumentTimeline<br>AnimationTimeline &lt;|-- ScrollTimeline<br>ScrollTimeline &lt;|-- ViewTimeline</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/DocumentTimeline\">DocumentTimeline</a></h2>\n<p>时间驱动的时间轴</p>\n<blockquote>\n<p>默认情况下，附加在一个元素上的动画在文档时间轴 (<code>DocumentTimeline</code>) 上运行。<br>当网页加载时，它的起源时间从 0 开始，并随时间顺序增加。</p>\n</blockquote>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/ScrollTimeline\">ScrollTimeline</a></h2>\n<p>连接到某个滚动容器沿着特定轴（如：水平方向或竖直方向）的滚动位置（即滚动了多少距离）的时间轴。<br>它将滚动范围内的位置转换为进度的百分比。开始滚动位置表示 0% 进度，结束滚动位置表示 100% 进度。</p>\n<p>参考： <a href=\"https://developer.chrome.com/articles/scroll-driven-animations/#scroll-progress-timeline\">https://developer.chrome.com/articles/scroll-driven-animations/#scroll-progress-timeline</a></p>\n<pre><code class=\"language-js\">const timeline = new ScrollTimeline({\n    source: document.documentElement,\n    axis: &quot;block&quot;,\n});\n</code></pre>\n<p>CSS 属性 <a href=\"https://developer.mozilla.org/docs/Web/CSS/scroll-timeline-name\">scroll-timeline-name</a> 声明一个以双横线开头的名称，应用在滚动容器上。<br><a href=\"https://developer.mozilla.org/docs/Web/CSS/scroll-timeline-axis\">scroll-timeline-axis</a> 属性声明监听滚动的方向。<br><a href=\"https://developer.mozilla.org/docs/Web/CSS/scroll-timeline\">scroll-timeline</a> 属性是以上两个属性的简写。</p>\n<p>CSS 属性 <a href=\"https://developer.mozilla.org/docs/Web/CSS/animation-timeline\">animation-timeline</a> 则可以引用此名称，这就将当前元素动画的时间轴设置为该滚动容器的 ScrollTimeline</p>\n<p>animation-timeline 也可以指定为 <a href=\"https://developer.mozilla.org/docs/Web/CSS/animation-timeline/scroll\"><code>scroll()</code></a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"ScrollTimeline\" src=\"https://codepen.io/YieldRay/embed/xxMObOr?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/xxMObOr\">\n  ScrollTimeline</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/ViewTimeline\">ViewTimeline</a></h2>\n<p>连接到特定元素在其滚动容器中的相对位置的时间轴</p>\n<p>参考：<a href=\"https://developer.chrome.com/articles/scroll-driven-animations/#getting-practical-with-view-progress-timeline\">https://developer.chrome.com/articles/scroll-driven-animations/#getting-practical-with-view-progress-timeline</a></p>\n<pre><code class=\"language-js\">const timeline = new ViewTimeline({\n    subject: document.querySelector(&quot;.subject&quot;),\n    axis: &quot;block&quot;,\n    inset: [CSS.px(&quot;200&quot;), CSS.px(&quot;300&quot;)],\n});\n\n$el.animate(\n    {\n        opacity: [0, 1],\n    },\n    {\n        timeline,\n        rangeStart: &quot;entry 25%&quot;,\n        rangeEnd: &quot;cover 50%&quot;,\n    },\n);\n</code></pre>\n<p>与上面的 scroll-timeline 类似的是：</p>\n<p>CSS 属性 <a href=\"https://developer.mozilla.org/docs/Web/CSS/scroll-timeline-name\">view-timeline-name</a> 声明一个自定义名称，定义在监听可见性的元素上。<br><a href=\"https://developer.mozilla.org/docs/Web/CSS/view-timeline-axis\">view-timeline-axis</a> 属性声明监听可见性的方向。<br><a href=\"https://developer.mozilla.org/docs/Web/CSS/view-timeline\">view-timeline</a> 属性是以上两个属性的简写。</p>\n<p>同理，CSS 属性 <a href=\"https://developer.mozilla.org/docs/Web/CSS/animation-timeline\">animation-timeline</a> 可以引用此名称，这就将当前元素动画的时间轴设置为该元素的 ViewTimeline （在同一个元素上设置 view-timeline-name 然后用 animation-timeline 引用自身是很常见的用法，这也相当于直接将 animation-timeline 设置为 <code>view()</code> ，参见下面的演示）</p>\n<p>此外还可以使用 <a href=\"https://developer.mozilla.org/docs/Web/CSS/view-timeline-inset\">view-timeline-inset</a> 属性可指定一个或两个值，用于指定偏移时间线位置的开始和/或结束插入（或结束）值。</p>\n<p>animation-timeline 也可以指定为 <a href=\"https://developer.mozilla.org/docs/Web/CSS/animation-timeline/view\"><code>view()</code></a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"ViewTimeline\" src=\"https://codepen.io/YieldRay/embed/XWOKgYW?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/XWOKgYW\">\n  ViewTimeline</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>其它 CSS 属性</h1>\n<p>CSS 部分定义在 <a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_scroll-driven_animations\">CSS scroll-driven animations</a> 模块</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_scroll-driven_animations\">https://developer.mozilla.org/docs/Web/CSS/CSS_scroll-driven_animations</a></p>\n","tags":["web-api"]},{"id":"css-at-container","url":"https://yieldray.fun/posts/css-at-container","title":"css容器查询","date_published":"2023-10-16T16:17:07.000Z","date_modified":"2025-12-31T00:00:00.000Z","content_text":"<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_Container_Queries\">https://developer.mozilla.org/docs/Web/CSS/CSS_Container_Queries</a></p>\n<h1>Syntax</h1>\n<pre><code class=\"language-css\">@container &lt;container-condition&gt; {\n  &lt;stylesheet&gt;\n}\n</code></pre>\n<blockquote>\n<p>注：<a href=\"https://caniuse.com/css-media-range-syntax\">Media Queries: Range Syntax</a></p>\n</blockquote>\n<pre><code class=\"language-css\">/* @container (min-width: 400px) and (max-width: 800px) */\n/* @container (width &gt;= 400) and (width &lt;= 800px) */\n@container (400px &lt;= width &lt;= 800px) {\n    h2 {\n        font-size: 1.5em;\n    }\n}\n</code></pre>\n<h1>基本语法</h1>\n<blockquote>\n<p>注：一般都是水平书写，所以只需要知道 inline 为水平方向，block 为竖直方向<br>对于水平书写方向 <code>writing-mode: horizontal-tb</code>，inline 为水平方向，block 为竖直方向<br>对于竖直书写方向 <code>writing-mode: vertical-rl</code>，inline 为竖直方向，block 为水平方向</p>\n</blockquote>\n<pre><code class=\"language-css\">.post {\n    container-name: sidebar;\n    /* 将当前容器命名为 sidebar */\n    container-type: inline-size;\n    /* 默认值 normal，表示不参与容器查询 */\n    /* inline-size，表示仅参与 inline轴 的容器查询 */\n    /* size，表示参与 inline轴 和 block轴 的容器查询 */\n\n    /* \n    简写\n    container: sidebar / inline-size; \n    */\n}\n\n/* 不指定容器名称则查询所有容器 */\n@container sidebar(width &lt; 650px) {\n    /* 选中在 sidebar 容器内符合指定查询的... */\n    /* container-type: inline-size 指定容器仅参与水平方向查询，\n       即只能查询 width，不能查询 height */\n    .card {\n        width: 50%;\n        background-color: gray;\n        font-size: 1em;\n    }\n}\n</code></pre>\n<p>支持嵌套使用</p>\n<h1>容器 CSS 局限</h1>\n<p>设置了 container-type 的容器会同时被施加 CSS 局限。</p>\n<table>\n<thead>\n<tr>\n<th><code>container-type</code></th>\n<th><code>normal</code></th>\n<th><code>inline-size</code></th>\n<th><code>size</code></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>contain</code></td>\n<td><code>layout</code> <code>style</code> <code>size</code></td>\n<td><code>layout</code> <code>style</code> <code>inline-size</code></td>\n<td><code>none</code></td>\n</tr>\n</tbody></table>\n<p>CSS 局限：<a href=\"https://developer.mozilla.org/docs/Web/CSS/contain\"><code>contain</code></a> 属性用于标识元素的子树是否独立于页面中的其他部分</p>\n<p>contain 属性取值如下：</p>\n<table>\n<thead>\n<tr>\n<th>值</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>none</code></td>\n<td>-</td>\n</tr>\n<tr>\n<td><code>size</code></td>\n<td>开启<a href=\"https://drafts.csswg.org/css-contain/#size-containment\">size containment</a>。意味着此元素的子元素尺寸无法影响元素本身的尺寸，计算其尺寸时视为无子元素（包括伪元素，如 <code>::before</code> <code>::after</code> <code>::marker</code>）。可使用 <a href=\"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-size\"><code>contain-intrinsic-size</code></a> 指定应用此值的元素的尺寸。若不手动给予其尺寸，则多数时候元素尺寸将变为零。替换元素视为有自然宽度，无自然高度，无自然横纵比（可以有 <code>aspect-ratio</code>）。</td>\n</tr>\n<tr>\n<td><code>inline-size</code></td>\n<td>仅在 inline 方向应用 size containment</td>\n</tr>\n<tr>\n<td><code>layout</code></td>\n<td>开启<a href=\"https://drafts.csswg.org/css-contain/#layout-containment\">layout containment</a>。建立独立的格式化上下文。外部元素不能影响内部布局，反之亦然</td>\n</tr>\n<tr>\n<td><code>style</code></td>\n<td>开启<a href=\"https://drafts.csswg.org/css-contain/#style-containment\">style containment</a>。确保由 <code>counter-increment</code> 和 <code>counter-set</code> 属性所创建的新计数器的作用域被限制为此子树。</td>\n</tr>\n<tr>\n<td><code>paint</code></td>\n<td>开启<a href=\"https://drafts.csswg.org/css-contain/#paint-containment\">paint containment</a>。与 <code>overflow: clip</code> 类似，不会创建格式化上下文，但会创建层叠上下文。</td>\n</tr>\n<tr>\n<td><code>strict</code></td>\n<td>相当于 size layout paint style。即开启所有局限。尽可能使用此值来获得优化。</td>\n</tr>\n<tr>\n<td><code>content</code></td>\n<td>相当于 layout paint style。相对于 strict 保险一些。因为没有应用尺寸局限，元素仍然可以响应其内容的尺寸。</td>\n</tr>\n</tbody></table>\n<h1>容器样式查询</h1>\n<p>查询含有指定键值对属性的容器（例如：<code>--accent-color</code> 属性的值为 <code>blue</code>）<br>属性的值是以 <a href=\"https://developer.mozilla.org/docs/Web/CSS/computed_value\">computed_value</a> 进行评估的（简而言之，考虑 inherits）</p>\n<pre><code class=\"language-css\">@container style(--accent-color: blue) {\n    /* &lt;stylesheet&gt; */\n}\n\n/* \n如果不将自定义属性的数据类型声明为 &lt;color&gt;\n则 blue 与 #0000ff 不相同\n*/\n@property --accent-color {\n    syntax: &quot;&lt;color&gt;&quot;;\n    inherits: true;\n}\n</code></pre>\n<h1>容器查询单位</h1>\n<table>\n<thead>\n<tr>\n<th>Unit</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>cqw</td>\n<td>表示容器查询宽度（Container Query Width）占比。1cqw 等于容器宽度的 1%</td>\n</tr>\n<tr>\n<td>cqh</td>\n<td>表示容器查询高度（Container Query Height）占比。1cqh 等于容器高度的 1%</td>\n</tr>\n<tr>\n<td>cqi</td>\n<td>表示容器查询 inline 轴方向的尺寸（Container Query Inline-Size）占比</td>\n</tr>\n<tr>\n<td>cqb</td>\n<td>表示容器查询 block 轴方向的尺寸（Container Query Block-Size）占比</td>\n</tr>\n<tr>\n<td>cqmin</td>\n<td>表示容器查询较小尺寸的（Container Query Min）占比</td>\n</tr>\n<tr>\n<td>cqmax</td>\n<td>表示容器查询较大尺寸的（Container Query Min）占比</td>\n</tr>\n</tbody></table>\n","tags":["css"]},{"id":"js-decorator","url":"https://yieldray.fun/posts/js-decorator","title":"Javascript装饰器(stage 3)","date_published":"2023-10-03T19:49:03.000Z","date_modified":"2023-10-03T19:49:03.000Z","content_text":"<p>stage3 装饰器在 typescript 5.0 得到正式支持了</p>\n<p>stage2 装饰器 <a href=\"/posts/ts-decorator/\">参见此处</a></p>\n<p>它们在 typescript 中的差异，参见<a href=\"https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#differences-with-experimental-legacy-decorators\">此处</a></p>\n<p>Proposal: <a href=\"https://github.com/tc39/proposal-decorators\">https://github.com/tc39/proposal-decorators</a></p>\n<p>stage2 的装饰器基于 <code>PropertyDescriptor</code>，stage3 的装饰器则不同</p>\n<p>最大的区别是 stage3 装饰器不允许改变类成员的类型（字段、访问器、方法）（出于性能考虑）</p>\n<h1>例子</h1>\n<pre><code class=\"language-ts\">function loggedMethod&lt;This, Args extends any[], Return&gt;(\n    target: (this: This, ...args: Args) =&gt; Return,\n    context: ClassMethodDecoratorContext&lt;This, (this: This, ...args: Args) =&gt; Return&gt;,\n) {\n    const methodName = String(context.name);\n\n    function replacementMethod(this: This, ...args: Args): Return {\n        console.log(`LOG: Entering method &#39;${methodName}&#39;.`);\n        const result = target.call(this, ...args);\n        console.log(`LOG: Exiting method &#39;${methodName}&#39;.`);\n        return result;\n    }\n\n    return replacementMethod;\n}\n\nfunction bound&lt;This, Args extends any[], Return&gt;(\n    target: (this: This, ...args: Args) =&gt; Return,\n    context: ClassMethodDecoratorContext&lt;This, (this: This, ...args: Args) =&gt; Return&gt;,\n) {\n    const methodName = String(context.name);\n\n    if (context.private) {\n        throw new Error(`&#39;bound&#39; cannot decorate private properties like ${methodName}.`);\n    }\n    context.addInitializer(function (this: This) {\n        Reflect.set(this as object, methodName, target.bind(this));\n    });\n}\n\nclass Person {\n    name: string;\n    constructor(name: string) {\n        this.name = name;\n    }\n\n    @loggedMethod\n    @bound\n    greet() {\n        console.log(`Hello, my name is ${this.name}.`);\n    }\n}\n\nconst { greet } = new Person(&quot;Ray&quot;);\ngreet();\n</code></pre>\n<h1>定义</h1>\n<p><a href=\"https://github.com/tc39/proposal-decorators#detailed-design\">提案</a>对装饰器函数的定义如下</p>\n<pre><code class=\"language-ts\">type Decorator = (\n    value: Input,\n    context: {\n        kind: string;\n        name: string | symbol;\n        access: {\n            get?(): unknown;\n            set?(value: unknown): void;\n        };\n        private?: boolean;\n        static?: boolean;\n        addInitializer?(initializer: () =&gt; void): void;\n    },\n) =&gt; Output | void;\n</code></pre>\n<blockquote>\n<p>注意：由于讨论的是 typescript 实现，具体以下面 typescript 的定义为准</p>\n</blockquote>\n<p>可以发现 stage3 的装饰器的形式更固定，其第一个参数接收被装饰的值，第二个参数是对应的 context，context 描述了值的形状，<br>addInitializer 添加的函数将在实例化之前执行（具体参见下文），函数内部 this 被绑定为对应实例，<br>函数体可以返回一个新方法来替换原来的值，参见下表。\naccess 对象包含一组方法，它们的第一个参数都是<em>任意对象</em>，这些方法会去操作该对象上与当被装饰的键名相同的值</p>\n<table>\n<thead>\n<tr>\n<th>kind</th>\n<th>Input</th>\n<th>Output</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>class</td>\n<td><code>Class</code></td>\n<td><code>Callable</code></td>\n</tr>\n<tr>\n<td>method</td>\n<td><code>Method</code></td>\n<td><code>Method</code></td>\n</tr>\n<tr>\n<td>setter</td>\n<td><code>(T):void</code></td>\n<td><code>(T):void</code></td>\n</tr>\n<tr>\n<td>getter</td>\n<td><code>():T</code></td>\n<td><code>():T</code></td>\n</tr>\n<tr>\n<td>accessor</td>\n<td><code>{ get():T, set(T):void }</code></td>\n<td><code>{ get():T, set(T):void, init(T):T }</code></td>\n</tr>\n<tr>\n<td>field</td>\n<td><code>undefined</code></td>\n<td><code>(T) =&gt; T</code></td>\n</tr>\n</tbody></table>\n<p><a href=\"https://github.com/microsoft/TypeScript/blob/main/src/lib/decorators.d.ts\">lib.decorators.d.ts</a></p>\n<p>装饰器只能修饰类或类的成员（相比于 stage2 删除了参数装饰器，但 proposal 的扩展部分描述了参数装饰器）<br>一共有六种装饰器，对应 kind 的值为 class method getter setter field accessor</p>\n<pre><code class=\"language-ts\">type DecoratorContext =\n    | ClassDecoratorContext // 类\n    | ClassMemberDecoratorContext; // 类的成员\n\ntype ClassMemberDecoratorContext =\n    | ClassMethodDecoratorContext\n    | ClassGetterDecoratorContext\n    | ClassSetterDecoratorContext\n    | ClassFieldDecoratorContext\n    | ClassAccessorDecoratorContext;\n</code></pre>\n<p>具体声明如下（删去了 metadata）<br>注意：typescript 实现是 proposal 描述的超集，其添加了一些内容</p>\n<pre><code class=\"language-ts\">interface ClassDecoratorContext&lt;Class extends abstract new (...args: any) =&gt; any = abstract new (...args: any) =&gt; any&gt; {\n    readonly kind: &quot;class&quot;;\n    readonly name: string | undefined;\n    addInitializer(initializer: (this: Class) =&gt; void): void;\n}\n\ninterface ClassMethodDecoratorContext&lt;\n    This = unknown,\n    Value extends (this: This, ...args: any) =&gt; any = (this: This, ...args: any) =&gt; any,\n&gt; {\n    readonly kind: &quot;method&quot;;\n    readonly name: string | symbol;\n    readonly static: boolean;\n    readonly private: boolean;\n    readonly access: {\n        has(object: This): boolean;\n        get(object: This): Value;\n    };\n    addInitializer(initializer: (this: This) =&gt; void): void;\n}\n\ninterface ClassGetterDecoratorContext&lt;This = unknown, Value = unknown&gt; {\n    readonly kind: &quot;getter&quot;;\n    readonly name: string | symbol;\n    readonly static: boolean;\n    readonly private: boolean;\n    readonly access: {\n        has(object: This): boolean;\n        get(object: This): Value;\n    };\n    addInitializer(initializer: (this: This) =&gt; void): void;\n}\n\ninterface ClassSetterDecoratorContext&lt;This = unknown, Value = unknown&gt; {\n    readonly kind: &quot;setter&quot;;\n    readonly name: string | symbol;\n    readonly static: boolean;\n    readonly private: boolean;\n    readonly access: {\n        has(object: This): boolean;\n        set(object: This, value: Value): void;\n    };\n    addInitializer(initializer: (this: This) =&gt; void): void;\n}\n\n// 即 getter 和 setter\ninterface ClassAccessorDecoratorContext&lt;This = unknown, Value = unknown&gt; {\n    readonly kind: &quot;accessor&quot;;\n    readonly name: string | symbol;\n    readonly static: boolean;\n    readonly private: boolean;\n    readonly access: {\n        has(object: This): boolean;\n        get(object: This): Value;\n        set(object: This, value: Value): void;\n    };\n    addInitializer(initializer: (this: This) =&gt; void): void;\n}\n\ninterface ClassFieldDecoratorContext&lt;This = unknown, Value = unknown&gt; {\n    readonly kind: &quot;field&quot;;\n    readonly name: string | symbol;\n    readonly static: boolean;\n    readonly private: boolean;\n    readonly access: {\n        has(object: This): boolean;\n        get(object: This): Value;\n        set(object: This, value: Value): void;\n    };\n    addInitializer(initializer: (this: This) =&gt; void): void;\n}\n</code></pre>\n<h1>使用 <code>addInitializer</code> 添加初始化逻辑</h1>\n<p>装饰器 context 对象上的 <code>addInitializer</code> 方法可用于对类或类成员添加初始化函数。<br>该函数将在值已经定义完成后运行，具体执行时机如下：</p>\n<ul>\n<li>类装饰器：在类完全定义<em>之后</em>运行，包括类的静态字段指定<em>之后</em> （即，在类的 prototype 属性和自身属性都确定之后）</li>\n<li>类成员装饰器：在类被构造（即，被 new）的过程中执行，在类字段初始化<em>之前</em>执行</li>\n<li>类静态成员装饰器：在类定义时执行，在静态字段定义<em>之前</em>，但在类成员定义<em>之后</em>执行</li>\n</ul>\n<blockquote>\n<p>附：<a href=\"https://github.com/tc39/proposal-decorators?#adding-initialization-logic-with-addinitializer\">原文</a></p>\n</blockquote>\n<h1>需要注意的前置知识</h1>\n<p>装饰器用于修饰 class 语法创建的类<br>但是 class 语法无法在 prototype 属性上设置字段（只能设置方法和访问器）<br>设置字段的语法是将字段设置在被实例化的对象身上</p>\n<p>类字段是这样初始化的：</p>\n<ul>\n<li>对于基类，在构造函数调用前初始化</li>\n<li>对于派生类，在 super() 后立刻初始化</li>\n</ul>\n<blockquote>\n<p>这个初始化顺序其实和其它的语言没区别，比如 c++<br>在 constructor 中使用 this 和 java 是一样的，也相当于 c++ 使用 override</p>\n</blockquote>\n<p>super 关键字仅允许在派生类和对象字面量中的方法使用<br>一个对象内部声明的方法（而不是声明一个字段。我们知道，字段可以是包括函数的任意值，但作为字段的函数并不绑定到对象上）<br>具有特殊内部属性 <code>[[HomeObject]]</code> 绑定到这个对象<br>这个函数的 super 则指向自身的 <code>[[HomeObject]]</code> 内部属性（与 this 不同，它是绑定的值）<br>但是只能在 super 上访问 super 指向的对象的 <code>[[Prototype]]</code> 上的属性，不能访问对象自身的属性</p>\n<blockquote>\n<p>参见：<a href=\"https://zh.javascript.info/class-inheritance#shen-ru-nei-bu-tan-jiu-he-homeobject\">此例子</a></p>\n</blockquote>\n<pre><code class=\"language-js\">let obj = {\n    fn1() {\n        // 对象方法，是一个具名函数。这个函数不能被 new，允许使用 super\n        //（super 绑定到 obj，而 this 则始终指向被调用的对象）\n        // obj.fn1.[[HomeObject]] == obj1\n    },\n    fn2: function () {\n        // 对象字段，是一个匿名函数。这个函数可以被 new\n        // 不允许使用 super\n    },\n};\n\nnew obj.fn1(); // TypeError: obj.fn1 is not a constructor\nnew obj.fn2(); // =&gt; fn2 {}\n</code></pre>\n<p>类方法只不过是设置在类的 prototype 属性上的对象方法而已</p>\n<h1>accessor 关键字</h1>\n<p>accessor 关键字的作用是将 field 自动变为 getter 和 setter<br>也就是 context.kind 会变成 accessor</p>\n<p>显然，accessor 相当于 getter 和 setter，那就是设置在类的 prototype 上，而不是实例对象上<br>因此也可以将 accessor 看作是在 prototype 上设置字段（弥补了没有 accessor 关键字时只能在实例对象上设置字段的缺陷）</p>\n<p>（虽然没什么用，但是这样还是不能在 实例对象自身 上设置具有 <code>[[HomeObject]]</code> 内部属性的对象方法）</p>\n<pre><code class=\"language-js\">class C {\n    accessor x = 1;\n}\n\n// 相当于\n\nclass C {\n    #x = 1;\n\n    get x() {\n        return this.#x;\n    }\n\n    set x(val) {\n        this.#x = val;\n    }\n}\n</code></pre>\n<h1>metadata</h1>\n<p>metadata 为 proposal 的<a href=\"https://github.com/tc39/proposal-decorators/blob/master/EXTENSIONS.md\">扩展部分</a></p>\n<p>见：<a href=\"https://github.com/tc39/proposal-decorator-metadata\">tc39/proposal-decorator-metadata</a></p>\n<p>待补充。。。</p>\n","tags":["js"]},{"id":"web-view-transition","url":"https://yieldray.fun/posts/web-view-transition","title":"View Transitions API","date_published":"2023-09-02T11:11:11.000Z","date_modified":"2023-09-17T23:23:23.000Z","content_text":"<h1><a href=\"https://developer.mozilla.org/docs/Web/API/View_Transitions_API\">View Transitions API</a></h1>\n<p>调用 <a href=\"https://developer.mozilla.org/docs/Web/API/Document/startViewTransition\">document.startViewTransition(callback)</a> 使得 DOM 变动时，浏览器生成如下伪元素结构<br>（注：DOM 变动可以是 Add/removing elements, changing class names, changing styles…）</p>\n<pre><code>::view-transition\n├─ ::view-transition-group(root)\n│  └─ ::view-transition-image-pair(root)\n│     ├─ ::view-transition-old(root)\n      └─ ::view-transition-new(root)\n├─ ::view-transition-group(name-1)\n│  └─ ::view-transition-image-pair(name-1)\n│     ├─ ::view-transition-old(name-1)     👈 快照\n│     └─ ::view-transition-new(name-1)     👈 实时表示\n├─ ::view-transition-group(name-2)\n│  └─ ::view-transition-image-pair(name-2)\n│     ├─ ::view-transition-old(name-2)\n│     └─ ::view-transition-new(name-2)\n│ /* and so one... */\n</code></pre>\n<p>其中， <code>root</code> 是元素 CSS 属性 <code>view-transition-name</code> 的默认值</p>\n<pre><code class=\"language-ts\">// 伪代码：模拟 document.startViewTransition 的实现流程\nfunction startViewTransition(callback: () =&gt; Promise&lt;void&gt; | void): ViewTransition {\n    // 1. 检查当前是否有活跃的视图过渡\n    if (activeViewTransition &amp;&amp; !activeViewTransition.isCreatedViaScriptAPI()) {\n        // 如果有导航触发的过渡，脚本触发的过渡会被跳过\n        return ViewTransition.createSkipped(callback);\n    }\n\n    // 2. 如果有活跃的 transition，先跳过它\n    if (activeViewTransition) {\n        activeViewTransition.skipTransition();\n    }\n\n    // 3. 视图必须可用，否则直接返回 null\n    if (!documentViewAvailable()) {\n        return null;\n    }\n\n    // 4. 创建新的 ViewTransition 对象\n    const transition = ViewTransition.createFromScript(callback /* types */);\n\n    // 5. 如果文档当前不可见，直接跳过动画\n    if (document.hidden) {\n        transition.skipTransition(/* reason: invalid state */);\n        return transition;\n    }\n\n    // 6. 执行回调，等待 DOM 更新\n    Promise.resolve()\n        .then(() =&gt; callback())\n        .then(() =&gt; {\n            // 7. 过渡动画开始\n            transition.startAnimation();\n        })\n        .catch(() =&gt; {\n            // 8. 回调失败则放弃过渡\n            transition.skipTransition(/* reason: callback rejected */);\n        });\n\n    // 9. 返回 ViewTransition 对象，包含 ready/finished 等 Promise\n    return transition;\n}\n</code></pre>\n<p>简单来说，对于每组 <code>view-transition-name</code>， view transition 捕获其变动前和变动后的 DOM 结构<br>（注：与 DOM 元素本身无关，仅与 <code>view-transition-name</code> 指定的名称有关）<br>然后将 <code>::view-transition-old</code> 过渡到 <code>::view-transition-new</code><br>（注：<code>view-transition-name</code> 在渲染前后的 DOM 中必须唯一）</p>\n<p><code>::view-transition-old(root)</code> 捕获了旧视图（的快照）， <code>::view-transition-new(root)</code> 捕获了新视图（的实时表示）<br>它们都被渲染为 CSS 的<a href=\"https://developer.mozilla.org/docs/Web/CSS/Replaced_element\">可替换元素</a>（<a href=\"https://html.spec.whatwg.org/multipage/rendering.html#replaced-elements\">replaced elements</a>） （就像 <code>&lt;img /&gt;</code>）</p>\n<blockquote>\n<p>快照意味着旧视图是<strong>静态</strong>的截图，实时则表示新视图是<strong>动态</strong>反应真实DOM渲染结果的。</p>\n</blockquote>\n<p>注意，对于旧视图和新视图，仅当其实际存在时，对应伪元素才存在<br>（注：使用 <a href=\"https://developer.mozilla.org/docs/Web/CSS/:only-child\">:only-child</a>伪类可选中此情况）\n但 <code>::view-transition-image-pair</code> 及其上层伪元素总是存在的</p>\n<p>使用参考：<br><a href=\"https://developer.chrome.com/docs/web-platform/view-transitions/#api-reference\">https://developer.chrome.com/docs/web-platform/view-transitions/#api-reference</a><br><a href=\"https://developer.chrome.com/blog/view-transitions-case-studies/\">https://developer.chrome.com/blog/view-transitions-case-studies/</a><br>Debug 参考：<br>使用 DevTools 的动画面板即可，演示：<a href=\"https://http203-playlist.netlify.app/\">https://http203-playlist.netlify.app/</a></p>\n<hr>\n<p><a href=\"https://www.w3.org/TR/css-view-transitions-1/#ua-styles\">规范</a>定义默认样式如下（非常重要）</p>\n<pre><code class=\"language-css\">:root {\n    view-transition-name: root;\n}\n\n:root::view-transition {\n    position: fixed;\n    inset: 0;\n}\n\n:root::view-transition-group(*) {\n    position: absolute;\n    top: 0;\n    left: 0;\n\n    animation-duration: 0.25s;\n    animation-fill-mode: both;\n}\n\n:root::view-transition-image-pair(*) {\n    position: absolute;\n    inset: 0;\n\n    animation-duration: inherit;\n    animation-fill-mode: inherit;\n    animation-delay: inherit;\n}\n\n:root::view-transition-old(*),\n:root::view-transition-new(*) {\n    position: absolute;\n    inset-block-start: 0;\n    inline-size: 100%;\n    block-size: auto;\n\n    animation-duration: inherit;\n    animation-fill-mode: inherit;\n    animation-delay: inherit;\n}\n\n/* Default cross-fade transition */\n@keyframes -ua-view-transition-fade-out {\n    to {\n        opacity: 0;\n    }\n}\n@keyframes -ua-view-transition-fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n/* Keyframes for blending when there are 2 images */\n@keyframes -ua-mix-blend-mode-plus-lighter {\n    from {\n        mix-blend-mode: plus-lighter;\n    }\n    to {\n        mix-blend-mode: plus-lighter;\n    }\n}\n</code></pre>\n<p>Chromium 默认样式如下</p>\n<pre><code class=\"language-css\">html::view-transition {\n    position: fixed;\n    inset: 0px;\n}\n\nhtml::view-transition-group(root) {\n    right: 0px;\n    bottom: 0px;\n}\nhtml::view-transition-group() {\n    position: absolute;\n    top: 0px;\n    left: 0px;\n    animation-duration: 0.25s;\n    animation-fill-mode: both;\n}\n\nhtml::view-transition-image-pair(root) {\n    isolation: isolate;\n}\nhtml::view-transition-image-pair() {\n    animation-duration: inherit;\n    animation-fill-mode: inherit;\n}\n::view-transition-image-pair() {\n    position: absolute;\n    inset: 0px;\n}\n\nhtml::view-transition-old(root) {\n    mix-blend-mode: plus-lighter;\n}\nhtml::view-transition-old() {\n    animation-name: -ua-view-transition-fade-out;\n    animation-duration: inherit;\n    animation-fill-mode: inherit;\n}\n::view-transition-old() {\n    position: absolute;\n    inset-block-start: 0px;\n    inline-size: 100%;\n    block-size: auto;\n}\n\nhtml::view-transition-new(root) {\n    mix-blend-mode: plus-lighter;\n}\nhtml::view-transition-new() {\n    animation-name: -ua-view-transition-fade-in;\n    animation-duration: inherit;\n    animation-fill-mode: inherit;\n}\n::view-transition-new() {\n    position: absolute;\n    inset-block-start: 0px;\n    inline-size: 100%;\n    block-size: auto;\n}\n</code></pre>\n<p>默认是一个 cross-fade 的过渡动画，旧视图 opacity 从 1 到 0，新视图 opacity 从 0 到 1<br>旧视图和新视图具有 <code>mix-blend-box: plus-lighter</code><br><code>::view-transition-image-pair</code> 具有 <code>isolation: isolate</code><br>这些样式都是为了能够正确实现 cross-fade 效果，但是我们的自定义过渡动画很可能需要覆写它们</p>\n<h1>示例</h1>\n<pre><code class=\"language-jsx\">import { useState } from &quot;react&quot;;\nimport { flushSync } from &quot;react-dom&quot;;\n\nconst items = [&lt;&gt;Apple&lt;/&gt;, &lt;&gt;Banana&lt;/&gt;, &lt;&gt;Cherry&lt;/&gt;];\nfunction App() {\n    const [idx, setIdx] = useState(0);\n\n    const onClick = () =&gt; {\n        document.startViewTransition(() =&gt; {\n            flushSync(() =&gt; {\n                setIdx((1 + idx) % items.length);\n            });\n        });\n    };\n\n    return (\n        &lt;&gt;\n            {items.map(\n                (item, i) =&gt;\n                    idx === i &amp;&amp; (\n                        &lt;button key={i} className=&quot;tab&quot; onClick={onClick}&gt;\n                            {item}\n                        &lt;/button&gt;\n                    ),\n            )}\n        &lt;/&gt;\n    );\n}\n</code></pre>\n<p>使用 animation 实现样式</p>\n<pre><code class=\"language-css\">.tab {\n    view-transition-name: tab;\n    height: 50px;\n    width: 120px;\n}\n\n::view-transition-group(tab) {\n    overflow: hidden;\n}\n\n::view-transition-image-pair(tab) {\n}\n\n::view-transition-old(tab) {\n    animation: vt-old-tab 0.3s cubic-bezier(0.4, 0, 0.2, 1) both;\n}\n\n::view-transition-new(tab) {\n    animation: vt-new-tab 0.3s cubic-bezier(0.4, 0, 0.2, 1) both;\n}\n\n@keyframes vt-old-tab {\n    from {\n        transform: translateX(0%);\n    }\n    to {\n        transform: translateX(-100%);\n    }\n}\n\n@keyframes vt-new-tab {\n    from {\n        transform: translateX(100%);\n    }\n    to {\n        transform: translateX(0%);\n    }\n}\n</code></pre>\n<p>使用 transition 实现样式</p>\n<pre><code class=\"language-css\">.tab {\n    view-transition-name: tab;\n    height: 50px;\n    width: 120px;\n}\n\n::view-transition-group(tab) {\n    overflow: hidden;\n}\n\n::view-transition-image-pair(tab) {\n}\n\n::view-transition-old(tab),\n::view-transition-new(tab) {\n    animation: none; /* 这里默认继承了 animation 动画，所以需要移除 */\n    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n::view-transition-old(tab) {\n    transform: translateX(-100%);\n\n    @starting-style {\n        transform: translateX(0%);\n    }\n}\n\n::view-transition-new(tab) {\n    transform: translateX(0%);\n\n    @starting-style {\n        transform: translateX(100%);\n    }\n}\n</code></pre>\n<h1>跨文档过渡</h1>\n<p>TODO</p>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API\">https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API</a></p>\n<h1>使用参考</h1>\n<p>使用时，只需考虑旧视图的哪个元素要过渡到新视图的哪个元素（可以是同一个元素，也可以不同）\n然后给它们加上相同的 <code>view-transition-name</code> 名称即可（必须保证前后视图只能存在唯一同名 <code>view-transition-name</code> 元素）</p>\n<p>一般来说， document.startViewTransition 传入的回调修改 DOM 即可，网络请求应在此之前发生\n要使用 Javascript 控制动画，参见：<a href=\"https://developer.chrome.com/docs/web-platform/view-transitions/#animating-with-javascript\">https://developer.chrome.com/docs/web-platform/view-transitions/#animating-with-javascript</a></p>\n<pre><code class=\"language-ts\">// TODO: 目前 typescript5.4dev 还没有提供定义，可以参考如下：\ninterface Document {\n    /**\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/startViewTransition)\n     * @param callback A callback function typically invoked to update the DOM during the view transition process, which returns a Promise. The callback is invoked once the API has taken a screenshot of the current page. When the promise returned by the callback fulfills, the view transition begins in the next frame. If the promise returned by the callback rejects, the transition is abandoned.\n     * @returns A ViewTransition object instance.\n     */\n    startViewTransition(callback: () =&gt; Promise&lt;void&gt; | void): ViewTransition;\n}\n\ninterface ViewTransition {\n    /**\n     * A Promise that fulfills once the transition animation is finished, and the new page view is visible and interactive to the user.\n     */\n    finished: Promise&lt;void&gt;;\n    /**\n     * A Promise that fulfills once the pseudo-element tree is created and the transition animation is about to start.\n     */\n    ready: Promise&lt;void&gt;;\n    /**\n     * A Promise that fulfills when the promise returned by the document.startViewTransition()&#39;s callback fulfills.\n     */\n    updateCallbackDone: Promise&lt;void&gt;;\n    /**\n     * Skips the animation part of the view transition, but doesn&#39;t skip running the document.startViewTransition() callback that updates the DOM.\n     */\n    skipTransition(): void;\n}\n</code></pre>\n<h1>See Also</h1>\n<p><a href=\"https://plainvanillaweb.com/blog/articles/2025-06-12-view-transitions/\">https://plainvanillaweb.com/blog/articles/2025-06-12-view-transitions/</a></p>\n<p><a href=\"https://css-tricks.com/almanac/pseudo-selectors/v/view-transition/\">https://css-tricks.com/almanac/pseudo-selectors/v/view-transition/</a></p>\n<p><a href=\"https://developer.chrome.com/blog/view-transitions-misconceptions\">https://developer.chrome.com/blog/view-transitions-misconceptions</a></p>\n<p><a href=\"https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/view_transition/scoped_view_transition.cc\">https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/view_transition/scoped_view_transition.cc</a></p>\n<p><a href=\"https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/view_transition/view_transition_supplement.cc\">https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/view_transition/view_transition_supplement.cc</a></p>\n<h1>附：Chromium 源码</h1>\n<pre><code class=\"language-cpp\">// ViewTransitionSupplement 是一个文档补充类，用于管理视图过渡功能\n// 它实现了 Supplement&lt;Document&gt; 模式，为 Document 对象添加视图过渡能力\nclass ViewTransitionSupplement : public Supplement&lt;Document&gt; {\npublic:\n  // 补充类的唯一标识符\n  static const char kSupplementName[] = &quot;ViewTransition&quot;;\n\n  // 从文档获取现有的 ViewTransitionSupplement 实例（如果存在）\n  static ViewTransitionSupplement* FromIfExists(const Document&amp; document) {\n    return Supplement&lt;Document&gt;::From&lt;ViewTransitionSupplement&gt;(document);\n  }\n\n  // 从文档获取 ViewTransitionSupplement 实例，如果不存在则创建新实例\n  static ViewTransitionSupplement* From(Document&amp; document) {\n    auto* supplement = Supplement&lt;Document&gt;::From&lt;ViewTransitionSupplement&gt;(document);\n    if (!supplement) {\n      // 使用垃圾回收机制创建新实例\n      supplement = MakeGarbageCollected&lt;ViewTransitionSupplement&gt;(document);\n      Supplement&lt;Document&gt;::ProvideTo(document, supplement);\n    }\n    return supplement;\n  }\n\n  // 为指定元素启动视图过渡的静态方法\n  static DOMViewTransition* StartViewTransitionForElement(\n      ScriptState* script_state,\n      Element* element,\n      V8ViewTransitionCallback* callback,\n      const std::optional&lt;Vector&lt;String&gt;&gt;&amp; types,\n      ExceptionState&amp; exception_state) {\n    DCHECK(script_state);\n    if (!element) {\n      return nullptr;\n    }\n\n    if (callback) {\n      // 为回调设置任务状态，用于任务归属追踪\n      // 扩展任务目前不被 TaskAttributionTracker 支持\n      callback-&gt;SetTaskState(CaptureCurrentTaskStateIfMainWorld(script_state));\n    }\n\n    auto* supplement = From(element-&gt;GetDocument());\n    return supplement-&gt;StartTransition(*element, callback, types, exception_state);\n  }\n\n  // 启动文档级别的视图过渡（使用回调函数）\n  DOMViewTransition* startViewTransition(\n      ScriptState* script_state,\n      Document&amp; document,\n      V8ViewTransitionCallback* callback,\n      ExceptionState&amp; exception_state) {\n    // 对文档根元素启动过渡\n    return StartViewTransitionForElement(script_state, document.documentElement(),\n                                         callback, std::nullopt, exception_state);\n  }\n\n  // 启动文档级别的视图过渡（使用选项对象）\n  DOMViewTransition* startViewTransition(\n      ScriptState* script_state,\n      Document&amp; document,\n      ViewTransitionOptions* options,\n      ExceptionState&amp; exception_state) {\n    // 验证选项对象的完整性\n    CHECK(!options || (options-&gt;hasUpdate() &amp;&amp; options-&gt;hasTypes()));\n    return StartViewTransitionForElement(\n        script_state, document.documentElement(),\n        options ? options-&gt;update() : nullptr,\n        options ? options-&gt;types() : std::nullopt, exception_state);\n  }\n\n  // 启动无参数的文档级别视图过渡\n  DOMViewTransition* startViewTransition(\n      ScriptState* script_state,\n      Document&amp; document,\n      ExceptionState&amp; exception_state) {\n    return StartViewTransitionForElement(\n        script_state, document.documentElement(),\n        static_cast&lt;V8ViewTransitionCallback*&gt;(nullptr), std::nullopt,\n        exception_state);\n  }\n\n  // 获取文档当前活跃的视图过渡\n  static DOMViewTransition* activeViewTransition(Document&amp; document) {\n    auto* supplement = FromIfExists(document);\n    if (!supplement) {\n      return nullptr;\n    }\n    return supplement-&gt;document_transition_\n               ? supplement-&gt;document_transition_-&gt;GetScriptDelegate()\n               : nullptr;\n  }\n\n  // 实际启动过渡的核心方法\n  DOMViewTransition* StartTransition(\n      Element&amp; element,\n      V8ViewTransitionCallback* callback,\n      const std::optional&lt;Vector&lt;String&gt;&gt;&amp; types,\n      ExceptionState&amp; exception_state) {\n    bool for_document = element.IsDocumentElement();\n    Document&amp; document = element.GetDocument();\n\n    // 在导航启动的过渡期间禁止脚本启动的过渡\n    if (document_transition_ &amp;&amp; !document_transition_-&gt;IsCreatedViaScriptAPI()) {\n      return ViewTransition::CreateSkipped(&amp;element, callback)-&gt;GetScriptDelegate();\n    }\n\n    ViewTransition* active_transition = GetTransition(element);\n    if (active_transition) {\n      // 启动新的视图过渡会跳过当前活跃的视图过渡\n      active_transition-&gt;SkipTransition();\n    } else {\n      auto it = skipped_with_pending_dom_callback_.find(&amp;element);\n      if (it != skipped_with_pending_dom_callback_.end()) {\n        // 最近跳过的视图过渡可能还没有触发其 DOM 回调\n        // 这个步骤需要在新视图过渡的捕获阶段之前完成\n        active_transition = it-&gt;value;\n      }\n    }\n\n    DCHECK(!GetTransition(element))\n        &lt;&lt; &quot;SkipTransition() should finish previously active view transition&quot;;\n\n    // 需要连接到视图才能进行过渡\n    if (!document.View()) {\n      return nullptr;\n    }\n\n    ViewTransition* transition = ViewTransition::CreateFromScript(\n        &amp;element, callback, types, this, active_transition);\n    DCHECK(transition);\n\n    // 根据元素类型存储过渡对象\n    if (for_document) {\n      document_transition_ = transition;\n    } else {\n      element_transitions_.insert(&amp;element, transition);\n    }\n\n    // 如果文档被隐藏，立即跳过过渡\n    if (document.hidden()) {\n      transition-&gt;SkipTransition(ViewTransition::PromiseResponse::kRejectInvalidState);\n      DCHECK(!document_transition_ || !for_document);\n      return transition-&gt;GetScriptDelegate();\n    }\n\n    return transition-&gt;GetScriptDelegate();\n  }\n\n  // 处理文档可见性状态变化\n  void DidChangeVisibilityState() {\n    if (GetSupplementable()-&gt;hidden() &amp;&amp; document_transition_) {\n      // 文档隐藏时跳过活跃的过渡\n      document_transition_-&gt;SkipTransition(ViewTransition::PromiseResponse::kRejectInvalidState);\n    }\n    SendOptInStatusToHost();\n  }\n\n  // 向宿主发送选择加入状态\n  void SendOptInStatusToHost() {\n    Document* document = GetSupplementable();\n    if (!document || !document-&gt;GetFrame() || !document-&gt;domWindow()) {\n      return;\n    }\n\n    // 通知框架宿主选择加入状态已更改\n    document-&gt;GetFrame()-&gt;GetLocalFrameHostRemote().OnViewTransitionOptInChanged(\n        (document-&gt;domWindow()-&gt;HasBeenRevealed() &amp;&amp; !document-&gt;hidden())\n            ? cross_document_opt_in_\n            : mojom::blink::ViewTransitionSameOriginOptIn::kDisabled);\n  }\n\n  // 设置跨文档选择加入状态\n  void SetCrossDocumentOptIn(\n      mojom::blink::ViewTransitionSameOriginOptIn cross_document_opt_in) {\n    if (cross_document_opt_in_ == cross_document_opt_in) {\n      return;\n    }\n\n    cross_document_opt_in_ = cross_document_opt_in;\n    SendOptInStatusToHost();\n  }\n\n  // 为导航创建文档快照的静态方法\n  static void SnapshotDocumentForNavigation(\n      Document&amp; document,\n      const blink::ViewTransitionToken&amp; navigation_id,\n      mojom::blink::PageSwapEventParamsPtr params,\n      ViewTransition::ViewTransitionStateCallback callback) {\n    auto* supplement = From(document);\n    supplement-&gt;StartTransition(document, navigation_id, std::move(params),\n                                std::move(callback));\n  }\n\n  // 启动导航相关的过渡\n  void StartTransition(\n      Document&amp; document,\n      const blink::ViewTransitionToken&amp; navigation_id,\n      mojom::blink::PageSwapEventParamsPtr params,\n      ViewTransition::ViewTransitionStateCallback callback) {\n    // TODO: 根据规范，此时应该检查选择加入状态\n\n    if (document_transition_) {\n      // 如果存在过渡，应该跳过它，因为导航过渡优先级更高\n      document_transition_-&gt;SkipTransition();\n    }\n\n    DCHECK(!document_transition_)\n        &lt;&lt; &quot;SkipTransition() should finish existing |document_transition_|&quot;;\n\n    // 为导航快照创建过渡对象\n    document_transition_ = ViewTransition::CreateForSnapshotForNavigation(\n        &amp;document, navigation_id, std::move(callback), cross_document_types_, this);\n\n    // 创建并分发页面交换事件\n    auto* page_swap_event = MakeGarbageCollected&lt;PageSwapEvent&gt;(\n        document, std::move(params), document_transition_-&gt;GetScriptDelegate());\n    document.domWindow()-&gt;DispatchEvent(*page_swap_event);\n  }\n\n  // 从导航快照创建过渡的静态方法\n  static void CreateFromSnapshotForNavigation(\n      Document&amp; document,\n      ViewTransitionState transition_state) {\n    auto* supplement = From(document);\n    supplement-&gt;StartTransition(document, std::move(transition_state));\n  }\n\n  // 中止过渡的静态方法\n  static void AbortTransition(Document&amp; document) {\n    auto* supplement = FromIfExists(document);\n    if (supplement &amp;&amp; supplement-&gt;document_transition_) {\n      supplement-&gt;document_transition_-&gt;SkipTransition();\n      DCHECK(!supplement-&gt;document_transition_);\n    }\n  }\n\n  // 从快照启动过渡\n  void StartTransition(Document&amp; document, ViewTransitionState transition_state) {\n    DCHECK(!document_transition_) &lt;&lt; &quot;Existing transition on new Document&quot;;\n    document_transition_ = ViewTransition::CreateFromSnapshotForNavigation(\n        &amp;document, std::move(transition_state), this);\n  }\n\n  // 过渡完成时的回调处理\n  void OnTransitionFinished(ViewTransition* transition) {\n    CHECK(transition);\n\n    // 清除过渡以便垃圾回收（并防止 GetTransition 调用者认为有正在进行的过渡）\n    if (transition == document_transition_) {\n      document_transition_ = nullptr;\n    } else {\n      Element* scope = transition-&gt;Scope();\n      element_transitions_.erase(scope);\n      LayoutObject* layout_object = scope-&gt;GetLayoutObject();\n\n      // 元素可能仅因为是视图过渡的作用域而添加了堆叠上下文\n      // 确保调整后样式的正确性\n      if (scope != scope-&gt;GetDocument().documentElement() &amp;&amp; layout_object &amp;&amp;\n          !(layout_object-&gt;StyleRef().Contain() &amp; kContainsViewTransition) &amp;&amp;\n          layout_object-&gt;HasLayer()) {\n        layout_object-&gt;EnclosingLayer()-&gt;SetNeedsCompositingInputsUpdate();\n        scope-&gt;SetNeedsStyleRecalc(kLocalStyleChange,\n                                   StyleChangeReasonForTracing::Create(\n                                       style_change_reason::kViewTransition));\n      }\n    }\n\n    // 如果活跃视图过渡集合为空，通知动画器\n    if (!document_transition_ &amp;&amp; element_transitions_.empty()) {\n      Document* document = To&lt;Document&gt;(GetSupplementable());\n      if (auto* page = document-&gt;GetPage()) {\n        page-&gt;Animator().SetHasViewTransition(false);\n      }\n    }\n  }\n\n  // 处理带有待处理回调的跳过过渡\n  void OnSkipTransitionWithPendingCallback(ViewTransition* transition) {\n    CHECK(transition);\n    skipped_with_pending_dom_callback_.insert(transition-&gt;Scope(), transition);\n  }\n\n  // 处理跳过过渡的 DOM 回调\n  void OnSkippedTransitionDOMCallback(ViewTransition* transition) {\n    CHECK(transition);\n    skipped_with_pending_dom_callback_.erase(transition-&gt;Scope());\n  }\n\n  // 获取文档级别的过渡\n  ViewTransition* GetTransition() {\n    return document_transition_.Get();\n  }\n\n  // 获取指定元素的过渡\n  ViewTransition* GetTransition(const Element&amp; element) {\n    if (element.IsDocumentElement()) {\n      return document_transition_.Get();\n    }\n    if (element.IsPseudoElement()) {\n      // 对于伪元素，获取其最终原始元素的过渡\n      return GetTransition(To&lt;PseudoElement&gt;(element).UltimateOriginatingElement());\n    }\n    auto transition = element_transitions_.find(&amp;element);\n    return transition == element_transitions_.end() ? nullptr : transition-&gt;value;\n  }\n\n  // 对每个过渡执行指定函数\n  void ForEachTransition(base::FunctionRef&lt;void(ViewTransition&amp;)&gt; function) {\n    if (!RuntimeEnabledFeatures::ScopedViewTransitionsEnabled()) {\n      // 如果未启用作用域视图过渡，只处理文档过渡\n      if (ViewTransition* document_transition = GetTransition()) {\n        function(*document_transition);\n      }\n      DCHECK(element_transitions_.empty());\n      return;\n    }\n\n    // 创建过渡列表的本地副本，因为函数可能修改过渡映射\n    HeapVector&lt;Member&lt;ViewTransition&gt;&gt; transitions;\n    if (ViewTransition* document_transition = GetTransition()) {\n      transitions.push_back(document_transition);\n    }\n    for (auto&amp; element_transition : element_transitions_.Values()) {\n      transitions.push_back(element_transition);\n    }\n    for (auto transition : transitions) {\n      function(*transition);\n    }\n  }\n\n  // 进入计算样式作用域时的处理\n  void WillEnterGetComputedStyleScope() {\n    CHECK(!in_get_computed_style_scope_);\n    in_get_computed_style_scope_ = true;\n\n    ForEachTransition([](ViewTransition&amp; transition) {\n      transition.WillEnterGetComputedStyleScope();\n    });\n  }\n\n   // 退出计算样式作用域时的处理\n  void WillExitGetComputedStyleScope() {\n    CHECK(in_get_computed_style_scope_);\n    in_get_computed_style_scope_ = false;\n\n    ForEachTransition([](ViewTransition&amp; transition) {\n      transition.WillExitGetComputedStyleScope();\n    });\n  }\n\n  // 样式和布局树更新前的处理\n  void WillUpdateStyleAndLayoutTree() {\n    if (in_get_computed_style_scope_ == last_update_had_computed_style_scope_) {\n      return;\n    }\n    last_update_had_computed_style_scope_ = in_get_computed_style_scope_;\n    ForEachTransition([](ViewTransition&amp; transition) {\n      // 使内部伪样式失效，强制重新计算\n      transition.InvalidateInternalPseudoStyle();\n    });\n  }\n\n  // 构造函数，初始化补充对象\n  ViewTransitionSupplement::ViewTransitionSupplement(Document&amp; document)\n      : Supplement&lt;Document&gt;(document) {}\n\n  // 析构函数\n  ViewTransitionSupplement::~ViewTransitionSupplement() = default;\n\n  // 垃圾回收追踪方法，标记需要保持活跃的对象\n  void ViewTransitionSupplement::Trace(Visitor* visitor) const {\n    visitor-&gt;Trace(document_transition_);\n    visitor-&gt;Trace(element_transitions_);\n    visitor-&gt;Trace(skipped_with_pending_dom_callback_);\n\n    Supplement&lt;Document&gt;::Trace(visitor);\n  }\n\n  // 添加待处理的视图过渡请求\n  void ViewTransitionSupplement::AddPendingRequest(\n      std::unique_ptr&lt;ViewTransitionRequest&gt; request) {\n    pending_requests_.push_back(std::move(request));\n\n    auto* document = GetSupplementable();\n    if (!document || !document-&gt;GetPage() || !document-&gt;View())\n      return;\n\n    // 安排新的帧渲染\n    document-&gt;View()-&gt;ScheduleAnimation();\n\n    // 确保绘制工件合成器进行更新，这是我们向合成器传递过渡请求的机制\n    document-&gt;View()-&gt;SetPaintArtifactCompositorNeedsUpdate();\n  }\n\n  // 获取并清空待处理的请求列表\n  VectorOf&lt;std::unique_ptr&lt;ViewTransitionRequest&gt;&gt;\n  ViewTransitionSupplement::TakePendingRequests() {\n    return std::move(pending_requests_);\n  }\n\n  // 当视图过渡样式更新时的回调\n  void ViewTransitionSupplement::OnViewTransitionsStyleUpdated(\n      bool cross_document_enabled,\n      const Vector&lt;String&gt;&amp; types) {\n    SetCrossDocumentOptIn(\n        cross_document_enabled\n            ? mojom::blink::ViewTransitionSameOriginOptIn::kEnabled\n            : mojom::blink::ViewTransitionSameOriginOptIn::kDisabled);\n    cross_document_types_ = types;\n  }\n\n  // 即将插入 body 元素时的处理\n  void ViewTransitionSupplement::WillInsertBody() {\n    if (!document_transition_ ||\n        !document_transition_-&gt;IsForNavigationOnNewDocument()) {\n      return;\n    }\n\n    auto* document = GetSupplementable();\n    CHECK(document);\n\n    // 更新活跃样式将计算 @view-transition 导航选择加入\n    // TODO: 这可能是一个过于重的方法\n    // 长期来看，我们可能不想在 WillInsertBody 时做这个决定\n    // 或者如果要做，我们可以专门查找 @view-transition 而不是所有规则\n    document-&gt;GetStyleEngine().UpdateActiveStyle();\n  }\n\n  // 解析跨文档视图过渡\n  DOMViewTransition*\n  ViewTransitionSupplement::ResolveCrossDocumentViewTransition() {\n    if (!document_transition_ ||\n        !document_transition_-&gt;IsForNavigationOnNewDocument()) {\n      return nullptr;\n    }\n\n    // 当文档尚未被显示时，我们自动跳过*出站*过渡\n    // 在解析入站过渡时，我们期望它尚未被显示\n    CHECK(!GetSupplementable()-&gt;domWindow()-&gt;HasBeenRevealed());\n\n    if (cross_document_opt_in_ ==\n        mojom::blink::ViewTransitionSameOriginOptIn::kDisabled) {\n      document_transition_-&gt;SkipTransition();\n      CHECK(!ViewTransitionUtils::GetTransition(*GetSupplementable()));\n      return nullptr;\n    }\n\n    // 初始化过渡类型\n    document_transition_-&gt;InitTypes(cross_document_types_);\n\n    // TODO: 这里应该应用来自使用的 @view-transition 的类型\n\n    return document_transition_-&gt;GetScriptDelegate();\n  }\n\n  // 生成资源 ID\n  viz::ViewTransitionElementResourceId\n  ViewTransitionSupplement::GenerateResourceId(\n      const blink::ViewTransitionToken&amp; transition_token,\n      bool for_subframe_snapshot) {\n    return viz::ViewTransitionElementResourceId(\n        transition_token, ++resource_local_id_sequence_, for_subframe_snapshot);\n  }\n\n  // 初始化资源 ID 序列\n  void ViewTransitionSupplement::InitializeResourceIdSequence(\n      uint32_t next_local_id) {\n    CHECK_GT(next_local_id,\n             viz::ViewTransitionElementResourceId::kInvalidLocalId);\n    resource_local_id_sequence_ =\n        std::max(next_local_id - 1, resource_local_id_sequence_);\n  }\n\n}  // namespace blink\n</code></pre>\n","tags":["web-api"]},{"id":"css-color-adjustment","url":"https://yieldray.fun/posts/css-color-adjustment","title":"CSS color adjustment","date_published":"2023-09-01T10:00:00.000Z","date_modified":"2023-09-01T10:00:00.000Z","content_text":"<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_color_adjustment%3E\">CSS color adjustment</a> 模块用于自动控制和调整颜色以处理用户偏好，比如：暗色模式，高对比度，及其他色彩模式。</p>\n<blockquote>\n<p>TODO: 本篇待完善</p>\n</blockquote>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/color-scheme\">color-scheme</a> (property)</h1>\n<p>指定当前元素使用何种配色方案（与操作系统是否指定了暗色模式无关）</p>\n<pre><code class=\"language-css\">color-scheme =\n  normal                                       |\n  [ light | dark | &lt;custom-ident&gt; ]+ &amp;&amp; only?\n</code></pre>\n<p>元素的默认颜色属性值实际上配置为 <a href=\"https://developer.mozilla.org/docs/Web/CSS/system-color\"><system-color></a> 系统颜色<br>该变量会根据 color-scheme 值为 light 或 dark 而表示不同的具体值。</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/@media/prefers-color-scheme\">prefers-color-scheme</a> (media-query)</h1>\n<p>查询用户代理偏好的主题色。（即：操作系统是否开启了暗色模式）</p>\n<pre><code class=\"language-css\">@media (prefers-color-scheme: dark) {\n}\n\n@media (prefers-color-scheme: light) {\n}\n\n@media (prefers-color-scheme: no-preference) {\n}\n</code></pre>\n<p>注：js 使用 <a href=\"https://developer.mozilla.org/docs/Web/API/Window/matchMedia\">matchMedia</a> 查询</p>\n<pre><code class=\"language-ts\">let isDark: boolean = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches;\n\nwindow.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).addEventListener(&quot;change&quot;, (ev: MediaQueryListEvent) =&gt; {\n    const matches: boolean = ev.matches;\n    const media: &quot;(prefers-color-scheme: dark)&quot; = ev.media;\n    console.log({ matches, media });\n});\n</code></pre>\n<h1>使用</h1>\n<p>暗色模式下使用系统色兜底。（仅适用于元素未配置颜色的属性，因为系统颜色优先级低）</p>\n<pre><code class=\"language-css\">@media (prefers-color-scheme: dark) {\n    :root {\n        color-scheme: dark;\n    }\n}\n</code></pre>\n","tags":["css"]},{"id":"web-img","url":"https://yieldray.fun/posts/web-img","title":"img元素","date_published":"2023-08-25T20:00:00.000Z","date_modified":"2023-08-25T20:00:00.000Z","content_text":"<h1>前置知识</h1>\n<p>Origin 标头在同源的 HEAD 和 GET 请求中不会自动发送，其它情况下（即跨源和同源非 HEAD GET 请求）则自动携带<br>Referer 标头的发送策略可通过当前文档的 <a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Referrer-Policy\">Referrer-Policy</a> 标头或 <code>&lt;meta&gt;</code> 标签进行全局控制<br>默认情况下，非 http 文档的请求和 http 向 https 的请求不会发送 Referer 标头，并且为发起请求的文档的完整 URI （不含 hash）</p>\n<p>一个最简单的 img 标签如下，仅考虑不设置其它策略的默认情况<br>将对 src 发送 GET，携带 Referer 标头，不携带 Origin 标头</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;http://example.net&quot; /&gt;\n</code></pre>\n<p>width、height 以及 alt 是众所周知的属性，略。</p>\n<h1>crossorigin</h1>\n<p>若指定 crossorigin 属性，则图片请求为 CORS 请求（验证相应的响应标头）<br>携带 Referer 标头和 Origin 标头<br>属性可选值为</p>\n<ul>\n<li>anonymous 默认值，不携带凭据</li>\n<li>use-credentials 携带凭据（例如 Cookie，Authorization 标头）</li>\n</ul>\n<h1>referrerpolicy</h1>\n<p>referrerpolicy 属性可内联地指定 Referer 标头的发送策略</p>\n<table>\n<thead>\n<tr>\n<th>值</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>no-referrer</td>\n<td>不发送 Referer 标头</td>\n</tr>\n<tr>\n<td>no-referrer-when-downgrade</td>\n<td>HTTPS 文档请求 HTTP 图片时，不发送 Referer 标头</td>\n</tr>\n<tr>\n<td>origin</td>\n<td>请求的 Referer 标头被限制为：协议、主机和端口</td>\n</tr>\n<tr>\n<td>origin-when-cross-origin</td>\n<td>请求的 Referer 标头被限制为：协议、主机和端口。同源时仍将包含路径</td>\n</tr>\n<tr>\n<td>same-origin</td>\n<td>仅同源请求发送 Referer 标头，跨源请求则不包含 Referer 标头</td>\n</tr>\n<tr>\n<td>strict-origin</td>\n<td>仅在协议安全级别保持不变（HTTPS→HTTPS）的情况下发送 Referer 标头。而在目标的安全性降低（HTTPS→HTTP）时则不发送</td>\n</tr>\n<tr>\n<td>strict-origin-when-cross-origin（default value）</td>\n<td>执行同源请求时发送完整的 URL，且仅在协议安全级别保持不变（HTTPS→HTTPS）时发送 Referer，在目标安全性降低（HTTPS→HTTP）时则不发送。</td>\n</tr>\n<tr>\n<td>unsafe-url</td>\n<td>请求的 Referer 标头为：协议、主机、端口以及路径。（不安全）</td>\n</tr>\n</tbody></table>\n<h1>decoding</h1>\n<p>decoding 属性可指定为</p>\n<ul>\n<li>auto 默认值</li>\n<li>sync 同步解码图像</li>\n<li>async 异步解码图像</li>\n</ul>\n<h1>fetchpriority</h1>\n<p>fetchpriority 属性可作为图像请求优先级的参考值，可指定为</p>\n<ul>\n<li>auto 默认值</li>\n<li>high 高优先级</li>\n<li>low 低优先级</li>\n</ul>\n<h1>loading</h1>\n<p>loading 属性可开启图像延迟加载，仅在浏览器开启 Javascript 时有效</p>\n<ul>\n<li><p>eager 默认值。立即加载，即使图像在 viewport 以外</p>\n</li>\n<li><p>lazy 延迟加载图像（懒加载）</p>\n</li>\n</ul>\n<p>可以参考：<a href=\"https://www.zhangxinxu.com/wordpress/2019/09/native-img-loading-lazy/\">https://www.zhangxinxu.com/wordpress/2019/09/native-img-loading-lazy/</a><br>谷歌：<a href=\"https://web.dev/browser-level-image-lazy-loading/\">https://web.dev/browser-level-image-lazy-loading/</a></p>\n<h1>ismap</h1>\n<p>ismap 属性要求 img 元素嵌套在 a 标签下，点击图片将转至 a 标签指定的 href 加上 <code>?{x},{y}</code><br>xy 为图片被点击的坐标，例如 <a href=\"https://example.net/?200,150\">https://example.net/?200,150</a><br>注意这是将 href 直接拼接 ? 和坐标，即使 img.src 的 URL 已有 query 部分（所以这里最好避免使用 query）</p>\n<pre><code class=\"language-html\">&lt;a href=&quot;https://example.net/&quot;&gt;\n    &lt;img src=&quot;https://placekitten.com/400/300&quot; ismap /&gt;\n&lt;/a&gt;\n</code></pre>\n<h1>usemap</h1>\n<p>usemap 属性可指定关联的 map 标签</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/HTML/Element/map\">https://developer.mozilla.org/docs/Web/HTML/Element/map</a></p>\n<pre><code class=\"language-html\">&lt;img\n    src=&quot;https://www.runoob.com/try/demo_source/planets.gif&quot;\n    width=&quot;145&quot;\n    height=&quot;126&quot;\n    alt=&quot;Planets&quot;\n    usemap=&quot;#planetmap&quot;\n/&gt;\n\n&lt;map name=&quot;planetmap&quot;&gt;\n    &lt;area shape=&quot;rect&quot; coords=&quot;0,0,82,126&quot; alt=&quot;Sun&quot; href=&quot;https://www.runoob.com/images/sun.gif&quot; /&gt;\n    &lt;area shape=&quot;circle&quot; coords=&quot;90,58,3&quot; alt=&quot;Mercury&quot; href=&quot;https://www.runoob.com/try/demo_source/merglobe.gif&quot; /&gt;\n    &lt;area shape=&quot;circle&quot; coords=&quot;124,58,8&quot; alt=&quot;Venus&quot; href=&quot;https://www.runoob.com/images/venglobe.gif&quot; /&gt;\n&lt;/map&gt;\n</code></pre>\n<h1>srcset</h1>\n<p>略，参见：<a href=\"https://developer.mozilla.org/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images\">https://developer.mozilla.org/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images</a></p>\n<pre><code class=\"language-html\">&lt;img\n    srcset=&quot;elva-fairy-320w.jpg, elva-fairy-480w.jpg 1.5x, elva-fairy-640w.jpg 2x&quot;\n    src=&quot;elva-fairy-640w.jpg&quot;\n    alt=&quot;Elva dressed as a fairy&quot;\n/&gt;\n</code></pre>\n","tags":["web-api","dom"]},{"id":"algorithm-backtrace","url":"https://yieldray.fun/posts/algorithm-backtrace","title":"回溯","date_published":"2023-08-10T22:22:22.000Z","date_modified":"2023-08-10T22:22:22.000Z","content_text":"<h1>回溯</h1>\n<p>回溯算法相当于 DFS</p>\n<pre><code class=\"language-cpp\">// 二叉树\nvoid traverse(TreeNode *root)\n{\n    if (!root) return;\n\n    // 前序遍历代码写在这里\n    traverse(root-&gt;right);\n    // 中序遍历代码写在这里\n    traverse(root-&gt;left);\n    // 后序遍历代码写在这里\n}\n\n// N叉树\nvoid traverse(TreeNode *root)\n{\n    if (!root) return;\n\n    for (auto child : root-&gt;children)\n    {\n        // 前序遍历代码写在这里\n        traverse(child);\n        // 后序遍历代码写在这里\n    }\n}\n</code></pre>\n<p>回溯伪代码如下</p>\n<pre><code class=\"language-py\">def backtrace(choiceList, track, answer):\n    if ok(track):\n        answer.append(track)\n        return\n\n    for choice in choiceList:\n        # choose:   选择一个 choice 加入 track\n        backtrace(choiceList, track, answer)  # 做下一个选择\n        # unchoose: 从 track 中撤销上面的选择\n</code></pre>\n<p>可以发现，回溯相当于 N 叉树的遍历</p>\n<h1>举例：最简单的回溯问题</h1>\n<p>描述：<br>给定一个没有重复数字的整数数组 <code>nums</code> 和一个整数 <code>n</code>，要求返回所有可能的 <code>n</code> 元组，其中每个元素都可以在 nums 中任意选择。</p>\n<p>示例：<br>假设给定数组 <code>nums = [1, 2, 3]</code>，整数 <code>n = 2</code>，则所有可能的 <code>2</code> 元组为：</p>\n<pre><code class=\"language-py\">[1, 1]\n[1, 2]\n[1, 3]\n[2, 1]\n[2, 2]\n[2, 3]\n[3, 1]\n[3, 2]\n[3, 3]\n</code></pre>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;vector&gt;\n#include &lt;algorithm&gt;\nusing namespace std;\n\nclass Solution\n{\nprivate:\n    vector&lt;vector&lt;int&gt;&gt; answer;\n\n    vector&lt;int&gt; track;\n\n    void backtrace(vector&lt;int&gt; &amp;nums, int n)\n    {\n        if (track.size() == n) // 终止条件\n        {\n            answer.push_back(track); // 提交结果\n            return;\n        }\n\n        for (int i = 0; i &lt; nums.size(); i++) // 遍历当前层所有可能项\n        {\n            track.push_back(nums[i]); // 生成当前项\n            backtrace(nums, n);       // 生成下一项\n            track.pop_back();         // 回溯，撤销当前项\n        }\n    }\n\npublic:\n    vector&lt;vector&lt;int&gt;&gt; permute(vector&lt;int&gt; &amp;nums, int n)\n    {\n        backtrace(nums, n);\n        return answer;\n    }\n};\n\n/**\n * 测试，可忽略\n */\nint main()\n{\n    Solution test;\n    vector&lt;int&gt; nums{1, 2, 3};\n    vector&lt;vector&lt;int&gt;&gt; result = test.permute(nums, 2);\n\n    for (auto vec : result)\n    {\n        cout &lt;&lt; &quot;[&quot;;\n        for (auto n : vec)\n        {\n            cout &lt;&lt; n;\n            cout &lt;&lt; &quot;, &quot;;\n        }\n        cout &lt;&lt; &quot;]&quot; &lt;&lt; endl;\n    }\n}\n</code></pre>\n<p>注意观察输出结果的顺序。</p>\n<p>上面的问题中，时间复杂度是 O(n^k) ，也就是要做 k 次循环<br>由于 k 是变量，我们无法通过编写 k 个 for 循环实现<br>而是要通过栈实现</p>\n<h1>N 皇后问题</h1>\n<blockquote>\n<p>leetcode 描述：<a href=\"https://leetcode.cn/problems/n-queens/\">https://leetcode.cn/problems/n-queens/</a></p>\n</blockquote>\n<p>朴素观点来看，N 皇后问题其实是时间复杂度为 O(n^n) 的回溯问题<br>合理剪枝可降低时间复杂度</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;vector&gt;\n\nusing namespace std;\n\nclass Solution\n{\nprivate:\n    vector&lt;vector&lt;string&gt;&gt; answer;\n    void backtrace(int n, int row, vector&lt;string&gt; &amp;board)\n    {\n        if (row &gt;= n)\n        {\n            answer.push_back(board);\n            return;\n        }\n\n        // evaluate board[row][col]\n        for (int col = 0; col &lt; n; col++)\n        {\n            // 注意：我们只放Q，不放.\n            if (isValid(n, row, col, board))\n            {\n                board[row][col] = &#39;Q&#39;;        // 放置\n                backtrace(n, row + 1, board); // 下一行 （同一行只能一个Q）\n                board[row][col] = &#39;.&#39;;        // 撤销\n            }\n            else\n            {\n                continue;\n                // 相当于：\n                // board[row][col] = &#39;.&#39;;\n            }\n        }\n    }\n\n    bool isValid(int n, int row, int col, vector&lt;string&gt; &amp;board)\n    {\n        // _|_\n        for (int i = 0; i &lt; row; i++)\n        {\n            if (board[i][col] == &#39;Q&#39;)\n                return false;\n        }\n        // _\\_\n        for (int i = row - 1, j = col - 1; i &gt;= 0 &amp;&amp; j &gt;= 0; i--, j--)\n        {\n            if (board[i][j] == &#39;Q&#39;)\n                return false;\n        }\n        // _/_\n        for (int i = row - 1, j = col + 1; i &gt;= 0 &amp;&amp; j &lt; n; i--, j++)\n        {\n            if (board[i][j] == &#39;Q&#39;)\n            {\n                return false;\n            }\n        }\n        return true;\n    }\n\npublic:\n    vector&lt;vector&lt;string&gt;&gt; solveNQueens(int n)\n    {\n        vector&lt;string&gt; board(n, string(n, &#39;.&#39;));\n        backtrace(n, 0, board);\n        return answer;\n    }\n};\n\n/**\n * 测试，可忽略\n */\nint main()\n{\n    int n = 4;\n    Solution solution;\n    vector&lt;vector&lt;string&gt;&gt; result = solution.solveNQueens(n);\n\n    for (auto &amp;res : result)\n    {\n        for (auto &amp;row : res)\n        {\n            cout &lt;&lt; row &lt;&lt; endl;\n        }\n        cout &lt;&lt; endl;\n    }\n}\n</code></pre>\n<h1>解数独</h1>\n<blockquote>\n<p>leetcode 描述：<a href=\"https://leetcode.cn/problems/sudoku-solver/submissions/\">https://leetcode.cn/problems/sudoku-solver/submissions/</a></p>\n</blockquote>\n<p>数独问题和 N 皇后问题如出一辙</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;vector&gt;\nusing namespace std;\n\nclass Solution\n{\nprivate:\n    bool backtrace(vector&lt;vector&lt;char&gt;&gt; &amp;board)\n    {\n        for (int row = 0; row &lt; 9; row++)\n            for (int col = 0; col &lt; 9; col++)\n            {\n                // 不是空白处不用放\n                if (board[row][col] != &#39;.&#39;)\n                    continue;\n\n                // 两层 for 遍历行和列，再一层 for 遍历所有可能的选择\n                for (char n = &#39;1&#39;; n &lt;= &#39;9&#39;; n++)\n                    if (isValid(board, row, col, n))\n                    {\n                        board[row][col] = n;\n\n                        if (backtrace(board)) // 由于已经放置了当前位置，下一次递归就会继续去下一个位置\n                            return true;      // 如果之后的位置也是都可以放置，那么就直接返回\n                        else                  // 如果后面的位置有不可以放的，就回溯\n                            board[row][col] = &#39;.&#39;;\n                    }\n\n                return false; // 当前位置都不可以放，直接返回（回溯）\n            }\n        // 已解完\n        return true;\n    }\n\n    bool isValid(vector&lt;vector&lt;char&gt;&gt; &amp;board, int row, int col, char n)\n    {\n        // 注意这里直接检查整行和整列，而不是只检查之前行和列\n        // 否则也可以通过但是会超时（想想为什么？）\n\n        // -\n        for (int j = 0; j &lt; 9; j++)\n            if (board[row][j] == n)\n                return false;\n\n        // |\n        for (int i = 0; i &lt; 9; i++)\n            if (board[i][col] == n)\n                return false;\n\n        // 3*3\n        int x = row - (row % 3);\n        int y = col - (col % 3);\n        for (int i = x; i &lt; x + 3; i++)\n            for (int j = y; j &lt; y + 3; j++)\n                if (board[i][j] == n)\n                    return false;\n\n        // ok\n        return true;\n    }\n\npublic:\n    void\n    solveSudoku(vector&lt;vector&lt;char&gt;&gt; &amp;board)\n    {\n        backtrace(board);\n    }\n};\n\n/**\n * 测试，可忽略\n */\nint main()\n{\n    vector&lt;vector&lt;char&gt;&gt; board = {\n        {&#39;5&#39;, &#39;3&#39;, &#39;.&#39;, &#39;.&#39;, &#39;7&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;},\n        {&#39;6&#39;, &#39;.&#39;, &#39;.&#39;, &#39;1&#39;, &#39;9&#39;, &#39;5&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;},\n        {&#39;.&#39;, &#39;9&#39;, &#39;8&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;6&#39;, &#39;.&#39;},\n        {&#39;8&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;6&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;3&#39;},\n        {&#39;4&#39;, &#39;.&#39;, &#39;.&#39;, &#39;8&#39;, &#39;.&#39;, &#39;3&#39;, &#39;.&#39;, &#39;.&#39;, &#39;1&#39;},\n        {&#39;7&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;2&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;6&#39;},\n        {&#39;.&#39;, &#39;6&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;2&#39;, &#39;8&#39;, &#39;.&#39;},\n        {&#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;4&#39;, &#39;1&#39;, &#39;9&#39;, &#39;.&#39;, &#39;.&#39;, &#39;5&#39;},\n        {&#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;.&#39;, &#39;8&#39;, &#39;.&#39;, &#39;.&#39;, &#39;7&#39;, &#39;9&#39;}};\n\n    cout &lt;&lt; &quot;Original:&quot; &lt;&lt; endl;\n    for (const auto &amp;row : board)\n        for (char ch : row)\n            cout &lt;&lt; ch &lt;&lt; &quot; &quot;;\n    cout &lt;&lt; endl;\n\n    Solution solution;\n    solution.solveSudoku(board);\n\n    cout &lt;&lt; &quot;Solved:&quot; &lt;&lt; endl;\n    for (const auto &amp;row : board)\n        for (char ch : row)\n            cout &lt;&lt; ch &lt;&lt; &quot; &quot;;\n    cout &lt;&lt; endl;\n}\n</code></pre>\n","tags":["cpp","algorithm"]},{"id":"clang-io","url":"https://yieldray.fun/posts/clang-io","title":"C语言 I/O","date_published":"2023-07-30T14:41:14.000Z","date_modified":"2023-07-30T14:41:14.000Z","content_text":"<h1>C 标准库文件操作</h1>\n<p>这里只写一下窄字符文件读写，其余参见：<a href=\"https://zh.cppreference.com/w/c/io\">https://zh.cppreference.com/w/c/io</a></p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\n#include &lt;stdlib.h&gt;\n#include &lt;string.h&gt;\n#include &lt;errno.h&gt;\n\nvoid my_print_ftell(FILE *stream);\n\nint main()\n{\n    FILE *fp;\n    // &quot;r&quot;：只读，文件必须已存在\n    // &quot;w&quot;：只写，如果文件不存在则创建，如果文件已存在则把文件长度截断（Truncate）为 0 字节再重新写，也就是替换掉原来的文件内容\n    // &quot;a&quot;：只能在文件末尾追加数据，如果文件不存在则创建\n    // &quot;r+&quot;：允许读和写，文件必须已存在\n    // &quot;w+&quot;：允许读和写，如果文件不存在则创建，如果文件已存在则把文件长度截断为 0 字节再重新写\n    // &quot;a+&quot;：允许读和追加数据，如果文件不存在则创建\n    if ((fp = fopen(&quot;./not_exist.txt&quot;, &quot;r&quot;)) == NULL)\n    {\n        perror(&quot;Open &#39;./not_exist.txt&#39;&quot;);\n        fprintf(stderr, &quot;%s\\n&quot;, strerror(errno)); // No such file or directory\n    }\n\n    fp = fopen(&quot;./todo.txt&quot;, &quot;w+&quot;);\n    fputs(&quot;this is line one\\n&quot;, fp);\n\n    // SEEK_SET：从文件开头移动 offset 个字节\n    // SEEK_CUR：从当前位置移动 offset 个字节\n    // SEEK_END：从文件末尾移动 offset 个字节\n    if (fseek(fp, 2, SEEK_SET) != 0)\n        perror(&quot;fseek()&quot;);\n\n    fputc(&#39;a&#39;, fp); // 2\n    fputc(&#39;t&#39;, fp); // 3\n\n    my_print_ftell(fp);\n    rewind(fp);\n\n    int size = 100;\n    char *str = malloc(100 * sizeof(char));\n    fgets(str, size, fp); // 扫描直至首个 &#39;\\n&#39; 字符，str 会包含换行符\n    fprintf(stdout, &quot;%s&quot;, str);\n\n    my_print_ftell(fp);\n    rewind(fp);\n\n    char *buf = malloc(100 * sizeof(char));\n    fread(buf, sizeof(char), size, fp); // fread/fwrite 读写字符串应当是跨平台的，但读写其它二进制结构时则需要考虑是否跨平台了\n    fprintf(stdout, &quot;%s&quot;, buf);\n\n    my_print_ftell(fp);\n    fclose(fp);\n}\n\nvoid my_print_ftell(FILE *stream)\n{\n    long ftold;\n    if ((ftold = ftell(stream)) == -1)\n    {\n        perror(&quot;ftell()&quot;);\n    }\n    else\n    {\n        printf(&quot;ftold=%ld\\n&quot;, ftold); // 4\n    }\n}\n</code></pre>\n<p>C 标准库的 I/O 缓冲区有三种类型：全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时，不同类型的缓冲区具有不同的特性。</p>\n<ul>\n<li>全缓冲：如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。</li>\n<li>行缓冲：如果用户程序写的数据中有换行符就把这一行写回内核，或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。</li>\n<li>无缓冲：用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的，这样用户程序产生的错误信息可以尽快输出到设备。</li>\n</ul>\n<p>调用 <code>int fflush(FILE *stream); </code> 可强制刷新 flush 缓冲，确保数据写回内核<br>成功返回 0，失败返回 EOF。传入空指针 <code>fflush(NULL)</code> 可对所有打开文件的 I/O 缓冲区做 Flush 操作</p>\n<h1>系统调用，Unbuffered I/O</h1>\n<p>系统调用是无（用户态）缓冲 I/O （C 标准库在用户态自动维护缓冲区，系统调用时内核可自己维护缓冲）<br>标准库 I/O 操作的对象是 <code>FILE*</code> 结构体指针，而 linux 系统调用则使用文件描述符（int）</p>\n<p>stdin stdout stderr 的文件描述符分别为：</p>\n<pre><code class=\"language-c\">#define STDIN_FILENO 0\n#define STDOUT_FILENO 1\n#define STDERR_FILENO 2\n</code></pre>\n<p>参考：<a href=\"https://man.archlinux.org/man/open.2.zh_CN\">open(2)</a></p>\n<pre><code class=\"language-c\">#include &lt;sys/types.h&gt;\n#include &lt;sys/stat.h&gt;\n#include &lt;fcntl.h&gt;\n#include &lt;unistd.h&gt;\n#include &lt;stdlib.h&gt;\n#include &lt;stdio.h&gt;\n\nint make_fd_readonly(int fd);\n\nint main()\n{\n    // O_RDONLY 只读打开\n    // O_WRONLY 只写打开\n    // O_RDWR 可读可写打开\n    int fd = open(&quot;./todo.txt&quot;, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);\n\n    ssize_t write_count = write(fd, &quot;hello&quot;, sizeof(&quot;hello&quot;) - 1); // 减一防止写入 &#39;\\0&#39;\n\n    off_t currpos = lseek(fd, 1 - (long)(sizeof(&quot;hello&quot;)), SEEK_CUR); // 负数，向前移动 5 字节\n\n    int len = 100;\n    size_t size = sizeof(char) * len;\n    char *buf = malloc(size);\n    ssize_t ssize = read(fd, buf, size);\n\n    if (ssize == -1)\n    {\n        perror(&quot;open()&quot;);\n        exit(EXIT_FAILURE);\n    }\n\n    printf(&quot;read %ld bytes = %s\\n&quot;, ssize, buf);\n\n    int flags = fcntl(STDIN_FILENO, F_GETFL);\n    printf(&quot;stdin&#39;s flags = %d mode=%d\\n&quot;, flags, flags &amp; O_ACCMODE);\n\n    if (make_fd_readonly(fd) == -1)\n        exit(1);\n    write(fd, &quot;x&quot;, 1);\n    perror(&quot;O_RDONLY&quot;); // Success, 还是可写，不影响实际的文件权限\n}\n\nint make_fd_readonly(int fd)\n{\n    int flags = fcntl(fd, F_GETFL);\n    if (flags == -1)\n    {\n        // 处理错误\n        return -1;\n    }\n\n    flags |= O_RDONLY; // 设置只读标志\n\n    if (fcntl(fd, F_SETFL, flags) == -1)\n    {\n        // 处理错误\n        return -1;\n    }\n\n    return 0;\n}\n</code></pre>\n<p>ioctl() 向设备发控制和配置命令<br>mmap() 将文件映射到内存</p>\n<h1>文件系统</h1>\n<blockquote>\n<p>以下摘录自 <a href=\"https://linux-c.yieldray.fun/#/3-Linux-%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B/ch29-%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F?id=_24-%e6%96%87%e4%bb%b6%e5%92%8c%e7%9b%ae%e5%bd%95%e6%93%8d%e4%bd%9c%e7%9a%84%e7%b3%bb%e7%bb%9f%e5%87%bd%e6%95%b0\">LinuxC 一站式编程</a></p>\n</blockquote>\n<p><code>stat(2)</code> 函数读取文件的 inode，然后把 inode 中的各种文件属性填入一个 <code>struct stat</code> 结构体传出给调用者（跟随符号链接）。</p>\n<p><code>lstat(2)</code> 同 <code>stat(2)</code>，但对于符号链接，返回其本身的 inode。</p>\n<p><code>fstat(2)</code> 函数传入一个已打开的文件描述符，传出 inode 信息。</p>\n<p><code>access(2)</code> 函数检查执行当前进程的用户是否有权限访问某个文件，传入文件路径和要执行的访问操作（读/写/执行），<br>access 函数取出文件 inode 中的 st_mode 字段，比较一下访问权限，然后返回 <code>0</code> 表示允许访问，返回 <code>-1</code> 表示错误或不允许访问。</p>\n<p><code>chmod(2)</code> 和 <code>fchmod(2)</code> 函数改变文件的访问权限，也就是修改 inode 中 的 st_mode 字段。<br>这两个函数的区别类似于 stat/fstat。</p>\n<p><code>chown(2)/fchown(2)/lchown(2)</code> 改变文件的所有者和组，也就是修改 inode 中的 User 和 Group 字段，<br>只有 root 用户才能正确调用这几个函数，这几个函数之间的区别类似于 stat/fstat/lstat。</p>\n<p><code>utime(2)</code> 函数改变文件的访问时间和修改时间，也就是修改 inode 中的 atime 和 mtime 字段。</p>\n<p><code>truncate(2)</code> 和 <code>ftruncate(2)</code> 函数把文件截断到某个长度，如果新的长度比原来的长度短，则后面的数据被截掉了，<br>如果新的长度比原来的长度长，则后面多出来的部分用 0 填充，这需要修改 inode 中的 Blocks 索引项以及块位图中相应的 bit。<br>这两个函数的区别类似于 stat/fstat。</p>\n<p><code>link(2)</code> 函数创建硬链接，其原理是在目录的数据块中添加一条新记录，其中的 inode 号字段和原文件相同。<br><code>symlink(2)</code> 函数创建一个符号链接，这需要创建一个新的 inode，其中 st_mode 字段的文件类型是符号链接，<br>原文件的路径保存在 inode 中或者分配一个数据块来保存。</p>\n<p><code>unlink(2)</code> 函数删除一个链接。如果是符号链接则释放这个符号链接的 inode 和数据块，清除 inode 位图和块位图中相应的位。<br>如果是硬链接则从目录的数据块中清除一条文件名记录，如果当前文件的硬链接数已经是 1 了还要删除它，<br>就同时释放它的 inode 和数据块，清除 inode 位图和块位图中相应的位，这样就真的删除文件了。</p>\n<p><code>rename(2)</code> 函数改变文件名，需要修改目录数据块中的文件名记录，如果原文件名和新文件名不在一个目录下，<br>则需要从原目录数据块中清除一条记录然后添加到新目录的数据块中。<br>注：在不同的分区之间移动文件就必须复制和删除 inode 和数据块</p>\n<p><code>readlink(2)</code> 函数读取一个符号链接所指向的目标路径，其原理是从符号链接的 inode 或数据块中读出保存的数据，这就是目标路径。</p>\n<p><code>mkdir(2)</code> 函数创建新的目录，要做的操作是在它的父目录数据块中添加一条记录，然后分配新的 inode 和 数据块，<br>新 inode 的 st_mode 字段的文件类型是目录，在新数据块中填两个记录，分别是 <code>.</code> 和 <code>..</code>，<br>由于 .. 表示父目录，因此父目录的硬链接数要加 1。</p>\n<p><code>rmdir(2)</code> 函数删除一个目录，这个目录必须是空的（只包含 <code>.</code> 和 <code>..</code>）才能删除，要做的操作是<br>释放它的 inode 和数据块，清除 inode 位图和块位图中相应的位，清除父目录数据块中的记录，父目录的硬链接数要减 1。</p>\n<pre><code class=\"language-c\">// 遍历目录演示\n#include &lt;sys/stat.h&gt;\n#include &lt;sys/types.h&gt;\n#include &lt;unistd.h&gt;\n#include &lt;dirent.h&gt;\n#include &lt;stdio.h&gt;\n#include &lt;errno.h&gt;\n\n#define MAX_PATH 1024\n\nint main()\n{\n    if (mkdir(&quot;./todo&quot;, 0777) == 0)\n    {\n        printf(&quot;成功创建目录：./todo\\n&quot;);\n    }\n    else\n    {\n        const char *dirPath = &quot;./todo&quot;;\n        switch (errno)\n        {\n        case EACCES:\n            printf(&quot;权限不足，无法创建目录：%s\\n&quot;, dirPath);\n            break;\n        case EEXIST:\n            printf(&quot;目录已存在：%s\\n&quot;, dirPath);\n            break;\n        case ENAMETOOLONG:\n            printf(&quot;路径名太长：%s\\n&quot;, dirPath);\n            break;\n        case ENOENT:\n            printf(&quot;路径中的一个目录不存在：%s\\n&quot;, dirPath);\n            break;\n        case ENOTDIR:\n            printf(&quot;路径中的一个组件不是目录：%s\\n&quot;, dirPath);\n            break;\n        default:\n            printf(&quot;无法创建目录：%s，错误码：%d\\n&quot;, dirPath, errno);\n            break;\n        }\n    }\n\n    // 创建一个文件，仅供测试\n    fputs(&quot;hello, world!&quot;, fopen(&quot;./todo/test.txt&quot;, &quot;w+&quot;));\n\n    // 打开目录\n    DIR *dir = opendir(&quot;./todo&quot;);\n    // 遍历目录\n    struct dirent *entry;\n    while ((entry = readdir(dir)) != NULL)\n    {\n        // 获取文件路径\n        char file_path[MAX_PATH];\n        snprintf(file_path, MAX_PATH, &quot;./todo/%s&quot;, entry-&gt;d_name);\n        // 获取文件/目录信息\n        struct stat file_info;\n        stat(file_path, &amp;file_info);\n        // 打印\n        printf(&quot;%-10ld &quot;, file_info.st_size);\n        printf((S_ISDIR(file_info.st_mode)) ? &quot;d&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IRUSR) ? &quot;r&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IWUSR) ? &quot;w&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IXUSR) ? &quot;x&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IRGRP) ? &quot;r&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IWGRP) ? &quot;w&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IXGRP) ? &quot;x&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IROTH) ? &quot;r&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IWOTH) ? &quot;w&quot; : &quot;-&quot;);\n        printf((file_info.st_mode &amp; S_IXOTH) ? &quot;x&quot; : &quot;-&quot;);\n        printf(&quot;  %-25s\\n&quot;, entry-&gt;d_name);\n        fflush(stdout);\n    }\n    closedir(dir);\n}\n</code></pre>\n<h1>复制文件描述符 dup() dup2()</h1>\n<p>复制文件描述符：不新建 file 结构体，而是其引用计数增加<br>显然，close() 系统调用使引用计数减一，并当计数归零时真正关闭文件</p>\n<p><a href=\"https://dashdash.io/2/dup\">https://dashdash.io/2/dup</a></p>\n<pre><code class=\"language-c\">/**\n * 创建文件描述符 oldfd 的副本，返回值是该进程未使用的最小文件描述符\n */\nint dup(int oldfd);\n\n/**\n * 与 dup 相同，但返回的是 newfd 指定的文件描述符，若该文件描述符已打开，则在重用之前静默关闭\n */\nint dup2(int oldfd, int newfd);\n\n/**\n * 与 dup2 相同，但可使用标志位指定 O_CLOEXEC 来强制为新文件描述符设置关闭执行标志\n */\nint dup3(int oldfd, int newfd, int flags);\n</code></pre>\n","tags":["c"]},{"id":"build-you-own-react","url":"https://yieldray.fun/posts/build-you-own-react","title":"Build your own React","date_published":"2023-07-25T17:00:00.000Z","date_modified":"2023-07-25T17:00:00.000Z","content_text":"<p>我们编写的 React，称之为 <a href=\"https://pomb.us/build-your-own-react/\">Didact</a></p>\n<h1>Step Zero: Review</h1>\n<pre><code class=\"language-tsx\">/** @jsx Didact.createElement */\n/** @jsxFrag Didact.Fragment */\n\nconst element1 = &lt;h1 title=&quot;foo&quot;&gt;Hello&lt;/h1&gt;;\n//       |\n//       v\nconst element2 = Didact.createElement(&quot;h1&quot;, { title: &quot;foo&quot; }, &quot;Hello&quot;);\n//       |\n//       v\nconst element3: Didact.DidactElement = {\n    type: &quot;h1&quot;,\n    props: {\n        title: &quot;foo&quot;,\n        children: [&quot;Hello&quot;],\n    },\n};\n</code></pre>\n<h1>Step I: The createElement Function</h1>\n<pre><code class=\"language-tsx\">namespace Didact {\n    export type DidactNode = DidactElement | Array&lt;DidactNode&gt; | string | number | boolean | null | undefined;\n    // 简化版类型，@types/react 中写的是 Iterable&lt;ReactNode&gt; 而不是数组\n\n    export interface DidactElement&lt;P = any, T extends string = string&gt; {\n        type: T; // react 内部会使用 Symbol 作为 type\n        props: P &amp; { children?: Array&lt;DidactElement | DidactElement[]&gt; }; // 注意 children 的类型\n        // 没有实现 key 属性 key: Key | null\n    }\n\n    // 将 Didact 对象转换为 Didact 元素\n    export function createElement(\n        type: string,\n        props?: Record&lt;string, any&gt; | null,\n        ...children: DidactNode[]\n    ): DidactElement {\n        // 注意返回值\n        function nodeToElement(node: DidactNode): DidactElement | DidactElement[] {\n            if (node === null) return createNullElement(); // falsy 值，不渲染\n            if (Array.isArray(node)) return node.map(nodeToElement) as DidactElement[]; // 处理数组\n            if (typeof node === &quot;object&quot;) return node;\n            if (typeof node === &quot;string&quot;) return createTextElement(node);\n            if (typeof node === &quot;number&quot;) return createTextElement(String(node));\n            if (Boolean(node) === false) return createNullElement(); // falsy 值，不渲染\n            return createTextElement(String(node)); // 强转为字符串\n        }\n        return {\n            type,\n            props: {\n                ...props,\n                children: children.map(nodeToElement),\n            },\n        };\n    }\n\n    // 辅助函数，将字符串转换为 Didact 元素\n    function createTextElement(text: string): DidactElement {\n        return {\n            type: &quot;TEXT_ELEMENT&quot;,\n            props: {\n                /**\n                 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/nodeValue)\n                 * 使用 nodeValue 属性方便在 render 函数中操作\n                 */\n                nodeValue: text,\n                children: [],\n            },\n        };\n    }\n\n    // 辅助函数，将 falsy 值转换为 Didact 元素\n    function createNullElement(): DidactElement {\n        return {\n            type: &quot;NULL_ELEMENT&quot;,\n            props: {\n                children: [],\n            },\n        };\n    }\n}\n</code></pre>\n<h1>Step II: The render Function</h1>\n<pre><code class=\"language-tsx\">namespace Didact {\n    export function render(element: DidactElement, container: Element) {\n        // 跳过空元素\n        if (element.type === &quot;NULL_ELEMENT&quot;) return;\n        // 生成 dom 节点\n        const dom = element.type == &quot;TEXT_ELEMENT&quot; ? document.createTextNode(&quot;&quot;) : document.createElement(element.type);\n        // 设置 dom 属性\n        Object.keys(element.props)\n            .filter((key) =&gt; key !== &quot;children&quot;) // children 是特殊的属性，排除之！\n            .forEach((name) =&gt; Reflect.set(dom, name, element.props[name]));\n        // 递归渲染\n        element.props.children.forEach((child) =&gt; render(child, dom));\n        // 渲染进容器\n        container.appendChild(dom);\n    }\n}\n</code></pre>\n<p><strong>题外话</strong>：实现一个基础的 SSR 也很简单，可以参考<a href=\"https://github.com/jeasx/jsx-async-runtime\">这个简单实现</a></p>\n<pre><code class=\"language-ts\">export function render(element: DidactElement) {\n    if (element.type === &quot;NULL_ELEMENT&quot;) return &quot;&quot;;\n    if (element.type === &quot;TEXT_ELEMENT&quot;) return element.props.nodeValue;\n\n    let left = `&lt;${element.type}`;\n    let right = `&lt;/${element.type}&gt;`;\n\n    Object.keys(element.props)\n        .filter((key) =&gt; key !== &quot;children&quot;)\n        .forEach((name) =&gt; {\n            left += ` ${name}=&quot;${element.props[name]}&quot;`;\n        });\n    left += &quot;&gt;&quot;;\n\n    element.props.children.forEach((child) =&gt; {\n        left += render(child); // [!code highlight]\n    });\n\n    return left + right;\n}\n</code></pre>\n<h1>Step III: Concurrent Mode</h1>\n<p>上面的递归实现简单易懂，但渲染函数是完全同步的<br>若 VDOM 很大，渲染需要<strong>阻塞</strong>主线程很长时间，导致页面卡顿</p>\n<p>解决办法是将渲染函数的渲染过程拆分为多个更小的单元，<br>每个渲染都通过浏览器提供的 <a href=\"https://developer.mozilla.org/docs/Web/API/Window/requestIdleCallback\">requestIdleCallback</a> 函数来调度，实现每次渲染只占用主线程一小段时间</p>\n<blockquote>\n<p>React 内部并非使用 requestIdleCallback，而是实现了 <a href=\"https://github.com/facebook/react/tree/main/packages/scheduler\">scheduler</a> 来调度<br>参见：<a href=\"https://yieldray.fun/posts/js-prioritized-task-scheduling-api#%E9%A2%98%E5%A4%96%E8%AF%9Dreact-scheduler\">https://yieldray.fun/posts/js-prioritized-task-scheduling-api#%E9%A2%98%E5%A4%96%E8%AF%9Dreact-scheduler</a><br>React 内部通过时间切片，实现了优先级调度（用于处理渲染、事件、动画等）、中断和恢复、并发渲染（Lane 机制）、避免避免饥饿，充分占用主线程<br>简而言之，实际实现更为复杂，这个简单实现不会引入这些复杂性</p>\n</blockquote>\n<pre><code class=\"language-ts\">// requestIdleCallback 定义\ndeclare function requestIdleCallback(\n    /**\n     * A reference to a function that should be called in the near future, when the event loop is idle. The callback function is passed an IdleDeadline object describing the amount of time available and whether or not the callback has been run because the timeout period expired.\n     */\n    callback: (deadline: {\n        /**\n         * A Boolean whose value is true if the callback is being executed because the timeout specified when the idle callback was installed has expired.\n         */\n        readonly didTimeout: boolean;\n        /**\n         * Returns a DOMHighResTimeStamp, which is a floating-point value providing an estimate of the number of milliseconds remaining in the current idle period. If the idle period is over, the value is 0. Your callback can call this repeatedly to see if there&#39;s enough time left to do more work before returning.\n         */\n        timeRemaining(): DOMHighResTimeStamp;\n    }) =&gt; void,\n    options?: {\n        /**\n         * If the number of milliseconds represented by this parameter has elapsed and the callback has not already been called, then a task to execute the callback is queued in the event loop (even if doing so risks causing a negative performance impact). timeout must be a positive value or it is ignored.\n         */\n        timeout?: number;\n    },\n): number;\n</code></pre>\n<h1>Step IV: Fibers</h1>\n<p>我们要构建一棵 fiber 树，每个 fiber 可作为一个工作单元来渲染</p>\n<p>每个 fiber 只引用其父 fiber，相邻的下一个 fiber，和<strong>第一个</strong>子 fiber</p>\n<p><img src=\"https://s2.loli.net/2023/07/25/1wEpOJAFXvSgxMb.png\" alt=\"FiberTree\"></p>\n<p>渲染时，我们递归地：</p>\n<ol>\n<li>渲染当前 fiber</li>\n<li>渲染当前 fiber 的所有子 fiber</li>\n<li>依次渲染当前 fiber 的兄弟 fiber</li>\n</ol>\n<pre><code class=\"language-ts\">namespace Didact {\n    interface Fiber extends DidactElement {\n        dom: Node | null;\n        parent: Fiber | null; // 父 fiber\n        child: Fiber | null; // 第一个子 fiber\n        sibling: Fiber | null; // 兄弟 fiber （相邻的下一个）\n    }\n\n    // 将原本的 render 方法抽离出一个用于创建 dom 节点的方法，我们用此方法生成 fiber 的 dom 属性\n    function createDom(fiber: Fiber): Node | null {\n        if (fiber.type === &quot;NULL_ELEMENT&quot;) return null;\n        const dom = fiber.type == &quot;TEXT_ELEMENT&quot; ? document.createTextNode(&quot;&quot;) : document.createElement(fiber.type);\n        Object.keys(fiber.props)\n            .filter((key) =&gt; key !== &quot;children&quot;)\n            .forEach((name) =&gt; Reflect.set(dom, name, element.props[name]));\n        return dom;\n    }\n\n    let nextUnitOfWork: Fiber | null = null; // 一个工作单元其实是一个不完整的 fiber\n\n    function workLoop({ timeRemaining }: { timeRemaining(): DOMHighResTimeStamp }) {\n        let shouldYield = false;\n        while (nextUnitOfWork &amp;&amp; !shouldYield) {\n            nextUnitOfWork = performUnitOfWork(nextUnitOfWork);\n            shouldYield = timeRemaining() &lt; 1;\n        }\n        //! 继续执行工作单元\n        if (nextUnitOfWork) requestIdleCallback(workLoop);\n    }\n\n    export function render(element: DidactElement, container: Node) {\n        // 第一个工作单元无需渲染，因为它是容器元素，而不是 Didact 元素\n        // 只有 Didact 元素才需要渲染\n        nextUnitOfWork = {\n            dom: container,\n            props: {\n                children: [element],\n            },\n        } as Fiber; // 其余属性在渲染时生成，这里先强转一下\n        //! 开始执行工作单元\n        requestIdleCallback(workLoop);\n    }\n\n    function performUnitOfWork(fiber: Fiber): Fiber | null {\n        if (!fiber.dom) {\n            // 如果还没有渲染，说明是 Didact 元素，执行渲染\n            // 只有容器元素不需要渲染，也就是说只有第一个工作单元不会发生渲染，之后的工作单元都会发生渲染\n            fiber.dom = createDom(fiber);\n            // 请注意每个工作单元仅调用一次 createDom 函数，也就是仅执行一次渲染\n        }\n        if (fiber.parent) {\n            // 父 fiber 的 dom 必然是已渲染的\n            if (fiber.dom) fiber.parent.dom!.appendChild(fiber.dom);\n            // 将当前 fiber 渲染进父 fiber （跳过空节点）\n        }\n        // 下面生成当前 fiber 的 child 和 sibling 属性（部分构建 fiber 树）\n        let prevSibling: Fiber | null = null; //\n        const elements: DidactElement[] = fiber.props.children.flat(1); // 对于当前 fiber 的所有子元素（flat 一层）\n        let index = 0;\n        while (index &lt; elements.length) {\n            const element: DidactElement = elements[index];\n            const newFiber: Fiber = {\n                type: element.type,\n                props: element.props,\n                parent: fiber,\n                dom: null,\n                child: null,\n                sibling: null,\n            }; // 将当前 fiber 的每一个子元素转换为子 fiber\n            if (index === 0) {\n                fiber.child = newFiber; // 第一个子 fiber，通过当前 fiber 的 child 属性引用\n            } else {\n                prevSibling!.sibling = newFiber; // 否则，将前一个子 fiber 的兄弟（相邻的下一个） fiber 设置为当前子 fiber\n            }\n            prevSibling = newFiber; // 将前一个 fiber 的引用更新为当前子 fiber\n            index++;\n        }\n\n        // 注：一看到 return 就表示发生了递归！\n\n        // 下面依次渲染子 fiber （递归）\n        if (fiber.child) {\n            // 如果当前 fiber 有子 fiber，令下一个工作单元渲染之\n            return fiber.child;\n        }\n        // 下面依次渲染兄弟 fiber （递归）\n        let nextFiber = fiber;\n        while (nextFiber) {\n            if (nextFiber.sibling) {\n                // 如果有兄弟 fiber，就依次渲染\n                return nextFiber.sibling;\n            } else {\n                // 否则，回到父 fiber\n                nextFiber = nextFiber.parent!;\n                // 继续渲染父 fiber 的兄弟节点\n            }\n        }\n        // 渲染完了（递归结束）\n        return null;\n    }\n}\n</code></pre>\n<h1>Step V: Render and Commit Phases</h1>\n<p>上面的渲染存在问题：每次 performUnitOfWork 函数渲染 fiber 就将其放入父节点（即： <code>parent.appendChild(child)</code>）<br>但浏览器可能在渲染某一个子节点时打断，导致浏览器渲染了一个未完成的 fiber 树，这就造成 UI 闪烁</p>\n<p>解决办法是先将 appendChild 操作收集起来，最后一次性应用到真实的 DOM 上</p>\n<p>1.移除 performUnitOfWork 函数中的 dom 添加操作<br>2.创建一个 wipRoot 引用，指向当前渲染中的根 fiber<br>3.待整棵 fiber 树渲染完毕后，再执行 dom 的添加操作（可以很方便的通过<em>递归 wipRoot 树</em>来实现）</p>\n<pre><code class=\"language-ts\">namespace Didact {\n    let nextUnitOfWork: Fiber | null = null; // 一个工作单元其实是一个不完整的 fiber\n\n    function workLoop({ timeRemaining }: IdleDeadline) {\n        let shouldYield = false;\n        while (nextUnitOfWork &amp;&amp; !shouldYield) {\n            nextUnitOfWork = performUnitOfWork(nextUnitOfWork);\n            shouldYield = timeRemaining() &lt; 1;\n        }\n        if (!nextUnitOfWork &amp;&amp; wipRoot) {\n            // 没有需要渲染的工作，说明渲染完毕，可以添加进 dom 了\n            commitRoot();\n        } else {\n            //! 否则，继续执行工作单元\n            requestIdleCallback(workLoop);\n        }\n    }\n\n    let wipRoot: Fiber | null = null; // 根 fiber，需等待其完全渲染完\n\n    export function render(element: DidactElement, container: Node) {\n        wipRoot = {\n            dom: container,\n            props: {\n                children: [element],\n            },\n        } as Fiber;\n\n        nextUnitOfWork = wipRoot;\n    }\n\n    function performUnitOfWork(fiber: Fiber): Fiber | null {\n        if (!fiber.dom) {\n            fiber.dom = createDom(fiber);\n        }\n\n        //if (fiber.parent) {\n        //    if (fiber.dom) fiber.parent.dom!.appendChild(fiber.dom);\n        //}\n\n        //\n        // ... ...\n        //\n    }\n\n    function commitRoot() {\n        // 只有 wipRoot 赋值后才会调用 commitRoot() 函数\n        commitWork(wipRoot!.child); // 递归函数\n        wipRoot = null; // 添加完了，置空\n    }\n\n    function commitWork(fiber: Fiber | null) {\n        if (!fiber) return;\n        // 传入 commitWork() 函数的 fiber 不是根 fiber，故必存在 parent 引用\n        const domParent = fiber.parent!.dom!;\n        // 递归地将 fiber 的 dom 添加进父 dom 节点\n        if (fiber.dom) domParent.appendChild(fiber.dom);\n        commitWork(fiber.child);\n        commitWork(fiber.sibling);\n    }\n}\n</code></pre>\n<h1>Step VI: Reconciliation</h1>\n<p>上面只处理了创建元素，还需要处理更新和移除元素</p>\n<p>1.为 Fiber 添加 alternate 属性，指向上一次渲染的对应 fiber，用于 diff 操作<br>2.添加 effectTag 属性，用于标识 commit 过程需要执行的操作类型（称为 reconcile）</p>\n<pre><code class=\"language-ts\">namespace Didact {\n    interface Fiber extends DidactElement {\n        dom: Node | null;\n        parent: Fiber | null;\n        child: Fiber | null;\n        sibling: Fiber | null;\n        alternate: Fiber | null; // 用于 diff\n        effectTag: &quot;UPDATE&quot; | &quot;PLACEMENT&quot; | &quot;DELETION&quot;; // 用于 commit\n    }\n\n    function createDom(fiber: Fiber): Node | null {\n        if (fiber.type === &quot;NULL_ELEMENT&quot;) return null;\n        const dom = fiber.type == &quot;TEXT_ELEMENT&quot; ? document.createTextNode(&quot;&quot;) : document.createElement(fiber.type);\n        updateDom(dom, {}, fiber.props);\n        return dom;\n    }\n\n    const isEvent = (key: string) =&gt; key.startsWith(&quot;on&quot;);\n    const isProperty = (key: string) =&gt; key !== &quot;children&quot; &amp;&amp; !isEvent(key);\n    const isNew = (prev: Record&lt;keyof any, any&gt;, next: Record&lt;keyof any, any&gt;) =&gt; (key: keyof any) =&gt;\n        prev[key] !== next[key];\n    const isGone = (prev: Record&lt;keyof any, any&gt;, next: Record&lt;keyof any, any&gt;) =&gt; (key: keyof any) =&gt; !(key in next);\n\n    function updateDom(dom: Node, prevProps: Record&lt;keyof any, any&gt;, nextProps: Record&lt;keyof any, any&gt;) {\n        Object.keys(prevProps)\n            .filter(isEvent)\n            // 选取旧的和改变的监听器\n            .filter((key) =&gt; !(key in nextProps) || isNew(prevProps, nextProps)(key))\n            // 移除之\n            .forEach((name) =&gt; {\n                const eventType = name.toLowerCase().substring(2);\n                dom.removeEventListener(eventType, prevProps[name]);\n            });\n\n        Object.keys(nextProps)\n            .filter(isEvent)\n            // 选取所有新的监听器\n            .filter(isNew(prevProps, nextProps))\n            // 添加之\n            .forEach((name) =&gt; {\n                const eventType = name.toLowerCase().substring(2);\n                dom.addEventListener(eventType, nextProps[name]);\n            });\n\n        Object.keys(prevProps)\n            // 选取所有旧的属性\n            .filter(isProperty)\n            // 若该属性不需要了\n            .filter(isGone(prevProps, nextProps))\n            // 移除之\n            .forEach((name) =&gt; {\n                Reflect.set(dom, name, &quot;&quot;);\n            });\n\n        Object.keys(nextProps)\n            .filter(isProperty)\n            // 选取所有新的属性\n            .filter(isNew(prevProps, nextProps))\n            // 添加之\n            .forEach((name) =&gt; {\n                Reflect.set(dom, name, nextProps[name]);\n            });\n    }\n\n    function commitRoot() {\n        // 对 dom 的更改操作都移入 commit 中\n        deletions!.forEach(commitWork);\n        commitWork(wipRoot!.child);\n        currentRoot = wipRoot;\n        wipRoot = null;\n    }\n\n    function commitWork(fiber: Fiber | null) {\n        if (!fiber) {\n            // 递归终止条件\n            return;\n        }\n\n        const domParent = fiber.parent!.dom!;\n        if (fiber.effectTag === &quot;PLACEMENT&quot; &amp;&amp; fiber.dom != null) {\n            domParent.appendChild(fiber.dom);\n        } else if (fiber.effectTag === &quot;DELETION&quot; &amp;&amp; fiber.dom != null) {\n            domParent.removeChild(fiber.dom);\n        } else if (fiber.effectTag === &quot;UPDATE&quot; &amp;&amp; fiber.dom != null) {\n            updateDom(fiber.dom, fiber.alternate!.props, fiber.props);\n        }\n\n        commitWork(fiber.child);\n        commitWork(fiber.sibling);\n    }\n\n    function render(element: DidactElement, container: Node) {\n        wipRoot = {\n            dom: container,\n            props: {\n                children: [element],\n            },\n            alternate: currentRoot,\n        } as Fiber;\n        deletions = [];\n        nextUnitOfWork = wipRoot;\n        requestIdleCallback(workLoop);\n    }\n\n    let nextUnitOfWork: Fiber | null = null;\n    let currentRoot: Fiber | null = null;\n    let wipRoot: Fiber | null = null;\n    let deletions: Fiber[] | null = null;\n\n    function workLoop(deadline: IdleDeadline) {\n        let shouldYield = false;\n        while (nextUnitOfWork &amp;&amp; !shouldYield) {\n            nextUnitOfWork = performUnitOfWork(nextUnitOfWork);\n            shouldYield = deadline.timeRemaining() &lt; 1;\n        }\n        if (!nextUnitOfWork &amp;&amp; wipRoot) {\n            // 渲染完整棵树\n            commitRoot();\n        } else {\n            requestIdleCallback(workLoop);\n        }\n    }\n\n    function performUnitOfWork(fiber: Fiber) {\n        if (!fiber.dom) {\n            fiber.dom = createDom(fiber);\n        }\n\n        const elements = fiber.props.children;\n        reconcileChildren(fiber, elements); // 调整所有子 fiber\n\n        if (fiber.child) {\n            return fiber.child;\n        }\n        let nextFiber = fiber;\n        while (nextFiber) {\n            if (nextFiber.sibling) {\n                return nextFiber.sibling;\n            } else {\n                nextFiber = nextFiber.parent!;\n            }\n        }\n        return null;\n    }\n\n    function reconcileChildren(wipFiber: Fiber, elems: Array&lt;DidactElement | DidactElement[]&gt;) {\n        // 重要：flat 一层\n        const elements: DidactElement[] = elems.flat(1);\n        // wipFiber 是 elements 的直接父节点\n        let index = 0;\n        // 与上一次渲染的相同位置做比较\n        let oldFiber: Fiber | null = wipFiber.alternate?.child;\n        // 当前子 fiber 的上一个兄弟 fiber\n        let prevSibling: Fiber | null = null;\n\n        while (index &lt; elements.length || oldFiber != null) {\n            const element = elements[index];\n\n            let newFiber: Fiber | null = null;\n            const sameType = oldFiber &amp;&amp; element &amp;&amp; element.type === oldFiber.type;\n\n            // 通过 effectTag 属性标记，待到 commitWork 函数中执行相应操作\n            if (sameType) {\n                newFiber = {\n                    type: oldFiber!.type,\n                    props: element.props,\n                    dom: oldFiber!.dom, // 无需重新创建 dom\n                    parent: wipFiber,\n                    alternate: oldFiber,\n                    effectTag: &quot;UPDATE&quot;,\n                    child: null,\n                    sibling: null,\n                };\n            }\n            if (element &amp;&amp; !sameType) {\n                newFiber = {\n                    type: element.type,\n                    props: element.props,\n                    dom: null, // 需要重新创建 dom\n                    parent: wipFiber,\n                    alternate: null,\n                    effectTag: &quot;PLACEMENT&quot;,\n                    child: null,\n                    sibling: null,\n                };\n            }\n            if (oldFiber &amp;&amp; !sameType) {\n                oldFiber.effectTag = &quot;DELETION&quot;;\n                deletions!.push(oldFiber); // 添加到删除列表\n            }\n\n            if (oldFiber) {\n                // 继续比较\n                oldFiber = oldFiber.sibling;\n            }\n\n            if (index === 0) {\n                wipFiber.child = newFiber;\n            } else if (element) {\n                // 注：if (element) 没必要\n                prevSibling!.sibling = newFiber;\n            }\n\n            prevSibling = newFiber;\n            index++;\n        }\n    }\n}\n</code></pre>\n<h1>Step VII: Function Components</h1>\n<p>函数组件的 type 属性指向一个返回 DidactElement 的函数，其 children 属性通过调用该函数得到<br>函数组件的 fiber 是没有 DOM 属性的，那么在移除函数组件的子节点时，就需要寻找到函数组件的上层 DOM 才能完成移除</p>\n<pre><code class=\"language-tsx\">function App(props: { name: string; children?: DidactNode }) {\n    return (\n        &lt;article&gt;\n            &lt;h1&gt;Hello, {props.name}&lt;/h1&gt;\n            &lt;div&gt;{props.children}&lt;/div&gt;\n        &lt;/article&gt;\n    );\n}\nconst element = &lt;App name=&quot;foo&quot; /&gt;;\n//               ^     ^\n//               |     |\n//              type  props\n</code></pre>\n<pre><code class=\"language-ts\">namespace Didact {\n    type FC&lt;P = any&gt; = (props: P) =&gt; DidactElement;\n\n    interface DidactElement&lt;P = any, T extends string | FC&lt;P&gt; = string | FC&lt;P&gt;&gt; {\n        type: T;\n        props: P;\n    }\n\n    interface Fiber&lt;T extends string | FC = string | FC&gt; extends DidactElement&lt;any, T&gt; {\n        dom: Node | null;\n        parent: Fiber | null;\n        child: Fiber | null;\n        sibling: Fiber | null;\n        alternate: Fiber | null;\n        effectTag: &quot;UPDATE&quot; | &quot;PLACEMENT&quot; | &quot;DELETION&quot;;\n    }\n\n    type DidactFragment = Array&lt;DidactNode&gt;;\n\n    // Fragment 函数的实现非常简单，它只是一个特殊的函数组件\n    function Fragment(props: { children?: DidactNode[] }): DidactFragment {\n        return props.children || [];\n    }\n\n    function createDom(fiber: Fiber&lt;string&gt;): Node | null {\n        // ... ...\n    }\n\n    function updateDom(dom: Node, prevProps: Record&lt;keyof any, any&gt;, nextProps: Record&lt;keyof any, any&gt;) {\n        // ... ...\n    }\n\n    function commitRoot() {\n        deletions!.forEach(commitWork);\n        commitWork(wipRoot!.child);\n        currentRoot = wipRoot;\n        wipRoot = null;\n    }\n\n    function commitWork(fiber: Fiber | null) {\n        if (!fiber) {\n            // 递归终止条件\n            return;\n        }\n        // commitWork 传入的 fiber 不是根 fiber，必有 parent 属性\n        let domParentFiber: Fiber = fiber.parent!;\n        while (!domParentFiber.dom) {\n            // 如果当前 fiber 没有 dom 属性，说明这个 fiber 是函数组件的 fiber\n            domParentFiber = domParentFiber.parent!;\n            // 由于上一级也可能是函数组件 fiber，一直向上寻找，直至非函数组件 fiber\n        }\n        const domParent = domParentFiber.dom;\n\n        if (fiber.effectTag === &quot;PLACEMENT&quot; &amp;&amp; fiber.dom != null) {\n            domParent.appendChild(fiber.dom);\n        } else if (fiber.effectTag === &quot;DELETION&quot; &amp;&amp; fiber.dom != null) {\n            commitDeletion(fiber, domParent); //? 处理移除\n        } else if (fiber.effectTag === &quot;UPDATE&quot; &amp;&amp; fiber.dom != null) {\n            updateDom(fiber.dom, fiber.alternate!.props, fiber.props);\n        }\n        // 继续递归\n        commitWork(fiber.child);\n        commitWork(fiber.sibling);\n    }\n\n    function commitDeletion(fiber: Fiber, domParent: Node) {\n        if (fiber.dom) {\n            domParent.removeChild(fiber.dom);\n        } else {\n            // 注：这里函数组件必有子节点\n            commitDeletion(fiber.child!, domParent);\n        }\n    }\n\n    function render(element: DidactElement, container: Node) {\n        // ... ...\n    }\n\n    function workLoop(deadline: IdleDeadline) {\n        // ... ...\n    }\n\n    function performUnitOfWork(fiber: Fiber) {\n        if (fiber.type instanceof Function) {\n            // 函数组件，特殊处理\n            updateFunctionComponent(fiber as Fiber&lt;FC&gt;);\n        } else {\n            // 非函数组件\n            updateHostComponent(fiber as Fiber&lt;string&gt;);\n        }\n\n        if (fiber.child) {\n            return fiber.child;\n        }\n        let nextFiber = fiber;\n        while (nextFiber) {\n            if (nextFiber.sibling) {\n                return nextFiber.sibling;\n            } else {\n                nextFiber = nextFiber.parent!;\n            }\n        }\n        return null;\n    }\n\n    function updateFunctionComponent(fiber: Fiber&lt;FC&gt;) {\n        // 函数组件的 children 通过调用得到\n        //@ts-ignore 处理 Fragment 函数\n        const children = fiber.type === Fragment ? fiber.type(fiber.props) : [fiber.type(fiber.props)];\n        reconcileChildren(fiber, children);\n    }\n\n    function updateHostComponent(fiber: Fiber&lt;string&gt;) {\n        if (!fiber.dom) {\n            fiber.dom = createDom(fiber);\n        }\n        reconcileChildren(fiber, fiber.props.children);\n    }\n\n    function reconcileChildren(wipFiber: Fiber, elems: Array&lt;DidactElement | DidactElement[]&gt;) {\n        // ... ...\n    }\n}\n</code></pre>\n<h1>Step VIII: Hooks</h1>\n<pre><code class=\"language-ts\">namespace Didact {\n    interface Fiber&lt;T extends string | FC = string | FC&gt; extends DidactElement&lt;any, T&gt; {\n        dom: Node | null;\n        parent: Fiber | null;\n        child: Fiber | null;\n        sibling: Fiber | null;\n        alternate: Fiber | null;\n        effectTag: &quot;UPDATE&quot; | &quot;PLACEMENT&quot; | &quot;DELETION&quot;;\n        // hookIndex 用于索引 hooks\n        hooks?: Array&lt;{\n            state: any;\n            queue: Array&lt;(prevState: any) =&gt; any&gt;;\n        }&gt;;\n    }\n\n    let wipFiber: Fiber | null = null;\n    let hookIndex: number | null = null;\n\n    function updateFunctionComponent(fiber: Fiber&lt;FC&gt;) {\n        wipFiber = fiber;\n        // 函数组件添加 hooks 属性\n        wipFiber.hooks = [];\n        // 全局索引置 0\n        hookIndex = 0;\n        // 函数组件内部可以调用 useState()\n        //@ts-ignore 处理 Fragment 函数\n        const children = fiber.type === Fragment ? fiber.type(fiber.props) : [fiber.type(fiber.props)];\n        reconcileChildren(fiber, children);\n    }\n\n    type Action&lt;T&gt; = (prevState: T) =&gt; T;\n\n    // useState 函数，添加 hook 到 fiber 的 hooks 数组\n    // setState 函数，添加 变更函数 到 hook 的 queue 数组\n    export function useState&lt;T = any&gt;(initialState: T): [state: T, setState: Action&lt;T&gt;] {\n        // 注：useState 被 updateFunctionComponent 间接调用\n        // wipFiber 必是一个函数组件的 fiber\n\n        // oldHook 是上一次渲染的对应 hook （第一次渲染时没有）\n        const oldHook = wipFiber!.alternate?.hooks?.[hookIndex!];\n\n        const hook = {\n            state: oldHook ? oldHook.state : initialState,\n            queue: [] as Array&lt;Action&lt;T&gt;&gt;,\n        };\n\n        // 依次执行上一次 fiber 的 hooks，得到上一次的最终 state\n        const actions = oldHook ? oldHook.queue : [];\n        actions.forEach((action: Action&lt;T&gt;) =&gt; {\n            hook.state = action(hook.state);\n        });\n\n        // 调用 setState 会导致调用 performUnitOfWork 然后 updateFunctionComponent\n        const setState = (action: Action&lt;T&gt;) =&gt; {\n            hook.queue.push(action); // 函数组件可多次调用 setState\n            wipRoot = {\n                dom: currentRoot!.dom,\n                props: currentRoot!.props,\n                alternate: currentRoot!,\n            } as Fiber;\n            // 调用 setState 导致重渲染，更新这些全局变量（调整工作单元，重新从根 fiber 渲染）\n            nextUnitOfWork = wipRoot;\n            deletions = [];\n            //! 执行工作单元\n            requestIdleCallback(workLoop);\n        };\n\n        wipFiber!.hooks!.push(hook);\n        hookIndex!++; // 渲染函数组件时，重置索引\n        return [hook.state, setState];\n    }\n\n    function updateHostComponent(fiber: Fiber&lt;string&gt;) {\n        if (!fiber.dom) {\n            fiber.dom = createDom(fiber);\n        }\n        reconcileChildren(fiber, fiber.props.children);\n    }\n\n    function reconcileChildren(wipFiber: Fiber, elems: Array&lt;DidactElement | DidactElement[]&gt;) {\n        // ... ...\n    }\n}\n</code></pre>\n<h1>Extra: useEffect</h1>\n<pre><code class=\"language-ts\">// useEffect 函数，添加 effect hook 到 fiber 的 hooks 数组\nexport function useEffect(create: (() =&gt; void) | void, deps?: any[]) {\n    // 注：useEffect 被 updateFunctionComponent 间接调用\n    // wipFiber 必是一个函数组件的 fiber\n\n    // oldHook 是上一次渲染的对应 hook （第一次渲染时没有）\n    const oldHook = wipFiber!.alternate?.hooks?.[hookIndex!];\n\n    const hasDepsChanged = oldHook ? !deps || !deps.every((dep, i) =&gt; Object.is(dep, oldHook.deps[i])) : true;\n\n    const hook = {\n        tag: &quot;useEffect&quot;,\n        create: hasDepsChanged ? create : oldHook?.create,\n        destroy: hasDepsChanged ? undefined : oldHook?.destroy,\n        deps: hasDepsChanged ? deps : oldHook?.deps,\n    };\n\n    wipFiber!.hooks!.push(hook);\n    hookIndex!++; // 渲染函数组件时，重置索引\n}\n\nfunction commitWork(fiber: Fiber | null) {\n    // 执行函数组件的 effect\n    fiber?.hooks\n        ?.filter((hook) =&gt; hook?.tag === &quot;useEffect&quot;)\n        .forEach((hook) =&gt; {\n            // 执行 cleanup 函数\n            hook.destroy?.();\n            // 执行 effect 函数\n            hook.destroy = hook.create();\n        });\n}\n\nfunction commitDeletion(fiber: Fiber, domParent: Node) {\n    if (fiber.dom) {\n        domParent.removeChild(fiber.dom);\n    } else {\n        // 注：这里函数组件必有子节点\n        commitDeletion(fiber.child!, domParent);\n    }\n    // 当组件被卸载时，执行 cleanup 函数\n    fiber.hooks?.filter((hook) =&gt; hook?.tag === &quot;useEffect&quot;).forEach((hook) =&gt; hook.destroy()); // [!code ++]\n}\n</code></pre>\n<h1>Extra: useLayoutEffect</h1>\n<p>这里把上一步 commitWork 的更改恢复</p>\n<pre><code class=\"language-ts\">export function useLayoutEffect(create: () =&gt; void | (() =&gt; void), deps?: any[]) {\n    const oldHook = wipFiber!.alternate?.hooks?.[hookIndex!];\n\n    const hasDepsChanged = oldHook ? !deps || !deps.every((dep, i) =&gt; Object.is(dep, oldHook.deps[i])) : true;\n\n    const hook = {\n        tag: &quot;useLayoutEffect&quot;, // [!code highlight]\n        create: hasDepsChanged ? create : oldHook?.create,\n        destroy: hasDepsChanged ? undefined : oldHook?.destroy,\n        deps: hasDepsChanged ? deps : oldHook?.deps,\n    };\n\n    wipFiber!.hooks!.push(hook);\n    hookIndex!++;\n}\n\nfunction commitRoot() {\n    // 对 dom 的更改操作都移入 commit 中\n    deletions!.forEach(commitDeletion);\n    commitWork(wipRoot!.child);\n    // 在所有 DOM 更改完成后，同步执行 layout effects\n    commitLayoutEffects(wipRoot);\n    currentRoot = wipRoot;\n    wipRoot = null;\n    // 在所有 DOM 更改和 layout effects 执行完成后，异步执行 effects\n    requestAnimationFrame(() =&gt; commitMutationEffects(currentRoot));\n}\n\nfunction commitDeletion(fiber: Fiber, domParent: Node) {\n    if (fiber.dom) {\n        domParent.removeChild(fiber.dom);\n    } else {\n        // 注：这里函数组件必有子节点\n        commitDeletion(fiber.child!, domParent);\n    }\n    // 当组件被卸载时，执行 cleanup 函数\n    fiber.hooks\n        ?.filter((hook) =&gt; hook?.tag === &quot;useLayoutEffect&quot; || hook?.tag === &quot;useEffect&quot;) // [!code highlight]\n        .forEach((hook) =&gt; hook.destroy?.());\n}\n\n// 同步执行 layout effects\nfunction commitLayoutEffects(fiber: Fiber | null) {\n    if (!fiber) return;\n\n    // 执行函数组件的 layout effect\n    fiber.hooks\n        ?.filter((hook) =&gt; hook?.tag === &quot;useLayoutEffect&quot;)\n        .forEach((hook) =&gt; {\n            // 执行 cleanup 函数\n            hook.destroy?.();\n            // 执行 effect 函数\n            hook.destroy = hook.create();\n        });\n\n    // 递归处理子节点和兄弟节点\n    commitLayoutEffects(fiber.child);\n    commitLayoutEffects(fiber.sibling);\n}\n\n// 异步执行 effects\nfunction commitMutationEffects(fiber: Fiber | null) {\n    if (!fiber) return;\n\n    // 执行函数组件的 effect\n    fiber.hooks\n        ?.filter((hook) =&gt; hook?.tag === &quot;useEffect&quot;)\n        .forEach((hook) =&gt; {\n            // 执行 cleanup 函数\n            hook.destroy?.();\n            // 执行 effect 函数\n            hook.destroy = hook.create();\n        });\n\n    // 递归处理子节点和兄弟节点\n    commitMutationEffects(fiber.child);\n    commitMutationEffects(fiber.sibling);\n}\n</code></pre>\n<h1>The End</h1>\n<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);border-radius:2px;width:100%;height:100vh\" src=\"https://codesandbox.io/p/sandbox/didact-ts-rqqpqv?embed=1\" allowfullscreen></iframe>\n","tags":["typescript","react"]},{"id":"web-file-system","url":"https://yieldray.fun/posts/web-file-system","title":"web文件系统","date_published":"2023-07-24T20:40:35.000Z","date_modified":"2023-07-24T20:40:35.000Z","content_text":"<blockquote>\n<p>本篇的主题为 <a href=\"https://developer.mozilla.org/docs/Web/API/File_System_Access_API\">File System Access API</a>，而不是 <a href=\"https://developer.mozilla.org/docs/Web/API/File_and_Directory_Entries_AP\">File and Directory Entries API</a></p>\n</blockquote>\n<h1>选取器</h1>\n<p>选取器默认有一个允许所有文件类型的选项，如图所示<br><img src=\"https://s2.loli.net/2023/07/24/fVoexzDnkhcjrEp.png\" alt=\"AcceptAll\"><br>如果已经设置了 types 选项，我们可能需要禁止这种行为，可以将 excludeAcceptAllOption 选项设置为 true</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/window/showOpenFilePicker\">showOpenFilePicker()</a></h2>\n<pre><code class=\"language-ts\">const btn_showOpenFilePicker = document.createElement(&quot;button&quot;);\nbtn_showOpenFilePicker.textContent = &quot;showOpenFilePicker&quot;;\ndocument.body.append(btn_showOpenFilePicker);\nbtn_showOpenFilePicker.addEventListener(&quot;click&quot;, async () =&gt; {\n    const fileHandleArray = await window.showOpenFilePicker({\n        types: [\n            {\n                description: &quot;请选择图片&quot;,\n                accept: {\n                    &quot;image/*&quot;: [&quot;.png&quot;, &quot;.gif&quot;, &quot;.jpeg&quot;, &quot;.jpg&quot;, &quot;.webp&quot;],\n                },\n            },\n        ],\n        multiple: true,\n        excludeAcceptAllOption: true,\n    });\n    console.log(fileHandleArray);\n});\n\ndeclare interface Window {\n    // TODO: 目前 typescript 5.4dev 还没有提供这些 Window 接口方法的定义\n    // 布尔值的默认值为 false\n\n    /**\n     * MDN 上写的是 AbortError 异常，但实际上是 DOMException\n     * 用户取消选择，触发 DOMException: The user aborted a request.\n     * 不是用户发起，触发 DOMException: Failed to execute &#39;showOpenFilePicker&#39; on &#39;Window&#39;: Must be handling a user gesture to show a file picker.\n     */\n    showOpenFilePicker: (options?: {\n        /**\n         * 是否选取多个文件\n         */\n        multiple?: boolean;\n        /**\n         * 若要开启此选项，需配合 types 选项食用，表示禁用选取框的“所有文件选项”\n         */\n        excludeAcceptAllOption?: boolean;\n        types?: Array&lt;{\n            /**\n             * 允许的文件类型类别的可选说明\n             */\n            description?: string;\n            /**\n             * 键为 MIME 类型，系统会根据 MIME 类型默认生成一些扩展名，值为扩展名（带点号）数组，这个数组只是作为系统生成的扩展名基础上的附加扩展名\n             */\n            accept?: Record&lt;string, string[]&gt;;\n        }&gt;;\n    }) =&gt; Promise&lt;FileSystemFileHandle[]&gt;; // 返回值总是数组\n}\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/window/showSaveFilePicker\">showSaveFilePicker()</a></h2>\n<pre><code class=\"language-ts\">const btn_showSaveFilePicker = document.createElement(&quot;button&quot;);\nbtn_showSaveFilePicker.textContent = &quot;showSaveFilePicker&quot;;\ndocument.body.append(btn_showSaveFilePicker);\nbtn_showSaveFilePicker.addEventListener(&quot;click&quot;, async () =&gt; {\n    const fileHandle = await window.showSaveFilePicker({\n        suggestedName: &quot;推荐文件名.txt&quot;,\n        types: [\n            {\n                description: &quot;文本文件&quot;,\n                accept: { &quot;text/plain&quot;: [&quot;.txt&quot;] },\n            },\n        ],\n    });\n    console.log(fileHandle);\n});\n\ndeclare interface Window {\n    /**\n     * 此方法的选项和 showOpenFilePicker 方法差不多，只返回单个文件接口\n     */\n    showSaveFilePicker: (options?: {\n        excludeAcceptAllOption?: boolean;\n        suggestedName?: string;\n        types?: Array&lt;{\n            description?: string;\n            accept?: Record&lt;string, string[]&gt;;\n        }&gt;;\n    }) =&gt; Promise&lt;FileSystemFileHandle&gt;;\n}\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/window/showDirectoryPicker\">showDirectoryPicker()</a></h2>\n<pre><code class=\"language-ts\">const btn_showDirectoryPicker = document.createElement(&quot;button&quot;);\nbtn_showDirectoryPicker.textContent = &quot;showDirectoryPicker&quot;;\ndocument.body.append(btn_showDirectoryPicker);\nbtn_showDirectoryPicker.addEventListener(&quot;click&quot;, async () =&gt; {\n    const dirHandle = await window.showDirectoryPicker({\n        mode: &quot;readwrite&quot;, // 会弹窗窗口提示用户我们的页面能够访问查看或编辑用户的目录\n    });\n    console.log(dirHandle);\n});\n\ndeclare interface Window {\n    /**\n     * 目录选择器就比较简单了，只返回一个目录接口\n     */\n    showDirectoryPicker: (options?: {\n        /**\n         * By specifying an ID, the browser can remember different directories for different IDs. If the same ID is used for another picker, the picker opens in the same directory.\n         * ID 是任意类型，但会被强制转换为字符串，不允许字符会报错\n         */\n        id?: any;\n        /**\n         * A string that defaults to &quot;read&quot; for read-only access or &quot;readwrite&quot; for read and write access to the directory.\n         * 用户选择完目录后还会弹出提示窗\n         */\n        mode?: &quot;read&quot; | &quot;readwrite&quot;;\n        /**\n         * A FileSystemHandle or a well known directory (&quot;desktop&quot;, &quot;documents&quot;, &quot;downloads&quot;, &quot;music&quot;, &quot;pictures&quot;, or &quot;videos&quot;) to open the dialog in.\n         */\n        startIn?: &quot;desktop&quot; | &quot;documents&quot; | &quot;downloads&quot; | &quot;music&quot; | &quot;pictures&quot; | &quot;videos&quot;;\n    }) =&gt; Promise&lt;FileSystemDirectoryHandle&gt;;\n}\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/StorageManager/getDirectory\">navigator.storage.getDirectory()</a></h2>\n<blockquote>\n<p><code>origin private file system</code> : <a href=\"https://developer.mozilla.org/docs/Web/API/File_System_Access_API#origin_private_file_system\">https://developer.mozilla.org/docs/Web/API/File_System_Access_API#origin_private_file_system</a></p>\n</blockquote>\n<pre><code class=\"language-ts\">window.navigator.storage.getDirectory(); // =&gt; Promise&lt;FileSystemDirectoryHandle&gt;\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/DataTransferItem/getAsFileSystemHandle\">dragEvent.dataTransfer.items[0].getAsFileSystemHandle()</a></h2>\n<p>参见 <a href=\"https://developer.mozilla.org/docs/Web/API/DragEvent/dataTransfer\">DragEvent.dataTransfer</a> 的有关 API</p>\n<h1>接口</h1>\n<p>FileSystemHandle &lt;- FileSystemFileHandle<br>FileSystemHandle &lt;- FileSystemDirectoryHandle</p>\n<p>选取器返回的接口是更具体的接口</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/FileSystemHandle\">FileSystemHandle</a></h2>\n<pre><code class=\"language-ts\">interface FileSystemHandle {\n    /**\n     * &#39;file&#39;      表示 FileSystemFileHandle\n     * &#39;directory&#39; 表示 FileSystemDirectoryHandle\n     */\n    readonly kind: FileSystemHandleKind;\n    readonly name: string;\n    /**\n     * 判断当前 handle 和另一个 handle 是不是同一个\n     */\n    isSameEntry(other: FileSystemHandle): Promise&lt;boolean&gt;;\n    /**\n     * 查询权限\n     */\n    queryPermission(fileSystemHandlePermissionDescriptor?: { mode: &quot;read&quot; | &quot;readwrite&quot; }): Promise&lt;PermissionState&gt;;\n    /**\n     * 请求权限\n     */\n    requestPermission(fileSystemHandlePermissionDescriptor?: { mode: &quot;read&quot; | &quot;readwrite&quot; }): Promise&lt;PermissionState&gt;;\n    remove(options?: {\n        /**\n         * 递归删除，只对目录有效\n         */\n        recursive?: boolean;\n    }): Promise&lt;void&gt;;\n}\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle\">FileSystemFileHandle</a></h2>\n<pre><code class=\"language-ts\">interface FileSystemFileHandle extends FileSystemHandle {\n    readonly kind: &quot;file&quot;;\n    /**\n     * 最主要的方法还是 getFile() 方法\n     */\n    getFile(): Promise&lt;File&gt;;\n    /**\n     * 创建一个可写文件流。实际情况是先创建临时文件，写入临时文件，直到句柄结束后再将实际文件替换为临时文件\n     */\n    createWritable(options?: {\n        /**\n         * A Boolean. Default false. When set to true if the file exists, the existing file is first copied to the temporary file. Otherwise the temporary file starts out empty.\n         */\n        keepExistingData?: boolean;\n    }): Promise&lt;FileSystemWritableFileStream&gt;;\n    /**\n     * 只能在 origin private file system 上调用！这里不拓展\n     */\n    createSyncAccessHandle(): Promise&lt;FileSystemSyncAccessHandle&gt;;\n}\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle\">FileSystemDirectoryHandle</a></h2>\n<pre><code class=\"language-ts\">interface FileSystemDirectoryHandle extends FileSystemHandle {\n    readonly kind: &quot;directory&quot;;\n    /**\n     * 遍历目录\n     */\n    entries(): Array&lt;[key: string, value: FileSystemHandle]&gt;;\n    keys(): Array&lt;string&gt;;\n    values(): Array&lt;FileSystemHandle&gt;;\n    // 指定文件名获取文件\n    getFileHandle(\n        name: string,\n        options?: {\n            create?: boolean;\n        },\n    ): Promise&lt;FileSystemFileHandle&gt;;\n    // 指定目录名获取目录\n    getDirectoryHandle(\n        name: string,\n        options?: {\n            create?: boolean;\n        },\n    ): Promise&lt;FileSystemDirectoryHandle&gt;;\n    // 移除\n    removeEntry(\n        name: string,\n        options?: {\n            recursive?: boolean;\n        },\n    ): Promise&lt;void&gt;;\n    /**\n     * 传入一个当前 FileSystemDirectoryHandle 的后代 FileSystemHandle （否则直接返回 null）\n     * 返回一个字符串数组，第一个是当前目录名，依次是子目录名，最后是 要解析的文件或目录 的名称\n     */\n    resolve(possibleDescendant: FileSystemHandle): Promise&lt;string[] | null&gt;;\n}\n</code></pre>\n<h1>polyfill</h1>\n<p>以往保存文件的情景下还是使用 <a href=\"https://github.com/eligrey/FileSaver.js/\">FileSaver.js</a> 即可<br>反正 polyfill 方案是无法选取保存位置的<br>兼容不支持的浏览器，可以使用谷歌的 <a href=\"https://github.com/GoogleChromeLabs/browser-fs-access\">browser-fs-access</a></p>\n","tags":["web-api"]},{"id":"vite-constants","url":"https://yieldray.fun/posts/vite-constants","title":"vite常量配置","date_published":"2023-07-16T21:50:05.000Z","date_modified":"2023-07-16T21:50:05.000Z","content_text":"<h1><a href=\"https://cn.vitejs.dev/guide/env-and-mode.html\">环境变量</a></h1>\n<p>在由 vite 构建的 js 模块中，模块可由 vite 提供的 <code>import.meta.env</code> 伪对象（<em>编译时替换</em>）获取环境变量</p>\n<p>默认情况下，vite 使用 dotenv 读取 <code>.env</code> 文件修改环境变量，并且只有以 <code>VITE_</code> 为前缀的变量才会暴露给经过 vite 处理的代码</p>\n<blockquote>\n<p>注：此处不讨论 如何修改<em>模式</em>，以加载不同的环境变量 （参见文档即可）<br>要在配置文件中使用环境变量，则可以参见<a href=\"https://cn.vitejs.dev/config/#using-environment-variables-in-config\">此处</a></p>\n<p>vite 默认提供的环境变量有： <code>MODE</code> <code>BASE_URL</code> <code>PROD</code> <code>DEV</code> <code>SSR</code></p>\n</blockquote>\n<p>此外，vite 还支持在 html 文件中替换环境变量，例如：</p>\n<pre><code class=\"language-html\">&lt;h1&gt;Vite is running in %MODE%&lt;/h1&gt;\n&lt;p&gt;Using data from %VITE_API_URL%&lt;/p&gt;\n</code></pre>\n<h1><a href=\"https://cn.vitejs.dev/config/shared-options.html#define\">常量替换</a></h1>\n<p>由于 vite 使用 esbuild，常量替换功能是通过 <a href=\"https://esbuild.github.io/api/#define\">esbuild 实现</a></p>\n<blockquote>\n<p>注： rollup 通过 <a href=\"https://github.com/rollup/plugins/tree/master/packages/replace\">@rollup/plugin-replace</a> 实现<br>webpack 则是通过 <a href=\"https://webpack.js.org/plugins/define-plugin/\">DefinePlugin</a> 实现<br>三者的行为差不多</p>\n</blockquote>\n<p>这些常量：在开发环境下会被定义在全局，在构建时被静态替换<br>它是不经过任何语法分析，直接替换文本实现的<br>这也就是说，我们实际上应该配置一个 <code>Record&lt;string, string&gt;</code>，出现给定键就原样替换为给定字符串<br>不过实际配置类型是 <code>Record&lt;string, any&gt;</code>，可以看作是值经过了 <code>String()</code> 处理</p>\n<pre><code class=\"language-ts\">import { defineConfig, loadEnv } from &quot;vite&quot;;\n\nexport default defineConfig(({ command, mode }) =&gt; {\n    // 根据当前工作目录中的 `mode` 加载 .env 文件\n    // 设置第三个参数为 &#39;&#39; 来加载所有环境变量，而不管是否有 `VITE_` 前缀。\n    const env = loadEnv(mode, process.cwd(), &quot;&quot;);\n    return {\n        // vite 配置\n        define: {\n            __APP_ENV__: env.APP_ENV,\n            __TEST__: JSON.stringify(&quot;test&quot;),\n        },\n    };\n});\n</code></pre>\n<p>注意：开发环境下会被定义在 <code>window</code>，在构建时仅文本替换（<strong>不在 <code>window</code> 上！</strong>）</p>\n<h1>插件实现</h1>\n<p><a href=\"https://cn.vitejs.dev/guide/api-plugin.html\">vite 插件</a> 是扩展 <a href=\"https://rollupjs.org/plugin-development/\">rollup 插件</a> 实现的</p>\n<p>插件是 node.js 环境，因此也可以通过插件创建虚拟模块来向其它模块暴露代码</p>\n<pre><code class=\"language-ts\">import { defineConfig } from &quot;vite&quot;;\n\nexport default defineConfig({\n    plugins: [myPlugin()],\n});\n\nfunction myPlugin() {\n    const virtualModuleId = &quot;virtual:my-module&quot;;\n    const resolvedVirtualModuleId = &quot;\\0&quot; + virtualModuleId;\n\n    return {\n        name: &quot;my-plugin&quot;, // 必须的，将会在 warning 和 error 中显示\n        resolveId(id) {\n            if (id === virtualModuleId) {\n                return resolvedVirtualModuleId;\n            }\n        },\n        load(id) {\n            if (id === resolvedVirtualModuleId) {\n                return `export const msg = &quot;from virtual module&quot;`;\n            }\n        },\n    };\n}\n</code></pre>\n","tags":["vite"]},{"id":"css-clip-path","url":"https://yieldray.fun/posts/css-clip-path","title":"css clip-path","date_published":"2023-07-13T11:30:08.000Z","date_modified":"2023-07-13T11:30:08.000Z","content_text":"<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/clip-path\">Syntax</a></h1>\n<pre><code class=\"language-css\">/* Keyword values */\nclip-path: none;\n\n/* &lt;clip-source&gt; values */\nclip-path: url(resources.svg#c1);\n\n/* &lt;geometry-box&gt; values */\nclip-path: margin-box;\nclip-path: border-box;\nclip-path: padding-box;\nclip-path: content-box;\nclip-path: fill-box;\nclip-path: stroke-box;\nclip-path: view-box;\n\n/* &lt;basic-shape&gt; values */\nclip-path: inset(100px 50px);\nclip-path: circle(50px at 0 100px);\nclip-path: ellipse(50px 60px at 0 10% 20%);\nclip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);\nclip-path: path(&quot;M0.5,1 C0.5,1,0,0.7,0,0.3 A0.25,0.25,1,1,1,0.5,0.3 A0.25,0.25,1,1,1,1,0.3 C1,0.7,0.5,1,0.5,1 Z&quot;);\n\n/* Box and shape values combined */\nclip-path: padding-box circle(50px at 0 100px);\n\n/* Global values */\nclip-path: inherit;\nclip-path: initial;\nclip-path: revert;\nclip-path: revert-layer;\nclip-path: unset;\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/basic-shape\"><code>&lt;basic-shape&gt;</code></a></h1>\n<pre><code class=\"language-css\">inset( &lt;shape-arg&gt;{1,4} [round &lt;border-radius&gt;]? )\ncircle( [&lt;shape-radius&gt;]? [at &lt;position&gt;]? )\nellipse( [&lt;shape-radius&gt;{2}]? [at &lt;position&gt;]? )\npolygon( [&lt;fill-rule&gt;,]? [&lt;shape-arg&gt; &lt;shape-arg&gt;]# )\npath( [&lt;fill-rule&gt;,]? &lt;string&gt;)\n\n&lt;shape-arg&gt; = &lt;length&gt; | &lt;percentage&gt;\n&lt;shape-radius&gt; = &lt;length&gt; | &lt;percentage&gt; | closest-side | farthest-side\n</code></pre>\n<h1>Example</h1>\n<p><code>&lt;basic-shape&gt;</code> 是支持 transition 的，但需要为同一种形状</p>\n<iframe style=\"width:100%;height:100vh\" scrolling=\"no\" title=\"clip-path\" src=\"https://codepen.io/YieldRay/embed/oNQEvWK?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/oNQEvWK\">\n  clip-path</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>元素被 clip-path 裁剪的部分不再触发鼠标事件，而是触发被暴露元素的鼠标事件</p>\n","tags":["css"]},{"id":"web-svg","url":"https://yieldray.fun/posts/web-svg","title":"SVG基础","date_published":"2023-07-12T22:22:22.000Z","date_modified":"2023-07-12T22:22:22.000Z","content_text":"<h1>svg</h1>\n<p>svg 文件的 Content-Type 为 image/svg+xml<br>svg 图像的坐标原点是左上角</p>\n<pre><code class=\"language-xml\">&lt;svg\n  xmlns=&quot;http://www.w3.org/2000/svg&quot;\n  viewBox=&quot;0 0 100 100&quot;\n  width=&quot;200&quot;\n  height=&quot;200&quot;&gt;\n&lt;/svg&gt;\n</code></pre>\n<p>svg 是 svg 图像的容器元素。对于 svg 文件，命名空间属性 xmlns 是必须指定的，但在 html 中可省略。</p>\n<p>width 和 height 属性默认值为 auto，此处指定宽高为 200*200。<br>viewBox 属性是空格或逗号分隔的四个数字：min-x, min-y, width and height<br>前两个指定<em>实际展示区域</em>的坐标原点（相对于原 svg 的坐标原点）<br>后两个指定在<em>坐标原点</em>的基础上<em>截取</em>的的宽高度<br>这个被截取的宽高度将放缩到原本 svg 的宽高度上展示</p>\n<h1>基本形状</h1>\n<pre><code class=\"language-xml\">&lt;rect x=&quot;60&quot; y=&quot;10&quot; rx=&quot;10&quot; ry=&quot;10&quot; width=&quot;30&quot; height=&quot;30&quot; /&gt;\nx y 指定矩形左上角的坐标\nrx ry 圆角半径\nwidth height 宽高度\n\n&lt;circle cx=&quot;25&quot; cy=&quot;75&quot; r=&quot;20&quot;/&gt;\ncx cy 圆心坐标\nr 半径\n\n&lt;ellipse cx=&quot;75&quot; cy=&quot;75&quot; rx=&quot;20&quot; ry=&quot;5&quot;/&gt;\ncx cy 圆心坐标\nrx ry 分别为x、y半径\n\n&lt;line x1=&quot;10&quot; x2=&quot;50&quot; y1=&quot;110&quot; y2=&quot;150&quot; stroke=&quot;black&quot; stroke-width=&quot;5&quot;/&gt;\nx1 y1 起点坐标\nx2 y2 终点坐标\n\n&lt;polyline points=&quot;60, 110 65, 120 70, 115 75, 130 80, 125 85, 140 90, 135 95, 150 100, 145&quot;/&gt;\npoints 点集数列，每两个数构成一个坐标。\n注意：分隔符可以是逗号或空格，分隔符可以随意混用！！！\n\n&lt;polygon points=&quot;50, 160 55, 180 70, 180 60, 190 65, 205 50, 195 35, 205 40, 190 30, 180 45, 180&quot;/&gt;\npoints 同上。但注意 polygon 的路径在最后一个点处自动回到第一个点\n\n&lt;path d=&quot;M20,230 Q40,205 50,230 T90,230&quot; fill=&quot;none&quot; stroke=&quot;blue&quot; stroke-width=&quot;5&quot;/&gt;\nd 指定一系列指令。指令用一个字母表示操作，后接数字为参数。\n大写指令指定绝对坐标，小写指令指定相对坐标。\n指令可以如下：\n\nM x y 将画笔移动至指定坐标\nm dx dy 将画笔移动指定距离\n\nL x y 将画笔移动至指定坐标，并画线\nl dx dy 将画笔移动指定距离，并画线\n\nH x 将画笔移动至指定横坐标，并画线\nh dx 将画笔在x方向移动指定距离，并画线\n\nV y 将画笔移动至指定纵坐标，并画线\nv dy 将画笔在y方向移动指定距离，并画线\n\nZ 无参数。将画笔移动到路径的起点，并画线\n\nC x1 y1, x2 y2, x y 画三次贝塞尔曲线，参数分别为起点的控制点、终点的控制点和曲线终点\nc dx1 dy1, dx2 dy2, dx dy\n\nS x2 y2, x y 画三次贝塞尔曲线。若此指令跟在C或S指令后，则它的第一个控制点变为前一个命令曲线的第二个控制点的中心对称点；否则，当前点将作为第一个控制点\ns dx2 dy2, dx dy\n\nQ x1 y1, x y 画二次贝塞尔曲线，参数分别为控制点和终点坐标\nq dx1 dy1, dx dy\n\nT x y 画二次贝塞尔曲线。若此指令跟在Q或T指令后，则它的控制点变为前一个曲线控制点的中心对称点\nt dx dy\n\nA rx ry x-axis-rotation large-arc-flag sweep-flag x y\na rx ry x-axis-rotation large-arc-flag sweep-flag dx dy\n</code></pre>\n<h1>填充和边框</h1>\n<ul>\n<li><code>fill</code> 属性设置对象内部的颜色</li>\n<li><code>stroke</code> 属性设置绘制对象的线条的颜色</li>\n<li><code>fill-opacity</code> 控制填充色的不透明度</li>\n<li><code>stroke-opacity</code> 控制描边的不透明度</li>\n<li><code>opacity</code> 控制透明度</li>\n<li><code>stroke-width</code> 属性定义了描边的宽度</li>\n<li><code>stroke-linecap</code> 控制边框终点的形状<ul>\n<li><code>butt</code> 用直边结束线段，它是常规做法，线段边界 90 度垂直于描边的方向、贯穿它的终点。</li>\n<li><code>square</code> 的效果差不多，但是会稍微超出实际路径的范围，超出的大小由 stroke-width 控制。</li>\n<li><code>round</code> 表示边框的终点是圆角，圆角的半径也是由 stroke-width 控制的。</li>\n</ul>\n</li>\n<li><code>stroke-linejoin</code> 控制两条描边线段之间用什么方式连接<ul>\n<li><code>miter</code> 是默认值，表示用方形画笔在连接处形成尖角</li>\n<li><code>round</code> 表示用圆角连接，实现平滑效果</li>\n<li><code>bevel</code> 在连接处会形成一个斜接。</li>\n</ul>\n</li>\n<li><code>stroke-dasharray</code> 虚线类型应用在描边上。参数是一组（必须）用逗号分隔的数字组成的数列<ul>\n<li>第一个用来表示填色区域的长度</li>\n<li>第二个用来表示非填色区域的长度</li>\n</ul>\n</li>\n<li><code>fill-rule</code> 定义如何给图形重叠的区域上色</li>\n<li><code>stroke-miterlimit</code> 定义什么情况下绘制或不绘制边框连接的 miter 效果</li>\n<li><code>stroke-dashoffset</code> 定义虚线开始的位置。</li>\n</ul>\n<p>如果通过 css 设置样式，将 background 改为 fill，将 border 改为 stroke</p>\n<h1>渐变</h1>\n<p>略。参见：<a href=\"https://developer.mozilla.org/docs/Web/SVG/Tutorial/Gradients\">https://developer.mozilla.org/docs/Web/SVG/Tutorial/Gradients</a></p>\n<h1>图案</h1>\n<p>略。参见：<a href=\"https://developer.mozilla.org/docs/Web/SVG/Tutorial/Patterns\">https://developer.mozilla.org/docs/Web/SVG/Tutorial/Patterns</a></p>\n<h1>文本</h1>\n<p>text 是文本的容器元素。可以直接包含文本节点，也可以包含一些特殊的文本标签<br>text 支持一些 css 属性，例如 font-* text-* letter-* word-*</p>\n<pre><code class=\"language-html\">&lt;svg width=&quot;150&quot; height=&quot;150&quot;&gt;\n    &lt;text x=&quot;0&quot; y=&quot;16&quot;&gt;Hello World!&lt;/text&gt;\n&lt;/svg&gt;\n</code></pre>\n<p>其它略。参见：<a href=\"https://developer.mozilla.org/docs/Web/SVG/Tutorial/Texts\">https://developer.mozilla.org/docs/Web/SVG/Tutorial/Texts</a></p>\n<h1>基本变形</h1>\n<p>首先介绍 <code>&lt;g&gt;</code> 标签，用于分组。在该元素上设置的属性将应用到所有子元素上。</p>\n<pre><code class=\"language-xml\">&lt;svg width=&quot;30&quot; height=&quot;10&quot;&gt;\n  &lt;g fill=&quot;red&quot;&gt;\n    &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;10&quot; /&gt;\n    &lt;rect x=&quot;20&quot; y=&quot;0&quot; width=&quot;10&quot; height=&quot;10&quot; /&gt;\n  &lt;/g&gt;\n&lt;/svg&gt;\n</code></pre>\n<p>svg 的图形元素支持 transform 属性，同 css 的 transform 属性</p>\n<p>与 css 一致，故略。参见：<a href=\"https://developer.mozilla.org/docs/Web/SVG/Tutorial/Basic_Transformations\">https://developer.mozilla.org/docs/Web/SVG/Tutorial/Basic_Transformations</a></p>\n<h1>Reference</h1>\n<ul>\n<li><a href=\"https://developer.mozilla.org/docs/Web/SVG/Element\">SVG element reference</a></li>\n<li><a href=\"https://github.com/HunorMarton/svg-tutorial\">SVG-Tutorial.com</a></li>\n<li><a href=\"https://interactively.info/article/svg-path-commands\">SVG Path Commands</a></li>\n<li><a href=\"https://css-tricks.com/mega-list-svg-information/\">A Compendium of SVG Information</a></li>\n<li><a href=\"https://css-tricks.com/tag/svg/\">CSS-Tricks SVG</a></li>\n<li><a href=\"https://github.com/Ceelog/svg-tutorials\">Ceelog/svg-tutorials</a></li>\n<li><a href=\"https://github.com/HuberTRoy/svgTutorial\">HuberTRoy/svgTutorial</a></li>\n<li><a href=\"https://www.aleksandrhovhannisyan.com/blog/svg-tutorial/\">SVG Tutorial: How to Code SVG Icons by Hand</a></li>\n<li><a href=\"https://webdesign.tutsplus.com/how-to-hand-code-svg--cms-30368t\">How to Hand Code SVG</a></li>\n<li><a href=\"https://www.joshwcomeau.com/svg/friendly-introduction-to-svg/\">A Friendly Introduction to SVG</a></li>\n</ul>\n","tags":["web-api","svg"]},{"id":"ts-type-challenges","url":"https://yieldray.fun/posts/ts-type-challenges","title":"TypeChallenges记录","date_published":"2023-07-07T20:02:06.000Z","date_modified":"2023-07-07T20:02:06.000Z","content_text":"<p><a href=\"https://github.com/type-challenges/type-challenges\">https://github.com/type-challenges/type-challenges</a></p>\n<p>相等性比较：<a href=\"https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650\">https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650</a></p>\n<p><a href=\"https://www.typescriptlang.org/docs/handbook/2/types-from-types.html\">https://www.typescriptlang.org/docs/handbook/2/types-from-types.html</a></p>\n<h1>12 Chainable Options</h1>\n<pre><code class=\"language-ts\">type Chainable&lt;T = {}&gt; = {\n    // 此处用泛型 T 保存欲输出的类型\n    option: &lt;K extends keyof any, V&gt;( // 获取参数 key value 的类型\n        key: Exclude&lt;K, keyof T&gt;, // key 不能为 T 中已存在的键（注意必须在形参key上限制类型，而不能在泛型参数上限制）\n        value: V, // 首先移除已存在的键名 K，再将键 K 设置为类型 V\n    ) =&gt; Chainable&lt;Omit&lt;T, K&gt; &amp; { [P in K]: V }&gt;;\n    get: () =&gt; T;\n};\n</code></pre>\n<h1>20 Promise.all</h1>\n<pre><code class=\"language-ts\">//prettier-ignore\ntype Recursive&lt;T extends readonly any[]&gt; =\n          // 处理空数组情况\n        T[&quot;length&quot;] extends 0 ? []\n        : // 处理元组情况\n        T extends [infer A, ...infer B] ? [Awaited&lt;A&gt;, ...Recursive&lt;B&gt;]\n        : // 处理数组情况\n        T extends Array&lt;infer U&gt; ? Array&lt;Awaited&lt;U&gt;&gt;\n        : // never\n          never;\n\n//prettier-ignore\ndeclare function PromiseAll&lt;T extends any[]&gt;(values: readonly [...T]): Promise&lt;\n  Recursive&lt;T&gt;\n&gt;;\n\n// 把数组当作对象处理，将每个下标的值进行映射\n//prettier-ignore\ndeclare function PromiseAll&lt;T extends any[]&gt;(values: readonly [...T]): Promise&lt;{\n  [K in keyof T]: Awaited&lt;T[K]&gt;\n}&gt;\n</code></pre>\n<h1>296 Permutation</h1>\n<p><a href=\"https://github.com/type-challenges/type-challenges/issues/614\">https://github.com/type-challenges/type-challenges/issues/614</a></p>\n<pre><code class=\"language-ts\">//prettier-ignore\ntype Permutation&lt;T, U = T&gt; = [T] extends [never]\n  ? []\n  : (T extends U\n    ? [T, ...Permutation&lt;Exclude&lt;U, T&gt;&gt;]\n    : never)\n</code></pre>\n<h1>459 Flatten</h1>\n<pre><code class=\"language-ts\">type Flatten&lt;T extends unknown[]&gt; = T extends [infer F, ...infer R]\n    ? F extends unknown[]\n        ? [...Flatten&lt;F&gt;, ...Flatten&lt;R&gt;]\n        : [F, ...Flatten&lt;R&gt;]\n    : [];\n\ntype Flatten&lt;S extends any[], T extends any[] = []&gt; = S extends [infer X, ...infer Y]\n    ? X extends any[]\n        ? Flatten&lt;[...X, ...Y], T&gt;\n        : Flatten&lt;[...Y], [...T, X]&gt;\n    : T;\n</code></pre>\n<p>另外也可以参考 <code>Array.prototype.flat</code> (<code>lib.es2019.array.d.ts</code>) 的 <code>FlatArray</code> 类型</p>\n<h1>1042 IsNever</h1>\n<pre><code class=\"language-ts\">type IsNever&lt;T&gt; = [T] extends [never] ? true : false;\ntype IsNever&lt;T&gt; = Equal&lt;never, T&gt;;\n</code></pre>\n<p><a href=\"https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types\">Distributive Conditional Types</a></p>\n<pre><code class=\"language-ts\">// 根据*联合类型*(unions)在条件(extends)中*分发*(distribute)，以下代码\ntype IsNever&lt;T&gt; = T extends never ? true : false;\n// 将导致结果为 never\ntype WillBeNever = IsNever&lt;never&gt;;\n// 这是因为输入泛型 never 是空集，空集(在extends中)不会发生任何分配，所以只能得到返回类型 never\n</code></pre>\n<p>引申：<code>any</code> 为全集，将直接分配为条件分支的两个结果的并集</p>\n<p>用方括号包裹 <code>extends</code> 两边的类型可防止类型分发</p>\n<blockquote>\n<p>Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.</p>\n</blockquote>\n<pre><code class=\"language-ts\">type P&lt;T&gt; = T extends never ? true : false;\ntype A1 = P&lt;never&gt;; // never\ntype A2 = P&lt;any&gt;; // boolean\n\ntype Q&lt;T&gt; = [T] extends [never] ? true : false;\ntype B1 = Q&lt;never&gt;; // true\ntype B2 = Q&lt;any&gt;; // false\n</code></pre>\n<p>实际上，内置类型就是依据此原理</p>\n<pre><code class=\"language-ts\">/**\n * 排除 T 中能分配(assign)给 U 的类型\n */\ntype Exclude&lt;T, U&gt; = T extends U ? never : T;\n\n/**\n * 提取 T 中能分配(assign)给 U 的类型\n */\ntype Extract&lt;T, U&gt; = T extends U ? T : never;\n</code></pre>\n<h1>1097 IsUnion</h1>\n<p><a href=\"https://github.com/type-challenges/type-challenges/issues/1140\">https://github.com/type-challenges/type-challenges/issues/1140</a></p>\n<pre><code class=\"language-ts\">// 根据：联合类型中的其中一个类型，不能分配给联合类型中的其它类型（因为能分配的类型会收束）\n// 通过两次使用*分发条件类型*，判断是否*不是*联合类型（而是单类型）\ntype DoubleDistribute&lt;T, TRUE, FALSE, C = T&gt; = T extends T ? (C extends T ? TRUE : FALSE) : never;\ntype IsUnion&lt;T&gt; = DoubleDistribute&lt;T, &quot;SingleType&quot;, &quot;UnionType&quot;&gt; extends &quot;SingleType&quot; ? false : true;\n// 注：此处写法只是为了便于理解\n</code></pre>\n<h1>1367 Remove Index Signature</h1>\n<pre><code class=\"language-ts\">//prettier-ignore\ntype RemoveIndexSignature&lt;T extends object&gt; = {\n  // Index Signature 指的就是 keyof any，但*不能是*一个 extends keyof any 的具体值\n  // 因此只需要排除 keyof T 中是 keyof any 的键即可\n    [K in keyof T as \n        string extends K ? never : \n        number extends K ? never : \n        symbol extends K ? never : \n    K]: T[K];\n};\n</code></pre>\n<h1>2257 MinusOne</h1>\n<p>递归法：<a href=\"https://github.com/type-challenges/type-challenges/issues/4377\">https://github.com/type-challenges/type-challenges/issues/4377</a><br>由于 ts 编译器限制递归层数只适用于较小的数</p>\n<pre><code class=\"language-ts\">type Pop&lt;T extends any[]&gt; = T extends [...infer U, any] ? U : never;\n//prettier-ignore\ntype MinusOne&lt;T extends number, A extends any[] = []&gt; = \n    // 这里使用一个额外的泛型参数 A，用于将 T 转换为 length 为 T 的数组\n    A[&#39;length&#39;] extends T ? Pop&lt;A&gt;[&#39;length&#39;] // 已经转换为 length 为 T 的数组，返回 length - 1\n  : MinusOne&lt;T, [...A, symbol]&gt; // 递归地将 A 的 length 加 1\n\n// 同理，只是用 spread 运算符替代了 Pop&lt;T&gt;\ntype MinusOne&lt;T extends number, A extends symbol[] = []&gt; = [...A, symbol][&quot;length&quot;] extends T\n    ? A[&quot;length&quot;]\n    : MinusOne&lt;T, [...A, symbol]&gt;;\n</code></pre>\n<p>另：PlusOne</p>\n<pre><code class=\"language-ts\">type PlusOne&lt;T extends number, A extends unknown[] = []&gt; = A[&quot;length&quot;] extends T\n    ? [...A, symbol][&quot;length&quot;]\n    : PlusOne&lt;T, [...A, symbol]&gt;;\n</code></pre>\n<h1>2757 PartialByKeys</h1>\n<p>本题不难，重点在于需要将交叉(Intersection)类型合并</p>\n<pre><code class=\"language-ts\">type IntersectionToObj&lt;T&gt; = { [K in keyof T]: T[K] };\n\ntype PartialByKeys&lt;T, K = any&gt; = IntersectionToObj&lt;\n    {\n        [P in keyof T as P extends K ? P : never]?: T[P];\n    } &amp; {\n        [P in keyof T as P extends K ? never : P]: T[P];\n    }\n&gt;;\n</code></pre>\n<p>也可以花哨一点，写成这样。这里 <code>Omit&lt;Intersection, never&gt;</code> 起到了合并作用</p>\n<pre><code class=\"language-ts\">//prettier-ignore\ntype PartialByKeys&lt;T, U = keyof T&gt; = Omit&lt;\n    Partial&lt;Pick&lt;T, U &amp; keyof T&gt;&gt; \n    &amp;\n    Omit&lt;T, U &amp; keyof T&gt;\n, never&gt;;\n</code></pre>\n<h1>2946 ObjectEntries</h1>\n<pre><code class=\"language-ts\">type ArrayValues&lt;T extends any[]&gt; = T[number];\ntype ObjectValues&lt;T extends object&gt; = T[keyof T];\n\ntype a = ArrayValues&lt;[1, 2, 3]&gt;; // 2 | 1 | 3\ntype b = ObjectValues&lt;{ 0: 1; 1: 2; 2: 3 }&gt;; // 2 | 1 | 3\n\ntype c = undefined extends never ? true : false; // false\ntype d = { key?: undefined } extends { key?: never } ? true : false; // true\n// { key?: never }\n//      |\n//    这个问号表示键不存在，或值为 undefined 与指定值的类型的联合类型\n//    注意：虽然键可能不存在，但 keyof 运算符还是可以得到键\n//    因此：d 实际相当于 e，而不是 c\ntype e = undefined extends never | undefined ? true : false; // true\n\ntype f = Required&lt;{ key?: undefined }&gt;; // { key: never }\n// type Required&lt;T&gt; = { [P in keyof T]-?: T[P]; }\n//                                     |\n//                                   这个减号把 undefined 类型排除了\n\ntype ObjectEntries&lt;T extends object, U = Required&lt;T&gt;&gt; = ObjectValues&lt;{\n    [K in keyof U]: [K, U[K] extends never ? undefined : U[K]];\n}&gt;;\n</code></pre>\n<h1>3376 InorderTraversal</h1>\n<p>本题不难，问题依旧出在 <a href=\"https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types\">Distributive Conditional Types</a></p>\n<blockquote>\n<p>再次强调：<em>联合类型</em>(unions)在条件(extends)中<em>分发</em>(distribute)</p>\n</blockquote>\n<pre><code class=\"language-ts\">type InorderTraversal&lt;T extends TreeNode | null&gt; = [T] extends [TreeNode]\n    ? [...InorderTraversal&lt;T[&quot;left&quot;]&gt;, T[&quot;val&quot;], ...InorderTraversal&lt;T[&quot;right&quot;]&gt;]\n    : [];\n</code></pre>\n<p>为了将一个联合类型收束到指定类型，须用方括号括起来。<br>还要注意，在本题中不能 <code>[T] extends [null] ? [] : /* 此处 T 还是 TreeNode | null */</code></p>\n<pre><code class=\"language-ts\">// type NonNullable&lt;T&gt; = T &amp; {}\n// 也可以用 NonNullable 规避 extends 中 null 的分发\ntype InorderTraversal&lt;T extends TreeNode | null, Node extends TreeNode = NonNullable&lt;T&gt;&gt; = T extends null\n    ? []\n    : [...InorderTraversal&lt;Node[&quot;left&quot;]&gt;, Node[&quot;val&quot;], ...InorderTraversal&lt;Node[&quot;right&quot;]&gt;];\n</code></pre>\n<h1>8767 Combination</h1>\n<p>联合类型在条件中分发，相当于 map 后再联合</p>\n<pre><code class=\"language-ts\">// Aux 用于递归 union 类型\ntype Aux&lt;U, A = U&gt; = U extends string ? U | `${U} ${Aux&lt;Exclude&lt;A, U&gt;&gt;}` : never;\n\n// 数组转换为 union\ntype Combination&lt;T extends string[]&gt; = Aux&lt;T[number]&gt;;\n</code></pre>\n<h1>8987 Subsequence</h1>\n<pre><code class=\"language-ts\">type Subsequence&lt;T extends any[]&gt; = T extends [infer U, ...infer Rest]\n    ? Subsequence&lt;Rest&gt; | [U, ...Subsequence&lt;Rest&gt;]\n    : [];\n</code></pre>\n<p>注意到 typescript 支持如下语法</p>\n<pre><code class=\"language-ts\">type ts = [1, ...([2] | [3])]; // [1, 2] | [1, 3]\n</code></pre>\n","tags":["typescript"]},{"id":"js-promise","url":"https://yieldray.fun/posts/js-promise","title":"JavaScript异步与并发","date_published":"2023-07-05T16:16:16.000Z","date_modified":"2023-07-05T16:16:16.000Z","content_text":"<h1>Promise</h1>\n<pre><code class=\"language-ts\">/**\n * @source lib.es5.d.ts\n * 注：es5 没有实现 Promise，故只有接口（供 polyfill）\n */\n\ndeclare type PromiseConstructorLike = new &lt;T&gt;(\n    executor: (resolve: (value: T | PromiseLike&lt;T&gt;) =&gt; void, reject: (reason?: any) =&gt; void) =&gt; void,\n) =&gt; PromiseLike&lt;T&gt;;\n\ninterface PromiseLike&lt;T&gt; {\n    then&lt;TResult1 = T, TResult2 = never&gt;(\n        onfulfilled?: ((value: T) =&gt; TResult1 | PromiseLike&lt;TResult1&gt;) | undefined | null,\n        onrejected?: ((reason: any) =&gt; TResult2 | PromiseLike&lt;TResult2&gt;) | undefined | null,\n    ): PromiseLike&lt;TResult1 | TResult2&gt;;\n}\n\n// Promise 的泛型参数 T 只指明了 fulfilled 状态时的默认类型\ninterface Promise&lt;T&gt; {\n    then&lt;TResult1 = T, TResult2 = never&gt;(\n        onfulfilled?: ((value: T) =&gt; TResult1 | PromiseLike&lt;TResult1&gt;) | undefined | null,\n        onrejected?: ((reason: any) =&gt; TResult2 | PromiseLike&lt;TResult2&gt;) | undefined | null,\n    ): Promise&lt;TResult1 | TResult2&gt;;\n    catch&lt;TResult = never&gt;(\n        onrejected?: ((reason: any) =&gt; TResult | PromiseLike&lt;TResult&gt;) | undefined | null,\n    ): Promise&lt;T | TResult&gt;;\n}\n\n/**\n * @source lib.es2015.promise.d.ts\n * 注：es2015 实现了 Promise 构造器\n */\n\ninterface PromiseConstructor {\n    readonly prototype: Promise&lt;any&gt;;\n    new &lt;T&gt;(\n        executor: (resolve: (value: T | PromiseLike&lt;T&gt;) =&gt; void, reject: (reason?: any) =&gt; void) =&gt; void,\n    ): Promise&lt;T&gt;;\n    all&lt;T extends readonly unknown[] | []&gt;(values: T): Promise&lt;{ -readonly [P in keyof T]: Awaited&lt;T[P]&gt; }&gt;;\n    race&lt;T extends readonly unknown[] | []&gt;(values: T): Promise&lt;Awaited&lt;T[number]&gt;&gt;;\n    reject&lt;T = never&gt;(reason?: any): Promise&lt;T&gt;;\n    resolve(): Promise&lt;void&gt;;\n    resolve&lt;T&gt;(value: T): Promise&lt;Awaited&lt;T&gt;&gt;;\n    resolve&lt;T&gt;(value: T | PromiseLike&lt;T&gt;): Promise&lt;Awaited&lt;T&gt;&gt;;\n}\n\ndeclare var Promise: PromiseConstructor;\n\n/**\n * @source lib.es2015.iterable.d.ts\n * 注：规范中 all 和 race 静态方法实际上接受迭代器，类型如下\n */\n\ninterface PromiseConstructor {\n    // 接受一个含 Promise 实例的迭代器，当迭代器中所有 Promise 实例都变为 fulfilled 状态时\n    // 则 resolve 所有 fulfillment 的值组成的数组\n    // 否则，若任意一个 Promise 实例变为 rejected 状态，则 reject 那个 Promise 实例的 rejection 值\n    all&lt;T&gt;(values: Iterable&lt;T | PromiseLike&lt;T&gt;&gt;): Promise&lt;Awaited&lt;T&gt;[]&gt;;\n    // 接受一个含 Promise 实例的迭代器，当迭代器中任意一个 Promise 对象\n    // 变为 fulfilled 或 rejected 状态时（即，不是 pending 状态）\n    // 就 resolve 该 fulfillment 值，或 reject 该 rejection 值\n    race&lt;T&gt;(values: Iterable&lt;T | PromiseLike&lt;T&gt;&gt;): Promise&lt;Awaited&lt;T&gt;&gt;;\n}\n\n/**\n * @source lib.es2018.promise.d.ts\n * 注：原型上添加了 finally 方法\n */\n\ninterface Promise&lt;T&gt; {\n    finally(onfinally?: (() =&gt; void) | undefined | null): Promise&lt;T&gt;;\n}\n\n/**\n * @source lib.es2020.promise.d.ts\n * 注：添加了 allSettled 静态方法\n */\n\ninterface PromiseFulfilledResult&lt;T&gt; {\n    status: &quot;fulfilled&quot;;\n    value: T;\n}\n\ninterface PromiseRejectedResult {\n    status: &quot;rejected&quot;;\n    reason: any;\n}\n\ntype PromiseSettledResult&lt;T&gt; = PromiseFulfilledResult&lt;T&gt; | PromiseRejectedResult;\n\ninterface PromiseConstructor {\n    allSettled&lt;T extends readonly unknown[] | []&gt;(\n        values: T,\n    ): Promise&lt;{ -readonly [P in keyof T]: PromiseSettledResult&lt;Awaited&lt;T[P]&gt;&gt; }&gt;;\n    // 当迭代器中所有 Promise 实例都 settle 时（就是说，不是 pedding 状态）\n    // resolve 一个数组，数组的每一项都是一个对象，对应于迭代器中的 Promise 实例\n    // 若 Promise 实例为 fulfilled 状态，则为 `{ status: &quot;fulfilled&quot;, value: 对应fulfillment值 }`\n    // 若 Promise 实例为 rejected 状态，则为 `{ status: &quot;rejected&quot;, reason: 对应rejection值 }`\n    allSettled&lt;T&gt;(values: Iterable&lt;T | PromiseLike&lt;T&gt;&gt;): Promise&lt;PromiseSettledResult&lt;Awaited&lt;T&gt;&gt;[]&gt;;\n}\n\n/**\n * @source lib.es2021.promise.d.ts\n * 注：添加了 any 静态方法\n */\n\ninterface AggregateError extends Error {\n    errors: any[];\n}\n\ninterface AggregateErrorConstructor {\n    new (errors: Iterable&lt;any&gt;, message?: string): AggregateError;\n    (errors: Iterable&lt;any&gt;, message?: string): AggregateError;\n    readonly prototype: AggregateError;\n}\n\ndeclare var AggregateError: AggregateErrorConstructor;\n\ninterface PromiseConstructor {\n    any&lt;T extends readonly unknown[] | []&gt;(values: T): Promise&lt;Awaited&lt;T[number]&gt;&gt;;\n    // 当迭代器中任意一个 Promise 实例变为 fulfilled 状态时，resolve 那个 fulfillment 值\n    // 否则，reject 一个 AggregateError 错误，该错误的 errors 属性是一个数组\n    // 包含所有 rejection 值（按照迭代器中的 Promise 实例的顺序）\n    any&lt;T&gt;(values: Iterable&lt;T | PromiseLike&lt;T&gt;&gt;): Promise&lt;Awaited&lt;T&gt;&gt;;\n}\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync\"><code>Array.fromAsync()</code></a></h1>\n<p>在不使用生成器的情况下：<br>对于普通迭代器，可以简单地看作类似于 <code>Promise.prototype.all()</code>，但依次（而不是并发，且惰性地）等待每一个 Promise。<br>对于异步迭代器，可以简单地看作类似于 <code>Array.from()</code></p>\n<pre><code class=\"language-ts\">// es2022\n// TODO: 目前 typescript5.4dev 暂未提供类型，且 chrome 暂未实现。\ninterface ArrayConstructor {\n    fromAsync&lt;T&gt;(asyncIterable: AsyncIterable&lt;T&gt;): Promise&lt;Array&lt;T&gt;&gt;;\n    fromAsync&lt;T&gt;(iterableOrArrayLike: Iterable&lt;T&gt; | ArrayLike&lt;T&gt;): Promise&lt;Array&lt;Awaited&lt;T&gt;&gt;&gt;;\n\n    fromAsync&lt;T, U&gt;(\n        asyncIterable: AsyncIterable&lt;T&gt;,\n        mapperFn: (value: T) =&gt; U,\n        thisArg?: any,\n    ): Promise&lt;Array&lt;Awaited&lt;U&gt;&gt;&gt;;\n    fromAsync&lt;T, U&gt;(\n        iterableOrArrayLike: Iterable&lt;T&gt; | ArrayLike&lt;T&gt;,\n        mapperFn: (value: Awaited&lt;T&gt;) =&gt; U,\n        thisArg?: any,\n    ): Promise&lt;Array&lt;Awaited&lt;U&gt;&gt;&gt;;\n}\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers\"><code>Promise.withResolvers()</code></a></h1>\n<p><code>Promise.withResolvers()</code> 添加自 Chrome119。</p>\n<pre><code class=\"language-ts\">interface PromiseConstructor {\n    withResolvers&lt;T&gt;(): {\n        promise: Promise&lt;T&gt;;\n        resolve: (value: T | PromiseLike&lt;T&gt;) =&gt; void;\n        reject: (reason?: any) =&gt; void;\n    };\n}\n</code></pre>\n<p>该方法非常方便，能够减少嵌套</p>\n<pre><code class=\"language-js\">function loadScript(src) {\n    const { promise, resolve, reject } = Promise.withResolvers();\n    const script = document.createElement(&quot;script&quot;);\n    script.src = src;\n    script.onload = resolve;\n    script.onerror = reject;\n    return promise;\n}\n</code></pre>\n","tags":["js"]},{"id":"java-spring-multipartfile","url":"https://yieldray.fun/posts/java-spring-multipartfile","title":"spring之MultipartFile","date_published":"2023-07-04T22:22:22.000Z","date_modified":"2023-07-04T22:22:22.000Z","content_text":"<p>以下在 spring boot 中演示</p>\n<p>配置 <code>application.yml</code>，配置说明参见<a href=\"https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.web.spring.servlet.multipart.enabled\">官方文档</a></p>\n<pre><code class=\"language-yaml\">spring:\n    servlet:\n        multipart:\n            enabled: true # 默认启用\n            max-file-size: 100MB # 默认值很小，调大一点方便上传文件\n            max-request-size: 100MB\n</code></pre>\n<blockquote>\n<p>附：<br>官方教程：<a href=\"https://spring.io/guides/gs/uploading-files/\">https://spring.io/guides/gs/uploading-files/</a><br><code>MultipartFile</code> 文档：<a href=\"https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html\">https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html</a></p>\n</blockquote>\n<p><code>@RequestParam</code> 注解可自动分辨各种请求体类型。将该注解应用在 <code>MultipartFile</code> 上，<br>那么请求体自然只能是 <code>multipart/form-data</code> 类型</p>\n<pre><code class=\"language-java\">@RequestMapping(&quot;/file&quot;)\npublic class FileUploadController {\n\n    @Value(&quot;${application.upload-path}&quot;)\n    private String path;\n\n    @PostMapping(&quot;/upload/{dirName}&quot;)\n    public String uploadFile(@PathVariable(&quot;dirName&quot;) String dirName,\n                             @RequestParam(&quot;file&quot;)    MultipartFile file,\n                             RedirectAttributes       redirectAttributes) throws IOException {\n        String originalFilename = file.getOriginalFilename();\n        // String contentType = file.getContentType();\n        // long size  = file.getSize();\n\n        String targetDir = path + File.separator + dirName;\n        File dir = new File(targetDir);\n        if(!dir.exists()) dir.mkdirs();\n\n        File fileJava = new File(targetDir + File.separator + originalFilename);\n        if(!fileJava.exists())fileJava.createNewFile();\n        file.transferTo(fileJava); // spring 提供的 transferTo 方法很实用\n\n        redirectAttributes.addFlashAttribute(&quot;message&quot;, &quot;successfully uploaded &quot; + originalFilename);\n        return &quot;redirect:/&quot;;\n    }\n}\n</code></pre>\n","tags":["java","spring"]},{"id":"java-spring-security","url":"https://yieldray.fun/posts/java-spring-security","title":"Spring Security","date_published":"2023-07-03T11:30:00.000Z","date_modified":"2023-07-03T11:30:00.000Z","content_text":"<p>依赖：<br><a href=\"https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security\">https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security</a><br><a href=\"https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api\">https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api</a></p>\n<h1>前置</h1>\n<p>使用 <code>MybatisGenerator</code> 生成 <code>UserDao</code> 类和对应 mapper 的 xml 文件<br>注意 <code>UserDao</code> 类需手动添加 <code>@Repository</code> 注解</p>\n<p>编写 <code>UserService</code> 接口如下：</p>\n<pre><code class=\"language-java\">public interface UserService {\n    List&lt;User&gt; selectAll();\n    void add(User user);\n    void update(User user);\n    void delete(String userName);\n    User selectByUserName(String userName);\n}\n</code></pre>\n<p>编写 <code>UserServiceImpl</code> 类如下：</p>\n<pre><code class=\"language-java\">@Service\npublic class UserServiceImpl implements UserService {\n    @Autowired\n    private UserDao userDao;\n    @Override\n    public List&lt;User&gt; selectAll() {return userDao.selectAll();}\n    @Override\n    public void add(User user) {userDao.insert(user);}\n    @Override\n    public void update(User user) {userDao.updateByPrimaryKey(user);}\n    @Override\n    public void delete(String userName) {userDao.deleteByPrimaryKey(userName);}\n    @Override\n    public User selectByUserName(String userName) {return userDao.selectByPrimaryKey(userName);}\n}\n</code></pre>\n<h1>认证</h1>\n<p><a href=\"https://springdoc.cn/spring-security/servlet/authentication/\">https://springdoc.cn/spring-security/servlet/authentication/</a></p>\n<p>Spring Security 提供下列方式实现登录认证。</p>\n<ul>\n<li>登录页面设置方式<ul>\n<li>使用 Spring Security 默认的登录页面及表单项</li>\n<li>自定义登录页面及表单项</li>\n</ul>\n</li>\n<li>用户名和密码设置方式<ul>\n<li>通过 application.properties 配置文件配置</li>\n<li>通过 java 代码配置在内存中</li>\n<li>通过 java 代码从数据库中加载</li>\n</ul>\n</li>\n</ul>\n<p><strong>这里只讨论数据库认证方式，并使用 jwt 鉴权，<code>io.jsonwebtoken</code> 依赖的使用参见<a href=\"https://github.com/jwtk/jjwt\">文档</a></strong></p>\n<p>下面我们创建一个 security 包，以下内容将置于该包内</p>\n<hr>\n<p>创建 <code>service/JwtUserDetailsServiceImpl</code>（实现 spring security 的 <code>UserDetailsService</code> 接口）</p>\n<p><strong>务必注意下面是实现 spring security 的接口，而不是我们自己的</strong></p>\n<pre><code class=\"language-java\">// org.springframework.security.core.userdetails.UserDetails\n// org.springframework.security.core.userdetails.UserDetailsService\n// org.springframework.security.core.userdetails.UsernameNotFoundException\n@Service\npublic class JwtUserDetailsServiceImpl implements UserDetailsService {\n    @Autowired\n    private UserService userService;\n\n    @Override\n    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n        User user = userService.selectUserByUsername(username);\n        if (user == null) throw new UsernameNotFoundException(String.format(&quot;用户 `%s` 不存在&quot;, username));\n        return new JwtUser(user.getUserName(),\n                           user.getNickName(),\n                           user.getPassword(),\n                           user.getAvatar(),\n                           AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole()));\n    }\n}\n</code></pre>\n<p>创建 <code>entity/JwtUser</code> 类如下（实现 spring security 的 <code>UserDetails</code> 接口）</p>\n<pre><code class=\"language-java\">@Data\npublic class JwtUser implements UserDetails {\n    private String username;\n    private String nickname;\n    private String password;\n    private String avatar;\n    private Collection&lt;? extends GrantedAuthority&gt; authorities;\n    public JwtUser(String username, String nickname, String password, String avatar, Collection&lt;? extends GrantedAuthority&gt; authorities) {\n        this.username = username;\n        this.nickname = nickname;\n        this.password = password;\n        this.avatar = avatar;\n        this.authorities = authorities;\n    }\n    public String getAvatar() {return avatar;}\n    public String getNickname() {return nickname;}\n    @Override\n    public String getUsername() {return username;}\n    @Override\n    public boolean isAccountNonExpired() {return true;}\n    @Override\n    public boolean isAccountNonLocked() {return true;}\n    @Override\n    public boolean isCredentialsNonExpired() {return true;}\n    @Override\n    public boolean isEnabled() {return true;}\n    public void setUsername(String username) {this.username = username;}\n    @Override\n    public String getPassword() {return password;}\n    public void setPassword(String password) {this.password = password;}\n    @Override\n    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {return authorities;}\n    public void setAuthorities(Collection&lt;? extends GrantedAuthority&gt; authorities) {this.authorities = authorities;}\n}\n</code></pre>\n<p>创建 <code>entity/AuthenticationBean</code> 类如下</p>\n<pre><code class=\"language-java\">@Getter\n@Setter\npublic class AuthenticationBean {\n    private String username;\n    private String password;\n}\n</code></pre>\n<p>创建 <code>filter/CustomAuthenticationFilter</code> 对象，自定义认证流程</p>\n<pre><code class=\"language-java\">public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {\n    @Override\n    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {\n        // 若请求头不是 json，不处理\n        if (!(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)\n                || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)))\n            return super.attemptAuthentication(request, response);\n\n        // 尝试根据请求构造出 UsernamePasswordAuthenticationToken 对象\n        UsernamePasswordAuthenticationToken authRequest = null;\n\n        try (InputStream is = request.getInputStream()) {\n            // 使用 jackson 解析 json，将请求的 json 读取为我们创建的 AuthenticationBean\n            ObjectMapper mapper = new ObjectMapper();\n            AuthenticationBean ab = mapper.readValue(is, AuthenticationBean.class);\n            authRequest = new UsernamePasswordAuthenticationToken(ab.getUsername(), ab.getPassword());\n        } catch (IOException e) {\n            e.printStackTrace();\n            authRequest = new UsernamePasswordAuthenticationToken(&quot;&quot;, &quot;&quot;);\n        } finally {\n            this.setDetails(request, authRequest);\n            return this.getAuthenticationManager().authenticate(authRequest);\n        }\n    }\n}\n</code></pre>\n<p>创建 <code>filter/JwtAuthenticationTokenFilter</code> 对象，处理 Authorization 请求头</p>\n<pre><code class=\"language-java\">@Component\npublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {\n    @Resource\n    private UserDetailsService userDetailsService;\n\n    @SneakyThrows\n    @Override\n    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {\n        String token = request.getHeader(&quot;Authorization&quot;);\n        if (!StringUtils.isEmpty(token)) {\n            // 这里的 JwtUtils 需要自己实现，这里省略\n            String username = JwtUtils.getUsername(token);\n            if (username != null &amp;&amp; SecurityContextHolder.getContext().getAuthentication() == null) {\n                // 强调：我们创建了 JwtUser 继承 spring security 的 UserDetails\n                UserDetails userDetails = userDetailsService.loadUserByUsername(username);\n                if (JwtUtils.validate(token, userDetails)) {\n                    // 认证成功时，我们保存 UsernamePasswordAuthenticationToken 对象（当然也是 spring security 提供的）\n                    UsernamePasswordAuthenticationToken authentication =\n                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());\n                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));\n                    SecurityContextHolder.getContext().setAuthentication(authentication);\n                }\n            }\n        }\n        chain.doFilter(request, response);\n    }\n}\n</code></pre>\n<p>创建 <code>handler/MyAuthenticationSuccessHandler</code>，处理认证成功</p>\n<pre><code class=\"language-java\">// org.springframework.security.core.userdetails.UserDetails\n@Component\npublic class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {\n    @Override\n    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {\n        UserDetails userDetails = (UserDetails) authentication.getPrincipal();\n        SecurityContextHolder.getContext().setAuthentication(authentication);\n\n        Map&lt;string, string&gt; map = new HashMap&lt;&gt;();\n        map.put(&quot;status&quot;, &quot;ok&quot;);\n        map.put(&quot;token&quot; , generateToken(userDetails));\n\n        httpServletResponse.setContentType(&quot;application/json;charset=UTF-8&quot;);\n        ServletOutputStream out = httpServletResponse.getOutputStream();\n        out.write(new ObjectMapper().writeValueAsBytes(map));\n        out.flush();\n        out.close();\n    }\n\n    // 使用 io.jsonwebtoken 依赖生成 jwt 字符串\n    private String generateToken(UserDetails userDetails) {\n        JwtUser jwtUser = (JwtUser) userDetails;\n        Map&lt;String, Object&gt; claims = new HashMap&lt;&gt;();\n        claims.put(Claims.SUBJECT  , userDetails.getUsername());\n        claims.put(Claims.ISSUED_AT, new Date());\n        claims.put(&quot;username&quot;      , jwtUser.getUsername());\n        claims.put(&quot;role&quot;          , jwtUser.getAuthorities());\n        return Jwts.builder()\n                .setClaims(claims)\n                .setExpiration(new Date(System.currentTimeMillis() + expiration))\n                .signWith(SignatureAlgorithm.HS512, secret)\n                .compact();\n    }\n}\n</code></pre>\n<p>创建 <code>handler/MyAuthenticationFailureHandler</code>，处理认证失败</p>\n<pre><code class=\"language-java\">@Component\npublic class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {\n    @Override\n    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {\n        Map&lt;string, string&gt; map = new HashMap&lt;&gt;();\n        map.put(&quot;status&quot;, &quot;error&quot;);\n        map.put(&quot;reason&quot;, &quot;auth fail&quot;);\n        ServletOutputStream out = httpServletResponse.getOutputStream();\n        out.write(new ObjectMapper().writeValueAsBytes(map));\n        out.flush();\n        out.close();\n    }\n}\n</code></pre>\n<p>创建 <code>handler/EntryPointUnauthorizedHandler</code>，处理未认证</p>\n<pre><code class=\"language-java\">@Component\npublic class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {\n    @Override\n    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {\n        Map&lt;string, string&gt; map = new HashMap&lt;&gt;();\n        map.put(&quot;status&quot;, &quot;error&quot;);\n        map.put(&quot;reason&quot;, &quot;unauthorized&quot;);\n        ServletOutputStream out = httpServletResponse.getOutputStream();\n        out.write(new ObjectMapper().writeValueAsBytes(map));\n        out.flush();\n        out.close();\n    }\n}\n</code></pre>\n<p>创建 <code>handler/RestAccessDeniedHandler</code>，处理无权限情况</p>\n<pre><code class=\"language-java\">@Component\npublic class RestAccessDeniedHandler implements AccessDeniedHandler {\n    @Override\n    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {\n        Map&lt;string, string&gt; map = new HashMap&lt;&gt;();\n        map.put(&quot;status&quot;, &quot;error&quot;);\n        map.put(&quot;reason&quot;, &quot;access denied&quot;);\n        ServletOutputStream out = httpServletResponse.getOutputStream();\n        out.write(new ObjectMapper().writeValueAsBytes(map));\n        out.flush();\n        out.close();\n    }\n}\n</code></pre>\n<p>创建 <code>config/WebSecurityConfig</code>，对 spring security 进行配置</p>\n<pre><code class=\"language-java\">@EnableWebSecurity\n@EnableGlobalMethodSecurity(prePostEnabled = true)\n@Configuration\npublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {\n    @Resource\n    private UserDetailsService userDetailsService;\n    @Autowired\n    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;\n    @Autowired\n    private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;\n    @Autowired\n    private RestAccessDeniedHandler restAccessDeniedHandler;\n    @Autowired\n    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;\n    @Autowired\n    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;\n\n    @Autowired\n    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {\n        authenticationManagerBuilder.userDetailsService(userDetailsService)\n                .passwordEncoder(new BCryptPasswordEncoder());\n    }\n\n    @Override\n    protected void configure(HttpSecurity http) throws Exception {\n        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)\n                .addFilterBefore(new CorsFilter(), ChannelProcessingFilter.class);// 跨域 Filter，此处省略实现\n\n        http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)\n                .and().authorizeRequests()\n                .antMatchers(HttpMethod.OPTIONS, &quot;/**&quot;).permitAll()\n                .antMatchers(&quot;/static/**&quot;).permitAll()\n                .anyRequest().authenticated() // 任何请求，登录后可访问\n                .and().addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)\n                .formLogin().loginProcessingUrl(&quot;/user/login&quot;)\n                .successHandler(myAuthenticationSuccessHandler)\n                .failureHandler(myAuthenticationFailureHandler)\n                .and().logout()\n                .and().headers().cacheControl();\n\n        http.exceptionHandling()\n                .authenticationEntryPoint(entryPointUnauthorizedHandler)\n                .accessDeniedHandler(restAccessDeniedHandler);\n    }\n\n    @Bean\n    CustomAuthenticationFilter customAuthenticationFilter() throws Exception {\n        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();\n        filter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);\n        filter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);\n        filter.setFilterProcessesUrl(&quot;/user/login&quot;);\n        filter.setAuthenticationManager(authenticationManagerBean());\n        return filter;\n    }\n}\n</code></pre>\n<h1>授权</h1>\n<p><a href=\"https://springdoc.cn/spring-security/servlet/authorization/\">https://springdoc.cn/spring-security/servlet/authorization/</a></p>\n","tags":["java","spring"]},{"id":"curl-cli","url":"https://yieldray.fun/posts/curl-cli","title":"curl命令行","date_published":"2023-07-01T16:41:48.000Z","date_modified":"2023-07-01T16:41:48.000Z","content_text":"<p>文档：<a href=\"https://curl.se/docs/manpage.html\">https://curl.se/docs/manpage.html</a><br>参考：<a href=\"https://curl.dev/\">https://curl.dev/</a></p>\n<p>以下仅使用 http 协议</p>\n<pre><code class=\"language-sh\">Usage: curl [options...] &lt;url&gt;\n</code></pre>\n<blockquote>\n<p>一般来说，filename 参数指定为 - 字符则从 stdin 读取<br>data 参数若以 @ 开头，视为二进制文件；以 &lt; 开头视为文本文件。<br>剩余部分为文件名，可通过双引号包含（文件名含特殊字符时）</p>\n</blockquote>\n<h1>请求选项</h1>\n<pre><code>--url &lt;url&gt;\n# 指定目标 URL。一般不需要这个选项\n\n-X, --request &lt;method&gt;\n# 指定请求方法，如 GET/POST\n\n-I, --head\n# 发送 HEAD 请求，并输出响应头\n\n-G, --get\n# 将 -d 选项的行为改变为 添加 查询字符串 而不是 请求体\n# 例如 -G -d q=a\n\n--url-query &lt;data&gt;\n# 添加查询字符串，不改变请求方法\n# 格式为  key=val  =key  key@file_as_val  @file_as_key  +unencoded\n\n-L, --location\n# 跟随服务器重定向\n\n--location-trusted\n# 跟随服务器重定向，并且跟随发送 -u 选项发送的账号密码\n\n-u, --user &lt;user:password&gt;\n# 指定账号密码\n\n-n, --netrc\n# 使用 ~/.netrc (_netrc on Windows) 来指定账号密码\n\n-H, --header &lt;header/@file&gt;\n# 添加 http 请求头，如 -H &#39;Content-Type: application/json&#39; （可多次使用）\n\n-k, --insecure\n# 跳过 ssl 检测\n\n-d, --data &lt;data&gt;\n# 默认为 POST 请求添加请求体，如 -d a=1 （多次使用时，直接将数据连接）\n# 自动添加 &quot;Content-Type: application/x-www-form-urlencoded&quot; 请求头\n\n--data-urlencode &lt;data&gt;\n# -d 选项不编码，这个选项会编码。如果有多个字段，应多次使用此选项，不能 --data-urlencode a=1&amp;b=2\n# 而应该 --data-urlencode a=1 --data-urlencode b=2\n\n--data-raw &lt;data&gt;\n# POST 请求直接发送二进制数据\n\n--json &lt;data&gt;\n# POST 请求发送 json\n# 自动添加 &quot;Content-Type: application/json&quot; 和 &quot;Accept: application/json&quot; 请求头\n\n-F, --form &lt;name=content&gt;\n# POST 请求发送文件，例如 -F &quot;story=&lt;file.txt&quot; -F &quot;image=@file.jpg&quot; -F &quot;web=@index.html;type=text/html&quot;\n# 自动添加 Content-Type: multipart/form-data 请求头\n</code></pre>\n<h1>Header 辅助选项</h1>\n<pre><code>-A, --user-agent &lt;name&gt;\n# 指定 User-Agent，若为空则不发送 User-Agent 请求头\n\n-e, --referer &lt;URL&gt;\n# 指定 Referer\n\n-b, --cookie &lt;data|filename&gt;\n# 若格式为 &quot;NAME1=VALUE1; NAME2=VALUE2&quot; ，则指定 Cookie 请求头为此字符串\n# 否则字符串视为文件路径，读取文件再设置 Cookie 头。配合 -c 选项使用\n\n-c, --cookie-jar &lt;filename&gt;\n指定将服务器返回的 Set-Cookie 响应头写入文件\n</code></pre>\n<h1>客户端选项</h1>\n<pre><code>-i\n# 输出响应头\n\n-K, --config &lt;file&gt;\n# 手动指定配置文件\n\n--limit-rate &lt;speed&gt;\n# 限制每秒发送的比特数，可使用 k,m,g,t,p 单位。1024 based\n\n-o, --output &lt;file&gt;\n# 输出到文件而不是 stdout\n\n-O, --remote-name\n# 输出到和远程文件名的同名文件\n\n-J, --remote-header-name\n# 令 -O 选项从 Content-Disposition  响应头获取文件名而不是从 url 中获取\n\n--remove-on-error\n# 请求失败时将写入的文件移除\n\n-f, --fail\nhttp 响应表明为错误时（例如：状态码不是 2xx 3xx）尽快使命令错误退出，\n并且不要输出（例如：404 响应时还会有响应体）\n\n-s, --silent\n# 安静模式，不显示进度条和错误信息\n\n-S, --show-error\n# 令 -s 选项，显示错误信息\n\n-v, --verbose\n# 详细模式\n\n--socks5 &lt;host[:port]&gt;\n-x, --proxy [protocol://]host[:port]\n# 指定代理\n</code></pre>\n","tags":["cli"]},{"id":"java-spring-mybatis-util","url":"https://yieldray.fun/posts/java-spring-mybatis-util","title":"spring中使用mybatis","date_published":"2023-06-30T16:55:55.000Z","date_modified":"2023-06-30T16:55:55.000Z","content_text":"<p>mybatis 可以使我们方便地创建 mapper 层</p>\n<h1>实例</h1>\n<p>下面使用 mysql 作为数据源，并在 spring boot 项目下演示。</p>\n<p>mysql 的 jdbc url 参见：<br><a href=\"https://dev.mysql.com/doc/connectors/en/connector-j-reference-jdbc-url-format.html\">https://dev.mysql.com/doc/connectors/en/connector-j-reference-jdbc-url-format.html</a><br><a href=\"https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html\">https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html</a><br>添加 mysql 驱动 maven 依赖：<br><a href=\"https://mvnrepository.com/artifact/com.mysql/mysql-connector-j\">https://mvnrepository.com/artifact/com.mysql/mysql-connector-j</a><br>spring boot 项目添加 mybatis 依赖：<br><a href=\"https://mvnrepository.com/artifact/tk.mybatis/mapper-spring-boot-starter\">https://mvnrepository.com/artifact/tk.mybatis/mapper-spring-boot-starter</a><br>为了方便生成 getter 和 setter 安装 lombok 依赖：<br><a href=\"https://mvnrepository.com/artifact/org.projectlombok/lombok\">https://mvnrepository.com/artifact/org.projectlombok/lombok</a><br>idea 使用 Free MyBatis Tool 插件自动生成 mybatis 的 mapper 的 xml 配置文件：<br>（注：可以将 model 创建在 entity 包下，将 dao 放在 dao 包下，将 mapper xml 放在 resources 的 mapper 包下）<br><a href=\"https://plugins.jetbrains.com/plugin/18617-free-mybatis-tool\">https://plugins.jetbrains.com/plugin/18617-free-mybatis-tool</a></p>\n<p><strong>（自动生成的文件就不演示了）</strong></p>\n<p><code>application.yml</code> 的内容例如：</p>\n<pre><code class=\"language-yaml\">spring:\n    datasource:\n        driver-class-name: com.mysql.cj.jdbc.Driver\n        url: jdbc:mysql://[host][:port]/[db]?serverTimezone=GMT%2B8&amp;useUnicode=true&amp;characterEncodeing=UTF-8&amp;useSSL=false\n        username: root\n        password: 123456\nmybatis:\n    mapper-locations: classpath:mapper/*.xml\n    configuration:\n        map-underscore-to-camel-case: true\n</code></pre>\n<p>我们创建（自动生成）的 mapper (即 DAO) 类置于 dao 包下，注意需要使用 <code>@Repository</code> 注解，<br>这样 mapper 类就可以被 spring 自动装配。<br>此时 srping boot 的启动类如下</p>\n<pre><code class=\"language-java\">@MapperScan(&quot;com.example.demo.dao&quot;)\n@SpringBootApplication\npublic class DemoApplication {/* .. */}\n</code></pre>\n<blockquote>\n<p>注：此处 DAO 和 Service 是接口，mapper 和 ServiceImpl 是实现类<br>model 层存放 entity，service 层存放业务逻辑（但不直接访问数据库）并被 controller 层直接调用<br>mapper 和 DAO 直接对数据库操作并被 service 层直接调用</p>\n</blockquote>\n<p>编写 service 实现类如下</p>\n<pre><code class=\"language-java\">@Service\npublic class OrderServiceImpl implements OrderService {\n\n    // 这个 DAO 是由 Free MyBatis Tool 插件生动生成的，此处直接注入即可\n    @Autowired\n    private OrderDao orderDao;\n\n    @Override\n    public void add(Order order) {orderDao.insert(order);}\n\n    @Override\n    public void update(Order order) {orderDao.updateByPrimaryKey(order);}\n\n    @Override\n    public void delete(Integer id) {orderDao.deleteByPrimaryKey(id);}\n\n    @Override\n    public Order selectById(Integer id) {return orderDao.selectByPrimaryKey(id);}\n}\n</code></pre>\n<p>在 controller 包内编写一个类来测试一下</p>\n<pre><code class=\"language-java\">@RestController\npublic class OrderController {\n    @Autowired\n    private OrderService orderService;\n\n    @GetMapping(&quot;/selectById/{id}&quot;)\n    public Result&lt;Order&gt; selectById(@PathVariable(&quot;id&quot;) Integer id) {\n        Order order = orderService.selectById(id);\n        return new Result(true, 200, &quot;ok&quot;, order);\n    }\n}\n\n// @Data 注解由 lombok 提供\n// Result 工具类，可放在 common 包内\n@Data\nclass Result&lt;T&gt; implements Serializable {\n    private boolean success;\n    private Integer code;\n    private String message;\n    private T data;\n    public Result(boolean success, Integer code, String message, T data) {\n        this.success = success;\n        this.code = code;\n        this.message = message;\n        this.data = data;\n    }\n}\n</code></pre>\n<h1>配置 (Servlet) Filter</h1>\n<p><code>FilterRegistrationBean</code> 类由 spring 提供<br>配置类声明 <code>@Configuration</code> 注解就能被 spring 扫描和配置</p>\n<pre><code class=\"language-java\">@Configuration\npublic class FilterConfig {\n    @Bean\n    public FilterRegistrationBean&lt;Filter&gt; Filter01() {\n        FilterRegistrationBean filter = new FilterRegistrationBean();\n        filter.setFilter(new LogFilter1());\n        List urls = new ArrayList();\n        urls.add(&quot;/*&quot;);\n        filter.setUrlPatterns(urls);\n        filter.setOrder(1);\n        return filter;\n    }\n\n    @Bean\n    public FilterRegistrationBean Filter02() {\n        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();\n        filterRegistrationBean.setFilter(new LogFilter2());\n        filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);\n        filterRegistrationBean.setOrder(2);\n        return filterRegistrationBean;\n    }\n\n}\n\n\nclass LogFilter1 implements Filter {\n    final Logger logger = LoggerFactory.getLogger(getClass());\n\n    @Override\n    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n        logger.info(&quot;LogFilter1 before&quot;);\n        filterChain.doFilter(servletRequest, servletResponse);\n        logger.info(&quot;LogFilter1 after&quot;);\n    }\n}\n\nclass LogFilter2 implements Filter {\n    final Logger logger = LoggerFactory.getLogger(getClass());\n\n    @Override\n    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n        logger.info(&quot;LogFilter2 before&quot;);\n        filterChain.doFilter(servletRequest, servletResponse);\n        logger.info(&quot;LogFilter2 after&quot;);\n    }\n}\n\nclass CorsFilter implements Filter {\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        HttpServletResponse res = (HttpServletResponse) response;\n        res.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);\n\n        HttpServletRequest req = (HttpServletRequest) request;\n        if (req.getMethod().equals(&quot;OPTIONS&quot;)){\n            res.setStatus(HttpServletResponse.SC_OK);\n            return;\n        }\n        chain.doFilter(request, res);\n    }\n}\n</code></pre>\n<h1>配置 (Spring) Interceptor</h1>\n<p>同上，<code>@Configuration</code> 注解表示通过 spring 扫描和配置</p>\n<pre><code class=\"language-java\">@Configuration\npublic class MyInterceptorConfig implements WebMvcConfigurer {\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        registry.addInterceptor(new MyInterceptor())\n                .addPathPatterns(&quot;/**&quot;)\n                .excludePathPatterns(&quot;/static/**&quot;);\n    }\n}\n\n\nclass MyInterceptor implements HandlerInterceptor {\n    final Logger logger = LoggerFactory.getLogger(getClass());\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        HandlerMethod handlerMethod = (HandlerMethod) handler;\n        Method method = handlerMethod.getMethod();\n        String methodName = method.getName();\n        logger.debug(&quot;Method = &quot; + methodName);\n        return true; // 返回值表示是否放行\n    }\n\n    @Override\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\n        logger.info(&quot;postHandle&quot;);\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n        logger.info(&quot;afterCompletion&quot;);\n    }\n}\n</code></pre>\n<p>使用了上面的过滤器和拦截器后，访问 servlet 的日志效果如下</p>\n<pre><code>&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.example.demo.LogFilter1              : LogFilter1 before\n&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.example.demo.LogFilter2              : LogFilter2 before\n&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...\n&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.\n&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.example.demo.MyInterceptor           : postHandle\n&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.example.demo.MyInterceptor           : afterCompletion\n&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.example.demo.LogFilter2              : LogFilter2 after\n&lt;time&gt;  INFO &lt;pid&gt; --- [nio-8080-exec-1] com.example.demo.LogFilter1              : LogFilter1 after\n</code></pre>\n<p>可以看到，是先进入 Filter 再进入 Interceptor</p>\n<h1>分页</h1>\n<p>使用<a href=\"https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter\">pagehelper</a>进行分页</p>\n<p>在 <code>mapper/OrderDao.xml</code> <code>&lt;mapper/&gt;</code> 结束标签之前添加如下</p>\n<pre><code class=\"language-xml\">&lt;select id=&quot;selectAllGoods&quot; resultType=&quot;com.example.demo.entity.Order&quot;&gt;\n    select\n    &lt;include refid=&quot;Base_Column_List&quot; /&gt;\n    from tb_order\n    &lt;where&gt;\n        &lt;if test=&quot;type!=null&quot;&gt;\n            and type=#{type}\n        &lt;/if&gt;\n    &lt;/where&gt;\n&lt;/select&gt;\n</code></pre>\n<p>OrderDao 接口中添加方法</p>\n<pre><code class=\"language-java\">List&lt;Order&gt; selectAllGoods(Order order);\n</code></pre>\n<p>OrderService 接口中添加方法</p>\n<pre><code class=\"language-java\">PageInfo&lt;Order&gt; selectAllGoods(Integer pageNum);\n</code></pre>\n<p>OrderServiceImpl 类中实现该方法</p>\n<pre><code class=\"language-java\">\n@Override\npublic PageInfo&lt;Order&gt; selectAllGoods(Integer pageNum) {\n    Order order = new Order();\n    order.setType(&quot;goods&quot;);\n\n    Integer pageSize = 10;\n    PageHelper.startPage(pageNum, pageSize);\n    List&lt;Order&gt; goods = orderDao.selectAllGoods(order);\n    return new PageInfo&lt;&gt;(goods);\n}\n</code></pre>\n<p>OrderController 类中添加调用该 service 层的方法</p>\n<pre><code class=\"language-java\">@GetMapping(&quot;/goods/{pageNum}&quot;)\npublic Result&lt;PageInfo&gt; selectAllGoods(@PathVariable(&quot;pageNum&quot;) Integer pageNum) {\n    PageInfo&lt;Order&gt; orders = orderService.selectAllGoods(pageNum);\n    return new Result&lt;PageInfo&gt;(true, 200, &quot;ok&quot;, orders);\n}\n</code></pre>\n<h1>参数校验</h1>\n<p>spring boot 可直接使用 <a href=\"https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation\">spring-boot-starter-validation</a>，实际上是依赖了 <a href=\"https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator\">hibernate-validator</a>。（具体使用方法可自行搜索，此处不展示）\n修改 Order 实体如下</p>\n<pre><code class=\"language-java\">@Data\npublic class Order implements Serializable {\n    /* ... ... */\n\n    @NotBlank(message = &quot;标题不能为空&quot;)\n    @Length(min = 2, max = 8, message = &quot;标题长度在2-8之间&quot;)\n    private String title;\n\n    @NotBlank(message = &quot;内容不能为空&quot;)\n    private String content;\n\n    /* ... ... */\n}\n</code></pre>\n<p>在 controller 中 可通过 <code>BindingResult</code> 参数获得校验结果</p>\n<pre><code class=\"language-java\">@PostMapping(&quot;/goods/add&quot;)\npublic Result&lt;String&gt; add(@Valid @RequestBody Order order, BindingResult bindingResult) {\n    // 处理校验失败\n    if (bindingResult.hasErrors()) {\n        StringBuffer stringBuffer = new StringBuffer();\n        List&lt;ObjectError&gt; allErrors = bindingResult.getAllErrors();\n        for (ObjectError objectError : allErrors) {\n            stringBuffer.append(objectError.getDefaultMessage()).append(&quot;; &quot;);\n        }\n        return new Result&lt;&gt;(false, 400, &quot;error&quot;, stringBuffer.toString());\n    }\n\n    // 处理校验成功，添加到数据库\n    order.setCreateTime(new Date());\n    order.setUpdateTime(new Date());\n    order.setOrderStatus(0);\n    order.setType(&quot;1&quot;);\n    order.setOwnName(&quot;name&quot;);\n    orderService.add(order);\n    return new Result(true, 200, &quot;ok&quot;, null);\n}\n</code></pre>\n<pre><code class=\"language-sh\"># 测试一下\ncurl -H &quot;Content-Type: application/json&quot; \\\n     -d &#39;{&quot;title&quot;:&quot;title&quot;, &quot;content&quot;:&quot;content&quot;}&#39; \\\n     http://127.0.0.1:8080/goods/add\n</code></pre>\n","tags":["java","spring"]},{"id":"db-sqlite-cheatsheet","url":"https://yieldray.fun/posts/db-sqlite-cheatsheet","title":"sqlite简单速查","date_published":"2023-06-28T21:59:58.000Z","date_modified":"2023-06-28T21:59:58.000Z","content_text":"<p>文档：<a href=\"https://www.sqlite.org/docs.html\">https://www.sqlite.org/docs.html</a></p>\n<p>语法参见：<a href=\"https://www.sqlite.org/syntaxdiagrams.html\">https://www.sqlite.org/syntaxdiagrams.html</a></p>\n<p>数据类型参见：</p>\n<ul>\n<li><a href=\"https://www.sqlite.org/datatype3.html\">https://www.sqlite.org/datatype3.html</a></li>\n<li><a href=\"https://www.sqlite.org/datatypes.html\">https://www.sqlite.org/datatypes.html</a></li>\n</ul>\n<p>函数参见：<a href=\"https://www.sqlite.org/lang_corefunc.html\">https://www.sqlite.org/lang_corefunc.html</a></p>\n<p>SQLite 支持：<a href=\"https://www.sqlite.org/fullsql.html\">https://www.sqlite.org/fullsql.html</a></p>\n<p>SQLite 不支持：<a href=\"https://www.sqlite.org/omitted.html\">https://www.sqlite.org/omitted.html</a></p>\n<h1>SQLite3 Cheat Sheet</h1>\n<blockquote>\n<p>from <a href=\"https://opensource.com/sites/default/files/gated-content/cheat_sheet_sqlite_0.pdf\">https://opensource.com/sites/default/files/gated-content/cheat_sheet_sqlite_0.pdf</a></p>\n</blockquote>\n<p>SQLite is a public domain C-language library implementing a small, fast, self-contained,\nreliabile, and full-featured, SQL database engine.</p>\n<h2>Manipulating data</h2>\n<h6>Create database</h6>\n<pre><code class=\"language-sql\">&gt; .open filename.db;\n</code></pre>\n<h6>Create table and define fields</h6>\n<pre><code class=\"language-sql\">&gt; CREATE TABLE IF NOT EXISTS mytable (foo TEXT NOT NULL);\n</code></pre>\n<h6>View tables in database</h6>\n<pre><code class=\"language-sql\">&gt; .tables\n</code></pre>\n<h6>Insert data into a table</h6>\n<pre><code class=\"language-sql\">&gt; INSERT INTO mytable (foo) VALUES (&#39;aaa&#39;), (&#39;bbb&#39;), (&#39;ccc&#39;);\n</code></pre>\n<h6>View table schema</h6>\n<pre><code class=\"language-sql\">&gt; .schema mytable\n</code></pre>\n<h6>Add a new column to mytable</h6>\n<pre><code class=\"language-sql\">&gt; ALTER TABLE mytable ADD bar INTEGER;\n</code></pre>\n<h6>Update data in a table</h6>\n<pre><code class=\"language-sql\">&gt; UPDATE mytable SET bar=123 WHERE foo=&#39;aaa&#39;;\n</code></pre>\n<h2>Joins</h2>\n<h6>Display an inner join</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM mytable\n→ INNER JOIN othertable\n→ ON mytable.rowid=othertable.foo;\n</code></pre>\n<h6>Display a left join</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM mytable LEFT JOIN\n→ ON mytable.id=othertable.foo;\n</code></pre>\n<h6>Display a cross join</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM mytable\n→ CROSS JOIN othertable;\n</code></pre>\n<h2>Data types &amp; Some SQLite functions</h2>\n<table>\n<thead>\n<tr>\n<th>Data types</th>\n<th></th>\n<th>Some SQLite functions</th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td>TEXT</td>\n<td>Text data</td>\n<td>abs()</td>\n<td>Absolute value</td>\n</tr>\n<tr>\n<td>INTEGER</td>\n<td>Whole number</td>\n<td>max() min()</td>\n<td>Maximum and minimum values</td>\n</tr>\n<tr>\n<td>REAL</td>\n<td>Fleating point number</td>\n<td>upper() lower()</td>\n<td>Convert case of string</td>\n</tr>\n<tr>\n<td>BLOB</td>\n<td>Binary data</td>\n<td>length()</td>\n<td>Length of string</td>\n</tr>\n<tr>\n<td>NULL</td>\n<td>Null value</td>\n<td>random()</td>\n<td>(Pseudo) random integer</td>\n</tr>\n</tbody></table>\n<h2>Select</h2>\n<h6>Display all data</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM mytable;\n</code></pre>\n<h6>Display data of the third row</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM mytable\n→ WHERE rowid=3;\n</code></pre>\n<h6>Display foo and bar columns</h6>\n<pre><code class=\"language-sql\">&gt; SELECT foo, bar FROM mytable;\n</code></pre>\n<h6>Display first 10 results</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM mytable LIMIT 10;\n</code></pre>\n<h6>Sort by column foo</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM mytable ORDER BY foo;\n</code></pre>\n<h2>Views</h2>\n<h6>Create a new view</h6>\n<pre><code class=\"language-sql\">&gt; CREATE VIEW myview AS\n→ SELECT foo FROM mytable\n→ WHERE example &gt; 10;\n</code></pre>\n<h6>Show existing views</h6>\n<pre><code class=\"language-sql\">&gt; .tables\n</code></pre>\n<h6>Display data with a view</h6>\n<pre><code class=\"language-sql\">&gt; SELECT * FROM myview;\n</code></pre>\n<h6>Delete (drop) a view</h6>\n<pre><code class=\"language-sql\">&gt; DROP VIEW myview;\n</code></pre>\n<h2>Column constraints</h2>\n<h6>Set default text for a field</h6>\n<pre><code class=\"language-sql\">DEFAULT &#39;default text&#39;\n</code></pre>\n<h6>Enforce unique value</h6>\n<pre><code class=\"language-sql\">UNIQUE\n</code></pre>\n<h6>Designate a column as a unique identifier</h6>\n<pre><code class=\"language-sql\">PRIMARY KEY\n</code></pre>\n<pre><code class=\"language-sql\">&gt; CREATE TABLE mytable (Id INTEGER PRIMARY KEY);\n</code></pre>\n<h6>Pointer to a primary key of a different table</h6>\n<pre><code class=\"language-sql\">FOREIGN KEY\n</code></pre>\n<h6>Impose a condition for validation</h6>\n<pre><code class=\"language-sql\">CHECK\n</code></pre>\n<pre><code class=\"language-sql\">&gt; CREATE TABLE mytable (CHECK(condition&gt; 0), bar TEXT);\n</code></pre>\n<h6>Prevent NULL values</h6>\n<pre><code class=\"language-sql\">NOT NULL\n</code></pre>\n<h1>SQLite Cheat Sheet</h1>\n<blockquote>\n<p>from <a href=\"https://raw.githubusercontent.com/Mehedi61/SQLite-Cheat-Sheet/master/README.md\">https://raw.githubusercontent.com/Mehedi61/SQLite-Cheat-Sheet/master/README.md</a></p>\n</blockquote>\n<h2>Creating Database:</h2>\n<pre><code class=\"language-sql\">sqlite3 database_name.db\n</code></pre>\n<h2>Managing databases</h2>\n<h6>Attach another database to the current database connection:</h6>\n<pre><code class=\"language-sql\">ATTACH DATABASE file_name AS database_name;\n</code></pre>\n<h6>List names and files of attached databases:</h6>\n<pre><code class=\"language-sql\">.databases\n</code></pre>\n<h6>Optimize the database:</h6>\n<pre><code class=\"language-sql\">VACCUM\n</code></pre>\n<h2>Managing Tables</h2>\n<h6>Create a new table:</h6>\n<pre><code class=\"language-sql\">CREATE TABLE [IF NOT EXISTS] table(\n   primary_key INTEGER PRIMARY KEY,\n   column_name type NOT NULL,\n   column_name type NULL,\n   ...\n);\n</code></pre>\n<h6>Rename a table:</h6>\n<pre><code class=\"language-sql\">ALTER TABLE table_name RENAME TO new_name;\n</code></pre>\n<h6>Add a new column to a table:</h6>\n<pre><code class=\"language-sql\">ALTER TABLE table ADD COLUMN column_definition;\n</code></pre>\n<h6>Drop an existing column in a table:</h6>\n<pre><code class=\"language-sql\">ALTER TABLE table DROP COLUMN column_name;\n</code></pre>\n<h6>Drop a table and its data:</h6>\n<pre><code class=\"language-sql\">DROP TABLE [IF EXISTS] table_name;\n</code></pre>\n<h6>List of tables:</h6>\n<pre><code class=\"language-sql\">.tables\n</code></pre>\n<h2>Managing Indexes</h2>\n<h6>Creating an index:</h6>\n<pre><code class=\"language-sql\">CREATE [UNIQUE] INDEX index_name\nON table_name (c1,c2,...)\n</code></pre>\n<h6>Delete an index:</h6>\n<pre><code class=\"language-sql\">DROP INDEX index_name;\n</code></pre>\n<h6>Create an expression index:</h6>\n<pre><code class=\"language-sql\">CREATE INDEX index_name ON table_name(expression);\n</code></pre>\n<h2>Querying Data</h2>\n<h6><strong>Query all data</strong> from a table:</h6>\n<pre><code class=\"language-sql\">SELECT * FROM table_name;\n</code></pre>\n<h6>Query data from the specified column of a table:</h6>\n<pre><code class=\"language-sql\">SELECT c1, c2\nFROM table_name;\n</code></pre>\n<h6>Query unique rows:</h6>\n<pre><code class=\"language-sql\">SELECT DISTINCT (c1)\nFROM table_name;\n</code></pre>\n<h6>Query rows that match a condition using a WHERE clause:</h6>\n<pre><code class=\"language-sql\">SELECT *\nFROM table_name\nWHERE condition;\n</code></pre>\n<h6>Rename column in the query&#39;s output:</h6>\n<pre><code class=\"language-sql\">SELECT c1 AS new_name\nFROM table_name;\n</code></pre>\n<h6>Query data from multiple tables using inner join, left join:</h6>\n<pre><code class=\"language-sql\">SELECT *\nFROM table_name_1\nINNER JOIN table_name_2 ON condition;\n</code></pre>\n<pre><code class=\"language-sql\">SELECT *\nFROM table_name_1\nLEFT JOIN table_name_2 ON condition;\n</code></pre>\n<h6>Count rows returned by a query:</h6>\n<pre><code class=\"language-sql\">SELECT COUNT (*)\nFROM table_name;\n</code></pre>\n<h6>Sort rows using ORDER BY clause:</h6>\n<pre><code class=\"language-sql\">SELECT c1, c2\nFROM table_name\nORDER BY c1 ASC [DESC], c2 ASC [DESC],...;\n</code></pre>\n<h6>Group rows using GROUP BY clause:</h6>\n<pre><code class=\"language-sql\">SELECT *\nFROM table_name\nGROUP BY c1, c2, ...;\n</code></pre>\n<h6>Filter group of rows using HAVING clause:</h6>\n<pre><code class=\"language-sql\">SELECT c1, aggregate(c2)\nFROM table_name\nGROUP BY c1\nHAVING condition;\n</code></pre>\n<h2>Changing Data</h2>\n<h6>Insert a row into a table:</h6>\n<pre><code class=\"language-sql\">INSERT INTO table_name(column1,column2,...)\nVALUES(value_1,value_2,...);\n</code></pre>\n<h6>Insert multiple rows into a table in a single statement:</h6>\n<pre><code class=\"language-sql\">INSERT INTO table_name(column1,column2,...)\nVALUES(value_1,value_2,...),\n      (value_1,value_2,...),\n      (value_1,value_2,...)...\n</code></pre>\n<h6>Update all rows in a table:</h6>\n<pre><code class=\"language-sql\">UPDATE table_name\nSET c1 = v1,\n    ...\n</code></pre>\n<h6>Update rows that match with a condition:</h6>\n<pre><code class=\"language-sql\">UPDATE table_name\nSET c1 = v1,\n    ...\nWHERE condition;\n</code></pre>\n<h6>Delete all rows in a table:</h6>\n<pre><code class=\"language-sql\">DELETE FROM table;\n</code></pre>\n<h6>Delete rows specified by a condition:</h6>\n<pre><code class=\"language-sql\">DELETE FROM table\nWHERE condition;\n</code></pre>\n<h2>Search</h2>\n<h6>Search using LIKE operator:</h6>\n<pre><code class=\"language-sql\">SELECT * FROM table\nWHERE column LIKE &#39;%value%&#39;;\n</code></pre>\n<h6>Search using full-text search:</h6>\n<pre><code class=\"language-sql\">SELECT *\nFROM table\nWHERE table MATCH &#39;search_query&#39;;\n</code></pre>\n","tags":["database"]},{"id":"rclone","url":"https://yieldray.fun/posts/rclone","title":"rclone常用命令","date_published":"2023-06-26T13:37:30.000Z","date_modified":"2023-06-26T13:37:30.000Z","content_text":"<p><a href=\"https://rclone.org/docs/\">https://rclone.org/docs/</a></p>\n<h1>commands</h1>\n<p>subcommands: <a href=\"https://rclone.org/docs/#subcommands\">https://rclone.org/docs/#subcommands</a></p>\n<p>commands: <a href=\"https://rclone.org/commands/\">https://rclone.org/commands/</a></p>\n<p>global flags: <a href=\"https://rclone.org/flags/\">https://rclone.org/flags/</a></p>\n<h2>rclone config</h2>\n<pre><code class=\"language-sh\">Usage:\n  rclone config [flags]\n  rclone config [command]\n\nAvailable Commands:\n  create      Create a new remote with name, type and options.\n  delete      Delete an existing remote.\n  disconnect  Disconnects user from remote\n  dump        Dump the config file as JSON.\n  file        Show path of configuration file in use.\n  password    Update password in an existing remote.\n  paths       Show paths used for configuration, cache, temp etc.\n  providers   List in JSON format all the providers and options.\n  reconnect   Re-authenticates user with remote.\n  show        Print (decrypted) config file, or the config for a single remote.\n  touch       Ensure configuration file exists.\n  update      Update options in an existing remote.\n  userinfo    Prints info about logged in user of remote.\n</code></pre>\n<h2>rclone copy</h2>\n<pre><code class=\"language-sh\">Usage:\n  rclone copy source:path dest:path [flags]\n\nFlags:\n      --create-empty-src-dirs   Create empty source dirs on destination after copy\n\n\n# example\nrclone copy remote:path /path/to/local/directory\nrclone copy /path/to/local/file remote:path\n</code></pre>\n<h2>rclone ls</h2>\n<ul>\n<li><code>ls</code> to list size and path of objects only</li>\n<li><code>lsl</code> to list modification time, size and path of objects only</li>\n<li><code>lsd</code> to list directories only</li>\n<li><code>lsf</code> to list objects and directories in easy to parse format</li>\n<li><code>lsjson</code> to list objects and directories in JSON format</li>\n</ul>\n<pre><code class=\"language-sh\">rclone ls remote:path\n</code></pre>\n<h2>rclone sync</h2>\n<pre><code class=\"language-sh\">Usage:\n  rclone sync source:path dest:path [flags]\n\nFlags:\n      --create-empty-src-dirs   Create empty source dirs on destination after sync\n</code></pre>\n<h2>rclone delete</h2>\n<pre><code class=\"language-sh\">rclone delete remote:path\n</code></pre>\n<h2>rclone mount</h2>\n<p><a href=\"https://rclone.org/commands/rclone_mount/\">https://rclone.org/commands/rclone_mount/</a></p>\n<pre><code class=\"language-sh\">Usage:\n  rclone mount remote:path /path/to/mountpoint [flags]\n\n# example (windows)\nrclone mount remote:/ X: --network-mode --volname NetDrive --vfs-cache-mode full --cache-dir D:\\\\Cache\n</code></pre>\n<h2>rclone cat</h2>\n<pre><code class=\"language-sh\">rclone cat remote:path [flags]\n\n      --count int    Only print N characters (default -1)\n      --discard      Discard the output instead of printing\n      --head int     Only print the first N characters\n  -h, --help         help for cat\n      --offset int   Start printing at offset N (or from end if -ve)\n      --tail int     Only print the last N characters\n</code></pre>\n<h2>rclone move</h2>\n<pre><code class=\"language-sh\">Usage:\n  rclone move source:path dest:path [flags]\n\nFlags:\n      --create-empty-src-dirs   Create empty source dirs on destination after move\n      --delete-empty-src-dirs   Delete empty source dirs after move\n</code></pre>\n<h1>web GUI</h1>\n<pre><code class=\"language-sh\">rclone rcd --rc-web-gui\n\n\n# open web gui\n--rc-web-gui\n\n# do not open browser\n--rc-web-gui-no-open-browser\n\n# update\n--rc-web-gui-update\n--rc-web-gui-force-update\n\n# directory list page\n--rc-serve\n\n# custom port\n--rc-addr :&lt;port&gt;\n\n# http auth\n--rc-user &lt;username&gt;\n--rc-pass &lt;password&gt;\n</code></pre>\n<h1>filter pattern</h1>\n<p><a href=\"https://rclone.org/filtering/\">https://rclone.org/filtering/</a></p>\n<pre><code class=\"language-sh\">--include &lt;pattern&gt;\n--exclude &lt;pattern&gt;\n</code></pre>\n","tags":["cli"]},{"id":"rust-for","url":"https://yieldray.fun/posts/rust-for","title":"rust for循环与闭包","date_published":"2023-06-22T13:13:13.000Z","date_modified":"2023-06-22T13:13:13.000Z","content_text":"<h1>for 区间</h1>\n<p>注意：区间的类型是 <a href=\"https://rustwiki.org/zh-CN/core/ops/struct.Range.html\">core::ops::range::Range<Idx></a> 泛型结构</p>\n<pre><code class=\"language-rust\">fn main() {\n    for n in 1..5 {\n        println!(&quot;{}&quot;, n);\n    }\n    for n in 5..=10 {\n        println!(&quot;{}&quot;, n);\n    }\n}\n</code></pre>\n<h1>三种迭代形式</h1>\n<p>参见：<a href=\"https://rustwiki.org/zh-CN/std/iter/index.html\">std::iter</a></p>\n<p>有三种常见的方法可以从集合中创建迭代器：</p>\n<p><code>iter()</code>，它在 <code>&amp;T</code> 上迭代。<br><code>iter_mut()</code>，它在 <code>&amp;mut T</code> 上迭代。<br><code>into_iter()</code>，它在 <code>T</code> 上迭代。</p>\n<pre><code class=\"language-rust\">fn main() {\n    let names = vec![&quot;Bob&quot;, &quot;Frank&quot;, &quot;Ferris&quot;];\n\n    // 不可变借用\n    for name in names.iter() {\n        match name {\n            &amp;&quot;Ferris&quot; =&gt; println!(&quot;There is a rustacean among us!&quot;),\n            _ =&gt; println!(&quot;Hello {}&quot;, name),\n        }\n    }\n\n    // 获取所有权，这*消耗*了集合，因为集合自身不再拥有其元素的所有权\n    for name in names.into_iter() {\n        match name {\n            &quot;Ferris&quot; =&gt; println!(&quot;There is a rustacean among us!&quot;),\n            _ =&gt; println!(&quot;Hello {}&quot;, name),\n        }\n    }\n\n    // 变量遮蔽\n    let mut names = vec![&quot;Bob&quot;, &quot;Frank&quot;, &quot;Ferris&quot;];\n\n    // 可变借用\n    for name in names.iter_mut() {\n        *name = match name {\n            &amp;mut &quot;Ferris&quot; =&gt; &quot;There is a rustacean among us!&quot;,\n            _ =&gt; &quot;Hello&quot;,\n        }\n    }\n    println!(&quot;names: {:?}&quot;, names);\n}\n</code></pre>\n<h1>闭包捕获</h1>\n<h2><code>Fn</code>：捕获方式为通过引用（<code>&amp;T</code>）的闭包</h2>\n<pre><code class=\"language-rust\">fn type_of&lt;T&gt;(_: &amp;T) -&gt; &amp;str {\n    std::any::type_name::&lt;T&gt;().into()\n}\n\nfn main() {\n    let color = String::from(&quot;green&quot;);\n\n    // 闭包*不可变借用*了 `color` 变量（因为闭包本身是不可变类型）\n    // 备注：`println!` 宏是通过引用使用的，不可变借用\n    let print = || println!(&quot;color={} type={}&quot;, color, type_of(&amp;color));\n    print();\n\n    // `color` 变量可再次被不可变借用，因为闭包只持有一个指向 `color` 的不可变引用。\n    let _reborrow = &amp;color;\n    print();\n\n    // 在最后使用 `print` 之后，移动或重新借用都是允许的。\n    let _color_moved = color;\n    // print(); 不允许使用 print，因为其捕获的变量已移动\n}\n</code></pre>\n<h2><code>FnMut</code>：捕获方式为通过可变引用（<code>&amp;mut T</code>）的闭包</h2>\n<pre><code class=\"language-rust\">fn main() {\n    let mut count = 0;\n    // `inc` 前面需要加上 `mut`，因为闭包里存储着一个 `&amp;mut` 可变借用的变量。\n    // 调用闭包时，该变量的变化就意味着闭包内部发生了变化。因此闭包需要是可变的。\n    let mut inc = || {\n        count += 1;\n        println!(&quot;`count`: {}&quot;, count);\n    };\n\n    // 使用可变借用调用闭包\n    inc();\n\n    // `count` 变量已经被闭包可变借用，因此*在最后一次使用闭包之前*不能再次被可变借用。\n    // 故以下语句不被允许：\n    // let _reborrow = &amp;count;\n    // inc();\n\n    // 现在，闭包不再可变借用 `&amp;mut count`，因此可以正确地重新借用\n    let _count_reborrowed = &amp;mut count;\n}\n</code></pre>\n<h2><code>FnOnce</code>：捕获方式为通过值（<code>T</code>）的闭包</h2>\n<pre><code class=\"language-rust\">fn type_of&lt;T&gt;(_: &amp;T) -&gt; &amp;str {\n    std::any::type_name::&lt;T&gt;().into()\n}\n\nfn main() {\n    // 不可复制类型（non-copy type）。\n    let movable = Box::new(3);\n\n    // `std::mem::drop` 要求 `T` 类型本身，所以闭包将会*捕获变量的值*。\n    // 这种情况下，可复制类型将会复制给闭包，从而原始值不受影响。\n    // 不可复制类型必须移动（move）到闭包中，因而 `movable` 变量在这里立即移动到了闭包中。\n    let consume = || {\n        println!(&quot;movable={:?} type={}&quot;, movable, type_of(&amp;movable));\n        std::mem::drop(movable);\n    };\n\n    // `consume` 消耗了该变量，所以该闭包只能调用一次。\n    consume();\n    // 不能再次调用闭包：\n    //consume();\n}\n</code></pre>\n<p>通过指定 move 可以强制使闭包获取其捕获的变量的所有权<br>即，被捕获的变量的所有权被移动进了闭包</p>\n<pre><code class=\"language-rust\">fn main() {\n    // `Vec` 在语义上是不可复制的。\n    let haystack = vec![1, 2, 3];\n\n    let contains = move |needle| haystack.contains(needle);\n\n    println!(&quot;{}&quot;, contains(&amp;1));\n    println!(&quot;{}&quot;, contains(&amp;4));\n\n    //println!(&quot;There&#39;re {} elements in vec&quot;, haystack.len());\n    // ^ 取消上面一行的注释将导致编译时错误，因为借用检查不允许在变量被移动走之后继续使用它。\n\n    // 在闭包的签名中删除 `move` 会导致闭包以不可变方式借用 `haystack`，\n    // 因此之后 `haystack` 仍然可用，取消上面的注释也不会导致错误。\n}\n</code></pre>\n<h2>闭包 trait</h2>\n<p>当闭包被定义，编译器会隐式地创建一个匿名类型的结构体，用以储存闭包捕获的变量，同时为这个未知类型的结构体实现函数功能，通过 Fn、FnMut 或 FnOnce 三种 trait 中的一种。</p>\n<pre><code class=\"language-rust\">fn test_closure_trait&lt;F&gt;(f: F) -&gt; bool\nwhere\n    F: FnOnce(i32, f64, &amp;str) -&gt; bool,\n{\n    // ^ 试一试：将 `FnOnce` 换成 `Fn` 或 `FnMut`。\n\n    f(1, 1.0, &quot;1&quot;)\n}\n</code></pre>\n<pre><code class=\"language-rust\">fn create_fn() -&gt; impl Fn() {\n    let text: String = &quot;Fn&quot;.to_owned();\n\n    // 闭包作为返回值，必须指明 move，将变量所有权移动进闭包\n    // 因为通过引用捕获的函数局部变量将在函数退栈后被丢弃（drop）\n    move || println!(&quot;This is a: {}&quot;, text)\n}\n</code></pre>\n<p>例子参见：<a href=\"https://rustwiki.org/zh-CN/rust-by-example/fn/closures/closure_examples.html\">std 中的例子</a></p>\n","tags":["rust"]},{"id":"web-access-control-headers","url":"https://yieldray.fun/posts/web-access-control-headers","title":"access-control 头部","date_published":"2023-06-17T19:07:53.000Z","date_modified":"2025-03-17T19:07:53.000Z","content_text":"<h1>请求</h1>\n<p>在发出 非<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/CORS#%E7%AE%80%E5%8D%95%E8%AF%B7%E6%B1%82\">简单请求</a> 之前，会先发送 Preflight 请求（请求方法为 OPTIONS）</p>\n<blockquote>\n<p><a href=\"https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests\">流式请求</a>永远是跨源请求，即必须预检</p>\n</blockquote>\n<p>对于 <a href=\"https://developer.mozilla.org/docs/Glossary/CORS-safelisted_request_header\">CORS-safelisted request header</a>，客户端无需指明 <code>Access-Control-Request-Headers</code>。<br>对于 <a href=\"https://developer.mozilla.org/docs/Glossary/CORS-safelisted_response_header\">CORS-safelisted response header</a>，服务端无需指明 <code>Access-Control-Allow-Headers</code>。</p>\n<p>显然：只有 OPTION 请求才会携带 <code>Access-Control-Request-*</code> 请求头。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Headers\">Access-Control-Request-Headers</a></h2>\n<p>指明将要使用的请求头。多个时，使用逗号分隔。</p>\n<p>注意：<a href=\"https://developer.mozilla.org/docs/Glossary/Forbidden_header_name\">Forbidden header name</a> 请求头禁止通过编程修改。因此这些请求不可能出现在 <code>Access-Control-Request-Headers</code> 中。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method\">Access-Control-Request-Method</a></h2>\n<p>指明将要使用的请求方法。只指定一个。</p>\n<h1>响应</h1>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Origin\">Access-Control-Allow-Origin</a></h2>\n<blockquote>\n<p>此标头必须对<strong>预检</strong>请求和<strong>实际</strong>请求都返回</p>\n</blockquote>\n<p>只能指定一个。若为不带 Credentials 请求，也可使用通配符。</p>\n<p>注意：浏览器的 Origin 请求头，仅当跨源请求会发送，仅当 URL 协议为 chrome-extension, chrome-untrusted, data, edge, http, https, isolated-app 时会正确携带，否则被设置为 <code>Origin: null</code>（例如浏览器直接打开文件，则为 file 协议）</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials\">Access-Control-Allow-Credentials</a></h2>\n<blockquote>\n<p>此标头必须对<strong>预检</strong>请求和<strong>实际</strong>请求都返回</p>\n</blockquote>\n<p>指定当客户端发送跨源的 Credentials 时，是否允许客户端获取服务端的响应内容。<br>（也可以说，是否允许客户端发送跨源 Credentials，不允许就不能获取服务端响应内容）</p>\n<p>注意：<a href=\"https://developer.mozilla.org/docs/Web/API/Request/credentials\"><code>Request.credential = &quot;include&quot;</code></a> 的请求，就是 发送跨源 Credentials 的请求。<br>此时请求头会自动携带 Cookie 或 <a href=\"https://developer.mozilla.org/docs/Web/HTTP/Guides/Authentication\">HTTP 认证</a>头。（此时<strong>不会</strong>携带 <code>Access-Control-Request-Headers: Cookie, Authorization</code>）</p>\n<p>在浏览器中手动构造请求时，手动构造的 Cookie 请求头永远会被忽略，就像没有被设置过一样（因为它属于<a href=\"https://developer.mozilla.org/docs/Glossary/Forbidden_request_header\">禁止修改的请求头</a>）。<br>手动构造请求携带的 HTTP 认证请求头，不属于跨源 Credentials。（会携带 <code>Access-Control-Request-Headers: Authorization</code> 预检请求头）</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Headers\">Access-Control-Allow-Headers</a></h2>\n<blockquote>\n<p>此标头仅对<strong>预检</strong>请求有效</p>\n</blockquote>\n<p>指明允许客户端发送的<a href=\"https://developer.mozilla.org/docs/Glossary/CORS-safelisted_request_header\">非 CORS 白名单请求头</a>。多个时，可用逗号分隔。<br>若预检请求携带了 Access-Control-Request-Headers 请求头，则必须响应 Access-Control-Allow-Headers，否则跨源失败。<br>若客户端的请求是无 Credentials 请求，则可以响应通配符 <code>*</code>，允许所有请求头。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Methods\">Access-Control-Allow-Methods</a></h2>\n<blockquote>\n<p>此标头仅对<strong>预检</strong>请求有效</p>\n</blockquote>\n<p>与上同理。</p>\n<blockquote>\n<p>有一种特殊情况是客户端手动构造 OPTIONS 请求，此时会发送两个 OPTIONS 请求。<br>不过实际情况下几乎不可能有人会手动构造 OPTIONS 请求。</p>\n</blockquote>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Expose-Headers\">Access-Control-Expose-Headers</a></h2>\n<blockquote>\n<p>此标头仅对<strong>实际</strong>请求有效</p>\n</blockquote>\n<p>指明向客户端暴露的响应头。多个时，使用逗号分隔。若为不带 Credentials 请求，也可使用通配符。<br>对于非暴露的且<a href=\"https://developer.mozilla.org/docs/Glossary/CORS-safelisted_response_header\">非 CORS 白名单的响应头</a>，客户端将获取到 null 值。</p>\n<p>注意：此响应头必须对实际请求返回，而非 OPTIONS 请求。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Max-Age\">Access-Control-Max-Age</a></h2>\n<blockquote>\n<p>此标头仅对<strong>预检</strong>请求有效</p>\n</blockquote>\n<p>指定 preflight 请求可缓存的秒数。若指定为 -1，则禁用缓存。<br>目前，Chromium 中默认值为 5，最大可为 7200</p>\n<h1>示例</h1>\n<p>一个最宽容的 CORS 配置（不要在生产环境下使用）</p>\n<pre><code class=\"language-js\">const express = require(&quot;express&quot;);\nconst app = express();\n\napp.use(&quot;/*&quot;, (req, res, next) =&gt; {\n    if (req.headers.origin) {\n        res.header(&quot;access-control-allow-origin&quot;, req.headers.origin !== &quot;null&quot; ? req.headers.origin : &quot;*&quot;);\n        res.header(&quot;access-control-allow-credentials&quot;, &quot;true&quot;);\n    }\n    next();\n});\n\napp.options(&quot;/*&quot;, (req, res) =&gt; {\n    res.header(&quot;access-control-allow-headers&quot;, req.headers[&quot;access-control-request-headers&quot;]);\n    res.header(&quot;access-control-allow-methods&quot;, req.headers[&quot;access-control-request-method&quot;]);\n\n    res.header(&quot;access-control-max-age&quot;, &quot;7200&quot;);\n\n    // 无意义！\n    // res.header(&quot;access-control-expose-headers&quot;, &quot;x-test-expose&quot;);\n\n    res.send();\n});\n\napp.get(&quot;/*&quot;, (req, res) =&gt; {\n    const customHeaders = new Headers({ &quot;x-test-expose&quot;: &quot;foo&quot; });\n    res.setHeaders(customHeaders);\n    res.header(&quot;access-control-expose-headers&quot;, [...customHeaders.keys()].join(&quot;, &quot;));\n\n    // 无意义！\n    // res.header(&quot;access-control-allow-headers&quot;, &quot;Authorization&quot;);\n    // res.header(&quot;access-control-allow-methods&quot;, &quot;GET,POST,PUT,PATCH,DELETE&quot;);\n    // res.header(&quot;access-control-max-age&quot;, &quot;7200&quot;);\n\n    res.send();\n});\n\napp.listen(3000);\n</code></pre>\n<p>或者直接使用 http 模块</p>\n<pre><code class=\"language-js\">const http = require(&quot;http&quot;);\nhttp.createServer((req, res) =&gt; {\n    enableCORS(req, res);\n    // Your logic here...\n    res.end();\n}).listen(3000);\n\nfunction enableCORS(req, res) {\n    if (!req.headers.origin) return;\n    res.setHeader(&quot;access-control-allow-origin&quot;, req.headers.origin !== &quot;null&quot; ? req.headers.origin : &quot;*&quot;);\n    res.setHeader(&quot;access-control-allow-credentials&quot;, &quot;true&quot;);\n\n    if (req.method === &quot;OPTIONS&quot;) {\n        res.setHeader(&quot;access-control-allow-headers&quot;, req.headers[&quot;access-control-request-headers&quot;] || &quot;*&quot;);\n        res.setHeader(&quot;access-control-allow-methods&quot;, req.headers[&quot;access-control-request-method&quot;] || &quot;*&quot;);\n        res.setHeader(&quot;access-control-max-age&quot;, &quot;7200&quot;);\n    } else {\n        res.setHeader(&quot;access-control-expose-headers&quot;, &quot;*&quot;);\n    }\n}\n</code></pre>\n","tags":["web-api","http"]},{"id":"css-pure-css-component","url":"https://yieldray.fun/posts/css-pure-css-component","title":"纯css组件","date_published":"2023-06-09T19:18:17.000Z","date_modified":"2023-06-09T19:18:17.000Z","content_text":"<h1>pure css</h1>\n<p>纯 css（或者原生标签）</p>\n<p>inspired by <a href=\"https://daisyui.com/\">daisyUI</a></p>\n<h1>dropdown</h1>\n<iframe height=\"500\" style=\"width: 100%;\" scrolling=\"no\" title=\"dropdown\" src=\"https://codepen.io/YieldRay/embed/eYQmPJB?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/eYQmPJB\">\n  dropdown</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>dialog</h1>\n<iframe height=\"500\" style=\"width: 100%;\" scrolling=\"no\" title=\"dialog\" src=\"https://codepen.io/YieldRay/embed/NWEPOmr?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/NWEPOmr\">\n  dialog</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>swap</h1>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"swap\" src=\"https://codepen.io/YieldRay/embed/ZEmYmqm?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/ZEmYmqm\">\n  swap</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>详细可搜索：checkbox hack</p>\n<h1>accordion</h1>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"accordion\" src=\"https://codepen.io/YieldRay/embed/xxQGQjM?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/xxQGQjM\">\n  accordion</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>progress</h1>\n<p class=\"codepen\" data-height=\"300\" data-default-tab=\"html,result\" data-slug-hash=\"XWyJoWL\" data-editable=\"true\" data-user=\"YieldRay\" style=\"height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;\">\n  <span>See the Pen <a href=\"https://codepen.io/YieldRay/pen/XWyJoWL\">\n  progress</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.</span>\n</p>\n<script async src=\"https://cpwebassets.codepen.io/assets/embed/ei.js\"></script>\n\n<h1>tooltip</h1>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"tooltip\" src=\"https://codepen.io/YieldRay/embed/NWEPeRB?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/NWEPeRB\">\n  tooltip</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>实际得上 js 库：<a href=\"https://github.com/atomiks/tippyjs\">https://github.com/atomiks/tippyjs</a></p>\n","tags":["css"]},{"id":"electron-ipc","url":"https://yieldray.fun/posts/electron-ipc","title":"Electron进程间通信","date_published":"2023-06-01T19:19:19.000Z","date_modified":"2023-06-01T19:19:19.000Z","content_text":"<p><a href=\"https://unpkg.com/browse/electron@latest/electron.d.ts\">typescript declaration</a></p>\n<h1>IPC 信道</h1>\n<p>在 Electron 中，进程使用 Electron 提供的 <code>ipcMain</code> 和 <code>ipcRenderer</code> 模块进行通信。<br>这些通道是 任意 （您可以随意命名它们）和 双向 （您可以在两个模块中使用相同的通道名称）的。</p>\n<p>显然：<code>ipcMain</code> 在主线程使用，<code>ipcRenderer</code> 在渲染器线程（preload 线程）使用</p>\n<blockquote>\n<p><strong>Preload 脚本</strong><br>预加载（preload）脚本包含了那些执行于渲染器进程中，且先于网页内容开始加载的代码。<br>这些脚本虽运行于渲染器的环境中，却因能访问 Node.js API 而拥有了更多的权限。<br>预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。<br>预加载脚本使用 Electron 提供的 contextBridge 模块从主线程向渲染线程的 Window 实例暴露接口</p>\n</blockquote>\n<h1>渲染器进程到主进程（单向）</h1>\n<p><a href=\"https://www.electronjs.org/zh/docs/latest/api/ipc-main\">ipcMain</a><br><a href=\"https://www.electronjs.org/zh/docs/latest/api/structures/ipc-main-event\">IpcMainEvent Object extends Event</a></p>\n<pre><code class=\"language-js\">const { app, BrowserWindow, ipcMain } = require(&quot;electron&quot;);\nconst path = require(&quot;path&quot;);\n\nfunction createWindow() {\n    const mainWindow = new BrowserWindow({\n        webPreferences: {\n            preload: path.join(__dirname, &quot;preload.js&quot;),\n        },\n    });\n\n    // 在主线程中使用 ipcMain 模块\n    // 主线程监听信道（单向，仅监听事件，无法返回值）\n    // on(channel: string, listener: (event: IpcMainEvent, ...args: any[]) =&gt; void): this;\n    ipcMain.on(&quot;set-title&quot;, (event, title) =&gt; {\n        const webContents = event.sender;\n        const win = BrowserWindow.fromWebContents(webContents);\n        win.setTitle(title);\n    });\n\n    mainWindow.loadFile(&quot;index.html&quot;);\n}\n\n// 下面是样板代码，忽略\napp.whenReady().then(() =&gt; {\n    createWindow();\n\n    app.on(&quot;activate&quot;, function () {\n        if (BrowserWindow.getAllWindows().length === 0) createWindow();\n    });\n});\n\napp.on(&quot;window-all-closed&quot;, function () {\n    if (process.platform !== &quot;darwin&quot;) app.quit();\n});\n</code></pre>\n<p><a href=\"https://www.electronjs.org/zh/docs/latest/api/ipc-renderer\">ipcRenderer</a></p>\n<pre><code class=\"language-js\">const { contextBridge, ipcRenderer } = require(&quot;electron&quot;);\n\ncontextBridge.exposeInMainWorld(&quot;electronAPI&quot;, {\n    // 在 preload 脚本中使用 ipcRenderer 模块\n    // send(channel: string, ...args: any[]): void;\n    setTitle: (title) =&gt; ipcRenderer.send(&quot;set-title&quot;, title),\n});\n</code></pre>\n<blockquote>\n<p><strong>安全警告</strong><br>出于<em>安全原因</em>，我们不会直接暴露整个 <code>ipcRenderer.send</code> API。 确保尽可能限制渲染器对 Electron API 的访问。</p>\n</blockquote>\n<pre><code class=\"language-js\">const setButton = document.getElementById(&quot;btn&quot;);\nconst titleInput = document.getElementById(&quot;title&quot;);\nsetButton.addEventListener(&quot;click&quot;, () =&gt; {\n    const title = titleInput.value;\n    window.electronAPI.setTitle(title);\n});\n</code></pre>\n<h1>渲染器进程到主进程（双向）</h1>\n<p>下面将省略样板代码</p>\n<pre><code class=\"language-js\">const { app, BrowserWindow, ipcMain, dialog } = require(&quot;electron&quot;);\nconst path = require(&quot;path&quot;);\n\napp.whenReady().then(() =&gt; {\n    // 主线程处理信道（双向，可返回值）\n    // handle(channel: string, listener: (event: IpcMainInvokeEvent, ...args: any[]) =&gt; (Promise&lt;void&gt;) | (any)): void;\n    ipcMain.handle(&quot;dialog:openFile&quot;, async () =&gt; {\n        const { canceled, filePaths } = await dialog.showOpenDialog();\n        if (!canceled) {\n            return filePaths[0];\n        }\n    });\n\n    const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, &quot;preload.js&quot;) } });\n    mainWindow.loadFile(&quot;index.html&quot;);\n});\n</code></pre>\n<pre><code class=\"language-js\">const { contextBridge, ipcRenderer } = require(&quot;electron&quot;);\n\ncontextBridge.exposeInMainWorld(&quot;electronAPI&quot;, {\n    // invoke(channel: string, ...args: any[]): Promise&lt;any&gt;\n    openFile: () =&gt; ipcRenderer.invoke(&quot;dialog:openFile&quot;), // =&gt; Promise&lt;string&gt;\n});\n</code></pre>\n<pre><code class=\"language-js\">const btn = document.getElementById(&quot;btn&quot;);\nconst filePathElement = document.getElementById(&quot;filePath&quot;);\n\nbtn.addEventListener(&quot;click&quot;, async () =&gt; {\n    const filePath = await window.electronAPI.openFile();\n    filePathElement.innerText = filePath;\n});\n</code></pre>\n<h1>主进程到渲染器进程</h1>\n<p>将消息从主进程发送到渲染器进程时，需要指定是哪一个渲染器接收消息。<br>消息需要通过其 WebContents 实例发送到渲染器进程。<br>此 WebContents 实例包含一个 send 方法，其使用方式与 ipcRenderer.send 相同。</p>\n<pre><code class=\"language-js\">const { app, BrowserWindow, Menu, ipcMain } = require(&quot;electron&quot;);\nconst path = require(&quot;path&quot;);\n\napp.whenReady().then(() =&gt; {\n    ipcMain.on(&quot;counter-value&quot;, (_event, value) =&gt; {\n        console.log(value); // 主线程是 Node.js 环境，故将打印在 Node.js 控制台\n    });\n\n    Menu.setApplicationMenu(\n        Menu.buildFromTemplate([\n            {\n                label: app.name,\n                submenu: [\n                    {\n                        click: () =&gt; mainWindow.webContents.send(&quot;update-counter&quot;, 1),\n                        label: &quot;Increment&quot;,\n                    },\n                    {\n                        click: () =&gt; mainWindow.webContents.send(&quot;update-counter&quot;, -1),\n                        label: &quot;Decrement&quot;,\n                    },\n                ],\n            },\n        ]),\n    );\n\n    const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, &quot;preload.js&quot;) } });\n    mainWindow.loadFile(&quot;index.html&quot;);\n    mainWindow.webContents.openDevTools();\n});\n</code></pre>\n<pre><code class=\"language-js\">const { contextBridge, ipcRenderer } = require(&quot;electron&quot;);\n\ncontextBridge.exposeInMainWorld(&quot;electronAPI&quot;, {\n    handleCounter: (callback) =&gt; ipcRenderer.on(&quot;update-counter&quot;, callback),\n});\n</code></pre>\n<pre><code class=\"language-js\">const counter = document.getElementById(&quot;counter&quot;);\n\nwindow.electronAPI.handleCounter((event, value) =&gt; {\n    const oldValue = Number(counter.innerText);\n    const newValue = oldValue + value;\n    counter.innerText = newValue;\n    event.sender.send(&quot;counter-value&quot;, newValue);\n});\n</code></pre>\n","tags":["js"]},{"id":"clang-gcc-gdb","url":"https://yieldray.fun/posts/clang-gcc-gdb","title":"GDB的使用，基本GCC命令","date_published":"2023-06-01T16:00:00.000Z","date_modified":"2023-06-01T16:00:00.000Z","content_text":"<pre><code class=\"language-sh\"># 编译32位所需\nsudo apt-get install build-essential module-assistant\nsudo apt-get install gcc-multilib g++-multilib\n</code></pre>\n<h1>基本 GCC 命令的使用</h1>\n<p><img src=\"/media/gcc-compile.svg\" alt=\"\"></p>\n<pre><code class=\"language-sh\">gcc -E hello.c -o hello.i\ngcc -S hello.i -o hello.s\ngcc -c hello.s -o hello.o\ngcc hello.o -o hello\n</code></pre>\n<pre><code class=\"language-sh\"># -m32 编译32位\n# -g 开启调试信息\ngcc -E -g -m32 gdbtest.c -o gdbtest.i\ngcc -S -g -m32 gdbtest.i -o gdbtest.s\ngcc -c -g -m32 gdbtest.s -o gdbtest.o\ngcc -O0 -m32 -g gdbtest.c -o gdbtestl\n\n# -S 在反汇编后的内容中添加源代码\nobjdump -S gdbtest.o &gt; gdbtesto.txt\nobjdump -S  gdbtest &gt; gdbtest.txt\n\n\ngcc -O0 -m32 -g gdbtest.c -o gdbtest\nobjdump -S gdbtest &gt; gdbtest.txt\n</code></pre>\n<h1>GDB 调试工具</h1>\n<h2>1 启动 GDB</h2>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>命令</th>\n<th>作用</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>①</td>\n<td>gdb [可执行文件名]</td>\n<td>启动 GDB 调试工具，并加载可执行文件</td>\n</tr>\n<tr>\n<td></td>\n<td>-</td>\n<td>-</td>\n</tr>\n<tr>\n<td>②</td>\n<td>gdb</td>\n<td>启动 GDB 调试工具</td>\n</tr>\n<tr>\n<td></td>\n<td>file [可执行文件名]</td>\n<td>加载可执行文件</td>\n</tr>\n</tbody></table>\n<h2>2 设置断点</h2>\n<table>\n<thead>\n<tr>\n<th>命令</th>\n<th>作用</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>break main</td>\n<td>在 main 函数的入口点处设置断点</td>\n</tr>\n<tr>\n<td>break gdbtest.c:3</td>\n<td>在源程序 gdbtest.c 的第 3 行处设置断点</td>\n</tr>\n</tbody></table>\n<h2>3 启动程序</h2>\n<table>\n<thead>\n<tr>\n<th>命令</th>\n<th>作用</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>run</td>\n<td>启动程序运行，程序会在断点处停下</td>\n</tr>\n</tbody></table>\n<h2>4 查看程序运行时的当前状态</h2>\n<h3>第一步：程序的当前断点位置</h3>\n<p>含义：反映程序已经执行了哪些指令，下一步要执行哪一条指令<br>eip 寄存器保存下一条将要执行指令的地址</p>\n<pre><code>i r    : 显示所有寄存器的内容\ni r eip: 只显示寄存器 eip 的内容\n</code></pre>\n<h3>第二步：通用寄存器的内容</h3>\n<pre><code>i r eax ebx ecx edx （或 i r）\n</code></pre>\n<h3>第三步：存储单元的内容</h3>\n<pre><code>x/8xb 0xffffd2bc\n# 从 0xffffd2bc 地址开始的 8 个 32 位的存储单元内容 ， 并用十六进制表示\nx/2xw 0xffffd2bc\n# 从 0xffffd2bc 地址开始的 2 个 32 位的存储单元内容 ， 并用十六进制表示\n</code></pre>\n<p>说明：IA·32 用栈来支持过程的嵌套调用，过程的入口参数、返回地址、被保存寄存器的值、<br>被调用过程中的非静态局部变量等都被保存在栈中。栈帧系统为每个执行的过程分配一个栈空间。</p>\n<h3>第四步：栈帧信息</h3>\n<p>esp 栈顶指针 ebp 栈底指针</p>\n<p>当前栈帧范围： i r esp ebp\n当前栈帧字节数： y=R[ebp] - R[esp]+4<br>显示当前栈帧内容：\nx/yxb $esp // y：R[ebp] - R[esp]+4 的值，栈帧起始地址是 esp 指向的单元地址\nx/zxw $esp // z=y/4, 显示从 esp 指向的地址开始</p>\n<p><img src=\"/media/stack-frame.svg\" alt=\"\"></p>\n<h3>第五步：继续执行下一条指令或语句</h3>\n<table>\n<thead>\n<tr>\n<th>命令</th>\n<th>作用</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>si</td>\n<td>执行一条机器指令</td>\n</tr>\n<tr>\n<td>s</td>\n<td>执行一条 c 语句</td>\n</tr>\n</tbody></table>\n","tags":["c"]},{"id":"js-auto-animate","url":"https://yieldray.fun/posts/js-auto-animate","title":"AutoAnimate","date_published":"2023-06-01T14:14:14.000Z","date_modified":"2023-06-01T14:14:14.000Z","content_text":"<h1>auto-animate</h1>\n<p><a href=\"https://auto-animate.formkit.com/\">https://auto-animate.formkit.com/</a></p>\n<pre><code class=\"language-sh\">npm install @formkit/auto-animate\n</code></pre>\n<h1>usage</h1>\n<p><code>autoAnimate</code> 函数的第一个参数指定父元素，动画将作用与该父元素和其直接子元素</p>\n<p>具体来说，动画将在下列情况触发：</p>\n<ul>\n<li>子元素添加至 DOM 节点</li>\n<li>子元素从 DOM 节点中移除</li>\n<li>子元素在 DOM 节点中移动</li>\n</ul>\n<p>若父元素是静态定位，则将自动添加 <code>position: relative</code></p>\n<p>flex 布局时，子元素的位置可能不能立即计算出（例如：<code>flex-grow: 1</code>），此时 <code>autoAnimate</code> 将无效<br>此时可以手动指定子元素尺寸</p>\n<pre><code class=\"language-js\">import autoAnimate from &quot;https://unpkg.com/@formkit/auto-animate&quot;;\n\nautoAnimate(el, {\n  // 动画持续时间，单位毫秒 (default: 250)\n  duration: 250,\n  // Easing for motion (default: &#39;ease-in-out&#39;)\n  easing: &#39;ease-in-out&#39;\n  // 强制开启动画，即不遵守 prefers-reduced-motion 规则\n  disrespectUserMotionPreference: false\n})\n</code></pre>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"Untitled\" src=\"https://codepen.io/YieldRay/embed/MWPMqep?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/MWPMqep\">\n  Untitled</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n","tags":["js","lib"]},{"id":"js-zod","url":"https://yieldray.fun/posts/js-zod","title":"zod","date_published":"2023-05-25T16:16:16.000Z","date_modified":"2023-05-25T16:16:16.000Z","content_text":"<h1>zod</h1>\n<p>Zod 是一个 TypeScript 优先的模式声明和验证库</p>\n<p><a href=\"https://zod.dev/\">https://zod.dev/</a><br><a href=\"https://zod.dev/README_ZH\">https://zod.dev/README_ZH</a></p>\n<h2>basic</h2>\n<pre><code class=\"language-ts\">import { z } from &quot;https://deno.land/x/zod/mod.ts&quot;;\nimport { ZodError } from &quot;https://deno.land/x/zod/ZodError.ts&quot;;\nimport { ZodObject } from &quot;https://deno.land/x/zod/mod.ts&quot;;\n\n// 原始值\n\nconst mySchema = z.string();\n\nmySchema.parse(&quot;zod&quot;); // =&gt; &quot;zod&quot;\n\ntry {\n    mySchema.parse(12); // ZodError\n} catch (e) {\n    if (e instanceof ZodError) {\n        // 这个 error 的 message 是 issues 属性 JSON.stringify 得到的\n        // 所以直接访问 issue 属性\n        console.log(e.issues);\n    }\n}\n\nmySchema.safeParse(&quot;zod&quot;); // =&gt; { success: true; data: &quot;zod&quot; }\nmySchema.safeParse(12); // =&gt; { success: false; error: ZodError }\n\n// 对象\n\nconst User = z.object({\n    username: z.string(),\n});\n\nUser.parse({ username: &quot;Ray&quot; }); // =&gt; { username: &quot;Ray&quot; }\n\ntype User = z.infer&lt;typeof User&gt;; // { username: string }\n\nconsole.assert(User instanceof ZodObject);\n</code></pre>\n<h1>字符串</h1>\n<pre><code class=\"language-js\">// 验证\nz.string().max(5);\nz.string().min(5);\nz.string().length(5);\nz.string().email();\nz.string().url();\nz.string().emoji();\nz.string().uuid();\nz.string().cuid();\nz.string().cuid2();\nz.string().ulid();\nz.string().regex(regex);\nz.string().includes(string);\nz.string().startsWith(string);\nz.string().endsWith(string);\nz.string().datetime(); // 默认为 UTC\nz.string().datetime({ offset: true }); // 其它选项略\nz.string().ip();\nz.string().ip({ version: &quot;v4&quot; });\nz.string().ip({ version: &quot;v6&quot; });\n\n// 转换\nz.string().trim();\nz.string().toLowerCase();\nz.string().toUpperCase();\n\n// 自定义错误信息\nz.string({\n    required_error: &quot;Name is required&quot;,\n    invalid_type_error: &quot;Name must be a string&quot;,\n});\nz.string().url({ message: &quot;Invalid url&quot; });\n</code></pre>\n<h1>数字</h1>\n<pre><code class=\"language-js\">// 自定义错误信息\nconst age = z.number({\n    required_error: &quot;Age is required&quot;,\n    invalid_type_error: &quot;Age must be a number&quot;,\n});\n\n// 验证\nz.number().gt(5);\nz.number().gte(5); // alias .min(5)\nz.number().lt(5);\nz.number().lte(5); // alias .max(5)\n\nz.number().int(); // value must be an integer\n\nz.number().positive(); //     &gt; 0\nz.number().nonnegative(); //  &gt;= 0\nz.number().negative(); //     &lt; 0\nz.number().nonpositive(); //  &lt;= 0\n\nz.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5)\n\nz.number().finite(); // value must be finite, not Infinity or -Infinity\nz.number().safe(); // value must be between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER\n\n// 自定义错误信息\nz.number().lte(5, { message: &quot;this👏is👏too👏big&quot; });\n</code></pre>\n<h1>自定义</h1>\n<pre><code class=\"language-ts\">const px = z.custom&lt;`${number}px`&gt;((val) =&gt; {\n    return /^\\d+px$/.test(val as string);\n});\n\ntype px = z.infer&lt;typeof px&gt;; // `${number}px`\n\npx.parse(&quot;42px&quot;); // &quot;42px&quot;\npx.parse(&quot;42vw&quot;); // throws;\n</code></pre>\n<pre><code class=\"language-ts\">const myString = z.string().refine((val) =&gt; val.length &lt;= 255, {\n    message: &quot;String can&#39;t be more than 255 characters&quot;,\n});\n\nconst longString = z.string().refine(\n    (val) =&gt; val.length &gt; 10,\n    (val) =&gt; ({ message: `${val} is not more than 10 characters` }),\n);\n</code></pre>\n<h1>异步</h1>\n<p>若 schema 定义了异步过程，则需要使用特殊的 parse 方法</p>\n<pre><code class=\"language-js\">const userId = z.string().refine(async (id) =&gt; {\n    // verify that ID exists in database\n    return true;\n});\n\nconst IdToUser = z\n    .string()\n    .uuid()\n    .transform(async (id) =&gt; {\n        return await getUserById(id);\n    });\n\nuserId.parseAsync(xxx);\nidToUser.safeParseAsync(xxx);\n</code></pre>\n<h1>Schema methods</h1>\n<p><a href=\"https://zod.dev/?id=schema-methods\">https://zod.dev/?id=schema-methods</a></p>\n","tags":["typescript","lib"]},{"id":"covariance-contravariance","url":"https://yieldray.fun/posts/covariance-contravariance","title":"协变和逆变","date_published":"2023-05-22T20:20:20.000Z","date_modified":"2023-05-22T20:20:20.000Z","content_text":"<p>此处参考 C# ：<a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/covariance-contravariance/\">https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/covariance-contravariance/</a></p>\n<h1>C#</h1>\n<h2>协变与逆变</h2>\n<p>在 C# 中，协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。</p>\n<p><strong>协变保留分配兼容性，逆变则与之相反。</strong></p>\n<pre><code class=\"language-cs\">// 分配兼容性\nstring str = &quot;test&quot;;\n// 派生程度更大 的类型可分配（赋值）给 派生程度更小 的类型\nobject obj = str;\n\n// 协变\nIEnumerable&lt;string&gt; strings = new List&lt;string&gt;();\n// 实例化为 派生程度更大 的类型参数，被分配给实例化为 派生程度更小 的类型参数\n// 分配兼容性被保留(preserved)了（即：能将 派生程度更大 的类型分配给 派生程度更小 的类型）\nIEnumerable&lt;object&gt; objects = strings;\n//                     |         |\n//                less derived   |\n//                          more derived\n\n// 逆变\n// 假设下列方法是类中的方法\nstatic void SetObject(object o) { }\nAction&lt;object&gt; actObject = SetObject;\n// 派生程度更小 类型参数实例化的对象，被分配给 派生程度更大 类型参数实例化的对象\n// 分配兼容性被逆转(reversed)了（即：能将 派生程度更小 的类型分配给 派生程度更大 的类型）\nAction&lt;string&gt; actString = actObject;\n//                  |          |\n//             more derived    |\n//                        less derived\n</code></pre>\n<p>MoreDerived = 更具体<br>LessDerived = 更抽象<br>MoreDerived extends LessDerived<br>LessDerived super MoreDerived</p>\n<p>返回派生程度更大的类型的方法（协变）<br>接受具有派生程度更小的类型的参数的方法（逆变）</p>\n<pre><code class=\"language-cs\">static object GetObject() { return null; }\nstatic void SetObject(object obj) { }\n\nstatic string GetString() { return &quot;&quot;; }\nstatic void SetString(string str) { }\n\nstatic void Test()\n{\n    // 协变。\n    // 委托类型的返回类型是 object，但该委托类型也接受返回 string 的委托类型\n    Func&lt;object&gt; del = GetString;\n    // 注：一个函数返回的值更具体，这个值就可以被分配给更抽象的类型\n\n    // 逆变。\n    // 委托类型的参数是 string 类型，但该委托类型也接受参数为 object 的委托类型\n    Action&lt;string&gt; del2 = SetObject;\n    // 注：一个函数能使用一个更抽象的参数就能使用更具体的参数\n}\n</code></pre>\n<h2>变体泛型接口</h2>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/covariance-contravariance/creating-variant-generic-interfaces\">https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/covariance-contravariance/creating-variant-generic-interfaces</a></p>\n<p>可通过对泛型类型参数使用 in 和 out 关键字来声明变体泛型接口</p>\n<h3>协变</h3>\n<p>可以使用 out 关键字将泛型类型参数声明为协变。<br>协变类型必须满足以下条件：</p>\n<ul>\n<li>类型仅用作接口方法的返回类型，不用作方法的参数类型。<br>下例演示了此要求，其中类型 R 为声明的协变。</li>\n</ul>\n<pre><code class=\"language-cs\">interface ICovariant&lt;out R&gt;\n{\n    R GetSomething();\n    // 下列声明将导致编译错误\n    // void SetSomething(R sampleArg);\n\n    // 此规则有一个例外。 如果具有用作方法参数的逆变泛型委托，则可将类型用作该委托的泛型类型参数\n    void DoSomething(Action&lt;R&gt; callback);\n}\n</code></pre>\n<ul>\n<li>类型不用作接口方法的泛型约束。<br>下面的代码阐释了这一点。</li>\n</ul>\n<pre><code class=\"language-cs\">interface ICovariant&lt;out R&gt;\n{\n    // 下列声明将导致编译错误\n    // 因为只能在泛型参数中哦使用 逆变 或 不变 类型\n    // void DoSomething&lt;T&gt;() where T : R;\n}\n</code></pre>\n<p><img src=\"https://s2.loli.net/2023/05/22/tsI6oXPR1Hnv5Li.png\" alt=\"1.png\"></p>\n<h3>逆变</h3>\n<p>可以使用 in 关键字将泛型类型参数声明为逆变。<br>逆变类型只能用作方法参数的类型，不能用作接口方法的返回类型。<br>逆变类型还可用于泛型约束。<br>以下代码演示如何声明逆变接口，以及如何将泛型约束用于其方法之一。</p>\n<pre><code class=\"language-cs\">interface IContravariant&lt;in A&gt;\n{\n    void SetSomething(A sampleArg);\n    void DoSomething&lt;T&gt;() where T : A;\n    // 下列声明将导致编译错误\n    // A GetSomething();\n}\n</code></pre>\n<p>此外，还可以在同一接口中同时支持协变和逆变，但需应用于不同的类型参数，如以下代码示例所示。</p>\n<pre><code class=\"language-cs\">interface IVariant&lt;out R, in A&gt;\n{\n    R GetSomething();\n    void SetSomething(A sampleArg);\n    R GetSetSomethings(A sampleArg);\n}\n</code></pre>\n<p><img src=\"https://s2.loli.net/2023/05/22/yXfHULcWdBqZDA1.png\" alt=\"2.png\"></p>\n<h1>Typescript</h1>\n<p>参见：<a href=\"https://github.com/microsoft/TypeScript/pull/48240\">https://github.com/microsoft/TypeScript/pull/48240</a></p>\n","tags":["design"]},{"id":"linux-screen","url":"https://yieldray.fun/posts/linux-screen","title":"screen命令","date_published":"2023-05-21T16:38:38.000Z","date_modified":"2023-05-21T16:38:38.000Z","content_text":"<p><a href=\"https://www.gnu.org/software/screen/manual/screen.html\">https://www.gnu.org/software/screen/manual/screen.html</a></p>\n<pre><code class=\"language-sh\"># 列出所有 session\nscreen -ls\n\n# 创建新 session，默认名称带有 tty.host 后缀\n# 注：创建新 session 后都会直接 attach 到该 session\nscreen\n\n# 创建新 session，并指定名称\nscreen -S &lt;session-name&gt;\n\n# 创建新 session，若该名称已存在就不创建\nscreen -R &lt;session-name&gt;\n\n# 重新回到指定 session\nscreen -r &lt;pid&gt;|&lt;session-name&gt;\n\n# -X 选项将指定命令发送至 session\nscreen -R/-r/-S &lt;pid&gt;|&lt;session-name&gt; -X quit\n\n# detach\nscreen -d\n# 也可以通过快捷键：C-a d\n</code></pre>\n","tags":["linux","cli"]},{"id":"linux-tmux","url":"https://yieldray.fun/posts/linux-tmux","title":"tmux命令","date_published":"2023-05-20T21:21:12.000Z","date_modified":"2023-05-20T21:21:12.000Z","content_text":"<h1>tmux</h1>\n<p>terminal multiplexer<br>tmux 是一个终端复用器<br>（注：tmux 来自于 OpenBSD）</p>\n<p><a href=\"https://github.com/tmux/tmux/wiki/Getting-Started\">https://github.com/tmux/tmux/wiki/Getting-Started</a><br><a href=\"https://wiki.archlinuxcn.org/wiki/Tmux\">https://wiki.archlinuxcn.org/wiki/Tmux</a><br><a href=\"https://learnxinyminutes.com/docs/zh-cn/tmux-cn/\">https://learnxinyminutes.com/docs/zh-cn/tmux-cn/</a></p>\n<p>tmux 的主要用途是：</p>\n<ul>\n<li>通过将程序运行在 tmux 中，以防止远程服务器连接断开时程序终止运行</li>\n<li>允许将程序运行在远程服务器上，然后被多台本地主机访问</li>\n<li>在一个终端中运行多个程序和 shell，有点像窗口管理器</li>\n</ul>\n<p><a href=\"https://man.openbsd.org/tmux\">https://man.openbsd.org/tmux</a></p>\n<pre><code class=\"language-sh\">$ man 1 tmux\n</code></pre>\n<p><img src=\"https://i0.wp.com/github.com/tmux/tmux/wiki/images/tmux_pane_diagram.png\" alt=\"tmux_pane_diagram\"></p>\n<p>术语</p>\n<table>\n<thead>\n<tr>\n<th>术语</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Client</td>\n<td>可以通过类似于 xterm 的终端 attach 到一个 tmux 的 session</td>\n</tr>\n<tr>\n<td>Session</td>\n<td>一个或一组 window</td>\n</tr>\n<tr>\n<td>Window</td>\n<td>一个或一组 pane</td>\n</tr>\n<tr>\n<td>Pane</td>\n<td>包含一个终端和运行中的程序，出现在一个 window 中</td>\n</tr>\n<tr>\n<td>Active pane</td>\n<td>current window 中的 pane</td>\n</tr>\n<tr>\n<td>Current window</td>\n<td>attached session 中的 window （where typing is sent）</td>\n</tr>\n<tr>\n<td>Last window</td>\n<td>上一个 current window</td>\n</tr>\n<tr>\n<td>Session name</td>\n<td>session 的名称，默认从 0 开始计数</td>\n</tr>\n<tr>\n<td>Window list</td>\n<td>一个 session 中的 window 列表，按编号的大小排序</td>\n</tr>\n<tr>\n<td>Window name</td>\n<td>window 的名称，默认是当前 active pane 中运行着的程序的名称</td>\n</tr>\n<tr>\n<td>Window index</td>\n<td>window 在一个 session 的 window list 中的编号</td>\n</tr>\n<tr>\n<td>Window layout</td>\n<td>window 中的所有 pane 中的尺寸和位置</td>\n</tr>\n</tbody></table>\n<h1>交互式（命令行）</h1>\n<p>当一个 tmux 的 client 被 attach，屏幕底部将显示一条状态栏</p>\n<p><img src=\"https://i0.wp.com/raw.githubusercontent.com/wiki/tmux/tmux/images/tmux_status_line_diagram.png\" alt=\"tmux_status_line_diagram\"></p>\n<h2>session</h2>\n<pre><code class=\"language-sh\"># 创建一个 session 并 attach\ntmux new\ntmux new-session\n\n# session 默认从 0 开始命名，可以手动指定名称\ntmux new -s my_session_name\n\n# 默认情况下 tmux 会 attach 到新建的 session，并运行 shell\n# 可以给定一个命令，tmux 就会将其传递给 shell\n# 注：不会新建 session，也不会 attach\ntmux new &#39;emacs ~/.tmux.conf&#39;\n# or\ntmux new -- emacs ~/.tmux.conf\n\n# 默认情况下 tmux 会在 session 中的第一个 window 中运行命令\n# 可以给定 -n 选项，指定一个 window 来运行命令\ntmux new -n mytopwindow -- top\n</code></pre>\n<h1>attach 和 detach</h1>\n<p>detach 意味着 client 存在的情况下从终端外部分离，回到 shell 并使 tmux 中运行的程序在后台运行</p>\n<pre><code class=\"language-sh\"># C-b d\ntmux detach\n\ntmux attach\ntmux attach-session\n\n# attach 到指定 session\ntmux attach -t &lt;session-name&gt;\n\n# -d 选项，attach 到终端时使其它 client 被 detach\ntmux attach -dt &lt;session-name&gt;\n\n# new-session 子命令提供 -A 选项，允许 attach 到指定 session\n# 若指定 session 不存在则创建\ntmux new -As &lt;session-name&gt;\n\n# 列出所有可被 attach 的 session\ntmux ls\ntmux list-session\n</code></pre>\n<pre><code class=\"language-sh\"># 若 tmux 中没有 session、window 和 pane，tmux 将自动 exit\n# 也可以使用此命令关闭\ntmux kill-server\n\ntmux kill-session -t &lt;session-name&gt;\n\ntmux switch -t &lt;session-name&gt;\n\ntmux swap-window\n\ntmux move-window\n\ntmux swap-pane\n\n# 重命名 session\n# C-b $\ntmux rename-session -t &lt;session-name&gt; &lt;new-name&gt;\n\n# 重命名 window\n# C-b ,\ntmux rename-window -t &lt;window-name&gt; &lt;new-name&gt;\n</code></pre>\n<pre><code class=\"language-sh\"># 列出所有子目录\ntmux list-commands\n\n# 列出所有快捷键\ntmux list-keys\n</code></pre>\n<h2>predix key</h2>\n<p>当一个 tmux 的 client 被 attach，键盘输入将转发到 current window 中的 active pane 中运行的程序。<br>prefix key 是特殊的快捷键，可用于控制 tmux 自身</p>\n<blockquote>\n<p>注：C=<code>Ctrl</code> M=Meta=<code>Alt</code></p>\n</blockquote>\n<p>tmux 的快捷键前缀是 C-b，按下 Ctrl+B 并<strong>松开</strong><br>再按下 <code>?</code> 可以查看快捷键列表<br>按两次 C-b 可向程序本身发送一次 Ctrl+B</p>\n<h1>分割 window</h1>\n<pre><code class=\"language-sh\"># 默认上下分\ntmux splitw\ntmux split-window\n\n# 上下分\n#  C-b &quot;\ntmux splitw -v\n\n# 左右分\n# C-b %\ntmux splitw -h\n</code></pre>\n<pre><code class=\"language-sh\"># 打印 pane 各自的序号\n# C-b q\ntmux display-panes\n\n# 切换到指定序号\n# C-b q &lt;序号&gt;\ntmux select-pane &lt;序号&gt;\n\n# 通过上下左右键切换 pane\n# C-b Up  C-b Down  C-b Left  C-b Right\n\n# 切换到下一个 pane\n# C-b o\n\n# 将下一个 pane 的内容与 当前 pane 交换\n# C-b C-o\n\n# kill 当前 window（包含所有 pane）\n# C-b &amp;\ntmux kill-window\n\n# kill 选中的 pane\n# C-b x\ntmux kill-pane\n</code></pre>\n<p>树模式将窗口分为上下两个部分：<br>上面是 session-window-pane 树<br>下面是 pane 的预览区域</p>\n<pre><code class=\"language-sh\"># 进入 tree 模式\ntmux choose-tree\n# C-b s\n# C-b w\n</code></pre>\n<table>\n<thead>\n<tr>\n<th>Key</th>\n<th>Function</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>Up</code> <code>Down</code></td>\n<td>在树中导航</td>\n</tr>\n<tr>\n<td><code>Left</code></td>\n<td>折叠</td>\n</tr>\n<tr>\n<td><code>Right</code></td>\n<td>展开</td>\n</tr>\n<tr>\n<td><code>Enter</code></td>\n<td>进入到选择的项目（变为被 attach 的 session）</td>\n</tr>\n<tr>\n<td><code>O</code></td>\n<td>改变项目的顺序</td>\n</tr>\n<tr>\n<td><code>q</code></td>\n<td>退出</td>\n</tr>\n<tr>\n<td><code>t</code></td>\n<td>选中（tag）或取消选中当前项目</td>\n</tr>\n<tr>\n<td><code>T</code></td>\n<td>全部取消选中</td>\n</tr>\n<tr>\n<td><code>C-t</code></td>\n<td>选中所有项目</td>\n</tr>\n<tr>\n<td><code>x</code></td>\n<td>kill 当前项目</td>\n</tr>\n<tr>\n<td><code>X</code></td>\n<td>kill 所有被 tag 的项目</td>\n</tr>\n<tr>\n<td><code>v</code></td>\n<td>打开或关闭下半部分的预览</td>\n</tr>\n</tbody></table>\n<h1>detach 其它 client</h1>\n<pre><code class=\"language-sh\"># 进入 client 模式\n# C-b D\n</code></pre>\n<table>\n<thead>\n<tr>\n<th>Key</th>\n<th>Function</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>Enter</code></td>\n<td>detach 当前 client</td>\n</tr>\n<tr>\n<td><code>d</code></td>\n<td>同上</td>\n</tr>\n<tr>\n<td><code>D</code></td>\n<td>detach 选中的（tagged）client</td>\n</tr>\n<tr>\n<td><code>x</code></td>\n<td>detach 当前 client 并尝试 kill 其启动的 shell</td>\n</tr>\n<tr>\n<td><code>X</code></td>\n<td>detach 选中的（tagged） client 并尝试 kill 其启动的 shell</td>\n</tr>\n</tbody></table>\n<h1>调整 pane 大小</h1>\n<pre><code class=\"language-sh\">tmux resize-pane\n</code></pre>\n<p>调小 <code>C-b C-Left</code> <code>C-b C-Right</code> <code>C-b C-Up</code> <code>C-b C-Down</code></p>\n<p>调大 <code>C-b M-Left</code> <code>C-b M-Right</code> <code>C-b M-Up</code> <code>C-b M-Down</code></p>\n<p>``</p>\n<table>\n<thead>\n<tr>\n<th>Key</th>\n<th>Function</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>C-b z</code></td>\n<td>将当前全屏，再按一次退出全屏</td>\n</tr>\n<tr>\n<td><code>C-b Space</code></td>\n<td>旋转布局</td>\n</tr>\n</tbody></table>\n<table>\n<thead>\n<tr>\n<th>Name</th>\n<th>Key</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>even-horizontal</td>\n<td><code>C-b M-1</code></td>\n<td>Spread out evenly across</td>\n</tr>\n<tr>\n<td>even-vertical</td>\n<td><code>C-b M-2</code></td>\n<td>Spread out evenly up and down</td>\n</tr>\n<tr>\n<td>main-horizontal</td>\n<td><code>C-b M-3</code></td>\n<td>One large pane at the top, the rest spread out evenly across</td>\n</tr>\n<tr>\n<td>main-vertical</td>\n<td><code>C-b M-4</code></td>\n<td>One large pane on the left, the rest spread out evenly up and down</td>\n</tr>\n<tr>\n<td>tiled</td>\n<td><code>C-b M-5</code></td>\n<td>Tiled in the same number of rows as columns</td>\n</tr>\n</tbody></table>\n<h1>command prompt</h1>\n<p><code>C-b :</code></p>\n<p><img src=\"https://i0.wp.com/github.com/tmux/tmux/wiki/images/tmux_command_prompt.png\" alt=\"tmux_command_prompt\"></p>\n<p>支持鼠标操作（右键）</p>\n<pre><code class=\"language-sh\">tmux set-option mouse on\n# :set -g mouse on\n</code></pre>\n","tags":["linux","cli"]},{"id":"golang-generics","url":"https://yieldray.fun/posts/golang-generics","title":"golang泛型","date_published":"2023-05-16T10:30:00.000Z","date_modified":"2023-05-16T10:30:00.000Z","content_text":"<h1>泛型与类型约束</h1>\n<pre><code class=\"language-go\">package main\n\nimport &quot;fmt&quot;\n\nvar ints = map[string]int64{&quot;0&quot;: 8, &quot;1&quot;: 0, &quot;2&quot;: 8, &quot;3&quot;: 6}\nvar floats = map[string]float64{&quot;k1&quot;: 1.1, &quot;k2&quot;: 2.2}\n\n// comparable 是预定义的类型约束\nfunc SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {\n    var s V\n    for _, v := range m {\n        s += v\n    }\n    return s\n}\n\nfunc init() {\n    // 手动指定泛型参数\n    fmt.Printf(&quot;Generic Sums: %v and %v\\n&quot;,\n        SumIntsOrFloats[string, int64](ints),\n        SumIntsOrFloats[string, float64](floats))\n\n    // 编译器自动推导泛型参数\n    fmt.Printf(&quot;Generic Sums, type parameters inferred: %v and %v\\n&quot;,\n        SumIntsOrFloats(ints),\n        SumIntsOrFloats(floats))\n}\n\n// 下面的语法可以定义一个类型约束\ntype Number interface {\n    int64 | float64\n}\n\n// 使用自定义类型约束\nfunc SumNumbers[K comparable, V Number](m map[K]V) V {\n    var s V\n    for _, v := range m {\n        s += v\n    }\n    return s\n}\n\nfunc init() {\n    fmt.Printf(&quot;Generic Sums with Constraint: %v and %v\\n&quot;,\n        SumNumbers(ints),\n        SumNumbers(floats))\n}\n\nfunc main() {}\n</code></pre>\n","tags":["golang"]},{"id":"golang-std-note","url":"https://yieldray.fun/posts/golang-std-note","title":"golang标准库笔记","date_published":"2023-05-14T12:00:00.000Z","date_modified":"2023-05-14T12:00:00.000Z","content_text":"<p>中文参考：<a href=\"https://studygolang.com/pkgdoc\">https://studygolang.com/pkgdoc</a></p>\n<h1><a href=\"https://pkg.go.dev/builtin\">builtin</a></h1>\n<pre><code class=\"language-go\">package main\n\nimport (\n    &quot;fmt&quot;\n    &quot;runtime&quot;\n)\n\nfunc thread() {\n    defer func() {\n        if err := recover(); err != nil {\n            fmt.Println(err)\n        }\n    }()\n    // 在 recover 函数之后的 panic 才会被捕获\n    panic(&quot;Oops! panic&quot;)\n}\n\nfunc main() {\n    go thread() // recover函数仅对当前goroutine有效\n\n    defer func() { // recover函数必须置于defer函数内，且在panic代码之前\n        if err := recover(); err != nil {\n\n            switch err.(type) {\n            case runtime.Error:\n                fmt.Println(&quot;[runtime.Error]&quot;, err)\n            default:\n                fmt.Println(&quot;[Error]:&quot;, err)\n            }\n\n        }\n    }()\n\n    var ptr *int\n    fmt.Println(*ptr) // 访问空指针\n}\n</code></pre>\n<h1><a href=\"https://pkg.go.dev/fmt\">fmt</a></h1>\n<pre><code class=\"language-go\">fmt.Print(x)\n// 相当于\nfmt.Printf(&quot;%v&quot;, x)\n</code></pre>\n<pre><code class=\"language-go\">package main\n\nimport &quot;fmt&quot;\n\nfunc echo(format string, a ...any) {\n    // fmt.Printf(&quot;%-20s %-6s&quot;, fmt.Sprint(a...), format)\n    fmt.Printf(format, a...)\n    fmt.Println()\n}\n\ntype TypeStruct struct {\n    Public  string\n    private string\n}\n\nfunc main() {\n    t := TypeStruct{&quot;PUBLIC&quot;, &quot;PRIVATE&quot;}\n    echo(&quot;%v&quot;, t)  // {PUBLIC PRIVATE}\n    echo(&quot;%+v&quot;, t) // {Public:PUBLIC private:PRIVATE}\n    echo(&quot;%#v&quot;, t) // main.TypeStruct{Public:&quot;PUBLIC&quot;, private:&quot;PRIVATE&quot;}\n    echo(&quot;%T&quot;, t)  // main.TypeStruct\n    // 注：匿名结构体的输出有所不同，此处略\n    // s := struct{ Public string; private string } {&quot;PUBLIC&quot;, &quot;PRIVATE&quot;}\n\n    n := 42\n    echo(&quot;%c&quot;, n) // * （Unicode）\n    echo(&quot;%q&quot;, n) // &#39;*&#39;\n    echo(&quot;%U&quot;, n) // U+002A\n    echo(&quot;%b&quot;, n) // 101010 （二进制）\n    echo(&quot;%d&quot;, n) // 42 （十进制）\n    echo(&quot;%o&quot;, n) // 52 （八进制）\n    echo(&quot;%O&quot;, n) // 0o52 （八进制）\n    echo(&quot;%x&quot;, n) // 2a （十六进制）\n    echo(&quot;%X&quot;, n) // 2A\n\n    f := 12.34\n    echo(&quot;%b&quot;, f) // 6946802425218990p-49\n    echo(&quot;%e&quot;, f) // 1.234000e+01\n    echo(&quot;%E&quot;, f) // 1.234000E+01\n    echo(&quot;%f&quot;, f) // 12.340000\n    echo(&quot;%F&quot;, f) // 12.340000 （同上，没有区别）\n    echo(&quot;%g&quot;, f) // 12.34\n    echo(&quot;%G&quot;, f) // 12.34\n    echo(&quot;%x&quot;, f) // 0x1.8ae147ae147aep+03\n    echo(&quot;%X&quot;, f) // 0X1.8AE147AE147AEP+03\n\n    s := &quot;golang&quot;\n    echo(&quot;%s&quot;, s) // golang\n    echo(&quot;%q&quot;, s) // &quot;golang&quot;\n    echo(&quot;%x&quot;, s) // 676f6c616e67\n    echo(&quot;%X&quot;, s) // 676F6C616E67\n    // 注：若 s := []byte(&quot;golang&quot;) 结果相同\n\n    a := []int{2, 3, 3} // 注意这是切片不是数组\n    echo(&quot;%p&quot;, a)       // 0xc0000aa0f0 （地址不是固定的）\n\n    echo(&quot;%t&quot;, true)  // true\n    echo(&quot;%t&quot;, false) // false\n\n    echo(&quot;%v&quot;, false)                  // 相当于 %t\n    echo(&quot;%v&quot;, int(-42))               // 相当于 %d\n    echo(&quot;%v&quot;, uint(42))               // 相当于 %d\n    echo(&quot;%v&quot;, float32(12.34))         // 相当于 %g\n    echo(&quot;%v&quot;, s)                      // 相当于 %s\n    echo(&quot;%v&quot;, make(chan interface{})) // 相当于 %p\n    echo(&quot;%v&quot;, &amp;s)                     // 相当于 %p\n}\n</code></pre>\n<p><code>%[标志][最小宽度][.精度][长度]类型</code><br>精度以 Unicode 码点计数（而不是像 C 一样以字节计数）</p>\n<h1><a href=\"https://pkg.go.dev/errors\">errors</a></h1>\n<pre><code class=\"language-go\">package main\n\nimport (\n    &quot;errors&quot;\n    &quot;fmt&quot;\n    &quot;io/fs&quot;\n    &quot;os&quot;\n)\n\nfunc main() {\n    // func New(text string) error\n    // 给定字符串生成（独一无二的）错误\n    // 参见：fmt.Errorf()\n    err := errors.New(&quot;emit macho dwarf: elf header corrupted&quot;)\n    if err != nil {\n        fmt.Print(err)\n    }\n\n    // func As(err error, target any) bool\n    // 将错误 err 断言为错误 target，返回是否断言成功\n    if _, err := os.Open(&quot;non-existing&quot;); err != nil {\n        var pathError *fs.PathError\n        if errors.As(err, &amp;pathError) {\n            fmt.Println(&quot;Failed at path:&quot;, pathError.Path)\n        } else {\n            fmt.Println(err)\n        }\n    }\n\n    // func Is(err, target error) bool\n    // 判断错误 err 是否为错误 target\n    if _, err := os.Open(&quot;non-existing&quot;); err != nil {\n        if errors.Is(err, fs.ErrNotExist) {\n            fmt.Println(&quot;file does not exist&quot;)\n        } else {\n            fmt.Println(err)\n        }\n    }\n\n    // func Join(errs ...error) error\n    // 提供包装给定的多个错误生成新错误\n    // 在给定的错误中，是 nil 的错误会被丢弃\n    err1 := errors.New(&quot;err1&quot;)\n    err2 := errors.New(&quot;err2&quot;)\n    err3 := errors.Join(err1, err2)\n    fmt.Println(err3) // 该错误的字符串是其包装的错误字符串之间加上换行\n    if errors.Is(err3, err1) {\n        fmt.Println(&quot;err3 is err1&quot;)\n    }\n    if errors.Is(err3, err2) {\n        fmt.Println(&quot;err3 is err2&quot;)\n    }\n\n    // func Unwrap(err error) error\n    // 若给定错误 err 对象实现了方法 Unwrap()，则调用该方法返回错误\n    // 否则，返回 nil\n    err4 := errors.New(&quot;error4&quot;)\n    err5 := fmt.Errorf(&quot;error5: %w&quot;, err4)   // 参见：https://pkg.go.dev/fmt#Errorf\n    fmt.Println(errors.Unwrap(err5) == err4) // true\n}\n</code></pre>\n<h1><a href=\"https://pkg.go.dev/io\">io</a></h1>\n<pre><code class=\"language-go\">type Reader interface {\n    // 读取最多 len(p) 个字节到 p，返回读取的字节数 0 &lt;= n &lt;= len(p) 及任何遇到的错误（如果有）\n    // 即使 n &lt; len(p) 也可能在调用期间 p 的所有空间作为暂存空间\n    // 并且，此时 Read() 一般直接返回读取的数据（p 未填满），而不会继续等待更多的数据（以填满 p）\n    Read(p []byte) (n int, err error)\n}\n\ntype Writer interface {\n    // 写入 len(p) 个字节到底层的数据流，返回写入的字节数 0 &lt;= n &lt;= len(p) 及任何遇到的错误（如果有）\n    // 若返回的 n &lt; len(p) 则必须返回非 nil 的错误\n    // Write() 不允许修改切片 p 中的数据（如果要实现此接口）\n    Write(p []byte) (n int, err error)\n}\n\ntype Closer interface {\n    // 包装基本的关闭方法。重复调用此方法的行为是未定义的\n    Close() error\n}\n\nconst (\n    SeekStart   = 0 // 相对于文件起始位置\n    SeekCurrent = 1 // 相对于当前位置\n    SeekEnd     = 2 // 相对于文件结尾 (for example, offset = -2 specifies the penultimate byte of the file)\n)\n\ntype Seeker interface {\n    // 设定下一次读写的偏移量为 offset\n    // whence 可取常量 SeekStart SeekCurrent SeekEnd（其含义参见上面的解释）\n    // 返回新的偏移量（相对于文件起始位置）及任何可能的错误\n    Seek(offset int64, whence int) (int64, error)\n}\n</code></pre>\n<blockquote>\n<p>注意：<code>io/ioutil</code> 包已废弃。其功能拆分到了 <code>io</code> 和 <code>os</code> 包里</p>\n</blockquote>\n<h1><a href=\"https://pkg.go.dev/os\">os</a></h1>\n<p>os 包主要提供了文件。进程相关的跨平台 API</p>\n<p>参见 <a href=\"https://pkg.go.dev/os#File\">File</a> 类方法</p>\n<pre><code class=\"language-go\">func Open(name string) (*File, error)\n// 文件描述符是 O_RDONLY。若发生错误，返回的错误类型是 *PathError\n\nfunc Create(name string) (*File, error)\n// 若文件已存在，会被清空。否则将以 0666 模式创建（before umask）。\n// 文件描述符是 O_RDWR。若发生错误，返回的错误类型是 *PathError\n\ntype PathError struct {\n    Op   string\n    Path string\n    Err  error\n}\n</code></pre>\n<p>参见 <a href=\"https://pkg.go.dev/os#Process\">Process</a> 类方法及 <a href=\"https://pkg.go.dev/os#ProcessState\">ProcessState</a> 类方法</p>\n<pre><code class=\"language-go\">func FindProcess(pid int) (*Process, error)\nfunc StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)\n// 一般使用 os/exec 包提供的高层接口，而不是此函数\n</code></pre>\n<p>常量</p>\n<pre><code class=\"language-go\">// 文件描述符，类型是 uintptr。例如：func NewFile(fd uintptr, name string) *File\nconst (\n    // 必须指定下面三个之中的一个\n    O_RDONLY int = syscall.O_RDONLY // open the file read-only.\n    O_WRONLY int = syscall.O_WRONLY // open the file write-only.\n    O_RDWR   int = syscall.O_RDWR   // open the file read-write.\n    // The remaining values may be or&#39;ed in to control behavior.\n    O_APPEND int = syscall.O_APPEND // append data to the file when writing.\n    O_CREATE int = syscall.O_CREAT  // create a new file if none exists.\n    O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.\n    O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.\n    O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.\n)\n// 指定如何解释偏移量。例如：func (f *File) Seek(offset int64, whence int) (ret int64, err error)\n// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.\nconst (\n    SEEK_SET int = 0 // 相当于文件起始\n    SEEK_CUR int = 1 // 相对于当前偏移量\n    SEEK_END int = 2 // 相对于文件结尾\n)\nconst (\n    PathSeparator     = &#39;/&#39; // OS-specific path separator\n    PathListSeparator = &#39;:&#39; // OS-specific path list separator\n)\n</code></pre>\n<h1><a href=\"https://pkg.go.dev/io/fs\">io/fs</a></h1>\n<pre><code class=\"language-go\">type File interface {\n    Stat() (FileInfo, error)\n    Read([]byte) (int, error)\n    Close() error\n}\ntype FileInfo interface {\n    Name() string       // 文件名，basename\n    Size() int64        // length in bytes for regular files; system-dependent for others\n    Mode() FileMode     // file mode bits\n    ModTime() time.Time // 修改时间\n    IsDir() bool        // 即 Mode().IsDir()\n    Sys() any           // underlying data source (can return nil)\n}\ntype DirEntry interface {\n    // 返回文件或目录名。仅名称，非完整路径\n    Name() string\n    IsDir() bool\n    Type() FileMode\n    // 返回子项（文件或子目录）的信息。返回的 FileInfo 既可能在读取父目录时发生，也可能在调用Info方法时发生\n    // 若文件在父目录被读取后被删除或重命名，Info方法可能返回一个满足 errors.Is(err, ErrNotExist) 的错误\n    // 若文件是符号链接，info方法返回符号链接自身的信息\n    Info() (FileInfo, error)\n}\n\n// 具体参见 FileMode 类方法\n// https://pkg.go.dev/io/fs#FileMode\ntype FileMode uint32\nconst (\n    // The single letters are the abbreviations\n    // used by the String method&#39;s formatting.\n    ModeDir        FileMode = 1 &lt;&lt; (32 - 1 - iota) // d: is a directory\n    ModeAppend                                     // a: append-only\n    ModeExclusive                                  // l: exclusive use\n    ModeTemporary                                  // T: temporary file; Plan 9 only\n    ModeSymlink                                    // L: symbolic link\n    ModeDevice                                     // D: device file\n    ModeNamedPipe                                  // p: named pipe (FIFO)\n    ModeSocket                                     // S: Unix domain socket\n    ModeSetuid                                     // u: setuid\n    ModeSetgid                                     // g: setgid\n    ModeCharDevice                                 // c: Unix character device, when ModeDevice is set\n    ModeSticky                                     // t: sticky\n    ModeIrregular                                  // ?: non-regular file; nothing else is known about this file\n\n    // Mask for the type bits. For regular files, none will be set.\n    ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular\n\n    ModePerm FileMode = 0777 // Unix permission bits\n)\n</code></pre>\n<h1><a href=\"https://pkg.go.dev/path\">path</a></h1>\n<p>注意：path 包用于处理正斜线分隔路径（及 URL）<br>跨平台时使用 path/filepath 包，该包还提供了正反斜线转换的函数<br>此外还提供了用于递归遍历目录的 <a href=\"https://pkg.go.dev/path/filepath#Walk\"><code>Walk()</code> 和 <code>WalkDir()</code> 函数</a></p>\n<h1><a href=\"https://pkg.go.dev/strings\">strings</a></h1>\n<p>Builder 类可用于构造字符串</p>\n<pre><code class=\"language-go\">var sb strings.Builder\nsb.WriteString(&quot;hello,&quot;)\nsb.Write([]byte(&quot;world&quot;))\nfmt.Print(sb.String())\n</code></pre>\n<h1><a href=\"https://pkg.go.dev/bytes\">bytes</a></h1>\n<p>bytes 包类似于 strings 包，提供了 byte 切片（<code>[]byte</code>）相关的实用函数</p>\n<h1><a href=\"https://pkg.go.dev/strconv\">strconv</a></h1>\n<p>strconv 包实现了基本数据类型与字符串之间的转换</p>\n<h1><a href=\"https://pkg.go.dev/json\">encoding/json</a></h1>\n<p>参见：<a href=\"https://go.dev/blog/json\">https://go.dev/blog/json</a></p>\n<p><a href=\"https://pkg.go.dev/encoding/json#Marshal\"><code>func Marshal(v any) ([]byte, error)</code></a></p>\n<p><a href=\"https://pkg.go.dev/encoding/json#Unmarshal\"><code>func Unmarshal(data []byte, v any) error</code></a></p>\n<h1><a href=\"https://pkg.go.dev/time\">time</a></h1>\n<p>此处略：Duration<br>参见：<a href=\"https://go.dev/blog/concurrency-timeouts\">https://go.dev/blog/concurrency-timeouts</a></p>\n<h2><a href=\"https://pkg.go.dev/time#Location\">Location</a></h2>\n<pre><code class=\"language-go\">var Local *Location = &amp;localLoc\nvar UTC *Location = &amp;utcLoc\nfunc FixedZone(name string, offset int) *Location\n\nutc_negative_8 := time.FixedZone(&quot;UTC-8&quot;, -8*60*60)\n</code></pre>\n<h2><a href=\"https://pkg.go.dev/time#Time\">Time</a></h2>\n<pre><code class=\"language-go\">func Now() Time\nfunc Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time\nfunc Unix(sec int64, nsec int64) Time\nfunc UnixMicro(usec int64) Time\nfunc UnixMilli(msec int64) Time\nfunc Parse(layout, value string) (Time, error)\nfunc (t Time) Format(layout string) string\n</code></pre>\n<p>layout 可取（或者说格式为）</p>\n<pre><code class=\"language-go\">const (\n    Layout      = &quot;01/02 03:04:05PM &#39;06 -0700&quot; // The reference time, in numerical order.\n    ANSIC       = &quot;Mon Jan _2 15:04:05 2006&quot;\n    UnixDate    = &quot;Mon Jan _2 15:04:05 MST 2006&quot;\n    RubyDate    = &quot;Mon Jan 02 15:04:05 -0700 2006&quot;\n    RFC822      = &quot;02 Jan 06 15:04 MST&quot;\n    RFC822Z     = &quot;02 Jan 06 15:04 -0700&quot; // RFC822 with numeric zone\n    RFC850      = &quot;Monday, 02-Jan-06 15:04:05 MST&quot;\n    RFC1123     = &quot;Mon, 02 Jan 2006 15:04:05 MST&quot;\n    RFC1123Z    = &quot;Mon, 02 Jan 2006 15:04:05 -0700&quot; // RFC1123 with numeric zone\n    RFC3339     = &quot;2006-01-02T15:04:05Z07:00&quot;\n    RFC3339Nano = &quot;2006-01-02T15:04:05.999999999Z07:00&quot;\n    Kitchen     = &quot;3:04PM&quot;\n    // Handy time stamps.\n    Stamp      = &quot;Jan _2 15:04:05&quot;\n    StampMilli = &quot;Jan _2 15:04:05.000&quot;\n    StampMicro = &quot;Jan _2 15:04:05.000000&quot;\n    StampNano  = &quot;Jan _2 15:04:05.000000000&quot;\n    DateTime   = &quot;2006-01-02 15:04:05&quot;\n    DateOnly   = &quot;2006-01-02&quot;\n    TimeOnly   = &quot;15:04:05&quot;\n)\n</code></pre>\n<h2><a href=\"https://pkg.go.dev/time#Timer\">Timer</a></h2>\n<pre><code class=\"language-go\">type Timer struct {\n    C &lt;-chan Time\n    // contains filtered or unexported fields\n}\n</code></pre>\n<p>Timer 类型表示一个单一的事件。当 Timer（计时器）到期时，当前时间会发送到信道 C（除非这个 Timer 是通过 AfterFunc 函数创建的）<br>Timer 类必须通过 NewTimer 或 AfterFunc 函数创建。</p>\n<h2><a href=\"https://pkg.go.dev/time#Ticker\">Ticker</a></h2>\n<pre><code class=\"language-go\">type Ticker struct {\n    C &lt;-chan Time // 这是 tick 将要发送到的信道\n    // contains filtered or unexported fields\n}\n</code></pre>\n<p>Ticker 类型持有一个信道，会定期向该信道发送 ticks。<br>注：通过 NewTicker 函数创建</p>\n<h1><a href=\"https://pkg.go.dev/regexp\">regexp</a></h1>\n<pre><code class=\"language-go\">func main() {\n    matched, err := regexp.MatchString(`^[A-Z]+?[A-Za-z]*$`, &quot;Admin&quot;)\n    fmt.Println(matched, err) // true &lt;nil&gt;\n}\n</code></pre>\n<p>对于更复杂的正则表达式，应构造 Regexp 对象</p>\n<pre><code class=\"language-go\">func Compile(expr string) (*Regexp, error)\nfunc MustCompile(str string) *Regexp\nfunc (re *Regexp) FindStringSubmatch(s string) []string\n</code></pre>\n<h1><a href=\"https://pkg.go.dev/embed\">embed</a></h1>\n<p><code>//go:embed</code> 指令可被用于导出和非导出变量，取决于使用此指令的包是否要向其它包公开此变量。此指令只能用于包作用域的变量，不能用于局部变量。</p>\n<pre><code class=\"language-go\">import _ &quot;embed&quot;\n\n//go:embed hello.txt\nvar s string\nprint(s)\n\n//go:embed hello.txt\nvar b []byte\nprint(string(b))\n\n//go:embed hello.txt\nvar f embed.FS\ndata, _ := f.ReadFile(&quot;hello.txt&quot;)\nprint(string(data))\n\n// The difference is that ‘image/*’ embeds ‘image/.tempfile’ while ‘image’ does not.\n// Neither embeds ‘image/dir/.tempfile’.\nimport &quot;embed&quot;\n//go:embed image/* template/*\n//go:embed html/index.html\nvar content embed.FS\n//go:embed image template html/index.html\nvar content embed.FS\n// If a pattern begins with the prefix ‘all:’, then the rule for walking directories is changed to include those files beginning with ‘.’ or ‘_’.\n// For example, ‘all:image’ embeds both ‘image/.tempfile’ and ‘image/dir/.tempfile’.\n</code></pre>\n<pre><code class=\"language-go\">func (f FS) Open(name string) (fs.File, error)\nfunc (f FS) ReadDir(name string) ([]fs.DirEntry, error)\nfunc (f FS) ReadFile(name string) ([]byte, error)\n</code></pre>\n<h1><a href=\"https://pkg.go.dev/context\">context</a></h1>\n<p>参见：<a href=\"https://go.dev/blog/context\">Go Concurrency Patterns: Context</a></p>\n<h1><a href=\"https://pkg.go.dev/sync\">sync</a></h1>\n<p>sync 包提供了基本的同步原语</p>\n<h1><a href=\"https://pkg.go.dev/reflect\">reflect</a></h1>\n<p>参见：<a href=\"https://go.dev/blog/laws-of-reflection\">https://go.dev/blog/laws-of-reflection</a></p>\n","tags":["golang"]},{"id":"cssom","url":"https://yieldray.fun/posts/cssom","title":"CSSOM","date_published":"2023-05-07T10:00:00.000Z","date_modified":"2023-05-07T10:00:00.000Z","content_text":"<h1><a href=\"https://developer.mozilla.org/docs/Web/API/CSS_Object_Model\">CSSOM</a></h1>\n<p><img src=\"https://www.plantuml.com/plantuml/svg/hLRDRkCs4BxxANYKKJiFOBJRNNGNMmF9feWMUmboeALnJ8GY1P9ALatoxXt96Pa_OfiYoIKczSqtCzyCX_ne7JUkQoJxeomlBnOBeHoOFQ-0VTPLru1obwOyy5e_N6djsEE2uTylk6uTrANh9PG706St0y5fVNrp11f0g98zipxqX3iNrYsU5xE-rki8E0_Jwz2rbqDaJz5Vbtspq_9C88mxRMwAvOPjXyLEmlPbyzU0z0C66P6bVYl9hGqt6Vnqe6hBlAt7I9hAsgjEmb72uTT07s7qWm8pV9j0x8cfJie8Pp942XuAB9dGoluUbMghX5j6UrJbUc0fB4Yed3PeQPqHwdPrBAUt5gg6dxXMNRC3iwGVHvuQxHsCL2FEWzrhKHz1GbamRWGDIRoIsa2ZxucQS0ctWLRo2Wg7rI1Y1FnbT2Cin9NsJSfULO4PyJul0YjiLbzqweMqwhMmGvVL6xRJMW9NKG4F1lRaQxx43TI2RzZ5y2_NATzqZGf-aJ2S5gJ1J2km5Zct2hcxQkY4UVgM6xGv1HvrnhoH4-vKiWoaRoS4gXTAHybCmj56Ubeag5jt2BjWbEIxLwNZjjFbk2gMuJvlMr3ruA9hYcRy5vNNxzl_nWa7JTpPSrfS1vYRCAZMQAS7aVpIFgUDz0fHyb75jSm75q6dIAZBtnG6aVnh00H2rxvFFHsqA2AAfh8sROMelUD9-cxd0Ej76GIIVJMwQp6QmH_T2UJ_zHV2-uzIYZGACJFEI84u2pBriw5B8wv5_2OuwVePq8lXmsYQ0rxmzkNbWDEi1UDwtF7ci1TtJ9TWOXfACDw_mWWkCOAaH5Ry3M6Vtu8hsXTBRqfqAFbUAAZPqnJP3ouxcAm-IJbZ68BfENNWDiM6LybUco-yEXGLbtB7gxkzsh3YVY19E5Vi3YQQrKl7Rx8YBDbl7yOkMp5t4FQJkSLMuge_NObP-VGwz9tJUsggV27y4PveiM9dPsTZiXQp3boo9zBh-YOuBPK4RegPmP-m1nWBxgtkKy4IeVp7B5tdSIzoXyIxpa6PFoR_x-1LkccHhoR2Ofdl2io20iiSFkzpJ99-InrGh_pv_U9yn90DzKfE5viVsnXqGhVElEVB7r_VqMtC5dlripqNwkvzlCPiEQ-ukMrQRLpcdig_LQDFalTWll46tlIMjHHWFhCmuZIlZvX1ryX_0000\" alt=\"plantuml\"></p>\n<p>实际上并没有单独的 <code>StyleSheet</code> 类出现，只有 <code>CSSStyleSheet</code>。<br>该类表示单个 CSS 样式表，样式表则是包含 <code>CSSRule</code> 的集合。<br><code>CSSStyleSheet</code> 是可直接构造的。如非手动通过 js 构造，则其<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#obtaining_a_stylesheet\">来源</a>如下：</p>\n<table>\n<thead>\n<tr>\n<th>来源</th>\n<th>存在于 <code>document.styleSheets</code></th>\n<th>获取 owner 元素或规则</th>\n<th>owner</th>\n<th>获取 CSSStyleSheet</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>&lt;style&gt;</code> 和 <code>&lt;link&gt;</code></td>\n<td>是</td>\n<td><code>.ownerNode</code></td>\n<td>对应 Element</td>\n<td><code>.sheet</code></td>\n</tr>\n<tr>\n<td>其它样式表中的 <code>@import</code> 规则</td>\n<td>是</td>\n<td><code>.ownerRule</code></td>\n<td>CSSImportRule</td>\n<td><code>.styleSheet</code></td>\n</tr>\n<tr>\n<td><code>&lt;?xml-stylesheet ?&gt;</code> 处理指令</td>\n<td>是</td>\n<td><code>.ownerNode</code></td>\n<td>ProcessingInstruction</td>\n<td><code>.sheet</code></td>\n</tr>\n<tr>\n<td>HTTP Link Header</td>\n<td>是</td>\n<td>无</td>\n<td>无</td>\n<td>无</td>\n</tr>\n<tr>\n<td>浏览器默认样式表</td>\n<td>否</td>\n<td>无</td>\n<td>无</td>\n<td>无</td>\n</tr>\n</tbody></table>\n<hr>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Document/styleSheets\">document.styleSheets</a>（<code>StyleSheetList</code>）只读，可以迭代为 <code>CSSStyleSheet</code>。</p>\n<pre><code class=\"language-ts\">interface StyleSheetList {\n    readonly length: number;\n    item(index: number): CSSStyleSheet | null;\n    [index: number]: CSSStyleSheet;\n}\n</code></pre>\n<hr>\n<blockquote>\n<p>Chrome&gt;=73<br><code>adoptedStyleSheets</code> 是 <code>DocumentOrShadowRoot</code> 接口上的一个属性</p>\n</blockquote>\n<pre><code class=\"language-ts\">/**\n * 即 Document 或 ShadowRoot\n *\n * @see https://github.com/microsoft/TypeScript/blob/16a36a71f13893bb0d99be45b9b0b3f53fa56a91/src/lib/dom.generated.d.ts#L7421\n */\ninterface DocumentOrShadowRoot {}\n\nconst shadowRoot: ShadowRoot = document.createElement(&quot;div&quot;).attachShadow({ mode: &quot;open&quot; });\nshadowRoot.adoptedStyleSheets; // CSSStyleSheet[]\n\nconst doc: Document = document;\ndoc.adoptedStyleSheets; // CSSStyleSheet[]\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Document/adoptedStyleSheets\">document.adoptedStyleSheets</a>（<code>CSSStyleSheet[]</code>）可读写，只包含通过 <code>CSSStyleSheet()</code> 构造函数构造的样式表。<br>（实际上是一个 Proxy）会按照 CSS 层叠算法与文档的其他样式表一起进行评估。</p>\n<hr>\n<p><code>CSSGroupingRule</code> 对应于 at-rule，嵌套其它 <code>CSSRules</code>。</p>\n<p><code>CSSStyleRule</code> 表示单个 CSS 样式规则，即表示最小的选择器规则。<br>它持有 <code>StylePropertyMap</code>，而 <code>StylePropertyMap</code> 就是一个 CSS 属性到 <code>CSSStyleValue</code> 数组的键值对。<br>（<code>StylePropertyMap</code> 是现代化的 <code>CSSStyleDeclaration</code> 替代）</p>\n<h1>可构造样式表</h1>\n<p>旧方法往往通过操纵 style 元素来操纵样式表，该方法可能产生重复的 css 代码，导致样式闪烁</p>\n<pre><code class=\"language-ts\">const style = document.createElement(&quot;style&quot;);\nstyle.innerHTML = `body {background:blue}`;\n\ndocument.head.append(style);\nconst sheet: CSSStyleSheet | null = style.sheet;\n// 注意 CSSStyleSheet 对象始终是对附加到文档的样式表的引用\n// 因此，必须保证 style 元素已附加到文档\n// style.sheet 才是 CSSStyleSheet\n// 否则 style.sheet 为 null\nconsole.log(sheet!);\n\nArray.from(document.styleSheets).some((ss) =&gt; ss === sheet); // =&gt; true\n</code></pre>\n<p>随着 ShadowDOM 的引入，现在可以手动构造 CSSStyleSheet 对象</p>\n<p>通过 adoptedStyleSheets 属性来附加构造的 CSSStyleSheet 对象，<br>样式表还可以在多个文档中共享引用，达到一处修改，多处同步的效果</p>\n<pre><code class=\"language-ts\">const sheet = new CSSStyleSheet();\nsheet.replaceSync(&quot;a { color: red; }&quot;);\n\ndocument.adoptedStyleSheets.push(sheet);\n\nconst node = document.createElement(&quot;div&quot;);\nconst shadow = node.attachShadow({ mode: &quot;open&quot; });\nshadow.adoptedStyleSheets.push(sheet);\n</code></pre>\n<p>参见：<a href=\"https://web.dev/articles/constructable-stylesheets\">https://web.dev/articles/constructable-stylesheets</a></p>\n<hr>\n<blockquote>\n<p>注意：</p>\n</blockquote>\n<p>下面给出一个简单示例：</p>\n<pre><code class=\"language-ts\">const sheet = new CSSStyleSheet(); // (Chrome&gt;=73)\nsheet.insertRule(`.selector0 { color: blue; }`);\n// 本质上是向 sheet.cssRules 插入 css 规则\n// 注意一次只能插入一条 CSSRule\n\n// 目前只支持插入字符串，不能直接插入 CSSRule 对象\n// const rule1 = new CSSStyleRule(); // TypeError: Illegal constructor\n// rule1.selectorText = &quot;.selector1&quot;;\n// rule1.style.color = &quot;red&quot;;\n// sheet.insertRule(rule1.cssText);\n\n// 要同时插入多条，可以使用 replace/replaceSync\nsheet.replaceSync(`\n    .selector0 { color: blue; }\n    .selector1 { color: red; }\n`);\n\nconsole.assert(sheet.cssRules.length === 2);\nconsole.assert(sheet.cssRules[0].cssText === &quot;.selector0 { color: blue; }&quot;);\nconsole.assert(sheet.cssRules[1].cssText === &quot;.selector1 { color: red; }&quot;);\n\nconst rule0: CSSRule = sheet.cssRules[0];\nconsole.assert(rule0 instanceof CSSStyleRule);\nconsole.assert((rule0 as CSSStyleRule).selectorText === &quot;.selector0&quot;);\n\nconst rule0Style: CSSStyleDeclaration = (rule0 as CSSStyleRule).style;\nconsole.assert(rule0Style.color === &quot;blue&quot;);\n\nconst rule0StyleMap: StylePropertyMap = (rule0 as CSSStyleRule).styleMap;\nconst rule0Color: CSSStyleValue = rule0StyleMap.get(&quot;color&quot;)!;\nconsole.assert(rule0Color.toString() === &quot;blue&quot;);\nrule0StyleMap.set(&quot;width&quot;, CSS.px(4));\nrule0StyleMap.set(&quot;height&quot;, new CSSUnitValue(4, &quot;px&quot;));\nrule0StyleMap.set(&quot;transform&quot;, CSSStyleValue.parse(&quot;transform&quot;, &quot;translate3d(10px,10px,0) scale(0.5)&quot;));\n</code></pre>\n","tags":["web-api","css"]},{"id":"web-stream","url":"https://yieldray.fun/posts/web-stream","title":"Web Streams API","date_published":"2023-05-06T16:00:00.000Z","date_modified":"2023-05-06T16:00:00.000Z","content_text":"<p>MDN: <a href=\"https://developer.mozilla.org/docs/Web/API/Streams_API\">https://developer.mozilla.org/docs/Web/API/Streams_API</a><br>WHATWG: <a href=\"https://streams.spec.whatwg.org/\">https://streams.spec.whatwg.org/</a><br>Node.js: <a href=\"https://nodejs.org/api/webstreams.html\">https://nodejs.org/api/webstreams.html</a><br>Deno: <a href=\"https://docs.deno.com/api/web/streams\">https://docs.deno.com/api/web/streams</a> <a href=\"https://docs.deno.com/api/node/stream/web/\">https://docs.deno.com/api/node/stream/web/</a><br>Bun: <a href=\"https://bun.com/reference/node/stream/web\">https://bun.com/reference/node/stream/web</a></p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/API/ReadableStream\">ReadableStream</a></h1>\n<pre><code class=\"language-ts\">interface ReadableStream&lt;R = any&gt; {\n    // Returns a boolean indicating whether or not the readable stream is locked to a reader.\n    readonly locked: boolean;\n    // Returns a Promise that resolves when the stream is canceled. Calling this method signals a loss of interest in the stream by a consumer. The supplied reason argument will be given to the underlying source, which may or may not use it.\n    cancel(reason?: any): Promise&lt;void&gt;;\n    // Creates a reader and locks the stream to it. While the stream is locked, no other reader can be acquired until this one is released.\n    getReader(options: { mode: &quot;byob&quot; }): ReadableStreamBYOBReader;\n    getReader(): ReadableStreamDefaultReader&lt;R&gt;;\n    getReader(options?: ReadableStreamGetReaderOptions): ReadableStreamReader&lt;R&gt;;\n    // Provides a chainable way of piping the current stream through a transform stream or any other writable/readable pair.\n    pipeThrough&lt;T&gt;(transform: ReadableWritablePair&lt;T, R&gt;, options?: StreamPipeOptions): ReadableStream&lt;T&gt;;\n    // Pipes the current ReadableStream to a given WritableStream and returns a Promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered.\n    pipeTo(destination: WritableStream&lt;R&gt;, options?: StreamPipeOptions): Promise&lt;void&gt;;\n    // The tee method tees this readable stream, returning a two-element array containing the two resulting branches as new ReadableStream instances. Each of those streams receives the same incoming data.\n    tee(): [ReadableStream&lt;R&gt;, ReadableStream&lt;R&gt;];\n}\n\ndeclare var ReadableStream: {\n    prototype: ReadableStream;\n    new (underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number }): ReadableStream&lt;Uint8Array&gt;;\n    new &lt;R = any&gt;(underlyingSource: UnderlyingDefaultSource&lt;R&gt;, strategy?: QueuingStrategy&lt;R&gt;): ReadableStream&lt;R&gt;;\n    new &lt;R = any&gt;(underlyingSource?: UnderlyingSource&lt;R&gt;, strategy?: QueuingStrategy&lt;R&gt;): ReadableStream&lt;R&gt;;\n};\n</code></pre>\n<h2>读取， getReader</h2>\n<p>目前只有两种 reader</p>\n<p>reader 可通过 异步迭代器 读取，参见 <a href=\"/posts/js-iteration/#%E5%BC%82%E6%AD%A5%E8%BF%AD%E4%BB%A3%E5%99%A8%E5%92%8C%E5%BC%82%E6%AD%A5%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%8D%8F%E8%AE%AE\">此处</a></p>\n<h3>ReadableStreamDefaultReader</h3>\n<pre><code class=\"language-js\">const reader = readableStream.getReader();\nconst { done, value } = await reader.read();\n</code></pre>\n<p>定义</p>\n<pre><code class=\"language-ts\">declare var ReadableStreamDefaultReader: {\n    prototype: ReadableStreamDefaultReader;\n    new &lt;R = any&gt;(stream: ReadableStream&lt;R&gt;): ReadableStreamDefaultReader&lt;R&gt;;\n};\n\ninterface ReadableStreamDefaultReader&lt;R = any&gt; extends ReadableStreamGenericReader {\n    read(): Promise&lt;ReadableStreamReadResult&lt;R&gt;&gt;;\n    releaseLock(): void;\n}\n\ntype ReadableStreamReadResult&lt;T&gt; = ReadableStreamReadValueResult&lt;T&gt; | ReadableStreamReadDoneResult&lt;T&gt;;\n\ninterface ReadableStreamReadDoneResult&lt;T&gt; {\n    done: true;\n    value?: T;\n}\n\ninterface ReadableStreamReadValueResult&lt;T&gt; {\n    done: false;\n    value: T;\n}\n</code></pre>\n<h3>ReadableStreamBYOBReader</h3>\n<p>BYOB: bing you own data 即自带缓冲区</p>\n<pre><code class=\"language-js\">const reader = readableStream.getReader({ mode: &quot;byob&quot; });\n</code></pre>\n<h2>迭代</h2>\n<p>ReadableStream 实现了异步可迭代协议</p>\n<pre><code class=\"language-js\">const response = await fetch(&quot;https://www.example.org&quot;);\nconst readableStream: ReadableStream&lt;Uint8Array&gt; = response.body;\nlet total = 0;\nfor await (const chunk of readableStream) total += chunk.length;\nconsole.log(total);\n</code></pre>\n<h2>构造 ReadableStream， UnderlyingSource</h2>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/ReadableStream/ReadableStream\">https://developer.mozilla.org/docs/Web/API/ReadableStream/ReadableStream</a></p>\n<pre><code class=\"language-ts\">interface UnderlyingDefaultSource&lt;R = any&gt; {\n    cancel?: UnderlyingSourceCancelCallback;\n    pull?: (controller: ReadableStreamDefaultController&lt;R&gt;) =&gt; void | PromiseLike&lt;void&gt;;\n    start?: (controller: ReadableStreamDefaultController&lt;R&gt;) =&gt; any;\n    type?: undefined;\n}\n\ninterface UnderlyingSourceCancelCallback {\n    (reason?: any): void | PromiseLike&lt;void&gt;;\n}\n\ninterface ReadableStreamDefaultController&lt;R = any&gt; {\n    // the desired size required to fill the stream&#39;s internal queue.\n    readonly desiredSize: number | null;\n    // Closes the associated stream.\n    close(): void;\n    // Enqueues a given chunk in the associated stream.\n    enqueue(chunk?: R): void;\n    // Causes any future interactions with the associated stream to error.\n    error(e?: any): void;\n}\n\ndeclare var ReadableStreamDefaultController: {\n    prototype: ReadableStreamDefaultController;\n    new (): ReadableStreamDefaultController;\n};\n</code></pre>\n<p>演示：</p>\n<pre><code class=\"language-ts\">let interval: number;\n\nconst underlyingSource: UnderlyingDefaultSource&lt;string&gt; = {\n    start(controller: ReadableStreamDefaultController&lt;string&gt;) {\n        let counter = 0;\n\n        interval = setInterval(() =&gt; {\n            // 随机生成一些字符串\n            let string = Math.random().toString().slice(2);\n\n            // Add the string to the stream\n            controller.enqueue(string);\n\n            if (++counter &gt;= 10) {\n                clearInterval(interval);\n                controller.close();\n            }\n        }, 300);\n    },\n    pull(controller) {\n        // We don&#39;t really need a pull in this example\n    },\n    cancel() {\n        // This is called if the reader cancels,\n        // so we should stop generating strings\n        clearInterval(interval);\n        // 在本演示中是可选的，因为调用controller.close之前已经clearInterval了\n    },\n};\n\nconst stream = new ReadableStream&lt;string&gt;(underlyingSource);\n\nfor await (const chunk of stream) {\n    console.log(chunk);\n}\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/API/WritableStream\">WriteableStream</a></h1>\n<pre><code class=\"language-ts\">interface WritableStream&lt;W = any&gt; {\n    // A boolean indicating whether the WritableStream is locked to a writer.\n    readonly locked: boolean;\n    // Aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded.\n    abort(reason?: any): Promise&lt;void&gt;;\n    // Closes the stream.\n    close(): Promise&lt;void&gt;;\n    // Returns a new instance of WritableStreamDefaultWriter and locks the stream to that instance. While the stream is locked, no other writer can be acquired until this one is released.\n    getWriter(): WritableStreamDefaultWriter&lt;W&gt;;\n}\n\ndeclare var WritableStream: {\n    prototype: WritableStream;\n    new &lt;W = any&gt;(underlyingSink?: UnderlyingSink&lt;W&gt;, strategy?: QueuingStrategy&lt;W&gt;): WritableStream&lt;W&gt;;\n};\n</code></pre>\n<h2>写入， getWriter</h2>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter\">https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter</a></p>\n<p>目前只有 WritableStreamDefaultWriter，直接调用 getWriter 方法获取</p>\n<pre><code class=\"language-js\">const writer = writeableStream.getWriter();\nawait writer.write(chunk);\n</code></pre>\n<p>定义：</p>\n<pre><code class=\"language-ts\">interface WritableStreamDefaultWriter&lt;W = any&gt; {\n    readonly closed: Promise&lt;undefined&gt;;\n    readonly desiredSize: number | null;\n    readonly ready: Promise&lt;undefined&gt;;\n    abort(reason?: any): Promise&lt;void&gt;;\n    close(): Promise&lt;void&gt;;\n    releaseLock(): void;\n    write(chunk?: W): Promise&lt;void&gt;;\n}\n\ndeclare var WritableStreamDefaultWriter: {\n    prototype: WritableStreamDefaultWriter;\n    new &lt;W = any&gt;(stream: WritableStream&lt;W&gt;): WritableStreamDefaultWriter&lt;W&gt;;\n};\n</code></pre>\n<h2>构造 WritableStream， UnderlyingSink</h2>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/WritableStream/WritableStream\">https://developer.mozilla.org/docs/Web/API/WritableStream/WritableStream</a></p>\n<pre><code class=\"language-ts\">interface UnderlyingByteSource {\n    autoAllocateChunkSize?: number;\n    cancel?: UnderlyingSourceCancelCallback;\n    pull?: (controller: ReadableByteStreamController) =&gt; void | PromiseLike&lt;void&gt;;\n    start?: (controller: ReadableByteStreamController) =&gt; any;\n    type: &quot;bytes&quot;;\n}\n\ninterface UnderlyingDefaultSource&lt;R = any&gt; {\n    cancel?: UnderlyingSourceCancelCallback;\n    pull?: (controller: ReadableStreamDefaultController&lt;R&gt;) =&gt; void | PromiseLike&lt;void&gt;;\n    start?: (controller: ReadableStreamDefaultController&lt;R&gt;) =&gt; any;\n    type?: undefined;\n}\n\ninterface UnderlyingSink&lt;W = any&gt; {\n    abort?: UnderlyingSinkAbortCallback;\n    close?: UnderlyingSinkCloseCallback;\n    start?: UnderlyingSinkStartCallback;\n    type?: undefined;\n    write?: UnderlyingSinkWriteCallback&lt;W&gt;;\n}\n</code></pre>\n<p>演示：</p>\n<pre><code class=\"language-ts\">let result = &quot;&quot;;\n\nconst writableStream = new WritableStream(\n    {\n        // Implement the sink\n        write(chunk) {\n            const buffer = new ArrayBuffer(1);\n            const view = new Uint8Array(buffer);\n            view[0] = chunk;\n            const decoded = new TextDecoder(&quot;utf-8&quot;).decode(view, { stream: true });\n\n            console.log(`[WritableStream write()]: ${decoded}`);\n            result += decoded;\n        },\n        close() {\n            console.log(`[WritableStream close()]: ${result}`);\n        },\n        abort(err) {\n            console.log(&quot;Sink error:&quot;, err);\n        },\n    },\n    new CountQueuingStrategy({ highWaterMark: 1 }),\n);\n\nasync function sendMessage(message: string, writableStream: WritableStream) {\n    // defaultWriter is of type WritableStreamDefaultWriter\n    const defaultWriter = writableStream.getWriter();\n    const encoded = new TextEncoder().encode(message);\n\n    try {\n        for (const chunk of encoded) {\n            await defaultWriter.ready;\n            await defaultWriter.write(chunk);\n            console.log(&quot;Chunk written to sink.&quot;);\n        }\n        // Call ready again to ensure that all chunks are written\n        // before closing the writer.\n        await defaultWriter.ready;\n        await defaultWriter.close();\n        console.log(&quot;All chunks written&quot;);\n    } catch (err) {\n        console.log(&quot;Error:&quot;, err);\n    }\n}\n\nsendMessage(&quot;Hello, world.&quot;, writableStream);\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/API/TransformStream\">TransformStream</a></h1>\n<pre><code class=\"language-ts\">interface TransformStream&lt;I = any, O = any&gt; {\n    readonly readable: ReadableStream&lt;O&gt;;\n    readonly writable: WritableStream&lt;I&gt;;\n}\n\ndeclare var TransformStream: {\n    prototype: TransformStream;\n    new &lt;I = any, O = any&gt;(\n        transformer?: Transformer&lt;I, O&gt;,\n        writableStrategy?: QueuingStrategy&lt;I&gt;,\n        readableStrategy?: QueuingStrategy&lt;O&gt;,\n    ): TransformStream&lt;I, O&gt;;\n};\n\ninterface TransformStreamDefaultController&lt;O = any&gt; {\n    readonly desiredSize: number | null;\n    enqueue(chunk?: O): void;\n    error(reason?: any): void;\n    terminate(): void;\n}\n\ndeclare var TransformStreamDefaultController: {\n    prototype: TransformStreamDefaultController;\n    new (): TransformStreamDefaultController;\n};\n</code></pre>\n<h1>QueuingStrategy</h1>\n<p>指定如何（自动）将数据块拆分为大小不等的块。</p>\n<p>参见：<a href=\"https://nodejs.org/es/docs/guides/backpressuring-in-streams\">背压（backpressure）机制</a></p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy\">ByteLengthQueuingStrategy</a></h2>\n<p>ByteLengthQueuingStrategy 构造函数接受一个参数 chunkSize。表示传输中的数据块最大字节数，即，给定一个块流，在施加背压之前，这些块中可以包含多少字节。<br>当传输的数据块大小超过 chunkSize 时，ReadableStream 会自动将其拆分为大小不超过 chunkSize 的块。</p>\n<p>该策略通常用于处理二进制数据，例如音频或视频流。在这种情况下，数据块的大小往往是不确定的，因此使用 ByteLengthQueuingStrategy 可以更好地平衡传输速度和内存使用。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy\">CountQueuingStrategy</a></h2>\n<p>CountQueuingStrategy 构造函数接受一个参数 highWaterMark。表示给定一个块流，在施加背压之前，可以包含在内部队列中的块的总数。<br>当队列中的数据块数量超过 highWaterMark 时，ReadableStream 将停止读取数据，直到队列中的数据量下降到 highWaterMark 以下。</p>\n<p>该策略通常用于处理文本数据，例如日志文件或 JSON 数据。在这种情况下，数据块的大小可能是固定的，但是数据块的数量可能会非常大，因此使用 CountQueuingStrategy 可更好地控制内存使用和传输速度。</p>\n<p>需要注意的是，CountQueuingStrategy 的 highWaterMark 参数是一个建议值，实际上可能会被忽略，具体取决于底层实现。</p>\n<h1>另见</h1>\n<p><a href=\"https://vercel.com/docs/functions/streaming/streaming-examples\">https://vercel.com/docs/functions/streaming/streaming-examples</a></p>\n","tags":["web-api"]},{"id":"java-spring-boot","url":"https://yieldray.fun/posts/java-spring-boot","title":"Spring Boot","date_published":"2023-04-28T20:00:00.000Z","date_modified":"2023-04-28T20:00:00.000Z","content_text":"<p><a href=\"https://spring.io/projects/spring-boot\">https://spring.io/projects/spring-boot</a><br>中文：<a href=\"https://springdoc.cn/spring-boot/\">https://springdoc.cn/spring-boot/</a><br>How-to：<a href=\"https://springdoc.cn/spring-boot/howto.html\">https://springdoc.cn/spring-boot/howto.html</a></p>\n<h1>创建项目</h1>\n<p><a href=\"https://start.spring.io/\">https://start.spring.io/</a><br>镜像：<a href=\"https://start.springboot.io/\">https://start.springboot.io/</a></p>\n<pre><code class=\"language-xml\">&lt;!-- pom.xml --&gt;\n\n&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;\n&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;\n    xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\n    &lt;parent&gt;\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\n        &lt;version&gt;3.0.6&lt;/version&gt;\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\n    &lt;/parent&gt;\n\n    &lt;groupId&gt;com.example&lt;/groupId&gt;\n    &lt;artifactId&gt;demo&lt;/artifactId&gt;\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\n    &lt;name&gt;demo&lt;/name&gt;\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\n    &lt;properties&gt;\n        &lt;java.version&gt;17&lt;/java.version&gt;\n    &lt;/properties&gt;\n\n    &lt;dependencies&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\n            &lt;scope&gt;test&lt;/scope&gt;\n        &lt;/dependency&gt;\n\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\n            &lt;version&gt;RELEASE&lt;/version&gt;\n            &lt;scope&gt;compile&lt;/scope&gt;\n        &lt;/dependency&gt;\n    &lt;/dependencies&gt;\n\n    &lt;build&gt;\n        &lt;plugins&gt;\n            &lt;plugin&gt;\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\n            &lt;/plugin&gt;\n        &lt;/plugins&gt;\n    &lt;/build&gt;\n\n&lt;/project&gt;\n</code></pre>\n<pre><code class=\"language-java\">@RestController\n@SpringBootApplication\npublic class MyApplication {\n\n    @RequestMapping(&quot;/&quot;)\n    String home() {\n        return &quot;Hello World!&quot;;\n    }\n\n    public static void main(String[] args) {\n         Logger logger = LoggerFactory.getLogger(MyApplication.class);\n\n        // 这些log不会被spring boot捕获\n        logger.trace(&quot;before trace&quot;);\n        logger.debug(&quot;before debug&quot;);\n        logger.info(&quot;before info&quot;);\n        logger.warn(&quot;before warn&quot;);\n        logger.error(&quot;before error&quot;);\n\n        SpringApplication.run(MyApplication.class, args);\n\n        // 这些log会被spring boot捕获，由spring boot处理\n        logger.trace(&quot;after trace&quot;);\n        logger.debug(&quot;after debug&quot;);\n        logger.info(&quot;after info&quot;);\n        logger.warn(&quot;after warn&quot;);\n        logger.error(&quot;after error&quot;);\n    }\n\n}\n</code></pre>\n<p>运行下面的命令，可以在项目根目录的 <code>target</code> 目录下得到可执行 jar 包</p>\n<pre><code class=\"language-sh\">mvn package\n</code></pre>\n<p>一个项目典型的布局如下</p>\n<pre><code>com\n +- example\n     +- myapplication\n         +- MyApplication.java\n         |\n         +- customer\n         |   +- Customer.java\n         |   +- CustomerController.java\n         |   +- CustomerService.java\n         |   +- CustomerRepository.java\n         |\n         +- order\n             +- Order.java\n             +- OrderController.java\n             +- OrderService.java\n             +- OrderRepository.java\n</code></pre>\n<p>@SpringBootApplication 注解默认会扫描当前类下的所有子包</p>\n<h1>配置 Spring Boot</h1>\n<p>starter 默认生成了空文件 <code>application.properties</code><br>也可以通过 <code>application.yml</code> 进行配置更直观</p>\n<pre><code class=\"language-yaml\">server:\n    port: 8080\n\nspring:\n    application:\n        name: ${APP_NAME:unnamed}\n    datasource:\n        url: jdbc:hsqldb:file:testdb\n        username: sa\n        password:\n        driver-class-name: org.hsqldb.jdbc.JDBCDriver\n        hikari:\n            auto-commit: false\n            connection-timeout: 3000\n            validation-timeout: 3000\n            max-lifetime: 60000\n            maximum-pool-size: 20\n            minimum-idle: 1\nlogging:\n    level:\n        root: INFO\n</code></pre>\n<h1>使用开发者工具（Developer Tools）</h1>\n<p><a href=\"https://springdoc.cn/spring-boot/using.html#using.devtools\">https://springdoc.cn/spring-boot/using.html#using.devtools</a></p>\n<pre><code class=\"language-xml\">&lt;dependencies&gt;\n    &lt;dependency&gt;\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n        &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;\n        &lt;optional&gt;true&lt;/optional&gt;\n    &lt;/dependency&gt;\n&lt;/dependencies&gt;\n</code></pre>\n","tags":["java","spring"]},{"id":"java-spring-mvc","url":"https://yieldray.fun/posts/java-spring-mvc","title":"Spring MVC","date_published":"2023-04-20T22:00:00.000Z","date_modified":"2023-04-20T22:00:00.000Z","content_text":"<p><a href=\"https://docs.spring.io/spring-framework/docs/current/reference/html/web.html\">https://docs.spring.io/spring-framework/docs/current/reference/html/web.html</a><br><a href=\"https://springdoc.cn/spring/web.html\">https://springdoc.cn/spring/web.html</a></p>\n<p>关于 servlet 接口参见<a href=\"/posts/java-web-intro/\">此处</a></p>\n<h1>MVC</h1>\n<p>Model: 模型是数据源到对象的映射（e.g. <code>javaBean</code>）</p>\n<p>View: 视图层是用户看见的 GUI 界面，在代码中则体现为 HTML 模板 （e.g. <code>jsp</code>）</p>\n<p>Controller: 控制器提供了视图层的数据源（e.g. <code>servlet</code>）</p>\n<h1>Spring MVC</h1>\n<h2>启用 Spring MVC</h2>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\n@EnableWebMvc // 启用Spring MVC\n@EnableTransactionManagement\n@PropertySource(&quot;classpath:/jdbc.properties&quot;)\npublic class AppConfig {\n    // ...\n}\n</code></pre>\n<h2>配置 SpringWebMvc （非必须）</h2>\n<pre><code class=\"language-java\">// 将 /static/** 路由映射为本地的 /static/ 路径，作静态资源处理\n@Bean\nWebMvcConfigurer createWebMvcConfigurer() {\n    return new WebMvcConfigurer() {\n        @Override\n        public void addResourceHandlers(ResourceHandlerRegistry registry) {\n            registry.addResourceHandler(&quot;/static/**&quot;).addResourceLocations(&quot;/static/&quot;);\n        }\n    };\n}\n\n\n// 配置 Pebble 模板引擎\n@Bean\nViewResolver createViewResolver(@Autowired ServletContext servletContext) {\n    var engine = new PebbleEngine.Builder().autoEscaping(true)\n            // cache:\n            .cacheActive(false)\n            // loader:\n            .loader(new Servlet5Loader(servletContext))\n            .build();\n    var viewResolver = new PebbleViewResolver(engine);\n    viewResolver.setPrefix(&quot;/WEB-INF/templates/&quot;);\n    viewResolver.setSuffix(&quot;&quot;);\n    return viewResolver;\n}\n</code></pre>\n<h2>编写 SpringMVC 的 Controller</h2>\n<p>参见：<a href=\"https://springdoc.cn/spring/web.html#mvc-ann-requestmapping\">https://springdoc.cn/spring/web.html#mvc-ann-requestmapping</a></p>\n<pre><code class=\"language-java\">@Controller  // 使用 @Controller 标记而不是 @Component\n@RequestMapping(&quot;/user&quot;) // 可以通过 RequestMapping 对路由进行分组\npublic class UserController {\n    @Autowired\n    UserService userService;\n\n    // 返回ModelAndView用于模板渲染（具体能返回什么，参见上面的文档）\n    @GetMapping(&quot;/&quot;)\n    public ModelAndView index() {\n        return new ModelAndView(&quot;redirect:/signin&quot;)\n    }\n    // 或者，可以直接返回String来重定向\n    public String index() {\n        if (...) {\n            return &quot;redirect:/signin&quot;;\n        } else {\n            return &quot;redirect:/profile&quot;;\n        }\n    }\n\n    // Controller方法具体能使用什么注解，能带什么参数，参见上面的文档\n    @PostMapping(&quot;/signin&quot;)\n    public ModelAndView doSignin(\n        @RequestParam(&quot;email&quot;) String email,\n        @RequestParam(&quot;password&quot;) String password,\n        HttpSession session) {\n        // ...\n    }\n\n    @GetMapping(&quot;/download&quot;)\n    public void download(HttpServletResponse response) throws IOException {\n        byte[] data = /* ... */\n        response.setContentType(&quot;application/octet-stream&quot;);\n        OutputStream output = response.getOutputStream();\n        output.write(data);\n        output.flush();\n    }\n\n    @PostMapping(value = &quot;/json&quot;,\n            consumes = &quot;application/json;charset=UTF-8&quot;,\n            produces = &quot;application/json;charset=UTF-8&quot;)\n    @ResponseBody\n    public String rest(@RequestBody User user) {\n        return &quot;{\\&quot;success\\&quot;:true}&quot;;\n    }\n}\n</code></pre>\n<h2>实现 Restful 接口</h2>\n<p>使用 <code>@RestController</code> 替代 <code>@Controller</code> 就可以实现 Restful 接口<br>注：需要安装 <code>com.fasterxml.jackson.core:jackson-databind</code></p>\n<pre><code class=\"language-java\">@RequestMapping(&quot;/api&quot;)\npublic class ApiController {\n    @Autowired\n    UserService userService;\n\n    @GetMapping(&quot;/users&quot;)\n    public List&lt;User&gt; users() {\n        return userService.getUsers();\n    }\n\n    @GetMapping(&quot;/users/{id}&quot;)\n    public User user(@PathVariable(&quot;id&quot;) long id) {\n        return userService.getUserById(id);\n    }\n\n    public static class SignInRequest {\n        public String email;\n        public String password;\n    }\n    @PostMapping(&quot;/signin&quot;)\n    public Map&lt;String, Object&gt; signin(@RequestBody SignInRequest signinRequest) {\n        try {\n            User user = userService.signin(signinRequest.email, signinRequest.password);\n            return Map.of(&quot;user&quot;, user);\n        } catch (Exception e) {\n            return Map.of(&quot;error&quot;, &quot;SIGNIN_FAILED&quot;, &quot;message&quot;, e.getMessage());\n        }\n    }\n}\n</code></pre>\n<p>由于 Spring 使用 Jackson 进行序列化，故应参见 Jackson 文档来应用其序列化规则</p>\n<pre><code class=\"language-java\">public class User {\n    /* ... */\n\n    @JsonIgnore\n    public String getInnerProperty() {\n        return innerProperty;\n    }\n\n    @JsonProperty(access = Access.WRITE_ONLY)\n    public String getPassword() {\n        return password;\n    }\n}\n</code></pre>\n<h2>Filter</h2>\n<p>配置 <code>web.xml</code></p>\n<pre><code class=\"language-xml\">&lt;web-app&gt;\n    &lt;filter&gt;\n        &lt;filter-name&gt;authFilter&lt;/filter-name&gt;\n        &lt;filter-class&gt;org.springframework.web.filter.DelegatingFilterProxy&lt;/filter-class&gt;\n        &lt;!-- 这里是 Spring 提供的 DelegatingFilterProxy 类，而不是我们的 AuthFilter 类 --&gt;\n        &lt;!-- 该类通过代理模式，使得我们的类可以像其它 Spring 类一样利用 IoC 特性 --&gt;\n    &lt;/filter&gt;\n\n    &lt;filter-mapping&gt;\n        &lt;filter-name&gt;authFilter&lt;/filter-name&gt;\n        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;\n    &lt;/filter-mapping&gt;\n    ...\n&lt;/web-app&gt;\n</code></pre>\n<p>若 Filter 名称与 Spring 容器的 Bean 名称不一致</p>\n<pre><code class=\"language-xml\">&lt;filter&gt;\n    &lt;filter-name&gt;basicAuthFilter&lt;/filter-name&gt;\n    &lt;filter-class&gt;org.springframework.web.filter.DelegatingFilterProxy&lt;/filter-class&gt;\n    &lt;init-param&gt;\n        &lt;!-- 此处指定 Bean 的名称 --&gt;\n        &lt;param-name&gt;targetBeanName&lt;/param-name&gt;\n        &lt;param-value&gt;authFilter&lt;/param-value&gt;\n    &lt;/init-param&gt;\n&lt;/filter&gt;\n</code></pre>\n<p>下面的 Filter 拦截 Authorization 请求头</p>\n<pre><code class=\"language-java\">@Component\npublic class AuthFilter implements Filter {\n    @Autowired\n    UserService userService;\n\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)\n            throws IOException, ServletException {\n        HttpServletRequest req = (HttpServletRequest) request;\n        // 获取Authorization头:\n        String authHeader = req.getHeader(&quot;Authorization&quot;);\n        if (authHeader != null &amp;&amp; authHeader.startsWith(&quot;Basic &quot;)) {\n            // 从Header中提取email和password:\n            String email = prefixFrom(authHeader);\n            String password = suffixFrom(authHeader);\n            // 登录:\n            User user = userService.signin(email, password);\n            // 放入Session:\n            req.getSession().setAttribute(UserController.KEY_USER, user);\n        }\n        // 继续处理请求:\n        chain.doFilter(request, response);\n    }\n}\n</code></pre>\n<h2>Interceptor</h2>\n<p>上面的 Filter 是由 Servlet 定义和处理的，故将应用到整个 Servlet 应用上<br>Spring 本身提供了 Interceptor，其可以细度到 Controller 上</p>\n<p>参见：<a href=\"https://springdoc.cn/spring/web.html#mvc-handlermapping-interceptor\">https://springdoc.cn/spring/web.html#mvc-handlermapping-interceptor</a></p>\n<p><a href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1347180610715681\">https://www.liaoxuefeng.com/wiki/1252599548343744/1347180610715681</a></p>\n","tags":["java","spring"]},{"id":"js-next13","url":"https://yieldray.fun/posts/js-next13","title":"next.js 13","date_published":"2023-04-18T13:00:00.000Z","date_modified":"2023-04-18T13:00:00.000Z","content_text":"<blockquote>\n<p>next.js app router 已稳定！<br>过时警告：以下内容根据 beta 版编写！<br>TODO：待修正</p>\n</blockquote>\n<blockquote>\n<p>see <a href=\"https://nextjs.org/docs\">docs here</a></p>\n</blockquote>\n<h1>Installation</h1>\n<pre><code class=\"language-sh\">npx create-next-app@latest --experimental-app\n\n# 默认选项如下\n√ What is your project named? ... nextjs-test\n√ Would you like to use TypeScript with this project? ... No / [Yes]\n√ Would you like to use ESLint with this project? ... [No] / Yes\n√ Would you like to use Tailwind CSS with this project? ... No / [Yes]\n√ Would you like to use `src/` directory with this project? ... [No] / Yes\n√ What import alias would you like configured? ... @/*\n</code></pre>\n<p><a href=\"https://beta.nextjs.org/docs/configuring/typescript#using-the-typescript-plugin\">vscode 插件</a></p>\n<h1>Routing</h1>\n<h2>文件约定</h2>\n<blockquote>\n<p><code>.js</code>, <code>.jsx</code>, 和 <code>.tsx</code> 后缀均可，下面以 <code>.js</code> 为例</p>\n</blockquote>\n<p><a href=\"https://beta.nextjs.org/docs/routing/pages-and-layouts#layouts\"><strong>layout.js</strong></a><br>layout 是不同页面之间的共享 UI。在导航时，layout 会保留 state，仍然可以交互，因为其不会重新渲染。</p>\n<pre><code class=\"language-js\">// app目录下的根layout.js如下\nexport const metadata = {\n    title: &quot;Create Next App&quot;,\n    description: &quot;Generated by create next app&quot;,\n};\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n    return (\n        &lt;html lang=&quot;en&quot;&gt;\n            &lt;body&gt;{children}&lt;/body&gt;\n        &lt;/html&gt;\n    );\n}\n</code></pre>\n<blockquote>\n<p>template.js 与 layout.js 类似，但在其子页面中导航时，会重新挂载 React 实例（因此不会保存 state）</p>\n</blockquote>\n<p><a href=\"https://beta.nextjs.org/docs/routing/pages-and-layouts#pages\"><strong>page.js</strong></a><br>Create the unique UI of a route and make the path publicly accessible.</p>\n<pre><code class=\"language-js\">// app目录下的根page.js如下\nimport Image from &quot;next/image&quot;;\nimport Link from &quot;next/link&quot;;\nimport { Inter } from &quot;next/font/google&quot;;\nconst inter = Inter({ subsets: [&quot;latin&quot;] });\n\nexport default function Home() {\n    return (\n        &lt;main className=&quot;flex min-h-screen flex-col items-center justify-between p-24&quot;&gt;\n            &lt;Image\n                className=&quot;relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert&quot;\n                src=&quot;/next.svg&quot;\n                alt=&quot;Next.js Logo&quot;\n                width={180}\n                height={37}\n                priority\n            /&gt;\n            &lt;Link href=&quot;/docs&quot;&gt;\n                &lt;h2 className={`${inter.className} mb-3 text-2xl font-semibold`}&gt;\n                    Docs{&quot; &quot;}\n                    &lt;span className=&quot;inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none&quot;&gt;\n                        -&amp;gt;\n                    &lt;/span&gt;\n                &lt;/h2&gt;\n            &lt;/Link&gt;\n        &lt;/main&gt;\n    );\n}\n</code></pre>\n<p>还可以通过 <code>useRouter</code> hook 导航，参见：<a href=\"https://beta.nextjs.org/docs/api-reference/use-router\">https://beta.nextjs.org/docs/api-reference/use-router</a></p>\n<p>一个典型的 page.js 如下</p>\n<pre><code class=\"language-js\">// app/blog/[slug]/page.tsx\n&quot;use client&quot;;\nexport default function Page({\n    params,\n    searchParams, // searchParams是动态API，使用searchParams将会导致动态渲染！\n}: {\n    params: { slug: string },\n    searchParams: { [key: string]: string | string[] | undefined },\n}) {\n    return &lt;h1&gt;My Page&lt;/h1&gt;;\n}\n// e.g.\n// /blog/test?a=1\n//  params = { slug: &#39;test&#39; }\n//  searchParams = { a: &#39;1&#39; }\n</code></pre>\n<p>还可以通过 <code>useSearchParams</code> 获取 searchParams， 参见：<a href=\"https://beta.nextjs.org/docs/api-reference/use-search-params\">https://beta.nextjs.org/docs/api-reference/use-search-params</a></p>\n<p>其余内容参见：<a href=\"https://beta.nextjs.org/docs/routing/fundamentals#file-conventions\">https://beta.nextjs.org/docs/routing/fundamentals#file-conventions</a></p>\n<h2>路由</h2>\n<p>一个目录下如存在 page.js 即成为路由的一部分，反之若不存在则不会成为路由<br><img src=\"https://assets.vercel.com/image/upload/f_auto,q_100,w_1600/v1666568301/nextjs-docs/darkmode/defining-routes-page.js.png\" alt=\"\"></p>\n<p>路由分组<br><img src=\"https://assets.vercel.com/image/upload/f_auto,q_100,w_1600/v1666568349/nextjs-docs/darkmode/route-group-organisation.png\" alt=\"\"></p>\n<p>可以给每个路由分组目录（<code>(groupname)</code>）下添加 layout.js，这样就可以作用到这组路由（而不影响生成的路径）<br><img src=\"https://assets.vercel.com/image/upload/f_auto,q_100,w_1600/v1666568349/nextjs-docs/darkmode/route-group-multiple-layouts.png\" alt=\"\"></p>\n<p>可以给每一组路由下使用自己的 layout.js，从而在每一组下应用自己的根路由（删除 <code>app/layout.js</code> 根路由，因为已经不需要了），此时注意每组的根路由是需要 <code>&lt;html&gt;</code> 和 <code>&lt;body&gt;</code> 标签的（就像 <code>app/layout.js</code> 一样）<br>注意：在这样的多根 layout 之间的页面导航会导致网页的重新加载<br><img src=\"https://assets.vercel.com/image/upload/f_auto,q_100,w_1600/v1666568348/nextjs-docs/darkmode/route-group-multiple-root-layouts.png\" alt=\"\"></p>\n<h2>动态路由段</h2>\n<pre><code class=\"language-js\">// app/blog/[slug]/page.js\nexport default function Page({ params }) {\n    return &lt;div&gt;My Post&lt;/div&gt;;\n}\n\n// e.g.\n// /blog/a  -&gt;  params = { slug: &#39;a&#39; }\n// /blog/b  -&gt;  params = { slug: &#39;b&#39; }\n\n// e.g.\n// app/[categoryId]/[itemId]/page.js\n// type param = { categoryId: string, itemId: string }\n</code></pre>\n<p>可以像这样捕获所有路由段</p>\n<pre><code class=\"language-js\">// app/shop/[...slug]/page.js\nexport default function Page({ params }) {\n    return &lt;div&gt;My Post&lt;/div&gt;;\n}\n\n// e.g.\n// /blog/a    -&gt;  params = { slug: [&#39;a&#39;] }\n// /blog/a/b  -&gt;  params = { slug: [&#39;a&#39;, &#39;b&#39;] }\n</code></pre>\n<p>可选捕获所有路由段</p>\n<pre><code class=\"language-js\">// app/shop/[[...slug]]/page.js\n\n// e.g.\n// /shop      -&gt;  params = {}\n// /shop/a    -&gt;  params = { slug: [&#39;a&#39;] }\n// /shop/a/b  -&gt;  params = { slug: [&#39;a&#39;, &#39;b&#39;] }\n</code></pre>\n<h1>Rendering</h1>\n<h2>Server Components 与 Client Components</h2>\n<p>app 目录默认渲染 Server Components<br>在一个组件的顶行使用 <code>&quot;use client&quot;</code> 指令即可声明为 Client Components<br><img src=\"https://assets.vercel.com/image/upload/f_auto,q_100,w_1600/v1680683516/nextjs-docs/darkmode/use-client-directive.png\" alt=\"\">\nClient Components 下导入的所有组件自动成为 Client Components，因此无需 <code>&quot;use client&quot;</code> 指令<br>Client Components 不能导入（import） Server Components，但可以将 Server Components 作为 Client Components 的子节点（children）<br><img src=\"https://assets.vercel.com/image/upload/f_auto,q_100,w_1600/v1678098147/nextjs-docs/darkmode/component-tree.png\" alt=\"\"><br>Server Components 传给其 Client Components 子节点的 props 必须能够被序列化（因此不能传函数等）</p>\n<p>有的组件既能在服务端渲染，又能在客户端渲染（但为了避免安全问题），要使之仅在服务端渲染，先安装 <code>npm install server-only</code></p>\n<pre><code class=\"language-js\">// lib/data.js\nimport &quot;server-only&quot;;\n\nexport async function getData() {\n    let resp = await fetch(&quot;https://external-service.com/data&quot;, {\n        headers: {\n            authorization: process.env.API_KEY,\n        },\n    });\n    return resp.json();\n}\n// 通过这样，Client Component在导入此模块时将抛出编译错误，提示此模块仅能在Server Components导入\n</code></pre>\n<p>第三方包提供的 Client Components 可能不含 <code>&quot;use client&quot;</code> 指令，因此在 Server Components 中导入这样的组件将导致编译错误，因为 nextjs 不知道这是 Client Components（默认当作 Server Components）<br>nextjs 文档中建议这样做：</p>\n<pre><code class=\"language-js\">// app/carousel.js\n&quot;use client&quot;;\nimport { AcmeCarousel } from &quot;acme-carousel&quot;;\nexport default AcmeCarousel;\n</code></pre>\n<p>在 Server Components 导入包装后的 Client Components 将不再导致编译错误</p>\n<h2>context</h2>\n<p>由于 Server Components 是静态的，所以其自身不能创建或消费 context</p>\n<p>参见：<a href=\"https://beta.nextjs.org/docs/rendering/server-and-client-components#context\">https://beta.nextjs.org/docs/rendering/server-and-client-components#context</a></p>\n<h2>静态渲染和动态渲染</h2>\n<p>参见：<a href=\"https://beta.nextjs.org/docs/rendering/static-and-dynamic-rendering\">https://beta.nextjs.org/docs/rendering/static-and-dynamic-rendering</a></p>\n<h2>edge &amp; node.js runtime</h2>\n<p>参见：<a href=\"https://beta.nextjs.org/docs/rendering/edge-and-nodejs-runtimes\">https://beta.nextjs.org/docs/rendering/edge-and-nodejs-runtimes</a></p>\n<h1>Data fetching</h1>\n<p>鉴于 react 对异步的支持（first-class-support-for-promises）未稳定，此处就直接贴出文档：<a href=\"https://beta.nextjs.org/docs/data-fetching/fundamentals\">https://beta.nextjs.org/docs/data-fetching/fundamentals</a></p>\n<h2>生成静态 Params</h2>\n<pre><code class=\"language-tsx\">// app/blog/[slug]/page.tsx\nexport async function generateStaticParams() {\n    const posts = await fetch(&quot;https://.../posts&quot;).then((res) =&gt; res.json());\n\n    return posts.map((post) =&gt; ({\n        slug: post.slug,\n    }));\n}\n</code></pre>\n<p><a href=\"https://beta.nextjs.org/docs/api-reference/generate-static-params\"><code>generateStaticParams</code> API Reference</a></p>\n<h2>Route Handlers</h2>\n<p>Route Handlers (<code>route.js</code>) allow you to create custom request handlers for a given route using the Web <code>Request</code> and <code>Response</code> APIs.<br>（仅在新的 <code>app</code> 文件夹内有效，相当于 <code>pages</code> 文件夹内的 <code>API Routes</code> (注: 其扩展了 nodejs 的 http 模块的 req,res )）<br>（<code>route.js</code> 与 <code>page.js</code> 不能共存与同一目录(路由)，因为这两种文件都能完全接管路由）<br><img src=\"https://assets.vercel.com/image/upload/f_auto,q_100,w_1600/v1675915652/nextjs-docs/darkmode/route-handler.png\" alt=\"\"></p>\n<pre><code class=\"language-ts\">// app/api/route.ts\n\n// supported_http_methods: GET, POST, PUT, PATCH, DELETE, HEAD\nexport async function GET(request: Request) {\n    const { searchParams } = new URL(request.url);\n    const id = searchParams.get(&quot;id&quot;);\n    return Response.json({ id });\n}\n</code></pre>\n<p>Next.js extends native <code>Request</code> and <code>Response</code> with <code>NextRequest</code> and <code>NextResponse</code> to provide convenient helpers for advanced use cases.</p>\n<pre><code class=\"language-ts\">// app/rss.xml/route.ts\n\n// 当Router Handler函数是 GET 且不带任何参数，例如不是这样： GET(request: Request)\n// 且没有使用 cookies 和 headers 等动态API函数时，该 handler 将被静态求值\nexport async function GET() {\n    return new Response(`&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;\n&lt;rss version=&quot;2.0&quot;&gt;\n\n&lt;channel&gt;\n  &lt;title&gt;Next.js Documentation&lt;/title&gt;\n  &lt;link&gt;https://nextjs.org/docs&lt;/link&gt;\n  &lt;description&gt;The React Framework for the Web&lt;/description&gt;\n&lt;/channel&gt;\n\n&lt;/rss&gt;`);\n}\n</code></pre>\n<p>动态 API 函数，参见：<a href=\"https://beta.nextjs.org/docs/routing/route-handlers#dynamic-functions\">https://beta.nextjs.org/docs/routing/route-handlers#dynamic-functions</a></p>\n<p>下面是演示：</p>\n<pre><code class=\"language-ts\">// app/api/route.ts\nimport { NextResponse } from &quot;next/server&quot;;\nimport { cookies, headers } from &quot;next/headers&quot;;\nimport { redirect } from &quot;next/navigation&quot;;\n\nexport async function GET(request: Request) {\n    const headersList = headers();\n    const referer = headersList.get(&quot;referer&quot;);\n\n    const cookieStore = cookies();\n    const token = cookieStore.get(&quot;token&quot;);\n\n    const res = await fetch(&quot;https://data.mongodb-api.com/...&quot;, {\n        next: { revalidate: 60 }, // Revalidate every 60 seconds\n    });\n    const data = await res.json();\n\n    if (!referer.startsWith(&quot;https://example.net&quot;)) {\n        redirect(&quot;https://nextjs.org/&quot;);\n    } else {\n        return new NextResponse(JSON.stringify({ data }), {\n            status: 200,\n            headers: { &quot;Set-Cookie&quot;: `token=${token}` },\n        });\n    }\n}\n\nexport const revalidate = 60; // false | &#39;force-cache&#39; | 0 | number\n// Route Segment Config Options\n// 参见：https://beta.nextjs.org/docs/api-reference/segment-config\n</code></pre>\n<h2>动态路由段 (for Route Handlers)</h2>\n<pre><code class=\"language-ts\">// app/items/[slug]/route.ts\n//prettier-ignore\nexport async function GET(request: Request, { params }: {\n  params: { slug: string }\n}) {\n  const slug = params.slug; // &#39;a&#39;, &#39;b&#39;, or &#39;c&#39;\n}\n// e.g.\n// /items/a -&gt; { slug: &#39;a&#39; }\n// /items/b -&gt; { slug: &#39;b&#39; }\n</code></pre>\n<p>暂不支持使用 <code>generateStaticParams()</code></p>\n<h2>流式响应</h2>\n<pre><code class=\"language-ts\">// app/api/route.ts\n// https://developer.mozilla.org/docs/Web/API/ReadableStream#convert_async_iterator_to_stream\nfunction iteratorToStream(iterator: any) {\n    return new ReadableStream({\n        async pull(controller) {\n            const { value, done } = await iterator.next();\n\n            if (done) {\n                controller.close();\n            } else {\n                controller.enqueue(value);\n            }\n        },\n    });\n}\n\nfunction sleep(time: number) {\n    return new Promise((resolve) =&gt; {\n        setTimeout(resolve, time);\n    });\n}\n\nconst encoder = new TextEncoder();\n\nasync function* makeIterator() {\n    yield encoder.encode(&quot;&lt;p&gt;One&lt;/p&gt;&quot;);\n    await sleep(200);\n    yield encoder.encode(&quot;&lt;p&gt;Two&lt;/p&gt;&quot;);\n    await sleep(200);\n    yield encoder.encode(&quot;&lt;p&gt;Three&lt;/p&gt;&quot;);\n}\n\nexport async function GET() {\n    const iterator = makeIterator();\n    const stream = iteratorToStream(iterator);\n\n    return new Response(stream);\n}\n</code></pre>\n<h1>其它</h1>\n<p>有关 Styling, Optimizing, Deploying 及 API Reference 等内容，参见文档剩余部分：<a href=\"https://beta.nextjs.org/docs/styling/css-modules\">https://beta.nextjs.org/docs/styling/css-modules</a></p>\n","tags":["js","react","lib"]},{"id":"java-spring-database","url":"https://yieldray.fun/posts/java-spring-database","title":"spring之数据库","date_published":"2023-04-09T18:00:00.000Z","date_modified":"2023-04-09T18:00:00.000Z","content_text":"<p><a href=\"https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html\">https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html</a></p>\n<h1>使用 JdbcTemplate</h1>\n<p>首先安装下面几个依赖</p>\n<pre><code>org.springframework:spring-context\norg.springframework:spring-jdbc\njakarta.annotation:jakarta.annotation-api\ncom.zaxxer:HikariCP\n</code></pre>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\n@PropertySource(&quot;jdbc.properties&quot;) // classpath 下的 jdbc.properties 文件\npublic class AppConfig {\n\n    @Value(&quot;${jdbc.url}&quot;)\n    String jdbcUrl;\n\n    @Value(&quot;${jdbc.username}&quot;)\n    String jdbcUsername;\n\n    @Value(&quot;${jdbc.password}&quot;)\n    String jdbcPassword;\n\n    @Bean\n    DataSource createDataSource() {\n        HikariConfig config = new HikariConfig();\n        config.setJdbcUrl(jdbcUrl);\n        config.setUsername(jdbcUsername);\n        config.setPassword(jdbcPassword);\n        config.addDataSourceProperty(&quot;autoCommit&quot;, &quot;true&quot;);\n        config.addDataSourceProperty(&quot;connectionTimeout&quot;, &quot;5&quot;);\n        config.addDataSourceProperty(&quot;idleTimeout&quot;, &quot;60&quot;);\n        return new HikariDataSource(config);\n    }\n\n    @Bean\n    JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {\n        return new JdbcTemplate(dataSource);\n    }\n}\n</code></pre>\n<p>上面主要是配置了 <code>JdbcTemplate</code> 对象，配置完成即可注入</p>\n<pre><code class=\"language-java\">@Component\npublic class UserService {\n    @Autowired\n    JdbcTemplate jdbcTemplate;\n    ...\n}\n</code></pre>\n<p>execute 方法是最基本的方法，下面获取 <code>Connection</code> 对象</p>\n<pre><code class=\"language-java\">public User getUserById(long id) {\n    return jdbcTemplate.execute((Connection conn) -&gt; {\n        try (var ps = conn.prepareStatement(&quot;SELECT * FROM users WHERE id = ?&quot;)) {\n            ps.setObject(1, id);\n            try (var rs = ps.executeQuery()) {\n                if (rs.next()) {\n                    return new User(\n                            rs.getLong(&quot;id&quot;),\n                            rs.getString(&quot;email&quot;),\n                            rs.getString(&quot;password&quot;),\n                            rs.getString(&quot;name&quot;));\n                }\n                throw new RuntimeException(&quot;cannot find user by id&quot;);\n            }\n        }\n    });\n}\n</code></pre>\n<p>上面还需要通过操作 <code>Connection</code> 对象传入 sql 语句，下面的方法可以直接执行 sql 语句并获取 <code>PreparedStatement</code> 对象</p>\n<pre><code class=\"language-java\">public User getUserByName(String name) {\n    return jdbcTemplate.execute(&quot;SELECT * FROM users WHERE name = ?&quot;, (PreparedStatement ps) -&gt; {\n        ps.setObject(1, name);\n        try (var rs = ps.executeQuery()) {\n            if (rs.next()) {\n                return new User(\n                        rs.getLong(&quot;id&quot;),\n                        rs.getString(&quot;email&quot;),\n                        rs.getString(&quot;password&quot;),\n                        rs.getString(&quot;name&quot;));\n            }\n            throw new RuntimeException(&quot;cannot find user by id&quot;);\n        }\n    });\n}\n</code></pre>\n<p>上面的方法还是需要操作 <code>PreparedStatement</code> 对象并手动设置占位处的值，下面又加以简化</p>\n<pre><code class=\"language-java\">public User getUserByEmail(String email) {\n    // 返回值的类型可以是任意的\n    return jdbcTemplate.queryForObject(&quot;SELECT * FROM users WHERE email = ?&quot;,\n            (ResultSet rs, int rowNum) -&gt; {\n                return new User(\n                        rs.getLong(&quot;id&quot;),\n                        rs.getString(&quot;email&quot;),\n                        rs.getString(&quot;password&quot;),\n                        rs.getString(&quot;name&quot;));\n            },\n            email);\n}\n</code></pre>\n<p>通过 spring 提供的 <code>BeanPropertyRowMapper</code> 对象，可以将数据库的记录自动转为 javaBean</p>\n<pre><code class=\"language-java\">// 注意返回值的类型\npublic List&lt;User&gt; getUsers(int pageIndex) {\n    int limit = 100;\n    int offset = limit * (pageIndex - 1);\n    return jdbcTemplate.query(&quot;SELECT * FROM users LIMIT ? OFFSET ?&quot;,\n            new BeanPropertyRowMapper&lt;&gt;(User.class),\n            limit, offset);\n}\n// 如果列名与javaBean不一致，则需要手动指定别名\n// SELECT id, email, office_address AS workAddress, name FROM users WHERE email = ?\n</code></pre>\n<p><code>jdbcTemplate</code> 的 update 方法返回受影响的行数</p>\n<pre><code class=\"language-java\">public void updateUsername(User user, String name) {\n    if (1 != jdbcTemplate.update(&quot;UPDATE users SET name = ? WHERE id = ?&quot;, name, user.getId())) {\n        throw new RuntimeException(&quot;cannot find user by id&quot;);\n    }\n}\n</code></pre>\n<p>若插入时含有自增列，可以通过 <code>GeneratedKeyHolder</code> 对象取出</p>\n<pre><code class=\"language-java\">public User register(String email, String password, String name) {\n    KeyHolder holder = new GeneratedKeyHolder();\n\n    if (1 != jdbcTemplate.update(\n        (conn) -&gt; {\n            var ps = conn.prepareStatement(&quot;INSERT INTO users(email, password, name) VALUES(?, ?, ?)&quot;,\n                    Statement.RETURN_GENERATED_KEYS /** 此处指定要返回自增键 */);\n            ps.setObject(1, email);\n            ps.setObject(2, password);\n            ps.setObject(3, name);\n            return ps;\n        }, holder /** 此处传入 GeneratedKeyHolder */)) {\n        throw new RuntimeException(&quot;failed to insert&quot;);\n    }\n    // 取出自增值\n    return new User(holder.getKey().longValue(), email, password, name);\n}\n</code></pre>\n<h1>声明式事务</h1>\n<p><a href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1282383642886177\">https://www.liaoxuefeng.com/wiki/1252599548343744/1282383642886177</a></p>\n<h1>spring 提供的 DAO 模式</h1>\n<p>spring 提供了 <code>JdbcDaoSupport</code> 抽象类，其持有 <code>jdbcTemplate</code> 对象</p>\n<pre><code class=\"language-java\">public abstract class JdbcDaoSupport extends DaoSupport {\n\n    private JdbcTemplate jdbcTemplate;\n\n    public final void setJdbcTemplate(JdbcTemplate jdbcTemplate) {\n        this.jdbcTemplate = jdbcTemplate;\n        initTemplateConfig();\n    }\n\n    public final JdbcTemplate getJdbcTemplate() {\n        return this.jdbcTemplate;\n    }\n\n    ...\n}\n</code></pre>\n<p>不过该类不会自动注入 <code>jdbcTemplate</code> 对象，而需要子类手动注入（如下）</p>\n<pre><code class=\"language-java\">@Component\n@Transactional\npublic class UserDao extends JdbcDaoSupport {\n    @Autowired\n    JdbcTemplate jdbcTemplate;\n\n    @PostConstruct\n    public void init() {\n        super.setJdbcTemplate(jdbcTemplate);\n    }\n}\n</code></pre>\n<p>可以自己实现一个自动注入的<code>jdbcTemplate</code> 对象的基类</p>\n<pre><code class=\"language-java\">`public abstract class AbstractDao extends JdbcDaoSupport {\n    @Autowired\n    private JdbcTemplate jdbcTemplate;\n\n    @PostConstruct\n    public void init() {\n        super.setJdbcTemplate(jdbcTemplate);\n    }\n}\n</code></pre>\n<p>这样，直接继承该基类就免于每次都手动注入</p>\n<h1>Hibernate ORM</h1>\n<p><a href=\"https://hibernate.org/orm/\">https://hibernate.org/orm/</a></p>\n<h1>Java Persistence API （JPA）</h1>\n<p><a href=\"https://www.oracle.com/java/technologies/persistence-jsp.html\">https://www.oracle.com/java/technologies/persistence-jsp.html</a></p>\n<h1>MyBatis</h1>\n<p>如果直接使用 Mybatis，参见文档：<a href=\"https://mybatis.org/mybatis-3/zh/index.html\">https://mybatis.org/mybatis-3/zh/index.html</a><br>下面是在 Spring 环境下使用，并且只通过注解创建 Mapper。</p>\n<p>首先保证我们已经创建了 <code>DataSource</code></p>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\n@EnableTransactionManagement\n@PropertySource(&quot;jdbc.properties&quot;)\npublic class AppConfig {\n    @Bean\n    DataSource createDataSource() { ... }\n}\n</code></pre>\n<p>使用 MyBatis 需要创建 <code>SqlSessionFactory</code>，但在 Spring 中我们创建 <code>SqlSessionFactoryBean</code><br>（在 <code>@Configuration</code> 类中配置）</p>\n<pre><code class=\"language-java\">@Bean\nSqlSessionFactoryBean createSqlSessionFactoryBean(@Autowired DataSource dataSource) {\n    var sqlSessionFactoryBean = new SqlSessionFactoryBean();\n    sqlSessionFactoryBean.setDataSource(dataSource);\n    return sqlSessionFactoryBean;\n}\n</code></pre>\n<p>MyBatis 可以直接使用 Spring 管理的声明式事务。<br>因此，创建 事务管理器 和在 JDBC 中创建 事务管理器 是完全一样的。</p>\n<pre><code class=\"language-java\">@Bean\nPlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {\n    return new DataSourceTransactionManager(dataSource);\n}\n</code></pre>\n<p>完成需要的配置后，就可以通过 MyBatis 来创建 Mapper 了</p>\n<blockquote>\n<p>注解文档参见：<a href=\"https://mybatis.org/mybatis-3/zh/java-api.html#%E6%98%A0%E5%B0%84%E5%99%A8%E6%B3%A8%E8%A7%A3\">https://mybatis.org/mybatis-3/zh/java-api.html#%E6%98%A0%E5%B0%84%E5%99%A8%E6%B3%A8%E8%A7%A3</a></p>\n</blockquote>\n<pre><code class=\"language-java\">public interface UserMapper {\n    // 使用 @Select 注解\n    @Select(&quot;SELECT * FROM users WHERE id = #{id}&quot;)\n    User getById(@Param(&quot;id&quot;) long id);\n\n    @Select(&quot;SELECT * FROM users LIMIT #{offset}, #{maxResults}&quot;)\n    List&lt;User&gt; getAll(@Param(&quot;offset&quot;) int offset, @Param(&quot;maxResults&quot;) int maxResults);\n\n    // 使用 @Insert 注解\n    @Insert(&quot;INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})&quot;)\n    void insert(@Param(&quot;user&quot;) User user);\n\n    // 自增主键，我们希望不用传入。使用 @Options 注解\n    // keyProperty=JavaBean属性\n    // keyColumn=数据库主键列名\n    @Options(useGeneratedKeys = true, keyProperty = &quot;id&quot;, keyColumn = &quot;id&quot;)\n    @Insert(&quot;INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})&quot;)\n    void insert(@Param(&quot;user&quot;) User user);\n\n    @Update(&quot;UPDATE users SET name = #{user.name}, createdAt = #{user.createdAt} WHERE id = #{user.id}&quot;)\n    void update(@Param(&quot;user&quot;) User user);\n\n    @Delete(&quot;DELETE FROM users WHERE id = #{id}&quot;)\n    void deleteById(@Param(&quot;id&quot;) long id);\n}\n</code></pre>\n<p>写好接口之后，我们通过 <code>@MapperScan</code> 注解让 Mybatis 自动创建对应实现类。</p>\n<pre><code class=\"language-java\">// 其它注解...\n@MapperScan(&quot;com.example.demo&quot;)\npublic class AppConfig {\n    ...\n}\n</code></pre>\n<p>这样我们就可以在 Service 层直接注入 Mapper 接口了</p>\n<pre><code class=\"language-java\">@Component\n@Transactional\npublic class UserService {\n    @Autowired\n    UserMapper userMapper;\n\n    public User getUserById(long id) {\n        User user = userMapper.getById(id);\n        if (user == null) throw new RuntimeException(&quot;User not found by id.&quot;);\n        return user;\n    }\n}\n</code></pre>\n","tags":["java","spring"]},{"id":"java-spring-aop","url":"https://yieldray.fun/posts/java-spring-aop","title":"spring之AOP","date_published":"2023-04-07T14:00:00.000Z","date_modified":"2023-04-07T14:00:00.000Z","content_text":"<h1>AOP</h1>\n<p>Aspect Oriented Programming</p>\n<p>AOP 类似于 Proxy 模式，其目的是方便将公共逻辑（例如：权限检查、日志、事务）进行复用</p>\n<blockquote>\n<p>首先需要安装 <a href=\"https://package-search.jetbrains.com/package?id=org.springframework%3Aspring-aspects\">org.springframework:spring-aspects</a> 依赖\n该依赖使用 AspectJ 实现了 AOP</p>\n</blockquote>\n<p>通过 <code>@Aspect</code> 注解定义一个 Aspect</p>\n<pre><code class=\"language-java\">@Aspect\n@Component\npublic class LoggingAspect {\n    // 在执行UserService的每个方法前执行:\n    @Before(&quot;execution(public * com.example.service.UserService.*(..))&quot;)\n    public void doAccessCheck() {\n        System.err.println(&quot;[Before] do access check...&quot;);\n    }\n\n    // 在执行MailService的每个方法前后执行:\n    @Around(&quot;execution(public * com.example.service.MailService.*(..))&quot;)\n    public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {\n        System.err.println(&quot;[Around] start &quot; + pjp.getSignature());\n        Object retVal = pjp.proceed();\n        System.err.println(&quot;[Around] done &quot; + pjp.getSignature());\n        return retVal;\n    }\n}\n</code></pre>\n<p>应用 Aspect</p>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\n@EnableAspectJAutoProxy\npublic class AppConfig {\n    ...\n}\n</code></pre>\n<p>通过注解实现 Aspect</p>\n<pre><code class=\"language-java\">@Component\npublic class XXXService {\n    @MyCustomAnno(&quot;myvalue&quot;) //自定义注解\n    public XXX myfun(String arg1, String arg2) {}\n}\n</code></pre>\n<p>截获注解</p>\n<pre><code class=\"language-java\">@Aspect\n@Component\npublic class MetricAspect {\n    @Around(&quot;@annotation(metricTime)&quot;)\n    public Object metric(ProceedingJoinPoint joinPoint, MyCustomAnno myCustomAnno) throws Throwable {\n        String myValue = myCustomAnno.value(); // 获取注解值\n\n        try {\n            return joinPoint.proceed();\n        } finally {\n            // ...\n        }\n    }\n}\n</code></pre>\n<p>不要用 final 修饰被 AOP 代理的类的成员<br>AOP 代理的对象仅继承了原对象的所有方法，其字段不会继承（因为字段可能是 IoC 注入的，无法保证正确初始化）<br>故访问 AOP 代理的对象时应仅调用其方法，而不访问其属性（否则 NPE）</p>\n","tags":["java","spring"]},{"id":"java-spring-ioc","url":"https://yieldray.fun/posts/java-spring-ioc","title":"spring之IoC","date_published":"2023-04-04T14:14:14.000Z","date_modified":"2023-04-04T14:14:14.000Z","content_text":"<p><a href=\"https://docs.spring.io/spring-framework/docs/current/reference/html/index.html\">https://docs.spring.io/spring-framework/docs/current/reference/html/index.html</a></p>\n<h1>IoC</h1>\n<p>Inversion of Control</p>\n<p><a href=\"https://docs.spring.io/spring-framework/docs/current/reference/html/core.html\">https://docs.spring.io/spring-framework/docs/current/reference/html/core.html</a></p>\n<h1>annotation 依赖注入</h1>\n<pre><code class=\"language-java\">// AppConfig.java\n@Configuration // 标记配置类\n@ComponentScan // 自动搜索当前类所在的包以及子包，把所有标注为@Component的Bean自动创建出来，并根据@Autowired进行装配\npublic class AppConfig {\n\n    @SuppressWarnings(&quot;resource&quot;)\n    public static void main(String[] args) {\n        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // 通过配置类创建Context\n\n        // 从Context获取Bean\n        UserService userService = context.getBean(UserService.class);\n\n        // 测试\n        User user = userService.login(&quot;bob@example.com&quot;, &quot;password&quot;);\n        System.out.println(user.getName());\n    }\n}\n\n// service/User.java\npublic class User {\n    private long id;\n    private String email;\n    private String password;\n    private String name;\n    public User(long id, String email, String password, String name);\n    // 略，javaBean\n}\n\n// service/MailService.java\n@Component\npublic class MailService {\n    ZoneId zoneId = ZoneId.systemDefault();\n    public String getTime() {\n        return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME); }\n    public void sendLoginMail(User user) {\n        System.err.println(String.format(&quot;Hi, %s! You are logged in at %s&quot;, user.getName(), getTime())); }\n}\n\n// service/UserService.java\n@Component\npublic class UserService {\n    @Autowired // 此处不必要，因为构造函数已注入\n    MailService mailService;\n\n    public UserService(@Autowired MailService mailService) {\n        this.mailService = mailService;\n    }\n\n    private List&lt;User&gt; users = new ArrayList&lt;&gt;(List.of(\n            new User(1, &quot;bob@example.com&quot;, &quot;password&quot;, &quot;Bob&quot;),\n            new User(2, &quot;alice@example.com&quot;, &quot;password&quot;, &quot;Alice&quot;),\n            new User(3, &quot;tom@example.com&quot;, &quot;password&quot;, &quot;Tom&quot;)));\n\n    public User login(String email, String password) {\n        for (User user : users) {\n            if (user.getEmail().equalsIgnoreCase(email) &amp;&amp; user.getPassword().equals(password)) {\n                mailService.sendLoginMail(user);\n                return user;\n            }\n        }\n        throw new RuntimeException(&quot;login failed.&quot;);\n    }\n\n    public User getUserById(long id) {\n        return this.users.stream().filter(user -&gt; user.getId() == id).findFirst().orElseThrow();\n    }\n}\n</code></pre>\n<h1>自定义装配 Bean</h1>\n<p>装配的 bean 默认为单例，下面则声明为原型（每次返回不同的实例）</p>\n<pre><code class=\"language-java\">@Component\n@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)\n// 或 @Scope(&quot;prototype&quot;)\npublic class ProtoService { }\n</code></pre>\n<pre><code class=\"language-java\">interface Checker {\n    public Boolean check(String str);\n}\n\n@Component\n@Order(1) // 可选，声明顺序\nclass CheckerOne implements Checker {\n    @Override\n    public Boolean check(String str) {\n        return str.length() &gt; 0;\n    }\n}\n\n@Component\n@Order(2)\nclass CheckTwo implements Checker {\n    @Override\n    public Boolean check(String str) {\n        return str.startsWith(&quot;*&quot;);\n    }\n}\n\npublic class ChainService {\n    @Autowired // 注入列表\n    List&lt;Checker&gt; checkers;\n}\n</code></pre>\n<p>可选注入</p>\n<pre><code class=\"language-java\">@Component\npublic class MailService {\n    @Autowired(required = false)\n    ZoneId zoneId = ZoneId.systemDefault();\n}\n</code></pre>\n<p>第三方 Bean（在 <code>@Configuration</code> 类中创建）</p>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\npublic class AppConfig {\n    @Bean // 此方法仅调用一次\n    ZoneId createZoneId() {\n        return ZoneId.of(&quot;Z&quot;);\n    }\n}\n</code></pre>\n<p>初始化和销毁 Bean</p>\n<pre><code class=\"language-java\">@Component\npublic class MailService {\n    @PostConstruct\n    public void init() {}\n\n    @PreDestroy\n    public void shutdown() {}\n}\n</code></pre>\n<p>指定别名</p>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\npublic class AppConfig {\n    @Bean\n    @Primary // 首选项\n    DataSource createMasterDataSource() {\n        ...\n    }\n\n    @Bean\n    @Qualifier(&quot;slave&quot;) // 相当于 @Bean(&quot;slave&quot;)\n    DataSource createSlaveDataSource() {\n        ...\n    }\n}\n</code></pre>\n<p>通过工厂模式创建 Bean</p>\n<pre><code class=\"language-java\">@Component\npublic class ZoneIdFactoryBean implements FactoryBean&lt;ZoneId&gt; {\n    String zone = &quot;Z&quot;;\n\n    @Override\n    public ZoneId getObject() throws Exception { return ZoneId.of(zone); }\n\n    @Override\n    public Class&lt;?&gt; getObjectType() { return ZoneId.class; }\n}\n</code></pre>\n<h1>根据 profile 条件注入</h1>\n<p>spring 启动时可以指定 profile</p>\n<pre><code class=\"language-sh\">-Dspring.profiles.active=test\n# 可以指定多个 profile\n-Dspring.profiles.active=test,master\n</code></pre>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\npublic class AppConfig {\n    @Bean\n    @Profile(&quot;!test&quot;)\n    ZoneId createZoneId() {\n        return ZoneId.systemDefault();\n    }\n\n    @Bean\n    @Profile(&quot;test&quot;)\n    ZoneId createZoneIdForTest() {\n        return ZoneId.of(&quot;America/New_York&quot;);\n    }\n\n    @Bean\n    @Profile({ &quot;test&quot;, &quot;master&quot; }) // 要求满足多个profile\n    ZoneId createZoneId() {\n        return ZoneId.systemDefault();\n    }\n}\n</code></pre>\n<h1>通过实现 Condition 接口条件创建 Bean</h1>\n<pre><code class=\"language-java\">public class OnSmtpEnvCondition implements Condition {\n    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {\n        return &quot;true&quot;.equalsIgnoreCase(System.getenv(&quot;smtp&quot;));\n    }\n}\n\n@Component\n@Conditional(OnSmtpEnvCondition.class)\npublic class SmtpMailService implements MailService {\n    ...\n}\n</code></pre>\n<p>类似的 conditional 注解还有</p>\n<pre><code class=\"language-java\">@ConditionalOnProperty(name=&quot;property_fieldname_is&quot;, havingValue=&quot;true&quot;)\n@ConditionalOnProperty(name = &quot;property_fieldname_test&quot;, havingValue = &quot;test_string_value&quot;, matchIfMissing = true)\n@ConditionalOnClass(name = &quot;javax.mail.Transport&quot;)\n</code></pre>\n<h1>注入 Resource <code>org.springframework.core.io.Resource</code></h1>\n<pre><code class=\"language-java\">@Component\npublic class AppService {\n    @Value(&quot;classpath:/file.txt&quot;) // classpath: 相当于 src/main/resources 目录\n    private Resource resource1;\n\n    @Value(&quot;file:/path/to/file.txt&quot;) // file: 相当于项目根目录\n    private Resource resource2;\n\n    @PostConstruct\n    public void init() throws IOException {\n        try (var reader = new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {\n            // ...\n        }\n    }\n}\n</code></pre>\n<p>从 classpath 下的 properties 文件注入</p>\n<pre><code class=\"language-java\">@Configuration\n@ComponentScan\n@PropertySource(&quot;app.properties&quot;) // classpath下的app.properties文件\npublic class AppConfig {\n    @Value(&quot;${option_key}&quot;)\n    // @Value(&quot;${option_key:default_value}&quot;)\n    String some_option;\n\n    @Bean\n    String injectToParam(@Value(&quot;${field_name:default_value}&quot;) String fieldName) {\n        return fieldName;\n    }\n}\n</code></pre>\n<p>通过 javaBean 进行配置</p>\n<pre><code class=\"language-java\">// 注入\n@Component\npublic class MyConfig {\n    @Value(&quot;${fieldName}&quot;)\n    private String fieldName;\n}\n\n// 读取\n@Component\npublic class MyService {\n    @Value(&quot;#{fieldName}&quot;)\n    private String fieldName;\n}\n</code></pre>\n","tags":["java","spring"]},{"id":"ip-tool","url":"https://yieldray.fun/posts/ip-tool","title":"ip query","date_published":"2023-04-01T12:00:00.000Z","date_modified":"2023-04-01T12:00:00.000Z","content_text":"<h1>current ip address</h1>\n<p><a href=\"https://ipconfig.me\">https://ipconfig.me</a></p>\n<p><a href=\"https://ifconfig.co/\">https://ifconfig.co/</a></p>\n<p><a href=\"https://zh-hans.ipshu.com/my_info\">https://zh-hans.ipshu.com/my_info</a></p>\n<p><a href=\"https://myip.ipip.net/\">https://myip.ipip.net/</a></p>\n<p><a href=\"https://ipapi.co/json\">https://ipapi.co/json</a></p>\n<p><a href=\"https://ip.guide/\">https://ip.guide/</a></p>\n<p><a href=\"https://www.ip.network/\">https://www.ip.network/</a></p>\n<p><a href=\"https://www.ipify.org/\">https://www.ipify.org/</a></p>\n<p><a href=\"https://icanhazip.com/\">https://icanhazip.com/</a></p>\n<p><a href=\"https://ip4.me/\">https://ip4.me/</a></p>\n<p><a href=\"https://curlmyip.org/\">https://curlmyip.org/</a></p>\n<p><a href=\"https://ip.useragentinfo.com/\">https://ip.useragentinfo.com/</a></p>\n<p><a href=\"https://codetabs.com/ip-geolocation/ip-geolocation.html\">https://codetabs.com/ip-geolocation/ip-geolocation.html</a></p>\n<p><a href=\"https://ipw.cn/\">https://ipw.cn/</a></p>\n<p><a href=\"http://ip.renfei.net/\">http://ip.renfei.net/</a></p>\n<p><a href=\"https://ipcheck.ing/\">https://ipcheck.ing/</a></p>\n<p><a href=\"https://ipinfo.io/\">https://ipinfo.io/</a> (commercial)</p>\n<h1>domain for ip</h1>\n<p><a href=\"https://nip.io/\">https://nip.io/</a></p>\n<p><a href=\"https://sslip.io/\">https://sslip.io/</a></p>\n<h1>domain for localhost</h1>\n<p><a href=\"https://local.gd/\">https://local.gd/</a></p>\n<p><a href=\"https://get.localhost.direct/\">https://get.localhost.direct/</a></p>\n<h1>certificate</h1>\n<p><a href=\"https://crt.sh/\">https://crt.sh/</a></p>\n<h1>dns</h1>\n<p><a href=\"https://dnschecker.org/\">https://dnschecker.org/</a></p>\n<p><a href=\"https://www.nslookup.io/\">https://www.nslookup.io/</a></p>\n<p><a href=\"https://dns.fish/\">https://dns.fish/</a></p>\n<p><a href=\"https://www.dns.toys/\">https://www.dns.toys/</a></p>\n<h1>speedtest</h1>\n<p><a href=\"https://www.itdog.cn/\">https://www.itdog.cn/</a></p>\n<h1>url shortener</h1>\n<p><a href=\"https://go.shorta.link/\">https://go.shorta.link/</a></p>\n<h1>others</h1>\n<p><a href=\"http://opendata.baidu.com/api.php?co=&amp;resource_id=6006&amp;oe=utf8&amp;query=1.2.4.8\">http://opendata.baidu.com/api.php?co=&amp;resource_id=6006&amp;oe=utf8&amp;query=1.2.4.8</a><br><a href=\"https://github.com/ihmily/ip-info-api\">https://github.com/ihmily/ip-info-api</a><br><a href=\"https://messwithdns.net/\">https://messwithdns.net/</a></p>\n","tags":["ip"]},{"id":"web-console","url":"https://yieldray.fun/posts/web-console","title":"Web Console","date_published":"2023-03-25T11:00:00.000Z","date_modified":"2023-03-25T11:00:00.000Z","content_text":"<p><a href=\"https://developer.mozilla.org/docs/Web/API/console\">https://developer.mozilla.org/docs/Web/API/console</a></p>\n<pre><code class=\"language-ts\">interface Console {\n    log(...data: any[]): void;\n    debug(...data: any[]): void;\n    info(...data: any[]): void;\n    warn(...data: any[]): void;\n    error(...data: any[]): void;\n    clear(): void;\n\n    trace(...data: any[]): void;\n    assert(condition?: boolean, ...data: any[]): void;\n    table(tabularData?: any, properties?: string[]): void;\n\n    dir(item?: any, options?: any): void;\n    dirxml(...data: any[]): void;\n\n    group(...data: any[]): void;\n    groupCollapsed(...data: any[]): void;\n    groupEnd(): void;\n\n    count(label?: string): void;\n    countReset(label?: string): void;\n\n    time(label?: string): void;\n    timeEnd(label?: string): void;\n    timeLog(label?: string, ...data: any[]): void;\n    timeStamp(label?: string): void;\n}\n\ndeclare var console: Console;\n</code></pre>\n","tags":["web-api"]},{"id":"csharp-intro","url":"https://yieldray.fun/posts/csharp-intro","title":"c#入门","date_published":"2023-03-23T21:00:00.000Z","date_modified":"2023-03-23T21:00:00.000Z","content_text":"<hr>\n<h1>Hello, world</h1>\n<p><a href=\"https://try.dot.net/\">https://try.dot.net/</a></p>\n<pre><code class=\"language-cs\">using System; // 命名空间\n\nclass Hello\n{\n    static void Main() // Main 静态方法 是 C# 程序的入口点\n    {\n        string str = &quot;world&quot;;\n        // Console 类在 System 命名空间下\n        Console.WriteLine($&quot;Hello, {str}&quot;);\n        // 相当于 System.Console.WriteLine\n    }\n}\n</code></pre>\n<h1>类型系统</h1>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/tour-of-csharp/#code-try-0\">C#语言介绍</a><br>C# 有两种类型：<em>值类型</em>和<em>引用类型</em></p>\n<ul>\n<li>值类型<ul>\n<li>简单类型<ul>\n<li>有符号整型：<code>sbyte</code>、<code>short</code>、<code>int</code>、<code>long</code></li>\n<li>无符号整型：<code>byte</code>、<code>ushort</code>、<code>uint</code>、<code>ulong</code></li>\n<li>Unicode 字符：<code>char</code> （表示 UTF-16 代码单元）</li>\n<li>IEEE 二进制浮点：<code>float</code>、<code>double</code></li>\n<li>高精度十进制浮点数：<code>decimal</code></li>\n<li>布尔值：<code>bool</code> （<code>true</code> 或 <code>false</code>）</li>\n<li>枚举类型 <code>enum E {...} </code></li>\n<li>结构 <code>struct S {...}</code></li>\n<li>可以为 null 的值类型 （对于所有不可以为 null 的类型 <code>T</code>，都有对应的可以为 null 的类型 <code>T?</code>）</li>\n<li>元组 <code>(T1, T2, ...)</code></li>\n</ul>\n</li>\n</ul>\n</li>\n<li>引用类型<ul>\n<li>类<ul>\n<li>其他<strong>所有</strong>类型的最终基类：<code>object</code></li>\n<li>Unicode 字符串：<code>string</code> （表示 UTF-16 代码单元序列）</li>\n<li><code>class C {...}</code></li>\n</ul>\n</li>\n<li>接口 <code>interface I {...}</code></li>\n<li>数组类型 （一维、多维和交错。 例如：<code>int[]</code>、<code>int[,]</code> 和 <code>int[][]</code>）</li>\n<li>委托类型 <code>delegate int D(...)</code></li>\n</ul>\n</li>\n</ul>\n<p><code>record</code> 类型（<code>record struct</code> 或 <code>record class</code>）<br><code>class</code>、<code>struct</code>、<code>interface</code> 和 <code>delegate</code> 类型全部支持泛型</p>\n<p>数组隐式派生自 <code>System.Array</code> 类<br>例如： <code>int</code> 类型实际为 <code>System.Int32</code></p>\n<h2>自动装箱</h2>\n<pre><code class=\"language-cs\">int i = 123;\nobject o = i;    // 将值类型的值分配给 object 对象引用时，会分配一个“箱”来保存此值。 该箱是引用类型的实例，此值会被复制到该箱。\nint j = (int)o;  // 当 object 引用被显式转换成值类型时，将检查引用的 object 是否是具有正确值类型的箱。 如果检查成功，则会将箱中的值复制到值类型\n</code></pre>\n<h2>类和对象</h2>\n<p><a href=\"https://learn.microsoft.com/zh-CN/dotnet/csharp/language-reference/operators/lambda-operator\"><code>=&gt;</code> lambda 运算符</a></p>\n<pre><code class=\"language-cs\">var p1 = new Point(0, 10); // 类型为 Point?\nPoint p2 = new(10, 20);    // 类型为 Point\nvar p3 = new Point(0, 0) { X = 20, Y = 30 }; // (20, 30)\n\nConsole.WriteLine(p1);\nConsole.WriteLine(p2);\nConsole.WriteLine(p3);\n\npublic class Point\n{\n    public int X;\n    public int Y;\n\n    public (int, int) Pos // 属性访问器\n    {\n        get =&gt; (X, Y);\n        set =&gt; (X, Y) = value; // value 代表等号右边的值\n    }\n\n    public Point(int x, int y) =&gt; (X, Y) = (x, y);\n\n    public override string ToString()\n    {\n        return $&quot;({X}, {Y})&quot;;\n    }\n}\n</code></pre>\n<p><code>readonly</code> 修饰符声明 <em>只读字段</em></p>\n<pre><code class=\"language-cs\">public class Color\n{\n    public static readonly Color Black = new(0, 0, 0);\n    public static readonly Color White = new(255, 255, 255);\n\n    public static readonly Color Red = new(255, 0, 0);\n    public static readonly Color Green = new(0, 255, 0);\n    public static readonly Color Blue = new(0, 0, 255);\n\n    public byte R;\n    public byte G;\n    public byte B;\n\n    public Color(byte r, byte g, byte b)\n    {\n        R = r;\n        G = g;\n        B = b;\n    }\n    public override string ToString() =&gt; $&quot;({r},{g},{b})&quot;;\n}\n</code></pre>\n<h2>泛型类</h2>\n<pre><code class=\"language-cs\">public class Pair&lt;TFirst, TSecond&gt;\n{\n    public TFirst First { get; }\n    public TSecond Second { get; }\n\n    public Pair(TFirst first, TSecond second) =&gt; (First, Second) = (first, second);\n}\n\nvar pair = new Pair&lt;int, string&gt;(1, &quot;two&quot;);\nint i = pair.First;     //TFirst int\nstring s = pair.Second; //TSecond string\n</code></pre>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/tour-of-csharp/program-building-blocks#other-function-members\">其他类型函数成员：构造函数、属性、索引器、事件、运算符和终结器</a></p>\n<h2>继承</h2>\n<pre><code class=\"language-cs\">public class Point3D : Point\n{\n    public int Z { get; set; }\n\n    public Point3D(int x, int y, int z) : base(x, y)\n    {\n        Z = z;\n    }\n}\n\nPoint a = new(10, 20);\nPoint b = new Point3D(10, 20, 30);\n</code></pre>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/tour-of-csharp/program-building-blocks#virtual-override-and-abstract-methods\">虚方法、重写方法和抽象方法</a></p>\n<h2>结构</h2>\n<pre><code class=\"language-cs\">public struct Point\n{\n    public double X { get; }\n    public double Y { get; }\n\n    public Point(double x, double y) =&gt; (X, Y) = (x, y);\n}\n</code></pre>\n<h2>接口</h2>\n<pre><code class=\"language-cs\">interface IControl\n{\n    void Paint();\n}\n\ninterface ITextBox : IControl\n{\n    void SetText(string text);\n}\n\ninterface IListBox : IControl\n{\n    void SetItems(string[] items);\n}\n\ninterface IComboBox : ITextBox, IListBox { }\n</code></pre>\n<p>实现接口</p>\n<pre><code class=\"language-cs\">interface IDataBound\n{\n    void Bind(Binder b);\n}\n\npublic class EditBox : IControl, IDataBound\n{\n    public void Paint() { }\n    public void Bind(Binder b) { }\n}\n\nEditBox editBox = new();\nIControl control = editBox;\nIDataBound dataBound = editBox;\n</code></pre>\n<h2>枚举</h2>\n<pre><code class=\"language-cs\">public enum SomeRootVegetable\n{\n    HorseRadish,\n    Radish,\n    Turnip\n}\n</code></pre>\n<pre><code class=\"language-cs\">[Flags]\npublic enum Seasons\n{\n    None = 0,\n    Summer = 1,\n    Autumn = 2,\n    Winter = 4,\n    Spring = 8,\n    All = Summer | Autumn | Winter | Spring\n}\n</code></pre>\n<pre><code class=\"language-cs\">var turnip = SomeRootVegetable.Turnip;\n\nvar spring = Seasons.Spring;\nvar startingOnEquinox = Seasons.Spring | Seasons.Autumn;\nvar theYear = Seasons.All;\n</code></pre>\n<h2>元组</h2>\n<pre><code class=\"language-cs\">(string Key, int Value) tp = (&quot;branch&quot;, 666);\nConsole.WriteLine($&quot;1 little black mole {tp.Key} {tp.Value}&quot;);\n\nvar (branch, six) = tp;\nConsole.WriteLine(branch + &quot; &quot; + six);\n\n(_, var a, string b) = (&quot;black mole&quot;, false, &quot;branch&quot;); // 手动声明类型\n</code></pre>\n<p>通过<code>Deconstruct</code>方法实现自定义析构：\n<a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/functional/deconstruct#user-defined-types\">https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/functional/deconstruct#user-defined-types</a></p>\n<h2>可 null 类型</h2>\n<p>可为 <code>null</code> 引用类型和可为 <code>null</code> 值类型提供类似的语义概念：变量可表示值或对象，或者该变量可以为 <code>null</code>。 但可为 <code>null</code> 引用类型和可为 <code>null</code> 值类型的实现方式不同：可为 <code>null</code> 值类型是使用 <code>System.Nullable&lt;T&gt;</code> 实现的，而可为 null 引用类型是使用编译器读取的属性实现的。 例如，<code>string?</code> 和 <code>string</code> 由同一类型表示：<code>System.String</code>。 但 <code>int?</code> 和 <code>int</code> 分别由 <code>System.Nullable&lt;System.Int32&gt;</code> 和 <code>System.Int32</code> 表示</p>\n<h3>可 null 值类型</h3>\n<p>本质为 <code>System.Nullable&lt;T&gt;</code></p>\n<pre><code class=\"language-cs\">double? pi = 3.14;\nchar? letter = &#39;a&#39;;\n\nint m2 = 10;\nint? m = m2;\n\nbool? flag = null;\nint?[] arr = new int?[10];\n</code></pre>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/functional/pattern-matching\">模式匹配</a></p>\n<p>is 运算符和类型模式</p>\n<pre><code class=\"language-cs\">int? a = 42;\nif (a is int valueOfA)\n{\n    Console.WriteLine($&quot;a is {valueOfA}&quot;);\n}\nelse // if (a is null)\n{\n    Console.WriteLine(&quot;a does not have a value&quot;);\n}\n</code></pre>\n<p>或者通过 <code>Nullable&lt;T&gt;</code> 实例</p>\n<pre><code class=\"language-cs\">int? c = 7;\nif (c != null) // or:  if (c.HasValue)\n{\n    Console.WriteLine($&quot;c is {c.Value}&quot;);\n}\nelse\n{\n    Console.WriteLine(&quot;c does not have a value&quot;);\n}\n</code></pre>\n<p>Boxing &amp; Unboxing</p>\n<pre><code class=\"language-cs\">int a = 41;\nobject aBoxed = a;\nint? aNullable = (int?)aBoxed;\nConsole.WriteLine($&quot;Value of aNullable: {aNullable}&quot;);\n// Value of aNullable: 41\n\nobject aNullableBoxed = aNullable;\nif (aNullableBoxed is int valueOfA)\n{\n    Console.WriteLine($&quot;aNullableBoxed is boxed int: {valueOfA}&quot;);\n// aNullableBoxed is boxed int: 41\n}\n</code></pre>\n<h3>可 null 引用类型</h3>\n<p>取消引用值为 null 的变量时，运行时会引发 System.NullReferenceException。</p>\n<pre><code class=\"language-cs\">string message = null;\n\ntry {\n    Console.WriteLine($&quot;The length of the message is {message.Length}&quot;);\n} catch {\n    Console.Write(&quot;NullReferenceException&quot;);\n}\n</code></pre>\n<p>对于返回值，<code>T?</code> 等效于 <code>[MaybeNull]T</code><br>对于参数值，<code>T?</code> 等效于 <code>[AllowNull]T</code></p>\n<h2>is 运算符</h2>\n<pre><code class=\"language-cs\">E is T\n// 返回布尔值\n</code></pre>\n<pre><code class=\"language-cs\">public class Animal { }\n\npublic class Giraffe : Animal { }\n\npublic static class TypeOfExample\n{\n    public static void Main()\n    {\n        object b = new Giraffe();\n        Console.WriteLine(b is Animal);  // output: True\n        Console.WriteLine(b.GetType() == typeof(Animal));  // output: False\n\n        Console.WriteLine(b is Giraffe);  // output: True\n        Console.WriteLine(b.GetType() == typeof(Giraffe));  // output: True\n    }\n}\n</code></pre>\n<h2>as 运算符</h2>\n<pre><code class=\"language-cs\">E as T\n// 相当于\nE is T ? (T)(E) : (T)null\n</code></pre>\n<h2>typeof() 运算符， System.Type 类型</h2>\n<p>typeof 运算符的实参必须为 <em>类型</em> （或 <em>类型形参</em>，即泛型参数）<br>返回 Type 类型实例</p>\n<pre><code class=\"language-cs\">Console.WriteLine(typeof(List&lt;string&gt;));\n\nvoid PrintType&lt;T&gt;() =&gt; Console.WriteLine(typeof(T));\nPrintType&lt;int&gt;();\nPrintType&lt;System.Int32&gt;();\nPrintType&lt;Dictionary&lt;int, char&gt;&gt;();\n</code></pre>\n<h2>switch 模式匹配</h2>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/functional/pattern-matching#compare-discrete-values\">https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/functional/pattern-matching#compare-discrete-values</a></p>\n<h2>函数</h2>\n<p><em>引用参数</em> 使用 <code>ref</code> 修饰符进行声明</p>\n<pre><code class=\"language-cs\">static void Swap(ref int x, ref int y)\n{\n    int temp = x;\n    x = y;\n    y = temp;\n}\n\npublic static void SwapExample()\n{\n    int i = 1, j = 2;\n    Swap(ref i, ref j);\n    Console.WriteLine($&quot;{i} {j}&quot;);    // &quot;2 1&quot;\n}\n</code></pre>\n<p><em>输出参数</em> 使用 <code>out</code> 修饰符进行声明</p>\n<pre><code class=\"language-cs\">static void Divide(int x, int y, out int quotient, out int remainder)\n{\n    quotient = x / y;\n    remainder = x % y;\n}\n\npublic static void OutUsage()\n{\n    Divide(10, 3, out int quo, out int rem);\n    Console.WriteLine($&quot;{quo} {rem}&quot;);\t// &quot;3 1&quot;\n}\n</code></pre>\n<p><em>参数数组</em> （不定参数）使用 <code>params</code> 修饰符进行声明</p>\n<pre><code class=\"language-cs\">public class Console\n{\n    public static void Write(string fmt, params object[] args) { }\n    public static void WriteLine(string fmt, params object[] args) { }\n    // ...\n}\n</code></pre>\n<pre><code class=\"language-cs\">int x = 3, y = 4, z = 5;\nConsole.WriteLine(&quot;x={0} y={1} z={2}&quot;, x,y,z);\n// 相当于\nobject[] args = new object[3] {x,y,z};\nConsole.WriteLine(&quot;x={0} y={1} z={2}&quot;, args);\n</code></pre>\n<h2>委托</h2>\n<p><code>delegate</code> 关键字声明 <em>委托</em> 类型<br>委托类型可为 lambda 表达式 提供类型</p>\n<pre><code class=\"language-cs\">using System;\n\npublic class Program\n{\n  public delegate int Add(int a, int b);\n  public static void Main()\n  {\n   Add add = (a, b) =&gt; a + b;\n   Console.WriteLine( add(1,2) );\n  }\n}\n</code></pre>\n<h2>async/await</h2>\n<p>异步函数返回 <code>System.Threading.Tasks.Task</code> 泛型类</p>\n<pre><code class=\"language-cs\">using System;\nusing System.Threading.Tasks;\n\nclass Test {\n  public static async Task&lt;string&gt; asyncMethod()\n  {\n    return nameof(asyncMethod);\n  }\n}\n\npublic class Program\n{\n  public static async void Main()\n  {\n    Console.WriteLine(&quot;before&quot;);\n    Console.WriteLine(await Test.asyncMethod());\n    Console.WriteLine(&quot;after&quot;);\n  }\n}\n</code></pre>\n<h2>Attribute</h2>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/tour-of-csharp/features#attributes\">https://learn.microsoft.com/zh-cn/dotnet/csharp/tour-of-csharp/features#attributes</a></p>\n<pre><code class=\"language-cs\">class Program\n{\n    public class HelpAttribute : Attribute\n    {\n        string _url;\n        string _topic;\n\n        public HelpAttribute(string url) =&gt; _url = url;\n\n        public string Url =&gt; _url;\n\n        public string Topic\n        {\n            get =&gt; _topic;\n            set =&gt; _topic = value;\n        }\n    }\n\n    // HelpAttribute 以 Attribute 结尾，可省略为 Help\n    [Help(&quot;https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features&quot;)]\n    public class Widget\n    {\n        [Help(&quot;https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features&quot;,\n        Topic = &quot;Display&quot;)]\n        public void Display(string text) { }\n    }\n\n    public static void Main()\n    {\n        Type widgetType = typeof(Widget);\n\n        object[] widgetClassAttributes = widgetType.GetCustomAttributes(typeof(HelpAttribute), /* inherit */false);\n\n        if (widgetClassAttributes.Length &gt; 0)\n        {\n            HelpAttribute attr = (HelpAttribute)widgetClassAttributes[0];\n            Console.WriteLine($&quot;Widget class help URL : {attr.Url} - Related topic : {attr.Topic}&quot;);\n        }\n\n        System.Reflection.MethodInfo displayMethod = widgetType.GetMethod(nameof(Widget.Display));\n\n        object[] displayMethodAttributes = displayMethod.GetCustomAttributes(typeof(HelpAttribute), false);\n\n        if (displayMethodAttributes.Length &gt; 0)\n        {\n            HelpAttribute attr = (HelpAttribute)displayMethodAttributes[0];\n            Console.WriteLine($&quot;Display method help URL : {attr.Url} - Related topic : {attr.Topic}&quot;);\n        }\n    }\n}\n</code></pre>\n<h2>匿名类型</h2>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/types/anonymous-types\">https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/types/anonymous-types</a></p>\n<pre><code class=\"language-cs\">var v = new { Amount = 108, Message = &quot;Hello&quot; };\nConsole.WriteLine(v.Amount + v.Message);\n</code></pre>\n<h2>错误处理</h2>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/exceptions/\">https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/exceptions/</a></p>\n<h1>语言集成查询 (LINQ)</h1>\n<p><a href=\"https://learn.microsoft.com/zh-cn/dotnet/csharp/tutorials/working-with-linq\">https://learn.microsoft.com/zh-cn/dotnet/csharp/tutorials/working-with-linq</a></p>\n","tags":["csharp"]},{"id":"tail-call","url":"https://yieldray.fun/posts/tail-call","title":"尾调用","date_published":"2023-03-14T17:00:00.000Z","date_modified":"2023-03-14T17:00:00.000Z","content_text":"<h1>尾调用</h1>\n<p>尾调用是指一个函数内的最后一个动作是<em>返回一个函数的调用结果</em>的情形。<br>函数最后返回的位置成为尾位置。</p>\n<pre><code class=\"language-js\">function fn(args) {\n    if (x) return a();\n    return b();\n}\n</code></pre>\n<p>尾位置是函数调用时，当前函数（可以）无需保留自身调用栈。因为函数捕获的变量可以直接成为尾调用函数的参数。</p>\n<h1>尾递归</h1>\n<p>若函数在尾位置调用自身（或是一个尾调用本身的其他函数等等），则称这种情况为尾递归。</p>\n<pre><code class=\"language-js\">function factorial(n) {\n    if (n === 1) return 1;\n    return n * factorial(n - 1);\n}\n\nfunction factorial(n, total = 1 /* 这个参数永远不应该被实际提供 */) {\n    if (n === 1) return total;\n    return factorial(n - 1, n * total);\n}\n// 注：需要 &quot;use strict&quot;\n</code></pre>\n<p>参见：<a href=\"https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8\">https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8</a><br>参见：<a href=\"https://es6.ruanyifeng.com/#docs/function#%E5%B0%BE%E8%B0%83%E7%94%A8%E4%BC%98%E5%8C%96\">https://es6.ruanyifeng.com/#docs/function#%E5%B0%BE%E8%B0%83%E7%94%A8%E4%BC%98%E5%8C%96</a></p>\n","tags":["js"]},{"id":"git-common","url":"https://yieldray.fun/posts/git-common","title":"git常用命令","date_published":"2023-02-27T12:00:00.000Z","date_modified":"2023-02-27T12:00:00.000Z","content_text":"<p>文档：<a href=\"https://git-scm.com/docs\">https://git-scm.com/docs</a><br>pro-git：<a href=\"https://git-scm.com/book/zh/v2/\">https://git-scm.com/book/zh/v2/</a><br>github：<a href=\"https://github.com/git-guides\">https://github.com/git-guides</a><br>参考：<a href=\"https://github.com/xjh22222228/git-manual/blob/main/README.md\">https://github.com/xjh22222228/git-manual/blob/main/README.md</a></p>\n<h1>提交</h1>\n<pre><code class=\"language-sh\">git commit [-m &lt;message&gt;]\ngit commit -a\ngit commit --amend\n\ngit status\ngit log [--oneline]\ngit diff\n</code></pre>\n<h1>分支</h1>\n<pre><code class=\"language-sh\">git branch &lt;branch&gt;\ngit checkout &lt;branch&gt;\ngit checkout -b &lt;branch&gt;\n\ngit switch &lt;branch&gt;\ngit switch -c &lt;branch&gt;\n\ngit branch -d &lt;branch&gt;\ngit push &lt;remote&gt; --delete &lt;branch&gt;\n</code></pre>\n<h1>重命名分支</h1>\n<pre><code class=\"language-sh\">git branch -m &lt;branch&gt;            # 重命名当前\ngit branch -m &lt;branch&gt; &lt;new-name&gt; # 重命名指定\ngit push origin :&lt;branch&gt;         # 删除旧分支\ngit push -u origin &lt;new-name&gt;     # 推送新分支\n</code></pre>\n<h1>远程</h1>\n<pre><code class=\"language-sh\">git fetch --all\ngit branch -a\ngit branch -vv # 查看本地分支所关联的远程分支\n\ngit remote add &lt;remote&gt; &lt;url&gt;\ngit remote # 查看远程仓库\ngit remote -v # 查看当前远程仓库地址\n\ngit remote show &lt;remote&gt; # 查看指定远程仓库信息\ngit remote rename &lt;remote&gt; &lt;new-name&gt;\ngit remote remove &lt;remote&gt;\ngit remote set-url origin &lt;url&gt;\n\ngit fetch &lt;remote&gt;\ngit fetch &lt;remote&gt;/&lt;branch&gt;\n\ngit push &lt;remote&gt; [&lt;branch&gt;]\ngit push &lt;remote&gt; &lt;branch&gt;:&lt;remote-branch&gt;\ngit push &lt;remote&gt; --delete &lt;branch&gt;\n\ngit checkout -b &lt;branch&gt; &lt;remote&gt;/&lt;branch&gt;\ngit checkout --track &lt;remote&gt;/&lt;branch&gt;\ngit checkout &lt;branch&gt; # 从远程分支创建同名本地分支\n\ngit branch -u &lt;remote&gt;/&lt;branch&gt; # 将当前分支关联至远程分支\ngit push -u &lt;remote&gt; &lt;branch&gt; # 关联并推送\n</code></pre>\n<h1>标签</h1>\n<pre><code class=\"language-sh\">git show &lt;tag&gt;\ngit tag &lt;tag&gt;\ngit tag -a &lt;tag&gt; -m &lt;msg&gt; [&lt;hash&gt;]\ngit push &lt;remote&gt; &lt;tag&gt;\ngit push &lt;remote&gt; --tags\ngit tag -d &lt;tag&gt;\ngit push &lt;remote&gt; --delete &lt;tag&gt;\n</code></pre>\n<h1>合并</h1>\n<pre><code class=\"language-sh\">git checkout master\ngit merge fix\n\ngit checkout fix\ngit rebase master\n</code></pre>\n<h1>撤销</h1>\n<pre><code class=\"language-sh\">git reset --soft &lt;commit-hash&gt;  # 仅回退提交历史，保留修改\ngit reset --hard &lt;commit-hash&gt;  # 完全回退提交、暂存区和工作区的更改\ngit revert &lt;commit-id&gt;          # 生成一个新提交，来撤销指定的提\n</code></pre>\n<h1>github create a new repository</h1>\n<pre><code class=\"language-sh\">echo &quot;# test&quot; &gt;&gt; README.md\ngit init\ngit add README.md\ngit commit -m &quot;first commit&quot;\ngit branch -M main # 将默认的master分支强制改为main\ngit remote add origin https://github.com/user/repo.git # 添加远程仓库origin\ngit push -u origin main # 将main分支关联并推送至远程origin\n</code></pre>\n","tags":["git"]},{"id":"userscript","url":"https://yieldray.fun/posts/userscript","title":"UserScript","date_published":"2023-01-29T20:00:00.000Z","date_modified":"2023-01-29T20:00:00.000Z","content_text":"<h1>metadata</h1>\n<p><a href=\"https://violentmonkey.github.io/api/gm/\">https://violentmonkey.github.io/api/gm/</a><br><a href=\"https://www.tampermonkey.net/documentation.php?locale=zh\">https://www.tampermonkey.net/documentation.php?locale=zh</a><br><a href=\"https://wiki.greasespot.net/Greasemonkey_Manual:API\">https://wiki.greasespot.net/Greasemonkey_Manual:API</a><br><a href=\"https://docs.scriptcat.org/docs/dev/api/\">https://docs.scriptcat.org/docs/dev/api/</a><br><a href=\"https://www.chromium.org/developers/design-documents/user-scripts/\">https://www.chromium.org/developers/design-documents/user-scripts/</a></p>\n<h2>项目相关</h2>\n<pre><code class=\"language-js\">// ==UserScript==\n// @name          Violentmonkey Script\n// @name:zh-CN    暴力猴脚本\n// @namespace https://violentmonkey.github.io #此字段一般填写项目主页，非必须\n// @version 1.0\n// @description         This script rocks.\n// @description:zh-CN   这个脚本很棒！\n// @icon https://my.cdn.com/icon.png #也可以使用 data:image/jpeg;base64,xxx\n// ==/UserScript==\n</code></pre>\n<h2>运行相关</h2>\n<pre><code class=\"language-js\">// @require https://my.cdn.com/jquery.js\n// @run-at #考虑到兼容性此字段可以不填，一般默认运行在DOMContentLoaded事件发送后\n// @noframes #不需要值，出现此字段则脚本不在frame中执行\n// @resource logo https://my.cdn.com/logo.png  #此字段声明了允许通过GM_getResourceText()和GM_getResourceURL()函数拿到的资源\n// @resource text https://my.cdn.com/some-text.txt\n// @connect tampermonkey.net #此字段声明了允许通过GM_xmlhttpRequest()函数访问的域名，指定顶级域名会同时允许其子域名\n// @grant none #默认值，允许脚本直接获取真实的DOM，相当于直接在页面中载入了一个脚本（但可以获取GM_info对象，@require同理）\n// @grant unsafeWindow #隔离浏览器上下文，unsafeWindow指向真实页面的window对象（@require虽然也隔离上下文，但可以直接访问DOM）\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_setClipboard\n// @grant window.close\n// @grant window.focus\n// @grant window.onurlchange\n</code></pre>\n<p>小技巧，绕过 unsafeWindow</p>\n<pre><code class=\"language-js\">console.log(GM_setValue); // OK\n\nfunction main() {\n    console.log(window);\n    // console.log(GM_setValue); // Uncaught ReferenceError: GM_setValue is not defined\n}\nconst script = document.createElement(&quot;script&quot;);\nscript.textContent = &quot;(&quot; + main.toString() + &quot;)();&quot;;\ndocument.body.appendChild(script);\n</code></pre>\n<h2>网址匹配</h2>\n<p><a href=\"https://developer.chrome.com/docs/extensions/mv3/match_patterns/\">https://developer.chrome.com/docs/extensions/mv3/match_patterns/</a></p>\n<pre><code class=\"language-js\">// @match *://*.google.com/foo*bar #不匹配锚点\n// @include /\\.com\\.hk\\// #不建议使用，使用@match即可\n// @exclude 与include相反\n</code></pre>\n<h2>更新相关</h2>\n<pre><code class=\"language-js\">// @downloadURL #此字段用于检测更新\n// @supportURL #此字段指定了“反馈”按钮指向的地址\n// @homepageURL #主页\n</code></pre>\n<h1>GM functions</h1>\n<h2>持久化存储</h2>\n<p>键名为字符串，值是通过 JSON 序列化</p>\n<pre><code class=\"language-js\">let value = GM_getValue(key, defaultValue);\n\nGM_setValue(key, value); // GM_setValue(String(key), value)\n\nGM_deleteValue(key);\n\nlet arrayOfKeys = GM_listValues();\n\nlet listenerId = GM_addValueChangeListener(key, (key, oldValue, newValue, remote) =&gt; void);\n\nGM_removeValueChangeListener(listenerId)\n</code></pre>\n<h2>获取资源</h2>\n<pre><code class=\"language-js\">// @resource logo https://my.cdn.com/logo.png\n// @resource text https://my.cdn.com/some-text.txt\n\nlet text = GM_getResourceText(&quot;text&quot;);\n\nlet blobUrl = GM_getResourceURL(&quot;logo&quot;);\nlet blobOrDataUrl = GM_getResourceURL(&quot;logo&quot;, isBlobUrl);\n</code></pre>\n<h2>DOM</h2>\n<pre><code class=\"language-js\">let element1 = GM_addElement(tagName, attributes);\nlet element2 = GM_addElement(parentNode, tagName, attributes);\n\nlet styleElement = GM_addStyle(css);\n\nlet tabControl = GM_openInTab(url, openInBackground);\nlet tabControl = GM_openInTab(url);\ntabControl.onclose = () =&gt; console.log(tabControl.closed === true);\ntabControl.close();\n</code></pre>\n<h2>杂项</h2>\n<pre><code class=\"language-js\">const id = GM_registerMenuCommand(caption, onClick);\nGM_unregisterMenuCommand(caption);\n\nGM_setClipboard(data, type ?? &quot;text/plain&quot;);\n\nGM_notification({ text, title, image, onclick, ondone });\nGM_notification(text, title, image, onclick);\n\n// 不受限制的 xmlhttpRequest\nlet control = GM_xmlhttpRequest({\n    url: &quot;https://example.net/&quot;,\n    method: &quot;POST&quot;,\n    data: &quot;string | ArrayBuffer | Blob | DataView | FormData | ReadableStream | TypedArray | URLSearchParams&quot;,\n    headers: {\n        Referer: &quot;https://example.com/&quot;,\n        &quot;User-Agent&quot;: &quot;GM&quot;,\n    },\n    responseType: &quot;text&quot;, // or &quot;json&quot; or &quot;blob&quot; or &quot;arraybuffer&quot;\n    timeout: 3000,\n});\ncontrol.onabort = (res) =&gt; {};\ncontrol.onerror = (res) =&gt; {};\ncontrol.onload = (res) =&gt; {};\ncontrol.onloadend = (res) =&gt; {};\ncontrol.onloadstart = (res) =&gt; {};\ncontrol.onprogress = (res) =&gt; {};\ncontrol.onreadystatechange = (res) =&gt; {};\ncontrol.ontimeout = (res) =&gt; {};\n// const { status, statusText, readyState, responseHeaders, response, finalUrl } = res;\n\nGM_download({ url, name /* 选项同 GM_xmlhttpRequest */ });\nGM_download(url, name);\n</code></pre>\n<p>通过 GM_xmlhttpRequest 实现 GM_fetch，参见<br><a href=\"https://github.com/Trim21/gm-fetch\">https://github.com/Trim21/gm-fetch</a><br><a href=\"https://github.com/AlttiRi/gm_fetch\">https://github.com/AlttiRi/gm_fetch</a><br><a href=\"https://github.com/mitchellmebane/GM_fetch\">https://github.com/mitchellmebane/GM_fetch</a></p>\n<pre><code class=\"language-js\">/* 例如 */\n\n// ==UserScript==\n// @name        GM_fetch\n// @match       *://*/*\n// @connect     *\n// @grant       unsafeWindow\n// @grant       GM_xmlhttpRequest\n// @require     https://raw.githubusercontents.com/mitchellmebane/GM_fetch/master/GM_fetch.min.js\n// ==/UserScript==\n\nGM_fetch(&quot;http://ip.42.pl/raw&quot;)\n    .then((res) =&gt; res.text())\n    .then(console.log);\n</code></pre>\n<h2><code>GM.*</code> async</h2>\n<p><a href=\"https://violentmonkey.github.io/api/gm/#gm\">https://violentmonkey.github.io/api/gm/#gm</a></p>\n","tags":["js"]},{"id":"css-flexbox","url":"https://yieldray.fun/posts/css-flexbox","title":"CSS flexible box / grid layout","date_published":"2023-01-15T14:30:00.000Z","date_modified":"2023-01-15T14:30:00.000Z","content_text":"<p><a href=\"https://css-tip.com/explore/alignment/\">https://css-tip.com/explore/alignment/</a><br><a href=\"https://2ality.com/2025/10/css-layout.html\">https://2ality.com/2025/10/css-layout.html</a></p>\n<h1><code>display</code></h1>\n<pre><code class=\"language-css\">display =\n  [ &lt;display-outside&gt; || &lt;display-inside&gt; ]         | ...\n</code></pre>\n<ol>\n<li>外部显示类型（outer display type）：决定主盒如何参与流布局。</li>\n<li>内部显示类型（inner display type）：决定非替换元素生成的格式化上下文类型，即其后代盒子的布局方式。</li>\n</ol>\n<p>当容器元素 display 为 <code>[inline-]{flex,grid}</code> 时，其子元素显然参与的不是流布局（而是flex或grid布局），故而此时子元素的外部显示类型无意义。<br>举个例子：子元素设置为 <code>display:inline</code> 和 <code>display:block</code> 效果相同。设置为 <code>display:inline-flex</code> 和 <code>display:flex</code> 效果相同。</p>\n<p><a href=\"https://developer.mozilla.org/docs/Learn/CSS/CSS_layout/Flexbox\">https://developer.mozilla.org/docs/Learn/CSS/CSS_layout/Flexbox</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_flexible_box_layout\">https://developer.mozilla.org/docs/Web/CSS/CSS_flexible_box_layout</a><br><a href=\"https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/\">https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/</a></p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/flex-direction\"><code>flex-direction</code></a></h1>\n<p>主轴</p>\n<p>row (horizontal)<br>column (vertical)</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/flex-wrap\"><code>flex-wrap</code></a></h1>\n<p>是否允许换行</p>\n<p>nowrap (default)<br>wrap<br>wrap-reverse</p>\n<h1><code>{justify,align}-{content,items}</code></h1>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>justify</code></td>\n<td>沿主轴定位</td>\n</tr>\n<tr>\n<td><code>align</code></td>\n<td>沿交叉轴定位</td>\n</tr>\n</tbody></table>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/justify-content\">https://developer.mozilla.org/docs/Web/CSS/justify-content</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/justify-items\">https://developer.mozilla.org/docs/Web/CSS/justify-items</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/align-content\">https://developer.mozilla.org/docs/Web/CSS/align-content</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/align-items\">https://developer.mozilla.org/docs/Web/CSS/align-items</a></p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>content</code></td>\n<td>指定这组元素在（整个）剩余空间的定位</td>\n</tr>\n<tr>\n<td><code>items</code></td>\n<td>指定元素在（元素自身）剩余空间内的位置</td>\n</tr>\n</tbody></table>\n<blockquote>\n<p>content 指整个容器方向的空间（单数，一个容器）；items 为复数形式，指每个元素自己可占据的空间。<br>主轴，justify，默认为水平方向。这些属性在 grid 布局中具有相同的语义。</p>\n</blockquote>\n<h1><code>{justify,align}-self</code></h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/justify-self\">https://developer.mozilla.org/docs/Web/CSS/justify-self</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/align-self\">https://developer.mozilla.org/docs/Web/CSS/align-self</a></p>\n<p>justify-items 为每个元素的 justify-self 指定默认值<br>align-items 为每个元素的 align-self 指定默认值</p>\n<p>显然，在不允许换行的情况下，交叉轴上不会出现一组元素，此时 align-content 无效</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/flex-basis\"><code>flex-basis</code></a></h1>\n<p>指定元素的基础大小/尺寸（而无需 <code>box-sizing: border-box</code>）<br>在 <code>flex-direction: row</code> 下，相当于 width<br>在 <code>flex-direction: column</code> 下，相当于 height</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/flex-grow\"><code>flex-grow</code></a></h1>\n<p>指定元素在（容器）<strong>有剩余空间</strong> 时，元素 <strong>分担</strong> 增长尺寸的 <strong>比例</strong>，<strong>默认值是 0</strong></p>\n<p>（每个元素）增长的尺寸 = 剩余空间 * （这个元素）根据比例得出百分比</p>\n<blockquote>\n<p>剩余空间 = 容器尺寸 - 所有元素的基础尺寸（flex-basis）<br>根据比例得出百分比 = 这个元素的比例 / 所有元素的比例之和</p>\n</blockquote>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/flex-shrink\"><code>flex-shrink</code></a></h1>\n<p>指定元素在（容器）<strong>溢出</strong> 时，元素 <strong>分担</strong> 减小尺寸（收缩一词并不准确）的 <strong>比例</strong>，<strong>默认值是 1</strong></p>\n<p>（每个元素）减小的尺寸 = 溢出空间 * （这个元素）根据比例得出百分比</p>\n<blockquote>\n<p>溢出空间 = 容器尺寸 - 所有元素的基础尺寸（flex-basis）<br>其实和剩余空间的计算方法相同</p>\n</blockquote>\n<p>由于默认值是 1，所以溢出时，每个元素都会收缩。对不应该收缩的元素指定 <code>flex-shrink: 0</code> 即可</p>\n<p>此属性会受到元素指定的最小尺寸的影响，如 min-width<br>min-width 的优先级更高。一旦元素收缩到最小尺寸将不再收缩<br>一些元素有默认的最小尺寸，如 <code>input[type=&quot;text&quot;]</code> 的最小尺寸是 170px-200px<br>要使这种元素也收缩，可以指定其 <code>min-width: 0px</code></p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/flex\"><code>flex</code></a></h1>\n<pre><code class=\"language-css\">flex =\n  none                                                |\n  [ &lt;&#39;flex-grow&#39;&gt; &lt;&#39;flex-shrink&#39;&gt;? || &lt;&#39;flex-basis&#39;&gt; ]\n</code></pre>\n<p>注意，在指定 flex 简写属性时：<br>省略 <code>&lt;flex-grow&gt;</code>时，默认值为 1<br>省略 <code>&lt;flex-shrink&gt;</code> 时，看是否指定了 <code>&lt;flex-basis&gt;</code>；若指定则 <code>&lt;flex-shrink&gt; = 1</code>，否则 <code>&lt;flex-shrink&gt; = 0</code><br>省略 <code>&lt;flex-basis&gt;</code> 时，默认值为 0</p>\n<p>作为单独属性时，flex-grow 默认值为 0，flex-shrink 默认值为 1</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/place-items\"><code>place-items</code></a></h1>\n<p>相当于同时指定 <code>{justify,align}-items</code></p>\n<pre><code class=\"language-css\">place-items =\n  &lt;&#39;align-items&#39;&gt; &lt;&#39;justify-items&#39;&gt;?\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/place-content\"><code>place-content</code></a></h1>\n<p>相当于同时指定 <code>{justify,align}-content</code></p>\n<pre><code class=\"language-css\">place-content =\n  &lt;&#39;align-content&#39;&gt; &lt;&#39;justify-content&#39;&gt;?\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/place-self\"><code>place-self</code></a></h1>\n<p>相当于同时指定 <code>{justify,align}-self</code></p>\n<pre><code class=\"language-css\">place-self =\n  &lt;&#39;align-self&#39;&gt; &lt;&#39;justify-self&#39;&gt;?\n</code></pre>\n<h1><code>grid-template-{columns,rows}</code></h1>\n<h1><code>grid-template-areas grid-area</code></h1>\n<h1><code>grid-{column,row}[-{start,end}]</code></h1>\n<h1><code>grid-auto-{columns,rows}</code></h1>\n<h1><code>grid-auto-flow</code></h1>\n<h1><code>grid</code></h1>\n","tags":["css"]},{"id":"python-tkinter","url":"https://yieldray.fun/posts/python-tkinter","title":"python tkinter","date_published":"2023-01-14T21:21:21.000Z","date_modified":"2023-01-14T21:21:21.000Z","content_text":"<p>tkinter 适合简单的 GUI 程序<br>文档：<a href=\"https://docs.python.org/zh-cn/3/library/tk.html\">https://docs.python.org/zh-cn/3/library/tk.html</a><br>参考：<a href=\"https://www.pytk.net/tkinter.html\">https://www.pytk.net/tkinter.html</a></p>\n<h1>tkinter 使用</h1>\n<pre><code class=\"language-py\"># tkinter 控件\nfrom tkinter import *\nfrom tkinter import messagebox, filedialog\nfrom tkinter.scrolledtext import ScrolledText\n\n# ttk 控件\nfrom tkinter.ttk import *\n\nimport time\nimport os\n\n\nwindow = Tk()\nscreen_w = window.winfo_screenwidth()\nscreen_h = window.winfo_screenheight()\nwindow_w = 800\nwindow_h = 600\nwindow.geometry(\n    f&quot;{window_w}x{window_h}+{int((screen_w-window_w)/2+0.5)}+{int((screen_h-window_h)/2+0.5)}&quot;)\n# 宽x高+横坐标+纵坐标 （整数，坐标可以是负数）\nwindow.title(&quot;Tkinter 测试&quot;)\n\nif os.path.isfile(&quot;favicon.ico&quot;):\n    window.iconbitmap(&quot;favicon.ico&quot;)\n\n# 布局\n# pack/grid/place\n# pack 和 grid 不能混用\n\n# msg = Message(window, text=&quot;你好&quot;, width=60)\n# msg.pack()\n# msg.config(text=&quot;世界&quot;)\n# msg[&quot;text&quot;] = &quot;再见&quot;\n\ndystr = StringVar()\n\n\ndef set_time():\n    dystr.set(time.strftime(&quot;%Y年%M月%d日 %H:%M:%S&quot;))\n    window.after(1000, set_time)\n\n\nset_time()\n\nLabel(window, textvariable=dystr, font=(&quot;微软雅黑&quot;, 16)).grid()\n\n\n########################################################\n\nLabel(window, text=&quot;账号：&quot;).grid(row=1)\nEntry(window).grid(row=1, column=1, padx=0, pady=5)\nLabel(window, text=&quot;密码：&quot;).grid(row=2)\nEntry(window, show=&quot;*&quot;).grid(row=2, column=1, padx=0, pady=5)\n\n\ndef ask_for_quit():\n    result = messagebox.askokcancel(&quot;提示&quot;, &quot;确认退出?&quot;, default=messagebox.CANCEL)\n    if result:\n        window.quit()\n\n\nButton(window, text=&quot;按钮&quot;, command=ask_for_quit).grid(row=3, pady=2)\nButton(window, text=&quot;退出&quot;, command=window.quit).grid(row=3, column=1)\n\n\n########################################################\n\narea = Frame(window)\narea.grid(row=4, rowspan=2, columnspan=5)\n\ntextarea = Text(area, width=50, height=10)\ntextarea.pack(side=LEFT)\n\nsb = Scrollbar(area, orient=VERTICAL, command=textarea.yview)\nsb.pack(side=RIGHT, fill=Y)\n\ntextarea.configure(yscrollcommand=sb.set)\ntextarea.insert(INSERT, &quot;多段文字\\n&quot;*20)\n# area.update()\n\n########################################################\n\nSpinbox(window, values=(&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cookie&quot;)).grid()\n\ncbox = Combobox(window, values=[&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cookie&quot;])\ncbox.bind(&quot;&lt;&lt;ComboboxSelected&gt;&gt;&quot;,\n          lambda e: messagebox.showinfo(&quot;选中&quot;, cbox.get()))\ncbox.grid()\n\n\n########################################################\n\ndybool = BooleanVar(value=False)\ncbtn = Checkbutton(window, text=&quot;Proxy?&quot;, onvalue=True,\n                   offvalue=False, variable=dybool,\n                   command=lambda: messagebox.showinfo(&quot;提示&quot;, str(dybool.get())))\n\ncbtn.grid()\n\n########################################################\n\n\ndef menuCommand(e=None):\n    messagebox.showinfo(&quot;菜单&quot;, &quot;这是菜单&quot;)\n\n\nmain_menu = Menu(window)\n\nsub_menu = Menu(main_menu, tearoff=False)  # tearoff=False 不显示分割线\nsub_menu.add_command(label=&quot;新建&quot;, command=menuCommand, accelerator=&quot;Ctrl+N&quot;)\nsub_menu.add_command(label=&quot;打开&quot;, command=menuCommand, accelerator=&quot;Ctrl+O&quot;)\nsub_menu.add_command(label=&quot;保存&quot;, command=menuCommand, accelerator=&quot;Ctrl+S&quot;)\nwindow.bind(&quot;&lt;Control-n&gt;&quot;, menuCommand)\nwindow.bind(&quot;&lt;Control-N&gt;&quot;, menuCommand)\nwindow.bind(&quot;&lt;Control-o&gt;&quot;, menuCommand)\nwindow.bind(&quot;&lt;Control-O&gt;&quot;, menuCommand)\nwindow.bind(&quot;&lt;Control-s&gt;&quot;, menuCommand)\nwindow.bind(&quot;&lt;Control-S&gt;&quot;, menuCommand)\nsub_menu.add_separator()\nsub_menu.add_command(label=&quot;退出&quot;, command=window.quit)\n\nmain_menu.add_cascade(label=&quot;文件&quot;, menu=sub_menu)\nmain_menu.add_command(label=&quot;编辑&quot;, command=menuCommand)\nmain_menu.add_command(label=&quot;帮助&quot;, command=menuCommand)\nwindow.config(menu=main_menu)\n\n########################################################\n\nclick_menu = Menu(window, tearoff=False)\nclick_menu.add_command(label=&quot;前进&quot;)\nclick_menu.add_command(label=&quot;后退&quot;)\nwindow.bind(&quot;&lt;Button-3&gt;&quot;, lambda e: click_menu.post(e.x_root, e.y_root))\n\n########################################################\n\n\ndef create_top_level():\n    t = Toplevel()\n    t.title(&quot;标题&quot;)\n    t.geometry(&quot;500x500&quot;)\n\n    pb = Progressbar(t, mode=&quot;indeterminate&quot;)\n    pb.start()\n    pb.pack()\n\n    tree = Treeview(t, columns=[&quot;1&quot;, &quot;2&quot;, &quot;3&quot;],\n                    selectmode=&quot;none&quot;, show=&quot;headings&quot;)\n    tree.column(&quot;1&quot;, width=100, anchor=W)\n    tree.column(&quot;2&quot;, width=100, anchor=CENTER)\n    tree.column(&quot;3&quot;, width=100, anchor=E)\n    tree.heading(&quot;1&quot;, text=&quot;column one&quot;)\n    tree.heading(&quot;2&quot;, text=&quot;column two&quot;)\n    tree.heading(&quot;3&quot;, text=&quot;column three&quot;)\n    tree.insert(&quot;&quot;, &quot;end&quot;, values=[&quot;aaa&quot;, &quot;bbb&quot;, &quot;ccc&quot;])\n    tree.pack()\n\n    Label(t, text=&quot;LabelView&quot;).pack()\n    Message(t, text=&quot;MessageView&quot;).pack()\n\n    textarea = ScrolledText(t)\n    textarea.insert(END, &quot;自带滚动条的文本框组件&quot;*100)\n    textarea.pack()\n\n\nmenu_btn = Menubutton(window, text=&quot;菜单按钮&quot;)\nmenu_btn.place(x=400, y=400)\nmenu_menu = Menu(menu_btn, tearoff=False)\nmenu_menu.add_command(label=&quot;确定&quot;, command=create_top_level)\nmenu_menu.add_command(label=&quot;取消&quot;)\nmenu_btn[&quot;menu&quot;] = menu_menu\n\n########################################################\n\n\ndef ask_for_file():\n    path = filedialog.askopenfilename()\n    if path != &quot;&quot;:\n        lb.config(text=path)\n    else:\n        lb.config(text=&quot;未选择文件&quot;)\n\n\nbtn = Button(window, text=&quot;点击选择文件&quot;, command=ask_for_file)\nbtn.grid()\nlb = Label(window, text=&quot;选择文件&quot;)\nlb.grid()\n\n########################################################\n\ncvs = Canvas(window, bg=&quot;black&quot;)\ncvs.create_text(50, 75, text=&quot;Consolas&quot;, fill=&quot;white&quot;,\n                anchor=W, font=(&quot;Consolas&quot;, 40, &quot;bold&quot;))\ncvs.grid()\n\n\ndef show_event(e: Event):\n    def obj_info(obj):\n        s = &quot;&quot;\n        keys = filter(lambda attr: not attr.startswith(&quot;_&quot;), dir(obj))\n        for k in keys:\n            s += f&quot;{k}: {getattr(obj, k)}\\n&quot;\n        return s\n    messagebox.showinfo(repr(e), obj_info(e))\n\n\n# &lt;[{事件修饰符}-]{事件类型}[-{事件选项}]&gt;\ncvs.bind(&quot;&lt;ButtonPress-1&gt;&quot;, show_event)\n\n########################################################\n\nwindow.mainloop()\n</code></pre>\n<h1>pyinstaller 打包</h1>\n<p><code>venv</code> 的使用，<a href=\"/posts/python-module/#%E4%BD%BF%E7%94%A8-venv\">参见此处</a></p>\n<pre><code class=\"language-sh\">mkdir my_proj\ncd my_proj\npython -m venv ./\nvim test.py\npyinstaller test.py -w # -w 选项，在 Windows 下不打开命令行窗口\n\n# 指定图标\npyinstaller test.py -w -i favicon.ico\n# 打包成单文件（不建议，会使程序启动变慢）\npyinstaller test.py -w -F\n</code></pre>\n<p>pyinstaller 会使用 <a href=\"https://github.com/upx/upx/releases\">upx</a> 进行压缩</p>\n","tags":["python"]},{"id":"python-asyncio-task","url":"https://yieldray.fun/posts/python-asyncio-task","title":"python协程与任务","date_published":"2023-01-13T17:44:44.000Z","date_modified":"2023-01-13T17:44:44.000Z","content_text":"<p><a href=\"https://docs.python.org/zh-cn/3/library/asyncio-task.html\">https://docs.python.org/zh-cn/3/library/asyncio-task.html</a></p>\n<h1>事件循环</h1>\n<p><a href=\"https://docs.python.org/zh-cn/3/library/asyncio-eventloop.html\">https://docs.python.org/zh-cn/3/library/asyncio-eventloop.html</a></p>\n<p>要运行 <code>Coroutine</code>，需要获取事件循环</p>\n<pre><code class=\"language-py\">import asyncio\n\nloop = asyncio.new_event_loop()  # 创建一个事件循环\nasyncio.set_event_loop(loop)  # 将当前线程的事件循环设置为该事件循环\nprint(loop == asyncio.get_event_loop())  # 当前事件循环就是刚才设置的事件循环\n# True\n</code></pre>\n<h1>可等待对象</h1>\n<pre><code class=\"language-py\">import asyncio\nfrom asyncio import Future, Task\nfrom typing import Coroutine, Awaitable\n\nf&quot;{Coroutine, Future, Task} 都是 {Awaitable} 对象&quot;\n\nasync def async_func(x=0.1, /):\n    await asyncio.sleep(x)\n    return x\n\ncoroutine = async_func(1)   # async 函数返回 Coroutine 对象\nasyncio.run(coroutine)  # 运行 Coroutine 的最简单方法是使用 asyncio.run() 函数（无需事件循环）\n\nloop = asyncio.new_event_loop()\nasyncio.set_event_loop(loop)\n\nloop.run_until_complete(async_func())  # 将 Coroutine 注册到事件循环，并启动事件循环\n# 相当于:（asyncio.create_task() 方法接受一个 Coroutine 或 Generator）\n# 当一个 Coroutine 通过 asyncio.create_task() 等函数被封装为一个 Task 时，该 Coroutine 会被自动调度执行\ntask = loop.create_task(async_func())  # 创建 Task。此时的 Task 尚未加入事件循环，状态为 pending\nloop.run_until_complete(task)  # 将 Task 注册到事件循环，并启动事件循环。Task 执行完后，状态为 finished\n\n\n# Task 对象允许设置完成时的回调\ndef callback(future: Future):\n    # Task 是 Future 的子类\n    print(future.result())\n\n\ntask = loop.create_task(async_func(0.12))\ntask.add_done_callback(callback)\nloop.run_until_complete(task)\n\n# Future 是一种特殊的低层级 Awaitable 对象，表示一个异步操作的最终结果。\n# ensure_future 函数将 Coroutine 或 Awaitable 对象包装成 Future\nfuture = asyncio.ensure_future(async_func())\nloop.run_until_complete(future)\n\n\n# 并发运行任务\n\n# asyncio.wait 函数将 Future 迭代器组合成一个协程\nloop.run_until_complete(\n    asyncio.wait([\n        asyncio.ensure_future(async_func(0.1)),\n        asyncio.ensure_future(async_func(0.2)),\n        asyncio.ensure_future(async_func(0.3)),\n    ])\n)\n\n# asyncio.gather 函数将传入的所有 Future 参数组合成一个迭代器\nloop.run_until_complete(\n    asyncio.gather(\n        asyncio.ensure_future(async_func(0.1)),\n        asyncio.ensure_future(async_func(0.2)),\n        asyncio.ensure_future(async_func(0.3)),\n    )\n)\n</code></pre>\n<p>python 3.11 还提供了 TaskGroup</p>\n<pre><code class=\"language-py\">async def main():\n    async with asyncio.TaskGroup() as tg:\n        task1 = tg.create_task(some_coro(...))\n        task2 = tg.create_task(another_coro(...))\n    print(&quot;Both tasks have completed now.&quot;)\n</code></pre>\n","tags":["python"]},{"id":"python-thread-intro","url":"https://yieldray.fun/posts/python-thread-intro","title":"python多线程入门","date_published":"2023-01-12T14:00:00.000Z","date_modified":"2023-01-12T14:00:00.000Z","content_text":"<h1>进程</h1>\n<p>进程：<a href=\"https://docs.python.org/zh-cn/3/library/multiprocessing.html#process-and-exceptions\">https://docs.python.org/zh-cn/3/library/multiprocessing.html#process-and-exceptions</a></p>\n<p>os 模块提供了操作系统相关的系统调用，multiprocessing 模块提供了跨平台的多进程</p>\n<pre><code class=\"language-py\">from multiprocessing import Process\nimport os\n\ndef info(title):\n    print(title)\n    print(&#39;module name:&#39;, __name__)\n    print(&#39;parent process:&#39;, os.getppid())\n    print(&#39;process id:&#39;, os.getpid())\n\ndef f(name):\n    info(&#39;function f&#39;)\n    print(&#39;hello&#39;, name)\n\nif __name__ == &#39;__main__&#39;:\n    info(&#39;main line&#39;)\n    p = Process(target=f, args=(&#39;bob&#39;,))\n    p.start()\n    p.join()\n</code></pre>\n<p>进程间通信，参见：<a href=\"https://docs.python.org/zh-cn/3/library/multiprocessing.html#exchanging-objects-between-processes\">https://docs.python.org/zh-cn/3/library/multiprocessing.html#exchanging-objects-between-processes</a></p>\n<p>进程池：<a href=\"https://docs.python.org/zh-cn/3/library/multiprocessing.html#multiprocessing.pool.Pool\">https://docs.python.org/zh-cn/3/library/multiprocessing.html#multiprocessing.pool.Pool</a></p>\n<pre><code class=\"language-py\">from multiprocessing import Pool\nimport os\nimport time\nimport random\n\ndef long_time_task(name):\n    process_info = f&#39;({name} pid={os.getpid():5})&#39;\n    print(f&#39;[Start  Process] {process_info}&#39;)\n    start = time.time()\n    time.sleep(random.random() * 3)\n    end = time.time()\n    print(f&#39;[End    Process] {process_info} takes {(end - start):.2f} seconds.&#39;)\n\nif __name__ == &#39;__main__&#39;:\n    print(f&#39;[Parent Process] (pid={os.getpid()})&#39;)\n    p = Pool(4)\n    for i in range(5):\n        p.apply_async(long_time_task, args=(i,))\n    print(&#39;Waiting for all subprocesses done...&#39;)\n    p.close()\n    p.join()  # 必须先关闭进程池\n    print(&#39;All subprocesses done.&#39;)\n</code></pre>\n<p>子进程：<a href=\"https://docs.python.org/zh-cn/3/library/subprocess.html\">https://docs.python.org/zh-cn/3/library/subprocess.html</a></p>\n<pre><code class=\"language-py\">import subprocess\n\nsubprocess.run([&quot;ls&quot;, &quot;-l&quot;])\n\np = subprocess.run([&quot;ls&quot;, &quot;-l&quot;], capture_output=True)\nprint(p.stdout.decode(&quot;utf-8&quot;))\n\np = subprocess.run([&quot;ls&quot;, &quot;-l&quot;], capture_output=True, encoding=&quot;utf-8&quot;)\nprint(p.stdout)\n\n\np = subprocess.Popen([&quot;nslookup&quot;], stdin=subprocess.PIPE,\n                     stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n                     )\noutput, err = p.communicate(\n    br&quot;&quot;&quot;\nset q=mx\npython.org\nexit\n&quot;&quot;&quot;)\nprint(output)\nprint(&#39;Exit code:&#39;, p.returncode)\n</code></pre>\n<h1>线程</h1>\n<p>线程：<a href=\"https://docs.python.org/zh-cn/3/library/threading.html#threading.Thread\">https://docs.python.org/zh-cn/3/library/threading.html#threading.Thread</a></p>\n<pre><code class=\"language-py\">import threading\nfrom threading import Thread\n\nprint(threading.current_thread().name)  # MainThread\n\n\ndef runnable(*args):\n    print(threading.current_thread().name)\n    print(args)\n\n\nt1 = Thread(target=runnable, args=(&quot;hello&quot;, &quot;world&quot;))\nt1.start()\nt1.join()\n\n\n# 若继承Thread对象，则可以覆写run方法（与java相似）\nclass MyThread(Thread):\n    def run(self):\n        print(threading.current_thread().name)\n\n\nt2 = MyThread()\nt2.start()\nt2.join()\n</code></pre>\n<p>线程锁：<a href=\"https://docs.python.org/zh-cn/3/library/threading.html#lock-objects\">https://docs.python.org/zh-cn/3/library/threading.html#lock-objects</a></p>\n<pre><code class=\"language-py\">import threading\nfrom threading import Thread, Lock, RLock\n\nlock = Lock()\nrlock = RLock()  # 可重入锁\na = 0\n\n\ndef runnable1():\n    global a\n    lock.acquire()\n    a += 1\n    a -= 1\n    lock.release()\n    print(a)\n\n\ndef runnable2():\n    # 保证锁可释放\n    global a\n    lock.acquire()\n    try:\n        a += 1\n        a -= 1\n    finally:\n        lock.release()\n    print(a)\n\n\ndef runnable3():\n    # with 语法糖\n    global a\n    with lock:\n        a += 1\n        a -= 1\n    print(a)\n\n\n# 创建线程本地数据\nmydata = threading.local()\n\ndef use_local_1():\n    mydata.x = 1\n\n\ndef use_local_2():\n    mydata.x = 2\n</code></pre>\n<p>标准库还提供了信号量和事件等机制：<a href=\"https://docs.python.org/zh-cn/3/library/threading.html\">https://docs.python.org/zh-cn/3/library/threading.html</a></p>\n<h1>并行任务</h1>\n<p><a href=\"https://docs.python.org/zh-cn/3/library/concurrent.futures.html\">https://docs.python.org/zh-cn/3/library/concurrent.futures.html</a></p>\n<p>参见：<a href=\"https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/\">https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/</a></p>\n","tags":["python"]},{"id":"python-date-time","url":"https://yieldray.fun/posts/python-date-time","title":"python时间与日期","date_published":"2023-01-11T15:53:39.000Z","date_modified":"2023-01-11T15:53:39.000Z","content_text":"<p>参见：<a href=\"https://docs.python.org/zh-cn/3/library/datetime.html\">https://docs.python.org/zh-cn/3/library/datetime.html</a></p>\n<pre><code class=\"language-py\">from datetime import datetime, timedelta, timezone\n\nprint(datetime(2023, 1, 2, 3, 4, 5, 6))  # datetime对象实现了__str__方法\nprint(datetime.now())\nprint(datetime.fromtimestamp(1672502400.000))  # python时间戳单位为秒而不是毫秒\nprint(datetime.strptime(&#39;2023/12/12 12:12:12&#39;,\n      &#39;%Y/%m/%d %H:%M:%S&#39;))  # 从字符串中通过占位符读取\n\nprint(datetime(2022, 1, 1) + timedelta(days=22))  # 计算时间差\n\nprint(datetime.now().strftime(&#39;%a, %b %d %H:%M&#39;))  # datetime对象格式化为字符串\n\n\nutc_tz = timezone.utc\nutc_8_tz = timezone(timedelta(hours=8))  # UTC+8:00\n\nprint(datetime.utcnow())  # 当前UTC时间\nprint(datetime.now().astimezone(utc_tz))  # 转换为指定时区\nprint(datetime.utcnow().replace(tzinfo=utc_8_tz))  # 通过replace方法强制设置时区\n</code></pre>\n","tags":["python"]},{"id":"python-module","url":"https://yieldray.fun/posts/python-module","title":"python模块和包","date_published":"2023-01-10T15:42:52.000Z","date_modified":"2023-01-10T15:42:52.000Z","content_text":"<h1>Ref</h1>\n<p><a href=\"https://docs.python.org/zh-cn/3/tutorial/modules.html\">https://docs.python.org/zh-cn/3/tutorial/modules.html</a><br><a href=\"https://docs.python.org/zh-cn/3/tutorial/venv.html\">https://docs.python.org/zh-cn/3/tutorial/venv.html</a><br><a href=\"https://docs.python.org/zh-cn/3/installing/\">https://docs.python.org/zh-cn/3/installing/</a></p>\n<h1>模块</h1>\n<p>模块包含可执行语句及函数定义。这些语句用于初始化模块，且仅在 import 语句 <em>第一次</em> 遇到模块名时执行<br>(文件作为脚本运行时，也会执行这些语句)</p>\n<p>每个模块都有自己的独立命名空间，import 语句并不限制必须置于脚本开头</p>\n<p>模块内部的全局变量 <code>__name__ </code> 包含了模块名(字符串)<br>当通过 import 导入时，<code>__name__ </code> = 模块名<br>当直接运行脚本时，<code>__name__ </code> = <code>&#39;__main__&#39;</code></p>\n<pre><code class=\"language-py\">if __name__ == &quot;__main__&quot;:\n    from sys import argv, __name__\n    print(__name__) # sys\n    print(argv)     # [&#39;xxx.py&#39;]\n</code></pre>\n<p><strong><a href=\"https://docs.python.org/zh-cn/3/tutorial/modules.html#the-module-search-path\">模块搜索路径</a></strong></p>\n<p>当一个名为 <code>xxx</code> 的模块被导入时，解释器首先搜索具有该名称的内置模块（这些模块的名字被列在 <a href=\"https://docs.python.org/zh-cn/3/library/sys.html#sys.builtin_module_names\"><code>sys.builtin_module_names</code></a> 中）<br>如果没有找到，它就在变量 <a href=\"https://docs.python.org/zh-cn/3/library/sys.html#sys.path\"><code>sys.path</code></a> 给出的目录列表中搜索一个名为 <code>xxx.py</code> 的文件，<code>sys.path</code> 从这些位置初始化:</p>\n<ul>\n<li>输入脚本的目录（或未指定文件时的当前目录）</li>\n<li><a href=\"https://docs.python.org/zh-cn/3/using/cmdline.html#envvar-PYTHONPATH\"><code>PYTHONPATH</code></a> （目录列表，与 shell 变量 PATH 的语法一样）</li>\n<li>依赖于安装的默认值（按照惯例包括一个 <code>site-packages</code> 目录，由 <a href=\"https://docs.python.org/zh-cn/3/library/site.html#module-site\"><code>site</code></a> 模块处理。linux 下是 <code>dist-packages</code>）</li>\n</ul>\n<blockquote>\n<p>更多细节请参阅 <a href=\"https://docs.python.org/zh-cn/3/library/sys_path_init.html#sys-path-init\">sys.path 模块搜索路径的初始化</a>。</p>\n</blockquote>\n<p>例如，直接通过 pip 安装的模块位于 <code>Python\\Python312\\Lib\\site-packages\\</code> (windows) 目录<br>默认情况下 <code>PYTHONPATH</code> 是未配置的。</p>\n<p><code>dir()</code> 函数返回模块（暴露的）变量列表，不带参数时返回当前定义的变量列表。</p>\n<pre><code class=\"language-py\">def dir(模块对象 = 当前模块) -&gt; list[str]: ...\n</code></pre>\n<h1>包</h1>\n<p>包就是包含模块的目录，搜索方式和模块一致（包是特殊的模块，即包含模块的模块）<br>python 仅将目录下包含 <code>__init__.py</code> 文件的目录当成包\n该脚本在包初始化时执行，还可以包含一个 <code>__all__</code> 列表变量，指定包导出的模块/自包名</p>\n<p>仅子包中可以使用相对导入，导入父包中或相同层级包中的内容</p>\n<pre><code class=\"language-py\">from . import echo\nfrom .. import formats\nfrom ..filters import equalizer\n</code></pre>\n<h1>使用 pip</h1>\n<pre><code class=\"language-sh\"># 安装最新版本\npip install requests\n\n# 安装指定版本\npip install requests==2.28.1\npip install &quot;SomeProject&gt;=1,&lt;2&quot;\npip install &quot;SomeProject~=1.4.2&quot;\n\n# 从git仓库安装 (-e: editable mode)\npip install -e git+https://git.repo/some_pkg.git#egg=SomeProject\npip install -e git+https://git.repo/some_pkg.git@BranchName#egg=SomeProject\n\n# 安装可选(额外)依赖\npip install SomePackage[PDF]\n\n# 升级到最新版本\npip install --upgrade requests\n\n#  显示安装的模块\npip list\n\n# 显示模块信息\npip show requests\n\n# 锁定版本\npip freeze &gt; requirements.txt\npip install -r requirements.txt\n</code></pre>\n<h1>使用 venv</h1>\n<p>venv 是 python 的内置模块（&gt;=3.3）</p>\n<pre><code class=\"language-sh\">$ python3 -m venv &lt;dirname&gt;\n$ cd &lt;dirname&gt;\n</code></pre>\n<p>首先需要执行脚本以进入虚拟环境</p>\n<pre><code class=\"language-sh\"># Windows\n.\\Scripts\\activate.bat\n\n# Windows MinGW/msys\nsource ./Scripts/activate\n\n# Unix\nsource ./bin/activate\n</code></pre>\n<p>这样通过 pip 安装的包就会安装在当前项目路径下<br>执行对应的 <em>deactivate 脚本</em> 退出虚拟环境</p>\n<hr>\n<p>pip 作为内置模块，与当前解释器版本是绑定的。</p>\n<p><a href=\"https://github.com/pypa/virtualenv\">virtualenv</a> 是 venv 的高级版本，提供更多功能。例如可使用不同解释器版本。</p>\n<h1>第三方包管理相关工具</h1>\n<blockquote>\n<p>另见：<a href=\"https://packaging.python.org/en/latest/key_projects/\">https://packaging.python.org/en/latest/key_projects/</a></p>\n</blockquote>\n<p>pip 会将包安装在当前解释器的 <a href=\"https://docs.python.org/zh-cn/3/library/site.html#site.getsitepackages\">site-packages 目录</a>下</p>\n<pre><code class=\"language-sh\">python -m site --user-site\n</code></pre>\n<p>对于包含可执行脚本的包，例如：</p>\n<pre><code class=\"language-py\"># setup.py\nfrom setuptools import setup\n\nsetup(\n    # ... 其他参数\n    entry_points={\n        &#39;console_scripts&#39;: [\n            &#39;my_script = my_package.my_module:main&#39;,\n            # 命令行中使用的脚本名 = 包名.模块名.可执行函数名\n        ],\n    }\n)\n# 对于 pyproject.toml，此处从略\n</code></pre>\n<p>pip 会创建包装脚本并将其放在当前解释器环境的 bin 目录下。</p>\n<p>总之，pip 安装的包的路径与当前解释器路径相关。</p>\n<h2>pipx</h2>\n<p><a href=\"https://github.com/pypa/pipx\">pipx</a> 用于安装和运行 Python 打包的应用程序, 而不是作为库使用（区别于 pip）。</p>\n<p>pipx 将每个应用程序安装到独立的虚拟环境中，并将其可执行文件链接到当前 shell 环境中。</p>\n<h2>pipenv</h2>\n<p><a href=\"https://github.com/pypa/pipenv\">pipenv</a> 可以看作是 virtualenv 和 pip 的结合体，能够自动管理虚拟环境和依赖关系。</p>\n<h2>hatch</h2>\n<p><a href=\"https://github.com/pypa/hatch\">hatch</a> 是一款现代的、可扩展的 Python 项目管理器。</p>\n<h2>poetry</h2>\n<p><a href=\"https://python-poetry.org/\">poetry</a> 是一个项目（包）管理工具，每个项目都是一个包，并为当前项目创建虚拟环境。</p>\n<h2>pdm</h2>\n<p><a href=\"https://github.com/pdm-project/pdm\">pdm</a> 是支持最新 PEP 标准的现代 Python 软件包和依赖关系管理器。</p>\n<h2>Rye</h2>\n<p><a href=\"https://github.com/astral-sh/rye\">Rye</a> 是适用于 Python 的全面项目和包管理解决方案。</p>\n<blockquote>\n<p>新项目建议使用 <a href=\"https://github.com/astral-sh/uv\">uv</a></p>\n</blockquote>\n<h2>uv</h2>\n<p><a href=\"https://github.com/astral-sh/uv\">uv</a> 是一个 Rust 写的极速包管理和项目管理工具。</p>\n","tags":["python"]},{"id":"python-functional","url":"https://yieldray.fun/posts/python-functional","title":"python函数式编程","date_published":"2023-01-09T22:19:59.000Z","date_modified":"2023-01-09T22:19:59.000Z","content_text":"<p><em>函数式编程指引</em><br><a href=\"https://docs.python.org/zh-cn/3/howto/functional.html\">https://docs.python.org/zh-cn/3/howto/functional.html</a></p>\n<h1>生成器</h1>\n<pre><code class=\"language-py\">def gen():\n    s = &quot;&quot;\n    while True:\n        val = (yield s)\n        if type(val) is str:\n            s += val\n\n\ny = gen()\nprint(next(y))  # 相当于 print(y.send(None))\n# send 方法向下一个 yield 语句发送值\n# yield 若未接收值，则 yield 语句返回 None\n# 不过 send 方法无法向刚开始的生成器发送非 None 值\nprint(y.send(&quot;hello&quot;))  # hello\nprint(y.__next__())  # hello\nprint(y.send(&quot;world&quot;))  # helloworld\ny.close()  # 调用 close 方法关闭生成器\n</code></pre>\n<h1>map</h1>\n<p>注意！map 是一个<strong>类</strong>而不只是函数（因为要实现迭代器）</p>\n<pre><code class=\"language-py\"># 部分定义\nclass map(Iterator[_S], Generic[_S]):\n    @overload\n    def __init__(self, __func: Callable[[_T1], _S], __iter1: Iterable[_T1]) -&gt; None: ...\n    @overload\n    def __init__(self, __func: Callable[[_T1, _T2], _S], __iter1: Iterable[_T1], __iter2: Iterable[_T2]) -&gt; None: ...\n\n\nprint(list(\n    map(lambda x: x**2, [1, 2, 3])\n    # [1, 4, 9]\n))\n\nprint(list(\n    map(lambda x, y: (x**2, y**2), [1, 2, 3], [2, 3, 4])\n    # [(1, 4), (4, 9), (9, 16)]\n))\n</code></pre>\n<h1>reduce</h1>\n<pre><code class=\"language-py\"># 定义大概可以写成这样\nfrom typing import TypeVar, Iterable, Callable\nT = TypeVar(&#39;T&#39;)\ndef reduce(function: Callable[[T, T], T], sequence: Iterable[T], initial: T): ...\n\n\nfrom functools import reduce\nprint(\n    reduce(lambda x, y: x*10+y, [3, 4, 5], 0)\n    # 345\n)\n\nprint(\n    reduce(lambda x, y: x+y, [3, 3, 3], 1)\n    # 10\n    # 相当于\n    # reduce(lambda x, y: x+y, [1, 3, 3, 3])\n)\n</code></pre>\n<h1>filter</h1>\n<p>filter 也是一个类，同样是因为要实现迭代器</p>\n<pre><code class=\"language-py\"># 部分定义\nclass filter(Iterator[_T], Generic[_T]):\n    @overload\n    def __init__(self, __function: Callable[[_S], TypeGuard[_T]], __iterable: Iterable[_S]) -&gt; None: ...\n    @overload\n    def __init__(self, __function: Callable[[_T], Any], __iterable: Iterable[_T]) -&gt; None: ...\n\n\nprint(list(\n    filter(lambda x: x % 2 == 0, [-2, -1, 0, 1, 2, 3])\n    # [-2, 0, 2]\n))\n</code></pre>\n<h1>装饰器</h1>\n<p>装饰器是高阶函数</p>\n<pre><code class=\"language-py\">import functools\n\n\ndef decorator(fn):  # fn 是装饰器修饰的函数对象\n    # @functools.wraps(fn)\n    # functools.wraps 这个装饰器保证被装饰器装饰后的函数还拥有原来的属性\n    def wrapper(*args, **kw):  # 装饰器返回的 wrapper 函数是实际调用的函数对象。换言之，装饰器修饰的函数被 wrapper 替代\n        # wrapper.__name__ = fn.__name__\n        print(&quot;{}(args={}, kw={})&quot;.format(fn.__name__, args, kw))\n        return fn(*args, **kw)\n    return wrapper\n\n\ndef decorator_lambda(fn):  # 装饰器就是返回函数的函数。使用匿名函数替代如下\n    return lambda *args, **kw: print(&quot;{}(args={}, kw={})&quot;.format(fn.__name__, args, kw))\n\n\ndef decorator_with_param(*params):  # 带参数的装饰器，是一个返回装饰器的函数\n    print(&quot;@decorator_with_param{}&quot;.format(params))\n\n    def decorator(fn):\n        def wrapper(*args, **kw):\n            print(&quot;{}(args={}, kw={})&quot;.format(fn.__name__, args, kw))\n            return fn(*args, **kw)\n        return wrapper\n    return decorator\n\n\ndef decorator_with_param_lambda(*params):  # 使用匿名函数替代如下\n    print(&quot;@decorator_with_param{}&quot;.format(params))\n    return lambda fn: lambda *args, **kw: print(&quot;{}(args={}, kw={})&quot;.format(fn.__name__, args, kw))\n\n\n# TEST\n\n@decorator\ndef test1(a, b, c):\n    pass\n\n\n@decorator_with_param(&quot;param1&quot;, &quot;param2&quot;)\ndef test2(a, b, c):\n    pass\n\n\ntest1(1, 2, c=3)\ntest2(4, 5, c=6)\n</code></pre>\n","tags":["python"]},{"id":"github-actions","url":"https://yieldray.fun/posts/github-actions","title":"Github Actions","date_published":"2022-12-14T14:00:00.000Z","date_modified":"2022-12-14T14:00:00.000Z","content_text":"<p><a href=\"https://github.com/features/actions\">https://github.com/features/actions</a><br><a href=\"https://docs.github.com/actions\">https://docs.github.com/actions</a><br><a href=\"https://www.actionsbyexample.com/\">https://www.actionsbyexample.com/</a></p>\n<p>Github Actions 是一个 CI/CD 平台</p>\n<h1>Quickstart</h1>\n<p>配置文件置于<code>.github/workflows</code>目录，文件名任意，后缀需为<code>.yml</code> <a href=\"https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions\">语法文档</a></p>\n<pre><code class=\"language-yaml\"># https://docs.github.com/actions/learn-github-actions/understanding-github-actions\n\nname: GitHub Actions Demo\nrun-name: ${{ github.actor }} is testing out GitHub Actions 🚀\non: [push, workflow_dispatch]\n\njobs:\n    Explore-GitHub-Actions:\n        runs-on: ubuntu-latest\n        steps:\n            - run: echo &quot;🎉 The job was automatically triggered by a ${{ github.event_name }} event.&quot;\n            - run: echo &quot;🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!&quot;\n            - run: echo &quot;🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}.&quot;\n            - name: Check out repository code\n              uses: actions/checkout@v3\n            - run: echo &quot;💡 The ${{ github.repository }} repository has been cloned to the runner.&quot;\n            - run: echo &quot;🖥️ The workflow is now ready to test your code on the runner.&quot;\n            - name: List files in the repository\n              run: |\n                  ls ${{ github.workspace }}\n            - run: echo &quot;🍏 This job&#39;s status is ${{ job.status }}.&quot;\n</code></pre>\n<h1>流程</h1>\n<p><a href=\"https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions\">https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions</a></p>\n<p><img src=\"https://s2.loli.net/2023/06/25/b8dXnfajB6mKJsu.webp\" alt=\"\"></p>\n<p>每一个 yaml 文件配置了一个 <strong>workflow</strong></p>\n<p>指定的 <strong>events</strong> 触发 <strong>workflow</strong> 的运行 （<a href=\"https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch\">所有 events</a>）</p>\n<p><strong>jobs</strong> 是一组 <strong>steps</strong> 的集合，这组 <strong>steps</strong> 运行于同一个 <strong>runner</strong> 环境下（<a href=\"https://docs.github.com/en/actions/using-jobs\">参见</a>）</p>\n<p><strong>step</strong> 可以是 shell 命令，亦或是 <strong>actions</strong></p>\n<p><strong>actions</strong> 是特殊的 <strong>step</strong>，类似于函数，允许提供参数来调用（<a href=\"https://docs.github.com/en/actions/creating-actions\">参见</a>）</p>\n<p><strong>runner</strong> 实际上就是运行的操作系统类型（<a href=\"https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources\">例如</a>： <code>windows-latest</code> <code>ubuntu-latest</code> <code>macos-latest</code>）</p>\n<h1>示例</h1>\n<p>参考：<a href=\"https://github.com/actions/starter-workflows/blob/main/pages/static.yml\">https://github.com/actions/starter-workflows/blob/main/pages/static.yml</a></p>\n<p>以下是一个部署 hexo 的示例，首先需要在 repo 中设置好 pages 从 actions 部署<br><img src=\"https://s2.loli.net/2023/06/25/2Zs5zGveXapCtYW.png\" alt=\"\"></p>\n<p>然后，假设目录名为 <code>repo</code>，创建 <code>repo/.github/workflows/pages.yml</code> 文件，内容如下</p>\n<pre><code class=\"language-yaml\"># Simple workflow for deploying static content to GitHub Pages\nname: Deploy static content to Pages\n\non:\n    # Runs on pushes targeting the default branch\n    push:\n        branches: [&quot;main&quot;]\n\n    # Allows you to run this workflow manually from the Actions tab\n    workflow_dispatch:\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n    contents: read\n    pages: write\n    id-token: write\n\n# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n    group: &quot;pages&quot;\n    cancel-in-progress: false\n\njobs:\n    # Single deploy job since we&#39;re just deploying\n    deploy:\n        environment:\n            name: github-pages\n            url: ${{ steps.deployment.outputs.page_url }}\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout\n              uses: actions/checkout@v4\n\n            - name: Setup Node.js\n              uses: actions/setup-node@v4\n\n            - name: Node.js\n              run: node -v\n\n            - name: Npm install\n              run: npm install\n\n            - name: Npm build\n              run: npm run build\n\n            - name: Setup Pages\n              uses: actions/configure-pages@v3\n\n            - name: Upload artifact\n              uses: actions/upload-pages-artifact@v1\n              with:\n                  # Upload ./public\n                  path: &quot;public&quot;\n\n            - name: Deploy to GitHub Pages\n              id: deployment\n              uses: actions/deploy-pages@v2\n</code></pre>\n<p><a href=\"https://docs.github.com/actions/examples\">更多示例</a></p>\n<h1><a href=\"https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows\">Workflow</a></h1>\n<h2><a href=\"https://docs.github.com/actions/using-workflows/events-that-trigger-workflows\">Events</a></h2>\n<h2><a href=\"https://docs.github.com/actions/using-jobs/using-jobs-in-a-workflow\">Jobs</a></h2>\n<h2><a href=\"https://docs.github.com/actions/learn-github-actions/finding-and-customizing-actions\">Actions</a></h2>\n<h1><a href=\"https://docs.github.com/actions/learn-github-actions/essential-features-of-github-actions\">Essential Features</a></h1>\n<h1><a href=\"https://docs.github.com/actions/learn-github-actions/expressions\">Expressions</a></h1>\n<h1><a href=\"https://docs.github.com/actions/learn-github-actions/contexts\">Contexts</a></h1>\n<h1><a href=\"https://docs.github.com/actions/learn-github-actions/variables\">Variables</a></h1>\n","tags":["github"]},{"id":"web-coordinate","url":"https://yieldray.fun/posts/web-coordinate","title":"DOM坐标","date_published":"2022-11-30T21:30:00.000Z","date_modified":"2022-11-30T21:30:00.000Z","content_text":"<p>以下 Typescript 定义为节选</p>\n<h1>鼠标事件 MouseEvent</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/MouseEvent\">https://developer.mozilla.org/docs/Web/API/MouseEvent</a></p>\n<p>mousedown/mouseup<br>mouseover/mouseout<br>mousemove<br>click/dblclick/contextmenu</p>\n<p>坐标系原点为左上角</p>\n<pre><code class=\"language-ts\">// prettier-ignore\ninterface MouseEvent extends UIEvent {\n    readonly altKey: boolean;\n    readonly shiftKey: boolean;\n    readonly ctrlKey: boolean;\n    readonly metaKey: boolean;\n    readonly button: number; // 左键0，中键1，右键2\n    readonly buttons: number; // 所有按键的数值做或运算\n\n    readonly clientX: number; // 相对于窗口的横坐标\n    readonly clientY: number; // 相对于窗口的纵坐标\n    readonly x: number; // 同 clientX\n    readonly y: number; // 同 clientY\n\n    readonly pageX: number; // 相对于文档的横坐标\n    readonly pageY: number; // 相对于文档的纵坐标\n\n    readonly screenX: number; // 相对于显示器的横坐标\n    readonly screenY: number; // 相对于显示器的纵坐标\n\n    readonly movementX: number; // 相对于最后mousemove事件位置的横坐标\n    readonly movementY: number; // 相对于最后mousemove事件位置的纵坐标\n\n    readonly relatedTarget: EventTarget | null;\n    /* 继承的 readonly target: EventTarget */\n    readonly offsetX: number; // 相对于target的padding-box的横坐标（若在边框上，则为负数）\n    readonly offsetY: number; // 相对于target的padding-box的纵坐标（若在边框上，则为负数）\n    /* ... */\n}\n</code></pre>\n<p>往往需要阻止默认事件</p>\n<pre><code class=\"language-js\">element.ondragstart = () =&gt; false;\n</code></pre>\n<h1>元素坐标 <code>Element.prototype.getBoundingClientRect(): DOMRect</code></h1>\n<p>返回元素 border-box 相当于窗口左上角的距离（精确到小数位）。</p>\n<p><img src=\"https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect/element-box-diagram.png\" alt=\"element-box-diagram\"></p>\n<pre><code class=\"language-ts\">interface DOMRect extends /* ... */ {\n    readonly left: number; // 元素border-box左边相对窗口的距离\n    readonly top: number;\n    readonly bottom: number;\n    readonly right: number;\n\n    x: number; // 元素起始点横坐标\n    y: number; // 元素起始点纵坐标\n    width: number;  // 可为负数\n    height: number; // 可为负数\n    /* ... */\n}\n</code></pre>\n<h1>Element 及 HTMLElement 接口</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Element\">https://developer.mozilla.org/docs/Web/API/Element</a><br><a href=\"https://developer.mozilla.org/docs/Web/API/HTMLElement\">https://developer.mozilla.org/docs/Web/API/HTMLElement</a></p>\n<p>这些属性的值会四舍五入</p>\n<pre><code class=\"language-ts\">interface Element extends /* ... */ {\n    // border + scrollBar\n    readonly clientLeft: number; // 元素padding外部与border外部的水平距离（若无滚动条，即为边框宽度）\n    readonly clientTop: number;  // 元素padding外部与border外部的垂直距离（若无滚动条，即为边框高度）\n\n    // content + padding\n    readonly clientWidth: number;  // 元素padding-box宽度（不包括滚动条宽度）\n    readonly clientHeight: number; // 元素padding-box高度（不包括滚动条宽度）\n\n    // scroll + content + padding\n    readonly scrollWidth: number;  // 元素border-box宽度，包括滚动出的部分（不包括滚动条宽度）\n    readonly scrollHeight: number; // 元素border-box高度，包括滚动出的部分（不包括滚动条宽度）\n\n    // scroll\n    scrollTop: number;  // 元素滚动出的垂直距离\n    scrollLeft: number; // 元素滚动出的水平距离\n    /* ... */\n}\n\ninterface HTMLElement extends Element /* ... */ {\n    // border + padding + content\n    readonly offsetWidth: number;  // 元素border-box宽度\n    readonly offsetHeight: number; // 元素border-box高度\n\n    readonly offsetParent: Element | null; // 最接近的祖先元素（定位）\n    readonly offsetLeft: number; // 元素相对offsetParent的border-box左上角的水平距离\n    readonly offsetTop: number;  // 元素相对offsetParent的border-box左上角的垂直距离\n    /* ... */\n}\n</code></pre>\n<h1>Window 接口</h1>\n<p><img src=\"https://developer.mozilla.org/docs/Web/API/Window/outerHeight/firefoxinnervsouterheight2.png\" alt=\"\"></p>\n<pre><code class=\"language-ts\">interface Window extends EventTarget /* ... */ {\n    // 包括滚动条的宽高（不包括滚动部分）\n    readonly innerHeight: number;\n    readonly innerWidth: number;\n\n    // 整个浏览器窗口的宽高\n    readonly outerHeight: number;\n    readonly outerWidth: number;\n\n    // 文档当前滚动距离，相当于document.documentElement.scrollTop/scrollLeft\n    readonly scrollX: number;\n    readonly scrollY: number;\n    // 下面的两个属性只是上面的别名\n    readonly pageXOffset: number;\n    readonly pageYOffset: number;\n\n    // 浏览器与显示器边界的距离\n    readonly screenX: number;\n    readonly screenY: number;\n    // 下面的两个属性只是上面的别名\n    readonly screenLeft: number;\n    readonly screenTop: number;\n\n    readonly screen: {\n        readonly availHeight: number;\n        readonly availWidth: number;\n        readonly width: number;\n        readonly height: number;\n        readonly colorDepth: number;\n        readonly pixelDepth: number;\n        readonly orientation: ScreenOrientation;\n    };\n    /* ... */\n}\n</code></pre>\n<h1>滚动方法</h1>\n<pre><code class=\"language-ts\">interface ScrollOptions {\n    behavior?: &quot;auto&quot; | &quot;smooth&quot;;\n}\n\ninterface ScrollToOptions extends ScrollOptions {\n    left?: number;\n    top?: number;\n}\n\ninterface ScrollIntoViewOptions extends ScrollOptions {\n    block?: &quot;center&quot; | &quot;end&quot; | &quot;nearest&quot; | &quot;start&quot;;\n    inline?: &quot;center&quot; | &quot;end&quot; | &quot;nearest&quot; | &quot;start&quot;;\n}\n</code></pre>\n<pre><code class=\"language-ts\">interface Element /* ... */ {\n    /* ... */\n    scroll(options?: ScrollToOptions): void;\n    scroll(x: number, y: number): void;\n    scrollBy(options?: ScrollToOptions): void;\n    scrollBy(x: number, y: number): void;\n    scrollIntoView(arg?: boolean | ScrollIntoViewOptions): void;\n    scrollTo(options?: ScrollToOptions): void;\n    scrollTo(x: number, y: number): void;\n}\n\ninterface Window /* ... */ {\n    /* ... */\n    scroll(options?: ScrollToOptions): void;\n    scroll(x: number, y: number): void;\n    scrollBy(options?: ScrollToOptions): void;\n    scrollBy(x: number, y: number): void;\n    scrollTo(options?: ScrollToOptions): void;\n    scrollTo(x: number, y: number): void;\n}\n</code></pre>\n","tags":["web-api","dom"]},{"id":"web-dom","url":"https://yieldray.fun/posts/web-dom","title":"DOM","date_published":"2022-11-27T09:00:00.000Z","date_modified":"2022-11-27T09:00:00.000Z","content_text":"<p><code>lib.dom.d.ts</code><br><img src=\"https://s2.loli.net/2022/11/30/BYK8bWmaS5ARkZO.png\" alt=\"httpszh.javascript.infobasic-dom-node-properties.png\"></p>\n<h1>Element</h1>\n<pre><code class=\"language-js\">/** Element is the most general base class from which all objects in a Document inherit. It only has methods and properties common to all kinds of elements. More specific classes inherit from Element. */\ninterface Element extends Node, ARIAMixin, Animatable, ChildNode, InnerHTML, NonDocumentTypeChildNode, ParentNode, Slottable {\n    readonly attributes: NamedNodeMap;\n    /** Allows for manipulation of element&#39;s class content attribute as a set of whitespace-separated tokens through a DOMTokenList object. */\n    readonly classList: DOMTokenList;\n    /** Returns the value of element&#39;s class content attribute. Can be set to change it. */\n    className: string;\n    readonly clientHeight: number;\n    readonly clientLeft: number;\n    readonly clientTop: number;\n    readonly clientWidth: number;\n    /** Returns the value of element&#39;s id content attribute. Can be set to change it. */\n    id: string;\n    /** Returns the local name. */\n    readonly localName: string;\n    /** Returns the namespace. */\n    readonly namespaceURI: string | null;\n    onfullscreenchange: ((this: Element, ev: Event) =&gt; any) | null;\n    onfullscreenerror: ((this: Element, ev: Event) =&gt; any) | null;\n    outerHTML: string;\n    readonly ownerDocument: Document;\n    readonly part: DOMTokenList;\n    /** Returns the namespace prefix. */\n    readonly prefix: string | null;\n    readonly scrollHeight: number;\n    scrollLeft: number;\n    scrollTop: number;\n    readonly scrollWidth: number;\n    /** Returns element&#39;s shadow root, if any, and if shadow root&#39;s mode is &quot;open&quot;, and null otherwise. */\n    readonly shadowRoot: ShadowRoot | null;\n    /** Returns the value of element&#39;s slot content attribute. Can be set to change it. */\n    slot: string;\n    /** Returns the HTML-uppercased qualified name. */\n    readonly tagName: string;\n    /** Creates a shadow root for element and returns it. */\n    attachShadow(init: ShadowRootInit): ShadowRoot;\n    /** Returns the first (starting at element) inclusive ancestor that matches selectors, and null otherwise. */\n    closest&lt;K extends keyof HTMLElementTagNameMap&gt;(selector: K): HTMLElementTagNameMap[K] | null;\n    closest&lt;K extends keyof SVGElementTagNameMap&gt;(selector: K): SVGElementTagNameMap[K] | null;\n    closest&lt;E extends Element = Element&gt;(selectors: string): E | null;\n    /** Returns element&#39;s first attribute whose qualified name is qualifiedName, and null if there is no such attribute otherwise. */\n    getAttribute(qualifiedName: string): string | null;\n    /** Returns element&#39;s attribute whose namespace is namespace and local name is localName, and null if there is no such attribute otherwise. */\n    getAttributeNS(namespace: string | null, localName: string): string | null;\n    /** Returns the qualified names of all element&#39;s attributes. Can contain duplicates. */\n    getAttributeNames(): string[];\n    getAttributeNode(qualifiedName: string): Attr | null;\n    getAttributeNodeNS(namespace: string | null, localName: string): Attr | null;\n    getBoundingClientRect(): DOMRect;\n    getClientRects(): DOMRectList;\n    /** Returns a HTMLCollection of the elements in the object on which the method was invoked (a document or an element) that have all the classes given by classNames. The classNames argument is interpreted as a space-separated list of classes. */\n    getElementsByClassName(classNames: string): HTMLCollectionOf&lt;Element&gt;;\n    getElementsByTagName&lt;K extends keyof HTMLElementTagNameMap&gt;(qualifiedName: K): HTMLCollectionOf&lt;HTMLElementTagNameMap[K]&gt;;\n    getElementsByTagName&lt;K extends keyof SVGElementTagNameMap&gt;(qualifiedName: K): HTMLCollectionOf&lt;SVGElementTagNameMap[K]&gt;;\n    getElementsByTagName(qualifiedName: string): HTMLCollectionOf&lt;Element&gt;;\n    getElementsByTagNameNS(namespaceURI: &quot;http://www.w3.org/1999/xhtml&quot;, localName: string): HTMLCollectionOf&lt;HTMLElement&gt;;\n    getElementsByTagNameNS(namespaceURI: &quot;http://www.w3.org/2000/svg&quot;, localName: string): HTMLCollectionOf&lt;SVGElement&gt;;\n    getElementsByTagNameNS(namespace: string | null, localName: string): HTMLCollectionOf&lt;Element&gt;;\n    /** Returns true if element has an attribute whose qualified name is qualifiedName, and false otherwise. */\n    hasAttribute(qualifiedName: string): boolean;\n    /** Returns true if element has an attribute whose namespace is namespace and local name is localName. */\n    hasAttributeNS(namespace: string | null, localName: string): boolean;\n    /** Returns true if element has attributes, and false otherwise. */\n    hasAttributes(): boolean;\n    hasPointerCapture(pointerId: number): boolean;\n    insertAdjacentElement(where: InsertPosition, element: Element): Element | null;\n    insertAdjacentHTML(position: InsertPosition, text: string): void;\n    insertAdjacentText(where: InsertPosition, data: string): void;\n    /** Returns true if matching selectors against element&#39;s root yields element, and false otherwise. */\n    matches(selectors: string): boolean;\n    releasePointerCapture(pointerId: number): void;\n    /** Removes element&#39;s first attribute whose qualified name is qualifiedName. */\n    removeAttribute(qualifiedName: string): void;\n    /** Removes element&#39;s attribute whose namespace is namespace and local name is localName. */\n    removeAttributeNS(namespace: string | null, localName: string): void;\n    removeAttributeNode(attr: Attr): Attr;\n    /**\n     * Displays element fullscreen and resolves promise when done.\n     *\n     * When supplied, options&#39;s navigationUI member indicates whether showing navigation UI while in fullscreen is preferred or not. If set to &quot;show&quot;, navigation simplicity is preferred over screen space, and if set to &quot;hide&quot;, more screen space is preferred. User agents are always free to honor user preference over the application&#39;s. The default value &quot;auto&quot; indicates no application preference.\n     */\n    requestFullscreen(options?: FullscreenOptions): Promise&lt;void&gt;;\n    requestPointerLock(): void;\n    scroll(options?: ScrollToOptions): void;\n    scroll(x: number, y: number): void;\n    scrollBy(options?: ScrollToOptions): void;\n    scrollBy(x: number, y: number): void;\n    scrollIntoView(arg?: boolean | ScrollIntoViewOptions): void;\n    scrollTo(options?: ScrollToOptions): void;\n    scrollTo(x: number, y: number): void;\n    /** Sets the value of element&#39;s first attribute whose qualified name is qualifiedName to value. */\n    setAttribute(qualifiedName: string, value: string): void;\n    /** Sets the value of element&#39;s attribute whose namespace is namespace and local name is localName to value. */\n    setAttributeNS(namespace: string | null, qualifiedName: string, value: string): void;\n    setAttributeNode(attr: Attr): Attr | null;\n    setAttributeNodeNS(attr: Attr): Attr | null;\n    setPointerCapture(pointerId: number): void;\n    /**\n     * If force is not given, &quot;toggles&quot; qualifiedName, removing it if it is present and adding it if it is not present. If force is true, adds qualifiedName. If force is false, removes qualifiedName.\n     *\n     * Returns true if qualifiedName is now present, and false otherwise.\n     */\n    toggleAttribute(qualifiedName: string, force?: boolean): boolean;\n    /** @deprecated This is a legacy alias of `matches`. */\n    webkitMatchesSelector(selectors: string): boolean;\n    addEventListener&lt;K extends keyof ElementEventMap&gt;(\n        type: K,\n        listener: (this: Element, ev: ElementEventMap[K]) =&gt; any,\n        options?: boolean | AddEventListenerOptions\n    ): void;\n    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n    removeEventListener&lt;K extends keyof ElementEventMap&gt;(\n        type: K,\n        listener: (this: Element, ev: ElementEventMap[K]) =&gt; any,\n        options?: boolean | EventListenerOptions\n    ): void;\n    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n\ndeclare var Element: {\n    prototype: Element;\n    new (): Element;\n};\n\ninterface ElementCSSInlineStyle {\n    readonly style: CSSStyleDeclaration;\n}\n\ninterface ElementContentEditable {\n    contentEditable: string;\n    enterKeyHint: string;\n    inputMode: string;\n    readonly isContentEditable: boolean;\n}\n\ninterface ElementInternals extends ARIAMixin {\n    /** Returns the form owner of internals&#39;s target element. */\n    readonly form: HTMLFormElement | null;\n    /** Returns a NodeList of all the label elements that internals&#39;s target element is associated with. */\n    readonly labels: NodeList;\n    /** Returns the ShadowRoot for internals&#39;s target element, if the target element is a shadow host, or null otherwise. */\n    readonly shadowRoot: ShadowRoot | null;\n    /** Returns the error message that would be shown to the user if internals&#39;s target element was to be checked for validity. */\n    readonly validationMessage: string;\n    /** Returns the ValidityState object for internals&#39;s target element. */\n    readonly validity: ValidityState;\n    /** Returns true if internals&#39;s target element will be validated when the form is submitted; false otherwise. */\n    readonly willValidate: boolean;\n    /** Returns true if internals&#39;s target element has no validity problems; false otherwise. Fires an invalid event at the element in the latter case. */\n    checkValidity(): boolean;\n    /** Returns true if internals&#39;s target element has no validity problems; otherwise, returns false, fires an invalid event at the element, and (if the event isn&#39;t canceled) reports the problem to the user. */\n    reportValidity(): boolean;\n    /**\n     * Sets both the state and submission value of internals&#39;s target element to value.\n     *\n     * If value is null, the element won&#39;t participate in form submission.\n     */\n    setFormValue(value: File | string | FormData | null, state?: File | string | FormData | null): void;\n    /** Marks internals&#39;s target element as suffering from the constraints indicated by the flags argument, and sets the element&#39;s validation message to message. If anchor is specified, the user agent might use it to indicate problems with the constraints of internals&#39;s target element when the form owner is validated interactively or reportValidity() is called. */\n    setValidity(flags?: ValidityStateFlags, message?: string, anchor?: HTMLElement): void;\n}\n\ndeclare var ElementInternals: {\n    prototype: ElementInternals;\n    new (): ElementInternals;\n};\n</code></pre>\n<h1>HTMLElement</h1>\n<pre><code class=\"language-js\">/** Any HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it. */\ninterface HTMLElement extends Element, DocumentAndElementEventHandlers, ElementCSSInlineStyle, ElementContentEditable, GlobalEventHandlers, HTMLOrSVGElement {\n    accessKey: string;\n    readonly accessKeyLabel: string;\n    autocapitalize: string;\n    dir: string;\n    draggable: boolean;\n    hidden: boolean;\n    inert: boolean;\n    innerText: string;\n    lang: string;\n    readonly offsetHeight: number;\n    readonly offsetLeft: number;\n    readonly offsetParent: Element | null;\n    readonly offsetTop: number;\n    readonly offsetWidth: number;\n    outerText: string;\n    spellcheck: boolean;\n    title: string;\n    translate: boolean;\n    attachInternals(): ElementInternals;\n    click(): void;\n    addEventListener&lt;K extends keyof HTMLElementEventMap&gt;(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) =&gt; any, options?: boolean | AddEventListenerOptions): void;\n    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n    removeEventListener&lt;K extends keyof HTMLElementEventMap&gt;(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) =&gt; any, options?: boolean | EventListenerOptions): void;\n    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n\ndeclare var HTMLElement: {\n    prototype: HTMLElement;\n    new(): HTMLElement;\n};\n</code></pre>\n<h1>Node</h1>\n<pre><code class=\"language-js\">\n/** Node is an interface from which a number of DOM API object types inherit. It allows those types to be treated similarly; for example, inheriting the same set of methods, or being tested in the same way. */\ninterface Node extends EventTarget {\n    /** Returns node&#39;s node document&#39;s document base URL. */\n    readonly baseURI: string;\n    /** Returns the children. */\n    readonly childNodes: NodeListOf&lt;ChildNode&gt;;\n    /** Returns the first child. */\n    readonly firstChild: ChildNode | null;\n    /** Returns true if node is connected and false otherwise. */\n    readonly isConnected: boolean;\n    /** Returns the last child. */\n    readonly lastChild: ChildNode | null;\n    /** Returns the next sibling. */\n    readonly nextSibling: ChildNode | null;\n    /** Returns a string appropriate for the type of node. */\n    readonly nodeName: string;\n    /** Returns the type of node. */\n    readonly nodeType: number;\n    nodeValue: string | null;\n    /** Returns the node document. Returns null for documents. */\n    readonly ownerDocument: Document | null;\n    /** Returns the parent element. */\n    readonly parentElement: HTMLElement | null;\n    /** Returns the parent. */\n    readonly parentNode: ParentNode | null;\n    /** Returns the previous sibling. */\n    readonly previousSibling: ChildNode | null;\n    textContent: string | null;\n    appendChild&lt;T extends Node&gt;(node: T): T;\n    /** Returns a copy of node. If deep is true, the copy also includes the node&#39;s descendants. */\n    cloneNode(deep?: boolean): Node;\n    /** Returns a bitmask indicating the position of other relative to node. */\n    compareDocumentPosition(other: Node): number;\n    /** Returns true if other is an inclusive descendant of node, and false otherwise. */\n    contains(other: Node | null): boolean;\n    /** Returns node&#39;s root. */\n    getRootNode(options?: GetRootNodeOptions): Node;\n    /** Returns whether node has children. */\n    hasChildNodes(): boolean;\n    insertBefore&lt;T extends Node&gt;(node: T, child: Node | null): T;\n    isDefaultNamespace(namespace: string | null): boolean;\n    /** Returns whether node and otherNode have the same properties. */\n    isEqualNode(otherNode: Node | null): boolean;\n    isSameNode(otherNode: Node | null): boolean;\n    lookupNamespaceURI(prefix: string | null): string | null;\n    lookupPrefix(namespace: string | null): string | null;\n    /** Removes empty exclusive Text nodes and concatenates the data of remaining contiguous exclusive Text nodes into the first of their nodes. */\n    normalize(): void;\n    removeChild&lt;T extends Node&gt;(child: T): T;\n    replaceChild&lt;T extends Node&gt;(node: Node, child: T): T;\n    readonly ATTRIBUTE_NODE: number;\n    /** node is a CDATASection node. */\n    readonly CDATA_SECTION_NODE: number;\n    /** node is a Comment node. */\n    readonly COMMENT_NODE: number;\n    /** node is a DocumentFragment node. */\n    readonly DOCUMENT_FRAGMENT_NODE: number;\n    /** node is a document. */\n    readonly DOCUMENT_NODE: number;\n    /** Set when other is a descendant of node. */\n    readonly DOCUMENT_POSITION_CONTAINED_BY: number;\n    /** Set when other is an ancestor of node. */\n    readonly DOCUMENT_POSITION_CONTAINS: number;\n    /** Set when node and other are not in the same tree. */\n    readonly DOCUMENT_POSITION_DISCONNECTED: number;\n    /** Set when other is following node. */\n    readonly DOCUMENT_POSITION_FOLLOWING: number;\n    readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number;\n    /** Set when other is preceding node. */\n    readonly DOCUMENT_POSITION_PRECEDING: number;\n    /** node is a doctype. */\n    readonly DOCUMENT_TYPE_NODE: number;\n    /** node is an element. */\n    readonly ELEMENT_NODE: number;\n    readonly ENTITY_NODE: number;\n    readonly ENTITY_REFERENCE_NODE: number;\n    readonly NOTATION_NODE: number;\n    /** node is a ProcessingInstruction node. */\n    readonly PROCESSING_INSTRUCTION_NODE: number;\n    /** node is a Text node. */\n    readonly TEXT_NODE: number;\n}\n\ndeclare var Node: {\n    prototype: Node;\n    new(): Node;\n    readonly ATTRIBUTE_NODE: number;\n    /** node is a CDATASection node. */\n    readonly CDATA_SECTION_NODE: number;\n    /** node is a Comment node. */\n    readonly COMMENT_NODE: number;\n    /** node is a DocumentFragment node. */\n    readonly DOCUMENT_FRAGMENT_NODE: number;\n    /** node is a document. */\n    readonly DOCUMENT_NODE: number;\n    /** Set when other is a descendant of node. */\n    readonly DOCUMENT_POSITION_CONTAINED_BY: number;\n    /** Set when other is an ancestor of node. */\n    readonly DOCUMENT_POSITION_CONTAINS: number;\n    /** Set when node and other are not in the same tree. */\n    readonly DOCUMENT_POSITION_DISCONNECTED: number;\n    /** Set when other is following node. */\n    readonly DOCUMENT_POSITION_FOLLOWING: number;\n    readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number;\n    /** Set when other is preceding node. */\n    readonly DOCUMENT_POSITION_PRECEDING: number;\n    /** node is a doctype. */\n    readonly DOCUMENT_TYPE_NODE: number;\n    /** node is an element. */\n    readonly ELEMENT_NODE: number;\n    readonly ENTITY_NODE: number;\n    readonly ENTITY_REFERENCE_NODE: number;\n    readonly NOTATION_NODE: number;\n    /** node is a ProcessingInstruction node. */\n    readonly PROCESSING_INSTRUCTION_NODE: number;\n    /** node is a Text node. */\n    readonly TEXT_NODE: number;\n};\n\n/** An iterator over the members of a list of the nodes in a subtree of the DOM. The nodes will be returned in document order. */\ninterface NodeIterator {\n    readonly filter: NodeFilter | null;\n    readonly pointerBeforeReferenceNode: boolean;\n    readonly referenceNode: Node;\n    readonly root: Node;\n    readonly whatToShow: number;\n    /** @deprecated */\n    detach(): void;\n    nextNode(): Node | null;\n    previousNode(): Node | null;\n}\n\ndeclare var NodeIterator: {\n    prototype: NodeIterator;\n    new(): NodeIterator;\n};\n\n/** NodeList objects are collections of nodes, usually returned by properties such as Node.childNodes and methods such as document.querySelectorAll(). */\ninterface NodeList {\n    /** Returns the number of nodes in the collection. */\n    readonly length: number;\n    /** Returns the node with index index from the collection. The nodes are sorted in tree order. */\n    item(index: number): Node | null;\n    /**\n     * Performs the specified action for each node in an list.\n     * @param callbackfn  A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the list.\n     * @param thisArg  An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.\n     */\n    forEach(callbackfn: (value: Node, key: number, parent: NodeList) =&gt; void, thisArg?: any): void;\n    [index: number]: Node;\n}\n\ndeclare var NodeList: {\n    prototype: NodeList;\n    new(): NodeList;\n};\n\ninterface NodeListOf&lt;TNode extends Node&gt; extends NodeList {\n    item(index: number): TNode;\n    /**\n     * Performs the specified action for each node in an list.\n     * @param callbackfn  A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the list.\n     * @param thisArg  An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.\n     */\n    forEach(callbackfn: (value: TNode, key: number, parent: NodeListOf&lt;TNode&gt;) =&gt; void, thisArg?: any): void;\n    [index: number]: TNode;\n}\n\ninterface NonDocumentTypeChildNode {\n    /** Returns the first following sibling that is an element, and null otherwise. */\n    readonly nextElementSibling: Element | null;\n    /** Returns the first preceding sibling that is an element, and null otherwise. */\n    readonly previousElementSibling: Element | null;\n}\n\ninterface NonElementParentNode {\n    /** Returns the first element within node&#39;s descendants whose ID is elementId. */\n    getElementById(elementId: string): Element | null;\n}\n</code></pre>\n<h1>Event</h1>\n<pre><code class=\"language-js\">\n/** Events providing information related to errors in scripts or in files. */\ninterface ErrorEvent extends Event {\n    readonly colno: number;\n    readonly error: any;\n    readonly filename: string;\n    readonly lineno: number;\n    readonly message: string;\n}\n\ndeclare var ErrorEvent: {\n    prototype: ErrorEvent;\n    new(type: string, eventInitDict?: ErrorEventInit): ErrorEvent;\n};\n\n/** An event which takes place in the DOM. */\ninterface Event {\n    /** Returns true or false depending on how event was initialized. True if event goes through its target&#39;s ancestors in reverse tree order, and false otherwise. */\n    readonly bubbles: boolean;\n    cancelBubble: boolean;\n    /** Returns true or false depending on how event was initialized. Its return value does not always carry meaning, but true can indicate that part of the operation during which event was dispatched, can be canceled by invoking the preventDefault() method. */\n    readonly cancelable: boolean;\n    /** Returns true or false depending on how event was initialized. True if event invokes listeners past a ShadowRoot node that is the root of its target, and false otherwise. */\n    readonly composed: boolean;\n    /** Returns the object whose event listener&#39;s callback is currently being invoked. */\n    readonly currentTarget: EventTarget | null;\n    /** Returns true if preventDefault() was invoked successfully to indicate cancelation, and false otherwise. */\n    readonly defaultPrevented: boolean;\n    /** Returns the event&#39;s phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE. */\n    readonly eventPhase: number;\n    /** Returns true if event was dispatched by the user agent, and false otherwise. */\n    readonly isTrusted: boolean;\n    /** @deprecated */\n    returnValue: boolean;\n    /** @deprecated */\n    readonly srcElement: EventTarget | null;\n    /** Returns the object to which event is dispatched (its target). */\n    readonly target: EventTarget | null;\n    /** Returns the event&#39;s timestamp as the number of milliseconds measured relative to the time origin. */\n    readonly timeStamp: DOMHighResTimeStamp;\n    /** Returns the type of event, e.g. &quot;click&quot;, &quot;hashchange&quot;, or &quot;submit&quot;. */\n    readonly type: string;\n    /** Returns the invocation target objects of event&#39;s path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root&#39;s mode is &quot;closed&quot; that are not reachable from event&#39;s currentTarget. */\n    composedPath(): EventTarget[];\n    /** @deprecated */\n    initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void;\n    /** If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled. */\n    preventDefault(): void;\n    /** Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects. */\n    stopImmediatePropagation(): void;\n    /** When dispatched in a tree, invoking this method prevents event from reaching any objects other than the current object. */\n    stopPropagation(): void;\n    readonly AT_TARGET: number;\n    readonly BUBBLING_PHASE: number;\n    readonly CAPTURING_PHASE: number;\n    readonly NONE: number;\n}\n\ndeclare var Event: {\n    prototype: Event;\n    new(type: string, eventInitDict?: EventInit): Event;\n    readonly AT_TARGET: number;\n    readonly BUBBLING_PHASE: number;\n    readonly CAPTURING_PHASE: number;\n    readonly NONE: number;\n};\n\ninterface EventCounts {\n    forEach(callbackfn: (value: number, key: string, parent: EventCounts) =&gt; void, thisArg?: any): void;\n}\n\ndeclare var EventCounts: {\n    prototype: EventCounts;\n    new(): EventCounts;\n};\n\ninterface EventListener {\n    (evt: Event): void;\n}\n\ninterface EventListenerObject {\n    handleEvent(object: Event): void;\n}\n\ninterface EventSourceEventMap {\n    &quot;error&quot;: Event;\n    &quot;message&quot;: MessageEvent;\n    &quot;open&quot;: Event;\n}\n\ninterface EventSource extends EventTarget {\n    onerror: ((this: EventSource, ev: Event) =&gt; any) | null;\n    onmessage: ((this: EventSource, ev: MessageEvent) =&gt; any) | null;\n    onopen: ((this: EventSource, ev: Event) =&gt; any) | null;\n    /** Returns the state of this EventSource object&#39;s connection. It can have the values described below. */\n    readonly readyState: number;\n    /** Returns the URL providing the event stream. */\n    readonly url: string;\n    /** Returns true if the credentials mode for connection requests to the URL providing the event stream is set to &quot;include&quot;, and false otherwise. */\n    readonly withCredentials: boolean;\n    /** Aborts any instances of the fetch algorithm started for this EventSource object, and sets the readyState attribute to CLOSED. */\n    close(): void;\n    readonly CLOSED: number;\n    readonly CONNECTING: number;\n    readonly OPEN: number;\n    addEventListener&lt;K extends keyof EventSourceEventMap&gt;(type: K, listener: (this: EventSource, ev: EventSourceEventMap[K]) =&gt; any, options?: boolean | AddEventListenerOptions): void;\n    addEventListener(type: string, listener: (this: EventSource, event: MessageEvent) =&gt; any, options?: boolean | AddEventListenerOptions): void;\n    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n    removeEventListener&lt;K extends keyof EventSourceEventMap&gt;(type: K, listener: (this: EventSource, ev: EventSourceEventMap[K]) =&gt; any, options?: boolean | EventListenerOptions): void;\n    removeEventListener(type: string, listener: (this: EventSource, event: MessageEvent) =&gt; any, options?: boolean | EventListenerOptions): void;\n    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n\ndeclare var EventSource: {\n    prototype: EventSource;\n    new(url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource;\n    readonly CLOSED: number;\n    readonly CONNECTING: number;\n    readonly OPEN: number;\n};\n\n/** EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. */\ninterface EventTarget {\n    /**\n     * Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.\n     *\n     * The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options&#39;s capture.\n     *\n     * When set to true, options&#39;s capture prevents callback from being invoked when the event&#39;s eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event&#39;s eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event&#39;s eventPhase attribute value is AT_TARGET.\n     *\n     * When set to true, options&#39;s passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.\n     *\n     * When set to true, options&#39;s once indicates that the callback will only be invoked once after which the event listener will be removed.\n     *\n     * If an AbortSignal is passed for options&#39;s signal, then the event listener will be removed when signal is aborted.\n     *\n     * The event listener is appended to target&#39;s event listener list and is not appended if it has the same type, callback, and capture.\n     */\n    addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;\n    /** Dispatches a synthetic event event to target and returns true if either event&#39;s cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. */\n    dispatchEvent(event: Event): boolean;\n    /** Removes the event listener in target&#39;s event listener list with the same type, callback, and options. */\n    removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;\n}\n\ndeclare var EventTarget: {\n    prototype: EventTarget;\n    new(): EventTarget;\n};\n</code></pre>\n","tags":["web-api","dom"]},{"id":"web-canvas-2d","url":"https://yieldray.fun/posts/web-canvas-2d","title":"Canvas 2D","date_published":"2022-11-26T22:22:22.000Z","date_modified":"2022-11-26T22:22:22.000Z","content_text":"<p><a href=\"https://www.canvasapi.cn/\">https://www.canvasapi.cn/</a><br><a href=\"https://wangdoc.com/webapi/canvas\">https://wangdoc.com/webapi/canvas</a><br><a href=\"https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors\">https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors</a></p>\n<h1>Typescript 定义</h1>\n<p>本页面的 Typescript 定义是经过省略的</p>\n<pre><code class=\"language-ts\">/** 提供用于操作 &lt;canvas&gt; 元素布局和显示的属性和方法。HTMLCanvasElement 接口还继承了 HTMLElement 接口的属性和方法 */\ninterface HTMLCanvasElement extends HTMLElement {\n    /** 获取或设置文档中 canvas 元素的高度 */\n    height: number;\n    /** 获取或设置文档中 canvas 元素的宽度 */\n    width: number;\n    captureStream(frameRequestRate?: number): MediaStream;\n    /**\n     * 返回上下文对象，该对象提供在文档中 canvas 元素上绘制和操作图像和图形的方法和属性。上下文对象包括关于颜色、线宽、字体和其他可以在 canvas 上绘制的图形参数的信息。\n     * @param contextId 要创建的 canvas 类型的标识符。IE 9 和 IE 10 仅支持使用 canvas.getContext(&quot;2d&quot;) 的二维上下文；IE11 Preview 还支持通过 canvas.getContext(&quot;experimental-webgl&quot;) 获取 3-D 或 WebGL 上下文\n     */\n    getContext(contextId: &quot;2d&quot;, options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D | null;\n    toBlob(callback: BlobCallback, type?: string, quality?: any): void;\n    /**\n     * 将当前 canvas 作为图形返回，可以用作其它 canvas 或 HTML 元素的源\n     * @param type 指定返回图像的标准 MIME 类型，默认为 PNG 格式\n     */\n    toDataURL(type?: string, quality?: any): string;\n}\n\n/** CanvasRenderingContext2D 接口是 Canvas API 的一部分，它为 &lt;Canvas&gt; 元素的绘图表面提供 2D 渲染上下文。它用于绘制形状、文本、图像和其他对象 */\n// prettier-ignore\ninterface CanvasRenderingContext2D extends CanvasCompositing,CanvasDrawImage,CanvasDrawPath,CanvasFillStrokeStyles,CanvasFilters,CanvasImageData,CanvasImageSmoothing,CanvasPath,CanvasPathDrawingStyles,CanvasRect,CanvasShadowStyles,CanvasState,CanvasText,CanvasTextDrawingStyles,CanvasTransform,CanvasUserInterface {\n    readonly canvas: HTMLCanvasElement;\n    getContextAttributes(): CanvasRenderingContext2DSettings;\n}\n</code></pre>\n<h1>canvas 元素，上下文</h1>\n<pre><code class=\"language-js\">const canvas = document.createElement(&quot;canvas&quot;);\n//  左上角为坐标原点\ncanvas.height = 150;\ncanvas.width = 150;\ndocument.body.append(canvas);\nconst ctx = canvas.getContext(&quot;2d&quot;);\n</code></pre>\n<h1>路径与矩形</h1>\n<pre><code class=\"language-ts\">interface CanvasDrawPath {\n    beginPath(): void;\n    clip(fillRule?: CanvasFillRule): void;\n    clip(path: Path2D, fillRule?: CanvasFillRule): void;\n    fill(fillRule?: CanvasFillRule): void;\n    fill(path: Path2D, fillRule?: CanvasFillRule): void;\n    isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean;\n    isPointInPath(path: Path2D, x: number, y: number, fillRule?: CanvasFillRule): boolean;\n    isPointInStroke(x: number, y: number): boolean;\n    isPointInStroke(path: Path2D, x: number, y: number): boolean;\n    stroke(): void;\n    stroke(path: Path2D): void;\n}\ninterface CanvasPath {\n    arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;\n    arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;\n    bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;\n    closePath(): void;\n    // prettier-ignore\n    ellipse(x: number,y: number,radiusX: number,radiusY: number,rotation: number,startAngle: number,endAngle: number,counterclockwise?: boolean\n    ): void;\n    lineTo(x: number, y: number): void;\n    moveTo(x: number, y: number): void;\n    quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void;\n    rect(x: number, y: number, w: number, h: number): void;\n}\ninterface CanvasRect {\n    clearRect(x: number, y: number, w: number, h: number): void;\n    fillRect(x: number, y: number, w: number, h: number): void;\n    strokeRect(x: number, y: number, w: number, h: number): void;\n}\n/** Canvas 2D API 接口用于声明一个路径，然后可以在 CanvasRenderingContext2D 上下文对象上使用该路径。CanvasRenderingContext2D 接口的路径方法也出现在这个接口上，允许我们在需要时方便地保留和重放路径 */\ninterface Path2D extends CanvasPath {\n    /** 将给定参数的路径添加到Path2D对象上 */\n    addPath(path: Path2D, transform?: DOMMatrix2DInit): void;\n}\ndeclare var Path2D: {\n    prototype: Path2D;\n    new (path?: Path2D | string): Path2D;\n};\n</code></pre>\n<pre><code class=\"language-js\">// 绘制矩形，参数为 x y width height 单位为px\nctx.fillRect(0, 0, 20, 20);\nctx.strokeRect(25, 25, 20, 20);\nctx.clearRect(15, 15, 20, 20);\n\n// 绘制路径\nctx.beginPath();\nctx.moveTo(0, 50); // 通常表示路径绘制的起始点\n// lineTo方法用于指定直线路径\nctx.lineTo(0, 70);\nctx.lineTo(20, 70);\nctx.stroke(); // 执行此方法才画线\nctx.closePath(); // 此方法不是必须的\n\nctx.beginPath();\nctx.moveTo(0, 80);\nctx.lineTo(10, 80);\nctx.lineTo(5, 80 + 5 * Math.sqrt(3));\nctx.fill(); // 执行此方法才填充\nctx.closePath();\n\n// 单位一律为弧度\nctx.beginPath();\n// arc方法通过圆心和半径指定弧形路径\n// 参数为 x, y, radius, startAngle, endAngle [, anticlockwise]\nctx.arc(20, 100, 5, 0, Math.PI * 2, true); // 此方法仅移动画笔\nctx.stroke();\nctx.closePath();\n\nctx.beginPath();\nctx.moveTo(70, 70);\n// arcTo方法通过给定两个控制点和半径指定弧形路径\n// 参数为 x1, y1, x2, y2, radius\nctx.arcTo(88, 146, 115, 90, 50);\nctx.lineTo(115, 70);\nctx.stroke();\n// quadraticCurveTo 二次贝塞尔曲线\n// bezierCurveTo 三次贝塞尔曲线\n\nctx.rect(100, 100, 50, 50); // 矩形路径\nctx.fill();\n\n/* Path2D 对象。表示一段自定义路径，通过构造函数可以从其它Path2D对象或SVG字符串派生 */\n// https://developer.mozilla.org/docs/Web/API/Path2D\nconst myFirstPath = new Path2D();\nmyFirstPath.moveTo(50, 50);\nmyFirstPath.lineTo(60, 60);\nctx.stroke(myFirstPath);\n</code></pre>\n<h1>样式与颜色</h1>\n<pre><code class=\"language-ts\">interface CanvasPathDrawingStyles {\n    lineCap: &quot;butt&quot; | &quot;round&quot; | &quot;square&quot;;\n    lineDashOffset: number;\n    lineJoin: &quot;bevel&quot; | &quot;miter&quot; | &quot;round&quot;;\n    lineWidth: number;\n    miterLimit: number;\n    getLineDash(): number[];\n    setLineDash(segments: number[]): void;\n}\n</code></pre>\n<h2>颜色</h2>\n<p>应指定为符合 <a href=\"https://www.w3.org/TR/css-color-3/\">CSS3 颜色值标准</a> 的有效字符串</p>\n<pre><code class=\"language-js\">ctx.strokeStyle = &quot;red&quot;;\nctx.fillStyle = &quot;blue&quot;;\n\n// 设置画布的全局透明度，范围[0, 1]，指定为范围外的值会被忽略\nctx.globalAlpha = 0.5;\n</code></pre>\n<h2>线条</h2>\n<pre><code class=\"language-js\">// 设置线条宽度。默认为1\nctx.lineWidth = 1;\n\n// 设置线条端点样式。可选：buzz(默认) round square\nctx.lineCap = &quot;square&quot;;\n\n// 设定线条转角处的样式。可选：miter(默认) round bevel\nctx.lineJoin = &quot;bevel&quot;;\n\n// 当lineJoin类型是miter时，miter效果生效的限制值。\nctx.miterLimit = 2;\n\n// 设置当前虚线样式\nctx.setLineDash([2, 1]);\n\n// 返回当前虚线样式。并不总是与设置的虚线样式数组内容相同\nctx.getLineDash(); // =&gt; [2, 1]\n\n// 指定虚线绘制的偏移距离\nctx.lineDashOffset = 2;\n</code></pre>\n<h2>渐变</h2>\n<pre><code class=\"language-ts\">interface CanvasFillStrokeStyles {\n    fillStyle: string | CanvasGradient | CanvasPattern;\n    strokeStyle: string | CanvasGradient | CanvasPattern;\n    createConicGradient(startAngle: number, x: number, y: number): CanvasGradient;\n    createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;\n    createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;\n    createPattern(image: CanvasImageSource, repetition: string | null): CanvasPattern | null;\n}\n/** 描述渐变的不透明对象。由 CanvasRenderingContext2D.createLinearGradient() 或 CanvasRenderingContext2D.createRadialGradient() 方法返回 */\ninterface CanvasGradient {\n    /**\n     * 将具有给定颜色的色标添加到渐变中的给定偏移量处。0.0 是渐变一端的偏移，1.0 是另一端的偏移。\n     * 偏移量超出范围时抛出 &quot;IndexSizeError&quot; DOMException。 当颜色无法解析时抛出 &quot;SyntaxError&quot; DOMException。\n     */\n    addColorStop(offset: number, color: string): void;\n}\ndeclare var CanvasGradient: {\n    prototype: CanvasGradient;\n    new (): CanvasGradient;\n};\n</code></pre>\n<pre><code class=\"language-js\">ctx.fillStyle = (() =&gt; {\n    const lingrad = ctx.createLinearGradient(0, 0, 0, 150);\n    lingrad.addColorStop(0, &quot;#00ABEB&quot;);\n    lingrad.addColorStop(0.5, &quot;#fff&quot;);\n    lingrad.addColorStop(0.5, &quot;#26C000&quot;);\n    lingrad.addColorStop(1, &quot;#fff&quot;);\n    return lingrad;\n})();\n\nctx.fillRect(10, 10, 130, 130);\n</code></pre>\n<h2>图案样式</h2>\n<pre><code class=\"language-js\">const img = new Image();\nimg.src =\n    &quot;https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas_createpattern.png&quot;;\nimg.onload = () =&gt; {\n    const ptrn = ctx.createPattern(img, &quot;repeat&quot;); // 此方法需要保证图片加载完毕\n    ctx.fillStyle = ptrn;\n    ctx.fillRect(0, 0, 150, 150);\n};\n</code></pre>\n<h2>阴影</h2>\n<pre><code class=\"language-ts\">interface CanvasShadowStyles {\n    shadowBlur: number;\n    shadowColor: string;\n    shadowOffsetX: number;\n    shadowOffsetY: number;\n}\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#%E9%98%B4%E5%BD%B1_shadows\">https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#%E9%98%B4%E5%BD%B1_shadows</a></p>\n<pre><code class=\"language-js\">ctx.shadowOffsetX = 2;\nctx.shadowOffsetY = 2;\nctx.shadowBlur = 2;\nctx.shadowColor = &quot;rgba(0, 0, 0, 0.5)&quot;;\n\nctx.font = &quot;20px Times New Roman&quot;;\nctx.fillStyle = &quot;Black&quot;;\nctx.fillText(&quot;Sample String&quot;, 5, 30);\n</code></pre>\n<h2>Canvas 填充规则</h2>\n<pre><code class=\"language-ts\">type CanvasFillRule = &quot;evenodd&quot; | &quot;nonzero&quot;;\n\ninterface CanvasDrawPath {\n    beginPath(): void;\n    clip(fillRule?: CanvasFillRule): void;\n    clip(path: Path2D, fillRule?: CanvasFillRule): void;\n    fill(fillRule?: CanvasFillRule): void;\n    fill(path: Path2D, fillRule?: CanvasFillRule): void;\n    isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean;\n    isPointInPath(path: Path2D, x: number, y: number, fillRule?: CanvasFillRule): boolean;\n    isPointInStroke(x: number, y: number): boolean;\n    isPointInStroke(path: Path2D, x: number, y: number): boolean;\n    stroke(): void;\n    stroke(path: Path2D): void;\n}\n</code></pre>\n<pre><code class=\"language-js\">ctx.beginPath();\nctx.arc(50, 50, 30, 0, Math.PI * 2, true);\nctx.arc(50, 50, 15, 0, Math.PI * 2, true);\nctx.fill(&quot;evenodd&quot;);\n</code></pre>\n<h1>文本</h1>\n<pre><code class=\"language-ts\">interface CanvasText {\n    // x 和 y 指定的是文本基线的起始点坐标\n    fillText(text: string, x: number, y: number, maxWidth?: number): void;\n    strokeText(text: string, x: number, y: number, maxWidth?: number): void;\n    measureText(text: string): TextMetrics;\n}\ninterface CanvasTextDrawingStyles {\n    font: string;\n    direction: &quot;inherit&quot; | &quot;ltr&quot; | &quot;rtl&quot;;\n    textAlign: &quot;center&quot; | &quot;end&quot; | &quot;left&quot; | &quot;right&quot; | &quot;start&quot;;\n    textBaseline: &quot;alphabetic&quot; | &quot;bottom&quot; | &quot;hanging&quot; | &quot;ideographic&quot; | &quot;middle&quot; | &quot;top&quot;;\n}\n/** canvas 中一段文本的尺寸，通过 CanvasRenderingContext2D.measureText() 方法创建 */\ninterface TextMetrics {\n    readonly actualBoundingBoxAscent: number;\n    readonly actualBoundingBoxDescent: number;\n    readonly actualBoundingBoxLeft: number;\n    readonly actualBoundingBoxRight: number;\n    readonly fontBoundingBoxAscent: number;\n    readonly fontBoundingBoxDescent: number;\n    readonly width: number; // 常用\n}\ndeclare var TextMetrics: {\n    prototype: TextMetrics;\n    new (): TextMetrics;\n};\n</code></pre>\n<pre><code class=\"language-js\">ctx.arc(0, 10, 1, 0, 2 * Math.PI);\nctx.fill(); // 基线默认是alphabetic，因此在文本的左下角绘制一个圆\nctx.font = &quot;16px serif&quot;;\nctx.fillText(&quot;Hello world&quot;, 0, 10);\n// prettier-ignore\nconst { actualBoundingBoxAscent, actualBoundingBoxDescent, actualBoundingBoxLeft, actualBoundingBoxRight, fontBoundingBoxAscent, fontBoundingBoxDescent, width } = ctx.measureText(&quot;Hello world&quot;);\n</code></pre>\n<h1>使用图片</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/drawImage\">https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/drawImage</a><br><a href=\"https://www.canvasapi.cn/CanvasRenderingContext2D/drawImage\">https://www.canvasapi.cn/CanvasRenderingContext2D/drawImage</a></p>\n<pre><code class=\"language-ts\">type CanvasImageSource = HTMLOrSVGImageElement | HTMLVideoElement | HTMLCanvasElement | ImageBitmap;\n// prettier-ignore\ninterface CanvasDrawImage {\n    drawImage(image: CanvasImageSource, dx: number, dy: number): void;\n    drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;\n    drawImage(image: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;\n}\n</code></pre>\n<h1>形变</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Transformations\">https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Transformations</a></p>\n<pre><code class=\"language-ts\">interface CanvasState {\n    restore(): void;\n    save(): void;\n}\ninterface CanvasTransform {\n    getTransform(): DOMMatrix;\n    resetTransform(): void;\n    rotate(angle: number): void;\n    scale(x: number, y: number): void;\n    setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;\n    setTransform(transform?: DOMMatrix2DInit): void;\n    transform(a: number, b: number, c: number, d: number, e: number, f: number): void;\n    translate(x: number, y: number): void;\n}\n</code></pre>\n<h1>合成与裁剪</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Compositing\">https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Compositing</a></p>\n<h1>像素操作</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas\">https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas</a></p>\n","tags":["web-api","dom"]},{"id":"js-lit-note","url":"https://yieldray.fun/posts/js-lit-note","title":"lit笔记","date_published":"2022-11-20T14:00:00.000Z","date_modified":"2022-11-20T14:00:00.000Z","content_text":"<h1>Lit</h1>\n<p>使用 Lit 必须提前了解 <a href=\"/posts/web-components/\">WebComponents</a></p>\n<p>Doc <a href=\"https://lit.dev/docs/\">https://lit.dev/docs/</a><br>API <a href=\"https://lit.dev/docs/api/LitElement/\">https://lit.dev/docs/api/LitElement/</a><br>另见 <a href=\"https://lit.how/\">https://lit.how/</a></p>\n<p>在 Typescript 中可以使用以下方式将自定义元素合并到 HTMLElementTagNameMap</p>\n<pre><code class=\"language-ts\">declare global {\n    interface HTMLElementTagNameMap {\n        &quot;my-element&quot;: MyElement;\n    }\n}\n</code></pre>\n<p><a href=\"https://lit.dev/docs/templates/expressions/\">https://lit.dev/docs/templates/expressions/</a></p>\n<p>子节点</p>\n<pre><code class=\"language-html\">html`\n&lt;h1&gt;Hello ${name}&lt;/h1&gt;\n&lt;ul&gt;\n    ${listItems}\n&lt;/ul&gt;\n`\n</code></pre>\n<p>标签属性</p>\n<pre><code class=\"language-html\">html`\n&lt;div class=&quot;${highlightClass}&quot;&gt;&lt;/div&gt;\n`\n</code></pre>\n<p>布尔值标签属性</p>\n<pre><code class=\"language-html\">html`\n&lt;div ?hidden=&quot;${!show}&quot;&gt;&lt;/div&gt;\n`\n</code></pre>\n<p>对象属性</p>\n<pre><code class=\"language-html\">html`&lt;input .value=&quot;${value}&quot; /&gt;`\n</code></pre>\n<p>事件监听器</p>\n<pre><code class=\"language-html\">html`&lt;button @click=&quot;${this._clickHandler}&quot;&gt;点我&lt;/button&gt;`\n</code></pre>\n<p>元素指令（获取 ref）</p>\n<pre><code class=\"language-html\">html`&lt;input ${ref(inputRef)} /&gt;`\n</code></pre>\n<h1><a href=\"https://lit.dev/docs/components/properties/\">属性 &amp; 状态</a></h1>\n<p>属性可以被外部访问（应视为外部输入），属性是响应式的（即，会自动触发重渲染）。<br>属性的相等性<em>默认</em>通过<em>严格相等比较运算</em>判断，因此非原始类型若要自动触发变更则需修改引用，<br>但可调用 <code>this.requestUpdate()</code> 手动触发重渲染。重渲染会推迟到下一个微任务进行。</p>\n<p>状态是一种特殊的（内部）属性，lit 不为其分配外部接口（标签属性 attr 和对象属性 prop）</p>\n<blockquote>\n<p>lit 默认为我们声明的响应式属性生成 accessor，定义在当前类的 prototype 上<br>很少需要创建<a href=\"https://lit.dev/docs/components/properties/#accessors-custom\">自定义 accessor</a>。避免设置同名实例属性，防止覆盖 accessor。</p>\n</blockquote>\n<p>property 装饰器选项：<a href=\"https://lit.dev/docs/components/properties/#property-options\">https://lit.dev/docs/components/properties/#property-options</a></p>\n<pre><code class=\"language-ts\">import { LitElement, html } from &quot;lit&quot;;\nimport { customElement } from &quot;lit/decorators.js&quot;;\n\n@customElement(&quot;my-element&quot;)\nexport class MyElement extends LitElement {\n    // 最简单的属性\n    @property()\n    name: string;\n\n    // 最简单的状态（往往标记为 private 或 protected）\n    @state()\n    private _counter = 0;\n\n    // 不生成标签属性\n    @property({ attribute: false })\n    myData = {};\n\n    // 生成指定名称的标签属性。默认是直接转换成小写（并不会将驼峰转为短横线）\n    @property({ attribute: &quot;my-name&quot; })\n    myName = &quot;Ogden&quot;;\n\n    // 转换器\n    // 由于标签属性都是 DOM String，因此需要转换器转换\n    // converter 若指定为函数，相当于只指定了 fromAttribute\n    @property({\n        converter: {\n            fromAttribute: (value, type) =&gt; {\n                // `value` is a string\n                // Convert it to a value of type `type` and return it\n            },\n            toAttribute: (value, type) =&gt; {\n                // `value` is of type `type`\n                // Convert it to a string and return it\n            },\n        },\n    })\n    myTime = new Date();\n\n    // 使用内置转换器 string -&gt; number\n    // 内置的转换器有；String(默认) Number Boolean Object Array\n    @property({ type: Number })\n    aNumber: number = 5;\n\n    // reflect选项，是否将属性反映到标签属性attr上\n    @property({ type: Boolean, reflect: true })\n    active: boolean = false;\n\n    // 自定义相等性比较\n    @property({\n        hasChanged(newVal: string, oldVal: string) {\n            return newVal?.toLowerCase() !== oldVal?.toLowerCase();\n        },\n    })\n    myProp: string | undefined;\n}\n</code></pre>\n<h1>样式</h1>\n<pre><code class=\"language-ts\">import { LitElement, html, css, unsafeCSS } from &quot;lit&quot;;\nimport { customElement } from &quot;lit/decorators.js&quot;;\n\n@customElement(&quot;my-element&quot;)\nexport class MyElement extends SuperElement {\n    // css 模板字符串函数\n    mainColor: CSSResultGroup = css`red`;\n    static styles = css`\n        div {\n            color: ${mainColor};\n        }\n    `;\n\n    // unsafeCSS 函数用于渲染字符串 css\n    mainColor: CSSResultGroup = &quot;red&quot;;\n    static styles = css`\n        div {\n            color: ${unsafeCSS(mainColor)};\n        }\n    `;\n\n    // styles 其实是一个数组（adoptedStyleSheets）\n    // 由于 styles 是静态属性，因此也可以访问父类的 styles\n    static styles: CSSResultGroup = [\n        SuperElement.styles,\n        css`\n            div {\n                color: red;\n            }\n        `,\n    ];\n\n    // html 模板中也可以使用 style 标签。style 标签对每个实例求值，静态 styles 属性则对每个类求值\n    protected render() {\n        return html`\n            &lt;p&gt;I am green!&lt;/p&gt;\n            &lt;style&gt;\n                :host {\n                    color: green;\n                }\n            &lt;/style&gt;\n        `;\n    }\n}\n\n// classMap 和 styleMap 实用函数\nimport { classMap } from &quot;lit/directives/class-map.js&quot;;\nimport { styleMap } from &quot;lit/directives/style-map.js&quot;;\n\n@customElement(&quot;my-element&quot;)\nexport class MyElement extends LitElement {\n    static styles = css`\n        .someclass {\n            border: 1px solid red;\n            padding: 4px;\n        }\n        .anotherclass {\n            background-color: navy;\n        }\n    `;\n    @property()\n    classes = { someclass: true, anotherclass: true };\n    @property()\n    styles = { color: &quot;lightgreen&quot;, fontFamily: &quot;Roboto&quot; };\n    protected render() {\n        return html` &lt;div class=${classMap(this.classes)} style=${styleMap(this.styles)}&gt;Some content&lt;/div&gt; `;\n    }\n}\n</code></pre>\n<h1><a href=\"https://lit.dev/docs/components/lifecycle/\">生命周期</a></h1>\n<p>LitElement 继承了 HTMLElement 的生命周期，即 WebComponents 的生命周期</p>\n<pre><code class=\"language-ts\">connectedCallback() {\n  super.connectedCallback();\n  this._timerInterval = setInterval(() =&gt; this.requestUpdate(), 1000);\n}\n\ndisconnectedCallback() {\n  super.disconnectedCallback();\n  clearInterval(this._timerInterval);\n}\n</code></pre>\n<p><strong>Pre-Update</strong><br><img src=\"https://i0.wp.com/lit.dev/images/docs/components/update-1.jpg\" alt=\"Pre-Update\"><br><img src=\"https://i0.wp.com/lit.dev/images/docs/components/update-2.jpg\" alt=\"Pre-Update\"><br><strong>Update</strong><br><img src=\"https://i0.wp.com/lit.dev/images/docs/components/update-3.jpg\" alt=\"Update\"><br><strong>Post-Update</strong><br><img src=\"https://i0.wp.com/lit.dev/images/docs/components/update-4.jpg\" alt=\"Post-Update\"></p>\n<pre><code class=\"language-ts\">import {LitElement, html, PropertyValues} from &#39;lit&#39;;\n...\n  shouldUpdate(changedProperties: PropertyValues&lt;this&gt;) {\n    ...\n  }\n</code></pre>\n<p>初次渲染的生命周期函数顺序如下（cp=changedProperties）：</p>\n<ul>\n<li>requestUpdate（响应式属性为：undefined）</li>\n<li>requestUpdate（响应式属性为：初始值）</li>\n<li>constructor</li>\n<li>requestUpdate（响应式属性为：标签属性值，若不存在则不触发）</li>\n<li>attributeChangedCallback（若不存在对应标签属性则不触发）</li>\n<li>connectedCallback（此时响应式属性已正确初始化，lit 保证 shadowRoot 已创建，但尚未开始渲染）</li>\n<li>shouldUpdate(cp)</li>\n<li>willUpdate(cp)（此处修改响应式属性不会触发重渲染）</li>\n<li>render（注意第一次渲染发生在此处）</li>\n<li>update(cp) （必须调用 super）</li>\n<li>firstUpdated(cp)（仅第一次渲染后触发）</li>\n<li>updated(cp)</li>\n</ul>\n<p>响应式属性变化后：</p>\n<ul>\n<li>requestUpdate(name, oldVal, options)</li>\n<li>shouldUpdate</li>\n<li>attributeChangedCallback（仅当是标签属性改变才触发）</li>\n<li>shouldUpdate</li>\n<li>willUpdate</li>\n<li>render</li>\n<li>update</li>\n<li>updated</li>\n</ul>\n<p>其余生命周期可用于自定义渲染时机，此处略</p>\n<blockquote>\n<p>参见<a href=\"https://lit.dev/playground/#project=W3sibmFtZSI6ImRlbW8tYXBwLnRzIiwiY29udGVudCI6ImltcG9ydCB7aHRtbCwgTGl0RWxlbWVudCwgbm90aGluZ30gZnJvbSAnbGl0JztcbmltcG9ydCB7Y3VzdG9tRWxlbWVudCwgcHJvcGVydHl9IGZyb20gJ2xpdC9kZWNvcmF0b3JzLmpzJztcbmltcG9ydCBcIi4vbGlmZS1jeWNsZS5qc1wiXG4gICAgXG5AY3VzdG9tRWxlbWVudCgnZGVtby1hcHAnKVxuZXhwb3J0IGNsYXNzIERlbW9BcHAgZXh0ZW5kcyBMaXRFbGVtZW50IHtcblxuICBAcHJvcGVydHkoKVxuICBtb3VudCA9IGZhbHNlO1xuXG4gIHJlbmRlcigpIHtcbiAgICByZXR1cm4gaHRtbGBcbiAgICAgICAgPGJ1dHRvbiBAY2xpY2s9JHsoKT0-dGhpcy5tb3VudD0hdGhpcy5tb3VudH0-XG4gICAgICAgICAgQ2xpY2sgbWUgdG8gJHt0aGlzLm1vdW50P1wiVU5NT1VOVFwiOlwiTU9VTlRcIn0gdGhlIGNvbXBvbmVudFxuICAgICAgICA8L2J1dHRvbj5cbiAgICAgICAgJHt0aGlzLm1vdW50ID8gaHRtbGA8bGlmZS1jeWNsZS8-YCA6IG5vdGhpbmd9YDtcbiAgfVxufVxuIn0seyJuYW1lIjoiaW5kZXguaHRtbCIsImNvbnRlbnQiOiI8IURPQ1RZUEUgaHRtbD5cbjxoZWFkPlxuICA8c2NyaXB0IHR5cGU9XCJtb2R1bGVcIiBzcmM9XCIuL2RlbW8tYXBwLmpzXCI-PC9zY3JpcHQ-XG48L2hlYWQ-XG48Ym9keT5cbiAgPGRlbW8tYXBwPjwvZGVtby1hcHA-XG48L2JvZHk-XG4ifSx7Im5hbWUiOiJwYWNrYWdlLmpzb24iLCJjb250ZW50Ijoie1xuICBcImRlcGVuZGVuY2llc1wiOiB7XG4gICAgXCJsaXRcIjogXCJeMy4wLjBcIixcbiAgICBcIkBsaXQvcmVhY3RpdmUtZWxlbWVudFwiOiBcIl4yLjAuMFwiLFxuICAgIFwibGl0LWVsZW1lbnRcIjogXCJeNC4wLjBcIixcbiAgICBcImxpdC1odG1sXCI6IFwiXjMuMC4wXCJcbiAgfVxufSIsImhpZGRlbiI6dHJ1ZX0seyJuYW1lIjoibGlmZS1jeWNsZS50cyIsImNvbnRlbnQiOiJpbXBvcnQge2h0bWwsIExpdEVsZW1lbnQsIFByb3BlcnR5VmFsdWVzfSBmcm9tICdsaXQnO1xuaW1wb3J0IHtjdXN0b21FbGVtZW50LCBwcm9wZXJ0eX0gZnJvbSAnbGl0L2RlY29yYXRvcnMuanMnO1xuaW1wb3J0IHsgcmVmIH0gZnJvbSAnbGl0L2RpcmVjdGl2ZXMvcmVmLmpzJztcblxuLy8gaHR0cHM6Ly9saXQuZGV2L2RvY3MvY29tcG9uZW50cy9saWZlY3ljbGUvXG5AY3VzdG9tRWxlbWVudCgnbGlmZS1jeWNsZScpXG5leHBvcnQgY2xhc3MgTGlmZUN5Y2xlIGV4dGVuZHMgTGl0RWxlbWVudCB7XG4gIC8vIFN0YW5kYXJkIGN1c3RvbSBlbGVtZW50IGxpZmVjeWNsZVxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICBzdXBlcigpXG4gICAgY29uc29sZS5sb2coXCJjb25zdHJ1Y3RvclwiKVxuICB9XG4gIGNvbm5lY3RlZENhbGxiYWNrKCkge1xuICAgIHN1cGVyLmNvbm5lY3RlZENhbGxiYWNrKClcbiAgICBjb25zb2xlLmxvZyhcImNvbm5lY3RlZENhbGxiYWNrXCIpXG4gIH1cbiAgZGlzY29ubmVjdGVkQ2FsbGJhY2soKSB7XG4gICAgc3VwZXIuZGlzY29ubmVjdGVkQ2FsbGJhY2soKVxuICAgIGNvbnNvbGUubG9nKFwiZGlzY29ubmVjdGVkQ2FsbGJhY2tcIilcbiAgfVxuICBhdHRyaWJ1dGVDaGFuZ2VkQ2FsbGJhY2sobmFtZTogc3RyaW5nLCBvbGRWYWw6IHN0cmluZyB8IG51bGwsIG5ld1ZhbDogc3RyaW5nIHwgbnVsbCkge1xuICAgIHN1cGVyLmF0dHJpYnV0ZUNoYW5nZWRDYWxsYmFjayhuYW1lLG9sZFZhbCxuZXdWYWwpXG4gICAgY29uc29sZS5sb2coXCJhdHRyaWJ1dGVDaGFuZ2VkQ2FsbGJhY2tcIixuYW1lLG9sZFZhbCxuZXdWYWwpXG4gIH1cbiAgYWRvcHRlZENhbGxiYWNrKCkge1xuICAgIGNvbnNvbGUubG9nKFwiYWRvcHRlZENhbGxiYWNrXCIpXG4gIH1cbiAgXG4gIC8vIFJlYWN0aXZlIHVwZGF0ZSBjeWNsZVxuICAvLyBub3RlIHRoYXQgd2UgY2FsbCBzdXBlciBvbmx5IHdoZW4gbmVjZXNzYXJ5XG4gIHNob3VsZFVwZGF0ZShjaGFuZ2VkUHJvcGVydGllczogUHJvcGVydHlWYWx1ZXM8dGhpcz4pe1xuICAgIGNvbnNvbGUubG9nKFwic2hvdWxkVXBkYXRlXCIsIGNoYW5nZWRQcm9wZXJ0aWVzKVxuICAgIHJldHVybiB0cnVlXG4gIH1cbiAgd2lsbFVwZGF0ZShjaGFuZ2VkUHJvcGVydGllczogUHJvcGVydHlWYWx1ZXM8dGhpcz4pe1xuICAgIGNvbnNvbGUubG9nKFwid2lsbFVwZGF0ZVwiLCBjaGFuZ2VkUHJvcGVydGllcylcbiAgfVxuICB1cGRhdGUoY2hhbmdlZFByb3BlcnRpZXM6IFByb3BlcnR5VmFsdWVzPHRoaXM-KXtcbiAgICBzdXBlci51cGRhdGUoY2hhbmdlZFByb3BlcnRpZXMpXG4gICAgY29uc29sZS5sb2coXCJ1cGRhdGVcIiwgY2hhbmdlZFByb3BlcnRpZXMpXG4gIH1cbiAgZmlyc3RVcGRhdGVkKGNoYW5nZWRQcm9wZXJ0aWVzOiBQcm9wZXJ0eVZhbHVlczx0aGlzPil7XG4gICAgY29uc29sZS5sb2coXCJmaXJzdFVwZGF0ZWRcIiwgY2hhbmdlZFByb3BlcnRpZXMpXG4gIH1cbiAgdXBkYXRlZChjaGFuZ2VkUHJvcGVydGllczogUHJvcGVydHlWYWx1ZXM8dGhpcz4pe1xuICAgIGNvbnNvbGUubG9nKFwidXBkYXRlZFwiLCBjaGFuZ2VkUHJvcGVydGllcylcbiAgfVxuICBcbiAgQHByb3BlcnR5KHtcbiAgaGFzQ2hhbmdlZChuZXdWYWw6IHN0cmluZywgb2xkVmFsOiBzdHJpbmcpIHtcbiAgICBjb25zb2xlLmxvZyhcImhhc0NoYW5nZWRcIiwgbmV3VmFsLCBvbGRWYWwpXG4gICAgcmV0dXJuIG5ld1ZhbCAhPT0gb2xkVmFsIFxuICB9XG59KVxuICBtc2cgPSBcIm1zZ1wiXG5cbiAgcmVuZGVyKCkge1xuICAgIGNvbnNvbGUubG9nKFwicmVuZGVyXCIpXG4gICAgdGhpcy51cGRhdGVDb21wbGV0ZS50aGVuKCgpPT5jb25zb2xlLmxvZyhcInVwZGF0ZUNvbXBsZXRlIHJlc29sdmVkXCIpKVxuICAgIHJldHVybiBodG1sYFxuICAgICAgICA8cCAke3JlZigoKT0-Y29uc29sZS5sb2coXCJyZWZcIikpfT4ke3RoaXMubXNnfTwvcD5cbiAgICAgICAgPGlucHV0IC52YWx1ZT0ke3RoaXMubXNnfSBAaW5wdXQ9JHtlPT50aGlzLm1zZz1lLnRhcmdldC52YWx1ZX0vPmA7XG4gIH1cbn1cbiJ9XQ\">此示例</a>\n演示代码如下：</p>\n</blockquote>\n<pre><code class=\"language-ts\">import { html, LitElement, PropertyValues } from &quot;lit&quot;;\nimport { customElement, property } from &quot;lit/decorators.js&quot;;\nimport { ref } from &quot;lit/directives/ref.js&quot;;\n\n// https://lit.dev/docs/components/lifecycle/\n@customElement(&quot;life-cycle&quot;)\nexport class LifeCycle extends LitElement {\n    // Standard custom element lifecycle\n    constructor() {\n        super();\n        console.log(&quot;constructor&quot;);\n    }\n    connectedCallback() {\n        super.connectedCallback();\n        console.log(&quot;connectedCallback&quot;);\n    }\n    disconnectedCallback() {\n        super.disconnectedCallback();\n        console.log(&quot;disconnectedCallback&quot;);\n    }\n    attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {\n        super.attributeChangedCallback(name, oldVal, newVal);\n        console.log(&quot;attributeChangedCallback&quot;, name, oldVal, newVal);\n    }\n    adoptedCallback() {\n        console.log(&quot;adoptedCallback&quot;);\n    }\n\n    // Reactive update cycle\n    // note that we call super only when necessary\n    shouldUpdate(changedProperties: PropertyValues&lt;this&gt;) {\n        console.log(&quot;shouldUpdate&quot;, changedProperties);\n        return true;\n    }\n    willUpdate(changedProperties: PropertyValues&lt;this&gt;) {\n        console.log(&quot;willUpdate&quot;, changedProperties);\n    }\n    update(changedProperties: PropertyValues&lt;this&gt;) {\n        super.update(changedProperties);\n        console.log(&quot;update&quot;, changedProperties);\n    }\n    firstUpdated(changedProperties: PropertyValues&lt;this&gt;) {\n        console.log(&quot;firstUpdated&quot;, changedProperties);\n    }\n    updated(changedProperties: PropertyValues&lt;this&gt;) {\n        console.log(&quot;updated&quot;, changedProperties);\n    }\n\n    @property({\n        hasChanged(newVal: string, oldVal: string) {\n            console.log(&quot;hasChanged&quot;, newVal, oldVal);\n            return newVal !== oldVal;\n        },\n    })\n    msg = &quot;msg&quot;;\n\n    render() {\n        console.log(&quot;render&quot;);\n        this.updateComplete.then(() =&gt; console.log(&quot;updateComplete resolved&quot;));\n        return html` &lt;p ${ref(() =&gt; console.log(&quot;ref&quot;))}&gt;${this.msg}&lt;/p&gt;\n            &lt;input .value=${this.msg} @input=${(e) =&gt; (this.msg = e.target.value)} /&gt;`;\n    }\n}\n</code></pre>\n<h1><a href=\"https://lit.dev/docs/components/shadow-dom/\">访问 Shadow DOM</a></h1>\n<p>在 lit 中，shadowRoot 可以通过组件的 renderRoot 属性访问</p>\n<pre><code class=\"language-ts\">// 可以在生命周期中访问\nfirstUpdated() {\n    this.staticNode = this.renderRoot.querySelector(&#39;#static-node&#39;);\n}\n\n// 可以作为getter访问器\nget _closeButton() {\n    return this.renderRoot.querySelector(&#39;#close-button&#39;);\n}\n</code></pre>\n<p>不过 lit 也提供了装饰器</p>\n<pre><code class=\"language-ts\">import { LitElement, html } from &quot;lit&quot;;\nimport { query, queryAll } from &quot;lit/decorators/query.js&quot;;\n\nclass MyElement extends LitElement {\n    @query(&quot;#first&quot;)\n    _first;\n    // 相当于\n    get _first() {\n        return this.renderRoot?.querySelector(&quot;#first&quot;) ?? null;\n    }\n\n    @queryAll(&quot;button&quot;)\n    _buttons!: NodeListOf&lt;HTMLButtonElement&gt;;\n\n    render() {\n        return html`\n            &lt;div id=&quot;first&quot;&gt;&lt;/div&gt;\n            &lt;div id=&quot;second&quot;&gt;&lt;/div&gt;\n        `;\n    }\n}\n</code></pre>\n<p>lit 内置装饰器参见<a href=\"https://lit.dev/docs/components/decorators/#built-in-decorators\">此处</a></p>\n<h1><a href=\"https://lit.dev/docs/components/events/\">事件</a></h1>\n<p>事件监听参数实际上就是传入 <code>addEventListener()</code> 的参数</p>\n<pre><code class=\"language-ts\">render() {\n  return html`&lt;button @click=${{handleEvent: () =&gt; this.onClick(), once: true}}&gt;click&lt;/button&gt;`\n}\n</code></pre>\n<p>使用组件实例的 dispatchEvent() 方法传入 <a href=\"https://developer.mozilla.org/docs/Web/API/CustomEvent/CustomEvent\">CustomEvent</a> 调度事件</p>\n<p>当自定义事件同时指定 bubbles 和 <a href=\"https://developer.mozilla.org/docs/Web/API/Event/composed\">composed</a> 时，该事件可冒泡至 ShadowDOM 外部</p>\n<pre><code class=\"language-ts\">this.dispatchEvent(\n    new CustomEvent(&quot;my-event&quot;, {\n        detail: { message: &quot;my-event happened.&quot; },\n        bubbles: true,\n        composed: true,\n        cancelable: true,\n    }),\n);\n</code></pre>\n<p>插槽中的事件根据实际渲染 DOM 节点的位置传递</p>\n<h1>静态表达式</h1>\n<p>动态标签和动态标签属性是不支持的</p>\n<pre><code class=\"language-ts\">import { LitElement } from &quot;lit&quot;;\nimport { customElement, property } from &quot;lit/decorators.js&quot;;\nimport { html, literal } from &quot;lit/static-html.js&quot;;\n\n@customElement(&quot;my-button&quot;)\nclass MyButton extends LitElement {\n    // tag = &quot;button&quot; 是错误的，必须以下面的方式声明\n    tag = literal`button`;\n    activeAttribute = literal`active`;\n    @property() caption = &quot;Hello static&quot;;\n    @property({ type: Boolean }) active = false;\n\n    render() {\n        return html`\n      &lt;${this.tag} ${this.activeAttribute}?=${this.active}&gt;\n        &lt;p&gt;${this.caption}&lt;/p&gt;\n      &lt;/${this.tag}&gt;`;\n    }\n}\n\n@customElement(&quot;my-anchor&quot;)\nclass MyAnchor extends MyButton {\n    tag = literal`a`;\n}\n</code></pre>\n<p>textarea 标签，在渲染动态内容时，一般使用对象属性<br>渲染静态内容时则无所谓</p>\n<pre><code class=\"language-html\">&lt;!--这样会丢失引用--&gt;\n&lt;textarea&gt;${content}&lt;/textarea&gt;\n\n&lt;!--使用对象属性可保持引用--&gt;\n&lt;textarea .value=&quot;${content}&quot;&gt;&lt;/textarea&gt;\n</code></pre>\n<h1>条件</h1>\n<p>when 指令</p>\n<pre><code class=\"language-js\">html`\n    ${when(\n        this.user, // 转化为布尔值判断\n        () =&gt; html`User: ${this.user.username}`,\n        () =&gt; html`Sign In...`,\n    )}\n`;\n</code></pre>\n<p>choose 指令</p>\n<pre><code class=\"language-js\">html`\n    ${choose(\n        this.section, // &quot;home&quot; or &quot;about&quot; or OTHER\n        [\n            [&quot;home&quot;, () =&gt; html`&lt;h1&gt;Home&lt;/h1&gt;`],\n            [&quot;about&quot;, () =&gt; html`&lt;h1&gt;About&lt;/h1&gt;`],\n        ],\n        () =&gt; html`&lt;h1&gt;Error&lt;/h1&gt;`, // if is OTHER\n    )}\n`;\n</code></pre>\n<p>ifDefined 指令</p>\n<pre><code class=\"language-ts\">@customElement(&quot;my-element&quot;)\nclass MyElement extends LitElement {\n    @property()\n    filename: string | undefined = undefined;\n\n    @property()\n    size: string | undefined = undefined;\n\n    render() {\n        // src attribute not rendered if either size or filename are undefined\n        return html`&lt;img src=&quot;/images/${ifDefined(this.size)}/${ifDefined(this.filename)}&quot; /&gt;`;\n    }\n}\n</code></pre>\n<p>cache 指令</p>\n<pre><code class=\"language-js\">html`${cache(this.userName ? html`Welcome ${this.userName}` : html`Please log in &lt;button&gt;Login&lt;/button&gt;`)}`;\n</code></pre>\n<p>nothing（Symbol）</p>\n<pre><code class=\"language-js\">html`&lt;user-name&gt;${this.userName ?? nothing}&lt;/user-name&gt;`;\n\nhtml`&lt;button aria-label=&quot;${this.ariaLabel || nothing}&quot;&gt;&lt;/button&gt;`;\n</code></pre>\n<h1>循环</h1>\n<p>repeat 指令</p>\n<pre><code class=\"language-ts\">@customElement(&quot;my-element&quot;)\nclass MyElement extends LitElement {\n    @property()\n    items: Array&lt;{ id: number; name: string }&gt; = [];\n\n    render() {\n        return html`\n            &lt;ul&gt;\n                ${repeat(\n                    this.items,\n                    (item) =&gt; item.id,\n                    (item, index) =&gt; html` &lt;li&gt;${index}: ${item.name}&lt;/li&gt;`,\n                )}\n            &lt;/ul&gt;\n        `;\n    }\n}\n</code></pre>\n<p>map 指令</p>\n<pre><code class=\"language-js\">html`&lt;ul&gt;\n    ${map(items, (i) =&gt; html`&lt;li&gt;${i}&lt;/li&gt;`)}\n&lt;/ul&gt;`;\n</code></pre>\n<p>range 指令</p>\n<pre><code class=\"language-js\">html`${map(range(8), (i) =&gt; html`${i + 1}`)}`;\n</code></pre>\n<h1>ref</h1>\n<p>传入 Ref 对象</p>\n<pre><code class=\"language-ts\">@customElement(&quot;my-element&quot;)\nclass MyElement extends LitElement {\n    inputRef: Ref&lt;HTMLInputElement&gt; = createRef();\n\n    render() {\n        // 将 Ref 对象传入ref指令后，Ref 对象的 value 属性将是对元素的引用\n        return html`&lt;input ${ref(this.inputRef)} /&gt;`;\n    }\n\n    firstUpdated() {\n        const input = this.inputRef.value!;\n        input.focus();\n    }\n}\n</code></pre>\n<p>传入回调函数</p>\n<pre><code class=\"language-ts\">@customElement(&quot;my-element&quot;)\nclass MyElement extends LitElement {\n    render() {\n        // 将回调函数传入 ref 指令，将在被引用的元素变更时调用\n        return html`&lt;input ${ref(this.inputChanged)} /&gt;`;\n    }\n\n    inputChanged(input?: HTMLInputElement) {\n        input?.focus();\n    }\n}\n</code></pre>\n<h1>其它内置指令</h1>\n<p><a href=\"https://lit.dev/docs/templates/directives/\">https://lit.dev/docs/templates/directives/</a></p>\n<h1>自定义指令</h1>\n<p><a href=\"https://lit.dev/docs/templates/custom-directives/\">https://lit.dev/docs/templates/custom-directives/</a></p>\n<h1>其它</h1>\n<p>lit 是基于类实现的，因此有一些设计很像 flutter</p>\n<p>Controllers：<a href=\"https://lit.dev/docs/composition/controllers/\">https://lit.dev/docs/composition/controllers/</a><br>Context：<a href=\"https://lit.dev/docs/data/context/\">https://lit.dev/docs/data/context/</a><br>AsyncTasks：<a href=\"https://lit.dev/docs/data/task/\">https://lit.dev/docs/data/task/</a></p>\n","tags":["lib","lit","js"]},{"id":"css-tips-translation","url":"https://yieldray.fun/posts/css-tips-translation","title":"几个css小技巧","date_published":"2022-11-19T19:19:19.000Z","date_modified":"2022-11-19T19:19:19.000Z","content_text":"<blockquote>\n<p>翻译自：<a href=\"https://markodenic.com/css-tips/\">https://markodenic.com/css-tips/</a>，部分省略</p>\n</blockquote>\n<h1>打字特效</h1>\n<p>原理参见：<a href=\"https://www.zhangxinxu.com/wordpress/2019/01/css-typewriter-effect/\">https://www.zhangxinxu.com/wordpress/2019/01/css-typewriter-effect/</a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"CSS Typing Effect\" src=\"https://codepen.io/YieldRay/embed/VwdrJJa?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/VwdrJJa\">\n  CSS Typing Effect</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>平滑滚动</h1>\n<p>本例核心为</p>\n<pre><code class=\"language-css\">html {\n    scroll-behavior: smooth;\n}\n</code></pre>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"CSS Smooth Scroll\" src=\"https://codepen.io/denic/embed/bGVeYqN?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\nSee the Pen <a href=\"https://codepen.io/denic/pen/bGVeYqN\">\nCSS Smooth Scroll</a> by Marko (<a href=\"https://codepen.io/denic\">@denic</a>)\non <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p><a href=\"https://www.zhangxinxu.com/wordpress/2018/10/scroll-behavior-scrollintoview-%e5%b9%b3%e6%bb%91%e6%bb%9a%e5%8a%a8/\">https://www.zhangxinxu.com/wordpress/2018/10/scroll-behavior-scrollintoview-%e5%b9%b3%e6%bb%91%e6%bb%9a%e5%8a%a8/</a></p>\n<h1>文本截断</h1>\n<p>核心在于</p>\n<pre><code class=\"language-css\">.overflow-ellipsis {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n</code></pre>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/text-overflow\">https://developer.mozilla.org/docs/Web/CSS/text-overflow</a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"Truncate the text with CSS only\" src=\"https://codepen.io/denic/embed/LYpZKMg?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/denic/pen/LYpZKMg\">\n  Truncate the text with CSS only</a> by Marko (<a href=\"https://codepen.io/denic\">@denic</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>目前仅 webkit 内核支持 line-clamp 属性，可以把 <strong>块容器</strong> 中的内容限制为指定的行数。</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/-webkit-line-clamp\">https://developer.mozilla.org/docs/Web/CSS/-webkit-line-clamp</a></p>\n<pre><code class=\"language-css\">.line-clamp {\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    /* 上面的两个属性必须指定，否则-webkit-line-clamp无效 */\n    -webkit-line-clamp: 3; /* 裁剪为3行 */\n    overflow: hidden;\n}\n</code></pre>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"Truncate the text to the specific number of lines (CSS)\" src=\"https://codepen.io/denic/embed/pojEKGX?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/denic/pen/pojEKGX\">\n  Truncate the text to the specific number of lines (CSS)</a> by Marko (<a href=\"https://codepen.io/denic\">@denic</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1><code>::selection</code> 伪元素</h1>\n<p><code>::selection</code> 伪元素应用于文档中被用户高亮的部分（比如使用鼠标或其他选择设备选中的部分）</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/::selection\">https://developer.mozilla.org/docs/Web/CSS/::selection</a></p>\n<pre><code class=\"language-css\">.custom-highlighting::selection {\n    /* 只有一小部分 CSS 属性可以用于::selection 选择器，具体请参见 MDN */\n}\n</code></pre>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"::selection pseudo-element\" src=\"https://codepen.io/denic/embed/LYZZQJe?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/denic/pen/LYZZQJe\">\n  ::selection pseudo-element</a> by Marko (<a href=\"https://codepen.io/denic\">@denic</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>纯 CSS 模态窗</h1>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/:target\">https://developer.mozilla.org/docs/Web/CSS/:target</a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"CSS-only modal\" src=\"https://codepen.io/YieldRay/embed/YzvEmEQ?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/YzvEmEQ\">\n  CSS-only modal</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>灵活使用<code>:target</code>伪类，可以实现诸如显示/隐藏屏幕侧边栏的效果<br>但由于会修改 URL 锚点，在某些情况下（如 hashRouter）就无法使用了</p>\n<h1><code>:empty</code> 伪类</h1>\n<p><code>:empty</code> 伪类在样式化占位内容时很实用</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/:empty\">https://developer.mozilla.org/docs/Web/CSS/:empty</a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"CSS :empty Selector\" src=\"https://codepen.io/denic/embed/KKMpZdP?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/denic/pen/KKMpZdP\">\n  CSS :empty Selector</a> by Marko (<a href=\"https://codepen.io/denic\">@denic</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>Scroll Snap</h1>\n<p>参见：<br><a href=\"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-type\">scroll-snap-type</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-align\">scroll-snap-align</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-stop\">scroll-snap-stop</a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"CSS Scroll Snap\" src=\"https://codepen.io/denic/embed/ExNZmwd?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/denic/pen/ExNZmwd\">\n  CSS Scroll Snap</a> by Marko (<a href=\"https://codepen.io/denic\">@denic</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p><a href=\"https://www.zhangxinxu.com/wordpress/2018/11/know-css-scroll-snap/\">https://www.zhangxinxu.com/wordpress/2018/11/know-css-scroll-snap/</a></p>\n<h1>光标颜色</h1>\n<p>caret-color 属性用来定义插入光标（caret）的颜色，这里说的插入光标，就是那个在网页的可编辑器区域内，用来指示用户的输入具体会插入到哪里的那个一闪一闪的形似竖杠 <code>|</code> 的东西。</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/caret-color\">https://developer.mozilla.org/docs/Web/CSS/caret-color</a></p>\n<h1><code>:in-range</code> 和 <code>::out-of-range</code> 伪类</h1>\n<p>参见：<br><a href=\"https://developer.mozilla.org/docs/Web/CSS/:in-range\">:in-range</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/:out-of-range\">:out-of-range</a></p>\n<h1>自定义边框</h1>\n<p>核心在于</p>\n<pre><code class=\"language-css\">.gradient-border {\n    border: solid 5px transparent; /* 颜色需定义为透明 */\n    background-origin: border-box;\n    background-clip: content-box, border-box;\n}\n</code></pre>\n<p>参见：<br><a href=\"https://developer.mozilla.org/docs/Web/CSS/background-clip\">background-clip</a>\n<a href=\"https://developer.mozilla.org/docs/Web/CSS/background-origin\">background-origin</a></p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"Rounded Gradient Border\" src=\"https://codepen.io/denic/embed/jOLmJrM?default-tab=html%2Cresult&editable=true\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/denic/pen/jOLmJrM\">\n  Rounded Gradient Border</a> by Marko (<a href=\"https://codepen.io/denic\">@denic</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>下面的是原文没有的</h1>\n<h1>aspect-ratio</h1>\n<p>参见：<a href=\"https://www.zhangxinxu.com/wordpress/2021/02/css-aspect-ratio/\">https://www.zhangxinxu.com/wordpress/2021/02/css-aspect-ratio/</a></p>\n<p>aspect-ratio：<a href=\"https://developer.mozilla.org/docs/Web/CSS/aspect-ratio\">https://developer.mozilla.org/docs/Web/CSS/aspect-ratio</a>\naspect-ratio 媒体查询：<a href=\"https://developer.mozilla.org/docs/Web/CSS/@media/aspect-ratio\">https://developer.mozilla.org/docs/Web/CSS/@media/aspect-ratio</a></p>\n<h1>backdrop-filter 滤镜</h1>\n<p>filter 是作用在元素本身上的滤镜，backdrop-filter 则作用在元素 z 轴后面的部分</p>\n<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/CSS/backdrop-filter\">https://developer.mozilla.org/docs/Web/CSS/backdrop-filter</a></p>\n<p>使用滤镜需考虑性能问题，可以通过 transform:translateZ(0) 和 will-change 开启 GPU 加速</p>\n<h1>clip-path</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/clip-path\">https://developer.mozilla.org/docs/Web/CSS/clip-path</a></p>\n<h1>mix-blend-mode/background-blend-mode</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/mix-blend-mode\">https://developer.mozilla.org/docs/Web/CSS/mix-blend-mode</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/background-blend-mode\">https://developer.mozilla.org/docs/Web/CSS/background-blend-mode</a><br><a href=\"https://www.zhangxinxu.com/wordpress/2015/05/css3-mix-blend-mode-background-blend-mode/\">https://www.zhangxinxu.com/wordpress/2015/05/css3-mix-blend-mode-background-blend-mode/</a></p>\n<h1>mask</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/mask\">https://developer.mozilla.org/docs/Web/CSS/mask</a><br><a href=\"https://www.zhangxinxu.com/wordpress/2017/11/css-css3-mask-masks/\">https://www.zhangxinxu.com/wordpress/2017/11/css-css3-mask-masks/</a></p>\n<h1>object-fit/object-position</h1>\n<p>object-fit 属性指定可替换元素的内容应该如何适应到其使用高度和宽度确定的框。</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/object-fit\">https://developer.mozilla.org/docs/Web/CSS/object-fit</a></p>\n<p>object-position 属性规定了可替换元素的内容，在这里我们称其为对象（即 object-position 中的 object），在其内容框中的位置。可替换元素的内容框中未被对象所覆盖的部分，则会显示该元素的背景（background）。</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/object-position\">https://developer.mozilla.org/docs/Web/CSS/object-position</a></p>\n<h1>offset</h1>\n<p>offset 属性可以使元素沿着不规则的路径移动</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/offset\">https://developer.mozilla.org/docs/Web/CSS/offset</a></p>\n<h1>overscroll-behavior</h1>\n<p>overscroll-behavior 指定滚动嵌套时的表现</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior\">https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior</a><br><a href=\"https://www.zhangxinxu.com/wordpress/2020/01/css-overscroll-behavior/\">https://www.zhangxinxu.com/wordpress/2020/01/css-overscroll-behavior/</a></p>\n<h1>rotate/scale/translate</h1>\n<p>这些属性可以单独声明 transform 变换（独立于 transform 属性），避免了 transform 属性的使用</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/rotate\">https://developer.mozilla.org/docs/Web/CSS/rotate</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/scale\">https://developer.mozilla.org/docs/Web/CSS/scale</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/translate\">https://developer.mozilla.org/docs/Web/CSS/translate</a></p>\n<h1>gap/column-gap/row-gap</h1>\n<p>可以单独设置行或列的间距</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/column-gap\">https://developer.mozilla.org/docs/Web/CSS/column-gap</a><br><a href=\"https://developer.mozilla.org/docs/Web/CSS/row-gap\">https://developer.mozilla.org/docs/Web/CSS/row-gap</a></p>\n<h1>shape-outside/shape-margin</h1>\n<p>shape-outside 可以指定一种形状，相邻的行内（inline）元素围绕该形状换行</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/shape-outside\">https://developer.mozilla.org/docs/Web/CSS/shape-outside</a></p>\n<p>shape-margin 属性可以设定 shape-outside 创建的 CSS 形状的外边距</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/shape-margin\">https://developer.mozilla.org/docs/Web/CSS/shape-margin</a></p>\n<h1>user-select</h1>\n<p>user-select 属性控制用户选中文本的行为</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/user-select\">https://developer.mozilla.org/docs/Web/CSS/user-select</a></p>\n<h1>vertical-align</h1>\n<p>vertical-align 属性用于指定行内元素（inline）或表格单元格（table-cell）元素的垂直对齐方式</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/vertical-align\">https://developer.mozilla.org/docs/Web/CSS/vertical-align</a></p>\n","tags":["css"]},{"id":"makefile-intro","url":"https://yieldray.fun/posts/makefile-intro","title":"makefile-intro","date_published":"2022-11-16T16:00:00.000Z","date_modified":"2022-11-16T16:00:00.000Z","content_text":"<p>参考<br><a href=\"https://github.com/seisman/how-to-write-makefile\">https://github.com/seisman/how-to-write-makefile</a><br><a href=\"https://makefiletutorial.com/\">https://makefiletutorial.com/</a></p>\n","tags":["linux"]},{"id":"js-iteration","url":"https://yieldray.fun/posts/js-iteration","title":"js迭代协议","date_published":"2022-11-15T15:15:15.000Z","date_modified":"2022-11-15T15:15:15.000Z","content_text":"<h1><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Iteration_protocols\">迭代协议</a></h1>\n<p>迭代协议分为 可迭代协议 和 迭代器协议。</p>\n<blockquote>\n<p>注：在下文中我们会将实现 可迭代协议 的对象称为 可迭代对象，将实现了 迭代器协议 的对象称为 迭代器对象（简称：迭代器）。</p>\n</blockquote>\n<p>可以使用 可迭代对象 的地方有： <code>for...of</code> 循环、<code>...</code> （spread 运算符）、<code>yield*</code> 语法 和 数组解构。<br>而 可迭代对象 的功能实际上是由 迭代器 来提供的。<br>因此，迭代器对象 无法脱离 迭代器，而 迭代器 可以离开 可迭代对象 使用。</p>\n<pre><code class=\"language-ts\">type Value = number; // 仅为了标识被迭代的泛型\n\nconst example1: Iterable&lt;Value&gt; = {\n    // 可迭代对象 的 Symbol.iterator 属性是返回 迭代器对象 的函数\n    // （对于 ts 而言就是实现 Iterable 接口）\n    [Symbol.iterator]: () =&gt; {\n        const iterator: Iterator&lt;Value&gt; = {\n            next() {\n                const result: IteratorResult&lt;Value&gt; = { value: 123, done: true };\n                return result;\n            },\n        };\n        return iterator;\n    },\n};\n\n// IterableIterator 是 ts 提供的实用接口，它把 可迭代对象 和 迭代器对象 结合成一个对象\nconst example2: IterableIterator&lt;Value&gt; = {\n    next() {\n        return { value: 123, done: true };\n    },\n    [Symbol.iterator]() {\n        return this;\n    },\n};\n\n// 生成器返回 Generator 对象，它是可迭代的迭代器\nconst viaGenerator: Iterable&lt;Value&gt; = {\n    [Symbol.iterator]: function* () {\n        yield 123;\n        yield 456;\n        yield 789;\n    },\n};\n</code></pre>\n<pre><code class=\"language-ts\">// lib.es2015.iterable.d.ts\n\ninterface IteratorYieldResult&lt;TYield&gt; {\n    done?: false;\n    value: TYield;\n}\n\ninterface IteratorReturnResult&lt;TReturn&gt; {\n    done: true;\n    value: TReturn;\n}\n\ntype IteratorResult&lt;T, TReturn = any&gt; = IteratorYieldResult&lt;T&gt; | IteratorReturnResult&lt;TReturn&gt;;\n\ninterface Iterator&lt;T, TReturn = any, TNext = undefined&gt; {\n    // NOTE: &#39;next&#39; is defined using a tuple to ensure we report the correct assignability errors in all places.\n    next(...args: [] | [TNext]): IteratorResult&lt;T, TReturn&gt;;\n    return?(value?: TReturn): IteratorResult&lt;T, TReturn&gt;;\n    throw?(e?: any): IteratorResult&lt;T, TReturn&gt;;\n}\n\ninterface Iterable&lt;T&gt; {\n    [Symbol.iterator](): Iterator&lt;T&gt;;\n}\n\ninterface IterableIterator&lt;T&gt; extends Iterator&lt;T&gt; {\n    [Symbol.iterator](): IterableIterator&lt;T&gt;;\n}\n</code></pre>\n<h1>生成器</h1>\n<p>生成器函数返回一个实现 <code>Generator</code> 接口的对象<br><code>Generator</code> 继承 <code>Iterator</code>，但仔细看可以发现 <code>Generator</code> 其实完全相当与 <code>Required&lt;IterableIterator&gt;</code><br>也就是说生成器函数返回的对象同时是 可迭代对象 和 迭代器对象</p>\n<p>使用生成器可以参见此处：<a href=\"https://zh.javascript.info/generators\">https://zh.javascript.info/generators</a></p>\n<pre><code class=\"language-ts\">// lib.es2015.generator.d.ts\n\ninterface Generator&lt;T = unknown, TReturn = any, TNext = unknown&gt; extends Iterator&lt;T, TReturn, TNext&gt; {\n    // NOTE: &#39;next&#39; is defined using a tuple to ensure we report the correct assignability errors in all places.\n    next(...args: [] | [TNext]): IteratorResult&lt;T, TReturn&gt;;\n    return(value: TReturn): IteratorResult&lt;T, TReturn&gt;;\n    throw(e: any): IteratorResult&lt;T, TReturn&gt;;\n    [Symbol.iterator](): Generator&lt;T, TReturn, TNext&gt;;\n}\n\ninterface GeneratorFunction {\n    /**\n     * Creates a new Generator object.\n     * @param args A list of arguments the function accepts.\n     */\n    new (...args: any[]): Generator;\n    /**\n     * Creates a new Generator object.\n     * @param args A list of arguments the function accepts.\n     */\n    (...args: any[]): Generator;\n    /**\n     * The length of the arguments.\n     */\n    readonly length: number;\n    /**\n     * Returns the name of the function.\n     */\n    readonly name: string;\n    /**\n     * A reference to the prototype.\n     */\n    readonly prototype: Generator;\n}\n\ninterface GeneratorFunctionConstructor {\n    /**\n     * Creates a new Generator function.\n     * @param args A list of arguments the function accepts.\n     */\n    new (...args: string[]): GeneratorFunction;\n    /**\n     * Creates a new Generator function.\n     * @param args A list of arguments the function accepts.\n     */\n    (...args: string[]): GeneratorFunction;\n    /**\n     * The length of the arguments.\n     */\n    readonly length: number;\n    /**\n     * Returns the name of the function.\n     */\n    readonly name: string;\n    /**\n     * A reference to the prototype.\n     */\n    readonly prototype: GeneratorFunction;\n}\n</code></pre>\n<p><code>GeneratorFunctionConstructor</code> 接口并没有什么实用意义，其对应 <code>FunctionConstructor</code><br><code>FunctionConstructor</code> 接口 被 <code>Function</code> 构造函数实现， 定义如下</p>\n<pre><code class=\"language-ts\">interface FunctionConstructor {\n    /**\n     * Creates a new function.\n     * @param args A list of arguments the function accepts.\n     */\n    new (...args: string[]): Function;\n    (...args: string[]): Function;\n    readonly prototype: Function;\n}\n\ndeclare var Function: FunctionConstructor;\n</code></pre>\n<p>注：通过原型获取生成器函数构造器</p>\n<pre><code class=\"language-js\">const GeneratorFunction = Object.getPrototypeOf(function* () {}).constructor;\n</code></pre>\n<h1>异步迭代器和异步可迭代协议</h1>\n<p>异步迭代器只不过是支持异步的迭代器，但注意同时实现 迭代协议 与 异步迭代协议 显然是不冲突的。</p>\n<pre><code class=\"language-ts\">type Value = number; // 仅为了标识被迭代的泛型\n\nconst example3: AsyncIterable&lt;Value&gt; = {\n    [Symbol.asyncIterator]: () =&gt; {\n        const asyncIterator: AsyncIterator&lt;Value&gt; = {\n            // 不同之处就在于这里的 async\n            async next() {\n                const result: IteratorResult&lt;Value&gt; = { value: 123, done: true };\n                return result;\n            },\n        };\n        return asyncIterator;\n    },\n};\n\nconst example4: AsyncIterableIterator&lt;Value&gt; = {\n    async next() {\n        return { value: 123, done: true };\n    },\n    [Symbol.asyncIterator]() {\n        return this;\n    },\n};\n\nconst viaAsyncGenerator: AsyncIterable&lt;Value&gt; = {\n    [Symbol.asyncIterator]: async function* () {\n        yield 123;\n        yield 456;\n        yield 789;\n    },\n};\n</code></pre>\n<pre><code class=\"language-ts\">// lib.es2018.asynciterable.d.ts\n\ninterface AsyncIterator&lt;T, TReturn = any, TNext = undefined&gt; {\n    // NOTE: &#39;next&#39; is defined using a tuple to ensure we report the correct assignability errors in all places.\n    next(...args: [] | [TNext]): Promise&lt;IteratorResult&lt;T, TReturn&gt;&gt;;\n    return?(value?: TReturn | PromiseLike&lt;TReturn&gt;): Promise&lt;IteratorResult&lt;T, TReturn&gt;&gt;;\n    throw?(e?: any): Promise&lt;IteratorResult&lt;T, TReturn&gt;&gt;;\n}\n\ninterface AsyncIterable&lt;T&gt; {\n    [Symbol.asyncIterator](): AsyncIterator&lt;T&gt;;\n}\n\ninterface AsyncIterableIterator&lt;T&gt; extends AsyncIterator&lt;T&gt; {\n    [Symbol.asyncIterator](): AsyncIterableIterator&lt;T&gt;;\n}\n</code></pre>\n<h1>异步生成器</h1>\n<pre><code class=\"language-ts\">// lib.es2018.asyncgenerator.d.ts\n\ninterface AsyncGenerator&lt;T = unknown, TReturn = any, TNext = unknown&gt; extends AsyncIterator&lt;T, TReturn, TNext&gt; {\n    // NOTE: &#39;next&#39; is defined using a tuple to ensure we report the correct assignability errors in all places.\n    next(...args: [] | [TNext]): Promise&lt;IteratorResult&lt;T, TReturn&gt;&gt;;\n    return(value: TReturn | PromiseLike&lt;TReturn&gt;): Promise&lt;IteratorResult&lt;T, TReturn&gt;&gt;;\n    throw(e: any): Promise&lt;IteratorResult&lt;T, TReturn&gt;&gt;;\n    [Symbol.asyncIterator](): AsyncGenerator&lt;T, TReturn, TNext&gt;;\n}\n\ninterface AsyncGeneratorFunction {\n    /**\n     * Creates a new AsyncGenerator object.\n     * @param args A list of arguments the function accepts.\n     */\n    new (...args: any[]): AsyncGenerator;\n    /**\n     * Creates a new AsyncGenerator object.\n     * @param args A list of arguments the function accepts.\n     */\n    (...args: any[]): AsyncGenerator;\n    /**\n     * The length of the arguments.\n     */\n    readonly length: number;\n    /**\n     * Returns the name of the function.\n     */\n    readonly name: string;\n    /**\n     * A reference to the prototype.\n     */\n    readonly prototype: AsyncGenerator;\n}\n\ninterface AsyncGeneratorFunctionConstructor {\n    /**\n     * Creates a new AsyncGenerator function.\n     * @param args A list of arguments the function accepts.\n     */\n    new (...args: string[]): AsyncGeneratorFunction;\n    /**\n     * Creates a new AsyncGenerator function.\n     * @param args A list of arguments the function accepts.\n     */\n    (...args: string[]): AsyncGeneratorFunction;\n    /**\n     * The length of the arguments.\n     */\n    readonly length: number;\n    /**\n     * Returns the name of the function.\n     */\n    readonly name: string;\n    /**\n     * A reference to the prototype.\n     */\n    readonly prototype: AsyncGeneratorFunction;\n}\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for...of\"><code>for...of</code></a> <a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for-await...of\"><code>for await..of</code></a></h1>\n<pre><code class=\"language-js\">for (const value of example1) console.log(value);\n// =&gt; 123\n\nfor await (const value of example3) console.log(value);\n// =&gt; 123\n</code></pre>\n<p>注意，<code>for await</code> 也可用于迭代普通迭代器 （反之则不行）</p>\n<pre><code class=\"language-js\">for await (var value of example1) console.log(value);\n\n// 就像 await 也可用于任意值一样\nconsole.log(await &quot;test&quot;);\n</code></pre>\n","tags":["js"]},{"id":"ts-decorator","url":"https://yieldray.fun/posts/ts-decorator","title":"TypeScript装饰器(stage 2)","date_published":"2022-11-14T17:28:56.000Z","date_modified":"2022-11-14T17:28:56.000Z","content_text":"<p><strong>注意</strong>：本篇为 experimental stage 2 装饰器，需要在 tsconfig.json 启用 <a href=\"https://www.typescriptlang.org/tsconfig#experimentalDecorators\">experimentalDecorators</a> 选项</p>\n<pre><code class=\"language-json\">{\n    &quot;compileOptions&quot;: {\n        &quot;experimentalDecorators&quot;: true\n    }\n}\n</code></pre>\n<p>stage 3 装饰器自 typescript 5.0 无需配置直接支持，此时启用 experimentalDecorators 选项将导致使用 stage 2 装饰器</p>\n<p>参见:<br><a href=\"https://www.typescriptlang.org/docs/handbook/decorators.html\">https://www.typescriptlang.org/docs/handbook/decorators.html</a><br><a href=\"https://mirone.me/zh-hans/a-complete-guide-to-typescript-decorator/\">https://mirone.me/zh-hans/a-complete-guide-to-typescript-decorator/</a><br><a href=\"https://bosens-china.github.io/Typescript-manual/download/zh/reference/decorators.html\">https://bosens-china.github.io/Typescript-manual/download/zh/reference/decorators.html</a><br>Playground:<br><a href=\"https://www.typescriptlang.org/zh/play\">https://www.typescriptlang.org/zh/play</a></p>\n<h1>定义</h1>\n<p>装饰器是函数。<br>一共有四种这样的函数，用于修饰不同的目标类型</p>\n<p><a href=\"https://github.com/microsoft/TypeScript/blob/main/src/lib/decorators.legacy.d.ts\">lib.decorators.legacy.d.ts</a></p>\n<pre><code class=\"language-ts\">interface TypedPropertyDescriptor&lt;T&gt; {\n    enumerable?: boolean;\n    configurable?: boolean;\n    writable?: boolean;\n    value?: T;\n    get?: () =&gt; T;\n    set?: (value: T) =&gt; void;\n}\n\n// 类装饰器\ndeclare type ClassDecorator = &lt;TFunction extends Function&gt;(target: TFunction) =&gt; TFunction | void;\n\n// 属性装饰器\ndeclare type PropertyDecorator = (target: Object, propertyKey: string | symbol) =&gt; void;\n\n// 方法装饰器 &amp; 访问器装饰器\ndeclare type MethodDecorator = &lt;T&gt;(\n    target: Object,\n    propertyKey: string | symbol,\n    descriptor: TypedPropertyDescriptor&lt;T&gt;,\n) =&gt; TypedPropertyDescriptor&lt;T&gt; | void;\n\n// 参数装饰器\ndeclare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) =&gt; void;\n</code></pre>\n<h1>类装饰器</h1>\n<p><em>JavaScript 中的类就是构造函数</em></p>\n<p>下面的装饰器作用于构造函数（但不修改）。很明显，装饰器函数在构造函数声明后被调用。</p>\n<pre><code class=\"language-ts\">function sealed(constructor: Function) {\n    Object.seal(constructor);\n    Object.seal(constructor.prototype);\n}\n\n@sealed\nclass Greeter {\n    greeting: string;\n    constructor(message: string) {\n        this.greeting = message;\n    }\n    greet() {\n        return &quot;Hello, &quot; + this.greeting;\n    }\n}\n</code></pre>\n<p>可以看作是</p>\n<pre><code class=\"language-js\">const Greeter = (() =&gt; {\n    // prettier-ignore\n    class Greeter {/* 略 */}\n    return sealed(_Greeter) ?? Greeter;\n})();\n</code></pre>\n<p>类装饰器若返回一个类，则声明的构造函数被替换</p>\n<pre><code class=\"language-ts\">function classDecorator&lt;T extends { new (...args: any[]): {} }&gt;(constructor: T) {\n    return class extends constructor {\n        newProperty = &quot;new property&quot;;\n        hello = &quot;override&quot;;\n    };\n}\n\n@classDecorator\nclass Greeter {\n    property = &quot;property&quot;;\n    hello: string;\n    constructor(m: string) {\n        this.hello = m;\n    }\n}\n</code></pre>\n<h1>装饰器工厂</h1>\n<p>装饰器工厂是返回装饰器的函数，据此实现带参装饰器</p>\n<h1>方法装饰器/访问器装饰器</h1>\n<p>方法装饰器/访问器装饰器 有三个参数：</p>\n<p>1.对于静态成员来说是类的构造函数，对于实例成员是类的原型对象<br>2.成员的名字<br>3.成员的<em>属性描述符</em></p>\n<p>如果 方法装饰器/访问器装饰器 返回一个值，它会被用作方法的<em>属性描述符</em>。</p>\n<p>注意，同名访问器的 getter 和 setter 只需在文档顺序的第一个上应用装饰器</p>\n<pre><code class=\"language-ts\">function enumerable(value: boolean) {\n    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n        descriptor.enumerable = value;\n    };\n}\n\nfunction configurable(value: boolean) {\n    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n        descriptor.configurable = value;\n    };\n}\n\n// 方法装饰器\nclass Greeter {\n    greeting: string;\n    constructor(message: string) {\n        this.greeting = message;\n    }\n\n    @enumerable(false)\n    greet() {\n        return &quot;Hello, &quot; + this.greeting;\n    }\n}\n\n// 访问器装饰器\nclass Point {\n    private _x: number;\n    private _y: number;\n    constructor(x: number, y: number) {\n        this._x = x;\n        this._y = y;\n    }\n\n    @configurable(false)\n    get x() {\n        return this._x;\n    }\n\n    @configurable(false)\n    get y() {\n        return this._y;\n    }\n}\n</code></pre>\n<h1>属性装饰器</h1>\n<blockquote>\n<p>注意<br><em>属性描述符</em>不会作为(第三个)参数传入属性装饰器，这与 TypeScript 是如何初始化属性装饰器的有关。<br>因为目前没有办法在定义一个原型对象的成员时描述一个实例属性，并且无法监视或修改一个属性的初始化方法。返回值也会被忽略。<br>因此，属性描述符只能用来监视类中是否声明了某个名字的属性。</p>\n</blockquote>\n<h1>参数装饰器</h1>\n<blockquote>\n<p>与属性装饰器相似，但第三个参数传入的是参数在函数参数列表中的索引</p>\n</blockquote>\n","tags":["typescript"]},{"id":"posix-ref","url":"https://yieldray.fun/posts/posix-ref","title":"POSIX参考","date_published":"2022-11-13T22:22:22.000Z","date_modified":"2022-11-13T22:22:22.000Z","content_text":"<p>规范<br><a href=\"https://pubs.opengroup.org/onlinepubs/9699919799/\">https://pubs.opengroup.org/onlinepubs/9699919799/</a><br>glibc 文档<br><a href=\"https://www.gnu.org/software/libc/manual/\">https://www.gnu.org/software/libc/manual/</a><br>man-pages<br><a href=\"https://www.kernel.org/doc/man-pages/\">https://www.kernel.org/doc/man-pages/</a><br>中文参考<br><a href=\"https://github.com/guodongxiaren/LinuxAPI/wiki\">https://github.com/guodongxiaren/LinuxAPI/wiki</a><br><a href=\"https://github.com/getiot/linux-c-functions\">https://github.com/getiot/linux-c-functions</a></p>\n<p>C/C++参考<br><a href=\"https://cplusplus.com/reference/\">https://cplusplus.com/reference/</a><br><a href=\"https://zh.cppreference.com/\">https://zh.cppreference.com/</a><br><a href=\"https://c-cpp.com/\">https://c-cpp.com/</a><br><a href=\"https://learn.microsoft.com/zh-cn/cpp/\">https://learn.microsoft.com/zh-cn/cpp/</a><br><a href=\"https://learn.microsoft.com/zh-cn/cpp/c-language/c-language-reference\">https://learn.microsoft.com/zh-cn/cpp/c-language/c-language-reference</a><br><a href=\"https://learn.microsoft.com/zh-cn/cpp/cpp/cpp-language-reference\">https://learn.microsoft.com/zh-cn/cpp/cpp/cpp-language-reference</a></p>\n","tags":["linux"]},{"id":"js-object","url":"https://yieldray.fun/posts/js-object","title":"js对象方法","date_published":"2022-11-12T22:22:22.000Z","date_modified":"2022-11-12T22:22:22.000Z","content_text":"<p><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object\">https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object</a></p>\n<h1>属性描述符</h1>\n<p>注意：在严格模式下，违反属性描述符会抛出 TypeError 异常\n在非严格模式下则是静默失败</p>\n<p>通过直接的赋值和属性初始化的属性，属性描述符都为 true</p>\n<pre><code class=\"language-ts\">interface PropertyDescriptor {\n    // 是否可配置，若设置为不可配置，则不能删除指定属性，且不能再修改属性描述符（除了将 writable: true 更改为 false）\n    // 务必注意这里的不可修改，就是不能再次定义已经定义的属性描述符，但如果本来未定义（默认行为）则可以修改\n    configurable?: boolean;\n    // 是否可枚举，如 for..in 循环\n    enumerable?: boolean;\n    // 是否可写\n    writable?: boolean;\n    // value和writeable 与 getter和setter 只能二选一，否则抛出TypeError\n    value?: any;\n    get?(): any;\n    set?(v: any): void;\n}\n\ninterface PropertyDescriptorMap {\n    [key: PropertyKey]: PropertyDescriptor;\n}\n</code></pre>\n<p>两个方法可以设置属性描述符</p>\n<pre><code class=\"language-js\">const obj = {};\n\nObject.defineProperty(obj, &quot;foo&quot;, {\n    enumerable: true,\n    get() {\n        return &quot;foo&quot;;\n    },\n});\n\nObject.defineProperty(obj, &quot;bar&quot;, {\n    enumerable: false,\n    writable: true,\n    value: &quot;bar&quot;,\n});\n\nObject.defineProperty(obj, &quot;bar&quot;, {\n    writable: false, // 例外\n    value: &quot;baz&quot;,\n});\n\nObject.defineProperty(obj, &quot;PI&quot;, {\n    configurable: false,\n    writable: false,\n    enumerable: true,\n    value: 3.14, // 相当于常量\n});\n\nfor (const [k, v] of Object.entries(obj)) console.log(&quot;%s=%s&quot;, k, v);\n// 输出\n// foo=foo\n// PI=3.14\n</code></pre>\n<p>对应的两个方法可以获取属性描述符</p>\n<pre><code class=\"language-js\">Object.getOwnPropertyDescriptor(obj, &quot;foo&quot;);\nObject.getOwnPropertyDescriptors(obj);\n</code></pre>\n<p>属性描述符在单个属性的级别上工作。</p>\n<p>还有一些限制访问 <strong>整个</strong> 对象的方法：</p>\n<pre><code class=\"language-js\">// 阻止在对象上添加新属性\nObject.preventExtensions(obj);\n// 阻止在对象上添加新属性，并将所有现有属性标记为不可配置\nObject.seal(obj);\n// 阻止在对象上添加新属性，并将所有现有属性标记为不可配置、不可修改\nObject.freeze(obj);\n\nObject.isExtensible(obj);\n\nObject.isSealed(obj);\n\nObject.isFrozen(obj);\n</code></pre>\n<h1>原型</h1>\n<pre><code class=\"language-js\">// 设置对象的内部 [[Prototype]] 属性\nObject.setPrototypeOf(obj, proto);\n\n// 获取对象的内部 [[Prototype]] 属性\nObject.getPrototypeOf(obj);\n\n// 返回是否对象自身上具有指定属性，而不会从原型上查找\nObject.hasOwn(instance, prop);\n\n// 通过指定原型和可选的属性描述符映射创建新对象\nObject.create(proto[, propertyDescriptorMap]);\n// 请注意！这里属性描述符映射中的属性描述符默认全为 false！\n</code></pre>\n<h1>枚举</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Enumerability_and_ownership_of_properties\">https://developer.mozilla.org/docs/Web/JavaScript/Enumerability_and_ownership_of_properties</a></p>\n<p>注意，<code>for...in</code> 循环遍历时，先遍历对象自身，再遍历原型</p>\n<pre><code class=\"language-js\">// 返回由给定对象 自身的 可枚举属性名组成的数组\nObject.keys(obj);\n\n// 返回由给定对象 自身的 可枚举属性值组成的数组\nObject.values(obj);\n\n// 返回由给定对象 自身的 可枚举属性键值对组成的数组\nObject.entries(obj);\n\n// 将键值对可迭代对象转换为一个对象\nObject.fromEntries(iterable);\n// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Iteration_protocols\n</code></pre>\n<pre><code class=\"language-js\">// 返回由给定对象 自身的 所有非 Symbol 属性名组成的数组\nObject.getOwnPropertyNames(obj);\n\n// 返回由给定对象 自身的 所有 Symbol 属性名组成的数组\nObject.getOwnPropertySymbols(obj);\n</code></pre>\n<pre><code class=\"language-js\">// 将（一个或多个）源对象自身的所有可枚举属性复制到目标对象上，然后返回目标对象\nObject.assign(target, ...sources);\n\n// 方法判断两个值是否为同一个值\nObject.is(value1, value2);\n\n// 演示\nObject.is(+0, -0); // =&gt; false\n+0 === -0; // =&gt; true\nObject.is(NaN, NaN); // =&gt; true\nNaN === NaN; // =&gt; false\n</code></pre>\n<h1>原型链方法</h1>\n<p><code>Object.prototype</code> 上定义的方法</p>\n<pre><code class=\"language-js\">// 判断当前对象自身的指定属性是否可枚举\nObject.prototype.propertyIsEnumerable(prop);\n\n// 判断当前对象自身是否具有指定的属性\nObject.prototype.hasOwnProperty(prop);\n\n// 判断当前对象是否在另一个对象的原型链上，与instanceof的区别在于后者判断的是构造函数，而不是原型对象本身\nObject.prototype.isPrototypeOf(obj);\n\n// 下面两个无参方法主要用于类型转换\n// 参见：https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive\nObject.prototype.valueOf();\nObject.prototype.toString();\n</code></pre>\n","tags":["js"]},{"id":"linux-bash-intro","url":"https://yieldray.fun/posts/linux-bash-intro","title":"bash入门","date_published":"2022-11-07T16:00:00.000Z","date_modified":"2022-11-07T16:00:00.000Z","content_text":"<p>手册<br><a href=\"https://www.gnu.org/software/bash/manual/\">https://www.gnu.org/software/bash/manual/</a><br><a href=\"https://dashdash.io/1/bash\">https://dashdash.io/1/bash</a>\n参考<br><a href=\"https://wangdoc.com/bash/\">https://wangdoc.com/bash/</a><br><a href=\"https://github.com/onceupon/Bash-Oneliner\">https://github.com/onceupon/Bash-Oneliner</a><br><a href=\"https://github.com/jaywcjlove/shell-tutorial\">https://github.com/jaywcjlove/shell-tutorial</a><br><a href=\"https://github.com/skywind3000/awesome-cheatsheets/blob/master/languages/bash.sh\">https://github.com/skywind3000/awesome-cheatsheets/blob/master/languages/bash.sh</a></p>\n<p>基本命令参见 <a href=\"/posts/linux-intro/\">此处</a>，命令帮助参见 <a href=\"/posts/shell-command-help/\">此处</a>，本篇主要在于 shell 编程</p>\n<h1>命令</h1>\n<p>分号是命令的分隔符，但不必要，也可以用换行代替</p>\n<pre><code class=\"language-sh\">command [ arg1 ... [ argN ]]\n</code></pre>\n<h1>exit 命令</h1>\n<p>退出时，脚本会返回一个退出值。脚本的退出值，0 表示正常，1 表示发生错误，2 表示用法不对，126 表示不是可执行脚本，127 表示命令没有发现。如果脚本被信号 N 终止，则退出值为 128 + N。简单来说，只要退出值非 0，就认为执行出错</p>\n<pre><code class=\"language-sh\"># 不带参数\nexit\n\n# 退出值为0（成功）\nexit 0\n\n# 退出值为1（失败）\nexit 1\n</code></pre>\n<h1>Shebang 行</h1>\n<p>放在脚本的第一行，指定解释器</p>\n<pre><code class=\"language-sh\"># 使用系统默认shell\n#!/bin/sh\n\n# 指定为bash\n#!/bin/bash\n\n# 通过env命令指定为bash\n#!/usr/bin/env bash\n</code></pre>\n<p><code>env</code> 命令指向 <code>/usr/bin/env</code> 这个程序，该命令用于获取环境变量</p>\n<pre><code class=\"language-sh\">env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]\n</code></pre>\n<h1>变量</h1>\n<p>Bash 没有数据类型的概念，所有的变量值都是字符串</p>\n<h2>声明变量</h2>\n<p>声明变量不需要美元符号<code>$</code>，引用变量需要<code>$</code><br>等号之间不能有空格</p>\n<pre><code class=\"language-sh\">myvar=&quot;hello world&quot;\nmyvar=hello,world\nsingle_quota=&#39;a\\nb&#39; # 单引号不转义\ndouble_quota=&quot;a\\nb&quot; # 双引号转义\ncmd=$(ls -l)\ncmd=`ls -l`\ncmd=$((5 * 7))\n\n# 单引号和双引号都是支持多行字符串的\nmyvar=&quot;hello\nworld&quot;\n# 美元符号（$）、反引号（`）和反斜杠（\\），这三个字符在双引号之中会被自动扩展\n# 单引号则保留字符的字面含义，上面三个特殊字符也不会扩展\necho &quot;$(( 2 * 3 )) = 6&quot; # 6 = 6\necho &#39;$(( 2 * 3 )) = 6&#39; # $(( 2 * 3 )) = 6\n\n# Here文档，相当于管道\ncat &lt;&lt; _token_\n$foo\n&quot;$foo&quot;\n&#39;$foo&#39;\n_token_\n\n# 将变量值转为标准输入\ncat &lt;&lt;&lt; &#39;hi there&#39;\necho &#39;hi there&#39; | cat\n</code></pre>\n<h2>输入变量，read 命令</h2>\n<p>Read a line from the standard input and split it into fields.</p>\n<pre><code class=\"language-sh\">read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]\n\n# 从标准输入读入变量\nread text\necho &quot;输入了：$text&quot;\n\n# 可以读入多个值\n# 如果用户的输入项少于read命令给出的变量数目，那么额外的变量值为空。\n# 如果用户的输入项多于定义的变量，那么多余的输入项会包含到最后一个变量中。\nread FN LN\necho &quot;Hi! $LN, $FN !&quot;\n\n# 如果read命令之后没有定义变量名，那么环境变量REPLY会包含所有的输入。\nread\necho &quot;$REPLY&quot;\n\n\n# read命令也可以读入文件\nfilename=&#39;/etc/hosts&#39;\nwhile read myline\ndo\n  echo &quot;$myline&quot;\ndone &lt; $filename\n</code></pre>\n<p>read 参数</p>\n<pre><code class=\"language-sh\"># -t 参数，指定等待输入的时间，单位为秒\n# 若超时，则 read 命令失败退出\nif read -t 3 response; then\n  echo &quot;用户已经输入了&quot;\nelse\n  echo &quot;用户没有输入&quot;\nfi\n\n# 环境变量TMOUT也可以起到同样作用\nTMOUT=3\nread response\n</code></pre>\n<pre><code class=\"language-sh\"># -p 参数，指定提示信息\nread -p &quot;Enter one or more values &gt; &quot;\necho &quot;$REPLY&quot;\n# 相当于\necho -n &quot;Enter one or more values &gt; &quot;\nread\necho &quot;$REPLY&quot;\n</code></pre>\n<p>IFS 环境变量（Internal Field Separator）</p>\n<pre><code class=\"language-sh\"># read命令读取的值，默认以空格分隔\n# 可以通过自定义环境变量 IFS 修改分隔标志\nIFS=&quot;:&quot; read input\n# 将 IFS 和 read 写在一行，则 IFS 仅在这一行生效（到了下一行就恢复）\n</code></pre>\n<h2>使用变量</h2>\n<pre><code class=\"language-sh\">a=test\ntest=123\nb=&quot;$a   $test&quot;\n\n# echo命令，不加引号，变量解释为多个参数，故只有一个空格\necho $b        # test 123\n\n# 加上双引号（转义，单引号不转义），变量作为一个字符串整体，而作为 echo 命令的一个参数，故保留了空格格式\necho &quot;$b&quot;      # test   123\n\n# 未声明的变量，相当于空字符串\necho $a_file   # &lt;空&gt;\n\n# ${变量名} 方式获取变量，防止与其它字符混淆\necho ${a}_file # test_file\n\n# ${!保存变量名的变量} 表示变量名本身也是变量，下面相当于 echo $test\necho ${!a}     # 123\n</code></pre>\n<h2>删除变量</h2>\n<pre><code class=\"language-sh\">myvar=123\nunset $myvar\n# 不存在的变量，相当于空字符串，故unset命令等效于下面的其一\nmyvar=&#39;&#39;\nmyvar=\n</code></pre>\n<h2>传递变量给子 shell</h2>\n<p>export 命令，将变量传递给子 shell<br>（相当于给启动的程序传递环境变量）</p>\n<pre><code class=\"language-sh\">NAME=foo\nexport NAME\n# 或者\nexport NAME=foo\n</code></pre>\n<h2>特殊变量</h2>\n<p><code>$?</code> 返回上一个命令的退出码</p>\n<p><code>$$</code> 返回当前 shell 的进程 ID</p>\n<p><code>$-</code> 返回当前 Shell 的启动参数</p>\n<p><code>$_</code> 返回上一个命令的最后一个参数</p>\n<p><code>$!</code> 返回最近一个后台执行的异步命令的进程 ID</p>\n<p><code>$0</code> ~ <code>$9</code> (超过 9 的以 <code>${10}</code> 形式获取) 参数列表，相当于 C 语言的 <code>argv</code> （注意，<code>$0</code> 一般为当前脚本文件名）</p>\n<p><code>$#</code> 参数的总数，相当于 C 语言的 <code>argc</code></p>\n<p><code>$@</code> 全部的参数，以空格分隔</p>\n<p><code>$*</code> 全部的参数，参数之间使用变量 <code>$IFS</code> 值的第一个字符分隔，默认为空格，但是可以自定义。</p>\n<h2>declare/readonly/let 命令</h2>\n<pre><code class=\"language-sh\"># declare 命令，声明特殊类型的变量\ndeclare [-aAfFgiIlnrtux] [-p] [name[=value] ...]\n\n# readonly 命令，声明只读变量\nreadonly [-aAf] [name[=value] ...] or readonly -p\n\n# let 命令，执行数学运算表达式\nlet arg [arg ...]\n</code></pre>\n<p>参见<br><a href=\"https://wangdoc.com/bash/variable#declare-%E5%91%BD%E4%BB%A4\">https://wangdoc.com/bash/variable#declare-%E5%91%BD%E4%BB%A4</a><br><a href=\"https://cn.tldr.inbrowser.app/pages/common/declare\">https://cn.tldr.inbrowser.app/pages/common/declare</a></p>\n<h2>数组变量</h2>\n<pre><code class=\"language-sh\">array[0]=value0\narray[1]=value1\narray[2]=value2\n\narray=(value0 value1 value3)\n# 可以写成多行\narray=(\n  value0\n  value1\n  value2\n)\n# 可以指定下标\narray=([2]=value2 [0]=value0 [1]=value1)\n# 省略\narray=(value0 [2]=value2 value1)\n# 使用通配符，自动展开\ntxts=( *.txt )\n\n# declare -a 选项，声明数组\ndeclare -a ARRAYNAME\n# read -a 选项，读入数组\nread -a ARRAYNAME\n# 获取数组成员时，必须使用花括号\necho ${array[0]}\n# 不能这样，因为此时方括号会单独解释\necho $array[0]\n</code></pre>\n<h2>变量的默认值</h2>\n<pre><code class=\"language-sh\"># 若变量存在，则返回+后面的值\n${varname:+value_if_exist}\n# 若变量不存在，则返回-后面的值\n${varname:-value_if_not_exist}\n# 若变量不存在，则返回=后面的值，且将变量设置为=后面的值\n${varname:=set_if_not_exist}\n# 若变量不存在，则打印?后面的值，并中断脚本的执行\n${varname:?exit_if_not_exist}\n</code></pre>\n<p>例如，可以编写以下脚本，要求传入第一个参数 <code>$1</code>。若不存在，则打印 filename missing. 并退出脚本的运行</p>\n<pre><code class=\"language-sh\">filename=${1:?&quot;filename missing.&quot;}\n</code></pre>\n<h1>字符串操作</h1>\n<h2>echo 命令</h2>\n<pre><code class=\"language-sh\">echo hello world\necho -n &quot;无换行&quot;\necho -e &quot;解释特殊字符\\n单引号和双引号皆有效&quot;\n\n# 反斜线，将下一行命令和当前行一起解释\necho hello,\\\nworld\n</code></pre>\n<h2>字符串变量</h2>\n<p>匹配模式 pattern 可以使用<code>*</code>、<code>?</code>、<code>[]</code>等通配符。</p>\n<pre><code class=\"language-sh\"># 字符串长度\n${#variable}\n# 提取字字符串。如果省略length，则从位置offset开始，一直返回到字符串的结尾。\n#  offset和length可为负值，注意冒号后必须有一空格，${variable: -offset:-length}\n${variable:offset:length}\n\n\n# 匹配字符串的开头\n# 返回字符串中移除了匹配的部分\n${variable#pattern}  # 非贪婪\n${variable##pattern} # 贪婪\n\n# 匹配字符串的末尾，同上\n${variable%pattern}  # 非贪婪\n${variable%%pattern} # 贪婪\n\n# 返回字符串中匹配部分被替换为给定字符串后的结果\n${variable/pattern/string}  # 贪婪，只替换第一处\n${variable//pattern/string} # 贪婪，替换所有\n${variable/#pattern/string} # 模式必须出现在字符串的开头\n${variable/%pattern/string} # 模式必须出现在字符串的结尾\n\n# 全部转为大写\n${varname^^}\n\n# 全部转为小写\n${varname,,}\n</code></pre>\n<h1>数学运算</h1>\n<p><code>((...))</code> 语法可以进行<strong>整数</strong>的算术运算</p>\n<pre><code class=\"language-sh\"># 这个语法不返回值\n# 只要算术结果不是 0，该命令退出码 0；否则为 1。（可以通过 $? 变量获取返回值）\n(( expression ))\n# 加上美元符号 $ 后，该命令返回运算结果\n$(( expression ))\n</code></pre>\n<p>也可以使用 expr 命令，同样只支持整数</p>\n<pre><code class=\"language-sh\">expr {{expression}}\n</code></pre>\n<p>let 命令用于将算术运算的结果赋予一个变量，等号之间不能有空格</p>\n<pre><code class=\"language-sh\">let x=2+3\n</code></pre>\n<h1>source 命令</h1>\n<p>source 命令在当前 shell 执行一个脚本</p>\n<pre><code class=\"language-sh\">source {{file}}\n</code></pre>\n<h1>shift 命令</h1>\n<p>shift 命令移除脚本的前 n 个参数（不包括<code>$0</code>）。若未指定 n，则为 n 为 1<br>若 n 大于 <code>$#</code> （或 n 为负数），则该命令失败；否则成功</p>\n<pre><code class=\"language-sh\">shift {{n}}\n</code></pre>\n<h1>getopt/getopts 命令</h1>\n<p>getopt/getopts 命令用于解析命令参数</p>\n<pre><code class=\"language-sh\">getopts optstring name [arg ...]\n\ngetopt &lt;optstring&gt; &lt;parameters&gt;\ngetopt [options] [--] &lt;optstring&gt; &lt;parameters&gt;\ngetopt [options] -o|--options &lt;optstring&gt; [options] [--] &lt;parameters&gt;\n</code></pre>\n<h1>trap 命令</h1>\n<p>trap 命令捕获该命令之后的系统信号（和其它事件）</p>\n<pre><code class=\"language-sh\">trap [-lp] [[arg] signal_spec ...]\n</code></pre>\n<p>简单的用法就是一旦捕获到 signal，就执行 command</p>\n<pre><code class=\"language-sh\">trap {{command}} {{signal}}\n\n# -l 选项列出所有信号（及对应数字）\ntrap -l\n</code></pre>\n<h1>test 命令</h1>\n<p>test 命令执行成功返回 0；否则返回 1</p>\n<pre><code class=\"language-sh\"># 写法一\ntest expression\n\n# 写法二（注意，这里的空格是必须的）\n[ expression ]\n\n# 写法三（支持正则判断）\n[[ expression ]]\n</code></pre>\n<p>参见 <a href=\"https://wangdoc.com/bash/condition#test-%E5%91%BD%E4%BB%A4\">https://wangdoc.com/bash/condition#test-%E5%91%BD%E4%BB%A4</a></p>\n<h1>控制流</h1>\n<p>注意，分号的作用和换行等效</p>\n<h2>if</h2>\n<p>若命令返回 0，则判断成立</p>\n<pre><code class=\"language-sh\">if commands; then\n  commands\n[elif commands; then\n  commands...]\n[else\n  commands]\nfi\n</code></pre>\n<h2>while</h2>\n<pre><code class=\"language-sh\">while condition; do\n  commands\ndone\n</code></pre>\n<h2>until</h2>\n<pre><code class=\"language-sh\">until condition; do\n  commands\ndone\n</code></pre>\n<h2>for</h2>\n<p>break 和 continue 也是支持的</p>\n<pre><code class=\"language-sh\">for (( expression1; expression2; expression3 )); do\n  commands\ndone\n</code></pre>\n<h2>for...in</h2>\n<pre><code class=\"language-sh\">for variable in list; do\n  commands\ndone\n</code></pre>\n<h2>select</h2>\n<pre><code class=\"language-sh\">select name [in list]; do\n  commands\ndone\n</code></pre>\n<h2>case</h2>\n<pre><code class=\"language-sh\">case $variable in\n    $value)\n      commands\n    ;;\n    $value1|$value2[|$value3 ...])\n      commands\n    ;;\n    *)\n      default_commands\n      break\n    ;;\nesac\n</code></pre>\n<h1>函数</h1>\n<h2>函数声明</h2>\n<pre><code class=\"language-sh\"># 第一种\nfn() {\n    # codes\n}\n\n# 第二种\nfunction fn() {\n    # codes\n}\n\n# 调用函数（相当于调用命令，没有括号）\nfn arg1 arg2 ...\n</code></pre>\n<p>函数内部也有特殊变量，和整个脚本的特殊变量含义相同\n<code>$0</code> ~ <code>$9</code> <code>$#</code> <code>$@</code> <code>$*</code></p>\n<h2>函数返回值，return</h2>\n<pre><code class=\"language-sh\">fn() {\n    return 123\n}\nfn\necho $?\n# 输出 123\n</code></pre>\n<p>下一个命令可以通过 <code>$?</code> 获取上一个函数的返回值</p>\n<h2>声明局部变量，local 命令</h2>\n<p>若不使用 local 命令，函数体中的变量和全局变量相同<br>也就是说，可以在函数体内创建、访问和修改全局变量</p>\n<p>local 命令声明的变量，只在函数体内有效</p>\n<pre><code class=\"language-sh\">#! /bin/bash\nfn() {\n    local foo\n    foo=123\n    echo $foo\n}\nfn        # 123\necho $foo # &lt;空&gt;\n</code></pre>\n","tags":["linux","bash"]},{"id":"golang-mod","url":"https://yieldray.fun/posts/golang-mod","title":"golang模块管理","date_published":"2022-11-06T12:00:00.000Z","date_modified":"2022-11-06T12:00:00.000Z","content_text":"<p>参考：\n<a href=\"https://go.dev/doc/modules/managing-dependencies\">https://go.dev/doc/modules/managing-dependencies</a><br><a href=\"https://go.dev/blog/using-go-modules\">https://go.dev/blog/using-go-modules</a></p>\n<h1>初始化模块</h1>\n<p>首先创建项目文件夹</p>\n<pre><code class=\"language-sh\">mkdir mymod\ncd mymod\n</code></pre>\n<p><code>go mod init</code> 命令：在当前目录下初始化新的模块</p>\n<pre><code class=\"language-sh\">go mod init example.com/m\n</code></pre>\n<p>该命令创建 <code>go.mod</code> 文件，内容如下</p>\n<pre><code>module example.com/m\n\ngo 1.19\n</code></pre>\n<p>该文件描述了当前模块和依赖</p>\n<h1>配置镜像</h1>\n<p><a href=\"https://goproxy.cn/\">https://goproxy.cn/</a><br><a href=\"https://goproxy.io/zh/\">https://goproxy.io/zh/</a><br><a href=\"https://mirrors.aliyun.com/goproxy/\">https://mirrors.aliyun.com/goproxy/</a></p>\n<h1>安装依赖</h1>\n<p><code>go get</code> 命令：安装依赖</p>\n<pre><code>go get example.com/pkg\n</code></pre>\n<p>例如：</p>\n<pre><code>go get github.com/subosito/gotenv\n</code></pre>\n<p>go.mod 文件如下</p>\n<pre><code>module example.com/m\n\ngo 1.19\n\nrequire github.com/subosito/gotenv v1.4.1 // indirect\n</code></pre>\n<p>为了测试，创建一个 <code>.env</code> 文件</p>\n<pre><code class=\"language-sh\">echo -e &quot;APP_ID=1234567\\nAPP_SECRET=abcdef&quot; &gt; .env\n</code></pre>\n<p>创建一个 go 源代码文件，内容如下</p>\n<pre><code class=\"language-go\">package main\n\nimport (\n    &quot;github.com/subosito/gotenv&quot;\n    &quot;log&quot;\n    &quot;os&quot;\n)\n\nfunc init() {\n    gotenv.Load()\n}\n\nfunc main() {\n    log.Println(os.Getenv(&quot;APP_ID&quot;))     // &quot;1234567&quot;\n    log.Println(os.Getenv(&quot;APP_SECRET&quot;)) // &quot;abcdef&quot;\n}\n</code></pre>\n<p>运行试试</p>\n<pre><code>go run test.go\n\n2022/11/06 12:00:00 1234567\n2022/11/06 12:00:01 abcdef\n</code></pre>\n<p>由于我们已经使用了该依赖，运行 <code>go mod tidy</code> 命令<br>该命令将去掉 go.mod 文件中项目不需要的依赖<br>现在，go.mod 内容如下<br>可以发现，<code>// indirect</code> 注释消失了</p>\n<pre><code>module example.com/m\n\ngo 1.19\n\nrequire github.com/subosito/gotenv v1.4.1\n</code></pre>\n<p>安装的依赖存在于 <code>~/go/pkg/mod</code> 目录中<br>运行 <code>go mod vendor</code> 命令，则将在当前项目生成 <code>vendor</code> 目录，保存所有依赖<br>此后一旦修改了 <code>go.mod</code> 文件，就需要执行 <code>go mod vendor</code>，重新生成 <code>vendor</code> 目录</p>\n<h1>导出</h1>\n<p>包名应当与目录名相同<br>包名为 <code>internal</code> 的包只可被直接父级包和同级的相邻包使用</p>\n<pre><code class=\"language-go\">// mymod/any_name.go\n\npackage mymod\n\nimport &quot;fmt&quot;\n\nfunc Hello(name string) {\n    fmt.Printf(&quot;Hello, %s!&quot;, name)\n}\n</code></pre>\n<h1>导入</h1>\n<p>要导入上面创建的包，我们再创建一个项目</p>\n<pre><code class=\"language-sh\">cd ..\nmkdir test\ngo mod init test\n\n# 因为我们的包在本地，所以执行\ngo mod edit -replace example.com/m/mymod=../mymod\ngo get example.com/m/mymod\n</code></pre>\n<pre><code class=\"language-go\">// test.go\npackage main\n\nimport &quot;example.com/m/mymod&quot;\n\nfunc main() {\n    mymod.Hello(&quot;Ray&quot;) // Hello, Ray!\n}\n</code></pre>\n<p>此时 <code>go.mod</code> 文件内容如下</p>\n<pre><code>module test\n\ngo 1.19\n\nreplace example.com/m/mymod =&gt; ../mymod\n\nrequire example.com/m/mymod v0.0.0-00010101000000-000000000000 // indirect\n</code></pre>\n<p>运行 <code>go mod tidy</code> 还是可以将 <code>// indirect</code> 注释消除的</p>\n<h1>更新依赖</h1>\n<pre><code class=\"language-sh\"># List all of the modules that are dependencies of your current module, along with the latest version available for each:\ngo list -m -u all\n\n# Display the latest version available for a specific module:\ngo list -m -u example.com/theirmodule\n\n# To get the latest version, append the module path with @latest:\ngo get example.com/theirmodule@latest\n</code></pre>\n<h1>发布包</h1>\n<p>参见：<a href=\"https://go.dev/blog/publishing-go-modules\">https://go.dev/blog/publishing-go-modules</a></p>\n","tags":["golang"]},{"id":"golang-intro","url":"https://yieldray.fun/posts/golang-intro","title":"golang入门","date_published":"2022-10-31T17:00:00.000Z","date_modified":"2022-10-31T17:00:00.000Z","content_text":"<p>Playground<br><a href=\"https://golang.google.cn/play/\">https://golang.google.cn/play/</a><br>Go 指南<br><a href=\"https://tour.go-zh.org/\">https://tour.go-zh.org/</a><br>Go by example<br><a href=\"https://gobyexample-cn.github.io/\">https://gobyexample-cn.github.io/</a><br>Go 语言圣经（中文版）<br><a href=\"https://gopl-zh.github.io/\">https://gopl-zh.github.io/</a><br>Mastering GO<br><a href=\"https://github.com/hantmac/Mastering_Go_ZH_CN\">https://github.com/hantmac/Mastering_Go_ZH_CN</a><br><a href=\"https://books.studygolang.com/Mastering_Go_ZH_CN/\">https://books.studygolang.com/Mastering_Go_ZH_CN/</a><br>Golang for Node.js Developers<br><a href=\"https://github.com/miguelmota/golang-for-nodejs-developers\">https://github.com/miguelmota/golang-for-nodejs-developers</a><br>参考<br><a href=\"https://books.studygolang.com/\">https://books.studygolang.com/</a><br><a href=\"https://github.com/jaywcjlove/golang-tutorial\">https://github.com/jaywcjlove/golang-tutorial</a><br><a href=\"https://github.com/iswbm/GolangCodingTime\">https://github.com/iswbm/GolangCodingTime</a><br><a href=\"https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md\">https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md</a><br><a href=\"https://github.com/polaris1119/The-Golang-Standard-Library-by-Example/blob/master/directory.md\">https://github.com/polaris1119/The-Golang-Standard-Library-by-Example/blob/master/directory.md</a><br>标准库<br><a href=\"https://pkg.go.dev/std\">https://pkg.go.dev/std</a><br><a href=\"https://studygolang.com/pkgdoc\">https://studygolang.com/pkgdoc</a></p>\n<p>分号是可选的！</p>\n<h1>包，导入导出</h1>\n<p>包实现了命名空间的管理<br>任何一个 Go 语言程序必须属于一个包，即每个 go 程序的开头要写上 <code>package &lt;pkg_name&gt;</code></p>\n<pre><code class=\"language-go\">package main\n\nimport (\n    &quot;fmt&quot;\n    &quot;math/rand&quot;\n)\n// 或者\n// import &quot;fmt&quot;\n// import &quot;math/rand&quot;\n\nfunc main() {\n    fmt.Println(&quot;My favorite number is&quot;, rand.Intn(10))\n}\n</code></pre>\n<p>在 Go 中，如果一个名字以大写字母开头，那么它就是已导出的。<br>比如函数 <code>rand.Intn</code> 和变量 <code>math.Pi</code></p>\n<pre><code class=\"language-go\">// 以别名导入\npackage main\n\nimport (\n    f &quot;fmt&quot;\n)\n\nfunc main() {\n    f.Println(&quot;f&quot;)\n}\n</code></pre>\n<pre><code class=\"language-go\">// 将包的命名空间合并到当前程序的命名空间\npackage main\n\nimport . &quot;fmt&quot;\n\nfunc main() {\n    Println(&quot;f&quot;)\n}\n</code></pre>\n<h1>函数</h1>\n<pre><code class=\"language-go\">func add(x int, y int) int {\n    return x + y\n}\n\n// 连续几个参数类型相同，可以省略前面的类型\nfunc add(x, y int) int {\n    return x + y\n}\n\n\n// 可以返回多个值\nfunc swap(x, y string) (string, string) {\n    return y, x\n}\n\nfunc main() {\n    a, b := swap(&quot;hello&quot;, &quot;world&quot;)\n    fmt.Println(a, b)\n}\n\n// 返回值可以被命名，这些命名可以出现在文档中，\n// 最后的 return 不用带参数就可以把命名的返回值全部返回\nfunc split(sum int) (x, y int) {\n    x = sum * 4 / 9\n    y = sum - x\n    return\n}\n</code></pre>\n<p>函数的类型就是函数声明去除函数名、函数参数名、函数体剩下的部分<br>当然，此时需标注所有参数的类型</p>\n<pre><code class=\"language-go\">func(int, int) int\n</code></pre>\n<p>函数也是值<br>所以闭包也是支持的</p>\n<pre><code class=\"language-go\">add := func(x, y int) int {\n    return x + y\n}\n\nfunc addFn(x, y int, fn func(int, int) int) int {\n    return fn(x, y)\n}\naddFn(1, 2, add)\n</code></pre>\n<h1>变量</h1>\n<h2>基本类型</h2>\n<p>bool<br>string<br>int int8 int16 int32 int64<br>uint uint8 uint16 uint32 uint64 uintptr<br>byte // uint8 的别名<br>rune // int32 的别名，表示一个 Unicode 码点<br>float32 float64<br>complex64 complex128</p>\n<h2>零值</h2>\n<p>没有明确初始值的变量声明会被赋予它们的<code>零值</code>。</p>\n<p>数值类型为 <code>0</code><br>布尔类型为 <code>false</code><br>字符串为 <code>&quot;&quot;</code></p>\n<pre><code class=\"language-go\">// var 声明一组变量列表\nvar num, i, count int\nvar a, b, c = true, 123, &quot;no!&quot;;\n\n// const 声明常量\nconst Pi = 3.14\nconst (\n    Big = 1 &lt;&lt; 100\n    Small = Big &gt;&gt; 99\n)\n\n// `:=` 简洁赋值语句，仅可在函数内使用\nfunc main() {\n    var i, j int = 1, 2\n    k := 3\n}\n</code></pre>\n<h2>类型转换</h2>\n<p>不同的类型之间不会进行隐式转换<br>显式转换，只需像这样<code>转换到的类型(被转换的变量)</code>：</p>\n<pre><code class=\"language-go\">var i = 42\nvar f = float64(i)\n</code></pre>\n<h1>控制结构</h1>\n<h2>for</h2>\n<p>只有 for，但包含多种功能</p>\n<pre><code class=\"language-go\">func main() {\n    sum := 0\n    for i := 0; i &lt; 10; i++ {\n        sum += i\n    }\n}\n\nfunc main() {\n    sum := 1\n    for sum &lt; 1000 {\n        sum += sum\n    }\n}\n\n// 无限循环\nfor {\n    // code goes here\n}\n</code></pre>\n<h2>if</h2>\n<p>if 语句可以在条件表达式前执行一个简单的语句，该语句声明的变量作用域仅在 if 之内</p>\n<pre><code class=\"language-go\">if x:= rand.Intn(10); x &gt; 5 {\n    fmt.Println(&quot;BIG!&quot;)\n} else {\n    fmt.Println(&quot;Small!&quot;)\n}\n</code></pre>\n<h2>switch</h2>\n<p>switch 无需 break，作为替代，在有需要时可以使用 fallthrough<br>此外 case 的值无需为常量（比如可以为一个函数返回值）<br>没有条件的 switch 同 switch true 一样</p>\n<pre><code class=\"language-go\">func main() {\n    fmt.Print(&quot;Go runs on &quot;)\n    switch os := runtime.GOOS; os {\n    case &quot;darwin&quot;:\n        fmt.Println(&quot;OS X.&quot;)\n    case &quot;linux&quot;:\n        fmt.Println(&quot;Linux.&quot;)\n    default:\n        // freebsd, openbsd,\n        // plan9, windows...\n        fmt.Printf(&quot;%s.\\n&quot;, os)\n    }\n}\n</code></pre>\n<h2>defer</h2>\n<p>defer 语句会将函数推迟到外层函数返回之后执行。<br>推迟调用的函数其参数会立即求值，但直到外层函数返回前该函数都不会被调用。<br>推迟的函数调用会被压入一个栈中。当外层函数返回时，被推迟的函数会按照后进先出的顺序调用。</p>\n<pre><code class=\"language-go\">func main() {\n    defer fmt.Println(&quot;world&quot;)\n    fmt.Println(&quot;hello&quot;)\n}\n// hello\n// world\n</code></pre>\n<h1>指针与结构体</h1>\n<p>指针类型<code>*T</code>表示指向<code>T</code>类型的指针<br><code>&amp;</code>操作符可创建变量的指针，指针的零值为<code>nil</code><br><code>*</code>操作符可访问指针指向的值</p>\n<pre><code class=\"language-go\">var i int = 1\nvar p *int = &amp;i\nfmt.Println(i)  // 1\nfmt.Println(*p) // 1\n*p = 2\nfmt.Println(i) // 2\n</code></pre>\n<pre><code class=\"language-go\">type Vertex struct {\n    X int\n    Y int\n}\n\nfunc main() {\n    v := Vertex{1, 2} // 相当于 Vertex{X: 1, Y: 2}\n    p := &amp;v\n    p.X = 1e9  // 相当于 (*p).X\n    fmt.Println(v)\n}\n</code></pre>\n<h1>数组与切片</h1>\n<p>类型 <code>[n]T</code> 表示拥有 n 个 T 类型的值的数组<br>数组的切片是对数组的引用，切片的零值则是<code>nil</code><br>切片下界的默认值为 0，上界则是该切片的长度</p>\n<pre><code class=\"language-go\">func main() {\n    var a [2]string\n    a[0] = &quot;Hello&quot;\n    a[1] = &quot;World&quot;\n    fmt.Println(a[0], a[1])\n    fmt.Println(a)\n\n    primes := [6]int{2, 3, 5, 7, 11, 13}\n    fmt.Println(primes)\n\n    var s []int = primes[1:4] // 切片，左闭右开！注意，切片的类型相当于对应数组类型但方括号中为空\n    fmt.Println(s)\n\n    primess := []int{2, 3, 5, 7, 11, 13} // 方括号中没有指明数组容量，则我们得到的是切片而不是数组！\n}\n</code></pre>\n<p>切片具有长度(len)和容量(cap)</p>\n<pre><code class=\"language-go\">a := make([]int, 5)  // len(a)=5\nb := make([]int, 0, 5) // len(b)=0, cap(b)=5\n\nb = b[:cap(b)] // len(b)=5, cap(b)=5\nb = b[1:]      // len(b)=4, cap(b)=4\n</code></pre>\n<pre><code class=\"language-go\">func append(s []T, vs ...T) []T\n// append 函数创建了一个在原切片上追加元素的新切片\n\n// range循环同时返回切片的下标和对应元素的*副本*\nfor i, v := range []int{1, 2, 3} {\n    fmt.Printf(&quot;index=%d value=%d\\n&quot;, i, v)\n}\n</code></pre>\n<h1>映射 (map)</h1>\n<p>映射的类型是 <code>map[K]V</code> K 是键的类型，V 是值的类型<br>若指定键不在映射中，那么其对应值是该映射元素类型的零值</p>\n<pre><code class=\"language-go\">m := make(map[string]Vertex)\nm[&quot;hello&quot;] = Vertex{3, 6}\nfmt.Println(m[&quot;hello&quot;]) // {3 6}\nfmt.Println(m[&quot;world&quot;]) // {0 0} 零值\n\n// or\nvar m = map[string]Vertex{\n    &quot;hello&quot;: Vertex{3, 6},\n    &quot;world&quot;: Vertex{4, 8},\n}\n// or\nvar m = map[string]Vertex{\n    &quot;hello&quot;: {3, 6},\n    &quot;world&quot;: {4, 8},\n}\n\ndelete(m, &quot;key&quot;)\nelem := m[&quot;key&quot;]\nelem, ok := m[&quot;key&quot;]\n</code></pre>\n<h1>方法和接口</h1>\n<h2>方法</h2>\n<p>方法只是个带接收者参数的函数</p>\n<pre><code class=\"language-go\">type Vertex struct {\n    X, Y float64\n}\n\n// 方法\nfunc (v Vertex) Abs() float64 {\n    return math.Sqrt(v.X*v.X + v.Y*v.Y)\n}\n\nfunc main() {\n    v := Vertex{3, 4}\n    fmt.Println(v.Abs())\n}\n\n// 相当于普通函数\nfunc Abs(v Vertex) float64 {\n    return math.Sqrt(v.X*v.X + v.Y*v.Y)\n}\n\nfunc main() {\n    v := Vertex{3, 4}\n    fmt.Println(Abs(v))\n}\n</code></pre>\n<p>接收者的类型定义和方法声明必须在同一包内<br>不能为内建类型声明方法（但可以通过给其它（内建）类型起别名来规避）</p>\n<p>在需要修改接收者指向的值时，可以为指针接收者声明方法</p>\n<pre><code class=\"language-go\">func (v *Vertex) Scale(f float64) {\n    v.X = v.X * f\n    v.Y = v.Y * f\n}\n</code></pre>\n<p>与普通函数不同的是：以指针为接收者的方法被调用时，接收者既能为值又能为指针<br>因为此时 Go 会将值解释为指针：<code>v.Scale(5)</code> -&gt; <code>(&amp;v).Scale(5)</code> 从而能对值进行修改\n以值为接收者的方法被调用时，接收者既能为值又能为指针，<code>p.Abs()</code> 会被解释为 <code>(*p).Abs()</code></p>\n<h2>接口</h2>\n<p>类型通过实现一个接口的所有方法来实现该接口</p>\n<pre><code class=\"language-go\">type I interface {\n    M()\n}\n\ntype T struct {\n    S string\n}\n\nfunc (t *T) M() {\n    if t == nil {\n        fmt.Println(&quot;&lt;nil&gt;&quot;)\n        return\n    }\n    fmt.Println(t.S)\n}\n\ntype F float64\n\nfunc (f F) M() {\n    fmt.Println(f)\n}\n\nfunc main() {\n    var i I\n\n    i = &amp;T{&quot;Hello&quot;}\n    describe(i) // (&amp;{Hello}, *main.T)\n    i.M()       // Hello\n\n    i = F(math.Pi)\n    describe(i) // (3.141592653589793, main.F)\n    i.M()       // 3.141592653589793\n\n    var t *T\n    i = t\n    describe(i) // (&lt;nil&gt;, *main.T)\n    i.M() // &lt;nil&gt;\n\n    i = nil\n    describe(i) // (&lt;nil&gt;, &lt;nil&gt;)\n    i.M() // panic: runtime error: invalid memory address or nil pointer dereference\n}\n\nfunc describe(i I) {\n    fmt.Printf(&quot;(%v, %T)\\n&quot;, i, i)\n}\n</code></pre>\n<p>空接口 <code>interface{}</code> 可以保存任意类型的值<br>类型断言<code>t := i.(T)</code>则可以访问接口底层的具体值<br>类型断言可返回两个值：其底层值以及一个报告断言是否成功的布尔值</p>\n<pre><code class=\"language-go\">func main() {\n    var i interface{} = &quot;hello&quot;\n\n    s := i.(string)\n    fmt.Println(s)\n\n    s, ok := i.(string)\n    fmt.Println(s, ok)\n\n    f, ok := i.(float64)\n    fmt.Println(f, ok) // 0 false\n\n    f = i.(float64) // panic: interface conversion: interface {} is string, not float64\n    fmt.Println(f)\n}\n</code></pre>\n<p>通过 类型选择 可以按顺序从几个类型断言中进行选择<br>与一般的 switch 语句相似，不过类型选择中的 case 为类型（而非值）</p>\n<pre><code class=\"language-go\">func do(i interface{}) {\n    switch v := i.(type) {\n    case int:\n        fmt.Printf(&quot;Twice %v is %v\\n&quot;, v, v*2)\n    case string:\n        fmt.Printf(&quot;%q is %v bytes long\\n&quot;, v, len(v))\n    default:\n        // 没有匹配，v 与 i 的类型相同（这里是 interface{}）\n        fmt.Printf(&quot;I don&#39;t know about type %T!\\n&quot;, v)\n    }\n}\n</code></pre>\n<h1>错误处理</h1>\n<p>Go 程序使用 error 值来表示错误状态<br>接口 error 是内建接口</p>\n<pre><code class=\"language-go\">type error interface {\n    Error() string\n}\n</code></pre>\n<p>通过检查返回的 error 接口值是否为 nil 得知是否发生错误</p>\n<pre><code class=\"language-go\">type MyError string\n\nfunc (e MyError) Error() string {\n    return string(e)\n}\n\nfunc divide(a, b int) (n int, err error) {\n    if b == 0 {\n        return 0, MyError(&quot;除数不能为零&quot;)\n    }\n    return a / b, nil\n}\n\nfunc main() {\n    result, err := divide(2, 0)\n    if err == nil { // 无错误\n        fmt.Println(result)\n    } else { // 有错误\n        fmt.Println(err)\n    }\n}\n</code></pre>\n<h1>并发</h1>\n<p>Go 程（goroutine）是由 Go 运行时管理的轻量级线程。</p>\n<p><code>go f(x, y, z)</code>启动一个 go 程</p>\n<p>go 程通过信道（channel）通信，信道是带有类型的管道</p>\n<pre><code class=\"language-go\">func sum(s []int, c chan int) {\n    sum := 0\n    for _, v := range s {\n        sum += v\n    }\n    c &lt;- sum // 将和送入 c\n}\n\nfunc main() {\n    s := []int{7, 2, 8, -9, 4, 0}\n\n    c := make(chan int)\n    go sum(s[:len(s)/2], c)\n    go sum(s[len(s)/2:], c)\n    x, y := &lt;-c, &lt;-c // 从 c 中接收\n\n    fmt.Println(x, y, x+y)\n}\n</code></pre>\n<p>信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道<br>仅当信道的缓冲区填满后，向其发送数据时才会阻塞。当缓冲区为空时，接受方会阻塞。</p>\n<pre><code class=\"language-go\">func main() {\n    ch := make(chan int, 2)\n    ch &lt;- 1\n    ch &lt;- 2\n    fmt.Println(&lt;-ch)\n    fmt.Println(&lt;-ch)\n}\n</code></pre>\n<p>发送者可通过 close 关闭一个信道（向一个已经关闭的信道发送数据会引发 panic）<br>接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭 <code>v, ok := &lt;-ch</code></p>\n<pre><code class=\"language-go\">func fibonacci(n int, c chan int) {\n    x, y := 0, 1\n    for i := 0; i &lt; n; i++ {\n        c &lt;- x\n        x, y = y, x+y\n    }\n    close(c)\n}\n\nfunc main() {\n    c := make(chan int, 10)\n    go fibonacci(cap(c), c)\n    for i := range c {\n        fmt.Println(i)\n    }\n}\n</code></pre>\n<p>select 语句使一个 Go 程可以等待多个通信操作。</p>\n<p>select 会阻塞到某个分支可以继续执行为止，这时就会执行该分支。<br>当多个分支都准备好时会随机选择一个执行。</p>\n<pre><code class=\"language-go\">func fibonacci(c, quit chan int) {\n    x, y := 0, 1\n    for {\n        select {\n        case c &lt;- x:\n            x, y = y, x+y\n        case &lt;-quit:\n            fmt.Println(&quot;quit&quot;)\n            return\n        }\n    }\n}\n\nfunc main() {\n    c := make(chan int)\n    quit := make(chan int)\n    go func() {\n        for i := 0; i &lt; 10; i++ {\n            fmt.Println(&lt;-c)\n        }\n        quit &lt;- 0\n    }() // 这是一个立即执行表达式，且在go程中执行\n    fibonacci(c, quit)\n}\n</code></pre>\n<p>当 select 中的其它分支都没有准备好时，default 分支就会执行。（不阻塞）</p>\n<pre><code class=\"language-go\">func main() {\n    tick := time.Tick(100 * time.Millisecond)\n    boom := time.After(500 * time.Millisecond)\n    for {\n        select {\n        case &lt;-tick:\n            fmt.Println(&quot;tick.&quot;)\n        case &lt;-boom:\n            fmt.Println(&quot;BOOM!&quot;)\n            return\n        default:\n            fmt.Println(&quot;    .&quot;)\n            time.Sleep(50 * time.Millisecond)\n        }\n    }\n}\n</code></pre>\n<p>sync.Mutex 互斥锁类型，支持互斥操作</p>\n<pre><code class=\"language-go\">import &quot;sync&quot;\n\ntype SafeCounter struct {\n    v   map[string]int\n    mux sync.Mutex\n}\n\nfunc (c *SafeCounter) Inc(key string) {\n    c.mux.Lock()\n    c.v[key]++ // 临界区\n    c.mux.Unlock()\n}\n\nfunc (c *SafeCounter) Value(key string) int {\n    c.mux.Lock()\n    defer c.mux.Unlock() // 注意defer的用法\n    return c.v[key] // 临界区\n}\n</code></pre>\n","tags":["golang"]},{"id":"java-concurrent","url":"https://yieldray.fun/posts/java-concurrent","title":"java并发、异步以及函数式","date_published":"2022-10-10T10:10:10.000Z","date_modified":"2022-10-10T10:10:10.000Z","content_text":"<p>见：<a href=\"/posts/java-thread-utils/\">多线程实用库</a> <a href=\"/posts/java-functional/\">函数式</a></p>\n<p>文档：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/package-summary.html\">java.util.concurrent</a> <a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/function/package-summary.html\">java.util.function</a></p>\n<h1>java.util.function</h1>\n<p>提供一些可以表示 lambda 函数的类型的泛型接口<br>根据命名，有以下几种类型：</p>\n<ul>\n<li><code>Function&lt;T,R&gt;</code> ( T -&gt; R 的一元函数)</li>\n<li><code>Consumer&lt;T&gt;</code> ( T -&gt; void 的一元函数)</li>\n<li><code>Predicate&lt;T&gt;</code> (T -&gt; boolean 的一元函数)</li>\n<li><code>Supplier&lt;T&gt;</code> (返回 R 的无参函数)</li>\n<li><code>UnaryOperator&lt;T&gt;</code> (T -&gt; T 的一元函数)</li>\n</ul>\n<p>以<code>Function&lt;T,R&gt;</code>为例，实际的方法函数就是 Function::apply 这个函数</p>\n<p>令这个函数为 <code>f</code> ，另一函数为 <code>g</code><br>则 compose 方法为 <code>f(g())</code>，andThen 方法为 <code>g(f())</code></p>\n<p>此外</p>\n<ul>\n<li><code>Runnable</code> (void -&gt; void)</li>\n<li><code>Callable&lt;V&gt;</code> (void -&gt; V)</li>\n</ul>\n<pre><code class=\"language-java\">// 很容易定义这种函数式接口\n@FunctionalInterface\npublic interface Runnable {\n    void run();\n}\n</code></pre>\n<pre><code class=\"language-java\">@FunctionalInterface\npublic interface Function&lt;T, R&gt; {\n    R apply(T var1);\n\n    default &lt;V&gt; Function&lt;V, R&gt; compose(Function&lt;? super V, ? extends T&gt; before) {\n        Objects.requireNonNull(before);\n        return (v) -&gt; {\n            return this.apply(before.apply(v));\n        };\n    }\n\n    default &lt;V&gt; Function&lt;T, V&gt; andThen(Function&lt;? super R, ? extends V&gt; after) {\n        Objects.requireNonNull(after);\n        return (t) -&gt; {\n            return after.apply(this.apply(t));\n        };\n    }\n\n    static &lt;T&gt; Function&lt;T, T&gt; identity() {\n        return (t) -&gt; {\n            return t;\n        };\n    }\n}\n</code></pre>\n<p>java 本身没有 Functor 类，但存在<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html\">Optional 类</a>作为容器类型。Stream 包中也存在其它的容器，但存储流而非单个值了。</p>\n<h1>java.util.concurrent</h1>\n<h2>Callable<V> Future FutureTask</h2>\n<pre><code class=\"language-java\">@FunctionalInterface\npublic interface Callable&lt;V&gt; {\n    V call() throws Exception;\n}\n</code></pre>\n<pre><code class=\"language-java\">public interface Future&lt;V&gt; {\n    boolean cancel(boolean var1);\n    boolean isCancelled();\n    boolean isDone();\n    V get() throws InterruptedException, ExecutionException;\n    V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException;\n}\n</code></pre>\n<pre><code class=\"language-java\">FutureTask task = new FutureTask((Callable&lt;Integer&gt;) () -&gt; 123);\nThread t1 = new Thread(task);\nt1.start();\nSystem.out.println(task.get()); // 123\n</code></pre>\n<h2>CompletableFuture</h2>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html</a></p>\n","tags":["java"]},{"id":"download-apk","url":"https://yieldray.fun/posts/download-apk","title":"apk下载","date_published":"2022-09-29T22:22:22.000Z","date_modified":"2022-09-29T22:22:22.000Z","content_text":"<p>记录一些直接下载 apk 的网站</p>\n<h1>overwall</h1>\n<table>\n<thead>\n<tr>\n<th>url</th>\n<th>历史版本</th>\n<th>中文搜索</th>\n<th>APK Variants</th>\n<th>是否 play 源</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://apkpure.com/cn/\">https://apkpure.com/cn/</a></td>\n<td>✅</td>\n<td>✅</td>\n<td>❌</td>\n<td>play 同步 ，且包含其它源</td>\n</tr>\n<tr>\n<td><a href=\"https://www.apkmirror.com/\">https://www.apkmirror.com/</a></td>\n<td>✅</td>\n<td>❌</td>\n<td>✅</td>\n<td>不全，但包含 f-droid 和 github 源</td>\n</tr>\n<tr>\n<td><a href=\"https://apkcombo.com/zh/\">https://apkcombo.com/zh/</a></td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n<td>包含，且含其它源</td>\n</tr>\n<tr>\n<td><a href=\"https://androidappsapk.co/\">https://androidappsapk.co/</a></td>\n<td>无法下载</td>\n<td>✅</td>\n<td>✅</td>\n<td>play 同步</td>\n</tr>\n<tr>\n<td><a href=\"https://cn.aptoide.com/\">https://cn.aptoide.com/</a></td>\n<td>✅</td>\n<td>✅</td>\n<td>❌</td>\n<td>大致同步谷歌源</td>\n</tr>\n<tr>\n<td><a href=\"https://1mobilemarket.net/\">https://1mobilemarket.net/</a></td>\n<td>❌</td>\n<td>✅</td>\n<td>❌</td>\n<td>不全，但包含其它源</td>\n</tr>\n<tr>\n<td><a href=\"https://freesoft.ru/\">https://freesoft.ru/</a></td>\n<td>✅</td>\n<td>❌</td>\n<td>❌</td>\n<td>未知，包含其它平台</td>\n</tr>\n<tr>\n<td><a href=\"https://www.9apps.com/\">https://www.9apps.com/</a></td>\n<td>✅</td>\n<td>✅</td>\n<td>❌</td>\n<td>差</td>\n</tr>\n</tbody></table>\n<p>根据包名下载（play）：<br><a href=\"https://apps.evozi.com/apk-downloader/\">https://apps.evozi.com/apk-downloader/</a><br><a href=\"https://androidappsapk.co/apkdownloader/\">https://androidappsapk.co/apkdownloader/</a></p>\n<p>源：<br><a href=\"https://play.google.com/\">https://play.google.com/</a><br><a href=\"https://f-droid.org/packages/\">https://f-droid.org/packages/</a></p>\n<h1>cn</h1>\n<p>仅记录可以下载、可以搜索的</p>\n<table>\n<thead>\n<tr>\n<th>url</th>\n<th>历史版本</th>\n<th>备注</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://www.wandoujia.com/\">https://www.wandoujia.com/</a></td>\n<td>✅</td>\n<td></td>\n</tr>\n<tr>\n<td><a href=\"https://sj.qq.com/\">https://sj.qq.com/</a></td>\n<td>❌</td>\n<td></td>\n</tr>\n<tr>\n<td><a href=\"https://3g.lenovomm.com/\">https://3g.lenovomm.com/</a></td>\n<td>❌</td>\n<td>仅移动端</td>\n</tr>\n<tr>\n<td><a href=\"https://www.shouji.com.cn/soft.html\">https://www.shouji.com.cn/soft.html</a></td>\n<td>✅</td>\n<td>搜索残废</td>\n</tr>\n</tbody></table>\n<h1>mod</h1>\n<p>乱七八糟的图个乐</p>\n<p><a href=\"https://apkmodyes.com/\">https://apkmodyes.com/</a><br><a href=\"https://apkmods.pk/\">https://apkmods.pk/</a><br><a href=\"https://apkmod.fr/\">https://apkmod.fr/</a><br><a href=\"https://apkmody.io/\">https://apkmody.io/</a><br><a href=\"http://apkmodi.io/\">http://apkmodi.io/</a><br><a href=\"https://apkmodyz.com/\">https://apkmodyz.com/</a><br><a href=\"https://apkmody.link/\">https://apkmody.link/</a><br><a href=\"https://tezapk.com/\">https://tezapk.com/</a><br><a href=\"https://apkmod.me/\">https://apkmod.me/</a><br><a href=\"https://apkpik.com/\">https://apkpik.com/</a><br><a href=\"https://takemod.com/\">https://takemod.com/</a><br><a href=\"https://www.apkmody.club/\">https://www.apkmody.club/</a></p>\n","tags":["note"]},{"id":"php-intro","url":"https://yieldray.fun/posts/php-intro","title":"php入门","date_published":"2022-09-20T16:16:16.000Z","date_modified":"2022-09-20T16:16:16.000Z","content_text":"<p>下面会省略一些 php8 的特性，虽然 php8 对语言的改进还是比较大的<br><a href=\"https://www.php.net/releases/8.1/zh.php\">https://www.php.net/releases/8.1/zh.php</a></p>\n<h1>php 安装</h1>\n<p>unix 下可以使用包管理器安装，或使用 lamp、lnmp 等一键包安装<br>手动安装参考文档：<a href=\"https://www.php.net/manual/zh/install.php\">https://www.php.net/manual/zh/install.php</a><br>windows 也可以使用一键包：<a href=\"https://www.apachefriends.org/zh_cn/index.html\">https://www.apachefriends.org/zh_cn/index.html</a>（同时支持 linux、macOS）<br>手动安装在此处下载预编译文件：<a href=\"https://windows.php.net/download\">https://windows.php.net/download</a></p>\n<p>一键包一般自带 http 服务器和数据库，但 php 本身也携带了一个轻量 http 服务器<br>为了简单下面会使用自带的 http 服务器</p>\n<p>php 手册：<a href=\"https://www.php.net/manual/zh/\">https://www.php.net/manual/zh/</a><br>php 语言参考：<a href=\"https://www.php.net/manual/zh/langref.php\">https://www.php.net/manual/zh/langref.php</a><br>php 函数参考：<a href=\"https://www.php.net/manual/zh/funcref.php\">https://www.php.net/manual/zh/funcref.php</a></p>\n<h1>composer 安装</h1>\n<p>composer 是 php 的包管理器</p>\n<p>phar 即 Php ARchive，与 java 的 jar 是类似的（不过 php 无需编译），可以将 php 源码和其它资源打包<br>composer 通过 phar 分发</p>\n<p>在 windows 下使用安装程序或在 unix 下使用脚本安装时，参见 composer 下载页面：<a href=\"https://getcomposer.org/download/\">https://getcomposer.org/download/</a><br>手动安装时，要先在 composer 下载页面下载 <code>composer.phar</code><br>然后参考文档即可：<a href=\"https://getcomposer.org/doc/00-intro.md\">https://getcomposer.org/doc/00-intro.md</a></p>\n<h1>命令行</h1>\n<pre><code class=\"language-sh\"># 查看帮助\nphp -h\n\n# 可以用以下命令做一些简单测试\nphp &lt;file&gt;                 # 执行php文件\nphp -S 0.0.0.0:8080 &lt;file&gt; # 启动开发服务器\nphp -r &lt;code&gt;              # 执行php代码\n</code></pre>\n<h1>数据类型与基本输入输出</h1>\n<p>PHP 支持 10 种原始数据类型：\n标量类型 bool int float string<br>复合类型 array object callable iterable<br>特殊类型 resource NULL</p>\n<pre><code class=\"language-php\">&lt;?php\n$a_bool = true;\n$a_str  = &quot;foo&quot;;\n$an_int = 12;\n\n$tmp = 23.4;\nunset($tmp);\n\n// 基本输出\necho gettype($a_str); // =&gt; string\nprint($a_str); // =&gt; foo\n\n// 类型检测\nif (is_int($an_int)) $an_int += 4;\nif (is_string($a_bool)) echo &quot;String: $a_bool&quot;;\n\n// 类型转换\n$str = (string) $an_int;\n$foo = &quot;5bar&quot;;\nsettype($foo, &quot;integer&quot;);\necho gettype($foo).&quot;$foo&quot;; // =&gt; integer5\n\n// 打印变量的相关信息\nvar_dump($var);\nvar_export($var);\nprint_r($var);\n\n// 可变变量（通过字符串变量来获取指定变量）\n$a = &quot;hello&quot;;\n$$a = &quot;world&quot;; // 相当于 $hello = &quot;world&quot;\n${$a} = &quot;world&quot;; // 为了避免误解，可以这样写\n\n// 常量\ndefine(&quot;FOO&quot;, &quot;something&quot;);   // 全局常量\nconst BAR = &quot;something else&quot;; // 作用域常量\n\n// 数组\n$arr = array(1,&quot;2&quot;,3);\n$arr = [1,&quot;2&quot;,3];\n$arr = [11=&gt;&quot;1&quot;, &quot;12&quot;=&gt;2, &quot;key&quot;=&gt;&quot;value&quot;];\necho $arr[11];   // =&gt; 1\necho $arr[&quot;11&quot;]; // =&gt; 1\necho count($arr); // =&gt; 3\nforeach ($arr as $key) {\n    echo &quot;$key\\n&quot;;\n}\nforeach ($arr as $key =&gt; $value) {\n    echo &quot;$key = $value\\n&quot;;\n}\n</code></pre>\n<h1>函数</h1>\n<pre><code class=\"language-php\">&lt;?php\nfunction add($a, $b = 0) {\n    return $a + $b;\n}\necho add(1,2);\necho add(...[1,2]);\n\nfunction foo(...$args){\n    return $args;\n}\n$bar = &quot;foo&quot;;\n[$x, $y, $z] = $bar(1, 2, 3);\n// 相当于\n$bar = function foo(...$args){\n    return $args;\n}\n\n// 箭头函数\n$y = 1;\n$fn1 = fn($x) =&gt; $x + $y;\n$fn2 = function ($x) use ($y) {\n    // 相当于通过 value 使用 $y：\n    return $x + $y;\n};\n</code></pre>\n<h1>引入其它脚本</h1>\n<pre><code class=\"language-php\">&lt;?php\ninclude &#39;vars.php&#39;;\nrequire(&#39;somefile.php&#39;); // 推荐使用下面的形式\nrequire &#39;somefile.php&#39;;\nrequire_once(&quot;config.php&quot;);\nrequire_once &quot;config.php&quot;;\ninclude_once(&quot;config.php&quot;);\ninclude_once &quot;config.php&quot;;\n</code></pre>\n<h1>流程控制与函数</h1>\n<p>流程控制与 c 语言类似，并且支持 goto<br>函数与 js 的函数声明类似</p>\n<h1>面向对象</h1>\n<pre><code class=\"language-php\">&lt;?php\n// 类声明\nclass SimpleClass\n{\n    function __construct() {/* ... */}\n    // 默认 public\n    public $foo = &quot;foo&quot;;\n    protected $bar = &quot;bar&quot;;\n    private $hide = 23.4;\n    static $str = &quot;hello&quot;;\n    const CONSTANT = &#39;constant value&#39;;\n\n    public function displayVar() {\n        echo $this-&gt;var;\n    }\n\n    function showConstant() {\n        echo self::CONSTANT;\n    }\n}\n\n// 继承\nclass SubClass extends SimpleClass {\n    function __construct() {\n        parent::__construct();\n    }\n}\n\n// 抽象类\nabstract class AbstractClass\n{\n // 强制要求子类定义这些方法\n    abstract protected function getValue();\n    abstract protected function prefixValue($prefix);\n    // 普通方法（非抽象方法）\n    public function printOut() {\n        print $this-&gt;getValue();\n    }\n}\n\n// 接口\ninterface Template\n{\n    public function setVariable($name, $var);\n    public function getHtml($template);\n}\nclass WorkingTemplate implements Template {/* ... */}\n?&gt;\n\n// trait\ntrait ezcReflectionReturnInfo {\n    function getReturnType() { /*1*/ }\n    function getReturnDescription() { /*2*/ }\n}\nclass ezcReflectionMethod extends ReflectionMethod {\n    use ezcReflectionReturnInfo;\n    /* ... */\n}\n</code></pre>\n<h1>命名空间</h1>\n<pre><code class=\"language-php\">&lt;?php\nnamespace my\\name;\nclass MyClass {}\nfunction myfunction() {}\nconst MYCONST = 1;\n</code></pre>\n<pre><code class=\"language-php\">&lt;?php\nnamespace my\\name{\n    class MyClass {}\n    function myfunction() {}\n    const MYCONST = 1;\n}\n</code></pre>\n<pre><code class=\"language-php\">&lt;?php\n\\my\\name::myfunction();\nmy\\name::myfunction(); // 第一个反斜线代表全局空间，这里可省略\n\nuse my\\name\\myfunction;\nmyfunction();\n\nuse my\\name\\myfunction as myfn;\nmyfn();\n</code></pre>\n<h1>异常</h1>\n<pre><code class=\"language-php\">&lt;?php\nfunction divide($a, $b) {\n    if($b===0) throw new Exception(&quot;分母不能为0&quot;);\n    return $a / $b;\n}\n\ntry {\n    divide(2,0);\n}\ncatch(Exception $e) {\n    echo $e-&gt;getMessage();\n}\n?&gt;\n</code></pre>\n<p>PHP 5 错误报告机制</p>\n<pre><code class=\"language-php\">&lt;?php\nerror_function(error_level,error_message,error_file,error_line,error_context);\ntrigger_error(error_message,error_types);\nfunction customErrorHandler($errno, $errstr){}\nset_error_handler(&quot;customErrorHandler&quot;);\n</code></pre>\n<h1>引用</h1>\n<p><a href=\"https://www.php.net/manual/zh/language.references.php\">https://www.php.net/manual/zh/language.references.php</a></p>\n<pre><code class=\"language-php\">&lt;?php\n$a = &amp;$b;\n\nfunction foo(&amp;$var) {\n    $var++;\n}\n\nfunction &amp;bar() {\n    $a = 5;\n    return $a;\n}\n\nfoo(bar());\n?&gt;\n</code></pre>\n<h1>PHP 8</h1>\n<p>类型声明<a href=\"https://www.php.net/manual/zh/language.types.declarations.php\">https://www.php.net/manual/zh/language.types.declarations.php</a><br>注解<a href=\"https://www.php.net/manual/zh/language.attributes.php\">https://www.php.net/manual/zh/language.attributes.php</a><br>枚举<a href=\"https://www.php.net/manual/zh/language.enumerations.php\">https://www.php.net/manual/zh/language.enumerations.php</a><br>纤程<a href=\"https://www.php.net/manual/zh/language.fibers.php\">https://www.php.net/manual/zh/language.fibers.php</a></p>\n","tags":["php"]},{"id":"web-socket","url":"https://yieldray.fun/posts/web-socket","title":"Web Socket","date_published":"2022-09-19T09:19:19.000Z","date_modified":"2022-09-19T09:19:19.000Z","content_text":"<h1>Web Socket</h1>\n<p>web socket 基于 TCP （通过 http 进行握手），可以在浏览器中建立与服务器（可跨域）的双向通信<br>协议标识符为 ws，默认端口为 80（wss 为 443）</p>\n<p>学习参考：<a href=\"https://github.com/Pines-Cheng/blog/issues/37\">https://github.com/Pines-Cheng/blog/issues/37</a></p>\n<h1>browser</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/WebSocket\">https://developer.mozilla.org/docs/Web/API/WebSocket</a><br><a href=\"https://zh.javascript.info/websocket\">https://zh.javascript.info/websocket</a><br><a href=\"https://wangdoc.com/webapi/websocket.html\">https://wangdoc.com/webapi/websocket.html</a></p>\n<h1>node.js</h1>\n<p>node.js 的 <a href=\"https://github.com/websockets/ws\">ws</a> 库实现了 WebSocket 服务器和 WebSocket 客户端（WebSocket API）<br>文档：<a href=\"https://github.com/websockets/ws/blob/master/doc/ws.md\">https://github.com/websockets/ws/blob/master/doc/ws.md</a></p>\n<h1>deno</h1>\n<p>deno 标准库实现了 <a href=\"https://deno.land/api?s=WebSocket\">WebSocket API</a></p>\n<h1>Socket.IO</h1>\n<p><a href=\"https://socket.io/\">Socket.IO</a> 是一个实现浏览器和服务器之间实时、双向和基于事件的通信的库。（也有其它语言实现的客户端和服务端）<br>Socket.IO 不是 WebSocket 实现。它是对 WebSocket 协议的封装，提供了很多实用功能。<br>Features: HTTP long-polling fallback, Automatic reconnection, Packet buffering, Acknowledgements, Broadcasting, Multiplexing</p>\n","tags":["web-api"]},{"id":"tauri-intro","url":"https://yieldray.fun/posts/tauri-intro","title":"tauri入门","date_published":"2022-09-16T14:14:14.000Z","date_modified":"2022-09-16T14:14:14.000Z","content_text":"<p><a href=\"https://tauri.app\">https://tauri.app</a></p>\n<h1>安装</h1>\n<p>首先安装 rust 和 nodejs，使用 vscode 开发时，安装 rust-analyzer 和 tauri 插件</p>\n<p>这里使用官方脚手架来创建项目</p>\n<pre><code class=\"language-sh\">npm create tauri-app\n</code></pre>\n<p>如果 UI 选择 vanilla 那么前端是没有配置构建和开发环境的<br>为了方便这里选择 vue，会自动配置 vite 并且将前端项目的开发服务器与 tauri 项目的开发环境结合起来。（否则需要手动配置）</p>\n<pre><code>✔ Project name · tauri-app\n✔ Choose your package manager · npm\n✔ Choose your UI template · vue\n\nDone, Now run:\n  cd tauri-app\n  npm install\n  npm run tauri dev\n</code></pre>\n<blockquote>\n<p>如果不使用脚手架，顺序是先用 npm 创建前端项目（如： vite，create-react-app）<br>安装 tauri 命令行工具 <code>npm install --save-dev @tauri-apps/cli</code><br>然后通过命令行工具来初始化 <code>npm tauri init</code><br>具体操作，参见文档</p>\n</blockquote>\n<p>下面还是基于脚手架，脚手架配置的 script 字段如下：</p>\n<pre><code class=\"language-json\">{\n    &quot;type&quot;: &quot;module&quot;,\n    &quot;scripts&quot;: {\n        &quot;dev&quot;: &quot;vite&quot;,\n        &quot;build&quot;: &quot;vite build&quot;,\n        &quot;preview&quot;: &quot;vite preview&quot;,\n        &quot;tauri&quot;: &quot;tauri&quot;\n    }\n}\n</code></pre>\n<p>vite 相关的命令仅用于开发前端，整个 tauri 项目的开发通过 tauri 命令。</p>\n<pre><code>USAGE:\n    npm run tauri [OPTIONS] &lt;SUBCOMMAND&gt;\n\nOPTIONS:\n    -h, --help       Print help information\n    -v, --verbose    Enables verbose logging\n    -V, --version    Print version information\n\nSUBCOMMANDS:\n    build     Tauri build\n    dev       Tauri dev\n    help      Print this message or the help of the given subcommand(s)\n    icon      Generates various icons for all major platforms\n    info      Shows information about Tauri dependencies and project configuration\n    init      Initializes a Tauri project\n    plugin    Manage Tauri plugins\n    signer    Tauri updater signer\n</code></pre>\n<h1>项目目录</h1>\n<pre><code>+---package.json\n+---vite.config.js   -|\n+---index.html        |\n+---public/            ╲\n+---dist/              ╱\n+---src/              |\n|   +---App.vue      -|\n|---src-tauri/\n    +---cargo.toml         Cargo\n    +---tauri.conf.json    项目总体配置 https://tauri.app/zh/v1/api/config/\n    +---build.rs           构建配置\n    +---icons/             图标资源\n    +---src/\n        +---main.rs        项目初始化配置（读取`tauri.conf.json`）\n</code></pre>\n<h1>API</h1>\n<h2>前端 API （@tauri-apps/api）</h2>\n<p><a href=\"https://tauri.app/zh/v1/api/js/\">https://tauri.app/zh/v1/api/js/</a><br>可以看到有以下模块</p>\n<pre><code>app\ncli\nclipboard\ndialog\nevent\nfs\nglobalShortcut\nhttp\nmocks\nnotification\nos\npath\nprocess\nshell\ntauri\nupdater\nwindow\n</code></pre>\n<p>像这样导入</p>\n<pre><code class=\"language-js\">import tauri from &quot;@tauri-apps/api&quot;;\nimport { invoke } from &quot;@tauri-apps/api/tauri&quot;;\n</code></pre>\n<p>某些 tauri 模块也可以通过全局对象拿到（需要 <code>tauri.conf.json</code> 配置文件中 <code>build.withGlobalTauri</code> 设为 true）<br>全局对象应该仅在 vanilla 模板下使用，vite 模板是默认设置为 false 的</p>\n<pre><code class=\"language-js\">const tauri = window.__TAURI__.tauri;\n</code></pre>\n<h2>后端 API （rust）</h2>\n<p><a href=\"https://docs.rs/tauri/latest/tauri/index.html\">https://docs.rs/tauri/latest/tauri/index.html</a></p>\n<p>主要是通过此文件 <code>src-tauri/src/main.rs</code><br>通过 <code>tauri::Builder</code> 配置整个项目（默认配置文件<code>tauri.conf.json</code>也是由此引入）<br><a href=\"https://docs.rs/tauri/latest/tauri/struct.Builder.html\">https://docs.rs/tauri/latest/tauri/struct.Builder.html</a></p>\n<h1>前端调用后端</h1>\n<p><a href=\"https://tauri.app/v1/guides/features/command/\">https://tauri.app/v1/guides/features/command/</a></p>\n<p>被调用的 rust 函数定义于 <code>src-tauri/src/main.rs</code>，并且需要在主函数中注册</p>\n<pre><code class=\"language-rs\">#[tauri::command] // 在这里声明函数\nfn my_custom_command(invoke_message: String) -&gt; String {\n    format!(\n        &quot;I was invoked from JS, with this message: {}&quot;,\n        invoke_message\n    )\n}\nfn main() {\n    tauri::Builder::default()\n        .invoke_handler(tauri::generate_handler![my_custom_command]) // 在这里注册函数\n        // 注册多个函数： invoke_handler(tauri::generate_handler![cmd_a, cmd_b])\n        .run(tauri::generate_context!())\n        .expect(&quot;error while running tauri application&quot;);\n}\n</code></pre>\n<p>在前端调用时，通过一个对象来指定参数（注意下划线命名与小驼峰命名的转换），并且函数返回的是一个 Promise<br>如果我们在 rust 中注册一个 async 函数，由于前端本身就返回 Promise，所以调用方法完全一致。</p>\n<pre><code class=\"language-js\">invoke(&quot;my_custom_command&quot;, { invokeMessage: &quot;Hello!&quot; }).then((message) =&gt; console.log(message));\n</code></pre>\n<p>错误处理很简单，在 rust 中返回 Result 即可，<code>Result&lt;T,U&gt;</code> 的 T 和 U 对应 <code>Promise</code> 的 then 和 catch</p>\n<pre><code class=\"language-rust\">#[tauri::command]\nfn my_custom_command() -&gt; Result&lt;String, String&gt; {\n  Err(&quot;This failed!&quot;.into())\n  Ok(&quot;This worked!&quot;.into())\n}\n</code></pre>\n<h1>前后端事件通信</h1>\n<p><a href=\"https://tauri.app/v1/guides/features/events\">https://tauri.app/v1/guides/features/events</a></p>\n<h1>图标配置</h1>\n<p><a href=\"https://tauri.app/v1/guides/features/icons\">https://tauri.app/v1/guides/features/icons</a></p>\n<p>tauri 提供了命令来创建图标，无需手动适配</p>\n<pre><code>npm run tauri icon\n</code></pre>\n<h1>窗口菜单</h1>\n<p><a href=\"https://tauri.app/v1/guides/features/menu\">https://tauri.app/v1/guides/features/menu</a></p>\n<h1>多窗口</h1>\n<p><a href=\"https://tauri.app/v1/guides/features/multiwindow\">https://tauri.app/v1/guides/features/multiwindow</a></p>\n","tags":["rust"]},{"id":"nodejs-npm-init","url":"https://yieldray.fun/posts/nodejs-npm-init","title":"npm初始化命令","date_published":"2022-09-15T11:11:11.000Z","date_modified":"2022-09-15T11:11:11.000Z","content_text":"<h1>npm init / npm create</h1>\n<p><code>npm init</code> 和 <code>npm create</code> 是等效的！</p>\n<p>v8 的文档可能有些问题，下面是 v7 的描述</p>\n<pre><code class=\"language-sh\">npm init [--yes|-y|--scope]\nnpm init &lt;@scope&gt; (same as `npm exec &lt;@scope&gt;/create`)\nnpm init [&lt;@scope&gt;/]&lt;name&gt; (same as `npm exec [&lt;@scope&gt;/]create-&lt;name&gt;`)\nnpm init [-w &lt;dir&gt;] [args...]\n</code></pre>\n<p><a href=\"https://docs.npmjs.com/cli/v8/commands/npm-exec\"><code>npm exec</code></a> 类似于 <code>npx</code>，但是 <code>npm exec</code> 会先把选项解释为传给 <code>npm</code> 的选项<br>应该是学习了 yarn 的 <code>yarn create</code> 命令，主要是方便使用一些脚手架</p>\n<pre><code class=\"language-sh\">npm create react-app my-app\n# npx create-react-app my-app\n\nnpm init vue@3\nnpm create vue@3\n# npx create-vue\n</code></pre>\n","tags":["npm","node.js"]},{"id":"js-esbuild-intro","url":"https://yieldray.fun/posts/js-esbuild-intro","title":"esbuild入门","date_published":"2022-09-14T14:14:14.000Z","date_modified":"2022-09-14T14:14:14.000Z","content_text":"<p>文档：<a href=\"https://esbuild.github.io/\">https://esbuild.github.io/</a><br>中文：<a href=\"https://esbuild.docschina.org/\">https://esbuild.docschina.org/</a></p>\n<h1>esbuild</h1>\n<p>esbuild 提供了打包 js 的 API，可以通过命令行、js 和 go 调用<br>esbuild 同时支持 esm 和 cjs<br>esbuild 内置支持 typescript 和代码压缩<br>esbuild 内置功能都是 go 写的，因此速度快</p>\n<h1>使用</h1>\n<p>在 esbuild 中，提供的三种 API 几乎是一致的（这里忽略 go）</p>\n<pre><code class=\"language-sh\">esbuild app.jsx --bundle --outfile=out.js\n</code></pre>\n<pre><code class=\"language-js\">require(&quot;esbuild&quot;)\n    .build({\n        entryPoints: [&quot;app.jsx&quot;],\n        bundle: true,\n        outfile: &quot;out.js&quot;,\n    })\n    .catch(() =&gt; process.exit(1));\n</code></pre>\n<h1>浏览器</h1>\n<p>命令行 API 是相似的，下面就忽略了</p>\n<pre><code class=\"language-js\">const { build, buildSync } = require(&quot;esbuild&quot;);\nbuildSync({\n    entryPoints: [&quot;./src/index.jsx&quot;],\n    target: [&quot;chrome58&quot;, &quot;firefox57&quot;, &quot;safari11&quot;, &quot;edge16&quot;],\n    external: [], // 需要排除打包的依赖列表\n    format: &quot;esm&quot;, // 支持 esm commonjs iife\n    outdir: &quot;dist&quot;,\n    bundle: true,\n    splitting: true, // 是否开启自动拆包\n    minify: true,\n    sourcemap: true,\n    metafile: true, // 是否生成打包的元信息文件\n    watch: false,\n    write: true, // 是否将产物写入磁盘\n    loader: {\n        &quot;.png&quot;: &quot;base64&quot;,\n        &quot;.jpg&quot;: &quot;file&quot;,\n    },\n});\n</code></pre>\n<h1>node</h1>\n<pre><code class=\"language-js\">buildSync({\n    entryPoints: [&quot;app.js&quot;],\n    bundle: true,\n    platform: &quot;node&quot;,\n    target: [&quot;node10.4&quot;],\n    external: [&quot;fsevents&quot;],\n    outfile: &quot;out.js&quot;,\n});\n</code></pre>\n","tags":["js","lib"]},{"id":"js-rollup","url":"https://yieldray.fun/posts/js-rollup","title":"rollup入门","date_published":"2022-09-13T13:13:13.000Z","date_modified":"2022-09-13T13:13:13.000Z","content_text":"<p>文档：<a href=\"https://cn.rollupjs.org/introduction/\">https://cn.rollupjs.org/introduction/</a></p>\n<h1>命令行</h1>\n<pre><code class=\"language-sh\">npm install --global rollup\n</code></pre>\n<p>打包成各种格式：</p>\n<pre><code class=\"language-sh\">rollup main.js --file bundle.js --format iife\nrollup main.js --file bundle.js --format cjs\nrollup main.js --file bundle.js --format umd --name &quot;myBundle&quot;\n\nrollup m1.js m2.js --dir dist\n</code></pre>\n<h1>配置文件</h1>\n<pre><code class=\"language-json\">{\n    &quot;scripts&quot;: {\n        &quot;build&quot;: &quot;rollup -c&quot;\n    }\n}\n</code></pre>\n<p><code>--config</code> 默认使用 <code>rollup.config.js</code> 配置文件</p>\n<pre><code class=\"language-js\">import pkg from &quot;./package.json&quot; assert { type: &quot;json&quot; };\nimport resolve from &quot;@rollup/plugin-node-resolve&quot;; // 此插件提供查找外部模块的功能（即 node_modules 中的模块）\nimport commonjs from &quot;@rollup/plugin-commonjs&quot;; // 此插件提供转换 commonjs 为 es6 模块的功能\n\nexport default {\n    input: &quot;./src/main.js&quot;,\n    output: [\n        {\n            file: &quot;./dist/main.esm.js&quot;,\n            format: &quot;es&quot;,\n        },\n        {\n            file: &quot;./dist/main.umd.js&quot;,\n            format: &quot;umd&quot;,\n            name: &quot;main&quot;,\n        },\n    ],\n    plugins: [resolve(), commonjs()],\n};\n</code></pre>\n<p>使用 typescript 编写配置文件</p>\n<pre><code class=\"language-sh\">rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript\n</code></pre>\n<p>声明配置文件环境变量</p>\n<pre><code class=\"language-sh\">rollup -c --environment INCLUDE_DEPS,BUILD:production\n</code></pre>\n<p>配置文件将接收到 <code>process.env.INCLUDE_DEPS === &#39;true&#39;</code> 和 <code>process.env.BUILD === &#39;production&#39;</code></p>\n<h1><a href=\"https://cn.rollupjs.org/plugin-development/\">插件开发</a></h1>\n<p>Rollup 插件中，我们声明一系列钩子，在模块构建的各种步骤中调用，支持异步。</p>\n<pre><code class=\"language-js\">import { CustomPluginOptions, LoadResult, Plugin, PluginContext, ResolveIdResult } from &quot;rollup&quot;;\n\nexport default function myExamplePlugin(): Plugin {\n    return {\n        name: &quot;my-example-plugin&quot;, // 此名称将出现在警告和错误中\n        // rollup 还支持 resolveDynamicImport 钩子来解析动态导入（即 await import()）\n        resolveId(\n            // https://cn.rollupjs.org/plugin-development/#resolveid\n            this: PluginContext,\n            // source 就是 import xxx from 后面的字符串（即被导入的模块路径）\n            // 这个钩子用于将 source 解析为 ID\n            source: string,\n            // importer 就是使用 import xxx from xxx 语句的这个模块被 resolveId 得到的 ID\n            // 在解析入口点时，importer 通常为 undefined\n            importer: string | undefined,\n            options: {\n                attributes: Record&lt;string, string&gt;;\n                custom?: CustomPluginOptions;\n                isEntry: boolean;\n            }\n        ): ResolveIdResult {\n            if (source === &quot;virtual-module&quot;) {\n                // 这表示 rollup 不应询问其他插件或\n                // 从文件系统检查以找到此 ID\n                return source;\n            }\n            return null; // 返回 null，则会转而使用其他 resolveId 函数，最终使用默认的解析行为\n        },\n        load(\n            // https://cn.rollupjs.org/plugin-development/#load\n            this: PluginContext,\n            // id 就是 resolveId 得到的 ID（未 resolveId 则保持 source 原样）\n            id: string\n        ): LoadResult {\n            if (id === &quot;virtual-module&quot;) {\n                // &quot;virtual-module&quot; 的源代码\n                return &#39;export default &quot;This is virtual!&quot;&#39;;\n            }\n            return null; // 其他ID应按通常方式处理\n        },\n    };\n}\n</code></pre>\n<pre><code class=\"language-ts\">import { defineConfig } from &quot;rollup&quot;;\nimport myExample from &quot;./rollup-plugin-my-example&quot;;\n\nexport default defineConfig({\n    input: &quot;virtual-module&quot;, // 由我们的插件解析\n    plugins: [myExample()],\n    output: [\n        {\n            file: &quot;bundle.js&quot;,\n            format: &quot;es&quot;,\n        },\n    ],\n});\n</code></pre>\n<p>运行 <code>npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript</code></p>\n<p>则会生成 bundle.js</p>\n<pre><code class=\"language-js\">var virtualModule = &quot;This is virtual!&quot;;\n\nexport { virtualModule as default };\n</code></pre>\n<hr>\n<p>实例（摘自文档）</p>\n<pre><code class=\"language-js\">function svgResolverPlugin() {\n    return {\n        name: &quot;svg-resolver&quot;,\n        resolveId(source, importer) {\n            if (source.endsWith(&quot;.svg&quot;)) {\n                return path.resolve(path.dirname(importer), source);\n            }\n        },\n        load(id) {\n            if (id.endsWith(&quot;.svg&quot;)) {\n                const referenceId = this.emitFile({\n                    type: &quot;asset&quot;,\n                    name: path.basename(id),\n                    source: fs.readFileSync(id),\n                    needsCodeReference: true,\n                });\n                return `export default import.meta.ROLLUP_FILE_URL_${referenceId};`;\n            }\n        },\n    };\n}\n</code></pre>\n","tags":["js","lib"]},{"id":"js-webpack-intro","url":"https://yieldray.fun/posts/js-webpack-intro","title":"webpack入门","date_published":"2022-09-12T12:12:12.000Z","date_modified":"2022-09-12T12:12:12.000Z","content_text":"<p>webpack 的核心是一个 js 打包器，也可以打包其它前端资源</p>\n<p>文档：<a href=\"https://webpack.js.org/concepts/\">https://webpack.js.org/concepts/</a><br>中文：<a href=\"https://webpack.docschina.org/concepts/\">https://webpack.docschina.org/concepts/</a><br>参考：<a href=\"http://webpack.wuhaolin.cn/\">http://webpack.wuhaolin.cn/</a></p>\n<h1>安装</h1>\n<pre><code class=\"language-sh\">npm install webpack webpack-cli --save-dev\n# or\nnpm i -D webpack webpack-cli\n</code></pre>\n<p><code>package.json</code> 一般如下配置</p>\n<pre><code class=\"language-json\">{\n    &quot;private&quot;: true,\n    &quot;scripts&quot;: {\n        &quot;build&quot;: &quot;webpack&quot;,\n        &quot;watch&quot;: &quot;webpack --watch&quot;\n    }\n}\n</code></pre>\n<p>一般有如下目录结构</p>\n<pre><code>|-- src/\n|-- dist/\n|-- package.json\n|-- webpack.config.js\n</code></pre>\n<h1>重要概念</h1>\n<h2>Entry &amp; Output &amp; Chunk</h2>\n<p>entry 指定模块的入口（可以是一个文件，也可以是多个）<br>output 指定输出</p>\n<pre><code class=\"language-js\">const path = require(&quot;path&quot;);\nmodule.exports = {\n    // context: path.resolve(__dirname, &quot;app&quot;),\n    entry: &quot;./src/index.js&quot;,\n    entry: {\n        index: &quot;./src/index.js&quot;,\n        print: &quot;./src/print.js&quot;,\n    },\n    output: {\n        // filename: &quot;bundle.js&quot;,\n        filename: &quot;[name].bundle.js&quot;,\n        path: path.resolve(__dirname, &quot;dist&quot;), // 指定输出文件目录\n        // clear: true,\n    },\n};\n</code></pre>\n<p>chunk 根据 entry 和动态导入 <code>import()</code> 得到，一个 chunk 对应一个生成的文件</p>\n<h2>Module &amp; Loader</h2>\n<p>module 配置如何处理模块<br>loader 则是模块转换器</p>\n<pre><code class=\"language-js\">module.exports = {\n    /* ... ... */\n    module: {\n        rules: [\n            {\n                test: /\\.js$/,\n                use: [&quot;babel-loader?cacheDirectory&quot;],\n                include: path.resolve(__dirname, &quot;src&quot;),\n            },\n            {\n                test: /\\.css$/i,\n                use: [&quot;style-loader&quot;, &quot;css-loader&quot;],\n            },\n            {\n                test: /\\.scss$/,\n                use: [&quot;style-loader&quot;, &quot;css-loader&quot;, &quot;sass-loader&quot;],\n                exclude: path.resolve(__dirname, &quot;node_modules&quot;),\n            },\n            {\n                test: /\\.(png|svg|jpg|jpeg|gif)$/i,\n                type: &quot;asset/resource&quot;,\n            },\n        ],\n    },\n};\n</code></pre>\n<h2>Plugin</h2>\n<p>plugin 可插入 webpack 的各种生命周期，因此可以实现各种功能<br>每个 plugin 的配置则与具体插件相关</p>\n<p>插件需要安装，例如 <code>npm install --save-dev html-webpack-plugin</code></p>\n<pre><code class=\"language-js\">const HtmlWebpackPlugin = require(&quot;html-webpack-plugin&quot;);\nconst path = require(&quot;path&quot;);\n\nmodule.exports = {\n    entry: &quot;./src/index.js&quot;,\n    output: {\n        path: path.resolve(__dirname, &quot;./dist&quot;),\n        filename: &quot;[name].[hash].js&quot;,\n    },\n    plugins: [\n        // 这个插件会在指定的输出目录下生成 index.html，其中会自动引入构建产物\n        new HtmlWebpackPlugin({\n            title: &quot;Webpack Demo&quot;,\n        }),\n    ],\n};\n</code></pre>\n<h1>其他概念</h1>\n<h2>Development</h2>\n<p>mode 设置为 &quot;development&quot; 则调整为开发模式</p>\n<pre><code class=\"language-js\">const path = require(&quot;path&quot;);\nconst HtmlWebpackPlugin = require(&quot;html-webpack-plugin&quot;);\n\nmodule.exports = {\n    mode: &quot;development&quot;,\n    devtool: &quot;inline-source-map&quot;,\n    entry: {\n        index: &quot;./src/index.js&quot;,\n        print: &quot;./src/print.js&quot;,\n    },\n    plugins: [\n        new HtmlWebpackPlugin({\n            title: &quot;Development&quot;,\n        }),\n    ],\n    output: {\n        filename: &quot;[name].bundle.js&quot;,\n        path: path.resolve(__dirname, &quot;dist&quot;),\n        clean: true,\n    },\n};\n</code></pre>\n<h1><a href=\"https://webpack.docschina.org/contribute/writing-a-plugin/\">插件开发</a></h1>\n<p><a href=\"https://webpack.docschina.org/concepts/under-the-hood/\">https://webpack.docschina.org/concepts/under-the-hood/</a></p>\n","tags":["js","lib"]},{"id":"js-react","url":"https://yieldray.fun/posts/js-react","title":"React笔记","date_published":"2022-09-11T13:00:00.000Z","date_modified":"2022-09-11T13:00:00.000Z","content_text":"<p>文档：<a href=\"https://zh-hans.react.dev/reference/\">https://zh-hans.react.dev/reference/</a></p>\n<p>旧文档：<a href=\"https://zh-hans.legacy.reactjs.org/docs/getting-started.html\">https://zh-hans.legacy.reactjs.org/docs/getting-started.html</a></p>\n<p>Typescript：<a href=\"https://react-typescript-cheatsheet.netlify.app/\">https://react-typescript-cheatsheet.netlify.app/</a></p>\n<p>本篇基于 React18，不会出现类 Class Component</p>\n<h1>一些 Typescript 常用类型</h1>\n<pre><code class=\"language-tsx\">import React, { forwardRef, useImperativeHandle } from &quot;react&quot;;\nimport ReactDOM from &quot;react-dom/client&quot;;\n\ninterface Props {\n    children?: React.ReactNode;\n    style?: React.CSSProperties;\n    onChange?: React.ChangeEventHandler&lt;HTMLInputElement&gt;;\n}\n\nfunction Comp(props?: Props): React.ReactNode {\n    // 返回 JSX 类型为 React.JSX.Element\n    return (\n        &lt;&gt;\n            &lt;div style={props?.style}&gt;{props?.children}&lt;/div&gt;\n            &lt;input type=&quot;text&quot; onChange={props?.onChange} /&gt;\n        &lt;/&gt;\n    );\n}\n// React.ComponentProps&lt;typeof Comp&gt; === Props | undefined\n\nconst FC: React.FunctionComponent&lt;Props&gt; = (props) =&gt; (\n    &lt;Comp {...props} onChange={(e: React.FormEvent&lt;HTMLInputElement&gt;) =&gt; console.log(e)}&gt;&lt;/Comp&gt;\n);\n\ninterface ButtonProps extends React.ComponentPropsWithoutRef&lt;&quot;button&quot;&gt; {\n    specialProp?: string;\n}\nfunction Button({ specialProp, ...props }: ButtonProps) {\n    return &lt;button {...props} /&gt;;\n}\n\nconst RefDOM = forwardRef&lt;HTMLDivElement, Props&gt;((props, ref) =&gt; &lt;div ref={ref}&gt;&lt;/div&gt;);\n// React.ElementRef&lt;typeof RefDOM&gt; === HTMLDivElement\n\nconst RefImperative = forwardRef&lt;{ justExample: string }&gt;((_, ref) =&gt; {\n    useImperativeHandle(ref, () =&gt; ({ justExample: &quot;ref&quot; }));\n    return undefined;\n});\n// React.ElementRef&lt;typeof RefImperative&gt; === { justExample: string }\n\nfunction PassThrough(props: { as: React.ElementType&lt;any&gt; }) {\n    const { as: Component } = props;\n    return &lt;Component /&gt;;\n}\n\nReactDOM.createRoot(document.body).render(\n    &lt;React.StrictMode&gt;\n        &lt;Comp /&gt;\n        &lt;FC /&gt;\n        &lt;RefDOM /&gt;\n        &lt;Button /&gt;\n        &lt;RefImperative /&gt;\n        &lt;PassThrough as=&quot;button&quot; /&gt;\n    &lt;/React.StrictMode&gt;,\n);\n</code></pre>\n<h1>事件</h1>\n<h2>捕获与冒泡</h2>\n<p>在 React 中所有事件都会冒泡（除了 onScroll，它仅适用于你附加到的 JSX 标签）</p>\n<p>显式调用 <code>e.stopPropagation()</code> 停止冒泡</p>\n<pre><code class=\"language-jsx\">//prettier-ignore\n&lt;div onClickCapture={() =&gt; { /* 这会首先执行 */ }}&gt;\n  &lt;button onClick={e =&gt; e.stopPropagation()} /&gt;\n  &lt;button onClick={e =&gt; e.stopPropagation()} /&gt;\n&lt;/div&gt;\n</code></pre>\n<p>每个事件分三个阶段传播：</p>\n<ol>\n<li>向下传播，调用所有的 onClickCapture 处理函数。</li>\n<li>执行被点击元素的 onClick 处理函数。</li>\n<li>向上传播，调用所有的 onClick 处理函数。</li>\n</ol>\n<h2><a href=\"https://zh-hans.react.dev/reference/react-dom/components/common#react-event-object\">合成事件</a></h2>\n<p>React 事件并非原生事件，而是合成事件。<br>事件监听器<em>不是</em>附加到 JSX 声明的那个元素上，而是委托给渲染根元素（旧版本则是委托给 document）。<br>因此我们的事件处理函数是由 React 调用，传入由 React 创建的合成事件。</p>\n<p>在混合使用原生事件和 React 事件时，需要注意。</p>\n<h1>hooks</h1>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/useState\">useState</a></h2>\n<pre><code class=\"language-js\">const [count, setCount] = useState(0);\n// 我们只是提供了初始值。useState 知道上一次渲染的 state，返回每次 state 的快照\n\nconsole.log(count); // =&gt; 0\n// count 是由 useState 返回的值，并不是由我们显式赋的值\n\nsetCount(count + 1);\n// 调用 set 函数，schedule 下一次渲染\n// set 函数真正接收的是 根据上一个状态得到下一个状态的函数\n\nconsole.log(count); // =&gt; 0\n// JavaScript 并没有获取一个变量的引用的机制，而 count 并没有被赋值，当然还是 0\n// 此外注意到我们使用 const 声明，阻止引用被改变\n</code></pre>\n<p>useState 是通过数组实现的，每个 state 按顺序关联到数组中的一项，因此其<strong>依托于一个稳定的调用顺序</strong>。<br>这就是为什么只能在组件顶层调用 useState，因为组件函数由 React 调用，其顺序是预期的。</p>\n<p>React 文档给出了一个<a href=\"https://zh-hans.react.dev/learn/state-a-components-memory#how-does-react-know-which-state-to-return\">简化版</a>实现。</p>\n<p>state 被 fiber 树节点（暂且可以简单地看作是 React.ReactElement）持有。这意味着只有 fiber 被销毁，state 才会被销毁。<br>在 React 中，fiber 树中（而非 JSX 中） <em>相同位置</em> 的 <em>相同组件</em> 被认为是同一 fiber，仅发生修改，而非重建。</p>\n<blockquote>\n<p>相同组件：对于原生标签，则根据标签名；对于函数组件，则比较是否为同一函数</p>\n</blockquote>\n<pre><code class=\"language-jsx\">function App() {\n    const [bool, setBool] = useState(false);\n\n    return (\n        &lt;&gt;\n            &lt;input type=&quot;checkbox&quot; checked={bool} onChange={(e) =&gt; setBool(e.target.checked)} /&gt;\n            {/* 相同位置，相同组件 */}\n            {isGood ? &lt;Comp isGood={true} /&gt; : &lt;Comp isGood={false} /&gt;}\n            {/* 注意空值：如 undefined、null 也会创建 fiber */}\n        &lt;/&gt;\n    );\n}\n</code></pre>\n<p>设置 key 属性，可以使 diff 算法根据给定的 key 来判断是否需要（在父 fiber 内部）重建。</p>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/useRef\">useRef</a></h2>\n<p>ref 也被 fiber 树持有，但不会触发重渲染。<br>换言之，可以看作是：</p>\n<pre><code class=\"language-js\">function useRef(initialValue) {\n    const [ref, unused] = useState({ current: initialValue });\n    return ref;\n}\n</code></pre>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/useEffect\">useEffect</a></h2>\n<p>effect 在渲染结束后运行，用于与外部系统同步（副作用），返回函数用于清理副作用（另一处可以运行副作用代码的地方是事件处理函数）。<br>依赖数组决定（通过 <code>Object.is()</code> 比较）重渲染后是否需要重新运行（不传表示每次都要运行）</p>\n<blockquote>\n<p>React18 提供了 <a href=\"https://zh-hans.react.dev/reference/react/useSyncExternalStore\"><code>useSyncExternalStore</code></a>，方便与外部 store 同步，并对 Server Component 友好</p>\n</blockquote>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/useContext\"><code>useContext&lt;T&gt;(context: Context&lt;T&gt;): T</code></a></h2>\n<pre><code class=\"language-tsx\">import { createContext, useContext, useState } from &quot;react&quot;;\n\nconst ThemeContext = createContext&lt;&quot;light&quot; | &quot;dark&quot;&gt;(&quot;light&quot;); // 默认值\n\nexport default function App() {\n    const [theme, setTheme] = useState&lt;&quot;light&quot; | &quot;dark&quot;&gt;(&quot;light&quot;);\n    return (\n        &lt;ThemeContext.Provider value={theme}&gt;\n            &lt;button onClick={() =&gt; setTheme(theme === &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot;)}&gt;toggle theme&lt;/button&gt;\n            &lt;Consumer /&gt;\n        &lt;/ThemeContext.Provider&gt;\n    );\n}\n\nfunction Consumer() {\n    const themeContext = useContext(ThemeContext);\n    // useContext 只在调用它的组件树上方寻找最近的 Provider\n    // 很明显是不可能能够考虑组件内部的 Provider 的\n    return &lt;div&gt;{themeContext}&lt;/div&gt;;\n}\n</code></pre>\n<p>使用 useContext 需要往往需要考虑性能，比如使用 useCallback、useMemo 和 momo。</p>\n<h1>组件/其它API</h1>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/startTransition\"><code>startTransition(scope: TransitionFunction): void</code></a></h2>\n<blockquote>\n<p>什么是 transition？<br>这里的 transition 当然不是指 css transition，而是由于 setState 函数调用导致的重渲染而发生的 transition<br>最需要该 API 的地方是完整的渲染很耗时的情况，因为同步渲染会阻塞 UI 线程，使得用户无法与页面交互</p>\n</blockquote>\n<p>React18 支持为渲染标记优先级。startTransition 传入一个无参的 scope 函数并立即执行它，<br>但在该函数内部的 setState 函数触发的状态变更被标记为<em>非阻塞</em>（也就是不立即发生实际渲染，注意 React18 是以 concurrent mode 渲染）。<br>这意味着如果有其它状态变更（优先级更高）发生，那么 React 优先处理该变更，然后才是 scope 内部变更。</p>\n<blockquote>\n<p><em>非阻塞</em>渲染也即<em>后台</em>渲染。由事件（例如输入）引起的任何更新都会中断后台渲染，并被优先处理。</p>\n</blockquote>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/useTransition\"><code>useTransition(): [boolean, TransitionStartFunction]</code></a></h2>\n<p>useTransition 是 startTransition 的强化，它返回 <code>[isPending, startTransition]</code><br>其中 isPending 告诉我们 transition 是否还在处理</p>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/useDeferredValue\"><code>useDeferredValue&lt;T&gt;(value: T): T</code></a></h2>\n<p>useDeferredValue 函数接受一个 state 变量，返回该 state 的延迟版本。<br>当 setState 触发组件更新时，如果新旧 state 不同（通过 <code>Object.is()</code> 比较），<br>那么 React 将先尝试使用延迟值（也就是与上一次相同的值，旧 state）进行渲染，<br>然后在<em>后台</em>使用新值再渲染一次（第一次渲染除外，因为第一次不存在旧值）。</p>\n<p>其中 Effect 将在后台渲染完成，提交到屏幕之后运行。</p>\n<p>注意由于使用 <code>Object.is()</code>，对象之间比较引用，所以需要防止向 useDeferredValue 传入<br>组件函数内部创建的对象，因为函数每次调用都产生不同的对象，导致不必要的重渲染</p>\n<h2><a href=\"https://zh-hans.react.dev/reference/react/Suspense\"><code>&lt;Suspense&gt;</code></a></h2>\n<pre><code class=\"language-jsx\">import React from &quot;react&quot;;\nconst MarkdownPreview = React.lazy(() =&gt; import(&quot;./MarkdownPreview.js&quot;));\n\nclass ErrorBoundary extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { hasError: false };\n    }\n\n    static getDerivedStateFromError(error) {\n        return { hasError: true };\n    }\n\n    componentDidCatch(error, info) {\n        console.error(error, info.componentStack);\n    }\n\n    render() {\n        if (this.state.hasError) {\n            return &lt;div&gt;Error: Something is wrong!&lt;/div&gt;;\n        }\n        return this.props.children;\n    }\n}\n\nfunction App() {\n    return (\n        &lt;ErrorBoundary&gt;\n            &lt;React.Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;\n                &lt;h2&gt;Preview&lt;/h2&gt;\n                &lt;MarkdownPreview /&gt;\n            &lt;/React.Suspense&gt;\n        &lt;/ErrorBoundary&gt;\n    );\n}\n</code></pre>\n<h2><a href=\"https://zh-hans.react.dev/reference/react-dom/flushSync\"><code>flushSync&lt;R&gt;(fn: () =&gt; R): R</code> <code>flushSync&lt;A, R&gt;(fn: (a: A) =&gt; R, a: A): R</code></a></h2>\n<p>React 并非使用微任务更新 DOM，因此 React 提供了 flushSync 函数。<br>该函数立即调用 callback，并使内部的 setState 函数立即更新 DOM</p>\n<blockquote>\n<p>vue.js 使用微任务，因此其提供的是 <a href=\"https://cn.vuejs.org/api/general.html#nexttick\">nextTick(callback?): Promise<void></a></p>\n</blockquote>\n<h1>生命周期</h1>\n<iframe src=\"https://codesandbox.io/embed/x59t59?view=Editor+%2B+Preview&module=%2Fsrc%2FLifeCycle.js\"\n     style=\"width:100%; height: 500px; border:0; border-radius: 4px; overflow:hidden;\"\n     title=\"react-lifecycle\"\n     allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n     sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n   ></iframe>\n","tags":["react","js","lib"]},{"id":"js-vue","url":"https://yieldray.fun/posts/js-vue","title":"Vue笔记","date_published":"2022-09-09T09:09:09.000Z","date_modified":"2022-09-09T09:09:09.000Z","content_text":"<h1>Hello，world</h1>\n<pre><code class=\"language-html\">&lt;script type=&quot;importmap&quot;&gt;\n    {\n        &quot;imports&quot;: {\n            &quot;vue&quot;: &quot;https://unpkg.com/vue@3/dist/vue.esm-browser.js&quot;\n        }\n    }\n&lt;/script&gt;\n\n&lt;div id=&quot;app&quot;&gt;{{ message }}&lt;/div&gt;\n\n&lt;script type=&quot;module&quot;&gt;\n    import { createApp, ref } from &quot;vue&quot;;\n\n    const app = createApp({\n        setup() {\n            const message = ref(&quot;Hello Vue!&quot;);\n            return {\n                message,\n            };\n        },\n    }).mount(&quot;#app&quot;);\n&lt;/script&gt;\n</code></pre>\n<p>vue2 版本如下，vue2 默认导出 Vue 对象。<br>本文仅使用 vue3，而不是 vue2。</p>\n<pre><code class=\"language-js\">const app = new Vue({\n    el: &quot;#app&quot;,\n    data: {\n        message: &quot;Hello Vue!&quot;,\n    },\n});\n</code></pre>\n<h1>模板语法</h1>\n<p>vue 使用 Mustache 语法</p>\n<pre><code class=\"language-html\">&lt;span&gt;Message: {{ msg }}&lt;/span&gt;\n&lt;span v-html=&quot;rawHtml&quot;&gt;&lt;/span&gt;\n\n&lt;!-- 绑定属性 --&gt;\n&lt;div v-bind:id=&quot;dynamicId&quot;&gt;&lt;/div&gt;\n&lt;div :id=&quot;dynamicId&quot;&gt;&lt;/div&gt;\n&lt;div v-bind=&quot;objectOfAttrs&quot;&gt;&lt;/div&gt;\n\n&lt;!-- vue 建议事件以 kebab-case 命名，但支持自动转换 --&gt;\n&lt;a v-on:event.modifier=&quot;handler&quot;&gt;&lt;/a&gt;\n&lt;a @event.modifier=&quot;handler&quot;&gt;&lt;/a&gt;\n\n&lt;!-- 动态属性 --&gt;\n&lt;a v-bind:[attributeName]=&quot;url&quot;&gt;&lt;/a&gt;\n&lt;a :[attributeName]=&quot;url&quot;&gt;&lt;/a&gt;\n\n&lt;!-- v-model 组件展开 --&gt;\n&lt;input v-model=&quot;searchText&quot; /&gt;\n&lt;input :value=&quot;searchText&quot; @input=&quot;searchText = $event.target.value&quot; /&gt;\n\n&lt;!-- 对于组件，v-model 默认绑定 model-value 属性 --&gt;\n&lt;CustomInput v-model=&quot;searchText&quot; /&gt;\n&lt;CustomInput :model-value=&quot;searchText&quot; @update:model-value=&quot;(newValue) =&gt; (searchText = newValue)&quot; /&gt;\n\n&lt;!-- 循环数组或对象 --&gt;\n&lt;div v-for=&quot;(item, index) in items&quot;&gt;&lt;/div&gt;\n&lt;div v-for=&quot;(value, key) in object&quot;&gt;&lt;/div&gt;\n&lt;div v-for=&quot;(value, name, index) in object&quot;&gt;&lt;/div&gt;\n\n&lt;!-- 默认插槽名 default 可以省略 --&gt;\n&lt;template v-slot:slot-name=&quot;slotProps&quot;&gt;fallback&lt;/template&gt;\n&lt;template #slot-name=&quot;slotProps&quot;&gt;具名插槽简写&lt;/template&gt;\n&lt;template #default=&quot;slotProps&quot;&gt;默认插槽简写&lt;/template&gt;\n&lt;template #[dynamicSlotName]&gt;动态插槽名&lt;/template&gt;\n</code></pre>\n<p>class/style 绑定：<a href=\"https://cn.vuejs.org/guide/essentials/class-and-style.html\">https://cn.vuejs.org/guide/essentials/class-and-style.html</a></p>\n<blockquote>\n<p><strong>内置指令：</strong><br><a href=\"https://cn.vuejs.org/api/built-in-directives.html\">https://cn.vuejs.org/api/built-in-directives.html</a></p>\n</blockquote>\n<p>模板表达式中能够访问变量实际是当前组件暴露的属性或方法<br>即 <code>xxx</code> 相当于 <code>this.xxx</code>，因此无法访问全局对象上的属性<br>但 vue 提供了一个<a href=\"https://github.com/vuejs/core/blob/main/packages/shared/src/globalsAllowList.ts#L3\">有限的全局对象列表</a>，提供给模板使用（比如 Math 和 Date）</p>\n<p>使用 <code>app.config.globalProperties</code> 可注册能够被应用内所有组件实例访问到的全局属性的对象</p>\n<pre><code class=\"language-ts\">interface AppConfig {\n    globalProperties: Record&lt;string, any&gt;;\n}\n\nObject.assign(app.config.globalProperties, {\n    alert: window.alert,\n    // 有限的全局对象列表不含 alert 方法\n});\n\n// 应用内其它任意组件\nexport default {\n    mounted() {\n        console.assert(this.alert, window.alert);\n    },\n};\n</code></pre>\n<h1>响应式</h1>\n<pre><code class=\"language-html\">&lt;script lang=&quot;ts&quot;&gt;\n    import { ref, reactive，nextTick } from &quot;vue&quot;;\n\n    const component: Component = {\n        setup() {\n            const count = ref(0);\n            async function increment() {\n                count.value++;\n                 await nextTick()\n                 // 现在 DOM 已经更新了\n            }\n            const state = reactive({ count: 0 });\n            return {\n                // setup 函数的返回值会暴露给模板\n                count,\n                increment,\n                state,\n            };\n        },\n    };\n\n    export default component;\n&lt;/script&gt;\n</code></pre>\n<p><code>&lt;script setup&gt;</code> 中的<em>顶层的</em>导入、声明的变量和函数可在同一组件的模板中直接使用。</p>\n<pre><code class=\"language-html\">&lt;script setup&gt;\n    import { ref, reactive，nextTick } from &quot;vue&quot;;\n    const count = ref(0);\n    async function increment() {\n        count.value++;\n         await nextTick()\n         // 现在 DOM 已经更新了\n    }\n    const state = reactive({ count: 0 });\n&lt;/script&gt;\n</code></pre>\n<p>在模板渲染上下文中，只有顶级的 ref 属性才会被解包。<br>即，顶级 ref 无需 <code>.value</code>（语法糖，编译器实现），非顶级 ref 需要 <code>.value</code></p>\n<blockquote>\n<p>对于 <code>ref</code> <code>reactive</code> API，vue 实现了深层响应式，即 vue 递归监听对象的所有属性<br>vue 还提供了进阶 API：<a href=\"https://cn.vuejs.org/api/reactivity-advanced.html\">https://cn.vuejs.org/api/reactivity-advanced.html</a></p>\n</blockquote>\n<p>vue3 基于 Proxy 监听对象属性的变化。对于解构赋值，则无法被监听<br>此外，响应式对象是 Proxy 实例而不是源对象。<br>vue 提供了响应式工具函数：<a href=\"https://cn.vuejs.org/api/reactivity-utilities.html\">https://cn.vuejs.org/api/reactivity-utilities.html</a><br>可用于帮助解决这些问题</p>\n<pre><code class=\"language-ts\">// 只读\nfunction computed&lt;T&gt;(getter: () =&gt; T, debuggerOptions?: DebuggerOptions): Readonly&lt;Ref&lt;Readonly&lt;T&gt;&gt;&gt;;\n\n// 可读写\nfunction computed&lt;T&gt;(\n    options: {\n        get: () =&gt; T;\n        set: (value: T) =&gt; void;\n    },\n    debuggerOptions?: DebuggerOptions,\n): Ref&lt;T&gt;;\n\ntype DebuggerOptions = {\n    // 当响应式对象被追踪为依赖时触发（即读取时触发）\n    onTrack?: (e: DebuggerEvent) =&gt; void;\n    // 当响应式对象被更改时触发\n    onTrigger?: (e: DebuggerEvent) =&gt; void;\n};\n\ntype DebuggerEvent = {\n    effect: ReactiveEffect;\n    target: object;\n    type: TrackOpTypes /* &#39;get&#39; | &#39;has&#39; | &#39;iterate&#39; */ | TriggerOpTypes /* &#39;set&#39; | &#39;add&#39; | &#39;delete&#39; | &#39;clear&#39; */;\n    key: any;\n    newValue?: any;\n    oldValue?: any;\n    oldTarget?: Map&lt;any, any&gt; | Set&lt;any&gt;;\n};\n</code></pre>\n<p>上面以 <code>computed()</code> 举例，<a href=\"https://cn.vuejs.org/api/reactivity-core.html\">响应式核心 API</a> 还包括<br><code>readonly()</code> <code>watchEffect()</code> <code>watch()</code></p>\n<h1>组件注册</h1>\n<p>app.component() 方法用于注册组件。该方法需要提供组件名称以及 Component 对象</p>\n<pre><code class=\"language-ts\">function createApp(rootComponent: Component, rootProps?: object): App;\ninterface App {\n    component(name: string): Component | undefined;\n    component(name: string, component: Component): this;\n}\n</code></pre>\n<p>一个 <code>.vue</code> 文件会被编译为 Component 类型的对象</p>\n<pre><code class=\"language-ts\">import ComponentA from &quot;./ComponentA.vue&quot;;\n\nconst component: Component = {\n    components: {\n        ComponentA,\n    },\n    setup() {\n        // ...\n    },\n};\n\n// 使用 defineComponent 辅助函数可以自动推导类型\nconst component = defineComponent({\n    /* ... */\n});\n\nexport default component;\n</code></pre>\n<p>Component 对象的 components 属性则用于声明当前组件导入的组件<br>其类型可视为 <code>Record&lt;string, Component&gt;</code><br>vue 建议使用 PascalCase 为组件命名，并支持通过 kebab-case 使用 PascalCase 命名的组件</p>\n<p>组件 setup 函数的第一个参数是父组件传入的属性，第二个参数是一个 （非响应式的）context 对象</p>\n<h1>组件声明</h1>\n<p><a href=\"https://cn.vuejs.org/api/composition-api-setup.html\">setup()</a></p>\n<pre><code class=\"language-js\">export default {\n    props: {\n        title: String,\n    },\n    setup(props, context) {\n        // 属性\n        console.log(props.title);\n\n        // 透传 Attributes（非响应式的对象，等价于 $attrs）\n        console.log(context.attrs);\n\n        // 插槽（非响应式的对象，等价于 $slots）\n        console.log(context.slots);\n\n        // 触发事件（函数，等价于 $emit）\n        console.log(context.emit);\n\n        // 暴露公共属性（函数）\n        console.log(context.expose);\n    },\n};\n</code></pre>\n<h2>组件实例</h2>\n<p>我们知道，组件就是一个 Component 对象<br>当 vue 使用该实例时，会为该对象的 this 附加属性和方法</p>\n<p><a href=\"https://cn.vuejs.org/api/component-instance.html\">https://cn.vuejs.org/api/component-instance.html</a></p>\n<h2><a href=\"https://cn.vuejs.org/guide/components/props.html\">定义 Props</a></h2>\n<blockquote>\n<p>Vue 强制使用单向数据流。子组件不允许修改父组件传入的 Props</p>\n</blockquote>\n<p>选项式声明</p>\n<pre><code class=\"language-js\">export default {\n    props: [&quot;title&quot;],\n    setup(props) {\n        console.log(props.title);\n    },\n};\n</code></pre>\n<p>setup 内部声明</p>\n<pre><code class=\"language-js\">const props = defineProps([&quot;title&quot;]);\nconsole.log(props.title);\n</code></pre>\n<p><a href=\"https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props\">标注类型</a></p>\n<pre><code class=\"language-ts\">const props = defineProps({\n    foo: { type: String, required: true },\n    bar: Number,\n});\n</code></pre>\n<p>TS 宏声明</p>\n<pre><code class=\"language-ts\">const props = defineProps&lt;{\n    foo: string;\n    bar?: number;\n}&gt;();\n</code></pre>\n<p>默认值（参见：<a href=\"https://cn.vuejs.org/guide/components/props.html#prop-validation\">Prop 校验</a>）</p>\n<pre><code class=\"language-ts\">const props = withDefaults(defineProps&lt;Props&gt;(), {\n    foo: &quot;hello&quot;,\n    labels: () =&gt; Math.random(),\n});\n</code></pre>\n<h2><a href=\"https://cn.vuejs.org/guide/components/events.html\">定义事件</a></h2>\n<p>模板触发事件</p>\n<pre><code class=\"language-html\">&lt;button @click=&quot;$emit(&#39;eventName&#39;, &#39;detail1&#39;, &#39;detail2&#39;)&quot;&gt;click me&lt;/button&gt;\n&lt;!-- 父组件直接接收 detail，而不是原生 Event --&gt;\n&lt;MyButton @event-name=&quot;(detail1, detail2) =&gt; console.assert(detail1, &#39;detail1&#39;)&quot; /&gt;\n</code></pre>\n<p>选项式声明</p>\n<pre><code class=\"language-js\">export default {\n    emits: [&quot;inFocus&quot;, &quot;submit&quot;],\n    setup(props, ctx) {\n        ctx.emit(&quot;submit&quot;);\n    },\n};\n</code></pre>\n<p>setup 内部声明</p>\n<pre><code class=\"language-js\">const emit = defineEmits([&quot;inFocus&quot;, &quot;submit&quot;]);\nfunction buttonClick() {\n    emit(&quot;submit&quot;);\n}\n</code></pre>\n<p>TS 宏声明</p>\n<pre><code class=\"language-ts\">// 基于类型\nconst emit = defineEmits&lt;{\n    (e: &quot;change&quot;, id: number): void;\n    (e: &quot;update&quot;, value: string): void;\n}&gt;();\n\n// v3.3+ 提供, 更简洁的语法\nconst emit = defineEmits&lt;{\n    change: [id: number];\n    update: [value: string];\n}&gt;();\n</code></pre>\n<p><a href=\"https://cn.vuejs.org/guide/components/events.html#events-validation\">事件校验</a></p>\n<h2><a href=\"https://cn.vuejs.org/guide/components/slots.html\">定义插槽</a></h2>\n<p>插槽允许父组件在子组件中渲染模板<br>在子组件模板中，Vue 使用与 WebComponents 相同的 <code>&lt;slot&gt;</code> 语法<br>父组件传入 slot 则使用 <code>v-slot:slot-name</code> 指令<br>此外，Vue 支持子组件插槽向父组件插槽模板提供数据</p>\n<pre><code class=\"language-html\">&lt;script setup lang=&quot;ts&quot;&gt;\n    const slots = defineSlots&lt;{\n        default(props: { msg: string }): any;\n    }&gt;();\n&lt;/script&gt;\n</code></pre>\n<h1><a href=\"https://cn.vuejs.org/api/composition-api-lifecycle.html\">生命周期</a></h1>\n<iframe height=\"478.4\" style=\"width: 100%;\" scrolling=\"no\" title=\"vue-lifecycle\" src=\"https://codepen.io/YieldRay/embed/Pogedzo?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/Pogedzo\">\n  vue-lifecycle</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>DOM / 组件 ref （模板引用）</h1>\n<p><a href=\"https://cn.vuejs.org/guide/essentials/template-refs.html\">https://cn.vuejs.org/guide/essentials/template-refs.html</a></p>\n<pre><code class=\"language-html\">&lt;script setup&gt;\n    import { ref, onMounted } from &quot;vue&quot;;\n    const input = ref(null);\n    onMounted(() =&gt; {\n        input.value.focus();\n    });\n&lt;/script&gt;\n\n&lt;template&gt;\n    &lt;input ref=&quot;input&quot; /&gt;\n&lt;/template&gt;\n</code></pre>\n<p>ref 属性传入函数时，会在每次组件更新后以及组件卸载时被调用</p>\n<pre><code class=\"language-html\">&lt;input :ref=&quot;(el) =&gt; { /* 将 el 赋值给一个数据属性或 ref 变量 */ }&quot; /&gt;\n</code></pre>\n<p>对于 Vue 组件而不是 DOM 节点，ref 得到的则是组件实例\n此时 setup 组件必须手动使用 <code>defineExpose</code> 显示暴露属性</p>\n<pre><code class=\"language-html\">&lt;script setup&gt;\n    import { ref } from &quot;vue&quot;;\n\n    const a = 1;\n    const b = ref(2);\n\n    // 像 defineExpose 这样的编译器宏不需要导入\n    defineExpose({\n        a,\n        b,\n    });\n&lt;/script&gt;\n</code></pre>\n<h1>透传 attributes（$attrs）</h1>\n<p>对于存在根节点的子组件，父组件提供子组件未声明的属性（包括事件）时，这些属性会自动透传到子组件的根节点上<br>这种行为是递归的，style 和 class 属性还会自动合并</p>\n<p>对于不存在根节点（即多个根节点）的子组件，需要正确地设置 <code>$attr</code> 对象<br>比如在某个节点上设置 <code>v-bind=&quot;$attrs&quot;</code></p>\n<p>在 Javascript 中得到的 attrs 不是响应式的<br>由于 attrs 变化会触发重渲染，可以使用 <code>onUpdated()</code> 监听</p>\n<pre><code class=\"language-html\">&lt;script setup&gt;\n    import { useAttrs } from &quot;vue&quot;;\n    const attrs = useAttrs();\n&lt;/script&gt;\n</code></pre>\n<pre><code class=\"language-ts\">const component: Component = {\n    setup(props, { attrs }) {\n        console.log(attrs);\n    },\n};\n</code></pre>\n<p>透传是默认行为。需要手动处理时，则可以提供以下方式禁用</p>\n<pre><code class=\"language-ts\">const component: Component = {\n    inheritAttrs: false,\n    // 或使用 defineOptions()\n    setup() {\n        defineOptions({\n            inheritAttrs: false,\n        });\n    },\n};\n</code></pre>\n<p><a href=\"https://cn.vuejs.org/guide/components/attrs.html\">https://cn.vuejs.org/guide/components/attrs.html</a><br><a href=\"https://cn.vuejs.org/api/options-misc.html#inheritattrs\">https://cn.vuejs.org/api/options-misc.html#inheritattrs</a></p>\n<h1>Provide / Inject</h1>\n<p><a href=\"https://cn.vuejs.org/guide/components/provide-inject\">https://cn.vuejs.org/guide/components/provide-inject</a><br><a href=\"https://cn.vuejs.org/api/composition-api-dependency-injection.html\">https://cn.vuejs.org/api/composition-api-dependency-injection.html</a><br><a href=\"https://cn.vuejs.org/api/application.html#app-provide\">https://cn.vuejs.org/api/application.html#app-provide</a></p>\n<pre><code class=\"language-ts\">function provide&lt;T&gt;(key: InjectionKey&lt;T&gt; | string, value: T): void;\n\n// 没有默认值\nfunction inject&lt;T&gt;(key: InjectionKey&lt;T&gt; | string): T | undefined;\n\n// 带有默认值\nfunction inject&lt;T&gt;(key: InjectionKey&lt;T&gt; | string, defaultValue: T): T;\n\n// 使用工厂函数\nfunction inject&lt;T&gt;(key: InjectionKey&lt;T&gt; | string, defaultValue: () =&gt; T, treatDefaultAsFactory: true): T;\n</code></pre>\n<h1>异步组件 / <code>&lt;Suspense&gt;</code></h1>\n<p>异步组件和普通组件一样，它只是 <a href=\"https://cn.vuejs.org/api/general.html#defineasynccomponent\"><code>defineAsyncComponent()</code></a> 函数包装一个异步函数实现的组件</p>\n<pre><code class=\"language-ts\">import { defineAsyncComponent } from &quot;vue&quot;;\n// defineAsyncComponent() 函数要求提供一个返回 Promise 的函数，而不是 Promise 本身\nconst AsyncComp: Component = defineAsyncComponent(() =&gt; import(&quot;./components/MyComponent.vue&quot;));\n</code></pre>\n<p>defineAsyncComponent 支持多种选项</p>\n<pre><code class=\"language-ts\">const AsyncComp = defineAsyncComponent({\n    // 加载函数\n    loader: () =&gt; import(&quot;./Foo.vue&quot;),\n\n    // 加载异步组件时使用的组件\n    loadingComponent: LoadingComponent,\n    // 展示加载组件前的延迟时间，默认为 200ms\n    delay: 200,\n\n    // 加载失败后展示的组件\n    errorComponent: ErrorComponent,\n    // 如果提供了一个 timeout 时间限制，并超时了\n    // 也会显示这里配置的报错组件，默认值是：Infinity\n    timeout: 3000,\n});\n</code></pre>\n<p><a href=\"https://cn.vuejs.org/api/built-in-components.html#suspense\"><code>&lt;Suspense&gt;</code> 内置组件</a>可以看作是通过包装 defineAsyncComponent 实现的组件（）</p>\n<p>如果一个组件的 setup 函数是 async 函数，则这个组件需要置于 <code>&lt;Suspense&gt;</code> 内部<br>这也相当于 defineAsyncComponent() 包装了一个 async 函数</p>\n<pre><code class=\"language-js\">export default {\n    async setup() {\n        const res = await fetch(/* ... */);\n        const posts = await res.json();\n        return {\n            posts,\n        };\n    },\n};\n</code></pre>\n<pre><code class=\"language-html\">&lt;script setup&gt;\n    const res = await fetch(/* ... */)\n    const posts = await res.json()\n&lt;/script&gt;\n</code></pre>\n<p>在 <code>&lt;Suspense&gt;</code> 组件中的异步组件，其加载/错误处理将由外部的 <code>&lt;Suspense&gt;</code> 处理<br>其内部加载/错误处理被忽略，除非异步组件指定 <code>suspensible: false</code> 选项</p>\n<pre><code class=\"language-html\">&lt;Suspense @pending @resolve @fallback&gt;\n    &lt;!-- 具有深层异步依赖的组件 --&gt;\n    &lt;AsyncComp /&gt;\n\n    &lt;template #fallback&gt; Loading... &lt;/template&gt;\n    &lt;template #fallback&gt; Loading... &lt;/template&gt;\n&lt;/Suspense&gt;\n</code></pre>\n<h1>自定义指令</h1>\n<p><a href=\"https://cn.vuejs.org/guide/reusability/custom-directives.html\">https://cn.vuejs.org/guide/reusability/custom-directives.html</a><br><a href=\"https://cn.vuejs.org/api/options-misc.html#directives\">https://cn.vuejs.org/api/options-misc.html#directives</a></p>\n<p>自定义指令是一个对象，其中包含一系列钩子函数</p>\n<p>在 setup 组件中注册</p>\n<pre><code class=\"language-html\">&lt;script setup&gt;\n    // 在模板中启用 v-focus\n    const vFocus = {\n        mounted: (el) =&gt; el.focus(),\n    };\n&lt;/script&gt;\n\n&lt;template&gt;\n    &lt;input v-focus /&gt;\n&lt;/template&gt;\n</code></pre>\n<p>在 setup 函数中注册</p>\n<pre><code class=\"language-js\">export default {\n    setup() {},\n    directives: {\n        // 在模板中启用 v-focus\n        focus: {\n            mounted: (el) =&gt; el.focus(),\n        },\n    },\n};\n</code></pre>\n<p>在全局实例上注册</p>\n<pre><code class=\"language-js\">app.directive(&quot;focus&quot;, {\n    mounted: (el) =&gt; el.focus(),\n});\n</code></pre>\n<h1>编写 Vue 插件</h1>\n<p><a href=\"https://cn.vuejs.org/guide/reusability/plugins.html\">https://cn.vuejs.org/guide/reusability/plugins.html</a><br><a href=\"https://cn.vuejs.org/api/application.html#app-use\">https://cn.vuejs.org/api/application.html#app-use</a></p>\n<p>插件应用在全局实例上，用于添加全局功能</p>\n<pre><code class=\"language-js\">app.use(myPlugin, ...options);\n\nconst myPlugin = {\n    install(app, ...options) {},\n};\n</code></pre>\n<h1>内置组件</h1>\n<p><a href=\"https://cn.vuejs.org/guide/built-ins/transition.html\">https://cn.vuejs.org/guide/built-ins/transition.html</a></p>\n<h1>组合式函数</h1>\n<h1>JSX</h1>\n<p><a href=\"https://cn.vuejs.org/guide/extras/render-function.html\">https://cn.vuejs.org/guide/extras/render-function.html</a></p>\n<pre><code class=\"language-jsx\">import { h, ref } from &quot;vue&quot;;\n\nexport default {\n    setup(props, { slots, expose }) {\n        const count = ref(0);\n        const increment = () =&gt; ++count.value;\n\n        expose({\n            increment,\n        });\n\n        return (\n            &lt;div&gt;\n                &lt;span&gt;{count}&lt;/span&gt;\n                &lt;button&gt;{slots.default()}&lt;/button&gt;\n                &lt;div&gt;{slots.footer({ text: props.message })}&lt;/div&gt;\n            &lt;/div&gt;\n        );\n    },\n};\n</code></pre>\n","tags":["vue","js","lib"]},{"id":"js-svelte","url":"https://yieldray.fun/posts/js-svelte","title":"Svelte笔记","date_published":"2022-09-08T20:00:00.000Z","date_modified":"2022-09-08T20:00:00.000Z","content_text":"<p>Learn:<a href=\"https://learn.svelte.dev/\">https://learn.svelte.dev/</a><br>Docs:<a href=\"https://svelte.dev/docs\">https://svelte.dev/docs</a><br>REPL:<a href=\"https://svelte.dev/repl\">https://svelte.dev/repl</a></p>\n<h1>Template Syntax</h1>\n<h2>Logic</h2>\n<pre><code class=\"language-svelte\">{#if expression}...{:else if expression}...{:else}...{/if}\n</code></pre>\n<pre><code class=\"language-svelte\">{#each expression as name, index (key)}...{:else}...{/each}\n</code></pre>\n<pre><code class=\"language-svelte\">{#await expression}...{:then name}...{:catch name}...{/await}\n{#await expression then name}...{/await}\n{#await expression catch name}...{/await}\n</code></pre>\n<pre><code class=\"language-svelte\">{#key expression}...{/key}\n</code></pre>\n<h2>Markup</h2>\n<p>以文本节点表示</p>\n<pre><code class=\"language-svelte\">{expression}\n</code></pre>\n<p>以 html 表示</p>\n<pre><code class=\"language-svelte\">{@html expression}\n</code></pre>\n<p>标记 debugger。仅允许使用变量标识符，不允许使用表达式。</p>\n<pre><code class=\"language-svelte\">{@debug var1, var2, ..., varN}\n</code></pre>\n<p>仅能在逻辑块中使用。在当前逻辑块作用域中声明变量。</p>\n<pre><code class=\"language-svelte\">{@const assignment}\n</code></pre>\n<h2>Directive</h2>\n<p>绑定事件监听器</p>\n<pre><code class=\"language-svelte\">on:eventname|modifiers={handler}\n</code></pre>\n<p>不提供 handler 时，原样转发当前监听器</p>\n<pre><code class=\"language-svelte\">on:eventname\n</code></pre>\n<p>绑定属性</p>\n<pre><code class=\"language-svelte\">bind:property={variable}\n</code></pre>\n<p>绑定属性简写 bind:value={value}</p>\n<pre><code class=\"language-svelte\">bind:value\n</code></pre>\n<p>元素引用（ref）</p>\n<pre><code class=\"language-svelte\">bind:this={dom_node}\n</code></pre>\n<p>绑定 class</p>\n<pre><code class=\"language-svelte\">class={expression}\nclass:name={value}\nclass:name\n</code></pre>\n<p>绑定 style</p>\n<pre><code class=\"language-svelte\">style:property={value}\nstyle:property=&quot;value&quot;\nstyle:property\nstyle:property|important={value}\n</code></pre>\n<p>绑定 CSS 变量</p>\n<pre><code class=\"language-svelte\">--style-props=&quot;anycssvalue&quot;\n</code></pre>\n<p>使用 action（相当于 vue 的自定义指令）</p>\n<pre><code class=\"language-svelte\">use:action\nuse:action={parameters}\n</code></pre>\n<pre><code class=\"language-ts\">action = (node: HTMLElement, parameters: any) =&gt; {\n    update?: (parameters: any) =&gt; void, // parameters 更新时调用\n    destroy?: () =&gt; void // 节点销毁时调用\n}\n</code></pre>\n<p>以下内容省略：</p>\n<ul>\n<li><a href=\"https://svelte.dev/docs/element-directives#transition-fn\">transition:fn</a></li>\n<li><a href=\"https://svelte.dev/docs/element-directives#animate-fn\">animate:fn</a></li>\n<li><a href=\"https://svelte.dev/docs/special-elements\">Special elements</a></li>\n</ul>\n<p>svelte 内置了多种 DOM 属性的绑定，此处省略<br>这些内容在文档的 <a href=\"https://svelte.dev/docs/element-directives\">Element directives</a> 节有详细说明</p>\n<h1>Reactivity</h1>\n<p>svelte 的响应式仅在变量被赋值时触发</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n    let count = 0;\n    $: doubled = count * 2;\n\n    function increment() {\n        count += 1;\n    }\n&lt;/script&gt;\n\n&lt;button on:click=&quot;{increment}&quot;&gt;click me&lt;/button&gt;\n&lt;p&gt;count is {count}&lt;/p&gt;\n&lt;p&gt;doubled is {doubled}&lt;/p&gt;\n</code></pre>\n<h1>Component</h1>\n<p>注意到 svelte 中的属性在子组件中是可变的，在父组件中也是可变的。<br>这意味着无论是子组件还是父组件都可以修改属性，并没有强制限制单向数据流。</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n    export let user = &quot;Ray&quot;;\n    //          ∧       ∧\n    //          |       |\n    //         属性    默认值\n&lt;/script&gt;\n\n&lt;h1 {...$$props}&gt;Hello, {user}!&lt;/h1&gt;\n</code></pre>\n<p>组件 <code>export const</code>（而不是 <code>export let</code>）用于提供 <code>bind:this</code></p>\n<pre><code class=\"language-html\">&lt;!-- Child.svelte --&gt;\n&lt;script&gt;\n    let data = [&quot;Apple&quot;, &quot;Banana&quot;];\n    export const empty = () =&gt; (data = []);\n&lt;/script&gt;\n&lt;!-- Parent.svelte --&gt;\n&lt;Child bind:this=&quot;{cart}&quot;&gt;&lt;/Child&gt;\n&lt;button on:click=&quot;{()&quot; =&quot;&quot;&gt;cart.empty()}&gt; Empty shopping cart&lt;/button&gt;\n</code></pre>\n<p><code>&lt;script context=&quot;module&quot;&gt;</code> 允许 svelte 模块提供其它导出（默认导出是组件本身）</p>\n<pre><code class=\"language-html\">&lt;script context=&quot;module&quot;&gt;\n    let totalComponents = 0;\n\n    // the export keyword allows this function to imported with e.g.\n    // `import Example, { alertTotal } from &#39;./Example.svelte&#39;`\n    export function alertTotal() {\n        alert(totalComponents);\n    }\n&lt;/script&gt;\n\n&lt;script&gt;\n    totalComponents += 1;\n    console.log(`total number of times this component has been created: ${totalComponents}`);\n&lt;/script&gt;\n</code></pre>\n<p>子组件发出（dispatch）事件</p>\n<pre><code class=\"language-html\">&lt;!-- Child.svelte --&gt;\n&lt;script&gt;\n    import { createEventDispatcher } from &quot;svelte&quot;;\n    const dispatch = createEventDispatcher();\n    function dispatchMyEvent() {\n        dispatch(&quot;myEvent&quot;, new Date().toString(), { cancelable: true });\n    }\n&lt;/script&gt;\n&lt;button on:click=&quot;{dispatchMyEvent}&quot;&gt;fire&lt;/button&gt;\n\n&lt;!-- Parent.svelte --&gt;\n&lt;script lang=&quot;ts&quot;&gt;\n    import Child from &quot;./Child.svelte&quot;;\n    import type { ComponentEvents } from &quot;svelte&quot;;\n    function handleMyEvent(event: ComponentEvents&lt;Component&gt;[&quot;myEvent&quot;]) {\n        console.log(event.detail);\n    }\n&lt;/script&gt;\n&lt;Child on:myEvent=&quot;{handleMyEvent}&quot;&gt;&lt;/Child&gt;\n</code></pre>\n<h1>Lifecycle &amp; Context</h1>\n<p>svelte 包导出了生命周期注册函数以及 context 相关函数：<code>import { xxx } from &#39;svelte&#39;;</code></p>\n<p><a href=\"https://svelte.dev/docs/svelte\">https://svelte.dev/docs/svelte</a></p>\n<blockquote>\n<p>参见此示例：<a href=\"https://svelte.dev/repl/cbea1bf8aa614fb2a22711a925394ee6\">https://svelte.dev/repl/cbea1bf8aa614fb2a22711a925394ee6</a></p>\n</blockquote>\n<pre><code class=\"language-ts\">function onMount&lt;T&gt;(fn: () =&gt; NotFunction&lt;T&gt; | Promise&lt;NotFunction&lt;T&gt;&gt; | (() =&gt; any)): void;\n\nonMount(() =&gt; {\n    const interval = setInterval(() =&gt; {\n        console.log(&quot;beep&quot;);\n    }, 1000);\n\n    return () =&gt; clearInterval(interval);\n});\n\nfunction beforeUpdate(fn: () =&gt; any): void;\nfunction afterUpdate(fn: () =&gt; any): void;\nfunction onDestroy(fn: () =&gt; any): void;\nfunction tick(): Promise&lt;void&gt;;\n\nbeforeUpdate(async () =&gt; {\n    console.log(&quot;the component is about to update&quot;);\n    await tick();\n    console.log(&quot;the component just updated&quot;);\n});\n</code></pre>\n<pre><code class=\"language-ts\">function setContext&lt;T&gt;(key: any, context: T): T;\nfunction getContext&lt;T&gt;(key: any): T;\nfunction hasContext(key: any): boolean;\nfunction getAllContexts&lt;T extends Map&lt;any, any&gt; = Map&lt;any, any&gt;&gt;(): T;\n</code></pre>\n<h1>Stores</h1>\n<p>svelte/store 包导出了 stores API</p>\n<p><a href=\"https://svelte.dev/docs/svelte-store\">https://svelte.dev/docs/svelte-store</a></p>\n<p>svelte 提供了美元符号 $ 语法糖，提供对 store 的自动 subscribe 和 unsubscribe</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n    import { count } from &quot;./stores.js&quot;;\n\n    // let count_value;\n    // const unsubscribe = count.subscribe(value =&gt; {\n    // \tcount_value = value;\n    // });\n    // onDestroy(unsubscribe);\n&lt;/script&gt;\n\n&lt;h1&gt;The count is {$count}&lt;/h1&gt;\n</code></pre>\n<h1>Motion/Transition/Animation/Easing</h1>\n<p>svelte 自带的动画方法，由于篇幅有限省略<br>参见文档：<a href=\"https://svelte.dev/docs/svelte-motion\">https://svelte.dev/docs/svelte-motion</a></p>\n","tags":["svelte","lib","js"]},{"id":"python-intro","url":"https://yieldray.fun/posts/python-intro","title":"python语法入门","date_published":"2022-09-03T22:22:22.000Z","date_modified":"2022-09-03T22:22:22.000Z","content_text":"<p>python 镜像下载：<a href=\"https://repo.huaweicloud.com/python/\">https://repo.huaweicloud.com/python/</a><br>教程：<a href=\"https://docs.python.org/zh-cn/3/tutorial/\">https://docs.python.org/zh-cn/3/tutorial/</a><br>参考：<a href=\"https://docs.python.org/zh-cn/3/reference/\">https://docs.python.org/zh-cn/3/reference/</a><br>文档：<a href=\"https://docs.python.org/zh-cn/3/contents.html\">https://docs.python.org/zh-cn/3/contents.html</a><br>标准库：<a href=\"https://docs.python.org/zh-cn/3/library/\">https://docs.python.org/zh-cn/3/library/</a><br>内置函数：<a href=\"https://docs.python.org/zh-cn/3/library/functions.html\">https://docs.python.org/zh-cn/3/library/functions.html</a></p>\n<h1>关于语言</h1>\n<p>类型注解在 python 3.5 后引入，仅作注解，不影响解释器。<br><a href=\"https://zhuanlan.zhihu.com/p/419955374\">https://zhuanlan.zhihu.com/p/419955374</a><br>分号不是必须的，用于在一行中编写多个语句。<br>表示字符串字面量和文档注释时，单引号与双引号作用相同。<br>单行注释使用 <code>#</code> 标记，多行字符串使用连续三个（单/双）引号，不支持嵌套。<br>布尔值是 <code>True</code> 和 <code>False</code> （首字母大写），空值是 <code>None</code>。</p>\n<h1>pip 镜像</h1>\n<p>pip 是 python 的包管理器，PyPI 指的是包索引</p>\n<p>linux: <code>~/.pip/pip.conf</code><br>windows: <code>%USERPROFILE%\\pip\\pip.ini</code></p>\n<pre><code class=\"language-toml\">[global]\nindex-url = https://pypi.tuna.tsinghua.edu.cn/simple\n[install]\ntrusted-host = https://pypi.tuna.tsinghua.edu.cn\n</code></pre>\n<h1>shebang</h1>\n<pre><code class=\"language-py\">#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\n</code></pre>\n<h1>数字</h1>\n<pre><code class=\"language-py\">int: int = 1\nfloat: float = 1.2\ncomplex1: complex = 1.2 + 3.4j\ncomplex2: complex = complex(1.2, 3.4)\ndiv1: float = 3/2\ndiv2: int = 3//2\n</code></pre>\n<h1>字符串</h1>\n<pre><code class=\"language-py\">str1 = &quot;x\\r\\ny&quot;\nstr2 = &#39;x\\r\\ny&#39;\nstr3 = r&quot;x\\r\\ny&quot; # 原始字符串\nstr4 = &quot;&quot;&quot;\\\nx\ny\n&quot;&quot;&quot; # 多行字符串，首行的反斜线表示丢弃首行的换行\nstr5 = 3 * &quot;ha&quot; # hahaha\nstr6 = &quot;ha&quot; + &#39;ha&#39; # haha （连接字符串变量）\nstr7 = &quot;ha&quot; &#39;ha&#39; # haha （连接字符串字面量）\nstr8 = f&quot;PI = {math.pi:.7f}&quot; # 格式化字符串，控制位语法类 c\n</code></pre>\n<p><a href=\"https://docs.python.org/zh-cn/3/tutorial/inputoutput.html#the-string-format-method\">字符串 format() 方法</a></p>\n<h1>类型转换</h1>\n<pre><code class=\"language-py\">int(x [,base ])         # 将 x 转换为一个整数\nlong(x [,base ])        # 将 x 转换为一个长整数\nfloat(x)                # 将 x 转换为一个浮点数\ncomplex(real [,imag ])  # 创建一个复数\nstr(x)                  # 将对象 x 转换为字符串\nrepr(x)                 # 将对象 x 转换为表达式字符串\neval(str)               # 用来计算在字符串中的有效 Python 表达式，并返回一个对象\ntuple(s)                # 将序列 s 转换为一个元组\nlist(s)                 # 将序列 s 转换为一个列表\nchr(x)                  # 将一个整数转换为一个字符\nunichr(x)               # 将一个整数转换为 Unicode 字符\nord(x)                  # 将一个字符转换为它的整数值\nhex(x)                  # 将一个整数转换为一个十六进制字符串\noct(x)                  # 将一个整数转换为一个八进制字符串\n</code></pre>\n<h1>函数</h1>\n<pre><code class=\"language-py\">def fib(n: int) -&gt; None:    # 注意，类型定义仅作语法演示\n    &quot;&quot;&quot;文档注释内容&quot;&quot;&quot;\n    a, b = 0, 1\n    while a &lt; n:\n        print(a, end=&#39; &#39;)\n        a, b = b, a+b\n    pass\n</code></pre>\n<p>支持默认值和关键字参数</p>\n<pre><code class=\"language-py\">def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):\n#     -----------    ----------     ----------\n#       |             |                  |\n#       |        位置或关键字参数           |\n#       |                                - 仅允许关键词参数\n#        -- 仅允许位置参数\n</code></pre>\n<p>支持 lambda</p>\n<pre><code class=\"language-py\">f = lambda a, b: a+b\nf(1, 2) # 3\n\ndef f(a, b):\n    return a + b\n\nff = f\n</code></pre>\n<p>支持解包</p>\n<pre><code class=\"language-py\">def concat(*args, sep=&quot;/&quot;):\n    return sep.join(args)\n\nconcat(&quot;aaa&quot;, &quot;bbb&quot;, sep=&quot;.&quot;) # aaabbb\n\ndef params(pos1, pos2):\n    print(pos1 + pos2)\n\nparams(**{&quot;pos1&quot;: &quot;aaa&quot;, &quot;pos2&quot;: &quot;bbb&quot;}) # aaabbb\nparams(*(&quot;aaa&quot;,&quot;bbb&quot;)) # aaabbb\nparams(*[&quot;aaa&quot;,&quot;bbb&quot;]) # aaabbb\nparams(*{&quot;aaa&quot;,&quot;bbb&quot;}) # aaabbb\n\n\ndef f(param, *args, **map):\n    print(param)\n    print(args)\n    print(map)\n\nf(123, &quot;aaa&quot;, &quot;bbb&quot;, k1=&quot;v1&quot;, k2=&quot;v2&quot;)\n# ---    --------     --------------\n#  |         |               |\n# param=123  |               |\n#         args=(&#39;aaa&#39;,&#39;bbb&#39;) |\n#                          map={&#39;k1&#39;:&#39;v1&#39;,&#39;k2&#39;:&#39;v2&#39;}\n</code></pre>\n<h1>简单输入输出</h1>\n<pre><code class=\"language-py\">###################\nimport sys\nprint(sys.argv)\n###################\na = input(&quot;请输入&quot;)\nprint(a)\n###################\n</code></pre>\n<h1>序列 sequence</h1>\n<h2>字符串 str</h2>\n<p>这里提到字符串是因为字符串也是序列的一种<br>和列表一样可以使用切片</p>\n<h2>列表 list</h2>\n<pre><code class=\"language-py\">list = [1, &quot;2&quot;, 3.0]\n\nlist.append(x)\nlist.extend(iterable)\nlist.insert(i, x)\nlist.remove(x)\nlist.pop([i])\nlist.clear()\nlist.index(x[, start[, end]])\nlist.count(x)\nlist.sort(*, key=None, reverse=False)\nlist.reverse()\nlist.copy()\n</code></pre>\n<h2>切片 slice</h2>\n<pre><code class=\"language-py\">del list[0]\ndel list[2:4]\ndel list[:]\n</code></pre>\n<h2>元组 tuple</h2>\n<pre><code class=\"language-py\">tuple = (&quot;hello&quot;, 123)\n# or\ntuple = &quot;hello&quot;, 123\n\nx, y = tuple\n</code></pre>\n<h2>集合 set</h2>\n<pre><code class=\"language-py\">set = {&#39;a&#39;, &#39;b&#39;, &#39;c&#39;}\n# or\nset = set(&quot;abc&quot;)\nset = set(list)\n</code></pre>\n<h2>字典 dict</h2>\n<pre><code class=\"language-py\">dict = {&#39;k&#39;: &#39;v&#39;, 1: 2}\n# or\ndict = dict(k=&#39;v&#39;, x=&#39;y&#39;)\n# or\ndict = dict([(&#39;k&#39;,&#39;v&#39;), (1, 2)])\n\ndict[&#39;key&#39;] = &#39;value&#39;\ndel dict[&#39;key&#39;]\n&#39;k&#39; in dict # True\n&#39;k&#39; not in dict # False\n</code></pre>\n<h2>迭代器</h2>\n<p>迭代器可用于 for in 循环</p>\n<pre><code class=\"language-py\">range(n)     # [0, n)\nrange(a, b)  # [a, b)\nreversed(range(...))\n</code></pre>\n<pre><code class=\"language-py\">for k, v in dict.items()\nfor i, v in enumerate(list)\nfor v1, v2 in zip(list1, list2)\n</code></pre>\n<pre><code class=\"language-py\">iter1 = map(lambda x: x**2, range(10))\nlist1 = list(iter1)\n</code></pre>\n<p>迭代器原理</p>\n<pre><code class=\"language-py\">from typing import Iterable, Iterator\n\n\nclass IterClass(Iterator):  # 迭代器对象\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        return 123\n        raise StopIteration  # 抛出此异常表示终止迭代器\n\n\nclass CanIterClass(Iterable):  # 可迭代对象\n    def __iter__(self):\n        return IterClass()\n\n\nobj = CanIterClass()\nit = iter(obj)\nni = next(it)\n</code></pre>\n<h2>生成器</h2>\n<pre><code class=\"language-py\">def gen():\n    yield 123\n\n\nit = gen()\nni = next(it)\n\n#  支持推导\nit = (123 for i in range(10))\n</code></pre>\n<h2>推导式</h2>\n<p>快速生成字面量</p>\n<pre><code class=\"language-py\">list = [x**2 for x in range(10)]\nset = {x for x in &#39;abracadabra&#39; if x not in &#39;abc&#39;}\ndict = {x: x**2 for x in (2, 4, 6)}\n</code></pre>\n<h1>模块化</h1>\n<p><a href=\"https://docs.python.org/zh-cn/3/tutorial/modules.html\">https://docs.python.org/zh-cn/3/tutorial/modules.html</a></p>\n<pre><code class=\"language-py\">import math\nfrom math import sin, cos\nfrom math import *\nimport math as mathematics\nfrom math import sin as my_sin, cos as my_cos\n</code></pre>\n<p><code>__init__.py</code> 表示目录为包<br>该脚本可指定导出的模块列表，不指定则导出全部</p>\n<pre><code class=\"language-py\">__all__ = [&quot;funcXXX&quot;, &quot;classXXX&quot;, &quot;varXXX&quot;]\n</code></pre>\n<p>目录结构</p>\n<pre><code>package/\n        __init__.py\n        model/\n                __init__.py\n                m1.py\n                m2.py\n        view/\n                __init__.py\n                v1.py\n                v2.py\n        controller/\n                __init__.py\n                c1.py\n                c2.py\n</code></pre>\n<p>项目管理可用 <code>virtualenv</code></p>\n<h1>错误处理</h1>\n<p><a href=\"https://docs.python.org/zh-cn/3/tutorial/errors.html\">https://docs.python.org/zh-cn/3/tutorial/errors.html</a></p>\n<pre><code class=\"language-py\">def func():\n    raise ConnectionError\n\ntry:\n    func()\nexcept ConnectionError as exc:\n    raise RuntimeError(&#39;Failed to open database&#39;) from exc\n\n\n\ndef divide(x, y):\n    try:\n        result = x / y\n    except (ZeroDivisionError, Exception):\n        print(&quot;division by zero!&quot;)\n    else:\n        print(&quot;result is&quot;, result)\n    finally:\n        print(&quot;executing finally clause&quot;)\n\n\nwith open(&quot;myfile.txt&quot;) as f:\n    for line in f:\n        print(line, end=&quot;&quot;)\n</code></pre>\n<h1>面向对象</h1>\n<pre><code class=\"language-py\">class MyClass:\n    &quot;&quot;&quot;文档注释&quot;&quot;&quot;\n    i = 123\n\n    def __init__(self, param):\n        self.param = param\n\n    def f(self):  # 第一个参数必须为 self\n        print(&#39;hello world&#39;)\n\n\nc = MyClass(&quot;some_str&quot;)\nprint(MyClass.i)  # 123\nprint(c.i)  # 123\nprint(c.param)  # some_str\nc.f()  # 调用 MyClass.f(c)\n</code></pre>\n<pre><code class=\"language-py\">class DerivedClassName(BaseClassName):\n    pass\n\n\nclass DerivedClassName(Base1, Base2, Base3):\n    pass\n</code></pre>\n<h1>协程</h1>\n<pre><code class=\"language-py\">import asyncio\n\n\nasync def test():\n    return &#39;test&#39;\n\n\nasync def async_function(*params):\n    s = await test()\n    return &quot;{} {}&quot;.format(s, &#39;,&#39;.join(list(map(str, params))))\n\n\ntry:\n    async_function(123, 456).send(None)\nexcept StopIteration as e:\n    print(e.value)  # async_function 的返回值\n\n\nasyncio.run(async_function(123, 456))  # 得不到返回值\n</code></pre>\n","tags":["python"]},{"id":"kotlin-intro","url":"https://yieldray.fun/posts/kotlin-intro","title":"kotlin语法入门","date_published":"2022-08-30T19:00:00.000Z","date_modified":"2022-08-30T19:00:00.000Z","content_text":"<p>简单的语法入门，代码摘自<a href=\"http://kotlin.liying-cn.net/docs/reference_zh/\">文档</a>。<br>kotlin 语法较多，无法全部顾及，详细参见文档。<br>kotlin 能编译到 jvm 和 js，不一致的方面将不涉及。</p>\n<p><a href=\"https://play.kotlinlang.org/\">Playground</a><br><a href=\"https://play.kotlinlang.org/koans/overview\">Kotlin Koans</a><br><a href=\"https://kotlin.liying-cn.net/docs/reference_zh/keyword-reference.html\">关键字与操作符</a></p>\n<h1>注意</h1>\n<ul>\n<li>语句结束无需分号</li>\n<li>存在两种相等判断：结构相等（通过 equals() 判断）、引用相等（两个引用指向同一个对象）</li>\n</ul>\n<h1>变量与类型</h1>\n<p>基本数据类型：Byte Short Int Long Float Double<br>在允许范围内，变量自动推断为 Int 和 Double。否则，添加 l/L/f/F 后缀<br>因为对象引用的原因，没有隐式转换，需使用 toByte() toShort() toInt() toChar() 等<br>较之 java，kotlin 支持无符号整数：UByte UShort UInt ULong。作为字面量时，加上后缀 u/U 即可</p>\n<pre><code class=\"language-kotlin\">val constant: Int = 123\nvar variable = &quot;number$constant&quot;\nvar nullable: Int? = constant // 不加问号不装箱，加问号则装箱为（可null）对象。\n</code></pre>\n<p>数组通过模板类表达，对于基础类型数组则可以选用不装箱的优化版本 ByteArray, ShortArray, IntArray</p>\n<pre><code class=\"language-kotlin\">class Array&lt;T&gt; private constructor() {\n    val size: Int\n    operator fun get(index: Int): T\n    operator fun set(index: Int, value: T): Unit\n\n    operator fun iterator(): Iterator&lt;T&gt;\n    // ...\n}\n</code></pre>\n<p>对于不会出现的值，返回 Nothing 类型（类似于 typescript 的 never 类型）</p>\n<h1>类型转换</h1>\n<p>参见：<a href=\"http://kotlin.liying-cn.net/docs/reference_zh/typecasts.html\">http://kotlin.liying-cn.net/docs/reference_zh/typecasts.html</a></p>\n<h1>控制流</h1>\n<ul>\n<li>没有三元表达式，用 if/else 替代（最后的语句作为返回值）</li>\n<li>没有 switch，使用 when 替代（比 java 的 switch 更好地支持模式匹配）</li>\n<li>foreach 迭代器使用 in（<code>for (item in collection)</code>）而不是冒号</li>\n<li>支持数值范围 <code>for (i in 1..3)</code>（闭区间，与 rust 不同）<code>for (i in 1 until 10)</code>（左闭右开），<a href=\"http://kotlin.liying-cn.net/docs/reference_zh/ranges.html\">具体参考此处</a></li>\n<li>支持 try catch，并可以返回值</li>\n</ul>\n<pre><code class=\"language-kotlin\">when (x) {\n    1 -&gt; print(&quot;x == 1&quot;)\n    2 -&gt; print(&quot;x == 2&quot;)\n    else -&gt; {\n        print(&quot;x is neither 1 nor 2&quot;)\n    }\n}\n</code></pre>\n<h1>面向对象编程</h1>\n<p>实例化对象无需 new 操作符，kotlin 也没有 new 操作符</p>\n<pre><code class=\"language-kotlin\">class InitOrderDemo(name: String) {\n    val firstProperty = &quot;First property: $name&quot;.also(::println)\n\n    init {\n        println(&quot;First initializer block that prints $name&quot;)\n    }\n\n    val secondProperty = &quot;Second property: ${name.length}&quot;.also(::println)\n\n    init {\n        println(&quot;Second initializer block that prints ${name.length}&quot;)\n    }\n}\n</code></pre>\n<p>次构造器，此处主构造器中的 val/var 一并声明对象的属性，支持默认值</p>\n<pre><code class=\"language-kotlin\">class Person(val name: String) {\n    val children: MutableList&lt;Person&gt; = mutableListOf()\n    constructor(name: String, parent: Person) : this(name) {\n        parent.children.add(this)\n    }\n}\n</code></pre>\n<p>在继承中，对象、方法和属性默认不可覆盖，否则需要显示声明 open<br>接口中允许定义属性</p>\n<pre><code class=\"language-kotlin\">// abstract\nabstract class Polygon {\n    abstract fun draw()\n}\n\nclass Rectangle : Polygon() {\n    override fun draw() {\n        // 描绘长方形\n    }\n}\n// super\nopen class Rectangle {\n    open fun draw() { /* ... */ }\n}\n\ninterface Polygon {\n    fun draw() { /* ... */ } // 接口的成员默认是 &#39;open&#39; 的\n}\n\nclass Square() : Rectangle(), Polygon {\n    // 编译器要求 draw() 方法必须覆盖:\n    override fun draw() {\n        super&lt;Rectangle&gt;.draw() // 调用 Rectangle.draw()\n        super&lt;Polygon&gt;.draw() // 调用 Polygon.draw()\n    }\n}\n</code></pre>\n<p>可见度修饰符与 java 一致，但一些方面与之有差异。（具体见文档）<br>支持 getter/setter</p>\n<pre><code class=\"language-kotlin\">var stringRepresentation: String\n    get() = this.toString()\n    set(value) {\n        setDataFromString(value) // 解析字符串内容, 并将解析得到的值赋给对应的其他属性\n    }\n</code></pre>\n<p>支持扩展函数，该函数不是对象的成员，而是静态的</p>\n<pre><code class=\"language-kotlin\">fun MutableList&lt;Int&gt;.swap(index1: Int, index2: Int) {\n    val tmp = this[index1] // &#39;this&#39; 指代 list 实例\n    this[index1] = this[index2]\n    this[index2] = tmp\n}\n\nval list = mutableListOf(1, 2, 3)\nlist.swap(0, 2) // &#39;swap()&#39; 函数内的 &#39;this&#39; 将指向 &#39;list&#39; 的值\n</code></pre>\n<h1>泛型</h1>\n<p>泛型与 java 一样通过类型擦除实现<br>较之 java，没有使用 <code>extends super</code> 修饰符，而是借鉴 C#，使用 <code>in out</code> 修饰</p>\n<pre><code class=\"language-kotlin\">fun copy(from: Array&lt;out Any&gt;, to: Array&lt;Any&gt;) { ... }\nfun fill(dest: Array&lt;in String&gt;, value: String) { ... }\nfun &lt;T&gt; singletonList(item: T): List&lt;T&gt; { ... }\nfun &lt;T&gt; T.basicToString(): String { ... }\nfun &lt;T : Comparable&lt;T&gt;&gt; sort(list: List&lt;T&gt;) { ... }\n</code></pre>\n<h1>函数式编程</h1>\n<p>kotlin 函数的类型声明同 typescript，支持默认值。<br>支持局部函数（即函数嵌套函数），因此支持闭包。</p>\n<pre><code class=\"language-java\">fun double(x: Int) = x * 2\n// 支持不定参数\nfun &lt;T&gt; asList(vararg ts: T): List&lt;T&gt; {\n    val result = ArrayList&lt;T&gt;()\n    for (t in ts) // ts 变量的类型为 Array&lt;out T&gt;\n        result.add(t)\n    return result\n}\n</code></pre>\n<p><em>在 kotlin 中，函数是一等公民</em></p>\n<pre><code class=\"language-kotlin\">// 注意回调函数（是一个 lambda 函数）的类型声明，与 typescript 是相似的\nfun &lt;T, R&gt; Collection&lt;T&gt;.fold(\n    initial: R,\n    combine: (acc: R, nextElement: T) -&gt; R\n): R {\n    var accumulator: R = initial\n    for (element: T in this) {\n        accumulator = combine(accumulator, element)\n    }\n    return accumulator\n}\n\n// 使用\nval items = listOf(1, 2, 3, 4, 5)\n// Lambda 表达式是大括号括起的那部分代码.\nitems.fold(0, {\n    // 如果 Lambda 表达式有参数, 首先声明这些参数, 后面是 &#39;-&gt;&#39; 符\n    acc: Int, i: Int -&gt;\n    print(&quot;acc = $acc, i = $i, &quot;)\n    val result = acc + i\n    println(&quot;result = $result&quot;)\n    // Lambda 表达式内的最后一个表达式会被看作返回值:\n    result\n})\n\n// Lambda 表达式的参数类型如果可以推断得到, 那么参数类型的声明可以省略:\nval joinedToString = items.fold(&quot;Elements:&quot;, { acc, i -&gt; acc + &quot; &quot; + i })\n\n// 在高阶函数调用中也可以使用函数引用:\nval product = items.fold(1, Int::times)\n</code></pre>\n<p>支持类型别名</p>\n<pre><code class=\"language-kotlin\">typealias ClickHandler = (Button, ClickEvent) -&gt; Unit\n</code></pre>\n<p>支持匿名函数，作为函数字面值</p>\n<pre><code class=\"language-kotlin\">fun(s: String): Int { return s.toIntOrNull() ?: 0 }\n</code></pre>\n<p>函数引用与 java 一致，但 <code>f.invoke(x)</code>与<code>f(x)</code>是相同的</p>\n<pre><code class=\"language-kotlin\">val stringPlus: (String, String) -&gt; String = String::plus\nval intPlus: Int.(Int) -&gt; Int = Int::plus\n\nprintln(stringPlus.invoke(&quot;&lt;-&quot;, &quot;-&gt;&quot;))\nprintln(stringPlus(&quot;Hello, &quot;, &quot;world!&quot;))\n\nprintln(intPlus.invoke(1, 1))\nprintln(intPlus(1, 2))\nprintln(2.intPlus(3)) // 与扩展函数类似的调用方式\n</code></pre>\n<p>还可以使用接口实现函数，与 c++类似</p>\n<pre><code class=\"language-kotlin\">class IntTransformer: (Int) -&gt; Int {\n    override operator fun invoke(x: Int): Int = TODO()\n}\nval intFunction: (Int) -&gt; Int = IntTransformer()\n</code></pre>\n<p>此外，函数式接口与 java 的写法不同</p>\n<pre><code class=\"language-kotlin\">fun interface IntPredicate {\n   fun accept(i: Int): Boolean\n}\n\nval isEven = IntPredicate { it % 2 == 0 }\n\nfun main() {\n   println(&quot;Is 7 even? - ${isEven.accept(7)}&quot;)\n}\n</code></pre>\n<p>由于 lambda 函数细节较多，更多参见：<a href=\"http://kotlin.liying-cn.net/docs/reference_zh/lambdas.html\">http://kotlin.liying-cn.net/docs/reference_zh/lambdas.html</a></p>\n<h1>运算符重载</h1>\n<p>kotlin 支持运算符重载，参见：<a href=\"http://kotlin.liying-cn.net/docs/reference_zh/operator-overloading.html\">http://kotlin.liying-cn.net/docs/reference_zh/operator-overloading.html</a></p>\n<h1>作用域函数</h1>\n<p>参见：<a href=\"https://kotlin.liying-cn.net/docs/reference_zh/scope-functions.html\">https://kotlin.liying-cn.net/docs/reference_zh/scope-functions.html</a></p>\n<p>作用域函数是标准库提供的函数，类型声明如下：</p>\n<pre><code class=\"language-kotlin\">inline fun &lt;T, R&gt; T.let(block: (T) -&gt; R): R\ninline fun &lt;T, R&gt; T.run(block: T.() -&gt; R): R\ninline fun &lt;R&gt; run(block: () -&gt; R): R\ninline fun &lt;T, R&gt; with(receiver: T, block: T.() -&gt; R): R\ninline fun &lt;T&gt; T.apply(block: T.() -&gt; Unit): T\ninline fun &lt;T&gt; T.also(block: (T) -&gt; Unit): T\n</code></pre>\n<h1>语法糖</h1>\n<h2>安全调用操作符 <code>?.</code></h2>\n<pre><code class=\"language-kotlin\">val a = &quot;Kotlin&quot;\nval b: String? = null\nprintln(a?.length) // 安全调用操作符在这里是不必要的\nprintln(b?.length) // -&gt; null\n</code></pre>\n<h2>Elvis 操作符 <code>?:</code></h2>\n<p>若左侧为 null 则返回右侧的值</p>\n<pre><code class=\"language-kotlin\">val l = b?.length ?: -1\n// 相当于\nval l: Int = if (b != null) b.length else -1\n</code></pre>\n<h2>非 null 判定操作符 <code>!!</code></h2>\n<p>如果需要可能抛出的 NullPointerException 可以使用</p>\n<pre><code class=\"language-kotlin\">val l = b!!.length\n</code></pre>\n<h2>安全的类型转换 <code>as?</code></h2>\n<p>转换失败时返回 null</p>\n<pre><code class=\"language-kotlin\">val aInt: Int? = a as? Int\n</code></pre>\n<h2>解构声明</h2>\n<pre><code class=\"language-kotlin\">val (name, age) = person\n// 相当于\nval name = person.component1()\nval age = person.component2()\n// 其中，componentN() 函数需要标记为 operator（即 operator fun componentN）\n\n// 用下划线代替未使用的变量\nval (_, status) = getResult()\n</code></pre>\n<h1>异步与协程</h1>\n<p>kotlin 的协程采用的不是 async/await 模型</p>\n<p>参见：<a href=\"http://kotlin.liying-cn.net/docs/reference_zh/coroutines/coroutines-guide.html\">http://kotlin.liying-cn.net/docs/reference_zh/coroutines/coroutines-guide.html</a></p>\n","tags":["kotlin"]},{"id":"java-collections","url":"https://yieldray.fun/posts/java-collections","title":"java集合相关实用方法Collections","date_published":"2022-08-28T16:00:00.000Z","date_modified":"2022-08-28T16:00:00.000Z","content_text":"<p>集合实用方法不仅可以操作 Collection，还能操作 Map<br>再次注意：Collection 和 Map 是两个完全不同的接口</p>\n<p>集合类本身，见<a href=\"/posts/java-collection/\">此处</a><br>数组相关实用方法，见<a href=\"/posts/java-arrays/\">此处</a></p>\n<h1>java.util.Collections</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html</a></p>\n<p>java.util.Collections 实用类的所有属性皆为静态。</p>\n<h1>关于 Collection</h1>\n<h2><code>boolean addAll(Collection&lt;? super T&gt; c, T... elements)</code></h2>\n<p>添加所有给定元素到指定集合。若集合被修改，返回 true，否则 false</p>\n<pre><code class=\"language-java\">List&lt;Integer&gt; list = new ArrayList&lt;Integer&gt;();\nCollections.addAll(list, 1, 2, 3);\nSystem.out.println(list); // [1, 2, 3]\n</code></pre>\n<h2><code>boolean disjoint(Collection&lt;?&gt; c1, Collection&lt;?&gt; c2)</code></h2>\n<p>检查两个集合是否没有相同元素</p>\n<pre><code class=\"language-java\">boolean f = Collections.disjoint(List.of(1, 2), List.of(2, 3));\nboolean t = Collections.disjoint(List.of(1, 2), List.of(&quot;2&quot;, &quot;3&quot;));\nSystem.out.println(f); // false\nSystem.out.println(t); // true\n</code></pre>\n<h2><code>int frequency(Collection&lt;?&gt; c, Object o)</code></h2>\n<p>返回指定元素在集合中出现的次数</p>\n<h2><code>T max(Collection&lt;? extends T&gt; coll, Comparator&lt;? super T&gt; comp)</code></h2>\n<p>返回给定集合中的最大/小值，方法的第二个参数可选，可以传入比较器 <code>Comparator</code></p>\n<h2><code>Comparator&lt;T&gt; reverseOrder()</code></h2>\n<h1>关于 Enumeration</h1>\n<h2><code>Enumeration&lt;T&gt; enumeration(Collection&lt;T&gt; c)</code></h2>\n<h2><code>ArrayList&lt;T&gt; list(Enumeration&lt;T&gt; e)</code></h2>\n<h1>关于 List</h1>\n<h2>binarySearch</h2>\n<p>二分查找</p>\n<pre><code class=\"language-java\">int binarySearch(List&lt;? extends Comparable&lt;? super T&gt;&gt; list, T key);\nint binarySearch(List&lt;? extends T&gt; list, T key, Comparator&lt;? super T&gt; c);\n</code></pre>\n<h2><code>void copy(List&lt;? super T&gt; dest, List&lt;? extends T&gt; src)</code></h2>\n<p>将源列表中的所有元素复制到目标列表中。操作后，目标列表中每个复制元素的索引将与其在源列表中的索引相同。<br>目标列表的大小必须大于或等于源列表的大小。如果它更大，则目标列表中的其余元素不受影响。</p>\n<h2><code>void fill(List&lt;? super T&gt; list, T obj)</code></h2>\n<p>将指定列表的所有位置上填充给定元素</p>\n<h2><code>int indexOfSubList/lastIndexOfSubList(List&lt;?&gt; source, List&lt;?&gt; target)</code></h2>\n<p>以正序（逆序）查找，返回目标列表在源列表中首次出现的位置</p>\n<h2><code>List&lt;T&gt; nCopies(int n, T o)</code></h2>\n<pre><code class=\"language-java\">List&lt;Integer&gt; list = List.of(1, 2, 3);\nList&lt;List&lt;Integer&gt;&gt; repeatList = Collections.nCopies(3, list);\nSystem.out.println(repeatList);\n// [[1, 2, 3], [1, 2, 3], [1, 2, 3]]\n</code></pre>\n<h2><code>boolean replaceAll(List&lt;T&gt; list, T oldVal, T newVal)</code></h2>\n<p>将列表中的所有指定元素替换为给定元素。如果造成了替换则返回 true，否则 false</p>\n<h2><code>void reverse(List&lt;?&gt; list)</code></h2>\n<p>将指定列表倒转顺序</p>\n<h2><code>void rotate(List&lt;?&gt; list, int distance)</code></h2>\n<pre><code class=\"language-java\">var list = new ArrayList&lt;Integer&gt;();\nlist.addAll(List.of(1, 2, 3, 4, 5));\nCollections.rotate(list, 3);\nSystem.out.println(list); // [3, 4, 5, 1, 2]\n</code></pre>\n<h2><code>void shuffle(List&lt;?&gt; list, Random rnd)</code></h2>\n<p>洗牌。Random 可选，否则使用默认值。</p>\n<pre><code class=\"language-java\">var list = new ArrayList&lt;Integer&gt;();\nlist.addAll(List.of(1, 2, 3, 4, 5));\nCollections.shuffle(list);// 使用默认值，每次输出都不同\nSystem.out.println(list);\n</code></pre>\n<h2><code>void sort(List&lt;T&gt; list, Comparator&lt;? super T&gt; c)</code></h2>\n<p>排序。Comparator 不指定时使用元素的自然比较(natural ordering)</p>\n<h2><code>void swap(List&lt;?&gt; list, int i, int j)</code></h2>\n<p>将列表中指定下标的两个元素交换</p>\n<h1>关于 Set</h1>\n<h2><code>Set&lt;E&gt; newSetFromMap(Map&lt;E,Boolean&gt; map)</code></h2>\n<pre><code class=\"language-java\">Set&lt;Object&gt; weakHashSet = Collections.newSetFromMap(new WeakHashMap&lt;Object, Boolean&gt;());\n</code></pre>\n<h1>关于 Queue</h1>\n<h2><code>Queue&lt;T&gt; asLifoQueue(Deque&lt;T&gt; deque)</code></h2>\n<p>Returns a view of a Deque as a Last-in-first-out (Lifo) Queue.</p>\n<h1>singletonXXX，创建<em>单元素集合</em></h1>\n<p>实际上，使用集合对应的静态方法 of() 更为方便。</p>\n<h2>singleton</h2>\n<p>注意，此方法是创建单元素 Set，而不可能是 Collection<br>没有所谓 singletonSet 方法</p>\n<pre><code class=\"language-java\">Set&lt;String&gt; set1 = Collections.singleton(&quot;single&quot;);\nSet&lt;String&gt; set2 = Set.of(&quot;single&quot;);\n</code></pre>\n<h2>singletonList</h2>\n<pre><code class=\"language-java\">List&lt;String&gt; list1 = List.of(&quot;single&quot;);\nList&lt;String&gt; list2 = Collections.singletonList(&quot;single&quot;);\n</code></pre>\n<h2>singletonMap</h2>\n<pre><code class=\"language-java\">Map&lt;String, String&gt; map1 = Collections.singletonMap(&quot;k&quot;, &quot;v&quot;);\nMap&lt;String, String&gt; map2 = Map.of(&quot;k&quot;, &quot;v&quot;);\n</code></pre>\n<h1>emptyXXX，创建<em>空集合</em></h1>\n<p>emptyEnumeration<br>emptyIterator<br>emptyList<br>emptyListIterator<br>emptyMap<br>emptyNavigableMap<br>emptyNavigableSet<br>emptySet<br>emptySortedMap<br>emptySortedSet</p>\n<pre><code class=\"language-java\">List&lt;String&gt; list = Collections.emptyList();\n</code></pre>\n<h1>synchronizedXXX，基于原集合生成<em>线程安全集合</em></h1>\n<p>synchronizedCollection<br>synchronizedList<br>synchronizedMap<br>synchronizedNavigableMap<br>synchronizedNavigableSet<br>synchronizedSet<br>synchronizedSortedMap<br>synchronizedSortedSet</p>\n<pre><code class=\"language-java\">List&lt;Integer&gt; syncList = Collections.synchronizedList(List.of(1, 2, 3));\n</code></pre>\n<h1>checkXXX，基于原集合生成<em>动态类型安全视图(dynamically typesafe view)集合</em></h1>\n<p>checkedCollection<br>checkedList<br>checkedMap<br>checkedNavigableMap<br>checkedNavigableSet<br>checkedQueue<br>checkedSet<br>checkedSortedMap<br>checkedSortedSet</p>\n<pre><code class=\"language-java\">List&lt;Integer&gt; list = new ArrayList&lt;&gt;();\nlist.addAll(List.of(1, 2));\nCollections.addAll(list, 3, 4);\nList&lt;Integer&gt; checkedList = Collections.checkedList(list, Integer.class);\ncheckedList.add(5);\nSystem.out.println(list); // [1, 2, 3, 4, 5]\nSystem.out.println(checkedList); // [1, 2, 3, 4, 5]\n</code></pre>\n<h1>unmodifiableXXX，基于原集合生成<em>不可变视图集合</em></h1>\n<p>原理是创建代理对象，拦截所有修改方法。因此，原集合仍可修改。<br>一些情况下看使用集合对应的静态方法 of() 替代。</p>\n<p>unmodifiableCollection<br>unmodifiableList<br>unmodifiableMap<br>unmodifiableNavigableMap<br>unmodifiableNavigableSet<br>unmodifiableSet<br>unmodifiableSortedMap<br>unmodifiableSortedSet</p>\n<pre><code class=\"language-java\">List&lt;Integer&gt; list = new ArrayList&lt;&gt;();\nlist.add(1);\nlist.add(2);\nList&lt;Integer&gt; unmod = Collections.unmodifiableList(list);\nunmod.add(3); // UnsupportedOperationException\n</code></pre>\n","tags":["java"]},{"id":"java-collection","url":"https://yieldray.fun/posts/java-collection","title":"java之Collection","date_published":"2022-08-27T22:22:22.000Z","date_modified":"2022-08-27T22:22:22.000Z","content_text":"<h1>概述</h1>\n<p>此篇为 Collection，关于 Map，<a href=\"/posts/java-map/\">见此</a></p>\n<p><img src=\"https://docs.oracle.com/javase/tutorial/figures/collections/colls-coreInterfaces.gif\" alt=\"The core collection interfaces\"></p>\n<p><img src=\"https://s2.loli.net/2022/06/27/LWkYGcNerBtuogQ.png\" alt=\"collection.png\"><br>摘自<a href=\"https://www.jishuchi.com/read/onjava8/12011\">https://www.jishuchi.com/read/onjava8/12011</a><br>黄色为接口，绿色为抽象类，蓝色为具体类。虚线箭头表示实现关系，实线箭头表示继承关系。</p>\n<p><code>java.util.*</code></p>\n<table>\n<thead>\n<tr>\n<th>Interface</th>\n<th>Hash Table</th>\n<th>Resizable Array</th>\n<th>Balanced Tree</th>\n<th>Linked List</th>\n<th>Hash Table + Linked List</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Set</td>\n<td>HashSet</td>\n<td></td>\n<td>TreeSet</td>\n<td></td>\n<td>LinkedHashSet</td>\n</tr>\n<tr>\n<td>List</td>\n<td></td>\n<td>ArrayList</td>\n<td></td>\n<td>LinkedList</td>\n<td></td>\n</tr>\n<tr>\n<td>Deque</td>\n<td></td>\n<td>ArrayDeque</td>\n<td></td>\n<td>LinkedList</td>\n<td></td>\n</tr>\n<tr>\n<td>Map</td>\n<td>HashMap</td>\n<td></td>\n<td>TreeMap</td>\n<td></td>\n<td>LinkedHashMap</td>\n</tr>\n</tbody></table>\n<p><a href=\"https://docs.oracle.com/javase/tutorial/collections/index.html\">https://docs.oracle.com/javase/tutorial/collections/index.html</a></p>\n<h1>Iterator</h1>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html</a></p>\n<pre><code class=\"language-java\">public interface Iterator&lt;E&gt; {\n    boolean hasNext();\n    E next();\n    default void remove() {\n        throw new UnsupportedOperationException(&quot;remove&quot;);\n    }\n    default void forEachRemaining(Consumer&lt;? super E&gt; action) {\n        Objects.requireNonNull(action);\n        while (hasNext())\n            action.accept(next());\n    }\n}\n</code></pre>\n<p>若实现 Iterable 接口并实现 Iterator 方法，则还可以使用 for in 遍历<br>（注意，of 方法返回的集合是不可修改的）</p>\n<pre><code class=\"language-java\">List&lt;Integer&gt; list = List.of(21, 13, 32);\n\nfor (int i = 0; i &lt; list.size(); i++)\n    System.out.print(list.get(i) + &quot;,&quot;);\n\n\nIterator it = list.iterator();\nwhile (it.hasNext())\n    System.out.print(it.next() + &quot;,&quot;);\n\nfor (Integer i : list)\n    System.out.print(i + &quot;,&quot;);\n</code></pre>\n<h1>Collection</h1>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html</a></p>\n<pre><code class=\"language-java\">public interface Collection&lt;E&gt; extends Iterable&lt;E&gt; {\n    int size();\n    boolean isEmpty();\n    boolean contains(Object o);\n    Iterator&lt;E&gt; iterator();\n    Object[] toArray();\n    &lt;T&gt; T[] toArray(T[] a);\n    default &lt;T&gt; T[] toArray(IntFunction&lt;T[]&gt; generator);\n    boolean add(E e);\n    boolean remove(Object o);\n    boolean containsAll(Collection&lt;?&gt; c);\n    boolean addAll(Collection&lt;? extends E&gt; c);\n    boolean removeAll(Collection&lt;?&gt; c);\n    default boolean removeIf(Predicate&lt;? super E&gt; filter);\n    boolean retainAll(Collection&lt;?&gt; c);\n    void clear();\n    default Spliterator&lt;E&gt; spliterator();\n    default Stream&lt;E&gt; stream();\n    default Stream&lt;E&gt; parallelStream();\n    // 省略继承 Object 方法\n}\n</code></pre>\n<h2>Set</h2>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Set.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Set.html</a></p>\n<pre><code class=\"language-java\">public interface Set&lt;E&gt; extends Collection&lt;E&gt; {\n    // 省略继承 Collection&lt;E&gt; 方法\n    // ! 以下 API 自 java 9\n    static &lt;E&gt; Set&lt;E&gt; of();\n    static &lt;E&gt; Set&lt;E&gt; of(E e1, E e2, ...省略, E e10);\n    static &lt;E&gt; Set&lt;E&gt; of(E... elements);\n    // ! 以下 API 自 java 10\n    static &lt;E&gt; Set&lt;E&gt; copyOf(Collection&lt;? extends E&gt; coll);\n}\n</code></pre>\n<h2>List</h2>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/List.html\">https://docs.oracle.com/javase/8/docs/api/java/util/List.html</a></p>\n<pre><code class=\"language-java\">public interface List&lt;E&gt; extends Collection&lt;E&gt; {\n    // 省略继承 Collection&lt;E&gt; 方法\n    boolean addAll(int index, Collection&lt;? extends E&gt; c);\n    default void replaceAll(UnaryOperator&lt;E&gt; operator);\n    default void sort(Comparator&lt;? super E&gt; c);\n    E get(int index);\n    E set(int index, E element);\n    void add(int index, E element);\n    E remove(int index);\n    int indexOf(Object o);\n    int lastIndexOf(Object o);\n    ListIterator&lt;E&gt; listIterator();\n    ListIterator&lt;E&gt; listIterator(int index);\n    List&lt;E&gt; subList(int fromIndex, int toIndex);\n    // ! 以下 API 自 java 9\n    static &lt;E&gt; List&lt;E&gt; of();\n    static &lt;E&gt; List&lt;E&gt; of(E e1, E e2,...省略, E e10);\n    static &lt;E&gt; List&lt;E&gt; of(E... elements);\n    // ! 以下 API 自 java 10\n    static &lt;E&gt; List&lt;E&gt; copyOf(Collection&lt;? extends E&gt; coll);\n}\n</code></pre>\n<h2>Queue</h2>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Queue.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Queue.html</a></p>\n<p><em>队列方法总结</em></p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>抛出异常</th>\n<th>仅返回特定值</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Insert</td>\n<td>add(e)</td>\n<td>offer(e)</td>\n</tr>\n<tr>\n<td>Remove</td>\n<td>remove()</td>\n<td>poll()</td>\n</tr>\n<tr>\n<td>Examine</td>\n<td>element()</td>\n<td>peek()</td>\n</tr>\n</tbody></table>\n<pre><code class=\"language-java\">public interface Queue&lt;E&gt; extends Collection&lt;E&gt; {\n    boolean add(E e);\n    boolean offer(E e);V\n    E remove();\n    E poll();\n    E element()\n    E peek();\n}\n</code></pre>\n<h2>Deque</h2>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Deque.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Deque.html</a></p>\n<p><em>队列方法总结</em></p>\n<pre><code>         首元素                   尾元素\n</code></pre>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>抛出异常</th>\n<th>返回特定值</th>\n<th>抛出异常</th>\n<th>返回特定值</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Insert</td>\n<td>addFirst(e)</td>\n<td>offerFirst(e)</td>\n<td>addLast(e)</td>\n<td>offerLast(e)</td>\n</tr>\n<tr>\n<td>Remove</td>\n<td>removeFirst()</td>\n<td>pollFirst()</td>\n<td>removeLast()</td>\n<td>pollLast()</td>\n</tr>\n<tr>\n<td>Examine</td>\n<td>getFirst()</td>\n<td>peekFirst()</td>\n<td>getLast()</td>\n<td>peekLast()</td>\n</tr>\n</tbody></table>\n<hr>\n<p><em>队列方法与双端队列方法的比较</em></p>\n<table>\n<thead>\n<tr>\n<th>队列方法</th>\n<th>等效双端队列方法</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>add(e)</td>\n<td>addLast(e)</td>\n</tr>\n<tr>\n<td>offer(e)</td>\n<td>offerLast(e)</td>\n</tr>\n<tr>\n<td>remove()</td>\n<td>removeFirst()</td>\n</tr>\n<tr>\n<td>poll()</td>\n<td>pollFirst()</td>\n</tr>\n<tr>\n<td>element()</td>\n<td>getFirst()</td>\n</tr>\n<tr>\n<td>peek()</td>\n<td>peekFirst()</td>\n</tr>\n</tbody></table>\n<hr>\n<p><em>栈方法与双端队列的比较</em></p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>栈方法</th>\n<th>等效双端队列方法</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>入栈</td>\n<td>push(e)</td>\n<td>addFirst(e)</td>\n</tr>\n<tr>\n<td>出栈并返回</td>\n<td>pop()</td>\n<td>removeFirst()</td>\n</tr>\n<tr>\n<td>查看栈顶</td>\n<td>peek()</td>\n<td>peekFirst()</td>\n</tr>\n</tbody></table>\n<hr>\n<pre><code class=\"language-java\">public interface Deque&lt;E&gt; extends Queue&lt;E&gt; {\n    void addFirst(E e);\n    void addLast(E e);\n    boolean offerFirst(E e);\n    boolean offerLast(E e);\n    E removeFirst();\n    E removeLast();\n    E pollFirst();\n    E pollLast();\n    E getFirst();\n    E getLast();\n    E peekFirst();\n    E peekLast();\n    boolean removeFirstOccurrence(Object o);\n    boolean removeLastOccurrence(Object o);\n    boolean add(E e);\n    boolean offer(E e);\n    E remove();\n    E poll();\n    E element();\n    E peek();\n    boolean addAll(Collection&lt;? extends E&gt; c);\n    void push(E e);\n    E pop();\n    boolean remove(Object o);\n    boolean contains(Object o);\n    int size();\n    Iterator&lt;E&gt; iterator();\n    Iterator&lt;E&gt; descendingIterator();\n}\n</code></pre>\n<h2>Priority Queue</h2>\n<pre><code class=\"language-java\">public class PriorityQueue&lt;E&gt; extends AbstractQueue&lt;E&gt; implements java.io.Serializable {\n    int size;\n    public PriorityQueue();\n    public PriorityQueue(int initialCapacity);\n    public PriorityQueue(Comparator&lt;? super E&gt; comparator);\n    public PriorityQueue(int initialCapacity, Comparator&lt;? super E&gt; comparator);\n    public PriorityQueue(Collection&lt;? extends E&gt; c);\n    public PriorityQueue(PriorityQueue&lt;? extends E&gt; c);\n    public PriorityQueue(SortedSet&lt;? extends E&gt; c);\n    public boolean add(E e)\n    public boolean offer(E e)\n    public E peek()\n    public boolean remove(Object o)\n    public boolean contains(Object o);\n    public Object[] toArray()\n    public &lt;T&gt; T[] toArray(T[] a)\n    public Iterator&lt;E&gt; iterator();\n    public boolean removeIf(Predicate&lt;? super E&gt; filter);\n    public boolean removeAll(Collection&lt;?&gt; c);\n    public boolean retainAll(Collection&lt;?&gt; c);\n    public void forEach(Consumer&lt;? super E&gt; action);\n}\n</code></pre>\n","tags":["java"]},{"id":"java-arrays","url":"https://yieldray.fun/posts/java-arrays","title":"java数组相关实用方法Arrays","date_published":"2022-08-26T13:00:00.000Z","date_modified":"2022-08-26T13:00:00.000Z","content_text":"<p>java 数组是特殊的类。数组本身继承自 Object，因此具有 clone(), equals(), hashCode(), toString(), getClass() 方法。<br>操作数组需要使用实用类 <code>java.util.Arrays</code></p>\n<h1>java.util.Arrays</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Arrays.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Arrays.html</a></p>\n<p>java.util.Arrays 实用类的所有方法都是静态方法。</p>\n<h2>Arrays.asList(T... a)</h2>\n<p>此方法既可以传入一个数组，也可以传入多个同类型参数以转换为列表</p>\n<pre><code class=\"language-java\">List&lt;Integer&gt; even = Arrays.asList(new Integer[]{0, 2, 4, 6});\nSystem.out.println(even); // [0, 2, 4, 6]\nList&lt;Integer&gt; odd = Arrays.asList(1, 3, 5, 7);\nSystem.out.println(odd); // [1, 3, 5, 7]\n</code></pre>\n<h2>Collection<E> 的 Object[] toArray() 和 <T> T[] toArray(T[] a) 方法</h2>\n<p>这两个方法是实例方法，用于将集合转换为数组。</p>\n<pre><code class=\"language-java\">Integer[] errs = (Integer[]) even.toArray(); // 报错（ClassCastException），无法转型  class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer;\nObject[] objs = even.toArray(); // 不传参，转换为 Object[]\nInteger[] ints_plus1 = even.toArray(new Integer[even.size() + 1]); // 传入参数以指定类型，声明的数组长度若大于集合长度，则多余元素为null\nInteger[] ints_minus1 = even.toArray(new Integer[even.size() - 1]); // 传入参数以指定类型，声明的数组长度若小于或等于集合长度，数组长度仍为集合长度\nSystem.out.println(Arrays.toString(ints_plus1)); // [0, 2, 4, 6, null]\nSystem.out.println(Arrays.toString(ints_minus1)); // [0, 2, 4, 6]\n</code></pre>\n<h2>Arrays.binarySearch()</h2>\n<p><strong>注意，二分查找需要数组已经有序!</strong></p>\n<pre><code class=\"language-java\">int[] arr = {1, 3, 5, 7, 9};\n// 指定 key\nSystem.out.println(Arrays.binarySearch(arr, 1)); // 0\nSystem.out.println(Arrays.binarySearch(arr, 2)); // -2\nSystem.out.println(Arrays.binarySearch(arr, 3)); // 1\nSystem.out.println(Arrays.binarySearch(arr, 4)); // -3\n// 指定 from, to, key\nSystem.out.println(Arrays.binarySearch(arr, 1, 3, 1)); // -2\n</code></pre>\n<h2>sort parallelSort</h2>\n<pre><code class=\"language-java\">int[] arr1 = {3, 9, 6, 1};\nint[] arr2 = arr1.clone();\nArrays.sort(arr1); // Timsort\nArrays.parallelSort(arr2); // 简而言之，多线程排序（归并）\nSystem.out.println(Arrays.toString(arr1)); // [1, 3, 6, 9]\nSystem.out.println(Arrays.toString(arr2)); // [1, 3, 6, 9]\n</code></pre>\n<h2>copyOf copyOfRange equals hashCode toString</h2>\n<pre><code class=\"language-java\">String[] arr = {&quot;Hello&quot;, &quot;World&quot;};\nString[] copied = Arrays.copyOf(arr, arr.length);\nSystem.out.println(Arrays.equals(arr, copied)); // =&gt; true\nString[] hello = Arrays.copyOfRange(arr, 0, 1);\nSystem.out.println(Arrays.toString(hello)); // =&gt; [Hello]\nSystem.out.println(Arrays.hashCode(arr)); // =&gt; -2053301055\nSystem.out.println(Arrays.hashCode(copied)); // =&gt; -2053301055\n</code></pre>\n<h2>deepEquals deepHashCode deepToString</h2>\n<pre><code class=\"language-java\">String[][] arr = {{&quot;Hello&quot;}, {&quot;World&quot;}};\n\nSystem.out.println(Arrays.toString(arr)); // =&gt; [[Ljava.lang.String;@16b98e56, [Ljava.lang.String;@7ef20235]\nSystem.out.println(Arrays.deepToString(arr)); // =&gt; [[Hello], [World]]\n\nSystem.out.println(Arrays.hashCode(arr)); // =&gt; 1063928416\nSystem.out.println(Arrays.deepHashCode(arr)); // =&gt; -2053300063\n\nString[] s1 = {&quot;Hello&quot;};\nString[] s2 = {&quot;World&quot;};\nString[][] composed = {s1, s2};\nSystem.out.println(Arrays.equals(arr, composed)); // =&gt; false\nSystem.out.println(Arrays.deepEquals(arr, composed)); // =&gt; true\n</code></pre>\n<h2>compare compareUnsigned mismatch</h2>\n<pre><code class=\"language-java\">import java.util.Arrays;\nimport java.util.Comparator;\n\npublic class Main {\n    public static void main(String[] args) {\n        String[] arr1 = {&quot;Hello&quot;};\n        String[] arr2 = {&quot;World&quot;};\n        System.out.println(Arrays.mismatch(arr1, arr2)); // 返回两个数组中第一个不一致元素的索引\n        System.out.println(Arrays.mismatch(arr1, arr2, new StringComparator())); // 数组元素一致时，返回-1\n        System.out.println(Arrays.compare(arr1, arr2)); // 比较字典序，返回 -1 | 0 | 1\n        System.out.println(Arrays.compare(arr1, arr2, new StringComparator())); //  给定比较器\n        System.out.println(Arrays.compareUnsigned(new int[]{1}, new int[]{2})); // 仅支持数字类型数组\n    }\n}\n\nclass StringComparator implements Comparator&lt;String&gt; {\n    @Override\n    public int compare(String s1, String s2) {\n        return 0; // 仅供测试\n    }\n}\n</code></pre>\n<h2>stream</h2>\n<p>另见：<a href=\"/posts/java-stream/\">java 流</a></p>\n<pre><code class=\"language-java\">int[] fib = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34};\nIntStream s = Arrays.stream(fib);\ns.forEach(System.out::println);\n</code></pre>\n<h2>fill setAll</h2>\n<pre><code class=\"language-java\">int[] arr = new int[3];\nSystem.out.println(Arrays.toString(arr)); // =&gt; [0, 0, 0]\nArrays.fill(arr, 6);\nSystem.out.println(Arrays.toString(arr)); // =&gt; [6, 6, 6]\nArrays.fill(arr, 0, 2, 8); // fromIndex, toIndex, val\nSystem.out.println(Arrays.toString(arr)); // =&gt; [8, 8, 6]\nArrays.setAll(arr, new IntUnaryOperator() { // 通过索引设置数组元素\n    @Override\n    public int applyAsInt(int operand) {\n        return operand * 2; // 0, 1, 2 =&gt; 0, 2, 4\n    }\n});\nSystem.out.println(Arrays.toString(arr)); // [0, 2, 4]\n</code></pre>\n<h2>spliterator</h2>\n<p>分割数组，支持并行处理</p>\n<pre><code class=\"language-java\">int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\nSpliterator s = Arrays.spliterator(arr);\nSpliterator s1 = s.trySplit();\nSpliterator s2 = s.trySplit();\nvar printEach = new Consumer&lt;Integer&gt;() {\n    @Override\n    public void accept(Integer i) {\n        System.out.print(i);\n    }\n};\nSystem.out.print(&quot;s1: &quot;);\ns1.forEachRemaining(printEach); // s1: 12345\nSystem.out.print(&quot;\\ns2: &quot;);\ns2.forEachRemaining(printEach); // s2: 67\n</code></pre>\n<h2>parallelSort parallelPrefix parallelSetAll</h2>\n<p>parallel 系列函数用于多线程时，并行运算</p>\n","tags":["java"]},{"id":"java-map","url":"https://yieldray.fun/posts/java-map","title":"java之Map","date_published":"2022-08-25T16:00:00.000Z","date_modified":"2022-08-25T16:00:00.000Z","content_text":"<p>此篇为 <code>Map</code>，关于 <code>Collection</code> 及集合类概述，<a href=\"/posts/java-collection/\">见此</a></p>\n<h1>Map</h1>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Map.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Map.html</a></p>\n<pre><code class=\"language-java\">public interface Map&lt;K, V&gt; {\n    int size();\n    boolean isEmpty();\n    boolean containsKey(Object key);\n    boolean containsValue(Object value);\n    V get(Object key);\n    V put(K key, V value);\n    V remove(Object key);\n    void putAll(Map&lt;? extends K, ? extends V&gt; m);\n    void clear();\n    Set&lt;K&gt; keySet();\n    Collection&lt;V&gt; values();\n    Set&lt;Map.Entry&lt;K, V&gt;&gt; entrySet();\n    interface Entry&lt;K, V&gt; {\n        K getKey();\n        V getValue();\n        V setValue(V value);\n        boolean equals(Object o);\n        int hashCode();\n        public static &lt;K extends Comparable&lt;? super K&gt;, V&gt; Comparator&lt;Map.Entry&lt;K, V&gt;&gt; comparingByKey();\n        public static &lt;K, V extends Comparable&lt;? super V&gt;&gt; Comparator&lt;Map.Entry&lt;K, V&gt;&gt; comparingByValue();\n        public static &lt;K, V&gt; Comparator&lt;Map.Entry&lt;K, V&gt;&gt; comparingByKey(Comparator&lt;? super K&gt; cmp);\n        public static &lt;K, V&gt; Comparator&lt;Map.Entry&lt;K, V&gt;&gt; comparingByValue(Comparator&lt;? super V&gt; cmp);\n        // ! 以下 API 自 java 17\n        public static &lt;K, V&gt; Map.Entry&lt;K, V&gt; copyOf(Map.Entry&lt;? extends K, ? extends V&gt; e);\n    }\n    boolean equals(Object o);\n    int hashCode();\n    default V getOrDefault(Object key, V defaultValue);\n    default void forEach(BiConsumer&lt;? super K, ? super V&gt; action);\n    default void replaceAll(BiFunction&lt;? super K, ? super V, ? extends V&gt; function);\n    default V putIfAbsent(K key, V value);\n    default boolean remove(Object key, Object value);\n    default boolean replace(K key, V oldValue, V newValue);\n    default V replace(K key, V value);\n    default V computeIfAbsent(K key, Function&lt;? super K, ? extends V&gt; mappingFunction);\n    default V computeIfPresent(K key, BiFunction&lt;? super K, ? super V, ? extends V&gt; remappingFunction);\n    default V compute(K key, BiFunction&lt;? super K, ? super V, ? extends V&gt; remappingFunction);\n    default V merge(K key, V value, BiFunction&lt;? super V, ? super V, ? extends V&gt; remappingFunction);\n    // ! 以下 API 自 java 9\n    static &lt;K, V&gt; Map&lt;K, V&gt; of();\n    static &lt;K, V&gt; Map&lt;K, V&gt; of(K k1, V v1, K k2, V v2,...省略, K k10, V v10);\n    static &lt;K, V&gt; Map&lt;K, V&gt; ofEntries(Entry&lt;? extends K, ? extends V&gt;... entries);\n    static &lt;K, V&gt; Entry&lt;K, V&gt; entry(K k, V v);\n    // ! 以下 API 自 java 10\n    static &lt;K, V&gt; Map&lt;K, V&gt; copyOf(Map&lt;? extends K, ? extends V&gt; map);\n}\n</code></pre>\n","tags":["java"]},{"id":"minimal-safe-bash-script-template","url":"https://yieldray.fun/posts/minimal-safe-bash-script-template","title":"最小化的安全bash脚本模板","date_published":"2022-08-22T18:55:55.000Z","date_modified":"2022-08-22T18:55:55.000Z","content_text":"<blockquote>\n<p>原文：<a href=\"https://betterdev.blog/minimal-safe-bash-script-template\">https://betterdev.blog/minimal-safe-bash-script-template</a><br>仅意译，文章推荐使用此仓库：<a href=\"https://github.com/ralish/bash-script-template\">https://github.com/ralish/bash-script-template</a><br>Bash 脚本参考：<a href=\"https://wangdoc.com/bash/\">https://wangdoc.com/bash/</a></p>\n</blockquote>\n<p>Bash 脚本几乎是每个人都迟早该会一点的。大抵没人会说自己喜欢写 Bash 脚本，可能正因此大部分人都没怎么关注它。</p>\n<p>本文不是为了精通 Bash 脚本，而是展示一个最小化的模板，可以让脚本更安全。</p>\n<h1>为何使用 Bash 脚本？</h1>\n<blockquote>\n<p>与 “就像骑自行车一样” 相反的是： “通过 Bash 编程”。 <em>(比喻：像骑自行车一样，学过就忘不了)</em><br>这句话的意思是，无论你做了多少次，你每次还是要重新学习。</p>\n<p>— Jake Wharton (@JakeWharton) <a href=\"https://twitter.com/JakeWharton/status/1334177665356587008\">December 2, 2020</a></p>\n</blockquote>\n<p>不过 Bash 脚本就像其它广受喜爱的编程语言一样，比如说 JavaScript，不会轻易消失。<br>尽管我们希望 Bash 脚本不会变成所有事物的主要语言，但它总是无处不在。</p>\n<p>Bash 脚本继承了 shell 的宝座，在 Linux 系统和 Docker 镜像中随处可见，这也是大多数后端运行的环境。\n所以如果你需要为服务器程序的启动、CI/CD 步骤或集成测试的运行编写脚本，Bash 总在这里。</p>\n<p>要把几个命令连在一起，把输出从一个传到另一个，或者是运行一些可执行程序，Bash 是最简单和最本源的解决方案。<br>虽然用其他语言编写更强大、更复杂的脚本是很好的，但你不能指望到处都有 Python、Ruby、fish 或其它你觉得好的解释器。<br>而且在你将其它的解释器添加到某些生产服务器、Docker 镜像或 CI 环境之前，你可能要三思而后行。</p>\n<p>Bash 尚不完美，语法繁杂，很难做错误处理。编写的过程中容易踩坑，但我们必须解决它。</p>\n<h1>Bash 脚本模板</h1>\n<p>直接上代码</p>\n<pre><code class=\"language-bash\">#!/usr/bin/env bash\n\nset -Eeuo pipefail\ntrap cleanup SIGINT SIGTERM ERR EXIT\n\nscript_dir=$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&gt;/dev/null &amp;&amp; pwd -P)\n\nusage() {\n  cat &lt;&lt; EOF # remove the space between &lt;&lt; and EOF, this is due to web plugin issue\nUsage: $(basename &quot;${BASH_SOURCE[0]}&quot;) [-h] [-v] [-f] -p param_value arg1 [arg2...]\n\nScript description here.\n\nAvailable options:\n\n-h, --help      Print this help and exit\n-v, --verbose   Print script debug info\n-f, --flag      Some flag description\n-p, --param     Some param description\nEOF\n  exit\n}\n\ncleanup() {\n  trap - SIGINT SIGTERM ERR EXIT\n  # script cleanup here\n}\n\nsetup_colors() {\n  if [[ -t 2 ]] &amp;&amp; [[ -z &quot;${NO_COLOR-}&quot; ]] &amp;&amp; [[ &quot;${TERM-}&quot; != &quot;dumb&quot; ]]; then\n    NOFORMAT=&#39;\\033[0m&#39; RED=&#39;\\033[0;31m&#39; GREEN=&#39;\\033[0;32m&#39; ORANGE=&#39;\\033[0;33m&#39; BLUE=&#39;\\033[0;34m&#39; PURPLE=&#39;\\033[0;35m&#39; CYAN=&#39;\\033[0;36m&#39; YELLOW=&#39;\\033[1;33m&#39;\n  else\n    NOFORMAT=&#39;&#39; RED=&#39;&#39; GREEN=&#39;&#39; ORANGE=&#39;&#39; BLUE=&#39;&#39; PURPLE=&#39;&#39; CYAN=&#39;&#39; YELLOW=&#39;&#39;\n  fi\n}\n\nmsg() {\n  echo &gt;&amp;2 -e &quot;${1-}&quot;\n}\n\ndie() {\n  local msg=$1\n  local code=${2-1} # default exit status 1\n  msg &quot;$msg&quot;\n  exit &quot;$code&quot;\n}\n\nparse_params() {\n  # default values of variables set from params\n  flag=0\n  param=&#39;&#39;\n\n  while :; do\n    case &quot;${1-}&quot; in\n    -h | --help) usage ;;\n    -v | --verbose) set -x ;;\n    --no-color) NO_COLOR=1 ;;\n    -f | --flag) flag=1 ;; # example flag\n    -p | --param) # example named parameter\n      param=&quot;${2-}&quot;\n      shift\n      ;;\n    -?*) die &quot;Unknown option: $1&quot; ;;\n    *) break ;;\n    esac\n    shift\n  done\n\n  args=(&quot;$@&quot;)\n\n  # check required params and arguments\n  [[ -z &quot;${param-}&quot; ]] &amp;&amp; die &quot;Missing required parameter: param&quot;\n  [[ ${#args[@]} -eq 0 ]] &amp;&amp; die &quot;Missing script arguments&quot;\n\n  return 0\n}\n\nparse_params &quot;$@&quot;\nsetup_colors\n\n# script logic here\n\nmsg &quot;${RED}Read parameters:${NOFORMAT}&quot;\nmsg &quot;- flag: ${flag}&quot;\nmsg &quot;- param: ${param}&quot;\nmsg &quot;- arguments: ${args[*]-}&quot;\n</code></pre>\n<p>我的想法是不要让代码太长了，但又能为具体的脚本提供基础。<br>实现起来不容易，因为 Bash 是没有依赖管理的。</p>\n<p>一个想法是用一个基础的脚本去启动一个实现实际逻辑的脚本，不过这样就要两个脚本，不算是最小化。<br>所以我们尽量减小模板代码，保持精简。</p>\n<p>下面具体分析代码。</p>\n<h1>shebang 行，选择 bash</h1>\n<pre><code class=\"language-bash\">#!/usr/bin/env bash\n</code></pre>\n<p>选 <code>/usr/bin/env</code> 而不是直接 <code>/bin/bash</code></p>\n<h1>失败时立刻停止运行</h1>\n<pre><code class=\"language-bash\">set -Eeuo pipefail\n</code></pre>\n<p><code>set</code> 命令改变了脚本的运行模式。<br>比如说，<strong>通常 Bash 就算命令运行失败了（非零返回）</strong>，都会直接继续执行下一行。<br>考虑如下脚本：</p>\n<pre><code class=\"language-bash\">#!/usr/bin/env bash\ncp important_file ./backups/\nrm important_file\n</code></pre>\n<p>上面的脚本中，如果 <code>backups</code> 文件夹不存在，控制台会输出报错信息，但是下一行移除文件夹的代码还是会运行。</p>\n<p>关于<code>set -Eeuo pipefail</code>选项的具体内容，参见<a href=\"https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/\">这篇文章</a></p>\n<p>尽管你应该知道，有一些论点反对设置这些选项。</p>\n<h1>获取运行目录</h1>\n<pre><code class=\"language-bash\">script_dir=$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&gt;/dev/null &amp;&amp; pwd -P)\n</code></pre>\n<p>这行代码<a href=\"https://stackoverflow.com/a/246128/2512304\">尽可能</a>地获取当前运行目录<del>，然后<code>cd</code>进这个目录，为何？</del></p>\n<p>一般来说我们的脚本在相对与脚本文件所在目录下工作（复制文件、执行命令），也就是说脚本所在目录即为当前工作目录。<br>这也是我们在脚本所在目录执行脚本将得到的结果。</p>\n<p>但是如果我们在命令行通过以下方式执行脚本：</p>\n<pre><code class=\"language-bash\">/opt/ci/project/script.sh\n</code></pre>\n<p>则脚本的工作目录不是脚本所在目录，而是完全未知的一个任意工作目录。<br>虽然说可以通过预先进入脚本所在目录，然后再运行脚本来解决此问题：</p>\n<pre><code class=\"language-bash\">cd /opt/ci/project &amp;&amp; ./script.sh\n</code></pre>\n<p>但是如果我们能在脚本内部解决此问题就更好了。<br>所以如果我们的脚本需要在相对于脚本所在路径读取文件或者执行命令，可以通过以下方式：</p>\n<pre><code class=\"language-bash\">cat &quot;$script_dir/my_file&quot;\n</code></pre>\n<p>同时，该脚本不会更改工作目录。如果脚本是在其他目录执行的，并且用户提供了某个文件的相对路径，我们仍然能够读取它。</p>\n<h1>执行清理工作</h1>\n<p><em>（译注：关于 <code>trap</code> 命令，参见：<a href=\"https://tldr.ostera.io/trap\">https://tldr.ostera.io/trap</a>，下面 <code>trap</code> 将使程序接收指定信号时执行 <code>cleanup</code> 函数）</em></p>\n<pre><code class=\"language-bash\">trap cleanup SIGINT SIGTERM ERR EXIT\n\ncleanup() {\n  trap - SIGINT SIGTERM ERR EXIT\n  # script cleanup here\n}\n</code></pre>\n<p>可以把 <code>trap</code> 视作脚本的 <code>finally</code> 块。在脚本结束时——正常情况下，由错误或外部信号引起——将执行 <code>cleanup()</code> 函数。例如，可以在这里删除脚本创建的所有临时文件。</p>\n<p>只需注意 <code>cleanup()</code> 函数不仅可以在最后调用，还可以让脚本完成任何部分的工作。不一定所有你试图清理的资源都会存在。</p>\n<h1>显示帮助</h1>\n<pre><code class=\"language-bash\">usage() {\n  cat &lt;&lt; EOF # remove the space between &lt;&lt; and EOF, this is due to web plugin issue\nUsage: $(basename &quot;${BASH_SOURCE[0]}&quot;) [-h] [-v] [-f] -p param_value arg1 [arg2...]\n\nScript description here.\n\n...\nEOF\n  exit\n}\n</code></pre>\n<p>让 <code>usage()</code> 相对靠近脚本的顶部，有两个作用:</p>\n<ul>\n<li>向那些不了解所有选项和不想翻阅整个脚本的人<strong>展示帮助信息</strong></li>\n<li>给修改此脚本的人作为一个<strong>简洁的文档</strong>（比如说，两个星期后你可能自己忘记写了这个脚本）</li>\n</ul>\n<p>我不主张在这里给每一个函数都写文档。但是一个简短的、好的脚本使用方法（usage message）的文档是最起码的要求。</p>\n<h1>输出彩色文本</h1>\n<pre><code class=\"language-bash\">setup_colors() {\n  if [[ -t 2 ]] &amp;&amp; [[ -z &quot;${NO_COLOR-}&quot; ]] &amp;&amp; [[ &quot;${TERM-}&quot; != &quot;dumb&quot; ]]; then\n    NOFORMAT=&#39;\\033[0m&#39; RED=&#39;\\033[0;31m&#39; GREEN=&#39;\\033[0;32m&#39; ORANGE=&#39;\\033[0;33m&#39; BLUE=&#39;\\033[0;34m&#39; PURPLE=&#39;\\033[0;35m&#39; CYAN=&#39;\\033[0;36m&#39; YELLOW=&#39;\\033[1;33m&#39;\n  else\n    NOFORMAT=&#39;&#39; RED=&#39;&#39; GREEN=&#39;&#39; ORANGE=&#39;&#39; BLUE=&#39;&#39; PURPLE=&#39;&#39; CYAN=&#39;&#39; YELLOW=&#39;&#39;\n  fi\n}\n\nmsg() {\n  echo &gt;&amp;2 -e &quot;${1-}&quot;\n}\n</code></pre>\n<p>首先，如果你不需要输出彩色文本，删除 <code>setup_colors()</code> 函数即可。<br>我保留它是因为我知道如果我不需要每次都去查颜色的代码，我将更频繁地使用彩色文本。</p>\n<p>其次，指定的颜色仅在 <code>msg()</code> 函数内有效，其它的 <code>echo</code> 命令是无效的。</p>\n<p><code>msg()</code> 意在用于打印所有非脚本输出。这包括所有日志（logs）和消息（messages），而不仅仅是错误（errors）。</p>\n<p>引用自 <a href=\"https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46\">12 Factor CLI Apps article</a>：</p>\n<blockquote>\n<p><strong>简而言之：stdout 用来输出（output），stderr 用来打印消息（messaging）</strong></p>\n<p>Jeff Dickey, who knows a little about building CLI apps</p>\n</blockquote>\n<p>所以说在大多数情况下你不应该在 <code>stdout</code> 中使用彩色文本。</p>\n<p>通过 <code>msg()</code> 函数打印的消息将发送至 <code>stderr</code>，并支持特殊序列，如颜色。如果 <code>stderr</code> 输出不是一个交互式终端或者传递了一个标准参数，则颜色将被禁用。</p>\n<p>用法：</p>\n<pre><code class=\"language-bash\">msg &quot;This is a ${RED}very important${NOFORMAT} message, but not a script output value!&quot;\n</code></pre>\n<p>要检查它在当 <code>stderr</code> 不是交互式终端时的行为，可以像上面这样在脚本中添加一行（使用 msg 函数的语句），<br>然后执行它，将 <code>stderr</code> 重定向到 <code>stdout</code>，并通过管道将其发送到 <code>cat</code>。<br>管道操作使得输出不再直接发送到终端，而是发送到下一个命令，因此现在颜色应该会被禁用。</p>\n<pre><code>$ ./test.sh 2&gt;&amp;1 | cat\nThis is a very important message, but not a script output value!\n</code></pre>\n<h1>解析参数</h1>\n<pre><code class=\"language-bash\">parse_params() {\n  # default values of variables set from params\n  flag=0\n  param=&#39;&#39;\n\n  while :; do\n    case &quot;${1-}&quot; in\n    -h | --help) usage ;;\n    -v | --verbose) set -x ;;\n    --no-color) NO_COLOR=1 ;;\n    -f | --flag) flag=1 ;; # example flag\n    -p | --param) # example named parameter\n      param=&quot;${2-}&quot;\n      shift\n      ;;\n    -?*) die &quot;Unknown option: $1&quot; ;;\n    *) break ;;\n    esac\n    shift\n  done\n\n  args=(&quot;$@&quot;)\n\n  # check required params and arguments\n  [[ -z &quot;${param-}&quot; ]] &amp;&amp; die &quot;Missing required parameter: param&quot;\n  [[ ${#args[@]} -eq 0 ]] &amp;&amp; die &quot;Missing script arguments&quot;\n\n  return 0\n}\n</code></pre>\n<p>如果脚本需要传入参数的话，我会一般这样做，就算脚本仅在这一处使用。复制和重用它很容易，这也是迟早要做的。<br>此外，即使有些东西需要硬编码，通常在比 Bash 脚本更高的层次上也有更好的地方。</p>\n<p>命令行参数主要有三种类型：标志（flags）、命名参数（named parameters）和位置参数（positional arguments）。<code>parse_params()</code> 函数支持提到的所有参数。</p>\n<p>这里唯一没有处理的一种常见参数模式是串联多个单字母标志。为了能够传递两个标志作为-ab，而不是-a -b，需要一些额外的代码。</p>\n<p>while 循环是一种手动解析参数的方式。在其他语言中，您应该使用内置的解析器或其它可用库，但是，这是 Bash。</p>\n<p>模板中有一个示例标志 (-f) 和命名参数 (-p)。只需更改或复制它们来添加其他参数。之后不要忘记更新 <code>usage()</code> 函数。</p>\n<p>这里重要的一点是，当你用谷歌搜索出来的第一个结果来解析 Bash 参数，通常会忽略的一点是，在未知选项上抛出一个错误。<br>脚本接收了一个未知选项就意味着用户想做一些脚本无法实现的事情。所以用户期望和脚本行为可能会大不相同。<br>最好在坏事发生之前就完全阻止程序的执行。</p>\n<p>在 Bash 中，解析参数有两种方法，即 <code>getopt</code> 和 <code>getopts</code>。赞成和反对使用它们的意见都有。<br>我认为这些工具并不是最好的，因为默认情况下，macOS 上的 <code>getopt</code> 行为完全不同，而且 <code>getopts</code> 不支持长参数(例如 --help)。</p>\n<h1>使用模板</h1>\n<p>复制粘贴即可，就像抄网上其它代码一样。</p>\n<p>嗯，实际上，这是个诚恳的建议。毕竟 Bash 没有像 npm 一样的包管理器可以执行 install。</p>\n<p>复制之后只需改变四处：</p>\n<ul>\n<li><code>usage()</code> 处文本改为脚本说明</li>\n<li><code>cleanup()</code> 中的内容</li>\n<li><code>parse_params()</code> 中的参数。保留 <code>--help</code> 和 <code>--no-color</code>，替换掉实例参数：<code>-f</code> 和 <code>-p</code></li>\n<li>脚本的实际逻辑</li>\n</ul>\n<h1>可移植性</h1>\n<p>我在 MacOS（默认 archaic Bash 3.2）和几个 docker 镜像：Debian, Ubuntu, CentOS, Amazon Linux, Fedora 下测试此模板。能够正常工作。</p>\n<p>当然，它不能在缺少 Bash 的环境中工作，比如 Alpine Linux。Alpine 作为一个极简主义系统，使用了轻量级的 shell <code>ash</code> (Almquist shell)。</p>\n<p>你可能会问，使用几乎在任何地方都适用的 Bourne Shell 兼容脚本不是更好吗？至少对我来说，答案是否定的。Bash 更安全、更强大(但仍然不容易使用)，所以我可以接受对一些我很少处理的 Linux 发行版缺乏支持。</p>\n<h1>扩展阅读</h1>\n<p>在 Bash 或其它更好的编程语言中创建 CLI 脚本时，有一些通用规则。这些资源将指导你如何让你的小型脚本和大型 CLI 应用程序变得可靠:</p>\n<ul>\n<li><a href=\"https://clig.dev/\">Command Line Interface Guidelines</a></li>\n<li><a href=\"https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46\">12 Factor CLI Apps</a></li>\n<li><a href=\"https://betterdev.blog/command-line-arguments-anatomy-explained/\">Command line arguments anatomy explained with examples</a></li>\n</ul>\n<h1>结束语</h1>\n<p>我不是第一个也不是最后一个创建 Bash 脚本模板的人。一个很好的选择是<a href=\"https://github.com/ralish/bash-script-template\">这个项目</a>，虽然对我的日常需求来说有点太大了。毕竟，我试图让 Bash 脚本尽可能的小(和少)。</p>\n<p>编写 Bash 脚本时，使用支持 <a href=\"https://github.com/koalaman/shellcheck\">ShellCheck</a> linter 的 IDE，比如 JetBrains IDEs。它会阻止你做一堆<a href=\"https://github.com/koalaman/shellcheck/blob/master/README.md#user-content-gallery-of-bad-code\">事与愿违的事情</a>。</p>\n<p>我的 Bash 脚本模板也可以通过 GitHub Gist 获得(<a href=\"https://gist.github.com/m-radzikowski/d925ac457478db14c2146deadd0020cd\">在 MIT 许可下</a>):</p>\n<p><a href=\"https://gist.github.com/m-radzikowski/53e0b39e9a59a1518990e76c2bff8038\">script-template.sh</a></p>\n<p>如果你发现模板有任何问题，或者你认为缺少了什么重要的东西，请在评论中告诉我。</p>\n<blockquote>\n<p>评论见原文评论区：<a href=\"https://betterdev.blog/minimal-safe-bash-script-template\">https://betterdev.blog/minimal-safe-bash-script-template</a></p>\n</blockquote>\n","tags":["linux"]},{"id":"js-import-map","url":"https://yieldray.fun/posts/js-import-map","title":"浏览器 Import Maps","date_published":"2022-08-21T10:00:00.000Z","date_modified":"2022-08-21T10:00:00.000Z","content_text":"<p>Import Maps 可以控制 <code>import</code> 语句 和 <code>import()</code> 表达式的行为。<br>参见：<a href=\"https://caniuse.com/import-maps\">https://caniuse.com/import-maps</a><br>提案：<a href=\"https://github.com/WICG/import-maps\">https://github.com/WICG/import-maps</a><br>规范：<a href=\"https://html.spec.whatwg.org/multipage/webappapis.html#import-maps\">https://html.spec.whatwg.org/multipage/webappapis.html#import-maps</a></p>\n<p>非浏览器环境也有相关实现：<br>对于 Node.js：<a href=\"https://nodejs.org/dist/latest-v16.x/docs/api/policy.html\">https://nodejs.org/dist/latest-v16.x/docs/api/policy.html</a><br>对于 Deno：<a href=\"https://deno.land/manual/linking_to_external_code/import_maps\">https://deno.land/manual/linking_to_external_code/import_maps</a></p>\n<h1>指定 Maps</h1>\n<pre><code class=\"language-html\">&lt;script type=&quot;importmap&quot;&gt;\n    {\n        &quot;imports&quot;: {\n            &quot;moment&quot;: &quot;/node_modules/moment/src/moment.js&quot;,\n            &quot;lodash&quot;: &quot;/node_modules/lodash-es/lodash.js&quot;\n        }\n    }\n&lt;/script&gt;\n</code></pre>\n<p>根据 Import Maps 导入：</p>\n<pre><code class=\"language-js\">import moment from &quot;moment&quot;;\nimport { partition } from &quot;lodash&quot;;\n</code></pre>\n<p>注意，相对路径会受到 <code>&lt;base&gt;</code> 标签的影响</p>\n<pre><code class=\"language-html\">&lt;base href=&quot;https://www.unpkg.com/vue@2/dist/&quot; /&gt;\n&lt;script type=&quot;importmap&quot;&gt;\n    {\n        &quot;imports&quot;: {\n            &quot;vue&quot;: &quot;./vue.runtime.esm.js&quot;\n        }\n    }\n&lt;/script&gt;\n\n&lt;script type=&quot;module&quot;&gt;\n    import(&quot;vue&quot;); // resolves to https://www.unpkg.com/vue@2/dist/vue.runtime.esm.js\n&lt;/script&gt;\n</code></pre>\n<h1>导入子模块</h1>\n<pre><code class=\"language-json\">{\n    &quot;imports&quot;: {\n        &quot;moment&quot;: &quot;/node_modules/moment/src/moment.js&quot;,\n        &quot;moment/&quot;: &quot;/node_modules/moment/src/&quot;,\n        &quot;lodash&quot;: &quot;/node_modules/lodash-es/lodash.js&quot;,\n        &quot;lodash/&quot;: &quot;/node_modules/lodash-es/&quot;\n    }\n}\n</code></pre>\n<pre><code class=\"language-js\">// 导入主模块\nimport moment from &quot;moment&quot;;\nimport _ from &quot;lodash&quot;;\n// 导入子模块\nimport localeData from &quot;moment/locale/zh-cn.js&quot;;\nimport fp from &quot;lodash/fp.js&quot;;\n</code></pre>\n<h1>重映射</h1>\n<pre><code class=\"language-json\">{\n    &quot;imports&quot;: {\n        &quot;https://www.unpkg.com/vue@2/dist/vue.runtime.esm.js&quot;: &quot;/node_modules/vue/dist/vue.runtime.esm.js&quot;\n    }\n}\n</code></pre>\n<p>这就会把</p>\n<pre><code class=\"language-js\">import Vue from &quot;https://www.unpkg.com/vue@2/dist/vue.runtime.esm.js&quot;;\n</code></pre>\n<p>改为本地获取</p>\n<pre><code class=\"language-js\">import Vue from &quot;/node_modules/vue/dist/vue.runtime.esm.js&quot;;\n</code></pre>\n<p>可以将任意导入进行重映射</p>\n<pre><code class=\"language-json\">{\n    &quot;imports&quot;: {\n        &quot;/app/helpers.mjs&quot;: &quot;/app/helpers/index.mjs&quot;\n    }\n}\n</code></pre>\n<p>使用尾斜杠时，进行前缀匹配</p>\n<pre><code class=\"language-json\">{\n    &quot;imports&quot;: {\n        &quot;https://www.unpkg.com/vue/&quot;: &quot;/node_modules/vue/&quot;\n    }\n}\n</code></pre>\n<h1>scopes 范围更改</h1>\n<p>用 scopes 可以控制指定路径下模块中的 import 导入的行为<br>使之从 Import Maps 的 scopes 指定的特定源进行导入，主要使用场景是版本控制。</p>\n<pre><code class=\"language-js\">{\n  &quot;imports&quot;: {\n    &quot;a&quot;: &quot;/a-1.mjs&quot;,\n    &quot;b&quot;: &quot;/b-1.mjs&quot;,\n    &quot;c&quot;: &quot;/c-1.mjs&quot;\n  },\n  &quot;scopes&quot;: {\n    &quot;/scope2/&quot;: {\n      &quot;a&quot;: &quot;/a-2.mjs&quot;\n    },\n    &quot;/scope2/scope3/&quot;: {\n      &quot;b&quot;: &quot;/b-3.mjs&quot;\n    }\n  }\n}\n</code></pre>\n<p>指定上面的 scope，则：</p>\n<p>在模块 <code>/scope1/foo.mjs</code> 中（未在 scope 中指定），<code>import(&quot;a&quot;)</code> =&gt; <code>import(&quot;/a-1.mjs&quot;)</code><br>在模块 <code>/scope2/foo.mjs</code> 中（通过 scope 中指定），<code>import(&quot;a&quot;)</code> =&gt; <code>import(&quot;/a-2.mjs&quot;)</code></p>\n<p>这个例子摘自提案，解析如下：</p>\n<table>\n<thead>\n<tr>\n<th>Specifier</th>\n<th>Referrer</th>\n<th>Resulting URL</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>a</td>\n<td>/scope1/foo.mjs</td>\n<td>/a-1.mjs</td>\n</tr>\n<tr>\n<td>b</td>\n<td>/scope1/foo.mjs</td>\n<td>/b-1.mjs</td>\n</tr>\n<tr>\n<td>c</td>\n<td>/scope1/foo.mjs</td>\n<td>/c-1.mjs</td>\n</tr>\n<tr>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td>a</td>\n<td>/scope2/foo.mjs</td>\n<td>/a-2.mjs</td>\n</tr>\n<tr>\n<td>b</td>\n<td>/scope2/foo.mjs</td>\n<td>/b-1.mjs</td>\n</tr>\n<tr>\n<td>c</td>\n<td>/scope2/foo.mjs</td>\n<td>/c-1.mjs</td>\n</tr>\n<tr>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td>a</td>\n<td>/scope2/scope3/foo.mjs</td>\n<td>/a-2.mjs</td>\n</tr>\n<tr>\n<td>b</td>\n<td>/scope2/scope3/foo.mjs</td>\n<td>/b-3.mjs</td>\n</tr>\n<tr>\n<td>c</td>\n<td>/scope2/scope3/foo.mjs</td>\n<td>/c-1.mjs</td>\n</tr>\n</tbody></table>\n<h1>导入 importmap 文件，替代硬编码</h1>\n<pre><code class=\"language-html\">&lt;script type=&quot;importmap&quot; src=&quot;import-map.importmap&quot;&gt;&lt;/script&gt;\n</code></pre>\n<p>header 头应指定 MIME 类型为<code>application/importmap+json application/json</code></p>\n<h1>动态生成 script 标签</h1>\n<p>可以动态生成 script 标签来控制 Import Maps，但必须保证该标签在模块解析前加载。</p>\n","tags":["web-api","js"]},{"id":"cpp-rvalue-reference","url":"https://yieldray.fun/posts/cpp-rvalue-reference","title":"c++右值引用","date_published":"2022-08-20T13:00:00.000Z","date_modified":"2022-08-20T13:00:00.000Z","content_text":"<h1>左值，右值</h1>\n<p><a href=\"https://zh.cppreference.com/w/cpp/language/value_category\">https://zh.cppreference.com/w/cpp/language/value_category</a></p>\n<p>简单来说，变量名就是左值，变量的具体值就是右值。</p>\n<pre><code class=\"language-cpp\">int a = 1;\n// 左值 右值\n</code></pre>\n<p>那么很明显，在 c++ 中可以获取一个左值的地址，而无法获取一个右值的地址。</p>\n<p>纯右值：除字符串字面量以外的字面量（或由表达式产生）、临时（匿名）对象等。<br>亡值：即将被销毁但能够被移动的值（通过移动语义）。</p>\n<h1>左值引用</h1>\n<p>传统 c++ 引用就称左值引用，右值引用添加自 c++11</p>\n<p>在左值引用中，一个<em>引用变量名</em>引用另一个<em>变量名</em>。</p>\n<pre><code class=\"language-cpp\">int a = 1;\nint&amp; ref = a;\nref = 2;\ncout &lt;&lt; a; // =&gt; 2\n</code></pre>\n<p>非常量的引用类型的变量无法引用一个右值，<br>不过常量引用类型则可以。</p>\n<pre><code class=\"language-cpp\">int &amp;ref = 2; // ❎\n// error: cannot bind non-const lvalue reference of type &#39;int&amp;&#39; to an rvalue of type &#39;int&#39;\n\nconst int &amp;ref = 2; // ✅\n</code></pre>\n<p>一个引用的引用相当于对原来那个变量的引用，类型仍然是原来那个变量的类型的引用。<br>（而不是引用的引用类型，这就引出了右值引用）</p>\n<pre><code class=\"language-cpp\">int a = 1;\nint &amp;ref = a;\nint &amp;&amp;ref_ref = ref; // ❎，相当于 int &amp;&amp;ref_ref = a\n// error: cannot bind rvalue reference of type &#39;int&amp;&amp;&#39; to lvalue of type &#39;int&#39;\nint &amp;ref_ref = ref;  // ✅\n\nref_ref = 2;\ncout &lt;&lt; a; // =&gt; 2\n</code></pre>\n<h1>右值引用</h1>\n<p>右值引用可引用一个右值，而不能引用左值。</p>\n<pre><code class=\"language-cpp\">int a = 1;\nint &amp;&amp; rref = a; // ❎\n// error: cannot bind rvalue reference of type &#39;int&amp;&amp;&#39; to lvalue of type &#39;int&#39;\nint &amp;&amp; rref = 1; // ✅\n</code></pre>\n<p>可以令右值引用重新引用另一个右值</p>\n<pre><code class=\"language-cpp\">int &amp;&amp; rref = 1;\nrref = 2;\ncout &lt;&lt; rref; // =&gt; 2\n</code></pre>\n<h1><code>std::move()</code> 将左值强制转换为右值</h1>\n<p><code>std::move</code> 相当于 <code>static_cast&lt;T&amp;&amp;&gt;(lvalue)</code>，返回一个右值</p>\n<pre><code class=\"language-cpp\">int a = 1;\nint &amp;&amp; rref = a; // ❎\nint &amp;&amp; rref = std::move(a); // ✅\n\n// move函数不发生所有权变化\ncout &lt;&lt; a; // =&gt; 1\na = 2;\ncout &lt;&lt; rref; // =&gt; 2\n</code></pre>\n<p>从形式上看，<code>std::move(xxx)</code> 作为一个表达式，是一个右值</p>\n<pre><code class=\"language-cpp\">int &amp;&amp; rref = 1;\n// 相当于\nint temp = 1;\nint &amp;&amp; rref = std::move(temp);\ncout &lt;&lt; rref; // =&gt; 1\n// 注意此函数参数仅对左值有效\nint &amp;&amp; rref = std::move(1);\ncout &lt;&lt; rref; // =&gt; 0\n</code></pre>\n<p>右值引用的这个变量名本身也是个左值。<br>如果有一个形参为右值引用的函数，该函数仅接受右值，那么就需要使用<code>std::move</code>函数</p>\n<pre><code class=\"language-cpp\">void use_rvalue(int &amp;&amp; rvalue) {}\n\nint a = 1;\nint &amp; lref = a;\nint &amp;&amp; rref = 1;\nuse_rvalue(1); // ✅\nuse_rvalue(a); // ❎\nuse_rvalue(lref); // ❎\nuse_rvalue(rref); // ❎\nuse_rvalue(std::move(a)); // ✅\n</code></pre>\n<p>不过，const 形参既能接受左值也能接收右值，但无法修改</p>\n<pre><code class=\"language-cpp\">void use_const(const int&amp; n) {}\n// 以下调用皆可通过编译\nuse_const(1);\nint a = 1;\nint &amp; lref = a;\nint &amp;&amp; rref = 1;\nuse_const(a);\nuse_const(lref);\nuse_const(rref);\nuse_const(std::move(a));\n</code></pre>\n<h1>移动语义</h1>\n<p>对于临时值来说，使用移动语义替代拷贝可提高性能。</p>\n<p>给出一段修改自 cppreference 的演示代码如下：</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;utility&gt;\n#include &lt;vector&gt;\n#include &lt;string&gt;\nusing namespace std;\n\nstring wrap_string(string str){ return &quot;\\&quot;&quot; + str + &quot;\\&quot;&quot;; }\n\nint main()\n{\n    string str = &quot;Hello&quot;;\n    vector&lt;string&gt; v;\n\n    // 使用 push_back(const T&amp;) 重载，\n    // 表示我们将带来复制 str 的成本\n    v.push_back(str);\n    cout &lt;&lt; &quot;After copy, str is &quot; &lt;&lt; wrap_string(str) &lt;&lt; endl;\n\n    // 使用右值引用 push_back(T&amp;&amp;) 重载，\n    // 表示不复制字符串；而是\n    // str 的内容被移动进 vector\n    // 这个开销比较低，但也意味着 str 现在可能为空。\n    v.push_back(std::move(str));\n    std::cout &lt;&lt; &quot;After move, str is &quot; &lt;&lt; wrap_string(str) &lt;&lt; endl;\n\n    std::cout &lt;&lt; &quot;The contents of the vector are &quot;\n     &lt;&lt; wrap_string(v[0]) &lt;&lt; &quot;, &quot; &lt;&lt; wrap_string(v[1]) &lt;&lt; endl;\n}\n</code></pre>\n<p>输出：</p>\n<pre><code>After copy, str is &quot;Hello&quot;\nAfter move, str is &quot;&quot;\nThe contents of the vector are &quot;Hello&quot;, &quot;Hello&quot;\n</code></pre>\n<p>移动语义的<em>移动</em>，指将某对象持有的资源或内容转移给另一个对象。<br>在转移资源后，被移动的对象处于<em>有效但未定义的状态</em></p>\n<p>移动一个 vector，不同于拷贝的是，这是通过<em>移动构造函数</em>而不是<em>拷贝构造函数</em>实现的。</p>\n<pre><code class=\"language-cpp\">vector&lt;int&gt; fibonacci = { 0, 1, 1, 2, 3, 5, 8};\ncout &lt;&lt; fibonacci.size() &lt;&lt; endl; // =&gt; 7\nvector&lt;int&gt; fib = move(fibonacci);\ncout &lt;&lt; fibonacci.size() &lt;&lt; endl; // =&gt; 0\n</code></pre>\n<p>代码摘自<a href=\"https://zhuanlan.zhihu.com/p/347977300\">https://zhuanlan.zhihu.com/p/347977300</a></p>\n<pre><code class=\"language-cpp\">class my_vector\n{\n    int* data_;\n    size_t size_;\n    size_t capacity_;\n\npublic:\n    // 复制构造函数\n    my_vector(const my_vector&amp; oth) :\n        size_(oth.size_),\n        capacity_(oth.capacity_)\n    {\n        data_ = static_cast&lt;int*&gt;(malloc(sizeof(int) * size_));\n        for (size_t i = 0; i &lt; size_; ++i) {\n            data_[i] = oth.data_[i];\n        }\n    }\n\n    // 移动构造函数\n    // std::exchange(obj, new_val)的作用是把返回obj的旧值，并把new_val赋值给obj\n    my_vector(my_vector&amp;&amp; oth) :\n        data_(std::exchange(oth.data_, nullptr)),\n        size_(std::exchange(oth.size_, 0)),\n        capacity_(std::exchange(oth.capacity_, 0))\n    {}\n};\n</code></pre>\n<h1><code>std::forward&lt;&gt;()</code> 完美转发</h1>\n<p>完美转发，指不仅转发参数的值，还保证被转发参数的左、右值属性不变。</p>\n<pre><code class=\"language-cpp\">#include &lt;utility&gt;\nvoid use_rvalue(int&amp;&amp; rv) {}\nvoid use_lvalue(int&amp; lv) {}\n\n\nvoid fn(int&amp;&amp; rvalue) {\n    use_rvalue(rvalue); // 错误，rvalue 本身是个左值\n    use_rvalue(std::move(rvalue)); // 将左值转换为右值\n    use_rvalue(std::forward&lt;int &amp;&amp;&gt;(rvalue));  // 相当于上面\n\n    use_lvalue(rvalue);  // 正确，需要传入左值\n    use_lvalue(std::forward&lt;int &amp;&gt;(rvalue)); // 相当于上面\n\n}\n\nint main() {\n    int a = 1;\n    fn(std::move(a));\n}\n</code></pre>\n<h1>参见</h1>\n<p><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/rvalue-reference-declarator-amp-amp\">https://docs.microsoft.com/zh-cn/cpp/cpp/rvalue-reference-declarator-amp-amp</a><br><a href=\"http://zh.cppreference.com/w/cpp/language/reference\">http://zh.cppreference.com/w/cpp/language/reference</a><br><a href=\"https://zh.cppreference.com/w/cpp/utility/move\">https://zh.cppreference.com/w/cpp/utility/move</a><br><a href=\"https://zh.cppreference.com/w/cpp/utility/exchange\">https://zh.cppreference.com/w/cpp/utility/exchange</a></p>\n","tags":["cpp"]},{"id":"cpp-function-object","url":"https://yieldray.fun/posts/cpp-function-object","title":"c++函数对象","date_published":"2022-08-17T12:00:00.000Z","date_modified":"2022-08-17T12:00:00.000Z","content_text":"<p>函数对象：函数指针、函数符、lambda 表达式<br>后两者实际上通过重载 <code>()</code> 运算符的对象实现函数功能。</p>\n<p>函数指针参见<a href=\"/posts/cpp-function-pointer/\">另一篇</a></p>\n<h1>函数符</h1>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;vector&gt;\n#include &lt;algorithm&gt;\n#include &lt;cstdlib&gt;\nusing namespace std;\n\ntemplate &lt;typename T&gt;\nvoid print_vec(vector&lt;T&gt; &amp;vec)\n{\n    for (auto i : vec)\n        cout &lt;&lt; i &lt;&lt; &quot;, &quot;;\n    cout &lt;&lt; endl;\n}\n\nint always_one()\n{\n    return 1;\n}\n\nclass Always_two\n{\npublic:\n    int operator()()\n    {\n        return 2;\n    }\n};\n\nAlways_two always_two;\n\nint main()\n{\n    vector&lt;int&gt; numbers(5);\n    generate(numbers.begin(), numbers.end(), rand);\n    // rand 的签名是 int rand()\n    // generate 的签名在此处可以看作\n    // template&lt; class ForwardIt, class Generator &gt;\n    // void generate( ForwardIt first, ForwardIt last, Generator g );\n\n    print_vec(numbers); // 41, 18467, 6334, 26500, 19169,\n\n    generate(numbers.begin(), numbers.end(), always_one);\n    print_vec(numbers); // 1, 1, 1, 1, 1,\n\n    generate(numbers.begin(), numbers.end(), always_two);\n    print_vec(numbers); // 2, 2, 2, 2, 2,\n}\n</code></pre>\n<h1>lambda</h1>\n<p>模仿上例，给出如下代码</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;vector&gt;\n#include &lt;algorithm&gt;\n#include &lt;cstdlib&gt;\nusing namespace std;\n\nint always_one()\n{\n    return 1;\n}\n\nclass Always_two\n{\npublic:\n    int operator()()\n    {\n        return 2;\n    }\n};\n\nAlways_two always_two;\n\ntemplate &lt;typename T&gt;\nclass Store\n{\nprivate:\n    T value;\n\npublic:\n    template &lt;typename Fn&gt;\n    void set_fn(Fn func) noexcept\n    {\n        value = func();\n    }\n    void set(T val) noexcept\n    {\n        value = val;\n    }\n    T get() const noexcept\n    {\n        return value;\n    }\n    void print() const noexcept\n    {\n        cout &lt;&lt; value &lt;&lt; endl;\n    }\n};\n\nint main()\n{\n    Store&lt;int&gt; store;\n\n    store.set_fn(rand);\n    store.print(); // 41\n\n    store.set_fn(always_one);\n    store.print(); // 1\n\n    store.set_fn(always_two);\n    store.print(); // 2\n\n    store.set_fn([](){ return 3; });\n    store.print(); // 3\n}\n</code></pre>\n<p>Microsoft C++ 文档中的描述非常直观<br><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/lambda-expressions-in-cpp\">https://docs.microsoft.com/zh-cn/cpp/cpp/lambda-expressions-in-cpp</a><br><img src=\"https://docs.microsoft.com/zh-cn/cpp/cpp/media/lambdaexpsyntax.png\" alt=\"lambda 表达式的各个部分\"></p>\n<ol>\n<li>capture 子句 (C++ 规范中也称为 lambda 引入器)</li>\n</ol>\n<p><code>[]</code> 表示不捕获 <code>[=]</code> 表示默认按值捕获(不能修改) <code>[&amp;]</code> 表示默认按引用捕获<br><code>[x]</code> 表示按值捕获变量 <code>x</code> (不能修改)<br><code>[&amp;x]</code> 表示按引用捕获变量 <code>x</code><br><code>[x, &amp;y]</code> 表示变量 <code>x</code> 按值捕获，变量 <code>y</code> 按引用捕获<br><code>[=, &amp;x]</code> 表示除变量 <code>x</code> 按引用捕获外，其余变量按值捕获<br><code>[&amp;, x]</code> 表示除变量 <code>x</code> 按值捕获外，其余变量按引用捕获<br>此外，不允许这样：<code>[&amp;, &amp;x]</code> <code>[=, x]</code><br>捕获 this 同理 <code>[this]</code> <code>[&amp;, this]</code> <code>[=, *this]</code><br>捕获一个可变模板 <code>[args...]</code><br>声明临时变量（v1, v2 仅在函数体有效） <code>[v1 = 1, v2 = std::move(important)]</code></p>\n<ol start=\"2\">\n<li>参数列表，可选 (也称为 lambda 声明符)</li>\n</ol>\n<p>lambda 函数的参数列表与普通函数基本相同，不过不支持默认参数和可变参数<br>使用 auto 关键字替代模板语法，来实现泛型</p>\n<pre><code class=\"language-cpp\">auto plus = [] (auto first, auto second) { return first + second; };\n</code></pre>\n<ol start=\"3\">\n<li>可变规范，可选</li>\n</ol>\n<p>mutable 表示允许修改按值捕获的变量</p>\n<pre><code class=\"language-cpp\">int a = 123;\nauto f = [a](int x) mutable { a=x;cout&lt;&lt;a&lt;&lt;endl; };\n// 不加 mutable 则报编译错误\nf(456); // 456\ncout &lt;&lt; a; // 123\n</code></pre>\n<ol start=\"4\">\n<li>异常规范，可选</li>\n</ol>\n<p>异常规范同普通函数</p>\n<pre><code class=\"language-cpp\">auto err = []() noexcept { throw &quot;ERROR&quot;; };\n// 编译器警告\n</code></pre>\n<ol start=\"5\">\n<li>返回类型，可选</li>\n</ol>\n<p>返回类型可以自动推导时无需声明返回类型。</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;functional&gt;\nusing namespace std;\nint main()\n{\n    auto add_two_integers = [](int x) -&gt; function&lt;int(int)&gt;\n    {// 这里也是可以自动推导返回类型的\n        return [=](int y) { return x + y; };\n    };\n    cout &lt;&lt; add_two_integers(1)(2); // 3\n}\n</code></pre>\n<ol start=\"6\">\n<li>lambda 正文</li>\n</ol>\n<p>lambda 函数根据 capture 子句获取封闭范围捕获的变量</p>\n<h1>functional 头文件</h1>\n<h2><code>std::function&lt;&gt;</code></h2>\n<p>类模板 std::function 是通用多态函数封装器。<br>std::function 的实例能存储、复制及调用任何可调用函数对象（函数指针、函数符、lambda 表达式）的类型。<br>尖括号中传入函数对象表达的函数的类型：</p>\n<pre><code class=\"language-cpp\">function&lt;int(int,int)&gt; add = [](auto a, auto b){return a+b;};\n</code></pre>\n<h2><code>std::invoke()</code></h2>\n<h2><code>std::ref()</code></h2>\n<h2><code>std::bind()</code> <code>std::placeholders</code> （弃用）</h2>\n","tags":["cpp"]},{"id":"cpp-smart-pointers","url":"https://yieldray.fun/posts/cpp-smart-pointers","title":"c++智能指针","date_published":"2022-08-16T10:10:10.000Z","date_modified":"2022-08-16T10:10:10.000Z","content_text":"<h1>智能指针</h1>\n<p>智能指针利用对象来管理指针，该对象是存储于栈中的普通对象，因此在离开作用域时会自动调用折构函数而释放指针。<br>要管理指针，引来了所有权问题（指针所有者唯一）。</p>\n<p>所有智能指针类使用 explicit 构造函数。</p>\n<h1><a href=\"https://docs.microsoft.com/zh-cn/cpp/standard-library/auto-ptr-class\">auto_ptr</a> （已弃用）</h1>\n<p>auto_ptr 在 C++98 引入，于 C++11 废弃。定义于头文件 memory</p>\n<pre><code class=\"language-cpp\">#include &lt;memory&gt;\nusing std::string;\nusing std::auto_ptr;\nvoid remodel(string &amp; str){\n    auto_ptr&lt;string&gt; ps (new string(str));\n    // or:  auto_ptr&lt;string&gt; ps = new string(str);\n    // ... do something\n    if(weird_thing()){\n        throw exception();\n    }\n    str = *ps;\n    return; // 自动销毁智能指针 ps\n}\n</code></pre>\n<p>赋值操作转让所有权</p>\n<pre><code class=\"language-cpp\">auto_ptr&lt;string&gt; p1 = new string(&quot;hello&quot;);\nauto_ptr&lt;string&gt; p2;\np2 = p1; // p1具有的指针所有权已移动至p2\n\ncout &lt;&lt; *p1; // 错误，p1 是空指针（悬垂指针）\ncout &lt;&lt; (void*)&amp;*p1 &lt;&lt; endl; // =&gt; 0\ncout &lt;&lt; *p2; // =&gt; hello\n</code></pre>\n<h1><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/how-to-create-and-use-unique-ptr-instances\">unique_ptr</a> make_unique()</h1>\n<p>自 C++11 起，使用 unique_ptr 替代 auto_ptr。<br>unique_ptr 关闭了 auto_ptr 的赋值语义，避免留下悬垂指针。</p>\n<pre><code class=\"language-cpp\">unique_ptr&lt;string&gt; p1 (new string(&quot;hello&quot;));\nunique_ptr&lt;string&gt; p2;\np2 = p1; // 编译器直接在这一行报错\n\np2 = move(p1); // 允许显式使用move函数\n</code></pre>\n<p>程序试图将一个 unique_ptr 赋给另一个时，如果源 unique_ptr 是个临时右值，编译器允许这样做；<br>如果源 unique_ptr 将存在一段时间，编译器将禁止这样做。<br>此外， unique_ptr 还可以用于数组，折构函数将使用 <code>delete[]</code> 释放内存（auto_ptr 和 shared_ptr 则不行）</p>\n<p>关于 <code>make_unique()</code> ，参考文档</p>\n<h1><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/how-to-create-and-use-shared-ptr-instance\">shared_ptr</a> make_shared()</h1>\n<p>shared_ptr 就是引用计数指针，只有当引用计数为零时才释放指针。</p>\n<pre><code class=\"language-cpp\">shared_ptr&lt;int&gt; p1(new int(2));\nshared_ptr&lt;int&gt; p2(p1);\nshared_ptr&lt;int&gt; p3;\np3 = p2;\n\ncout &lt;&lt; *p1 &lt;&lt; endl; // =&gt; 2\ncout &lt;&lt; *p1.get() &lt;&lt; endl; // =&gt; 2\n\np1.reset(new int(3)); // p1不再指向new int(2)\np2 = shared_ptr&lt;int&gt;(new int(4)); // p2不再指向new int(2)\ncout &lt;&lt; p3.use_count(); // =&gt; 1  返回所有者的数量，p3仍指向new int(2)\n</code></pre>\n<h1><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/how-to-create-and-use-weak-ptr-instances\">weak_ptr</a></h1>\n<p>weak_ptr 用于处理循环引用。</p>\n<blockquote>\n<p>待补</p>\n</blockquote>\n","tags":["cpp"]},{"id":"cpp-rtti","url":"https://yieldray.fun/posts/cpp-rtti","title":"c++RTTI","date_published":"2022-08-15T18:30:00.000Z","date_modified":"2022-08-15T18:30:00.000Z","content_text":"<h1>RTTI（Runtime Type Identification）</h1>\n<p>RTTI 允许程序在运行时确定对象的类型。</p>\n<p>C++ 类型转换运算符将传统 C 语言的类型转换进行了细分。<br>类型转换运算符的形式类似于泛型函数，尖括号指定目标类型，括号传入目标变量。</p>\n<p><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/casting-operators\">https://docs.microsoft.com/zh-cn/cpp/cpp/casting-operators</a></p>\n<h1><code>type_info typeid()</code></h1>\n<p><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/run-time-type-information\">https://docs.microsoft.com/zh-cn/cpp/cpp/run-time-type-information</a><br>运行时类型信息</p>\n<p>typeid() 运算符可以临时获取 type_info 类，括号内部既可以是变量，也可以是类型。<br>关于 type_info 类的定义，参见：<a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/type-info-class\">https://docs.microsoft.com/zh-cn/cpp/cpp/type-info-class</a></p>\n<pre><code class=\"language-cpp\">const type_info &amp;inf = typeid(123 * 0.1);\ncout &lt;&lt; inf.name() &lt;&lt; endl\n     &lt;&lt; inf.raw_name() &lt;&lt; endl\n     &lt;&lt; inf.hash_code() &lt;&lt; endl;\n\ntemplate &lt;typename T&gt;\nT max(T lhs, T rhs) {\n   cout &lt;&lt; &quot;Compared type is: &quot; &lt;&lt; typeid(T).name() &lt;&lt; endl;\n   return (lhs &gt; rhs ? lhs : rhs);\n}\n</code></pre>\n<h1><code>dynamic_cast&lt;T&gt;()</code></h1>\n<p><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/dynamic-cast-operator\">https://docs.microsoft.com/zh-cn/cpp/cpp/dynamic-cast-operator</a></p>\n<p>在运行时进行类型转换，允许在类层次结构中进行向上转换（向下转型）</p>\n<p>转换失败时，对于指针，返回 <code>0</code>；对于引用，抛出 <code>bad_cast</code> 异常（定义于头文件 typeinfo）</p>\n<h1><code>const_cast&lt;T&gt;()</code></h1>\n<p><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/const-cast-operator\">https://docs.microsoft.com/zh-cn/cpp/cpp/const-cast-operator</a></p>\n<p>const_cast 运算符用于改变 const 或 volatile<br>const_cast 允许临时将 const 类型转换为非 const 类型，但除 const 部分的类型不能修改</p>\n<h1><code>static_cast&lt;T&gt;()</code></h1>\n<p><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/static-cast-operator\">https://docs.microsoft.com/zh-cn/cpp/cpp/static-cast-operator</a></p>\n<p>static_cast 对应于 dynamic_cast， 不会进行运行时类型检查（进行编译时检查，失败时抛出编译错误）<br>static_cast 运算符用于告诉编译器进行显示类型转换，例如将精度大的类型转换为精度小的类型</p>\n<h1><code>reinterpret_cast&lt;T&gt;()</code></h1>\n<p><a href=\"https://docs.microsoft.com/zh-cn/cpp/cpp/reinterpret-cast-operator\">https://docs.microsoft.com/zh-cn/cpp/cpp/reinterpret-cast-operator</a></p>\n<p>reinterpret_cast 运算符允许除转换除 const 和 volatile 以外的任何类型（甚至将整形解释为指针）</p>\n","tags":["cpp"]},{"id":"java-jdbc","url":"https://yieldray.fun/posts/java-jdbc","title":"java使用jdbc","date_published":"2022-08-14T22:22:22.000Z","date_modified":"2022-08-14T22:22:22.000Z","content_text":"<h1>JDBC（Java Database Connectivity）</h1>\n<p>JDBC 定义了访问和处理（关系型）数据库的接口</p>\n<p>主要有这两个包提供：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/package-summary.html\">java.sql</a> 和 <a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/javax/sql/package-summary.html\">javax.sql</a></p>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Types.html\">java.sql.Types</a> 定义了数据库类型与 java 类型的映射</p>\n<p>通过 jdbc 访问数据库需要项目安装对应数据库的驱动，依赖类型只需为<code>&lt;scope&gt;runtime&lt;/scope&gt;</code><br>例如，MySQL 的驱动：<a href=\"https://mvnrepository.com/artifact/mysql/mysql-connector-java\">https://mvnrepository.com/artifact/mysql/mysql-connector-java</a><br>安装与 MySQL 服务器对应版本的驱动即可</p>\n<pre><code class=\"language-xml\">&lt;dependency&gt;\n    &lt;groupId&gt;mysql&lt;/groupId&gt;\n    &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;\n    &lt;version&gt;x.x.x&lt;/version&gt;\n    &lt;scope&gt;runtime&lt;/scope&gt;\n&lt;/dependency&gt;\n</code></pre>\n<h1>执行 SQL</h1>\n<p>查询，executeQuery</p>\n<pre><code class=\"language-java\">import java.sql.*;\n\npublic class Main {\n    public static void main(String[] args) throws Exception {\n        final String JDBC_URL = &quot;jdbc:mysql://localhost:3306/dbname?useSSL=false&amp;characterEncoding=utf8&quot;;\n        final String JDBC_USER = &quot;root&quot;;\n        final String JDBC_PASSWORD = &quot;pa$$word&quot;;\n\n        // 连接数据库\n        try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {\n            // 直接构造 SQL\n            try (Statement stmt = conn.createStatement()) {\n                try (ResultSet rs = stmt.executeQuery(&quot;SELECT id, grade, name, gender FROM students WHERE gender=1&quot;)) {\n                    while (rs.next()) {\n                        // 必须根据 SELECT 语句查询的顺序来调用ResultSet上面的方法，否则将抛出异常\n                        long id = rs.getLong(1); // 注意：索引从1开始\n                        long grade = rs.getLong(2);\n                        String name = rs.getString(3);\n                        int gender = rs.getInt(4);\n                    }\n                }\n            }\n            // 使用 PreparedStatement\n            try (PreparedStatement ps = conn.prepareStatement(&quot;SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?&quot;)) {\n                ps.setObject(1, &quot;M&quot;); // 注意：索引从1开始\n                ps.setObject(2, 3);\n                try (ResultSet rs = ps.executeQuery()) {\n                    while (rs.next()) {\n                        long id = rs.getLong(&quot;id&quot;);\n                        long grade = rs.getLong(&quot;grade&quot;);\n                        String name = rs.getString(&quot;name&quot;);\n                        String gender = rs.getString(&quot;gender&quot;);\n                    }\n                }\n            }\n            // 释放 Connection\n        }\n    }\n}\n</code></pre>\n<p>插入，executeUpdate<br>插入时应始终使用 PreparedStatement</p>\n<pre><code class=\"language-java\">try (PreparedStatement ps = conn.prepareStatement(\n        &quot;INSERT INTO students (grade, name, gender) VALUES (?,?,?)&quot;,\n        Statement.RETURN_GENERATED_KEYS))\n// 如果需要获取自增主键，需要指定 prepareStatement 函数的第二个参数\n{\n    ps.setObject(1, 1); // grade\n    ps.setObject(2, &quot;Bob&quot;); // name\n    ps.setObject(3, &quot;M&quot;); // gender\n    int n = ps.executeUpdate(); // 返回表中受影响的行数，这里将返回 1\n    try (ResultSet rs = ps.getGeneratedKeys()) {\n        if (rs.next()) {\n            long id = rs.getLong(1); // 注意：索引从1开始\n        }\n    }\n}\n</code></pre>\n<p>更新和删除语句和插入语句调用方法一致。</p>\n<h1>Batch 批量操作</h1>\n<p>使用 addBatch/executeBatch 而不是直接循环来执行批量操作</p>\n<pre><code class=\"language-java\">try (PreparedStatement ps = conn.prepareStatement(&quot;INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)&quot;)) {\n    for (Student s : students) {\n        ps.setString(1, s.name);\n        ps.setBoolean(2, s.gender);\n        ps.setInt(3, s.grade);\n        ps.setInt(4, s.score);\n        ps.addBatch(); // 每次循环结束，执行addBatch方法\n    }\n    // 运行所有添加的batch\n    int[] ns = ps.executeBatch();\n    // 一般来说，此方法返回值类似于executeUpdate方法，只不过返回的是数组\n}\n</code></pre>\n<h1>SQL 事务</h1>\n<pre><code class=\"language-java\">try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {\n    // 手动修改隔离级别，否则使用默认值\n    try {\n        conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);\n    } catch (SQLException e) {\n        // ... ...\n    }\n    // 执行事务\n    try {\n        // 必须关闭自动提交，才可以执行事务\n        conn.setAutoCommit(false);\n        // 执行多条SQL语句:\n        insert();\n        update();\n        delete();\n        // 提交事务:\n        conn.commit();\n    } catch (SQLException e) {\n        // 回滚事务:\n        conn.rollback();\n    } finally {\n        // 必须开启自动提交，因为单条SQL语句是隐式事务\n        conn.setAutoCommit(true);\n    }\n    // 自动释放 Connection（conn.close()）\n}\n</code></pre>\n<h1>JDBC 连接池</h1>\n<p>标准库仅提供了接口<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/javax/sql/DataSource.html\">javax.sql.DataSource</a>，标准库没有提供接口实现。<br>第三方实现：HikariCP 是 spring boot 的默认数据库连接池</p>\n","tags":["java"]},{"id":"js-observer-api","url":"https://yieldray.fun/posts/js-observer-api","title":"浏览器 Observer API","date_published":"2022-08-13T12:00:00.000Z","date_modified":"2022-08-13T12:00:00.000Z","content_text":"<p>观察器都是异步的，因此会性能更好</p>\n<h1>IntersectionObserver</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Intersection_Observer_API\">https://developer.mozilla.org/docs/Web/API/Intersection_Observer_API</a><br><a href=\"https://developer.mozilla.org/docs/Web/API/IntersectionObserverEntry\">https://developer.mozilla.org/docs/Web/API/IntersectionObserverEntry</a></p>\n<p><em>交叉区域：被观察元素在根元素中的可见区域</em></p>\n<pre><code class=\"language-ts\">const io = new IntersectionObserver(\n    /* IntersectionObserverCallback */\n    (entries: IntersectionObserverEntry[], observer: IntersectionObserver) =&gt; {\n        // entries : https://developer.mozilla.org/docs/Web/API/IntersectionObserverEntry\n        // observer: 当前 IntersectionObserver 实例\n        entries.forEach((entry) =&gt; {\n            // 每一个 entry 都描述了一个被观察元素的 intersection 变化\n            // 相当于 被观察元素.getBoundingClientRect()\n            // entry.boundingClientRect\n            // 等于 intersectionRect 与 boundingClientRect 的比例，[0, 1]\n            // entry.intersectionRatio\n            // 相当于 交叉区域的 DOMRectReadOnly\n            // entry.intersectionRect\n            // 是否交叉\n            // entry.isIntersecting\n            // 相当于 根元素的.getBoundingClientRect()\n            // entry.rootBounds\n            // 等于被观察元素\n            // entry.target\n            // intersection 变化发生的时间戳\n            // entry.time\n        });\n    },\n    /* IntersectionObserverInit */\n    {\n        root: document.querySelector(&quot;#scrollArea&quot;),\n        // 指定根 (root) 元素，用于检查目标的可见性。必须是目标元素的父级元素。\n        // 如果未指定或者为 null，则默认为浏览器视窗。\n        rootMargin: &quot;0px&quot;,\n        // 根 (root) 元素的外边距。类似于 CSS 中的 margin 属性，\n        // 比如 &quot;10px 20px 30px 40px&quot; (top, right, bottom, left)。\n        // 如果有指定 root 参数，则 rootMargin 也可以使用百分比来取值。\n        // 该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围，\n        // 使用该属性可以控制 root 元素每一边的收缩或者扩张。默认值为 0。\n        threshold: 1.0,\n        // 可以是单一的 number 也可以是 number 数组，\n        // target 元素和 root 元素相交程度达到该值的时候\n        // IntersectionObserver 注册的回调函数将会被执行。\n        // 如果你只是想要探测当 target 元素的在 root 元素中的可见性超过 50% 的时候，\n        // 你可以指定该属性值为 0.5。\n        // 如果你想要 target 元素在 root 元素的可见程度每多 25% 就执行一次回调，\n        // 那么你可以指定一个数组 [0, 0.25, 0.5, 0.75, 1]。\n        // 默认值是 0 (意味着只要有一个 target 像素出现在 root 元素中，回调函数将会被执行)。\n        // 该值为 1.0 的含义是当 target 完全出现在 root 元素中时候 回调才会被执行。\n    },\n);\n\n// 开始监听指定元素\nio.observe(document.querySelector(&quot;#target&quot;));\n\n// 停止监听指定元素\nio.unobserve(document.querySelector(&quot;#target&quot;));\n\n// 停止监听所有元素\nio.disconnect();\n\n// 即时获取 IntersectionObserverEntry 对象数组，同 IntersectionObserver 回调函数的第一个参数\nconst entries = intersectionObserver.takeRecords();\n</code></pre>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"IntersectionObserver\" src=\"https://codepen.io/YieldRay/embed/wvmENBd?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/wvmENBd\">\n  IntersectionObserver</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p>简单示例：</p>\n<pre><code>_______________\n|-------------|\n| #nav        |\n|-------------|\n|             |\n|             |\n|             |\n|             |\n|             |\n\nconst nav = document.getElementById(&quot;nav&quot;);\n</code></pre>\n<pre><code class=\"language-js\">const io = new IntersectionObserver(\n    (entries) =&gt; {\n        entries.forEach((entry) =&gt; console.log(entry));\n    },\n    {\n        threshold: [1],\n    },\n);\nio.observe(nav);\n// 在页面加载完成后立即触发一次\n// 由于浏览器默认的body具有margin，此时nav的顶部与视窗顶部具有一小段距离\n// 向下滚动页面，nav的顶部到达视窗顶时，触发一次\n// 继续向下滚动\n// 向上滚动页面，nav的顶部到达视窗顶时，触发一次\n// ... ...\n</code></pre>\n<pre><code class=\"language-js\">const io = new IntersectionObserver(\n    (entries) =&gt; {\n        entries.forEach((entry) =&gt; console.log(entry));\n    },\n    {\n        threshold: [0],\n    },\n);\nio.observe(nav);\n// 在页面加载完成后立即触发一次\n// 向下滚动直至nav的底部到达视窗顶时，触发一次\n// ... ...\n</code></pre>\n<pre><code class=\"language-js\">// 回调总是立即调用一次，无论 threshold 指定为任何值\ndocument.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {\n    io.observe(nav);\n});\n</code></pre>\n<h1>MutationObserver</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/MutationObserver\">https://developer.mozilla.org/docs/Web/API/MutationObserver</a></p>\n<pre><code class=\"language-ts\">const mo = new MutationObserver((mutations: MutationRecord[], observer: MutationObserver) =&gt; {\n    // entries : https://developer.mozilla.org/docs/Web/API/MutationRecord\n    // observer: 当前 MutationObserver 实例\n});\n\ninterface MutationRecord {\n    readonly addedNodes: NodeList;\n    readonly attributeName: string | null;\n    readonly attributeNamespace: string | null;\n    readonly nextSibling: Node | null;\n    readonly oldValue: string | null;\n    readonly previousSibling: Node | null;\n    readonly removedNodes: NodeList;\n    readonly target: Node;\n    readonly type: &quot;attributes&quot; | &quot;characterData&quot; | &quot;childList&quot;;\n}\n\nmo.observe(targetNode, config as MutationObserverInit);\n\ninterface MutationObserverInit {\n    /** 当为 true 时，将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性，而不仅仅是针对 target。默认值为 false。 */\n    subtree?: boolean;\n    /** 当为 true 时，监听 target 节点中发生的节点的新增与删除（同时，如果 subtree 为 true，会针对整个子树生效）。默认值为 false。 */\n    childList?: boolean;\n\n    /** 当为 true 时观察所有监听的节点属性值的变化。默认值为 true，当声明了 attributeFilter 或 attributeOldValue，默认值则为 false。 */\n    attributes?: boolean;\n    /** 一个用于声明哪些属性名会被监听的数组。如果不声明该属性，所有属性的变化都将触发通知。 */\n    attributeFilter?: string[];\n    /** 当为 true 时，记录上一次被监听的节点的属性变化；可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false。 */\n    attributeOldValue?: boolean;\n\n    /** 当为 true 时，监听声明的 target 节点上所有字符的变化。默认值为 true，如果声明了 characterDataOldValue，默认值则为 false */\n    characterData?: boolean;\n    /** 当为 true 时，记录前一个被监听的节点中发生的文本变化。默认值为 false */\n    characterDataOldValue?: boolean;\n}\n\n// 停止监听\n// 注意没有 unobserve 方法\nmo.disconnect();\n\n// 立即获取 MutationRecord 数组\nconst entries = mo.takeRecords();\n</code></pre>\n<h1>ResizeObserver</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/ResizeObserver\">https://developer.mozilla.org/docs/Web/API/ResizeObserver</a></p>\n<p>监听元素的内容区域（不包括 padding）或 SVG 元素的边界框改变。</p>\n<pre><code class=\"language-ts\">const ro = new ResizeObserver((entries: ResizeObserverEntry[], observer: ResizeObserver) =&gt; {\n    // entries:  https://developer.mozilla.org/docs/Web/API/ResizeObserverEntry\n    for (let entry of entries) {\n        // entry.target\n        // entry.borderBoxSize\n        // entry.contentBoxSize\n        // entry.devicePixelContentBoxSize\n        // entry.contentRect\n        console.log(entry);\n    }\n});\n\nro.observe(document.querySelector(&quot;#example&quot;), options as ResizeObserverOptions);\n\ninterface ResizeObserverOptions {\n    box?: ResizeObserverBoxOptions;\n}\n\nro.unobserve(document.querySelector(&quot;#example&quot;));\n\nro.disconnect();\n</code></pre>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"ResizeObserver\" src=\"https://codepen.io/YieldRay/embed/qBoJpGg?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/qBoJpGg\">\n  ResizeObserver</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1>PerformanceObserver</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/PerformanceObserver\">https://developer.mozilla.org/docs/Web/API/PerformanceObserver</a></p>\n<h1>ReportingObserver (目前仅 Chrome)</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/ReportingObserver\">https://developer.mozilla.org/docs/Web/API/ReportingObserver</a></p>\n","tags":["web-api","js"]},{"id":"js-pinia-intro","url":"https://yieldray.fun/posts/js-pinia-intro","title":"pinia入门","date_published":"2022-08-12T13:13:13.000Z","date_modified":"2022-08-12T13:13:13.000Z","content_text":"<p>pinia 是 vue 的状态管理库</p>\n<h1>store</h1>\n<p>创建 store</p>\n<pre><code class=\"language-js\">import { defineStore } from &quot;pinia&quot;;\n\n// useStore 可以是 useUser、useCart 之类的任何东西\n// 第一个参数是应用程序中 store 的唯一 id\nexport const useStore = defineStore(&quot;storeId&quot;, {\n    state: () =&gt; {\n        return {\n            counter: 0,\n            name: &quot;Eduardo&quot;,\n            isAdmin: true,\n        };\n    },\n    getters: {\n        doubleCount: (state) =&gt; state.counter * 2,\n    },\n    // other options...\n});\n</code></pre>\n<p>在 vue 应用中使用 store</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n    import { useStore } from &quot;@/stores/counter&quot;;\n\n    export default {\n        setup() {\n            const store = useStore();\n            // store 是一个用 reactive 包裹的对象\n            return {\n                // 返回整个 store 实例以在模板中使用它\n                store,\n            };\n        },\n    };\n&lt;/script&gt;\n</code></pre>\n<p>提取 store 属性同时保持其响应式</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n    import { storeToRefs } from &quot;pinia&quot;;\n\n    export default defineComponent({\n        setup() {\n            const store = useStore();\n            // `name` 和 `doubleCount` 是响应式引用\n            // 这也会为插件添加的属性创建引用\n            // 但跳过任何 action 或 非响应式（不是 ref/reactive）的属性\n            const { name, doubleCount } = storeToRefs(store);\n\n            return {\n                name,\n                doubleCount,\n            };\n        },\n    });\n&lt;/script&gt;\n</code></pre>\n<h1>state</h1>\n<p>重置状态为初始值</p>\n<pre><code class=\"language-js\">import { defineStore } from &quot;pinia&quot;;\n\nconst useStore = defineStore(&quot;storeId&quot;, {\n    state: () =&gt; {\n        return {\n            counter: 0,\n        };\n    },\n});\n\nconst store = useStore();\nstore.$reset();\n</code></pre>\n<p>改变状态</p>\n<pre><code class=\"language-js\">const store = useStore();\n// 直接改变状态\nstore.counter++;\n// 替换状态\nstore.$state = { counter: 666, name: &quot;Paimon&quot; };\n// 批量改变状态\nstore.$patch({\n    counter: store.counter + 1,\n    name: &quot;Abalam&quot;,\n});\n// 函数式批量改变状态，对于数组的修改尤为方便\ncartStore.$patch((state) =&gt; {\n    state.items.push({ name: &quot;shoes&quot;, quantity: 1 });\n    state.hasChanged = true;\n});\n// 订阅状态\ncartStore.$subscribe((mutation, state) =&gt; {\n    // import { MutationType } from &#39;pinia&#39;\n    mutation.type; // &#39;direct&#39; | &#39;patch object&#39; | &#39;patch function&#39;\n    // 与 cartStore.$id 相同\n    mutation.storeId; // &#39;cart&#39;\n    // 仅适用于 mutation.type === &#39;patch object&#39;\n    mutation.payload; // 补丁对象传递给 to cartStore.$patch()\n\n    // 每当它发生变化时，将整个状态持久化到本地存储\n    localStorage.setItem(&quot;cart&quot;, JSON.stringify(state));\n});\n</code></pre>\n<p>不使用组合式 API （<code>setup() hook</code>）时</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n    import { mapState } from &quot;pinia&quot;;\n    import { useCounterStore } from &quot;../stores/counterStore&quot;;\n\n    export default {\n        computed: {\n            // 允许访问组件内部的 this.counter\n            // 与从 store.counter 读取相同\n            ...mapState(useCounterStore, {\n                // myOwnName 映射到 store 的 counter 属性\n                myOwnName: &quot;counter&quot;,\n                // 您还可以编写一个访问 store 的函数\n                double: (store) =&gt; store.counter * 2,\n                // 它可以正常读取“this”，但无法正常写入...\n                magicValue(store) {\n                    return store.someGetter + this.counter + this.double;\n                },\n            }),\n            // 如果需要这些状态（原始值）可写，使用 mapWritableState() 函数替代\n            // 注意，如果是对象或数组直接使用 mapState 即可，无需使用此函数\n            ...mapWritableState(useCounterStore, [&quot;counter&quot;]),\n            // 当然也可以进行重导出，就像上方 mapState 一样\n            ...mapWritableState(useCounterStore, {\n                myOwnName: &quot;counter&quot;,\n            }),\n        },\n    };\n&lt;/script&gt;\n</code></pre>\n<h1>getters</h1>\n<p>getters 相当于 computed</p>\n<pre><code class=\"language-js\">export const useStore = defineStore(&quot;main&quot;, {\n    state: () =&gt; ({\n        counter: 0,\n    }),\n    getters: {\n        // 使用 state 参数时，自动将返回类型推断为数字\n        doubleCount(state) {\n            return state.counter * 2;\n            // or: doubleCount: (state) =&gt; state.counter * 2,\n        },\n        // 使用 this 时，返回类型必须明确设置\n        doublePlusOne(): number {\n            return this.counter * 2 + 1;\n        },\n    },\n});\n</code></pre>\n<h1>actions</h1>\n<p>actions 相当于 methods</p>\n<pre><code class=\"language-js\">export const useStore = defineStore(&quot;main&quot;, {\n    state: () =&gt; ({\n        counter: 0,\n    }),\n    actions: {\n        increment() {\n            this.counter++;\n        },\n        randomizeCounter() {\n            this.counter = Math.round(100 * Math.random());\n        },\n        async login(username, password) {\n            // actions 可以是异步的\n        },\n    },\n});\n</code></pre>\n<p>订阅 actions</p>\n<pre><code class=\"language-js\">const unsubscribe = someStore.$onAction(\n    ({\n        name, // action 的名字\n        store, // store 实例\n        args, // 调用这个 action 的参数\n        after, // 在这个 action 执行完毕之后，执行这个函数\n        onError, // 在这个 action 抛出异常的时候，执行这个函数\n    }) =&gt; {\n        // 记录开始的时间变量\n        const startTime = Date.now();\n        // 这将在 `store` 上的操作执行之前触发\n        console.log(`Start &quot;${name}&quot; with params [${args.join(&quot;, &quot;)}].`);\n\n        // 如果 action 成功并且完全运行后，after 将触发。\n        // 它将等待任何返回的 promise\n        after((result) =&gt; {\n            console.log(`Finished &quot;${name}&quot; after ${Date.now() - startTime}ms.\\nResult: ${result}.`);\n        });\n\n        // 如果 action 抛出或返回 Promise.reject ，onError 将触发\n        onError((error) =&gt; {\n            console.warn(`Failed &quot;${name}&quot; after ${Date.now() - startTime}ms.\\nError: ${error}.`);\n        });\n    },\n);\n\n// 手动移除订阅\nunsubscribe();\n</code></pre>\n<p>注意，无论是 state 订阅还是 actions 订阅，如果 store 位于组件的 setup() 内<br>那么组件卸载时自动取消订阅</p>\n<p>需要在卸载组件后保留，则：</p>\n<pre><code class=\"language-js\">someStore.$onAction(callback, true);\nsomeStore.$subscribe(callback, { detached: true });\n</code></pre>\n<h1>plugins</h1>\n<p><a href=\"https://pinia.web3doc.top/core-concepts/plugins.html\">https://pinia.web3doc.top/core-concepts/plugins.html</a></p>\n","tags":["js","lib"]},{"id":"docker-intro","url":"https://yieldray.fun/posts/docker-intro","title":"docker入门","date_published":"2022-08-11T10:00:00.000Z","date_modified":"2022-08-11T10:00:00.000Z","content_text":"<h1>安装 Docker CE</h1>\n<p><a href=\"https://docs.docker.com/engine/install/\">https://docs.docker.com/engine/install/</a><br><a href=\"https://docs.docker.com/get-started/\">https://docs.docker.com/get-started/</a><br><a href=\"https://labs.play-with-docker.com/\">https://labs.play-with-docker.com/</a><br><a href=\"https://github.com/jaywcjlove/docker-tutorial\">https://github.com/jaywcjlove/docker-tutorial</a><br><a href=\"https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html\">https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html</a></p>\n<h1>容器与镜像</h1>\n<p>容器是在宿主机运行的，与其它所有进程隔离的沙盒化的进程，是<em>运行中</em>的镜像的实例。</p>\n<p>（Docker 是 C/S 架构，我们可以使用命令行控制该容器）</p>\n<p>容器需要在隔离的文件系统中运行（试想 chroot），这个文件系统就是由镜像提供的。</p>\n<p>使用 docker，我们将应用程序与该程序的依赖，打包在一个镜像文件中。</p>\n<p>镜像文件也可包括容器所需的其它配置项，例如环境变量、默认的启动命令以及其它元数据。</p>\n<p>docker 根据该文件生成虚拟容器。程序在这个虚拟容器里运行，运行时生成自己的容器文件。</p>\n<p>简单来说，镜像文件就是生成 docker 应用的模板。</p>\n<h1>Getting Started</h1>\n<p>镜像完整名为：<code>[registry[:port]/][group/]image[:tag]</code></p>\n<p>registry 用于存放镜像文件，默认源为 <a href=\"https://registry.hub.docker.com\">https://registry.hub.docker.com</a>，国内可<a href=\"https://mirrors.ustc.edu.cn/help/dockerhub.html\">参考修改</a>之<br>默认组为 <code>library</code>（Docker 官方提供），因此 <code>hello-world</code> 相当于 <code>library/hello-world</code></p>\n<pre><code class=\"language-sh\"># 拉取\ndocker image pull hello-world\n# 查看\ndocker image ls\n# 查看（更短）\ndocker ps\n# 运行\ndocker container run hello-world\n</code></pre>\n<p>注意 <code>docker container run</code> 命令在本地没有指定镜像时会自动从远程拉取。</p>\n<p>运行上面的 <a href=\"https://hub.docker.com/_/hello-world\"><code>hello-world</code> 镜像</a>，该容器内部会执行一个二进制文件并打印一段消息到标准输出，然后自动停止。<br>再次注意 Docker 是 C/S 架构，二进制文件在容器中运行，容器的标准输出当然并不是宿主机的标准输出。<br>因此实际上是 Docker daemon 将容器内部的标准输出转发到宿主机上了。</p>\n<hr>\n<p>下面镜像生成的容器不会自动停止，而是生成伪终端，使我们进入容器的 Shell</p>\n<pre><code class=\"language-sh\"># -i, --interactive  Keep STDIN open even if not attached\n# -t, --tty          Allocate a pseudo-TTY\ndocker run -it ubuntu bash\n# 运行上面的命令后我们已经进入容器的 Shell，可以输入 exit 退出（或 Ctrl+D）\n# 要仅脱离 Shell，键入 Ctrl+P+Q\n\n# 如果我们没有使用 -it 参数，自然就不会直接进入容器的 Shell\n# 此时可以尝试下面的命令\n\n# 查看容器的标准输出（containerID 只需要输入前几位）\ndocker container logs [containerID]\n# 生成伪终端，并执行容器内的 bash 命令\ndocker container exec -it [containerID] /bin/bash\n# 将容器内的文件复制到宿主机\ndocker container cp [containID]:[/path/to/file] .\n\n# 终止容器（向容器内的主进程发出 SIGKILL 信号）\ndocker container kill [containID]\n# 发送 SIGTERM 信号，过一段时间再发出 SIGKILL 信号\ndocker container stop [containID]\n# 终止运行的容器文件，依然会占据硬盘空间\ndocker container ls --all\ndocker container rm [containerID]\n</code></pre>\n<h1>Dockerfile</h1>\n<blockquote>\n<p><code>Dockerfile</code> 只是用于生成镜像文件的脚本，可以只使用 docker 命令行达成相同的效果<br>不过 <code>Dockerfile</code> 更有利于版本控制</p>\n</blockquote>\n<p>下面给出一个 <a href=\"https://github.com/nodesource/distributions\">nodejs</a> 的示例，首先创建工作目录</p>\n<pre><code class=\"language-sh\">mkdir workspace\ncd workspace\necho -e &quot;node_modules&quot; &gt; .dockerignore\nnpm i express\necho -e &quot;require(&#39;express&#39;)().listen(3000).on(&#39;listening&#39;,()=&gt;console.log(process))&quot; &gt; index.js\ntouch Dockerfile\n</code></pre>\n<p>编辑 <code>Dockerfile</code> 文件</p>\n<pre><code class=\"language-dockerfile\"># syntax=docker/dockerfile:1\n\n# 继承指定镜像，可以在 https://index.docker.io/ 查看镜像\nFROM node:18-alpine\n# 设置镜像工作目录\nWORKDIR /app\n# 将当前目录的所有文件复制到镜像文件的 /app 目录\nCOPY . /app\n# 运行 shell 命令\nRUN npm install\n# 暴露镜像的 3000 端口\nEXPOSE 3000\n# 设置启动时默认执行的命令\nCMD [&quot;node&quot;, &quot;index.js&quot;]\n</code></pre>\n<pre><code class=\"language-sh\"># 构建镜像（build 子命令自动寻找 Dockerfile 文件）\ndocker image build -t docker-test[:版本] .\n# 将宿主机的 8000 端口绑定至容器的 3000 端口\n# --rm 参数使容器在运行结束后自动删除容器文件\ndocker container run [--rm] -p 8000:3000 -it docker-test [/bin/bash]\n# 列出容器\ndocker container ls [-all]\n# 恢复运行容器文件（不要指定 --rm）\ndocker container start [containID]\n</code></pre>\n<p>发布镜像，参见：<a href=\"https://docs.docker.com/get-started/04_sharing_app/\">https://docs.docker.com/get-started/04_sharing_app/</a></p>\n<h1>Docker Compose</h1>\n<p>Docker 的每个容器应当仅运行单个应用，但一个完整的服务往往由多个应用组成。<br>Docker Compose 可用于容器编排。如果没有它，则需要手动运行多个冗长的 docker 命令。</p>\n<pre><code class=\"language-sh\"># -d                           Run container in background and print container ID\n# --network &lt;network&gt;          Connect a container to a network\n# --network-alias &lt;alias&gt;      Add network-scoped alias for the container\n# -v &lt;local_dir&gt;:&lt;remote_dir&gt;  Bind mount a volume\n# -e &lt;env&gt;                     Set environment variables\ndocker run -d \\\n    --network todo-app --network-alias mysql \\\n    -v todo-mysql-data:/var/lib/mysql \\\n    -e MYSQL_ROOT_PASSWORD=secret \\\n    -e MYSQL_DATABASE=todos \\\n    mysql:8.0\n\n# docker exec -it &lt;mysql-container-id&gt; mysql -u root -p\n# 指定 --network-alias mysql 后\n# 可以在其它容器（也指定了 --network todo-app）内直接连接 mysql:3306\n\n\n# -w &lt;workdir&gt;                 Working directory inside the container\n# -p &lt;local&gt;:&lt;remote&gt;          Publish a container&#39;s port(s) to the host\ndocker run -dp 127.0.0.1:3000:3000 \\\n  -w /app -v &quot;$(pwd):/app&quot; \\\n  --network todo-app \\\n  -e MYSQL_HOST=mysql:3306 \\\n  -e MYSQL_USER=root \\\n  -e MYSQL_PASSWORD=secret \\\n  -e MYSQL_DB=todos \\\n  node:18-alpine \\\n  sh -c &quot;yarn install &amp;&amp; yarn run dev&quot;\n</code></pre>\n<hr>\n<p>使用 Docker Compose，可以改写为如下 <code>compose.yaml</code> 文件</p>\n<pre><code class=\"language-yml\">services:\n    app:\n        image: node:18-alpine\n        command: sh -c &quot;yarn install &amp;&amp; yarn run dev&quot;\n        ports:\n            - 127.0.0.1:3000:3000\n        working_dir: /app\n        volumes:\n            - ./:/app\n        environment:\n            MYSQL_HOST: mysql\n            MYSQL_USER: root\n            MYSQL_PASSWORD: secret\n            MYSQL_DB: todos\n\n    mysql:\n        image: mysql:8.0\n        volumes:\n            - todo-mysql-data:/var/lib/mysql\n        environment:\n            MYSQL_ROOT_PASSWORD: secret\n            MYSQL_DATABASE: todos\n\nvolumes:\n    todo-mysql-data:\n</code></pre>\n<p>使用如下命令运行</p>\n<pre><code class=\"language-sh\">docker compose up -d\n</code></pre>\n","tags":["docker","linux"]},{"id":"js-redux-intro","url":"https://yieldray.fun/posts/js-redux-intro","title":"redux入门","date_published":"2022-08-10T21:21:21.000Z","date_modified":"2022-08-10T21:21:21.000Z","content_text":"<h1>store，以及概念</h1>\n<pre><code class=\"language-js\">import { createStore } from &quot;redux&quot;;\n\n// store 是 redux 存储数据的地方\n// reducer 函数用于描述数据如何改变（给定任意 action）\nconst store = createStore(reducer[, init]);\n// state 对象表现了 store 的瞬时状态（快照）\nconst state = store.getState();\n// action 对象会派发给 reducer 进行操作，携带了供 reducer 函数操作的信息\nstore.dispatch(action);\n// 可以使用 listener 函数订阅 store 的变化\nconst unsubscribe = store.subscribe(listener);\n</code></pre>\n<h1>action</h1>\n<pre><code class=\"language-js\">// redux 中的 action 一般有如下结构\nconst ADD_TODO = &quot;ADD_TODO&quot;;\nconst action_1 = {\n    type: ADD_TODO,\n    text: &quot;Build my first Redux app&quot;,\n};\n// 为了简洁，以下直接使用字符串而不是常量作为type\nconst action_1 = {\n    type: &quot;ADD_TODO&quot;,\n    text: &quot;Build my first Redux app&quot;,\n};\n// 在redux中，为了方便生成一类 type 的 action，一般使用函数生成 action 如下\nfunction addTodo(text) {\n    return {\n        type: &quot;ADD_TODO&quot;,\n        text,\n    };\n}\nconst action_1 = addTodo(&quot;Build my first Redux app&quot;);\n</code></pre>\n<h1>reducer</h1>\n<p>reducer 是<em>纯函数</em>，描述 action 如何改变 state</p>\n<pre><code class=\"language-js\">const defaultState = {}; // 默认值是可选的，也可以在 createStore 函数中指定\n// reducer 函数通过当前 state 和当前 action 计算出下一次的 state （并返回）\n// 当前指的是 store.dispatch(action) 调用执行的前一刻\n// 很明显，store.dispatch(action) 调用的内部提供了 reducer 函数需要的 state 和 action 参数\n// 并且，在该调用完成之后，store.getState() 就反应计算后的 state 了\nconst reducer = (state = defaultState, action) =&gt; {\n    switch (action.type) {\n        case &quot;ADD_TODO&quot;:\n            return Object.assign({}, state, { currentTodo: action.text });\n        // 不应修改 state 对象，应当认为 state 是只读的\n        default:\n            return state;\n    }\n};\n</code></pre>\n<p>结合之前的代码</p>\n<pre><code class=\"language-js\">const store = createStore(reducer, { currentTodo: &quot;None&quot; }); // 初始值可选\nstore.dispatch(addTodo(&quot;Build my first Redux app&quot;));\nconst { currentTodo } = store.getState(); // =&gt; Build my first Redux app\n</code></pre>\n<p>如何保持 reducer 的纯洁性?</p>\n<pre><code class=\"language-js\">// 对于对象，除了使用 Object.assign 之外，还可以\nreturn { ...state, replaceOrSetNewKey: value, ...orDestructAnotherObject };\n// 对于数组，也可以使解构\nreturn [...state, ...newState];\n// 使用数组concat\nreturn state.concat(newState);\n// 总之，只要保证是纯函数即可\n</code></pre>\n<h1>state</h1>\n<p>state 反映的 store 的当前状态，应当认为是只读的</p>\n<pre><code class=\"language-js\">const state = store.getState();\n</code></pre>\n<h1>listener</h1>\n<p>listener 只是一个 store 变化就会触发的函数而已</p>\n<pre><code class=\"language-js\">const unsubscribe = store.subscribe(() =&gt; console.log(&quot;Current state is: &quot;, store.getState()));\nstore.dispatch(addTodo(&quot;Store is changed&quot;));\nunsubscribe();\n</code></pre>\n<h1>utils</h1>\n<h2>combineReducers()</h2>\n<p>一个应用只能有一个 store，但在数据较多时将导致 reducer 函数臃肿<br>可以将 reducer 拆分为多个子 reducer（每个子 reducer 负责对应的属性），然后通过 combineReducers 函数合并</p>\n<pre><code class=\"language-js\">function todos(state = [], action) {\n    switch (action.type) {\n        case &quot;ADD_TODO&quot;:\n            return state.concat({\n                text: action.text,\n                completed: false,\n            });\n\n        case &quot;TOGGLE_TODO&quot;:\n            return state.map((todo, index) =&gt;\n                index === action.index ? { ...todo, completed: !todo.completed } : todo,\n            );\n\n        default:\n            return state;\n    }\n}\n\nfunction visibilityFilter(state = &quot;SHOW_ALL&quot;, action) {\n    switch (action.type) {\n        case &quot;SET_VISIBILITY_FILTER&quot;:\n            return action.filter;\n        default:\n            return state;\n    }\n}\n\n// 在不使用 combineReducers 工具函数时，可以这样写\nfunction todoApp(state = {}, action) {\n    return {\n        visibilityFilter: visibilityFilter(state.visibilityFilter, action),\n        todos: todos(state.todos, action),\n    };\n}\n\n// 使用 combineReducers\nimport { combineReducers } from &quot;redux&quot;;\nconst todoApp = combineReducers({\n    visibilityFilter,\n    todos,\n});\n\n// 注意，在合并子 reducer 时，完全可以将 子reducer 重命名为 reducer 的其它属性\n</code></pre>\n","tags":["js","react","lib"]},{"id":"js-tagged-templates","url":"https://yieldray.fun/posts/js-tagged-templates","title":"js标签模板","date_published":"2022-08-09T16:26:15.000Z","date_modified":"2022-08-09T16:26:15.000Z","content_text":"<h1>标签模板结构</h1>\n<pre><code class=\"language-js\">function tag(...args) {\n    console.log(JSON.stringify(args, null, 4));\n}\n</code></pre>\n<pre><code class=\"language-js\">tag`Hello`; // =&gt; [[&quot;Hello&quot;]]\ntag`Hello, world!`; // [[&quot;Hello, world!&quot;]]\ntag`Hello, ${&quot;world&quot;}!`; // [[&quot;Hello, &quot;, &quot;!&quot;], &quot;world&quot;]\ntag`left ${123} right`; // [[&quot;left &quot;, &quot; right&quot;], 123]\ntag`one${1}two${2}tree${3}`; // [[&quot;one&quot;, &quot;two&quot;, &quot;tree&quot;, &quot;&quot;], 1, 2, 3]\ntag`obj${{}}arr${[]}`; // [[&quot;obj&quot;, &quot;arr&quot;, &quot;&quot;], {}, []]\n</code></pre>\n<h1>标签模板使用</h1>\n<p>通用形式</p>\n<pre><code class=\"language-ts\">interface TemplateStringsArray extends ReadonlyArray&lt;string&gt; {\n    readonly raw: readonly string[];\n}\n\nfunction tag(strings: TemplateStringsArray, ...data: string[]) {\n    console.log(strings);\n    console.log(data);\n}\n</code></pre>\n<p>通过观察可以发现，<code>strings.length === data.length + 1</code> 必然成立<br><img src=\"https://s2.loli.net/2022/08/12/MYpFqc891uRmSUd.png\" alt=\"tagged-templates.png\"></p>\n","tags":["js"]},{"id":"db-sql","url":"https://yieldray.fun/posts/db-sql","title":"SQL数据库","date_published":"2022-08-08T18:22:22.000Z","date_modified":"2022-08-08T18:22:22.000Z","content_text":"<p>参见<br><a href=\"https://github.com/jaywcjlove/mysql-tutorial\">https://github.com/jaywcjlove/mysql-tutorial</a><br><a href=\"https://dev.mysql.com/doc/refman/8.0/en/\">https://dev.mysql.com/doc/refman/8.0/en/</a><br><a href=\"https://learn.microsoft.com/zh-cn/sql/sql-server/\">https://learn.microsoft.com/zh-cn/sql/sql-server/</a><br><a href=\"https://mariadb.com/kb/en/sql-statements/\">https://mariadb.com/kb/en/sql-statements/</a></p>\n<h1>管理数据库</h1>\n<pre><code class=\"language-sql\">-- 创建数据库\nCREATE DATABASE &lt;数据库名&gt;;\n-- 创建并设定字符集\nCREATE DATABASE &lt;数据库名&gt; CHARACTER SET utf8;\n-- 删除数据库\nDROP DATABASE &lt;数据库名&gt;;\n-- 修改数据库字符集\nALTER DATABASE &lt;数据库名&gt; CHARACTER SET utf8;\n\n\n-- 列出所有数据库\nSHOW DATABASES;\n-- 切换当前数据库\nUSE &lt;数据库名&gt;;\n\n\n-- 显示连接的用户进程\nSHOW PROCESSLIST;\n-- 删除连接\nKILL &lt;进程号&gt;;\n</code></pre>\n<h1>创建和删除表</h1>\n<p><a href=\"https://dev.mysql.com/doc/refman/8.0/en/data-types.html\">https://dev.mysql.com/doc/refman/8.0/en/data-types.html</a><br><a href=\"https://learn.microsoft.com/zh-cn/sql/t-sql/data-types/data-types-transact-sql\">https://learn.microsoft.com/zh-cn/sql/t-sql/data-types/data-types-transact-sql</a></p>\n<p>常用列类型：</p>\n<ul>\n<li>INTEGER, BOOLEAN</li>\n<li>FLOAT, DOUBLE, REAL</li>\n<li>CHARACTER(最多字符数), VARCHAR(最多字符数), TEXT <em>超出最多字符数的可能会被截断</em></li>\n<li>DATE, DATETIME</li>\n<li>BLOB</li>\n</ul>\n<p>常用列常量：</p>\n<ul>\n<li>PRIMARY KEY</li>\n<li>AUTOINCREMENT</li>\n<li>UNIQUE</li>\n<li>NOT NULL</li>\n<li>CHECK(表达式)</li>\n<li>FOREIGN KEY</li>\n</ul>\n<pre><code class=\"language-sql\">-- 列出当前数据库的所有表\nSHOW TABLES;\n-- 查看创建表的SQL语句\nSHOW CREATE TABLE &lt;表名&gt;;\n-- 查看一个表的结构\nDESC &lt;表名&gt;;\nDESCRIBE &lt;表名&gt;;\nSHOW FIELDS FROM &lt;表名&gt;;\n\n\n-- 创建表\nCREATE TABLE &lt;表名&gt; (\n    &lt;列名1&gt; &lt;列类型1&gt;,\n    &lt;列名2&gt; &lt;列类型2&gt;,\n    &lt;列名3&gt; &lt;列类型3&gt;,\n    PRIMARY KEY (&lt;列名1&gt;),\n    FOREIGN KEY (&lt;列名2&gt;) REFERENCES &lt;表名2&gt;(&lt;列名2&gt;)\n);\n\n-- 使用 IF (NOT) EXISTS 时，不会报错\nCREATE TABLE IF NOT EXISTS &lt;表名&gt; (\n    &lt;列名&gt; &lt;列类型&gt; &lt;列常量&gt; DEFAULT &lt;默认值&gt;,\n    …\n);\n\n-- 增加列\nALTER TABLE &lt;表名&gt;\nADD &lt;新增的列名&gt; &lt;列类型&gt; &lt;可选列常量&gt; DEFAULT &lt;默认值&gt;;\n\n-- 删除列\nALTER TABLE &lt;表名&gt;\nDROP &lt;待删除的列名&gt;;\n\n-- 修改表\nALTER TABLE &lt;表名&gt;\nRENAME TO &lt;新的表名&gt;;\n\n-- 删除表\nDROP TABLE IF EXISTS &lt;表名&gt;;\n-- 删除表但保留定义\nTRUNCATE &lt;表名&gt;;\n-- 保留定义，且可回滚\nDELETE FROM &lt;表名&gt;;\n</code></pre>\n<h1>查询表</h1>\n<table>\n<thead>\n<tr>\n<th>常用条件</th>\n<th>SQL</th>\n<th>示例</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>空值判断</td>\n<td>IS/IS NOT NULL</td>\n<td><code>&lt;列名&gt; IS NULL</code></td>\n</tr>\n<tr>\n<td>比较操作符（注意，是一个等于号）</td>\n<td>=, !=, &lt; &lt;=, &gt;, &gt;=</td>\n<td><code>&lt;列名&gt; &lt;操作符&gt; &lt;值&gt;</code></td>\n</tr>\n<tr>\n<td>闭区间</td>\n<td>BETWEEN … AND …</td>\n<td><code>&lt;列名&gt; BETWEEN &lt;起始值&gt; AND &lt;终止值&gt;</code></td>\n</tr>\n<tr>\n<td>反选闭区间</td>\n<td>NOT BETWEEN … AND …</td>\n<td></td>\n</tr>\n<tr>\n<td>在集合内</td>\n<td>IN (…)</td>\n<td><code>&lt;列名&gt; IN (&lt;值1&gt;, &lt;值2&gt;, …)</code></td>\n</tr>\n<tr>\n<td>不在集合内</td>\n<td>NOT IN (…)</td>\n<td></td>\n</tr>\n<tr>\n<td>不分大小写比较相同字符串</td>\n<td>LIKE</td>\n<td><code>&lt;列名，字符串类型&gt; LIKE &quot;字符串&quot;</code></td>\n</tr>\n<tr>\n<td>不分大小写比较不相同字符串</td>\n<td>NOT LIKE</td>\n<td></td>\n</tr>\n<tr>\n<td>LIKE 匹配下，表示零个或多个任意字符的占位符</td>\n<td>%</td>\n<td><code>&lt;列名，字符串类型&gt; LIKE &quot;字符串%&quot;</code></td>\n</tr>\n<tr>\n<td>LIKE 匹配下，表示一个任意字符的占位符</td>\n<td>_</td>\n<td><code>&lt;列名，字符串类型&gt; LIKE &quot;字符串_&quot;</code></td>\n</tr>\n</tbody></table>\n<pre><code class=\"language-sql\">-- 选择所有表\nSELECT * FROM &lt;表名&gt;;\n\n-- 条件选择表\nSELECT &lt;列名1&gt;, &lt;列名2&gt;, … FROM &lt;表名&gt;\nWHERE &lt;条件1&gt;\n    AND/OR &lt;条件2&gt;\n    AND/OR …\nORDER BY &lt;列名&gt; ASC/DESC\nLIMIT &lt;限制数量&gt; OFFSET &lt;跳过数量&gt;;\n\n-- 过滤重复值\nSELECT DISTINCT &lt;列名1&gt;, &lt;列名2&gt;, … FROM &lt;表名&gt;;\n\n-- 别名\nSELECT &lt;列名&gt; AS &lt;列名的别名&gt;, … FROM &lt;表名&gt;;\nSELECT &lt;函数返回值&gt; AS &lt;命名为某列&gt;, … FROM &lt;表名&gt;;\n\n-- 连接表\nSELECT &lt;列名1&gt;, &lt;列名2&gt;, … FROM &lt;表名&gt;\nINNER/LEFT/RIGHT/FULL JOIN &lt;另一个表名&gt; ON &lt;表名&gt;.&lt;某列名&gt; = &lt;另一个表名&gt;.&lt;某列名&gt;\nWHERE &lt;条件&gt;\nORDER BY &lt;列名1&gt;, … ASC/DESC\nLIMIT &lt;限制数量&gt; OFFSET &lt;跳过数量&gt;;\n\n-- 分组\nSELECT &lt;列名&gt;, &lt;函数返回值&gt; AS &lt;命名为某列&gt;, … FROM &lt;表名&gt;\nWHERE &lt;条件&gt;\nGROUP BY &lt;列名&gt;\nHAVING &lt;条件&gt;;\n\n-- 笛卡尔查询（慎用，多个表相乘）\nSELECT &lt;表或别名&gt;.&lt;列名&gt; &lt;可选别名&gt;,\n       &lt;表或别名&gt;.&lt;列名&gt; &lt;可选别名&gt;,\n       …\nFROM &lt;表1&gt; &lt;别名1&gt;, &lt;表2&gt; &lt;别名&gt;, …;\n</code></pre>\n<h1>插入与修改表</h1>\n<pre><code class=\"language-sql\">-- 插入一行（值可以由表达式或函数产生）\nINSERT INTO &lt;表名&gt; (&lt;列名1&gt;, &lt;列名2&gt;, &lt;列名3&gt;, …)\nVALUES (&lt;值11&gt;, &lt;值12&gt;, &lt;值13&gt;, …),\nVALUES (&lt;值21&gt;, &lt;值22&gt;, &lt;值23&gt;, …);\n\n-- 插入一行，省略列名\nINSERT INTO &lt;表名&gt;\nVALUES (&lt;值1&gt;, &lt;值2&gt;, &lt;值3&gt;, …);\n\n-- 更新行\nUPDATE &lt;表名&gt;\nSET &lt;列名11&gt; = &lt;值11&gt;, &lt;列名12&gt; = &lt;值12&gt;, ...\n    &lt;列名21&gt; = &lt;值21&gt;, &lt;列名22&gt; = &lt;值22&gt;, ...\nWHERE &lt;条件&gt;;\n\n-- 删除行\nDELETE FROM &lt;表名&gt;\nWHERE &lt;条件&gt;;\n</code></pre>\n<h1>视图</h1>\n<p>视图是<strong>虚拟的表</strong>，反映了数据的最新状态</p>\n<pre><code class=\"language-sql\">-- 创建视图\nCREATE VIEW &lt;视图名&gt; AS\n    SELECT &lt;列名1&gt;, &lt;列名2&gt;, …\n    FROM &lt;表名&gt;\n    WHERE &lt;条件&gt;;\n\n-- 删除视图\nDROP VIEW &lt;视图名&gt;;\n\n-- 选择视图\nSELECT * FROM &lt;视图名&gt;;\n</code></pre>\n<h1>函数</h1>\n<pre><code class=\"language-sql\">-- 返回总行数\nCOUNT(*)\n-- 返回指定列的非空行数\nCOUNT(&lt;列名&gt;)\n-- 返回指定列中所有行中的最小值\nMIN(&lt;列名&gt;)\nMAX(&lt;列名&gt;)\nAVG(&lt;列名&gt;)\nSUM(&lt;列名&gt;)\n-- 取绝对值\nABS(&lt;数字&gt;)\n</code></pre>\n","tags":["database"]},{"id":"java-design-pattern","url":"https://yieldray.fun/posts/java-design-pattern","title":"设计模式java描述","date_published":"2022-08-07T15:00:00.000Z","date_modified":"2022-08-07T15:00:00.000Z","content_text":"<p>注意：设计模式适用于<code>面向对象设计</code></p>\n<p>下面演示的代码仅作为一种思路，实际代码不应是死板的，而应根据需要进行编写</p>\n<blockquote>\n<p>参考：<br><a href=\"https://refactoringguru.cn/\">https://refactoringguru.cn/</a><br><a href=\"https://design-patterns.readthedocs.io/zh_CN/latest/\">https://design-patterns.readthedocs.io/zh_CN/latest/</a></p>\n</blockquote>\n<h1>创建型模式</h1>\n<p>在创建型模式中，我们向调用者隐藏了构建产品的实现过程。</p>\n<h2>工厂方法 Virtual Constructor、Factory Method</h2>\n<p>工厂方法模式建议使用特殊的工厂方法代替对构造函数的直接调用 （即使用 new 运算符）。\n工厂方法返回的对象通常被称作 “产品”。</p>\n<p>对于产品，我们（在产品有多种实现时）（在函数签名上）提供的是抽象类，而非具体实现类。<br>也就是说，产品对应唯一的抽象类（有时也可以是接口）。调用者获取的也是这个抽象类。（调用者面向抽象而非面向实现）</p>\n<p>往往使用静态工厂方法：</p>\n<pre><code class=\"language-java\">Integer n = Integer.valueOf(100);\nList&lt;String&gt; list = List.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;);\nMessageDigest md5 = MessageDigest.getInstance(&quot;MD5&quot;);\n</code></pre>\n<p>上面，抽象类本身提供了静态方法，用于获取一个抽象类实例。</p>\n<p>此外，工厂方法还隐藏了对象的构造过程。（其中可能包括缓存等操作，提高性能）</p>\n<h2>抽象工厂 Abstract Factory</h2>\n<p>在抽象工厂中，我们有多个工厂，这些工厂都可以生产我们需要的产品。<br>因此，我们将这些工厂抽象化，只需了解抽象工厂就可以生产产品。（因此，调用者无需了解工厂的具体实现）</p>\n<h2>生成器 Builder</h2>\n<p>生成器允许我们分步骤地创建产品。</p>\n<pre><code class=\"language-java\">import java.net.http.*;\nimport java.net.*;\nimport java.time.Duration;\n\npublic class Main {\n    public static void main(String[] args) {\n        HttpRequest request = HttpRequest.newBuilder() // 获取一个生成器，类型为 HttpRequest.Builder\n                .GET()\n                .uri(URI.create(&quot;https://example.net/&quot;))\n                .timeout(Duration.ofSeconds(10))\n                .build(); // 获取产品，类型为 HttpRequest\n\n        // 下方代码可以忽略\n        HttpClient client = HttpClient.newHttpClient();\n        client.sendAsync(request, HttpResponse.BodyHandlers.ofString())\n                .thenApply(HttpResponse::body)\n                .thenAccept(System.out::println)\n                .join();\n    }\n}\n</code></pre>\n<p>注意，在 builder() 之后、build() 之前的链式调用中，返回的都是生成器。因此，一般来说构建方法不分顺序。</p>\n<p>在 java 中，StringBuilder 也是一个常见的 Builder API</p>\n<h2>原型 Clone、Prototype</h2>\n<p>原型允许我们复制（克隆）一个原有的对象，并且<strong>新旧对象相互独立，互不影响</strong>。</p>\n<pre><code class=\"language-java\">String[] proto = {&quot;One&quot;, &quot;Two&quot;, &quot;Tree&quot;};\nString[] cloned = Arrays.copyOf(proto, proto.length);\nSystem.out.println(Arrays.equals(proto, cloned)); // =&gt; true\n</code></pre>\n<p>在 java 中，Cloneable 是一个空接口（标记接口），表示实现者可以使用 Object.clone() 方法。</p>\n<pre><code class=\"language-java\">class Example implements Cloneable {\n    private int data;\n\n    public Object clone() {\n        var cloned = new Example();\n        cloned.data = this.data; // 复制原型的数据\n        return cloned;\n    }\n}\n</code></pre>\n<p>原型模式只适用于原型实例的数据可以被无副作用地复制的情况，否则不符合原型的定义。</p>\n<h2>单例 Singleton</h2>\n<p>单例保证这个类只有一个实例， 并提供一个访问该实例的全局节点。</p>\n<pre><code class=\"language-java\">class Singleton {\n    // 私有构造函数\n    private Singleton() {\n    }\n\n    private static final Singleton INSTANCE = new Singleton();\n\n    // 对外公开getInstance方法获取单例\n    public static Singleton getInstance() {\n        return INSTANCE;\n    }\n}\n</code></pre>\n<h1>结构型模式</h1>\n<p>结构型模式涉及不同对象之间形成的一种结构</p>\n<h2>适配器 Adapter</h2>\n<p><img src=\"https://s2.loli.net/2022/08/07/NVJAxqg1fQzdjbH.png\" alt=\"adapter.png\"></p>\n<p>适配器将我们已有的对象（包装起来），转换为我们希望的对象类型。<br>因为现有的对象不对接口兼容，因此适配器实际上作为兼容层，提供了我们需要的接口。</p>\n<p>例如：</p>\n<pre><code class=\"language-java\">InputStream input = otherSource(); // 已有一个InputStream类型\n// 但我们更希望以 Reader 的方式使用\nReader reader = new InputStreamReader(input, StandardCharsets.UTF_8);\n// 这里 InputStreamReader 就作为一个适配器，提供 InputStream 到 Reader 的转换\n</code></pre>\n<pre><code class=\"language-java\">// 旧的接口\ninterface OldInterface {\n    public void oldFunc();\n}\n\n// 新的接口\ninterface NewInterface {\n    public void newFunc();\n}\n\n// 旧的实现类\nclass OldClass implements OldInterface {\n    public void oldFunc() {\n    }\n}\n\n// 适配器（相当于新的实现类）\nclass Adapter implements NewInterface {\n    private OldInterface old;\n\n    // 需要持有（包装）需要适配的对象\n    public Adapter(OldInterface old) {\n        this.old = old;\n    }\n\n    public void newFunc() {\n        old.oldFunc(); // 包装旧接口，转换为新接口\n    }\n}\n</code></pre>\n<p>最终，适配器就符合我们需要的形状（提供需要的接口）</p>\n<h2>桥接 Bridge</h2>\n<p><img src=\"https://s2.loli.net/2022/08/07/D9BFjbLAehOS2nz.png\" alt=\"bridge.png\"></p>\n<p>桥接模式可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构，从而能在开发时分别使用。</p>\n<p>举个例子，我们想编写一个应用程序，这个程序需要在 Windows、Linux、MacOS 平台上都能运行。<br>我们可以针对每一个平台都编写相应代码，从而支持多种平台。<br>桥接模式类似于我们选用一种跨平台框架，这个框架已经适配了多个平台。<br>因此，我们只需针对这个框架编写代码就能实现跨平台。</p>\n<pre><code class=\"language-java\">// 当然，除了interface也可以是class\ninterface Color {}\nclass Red implements Color {}\nclass Blue implements Color {}\n\nabstract class Shape {\n    public Color color;\n    public abstract void paint();\n}\nclass Square extends Shape {\n    public Square(Color color) {this.color = color;}\n    public void paint() {}\n}\nclass Circle extends Shape {\n    public Circle(Color color) {this.color = color;}\n    public void paint() {}\n}\n\n// 使用桥接模式避免了编写如下代码：\nabstract class Shape {\n    public abstract void paint();\n}\nclass RedSquare extends Shape{}\n</code></pre>\n<h2>组合 Composite</h2>\n<p>你可以使用组合模式，将对象组合成树状结构，并能像使用独立对象一样使用它们。</p>\n<p>例如，一颗 DOM 树是由多个子结点“组合”而成的，而子结点又是由它的子节点“组合”而成的。</p>\n<h2>装饰器 Decorator</h2>\n<p>装饰器允许你通过 将对象放入 包含行为的 特殊封装对象中 来为 原对象 绑定 新的行为。</p>\n<pre><code class=\"language-java\">InputStream input =\n        new GZIPInputStream(\n                new BufferedInputStream(\n                        new FileInputStream(&quot;test.gz&quot;)\n                )\n        );\n</code></pre>\n<h2>外观 Facade</h2>\n<p>外观类为包含许多活动部件的复杂子系统提供一个简单的接口。与直接调用子系统相比，外观提供的功能可能比较有限，但它却包含了客户端真正关心的功能。</p>\n<h2>享元 Flyweight</h2>\n<p>享元模式让我们无需在每个对象中都保存所有数据。而是共享多个对象所共有的相同状态（复用公共状态），让你能在有限的内存容量中载入更多对象。</p>\n<h2>代理 Proxy</h2>\n<p>代理模式允许我们代理原对象的行为，（允许我们在原本的行为前后增加新行为）从而增加（或者控制）已有的功能</p>\n<p>注意，适配器是 X -&gt; Y，代理则是 X -&gt; X</p>\n<h1>行为型模式</h1>\n<p>行为型模式描述了一组对象应当表现的行为，从而协同完成整体任务。</p>\n<h2>责任链 Chain of Responsibility</h2>\n<p><img src=\"https://s2.loli.net/2022/08/07/SpEoDULzBsNl9eb.png\" alt=\"CoR.png\"></p>\n<p>责任链模式允许我们将请求沿着处理者链进行传递。收到请求后，每个处理者均可对请求进行处理，或将其传递给链上的下个处理者。</p>\n<h2>命令 Action、Transaction、Command</h2>\n<p>命令模式在发送者和请求者之间建立单向连接。允许我们将请求转换为一个包含与请求相关的所有信息的独立对象。<br>该转换根据不同的请求将方法参数化、延迟请求执行或将其放入队列中，且能实现可撤销操作。</p>\n<p>例如，我们有多种请求。在命令模式中，我们将不同的请求都封装成<strong>同一类型</strong>的对象，然后将这个对象发送给实现者。</p>\n<p>发送者完成了告知命令的要求，而实现者则可以构造一个任务队列进行处理。</p>\n<h2>解释器 Interpreter</h2>\n<p>解释器模式允许我们通过规定一种文法（定义一个解释器）来解释符合规定的输入。</p>\n<p>数据交换格式（JSON XML）、正则表达式、解释型语言（Python）以及某些 DSL （SQL）都通过解释器模式工作。</p>\n<h2>迭代器 Iterator</h2>\n<p>迭代器模式让我们能在不暴露集合底层表现形式 （列表、 栈和树等） 的情况下遍历集合中所有的元素。</p>\n<p>在 java 中，能够迭代的对象实现 <code>Iterable&lt;T&gt;</code> 接口，接口方法 <code>iterator()</code> 返回 <code>Iterator&lt;E&gt;</code> 迭代器<br>迭代器方法主要通过 <code>hasNext()</code> <code>next()</code> 实现迭代</p>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Iterable.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Iterable.html</a><br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Iterator.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Iterator.html</a></p>\n<h2>中介者 Intermediary、Controller、Mediator</h2>\n<p>中介者模式能让我们减少对象之间混乱无序的依赖关系（阻止对象的直接依赖）。<br>该模式会限制对象之间的直接交互，迫使它们通过一个中介者对象进行合作（统一依赖一个中介对象实现间接操作）。</p>\n<h2>备忘录 Snapshot、Memento</h2>\n<p>备忘录模式允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。</p>\n<p>典型的例子就是文本编辑器，实现了保存、重做、撤销功能。</p>\n<h2>观察者 Event-Subscriber、Listener、Observer</h2>\n<p>观察者模式允许我们定义一种订阅机制，可在对象事件发生时通知多个 “观察” 该对象的观察者对象。</p>\n<p>需要（被）观察的对象应维护一个观察者列表，触发事件时，通知列表中的所有观察者。</p>\n<h2>状态 State</h2>\n<p>状态模式是让我们能在一个对象的内部状态变化时改变其行为，使其看上去就像改变了自身所属的类一样。</p>\n<p>状态模式建议为对象的所有可能状态新建一个类（而不是将行为写死在 if else switch 里），然后将所有状态的对应行为抽取到这些类中。</p>\n<p>原始对象被称为上下文（con­text），它并不会自行实现所有行为，<br>而是保存一个指向表示当前状态的状态对象的引用，且将所有与状态相关的工作委派给该对象。<br>（所有状态都实现同一接口）</p>\n<h2>策略 Strategy</h2>\n<p>为了实现同一个目标，我们可以有多种策略。策略模式让我们在实现同一个目标时使用唯一接口，但允许我们用额外的选项来切换真正实施的策略。</p>\n<h2>模板方法 Template Method</h2>\n<p>在模板方法模式中，超类中定义了一个算法的框架，允许子类在不修改结构的情况下重写算法的特定步骤。</p>\n<p>假设我们有一组对象，它们的具有相同的方法，大部分方法的行为相同，但又有一小部分不同。<br>我们将这些不同的对象都抽象为一个超类，超类的所有方法（可以有默认实现）就是模板（或者说超类本身是模板）。<br>每个需要与之有不同行为的子类都按需要覆写特定的方法。</p>\n<h2>访问者 Visitor</h2>\n<p>访问者模式将算法与其所作用的对象隔离开来。</p>\n<p>需要执行操作的原始对象将作为参数被传递给访问者中。</p>\n<p>泛型机制可以看作是一种访问者模式。<br>某些语言的“鸭子类型”也与之类似，因为我们不关心具体的类型，而是这些类型公共的方法。</p>\n<pre><code class=\"language-java\">interface Element {\n    void accept(Visitor v);\n}\n\nclass One implements Element {\n    public void accept(Visitor v) {}\n}\n\nclass Two implements Element {\n    public void accept(Visitor v) {}\n}\n\ninterface Visitor {\n    void visitOne(One one);\n    void visitTwo(Two two);\n}\n\nclass SpecialVisitor implements Visitor {\n    public void visitOne(One one) {}\n    public void visitTwo(Two two) {}\n}\n\n\npublic class Main {\n    public static void main(String[] args) {\n        // 元素接收（任意）访问者\n        new One().accept(new SpecialVisitor());\n        // 访问者访问（特定）元素\n        new SpecialVisitor().visitTwo(new Two());\n    }\n}\n</code></pre>\n","tags":["java"]},{"id":"java-test","url":"https://yieldray.fun/posts/java-test","title":"java测试","date_published":"2022-08-06T12:00:00.000Z","date_modified":"2022-08-06T12:00:00.000Z","content_text":"<h1>单元测试</h1>\n<h1>JUnit5</h1>\n<p>在 <a href=\"https://junit.org/junit5/docs/current/user-guide/\">这里</a> 阅读 JUnit 的指引文档<br>文章仅介绍了部分 API，具体参见文档</p>\n<pre><code class=\"language-java\">import static org.junit.jupiter.api.Assertions.*; // 导入Assertions类下的的所有静态方法\n\nimport java.util.*;\n\nimport org.junit.jupiter.api.Test; // Test 注解\nimport org.junit.jupiter.api.condition.*; // 条件相关注解\nimport org.junit.jupiter.params.ParameterizedTest; // 参数化测试相关\nimport org.junit.jupiter.params.provider.*; // 参数化测试，数据源提供相关\n\n\npublic class 测试文件名 {\n    // 测试函数需要标注此注解\n    @Test\n    // 若使用 EnabledOnOs 注解，测试将只在指定平台下运行\n    // 不匹配的平台，测试函数将跳过而不执行\n    @EnabledOnOs(OS.WINDOWS)\n    void test1() { // 函数名是任意的，应当与具体测试内容相关\n        assertEquals(2, 1 + 1);\n        assertTrue(1 + 1 == 2);\n        assertFalse(1 + 1 != 2);\n    }\n\n\n    @Test\n    @DisabledOnOs({OS.LINUX, OS.MAC})\n    @DisabledOnJre(JRE.JAVA_8)\n    // 匹配 os.arch 类型为 .*64.* （64位）\n    @EnabledIfSystemProperty(named = &quot;os.arch&quot;, matches = &quot;.*64.*&quot;)\n    // 需指定环境变量 DEBUG 为 true\n    @EnabledIfEnvironmentVariable(named = &quot;DEBUG&quot;, matches = &quot;true&quot;)\n    void test2() { // 测试类下的所有@Test标注的函数都会被调用\n        assertNotNull(new Object());\n        assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2, 3});\n        assertEquals(0.1, Math.abs(1 - 9 / 10.0), 0.0000001); // 比较浮点数时需指定误差值\n        assertThrows(Exception.class, () -&gt; {\n            throw new Exception();\n        }); // 测试异常，通过的测试在需在lambda函数抛出指定异常\n    }\n\n\n    // 参数化测试，测试的函数需要参数\n    @ParameterizedTest\n    // 指定待测参数\n    @ValueSource(ints = {0, 1, 5, 100})\n    void testAbs(int x) {\n        assertEquals(x, Math.abs(x));\n    }\n\n\n    @ParameterizedTest\n    @MethodSource\n    void testUpperCase(String input, String result) {\n        assertEquals(result, input.toUpperCase(Locale.ROOT));\n    }\n\n    // 同名静态方法将成为数据源，注意 Arguments 类，指定了输入和预期输出\n    static List&lt;Arguments&gt; testUpperCase() {\n        return List.of(Arguments.of(&quot;abc&quot;, &quot;ABC&quot;), Arguments.of(&quot;apple&quot;, &quot;APPLE&quot;), Arguments.of(&quot;good&quot;, &quot;GOOD&quot;));\n    }\n\n\n    @ParameterizedTest\n    @MethodSource(&quot;testLowerCase_Source&quot;)\n    void testLowerCase(String input, String result) {\n        assertEquals(result, input.toLowerCase(Locale.ROOT));\n    }\n\n    // MethodSource 注解也可以指定一个函数作为数据源\n    static List&lt;Arguments&gt; testLowerCase_Source() {\n        return List.of(Arguments.of(&quot;ABC&quot;, &quot;abc&quot;));\n    }\n\n    // 指定CSV格式作为数据源，输入和预期输出之间用逗号分隔，逗号旁边空格为可选\n    @ParameterizedTest\n    @CsvSource({&quot;ha, haha&quot;, &quot;bye,byebye&quot;})\n    void testRepeatTwice(String input, String result) {\n        assertEquals(result, input.repeat(2));\n    }\n\n\n    // 将CSV文件作为数据源，文件将从classpath中查找，建议置于测试源码同级目录\n    @ParameterizedTest\n    @CsvFileSource(resources = {&quot;/testRepeatTwiceCSVFile.csv&quot;})\n    void testRepeatTwiceCSVFile(String input, String result) {\n        assertEquals(result, input.repeat(2));\n    }\n    // testRepeatTwiceCSVFile.csv 内容如下（一行一组参数）：\n    // ha, haha\n    // bye, byebye\n}\n</code></pre>\n<h1>运行测试</h1>\n<p>maven 项目可以通过 <code>mvn clean test</code> 运行测试（注意预装的 JUnit 版本）</p>\n<p>idea 中，可以在 <code>File -&gt; Project Structure</code> 中指定一个测试文件夹，运行时将通过 JUnit 环境执行</p>\n<p>不建议直接在命令行运行，依赖项比较多，且编译和执行都比较复杂</p>\n<h1>Fixture</h1>\n<p>测试 Fixture 可用（统一地）初始化测试环境，也可以进行清理</p>\n<p>标注任意方法：<br>@BeforeEach<br>@AfterEach</p>\n<p>标注静态方法：<br>@BeforeAll<br>@AfterAll</p>\n<pre><code class=\"language-java\">// 标注的函数名是任意的\n\n@BeforeEach\nvoid beforeEach() {\n}\n\n@BeforeAll\nstatic void beforeAll() {\n    // 必须声明为静态方法\n}\n</code></pre>\n<h1>集成测试</h1>\n<p>集成测试用于测试系统的各个部件是否能协同工作</p>\n","tags":["java"]},{"id":"java-encrypt","url":"https://yieldray.fun/posts/java-encrypt","title":"java加密","date_published":"2022-08-05T12:00:00.000Z","date_modified":"2022-08-05T12:00:00.000Z","content_text":"<p>主要接口：<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/package-summary.html\">java.security</a><br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/javax/crypto/package-summary.html\">javax.crypto</a></p>\n<h1>hashCode()</h1>\n<p>不同的对象（通过 equals 方法比较）应当返回不同的 hashCode。</p>\n<p>主要提供给哈希表</p>\n<h1>java.security.MessageDigest 接口 （提供散列算法）</h1>\n<pre><code class=\"language-java\">import java.math.BigInteger;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\npublic class Main {\n    public static void main(String[] args) throws NoSuchAlgorithmException {\n        // 指定加密算法，若无法获取指定算法则抛出 NoSuchAlgorithmException 异常\n        MessageDigest md = MessageDigest.getInstance(&quot;MD5&quot;);\n        // update 方法传入字节数组，多次调用可依次传入\n        md.update(&quot;Hello&quot;.getBytes(StandardCharsets.UTF_8));\n        md.update(&quot;World&quot;.getBytes(StandardCharsets.UTF_8));\n        // digest 方法消费所有输入，得到加密后的值\n        byte[] result = md.digest();\n        System.out.println(new BigInteger(1, result).toString(16));\n        // =&gt; 68e109f0f40ca72a15e05cc22786f8e6\n    }\n}\n</code></pre>\n<p>标准库默认支持的算法：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#messagedigest-algorithms\">https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#messagedigest-algorithms</a></p>\n<p>通过外部依赖获取其它算法：</p>\n<pre><code class=\"language-java\">Security.addProvider(new BouncyCastleProvider());\nMessageDigest md = MessageDigest.getInstance(&quot;RipeMD160&quot;);\n</code></pre>\n<h1>javax.crypto.Mac 对象 （HMAC）</h1>\n<p>HMAC 在散列函数的基础上添加了消息认证码，加密时需要额外的认证码</p>\n<p>出于安全考虑，认证码一般通过 KeyGenerator 对象来生成</p>\n<pre><code class=\"language-java\">import java.math.BigInteger;\nimport java.nio.charset.StandardCharsets;\nimport javax.crypto.*;\nimport java.security.*;\n\n\npublic class Main {\n    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {\n        // 生成随机 key\n        KeyGenerator keyGen = KeyGenerator.getInstance(&quot;HmacMD5&quot;);\n        SecretKey key = keyGen.generateKey();\n\n        // HMAC\n        Mac mac = Mac.getInstance(&quot;HmacMD5&quot;);\n        mac.init(key); // 提供 key，key无效时抛出 InvalidKeyException 异常\n        mac.update(&quot;HelloWorld&quot;.getBytes(StandardCharsets.UTF_8));\n        byte[] result = mac.doFinal();\n        System.out.println(new BigInteger(1, result).toString(16));\n        // 这个输出根据key的不同而不同，由于这里使用的key是随机生成的，因此加密结果也是随机的\n    }\n}\n</code></pre>\n<blockquote>\n<p>哈希函数没有解密接口，通过再次加密然后比对进行检查</p>\n</blockquote>\n<pre><code class=\"language-java\">import java.math.BigInteger;\nimport javax.crypto.spec.SecretKeySpec;\nimport javax.crypto.*;\nimport java.security.*;\n\npublic class Main {\n    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {\n        // 生成随机 key\n        KeyGenerator keyGen = KeyGenerator.getInstance(&quot;HmacMD5&quot;);\n        SecretKey key = keyGen.generateKey();\n        byte[] keyBytes = key.getEncoded();\n        String keyAsString =  new BigInteger(1, keyBytes).toString(16);\n        System.out.println(keyAsString);\n\n        // 通过已有的 key 获取 SecretKey 对象 （下面是将16进制表示的key转换为字节数组）\n        byte[] privateKey = new BigInteger(&quot;a21a50424f87a4e4e0c0967a47b615f495da2dfda1e30ff0b839924033dd5225be3dd7af2f5095340412e4fd6ea0b533f00a3b2e8bdea43200acd9192bbb6a69&quot;,16).toByteArray();\n        SecretKey keyFromBytes = new SecretKeySpec(privateKey, &quot;HmacMD5&quot;);\n\n        // 提供我们已有的 key\n        Mac mac = Mac.getInstance(&quot;HmacMD5&quot;);\n        mac.init(keyFromBytes);\n        // ... 其它逻辑\n    }\n}\n</code></pre>\n<h1>对称加密算法</h1>\n<p>对称加密算法能通过 key 将数据加密，并能通过 key 将加密的结果还原</p>\n<h1>口令加密算法</h1>\n<p>口令加密算法能通过任意长度的密码和随机盐值得到 key （key 是固定长度的）</p>\n<h1>密钥交换算法</h1>\n<p>密钥交换算法用于协商一个 key，这个 key 可以提供给 对称加密算法 使用</p>\n<p>参见：Diffie-Hellman 算法</p>\n<h1>非对称加密算法</h1>\n<p>在密钥交换算法中存在公钥和私钥。通过私钥可计算出公钥，但无法通过公钥得到私钥（这类似于散列算法）</p>\n<p>公钥是对外公开的，对所有人都可见；私钥则必须保密</p>\n<p>通信时，发送方将 接收方的公钥 作为 key 加密数据，而接收方可以通过 自己的私钥 解密 发送方加密的数据（公钥加密私钥解密）</p>\n<h1>签名算法</h1>\n<p>（接上文）<br>也可以反过来通过私钥加密公钥解密：<br>在进行数字签名时，发布者通过私钥进行签名（加密），验证者用发布者的公钥来验证（解密）</p>\n<h1>数字证书</h1>\n<p>java 提供了 keytool 命令行工具用于生成和管理证书，证书文件的后缀是 <code>.keystore</code> (nginx 证书则是 <code>.crt</code>)</p>\n<h1>参见</h1>\n<p><a href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1255943717668160\">https://www.liaoxuefeng.com/wiki/1252599548343744/1255943717668160</a></p>\n","tags":["java"]},{"id":"java-encode-decode","url":"https://yieldray.fun/posts/java-encode-decode","title":"java编/解码","date_published":"2022-08-04T12:00:00.000Z","date_modified":"2022-08-04T12:00:00.000Z","content_text":"<h1>URL 编/解码</h1>\n<p><code>application/x-www-form-urlencoded</code></p>\n<p><a href=\"https://www.online-java.com/cyRKl8GOYf\">Edit Online</a></p>\n<pre><code class=\"language-java\">import java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\n\npublic class Main {\n    public static void main(String[] args) {\n        // 此函数需要指定编码\n        String encoded = URLEncoder.encode(&quot;中文&quot;, StandardCharsets.UTF_8);\n        System.out.println(encoded); // =&gt; %E4%B8%AD%E6%96%87\n\n        String decoded = URLDecoder.decode(&quot;%E4%B8%AD%E6%96%87&quot;, StandardCharsets.UTF_8);\n        System.out.println(decoded); // =&gt; 中文\n    }\n}\n</code></pre>\n<p>注意这两个函数都有一个字符编码可指定为字符串的重载函数，但会抛出异常，故不推荐<br>应当始终使用 UTF-8 编码</p>\n<pre><code class=\"language-java\">try {\n    String encoded = URLEncoder.encode(&quot;中文&quot;, &quot;utf-8&quot;);\n    String decoded = URLDecoder.decode(&quot;%E4%B8%AD%E6%96%87&quot;, &quot;UTF-8&quot;);\n} catch (UnsupportedEncodingException e) {\n    // ...\n}\n</code></pre>\n<h1>Base64 编/解码</h1>\n<pre><code class=\"language-java\">import java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\nimport java.util.Base64;\n\npublic class Main {\n    public static void main(String[] args) {\n        Base64.Encoder base64Encoder = Base64.getEncoder(); // 编码器\n        Base64.Decoder base64Decoder = Base64.getDecoder(); // 解码器\n\n        byte[] originalBytes = new byte[]{-28, -72, -83, -26, -106, -121};\n        // 相当于： &quot;中文&quot;.getBytes(StandardCharsets.UTF_8)\n\n        // 编码为字节数组\n        byte[] encodedBytes = base64Encoder.encode(originalBytes);\n        // 编码为字符串\n        String encodedString = base64Encoder.encodeToString(originalBytes);\n\n        System.out.println(Arrays.toString(originalBytes)); // =&gt; [-28, -72, -83, -26, -106, -121]\n        System.out.println(Arrays.toString(encodedBytes)); // =&gt; [53, 76, 105, 116, 53, 112, 97, 72]\n        System.out.println(encodedString); // =&gt; 5Lit5paH\n        // 下面的方法将字节数组转换为String\n        System.out.println(new String(encodedBytes, StandardCharsets.UTF_8)); // =&gt; 5Lit5paH\n\n\n        // 解码方法是一个重载方法\n        byte[] decodedBytes1 = base64Decoder.decode(&quot;5Lit5paH&quot;);\n        byte[] decodedBytes2 = base64Decoder.decode(encodedBytes);\n        System.out.println(Arrays.toString(decodedBytes1)); // =&gt; [-28, -72, -83, -26, -106, -121]\n        System.out.println(Arrays.toString(decodedBytes2)); // =&gt; [-28, -72, -83, -26, -106, -121]\n        System.out.println(new String(decodedBytes1, StandardCharsets.UTF_8)); // =&gt; 中文\n\n\n        // 还可以通过withoutPadding方法获取一个不添加末尾`=`的编码器（无需对应解码器）\n        Base64.Encoder encoder2 = Base64.getEncoder().withoutPadding();\n\n        // 获取适用于URL的Base64变种编/解码器\n        Base64.Encoder encoder3 = Base64.getUrlEncoder();\n        Base64.Decoder decoder3 = Base64.getUrlDecoder();\n\n        // 获取适用于Mime的Base64变种编/解码器\n        Base64.Encoder encoder4 = Base64.getMimeEncoder();\n        Base64.Decoder decoder4 = Base64.getMimeDecoder();\n    }\n}\n</code></pre>\n<p>具体参见文档：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Base64.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Base64.html</a></p>\n","tags":["java"]},{"id":"rust-mod","url":"https://yieldray.fun/posts/rust-mod","title":"rust模块","date_published":"2022-08-02T08:45:23.000Z","date_modified":"2022-08-02T08:45:23.000Z","content_text":"<h1>使用 Cargo 管理包</h1>\n<p>一个包中至多包含一个 <code>库 crate</code> (<code>library crate</code>)<br>可以包含任意多个 <code>二进制 crate</code> (<code>binary crate</code>)<br>每个包中至少包含一个 crate</p>\n<h1>目录结构</h1>\n<p><code>src/main.rs</code> 就是一个与包同名的 <code>二进制 crate</code> 的 crate 根</p>\n<p><code>src/lib.rs</code> 则是一个与包同名的 <code>库 crate</code> 的 crate 根</p>\n<p><code>库 crate</code> 至多存在一个，而 <code>二进制 crate</code> 若存在多个，则每个 <code>src/bin</code> 下的文件都会被编译成一个独立的 <code>二进制 crate</code></p>\n<h1>mod 关键字</h1>\n<p>使用 mod 关键字声明模块，模块可以任意层级嵌套</p>\n<pre><code class=\"language-rust\">// src/lib.rs\nmod module_name {\n    mod sub_module {\n        fn do_something() {}\n    }\n}\n</code></pre>\n<h1>路径</h1>\n<p>使用路径获取模块。<br>以 crate:: 开头的为绝对路径，从 crate 根开始查找；相对路径则从当前文件进行查找<br>例如：</p>\n<pre><code class=\"language-rust\">// src/lib.rs\ncreate::module_name::sub_module::do_something();\nmodule_name::sub_module::do_something();\n</code></pre>\n<p>以 <code>super::</code> 开头的，<code>super</code> 指代当前模块的父模块名<br>例如：</p>\n<pre><code class=\"language-rust\">// src/lib.rs\nmod outer {\n    fn f1() {}\n    mod inner {\n        fn f2() {}\n        fn f3() {\n            super::f1(); // 获取父层级\n            f2(); // 获取同层级\n        }\n    }\n}\n</code></pre>\n<h1>pub 关键字</h1>\n<pre><code class=\"language-rust\">// src/lib.rs\nmod outer {\n    pub mod inner {\n        pub fn f() {}\n    }\n    pub struct Season { // pub 结构体只有 pub 字段才对外可见\n        pub display: String, // 此字段可见\n        value: i32, // 此字段不可见\n    }\n    pub enum Boolean { // pub 枚举的成员都自动为 pub\n        False,\n        True,\n    }\n}\n\npub fn todo() {\n    // Absolute path\n    crate::outer::inner::f();\n\n    // Relative path\n    outer::inner::f();\n}\n</code></pre>\n<h1>use 关键字</h1>\n<p>use 将暴露的项引入作用域</p>\n<pre><code class=\"language-rust\">use outer::inner::f;\nf();\n// or\nuse outer::inner;\ninner::f();\n// or ...\n</code></pre>\n<p>在测试时，可以使用通配符，引入所有</p>\n<pre><code class=\"language-rust\">use outer::*;\ninner::f();\nlet bool = Boolean::True;\n// 这里注意，如果结构体含有非 pub 字段，则无法直接实例化\n// 例如，以下代码无法编译：\nlet s = Season {\n    display: String::from(&quot;123&quot;),\n    value: 123, // 这个字段是私有的！！\n};\n</code></pre>\n<p>使用花括号导入路径的多个部分</p>\n<pre><code class=\"language-rust\">use outer::inner::{Season, Boolean};\n\nuse outer::{inner::Season, f};\n\nuse outer::inner::{self, f}; // self 表示路径本身\n/* 相当于：\nuse outer::inner;\nuse outer::inner::f;\n*/\n</code></pre>\n<h1>as 关键字</h1>\n<p>使用 as 关键字提供新的名称</p>\n<pre><code class=\"language-rust\">use outer::inner::f as function;\nfunction();\n</code></pre>\n<p>使用 pub use 重导出名称</p>\n<pre><code class=\"language-rust\">// src/lib.rs\nmod outer {\n    fn f1() {}\n    mod inner {\n        fn f2() {}\n    }\n}\n\npub use crate::outer::inner;\npub fn f3() {\n    inner::f2();\n}\n</code></pre>\n<h1>使用外部包</h1>\n<pre><code class=\"language-toml\">//  Cargo.toml\nclap = &quot;2.27.1&quot; # 来自 crates.io\nrand = { git = &quot;https://github.com/rust-lang-nursery/rand&quot; } # 来自网上的仓库\nbar = { path = &quot;../bar&quot; } # 来自本地文件系统的路径\n</code></pre>\n<pre><code class=\"language-rust\">use rand::Rng;\nfn main() {\n    let secret_number = rand::thread_rng().gen_range(1..101);\n}\n</code></pre>\n<h1>将模块分割进文件</h1>\n<pre><code class=\"language-rust\">// src/utils.rs\npub mod toolbox {\n    pub fn fix() {}\n}\n</code></pre>\n<pre><code class=\"language-rust\">// src/lib.rs\nmod utils; // 在另一个文件的模块\npub use crate::utils::toolbox;\n\ntoolbox::fix();\n</code></pre>\n<h1>文件分层</h1>\n<pre><code>|-- my\n|   |-- inaccessible.rs\n|   |-- mod.rs\n|   `-- nested.rs\n`-- split.rs\n</code></pre>\n<pre><code class=\"language-rust\">mod my;\n// 这个声明会查找 `my.rs` 或 `my/mod.rs` 文件，然后将该文件的内容放到当前作用域中一个名为 `my` 的模块里面。\n</code></pre>\n<pre><code class=\"language-rust\">mod inaccessible;\npub mod nested;\n// 此处同理\n</code></pre>\n<h1>一个包中管理多个 crate</h1>\n<p><a href=\"https://www.rustwiki.org.cn/zh-CN/book/ch14-03-cargo-workspaces.html\">https://www.rustwiki.org.cn/zh-CN/book/ch14-03-cargo-workspaces.html</a></p>\n<h1>库，生成 rlib 文件</h1>\n<pre><code class=\"language-rust\">pub fn public_function() {\n    println!(&quot;`public_function()` is called&quot;);\n}\n</code></pre>\n<pre><code class=\"language-sh\">$ rustc --crate-type=lib mylib.rs\n</code></pre>\n<p>同目录下得到 <code>libmylib.rlib</code> 文件（默认添加了 <code>lib</code> 前缀）<br>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/rust-by-example/attribute/crate.html\">https://www.rustwiki.org.cn/zh-CN/rust-by-example/attribute/crate.html</a></p>\n<pre><code class=\"language-rust\">fn main(){\n    mylib::public_function();\n}\n</code></pre>\n<pre><code class=\"language-sh\">$ rustc run.rs --extern mylib=libmylib.rlib\n# extern 的参数是 导入的库名=库的路径\n</code></pre>\n<pre><code class=\"language-sh\">$ ./run\n\n`public_function()` is called\n</code></pre>\n","tags":["rust"]},{"id":"rust-trait","url":"https://yieldray.fun/posts/rust-trait","title":"rust trait","date_published":"2022-07-31T12:00:00.000Z","date_modified":"2022-07-31T12:00:00.000Z","content_text":"<h1>父（超） trait（supertrait）</h1>\n<p>实现子 trait 时还需要已经实现父 trait</p>\n<pre><code class=\"language-rust\">trait Super {\n    fn s(&amp;self);\n}\n\ntrait Child: Super {\n    fn c(&amp;self);\n    fn s(&amp;self); // 不是覆盖\n}\n\nstruct Test();\n\nimpl Super for Test {\n    fn s(&amp;self) {\n        println!(&quot;call Super::s()&quot;)\n    }\n}\n\nimpl Child for Test {\n    // 只能实现 Child 中的函数，而不能实现 Super 中的函数\n    fn c(&amp;self) {\n        println!(&quot;call Child::c()&quot;)\n    }\n    fn s(&amp;self) {\n        println!(&quot;call Child::s()&quot;)\n    }\n}\n\nfn main() {\n    let test = Test();\n    test.c();\n    // 无法使用 test.s();\n    Super::s(&amp;test);\n    Child::s(&amp;test);\n}\n</code></pre>\n<p>相关内容参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/book/ch19-03-advanced-traits.html#%E5%AE%8C%E5%85%A8%E9%99%90%E5%AE%9A%E8%AF%AD%E6%B3%95%E4%B8%8E%E6%B6%88%E6%AD%A7%E4%B9%89%E8%B0%83%E7%94%A8%E7%9B%B8%E5%90%8C%E5%90%8D%E7%A7%B0%E7%9A%84%E6%96%B9%E6%B3%95\">完全限定语法与消歧义：调用相同名称的方法</a></p>\n<h1>trait 标注生命周期</h1>\n<p>结构体若含引用成员（则结构体本身必须标注生命周期），结构体在实现 trait 时也需要通过泛型指定生命周期</p>\n<blockquote>\n<p>摘自 <a href=\"https://www.rustwiki.org.cn/zh-CN/rust-by-example/scope/lifetime/trait.html\">https://www.rustwiki.org.cn/zh-CN/rust-by-example/scope/lifetime/trait.html</a></p>\n</blockquote>\n<pre><code class=\"language-rust\">// 带有生命周期标注的结构体。\n#[derive(Debug)]\n struct Borrowed&lt;&#39;a&gt; {\n     x: &amp;&#39;a i32,\n }\n\n// 给 impl 标注生命周期。\nimpl&lt;&#39;a&gt; Default for Borrowed&lt;&#39;a&gt; {\n    fn default() -&gt; Self {\n        Self {\n            x: &amp;10,\n        }\n    }\n}\n\nfn main() {\n    let b: Borrowed = Default::default();\n    println!(&quot;b is {:?}&quot;, b);\n}\n</code></pre>\n<h1>trait bond</h1>\n<p>假设已有如下定义：</p>\n<pre><code class=\"language-rust\">trait Value {\n    fn value(&amp;self) -&gt; isize;\n}\n\nstruct Int32(i32);\n\nstruct Int64(i64);\n\nimpl Value for Int32 {\n    fn value(&amp;self) -&gt; isize {\n        self.0 as isize\n    }\n}\nimpl Value for Int64 {\n    fn value(&amp;self) -&gt; isize {\n        self.0 as isize\n    }\n}\n</code></pre>\n<p>使用 impl 约束，可指定参数类型实现了给定 trait</p>\n<pre><code class=\"language-rust\">\nfn sum(lhs: impl Value, rhs: impl Value) -&gt; isize {\n    lhs.value() + rhs.value()\n}\n\nfn main() {\n    println!(&quot;{}&quot;, sum(Int32(1), Int64(2)));\n}\n</code></pre>\n<p>在上例中，只要实现了给定 trait 就可以作为参数</p>\n<p>下面的例子实现了对参数类型的限制，要求两个参数不仅实现了给定 trait，二者类型还需相同</p>\n<pre><code class=\"language-rust\">fn sum&lt;T: Value&gt;(lhs: T, rhs: T) -&gt; isize {\n    // 注意 T: Value 表示类型 T 实现了 Value，而不使用语法： T impl Value\n    lhs.value() + rhs.value()\n}\n\nfn main() {\n    println!(&quot;{}&quot;, sum(Int32(1), Int32(2)));\n    println!(&quot;{}&quot;, sum(Int64(1), Int64(2)));\n}\n</code></pre>\n<p>也可以使用 where 限定，效果一致</p>\n<pre><code class=\"language-rust\">fn sum&lt;T&gt;(lhs: T, rhs: T) -&gt; isize\nwhere\n    T: Value,\n{\n    lhs.value() + rhs.value()\n}\n</code></pre>\n<p>需要多重约束时，将多个 trait 用 加号 <code>+</code> 连接即可</p>\n<h1>使用 dyn 返回 trait</h1>\n<p>欲返回非具体的类型（动态大小类型，DST，unsized 在 rust 中表达为 <code>?Sized</code>）<br>需要返回 Box 包装的值，并使用 dyn 语法，表示允许任意实现 trait 的类型<br>当然，返回值（和使用 impl 约束一样）将只能使用 trait 上存在的函数</p>\n<pre><code class=\"language-rust\">enum IntStruct {\n    Int32,\n    Int64,\n}\n\nfn get_int_struct(r#type: IntStruct, value: isize) -&gt; Box&lt;dyn Value&gt; {\n    match r#type {\n        IntStruct::Int32 =&gt; Box::new(Int32(value as i32)),\n        IntStruct::Int64 =&gt; Box::new(Int64(value as i64)),\n    }\n}\n\nfn main() {\n    println!(&quot;{}&quot;, get_int_struct(IntStruct::Int32, 32).value()); // =&gt; 32\n    println!(&quot;{}&quot;, get_int_struct(IntStruct::Int64, 64).value()); // =&gt; 64\n}\n</code></pre>\n<p>显然该 trait 不能是 Sized</p>\n<pre><code class=\"language-rs\">trait Trait {}\nstruct Impl;\nimpl Trait for Impl {}\nlet x: &amp;dyn Trait = &amp;Impl; // OK\n\n\ntrait TraitSized: Sized {}\nstruct ImplSized;\nimpl TraitSized for ImplSized {}\nlet y: &amp;dyn TraitSized = &amp;ImplSized; // Error!\n</code></pre>\n<p>参见：<br><a href=\"https://www.rustwiki.org.cn/zh-CN/book/ch19-04-advanced-types.html#%E5%8A%A8%E6%80%81%E5%A4%A7%E5%B0%8F%E7%B1%BB%E5%9E%8B%E5%92%8C-sized-trait\">动态大小类型和 Sized trait</a><br><a href=\"https://www.rustwiki.org.cn/zh-CN/book/ch17-02-trait-objects.html\">为使用不同类型的值而设计的 trait 对象</a></p>\n<h1>泛型覆盖实现（Generic Blanket Impls）</h1>\n<pre><code class=\"language-rust\">use std::convert::TryInto;\nuse std::fmt::Debug;\nuse std::ops::Rem;\n\ntrait Even {\n    fn is_even(self) -&gt; bool;\n}\n\n// generic blanket impl\nimpl&lt;T&gt; Even for T\nwhere\n    T: Rem&lt;Output = T&gt; + PartialEq&lt;T&gt; + Sized,\n    u8: TryInto&lt;T&gt;,\n    &lt;u8 as TryInto&lt;T&gt;&gt;::Error: Debug,\n{\n    fn is_even(self) -&gt; bool {\n        // these unwraps will never panic\n        self % 2.try_into().unwrap() == 0.try_into().unwrap()\n    }\n}\n\n#[test] // ✅\nfn test_is_even() {\n    assert!(2_i8.is_even());\n    assert!(4_u8.is_even());\n    assert!(6_i16.is_even());\n    // etc\n}\n</code></pre>\n<h1>虚类型参数</h1>\n<p>“虚”指的是该类型<em>仅</em>在编译时静态检查，运行时不存在（零成本抽象）</p>\n<p><code>std::marker</code> 提供两个虚类型</p>\n<pre><code class=\"language-rs\">struct PhantomData&lt;T&gt; where T: ?Sized;\n// size_of::&lt;PhantomData&lt;T&gt;&gt;() == 0\n// align_of::&lt;PhantomData&lt;T&gt;&gt;() == 1\n\npub struct PhantomPinned;\n</code></pre>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/rust-by-example/generics/phantom.html\">https://www.rustwiki.org.cn/zh-CN/rust-by-example/generics/phantom.html</a></p>\n<h1>派生 trait</h1>\n<p>#[derive] 属性 能够让编译器提供某些 trait 的实现（无需手动实现）<br>具体参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/rust-by-example/trait/derive.html\">https://www.rustwiki.org.cn/zh-CN/rust-by-example/trait/derive.html</a></p>\n<h1>标准库 trait</h1>\n<p><a href=\"https://github.com/pretzelhammer/rust-blog/blob/master/posts/tour-of-rusts-standard-library-traits.md\">https://github.com/pretzelhammer/rust-blog/blob/master/posts/tour-of-rusts-standard-library-traits.md</a></p>\n<p><a href=\"https://doc.rust-lang.org/std/\">https://doc.rust-lang.org/std/</a></p>\n<p><a href=\"https://www.rustwiki.org.cn/zh-CN/std/\">https://www.rustwiki.org.cn/zh-CN/std/</a></p>\n<h2>闭包 trait</h2>\n<blockquote>\n<p>定义于 <code>std::ops</code></p>\n</blockquote>\n<p>Fn</p>\n<p>FnMut</p>\n<p>FnOnce</p>\n<h2>类型转换 trait</h2>\n<blockquote>\n<p>定义于 <code>std::str</code></p>\n</blockquote>\n<p>FromStr</p>\n<blockquote>\n<p>定义于 <code>std::convert</code></p>\n</blockquote>\n<p>From &amp; Into</p>\n<p>TryFrom &amp; TryInto</p>\n<p>AsRef &amp; AsMut</p>\n<blockquote>\n<p>定义于 <code>std::borrow</code></p>\n</blockquote>\n<p>Borrow &amp; BorrowMut</p>\n<p>ToOwned</p>\n<hr>\n<p>关于 <code>AsRef</code>/<code>Borrow</code> 和 <code>AsMut</code>/<code>BorrowMut</code> （下以 <code>AsRef</code>/<code>Borrow</code> 举例）</p>\n<p><code>AsRef</code> 和 <code>Borrow</code> 的签名实际上<em>完全相同</em>，因此它们之间主要是语义方面的区别</p>\n<p>为类型 <code>X</code> 实现 <code>Borrow&lt;Borrowed&gt;.borrow()</code> 得到 <code>&amp;Borrowed</code>，<br>要求 <code>X</code> 与 <code>Borrowed</code> 等价，即它们的 <code>Eq</code> <code>Ord</code> 和 <code>Hash</code> （在转换前后）必须相等<br>标准库的表述就是<em>类型的不同表现形式</em>，如 <code>Box&lt;T&gt;</code> 和 <code>T</code>，<code>Rc&lt;T&gt;</code> 和 <code>T</code>，<code>String</code> 和 <code>str</code><br>这些类型提供了对底层数据的引用，即它们能够通过底层数据类型的形式而被借用（如果还允许可变，则还可以实现 <code>BorrowMut&lt;Borrowed&gt;</code>）<br>此 trait 带有默认实现</p>\n<p>为类型 <code>X</code> 实现 <code>AsRef&lt;T&gt;.as_ref()</code> 得到 <code>&amp;T</code>，\n要求 <code>X</code> 与 <code>T</code> 均自反（不要求 <code>X</code> 与 <code>T</code> 相等），即，为 <code>T</code> 实现 <code>AsRef&lt;T&gt;.as_ref()</code> 得到 <code>&amp;T</code><br>不过此 trait 并没有自反的默认实现，此外 <code>AsMut</code> 不依赖 <code>AsRef</code>（而 <code>BorrowMut</code> 依赖 <code>Borrow</code>）<br>常见的例如函数接受 <code>AsRef&lt;Path&gt;</code>，这样更方便调用方使用（相当于一种重载(Overload)机制）</p>\n<p>其次，<code>Borrow</code> 在 <code>std::borrow</code> 模块下，而 <code>AsRef</code> 在 <code>std::convert</code> 模块下<br>我们可以看出 <code>AsRef</code> 具有转换语义，<code>Borrow</code> 则是借用语义</p>\n<hr>\n<p>我们知道 rust 是没有重载 <code>&amp;</code> 运算符的，重载 <code>*</code> 的是 <code>Deref</code> 和 <code>DerefMut</code>（下以 <code>Deref</code> 为例）<br>为 <code>T</code> 实现 <code>Deref&lt;Target = U&gt;</code>，要求 <code>T</code> 与 <code>U</code> 行为相同<br><code>Deref</code> 使用关联类型而不是泛型，这意味着 <code>Deref</code> 只能得到一种类型<br>使得 <code>&amp;T</code> 可隐式转换为 <code>&amp;U</code>, <code>*t == *Deref::deref(&amp;t)</code><br>与前面的 trait 不同的是，<code>Deref</code> 是为智能指针设计的</p>\n<h2>比较 trait</h2>\n<blockquote>\n<p>定义于 <code>std::cmp</code></p>\n</blockquote>\n<p>PartialEq &amp; Eq</p>\n<p>PartialOrd &amp; Ord</p>\n<blockquote>\n<p>定义于 <code>std::hash</code></p>\n</blockquote>\n<p>Hash</p>\n<h2>格式化输出 trait</h2>\n<blockquote>\n<p>定义于 <code>std::fmt</code></p>\n</blockquote>\n<p>Display &amp; Debug</p>\n<blockquote>\n<p>定义于 <code>std::string</code></p>\n</blockquote>\n<p>ToString<br>实现了 Display 则编译器自动实现 ToString<br>偏好实现 Display 而不是实现 ToString</p>\n<h2>IO trait</h2>\n<blockquote>\n<p>定义于 <code>std::io</code></p>\n</blockquote>\n<p>Read &amp; Write &amp; Seek</p>\n<h2>运算符重载 trait</h2>\n<blockquote>\n<p>定义于 <code>std::ops</code></p>\n</blockquote>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/std/ops/index.html\">https://www.rustwiki.org.cn/zh-CN/std/ops/index.html</a></p>\n<h2>std::default::Default</h2>\n<p>表示某个类型的默认值，rust 也提供 <code>#[derive(Default)]</code> 派生宏</p>\n<h2>std::marker::Sized</h2>\n<p>表示在编译时大小已知的类型。</p>\n<p><a href=\"https://stackoverflow.com/questions/30938499/why-is-the-sized-bound-necessary-in-this-trait\">https://stackoverflow.com/questions/30938499/why-is-the-sized-bound-necessary-in-this-trait</a></p>\n<p>未来会有 <a href=\"https://doc.rust-lang.org/std/marker/trait.Unsize.html\"><code>Unsize</code></a> 来表示 <code>?Sized</code></p>\n<h2>std::clone::Clone</h2>\n<p>一般实现为：通过一个引用，克隆出一个实值（有所有权）</p>\n<blockquote>\n<p>More formally: if <code>T: Copy</code>, <code>x: T</code>, and <code>y: &amp;T</code>, then <code>let x = y.clone();</code> is equivalent to <code>let x = *y；</code></p>\n</blockquote>\n<pre><code class=\"language-rust\">pub trait Clone: Sized {\n    // Required method\n    fn clone(&amp;self) -&gt; Self;\n\n    // Provided method\n    fn clone_from(&amp;mut self, source: &amp;Self) { ... }\n}\n</code></pre>\n<p>如果所有字段均为 <code>Clone</code>，则此 trait 可以与 <code>#[derive]</code> 一起使用。</p>\n<h2>std::marker::Copy</h2>\n<p>如果类型实现 Copy，则它具有复制语义。也就是不发生所有权转移。</p>\n<pre><code class=\"language-rust\">#[derive(Copy, Clone)]\n// Clone 是 Copy 的一个 super trait，所以实现了 Copy 的类型也必须实现 Clone\n</code></pre>\n<p>表明实现该 trait 的值足够简单（就像基本类型），以至于可以直接在栈上复制，无需转移所有权（赋值时）\n（我们知道 <code>Clone</code> 则需要显式调用一些方法）</p>\n<p>注意实现了 <code>Drop</code> 就不能实现 <code>Copy</code> 了，因为实现 <code>Drop</code> 就会有额外的释放行为<br>可变引用和一些智能指针是不能 <code>Copy</code> 的，因为这样复制的是指针本身，而不是指针指向的数据<br>（只复制指针肯定是不安全的，此时应当使用 <code>Rc</code>）</p>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/core/marker/trait.Copy.html\">https://www.rustwiki.org.cn/zh-CN/core/marker/trait.Copy.html</a></p>\n<h2>std::ops::Drop</h2>\n<p>相当于析构函数，生命期结束时自动调用（先进后出），只能通过 <code>std::mem::drop</code> 间接调用</p>\n<p>另见：<a href=\"/posts/rust-smart-pointer/\">rust 智能指针</a></p>\n<h2>std::marker::Sync</h2>\n<p>（编译器自动实现）表明类型可以在线程之间安全地共享引用</p>\n<ul>\n<li><code>&amp;T</code> 是 <code>Send</code> 当且仅当 <code>T</code> 是 <code>Sync</code></li>\n<li><code>&amp;mut T</code> 是 <code>Send</code> 当且仅当 <code>T</code> 是 <code>Send</code></li>\n<li><code>&amp;T</code> 和 <code>&amp;mut T</code> 是 <code>Sync</code> 当且仅当 <code>T</code> 是 <code>Sync</code></li>\n</ul>\n<h1>std::marker::Send</h1>\n<p>（编译器自动实现）表明类型可以在线程边界间传递</p>\n<p>例如，<code>Rc</code> 不是 <code>Send</code>，而 <code>Arc</code> 是 <code>Send</code></p>\n<h1>std::pin::Pin</h1>\n<pre><code class=\"language-rs\">#[repr(transparent)]\npub struct Pin&lt;P&gt; { /* private fields */ }\n</code></pre>\n<p>表明数据在内存中的位置是固定的（不会移动）<br>即指针（是地址）指向的数据不会改变（但注意Rust编译器认为任何值都是可移动的）</p>\n<h1>std::marker::Unpin</h1>\n<p>几乎所有类型都会自动实现这个 trait</p>\n<p>需要持有 <code>PhantomPinned</code> 表明不是 <code>Unpin</code>，即 <code>impl !Unpin for PhantomPinned</code></p>\n<h2>std::iter::Iterator</h2>\n<pre><code class=\"language-rs\">trait Iterator {\n    type Item;\n    fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt;;\n}\n</code></pre>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/std/iter/\">https://www.rustwiki.org.cn/zh-CN/std/iter/</a></p>\n","tags":["rust"]},{"id":"rust-smart-pointers","url":"https://yieldray.fun/posts/rust-smart-pointers","title":"rust智能指针","date_published":"2022-07-30T13:29:37.000Z","date_modified":"2022-07-30T13:29:37.000Z","content_text":"<p><code>Vec&lt;T&gt;</code> 以及 <code>String</code> 也是智能指针</p>\n<h1><code>Box&lt;T&gt;</code> 在堆上分配值</h1>\n<p><code>Box&lt;T&gt;</code> 的表现与引用（指针）相仿</p>\n<p>指针本身占用的内存大小是确定的，其大小并不会根据其指向的数据量而改变</p>\n<pre><code class=\"language-rust\">fn print_type_of&lt;T&gt;(_: T) {\n    println!(&quot;{}&quot;, std::any::type_name::&lt;T&gt;())\n}\n\nfn main() {\n    let a = &amp;1;\n    let b = Box::new(2);\n    let c = Box::new(3);\n    print_type_of(a); // &amp;i32\n    print_type_of(*a); // i32\n    print_type_of(b); // alloc::boxed::Box&lt;i32&gt;\n    print_type_of(*c); // i32\n}\n</code></pre>\n<p>其实现原理主要是重载了解引用运算符 <code>*</code> （<code>std::ops::Deref</code> trait）</p>\n<pre><code class=\"language-rust\">pub trait Deref {\n    type Target: ?Sized;\n    fn deref(&amp;self) -&gt; &amp;Self::Target;\n}\n</code></pre>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/std/ops/trait.Deref.html\">https://www.rustwiki.org.cn/zh-CN/std/ops/trait.Deref.html</a></p>\n<h1>析构函数 <code>std::ops::Drop</code> trait</h1>\n<p>实现了 Drop 的类型将在被销毁时（主要为变量离开作用域）调用 drop 方法</p>\n<pre><code class=\"language-rust\">pub trait Drop {\n    fn drop(&amp;mut self);\n}\n</code></pre>\n<p>对于离开作用域的情况下，销毁遵循先进后出（栈），即先声明的变量后销毁</p>\n<p>编译器将阻止显式调用此 drop 方法，而应通过 <code>std::mem::drop</code> 函数进行间接调用</p>\n<pre><code class=\"language-rust\">pub fn drop&lt;T&gt;(_x: T)\n</code></pre>\n<h1><code>Rc&lt;T&gt;</code> 引用计数类型（reference counting），其数据可以有多个所有者</h1>\n<pre><code class=\"language-rust\">fn print_type_of&lt;T&gt;(_: T) {\n    println!(&quot;{}&quot;, std::any::type_name::&lt;T&gt;())\n}\nuse std::rc::Rc;\nfn main() {\n    let x = ();\n    let a = Rc::new(x);\n    println!(&quot;count={}&quot;, Rc::strong_count(&amp;a)); // count=1\n    let b = Rc::clone(&amp;a);\n    println!(&quot;count={}&quot;, Rc::strong_count(&amp;a)); // count=2\n    let c = Rc::clone(&amp;a);\n    println!(&quot;count={}&quot;, Rc::strong_count(&amp;a)); // count=3\n\n    print_type_of(a); // alloc::rc::Rc&lt;()&gt;\n    print_type_of(*b); // ()\n    print_type_of(&amp;c); // &amp;alloc::rc::Rc&lt;()&gt;\n}\n</code></pre>\n<p>strong_count 方法返回指向此分配的强 (Rc) 指针的数量。</p>\n<pre><code class=\"language-rust\">fn main() {\n    let x = ();\n    let a = Rc::new(x);\n    println!(&quot;count={}&quot;, Rc::strong_count(&amp;a)); // count=1\n    {\n        let b = Rc::clone(&amp;a);\n        println!(&quot;count={}&quot;, Rc::strong_count(&amp;a)); // count=2\n    }// 在这里，指针 b 失效\n    let c = Rc::clone(&amp;a);\n    println!(&quot;count={}&quot;, Rc::strong_count(&amp;a)); // count=2\n}\n</code></pre>\n<h1><code>Ref&lt;T&gt;</code> 和 <code>RefMut&lt;T&gt;</code>智能指针，通过 <code>RefCell&lt;T&gt;</code> 实现内部可变性</h1>\n<p>RefCell 允许在在运行时借用，无论数据本身是否可以被借用（或者说，即使本身无法被借用）<br>运行时意味着如果出错将发生 panic（编译时的错误导致编译不通过）</p>\n<p><code>Ref&lt;T&gt;</code> 和 <code>RefMut&lt;T&gt;</code> 是智能指针，实现了 <code>Deref</code> trait</p>\n<p>不过，RefCell 也同样遵循借用规则，即相同作用域中要么只能有一个可变引用，要么只能有多个不可变引用</p>\n<pre><code class=\"language-rust\">use std::cell::*;\n#[derive(Debug)]\nstruct Int(i32);\nfn main() {\n    let x = Int(0);\n    let a: RefCell&lt;Int&gt; = RefCell::new(x);\n    println!(&quot;{:?}&quot;, a); // RefCell { value: Int(0) }\n    {\n        let b: Ref&lt;Int&gt; = a.borrow(); // 不可变地借用\n        println!(&quot;{:?}&quot;, b); // Int(0)\n    }\n    {\n        let mut c: RefMut&lt;Int&gt; = a.borrow_mut(); // 可变地借用，这里因为手动修改所以还添加了 mut\n        println!(&quot;{:?}&quot;, c); // Int(0)\n        c.0 = 1; // 本来是无法修改的，通过 RefCell 修改\n        println!(&quot;{:?}&quot;, c); // Int(1)\n    }\n}\n</code></pre>\n<h1>原子引用计数 <code>Arc&lt;T&gt;</code></h1>\n<p><code>Rc&lt;T&gt;</code> 非线程安全，<code>Arc&lt;T&gt;</code> 是 <code>Rc&lt;T&gt;</code> 的线程安全版本，适合在多线程环境下使用，API 一致。</p>\n<h1>互斥器 <code>Mutex&lt;T&gt;</code></h1>\n<p>在 rust 中，加锁之后无需手动释放，因为当锁离开作用域时就会自动释放。</p>\n<pre><code class=\"language-rust\">use std::sync::Mutex;\n\nfn main() {\n    let m = Mutex::new(5); // 互斥锁是一个智能指针\n\n    {\n        let mut num = m.lock().unwrap(); // 获取锁\n        *num = 6;\n    }// 释放锁\n\n    println!(&quot;m = {:?}&quot;, m);\n}\n</code></pre>\n<p>在多个线程中使用同一个锁时，还需要使用 <code>Arc&lt;T&gt;</code> 以便在不同的线程中都能取得同一个锁</p>\n<pre><code class=\"language-rust\">use std::sync::{Mutex, Arc};\nuse std::thread;\n\nfn main() {\n    let counter = Arc::new(Mutex::new(0)); // 通过 Arc 共享该锁\n    let mut handles = vec![];\n\n    for _ in 0..10 {\n        let counter = Arc::clone(&amp;counter); // 每个线程获取 Arc 的克隆\n        let handle = thread::spawn(move || {\n            let mut num = counter.lock().unwrap();\n\n            *num += 1; // Mutex 和 Arc 都是智能指针\n        });\n        handles.push(handle);\n    }\n\n    for handle in handles {\n        handle.join().unwrap();\n    }\n\n    println!(&quot;Result: {}&quot;, *counter.lock().unwrap());\n}\n</code></pre>\n<h1><code>Weak&lt;T&gt;</code> 避免引用循环</h1>\n<p>引用循环，参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/book/ch15-06-reference-cycles.html\">https://www.rustwiki.org.cn/zh-CN/book/ch15-06-reference-cycles.html</a></p>\n<h1><code>Cell&lt;T&gt;</code> 实现内部可变性</h1>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/std/cell/struct.Cell.html\">https://www.rustwiki.org.cn/zh-CN/std/cell/struct.Cell.html</a></p>\n<h1><code>UnsafeCell&lt;T&gt;</code></h1>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/std/cell/struct.UnsafeCell.html\">https://www.rustwiki.org.cn/zh-CN/std/cell/struct.UnsafeCell.html</a></p>\n<h1><code>Cow</code></h1>\n<p>参见：<a href=\"https://www.rustwiki.org.cn/zh-CN/alloc/borrow/enum.Cow.html\">https://www.rustwiki.org.cn/zh-CN/alloc/borrow/enum.Cow.html</a></p>\n<pre><code class=\"language-rust\">pub enum Cow&lt;&#39;a, B: ?Sized + &#39;a&gt;\nwhere\n    B: ToOwned,\n {\n    Borrowed(&amp;&#39;a B),\n    Owned(&lt;B as ToOwned&gt;::Owned),\n}\n</code></pre>\n","tags":["rust"]},{"id":"rust-generic-types-and-associated-types","url":"https://yieldray.fun/posts/rust-generic-types-and-associated-types","title":"rust泛型类型与关联类型","date_published":"2022-07-29T20:20:20.000Z","date_modified":"2022-07-29T20:20:20.000Z","content_text":"<p>此篇是关于泛型类型和关联类型实现 trait 上面的区别</p>\n<h1>关联类型</h1>\n<p>关联类型允许在实现 trait 时再指定具体类型，而不是在 trait 定义时决定。</p>\n<blockquote>\n<p>此处定义的 Add 非 std::ops::Add，后者是实现运算符重载的 trait</p>\n</blockquote>\n<pre><code class=\"language-rust\">trait Add {\n    type OtherOne; // 这个类型需要实现者指定\n    fn add(&amp;self, other: Self::OtherOne) -&gt; Self; // 使用 Self::OtherOne 而不是 OtherOne\n}\n\n#[derive(Debug)]\nstruct Int(i32);\n\nimpl Add for Int {\n    type OtherOne = i32; // 实现者指定具体类型\n    fn add(&amp;self, other: Self::OtherOne) -&gt; Self {\n        Int(self.0 + other)\n    }\n}\n\nfn main() {\n    let i = Int(1);\n    let sum = i.add(2);\n    println!(&quot;{:?}&quot;, sum); // -&gt; Int(3)\n}\n</code></pre>\n<p>关联类型一旦实现，该 trait 中的函数签名就已经确定，无法再实现关于令一个类型的同 trait 函数<br>在需要实现同名 trait 函数并允许使用多个不同类型时，则采用泛型类型</p>\n<blockquote>\n<p>关联类型只能实现一次，泛型类型可以实现多次</p>\n</blockquote>\n<h1>在尖括号进行关联</h1>\n<p>可以在尖括号内部通过：<code>type名 = 实际类型</code> 直接进行类型关联<br>不过，虽然使用了尖括号语法，但这里并不是泛型，而是关联类型<br>例如：</p>\n<pre><code class=\"language-rust\">fn example1(i: impl Add&lt;OtherOne = i32&gt; + std::fmt::Debug, j: i32) {\n    let out = i.add(j);\n    println!(&quot;{:?}&quot;, out);\n}\n\nfn example2&lt;T&gt;(i: T, j: i32)\nwhere\n    T: Add&lt;OtherOne = i32&gt; + std::fmt::Debug,\n{\n    let out = i.add(j);\n    println!(&quot;{:?}&quot;, out);\n}\n\nfn main() {\n    example1(Int(1), 2); // -&gt; Int(3)\n    example2(Int(1), 2); // -&gt; Int(3)\n}\n</code></pre>\n<h1>泛型类型</h1>\n<pre><code class=\"language-rust\">trait Add&lt;T&gt; {\n    fn add(&amp;self, other: T) -&gt; Self;\n}\n\n#[derive(Debug)]\nstruct Int(i32);\n\nimpl Add&lt;&amp;i32&gt; for Int {\n    // 在这里，实现了泛型的某一具体类型，但也可以同时实现多个类型\n    fn add(&amp;self, other: &amp;i32) -&gt; Self {\n        Int(self.0 + *other)\n    }\n}\n\nfn main() {\n    let i = Int(1);\n    let j = 2;\n    let sum = i.add(&amp;j);\n    println!(&quot;{:?}+{}={:?}&quot;, i, j, sum); // -&gt; Int(1)+2=Int(3)\n}\n</code></pre>\n<h1>默认类型参数</h1>\n<p>目前，rust 在除泛型函数之外，可以给泛型参数指定默认泛型类型<br>例如：</p>\n<pre><code class=\"language-rust\">#[derive(Debug)]\nstruct Int(i32);\n\ntrait Same&lt;T = Int&gt; {\n    fn same(x: T) -&gt; T {\n        x\n    }\n}\n\nimpl Same for Int {}\n\nfn main() {\n    let x = Int(8);\n    println!(&quot;{:?}&quot;, Int::same(x)); // =&gt; Int(8)\n}\n</code></pre>\n<h1>结合使用</h1>\n<p>实际上，二者是不冲突的。<br>例如，标准库提供的 <code>std::ops::Add</code> trait</p>\n<pre><code class=\"language-rust\">// RHS = right hand side\ntrait Add&lt;RHS=Self&gt; {\n    type Output;\n    fn add(self, rhs: RHS) -&gt; Self::Output;\n}\n</code></pre>\n","tags":["rust"]},{"id":"java17-switch","url":"https://yieldray.fun/posts/java17-switch","title":"java17switch","date_published":"2022-07-28T12:00:00.000Z","date_modified":"2022-07-28T12:00:00.000Z","content_text":"<p>支持版本大于 <code>JDK 11</code>，一律使用 <code>JDK 17</code></p>\n<h1>switch 返回值 (JDK 12)</h1>\n<pre><code class=\"language-java\">// 注意，返回值类型可以不同，但不建议\nvar ret = switch (&quot;test&quot;) {\n    case &quot;1&quot; -&gt; 1; // 无需 break\n    case &quot;2&quot; -&gt; 2;\n    case &quot;3&quot; -&gt; {\n        System.out.println(&quot;is three!&quot;);\n        yield 3;\n        // 多个语句时需要花括号，使用 yield 返回值\n    }\n    default -&gt; 0;\n};\n</code></pre>\n<p>匹配多个值<br>注意，在需要返回值时，必须覆盖所有分支，因此 default 是必要的<br>不需要返回值时则不要求 default 分支</p>\n<pre><code class=\"language-java\">switch (&quot;Result&quot;) {\n    case &quot;T&quot;, &quot;OK&quot; -&gt; System.out.println(&quot;OK&quot;);\n    case &quot;E&quot;, &quot;Err&quot; -&gt; System.out.println(&quot;Err&quot;);\n}\n</code></pre>\n<h1>模式匹配 (JDK 17, Preview)</h1>\n<pre><code class=\"language-java\">static String patternSwitch(Object o) {\n    return switch (o) {\n        case Integer i -&gt; String.format(&quot;int %d&quot;, i);\n        // 相当于：\n        // if(o instanceof Integer i) return String.format(&quot;int %d&quot;, i);\n        case Long l -&gt; String.format(&quot;long %d&quot;, l);\n        case Double d -&gt; String.format(&quot;double %f&quot;, d);\n        case String s -&gt; String.format(&quot;String %s&quot;, s);\n        case null -&gt; &quot;null&quot;;\n        // 支持 null\n        default -&gt; o.toString();\n    };\n}\n</code></pre>\n<h1>匹配 enum</h1>\n<p>匹配 enum 时，如已覆盖所有情况，则无需 default 分支</p>\n<pre><code class=\"language-java\">enum Option {\n    Some, None\n}\n\nstatic String unwrap(Option opt) {\n    return switch (opt) {\n        case Some -&gt; &quot;something&quot;;\n        case None -&gt; &quot;nothing&quot;;\n    };\n}\n</code></pre>\n<p>sealed Classes 同理</p>\n<pre><code class=\"language-java\">sealed interface Result&lt;T, E&gt; permits Ok, Err {}\n\nstatic final class Ok&lt;T&gt; implements Result {}\n\nstatic final class Err&lt;E&gt; implements Result {}\n\nstatic &lt;T, E&gt; String unwrap(Result rst) {\n    return switch (rst) {\n        case Ok ok -&gt; &quot;ok&quot;;\n        case Err err -&gt; &quot;err&quot;;\n    };\n}\n</code></pre>\n","tags":["java"]},{"id":"rust-lifetimes","url":"https://yieldray.fun/posts/rust-lifetimes","title":"rust生命期","date_published":"2022-07-27T19:39:42.000Z","date_modified":"2022-07-27T19:39:42.000Z","content_text":"<blockquote>\n<p>此处的生命期指 lifetimes 而非 lifecycle</p>\n</blockquote>\n<h1>rust 生命期</h1>\n<p>生命期是一类允许我们向编译器提供引用如何相互关联的泛型。</p>\n<p>Rust 的生命期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。</p>\n<p>也就是说，生命期是编写者提供给编译器用于检查的，用于确保引用有效（不会构成悬垂引用）。</p>\n<p>Rust 中的<strong>每一个引用</strong>都有其<em>生命期（lifetime）</em>，也就是引用保持有效的作用域。</p>\n<p>大部分时候生命期是隐含并可以推断的，正如大部分时候类型也是可以推断的一样。</p>\n<p>（函数和结构体往往需要额外的生命期标注，因为难以静态分析）</p>\n<p>类似于当因为有多种可能类型的时候必须注明类型，也会出现引用的生命期以一些不同方式相关联的情况，</p>\n<p>所以 Rust 需要我们使用泛型生命期参数来注明他们的关系，这样就能确保运行时实际使用的引用绝对是有效的。</p>\n<h1>生命期标注语法</h1>\n<pre><code class=\"language-rust\">&amp;i32        // 引用\n&amp;&#39;a i32     // 带有显式生命期的引用\n&amp;&#39;a mut i32 // 带有显式生命期的可变引用\n\nfn choose_first&lt;&#39;a: &#39;b, &#39;b&gt;(first: &amp;&#39;a i32, _: &amp;&#39;b i32) -&gt; &amp;&#39;b i32 {\n    first\n}\n// `&lt;&#39;a: &#39;b, &#39;b&gt;` 读作生命期 `&#39;a` 至少和 `&#39;b` 一样长。\n</code></pre>\n<h1>函数的生命期</h1>\n<p>在 rust 中，普通函数的函数体都是一块作用域。</p>\n<p>rust 中的借用是指对一块内存空间（或者说，对另一个变量）的引用。<br>rust 有一条借用规则是借用方的生命期不能比出借方的生命期还要长。</p>\n<p>对于一个参数和返回值都包含引用的函数而言，函数的参数是出借方，函数返回值所绑定到的那个变量就是借用方。</p>\n<p>所以，这种函数也需要满足借用规则（借用方的生命期不能比出借方的生命期还要长）。<br>那么就需要对函数返回值的生命期进行标注，告知编译器函数返回值的生命期信息。</p>\n<p>一个典型的错误示例如下：</p>\n<pre><code class=\"language-rust\">fn max(a: &amp;i32, b: &amp;i32) -&gt; &amp;i32 {\n    if a &gt; b {\n        a\n    } else {\n        b\n    }\n}\n\nfn main() {\n    let x = 1;\n    let y = 0;\n    let big = max(&amp;x, &amp;y);\n    println!(&quot;{}&quot;, big);\n}\n</code></pre>\n<p>编译器给出如下错误：</p>\n<pre><code>   Compiling playground v0.0.1 (/playground)\nerror[E0106]: missing lifetime specifier\n --&gt; src/main.rs:1:29\n  |\n1 | fn max(a: &amp;i32, b: &amp;i32) -&gt; &amp;i32 {\n  |           ----     ----     ^ expected named lifetime parameter\n  |\n  = help: this function&#39;s return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`\nhelp: consider introducing a named lifetime parameter\n  |\n1 | fn max&lt;&#39;a&gt;(a: &amp;&#39;a i32, b: &amp;&#39;a i32) -&gt; &amp;&#39;a i32 {\n  |       ++++     ++          ++          ++\n\nFor more information about this error, try `rustc --explain E0106`.\nerror: could not compile `playground` due to previous error\n</code></pre>\n<p>根据提示做如下修改：</p>\n<pre><code class=\"language-rust\">fn max&lt;&#39;a&gt;(a: &amp;&#39;a i32, b: &amp;&#39;a i32) -&gt; &amp;&#39;a i32 {\n    if a &gt; b {\n        a\n    } else {\n        b\n    }\n}\n</code></pre>\n<p>这个生命期表示获取了返回值（引用）的变量，其生命期应当与入参 a 和 b 相同</p>\n<p>因此，如下代码不被允许编译：</p>\n<pre><code class=\"language-rust\">fn main() {\n    let big: &amp;i32;\n    let x = 1;\n    {\n        let y = 0;\n        big = max(&amp;x, &amp;y);\n    }// 在这里，入参 y 的生命期已结束\n    println!(&quot;{}&quot;, big); // 然而，在这里，另一个入参 x 的生命期存在，并且返回值的生命期也存在\n    // 但函数的生命期标注表示，两个入参的生命期一致，并且与返回值的生命期一致\n    // 这里的代码很明显不符合函数生命期标注，而可能获取无效引用。\n    // 因此，编译器拒绝编译（即使通过观察作用域，代码可以正常运行，但会造成潜在的错误）\n}\n</code></pre>\n<p>编译器给出如下错误：</p>\n<pre><code>   Compiling playground v0.0.1 (/playground)\nerror[E0597]: `y` does not live long enough\n  --&gt; src/main.rs:14:23\n   |\n14 |         big = max(&amp;x, &amp;y);\n   |                       ^^ borrowed value does not live long enough\n15 |     }\n   |     - `y` dropped here while still borrowed\n16 |     println!(&quot;{}&quot;, big);\n   |                    --- borrow later used here\n\nFor more information about this error, try `rustc --explain E0597`.\nerror: could not compile `playground` due to previous error\n</code></pre>\n<p>以下代码有效：</p>\n<pre><code class=\"language-rust\">fn first&lt;&#39;a, &#39;b&gt;(a: &amp;&#39;a i32, _b: &amp;&#39;b i32) -&gt; &amp;&#39;a i32 {\n    a\n}\n\nfn main() {\n    let big: &amp;i32;\n    let x = 1;\n    {\n        let y = 0;\n        big = first(&amp;x, &amp;y);\n    }\n    println!(&quot;{}&quot;, big);\n}\n</code></pre>\n<h1>结构体的生命期</h1>\n<p>对于一个含有引用成员的结构体，需要满足结构体的生命期不能超过其包含的任意引用成员的生命期。（同理，否则将结构体本身将构成悬垂引用）</p>\n<p>编译器不允许如下结构体声明，因为需要提供生命期标注：</p>\n<pre><code class=\"language-rust\">struct Foo {\n  v: &amp;i32\n}\n</code></pre>\n<p>修改为：</p>\n<pre><code class=\"language-rust\">struct Foo&lt;&#39;a&gt; {\n  v: &amp;&#39;a i32\n}\n</code></pre>\n<h1>生命期省略</h1>\n<p>函数或方法的参数的生命期被称为<strong>输入生命期（input lifetimes）</strong>，而返回值的生命期被称为<strong>输出生命期（output lifetimes）</strong>。</p>\n<p>对于入参和返回值都是引用的函数，无需声明生命期，编译器将推断输入生命期与输出生命期相同</p>\n<p>对于对象方法（函数的其中一个参数是 &amp;self 或 &amp;mut self），编译器将推断输出生命期参数等同于 self 的生命期</p>\n<h1>静态生命期 <code>&#39;static</code></h1>\n<p>静态生命期表示引用在整个程序期间都有效。</p>\n<p>所有的字符串字面量都拥有 <code>&#39;static</code> 生命期，即：</p>\n<pre><code class=\"language-rust\">let s: &amp;&#39;static str = &quot;I have a static lifetime.&quot;;\n// 完全相等，无需标注\nlet s: &amp;str = &quot;I have a static lifetime.&quot;;\n</code></pre>\n<p>可以通过 static 赋值来声明一个在整个程序期间都有效的变量</p>\n<p>那么，这个变量的引用将有 <code>&#39;static</code> 生命期</p>\n<p>因此，以下代码有效：</p>\n<pre><code class=\"language-rust\">fn main() {\n    let big: &amp;i32;\n    let x = 1;\n    {\n        #[allow(non_upper_case_globals)] // static 变量名应当使用大写，这里出于演示而使用小写\n        static y: i32 = 0; // 需要声明类型\n        big = max(&amp;x, &amp;y);\n    } // 在这里，入参 y 的生命期一直有效\n    println!(&quot;{}&quot;, big); // 函数调用有效\n}\n</code></pre>\n","tags":["rust"]},{"id":"java-json","url":"https://yieldray.fun/posts/java-json","title":"java使用json","date_published":"2022-07-22T13:33:33.000Z","date_modified":"2022-07-22T13:33:33.000Z","content_text":"<h1>JSON-java 库</h1>\n<p><a href=\"https://mvnrepository.com/artifact/org.json/json\">https://mvnrepository.com/artifact/org.json/json</a><br><a href=\"https://github.com/stleary/JSON-java\">https://github.com/stleary/JSON-java</a></p>\n<p>一个轻量级的 json 库</p>\n<h2>安装</h2>\n<pre><code class=\"language-xml\">&lt;dependency&gt;\n  &lt;groupId&gt;org.json&lt;/groupId&gt;\n  &lt;artifactId&gt;json&lt;/artifactId&gt;\n  &lt;version&gt;20220320&lt;/version&gt;\n  &lt;type&gt;bundle&lt;/type&gt;\n&lt;/dependency&gt;\n</code></pre>\n<h2>使用</h2>\n<pre><code class=\"language-java\">import org.json.*;\npublic class Main {\n    public static void main(String[] args) {\n        JSONObject jsonObj = new JSONObject();\n        jsonObj.put(&quot;name&quot;, &quot;练习生&quot;);\n        jsonObj.put(&quot;time&quot;, 0);\n        jsonObj.put(&quot;time&quot;, 2.5); // 后面的覆盖前面\n        jsonObj.put(&quot;hobbies&quot;, new String[]{&quot;唱&quot;, &quot;跳&quot;, &quot;RAP&quot;, &quot;篮球&quot;});\n        jsonObj.append(&quot;bool&quot;, true); // 添加为数组元素\n\n        JSONArray jsonArr = jsonObj.getJSONArray(&quot;bool&quot;);\n        jsonArr.put(false); // 数组追加元素\n\n        System.out.println(jsonObj.toString()); // =&gt; {...json}\n        System.out.println(jsonArr.getBoolean(0)); // =&gt; true\n        System.out.println(jsonObj.get(&quot;bool&quot;)); // get 方法返回object\n        if (!jsonObj.isNull(&quot;name&quot;)) System.out.println(jsonObj.getString(&quot;name&quot;));\n        if (!jsonObj.isNull(&quot;time&quot;)) System.out.println(jsonObj.getDouble(&quot;time&quot;));\n    }\n}\n</code></pre>\n<pre><code>{&quot;bool&quot;:[true,false],&quot;hobbies&quot;:[&quot;唱&quot;,&quot;跳&quot;,&quot;RAP&quot;,&quot;篮球&quot;],&quot;name&quot;:&quot;练习生&quot;,&quot;time&quot;:2.5}\ntrue\n[true,false]\n练习生\n2.5\n</code></pre>\n<h2>API 演示</h2>\n<pre><code class=\"language-java\">import org.json.*;\nimport java.util.*;\npublic class Main {\n    public static void main(String[] args) {\n        /* 建造者模式 */\n        JSONStringer stringer = new JSONStringer();\n        stringer.object().key(&quot;k&quot;).value(&quot;v&quot;).key(&quot;number&quot;).value(1).endObject();\n        System.out.println(stringer); // =&gt; {&quot;k&quot;:&quot;v&quot;,&quot;number&quot;:1}\n\n        /* 编码数组，集合 */\n        var aa = new JSONArray(new int[]{1, 2, 3});\n        var ll = new JSONArray(List.of(4, 5, 6));\n        var ss = new JSONArray(Set.of(7, 8, 9)); // 输出顺序随机\n        System.out.println(aa); // =&gt; [1,2,3]\n        System.out.println(ll); // =&gt; [4,5,6]\n        System.out.println(ss); // =&gt; [7,9,8]\n\n        /* 编码哈希表 */\n        var mm = new JSONObject(Map.of(&quot;k1&quot;, &quot;v1&quot;, &quot;k2&quot;, &quot;v2&quot;));\n        System.out.println(mm); // =&gt; {&quot;k1&quot;:&quot;v1&quot;,&quot;k2&quot;:&quot;v2&quot;}\n\n        /* 解码json */\n        JSONTokener token = new JSONTokener(&quot;[{\\&quot;key1\\&quot;:\\&quot;value1\\&quot;},{\\&quot;key2\\&quot;:\\&quot;value2\\&quot;}]&quot;);\n        JSONArray ja = new JSONArray(token);\n        System.out.println(ja.get(0)); // =&gt; {&quot;key1&quot;:&quot;value1&quot;}\n    }\n}\n</code></pre>\n<h1>Gson</h1>\n<h2>安装</h2>\n<pre><code class=\"language-xml\">&lt;dependency&gt;\n  &lt;groupId&gt;com.google.code.gson&lt;/groupId&gt;\n  &lt;artifactId&gt;gson&lt;/artifactId&gt;\n  &lt;version&gt;2.9.0&lt;/version&gt;\n&lt;/dependency&gt;\n</code></pre>\n<h2>使用</h2>\n<pre><code class=\"language-java\">import com.google.gson.*;\nimport com.google.gson.reflect.*;\n\nimport java.util.*;\n\npublic class Main {\n    public static void main(String[] args) throws Throwable {\n        GsonBuilder builder = new GsonBuilder();\n        builder.setPrettyPrinting();\n        Gson gson = builder.create();\n        // or simply:\n        // Gson gson = new Gson();\n\n        // 反序列化为JavaBean\n        Student student = gson.fromJson(&quot;{\\&quot;name\\&quot;:\\&quot;Max\\&quot;, \\&quot;age\\&quot;:24}&quot;, Student.class);\n        System.out.println(student);\n\n        // 反序列化数组\n        String[] sa = gson.fromJson(&quot;[\\&quot;a\\&quot;,\\&quot;b\\&quot;,\\&quot;c\\&quot;]&quot;, String[].class);\n        System.out.println(sa);\n\n        // 反序列化其它\n        List&lt;String&gt; sl = gson.fromJson(&quot;[\\&quot;a\\&quot;,\\&quot;b\\&quot;,\\&quot;c\\&quot;]&quot;, new TypeToken&lt;List&lt;String&gt;&gt;() {\n        }.getType());\n        System.out.println(sl);\n\n        // 从Reader中获取\n        // Reader r = new BufferedReader(new FileReader(&quot;student.json&quot;));\n        // Student stu = gson.fromJson(r, Student.class);\n\n\n        // 序列化为JSON\n        String json = gson.toJson(student);\n        System.out.println(json);\n\n        // 直接编码JSON\n        System.out.println(gson.toJson(new int[]{1, 2, 3}));\n        System.out.println(gson.toJson(List.of(4, 5, 6)));\n        System.out.println(gson.toJson(&quot;json-string&quot;));\n        System.out.println(gson.toJson(2022));\n        System.out.println(gson.toJson(Map.of(&quot;k1&quot;, &quot;v1&quot;, &quot;k2&quot;, &quot;v2&quot;)));\n    }\n}\n\nclass Student {\n    private String name;\n    private int age;\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public int getAge() {\n        return age;\n    }\n\n    public void setAge(int age) {\n        this.age = age;\n    }\n\n    @Override\n    public String toString() {\n        return &quot;Student{&quot; +\n                &quot;name=&#39;&quot; + name + &#39;\\&#39;&#39; +\n                &quot;, age=&quot; + age +\n                &#39;}&#39;;\n    }\n}\n</code></pre>\n<p>输出</p>\n<pre><code>Student{name=&#39;Max&#39;, age=24}\n[Ljava.lang.String;@4f47d241\n[a, b, c]\n{\n  &quot;name&quot;: &quot;Max&quot;,\n  &quot;age&quot;: 24\n}\n[\n  1,\n  2,\n  3\n]\n[\n  4,\n  5,\n  6\n]\n&quot;json-string&quot;\n2022\n{\n  &quot;k1&quot;: &quot;v1&quot;,\n  &quot;k2&quot;: &quot;v2&quot;\n}\n</code></pre>\n<h2>注解演示</h2>\n<pre><code class=\"language-java\">import com.google.gson.*;\nimport com.google.gson.annotations.*;\n\npublic class Main {\n    public static void main(String[] args) throws Throwable {\n        GsonBuilder builder = new GsonBuilder();\n        builder.excludeFieldsWithoutExposeAnnotation(); // 排除没有Expose注解的字段\n        Gson gson = builder.create();\n\n        Student student = new Student();\n        student.setName(&quot;Jvav&quot;);\n        student.setAge(20);\n        student.setId(202212345678L);\n        System.out.println(gson.toJson(student)); // =&gt; {&quot;student_name&quot;:&quot;Jvav&quot;}\n    }\n}\n\nclass Student {\n    // 序列化时，更名为student_name\n    // 反序列化时，从student_name获取，或从alternate指定的数组中给定的键名获取\n    @SerializedName(value = &quot;student_name&quot;, alternate = {&quot;NAME&quot;, &quot;name&quot;})\n    // 参与序列化，也参与反序列化\n    @Expose(serialize = true, deserialize = true)\n    private String name;\n\n    // 设置了 excludeFieldsWithoutExposeAnnotation，但没有 Expose 注解，忽略此字段\n    private int age;\n\n    // 使用 transient 关键字，无需配置即可指定忽略此字段\n    private transient long id;\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public int getAge() {\n        return age;\n    }\n\n    public void setAge(int age) {\n        this.age = age;\n    }\n\n    public long getId() {\n        return id;\n    }\n\n    public void setId(long id) {\n        this.id = id;\n    }\n}\n</code></pre>\n","tags":["java"]},{"id":"java-web-intro","url":"https://yieldray.fun/posts/java-web-intro","title":"java Web开发入门","date_published":"2022-07-20T22:00:00.000Z","date_modified":"2022-07-20T22:00:00.000Z","content_text":"<h1>Servlet 入门</h1>\n<p>Servlet API 由 javaEE 提供</p>\n<p><a href=\"https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServlet.html\">https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServlet.html</a></p>\n<p>servlet 应用运行在容器下，开发时，需要指定其作为 provided 依赖</p>\n<pre><code class=\"language-xml\"> &lt;!-- version 的同级，指定打包类型为 war 而非 jar。例如： --&gt;\n&lt;groupId&gt;com.mycompany.app&lt;/groupId&gt;\n&lt;artifactId&gt;my-app&lt;/artifactId&gt;\n&lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;\n&lt;packaging&gt;war&lt;/packaging&gt;\n\n\n&lt;dependencies&gt;\n    &lt;dependency&gt;\n        &lt;groupId&gt;javax.servlet&lt;/groupId&gt;\n        &lt;artifactId&gt;javax.servlet-api&lt;/artifactId&gt;\n        &lt;version&gt;4.0.1&lt;/version&gt;\n        &lt;scope&gt;provided&lt;/scope&gt;\n    &lt;/dependency&gt;\n&lt;/dependencies&gt;\n</code></pre>\n<p>仓库地址：<a href=\"https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api\">https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api</a></p>\n<p>此外，还需要安装 maven-war-plugin 插件，用于打包 war 文件</p>\n<pre><code class=\"language-xml\">&lt;build&gt;\n    &lt;plugins&gt;\n        &lt;plugin&gt;\n            &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;\n            &lt;artifactId&gt;maven-war-plugin&lt;/artifactId&gt;\n            &lt;version&gt;3.2.2&lt;/version&gt;\n        &lt;/plugin&gt;\n    &lt;/plugins&gt;\n&lt;/build&gt;\n</code></pre>\n<p>仓库地址：<a href=\"https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-war-plugin\">https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-war-plugin</a></p>\n<p>在项目的源文件目录下，编写如下代码</p>\n<pre><code class=\"language-java\">// HelloServlet.java\n\npackage com.mycompany.app;\n\nimport javax.servlet.http.*;\nimport javax.servlet.annotation.*;\nimport java.io.*;\n\n@WebServlet(urlPatterns = &quot;/&quot;)\npublic class HelloServlet extends HttpServlet {\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {\n        resp.setContentType(&quot;text/html&quot;);\n        PrintWriter pw = resp.getWriter();\n        pw.write(&quot;&lt;h1&gt;Hello, world!&lt;/h1&gt;&quot;);\n        pw.flush();\n    }\n}\n</code></pre>\n<p>目录结构如下</p>\n<pre><code>my-app\n │  pom.xml\n └─src\n     ├─main\n     │  └─java\n     │      └─com\n     │          └─mycompany\n     │              └─app\n     │                      HelloServlet.java\n     │\n     └─test\n         └─java\n             └─com\n                 └─mycompany\n                     └─app\n</code></pre>\n<p>运行命令</p>\n<pre><code class=\"language-sh\">mvn clean package\n</code></pre>\n<p>在 target 目录下就得到 <code>my-app-1.0-SNAPSHOT.war</code> 文件</p>\n<p>运行 servlet 的容器选择 tomcat，在<a href=\"https://tomcat.apache.org/download-90.cgi\">这里</a>下载 tomcat9（该版本支持 Servlet 4.0, JSP 2.3, EL 3.0, WebSocket 1.1, JASPIC 1.1, java 8 及以上）<br>tomcat10 开始，提供的 API 主要包已从 javax 改为 jakarta。\n关于不同版本的区别，查看：<a href=\"https://tomcat.apache.org/whichversion.html\">https://tomcat.apache.org/whichversion.html</a></p>\n<p>tomcat 有如下文件结构</p>\n<pre><code>apache-tomcat-9.0.64\n  ├─bin\n  ├─conf\n  ├─lib\n  ├─logs\n  ├─temp\n  ├─webapps\n  └─work\n</code></pre>\n<p>其中 webapps 目录已经有一些 servlet 应用了</p>\n<pre><code>webapps\n ├─app\n ├─docs\n ├─examples\n ├─host-manager\n ├─manager\n └─ROOT\n     └─WEB-INF\n</code></pre>\n<p>运行 apache-tomcat-9.0.64/bin/startup.sh 脚本（windows 下一律为 .bat ，命令行或者双击即可运行），以启动 tomcat 服务器</p>\n<p>之后，tomcat 便运行在 8080 端口上</p>\n<p>访问 <a href=\"http://localhost:8080/\">http://localhost:8080/</a> ，若出现 Tomcat 页面，则表示启动成功</p>\n<p>并且，此时运行的 servlet 应用，正是 webapps/ROOT 对应的程序</p>\n<p>访问 <a href=\"http://localhost:8080/examples/\">http://localhost:8080/examples/</a> ，则对应 webapps/example 程序</p>\n<p>将 <code>my-app-1.0-SNAPSHOT.war</code> 文件置于 webapps 目录下，浏览器访问 <a href=\"http://localhost:8080/my-app-1.0-SNAPSHOT/\">http://localhost:8080/my-app-1.0-SNAPSHOT/</a>，则显示刚才编写的程序</p>\n<blockquote>\n<p>Windows 下 tomcat 控制台输出乱码，可修改 <code>conf/logging.properties</code> 文件\n只需修改下面这行配置即可\n<code>java.util.logging.ConsoleHandler.encoding = GBK</code></p>\n</blockquote>\n<h1>Servlet 使用</h1>\n<p><a href=\"https://docs.oracle.com/javaee/7/api/javax/servlet/ServletRequest.html\">https://docs.oracle.com/javaee/7/api/javax/servlet/ServletRequest.html</a><br><a href=\"https://docs.oracle.com/javaee/7/api/javax/servlet/ServletResponse.html\">https://docs.oracle.com/javaee/7/api/javax/servlet/ServletResponse.html</a><br><a href=\"https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html\">https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html</a><br><a href=\"https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletResponse.html\">https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletResponse.html</a></p>\n<pre><code class=\"language-java\">package com.mycompany.app;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.*;\nimport javax.servlet.annotation.*;\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.*;\n\n@WebServlet(urlPatterns = &quot;/test&quot;) // 其中，&#39;/&#39; 路径不仅匹配根路径，还匹配所有未匹配路径\npublic class HelloServlet extends HttpServlet {\n    // Servlet中定义的字段，需要考虑多线程并发访问\n    private Map&lt;String, String&gt; map = new ConcurrentHashMap&lt;&gt;();\n\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {\n        resp.setContentType(&quot;text/html&quot;);\n\n        Map&lt;String, String[]&gt; query = req.getParameterMap(); // query string\n        String method = req.getMethod();\n        String uri = req.getRequestURI();\n\n        PrintWriter pw = resp.getWriter();\n        pw.write(method);\n        pw.write(uri);\n        pw.write(query.toString());\n        pw.flush();\n    }\n\n    @Override\n    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {\n        // ...\n        // 此时即面向 HttpServletRequest 和 HttpServletResponse 编程\n        // 注意 HttpServletRequest 继承了 ServletRequest，HttpServletResponse 继承了 ServletResponse\n\n        req.getRequestDispatcher(&quot;/&quot;).forward(req, resp); // 转发给其它servlet\n\n        resp.sendRedirect(&quot;/hi&quot;); // 302\n\n        resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301\n        resp.setHeader(&quot;Location&quot;, &quot;/hello&quot;);\n    }\n}\n</code></pre>\n<h1>JSP</h1>\n<blockquote>\n<p>jsp 了解即可。</p>\n</blockquote>\n<p>jsp 的访问规则类似于 php</p>\n<p>在 <code>my-app/src/main/webapp</code> 目录新建 <code>index.jsp</code>，内容如下：</p>\n<pre><code class=\"language-html\">&lt;html&gt;\n    &lt;head&gt;\n        &lt;title&gt;Hello World - JSP&lt;/title&gt;\n    &lt;/head&gt;\n    &lt;body&gt;\n        &lt;%-- JSP Comment --%&gt;\n        &lt;h1&gt;Hello World!&lt;/h1&gt;\n        &lt;p&gt;\n            &lt;% out.println(&quot;Your IP address is &quot;); %&gt;\n            &lt;span style=&quot;color:red&quot;&gt; &lt;%= request.getRemoteAddr() %&gt; &lt;/span&gt;\n        &lt;/p&gt;\n    &lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>其中，JSP 内置了几个变量：</p>\n<p>out：表示 HttpServletResponse 的 PrintWriter；<br>session：表示当前 HttpSession 对象；<br>request：表示 HttpServletRequest 对象。</p>\n<h1>Filter</h1>\n<p>Filter 可以在 HTTP 请求到达 Servlet 之前，对 ServletRequest 和 ServletResponse 进行一些预处理（一般是一些公共逻辑）<br>多个 Filter 可以链式处理</p>\n<pre><code class=\"language-java\">@WebFilter(urlPatterns = &quot;/*&quot;)\npublic class EncodingFilter implements Filter {\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        request.setCharacterEncoding(&quot;UTF-8&quot;);\n        response.setCharacterEncoding(&quot;UTF-8&quot;);\n        chain.doFilter(request, response); // 允许链式Filter\n    }\n}\n\n@WebFilter(&quot;/*&quot;)\npublic class LogFilter implements Filter {\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        System.out.println(&quot;LogFilter: process &quot; + ((HttpServletRequest) request).getRequestURI());\n        chain.doFilter(request, response);\n    }\n}\n</code></pre>\n<p>注意，一般情况下都调用 <code>chain.doFilter(request, response)</code>，否则将不会移交下一个 Filter 处理</p>\n<h1>Listener</h1>\n<p>Listener 在触发某些生命周期时，调用回调</p>\n<pre><code class=\"language-java\">@WebListener\npublic class AppListener implements ServletContextListener {\n    public void contextInitialized(ServletContextEvent sce) {\n        System.out.println(&quot;WebApp initialized: ServletContext = &quot; + sce.getServletContext());\n    }\n    public void contextDestroyed(ServletContextEvent sce) {\n        System.out.println(&quot;WebApp destroyed.&quot;);\n    }\n    public void HttpSessionListener(ServletContextEvent sce) {}\n    public void ServletRequestListener(ServletContextEvent sce) {}\n    public void ServletRequestAttributeListener(ServletContextEvent sce) {}\n    public void ServletContextAttributeListener(ServletContextEvent sce) {}\n}\n</code></pre>\n","tags":["java"]},{"id":"java-maven","url":"https://yieldray.fun/posts/java-maven","title":"java依赖管理和构建工具Maven","date_published":"2022-07-19T12:00:00.000Z","date_modified":"2022-07-19T12:00:00.000Z","content_text":"<blockquote>\n<p>Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project&#39;s build, reporting and documentation from a central piece of information.</p>\n</blockquote>\n<h1>Maven 镜像</h1>\n<p>进入<code>~/.m2</code>目录（Windows 下为<code>%USERPROFILE%/.m2</code>），新建<code>settings.xml</code>文件，内容修改为</p>\n<pre><code class=\"language-xml\">&lt;settings&gt;\n    &lt;mirrors&gt;\n        &lt;mirror&gt;\n            &lt;id&gt;aliyun&lt;/id&gt;\n            &lt;name&gt;aliyun&lt;/name&gt;\n            &lt;mirrorOf&gt;central&lt;/mirrorOf&gt;\n            &lt;url&gt;https://maven.aliyun.com/repository/central&lt;/url&gt;\n        &lt;/mirror&gt;\n    &lt;/mirrors&gt;\n&lt;/settings&gt;\n</code></pre>\n<h1>新建一个项目</h1>\n<p><a href=\"https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html\">https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html</a></p>\n<pre><code class=\"language-sh\">mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false\n</code></pre>\n<p>第一次运行这个命令会下载一些 artifacts(plugin jars and other files)<br>生成<code>my-app</code>文件夹，含以下目录结构</p>\n<pre><code>my-app\n │  pom.xml\n │\n └─src\n     ├─main\n     │  └─java\n     │      └─com\n     │          └─mycompany\n     │              └─app\n     │                      App.java\n     │\n     └─test\n         └─java\n             └─com\n                 └─mycompany\n                     └─app\n                             AppTest.java\n</code></pre>\n<p>在 <code>pom.xml</code> 中可以看到项目名、包名和版本<br>与指定 -DgroupId=com.mycompany.app -DartifactId=my-app 的一致</p>\n<pre><code class=\"language-xml\">&lt;groupId&gt;com.mycompany.app&lt;/groupId&gt;\n&lt;artifactId&gt;my-app&lt;/artifactId&gt;\n&lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;\n</code></pre>\n<p>项目属性，构建选项</p>\n<pre><code class=\"language-xml\">&lt;properties&gt;\n  &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;\n  &lt;maven.compiler.source&gt;1.7&lt;/maven.compiler.source&gt;\n  &lt;maven.compiler.target&gt;1.7&lt;/maven.compiler.target&gt;\n&lt;/properties&gt;\n</code></pre>\n<p>依赖</p>\n<pre><code class=\"language-xml\">&lt;dependencies&gt;\n  &lt;dependency&gt;\n    &lt;groupId&gt;junit&lt;/groupId&gt;\n    &lt;artifactId&gt;junit&lt;/artifactId&gt;\n    &lt;version&gt;4.11&lt;/version&gt;\n    &lt;scope&gt;test&lt;/scope&gt;\n  &lt;/dependency&gt;\n&lt;/dependencies&gt;\n</code></pre>\n<blockquote>\n<h4>What did I just do?</h4>\n<p>You executed the Maven goal archetype:generate, and passed in various parameters to that goal. The prefix archetype is the plugin that provides the goal. If you are familiar with Ant, you may conceive of this as similar to a task. This archetype:generate goal created a simple project based upon a maven-archetype-quickstart archetype. Suffice it to say for now that a plugin is a collection of goals with a general common purpose. For example the jboss-maven-plugin, whose purpose is &quot;deal with various jboss items&quot;.</p>\n</blockquote>\n<p>执行以下命令构建这个项目</p>\n<pre><code class=\"language-sh\">mvn package\n</code></pre>\n<p>运行构建产物</p>\n<pre><code class=\"language-sh\">java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App\n</code></pre>\n<p>输出</p>\n<pre><code>Hello World!\n</code></pre>\n<p>注意，如果需要支持 java9 及以上，需要指定<code>pom.xml</code> ，例如：</p>\n<pre><code class=\"language-xml\">    &lt;properties&gt;\n        &lt;maven.compiler.release&gt;11&lt;/maven.compiler.release&gt;\n    &lt;/properties&gt;\n\n    &lt;build&gt;\n        &lt;pluginManagement&gt;\n            &lt;plugins&gt;\n                &lt;plugin&gt;\n                    &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;\n                    &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;\n                    &lt;version&gt;3.8.1&lt;/version&gt;\n                &lt;/plugin&gt;\n            &lt;/plugins&gt;\n        &lt;/pluginManagement&gt;\n    &lt;/build&gt;\n</code></pre>\n<h1>一些命令</h1>\n<p><a href=\"https://maven.apache.org/guides/getting-started/index.html\">https://maven.apache.org/guides/getting-started/index.html</a></p>\n<h2>编译应用源码</h2>\n<pre><code>mvn compile\n</code></pre>\n<p>编译后的 class 文件将置于 <code>${basedir}/target/classes</code> 文件夹</p>\n<h2>编译测试和运行单元测试</h2>\n<pre><code>mvn test\n</code></pre>\n<p>如果只编译测试不运行，则执行以下命令</p>\n<pre><code>mvn test-compile\n</code></pre>\n<h2>生成 jar 文件</h2>\n<pre><code>mvn package\n</code></pre>\n<p>jar 文件将生成至 <code>${basedir}/target</code>目录</p>\n<h2>安装至本地仓库</h2>\n<pre><code>mvn install\n</code></pre>\n<h2>生成文档网站</h2>\n<pre><code>mvn site\n</code></pre>\n<h2>清理 target 文件夹</h2>\n<pre><code>mvn clean\n</code></pre>\n<h1>添加资源至 jar 包</h1>\n<p>所有置于 <code>${basedir}/src/main/resources</code> 目录的文件都会被打包至 jar 文件，并且资源文件相对于 jar 包根路径有相同的目录结构</p>\n<h1>添加依赖</h1>\n<p><a href=\"https://search.maven.org/\">https://search.maven.org/</a></p>\n<p>依赖定义于 <code>pom.xml</code> 如下</p>\n<pre><code class=\"language-xml\">&lt;dependencies&gt;\n  &lt;dependency&gt;\n    &lt;groupId&gt;junit&lt;/groupId&gt;\n    &lt;artifactId&gt;junit&lt;/artifactId&gt;\n    &lt;version&gt;4.11&lt;/version&gt;\n    &lt;scope&gt;test&lt;/scope&gt;\n  &lt;/dependency&gt;\n&lt;/dependencies&gt;\n</code></pre>\n<p>依赖有四种类型：compile provided runtime test</p>\n<p>Maven 读取依赖的顺序是，先在 <code>~/.m2/repository</code> （本地仓库）查找，没有查找到则在<a href=\"https://repo.maven.apache.org/maven2/\">远程仓库</a>（中央仓库）查找</p>\n<h1>生命周期</h1>\n<p>Maven 中存在三种生命周期：clean、default、site，分别用于清理项目、构建项目、生成项目站点，而在一个生命周期中通常又会包含若干个 phase（阶段）</p>\n<p>default 生命周期是所有生命周期中最核心部分，包括以下 phase</p>\n<pre><code>validate\ninitialize\ngenerate-sources\nprocess-sources\ngenerate-resources\nprocess-resources\ncompile\nprocess-classes\ngenerate-test-sources\nprocess-test-sources\ngenerate-test-resources\nprocess-test-resources\ntest-compile\nprocess-test-classes\ntest\nprepare-package\npackage\npre-integration-test\nintegration-test\npost-integration-test\nverify\ninstall\ndeploy\n</code></pre>\n<p>例如，运行 <code>mvn package</code> 后，Maven 执行 default 生命周期，将运行 validate 到 package 之间（闭区间）的所有 phase</p>\n<hr>\n<p>clean 生命周期（lifecycle），包括以下 phase</p>\n<pre><code>pre-clean\nclean\npost-clean\n</code></pre>\n<hr>\n<p>上文介绍了一些 mvn 命令，实际上，命令还可以指定多个 phase，这些 phase 依次进行，例如</p>\n<pre><code class=\"language-sh\">mvn clean compile # 清理后编译\nmvn clean package # 清理后打包\n</code></pre>\n<p>执行 phase 时，触发多个 goal。goal 的命名总是 abc:xyz 这种形式。</p>\n<p>例如，执行 <code>test</code> 时，触发 <code>compiler:testCompile</code> <code>surefire:test</code></p>\n<p>也可以直接指定执行 goal，例如 <code>mvn tomcat:run</code></p>\n<p>注：也可以这样执行 goal：</p>\n<pre><code class=\"language-sh\">mvn [groupId]:[artifactId]:[version]:[goalName]\nmvn org.apache.maven.plugins:maven-dependency-plugin:2.1:list\n</code></pre>\n<p>一个 Maven 插件中，可以具备一个或多个功能，插件功能实现了 goal。</p>\n<p>因此，phase 实际上是绑定到一个或多个 goal 工作，goal 由 plugin 提供</p>\n<p>Maven 自带 phase 绑定了如下 goal</p>\n<style type=\"text/css\">\n.tg  {border-collapse:collapse;border-spacing:0;}\n.tg td{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;\n  overflow:hidden;padding:10px 5px;word-break:normal;}\n.tg th{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;\n  font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal;}\n.tg .tg-x437{background-color:#333333;border-color:#ffffff;color:#ffffff;text-align:left;vertical-align:middle}\n.tg .tg-ja5b{background-color:#ffffc7;border-color:#ffffff;color:#000000;text-align:left;vertical-align:middle}\n.tg .tg-jhh9{background-color:#ffccc9;border-color:#ffffff;color:#000000;text-align:left;vertical-align:middle}\n.tg .tg-sv22{background-color:#ffffc7;border-color:#ffffff;color:#000000;font-family:inherit;text-align:left;vertical-align:top}\n.tg .tg-urei{background-color:#dae8fc;border-color:#ffffff;color:#000000;text-align:left;vertical-align:middle}\n</style>\n<table class=\"tg\">\n<thead>\n  <tr>\n    <th class=\"tg-x437\">生命周期（lifecycle）</th>\n    <th class=\"tg-x437\">阶段（phase）</th>\n    <th class=\"tg-x437\">目标（goal）</th>\n  </tr>\n</thead>\n<tbody>\n  <tr>\n    <td class=\"tg-jhh9\">clean</td>\n    <td class=\"tg-jhh9\">clean</td>\n    <td class=\"tg-jhh9\">maven-clean-plugin:clean</td>\n  </tr>\n  <tr>\n    <td class=\"tg-sv22\" rowspan=\"7\">default</td>\n    <td class=\"tg-ja5b\">process-resources</td>\n    <td class=\"tg-ja5b\">maven-resources-plugin:resources</td>\n  </tr>\n  <tr>\n    <td class=\"tg-ja5b\">compile</td>\n    <td class=\"tg-ja5b\">maven-compiler-plugin:compile</td>\n  </tr>\n  <tr>\n    <td class=\"tg-ja5b\">process-test-resources</td>\n    <td class=\"tg-ja5b\">maven-resources-plugin:testResources</td>\n  </tr>\n  <tr>\n    <td class=\"tg-ja5b\">test</td>\n    <td class=\"tg-ja5b\">maven-surefire-plugin:test</td>\n  </tr>\n  <tr>\n    <td class=\"tg-ja5b\">package</td>\n    <td class=\"tg-ja5b\">maven-jar-plugin:jar</td>\n  </tr>\n  <tr>\n    <td class=\"tg-ja5b\">install</td>\n    <td class=\"tg-ja5b\">maven-install-plugin:install</td>\n  </tr>\n  <tr>\n    <td class=\"tg-ja5b\">deploy</td>\n    <td class=\"tg-ja5b\">maven-deploy-plugin:deploy</td>\n  </tr>\n  <tr>\n    <td class=\"tg-urei\" rowspan=\"2\">site</td>\n    <td class=\"tg-urei\">site</td>\n    <td class=\"tg-urei\">maven-site-plugin:site</td>\n  </tr>\n  <tr>\n    <td class=\"tg-urei\">site-deploy</td>\n    <td class=\"tg-urei\">maven-site-plugin:deploy</td>\n  </tr>\n</tbody>\n</table>\n\n<p>其余绑定需要在 <code>pom.xml</code> 中配置，例如</p>\n<pre><code class=\"language-xml\">&lt;plugin&gt;\n    &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;\n    &lt;artifactId&gt;maven-source-plugin&lt;/artifactId&gt;\n    &lt;version&gt;2.1.1&lt;/version&gt;\n    &lt;executions&gt;\n        &lt;execution&gt;\n            &lt;id&gt;attach-sources&lt;/id&gt;\n            &lt;phase&gt;package&lt;/phase&gt;\n            &lt;goals&gt;\n                &lt;goal&gt;jar-no-fork&lt;/goal&gt;\n            &lt;/goals&gt;\n        &lt;/execution&gt;\n    &lt;/executions&gt;\n&lt;/plugin&gt;\n</code></pre>\n<p>此时，如果运行 <code>mvn package</code>，则先触发 default 生命周期 package 阶段，再触发 attach-sources 的 package 阶段</p>\n<h1>拓展阅读</h1>\n<p><a href=\"https://maven.apache.org/users/index.html#maven-users-centre\">Maven Users Centre</a></p>\n<p>《Maven 实战》</p>\n","tags":["java"]},{"id":"java-thread-utils","url":"https://yieldray.fun/posts/java-thread-utils","title":"java多线程实用库","date_published":"2022-07-18T12:30:00.000Z","date_modified":"2022-07-18T12:30:00.000Z","content_text":"<p>多线程工具类由 java.util.concurrent 包提供<br>锁工具类由 java.util.concurrent.locks 包提供</p>\n<p>java.util.concurrent.locks 包：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html</a><br>锁接口：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Lock.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Lock.html</a></p>\n<h1>ReentrantLock 类， 可重入锁</h1>\n<p>ReentrantLock 锁由 java.util.concurrent.locks 包提供，可作为 synchronized 语法的替代</p>\n<pre><code class=\"language-java\">import java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nclass X {\n    private final Lock lock = new ReentrantLock();\n\n    public void m() {\n        lock.lock();  // 加锁\n        try {\n            // ... method body\n        } finally {\n            lock.unlock(); // 解锁\n        }\n    }\n}\n</code></pre>\n<p>ReentrantLock（而非 Lock 接口）还提供更多实例方法。<br>参见 API：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html</a></p>\n<h1>Lock 接口</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Lock.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Lock.html</a></p>\n<p>Lock 接口还允许尝试获取锁</p>\n<pre><code class=\"language-java\">// lock.lockInterruptibly(); // 尝试获取锁除非当前线程被 interrupted\n// lock.tryLock(1, TimeUnit.SECONDS); // 指定1s\nif (lock.tryLock()) { // 尝试获取锁\n    try {\n        // ...\n    } finally {\n        lock.unlock();\n    }\n}\n</code></pre>\n<h1>Condition 类， 实现 wait/notify</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Condition.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Condition.html</a></p>\n<pre><code class=\"language-java\">import java.util.*;\nimport java.util.concurrent.locks.*;\n\nclass TaskQueue {\n    private final Lock lock = new ReentrantLock();\n    private final Condition condition = lock.newCondition(); // 注意这个 Condition 对象是绑定到 Lock 对象上的\n    private final LinkedList&lt;String&gt; queue = new LinkedList&lt;&gt;();\n\n    public void addTask(String s) {\n        lock.lock();\n        try {\n            queue.add(s);\n            condition.signalAll(); // 相当于 notifyAll， signal 则相当于 notify\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    public String getTask() throws InterruptedException {\n        lock.lock();\n        try {\n            while (queue.isEmpty()) {\n                condition.await(); // 相当于 wait，此方法可能抛出 InterruptedException，因为在等待期间可能触发 interrupt()\n            }\n            return queue.remove();\n        } finally {\n            lock.unlock();\n        }\n    }\n}\n</code></pre>\n<h1>ReadWriteLock</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/ReadWriteLock.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/ReadWriteLock.html</a></p>\n<p>ReadWriteLock 是一个接口，提供了 readLock 和 writeLock 两个实例方法（返回 Lock 接口）。<br>可实现一个资源可以被多个线程同时读，或者被一个线程写，但是不能同时存在读和写线程。</p>\n<p>ReentrantReadWriteLock <em>可重入读写锁</em>实现了 ReadWriteLock 接口</p>\n<pre><code class=\"language-java\">import java.util.*;\nimport java.util.concurrent.locks.*;\n\nclass MyReadWriteMap {\n    private volatile Map&lt;String, String&gt; map = new HashMap&lt;&gt;();\n    private final ReadWriteLock lock = new ReentrantReadWriteLock();\n\n    public void put(String key, String value) {\n        lock.writeLock().lock(); // 加写锁\n        try {\n            map.put(key, value);\n        } finally {\n            lock.writeLock().unlock(); // 释放写锁\n        }\n    }\n\n    public String get(String key) {\n        lock.readLock().lock(); // 加读锁\n        try {\n            return map.get(key);\n        } finally {\n            lock.readLock().unlock(); // 释放读锁\n        }\n    }\n}\n</code></pre>\n<h1>StampedLock</h1>\n<p>&lt;<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/StampedLock.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/StampedLock.html</a> &gt;</p>\n<p>StampedLock 在 ReentrantReadWriteLock 的基础上新增一种乐观读的模式，允许获取写锁后写入。<br>（StampedLock 是提供的是独立的接口，并非 ReadWriteLock 接口实现，即使其 API 有相似性）</p>\n<p>乐观读模式（没有加锁）为了防止读写过程中资源不同步，还需要额外的判断。<br>此外，StampedLock 也同样提供<em>独占写锁</em>和<em>悲观读锁</em> （就像 ReadWriteLock）<br>并且，StampedLock 提供转换为读锁、写锁、读写锁的方法</p>\n<pre><code class=\"language-java\">import java.util.concurrent.locks.*;\n\nclass Complex {\n    private final StampedLock stampedLock = new StampedLock();\n    private double x = 0;\n    private double y = 0;\n\n    public void set(double newX, double newY) {\n        long stamp = stampedLock.writeLock(); // 获取写锁\n        try {\n            x = newX;\n            y = newY;\n        } finally {\n            stampedLock.unlockWrite(stamp); // 释放写锁\n        }\n    }\n\n    public double[] getAsArray() {\n        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁\n        double currentX = x;\n        double currentY = y;\n\n        if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生\n            stamp = stampedLock.readLock(); // 获取一个悲观读锁\n            try {\n                currentX = x;\n                currentY = y;\n            } finally {\n                stampedLock.unlockRead(stamp); // 释放悲观读锁\n            }\n        }\n        return new double[]{currentX, currentY};\n    }\n}\n</code></pre>\n<h1>线程安全集合类</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/package-summary.html\">java.util.concurrent</a> 包提供了一些线程安全的集合类，实现了集合类接口，可在多线程时使用。</p>\n<p>例如：<code>ConcurrentMap&lt;K,V&gt;</code> <code>ConcurrentHashMap&lt;K,V&gt;</code> <code>ConcurrentLinkedQueue&lt;E&gt;</code> <code>ConcurrentLinkedDeque&lt;E&gt;</code> <code>CopyOnWriteArraySet&lt;E&gt;</code> <code>CopyOnWriteArrayList&lt;E&gt;</code> 等等。</p>\n<p>需要时参阅 API 文档即可。<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/package-summary.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/package-summary.html</a></p>\n<h1>原子操作 java.util.concurrent.atomic 包</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html\">java.util.concurrent.atomic</a> 包提供了基于原子操作的封装类，以一种自旋锁的方式进行读写</p>\n<pre><code class=\"language-java\">var i = new AtomicInteger(1); // 可指定初始值，不指定则为默认值\nSystem.out.println(i.get()); // 1\nSystem.out.println(i.addAndGet(9)); // 10\nSystem.out.println(i.incrementAndGet()); // 11\n</code></pre>\n<h1>Future 类，线程返回值</h1>\n<p>Future 接口：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Future.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Future.html</a>\nFutureTask 类：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/FutureTask.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/FutureTask.html</a></p>\n<pre><code class=\"language-java\">FutureTask task = new FutureTask(new Callable&lt;Integer&gt;() {\n    @Override\n    public Integer call() throws Exception {\n        return 123;\n    }\n});\nThread t1 = new Thread(task);\nt1.start();\nSystem.out.println(task.get()); // 123\n</code></pre>\n<p>注意到线程池 ExecutorService 接口的 submit 方法还有一个参数为 Callable 的重载方法，该方法返回 Future&lt;?&gt;</p>\n<pre><code class=\"language-java\">ExecutorService executor = Executors.newFixedThreadPool(4);\nCallable&lt;Integer&gt; task = new Callable&lt;Integer&gt;() {\n    @Override\n    public Integer call() throws Exception {\n        return 123;\n    }\n};\nFuture&lt;Integer&gt; future = executor.submit(task);\nInteger result = future.get();\nSystem.out.println(result); // 1\n</code></pre>\n<h1>CompletableFuture 类，异步返回</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html</a></p>\n<pre><code class=\"language-java\">CompletableFuture&lt;Integer&gt; cf = CompletableFuture.supplyAsync(() -&gt; 123);\ncf = cf.thenApplyAsync(n -&gt; n - 23); // 类似于 Promise.then 回调参数是上一次执行的返回值\ncf.thenAccept(System.out::println); // 100，删掉上面一行则是 123\ncf.exceptionally((e) -&gt; {\n    e.printStackTrace();\n    return null;\n});\n\nCompletableFuture&lt;Object&gt; cfs = CompletableFuture.anyOf(CompletableFuture.supplyAsync(() -&gt; 123), CompletableFutursupplyAsync(() -&gt; 456));\ncfs.thenAccept(System.out::println); // 返回的是两个CompletableFuture中先完成的那个\n\n\n// 主线程不要立刻结束，否则CompletableFuture默认使用的线程池会立刻关闭:\nThread.sleep(200);\n</code></pre>\n<h1>ThreadLocal</h1>\n<p>ThreadLocal 为变量在每个线程中创建了一个副本，这样每个线程都可以访问自己内部的副本变量</p>\n<pre><code class=\"language-java\">ThreadLocal&lt;Integer&gt; local = new ThreadLocal&lt;&gt;();\nIntStream.range(0, 5).forEach(i -&gt; new Thread(() -&gt; {\n    try {\n        local.set(i);\n        System.out.println(&quot;[&quot; + Thread.currentThread().getName() + &quot;]: &quot; + local.get());\n    } finally {\n        local.remove();\n        // 如果不进行清除，当这个线程再次执行时，仍会获取到上次设置的变量\n        // 本例中由于每个线程都对local进行设置，所以也可以不用清除。\n\n        // 原理类似于：\n        // var tlm = new Map&lt;Thread, T&gt;;              // var local = new ThreadLocal&lt;T&gt;()\n        // tlm.set(Thread.currentThread(), value);    // local.set(value);\n        // tlm.get(Thread.currentThread());           // local.get();\n    }\n}).start());\n</code></pre>\n<p>输出（顺序随机）</p>\n<pre><code>[Thread-0]: 0\n[Thread-4]: 4\n[Thread-1]: 1\n[Thread-3]: 3\n[Thread-2]: 2\n</code></pre>\n","tags":["java"]},{"id":"java-thread-pool","url":"https://yieldray.fun/posts/java-thread-pool","title":"java线程池","date_published":"2022-07-17T15:10:56.000Z","date_modified":"2022-07-17T15:10:56.000Z","content_text":"<p>线程池工具类由 java.util.concurrent 包提供</p>\n<h1>线程池</h1>\n<p>线程池用于管理和分配线程</p>\n<h1>ExecutorService 接口</h1>\n<p>java.util.concurrent.Executors 类提供静态方法，返回不同类型的线程池，这些线程池用 ExecutorService 接口操作</p>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html</a><br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html</a></p>\n<pre><code class=\"language-java\">import java.util.concurrent.*;\n\npublic class Main {\n    public static void main(String[] args) throws InterruptedException {\n        ExecutorService es = Executors.newFixedThreadPool(4); // 创建一个固定大小的线程池\n\n        for (int i = 1; i &lt;= 6; i++) {\n            int id = i;\n            es.submit(() -&gt; System.out.println(&quot;Running Task: &quot; + id));\n        }\n        es.shutdown(); // 立即关闭线程池\n//        es.awaitTermination(5, TimeUnit.SECONDS); //  等待5s后再关闭线程池\n    }\n}\n</code></pre>\n<p>上面的线程池管理固定的 4 个线程，所以 1 2 3 4 号线程同时运行，5 6 号线程总是在前面的线程执行完毕后运行<br>因此，输出结果应该是 1 2 3 4 之间任意排序，然后是 5 6 之间任意排序</p>\n<pre><code class=\"language-java\">// 该线程池会动态调整线程数\nExecutorService es = Executors.newCachedThreadPool();\n</code></pre>\n<pre><code class=\"language-java\">// 指定动态调整的线程数范围\nint min = 4;\nint max = 10;\nExecutorService es = new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new SynchronousQueue&lt;Runnable&gt;());\n</code></pre>\n<h1>ScheduledThreadPoolExecutor （ExecutorService 实现）</h1>\n<pre><code class=\"language-java\">ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);\n// 1秒后执行一次性任务:\nses.schedule(new Task(&quot;one-time&quot;), 1, TimeUnit.SECONDS);\n// 2秒后开始执行定时任务，每3秒执行:\nses.scheduleAtFixedRate(new Task(&quot;fixed-rate&quot;), 2, 3, TimeUnit.SECONDS);\n// 2秒后开始执行定时任务，以3秒为间隔执行:\nses.scheduleWithFixedDelay(new Task(&quot;fixed-delay&quot;), 2, 3, TimeUnit.SECONDS);\n</code></pre>\n<h1>ForkJoinPool （ExecutorService 实现）</h1>\n<p>ForkJoin 线程池用于将大任务拆成多个小任务并行执行。</p>\n<pre><code class=\"language-java\">import java.util.*;\nimport java.util.concurrent.*;\n\npublic class Main {\n    public static void main(String[] args) throws Exception {\n        long[] array = new Random(233).longs(1, 100)\n                .limit(2000).toArray();\n        long sum = Arrays.stream(array).sum(); // 求和\n\n        // 执行 ForkJoin 任务\n        ForkJoinTask&lt;Long&gt; task = new SumTask(array, 0, array.length);\n        Long result = ForkJoinPool.commonPool().invoke(task);\n        System.out.println(&quot;expect: &quot; + sum + &quot; got: &quot; + result);\n        // expect: 100732 got: 100732\n    }\n}\n\n// 递归任务实现\nclass SumTask extends RecursiveTask&lt;Long&gt; {\n    static final int THRESHOLD = 500;\n    long[] array;\n    int start;\n    int end;\n\n    SumTask(long[] array, int start, int end) {\n        this.array = array;\n        this.start = start;\n        this.end = end;\n    }\n\n    @Override\n    protected Long compute() {\n        if (end - start &lt;= THRESHOLD) {\n            // 如果任务足够小，直接计算\n            return Arrays.stream(array).skip(start).limit(end - start).sum(); // 数组指定范围求和\n        } else {\n            // 任务太大，一分为二\n            int middle = (end + start) / 2;\n            SumTask subtask1 = new SumTask(this.array, start, middle);\n            SumTask subtask2 = new SumTask(this.array, middle, end);\n            invokeAll(subtask1, subtask2);\n            long subresult1 = subtask1.join();\n            long subresult2 = subtask2.join();\n            return subresult1 + subresult2;\n        }\n    }\n}\n</code></pre>\n<h1>ThreadPoolExecutor （ExecutorService 实现）</h1>\n<p>ThreadPoolExecutor 是通用的 ExecutorService 实现<br>能够实现相较之 Executors 静态方法提供的线程池 newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool() 更灵活的线程池</p>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html</a></p>\n","tags":["java"]},{"id":"java-thread-intro","url":"https://yieldray.fun/posts/java-thread-intro","title":"java多线程入门","date_published":"2022-07-16T17:35:26.000Z","date_modified":"2022-07-16T17:35:26.000Z","content_text":"<h1>多线程基础</h1>\n<p>创建新的线程的方法主要有两个。</p>\n<ol>\n<li>继承 Thread 类，覆写 <strong>run() 实例方法</strong> （该方法将由新线程运行）</li>\n<li>Thread 构造函数中传入 <strong>Runnable 接口</strong>，实现 Runnable 接口的 run() 方法</li>\n</ol>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html</a></p>\n<pre><code class=\"language-java\">public class Main {\n    public static void main(String[] args) {\n        // https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html\n        Thread t1 = new MyThread();\n        Thread t2 = new Thread(new MyRunnable());\n        Thread t3 = new Thread(() -&gt; System.out.println(&quot;Lambda&quot;));\n        t1.start();\n        t2.start();\n        t3.start();\n        // 注意是 start() 方法启动线程，调用run()方法仅在当前线程执行，不会创建线程\n\n        // 下面演示一些实例对象方法\n        t1.setPriority(3);// 优先级。范围从 MIN_PRIORITY 到 MAX_PRIORITY\n        t1.join(); // 调用此方法的线程等待该线程执行完毕再继续执行，即 await\n        t1.interrupt(); // 将该线程设置为interrupted，在该线程中通过Thread.interrupted()或isInterrupted()方法返回是否interrupted\n        t1.isInterrupted(); // 返回该线程是否interrupted\n        t1.setDaemon(true); // 设置该线程为守护线程，守护线程在其他线程执行完毕后自动退出\n        t1.getState(); // 返回运行状态，字符串\n    }\n\n    Runnable example = () -&gt; {\n        // Thread类的静态方法，不建议在实例对象上调用\n        // 这些静态方法用于控制调用该方法的线程\n        Thread.sleep(1000); // ms\n        Thread.interrupted(); // 返回是否当前线程被调用了interrupt()方法\n    };\n}\n\nclass Counter {// 多个线程之间共享内存，所以此对象在所有线程中共享，皆可以访问\n    public static final Object lock = new Object();\n    public static int count = 0;\n}\n\nclass MyThread extends Thread {\n    public volatile int syncValue = 123; // volatile 关键字确保变量在不同线程同步\n    public int asyncValue = 456; // 非 volatile 变量需要加锁才能保证不出异常\n\n    public synchronized void syncMethod() {\n        // synchronized 修饰的方法，是 synchronized(this) {/* 函数体 */} 的语法糖\n        // 加锁的对象是 this，即当前对象\n    };\n    public static synchronized void syncStaticMethod(){\n        // synchronized 修饰的静态方法，是 synchronized(当前类.class) {/* 函数体 */} 的语法糖\n        // 例如，这里相当于 synchronized(MyThread.class){}\n    }\n\n    @Override\n    public void run() {\n        synchronized (Counter.lock) { // 线程锁，通过给定锁保证（非原子）操作是同步的\n            Counter.count += 1;\n        }\n        // 死锁产生的原因及四个必要条件\n        // https://zhuanlan.zhihu.com/p/25677118\n\n        System.out.println(&quot;MyThread&quot;);\n    }\n}\n\nclass MyRunnable implements Runnable {\n    @Override\n    public void run() {\n        System.out.println(&quot;MyRunnable&quot;);\n    }\n}\n</code></pre>\n<h1>中断线程</h1>\n<p>注意：当前线程若处于 wait() sleep() join() 方法的等待状态，而被触发了 interrupt() ，则抛出 InterruptedException 异常</p>\n<pre><code class=\"language-java\">public class Main {\n    public static void main(String[] args) throws InterruptedException {\n        Thread t = new MyThread();\n        t.start();\n        Thread.sleep(10); // 当前线程暂停10毫秒\n        t.interrupt(); // 中断t线程\n        t.join(); // 等待t线程结束\n        System.out.println(&quot;end&quot;);\n    }\n}\n\nclass MyThread extends Thread {\n    public void run() {\n        int n = 0;\n        while (!isInterrupted())\n            System.out.println(&quot;loop &quot; + (++n) + &quot; times!&quot;);\n    }\n}\n</code></pre>\n<p>上面的代码相当于主线程允许 MyThread 线程执行 10ms，然后向 MyThread 线程发出中断信号。<br>MyThread 线程循环检测自身来决定是否继续运行。</p>\n<p>此外，还可以通过设置标志位来实现线程中断。<br>注意 <strong>volatile</strong> 关键字，表示标识的变量在线程间实时共享（即：变量在不同的线程中是一致的，否则可能会获取到缓存的值，导致不一致）<br>细节上，注意由于 running 标志位附加在 MyThread 实例上，需要声明<code>MyThread t = new MyThread()</code>而非<code>Thread t = new MyThread()</code></p>\n<pre><code class=\"language-java\">public class Main {\n    public static void main(String[] args) throws InterruptedException {\n        MyThread t = new MyThread();\n        t.start();\n        Thread.sleep(10);\n        t.running = false; // 标志位置为false\n        System.out.println(&quot;end!&quot;);\n    }\n}\n\nclass MyThread extends Thread {\n    public volatile boolean running = true;\n\n    public void run() {\n        int n = 0;\n        while (running)\n            System.out.println(&quot;loop &quot; + (++n) + &quot; times!&quot;);\n    }\n}\n</code></pre>\n<h1>守护线程</h1>\n<p>jvm 将在所有<em>非守护线程</em>执行完后，自动退出。此时所有守护线程也立即停止。<br>同理，若存在<em>守护线程</em>，<em>守护线程</em>将只能在任意<em>非守护线程</em>运行时运行。</p>\n<p>因此，守护线程一般用于服务非守护线程运行。<br>并且，守护线程不应持有需要关闭的资源，因为守护线程没有机会执行关闭操作。</p>\n<p>对某一线程执行实例方法 <code>setDaemon(true)</code> 即可将该线程指定为守护线程</p>\n<h1>线程同步，线程锁</h1>\n<p>在执行非原子操作时，我们一般需要进行线程同步，以保证数据一致</p>\n<blockquote>\n<p>原子操作可简单理解为<em>单独的 CPU 指令</em> （在多核处理器中，情况更为复杂）</p>\n</blockquote>\n<p>线程锁的声明：</p>\n<pre><code class=\"language-java\">synchronized(LOCK) {\n    // 临界区（Critical Section）\n}\n</code></pre>\n<p>临界区最多只有一个线程能执行，因此可保证其内部操作的原子性（前提是正确使用锁）。</p>\n<p><code>synchronized</code> 标识则是 <code>synchronized(){}</code> 的语法糖，具体见第一节演示。</p>\n<h1>可重入锁</h1>\n<p>在 jvm 中，对于同一个锁，<code>synchronized(){}</code> 也允许嵌套使用（不同的锁当然允许嵌套）。<br>可重入锁在所有嵌套的锁都释放后释放。</p>\n<p>例如：</p>\n<pre><code class=\"language-java\">synchronized(LOCK){\n    // 未释放\n    synchronized(LOCK){\n        // 未释放\n    }\n    // 未释放\n}\n// 释放\n</code></pre>\n<h1>死锁，活锁</h1>\n<blockquote>\n<p>死锁是线程都在等待对方先释放资源\n活锁则是线程彼此释放资源又同时占用对方释放的资源</p>\n</blockquote>\n<h1>wait notify</h1>\n<p>位于 Object 类上的实例方法 wait() notify() 将在调用这些方法的线程中生效，控制锁的行为（这个锁即是该 Object 实例对象）</p>\n<p>在某一线程中，对一对象调用 wait() 实例方法，线程将会暂停（直至其它线程中调用这个对象的 notify() 实例方法唤醒该线程）<br>wait() 实例方法可指定等待时间作为参数，参见<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Object.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Object.html</a>\nnotify() 实例方法随机唤醒一个处于 wait() 状态的线程<br>notifyAll() 实例方法唤醒所有处于 wait() 状态的线程，一般情况下都使用 notifyAll</p>\n<hr>\n<p>示例：</p>\n<pre><code class=\"language-java\">import java.util.*;\n\nclass TaskQueue {\n    private final Queue&lt;String&gt; queue = new LinkedList&lt;&gt;();\n\n    public synchronized void addTask(String s) {\n        queue.add(s);\n        notifyAll(); // 通知所有 wait() 状态的线程\n    }\n\n    public synchronized String getTask() {\n        while (queue.isEmpty()) // 循环执行\n            try {\n                wait(); // 此方法可能抛出 InterruptedException，因为在等待期间可能触发 interrupt()\n            } catch (InterruptedException e) {\n                e.printStackTrace(); // 处于演示目的，可以忽略此处的错误处理\n            }\n        return queue.remove();\n    }\n}\n</code></pre>\n<p>在主线程中：</p>\n<pre><code class=\"language-java\">public class Main {\n    public static void main(String[] args) throws InterruptedException {\n        TaskQueue task = new TaskQueue();\n        Thread t1 = new Thread(() -&gt; System.out.println(&quot;Got Task: &quot; + task.getTask()));\n        Thread t2 = new Thread(() -&gt; task.addTask(&quot;new task&quot;));\n        t1.start(); // t1 先运行，等待有任务才输出\n        t2.start(); // t2 后运行，添加任务\n    }\n}\n</code></pre>\n","tags":["java"]},{"id":"java-stream","url":"https://yieldray.fun/posts/java-stream","title":"java流","date_published":"2022-07-15T11:46:12.000Z","date_modified":"2022-07-15T11:46:12.000Z","content_text":"<p>日前在<a href=\"/posts/java-functional/\">java 函数式编程</a>中提到了 java 流<br>前面讲到通过 <code>Stream.iterate()</code> 和 <code>Stream.of()</code> 创建流。<br>一些数组 API 和 集合 API 也提供了转换为流的方法，例如：<code>Arrays.stream()</code> 和 <code>Collection&lt;E&gt;</code> 实例方法 <code>stream()</code><br>其它 API 也可能以流的形式提供，例如 <a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Random.html\">java.util.Random</a> 类</p>\n<p>此外，除了通用的流（java.util.stream.Stream）外，还<a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html\">提供了几个特化的流</a>：IntStream LongStream DoubleStream 。有关 java8 之后提供的 API，查阅<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/package-summary.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/package-summary.html</a> ，详细内容务必查阅 API 文档。</p>\n<h1>Interface Stream<T> （java.util.stream）</h1>\n<h2>静态方法</h2>\n<p>Stream.of() Stream.generate() Stream.iterate() 用于生成流。<br>Stream.empty() 用于生成一个空流。<br>Stream.concat() 用于合并流。<br>Stream.builder() 返回流建造者接口，在下一节讨论。</p>\n<h2>实例方法</h2>\n<pre><code class=\"language-java\">public interface Stream&lt;T&gt; extends BaseStream&lt;T, Stream&lt;T&gt;&gt; {\n    // map 系列函数\n    &lt;R&gt; Stream&lt;R&gt; map(Function&lt;? super T, ? extends R&gt; mapper);\n    IntStream mapToInt(ToIntFunction&lt;? super T&gt; mapper);\n    LongStream mapToLong(ToLongFunction&lt;? super T&gt; mapper);\n    DoubleStream mapToDouble(ToDoubleFunction&lt;? super T&gt; mapper);\n    &lt;R&gt; Stream&lt;R&gt; flatMap(Function&lt;? super T, ? extends Stream&lt;? extends R&gt;&gt; mapper);\n    IntStream flatMapToInt(Function&lt;? super T, ? extends IntStream&gt; mapper);\n    LongStream flatMapToLong(Function&lt;? super T, ? extends LongStream&gt; mapper);\n    DoubleStream flatMapToDouble(Function&lt;? super T, ? extends DoubleStream&gt; mapper);\n\n    // 链式调用函数\n    Stream&lt;T&gt; distinct();\n    Stream&lt;T&gt; sorted();\n    Stream&lt;T&gt; sorted(Comparator&lt;? super T&gt; comparator);\n    Stream&lt;T&gt; peek(Consumer&lt;? super T&gt; action);\n    Stream&lt;T&gt; limit(long maxSize);\n    Stream&lt;T&gt; skip(long n);\n    Stream&lt;T&gt; filter(Predicate&lt;? super T&gt; predicate);\n\n    // 返回流中元素的数量\n    long count();\n\n    // 匹配函数\n    boolean anyMatch(Predicate&lt;? super T&gt; predicate);\n    boolean allMatch(Predicate&lt;? super T&gt; predicate);\n    boolean noneMatch(Predicate&lt;? super T&gt; predicate);\n\n    // 循环函数\n    void forEach(Consumer&lt;? super T&gt; action);\n    void forEachOrdered(Consumer&lt;? super T&gt; action);\n\n    // 返回Optional的函数\n    Optional&lt;T&gt; min(Comparator&lt;? super T&gt; comparator);\n    Optional&lt;T&gt; max(Comparator&lt;? super T&gt; comparator);\n    Optional&lt;T&gt; findFirst();\n    Optional&lt;T&gt; findAny();\n\n    // 转换为数组\n    Object[] toArray();\n    &lt;A&gt; A[] toArray(IntFunction&lt;A[]&gt; generator);\n\n    // reduce系列函数\n    T reduce(T identity, BinaryOperator&lt;T&gt; accumulator);\n    Optional&lt;T&gt; reduce(BinaryOperator&lt;T&gt; accumulator);\n    &lt;U&gt; U reduce(U identity, BiFunction&lt;U, ? super T, U&gt; accumulator, BinaryOperator&lt;U&gt; combiner);\n    &lt;R&gt; R collect(Supplier&lt;R&gt; supplier, BiConsumer&lt;R, ? super T&gt; accumulator, BiConsumer&lt;R, R&gt; combiner);\n    &lt;R, A&gt; R collect(Collector&lt;? super T, A, R&gt; collector);\n\n    // java 9\n    default Stream&lt;T&gt; takeWhile(Predicate&lt;? super T&gt; predicate);\n    default Stream&lt;T&gt; dropWhile(Predicate&lt;? super T&gt; predicate);\n\n    // java 16\n    default &lt;R&gt; Stream&lt;R&gt; mapMulti(BiConsumer&lt;? super T, ? super Consumer&lt;R&gt;&gt; mapper);\n    default IntStream mapMultiToInt(BiConsumer&lt;? super T, ? super IntConsumer&gt; mapper);\n    default LongStream mapMultiToLong(BiConsumer&lt;? super T, ? super LongConsumer&gt; mapper);\n    default DoubleStream mapMultiToDouble(BiConsumer&lt;? super T, ? super DoubleConsumer&gt; mapper);\n    default List&lt;T&gt; toList();\n}\n</code></pre>\n<h1>Interface Stream.Builder<T> （java.util.stream）</h1>\n<p>此接口继承了 Consumer<T> 接口，没有静态方法。</p>\n<p>accept(T t) 方法将 t 添加至流，不返回值<br>add(T t) 方法将 t 添加至流，返回 Stream.Builder<T>，因此允许链式调用<br>添加元素后，调用 build() 方法，返回 Stream<T></p>\n<h1>Class Optional<T> （java.util）</h1>\n<p>Optional 容器包装了需要存储的值或对象，因此可以在不使用异常的情况下处理 null</p>\n<p>java8 <a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html</a><br>java17 <a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html</a></p>\n<h1>扩展阅读</h1>\n<p><a href=\"https://www.jishuchi.com/read/onjava8/12013\">https://www.jishuchi.com/read/onjava8/12013</a></p>\n","tags":["java"]},{"id":"java-io","url":"https://yieldray.fun/posts/java-io","title":"java流式IO","date_published":"2022-07-11T14:22:22.000Z","date_modified":"2022-07-11T14:22:22.000Z","content_text":"<h1>IO 流</h1>\n<p>注意！ 这里的 IO 流 不是 <code>java.util.stream.*</code> 包提供的流</p>\n<p>面向字节(java.io):</p>\n<ul>\n<li>InputStream</li>\n<li>OutputStream</li>\n</ul>\n<p>面向字符和基于 Unicode (java.io):</p>\n<ul>\n<li>Reader</li>\n<li>Writer</li>\n</ul>\n<p>同步非阻塞(java.nio):</p>\n<ul>\n<li>Buffer</li>\n</ul>\n<p>Package java.io<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/package-summary.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/package-summary.html</a></p>\n<p>Package java.nio<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/package-summary.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/package-summary.html</a></p>\n<h1>Scanner</h1>\n<p>java.util.Scanner 实用类可通过正则表达式从流中解析原始类型和字符串。<br>这个<em>流</em>，是通过构造函数传入 Scanner 类中的。</p>\n<pre><code class=\"language-java\">// 构造函数，这里 ? 表示可选\nScanner(File source, ?String charsetName)\nScanner(InputStream source, ?String charsetName)\nScanner(Path source, ?String charsetName)\nScanner(ReadableByteChannel source, ?String charsetName)\nScanner(Readable source)\nScanner(String source)\n</code></pre>\n<p>演示如下，具体 API，参阅<a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Scanner.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Scanner.html</a></p>\n<pre><code class=\"language-java\">Scanner sc = new Scanner(System.in);\nif (sc.hasNext()) {\n    String s = sc.next();\n}\n</code></pre>\n<h1>File</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/File.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/File.html</a><br>java.io.File 类，其构造函数传入文件路径，实例方法可获取文件信息</p>\n<pre><code class=\"language-java\">public class File implements Serializable, Comparable&lt;File&gt;\n{\n    public File(String pathname);\n    public File(String parent, String child);\n    public File(File parent, String child);\n    public File(URI uri);\n    public String getName();\n    public String getParent();\n    public File getParentFile();\n    public String getPath();\n    public boolean isAbsolute();\n    public String getAbsolutePath()\n    public File getAbsoluteFile()\n    public String getCanonicalPath() throws IOException;\n    public File getCanonicalFile() throws IOException;\n    public URI toURI();\n    public boolean canRead();\n    public boolean canWrite();\n    public boolean exists();\n    public boolean isDirectory();\n    public boolean isFile();\n    public boolean isHidden();\n    public long lastModified();\n    public long length();\n    public boolean createNewFile() throws IOException;\n    public boolean delete();\n    public void deleteOnExit();\n    public String[] list();\n    public String[] list(FilenameFilter filter);\n    public File[] listFiles();\n    public File[] listFiles(FilenameFilter filter);\n    public File[] listFiles(FileFilter filter);\n    public boolean mkdir();\n    public boolean mkdirs();\n    public boolean renameTo(File dest);\n    public boolean setLastModified(long time);\n    public boolean setReadOnly();\n    public boolean setWritable(boolean writable, boolean ownerOnly);\n    public boolean setWritable(boolean writable);\n    public boolean setReadable(boolean readable, boolean ownerOnly);\n    public boolean setReadable(boolean readable);\n    public boolean setExecutable(boolean executable, boolean ownerOnly);\n    public boolean setExecutable(boolean executable);\n    public boolean canExecute();\n    public static File[] listRoots();\n    public long getTotalSpace();\n    public long getFreeSpace();\n    public long getUsableSpace();\n    public static File createTempFile(String prefix, String suffix, File directory) throws IOException;\n    public static File createTempFile(String prefix, String suffix) throws IOException;\n    public int compareTo(File pathname);\n    public boolean equals(Object obj) {return (obj instanceof File file) ? compareTo(file) == 0 : false;};\n    public int hashCode();\n    public String toString() {return getPath();}\n    public Path toPath();\n}\n</code></pre>\n<h1>InputStream 抽象类</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/InputStream.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/InputStream.html</a></p>\n<pre><code class=\"language-java\">public abstract class InputStream implements Closeable {\n    public InputStream();\n    public abstract int read() throws IOException;\n    public int read(byte b[]) throws IOException;\n    public int read(byte b[], int off, int len) throws IOException;\n    public long skip(long n) throws IOException;\n    public int available() throws IOException;\n    public void close() throws IOException;\n    public synchronized void mark(int readlimit);\n    public synchronized void reset() throws IOException;\n    public boolean markSupported();\n    // ! 以下 API 自 java 9\n    public byte[] readAllBytes() throws IOException;\n    public byte[] readNBytes(int len) throws IOException;\n    public long transferTo(OutputStream out) throws IOException;\n    // ! 以下 API 自 java 11\n    public int readNBytes(byte[] b, int off, int len) throws IOException;\n    public static InputStream nullInputStream();\n    // ! 以下 API 自 java 12\n    public void skipNBytes(long n) throws IOException;\n}\n</code></pre>\n<p>FileInputStream 是该类是一个实现</p>\n<pre><code class=\"language-java\">InputStream input = new FileInputStream(&quot;path/to/file&quot;);\ntry {\n    int x = input.read();\n} finally {\n    input.close();\n}\n</code></pre>\n<p>使用 try(resource) 语法，自动 finally 并调用 close()</p>\n<pre><code class=\"language-java\">try (InputStream input = new FileInputStream(&quot;path/to/file&quot;)) {\n    int x = input.read();\n}// 自动 close (for java.lang.AutoCloseable)\n</code></pre>\n<p>System.in 也是 InputStream 的一个实现</p>\n<p>此外，还可以通过字节数组获取一个字节流</p>\n<pre><code class=\"language-java\">InputStream input = new ByteArrayInputStream(new byte[] { 72, 101, 108, 108, 111, 33 });\n</code></pre>\n<h1>OutputStream 抽象类</h1>\n<p><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/OutputStream.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/OutputStream.html</a></p>\n<pre><code class=\"language-java\">public abstract class OutputStream implements Closeable, Flushable {\n    public OutputStream();\n    public abstract void write(int b) throws IOException;\n    public void write(byte b[]) throws IOException;\n    public void write(byte b[], int off, int len) throws IOException;\n    public void flush() throws IOException;\n    public void close() throws IOException;\n    // ! 以下 API 自 java 11\n    public static OutputStream nullOutputStream()\n}\n</code></pre>\n<p>与 InputStream 相同，OutputStream 也有类似的实现类：FileOutputStream，ByteArrayOutputStream</p>\n<pre><code class=\"language-java\">try (OutputStream output = new FileOutputStream(&quot;path/to/file&quot;)) {\n    output.write(&quot;Hello,world!&quot;.getBytes(&quot;UTF-8&quot;));\n}\n/* -------------------------------------------------- */\nbyte[] data;\ntry (ByteArrayOutputStream output = new ByteArrayOutputStream()) {\n    output.write(&quot;Hello,&quot;.getBytes(&quot;UTF-8&quot;));\n    output.write(&quot;world!&quot;.getBytes(&quot;UTF-8&quot;));\n    data = output.toByteArray();\n}\nSystem.out.println(new String(data, &quot;UTF-8&quot;));\n</code></pre>\n<h1>序列化，反序列化</h1>\n<p>序列化指将 Java 对象转换为二进制数据，二进制数据可以视作 byte[]数组。<br>需要序列化的 Java 对象必须实现<code>java.io.Serializable</code>接口，即</p>\n<pre><code class=\"language-java\">class Clazz implements java.io.Serializable {\n    // ... ...\n}\n</code></pre>\n<p>注意，如果对象的属性是对象，属性对应类也必须实现 java.io.Serializable 接口。<br>java.io.Serializable 是一个空接口，仅用做标记，jvm 会为我们完成对象转换为二进制的操作</p>\n<p>具体操作由 ObjectInputStream 和 ObjectOutputStream 类提供</p>\n<h1>Reader 和 Writer 抽象类</h1>\n<p>Reader 和 Writer 抽象类 （与 InputStream 和 OutputStream 面向字节不同的是）提供了面向字符的接口<br>Java 提供的字符流类型 char 是 16 bits （2 bytes）的 Unicode 字符</p>\n<p>这两个抽象类可以方便地操作 字符串 <code>String</code> 和 字符数组 <code>char[]</code></p>\n<p>与 FileInputStream 和 FileOutputStream 对应的类有 FileReader 和 FileWriter</p>\n<p>参见 <a href=\"https://www.jishuchi.com/read/onjava8/12029\">https://www.jishuchi.com/read/onjava8/12029</a><br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Reader.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Reader.html</a><br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Writer.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Writer.html</a></p>\n<h1>将 InputStream/OutputStream 转换为 Reader/Writer</h1>\n<pre><code class=\"language-java\">// 假设其它API返回InputStream，但我们希望作为Reader处理（出于演示目的不使用FileReader）\nInputStream input = new FileInputStream(&quot;filename.txt&quot;);\nInputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8);\n\nOutputStream output = new FileOutputStream(&quot;filename.txt&quot;);\nOutputStreamWriter writer = new OutputStreamWriter(output);\n</code></pre>\n<h1>PrintStream 和 PrintWriter</h1>\n<p>java.io.OutputStream &lt;- java.io.FilterOutputStream &lt;- java.io.PrintStream</p>\n<p>PrintStream 类提供了两组重载方法，print 和 println，用于打印各种数据。System.out 和 System.err 都是该类的实现。</p>\n<hr>\n<p>java.io.Writer &lt;- java.io.PrintWriter</p>\n<p>PrintWriter 与 PrintStream 基本一致，区别主要在于其面向字符而非字节</p>\n<h1>FilterInputStream 和 FilterOutputStream</h1>\n<p>关于 FilterInputStream 和 FilterOutputStream 类，通过<em>装饰器模式</em>实现了对 InputStream 和 OutputStream 类的包装</p>\n<p>BufferedInputStream 和 BufferedOutputStream 类 ：提供了缓冲功能</p>\n<p>ZipInputStream<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/zip/ZipInputStream.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/zip/ZipInputStream.html</a></p>\n<p>GZIPInputStream<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/zip/GZIPInputStream.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/zip/GZIPInputStream.html</a></p>\n<p>JarInputStream<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/jar/JarInputStream.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/jar/JarInputStream.html</a></p>\n<pre><code class=\"language-java\">// 在java中，我们将一个更基础的Stream作为功能性Stream的构造函数的参数，来实现类似于管道的功能\nInputStream file = new FileInputStream(&quot;test.gz&quot;);\nInputStream buffered = new BufferedInputStream(file);\nInputStream gzip = new GZIPInputStream(buffered);\n// 在java中这种模式称作Filter模式\n\n// 如果我们自己要实现一个包装层，应当继承 FilterInputStream 抽象类，实现其方法\nclass MyInputStream extends FilterInputStream {}\n// 相较于直接继承其它已有的InputStream实现类，继承 FilterInputStream 类更有利于维护\n</code></pre>\n<p>文档：<a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/FilterInputStream.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/FilterInputStream.html</a></p>\n<h1>FilterReader 和 FilterWriter</h1>\n<p>BufferedReader 和 BufferedWriter 类 ：提供了缓存功能</p>\n<p>（类似于 FilterInputStream 和 FilterOutputStream）</p>\n<h1>Path, Paths, PathMatcher (glob 支持)</h1>\n<p>Package java.nio.file<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/package-summary.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/package-summary.html</a></p>\n<hr>\n<p>Path 类表示文件系统中的某一路径，并提供了一些实用方法用于解析该路径</p>\n<p>Paths 类提供了静态方法，用于将路径或 URI 类转换为 Path 类</p>\n<pre><code class=\"language-java\">Path Paths.get(String first, String... more)\nPath Paths.get(URI uri)\n</code></pre>\n<p>PathMatcher 类表示一个 glob 或 regex ，用于进行文件查找，提供一个实例方法 <code>matches(Path path)</code> 返回是否匹配</p>\n<pre><code class=\"language-java\">PathMatcher matcher = FileSystems.getDefault().getPathMatcher(&quot;glob:**/*.{tmp,txt}&quot;);\nFiles.walk(test).filter(matcher::matches).forEach(System.out::println);\n</code></pre>\n<h1>Files, FileSystem, FileSystems</h1>\n<p>Package java.nio.file<br><a href=\"https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/package-summary.html\">https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/package-summary.html</a></p>\n<hr>\n<p>Files 类提供了丰富的静态方法用于文件操作</p>\n<p>FileSystem 抽象类表示文件系统接口</p>\n<p>FileSystems 类提供了静态方法用于获取文件系统（返回 FileSystem 类）</p>\n<h1>参考</h1>\n<p>文件: <a href=\"https://www.jishuchi.com/read/onjava8/12016\">https://www.jishuchi.com/read/onjava8/12016</a><br>流式 IO: <a href=\"https://www.jishuchi.com/read/onjava8/12029\">https://www.jishuchi.com/read/onjava8/12029</a><br>标准 IO: <a href=\"https://www.jishuchi.com/read/onjava8/12036\">https://www.jishuchi.com/read/onjava8/12036</a><br>新 IO: <a href=\"https://www.jishuchi.com/read/onjava8/12032\">https://www.jishuchi.com/read/onjava8/12032</a><br><a href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1255945227202752\">https://www.liaoxuefeng.com/wiki/1252599548343744/1255945227202752</a></p>\n","tags":["java"]},{"id":"java-date-time","url":"https://yieldray.fun/posts/java-date-time","title":"java时间与日期","date_published":"2022-07-04T19:39:12.000Z","date_modified":"2022-07-04T19:39:12.000Z","content_text":"<h1>java.util.Date (Deprecated)</h1>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Date.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Date.html</a><br>该 API 已 Deprecated，不应在新项目中使用<br>此对象与 js 的 Date 对象实现基本一致</p>\n<pre><code class=\"language-java\">import java.text.SimpleDateFormat;\nimport java.util.*;\n\npublic class Main {\n    public static void main(String[] args) {\n        Date date = new Date();\n        println(&quot;年&quot;, date.getYear() + 1900); // 必须加上1900\n        println(&quot;月&quot;, date.getMonth() + 1);// 0~11，必须加上1\n        println(&quot;日&quot;, date.getDate()); // 1~31，不能加1\n        println(&quot;星期&quot;, date.getDay());\n        println(&quot;小时&quot;, date.getHours());\n        println(&quot;分钟&quot;, date.getMinutes());\n        println(&quot;秒钟&quot;, date.getSeconds());\n        println(&quot;格林威治时间&quot;, date.getTime());\n        println(&quot;UTC相对于当前时区的时间差值&quot;, date.getTimezoneOffset());\n        // 下面注释的方法java没有，是js实现\n        // println(date.getMilliseconds());\n        // println(date.getFullYear());\n        // println(date.getUTCDate());\n        // println(date.getUTCDay());\n        // println(date.getUTCFullYear());\n        // println(date.getUTCHours());\n        // println(date.getUTCMilliseconds());\n        // println(date.getUTCMinutes());\n        // println(date.getUTCMonth());\n        // println(date.getUTCSeconds());\n        // println(date.toTimeString());\n        // println(date.toUTCString());\n        // println(date.toLocaleTimeString());\n        println(date.toString());\n        println(date.toGMTString());\n        println(date.toLocaleString());\n        println(Date.UTC(0, 0, 0, 0, 0, 0));\n        // 一系列 date.setXXX 实例函数则不再赘述\n\n        var sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);\n        println(sdf.format(date));\n\n    }\n\n    static void println(Object... objs) {\n        Arrays.stream(objs).forEach(System.out::print);\n        System.out.println();\n    }\n}\n</code></pre>\n<h1>java.util.Calendar; java.util.TimeZone</h1>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/Calendar.html\">https://docs.oracle.com/javase/8/docs/api/java/util/Calendar.html</a><br><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/TimeZone.html\">https://docs.oracle.com/javase/8/docs/api/java/util/TimeZone.html</a></p>\n<pre><code class=\"language-java\">import java.text.SimpleDateFormat;\nimport java.util.*;\n\npublic class Main {\n    public static void main(String[] args) {\n        Calendar c = Calendar.getInstance();// 获取基于当前时间的Calendar实例对象（注意无法new此对象，因为构造函数是protected）\n        c.clear(); // 将此 Calendar 的给定日历字段值和时间值设置成未定义\n        c.set(Calendar.YEAR, 2022);\n        c.set(Calendar.MONTH, 7); // 设置为8月，0表示1月\n        c.set(Calendar.DATE, 2);\n        c.set(Calendar.HOUR_OF_DAY, 21);\n        c.set(Calendar.MINUTE, 22);\n        c.set(Calendar.SECOND, 23);\n        c.setTimeZone(TimeZone.getTimeZone(&quot;Asia/Shanghai&quot;));\n        System.out.println(new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;).format(c.getTime()));\n        // 2022-08-02 21:22:23\n    }\n\n    static void println(Object... objs) {\n        Arrays.stream(objs).forEach(System.out::print);\n        System.out.println();\n    }\n}\n</code></pre>\n<h1>java.time 包, java.time.format 包，自 Java 8</h1>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html\">https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html</a></p>\n<p>注意，在新 API 中，Month 的范围用 1~12 表示 1 月到 12 月，Week 的范围用 1~7 表示周一到周日。\n也就是说，不再有从零开始数的了，减少了心智负担。<br>API 实施 ISO-8601 标准。</p>\n<p>类</p>\n<ul>\n<li>Clock</li>\n<li>Duration</li>\n<li>Instant （时间戳）</li>\n<li>LocalDate</li>\n<li>LocalDateTime</li>\n<li>LocalTime</li>\n<li>MonthDay</li>\n<li>OffsetDateTime</li>\n<li>OffsetTime</li>\n<li>Period</li>\n<li>Year</li>\n<li>YearMonth</li>\n<li>ZonedDateTime</li>\n<li>ZoneId</li>\n<li>ZoneOffset</li>\n</ul>\n<p>枚举</p>\n<ul>\n<li>DayOfWeek</li>\n<li>Month</li>\n</ul>\n<p>异常</p>\n<ul>\n<li>DateTimeException</li>\n</ul>\n<p>此外，java.time.format 包提供一些类用于打印（格式化）和解析日期<br><a href=\"https://docs.oracle.com/javase/8/docs/api/java/time/format/package-summary.html\">https://docs.oracle.com/javase/8/docs/api/java/time/format/package-summary.html</a></p>\n<p>类</p>\n<ul>\n<li>DateTimeFormatter</li>\n<li>DateTimeFormatterBuilder</li>\n<li>DecimalStyle</li>\n</ul>\n<p>枚举</p>\n<ul>\n<li>FormatStyle</li>\n<li>ResolverStyle</li>\n<li>SignStyle</li>\n<li>TextStyle</li>\n</ul>\n<p>异常</p>\n<ul>\n<li>DateTimeParseException</li>\n</ul>\n","tags":["java"]},{"id":"linux-command-help","url":"https://yieldray.fun/posts/linux-command-help","title":"shell命令帮助","date_published":"2022-06-29T10:18:54.000Z","date_modified":"2022-06-29T10:18:54.000Z","content_text":"<h1>coreutils</h1>\n<p><a href=\"https://github.com/coreutils/coreutils\">https://github.com/coreutils/coreutils</a><br><a href=\"https://github.com/mirror/busybox\">https://github.com/mirror/busybox</a><br><a href=\"https://github.com/uutils/coreutils\">https://github.com/uutils/coreutils</a><br><a href=\"https://github.com/rustcoreutils/posixutils-rs\">https://github.com/rustcoreutils/posixutils-rs</a><br><a href=\"https://github.com/vlang/coreutils\">https://github.com/vlang/coreutils</a><br><a href=\"https://github.com/mystor/micro-coreutils\">https://github.com/mystor/micro-coreutils</a></p>\n<h1>shell 帮助格式</h1>\n<pre><code class=\"language-sh\">command [OPTION]...\ncommand [OPTION...]\ncommand [FLAGS] [OPTIONS]\n# 有子命令的\ncommand &lt;command&gt; [&lt;args&gt;]\ncommand &lt;command&gt; [options]\n</code></pre>\n<p>貌似 glib 中，占位符用全大写字母代替</p>\n<table>\n<thead>\n<tr>\n<th>参数修饰符</th>\n<th>说明</th>\n<th>示例</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>无修饰符</td>\n<td>原生参数</td>\n<td><code>git reset</code></td>\n</tr>\n<tr>\n<td>&lt;&gt;</td>\n<td>占位参数</td>\n<td><code>git help &lt;command&gt;</code></td>\n</tr>\n<tr>\n<td>[]</td>\n<td>可选组合</td>\n<td><code>git [--version]</code></td>\n</tr>\n<tr>\n<td>()</td>\n<td>必选组合</td>\n<td><code>command (--patch | -p)</code></td>\n</tr>\n<tr>\n<td>|</td>\n<td>互斥参数</td>\n<td><code>ps --help &lt;simple|list|output|threads|misc|all&gt;</code></td>\n</tr>\n<tr>\n<td>...</td>\n<td>可重复指定前一个参数</td>\n<td><code>mv [OPTION]... SOURCE... DIRECTORY</code></td>\n</tr>\n<tr>\n<td>--</td>\n<td>标记后续参数类型</td>\n<td><code>rm -- -h</code> 表示删除文件<code>-h</code></td>\n</tr>\n</tbody></table>\n<h1>通配符</h1>\n<p><a href=\"https://globster.xyz/\">https://globster.xyz/</a></p>\n<table>\n<thead>\n<tr>\n<th>符号</th>\n<th>说明</th>\n<th>示例</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>*</td>\n<td>占位处匹配任意长度字符串</td>\n<td>file_*.txt</td>\n</tr>\n<tr>\n<td>?</td>\n<td>占位处匹配一个字符</td>\n<td>file_?.txt</td>\n</tr>\n<tr>\n<td>[...]</td>\n<td>占位处匹配方括号内指定字符的一个</td>\n<td>file_[abc].txt</td>\n</tr>\n<tr>\n<td>[-]</td>\n<td>占位处匹配方括号内指定字符范围内的一个</td>\n<td>file_[a-z].txt</td>\n</tr>\n<tr>\n<td>[^...]</td>\n<td>占位处匹配非方括号内指定字符的其它一个字符</td>\n<td>file_[^a-z].txt</td>\n</tr>\n</tbody></table>\n<h1>管道</h1>\n<p>第一个命令的输出将作为第二个命令的输入</p>\n<pre><code class=\"language-sh\">ls | less\n</code></pre>\n<h1><a href=\"https://www.gnu.org/software/bash/manual/html_node/Redirections.html\">重定向</a></h1>\n<p>将程序默认的标准输入/输出重新定向至新的目标</p>\n<pre><code class=\"language-sh\">{{command}} &lt; {{file}}  # 命令输出重定向至文件\n{{command}} &gt; {{file}}  # 命令输出覆盖写入文件\n{{command}} &gt;&gt; {{file}} # 命令输出追加写入文件\n{{command}} 1&gt; {{file}} # 1 代表stdout\n{{command}} 2&gt; {{file}} # 2 代表stderr\n{{command}} &amp;&gt; {{file}} # 也是 stderr\n\n# 将字符串重定向到标准输入\n{{command}} &lt;&lt;&lt; {{string}}\n\n{{command}} &lt;&lt; {{Heredoc}}\n    {{string}}\n{{Heredoc}}\n</code></pre>\n<h1>命令置换</h1>\n<p>命令置换是把一个命令的输出结果赋值到一个变量中</p>\n<pre><code class=\"language-sh\">ls `pwd`\nls $(pwd)\n</code></pre>\n<h1>行操作（快捷键）</h1>\n<p>设置快捷键方式</p>\n<pre><code class=\"language-sh\">set -o vi\nset -o emacs\n</code></pre>\n<p>参见 <a href=\"https://wangdoc.com/bash/readline\">https://wangdoc.com/bash/readline</a></p>\n<h1>配置项参数终止符</h1>\n<p>配置项参数终止符<code>--</code>，它的作用是告诉 Bash，在它后面的参数开头的<code>-</code>和<code>--</code>不是配置项，只能当作实体参数解释。</p>\n<pre><code class=\"language-sh\">echo &quot;hello,world&quot; &gt; -f\ncat -- -f\n# 输出 hello,world\n</code></pre>\n","tags":["linux","bash"]},{"id":"java-jdk","url":"https://yieldray.fun/posts/java-jdk","title":"各种jdk下载","date_published":"2022-06-28T12:41:11.000Z","date_modified":"2022-06-28T12:41:11.000Z","content_text":"<p>JDK 目前的 LTS 有三个：8，11，17<br><a href=\"https://www.injdk.cn/\">https://www.injdk.cn/</a><br><a href=\"http://www.codebaoku.com/jdk/jdk-index.html\">http://www.codebaoku.com/jdk/jdk-index.html</a></p>\n<h1>Oracle JDK</h1>\n<p><a href=\"https://www.oracle.com/java/technologies/downloads/\">https://www.oracle.com/java/technologies/downloads/</a><br><a href=\"https://www.oracle.com/java/technologies/downloads/archive/\">https://www.oracle.com/java/technologies/downloads/archive/</a><br><a href=\"https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html\">https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html</a></p>\n<p><a href=\"https://mirrors.huaweicloud.com/java/jdk/\">https://mirrors.huaweicloud.com/java/jdk/</a></p>\n<h1>Adoptium</h1>\n<p><a href=\"https://adoptium.net/zh-CN/\">https://adoptium.net/zh-CN/</a></p>\n<p><a href=\"https://mirror.tuna.tsinghua.edu.cn/help/adoptium/\">https://mirror.tuna.tsinghua.edu.cn/help/adoptium/</a><br><a href=\"https://mirrors.tuna.tsinghua.edu.cn/Adoptium/\">https://mirrors.tuna.tsinghua.edu.cn/Adoptium/</a></p>\n<h1>AdoptOpenJDK</h1>\n<p>AdoptOpenJDK 已经更名为 Adoptium</p>\n<p><a href=\"https://adoptopenjdk.net/\">https://adoptopenjdk.net/</a></p>\n<h1>OpenJDK</h1>\n<p><a href=\"https://openjdk.org/\">https://openjdk.org/</a></p>\n<h1>GraalVM</h1>\n<p><a href=\"https://www.graalvm.org/\">https://www.graalvm.org/</a></p>\n<h1>IBM Semeru Runtimes</h1>\n<p><a href=\"https://developer.ibm.com/languages/java/semeru-runtimes/downloads/\">https://developer.ibm.com/languages/java/semeru-runtimes/downloads/</a></p>\n<h1>Azul JDK</h1>\n<p><a href=\"https://www.azul.com/downloads/?package=jdk\">https://www.azul.com/downloads/?package=jdk</a></p>\n<h1>Liberica JDK</h1>\n<p><a href=\"https://bell-sw.com/pages/downloads/\">https://bell-sw.com/pages/downloads/</a></p>\n<h1>OpenJDK (Microsoft)</h1>\n<p><a href=\"https://docs.microsoft.com/zh-cn/java/openjdk/download\">https://docs.microsoft.com/zh-cn/java/openjdk/download</a></p>\n<h1>OpenJDK (OpenLogic)</h1>\n<p><a href=\"https://www.openlogic.com/openjdk-downloads\">https://www.openlogic.com/openjdk-downloads</a></p>\n<h1>OpenJDK (Red Hat)</h1>\n<p><a href=\"https://developers.redhat.com/products/openjdk/download\">https://developers.redhat.com/products/openjdk/download</a></p>\n<h1>Amazon Corretto</h1>\n<p><a href=\"https://aws.amazon.com/cn/corretto/\">https://aws.amazon.com/cn/corretto/</a></p>\n<h1>SapMachine</h1>\n<p><a href=\"https://sap.github.io/SapMachine/\">https://sap.github.io/SapMachine/</a></p>\n<h1>毕昇 JDK (华为)</h1>\n<p><a href=\"https://www.openeuler.org/zh/other/projects/bishengjdk/\">https://www.openeuler.org/zh/other/projects/bishengjdk/</a></p>\n<h1>Dragonwell (Alibaba)</h1>\n<p><a href=\"https://dragonwell-jdk.io/\">https://dragonwell-jdk.io/</a></p>\n<h1>Kona（腾讯）</h1>\n<p><a href=\"https://cloud.tencent.com/product/tkjdk\">https://cloud.tencent.com/product/tkjdk</a><br><a href=\"https://cloud.tencent.com/document/product/1149/38537\">https://cloud.tencent.com/document/product/1149/38537</a></p>\n","tags":["java","mirror","cdn"]},{"id":"java-regexp","url":"https://yieldray.fun/posts/java-regexp","title":"java正则表达式初步","date_published":"2022-06-26T14:28:10.000Z","date_modified":"2022-06-26T14:28:10.000Z","content_text":"<p>下面不会介绍正则表达式，只是介绍一下 java 关于正则表达式的 API</p>\n<p>有关正则表达式的 API 可能部署在 <code>String</code> 类上，或由 <code>java.util.regex</code> 包提供<br>这个包主要提供三个类 <code>Pattern</code> <code>Matcher</code> <code>PatternSyntaxException</code></p>\n<pre><code class=\"language-java\">import java.util.regex.*;\n\npublic class Main {\n    public static void main(String[] args) {\n        // Pattern.matches 静态方法\n        // boolean java.util.regex.Pattern.matches(String regex, CharSequence input)\n        println(Pattern.matches(&quot;abc&quot;, &quot;aabcccd&quot;)); // false\n        println(Pattern.matches(&quot;aabc&quot;, &quot;aabcccd&quot;)); // false\n        println(Pattern.matches(&quot;\\\\w+&quot;, &quot;aabcccd&quot;)); // true\n\n        // String.matches 实例方法\n        // boolean java.lang.String.matches(String regex)\n        println(&quot;aabcccd&quot;.matches(&quot;\\\\w+&quot;)); // true\n\n        // Pattern.compile 工厂函数\n        // Pattern java.util.regex.Pattern.compile(String regex)\n        // ! 下面展示 Matcher 类上的实例方法\n        Pattern p = Pattern.compile(&quot;\\\\w+&quot;);\n        Matcher m = p.matcher(&quot;Evening is full of the linnet&#39;s wings&quot;);\n\n        // use Matcher.find()\n        while (m.find()) { // m.find(0)\n            System.out.print(m.group() + &quot; &quot;);\n        }\n        println(&quot;\\n---&quot;);\n\n        // use Matcher.find(int start)\n        m.reset();\n        int i = 0;\n        while (m.find(i)) {\n            System.out.print(m.group() + &quot; &quot;);\n            i++;\n        }\n        println(&quot;\\n---&quot;);\n\n        // use Matcher.reset(CharSequence input)\n        m.reset(&quot;abc123&quot;);\n        m.find();\n        println(m.group());\n        println(&quot;---&quot;);\n\n        // use Matcher.group(int group)\n        m = Pattern.compile(&quot;(aa)(b)(ccc)d&quot;).matcher(&quot;aabcccd&quot;);\n        if (m.find()) {\n            println(m.group(0)); // aabcccd\n            println(m.group(1)); // aa\n            println(m.group(2)); // b\n            println(m.group(3)); // ccc\n\n            println(m.start()); // 0 第n个组的起始位置\n            println(m.start(0)); // 0\n            println(m.start(1)); // 0\n            println(m.start(2)); // 2\n\n            println(m.end()); // 7 第n个组的末尾位置\n            println(m.end(0)); // 7\n            println(m.end(1)); // 2\n            println(m.end(2)); // 3\n\n        }\n\n        // 其它\n        println(String.join(&quot;-&quot;, &quot;a,b.c,d.e&quot;.split(&quot;,|\\\\.&quot;))); // a-b-c-d-e\n        // not replace, but replaceFirst\n        println(&quot;a0c&quot;.replaceFirst(&quot;\\\\d&quot;, &quot;b&quot;)); // abc\n        println(&quot;a0c1&quot;.replaceAll(&quot;\\\\d&quot;, &quot;b&quot;)); // abcb\n\n        // Pattern 标记\n        Pattern pt = Pattern.compile(&quot;^java&quot;,\n                Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);\n    }\n\n    public static void println(Object o) {\n        System.out.println(o);\n    }\n}\n</code></pre>\n","tags":["java"]},{"id":"web-components","url":"https://yieldray.fun/posts/web-components","title":"WebComponents指南","date_published":"2022-06-23T16:40:52.000Z","date_modified":"2022-06-23T16:40:52.000Z","content_text":"<h1>tips</h1>\n<p>兼容性：<a href=\"https://caniuse.com/custom-elementsv1\">https://caniuse.com/custom-elementsv1</a><br>网址：<a href=\"https://www.webcomponents.org/\">https://www.webcomponents.org/</a><br>网站上面有一些 webComponents 写的组件（npm），和 polyfill 等资源，还是比较实用的<br>MDN：<a href=\"https://developer.mozilla.org/docs/Web/Web_Components\">https://developer.mozilla.org/docs/Web/Web_Components</a><br>Standard：<a href=\"https://html.spec.whatwg.org/multipage/custom-elements.html\">https://html.spec.whatwg.org/multipage/custom-elements.html</a></p>\n<p>我们自定义的元素是直接继承 HTMLElement 的，所以根据需要查阅<a href=\"https://developer.mozilla.org/docs/Web/API/HTMLElement\">https://developer.mozilla.org/docs/Web/API/HTMLElement</a></p>\n<h1>入门</h1>\n<p>本篇仅介绍 Autonomous custom elements，即直接继承 HTMLElement 类<br>此类自定义元素可直接作为 html 标签使用，如：<code>&lt;my-element&gt;</code></p>\n<pre><code class=\"language-js\">class MyElement extends HTMLElement {}\ncustomElements.define(&quot;my-element&quot;, MyElement);\n</code></pre>\n<p>另一种则是 Customized built-in elements，继承其它特定的 HTML 元素<br>需要通过 is 属性指定，如：<code>&lt;button is=&quot;plastic-button&quot;&gt;Click Me!&lt;/button&gt;</code></p>\n<pre><code class=\"language-js\">class PlasticButton extends HTMLButtonElement {}\ncustomElements.define(&quot;plastic-button&quot;, PlasticButton, { extends: &quot;button&quot; });\n</code></pre>\n<h2>定义元素</h2>\n<p>自定义的元素名称不分大小写，且必须包含连字符<code>-</code>，避免与原生元素冲突。如果我们指定的名称无效，会报 SyntaxError 错误。<br>标准描述的允许的自定义元素名称：<a href=\"https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name\">https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name</a><br>API 参见：<a href=\"https://developer.mozilla.org/docs/Web/API/Window/customElements\">https://developer.mozilla.org/docs/Web/API/Window/customElements</a></p>\n<p><img src=\"https://s2.loli.net/2022/06/23/XDoCJQRhMGT1cIv.png\" alt=\"1.png\"><br><code>customElements.define(&quot;my-element&quot;, class extends HTMLElement {...})</code> 后还可以通过 <code>customElements.get(&#39;my-element&#39;)</code> 获取这个类</p>\n<blockquote>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/CustomElementRegistry\"><code>CustomElementRegistry</code></a> 接口向开发者暴露了管理自定义元素的接口，该接口提供全局变量 <code>customElements</code> 暴露</p>\n</blockquote>\n<hr>\n<p>WebComponents 提供了 <a href=\"https://developer.mozilla.org/docs/Web/HTML/Element/template\">template 标签</a> ，这个元素写在 html 中<strong>不会被渲染</strong>（相当于 <code>display: none</code>），可以提供给 WebComponents 作为模板。</p>\n<p><img src=\"https://s2.loli.net/2022/06/23/siJn3HtOrNUVlTz.png\" alt=\"2.png\"></p>\n<h2><code>:defined</code> 伪类</h2>\n<p>CSS 提供伪类 <a href=\"https://developer.mozilla.org/docs/Web/CSS/:defined\"><code>:defined</code></a> 来表示任何已定义的元素。包括所有浏览器内置标准元素以及<em>已成功定义的</em>自定义元素。</p>\n<p>例如</p>\n<pre><code class=\"language-css\">my-element:not(:defined) {\n}\n\nmy-element:defined {\n}\n</code></pre>\n<blockquote>\n<p><a href=\"https://html.spec.whatwg.org/multipage/custom-elements.html#upgrades\">upgrade</a>：浏览器将自定义标签从未识别到解析为自定义元素的过程</p>\n</blockquote>\n<p>可以配合 <code>customElements.whenDefined(name)</code> 方法使用。</p>\n<pre><code class=\"language-js\">customElements.whenDefined(&quot;my-element&quot;).then(() =&gt; {\n    // my-element is now defined\n});\n</code></pre>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/Web_Components/Using_shadow_DOM\">Shadow DOM</a></h2>\n<p>Shadow DOM 是独立的 API（指不是和 Custom Element 绑定的 API），允许将隐藏的 DOM 树（Shadow DOM Tree）附加到常规的 DOM 树中。<br>自定义元素本身是一个常规 DOM 节点。<a href=\"https://developer.mozilla.org/docs/Web/API/Element/attachShadow\"><code>Element.attachShadow()</code></a> 方法可将一个 Shadow Root 附加至此节点上，此时当前节点就成为一个 Shadow Host。</p>\n<blockquote>\n<p>注意：CustomElement 与 ShadowRoot 及其子节点都是独立的 DOM 节点。</p>\n</blockquote>\n<pre><code class=\"language-js\">const shadowRoot = element.attachShadow({ mode: &quot;open&quot; });\nconst shadowRoot = element.attachShadow({ mode: &quot;closed&quot; });\n\n// 如果设置为 open，则元素的 shadowRoot 属性指向该 shadowRoot\nconsole.assert(shadowRoot, element.shadowRoot);\n</code></pre>\n<p>Shadow DOM 与文档的主 DOM 树分开渲染。因此 Shadow DOM 内部的样式表局限在其内部，不会影响外部元素。（除了 <code>:focus-within</code>）</p>\n<blockquote>\n<p>注：样式表包括 <code>&lt;style&gt;</code> 和 <code>&lt;link rel=&quot;stylesheet&quot; href=&quot;&quot; /&gt;</code></p>\n</blockquote>\n<p>在 Shadow DOM 内部的 <code>&lt;style&gt;</code> 标签内可以通过 <code>:host</code> 选择器表示自身。</p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"Untitled\" src=\"https://codepen.io/YieldRay/embed/abqgPEy?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/abqgPEy\">\n  Untitled</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_scoping\">CSS scoping</a></h1>\n<p><code>:host</code> 或 <code>:host()</code> 选择器。选中当前自定义元素，参数可用于更具体地选中自定义元素。<br><code>:host-context()</code> 选择器。选中当前自定义元素，当且仅当自定义元素是参数所选中的元素的子元素。</p>\n<p>例如：</p>\n<pre><code class=\"language-css\">:host([data-dark-theme]) {\n    display: inline-block;\n    color: black;\n    background: white;\n}\n\n:host-context(body[data-dark-theme]) {\n    color: white;\n    background: black;\n}\n</code></pre>\n<blockquote>\n<p>注意：自定义元素继承自 HTMLElement，其默认 style 与 HTMLSpanElement 相同\n（简单来说就是要注意 :host 默认具有 display:inline）</p>\n</blockquote>\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/::slotted\"><code>::slotted()</code></a> 伪元素可用于选中通过插槽（slot）插入 shadow tree 的节点，并设置其样式。</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/CSS_shadow_parts\">CSS shadow parts</a></h1>\n<p>CSS shadow parts 模块定义了 <a href=\"https://developer.mozilla.org/docs/Web/CSS/::part\"><code>::part()</code></a>伪元素，可设置在 shadow host 上。通过这个伪元素可以将 shadow host 的选定元素暴露给外部页面来应用样式。</p>\n<p>默认情况下，shadow tree 中的元素只能在自己的 shadow root 内进行样式设置。CSS shadow parts 模块允许在构成自定义元素的 <code>&lt;template&gt;</code> 子元素上包含一个 part 属性，通过 <code>::part()</code> 伪元素将 shadow tree 节点暴露给外部样式设置。</p>\n<blockquote>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/ShadowRoot\">ShadowRoot</a> 接口还有一些其它属性：如 adoptedStyleSheets 和 styleSheets。<br>就像 <a href=\"https://developer.mozilla.org/docs/Web/API/Document\">Document</a> 接口的同名属性一样，可以用于操纵样式表<br>此处不展开说明</p>\n</blockquote>\n<h1><a href=\"https://dom.spec.whatwg.org/#concept-slot\">插槽</a></h1>\n<p>插槽允许将自定义元素中的常规 DOM 节点（myElement.innerHTML）渲染至 ShadowDOM 中（myElement.shadowRoot.innerHTML）</p>\n<p>在自定义标签内部的标签，指定 <code>slot</code> 属性为某一插槽名。</p>\n<pre><code class=\"language-html\">&lt;my-element&gt;\n    &lt;div slot=&quot;foo&quot;&gt;&lt;/div&gt;\n    &lt;div slot=&quot;foo&quot;&gt;&lt;/div&gt;\n&lt;/my-element&gt;\n</code></pre>\n<p>在模板（Shadow DOM）中，用 <a href=\"https://developer.mozilla.org/docs/Web/API/HTMLSlotElement\"><code>&lt;slot&gt;</code></a> 占位标签，指定 <code>name</code> 属性为该插槽名，则渲染时占位标签（在渲染上）替换为所有对应传入的标签（但在实际 DOM 树中的位置不变）。</p>\n<pre><code class=\"language-html\">&lt;template&gt;\n    &lt;p&gt;example&lt;/p&gt;\n    &lt;slot name=&quot;foo&quot;&gt;&lt;/slot&gt;\n&lt;/template&gt;\n</code></pre>\n<p>渲染表现如下</p>\n<pre><code class=\"language-html\">&lt;p&gt;example&lt;/p&gt;\n&lt;slot name=&quot;foo&quot;&gt;\n    &lt;div slot=&quot;foo&quot;&gt;&lt;/div&gt;\n    &lt;div slot=&quot;foo&quot;&gt;&lt;/div&gt;\n&lt;/slot&gt;\n</code></pre>\n<p>上面演示的是具名插槽，如果模板（Shadow DOM）中存在 <code>&lt;slot&gt;</code> 元素而未指定插槽名，则这个插槽被替换为自定义标签内部所有未指定 <code>slot</code> 属性的元素。</p>\n<blockquote>\n<p>其实相当于插槽名为空字符串。未指定 <code>slot=&quot;...&quot;</code> 则相当于 <code>slot=&quot;&quot;</code> （空字符串）；<br>而模板中未指定 <code>&lt;slot name=&quot;...&quot;&gt;</code> （即<code>&lt;slot&gt;</code>）则相当于<code>&lt;slot name=&quot;&quot;&gt;</code>（空字符串）。</p>\n</blockquote>\n<p>被插入元素的 <a href=\"https://developer.mozilla.org/docs/Web/API/Element/assignedSlot\">Element.assignedSlot</a> 属性返回其插入到的 <a href=\"https://developer.mozilla.org/docs/Web/API/HTMLSlotElement\">HTMLSlotElement</a> 元素。<br>而插槽元素的 <code>HTMLSlotElement.assignedElements({flatten:false/true})</code> 和 <code>HTMLSlotElement.assignedNodes({flatten:false/true})</code> 则方法可以分别获取插槽中插入的节点和元素集合</p>\n<blockquote>\n<p><code>flatten: true</code> 选项可用于拍平一层 slot 嵌套<br><code>flatten: true/false</code> 区别的演示，参见<a href=\"https://embed.plnkr.co/plunk/1HZL2KTguIyi8Jjr\">此处</a></p>\n</blockquote>\n<p>插槽中的<em>直接</em>节点（即不包括子节点）改变时（包括第一次插入），插槽元素触发 <a href=\"https://developer.mozilla.org/docs/Web/API/HTMLSlotElement/slotchange_event\"><code>slotchange</code></a></p>\n<p>模板（Shadow DOM）中还允许使用 <code>::slotted()</code> 伪元素选中因插槽插入而渲染的元素（而不是模板中兜底的元素）</p>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"slot\" src=\"https://codepen.io/YieldRay/embed/GRQVRvE?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/GRQVRvE\">\n  slot</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<h1><a href=\"https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions\">生命周期</a></h1>\n<p>生命周期函数的顺序如下：</p>\n<p>constructor -&gt; attributeChangedCallback -&gt; connectedCallback -&gt; disconnectedCallback -&gt; adoptedCallback -&gt; connectedCallback</p>\n<ul>\n<li><code>constructor</code>：当自定义标签 upgrade 为 custom element 时，被调用（无参数）</li>\n<li><code>connectedCallback</code>：每当 custom element 被插入文档 DOM 时，被调用（无参数）</li>\n<li><code>disconnectedCallback</code>：每当 custom element 从文档 DOM 中删除时，被调用（无参数）</li>\n<li><code>adoptedCallback</code>：每当 custom element 被移动到新的文档时，被调用（例如从主 document 移动到某个 iframe.contentDocument）</li>\n<li><a href=\"https://dom.spec.whatwg.org/#handle-attribute-changes\"><code>attributeChangedCallback</code></a>: 每当 custom element：change, add, remove, replace 自身 attribute 时，被调用</li>\n</ul>\n<p>根据<a href=\"https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance\">规范</a>，不应该在 constructor 里获取自定义元素（即 this）的 attribute 和子元素（因为在 upgrade 之前，它们根本不存在）</p>\n<p>规范建议：</p>\n<ul>\n<li><code>constructor</code> 只用于设置值的初始状态或默认值、设置事件监听器、创建 shadow root</li>\n<li>将工作尽可能推迟到 <code>connectedCallback</code> 回调（特别是获取资源和渲染操纵）。但要注意该回调是可以多次触发的，所以对于一些一次性的工作，需要额外的代码来防止多次运行</li>\n</ul>\n<h2>监听 attribute 变化</h2>\n<p>只有在静态属性 <code>observedAttributes</code> 数组中声明的 attribute 才能触发 <code>attributeChangedCallback</code> 回调</p>\n<p><code>attributeChangedCallback</code> 对每个变更的 attribute 依次触发，包括直接通过 html 提供的 attribute</p>\n<pre><code class=\"language-js\">/**\n * 注：typescript 没有提供这些生命周期函数的定义\n */\nabstract class CustomElement {\n    static observedAttributes?: string[];\n    constructor() {}\n    connectedCallback?(): void;\n    disconnectedCallback?(): void;\n    attributeChangedCallback?(name: string, oldVal: string | null, newVal: string | null): void;\n    adoptedCallback?(oldDocument: Document, newDocument: Document): void;\n}\n\nclass MyElement implements CustomElement {\n    static get observedAttributes() {\n        return [&quot;foo&quot;, &quot;bar&quot;];\n    }\n    // 或者：\n    // static observedAttributes = [&quot;foo&quot;, &quot;bar&quot;];\n\n    attributeChangedCallback(attr: string, oldVal: string | null, newVal: string | null) {\n        switch (attr) {\n            case &quot;foo&quot;:\n            // do something with &#39;foo&#39; attribute\n\n            case &quot;bar&quot;:\n            // do something with &#39;bar&#39; attribute\n        }\n    }\n}\n</code></pre>\n<h1>注意事项</h1>\n<p><code>Event</code> 接口新增 <code>composed</code> 属性和 <code>composedPath</code> 方法<br>当可冒泡的事件（<code>bubbles</code> 属性为 <code>true</code>）可从 <code>Shadow DOM</code> 传递到一般的 DOM 时<br><code>Event.composed</code> 为 <code>true，``Event.composedPath()</code> 则返回一个 <code>EventTarget</code> 对象数组，表示将在其上调用事件侦听器的对象<br>对于 <code>{ mode: &quot;open&quot; })</code> 的 <code>ShadowRoot</code>，数组可包含其内部子节点，例如： <code>[ p, ShadowRoot, open-shadow, body, html, HTMLDocument</code><br>对于 <code>{ mode: &quot;close&quot; })</code> 的 <code>ShadowRoot</code>，数组不含其内部子节点及 <code>ShadowRoot</code> 自身，例如：<code>[ closed-shadow, body, html, HTMLDocument</code></p>\n<p>自定义元素还允许与表单元素集成，参见：<a href=\"https://web.dev/articles/more-capable-form-controls\">https://web.dev/articles/more-capable-form-controls</a></p>\n","tags":["web-api"]},{"id":"css-selectors","url":"https://yieldray.fun/posts/css-selectors","title":"css选择器 :is(),not(),:where(),:has()","date_published":"2022-06-23T16:24:22.000Z","date_modified":"2022-06-23T16:24:22.000Z","content_text":"<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/:is\"><code>:is()</code></a></h1>\n<p>兼容性：<a href=\"https://caniuse.com/css-matches-pseudo\">https://caniuse.com/css-matches-pseudo</a><br>接受选择器参数列表，对其中每一个选择器，都等效于此伪类被替换为该选择器的效果<br>该选择器的优先级由列表中最高的优先级决定</p>\n<pre><code class=\"language-css\">:is(header, main, footer) p:hover {\n    color: red;\n    cursor: pointer;\n}\n\n/* 以上内容相当于以下内容 */\nheader p:hover,\nmain p:hover,\nfooter p:hover {\n    color: red;\n    cursor: pointer;\n}\n</code></pre>\n<p>也可以连续使用，比如</p>\n<pre><code class=\"language-css\">:is(a, b, c) :is(x, y, z) u {\n}\n</code></pre>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/:where\"><code>:where()</code></a></h1>\n<p>此选择器同 <code>:is()</code><br>但选择器优先级始终为 0，即拥有最低优先级</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/:not\"><code>:not()</code></a></h1>\n<p><code>:not()</code> 原本只支持一个选择器作为参数，现在也支持选择器参数列表。选中规则同 <code>:is()</code></p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/CSS/:has\"><code>:has()</code></a></h1>\n<p><code>:has()</code> 选中当前元素，当且仅当参数声明的 <em>相对选择器列表</em> 能够选中 <em>任意元素</em></p>\n<p>相对选择器列表参数是可容错的。通常在 CSS 中，选择器列表中的某个选择器无效时，那么整个列表则被视为无效。<br>当 :has() 选择器列表中的一个选择器无法解析时，不正确或不受支持的选择器将被忽略，而其他的则将被正常使用。</p>\n<pre><code class=\"language-html\">&lt;h1&gt;aqua&lt;/h1&gt;\n&lt;h2&gt;blue&lt;/h2&gt;\n\n&lt;style&gt;\n    h1 + h2 {\n        /* 紧接在 h1 后面的 h2 */\n        color: blue;\n    }\n\n    h1:has(+ h2) {\n        /* h1，当且仅当存在 h2 紧接这个 h1 后面*/\n        color: aqua;\n    }\n&lt;/style&gt;\n</code></pre>\n","tags":["css"]},{"id":"css-nth-child-nth-of-type","url":"https://yieldray.fun/posts/css-nth-child-nth-of-type","title":"css伪类:nth-child(),nth-of-type()","date_published":"2022-06-23T15:47:02.000Z","date_modified":"2022-06-23T15:47:02.000Z","content_text":"<h1>:nth-child(an+b)</h1>\n<p><code>seletor:nth-child(an+b)</code> 先获取 <code>seletor</code> 的所有<em>兄弟元素</em> （当然也包括自身），再获取其中第 <code>an+b</code> 个元素（n=0,1,2...），<strong>最后选中其中是 selector 的元素</strong></p>\n<p><img src=\"https://s2.loli.net/2022/06/23/J3fzn2Tw8ZSgW6L.png\" alt=\"1.png\"></p>\n<p>比方说下面的 <code>p</code>，是 <code>li</code> 的兄弟元素，也满足 <code>li:nth-child(4)</code>，但不会被选中</p>\n<p><img src=\"https://s2.loli.net/2022/06/23/QFTButAxXLcd3gV.png\" alt=\"2.png\"></p>\n<h1>nth-of-type</h1>\n<p><code>seletor:nth-of-type(an+b)</code> 则是获取 <code>seletor</code> 的所有是 <code>seletor</code> 的兄弟元素</p>\n<p><img src=\"https://s2.loli.net/2022/06/23/zqnOYMs78dQJWyl.png\" alt=\"3.png\"></p>\n","tags":["css"]},{"id":"java-functional","url":"https://yieldray.fun/posts/java-functional","title":"java函数式编程","date_published":"2022-06-21T10:34:44.000Z","date_modified":"2022-06-21T10:34:44.000Z","content_text":"<p>参考<a href=\"https://www.jishuchi.com/read/onjava8/12012\">https://www.jishuchi.com/read/onjava8/12012</a></p>\n<h1>Lambda，方法引用</h1>\n<pre><code class=\"language-java\">public class Main {\n    public static void main(String[] args) {\n        OneParam ref = Lambda::methodRef; // 静态方法的引用，类或者对象名::方法名\n        ref.func(123);\n\n        OneParam lambda = Lambda.one; // Lambda\n        lambda.func(456);\n\n        // 我们发现，方法引用和 Lambda 实际上都是函数式接口的实现，是实例对象而不是 Method（方法）\n        // 也就是说，等效于：\n        OneParam obj = new OneParam() {\n            @Override\n            public void func(int i) {\n                System.out.println(i);\n            }\n        };\n        obj.func(789);\n    }\n}\n\n// @FunctionalInterface 注解让编译器检查接口是否为函数式接口，因为函数式接口只有一个抽象方法\n// 其他的方法都是 default 方法或 static 方法或重载 Object 方法（如 boolean equals(Object obj)）\n@FunctionalInterface\ninterface OneParam {\n    // 这个接口内部只能有一个抽象方法，对应类型为 OneParam 的 lambda 表达式/方法引用\n    void func(int a);\n}\n// 总而言之，只有一个方法的接口，可以视作函数式接口\n\nclass Lambda {\n    static OneParam one = (int a) -&gt; System.out.println(a);\n\n    static void methodRef(int a) {\n        System.out.println(a);\n    }\n}\n</code></pre>\n<h1>绑定 this，构造函数引用</h1>\n<p>通过 <code>类名::new</code> 获取构造函数引用。</p>\n<pre><code class=\"language-java\">public class Main {\n    public static void main(String[] args) {\n        OneParam s = Lambda::say; // 普通（非静态）方法引用\n        s.func(new Lambda(), &quot;Hello&quot;); // 必须关联到一个对象，此时，函数式接口的第一个参数必须为绑定的对象\n\n        Con con = Lambda::new; // 构造函数引用\n        con.make().say(&quot;greet&quot;);\n    }\n}\n\n@FunctionalInterface\ninterface Con {\n    Lambda make();\n}\n\n@FunctionalInterface\ninterface OneParam {\n    void func(Lambda aThis, String s);\n}\n\nclass Lambda {\n    private String name = &quot;Lambda&quot;;\n\n    public void say(String greet) {\n        System.out.println(greet + &quot; &quot; + name);\n    }\n\n}\n</code></pre>\n<h1>java.util.function.*</h1>\n<p>标准库本身提供了一些实用函数式接口 API<br>详细内容参见文档<a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html\">https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html</a></p>\n<p>例如，<code>interface Function&lt;T, R&gt;</code>表示只有一个参数 T，返回 R 的方法：</p>\n<pre><code class=\"language-java\">import java.util.function.*;\npublic class Main {\n    public static void main(String[] args) {\n        Function&lt;String, Integer&gt; length = (String s) -&gt; s.length();\n        System.out.println(length.apply(&quot;Hello World&quot;)); // 11\n    }\n}\n</code></pre>\n<h1>流式编程，Stream API</h1>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html\">https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html</a></p>\n<pre><code class=\"language-java\">import java.util.*;\nimport java.util.stream.*;\nimport java.util.function.*;\n\npublic class Main {\n    public static void main(String[] args) {\n        // 给定初值和操作函数生成流\n        Stream&lt;Integer&gt; s1 = Stream.iterate(0, i -&gt; i + 1);\n        // 生成含一个或多个元素的流\n        Stream&lt;Integer&gt; s2 = Stream.of(1, 2, 3, 4, 5);\n\n        // 通过Supplier生成流\n        Stream&lt;Integer&gt; s3 = Stream.generate(() -&gt; (int) (Math.random() * 100));\n        Stream&lt;Integer&gt; s4 = Stream.generate(new Supplier&lt;Integer&gt;() {\n            int n = 0;\n\n            public Integer get() {\n                n++;\n                return n;\n            }\n        });\n\n        // 通过集合生成流\n        Stream&lt;String&gt; s5 = Arrays.stream(new String[] { &quot;A&quot;, &quot;B&quot;, &quot;C&quot; });\n        Stream&lt;String&gt; s6 = List.of(&quot;X&quot;, &quot;Y&quot;, &quot;Z&quot;).stream();\n\n        s1.limit(10).forEach(System.out::print);\n        System.out.println();\n        s2.limit(10).forEach(System.out::print);\n        System.out.println();\n        s3.limit(10).forEach(System.out::print);\n        System.out.println();\n        s4.limit(10).forEach(System.out::print);\n        System.out.println();\n        s5.limit(10).forEach(System.out::print);\n        System.out.println();\n        s6.limit(10).forEach(System.out::print);\n        System.out.println();\n\n        // 合并两个流\n        Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5)).forEach(System.out::print);\n\n        // 转换为数组\n        Integer[] a1 = Stream.of(1, 2, 3, 4, 5).toArray(Integer[]::new);\n        // 转换为集合\n        List&lt;Integer&gt; a2 = Stream.of(1, 2, 3, 4, 5).collect(Collectors.toList());\n        Set&lt;Integer&gt; a3 = Stream.of(1, 2, 3, 4, 5).collect(Collectors.toSet());\n        Map&lt;Integer, Integer&gt; a4 = Stream.of(1, 2, 3, 4, 5).collect(Collectors.toMap(i -&gt; i, i -&gt; i));\n\n        Map&lt;String, List&lt;String&gt;&gt; a5 = Stream\n                .of(&quot;Apple&quot;, &quot;Banana&quot;, &quot;Blackberry&quot;, &quot;Coconut&quot;, &quot;Avocado&quot;, &quot;Cherry&quot;, &quot;Apricots&quot;)\n                .collect(Collectors.groupingBy(s -&gt; s.substring(0, 1), Collectors.toList()));\n\n    }\n}\n</code></pre>\n<p>有关更多消费流的接口，参见<a href=\"/posts/java-stream/\">java 流</a></p>\n","tags":["java"]},{"id":"java-generics","url":"https://yieldray.fun/posts/java-generics","title":"java泛型","date_published":"2022-06-20T20:22:34.000Z","date_modified":"2022-06-20T20:22:34.000Z","content_text":"<p>Java 泛型的实现是 type erasure（类型擦拭），也就是运行时做 cast （转型）</p>\n<p>Java 常用泛型参数有</p>\n<ul>\n<li>E - Element （集合元素）</li>\n<li>T - Type（类）</li>\n<li>K - Key（键）</li>\n<li>V - Value（值）</li>\n<li>N - Number（数值类型）</li>\n<li>? - 表示不确定的 java 类型</li>\n</ul>\n<pre><code class=\"language-java\">public class Main {\n    public static void main(String[] args) {\n        System.out.println(Pair.makePair(&quot;Hello&quot;, &quot;World&quot;));\n    }\n}\n\nclass Pair&lt;T, U&gt; {\n    public T first;\n    public U second;\n\n    public Pair(T first, U second) {\n        this.first = first;\n        this.second = second;\n    }\n\n    public static &lt;T, U&gt; Pair&lt;T, U&gt; makePair(T first, U second) {\n        return new Pair&lt;T, U&gt;(first, second);\n    }\n\n    @Override\n    public String toString() {\n        return &quot;(&quot; + first + &quot;, &quot; + second + &quot;)&quot;;\n    }\n}\n</code></pre>\n<p>擦拭法意味着实际只存在 <code>Pair</code> 这一个类，而不存在 <code>Pair&lt;String, String&gt;</code><br>注意这与 C++ 模板不同</p>\n<pre><code class=\"language-java\">var cls1 = Pair.class;\n// 错误： var cls2 = Pair&lt;String, String&gt;.class;\n</code></pre>\n<p>可以看作是泛型参数都视作 Object，用实参替换时编译器自动做了安全的转型</p>\n<pre><code class=\"language-java\">var pair = Pair.makePair(&quot;Hello&quot;, &quot;World&quot;);\n\nvar what = pair.second;\n//  隐式转型\nvar what = (String) pair.second;\n</code></pre>\n<p>我们发现，<strong>泛型参数只能是对象</strong>，不能为原始类型<br>此时 Java 也不会为原始类型做自动装箱操作，这可能是出于对兼容性的考虑<br>无法实例化泛型参数<code>new T()</code>，因为参数将变为 Object，结果变为实例化 Object 对象</p>\n<h1>通配符</h1>\n<p>Producer Extends, Consumer Super<br>extends 只读（以父类形式读取，仅能写入 null），super 只写（以子类形式写入，仅能以 Object 形式获取）</p>\n<pre><code class=\"language-java\">&lt;T&gt;\n// 表示指定类型 T\n\n&lt;?&gt;\n// 表示任意类型，是所有 &lt;T&gt; 类型的父类\n\n&lt;? extends Number&gt;\n// 表示 Number 及其子类\n\n&lt;? super Integer&gt;\n// 表示 Integer 及其父类\n</code></pre>\n","tags":["java"]},{"id":"java-annotation","url":"https://yieldray.fun/posts/java-annotation","title":"java注解","date_published":"2022-06-20T11:35:24.000Z","date_modified":"2022-06-20T11:35:24.000Z","content_text":"<p><strong>Java 注解是特殊的 interface</strong><br>注解作用于：类、方法、字段、参数以及注解本身等等<br>Java 作为编译型解释语言，有的注解可以在编译时生效，有的可以在运行时生效<br>注解可以有参数及默认参数，参数必须为常量，且只能为：基本类型、String、Class 以及枚举的数组</p>\n<h1>注解的作用位置：</h1>\n<pre><code class=\"language-java\">public enum ElementType {\n    TYPE,\n    FIELD,\n    METHOD,\n    PARAMETER,\n    CONSTRUCTOR,\n    LOCAL_VARIABLE,\n    ANNOTATION_TYPE,\n    PACKAGE,\n    TYPE_PARAMETER,\n    TYPE_USE,\n    MODULE,\n    RECORD_COMPONENT;\n}\n</code></pre>\n<h1>注解的生命周期：</h1>\n<pre><code class=\"language-java\">public enum RetentionPolicy {\n    /**\n     * Annotations are to be discarded by the compiler.\n     */\n    SOURCE,\n\n    /**\n     * Annotations are to be recorded in the class file by the compiler\n     * but need not be retained by the VM at run time.  This is the default\n     * behavior.\n     */\n    CLASS,\n\n    /**\n     * Annotations are to be recorded in the class file by the compiler and\n     * retained by the VM at run time, so they may be read reflectively.\n     *\n     * @see java.lang.reflect.AnnotatedElement\n     */\n    RUNTIME\n}\n</code></pre>\n<h1>声明注解</h1>\n<pre><code class=\"language-java\">import java.lang.annotation.*;\n\npublic class Main {\n    @AnnotationName(&quot;Example annotation value&quot;)\n    // 等效于 @AnnotationName(value = &quot;Example annotation value&quot;)\n    public String str = &quot;Hello&quot;;\n}\n\n// @Target 用于指定注解作用的位置。参数是ElementType[]，元素只有一个时可以省略数组写法\n// @Target(ElementType.TYPE)\n@Target({ // ElementType.TYPE, ElementType.METHOD, ElementType.FIELD,\n          // ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE,\n          // ElementType.ANNOTATION_TYPE, ElementType.PACKAGE, ElementType.TYPE_PARAMETER,\n          // ElementType.TYPE_USE\n        ElementType.METHOD,\n        ElementType.FIELD\n})\n// @Retention 用于指定注解的生命周期。参数默认为RetentionPolicy.CLASS，可选\n// RetentionPolicy.SOURCE, RetentionPolicy.CLASS, RetentionPolicy.RUNTIME\n@Retention(RetentionPolicy.RUNTIME)\n// @Repeatable(AnnotationNames.class)\n@Inherited // 允许子类继承父类的注解（子类继承父类时也同时使用了父类的注解）\n@Documented // 允许被javadoc工具获取\n// ... ...\n@interface AnnotationName {\n    int type() default 0; // 声明默认值，则在使用注解时可以不指定该值\n\n    String level() default &quot;info&quot;; // 若无默认值，则必须指定，例如 @AnnotationName(type = 1, level = &quot;Example&quot;)\n\n    String value() default &quot;&quot;; // 参数名称是value，且只有一个参数（其它参数可为默认值），可以省略参数名称，例如 @AnnotationName(&quot;Value&quot;)\n}\n\n// 若 AnnotationName 指定 @Repeatable 则还需要定义 AnnotationNames 注解\n// 其 @Retention 需指定为 RetentionPolicy.RUNTIME\n// 修饰 AnnotationName 的许多注解也必须用于修饰 AnnotationNames\n// 为了节省篇幅，下面没有重复指定与 AnnotationName 相同的注解\n@Retention(RetentionPolicy.RUNTIME)\n@interface AnnotationNames {\n    AnnotationName[] value();\n}\n</code></pre>\n<h1>获取注解</h1>\n<p>下面只考虑 RUNTIME 类型的注解，需要使用反射 API 获取<br>注解的完整类名为 java.lang.annotation.Annotation</p>\n<pre><code class=\"language-java\">// 形式化语法，不是Java代码\nclass Class,Method,Field,Constructor {\n    // 顾名思义，这个 annotationClass 就是注解对应的 Class 对象\n    public &lt;A extends Annotation&gt; A getAnnotation(Class&lt;A&gt; annotationClass);\n    public &lt;T extends Annotation&gt; T getAnnotation(Class&lt;T&gt; annotationClass);\n}\n</code></pre>\n<p>example from <a href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1265102026065728\">https://www.liaoxuefeng.com/wiki/1252599548343744/1265102026065728</a></p>\n<pre><code class=\"language-java\">@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface Range {\n    int min() default 0;\n    int max() default 255;\n}\n\n\npublic class Person {\n    @Range(min=1, max=20)\n    public String name;\n\n    @Range(max=10)\n    public String city;\n}\n\n\nvoid check(Person person) throws IllegalArgumentException, ReflectiveOperationException {\n    // 遍历所有Field:\n    for (Field field : person.getClass().getFields()) {\n        // 获取Field定义的@Range:\n        Range range = field.getAnnotation(Range.class);\n        // 如果@Range存在:\n        if (range != null) {\n            // 获取Field的值:\n            Object value = field.get(person);\n            // 如果值是String:\n            if (value instanceof String) {\n                String s = (String) value;\n                // 判断值是否满足@Range的min/max:\n                if (s.length() &lt; range.min() || s.length() &gt; range.max()) {\n                    throw new IllegalArgumentException(&quot;Invalid field: &quot; + field.getName());\n                }\n            }\n        }\n    }\n}\n</code></pre>\n","tags":["java"]},{"id":"java-reflect","url":"https://yieldray.fun/posts/java-reflect","title":"java反射","date_published":"2022-06-19T16:22:22.000Z","date_modified":"2022-06-19T16:22:22.000Z","content_text":"<p>扩展阅读 <a href=\"https://zhuanlan.zhihu.com/p/32286740\">https://zhuanlan.zhihu.com/p/32286740</a></p>\n<h1>Class 对象</h1>\n<p>Java 原始类型： char、boolean、byte、short、int、long、float、double<br>Java 中除了原始类型之外的所有的一切类型都是对象（引用类型）。</p>\n<p>反射就是在运行时获取对象类型（而不是对象实例，可以说是类）信息的 API 。\n注意：反射 API 也可以获取基本类型（原始类型）的信息（另外，Java 数组也是对象）。</p>\n<p>使用时需导入 <code>import java.lang.reflect.*;</code></p>\n<p>对于每一个对象类型（类），jvm 都在加载时为其创建对应的 <code>Class&lt;T&gt;</code> 对象<br>只有 jvm 才可以构造这个 <code>Class</code> 对象，因为构造函数被修饰为 private ：<code>private Class(...)</code><br>获取这个对象的方法有三种：</p>\n<pre><code class=\"language-java\">// 从类型上获取\nClass&lt;String&gt; cls = String.class;\n\n// 从实例对象上获取，注意 &quot;Hello&quot; 在这里是 String 类型\nClass&lt;String&gt; cls = &quot;Hello&quot;.getClass();\n\n// 利用反射API，通过完整类名获取\nClass&lt;String&gt; cls = Class.forName(&quot;java.lang.String&quot;);\n</code></pre>\n<p>很明显，上面前两种方法是继承了 <code>Object</code> 。<br>特殊的是，基本类型（原始类型）也有对应的 <code>Class</code>，<br>数组类型也有对应的 <code>Class</code>，例如 <code>String[]</code>，它的类名是<code>[Ljava.lang.String</code>。<br>然而由于 Java 泛型参数不能是原始类型，看起来 <code>int.class</code> 和 <code>Integer.class</code> 的类型都是相同的，实际上还是不同的</p>\n<pre><code class=\"language-java\">Class&lt;Integer&gt; cls1 = int.class;\nClass&lt;Integer&gt; cls2 = Integer.class;\nSystem.out.println(cls1 == cls2); // false\n\n// 也可以\nClass&lt;?&gt; cls = int.class;\n// 或者\nvar cls = int.class;\n</code></pre>\n<p><code>Class&lt;T&gt;</code> 类的具体方法，参阅 Java8 文档<a href=\"https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html\">https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html</a><br>下面参考<a href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512\">https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512</a><br>简略描述一下 <code>Class&lt;T&gt;</code> 类的一些公开方法，具体内容参见文档</p>\n<h1>Field 对象</h1>\n<p>获取 Field 对象</p>\n<pre><code class=\"language-java\">/*...*/ class Class&lt;T&gt; /*...*/ {\n    // 获取公开字段，包括继承字段\n    public Field getField(String name) throws NoSuchFieldException, SecurityException;\n    // 获取所有非继承字段\n    public Field getDeclaredField(String name) throws NoSuchFieldException, SecurityException;\n    public Field[] getFields() throws SecurityException;\n    public Field[] getDeclaredFields() throws SecurityException;\n}\n</code></pre>\n<p>Field 类</p>\n<pre><code class=\"language-java\">class Field /*...*/ {\n    public void setAccessible(boolean flag);\n    public String getName(); // 返回字段名称\n    public Class&lt;?&gt; getDeclaringClass(); // 返回字段对应Class\n\n    public Class&lt;?&gt; getType(); // 返回字段类型对应Class\n    public int getModifiers();  // 返回字段类型对应Java语言修饰符，配合Modifier类使用\n\n    public Object get(Object obj); // 获取字段在指定实例对象上的值\n    public /*boolean|byte|char|short|int|long|float|double*/ get/*Boolean|Byte|Char|Short|Int|Long|Float|Double*/(Object obj);\n    // e.g. public boolean getBoolean(Object obj) throws IllegalArgumentException, IllegalAccessException;\n\n    public void set(Object obj, Object value);// 设置字段在指定实例对象上的值为给定值\n    public void set/*Boolean|Byte|Char|Short|Int|Long|Float|Double*/(Object obj, /*boolean|byte|char|short|int|long|float|double*/ x);\n    // e.g. public void setBoolean(Object obj, boolean z);\n\n    public &lt;T extends Annotation&gt; T getAnnotation(Class&lt;T&gt; annotationClass);\n    public &lt;T extends Annotation&gt; T[] getAnnotationsByType(Class&lt;T&gt; annotationClass);\n    public Annotation[] getDeclaredAnnotations();\n    public AnnotatedType getAnnotatedType();\n}\n</code></pre>\n<p>演示</p>\n<pre><code class=\"language-java\">Field f = String.class.getDeclaredField(&quot;value&quot;);\nf.setAccessible(true); // 设置允许访问非public字段\nf.getName(); // &quot;value&quot;\nf.getType(); // class [B 表示byte[]类型\nint m = f.getModifiers();\nModifier.isFinal(m); // true\nModifier.isPublic(m); // false\nModifier.isProtected(m); // false\nModifier.isPrivate(m); // true\nModifier.isStatic(m); // false\n</code></pre>\n<p><a href=\"https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html\">https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html</a></p>\n<h1>Method 对象</h1>\n<p>获取 Method 对象</p>\n<pre><code class=\"language-java\">/*...*/ class Class&lt;T&gt; /*...*/ {\n    // 通过方法名及参数类型获取方法\n    public Method getMethod(String name, Class&lt;?&gt;... parameterTypes) throws NoSuchMethodException, SecurityException;\n    public Method getDeclaredMethod(String name, Class&lt;?&gt;... parameterTypes) throws NoSuchMethodException, SecurityException;\n    public Method[] getMethods() throws SecurityException;\n    public Method[] getDeclaredMethods() throws SecurityException;\n}\n</code></pre>\n<p>Method 类</p>\n<pre><code class=\"language-java\">public final class Method extends Executable {\n    public void setAccessible(boolean flag);\n    public String getName();\n    public int getModifiers();\n    public Class&lt;?&gt; getReturnType();\n    public Class&lt;?&gt;[] getParameterTypes();\n    public int getParameterCount();\n    public Class&lt;?&gt;[] getExceptionTypes();\n    // invoke的第一个参数是对象实例，即在哪个实例上调用该方法（相当于js的Function.prototype.call）\n    public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;\n}\n</code></pre>\n<h1>Constructor<T> 对象，调用构造方法</h1>\n<pre><code class=\"language-java\">// deprecated\ntry {\n    String str = String.class.newInstance();\n} catch (Exception e) {\n    e.printStackTrace();\n}\n</code></pre>\n<p>获取 Constructor 类</p>\n<pre><code class=\"language-java\">/*...*/ class Class&lt;T&gt; /*...*/ {\n    public Constructor&lt;T&gt; getConstructor(Class&lt;?&gt;... parameterTypes) throws NoSuchMethodException, SecurityException;\n    public Constructor&lt;?&gt;[] getConstructors() throws SecurityException;\n}\n</code></pre>\n<p>Constructor 类</p>\n<pre><code class=\"language-java\">public final class Constructor&lt;T&gt; extends Executable {\n    public void setAccessible(boolean flag);\n    public T newInstance(Object ... initargs);\n    // 判断cls是否继承当前类（如果cls可以assign为当前类，则cls可以向上转型为当前类。也就是说，这个方法判断cls能否向上转型为当前类）\n    public native boolean isAssignableFrom(Class&lt;?&gt; cls);\n}\n</code></pre>\n<p>演示</p>\n<pre><code class=\"language-java\">try {\n    Constructor&lt;Integer&gt; cons = Integer.class.getConstructor(int.class);\n    Integer n = (Integer) cons.newInstance(123);\n    System.out.println(n); // 123\n} catch (Exception e) {\n    e.printStackTrace();\n}\n</code></pre>\n<h1>获取继承关系</h1>\n<pre><code class=\"language-java\">/*...*/ class Class&lt;T&gt; /*...*/ {\n    // 获取父类的Class\n    public native Class&lt;? super T&gt; getSuperclass();\n    // 获取所有implement的interface的Class（不包括父类interface）\n    public Class&lt;?&gt;[] getInterfaces();\n}\n</code></pre>\n<h1>Proxy</h1>\n<p>Java 中的 Proxy 用于在运行期动态创建某个 interface 的实例<br>example</p>\n<pre><code class=\"language-java\">import java.lang.reflect.*;\n\npublic class Main {\n    public static void main(String[] args) {\n        // 1.构造调用处理器对象\n        InvocationHandler handler = new InvocationHandler() {\n            @Override\n            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n                if (method.getName() == &quot;morning&quot;) {\n                    System.out.println(&quot;Good morning, &quot; + args[0]);\n                }\n                return null;\n            }\n        };\n\n        // 2.构造代理对象\n        Hello hello = (Hello) Proxy.newProxyInstance(\n                Hello.class.getClassLoader(), // 传入ClassLoader\n                new Class[] { Hello.class }, // 传入要实现的接口\n                handler); // 传入处理调用方法的InvocationHandler\n\n        // 3.调用代理对象的方法\n        hello.morning(&quot;Bob&quot;);\n    }\n}\n\n// 接口定义\ninterface Hello {\n    void morning(String name);\n}\n</code></pre>\n<h1>关系</h1>\n<p>扩展阅读<a href=\"https://zhuanlan.zhihu.com/p/64584427\">https://zhuanlan.zhihu.com/p/64584427</a></p>\n<pre><code class=\"language-java\">public final class Class&lt;T&gt; implements java.io.Serializable,\n                              GenericDeclaration,\n                              Type,\n                              AnnotatedElement,\n                              TypeDescriptor.OfField&lt;Class&lt;?&gt;&gt;,\n                              Constable\n\npublic interface ParameterizedType extends Type\npublic interface GenericArrayType extends Type\npublic interface WildcardType extends Type\n\npublic class AccessibleObject implements AnnotatedElement\n\npublic final class Field extends AccessibleObject\n    implements Member\n\npublic abstract sealed class Executable extends AccessibleObject\n    implements Member, GenericDeclaration permits Constructor, Method\n\npublic final class Method extends Executable\npublic final class Constructor&lt;T&gt; extends Executable\n\n\n\n                      ┌────┐\n                      │Type│\n                      └────┘\n                         ▲\n                         │\n   ┌────────────┬────────┴─────────┬───────────────┐\n   │            │                  │               │\n┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐\n│Class││ParameterizedType││GenericArrayType││WildcardType│\n└─────┘└─────────────────┘└────────────────┘└────────────┘\n\n\n┌────────────────┐\n│AccessibleObject│\n└────────────────┘\n        ▲\n        │\n   ┌─────────┐\n   │         │\n┌─────┐┌──────────┐\n│Field││Executable│\n└─────┘└──────────┘\n            ▲\n            │\n       ┌──────────┐\n       │          │\n    ┌──────┐┌───────────┐\n    │Method││Constructor│\n    └──────┘└───────────┘\n</code></pre>\n","tags":["java"]},{"id":"java-package-import","url":"https://yieldray.fun/posts/java-package-import","title":"java封装:package,import,classpath,jar","date_published":"2022-06-15T14:41:23.000Z","date_modified":"2022-06-15T14:41:23.000Z","content_text":"<h1>package</h1>\n<p>java 的包是一种命名空间，包名按照惯例是需要以反向域名开头<br>例如：<code>net.example</code><br>然后具体名称跟在这个反向域名后面即可<br>例如：<code>net.example.name</code><br>在 java 中，一个文件只允许有一个公开类，这个类名<strong>必须</strong>与文件名相同<br>而如果我们要将一个类置于一个包（package）下，则源代码<strong>必须</strong>按照对应的文件路径存放<br>实际的名称只是<code>name</code>而已，所以无论是<code>net.example.name</code>还是<code>net.example.java.name</code>，在<code>.name</code>之前的都只是前缀，没有层次关系</p>\n<pre><code class=\"language-java\">// src/net/example/name/Hello.java, 编译后生成 bin/net/example/name/Hello.class\npackage net.example.name; // package 语句必须是源代码中的第一句实际代码，当然注释不是代码\npublic class Hello {\n    public Hello(){\n        System.out.println(&quot;Hello&quot;);\n    }\n}\n</code></pre>\n<p>完成上述操作，我们就在 net.example.name 包下提供了 Hello 类<br>需要注意的是，如果没有 package 语句，实际上也还是在包作用域下，只不过是“默认包”或者“未命名包”</p>\n<h1>import</h1>\n<p>如果要使用上面创建的 Hello 类，可以直接使用其完整名称<br>类似于 c++ 的命名空间，也可以导入（import）该包，之后就可以直接指定类名</p>\n<pre><code class=\"language-java\">new net.example.name.Hello();\n\nimport net.example.name.Hello; // 导入包下的指定类\nnew Hello();\n\nimport net.example.name.*; // 导入包下的所有类\n</code></pre>\n<h1>CLASSPATH</h1>\n<p>CLASSPATH 是环境变量，是一组路径的集合，用于在 import 时，查找 class 文件（而不是源代码）所在路径\n例如在 package 节里我们指定 <code>src/net/example/name/Hello.java</code>, 编译后生成 <code>bin/net/example/name/Hello.class</code><br>那就需要将 <code>bin/</code>加入 CLASSPATH。当然，也可以运行时指定（-classpath [env] / -cp [env]）或者在当前路径下运行程序（CLASSPATH 指定当前目录）</p>\n<p>假定使用 Windows 系统，有如下环境变量: <code>CLASSPATH=.;D:\\JAVA\\LIB;C:\\DOC\\Java</code> (linux 环境变量分隔符是冒号)<br>这里指定了三个路径，<code>.</code>当前路径;<code>D:\\JAVA\\LIB</code>java 标准库路径; <code>C:\\DOC\\Java</code>用户指定的路径<br>如果需要使用 jar 包，则 CLASSPATH 需指明 jar 包路径，例如：<code>CLASSPATH=.;D\\JAVA\\LIB;C:\\flavors\\grape.jar</code></p>\n<p>对于一个简单类名，查找这个类的方法很明显，就是先在当前包下查找，其次在 import 的包下查找，在次是在<code>java.lang</code>包下查找<br><code>java.lang</code>包是默认导入的</p>\n<h1>默认包</h1>\n<p>默认包就是在文件路径相同的情况下，即使没有 import，但同目录的 class 还是可以直接使用<br>这要求文件在同一个目录下，而且没有设置包名（即默认包），例如</p>\n<pre><code class=\"language-java\">// net/example/One.java\n// no `package net.example;`\npublic class One{}\n</code></pre>\n<pre><code class=\"language-java\">// net/example/Two.java\n// no `import net.example.one;`\npublic class Two{\n    public Two(){\n        new One();\n    }\n}\n</code></pre>\n<h1>jar</h1>\n<p>jar 包是 java 中进行包管理的一种手段<br>原理是将 class 文件打包在一起，例如<code>bin/net/example/name/</code>是包<code>net.example.name</code>包存放的路径<br>实际打包的就是<code>bin/</code>目录内部（不是<code>bin</code>目录自身）内部的所有文件<br>当然，还可以有<code>MANIFEST.MF</code>文件，用于指定<code>Main-Class</code>和其它信息</p>\n<p>例如</p>\n<pre><code class=\"language-sh\">java -cp ./name.jar net.example.Hello\njava -jar ./name.jar\n</code></pre>\n","tags":["java"]},{"id":"algorithm-qsort","url":"https://yieldray.fun/posts/algorithm-qsort","title":"快速排序:单/双边循环法、栈实现","date_published":"2022-06-08T19:24:51.000Z","date_modified":"2022-06-08T19:24:51.000Z","content_text":"<p>快速排序是不稳定的排序</p>\n<h1>双边循环法</h1>\n<pre><code class=\"language-ts\">function qsortWrapper1&lt;T = number&gt;(arr: T[], start?: number, end?: number): T[] {\n    function qsort&lt;T&gt;(arr: T[], start: number, end: number) {\n        if (start &gt;= end) return;\n        const pivotIndex = partition(arr, start, end);\n        qsort(arr, start, pivotIndex - 1);\n        qsort(arr, pivotIndex + 1, end);\n    }\n\n    // 双边循环法\n    function partition&lt;T&gt;(arr: T[], start: number, end: number): number {\n        const pivot = arr[start];\n        // 基准元素可以设置为任意元素，直接设为首元素即可\n        let left = start;\n        let right = end;\n        // left 和 right 指针首先指向数组两端\n\n        // 我们的最终目的是左边的元素比基准元素都小，右边的元素比基准元素都大\n        // 所以双边循环法对两端进行遍历，一旦找到右指针指向的元素比基准元素小，并且有左指针指向的元素比基准元素大\n        // 就将这两个指针指向的元素交换，终止条件很明显就是指针相等\n        while (left !== right) {\n            // 强调一下 left &lt; right 条件，保证左右指针的顺序\n            while (left &lt; right &amp;&amp; arr[right] &gt; pivot) right--;\n            while (left &lt; right &amp;&amp; arr[left] &lt;= pivot) left++;\n            // 这里的判断是保证指针相等时，不交换元素，不做无意义的交换\n            if (left &lt; right) [arr[left], arr[right]] = [arr[right], arr[left]];\n        }\n        // 最终，左右指针相等，指向一个比基准元素小的元素\n        arr[start] = arr[left];\n        arr[left] = pivot;\n        // 将指针指向的元素与基准元素交换 (这里用 left 指针和 right 指针皆可)\n        return left;\n    }\n\n    if (typeof start !== &quot;number&quot;) start = 0;\n    if (typeof end !== &quot;number&quot;) end = arr.length - 1;\n    qsort(arr, start, end);\n    return arr;\n}\n\nconsole.log(qsortWrapper1([1, 5, 8, 2, 4, 6, 3, 7, 0, 9]));\n</code></pre>\n<h1>单边循环法</h1>\n<pre><code class=\"language-ts\">function qsortWrapper2&lt;T = number&gt;(arr: T[], start?: number, end?: number): T[] {\n    function qsort&lt;T&gt;(arr: T[], start: number, end: number) {\n        if (start &gt;= end) return;\n        const pivotIndex = partition(arr, start, end);\n        qsort(arr, start, pivotIndex - 1);\n        qsort(arr, pivotIndex + 1, end);\n    }\n\n    // 单边循环法\n    function partition&lt;T&gt;(arr: T[], start: number, end: number): number {\n        const pivot = arr[start];\n        // 基准元素可以设置为任意元素，直接设为首元素即可\n\n        let mark = start; // mark 指针首先指向数组起始位置，我们的目标是 mark 指针指向小于基准元素的区域边界 (其指向的元素也小于基准元素)\n\n        for (let i = start + 1; i &lt;= end; i++)\n            if (arr[i] &lt; pivot) {\n                // 遍历的元素小于基准元素，则边界扩大了\n                // 令 mark 自增。当前 mark 指向的元素必然大于或等于遍历的元素，交换后保证小于基准元素的当前遍历元素在边界内部 (闭区间)\n                mark++;\n                [arr[mark], arr[i]] = [arr[i], arr[mark]];\n                // 快速排序是不稳定的排序，无需保证稳定性\n            }\n\n        arr[start] = arr[mark];\n        arr[mark] = pivot;\n        return mark;\n    }\n\n    if (typeof start !== &quot;number&quot;) start = 0;\n    if (typeof end !== &quot;number&quot;) end = arr.length - 1;\n    qsort(arr, start, end);\n    return arr;\n}\n\nconsole.log(qsortWrapper2([1, 5, 8, 2, 4, 6, 3, 7, 0, 9]));\n</code></pre>\n<h1>栈实现</h1>\n<p>partition 函数任选单/双边循环法</p>\n<pre><code class=\"language-ts\">interface Param {\n    start: number;\n    end: number;\n}\n\nfunction qsort&lt;T&gt;(arr: T[], start: number, end: number) {\n    const stack: Array&lt;Param&gt; = [{ start, end }]; // 栈初始化\n\n    while (stack.length &gt; 0) {\n        const param: Param = stack.pop()!; // stack 非空，断言必然成立\n        const pivotIndex = partition(arr, param.start, param.end);\n        if (param.start &lt; pivotIndex - 1) stack.push({ start, end: pivotIndex - 1 });\n        if (pivotIndex + 1 &lt; param.end) stack.push({ start: pivotIndex + 1, end });\n    }\n}\n</code></pre>\n","tags":["typescript","algorithm"]},{"id":"ts-utility","url":"https://yieldray.fun/posts/ts-utility","title":"TypeScript实用类","date_published":"2022-05-28T13:50:07.000Z","date_modified":"2022-05-28T13:50:07.000Z","content_text":"<p>在 typescript 的 playground 测试，观察其编译结果<br><a href=\"https://www.typescriptlang.org/play\">https://www.typescriptlang.org/play</a></p>\n<h1>namespace, declare</h1>\n<pre><code class=\"language-ts\">namespace test {\n    let inside: 111;\n    let outside: 222;\n    export const _const = 333;\n    export let _let = 444;\n    export var _var = 555;\n    // export outside; // 不行！！！\n    // Variable &#39;outside&#39; is used before being assigned\n}\n// 编译成如下代码\n// &quot;use strict&quot;;\n// var test;\n// (function (test) {\n//     let inside;\n//     let outside;\n//     test._const = 333;\n//     test._let = 444;\n//     test._var = 555;\n// })(test || (test = {}));\n// 可以看出，export 的 const,let,var 是由 typescript 进行检查的\n\ndeclare namespace jQuery {\n    // declare 仅仅是环境声明，不会编译成任何代码\n    export const jquery1: string;\n    let jquery2: string;\n}\n</code></pre>\n<p><a href=\"https://jkchao.github.io/typescript-book-chinese/typings/ambient.html\">https://jkchao.github.io/typescript-book-chinese/typings/ambient.html</a><br><a href=\"https://jkchao.github.io/typescript-book-chinese/project/namespaces.html\">https://jkchao.github.io/typescript-book-chinese/project/namespaces.html</a></p>\n<h1>Conditional</h1>\n<p>extends 操作符用于条件表达式中（的条件部分），仅用于 ts 判断，不返回类型<br>extends 也可以用于限制泛型参数<br>infer 通过占位匹配一个类型</p>\n<pre><code class=\"language-ts\">SomeType extends OtherType ? TrueType : FalseType;\n</code></pre>\n<pre><code class=\"language-ts\">type test1 = &quot;a&quot; | &quot;b&quot; extends &quot;a&quot; | &quot;b&quot; | &quot;c&quot; ? true : false; // =&gt; true\ntype test2 = &quot;a&quot; | &quot;b&quot; | &quot;c&quot; extends &quot;a&quot; | &quot;b&quot; ? true : false; // =&gt; false\ntype test3 = Function extends Object ? true : false; // =&gt; true\n\ntype ElementOf&lt;T&gt; = T extends Array&lt;infer E&gt; ? E : never;\n// ElementOf&lt;ArrType&gt;\n// 相当于\n// ArrType[number]\n</code></pre>\n<h1>Utility</h1>\n<p>阅读<a href=\"https://www.typescriptlang.org/docs/\">https://www.typescriptlang.org/docs/</a>的 Handbook 部分</p>\n<p>快速回顾</p>\n<pre><code class=\"language-ts\">interface P {\n    name: string;\n    age: number;\n    alive?: boolean;\n}\n\ntype a = P[&quot;name&quot;]; // string\ntype b = P[&quot;age&quot;]; // number\ntype c = P[&quot;alive&quot;]; // boolean\ntype d = keyof P; // &quot;name&quot; | &quot;age&quot; | &quot;alive&quot;\ntype e = P[keyof P]; // string | number | boolean\ntype f = { [K in keyof P]: P[K] }; // { name: string; age: number; alive?: boolean; }\ntype g = { [K in keyof P]?: P[K] }; // { name?: string; age?: number; alive?: boolean; }\ntype h = { [K in keyof P]+?: P[K] }; // { name?: string; age?: number; alive?: boolean; }\ntype i = { [K in keyof P]-?: P[K] }; // { name: string; age: number; alive: boolean; }\n\ntype anyKey = keyof any; //  string | number | symbol\ntype anyFunction = (...args: any[]) =&gt; any;\ntype anyConstructor = new (...args: any[]) =&gt; any;\ntype test1 = anyConstructor extends anyFunction ? true : false; // false\ntype test2 = anyFunction extends anyConstructor ? true : false; // false\n</code></pre>\n<p>Utility Types</p>\n<pre><code class=\"language-ts\">// 这里加上 $ 前缀防止与自带的类型冲突\ntype $Awaited&lt;T&gt; = 略;\ntype $Partial&lt;T&gt; = { [P in keyof T]?: T[P] };\ntype $Required&lt;T&gt; = { [P in keyof T]-?: T[P] }; // 这里的 - 表示移除 ? 标识，也就是全部设置为必选项。\ntype $Readonly&lt;T&gt; = { readonly [P in keyof T]: T[P] };\ntype $Record&lt;K extends keyof any, T&gt; = { [P in K]: T };\ntype $Pick&lt;T, K extends keyof T&gt; = { [P in K]: T[P] };\ntype $Exclude&lt;T, U&gt; = T extends U ? never : T;\ntype $Omit&lt;T, K extends keyof any&gt; = $Pick&lt;T, $Exclude&lt;keyof T, K&gt;&gt;;\ntype $Extract&lt;T, U&gt; = T extends U ? T : never;\ntype $NonNullable&lt;T&gt; = T extends null | undefined ? never : T;\ntype $NonNullable&lt;T&gt; = T &amp; {};\ntype $Parameters&lt;T extends (...args: any[]) =&gt; any&gt; = T extends (...args: infer P) =&gt; any ? P : never; // infer P 表示推断出参数的类型为 P (这里当然是所有参数构成的具名元组)\ntype $ConstructorParameters&lt;T extends new (...args: any[]) =&gt; any&gt; = T extends new (...args: infer P) =&gt; any\n    ? P\n    : never;\ntype $ReturnType&lt;T extends (...args: any[]) =&gt; any&gt; = T extends (...args: any[]) =&gt; infer R ? R : any; // infer R 表示推断出函数的返回类型为 R\ntype $InstanceType&lt;T extends new (...args: any[]) =&gt; any&gt; = T extends new (...args: any[]) =&gt; infer R ? R : any;\ntype $ThisParameterType&lt;T&gt; = T extends (this: infer U, ...args: any[]) =&gt; any ? U : unknown;\ntype $OmitThisParameter&lt;T&gt; =\n    unknown extends ThisParameterType&lt;T&gt; ? T : T extends (...args: infer A) =&gt; infer R ? (...args: A) =&gt; R : T;\ninterface $ThisType&lt;T&gt; {}\n\n// 配合模版字面量类型使用，intrinsic 表示是内部实现（通过 toLowerCase 和 toUpperCase 实现）\ntype Uppercase&lt;S extends string&gt; = intrinsic; // 字符串转为全大写\ntype Lowercase&lt;S extends string&gt; = intrinsic; // 字符串转为全小写\ntype Capitalize&lt;S extends string&gt; = intrinsic; // 字符串第一个字符转为大写\ntype Uncapitalize&lt;S extends string&gt; = intrinsic; // 字符串第一个字符转为小写\n</code></pre>\n<h1>拓展阅读</h1>\n<p><a href=\"https://www.typescriptlang.org/docs/handbook/utility-types.html\">https://www.typescriptlang.org/docs/handbook/utility-types.html</a><br><a href=\"https://github.com/zhongsp/TypeScript/blob/dev/zh/reference/utility-types.md\">https://github.com/zhongsp/TypeScript/blob/dev/zh/reference/utility-types.md</a><br><a href=\"https://jkchao.github.io/typescript-book-chinese/typings/neverType.html\">https://jkchao.github.io/typescript-book-chinese/typings/neverType.html</a><br><a href=\"https://jkchao.github.io/typescript-book-chinese/tips/infer.html\">https://jkchao.github.io/typescript-book-chinese/tips/infer.html</a><br><a href=\"https://github.com/sindresorhus/type-fest\">https://github.com/sindresorhus/type-fest</a><br><a href=\"https://learntypescript.dev/\">https://learntypescript.dev/</a></p>\n","tags":["typescript"]},{"id":"windows-mklink","url":"https://yieldray.fun/posts/windows-mklink","title":"windows创建链接命令mklink","date_published":"2022-05-16T22:28:10.000Z","date_modified":"2022-05-16T22:28:10.000Z","content_text":"<h1><a href=\"https://ss64.com/nt/mklink.html\">mklink</a></h1>\n<p>链接与<a href=\"https://ss64.com/nt/shortcut.html\">快捷方式</a>不同，快捷方式实际是 .lnk 文件。</p>\n<p>符号链接是最新且最灵活的链接类型 (首次在 Vista 中引入)，它们对于用户是透明的；这些链接显示为普通的 NTFS 文件或目录，并且用户或应用程序可以完全以相同的方式对其进行操作。<br>符号链接可以跨卷，并且可以使用 UNC 路径。 符号链接也是唯一一种可以设置为<em>相对</em>路径（在同一卷上）的链接类型。</p>\n<p>一个符号链接可以是一个绝对路径 C:\\Programs 或者一个相对于链接位置的路径 \\Programs。</p>\n<pre><code class=\"language-sh\">创建符号链接。\n\nMKLINK [[/D] | [/H] | [/J]] Link Target\n\n        /D      创建目录(Directory)符号链接。不带flag时为文件符号链接。\n        /H      创建硬链接(HardLink)而非符号链接。\n        /J      创建目录联接(Junction)。\n        Link    指定新的符号链接名称。\n        Target  指定新链接引用的路径(相对或绝对)。\n</code></pre>\n<p>/D 和 /J 选项区别：/J 在创建时指向原目录的绝对路径， /D 则自动指向源目录的路径，即使源目录移动</p>\n<h2>示例</h2>\n<pre><code class=\"language-sh\">mklink link File # 创建 link.symlink\nmklink link Folder # 文件管理器不显示为文件夹，而是文件，无法双击进入\nmklink /d linkFolder Folder # 文件管理器显示为文件夹，可以双击进入\nmklink /h hardLinkFile File # 文件夹无法创建硬链接\n</code></pre>\n<h2>链接的类型</h2>\n<blockquote>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>可以链接到文件吗？</th>\n<th>可以链接到文件夹吗？</th>\n<th>可以跨硬盘驱动器链接吗？</th>\n<th>可以指向不存在的目标吗？</th>\n<th>如何删除：</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><a href=\"https://ss64.com/nt/shortcut.html\">快捷方式</a></td>\n<td>是</td>\n<td>是</td>\n<td>是</td>\n<td>是</td>\n<td><a href=\"https://ss64.com/nt/del.html\">Del</a></td>\n</tr>\n<tr>\n<td>硬链接</td>\n<td>是</td>\n<td>否</td>\n<td>否</td>\n<td>否</td>\n<td><a href=\"https://ss64.com/nt/del.html\">Del</a></td>\n</tr>\n<tr>\n<td>连接 (软链接)</td>\n<td>否</td>\n<td>是</td>\n<td>是 (在同一计算机上)</td>\n<td>是</td>\n<td><a href=\"https://ss64.com/nt/rd.html\">RD</a></td>\n</tr>\n<tr>\n<td>符号链接</td>\n<td>是</td>\n<td>是</td>\n<td>是</td>\n<td>是</td>\n<td>RD <em>文件夹</em> 或者 Del <em>文件</em></td>\n</tr>\n</tbody></table>\n<p>符号链接和目录连接是使用<a href=\"https://docs.microsoft.com/windows/desktop/FileIO/reparse-points\">重新分析点</a>实现的。</p>\n<p>快捷方式文件除了链接到另一个文件之外，还有一些额外的功能：设置<a href=\"https://ss64.com/nt/syntax-elevate.html\">以管理员身份运行</a>标志，制作图标，使用参数调用可执行文件。</p>\n<p>硬链接是使用多个文件表条目实现的，这些条目指向同一个 inode – 与 Unix 硬链接相同。 如果原始文件名被删除，硬链接仍然可以工作 - 它直接指向磁盘上的数据。</p>\n<p>创建相互指向的循环链接或指向自身的链接是可能的（但不建议这样做）。 符号链接可能会暴露未设计为处理它们的应用程序中的安全漏洞。</p>\n<p>遗憾的是，在 Microsoft Windows 下，.zip 文件不支持硬链接或符号链接。</p>\n</blockquote>\n<h2>列出现有链接和连接</h2>\n<blockquote>\n<p>标准的 <a href=\"dir.html\">DIR</a> 命令将显示符号链接，用 <SYMLINKD> 指示。<br><a href=\"dir.html\">DIR /A:S</a> 命令将显示连接，用 <JUNCTION> 指示。</p>\n<p>DIR /A:S %userprofile%</p>\n</blockquote>\n<h2>权限提升</h2>\n<blockquote>\n<p>默认情况下，只有管理员才能创建符号链接。 可以在以下位置授予安全设置“创建符号链接”：配置\\Windows 设置\\安全设置\\本地策略\\用户权限分配\\</p>\n<p>创建符号链接需要权限提升，但是从 Windows 10 <a href=\"https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/\">版本 14972</a> 开始，可以创建符号链接而无需提升控制台为管理员 - 但是，这需要您启用<a href=\"https://docs.microsoft.com/windows/uwp/get-started/enable-your-device-for-development\">开发者模式</a>。</p>\n</blockquote>\n<blockquote>\n<p>备注：Win + R 输入 cmd 然后 CTRL + SHIFT + ENTER 就可以以管理员身份启动了</p>\n</blockquote>\n<h2>Windows 资源管理器 - 拖放</h2>\n<blockquote>\n<ul>\n<li>在 Windows 资源管理器中选择一个符号链接将选择原始目录。</li>\n<li>在 Windows 资源管理器中选择一个连接将选择该连接。</li>\n<li>将符号链接拖到 Windows 资源管理器中的新目录会将符号链接移动到新目录。</li>\n<li>将连接拖到 Windows 资源管理器中的新目录会将原始目录移动到新目录。</li>\n</ul>\n</blockquote>\n<h2>错误级别</h2>\n<blockquote>\n<p>如果链接成功创建 <a href=\"https://ss64.com/nt/errorlevel.html\">%ERRORLEVEL%</a> = 0<br>链接无法创建或给出了错误的参数 <a href=\"https://ss64.com/nt/errorlevel.html\">%ERRORLEVEL%</a> = 1</p>\n</blockquote>\n<h3>示例</h3>\n<p>为文件创建链接：</p>\n<blockquote>\n<p>C:\\&gt; MKlink ss64.exe C:\\Windows\\system32\\notepad.exe<br>C:\\&gt; Dir<br>C:\\&gt; Del ss64.exe</p>\n</blockquote>\n<p>为文件夹创建链接：</p>\n<blockquote>\n<p>C:\\&gt; MKlink /D Apr C:\\work\\April<br>C:\\&gt; Dir<br>C:\\&gt; RD Apr</p>\n</blockquote>\n<p>为当前用户重新创建“<strong>Application Data</strong>”目录连接。<br>如果此目录连接被删除，某些旧软件可能无法正常工作：</p>\n<blockquote>\n<p>CD &quot;C:\\Users\\%username%&quot;<br>MKlink /j &quot;Application Data&quot; &quot;C:\\Users\\%username%\\AppData\\Roaming&quot;</p>\n</blockquote>\n<p>MKLINK 是一个<a href=\"https://ss64.com/nt/syntax-internal.html\">内部</a>命令。</p>\n<h1>装入点</h1>\n<p><a href=\"https://learn.microsoft.com/zh-cn/windows-server/storage/disk-management/assign-a-mount-point-folder-path-to-a-drive\">装入点（mount points）</a>是NTFS文件系统将磁盘挂载到指定目录的机制。这样就可以实现同一个盘符中访问不同的物理磁盘。</p>\n","tags":["windows"]},{"id":"linux-intro","url":"https://yieldray.fun/posts/linux-intro","title":"linux入门命令","date_published":"2022-05-03T18:00:00.000Z","date_modified":"2022-05-03T18:00:00.000Z","content_text":"<p>bash 编程请参见<a href=\"/posts/linux-bash-intro/\">此处</a></p>\n<h1>提示及参考</h1>\n<p>i interactive 交互式<br>v verbose 冗长<br>r recursive 递归<br>f forcibly 强行<br>f format 格式化</p>\n<p><a href=\"https://cheat.sh/\">https://cheat.sh/</a><br><a href=\"https://www.shell.how/\">https://www.shell.how/</a><br><a href=\"https://tldr.inbrowser.app/\">https://tldr.inbrowser.app/</a><br><a href=\"https://jaywcjlove.gitee.io/linux-command/\">https://jaywcjlove.gitee.io/linux-command/</a><br><a href=\"https://github.com/shifudao/linux_basis\">https://github.com/shifudao/linux_basis</a><br><a href=\"https://github.com/trinib/Linux-Bash-Commands\">https://github.com/trinib/Linux-Bash-Commands</a><br><a href=\"https://github.com/me115/linuxtools_rst\">https://github.com/me115/linuxtools_rst</a><br><a href=\"https://github.com/dunwu/linux-tutorial\">https://github.com/dunwu/linux-tutorial</a><br><a href=\"https://wangdoc.com/bash/\">https://wangdoc.com/bash/</a><br><a href=\"https://dashdash.io/\">https://dashdash.io/</a><br><a href=\"https://devhints.io/bash\">https://devhints.io/bash</a><br><a href=\"https://learnxinyminutes.com/docs/bash/\">https://learnxinyminutes.com/docs/bash/</a><br><a href=\"https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md\">https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md</a></p>\n<p>command --help<br>man command</p>\n<h1>命令提示符</h1>\n<p>命令提示符根据发行版的不同有所不同，这里波浪线可以是当前目录</p>\n<p>$ 代表普通用户，# 代表 root 用户</p>\n<pre><code class=\"language-sh\">user@server:~$\n[user@serve ~]$\n\nroot@server:~#\n[root@server ~]#\n</code></pre>\n<h1>通配符</h1>\n<pre><code class=\"language-sh\">?  匹配任意单个字符\n*  匹配任意长度的任意字符\n** 匹配任意级别目录\n[] 匹配一个单字符范围\n</code></pre>\n<h1>Shell</h1>\n<h2>sh</h2>\n<h2>bash</h2>\n<p><a href=\"https://dashdash.io/1/bash\">https://dashdash.io/1/bash</a></p>\n<h2>zsh</h2>\n<h1>重定向</h1>\n<p>文件描述符 0 代表标准输入（STDIN），1 标准输出（STDOUT），2 标准错误输出（STDERR）。</p>\n<p>&amp; 表示重定向的目标不是一个文件，而是一个文件描述符</p>\n<pre><code class=\"language-sh\">{{command}} &lt; {{file}}  # 命令输出重定向至文件\n{{command}} &gt; {{file}}  # 命令输出覆盖写入文件\n{{command}} &gt;&gt; {{file}} # 命令输出追加写入文件\n{{command}} 1&gt; {{file}} # 1 代表 stdout\n{{command}} 2&gt; {{file}} # 2 代表 stderr\n{{command}} &amp;&gt; {{file}} # 也是 stderr\n\nn &gt;&amp; m\t# 将输出文件 m 和 n 合并\nn &gt;&amp; m\t# 将输出文件 m 和 n 合并\n&lt;&lt; tag  #\t将开始标记 tag 和结束标记 tag 之间的内容作为输入\n\n{{command}} &gt; {{file}} 2&gt;&amp;1  # stdout和stderr都重定向至文件\n{{command}} &gt; /dev/null      # 丢弃输出\n{{command}} &gt; /dev/null 2&gt;&amp;1 # 丢弃所有输出\n</code></pre>\n<h1>文件管理</h1>\n<h2>ls</h2>\n<p>list files</p>\n<pre><code class=\"language-sh\">Usage: ls [OPTION]... [FILE]...\n\nls -a # 显示所有\nls -A # 显示所有，但不包括 . 和 ..\nls -F # 显示的文件带有标识\nls -R # 递归显示\nls -l # 显示详细信息\n</code></pre>\n<pre><code class=\"language-sh\">$ ls -lF\n-rwxrwxrwx 1 ubuntu ubuntu 2211 May  4 16:59 file.txt\ndrwxrwxrwx 1 ubuntu ubuntu 4096 May  4 17:42 folder/\n\n（第一个字符表示）文件类型，比如目录（d）、文件（-）、字符型文件（c）或块设备（b）\n文件的权限\n文件的硬链接总数\n文件属主的用户名\n文件属组的组名\n文件的大小（以字节为单位）\n文件的上次修改时间\n文件名或目录名，如果使用了-F参数（有时会自动使用），则文件名则会带有额外的修饰符号\ndir/ 表示目录， exec* 表示可执行文件， link@ 表示符号链接（软链接）在长列表中则有箭头指向源文件 link -&gt; file.txt\n\n\n文件权限\n(r)ead (w)rite e(x)cute\n(u)ser (g)roup (o)ther\n\n八进制权限表示\n--- 000 0\n--x 001 1\n-w- 010 2\n-wx 011 3\nr-- 100 4\nr-x 101 5\nrw- 110 6\nrwx 111 7\n\n type   u     g     o\n  -  | rwx | r-x | r-x\n 文件   7     5     5\n</code></pre>\n<h2>umask</h2>\n<pre><code class=\"language-sh\">权限掩码（默认遮掩权限）\n$ umask\n0022\n\n若目录完全权限是 777 ，文件完全权限是 666\n则创建的目录默认权限为 777-022=755 ，创建的文件默认权限为 666-022=644\n</code></pre>\n<pre><code class=\"language-sh\">umask: umask [-p] [-S] [mode]\n    Display or set file mode mask.\n\numask -S\n</code></pre>\n<h2>chmod getacl setacl chattr</h2>\n<p>change mode</p>\n<pre><code class=\"language-sh\">Usage: chmod [OPTION]... MODE[,MODE]... FILE...\n  or:  chmod [OPTION]... OCTAL-MODE FILE...\n  or:  chmod [OPTION]... --reference=RFILE FILE...\n</code></pre>\n<pre><code class=\"language-sh\"># ACL\ngetacl [选项] 文件名\nsetacl [选项] 设定值 文件名\n</code></pre>\n<pre><code class=\"language-sh\"># SUID (Set UID) 权限\n为了让一般使用者在执行某些程序的时候，能够暂时具有该程序的所有者的权限（如/usr/bin/passwd程序可以修改/etc/shadow文件）\ns权限在user的x执行位（只能用于文件），例如：-rwsr-xr-x\nchmod u+s &lt;path-to-file-or-dir&gt;\n\n# SGID (Set GID) 权限\ns权限在group，SGID可以用在文件和目录上\n（1）文件：不论使用者是谁，在执行程序时其有效群组将会变成该文件的群组所有人\n（2）目录：在该目录内建立的文件的或目录的所属组将会自动成为该应用程序的所有组\nchmod g+s &lt;path-to-file-or-dir&gt;\n\n# SBit (Sticky Bit, 粘滞位) 权限\n只针对目录，使用t标识\n在具有SBit目录的目录下，使用者若在该目录下具有w及x的权限\n则当使用者在该目录下建立文件或目录时，只有所有者（与root）才有权限删除\nchmod o+t &lt;path-to-file-or-dir&gt;\n</code></pre>\n<h2>chown</h2>\n<p>change owner</p>\n<pre><code class=\"language-sh\">Usage: chown [OPTION]... [OWNER][:[GROUP]] FILE...\n  or:  chown [OPTION]... --reference=RFILE FILE...\n</code></pre>\n<h2>chgrp</h2>\n<p>change group</p>\n<h2>chroot</h2>\n<p>change root</p>\n<h2>tree (非预装)</h2>\n<p>树状显示当前目录内容，类似于 DOS 的 tree 命令</p>\n<h2>cd</h2>\n<p>change directory</p>\n<pre><code class=\"language-sh\">Usage: cd [-L|[-P [-e]] [-@]] [dir]\n\ncd - # 返回前一次目录\n</code></pre>\n<h2>pushd，popd，dirs</h2>\n<p>目录堆栈，栈索引从 0 开始</p>\n<pre><code class=\"language-sh\">pushd {{dirname}} # 进入指定目录，并将该目录放入堆栈顶部\npushd +{{n}} # 将从栈顶算起的n号目录移动到栈顶，同时切换到该目录\npushd -{{n}} # 将从栈底算起的n号目录移动到栈顶，同时切换到该目录\n\npopd # 移除堆栈的顶部记录，并进入新的栈顶目录\npopd -n # 仅移除堆栈顶部，不改变当前目录\npopd +{{n}} # 删除从栈顶算起的n号目录，不改变当前目录\npopd -{{n}} # 删除从栈底算起的n号目录，不改变当前目录\n\ndirs # 查看当前目录堆栈，栈顶在左，栈底在右\ndirs -c # 清空目录栈\ndirs -l # 用户主目录不显示波浪号前缀，而打印完整的目录\ndirs -p # 每行一个条目打印目录栈，默认是打印在一行\ndirs -v # 每行一个条目，每个条目之前显示位置编号\ndirs +{{n}} # 显示堆顶算起的第n个目录\ndirs -{{n}} # 显示堆底算起的第n个目录\n</code></pre>\n<h2>pwd</h2>\n<p>print work directory</p>\n<pre><code class=\"language-sh\">pwd: pwd [-LP]\n</code></pre>\n<h2>touch</h2>\n<pre><code class=\"language-sh\">Usage: touch [OPTION]... FILE...\n</code></pre>\n<h2>cp</h2>\n<p>copy file</p>\n<pre><code class=\"language-sh\">Usage: cp [OPTION]... [-T] SOURCE DEST\n  or:  cp [OPTION]... SOURCE... DIRECTORY\n  or:  cp [OPTION]... -t DIRECTORY SOURCE...\n\ncp -R {{path/to/source_directory}} {{path/to/target_directory}}\ncp -R source/ target # 递归复制文件夹\ncp -i # 交互式\ncp -v # 显示过程\n</code></pre>\n<h2>mv</h2>\n<p>move file</p>\n<pre><code class=\"language-sh\">Usage: mv [OPTION]... [-T] SOURCE DEST\n  or:  mv [OPTION]... SOURCE... DIRECTORY\n  or:  mv [OPTION]... -t DIRECTORY SOURCE...\n\nmv -i # 交互式\nmv -v # 显示过程\n</code></pre>\n<h2>ln</h2>\n<p>link files</p>\n<pre><code class=\"language-sh\">Usage: ln [OPTION]... [-T] TARGET LINK_NAME   (1st form)\n  or:  ln [OPTION]... TARGET                  (2nd form)\n  or:  ln [OPTION]... TARGET... DIRECTORY     (3rd form)\n  or:  ln [OPTION]... -t DIRECTORY TARGET...  (4th form)\n\nln {{/path/to/file}} {{path/to/hardlink}}  # 硬链接\nln -s {{/path/to/file_or_directory}} {{path/to/symlink}} # 软链接（符号链接）\nln -sf {{/path/to/new_file}} {{path/to/symlink}} # 覆盖软链接\n</code></pre>\n<h2>realpath</h2>\n<pre><code class=\"language-sh\">Usage: realpath [OPTION]... FILE...\n\nrealpath {{path/to/file_or_directory}} # 显示一个文件的最终绝对路径，可以用于判断符号链接\n</code></pre>\n<h2>rm</h2>\n<p>remove</p>\n<pre><code class=\"language-sh\">Usage: rm [OPTION]... [FILE]...\n\nrm -r directory # 删除目录\nrm -R directory # 删除目录\nrm -f # 强行删除\nrm -v # 显示过程\nrm -i # 交互式\n</code></pre>\n<h2>mkdir</h2>\n<p>make directory</p>\n<pre><code class=\"language-sh\">Usage: mkdir [OPTION]... DIRECTORY...\n\nmkdir dirname # 创建一级目录\nmkdir -p dir/name # 创建多级目录\n</code></pre>\n<h2>rmdir</h2>\n<p>remove directory</p>\n<pre><code class=\"language-sh\">Usage: rmdir [OPTION]... DIRECTORY...\n\nrmdir dirname # 删除空目录\nrmdir -v # 显示过程\n</code></pre>\n<h1>文件 IO</h1>\n<h2>stat</h2>\n<pre><code class=\"language-sh\">Usage: stat [OPTION]... FILE...\nDisplay file or file system status.\n</code></pre>\n<h2>file</h2>\n<pre><code class=\"language-sh\">Usage: file [OPTION...] [FILE...]\nDetermine type of FILEs.\n</code></pre>\n<h2>cat</h2>\n<p>concatenate</p>\n<pre><code class=\"language-sh\">Usage: cat [OPTION]... [FILE]...\nConcatenate FILE(s) to standard output.\n\nWith no FILE, or when FILE is -, read standard input.\n\ncat -n {{file}} # 显示行号\ncat {{file1}} {{file2}} &gt; {{target_file}} # 将指定文件合并输出到另一个文件\ncat {{file1}} {{file2}} &gt;&gt; {{target_file}}\n</code></pre>\n<h2>cut</h2>\n<h2>join</h2>\n<h2>paste</h2>\n<h2>wc</h2>\n<pre><code class=\"language-sh\">Usage: wc [OPTION]... [FILE]...\n  or:  wc [OPTION]... --files0-from=F\n针对每个指定的文件，输出该文件中的换行符数、单词数和字节数。并在指定了多个文件时，输出合并后的总行数。\n\n当不指定文件名或者文件名为 &quot;-&quot; 时，从标准输入读取数据。\n\n下面的选项可用于选择要打印哪些计数，总是按照以下顺序:\n换行符数、单词数、字符数、字节数、最大行长度。\n\n  -c, --bytes            打印字节数\n  -m, --chars            打印字符数\n  -l, --lines            打印行数\n      --files0-from=F    从文件 F 中以 NUL 为分隔符读取文件名列表，并依次读取每个文件的内容作为输入。\n                         如果 F 的值为 &quot;-&quot;，则从标准输入中读取文件名列表。\n  -L, --max-line-length  打印最长行的长度\n  -w, --words            打印单词数。单词是由空白字符分隔开的、非空的字符序列。\n</code></pre>\n<h2>more</h2>\n<p>分页工具</p>\n<pre><code class=\"language-sh\"> more [options] &lt;file&gt;...\n\nA file perusal filter for CRT viewing.\n\nmore +{{line_number}} {{path/to/file}} # 从指定行打开文本\n\n&lt;Space&gt; # 下一面\nh # 帮助\nq # 退出\n</code></pre>\n<h2>less</h2>\n<p>易用的分页工具<br>less is more</p>\n<pre><code class=\"language-sh\">less {{source_file}}\n\nb # 上一面\n&lt;Space&gt; # 下一面\nG # 转到末尾\ng # 转到开头\nh # 帮助\nv # 打开编辑器\nq # 退出\n</code></pre>\n<h2>tail</h2>\n<pre><code class=\"language-sh\">Usage: tail [OPTION]... [FILE]...\n\ntail # 默认显示最后10行\n# 与head相比，tail还支持以下参数，使其擅长用于监听日志文件\ntail -f {{path/to/file}} # 追踪文件最后几行的变更，每次变更都会输出一次\ntail --follow {{path/to/file}}\ntail --follow={{path/to/file}}\ntail --pid={{PID}} -f {{path/to/file}} # 指定PID程序结束时自动终止\ntail --pid={{PID}} -s {{second}} -f {{path/to/file}} # 调整检查PID程序的时间间隔秒数，默认为1.0\n</code></pre>\n<h2>head</h2>\n<pre><code class=\"language-sh\">Usage: head [OPTION]... [FILE]...\n\nhead # 默认显示最前10行\nhead -q # 不显示文件名\nhead --quiet\nhead --silent\nhead -v # 显示文件名  ==&gt; filename &lt;==\nhead --verbose\nhead -n{{count}} # 显示最前count行，具体数字可以紧跟在-n后面\nhead -n {{count}} # 显示最前count行，具体数字与-n之间有空格\nhead --lines {{count}} {{path/to/file}} # GNU风格，以上命令count为负数时显示除了最后count行之外的内容\nhead --lines={{count}} {{path/to/file}} # GNU风格，也可以加上等号\nhead -c {{count}} {{path/to/file}} # 显示最前count字节，支持负数\nhead --bytes {{count}} {{path/to/file}}\nhead --bytes={{count}} {{path/to/file}}\nhead -z # 只将NUL解释为换行分隔符，在结合其他命令时实用\n</code></pre>\n<h2>watch</h2>\n<h2>find</h2>\n<p>Find files or directories under the given directory tree, recursively</p>\n<pre><code class=\"language-sh\">Usage: find [-H] [-L] [-P] [-Olevel] [-D debugopts] [path...] [expression]\n</code></pre>\n<h2>locate</h2>\n<pre><code class=\"language-sh\">Usage: locate [OPTION]... [PATTERN]...\nSearch for entries in a mlocate database.\n</code></pre>\n<h2>sort</h2>\n<p>Sort lines of text files</p>\n<pre><code class=\"language-sh\">Usage: sort [OPTION]... [FILE]...\n  or:  sort [OPTION]... --files0-from=F\nWrite sorted concatenation of all FILE(s) to standard output.\n</code></pre>\n<h2>uniq</h2>\n<p>Output the unique lines from the given input or file</p>\n<pre><code class=\"language-sh\">Usage: uniq [OPTION]... [INPUT [OUTPUT]]\nFilter adjacent matching lines from INPUT (or standard input),\nwriting to OUTPUT (or standard output).\n</code></pre>\n<h2>seq</h2>\n<pre><code class=\"language-sh\">Usage: seq [OPTION]... LAST\n  or:  seq [OPTION]... FIRST LAST\n  or:  seq [OPTION]... FIRST INCREMENT LAST\nPrint numbers from FIRST to LAST, in steps of INCREMENT.\n</code></pre>\n<h2>sed</h2>\n<p>Stream Editor</p>\n<pre><code class=\"language-sh\">Usage: sed [OPTION]... {script-only-if-no-other-script} [input-file]...\n</code></pre>\n<h2>grep / fgrep / egrep</h2>\n<p>Global Regular Expression Print</p>\n<pre><code class=\"language-sh\">Usage: grep [OPTION]... PATTERN [FILE]...\nSearch for PATTERN in each FILE.\nExample: grep -i &#39;hello world&#39; menu.h main.c\n</code></pre>\n<h1>文本编辑</h1>\n<h2>editor</h2>\n<p>指向系统默认编辑器</p>\n<h2>vi / vim</h2>\n<h2>nano</h2>\n<h2>emacs (非预装)</h2>\n<h2>iconv</h2>\n<p>转换文件的编码方式</p>\n<pre><code class=\"language-sh\">Usage: iconv [OPTION...] [FILE...]\nConvert encoding of given files from one encoding to another.\n\n Input/Output format specification:\n  -f, --from-code=NAME       encoding of original text\n  -t, --to-code=NAME         encoding for output\n\n Information:\n  -l, --list                 list all known coded character sets\n\n Output control:\n  -c                         omit invalid characters from output\n  -o, --output=FILE          output file\n  -s, --silent               suppress warnings\n      --verbose              print progress information\n</code></pre>\n<h1>(解)压缩/归档</h1>\n<h2>gzip</h2>\n<p>压缩文件</p>\n<h2>gnuzip</h2>\n<p>解压文件</p>\n<h2>tar</h2>\n<p>Tape archive<br><a href=\"https://cn.tldr.inbrowser.app/pages/common/tar\">https://cn.tldr.inbrowser.app/pages/common/tar</a></p>\n<pre><code class=\"language-sh\">tar czf path/to/target.tar.gz path/to/file1 path/to/file2 ...\ntar xvf path/to/source.tar[.gz|.bz2|.xz]\n</code></pre>\n<h2>zip / unzip</h2>\n<h2>zless zmore zcat zgrep</h2>\n<h1>进程管理</h1>\n<p><code>command &amp;</code> 后台运行命令</p>\n<p><code>Ctrl + Z</code> 将当前命令放在后台且暂停</p>\n<h2>nice</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/nice\">https://cn.tldr.inbrowser.app/pages/common/nice</a></p>\n<pre><code class=\"language-sh\">nice -n {{niceness_value}} {{command}}\n</code></pre>\n<h2>renice</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/renice\">https://cn.tldr.inbrowser.app/pages/common/renice</a></p>\n<pre><code class=\"language-sh\">renice -n {{niceness_value}} -p {{pid}}\n</code></pre>\n<h2>sleep</h2>\n<pre><code class=\"language-sh\">#!/bin/bash\n\nb=&#39;&#39;\nfor ((i = 0; $i &lt;= 100; i++)); do\n    printf &quot;Progress:[%-100s]%d%%\\r&quot; $b $i\n    sleep 0.01\n    b=#$b\ndone\necho\necho &quot;Done!&quot;\n</code></pre>\n<p>加上 <code>&amp;</code> ，使之在后台运行</p>\n<pre><code class=\"language-sh\">$ sleep 60 &amp;\n[1] 36\n</code></pre>\n<h2>jobs</h2>\n<pre><code class=\"language-sh\">$ jobs\n[1]+  Running                 sleep 60 &amp;\n\n\njobs # 显示后台运行的命令的状态\njobs %{{job_id}} # 显示指定job_id状态\njobs -l # 显示详细信息\njobs -p # 显示pid\n</code></pre>\n<h2>fg</h2>\n<pre><code class=\"language-sh\">$ sleep 60 &amp;\n[1] 38\n$ fg\nsleep 60\n^Z # 按 Ctrl-Z 暂停\n[1]+  Stopped                 sleep 60\n$ jobs\n[1]+  Stopped                 sleep 60\n# 再次使用 fg 命令，恢复运行\n\n\nfg %{{job_id}} # 将后台中的命令调至前台继续运行\n</code></pre>\n<h2>bg</h2>\n<pre><code class=\"language-sh\">$ jobs # 一个被 Ctrl-Z 停止的命令\n[1]+  Stopped                 sleep 60\n$ bg   # 使之在后台继续执行\n[1]+ sleep 60 &amp;\n$ jobs # 正在后台继续执行\n[1]+  Running                 sleep 60 &amp;\n\nbg # 显示后台任务，类似于jobs\nbg %{{job_id}} # 将一个在后台暂停的命令（如：Ctrl+Z），变成在后台继续执行\n</code></pre>\n<h2>nohup</h2>\n<p>no hang up</p>\n<pre><code class=\"language-sh\">nohup {{command}} {{command_arguments}} # 默认输出至 &#39;nohup.out&#39;\nnohup {{command}} {{command_arguments}} &amp;\n</code></pre>\n<h2>disown</h2>\n<p>disown 只是移除作业，进程并没有停止</p>\n<pre><code class=\"language-sh\">disown %{{job_id}}\ndisown {{pid}}\n</code></pre>\n<h2>ps</h2>\n<pre><code class=\"language-sh\">Usage:\n ps [options]\n\n Try &#39;ps --help &lt;simple|list|output|threads|misc|all&gt;&#39;\n  or &#39;ps --help &lt;s|l|o|t|m|a&gt;&#39;\n for additional help text.\n\nps # 支持Unix,BSD,GNU风格\nps -l # 显示长列表\nps -f\nps --forest # 用层级结构显示出进程和父进程的关系\n</code></pre>\n<pre><code class=\"language-sh\">F 　   内核分配给进程的系统标记\nS 　   进程的状态（O正在运行；S在休眠；R可运行；Z僵化，进程已结束但父进程已不存在；T停止）\nUID　  启动这些进程的用户\nPID　  进程的进程ID\nPPID   父进程的进程号（如果是由另一个进程启动的）\nC 　 　进程生命周期中的CPU利用率\nPRI　  进程的优先级（数字越大，优先级越低）\nNI　　 谦让度值用来参与决定优先级\nADDR　 进程的内存地址\nSZ　　 假如进程被换出，所需交换空间的大致大小\nWCHAN　进程休眠的内核函数的地址\nSTIME　进程启动时的系统时间\nTTY　　进程启动时的终端设备\nTIME　 运行进程需要的累计CPU时间\nCMD　　启动的程序名称\n\n\n$ ps -l # Unix风格\nF S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD\n4 S     0    96     0  0  80   0 -  2890 do_wai pts/1    00:00:00 bash\n0 R     0   167    96  0  80   0 -  3283 -      pts/1    00:00:00 ps\n\nVSZ　进程在内存中的大小，单位为KB\nRSS　进程在未换出时占用的物理内存\nSTAT　代表当前进程状态的双字符状态码（第一个字符和Unix风格的S列相同，第二个表示该进程：&lt;运行在高优先级上；\n      N运行在低优先级上；L有页面锁定在内存中；s是控制进程；l是多线程；+运行在前台）\n\n$ ps l # BSD风格\nF   UID   PID  PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND\n4     0    96     0  20   0  11560  3996 do_wai Ss   pts/1      0:00 bash\n0     0   165    96  20   0  13132  3112 -      R+   pts/1      0:00 ps l\n</code></pre>\n<h2>pstree</h2>\n<pre><code class=\"language-sh\">pstree\npstree -p # 显示 PID\n</code></pre>\n<h2>top</h2>\n<pre><code class=\"language-sh\">Usage:\n  top -hv | -bcHiOSs -d secs -n max -u|U user -p pid(s) -o field -w [cols]\n\nq # 退出\nh # 帮助\n</code></pre>\n<pre><code class=\"language-sh\">PID     进程的ID。\nUSER    进程属主的名字。\nPR      进程的优先级。\nNI      进程的谦让度值。\nVIRT    进程占用的虚拟内存总量。\nRES     进程占用的物理内存总量。\nSHR     进程和其他进程共享的内存总量。\nS       进程的状态（D代表可中断的休眠状态，R 代表在运行状态，S代表休眠状态，T代表跟踪状 态或停止状态，Z代表僵化状态）。\n%CPU    进程使用的CPU时间比例。\n%MEM    进程使用的内存占可用内存的比例。\nTIME+   自进程启动到目前为止的CPU时间总 量。\nCOMMAND 进程所对应的命令行名称，也就 是启动的程序名。\n</code></pre>\n<h2>kill</h2>\n<pre><code class=\"language-sh\">kill process_id\nkill -s signal/id process_id\nkill %job_id\n</code></pre>\n<h2>pgrep</h2>\n<pre><code class=\"language-sh\">Usage:\n pgrep [options] &lt;pattern&gt;\n</code></pre>\n<h2>pkill</h2>\n<pre><code class=\"language-sh\">Usage:\n pkill [options] &lt;pattern&gt;\n</code></pre>\n<h2>skill</h2>\n<pre><code class=\"language-sh\">Usage:\n skill [signal] [options] &lt;expression&gt;\n</code></pre>\n<h2>killall</h2>\n<pre><code class=\"language-sh\">Usage: killall [ -Z CONTEXT ] [ -u USER ] [ -y TIME ] [ -o TIME ] [ -eIgiqrvw ]\n               [ -s SIGNAL | -SIGNAL ] NAME...\n\n\nkillall process_name\nkillall -i # 交互式\n</code></pre>\n<h2>at</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/at\">https://cn.tldr.inbrowser.app/pages/common/at</a></p>\n<pre><code class=\"language-sh\"># /etc/init.d/atd\nservice start atd\nsystemctl start atd\n</code></pre>\n<h2>crontab</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/crontab\">https://cn.tldr.inbrowser.app/pages/common/crontab</a><br><a href=\"https://jaywcjlove.gitee.io/linux-command/c/crontab.html\">https://jaywcjlove.gitee.io/linux-command/c/crontab.html</a><br><a href=\"https://devhints.io/bash\">https://devhints.io/bash</a></p>\n<pre><code>Min  Hour Day  Mon  Weekday\n*    *    *    *    *  command to be executed\n┬    ┬    ┬    ┬    ┬\n│    │    │    │    └─  Weekday  (0=Sun .. 6=Sat)\n│    │    │    └──────  Month    (1..12)\n│    │    └───────────  Day      (1..31)\n│    └────────────────  Hour     (0..23)\n└─────────────────────  Minute   (0..59)\n</code></pre>\n<pre><code class=\"language-sh\"># /etc/crontab\n</code></pre>\n<h1>磁盘管理</h1>\n<p>lvm 此处略</p>\n<h2>mount</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/mount\">https://cn.tldr.inbrowser.app/pages/common/mount</a></p>\n<h2>umount</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/umount\">https://cn.tldr.inbrowser.app/pages/common/umount</a></p>\n<h2>free</h2>\n<p><a href=\"https://jaywcjlove.gitee.io/linux-command/c/free.html\">https://jaywcjlove.gitee.io/linux-command/c/free.html</a></p>\n<h2>df</h2>\n<p>disk free</p>\n<pre><code class=\"language-sh\">df\ndf -h\n</code></pre>\n<h2>du</h2>\n<p>disk usage</p>\n<pre><code class=\"language-sh\">du\ndu -h\n</code></pre>\n<h2>dd</h2>\n<p>disk dump</p>\n<h2>fdisk</h2>\n<p><a href=\"https://jaywcjlove.gitee.io/linux-command/c/fdisk.html\">https://jaywcjlove.gitee.io/linux-command/c/fdisk.html</a></p>\n<h2>mkfs</h2>\n<p><a href=\"https://jaywcjlove.gitee.io/linux-command/c/mkfs.html\">https://jaywcjlove.gitee.io/linux-command/c/mkfs.html</a></p>\n<h1>用户与组</h1>\n<pre><code>/etc/passwd\nusername:password:uid:gid:description:home_directory:shell\n\ne.g.\nroot:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\n\n\n/etc/shadow\nusername:password:last_password_change:min_days:max_days:warn_days:inactive_days:expire_date:reserved\n\ne.g.\nroot:$5$vT6FSN/TDyk.DeVO$oPamf8sjZqWaWF0DuM.BtvuuxCDUMx15/RiwTVeKwD9::0:99999:7:::\nbin:*:18353:0:99999:7:::\n</code></pre>\n<h2>whoami</h2>\n<pre><code class=\"language-sh\">Usage: whoami [OPTION]...\nPrint the user name associated with the current effective user ID.\nSame as id -un.\n</code></pre>\n<h2>su</h2>\n<p>switch user</p>\n<pre><code class=\"language-sh\">su # 切换至 root\nsu {{username}} # 切换至 username，继承当前环境变量\nsu - {{username}} # 切换至 username，模拟一次完全登录\nsu - {{username}} -c {{command}} # 以 username 身份执行命令\n</code></pre>\n<h2>useradd</h2>\n<pre><code class=\"language-sh\">useradd &lt;USER NAME&gt; -g &lt;GROUP NAME&gt; -u &lt;UID&gt;\n</code></pre>\n<h1>groupadd</h1>\n<pre><code class=\"language-sh\">groupadd -g &lt;GID&gt; &lt;GROUP NAME&gt;\n</code></pre>\n<h2>userdel</h2>\n<pre><code class=\"language-sh\">userdel &lt;USER NAME&gt;\nuserdel -r &lt;USER NAME&gt; # 同时删除用户的所有文件和目录\n</code></pre>\n<h2>groupdel</h2>\n<pre><code class=\"language-sh\">groupdel &lt;GROUP NAME&gt;\n</code></pre>\n<h2>usermod</h2>\n<p>和 useradd 的选项基本一致</p>\n<h2>groupmod</h2>\n<p>和 groupadd 的选项基本一致</p>\n<h2>passwd</h2>\n<p>更改密码</p>\n<pre><code class=\"language-sh\">passwd # 更改自己的密码\npasswd &lt;USER NAME&gt; # root用户更改指定用户的密码\npasswd -l &lt;USER NAME&gt; # 锁定指定用户不能修改密码\n</code></pre>\n<h1>gpasswd</h1>\n<p>管理 <code>/etc/group</code> 和 <code>/etc/gshadow</code> 文件</p>\n<h2>chpasswd</h2>\n<p>change password\n批量更新用户口令</p>\n<h2>chage</h2>\n<p>change age<br>修改帐号和密码的有效期限</p>\n<h2>chsh</h2>\n<p>change shell<br>更换登录系统时使用的 shell</p>\n<h2>chfg (可能需要安装)</h2>\n<h1>网络</h1>\n<h2>wget</h2>\n<pre><code class=\"language-sh\">Usage: wget [OPTION]... [URL]...\n\nwget {{url}} # 下载该url，文件名自动从url中获取，默认会输出下载信息\nwget {{url}} -o {{path}} # 下载url至指定路径，不会输出信息\nwget {{url}} -a {{path}} # 下载url至指定路径，如果文件存在，则附加\nwget -q # 安静模式\n\n\n$ wget ip.sb\n--2022-02-02 22:22:21--  http://ip.sb/\nResolving ip.sb (ip.sb)... 172.67.75.172, 104.26.12.31, 104.26.13.31\nConnecting to ip.sb (ip.sb)|172.67.75.172|:80... connected.\nHTTP request sent, awaiting response... 200 OK\nLength: 15 [text/plain]\nSaving to: ‘index.html’\n\nindex.html               100%[==================================&gt;]      15  --.-KB/s    in 0s\n\n2022-02-02 22:22:22 (783 KB/s) - ‘index.html’ saved [15/15]\n</code></pre>\n<h2>curl</h2>\n<pre><code class=\"language-sh\">Usage: curl [options...] &lt;url&gt;\n\ncurl {{url}}\ncurl {{url}} -o {{path}}\ncurl -s # 安静模式\n</code></pre>\n<h2>ping</h2>\n<pre><code class=\"language-sh\">ping {{host}}\nping -c {{count}} {{host}}\nping -i {{seconds}} {{host}}\n</code></pre>\n<h2>nslookup</h2>\n<pre><code class=\"language-sh\">nslookup [-option] [name | -] [server]\n\nnslookup {{example.com}}\nnslookup -type=NS {{example.com}} {{8.8.8.8}} # 指定dns\nnslookup -ty=NS {{example.com}} # 可简写为ty，注意都是单横线\nnslookup -ty=CNAME {{example.com}} # type不区分大小写，未知type则fallback到type=a\n\nnslookup # 进入交互模式\nset ty=A # 交互模式中设置type\n{{example.com}} # 回车查询\nexit #退出\n</code></pre>\n<h2>dig</h2>\n<pre><code class=\"language-sh\">dig {{host}}\ndig +short {{example.com}}\ndig +short {{example.com}} {{A|MX|TXT|CNAME|NS}}\n</code></pre>\n<h2>lsof</h2>\n<h2>mtr</h2>\n<h2>ifconfig</h2>\n<h2>ip</h2>\n<h2>netstat</h2>\n<h2>ss</h2>\n<h1>包管理器</h1>\n<h2>dpkg</h2>\n<p>Debian 系包管理器 （Ubuntu、Linux Mint）</p>\n<h2>rpm</h2>\n<p>Red Hat 系包管理器 （Fedora、openSUSE）</p>\n<h2>aptitude (可能非预装)</h2>\n<p>兼具图形化与命令行</p>\n<h2>apt / apt-get</h2>\n<pre><code class=\"language-sh\">apt-get install      # 安装新包\napt-get remove       # 卸载已安装的包（保留配置文件）\napt-get purge        # 卸载已安装的包（删除配置文件）\napt-get update       # 更新软件包列表\napt-get upgrade      # 更新所有已安装的包\napt-get autoremove   # 卸载已不需要的包依赖\napt-get dist-upgrade # 自动处理依赖包升级\napt-get autoclean    # 将已经删除了的软件包的.deb安装文件从硬盘中删除掉\napt-get clean        # 删除软件包的安装包\n\n# -c：指定配置文件\n</code></pre>\n<h2>yum</h2>\n<h1>shell 编程</h1>\n<h2>xargs</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/xargs\">https://cn.tldr.inbrowser.app/pages/common/xargs</a><br><a href=\"https://jaywcjlove.gitee.io/linux-command/c/xargs.html\">https://jaywcjlove.gitee.io/linux-command/c/xargs.html</a></p>\n<p>xargs 的默认命令是 echo，空格是默认定界符</p>\n<pre><code class=\"language-sh\">Usage: xargs [OPTION]... COMMAND [INITIAL-ARGS]...\nRun COMMAND with arguments INITIAL-ARGS and more arguments read from input.\n</code></pre>\n<h2>read</h2>\n<pre><code class=\"language-sh\"># 从标准输入读取一行并赋值给变量 REPLY\nread\n\n# 打印提示 text，等待输入，并将输入存储在 REPLY 中\nread -p &quot;text&quot;\n\n# 从标准输入读取输入并将值赋给指定变量\nread varname\n\n# 从标准输入读取输入到第一个空格或者回车，将输入的\n# 第一个单词放到变量 first 中，并将该行其他的输入放在变量 last 中\nread first last\n\n# 把单词清单读入 arrayname 数组里\nread -a arrayname\n\n# 指定读取等待时间为 3 秒\nread -t 3\n\n# 从输入中读取两个字符并存入变量 var，不需要按回车读取\nread -n 2 var\n\n# 用定界符 &quot;:&quot; 结束输入行\nread -d &quot;:&quot; var\n\n# 不显示输入值\nread -p &quot;Enter Password: &quot; -s pwd\n</code></pre>\n<h2>eval</h2>\n<pre><code class=\"language-sh\">eval: eval [arg ...]\n    Execute arguments as a shell command.\n\n    Combine ARGs into a single string, use the result as input to the shell,\n    and execute the resulting commands.\n\n    Exit Status:\n    Returns exit status of command or success if command is null.\n</code></pre>\n<h2>let</h2>\n<pre><code class=\"language-sh\"># 中间不能有空格\nlet a=3*4\n\n# 可以空格\n(( a = 3 * 4 ))\n\necho $a  # =&gt; 12\n</code></pre>\n<h2>set, unset</h2>\n<h2>source</h2>\n<pre><code class=\"language-sh\">source: source filename [arguments]\n    Execute commands from a file in the current shell.\n\n    Read and execute commands from FILENAME in the current shell.  The\n    entries in $PATH are used to find the directory containing FILENAME.\n    If any ARGUMENTS are supplied, they become the positional parameters\n    when FILENAME is executed.\n\n    Exit Status:\n    Returns the status of the last command executed in FILENAME; fails if\n    FILENAME cannot be read.\n</code></pre>\n<h2>declare</h2>\n<pre><code class=\"language-sh\">\n# 定义只读数组，设置属性的同时定义赋值\ndeclare -ar season=(&#39;Spring&#39; &#39;Summer&#39; &#39;Autumn&#39; &#39;Winter&#39;)\n\n# 或者这样\nseason=(&#39;Spring&#39; &#39;Summer&#39; &#39;Autumn&#39; &#39;Winter&#39;)\ndeclare -ar season\n\n# 显示所有数组\ndeclare -a\n\n# 定义关联数组\ndeclare -A fruits=([&#39;apple&#39;]=&#39;red&#39; [&#39;banana&#39;]=&#39;yellow&#39;)\n\n# 显示所有关联数组\ndeclare -A\n</code></pre>\n<h2>export</h2>\n<pre><code class=\"language-sh\"># 声明时导出\nexport a=1\n\n# 声明后导出\nb=2\nexport b\n</code></pre>\n<h2>env</h2>\n<h2>printenv</h2>\n<h2>printf</h2>\n<p><a href=\"https://jaywcjlove.gitee.io/linux-command/c/printf.html\">https://jaywcjlove.gitee.io/linux-command/c/printf.html</a></p>\n<pre><code class=\"language-sh\">printf: printf [-v var] format [arguments]\n</code></pre>\n<h1>其他</h1>\n<h2>uname</h2>\n<h2>login</h2>\n<h2>logout</h2>\n<h2>shutdown</h2>\n<h2>poweroff</h2>\n<h2>reboot</h2>\n<h2>exit</h2>\n<pre><code class=\"language-sh\">exit: exit [n]\n    Exit the shell.\n\n    Exits the shell with a status of N.  If N is omitted, the exit status\n    is that of the last command executed.\n</code></pre>\n<h2>tput</h2>\n<p>tput 命令 将通过 terminfo 数据库对您的终端会话进行初始化和操作。通过使用 tput，您可以更改几项终端功能，如移动或更改光标、更改文本属性，以及清除终端屏幕的特定区域。</p>\n<h2>clear</h2>\n<p><code>Ctrl + l</code></p>\n<h2>which</h2>\n<p>类似于 DOS 的 where</p>\n<h2>type</h2>\n<p>判断 shell 执行的命令的类型</p>\n<pre><code class=\"language-sh\">type {{command}}\ntype -a {{command}} # 显示所有\ntype -p {{command}} # 显示执行命令的实际文件路径，相当于which命令（有所不同的是其只输出第一个）\n\n$ which ls\n/bin/ls\n$ type ls\nls is aliased to `ls --color=auto&#39;\n$ type -a ls\nls is aliased to `ls --color=auto&#39;\nls is /bin/ls\n$ type -p ls # 没有输出\n$ type -ap ls\n/bin/lsd\n</code></pre>\n<h2>tee</h2>\n<p><a href=\"https://cn.tldr.inbrowser.app/pages/common/tee\">https://cn.tldr.inbrowser.app/pages/common/tee</a></p>\n<pre><code class=\"language-sh\">Usage: tee [OPTION]... [FILE]...\nCopy standard input to each FILE, and also to standard output.\n</code></pre>\n<h2>dmesg</h2>\n<h2>hddmesg</h2>\n<pre><code class=\"language-sh\">$ echo &quot;hello,world!&quot; &gt; test.txt\n$ hd test.txt\n00000000  68 65 6c 6c 6f 2c 77 6f  72 6c 64 21 0a           |hello,world!.|\n0000000d\n</code></pre>\n<h2>xxd</h2>\n<pre><code class=\"language-sh\">$ xxd test.txt\n00000000: 6865 6c6c 6f2c 776f 726c 6421 0a         hello,world!.\n</code></pre>\n<h2>hexdump</h2>\n<pre><code class=\"language-sh\">$ hexdump test.txt\n0000000 6568 6c6c 2c6f 6f77 6c72 2164 000a\n000000d\n</code></pre>\n<h2>base64</h2>\n<pre><code class=\"language-sh\">$ base64 &lt; test.txt\naGVsbG8sd29ybGQhCg==\n\n$ base64 -d &lt;(base64 &lt; test.txt)\nhello,world!\n</code></pre>\n<h2>diff</h2>\n<pre><code class=\"language-sh\">$ cp test.txt test2.txt\n$ echo -e &quot;\\nby.Ray&quot; &gt;&gt; test2.txt\n$ diff test.txt test2.txt\n1a2,3\n&gt;\n&gt; by.Ray\n</code></pre>\n<h2>patch</h2>\n<h2>diffstat</h2>\n<h2>date</h2>\n<p><a href=\"https://jaywcjlove.gitee.io/linux-command/c/date.html\">https://jaywcjlove.gitee.io/linux-command/c/date.html</a></p>\n<pre><code class=\"language-sh\">date +&quot;%Y-%m-%d&quot;\n</code></pre>\n<h2>cal</h2>\n<h2>bc</h2>\n<p>计算器</p>\n<h2>history</h2>\n<h2>alias, unalias</h2>\n<pre><code class=\"language-sh\">alias: usage: alias [-p] [name[=value] ... ]\n\n# 可将别名命令添加到 ~/.bashrc 中\necho &quot;alias &#39;ll=ls -alF&#39;&quot; &gt;&gt; ~/.bashrc\n</code></pre>\n<h2>time</h2>\n<p>测量程序耗时</p>\n<h2>strace ltrace</h2>\n<h2>ldd</h2>\n<h2>busybox</h2>\n<h2>mtools</h2>\n<h1><code>/proc</code> 目录</h1>\n","tags":["linux"]},{"id":"js-load-events","url":"https://yieldray.fun/posts/js-load-events","title":"DOMContentLoaded, readystatechange 等事件","date_published":"2022-04-30T14:41:37.000Z","date_modified":"2022-04-30T14:41:37.000Z","content_text":"<h1>document.readyState</h1>\n<p>一个文档的 readyState 可以是以下之一：</p>\n<p>loading（正在加载）<br>document 仍在加载。<br>interactive（可交互）<br>文档已被解析，&quot;正在加载&quot;状态结束，但是诸如图像，样式表和框架之类的子资源仍在加载。<br>complete（完成）<br>文档和所有子资源已完成加载。表示 load 状态的事件即将被触发。</p>\n<h1><a href=\"https://developer.mozilla.org/docs/Web/API/Document/DOMContentLoaded_event\">DOMContentLoaded</a></h1>\n<p>当初始的 HTML 文档被完全加载和解析完成之后，DOMContentLoaded 事件被触发，而无需等待样式表、图像和子框架的完全加载。</p>\n<h1>load</h1>\n<p>当整个页面及所有依赖资源如样式表和图片都已完成加载时，将触发 load 事件。</p>\n<p>它与 DOMContentLoaded 不同，后者只要页面 DOM 加载完成就触发，无需等待依赖资源的加载。</p>\n<h1>示例</h1>\n<p>在 html 中任意位置添加 <code>&lt;script&gt;&lt;/script&gt;</code>，内容如下</p>\n<pre><code class=\"language-js\">console.log(&quot;document.readyState=&quot; + document.readyState); // loading\n\ndocument.addEventListener(&quot;readystatechange&quot;, () =&gt; {\n    // readystatechange 事件触发时，document.readyState 往往已经结束 loading 状态了\n    console.log(&quot;document.readyState=&quot; + document.readyState); // interactive / complete\n});\nwindow.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {\n    // window 和 document 都具有 DOMContentLoaded 事件，这两个事件是不同的\n    console.log(&quot;window.onDOMContentLoaded&quot;);\n});\ndocument.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {\n    // document 的 DOMContentLoaded 事件先于 window 的 DOMContentLoaded 事件触发\n    console.log(&quot;document.onDOMContentLoaded&quot;);\n});\nwindow.addEventListener(&quot;load&quot;, () =&gt; {\n    // document 对象上没有 load 事件\n    console.log(&quot;window.onload&quot;);\n});\n</code></pre>\n<pre><code>document.readyState=loading\ndocument.readyState=interactive\ndocument.onDOMContentLoaded\nwindow.onDOMContentLoaded\ndocument.readyState=complete\nwindow.onload\n</code></pre>\n<h1>事件循环</h1>\n<p>第一次宏任务在 DOMContentLoaded 事件触发之后运行</p>\n<p>第一次微任务在 readystate 变为 loading 之后运行</p>\n<p>例如，在上文的 <code>&lt;script&gt;</code> 最前面添加这几行（添加在最后效果相同）</p>\n<pre><code class=\"language-js\">setTimeout(() =&gt; console.log(&quot;timer&quot;)); // 宏任务\nqueueMicrotask(() =&gt; console.log(&quot;microtask&quot;)); // 微任务\n</code></pre>\n<p>输出</p>\n<pre><code>document.readyState=loading\nmicrotask\ndocument.readyState=interactive\ndocument.onDOMContentLoaded\nwindow.onDOMContentLoaded\ntimer\ndocument.readyState=complete\nwindow.onload\n</code></pre>\n","tags":["web-api","js"]},{"id":"cpp-memory-management","url":"https://yieldray.fun/posts/cpp-memory-management","title":"C/C++内存分配及C++引用","date_published":"2022-04-30T13:38:45.000Z","date_modified":"2022-04-30T13:38:45.000Z","content_text":"<p>先前已经描述了 C 语言的内存分配，现在侧重于 C++</p>\n<h2>C 风格内存分配</h2>\n<p>在 C++ 中使用 malloc 系列函数，可直接引入 iostream 头文件。<br>当然，也可以引入 cstdlib 头文件</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n#include &lt;cstdlib&gt;\n#include &lt;cstdio&gt;\nusing namespace std;\nint main()\n{\n    int *a = (int *)malloc(sizeof(int) * 2);\n    a[0] = 1;\n    a[1] = 2;\n    // a[2] = 3; //  越界，极度危险！\n    a = (int *)realloc(a, sizeof(int) * 3);\n    a[2] = 3;\n    cout &lt;&lt; a[0] &lt;&lt; endl;\n    cout &lt;&lt; a[1] &lt;&lt; endl;\n    cout &lt;&lt; a[2] &lt;&lt; endl;\n    free(a);\n\n    int *__restrict b = (int *)calloc(3, sizeof(int));\n    // C99标准使用restrict，C++中使用__restrict\n    b[0] = 1;\n    b[1] = 2;\n    b[2] = 3;\n    cout &lt;&lt; b[0] &lt;&lt; endl;\n    cout &lt;&lt; b[1] &lt;&lt; endl;\n    cout &lt;&lt; b[2] &lt;&lt; endl;\n    free(b);\n}\n</code></pre>\n<h2>new/delete</h2>\n<p>new 和 delete，new[] 和 delete[] 必须配套使用</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\nusing namespace std;\nint main()\n{\n    int *p = new int;\n    *p = 10;\n    cout &lt;&lt; *p &lt;&lt; endl;\n    delete p;\n\n    int *q = new int[10];\n    for (int i = 0; i &lt; 10; i++)\n        q[i] = i;\n    for (int i = 0; i &lt; 10; i++)\n        cout &lt;&lt; q[i] &lt;&lt; endl;\n    delete[] q;\n\n    // 使用 nothrow 异常处理\n    int *r = new (nothrow) int[99999999999];\n    if (r == NULL)\n        cout &lt;&lt; &quot;Out of memory&quot; &lt;&lt; endl;\n    else\n        delete[] r;\n\n    // 使用 try/catch 异常处理\n    try\n    {\n        int *s = new int[99999999999];\n    }\n    catch (bad_alloc &amp;e)\n    {\n        cout &lt;&lt; &quot;Out of memory&quot; &lt;&lt; endl;\n    }\n    return 0;\n}\n</code></pre>\n<h2>C++ 引用</h2>\n<p>在函数参数外，引用是给已知变量起一别名。因此，声明引用时必须指定</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\nusing namespace std;\nstring print_bool(bool b)\n{\n    return b ? &quot;true&quot; : &quot;false&quot;;\n}\nint main()\n{\n    int a = 1;\n    int &amp;b = a;\n    int &amp;c = b; // 这里 &amp; 是引用运算符\n    cout &lt;&lt; print_bool(&amp;a == &amp;b) &lt;&lt; endl;\n    cout &lt;&lt; print_bool(&amp;b == &amp;c) &lt;&lt; endl; // 这里 &amp; 是取地址运算符\n}\n</code></pre>\n<p>在函数参数内，引用表示获取参数的引用而非其拷贝</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\nusing namespace std;\ntemplate &lt;typename T&gt;\nvoid change(T &amp;a, T b)\n{\n    a = b;\n}\nint main()\n{\n    int a = 1;\n    change(a, 2);\n    cout &lt;&lt; a &lt;&lt; endl; // 2\n}\n</code></pre>\n<p>引用可以认为没有创建类型，也可以认为是 Type&amp; 类型</p>\n<p>在 change 函数中， 参数 a 是 int&amp; 类型，则 a 只接受 int 类型的变量，所以</p>\n<pre><code class=\"language-cpp\">int a = 1;\nchange(2, a); // 不合法，2 不是变量\n\nconst int b = 2;\nchange(b, a); // 不合法，b 的类型为 const int\n</code></pre>\n<p>因此，change 的第一个参数 a 必须传入 T&amp; 类型的变量<br>而无法接受 常量 和 字面量常量</p>\n","tags":["cpp"]},{"id":"db-mongodb","url":"https://yieldray.fun/posts/db-mongodb","title":"MongoDB使用","date_published":"2022-04-21T21:54:32.000Z","date_modified":"2022-04-21T21:54:32.000Z","content_text":"<p>MongoDB 非常适合与 JavaScript 一起使用</p>\n<h1>概念</h1>\n<p>collection （可以看作 sql 的 table）</p>\n<p>document （可以看作 sql 的 row）</p>\n<p>每个 document 都是一个 <a href=\"https://bsonspec.org/\">BSON</a> 对象 （形式上类似于 JSON，或者说是 JavaScript\n对象）</p>\n<pre><code class=\"language-js\">// 一个 document\n{\n    &quot;_id&quot;: ObjectId(&quot;58904e3796f927a806a182d&quot;),\n    &quot;name&quot;: &quot;John&quot;,\n    &quot;age&quot;: 25,\n    &quot;email&quot;: &quot;john@email.email&quot;,\n    &quot;language&quot;: [&quot;C++&quot;, &quot;Python&quot;]\n}\n</code></pre>\n<p>collection 即 document 的集合</p>\n<p>MongoDB 支持的类型比较多，参考<a href=\"https://www.mongodb.com/docs/manual/reference/bson-types/\">https://www.mongodb.com/docs/manual/reference/bson-types/</a></p>\n<h1>命令行</h1>\n<p>可以在 <a href=\"https://www.mongodb.com/docs/manual/tutorial/getting-started/\">https://www.mongodb.com/docs/manual/tutorial/getting-started/</a> 体验 mongoDB 的 shell，它是一个 JavaScript 的 shell</p>\n<h2>db</h2>\n<p>显示当前数据库名称</p>\n<h2>use &lt;db&gt;</h2>\n<p>切换数据库，如果不存在则创建</p>\n<h2>db.collection.insertMany()</h2>\n<p>在指定 collection 插入多个 document</p>\n<pre><code class=\"language-js\">db.movies.insertMany([\n    {\n        title: &quot;Titanic&quot;,\n        year: 1997,\n        genres: [&quot;Drama&quot;, &quot;Romance&quot;],\n        rated: &quot;PG-13&quot;,\n        languages: [&quot;English&quot;, &quot;French&quot;, &quot;German&quot;, &quot;Swedish&quot;, &quot;Italian&quot;, &quot;Russian&quot;],\n        released: ISODate(&quot;1997-12-19T00:00:00.000Z&quot;),\n        awards: {\n            wins: 127,\n            nominations: 63,\n            text: &quot;Won 11 Oscars. Another 116 wins &amp; 63 nominations.&quot;,\n        },\n        cast: [&quot;Leonardo DiCaprio&quot;, &quot;Kate Winslet&quot;, &quot;Billy Zane&quot;, &quot;Kathy Bates&quot;],\n        directors: [&quot;James Cameron&quot;],\n    },\n    {\n        title: &quot;The Dark Knight&quot;,\n        year: 2008,\n        genres: [&quot;Action&quot;, &quot;Crime&quot;, &quot;Drama&quot;],\n        rated: &quot;PG-13&quot;,\n        languages: [&quot;English&quot;, &quot;Mandarin&quot;],\n        released: ISODate(&quot;2008-07-18T00:00:00.000Z&quot;),\n        awards: {\n            wins: 144,\n            nominations: 106,\n            text: &quot;Won 2 Oscars. Another 142 wins &amp; 106 nominations.&quot;,\n        },\n        cast: [&quot;Christian Bale&quot;, &quot;Heath Ledger&quot;, &quot;Aaron Eckhart&quot;, &quot;Michael Caine&quot;],\n        directors: [&quot;Christopher Nolan&quot;],\n    },\n    {\n        title: &quot;Spirited Away&quot;,\n        year: 2001,\n        genres: [&quot;Animation&quot;, &quot;Adventure&quot;, &quot;Family&quot;],\n        rated: &quot;PG&quot;,\n        languages: [&quot;Japanese&quot;],\n        released: ISODate(&quot;2003-03-28T00:00:00.000Z&quot;),\n        awards: {\n            wins: 52,\n            nominations: 22,\n            text: &quot;Won 1 Oscar. Another 51 wins &amp; 22 nominations.&quot;,\n        },\n        cast: [&quot;Rumi Hiiragi&quot;, &quot;Miyu Irino&quot;, &quot;Mari Natsuki&quot;, &quot;Takashi Naitè&quot;],\n        directors: [&quot;Hayao Miyazaki&quot;],\n    },\n    {\n        title: &quot;Casablanca&quot;,\n        genres: [&quot;Drama&quot;, &quot;Romance&quot;, &quot;War&quot;],\n        rated: &quot;PG&quot;,\n        cast: [&quot;Humphrey Bogart&quot;, &quot;Ingrid Bergman&quot;, &quot;Paul Henreid&quot;, &quot;Claude Rains&quot;],\n        languages: [&quot;English&quot;, &quot;French&quot;, &quot;German&quot;, &quot;Italian&quot;],\n        released: ISODate(&quot;1943-01-23T00:00:00.000Z&quot;),\n        directors: [&quot;Michael Curtiz&quot;],\n        awards: {\n            wins: 9,\n            nominations: 6,\n            text: &quot;Won 3 Oscars. Another 6 wins &amp; 6 nominations.&quot;,\n        },\n        lastupdated: &quot;2015-09-04 00:22:54.600000000&quot;,\n        year: 1942,\n    },\n]);\n</code></pre>\n<h2>db.collection.find()</h2>\n<pre><code class=\"language-javascript\">// 查询所有文档\ndb.movies.find({});\n// 查询 directors 值为 &quot;Christopher Nolan&quot; 的文档\ndb.movies.find({ directors: &quot;Christopher Nolan&quot; });\n// 查询 released 值小于（前于）ISODate(&quot;2000-01-01&quot;) 的文档，lt = less than\ndb.movies.find({ released: { $lt: ISODate(&quot;2000-01-01&quot;) } });\n// 查询 awards.wins 值大于 100 的文档，gt = greater than，注意此处的 awards.wins\ndb.movies.find({ &quot;awards.wins&quot;: { $gt: 100 } });\n// 查询 languages 包括 [&quot;Japanese&quot;, &quot;Mandarin&quot;] 的文档\ndb.movies.find({ languages: { $in: [&quot;Japanese&quot;, &quot;Mandarin&quot;] } });\n</code></pre>\n<p>指定返回字段</p>\n<pre><code class=\"language-js\">db.movies.find({}, { title: 1, directors: 1, year: 1 });\n// 无需指定_id，因为默认就会返回，如果不需要_id，可以设置为0\ndb.movies.find({}, { _id: 0 });\n</code></pre>\n<h2>db.collection.aggregate()</h2>\n<p>聚合查询</p>\n<pre><code class=\"language-js\">db.movies.aggregate([\n    { $unwind: &quot;$genres&quot; },\n    {\n        $group: {\n            _id: &quot;$genres&quot;,\n            genreCount: { $count: {} },\n        },\n    },\n    { $sort: { genreCount: -1 } },\n]);\n</code></pre>\n<h1>详细参见文档</h1>\n<p><a href=\"https://www.mongodb.com/docs/manual/crud/\">https://www.mongodb.com/docs/manual/crud/</a></p>\n","tags":["database"]},{"id":"cpp-function-pointer","url":"https://yieldray.fun/posts/cpp-function-pointer","title":"C/C++函数指针","date_published":"2022-04-10T14:04:08.000Z","date_modified":"2022-04-10T14:04:08.000Z","content_text":"<p>函数的类型由其参数及返回类型共同决定，与函数名无关。</p>\n<pre><code class=\"language-cpp\">// 对于函数定义\nint add(int nLeft,int nRight);\n// 其类型为\nint(int,int);\n</code></pre>\n<p>函数与其他数据类型不同的是，函数名位于形参列表之前，返回类型之后。<br>对于其他变量，类型在变量名之前，函数名却与函数类型是复合在一起的。</p>\n<p>自然地，函数指针的声明如下。按照 C 风格，将 <code>*pf</code> 看作整体<br>用括号确保优先级，否则 <code>*</code> 将与 <code>int</code> 结合</p>\n<pre><code class=\"language-cpp\">int (*pf)(int,int);\n// typedef的名称也是在相同的位置\ntypedef int (*PF)(int,int);\n// 所以函数指针的类型为\nint (*)(int,int);\n// 不能这样\ntypedef int (*)(int, int) pFunc;\n</code></pre>\n<p>注意到 <code>函数名</code> 和 <code>*函数名</code> 和 <code>&amp;函数名</code> 的地址都相同</p>\n<pre><code class=\"language-cpp\">#include &lt;cstdio&gt;\nint add(int nLeft, int nRight)\n{\n    return nLeft + nRight;\n}\nint main()\n{\n    printf(&quot;%p\\n&quot;, add);\n    printf(&quot;%p\\n&quot;, &amp;add);\n    printf(&quot;%p\\n&quot;, *add);\n}\n</code></pre>\n<p>输出（具体值取决于编译器）</p>\n<pre><code>00007ff623381591\n00007ff623381591\n00007ff623381591\n</code></pre>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\nusing namespace std;\nint add(int nLeft, int nRight)\n{\n    return nLeft + nRight;\n}\ntemplate &lt;typename T, typename U&gt;\nstring judge(T t, U u)\n{\n    return t == u ? &quot;true&quot; : &quot;false&quot;;\n}\nint main()\n{\n    cout &lt;&lt; judge(add, &amp;add) &lt;&lt; endl; // true\n    cout &lt;&lt; judge(add, *add) &lt;&lt; endl; // true\n}\n</code></pre>\n<p>发现对于函数，<code>*</code> 表现得不正常<br>认为函数类似数组，函数名等同于函数的地址，即有 <code>函数 == &amp;函数</code><br>因此认为 <code>*</code> 对函数操作有特殊的语义，对函数地址解除引用得到的还是函数地址</p>\n<p>函数指针则没有这种特殊待遇</p>\n<pre><code class=\"language-cpp\">#include &lt;cstdio&gt;\nusing namespace std;\nint add(int nLeft, int nRight)\n{\n    return nLeft + nRight;\n}\ntypedef int (*pFunc)(int, int);\nint main()\n{\n    pFunc pf = add;\n    printf(&quot;%p\\n&quot;, add);\n    printf(&quot;%p\\n&quot;, pf);\n    printf(&quot;%p\\n&quot;, *pf);\n    printf(&quot;%p\\n&quot;, &amp;pf);\n}\n</code></pre>\n<p>输出</p>\n<pre><code>00007ff6c1f91540\n00007ff6c1f91540\n00007ff6c1f91540\n00000096e93ff878\n</code></pre>\n<p>函数指针作为一个地址变量，当然有自己的地址。\n观察输出，发现<em>函数指针</em>等同于<em>对函数指针解除引用</em> ，这也与普通指针不同<br>认为<em>函数指针名</em>有特殊的语义，等同于其引用的函数</p>\n<p>上面的例子中，函数名与函数指针的地址相同，可以认为它们的类型也是相同的</p>\n<pre><code class=\"language-cpp\">#include &lt;cstdio&gt;\nint add(int nLeft, int nRight)\n{\n    return nLeft + nRight;\n}\nint doSomething(int a, int b, int(f)(int, int))\n{\n    return f(a, b);\n}\nint main()\n{\n    int (*fp)(int, int) = add;\n    printf(&quot;%d\\n&quot;, doSomething(1, 2, fp)); // 3\n    printf(&quot;%d\\n&quot;, doSomething(1, 2, add)); // 3\n}\n</code></pre>\n<p>更具体的例子</p>\n<pre><code class=\"language-cpp\">int main()\n{\n    int (*fp)(int, int) = add;\n    int (*doSth1)(int, int, int(int, int)) = doSomething;\n    int (*doSth2)(int, int, int (*)(int, int)) = doSomething;\n    printf(&quot;%d\\n&quot;, doSth1(1, 2, add)); // 3\n    printf(&quot;%d\\n&quot;, doSth1(1, 2, add)); // 3\n    printf(&quot;%d\\n&quot;, doSth1(1, 2, fp)); // 3\n    printf(&quot;%d\\n&quot;, doSth2(1, 2, fp)); // 3\n}\n</code></pre>\n<p>对于返回函数指针的函数，其类型比较复杂</p>\n<pre><code class=\"language-cpp\">#include &lt;cstdio&gt;\nint add(int a, int b)\n{\n    return a + b;\n}\ntypedef int (*pFunc)(int, int);\npFunc fname(int useless)\n{\n    return add;\n}\nint (*fnamep(int useless))(int x, int y) // 这里注意，x和y可有可无，并且在函数体内无效\n{// 完整定义，由内往外阅读。注意！fnamep是函数，返回函数指针（类型即上面的pFunc）。函数不能返回函数\n    return add;\n}\nint main()\n{\n    printf(&quot;%d\\n&quot;, fname(123)(1, 2)); // 3\n    printf(&quot;%d\\n&quot;, fnamep(123)(1, 2)); // 3\n}\n</code></pre>\n<p>c++ 中可以使用 delctype 和 auto 自动推导类型</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\nusing namespace std;\nint add(int a, int b)\n{\n    return a + b;\n}\ntypedef decltype(add) add_t;\nint main()\n{\n    add_t *add_func = add;\n    auto add_func2 = add;\n    cout &lt;&lt; add_func(1, 2) &lt;&lt; endl;  // 3\n    cout &lt;&lt; add_func2(1, 2) &lt;&lt; endl; // 3\n}\n</code></pre>\n<p>函数返回函数指针，更加直观</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\nusing namespace std;\nint add(int a, int b)\n{\n    return a + b;\n}\ntypedef decltype(add) add_t;\nauto fnamep1(int useless) -&gt; add_t *\n{\n    return add;\n}\nauto fnamep2(int useless) -&gt; int (*)(int, int)\n{\n    return add;\n}\ndecltype(add) *fnamep3(int useless)\n{\n    return add;\n}\nint main()\n{\n    cout &lt;&lt; fnamep1(123)(1, 2) &lt;&lt; endl; // 3\n    cout &lt;&lt; fnamep2(123)(1, 2) &lt;&lt; endl; // 3\n    cout &lt;&lt; fnamep3(123)(1, 2) &lt;&lt; endl; // 3\n}\n</code></pre>\n<p>成员函数</p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\nusing namespace std;\nclass Test\n{\npublic:\n    void hello()\n    {\n        cout &lt;&lt; &quot;hello&quot; &lt;&lt; endl;\n    }\n};\ntypedef void (Test::*CFP)();\nCFP func = &amp;Test::hello;\n\nint main()\n{\n    Test test;\n    (test.*func)();\n}\n</code></pre>\n","tags":["cpp"]},{"id":"nodejs-npm","url":"https://yieldray.fun/posts/nodejs-npm","title":"npm","date_published":"2022-04-07T14:27:57.000Z","date_modified":"2022-04-07T14:27:57.000Z","content_text":"<p><a href=\"https://docs.npmjs.com/cli/v8/commands\">https://docs.npmjs.com/cli/v8/commands</a></p>\n<h1>仅安装生产依赖</h1>\n<pre><code class=\"language-sh\">$ npm install --production\n# or\n$ NODE_ENV=production npm install\n</code></pre>\n<h1>声明依赖为同级依赖</h1>\n<pre><code class=\"language-json\">{\n    &quot;name&quot;: &quot;PackageA&quot;,\n    &quot;peerDependencies&quot;: {\n        &quot;PackageB&quot;: &quot;^1.0.0&quot;\n    }\n}\n\n// $ npm install PackageA\n// MyProject\n// |- node_modules\n//    |- PackageA\n\n//    |- PackageB\n</code></pre>\n<h1>可选依赖</h1>\n<pre><code class=\"language-json\">{\n    &quot;name&quot;: &quot;PackageA&quot;,\n    &quot;optionalDependencies&quot;: {\n        &quot;PackageB&quot;: &quot;^1.0.0&quot;\n    }\n}\n</code></pre>\n<h1>按照 package-lock.json 安装</h1>\n<pre><code class=\"language-sh\">$ npm ci\n</code></pre>\n<p>先删除 node_modules ，然后再按照 package-lock.json 或 npm-shrinkwrap.json 安装依赖</p>\n<h1>npm 镜像</h1>\n<pre><code class=\"language-sh\">$ alias cnpm=&quot;npm --registry=https://registry.npmmirror.com&quot;\n$ npm install -g cnpm --registry=https://registry.npmmirror.com\n$ npm config set registry &quot;https://registry.npmmirror.com&quot;\n\n# use .npmrc in ~/\nregistry=&quot;https://registry.npmmirror.com&quot;\n</code></pre>\n<p><a href=\"https://docs.npmjs.com/cli/configuring-npm/npmrc\">参见 npmrc</a></p>\n<h1>使用 npm-shrinkwrap.json</h1>\n<pre><code class=\"language-sh\">$ npm shrinkwrap\n</code></pre>\n<p>生成 npm-shrinkwrap.json 文件，此后 npm install 将会优先使用 npm-shrinkwrap.json</p>\n<h1>检查陈旧的依赖</h1>\n<pre><code class=\"language-sh\">$ npm outdated\nPackage      Current   Wanted   Latest  Location                  Depended by\nglob          5.0.15   5.0.15    6.0.1  node_modules/glob         dependent-package-name\nnothingness    0.0.3      git      git  node_modules/nothingness  dependent-package-name\nnpm            3.5.1    3.5.2    3.5.1  node_modules/npm          dependent-package-name\nlocal-dev      0.0.3   linked   linked  local-dev                 dependent-package-name\nonce           1.3.2    1.3.3    1.3.3  node_modules/once         dependent-package-name\n</code></pre>\n<h1>.npmrc</h1>\n<p>node 安装时自动安装 npm（到 node 同级目录下），<br>如果通过 <code>npm i -g npm</code> 更新过 npm 则还存在更新后的 npm（在 npm 安装全局包的目录下）</p>\n<p>项目配置文件: <code>项目根目录/.npmrc</code><br>用户配置文件：<code>~/.npmrc</code><br>全局配置文件：<code>$PREFIX/etc/npmrc</code><br>npm 内置配置文件： <code>npm安装目录/npmrc</code></p>\n<p>通过 <code>npm config</code> 修改的是用户配置文件（<code>~/.npmrc</code>）<br>加上 <code>-g</code> （<code>npm config -g</code>）则对应全局配置文件</p>\n<pre><code class=\"language-ini\"># 全局安装目录\nprefix=/usr/local/npm\n# 缓存目录\ncache=~/.cache/npm_cache\n</code></pre>\n<p>命令行操作如下</p>\n<pre><code class=\"language-sh\">$ npm config --help\nManage the npm configuration files\n\nUsage:\nnpm config set &lt;key&gt;=&lt;value&gt; [&lt;key&gt;=&lt;value&gt; ...]\nnpm config get [&lt;key&gt; [&lt;key&gt; ...]]\nnpm config delete &lt;key&gt; [&lt;key&gt; ...]\nnpm config list [--json]\nnpm config edit\n\n# 设置镜像源\nnpm config set registry https://repo.huaweicloud.com/repository/npm/\n# 获取用户配置文件路径\nnpm config get userconfig\n</code></pre>\n<p>配置文件格式同 ini，例如</p>\n<pre><code class=\"language-ini\">; registry=https://registry.npmjs.org/\nregistry=https://registry.npmmirror.com/\n</code></pre>\n","tags":["npm","nodejs"]},{"id":"js-object-tostring","url":"https://yieldray.fun/posts/js-object-tostring","title":"Object.prototype.toString() 方法","date_published":"2022-04-02T10:33:39.000Z","date_modified":"2022-04-02T10:33:39.000Z","content_text":"<p>对于许多内置的 JavaScript 对象类型，toString() 方法自动识别并返回特定的类型标签</p>\n<pre><code class=\"language-js\">Object.prototype.toString.call(&quot;foo&quot;); // &quot;[object String]&quot;\nObject.prototype.toString.call({}); // &quot;[object Object]&quot;\nObject.prototype.toString.call([1, 2]); // &quot;[object Array]&quot;\nObject.prototype.toString.call(Symbol()); // &quot;[object Symbol]&quot;\nObject.prototype.toString.call(3); // &quot;[object Number]&quot;\nObject.prototype.toString.call(5n); // &quot;[object BigInt]&quot;\nObject.prototype.toString.call(true); // &quot;[object Boolean]&quot;\nObject.prototype.toString.call(undefined); // &quot;[object Undefined]&quot;\nObject.prototype.toString.call(null); // &quot;[object Null]&quot;\n// ... and more\n</code></pre>\n<p>另外一些对象类型则不然，toString() 方法能识别它们是因为引擎为它们设置好了 toStringTag 标签：</p>\n<pre><code class=\"language-js\">Object.prototype.toString.call(new Map()); // &quot;[object Map]&quot;\nObject.prototype.toString.call(function* () {}); // &quot;[object GeneratorFunction]&quot;\nObject.prototype.toString.call(Promise.resolve()); // &quot;[object Promise]&quot;\n// ... and more\n</code></pre>\n<p>对于自定义对象，没有被重载的情况下返回<code>&quot;[object Object]&quot;</code><br>也就是说，<code>Object.prototype.toString()</code>方法总返回<code>&quot;[object toStringTag]&quot;</code> 的形式<br>实际上对于自定义对象，<code>toStringTag</code>会取得<code>Symbol.toStringTag</code>属性值</p>\n<pre><code class=\"language-js\">const myObj = {\n    [Symbol.toStringTag]: &quot;MyTag&quot;,\n};\nconsole.log(myObj.toString()); // [object MyTag]\n\nclass ValidatorClass {\n    get [Symbol.toStringTag]() {\n        return &quot;Validator&quot;;\n    }\n}\nObject.prototype.toString.call(new ValidatorClass()); // &quot;[object Validator]&quot;\n</code></pre>\n<p>综上，<code>Object.prototype.toString()</code>方法适用于判断原生对象的类型，<br>这必须建立在自定义对象的<code>Symbol.toStringTag</code>属性没有被修改成和原生<code>toStringTag</code>相同的情况下</p>\n<pre><code class=\"language-js\">// 利用toString进行类型检测\n\nconst check = (any, cons) =&gt; Object.prototype.toString.call(any).slice(8, -1) === cons.name;\n\ncheck({}, Object); // =&gt; true\ncheck([], Array); // =&gt; true\ncheck(123, Number); // =&gt; true\ncheck(true, Boolean); // =&gt; true\ncheck(&quot;str&quot;, String); // =&gt; true\n</code></pre>\n","tags":["js"]},{"id":"js-prototype","url":"https://yieldray.fun/posts/js-prototype","title":"理解JavaScript原型继承","date_published":"2022-03-31T16:51:27.000Z","date_modified":"2022-03-31T16:51:27.000Z","content_text":"<p>参见：<a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Inheritance_and_the_prototype_chain\">https://developer.mozilla.org/docs/Web/JavaScript/Inheritance_and_the_prototype_chain</a></p>\n<h1>原型</h1>\n<p>面向对象编程有三大途径：类(classes)、原型(prototypes) 和 <a href=\"https://en.wikipedia.org/wiki/Multiple_dispatch\">多方法(multimethods)</a>。</p>\n<p>在基于类的系统中，存在类和对象。我们往往将类看作模板，将对象看作实例。只有类可以继承类。<br>在基于原型的系统中，不存在这种意义的<em>类</em>。相反，只存在对象，而对象可以相互<em>委托</em>，被委托的那个对象称为原型，从而实现继承。</p>\n<p>JavaScript 是最流行的基于<strong>原型</strong>的<strong>面向对象</strong>编程语言之一。</p>\n<blockquote>\n<p>JavaScript 原型的实质就是将一个对象的 <code>[[prototype]]</code>（语言内部槽）指向为另一个已知对象<br><a href=\"https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinaryget\">获取一个对象的属性</a>时，先在对象自身上查找，再逐级向其 <code>[[prototype]]</code>（原型链）上查找，<br>直至找到或者未找到（抵达原型链的根：<code>null</code>）而返回 undefined</p>\n</blockquote>\n<p>每个 JavaScript 对象都有一个特殊的内部槽 <code>[[Prototype]]</code>（参见：<a href=\"https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots\">ECMA262</a>），它指向该对象的<em>原型对象</em><br>通过 <a href=\"https://tc39.es/ecma262/#sec-object.getprototypeof\"><code>Object.getPrototypeOf(obj)</code></a> 方法，可以获取指定对象的原型对象。</p>\n<blockquote>\n<p>也可以通过这个对象（原型上的）的 <code>__proto__</code> 属性获取（非标准）获得该原型对象<br>注意：<code>__proto__</code> 是从 <code>Object.prototype</code> 上继承的属性（实际上是一个 <code>getter</code> 和 <code>setter</code>）。所以该属性可以被覆写（override）<br>而 <code>Object.getPrototypeOf()</code> 必然能获取到一个对象的 <code>[[Prototype]]</code></p>\n<pre><code class=\"language-js\">// 例如：\nObject.getPrototypeOf(Object.create(null)); // =&gt; null （[[Prototype]] 为 null）\nObject.create(null).__proto__; // =&gt; undefined （不存在 __proto__ 这个属性）\n</code></pre>\n<pre><code class=\"language-js\">const obj = {};\nconsole.assert(Object.getPrototypeOf(obj) === obj.__proto__);\n</code></pre>\n</blockquote>\n<h2>直接（手动）设置原型</h2>\n<p>创建对象后再设置原型</p>\n<pre><code class=\"language-js\">const obj = {}; // 这里假设obj是任意对象\nconst proto = { fallback: &quot;fallback&quot; }; // 这里假设proto是要设置的原型对象\n\n// 1.非标准\nobj.__proto__ = proto;\n// 2.标准\nObject.setPrototypeOf(obj, proto);\n\n// 测试\nconsole.log(obj.fallback); // =&gt; &quot;fallback&quot;\n</code></pre>\n<p>在创建对象的同时设置原型</p>\n<pre><code class=\"language-js\">// 1.不应该这样（**注意**：这种方法也是可以正确建立原型链的，原理是 __proto__ 是原型上的 setter）\nconst obj = { __proto__: proto };\n// 2.应该\nconst obj = Object.create(proto);\n</code></pre>\n<p>虽然 js 引擎往往实现了从对象的 <code>__proto__</code> 属性访问其原型，但该 <code>__proto__</code> 属性默认情况下是在 <code>Object.prototype</code> 对象上，<br>不应在对象自身上设置 <code>__proto__</code> 属性，这往往是造成安全问题的来源。</p>\n<h2>通过构造函数自动设置原型（通过 new 操作符）</h2>\n<p>下面通过两种方式声明一个构造函数<br>注意：这两种方式几乎是等效的（只不过类是 ES6 引入的，因此会更加严格），下面不加以区分</p>\n<blockquote>\n<p>换句话说：构造函数就相当于类</p>\n</blockquote>\n<pre><code class=\"language-js\">// 1.ES5\nfunction Cls(a) {\n    this.a = a;\n}\nCls.prototype.fallback = &quot;fallback&quot;;\n\n// 2.ES6\nclass Cls {\n    constructor(a) {\n        this.a = a;\n    }\n    fallback = &quot;fallback&quot;;\n}\n\n// 测试\nconst instance = new Cls(&quot;a&quot;);\nconsole.log(instance.a); // =&gt; &quot;a&quot;\nconsole.log(instance.fallback); // =&gt; &quot;fallback&quot;\n// 函数的 prototype 属性指向一个特殊的对象\nconsole.assert(Cls.prototype.constructor === Cls);\n</code></pre>\n<p>通过构造函数创建的对象，其原型对象就是构造函数的 prototype 属性所指向的那个对象</p>\n<pre><code class=\"language-js\">console.assert(instance.__proto__ === Cls.prototype);\n</code></pre>\n<p>通过构造函数创建的对象，它的原型对象上存在 <code>constructor</code> 属性，指向其构造函数<br><em>这就与手动创建对象再设置其原型不同，因为其原型对象上存在指向构造函数的 constructor 属性</em></p>\n<pre><code class=\"language-js\">console.assert(instance.constructor === Cls);\nconsole.assert(instance.__proto__.constructor === instance.constructor);\n</code></pre>\n<h2>普通函数</h2>\n<p>Javascript 函数继承自 Function，函数是对象，自身具有 <code>prototype</code> 属性（自动生成），指向一个特殊的对象（自动生成）<br>（上面提到，如果函数被用作构造函数，那么该构造函数的 prototype 属性就是其 实例 的 原型）</p>\n<pre><code class=\"language-js\">const func = function () {}; // func 是 Function 的实例对象\nconsole.assert(func.constructor === Function);\nconsole.assert(func.__proto__ === Function.prototype);\nObject.getOwnPropertyNames(func); // =&gt; [&#39;length&#39;, &#39;name&#39;, &#39;arguments&#39;, &#39;caller&#39;, &#39;prototype&#39;]\nfunc.prototype; // =&gt; {constructor: ƒ}  函数的 prototype 属性是 Object 实例，由 JavaScript 引擎自动生成\nconsole.assert(func.prototype.constructor === func); // 函数的 prototype 属性指向的对象，具有 constructor 属性，指向函数自身\n</code></pre>\n<p>注意：箭头函数也继承自 Function，但其自身只存在 length 和 name 属性<br>（没有 <code>prototype</code> 属性，因此不能作构造函数使用）</p>\n<pre><code class=\"language-js\">const af = () =&gt; {};\nconsole.assert(af.constructor === Function);\nconsole.assert(af.__proto__ === Function.prototype);\nObject.getOwnPropertyNames(af); // =&gt; [ &#39;length&#39;, &#39;name&#39; ]\n</code></pre>\n<h2>new 操作符做了什么</h2>\n<p>new 操作符要求其后面是一个构造函数，否则抛出 TypeError 异常（任何一个函数都可以是构造函数，只不过一般命名时首字母大写）<br>参见：<a href=\"https://tc39.es/ecma262/#sec-evaluatenew\">https://tc39.es/ecma262/#sec-evaluatenew</a></p>\n<pre><code class=\"language-js\">function Cls(a) {\n    this.a = a;\n}\nCls.prototype.fallback = &quot;fallback&quot;;\n\nconst instance = {};\nCls.call(instance, a); // a 是构造函数的参数\nObject.setPrototypeOf(instance, Cls.prototype); // 设置原型对象\n</code></pre>\n<p>构造函数（任意函数）具有 <code>[[Construct]]</code> 内部槽，构造函数实例化对象就是调用了此方法<br>参见： <a href=\"https://tc39.es/ecma262/#sec-ecmascript-function-objects-construct-argumentslist-newtarget\">https://tc39.es/ecma262/#sec-ecmascript-function-objects-construct-argumentslist-newtarget</a></p>\n<h1>继承</h1>\n<p>继承的实质就是将 <em>子构造函数的 prototype 属性</em> 的 <em>原型</em> 设置为 <em>父构造函数的 prototype 属性</em></p>\n<pre><code class=\"language-js\">Object.setPrototypeOf(SubClass.prototype, ParentClass.prototype);\n</code></pre>\n<blockquote>\n<p>继承是发生在构造函数之间，而不是实例对象之间。因此我们只会说子类继承父类，而不是实例对象去继承别的实例对象。<br>注意这之间的区别：因为构造函数也是对象，但在这里不将其称为实例对象，而称为类。</p>\n</blockquote>\n<p><img src=\"https://zh.javascript.info/article/class-inheritance/animal-rabbit-extends.svg\" alt=\"extends\"><br>(本图来自<a href=\"https://zh.javascript.info/class-inheritance\">https://zh.javascript.info/class-inheritance</a>)</p>\n<h1>有关原型的重要函数</h1>\n<p>这里不讨论 <code>Symbol</code></p>\n<h3>Object.getPrototypeOf(obj)</h3>\n<p>返回指定对象的原型（内部 <code>[[Prototype]]</code> 槽的值）。如果没有继承属性，则返回 <code>null</code></p>\n<h3>Object.getOwnPropertyNames(obj)</h3>\n<p>返回一个由指定对象的所有自身属性的属性名（包括不可枚举属性但不包括 <code>Symbol</code> 值作为名称的属性）组成的数组。</p>\n<pre><code class=\"language-js\">const func = function () {};\nconsole.log(Object.getOwnPropertyNames(func)); // =&gt; [&#39;length&#39;, &#39;name&#39;, &#39;arguments&#39;, &#39;caller&#39;, &#39;prototype&#39;]\n</code></pre>\n<h3>Object.prototype.hasOwnProperty(prop)</h3>\n<p>这是一个原型方法，在对象自身上调用时，指示对象自身属性中是否具有指定的属性</p>\n<pre><code class=\"language-js\">const obj = {};\nobj.hasOwnProperty(&quot;toString&quot;); // =&gt; false\n</code></pre>\n<p>顺便提一句，<code>in</code> 操作符会在原型链上检查</p>\n<h3>Object.hasOwn(instance, prop)</h3>\n<p><em>这是一个较新的函数，目前 chrome&gt;=93 才支持</em><br><code>hasOwnProperty</code> 方法是一个原型方法，只有继承了 <code>Object</code> 的对象才有此原型方法，而且这个方法可以被覆盖<br>但 <code>Object.hasOwn()</code> 是一个静态方法，适用于任何对象</p>\n<pre><code class=\"language-js\">const obj = {};\nObject.hasOwn(obj, &quot;toString&quot;); // =&gt; false\n\nconst alone = Object.create(null);\nalone.hasOwnProperty(&quot;toString&quot;); // =&gt; Uncaught TypeError: alone.hasOwnProperty is not a function\nObject.hasOwn(alone, &quot;toString&quot;); // =&gt; false\n\n// polyfill\nconst hasOwn = (instance, prop) =&gt; Object.prototype.hasOwnProperty.call(instance, prop);\n</code></pre>\n<h1>instanceof</h1>\n<p><code>lhs instanceof rhs</code><br>检查 lhs 是否继承 rhs<br>即：lhs 的原型链上是否有等于 rhs.prototype 的对象<br>换言之，lhs 的原型链上是否有构造函数是 rhs 的对象</p>\n<p>模拟一个 <code>instanceof</code></p>\n<pre><code class=\"language-js\">// 递归\nconst instanceOf1 = (obj, func) =&gt; {\n    if (!(obj &amp;&amp; [&quot;object&quot;, &quot;function&quot;].includes(typeof obj))) return false;\n    let proto = Object.getPrototypeOf(obj);\n    if (proto === func.prototype) return true;\n    if (proto === null) return false;\n    return instanceOf1(proto, func);\n};\n// 迭代\nconst instanceOf2 = (obj, func) =&gt; {\n    if (!(obj &amp;&amp; [&quot;object&quot;, &quot;function&quot;].includes(typeof obj))) return false;\n    let proto = obj;\n    while ((proto = Object.getPrototypeOf(proto))) {\n        // if (proto === null) return false;\n        if (proto === func.prototype) return true;\n    }\n    return false;\n};\n</code></pre>\n<h1>悖论</h1>\n<pre><code class=\"language-js\">// Object 是一个构造函数，所以继承 Function\nconsole.assert(Object instanceof Function);\n// Function 是一个对象，继承 Object\nconsole.assert(Function instanceof Object);\n</code></pre>\n<p>实际上 <code>Function.__proto__</code> 指向 <code>Function.prototype</code>，而不是 <code>Object.prototype</code><br>并且有 <code>Function.__proto__ === Function.prototype === Object.__proto__</code></p>\n<pre><code class=\"language-js\">console.assert(Function.prototype === Function.__proto__);\nconsole.assert(Function.prototype === Object.__proto__);\n</code></pre>\n<p><code>Object.prototype.__proto__</code> 为 <code>null</code></p>\n<p>下面制作的图片为了准确，使用 <code>[[prototype]]</code> 而不是 <code>__proto__</code>\n<img src=\"https://s2.loli.net/2022/04/01/qmHksRaGNQDXOvV.png\" alt=\"prototype.png\"></p>\n<h1>关于 Object.create()</h1>\n<p><code>Object.create()</code> 方法创建的对象，将给定对象作为新创建对象的 <code>[[Prototype]]</code></p>\n<pre><code class=\"language-js\">const provider = { a: 123 };\nconst created = Object.create(provider);\nconsole.log(created.a); // =&gt; 123\nconsole.assert(created.__proto__ === provider);\nconsole.assert(created.constructor === Object);\n\nconst provider2 = Object.create(null);\nprovider2.a = 456;\nconst created2 = Object.create(provider2);\nconsole.log(created.a); // =&gt; 456\nconsole.log(created2.__proto__); // =&gt; undefined\nconsole.assert(Object.getPrototypeOf(created2) === provider2);\nconsole.log(created2.constructor); // =&gt; undefined\n</code></pre>\n<p>结合上面的图片发现，对象字面量 <code>{}</code> 等效于 <code>new Object()</code> 构造，因此其 <code>__proto__</code> 属性指向 <code>Object.prototype</code><br><code>get __proto__: ƒ __proto__()</code>和 <code>set __proto__: ƒ __proto__()</code> 正是通过继承 <code>Object.prototype</code> 得到的<br><code>Object.create(null)</code> 当然没有继承 <code>Object.prototype</code>，所以也就没有 <code>__proto__</code> 属性</p>\n<p>额外提一句，<code>Object.create(o: any, properties?: PropertyDescriptorMap)</code> 的第二个参数相当于 <code>Object.defineProperties&lt;T&gt;(o: T, properties: PropertyDescriptorMap): T</code></p>\n<h1>总结</h1>\n<p>一般来说，一个对象</p>\n<ul>\n<li>原型链上具有 <code>__proto__</code> 属性，指向其构造函数的 <code>prototype</code> 属性</li>\n<li>原型链上具有 <code>constructor</code> 属性，指向其构造函数</li>\n<li><code>obj.__proto__ === obj.constructor.prototype</code></li>\n</ul>\n<p>函数作为特殊的对象</p>\n<ul>\n<li>自身具有 <code>prototype</code> 属性，new 操作符会将被创建的对象的 <code>[[Prototype]]</code> 指向函数的 <code>prototype</code> 属性</li>\n<li><code>func.prototype.constructor === func</code></li>\n</ul>\n","tags":["js"]},{"id":"jsonwebtoken","url":"https://yieldray.fun/posts/jsonwebtoken","title":"JsonWebToken","date_published":"2022-03-18T19:04:56.000Z","date_modified":"2024-03-21T12:00:00.000Z","content_text":"<blockquote>\n<p>下文基本翻译自：<a href=\"https://jwt.io/introduction\">https://jwt.io/introduction</a><br>另见：\nThe JWT Handbook: <a href=\"https://jwt.surge.sh\">https://jwt.surge.sh</a><br>JSON Web Token 入门教程：<a href=\"https://ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html\">https://ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html</a></p>\n</blockquote>\n<h2>什么是 JSON Web Token？</h2>\n<p>JSON Web Token (JWT) 是一种开放标准 (<a href=\"https://tools.ietf.org/html/rfc7519\">RFC 7519</a>)，它定义了一种紧凑且自包含的方式，可以以 JSON 对象的形式在各方之间安全地传输信息。这些信息可以得到验证和信任，因为它们是经过数字签名的。可以通过使用密钥（使用 <strong>HMAC</strong> 算法）或使用 <strong>RSA</strong> 或 <strong>ECDSA</strong> 的公钥/私钥对来对 JWT 进行签名。</p>\n<p>尽管可以对 JWT 进行加密，以便在各方之间提供机密性，但我们重点关注<em>签名</em>令牌。签名令牌可以验证其中包含的声明的<em>完整性</em>，而加密令牌会向其他方<em>隐藏</em>这些声明。当使用公钥/私钥对对令牌签名时，签名还证明持有私钥的一方就是对其进行签名的一方。</p>\n<h2>你应该在何时使用 JSON Web Token？</h2>\n<p>以下是一些使用 JSON Web Token 有用的场景：</p>\n<ul>\n<li><p>**授权：**这是使用 JWT 最常见的场景。用户登录后，每个后续请求都将包含 JWT，允许用户访问该令牌允许的路由、服务和资源。单点登录是一种广泛使用 JWT 的功能，因为其开销小，而且可以轻松地用于不同的域中。</p>\n</li>\n<li><p>**信息交换：**JSON Web Token 是安全地在各方之间传输信息的好方法。由于可以对 JWT 进行签名（例如，使用公钥/私钥对），因此你可以确信发送者就是他们所说的那样。此外，由于签名是使用 Header 和有效负载计算的，因此你还可以验证内容是否未被篡改。</p>\n</li>\n</ul>\n<h2>JSON Web Token 的结构是什么？</h2>\n<p>在紧凑形式中，JSON Web Token 由三个用点（<code>.</code>）分隔的部分组成，它们是：</p>\n<ul>\n<li>Header</li>\n<li>有效负载（Payload）</li>\n<li>签名（Signature）</li>\n</ul>\n<p>因此，一个 JWT 通常如下所示。</p>\n<p><code>xxxxx.yyyyy.zzzzz</code></p>\n<p>我们分解一下不同的部分。</p>\n<h3>Header</h3>\n<p>Header<em>通常</em>由两部分组成：令牌的类型（即 JWT）和正在使用的签名算法，例如 HMAC SHA256 或 RSA。</p>\n<p>例如：</p>\n<pre><code>{\n  &quot;alg&quot;: &quot;HS256&quot;,\n  &quot;typ&quot;: &quot;JWT&quot;\n}\n</code></pre>\n<p>然后，将此 JSON <strong>Base64Url</strong> 编码为 JWT 的第一部分。</p>\n<h3>有效负载</h3>\n<p>令牌的第二部分是有效负载，其中包含声明。声明是对实体（通常是用户）和附加数据的陈述。有三种类型的声明：<em>已注册</em>、<em>公共</em>和<em>私有</em>声明。</p>\n<ul>\n<li><p><a href=\"https://tools.ietf.org/html/rfc7519#section-4.1\"><strong>已注册声明</strong>（Registered claims）</a>：这些是一组预定义的声明，它们不是强制性的，但建议使用，以提供一组有用的、可互操作的声明。其中一些声明为：<strong>iss</strong>（颁发者）、<strong>exp</strong>（到期时间）、<strong>sub</strong>（主题）、<strong>aud</strong>（受众），以及 <a href=\"https://tools.ietf.org/html/rfc7519#section-4.1\">其它</a>。</p>\n<blockquote>\n<p>请注意，由于 JWT 的目的是紧凑，因此声明名称只有三个字符长。</p>\n</blockquote>\n</li>\n<li><p><a href=\"https://tools.ietf.org/html/rfc7519#section-4.2\"><strong>公共声明</strong>（Public claims）</a>：这些声明可以由使用 JWT 的人随意定义。但是，为了避免冲突，它们应在 <a href=\"https://www.iana.org/assignments/jwt/jwt.xhtml\">IANA JSON Web Token Registry</a> 中进行定义，或定义为包含防冲突命名空间的 URI。</p>\n</li>\n<li><p><a href=\"https://tools.ietf.org/html/rfc7519#section-4.3\"><strong>私有声明</strong>（Private claims）</a>：这些是创建的自定义声明，用于在同意使用它们且不是<em>已注册</em>或<em>公共</em>声明的各方之间共享信息。</p>\n</li>\n</ul>\n<p>一个示例有效负载可以是：</p>\n<pre><code>{\n  &quot;sub&quot;: &quot;1234567890&quot;,\n  &quot;name&quot;: &quot;John Doe&quot;,\n  &quot;admin&quot;: true\n}\n</code></pre>\n<p>然后将有效负载 <strong>Base64Url</strong> 编码为 JSON Web Token 的第二部分。</p>\n<blockquote>\n<p>请注意，对于已签名令牌，此信息虽然受保护，以免篡改，但任何人都可以读到。除非已加密，否则不要将机密信息放入 JWT 的有效负载或 Header 中。</p>\n</blockquote>\n<h3>签名</h3>\n<p>要创建签名部分，你必须获取编码 Header、编码有效负载、一个密钥、Header 中指定的算法，并对其进行签名。</p>\n<p>例如，如果你想使用 HMAC SHA256 算法，则将按如下方式创建签名：</p>\n<pre><code>HMACSHA256(\n  base64UrlEncode(header) + &quot;.&quot; +\n  base64UrlEncode(payload),\n  secret)\n</code></pre>\n<p>签名用于验证消息不会在传输过程中被更改，并且，对于用私钥签名的令牌，它还可以验证 JWT 的发送方是否为其声称的身份。</p>\n<blockquote>\n<p>可使用 <em>HMAC</em> 或 <em>公钥/密钥对</em> 算法进行签名。显然，HMAC 的 secret 就相当于 公钥/密钥对 的 公钥+私钥<br>有关具体密钥算法，另见：<a href=\"https://learn.microsoft.com/zh-cn/azure/key-vault/keys/about-keys-details\">密钥类型、算法和操作</a></p>\n</blockquote>\n<h3>Putting all together</h3>\n<p>输出是三个 Base64-URL 字符串，它们由点号分隔，可轻松地通过 HTML 和 HTTP 环境传递，而与基于 XML 的标准（例如 SAML）相比，其更紧凑。</p>\n<p>下面显示了一个具有先前编码的 Header 和有效负载、并使用密钥进行签名的 JWT。<img src=\"https://cdn.auth0.com/content/jwt/encoded-jwt3.png\" alt=\"编码的 JWT\"></p>\n<p>如果你想使用 JWT 并将这些概念付诸实践，则可以使用 <a href=\"https://jwt.io/#debugger-io\">jwt.io 调试器</a> 来解码、验证和生成 JWT。</p>\n<p><img src=\"https://cdn.auth0.com/website/jwt/introduction/debugger.png\" alt=\"JWT.io 调试器\"></p>\n<h2>JSON Web Token 如何工作？</h2>\n<p>在身份验证中，当用户使用其凭据成功登录时，将返回一个 JSON Web Token。由于令牌是凭据，因此必须非常小心，以防止出现安全问题。通常，你不应将令牌保留的时间超过需要。</p>\n<p>你也不应 <a href=\"https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage\">由于缺乏安全性而将敏感会话数据存储在浏览器存储中</a>。</p>\n<p>每当用户想要访问受保护的路由或资源时，用户代理都应发送 JWT，通常是在 <strong>Authorization</strong> Header 中使用 <strong>Bearer</strong> 架构。Header 的内容应如下所示：</p>\n<pre><code>Authorization: Bearer &lt;token&gt;\n</code></pre>\n<p>在某些情况下，这可以是一种无状态授权机制。服务器的受保护路由将在 <code>Authorization</code> Header 中检查有效的 JWT，如果存在，则用户将被允许访问受保护的资源。如果 JWT 包含必要的数据，则可能减少特定操作的数据库访问需求，尽管情况并非总是如此。</p>\n<p>请注意，如果你通过 HTTP Header 发送 JWT 令牌，你应尝试防止令牌变得太大。某些服务器不接受 Header 中超过 8 KB 的内容。如果你试图在 JWT 令牌中嵌入过多信息，例如此处包括了所有用户权限，则可能需要一种替代方案，例如 <a href=\"https://fga.dev/\">Auth0 细粒度授权</a>。</p>\n<p>如果令牌在 <code>Authorization</code> Header 中发送，那么跨源资源共享 (CORS) 不会成为问题，因为它不使用 cookie。</p>\n<p>下图显示了如何获取 JWT 以及如何使用 JWT 来访问 API 或资源：</p>\n<p><img src=\"https://cdn.auth0.com/website/jwt/introduction/client-credentials-grant.png\" alt=\"JSON Web Token 如何工作\"></p>\n<ol>\n<li>应用程序或客户端向授权服务器请求授权。这是通过不同的授权流执行的。例如，一个典型的 <a href=\"http://openid.net/connect/\">OpenID Connect</a> 兼容 Web 应用程序将使用 <a href=\"http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth\">授权码流</a> 经过 <code>/oauth/authorize</code> 端点。</li>\n<li>当授予授权时，授权服务器向应用程序返回访问令牌。</li>\n<li>应用程序使用访问令牌访问受保护的资源（如 API）。</li>\n</ol>\n<p>请注意，对于已签名令牌，令牌中包含的所有信息都对用户或其他方公开，即使他们无法更改令牌也是如此。这意味着你不应在令牌中放入机密信息。</p>\n<h2>为什么我们应该使用 JSON Web Token？</h2>\n<p>让我们讨论 <strong>JSON Web Token (JWT)</strong> 与 <strong>简单 Web 令牌 (SWT)</strong> 和 <strong>安全断言标记语言令牌 (SAML)</strong> 相比的优势。</p>\n<p>由于 JSON 比 XML 的冗余性较低，因此编码后的大小也较小，这使得 JWT 比 SAML 更紧凑。这使得 JWT 成为在 HTML 和 HTTP 环境中传递的明智选择。</p>\n<p>从安全角度看，SWT 只能使用 HMAC 算法通过共享密钥进行对称签名。然而，JWT 和 SAML 令牌可以使用 X.509 证书中的公钥/私钥对进行签名。与使用 JSON 签名的简单性相比，使用 XML 数字签名对 XML 签名而不会引入晦涩的安全漏洞非常困难。</p>\n<p>大多数编程语言中都有 JSON 解析器，因为它们直接映射到对象。相反，XML 却没有自然的文档到对象的映射。这使得使用 JWT 变得比使用 SAML 断言更容易。</p>\n<p>关于使用情况，JWT 是在互联网规模上使用的。这突出了 JSON Web Token 在多个平台（尤其是移动平台）上进行客户端处理的容易程度。</p>\n<p><img src=\"https://cdn.auth0.com/content/jwt/comparing-jwt-vs-saml2.png\" alt=\"比较编码的 JWT 和编码的 SAML 的长度\"> <em>比较编码的 JWT 和编码的 SAML 的长度</em></p>\n<p>如果你想详细了解 JSON Web Token，甚至开始使用它们在自己的应用程序中执行身份验证，请浏览 Okta 旗下的 Auth0 的 <a href=\"http://auth0.com/learn/json-web-tokens\">JSON Web Token 登陆页面</a>。</p>\n","tags":["rfc","lib"]},{"id":"cpp-intro","url":"https://yieldray.fun/posts/cpp-intro","title":"c++入门","date_published":"2022-03-17T17:33:33.000Z","date_modified":"2022-03-17T17:33:33.000Z","content_text":"<p><a href=\"https://upload.lanzouj.com/iqQsJ01lgqid\">C++ Primer Plus（第 6 版）中文版 by Stephen Prata (z-lib.org).pdf</a><br><a href=\"https://upload.lanzouj.com/iS6hX01lgrpg\">C++ Primer Plus（第 6 版）中文版 by Stephen Prata (z-lib.org).epub</a></p>\n<pre><code class=\"language-cpp\">#include &lt;iostream&gt;\n\nusing namespace std;\nclass Box\n{\npublic:\n    double data;\n\nprivate:\n    int *ptr;\n\npublic:\n    friend void console_log(Box box); // 友元函数，可以访问私有成员，不是类的成员。友元函数实现无需friend关键字\n    friend Box operator*(int num, const Box &amp;box);\n    void setData(double d);\n    // 成员函数可以定义在类定义内部，或者单独使用范围解析运算符 :: 来定义。\n    // 在类定义中定义的成员函数把函数声明为内联的，即便没有使用 inline 标识符。\n    // 成员函数定义了就必须要实现，只能实现一次，如果类内部实现了就不能在外部实现了\n    inline double getData() const\n    { // 后面的 const 关键字表示这个函数不能修改类的数据成员\n        return data;\n    }\n    virtual string whatABoxItIs(); // 虚函数： virtual 修饰符修饰的方法可以由子类实现，否则子类实现的方法不会被绑定，仍绑定到基类方法\n    // virtual bool isBoxFull() = 0; // 纯虚函数：声明了此方法（不能定义）的类为抽象类，无法实例化\n    static void sayHello() // 静态函数\n    {\n        cout &lt;&lt; &quot;hello, I am a box&quot; &lt;&lt; endl;\n    }\n    Box operator+(const Box &amp;another)\n    { // 重载 + 运算符，用于把两个 Box 对象相加\n        Box box(0);\n        // this是一个指向当前对象的指针\n        box.data = this-&gt;data + another.data;\n        return box;\n    }\n    Box(double d);       // 构造函数\n    Box(const Box &amp;obj); // 拷贝构造函数\n    ~Box()               // 析构函数\n    {\n        delete ptr; // 删除指针，释放内存\n        cout &lt;&lt; &quot;a box was destroyed&quot; &lt;&lt; endl;\n    }\n};\n\nBox::Box(double d = 1.0) : data(d)\n{\n    // 此语法等同于 data = d;\n    // 多个参数用逗号分隔\n    ptr = new int(233);\n    cout &lt;&lt; &quot;a box was created&quot; &lt;&lt; endl;\n}\n\nBox::Box(const Box &amp;obj) // 拷贝构造函数\n{\n    // 隐含this\n    ptr = new int;\n    ptr = obj.ptr; // 拷贝值\n    cout &lt;&lt; &quot;a box was created by copy&quot; &lt;&lt; endl;\n}\n\nvoid Box::setData(double d)\n{\n    // 在外部定义函数\n    cout &lt;&lt; &quot;previous data is &quot; &lt;&lt; this-&gt;getData() &lt;&lt; endl; // this 是地址 Box*\n    data = d;\n    cout &lt;&lt; &quot;data was set to &quot; &lt;&lt; data &lt;&lt; endl;\n}\n\nstring Box::whatABoxItIs()\n{\n    // 返回string而不是char*\n    return &quot;Prototype Box&quot;;\n}\n\n// 友元函数\nvoid console_log(Box box)\n{\n    printf(&quot;friend: data in the box %.2lf\\n&quot;, box.data);\n    printf(&quot;friend: ptr in the box %.2lf\\n&quot;, box.ptr);\n}\n\nBox operator*(int num, const Box &amp;box)\n{ // Box的友元\n    Box newBox(num * box.data);\n    return newBox;\n}\nostream &amp;operator&lt;&lt;(ostream &amp;os, const Box &amp;box)\n{ // 重载&lt;&lt;运算符，这里没有声明为友元\n    os &lt;&lt; &quot;overload &lt;&lt; &quot; &lt;&lt; box.getData() &lt;&lt; endl;\n    return os;\n}\n\n// class SmallBox : public/private/protected[, access-specifier base-class [, ...]] Box {};\n/*\n公有继承（public）：当一个类派生自公有基类时，基类的公有成员也是派生类的公有成员，基类的保护成员也是派生类的保护成员，基类的私有成员不能直接被派生类访问，但是可以通过调用基类的公有和保护成员来访问。\n保护继承（protected）： 当一个类派生自保护基类时，基类的公有和保护成员将成为派生类的保护成员。\n私有继承（private）：当一个类派生自私有基类时，基类的公有和保护成员将成为派生类的私有成员。\n*/\n\nnamespace namespace_name\n{\n    int number = 123;\n    void hello_world()\n    {\n        cout &lt;&lt; &quot;Hello, world!&quot; &lt;&lt; endl;\n    }\n    namespace namespace_innner\n    {\n        float secret_number = 22.0 / 7;\n    }\n}\n\nvoid showNumber()\n{\n    namespace_name::hello_world();\n    cout &lt;&lt; namespace_name::number &lt;&lt; endl;\n    cout &lt;&lt; namespace_name::namespace_innner::secret_number &lt;&lt; endl;\n}\n\n// 泛型\ntemplate &lt;typename T, typename U&gt;\nT addAndBecomeTheFirstType(T const a, U const b)\n{\n    return (T)(a + b);\n}\n\nint number = 222;\n\nint _main()\n{\n    // Box box;   // 拷贝初始化，调用拷贝构造函数\n    Box box(23.4); // 直接初始化，调用构造函数\n    Box::sayHello();\n    box.setData(10.0);\n    cout &lt;&lt; box &lt;&lt; endl; // 10\n    // console_log(box);\n    cout &lt;&lt; &quot;data in the box &quot; &lt;&lt; box.getData() &lt;&lt; endl;          // 10\n    cout &lt;&lt; &quot;data in the newBox &quot; &lt;&lt; (3 * box).getData() &lt;&lt; endl; // 30\n    showNumber();\n    cout &lt;&lt; &quot;1 + 1.1 = &quot; &lt;&lt; addAndBecomeTheFirstType(1, 1.1) &lt;&lt; endl;\n    cout &lt;&lt; &quot;1.1 + 1 = &quot; &lt;&lt; addAndBecomeTheFirstType(1.1, 1) &lt;&lt; endl;\n    cout &lt;&lt; ::number &lt;&lt; endl; // 访问全局变量\n    return 0;\n}\n</code></pre>\n","tags":["cpp"]},{"id":"js-fp-tentative","url":"https://yieldray.fun/posts/js-fp-tentative","title":"函数式编程初探","date_published":"2022-03-11T22:12:14.000Z","date_modified":"2022-03-11T22:12:14.000Z","content_text":"<p>utils: Ramda, Lodash/fp<br>简单实现</p>\n<h1>柯里化 curry</h1>\n<p>curried(x, y, z) = curried(x, y)(z) = curried(x)(y, z) = curried(x)(y)(z)</p>\n<pre><code class=\"language-js\">function curry(func) {\n    return function curried(...args) {\n        if (args.length &gt;= func.length) {\n            return func.apply(this, args);\n        } else {\n            return function (...args2) {\n                return curried.apply(this, args.concat(args2));\n            };\n        }\n    };\n}\n\n// Functor f =&gt; (a → b) → f a → f b\nconst map = curry((f, ary) =&gt; ary.map(f));\n</code></pre>\n<h1>代码组合 compose/pipe</h1>\n<h2>compose(f, g, h) == f•g•h</h2>\n<p>compose(f, compose(g, h)) = compose(compose(f, g), h) = compose(f, g, h)<br><code>const compose = (...args) =&gt; params =&gt; args.reduceRight((pre, next) =&gt; next(pre), params);</code></p>\n<pre><code class=\"language-js\">R.compose(Math.abs, R.add(1), R.multiply(2))(-4); // |(-4) * 2 + 1| = 7\n</code></pre>\n<h2>pipe(f, g, h) == h•g•f</h2>\n<p><code>const pipe = (...args) =&gt; params =&gt; args.reduce((pre, next) =&gt; next(pre), params);</code></p>\n<pre><code class=\"language-js\">R.pipe(Math.pow, R.negate, R.inc)(3, 4); // -(3^4) + 1 = -80\n</code></pre>\n<h2>id</h2>\n<pre><code class=\"language-js\">const id = (x) =&gt; x;\n</code></pre>\n<h1>函子 Functor</h1>\n<p>functor 是实现了 map 函数并遵守一些特定规则的容器类型。</p>\n<pre><code class=\"language-js\">class Functor {\n    constructor(val) {\n        this.val = val;\n    }\n    // pointed functor 是实现了 of 方法的 functor\n    of(val) {\n        return new this.constructor(val);\n    }\n    map(f) {\n        return this.of(f(this.val));\n    }\n}\n</code></pre>\n<h1>Maybe</h1>\n<p>Maybe 函子的 map 方法会自行做空值检查，空值则不调用 f 函数</p>\n<pre><code class=\"language-js\">class Maybe extends Functor {\n    isNothing() {\n        return this.val === null || this.val === undefined;\n    }\n    map(f) {\n        return this.isNothing() ? this.of(null) : this.of(f(this.val));\n    }\n}\n\n//  maybe :: b -&gt; (a -&gt; b) -&gt; Maybe a -&gt; b\nconst maybe = curry((x, f, m) =&gt; (m.isNothing() ? x : f(m.val)));\n</code></pre>\n<h1>Either</h1>\n<p>Either 函子的 map 方法先右值再左值</p>\n<pre><code class=\"language-js\">class Either extends Functor {\n    constructor(left, right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    map(f) {\n        return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right);\n    }\n}\n\nEither.of = function (left, right) {\n    return new Either(left, right);\n};\n\nclass Left {\n    constructor(val) {\n        this.val = val;\n    }\n    map(f) {\n        return this;\n    }\n}\nclass Right {\n    constructor(val) {\n        this.val = val;\n    }\n    map(f) {\n        return new Right(f(this.val));\n    }\n}\n//  either :: (a -&gt; c) -&gt; (b -&gt; c) -&gt; Either a b -&gt; c\nconst either = curry((f, g, e) =&gt; {\n    switch (e.constructor) {\n        case Left:\n            return f(e.__value);\n        case Right:\n            return g(e.__value);\n    }\n});\n</code></pre>\n<h1>Monad</h1>\n<p>一个 functor，只要它定义个了一个 join 方法和一个 of 方法，并遵守一些定律，那么它就是一个 monad</p>\n<pre><code class=\"language-js\">class Monad extends Maybe {\n    join() {\n        return this.isNothing() ? this.of(null) : this.val;\n    }\n    map(f) {\n        return this.of(f(this.val)).join();\n    }\n    // of\n}\n</code></pre>\n<h1>IO</h1>\n<pre><code class=\"language-js\">class IO extends Monad {}\n</code></pre>\n<h1>Ap</h1>\n<pre><code class=\"language-js\">class Ap extends Functor {\n    ap(F) {\n        return this.of(this.val(F.val));\n    }\n}\n</code></pre>\n","tags":["js"]},{"id":"js-array-reduce","url":"https://yieldray.fun/posts/js-array-reduce","title":"Array.prototype.reduce","date_published":"2022-03-11T20:01:21.000Z","date_modified":"2022-03-11T20:01:21.000Z","content_text":"<h1>Typescript 定义</h1>\n<pre><code class=\"language-ts\">interface Array&lt;T&gt; {\n    // ... ...\n\n    /**\n     * 为数组中的所有元素（升序）调用给定回调函数。回调函数的返回值是累计的（accumulated）结果，这个累计值（callbackfn 回调函数的返回值）也会被入下一次回调作为该函数的第一个参数（previousValue）\n     * @param callbackfn 一个接受最多四个参数的函数。reduce 方法对数组中的每一个元素都调用一次 callbackfn 函数\n     * @param initialValue 如果指定了初始值（initialValue），那么这个初始值就会被用作累计值（accumulation）的初始值。对 callbackfn 函数的首次调用会提供这个值作为其第一个参数（previousValue）而不是数组第一个元素的值（如果没有提供初始值，则使用数组中的第一个元素作为初始值，并且 callbackfn 会跳过对数组的第一个元素的调用）\n     */\n    reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) =&gt; T, initialValue?: T): T;\n    /**\n     * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.\n     * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.\n     * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.\n     */\n    reduce&lt;U&gt;(\n        callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) =&gt; U,\n        initialValue: U,\n    ): U;\n\n    /**\n     * 为数组中的所有元素（降序）调用给定回调函数。回调函数的返回值是累计的（accumulated）结果，这个累计值（callbackfn 回调函数的返回值）也会被入下一次回调作为该函数的第一个参数（previousValue）\n     * @param callbackfn 一个接受最多四个参数的函数。reduce 方法对数组中的每一个元素都调用一次 callbackfn 函数\n     * @param initialValue 如果指定了初始值（initialValue），那么这个初始值就会被用作累计值（accumulation）的初始值。对 callbackfn 函数的首次调用会提供这个值作为其第一个参数（previousValue）而不是数组第一个元素的值（如果没有提供初始值，则使用数组中的第一个元素作为初始值，并且 callbackfn 会跳过对数组的第一个元素的调用）\n     */\n    reduceRight(\n        callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) =&gt; T,\n        initialValue?: T,\n    ): T;\n    /**\n     * Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.\n     * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls the callbackfn function one time for each element in the array.\n     * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.\n     */\n    reduceRight&lt;U&gt;(\n        callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) =&gt; U,\n        initialValue: U,\n    ): U;\n\n    [n: number]: T;\n}\n</code></pre>\n<p>提供初始值（initialValue）通常更安全，因为如果没有给定初始值，初始值就默认为数组的第一个元素。我们在回调函数中对这个首元素提供的 previousValue 进行不恰当操作可能导致报错<br>提供初始值回调函数就会对数组中的每一个元素进行调用，而且通常我们也想这么做</p>\n<h1>例子</h1>\n<h2>修改自 MDN</h2>\n<pre><code class=\"language-js\">[0, 1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) =&gt; {\n    console.log([currentIndex, currentValue, accumulator]); // 这里没有指定初始值，从下标为1的元素开始调用 =&gt; [1, 1, 0] [2, 2, 1] [3, 3, 3] [4, 4, 6]\n    return accumulator + currentValue;\n}); // =&gt; 10\n\n[0, 1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) =&gt; {\n    console.log([currentIndex, currentValue, accumulator]); // 指定初始值，对每个元素都进行调用 =&gt;  [0, 0, 10] [1, 1, 10] [2, 2, 11] [3, 3, 13] [4, 4, 16]\n    return accumulator + currentValue;\n}, 10); // =&gt; 20\n</code></pre>\n<h2>求和</h2>\n<pre><code class=\"language-js\">const sum = (...arr) =&gt; arr.reduce((acc, cur) =&gt; acc + cur, 0);\nsum(1, 2, 3, 4); // =&gt; 10\n</code></pre>\n<h2>二维数组转化为一维</h2>\n<pre><code class=\"language-js\">const flatArray = (arr) =&gt; arr.reduce((a, b) =&gt; a.concat(b), []);\nflatArray([\n    [0, 1],\n    [2, 3],\n    [4, 5],\n]); // =&gt; [0, 1, 2, 3, 4, 5]\n</code></pre>\n<h2>计算数组中每个元素出现的次数</h2>\n<pre><code class=\"language-js\">const countNames = (names) =&gt;\n    names.reduce((allNames, name) =&gt; {\n        name in allNames ? allNames[name]++ : (allNames[name] = 1);\n        return allNames;\n    }, {});\ncountNames([&quot;Alice&quot;, &quot;Bob&quot;, &quot;Tiff&quot;, &quot;Bruce&quot;, &quot;Alice&quot;]); // =&gt; {Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}\n</code></pre>\n<h2>按属性对 object 分类</h2>\n<pre><code class=\"language-js\">const groupBy = (objectArray, property) =&gt;\n    objectArray.reduce((acc, obj) =&gt; {\n        let key = obj[property];\n        if (!acc[key]) acc[key] = [];\n        acc[key].push(obj);\n        return acc;\n    }, {});\n\nlet people = [\n    { name: &quot;Alice&quot;, age: 21 },\n    { name: &quot;Max&quot;, age: 20 },\n    { name: &quot;Jane&quot;, age: 20 },\n];\ngroupBy(people, &quot;age&quot;);\n// {\n//   20: [\n//     { name: &#39;Max&#39;, age: 20 },\n//     { name: &#39;Jane&#39;, age: 20 }\n//   ],\n//   21: [{ name: &#39;Alice&#39;, age: 21 }]\n// }\n</code></pre>\n<h2>数组去重</h2>\n<pre><code class=\"language-js\">const deduplication1 = (arr) =&gt;\n    arr.reduce((acc, cur) =&gt; {\n        acc.indexOf(cur) === -1 &amp;&amp; acc.push(cur);\n        return acc;\n    }, []);\ndeduplication1([&quot;a&quot;, &quot;b&quot;, &quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;e&quot;, &quot;e&quot;, &quot;c&quot;, &quot;d&quot;, &quot;d&quot;, &quot;d&quot;, &quot;d&quot;]); // =&gt; [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;e&#39;, &#39;d&#39;]\n\nconst deduplication2 = (arr) =&gt;\n    arr.sort().reduce((init, current) =&gt; {\n        (init.length === 0 || init[init.length - 1] !== current) &amp;&amp; init.push(current);\n        return init;\n    }, []);\ndeduplication2([1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]); // =&gt; [1, 2, 3, 4, 5]\n</code></pre>\n<h2>更多</h2>\n<p><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce\">https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce</a></p>\n","tags":["js"]},{"id":"js-debounce-throttle","url":"https://yieldray.fun/posts/js-debounce-throttle","title":"js节流与防抖","date_published":"2022-03-03T22:41:29.000Z","date_modified":"2022-03-03T22:41:29.000Z","content_text":"<h1>概念</h1>\n<p>节流和防抖都是用于限制函数在<em>一小段时间内</em>执行次数的机制。</p>\n<p>以浏览器环境为例：DOM 事件的生成当然是无法控制的，而且可能频繁触发。如果一个事件处理函数高频触发，即使这个函数并不做太多工作，<br>并且如果是在主线程（UI 线程，而非 Workers 线程）执行任务，还是会有性能问题。</p>\n<p>为防止 UI 卡顿，很容易想到通过 <a href=\"https://developer.mozilla.org/docs/Web/API/Window/requestAnimationFrame\">requestAnimationFrame</a> 函数调度。<br>对于需要变更 DOM 的任务，这能够避免任务的不必要执行。缺点就是无法预测调度时间，比如如果是在后台标签页中就不会被调度。</p>\n<p>后端（Node.js）往往是服务于多个用户的，而我们这里提到的节流和防抖是针对单个用户的（即浏览器）。<br>因此，节流和防抖只需要利用定时器并且在常量空间内存中记录状态即可。</p>\n<p>下以 lodash 的接口为例，说明 debounce 和 throttle 函数。</p>\n<h2><a href=\"https://lodash.com/docs#debounce\">Debounce</a></h2>\n<p>从逻辑的角度来看，debounce 将多个连续的（两两之间不超过指定时间间隔）任务分组。<br>对于每个任务组，仅<em>实际</em>执行其中的一个任务，并可指定执行的是第一个还是最后一个。</p>\n<p>我们暂且忽略 lodash 的使用细节。从实现者的角度来看，我们实际上只需检测连续的两个任务之间的时间间隔，就可完成分组。<br>即，我们等待指定的时间间隔，若在时间间隔之内被调用，则属于已有的任务组；否则，属于新增的任务组，并重新循环此步骤。</p>\n<pre><code class=\"language-js\">// 避免在窗口大小变化时进行高耗时的计算\n$(window).on(&quot;resize&quot;, _.debounce(calculateLayout, 150));\n\n// 在点击时调用 `sendMail`，防抖后续的调用\n$(element).on(\n    &quot;click&quot;,\n    _.debounce(sendMail, 300, {\n        leading: true,\n        trailing: false,\n    }),\n);\n\n// 确保 `batchLog` 在防抖调用1秒后被调用一次\nconst debounced = _.debounce(batchLog, 250, { maxWait: 1000 });\nconst source = new EventSource(&quot;/stream&quot;);\n$(source).on(&quot;message&quot;, debounced);\n\n// 取消尾部的防抖调用\n$(window).on(&quot;popstate&quot;, debounced.cancel);\n</code></pre>\n<p>lodash 的 debounce 函数功能丰富，因此实现需要更多<a href=\"https://github.com/lodash/lodash/blob/main/src/debounce.ts\">代码</a></p>\n<pre><code class=\"language-ts\">/**\n * 防抖函数，用于限制某个函数的执行频率。\n * @param {Function} func 需要防抖处理的函数。\n * @param {number} [wait=0] 延迟的毫秒数；如果未指定，则使用 `requestAnimationFrame`（如果可用）。\n * @param {Object} [options={}] 配置选项对象。\n * @param {boolean} [options.leading=false] 指定是否在延迟开始前调用函数。\n * @param {number} [options.maxWait] 函数允许延迟的最大时间。\n * @param {boolean} [options.trailing=true] 指定是否在延迟结束后调用函数。\n * @returns {Function} 返回新的防抖函数。\n */\nfunction debounce(func, wait, options) {\n    let lastArgs; // 上一次调用的参数\n    let lastThis; // 上一次调用的this\n    let maxWait; // 最大等待时间\n    let result; // 函数执行结果\n    let timerId; // 定时器ID\n    let lastCallTime; // 上一次调用时间\n    let lastInvokeTime = 0; // 上一次执行函数时间\n    let leading = false; // 是否在延迟开始前调用\n    let maxing = false; // 是否启用最大等待时间\n    let trailing = true; // 是否在延迟结束后调用\n\n    // 如果 `wait` 为0且 `requestAnimationFrame` 可用，则使用 `requestAnimationFrame` 。\n    const useRAF = !wait &amp;&amp; wait !== 0 &amp;&amp; typeof requestAnimationFrame === &quot;function&quot;;\n\n    if (typeof func !== &quot;function&quot;) throw new TypeError(&quot;Expected a function&quot;);\n\n    wait = +wait || 0;\n    if (isObject(options)) {\n        leading = !!options.leading;\n        maxing = &quot;maxWait&quot; in options;\n        maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;\n        trailing = &quot;trailing&quot; in options ? !!options.trailing : trailing;\n    }\n\n    // 执行函数并记录执行时间\n    function invokeFunc(time) {\n        const args = lastArgs;\n        const thisArg = lastThis;\n\n        lastArgs = lastThis = undefined;\n        lastInvokeTime = time;\n        result = func.apply(thisArg, args);\n        return result;\n    }\n\n    // 启动定时器\n    function startTimer(pendingFunc, milliseconds) {\n        if (useRAF) {\n            cancelAnimationFrame(timerId);\n            return requestAnimationFrame(pendingFunc);\n        }\n\n        return setTimeout(pendingFunc, milliseconds);\n    }\n\n    // 取消定时器\n    function cancelTimer(id) {\n        if (useRAF) {\n            cancelAnimationFrame(id);\n            return;\n        }\n        clearTimeout(id);\n    }\n\n    // 处理前沿调用\n    function leadingEdge(time) {\n        lastInvokeTime = time;\n        timerId = startTimer(timerExpired, wait);\n        return leading ? invokeFunc(time) : result;\n    }\n\n    // 计算剩余等待时间\n    function remainingWait(time) {\n        const timeSinceLastCall = time - lastCallTime;\n        const timeSinceLastInvoke = time - lastInvokeTime;\n        const timeWaiting = wait - timeSinceLastCall;\n\n        return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;\n    }\n\n    // 判断是否应该调用函数\n    function shouldInvoke(time) {\n        const timeSinceLastCall = time - lastCallTime;\n        const timeSinceLastInvoke = time - lastInvokeTime;\n\n        return (\n            lastCallTime === undefined ||\n            timeSinceLastCall &gt;= wait ||\n            timeSinceLastCall &lt; 0 ||\n            (maxing &amp;&amp; timeSinceLastInvoke &gt;= maxWait)\n        );\n    }\n\n    // 定时器到期后调用的函数\n    function timerExpired() {\n        const time = Date.now();\n        if (shouldInvoke(time)) {\n            return trailingEdge(time);\n        }\n        timerId = startTimer(timerExpired, remainingWait(time));\n        return undefined;\n    }\n\n    // 处理后沿调用\n    function trailingEdge(time) {\n        timerId = undefined;\n\n        if (trailing &amp;&amp; lastArgs) {\n            return invokeFunc(time);\n        }\n        lastArgs = lastThis = undefined;\n        return result;\n    }\n\n    // 取消防抖\n    function cancel() {\n        if (timerId !== undefined) {\n            cancelTimer(timerId);\n        }\n        lastInvokeTime = 0;\n        lastArgs = lastCallTime = lastThis = timerId = undefined;\n    }\n\n    // 立即执行防抖函数\n    function flush() {\n        return timerId === undefined ? result : trailingEdge(Date.now());\n    }\n\n    // 检查防抖函数是否在等待调用\n    function pending() {\n        return timerId !== undefined;\n    }\n\n    // 防抖函数的主体\n    function debounced(...args) {\n        const time = Date.now();\n        const isInvoking = shouldInvoke(time);\n\n        lastArgs = args;\n        lastThis = this;\n        lastCallTime = time;\n\n        if (isInvoking) {\n            if (timerId === undefined) {\n                return leadingEdge(lastCallTime);\n            }\n            if (maxing) {\n                timerId = startTimer(timerExpired, wait);\n                return invokeFunc(lastCallTime);\n            }\n        }\n        if (timerId === undefined) {\n            timerId = startTimer(timerExpired, wait);\n        }\n        return result;\n    }\n\n    // 添加取消、防抖、和挂起的方法\n    debounced.cancel = cancel;\n    debounced.flush = flush;\n    debounced.pending = pending;\n\n    return debounced;\n}\n</code></pre>\n<h2><a href=\"https://lodash.com/docs/throttle\">Throttle</a></h2>\n<p>从逻辑的角度来看，一旦发生连续的任务，throttle 就将这段时间按时间间隔划分，每个时间间隔仅<em>真正</em>执行一个任务。</p>\n<pre><code class=\"language-js\">// 避免在滚动时过于频繁地更新位置\n$(window).on(&quot;scroll&quot;, _.throttle(updatePosition, 100));\n\n$(document).on(&quot;scroll&quot;, _.throttle(infiniteScrolling, 300));\n\nfunction infiniteScrolling() {\n    const pixelsFromWindowBottomToPageBottom = $(document).height() - $(window).scrollTop() - $(window).height();\n    if (pixelsFromWindowBottomToPageBottom &lt; 200) {\n        $(&quot;body&quot;).append(&quot;&lt;p&gt;maybe new ajax content&lt;/p&gt;&quot;);\n    }\n}\n\n// 在点击事件触发时调用 renewToken，但每五分钟最多调用一次\nconst throttled = _.throttle(renewToken, 300000, { trailing: false });\n$(element).on(&quot;click&quot;, throttled);\n\n// 取消尾部的节流调用\n$(window).on(&quot;popstate&quot;, throttled.cancel);\n</code></pre>\n<p>lodash 的 debounce 函数允许提供 maxWait 参数，这样 throttle 的实现就很简单了。<br>throttle 只不过是 wait 与 maxWait 相同的特化版 debounce</p>\n<pre><code class=\"language-ts\">/**\n * 节流函数，用于限制某个函数的执行频率。\n * @param {Function} func 需要节流处理的函数。\n * @param {number} [wait=0] 每次调用之间的最小间隔时间（毫秒）；如果未指定，则使用 `requestAnimationFrame`（如果可用）。\n * @param {Object} [options={}] 配置选项对象。\n * @param {boolean} [options.leading=true] 指定是否在延迟开始前调用函数。\n * @param {boolean} [options.trailing=true] 指定是否在延迟结束后调用函数。\n * @returns {Function} 返回新的节流函数。\n */\nfunction throttle(func, wait, options) {\n    let leading = true;\n    let trailing = true;\n\n    if (typeof func !== &quot;function&quot;) throw new TypeError(&quot;Expected a function&quot;);\n\n    if (isObject(options)) {\n        leading = &quot;leading&quot; in options ? !!options.leading : leading;\n        trailing = &quot;trailing&quot; in options ? !!options.trailing : trailing;\n    }\n\n    // 使用 debounce 实现节流功能\n    return debounce(func, wait, {\n        leading,\n        trailing,\n        maxWait: wait, // 将 maxWait 设置为 wait，实现节流效果\n    });\n}\n</code></pre>\n<h2>其它</h2>\n<p>参考实现：</p>\n<p><a href=\"https://github.com/denoland/std/blob/main/async/debounce.ts\">https://github.com/denoland/std/blob/main/async/debounce.ts</a></p>\n<p>推荐阅读：</p>\n<p><a href=\"https://redd.one/blog/debounce-vs-throttle\">Debounce vs Throttle: Definitive Visual Guide</a><br><a href=\"https://css-tricks.com/debouncing-throttling-explained-examples/\">Debouncing and Throttling Explained Through Examples</a></p>\n","tags":["js"]},{"id":"nodejs-stream","url":"https://yieldray.fun/posts/nodejs-stream","title":"nodejs:从events到stream","date_published":"2022-02-22T18:23:23.000Z","date_modified":"2022-02-22T18:23:23.000Z","content_text":"<p>events 文档：<a href=\"http://nodejs.cn/api/events.html\">http://nodejs.cn/api/events.html</a><br>stream 文档：<a href=\"http://nodejs.cn/api/stream.html\">http://nodejs.cn/api/stream.html</a></p>\n<h1><a href=\"https://docs.deno.com/api/node/events/\">events</a></h1>\n<p>注册的监听器会按照注册顺序<strong>同步</strong>调用，监听器返回的任何值都将被忽略和丢弃。</p>\n<pre><code class=\"language-js\">const EventEmitter = require(&quot;events&quot;);\n\nclass MyEmitter extends EventEmitter {}\n\nconst myEmitter = new MyEmitter();\nmyEmitter.on(&quot;eventName&quot;, (a, b) =&gt; {\n    console.log(&quot;an event occurred!&quot;);\n});\nmyEmitter.once(&quot;disposable&quot;, () =&gt; console.log(&quot;只会触发一次&quot;));\nmyEmitter.on(&quot;eventName&quot;, function (a, b) {\n    // this 被绑定到当前 EventEmitter 实例\n    console.assert(this === myEmitter);\n});\nmyEmitter.addListener(eventName, listener); // 浏览器类似的API也是提供的，作为on的别名\nmyEmitter.emit(&quot;eventName&quot;, &quot;arg1&quot;, [&quot;arg2&quot;]); // 多参可选\n</code></pre>\n<h2>错误事件</h2>\n<p>当触发 error 事件时，若没有为 error 事件注册监听器，则该错误将同步抛出，因此将导致当前 Node.js 进程退出。</p>\n<pre><code class=\"language-js\">myEmitter.emit(&quot;error&quot;, new Error(&quot;whoops!&quot;));\n\n// 这样不会退出进程了\nmyEmitter.on(&quot;error&quot;, (err) =&gt; {\n    console.error(&quot;whoops! there was an error&quot;);\n});\n\n// errorMonitor 为 Symbol 键事件，该事件也会在错误时出发，但错误本身不会被消费\nconst { errorMonitor } = require(&quot;node:events&quot;);\nmyEmitter.on(errorMonitor, (err) =&gt; {\n    captureButNotConsume(err);\n});\n</code></pre>\n<p>默认情况下，异步函数错误不会被捕获，毕竟监听器是同步调用的。因此异步函数错误将触发未处理的拒绝，若全局未监听此拒绝，同样导致进程退出。</p>\n<pre><code class=\"language-js\">const { EventEmitter } = require(&quot;events&quot;);\n\nconst emitter = new EventEmitter();\n\nemitter.on(&quot;event&quot;, async () =&gt; {\n    return Promise.reject(&quot;REJECTED&quot;);\n});\n\nemitter.on(&quot;error&quot;, console.error);\nemitter.emit(&quot;event&quot;);\n\n// 这样不会退出进程了\nprocess.on(&quot;unhandledRejection&quot;, (reason, promise) =&gt; {\n    console.error(&quot;Unhandled Rejection at:&quot;, promise, &quot;reason:&quot;, reason);\n});\n</code></pre>\n<p>将 captureRejections 配置为 true，则将自动为返回的 Promise 注册 catch 来捕获拒绝。既然拒绝已被处理，自然就不会抛出未处理的拒绝事件。<br>此时，拒绝被将路由到 <code>Symbol.for(&quot;nodejs.rejection&quot;)</code> 方法（如有）或 error 事件。</p>\n<pre><code class=\"language-js\">const { EventEmitter, captureRejectionSymbol } = require(&quot;events&quot;);\nconst emitter = new EventEmitter({ captureRejections: true });\n\nemitter[captureRejectionSymbol] = (err) =&gt; console.log(captureRejectionSymbol, err);\nemitter.on(&quot;error&quot;, (err) =&gt; console.log(&quot;error&quot;, err));\n\nemitter.on(&quot;event&quot;, async () =&gt; Promise.reject(&quot;REJECTED&quot;));\nemitter.emit(&quot;event&quot;);\n</code></pre>\n<p>error 事件监听器若为异步函数，则可能导致错误无限递归。因此建议 error 事件监听器同步。<br>以下方法在设置所有新 EventEmitter 实例默认捕获拒绝：</p>\n<pre><code class=\"language-js\">const events = require(&quot;node:events&quot;);\nevents.captureRejections = true;\n</code></pre>\n<h2>边缘情况</h2>\n<p>注意：<code>process.nextTick</code> 微任务运行在 v8 微任务之前。</p>\n<pre><code class=\"language-js\">const { EventEmitter, once } = require(&quot;node:events&quot;);\n\nconst myEE = new EventEmitter();\n\nasync function foo() {\n    await once(myEE, &quot;bar&quot;);\n    console.log(&quot;bar&quot;);\n    // 控制流到达此处，foo 事件已经同步触发了\n\n    // 因此下面的 Promise 永远不会 resolve\n    await once(myEE, &quot;foo&quot;);\n    console.log(&quot;foo&quot;);\n}\n\nprocess.nextTick(() =&gt; {\n    myEE.emit(&quot;bar&quot;);\n    myEE.emit(&quot;foo&quot;);\n});\n\nfoo().then(() =&gt; console.log(&quot;done&quot;));\n</code></pre>\n<h1>兼容 Web：EventTarget/Event/CustomEvent</h1>\n<p>兼容 WHATWG 规范，<a href=\"https://nodejs.cn/api/events.html#eventtarget-error-handling\">错误处理</a> 与 EventEmitter 机制不同。</p>\n<p>EventTarget 不会特殊处理 error 事件。</p>\n<pre><code class=\"language-js\">process.nextTick(() =&gt; console.log(&quot;nextTick1&quot;));\n\nconst target = new EventTarget();\ntarget.addEventListener(&quot;error&quot;, () =&gt; {\n    throw new Error(&quot;ERROR&quot;);\n    return Promise.reject(&quot;REJECT&quot;);\n});\ntarget.dispatchEvent(new Event(&quot;error&quot;));\n\nprocess.nextTick(() =&gt; console.log(&quot;nextTick2&quot;)); // 不会触发\n</code></pre>\n<p>无论是抛出错误还是返回拒绝的 Promise，该错误都将被 process.nextTick 抛出。</p>\n<pre><code class=\"language-sh\">$ node main.ts\n\nnextTick1\nnode:internal/event_target:1101\n  process.nextTick(() =&gt; { throw err; });\n</code></pre>\n<h2>abort 事件</h2>\n<p>由于在 abort 事件上可调用 e.stopImmediatePropagation() 阻止冒泡，因此原本注册的 abort 事件可能不被触发，导致可能的资源泄漏。<br>node.js 提供 addAbortListener 函数来注册 abort 监听器，该监听器不会受阻止传播。</p>\n<pre><code class=\"language-js\">const { addAbortListener } = require(&quot;node:events&quot;);\n\nfunction example(signal) {\n    let disposable;\n    try {\n        signal.addEventListener(&quot;abort&quot;, (e) =&gt; e.stopImmediatePropagation());\n        disposable = addAbortListener(signal, (e) =&gt; {\n            // Do something when signal is aborted.\n        });\n    } finally {\n        disposable?.[Symbol.dispose]();\n    }\n}\n</code></pre>\n<h1><a href=\"https://docs.deno.com/api/node/stream/\">stream</a></h1>\n<p>所有 nodejs stream 都继承自 EventEmitter，流又分为：Readable Writeable Duplex Transform</p>\n<p>自动化处理流：<code>readableSrc.pipe(writeableDest)</code><br>其中，Readable/Duplex 属于 readableSrc，Writeable/Duplex/Transform 属于 writeableDest</p>\n<h2>构造流</h2>\n<h3>继承方式构造</h3>\n<pre><code class=\"language-ts\">const { Readable, Writable, Transform } = require(&quot;stream&quot;);\n\nclass MyReadable extends Readable {\n    private current = 0;\n\n    _read() {\n        if (this.current &lt; 3) {\n            this.push(`data-${this.current++}`);\n        } else {\n            this.push(null); // 结束流\n        }\n    }\n}\n\nclass MyWritable extends Writable {\n    _write(chunk: any, encoding: string, callback: (error?: Error | null) =&gt; void) {\n        console.log(chunk.toString());\n        callback();\n    }\n}\n\nclass MyTransform extends Transform {\n    _transform(chunk: any, encoding: string, callback: (error?: Error | null, data?: any) =&gt; void) {\n        this.push(chunk.toString().toUpperCase());\n        callback();\n    }\n}\n</code></pre>\n<h3>简化构造函数</h3>\n<pre><code class=\"language-ts\">// 直接传入方法选项\nconst readable = new Readable({\n    read() {\n        this.push(&quot;some data&quot;);\n        this.push(null);\n    },\n});\n\nconst writable = new Writable({\n    write(chunk: any, encoding: string, callback: (error?: Error | null) =&gt; void) {\n        console.log(chunk.toString());\n        callback();\n    },\n});\n\nconst transform = new Transform({\n    transform(chunk: any, encoding: string, callback: (error?: Error | null, data?: any) =&gt; void) {\n        callback(null, chunk.toString().toUpperCase());\n    },\n});\n</code></pre>\n<h2>消费流的 API 风格</h2>\n<p>Node.js 提供多种消费流数据的方式，<strong>选择一种并坚持使用</strong>，混用会导致意外行为。</p>\n<h3>流动模式 (Flowing Mode)</h3>\n<pre><code class=\"language-ts\">// 添加 &#39;data&#39; 监听器自动切换到流动模式\nreadable.on(&quot;data&quot;, (chunk) =&gt; {\n    console.log(&quot;收到数据:&quot;, chunk);\n});\n\nreadable.on(&quot;end&quot;, () =&gt; {\n    console.log(&quot;流结束&quot;);\n});\n</code></pre>\n<h3>暂停模式 (Paused Mode)</h3>\n<pre><code class=\"language-ts\">// &#39;readable&#39; 事件驱动，手动读取\nreadable.on(&quot;readable&quot;, () =&gt; {\n    let chunk;\n    while (null !== (chunk = readable.read())) {\n        console.log(&quot;读取数据:&quot;, chunk);\n    }\n});\n\nreadable.on(&quot;end&quot;, () =&gt; {\n    console.log(&quot;流结束&quot;);\n});\n</code></pre>\n<h3>管道方式</h3>\n<pre><code class=\"language-ts\">// 最简单的数据传输方式\nreadable.pipe(writable);\n\n// 链式管道\nreadable.pipe(transform).pipe(writable);\n\n// 使用 pipeline 处理错误\nimport { pipeline } from &quot;stream/promises&quot;;\n\nasync function pipelineExample() {\n    try {\n        await pipeline(readable, transform, writable);\n        console.log(&quot;管道完成&quot;);\n    } catch (error) {\n        console.error(&quot;管道错误:&quot;, error);\n    }\n}\n</code></pre>\n<h3>异步迭代器</h3>\n<pre><code class=\"language-ts\">async function consume(readable: NodeJS.ReadableStream) {\n    try {\n        for await (const chunk of readable) {\n            console.log(&quot;处理数据:&quot;, chunk);\n        }\n    } catch (error) {\n        console.error(&quot;流错误:&quot;, error);\n    }\n}\n</code></pre>\n<h3>⚠️ 混用陷阱</h3>\n<pre><code class=\"language-javascript\">// ❌ 错误：混用不同 API 风格\nreadable.on(&quot;data&quot;, handleData);\nreadable.on(&quot;readable&quot;, handleReadable); // 会相互干扰\n\n// ✅ 正确：选择一种风格\nreadable.on(&quot;data&quot;, handleData);\nreadable.on(&quot;end&quot;, handleEnd);\n</code></pre>\n<h2>流的重要概念</h2>\n<h3>流状态管理</h3>\n<pre><code class=\"language-ts\">const { PassThrough } = require(&quot;stream&quot;);\n\n// readable.readableFlowing 的三种状态：\n// null: 没有提供消费机制，流不会产生数据\n// false: 暂时停止流动，但仍在生成数据（缓冲）\n// true: 主动发出事件，数据流动\n\nconst readable = new PassThrough();\nconsole.log(readable.readableFlowing); // null\n\nreadable.on(&quot;data&quot;, console.log);\nconsole.log(readable.readableFlowing); // true\n\nreadable.pause();\nconsole.log(readable.readableFlowing); // false\n</code></pre>\n<h3>highWaterMark 与背压控制</h3>\n<pre><code class=\"language-ts\">const writable = new Writable({\n    highWaterMark: 16, // 控制缓冲区大小\n    write(chunk: any, encoding: string, callback: (error?: Error | null) =&gt; void) {\n        callback();\n    },\n});\n\n// 检查写入状态\nconst success = writable.write(data);\nif (!success) {\n    // 缓冲区满，等待 drain 事件\n    writable.once(&quot;drain&quot;, () =&gt; {\n        // 可以继续写入\n    });\n}\n</code></pre>\n<h3>对象模式陷阱</h3>\n<pre><code class=\"language-ts\">interface CustomObject {\n    id: number;\n    value: string;\n}\n\nconst transform = new Transform({\n    objectMode: true,\n    transform(obj: CustomObject | null, encoding: string, callback: (error?: Error | null, data?: any) =&gt; void) {\n        // ⚠️ null 有特殊含义（结束流）\n        if (obj === null) {\n            return callback();\n        }\n        callback(null, obj);\n    },\n});\n</code></pre>\n<h2>流事件</h2>\n<h3>错误处理</h3>\n<p>和 EventEmitter 一样，未处理的流错误会导致程序崩溃：</p>\n<pre><code class=\"language-javascript\">// ❌ 危险：未处理错误\nconst stream = fs.createReadStream(&quot;non-existent-file.txt&quot;);\nstream.on(&quot;data&quot;, console.log); // 文件不存在时崩溃\n\n// ✅ 正确：始终处理 error 事件\nstream.on(&quot;error&quot;, (err) =&gt; {\n    console.error(&quot;流错误:&quot;, err.message);\n});\n</code></pre>\n<h3>监听器泄漏</h3>\n<pre><code class=\"language-javascript\">// ⚠️ 危险：重复添加监听器\nsetInterval(() =&gt; {\n    stream.on(&quot;data&quot;, handler); // 不断累积监听器\n}, 100);\n\n// ✅ 正确：使用 once 或及时清理\nstream.once(&quot;data&quot;, handler);\n// 或\nstream.removeListener(&quot;data&quot;, handler);\n</code></pre>\n<h3>drain 事件</h3>\n<p>忽略 <code>write()</code> 返回值会导致内存耗尽：</p>\n<pre><code class=\"language-javascript\">// ❌ 错误：忽略返回值\nfor (let i = 0; i &lt; 1000000; i++) {\n    writable.write(`Line ${i}\\n`); // 可能内存耗尽\n}\n\n// ✅ 正确：检查返回值\nfor (let i = 0; i &lt; 1000000; i++) {\n    const success = writable.write(`Line ${i}\\n`);\n    if (!success) {\n        await new Promise((resolve) =&gt; writable.once(&quot;drain&quot;, resolve));\n    }\n}\n</code></pre>\n<h3>pipe 错误传播</h3>\n<p><code>pipe</code> 不会自动传播错误：</p>\n<pre><code class=\"language-javascript\">// ❌ 错误：pipe 不传播错误\nsrc.pipe(dest);\nsrc.on(&quot;error&quot;, console.error); // dest 不会自动关闭\n\n// ✅ 正确：使用 pipeline\npipeline(src, dest, (err) =&gt; {\n    if (err) console.error(err);\n    // 自动清理所有流\n});\n</code></pre>\n<h3>资源清理</h3>\n<pre><code class=\"language-javascript\">class ResourceStream extends Readable {\n    _destroy(err, callback) {\n        // ✅ 重要：清理资源\n        if (this.resource) {\n            this.resource.close();\n        }\n        callback(err);\n    }\n}\n\n// 使用 AbortController\nconst controller = new AbortController();\nconst stream = new Readable({ signal: controller.signal });\ncontroller.abort(); // 自动销毁\n</code></pre>\n<h3>手动管道实现</h3>\n<pre><code class=\"language-js\">readableSrc.on(&quot;data&quot;, (chunk) =&gt; {\n    writeableDest.write(chunk);\n});\n\nreadableSrc.on(&quot;end&quot;, (chunk) =&gt; {\n    writeableDest.end();\n});\n</code></pre>\n<h1>流实用函数</h1>\n<p>stream/consumers 模块</p>\n<pre><code class=\"language-ts\">function arrayBuffer(stream: WebReadableStream | ReadableStream | AsyncIterable&lt;any&gt;): Promise&lt;ArrayBuffer&gt;;\nfunction blob(stream: WebReadableStream | ReadableStream | AsyncIterable&lt;any&gt;): Promise&lt;NodeBlob&gt;;\nfunction buffer(stream: WebReadableStream | ReadableStream | AsyncIterable&lt;any&gt;): Promise&lt;Buffer&gt;;\nfunction json(stream: WebReadableStream | ReadableStream | AsyncIterable&lt;any&gt;): Promise&lt;unknown&gt;;\nfunction text(stream: WebReadableStream | ReadableStream | AsyncIterable&lt;any&gt;): Promise&lt;string&gt;;\n</code></pre>\n<p>stream/promises 模块</p>\n<pre><code class=\"language-ts\">function finished(stream: ReadableStream | WritableStream | ReadWriteStream, options?: FinishedOptions): Promise&lt;void&gt;;\n\nfunction pipeline&lt;A extends PipelineSource&lt;any&gt;, B extends PipelineDestination&lt;A, any&gt;&gt;(\n    source: A,\n    destination: B,\n    options?: PipelineOptions,\n): PipelinePromise&lt;B&gt;;\n</code></pre>\n<h1>兼容 Web：Stream API</h1>\n<p><a href=\"https://nodejs.org/api/webstreams.html\">stream/web</a> 模块</p>\n<h1>另见</h1>\n<p><a href=\"https://blog.platformatic.dev/a-guide-to-reading-and-writing-nodejs-streams\">https://blog.platformatic.dev/a-guide-to-reading-and-writing-nodejs-streams</a><br><a href=\"https://pavel-romanov.com/writable-streams-in-nodejs-a-practical-guide\">https://pavel-romanov.com/writable-streams-in-nodejs-a-practical-guide</a><br><a href=\"https://www.sitepoint.com/node-js-streams-with-typescript/\">https://www.sitepoint.com/node-js-streams-with-typescript/</a></p>\n","tags":["node.js"]},{"id":"build-a-pwa-app","url":"https://yieldray.fun/posts/build-a-pwa-app","title":"构建PWA应用","date_published":"2022-02-18T16:35:14.000Z","date_modified":"2022-02-18T16:35:14.000Z","content_text":"<p>参见<br><a href=\"https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers\">https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers</a><br><a href=\"https://developer.mozilla.org/docs/Web/Progressive_web_apps\">https://developer.mozilla.org/docs/Web/Progressive_web_apps</a><br><a href=\"https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g\">https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g</a></p>\n<p>注意：下面的代码是没有做错误处理的</p>\n<h1>Web Worker</h1>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/WorkerGlobalScope\">https://developer.mozilla.org/docs/Web/API/WorkerGlobalScope</a></p>\n<p>web worker 是由 JavaScript 创建的新线程<br>每一个 web worker 都是一个独立 JavaScript 脚本，具有独立的运行环境（不同于 DOM 环境）</p>\n<h2>main.js</h2>\n<pre><code class=\"language-js\">// 创建新的web worker\nconst myWorker = new Worker(&quot;worker.js&quot;, { name: &quot;myWorker&quot; }); // 第二个参数可选\n\n// 向worker发送数据\nworker.postMessage(&quot;Hello World&quot;);\nworker.postMessage({ method: &quot;echo&quot;, args: [&quot;Work&quot;] });\n// 参见 https://developer.mozilla.org/docs/Glossary/Transferable_objects\nworker.postMessage(arrayBuffer, [arrayBuffer]);\n\n// 监听数据，亦可使用 addEventListener 方法\nworker.onmessage = function (event) {\n    console.log(&quot;Received message &quot; + event.data);\n    doSomething();\n};\nfunction doSomething() {\n    // 执行任务\n    worker.postMessage(&quot;Work done!&quot;);\n}\n\n// 处理错误\nworker.addEventListener(&quot;error&quot;, function (event) {\n    // ...\n});\n\n// 关闭 worker\nworker.terminate();\n</code></pre>\n<h2>worker.js</h2>\n<pre><code class=\"language-js\">console.log(self.name); // =&gt; myWorker\n\n// 引入其他脚本，多参可选\nimportScripts(&quot;script1.js&quot;, &quot;script2.js&quot;);\n\nself.addEventListener(\n    // self === this\n    &quot;message&quot;,\n    function (e) {\n        self.postMessage(&quot;You said: &quot; + e.data);\n    },\n    false,\n);\n\n// 关闭 worker\nself.close();\n</code></pre>\n<h1>构建 PWA，从缓存请求开始</h1>\n<p>Service Worker 是 pwa 的重要实现方法，Service Worker 运行在 worker 线程中，必须运行在 https 协议下（或 localhost）\nService Worker 能够代理网页与服务器之间的请求\nService Worker 的 API 都是异步的，通过 Promise 实现，所以为了减少嵌套，请使用 async/await 实现<br>相关接口 <a href=\"https://developer.mozilla.org/docs/Web/API/Service_Worker_API\">https://developer.mozilla.org/docs/Web/API/Service_Worker_API</a></p>\n<h2>index.html</h2>\n<pre><code class=\"language-html\">&lt;link rel=&quot;manifest&quot; href=&quot;manifest.webmanifest&quot; /&gt;\n&lt;script src=&quot;app.js&quot; defer&gt;&lt;/script&gt;\n&lt;h1&gt;Hello, PWA!&lt;/h1&gt;\n</code></pre>\n<h2>app.js</h2>\n<pre><code class=\"language-js\">if (&quot;serviceWorker&quot; in navigator) {\n    navigator.serviceWorker.register(&quot;./sw.js&quot;); // =&gt; Promise&lt;ServiceWorkerRegistration&gt;\n    // 此处省略了 options 参数，详见 https://developer.mozilla.org/docs/Web/API/ServiceWorkerContainer/register\n    // 与 Push API 有关的内容则参见 https://developer.mozilla.org/docs/Web/API/Push_API\n}\n\nconst button = document.getElementById(&quot;notifications&quot;);\nbutton.addEventListener(&quot;click&quot;, function (e) {\n    Notification.requestPermission().then(function (result) {\n        if (result === &quot;granted&quot;) {\n            // testNotification();\n        }\n    });\n});\n\nfunction testNotification() {\n    // Notifications API 参见 https://developer.mozilla.org/docs/Web/API/notification\n    // 使用参见 https://developer.mozilla.org/docs/Web/API/Notifications_API/Using_the_Notifications_API\n    const options = {\n        body: &quot;notifBody&quot;,\n        icon: &quot;./pwa.png&quot;,\n    };\n    const notif = new Notification(&quot;notifTitle&quot;, options); // options 可选\n    notif.onerror = () =&gt; {\n        // doSomething();\n    };\n}\n</code></pre>\n<h2>sw.js</h2>\n<pre><code class=\"language-js\">const cacheName = &quot;PWA-v1&quot;; // sw 版本\nconst contentToCache = [&quot;/index.html&quot;, &quot;/css/styles.css?v1&quot;]; // 缓存清单\n\n// 安装 Service Worker，添加缓存清单\nself.addEventListener(&quot;install&quot;, (e) =&gt; {\n    // caches 在 sw 作用域下，是一个特殊的 CacheStorage\n    // CacheStorage API 参见 https://developer.mozilla.org/docs/Web/API/CacheStorage\n    const preCache = async () =&gt; {\n        const cache = await caches.open(cacheName); // =&gt; Cache\n        // Cache API 参见 https://developer.mozilla.org/docs/Web/API/Cache\n        return cache.addAll(contentToCache);\n    };\n    e.waitUntil(preCache());\n    // 参见 https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil\n});\n\n// 接管请求\nself.addEventListener(&quot;fetch&quot;, async (e) =&gt; {\n    const cacheOrRequestThenCache = async () =&gt; {\n        const req = await caches.match(e.request);\n        if (req) {\n            return req;\n        } else {\n            const resp = await fetch(e.request); // =&gt; Response\n            // Response API　参见 https://developer.mozilla.org/docs/Web/API/Response\n            // 通过 Steam API 操纵 Response 可以方便地处理数据，参见 https://developer.mozilla.org/docs/Web/API/Streams_API\n            const cache = await caches.open(cacheName);\n            cache.put(e.request, resp.clone());\n            // 以 Request 为键名，参见 https://developer.mozilla.org/docs/Web/API/Request\n            return resp;\n        }\n    };\n    e.respondWith(cacheOrRequestThenCache());\n});\n\n// 清理旧版本缓存\nself.addEventListener(&quot;activate&quot;, function (e) {\n    const deleteCache = async () =&gt; {\n        const keyList = await caches.keys();\n        return Promise.all(\n            keyList.map((key) =&gt; {\n                if (cacheName.indexOf(key) === -1) {\n                    return caches.delete(key);\n                }\n            }),\n        );\n    };\n    e.waitUntil(deleteCache());\n});\n\n// 向主线程发送消息\nself.clients.matchAll().then((clients) =&gt; {\n    clients.forEach((client) =&gt; client.postMessage(&quot;msg from sw&quot;));\n});\n</code></pre>\n<h2>更新 sw.js</h2>\n<pre><code class=\"language-js\">const cacheName = &quot;PWA-v2&quot;; // sw版本\nconst contentToCache = [&quot;/index.html&quot;, &quot;/css/styles.css?v2&quot;]; // 缓存清单\nself.addEventListener(&quot;install&quot;, (e) =&gt; {\n    const preCache = async () =&gt; {\n        const cache = await caches.open(cacheName);\n        return cache.addAll(contentToCache);\n    };\n    e.waitUntil(preCache());\n});\n// ... ...\n</code></pre>\n<p>这个时候一个新的 Service Worker 会在后台被安装，而旧的 Service Worker 仍然会正常运行，直到没有任何页面使用到它为止，这时候新的 Service Worker 将会被激活，然后接管所有的页面。</p>\n<h2><a href=\"https://developer.mozilla.org/docs/Web/Manifest\">manifest.webmanifest</a>，添加 PWA 到主屏幕</h2>\n<p>参见：<a href=\"https://web.dev/add-manifest/\">https://web.dev/add-manifest/</a></p>\n<p>扩展名亦可使用 json</p>\n<pre><code class=\"language-json\">{\n    &quot;background_color&quot;: &quot;purple&quot;,\n    &quot;description&quot;: &quot;This is an example PWA App&quot;,\n    &quot;display&quot;: &quot;fullscreen&quot;,\n    &quot;icons&quot;: [\n        {\n            &quot;src&quot;: &quot;icon/icon.png&quot;,\n            &quot;sizes&quot;: &quot;192x192&quot;,\n            &quot;type&quot;: &quot;image/png&quot;\n        }\n    ],\n    &quot;name&quot;: &quot;My PWA App Name&quot;,\n    &quot;short_name&quot;: &quot;My App&quot;,\n    &quot;start_url&quot;: &quot;/index.html&quot;\n}\n</code></pre>\n<p>有关事件（Chrome）</p>\n<pre><code class=\"language-js\">window.addEventListener(&quot;beforeinstallprompt&quot;, (e) =&gt; {\n    // chromium &gt;= 44\n    // e.preventDefault(); 阻止默认显示安装提示\n    // e.prompt(); 手动显示安装提示 // chromium &gt;= 45\n    console.log(e.platforms); // e.g., [&quot;web&quot;, &quot;android&quot;, &quot;windows&quot;]\n    e.userChoice.then((outcome) =&gt; {\n        console.log(outcome); // either &quot;installed&quot;, &quot;dismissed&quot;, etc.\n    }, handleError);\n});\n</code></pre>\n<p>仅使用 manifest 无法使应用离线运行，离线运行实际上还是靠 Service Worker 实现<br>注意：Chrome 要求应用程序注册一个 Service Worker 才会显示添加到主屏幕提示\n<a href=\"https://developer.mozilla.org/docs/Web/Manifest\">https://developer.mozilla.org/docs/Web/Manifest</a></p>\n<h1>使用 UpUp 库</h1>\n<p>对于简单的离线应用，可以使用 UpUp 库<br><a href=\"https://github.com/TalAter/UpUp\">https://github.com/TalAter/UpUp</a><br>注意：Service Worker 必须从同域加载，所以无法使用 cdn</p>\n<pre><code class=\"language-html\">&lt;script src=&quot;upup.min.js&quot;&gt;&lt;/script&gt;\n&lt;script&gt;\n    UpUp.start({\n        &quot;content-url&quot;: &quot;offline.html&quot;, // 离线时显示offline.html\n        assets: [&quot;/img/logo.png&quot;, &quot;/css/style.css&quot;, &quot;headlines.json&quot;],\n    });\n&lt;/script&gt;\n</code></pre>\n","tags":["web-api","js"]},{"id":"nodejs-package","url":"https://yieldray.fun/posts/nodejs-package","title":"打包Node.js项目","date_published":"2022-02-07T22:56:51.000Z","date_modified":"2022-02-07T22:56:51.000Z","content_text":"<h2>@vercel/ncc</h2>\n<p><a href=\"https://github.com/vercel/ncc\">https://github.com/vercel/ncc</a><br>这个工具可以把 node.js 项目打包成单个 js 文件，打包后还可以顺便用 <a href=\"https://github.com/terser/terser\">terser</a>/<a href=\"https://github.com/mishoo/UglifyJS\">uglify-js</a> 之类的压缩一下，也可以加上<a href=\"https://github.com/javascript-obfuscator/javascript-obfuscator\">混淆</a></p>\n<h2>pkg</h2>\n<p><a href=\"https://github.com/vercel/pkg\">https://github.com/vercel/pkg</a><br>也是来自 vercel 的，直接打包成单个二进制文件，自带运行时</p>\n<h2>nexe</h2>\n<p><a href=\"https://github.com/nexe/nexe\">https://github.com/nexe/nexe</a><br>打包成二进制文件，貌似不怎么维护了，不支持动态 require<br><img src=\"https://s2.loli.net/2022/02/07/SjZHncsmIACX6Fk.png\" alt=\"release\"></p>\n<h2>node-packer</h2>\n<p><a href=\"https://github.com/pmq20/node-packer\">https://github.com/pmq20/node-packer</a></p>\n<h2>postject</h2>\n<p><a href=\"https://github.com/nodejs/postject\">https://github.com/nodejs/postject</a></p>\n<h2>caxa</h2>\n<p><a href=\"https://github.com/leafac/caxa\">https://github.com/leafac/caxa</a></p>\n","tags":["node.js"]},{"id":"android6-webview","url":"https://yieldray.fun/posts/android6-webview","title":"Android6折腾webview","date_published":"2022-01-18T20:46:38.000Z","date_modified":"2022-01-18T20:46:38.000Z","content_text":"<p>想让旧手机物尽其用，但是它的系统是安卓 6，自带的 webview 是 chromium44，想着给它升级一下，否则没法用了。<br>然而蛋疼的是这国产 ROM 改了 SystemWebview 的包名，搞得没法直接覆盖安装新版本，遂在此记录一下。</p>\n<p>先下载了最新的 webview，它的包名是 <code>com.google.android.webview</code>，系统的是 <code>com.android.webview</code><br>在网上搜索发现要改<code>/frameworks/base/core/res/res/values/config.xml</code>文件，然而这手机并没<code>/frameworks</code>这个路径</p>\n<p>最后是这么解决的（参考<a href=\"https://www.cnblogs.com/kinglandsoft/p/13261375.html\">https://www.cnblogs.com/kinglandsoft/p/13261375.html</a>）<br>用 mt 文件管理器，找到<code>/system/framework/framework-res.apk</code> 这个 apk。点击它，选查看，找到 <code>resources.arsc</code>，点击它，选 <code>Arsc 编辑器</code>，选<code>字符常量池</code>，右上角菜单点击<code>过滤</code>，内容为 <code>com.android.webview</code>（或者直接 webview）。点击搜索结果中的 <code>com.android.webview</code>，改成 <code>com.google.android.webview</code>。保存并一直返回，mt 会自动更新 apk 并自动备份。</p>\n<p>顺便记一下安卓 5、6 怎么用 termux，termux 已经不维护安卓 5、6 的源了。</p>\n<pre><code class=\"language-sh\">echo &quot;deb https://termux.com/game-packages-21-bin games stable&quot; &gt; $PREFIX/etc/apt/sources.list.d/game.list\necho &quot;deb https://termux.com/science-packages-21-bin science stable&quot; &gt; $PREFIX/etc/apt/sources.list.d/science.list\n</code></pre>\n","tags":["note"]},{"id":"css-word-break-and-word-wrap","url":"https://yieldray.fun/posts/css-word-break-and-word-wrap","title":"word-break和word-wrap","date_published":"2021-12-24T14:24:51.000Z","date_modified":"2021-12-24T14:24:51.000Z","content_text":"<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/word-break\">word-break</a></p>\n<table>\n<thead>\n<tr>\n<th>字母换行规则：word-break属性</th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td>名称</td>\n<td>word-break</td>\n</tr>\n<tr>\n<td>值</td>\n<td>normal | keep-all | break-all | break-word</td>\n</tr>\n<tr>\n<td>初始值</td>\n<td>normal</td>\n</tr>\n<tr>\n<td>应用于</td>\n<td>文本</td>\n</tr>\n<tr>\n<td>继承</td>\n<td>是</td>\n</tr>\n</tbody></table>\n<p>word-wrap 是 <a href=\"https://developer.mozilla.org/docs/Web/CSS/overflow-wrap\">overflow-wrap</a> 的别名</p>\n<table>\n<thead>\n<tr>\n<th>溢出换行：overflow-wrap/word-wrap属性</th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td>名称</td>\n<td>overflow-wrap, word-wrap</td>\n</tr>\n<tr>\n<td>值</td>\n<td>normal | break-word | anywhere</td>\n</tr>\n<tr>\n<td>初始值</td>\n<td>normal</td>\n</tr>\n<tr>\n<td>应用于</td>\n<td>文本</td>\n</tr>\n<tr>\n<td>继承</td>\n<td>是</td>\n</tr>\n</tbody></table>\n<iframe height=\"300\" style=\"width: 100%;\" scrolling=\"no\" title=\"Untitled\" src=\"https://codepen.io/YieldRay/embed/XWezrWm?default-tab=html%2Cresult\" frameborder=\"no\" loading=\"lazy\" allowtransparency=\"true\" allowfullscreen=\"true\">\n  See the Pen <a href=\"https://codepen.io/YieldRay/pen/XWezrWm\">\n  Untitled</a> by Ray (<a href=\"https://codepen.io/YieldRay\">@YieldRay</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.\n</iframe>\n\n<p><a href=\"https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/text-wrap\">text-wrap</a></p>\n<table>\n<thead>\n<tr>\n<th>text-wrap 简写属性</th>\n<th></th>\n</tr>\n</thead>\n<tbody><tr>\n<td>初始值</td>\n<td>wrap</td>\n</tr>\n<tr>\n<td>应用于</td>\n<td>文本和块容器</td>\n</tr>\n<tr>\n<td>继承</td>\n<td>是</td>\n</tr>\n<tr>\n<td>百分比</td>\n<td>简写属性的各个属性：<br/>text-wrap-mode: 无<br/>text-wrap-style: 无</td>\n</tr>\n<tr>\n<td>计算值</td>\n<td>简写属性的各个属性：<br/>text-wrap-mode: 按指定值<br/>text-wrap-style: 按指定值</td>\n</tr>\n<tr>\n<td>动画类型</td>\n<td>简写属性的各个属性：<br/>text-wrap-mode: 离散<br/>text-wrap-style: 离散</td>\n</tr>\n</tbody></table>\n","tags":["css"]},{"id":"timing-with-curl","url":"https://yieldray.fun/posts/timing-with-curl","title":"用curl来测速","date_published":"2021-12-22T09:51:17.000Z","date_modified":"2021-12-22T09:51:17.000Z","content_text":"<p>翻译自<a href=\"https://susam.in/maze/timing-with-curl.html\">https://susam.in/maze/timing-with-curl.html</a></p>\n<p>下面的命令可以用来测试 HTTP 请求花费的时间：</p>\n<pre><code class=\"language-sh\">curl -L -w &quot;time_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_pretransfer: %{time_pretransfer}\ntime_redirect: %{time_redirect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n&quot; https://example.com/\n</code></pre>\n<p>这里还有一个写成一行的命令，要用的时候直接复制粘贴就可以了：</p>\n<pre><code class=\"language-sh\">curl -L -w &quot;time_namelookup: %{time_namelookup}\\ntime_connect: %{time_connect}\\ntime_appconnect: %{time_appconnect}\\ntime_pretransfer: %{time_pretransfer}\\ntime_redirect: %{time_redirect}\\ntime_starttransfer: %{time_starttransfer}\\ntime_total: %{time_total}\\n&quot; https://example.com/\n</code></pre>\n<p>一般来说，这个命令有如下输出：</p>\n<pre><code class=\"language-sh\">$ curl -L -w &quot;namelookup: %{time_namelookup}\\nconnect: %{time_connect}\\nappconnect: %{time_appconnect}\\npretransfer: %{time_pretransfer}\\nstarttransfer: %{time_starttransfer}\\ntotal: %{time_total}\\n&quot; https://example.com/\n&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot;&gt;\n&lt;html&gt;\n...\n&lt;/html&gt;\ntime_namelookup: 0.001403\ntime_connect: 0.245464\ntime_appconnect: 0.757656\ntime_pretransfer: 0.757823\ntime_redirect: 0.000000\ntime_starttransfer: 0.982111\ntime_total: 0.982326\n</code></pre>\n<p>为了简洁，上面的输出中用省略号代替了 HTML 内容</p>\n<p>下面对每一行的输出进行解释，测试版本为 curl 7.20.0：</p>\n<p>time_namelookup: 解析域名所用时，单位为秒</p>\n<p>time_connect: 从开始建立 TCP 连接到服务器（或者代理服务器）到完成所用时</p>\n<p>time_appconnect: 从开始进行 SSL（或 SSH）等连接（或握手）到完成所用时（添加自 7.19.0）</p>\n<p>time_pretransfer: 文件传输即将开始之前的过程所用时间，包括了所有预传输命令和协议选择等</p>\n<p>time_redirect: 所有重定向所用时间，包括了域名解析，建立连接，预传输和直到最后的事务处理开始之前的传输用时。time_redirect 表示所有多重重定向花费的时间。（添加自 7.12.3）</p>\n<p>time_starttransfer: 在一个字节将于开始传输之前所有操作所用时。包括了 time_pretransfer 当然还有服务器计算结果所花费的时间</p>\n<p>注意，time_appconnect 和 time_connect 时间相减就可以得出 SSL/TLS 握手花费的时间。对于没有 SSL/TLS 的明文连接，这个数字为零。下面是一个示例输出：</p>\n<pre><code class=\"language-sh\">$ curl -L -w &quot;time_namelookup: %{time_namelookup}\\ntime_connect: %{time_connect}\\ntime_appconnect: %{time_appconnect}\\ntime_pretransfer: %{time_pretransfer}\\ntime_redirect: %{time_redirect}\\ntime_starttransfer: %{time_starttransfer}\\ntime_total: %{time_total}\\n&quot; http://example.com/\n&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot;&gt;\n&lt;html&gt;\n...\n&lt;/html&gt;\ntime_namelookup: 0.001507\ntime_connect: 0.247032\ntime_appconnect: 0.000000\ntime_pretransfer: 0.247122\ntime_redirect: 0.000000\ntime_starttransfer: 0.512645\ntime_total: 0.512853\n</code></pre>\n<p>注意到上面两个输出中 time_redirect 都为零。这是因为访问 example.com 时，没有发生重定向。下面的示例演示了发生重定向时的输出：</p>\n<pre><code class=\"language-sh\">$ curl -L -w &quot;time_namelookup: %{time_namelookup}\\ntime_connect: %{time_connect}\\ntime_appconnect: %{time_appconnect}\\ntime_pretransfer: %{time_pretransfer}\\ntime_redirect: %{time_redirect}\\ntime_starttransfer: %{time_starttransfer}\\ntime_total: %{time_total}\\n&quot; https://susam.in/blog\n&lt;!DOCTYPE HTML&gt;\n&lt;html&gt;\n...\n&lt;/html&gt;\ntime_namelookup: 0.001886\ntime_connect: 0.152445\ntime_appconnect: 0.465326\ntime_pretransfer: 0.465413\ntime_redirect: 0.614289\ntime_starttransfer: 0.763997\ntime_total: 0.765413\n</code></pre>\n<p>这个命令的结果有助于发现可能导致延迟的因素，所以当在 web 服务中遇到某些的延迟问题时，往往可以用这个命令来测试。</p>\n","tags":[]},{"id":"clang-test-code-pieces","url":"https://yieldray.fun/posts/clang-test-code-pieces","title":"C语言测试代码","date_published":"2021-12-07T14:43:01.000Z","date_modified":"2021-12-07T14:43:01.000Z","content_text":"<pre><code class=\"language-c\">int GCD(int a, int b)\n{\n    if (b)\n        while ((a %= b) &amp;&amp; (b %= a))\n            ;\n    return a + b;\n}\n</code></pre>\n<pre><code class=\"language-c\">// 遍历整数的每一位\nint count = 0;\nwhile (cal &gt; 0)\n{\n    int last = cal % 10;\n    count++;\n    cal /= 10;\n}\n</code></pre>\n<pre><code class=\"language-c\">// 非实用\n// 不占用额外空间交换整数\nvoid swap(int *a, int *b)\n{\n    *a = *a ^ *b;\n    *b = *a ^ *b;\n    *a = *a ^ *b;\n}\n</code></pre>\n<pre><code class=\"language-c\">char *strcpy(char *dest, const char *source)\n{\n    char *ptr = dest;\n    while (*dest++ = *source++)\n        ;\n    return ptr;\n}\n</code></pre>\n<pre><code class=\"language-c\">// 不引入 stdbool\ntypedef enum {false, true} bool;\n</code></pre>\n<pre><code class=\"language-c\">// 约瑟夫\nint josephus(int n, int m)\n{\n    if (n == 1)\n        return 0;\n    else\n        return (josephus(n - 1, m) + m) % n;\n}\n</code></pre>\n<pre><code class=\"language-c\">// 汉诺塔\nvoid move(int id, char from, char to)\n{\n    static int cnt = 0;\n    printf(&quot;step %d: move %d from %c-&gt;%c\\n&quot;, ++cnt, id, from, to);\n}\nvoid hanoi(int n, char x, char y, char z)\n{\n    if (n == 0)\n        return;\n    hanoi(n - 1, x, z, y);\n    move(n, x, z);\n    hanoi(n - 1, y, x, z);\n}\n// USAGE\n// int main()\n// {\n//     int n;\n//     scanf(&quot;%d&quot;, &amp;n);\n//     hanoi(n, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;);\n// }\n</code></pre>\n","tags":["c"]},{"id":"js-typed-array-blob","url":"https://yieldray.fun/posts/js-typed-array-blob","title":"TypedArray/Blob/ReadableStream","date_published":"2021-12-06T21:51:56.000Z","date_modified":"2021-12-06T21:51:56.000Z","content_text":"<p>JavaScript 中操纵二进制的接口</p>\n<h1>TypedArray</h1>\n<pre><code class=\"language-js\">Math.log2(32768); // =&gt; 15\n\nnew Int16Array([0o77777]).toString(); // =&gt; &#39;32767&#39;\nnew Int16Array([2 ** 15 - 1]).toString(); // =&gt; &#39;32767&#39;\n// int16 可以保存2^15位的整数和正负号\nnew Uint16Array([0o777777]).toString(); // =&gt; &#39;65535&#39;\nnew Uint16Array([2 ** 16 - 1]).toString(); // =&gt; &#39;65535&#39;\nnew Uint16Array([0xffff]).toString(); // =&gt; &#39;65535&#39;\n// unsigned int16 即16位无符号整数，可以保存2^16位的正整数\n\nnew Int16Array([23.4]).toString(); // =&gt; &#39;23&#39;\n// 小数部分会被截断\nnew Int16Array([2 ** 15]).toString(); // =&gt; &#39;-32768&#39;\n// 越界到控制位\n</code></pre>\n<h1>ArrayBuffer &amp; DataView</h1>\n<p>ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。<br>它是一个字节数组，通常在其他语言中称为“byte array”。</p>\n<p><a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer\">https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer</a>\n<a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/DataView\">https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/DataView</a></p>\n<p>构造</p>\n<pre><code class=\"language-ts\">ArrayBuffer: new (byteLength: number) =&gt; ArrayBuffer\nDataView: new (buffer: ArrayBuffer | SharedArrayBuffer, byteOffset?: number, byteLength?: number) =&gt; DataView\n(method) DataView.set$Type$(byteOffset: number, value: number, littleEndian?: boolean): void\n(method) DataView.get$Type$(byteOffset: number, littleEndian?: boolean): number\n// 其中 $Type$ 替换为实际类型\n</code></pre>\n<p>ArrayBuffer 无法直接读写，要通过 DataView 对象操作（或 TypedArray）</p>\n<pre><code class=\"language-js\">const buffer = new ArrayBuffer(16); // create an ArrayBuffer with a size in bytes\nconst view = new DataView(buffer);\nview.setInt8(1, 127); // (max signed 8-bit integer)\nconsole.log(view.getInt8(1)); // =&gt; 127\n</code></pre>\n<p><img src=\"https://s2.loli.net/2021/12/07/XGgVTWv8NCtZnJL.png\" alt=\"dataview.png\"></p>\n<h2>Blob (Binary Large Object)</h2>\n<pre><code class=\"language-ts\">Blob: new (blobParts?: BlobPart[], options?: BlobPropertyBag) =&gt; Blob\n\ninterface Blob {\n    readonly size: number;\n    readonly type: string;\n    arrayBuffer(): Promise&lt;ArrayBuffer&gt;;\n    slice(start?: number, end?: number, contentType?: string): Blob;\n    stream(): ReadableStream;\n    text(): Promise&lt;string&gt;;\n}\n\n\nconst debug = { hello: &quot;world&quot; };\nconst blob = new Blob([JSON.stringify(debug)], { type: &quot;application/json&quot; }); // =&gt; Blob { size: 17, type: &quot;application/json&quot; }\nconsole.log(await blob.text()); // =&gt; {&quot;hello&quot;:&quot;world&quot;}\nconsole.log(await blob.slice(2).text()); // =&gt; hello&quot;:&quot;world&quot;}\n\n\nURL: new (url: string | URL, base?: string | URL) =&gt; URL\n(method) createObjectURL(obj: Blob | MediaSource): string\n</code></pre>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/Blob\">https://developer.mozilla.org/docs/Web/API/Blob</a></p>\n<p><img src=\"https://s2.loli.net/2021/12/07/3ZK4Bvlxd587Arc.png\" alt=\"屏幕截图 2021-12-07 122826.png\">\n<img src=\"https://s2.loli.net/2021/12/07/zKYEkhpOvRXTeDU.png\" alt=\"屏幕截图 2021-12-07 123407.png\"></p>\n<pre><code class=\"language-js\">// 因为是json格式所以也可以这样读取\nlet text = await new Response(blob).json();\n\n// 注意，数组会进行合并\nconsole.log(await new Response(new Blob([&quot;123&quot;, &quot;456&quot;])).text()); // =&gt; &#39;123456&#39;\n</code></pre>\n<h2>URL.createObjectURL()</h2>\n<p><a href=\"https://developer.mozilla.org/docs/Web/API/URL/createObjectURL\">https://developer.mozilla.org/docs/Web/API/URL/createObjectURL</a></p>\n<pre><code class=\"language-js\">// 获取一张图片\n\n//  &lt;img id=&quot;myImage&quot; /&gt;\nfetch(&quot;test.png&quot;)\n    .then((res) =&gt; res.blob())\n    .then((res) =&gt; {\n        const objectURL = URL.createObjectURL(res);\n        myImage.src = objectURL;\n    });\n</code></pre>\n<h1>ReadableStream</h1>\n<p>浏览器原生 ReadableStream 不同于 nodejs 在 node:stream 中提供的 Readable<br><a href=\"https://developer.mozilla.org/docs/Web/API/ReadableStream\">https://developer.mozilla.org/docs/Web/API/ReadableStream</a></p>\n<pre><code class=\"language-ts\">ReadableStream: new &lt;any&gt;(underlyingSource?: UnderlyingSource&lt;any&gt;, strategy?: QueuingStrategy&lt;any&gt;) =&gt; ReadableStream&lt;any&gt;\n\ninterface ReadableStream&lt;R = any&gt; {\n    [Symbol.iterator](): IterableIterator&lt;any&gt;;\n    entries(): IterableIterator&lt;[number, any]&gt;;\n    keys(): IterableIterator&lt;number&gt;;\n    values(): IterableIterator&lt;any&gt;;\n}\n\ninterface UnderlyingSource&lt;R = any&gt; {\n    cancel?: UnderlyingSourceCancelCallback;\n    pull?: UnderlyingSourcePullCallback&lt;R&gt;;\n    start?: UnderlyingSourceStartCallback&lt;R&gt;;\n    type?: undefined;\n}\n\ninterface QueuingStrategy&lt;T = any&gt; {\n    highWaterMark?: number;\n    size?: QueuingStrategySize&lt;T&gt;;\n}\n\ninterface ReadableStream&lt;R = any&gt; {\n    readonly locked: boolean;\n    cancel(reason?: any): Promise&lt;void&gt;;\n    getReader(): ReadableStreamDefaultReader&lt;R&gt;;\n    pipeThrough&lt;T&gt;(transform: ReadableWritablePair&lt;T, R&gt;, options?: StreamPipeOptions): ReadableStream&lt;T&gt;;\n    pipeTo(destination: WritableStream&lt;R&gt;, options?: StreamPipeOptions): Promise&lt;void&gt;;\n    tee(): [ReadableStream&lt;R&gt;, ReadableStream&lt;R&gt;];\n}\n</code></pre>\n","tags":["web-api","js"]},{"id":"base64","url":"https://yieldray.fun/posts/base64","title":"base64笔记","date_published":"2021-12-05T18:19:28.000Z","date_modified":"2021-03-07T22:07:00.000Z","content_text":"<h1>base64</h1>\n<p><a href=\"https://zh.wikipedia.org/wiki/Base64\">Base64 - 维基百科，自由的百科全书 (wikipedia.org)</a></p>\n<p>base64 可以将二进制数据以文本形式呈现</p>\n<p>首先将任意数据以二进制表示，每六比特分为一组，再向每组之前补上两个零，构成八比特（ASCII 中一个字符占一个字节，即八比特，2^8=256）</p>\n<p>然后再通过 base64 表转换成 ASCII，即可编码为 base64</p>\n<p>因此，在 base64（以 ASCII 表示）中，共有 2^6=64 种 ASCII 字符</p>\n<p>但是额外占用了约 4/3 的空间</p>\n<p>例如：</p>\n<p>文本：Man<br>ASCII（DEC）：77,97,110<br>BIN：01001101,01100001,01101110<br>base64 group：010011,010110,000101,101110<br>base64：TWFu</p>\n<blockquote>\n<p>注：<br>binary 二进制<br>octal 八进制<br>hexadecimal 十六进制<br>decimal 十进制</p>\n</blockquote>\n<p><strong>Base64 索引表</strong></p>\n<table>\n<thead>\n<tr>\n<th align=\"left\">数值</th>\n<th>字符</th>\n<th align=\"left\">数值</th>\n<th>字符</th>\n<th align=\"left\">数值</th>\n<th>字符</th>\n<th align=\"left\">数值</th>\n<th>字符</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"left\">0</td>\n<td>A</td>\n<td align=\"left\">16</td>\n<td>Q</td>\n<td align=\"left\">32</td>\n<td>g</td>\n<td align=\"left\">48</td>\n<td>w</td>\n</tr>\n<tr>\n<td align=\"left\">1</td>\n<td>B</td>\n<td align=\"left\">17</td>\n<td>R</td>\n<td align=\"left\">33</td>\n<td>h</td>\n<td align=\"left\">49</td>\n<td>x</td>\n</tr>\n<tr>\n<td align=\"left\">2</td>\n<td>C</td>\n<td align=\"left\">18</td>\n<td>S</td>\n<td align=\"left\">34</td>\n<td>i</td>\n<td align=\"left\">50</td>\n<td>y</td>\n</tr>\n<tr>\n<td align=\"left\">3</td>\n<td>D</td>\n<td align=\"left\">19</td>\n<td>T</td>\n<td align=\"left\">35</td>\n<td>j</td>\n<td align=\"left\">51</td>\n<td>z</td>\n</tr>\n<tr>\n<td align=\"left\">5</td>\n<td>F</td>\n<td align=\"left\">21</td>\n<td>V</td>\n<td align=\"left\">37</td>\n<td>l</td>\n<td align=\"left\">53</td>\n<td>1</td>\n</tr>\n<tr>\n<td align=\"left\">6</td>\n<td>G</td>\n<td align=\"left\">22</td>\n<td>W</td>\n<td align=\"left\">38</td>\n<td>m</td>\n<td align=\"left\">54</td>\n<td>2</td>\n</tr>\n<tr>\n<td align=\"left\">8</td>\n<td>I</td>\n<td align=\"left\">24</td>\n<td>Y</td>\n<td align=\"left\">40</td>\n<td>o</td>\n<td align=\"left\">56</td>\n<td>4</td>\n</tr>\n<tr>\n<td align=\"left\">9</td>\n<td>J</td>\n<td align=\"left\">25</td>\n<td>Z</td>\n<td align=\"left\">41</td>\n<td>p</td>\n<td align=\"left\">57</td>\n<td>5</td>\n</tr>\n<tr>\n<td align=\"left\">10</td>\n<td>K</td>\n<td align=\"left\">26</td>\n<td>a</td>\n<td align=\"left\">42</td>\n<td>q</td>\n<td align=\"left\">58</td>\n<td>6</td>\n</tr>\n<tr>\n<td align=\"left\">11</td>\n<td>L</td>\n<td align=\"left\">27</td>\n<td>b</td>\n<td align=\"left\">43</td>\n<td>r</td>\n<td align=\"left\">59</td>\n<td>7</td>\n</tr>\n<tr>\n<td align=\"left\">12</td>\n<td>M</td>\n<td align=\"left\">28</td>\n<td>c</td>\n<td align=\"left\">44</td>\n<td>s</td>\n<td align=\"left\">60</td>\n<td>8</td>\n</tr>\n<tr>\n<td align=\"left\">13</td>\n<td>N</td>\n<td align=\"left\">29</td>\n<td>d</td>\n<td align=\"left\">45</td>\n<td>t</td>\n<td align=\"left\">61</td>\n<td>9</td>\n</tr>\n<tr>\n<td align=\"left\">14</td>\n<td>O</td>\n<td align=\"left\">30</td>\n<td>e</td>\n<td align=\"left\">46</td>\n<td>u</td>\n<td align=\"left\">62</td>\n<td>+</td>\n</tr>\n<tr>\n<td align=\"left\">15</td>\n<td>P</td>\n<td align=\"left\">31</td>\n<td>f</td>\n<td align=\"left\">47</td>\n<td>v</td>\n<td align=\"left\">63</td>\n<td>/</td>\n</tr>\n</tbody></table>\n<p>最后，如果剩余了四比特或者二比特不够六比特的，在其后面填充零以构成六比特，每补充两比特，在生成的 base64 后面补充一个等号（=）</p>\n<p>例如：\n文本：A (dec:65)\nBIN：01000001<br>base64 group：010000,010000<br>base64：QQ==</p>\n<pre><code class=\"language-js\">// binary to ascii\nbtoa(); // encode\nbtoa(&quot;A&quot;); // =&gt; &#39;QQ==&#39;\n</code></pre>\n<pre><code class=\"language-js\">// ascii to binary\natob(); // decode\natob(&quot;QQ==&quot;); // =&gt; &#39;A&#39;\n</code></pre>\n<h1>Data URLs</h1>\n<pre><code>data:[&lt;mediatype&gt;][;base64],&lt;data&gt;\n</code></pre>\n<p>mediatype 默认值 text/plain;charset=US-ASCII</p>\n<h1>Base64 URL</h1>\n<p>Base64 URL 与普通 Base4 的区别</p>\n<ol>\n<li><p>移除尾部的 =</p>\n</li>\n<li><p>+ 替换成 -</p>\n</li>\n<li><p>/ 替换成 _</p>\n</li>\n</ol>\n<p>如此 base64 编码后的字符串就能在 url 中传递了</p>\n","tags":["crypto"]},{"id":"clang-null-ptr","url":"https://yieldray.fun/posts/clang-null-ptr","title":"C语言空指针","date_published":"2021-11-12T19:22:29.000Z","date_modified":"2021-11-12T19:22:29.000Z","content_text":"<p>NULL 是一个表示空指针的宏，这个宏在多个头文件中（如 stdio.h）都有定义。如果使用 NULL，代码必须包含带有 NULL 宏的头文件。</p>\n<p><img src=\"https://i.loli.net/2021/11/12/8GiDnslM2aL7FkJ.png\" alt=\"mingw64 stdio.h\"></p>\n<blockquote>\n<p>[6.3.2.3-3] If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.</p>\n</blockquote>\n<blockquote>\n<p>[6.3.2.3-Footnote] The macro NULL is defined in &lt;stddef.h&gt; (and other headers) as a null pointer constant</p>\n</blockquote>\n<p>NULL 是一个指针，指向一个随机的地址。有时指向的地址 <code>0</code>，<br>地址 <code>0</code> 是一些系统的保留地址，无法访问。</p>\n<p>NULL 在 C 标准库的头文件 stddef.h 中定义如下</p>\n<pre><code class=\"language-c\">#define NULL ((void *)0)\n</code></pre>\n<p>访问 NULL 就会报 Segmentation fault</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nint main()\n{\n    printf(NULL);  // =&gt; Segmentation fault\n}\n</code></pre>\n<p>C 语言中，字符串（或者说数组）的类型是指针<br>因此，需要注意区分空字符串和空指针</p>\n<p>例如：<code>getenv()</code> 函数在环境变量不存在时返回 <code>NULL</code> 而不是空字符串</p>\n<pre><code class=\"language-c\">if(getenv(&quot;OS&quot;) == NULL) {\n    printf(&quot;env &#39;OS&#39; do not exist&quot;);\n}\n</code></pre>\n","tags":["c"]},{"id":"clang-dynamic-memory-management","url":"https://yieldray.fun/posts/clang-dynamic-memory-management","title":"C语言 内存分配 笔记","date_published":"2021-11-08T21:04:14.000Z","date_modified":"2021-11-08T21:04:14.000Z","content_text":"<p>参见<a href=\"https://zh.cppreference.com/w/c/memory\">https://zh.cppreference.com/w/c/memory</a></p>\n<h1>#include &lt;stdlib.h&gt;</h1>\n<p>内存分配相关函数定义在 <code>stdlib.h</code> 中</p>\n<h2><code>void* malloc( size_t size );</code></h2>\n<p>分配 <code>size</code> 字节的<strong>未初始化</strong>内存。</p>\n<p>若分配成功，则返回为任何拥有<em>基础对齐</em>的对象类型对齐的指针。</p>\n<p>若 <code>size</code> 为零，则 <code>malloc</code> 的行为是实现定义的。例如可返回空指针。亦可返回非空指针；但不应当<em>解引用</em>这种指针，而且应将它传递给 <code>free</code> 以避免内存泄漏。</p>\n<p>为获取返回值，在 <code>malloc()</code> 函数前进行强制类型转换</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\n#include &lt;stdlib.h&gt;\nint main()\n{\n    int *arr = (int *)malloc(3 * sizeof(int));\n    arr[0] = 1;\n    arr[1] = 2;\n    arr[2] = 3;\n    for (int i = 0; i &lt; 3; i++)\n    {\n        printf(&quot;%d &quot;, arr[i]);\n    }\n}\n</code></pre>\n<p>动态创建数组，实际上分配了 <code>3 * sizeof(int)</code>的空间给 <code>arr</code>，<code>arr</code> 被定义为指向这块内存首个元素的指针（因为已经进行了强制转换）<br><code>void</code> 类型时返回内存块首字节的地址<br>于是可以通过数组语法访问这块内存</p>\n<p><code>malloc()</code> 分配的空间是未初始化的，这意味着使用时需要手动初始化</p>\n<h2><code>void* calloc( size_t num, size_t size );</code></h2>\n<p>为 <code>num</code> 个对象的数组分配内存，并<strong>初始化</strong>所有分配存储中的字节为零。</p>\n<p>若分配成功，会返回指向分配内存块最低位（首位）字节的指针，它为任何类型适当地对齐。</p>\n<p>若 <code>size</code> 为零，则行为是实现定义的（可返回空指针，或返回不可用于访问存储的非空指针）。</p>\n<p>注意：\n因为对齐需求的缘故，分配的字节数不必等于 <code>num * size</code> 。\n初始化所有位为零不保证浮点数或指针被各种初始化为 <code>0.0</code> 或空指针（尽管这在所有常见平台上为真）。</p>\n<h2><code>void* realloc( void *ptr, size_t new_size );</code></h2>\n<p>重新分配给定的内存区域。它必须是之前为 <code>malloc()</code> 、 <code>calloc()</code> 或 <code>realloc()</code> 所分配，并且仍未被 <code>free()</code> 或 <code>realloc()</code> 的调用所释放。否则，结果未定义。</p>\n<p>重新分配按以下二者之一执行：</p>\n<p>a) 可能的话，扩张或收缩 <code>ptr</code> 所指向的已存在内存。内容在新旧大小中的较小者范围内保持不变。若扩张范围，则数组新增部分的内容是未定义的。<br>b) 分配一个大小为 <code>new_size</code> 字节的新内存块，并复制大小等于新旧大小中较小者的内存区域，然后释放旧内存块。<br>若无足够内存，则不释放旧内存块，并返回空指针。</p>\n<p>若 <code>ptr</code> 是 <code>NULL</code> ，则行为与调用 <code>malloc(new_size)</code> 相同。</p>\n<p>若 <code>new_size</code> 为零，则行为是实现定义的（可返回空指针，此情况下可能或可能不释放旧内存，或返回不会用于访问存储的非空指针）。</p>\n<h2><code>void free( void* ptr );</code></h2>\n<p>解分配之前由 <code>malloc()</code> 、 <code>calloc()</code> 、 <code>aligned_alloc()</code> (C11 起) 或 <code>realloc()</code> 分配的空间。</p>\n<p>若 <code>ptr</code> 为空指针，则函数不进行操作。</p>\n<p>若 <code>ptr</code> 的值不等于之前从 <code>malloc()</code> 、 <code>calloc()</code> 、 <code>realloc()</code> 或 <code>aligned_alloc()</code> (C11 起) 返回的值，则行为未定义。</p>\n<p>若 <code>ptr</code> 所指代的内存区域已经被解分配，则行为未定义，即是说已经以 <code>ptr</code> 为参数调用 <code>free()</code> 或 <code>realloc()</code> ，而且没有后继的 <code>malloc()</code> 、 <code>calloc()</code> 或 <code>realloc()</code> 调用以 <code>ptr</code> 为结果。</p>\n<p>若在 <code>free()</code> 返回后通过指针 <code>ptr</code> 访问内存，则行为未定义（除非另一个分配函数恰好返回等于 <code>ptr</code> 的值）。</p>\n<h1><code>#include &lt;string.h&gt;</code></h1>\n<p>字符数组操作相关函数定义于在 string.h<br>mem 系列函数</p>\n<h2><code>void* memchr( const void* ptr, int ch, size_t count );</code></h2>\n<p>在 <code>ptr</code> 所指向对象的首 <code>count</code> 个字符（每个都转译成 <code>unsigned char</code> ）中寻找 <code>ch</code> （在如同以 <code>(unsigned char)ch</code> 转换到 <code>unsigned char</code> 后）的首次出现。</p>\n<p>若访问出现于被搜索的数组结尾后，则行为未定义。若 ptr 为空指针则行为未定义</p>\n<p>注：该函数返回指向找到的字符位置的指针（也就是说输出该指针如同输出<strong>找到的字符及其之后的字符</strong>（直到<code>&#39;\\0&#39;</code>）），或若找不到该字符则为空指针。</p>\n<h2><code>int memcmp( const void* lhs, const void* rhs, size_t count );</code></h2>\n<p>比较 <code>lhs</code> 和 <code>rhs</code> 所指向对象的首 <code>count</code> 个字节。比较按字典序进行。</p>\n<p>结果的符号是在被比较对象中相异的首对字节的值（都转译成 <code>unsigned char</code> ）的差。</p>\n<p>若在 <code>lhs</code> 和 <code>rhs</code> 所指向的任一对象结尾后出现访问，则行为未定义。若 lhs 或 rhs 为空指针则行为未定义。</p>\n<p>注：可以看作 <code>lhs</code> - <code>rhs</code></p>\n<h2><code>void *memset( void *dest, int ch, size_t count );</code></h2>\n<h2><code>errno_t memset_s( void *dest, rsize_t destsz, int ch, rsize_t count );</code> (C11 起)</h2>\n<p>复制值 <code>ch</code> （如同以 <code>(unsigned char)ch</code> 转换到 <code>unsigned char</code> 后）到 <code>dest</code> 所指向对象的首 <code>count</code> 个字节。<br>若出现 <code>dest</code> 数组结尾后的访问则行为未定义。若 <code>dest</code> 为空指针则行为未定义。</p>\n<h2><code>void* memcpy( void *dest, const void *src, size_t count );</code> (C99 前)</h2>\n<h2><code>void* memcpy( void *restrict dest, const void *restrict src, size_t count );</code> (C99 起)</h2>\n<h2><code>errno_t memcpy_s( void *restrict dest, rsize_t destsz, const void *restrict src, rsize_t count );</code> (C11 起)</h2>\n<p>从 <code>src</code> 所指向的对象复制 <code>count</code> 个字符到 <code>dest</code> 所指向的对象。两个对象都被转译成 <code>unsigned char</code> 的数组。<br>若访问发生在 <code>dest</code> 数组结尾后则行为未定义。若对象重叠（这违背 <code>restrict</code> 契约） (C99 起)，则行为未定义。若 <code>dest</code> 或 <code>src</code> 为非法或空指针则行为未定义。</p>\n<h2><code>void* memmove( void* dest, const void* src, size_t count );</code></h2>\n<h2><code>errno_t memmove_s(void *dest, rsize_t destsz, const void *src, rsize_t count);</code> (C11 起)</h2>\n<p>从 src 所指向的对象复制 count 个字节到 dest 所指向的对象。两个对象都被转译成 unsigned char 的数组。对象可以重叠：如同复制字符到临时数组，再从该数组到 dest 一般发生复制。<br>若出现 dest 数组末尾后的访问则行为未定义。若 dest 或 src 为非法或空指针则行为未定义。</p>\n<h2><code>void _memccpy(void _ restrict dest, const void * restrict src, int c, size_t count);</code> (C23 起)</h2>\n<p>从 <code>src</code> 所指向的对象复制字符到 <code>dest</code> 所指向的对象，在满足接下来的任何两个条件之一后停止：</p>\n<ol>\n<li>复制了 <code>count</code> 个字符</li>\n<li>找到（并复制）了字符 <code>(unsigned char)c</code> 。</li>\n</ol>\n<p>转译 <code>src</code> 与 <code>dest</code> 对象为 <code>unsigned char</code> 的数组。<br>若符合任何以下条件则行为未定义：</p>\n<ol start=\"3\">\n<li>出现越过 <code>dest</code> 数组结尾的访问</li>\n<li>对象重叠（这违反 <code>restrict</code> 契约）</li>\n<li><code>dest</code> 或 <code>src</code> 为非法或空指针值</li>\n</ol>\n","tags":["c"]},{"id":"clang-variadic-functions","url":"https://yieldray.fun/posts/clang-variadic-functions","title":"C语言可变参数","date_published":"2021-11-06T21:14:49.000Z","date_modified":"2021-11-06T21:14:49.000Z","content_text":"<p>参见 <a href=\"https://zh.cppreference.com/w/c/variadic\">https://zh.cppreference.com/w/c/variadic</a><br><a href=\"https://linux-c.yieldray.fun/#/2-C-%E8%AF%AD%E8%A8%80%E6%9C%AC%E8%B4%A8/ch24-%E5%87%BD%E6%95%B0%E6%8E%A5%E5%8F%A3?id=_6-%e5%8f%af%e5%8f%98%e5%8f%82%e6%95%b0\">另见</a></p>\n<h2>#include &lt;stdarg.h&gt;</h2>\n<p>要使用可变参数，引入 stdarg.h</p>\n<p>va_list 一个数据类型（指针），声明可变参数指针 ，实际上是 <code>typedef struct __va_list_tag va_list[1]</code></p>\n<p><code>void va_start( va_list ap, parmN );</code> 宏函数，初始化可变参数，以首个可变参数前一个参数定位（parmN 为首个可变参数前的参数名）</p>\n<p><code>T va_arg( va_list ap, T );</code> 宏函数，返回指定数据类型的指针，每读取一次，指针就指向下一个可变参数</p>\n<p><code>void va_end( va_list ap );</code> 宏函数，清理 <code>va_list</code> 类型实例</p>\n<h2>va, variable arguments</h2>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\n#include &lt;stdarg.h&gt;\nvoid printAll(int strAmount, ...)\n{\n    va_list ap; // ap = argument pointer\n    va_start(ap, strAmount);\n    for (int i = 0; i &lt; strAmount; i++)\n    {\n        char *text = va_arg(ap, char *);\n        printf(text);\n    }\n    va_end(ap);\n}\nint main()\n{\n    printAll(3, &quot;111&quot;, &quot;222&quot;, &quot;333&quot;);  // =&gt; 111222333\n}\n</code></pre>\n","tags":["c"]},{"id":"clang-ptr","url":"https://yieldray.fun/posts/clang-ptr","title":"C语言指针","date_published":"2021-10-14T20:31:01.000Z","date_modified":"2021-10-14T20:31:01.000Z","content_text":"<blockquote>\n<h1><strong>注意！以下内容均在 gcc 10.3.0 下测试，情况可能会与 vc++6.0 不一致</strong></h1>\n<p>可以跳着阅读</p>\n</blockquote>\n<h1>指针声明</h1>\n<p><code>指针类型 * 指针名</code><br>注意，这里星号可以在位置任意（星号总是写在类型与指针名之间）<br>甚至无需空格，如以下声明等效<br><code>指针类型* 指针名</code>\n<code>指针类型 *指针名</code><br><code>指针类型*指针名</code></p>\n<p><strong>指针的意思是指针变量，指针变量中存储了地址（内存地址）</strong></p>\n<p>这意味着指针是一个变量（左值），而地址是一个字面量（右值）<br>或者说地址是一个实际的值，而指针是变量</p>\n<p>如<code>00007ff673193000</code>称为地址（字面量），声明<code>int* p;</code>后，<code>p</code> 称为指针变量</p>\n<p><strong>声明<code>int* p;</code>，可以发现，<code>p</code> 的类型为 <code>int*</code>，即 int 类型的指针</strong><br><strong>因为需要知道<em>指针指向的地址中保存的值</em>的类型，所以指针也自然需要类型</strong></p>\n<p>类似于<code>3.14f</code>为浮点数，声明<code>float pi;</code>后，<code>pi</code> 称为浮点变量<br>浮点变量中存储了浮点数，而指针变量中存储的地址</p>\n<p><strong>注意，<code>*p</code>不是指针，<code>p</code>才是指针，<code>*p</code>意味着取得指针指向的地址的值</strong></p>\n<p>因此，一般指针和地址可以互换，所以我们会说指针指向一个值，或者地址指向一个值，意思都是相同的</p>\n<p>指针必须声明其指向的存储单元的类型。例如<code>int* p</code>，可以说<code>p</code>的类型是<code>int*</code>，也可以说 <code>*p</code>的类型是 <code>int</code></p>\n<p>通过 <code>&amp;变量</code> 获取变量的地址</p>\n<p>通过 <code>*地址</code> 来访问该地址</p>\n<pre><code class=\"language-c\">int a = 1;\nint *ptr;\nptr = &amp;a;\n*ptr = *ptr + 1;  // =&gt; 2\n</code></pre>\n<p>上述代码中，声明了指向<code>int</code>类型的指针变量<code>ptr</code>，并将其(指针变量 ptr)赋值为变量 <code>a</code> 的地址</p>\n<p>这样，<code>ptr</code> 就保存了 <code>a</code> 的地址，然后，通过<code>*</code>星号<em>访问</em>(访问意味着可以获取和修改)该地址指向的值</p>\n<p>通过<code>*ptr</code> 访问某一地址指向的值。在上例中，<code>*ptr + 1</code> 返回 <code>2</code>，然后，通过<code>*ptr = *ptr + 1</code> 将该地址指向的值修改为 2</p>\n<h1>指针运算（实质为地址运算）</h1>\n<p>地址类似于<code>00007ff673193000</code>，表示内存单元的编号，它和一般的数不同</p>\n<p>地址与整数值的运算表示地址的移动</p>\n<p><code>地址 + 1</code> 表示地址向后移动一位的地址（一位是指地址类型的长度）</p>\n<p>假设地址为<code>ads = 00000001</code>，那么<code>ads + 1</code>究竟是多少，并不是说一定是<code>00000002</code>，其实取决于 ads 的类型，因为不同类型的变量所占内存空间也不同。</p>\n<p>地址与地址相减，得到地址之间的距离</p>\n<p>地址之间的比较运算(&gt;,&lt;)，比较的是各自的内存地址哪一个更大，返回值是整数 1（true）或 0（false）</p>\n<h1>数组指针</h1>\n<p><strong>数组名是一个常量指针，返回其首元素的地址</strong></p>\n<p>先声明以下变量</p>\n<pre><code class=\"language-c\">int arr[3]; // 为 arr 划分了占用 3 个 int 类型的连续的空间，并可通过 `arr` 访问\narr[0] = 0;\narr[1] = 1;\narr[2] = 2;\n</code></pre>\n<p>依据以上原则，<code>arr</code> 返回<code>&amp;arr[0]</code></p>\n<p><code>*arr</code> 指向 <code>arr[0]</code><br><code>*(arr + 1)</code> 指向 <code>arr[1]</code><br><code>*(arr + 2)</code> 指向 <code>arr[2]</code></p>\n<p>很明显，对于 <code>arr[n]</code>， <code>*(arr + i)</code> 访问 <code>arr[i]</code></p>\n<p>可以这么认为：数组声明<code>int arr[n]</code>实质上开辟了 <code>n</code> 个类型为 <code>int</code> 的空间。<br>此操作类似于<code>int* arr = malloc(n * sizeof(int))</code>（或<code>int* arr = calloc(n, sizeof(int))</code>），手动划分了 <code>n</code> 个类型为 <code>int</code> 的空间。<br>然后，<code>arr</code> 是这段空间起始位置的地址，即 <code>arr == &amp;arr[0]</code>，只不过数组声明的变量由系统管理（栈空间，而不是堆）。</p>\n<p>根据上文可以发现，<code>arr[i]</code> 实际上就是 <code>*(arr + i)</code>，因为 arr 其实是数组起始位置的地址</p>\n<h1>指针数组</h1>\n<p>声明一个元素全为指针的数组，即得到一个指针数组</p>\n<pre><code class=\"language-c\">char* words[3];\nwords[0] = &quot;Hello&quot;;\nwords[1] = &quot;world&quot;;\nwords[2] = &quot;clang&quot;;\n// or  char* words[] = {&quot;Hello&quot;, &quot;world&quot;, &quot;clang&quot;};\nprintf(words[0]);\nprintf(&quot;, &quot;);\nprintf(words[1]);\nprintf(&quot;!  print by &quot;);\nprintf(words[2]);\n// =&gt; Hello, world!  print by clang\n</code></pre>\n<h1>main 函数形参</h1>\n<p><code>int main(int argc, char* argv[])</code><br>一般使用 const 修饰 <code>int main(int argc, char const *argv[])</code><br>其中 argc 意为 argument count，argv 为 argument vector</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nint main(int argc, char const *argv[])\n{\n    for (int i = 0; i &lt; argc; i++)\n        printf(&quot;argv[%d]=%s\\n&quot;, i, argv[i]);\n}\n</code></pre>\n<p>假设编译后文件名为 code.out 或 code.exe</p>\n<pre><code class=\"language-sh\">code.exe hello world hello clang\n或 code.out hello world hello clang\n</code></pre>\n<p>输出</p>\n<pre><code>argv[0]=disk:\\path\\to\\file\\code.exe 或 /path/to/file/code.out\nargv[1]=hello\nargv[2]=world\nargv[3]=hello\nargv[4]=clang\n</code></pre>\n<p>注意 argv[0]是执行文件所在路径，后面的为命令行参数</p>\n<h1>指针作为函数参数</h1>\n<pre><code class=\"language-c\">int sum(int arr[], int len);\nint sum(int* arr, int len);\n</code></pre>\n<p>以上声明是等价的，因为调用数组时都传入数组名，函数内部即获得数组首位置的地址<br>由于函数内部获取的是数组的地址，在函数内部对数组的改动会影响到传入的数组本身</p>\n<h1>引用传递(C++)</h1>\n<pre><code class=\"language-c\">void swap(int &amp;a, int &amp;b){\n    int temp = a;\n    a = b;\n    b = temp;\n}\n</code></pre>\n<p>函数定义时使用了&amp;，表示传递引用，传入的参数和函数内得到的参数是一致的，否则则传递值，此时函数内部无法访问外部参数</p>\n<h1>字符串指针</h1>\n<p><strong>字符串是字符数组</strong></p>\n<p>声明以下变量 <code>s</code>，<code>s</code> 占用连续 14 个长度为 char 的地址</p>\n<p><code>char s[14] = &quot;Hello, world!&quot;;</code></p>\n<p>声明以下变量 <code>p</code>。\n&quot;Hello, world!&quot;为一字符串数组常量，按照约定，其字面量返回其首元素的地址， 即&#39;H&#39;的地址。<br>根据上文，我们声明了一个指针变量 <code>p</code>，其指向的类型为 char。</p>\n<p><code>char *p = &quot;Hello, world!&quot;;</code></p>\n<p>现在，<code>p</code> 是<code>&#39;H&#39;</code>的地址（即 <code>p == &amp;p[0]</code>），通过<code>*p</code> 可访问<code>&#39;H&#39;</code>本身</p>\n<p>我们可以做以下验证</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nint main()\n{\n    char *p = &quot;abc&quot;;\n    printf(&quot;%p %p %p %p %p\\n&quot;, p, &amp;p[0], &amp;p[1], &amp;p[2], &amp;p[3]);\n    char s[4] = &quot;abc&quot;;\n    printf(&quot;%p %p %p %p %p\\n&quot;, s, &amp;s[0], &amp;s[1], &amp;s[2], &amp;s[3]);\n}\n</code></pre>\n<p>输出</p>\n<pre><code>00007ff673193000 00007ff673193000 00007ff673193001 00007ff673193002 00007ff673193003\n000000a1f53ff8a4 000000a1f53ff8a4 000000a1f53ff8a5 000000a1f53ff8a6 000000a1f53ff8a7\n</code></pre>\n<p>证明了 <code>p == &amp;p[0]</code>是正确的</p>\n<p>然而对于字符串（字符数组），数组名返回首字符的地址。<br>使用标准库时，我们通过传入首字符的地址 <code>printf(p)</code> <code>printf(s)</code> 来输出整个字符串</p>\n<p>我们可以做以下验证</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nint main()\n{\n    char *p = &quot;abc&quot;;\n    puts(&amp;p[0]);\n    char s[4] = &quot;abc&quot;;\n    puts(&amp;s[0]);\n}\n</code></pre>\n<p>证明了我们实际使用的就是字符数组第一个元素的地址，因为 <code>puts()</code>及某些 io 函数的某些参数其实就是一个指针<br>实际上函数定义如下<br><code>int puts(const char *_Str)</code><br><code>int printf(const char *__format, ...)</code></p>\n<h1>多级指针</h1>\n<p>多级指针是指针的指针</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nint main()\n{\n    int a = 1;\n    int **p;           //p是地址的地址\n    int *address = &amp;a; //address保存了a的地址\n    p = &amp;address; //p保存了地址变量address的地址\n    printf(&quot;%p %p %p %p&quot;, address, p, *p, **p);\n    printf(&quot;\\n%d&quot;, **p);\n}\n</code></pre>\n<p>输出</p>\n<pre><code>000000fb891ff974 000000fb891ff968 000000fb891ff974 0000024300000001\n1\n</code></pre>\n<p>通过访问 <code>p</code>，<code>*p</code> 得到一个地址，然后访问这个地址<code>**p</code>，得到这个地址指向的值 <code>1</code></p>\n<h1>指针与多维数组</h1>\n<h2>回顾多维数组</h2>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nint main()\n{\n    // 定义二维数组时，必须指明第二维的长度，第一维的长度可以自动推导\n    // 相当于 int arr[2][3]\n    // arr 的类型为 int (*)[3]\n    int arr[][3] = {\n        {1, 2, 3},\n        {4, 5, 6},\n    };\n    // 首尾元素的地址\n    printf(&quot;%p %p\\n&quot;, &amp;arr[0][0], &amp;arr[1][2]);\n    // 若不指明第二维的长度，则默认为一维数组第一个元素，即使溢出\n    printf(&quot;%p %p\\n&quot;, &amp;arr[0][0], &amp;arr[0]);\n    printf(&quot;%p %p\\n&quot;, &amp;arr[1][0], &amp;arr[1]);\n    printf(&quot;%p %p\\n&quot;, &amp;arr[2][0], &amp;arr[2]);\n    printf(&quot;%p %p\\n&quot;, &amp;arr[3][0], &amp;arr[3]);\n    // 首尾地址距离为 5\n    printf(&quot;%d\\n&quot;, &amp;arr[1][2] - &amp;arr[0][0]);\n}\n</code></pre>\n<p>输出</p>\n<pre><code>0000006553fffd00 0000006553fffd14\n0000006553fffd00 0000006553fffd00\n0000006553fffd0c 0000006553fffd0c\n0000006553fffd18 0000006553fffd18\n0000006553fffd24 0000006553fffd24\n5\n</code></pre>\n<h2>sizeof 运算符</h2>\n<p>sizeof 作用于数组时，返回整个数组的字节大小</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nint main()\n{\n    int one[3] = {1, 2, 3};\n    int two[][3] = {\n        {1, 2, 3},\n        {4, 5, 6},\n    };\n    int three[2][2][3] = {\n        {\n            {1, 2, 3},\n            {4, 5, 6},\n        },\n        {\n            {7, 8, 9},\n            {10, 11, 12},\n        },\n    };\n    printf(&quot;sizeof(int)=%d\\n&quot;, sizeof(int));\n    printf(&quot;%d %d %d\\n&quot;, sizeof(one), sizeof(two), sizeof(three));\n    printf(&quot;%d %d %d\\n&quot;, sizeof(one), sizeof(two[0]), sizeof(three[0][0]));\n    printf(&quot;%d %d %d\\n&quot;, sizeof(one[0]), sizeof(two[0][0]), sizeof(three[0][0][0]));\n}\n</code></pre>\n<p>输出</p>\n<pre><code>sizeof(int)=4\n12 24 48\n12 12 12\n4 4 4\n</code></pre>\n<h2>指针访问二维数组</h2>\n<p>多维数组实质是数组的数组。</p>\n<p>可以通过指针访问二维数组，就像用指针访问一维数组一样。</p>\n<p>数组作为函数形参时会退化为指针。</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nvoid printArray(int arr[][3])\n{ // arr 的类型为 int (*)[3]\n    printf(&quot;%d %d\\n&quot;, arr[1][2], *(*(arr + 1) + 2));\n    printf(&quot;%p %p\\n&quot;, &amp;arr[1][2], *(arr + 1) + 2);\n}\nint main()\n{\n    int arr[][3] = {\n        {1, 2, 3},\n        {4, 5, 6},\n    };\n    printArray(arr);\n    // 因为多维数组本质上还是线性存储，所以表面上看是越界，实际上仍然可以访问\n    printf(&quot;%d %d\\n&quot;, arr[2][0], arr[1][3]);\n    printf(&quot;%d %d\\n&quot;, *(*(arr + 2)), *(*(arr + 1) + 3));\n}\n</code></pre>\n<p>输出</p>\n<pre><code>6 6\n00000061fadff6f4 00000061fadff6f4\n16 16\n16 16\n</code></pre>\n<h1>const 修饰指针变量</h1>\n<p>下面两个语句完全等效<br>声明了 int 常量</p>\n<pre><code class=\"language-c\">const int a = 1;\nint const a = 1;\n</code></pre>\n<p>下面两个语句也完全等效<br>声明了指向的值不可变的指针<br>因为是 <code>const *p</code> 形式，<code>*p</code> 被 <code>const</code> 修饰，所以<code>*p</code> 不可变 (但是<code>p</code>可变)</p>\n<pre><code class=\"language-c\">const int* p = 2;\nint const *p = 2;\n</code></pre>\n<p>下面声明了地址不可变的指针<br>因为是 <code>const p</code> 形式，<code>const</code> 修饰 <code>p</code>，所以 <code>p</code> 不可变 (但是<code>*p</code>可变)</p>\n<pre><code class=\"language-c\">int* const p = 2;\n</code></pre>\n<p>下面声明了地址和指向的值都不可变的指针</p>\n<pre><code class=\"language-c\">const int* const p = 2;\n</code></pre>\n<p>注意，指针不能指向 const 定义的常量的地址，因为 const 定义的常量类型带有 const，而不是原类型<br>所以编译不通过<br>例如：</p>\n<pre><code class=\"language-c\">const int n = 2; // n的类型是const int，而不是int\n</code></pre>\n<p>使用强制转换时，编译通过， const 定义的常量可以被修改，这是很危险的操作\n这证明了 const 修饰符修饰的变量仅在编译的防止被修改，但无法保证通过对其地址访问进行修改\n此种情况下使用宏定义常量略有优势</p>\n<pre><code class=\"language-c\">int *p = (int*) &amp;n;\n*p = 2;\nprintf(&quot;%d&quot;, p); // =&gt; 2\n</code></pre>\n<h1>函数名与函数指针</h1>\n<pre><code class=\"language-c\">void print(int a) {\n  printf(&quot;%d\\n&quot;, a);\n}\n\nvoid (*print_ptr)(int) = &amp;print;\n</code></pre>\n<p>注意<code>*</code>的优先级问题，<code>*</code>修饰函数指针</p>\n<p>函数名是指向函数代码的指针，通过函数名可以获取函数地址</p>\n<pre><code class=\"language-c\">// 因此\nprint == &amp;print == *print == print_ptr == *print_ptr;\nprint(10) == (*print)(10) == (&amp;print)(10) == (*print_ptr)(10) == print_ptr(10)\n</code></pre>\n<p>以下代码作为验证</p>\n<pre><code class=\"language-c\">#include &lt;stdio.h&gt;\nvoid print(int a)\n{\n    printf(&quot;%d\\n&quot;, a);\n}\nint main()\n{\n    void (*print_ptr)(int) = &amp;print;\n    printf(&quot;%p %p %p %p %p&quot;, print, &amp;print, *print, print_ptr, *print_ptr);\n}\n</code></pre>\n<p>输出</p>\n<pre><code>00007ff7fcf31591 00007ff7fcf31591 00007ff7fcf31591 00007ff7fcf31591 00007ff7fcf31591\n</code></pre>\n<p>注意，<code>&amp;print_ptr != print_ptr</code>, 因为 <code>print_ptr</code> 是函数指针，而非函数名</p>\n","tags":["c"]},{"id":"cdn","url":"https://yieldray.fun/posts/cdn","title":"实用CDN/镜像","date_published":"2021-08-28T11:00:00.000Z","date_modified":"2021-08-28T11:00:00.000Z","content_text":"<h1>Module CDN</h1>\n<p><a href=\"https://esm.sh/\">https://esm.sh/</a><br><a href=\"https://esm.run/\">https://esm.run/</a> (by JsDelivr)<br><a href=\"https://unpkg.com/\">https://unpkg.com/</a> (?module)<br><a href=\"https://jspm.org/\">https://jspm.org/</a><br><a href=\"https://www.skypack.dev/\">https://www.skypack.dev/</a></p>\n<h1>Deno</h1>\n<p><a href=\"https://deno.land/x/\">https://deno.land/x/</a><br><a href=\"https://nest.land/\">https://nest.land/</a><br><a href=\"https://crux.land/\">https://crux.land/</a><br><a href=\"https://denopkg.com/\">https://denopkg.com/</a><br><del><a href=\"https://denopkg.dev/\">https://denopkg.dev/</a></del><br><a href=\"https://deno.toolforge.org/\">https://deno.toolforge.org/</a><br><a href=\"https://cdn.vfiles.no/\">https://cdn.vfiles.no/</a><br><a href=\"https://ghuc.cc/\">https://ghuc.cc/</a></p>\n<h1>Bundle/Transpile</h1>\n<p><a href=\"https://bundlejs.com/\">https://bundlejs.com/</a></p>\n<ul>\n<li><a href=\"https://deno.bundlejs.com/\">https://deno.bundlejs.com/</a></li>\n<li><a href=\"https://edge.bundlejs.com/\">https://edge.bundlejs.com/</a></li>\n</ul>\n<p><a href=\"https://esbuild.vercel.app/\">https://esbuild.vercel.app/</a><br><a href=\"https://esb.deno.dev/\">https://esb.deno.dev/</a><br><a href=\"https://est.deno.dev/\">https://est.deno.dev/</a><br><a href=\"https://bundle.deno.dev\">https://bundle.deno.dev</a><br><a href=\"https://esb.denoflare.dev/\">https://esb.denoflare.dev/</a><br><a href=\"https://bundle.denoflare.dev\">https://bundle.denoflare.dev</a></p>\n<h1>Polyfill</h1>\n<p><a href=\"https://polyfill-fastly.net/\">https://polyfill-fastly.net/</a> <a href=\"https://polyfill-fastly.io/\">https://polyfill-fastly.io/</a><br>ref: <a href=\"https://community.fastly.com/t/new-options-for-polyfill-io-users/2540\">https://community.fastly.com/t/new-options-for-polyfill-io-users/2540</a></p>\n<p><a href=\"https://cdnjs.cloudflare.com/polyfill\">https://cdnjs.cloudflare.com/polyfill</a><br>ref: <a href=\"https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk/\">https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk/</a></p>\n<p>see also: <a href=\"https://polykill.io/\">https://polykill.io/</a></p>\n<h1>Primary Github</h1>\n<p><a href=\"https://statically.io/\">https://statically.io/</a></p>\n<p><a href=\"https://raw.githack.com/\">https://raw.githack.com/</a></p>\n<p><a href=\"https://rawgit.net/\">https://rawgit.net/</a></p>\n<p><a href=\"https://githubraw.com/\">https://githubraw.com/</a></p>\n<p><a href=\"https://www.sashimi.zip/\">https://www.sashimi.zip/</a></p>\n<p><a href=\"https://gitmirror.com/raw.html\">https://gitmirror.com/raw.html</a><br><a href=\"https://raw.githubusercontents.com/\">https://raw.githubusercontents.com/</a></p>\n<p><a href=\"https://github-proxy.com/\">https://github-proxy.com/</a></p>\n<p><a href=\"https://wget.la/\">https://wget.la/</a></p>\n<h1>Primary Fonts</h1>\n<p><a href=\"https://upset.dev/fonts\">https://upset.dev/fonts</a><br><a href=\"https://fonts.bunny.net/\">https://fonts.bunny.net/</a><br><a href=\"https://font.im/\">https://font.im/</a><br><a href=\"https://fonts.xz.style/\">https://fonts.xz.style/</a><br><a href=\"https://docs.xz.style/fonts\">https://docs.xz.style/fonts</a></p>\n<h1>Other</h1>\n<p><a href=\"https://ejm.sh/\">https://ejm.sh/</a></p>\n<p><a href=\"https://gaac.vercel.app/\">https://gaac.vercel.app/</a></p>\n<h1>Primary NPM</h1>\n<h6>jsDelivr</h6>\n<p>npm/github/wordpress<br>Home <a href=\"https://www.jsdelivr.com/\">https://www.jsdelivr.com/</a></p>\n<p><a href=\"https://cdn.jsdelivr.net/\">https://cdn.jsdelivr.net/</a><br><a href=\"https://fastly.jsdelivr.net/\">https://fastly.jsdelivr.net/</a><br><a href=\"https://gcore.jsdelivr.net/\">https://gcore.jsdelivr.net/</a></p>\n<h6>UNPKG</h6>\n<p>npm<br>Home <a href=\"https://unpkg.com/\">https://unpkg.com/</a>\nLegacy <a href=\"https://npmcdn.com/\">https://npmcdn.com/</a></p>\n<h6>static delivr</h6>\n<p>jsdelivr<br>Home <a href=\"https://staticdelivr.com/\">https://staticdelivr.com/</a></p>\n<h6>cbd.int</h6>\n<p>unpkg proxy<br>Link <a href=\"https://cdn.cbd.int/\">https://cdn.cbd.int/</a></p>\n<h6>ioCDN</h6>\n<p>jsdelivr\nHome <a href=\"https://cdn.iocdn.cc/\">https://cdn.iocdn.cc/</a></p>\n<h6>WebCache</h6>\n<p>Home <a href=\"https://www.webcache.cn/\">https://www.webcache.cn/</a></p>\n<h6>onmicrosoft</h6>\n<p>Home <a href=\"https://cdn.onmicrosoft.cn/\">https://cdn.onmicrosoft.cn/</a></p>\n<h6>jsdeliver proxy</h6>\n<p><a href=\"https://cdn.jsdmirror.com/\">https://cdn.jsdmirror.com/</a></p>\n<p>Home <a href=\"https://github.com/54ayao/Chinajsdelivr\">https://github.com/54ayao/Chinajsdelivr</a></p>\n<p>refs: <a href=\"https://github.com/zkeq/Coding/discussions/644#discussioncomment-4571515\">https://github.com/zkeq/Coding/discussions/644#discussioncomment-4571515</a></p>\n<h2>White List</h2>\n<h6>registry.npmmirror.com</h6>\n<p><a href=\"https://registry.npmmirror.com/%7Bpkg%7D/%7Bversion%7D/files/%7Bpath%7D\">https://registry.npmmirror.com/{pkg}/{version}/files/{path}</a></p>\n<p>See <a href=\"https://zhuanlan.zhihu.com/p/633904268\">https://zhuanlan.zhihu.com/p/633904268</a></p>\n<p>WhiteList <a href=\"https://github.com/cnpm/unpkg-white-list\">https://github.com/cnpm/unpkg-white-list</a></p>\n<h6>知乎</h6>\n<p>unpkg<br>Link <a href=\"https://unpkg.zhimg.com/\">https://unpkg.zhimg.com/</a></p>\n<h6>饿了吗</h6>\n<p>unpkg<br>Link <a href=\"https://npm.elemecdn.com/\">https://npm.elemecdn.com/</a></p>\n<h6>bdStatic(BaiDu)</h6>\n<p>npm<br>Link <a href=\"https://code.bdstatic.com/npm/\">https://code.bdstatic.com/npm/</a><br>Link <a href=\"https://www.v2ex.com/t/521411\">https://www.v2ex.com/t/521411</a></p>\n<h1>Primary CdnJs</h1>\n<h6>cdnjs</h6>\n<p>Home <a href=\"https://cdnjs.com/\">https://cdnjs.com/</a></p>\n<h6>StaticFile(七牛云)</h6>\n<p>cdnjs<br>Home <del><a href=\"https://www.staticfile.org/\">https://www.staticfile.org/</a></del> <a href=\"https://www.staticfile.net/\">https://www.staticfile.net/</a></p>\n<h6>75CDN(360)</h6>\n<p>cdnjs,GoogleFonts<br><del>Home <a href=\"https://cdn.baomitu.com/\">https://cdn.baomitu.com/</a></del></p>\n<h6>字节跳动(今日头条)</h6>\n<p>cdnjs<br><del>Home <a href=\"https://cdn.bytedance.com/\">https://cdn.bytedance.com/</a></del></p>\n<h6>cdnjs.loli.net</h6>\n<p>cdnjs,GoogleFonts,GoogleAjax,Gravatar<br>Doc <a href=\"https://u.sb/css-cdn/\">https://u.sb/css-cdn/</a><br><a href=\"https://cdnjs-jp.loli.net/\">https://cdnjs-jp.loli.net/</a></p>\n<h6>sustech</h6>\n<p>docs <a href=\"https://mirrors.sustech.edu.cn/help/cdnjs.html\">https://mirrors.sustech.edu.cn/help/cdnjs.html</a></p>\n<h6>ZStatic</h6>\n<p><a href=\"https://www.zstatic.net/\">https://www.zstatic.net/</a></p>\n<h6>极客族公共加速服务</h6>\n<p>cdnjs,GoogleFonts,GoogleAjax,Gravatar<br><del>Doc <a href=\"https://cdn.geekzu.org/cached.html\">https://cdn.geekzu.org/cached.html</a></del></p>\n<h6>jsHub</h6>\n<p>cdnjs<br><del>Home <a href=\"https://jshub.com/\">https://jshub.com/</a></del></p>\n<h6>BootCDN</h6>\n<p>cdnjs\n<del>Home <a href=\"https://www.bootcdn.cn/\">https://www.bootcdn.cn/</a></del></p>\n<h6>未闻花名静态资源加速</h6>\n<p><a href=\"https://cdnjs.snrat.com/\">https://cdnjs.snrat.com/</a></p>\n<h6>7ed</h6>\n<p>cdnjs,GoogleFonts,GoogleAjax,Gravatar<br><a href=\"https://www.7ed.net/start/public-cdn.html\">https://www.7ed.net/start/public-cdn.html</a></p>\n<hr>\n<h1>For fun</h1>\n<h6>Google Hosted Libraries(blocked)</h6>\n<p>Doc <a href=\"https://developers.google.com/speed/libraries/\">https://developers.google.com/speed/libraries/</a></p>\n<h6>asp.net</h6>\n<p>jQuery<br>Doc <a href=\"http://www.asp.net/ajax/cdn\">http://www.asp.net/ajax/cdn</a></p>\n<h6>Sina SAE</h6>\n<p>Home <a href=\"https://lib.sinaapp.com/\">https://lib.sinaapp.com/</a></p>\n<h1>Mirrors</h1>\n<p>ref: <a href=\"https://mirrors.quickso.cn/\">https://mirrors.quickso.cn/</a></p>\n<h6>Ali</h6>\n<p><a href=\"https://developer.aliyun.com/mirror/\">https://developer.aliyun.com/mirror/</a></p>\n<h6>Tecent</h6>\n<p><a href=\"https://mirrors.cloud.tencent.com/\">https://mirrors.cloud.tencent.com/</a></p>\n<h6>HuaWei</h6>\n<p><a href=\"https://mirrors.huaweicloud.com/\">https://mirrors.huaweicloud.com/</a></p>\n<h6>Netease</h6>\n<p><a href=\"https://mirrors.163.com/\">https://mirrors.163.com/</a></p>\n<h6><del>pubyun</del></h6>\n<p><del><a href=\"http://mirrors.pinganyun.com/\">http://mirrors.pinganyun.com/</a></del></p>\n<h6>PubYun</h6>\n<p><a href=\"http://mirrors.pubyun.com/\">http://mirrors.pubyun.com/</a></p>\n<h6>yun-idc</h6>\n<p><a href=\"http://mirrors.yun-idc.com/\">http://mirrors.yun-idc.com/</a></p>\n<h6>dotsrc</h6>\n<p><a href=\"https://mirrors.dotsrc.org/\">https://mirrors.dotsrc.org/</a></p>\n<h6>skyshe</h6>\n<p><a href=\"https://mirrors.skyshe.cn/\">https://mirrors.skyshe.cn/</a></p>\n<h6>TWAREN</h6>\n<p><a href=\"http://ftp.twaren.net/\">http://ftp.twaren.net/</a></p>\n<h6>中国科学技术大学</h6>\n<p><a href=\"https://mirrors.ustc.edu.cn/\">https://mirrors.ustc.edu.cn/</a></p>\n<h6>清华大学</h6>\n<p><a href=\"https://mirrors.tuna.tsinghua.edu.cn/\">https://mirrors.tuna.tsinghua.edu.cn/</a></p>\n<h6>北京大学</h6>\n<p><a href=\"https://mirrors.pku.edu.cn/\">https://mirrors.pku.edu.cn/</a></p>\n<h6>浙江大学</h6>\n<p><a href=\"https://mirrors.zju.edu.cn/\">https://mirrors.zju.edu.cn/</a></p>\n<h6>上海交通大学</h6>\n<p><a href=\"https://mirrors.sjtug.sjtu.edu.cn/\">https://mirrors.sjtug.sjtu.edu.cn/</a></p>\n<h6>北京交通大学</h6>\n<p><a href=\"https://mirror.bjtu.edu.cn/\">https://mirror.bjtu.edu.cn/</a></p>\n<h6>北京理工大学</h6>\n<p><a href=\"https://mirrors.bit.edu.cn/\">https://mirrors.bit.edu.cn/</a></p>\n<h6>北京外国语大学</h6>\n<p><a href=\"https://mirrors.bfsu.edu.cn/\">https://mirrors.bfsu.edu.cn/</a></p>\n<h6>同济大学</h6>\n<p><a href=\"https://mirrors.tongji.edu.cn/\">https://mirrors.tongji.edu.cn/</a></p>\n<h6>南方科技大学</h6>\n<p><a href=\"https://mirrors.bit.edu.cn/\">https://mirrors.bit.edu.cn/</a></p>\n<h6>兰州大学</h6>\n<p><a href=\"https://mirror.lzu.edu.cn/\">https://mirror.lzu.edu.cn/</a></p>\n<h6>东北大学</h6>\n<p><a href=\"http://mirror.neu.edu.cn/\">http://mirror.neu.edu.cn/</a></p>\n<h6>哈尔滨工业大学</h6>\n<p><a href=\"https://mirrors.hit.edu.cn/\">https://mirrors.hit.edu.cn/</a></p>\n<h6>大连理工大学</h6>\n<p><a href=\"http://mirror.dlut.edu.cn/\">http://mirror.dlut.edu.cn/</a></p>\n<h6>山东大学</h6>\n<p><a href=\"https://mirrors.sdu.edu.cn/\">https://mirrors.sdu.edu.cn/</a></p>\n<h6>重庆大学</h6>\n<p><a href=\"https://mirrors.cqu.edu.cn/\">https://mirrors.cqu.edu.cn/</a></p>\n<h6>重庆邮电大学</h6>\n<p><a href=\"http://mirrors.cqupt.edu.cn/\">http://mirrors.cqupt.edu.cn/</a></p>\n<h6>云南大学</h6>\n<p><a href=\"https://mirrors.ynu.edu.cn/index/\">https://mirrors.ynu.edu.cn/index/</a></p>\n<h6>南京大学</h6>\n<p><a href=\"http://mirrors.nju.edu.cn/\">http://mirrors.nju.edu.cn/</a></p>\n<h6>西北农林科技大学</h6>\n<p><a href=\"https://mirrors.nwsuaf.edu.cn/\">https://mirrors.nwsuaf.edu.cn/</a></p>\n<h6>大连东软信息学院</h6>\n<p><a href=\"http://mirrors.neusoft.edu.cn/\">http://mirrors.neusoft.edu.cn/</a></p>\n","tags":["note","cdn"]},{"id":"github","url":"https://yieldray.fun/posts/github","title":"Github镜像","date_published":"2021-08-28T11:00:00.000Z","date_modified":"2021-08-28T11:00:00.000Z","content_text":"<h1>Web</h1>\n<p><a href=\"https://github.ink/\">https://github.ink/</a><br><a href=\"https://kkgithub.com/\">https://kkgithub.com/</a> (<a href=\"https://help.kkgithub.com/\">help</a>)<br><a href=\"https://hub.nuaa.cf/\">https://hub.nuaa.cf/</a><br><a href=\"https://hub.yzuu.cf/\">https://hub.yzuu.cf/</a></p>\n<h1>DL Proxy</h1>\n<p><a href=\"https://mirror.ghproxy.com/\">https://mirror.ghproxy.com/</a><br><a href=\"https://ghps.cc/\">https://ghps.cc/</a><br><a href=\"https://toolwa.com/github/\">https://toolwa.com/github/</a><br><a href=\"https://github.zhlh6.cn/\">https://github.zhlh6.cn/</a><br><a href=\"https://gitclone.com/\">https://gitclone.com/</a><br><a href=\"https://www.gitmirror.com/files.html\">https://www.gitmirror.com/files.html</a><br><a href=\"https://gh.ddlc.top/\">https://gh.ddlc.top/</a><br><a href=\"https://gh.api.99988866.xyz/\">https://gh.api.99988866.xyz/</a><br>Source Code: <a href=\"https://github.com/hunshcn/gh-proxy\">Github 文件加速</a></p>\n<h1>Useful Tools</h1>\n<p><a href=\"https://github.com/dotnetcore/FastGithub\">https://github.com/dotnetcore/FastGithub</a></p>\n","tags":["mirror","github"]},{"id":"google","url":"https://yieldray.fun/posts/google","title":"Google镜像Collection","date_published":"2021-08-28T11:00:00.000Z","date_modified":"2023-10-11T18:00:00.000Z","content_text":"<p><em>Tips:请安全、合法地使用镜像. 建议不要在镜像站上登录账户，也不要搜索敏感词汇.</em></p>\n<h1>google</h1>\n<p><a href=\"https://g.damfu.com/\">https://g.damfu.com/</a></p>\n<h1><strong>集合</strong></h1>\n<p><a href=\"https://github.com/librarycloud/list\">https://github.com/librarycloud/list</a></p>\n<p><a href=\"https://github.com/Heroic-Studio/Google-Mirrors\">https://github.com/Heroic-Studio/Google-Mirrors</a></p>\n<p><a href=\"https://wangchujiang.com/google/\">https://wangchujiang.com/google/</a></p>\n<p><a href=\"https://kfd.me/\">https://kfd.me/</a></p>\n<h1><strong>学术搜索</strong></h1>\n<p><a href=\"https://c3.glgoo.top/scholar\">https://c3.glgoo.top/scholar</a>(AD)</p>\n<p><a href=\"https://gg.xueshu5.com/\">https://gg.xueshu5.com/</a>(非原版)</p>\n<p><a href=\"http://www.hlhmf.com/\">http://www.hlhmf.com/</a>(集合)</p>\n<p><a href=\"http://ac.scmor.com/\">http://ac.scmor.com/</a>(集合)</p>\n<h1>其他</h1>\n<p><a href=\"https://github.com/gfw-breaker/nogfw/blob/master/README.md\">https://github.com/gfw-breaker/nogfw/blob/master/README.md</a></p>\n<p><a href=\"https://neeva.com/\">https://neeva.com/</a></p>\n","tags":["mirror"]},{"id":"migrate","url":"https://yieldray.fun/posts/migrate","title":"博客迁移","date_published":"2021-08-28T11:00:00.000Z","date_modified":"2021-08-28T11:00:00.000Z","content_text":"<p>从 typecho 迁移到 hexo 了</p>\n<p>仅做备份 <a href=\"https://typecho.naoh.eu.org/\">Crazy 白茫茫</a></p>\n","tags":["about"]},{"id":"pixiv","url":"https://yieldray.fun/posts/pixiv","title":"Pixiv 相关","date_published":"2021-08-28T11:00:00.000Z","date_modified":"2021-08-28T11:00:00.000Z","content_text":"<h2>一篇详细的文章。。</h2>\n<p><a href=\"https://2heng.xin/2017/09/19/pixiv/\">https://2heng.xin/2017/09/19/pixiv/</a></p>\n<h2>Pixeval(Windows;dotnet)</h2>\n<blockquote>\n<p>Wow. Yet another Pixiv client!</p>\n</blockquote>\n<p><a href=\"https://github.com/Pixeval/Pixeval\">https://github.com/Pixeval/Pixeval</a></p>\n<h2>PixivBiu(Windows,Mac;python)</h2>\n<blockquote>\n<p>一款 Pixiv 搜索辅助工具</p>\n</blockquote>\n<p><a href=\"https://biu.tls.moe/\">https://biu.tls.moe/</a></p>\n<h2>PixivNow(web;vue,Node.js)</h2>\n<blockquote>\n<p>Provide Pixiv backend proxy &amp; frontend service based on serverless technology</p>\n</blockquote>\n<p><a href=\"https://pixiv.js.org/\">https://pixiv.js.org/</a></p>\n<h2>pixiv.moe(web;react,redux)</h2>\n<blockquote>\n<p>A pinterest-style layout site, shows illusts on pixiv.net order by popularity.</p>\n</blockquote>\n<p><a href=\"https://pixiv.moe/\">https://pixiv.moe/</a></p>\n<h2>Pixiv Illustration Collection (web,Java)</h2>\n<blockquote>\n<p>提供有限的 pixiv 排行查看与免费高级会员(热门排序)搜索的站点</p>\n</blockquote>\n<p><a href=\"https://pixivic.com/\">https://pixivic.com/</a><br><a href=\"https://github.com/OysterQAQ/Pixiv-Illustration-Collection\">https://github.com/OysterQAQ/Pixiv-Illustration-Collection</a></p>\n<h2>pixiv-viewer(web;vue)</h2>\n<blockquote>\n<p>又一个 Pixiv 阅览工具</p>\n</blockquote>\n<p><a href=\"https://lab.getloli.com/pixiv-viewer/\">https://lab.getloli.com/pixiv-viewer/</a><br><a href=\"https://github.com/journey-ad/pixiv-viewer\">https://github.com/journey-ad/pixiv-viewer</a></p>\n<h2>Pixivel(web)</h2>\n<p><a href=\"https://pixivel.moe/\">https://pixivel.moe/</a></p>\n<h2>vilipix(web)</h2>\n<p><a href=\"https://www.vilipix.com/\">https://www.vilipix.com/</a></p>\n<h2>acg-pixiv</h2>\n<p><a href=\"https://www.acg-pixiv.com/\">https://www.acg-pixiv.com/</a></p>\n<h2>APIs</h2>\n<p><a href=\"https://github.com/upbit/pixivpy\">https://github.com/upbit/pixivpy</a><br><a href=\"https://github.com/alphasp/pixiv-api-client\">https://github.com/alphasp/pixiv-api-client</a><br><a href=\"https://github.com/akameco/pixiv-app-api\">https://github.com/akameco/pixiv-app-api</a></p>\n<h2>图片代理(web)</h2>\n<p><a href=\"https://pixiv.cat/\">https://pixiv.cat/</a></p>\n<h2>Pivision(Android4.4+)</h2>\n<p><a href=\"https://github.com/mouyase/PivisionM\">https://github.com/mouyase/PivisionM</a><br><a href=\"https://www.himiku.com/archives/pivision.html\">https://www.himiku.com/archives/pivision.html</a></p>\n<h2>Shaft(Android5+;Java)</h2>\n<blockquote>\n<p>Pixiv 第三方 Android 客户端</p>\n</blockquote>\n<p><a href=\"https://github.com/CeuiLiSA/Pixiv-Shaft\">https://github.com/CeuiLiSA/Pixiv-Shaft</a></p>\n<h2>Pix-EzViewer(Android5+;Java)</h2>\n<blockquote>\n<p>一个支持免代理直连及查看动图的第三方 Pixiv Android 客户端。</p>\n</blockquote>\n<p><a href=\"https://github.com/Notsfsssf/Pix-EzViewer\">https://github.com/Notsfsssf/Pix-EzViewer</a></p>\n<h2>pixez-flutter(Android;Flutter)</h2>\n<blockquote>\n<p>一个支持免代理直连及查看动图的第三方 Pixiv flutter 客户端\n<a href=\"https://github.com/Notsfsssf/pixez-flutter\">https://github.com/Notsfsssf/pixez-flutter</a></p>\n</blockquote>\n<h2>PxView(React Native)</h2>\n<blockquote>\n<p>Unofficial Pixiv app client for Android and iOS, built in React Native.</p>\n</blockquote>\n<p><a href=\"https://github.com/alphasp/pxview\">https://github.com/alphasp/pxview</a></p>\n<h2>Pxder(Node.js)</h2>\n<blockquote>\n<p>Download illusts from pixiv.net P 站插画批量下载器</p>\n</blockquote>\n<p><a href=\"https://github.com/Tsuk1ko/pxder\">https://github.com/Tsuk1ko/pxder</a></p>\n<h2>P 站助手(IOS,Android5+)</h2>\n<p><a href=\"http://pixivlite.com/\">http://pixivlite.com/</a></p>\n<h2>pxvr(Android5+)</h2>\n<p><a href=\"https://pxvr.xyz/\">https://pxvr.xyz/</a></p>\n<h2>Pan(Android5+,Windows)</h2>\n<p><a href=\"https://pan.st/\">https://pan.st/</a></p>\n","tags":["note"]}]}