<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Alicia Lan</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://believed-breadfruit.top/</id>
  <link href="https://believed-breadfruit.top/" rel="alternate"/>
  <link href="https://believed-breadfruit.top/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Alicia Lan</rights>
  <subtitle>Professional Dumbass · System 2 Thinker · Inner Stoic</subtitle>
  <title>Atypical</title>
  <updated>2026-07-26T09:59:45.445Z</updated>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Thinking" scheme="https://believed-breadfruit.top/categories/Thinking/"/>
    <category term="自我认知" scheme="https://believed-breadfruit.top/tags/%E8%87%AA%E6%88%91%E8%AE%A4%E7%9F%A5/"/>
    <category term="思考" scheme="https://believed-breadfruit.top/tags/%E6%80%9D%E8%80%83/"/>
    <category term="身份" scheme="https://believed-breadfruit.top/tags/%E8%BA%AB%E4%BB%BD/"/>
    <content>
      <![CDATA[<p>今天了解到「同一性资本」理论。看完后，我有一种比较强烈的新鲜感和顿悟感，因为它提供了一个我以前没有使用过的角度。</p><p>过去谈到理想和现实，通常有三种立场。</p><p>第一种是站在现实这一边。这种观点认为，普通人的容错率很低，生存和收入才是最重要的。兴趣、理想和个性都是资源充足以后才有资格考虑的东西。先选择就业好、收入高、风险低的道路，才是对自己负责。</p><p>第二种是站在理想这一边。这种人不认同现实中的功利、竞争和庸俗，更愿意保护自己的精神世界。但与此同时，他又会时刻感受到现实的威胁，担心理想被侵蚀，担心自己最终不得不屈服。</p><p>这两种观点表面上相反，实际上共享同一个前提：理想和现实是互相对立的，只能选择其中一边。</p><p>第三种观点看起来更成熟一些。它认为既不能完全理想主义，也不能完全现实主义，而应该具体情况具体分析，在现实的基础上保留一部分理想。不同阶段可以采用不同的比例，例如七分现实、三分理想，或者三分现实、七分理想。</p><p>我也停留在这种理解里（去年我是第二种）。我会觉得，理想和现实之间不存在统一答案，重点是根据自己的资源、环境和风险承受能力不断调整。这个策略式组合的判断看起来比简单地站队更合理，但它仍然把理想和现实看成了两种需要相互妥协的力量，而且更重要的是，这种妥协每次都要重新计算，天然具有认知决策负担。</p><hr><p>同一性资本理论提供的是完全不同的角度。它不再问：「理想和现实各应该占多少？」它问的是：「一个人围绕『我是谁』所做的选择，最终积累了什么？」</p><p>同一性资本分成两类。</p><p>一类是有形的同一性资本，例如经济资源、学历证书、社会关系、职业网络和家庭背景。</p><p>另一类是无形的同一性资本，例如认知复杂度、内控感、元认知能力、批判性思维和道德推理能力。这些能力并不专属于某一个职业，而是可以迁移到不同环境中。</p><p>这个理论最重要的地方，是它改变了我理解「现实」的方式。</p><p>通常所说的现实，往往只包括可以立即看见的外部回报：工资、专业热度、学历、岗位数量和社会评价。</p><p>但同一性资本理论认为，一个人内部形成的能力、动机和自我理解，同样属于现实资源。它们不是装饰，也不只是精神安慰，而是一个人在环境发生变化时，能够依靠的资本。</p><hr><p>前段时间，有个很火的概念叫做「奥德赛时期」，通常指 20 岁到 35 岁之间，也就是从大学毕业到进入稳定工作、组建家庭之前的这段时间。你会有种漂泊感和迷茫感，在事业、情感和自我认同上不断试错和探索，就像在海上航行，不知道下一个港口在哪里。</p><p>传统社会中，一个人的身份很大程度上由家庭、社区和职业结构直接赋予。出生在什么家庭，通常就会沿着相对确定的道路生活。在这样的社会里，个人自由较少，但「我是谁」也没有那么难回答。</p><p>现代社会完全不一样了。原有身份路径越来越不稳定，专业可能贬值，行业可能收缩，过去有效的技能也可能因为技术变化而迅速失去价值。一个人越来越难依靠某个固定身份度过一生，只能不断重新选择和组织自己。</p><p>在这种情况下，只积累某种具体技能是很难度过奥德赛时期的。具体技能当然重要，但它通常依赖特定环境。掌握一种编程语言、一项工程技术或者一个热门专业，可能在某个阶段具有很高价值，但不能保证这种价值长期存在。</p><p>无形的同一性资本则更加基础。它决定一个人能不能理解新的环境，能不能判断外部建议是否适合自己，能不能在原有路径失效以后重新学习，也决定一个人在没有明确答案的时候，能不能继续行动。</p><hr><p>很多人会认为「普通家庭的孩子容错率低，所以更应该放弃理想」，但是用同一性资本的理论来看，这是恰恰相反的。</p><p>普通人的外部资源确实更少，这个前提没有错。</p><p>但正因为缺乏家庭兜底，一个人才更需要发展那些不会轻易被外部环境夺走的东西。理想不保证成功，但它代表的兴趣和内在动机可能是一个人在遇到困难、学习成本增加或者行业下行以后，仍然愿意继续理解问题和调整方向的原因。</p><p>假如一个人从很早开始就只根据外部回报做选择，他可能会获得一条短期内更安全的路径，却没有练习过怎样从自己的内部作出判断。一旦原本承诺的回报消失，他不仅失去了外部动力，也很难从自身重新产生新的方向。</p><p>因此，普通家庭的孩子如果被要求过早放弃兴趣，选择当下最热门的专业，他放弃的可能恰好是自己少数不依赖家庭资源的竞争力：内在动机。</p><hr><p>坚持理想并不必然意味着无视现实。恰恰相反，真正地坚持理想，前提是最理解现实。</p><p>理想和现实不是一场比例分配。兴趣、价值判断和内在动机，本身也可以构成一种现实能力；学历、收入和职业技能也不只是外部工具，它们同样可以参与一个人的身份建构。</p><p>这就是同一性资本让我有启发的地方。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/07/26/2026-07-26-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%99%AE%E9%80%9A%E4%BA%BA%E6%9B%B4%E5%BA%94%E8%AF%A5%E5%9D%9A%E6%8C%81%E7%90%86%E6%83%B3/</id>
    <link href="https://believed-breadfruit.top/2026/07/26/2026-07-26-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%99%AE%E9%80%9A%E4%BA%BA%E6%9B%B4%E5%BA%94%E8%AF%A5%E5%9D%9A%E6%8C%81%E7%90%86%E6%83%B3/"/>
    <published>2026-07-25T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>今天了解到「同一性资本」理论。看完后，我有一种比较强烈的新鲜感和顿悟感，因为它提供了一个我以前没有使用过的角度。</p>
<p>过去谈到理想和现实，通常有三种立场。</p>
<p>第一种是站在现实这一边。这种观点认为，普通人的容错率很低，生存和收入才是最重要的。兴趣、理想和]]>
    </summary>
    <title>为什么普通人更应该坚持理想？</title>
    <updated>2026-07-26T09:59:45.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Thinking" scheme="https://believed-breadfruit.top/categories/Thinking/"/>
    <category term="自我认知" scheme="https://believed-breadfruit.top/tags/%E8%87%AA%E6%88%91%E8%AE%A4%E7%9F%A5/"/>
    <category term="思考" scheme="https://believed-breadfruit.top/tags/%E6%80%9D%E8%80%83/"/>
    <content>
      <![CDATA[<p>看到一个讲投资的视频，里面有一句话让我挺有共鸣。</p><p>他说，真正可怕的不是亏钱，而是人的阈值会被改写。见过太大的收益之后，就很难再接受正常、缓慢的赚钱方式。</p><p>我没有投资经历，但我突然发现，我去年好像经历过一种类似的事情，只不过不是投资，而是思考。</p><p>去年有一段时间，我真的想得太多了，思考密度极大，而且抽象层次越来越高，这是一种认知上异常高刺激的状态。</p><hr><p>今天去南山博物馆的时候，看着深圳以前发展的历史，我就在想，如果我是那个时代的人，会怎么样？</p><p>如果我出生在那里，可能就是一个采牡蛎的人，每天干着这样的活。我会觉得挺惨的，因为很多事情不是自己喜欢不喜欢的问题，而是根本没有得选。从出生开始，你所处的时代、接触的人、生活的环境，很多东西就已经决定了人生的大方向。</p><p>但同样是在那个时代，有的人可能出生在战争里，从小颠沛流离；有的人却出生在富裕家庭，无忧无虑地长大。同一个时间，同一个地球，居然有人过着完全不一样的人生，而这些都不是自己能选择的。</p><p>后来我意识到，这种痛苦可能来自两个原因。</p><hr><p>第一个原因，是现在的信息太发达了。</p><p>我们可以一边过着自己简简单单、三点一线的生活，一边不断看到世界另一端完全不同的人生。今天看到美国常春藤毕业的人离职去硅谷创业，明天看到非洲的孩子吃不饱饭，后天又看到几百年前某个人的一生。我们的认知可以很丝滑地在不同国家、不同阶层、不同历史时期之间不断切换。</p><p>第二个原因，我本身喜欢思考，因为这件事情给我带来很多正反馈。但是，我到了一种极端，思考抽象层次越来越高，毫无边界，认知枯竭后甚至一度觉得很虚无。</p><p>当习惯了这种不断跳跃、不断抽象之后，那些真实生活里的摩擦，反而会被忽略。明明真正的生活就是这些具体的小事，但因为大脑已经习惯了站在很高的视角去思考，再回到现实的时候，就会觉得现实特别有摩擦。</p><p>比如读一本书需要慢慢读，学一个东西需要一点点积累，甚至蒸个蛋失败一次，都会觉得特别受打击。</p><hr><p>现在回头看，我觉得这和那个投资视频讲的是同一件事。</p><p>投资会把一个人的收益阈值拉高，而不断停留在抽象世界里，也会慢慢把一个人的认知阈值拉高。</p><p>阈值一旦被改写，再回到普通的现实生活，就需要重新学会接受那些缓慢、具体、充满摩擦的过程，而这个过程必定会非常痛苦。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/07/24/2026-07-24-%E5%BD%93%E4%B8%80%E4%B8%AA%E4%BA%BA%E7%9A%84%E8%AE%A4%E7%9F%A5%E9%98%88%E5%80%BC%E8%A2%AB%E6%8B%89%E9%AB%98%E5%90%8E/</id>
    <link href="https://believed-breadfruit.top/2026/07/24/2026-07-24-%E5%BD%93%E4%B8%80%E4%B8%AA%E4%BA%BA%E7%9A%84%E8%AE%A4%E7%9F%A5%E9%98%88%E5%80%BC%E8%A2%AB%E6%8B%89%E9%AB%98%E5%90%8E/"/>
    <published>2026-07-24T07:06:15.000Z</published>
    <summary>
      <![CDATA[<p>看到一个讲投资的视频，里面有一句话让我挺有共鸣。</p>
<p>他说，真正可怕的不是亏钱，而是人的阈值会被改写。见过太大的收益之后，就很难再接受正常、缓慢的赚钱方式。</p>
<p>我没有投资经历，但我突然发现，我去年好像经历过一种类似的事情，只不过不是投资，而是思考。</]]>
    </summary>
    <title>当一个人的认知阈值被拉高后</title>
    <updated>2026-07-24T15:07:17.582Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Thinking" scheme="https://believed-breadfruit.top/categories/Thinking/"/>
    <category term="自我认知" scheme="https://believed-breadfruit.top/tags/%E8%87%AA%E6%88%91%E8%AE%A4%E7%9F%A5/"/>
    <category term="思考" scheme="https://believed-breadfruit.top/tags/%E6%80%9D%E8%80%83/"/>
    <category term="系统论" scheme="https://believed-breadfruit.top/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BA/"/>
    <content>
      <![CDATA[<p>今天和姐姐视频聊天时，发现自己在对话中像一个心理辅导师：当对方陷入情绪时，会引导她切换到更理性的角度；当对方过度理性时，又会提醒她看到感性和情绪的一面。</p><p>这个过程让我意识到，自己常用的策略是“对症下药”，也就是当一个人偏向某一端时，帮助她看到另一端，以恢复某种平衡。</p><p>但进一步思考后，我开始怀疑“平衡一定是好的”这个默认假设。</p><p>过去我会把解决问题理解为恢复平衡：遇到冲突，就用对立面去调节它；遇到情绪，就用理性去处理它；遇到过度理性，又补充情绪视角。但如果平衡已经达成，冲突消失之后，我反而不知道下一步该做什么。</p><hr><p>和 AI 聊了一通，它指出了一个很有意思的点：</p><blockquote><p>如果想要成长，就不应该要平衡，而是梯度。</p></blockquote><p>举个例子。一个创业者，如果一直保持心理平衡，他可能不会创业。一个科学家，如果一直保持平衡，他可能不会花十年研究一个问题。一个艺术家，如果一直平衡，也可能不会产生那些极端投入的作品。</p><p>也就是说：<strong>稳定系统需要平衡，而成长系统需要梯度。</strong></p><p>我希望我的人生是不断往上增长的，所以我不该只是用策略保持平衡。当我今天写下这篇思考，这意味着我已经开始从平衡阶段向成长阶段过渡。</p><hr><p>下一个点，就是得主动创造想成长的方向：是否有一个方向，我愿意长期投入精力去做？</p><p>我想了下自己感兴趣的东西，其实不是某个固定领域，而是类似《硅谷》（Silicon Valley）里那种荒诞但可建模的问题：“10 分钟给 800 人……”也就是在约束条件下如何提高效率、如何建立模型、如何逼近最优解。</p><p>这种兴趣可以出现在光学、AI、人的思维、组织系统、职业选择等多个场景中。</p><p>也就是说，我的底层兴趣可能是复杂系统建模。但是经过一年多的高思考、低行动，我很清楚抽象的局限性，所以长期做的事情肯定还是得有一个具体现实的载体。</p><p>事实上，我没有任何答案。但是好在，我知道行动是方向的解药。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/07/05/2026-07-05-%E6%80%80%E7%96%91%E5%B9%B3%E8%A1%A1%E4%B8%80%E5%AE%9A%E6%98%AF%E5%A5%BD%E7%9A%84%E8%BF%99%E4%B8%AA%E5%81%87%E8%AE%BE/</id>
    <link href="https://believed-breadfruit.top/2026/07/05/2026-07-05-%E6%80%80%E7%96%91%E5%B9%B3%E8%A1%A1%E4%B8%80%E5%AE%9A%E6%98%AF%E5%A5%BD%E7%9A%84%E8%BF%99%E4%B8%AA%E5%81%87%E8%AE%BE/"/>
    <published>2026-07-04T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>今天和姐姐视频聊天时，发现自己在对话中像一个心理辅导师：当对方陷入情绪时，会引导她切换到更理性的角度；当对方过度理性时，又会提醒她看到感性和情绪的一面。</p>
<p>这个过程让我意识到，自己常用的策略是“对症下药”，也就是当一个人偏向某一端时，帮助她看到另一端，以恢复某种]]>
    </summary>
    <title>怀疑『平衡一定是好的』这个假设</title>
    <updated>2026-07-24T15:15:04.845Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="AI" scheme="https://believed-breadfruit.top/tags/AI/"/>
    <category term="工程实践" scheme="https://believed-breadfruit.top/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<p>最近看到一篇综述兼评论风格的文章（<a href="https://mp.weixin.qq.com/s/bk3-aSyTGsx_hEo-vt5KfQ">原文链接</a>）。起因是 Anthropic 新发布了 Dynamic Workflows，作者借此回顾了从 2022 年到 2025&#x2F;2026 年这几年间 Agent 技术的研究脉络。</p><p>文章的核心观点可以总结为一句话：Agent 能力的提升，并不只是靠模型本身更强，而是把那些不适合塞进上下文窗口的东西，挪到了 runtime、代码、技能库和工作流脚本里。</p><p>这篇文章让我对工程实践产生了三点新的思考。</p><p>一、工程的高可用，来自确定性</p><p>什么是确定性？相同的输入，系统总能产生相同的输出。</p><p>在计算机工程里，确定性有：计算确定（数学运算、排序、解析）和逻辑确定（if&#x2F;else、布尔判断）。</p><p>所以在 Agent 技术里，代码做了确定的事情（计算、执行、可重复运行），大模型则做”模糊”的事（制定计划、解释概念、拆解意图）；人做”决策”的事（定目标、做取舍）。</p><p>二、厉害的技术，未必新颖</p><p>还有另一个感悟，是关于 Agent 这一路的发展历程。</p><p>提示词工程、思维链（CoT）、ReAct 推理、Function Calling、工作流……回头看，这些概念在底层原理上其实都谈不上多么”高明”。</p><p>这让我意识到一个长期存在的误区：我们总觉得，真正厉害的东西一定得足够新颖。</p><p>所以，当一项技术的底层实现并不新颖时，很多人会下意识地说：”这不就跟那个谁谁差不多吗？”——这种评价背后的潜台词是：一项技术要够牛，就必须够新。</p><p>但现实恰恰相反。技术演进路上的每一步，其提出的观念往往本身都算不上惊艳。它们只是”恰好有用”，并且被足够多的人验证过、确实能跑出好结果而已。</p><p>三、为什么技术总是”沿着第一条路”走下去</p><p>再延伸一点。假设技术演进是一条 A1 → A2 → A3 → A4 不断向前的路径。</p><p>但其实在 A1 这个节点上，原本可能同时存在三条岔路：A1、B1、C1。</p><p>只是因为 A1 最先出现、最先被大多数人验证，又恰好反响热烈，于是大家会迅速围绕 A1 去做周边迭代、做配套基建。结果就是——整个技术生态都顺着 A1 这条路一路长了下去，而不是从 B1 或 C1 出发。</p><p>所以技术的演进方向，很多时候并不是因为 A1 一定最优，而是因为它”先到”，先吃到了验证和生态的红利。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/06/05/2026-06-05-%E5%AF%B9%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5%E6%9C%89%E4%BA%86%E6%96%B0%E8%AE%A4%E8%AF%86/</id>
    <link href="https://believed-breadfruit.top/2026/06/05/2026-06-05-%E5%AF%B9%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5%E6%9C%89%E4%BA%86%E6%96%B0%E8%AE%A4%E8%AF%86/"/>
    <published>2026-06-04T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>最近看到一篇综述兼评论风格的文章（<a href="https://mp.weixin.qq.com/s/bk3-aSyTGsx_hEo-vt5KfQ">原文链接</a>）。起因是 Anthropic 新发布了 Dynamic Workflows，作者借此回顾了从 2022]]>
    </summary>
    <title>对工程实践有了新认识</title>
    <updated>2026-06-04T17:03:35.324Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="AI" scheme="https://believed-breadfruit.top/tags/AI/"/>
    <category term="技术洞察" scheme="https://believed-breadfruit.top/tags/%E6%8A%80%E6%9C%AF%E6%B4%9E%E5%AF%9F/"/>
    <content>
      <![CDATA[<p><a href="https://aiweekly.co/learning-ai/artificial-intelligence/ai-weekly-index">AI Weekly Index</a>，它用一份报表呈现了 2015–2026 年的 AI 话语趋势。</p><p>其中有一个观察很有意思：这两年，扩散模型、RAG、Transformer 这些概念几乎从讨论中消失了。但它们消失，并不是因为失败，而是因为它们已经沉淀成了一种”不可见的基础设施”。</p><p>这句话可以从三个层面来理解。</p><p>一、为什么”消失”不等于”失败”</p><p>我们之所以会反复谈论一项新技术，往往是因为它还停留在”需要被解释、需要被证明”的阶段。</p><p>Transformer 在 2022 年被高频提及（0.44 次&#x2F;期），正是因为人们还在争论它的潜力；而到了 2026 年，提及次数归零，恰恰是因为它已经成为所有主流大模型的底层架构。它不再需要被讨论，因为它已经被默认了。</p><p>这就像今天没有人会特意说”我用互联网来发邮件”：互联网早已融入一切，反而不再作为一个独立话题存在。扩散模型和 RAG 走的是同一条路：从”前沿概念”变成”标准工具”，于是话语自然向更上层迁移。</p><p>二、”不可见的基础设施”意味着什么</p><p>报表里的数据把这个模式呈现得很清楚：机器学习在 2021 年达到峰值（3.72 次&#x2F;期），到 2026 年降为 0；深度学习晚两年，但走出了同样的曲线；扩散模型只”存活”了两年（2022–2023）便淡出了讨论。</p><p>但它们并没有退出舞台，恰恰相反，它们渗透进了每一个产品、每一篇论文、每一次对话的底层。</p><p>当一项技术不再需要被特意命名，就说明它已经从”创新”变成了”常识”，从”需要被选择”变成了”无需选择”。这正是技术走向成熟的最强信号。</p><p>三、”消失得最快 &#x3D; 最成功”的深层逻辑</p><p>这句话真正精妙的地方，在于它翻转了我们判断”成功”的标准。</p><p>我们通常用热度、讨论量、搜索指数来衡量一项技术的影响力。但这份数据揭示了相反的规律：真正改变行业的技术，往往是最快从话语中退场的那一批。原因很简单：当所有人都已经在用它，就没人再需要谈论它了。</p><p>反过来看，那些长期停留在讨论阶段的概念（比如”伦理”在 2019–2022 年持续高热），往往意味着行业还没有形成共识、找到解法，问题仍然悬而未决。</p><p>到这里，这让我想起 skills 刚出来的时候，很多文章把它和 MCP 拿来对比，断言”MCP 被推翻了”。</p><p>我当时专门调研过，发现 MCP 不仅没有退场，反而沉到了更底层，渗透进了 agent 技术的基础之中。</p><p>skills 和 MCP 本就是两个不同维度的东西，很多人把它们放在一起比较，其实是不了解二者的本质：skills 是一套稳定的工作流，MCP 是一种通信协议。</p><p>它们唯一可比的角度，是对上下文的影响：skills 按需加载，对上下文友好；MCP 偏向全量加载，更容易让上下文膨胀。除此之外，二者并不在同一条赛道上。</p><p>每当一个新概念出现，人们总是习惯性地把它和最近一个”看起来相关”的旧概念做替代式对比，而不是去理解它们各自在技术栈中所处的位置。</p><p>身处技术栈内部的人，能感知到各层之间的相对位置和依赖关系；而身处外部的人，只能看到话语层的表面，哪个词上了热搜，哪个词消失了，就据此判断谁赢谁输。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/06/03/2026-06-03-%E6%8A%80%E6%9C%AF%E6%B6%88%E5%A4%B1%E7%9A%84%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/</id>
    <link href="https://believed-breadfruit.top/2026/06/03/2026-06-03-%E6%8A%80%E6%9C%AF%E6%B6%88%E5%A4%B1%E7%9A%84%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/"/>
    <published>2026-06-02T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p><a href="https://aiweekly.co/learning-ai/artificial-intelligence/ai-weekly-index">AI Weekly Index</a>，它用一份报表呈现了 2015–2026 年的 AI 话语趋势。</p>]]>
    </summary>
    <title>这两年扩散模型、RAG、Transformer 这些概念几乎从讨论中消失了</title>
    <updated>2026-06-04T16:24:08.223Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="技术" scheme="https://believed-breadfruit.top/tags/%E6%8A%80%E6%9C%AF/"/>
    <category term="Claude Code" scheme="https://believed-breadfruit.top/tags/Claude-Code/"/>
    <content>
      <![CDATA[<p>Claude Code 的过程观测有几个层次，从轻量到重型都有，下面分层介绍。</p><hr><h2 id="一、交互模式下的即时查看"><a href="#一、交互模式下的即时查看" class="headerlink" title="一、交互模式下的即时查看"></a>一、交互模式下的即时查看</h2><p><strong><code>--debug</code> 标志</strong> — 最直接的方式</p><p>启动时加上 <code>--debug</code> 可以看到 API 调用、工具调用、执行耗时等详细信息，输出到 stderr：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">claude --debug                          <span class="comment"># 完整 debug 输出</span></span><br><span class="line">claude --debug <span class="string">&quot;api,hooks&quot;</span>              <span class="comment"># 只看特定类别</span></span><br><span class="line">claude --debug-file /tmp/claude-debug.log  <span class="comment"># 写入文件，保持终端整洁</span></span><br></pre></td></tr></table></figure><p><strong><code>--verbose</code></strong> 则是更轻量的开关，可在配置里持久化，或启动时传入。</p><hr><h2 id="二、读取本地-JSONL-日志（离线回溯）"><a href="#二、读取本地-JSONL-日志（离线回溯）" class="headerlink" title="二、读取本地 JSONL 日志（离线回溯）"></a>二、读取本地 JSONL 日志（离线回溯）</h2><p>Claude Code 的 session transcript 以 JSONL 格式存储在 <code>~/.claude/projects/</code> 目录下，里面记录了每次 tool call、thinking step、token 用量等内容。原始文件可读性较差（是 escaped JSON），但可以直接 <code>cat</code> 或用 <code>jq</code> 解析。</p><p>社区工具 <strong><a href="https://claude-dev.tools/">claude-devtools</a></strong> 可以解析这些文件，它会重建完整的 session transcript —— 每个 tool call 展开、thinking step 可见、token 用量按 turn 分解，subagent 执行以树状结构渲染。</p><hr><h2 id="三、通过-Hooks-实时拦截（适合自定义观测）"><a href="#三、通过-Hooks-实时拦截（适合自定义观测）" class="headerlink" title="三、通过 Hooks 实时拦截（适合自定义观测）"></a>三、通过 Hooks 实时拦截（适合自定义观测）</h2><p>Hooks 是 Claude Code 的扩展点，可以在 <code>PreToolUse</code>、<code>PostToolUse</code> 等生命周期事件上挂载脚本。这是目前<strong>最灵活</strong>的方案——你可以在 hook 脚本里把 tool call 的输入&#x2F;输出写到自己的日志系统，或触发告警。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">// .claude/settings.json</span><br><span class="line">&#123;</span><br><span class="line">  &quot;hooks&quot;: &#123;</span><br><span class="line">    &quot;PostToolUse&quot;: [&#123;</span><br><span class="line">      &quot;matcher&quot;: &quot;*&quot;,</span><br><span class="line">      &quot;hooks&quot;: [&#123;</span><br><span class="line">        &quot;type&quot;: &quot;command&quot;,</span><br><span class="line">        &quot;command&quot;: &quot;python3 /path/to/logger.py&quot;</span><br><span class="line">      &#125;]</span><br><span class="line">    &#125;]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Hook 脚本通过 stdin 收到 JSON 事件，包含 tool 名称、参数、结果等。</p><hr><h2 id="四、OpenTelemetry-导出（生产-团队级别）"><a href="#四、OpenTelemetry-导出（生产-团队级别）" class="headerlink" title="四、OpenTelemetry 导出（生产&#x2F;团队级别）"></a>四、OpenTelemetry 导出（生产&#x2F;团队级别）</h2><p>Claude Code CLI 内置了 OpenTelemetry instrumentation：它会在每次 model request 和 tool execution 上记录 span，发出 token&#x2F;cost 计数的 metrics，并为 prompt 和 tool result 发出结构化 log event。</p><p>开启方式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> CLAUDE_CODE_ENABLE_TELEMETRY=1</span><br><span class="line"><span class="built_in">export</span> CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1   <span class="comment"># 开启 Traces（beta）</span></span><br><span class="line"><span class="built_in">export</span> OTEL_TRACES_EXPORTER=otlp</span><br><span class="line"><span class="built_in">export</span> OTEL_LOGS_EXPORTER=otlp</span><br><span class="line"><span class="built_in">export</span> OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318</span><br></pre></td></tr></table></figure><p>支持导出到任何接受 OTLP 协议的后端，比如 Honeycomb、Datadog、Grafana、Langfuse，或自托管的 collector。</p><hr><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><table><thead><tr><th>需求</th><th>方案</th></tr></thead><tbody><tr><td>临时调试某次执行</td><td><code>claude --debug</code></td></tr><tr><td>事后回溯 session</td><td>读 <code>~/.claude/projects/*.jsonl</code> 或用 claude-devtools</td></tr><tr><td>自定义日志&#x2F;告警</td><td>Hooks + 脚本</td></tr><tr><td>团队监控 &#x2F; 成本追踪</td><td>OpenTelemetry 导出</td></tr></tbody></table>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/06/01/2026-06-01-claude-code%E8%BF%87%E7%A8%8B%E8%A7%82%E6%B5%8B/</id>
    <link href="https://believed-breadfruit.top/2026/06/01/2026-06-01-claude-code%E8%BF%87%E7%A8%8B%E8%A7%82%E6%B5%8B/"/>
    <published>2026-06-01T00:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Claude Code 的过程观测有几个层次，从轻量到重型都有，下面分层介绍。</p>
<hr>
<h2 id="一、交互模式下的即时查看"><a href="#一、交互模式下的即时查看" class="headerlink" title="一、交互模式下的即时查看"></]]>
    </summary>
    <title>Claude Code 过程观测的几种方式</title>
    <updated>2026-05-31T16:16:24.100Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="哲学" scheme="https://believed-breadfruit.top/tags/%E5%93%B2%E5%AD%A6/"/>
    <category term="思考" scheme="https://believed-breadfruit.top/tags/%E6%80%9D%E8%80%83/"/>
    <content>
      <![CDATA[<p>斯多葛哲学（Stoicism）起源于古希腊、在古罗马发展成熟，代表人物包括 Zeno of Citium、Epictetus、Seneca 和 Marcus Aurelius。很多人以为斯多葛就是”忍耐””压抑情绪”，但那只是很小的一部分——它本质上是一套关于如何生活的哲学体系。浓缩成几个核心思想，大致如下。</p><ol><li>区分可控与不可控</li></ol><p>Epictetus 在《手册》开篇就指出：”有些事情取决于我们，有些事情不取决于我们。”取决于我们的，是判断、选择、价值观和行动；不取决于我们的，则是天气、他人评价、财富、身体健康的很多方面、社会环境，以及最终结果。</p><p>斯多葛认为，痛苦往往来自于试图控制无法控制的东西——希望所有人喜欢自己、希望永远不失败、希望事情完全按计划发展，这些目标本身就与现实冲突。因此他们主张把精力集中于行动而非结果，这与现代心理学中的”过程导向”很接近。</p><ol start="2"><li>德性是唯一真正的善</li></ol><p>这是斯多葛最容易被现代人忽略的部分。他们认为财富、地位、健康、名声都不是人生最高目标，真正重要的是 智慧（Wisdom）、勇气（Courage）、正义（Justice）、节制（Temperance）。一个人贫穷但正直，依然是成功的人；一个人富有却卑劣，依然是失败的人。幸福建立在品格上，而不是建立在运气上。</p><ol start="3"><li>情绪来自判断</li></ol><p>斯多葛并不认为外界直接产生情绪，而是 事件 → 判断 → 情绪。同样是”失业”，解释成”人生完了”会带来焦虑和绝望，解释成”一次新的机会”则带来平静和积极行动。事件相同、情绪不同，区别完全来自认知解释——这也是后来 CBT（认知行为疗法）的重要思想来源。</p><ol start="4"><li>接受现实（Amor Fati）</li></ol><p>斯多葛强调不要与已经发生的现实对抗，Nietzsche 后来把这种思想概括为 Amor Fati（热爱命运）。这不是消极认命，而是承认现实已经发生、与现实争吵毫无意义，应当把精力放在”接下来我能做什么”上。面对考试失败、项目延期、被人拒绝，斯多葛不会否认痛苦的存在，只是认为抱怨无法改变事实，行动才可能改变未来。</p><ol start="5"><li>死亡意识（Memento Mori）</li></ol><p>斯多葛经常提醒自己”人终将死亡”，这听起来悲观，目的却恰恰相反：当意识到生命有限时，网络争吵、面子问题、一时得失这些烦恼都会缩小。他们借助死亡意识来校准人生优先级。</p><ol start="6"><li>负面预演（Premeditatio Malorum）</li></ol><p>主动去想象失业、失败、被拒绝、贫穷，目的不是吓自己，而是降低对不确定性的恐惧。很多焦虑来自”我无法接受最坏情况”，但当真正面对最坏情况时，人往往发现自己能够承受，这会显著提高心理韧性。</p><ol start="7"><li>人是共同体动物</li></ol><p>斯多葛并不主张独来独往。Marcus Aurelius 写道：”人生来就是为了彼此合作。”他们认为每个人都是更大整体的一部分，因此应当善待他人、履行责任、贡献社会——这与纯粹的个人主义并不相同。</p><hr><p>一句话总结：接受无法改变的现实，专注于能够控制的行动，并通过智慧、勇气、正义和节制塑造自己的品格。</p><p>斯多葛哲学最有价值的部分可能不是”坚强”，而是那个持续追问：<strong>“这件事究竟是现实本身让我痛苦，还是我对现实的解释让我痛苦？”</strong> 这个问题，正是斯多葛哲学和认知行为疗法的交汇点。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/05/30/2026-05-30-%E6%96%AF%E5%A4%9A%E8%91%9B%E5%93%B2%E5%AD%A6%E5%BE%88%E5%A4%9A%E7%90%86%E5%BF%B5%E5%92%8C%E6%88%91%E7%B1%BB%E4%BC%BC/</id>
    <link href="https://believed-breadfruit.top/2026/05/30/2026-05-30-%E6%96%AF%E5%A4%9A%E8%91%9B%E5%93%B2%E5%AD%A6%E5%BE%88%E5%A4%9A%E7%90%86%E5%BF%B5%E5%92%8C%E6%88%91%E7%B1%BB%E4%BC%BC/"/>
    <published>2026-05-29T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>斯多葛哲学（Stoicism）起源于古希腊、在古罗马发展成熟，代表人物包括 Zeno of Citium、Epictetus、Seneca 和 Marcus Aurelius。很多人以为斯多葛就是”忍耐””压抑情绪”，但那只是很小的一部分——它本质上是一套关于如何生活的哲学]]>
    </summary>
    <title>斯多葛哲学很多理念和我类似</title>
    <updated>2026-06-04T17:05:28.774Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Thinking" scheme="https://believed-breadfruit.top/categories/Thinking/"/>
    <category term="自我认知" scheme="https://believed-breadfruit.top/tags/%E8%87%AA%E6%88%91%E8%AE%A4%E7%9F%A5/"/>
    <category term="自律" scheme="https://believed-breadfruit.top/tags/%E8%87%AA%E5%BE%8B/"/>
    <category term="精力管理" scheme="https://believed-breadfruit.top/tags/%E7%B2%BE%E5%8A%9B%E7%AE%A1%E7%90%86/"/>
    <content>
      <![CDATA[<p>这周上班用脑过度，周五下班时已经累到连吃饭都觉得是负担，那天晚上很早就睡了，其他事一概不想干。周六索性休息了一整天；到了周天，整个人才重新「get my shit together」——有精力健身，也愿意坐下来复盘了。</p><p>这个小小的循环让我意识到一件事：能不能做事，本质上取决于精力，而不是意志力。休息好了，思考自然涌出来、想法自然冒出来，做事的欲望也跟着回来；可一旦精力被榨干，再有道理的事也推不动。顺着这个观察，我对「自律」又有了一些新的理解。</p><hr><p>去年我得出过一个结论：所谓自律，其实并不需要刻意强迫自己。当一件事变成你当下「最有可能、也最合适」做的选项，去做它就是顺理成章的事，根本谈不上什么自律。那时候我的方法论也很简单：不依赖意志力，而是设计环境，让目标行为成为大脑唯一可选项。顺着本能走，而不是顶着本能上。</p><p>今年我发现，「自然而然做事」这种状态其实不是凭空发生的。它有两个前提：身体已经恢复，以及饮食、休息、睡眠都到位。只有这些条件齐备，人才会自动滑入下一种状态；如果地基不在，再去硬撑着做事——确实也能做——那大概就是大家口中真正的「自律」吧。</p><hr><p>小时候被灌输的「自律」是另一套叙事：克服意志力、突破生理极限，运动员带伤跑完比赛、学生周末还在埋头做题。后来我发现，那些被鼓励去「自律」的事——学习、思考、锻炼——在生活足够单调、选项有限的时候，本来就会自然发生，根本不需要咬牙坚持。</p><p>这是去年的我看到的一面。</p><p>今年又看到了另一面：工作日精力被榨干之后，这些事真的推不动；可一旦周末休息够了，它们又会自发回来。这至少说明两点：</p><ol><li><p>学习、思考、锻炼本身就是高消耗的事。你不想做，不一定是懒、不一定是不自律，更可能是身体资源已经见底。</p></li><li><p>身体就像一块电池。低电量时确实可以再逼自己一把，但这种透支不可持续。与其执着于「再坚持一次」，不如想清楚：这一次硬撑能换来什么？如果回报有限，那休息才是更划算的选择。</p></li></ol><hr><p>这些东西表面上是个人感受，但其实背后都能找到一些更底层的逻辑。</p><p><strong>第一件事是，意志力本身就是一种有限的生理资源。</strong> 心理学里有个说法叫「自我损耗」（Ego Depletion）——Roy Baumeister 那一批研究的结论是，自控力像肌肉一样，高强度的脑力劳动、决策、情绪控制，都在持续抽取同一个能量池。能量耗尽后，人会自然倾向于阻力最小的路径，此时再去迫自己做难事，失败率高得离谱。所以「精力被榨干之后什么也不想做」，不是性格问题，是生理问题。</p><p><strong>第二件事是，真正高绩效的人管理的不是时间，而是精力。</strong> 小时候作文里那个「带伤跑完比赛的运动员」，在现代职业体育里是被严厉禁止的——那种「坚持」会毁掉运动员的整个职业生涯。</p><p>《哈佛商业评论》那篇被反复引用的 <em>The Making of a Corporate Athlete</em>，Jim Loehr 和 Tony Schwartz 说得很直白：时间是固定的，精力才是可以扩展、可以恢复的资源。顶级运动员的训练模式从来不是「持续发力」，而是压力与恢复有节奏地交替；职场上所谓的「企业运动员」也一样——不是撑着硬干，而是刻意留出恢复周期。长期只消耗不恢复，表面上还在运转，实际上决策质量、创造力和情绪稳定性都在悄悄滑坡。</p><p>所以所谓的「咬牙坚持」，本质上是在透支核心储备。如果不计投资回报，仅仅为了「感动自己」或者「看起来比别人自律」而硬撑，那不叫自律，叫自我消耗。</p><p><strong>第三件事更伤感一点，是我们被教会的「坚持」叙事，本来就是为了学校这个场景设计的。</strong> 哲学家 James P. Carse 把世间的游戏分成两种：有限游戏和无限游戏。学校是典型的有限游戏——规则固定、玩家固定（同班同学）、目标唯一（分数），且常常是零和博弈。为了在这场有限游戏里胜出，「吃苦」和「盲目坚持」自然就被括刷成了筛选机制。但社会是一场无限游戏，那套逻辑拿出来就完全不成立了。</p><p>如果社会是一场多维度的无限游戏，那么真正值得问的不是「我周末要不要逼自己学习」，而是：<strong>我该如何利用周末和精力，构建适合自己的「多维竞争优势」？</strong></p><hr><p>顺着这个问题，我比较认可三条思路。</p><p><strong>1. 从「一万小时」转向「技能叠加」</strong></p><p>学校要求你在单一学科上做到前 1%，但在社会这场无限游戏里，更聪明的策略是在两到三个领域都做到前 25%，再把它们组合起来。Marc Andreessen 说过一句话，我记很清楚：除了极少数天才，普通人获得超额回报最可靠的方式，就是同时掌握两种「组合起来很值钱」的技能。</p><p>这意味着，精力充沛的周末，不该再去死磕本职工作中边际收益递减的技能。可以挑一个完全不同、但能与现有能力发生化学反应的领域去探索——比如程序员去学设计心理学。</p><p><strong>2. 用「杠铃策略」分配精力</strong></p><p>既然身体是一块电池，那周末的精力分配就不该平均用力。Nassim Taleb 在《反脆弱》里讲过一个我觉得很好用的思路：把资源推向两个极端，避开中间地带。落到周末上是这样的：</p><p>约 <strong>80%</strong> 的精力投入「绝对恢复」。周五晚上的彻底摆烂、充足睡眠、毫无压力的散步、优质饮食。这一端守住基本盘。</p><p>约 <strong>20%</strong> 的精力投入「激进探索」。在周六下午或周日上午精力顶峰的 2–3 小时，去做有长期复利的事：写一篇东西、研究一个新工具、做一次深度训练。</p><p>砍掉中间地带。那些既不能让你彻底放松、也不能让你真正成长的活动（漫无目的刷短视频、无效社交），是最不划算的投入。</p><p><strong>3. 重新看待「休闲」</strong></p><p>休闲不是生产力的对立面。Cal Newport 讲「深度游戏」（Deep Play）时说得很准：高质量的休闲并不是对生产力的浪费，反而是维持高水平深度工作的前提。</p><p>对我来说，这意味着周末除了「纯休息」，还可以主动去找那种让人愿意投入、甚至能带来能量回流的活动：需要专注的运动（球类）、一门手艺（做饭）、沉浸式的阅读。它们也消耗体力，但不榨干心智带宽，反而能清理工作日积下的认知垃圾。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/05/24/2026-05-24-%E6%88%91%E7%9A%84%E8%87%AA%E5%BE%8B%E8%A7%82-2-0/</id>
    <link href="https://believed-breadfruit.top/2026/05/24/2026-05-24-%E6%88%91%E7%9A%84%E8%87%AA%E5%BE%8B%E8%A7%82-2-0/"/>
    <published>2026-05-24T08:30:00.000Z</published>
    <summary>
      <![CDATA[<p>这周上班用脑过度，周五下班时已经累到连吃饭都觉得是负担，那天晚上很早就睡了，其他事一概不想干。周六索性休息了一整天；到了周天，整个人才重新「get my shit together」——有精力健身，也愿意坐下来复盘了。</p>
<p>这个小小的循环让我意识到一件事：能不能做]]>
    </summary>
    <title>我的自律观 2.0</title>
    <updated>2026-06-04T17:08:35.374Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="技术" scheme="https://believed-breadfruit.top/tags/%E6%8A%80%E6%9C%AF/"/>
    <category term="JavaScript" scheme="https://believed-breadfruit.top/tags/JavaScript/"/>
    <category term="Promise" scheme="https://believed-breadfruit.top/tags/Promise/"/>
    <category term="Deferred Pattern" scheme="https://believed-breadfruit.top/tags/Deferred-Pattern/"/>
    <category term="Vue" scheme="https://believed-breadfruit.top/tags/Vue/"/>
    <content>
      <![CDATA[<blockquote><p>本文从一段 Vue 项目中「防抖 + Promise resolve 外挂」的弹窗代码出发，分析它为什么别扭、背后想实现的 Deferred Pattern 究竟是什么，以及在现代 JavaScript 里更干净的写法。</p></blockquote><h2 id="一、问题代码"><a href="#一、问题代码" class="headerlink" title="一、问题代码"></a>一、问题代码</h2><p>先看引发讨论的这段代码：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> debounceOpenRecomfirmPopup = <span class="title function_">debounce</span>(</span><br><span class="line"><span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="comment">// 记录 Promise resolve，等待关闭回调传回 actionCode</span></span><br><span class="line">recomfirmPopupPromiseResolve.<span class="property">value</span> = resolve <span class="keyword">as</span> (<span class="attr">actionCode</span>: <span class="string">&#x27;confirm&#x27;</span> | <span class="string">&#x27;cancel&#x27;</span> | <span class="string">&#x27;close&#x27;</span>) =&gt; <span class="built_in">void</span></span><br><span class="line"><span class="comment">// 打开二次确认弹窗</span></span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line">&#125;,</span><br><span class="line"><span class="number">300</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 利息换超市卡二次确认弹窗：打开</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;<span class="string">&#x27;confirm&#x27;</span> | <span class="string">&#x27;cancel&#x27;</span> | <span class="string">&#x27;close&#x27;</span>&gt;(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="title function_">debounceOpenRecomfirmPopup</span>(resolve)</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>第一眼看上去会有几个疑问：</p><ul><li>防抖的回调里再去记录 Promise 的 <code>resolve</code>，打开和完成是断开的，<strong>顺序怎么保证？</strong></li><li>这是某种「高级稳定」的写法吗？</li><li>我是不是应该照着学？</li></ul><p>直觉是对的——<strong>这不是值得照搬的高级模式，而是一个把两个机制硬耦合在一起的 workaround。</strong></p><hr><h2 id="二、为什么这段代码别扭"><a href="#二、为什么这段代码别扭" class="headerlink" title="二、为什么这段代码别扭"></a>二、为什么这段代码别扭</h2><h3 id="2-1-完整的执行链路"><a href="#2-1-完整的执行链路" class="headerlink" title="2.1 完整的执行链路"></a>2.1 完整的执行链路</h3><ol><li>调用 <code>openRecomfirmPopupAsync()</code>：创建 Promise，把 <code>resolve</code> 传给 <code>debounceOpenRecomfirmPopup</code>。</li><li>防抖 300ms 后才执行：把 <code>resolve</code> 存到 <code>recomfirmPopupPromiseResolve.value</code>，并打开弹窗。</li><li>弹窗关闭：<code>handleRecomfirmClose</code> 调用存起来的 <code>resolve(actionCode)</code>，Promise 才算完成。</li></ol><h3 id="2-2-顺序隐患：debounce-会吞掉-resolve"><a href="#2-2-顺序隐患：debounce-会吞掉-resolve" class="headerlink" title="2.2 顺序隐患：debounce 会吞掉 resolve"></a>2.2 顺序隐患：debounce 会吞掉 resolve</h3><p>如果用户快速连续触发两次 <code>openRecomfirmPopupAsync()</code>：</p><ul><li>第一次的 <code>resolve</code> 被防抖<strong>吞掉了</strong>（不执行），但对应的 Promise 已经挂起在等待。</li><li>防抖只执行最后一次回调，第二次的 <code>resolve</code> 覆盖了 <code>recomfirmPopupPromiseResolve.value</code>。</li><li><strong>第一次的 Promise 永远不会被 resolve —— 直接挂死。</strong></li></ul><blockquote><p><strong>核心问题</strong>：防抖的语义是「只保留最后一次调用」，而 Promise 的语义是「每次调用都必须有结果」。把 <code>resolve</code> 丢进 debounce，相当于允许「Promise 完成事件」被静默丢弃。</p></blockquote><h3 id="2-3-「resolve-外挂到-ref」本身也不稳"><a href="#2-3-「resolve-外挂到-ref」本身也不稳" class="headerlink" title="2.3 「resolve 外挂到 ref」本身也不稳"></a>2.3 「resolve 外挂到 ref」本身也不稳</h3><ul><li>依赖一个全局 ref 做中转，任何地方意外清空就会挂死。</li><li>和 Vue 响应式系统混用，调试困难。</li><li>弹窗如果走了非预期路径（组件销毁、路由跳转、异常），<code>resolve</code> 永远不会被调用，<strong>Promise 泄漏</strong>。</li></ul><hr><h2 id="三、它想做的事：Deferred-Pattern"><a href="#三、它想做的事：Deferred-Pattern" class="headerlink" title="三、它想做的事：Deferred Pattern"></a>三、它想做的事：Deferred Pattern</h2><p>抛开实现，这段代码的实质意图非常简单：</p><blockquote><p>弹窗打开 → 等用户操作 → 拿到结果继续。</p></blockquote><p>业务代码希望能这样写：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一气呵成，逻辑连贯</span></span><br><span class="line"><span class="keyword">const</span> actionCode = <span class="keyword">await</span> <span class="title function_">openRecomfirmPopupAsync</span>()</span><br><span class="line"><span class="keyword">if</span> (actionCode === <span class="string">&#x27;cancel&#x27;</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"><span class="comment">// 继续下单...</span></span><br></pre></td></tr></table></figure><p>但弹窗本身是异步的：</p><ol><li><code>isShowRecomfirmPopup = true</code> 打开它；</li><li>用户看到弹窗，点击「确认 &#x2F; 取消」；</li><li>Vue 组件 emit <code>close</code> 事件，传回 <code>actionCode</code>。</li></ol><p><strong>步骤 2 是不可预测的时间间隔</strong>——你不可能在打开弹窗后同步拿到结果。这正是 Promise 的用武之地。</p><blockquote><p><strong>Promise 的本质</strong>：把「未来才会发生的结果」变成一个现在就能持有的对象。</p></blockquote><p>这种「把异步操作和它的完成控制权解耦」的模式，就是 <strong>Deferred Pattern（延迟对象模式）</strong>。</p><hr><h2 id="四、Deferred-Pattern-详解"><a href="#四、Deferred-Pattern-详解" class="headerlink" title="四、Deferred Pattern 详解"></a>四、Deferred Pattern 详解</h2><h3 id="4-1-Promise-vs-Deferred"><a href="#4-1-Promise-vs-Deferred" class="headerlink" title="4.1 Promise vs. Deferred"></a>4.1 Promise vs. Deferred</h3><table><thead><tr><th>对比维度</th><th>Promise</th><th>Deferred</th></tr></thead><tbody><tr><td>关注点</td><td>异步操作的<strong>结果</strong></td><td>异步操作<strong>本身及其控制权</strong></td></tr><tr><td>类比</td><td>取餐票，只能等出餐</td><td>厨师手里的底单，决定何时出餐</td></tr><tr><td>谁能完成它</td><td>构造函数内部的逻辑</td><td>外部任意代码（持有 resolve &#x2F; reject 的人）</td></tr></tbody></table><h3 id="4-2-基础实现"><a href="#4-2-基础实现" class="headerlink" title="4.2 基础实现"></a>4.2 基础实现</h3><p>在现代 JavaScript 里，可以用一个工具函数构造 Deferred：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> createDeferred&lt;T&gt;() &#123;</span><br><span class="line"><span class="keyword">let</span> resolve!: <span class="function">(<span class="params">value: T</span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"><span class="keyword">let</span> reject!: <span class="function">(<span class="params">reason?: <span class="built_in">unknown</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;T&gt;(<span class="function">(<span class="params">res, rej</span>) =&gt;</span> &#123;</span><br><span class="line">resolve = res</span><br><span class="line">reject = rej</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> &#123; promise, resolve, reject &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要点：</p><ul><li><code>resolve / reject</code> 在构造函数中被「偷出来」赋值给外部变量。</li><li>返回一个三件套：<code>promise</code> 给消费方 await，<code>resolve / reject</code> 给控制方调用。</li></ul><h3 id="4-3-适用场景"><a href="#4-3-适用场景" class="headerlink" title="4.3 适用场景"></a>4.3 适用场景</h3><ul><li><strong>解耦异步逻辑</strong>：开始和结束不在同一个作用域里（事件监听、Socket 消息、弹窗回调等）。</li><li><strong>封装回调式 API</strong>：旧接口不便直接塞进 <code>new Promise</code> 构造函数时，用 Deferred 当外挂。</li><li><strong>跨函数共享 resolve</strong>：多个回调函数协同决定一个 Promise 的命运。</li></ul><hr><h2 id="五、现代标准：Promise-withResolvers"><a href="#五、现代标准：Promise-withResolvers" class="headerlink" title="五、现代标准：Promise.withResolvers()"></a>五、现代标准：<code>Promise.withResolvers()</code></h2><p>ECMAScript 2024 (ES15) 已经把这个模式标准化了，直接内置：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; promise, resolve, reject &#125; = <span class="title class_">Promise</span>.<span class="property">withResolvers</span>&lt;<span class="built_in">string</span>&gt;()</span><br><span class="line"></span><br><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line"><span class="title function_">resolve</span>(<span class="string">&#x27;任务完成！&#x27;</span>)</span><br><span class="line">&#125;, <span class="number">2000</span>)</span><br><span class="line"></span><br><span class="line">promise.<span class="title function_">then</span>(<span class="variable language_">console</span>.<span class="property">log</span>)</span><br></pre></td></tr></table></figure><p>以后写 Deferred 不需要再手撸 <code>createDeferred</code>，直接用原生 API 即可。</p><hr><h2 id="六、为什么-Promise-一开始是-pending"><a href="#六、为什么-Promise-一开始是-pending" class="headerlink" title="六、为什么 Promise 一开始是 pending"></a>六、为什么 Promise 一开始是 pending</h2><p>回到最初的疑问：</p><blockquote><p>点击下单要打开二次弹窗时，<code>openRecomfirmPopupAsync</code> 返回的 Promise 里也没立刻 <code>resolve</code> 啊，它一直在 pending —— 这是怎么工作的？</p></blockquote><p>答案是：<strong><code>resolve</code></strong> <strong>是一个普通函数对象，可以被传递、存储、延后调用。Promise 不需要在创建时就完成。</strong></p><h3 id="6-1-执行时间线"><a href="#6-1-执行时间线" class="headerlink" title="6.1 执行时间线"></a>6.1 执行时间线</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">时间 0ms ─── 业务代码调用 openRecomfirmPopupAsync()</span><br><span class="line">│</span><br><span class="line">│  new Promise(resolve =&gt; debounceOpenRecomfirmPopup(resolve))</span><br><span class="line">│  → resolve 被当作参数传进 debounce，但 debounce 不立即执行</span><br><span class="line">│  → 此时 Promise 状态：pending，resolve 函数&quot;悬空&quot;</span><br><span class="line">│  → await 暂停，业务代码停在这里等待</span><br><span class="line">│</span><br><span class="line">时间 300ms ─── debounce 延迟结束，执行回调</span><br><span class="line">│</span><br><span class="line">│  recomfirmPopupPromiseResolve.value = resolve   ← resolve 被存起来</span><br><span class="line">│  isShowRecomfirmPopup.value = true              ← 弹窗打开</span><br><span class="line">│</span><br><span class="line">│  → Promise 状态：仍然是 pending</span><br><span class="line">│  → 但现在 resolve 有了&quot;持有者&quot;（ref），未来可以被调用</span><br><span class="line">│</span><br><span class="line">时间 ?s ─── 用户在弹窗里点了确认 / 取消 / 关闭</span><br><span class="line">│</span><br><span class="line">│  handleRecomfirmClose(&#123; actionCode &#125;)</span><br><span class="line">│  recomfirmPopupPromiseResolve.value?.(actionCode) ← resolve 被调用</span><br><span class="line">│  recomfirmPopupPromiseResolve.value = null         ← 清理</span><br><span class="line">│</span><br><span class="line">│  → Promise 状态：pending → fulfilled，值 = actionCode</span><br><span class="line">│  → await 恢复执行，业务代码拿到 &#x27;confirm&#x27; / &#x27;cancel&#x27; / &#x27;close&#x27;</span><br></pre></td></tr></table></figure><h3 id="6-2-类比：事件监听"><a href="#6-2-类比：事件监听" class="headerlink" title="6.2 类比：事件监听"></a>6.2 类比：事件监听</h3><p>这就像你给 <code>addEventListener</code> 注册了一个回调：事件没触发时回调当然不会执行，但回调已经「挂上」了。Promise 的 <code>resolve</code> 也是同理：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="comment">// resolve 是一个函数，可以像任何变量一样被传递</span></span><br><span class="line"><span class="comment">// 传给 debounce、存到 ref、传给另一个函数——都可以</span></span><br><span class="line"><span class="comment">// 只要最终有人调用 resolve(值)，Promise 就会完成</span></span><br><span class="line"><span class="title function_">debounceOpenRecomfirmPopup</span>(resolve)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><strong>Deferred Pattern 的核心</strong>就是：把「何时完成」的决定权从 Promise 内部移到了外部。</p><ul><li>普通 Promise 是<strong>内部自治</strong>的（网络请求成功就 resolve）。</li><li>Deferred 是<strong>外部控制</strong>的（弹窗关闭才 resolve）。</li></ul><hr><h2 id="七、更干净的写法"><a href="#七、更干净的写法" class="headerlink" title="七、更干净的写法"></a>七、更干净的写法</h2><p>回到最初那段代码，改造方向有两个原则：</p><ol><li><strong>防双击应该在 UI 层做（按钮点击节流），不要和 Promise 机制纠缠。</strong></li><li><strong><code>resolve</code></strong>** 不要散落在 ref 里，尽量保持在同一个闭包内管理。**</li></ol><h3 id="方案-A：用-Deferred-工具函数封装"><a href="#方案-A：用-Deferred-工具函数封装" class="headerlink" title="方案 A：用 Deferred 工具函数封装"></a>方案 A：用 Deferred 工具函数封装</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> createDeferred&lt;T&gt;() &#123;</span><br><span class="line"><span class="keyword">let</span> resolve!: <span class="function">(<span class="params">value: T</span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"><span class="keyword">let</span> reject!: <span class="function">(<span class="params">reason?: <span class="built_in">unknown</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"><span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;T&gt;(<span class="function">(<span class="params">r, j</span>) =&gt;</span> &#123; resolve = r; reject = j &#125;)</span><br><span class="line"><span class="keyword">return</span> &#123; promise, resolve, reject &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 弹窗使用</span></span><br><span class="line"><span class="keyword">let</span> <span class="attr">currentDeferred</span>: <span class="title class_">ReturnType</span>&lt;<span class="keyword">typeof</span> createDeferred&lt;<span class="title class_">ActionCode</span>&gt;&gt; | <span class="literal">null</span> = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">currentDeferred = createDeferred&lt;<span class="title class_">ActionCode</span>&gt;()</span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line"><span class="keyword">return</span> currentDeferred.<span class="property">promise</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleRecomfirmClose</span> = (<span class="params">actionCode: ActionCode</span>) =&gt; &#123;</span><br><span class="line">currentDeferred?.<span class="title function_">resolve</span>(actionCode)</span><br><span class="line">currentDeferred = <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="方案-B：直接用-Promise-withResolvers"><a href="#方案-B：直接用-Promise-withResolvers" class="headerlink" title="方案 B：直接用 Promise.withResolvers()"></a>方案 B：直接用 <code>Promise.withResolvers()</code></h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="attr">pending</span>: <span class="title class_">ReturnType</span>&lt;<span class="keyword">typeof</span> <span class="title class_">Promise</span>.<span class="property">withResolvers</span>&lt;<span class="title class_">ActionCode</span>&gt;&gt; | <span class="literal">null</span> = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">pending = <span class="title class_">Promise</span>.<span class="property">withResolvers</span>&lt;<span class="title class_">ActionCode</span>&gt;()</span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line"><span class="keyword">return</span> pending.<span class="property">promise</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleRecomfirmClose</span> = (<span class="params">actionCode: ActionCode</span>) =&gt; &#123;</span><br><span class="line">pending?.<span class="title function_">resolve</span>(actionCode)</span><br><span class="line">pending = <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="方案-C：组件内部用-watchOnce-自动收尾"><a href="#方案-C：组件内部用-watchOnce-自动收尾" class="headerlink" title="方案 C：组件内部用 watchOnce 自动收尾"></a>方案 C：组件内部用 <code>watchOnce</code> 自动收尾</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;<span class="title class_">ActionCode</span>&gt;(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line"><span class="title function_">watchOnce</span>(isShowRecomfirmPopup, <span class="function">(<span class="params">val</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="keyword">if</span> (!val) <span class="title function_">resolve</span>(lastActionCode.<span class="property">value</span>)</span><br><span class="line">&#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>三种写法的共同点：<strong>逻辑是线性的，不会丢 resolve，也没有防抖和 Promise 的耦合。</strong></p><hr><h2 id="八、总结"><a href="#八、总结" class="headerlink" title="八、总结"></a>八、总结</h2><blockquote><p><strong>值得学的</strong>：Deferred Pattern 的思路 —— Promise 可以拆开，手动控制完成时机。这是处理异步 UI 交互、事件驱动逻辑的标准手段。</p></blockquote><blockquote><p><strong>不值得照搬的</strong>：在原代码里，把 <code>debounce</code> 和 Deferred 混用、把 <code>resolve</code> 外挂到 Vue ref —— 这是两个不相干机制的硬耦合，会带来顺序丢失和 Promise 泄漏的隐患。</p></blockquote><p>一句话总结：</p><blockquote><p>学到的是<strong>思路</strong>（Promise 可以手动控制完成时机），不是<strong>具体写法</strong>（debounce + ref 外挂 resolve）。</p></blockquote>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/05/10/2026-05-10-Promise-Deferred-Pattern/</id>
    <link href="https://believed-breadfruit.top/2026/05/10/2026-05-10-Promise-Deferred-Pattern/"/>
    <published>2026-05-10T08:30:00.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>本文从一段 Vue 项目中「防抖 + Promise resolve 外挂」的弹窗代码出发，分析它为什么别扭、背后想实现的 Deferred Pattern 究竟是什么，以及在现代 JavaScript 里更干净的写法。</p>
</blockqu]]>
    </summary>
    <title>Promise Deferred Pattern：从一段别扭的弹窗代码说起</title>
    <updated>2026-05-10T15:40:22.889Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="运动与健康" scheme="https://believed-breadfruit.top/categories/%E8%BF%90%E5%8A%A8%E4%B8%8E%E5%81%A5%E5%BA%B7/"/>
    <category term="跑步" scheme="https://believed-breadfruit.top/tags/%E8%B7%91%E6%AD%A5/"/>
    <category term="运动" scheme="https://believed-breadfruit.top/tags/%E8%BF%90%E5%8A%A8/"/>
    <category term="健身" scheme="https://believed-breadfruit.top/tags/%E5%81%A5%E8%BA%AB/"/>
    <category term="训练计划" scheme="https://believed-breadfruit.top/tags/%E8%AE%AD%E7%BB%83%E8%AE%A1%E5%88%92/"/>
    <content>
      <![CDATA[<h2 id="衡量跑步性能的指标"><a href="#衡量跑步性能的指标" class="headerlink" title="衡量跑步性能的指标"></a>衡量跑步性能的指标</h2><p>作为有氧运动，跑步的性能衡量主要分为4个方面：</p><ul><li>持续性和坚持: 能够长期坚持跑步，养成稳定的锻炼习惯，例如每周跑步3-5次。</li><li>距离和耐力: 可以连续跑完一定的距离，如5公里、10公里，甚至半程或全程马拉松。</li><li>速度和配速: 在特定距离内达到较快的速度，例如5公里跑进25分钟。</li><li>心率: 心率是衡量心肺能力的重要指标。在跑步中能够长时间维持在有氧区间（Zone 2, 60%-70%的最大心率）是良好心肺健康的表现。</li></ul><h2 id="现状"><a href="#现状" class="headerlink" title="现状"></a>现状</h2><h3 id="近3个月跑步记录"><a href="#近3个月跑步记录" class="headerlink" title="近3个月跑步记录"></a>近3个月跑步记录</h3><table><thead><tr><th>Month</th><th>Consistency (Runs)</th><th>Speed (Pace)</th><th>Distance (Avg km)</th><th>Heart Rate (Avg bpm)</th></tr></thead><tbody><tr><td>11月</td><td>10</td><td>6.21 min&#x2F;km</td><td>7.11</td><td>149.9</td></tr><tr><td>10月</td><td>12</td><td>6.59 min&#x2F;km</td><td>4.47</td><td>137.2</td></tr><tr><td>9月</td><td>17</td><td>7.23 min&#x2F;km</td><td>5.37</td><td>145.35</td></tr></tbody></table><h3 id="近两周跑步记录"><a href="#近两周跑步记录" class="headerlink" title="近两周跑步记录"></a>近两周跑步记录</h3><table><thead><tr><th>Week</th><th>Consistency (Runs)</th><th>Speed (Pace)</th><th>Distance (Avg km)</th><th>Heart Rate (Avg bpm)</th></tr></thead><tbody><tr><td>11.24-12.01</td><td>3</td><td>5.59 min&#x2F;km</td><td>7.57</td><td>147.33</td></tr><tr><td>11.17-11.24</td><td>3</td><td>6.05 min&#x2F;km</td><td>7.86</td><td>152</td></tr></tbody></table><h3 id="数据趋势总结"><a href="#数据趋势总结" class="headerlink" title="数据趋势总结"></a>数据趋势总结</h3><ol><li>跑步频率（Consistency）：形成跑步习惯</li></ol><ul><li>近3个月：9月最多（17次），10月减少到12次，11月进一步下降到10次。</li><li>近两周：保持稳定，每周跑步3次，目前跑步频率趋于均衡，已经形成跑步习惯</li></ul><ol start="2"><li>配速（Speed &#x2F; Pace）：配速稳定提升，尤其是近两周已经显著超过前三个月的平均水平，跑步效率和速度都有所改善。</li></ol><ul><li>近3个月：配速持续提升：9月为 7.23 min&#x2F;km，10月为 6.59 min&#x2F;km，11月进一步提升到 6.21 min&#x2F;km。</li><li>近两周：11.24-12.01：5.59 min&#x2F;km，比前一周 6.05 min&#x2F;km 提升显著。</li></ul><ol start="3"><li>距离（Distance）：耐力逐步提升</li></ol><ul><li>近3个月：9月平均 5.37 km，10月减少到 4.47 km，但11月回升到 7.11 km。</li><li>近两周：11.17-11.24 跑步距离较高（7.86 km），11.24-12.01 略微下降到 7.57 km。</li></ul><ol start="4"><li>平均心率（Heart Rate）：心肺适应性有所改善，逐渐适应高强度训练</li></ol><ul><li>近3个月：9月心率为 145.35 bpm，10月下降到 137.2 bpm，但11月上升到 149.9 bpm。</li><li>近两周：11.24-12.01：147.33 bpm，略低于前一周的 152 bpm。</li><li>心率整体呈波动趋势：<ul><li>9月和10月的心率偏低，可能因为配速较慢。</li><li>11月心率回升至较高水平，与配速加快和训练强度提升相关。</li><li>近两周心率略有下降，表明跑步的心肺适应性有所改善。</li></ul></li></ul><h3 id="数据对标"><a href="#数据对标" class="headerlink" title="数据对标"></a>数据对标</h3><p>根据<a href="https://runninglevel.com/#results">running level 网站</a>的计算，我的5k, 10k的水平如下图所示：</p><ul><li><p>5k<br><img src="/../images/better_at_running/image-2.png" alt="alt text"></p></li><li><p>10k<br><img src="/../images/better_at_running/image-1.png" alt="alt text"></p></li></ul><p>可以看到，对标我年龄段的业余跑者来看，我处于入门水平。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>总体来讲，我很满意目前的跑步性能，看到了我未来的提升空间很大，看到了进步的趋势。最重要的是，每次跑完，感觉真的很爽，心情舒畅。</p><h2 id="回忆以前"><a href="#回忆以前" class="headerlink" title="回忆以前"></a>回忆以前</h2><p>我是今年5月份开始跑步，那段时间，我面临着两个不争的事实：</p><ol><li><p>我感觉自己达不到现在的水平。我的微信朋友圈有一个好友一直在坚持跑步，他的配速时5-6min&#x2F;km，距离是5k。有一次我直接向外发出了一个失败的感叹，“我永远也做不到像他这”，这句话我印象十分深刻。</p></li><li><p>第2公里很难坚持下去，每次往前跑一步，我都有疑问“自己为什么要迈出这步”。那个时候，我根本不知道自己为什么要跑步，只知道身体需要锻炼，我没有任何方法。</p></li></ol><p>今年上半年，我的跑步情况是这样的：</p><ul><li>1月跑了1次，2.67km, 配速9:16 min&#x2F;km</li><li>2月跑了1次，2.83km, 配速10:53 min&#x2F;km</li><li>3月没跑</li><li>4月跑了6次，平均2km, 配速9:12 min&#x2F;km</li><li>5月跑了10次，平均3.23km, 配速7:54 min&#x2F;km</li><li>6月跑了11次，平均3.23km, 配速8:48 min&#x2F;km</li></ul><p><img src="/../images/better_at_running/image-3.png" alt="alt text"></p><p>可以发现，这些数据真的很糟糕。</p><h2 id="跑步阶段回顾"><a href="#跑步阶段回顾" class="headerlink" title="跑步阶段回顾"></a>跑步阶段回顾</h2><p>从今年5月份开始决定跑步，到现在，我的跑步历程主要分为下面5个阶段，</p><ol><li>起步（5-7月份）。定一个简单的目标: 每周跑3次，随便找了个habit tracker工具，跑完记录一次。</li></ol><p><img src="/../images/better_at_running/habit.jpeg" alt="alt text"></p><p>这个阶段有两个问题：非常多断跑，跑的过程中很痛苦，不知道为啥要跑下一步。</p><ol start="2"><li>稳定跑（8月份）。主要是解决断跑+跑多少的问题。开了个keep会员，可以选一些跑步训练，比如3公里，跑的过程中还可以听节奏。</li></ol><p>这个阶段有点跑步的感觉了，但是感觉每次跑步都很累，如果跑步的情绪记忆是痛苦的，那肯定会影响跑步积极性。</p><p><img src="/../images/better_at_running/IMG_1374.jpg" alt="alt text"></p><ol start="3"><li><p>数据跑（9月份）。买了个新表garmin，忘了为什么要买，但是这表可以记录更多数据，从这个月开始，我开始了解跑步数据了，每次跑完就看看数据，还让ai给我生成一些跑步计划。</p></li><li><p>科学跑（10月份）。无意间了解到一个“二区跑”这个概念，然后在网上看了很多讲解，开始关注心率、有氧、耐力这些性能指标了。</p></li></ol><p><img src="/../images/better_at_running/image-4.png" alt="alt text"></p><ol start="5"><li>上瘾跑（11月份）。二区跑开始慢慢提速了，虽然心率跑到3、4区了，但是配速快了，而且跑完也不累，心情感觉也很爽，这个月跑步性能突然有了质的提升，数据给的正向反馈+多巴胺内啡肽的释放+习惯，各种buff叠加，发现跑步越来越上瘾了。</li></ol><h2 id="经验总结"><a href="#经验总结" class="headerlink" title="经验总结"></a>经验总结</h2><h3 id="阶段路径总结"><a href="#阶段路径总结" class="headerlink" title="阶段路径总结"></a>阶段路径总结</h3><p>每一个阶段，我都有一套统一的规则：目标，工具，反思。工具的选择其实并没有精心挑选过，大部分都是过程中体验，而且在好奇心的驱使下，无意间建立了很多认知。</p><h3 id="经验分析"><a href="#经验分析" class="headerlink" title="经验分析"></a>经验分析</h3><p>过程中确实有一些弯路，但是正是因为有这些弯路，才逐渐建立了对跑步的全局认知，这种认知我并不需要前置，不必要求一开始就全部知道。所以这样看，谈论规避弯路没有意义，但是确实两点可以前置。</p><ul><li>目标感加强。一开始动机不强不要紧，但是必须清楚地意识到，一件事情，要么做，要么不做，要是做了，就早点get going，缩短无效探索时间。</li><li>早点建立科学理念，在这个场景下，就是跑步理论，防止受伤。</li></ul><p>第二点对我来说不强烈，可能因为我这方面底子好，但是第一点是非常真实的感悟，通过这次跑步的经验总结，我似乎拥有了一种可以实现目标的超能力。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/12/01/2024-12-01-how-do-i-get-better-at-running/</id>
    <link href="https://believed-breadfruit.top/2024/12/01/2024-12-01-how-do-i-get-better-at-running/"/>
    <published>2024-12-01T03:05:01.562Z</published>
    <summary>
      <![CDATA[<h2 id="衡量跑步性能的指标"><a href="#衡量跑步性能的指标" class="headerlink" title="衡量跑步性能的指标"></a>衡量跑步性能的指标</h2><p>作为有氧运动，跑步的性能衡量主要分为4个方面：</p>
<ul>
<li>持续性和坚]]>
    </summary>
    <title>How do I get better at RUNNING</title>
    <updated>2024-12-22T06:29:08.041Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="微前端" scheme="https://believed-breadfruit.top/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <content>
      <![CDATA[<p>微前端是现代前端开发的趋势，许多企业选择将微前端架构融入其技术战略中，有些是选择现有微前端框架如 <a href="https://single-spa.js.org/docs/getting-started-overview">Single-SPA</a>、<a href="https://qiankun.umijs.org/zh/guide">Qiankun</a>、<a href="https://micro-zoe.github.io/micro-app/docs.html#/">Micro-App</a> 或 <a href="https://wujie-micro.github.io/doc/guide/#%E6%97%A0%E7%95%8C%E6%96%B9%E6%A1%88">Wujie</a>，有些是根据自身独特的业务需求研发了定制的解决方案。</p><p>这一技术趋势的背后是前端技术的快速进化和单页面应用（SPA）开发模式的普及。</p><h3 id="多页应用"><a href="#多页应用" class="headerlink" title="多页应用"></a>多页应用</h3><p>在早期的前端开发中，网站通常采用多页面应用（MultiPage Application, MPA）的形式，每个页面都有自己独立的 HTML、JavaScript 和 CSS 文件。这种架构的一个显著特点是，当浏览器的 URL 发生变化时，浏览器会加载一个新的 HTML 页面，并随之加载相应的 JavaScript 和 CSS 资源。这导致页面不断刷新，虽然简单直接，但对于用户来说体验并不友好，尤其是在需要快速响应和交互的现代Web应用中。</p><h3 id="单页应用"><a href="#单页应用" class="headerlink" title="单页应用"></a>单页应用</h3><p>为了减少页面全局刷新，提升用户体验，单页面应用（SinglePage Web Application, SPA）应运而生。SPA 通过在一个单一的HTML页面加载所有JavaScript 和 CSS 资源，在用户交互过程中，网页不会重新加载或跳转，而是动态地重写当前页面与用户交互的部分，从而避免了页面的全面刷新。</p><p>SPA能实现局部渲染主要是基于两个技术实现：</p><ul><li><strong>AJAX异步</strong></li></ul><p>通过AJAX技术异步请求数据，客户端可以在不刷新整个页面的情况下，与服务器交换数据，并根据返回的数据更新页面的部分内容。这种方式允许用户在网页上进行快速交互，只加载或变更需要更新的部分。</p><ul><li><strong>JavaScript 路由</strong></li></ul><p>路由管理通常由JavaScript在前端处理，而不是服务器端。通过在客户端监听URL的变化，然后根据URL的不同展示相应的视图，而无需重新加载页面。单页面应用主要依赖两种路由模式来管理 URL 的变化：一种是基于 URL 的哈希（hash），另一种是利用 HTML5 的 History API。</p><h3 id="微前端应用"><a href="#微前端应用" class="headerlink" title="微前端应用"></a>微前端应用</h3><p>随着业务的增长和应用的扩展，单页面应用往往逐渐演变为庞大的单体应用，这不仅使得项目难以维护，同时由于前端技术栈的快速迭代，老旧技术的技术债务也日益增加。</p><p>在这样的背景下，传统的巨石单体应用已经难以应对快速迭代和灵活部署的要求。微前端架构应运而生，在微前端架构下，一个大型单一的前端应用被拆分成多个小型、独立的子应用，这些子应用负责应用的一部分功能，可以独立开发、部署、运行，并且可以使用不同的技术栈，从而带来更加灵活高效的项目开发和管理。</p><p><img src="/../images/2024-04/mfe.png" alt="Small Picture"></p><p>微前端架构本质上依然采用单页面应用的形式，即在一个单一的 HTML 文件中加载多个 JavaScript 和 CSS 资源。然而，它通过精巧的路由和应用生命周期管理，以及不可缺少的隔离和共享机制，能够将多个独立开发的子应用集成到一个协调一致的用户界面中。</p><p>从微前端概念和产生背景出发，基于对现有微前端框架的考查，可以总结出微前端架构的设计具有以下特征：</p><ul><li>子应用的独立性：独立开发、技术栈无关。</li><li>子应用的自治性（隔离）：应用运行期间有明显的边界，不受影响。</li><li>共享机制：复用性、统一体验、高效协调。</li></ul><p>这种模式不仅提高了开发效率，降低了维护难度，还使得旧系统的逐步迁移变得可行，无需重构整个应用。因此，微前端是现代Web开发中的一个重要趋势。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/04/22/%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%8A%80%E6%9C%AF%E7%BB%BC%E8%BF%B0/</id>
    <link href="https://believed-breadfruit.top/2024/04/22/%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%8A%80%E6%9C%AF%E7%BB%BC%E8%BF%B0/"/>
    <published>2024-04-22T13:15:58.000Z</published>
    <summary>
      <![CDATA[<p>微前端是现代前端开发的趋势，许多企业选择将微前端架构融入其技术战略中，有些是选择现有微前端框架如 <a href="https://single-spa.js.org/docs/getting-started-overview">Single-SPA</a>、<a href]]>
    </summary>
    <title>从多应用到微应用：前端架构的技术演变</title>
    <updated>2024-12-22T06:32:39.851Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="微前端" scheme="https://believed-breadfruit.top/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>微前端是微服务概念在前端的应用。在微前端架构下，一个大型单一的前端应用被拆分成多个小型、独立的子应用，这些子应用可以独立开发、独立部署、独立运行，从而带来更加灵活高效的项目开发和管理。</p><p>作为现代前端开发的趋势，许多企业的技术栈融入了微前端，有些是选择成熟的框架如 Qiankun、Micro-App 或 Single-SPA，有些是自研解决方案。</p><p>微前端最简单的实现形式是基于Iframe。本文主要从<a href="https://github.com/andregardi/micro-frontends-iframe">一个简单的微前端</a>案例入手，通过这个简单的Demo，让我们对微前端框架及其设计理念有个初步的理解。</p><h2 id="Demo源代码讲解"><a href="#Demo源代码讲解" class="headerlink" title="Demo源代码讲解"></a>Demo源代码讲解</h2><h3 id="目录结构"><a href="#目录结构" class="headerlink" title="目录结构"></a>目录结构</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">iframe-demo</span><br><span class="line">├─ <span class="title class_">EventBus</span></span><br><span class="line">│  └─ <span class="title class_">EventBus</span>.<span class="property">js</span></span><br><span class="line">├─ cart</span><br><span class="line">│  ├─ index.<span class="property">html</span></span><br><span class="line">│  ├─ script.<span class="property">js</span></span><br><span class="line">│  └─ style.<span class="property">css</span></span><br><span class="line">├─ catalog</span><br><span class="line">│  ├─ index.<span class="property">html</span></span><br><span class="line">│  └─ script.<span class="property">js</span></span><br><span class="line">├─ composition.<span class="property">html</span></span><br><span class="line">├─ script.<span class="property">js</span></span><br><span class="line">└─ style.<span class="property">css</span></span><br></pre></td></tr></table></figure><p>这个Demo的目录结构非常简单，主要分为三部分：</p><ul><li>两个子应用cart、catalog</li><li>全局事件总线</li><li>主应用</li></ul><p>可以看到这个简单的项目已经体现了微前端的核心设计：应用管理、隔离和共享。</p><h3 id="应用管理"><a href="#应用管理" class="headerlink" title="应用管理"></a>应用管理</h3><p>从<code>composition.html</code>文件我们可以看出这个项目的应用管理非常简单，两个子应用通过<code>iframe</code>标签加载，共同存在于同一个页面。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;link rel=<span class="string">&quot;stylesheet&quot;</span> href=<span class="string">&quot;style.css&quot;</span> /&gt;</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">iframe</span> <span class="attr">src</span>=<span class="string">&quot;./catalog/index.html&quot;</span> <span class="attr">id</span>=<span class="string">&quot;catalog&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">iframe</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">iframe</span> <span class="attr">src</span>=<span class="string">&quot;./cart/index.html&quot;</span> <span class="attr">id</span>=<span class="string">&quot;cart&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">iframe</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;script.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="隔离"><a href="#隔离" class="headerlink" title="隔离"></a>隔离</h2><p>由于使用的是iframe标签，两个子应用天然地存在CSS和JS隔离，不会有样式和运行时的冲突。</p><h3 id="微前端-why-iframe-not-div"><a href="#微前端-why-iframe-not-div" class="headerlink" title="微前端 why iframe not div?"></a><strong>微前端 why iframe not div?</strong></h3><p>为什么微前端使用<code>iframe</code>做隔离，而不是普通<code>div</code>。</p><p>主要是因为<code>iframe</code>在浏览器中具有天然的隔离环境，主要表现在：</p><ol><li><strong>JavaScript 隔离</strong>：<code>iframe</code> 内的 JavaScript 运行在独立的全局上下文中。这意味着，<code>iframe</code> 内的 JavaScript 变量和函数不会影响到主页面，反之亦然。这种特性对于确保应用间不会相互干扰是非常重要的。</li><li><strong>样式隔离</strong>：<code>iframe</code> 内的样式不会影响到外部页面。每个 <code>iframe</code> 有自己的文档流，所以里面的 CSS 只会作用于 <code>iframe</code>内部，不会泄露到外部，这保证了样式的独立性和一致性。</li><li><strong>DOM 隔离</strong>：每个 <code>iframe</code> 都有自己的 DOM 树，与主页面的 DOM 树完全隔离。这意味着，iframe 内的 DOM 操作不会影响到外部页面，减少了应用间的直接 DOM 冲突。</li></ol><p>这些隔离特性对于普通的<code>div</code>标签是没有的。</p><h2 id="共享-通信"><a href="#共享-通信" class="headerlink" title="共享(通信)"></a>共享(通信)</h2><p>虽然<code>iframe</code>能实现简单的微前端架构，但是通过代码中设计的全局事件总线（<code>EventBus.js</code>文件），我们可以看出基于<code>iframe</code>的微前端需要单独设计跨域通信方式。</p><p>本案例主要是通过<code>PostMessage</code>方法实现的。<code>PostMessage</code>  是一种安全地实现不同浏览器窗口（包括弹出窗口和<code>iframe</code>）间通信的方式。允许不同源（<code>origin</code>）的窗口进行数据交换，从而克服了同源策略的限制。适用于多种场景，如页面与弹出窗口、页面与嵌入的<code>iframe</code>、甚至是不同的web workers之间的通信。</p><p><strong>基本用法</strong><br>包括两部分：发送消息和接收消息。</p><p><strong>发送消息:</strong> <code>window.postMessage</code><br>这个方法接受两个参数：要发送的消息和消息接收方的源（<code>origin</code>）。但出于安全考虑，这通常不推荐。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">otherWindow.<span class="title function_">postMessage</span>(message, targetOrigin); <span class="comment">// otherWindow 是另一个窗口的引用，例如一个 iframe 或者通过 window.open 打开的窗口</span></span><br></pre></td></tr></table></figure><ul><li><code>message</code>：你想要发送的数据。</li><li><code>targetOrigin</code>：指定目标窗口的源，例如 “<code>https://example.com</code>“。这是一种安全机制，用来确保消息只被预期的接收者接收。如果你不关心目标窗口的源，也可以使用 “*” 作为通配符。</li></ul><p>**接收消息：**在目标窗口添加一个事件监听器来监听 <code>message</code> 事件</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&quot;message&quot;</span>, <span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 可以通过 event.origin 检查消息来源的源</span></span><br><span class="line">    <span class="keyword">if</span> (event.<span class="property">origin</span> !== <span class="string">&quot;&lt;http://expected-origin.com&gt;&quot;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span>; <span class="comment">// 忽略来自不期望源的消息</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 处理接收到的数据</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(event.<span class="property">data</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>在本项目中，<code>composition</code>主应用的<code>JS</code>脚本中注册了<code>message</code>事件监听，将基座应用接收到的消息转发给所有子应用。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">sendReceivedMessageToAllIframes</span> = (<span class="params">event</span>) =&gt; &#123;</span><br><span class="line">  <span class="variable language_">document</span></span><br><span class="line">    .<span class="title function_">querySelectorAll</span>(<span class="string">&#x27;iframe&#x27;</span>)</span><br><span class="line">    .<span class="title function_">forEach</span>(<span class="function">(<span class="params">iframe</span>) =&gt;</span> iframe.<span class="property">contentWindow</span>.<span class="title function_">postMessage</span>(event.<span class="property">data</span>, <span class="string">&#x27;*&#x27;</span>));</span><br><span class="line">&#125;;</span><br><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, sendReceivedMessageToAllIframes);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在<code>catalog</code>子应用中，注册了一个全局事件总线，并且给按钮注册了点击事件，当按钮点击后，会触发<code>productToCart</code>事件，在该事件中，会向主应用发送消息。</p><p>最终该消息会经过主应用，接着由<code>cart</code>子应用中的全局事件总线接收，执行<code>add</code>方法。</p><p>大致的交互示意图如下所示：</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/009cb022-bc62-4374-9163-d687c21e83ee/Untitled.png" alt="Untitled"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>一个 <code>iframe</code> 具备四层能力：文档的加载能力、HTML 的渲染能力、独立执行 JavaScript 的能力、隔离样式的能力。这些能力，其实也是微前端项目设计的核心要求。通过学习本项目，我们可以很好地初步学习微前端，为后来深入学习Single-SPA、qiankun等框架提供理论基础。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/04/01/%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84Demo%E4%BA%86%E8%A7%A3Iframe%E5%BE%AE%E5%89%8D%E7%AB%AF/</id>
    <link href="https://believed-breadfruit.top/2024/04/01/%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84Demo%E4%BA%86%E8%A7%A3Iframe%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <published>2024-04-01T13:41:25.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>微前端是微服务概念在前端的应用。在微前端架构下，一个大型单一的前端应用被拆分成多个小型、独立的子应用，这些子应用可以独立开发、独立部署、独立]]>
    </summary>
    <title>一个简单的Demo了解Iframe微前端</title>
    <updated>2024-12-22T06:31:32.148Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="SVG" scheme="https://believed-breadfruit.top/tags/SVG/"/>
    <content>
      <![CDATA[<p>在SVG元素中，**<code>width</code><strong>和</strong><code>height</code><strong>属性以及</strong><code>viewBox</code>**属性中的宽度（width）和高度（height）都可以影响SVG的布局，前者宽高和普通元素的宽高类似，比较好理解，但是viewBox中的宽高不好理解，本文会将viewBox属性讲清楚。</p><h2 id="width-和-height-属性"><a href="#width-和-height-属性" class="headerlink" title="width 和 height 属性"></a><strong><code>width</code> 和 <code>height</code> 属性</strong></h2><p>我们先来理解简单的<code>width</code>和<code>height</code>属性，用法如下：</p><p><a href="https://codepen.io/janice143/pen/dyrxvjO">https://codepen.io/janice143/pen/dyrxvjO</a></p><p>在这段代码中，我们通过css的<code>background</code>属性建立了一个间距为10px的网格坐标系统，左上角起始点为原点（0,0)，向右和向下分别为横向和纵向的正方向。</p><p>所以svg元素的<code>width</code>和<code>height</code>属性很好理解，这两个属性确定了元素相对于整个画布的<strong>绝对大小</strong>，里面的<strong>元素肯定局限在这两个属性限定的范围内</strong>。</p><p>在这个限定范围内，我们要对svg内的元素进行缩放，或者移动内部元素怎么办？这个时候就要用到<code>viewBox</code>属性。</p><h2 id="viewBox属性"><a href="#viewBox属性" class="headerlink" title="viewBox属性"></a><code>viewBox</code>属性</h2><p>viewBox也是一个容器，有位置和大小。viewBox属性的用法如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">viewBox=<span class="string">&quot;x y width height&quot;</span></span><br><span class="line"></span><br><span class="line">or</span><br><span class="line"></span><br><span class="line">viewBox=<span class="string">&quot;x,y,width,height&quot;</span></span><br></pre></td></tr></table></figure><p>该属性内部有4个属性，可以用空格或者逗号进行分割。</p><ul><li><code>x</code>, <code>y</code> 表示viewBox这个容器的左上角位置坐标（相对于svg元素的绝对位置）</li><li><code>width</code>, <code>height</code>表示viewBox这个容器的宽高</li></ul><p>请看下面这里例子：</p><p><a href="https://codepen.io/janice143/pen/dyrxvjO">https://codepen.io/janice143/pen/dyrxvjO</a></p><p>在这里例子中，我们限定了一个200 <em>200 的区域，整个svg元素在这块区域内显示。svg内部的元素rect的宽高为100</em> 100。</p><p>可以看到左右两个设置了viewBox和没有设置的差异。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/d9fc530e-4cdf-42f6-8497-e50599d9c6bc/Untitled.png" alt="Untitled"></p><p>在这个图的下面，我们又以viewBox的视角，画出了viewBox这个容器中元素的展示</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/4f309c00-776c-4339-9093-1b117a7ce3cb/Untitled.png" alt="Untitled"></p><p>可以发现，在viewBox视角下，元素的展示和限定在<code>width</code>和<code>height</code>内的展示一模一样，唯一的差别是前者进行另一个缩放，需要完全限制在<code>width</code>和<code>height</code>内部。</p><p>这就是viewBox的作用。</p><h2 id="理解viewBox的SOP"><a href="#理解viewBox的SOP" class="headerlink" title="理解viewBox的SOP"></a>理解viewBox的SOP</h2><p>所以我们可以这样理解svg的viewBox属性：</p><ol><li>根据viewBox的四个值确定viewBox的大小</li><li>根据svg内部元素的宽高确定其在viewBox中的位置</li><li>最后把上面二者结合的图像缩放到svg元素的宽高内</li></ol><h2 id="举一反三"><a href="#举一反三" class="headerlink" title="举一反三"></a>举一反三</h2><p>我们在举一些例子来继续理解viewBox。</p><h3 id="1-案例1"><a href="#1-案例1" class="headerlink" title="1. 案例1"></a>1. 案例1</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;100&quot;</span> </span><br><span class="line">  height=<span class="string">&quot;100&quot;</span> </span><br><span class="line">  viewBox=<span class="string">&quot;0 0 200 200&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;100&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;100&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><h3 id="2-案例2"><a href="#2-案例2" class="headerlink" title="2. 案例2"></a>2. 案例2</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  height=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  viewBox=<span class="string">&quot;0 0 200 200&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;100&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;100&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><h3 id="3-案例3"><a href="#3-案例3" class="headerlink" title="3. 案例3"></a>3. 案例3</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  height=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  viewBox=<span class="string">&quot;0 0 100 100&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;100&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;100&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><h3 id="4-案例4"><a href="#4-案例4" class="headerlink" title="4. 案例4"></a>4. 案例4</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;200&quot;</span></span><br><span class="line">  height=<span class="string">&quot;200&quot;</span></span><br><span class="line">  viewBox=<span class="string">&quot;-100 -100 200 200&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;0&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;0&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><p>上面四个案例中，我们使用<code>**蓝色底+紫色元素**</code>画出了viewBox容器中的元素呈现，可以看到这个相对元素展示就是元素在绝对位置上的展示。</p><p>按照上述给出的理解步骤，我们很容易还原svg的图像展示。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文通过一些案例说明了svg元素中width、height 和viewBox属性的理解，其中，**<code>width</code><strong>和</strong><code>height</code><strong>属性控制SVG的物理尺寸，而</strong><code>viewBox</code>**属性则定义了一个内部的视图框架，允许内容在保持比例的前提下缩放以适应不同的物理尺寸。</p><ul><li><strong>无<code>viewBox</code>时</strong>：SVG的显示大小严格遵循**<code>width</code><strong>和</strong><code>height</code>**属性。图形的大小不会自动适应容器的大小变化。</li><li><strong>有<code>viewBox</code>时</strong>：SVG内部的图形（在这个例子中是蓝色的正方形）会被缩放以适应指定的物理尺寸（200x200像素），而不失去比例或变形。这是因为**<code>viewBox</code>**提供了一种机制，允许SVG内容在不同尺寸的容器中灵活显示，同时保持其内部图形的正确比例和定位。</li></ul>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/02/26/Understanding-ViewBox-in-SVG/</id>
    <link href="https://believed-breadfruit.top/2024/02/26/Understanding-ViewBox-in-SVG/"/>
    <published>2024-02-26T13:42:09.000Z</published>
    <summary>
      <![CDATA[<p>在SVG元素中，**<code>width</code><strong>和</strong><code>height</code><strong>属性以及</strong><code>viewBox</code>**属性中的宽度（width）和高度（height）都可以影响]]>
    </summary>
    <title>Understanding ViewBox in SVG</title>
    <updated>2024-12-22T06:31:11.653Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="数据结构与算法" scheme="https://believed-breadfruit.top/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"/>
    <category term="算法" scheme="https://believed-breadfruit.top/tags/%E7%AE%97%E6%B3%95/"/>
    <content>
      <![CDATA[<h2 id="题目"><a href="#题目" class="headerlink" title="题目"></a>题目</h2><p>如题：</p><p>给定两个由小写字母组成的字符串 <code>s1</code> 和 <code>s2</code>，请编写一个程序，确定其中一个字符串的字符重新排列后，能否变成另一个字符串。</p><p><strong>示例 1：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">输入:s1 = &quot;abc&quot;,s2 = &quot;bca&quot;</span><br><span class="line">输出: true</span><br></pre></td></tr></table></figure><p><strong>示例 2：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">输入:s1 = &quot;abc&quot;,s2 = &quot;bad&quot;</span><br><span class="line">输出: false</span><br></pre></td></tr></table></figure><p>该题的解题思路很多，如数组计数、哈希集合、排序等。本文讲解一种<strong>巧用数学公式</strong>的解法。</p><h2 id="有趣的推理"><a href="#有趣的推理" class="headerlink" title="有趣的推理"></a>有趣的推理</h2><p>首先，我们来推导一个公式，已知以下两个等式：</p><p>$$<br>A+B&#x3D;X+Y\<br>A<em>B&#x3D;X</em>Y<br>$$</p><p>其中A,B, X, Y不为0。我们的目标是证明：A，B和X，Y两两相等，即$A&#x3D;X,B&#x3D;Y$ 或者$A&#x3D;Y,B&#x3D;X$。</p><p>推导过程如下：</p><p>对已知的第一个等式进行移向，可以得到</p><p>$$<br>B&#x3D;Y−A+X<br>$$</p><p>将B带入到第二个等式，可得</p><p>$$<br>A(Y−A+X) &#x3D; XY<br>$$</p><p>经过代数运算，可得</p><p>$$<br>AY-A^2+AX &#x3D; XY\<br>A(X-A) &#x3D; (X-A)Y\<br>$$</p><p>所以可得</p><p>$$<br>X&#x3D;A, Y&#x3D;B<br>$$</p><p>或者</p><p>$$<br>Y&#x3D;A, X&#x3D;B<br>$$</p><p>证明完毕。</p><p>$A&#x3D;X,B&#x3D;Y$ 或者$A&#x3D;Y,B&#x3D;X$，这意味着A,B 和X,Y这两对数字是全排列组合，数字都是相同的，只是打乱的顺序。</p><p>有了这个推理，根据加法和乘法的交换律和结合律，我们可以继续增加变量，得出这么一个结论：</p><p>若A1 + A2 + A3 + … +An &#x3D; B1 + B2+ B3 + … + Bn，那么A1到An和B1到Bn肯定一一对应，即是全排列组合。</p><h2 id="字符串排列组合"><a href="#字符串排列组合" class="headerlink" title="字符串排列组合"></a>字符串排列组合</h2><p>知道了上述的推理，和我们的题目有什么关系呢？</p><p>题目是要我们证明两个字符串是否是全排列组合，我们是否可以利用刚才得到的全排列性质来解题呢？</p><p>答案是可以的，思路是这样的：<strong>如果我们把每个字符都看成一个不重合的数字，通过遍历字符串，对字符映射成的数字进行A+B和A*B的计算，只要两个字符串得出的结果一样，就说明这两个字符串互为全排列组合，即其中一个字符串的字符重新排列后是另一个字符串</strong>。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s1</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s2</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> &#123;<span class="type">boolean</span>&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">var</span> <span class="title class_">CheckPermutation</span> = <span class="keyword">function</span>(<span class="params">s1, s2</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span>(s1.<span class="property">length</span> !== s2.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    <span class="keyword">const</span> [s11, s22] =[s1.<span class="title function_">toLowerCase</span>(),s2.<span class="title function_">toLowerCase</span>()]</span><br><span class="line">    <span class="keyword">let</span> sum1 = <span class="number">0</span>, sum2 = <span class="number">0</span>, multi1 = <span class="number">1</span>, multi2 = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>;i&lt;s11.<span class="property">length</span>;i++)&#123;</span><br><span class="line">        sum1 += s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line">        sum2 += s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line"></span><br><span class="line">        multi1 *= s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line">        multi2 *= s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> sum1 === sum2 &amp;&amp; multi1 === multi2</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// case1: s1=&quot;abc&quot;, s2=&quot;bca&quot;, 输出true，预期true</span></span><br><span class="line"><span class="comment">// case2: s1=&quot;abc&quot;, s2=&quot;bad&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case3: s1=&quot;aac&quot;, s2=&quot;bbb&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case4: s1=&quot;abbcdde&quot;, s2=&quot;abcccde&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case5: s1=&quot;bkhfhqlayvlhdqmxvnkqvtkojouugfsnwmyoywkilsnubnkvhdbrltuxvoblurpfinpigajttcvkcxlylblcaocsjmwdvwepvnfr&quot;, </span></span><br><span class="line"><span class="comment">// s2=&quot;mtycyvobjldulmhsuqvtrhqnisjkuxhvaxqkvpbllnkvvakxjbolefpyrtiivvwctunasbbocldflkcknmwgofngorduwlwhyfnp&quot; false</span></span><br><span class="line"><span class="comment">// 输出false，预期true</span></span><br></pre></td></tr></table></figure><p>可以看到，这个算法在执行第5个case的时候出错了，这是因为字符串的长度很长，我们的字符映射是从1-26，相乘很容易遇到溢出的问题，导致计算不准确。</p><p>但是好消息是上述解法证明我们的推理没有问题，思路没有问题，接下来我们思考如何解决计算溢出导致失去精度问题。</p><h2 id="位运算：解决溢出"><a href="#位运算：解决溢出" class="headerlink" title="位运算：解决溢出"></a>位运算：解决溢出</h2><p>我们引入位向量，什么是位向量呢？</p><p><strong>位向量（bit vector）</strong>，是一个仅包含0和1的数组或者序列，其中每一位都代表某个元素的状态或属性。例如，1表示存在，0表示不存在。<strong>位向量</strong>表示方法非常高效，可以在固定大小的数组中存储大量的信息，而且位向量可以适合进行位运算，可以提高计算效率。</p><p>例如我们可以用以下位向量表示0-5这几个数字：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">0</span> --&gt; <span class="number">1</span></span><br><span class="line"><span class="number">1</span> --&gt; <span class="number">10</span></span><br><span class="line"><span class="number">2</span> --&gt; <span class="number">100</span></span><br><span class="line"><span class="number">3</span> --&gt; <span class="number">1000</span></span><br><span class="line"><span class="number">4</span> --&gt; <span class="number">10000</span></span><br><span class="line"><span class="number">5</span> --&gt; <span class="number">100000</span></span><br></pre></td></tr></table></figure><p>数字和位向量的关系为：数字表示位向量中为1的位置（从右往左看）。值得注意的是**，位向量的创建需要自己赋予含义，并不是固定的。**</p><p>由于数字和字母之间可以通过ASCII码联系起来，例如a对应97，b对应98，c对应99，所以我们也可以约定一些位向量来表示字母，例如</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">a --&gt; <span class="number">0</span> --&gt; <span class="number">1</span></span><br><span class="line">b --&gt; <span class="number">1</span> --&gt; <span class="number">10</span></span><br><span class="line">c --&gt; <span class="number">2</span> --&gt; <span class="number">100</span></span><br><span class="line">...</span><br><span class="line">y --&gt; <span class="number">24</span> --&gt; <span class="number">1000000000000000000000000</span></span><br><span class="line">z --&gt; <span class="number">25</span> --&gt; <span class="number">10000000000000000000000000</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以发现此处我们做了相对处理，把a看成0，而不是97，这样可以<strong>节省空间</strong>，只需要26位就可以表示26个字母。</p><p>利用**左移运算，**可以创建上述位向量：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span> &lt;&lt; <span class="number">0</span> = <span class="number">1</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; <span class="number">1</span> = <span class="number">10</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; <span class="number">2</span> = <span class="number">100</span></span><br><span class="line"><span class="number">1</span>(十进制） &lt;&lt; <span class="number">3</span>(十进制） = <span class="number">1000</span>(二进制）</span><br><span class="line"></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">1</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;b&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">10</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;c&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">100</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;d&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">1000</span></span><br></pre></td></tr></table></figure><p>加法和乘法有这样的规律：</p><ul><li>交换律</li><li>任何数字和0 相加，等与自身</li><li>任何数字和1相乘，等于自身</li></ul><p>在位运算中，和加法和乘法规律非常相似的运算是位运算的**加法和异或。**加法比较简单，任何进制的加法都是一样的规律，而且相等，也就是二进制的x肯定等于n进制的y。我们重点来看看异或。</p><p>异或的运算逻辑如下：</p><p>1 ^ 1 &#x3D; 0</p><p>1 ^ 0 &#x3D; 1</p><p>0 ^ 1 &#x3D; 1</p><p>0 ^ 0 &#x3D; 0</p><p>简单来说，异或的特性是，两个值相同，结果为0，不同则结果为1。异或运算具有一定定律：</p><ol><li>任何值与自身异或，结果为0</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ x = 0</span><br></pre></td></tr></table></figure><ol><li>任何值与0异或，结果为其自身</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ 0 = x</span><br></pre></td></tr></table></figure><ol><li>交换律</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ y ^ z = x ^ z ^ y</span><br></pre></td></tr></table></figure><ol><li>结合律</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ y ^ z = x ^ (y ^ z)</span><br></pre></td></tr></table></figure><p>除了第一条，其他都和乘法非常类似。所以我们用位运算来改进我们上述算法。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s1</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s2</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> &#123;<span class="type">boolean</span>&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">var</span> <span class="title class_">CheckPermutation</span> = <span class="keyword">function</span>(<span class="params">s1, s2</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span>(s1.<span class="property">length</span> !== s2.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    <span class="keyword">const</span> [s11, s22] =[s1.<span class="title function_">toLowerCase</span>(),s2.<span class="title function_">toLowerCase</span>()]</span><br><span class="line">  <span class="keyword">let</span> [bitSum1, bitSum2] = [<span class="number">0</span>,<span class="number">0</span>]</span><br><span class="line">  <span class="keyword">let</span> [bitMulti1, bitMulti2] = [<span class="number">0</span>,<span class="number">0</span>]</span><br><span class="line">  <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>;i&lt;s11.<span class="property">length</span>;i++)&#123;</span><br><span class="line">    bitSum1 += <span class="number">1</span> &lt;&lt; (s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line">    bitSum2 += <span class="number">1</span> &lt;&lt; (s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 异或的两条规律：交换率和任何值与自身异或，结果为0</span></span><br><span class="line">    bitMulti1 ^= <span class="number">1</span> &lt;&lt; (s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line">    bitMulti2 ^= <span class="number">1</span> &lt;&lt; (s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> bitSum1 === bitSum2 &amp;&amp; bitMulti1 === bitMulti2</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// case1: s1=&quot;abc&quot;, s2=&quot;bca&quot;, 输出true，预期true</span></span><br><span class="line"><span class="comment">// case2: s1=&quot;abc&quot;, s2=&quot;bad&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case3: s1=&quot;aac&quot;, s2=&quot;bbb&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case4: s1=&quot;abbcdde&quot;, s2=&quot;abcccde&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case5: s1=&quot;bkhfhqlayvlhdqmxvnkqvtkojouugfsnwmyoywkilsnubnkvhdbrltuxvoblurpfinpigajttcvkcxlylblcaocsjmwdvwepvnfr&quot;, </span></span><br><span class="line"><span class="comment">// s2=&quot;mtycyvobjldulmhsuqvtrhqnisjkuxhvaxqkvpbllnkvvakxjbolefpyrtiivvwctunasbbocldflkcknmwgofngorduwlwhyfnp&quot; false</span></span><br><span class="line"><span class="comment">// 输出true，预期true</span></span><br></pre></td></tr></table></figure><h2 id="算法复杂度分析"><a href="#算法复杂度分析" class="headerlink" title="算法复杂度分析"></a>算法复杂度分析</h2><h3 id="时间复杂度分析"><a href="#时间复杂度分析" class="headerlink" title="时间复杂度分析"></a>时间复杂度分析</h3><ol><li>字符串长度为 <em>n</em>，因此循环的次数是 <em>O</em>(<em>n</em>)。</li><li>在循环内部，主要执行了一些位运算和常数时间的操作，这些操作的时间复杂度可以视为 <em>O</em>(1)。</li><li>因此，整个算法的时间复杂度为 <em>O</em>(<em>n</em>)。</li></ol><h3 id="空间复杂度分析"><a href="#空间复杂度分析" class="headerlink" title="空间复杂度分析"></a>空间复杂度分析</h3><ol><li>使用了一些常量空间变量（例如，<code>bitSum1</code>、<code>bitSum2</code>、<code>bitMulti1</code>、<code>bitMulti2</code>）和循环变量（例如，<code>i</code>）。</li><li>不随输入规模 <em><code>n</code></em> 的增长而变化，因此是 <em>O</em>(1) 的空间复杂度。</li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><ul><li>时间复杂度：<em>O</em>(<em>n</em>)</li><li>空间复杂度：<em>O</em>(1)</li></ul><p>总的来讲，这个算法基于一则数学推理，采用了一种巧妙的位运算方法来判断两个字符串是否为排列。相比于其他解题思路，例如排序，暴力解法，等，本文章的解题思路更加高效。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/01/15/%E5%88%A9%E7%94%A8%E2%80%9D%E8%8B%A5A-B-X-Y-A-B-X-Y%EF%BC%8C%E9%82%A3%E4%B9%88A-X-B-Y%E6%88%96A-Y-B-X%E2%80%9C%E5%88%A4%E6%96%AD%E4%B8%A4%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%98%AF%E5%90%A6%E4%BA%92%E4%B8%BA%E6%8E%92%E5%88%97%E7%BB%84%E5%90%88/</id>
    <link href="https://believed-breadfruit.top/2024/01/15/%E5%88%A9%E7%94%A8%E2%80%9D%E8%8B%A5A-B-X-Y-A-B-X-Y%EF%BC%8C%E9%82%A3%E4%B9%88A-X-B-Y%E6%88%96A-Y-B-X%E2%80%9C%E5%88%A4%E6%96%AD%E4%B8%A4%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%98%AF%E5%90%A6%E4%BA%92%E4%B8%BA%E6%8E%92%E5%88%97%E7%BB%84%E5%90%88/"/>
    <published>2024-01-15T13:43:04.000Z</published>
    <summary>
      <![CDATA[<h2 id="题目"><a href="#题目" class="headerlink" title="题目"></a>题目</h2><p>如题：</p>
<p>给定两个由小写字母组成的字符串 <code>s1</code> 和 <code>s2</code>，请编写一个程序，确]]>
    </summary>
    <title>利用”若A+B=X+Y, A*B=X*Y，那么A=X, B=Y或A=Y,B=X“判断两字符串是否互为排列组合</title>
    <updated>2024-04-22T17:22:13.405Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Git" scheme="https://believed-breadfruit.top/tags/Git/"/>
    <category term="Husky" scheme="https://believed-breadfruit.top/tags/Husky/"/>
    <content>
      <![CDATA[<p>在了解husky之前，先了解一下Git hooks。</p><h2 id="GitHooks"><a href="#GitHooks" class="headerlink" title="GitHooks"></a>GitHooks</h2><p>官方文档：<a href="https://git-scm.com/docs/githooks">https://git-scm.com/docs/githooks</a></p><h3 id="是什么"><a href="#是什么" class="headerlink" title="是什么"></a>是什么</h3><p>git hooks 是git提供的hooks，类似Vue的生命周期钩子函数一样，Git也会在它运行周期里面的某些时间点，提供一些让用户添加和执行自定义的函数。</p><p>默认情况下，hooks 目录是 <code>$GIT_DIR/hooks</code> ，但可以通过 <code>core.hooksPath</code> 配置变量进行更改。打开之后能看到很多<code>Hooks</code>的<code>sample</code>。</p><p><a href="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f80e778aa94147f2acbaefa8b5fb5bce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp">https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f80e778aa94147f2acbaefa8b5fb5bce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp</a></p><p><code>Simple</code>文件的内容如下：</p><p><a href="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55484a649af74601b493eeffa8a31bd0~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp">https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55484a649af74601b493eeffa8a31bd0~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp</a></p><p>可以发现，Githooks文件其实是一个<code>shell</code>脚本，<strong>可以通过修改<code>shell</code>脚本来实现自定义功能</strong>。</p><h3 id="常用的hooks"><a href="#常用的hooks" class="headerlink" title="常用的hooks"></a>常用的hooks</h3><p><strong>pre-commit 预提交</strong></p><p>该钩子由 <code>git-commit</code>调用，可以使用 <code>--no-verify</code> 选项绕过。它不带任何参数，在提交之前调用。从此脚本中<strong>以非零状态退出</strong>会导致 <code>git commit</code> 命令在创建提交前中止。</p><p><strong>prepare-commit-msg 准备-提交-消息</strong></p><p>在准备好默认日志消息之后、启动编辑器之前，git-commit[1] 会立即调用此挂钩。</p><p>它需要一到三个参数。</p><ul><li>第一个是包含提交日志消息的文件的名称。</li><li>第二个是提交消息的来源，可以是： <code>message</code> （如果给出了 <code>-m</code> 或 <code>-F</code> 选项）； <code>template</code> （如果给出了 <code>-t</code> 选项或设置了配置选项 <code>commit.template</code> ）； <code>merge</code> （如果提交是合并或存在 <code>.git/MERGE_MSG</code> 文件）； <code>squash</code> （如果 <code>.git/SQUASH_MSG</code> 文件存在）；或 <code>commit</code> ，后跟提交对象名称（如果给出了 <code>-c</code> 、 <code>-C</code> 或 <code>--amend</code> 选项）。</li></ul><p>如果退出状态非零， <code>git commit</code> 将中止。</p><h3 id="Githooks能解决什么"><a href="#Githooks能解决什么" class="headerlink" title="Githooks能解决什么"></a>Githooks能解决什么</h3><p>回到项目中的实际问题</p><ul><li>问题：远程仓库的代码中存在语法错误</li><li>原因：有成员没有仔细检查，误把有语法错误的代码上传了</li><li>解决方法：在<code>commit之前</code>检查在暂存区的代码，看是否存在语法错误 显然，如果只靠当前成员去检查自己添加到暂存区的代码有没有错误，是很不可靠且低效的行为，那么这里我们可以用到<code>pre-commit</code>这一个hook了，它可以在git commit之前执行我们自定义的语句，当<code>执行错误</code>或者<code>返回非0状态</code>的时候阻止commit行为。</li></ul><p>除了这个Hook以外，还有哪些就不一一介绍了，一般的Git操作，例如commit、push等会提供操作前或操作后的Hook，有兴趣的可以查看<a href="https://link.juejin.cn/?target=https://git-scm.com/docs/Githooks">Githooks</a>去了解。</p><h1 id="Husky"><a href="#Husky" class="headerlink" title="Husky"></a>Husky</h1><p>git hooks和husky有什么关系呢？用一句话概括，那就是husky能够简化上述创建或者修改Githooks的操作流程。</p><h3 id="husky工作原理"><a href="#husky工作原理" class="headerlink" title="husky工作原理"></a>husky工作原理</h3><ul><li>初始化：先检查该项目是否通过Git来托管代码的</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="title function_">spawnSync</span>(<span class="string">&#x27;git&#x27;</span>, [<span class="string">&#x27;rev-parse&#x27;</span>]).<span class="property">status</span> !== <span class="number">0</span>) &#123;</span><br><span class="line">  <span class="title function_">l</span>(<span class="string">&#x27;not a Git repository, skipping hooks installation&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>创建<code>.husky</code>文件夹：创建<code>.husky</code>文件夹用来存放<code>Githooks</code>和<code>husky</code>的配置</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建 .husky/_</span></span><br><span class="line"><span class="title function_">mkdirSync</span>(<span class="title function_">join</span>(dir, <span class="string">&#x27;_&#x27;</span>), &#123; <span class="attr">recursive</span>: <span class="literal">true</span> &#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建 .husky/.gitignore</span></span><br><span class="line"><span class="title function_">writeFileSync</span>(<span class="title function_">join</span>(dir, <span class="string">&#x27;.gitignore&#x27;</span>), <span class="string">&#x27;_\n&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 将husky.sh 复制到 .husky/_/husky.sh</span></span><br><span class="line"><span class="title function_">copyFileSync</span>(</span><br><span class="line">  <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">&#x27;./husky.sh&#x27;</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>)),</span><br><span class="line">  <span class="title function_">join</span>(dir, <span class="string">&#x27;_/husky.sh&#x27;</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul><li>修改<code>git hooks</code>路径：通过<code>core.hooksPath</code>使项目<code>Githooks</code>的路径，指向新创建的<code>.husky</code>文件夹</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; error &#125; = <span class="title function_">spawnSync</span>(<span class="string">&#x27;git&#x27;</span>, [<span class="string">&#x27;config&#x27;</span>, <span class="string">&#x27;core.hooksPath&#x27;</span>, dir]);</span><br><span class="line"><span class="keyword">if</span> (error) &#123;</span><br><span class="line">  <span class="keyword">throw</span> error;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>初始化之后，可以通过<code>add</code>命令来进行创建或者往文件后面增加语句</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="title function_">existsSync</span>(file)) &#123;</span><br><span class="line">  <span class="title function_">appendFileSync</span>(file, <span class="string">`<span class="subst">$&#123;cmd&#125;</span>\n`</span>);</span><br><span class="line">  <span class="title function_">l</span>(<span class="string">`updated <span class="subst">$&#123;file&#125;</span>`</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  <span class="title function_">set</span>(file, cmd);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="如何使用Husky"><a href="#如何使用Husky" class="headerlink" title="如何使用Husky"></a>如何使用Husky</h3><p>为了更好的演示这个功能，我们来做一个小Demo。</p><p>需求如下：</p><p>在代码commit之前，检查暂存区的代码的语法错误。</p><p>解决方案：</p><p><code>pre-commit hook</code>+<code>husky</code>。它可以在git commit之前执行我们自定义的语句，当<code>执行错误</code>或者<code>返回非0状态</code>的时候阻止<code>commit</code>行为。</p><ul><li><code>npm</code>初始化：安装<code>eslint</code>、<code>lint-staged</code>和<code>husky</code></li></ul><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install <span class="attr">--save-dev</span> eslint lint-staged husky</span><br></pre></td></tr></table></figure><ul><li><code>eslint</code>初始化</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm init @eslint/config</span><br></pre></td></tr></table></figure><ul><li><code>husky</code>初始化</li></ul><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky install</span><br></pre></td></tr></table></figure><p>这时候我们就看到了.husky的文件夹</p><ul><li>添加<code>lintstagedrc.js</code>：用来配置暂存区文件相对应的<code>lint</code></li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="string">&#x27;*.js&#x27;</span>: [<span class="string">&#x27;eslint&#x27;</span>]</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>使用husky add添加一条检查命令：</li></ul><p>创建一个<code>pre-commit hook</code>，用来对暂存区的文件进行检查</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky <span class="keyword">add</span> .husky<span class="operator">/</span>pre<span class="operator">-</span><span class="keyword">commit</span> &quot;npx lint-staged -c .husky/lintstagedrc.js&quot;</span><br></pre></td></tr></table></figure><ul><li>在项目中新增一个js，故意写上语法错误</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">const testA = &#123;</span><br><span class="line">    console.log(testA);</span><br></pre></td></tr></table></figure><ul><li>尝试提交代码，得到以下报错</li></ul><p><a href="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3facc870d17141838aec9bbd2a55cc7d~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp">https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3facc870d17141838aec9bbd2a55cc7d~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp</a></p><ul><li>把语法错误修复之后再<code>commit</code>，这时候就可以顺利通过检测且commit了，这样我们的目的就达成了</li></ul>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/12/19/%E4%B8%80%E6%96%87%E4%BA%86%E8%A7%A3Husky/</id>
    <link href="https://believed-breadfruit.top/2023/12/19/%E4%B8%80%E6%96%87%E4%BA%86%E8%A7%A3Husky/"/>
    <published>2023-12-19T13:44:10.000Z</published>
    <summary>
      <![CDATA[<p>在了解husky之前，先了解一下Git hooks。</p>
<h2 id="GitHooks"><a href="#GitHooks" class="headerlink" title="GitHooks"></a>GitHooks</h2><p>官方文档：<a href]]>
    </summary>
    <title>一文了解Husky</title>
    <updated>2024-12-22T06:31:36.092Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Canvas" scheme="https://believed-breadfruit.top/tags/Canvas/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>最近在业务上遇到了一个问题是要将页面打印pdf文件，产品的预期是希望点击一个按钮，就能够将页面数据写在一个pdf上，并下载下来，需要保证pdf的内容具有很好的可读性。</p><p>评估下来这个需求的本质是要实现一个能够将HTML页面转为PDF并实现下载的功能。通过技术调研，我最终的方案是采购<code>html2canvas + jspdf</code>这两个库来实现，通过使用html2canvas将使用canvas将页面转为base64图片流，然后将其插入jspdf插件中，实现保存并下载pdf。</p><p>这个方案其实也是目前比较常用的一个方案，但是在实践过程中，遇到的最大问题就是分页截断的问题。当html页面超过一页A4纸的时候，页面就是出现被截断，影响了pdf的可读性。</p><p>由于网上关于分页截断的解决思路比较少，所以特意将此次的解决方案记录下来，也作为一次个人的技术成长实践。</p><h2 id="使用-JSPDF-和-html2canvas-创建简单的-PDF文件"><a href="#使用-JSPDF-和-html2canvas-创建简单的-PDF文件" class="headerlink" title="使用 JSPDF 和 html2canvas 创建简单的 PDF文件"></a><strong>使用 JSPDF 和 html2canvas 创建简单的 PDF文件</strong></h2><p>首先，我们开始使用 JSPDF 和 html2canvas 创建一个简单的 PDF。</p><h3 id="创建一个-JSPDF-实例"><a href="#创建一个-JSPDF-实例" class="headerlink" title="创建一个 JSPDF 实例"></a>创建一个 JSPDF 实例</h3><p>创建一个 JSPDF 实例，设置页面的大小、方向和其他参数。参考官网可以写一个很简单的实例：<a href="https://artskydj.github.io/jsPDF/docs/index.html">https://artskydj.github.io/jsPDF/docs/index.html</a></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> doc = <span class="keyword">new</span> <span class="title function_">jsPDF</span>(&#123;</span><br><span class="line">  <span class="attr">orientation</span>: <span class="string">&#x27;landscape&#x27;</span>,</span><br><span class="line">  <span class="attr">unit</span>: <span class="string">&#x27;in&#x27;</span>,</span><br><span class="line">  <span class="attr">format</span>: [<span class="number">4</span>, <span class="number">2</span>]</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">doc.<span class="title function_">text</span>(<span class="string">&#x27;Hello world!&#x27;</span>, <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">doc.<span class="title function_">save</span>(<span class="string">&#x27;two-by-four.pdf&#x27;</span>)</span><br></pre></td></tr></table></figure><p>生成一个pdf文件，并且在文件中写入一定内容，其实<strong>JSPDF</strong> 这个库就能做到。</p><p>但是很多场景下，我们的目标pdf中的内容会更复杂，而且还要考虑排版，所以最好的方式是引入<strong>html2canvas</strong>这个库，将页面元素转换成base64数据，然后贴在pdf中(使用<a href="https://artskydj.github.io/jsPDF/docs/module-addImage.html">addImage方法</a>），这样就能保证页面的内容。引入了<strong>html2canvas</strong>库后，我们更多关注就是利用现成组件库、或者原生html和css实现更复杂的页面内容。</p><h3 id="引入-html2canvas"><a href="#引入-html2canvas" class="headerlink" title="引入 html2canvas"></a>引入 html2canvas</h3><p>官方文档：<a href="https://html2canvas.hertzen.com/getting-started">https://html2canvas.hertzen.com/getting-started</a></p><p> 使用 html2canvas 捕捉 HTML 内容或特定的 HTML 元素，并将其转换为 Canvas。下面是一个简单的demo, 可以可以看到html2canvas能够将dom元素转化为一张base64图片，尝试选择页面元素可以感受到图片和文字的不同。</p><p><a href="https://codepen.io/janice143/pen/RwvpQbZ">https://codepen.io/janice143/pen/RwvpQbZ</a></p><h3 id="将html2canvas转化的图片放到pdf中"><a href="#将html2canvas转化的图片放到pdf中" class="headerlink" title="将html2canvas转化的图片放到pdf中"></a>将html2canvas转化的图片放到pdf中</h3><p>这一步我们需要使用JSPDF 的addImage方法，其语法如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">addImage</span>(imageData, format, x, y, width, height, alias, compression)</span><br></pre></td></tr></table></figure><ul><li>imageData - 要添加的图像数据。可以是图像的 URL、图像的 base64 编码字符串或图像的二进制数据</li><li>format - 图像的格式。可以是 “JPEG”、”PNG” 或 “TIFF”。</li><li>x - 图像在 PDF 文档中的 x 坐标。</li><li>y - 图像在 PDF 文档中的 y 坐标。</li><li>width - 图像的宽度。</li><li>height - 图像的高度。</li><li>alias - 图像的别名。此别名可用于在 PDF 文档中引用图像。</li><li>compression - 图像的压缩级别。可以是 “NONE”、”FAST” 或 “SLOW”。</li></ul><p>下面是一串示例代码：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> doc = <span class="keyword">new</span> <span class="title function_">jsPDF</span>();</span><br><span class="line">doc.<span class="title function_">addImage</span>(<span class="string">&#x27;image.jpg&#x27;</span>, <span class="string">&#x27;JPEG&#x27;</span>, <span class="number">10</span>, <span class="number">10</span>, <span class="number">100</span>, <span class="number">100</span>);</span><br><span class="line">doc.<span class="title function_">save</span>(<span class="string">&#x27;output.pdf&#x27;</span>);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>此示例将在 PDF 文档的 (10, 10) 处添加一个名为 “image.jpg” 的 JPEG 图像。图像的宽度和高度将分别为 100 和 100 像素。</p><h3 id="JSPDF-和-html2canvas结合起来用"><a href="#JSPDF-和-html2canvas结合起来用" class="headerlink" title="JSPDF 和 html2canvas结合起来用"></a><strong>JSPDF 和 html2canvas结合起来用</strong></h3><p>知道上面的关键三个点，接下来我们将这三个步骤串联起来，实现一个基本的html→pdf的方案。大致步骤如下：</p><ol><li>写一个基本html页面</li><li>创建jspdf实例</li><li>获取页面的dom节点，使用html2canvas将其转化为base64数据流</li><li>将base64数据流装载到jspdf的addImage方法中</li><li>保存pdf</li></ol><p>基于这5个步骤，可以实现基本的单页打印。</p><h2 id="多页：比例缩放-循环移位"><a href="#多页：比例缩放-循环移位" class="headerlink" title="多页：比例缩放+循环移位"></a>多页：比例缩放+循环移位</h2><p>但是，在你实践中，你会发现2个问题：</p><ul><li>页面的内容超出了pdf页面</li><li>打印的pdf只有一页，没有展示全部的html信息</li></ul><p>这篇博客给了一个非常好的解决方案，其原理是：</p><ul><li>通过比例缩放，实现等比展示在pdf中</li><li>通过循环迭代，不停移动base64的图片位置，也就是调整X,Y参数（x - 图像在 PDF 文档中的 x 坐标；y - 图像在 PDF 文档中的 y 坐标），移动使用jspdf的addPage方法新加一个pdf。</li></ul><p><a href="https://juejin.cn/post/7090368199291568165#heading-4">Vue前端实现HTML转PDF并导出 - 掘金</a></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 当内容未超过pdf一页显示的范围，无需分页</span></span><br><span class="line"><span class="keyword">if</span> (leftHeight &lt; pageHeight) &#123;</span><br><span class="line">  <span class="comment">// addImage(pageData, &#x27;JPEG&#x27;, 左，上，宽度，高度)设置</span></span><br><span class="line">  <span class="variable constant_">PDF</span>.<span class="title function_">addImage</span>(pageData, <span class="string">&#x27;JPEG&#x27;</span>, <span class="number">0</span>, <span class="number">0</span>, imgWidth, imgHeight)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  <span class="comment">// 超过一页时，分页打印（每页高度841.89）</span></span><br><span class="line">  <span class="keyword">while</span> (leftHeight &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="variable constant_">PDF</span>.<span class="title function_">addImage</span>(pageData, <span class="string">&#x27;JPEG&#x27;</span>, <span class="number">0</span>, position, imgWidth, imgHeight)</span><br><span class="line">    leftHeight -= pageHeight</span><br><span class="line">    position -= <span class="number">841.89</span></span><br><span class="line">    <span class="keyword">if</span> (leftHeight &gt; <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="variable constant_">PDF</span>.<span class="title function_">addPage</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="分页截断的挑战"><a href="#分页截断的挑战" class="headerlink" title="分页截断的挑战"></a><strong>分页截断的挑战</strong></h2><p>尽管 JSPDF 和 html2canvas 是功能强大的工具，但是 他们也有很多槽点，比如得手动分页，手动处理分页截断的问题。等你实践到这一步，就开始面临分页截断的问题，类似的问题也有网友在Github上提出，但是底下依然没有很好的解决思路：<a href="https://github.com/parallax/jsPDF/issues/1517">https://github.com/parallax/jsPDF/issues/1517</a></p><p>好在掘金上有人分享了一个不错的方法：</p><p><a href="https://juejin.cn/post/7138370283739545613">jsPDF + html2canvas A4分页截断 完美解决方案（含代码 + 案例） - 掘金</a></p><p>概括一下，其处理分页截断的原理就是在使用addImage之前，将html进行分页，通过维护一个高度位置数据，来记录每次循环迭代addImage的位置。</p><blockquote><p><strong>从高到低</strong>遍历维护一个<strong>分页数组pages</strong>，该数组记录每一页的起始位置，如：<strong>pages[0]</strong> 对应 <strong>第一页起始位置</strong>， <strong>pages[1]</strong> 对应 <strong>第二页起始位置</strong></p></blockquote><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/d9dfea7c-1249-485f-9df8-fc89a94b113b/Untitled.png" alt="Untitled"></p><p>接下来我们重点讨论如何将html进行切割，然后生成<code>Pages</code>这个数组。</p><p>假设html高度是1500，pdf宽高是[500, 900]，如果不用处理分页截断的问题，我们可以想到第一页（0-900）是用来承载html从高度为0到900的信息；第二页（900-1800）是用来承载html从高度900到1500的，所以Pages数组为[0, 900]。</p><p>如果要处理分页截断呢，这时候就需要计算html元素的距离打印起始位置的高度<code>h1</code>，以及该元素的内部高度<code>h2</code>，通过这两个高度来判断这个元素要不要放在下一页，防止截断。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/fd747bbb-7960-4f94-af50-5f8a93643edf/Untitled.png" alt="Untitled"></p><p>如果<code>h1 + h2 &gt; 页面高度</code>， 这时候说明这个元素不处理的就会被分页截断，所以应该要把这个元素放到第二页去渲染，这就以为这<code>Pages</code>记录的数据要变化，示意图如下，可以看到<code>Pages[1]</code>我们往上调整了，比第二页pdf的起始位置更高。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/aeb35600-1bce-4640-b423-a7db8e27d1ba/Untitled.png" alt="Untitled"></p><p>说明渲染第二页pdf的时候，要从<code>h1</code>开始渲染，pages数组为<code>[0, h1]。</code>解释为第一页pdf渲染html高度区域为0-900, 第二页pdf渲染html高度区域为h1-1500。注意到第一页渲染的时候到尾部的时候，<strong>会有部分内容和第二页头部内容重合。</strong></p><p>为了解决这个问题，我们可以使用<code>jspdf</code>的<code>rect</code>和<code>setFillColor</code>方法，把重合的区域遮百处理。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pdf.<span class="title function_">setFillColor</span>(<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>);</span><br><span class="line">pdf.<span class="title function_">rect</span>(x, y, <span class="title class_">Math</span>.<span class="title function_">ceil</span>(_width), <span class="title class_">Math</span>.<span class="title function_">ceil</span>(_height), <span class="string">&#x27;F&#x27;</span>);</span><br></pre></td></tr></table></figure><h3 id="如何获得h1和h2"><a href="#如何获得h1和h2" class="headerlink" title="如何获得h1和h2"></a>如何获得h1和h2</h3><p>上面我们谈到了<code>h1</code>和<code>h2</code>，其中<code>h1</code>是元素盒子的上边距到打印区域的高度（比例缩放后的高度），<code>h2</code>是元素盒子的内部高度。</p><p>计算<code>h1</code>: <code>[getBoundingClientRect</code>方法](<a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect</a>)</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> rect = contentElement.<span class="title function_">getBoundingClientRect</span>() || &#123;&#125;;</span><br><span class="line"><span class="keyword">const</span> topDistance = rect.<span class="property">top</span>;</span><br><span class="line"><span class="keyword">return</span> topDistance;</span><br></pre></td></tr></table></figure><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/c75e3d32-abcd-4d60-b4ab-45a8b2451d2c/Untitled.png" alt="Untitled"></p><p>计算<code>h2</code>： <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight">offsetHeight方法</a></p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/b57228e1-d395-4d78-b281-ce63576ab6a4/Untitled.png" alt="Untitled"></p><p>值得注意的是，因为打印区域的html元素不一定是从窗口顶部开始，所以为了计算实际的<code>h1</code>(元素到打印区域的顶部距离），可以采用这样的方法：</p><ul><li>用<code>getBoundingClientRect</code>方法计算元素到窗口顶部的距离</li><li>循环打印之前将<code>pages</code>信息针对第一个元素进行一个高度校准。</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 对pages进行一个值的修正，因为pages生成是根据根元素来的，根元素并不是我们实际要打印的元素，而是element，</span></span><br><span class="line">  <span class="comment">// 所以要把它修正，让其值是以真实的打印元素顶部节点为准</span></span><br><span class="line">  <span class="keyword">const</span> newPages = pages.<span class="title function_">map</span>(<span class="function">(<span class="params">item</span>) =&gt;</span> item - pages[<span class="number">0</span>]);</span><br></pre></td></tr></table></figure><h2 id="源代码"><a href="#源代码" class="headerlink" title="源代码"></a>源代码</h2><p> 文章的最后，我基于<a href="https://juejin.cn/post/7138370283739545613">这篇博客</a>的思路，另写了一份demo，源代码如下：<a href="https://github.com/janice143/pdf-demo%E3%80%82%E4%B8%8E%E7%8E%B0%E6%9C%89%E6%96%87%E7%AB%A0%E4%B8%8D%E5%90%8C%E7%9A%84%E6%98%AF%EF%BC%8C%E6%9C%AC%E4%BB%93%E5%BA%93%E7%9A%84%E4%BB%A3%E7%A0%81%E7%89%B9%E7%82%B9%E5%9C%A8%E4%BA%8E%EF%BC%9A">https://github.com/janice143/pdf-demo。与现有文章不同的是，本仓库的代码特点在于：</a></p><ul><li>支持设置pdf打印的方向，比如横向</li><li>修正了高度计算问题，解决了多出一个空白页问题。掘金那篇文章计算元素高度时候没有减去容器距离顶部高度，所以导致很多新手使用那份代码的时候，会发现自己的页面顶部被裁剪到了，原因就是这个</li><li>支持自定义页眉页脚</li><li>支持扩展自定义分页方法，如果遇到复杂的组件，可以自定扩展逻辑计算高度</li></ul><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/9aef8349-a9a1-48a7-9cf7-381c53878bb8/Untitled.png" alt="Untitled"></p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/d0ecb1ba-e017-4a75-8be3-1c1dc49d73b1/Untitled.png" alt="Untitled"></p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/11/08/JSPDF-html2canvas-A4%E5%88%86%E9%A1%B5%E6%88%AA%E6%96%AD/</id>
    <link href="https://believed-breadfruit.top/2023/11/08/JSPDF-html2canvas-A4%E5%88%86%E9%A1%B5%E6%88%AA%E6%96%AD/"/>
    <published>2023-11-08T13:45:04.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>最近在业务上遇到了一个问题是要将页面打印pdf文件，产品的预期是希望点击一个按钮，就能够将页面数据写在一个pdf上，并下载下来，需要保证pd]]>
    </summary>
    <title>JSPDF + html2canvas A4分页截断</title>
    <updated>2024-12-22T06:30:35.615Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <content>
      <![CDATA[<p>作为一个初学者，第一次接触单测的时候，我其实有很多疑问，单测是什么，为什么要做单测，如何做单测，最佳实践是什么。</p><p>怎么罗列测试用例，如何调试代码</p><p>本文就是一篇浅浅的入门指南。主要围绕上面这些问题做一些解答和记录。</p><h2 id="单测是什么"><a href="#单测是什么" class="headerlink" title="单测是什么"></a>单测是什么</h2><p>单测就是单元测试，属于软件开发中的一个测试方法。正如其名字一样，单元测试关注的是代码中比较小的单元，可能是一个函数，一个方法，甚至一个属性。只要在逻辑上可以独立存在的，都是单元测试可进行的对象。</p><h2 id="Why"><a href="#Why" class="headerlink" title="Why"></a>Why</h2><p>减少上线bug，可以保证我们写的代码按照预想的执行。同时，对于代码维护也可得带来更高效率。</p><h2 id="How"><a href="#How" class="headerlink" title="How"></a>How</h2><p>主要围绕如何进行javascript代码的单元测试讲解。（单元测试是软件工程里的概念，不应该只局限与前端，由于网上的资源看起来很繁杂，我暂时没有建立起一个完整的单测认识，所以这篇文章暂时只局限于unit test with javascript）</p><h3 id="选择一个测试框架JavaScript-unit-testing-framework"><a href="#选择一个测试框架JavaScript-unit-testing-framework" class="headerlink" title="选择一个测试框架JavaScript unit testing framework"></a>选择一个测试框架JavaScript unit testing framework</h3><p>关于js的单测框架，目前使用最多的是Jest 和mocha，下面这张图给出了2016年到2021年框架使用比例图。（主要是js的发展很快，所以基于js的工具也要不断迭代和更新）</p><p><img src="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5038ccd6-04f5-4373-9086-6180b6292a03/Untitled.png" alt="Untitled"></p><p>如果第一次接触单测，肯定要选择使用最多的，因为参考资料多，不管是学习好还是开发过程中遇到问题，都能很容易找到相关的资料。还有一个好处是，使用多意味着这个工具被维护的可能性更高且更持久。</p><h3 id="Jest测试框架"><a href="#Jest测试框架" class="headerlink" title="Jest测试框架"></a>Jest测试框架</h3><p>有两个网站我推荐看，我相信会有很多收获。</p><p><a href="https://jestjs.io/docs/getting-started">Getting Started · Jest</a></p><p><a href="https://www.softwaretestinghelp.com/jest-testing-tutorial/">Jest Tutorial - JavaScript Unit Testing Using Jest Framework</a></p><p><a href="https://www.lambdatest.com/jest">Jest Tutorial: Complete Guide to Jest Testing</a></p><p>第三个文档我只简易看目录。具体内容我觉得他写的很简陋，所以不简易看。</p><p>首先根据官网的文档，可以着手写一个简单的单元测试。测试总的来说就是输入-预测输出-判断结果的过程（<strong>input - expected output - assert the result）</strong></p><p>我认为一个技术官网上的东西初学的时候不需要全部看完，技术官网是一个工具网站，我们可以把他看成是一个字典，遇到不认识的字的时候再使用它。对于一个技术网站，首先就是要快速了解这个技术最基本的几个东西是什么，比如说jest，最基本的就是如何输入命令使用、积累一些常用的matchers。如何判断哪些最基本，你可以根据js的基本知识来学习，在学js的时候，逻辑运算、字符串什么都是是常用的，所以学习jest的时候你需要了解一些对应的匹配器；页面上的交互经常涉及到一些click,change事件，这是jest有一些mock函数需要了解。等等。</p><h3 id="测什么"><a href="#测什么" class="headerlink" title="测什么"></a>测什么</h3><p>How do I know what to test?当你对jest有个初步的实操之后，下一个关注的问题是测什么？</p><p>如果是写一个页面的话，页面上每个组件、每个交互理论上都需要测试。如果自己罗列，难免有遗漏的地方，所以在jest中有个coverage的概念（<strong><strong>Code coverage in Jest）</strong></strong>，可以把它理解为Jest的一个命令或者工具，可以告诉我们代码中哪些需要测的地方。执行完这个命令后，甚至有一个可视化的Html页面可以直观地看到测试覆盖率。</p><h3 id="test-React-with-Jest"><a href="#test-React-with-Jest" class="headerlink" title="test React with Jest"></a><strong><strong>test React with Jest</strong></strong></h3><p>了解到这里，我们知道了简单的基于js的单元测试，使用的单测框架是jest。为了最佳实践，下一步我们要把单测和前端框架结合，以react框架为例，还是因为react使用的最多。</p><p>针对react组件库的测试，需要用到另一个有用工具</p><p><a href="https://testing-library.com/docs/react-testing-library/intro">React Testing Library | Testing Library</a></p><p>另外推荐阅读这两篇文章</p><p><a href="https://www.softwaretestinghelp.com/testing-react-apps-using-jest/">Jest React Tutorial - How To Test React Apps Using Jest Framework</a></p><p><a href="https://www.valentinog.com/blog/testing-react/">Testing React Components with react-test-renderer, and the Act API</a></p><p>我目前还是有疑问，snapshot到底在什么时候推荐使用。这个问题我还在思考中。尽管我搜索了不少答案，甚至问了chatgpt，我还是没有一个具体答案。</p><h3 id="如何调试"><a href="#如何调试" class="headerlink" title="如何调试"></a>如何调试</h3><p>不重要但是必不可少的是我们得知道如何调试代码。</p><p><a href="https://www.softwaretestinghelp.com/jest-configuration-and-debugging/">Jest Configuration And Debugging Jest Based Tests</a></p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a><strong>Conclusion</strong></h2><p>have fun！这篇文章并没有给出具体的命令，也没有代码实例，只是给出了一些初步的理解，以及如何找到最佳实践，其中给了一些参考博客。我觉得对于一个有基础的前端开发基础，但是缺乏软件工程完整体系，且想了解单侧的同学来说，这篇文章会有一点点帮助。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/11/06/%E3%80%90%E5%8D%95%E6%B5%8B%E3%80%91%E5%8D%95%E6%B5%8B%E5%85%A5%E9%97%A8/</id>
    <link href="https://believed-breadfruit.top/2023/11/06/%E3%80%90%E5%8D%95%E6%B5%8B%E3%80%91%E5%8D%95%E6%B5%8B%E5%85%A5%E9%97%A8/"/>
    <published>2023-11-05T16:20:45.000Z</published>
    <summary>
      <![CDATA[<p>作为一个初学者，第一次接触单测的时候，我其实有很多疑问，单测是什么，为什么要做单测，如何做单测，最佳实践是什么。</p>
<p>怎么罗列测试用例，如何调试代码</p>
<p>本文就是一篇浅浅的入门指南。主要围绕上面这些问题做一些解答和记录。</p>
<h2 id="单测是什]]>
    </summary>
    <title>【单测】单测入门 A Beginner's Guide to Writing Unit Tests for React Components with Jest and React Testing Library</title>
    <updated>2024-12-22T06:28:13.270Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://believed-breadfruit.top/tags/TypeScript/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript 的类型系统已经从基本的类型注释，发展成为一种大型且复杂的编程语言。在上篇文章中，我们打开了类型编程的潘多拉魔盒，我们知道TS的强大之处，不仅在于其类型检查系统，还在于其具有图灵完备特性，可以实现强大的类型级编程能力。</p><p>在学习一般的编程语言的时候，我们首先会学习变量声明和数据结构，其次是基本的代数运算、条件分支和循环，更高阶地，我们会接触到递归，通过递归，我们可以了解到排序、动态规划等各种算法。掌握这些特性可以让我们快速入门一门编程语言，并且编写出更复杂的应用。</p><p>TS类型编程也有类似的特性，本文继续延续类型编程这个话题，从类型编程角度，重点探讨TS中的递归，我们特别以实现计算斐波那契序列为例，展现出不同的实现算法，来感受TS类型编程的魅力。</p><p>应该指出的是，本文话题仅仅是一次有意思的学术探索，内容深度已经远远超出了我们日常对TS的普通使用，给出的代码不建议在生产环境中使用。</p><h2 id="类型编程的思维"><a href="#类型编程的思维" class="headerlink" title="类型编程的思维"></a>类型编程的思维</h2><p>TS编程语言可以看成两部分组成：</p><ul><li>普通编程：在运行时体现，使用值和函数</li><li>类型编程：在编译时体型，使用类型和泛型</li></ul><p>对于普通编程，我们经常涉及到的元素是</p><ul><li>数据结构</li><li>函数</li><li>控制流</li><li>循环</li><li>基本代数运算（加减乘除等）</li></ul><p>有了这些特性，大部分的编程语言都是图灵完备的，可以实现任何算法的编写，实现各种各样的复杂运算。TS的类型编程也是图灵完备的，这说明这些概念同样在类型编程的世界里也有体现。</p><ul><li>数据结构 → 对象和元祖</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 数组 -&gt; 元组类型</span></span><br><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line"><span class="comment">// 获取下标为1的元素，arr[1]</span></span><br><span class="line"><span class="keyword">const</span> v = arr[<span class="number">1</span>] <span class="comment">// 2</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Arr</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line"><span class="comment">// 通过下标索引类型获取某个类型元素，Arr[1]</span></span><br><span class="line"><span class="keyword">type</span> t = arr[<span class="number">1</span>] <span class="comment">// 2</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 对象 -&gt; 对象类型</span></span><br><span class="line"><span class="keyword">const</span> obj = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&quot;John&quot;</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="number">23</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> name = obj.<span class="property">name</span>; <span class="comment">// &#x27;John&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Obj</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Name</span> = <span class="title class_">Obj</span>[<span class="string">&quot;name&quot;</span>]; <span class="comment">// string</span></span><br></pre></td></tr></table></figure><ul><li>函数 → 泛型类型</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">f</span> = (<span class="params">num</span>) =&gt; num;</span><br><span class="line"><span class="keyword">const</span> v1 = <span class="title function_">f</span>(<span class="number">1</span>)(); <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">const</span> v2 = <span class="title function_">f</span>(<span class="string">&quot;str&quot;</span>)(); <span class="comment">// &#x27;str&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> F&lt;T&gt; = T;</span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">T1</span> = F&lt;<span class="built_in">number</span>&gt;; <span class="comment">// number</span></span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">T2</span> = F&lt;<span class="built_in">string</span>&gt;; <span class="comment">// string</span></span><br></pre></td></tr></table></figure><ul><li>控制流 → 条件类型<code>extends</code></li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> ans = <span class="number">2</span> === <span class="number">1</span> ? <span class="literal">true</span> : <span class="literal">false</span>;</span><br><span class="line"><span class="comment">// 如果2等于1，进入true分支，否则进入false分支</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Ans</span> = <span class="number">2</span> <span class="keyword">extends</span> <span class="number">1</span> ? <span class="literal">true</span> : <span class="literal">false</span>;</span><br><span class="line"><span class="comment">// 如果2（字面量类型）是1类型的子集，进入true分支，否则进入false分支</span></span><br></pre></td></tr></table></figure><ul><li>循环 → 函数式编程的递归</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">reverse</span> = (<span class="params">arr</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> n = arr.<span class="property">length</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; n / <span class="number">2</span>; i++) &#123;</span><br><span class="line">    [arr[i], arr[n - i - <span class="number">1</span>]] = [arr[n - i - <span class="number">1</span>], arr[i]];</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> arr;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"><span class="keyword">const</span> res = <span class="title function_">reverse</span>(arr); <span class="comment">// [5,4,3,2,1]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Reverse</span>&lt;</span><br><span class="line">  T <span class="keyword">extends</span> <span class="built_in">number</span>[],</span><br><span class="line">  N = T[<span class="string">&quot;length&quot;</span>],</span><br><span class="line">  U <span class="keyword">extends</span> <span class="built_in">number</span>[] = []</span><br><span class="line">&gt; = U[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> N ? U : <span class="title class_">Reverse</span>&lt;T, N, [T[U[<span class="string">&quot;length&quot;</span>]], ...U]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Target</span> = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Res1</span> = <span class="title class_">Reverse</span>&lt;<span class="title class_">Target</span>&gt;; <span class="comment">// [5,4,3,2,1]</span></span><br></pre></td></tr></table></figure><ul><li>基本代数运算（加减乘除等） → 通过访问元祖长度实现加减乘除</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 类型编程的加减乘除运算</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">BuildArr</span>&lt;</span><br><span class="line">  <span class="title class_">Num</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Arr</span> <span class="keyword">extends</span> <span class="built_in">unknown</span>[] = []</span><br><span class="line">&gt; = <span class="title class_">Arr</span>[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> <span class="title class_">Num</span> ? <span class="title class_">Arr</span> : <span class="title class_">BuildArr</span>&lt;<span class="title class_">Num</span>, [<span class="number">1</span>, ...<span class="title class_">Arr</span>]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 减法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Sub</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt; <span class="keyword">extends</span> [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: infer <span class="title class_">Rest</span></span><br><span class="line">]</span><br><span class="line">  ? <span class="title class_">Rest</span>[<span class="string">&quot;length&quot;</span>]</span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 加法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Add</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;</span><br><span class="line">][<span class="string">&quot;length&quot;</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 乘法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Mul</span>&lt;</span><br><span class="line">  <span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Res</span> <span class="keyword">extends</span> <span class="built_in">number</span> = <span class="number">0</span></span><br><span class="line">&gt; = <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="title class_">Res</span></span><br><span class="line">  : <span class="title class_">Mul</span>&lt;<span class="variable constant_">T1</span>, <span class="title class_">Sub</span>&lt;<span class="variable constant_">T2</span>, <span class="number">1</span>&gt;, <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="variable constant_">T1</span>&gt; <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="variable constant_">T1</span>&gt; : <span class="built_in">never</span>&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 除法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Div</span>&lt;</span><br><span class="line">  <span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Res</span> <span class="keyword">extends</span> <span class="built_in">number</span> = <span class="number">0</span></span><br><span class="line">&gt; = <span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="title class_">Res</span></span><br><span class="line">  : <span class="title class_">Div</span>&lt;<span class="title class_">Sub</span>&lt;<span class="variable constant_">T1</span>, <span class="variable constant_">T2</span>&gt;, <span class="variable constant_">T2</span>, <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="number">1</span>&gt; <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="number">1</span>&gt; : <span class="built_in">never</span>&gt;;</span><br></pre></td></tr></table></figure><p>可以看出，正是TS类型有条件分支和递归能力，才有了实现强大类型编程的基础。</p><h2 id="斐波那契序列的递归实现"><a href="#斐波那契序列的递归实现" class="headerlink" title="斐波那契序列的递归实现"></a>斐波那契序列的递归实现</h2><p>在学习一门新的编程语言时，我们通常以打印“Hello World”作为入门案例。同样，通过递归实现斐波那契数列，也可以看作是学习递归算法的一个“Hello World”级别的案例。</p><p>作为数学中一个重要的序列，斐波那契序列在理论和实践应用中都有重要价值。深入理解它可以帮助我们更全面地掌握数字模式和算法思路。</p><p>斐波那契序列具有这样的特点：除了前两个数字0和1之外，任意一个数字都等于前两个数字之和。数列的展开式为：0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987…。也就是说:</p><p>!<a href="https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230827/image.454v4zqwsaq0.png">https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230827/image.454v4zqwsaq0.png</a></p><p>实现斐波那契序列最简单的算法就是递归，下面是用JS编写的递归算法：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">fib</span> = (<span class="params">n</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">1</span>) <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">fib</span>(n - <span class="number">1</span>) + <span class="title function_">fib</span>(n - <span class="number">2</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">fib</span>(<span class="number">10</span>)); <span class="comment">// 55</span></span><br></pre></td></tr></table></figure><p>可以看到递归最重要的两个特性：</p><ul><li>base case：逃出递归的条件</li><li>调用自身</li></ul><p>TS类型编程中的递归和JS的递归其实没什么太大区别，也具有这两个特性，只不过语法不一样。下面的代码是TS的纯类型编程版本：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Fib1</span>&lt;N <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = N <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="number">0</span></span><br><span class="line">  : N <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? <span class="number">1</span></span><br><span class="line">  : <span class="title class_">Fib1</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">1</span>&gt;&gt; <span class="keyword">extends</span> infer t1 ? <span class="title class_">Add</span>&lt;t1 <span class="keyword">extends</span> <span class="built_in">number</span> ? t1 : <span class="built_in">never</span>, <span class="title class_">Fib1</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">2</span>&gt;&gt; <span class="keyword">extends</span> infer t2 ? t2 <span class="keyword">extends</span> <span class="built_in">number</span> ? t2 : <span class="built_in">never</span> : <span class="built_in">never</span>&gt; : <span class="built_in">never</span></span><br></pre></td></tr></table></figure><p>可以看到我们用两个<code>extends</code>条件类型给出了两种base case, 第三种情况是不断调用自身。在进行类型编程的时候，我们思路其实就是按照算法来编写，然后按照TS提供的特性来实现。因为TS类型编程不能修改类型的状态，而且只用”表达式”，不用”语句”，这两个特点和<strong>函数式编程</strong>一样，所以进行类型编程的时候，我们采用的是函数式编程思想。</p><ul><li>不能修改类型状态</li></ul><p>TS可以用<code>Infer</code>来存储一个类型，但是该类型不能被修改，保存新的类型值。普通编程语言一般都有<code>for</code>循环，这是因为他们定义的变量的状态可以改变，通过可变的状态来作为计数器实现循环，但是TS的类型编程没有<code>for</code>循环，要实现遍历则需要使用递归，通过递归函数的参数来保存或更新状态。</p><ul><li>只用”表达式”，不用”语句”</li></ul><p>TS的类型编程基本上只有一个表达式，例如<code>X extends Y ? A : B</code>，这个表达式是一个单纯的运算过程，总有返回值。从斐波那契递归算法的例子可以看到，我们写的类型编程其实也就是一个比较长的表达式，而且表达式里可能有别的表达式。</p><h2 id="动态规划和矩阵快速幂运算"><a href="#动态规划和矩阵快速幂运算" class="headerlink" title="动态规划和矩阵快速幂运算"></a>动态规划和矩阵快速幂运算</h2><p>递归方法具有很高的时间复杂度，因为有大量的重复计算。为了减少时间复杂度，斐波那契序列的计算也有很多优化算法，例如动态规划和矩阵快速幂运算。本节也用TS类型编程来实践一下。</p><h3 id="动态规划"><a href="#动态规划" class="headerlink" title="动态规划"></a>动态规划</h3><p>先讲动态规划，动态规划分两种：带有备忘录的自上而下和自下而上。下面给出了JS实现代码：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// top down</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">fibMemo</span> = (<span class="params">n, memo = [<span class="number">0</span>, <span class="number">1</span>]</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (memo[n] !== <span class="literal">undefined</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> memo[n];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  memo[n] = <span class="title function_">fibMemo</span>(n - <span class="number">1</span>, memo) + <span class="title function_">fibMemo</span>(n - <span class="number">2</span>, memo);</span><br><span class="line">  <span class="keyword">return</span> memo[n];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// bottom up</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">fib</span> = (<span class="params">n, arr = [<span class="number">0</span>, <span class="number">1</span>]</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">2</span>; i &lt;= n; i++) &#123;</span><br><span class="line">    arr[i] = arr[i - <span class="number">1</span>] + arr[i - <span class="number">2</span>];</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> arr[n];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>因为TS类型编程无法修改备忘录的状态，所以目前无法实现带有备忘录机制的纯类型编程。但是可以实现自下而上的动态规划，代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">FibBottomUp</span>&lt;N <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="title class_">Arr</span> <span class="keyword">extends</span> <span class="built_in">any</span>[]=[<span class="number">0</span>,<span class="number">1</span>]&gt; = </span><br><span class="line">  <span class="title class_">Arr</span>[N] <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">    ? <span class="title class_">Arr</span>[N]</span><br><span class="line">    : <span class="title class_">FibBottomUp</span>&lt;N, [ ...<span class="title class_">Arr</span>, ...[ <span class="title class_">Add</span>&lt;<span class="title class_">Arr</span>[<span class="title class_">Sub</span>&lt;<span class="title class_">Arr</span>[<span class="string">&#x27;length&#x27;</span>], <span class="number">1</span>&gt;], <span class="title class_">Arr</span>[<span class="title class_">Sub</span>&lt;<span class="title class_">Arr</span>[<span class="string">&#x27;length&#x27;</span>], <span class="number">2</span>&gt;]&gt; ]]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试</span></span><br><span class="line"><span class="keyword">type</span> res2 = <span class="title class_">FibBottomUp</span>&lt;<span class="number">10</span>&gt;; <span class="comment">// 55</span></span><br></pre></td></tr></table></figure><p>可以看到我们通过<code>Arr</code>的长度来实现循环迭代，元组<code>Arr</code>的每个下标<code>index</code>对应着<code>fib(index)</code>结果。每次计算计算<code>fib(n)</code>的时候，都会先计算<code>fib(n-1)</code>和<code>fib(n-2)</code>的值，这种思路就是自底向上的动态规划，求解最终问题之前，会从问题的子问题开始，逐步构建到最终问题。</p><h3 id="矩阵快速幂"><a href="#矩阵快速幂" class="headerlink" title="矩阵快速幂"></a>矩阵快速幂</h3><p>动态规划算法由于每个节点只会计算一次，所以时间复杂度是O(n)。矩阵快速幂比动态规划的时间复杂度更低。那么矩阵快速幂是怎么推导来的呢？</p><p>我们知道，斐波那契数列有递推公式：</p><p>$$<br>F_{n+2}&#x3D;F_{n+1}+F_n, n ∈ 0,1,2..<br>$$</p><p>把这个公式改写成矩阵运算，可以得到下面的式子：</p><p>$$<br>\begin{bmatrix}<br>F_{n+2}\<br>F_{n+1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}*\begin{bmatrix}<br>F_{n+1}\<br>F_{n}<br>\end{bmatrix}<br>$$</p><p>也就是说，当n&#x3D;0, 1, 2的时候，我们又下列公式，</p><p>$$<br>\begin{bmatrix}<br>F_{2}\<br>F_{1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}*\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>$$<br>\begin{bmatrix}<br>F_{3}\<br>F_{2}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}<em>\begin{bmatrix}<br>F_{2}\<br>F_{1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}^2</em>\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>$$<br>\begin{bmatrix}<br>F_{4}\<br>F_{3}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}<em>\begin{bmatrix}<br>F_{3}\<br>F_{2}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}^3</em>\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>所以对于<code>n</code>，我们有</p><p>$$<br>\begin{bmatrix}<br>F_{n}\<br>F_{n-1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}^{n-1}*\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>所以只要求解出幂次部分就可以求解<code>Fn</code>，而幂次部分求解可以使用快速幂公式，如下</p><p>$$<br>T^{n} &#x3D; T^{n&#x2F;2}*T^{n&#x2F;2}, n为偶数，\<br>T^{n} &#x3D; T^{n&#x2F;2}*T^{n&#x2F;2}*T, n为奇数，<br>$$</p><p>这说明要求解$T^n$只需求解$T^{n&#x2F;2}$即可，而求解$T^{n&#x2F;2}$，只需求解$T^{n&#x2F;4}$，依次递推，我们可以发现整个幂运算其实就是不断二分，时间复杂度就是二分法的时间复杂度，为<code>O(lgn)</code>.</p><p>知道了什么是矩阵快速幂，我们可以很容易得写出对应的类型代码：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Matrix</span> = [<span class="built_in">number</span>, <span class="built_in">number</span>, <span class="built_in">number</span>, <span class="built_in">number</span>]; <span class="comment">// [a, b, c, d] represents the matrix [[a, b], [c, d]]</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">MatrixMultiply</span>&lt;A <span class="keyword">extends</span> <span class="title class_">Matrix</span>, B <span class="keyword">extends</span> <span class="title class_">Matrix</span>&gt; = [</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">0</span>], B[<span class="number">0</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">1</span>], B[<span class="number">2</span>]&gt;&gt;,</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">0</span>], B[<span class="number">1</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">1</span>], B[<span class="number">3</span>]&gt;&gt;,</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">2</span>], B[<span class="number">0</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">3</span>], B[<span class="number">2</span>]&gt;&gt;,</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">2</span>], B[<span class="number">1</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">3</span>], B[<span class="number">3</span>]&gt;&gt;</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">MatrixPower</span>&lt;M <span class="keyword">extends</span> <span class="title class_">Matrix</span>, N <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = N <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? M</span><br><span class="line">  : <span class="title class_">IsOdd</span>&lt;N&gt; <span class="keyword">extends</span> <span class="literal">true</span></span><br><span class="line">  ? <span class="title class_">MatrixMultiply</span>&lt;</span><br><span class="line">      <span class="title class_">Div</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">1</span>&gt;, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span>,</span><br><span class="line">      <span class="title class_">Div</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">1</span>&gt;, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span></span><br><span class="line">    &gt; <span class="keyword">extends</span> infer <span class="variable constant_">R2</span></span><br><span class="line">    ? <span class="variable constant_">R2</span> <span class="keyword">extends</span> <span class="title class_">Matrix</span></span><br><span class="line">      ? <span class="title class_">MatrixMultiply</span>&lt;M, <span class="variable constant_">R2</span>&gt;</span><br><span class="line">      : <span class="built_in">never</span></span><br><span class="line">    : <span class="built_in">never</span></span><br><span class="line">  : <span class="title class_">MatrixMultiply</span>&lt;</span><br><span class="line">      <span class="title class_">Div</span>&lt;N, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span>,</span><br><span class="line">      <span class="title class_">Div</span>&lt;N, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span></span><br><span class="line">    &gt; <span class="keyword">extends</span> infer <span class="variable constant_">R2</span></span><br><span class="line">  ? <span class="variable constant_">R2</span> <span class="keyword">extends</span> <span class="title class_">Matrix</span></span><br><span class="line">    ? <span class="variable constant_">R2</span></span><br><span class="line">    : <span class="built_in">never</span></span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">FibMatrix</span>&lt;N <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = N <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="number">0</span></span><br><span class="line">  : N <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? <span class="number">1</span></span><br><span class="line">  : <span class="title class_">MatrixPower</span>&lt;[<span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">0</span>], N&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">  ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="title class_">Matrix</span></span><br><span class="line">    ? <span class="variable constant_">R1</span>[<span class="number">1</span>]</span><br><span class="line">    : <span class="built_in">never</span></span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Test</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">MatrixResult</span> = <span class="title class_">FibMatrix</span>&lt;<span class="number">15</span>&gt;; <span class="comment">// 610</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文给出了三种斐波那契序列算法的类型编程实现方式，最终的代码可以在查看这个<a href="https://www.typescriptlang.org/play#code/PTAEBcGdG8fRo9UNH9DQXoPO1ANzoBNNBHNoYoTCKmodO9BVmwChiRRBO00IEZAsf-AHsAHUAEwYHcA7UQK8DAoOUAwAYGg5QCFugCqVAgAaARyMBISsXABPJgFNQAMQCWAI2oAeAHKgVAD3AqurSKC4BXALbaVAJwB8oALygjp85esADMSgoAD8oEEhAFzexmYWVqDUwWFJKTFaunoAyrbahgA0Sa7uvgnWmlwAZi4Q1KkAgqyseuD1Zf42Dk7OqW2gMVwqAG4uRZn6ufkGRQBMJXF+iZU1veCzfRsdiXaOteHrAzYjtYMnbkdDo84p5ID+8oAUrqBNLRM5eYXFi+WgK7Wa5ns9XCrnGOkmHxmoHmC0AWPKAUqNAJip9GUakqkHAAEMuOBNJjcQweJprKYAMYqSCQTSjAA2ijYKhULGxrFATAYlJ0dN+1UqAJUACJjM5nAxnAA6UhKVSgZwUrxvagAVnc5AAbNQgmQwFRZrReMJBPwJDJ5NK1JkAEIMcCMewAVSYhm+nV2PSKDRFLsS2MUAG0ALoeQPuLwpT3OP0GAPe6xulwpELhCNRgOJo5Wm12x2fP2gcUFiNFAvigAUKYA5DSLABzcAACwrMe2gVAAB8kqk-ZXq1w642AzGYnmXnoU1MxyK-VXaw2m0VqK4Ax6pxOe7OB3Ml+4AwBKAOuADcUtRGh01ttDAdTFmzpbXT2zhXvXvvsDwYCBWoB88oHDU+jWMHx6dNkwAtMQmiM9tAvbMb1zfNCxFYsCxHZpJ0jNcpxnPs52XYp8PHD511wzdoW3UBByPE8ZTlSANgVc8syvR1b01I9QHIJUlVIbVKEIABmWhAEv3QBWNMAa-1AHwEwAhPQUU8AFl8WcTQTF-P14yfYCxi0zSNIDQ9OLAP1MSKbQilJIpWBjOUmDoiwoAges1HsJSVNAP1jNM-C-QsthBzkmVFPAZSTHk2waVxJg6THICgpCopLVi1yTFDdzw3QsKaTHP0Anwy0coPIpMuy78Er9WYD1BdKWmKhoCrK79QVAWq-VK0B8oEyqCmqvQWoqsrcqalrOrKiqSm6kJRz6vLWsK5rwuykb2r9TqSmIfTSHIQBYOUeQAYuUAMCVCFEMTpMAQuj8FEKhACDNYRaD481muSgAFTgXF6pLgpUoofHiV1uhcVKfqWaxknIJNmr4qDSzilSXo4N75KKCcoUXUpfuWapagAJWBEIcY+kLUhh0Lwsi6LEdAHH3DOa5LnOXdjz4sSpNkh7ibhhGCa+2J7w0wGgOScH5PSUAAElIAAeXQgw0eBiBnFsFQUnCYnMrJxQ9HTEIABFqXeaYFya+YgL+Xoca1yDwnx3n-puSD7ctx7PpMDnnF6ooqYt+2aYTB3IJ9p8Ld14Z9c+RctxNzGzcFv3Umt9G41tr3HfZ163Ypz3Y-945rmTgP01ln5Tcp2ZQJLrmTAtlXkrVzQoo1jP5gt-PvZz32jlV0m6+ioO9ahY372L82s6t9oE505OheetP3cpxdJ7p3Os4DiaHeDz4B-HoeY790egI0hfq+d13Z8zrPF-bv2W8gwvOiH0vwaxrZx+Jsun-Ta+A8ZgKLR0Ym73HnzX8QMfiRFSGAmIIDOgx3CDHGIqd4Zu1aguFBER8Iy0jqsOeys54VzftQWaH8252wvs4Rm5AAAqFJwA-ydiFLGFJSa-kyP-ZUHF1Sal4uQQAX+qABpzQAx3KAAdTQAC-H4EAKHxgBKpUANs2sA4C0InBQsecsNJFAoc-ZRttUqWlsJoGkrAIx6EUbfRIfoUglkxCKagMRtG6P0SKQx8xV7mJFLMGIQ9qHrRwQwjEfoBS9n7AKCCpDGYPVHIo-ettVHqJ+EArwpiQjOOcFY9qOi9EGKMU4gsFjnCuJSbY9JTcAzTn8XhEJCkFrhJto+KJETqmUwpLUnoHgAipTUUBAIccKTwIqdQJGHw1FfiaqObxBQjGNP2M8dCIyxlRCuADUAZSZTrxSJUwBkSVnRL+tUlI3jxm9C8EEVpSjQFeIpCLdeCjemgDUU1AZkyWgjK+FUnojQpkUkNqQ6iIQaJqBsWk+xKQDAOD2avCMQFbBcAANZcE4FwQMak0ypRTH4jcgSgJAvsI0L01jUl2LdhiooyDELigjAeRZahxZSxaFAnYmjgECz6ArNQMRKXoWRgUGE+9zipCqJiGkkBmVi0lmyyEHK1oPXFgAUVGFwABGjHz83vB0g4TKjhSplaHGYnLebcvCLy-lgr1UWE1WK1wxAgA">playground</a>。</p><p>虽然TS的类型编程很强大，但是在编写的时候，我不得不指出一些体感不好的地方，比如很难debug，使用递归会经常遇到<code>”type instantiation is excessively deep and possibly infinite&quot; error.“</code>报错，这个时候就得通过<code>infer</code>来定义一个局部类型，再给这个类型加一个条件判断来解决，这会让代码更加复杂难懂。除此之外，我觉得类型编程的性能不高，而且难以衡量，比如当你想计算更大的<code>n</code>的时候，上述给出的三种方法可能会算不出来，返回一个<code>any</code>类型。如果想要看结果，只能通过把鼠标hover到这个类型上查看，没有一个console.log之类的方法打印出来。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/24/TS%E5%AE%9E%E7%8E%B0%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97%E7%AE%97%E6%B3%95/</id>
    <link href="https://believed-breadfruit.top/2023/10/24/TS%E5%AE%9E%E7%8E%B0%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97%E7%AE%97%E6%B3%95/"/>
    <published>2023-10-24T13:46:54.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript 的类型系统已经从基本的类型注释，发展成为一种大型且复杂的编程语言。在上篇文章中，我们打开了类型编程的潘多拉魔盒，我们知]]>
    </summary>
    <title>TS实现斐波那契序列算法</title>
    <updated>2024-12-22T06:31:01.977Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="FormData" scheme="https://believed-breadfruit.top/tags/FormData/"/>
    <content>
      <![CDATA[<p>文件上传是我们在业务中经常遇到的实战场景。本文从自顶向下的角度讲解文件上传的原理。</p><h2 id="表现层"><a href="#表现层" class="headerlink" title="表现层"></a>表现层</h2><p>我们简单写一个”上传文件demo“，来开始学习”文件上传“的知识点。</p><p>通过下面这个命令构建一个基本的nextjs应用。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx create-next-app@latest</span><br></pre></td></tr></table></figure><p>之后通过<code>npm install antd</code>安装<code>antd</code>组件库，因为我们会使用它的upload组件。</p><p>安装好后，修改pages.tsx文件，代码如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Button</span>, <span class="title class_">Upload</span>, <span class="title class_">UploadProps</span>, message &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">FILE_PATH</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./constants&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> styles <span class="keyword">from</span> <span class="string">&#x27;./page.module.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Home</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">props</span>: <span class="title class_">UploadProps</span> = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;file&#x27;</span>,</span><br><span class="line">    <span class="attr">action</span>: <span class="string">&#x27;/api/upload&#x27;</span>,</span><br><span class="line">    <span class="title function_">onChange</span>(<span class="params">info</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (info.<span class="property">file</span>.<span class="property">status</span> !== <span class="string">&#x27;uploading&#x27;</span>) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(info.<span class="property">file</span>, info.<span class="property">fileList</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (info.<span class="property">file</span>.<span class="property">status</span> === <span class="string">&#x27;done&#x27;</span>) &#123;</span><br><span class="line">        message.<span class="title function_">success</span>(</span><br><span class="line">          <span class="string">`<span class="subst">$&#123;info.file.name&#125;</span> 文件上传成功, 前往 <span class="subst">$&#123;FILE_PATH&#125;</span> 查看上传文件`</span></span><br><span class="line">        );</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (info.<span class="property">file</span>.<span class="property">status</span> === <span class="string">&#x27;error&#x27;</span>) &#123;</span><br><span class="line">        message.<span class="title function_">error</span>(<span class="string">`<span class="subst">$&#123;info.file.name&#125;</span> file upload failed.`</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">main</span> <span class="attr">className</span>=<span class="string">&#123;styles.main&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span>&gt;</span>文件上传demo<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Upload</span> &#123;<span class="attr">...props</span>&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Button</span>&gt;</span>上传文件<span class="tag">&lt;/<span class="name">Button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Upload</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">main</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>写法其实和react差不多，唯一不同的是在文件顶部，我们写了<code>&#39;use client&#39;;</code> 来声明这个文件是一个<a href="https://nextjs.org/docs/app/building-your-application/rendering/client-components">客户端组件</a>。</p><p>写好了客户端部分的代码，接下来我们开始写服务端的<code>/api/upload</code>接口，nextjs是一个全栈应用框架，通过<a href="https://nextjs.org/docs/app/api-reference/file-conventions/route">route handler</a>，我们可以快速写一个rest风格的接口。</p><p>我们在app文件夹下，新建一个upload文件夹，在upload文件夹中，我们新建一个route.ts文件，该文件的代码如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">FILE_PATH</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@/app/constants&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; writeFile &#125; <span class="keyword">from</span> <span class="string">&#x27;fs/promises&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">NextRequest</span>, <span class="title class_">NextResponse</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next/server&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; join &#125; <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">POST</span>(<span class="params">request: NextRequest</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> request.<span class="title function_">formData</span>();</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">file</span>: <span class="title class_">File</span> | <span class="literal">null</span> = data.<span class="title function_">get</span>(<span class="string">&#x27;file&#x27;</span>) <span class="keyword">as</span> unknown <span class="keyword">as</span> <span class="title class_">File</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!file) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">NextResponse</span>.<span class="title function_">json</span>(&#123; <span class="attr">message</span>: <span class="string">&#x27;请上传文一个件&#x27;</span>, <span class="attr">code</span>: <span class="number">400</span> &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> bytes = <span class="keyword">await</span> file.<span class="title function_">arrayBuffer</span>();</span><br><span class="line">  <span class="keyword">const</span> buffer = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(bytes);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> path = <span class="title function_">join</span>(<span class="variable constant_">FILE_PATH</span>, file.<span class="property">name</span>);</span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">writeFile</span>(path, buffer);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`open <span class="subst">$&#123;path&#125;</span> to see the uploaded file`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">NextResponse</span>.<span class="title function_">json</span>(&#123; <span class="attr">data</span>: <span class="literal">null</span>, <span class="attr">message</span>: <span class="string">&#x27;success&#x27;</span>, <span class="attr">code</span>: <span class="number">200</span> &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过运行<code>npm run dev</code> 命令，我们运行该项目。界面如下：</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/55b394b4-532e-4b78-9145-6944203609df/Untitled.png" alt="Untitled"></p><p>点击上传文件，选择一个文件，可以看到文件成功上传了，打开dev tool查看接口详情。我们可以看到一个在预期之内的upload请求，请求头的content-type如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Content</span>-<span class="title class_">Type</span>: multipart/form-data; boundary=----<span class="title class_">WebKitFormBoundaryiI4</span>jCWvT8XkA2pgg</span><br></pre></td></tr></table></figure><p>请求参数：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Form</span> <span class="title class_">Data</span></span><br><span class="line"><span class="attr">file</span>: (binary)</span><br></pre></td></tr></table></figure><p>以上是一个基本的上传文件实践。在这个过程中，我们观察到了antd的upload组件在调upload接口的时候，会将请求的content-type转化成<code>multipart/form-data</code>, 并且文件数据以form data的数据结构传递给服务器，服务器为了解析文件类型与类型，也以formdata对应的解析形式进行解析（在nextjs中，我们使用nextjs 内置的request.formData来解析表单数据）。</p><p>我们的demo代码已经放到github仓库中：<a href="https://github.com/janice143/file-upload-demo">https://github.com/janice143/file-upload-demo</a></p><h2 id="HTTP协议：客户端和服务器传输数据的规范"><a href="#HTTP协议：客户端和服务器传输数据的规范" class="headerlink" title="HTTP协议：客户端和服务器传输数据的规范"></a>HTTP协议：客户端和服务器传输数据的规范</h2><p>文件上传的流程其实就是客户端和服务器进行数据交换的过程，而客户端与服务器通信遵循HTTP协议，包括请求和响应的格式、状态码、header字段等。</p><h3 id="客户端"><a href="#客户端" class="headerlink" title="客户端"></a>客户端</h3><p>在文件上传的流程中，前端通过XMLHttpRequest请求，设置Content-Type这个请求头字段，并取值为<code>multipart/form-data</code>。**<code>Content-Type</code><strong>头部告诉服务器如何解析主体数据。例如，</strong><code>Content-Type: text/html</code><strong>表示主体数据是HTML文档，而</strong><code>Content-Type: application/json</code><strong>表示主体数据是JSON格式的数据。</strong><code>Content-Type:** multipart/form-data</code>在告诉服务器要用解析form data的形式解析才能获取文件。</p><h3 id="multipart-form-data"><a href="#multipart-form-data" class="headerlink" title="multipart/form-data"></a><code>multipart/form-data</code></h3><p><code>multipart/form-data</code> 是一种**MIME类型，<strong>浏览器使用MIME类型（而不是文件扩展符）来区分文件类型，通常由一个主类型（type）和一个子类型（subtype）组成，例如</strong><code>text/html</code><strong>表示HTML文档，</strong><code>image/jpeg</code>**表示JPEG图像。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">type/subtype</span><br></pre></td></tr></table></figure><p>对于type来讲，一般有分为两种：discrete and multipart**。**discrete类型表示单文件，常见的有application, audio, image, text等。multipart表示有多个文件，一个合成文件，这些部分可以有自己的 MIME 类型。分为两种: message, multipart。虽然multipart用于处理多文件的场景，但是在文件上传的时候，不管是单文件还是多文件，我们都是用<code>multipart/form-data</code>这个类型，这是为什么呢？</p><p>因为<code>multipart</code>可以进行传输原始的二进制文件，在这之前，文件上传最早是使用<code>application/x-www-form-urlencoded</code>，这就要求客户端在上传文件之前对文件进行 URL 编码。如果文件主要是 ASCII 文本，则 URL 编码是有效的，但如果文件主要是二进制数据，则必须对每个字节进行都 URL 转义，这是非常低效的。</p><p>所以在<a href="https://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>提案中，最早提出了<code>multipart/form-data</code>这一MIME来兼容文件上传的表示，它允许您在一个 HTTP 正文中发送多个文件，而无需对它们进行编码。</p><p>所以总结下来，调用上传接口的时候设置form-data请求头，是为了告诉服务器客户端要给你传输一个文件，而为什么要使用form-data这个content-type，这是http协议的一个规范，为了实现客户端和服务器之间高效的文件传输。</p><p>在dev tool中，我们的请求体其实还包含一个boundary字段，这是“边界定界符”，用来分割多个文件，这样服务器就能知道每个文件的边界。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Content</span>-<span class="title class_">Type</span>: multipart/form-data; boundary=----<span class="title class_">WebKitFormBoundaryiI4</span>jCWvT8XkA2pgg</span><br></pre></td></tr></table></figure><p>我们上述的代码是使用了antd封装的upload组件，文件上传的时候本质是发送了一个异步的AJAX请求。在这种技术之前，我们最早是利用form表单来实现文件上传。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;form method=<span class="string">&quot;POST&quot;</span> enctype=<span class="string">&quot;multipart/form-data&quot;</span>&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;file&quot;</span> <span class="attr">name</span>=<span class="string">&quot;file&quot;</span> <span class="attr">value</span>=<span class="string">&quot;请选择文件&quot;</span>&gt;</span><span class="tag">&lt;<span class="name">br</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>可见，在form表单中，我们需要手动将表单数据编码格式设置为 <code>multipart/form-data</code> 类型，这个类型也是同样遵守http协议的规范。</p><p>为什么<code>XMLHttpRequest</code>无需手动设置这个请求头类型呢？</p><p>因为使用了浏览器提供的FormData构造函数。FormData对象是用于存储键值对的数据结构，是为了保存表单数据而设计的。formData的特殊用途在于进行网络请求（如<code>fetch</code>, <code>XMLHttpRequest</code>)，网络请求可以接受formData对象，并将其编码并用 <code>Content-Type: multipart/form-data</code> 发送给服务器。</p><p>所以在dev tool中，我们可以看到请求参数是form data, 其key为file, value是一个二进制文件。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/8d5d06a7-b6ec-4936-964e-db673e58daa3/Untitled.png" alt="Untitled"></p><h3 id="服务器端"><a href="#服务器端" class="headerlink" title="服务器端"></a>服务器端</h3><p>在服务器端，主要是接受和处理客户端发送的文件数据。</p><ol><li><strong>接收请求：</strong> 服务器接收来自客户端的HTTP POST请求，请求头部中包括**<code>Content-Type</code><strong>为</strong><code>multipart/form-data</code>**的标识。</li><li><strong>解析数据：</strong> 服务器使用**<code>multipart/form-data</code>**格式解析请求体中的数据。这个格式将数据划分为多部分，每个部分包含一个表单字段或一个文件。</li><li><strong>处理文件：</strong> 对于文件部分，服务器会将其保存到指定的位置，通常是文件系统中的某个目录。文件名和路径通常是根据服务器端的需求和配置而定。</li><li><strong>返回响应：</strong> 服务器处理完文件后，会向客户端发送响应，通常包括状态码（如200表示成功）和一些附加信息。</li></ol><p>服务器端的文件上传流程可以根据具体的后端框架和语言而有所不同，但基本原理是相似的。通常，服务器端会提供文件处理的API或路由，以接收和处理上传的文件。</p><h2 id="非Form-Data形式上传的文件"><a href="#非Form-Data形式上传的文件" class="headerlink" title="非Form Data形式上传的文件"></a>非Form Data形式上传的文件</h2><p>客户端和服务器之间上传文件并不限于使用FormData格式。FormData是一种常用的方法，但还有其他方式可以进行文件上传。</p><p>例如可以将文件内容转换为Base64编码，然后将其作为文本数据上传。服务器端可以将Base64解码为原始文件。</p><p>如果后端需要的数据是其他形式的二进制文件，如果blob, array buffer，客户端可以通过浏览器提供的Blob和File实例格式化数据，服务器为了能够识别这些二进制文件，可以才有适配的方法，例如使用Node.js（Express）接收Blob文件，可以使用中间件来处理文件上传。以下是一个基本示例：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">const</span> express = <span class="built_in">require</span>(<span class="string">&#x27;express&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">express</span>();</span><br><span class="line"><span class="keyword">const</span> multer = <span class="built_in">require</span>(<span class="string">&#x27;multer&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 配置文件上传</span></span><br><span class="line"><span class="keyword">const</span> storage = multer.<span class="title function_">memoryStorage</span>();</span><br><span class="line"><span class="keyword">const</span> upload = <span class="title function_">multer</span>(&#123; storage &#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理文件上传</span></span><br><span class="line">app.<span class="title function_">post</span>(<span class="string">&#x27;/upload&#x27;</span>, upload.<span class="title function_">single</span>(<span class="string">&#x27;file&#x27;</span>), <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> blobData = req.<span class="property">file</span>.<span class="property">buffer</span>; <span class="comment">// Blob数据可以在req.file.buffer中找到</span></span><br><span class="line">  <span class="comment">// 在这里对blobData进行处理，保存到磁盘或数据库</span></span><br><span class="line">  res.<span class="title function_">status</span>(<span class="number">200</span>).<span class="title function_">json</span>(&#123; <span class="attr">message</span>: <span class="string">&#x27;File uploaded successfully&#x27;</span> &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">listen</span>(<span class="number">3000</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Server is running on port 3000&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>在这个示例中，**<code>multer</code><strong>中间件用于处理文件上传，</strong><code>req.file.buffer</code>**包含了Blob数据。</p><p>客户端和服务器进行文件传输的其他常见方式还有一种是<strong>基于WebSocket的文件上传</strong>，WebSocket协议允许双向通信，因此可以使用WebSocket来实现实时文件上传和流传输。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要介绍了基于form data形式的文件上传原理。从一个实例出发，解释了multipart&#x2F;form-data类型的文件传输协议，通过解释MIME type规范的意义，我们进一步延伸了非form data类型的文件上传。总之，文件上传本质上就是一个浏览器和服务器通信的问题，最重要的是把握的是二者之间需要遵守什么规范，才能保证客户端正确地发送文件，服务器正确地接受文件。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/23/%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%EF%BC%9A%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E7%90%86%E8%A7%A3FormData%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/</id>
    <link href="https://believed-breadfruit.top/2023/10/23/%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%EF%BC%9A%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E7%90%86%E8%A7%A3FormData%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/"/>
    <published>2023-10-23T13:46:08.000Z</published>
    <summary>
      <![CDATA[<p>文件上传是我们在业务中经常遇到的实战场景。本文从自顶向下的角度讲解文件上传的原理。</p>
<h2 id="表现层"><a href="#表现层" class="headerlink" title="表现层"></a>表现层</h2><p>我们简单写一个”上传文件demo“]]>
    </summary>
    <title>文件上传：自顶向下理解FormData文件上传的工作原理</title>
    <updated>2024-04-22T17:22:13.407Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://believed-breadfruit.top/tags/TypeScript/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript（TS）的类型系统十分强大，除了定义了各种类型方便我们对变量、函数、类等进行类型约束，实现类型安全，而且还支持泛型，可以很大程度实现代码复用。另外，为了迎合JS语言的灵活特性，保证更安全的类型检查，TS还提供了很多类型操作符，可以帮助我们实现强大的类型编程，避免<code>any</code>类型的滥用。</p><p>本文主要讲解TS的类型编程。事实上，在编写TS的时候，我们很容易发现，除了要编写业务本身的复杂逻辑，有的时候类型逻辑写起来也很复杂。如何更好地理解TS类型编程，让TS的类型逻辑不会成为代码编写的负担，是我们很关心的一个问题。</p><p>本文的主要观点是将TS看成由两种语言组成：一种是普通的编程语言JS，另一种是类型语言。在JS中，我们的主要操作对象是值，通过声明变量对值进行引用和存储，并使用函数对值进行各种计算、处理和转换。</p><p>在类型语言中，我们的关注点从普通编程语言中对值的操作转移到了对类型的操作。通过类型注解和泛型，类型成为一等公民，可以实现类型的赋值、组合和转换，从而产生各种不同的类型。</p><p>当我们在编写TS的时候，我们其实是在这两种语言中反复转换：我们用普通的JS语言编写逻辑，处理各种值数据；我们用类型编程创建各种类型，并使用类型注释约束JS中的各种元素。</p><p>!<a href="https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230814/image.1aoi2cvm2u2o.png">https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230814/image.1aoi2cvm2u2o.png</a></p><h2 id="静态类型系统的泛型"><a href="#静态类型系统的泛型" class="headerlink" title="静态类型系统的泛型"></a>静态类型系统的泛型</h2><p>在静态类型系统中，泛型是一种参数化类型的机制，意思说我们可以通过将类型看成参数，用一个符号来表示多种类型，从而编写出可以复用的类型代码。</p><p>泛型的关键是将类型作为参数（类型参数），传递给代码单元，如函数、类、接口，在其内部，我们使用的是参数化的类型，而不是某一个具体类型。在类型理论中，泛型背后的理论是多态（<strong><a href="https://en.wikipedia.org/wiki/Polymorphism_(computer_science)#Row_polymorphism">Polymorphism</a>）</strong>，即通过一个单一的符号来表示多个不同类型，或者通过单一的接口描述不同的数据类型实体。泛型通过参数多态成为实现多态的方式之一。</p><p>在没有泛型之前，我们需要多次定义来满足不同类型的情况：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> t1 = <span class="function">(<span class="params">a: <span class="built_in">number</span></span>) =&gt;</span> <span class="built_in">number</span>;</span><br><span class="line"><span class="keyword">type</span> t2 = <span class="function">(<span class="params">a: <span class="built_in">string</span></span>) =&gt;</span> <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">f1</span>: t1 = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br><span class="line"><span class="keyword">const</span> <span class="attr">f2</span>: t2 = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br></pre></td></tr></table></figure><p>有了泛型后，我们使用尖括号（<code>&lt;&gt;</code>）和类型参数来定义一个泛型：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 泛型函数1：类似普通函数的定义</span></span><br><span class="line"><span class="keyword">const</span> generics_f1 = &lt;T&gt;(<span class="attr">a</span>: T): <span class="function"><span class="params">T</span> =&gt;</span> a;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 泛型函数2：泛型类型</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">generics_f2</span>: &lt;T&gt;<span class="function">(<span class="params">a: T</span>) =&gt;</span> T = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 泛型函数3：对象字面量形式的泛型类型</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">generics_f3</span>: &#123; &lt;T&gt;(<span class="attr">a</span>: T): T &#125; = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> f3 = generics_f1&lt;<span class="built_in">number</span>&gt;;</span><br><span class="line"><span class="keyword">const</span> f4 = generics_f1&lt;<span class="built_in">string</span>&gt;;</span><br></pre></td></tr></table></figure><p>不仅TS的类型系统支持泛型，很多其他编程语言也支持，如Java, C#，GO等，所以对于计算机语言来讲，泛型并不是一个很新的概念。但是TS类型系统提供的能力远远超过一般的泛型，在TS类型编程中，我们可以基于一个类型构建另一个类型，限制类型的范围，甚至对传入的类型参数进行各种逻辑运算。</p><p>在上面这片代码中，我们定义的泛型很简单，输入是什么类型，输出就是什么类型，但是对于十分灵活的JS来讲，这种简单的泛型远远不够保证类型安全，因为大部分的场景是这样的：我们输入的类型T，最终需要通过某种逻辑运算，转化成类型U，我们用数学的函数表达式来描述T和U的关系<code>U=F(U)</code>.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="attr">generics_complex_f</span>: &lt;T&gt;<span class="function">(<span class="params">a: T</span>) =&gt;</span> U = <span class="function">(<span class="params">a</span>) =&gt;</span> b;</span><br><span class="line"><span class="comment">// U = F(T), b = f(a)</span></span><br></pre></td></tr></table></figure><p>TS的类型编程主要内容，就是按照TS给定的语言规则，编写<code>U=F(U)</code>的代码实现。</p><h2 id="类型编程与普通编程"><a href="#类型编程与普通编程" class="headerlink" title="类型编程与普通编程"></a>类型编程与普通编程</h2><p>普通编程语言中有的思想，在类型编程编程中可以找到类似的实现。</p><h3 id="类型注解和赋值"><a href="#类型注解和赋值" class="headerlink" title="类型注解和赋值"></a>类型注解和赋值</h3><p>在JS中，我们的数据是值，可以通过<code>=</code>将数据赋值给变量。在TS中，我们的数据是类型，通过类型注解<code>:</code>，可以为变量、函数参数、函数返回值等明确指定类型，从而约束其取值范围和操作。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="attr">message</span>: <span class="built_in">string</span> = <span class="string">&quot;Hello, World!&quot;</span>;</span><br></pre></td></tr></table></figure><h3 id="变量声明"><a href="#变量声明" class="headerlink" title="变量声明"></a>变量声明</h3><p>JS 使用关键字 <code>var</code> 、 <code>const</code> 和 <code>let</code> 声明一个变量。在TS类型语言中，我们使用<code>type</code> 和 <code>interface</code> 声明类型变量。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> user = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&quot;Bob&quot;</span>,</span><br><span class="line">  <span class="attr">login</span>: <span class="string">&quot;robert&quot;</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="number">30</span>,</span><br><span class="line">  <span class="attr">comments</span>: [],</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">User</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">login</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">  <span class="attr">comments</span>: <span class="title class_">Comment</span>[];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>局部变量<code>infer</code></strong></p><p>JS的变量声明具有作用域，类型语言也有作用域，用<code>type</code>和<code>interface</code>创建的变量具有全局作用域，要创建局部作用域，可以使用<code>infer</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> A = <span class="string">&#x27;foo&#x27;</span>; <span class="comment">// 全局作用域</span></span><br><span class="line"><span class="keyword">type</span> B = A <span class="keyword">extends</span> infer C ? (</span><br><span class="line">    C <span class="keyword">extends</span> <span class="string">&#x27;foo&#x27;</span> ? <span class="literal">true</span> : <span class="literal">false</span> <span class="comment">// 局部作用域：C类型变量只在该表达式中有用</span></span><br><span class="line">) : <span class="built_in">never</span></span><br></pre></td></tr></table></figure><p>在上面这片代码中，类型B的结果可以看成两个过程：</p><ol><li>将 <code>A extends &#39;foo&#39;</code> 的结果，通过<code>infer</code>缓存在C中</li><li>根据<code>C ? true : false</code>将结果带入，计算类型B。</li></ol><h3 id="条件分支"><a href="#条件分支" class="headerlink" title="条件分支"></a>条件分支</h3><p>类型语言和JS语言都可以使用三元运算符：<code>condition ? trueExpression : falseExpression</code> ，如果条件成立，那么会进入<code>trueExpression</code>分支，否则进入<code>falseExpression</code>分支。在JS中，判断条件成立可以使用<code>==</code>、 <code>===</code>或者逻辑运算符。</p><p>在类型语言中，使用 <code>extends</code> 关键字来判断条件是否成立，<code>X extends Y</code> 可以理解为”<code>X</code>是<code>Y</code>的子集吗？“，如果是，进入<code>trueExpression</code>分支，否则进入<code>falseExpression</code>分支。</p><p>注：对于<code>X extends Y</code> ，如果<code>X</code>是联合类型，结果会按照分布规则进行计算，这一点可以参考<a href="https://juejin.cn/post/7264549320362934309">这篇文章</a>更好理解。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> R = <span class="string">&#x27;a&#x27;</span> <span class="keyword">extends</span> <span class="built_in">string</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> S = <span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;b&#x27;</span> <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> T = &#123; <span class="attr">a</span>: <span class="number">1</span>; <span class="attr">b</span>: <span class="number">2</span> &#125; <span class="keyword">extends</span> &#123; <span class="attr">a</span>: <span class="number">1</span> &#125; ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> U = &#123; <span class="attr">a</span>: <span class="number">1</span> &#125; <span class="keyword">extends</span> &#123; <span class="attr">a</span>: <span class="number">1</span>; <span class="attr">b</span>: <span class="number">2</span> &#125; ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br></pre></td></tr></table></figure><h3 id="索引：检索属性值和属性类型"><a href="#索引：检索属性值和属性类型" class="headerlink" title="索引：检索属性值和属性类型"></a>索引：检索属性值和属性类型</h3><p>在 JS 中，我们可以使用方括号访问对象属性值，例如 <code>obj[&#39;prop&#39;]</code> ，或者根据下标获取数组的值。TS的类型语言也提供了方括号运算符，可以用来获取对象类型的属性类型，或者对元祖和数组进行索引。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 对象</span></span><br><span class="line"><span class="keyword">const</span> person = &#123; <span class="attr">age</span>: <span class="number">12</span>; <span class="attr">name</span>: <span class="string">&#x27;John&#x27;</span>; <span class="attr">alive</span>: <span class="literal">true</span> &#125;;</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Age</span> = person[<span class="string">&quot;age&quot;</span>]; <span class="comment">// 12</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Person</span> = &#123; <span class="attr">age</span>: <span class="built_in">number</span>; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">alive</span>: <span class="built_in">boolean</span> &#125;;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Age</span> = <span class="title class_">Person</span>[<span class="string">&quot;age&quot;</span>]; <span class="comment">// number</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 数组（元祖）</span></span><br><span class="line"><span class="keyword">const</span> names = [<span class="string">&#x27;John&#x27;</span>, <span class="string">&#x27;Bob&#x27;</span>];</span><br><span class="line"><span class="keyword">const</span> name1 = names[<span class="number">0</span>] <span class="comment">// John</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Names</span> = <span class="built_in">string</span>[]</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Name</span> = <span class="title class_">Names</span>[<span class="built_in">number</span>] <span class="comment">// string</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Name1</span> = <span class="title class_">Names</span>[<span class="number">1</span>] <span class="comment">// string</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Tuple</span> = [<span class="built_in">string</span>, <span class="built_in">number</span>]</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Age</span> = <span class="title class_">Tuple</span>[<span class="number">1</span>] <span class="comment">// string</span></span><br></pre></td></tr></table></figure><p>对类型语言中，方括号中使用的索引是一个类型（索引类型），这意味着我们可以使用<code>union</code>, <code>keyof</code>等其他类型操作符对索引类型进行改造，从而获得更多类型。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Person</span> = &#123; <span class="attr">age</span>: <span class="built_in">number</span>; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">alive</span>: <span class="built_in">boolean</span> &#125;;</span><br><span class="line">     </span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Age</span> = <span class="built_in">number</span></span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">I1</span> = <span class="title class_">Person</span>[<span class="string">&quot;age&quot;</span> | <span class="string">&quot;name&quot;</span>]; <span class="comment">// number | string</span></span><br><span class="line">      </span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">I2</span> = <span class="title class_">Person</span>[keyof <span class="title class_">Person</span>]; <span class="comment">// number | string | boolean</span></span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"><span class="keyword">type</span> <span class="title class_">AliveOrName</span> = <span class="string">&quot;alive&quot;</span> | <span class="string">&quot;name&quot;</span>;</span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">I3</span> = <span class="title class_">Person</span>[<span class="title class_">AliveOrName</span>]; <span class="comment">// string | boolean</span></span><br></pre></td></tr></table></figure><h3 id="泛型和函数"><a href="#泛型和函数" class="headerlink" title="泛型和函数"></a>泛型和函数</h3><p>函数是JS等编程语言的核心之一，具有入参和出参，通过传入一些特定值，会根据内部逻辑转化成新的值，并输出。在TS类型语言中，泛型是类型编程中非常重要的概念，它允许我们像JS编写函数一样，编写出能够适用于多种类型的代码。</p><p>我们以TS内置的<code>pick</code>方法为例，该方法接口两个参数，<code>T</code>和<code>K</code>，其中<code>T</code>一般是一个对象类型，<code>K</code>为该类型的某个属性，<code>pick&lt;T,K&gt;</code>可以获取T对象中，属性为K的对象转成的新类型：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">Todo</span> &#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">description</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">completed</span>: <span class="built_in">boolean</span>;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">type</span> <span class="title class_">TodoPreview</span> = <span class="title class_">Pick</span>&lt;<span class="title class_">Todo</span>, <span class="string">&quot;title&quot;</span> | <span class="string">&quot;completed&quot;</span>&gt;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// type TodoPreview = &#123;</span></span><br><span class="line"><span class="comment">//     title: string;</span></span><br><span class="line"><span class="comment">//     completed: boolean;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br></pre></td></tr></table></figure><p>在JS中，尝试写一个<code>pick</code>函数，入参也是两个，第一个是对象，第二个是对象的属性列表，函数返回值是具有这些属性的新对象，该函数可以写成如下这样：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> todo = &#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="string">&quot;1&quot;</span>,</span><br><span class="line">  <span class="attr">description</span>: <span class="string">&quot;1&quot;</span>,</span><br><span class="line">  <span class="attr">completed</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">pick</span> = (<span class="params">obj, keys</span>) =&gt; keys.<span class="title function_">map</span>(<span class="function">(<span class="params">key</span>) =&gt;</span> (&#123; [key]: obj[key] &#125;));</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> res = <span class="title function_">pick</span>(todo, [<span class="string">&quot;title&quot;</span>, <span class="string">&quot;completed&quot;</span>]);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(res); <span class="comment">// [ &#123; title: &#x27;1&#x27; &#125;, &#123; completed: true &#125; ]</span></span><br></pre></td></tr></table></figure><p>在类型编程中，需要把所有值看成类型，类型的遍历用<code>in keyof</code>操作符，用索引类型获取对应属性的类型，通过<code>extends</code>实现属性名的限制，所以类型编程版本的 <code>pick</code>方法的实现如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Pick</span>&lt;T, K <span class="keyword">extends</span> keyof T&gt; = &#123;</span><br><span class="line">  [P <span class="keyword">in</span> K]: T[P];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>默认参数</strong></p><p>和JS函数参数有默认值一样，TS的类型编程也可以定义一个默认参数：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> getName&lt;T=<span class="built_in">string</span>&gt;(<span class="attr">name</span>: T): T &#123;</span><br><span class="line">  <span class="keyword">return</span> name;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> name1 = getName&lt;<span class="built_in">string</span>&gt;(<span class="string">&quot;myString&quot;</span>); <span class="comment">// myString</span></span><br><span class="line"><span class="keyword">let</span> name2 = <span class="title function_">getName</span>(<span class="string">&quot;myString&quot;</span>); <span class="comment">//myString</span></span><br><span class="line"><span class="keyword">let</span> name3 = getName&lt;<span class="built_in">number</span>&gt;(<span class="number">1</span>); <span class="comment">// 1</span></span><br></pre></td></tr></table></figure><h3 id="遍历和过滤"><a href="#遍历和过滤" class="headerlink" title="遍历和过滤"></a>遍历和过滤</h3><p>在JS中，我们可以通过<code>for in</code> 遍历对象。在遍历的过程中，我们可以修改原数组，例如</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> obj = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&quot;John&quot;</span>,</span><br><span class="line">  <span class="attr">gender</span>: <span class="string">&quot;male&quot;</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="number">23</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> key <span class="keyword">in</span> obj) &#123;</span><br><span class="line">  obj[key + <span class="string">&quot;_suffix&quot;</span>] = obj[key];</span><br><span class="line">  <span class="keyword">delete</span> obj[key];</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(obj); <span class="comment">// &#123; name_suffix: &#x27;John&#x27;, gender_suffix: &#x27;male&#x27;, age_suffix: 23 &#125;</span></span><br></pre></td></tr></table></figure><p>TS的类型编程则可以通过 <code>[K in keyof T]</code> 实现对象类型的遍历，并通过<code>as</code> 将遍历后的类型转化为新类型：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Obj</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>,</span><br><span class="line">  <span class="attr">gender</span>: <span class="built_in">string</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">NewObj</span>&lt;T&gt; =&#123;</span><br><span class="line">  [T <span class="keyword">in</span> keyof <span class="title class_">Obj</span> <span class="keyword">as</span> <span class="string">`<span class="subst">$&#123;T&#125;</span>_suffix`</span>]: <span class="title class_">Obj</span>[T];</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// type NewObj&lt;T&gt; = &#123;</span></span><br><span class="line"><span class="comment">//     name_suffix: string;</span></span><br><span class="line"><span class="comment">//     gender_suffix: string;</span></span><br><span class="line"><span class="comment">//     age_suffix: number;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>如果要过滤某些属性，JS可以使用<code>delete</code>方法，而在类型编程中，则通过<code>as</code>将类型转为<code>never</code>实现剔除某些属性。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">NewObj1</span>&lt;T&gt; =&#123;</span><br><span class="line">  [T <span class="keyword">in</span> keyof <span class="title class_">Obj</span> <span class="keyword">as</span> <span class="title class_">Obj</span>[T] <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="built_in">never</span> : T ]: <span class="title class_">Obj</span>[T];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// type NewObj1&lt;T&gt; = &#123;</span></span><br><span class="line"><span class="comment">//     name: string;</span></span><br><span class="line"><span class="comment">//     gender: string;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br></pre></td></tr></table></figure><h3 id="模式匹配"><a href="#模式匹配" class="headerlink" title="模式匹配"></a>模式匹配</h3><p>在JS的正则表达式中，括号具有一个很重要的作用，就是用来进行模式提取，可以通过<code>$1</code><em>、</em><code>$2</code><em>、</em><code>$3</code> **指代相应的分组，例如下面代码中，<code>$1</code>表示第一个分组<code>\d&#123;4&#125;</code>，<code>$2</code>表示第一个分组<code>\d&#123;2&#125;</code>，<code>$3</code>表示第一个分组<code>\d&#123;2&#125;</code>。通过字符串的<code>replace</code>方法，我们可以实现字符串的位置替换。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> regex = <span class="regexp">/(\d&#123;4&#125;)-(\d&#123;2&#125;)-(\d&#123;2&#125;)/</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="built_in">string</span> = <span class="string">&quot;2023-08-13&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> result = <span class="built_in">string</span>.<span class="title function_">replace</span>(regex, <span class="string">&quot;$2/$3/$1&quot;</span>);</span><br></pre></td></tr></table></figure><p>在TS类型语言中，主要通过<code>extends</code>,<code>infer</code>以及扩展运算符等组合使用实现模式匹配：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Str</span> = <span class="string">&#x27;foo-bar&#x27;</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Bar</span> = <span class="title class_">Str</span> <span class="keyword">extends</span> <span class="string">`foo-<span class="subst">$&#123;infer rest&#125;</span>`</span> ? rest : <span class="built_in">never</span> <span class="comment">// &#x27;bar&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Arr</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>]</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">First</span> = <span class="title class_">Arr</span> <span class="keyword">extends</span> [infer F, ...infer <span class="title class_">Rest</span>] ? F : <span class="built_in">never</span> <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Last</span> = <span class="title class_">Arr</span> <span class="keyword">extends</span> [...infer <span class="title class_">Rest</span>, infer L] ? L : <span class="built_in">never</span> <span class="comment">// 5</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Mid</span> = <span class="title class_">Arr</span> <span class="keyword">extends</span> [infer F, ...infer <span class="title class_">Rest</span>, infer L] ? <span class="title class_">Rest</span> : <span class="built_in">never</span> <span class="comment">// [2,3,4]</span></span><br></pre></td></tr></table></figure><p>通过类型匹配，我们使用TS类型编程来模拟JS中的各种数组和字符串方法。具体举例可以参考<a href="https://juejin.cn/book/7047524421182947366?scrollMenuIndex=1"><strong>神光</strong>的掘金小册</a>，里面有一个章节是讲模式提取。</p><h3 id="函数递归"><a href="#函数递归" class="headerlink" title="函数递归"></a>函数递归</h3><p>像很多纯函数式编程语言一样，TS的类型语言虽然没有<code>for</code>循环实现数据迭代，但是有递归，递归可以代替了循环迭代。</p><p>如果用JS，使用递归来实现一个数组的翻转，代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> target = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">reverse</span> = (<span class="params">target, n = target.length, arr = []</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (arr.<span class="property">length</span> === n) &#123;</span><br><span class="line">    <span class="keyword">return</span> arr;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> pre = arr;</span><br><span class="line">  <span class="keyword">const</span> cur = target[arr.<span class="property">length</span>];</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">reverse</span>(target, n, [cur, ...pre]);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> res = <span class="title function_">reverse</span>(target);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(res); <span class="comment">// [5,4,3,2,1]</span></span><br></pre></td></tr></table></figure><p>类比到类型编程中，我们使用条件分支、默认参数、递归来实现成等价的类型编码，代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> reverse&lt;</span><br><span class="line">  T <span class="keyword">extends</span> <span class="built_in">number</span>[],</span><br><span class="line">  N = T[<span class="string">&quot;length&quot;</span>],</span><br><span class="line">  U <span class="keyword">extends</span> <span class="built_in">number</span>[] = []</span><br><span class="line">&gt; = U[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> N ? U : reverse&lt;T, N, [T[U[<span class="string">&quot;length&quot;</span>]], ...U]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> target = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"><span class="keyword">type</span> res1 = reverse&lt;target&gt; <span class="comment">// [5,4,3,2,1]</span></span><br></pre></td></tr></table></figure><p>看到这里是不是很惊讶，原来TS的类型编码和JS编码其实一模一样，只不是换了一套语法规则！让我们再用斐波那契递归来体验一把。</p><p>一个简单的斐波那契数列，其用JS实现的算法如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">fib</span> = (<span class="params">n</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">1</span>) <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">fib</span>(n - <span class="number">1</span>) + <span class="title function_">fib</span>(n - <span class="number">2</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">fib</span>(<span class="number">6</span>)); <span class="comment">// 8</span></span><br></pre></td></tr></table></figure><p>对应的TS类型编码如下：可以看到类型编码的实现比JS更加复杂一点，因为要实现加减操作，我们需要定义额外的类型实现。在TS的类型编码中，我们可以根据数组类型的<code>length</code>获取到数字，然后通过<code>模式匹配</code>以及<code>扩展运算符</code>实现数组长度的加减，进行实现基本的代数运算。</p><p>试试这个 👉👉 <a href="https://www.typescriptlang.org/play#code/C4TwDgpgBAYglgIwDwBUoQB7AgOwCYDOUOArgLYIQBOAfFALxRqbb5EAMAUFFAPxRceALibosuQlACM3PtNkiAgnjxJ4yAMolkKADTSaNfeqRad+gEyGaAbk6dQkKGdRSxrSaQrV9KC+4kiL0paBigAIRI4ABs8RSoqVzoWQKgAbVkAOmyAQwSpEUiYuITUK10s3ISLETgcADNqKAAlCAJgTgBdWX5W9rSAImjcAHNgAAsB7uFiCAA3ajsHcGhlVRQ3FLZichDffy3PXeo6RgyebMy8qgKIqNj4xI2jSqvqwvuSp6sutIByYY4MbjP6dJaOaBFB6lWQAOXIAW2wR8skeiMkJBwAGscAB7ADuODSnTCxM4pygj0GgOBU3RRHhZDkaI+xUeSEZ+jSUn0l0enVs9ghUCobUYJgAbHQAPTSqAADiAA">playground</a> 👈👈</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Fib</span>&lt;T <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = T <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="number">0</span></span><br><span class="line">  : T <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? <span class="number">1</span></span><br><span class="line">  : <span class="title class_">Add</span>&lt;<span class="title class_">Fib</span>&lt;<span class="title class_">Sub</span>&lt;T, <span class="number">1</span>&gt;&gt;, <span class="title class_">Fib</span>&lt;<span class="title class_">Sub</span>&lt;T, <span class="number">2</span>&gt;&gt;&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Sub</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt; <span class="keyword">extends</span> [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: infer <span class="title class_">Rest</span></span><br><span class="line">]</span><br><span class="line">  ? <span class="title class_">Rest</span>[<span class="string">&quot;length&quot;</span>]</span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Add</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;</span><br><span class="line">][<span class="string">&#x27;length&#x27;</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">BuildArr</span>&lt;</span><br><span class="line">  <span class="title class_">Num</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Arr</span> <span class="keyword">extends</span> <span class="built_in">unknown</span>[] = []</span><br><span class="line">&gt; = <span class="title class_">Arr</span>[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> <span class="title class_">Num</span> ? <span class="title class_">Arr</span> : <span class="title class_">BuildArr</span>&lt;<span class="title class_">Num</span>, [<span class="number">1</span>, ...<span class="title class_">Arr</span>]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> res= <span class="title class_">Fib</span>&lt;<span class="number">6</span>&gt; <span class="comment">// 8</span></span><br></pre></td></tr></table></figure><p>写到这里，我们见证了TS的类型系统强大的类型编程能力，事实上，TS的潜力远不止上述这些主要的类型操作，目前已经有不少文章证明了TS的类型系统具有<a href="https://itnext.io/typescript-and-turing-completeness-ba8ded8f3de3">图灵完备</a>（Turing completeness) 特性，一个系统具有图灵完备特性，意味着它可以表达各种算法，实现各种逻辑运算。</p><p>所以我们可以理解，TS的类型编程其实已经远远超出了简单的、用一个已知的类型（或者<code>union</code>, <code>intersection</code>, <code>record</code>后的类型)给JS变量进行注解的范围，用TS实现的类型编码确实可以变得异常复杂（但是这种复杂大部分生产环境用不到）。</p><p>如果想更近一步探索TS极限类型编程，我觉得<a href="https://www.learningtypescript.com/articles/extreme-explorations-of-typescripts-type-system">这篇文章</a>很不错，其中列举了用纯TS类型编程实现的应用案例，如模拟一个4比特的虚拟机、SQL数据库引擎、国际象棋等等。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文主要讲解了TS的泛型和类型编程。通过类比JS这类普通编程语言中的基本特性，我们在TS的类型编程中找到了对应的实现方式。</p><p>尽管类型编程比普通编程更复杂，甚至被很戏谑为类型体操，但是这复杂的原因，其实是这种技术JSer们很新鲜，而且不常用。我们接触的大部分语言的类型系统，支持基本的泛型其实就足够了。但是为了契合高度灵活的JS，TS还需要更加灵活的类型编程系统。</p><p>另外，使用类型进行编程，比起传统的利用各种API和操作符来实现对数据值的处理，也是一个需要适应的思维转换。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/19/TS%E7%B1%BB%E5%9E%8B%E7%BC%96%E7%A8%8B%EF%BC%9A%E7%B1%BB%E5%9E%8B%E6%98%AF%E4%B8%80%E7%AD%89%E5%85%AC%E6%B0%91/</id>
    <link href="https://believed-breadfruit.top/2023/10/19/TS%E7%B1%BB%E5%9E%8B%E7%BC%96%E7%A8%8B%EF%BC%9A%E7%B1%BB%E5%9E%8B%E6%98%AF%E4%B8%80%E7%AD%89%E5%85%AC%E6%B0%91/"/>
    <published>2023-10-19T13:47:36.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript（TS）的类型系统十分强大，除了定义了各种类型方便我们对变量、函数、类等进行类型约束，实现类型安全，而且还支持泛型，可以]]>
    </summary>
    <title>TS类型编程：类型是一等公民</title>
    <updated>2024-12-22T06:31:08.046Z</updated>
  </entry>
</feed>
