<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Link</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://linkdiary.com/</id>
  <link href="https://linkdiary.com/" rel="alternate"/>
  <link href="https://linkdiary.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Link</rights>
  <subtitle>Link Diary</subtitle>
  <title>林克日记</title>
  <updated>2026-05-31T09:20:18.999Z</updated>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-chart-line"></i><p>某天打开 GitHub 通知，看到一个用户提的 issue 标题写着：</p><blockquote><p>建议投稿到「阮一峰的科技爱好者周刊」</p></blockquote><p>我笑了笑，没太当回事 —— 一个刚开源不久的小工具，能被周刊收录的概率并不大。</p><p>但几周之后，我真的在 <a href="https://github.com/ruanyf/weekly/blob/master/docs/issue-397.md">issue-397</a> 里看到了自己的项目。那一刻，比拿到 star 还开心 —— 因为这意味着，一个原本只是为了「让前端不再绕道 Python 拿股票数据」而写的小工具，被更多需要它的人看到了。</p></div><p>随之而来的变化也很真实：</p><ul><li>📦 版本一路迭代到 <strong>v1.10.0</strong></li><li>⭐ GitHub 收获 <strong>940+ stars</strong></li><li>🐛 收到并解决了 <strong>10+ 个 issues</strong> —— 从 “希望支持板块概念”、”希望支持基金行情”，到 “希望搜索接口返回股票&#x2F;ETF 类型”，每一条都让 SDK 变得更扎实</li></ul><p>这篇文章想做的事很简单：把 <code>stock-sdk</code> 当前的全景能力，完整地讲一遍。如果你是前端工程师、量化爱好者、做行情看板的独立开发者，或者正在用 AI 工具搭金融小应用，希望它能帮你少走点弯路。</p><hr><h2 id="它解决了什么问题？"><a href="#它解决了什么问题？" class="headerlink" title="它解决了什么问题？"></a>它解决了什么问题？</h2><p>如果你是前端开发者，大概率遇到过这些场景：</p><ul><li>想做个行情看板 &#x2F; 个股监控 demo，但发现成熟的工具基本都在 <strong>Python 生态</strong>（akshare、tushare、efinance……）</li><li>不想为了拿点行情数据，专门维护一个 Node &#x2F; Python 后端</li><li>财经接口返回的格式五花八门：腾讯返回 <code>~</code> 分隔字符串、东方财富返回 GBK 编码、新浪要处理 jsonp 包裹…</li><li>AkShare 很强，但它是 Python 的，浏览器里跑不起来</li></ul><p><code>stock-sdk</code> 想做的事情很纯粹：</p><blockquote><p><strong>让前端工程师，用最熟悉的 JavaScript &#x2F; TypeScript，优雅地获取股票行情数据。</strong></p></blockquote><p>零依赖、双端运行（浏览器 + Node.js ≥ 18）、完整 TypeScript 类型，装上就能用。</p><figure class="highlight ts"><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">import</span> &#123; <span class="title class_">StockSDK</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;stock-sdk&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> sdk = <span class="keyword">new</span> <span class="title class_">StockSDK</span>();</span><br><span class="line"><span class="keyword">const</span> quotes = <span class="keyword">await</span> sdk.<span class="title function_">getSimpleQuotes</span>([<span class="string">&#x27;sh000001&#x27;</span>, <span class="string">&#x27;sz000858&#x27;</span>, <span class="string">&#x27;sh600519&#x27;</span>]);</span><br><span class="line"></span><br><span class="line">quotes.<span class="title function_">forEach</span>(<span class="function"><span class="params">q</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;q.name&#125;</span>: <span class="subst">$&#123;q.price&#125;</span> (<span class="subst">$&#123;q.changePercent&#125;</span>%)`</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>10 行代码，浏览器或 Node 都能跑。</p><hr><h2 id="全景能力一览"><a href="#全景能力一览" class="headerlink" title="全景能力一览"></a>全景能力一览</h2><p>经过十几个版本的迭代，<code>stock-sdk</code> 现在覆盖的能力面已经相当宽。下面按维度拆开讲。</p><h3 id="1-多市场实时行情"><a href="#1-多市场实时行情" class="headerlink" title="1. 多市场实时行情"></a>1. 多市场实时行情</h3><p>A 股、港股、美股、公募基金，一套 SDK 全覆盖：</p><table><thead><tr><th>方法</th><th>覆盖范围</th></tr></thead><tbody><tr><td><code>getFullQuotes</code> &#x2F; <code>getSimpleQuotes</code></td><td>A 股 &#x2F; 指数全量 + 简要行情</td></tr><tr><td><code>getHKQuotes</code></td><td>港股</td></tr><tr><td><code>getUSQuotes</code></td><td>美股</td></tr><tr><td><code>getFundQuotes</code></td><td>公募基金</td></tr></tbody></table><p>而且 SDK 内置批量并发拉取能力 —— 一次调用就能拿到全市场 5000+ A 股的行情：</p><figure class="highlight ts"><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> allQuotes = <span class="keyword">await</span> sdk.<span class="title function_">getAllAShareQuotes</span>(&#123;</span><br><span class="line">  <span class="attr">batchSize</span>: <span class="number">300</span>,</span><br><span class="line">  <span class="attr">concurrency</span>: <span class="number">5</span>,</span><br><span class="line">  <span class="attr">onProgress</span>: <span class="function">(<span class="params">completed, total</span>) =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;completed&#125;</span>/<span class="subst">$&#123;total&#125;</span>`</span>),</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>无需后端服务，浏览器里直接跑通。</p><hr><h3 id="2-K-线数据：日线-周线-月线-分钟线-分时"><a href="#2-K-线数据：日线-周线-月线-分钟线-分时" class="headerlink" title="2. K 线数据：日线 &#x2F; 周线 &#x2F; 月线 &#x2F; 分钟线 &#x2F; 分时"></a>2. K 线数据：日线 &#x2F; 周线 &#x2F; 月线 &#x2F; 分钟线 &#x2F; 分时</h3><p>不止是当前行情，历史 K 线和盘中分时也都有：</p><ul><li><code>getHistoryKline</code> &#x2F; <code>getHKHistoryKline</code> &#x2F; <code>getUSHistoryKline</code> —— 三大市场的日 &#x2F; 周 &#x2F; 月 K 线</li><li><code>getMinuteKline</code> —— 1 &#x2F; 5 &#x2F; 15 &#x2F; 30 &#x2F; 60 分钟 K 线</li><li><code>getTodayTimeline</code> —— 当日分时走势</li></ul><p>港股、美股的 K 线类型从 v1.9.1 起拆分为 <code>HKHistoryKline</code> &#x2F; <code>USHistoryKline</code>，各自带 <code>currency</code> 和时区元信息 —— 不用再手动处理「这条数据是哪国货币、几点开盘」的繁琐细节。</p><hr><h3 id="3-技术指标：14-个常用指标内置"><a href="#3-技术指标：14-个常用指标内置" class="headerlink" title="3. 技术指标：14 个常用指标内置"></a>3. 技术指标：14 个常用指标内置</h3><p>K 线拿到了，下一步通常是算指标。SDK 内置了一整套：</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">MA / EMA / WMA / MACD / BOLL / KDJ / RSI / WR</span><br><span class="line">BIAS / CCI / ATR / OBV / ROC / DMI / SAR / KC</span><br></pre></td></tr></table></figure><p>最方便的是 <code>getKlineWithIndicators</code> —— 一次返回 K 线 + 你指定的指标，不用自己再算一遍：</p><figure class="highlight ts"><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> data = <span class="keyword">await</span> sdk.<span class="title function_">getKlineWithIndicators</span>(&#123;</span><br><span class="line">  <span class="attr">code</span>: <span class="string">&#x27;sh600519&#x27;</span>,</span><br><span class="line">  <span class="attr">period</span>: <span class="string">&#x27;day&#x27;</span>,</span><br><span class="line">  <span class="attr">indicators</span>: [<span class="string">&#x27;MA(5)&#x27;</span>, <span class="string">&#x27;MA(20)&#x27;</span>, <span class="string">&#x27;MACD&#x27;</span>, <span class="string">&#x27;BOLL&#x27;</span>],</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><hr><h3 id="4-板块数据：行业-概念"><a href="#4-板块数据：行业-概念" class="headerlink" title="4. 板块数据：行业 + 概念"></a>4. 板块数据：行业 + 概念</h3><p><code>getIndustryList</code> &#x2F; <code>getIndustrySpot</code> &#x2F; <code>getIndustryConstituents</code> &#x2F; <code>getIndustryKline</code> &#x2F; <code>getIndustryMinuteKline</code> —— 行业板块的「列表 &#x2F; 行情 &#x2F; 成分股 &#x2F; K 线 &#x2F; 分时」一套打通。</p><p>概念板块同理，5 个对称的 <code>getConcept*</code> 方法。</p><p>如果你做过板块轮动的 demo，应该知道凑齐这套数据有多麻烦。SDK 把这一层抹平了。</p><hr><h3 id="5-期货-期权：相对小众但完整"><a href="#5-期货-期权：相对小众但完整" class="headerlink" title="5. 期货 &amp; 期权：相对小众但完整"></a>5. 期货 &amp; 期权：相对小众但完整</h3><p>很多人不知道这套 SDK 也支持期货和期权：</p><ul><li><strong>期货</strong>：国内期货 K 线、全球期货实时行情 + K 线、期货库存数据、COMEX 黄金&#x2F;白银库存</li><li><strong>期权</strong>：中金所股指期权 T 型报价、ETF 期权当日分钟 &#x2F; 历史日 K &#x2F; 5 日分钟、商品期权报价 + K 线、期权龙虎榜</li></ul><p>对做衍生品研究、写策略 demo 的开发者，这一块很顶用。</p><hr><h3 id="6-资金流向-北向资金"><a href="#6-资金流向-北向资金" class="headerlink" title="6. 资金流向 &amp; 北向资金"></a>6. 资金流向 &amp; 北向资金</h3><p>A 股玩家最关心的资金面数据，全在这：</p><ul><li><code>getIndividualFundFlow</code> &#x2F; <code>getMarketFundFlow</code> —— 个股、大盘资金流历史</li><li><code>getFundFlowRank</code> &#x2F; <code>getSectorFundFlowRank</code> —— 今日 &#x2F; 3 日 &#x2F; 5 日 &#x2F; 10 日资金流排名</li><li><code>getNorthboundMinute</code> &#x2F; <code>getNorthboundHoldingRank</code> &#x2F; <code>getNorthboundHistory</code> &#x2F; <code>getNorthboundIndividual</code> —— 北向 &#x2F; 南向资金的分时、汇总、持股排行、历史、个股持仓</li></ul><hr><h3 id="7-龙虎榜-大宗交易-融资融券"><a href="#7-龙虎榜-大宗交易-融资融券" class="headerlink" title="7. 龙虎榜 + 大宗交易 + 融资融券"></a>7. 龙虎榜 + 大宗交易 + 融资融券</h3><p>游资跟踪、机构动向、杠杆资金，这套国内独有的市场数据也覆盖：</p><ul><li>龙虎榜：详情、个股统计、机构买卖、营业部排行、席位明细</li><li>大宗交易：市场总览、明细、按股票每日统计</li><li>融资融券：账户统计 + 标的明细</li></ul><hr><h3 id="8-涨停板-盘口异动"><a href="#8-涨停板-盘口异动" class="headerlink" title="8. 涨停板 &#x2F; 盘口异动"></a>8. 涨停板 &#x2F; 盘口异动</h3><p>A 股短线玩家高频需要的「涨停板池」和「盘口异动」：</p><ul><li><code>getZTPool</code> —— 涨停 &#x2F; 昨日涨停 &#x2F; 强势 &#x2F; 次新 &#x2F; 炸板 &#x2F; 跌停，6 大股池</li><li><code>getStockChanges</code> —— 22 种盘口异动（火箭发射、大笔买入、封涨停……）</li><li><code>getBoardChanges</code> —— 当日板块异动详情</li></ul><hr><h3 id="9-基金深度数据（v1-10-0-新增）"><a href="#9-基金深度数据（v1-10-0-新增）" class="headerlink" title="9. 基金深度数据（v1.10.0 新增）"></a>9. 基金深度数据（v1.10.0 新增）</h3><p>最新版本针对公募基金做了一次大幅扩展，<strong>这块是阮一峰周刊那波关注之后最常被催的能力之一</strong>（issue #16：希望支持基金行情）：</p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td><code>getFundNavHistory</code></td><td>历史净值（单位 + 累计），全历史一次返回</td></tr><tr><td><code>getFundEstimate</code></td><td>当日实时估值（含 T-1 单位净值 + 盘中估算）</td></tr><tr><td><code>getFundRankHistory</code></td><td>同类排名走势（每日近三月排名 + 百分位）</td></tr><tr><td><code>getFundDividendList</code></td><td>基金 &#x2F; ETF 分红明细，全市场，按年份分页</td></tr></tbody></table><p>对做基金 &#x2F; ETF 看板的同学，这套数据基本就是「即插即用」。</p><hr><h3 id="10-扩展能力：搜索、交易日历、分红、外链"><a href="#10-扩展能力：搜索、交易日历、分红、外链" class="headerlink" title="10. 扩展能力：搜索、交易日历、分红、外链"></a>10. 扩展能力：搜索、交易日历、分红、外链</h3><p>一些「不抢眼但很省心」的小功能：</p><ul><li><code>search</code> —— 按代码 &#x2F; 名称 &#x2F; 拼音搜索（issue #24 反馈后增加了 ETF 类型区分）</li><li><code>getTradingCalendar</code> + <code>isTradingDay()</code> 等工具 —— A 股交易日历</li><li><code>getDividendDetail</code> —— 个股分红派送（issue #13 的需求）</li><li><code>generateSearchExternalLinks(result)</code> —— 一键生成东方财富 &#x2F; 雪球外链</li></ul><hr><h3 id="11-请求治理：重试-限流-熔断-错误码"><a href="#11-请求治理：重试-限流-熔断-错误码" class="headerlink" title="11. 请求治理：重试 &#x2F; 限流 &#x2F; 熔断 &#x2F; 错误码"></a>11. 请求治理：重试 &#x2F; 限流 &#x2F; 熔断 &#x2F; 错误码</h3><p>如果你打算把它放进生产环境，会喜欢这一层：</p><figure class="highlight ts"><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> sdk = <span class="keyword">new</span> <span class="title class_">StockSDK</span>(&#123;</span><br><span class="line">  <span class="attr">retry</span>: &#123; <span class="attr">maxRetries</span>: <span class="number">2</span>, <span class="attr">baseDelay</span>: <span class="number">500</span> &#125;,</span><br><span class="line">  <span class="attr">providerPolicies</span>: &#123;</span><br><span class="line">    <span class="attr">eastmoney</span>: &#123;</span><br><span class="line">      <span class="attr">timeout</span>: <span class="number">12000</span>,</span><br><span class="line">      <span class="attr">rateLimit</span>: &#123; <span class="attr">requestsPerSecond</span>: <span class="number">3</span>, <span class="attr">maxBurst</span>: <span class="number">3</span> &#125;,</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">try</span> &#123;</span><br><span class="line">  <span class="keyword">await</span> sdk.<span class="title function_">getSimpleQuotes</span>([<span class="string">&#x27;sh600519&#x27;</span>]);</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">  <span class="keyword">if</span> (error <span class="keyword">instanceof</span> <span class="title class_">HttpError</span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(error.<span class="property">status</span>, error.<span class="property">statusText</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">getSdkErrorCode</span>(error)); <span class="comment">// HTTP_ERROR / NETWORK_ERROR / TIMEOUT ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>全局可配重试 + 退避</li><li>每个数据源（腾讯 &#x2F; 东方财富 &#x2F; 新浪 &#x2F; 天天基金）都可以独立设置 timeout &#x2F; 限流策略</li><li>标准化错误码（<code>getSdkErrorCode</code>），不破坏原始错误类型</li><li>内置「全局 mutex」防止浏览器并发踩同一接口（issue #12：用一阵被东财封了 —— 这条 issue 直接推动了 P1 并发安全治理）</li></ul><hr><h3 id="12-AI-MCP-就绪"><a href="#12-AI-MCP-就绪" class="headerlink" title="12. AI &#x2F; MCP 就绪"></a>12. AI &#x2F; MCP 就绪</h3><p>最后一块，也是这一年增长最猛的方向：<strong>让 AI 工具直接调用行情数据</strong>。</p><p><code>stock-sdk</code> 配套发布了 <a href="https://www.npmjs.com/package/stock-sdk-mcp"><code>stock-sdk-mcp</code></a> —— 一个标准的 MCP Server，支持 Cursor &#x2F; Claude Desktop &#x2F; Codex CLI &#x2F; Gemini CLI 等主流 AI 工具。配置只需要一行：</p><figure class="highlight json"><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="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;stock-sdk&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;-y&quot;</span><span class="punctuation">,</span> <span class="string">&quot;stock-sdk-mcp&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>接好之后，就可以直接对 AI 说：</p><blockquote><p>“帮我看一下贵州茅台最近的 MACD 走势”<br>“今天涨停板有哪些？”<br>“北向资金最近 5 天加仓最多的 10 只股票是什么？”</p></blockquote><p>内置 4 个专业 AI Skills：技术分析 &#x2F; 智能选股 &#x2F; 市场概览 &#x2F; 实时监控。</p><hr><h2 id="一些数字"><a href="#一些数字" class="headerlink" title="一些数字"></a>一些数字</h2><p>写到这里盘了一下从开源到现在的数据：</p><ul><li><strong>940+ GitHub stars</strong></li><li><strong>68 forks</strong></li><li><strong>v1.10.0</strong> —— 在持续迭代</li><li><strong>10+ closed issues</strong> —— 每一条社区反馈都尽量当周内回应</li><li><strong>被阮一峰周刊 <a href="https://github.com/ruanyf/weekly/blob/master/docs/issue-397.md">issue-397</a> 收录</strong></li></ul><p>老实说，做开源不是为了数字，但当你看到 issue 里有人说 “用了一阵被封了”、”希望支持基金”、”我用 ts 重写了 akshare 的可转债板块”—— 你会意识到，写代码这件事，原来真的可以被很多陌生人接住。</p><hr><h2 id="后续在做什么？"><a href="#后续在做什么？" class="headerlink" title="后续在做什么？"></a>后续在做什么？</h2><ul><li>完善基金一侧的数据深度（盘中估值的多源对比、定投回测工具）</li><li>把 <code>RequestClient</code> 的可观测性进一步打开（per-provider 的 metrics）</li><li>MCP Skills 继续扩展，让 AI 在金融数据这块做得更顺手</li><li>期待你的下一条 issue</li></ul><hr><h2 id="资源链接"><a href="#资源链接" class="headerlink" title="资源链接"></a>资源链接</h2><p>如果这篇文章让你想试一试，下面这些链接你应该会用到：</p><ul><li>📦 <strong>NPM</strong>：<a href="https://www.npmjs.com/package/stock-sdk">https://www.npmjs.com/package/stock-sdk</a></li><li>🐙 <strong>项目地址</strong>：<a href="https://github.com/chengzuopeng/stock-sdk">https://github.com/chengzuopeng/stock-sdk</a></li><li>📖 <strong>官方文档</strong>：<a href="https://stock-sdk.linkdiary.cn/">https://stock-sdk.linkdiary.cn/</a></li><li>🎮 <strong>在线 Playground</strong>：<a href="https://stock-sdk.linkdiary.cn/playground/">https://stock-sdk.linkdiary.cn/playground/</a></li><li>🧭 <strong>看板 Demo</strong>：<a href="https://chengzuopeng.github.io/stock-dashboard/">https://chengzuopeng.github.io/stock-dashboard/</a></li><li>📰 <strong>阮一峰周刊收录</strong>：<a href="https://github.com/ruanyf/weekly/blob/master/docs/issue-397.md">issue-397</a></li></ul><p>如果觉得有用，欢迎 Star ⭐；如果踩坑了，欢迎来 <a href="https://github.com/chengzuopeng/stock-sdk/issues">issues</a> 拍砖。下一版本，咱们见。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2026/stock-sdk-overview/</id>
    <link href="https://linkdiary.com/2026/stock-sdk-overview/"/>
    <published>2026-05-27T11:52:34.000Z</published>
    <summary>stock-sdk 从一个&quot;让前端不再绕道 Python 拿股票数据&quot;的小工具，到被阮一峰周刊收录、迭代到 v1.10.0、收获 940+ stars 的全景能力回顾。</summary>
    <title>我做的前端友好的股票行情 SDK，被阮一峰的科技爱好者周刊收录了</title>
    <updated>2026-05-31T09:20:18.999Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-robot"></i><p>当 AI 助手变成你的”私人盯盘员”：一行配置，让 Cursor &#x2F; Claude 拥有实时股票行情、技术分析和全市场选股能力。</p><p>基于 <a href="https://stock-sdk.linkdiary.cn/">stock-sdk</a> 构建的 MCP Server + 4 个 AI Skills，不用写一行代码就能让 AI 帮你看盘、分析、筛股。</p></div><h2 id="这件事是怎么开始的"><a href="#这件事是怎么开始的" class="headerlink" title="这件事是怎么开始的"></a>这件事是怎么开始的</h2><p>用过 Cursor 或者 Claude 的人应该都有感受：</p><p>AI 越来越聪明了，有些工具也开始能联网查信息了。但说到”查一下茅台今天的 MACD 什么情况”、”帮我拉一下最近 60 天的日 K 线”这种专业的金融数据查询，大部分 AI 工具还是力不从心——要么没有对应的数据源，要么返回的结果太粗糙，缺少技术指标、盘口数据这些细节。</p><p>更别说”帮我筛一下今天科创板涨幅前 10 的股票”这种需要全市场扫描的需求了。</p><p>但如果我把 <a href="https://github.com/chengzuopeng/stock-sdk">stock-sdk</a> 的能力通过 MCP 协议”喂”给它呢？</p><p>这就是 <a href="https://www.npmjs.com/package/stock-sdk-mcp">stock-sdk-mcp</a> 干的事。</p><h2 id="MCP-是什么？30-秒讲清楚"><a href="#MCP-是什么？30-秒讲清楚" class="headerlink" title="MCP 是什么？30 秒讲清楚"></a>MCP 是什么？30 秒讲清楚</h2><p>MCP 全称 Model Context Protocol，Anthropic 搞的一个开放协议。你可以把它理解成”AI 的 USB 接口”——给 AI 插上不同的”U 盘”（MCP Server），它就获得了对应的能力。</p><p>插上文件系统的 MCP Server，AI 就能读写你的文件。</p><p>插上数据库的 MCP Server，AI 就能查表写 SQL。</p><p>插上 stock-sdk-mcp，AI 就能查行情、拉 K 线、算技术指标。</p><p>就这么简单。</p><h2 id="三分钟跑起来"><a href="#三分钟跑起来" class="headerlink" title="三分钟跑起来"></a>三分钟跑起来</h2><p>不废话，先跑起来再说。</p><p>你只需要在你用的 AI 工具的配置文件里加一段 JSON，连 npm install 都不用：</p><h3 id="Cursor"><a href="#Cursor" class="headerlink" title="Cursor"></a>Cursor</h3><p>编辑 <code>~/.cursor/mcp.json</code>：</p><figure class="highlight json"><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="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;stock-sdk&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;-y&quot;</span><span class="punctuation">,</span> <span class="string">&quot;stock-sdk-mcp&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="Claude-Desktop"><a href="#Claude-Desktop" class="headerlink" title="Claude Desktop"></a>Claude Desktop</h3><p>macOS 编辑 <code>~/Library/Application Support/Claude/claude_desktop_config.json</code>：</p><figure class="highlight json"><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="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;stock-sdk&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;-y&quot;</span><span class="punctuation">,</span> <span class="string">&quot;stock-sdk-mcp&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>重启 AI 工具，然后试试跟它说：</p><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">帮我查一下贵州茅台今天的行情</span><br></pre></td></tr></table></figure><p>如果它不再跟你说”我无法获取实时数据”，而是真的给你报了一个带价格、涨跌幅、成交量的结果——恭喜，接通了。</p><h2 id="它能干啥？32-个工具-7-个资源"><a href="#它能干啥？32-个工具-7-个资源" class="headerlink" title="它能干啥？32 个工具 + 7 个资源"></a>它能干啥？32 个工具 + 7 个资源</h2><p>stock-sdk-mcp 把 stock-sdk 的能力拆成了 32 个 MCP 工具，AI 可以根据你的问题自动选择合适的工具来调用。</p><p><strong>我最推荐的玩法是——你不用关心这 32 个工具叫什么，直接用自然语言跟 AI 聊就行。</strong> </p><p>比如：</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></pre></td><td class="code"><pre><span class="line">&gt; 帮我查一下腾讯控股和比亚迪今天的行情</span><br><span class="line"></span><br><span class="line">&gt; 拉一下茅台最近 3 个月的日 K 线，计算一下 MACD 和 RSI</span><br><span class="line"></span><br><span class="line">&gt; 今天涨幅最大的行业板块是哪个？</span><br><span class="line"></span><br><span class="line">&gt; 人工智能概念板块有哪些成分股？</span><br><span class="line"></span><br><span class="line">&gt; 苹果公司最近的 RSI 是多少？是不是超买了？</span><br></pre></td></tr></table></figure><p>AI 会自己决定调用 <code>get_quotes_by_query</code>、<code>get_kline_with_indicators</code>、<code>get_industry_list</code> 还是什么别的。你只管提问。</p><p>不过既然要写博客，还是简单列一下核心能力：</p><h3 id="行情类"><a href="#行情类" class="headerlink" title="行情类"></a>行情类</h3><p>A 股、港股、美股、基金的实时行情都能查，还有个 <code>get_quotes_by_query</code> 支持按名称、代码、拼音模糊搜索——你跟 AI 说”查一下茅台”就行，不用记代码。</p><p>全市场批量行情也有：5000+ A 股、2000+ 港股、8000+ 美股，一次性拉齐。</p><h3 id="K-线类"><a href="#K-线类" class="headerlink" title="K 线类"></a>K 线类</h3><p>日&#x2F;周&#x2F;月 K 线，分钟 K 线（1&#x2F;5&#x2F;15&#x2F;30&#x2F;60），当日分时走势。</p><p>重点说一下 <code>get_kline_with_indicators</code> 这个工具——<strong>这是整个 MCP Server 里对 AI 最友好的一个工具。</strong></p><p>为什么这么说？因为普通的 K 线接口只返回原始的 OHLC（开高低收）数据。AI 拿到这些裸数据，要自己算 MACD、RSI 什么的，它算得又慢又容易出错（大模型做浮点运算真的不行）。</p><p>而 <code>get_kline_with_indicators</code> 直接在 SDK 层面就把指标算好了，返回给 AI 的每一天数据都带着 <code>ma5</code>、<code>ma20</code>、<code>macd_dif</code>、<code>macd_dea</code>、<code>rsi</code>、<code>kdj_k</code>、<code>kdj_d</code> 这些字段。AI 直接拿来分析就行，不用自己做数学题。</p><h3 id="板块-搜索-扩展"><a href="#板块-搜索-扩展" class="headerlink" title="板块 &#x2F; 搜索 &#x2F; 扩展"></a>板块 &#x2F; 搜索 &#x2F; 扩展</h3><p>行业板块、概念板块的行情和成分股查询，股票搜索，资金流向，大单占比，交易日历，分红详情——基本上 stock-sdk 有的，MCP 里都有。</p><p>除了工具，还有 7 个”资源”（Resources），这些是 AI 可以主动读取的静态数据，比如交易日历、各市场代码列表、板块列表。AI 在需要的时候会自动去查，你不用管。</p><h2 id="Skills：让-AI-不只是查数据，而是”会分析”"><a href="#Skills：让-AI-不只是查数据，而是”会分析”" class="headerlink" title="Skills：让 AI 不只是查数据，而是”会分析”"></a>Skills：让 AI 不只是查数据，而是”会分析”</h2><p>工具只是基础设施。单纯查个行情、拉个 K 线，说实话你自己开个 APP 也能做到。</p><p>Skills 的意义在于：<strong>把多个工具串成一个”分析流程”，让 AI 像一个真正的分析师一样思考问题。</strong></p><p>我做了 4 个内置 Skill：</p><h3 id="1-股票技术分析专家"><a href="#1-股票技术分析专家" class="headerlink" title="1. 股票技术分析专家"></a>1. 股票技术分析专家</h3><p>这是用得最多的一个。</p><p>你跟 AI 说”分析一下茅台的技术走势”，它会自动执行这套流程：</p><ol><li>先查实时行情，了解当前价格和涨跌</li><li>拉带指标的日 K 数据（MA、MACD、KDJ、RSI、BOLL 一把全上）</li><li>分析均线排列（多头还是空头？有没有金叉死叉？）</li><li>看 MACD（DIF 和 DEA 什么关系？红柱还是绿柱？有没有背离？）</li><li>看 KDJ 和 RSI（超买了还是超卖了？）</li><li>看布林带（价格在上轨还是下轨？带宽在收窄还是张口？）</li><li>最后给出一个结构化的技术分析报告</li></ol><p>输出大概长这样：</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><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">📈 技术分析报告：贵州茅台 (600519)</span><br><span class="line"></span><br><span class="line">当前价格：1474.92 元 | 今日涨跌：+3.36%</span><br><span class="line"></span><br><span class="line">【趋势】</span><br><span class="line">短期均线上穿中期均线，多头排列初步确立</span><br><span class="line"></span><br><span class="line">【MACD】DIF 上穿 DEA，红柱放大，短期看多</span><br><span class="line">【KDJ】K=75, D=68, J=89，偏高但未超买</span><br><span class="line">【RSI(6)】68.5，接近超买区</span><br><span class="line"></span><br><span class="line">综合建议：短期技术面偏多，但 RSI 接近超买区，</span><br><span class="line">建议等待回调后再介入或设好止损。</span><br><span class="line"></span><br><span class="line">⚠️ 技术分析仅供参考，不构成投资建议。</span><br></pre></td></tr></table></figure><p>说实话每次看到 AI 给我输出这个报告的时候我都有点恍惚——这东西是我教它的，但它分析得比我还有模有样。</p><h3 id="2-智能股票筛选器"><a href="#2-智能股票筛选器" class="headerlink" title="2. 智能股票筛选器"></a>2. 智能股票筛选器</h3><p>“帮我找出今天科创板涨幅前 10 且市盈率低于 50 的股票。”</p><p>Skill 会指导 AI 先定范围、再拉批量数据、再做条件过滤、最后排序输出。</p><h3 id="3-市场深度概览"><a href="#3-市场深度概览" class="headerlink" title="3. 市场深度概览"></a>3. 市场深度概览</h3><p>“今天盘面怎么样？有什么热点？”</p><p>AI 会自动扫描主要指数、行业板块涨跌排名、概念板块热度，然后给你一份”开盘&#x2F;复盘简报”。</p><h3 id="4-自选股实时监控"><a href="#4-自选股实时监控" class="headerlink" title="4. 自选股实时监控"></a>4. 自选股实时监控</h3><p>“查一下我的持仓：茅台买入价 1400，美团买入价 120，比亚迪买入价 250。”</p><p>AI 会拉实时行情，算每只股票的浮盈浮亏，用表格展示出来。甚至还会告诉你”比亚迪今天放量上涨，注意关注突破情况”。</p><h2 id="在-OpenClaw-上怎么玩"><a href="#在-OpenClaw-上怎么玩" class="headerlink" title="在 OpenClaw 上怎么玩"></a>在 OpenClaw 上怎么玩</h2><p>最近 OpenClaw 挺火的，简单说它就是一个 MCP 的”网关”——你可以把多个 MCP Server 聚合在一起，然后通过 HTTP API 统一调用。</p><p>对 stock-sdk-mcp 来说，OpenClaw 的意义在于：<strong>你不再局限于在 Cursor 或 Claude 里用，而是可以把股票数据能力接入任何应用。</strong></p><h3 id="第一步：在-OpenClaw-里注册-stock-sdk"><a href="#第一步：在-OpenClaw-里注册-stock-sdk" class="headerlink" title="第一步：在 OpenClaw 里注册 stock-sdk"></a>第一步：在 OpenClaw 里注册 stock-sdk</h3><p>编辑 <code>~/.clawdbot/config.yaml</code>：</p><figure class="highlight yaml"><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="attr">servers:</span></span><br><span class="line">  <span class="attr">stock-sdk:</span></span><br><span class="line">    <span class="attr">command:</span> <span class="string">npx</span></span><br><span class="line">    <span class="attr">args:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;-y&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;stock-sdk-mcp&quot;</span></span><br><span class="line">    <span class="attr">description:</span> <span class="string">&quot;股票行情数据服务&quot;</span></span><br><span class="line">    <span class="attr">tags:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">finance</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">stock</span></span><br></pre></td></tr></table></figure><h3 id="第二步：启动网关"><a href="#第二步：启动网关" class="headerlink" title="第二步：启动网关"></a>第二步：启动网关</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">clawdbot gateway start</span><br></pre></td></tr></table></figure><h3 id="第三步：随便调"><a href="#第三步：随便调" class="headerlink" title="第三步：随便调"></a>第三步：随便调</h3><p>现在你可以用 HTTP 请求来调用 stock-sdk 的能力了：</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><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="comment"># 查行情</span></span><br><span class="line">curl -X POST http://localhost:8080/v1/tools/call \</span><br><span class="line">  -H <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  -d <span class="string">&#x27;&#123;</span></span><br><span class="line"><span class="string">    &quot;server&quot;: &quot;stock-sdk&quot;,</span></span><br><span class="line"><span class="string">    &quot;tool&quot;: &quot;get_quotes_by_query&quot;,</span></span><br><span class="line"><span class="string">    &quot;arguments&quot;: &#123; &quot;queries&quot;: [&quot;茅台&quot;, &quot;腾讯&quot;] &#125;</span></span><br><span class="line"><span class="string">  &#125;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 拉带指标的 K 线</span></span><br><span class="line">curl -X POST http://localhost:8080/v1/tools/call \</span><br><span class="line">  -H <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  -d <span class="string">&#x27;&#123;</span></span><br><span class="line"><span class="string">    &quot;server&quot;: &quot;stock-sdk&quot;,</span></span><br><span class="line"><span class="string">    &quot;tool&quot;: &quot;get_kline_with_indicators&quot;,</span></span><br><span class="line"><span class="string">    &quot;arguments&quot;: &#123;</span></span><br><span class="line"><span class="string">      &quot;symbol&quot;: &quot;600519&quot;,</span></span><br><span class="line"><span class="string">      &quot;indicators&quot;: &#123; &quot;ma&quot;: &#123; &quot;periods&quot;: [5, 10, 20] &#125;, &quot;macd&quot;: true &#125;</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">  &#125;&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="第四步：加载-Skills"><a href="#第四步：加载-Skills" class="headerlink" title="第四步：加载 Skills"></a>第四步：加载 Skills</h3><p>OpenClaw 有个很好用的功能：它能直接加载 Skill 文件夹。在 config 里加一行：</p><figure class="highlight yaml"><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="attr">skills:</span></span><br><span class="line">  <span class="attr">directories:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">/你的路径/stock-sdk-mcp/skills</span></span><br></pre></td></tr></table></figure><p>然后你跟 OpenClaw 的 Agent 说”分析一下茅台走势”，它就会按 Skill 里定义的流程自动执行了。</p><h3 id="能怎么玩？"><a href="#能怎么玩？" class="headerlink" title="能怎么玩？"></a>能怎么玩？</h3><p>说几个我觉得有意思的场景：</p><p><strong>1. 定时复盘机器人</strong></p><p>用 OpenClaw 的 HTTP API + cron 定时任务，每天收盘后自动调用 market-overview Skill，生成当日复盘报告，推送到飞书&#x2F;钉钉&#x2F;微信。</p><p><strong>2. 自建 AI 选股助手</strong></p><p>搭一个简单的 Web 页面，后端通过 OpenClaw API 调用 stock-screener Skill，让用户输入筛选条件，AI 自动返回符合条件的股票列表。</p><p><strong>3. 接入已有的 AI 应用</strong></p><p>如果你已经有一个 AI chatbot 或者 Agent 框架，通过 OpenClaw 的 HTTP 接口就能把股票数据能力”插”进去，不用改底层架构。</p><p><strong>4. 多 MCP Server 联动</strong></p><p>OpenClaw 的价值在于聚合。你可以同时挂上 stock-sdk-mcp（股票数据）+ 文件系统 MCP（保存分析报告）+ 邮件 MCP（发送预警通知），让 AI 自己编排工作流。</p><h2 id="其他-AI-工具的配置"><a href="#其他-AI-工具的配置" class="headerlink" title="其他 AI 工具的配置"></a>其他 AI 工具的配置</h2><p>除了 Cursor 和 Claude Desktop，基本上支持 MCP 协议的 AI 工具都能接：</p><table><thead><tr><th>AI 工具</th><th>配置文件</th></tr></thead><tbody><tr><td>Cursor</td><td><code>~/.cursor/mcp.json</code></td></tr><tr><td>Claude Desktop</td><td><code>claude_desktop_config.json</code></td></tr><tr><td>Antigravity (Gemini in VS Code)</td><td><code>~/.antigravity/mcp.json</code></td></tr><tr><td>Codex CLI (OpenAI)</td><td><code>~/.codex/config.json</code></td></tr><tr><td>Gemini CLI (Google)</td><td><code>~/.gemini/settings.json</code></td></tr><tr><td>OpenClaw</td><td><code>~/.clawdbot/config.yaml</code></td></tr></tbody></table><p>配置内容都一样，核心就是那几行 JSON：</p><figure class="highlight json"><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="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;stock-sdk&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;-y&quot;</span><span class="punctuation">,</span> <span class="string">&quot;stock-sdk-mcp&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>对，Codex CLI 和 Gemini CLI 也都支持了。所以你在终端里用 <code>codex &quot;查一下苹果股价&quot;</code> 或者 <code>gemini &quot;今天 A 股热点板块有哪些&quot;</code> 也是可以的。</p><h2 id="一些想说的"><a href="#一些想说的" class="headerlink" title="一些想说的"></a>一些想说的</h2><p>做这个 MCP Server 的初衷很简单——stock-sdk 已经把股票数据获取的”工程脏活”都封装好了，A 股港股美股、K 线指标资金流都有，而且浏览器和 Node.js 都能跑。既然 MCP 协议提供了一个标准的方式让 AI 调用外部工具，那把 stock-sdk 包一层 MCP 接口不就顺理成章了？AI 负责理解意图和分析，stock-sdk 负责提供数据，各干各的擅长的事。</p><p>Skills 那部分是后来加的。一开始只有工具，AI 虽然能查数据，但分析报告的质量参差不齐——有时候面面俱到，有时候又漏掉关键指标。加了 Skill 之后，相当于给 AI 一个”标准作业流程”，输出质量明显稳定多了。</p><p>当然也有局限性：</p><ul><li><strong>数据延迟</strong>：数据源是公开接口，延迟在秒级，做不了高频</li><li><strong>只有行情数据</strong>：财报、公告、新闻这些暂时没有</li><li><strong>AI 的分析能力有上限</strong>：它能看数据、套公式，但不会真正”理解”市场。技术分析报告看看就好，别当投资建议</li></ul><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>stock-sdk 从一个自用的小工具，到加上 K 线图组件、行情看板，再到现在接入 AI 生态——回头看，每一步都是在解决自己的实际需求。</p><p>MCP 这条路我觉得挺有意思的。AI 现在最大的短板不是”不够聪明”，而是”不够了解世界的实时状态”。MCP 就是在补这块短板。而股票行情恰好是一个天然适合 MCP 的场景——数据实时性要求高、查询维度多、分析逻辑明确。</p><p>如果你也在用 Cursor 或者 Claude，试试配一下 stock-sdk-mcp，三分钟的事。也许你会跟我一样，配完之后再也回不去了。</p><hr><p>🔗 <strong>链接汇总</strong></p><ul><li><a href="https://github.com/chengzuopeng/stock-sdk">stock-sdk GitHub</a></li><li><a href="https://stock-sdk.linkdiary.cn/">stock-sdk 官方文档</a></li><li><a href="https://stock-sdk.linkdiary.cn/mcp/">stock-sdk MCP 文档</a></li><li><a href="https://www.npmjs.com/package/stock-sdk-mcp">stock-sdk-mcp NPM 包</a></li><li><a href="https://www.npmjs.com/package/stock-sdk">stock-sdk NPM 包</a></li><li><a href="https://stock-sdk.linkdiary.cn/playground/">stock-sdk Playground</a></li><li><a href="https://chengzuopeng.github.io/stock-dashboard/">Stock Dashboard 演示</a></li></ul><p>安装 stock-sdk：</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">npm install stock-sdk</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">yarn add stock-sdk</span><br></pre></td></tr></table></figure><p>配置 MCP Server（无需安装）：</p><figure class="highlight json"><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="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;stock-sdk&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;-y&quot;</span><span class="punctuation">,</span> <span class="string">&quot;stock-sdk-mcp&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>觉得有用的话，给 <a href="https://github.com/chengzuopeng/stock-sdk">stock-sdk</a> 点个 Star 呗。</p><p><strong>Happy Coding &amp; AI Trading!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2026/stock-sdk-mcp/</id>
    <link href="https://linkdiary.com/2026/stock-sdk-mcp/"/>
    <published>2026-03-03T11:47:12.000Z</published>
    <summary>给 stock-sdk 做了一个 MCP Server，让 Cursor、Claude 这些 AI 工具能直接查股票、画 K 线、做技术分析。还内置了 4 个 Skills，AI 可以像分析师一样帮你看盘选股。</summary>
    <title>给 stock-sdk 接了个 AI 大脑：MCP + Skills 玩法全解</title>
    <updated>2026-05-31T09:20:18.999Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-chart-bar"></i><p>前端开发者想画个 K 线图有多难？</p><p>市面上的组件大致分三类：<strong>专业级的</strong>要授权费还体积巨大；<strong>简陋级的</strong> UI 像 2012 年的审美；<strong>祖传代码级的</strong>上次更新是三年前。所以干脆自己撸一个：<strong>传个股票代码就能渲染，支持 15 种技术指标，数据源可插拔</strong>。</p></div><h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>事情是这样的。</p><p>前段时间接了个需求，要在页面里嵌一个股票 K 线图。我心想，这还不简单？npm 上搜一搜，装一个不就完了。</p><p>然后我就去搜了。</p><p>搜完之后我沉默了。</p><p>市面上的 K 线图组件，大致分三类：</p><ol><li><strong>专业级的</strong> —— TradingView 那种，功能确实强，但商用要授权费，而且体积大到能撑爆你的 bundle</li><li><strong>简陋级的</strong> —— 功能勉强能用，但 UI 像是 2012 年的审美，鼠标交互约等于没有</li><li><strong>祖传代码级的</strong> —— 上一次更新是三年前，issues 里面全是 “Is this project still maintained?”</li></ol><p>我就想，行吧，自己撸一个。</p><h2 id="kline-charts-react"><a href="#kline-charts-react" class="headerlink" title="kline-charts-react"></a>kline-charts-react</h2><p>在线演示：<strong><a href="https://chengzuopeng.github.io/kline-charts-react/">kline-charts-react Demo</a></strong></p><p><img src="/images/kline-charts/demo.webp" alt="kline-charts-react 演示"></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">KLineChart</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;kline-charts-react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;kline-charts-react/style.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">KLineChart</span> <span class="attr">symbol</span>=<span class="string">&quot;sh600519&quot;</span> <span class="attr">height</span>=<span class="string">&#123;600&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对，你没看错，传个股票代码就行了。数据获取的事儿组件自己搞定。</p><p>“等等，数据从哪来的？”</p><p>别急，下面专门聊。</p><h2 id="这玩意儿能干啥"><a href="#这玩意儿能干啥" class="headerlink" title="这玩意儿能干啥"></a>这玩意儿能干啥</h2><p>我尽量把雪球那套交互体验搬过来了（虽然还有差距，但也八九不离十了）：</p><p><strong>周期切换</strong>：分时、五日、日 K、周 K、月 K、1&#x2F;5&#x2F;15&#x2F;30&#x2F;60 分钟，基本上你想看的颗粒度都有。</p><p><strong>技术指标</strong>：MA、BOLL、MACD、KDJ、RSI、WR、BIAS、CCI、ATR、OBV、ROC、DMI、SAR、KC —— 没错，一共 15 个。这些指标全部在前端计算，不依赖后端接口返回。写这些指标公式的时候我重温了一遍大学的数学课（并不是，我大学学的不是金融）。</p><p><strong>交互</strong>：鼠标滚轮缩放、拖拽平移、十字准线、Tooltip、撤销&#x2F;重做、全屏、导出图片。该有的都有了。</p><p><strong>主题</strong>：浅色&#x2F;深色主题一键切换，也支持自定义颜色，你想搞个赛博朋克风的 K 线图也不是不行。</p><p><strong>复权</strong>：前复权、后复权、不复权，默认前复权。</p><h2 id="数据从哪来-——-stock-sdk"><a href="#数据从哪来-——-stock-sdk" class="headerlink" title="数据从哪来 —— stock-sdk"></a>数据从哪来 —— stock-sdk</h2><p>做 K 线图最头疼的是什么？不是画图，是搞数据。</p><p>你去找股票行情 API，会发现这个领域基本被 Python 生态垄断了 —— tushare、akshare、baostock，一个比一个好用，但它们跟前端没半毛钱关系。前端想拿个 A 股日 K 数据？要么自己去爬接口、处理 GBK 编码、解析各种奇奇怪怪的返回格式，要么搭个 Python 后端做中转。</p><p>所以我顺手写了 <a href="https://stock-sdk.linkdiary.cn/">stock-sdk</a>，一个专门给前端和 Node.js 用的股票行情 SDK。</p><p>它解决的问题很直接：<strong>让前端工程师用 JavaScript&#x2F;TypeScript 就能拿到股票数据，不用再绕一圈 Python。</strong></p><p>几个特点：</p><ul><li><strong>零依赖</strong>，纯 TypeScript 实现，压缩后不到 20KB</li><li><strong>双端运行</strong>，浏览器和 Node.js 18+ 都能跑，ESM&#x2F;CJS 双格式</li><li><strong>多市场</strong>，A 股、港股、美股、公募基金的实时行情和历史 K 线都能拿</li><li><strong>内置技术指标计算</strong>，MA、MACD、BOLL、KDJ、RSI 这些常用的都有</li><li><strong>完整的 TypeScript 类型定义</strong>，写代码有智能提示，不用翻文档猜字段名</li></ul><p>用起来大概长这样：</p><figure class="highlight ts"><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">import</span> &#123; <span class="title class_">StockSDK</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;stock-sdk&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> sdk = <span class="keyword">new</span> <span class="title class_">StockSDK</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> quotes = <span class="keyword">await</span> sdk.<span class="title function_">getSimpleQuotes</span>([<span class="string">&#x27;sh600519&#x27;</span>, <span class="string">&#x27;sz000858&#x27;</span>]);</span><br><span class="line">quotes.<span class="title function_">forEach</span>(<span class="function"><span class="params">q</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;q.name&#125;</span>: <span class="subst">$&#123;q.price&#125;</span> (<span class="subst">$&#123;q.changePercent&#125;</span>%)`</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取日 K 数据</span></span><br><span class="line"><span class="keyword">const</span> kline = <span class="keyword">await</span> sdk.<span class="title function_">getKline</span>(<span class="string">&#x27;sh600519&#x27;</span>, &#123; <span class="attr">period</span>: <span class="string">&#x27;daily&#x27;</span>, <span class="attr">adjust</span>: <span class="string">&#x27;qfq&#x27;</span> &#125;);</span><br></pre></td></tr></table></figure><p>kline-charts-react 内部就是用 stock-sdk 来获取数据的。所以你传个股票代码，组件就能自动把数据拉回来、算好指标、画到图上。整个链路不需要后端参与。</p><p>当然，stock-sdk 也可以脱离图表组件单独使用 —— 比如你只想做个行情看板、搞个数据监控脚本，直接用 stock-sdk 就够了。</p><p>不过话说回来，实际项目里你可能有自己的数据源。所以组件也支持完全替换数据层，后面会说。</p><h2 id="底层用的啥"><a href="#底层用的啥" class="headerlink" title="底层用的啥"></a>底层用的啥</h2><p>渲染层用的 ECharts。</p><p>我知道可能有人会问 “为什么不用 Canvas 从头画？” —— 因为我还想早点下班。ECharts 的图表渲染、交互事件、自适应这些都很成熟了，没必要重复造轮子。而且 ECharts 支持按需引入，最终打包只会包含用到的组件（Candlestick、Line、Bar、Scatter），不会把整个 ECharts 都塞进去。</p><p>说到体积，组件本身（不算 echarts 和 react）打包后 <strong>77KB</strong>（gzip 后约 19KB）。echarts 作为 peerDependency，不会被重复打包。</p><h2 id="几个我觉得做得还行的设计"><a href="#几个我觉得做得还行的设计" class="headerlink" title="几个我觉得做得还行的设计"></a>几个我觉得做得还行的设计</h2><h3 id="1-零配置也能用，想定制也能定制"><a href="#1-零配置也能用，想定制也能定制" class="headerlink" title="1. 零配置也能用，想定制也能定制"></a>1. 零配置也能用，想定制也能定制</h3><p>最简场景一个 <code>symbol</code> 就够了。但如果你有定制需求，API 也足够灵活：</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">&lt;<span class="title class_">KLineChart</span></span><br><span class="line">  <span class="built_in">symbol</span>=<span class="string">&quot;sh600519&quot;</span></span><br><span class="line">  period=<span class="string">&quot;weekly&quot;</span></span><br><span class="line">  adjust=<span class="string">&quot;qfq&quot;</span></span><br><span class="line">  theme=<span class="string">&quot;dark&quot;</span></span><br><span class="line">  indicators=&#123;[<span class="string">&#x27;ma&#x27;</span>, <span class="string">&#x27;volume&#x27;</span>, <span class="string">&#x27;kdj&#x27;</span>, <span class="string">&#x27;rsi&#x27;</span>]&#125;</span><br><span class="line">  indicatorOptions=&#123;&#123;</span><br><span class="line">    <span class="attr">ma</span>: &#123; <span class="attr">periods</span>: [<span class="number">5</span>, <span class="number">10</span>, <span class="number">20</span>, <span class="number">60</span>] &#125;,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">  maxSubPanes=&#123;<span class="number">3</span>&#125;</span><br><span class="line">  height=&#123;<span class="number">700</span>&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><p>指标选哪些、均线周期多少、副图最多显示几个、高度多少，都可以配。</p><h3 id="2-数据源可插拔"><a href="#2-数据源可插拔" class="headerlink" title="2. 数据源可插拔"></a>2. 数据源可插拔</h3><p>内置的 stock-sdk 开箱即用挺爽的，但实际项目里你可能有自己的行情数据接口，或者需要在 Node 端做 SSR 获取数据。</p><p>所以组件留了 <code>dataProvider</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> myProvider = &#123;</span><br><span class="line">  <span class="attr">getKline</span>: <span class="title function_">async</span> (params, signal) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">`/api/kline?symbol=<span class="subst">$&#123;params.<span class="built_in">symbol</span>&#125;</span>&amp;period=<span class="subst">$&#123;params.period&#125;</span>`</span>, &#123; signal &#125;);</span><br><span class="line">    <span class="keyword">return</span> res.<span class="title function_">json</span>();</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">getTimeline</span>: <span class="title function_">async</span> (params, signal) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">`/api/timeline?symbol=<span class="subst">$&#123;params.<span class="built_in">symbol</span>&#125;</span>`</span>, &#123; signal &#125;);</span><br><span class="line">    <span class="keyword">return</span> res.<span class="title function_">json</span>();</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">KLineChart</span> <span class="attr">symbol</span>=<span class="string">&quot;sh600519&quot;</span> <span class="attr">dataProvider</span>=<span class="string">&#123;myProvider&#125;</span> /&gt;</span></span></span><br></pre></td></tr></table></figure><p>数据格式也不复杂，K 线就是 <code>&#123; date, open, close, high, low, volume, amount &#125;</code> 这些字段，分时就是 <code>&#123; time, price, volume, amount, avgPrice &#125;</code>。技术指标不用你算，组件拿到原始数据后会自动计算。</p><h3 id="3-Ref-API"><a href="#3-Ref-API" class="headerlink" title="3. Ref API"></a>3. Ref API</h3><p>有些场景你需要从外部控制图表，比如在别的按钮里触发刷新、导出图片、切换周期。用 <code>ref</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">const</span> chartRef = useRef&lt;<span class="title class_">KLineChartRef</span>&gt;(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 刷新数据</span></span><br><span class="line">chartRef.<span class="property">current</span>?.<span class="title function_">refresh</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> dataUrl = chartRef.<span class="property">current</span>?.<span class="title function_">exportImage</span>(<span class="string">&#x27;png&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 切换到周 K</span></span><br><span class="line">chartRef.<span class="property">current</span>?.<span class="title function_">setPeriod</span>(<span class="string">&#x27;weekly&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 拿到 ECharts 实例（高级用法）</span></span><br><span class="line"><span class="keyword">const</span> instance = chartRef.<span class="property">current</span>?.<span class="title function_">getEchartsInstance</span>();</span><br></pre></td></tr></table></figure><h3 id="4-自动刷新"><a href="#4-自动刷新" class="headerlink" title="4. 自动刷新"></a>4. 自动刷新</h3><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">&lt;<span class="title class_">KLineChart</span></span><br><span class="line">  <span class="built_in">symbol</span>=<span class="string">&quot;sh600519&quot;</span></span><br><span class="line">  period=<span class="string">&quot;timeline&quot;</span></span><br><span class="line">  autoRefresh=&#123;&#123; <span class="attr">intervalMs</span>: <span class="number">5000</span>, <span class="attr">onlyTradingTime</span>: <span class="literal">true</span> &#125;&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><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></pre></td><td class="code"><pre><span class="line">yarn add kline-charts-react</span><br><span class="line"></span><br><span class="line"><span class="comment"># 别忘了 peer dependencies</span></span><br><span class="line">yarn add react react-dom echarts</span><br></pre></td></tr></table></figure><p>对，echarts 是 peerDependency。这样如果你项目里已经在用 echarts 了，不会重复打包两份。</p><h2 id="目前的不足"><a href="#目前的不足" class="headerlink" title="目前的不足"></a>目前的不足</h2><p>坦诚说几个还没做好的地方：</p><ul><li><strong>移动端适配</strong>：目前主要针对桌面端优化，移动端的触控交互（捏合缩放之类的）还没有专门处理</li><li><strong>导出图片</strong>：<code>exportImage()</code> 导出的图片只包含 ECharts 画布部分，左上角的指标数值文字（那部分是 React DOM 渲染的）不会出现在导出图片里</li><li><strong>实时推送</strong>：目前是轮询模式，还没接 WebSocket</li></ul><p>这些后续会慢慢补上。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>这个组件最初是为了解决自己的需求写的，写着写着发现越做越完善，就干脆开源出来了。如果你也有在 React 项目里画 K 线图的需求，欢迎试试看。</p><p>有问题或者建议可以到 GitHub 上提 issue，也欢迎 PR。</p><p>如果觉得有用的话，给个 Star 呗。毕竟写这些指标计算公式真的挺费头发的。</p><hr><h2 id="相关链接"><a href="#相关链接" class="headerlink" title="相关链接"></a>相关链接</h2><ul><li><a href="https://chengzuopeng.github.io/kline-charts-react/">在线演示</a></li><li><a href="https://github.com/chengzuopeng/kline-charts-react">GitHub 仓库</a></li><li><a href="https://www.npmjs.com/package/kline-charts-react">NPM 包</a></li><li><a href="https://stock-sdk.linkdiary.cn/">stock-sdk 文档</a></li></ul><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2026/kline-charts/</id>
    <link href="https://linkdiary.com/2026/kline-charts/"/>
    <published>2026-02-28T09:38:22.000Z</published>
    <summary>市面上的 K 线图组件要么丑要么贵，干脆自己撸一个。基于 ECharts + stock-sdk 的 React K 线图组件，支持 15 种技术指标、多周期切换和数据源可插拔。</summary>
    <title>kline-charts-react：我写了个 React K 线图组件</title>
    <updated>2026-05-31T09:20:18.999Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-desktop"></i><p>先交代一下背景：我平时看盘有个“坏习惯”——开一堆 APP&#x2F;网页，然后在它们之间疯狂切换，最后得到的不是信息，而是焦虑。</p><p>所以干脆自己写一个：<strong>打开一个页面，把常用的行情、板块、分时、K 线、资金面、筛选工具都塞进去</strong>。</p><p>这个项目就是 <code>stock-dashboard</code>：React + TypeScript + Vite 的前端看板。数据源全部来自 <a href="https://stock-sdk.linkdiary.cn/">stock-sdk</a>，<strong>没有后端、没有 Python 定时脚本</strong>，就是纯前端直接拉数据。</p></div><p>体验地址我直接放这： <strong><a href="https://chengzuopeng.github.io/stock-dashboard/">stock-dashboard</a></strong> （如果你也想摸鱼，记得开小窗）。</p><p><img src="/images/stock-dashboard/stock-overview.webp" alt="stock-overview"></p><hr><h2 id="先说最关键的：数据层怎么接的？"><a href="#先说最关键的：数据层怎么接的？" class="headerlink" title="先说最关键的：数据层怎么接的？"></a>先说最关键的：数据层怎么接的？</h2><p>项目里我把 <code>stock-sdk</code> 的调用统一收口到了 <code>src/services/sdk.ts</code>。</p><p>这里做了三件很“工程化但不装”的事：</p><ol><li><p><strong>SDK 单例 + 重试</strong><br><code>new StockSDK(&#123; timeout, retry &#125;)</code>，网络抖一下、接口偶发超时这种事就交给 SDK 自己兜底（最多重试 3 次，指数退避那套）。</p></li><li><p><strong>内存缓存（TTL）</strong><br>行业&#x2F;概念列表这种 10 秒内不会突然“宇宙大爆炸”的数据，缓存一下省请求；实时行情则是 2~3 秒 TTL，既不会太旧，也不会把接口当压力测试工具。</p></li><li><p><strong>页面只认服务层，不直接碰 SDK</strong><br>你在 <code>src/pages/**</code> 里基本看不到 <code>new StockSDK()</code>，页面只调用 <code>getFullQuotes / getTodayTimeline / getKlineWithIndicators ...</code> 这些封装后的方法（类型会从 <code>stock-sdk</code> 里导入）。</p></li></ol><p>顺便贴两段“骨架代码”，后面每个功能都是围绕它转的：</p><figure class="highlight ts"><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="comment">// src/services/sdk.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> sdk = <span class="keyword">new</span> <span class="title class_">StockSDK</span>(&#123; <span class="attr">timeout</span>: <span class="number">30000</span>, <span class="attr">retry</span>: &#123; <span class="attr">maxRetries</span>: <span class="number">3</span>, <span class="attr">baseDelay</span>: <span class="number">1000</span>, <span class="attr">maxDelay</span>: <span class="number">10000</span>, <span class="attr">backoffMultiplier</span>: <span class="number">2</span> &#125; &#125;);</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_">getFullQuotes</span>(<span class="params"><span class="attr">codes</span>: <span class="built_in">string</span>[], useCache = <span class="literal">true</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> key = <span class="title function_">getCacheKey</span>(<span class="string">&#x27;getFullQuotes&#x27;</span>, codes);</span><br><span class="line">  <span class="keyword">if</span> (useCache) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">withCache</span>(key, <span class="variable constant_">DEFAULT_TTL</span>.<span class="property">quotes</span>, <span class="function">() =&gt;</span> sdk.<span class="title function_">getFullQuotes</span>(codes));</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> sdk.<span class="title function_">getFullQuotes</span>(codes);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight ts"><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">// src/services/sdk.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">getAllAShareQuotes</span>(<span class="params"><span class="attr">options</span>?: &#123; batchSize?: <span class="built_in">number</span>; concurrency?: <span class="built_in">number</span>; onProgress?: (completed: <span class="built_in">number</span>, total: <span class="built_in">number</span>) =&gt; <span class="built_in">void</span> &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> sdk.<span class="title function_">getAllAShareQuotes</span>(options);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="模块和功能：每一块都是怎么用-stock-sdk-拿数据的？"><a href="#模块和功能：每一块都是怎么用-stock-sdk-拿数据的？" class="headerlink" title="模块和功能：每一块都是怎么用 stock-sdk 拿数据的？"></a>模块和功能：每一块都是怎么用 stock-sdk 拿数据的？</h2><p>路由都在 <code>src/router/index.tsx</code>，页面基本按功能拆在 <code>src/pages/*</code>。下面按“用户能点到的地方”来讲。</p><h3 id="1-顶部搜索：别让我翻代码查股票"><a href="#1-顶部搜索：别让我翻代码查股票" class="headerlink" title="1) 顶部搜索：别让我翻代码查股票"></a>1) 顶部搜索：别让我翻代码查股票</h3><p>入口在 <code>src/components/layout/Header.tsx</code>，核心就是一行：</p><ul><li><code>search(keyword)</code> → <code>stock-sdk</code> 的 <code>sdk.search(keyword)</code></li></ul><p>我做了 300ms 的输入防抖，结果列表支持股票&#x2F;板块混搜；点一下直接跳转：</p><ul><li>行业板块：<code>/boards/industry/:code</code></li><li>概念板块：<code>/boards/concept/:code</code></li><li>个股：<code>/s/:code</code></li></ul><p>顺便把搜索历史塞进了 localStorage（<code>src/services/storage.ts</code>），这点很像“你以为你在找股票，其实你在找昨天的自己”。</p><hr><h3 id="2-总览-Dashboard：打开就能看个大概（以及自选快照）"><a href="#2-总览-Dashboard：打开就能看个大概（以及自选快照）" class="headerlink" title="2) 总览 Dashboard：打开就能看个大概（以及自选快照）"></a>2) 总览 Dashboard：打开就能看个大概（以及自选快照）</h3><p>页面在 <code>src/pages/Dashboard/Dashboard.tsx</code>。</p><p>它拿数据很直接：</p><ul><li>指数行情：<code>getFullQuotes(MAIN_INDICES)</code>（上证、深成指、创业板、科创 50…）</li><li>行业&#x2F;概念列表：<code>getIndustryList()</code> + <code>getConceptList()</code></li><li>自选快照：从 <code>src/services/storage.ts</code> 取自选代码，再 <code>getFullQuotes(watchlistCodes.slice(0, 50))</code></li></ul><p>然后用 <code>usePolling</code> 每 5 秒轮询一次（<code>src/hooks/usePolling.ts</code>），页面不可见还会自动暂停，避免你切到别的标签页它还在疯狂刷新。</p><p>小插曲：Dashboard 里有个“榜单”区域目前主要还是板块数据的延伸（全市场个股榜要做的话，思路就是上 <code>getAllAShareQuotes</code>，后面“一日持股法”已经把路走通了）。</p><hr><h3 id="3-热力图-Heatmap：今天到底谁在“发热”？"><a href="#3-热力图-Heatmap：今天到底谁在“发热”？" class="headerlink" title="3) 热力图 Heatmap：今天到底谁在“发热”？"></a>3) 热力图 Heatmap：今天到底谁在“发热”？</h3><p>页面在 <code>src/pages/Heatmap/Heatmap.tsx</code>，用 ECharts 的 treemap 做热力图。</p><p><img src="/images/stock-dashboard/stock-heatmap.webp" alt="stock-heatmap"></p><p>按维度不同，数据来源也不同：</p><ul><li>行业热力图：<code>getIndustryList()</code>（每个行业自带涨跌幅、换手、领涨股等字段）</li><li>概念热力图：<code>getConceptList()</code></li><li>自选热力图：<code>getAllWatchlistCodes()</code> → <code>getAllQuotesByCodes(codes.slice(0, topK))</code></li></ul><p>如果你把维度切到“个股”（目前代码里留了入口但暂时没放开），我这边的思路是：</p><ol><li>先 <code>getIndustryConstituents(industryCode)</code> 拿成分股  </li><li>再 <code>getAllQuotesByCodes(stockCodes)</code> 拉行情  </li><li>拼起来做 treemap</li></ol><p>热力图这个功能我最喜欢的一点是：<strong>你不用盯着涨跌榜刷屏，颜色一铺开，强弱结构一眼就出来了。</strong></p><hr><h3 id="4-榜单-Rankings：卷不过就看别人怎么卷"><a href="#4-榜单-Rankings：卷不过就看别人怎么卷" class="headerlink" title="4) 榜单 Rankings：卷不过就看别人怎么卷"></a>4) 榜单 Rankings：卷不过就看别人怎么卷</h3><p>页面在 <code>src/pages/Rankings/Rankings.tsx</code>。</p><p><img src="/images/stock-dashboard/stock-rankings.webp" alt="stock-rankings"></p><p>这里其实很“偷懒但有效”：</p><ul><li>先 <code>getIndustryList()</code> &#x2F; <code>getConceptList()</code> 拿到板块列表</li><li>然后前端按 <code>changePercent</code> &#x2F; <code>turnoverRate</code> 排序，取 Top 50</li></ul><p>也就是说它的榜单是“板块榜”，不是“全市场个股榜”。要做全市场个股榜其实也不难（后面“一日持股法”就是全市场路线）。</p><hr><h3 id="5-板块列表-板块详情：想知道“这波是谁带的节奏”"><a href="#5-板块列表-板块详情：想知道“这波是谁带的节奏”" class="headerlink" title="5) 板块列表 + 板块详情：想知道“这波是谁带的节奏”"></a>5) 板块列表 + 板块详情：想知道“这波是谁带的节奏”</h3><p>板块列表在 <code>src/pages/Boards/Boards.tsx</code>：</p><ul><li><code>getIndustryList()</code> + <code>getConceptList()</code> 一次拿齐</li><li>切 tab 只是前端切换数组</li><li>支持搜索板块名&#x2F;领涨股</li></ul><p>板块详情在 <code>src/pages/Boards/BoardDetail.tsx</code>，这里用到的 API 就比较“齐活”了（按行业&#x2F;概念分流）：</p><ul><li>详情信息：还是从 <code>getIndustryList()</code> &#x2F; <code>getConceptList()</code> 里 find 出来（少一次请求）</li><li>成分股：<code>getIndustryConstituents(code)</code> &#x2F; <code>getConceptConstituents(code)</code></li><li>板块 K 线：<code>getIndustryKline(code, &#123; period &#125;)</code> &#x2F; <code>getConceptKline(code, &#123; period &#125;)</code></li><li>板块 Spot 指标：<code>getIndustrySpot(code)</code> &#x2F; <code>getConceptSpot(code)</code></li></ul><p>板块 K 线我只保留了最近 60 根（<code>slice(-60)</code>），不然你拖动 dataZoom 的时候会明显感觉浏览器开始“喘”。</p><hr><h3 id="6-自选-Watchlist：我盯的不是股票，是“我的偏见”"><a href="#6-自选-Watchlist：我盯的不是股票，是“我的偏见”" class="headerlink" title="6) 自选 Watchlist：我盯的不是股票，是“我的偏见”"></a>6) 自选 Watchlist：我盯的不是股票，是“我的偏见”</h3><p>页面在 <code>src/pages/Watchlist/Watchlist.tsx</code>，自选分组&#x2F;增删改都在 <code>src/services/storage.ts</code>。</p><p>行情获取靠：</p><ul><li><code>getAllQuotesByCodes(normalizedActiveCodes)</code></li></ul><p>这里有个小细节：我把股票代码做了 <code>normalizeStockCode</code>（<code>src/utils/format.ts</code>），避免 <code>SZ000001</code>、<code>sz000001</code>、<code>000001</code> 这种“同一个人换三套马甲”导致重复&#x2F;取不到数据。</p><hr><h3 id="7-个股详情-StockDetail：该看的都给你，看不看的也顺便给你"><a href="#7-个股详情-StockDetail：该看的都给你，看不看的也顺便给你" class="headerlink" title="7) 个股详情 StockDetail：该看的都给你，看不看的也顺便给你"></a>7) 个股详情 StockDetail：该看的都给你，看不看的也顺便给你</h3><p>页面在 <code>src/pages/StockDetail/StockDetail.tsx</code>，这是全项目里最“重”的页面之一，因为信息密度高。</p><p><img src="/images/stock-dashboard/stock-detail.webp" alt="stock-detail"></p><p>它分别拿这些数据：</p><ul><li>实时行情：<code>getFullQuotes([code])</code></li><li>分时（1 分钟）：<code>getTodayTimeline(code)</code></li><li>分钟 K（5&#x2F;15&#x2F;30&#x2F;60）：<code>getMinuteKline(code, &#123; period &#125;)</code></li><li>日&#x2F;周&#x2F;月 K + 技术指标：<code>getKlineWithIndicators(code, &#123; period, adjust: &#39;qfq&#39;, indicators &#125;)</code></li><li>资金流：<code>getFundFlow([code])</code></li><li>盘口大单：<code>getPanelLargeOrder([code])</code></li></ul><p>我比较喜欢 <code>getKlineWithIndicators</code> 这个点：页面上勾选 MA&#x2F;MACD&#x2F;BOLL&#x2F;KDJ&#x2F;RSI，后端（准确说是 SDK）直接把指标结果算好塞回来了，前端只需要画线，不用自己写一坨技术指标计算（少写 bug，多活几年）。</p><p>同样配了轮询：</p><ul><li>行情 2 秒一刷</li><li>分时 3 秒一刷</li><li>资金 10 秒一刷</li></ul><hr><h3 id="8-信号扫描-Scanner：给我一点“量化的幻觉”"><a href="#8-信号扫描-Scanner：给我一点“量化的幻觉”" class="headerlink" title="8) 信号扫描 Scanner：给我一点“量化的幻觉”"></a>8) 信号扫描 Scanner：给我一点“量化的幻觉”</h3><p>页面在 <code>src/pages/Scanner/Scanner.tsx</code>。</p><p>扫描流程大概是：</p><ol><li>先选股票池来源  <ul><li>自选：本地拿代码  </li><li>行业&#x2F;概念：用 <code>getIndustryConstituents(&#39;BK0475&#39;)</code> &#x2F; <code>getConceptConstituents(&#39;BK0891&#39;)</code> 抽一批成分股</li></ul></li><li>对每只股票调用：  <ul><li><code>getKlineWithIndicators(code, &#123; indicators: &#123; ma/macd/rsi/boll &#125; &#125;)</code></li></ul></li><li>前端用最近两根 K 线做信号判断（MA 金叉、MACD 金叉、RSI 超买超卖…）</li></ol><p>这个功能的心理作用大于实际作用，但它确实能帮我把“我觉得它要涨”变成“它真的触发了某个条件”（哪怕条件是我自己写的）。</p><hr><h3 id="9-设置-Settings：不拉行情，只管体验"><a href="#9-设置-Settings：不拉行情，只管体验" class="headerlink" title="9) 设置 Settings：不拉行情，只管体验"></a>9) 设置 Settings：不拉行情，只管体验</h3><p>页面在 <code>src/pages/Settings/Settings.tsx</code>。</p><p><img src="/images/stock-dashboard/stock-settings.webp" alt="stock-settings"></p><p>这页<strong>不调用 <code>stock-sdk</code></strong>，它只负责把刷新频率、涨跌配色、指标默认参数这些偏好写进 localStorage（<code>src/services/storage.ts</code>），让你下次打开页面不至于“重置成出厂设置”。</p><hr><h2 id="重点：一日持股法（尾盘选股）-前端全市场扫描的“重武器”"><a href="#重点：一日持股法（尾盘选股）-前端全市场扫描的“重武器”" class="headerlink" title="重点：一日持股法（尾盘选股）&#x3D; 前端全市场扫描的“重武器”"></a>重点：一日持股法（尾盘选股）&#x3D; 前端全市场扫描的“重武器”</h2><p>页面在 <code>src/pages/EndOfDayPicker/EndOfDayPicker.tsx</code>，我在里面实现了一个“三段式”流程，核心就是 <strong><code>getAllAShareQuotes</code></strong>：</p><p><img src="/images/stock-dashboard/stock-last.webp" alt="stock-last"></p><h3 id="第一步：全量拉取-A-股行情（5000-）"><a href="#第一步：全量拉取-A-股行情（5000-）" class="headerlink" title="第一步：全量拉取 A 股行情（5000+）"></a>第一步：全量拉取 A 股行情（5000+）</h3><figure class="highlight ts"><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="comment">// src/pages/EndOfDayPicker/EndOfDayPicker.tsx</span></span><br><span class="line"><span class="keyword">const</span> quotes = <span class="keyword">await</span> <span class="title function_">getAllAShareQuotes</span>(&#123;</span><br><span class="line">  <span class="attr">batchSize</span>: <span class="number">500</span>,</span><br><span class="line">  <span class="attr">concurrency</span>: <span class="number">5</span>,</span><br><span class="line">  <span class="attr">onProgress</span>: <span class="function">(<span class="params">completed, total</span>) =&gt;</span> <span class="title function_">setLoadingProgress</span>(&#123; completed, total, <span class="attr">stage</span>: <span class="string">&#x27;获取行情数据&#x27;</span> &#125;),</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>这段调用背后对应的是 <code>stock-sdk</code> 的：</p><ul><li><code>sdk.getAllAShareQuotes(options?: GetAllAShareQuotesOptions): Promise&lt;FullQuote[]&gt;</code></li><li><code>GetAllAShareQuotesOptions</code> 支持：<ul><li><code>batchSize</code>：单次请求股票数量（默认 500）</li><li><code>concurrency</code>：最大并发数（默认 7）</li><li><code>onProgress</code>：进度回调</li></ul></li></ul><p>我这里把并发设成 5，属于“别太狂，浏览器和网速都要面子”的保守路线；配合 <code>onProgress</code>，页面上能实时展示进度条，不会让你以为网页卡死了。</p><h3 id="第二步：用基础条件先砍一刀"><a href="#第二步：用基础条件先砍一刀" class="headerlink" title="第二步：用基础条件先砍一刀"></a>第二步：用基础条件先砍一刀</h3><p>全市场 <code>FullQuote[]</code> 到手后，先按这些字段筛一遍（都是从 <code>FullQuote</code> 里直接拿）：</p><ul><li>流通市值 <code>circulatingMarketCap</code></li><li>量比 <code>volumeRatio</code></li><li>涨跌幅 <code>changePercent</code></li><li>换手率 <code>turnoverRate</code></li><li>以及是否过滤 <code>ST/*ST</code></li></ul><p>这一步在 <code>filterStocksBasic()</code>，做完基本能从 5000+ 砍到几十&#x2F;几百只，不然后面拉分时会把自己送走。</p><h3 id="第三步：对候选股拉分时，算“价格在均线之上”的比例"><a href="#第三步：对候选股拉分时，算“价格在均线之上”的比例" class="headerlink" title="第三步：对候选股拉分时，算“价格在均线之上”的比例"></a>第三步：对候选股拉分时，算“价格在均线之上”的比例</h3><p>对基础筛出来的候选，我会再逐批拉分时：</p><ul><li><code>getTodayTimeline(fullCode)</code>（注意这里要拼 <code>sh/sz/bj</code> 前缀）</li></ul><p>然后用分时里 <code>price</code> 和 <code>avgPrice</code> 做一个很直白的强度指标：</p><ul><li><code>price &gt;= avgPrice</code> 的点数 &#x2F; 总点数 &#x3D; <code>timelineAboveAvgRatio</code></li></ul><p>我在 <code>filterWithTimeline()</code> 里把分时请求按 <code>batchSize = 5</code> 分批并发（不是 SDK 的那个 batchSize，是我自己控制的），防止你一口气对几百只股票同时发请求，最后浏览器先躺平。</p><p>最后结果按 <code>timelineAboveAvgRatio</code> 排序，页面上每只股票还带一张迷你分时图，基本就能完成“尾盘快速过一遍候选”的目标。</p><hr><h2 id="结尾：这玩意儿适合谁？"><a href="#结尾：这玩意儿适合谁？" class="headerlink" title="结尾：这玩意儿适合谁？"></a>结尾：这玩意儿适合谁？</h2><p>如果你想要的是“一个能看、能筛、还能顺手把自选管理了”的轻量看板，而且你不想为此养一个后端，那这个项目的思路就挺合适：<strong>用 <code>stock-sdk</code> 把数据能力直接带进前端，然后在 UI 里做组合、筛选和展示</strong>。</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></pre></td><td class="code"><pre><span class="line">yarn install</span><br><span class="line">yarn dev</span><br></pre></td></tr></table></figure><p>最后的最后：页面底部那句“仅供学习参考，不构成投资建议”我不是摆设——毕竟写代码可以自信，买股票还是得谦虚点。</p><hr><h2 id="相关链接"><a href="#相关链接" class="headerlink" title="相关链接"></a>相关链接</h2><ul><li><a href="https://chengzuopeng.github.io/stock-dashboard/">stock-dashboard 演示</a></li><li><a href="https://stock-sdk.linkdiary.cn/">stock-sdk 文档</a></li><li><a href="https://stock-sdk.linkdiary.cn/playground/">stock-sdk Playground</a></li></ul><p><strong>Happy Coding &amp; Trading!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2026/stock-dashboard/</id>
    <link href="https://linkdiary.com/2026/stock-dashboard/"/>
    <published>2026-01-15T10:00:00.000Z</published>
    <summary>基于 stock-sdk 构建的纯前端 A 股行情看板，支持实时行情、热力图、K 线分析与尾盘选股，主打零后端、开箱即用。</summary>
    <title>我用 stock-sdk 做了一个纯前端的 A 股看板</title>
    <updated>2026-05-31T09:20:18.999Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-exchange-alt"></i><p>如果你做过电商、出海、旅行、订阅制产品，或者任何“用户可能会拿着不同货币来烦你”的业务，你大概率经历过这种对话：</p><p>产品&#x2F;老板：这个页面加个汇率换算吧，很简单的。<br>你：行啊。（内心：又来了）</p><p>因为“很简单”的背后通常是：</p><ul><li>选个汇率接口（最好不要 key，不要注册，不要配额，不要收费……懂的都懂）</li><li>处理超时、网络抖动、429&#x2F;5xx（不然线上会让你看到人间真实）</li><li>缓存（不然你会被同事&#x2F;自己&#x2F;用户一起打爆）</li><li>货币列表（最好还能搜中文，不然“人民币”三个字能让你写一整套映射）</li><li>最后再把这些东西重复粘贴到下一个项目里，继续假装这叫“沉淀”</li></ul></div><p>于是我写了 <code>exchange-rate-sdk</code>：一个轻量级、跨平台（浏览器 + Node.js）的汇率查询 SDK。它的目标就一个：把这些“每个项目都要做一遍、又不想做一遍”的活儿，打包成一个你能直接用的工具。</p><p>如果你只想先试试水，直接去 Playground：<br><a href="https://chengzuopeng.github.io/exchange-rate-sdk/">https://chengzuopeng.github.io/exchange-rate-sdk/</a></p><p>顺手把链接也放这儿（方便你收藏&#x2F;转发&#x2F;甩给同事）：</p><ul><li>GitHub：<a href="https://github.com/chengzuopeng/exchange-rate-sdk">https://github.com/chengzuopeng/exchange-rate-sdk</a></li><li>npm：<a href="https://www.npmjs.com/package/exchange-rate-sdk">https://www.npmjs.com/package/exchange-rate-sdk</a></li></ul><hr><h2 id="它到底解决了什么问题？"><a href="#它到底解决了什么问题？" class="headerlink" title="它到底解决了什么问题？"></a>它到底解决了什么问题？</h2><p>我把它做成了“开箱即用”的风格，尽量不让你在接入时做选择题。</p><ol><li><p>跨平台：浏览器和 Node.js 都能跑<br>同一套 API，不用你写两份逻辑。</p></li><li><p>智能缓存：默认就给你省请求</p></li></ol><ul><li>浏览器：<code>localStorage + 内存</code> 双层缓存（刷新页面也还在）</li><li>Node.js：内存缓存（进程重启就清空，简单直接）</li></ul><ol start="3"><li><p>自动重试：网络不稳定时别立刻跪<br>遇到网络错误、超时、5xx、429，会按指数退避自动重试（你可以配置最大重试次数和延迟基数）。</p></li><li><p>零运行时依赖<br>我知道很多人对依赖洁癖（我也是）。所以运行时不带第三方库，尽量减少体积和供应链焦虑。</p></li><li><p>离线货币信息 + 中英文搜索<br>内置了 168 种货币信息：你可以离线查 <code>USD</code> 是什么、也可以直接搜“人民币 &#x2F; Dollar &#x2F; United States”。</p></li><li><p>TypeScript 友好<br>类型定义齐全，写起来不靠猜。</p></li></ol><hr><h2 id="30-秒上手（真的）"><a href="#30-秒上手（真的）" class="headerlink" title="30 秒上手（真的）"></a>30 秒上手（真的）</h2><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></pre></td><td class="code"><pre><span class="line">npm i exchange-rate-sdk</span><br><span class="line"><span class="comment"># or</span></span><br><span class="line">yarn add exchange-rate-sdk</span><br></pre></td></tr></table></figure><p>npm 包主页：<a href="https://www.npmjs.com/package/exchange-rate-sdk">https://www.npmjs.com/package/exchange-rate-sdk</a></p><p>查询汇率：</p><figure class="highlight ts"><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">import</span> &#123; <span class="title class_">ExchangeRateSDK</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;exchange-rate-sdk&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> client = <span class="keyword">new</span> <span class="title class_">ExchangeRateSDK</span>();</span><br><span class="line"><span class="keyword">const</span> rate = <span class="keyword">await</span> client.<span class="title function_">getRate</span>(<span class="string">&#x27;USD&#x27;</span>, <span class="string">&#x27;CNY&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`1 USD = <span class="subst">$&#123;rate&#125;</span> CNY`</span>);</span><br></pre></td></tr></table></figure><p>金额换算：</p><figure class="highlight ts"><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> result = <span class="keyword">await</span> client.<span class="title function_">convert</span>(<span class="number">100</span>, <span class="string">&#x27;USD&#x27;</span>, <span class="string">&#x27;CNY&#x27;</span>);</span><br><span class="line"><span class="comment">// =&gt; &#123; amount: 725, from: &#x27;USD&#x27;, to: &#x27;CNY&#x27;, rate: 7.25 &#125;</span></span><br></pre></td></tr></table></figure><hr><h2 id="你会喜欢它的几个“细节”"><a href="#你会喜欢它的几个“细节”" class="headerlink" title="你会喜欢它的几个“细节”"></a>你会喜欢它的几个“细节”</h2><h3 id="1-缓存是默认开启的"><a href="#1-缓存是默认开启的" class="headerlink" title="1) 缓存是默认开启的"></a>1) 缓存是默认开启的</h3><p>绝大多数业务里，汇率没必要每次都去打接口。SDK 默认缓存 24 小时，你也可以改：</p><figure class="highlight ts"><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> client = <span class="keyword">new</span> <span class="title class_">ExchangeRateSDK</span>(&#123; <span class="attr">cacheTTL</span>: <span class="number">60</span> * <span class="number">60</span> * <span class="number">1000</span> &#125;); <span class="comment">// 1 小时</span></span><br></pre></td></tr></table></figure><p>必要时一键清空：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">client.<span class="title function_">clearCache</span>();</span><br></pre></td></tr></table></figure><h3 id="2-重试不是“无限莽”，而是可控的"><a href="#2-重试不是“无限莽”，而是可控的" class="headerlink" title="2) 重试不是“无限莽”，而是可控的"></a>2) 重试不是“无限莽”，而是可控的</h3><p>你可以把它当成“网络偶尔抽风时的缓冲垫”：</p><figure class="highlight ts"><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> client = <span class="keyword">new</span> <span class="title class_">ExchangeRateSDK</span>(&#123;</span><br><span class="line">  <span class="attr">timeout</span>: <span class="number">10_000</span>,</span><br><span class="line">  <span class="attr">maxRetries</span>: <span class="number">3</span>,</span><br><span class="line">  <span class="attr">retryDelay</span>: <span class="number">1000</span>, <span class="comment">// 1s, 2s, 4s...</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="3-货币信息离线可查，还能搜中文（这是我自己踩过的坑）"><a href="#3-货币信息离线可查，还能搜中文（这是我自己踩过的坑）" class="headerlink" title="3) 货币信息离线可查，还能搜中文（这是我自己踩过的坑）"></a>3) 货币信息离线可查，还能搜中文（这是我自己踩过的坑）</h3><figure class="highlight ts"><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">client.<span class="title function_">getCurrency</span>(<span class="string">&#x27;USD&#x27;</span>);</span><br><span class="line">client.<span class="title function_">getCurrencies</span>();</span><br><span class="line">client.<span class="title function_">searchCurrency</span>(<span class="string">&#x27;人民币&#x27;</span>);</span><br><span class="line">client.<span class="title function_">searchCurrency</span>(<span class="string">&#x27;Dollar&#x27;</span>);</span><br><span class="line">client.<span class="title function_">searchCurrency</span>(<span class="string">&#x27;United States&#x27;</span>);</span><br></pre></td></tr></table></figure><p>我做这个功能的动机非常朴素：<br>当 PM 在群里发“把人民币也支持一下”的时候，你不想回一句“人民币本来就支持啊，只是叫 CNY”，然后被回一句“用户不知道 CNY”。</p><h3 id="4-错误是“可判断”的，不是“随缘-catch”"><a href="#4-错误是“可判断”的，不是“随缘-catch”" class="headerlink" title="4) 错误是“可判断”的，不是“随缘 catch”"></a>4) 错误是“可判断”的，不是“随缘 catch”</h3><p>SDK 统一抛出带 <code>code</code> 的错误，你可以按错误码做兜底策略：</p><figure class="highlight ts"><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="keyword">import</span> &#123; isSDKError &#125; <span class="keyword">from</span> <span class="string">&#x27;exchange-rate-sdk&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  <span class="keyword">await</span> client.<span class="title function_">getRate</span>(<span class="string">&#x27;INVALID&#x27;</span>, <span class="string">&#x27;USD&#x27;</span>);</span><br><span class="line">&#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isSDKError</span>(err)) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(err.<span class="property">code</span>); <span class="comment">// INVALID_CURRENCY_CODE / NETWORK_ERROR / TIMEOUT_ERROR / API_ERROR ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="关于数据源（以及一点点真诚的免责声明）"><a href="#关于数据源（以及一点点真诚的免责声明）" class="headerlink" title="关于数据源（以及一点点真诚的免责声明）"></a>关于数据源（以及一点点真诚的免责声明）</h2><p>SDK 默认请求的是 <a href="https://api.exchangerate-api.com/v4/latest/%7BBASE%7D">https://api.exchangerate-api.com/v4/latest/{BASE}</a>。</p><p>这意味着：</p><ul><li>接入时不需要你额外配置 key（主打一个少操心）</li><li>但它终究是外部 API：可能会有配额、限流、偶发波动<br>所以我才把缓存、超时、重试都做成了默认能力</li></ul><p>另外，汇率适合做“展示&#x2F;估算&#x2F;结算前提示”，不适合做“高频交易&#x2F;套利系统的核心依据”。<br>别问我为什么突然严肃，问就是我不想让你背锅（也不想我背锅）。</p><hr><h2 id="Node-js-浏览器使用提示"><a href="#Node-js-浏览器使用提示" class="headerlink" title="Node.js &#x2F; 浏览器使用提示"></a>Node.js &#x2F; 浏览器使用提示</h2><ul><li>浏览器：直接用就行（有 <code>localStorage</code> 的地方缓存会更香）</li><li>Node.js：需要全局 <code>fetch</code>（Node 18+ 自带；更老版本你可以自己加一个 fetch polyfill）</li></ul><hr><h2 id="最后：你可以怎么开始用它？"><a href="#最后：你可以怎么开始用它？" class="headerlink" title="最后：你可以怎么开始用它？"></a>最后：你可以怎么开始用它？</h2><ol><li>先去 Playground 玩一圈：<a href="https://chengzuopeng.github.io/exchange-rate-sdk/">https://chengzuopeng.github.io/exchange-rate-sdk/</a>  </li><li>在你的项目里装上：<code>npm i exchange-rate-sdk</code>  </li><li>把你那段“汇率请求 + 缓存 + 重试 + 错误处理”的代码删掉（这一步最解压）</li></ol><p>如果你用起来有任何不顺手的地方，欢迎提 issue。<br>我写这个 SDK 的初衷就是：让“汇率这件小事”不要再占用我们太多生命值。</p><p>觉得有用的话，顺手点个 Star ⭐ 支持一下~</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2026/exchange-rate/</id>
    <link href="https://linkdiary.com/2026/exchange-rate/"/>
    <published>2026-01-12T02:00:00.000Z</published>
    <summary>一个轻量级、跨平台（浏览器 + Node.js）的汇率查询 SDK，支持智能缓存与离线货币信息。</summary>
    <title>我写了个“能少写点代码”的汇率 SDK：Exchange Rate SDK</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工程化" scheme="https://linkdiary.com/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-chart-line"></i><p>这篇文章不是一份“正式的技术方案文档”，而是站在<strong>工具作者</strong>的视角，记录我在给 stock-sdk 做一次比较大的架构升级时，<strong>真实的思考过程、取舍以及落地方案</strong>。</p><p>如果你也维护过一个“越写越大的 SDK &#x2F; 工具库”，大概率会在下面的某些场景里看到自己的影子。</p></div><hr><h2 id="背景：当一个-SDK-开始“失控”"><a href="#背景：当一个-SDK-开始“失控”" class="headerlink" title="背景：当一个 SDK 开始“失控”"></a>背景：当一个 SDK 开始“失控”</h2><p>在很长一段时间里，stock-sdk 的代码结构其实非常简单：</p><ul><li>一个 <code>sdk.ts</code></li><li>一些工具函数</li><li>一份类型定义</li></ul><p>这样写在早期非常爽：</p><ul><li>新功能直接往里加</li><li>一个文件就能看到所有逻辑</li><li>调试也很直观</li></ul><p>但问题在于，<strong>它会一直长</strong>。</p><p>当 <code>sdk.ts</code> 的体量来到一千多行之后，一些信号开始变得明显：</p><ul><li>找一个方法要不停地滚动</li><li>改一个功能，总担心影响到旁边完全不相关的逻辑</li><li>测试文件也跟着一起膨胀</li><li>心理上开始抗拒“再往这里加东西”</li></ul><p>这不是某一行代码的问题，而是<strong>结构已经不再适合继续演进</strong>。</p><hr><h2 id="我给这次重构定下的几个底线"><a href="#我给这次重构定下的几个底线" class="headerlink" title="我给这次重构定下的几个底线"></a>我给这次重构定下的几个底线</h2><p>在真正动手之前，我先给自己划了几条“红线”，否则很容易越改越乱：</p><ol><li><strong>对外 API 必须完全兼容</strong>（这是 SDK，不是业务项目）</li><li><strong>可以慢慢迁，但不能一次性推翻重来</strong></li><li><strong>结构要能撑得住未来继续加功能</strong></li><li><strong>不要为了“优雅”而牺牲可读性</strong></li></ol><p>这几条基本决定了后面的所有设计选择。</p><hr><h2 id="一个核心判断：问题不在逻辑，而在“职责混在一起”"><a href="#一个核心判断：问题不在逻辑，而在“职责混在一起”" class="headerlink" title="一个核心判断：问题不在逻辑，而在“职责混在一起”"></a>一个核心判断：问题不在逻辑，而在“职责混在一起”</h2><p>把 <code>sdk.ts</code> 拆开看，会发现里面其实混着几类完全不同的事情：</p><ul><li>HTTP 请求和超时控制</li><li>GBK 编码解码、响应解析</li><li>腾讯 &#x2F; 东财等不同数据源的细节</li><li>行情、K 线、分时、资金流向</li><li>技术指标计算</li></ul><p>这些代码<strong>本身并不复杂</strong>，复杂的是：</p><blockquote><p>它们全都挤在同一个文件里。</p></blockquote><p>所以这次升级的核心目标也很明确：</p><blockquote><p><strong>把“不同职责的代码”拆到不同层级里，而不是单纯拆文件。</strong></p></blockquote><hr><h2 id="新架构的整体思路（先说人话版）"><a href="#新架构的整体思路（先说人话版）" class="headerlink" title="新架构的整体思路（先说人话版）"></a>新架构的整体思路（先说人话版）</h2><p>我最终采用的是一个非常传统、但足够稳的分层思路：</p><ul><li><strong>Core 层</strong>：和“股票”无关的基础设施</li><li><strong>Provider 层</strong>：对接不同数据源的适配器</li><li><strong>Indicators 层</strong>：纯计算逻辑</li><li><strong>SDK 层</strong>：一个薄薄的门面</li><li><strong>Types 层</strong>：纯类型声明</li></ul><p>换句话说：</p><blockquote><p>SDK 只负责“我能做什么”，<br>Provider 负责“数据从哪来”，<br>Core 负责“怎么请求、怎么解析”。</p></blockquote><hr><h2 id="为什么我要引入-Provider-层"><a href="#为什么我要引入-Provider-层" class="headerlink" title="为什么我要引入 Provider 层"></a>为什么我要引入 Provider 层</h2><p>这是这次重构里<strong>最关键的一步</strong>。</p><p>以前的代码里，腾讯、东方财富的数据处理逻辑是交织在一起的：</p><ul><li>一个方法里判断市场</li><li>根据条件拼不同 URL</li><li>再写一堆 if&#x2F;else 解析返回值</li></ul><p>短期看没问题，长期看非常痛苦。</p><p>于是我干脆换了一个思路：</p><blockquote><p><strong>每一个数据源，都只干一件事：把“它自己的接口”适配成 SDK 需要的结构。</strong></p></blockquote><p>比如：</p><ul><li><code>providers/tencent/quote.ts</code> 只负责腾讯的行情</li><li><code>providers/eastmoney/aShareKline.ts</code> 只负责东财的 A 股 K 线</li></ul><p>它们的共同点只有一个：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="attr">client</span>: <span class="title class_">RequestClient</span>, params...) =&gt; <span class="title class_">Promise</span>&lt;T&gt;</span><br></pre></td></tr></table></figure><p>这样一来：</p><ul><li>新增数据源 &#x3D; 新增一个目录</li><li>不需要去碰原有实现</li><li>测试也可以直接按数据源拆开</li></ul><hr><h2 id="Core-层：把“工程脏活”集中处理"><a href="#Core-层：把“工程脏活”集中处理" class="headerlink" title="Core 层：把“工程脏活”集中处理"></a>Core 层：把“工程脏活”集中处理</h2><p>Core 层只做一件事：</p><blockquote><p><strong>处理那些“业务不关心，但每个功能都会用到”的细节。</strong></p></blockquote><p>比如：</p><ul><li>请求超时</li><li>abort 控制</li><li>GBK 解码</li><li>响应解析</li><li>通用工具函数</li></ul><p>这样做有两个非常直接的好处：</p><ol><li>Provider 的代码会变得非常“干净”</li><li>SDK 层几乎不再关心底层实现细节</li></ol><p><code>RequestClient</code> 基本成了整个 SDK 的“地基”。</p><hr><h2 id="SDK-层：刻意做“薄”"><a href="#SDK-层：刻意做“薄”" class="headerlink" title="SDK 层：刻意做“薄”"></a>SDK 层：刻意做“薄”</h2><p>重构后的 <code>sdk.ts</code>，我给自己定了一个硬指标：</p><blockquote><p><strong>不超过 200 行</strong>。</p></blockquote><p>它只做三件事：</p><ul><li>创建并持有 <code>RequestClient</code></li><li>组合 Provider 方法</li><li>做极少量的参数整理</li></ul><p>所有真正的逻辑，都被推到了更合适的地方。</p><p>结果也很直观：</p><ul><li>原来 1000+ 行的文件</li><li>现在稳定在 150 行左右</li></ul><p>阅读体验和心理负担完全不是一个量级。</p><hr><h2 id="分层架构图"><a href="#分层架构图" class="headerlink" title="分层架构图"></a>分层架构图</h2><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><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></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                           Application Layer                              │</span><br><span class="line">│                    (用户代码 / Playground / Demo)                         │</span><br><span class="line">└─────────────────────────────────────┬───────────────────────────────────┘</span><br><span class="line">                                      │</span><br><span class="line">                                      ▼</span><br><span class="line">┌─────────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                              SDK Facade                                  │</span><br><span class="line">│                         StockSDK (src/sdk.ts)                           │</span><br><span class="line">│                                                                          │</span><br><span class="line">│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   │</span><br><span class="line">│   │ A股行情方法  │  │ K线数据方法 │  │ 技术指标方法 │  │  扩展方法   │   │</span><br><span class="line">│   └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘   │</span><br><span class="line">└─────────────────────────────────────┬───────────────────────────────────┘</span><br><span class="line">                                      │</span><br><span class="line">        ┌─────────────────────────────┼─────────────────────────────┐</span><br><span class="line">        │                             │                             │</span><br><span class="line">        ▼                             ▼                             ▼</span><br><span class="line">┌───────────────────┐   ┌───────────────────┐   ┌───────────────────┐</span><br><span class="line">│   Providers Layer │   │  Indicators Layer │   │    Core Layer     │</span><br><span class="line">│   (数据源适配器)   │   │    (指标计算)     │   │   (基础设施)       │</span><br><span class="line">├───────────────────┤   ├───────────────────┤   ├───────────────────┤</span><br><span class="line">│ ┌───────────────┐ │   │ ┌───────────────┐ │   │ ┌───────────────┐ │</span><br><span class="line">│ │   tencent/    │ │   │ │    ma.ts      │ │   │ │  request.ts   │ │</span><br><span class="line">│ │   - quote.ts  │ │   │ ├───────────────┤ │   │ ├───────────────┤ │</span><br><span class="line">│ │   - flow.ts   │ │   │ │   macd.ts     │ │   │ │  parser.ts    │ │</span><br><span class="line">│ │   - timeline  │ │   │ ├───────────────┤ │   │ ├───────────────┤ │</span><br><span class="line">│ └───────────────┘ │   │ │   boll.ts     │ │   │ │  utils.ts     │ │</span><br><span class="line">│ ┌───────────────┐ │   │ ├───────────────┤ │   │ ├───────────────┤ │</span><br><span class="line">│ │  eastmoney/   │ │   │ │   kdj.ts      │ │   │ │  constants.ts │ │</span><br><span class="line">│ │ - aShareKline │ │   │ ├───────────────┤ │   │ └───────────────┘ │</span><br><span class="line">│ │ - hkKline.ts  │ │   │ │    ...        │ │   │                   │</span><br><span class="line">│ │ - usKline.ts  │ │   │ └───────────────┘ │   │                   │</span><br><span class="line">│ └───────────────┘ │   │                   │   │                   │</span><br><span class="line">└───────────────────┘   └───────────────────┘   └───────────────────┘</span><br><span class="line">        │                             │                             │</span><br><span class="line">        └─────────────────────────────┼─────────────────────────────┘</span><br><span class="line">                                      │</span><br><span class="line">                                      ▼</span><br><span class="line">┌─────────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                              Types Layer                                 │</span><br><span class="line">│                         (类型定义，纯声明)                                │</span><br><span class="line">│                                                                          │</span><br><span class="line">│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   │</span><br><span class="line">│   │  common.ts  │  │  quote.ts   │  │  kline.ts   │  │ indicator.ts│   │</span><br><span class="line">│   └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘   │</span><br><span class="line">└─────────────────────────────────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><table><thead><tr><th>模式</th><th>应用场景</th><th>说明</th></tr></thead><tbody><tr><td><strong>门面模式 (Facade)</strong></td><td><code>StockSDK</code> 类</td><td>统一对外接口，隐藏内部复杂性</td></tr><tr><td><strong>适配器模式 (Adapter)</strong></td><td><code>providers/</code></td><td>不同数据源适配为统一接口</td></tr><tr><td><strong>策略模式 (Strategy)</strong></td><td>指标计算、市场识别</td><td>可替换的算法实现</td></tr><tr><td><strong>工厂模式 (Factory)</strong></td><td>请求客户端创建</td><td>统一创建请求实例</td></tr><tr><td><strong>依赖注入 (DI)</strong></td><td>Provider 函数</td><td>注入 RequestClient</td></tr></tbody></table><hr><h2 id="渐进式迁移，而不是“一把梭”"><a href="#渐进式迁移，而不是“一把梭”" class="headerlink" title="渐进式迁移，而不是“一把梭”"></a>渐进式迁移，而不是“一把梭”</h2><p>这次升级我<strong>刻意没有一次性重写</strong>，而是拆成了几个阶段：</p><ol><li>先抽 Core（风险最低）</li><li>再拆 Types</li><li>然后一个一个迁 Provider</li><li>最后才动测试结构</li></ol><p>每一步都有一个明确标准：</p><ul><li>测试必须全绿</li><li>对外 API 不变</li></ul><p>这样哪怕中途停下来，项目也是健康的。</p><hr><h2 id="一个数字对比，最能说明问题"><a href="#一个数字对比，最能说明问题" class="headerlink" title="一个数字对比，最能说明问题"></a>一个数字对比，最能说明问题</h2><table><thead><tr><th>项目</th><th>重构前</th><th>重构后</th></tr></thead><tbody><tr><td><code>sdk.ts</code> 行数</td><td>1198</td><td>~150</td></tr><tr><td>最大单文件</td><td>1198</td><td>&lt;300</td></tr><tr><td>模块数量</td><td>少</td><td>15+</td></tr><tr><td>新增数据源成本</td><td>高</td><td>低</td></tr></tbody></table><p>总代码量几乎没变，但<strong>可维护性完全不是一个级别</strong>。</p><hr><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>这次架构升级并没有引入什么“很新”的东西：</p><ul><li>没有复杂框架</li><li>没有花哨模式</li><li>大多数设计都很传统</li></ul><p>但它解决了一个非常现实的问题：</p><blockquote><p><strong>这个 SDK 还能不能被我和别人继续放心地往下写。</strong></p></blockquote><p>如果你也在维护一个“慢慢变大的工具项目”，我的经验只有一句话：</p><blockquote><p>当你开始抗拒改代码的时候，问题往往已经不是业务复杂，而是结构该升级了。</p></blockquote><p>希望这次 stock-sdk 的重构思路，能对你有所参考。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2025/sdk-upgrade/</id>
    <link href="https://linkdiary.com/2025/sdk-upgrade/"/>
    <published>2025-12-24T13:05:33.000Z</published>
    <summary>我在给 stock-sdk 做一次比较大的架构升级时，真实的思考过程、取舍以及落地方案。</summary>
    <title>从单文件到可扩展：我给 stock-sdk 做的一次架构升级</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-chart-line"></i><p>做过金融相关的前端（哪怕只是行情看板 Demo），大概率都会遇到一些工程层面的麻烦：</p><ul><li>股票行情工具大多集中在 <strong>Python 生态</strong>，前端难以直接使用</li><li>为了拉数据，不得不自建一层后端中转</li><li>接口格式混乱、字段不友好，还要处理 <strong>GBK 编码</strong></li><li>全市场批量拉取时，并发、限频、容错一堆细节要自己兜</li></ul><p><code>stock-sdk</code> 做的事很朴素：把这些公开数据源的“工程脏活”封装起来，提供一个 <strong>零依赖、TypeScript 友好、浏览器与 Node.js 都能用</strong> 的行情 SDK，让前端可以像调用普通 API 一样获取行情、K 线和分时数据。</p></div><p>前阵子有个朋友问我：抓股票行情数据是不是必须学 Python？</p><p>我说不一定啊，JavaScript 也能搞。</p><p>他不信，说你随便找个能用的 JS 股票数据库试试？</p><p>我还真试了。然后发现……他说得对，真没几个能用的。</p><h2 id="被迫营业"><a href="#被迫营业" class="headerlink" title="被迫营业"></a>被迫营业</h2><p>事情是这样的。</p><p>我本来只是想给自己做个小工具，每天早上打开浏览器就能看到自选股涨跌的那种。功能很简单，就是个看板页面，技术上也没什么难度。</p><p>难的是数据从哪来。</p><p>找了一圈才发现，股票数据这块，Python 生态是真的强。AkShare、Tushare 这些库功能齐全、文档完善、社区活跃，用 Python 的话几行代码就搞定了。</p><p>但问题是，我一个写前端的，为了个小看板专门去写 Python 后端？这也太杀鸡用牛刀了。</p><p>npm 上倒是有一些股票相关的包，但要么好几年没更新了，要么只支持 Node.js 不支持浏览器，要么类型定义稀烂。能打的真没几个。</p><p>既然找不到合适的，那就自己造一个呗。</p><h2 id="stock-sdk-诞生了"><a href="#stock-sdk-诞生了" class="headerlink" title="stock-sdk 诞生了"></a>stock-sdk 诞生了</h2><p>花了点时间，我把自己用的代码整理成了一个 npm 包，取名 <strong>stock-sdk</strong>。</p><p>设计思路很简单：让前端能用最熟悉的方式获取股票数据，不要额外依赖，不用搭后端，浏览器里就能跑。</p><p>先看个最简单的例子：</p><figure class="highlight ts"><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">import</span> &#123; <span class="title class_">StockSDK</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;stock-sdk&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> sdk = <span class="keyword">new</span> <span class="title class_">StockSDK</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> quotes = <span class="keyword">await</span> sdk.<span class="title function_">getSimpleQuotes</span>([<span class="string">&#x27;sh000001&#x27;</span>, <span class="string">&#x27;sz000858&#x27;</span>, <span class="string">&#x27;sh600519&#x27;</span>]);</span><br><span class="line"></span><br><span class="line">quotes.<span class="title function_">forEach</span>(<span class="function"><span class="params">q</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;q.name&#125;</span>: <span class="subst">$&#123;q.price&#125;</span> (<span class="subst">$&#123;q.changePercent&#125;</span>%)`</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>没有 API Key，不用注册账号，直接 npm install 就能用。</p><h2 id="功能盘点"><a href="#功能盘点" class="headerlink" title="功能盘点"></a>功能盘点</h2><p>用了一段时间，功能也慢慢加上来了。目前支持的东西还挺多：</p><h3 id="行情获取"><a href="#行情获取" class="headerlink" title="行情获取"></a>行情获取</h3><table><thead><tr><th>市场</th><th>说明</th></tr></thead><tbody><tr><td>A 股</td><td>沪深两市 + 北交所，5000+ 只股票</td></tr><tr><td>港股</td><td>港交所股票行情</td></tr><tr><td>美股</td><td>纳斯达克、纽交所股票</td></tr><tr><td>基金</td><td>公募基金净值和估值</td></tr></tbody></table><p>除了实时行情，还支持历史 K 线（日&#x2F;周&#x2F;月）、分钟 K 线（1&#x2F;5&#x2F;15&#x2F;30&#x2F;60 分钟）、当日分时走势。</p><h3 id="板块数据"><a href="#板块数据" class="headerlink" title="板块数据"></a>板块数据</h3><p>行业板块和概念板块都支持：</p><ul><li>板块列表和实时涨跌</li><li>板块成分股查询</li><li>板块 K 线数据</li></ul><p>这个在分析热点轮动的时候挺有用的。</p><h3 id="技术指标"><a href="#技术指标" class="headerlink" title="技术指标"></a>技术指标</h3><p>不想自己算指标？SDK 里内置了常用的：</p><ul><li><strong>均线</strong>：MA（支持 SMA、EMA、WMA）</li><li><strong>趋势</strong>：MACD、BOLL</li><li><strong>超买超卖</strong>：KDJ、RSI、WR</li><li><strong>其他</strong>：BIAS、CCI、ATR</li></ul><p>一个接口就能拿到 K 线 + 指标数据，直接扔给 ECharts 画图。</p><h3 id="批量能力"><a href="#批量能力" class="headerlink" title="批量能力"></a>批量能力</h3><p>这个是我比较满意的一点。很多接口一次只能查几只股票，想拉全市场数据就得自己处理并发、限流、重试……</p><p>stock-sdk 把这些都封装好了：</p><figure class="highlight ts"><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> allQuotes = <span class="keyword">await</span> sdk.<span class="title function_">getAllAShareQuotes</span>(&#123;</span><br><span class="line">  <span class="attr">batchSize</span>: <span class="number">300</span>,</span><br><span class="line">  <span class="attr">concurrency</span>: <span class="number">5</span>,</span><br><span class="line">  <span class="attr">onProgress</span>: <span class="function">(<span class="params">done, total</span>) =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;done&#125;</span>/<span class="subst">$&#123;total&#125;</span>`</span>),</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>一行代码拉 5000+ 只股票，内置并发控制，还有进度回调。</p><h2 id="技术细节"><a href="#技术细节" class="headerlink" title="技术细节"></a>技术细节</h2><p>说几个我觉得做得还行的点：</p><p><strong>1. 真·零依赖</strong></p><p>没有用 axios，没有 lodash，什么都没有。用的是原生 fetch，所以浏览器和 Node.js 18+ 都能跑。打包出来不到 20KB。</p><p><strong>2. 双模块格式</strong></p><p>同时输出 ESM 和 CommonJS，现代项目用 import，老项目用 require，都没问题。</p><p><strong>3. TypeScript First</strong></p><p>从第一行代码就是用 TypeScript 写的，类型定义是跟着代码一起维护的，不是后补的 <code>.d.ts</code>。写代码的时候 IDE 提示很准，返回字段有哪些、参数怎么传，鼠标悬停就能看到。</p><p><strong>4. 测试覆盖</strong></p><p>核心逻辑都有单元测试，覆盖率 95% 以上。毕竟是处理金融数据的，精度和稳定性还是要保证的。</p><h2 id="我拿它来干嘛"><a href="#我拿它来干嘛" class="headerlink" title="我拿它来干嘛"></a>我拿它来干嘛</h2><p>分享几个我自己的用法：</p><p><strong>早盘快报</strong></p><p>写了个 Node.js 脚本，每天早上 9:30 自动拉一遍自选股行情，推送到微信。</p><p><strong>热门板块监控</strong></p><p>用行业板块接口 + 概念板块接口，每隔 5 分钟刷新一次，看看今天资金在往哪里跑。</p><p><strong>策略回测</strong></p><p>拉历史 K 线 + 技术指标，验证一些简单的交易逻辑。虽然比不上专业的回测框架，但用来快速验证想法够用了。</p><p><strong>给朋友用</strong></p><p>做了个简单的 Web 页面，朋友想查什么股票直接输入代码就行。不用装 App，浏览器打开就能用。</p><h2 id="它不适合什么"><a href="#它不适合什么" class="headerlink" title="它不适合什么"></a>它不适合什么</h2><p>说完优点也说说局限性：</p><ul><li><strong>不适合高频交易</strong>：数据源是公开接口，延迟在秒级，毫秒级的场景不行</li><li><strong>不适合专业量化</strong>：真正做量化还是 Python 生态更成熟，stock-sdk 更适合快速验证和原型开发</li><li><strong>不保证数据准确性</strong>：数据来自腾讯和东财的公开接口，仅供学习参考，投资决策还是要以券商数据为准</li></ul><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>这个项目从自用到开源，也维护了有一段时间了。</p><p>功能在慢慢完善，文档也在慢慢补齐。最近还上线了官网和在线 Playground，不用本地安装就能直接试用。</p><p>如果你也是前端，也想折腾点股票数据相关的东西，可以试试这个库。</p><p>有问题随时提 Issue，我尽量响应。</p><hr><p>🔗 <strong>链接汇总</strong></p><ul><li><a href="https://www.npmjs.com/package/stock-sdk">NPM 包</a></li><li><a href="https://github.com/chengzuopeng/stock-sdk">GitHub 仓库</a></li><li><a href="https://stock-sdk.linkdiary.cn/">官方文档</a></li><li><a href="https://stock-sdk.linkdiary.cn/playground/">在线 Playground</a></li></ul><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">npm install stock-sdk</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">yarn add stock-sdk</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">pnpm add stock-sdk</span><br></pre></td></tr></table></figure><p>觉得有用的话，顺手点个 Star ⭐ 支持一下~</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2025/stock-sdk/</id>
    <link href="https://linkdiary.com/2025/stock-sdk/"/>
    <published>2025-12-18T11:44:01.000Z</published>
    <summary>为浏览器与 Node.js 设计的股票行情 SDK，记录一次工程化封装与取舍。</summary>
    <title>stock-sdk：为前端设计的股票行情 SDK</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="React" scheme="https://linkdiary.com/tags/React/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-rocket"></i><p>在前四篇中，我们走完了 React 核心架构的”主线任务”：Fiber、渲染流程、Hooks、并发模式。从这一篇开始，我们进入”支线副本”——那些你日常开发中高频使用，但底层原理很少有人说清楚的 API。</p><p>本篇覆盖两个主题：<strong>Memoization 双子星（useMemo &#x2F; useCallback）</strong> 和 <strong>Suspense 机制</strong>。它们一个关于”跳过不必要的计算”，一个关于”等待异步数据”，但底层都深深扎根在 Fiber 链表和工作循环中。</p></div><hr><h2 id="第一部分：性能优化的双子星——useMemo-与-useCallback"><a href="#第一部分：性能优化的双子星——useMemo-与-useCallback" class="headerlink" title="第一部分：性能优化的双子星——useMemo 与 useCallback"></a>第一部分：性能优化的双子星——useMemo 与 useCallback</h2><p><code>useMemo</code> 和 <code>useCallback</code> 在社区里经常被过度使用，也经常被误解。要搞清楚什么时候该用、什么时候不该用，最好的方式就是看看它们在源码里到底做了什么——你会发现，它们的实现<strong>朴素得令人意外</strong>。</p><h3 id="1-底层存储结构"><a href="#1-底层存储结构" class="headerlink" title="1. 底层存储结构"></a>1. 底层存储结构</h3><p>在第三篇中我们讲过，所有 Hooks 的数据都挂在 Fiber 节点的 <code>memoizedState</code> 链表上。<code>useMemo</code> 和 <code>useCallback</code> 也不例外，它们存的只是一个简单的二元组：</p><figure class="highlight js"><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="comment">// 每个 useMemo / useCallback 的 Hook 对象中</span></span><br><span class="line">hook.<span class="property">memoizedState</span> = [</span><br><span class="line">  value, <span class="comment">// useMemo 存计算结果，useCallback 存函数引用</span></span><br><span class="line">  deps   <span class="comment">// 依赖数组的快照</span></span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>没有缓存淘汰策略，没有 LRU，没有任何复杂的数据结构——就是一个数组，存一个值和一份依赖。</p><h3 id="2-Mount-阶段：首次渲染"><a href="#2-Mount-阶段：首次渲染" class="headerlink" title="2. Mount 阶段：首次渲染"></a>2. Mount 阶段：首次渲染</h3><p>首次渲染时，React 使用 <code>mountMemo</code> 和 <code>mountCallback</code>：</p><figure class="highlight js"><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 class="comment">// useMemo 的首次渲染（简化自 React 源码）</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">mountMemo</span>(<span class="params">nextCreate, deps</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">mountWorkInProgressHook</span>(); <span class="comment">// 创建新的 Hook 节点，追加到链表</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> nextDeps = deps === <span class="literal">undefined</span> ? <span class="literal">null</span> : deps;</span><br><span class="line">  <span class="keyword">const</span> nextValue = <span class="title function_">nextCreate</span>(); <span class="comment">// 立即执行工厂函数，拿到计算结果</span></span><br><span class="line"></span><br><span class="line">  hook.<span class="property">memoizedState</span> = [nextValue, nextDeps]; <span class="comment">// 存入链表</span></span><br><span class="line">  <span class="keyword">return</span> nextValue;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// useCallback 的首次渲染（简化自 React 源码）</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">mountCallback</span>(<span class="params">callback, deps</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">mountWorkInProgressHook</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> nextDeps = deps === <span class="literal">undefined</span> ? <span class="literal">null</span> : deps;</span><br><span class="line">  hook.<span class="property">memoizedState</span> = [callback, nextDeps]; <span class="comment">// 直接存函数本身，不执行</span></span><br><span class="line">  <span class="keyword">return</span> callback;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意两者的区别：</p><ul><li><code>mountMemo</code>：<strong>执行</strong> <code>nextCreate()</code>，存的是返回值</li><li><code>mountCallback</code>：<strong>不执行</strong> <code>callback</code>，存的是函数引用本身</li></ul><h3 id="3-Update-阶段：后续渲染的”找不同”"><a href="#3-Update-阶段：后续渲染的”找不同”" class="headerlink" title="3. Update 阶段：后续渲染的”找不同”"></a>3. Update 阶段：后续渲染的”找不同”</h3><p>后续渲染时，React 使用 <code>updateMemo</code> 和 <code>updateCallback</code>，核心逻辑就是<strong>比较依赖数组</strong>：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// useMemo 的更新渲染（简化自 React 源码）</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateMemo</span>(<span class="params">nextCreate, deps</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">updateWorkInProgressHook</span>(); <span class="comment">// 从链表取出对应的 Hook</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> nextDeps = deps === <span class="literal">undefined</span> ? <span class="literal">null</span> : deps;</span><br><span class="line">  <span class="keyword">const</span> prevState = hook.<span class="property">memoizedState</span>; <span class="comment">// [prevValue, prevDeps]</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (prevState !== <span class="literal">null</span> &amp;&amp; nextDeps !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> prevDeps = prevState[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 逐项用 Object.is 比较</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">areHookInputsEqual</span>(nextDeps, prevDeps)) &#123;</span><br><span class="line">      <span class="comment">// 依赖没变 → 返回缓存的旧值，跳过计算</span></span><br><span class="line">      <span class="keyword">return</span> prevState[<span class="number">0</span>];</span><br><span class="line">    &#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">const</span> nextValue = <span class="title function_">nextCreate</span>();</span><br><span class="line">  hook.<span class="property">memoizedState</span> = [nextValue, nextDeps];</span><br><span class="line">  <span class="keyword">return</span> nextValue;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// useCallback 的更新渲染（简化自 React 源码）</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateCallback</span>(<span class="params">callback, deps</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">updateWorkInProgressHook</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> nextDeps = deps === <span class="literal">undefined</span> ? <span class="literal">null</span> : deps;</span><br><span class="line">  <span class="keyword">const</span> prevState = hook.<span class="property">memoizedState</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (prevState !== <span class="literal">null</span> &amp;&amp; nextDeps !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> prevDeps = prevState[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">areHookInputsEqual</span>(nextDeps, prevDeps)) &#123;</span><br><span class="line">      <span class="comment">// 依赖没变 → 返回旧的函数引用</span></span><br><span class="line">      <span class="keyword">return</span> prevState[<span class="number">0</span>];</span><br><span class="line">    &#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">  hook.<span class="property">memoizedState</span> = [callback, nextDeps];</span><br><span class="line">  <span class="keyword">return</span> callback;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关键的比较函数 <code>areHookInputsEqual</code> 在第三篇已经见过：</p><figure class="highlight js"><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="keyword">function</span> <span class="title function_">areHookInputsEqual</span>(<span class="params">nextDeps, prevDeps</span>) &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; prevDeps.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="title class_">Object</span>.<span class="title function_">is</span>(nextDeps[i], prevDeps[i])) &#123;</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>; <span class="comment">// 任何一项不同就返回 false</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Object.is</code> 做的是<strong>引用比较</strong>（对于对象）和<strong>值比较</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 每次渲染都会创建新对象 → 引用不同 → useMemo 每次都重新计算</span></span><br><span class="line"><span class="keyword">const</span> result = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> <span class="title function_">expensiveCalc</span>(data), [&#123; <span class="attr">id</span>: <span class="number">1</span> &#125;]);</span><br><span class="line"><span class="comment">//                                                 ^^^^^^^^</span></span><br><span class="line"><span class="comment">//                            每次渲染 &#123; id: 1 &#125; 都是新对象，Object.is 返回 false</span></span><br></pre></td></tr></table></figure><h3 id="4-useCallback-的本质：useMemo-的语法糖"><a href="#4-useCallback-的本质：useMemo-的语法糖" class="headerlink" title="4. useCallback 的本质：useMemo 的语法糖"></a>4. useCallback 的本质：useMemo 的语法糖</h3><p>从源码可以看出，<code>useCallback(fn, deps)</code> 和 <code>useMemo(() =&gt; fn, deps)</code> 在行为上<strong>完全等价</strong>：</p><figure class="highlight js"><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">// 这两行效果完全一样</span></span><br><span class="line"><span class="keyword">const</span> memoizedFn = <span class="title function_">useCallback</span>(fn, deps);</span><br><span class="line"><span class="keyword">const</span> memoizedFn = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> fn, deps);</span><br></pre></td></tr></table></figure><p>区别只是 <code>useCallback</code> 省去了那层包裹的箭头函数——React 源码里甚至有注释说明这一点。</p><h3 id="5-关键误区：useCallback-不能阻止函数创建"><a href="#5-关键误区：useCallback-不能阻止函数创建" class="headerlink" title="5. 关键误区：useCallback 不能阻止函数创建"></a>5. 关键误区：useCallback 不能阻止函数创建</h3><p>这是社区里最常见的误解：</p><blockquote><p><strong>误区</strong>：用了 <code>useCallback</code> 就不会创建新函数了。</p></blockquote><p><strong>真相</strong>：JavaScript 引擎在解析到函数表达式的那一刻，就已经创建了函数对象。<code>useCallback</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Parent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 不管有没有 useCallback，这一行每次渲染都会创建一个新的函数对象</span></span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="title function_">useCallback</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;clicked&#x27;</span>);</span><br><span class="line">  &#125;, []);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Child</span> <span class="attr">onClick</span>=<span class="string">&#123;handleClick&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>useCallback</code> 做的事情是：<strong>每次渲染都创建了新函数，但如果依赖没变，React 把新函数扔掉，返回旧函数的引用</strong>。这样 <code>handleClick</code> 在两次渲染之间保持了引用稳定。</p><h3 id="6-什么时候该用，什么时候不该用"><a href="#6-什么时候该用，什么时候不该用" class="headerlink" title="6. 什么时候该用，什么时候不该用"></a>6. 什么时候该用，什么时候不该用</h3><p>理解了源码之后，判断标准就很清晰了：</p><p><strong>useMemo 该用的场景：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// ✅ 计算成本高（比如对大数组排序/过滤）</span></span><br><span class="line"><span class="keyword">const</span> sortedList = <span class="title function_">useMemo</span>(</span><br><span class="line">  <span class="function">() =&gt;</span> hugeArray.<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> a.<span class="property">score</span> - b.<span class="property">score</span>),</span><br><span class="line">  [hugeArray]</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ❌ 计算成本低——useMemo 本身的开销（比较依赖 + 存储）可能比直接算还大</span></span><br><span class="line"><span class="keyword">const</span> fullName = <span class="title function_">useMemo</span>(</span><br><span class="line">  <span class="function">() =&gt;</span> <span class="string">`<span class="subst">$&#123;firstName&#125;</span> <span class="subst">$&#123;lastName&#125;</span>`</span>, <span class="comment">// 字符串拼接比 Object.is 比较还快</span></span><br><span class="line">  [firstName, lastName]</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p><strong>useCallback 该用的场景：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// ✅ 传给用 React.memo 包裹的子组件</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">MemoChild</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(<span class="keyword">function</span> <span class="title function_">Child</span>(<span class="params">&#123; onClick &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;onClick&#125;</span>&gt;</span>Click<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span>;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Parent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">/* ... */</span> &#125;, []);</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">MemoChild</span> <span class="attr">onClick</span>=<span class="string">&#123;handleClick&#125;</span> /&gt;</span></span>;</span><br><span class="line">  <span class="comment">// 如果不用 useCallback，每次 Parent 渲染都会传新的函数引用</span></span><br><span class="line">  <span class="comment">// React.memo 的浅比较会认为 props 变了，导致 MemoChild 重新渲染</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ❌ 子组件没有用 React.memo，useCallback 纯属浪费</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Parent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">/* ... */</span> &#125;, []);</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">NormalChild</span> <span class="attr">onClick</span>=<span class="string">&#123;handleClick&#125;</span> /&gt;</span></span>;</span><br><span class="line">  <span class="comment">// NormalChild 没有 memo，Parent 渲染时它一定会重新渲染</span></span><br><span class="line">  <span class="comment">// 引用是否稳定没有任何意义</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-React-memo：useMemo-useCallback-的”搭档”"><a href="#7-React-memo：useMemo-useCallback-的”搭档”" class="headerlink" title="7. React.memo：useMemo&#x2F;useCallback 的”搭档”"></a>7. React.memo：useMemo&#x2F;useCallback 的”搭档”</h3><p>单独说一下 <code>React.memo</code>，因为它和上面两个 Hook 是配套使用的。</p><p><code>React.memo</code> 是一个<strong>高阶组件</strong>，它在组件外层加了一层浅比较：</p><figure class="highlight js"><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="comment">// React.memo 的简化实现</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">memo</span>(<span class="params">Component, compare</span>) &#123;</span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">MemoComponent</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title class_">MemoComponent</span>.<span class="property">$$typeof</span> = <span class="variable constant_">REACT_MEMO_TYPE</span>;</span><br><span class="line">  <span class="title class_">MemoComponent</span>.<span class="property">compare</span> = compare || shallowEqual; <span class="comment">// 默认浅比较</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">MemoComponent</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>beginWork</code> 阶段，React 遇到 Memo 类型的组件时会：</p><figure class="highlight js"><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="keyword">function</span> <span class="title function_">updateMemoComponent</span>(<span class="params">current, workInProgress, renderLanes</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title class_">Component</span> = workInProgress.<span class="property">type</span>; <span class="comment">// MemoComponent</span></span><br><span class="line">  <span class="keyword">const</span> compare = <span class="title class_">Component</span>.<span class="property">compare</span> || shallowEqual;</span><br><span class="line">  <span class="keyword">const</span> prevProps = current.<span class="property">memoizedProps</span>;</span><br><span class="line">  <span class="keyword">const</span> nextProps = workInProgress.<span class="property">pendingProps</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 用 compare 函数对比 props</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">compare</span>(prevProps, nextProps)) &#123;</span><br><span class="line">    <span class="comment">// props 没变 → 跳过这个组件的渲染</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">bailoutOnAlreadyFinishedWork</span>(current, workInProgress);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// props 变了 → 正常渲染</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">updateFunctionComponent</span>(current, workInProgress, renderLanes);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>shallowEqual</code> 的逻辑：</p><figure class="highlight js"><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 class="keyword">function</span> <span class="title function_">shallowEqual</span>(<span class="params">objA, objB</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title class_">Object</span>.<span class="title function_">is</span>(objA, objB)) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> keysA = <span class="title class_">Object</span>.<span class="title function_">keys</span>(objA);</span><br><span class="line">  <span class="keyword">const</span> keysB = <span class="title class_">Object</span>.<span class="title function_">keys</span>(objB);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (keysA.<span class="property">length</span> !== keysB.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; keysA.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (</span><br><span class="line">      !objB.<span class="title function_">hasOwnProperty</span>(keysA[i]) ||</span><br><span class="line">      !<span class="title class_">Object</span>.<span class="title function_">is</span>(objA[keysA[i]], objB[keysA[i]])</span><br><span class="line">    ) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</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 class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以完整的优化链条是：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    A[&quot;Parent 重新渲染&quot;] --&gt; B[&quot;useCallback 保证&lt;br&#x2F;&gt;handleClick 引用不变&quot;]    B --&gt; C[&quot;传 props 给 MemoChild&quot;]    C --&gt; D[&quot;React.memo 用 shallowEqual&lt;br&#x2F;&gt;比较 props&quot;]    D --&gt; E[&quot;handleClick 引用没变&lt;br&#x2F;&gt;→ props 相同&quot;]    E --&gt; F[&quot;跳过 MemoChild 的渲染 bailout&quot;]  </pre></div><p>三者缺一不可：没有 <code>React.memo</code>，<code>useCallback</code> 白费；没有 <code>useCallback</code>，<code>React.memo</code> 每次都通不过 props 比较。</p><hr><h2 id="第二部分：Suspense-的悬停魔法"><a href="#第二部分：Suspense-的悬停魔法" class="headerlink" title="第二部分：Suspense 的悬停魔法"></a>第二部分：Suspense 的悬停魔法</h2><p>如果说 Memoization 是”跳过不必要的工作”，那 Suspense 就是”等待还没准备好的工作”。React Suspense 的实现方式相当”离经叛道”——它利用了 JavaScript 的<strong>错误处理机制</strong>来实现异步流程控制。</p><h3 id="1-核心原理：抛出-Promise"><a href="#1-核心原理：抛出-Promise" class="headerlink" title="1. 核心原理：抛出 Promise"></a>1. 核心原理：抛出 Promise</h3><p>在传统的 React 组件中，如果数据还没加载完，你通常会这样写：</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">// 传统方式：自己管理加载状态</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UserProfile</span>(<span class="params">&#123; userId &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [data, setData] = <span class="title function_">useState</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> [loading, setLoading] = <span class="title function_">useState</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">fetchUser</span>(userId).<span class="title function_">then</span>(<span class="function"><span class="params">d</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setData</span>(d);</span><br><span class="line">      <span class="title function_">setLoading</span>(<span class="literal">false</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;, [userId]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (loading) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Spinner</span> /&gt;</span></span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;data.name&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Suspense 的思路完全不同——组件不管理加载状态，<strong>而是在数据没准备好时直接”中断”自己的渲染</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Suspense 方式：组件假设数据一定存在</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UserProfile</span>(<span class="params">&#123; userId &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> data = resource.<span class="title function_">read</span>(userId);</span><br><span class="line">  <span class="comment">// 如果数据没好 → read() 内部会 throw 一个 Promise</span></span><br><span class="line">  <span class="comment">// 如果数据好了 → 直接返回数据</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;data.name&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用时用 Suspense 包裹</span></span><br><span class="line">&lt;<span class="title class_">Suspense</span> fallback=&#123;<span class="language-xml"><span class="tag">&lt;<span class="name">Spinner</span> /&gt;</span></span>&#125;&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">UserProfile</span> <span class="attr">userId</span>=<span class="string">&#123;1&#125;</span> /&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">Suspense</span>&gt;</span><br></pre></td></tr></table></figure><p><code>resource.read()</code> 内部的实现大致是：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 简化版的&quot;数据资源&quot;实现</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">createResource</span>(<span class="params">fetcher</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> status = <span class="string">&#x27;pending&#x27;</span>;</span><br><span class="line">  <span class="keyword">let</span> result;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> promise = <span class="title function_">fetcher</span>().<span class="title function_">then</span>(</span><br><span class="line">    <span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      status = <span class="string">&#x27;success&#x27;</span>;</span><br><span class="line">      result = data;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="function">(<span class="params">error</span>) =&gt;</span> &#123;</span><br><span class="line">      status = <span class="string">&#x27;error&#x27;</span>;</span><br><span class="line">      result = error;</span><br><span class="line">    &#125;</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="title function_">read</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="keyword">switch</span> (status) &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&#x27;pending&#x27;</span>:</span><br><span class="line">          <span class="keyword">throw</span> promise;    <span class="comment">// 数据没好 → 抛出 Promise</span></span><br><span class="line">        <span class="keyword">case</span> <span class="string">&#x27;error&#x27;</span>:</span><br><span class="line">          <span class="keyword">throw</span> result;     <span class="comment">// 出错了 → 抛出 Error</span></span><br><span class="line">        <span class="keyword">case</span> <span class="string">&#x27;success&#x27;</span>:</span><br><span class="line">          <span class="keyword">return</span> result;    <span class="comment">// 数据好了 → 正常返回</span></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>注意这里最关键的一行：<strong><code>throw promise</code></strong>。这不是 <code>throw new Error()</code>，而是抛出了一个 Promise 对象。这是 Suspense 整个机制的核心。</p><h3 id="2-React-工作循环中的-try…catch"><a href="#2-React-工作循环中的-try…catch" class="headerlink" title="2. React 工作循环中的 try…catch"></a>2. React 工作循环中的 try…catch</h3><p>在第一篇中我们讲过，React 的工作循环（workLoop）会逐个处理 Fiber 节点。为了支持 Suspense，这个循环被包裹在一个 <code>try...catch</code> 中：</p><figure class="highlight js"><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="comment">// renderRootSync / renderRootConcurrent 内部（简化）</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">renderRoot</span>(<span class="params">root, lanes</span>) &#123;</span><br><span class="line">  <span class="comment">// 准备 workInProgress</span></span><br><span class="line">  <span class="title function_">prepareFreshStack</span>(root, lanes);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">do</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// 正常的工作循环</span></span><br><span class="line">      <span class="title function_">workLoop</span>();</span><br><span class="line">      <span class="keyword">break</span>; <span class="comment">// 正常结束</span></span><br><span class="line">    &#125; <span class="keyword">catch</span> (thrownValue) &#123;</span><br><span class="line">      <span class="comment">// 有东西被 throw 了！</span></span><br><span class="line">      <span class="title function_">handleThrow</span>(root, thrownValue);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">while</span> (<span class="literal">true</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>handleThrow</code> 的逻辑是判断被抛出的东西是什么类型：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">handleThrow</span>(<span class="params">root, thrownValue</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (</span><br><span class="line">    thrownValue !== <span class="literal">null</span> &amp;&amp;</span><br><span class="line">    <span class="keyword">typeof</span> thrownValue === <span class="string">&#x27;object&#x27;</span> &amp;&amp;</span><br><span class="line">    <span class="keyword">typeof</span> thrownValue.<span class="property">then</span> === <span class="string">&#x27;function&#x27;</span> <span class="comment">// duck typing：像 Promise 吗？</span></span><br><span class="line">  ) &#123;</span><br><span class="line">    <span class="comment">// 是 Promise → 这是一个 Suspense 场景</span></span><br><span class="line">    <span class="keyword">const</span> wakeable = thrownValue;</span><br><span class="line">    <span class="title function_">throwException</span>(root, workInProgress, wakeable);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 是 Error → 这是一个 ErrorBoundary 场景</span></span><br><span class="line">    <span class="keyword">throw</span> thrownValue; <span class="comment">// 向上传播，被 ErrorBoundary 的 catch 接住</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-挂起（Suspend）：Suspense-边界的查找"><a href="#3-挂起（Suspend）：Suspense-边界的查找" class="headerlink" title="3. 挂起（Suspend）：Suspense 边界的查找"></a>3. 挂起（Suspend）：Suspense 边界的查找</h3><p>当 React 确认这是一个 Suspense 场景后，它需要做两件事：</p><p><strong>第一步：向上查找最近的 Suspense 边界</strong></p><figure class="highlight js"><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="keyword">function</span> <span class="title function_">throwException</span>(<span class="params">root, sourceFiber, wakeable</span>) &#123;</span><br><span class="line">  <span class="comment">// 给当前节点标记为&quot;未完成&quot;</span></span><br><span class="line">  sourceFiber.<span class="property">flags</span> |= <span class="title class_">Incomplete</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 向上遍历，查找最近的 &lt;Suspense&gt; 组件</span></span><br><span class="line">  <span class="keyword">let</span> node = sourceFiber.<span class="property">return</span>;</span><br><span class="line">  <span class="keyword">while</span> (node !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (node.<span class="property">tag</span> === <span class="title class_">SuspenseComponent</span>) &#123;</span><br><span class="line">      <span class="comment">// 找到了 Suspense 边界</span></span><br><span class="line"></span><br><span class="line">      <span class="comment">// 在 Suspense 节点上记录这个 wakeable（Promise）</span></span><br><span class="line">      <span class="keyword">const</span> wakeables = node.<span class="property">updateQueue</span>;</span><br><span class="line">      <span class="keyword">if</span> (wakeables === <span class="literal">null</span>) &#123;</span><br><span class="line">        node.<span class="property">updateQueue</span> = <span class="keyword">new</span> <span class="title class_">Set</span>([wakeable]);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        wakeables.<span class="title function_">add</span>(wakeable);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 给 Suspense 节点打上标记：需要显示 fallback</span></span><br><span class="line">      node.<span class="property">flags</span> |= <span class="title class_">ShouldCapture</span>;</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    node = node.<span class="property">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果找不到 Suspense 边界 → 这是一个未捕获的异常</span></span><br><span class="line">  <span class="comment">// React 会把整个应用标记为错误状态</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>第二步：渲染 fallback</strong></p><p>找到 Suspense 边界后，React 会让这个 Suspense 组件渲染它的 <code>fallback</code> 而不是 <code>children</code>：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 简化的 Suspense 组件 beginWork 逻辑</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateSuspenseComponent</span>(<span class="params">current, workInProgress</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> nextProps = workInProgress.<span class="property">pendingProps</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> showFallback = <span class="title function_">isSuspended</span>(workInProgress);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (showFallback) &#123;</span><br><span class="line">    <span class="comment">// 挂起了 → 渲染 fallback</span></span><br><span class="line">    <span class="keyword">const</span> fallbackChildren = nextProps.<span class="property">fallback</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 标记 primary children 为隐藏（offscreen）</span></span><br><span class="line">    <span class="keyword">const</span> primaryChildFragment = <span class="title function_">createFiberFromOffscreen</span>(nextProps.<span class="property">children</span>);</span><br><span class="line">    primaryChildFragment.<span class="property">mode</span> |= <span class="title class_">OffscreenMode</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建 fallback 的 Fiber</span></span><br><span class="line">    <span class="keyword">const</span> fallbackChildFragment = <span class="title function_">createFiberFromFragment</span>(fallbackChildren);</span><br><span class="line"></span><br><span class="line">    workInProgress.<span class="property">child</span> = primaryChildFragment;</span><br><span class="line">    primaryChildFragment.<span class="property">sibling</span> = fallbackChildFragment;</span><br><span class="line">    primaryChildFragment.<span class="property">return</span> = workInProgress;</span><br><span class="line">    fallbackChildFragment.<span class="property">return</span> = workInProgress;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> fallbackChildFragment; <span class="comment">// beginWork 返回 fallback，优先渲染它</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 没有挂起 → 正常渲染 children</span></span><br><span class="line">    <span class="keyword">const</span> primaryChildren = nextProps.<span class="property">children</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">reconcileChildren</span>(current, workInProgress, primaryChildren);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-恢复（Resume）：Promise-决议后的重渲染"><a href="#4-恢复（Resume）：Promise-决议后的重渲染" class="headerlink" title="4. 恢复（Resume）：Promise 决议后的重渲染"></a>4. 恢复（Resume）：Promise 决议后的重渲染</h3><p>挂起后，React 需要在 Promise 决议时收到通知。它会给 Promise 附加一个回调：</p><figure class="highlight js"><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">function</span> <span class="title function_">attachPingListener</span>(<span class="params">root, wakeable, lanes</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">ping</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// Promise 完成了！安排一次新的渲染</span></span><br><span class="line">    <span class="comment">// 这次渲染时，resource.read() 会返回数据而不是 throw</span></span><br><span class="line">    <span class="title function_">ensureRootIsScheduled</span>(root);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 监听 Promise 的完成</span></span><br><span class="line">  wakeable.<span class="title function_">then</span>(ping, ping); <span class="comment">// 无论 resolve 还是 reject 都要处理</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当 Promise 决议后，整个流程重新来一遍：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    subgraph render1[&quot;第一次渲染&quot;]        A1[&quot;UserProfile 执行&quot;] --&gt; A2[&quot;resource.read → throw Promise&quot;]        A2 --&gt; A3[&quot;React catch → 找到 Suspense&quot;]        A3 --&gt; A4[&quot;渲染 Spinner，监听 Promise&quot;]    end    A4 --&gt; B[&quot;Promise 决议&lt;br&#x2F;&gt;ping 触发 → 安排新一轮渲染&quot;]    subgraph render2[&quot;第二次渲染&quot;]        C1[&quot;UserProfile 执行&quot;] --&gt; C2[&quot;resource.read → 返回 data&quot;]        C2 --&gt; C3[&quot;正常渲染内容&quot;]        C3 --&gt; C4[&quot;隐藏 fallback，显示 children&quot;]    end    B --&gt; C1  </pre></div><p>用时间线来表示：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    A[&quot;Render 1&lt;br&#x2F;&gt;throw Promise&lt;br&#x2F;&gt;渲染 Spinner&quot;] --&gt; B[&quot;网络请求进行中...&quot;]    B --&gt; C[&quot;Promise resolved&lt;br&#x2F;&gt;ping → scheduleWork&quot;]    C --&gt; D[&quot;Render 2&lt;br&#x2F;&gt;正常返回 data&lt;br&#x2F;&gt;Spinner 消失，内容出现&quot;]  </pre></div><h3 id="5-Suspense-与并发模式的协作"><a href="#5-Suspense-与并发模式的协作" class="headerlink" title="5. Suspense 与并发模式的协作"></a>5. Suspense 与并发模式的协作</h3><p>Suspense 在并发模式下会变得更加强大。当 Suspense 与 <code>useTransition</code> 配合时，React 可以在”等待数据”的同时<strong>保持旧 UI</strong>，而不是立即显示 Loading：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [tab, setTab] = <span class="title function_">useState</span>(<span class="string">&#x27;home&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> [isPending, startTransition] = <span class="title function_">useTransition</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">switchTab</span> = (<span class="params">newTab</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">startTransition</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setTab</span>(newTab); <span class="comment">// 过渡更新</span></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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">TabBar</span> <span class="attr">currentTab</span>=<span class="string">&#123;tab&#125;</span> <span class="attr">onSwitch</span>=<span class="string">&#123;switchTab&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">opacity:</span> <span class="attr">isPending</span> ? <span class="attr">0.7</span> <span class="attr">:</span> <span class="attr">1</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">Spinner</span> /&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">TabContent</span> <span class="attr">tab</span>=<span class="string">&#123;tab&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <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">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个场景下：</p><ol><li>用户点击新 Tab</li><li><code>startTransition</code> 标记这个更新为低优先级</li><li>React 开始在后台渲染新 Tab 的内容</li><li>如果新 Tab 的组件 <code>throw Promise</code>（数据还没好），React <strong>不会</strong>立即显示 Spinner</li><li>而是继续显示旧 Tab 的内容（加上 <code>isPending</code> 的半透明效果）</li><li>直到数据加载完成、新界面完全准备好后，一次性切换过去</li></ol><p>这就是”避免 Loading 闪烁”的底层机制——并发模式让 React 可以<strong>选择性地延迟 Suspense fallback 的显示</strong>。</p><h3 id="6-Suspense-的边界嵌套"><a href="#6-Suspense-的边界嵌套" class="headerlink" title="6. Suspense 的边界嵌套"></a>6. Suspense 的边界嵌套</h3><p>多个 Suspense 可以嵌套使用，React 会按照”就近原则”向上查找边界：</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></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Suspense</span> fallback=&#123;<span class="language-xml"><span class="tag">&lt;<span class="name">PageSkeleton</span> /&gt;</span></span>&#125;&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Header</span> /&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">ContentSpinner</span> /&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">MainContent</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">SidebarSkeleton</span> /&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Sidebar</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Footer</span> /&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">Suspense</span>&gt;</span><br></pre></td></tr></table></figure><p>如果 <code>Sidebar</code> 的数据还没好，只有最内层的 Suspense 会显示 <code>&lt;SidebarSkeleton /&gt;</code>，<code>Header</code>、<code>MainContent</code>、<code>Footer</code> 都不受影响。</p><p>如果 <code>MainContent</code> 也挂了，中间层的 Suspense 接管，显示 <code>&lt;ContentSpinner /&gt;</code>——此时 <code>Sidebar</code> 的 Suspense 也被包含在 fallback 范围内。</p><p>这种嵌套设计让你可以细粒度地控制加载体验：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    subgraph s1[&quot;Sidebar 未就绪&quot;]        direction LR        A1[&quot;Header ✓&quot;] --- A2[&quot;MainContent ✓&quot;] --- A3[&quot;SidebarSkeleton&quot;]    end    subgraph s2[&quot;MainContent 未就绪&quot;]        direction LR        B1[&quot;Header ✓&quot;] --- B2[&quot;ContentSpinner&quot;] --- B3[&quot;Footer ✓&quot;]    end    subgraph s3[&quot;全部未就绪&quot;]        C1[&quot;PageSkeleton&quot;]    end  </pre></div><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>主题</th><th>核心机制</th><th>一句话概括</th></tr></thead><tbody><tr><td><strong>useMemo</strong></td><td>deps 浅比较 → 复用旧值或重新计算</td><td>用比较的开销换取计算的跳过</td></tr><tr><td><strong>useCallback</strong></td><td>deps 浅比较 → 复用旧函数引用</td><td>useMemo 的语法糖，核心是引用稳定性</td></tr><tr><td><strong>React.memo</strong></td><td>props 浅比较 → bailout 跳过渲染</td><td>useCallback 的搭档，缺一不可</td></tr><tr><td><strong>Suspense</strong></td><td>throw Promise → catch → 渲染 fallback → Promise 决议 → 重新渲染</td><td>用”假装报错”实现异步流程控制</td></tr></tbody></table><p>Memoization 是关于<strong>比较与跳过</strong>——通过对比依赖数组，决定是复用旧值还是计算新值。</p><p>Suspense 是关于<strong>中断与恢复</strong>——通过抛出 Promise 中断渲染，利用 Promise 的状态变化重启渲染。</p><p>两者在底层都依赖 Fiber 架构提供的基础能力：链表存储让数据有地方放，可中断的工作循环让异常能被安全处理。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2025/react-source-5/</id>
    <link href="https://linkdiary.com/2025/react-source-5/"/>
    <published>2025-07-08T13:17:42.000Z</published>
    <summary>拆解 useMemo/useCallback 在 Fiber 链表中的存储与比较机制，以及 Suspense 如何通过&quot;抛出 Promise&quot;实现异步渲染的底层原理。</summary>
    <title>React 源码深潜（五）：Memoization 双子星与 Suspense 的悬停魔法</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="React" scheme="https://linkdiary.com/tags/React/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-bolt"></i><p>在前三篇中，我们构建了 Fiber 架构（骨架）、梳理了渲染流程（经络）、解析了 Hooks 原理（记忆）。现在，我们要解决最后一个、也是最直接影响用户体验的问题：</p><p><strong>如何让应用在繁重的计算下依然保持丝滑的响应？</strong></p><p>这就是 React 18 并发模式要回答的核心命题。</p></div><hr><h2 id="1-经典难题：输入框卡顿之谜"><a href="#1-经典难题：输入框卡顿之谜" class="headerlink" title="1. 经典难题：输入框卡顿之谜"></a>1. 经典难题：输入框卡顿之谜</h2><p>想象一个常见的场景：<strong>搜索框</strong>。</p><p>用户在输入框打字，下方需要根据输入内容渲染一个包含几千条数据的列表。</p><p>这里有两个性质完全不同的任务：</p><ul><li><strong>输入回显（High Priority）</strong>：用户敲键盘，屏幕需要<strong>立即</strong>显示字符，延迟超过 100ms 就会产生明显的”卡顿感”</li><li><strong>列表渲染（Low Priority）</strong>：下方的搜索结果需要大量 DOM 操作，可能要跑几百毫秒</li></ul><p><strong>在 React 18 之前（或不加优化）：</strong></p><p>React 会把这两个更新当成一回事，放在同一个渲染任务里处理。一旦开始渲染列表，主线程就被占满。用户敲键盘，但屏幕上字出不来——因为浏览器正忙着画那几千条列表。</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    A[&quot;用户按下 &#39;a&#39;&quot;] --&gt; B[&quot;setInput(&#39;a&#39;)&quot;]    A --&gt; C[&quot;setList filter(&#39;a&#39;)&quot;]    B --&gt; D[&quot;合并为一个同步任务&quot;]    C --&gt; D    D --&gt; E[&quot;渲染输入框 + 渲染列表&lt;br&#x2F;&gt;主线程被占满&lt;br&#x2F;&gt;200ms 阻塞&quot;]    E --&gt; F[&quot;用户看到 &#39;a&#39; 出现&lt;br&#x2F;&gt;已经过去 200ms，感觉很卡&quot;]  </pre></div><hr><h2 id="2-并发模式的核心思想：任务分优先级"><a href="#2-并发模式的核心思想：任务分优先级" class="headerlink" title="2. 并发模式的核心思想：任务分优先级"></a>2. 并发模式的核心思想：任务分优先级</h2><p>React 18 引入的<strong>并发模式（Concurrent Mode）</strong> 核心思想非常直觉：<strong>不是所有更新都一样紧急。</strong></p><p>React 把更新分成了两大类：</p><ol><li><strong>紧急更新（Urgent updates）</strong>：打字、点击、拖拽——这些必须在毫秒级内响应</li><li><strong>过渡更新（Transition updates）</strong>：渲染列表、切换页面——这些稍慢一点，用户完全可以接受</li></ol><p>打个比方：</p><blockquote><p>React 15 就像一条单车道高速公路——所有车都排着队。前面的大货车（列表渲染）开得慢，后面的跑车（用户打字）被堵死了。</p><p>React 18 开辟了 VIP 车道——跑车可以随时超车，甚至可以让大货车先靠边停（中断低优先级渲染），等跑车过去了，大货车再继续。</p></blockquote><hr><h2 id="3-Lane-模型：React-的优先级系统"><a href="#3-Lane-模型：React-的优先级系统" class="headerlink" title="3. Lane 模型：React 的优先级系统"></a>3. Lane 模型：React 的优先级系统</h2><p>在 React 内部，优先级不是简单的”高 &#x2F; 中 &#x2F; 低”，而是用一套精心设计的<strong>位运算系统</strong>来表达的，叫做 <strong>Lane 模型</strong>。</p><h3 id="用二进制位表达优先级"><a href="#用二进制位表达优先级" class="headerlink" title="用二进制位表达优先级"></a>用二进制位表达优先级</h3><p>每条”车道（Lane）”对应一个二进制位。位置越低（越靠右），优先级越高：</p><figure class="highlight js"><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="comment">// React 源码中的 Lane 定义（简化）</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">NoLane</span>              = <span class="number">0b0000000000000000000000000000000</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">SyncLane</span>            = <span class="number">0b0000000000000000000000000000010</span>; <span class="comment">// 同步，最高</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">InputContinuousLane</span> = <span class="number">0b0000000000000000000000000001000</span>; <span class="comment">// 连续输入（拖拽）</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">DefaultLane</span>         = <span class="number">0b0000000000000000000000000100000</span>; <span class="comment">// 默认</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">TransitionLane1</span>     = <span class="number">0b0000000000000000000001000000000</span>; <span class="comment">// 过渡 1</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">TransitionLane2</span>     = <span class="number">0b0000000000000000000010000000000</span>; <span class="comment">// 过渡 2</span></span><br><span class="line"><span class="comment">// ... 一共 31 条 Lane</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">IdleLane</span>            = <span class="number">0b0100000000000000000000000000000</span>; <span class="comment">// 空闲</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">OffscreenLane</span>       = <span class="number">0b1000000000000000000000000000000</span>; <span class="comment">// 离屏</span></span><br></pre></td></tr></table></figure><h3 id="为什么用位运算"><a href="#为什么用位运算" class="headerlink" title="为什么用位运算"></a>为什么用位运算</h3><p>位运算让优先级的<strong>合并、比较、过滤</strong>变得非常高效：</p><figure class="highlight js"><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="comment">// 合并多个 Lane（用 OR）</span></span><br><span class="line"><span class="keyword">const</span> mergedLanes = <span class="title class_">SyncLane</span> | <span class="title class_">DefaultLane</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 判断某个 Lane 是否被包含（用 AND）</span></span><br><span class="line"><span class="keyword">const</span> isSyncIncluded = (mergedLanes &amp; <span class="title class_">SyncLane</span>) !== <span class="number">0</span>; <span class="comment">// true</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 从集合中移除某个 Lane（用 AND NOT）</span></span><br><span class="line"><span class="keyword">const</span> remaining = mergedLanes &amp; ~<span class="title class_">SyncLane</span>; <span class="comment">// 只剩 DefaultLane</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 取最高优先级的 Lane（取最低位的 1）</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getHighestPriorityLane</span>(<span class="params">lanes</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> lanes &amp; -lanes;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Lane-如何影响渲染"><a href="#Lane-如何影响渲染" class="headerlink" title="Lane 如何影响渲染"></a>Lane 如何影响渲染</h3><p>当一个 <code>setState</code> 被调用时，React 会给这个更新分配一条 Lane：</p><figure class="highlight js"><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">function</span> <span class="title function_">requestUpdateLane</span>(<span class="params">fiber</span>) &#123;</span><br><span class="line">  <span class="comment">// 如果当前在 transition 中，分配 TransitionLane</span></span><br><span class="line">  <span class="keyword">if</span> (isTransition) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">claimNextTransitionLane</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="keyword">const</span> eventLane = <span class="title function_">getCurrentEventPriority</span>();</span><br><span class="line">  <span class="comment">// 点击 → SyncLane</span></span><br><span class="line">  <span class="comment">// 拖拽 → InputContinuousLane</span></span><br><span class="line">  <span class="comment">// 默认 → DefaultLane</span></span><br><span class="line">  <span class="keyword">return</span> eventLane;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后在 Render 阶段，React 只处理<strong>当前批次选中的 Lane</strong> 对应的更新：</p><figure class="highlight js"><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">function</span> <span class="title function_">beginWork</span>(<span class="params">current, workInProgress, renderLanes</span>) &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 该节点上如果有 update，但 update 的 Lane 不在 renderLanes 中</span></span><br><span class="line">  <span class="comment">// React 会跳过这个 update，等后续批次再处理</span></span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isSubsetOfLanes</span>(renderLanes, updateLane)) &#123;</span><br><span class="line">    <span class="comment">// 优先级不够，先跳过</span></span><br><span class="line">    <span class="keyword">return</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="title function_">processUpdateQueue</span>(workInProgress, renderLanes);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是”高优先级打断低优先级”的底层机制：</p><ol><li>低优先级的 Transition 更新开始渲染（分配了 TransitionLane）</li><li>用户敲键盘，产生一个 SyncLane 的更新</li><li>Scheduler 发现有更高优先级的任务，中断当前渲染</li><li>用 SyncLane 重新从根节点开始 Render，只处理高优先级的更新</li><li>高优先级渲染完毕、Commit 完毕后，再回来继续（或重启）低优先级的渲染</li></ol><hr><h2 id="4-useTransition：手动标记”不着急”的更新"><a href="#4-useTransition：手动标记”不着急”的更新" class="headerlink" title="4. useTransition：手动标记”不着急”的更新"></a>4. useTransition：手动标记”不着急”的更新</h2><p>有了 Lane 模型的底层支持，React 提供了 <code>useTransition</code> Hook，让开发者手动告诉 React：”这个更新没那么急。”</p><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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useState, useTransition &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">SearchList</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [input, setInput] = <span class="title function_">useState</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> [list, setList] = <span class="title function_">useState</span>([]);</span><br><span class="line">  <span class="keyword">const</span> [isPending, startTransition] = <span class="title function_">useTransition</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params">e</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 紧急任务：直接更新，保证输入框瞬间响应</span></span><br><span class="line">    <span class="title function_">setInput</span>(e.<span class="property">target</span>.<span class="property">value</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 过渡任务：用 startTransition 包裹</span></span><br><span class="line">    <span class="title function_">startTransition</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> results = <span class="title function_">heavyFilterFunction</span>(e.<span class="property">target</span>.<span class="property">value</span>);</span><br><span class="line">      <span class="title function_">setList</span>(results);</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">div</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">value</span>=<span class="string">&#123;input&#125;</span> <span class="attr">onChange</span>=<span class="string">&#123;handleChange&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;isPending ? <span class="tag">&lt;<span class="name">p</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">p</span>&gt;</span> : <span class="tag">&lt;<span class="name">List</span> <span class="attr">data</span>=<span class="string">&#123;list&#125;</span> /&gt;</span>&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="底层发生了什么"><a href="#底层发生了什么" class="headerlink" title="底层发生了什么"></a>底层发生了什么</h3><p>当你调用 <code>startTransition(callback)</code> 时，React 内部的处理流程是：</p><figure class="highlight js"><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 class="keyword">function</span> <span class="title function_">startTransition</span>(<span class="params">callback</span>) &#123;</span><br><span class="line">  <span class="comment">// 1. 先把 isPending 置为 true（这个 setState 是高优先级的！）</span></span><br><span class="line">  <span class="title function_">setPending</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 设置一个标记，表示后续的 setState 应该使用 TransitionLane</span></span><br><span class="line">  <span class="keyword">const</span> prevTransition = <span class="title class_">ReactCurrentBatchConfig</span>.<span class="property">transition</span>;</span><br><span class="line">  <span class="title class_">ReactCurrentBatchConfig</span>.<span class="property">transition</span> = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// 3. 执行回调——里面的 setState 会被分配 TransitionLane</span></span><br><span class="line">    <span class="title function_">callback</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 再把 isPending 置为 false（也是 TransitionLane）</span></span><br><span class="line">    <span class="title function_">setPending</span>(<span class="literal">false</span>);</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="comment">// 5. 恢复标记</span></span><br><span class="line">    <span class="title class_">ReactCurrentBatchConfig</span>.<span class="property">transition</span> = prevTransition;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>分解一下优先级分配：</p><table><thead><tr><th>操作</th><th>分配的 Lane</th><th>优先级</th></tr></thead><tbody><tr><td><code>setPending(true)</code></td><td>SyncLane（正常 setState）</td><td>高</td></tr><tr><td><code>setList(results)</code></td><td>TransitionLane（在 transition 上下文中）</td><td>低</td></tr><tr><td><code>setPending(false)</code></td><td>TransitionLane</td><td>低</td></tr></tbody></table><p>这意味着：</p><ol><li><code>setPending(true)</code> 立即被处理，UI 上马上显示 Loading</li><li><code>setList(results)</code> 被标记为低优先级，React 在”空闲时间”慢慢渲染</li><li>如果用户继续打字，React 会中断列表渲染，优先处理新的输入</li><li>等列表渲染完成，<code>setPending(false)</code> 也会一起被提交，Loading 消失</li></ol><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    A[&quot;用户按下 &#39;a&#39;&quot;] --&gt; B[&quot;setInput(&#39;a&#39;) → SyncLane → 立即渲染&quot;]    A --&gt; C[&quot;setPending true → SyncLane → 显示 Loading&quot;]    A --&gt; D[&quot;setList filter(&#39;a&#39;) → TransitionLane → 后台渲染&quot;]    B --&gt; E[&quot;渲染输入框 + 显示 Loading&lt;br&#x2F;&gt;几 ms，用户立刻看到 &#39;a&#39;&quot;]    E --&gt; F{&quot;用户又按了 &#39;ab&#39;?&quot;}    F --&gt;|是| G[&quot;中断列表渲染&lt;br&#x2F;&gt;立即渲染输入框 &#39;ab&#39;&lt;br&#x2F;&gt;重新开始列表渲染&quot;]    F --&gt;|否| H[&quot;继续渲染列表&lt;br&#x2F;&gt;每 5ms 一个切片&lt;br&#x2F;&gt;渲染完毕 → Loading 消失&quot;]  </pre></div><hr><h2 id="5-useDeferredValue：另一种标记”不着急”的方式"><a href="#5-useDeferredValue：另一种标记”不着急”的方式" class="headerlink" title="5. useDeferredValue：另一种标记”不着急”的方式"></a>5. useDeferredValue：另一种标记”不着急”的方式</h2><p>除了 <code>useTransition</code>，React 18 还提供了 <code>useDeferredValue</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><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">function</span> <span class="title function_">SearchList</span>(<span class="params">&#123; query &#125;</span>) &#123;</span><br><span class="line">  <span class="comment">// deferredQuery 会&quot;延迟&quot;跟上 query 的更新</span></span><br><span class="line">  <span class="keyword">const</span> deferredQuery = <span class="title function_">useDeferredValue</span>(query);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 当 query 和 deferredQuery 不一致时，说明正在&quot;追赶&quot;</span></span><br><span class="line">  <span class="keyword">const</span> isStale = query !== deferredQuery;</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">div</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">opacity:</span> <span class="attr">isStale</span> ? <span class="attr">0.6</span> <span class="attr">:</span> <span class="attr">1</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">List</span> <span class="attr">query</span>=<span class="string">&#123;deferredQuery&#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></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="useTransition-vs-useDeferredValue"><a href="#useTransition-vs-useDeferredValue" class="headerlink" title="useTransition vs useDeferredValue"></a>useTransition vs useDeferredValue</h3><table><thead><tr><th>维度</th><th>useTransition</th><th>useDeferredValue</th></tr></thead><tbody><tr><td><strong>控制对象</strong></td><td>控制 <code>setState</code> 的优先级</td><td>控制<strong>值</strong>的更新时机</td></tr><tr><td><strong>使用场景</strong></td><td>你能直接控制 setState 的地方</td><td>值来自 props 或外部，你控制不了更新方式</td></tr><tr><td><strong>返回值</strong></td><td><code>[isPending, startTransition]</code></td><td>延迟后的值</td></tr><tr><td><strong>底层实现</strong></td><td>直接修改 Lane 分配</td><td>内部触发一个低优先级的 setState</td></tr></tbody></table><p>简单来说：</p><ul><li>如果你能控制 <code>setState</code>，用 <code>useTransition</code></li><li>如果值是从 props 传进来的，你控制不了上层怎么更新，用 <code>useDeferredValue</code></li></ul><hr><h2 id="6-终极对比：Debounce-vs-Throttle-vs-useTransition"><a href="#6-终极对比：Debounce-vs-Throttle-vs-useTransition" class="headerlink" title="6. 终极对比：Debounce vs Throttle vs useTransition"></a>6. 终极对比：Debounce vs Throttle vs useTransition</h2><p>这是”面试杀手级”问题：<strong>为什么不用防抖（Debounce）或节流（Throttle）？</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 方案 A：防抖</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params">e</span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">setInput</span>(e.<span class="property">target</span>.<span class="property">value</span>);</span><br><span class="line">  <span class="title function_">debounce</span>(<span class="function">() =&gt;</span> <span class="title function_">setList</span>(<span class="title function_">filter</span>(e.<span class="property">target</span>.<span class="property">value</span>)), <span class="number">300</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方案 B：useTransition</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params">e</span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">setInput</span>(e.<span class="property">target</span>.<span class="property">value</span>);</span><br><span class="line">  <span class="title function_">startTransition</span>(<span class="function">() =&gt;</span> <span class="title function_">setList</span>(<span class="title function_">filter</span>(e.<span class="property">target</span>.<span class="property">value</span>)));</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>看起来效果差不多？本质区别很大：</p><table><thead><tr><th>特性</th><th>Debounce（防抖）</th><th>Throttle（节流）</th><th>useTransition（并发）</th></tr></thead><tbody><tr><td><strong>策略</strong></td><td>消极等待</td><td>定频执行</td><td>积极工作</td></tr><tr><td><strong>原理</strong></td><td>强制让 CPU 休息（等 300ms）</td><td>每隔固定时间执行一次</td><td>利用按键间隙尝试渲染</td></tr><tr><td><strong>CPU 利用率</strong></td><td>低（等待期空转）</td><td>中等</td><td>高（榨干每一毫秒）</td></tr><tr><td><strong>用户体验</strong></td><td>停止输入后还要等一会</td><td>固定延迟</td><td>即时响应，不卡顿</td></tr><tr><td><strong>是否中断旧渲染</strong></td><td>否（只是延迟触发）</td><td>否</td><td><strong>是</strong>（旧渲染被丢弃）</td></tr><tr><td><strong>响应速度</strong></td><td>最慢（必须等超时）</td><td>中等</td><td><strong>最快</strong>（有空就渲染）</td></tr><tr><td><strong>等待时间</strong></td><td>固定（300ms）</td><td>固定</td><td>动态（取决于 CPU 空闲）</td></tr></tbody></table><p>核心差异用一句话概括：</p><blockquote><p><strong>Debounce 是让任务晚点开始，useTransition 是让任务可以被打断。</strong></p></blockquote><p>Debounce 在等待的 300ms 里 CPU 什么都没干——白白浪费了。而 useTransition 在用户两次按键之间的几十毫秒间隙里，可能已经渲染了一部分列表。如果用户不再打字，列表会比 debounce 方案更快出现；如果用户继续打字，旧的渲染会被中断，不会卡顿。</p><hr><h2 id="7-并发模式的本质：可中断的渲染"><a href="#7-并发模式的本质：可中断的渲染" class="headerlink" title="7. 并发模式的本质：可中断的渲染"></a>7. 并发模式的本质：可中断的渲染</h2><p>回顾整个系列，并发模式并不是一个独立的”新功能”，而是前三篇所有基础设施的<strong>最终组合</strong>：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    A[&quot;Fiber 链表结构&quot;] --&gt; A1[&quot;让渲染可以暂停在任意节点&quot;]    B[&quot;Scheduler 调度器&quot;] --&gt; B1[&quot;控制什么时候让出主线程&quot;]    C[&quot;双缓存机制&quot;] --&gt; C1[&quot;未完成的渲染可以安全丢弃&quot;]    D[&quot;Lane 优先级模型&quot;] --&gt; D1[&quot;区分不同更新的紧急程度&quot;]  </pre></div><p>这四层叠在一起，才实现了”高优先级可以打断低优先级”这个看似简单的用户体验优化。</p><hr><h2 id="8-全系列总结"><a href="#8-全系列总结" class="headerlink" title="8. 全系列总结"></a>8. 全系列总结</h2><p>至此，我们已经完成了 React 核心原理的四大拼图：</p><table><thead><tr><th>篇章</th><th>主题</th><th>解决的问题</th><th>核心概念</th></tr></thead><tbody><tr><td><strong>第一篇</strong></td><td>Fiber 架构</td><td>渲染不可中断导致卡顿</td><td>Fiber 链表、时间切片、Scheduler</td></tr><tr><td><strong>第二篇</strong></td><td>渲染流程</td><td>更新过程中 UI 可能不一致</td><td>双缓存、Render&#x2F;Commit 分离</td></tr><tr><td><strong>第三篇</strong></td><td>Hooks 原理</td><td>函数组件如何保存状态</td><td>闭包快照、链表存储、更新队列</td></tr><tr><td><strong>第四篇</strong></td><td>并发模式</td><td>繁重计算导致输入卡顿</td><td>Lane 优先级、useTransition</td></tr></tbody></table><p>这四篇的关系是<strong>层层递进</strong>的：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    A[&quot;第一篇（骨架）&lt;br&#x2F;&gt;Fiber&lt;br&#x2F;&gt;可中断的结构&quot;] --&gt; B[&quot;第二篇（经络）&lt;br&#x2F;&gt;Render&#x2F;Commit&lt;br&#x2F;&gt;安全的更新流程&quot;]    B --&gt; C[&quot;第三篇（记忆）&lt;br&#x2F;&gt;Hooks&lt;br&#x2F;&gt;状态的持久化&quot;]    C --&gt; D[&quot;第四篇（智能）&lt;br&#x2F;&gt;Concurrent&lt;br&#x2F;&gt;优先级调度&quot;]  </pre></div><p>拥有了这套思维模型，再去看 React 的源码或面对面试难题，你将不再是死记硬背，而是能够从设计哲学的角度去理解每一行代码为什么要那样写。</p><hr><h2 id="相关链接"><a href="#相关链接" class="headerlink" title="相关链接"></a>相关链接</h2><ul><li><a href="https://github.com/facebook/react">React 源码仓库</a></li><li><a href="https://react.dev/blog/2022/03/29/react-v18">React 并发模式文档</a></li><li><a href="https://github.com/facebook/react/pull/18796">Lane 模型设计 RFC</a></li></ul><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2025/react-source-4/</id>
    <link href="https://linkdiary.com/2025/react-source-4/"/>
    <published>2025-07-01T11:23:11.000Z</published>
    <summary>从输入框卡顿问题出发，拆解 React 18 并发模式的底层实现——Lane 优先级模型、useTransition 的中断恢复机制，以及它和防抖的本质区别。</summary>
    <title>React 源码深潜（四）：并发模式与 useTransition 的&quot;时间魔法&quot;</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="React" scheme="https://linkdiary.com/tags/React/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-link"></i><p>在 React 16.8 之前，只有 Class 组件才能保存状态。Hooks 的出现让函数组件拥有了”记忆”，但如果你停下来想一想，会发现一个反直觉的事实：<strong>函数执行完毕后，所有局部变量都会被销毁——React 到底把状态藏在了哪里？</strong></p><p>本篇从这个问题出发，完整拆解 Hooks 的两大核心原理。</p></div><hr><h2 id="1-直击灵魂的拷问：状态去哪了？"><a href="#1-直击灵魂的拷问：状态去哪了？" class="headerlink" title="1. 直击灵魂的拷问：状态去哪了？"></a>1. 直击灵魂的拷问：状态去哪了？</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Counter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Render:&#x27;</span>, count);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(count + 1)&#125;&gt;&#123;count&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当我们点击按钮：</p><ol><li><code>setCount(1)</code> 触发 React 更新</li><li><code>Counter</code> 函数<strong>重新执行</strong></li><li>再次运行到 <code>const [count, setCount] = useState(0)</code></li></ol><p><strong>关键问题：</strong> 按理说 <code>count</code> 应该被重置为初始值 <code>0</code>，为什么它变成了 <code>1</code>？</p><p>要回答这个问题，需要理解两个独立但紧密协作的机制：</p><ul><li><strong>闭包快照</strong>：每一次渲染都是独立的”帧”</li><li><strong>链表存储</strong>：状态不在函数内部，而在 Fiber 节点上</li></ul><hr><h2 id="2-原理一：闭包快照（Closure-Snapshot）"><a href="#2-原理一：闭包快照（Closure-Snapshot）" class="headerlink" title="2. 原理一：闭包快照（Closure Snapshot）"></a>2. 原理一：闭包快照（Closure Snapshot）</h2><h3 id="每一次渲染都是一张独立的”照片”"><a href="#每一次渲染都是一张独立的”照片”" class="headerlink" title="每一次渲染都是一张独立的”照片”"></a>每一次渲染都是一张独立的”照片”</h3><p>React 的函数组件通过<strong>闭包</strong>机制，让每一次渲染都拥有独立的 props 和 state 值。</p><p>你可以把 <code>Counter</code> 想象成一台拍立得相机：</p><ul><li><strong>每一次渲染</strong>（Render），就是按下一次快门</li><li><strong>Props 和 State</strong>，就是被定格在照片里的”景色”</li></ul><p>当我们说 <code>count</code> 变了，不是同一个变量的值变了，而是 React <strong>重新拍了一张照片</strong>，这张新照片里的 <code>count</code> 是 <code>1</code>。</p><p>用代码来理解：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 第一次渲染时，React 执行：</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Counter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> count = <span class="number">0</span>; <span class="comment">// 从 Fiber 拿到的值</span></span><br><span class="line">  <span class="comment">// 这次渲染中，所有用到 count 的地方都&quot;定格&quot;在 0</span></span><br><span class="line">  <span class="comment">// 包括事件处理函数、useEffect 回调，全部捕获的是 0</span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(0 + 1)&#125;&gt;&#123;0&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第二次渲染时，React 执行的其实是&quot;另一个函数作用域&quot;：</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Counter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> count = <span class="number">1</span>; <span class="comment">// 从 Fiber 拿到的新值</span></span><br><span class="line">  <span class="comment">// 这次渲染中，所有地方捕获的都是 1</span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(1 + 1)&#125;&gt;&#123;1&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每次渲染创建了一个新的函数作用域，闭包捕获了当次渲染的值。这就是为什么 React 文档说”每一次渲染都有它自己的 props 和 state”。</p><h3 id="闭包陷阱：证据确凿"><a href="#闭包陷阱：证据确凿" class="headerlink" title="闭包陷阱：证据确凿"></a>闭包陷阱：证据确凿</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Counter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleAlert</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 这里的 count 是点击那一瞬间的&quot;快照&quot;</span></span><br><span class="line">      <span class="comment">// 即使你后来狂点按钮把界面上的 count 变成了 10</span></span><br><span class="line">      <span class="comment">// 3 秒后弹出的依然是点击时的那个数字</span></span><br><span class="line">      <span class="title function_">alert</span>(<span class="string">&#x27;Count is: &#x27;</span> + count);</span><br><span class="line">    &#125;, <span class="number">3000</span>);</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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>Count: &#123;count&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(count + 1)&#125;&gt;增加<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">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleAlert&#125;</span>&gt;</span>3 秒后弹窗<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">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实验步骤：</p><ol><li>页面显示 <code>Count: 0</code></li><li>点击”3 秒后弹窗”</li><li>立刻疯狂点击”增加”，让计数器涨到 10</li><li>3 秒后弹窗显示的是 <code>Count is: 0</code>，而不是 <code>10</code></li></ol><p><strong>原因</strong>：<code>handleAlert</code> 是在 <code>count === 0</code> 的那次渲染中被创建的。<code>setTimeout</code> 的回调通过闭包捕获了那次渲染中的 <code>count</code> 值。后续的渲染创建了新的 <code>handleAlert</code> 函数，但旧的闭包已经”定格”了。</p><blockquote><p><strong>结论：在 React 函数组件中，State 是常量，不是变量。每次渲染都有它自己独立的 State 值。</strong></p></blockquote><h3 id="useRef：逃出闭包快照的”逃生通道”"><a href="#useRef：逃出闭包快照的”逃生通道”" class="headerlink" title="useRef：逃出闭包快照的”逃生通道”"></a>useRef：逃出闭包快照的”逃生通道”</h3><p>如果确实需要在回调中读到”最新值”，React 提供了 <code>useRef</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><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">function</span> <span class="title function_">Counter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> countRef = <span class="title function_">useRef</span>(count);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 每次渲染都把最新值同步到 ref</span></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    countRef.<span class="property">current</span> = count;</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_">handleAlert</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// ref.current 始终指向最新值</span></span><br><span class="line">      <span class="title function_">alert</span>(<span class="string">&#x27;Latest count is: &#x27;</span> + countRef.<span class="property">current</span>);</span><br><span class="line">    &#125;, <span class="number">3000</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">&#125;</span><br></pre></td></tr></table></figure><p><code>useRef</code> 返回的对象在整个组件生命周期里始终是<strong>同一个引用</strong>（不会随着渲染创建新对象），所以 <code>.current</code> 始终指向最新的值。</p><hr><h2 id="3-原理二：链表存储（Linked-List-Storage）"><a href="#3-原理二：链表存储（Linked-List-Storage）" class="headerlink" title="3. 原理二：链表存储（Linked List Storage）"></a>3. 原理二：链表存储（Linked List Storage）</h2><p>闭包解释了”每次渲染看到的值为什么不同”，但还没回答最根本的问题：<strong>值本身存在哪？</strong></p><h3 id="状态挂在-Fiber-节点上"><a href="#状态挂在-Fiber-节点上" class="headerlink" title="状态挂在 Fiber 节点上"></a>状态挂在 Fiber 节点上</h3><p>还记得上一篇学的 Fiber 节点吗？每个组件对应的 Fiber 都有一个 <code>memoizedState</code> 属性，<strong>Hooks 的数据就挂在这里</strong>。</p><p>但一个组件可能有多个 Hook（<code>useState</code>、<code>useEffect</code>、<code>useMemo</code>…），React 怎么区分它们？</p><p><strong>答案是：依靠调用顺序。</strong> React 内部用一个<strong>单向链表</strong>把它们串起来。</p><h3 id="链表结构详解"><a href="#链表结构详解" class="headerlink" title="链表结构详解"></a>链表结构详解</h3><p>每个 Hook 在内部对应一个 <code>Hook</code> 对象，大致结构如下：</p><figure class="highlight ts"><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">interface</span> <span class="title class_">Hook</span> &#123;</span><br><span class="line">  <span class="attr">memoizedState</span>: <span class="built_in">any</span>;        <span class="comment">// 存储的值（useState 存状态值，useEffect 存 effect 对象）</span></span><br><span class="line">  <span class="attr">baseState</span>: <span class="built_in">any</span>;            <span class="comment">// 基础状态（用于并发模式下的状态计算）</span></span><br><span class="line">  <span class="attr">baseQueue</span>: <span class="title class_">Update</span> | <span class="literal">null</span>;  <span class="comment">// 未处理的更新队列</span></span><br><span class="line">  <span class="attr">queue</span>: <span class="title class_">UpdateQueue</span> | <span class="literal">null</span>; <span class="comment">// 当前更新队列</span></span><br><span class="line">  <span class="attr">next</span>: <span class="title class_">Hook</span> | <span class="literal">null</span>;         <span class="comment">// 指向下一个 Hook → 形成链表</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>假如我们在组件里写了三个 Hook：</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"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&#x27;Mary&#x27;</span>);    <span class="comment">// Hook 1</span></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">/* ... */</span> &#125;);              <span class="comment">// Hook 2</span></span><br><span class="line">  <span class="keyword">const</span> [age, setAge] = <span class="title function_">useState</span>(<span class="number">18</span>);          <span class="comment">// Hook 3</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>React 解析后，Fiber 上的 <code>memoizedState</code> 是一个链表：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    F[&quot;fiber.memoizedState&quot;] --&gt; H1    H1[&quot;Hook 1&lt;br&#x2F;&gt;memoizedState: &#39;Mary&#39;&lt;br&#x2F;&gt;next: Hook2&quot;] --&gt; H2[&quot;Hook 2&lt;br&#x2F;&gt;memoizedState: effectObj&lt;br&#x2F;&gt;next: Hook3&quot;]    H2 --&gt; H3[&quot;Hook 3&lt;br&#x2F;&gt;memoizedState: 18&lt;br&#x2F;&gt;next: null&quot;]  </pre></div><h3 id="首次渲染-vs-更新渲染"><a href="#首次渲染-vs-更新渲染" class="headerlink" title="首次渲染 vs 更新渲染"></a>首次渲染 vs 更新渲染</h3><p>React 内部为 Hooks 维护了两套实现，通过一个全局的 <code>ReactCurrentDispatcher</code> 来切换：</p><figure class="highlight js"><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">// React 内部（简化）</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">HooksDispatcherOnMount</span> = &#123;</span><br><span class="line">  <span class="attr">useState</span>: mountState,</span><br><span class="line">  <span class="attr">useEffect</span>: mountEffect,</span><br><span class="line">  <span class="comment">// ...</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 class_">HooksDispatcherOnUpdate</span> = &#123;</span><br><span class="line">  <span class="attr">useState</span>: updateState,</span><br><span class="line">  <span class="attr">useEffect</span>: updateEffect,</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">renderWithHooks</span>(<span class="params">current, workInProgress, Component, props</span>) &#123;</span><br><span class="line">  <span class="comment">// 根据是首次渲染还是更新，切换不同的 dispatcher</span></span><br><span class="line">  <span class="keyword">if</span> (current !== <span class="literal">null</span> &amp;&amp; current.<span class="property">memoizedState</span> !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="title class_">ReactCurrentDispatcher</span>.<span class="property">current</span> = <span class="title class_">HooksDispatcherOnUpdate</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="title class_">ReactCurrentDispatcher</span>.<span class="property">current</span> = <span class="title class_">HooksDispatcherOnMount</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="keyword">const</span> children = <span class="title class_">Component</span>(props);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> children;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>首次渲染（Mount）：</strong> 每个 <code>useState</code> 调用都会创建一个新的 Hook 对象，串入链表：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">mountState</span>(<span class="params">initialState</span>) &#123;</span><br><span class="line">  <span class="comment">// 创建新的 Hook 对象，追加到链表末尾</span></span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">mountWorkInProgressHook</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果初始值是函数，执行它</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> initialState === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">    initialState = <span class="title function_">initialState</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  hook.<span class="property">memoizedState</span> = initialState;</span><br><span class="line">  hook.<span class="property">baseState</span> = initialState;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 创建更新队列</span></span><br><span class="line">  <span class="keyword">const</span> queue = &#123; <span class="attr">pending</span>: <span class="literal">null</span>, <span class="attr">dispatch</span>: <span class="literal">null</span>, <span class="comment">/* ... */</span> &#125;;</span><br><span class="line">  hook.<span class="property">queue</span> = queue;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 创建 dispatch 函数（就是 setXxx）</span></span><br><span class="line">  <span class="keyword">const</span> dispatch = (queue.<span class="property">dispatch</span> = dispatchSetState.<span class="title function_">bind</span>(</span><br><span class="line">    <span class="literal">null</span>,</span><br><span class="line">    currentlyRenderingFiber,</span><br><span class="line">    queue</span><br><span class="line">  ));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> [hook.<span class="property">memoizedState</span>, dispatch];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>更新渲染（Update）：</strong> 每个 <code>useState</code> 调用不再创建 Hook，而是从已有链表中取下一个 Hook：</p><figure class="highlight js"><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">function</span> <span class="title function_">updateState</span>(<span class="params">initialState</span>) &#123;</span><br><span class="line">  <span class="comment">// 从链表中取出下一个 Hook（按顺序取）</span></span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">updateWorkInProgressHook</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 计算最新状态：遍历 queue 中的所有 update</span></span><br><span class="line">  <span class="keyword">const</span> queue = hook.<span class="property">queue</span>;</span><br><span class="line">  <span class="keyword">let</span> newState = hook.<span class="property">baseState</span>;</span><br><span class="line">  <span class="keyword">let</span> update = queue.<span class="property">pending</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (update !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> action = update.<span class="property">action</span>;</span><br><span class="line">      <span class="comment">// 如果 action 是函数（如 setCount(prev =&gt; prev + 1)），执行它</span></span><br><span class="line">      <span class="comment">// 如果是值（如 setCount(5)），直接赋值</span></span><br><span class="line">      newState = <span class="keyword">typeof</span> action === <span class="string">&#x27;function&#x27;</span> ? <span class="title function_">action</span>(newState) : action;</span><br><span class="line">      update = update.<span class="property">next</span>;</span><br><span class="line">    &#125; <span class="keyword">while</span> (update !== <span class="literal">null</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  hook.<span class="property">memoizedState</span> = newState;</span><br><span class="line">  <span class="keyword">return</span> [newState, queue.<span class="property">dispatch</span>];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="关键函数：updateWorkInProgressHook"><a href="#关键函数：updateWorkInProgressHook" class="headerlink" title="关键函数：updateWorkInProgressHook"></a>关键函数：updateWorkInProgressHook</h3><p>这个函数做的事情很简单但很关键——<strong>从链表上取出当前位置的 Hook，然后把指针移到下一个</strong>：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> currentHook = <span class="literal">null</span>;        <span class="comment">// current 树上的当前 Hook</span></span><br><span class="line"><span class="keyword">let</span> workInProgressHook = <span class="literal">null</span>; <span class="comment">// WIP 树上的当前 Hook</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateWorkInProgressHook</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 从 current 树的 Hook 链表取出下一个</span></span><br><span class="line">  <span class="keyword">if</span> (currentHook === <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> current = currentlyRenderingFiber.<span class="property">alternate</span>;</span><br><span class="line">    currentHook = current.<span class="property">memoizedState</span>; <span class="comment">// 链表头</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    currentHook = currentHook.<span class="property">next</span>; <span class="comment">// 链表的下一个</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 基于 current Hook 创建 WIP Hook（复用/克隆）</span></span><br><span class="line">  <span class="keyword">const</span> newHook = &#123;</span><br><span class="line">    <span class="attr">memoizedState</span>: currentHook.<span class="property">memoizedState</span>,</span><br><span class="line">    <span class="attr">baseState</span>: currentHook.<span class="property">baseState</span>,</span><br><span class="line">    <span class="attr">baseQueue</span>: currentHook.<span class="property">baseQueue</span>,</span><br><span class="line">    <span class="attr">queue</span>: currentHook.<span class="property">queue</span>,</span><br><span class="line">    <span class="attr">next</span>: <span class="literal">null</span>,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 追加到 WIP 链表</span></span><br><span class="line">  <span class="keyword">if</span> (workInProgressHook === <span class="literal">null</span>) &#123;</span><br><span class="line">    currentlyRenderingFiber.<span class="property">memoizedState</span> = newHook; <span class="comment">// 链表头</span></span><br><span class="line">    workInProgressHook = newHook;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    workInProgressHook.<span class="property">next</span> = newHook;</span><br><span class="line">    workInProgressHook = newHook;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> workInProgressHook;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里就能看清楚 React 的策略了：<strong>按照 Hook 在组件中的调用顺序，逐个从旧链表取值，构建新链表</strong>——不靠变量名，纯靠顺序。</p><hr><h2 id="4-为什么不能写在-if-里（Hook-规则的底层原因）"><a href="#4-为什么不能写在-if-里（Hook-规则的底层原因）" class="headerlink" title="4. 为什么不能写在 if 里（Hook 规则的底层原因）"></a>4. 为什么不能写在 <code>if</code> 里（Hook 规则的底层原因）</h2><p>理解了”链表 + 按顺序取”的机制，就彻底明白了为什么官网强调：<strong>不要在循环、条件或嵌套函数中调用 Hook</strong>。</p><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Form</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&#x27;Mary&#x27;</span>);      <span class="comment">// Hook A</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 假设某次渲染 Math.random() 返回了一个小值</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title class_">Math</span>.<span class="title function_">random</span>() &gt; <span class="number">0.5</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> [surname, setSurname] = <span class="title function_">useState</span>(<span class="string">&#x27;Poppins&#x27;</span>); <span class="comment">// Hook B</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> [width, setWidth] = <span class="title function_">useState</span>(<span class="number">500</span>);       <span class="comment">// Hook C</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>第一次渲染</strong>（所有 Hook 都执行了）：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    A[&quot;Hook A &#39;Mary&#39;&lt;br&#x2F;&gt;顺序 1&quot;] --&gt;|next| B[&quot;Hook B &#39;Poppins&#39;&lt;br&#x2F;&gt;顺序 2&quot;]    B --&gt;|next| C[&quot;Hook C 500&lt;br&#x2F;&gt;顺序 3&quot;]  </pre></div><p><strong>第二次渲染</strong>（条件不满足，Hook B 被跳过了）：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    subgraph list[&quot;链表不变&quot;]        direction LR        A[&quot;Hook A &#39;Mary&#39;&quot;] --&gt;|next| B[&quot;Hook B &#39;Poppins&#39;&quot;] --&gt;|next| C[&quot;Hook C 500&quot;]    end    D[&quot;第 1 个 useState → 取链表第 1 个 → &#39;Mary&#39; ✅&quot;] --&gt; E[&quot;第 2 个 useState → 取链表第 2 个 → &#39;Poppins&#39; ❌&quot;]    E --&gt; G[&quot;width 期望拿到 500&lt;br&#x2F;&gt;结果拿到了 &#39;Poppins&#39;&quot;]  </pre></div><p>链表上的顺序是固定的，但代码中 Hook 的调用顺序变了。React 傻傻地按顺序取，取到的值和 Hook 对不上，整个状态系统就乱套了。</p><h3 id="eslint-plugin-react-hooks"><a href="#eslint-plugin-react-hooks" class="headerlink" title="eslint-plugin-react-hooks"></a>eslint-plugin-react-hooks</h3><p>正是因为这个原因，React 官方提供了 ESLint 插件 <code>eslint-plugin-react-hooks</code>，它会在编译时检查：</p><ul><li>Hook 是否在函数组件或自定义 Hook 的<strong>顶层</strong>调用</li><li>Hook 的调用是否可能被条件&#x2F;循环影响</li></ul><p>这不是”编码风格”层面的约束，而是<strong>数据结构层面的硬性要求</strong>。</p><hr><h2 id="5-useState-的更新队列：批量更新与优先级"><a href="#5-useState-的更新队列：批量更新与优先级" class="headerlink" title="5. useState 的更新队列：批量更新与优先级"></a>5. useState 的更新队列：批量更新与优先级</h2><p>当你调用 <code>setCount(count + 1)</code> 时，React 并不会立即重新渲染组件。它会创建一个 <strong>Update 对象</strong>并挂到 Hook 的 <code>queue</code> 上：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">dispatchSetState</span>(<span class="params">fiber, queue, action</span>) &#123;</span><br><span class="line">  <span class="comment">// 创建 Update 对象</span></span><br><span class="line">  <span class="keyword">const</span> update = &#123;</span><br><span class="line">    action,              <span class="comment">// 新的值或者 updater 函数</span></span><br><span class="line">    <span class="attr">lane</span>: <span class="title function_">requestUpdateLane</span>(), <span class="comment">// 优先级</span></span><br><span class="line">    <span class="attr">next</span>: <span class="literal">null</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="keyword">const</span> pending = queue.<span class="property">pending</span>;</span><br><span class="line">  <span class="keyword">if</span> (pending === <span class="literal">null</span>) &#123;</span><br><span class="line">    update.<span class="property">next</span> = update; <span class="comment">// 自己指向自己，形成环</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    update.<span class="property">next</span> = pending.<span class="property">next</span>;</span><br><span class="line">    pending.<span class="property">next</span> = update;</span><br><span class="line">  &#125;</span><br><span class="line">  queue.<span class="property">pending</span> = update;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 调度更新</span></span><br><span class="line">  <span class="title function_">scheduleUpdateOnFiber</span>(fiber, lane);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意 queue 是一个<strong>环形链表</strong>——这样 <code>queue.pending</code> 始终指向最后一个 update，而 <code>queue.pending.next</code> 就是第一个 update，方便从头遍历。</p><p>当多个 <code>setState</code> 在同一个事件中被调用时，React 会把它们合并成一批（<strong>Automatic Batching</strong>，React 18 的特性）：</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="keyword">function</span> <span class="title function_">handleClick</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="title function_">setCount</span>(<span class="function"><span class="params">c</span> =&gt;</span> c + <span class="number">1</span>);  <span class="comment">// Update 1 → 挂到 queue</span></span><br><span class="line">  <span class="title function_">setFlag</span>(<span class="function"><span class="params">f</span> =&gt;</span> !f);      <span class="comment">// Update 2 → 挂到另一个 Hook 的 queue</span></span><br><span class="line">  <span class="title function_">setName</span>(<span class="string">&#x27;Bob&#x27;</span>);         <span class="comment">// Update 3 → 挂到又一个 Hook 的 queue</span></span><br><span class="line">  <span class="comment">// 以上三个调用只会触发一次重新渲染</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 React 18 之前，只有 React 事件处理函数内的 setState 才会被批量处理。React 18 的 <code>createRoot</code> 让所有场景（包括 <code>setTimeout</code>、<code>Promise</code>、原生事件回调）都默认启用批量更新。</p><hr><h2 id="6-useEffect-的挂载逻辑"><a href="#6-useEffect-的挂载逻辑" class="headerlink" title="6. useEffect 的挂载逻辑"></a>6. useEffect 的挂载逻辑</h2><p><code>useEffect</code> 的 Hook 对象和 <code>useState</code> 共享同一条链表，但 <code>memoizedState</code> 里存的是一个 <strong>Effect 对象</strong>：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">mountEffect</span>(<span class="params">create, deps</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">mountWorkInProgressHook</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> effect = &#123;</span><br><span class="line">    <span class="attr">tag</span>: <span class="title class_">HookPassive</span>,          <span class="comment">// 标记为 passive effect（异步执行）</span></span><br><span class="line">    create,                     <span class="comment">// effect 回调函数</span></span><br><span class="line">    <span class="attr">destroy</span>: <span class="literal">undefined</span>,         <span class="comment">// cleanup 函数（首次为空）</span></span><br><span class="line">    deps,                       <span class="comment">// 依赖数组</span></span><br><span class="line">    <span class="attr">next</span>: <span class="literal">null</span>,                 <span class="comment">// effect 链表的 next</span></span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  hook.<span class="property">memoizedState</span> = effect;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 把 effect 也挂到 Fiber 的 updateQueue 上</span></span><br><span class="line">  <span class="comment">// Commit 阶段会遍历这个队列来执行 effect</span></span><br><span class="line">  <span class="title function_">pushEffect</span>(effect);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateEffect</span>(<span class="params">create, deps</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> hook = <span class="title function_">updateWorkInProgressHook</span>();</span><br><span class="line">  <span class="keyword">const</span> prevEffect = hook.<span class="property">memoizedState</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (deps !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> prevDeps = prevEffect.<span class="property">deps</span>;</span><br><span class="line">    <span class="comment">// 浅比较依赖数组</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">areHookInputsEqual</span>(deps, prevDeps)) &#123;</span><br><span class="line">      <span class="comment">// 依赖没变 → 跳过，不执行 effect</span></span><br><span class="line">      hook.<span class="property">memoizedState</span> = <span class="title function_">pushEffect</span>(<span class="title class_">HookPassive</span>, create, prevEffect.<span class="property">destroy</span>, deps);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#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">  currentlyRenderingFiber.<span class="property">flags</span> |= <span class="title class_">PassiveEffect</span>;</span><br><span class="line">  hook.<span class="property">memoizedState</span> = <span class="title function_">pushEffect</span>(</span><br><span class="line">    <span class="title class_">HookPassive</span> | <span class="title class_">HookHasEffect</span>,</span><br><span class="line">    create,</span><br><span class="line">    prevEffect.<span class="property">destroy</span>,</span><br><span class="line">    deps</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关于依赖数组的比较逻辑 <code>areHookInputsEqual</code>：</p><figure class="highlight js"><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="keyword">function</span> <span class="title function_">areHookInputsEqual</span>(<span class="params">nextDeps, prevDeps</span>) &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; prevDeps.<span class="property">length</span> &amp;&amp; i &lt; nextDeps.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="title class_">Object</span>.<span class="title function_">is</span>(nextDeps[i], prevDeps[i])) &#123;</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是为什么依赖数组里放对象时，即使内容没变，如果引用变了（每次渲染创建了新对象），effect 还是会重新执行。</p><hr><h2 id="7-阶段总结"><a href="#7-阶段总结" class="headerlink" title="7. 阶段总结"></a>7. 阶段总结</h2><p>Hooks 并不神奇。去掉语法糖后，它只是利用了两个基础的编程概念：</p><table><thead><tr><th>机制</th><th>解决的问题</th><th>实现方式</th></tr></thead><tbody><tr><td><strong>闭包快照</strong></td><td>每次渲染看到独立的 state</td><td>JS 闭包捕获当次渲染的值</td></tr><tr><td><strong>链表存储</strong></td><td>函数执行完后状态不丢失</td><td>数据挂在 Fiber.memoizedState 上</td></tr><tr><td><strong>环形更新队列</strong></td><td>批量处理多个 setState</td><td>Update 对象形成环形链表</td></tr><tr><td><strong>两套 Dispatcher</strong></td><td>区分首次渲染和更新渲染</td><td>mount 创建链表，update 按序取值</td></tr></tbody></table><p>正是因为底层”按顺序存储”的链表实现，才有了”不能在条件语句里写 Hook”这条铁律。这不是 React 团队的洁癖，而是数据结构决定的。</p><p>下一篇是这个系列的最后一块拼图——<strong>并发模式</strong>。我们要搞清楚 React 18 的 <code>useTransition</code> 和优先级调度是如何在底层实现”高优先级打断低优先级”的。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2025/react-source-3/</id>
    <link href="https://linkdiary.com/2025/react-source-3/"/>
    <published>2025-06-24T14:03:09.000Z</published>
    <summary>深入 Hooks 的底层实现，理解闭包快照、链表存储机制，以及 Hook 规则背后的技术原因。</summary>
    <title>React 源码深潜（三）：Hooks 的幻术——闭包快照与链表存储</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="React" scheme="https://linkdiary.com/tags/React/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-layer-group"></i><p>在第一篇中，我们理解了 Fiber 是为了解决”卡顿”而生的。那么，React 拿到 Fiber 链表后，到底是如何一步步把它变成网页上的真实像素的呢？</p><p>本篇我们进入 React 更新流程的核心地带：<strong>Render 阶段</strong>（打草稿）和 <strong>Commit 阶段</strong>（去发布），以及连接它们的<strong>双缓存机制</strong>。</p></div><hr><h2 id="1-核心模型：Render-与-Commit-的职责划分"><a href="#1-核心模型：Render-与-Commit-的职责划分" class="headerlink" title="1. 核心模型：Render 与 Commit 的职责划分"></a>1. 核心模型：Render 与 Commit 的职责划分</h2><p>React 将每一次 UI 更新严格分成了两个阶段，这不是设计上的”洁癖”，而是为了实现<strong>可中断更新</strong>的前提条件。</p><table><thead><tr><th>特性</th><th>Render 阶段（协调）</th><th>Commit 阶段（提交）</th></tr></thead><tbody><tr><td><strong>工作内容</strong></td><td>在内存中计算 Fiber 树的差异，标记增删改</td><td>将计算结果一次性应用到真实 DOM</td></tr><tr><td><strong>是否可中断</strong></td><td><strong>是</strong>（配合 Scheduler 时间切片）</td><td><strong>否</strong>（必须同步执行完）</td></tr><tr><td><strong>对用户可见</strong></td><td><strong>不可见</strong>（全在内存中进行）</td><td><strong>可见</strong>（用户看到界面变化）</td></tr><tr><td><strong>内部核心函数</strong></td><td><code>beginWork</code> &#x2F; <code>completeWork</code></td><td><code>commitMutationEffects</code> &#x2F; <code>commitLayoutEffects</code></td></tr><tr><td><strong>有无副作用</strong></td><td>无（纯计算）</td><td>有（DOM 操作、ref 更新、effect 触发）</td></tr></tbody></table><p>这种分离的直接好处是：</p><blockquote><p>Render 阶段可以随时中断、丢弃、重来，因为它不会产生任何用户可见的变化。只有当整棵”草稿树”计算完毕，React 才会进入 Commit 阶段一次性提交。</p></blockquote><hr><h2 id="2-双缓存技术：为什么需要两棵树"><a href="#2-双缓存技术：为什么需要两棵树" class="headerlink" title="2. 双缓存技术：为什么需要两棵树"></a>2. 双缓存技术：为什么需要两棵树</h2><p>在理解 Render 阶段之前，需要先搞清一个基础设施：<strong>React 在内存中同时维护着两棵 Fiber 树</strong>。</p><h3 id="两棵树的角色"><a href="#两棵树的角色" class="headerlink" title="两棵树的角色"></a>两棵树的角色</h3><ul><li><strong>Current Tree</strong>：当前屏幕上正在显示的那棵树，每个 Fiber 节点对应着真实 DOM</li><li><strong>WorkInProgress Tree</strong>：正在内存中构建的”草稿树”，是下一次 UI 的预演</li></ul><p>两棵树上的对应节点通过 <code>alternate</code> 属性互相引用：</p><figure class="highlight js"><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="comment">// current 树上的节点</span></span><br><span class="line">currentFiber.<span class="property">alternate</span> === workInProgressFiber; <span class="comment">// true</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// workInProgress 树上的节点</span></span><br><span class="line">workInProgressFiber.<span class="property">alternate</span> === currentFiber; <span class="comment">// true</span></span><br></pre></td></tr></table></figure><h3 id="为什么不在原树上直接改"><a href="#为什么不在原树上直接改" class="headerlink" title="为什么不在原树上直接改"></a>为什么不在原树上直接改</h3><p>如果 React 直接修改 Current Tree，会遇到两个严重问题：</p><ol><li><strong>UI 撕裂</strong>：由于 Render 阶段是可中断的，如果直接改 Current Tree，用户可能看到”改了一半”的界面——比如列表前 5 项是新样式，后 5 项还是旧样式</li><li><strong>无法回滚</strong>：如果一个高优先级更新打断了当前的低优先级渲染，React 需要丢弃未完成的工作。如果改的是原树，就无法恢复</li></ol><p>双缓存解决了这两个问题：</p><ul><li>所有修改都发生在 WorkInProgress Tree 上，Current Tree 保持不变</li><li>如果需要丢弃，直接扔掉 WorkInProgress Tree 即可</li><li>只有在所有计算都完成后，才通过切换指针让 WorkInProgress Tree “上位”成为新的 Current Tree</li></ul><h3 id="双缓存的生命周期"><a href="#双缓存的生命周期" class="headerlink" title="双缓存的生命周期"></a>双缓存的生命周期</h3><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    subgraph mount[&quot;首次渲染 Mount&quot;]        M1[&quot;rootFiber.current → 空的 Current Tree&quot;] --&gt;|创建 workInProgress| M2[&quot;WorkInProgress Tree&lt;br&#x2F;&gt;从头构建每个节点&quot;]        M2 --&gt;|&quot;Commit：切换指针&quot;| M3[&quot;rootFiber.current → 新的 Current Tree&lt;br&#x2F;&gt;就是刚才的 WIP&quot;]    end    subgraph update[&quot;后续更新 Update&quot;]        U1[&quot;rootFiber.current → Current Tree&quot;] --&gt;|&quot;基于 Current 创建 WIP（复用节点）&quot;| U2[&quot;WorkInProgress Tree&lt;br&#x2F;&gt;只处理有变化的节点&quot;]        U2 --&gt;|&quot;Commit：切换指针&quot;| U3[&quot;rootFiber.current → 新的 Current Tree&quot;]        U2 -.-&gt;|闲置| U4[&quot;旧的 Current → 等待下次复用为 WIP&quot;]    end  </pre></div><p>注意最后一步：旧的 Current Tree 不会被销毁，它会在下次更新时被复用为新的 WorkInProgress Tree。这就是为什么叫”双缓存”——两棵树交替充当”正式版”和”草稿版”。</p><hr><h2 id="3-Render-阶段：精细的”打草稿”过程"><a href="#3-Render-阶段：精细的”打草稿”过程" class="headerlink" title="3. Render 阶段：精细的”打草稿”过程"></a>3. Render 阶段：精细的”打草稿”过程</h2><p>Render 阶段的目标是：<strong>在 WorkInProgress Tree 上标记出所有需要变更的节点</strong>。</p><p>React 通过上一篇讲过的深度优先遍历（workLoop），对每个 Fiber 节点执行两个核心函数：</p><h3 id="3-1-beginWork：自顶向下的”递”"><a href="#3-1-beginWork：自顶向下的”递”" class="headerlink" title="3.1 beginWork：自顶向下的”递”"></a>3.1 beginWork：自顶向下的”递”</h3><p><code>beginWork</code> 负责处理当前节点，并生成它的子 Fiber。</p><figure class="highlight js"><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="keyword">function</span> <span class="title function_">beginWork</span>(<span class="params">current, workInProgress, renderLanes</span>) &#123;</span><br><span class="line">  <span class="comment">// current 是 Current Tree 上的对应节点</span></span><br><span class="line">  <span class="comment">// workInProgress 是正在构建的节点</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第一步：判断是否可以跳过</span></span><br><span class="line">  <span class="keyword">if</span> (current !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> oldProps = current.<span class="property">memoizedProps</span>;</span><br><span class="line">    <span class="keyword">const</span> newProps = workInProgress.<span class="property">pendingProps</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (oldProps === newProps &amp;&amp; !<span class="title function_">hasContextChanged</span>()) &#123;</span><br><span class="line">      <span class="comment">// props 没变、context 没变 → 直接复用，跳过这棵子树</span></span><br><span class="line">      <span class="keyword">return</span> <span class="title function_">bailoutOnAlreadyFinishedWork</span>(current, workInProgress);</span><br><span class="line">    &#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">switch</span> (workInProgress.<span class="property">tag</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="title class_">FunctionComponent</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="title function_">updateFunctionComponent</span>(current, workInProgress, renderLanes);</span><br><span class="line">    <span class="keyword">case</span> <span class="title class_">ClassComponent</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="title function_">updateClassComponent</span>(current, workInProgress, renderLanes);</span><br><span class="line">    <span class="keyword">case</span> <span class="title class_">HostComponent</span>: <span class="comment">// 原生 DOM 元素，如 &lt;div&gt;</span></span><br><span class="line">      <span class="keyword">return</span> <span class="title function_">updateHostComponent</span>(current, workInProgress);</span><br><span class="line">    <span class="comment">// ... 还有十几种类型</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>拿函数组件举例，<code>updateFunctionComponent</code> 会做这些事：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">updateFunctionComponent</span>(<span class="params">current, workInProgress, renderLanes</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title class_">Component</span> = workInProgress.<span class="property">type</span>; <span class="comment">// 组件函数，比如 App</span></span><br><span class="line">  <span class="keyword">const</span> newProps = workInProgress.<span class="property">pendingProps</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 执行组件函数，拿到 JSX（也就是 React Element）</span></span><br><span class="line">  <span class="comment">// 在这里，Hooks 会被依次调用</span></span><br><span class="line">  <span class="keyword">const</span> nextChildren = <span class="title function_">renderWithHooks</span>(</span><br><span class="line">    current,</span><br><span class="line">    workInProgress,</span><br><span class="line">    <span class="title class_">Component</span>,</span><br><span class="line">    newProps,</span><br><span class="line">    renderLanes</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">if</span> (current !== <span class="literal">null</span> &amp;&amp; !didReceiveUpdate) &#123;</span><br><span class="line">    <span class="title function_">bailoutHooks</span>(current, workInProgress, renderLanes);</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">bailoutOnAlreadyFinishedWork</span>(current, workInProgress);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Diff：对比新旧 children，创建/复用/标记子 Fiber</span></span><br><span class="line">  <span class="title function_">reconcileChildren</span>(current, workInProgress, nextChildren, renderLanes);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> workInProgress.<span class="property">child</span>; <span class="comment">// 返回第一个子 Fiber，继续向下</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Diff-算法的核心策略"><a href="#Diff-算法的核心策略" class="headerlink" title="Diff 算法的核心策略"></a>Diff 算法的核心策略</h4><p><code>reconcileChildren</code> 是 Diff 算法的入口，React 对它做了三个重要的<strong>降级假设</strong>来把 O(n³) 的通用树 Diff 降低到 O(n)：</p><ol><li><strong>跨层级的节点移动极少发生</strong>：只比较同一层级的节点，不做跨层级复用</li><li><strong>不同类型的组件产生不同的树</strong>：如果一个 <code>&lt;div&gt;</code> 变成了 <code>&lt;span&gt;</code>，直接销毁整棵子树重建</li><li><strong>通过 <code>key</code> 标识同一元素</strong>：列表中同 key 的元素才认为是”同一个”</li></ol><p>对于列表的 Diff（多节点），React 分两轮遍历：</p><figure class="highlight js"><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="comment">// 简化版列表 Diff 思路</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">reconcileChildrenArray</span>(<span class="params">returnFiber, currentFirstChild, newChildren</span>) &#123;</span><br><span class="line">  <span class="comment">// === 第一轮：从头开始，逐个比对 ===</span></span><br><span class="line">  <span class="comment">// 如果 key 和 type 都匹配 → 复用旧 Fiber，标记 Update</span></span><br><span class="line">  <span class="comment">// 如果 key 不匹配 → 跳出第一轮</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// === 第二轮：处理剩余节点 ===</span></span><br><span class="line">  <span class="comment">// 把旧的剩余节点放进一个 Map&lt;key, Fiber&gt;</span></span><br><span class="line">  <span class="comment">// 遍历新的剩余节点，从 Map 里找可复用的</span></span><br><span class="line">  <span class="comment">// 找到了 → 复用，标记 Placement（移动）</span></span><br><span class="line">  <span class="comment">// 没找到 → 创建新 Fiber，标记 Placement（新增）</span></span><br><span class="line">  <span class="comment">// Map 里剩下的 → 标记 Deletion（删除）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每个需要变更的 Fiber 会被打上 <strong>Flags</strong>（旧版叫 effectTag）：</p><figure class="highlight js"><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="comment">// 常见的 Flags</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Placement</span>   = <span class="number">0b0000000000010</span>;  <span class="comment">// 新增或移动</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Update</span>      = <span class="number">0b0000000000100</span>;  <span class="comment">// 属性更新</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Deletion</span>    = <span class="number">0b0000000001000</span>;  <span class="comment">// 删除</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ChildDeletion</span> = <span class="number">0b0000000010000</span>; <span class="comment">// 子节点删除</span></span><br></pre></td></tr></table></figure><h3 id="3-2-completeWork：自底向上的”归”"><a href="#3-2-completeWork：自底向上的”归”" class="headerlink" title="3.2 completeWork：自底向上的”归”"></a>3.2 completeWork：自底向上的”归”</h3><p>当一个节点的所有子节点都处理完后（没有 child，或 child 的子树已经完成），React 会调用 <code>completeWork</code> 进行”收尾”。</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">completeWork</span>(<span class="params">current, workInProgress</span>) &#123;</span><br><span class="line">  <span class="keyword">switch</span> (workInProgress.<span class="property">tag</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="title class_">HostComponent</span>: &#123;</span><br><span class="line">      <span class="comment">// 原生 DOM 元素的处理</span></span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (current !== <span class="literal">null</span> &amp;&amp; workInProgress.<span class="property">stateNode</span> !== <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// === 更新阶段 ===</span></span><br><span class="line">        <span class="comment">// DOM 实例已存在，比较新旧 props</span></span><br><span class="line">        <span class="comment">// 找出变化的属性（className、style、事件等）</span></span><br><span class="line">        <span class="comment">// 生成 updateQueue：[propKey1, propValue1, propKey2, propValue2, ...]</span></span><br><span class="line">        <span class="title function_">updateHostComponent</span>(current, workInProgress);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// === 首次渲染 ===</span></span><br><span class="line">        <span class="comment">// 创建真实 DOM 实例</span></span><br><span class="line">        <span class="keyword">const</span> instance = <span class="title function_">createInstance</span>(workInProgress.<span class="property">type</span>, newProps);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 把已经创建好的子 DOM 挂到自己身上</span></span><br><span class="line">        <span class="comment">// 注意：此时还在内存里，不在页面上</span></span><br><span class="line">        <span class="title function_">appendAllChildren</span>(instance, workInProgress);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置 DOM 属性</span></span><br><span class="line">        <span class="title function_">finalizeInitialChildren</span>(instance, workInProgress.<span class="property">type</span>, newProps);</span><br><span class="line"></span><br><span class="line">        workInProgress.<span class="property">stateNode</span> = instance;</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="comment">// 把子树中的 Flags 汇总到自己的 subtreeFlags</span></span><br><span class="line">      <span class="title function_">bubbleProperties</span>(workInProgress);</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ... 其他类型</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="副作用冒泡（bubbleProperties）"><a href="#副作用冒泡（bubbleProperties）" class="headerlink" title="副作用冒泡（bubbleProperties）"></a>副作用冒泡（bubbleProperties）</h4><p>这是 React 18 引入的一个性能优化。每个节点在 <code>completeWork</code> 时，会把子树所有的 Flags 合并到自己的 <code>subtreeFlags</code> 上：</p><figure class="highlight js"><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">function</span> <span class="title function_">bubbleProperties</span>(<span class="params">completedWork</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> subtreeFlags = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">let</span> child = completedWork.<span class="property">child</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span> (child !== <span class="literal">null</span>) &#123;</span><br><span class="line">    subtreeFlags |= child.<span class="property">subtreeFlags</span>;</span><br><span class="line">    subtreeFlags |= child.<span class="property">flags</span>;</span><br><span class="line">    child = child.<span class="property">sibling</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  completedWork.<span class="property">subtreeFlags</span> = subtreeFlags;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样在 Commit 阶段，React 只需要检查 <code>subtreeFlags</code> 就知道某棵子树下面有没有需要处理的副作用——如果 <code>subtreeFlags === 0</code>，整棵子树都可以跳过，省去了大量无用遍历。</p><hr><h2 id="4-Commit-阶段：最终的”发布时刻”"><a href="#4-Commit-阶段：最终的”发布时刻”" class="headerlink" title="4. Commit 阶段：最终的”发布时刻”"></a>4. Commit 阶段：最终的”发布时刻”</h2><p>一旦 Render 阶段走完，整棵 WorkInProgress Tree 上已经标记好了所有变更。React 进入 Commit 阶段，<strong>这个阶段是同步、不可中断的</strong>——因为它涉及到真实 DOM 操作，半途而废会让用户看到不一致的界面。</p><p>Commit 阶段在 React 内部被细分为三个子阶段：</p><h3 id="4-1-Before-Mutation（DOM-变更前）"><a href="#4-1-Before-Mutation（DOM-变更前）" class="headerlink" title="4.1 Before Mutation（DOM 变更前）"></a>4.1 Before Mutation（DOM 变更前）</h3><p>这个阶段读取 DOM 的”旧状态”，为后续变更做准备：</p><ul><li>调用 Class 组件的 <code>getSnapshotBeforeUpdate</code> 生命周期</li><li>此时 DOM 还是旧的，可以安全地读取当前的 scrollTop、尺寸等信息</li></ul><figure class="highlight js"><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="keyword">function</span> <span class="title function_">commitBeforeMutationEffects</span>(<span class="params">firstChild</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> fiber = firstChild;</span><br><span class="line">  <span class="keyword">while</span> (fiber !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (fiber.<span class="property">flags</span> &amp; <span class="title class_">Snapshot</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> current = fiber.<span class="property">alternate</span>;</span><br><span class="line">      <span class="comment">// 调用 getSnapshotBeforeUpdate</span></span><br><span class="line">      <span class="title function_">commitBeforeMutationLifeCycles</span>(current, fiber);</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">if</span> (fiber.<span class="property">subtreeFlags</span> &amp; <span class="title class_">BeforeMutationMask</span>) &#123;</span><br><span class="line">      <span class="title function_">commitBeforeMutationEffects</span>(fiber.<span class="property">child</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fiber = fiber.<span class="property">sibling</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-2-Mutation（DOM-变更）"><a href="#4-2-Mutation（DOM-变更）" class="headerlink" title="4.2 Mutation（DOM 变更）"></a>4.2 Mutation（DOM 变更）</h3><p>这是真正修改 DOM 的阶段：</p><figure class="highlight js"><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="keyword">function</span> <span class="title function_">commitMutationEffects</span>(<span class="params">firstChild</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> fiber = firstChild;</span><br><span class="line">  <span class="keyword">while</span> (fiber !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> flags = fiber.<span class="property">flags</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 处理子节点的删除</span></span><br><span class="line">    <span class="keyword">if</span> (flags &amp; <span class="title class_">ChildDeletion</span>) &#123;</span><br><span class="line">      <span class="title function_">commitDeletions</span>(fiber.<span class="property">deletions</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="keyword">if</span> (flags &amp; <span class="title class_">Placement</span>) &#123;</span><br><span class="line">      <span class="title function_">commitPlacement</span>(fiber);       <span class="comment">// 插入 / 移动 DOM</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (flags &amp; <span class="title class_">Update</span>) &#123;</span><br><span class="line">      <span class="title function_">commitWork</span>(fiber);            <span class="comment">// 更新 DOM 属性</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="keyword">if</span> (fiber.<span class="property">subtreeFlags</span> &amp; <span class="title class_">MutationMask</span>) &#123;</span><br><span class="line">      <span class="title function_">commitMutationEffects</span>(fiber.<span class="property">child</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fiber = fiber.<span class="property">sibling</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>完成 Mutation 后，用户的屏幕发生了变化。</strong> 紧接着，React 执行关键的一步——指针切换：</p><figure class="highlight js"><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="comment">// 一行代码完成&quot;双缓存切换&quot;</span></span><br><span class="line">root.<span class="property">current</span> = finishedWork;</span><br></pre></td></tr></table></figure><p>此时，原本的 WorkInProgress Tree 变成了新的 Current Tree，旧的 Current Tree 退居二线，等待下次更新复用。</p><h3 id="4-3-Layout（DOM-变更后）"><a href="#4-3-Layout（DOM-变更后）" class="headerlink" title="4.3 Layout（DOM 变更后）"></a>4.3 Layout（DOM 变更后）</h3><p>这个阶段 DOM 已经更新完毕，可以安全地读取新的布局信息：</p><ul><li>调用 Class 组件的 <code>componentDidMount</code> &#x2F; <code>componentDidUpdate</code></li><li>调用 <code>useLayoutEffect</code> 的回调（同步执行，在浏览器绘制之前）</li><li>更新 ref</li></ul><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">commitLayoutEffects</span>(<span class="params">firstChild</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> fiber = firstChild;</span><br><span class="line">  <span class="keyword">while</span> (fiber !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (fiber.<span class="property">flags</span> &amp; <span class="title class_">LayoutMask</span>) &#123;</span><br><span class="line">      <span class="comment">// 函数组件：执行 useLayoutEffect 的回调</span></span><br><span class="line">      <span class="comment">// Class 组件：执行 componentDidMount / componentDidUpdate</span></span><br><span class="line">      <span class="title function_">commitLayoutEffectOnFiber</span>(fiber);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 更新 ref</span></span><br><span class="line">    <span class="keyword">if</span> (fiber.<span class="property">flags</span> &amp; <span class="title class_">Ref</span>) &#123;</span><br><span class="line">      <span class="title function_">commitAttachRef</span>(fiber);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (fiber.<span class="property">subtreeFlags</span> &amp; <span class="title class_">LayoutMask</span>) &#123;</span><br><span class="line">      <span class="title function_">commitLayoutEffects</span>(fiber.<span class="property">child</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fiber = fiber.<span class="property">sibling</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要注意 <code>useLayoutEffect</code> 和 <code>useEffect</code> 的区别：</p><ul><li><strong><code>useLayoutEffect</code></strong>：在 Layout 阶段<strong>同步</strong>执行，此时 DOM 已更新但浏览器还没绘制</li><li><strong><code>useEffect</code></strong>：在 Commit 阶段结束后<strong>异步</strong>调度执行，不阻塞浏览器绘制</li></ul><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    A[&quot;Before Mutation&lt;br&#x2F;&gt;getSnapshot...&quot;] --&gt; B[&quot;Mutation&lt;br&#x2F;&gt;DOM 增删改&lt;br&#x2F;&gt;切换 current 指针&quot;]    B --&gt; C[&quot;Layout&lt;br&#x2F;&gt;useLayoutEffect&lt;br&#x2F;&gt;componentDidMount&lt;br&#x2F;&gt;更新 ref&quot;]    C --&gt; D[&quot;浏览器绘制&quot;]    D -.-&gt;|异步| E[&quot;useEffect&quot;]  </pre></div><hr><h2 id="5-完整更新流程串联"><a href="#5-完整更新流程串联" class="headerlink" title="5. 完整更新流程串联"></a>5. 完整更新流程串联</h2><p>把前两篇的内容综合起来，一次由 <code>setState</code> 触发的更新从头到尾的完整流程是这样的：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    A[&quot;setState 调用&quot;] --&gt; B[&quot;创建 Update 对象&lt;br&#x2F;&gt;挂到 Fiber 的 updateQueue&quot;]    B --&gt; C[&quot;scheduleUpdateOnFiber&lt;br&#x2F;&gt;向上标记优先级&quot;]    C --&gt; D[&quot;Scheduler 根据优先级安排任务&quot;]    D --&gt; R    subgraph R[&quot;Render 阶段（可中断）&quot;]        R1[&quot;workLoop&quot;] --&gt; R2[&quot;beginWork&lt;br&#x2F;&gt;· 执行组件函数&lt;br&#x2F;&gt;· Diff 子节点，标记 Flags&lt;br&#x2F;&gt;· 返回 child&quot;]        R2 --&gt; R3[&quot;completeWork&lt;br&#x2F;&gt;· 创建&#x2F;更新 DOM 实例&lt;br&#x2F;&gt;· 副作用冒泡&lt;br&#x2F;&gt;· 返回 sibling 或 return&quot;]    end    R --&gt; CM    subgraph CM[&quot;Commit 阶段（同步不可中断）&quot;]        C1[&quot;1. Before Mutation&lt;br&#x2F;&gt;· getSnapshotBeforeUpdate&quot;] --&gt; C2[&quot;2. Mutation&lt;br&#x2F;&gt;· 真实 DOM 增删改&lt;br&#x2F;&gt;· root.current &#x3D; finishedWork&quot;]        C2 --&gt; C3[&quot;3. Layout&lt;br&#x2F;&gt;· useLayoutEffect&lt;br&#x2F;&gt;· componentDidMount&#x2F;Update&lt;br&#x2F;&gt;· 更新 ref&quot;]    end    CM --&gt; P[&quot;浏览器绘制 Paint&quot;]    P --&gt; UE[&quot;异步调度 useEffect&quot;]  </pre></div><hr><h2 id="6-本章总结"><a href="#6-本章总结" class="headerlink" title="6. 本章总结"></a>6. 本章总结</h2><p>通过将”计算”与”变更”分离，React 实现了<strong>渲染的原子性</strong>：</p><ul><li>Render 阶段纯计算、可中断、可丢弃——保证了并发渲染的可能性</li><li>Commit 阶段同步执行、不可中断——保证了 UI 的一致性</li><li>双缓存机制让两个阶段之间有了安全的”缓冲区”</li></ul><blockquote><p>React 的更新策略可以用一句话概括：<strong>要么不更新，要更新就一次性把完美的成品呈现给用户。</strong></p></blockquote><p>下一篇，我们要探索一个更有趣的问题：既然函数组件每次渲染都会重新执行，那组件里的状态（State）是如何逃过”被重置”的命运，稳稳地留在内存里的呢？</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2025/react-source-2/</id>
    <link href="https://linkdiary.com/2025/react-source-2/"/>
    <published>2025-06-17T01:53:12.000Z</published>
    <summary>拆解 React 更新的两大阶段——Render 和 Commit，理解 beginWork/completeWork 的工作细节以及双缓存机制如何保证 UI 的一致性。</summary>
    <title>React 源码深潜（二）：双缓存与渲染流程的&quot;两步走&quot;策略</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="React" scheme="https://linkdiary.com/tags/React/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-brain"></i><p>理解 React 的宏观架构，就像是在观察一个高效运行的<strong>微型操作系统</strong>。它不仅仅是把 UI 渲染出来，更是在毫秒级的时间里管理着资源的分配和任务的优先级。</p><p>本文是 React 源码系列的第一篇，我们从浏览器的帧预算讲起，搞清楚 React 为什么要做 Fiber 这件事，以及 Fiber 到底是什么。</p></div><hr><h2 id="1-核心挑战：浏览器的”16-6ms”军令状"><a href="#1-核心挑战：浏览器的”16-6ms”军令状" class="headerlink" title="1. 核心挑战：浏览器的”16.6ms”军令状"></a>1. 核心挑战：浏览器的”16.6ms”军令状</h2><p>显示器的刷新率通常是 <strong>60Hz</strong>，这意味着浏览器每秒需要刷新 60 次画面。留给每一帧的时间只有：</p><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">1000ms / 60 ≈ 16.6ms</span><br></pre></td></tr></table></figure><p>在这一帧内，浏览器需要完成以下工作：</p><ol><li><strong>处理用户输入</strong>（点击、滚动、按键）</li><li><strong>执行 JavaScript</strong></li><li><strong>计算样式和布局</strong>（Style &amp; Layout）</li><li><strong>绘制画面</strong>（Paint &amp; Composite）</li></ol><p>这里的要害在于：<strong>JavaScript 执行和 UI 渲染共享同一个主线程</strong>。如果 React 的 Diff 算法跑了 100ms，浏览器在这期间无法响应用户输入，也无法绘制下一帧——用户看到的就是画面”卡死”了。</p><p>为了直观理解，可以想象一条流水线：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    subgraph ideal[&quot;一帧 16.6ms 的预算分配（理想情况）&quot;]        A[&quot;Input 处理 ~1ms&quot;] --&gt; B[&quot;JS 执行 ~10ms&quot;] --&gt; C[&quot;Layout 计算 ~3ms&quot;] --&gt; D[&quot;Paint 绘制 ~2ms&quot;]    end    subgraph blocked[&quot;如果 JS 占了 100ms&quot;]        E[&quot;Input ~1ms&quot;] --&gt; F[&quot;JS 执行 100ms&lt;br&#x2F;&gt;后面全部被阻塞&lt;br&#x2F;&gt;用户看到 6 帧空白&quot;]    end  </pre></div><hr><h2 id="2-React-15-的困境：一干到底的递归"><a href="#2-React-15-的困境：一干到底的递归" class="headerlink" title="2. React 15 的困境：一干到底的递归"></a>2. React 15 的困境：一干到底的递归</h2><p>在 React 16 之前，使用的是 <strong>Stack Reconciler</strong>（栈协调器）。</p><h3 id="工作模式"><a href="#工作模式" class="headerlink" title="工作模式"></a>工作模式</h3><p>Stack Reconciler 利用 JavaScript 原生的函数调用栈进行递归。一旦开始更新，React 会从根节点一直比对到叶子节点，整个过程<strong>同步且不可中断</strong>。</p><p>打个比方：Stack Reconciler 就像一个”一根筋的跳水员”——跳入泳池后必须摸到池底才能浮上来，在水下的时候外界发生什么他都听不见、管不着。</p><h3 id="简化的伪代码"><a href="#简化的伪代码" class="headerlink" title="简化的伪代码"></a>简化的伪代码</h3><p>Stack Reconciler 的递归大概长这样：</p><figure class="highlight js"><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="comment">// React 15 的递归协调（简化）</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">reconcileChildren</span>(<span class="params">parentFiber, newChildren</span>) &#123;</span><br><span class="line">  <span class="comment">// 遍历新的子元素</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; newChildren.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">const</span> child = newChildren[i];</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 递归比较——一旦进入无法中断</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">hasChanged</span>(child)) &#123;</span><br><span class="line">      <span class="title function_">updateComponent</span>(child);            <span class="comment">// 更新该节点</span></span><br><span class="line">      <span class="title function_">reconcileChildren</span>(child, child.<span class="property">children</span>); <span class="comment">// 继续向下递归</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><p>当组件树很小、Diff 只需要几毫秒时，同步递归完全没问题。但组件一旦上千，递归的调用栈会变得很深，主线程被长期霸占，用户点击按钮没反应，输入框打字出不来字。</p><h3 id="问题的本质"><a href="#问题的本质" class="headerlink" title="问题的本质"></a>问题的本质</h3><p>问题不在于”算得慢”，而在于”不能停”：</p><ul><li>JavaScript 原生调用栈是<strong>一次性的</strong>，你无法在递归中途暂停、保存现场、下次恢复</li><li>浏览器没有提供”暂停 JS 执行，先刷新一帧”的能力</li><li>只要递归没结束，主线程就被独占</li></ul><hr><h2 id="3-Fiber-的革新：可中断的”纤程”"><a href="#3-Fiber-的革新：可中断的”纤程”" class="headerlink" title="3. Fiber 的革新：可中断的”纤程”"></a>3. Fiber 的革新：可中断的”纤程”</h2><p>React 团队为了打破”一干到底”的僵局，在 React 16 中引入了 <strong>Fiber</strong>（纤维 &#x2F; 纤程）。它将”一长条”的递归任务拆成了无数个”微型工作单元”。</p><h3 id="核心思维转变"><a href="#核心思维转变" class="headerlink" title="核心思维转变"></a>核心思维转变</h3><p>从<strong>递归（Stack）</strong> 转换到了<strong>循环（Loop）</strong>。</p><p>这个转变看起来简单，背后的意义却很大：</p><ul><li>递归依赖原生调用栈，无法暂停</li><li>循环可以在任意迭代后 <code>break</code>，下次从断点继续</li></ul><figure class="highlight js"><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="comment">// 递归：无法中断</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">walkTree</span>(<span class="params">node</span>) &#123;</span><br><span class="line">  <span class="title function_">process</span>(node);</span><br><span class="line">  node.<span class="property">children</span>.<span class="title function_">forEach</span>(walkTree); <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="keyword">let</span> current = rootNode;</span><br><span class="line"><span class="keyword">while</span> (current !== <span class="literal">null</span>) &#123;</span><br><span class="line">  current = <span class="title function_">processAndReturnNext</span>(current); <span class="comment">// 处理一个，返回下一个</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">shouldYield</span>()) <span class="keyword">break</span>; <span class="comment">// 时间到了就停</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="协作式调度（Cooperative-Scheduling）"><a href="#协作式调度（Cooperative-Scheduling）" class="headerlink" title="协作式调度（Cooperative Scheduling）"></a>协作式调度（Cooperative Scheduling）</h3><p>React 不再霸道地独占主线程，它学会了”看脸色”——每处理完一个微任务，就停下来问浏览器：”现在有更紧急的事吗？没有我再接着干。”</p><p>这种模式在操作系统领域有一个专门的术语叫<strong>协作式调度</strong>，和抢占式调度不同的是，任务必须主动”让出”CPU。React 的 Fiber 就是在 JavaScript 层面实现了这套协作机制。</p><hr><h2 id="4-Fiber-节点的数据结构"><a href="#4-Fiber-节点的数据结构" class="headerlink" title="4. Fiber 节点的数据结构"></a>4. Fiber 节点的数据结构</h2><p>为了让任务能随时停下来，React 必须弃用原生的函数调用栈，转而自己实现一套数据结构来记录”走到哪了”。这就是 Fiber 节点。</p><p><strong>每个 Fiber 节点本质上就是组件在内存中的”工作单元”</strong>，它既是虚拟 DOM 的升级版，也是调度系统的基本粒度。</p><h3 id="核心字段"><a href="#核心字段" class="headerlink" title="核心字段"></a>核心字段</h3><p>一个 Fiber 节点（简化后）的结构大致如下：</p><figure class="highlight ts"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">FiberNode</span> &#123;</span><br><span class="line">  <span class="comment">// === 节点身份 ===</span></span><br><span class="line">  <span class="attr">tag</span>: <span class="built_in">number</span>;              <span class="comment">// 节点类型：函数组件、类组件、原生 DOM...</span></span><br><span class="line">  <span class="attr">type</span>: <span class="built_in">any</span>;                <span class="comment">// 对应的组件函数或标签名（如 &#x27;div&#x27;）</span></span><br><span class="line">  <span class="attr">key</span>: <span class="built_in">string</span> | <span class="literal">null</span>;       <span class="comment">// 列表 Diff 用的唯一标识</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// === 树结构指针（链表化的关键）===</span></span><br><span class="line">  <span class="attr">child</span>: <span class="title class_">FiberNode</span> | <span class="literal">null</span>;    <span class="comment">// 第一个子节点</span></span><br><span class="line">  <span class="attr">sibling</span>: <span class="title class_">FiberNode</span> | <span class="literal">null</span>;  <span class="comment">// 下一个兄弟节点</span></span><br><span class="line">  <span class="attr">return</span>: <span class="title class_">FiberNode</span> | <span class="literal">null</span>;   <span class="comment">// 父节点（处理完子节点后返回的路径）</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// === 状态与副作用 ===</span></span><br><span class="line">  <span class="attr">memoizedState</span>: <span class="built_in">any</span>;       <span class="comment">// Hooks 链表 / Class 组件的 state</span></span><br><span class="line">  <span class="attr">memoizedProps</span>: <span class="built_in">any</span>;       <span class="comment">// 上一次渲染的 props</span></span><br><span class="line">  <span class="attr">pendingProps</span>: <span class="built_in">any</span>;        <span class="comment">// 本次待处理的 props</span></span><br><span class="line">  <span class="attr">flags</span>: <span class="built_in">number</span>;            <span class="comment">// 副作用标记（Placement / Update / Deletion）</span></span><br><span class="line">  <span class="attr">subtreeFlags</span>: <span class="built_in">number</span>;     <span class="comment">// 子树的副作用聚合</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// === 双缓存 ===</span></span><br><span class="line">  <span class="attr">alternate</span>: <span class="title class_">FiberNode</span> | <span class="literal">null</span>; <span class="comment">// 指向另一棵树上的对应节点</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// === 调度 ===</span></span><br><span class="line">  <span class="attr">lanes</span>: <span class="built_in">number</span>;            <span class="comment">// 优先级（Lane 模型）</span></span><br><span class="line">  <span class="attr">childLanes</span>: <span class="built_in">number</span>;       <span class="comment">// 子树中待处理的优先级</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="三个关键指针：child-sibling-return"><a href="#三个关键指针：child-sibling-return" class="headerlink" title="三个关键指针：child &#x2F; sibling &#x2F; return"></a>三个关键指针：child &#x2F; sibling &#x2F; return</h3><p>传统的树结构通常用 <code>children</code> 数组来存储子节点，但这样就回到了”递归遍历”的老路。Fiber 选择了<strong>链表化</strong>——每个节点只需要记住三个方向：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    App[&quot;App（root）&quot;] --&gt;|child| Header    Header --&gt;|sibling| Main    Main --&gt;|sibling| Footer    Header --&gt;|child| Nav    Main --&gt;|child| ArticleList    ArticleList --&gt;|child| Article  </pre></div><ol><li><strong><code>child</code></strong>：指向第一个子节点</li><li><strong><code>sibling</code></strong>：指向下一个兄弟节点</li><li><strong><code>return</code></strong>：指向父节点（处理完后沿着这条路返回）</li></ol><p>这种设计的精妙之处在于：React 只需要维护一个 <code>workInProgress</code> 指针，就能在任意时刻暂停遍历，下次恢复时从指针位置继续——因为所有”接下来该去哪”的信息都已经编码在节点的指针里了。</p><hr><h2 id="5-Fiber-的遍历算法：深度优先-链表回溯"><a href="#5-Fiber-的遍历算法：深度优先-链表回溯" class="headerlink" title="5. Fiber 的遍历算法：深度优先 + 链表回溯"></a>5. Fiber 的遍历算法：深度优先 + 链表回溯</h2><p>理解了数据结构，再来看 React 怎么遍历这棵 Fiber 树。核心逻辑可以用一段伪代码概括：</p><figure class="highlight js"><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="keyword">function</span> <span class="title function_">workLoop</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">while</span> (workInProgress !== <span class="literal">null</span> &amp;&amp; !<span class="title function_">shouldYield</span>()) &#123;</span><br><span class="line">    workInProgress = <span class="title function_">performUnitOfWork</span>(workInProgress);</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">function</span> <span class="title function_">performUnitOfWork</span>(<span class="params">fiber</span>) &#123;</span><br><span class="line">  <span class="comment">// 第一步：beginWork —— 处理当前节点，生成子 Fiber</span></span><br><span class="line">  <span class="keyword">const</span> next = <span class="title function_">beginWork</span>(fiber);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (next !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// 有子节点 → 继续向下</span></span><br><span class="line">    <span class="keyword">return</span> next;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 没有子节点了 → 开始&quot;回溯&quot;</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">completeUnitOfWork</span>(fiber);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">completeUnitOfWork</span>(<span class="params">fiber</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> node = fiber;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span> (node !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// 完成当前节点的收尾工作</span></span><br><span class="line">    <span class="title function_">completeWork</span>(node);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 有兄弟节点 → 转向兄弟</span></span><br><span class="line">    <span class="keyword">if</span> (node.<span class="property">sibling</span> !== <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> node.<span class="property">sibling</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">    node = node.<span class="property">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="literal">null</span>; <span class="comment">// 整棵树处理完毕</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用上面那棵示例树来走一遍：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph TD    S1[&quot;1. App beginWork&quot;] --&gt; S2[&quot;2. Header beginWork&quot;]    S2 --&gt; S3[&quot;3. Nav beginWork → completeWork&quot;]    S3 --&gt; S4[&quot;4. Header completeWork → 找 sibling&quot;]    S4 --&gt; S5[&quot;5. Main beginWork&quot;]    S5 --&gt; S6[&quot;6. ArticleList beginWork&quot;]    S6 --&gt; S7[&quot;7. Article beginWork → completeWork&quot;]    S7 --&gt; S8[&quot;8. ArticleList completeWork&quot;]    S8 --&gt; S9[&quot;9. Main completeWork → 找 sibling&quot;]    S9 --&gt; S10[&quot;10. Footer beginWork → completeWork&quot;]    S10 --&gt; S11[&quot;11. App completeWork → 全部完成&quot;]  </pre></div><p>整个过程就是：<strong>先一路向下（child），走到底了就完成当前节点，然后找兄弟（sibling），兄弟也没了就回父节点（return）</strong>。每一步都是通过指针跳转——不是递归调用——所以可以在任意节点暂停。</p><hr><h2 id="6-Scheduler（调度器）：React-内部的”包工头”"><a href="#6-Scheduler（调度器）：React-内部的”包工头”" class="headerlink" title="6. Scheduler（调度器）：React 内部的”包工头”"></a>6. Scheduler（调度器）：React 内部的”包工头”</h2><p>有了可中断的 Fiber 结构，还需要一个聪明的指挥官来决定”什么时候干活、什么时候休息”。这就是 <strong>Scheduler</strong>。</p><h3 id="时间切片（Time-Slicing）"><a href="#时间切片（Time-Slicing）" class="headerlink" title="时间切片（Time Slicing）"></a>时间切片（Time Slicing）</h3><p>Scheduler 给每个工作循环分配约 <strong>5ms</strong> 的时间片（不是 16.6ms，因为要给浏览器留余量做布局和绘制）。</p><figure class="highlight js"><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="comment">// Scheduler 的核心时间判断（简化）</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAME_YIELD_INTERVAL</span> = <span class="number">5</span>; <span class="comment">// ms</span></span><br><span class="line"><span class="keyword">let</span> deadline = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">shouldYield</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> performance.<span class="title function_">now</span>() &gt;= deadline;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">startWorkLoop</span>(<span class="params"></span>) &#123;</span><br><span class="line">  deadline = performance.<span class="title function_">now</span>() + <span class="variable constant_">FRAME_YIELD_INTERVAL</span>;</span><br><span class="line">  <span class="comment">// 开始处理任务...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="基于-MessageChannel-的调度"><a href="#基于-MessageChannel-的调度" class="headerlink" title="基于 MessageChannel 的调度"></a>基于 MessageChannel 的调度</h3><p>你可能会问：React 是怎么在”让出主线程”之后重新拿回执行权的？</p><p>答案是 <strong><code>MessageChannel</code></strong>。React 不使用 <code>setTimeout</code>（最小延迟 4ms，太慢）也不使用 <code>requestAnimationFrame</code>（和帧率绑定，不够灵活），而是用 <code>MessageChannel</code> 创建一个宏任务，让浏览器在处理完当前帧的渲染工作后，尽快回到 React 的工作循环。</p><figure class="highlight js"><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">// Scheduler 内部使用 MessageChannel（简化）</span></span><br><span class="line"><span class="keyword">const</span> channel = <span class="keyword">new</span> <span class="title class_">MessageChannel</span>();</span><br><span class="line"><span class="keyword">const</span> port = channel.<span class="property">port2</span>;</span><br><span class="line"></span><br><span class="line">channel.<span class="property">port1</span>.<span class="property">onmessage</span> = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 浏览器空闲了，继续处理 React 任务</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">hasWork</span>()) &#123;</span><br><span class="line">    deadline = performance.<span class="title function_">now</span>() + <span class="variable constant_">FRAME_YIELD_INTERVAL</span>;</span><br><span class="line">    <span class="title function_">workLoop</span>();</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">function</span> <span class="title function_">scheduleWork</span>(<span class="params"></span>) &#123;</span><br><span class="line">  port.<span class="title function_">postMessage</span>(<span class="literal">null</span>); <span class="comment">// 发消息，安排下一次执行</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样的调度节奏大概是：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    graph LR    A[&quot;React 工作 5ms&quot;] --&gt; B[&quot;浏览器渲染&quot;]    B --&gt; C[&quot;React 工作 5ms&quot;]    C --&gt; D[&quot;浏览器渲染&quot;]    D --&gt; E[&quot;...&quot;]  </pre></div><h3 id="优先级队列"><a href="#优先级队列" class="headerlink" title="优先级队列"></a>优先级队列</h3><p>Scheduler 内部维护了一个**最小堆（min-heap）**来管理任务队列，每个任务都有一个过期时间：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 任务优先级对应的超时时间</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">IMMEDIATE_PRIORITY</span>   = -<span class="number">1</span>;     <span class="comment">// 立即执行</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">USER_BLOCKING_PRIORITY</span> = <span class="number">250</span>;  <span class="comment">// 250ms 内必须执行</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">NORMAL_PRIORITY</span>      = <span class="number">5000</span>;   <span class="comment">// 5s 内执行</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">LOW_PRIORITY</span>         = <span class="number">10000</span>;  <span class="comment">// 10s 内执行</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">IDLE_PRIORITY</span>        = <span class="title class_">Infinity</span>; <span class="comment">// 空闲时再执行</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 任务入队时的过期时间 = 当前时间 + 超时时间</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">scheduleCallback</span>(<span class="params">priority, callback</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> startTime = performance.<span class="title function_">now</span>();</span><br><span class="line">  <span class="keyword">const</span> expirationTime = startTime + timeout[priority];</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> newTask = &#123;</span><br><span class="line">    callback,</span><br><span class="line">    expirationTime,</span><br><span class="line">    <span class="attr">sortIndex</span>: expirationTime, <span class="comment">// 用于最小堆排序</span></span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">push</span>(taskQueue, newTask); <span class="comment">// 插入最小堆</span></span><br><span class="line">  <span class="title function_">requestHostCallback</span>();     <span class="comment">// 安排执行</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>过期时间越早的任务排在堆顶，优先被执行。如果一个低优先级任务等了太久（超过了它的过期时间），它会”升级”成紧急任务被优先处理——这就防止了低优先级任务被无限饿死的问题。</p><hr><h2 id="7-shouldYield-：那个决定一切的判断"><a href="#7-shouldYield-：那个决定一切的判断" class="headerlink" title="7. shouldYield()：那个决定一切的判断"></a>7. <code>shouldYield()</code>：那个决定一切的判断</h2><p>把前面的内容串起来，React 的工作循环核心就是这个 <code>while</code> 条件：</p><figure class="highlight js"><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">while</span> (workInProgress !== <span class="literal">null</span> &amp;&amp; !<span class="title function_">shouldYield</span>()) &#123;</span><br><span class="line">  workInProgress = <span class="title function_">performUnitOfWork</span>(workInProgress);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>shouldYield()</code> 做的事情很简单：检查当前时间片是否已经用完。但它的影响是巨大的——<strong>它是 React 从”同步阻塞”走向”协作调度”的关键转折点</strong>。</p><p>每处理完一个 Fiber 节点（一个工作单元），React 都会调用一次 <code>shouldYield()</code>：</p><ul><li>返回 <code>false</code>：时间还够，继续处理下一个节点</li><li>返回 <code>true</code>：时间到了，记住当前的 <code>workInProgress</code> 指针，让出主线程</li></ul><p>等浏览器做完渲染工作，<code>MessageChannel</code> 的回调触发，React 从上次暂停的 <code>workInProgress</code> 位置继续——无缝衔接。</p><hr><h2 id="8-第一阶段总结"><a href="#8-第一阶段总结" class="headerlink" title="8. 第一阶段总结"></a>8. 第一阶段总结</h2><p>React 16 的这场架构重构，本质上是<strong>将同步的 UI 更新转变为异步的、可预排序的任务调度</strong>：</p><table><thead><tr><th>概念</th><th>解决的问题</th></tr></thead><tbody><tr><td><strong>Fiber 节点</strong></td><td>虚拟 DOM 的升级版，携带调度信息和副作用标记</td></tr><tr><td><strong>链表化树结构</strong></td><td>用 child&#x2F;sibling&#x2F;return 指针替代递归，实现可中断遍历</td></tr><tr><td><strong>时间切片</strong></td><td>每次只工作 5ms，留时间给浏览器渲染</td></tr><tr><td><strong>Scheduler</strong></td><td>优先级队列 + MessageChannel，决定什么时候做什么</td></tr><tr><td><strong>shouldYield()</strong></td><td>每个工作单元后检查，实现协作式让出</td></tr></tbody></table><p>这套机制给 React 带来了两个关键能力：<strong>时间切片</strong>和<strong>并发渲染</strong>。它们是后续双缓存、Hooks、并发模式等一切高级特性的基石。</p><p>下一篇，我们进入 React 更新的核心地带：<strong>Render 阶段</strong>与 <strong>Commit 阶段</strong>——看看 React 是如何通过”双缓存”技术把变更安全地从内存搬到屏幕上的。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2025/react-source-1/</id>
    <link href="https://linkdiary.com/2025/react-source-1/"/>
    <published>2025-06-10T13:03:10.000Z</published>
    <summary>从浏览器的帧预算出发，理解 React 16 为什么要引入 Fiber 架构，以及 Fiber 节点、链表遍历和 Scheduler 调度器背后的实现原理。</summary>
    <title>React 源码深潜（一）：走进 Fiber 架构的&quot;操作系统&quot;</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note success flat"><ul><li>本篇侧重 <strong>WebGL 基础</strong>，默认你是“没写过&#x2F;刚入门”的状态。</li><li>文章里会穿插一些小 demo（我更喜欢边跑边理解，而不是只看概念）。</li><li>会涉及少量矩阵&#x2F;坐标的推导，不想深究也没关系，先把流程和 API 跑通就已经很够用了。</li></ul></div><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><h3 id="WebGL-是什么"><a href="#WebGL-是什么" class="headerlink" title="WebGL 是什么"></a>WebGL 是什么</h3><p>WebGL，全称 <strong>Web Graphics Library</strong>，是一种用于在网页浏览器中渲染 2D 和 3D 图形的 API。它基于 <strong>OpenGL ES 2.0</strong>，通过 JavaScript 在不使用插件的情况下，直接利用 GPU 进行硬件加速渲染。</p><p>WebGL 是现代浏览器（如 Chrome、Firefox、Safari）支持的标准，能够在 PC、手机等多平台上运行。</p><h3 id="WebGL-的应用场景"><a href="#WebGL-的应用场景" class="headerlink" title="WebGL 的应用场景"></a>WebGL 的应用场景</h3><p><strong>游戏开发</strong>：许多浏览器游戏通过 WebGL 实现复杂的 3D 场景和交互效果。 <a href="https://playcanvas.com/">图形&#x2F;游戏引擎</a></p><p><strong>数据可视化</strong>：使用 WebGL 可以在网页上实现高性能的 3D 数据可视化工具。</p><p><strong>交互式 3D 网页</strong>：可以实现像 3D 地图、虚拟展览等内容。<a href="https://map.baidu.com/">百度地图</a></p><p><strong>VR&#x2F;AR</strong>：结合 WebGL 和 WebXR API，可以在网页上开发虚拟现实和增强现实应用。<a href="https://www.kuleiman.com/104842/index.html">VR 体验</a>、 <a href="https://showroom.littleworkshop.fr/">房间展示</a></p><h3 id="WebGL-的基础概念"><a href="#WebGL-的基础概念" class="headerlink" title="WebGL 的基础概念"></a>WebGL 的基础概念</h3><h4 id="OpenGL-与-WebGL-的关系"><a href="#OpenGL-与-WebGL-的关系" class="headerlink" title="OpenGL 与 WebGL 的关系"></a>OpenGL 与 WebGL 的关系</h4><p><strong>OpenGL</strong>（Open Graphics Library）是一个跨语言、跨平台的图形 API，主要用于2D和3D图形的渲染。它广泛应用于游戏开发、CAD、虚拟现实和可视化等领域。<strong>OpenGL ES</strong> 是 <strong>OpenGL</strong> 的一个精简版本，专门为嵌入式系统（如手机、平板）设计。</p><p>而 <strong>WebGL</strong> （Web Graphics Library）是基于 <strong>OpenGL ES 2.0</strong> 的一种 API，专门为网页设计，允许开发者在不需要插件的情况下通过 <strong>JavaScript</strong> 进行编程在浏览器中进行3D图形渲染。WebGL 使得网页应用能够使用 GPU 加速，提供了对图形渲染的高效支持。</p><h4 id="着色器编程-GLSL"><a href="#着色器编程-GLSL" class="headerlink" title="着色器编程 (GLSL)"></a>着色器编程 (GLSL)</h4><p>着色器编程是图形编程中的核心部分，使用 <strong>GLSL</strong>（OpenGL Shading Language）编写。<strong>GLSL</strong> 是一种高性能的着色器语言，允许开发者在 GPU 上执行自定义的图形计算。</p><p><strong>顶点着色器</strong>（Vertex Shader），用来描述顶点的特性，如描述顶点的位置。顶点就是指二维或三维空间中的一个坐标。</p><p><strong>片元着色器</strong>（Fragment Shader），用来描述每个像素的颜色。</p><p>着色器程序在 <strong>JavaScript</strong> 中就是一段字符串，着色器程序的工作流程就是 <strong>JavaScript</strong> 读取相关着色器信息，并传递给 <strong>WebGL</strong> 进行使用。</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224182908044.png"></p><h2 id="渲染基础"><a href="#渲染基础" class="headerlink" title="渲染基础"></a>渲染基础</h2><h3 id="点的绘制"><a href="#点的绘制" class="headerlink" title="点的绘制"></a>点的绘制</h3><h4 id="绘制一个点"><a href="#绘制一个点" class="headerlink" title="绘制一个点"></a>绘制一个点</h4><p>我们先画一个点，把 WebGL 的整套流程（写 shader → 编译 → link → draw）跑通。</p><p>在画布中心绘制一个红色的点。</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取 canvas 元素并获取 webgl 的上下文</span></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#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> <span class="variable constant_">VERTEX_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);</span></span><br><span class="line"><span class="string">    gl_PointSize = 10.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="comment">// gl_Position vec4(0.0,0.0,0.0,1.0)  x, y, z, w齐次坐标 (x/w, y/w, z/w)</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="variable constant_">FRAGMENT_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="comment">// gl_FragColor vec4(1.0,0.0,0.0,1.0) r, g, b, a</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> vertexShader = gl.<span class="title function_">createShader</span>(gl.<span class="property">VERTEX_SHADER</span>);</span><br><span class="line"><span class="keyword">const</span> fragmentShader = gl.<span class="title function_">createShader</span>(gl.<span class="property">FRAGMENT_SHADER</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 为着色器指定源码</span></span><br><span class="line">gl.<span class="title function_">shaderSource</span>(vertexShader, <span class="variable constant_">VERTEX_SHADER_SOURCE</span>)</span><br><span class="line">gl.<span class="title function_">shaderSource</span>(fragmentShader, <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 编译着色器</span></span><br><span class="line">gl.<span class="title function_">compileShader</span>(vertexShader)</span><br><span class="line">gl.<span class="title function_">compileShader</span>(fragmentShader)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建程序对象</span></span><br><span class="line"><span class="keyword">const</span> program = gl.<span class="title function_">createProgram</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 为程序对象关联着色器</span></span><br><span class="line">gl.<span class="title function_">attachShader</span>(program, vertexShader)</span><br><span class="line">gl.<span class="title function_">attachShader</span>(program, fragmentShader)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 连接着色器程序</span></span><br><span class="line">gl.<span class="title function_">linkProgram</span>(program)</span><br><span class="line"><span class="comment">// 将连接好的程序设置为当前的活动程序</span></span><br><span class="line">gl.<span class="title function_">useProgram</span>(program)</span><br><span class="line"><span class="comment">// 执行点的绘制</span></span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>以上代码实现效果如下：<br><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224183028825.png"></p><h4 id="2-1-2-基础绘制流程"><a href="#2-1-2-基础绘制流程" class="headerlink" title="2.1.2 基础绘制流程"></a>2.1.2 基础绘制流程</h4><p>从一个点的绘制中可以看到，<strong>WebGL</strong> 执行绘制之前需要一系列的着色器创建、关联、编译等初始化步骤，初始化步骤完成之后就可以执行图形的绘制。</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224183052712.png"></p><p>初始化的一系列步骤重复且必需，所以对这系列步骤可进行封装复用。封装方法如下：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">initShader</span> = (<span class="params">gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> vertexShader = gl.<span class="title function_">createShader</span>(gl.<span class="property">VERTEX_SHADER</span>);</span><br><span class="line">  <span class="keyword">const</span> fragmentShader = gl.<span class="title function_">createShader</span>(gl.<span class="property">FRAGMENT_SHADER</span>);</span><br><span class="line"> </span><br><span class="line">  gl.<span class="title function_">shaderSource</span>(vertexShader, <span class="variable constant_">VERTEX_SHADER_SOURCE</span>) <span class="comment">// 指定顶点着色器的源码</span></span><br><span class="line">  gl.<span class="title function_">shaderSource</span>(fragmentShader, <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span>) <span class="comment">// 指定片元着色器的源码</span></span><br><span class="line"> </span><br><span class="line">  gl.<span class="title function_">compileShader</span>(vertexShader)</span><br><span class="line">  gl.<span class="title function_">compileShader</span>(fragmentShader)</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">const</span> program = gl.<span class="title function_">createProgram</span>();</span><br><span class="line"> </span><br><span class="line">  gl.<span class="title function_">attachShader</span>(program, vertexShader)</span><br><span class="line">  gl.<span class="title function_">attachShader</span>(program, fragmentShader)</span><br><span class="line"> </span><br><span class="line">  gl.<span class="title function_">linkProgram</span>(program)</span><br><span class="line"> </span><br><span class="line">  gl.<span class="title function_">useProgram</span>(program)</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">return</span> program;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="变量的使用"><a href="#变量的使用" class="headerlink" title="变量的使用"></a>变量的使用</h3><h4 id="attribute-变量和-vertexAttrib4f-同族函数"><a href="#attribute-变量和-vertexAttrib4f-同族函数" class="headerlink" title="attribute 变量和 vertexAttrib4f() 同族函数"></a>attribute 变量和 vertexAttrib4f() 同族函数</h4><p><code>attribute</code>  是顶点着色器中的一个输入变量，用于接收与每个顶点相关的数据，如位置、颜色等。<code>attribute</code> 只能在顶点着色器中使用，不能用于片元着色器。</p><p><code>vertexAttrib4f</code> 是一个用于设置指定 attribute 的函数，允许将四个浮点数分量传递给顶点着色器。</p><p>另外 <code>vertexAttrib1f</code> 、<code>vertexAttrib2f</code> 、<code>vertexAttrib3f</code> 分别可以只指定一个、两个、三个分量传递给着色器。</p><p><code>attribute</code> 和 <code>vertexAttrib4f</code> 函数的应用代码如下：</p><figure class="highlight js"><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="keyword">const</span> <span class="variable constant_">VERTEX_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  // 使用 attribute 变量定义顶点着色器的位置</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">// 将变量赋值给顶点位置</span></span><br><span class="line"><span class="string">    gl_Position = aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 10.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="comment">// 使用 vertexAttrib4f 同族函数为顶点着色器传入位置变量</span></span><br><span class="line"><span class="comment">// 注意：这里的 aPosition 是 gl.getAttribLocation(program, &#x27;aPosition&#x27;) 拿到的 location</span></span><br><span class="line">gl.<span class="title function_">vertexAttrib1f</span>(aPosition, x)</span><br><span class="line">gl.<span class="title function_">vertexAttrib2f</span>(aPosition, x, y)</span><br><span class="line">gl.<span class="title function_">vertexAttrib3f</span>(aPosition, x, y, z)</span><br><span class="line">gl.<span class="title function_">vertexAttrib4f</span>(aPosition, x, y, z, w)</span><br></pre></td></tr></table></figure><h4 id="uniform-变量和-uniform4f-同族函数介绍"><a href="#uniform-变量和-uniform4f-同族函数介绍" class="headerlink" title="uniform 变量和 uniform4f() 同族函数介绍"></a>uniform 变量和 uniform4f() 同族函数介绍</h4><p><code>uniform</code> 是一个全局变量，所有的顶点和片元共享同一个值。不同于  <code>attribute</code> ，<code>attribute</code> 是顶点着色器的输入变量，每个顶点都有自己的值 。</p><p><code>uniform</code> 用于传递不随顶点变化而变化的参数，如变换矩阵、全局颜色等常量数据。<br>同理，<code>float/vec2/vec3/vec4</code> 对应 <code>uniform1f/2f/3f/4f</code> 这一组 API。</p><figure class="highlight js"><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">const</span> <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  precision mediump float;</span></span><br><span class="line"><span class="string">  // 使用 uniform 变量定义颜色</span></span><br><span class="line"><span class="string">  uniform vec4 uColor;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = uColor;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// vec4 对应 uniform4f（r, g, b, a）</span></span><br><span class="line"><span class="comment">// uColor 是 gl.getUniformLocation(program, &#x27;uColor&#x27;) 拿到的 location</span></span><br><span class="line">gl.<span class="title function_">uniform4f</span>(uColor, r, g, b, a)</span><br></pre></td></tr></table></figure><p>以下是使用 <code>attribute</code> 和 <code>uniform</code> 变量及其对应赋值函数的应用示例代码。 </p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 20.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  precision mediump float;</span></span><br><span class="line"><span class="string">  uniform vec4 uColor;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = uColor;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER_SOURCE</span>, <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> uColor = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;uColor&#x27;</span>)</span><br><span class="line"></span><br><span class="line">gl.<span class="title function_">uniform4f</span>(uColor, <span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> x = <span class="number">0</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  x += <span class="number">0.05</span></span><br><span class="line">  <span class="keyword">if</span> (x &gt; <span class="number">1</span>) x = -<span class="number">1</span></span><br><span class="line">  gl.<span class="title function_">vertexAttrib1f</span>(aPosition, x)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">1</span>)</span><br><span class="line">&#125;, <span class="number">100</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">colorChangeHandler</span> = (<span class="params">rgb</span>) =&gt; &#123;</span><br><span class="line">  gl.<span class="title function_">uniform4f</span>(uColor, ...rgb, <span class="number">1</span>)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">1</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;#red-btn&#x27;</span>).<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">colorChangeHandler</span>([<span class="number">1</span>, <span class="number">0</span>, <span class="number">0</span>])</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;#green-btn&#x27;</span>).<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">colorChangeHandler</span>([<span class="number">0</span>, <span class="number">1</span>, <span class="number">0</span>])</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;#blue-btn&#x27;</span>).<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">colorChangeHandler</span>([<span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>])</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="坐标系介绍"><a href="#坐标系介绍" class="headerlink" title="坐标系介绍"></a>坐标系介绍</h3><h4 id="WebGL-坐标系与-Canvas-坐标系"><a href="#WebGL-坐标系与-Canvas-坐标系" class="headerlink" title="WebGL 坐标系与 Canvas 坐标系"></a>WebGL 坐标系与 Canvas 坐标系</h4><p>**Canvas 坐标系，**原点位于画布的左上角，横坐标和纵坐标向左和向下依次增加，坐标单位为 <strong>像素(px)</strong>。同 <strong>CSS  position</strong> 的 <strong>top、left</strong> 计算逻辑类似。如下图所示：</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224183327093.png"></p><p><strong>WebGL 坐标系</strong>不同于网页布局常规坐标系。</p><p><strong>二维 WebGL</strong> <strong>坐标系</strong>更像是数学中的二维坐标系，原点在画布的正中心，坐标上增下减、右增左减。坐标单位为百分比， 值范围在 -1.0 与 1.0 之间。如下图所示。</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224183342276.png"></p><p><strong>三维 WebGL 坐标系</strong>则是在二维坐标系基础上，增加一个 Z 轴，Z 轴正方向指向屏幕外，类似 CSS 定位中的 <code>z-index</code> 。如下图所示：</p><p><em>PS: 上述 WebGL 中的三维坐标系的 Z 轴方向为默认方向，Z 轴方向可设置，感兴趣可自查左右手坐标系。</em></p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224183501299.png"></p><h4 id="坐标系的转换"><a href="#坐标系的转换" class="headerlink" title="坐标系的转换"></a>坐标系的转换</h4><p>由于 <strong>WebGL</strong> 和网页事件中的坐标系对于坐标的计算方式不一致。所以如果 <strong>WebGL</strong> 中元素涉及对鼠标事件进行相应时，需要对鼠标坐标进行转换，转换成在 <strong>WebGL</strong> 中的坐标以进行位置设置。</p><div class="note default flat"><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224183854013.png"><br><strong>已知：</strong></p><ol><li>鼠标在视窗的位置信息，以左上角为顶点，横坐标 e.clientX,  纵坐标 e.clientY，单位为 px</li><li>画布的左上角的位置信息以及宽高，e.target.getBoundingClientRect()， top、left，width、height 单位 px</li></ol><p><strong>求：</strong> 鼠标所在的位置在 WebGL 坐标系中的坐标</p></div><p>答案如下：</p><figure class="highlight js"><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">canvas.<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">(<span class="params">e</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 获取画布的位置信息</span></span><br><span class="line">  <span class="keyword">const</span> sketch = e.<span class="property">target</span>.<span class="title function_">getBoundingClientRect</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> offsetX = e.<span class="property">clientX</span> - sketch.<span class="property">left</span></span><br><span class="line">  <span class="keyword">const</span> offsetY = e.<span class="property">clientY</span> - sketch.<span class="property">top</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> halfWidth = canvas.<span class="property">offsetWidth</span> / <span class="number">2</span></span><br><span class="line">  <span class="keyword">const</span> halfHeight = canvas.<span class="property">offsetHeight</span> / <span class="number">2</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 相对 WebGL 坐标系原点计算偏移量之后进行比例计算</span></span><br><span class="line">  <span class="keyword">const</span> clickX = (offsetX - halfWidth) / halfWidth</span><br><span class="line">  <span class="keyword">const</span> clickY = (halfHeight - offsetY) / halfHeight</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;鼠标点击的位置对应的 WebGL 坐标系中的坐标是&#x27;</span>)</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`clickX: <span class="subst">$&#123;clickX&#125;</span>, clickY: <span class="subst">$&#123;clickY&#125;</span>`</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h4 id="实现一个简单的画板"><a href="#实现一个简单的画板" class="headerlink" title="实现一个简单的画板"></a>实现一个简单的画板</h4><p>在上一小节中，通过对坐标系进行转换，已经实现了用鼠标控制点绘制的位置。基于此，我们可以重复绘制多个点来实现一个<em><strong>简易</strong></em> 的画板（突出一个简易）。</p><p>直接 Show Code。</p><figure class="highlight js"><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><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  precision mediump float;</span></span><br><span class="line"><span class="string">  uniform vec4 uColor;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = uColor;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="keyword">const</span> vertexShader = gl.<span class="title function_">createShader</span>(gl.<span class="property">VERTEX_SHADER</span>)</span><br><span class="line"><span class="keyword">const</span> fragmentShader = gl.<span class="title function_">createShader</span>(gl.<span class="property">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER_SOURCE</span>, <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> uColor = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;uColor&#x27;</span>)</span><br><span class="line">gl.<span class="title function_">uniform4f</span>(uColor, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> points = []</span><br><span class="line"><span class="comment">// 多个点绘制方法</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">drawPoints</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">for</span>(<span class="keyword">let</span> point <span class="keyword">of</span> points) &#123;</span><br><span class="line">    gl.<span class="title function_">vertexAttrib2f</span>(aPosition, ...point)</span><br><span class="line">    gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">1</span>);</span><br><span class="line">  &#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">const</span> <span class="title function_">colorChangeHandler</span> = (<span class="params">rgb</span>) =&gt; &#123;</span><br><span class="line">  gl.<span class="title function_">uniform4f</span>(uColor, ...rgb, <span class="number">1</span>)</span><br><span class="line">  <span class="title function_">drawPoints</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;#red-btn&#x27;</span>).<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">colorChangeHandler</span>([<span class="number">1</span>, <span class="number">0</span>, <span class="number">0</span>])</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;#green-btn&#x27;</span>).<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">colorChangeHandler</span>([<span class="number">0</span>, <span class="number">1</span>, <span class="number">0</span>])</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;#blue-btn&#x27;</span>).<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">colorChangeHandler</span>([<span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>])</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;#clear-btn&#x27;</span>).<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  points = []</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">canvas.<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">(<span class="params">e</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> position = e.<span class="property">target</span>.<span class="title function_">getBoundingClientRect</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> offsetX = e.<span class="property">clientX</span> - position.<span class="property">left</span></span><br><span class="line">  <span class="keyword">const</span> offsetY = e.<span class="property">clientY</span> - position.<span class="property">top</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> halfWidth = canvas.<span class="property">offsetWidth</span> / <span class="number">2</span></span><br><span class="line">  <span class="keyword">const</span> halfHeight = canvas.<span class="property">offsetHeight</span> / <span class="number">2</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> clickX = (offsetX - halfWidth) / halfWidth</span><br><span class="line">  <span class="keyword">const</span> clickY = (halfHeight - offsetY) / halfHeight</span><br><span class="line"></span><br><span class="line">  points.<span class="title function_">push</span>([clickX, clickY])</span><br><span class="line">  <span class="title function_">drawPoints</span>()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="实现简易版贪吃蛇"><a href="#实现简易版贪吃蛇" class="headerlink" title="实现简易版贪吃蛇"></a>实现简易版贪吃蛇</h3><p>通过对以上 WebGL 基础知识的学习，已经可以使用 WebGL 来实现一个小游戏了，以下是一个贪吃蛇的代码，感兴趣同学可以自行研究。</p><figure class="highlight html"><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><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;en&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">title</span>&gt;</span>贪吃蛇<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css">    <span class="selector-tag">canvas</span>&#123;</span></span><br><span class="line"><span class="language-css">      <span class="attribute">background-color</span>: <span class="number">#f0f0f0</span>;</span></span><br><span class="line"><span class="language-css">    &#125;</span></span><br><span class="line"><span class="language-css">  </span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">h3</span>&gt;</span><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;/&quot;</span>&gt;</span>Home<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="tag">&lt;/<span class="name">h3</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">canvas</span> <span class="attr">id</span>=<span class="string">&quot;canvas&quot;</span> <span class="attr">width</span>=<span class="string">&quot;600&quot;</span> <span class="attr">height</span>=<span class="string">&quot;600&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">canvas</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> ctx = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> gl = ctx.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 创建着色器源码</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER_SOURCE</span> = <span class="string">`</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">  // 只传递顶点数据</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">  attribute vec4 aPosition;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">  void main() &#123;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">    gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">    gl_PointSize = 15.0;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">  &#125;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">`</span>; <span class="comment">// 顶点着色器</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span> = <span class="string">`</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">  void main() &#123;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">  &#125;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">`</span>; <span class="comment">// 片元着色器</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER_SOURCE</span>, <span class="variable constant_">FRAGMENT_SHADER_SOURCE</span>)</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 蛇身的长度</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">let</span> points = [</span></span><br><span class="line"><span class="language-javascript">  &#123;<span class="attr">x</span>: <span class="number">0</span>, <span class="attr">y</span>: <span class="number">0</span>&#125;,</span></span><br><span class="line"><span class="language-javascript">]</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 食物的坐标</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> random = &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">isConnect</span>: <span class="literal">true</span></span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 移动的速度</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">let</span> originSpeed = <span class="number">0.02</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 行动的速度</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">let</span> speed = originSpeed;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 移动的方向</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">let</span> direction = <span class="string">&#x27;x&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 允许吃掉食物的误差范围</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">let</span> base = <span class="number">1.5</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="variable language_">document</span>.<span class="property">onkeydown</span> = <span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">switch</span> (event.<span class="property">keyCode</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">case</span> <span class="number">37</span>:</span></span><br><span class="line"><span class="language-javascript">      direction = <span class="string">&#x27;x&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">      speed = -originSpeed</span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">break</span>;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">case</span> <span class="number">38</span>:</span></span><br><span class="line"><span class="language-javascript">      direction = <span class="string">&#x27;y&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">      speed = originSpeed;</span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">break</span>;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">case</span> <span class="number">39</span>:</span></span><br><span class="line"><span class="language-javascript">      direction = <span class="string">&#x27;x&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">      speed = originSpeed;</span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">break</span>;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">case</span> <span class="number">40</span>:</span></span><br><span class="line"><span class="language-javascript">      direction = <span class="string">&#x27;y&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">      speed = -originSpeed;</span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">break</span>;</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">function</span> <span class="title function_">createRandom</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">if</span> (random.<span class="property">isConnect</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    random.<span class="property">x</span> = <span class="title class_">Math</span>.<span class="title function_">random</span>() * <span class="number">2</span> - <span class="number">1</span>;</span></span><br><span class="line"><span class="language-javascript">    random.<span class="property">y</span> = <span class="title class_">Math</span>.<span class="title function_">random</span>() * <span class="number">2</span> - <span class="number">1</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    random.<span class="property">isConnect</span> = <span class="literal">false</span>;</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">function</span> <span class="title function_">draw</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">  gl.<span class="title function_">vertexAttrib3f</span>(aPosition, random.<span class="property">x</span>, random.<span class="property">y</span>, <span class="number">0.0</span>);</span></span><br><span class="line"><span class="language-javascript">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">1</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">let</span> prex = <span class="number">0</span>;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">let</span> prey = <span class="number">0</span>;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; points.<span class="property">length</span>; i++) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">if</span> (i === <span class="number">0</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">      prex = points[<span class="number">0</span>].<span class="property">x</span></span></span><br><span class="line"><span class="language-javascript">      prey = points[<span class="number">0</span>].<span class="property">y</span></span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>][direction] += speed;</span></span><br><span class="line"><span class="language-javascript">    &#125; <span class="keyword">else</span> &#123;</span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">let</span> &#123;x, y&#125; = points[i]</span></span><br><span class="line"><span class="language-javascript">      points[i].<span class="property">x</span> = prex</span></span><br><span class="line"><span class="language-javascript">      points[i].<span class="property">y</span> = prey</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">      prex = x;</span></span><br><span class="line"><span class="language-javascript">      prey = y;</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    gl.<span class="title function_">vertexAttrib3f</span>(aPosition, points[i].<span class="property">x</span>, points[i].<span class="property">y</span>, <span class="number">0.0</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">1</span>);</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">let</span> timer = <span class="literal">null</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">function</span> <span class="title function_">start</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">  timer = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="comment">// 边界判断</span></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">if</span> (</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">x</span> &gt; <span class="number">1.0</span> ||</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">x</span> &lt; -<span class="number">1.0</span> ||</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">y</span> &lt; -<span class="number">1.0</span> ||</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">y</span> &gt; <span class="number">1.0</span></span></span><br><span class="line"><span class="language-javascript">    ) &#123;</span></span><br><span class="line"><span class="language-javascript">      <span class="title function_">alert</span>(<span class="string">&#x27;游戏结束&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript">      <span class="title function_">restart</span>();</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">if</span> (</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">x</span> &gt; random.<span class="property">x</span> - base * originSpeed &amp;&amp;</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">x</span> &lt; random.<span class="property">x</span> + base * originSpeed &amp;&amp;</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">y</span> &lt; random.<span class="property">y</span> + base * originSpeed &amp;&amp;</span></span><br><span class="line"><span class="language-javascript">      points[<span class="number">0</span>].<span class="property">y</span> &gt; random.<span class="property">y</span> - base * originSpeed</span></span><br><span class="line"><span class="language-javascript">    ) &#123;</span></span><br><span class="line"><span class="language-javascript">      points.<span class="title function_">push</span>(&#123; <span class="attr">x</span>: random.<span class="property">x</span>, <span class="attr">y</span>: random.<span class="property">y</span> &#125;)</span></span><br><span class="line"><span class="language-javascript">      random.<span class="property">isConnect</span> = <span class="literal">true</span>;</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="title function_">createRandom</span>();</span></span><br><span class="line"><span class="language-javascript">    <span class="title function_">draw</span>();</span></span><br><span class="line"><span class="language-javascript">  &#125;, <span class="number">100</span>)</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="title function_">start</span>();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">function</span> <span class="title function_">restart</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="built_in">clearInterval</span>(timer)</span></span><br><span class="line"><span class="language-javascript">  points = [</span></span><br><span class="line"><span class="language-javascript">    &#123;<span class="attr">x</span>: <span class="number">0</span>, <span class="attr">y</span>: <span class="number">0</span>&#125;</span></span><br><span class="line"><span class="language-javascript">  ]</span></span><br><span class="line"><span class="language-javascript">  direction = <span class="string">&#x27;x&#x27;</span></span></span><br><span class="line"><span class="language-javascript">  speed = originSpeed</span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">start</span>();</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</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><h4 id="缓冲区对象介绍"><a href="#缓冲区对象介绍" class="headerlink" title="缓冲区对象介绍"></a>缓冲区对象介绍</h4><p>在 <strong>WebGL</strong> 中，<strong>缓冲区对象</strong>（Buffer Object）是用于存储图形数据的内存区域，它们用于将数据传递给 GPU，以便进行绘制和渲染。</p><p><strong>WebGL</strong> 主要使用以下几种类型的缓冲区对象：</p><ul><li><strong>顶点缓冲区对象（gl.ARRAY_BUFFER）</strong>：用于存储顶点数据（如位置、法线、颜色、纹理坐标等）。</li><li><strong>索引缓冲区对象（gl.ELEMENT_ARRAY_BUFFER）</strong>：用于存储索引数据，以减少顶点数据的冗余。</li><li><strong>帧缓冲对象（gl.FRAMBUFFER）</strong>：用于离屏渲染，可以将渲染结果输出到纹理中。</li></ul><p>缓冲区数据的存储和传输有 <code>STATIC_DRAW</code>、<code>DYNAMIC_DRAW</code> 和 <code>STREAM_DRAW</code> 三种方式。</p><ul><li><strong><code>STATIC_DRAW</code></strong>：适用于静态数据，数据几乎不变，一次写入后被多次使用。</li><li><strong><code>DYNAMIC_DRAW</code></strong>：适用于频繁更新的数据，数据每次更新后可以被多次使用。</li><li><strong><code>STREAM_DRAW</code></strong>：适用于每次更新后只使用一次的数据，适合流式更新场景。</li></ul><p>以下是三种存储方式的对比：</p><table><thead><tr><th><strong>特性</strong></th><th>STATIC_DRAW</th><th>DYNAMIC_DRAW</th><th>STREAM_DRAW</th></tr></thead><tbody><tr><td><strong>数据更新频率</strong></td><td>数据<strong>一次写入</strong>，然后多次使用</td><td>数据<strong>频繁更新</strong>，但每次更新后<strong>使用多次</strong></td><td>数据<strong>频繁更新</strong>，每次更新后通常<strong>只使用一次</strong></td></tr><tr><td><strong>使用场景</strong></td><td>静态数据，如固定的顶点、几何图形、背景模型</td><td>动态数据，如物理模拟、实时交互、角色动画</td><td>实时生成的数据，如粒子系统、摄像机视图变化</td></tr><tr><td><strong>性能优化方向</strong></td><td>优化为一次传输后高效读取，使用 GPU 内存存储</td><td>优化为数据频繁更新且在短时间内重复使用</td><td>优化为数据频繁更新，减少 GPU 内存占用和传输</td></tr><tr><td><strong>举例</strong></td><td>固定模型、场景静态网格</td><td>角色动画、实时变形模型</td><td>粒子系统、实时变化的图形效果</td></tr></tbody></table><p>以下通过使用缓冲区对象来绘制多个点来对缓冲区对象的使用步骤进行介绍。</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#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> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, <span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</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> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绑定缓冲区对象为顶点缓冲区</span></span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 将数据填充到缓冲区对象</span></span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用 gl.enableVertexAttribArray() 启用顶点属性数组，并指定使用的属性索引：</span></span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绘制图形</span></span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br></pre></td></tr></table></figure><h4 id="多缓冲区和数据偏移"><a href="#多缓冲区和数据偏移" class="headerlink" title="多缓冲区和数据偏移"></a>多缓冲区和数据偏移</h4><p>在实际的开发中，往往会使用到多组属性，比如点的绘制中，位置、颜色和大小等信息可能都需要从缓冲区中读取。这时候可以创建多个缓冲区来供 <strong>WebGL</strong> 绘制使用。</p><p>以下是为上一小节的示例增加了点大小的缓冲数据。</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  attribute float aPointSize;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = aPointSize;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> aPointSize = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPointSize&#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> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</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> size = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([<span class="number">10.0</span>, <span class="number">20.0</span>, <span class="number">30.0</span>])</span><br><span class="line"><span class="keyword">const</span> sizeBuffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, sizeBuffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, size, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPointSize)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPointSize, <span class="number">1</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绘制图形</span></span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br></pre></td></tr></table></figure><p>如果每个元素的每个属性都要创建一个缓冲区，显然对代码的可读性和维护性不是很友好。接下来介绍一个方法 <code>gl.vertexAttribPointer()</code> 。这个方法让多个属性存在一个缓冲区对象里成为了可能。  </p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(index, size, type, normalized, stride, offset);</span><br></pre></td></tr></table></figure><ul><li>**index：**顶点属性的索引，指定要修改的顶点属性的编号。它通常是通过 <code>gl.getAttribLocation()</code> 获取的。</li><li><strong>size:</strong> 每个顶点属性的组成部分数量，表示每个顶点有多少个数值（即多少个数据分量）。取值范围：1、2、3、4。</li><li><strong>type:</strong> 指定顶点属性的数据类型，表示每个数据的类型。常用的数据类型有：<code>gl.FLOAT</code> 、<code>gl.UNSIGNED_BYTE</code> 、<code>gl.BYTE</code> 、 <code>gl.UNSIGNED_SHORT</code> 等。</li><li><strong>normalized:</strong> 是否将整数类型的数据归一化为浮点数，如果为 <code>true</code>，则在使用时将该值映射到 <code>[0, 1]</code>（无符号）或 <code>[-1, 1]</code>（有符号）；如果为 <code>false</code>，则直接将数据按原始值传递。对于浮点数类型（如 <code>gl.FLOAT</code>），这个参数可以忽略，通常设为 <code>false</code>。</li><li><strong>stride:</strong> 连续顶点属性间的字节偏移量。它用于指定从当前顶点属性到下一个顶点属性之间的间隔。如果所有顶点属性是紧密排列的（没有间隔），则可以设为 <code>0</code>，WebGL 会自动计算步幅。</li><li><strong>offset:</strong> 指定顶点属性数组中第一分量的字节偏移量，即从缓冲区开头到第一个顶点属性的起始位置的偏移量。通常，如果数据从头开始，则偏移量为 <code>0</code>，如果属性数据有其他数据在前面，则需要计算偏移量。</li></ul><p>以下是使用 <code>gl.vertexAttribPointer()</code> 方法对以上代码两个缓冲区对象进行的优化。</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  attribute float aPointSize;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = aPointSize;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> aPointSize = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPointSize&#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> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>, <span class="number">10.0</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>, <span class="number">20.0</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>, <span class="number">30.0</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</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> <span class="variable constant_">BYTES</span> = points.<span class="property">BYTES_PER_ELEMENT</span></span><br><span class="line"></span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="variable constant_">BYTES</span> * <span class="number">3</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPointSize)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPointSize, <span class="number">1</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="variable constant_">BYTES</span> * <span class="number">3</span>, <span class="variable constant_">BYTES</span> * <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绘制图形</span></span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br></pre></td></tr></table></figure><h3 id="3-2-多种图形绘制"><a href="#3-2-多种图形绘制" class="headerlink" title="3.2 多种图形绘制"></a>3.2 多种图形绘制</h3><p>在 <strong>WebGL</strong> 中，支持绘制的图形有 <strong>点</strong>、<strong>线</strong> 和 <strong>三角形</strong> 三种。</p><p>**为什么选择三角形呢？**这是因为任何多边形都可以最终分解为多个三角形，也就是说三角形是多边形的基本单位，并且三角形一定在一个平面上。</p><p>下表是所有支持绘制的图形的标识符。</p><table><thead><tr><th>值</th><th>图形</th><th>说明</th></tr></thead><tbody><tr><td><strong>gl.POINTS</strong></td><td>点</td><td>⼀系列点</td></tr><tr><td><strong>gl.LINES</strong></td><td>线段</td><td>⼀系列单独的线段，如果顶点是奇数，最后⼀个会被忽略</td></tr><tr><td><strong>gl.LINE_LOOP</strong></td><td>闭合线</td><td>⼀系列连接的线段，结束时，会闭合终点和起点</td></tr><tr><td><strong>gl.LINE_STRIP</strong></td><td>线条</td><td>⼀系列连接的线段，不会闭合终点和起点</td></tr><tr><td><strong>gl.TRIANGLES</strong></td><td>三角形</td><td>⼀系列单独的三角形</td></tr><tr><td><strong>gl.TRIANGLE_STRIP</strong></td><td>三角带</td><td>⼀系列条带状的三角形</td></tr><tr><td><strong>gl.TRIANGLE_FAN</strong></td><td>三角形</td><td>飘带状三角形</td></tr></tbody></table><p>以下是各图形绘制的示例代码：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">  <span class="number">1.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绘制图形</span></span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">POINTS</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">LINES</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">LINE_STRIP</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">LINE_LOOP</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">LINE_LOOP</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLE_STRIP</span>, <span class="number">0</span>, <span class="number">4</span>)</span><br><span class="line">gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLE_FAN</span>, <span class="number">0</span>, <span class="number">4</span>)</span><br></pre></td></tr></table></figure><h3 id="图形动画"><a href="#图形动画" class="headerlink" title="图形动画"></a>图形动画</h3><p>学习和了解了图形的绘制和顶点着色器位置信息的设置后，借助变量设置的方法，可以对图形设置简单的平移、缩放、旋转等动画效果。</p><p><em><strong>Show Code~</strong></em></p><h4 id="图形平移"><a href="#图形平移" class="headerlink" title="图形平移"></a>图形平移</h4><p>图形平移代码如下：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  attribute float aTranslate;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = vec4(aPosition.x + aTranslate, aPosition.y, aPosition.z, 1.0);</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> aTranslate = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aTranslate&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> x = <span class="number">0</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  x += <span class="number">0.01</span></span><br><span class="line">  <span class="keyword">if</span> (x &gt; <span class="number">1</span>) x = -<span class="number">1</span></span><br><span class="line">  gl.<span class="title function_">vertexAttrib1f</span>(aTranslate, x)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h4 id="图形缩放"><a href="#图形缩放" class="headerlink" title="图形缩放"></a>图形缩放</h4><p>图形缩放代码如下：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  attribute float aSale;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = vec4(aPosition.x * aSale, aPosition.y * aSale, aPosition.z * aSale, 1.0);</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> aSale = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aSale&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> x = <span class="number">1</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  x += <span class="number">0.01</span></span><br><span class="line">  <span class="keyword">if</span> (x &gt; <span class="number">3</span>) x = <span class="number">1</span></span><br><span class="line">  gl.<span class="title function_">vertexAttrib1f</span>(aSale, x)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h4 id="图形旋转"><a href="#图形旋转" class="headerlink" title="图形旋转"></a>图形旋转</h4><p>图形缩放代码如下：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  attribute float deg;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position.x = aPosition.x * cos(deg) - aPosition.y * sin(deg);</span></span><br><span class="line"><span class="string">    gl_Position.y = aPosition.x * sin(deg) + aPosition.y * cos(deg);</span></span><br><span class="line"><span class="string">    gl_Position.z = aPosition.z;</span></span><br><span class="line"><span class="string">    gl_Position.w = aPosition.w;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> deg = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;deg&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> x = <span class="number">1</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  x += <span class="number">0.01</span></span><br><span class="line">  gl.<span class="title function_">vertexAttrib1f</span>(deg, x)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="图形变换矩阵"><a href="#图形变换矩阵" class="headerlink" title="图形变换矩阵"></a>图形变换矩阵</h3><h4 id="回顾矩阵知识"><a href="#回顾矩阵知识" class="headerlink" title="回顾矩阵知识"></a>回顾矩阵知识</h4><p><strong>矩阵</strong>就是纵横排列的数据表格，在接下来 <strong>WebGL</strong> 图形变换的介绍中，矩阵的作用是<strong>把一个点转换到另一个点</strong>。</p><p><strong>行主序与列主序矩阵：</strong></p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184557419.png"></p><p><strong>点的映射公式：</strong></p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184607222.png"></p><ul><li>x’ &#x3D; x * a1 + y * a2 + z * a3  + w * a4</li><li>y’ &#x3D; x * b1 + y * b2 + z * b3  + w * b4</li><li>z’ &#x3D; x * c1 + y * c2 + z * c3  + w * c4</li><li>w’ &#x3D; x * d1 + y * d2 + z * d3  + w * d4</li></ul><p>为什么 <strong>WebGL</strong> 的矩阵采用列主序？参考 <a href="https://blog.csdn.net/byhuang/article/details/1476199">OpenGL 中的转换矩阵</a></p><h4 id="平移矩阵"><a href="#平移矩阵" class="headerlink" title="平移矩阵"></a>平移矩阵</h4><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184633181.png"></p><p>如上图，一点从（x,  y,  z）移动到（x’, y’, z’），对应每个点的坐标的计算公式为：</p><ul><li>x’ &#x3D; x + x1</li><li>y’ &#x3D; y + y1</li><li>z’ &#x3D; z + z1</li></ul><p>如要通过矩阵的运算实现以上坐标的平移计算，则需设法使得：</p><ul><li>x + x1 &#x3D;  x * a1 + y * a2 + z * a3  + w * a4</li><li>y + y1 &#x3D; x * b1 + y * b2 + z * b3  + w * b4</li><li>z + z1 &#x3D; x * c1 + y * c2 + z * c3  + w * c4</li></ul><p>其中 <strong>w</strong> 恒等于 1，所以由以上等式可得出以下结论：</p><ul><li>当 a1 &#x3D; 1,  a2 &#x3D; a3 &#x3D; 0, a4 &#x3D; x1 时，等式  x + x1 &#x3D;  x * a1 + y * a2 + z * a3  + w * a4 成立</li><li>当 b2 &#x3D; 1, b1 &#x3D; b3 &#x3D; 0, b4 &#x3D; y1 时，等式 y + y1 &#x3D; x * b1 + y * b2 + z * b3  + w * b4 成立</li><li>当 c3 &#x3D; 1, c1 &#x3D; c2 &#x3D; 0, c4 &#x3D; z1 时，等式 z + z1 &#x3D; x * c1 + y * c2 + z * c3  + w * c4 成立</li></ul><p>由此可得平移矩阵：</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184708875.png"></p><p>推导出平移矩阵后，之后的图形平移变换，都可以使用平移矩阵来进行操作。代码如下：</p><figure class="highlight js"><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">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  uniform mat4 mat;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = mat * aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> mat = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;mat&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</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> <span class="title function_">getTranslateMatrix</span> = (<span class="params">x = <span class="number">0</span>,y = <span class="number">0</span>,z = <span class="number">0</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">    <span class="number">1.0</span>,<span class="number">0.0</span>,<span class="number">0.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    <span class="number">0.0</span>,<span class="number">1.0</span>,<span class="number">0.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    <span class="number">0.0</span>,<span class="number">0.0</span>,<span class="number">1.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    x  ,y  ,z  , <span class="number">1</span>,</span><br><span class="line">  ])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> x = <span class="number">0</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  x += <span class="number">0.01</span></span><br><span class="line">  <span class="keyword">if</span> (x &gt; <span class="number">1</span>) x = -<span class="number">1</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> matrix = <span class="title function_">getTranslateMatrix</span>(x, x)</span><br><span class="line">  gl.<span class="title function_">uniformMatrix4fv</span>(mat, <span class="literal">false</span>, matrix)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;, <span class="number">10</span>)</span><br></pre></td></tr></table></figure><h4 id="缩放矩阵"><a href="#缩放矩阵" class="headerlink" title="缩放矩阵"></a>缩放矩阵</h4><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184750707.png"></p><p>如上图，图形缩放过程中，一点从（x,  y,  z）缩放到（x’, y’, z’），对应每个点的坐标的计算公式为：</p><ul><li>x’ &#x3D; x * x1</li><li>y’ &#x3D; y * y1</li><li>z’ &#x3D; z * z1</li></ul><p>同理可推导出以下结论：</p><ul><li>当 a1 &#x3D; x1,  a2 &#x3D; a3 &#x3D; a4 &#x3D; 0 时，等式  x * x1 &#x3D;  x * a1 + y * a2 + z * a3  + w * a4 成立</li><li>当 b2 &#x3D; y1, b1 &#x3D; b3 &#x3D; b4 &#x3D; 0 时，等式 y * y1 &#x3D; x * b1 + y * b2 + z * b3  + w * b4 成立</li><li>当 c3 &#x3D; z1, c1 &#x3D; c2 &#x3D; c4 &#x3D; 0 时，等式 z * z1 &#x3D; x * c1 + y * c2 + z * c3  + w * c4 成立</li></ul><p>由此可得缩放矩阵为：（此矩阵为对称矩阵，无需进行行列转换）</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184803882.png"></p><p>使用矩阵进行图形缩放操作的代码如下：</p><figure class="highlight js"><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">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  uniform mat4 mat;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = mat * aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> mat = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;mat&#x27;</span>)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</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> <span class="title function_">getScaleMatrix</span> = (<span class="params">x = <span class="number">1</span>,y = <span class="number">1</span>,z = <span class="number">1</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">    x  ,<span class="number">0.0</span>,<span class="number">0.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    <span class="number">0.0</span>,y  ,<span class="number">0.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    <span class="number">0.0</span>,<span class="number">0.0</span>,z  ,<span class="number">0.0</span>,</span><br><span class="line">    <span class="number">0.0</span>,<span class="number">0.0</span>,<span class="number">0.0</span>, <span class="number">1</span>,</span><br><span class="line">  ])</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">let</span> x = <span class="number">1</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  x += <span class="number">0.01</span></span><br><span class="line">  <span class="keyword">if</span> (x &gt; <span class="number">2</span>) x = <span class="number">0.1</span></span><br><span class="line"> </span><br><span class="line">  <span class="keyword">const</span> matrix = <span class="title function_">getScaleMatrix</span>(x, x)</span><br><span class="line">  gl.<span class="title function_">uniformMatrix4fv</span>(mat, <span class="literal">false</span>, matrix)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;, <span class="number">10</span>)</span><br></pre></td></tr></table></figure><h4 id="旋转矩阵"><a href="#旋转矩阵" class="headerlink" title="旋转矩阵"></a>旋转矩阵</h4><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184839928.png"></p><p>上图中，<strong>A</strong> 点由 (x, y, z) 旋转到 (x’, y’, z’) 的 <strong>A’</strong> 点，旋转角度为 <strong>β</strong>，接下来使用点 A 到原点的距离 <strong>R</strong> 来进行 <strong>A’</strong> 坐标的推导。（R &#x3D; Math.sqrt(x * x + y * y)）</p><p>推导过程如下：</p><figure class="highlight js"><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">// A 点坐标为</span></span><br><span class="line">x = R * <span class="title function_">cos</span>(α)</span><br><span class="line">y = R * <span class="title function_">sin</span>(α)</span><br><span class="line"></span><br><span class="line"><span class="comment">// A&#x27; 点坐标为</span></span><br><span class="line">x<span class="string">&#x27; = R * cos(α + β)</span></span><br><span class="line"><span class="string">   = R * (cos(α)* cos(β) - sin(α) * sin(β))</span></span><br><span class="line"><span class="string">   = R * cos(α)* cos(β) - R * sin(α) * sin(β)</span></span><br><span class="line"><span class="string">y&#x27;</span> = R * <span class="title function_">sin</span>(α + β)</span><br><span class="line">   = R * (<span class="title function_">sin</span>(α)* <span class="title function_">cos</span>(β) + <span class="title function_">cos</span>(α) * <span class="title function_">sin</span>(β))</span><br><span class="line">   = R * <span class="title function_">sin</span>(α)* <span class="title function_">cos</span>(β) + R * <span class="title function_">cos</span>(α) * <span class="title function_">sin</span>(β)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 将 A 点坐标代入到 A&#x27; 点</span></span><br><span class="line">x<span class="string">&#x27; = x * cos(β) - y * sin(β)</span></span><br><span class="line"><span class="string">y&#x27;</span> = y * <span class="title function_">cos</span>(β) + x * <span class="title function_">sin</span>(β)</span><br></pre></td></tr></table></figure><p>由以上 A’ 点坐标公式，结合矩阵乘法公式，可得出以下结论：</p><ul><li>当 a1 &#x3D; cos(β),  a2 &#x3D; -sin(β),  a3 &#x3D; a4 &#x3D; 0 时，等式 x * cos(β) - y * sin(β) &#x3D;  x * a1 + y * a2 + z * a3  + w * a4 成立</li><li>当 b1 &#x3D; sin(β), b2 &#x3D; cos(β),  b3 &#x3D; b4 &#x3D; 0 时，等式 y * cos(β) + x * sin(β) &#x3D; x * b1 + y * b2 + z * b3  + w * b4 成立</li><li>当 c1 &#x3D; c2 &#x3D; c3 &#x3D; c4 &#x3D; 0 时，等式 z &#x3D; x * c1 + y * c2 + z * c3  + w * c4 成立</li></ul><p>由此可得旋转矩阵如下：</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224184921922.png"></p><p>使用矩阵进行图形旋转操作的代码如下：</p><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  uniform mat4 mat;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = mat * aPosition;</span></span><br><span class="line"><span class="string">    gl_PointSize = 15.0;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> mat = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;mat&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绕z轴旋转的旋转矩阵</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">getRotateMatrix</span> = (<span class="params">deg</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">    <span class="title class_">Math</span>.<span class="title function_">cos</span>(deg)  ,<span class="title class_">Math</span>.<span class="title function_">sin</span>(deg) ,<span class="number">0.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    -<span class="title class_">Math</span>.<span class="title function_">sin</span>(deg)  ,<span class="title class_">Math</span>.<span class="title function_">cos</span>(deg) ,<span class="number">0.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    <span class="number">0.0</span>,            <span class="number">0.0</span>,            <span class="number">1.0</span>,<span class="number">0.0</span>,</span><br><span class="line">    <span class="number">0.0</span>,            <span class="number">0.0</span>,            <span class="number">0.0</span>, <span class="number">1</span>,</span><br><span class="line">  ])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> x = <span class="number">0</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  x += <span class="number">0.01</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> matrix = <span class="title function_">getRotateMatrix</span>(x)</span><br><span class="line">  gl.<span class="title function_">uniformMatrix4fv</span>(mat, <span class="literal">false</span>, matrix)</span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;, <span class="number">10</span>)</span><br></pre></td></tr></table></figure><h4 id="组合矩阵"><a href="#组合矩阵" class="headerlink" title="组合矩阵"></a>组合矩阵</h4><p>如果一个图形动画中，既有平移，又有缩放，还有旋转，我们使用矩阵可以轻松的同时实现这些动画。复合动画代码如下：</p><figure class="highlight js"><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><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader, getTranslateMatrix, getScaleMatrix, getRotateMatrix &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  uniform mat4 translateMatrix;</span></span><br><span class="line"><span class="string">  uniform mat4 scaleMatrix;</span></span><br><span class="line"><span class="string">  uniform mat4 rotationMatrix;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = translateMatrix * scaleMatrix * rotationMatrix * aPosition;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> translateMatrix = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;translateMatrix&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> scaleMatrix = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;scaleMatrix&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> rotationMatrix = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;rotationMatrix&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> deg = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">let</span> translateX = -<span class="number">1</span>;</span><br><span class="line"><span class="keyword">let</span> scaleX = <span class="number">0.1</span>;</span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  deg += <span class="number">0.01</span>;</span><br><span class="line">  translateX += <span class="number">0.01</span>;</span><br><span class="line">  scaleX += <span class="number">0.01</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (translateX &gt; <span class="number">1</span>) translateX = -<span class="number">1</span></span><br><span class="line">  <span class="keyword">if</span> (scaleX &gt; <span class="number">2</span>) scaleX = <span class="number">0.1</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> translate = <span class="title function_">getTranslateMatrix</span>(translateX);</span><br><span class="line">  <span class="keyword">const</span> scale = <span class="title function_">getScaleMatrix</span>(scaleX);</span><br><span class="line">  <span class="keyword">const</span> rotate = <span class="title function_">getRotateMatrix</span>(deg);</span><br><span class="line">  <span class="comment">// console.log(translate, scale, rotate)</span></span><br><span class="line">  gl.<span class="title function_">uniformMatrix4fv</span>(translateMatrix, <span class="literal">false</span>, translate);</span><br><span class="line">  gl.<span class="title function_">uniformMatrix4fv</span>(scaleMatrix, <span class="literal">false</span>, scale);</span><br><span class="line">  gl.<span class="title function_">uniformMatrix4fv</span>(rotationMatrix, <span class="literal">false</span>, rotate);</span><br><span class="line"></span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;, <span class="number">10</span>)</span><br></pre></td></tr></table></figure><p>但是，以上代码中需要为每一种动画都创建一个 <code>uniform</code> 变量，然后在程序执行过程中不断改变变量并注入，这种方式很不友好，且不利于代码维护。  </p><p>可以利用矩阵的特性，传入着色器之前，利用矩阵乘法，把多个矩阵混合成一个矩阵传入，在这一个矩阵中包含了所有的动画操作。</p><figure class="highlight js"><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><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initShader, getTranslateMatrix, getScaleMatrix, getRotateMatrix, mixMatrix &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> gl = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;webgl&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">VERTEX_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  attribute vec4 aPosition;</span></span><br><span class="line"><span class="string">  uniform mat4 mat;</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_Position = mat * aPosition;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FRAGMENT_SHADER</span> = <span class="string">`</span></span><br><span class="line"><span class="string">  void main() &#123;</span></span><br><span class="line"><span class="string">    gl_FragColor = vec4(1.0,0.0,0.0,1.0);</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> program = <span class="title function_">initShader</span>(gl, <span class="variable constant_">VERTEX_SHADER</span>, <span class="variable constant_">FRAGMENT_SHADER</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> aPosition = gl.<span class="title function_">getAttribLocation</span>(program, <span class="string">&#x27;aPosition&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> mat = gl.<span class="title function_">getUniformLocation</span>(program, <span class="string">&#x27;mat&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> points = <span class="keyword">new</span> <span class="title class_">Float32Array</span>([</span><br><span class="line">  -<span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.5</span>, -<span class="number">0.5</span>,</span><br><span class="line">  <span class="number">0.0</span>, <span class="number">0.5</span>,</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> buffer = gl.<span class="title function_">createBuffer</span>()</span><br><span class="line">gl.<span class="title function_">bindBuffer</span>(gl.<span class="property">ARRAY_BUFFER</span>, buffer)</span><br><span class="line">gl.<span class="title function_">bufferData</span>(gl.<span class="property">ARRAY_BUFFER</span>, points, gl.<span class="property">STATIC_DRAW</span>)</span><br><span class="line">gl.<span class="title function_">enableVertexAttribArray</span>(aPosition)</span><br><span class="line">gl.<span class="title function_">vertexAttribPointer</span>(aPosition, <span class="number">2</span>, gl.<span class="property">FLOAT</span>, <span class="literal">false</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> deg = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">let</span> translateX = -<span class="number">1</span>;</span><br><span class="line"><span class="keyword">let</span> scaleX = <span class="number">0.1</span>;</span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  deg += <span class="number">0.01</span>;</span><br><span class="line">  translateX += <span class="number">0.01</span>;</span><br><span class="line">  scaleX += <span class="number">0.01</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (translateX &gt; <span class="number">1</span>) translateX = -<span class="number">1</span></span><br><span class="line">  <span class="keyword">if</span> (scaleX &gt; <span class="number">2</span>) scaleX = <span class="number">0.1</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> translate = <span class="title function_">getTranslateMatrix</span>(translateX);</span><br><span class="line">  <span class="keyword">const</span> scale = <span class="title function_">getScaleMatrix</span>(scaleX);</span><br><span class="line">  <span class="keyword">const</span> rotate = <span class="title function_">getRotateMatrix</span>(deg);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 使用混合矩阵方法，将以上三个矩阵混合成一个</span></span><br><span class="line">  <span class="keyword">const</span> matrix = <span class="title function_">mixMatrix</span>(<span class="title function_">mixMatrix</span>(translate, scale), rotate)</span><br><span class="line"></span><br><span class="line">  gl.<span class="title function_">uniformMatrix4fv</span>(mat, <span class="literal">false</span>, matrix);</span><br><span class="line"></span><br><span class="line">  gl.<span class="title function_">drawArrays</span>(gl.<span class="property">TRIANGLES</span>, <span class="number">0</span>, <span class="number">3</span>)</span><br><span class="line">&#125;, <span class="number">10</span>)</span><br></pre></td></tr></table></figure><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>这篇主要把 WebGL 的“最小闭环”跑通：着色器怎么写、数据怎么喂给 GPU、坐标怎么理解、矩阵变换怎么上手。等这些概念有了直觉，后面再学纹理、光照、3D 变换会顺很多。</p><p>如果你发现文中有疏漏&#x2F;错误，欢迎指正（WebGL 这块细节挺多，写起来也容易漏边角）。</p><p><strong>下集预告：</strong></p><ul><li>颜色和纹理</li><li>GLSL &#x2F; OpenGL ES 的更多细节</li><li>WebGL 三维世界</li></ul><p>参考文章：<br><a href="https://coding.imooc.com/class/622.html">https://coding.imooc.com/class/622.html</a><br><a href="https://zhuanlan.zhihu.com/p/570452494">https://zhuanlan.zhihu.com/p/570452494</a><br><a href="https://webglfundamentals.org/webgl/lessons/zh_cn/">https://webglfundamentals.org/webgl/lessons/zh_cn/</a></p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2024/webgl/</id>
    <link href="https://linkdiary.com/2024/webgl/"/>
    <published>2024-09-24T07:23:56.000Z</published>
    <summary>从“画一个点”开始，把 WebGL 的基本概念、着色器、坐标系和矩阵变换串起来，给后面继续学 3D 打个底。</summary>
    <title>WebGL 基础入门篇</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工程化" scheme="https://linkdiary.com/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-chart-line"></i><p>做可视化时经常会卡在第一个问题：到底用 Canvas 还是 SVG？</p><p>我的经验是：别从“哪个更高级”开始比，先从你的需求开始问——图形数量多不多？交互复杂不复杂？需不需要无损缩放？要不要 DOM 事件和 CSS 控制？</p><p>下面就按这些问题，把 Canvas&#x2F;SVG 的差异掰开揉碎聊一遍。</p></div><h2 id="一、技术原理与渲染方式"><a href="#一、技术原理与渲染方式" class="headerlink" title="一、技术原理与渲染方式"></a>一、技术原理与渲染方式</h2><h3 id="Canvas：位图渲染"><a href="#Canvas：位图渲染" class="headerlink" title="Canvas：位图渲染"></a>Canvas：位图渲染</h3><ul><li><strong>原理</strong>：基于像素的位图渲染，通过 JavaScript API 在 2D 上下文上绘制图形。</li><li><strong>渲染流程</strong>：CPU 计算 → GPU 光栅化 → 像素填充 → 屏幕显示。</li><li><strong>特点</strong>：一次性绘制，绘制完成后图形失去”对象”属性，无法单独操作。</li></ul><figure class="highlight javascript"><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="comment">// Canvas 绘制示例</span></span><br><span class="line"><span class="keyword">const</span> canvas = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;canvas&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> ctx = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;2d&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绘制矩形</span></span><br><span class="line">ctx.<span class="property">fillStyle</span> = <span class="string">&#x27;#ff6b6b&#x27;</span>;</span><br><span class="line">ctx.<span class="title function_">fillRect</span>(<span class="number">10</span>, <span class="number">10</span>, <span class="number">100</span>, <span class="number">50</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 绘制圆形</span></span><br><span class="line">ctx.<span class="title function_">beginPath</span>();</span><br><span class="line">ctx.<span class="title function_">arc</span>(<span class="number">200</span>, <span class="number">35</span>, <span class="number">25</span>, <span class="number">0</span>, <span class="number">2</span> * <span class="title class_">Math</span>.<span class="property">PI</span>);</span><br><span class="line">ctx.<span class="property">fillStyle</span> = <span class="string">&#x27;#4ecdc4&#x27;</span>;</span><br><span class="line">ctx.<span class="title function_">fill</span>();</span><br></pre></td></tr></table></figure><h3 id="SVG：矢量图形"><a href="#SVG：矢量图形" class="headerlink" title="SVG：矢量图形"></a>SVG：矢量图形</h3><ul><li><strong>原理</strong>：基于 XML 的矢量图形，通过 DOM 元素描述图形结构。</li><li><strong>渲染流程</strong>：DOM 解析 → 矢量计算 → GPU 渲染 → 屏幕显示。</li><li><strong>特点</strong>：保留图形对象，支持事件绑定、样式修改、动画等。</li></ul><figure class="highlight html"><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="comment">&lt;!-- SVG 绘制示例 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">svg</span> <span class="attr">width</span>=<span class="string">&quot;300&quot;</span> <span class="attr">height</span>=<span class="string">&quot;100&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">rect</span> <span class="attr">x</span>=<span class="string">&quot;10&quot;</span> <span class="attr">y</span>=<span class="string">&quot;10&quot;</span> <span class="attr">width</span>=<span class="string">&quot;100&quot;</span> <span class="attr">height</span>=<span class="string">&quot;50&quot;</span> <span class="attr">fill</span>=<span class="string">&quot;#ff6b6b&quot;</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;200&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;35&quot;</span> <span class="attr">r</span>=<span class="string">&quot;25&quot;</span> <span class="attr">fill</span>=<span class="string">&quot;#4ecdc4&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="二、操作方式与交互能力"><a href="#二、操作方式与交互能力" class="headerlink" title="二、操作方式与交互能力"></a>二、操作方式与交互能力</h2><h3 id="Canvas：命令式操作"><a href="#Canvas：命令式操作" class="headerlink" title="Canvas：命令式操作"></a>Canvas：命令式操作</h3><ul><li><strong>绘制方式</strong>：通过 API 命令绘制，如 <code>fillRect()</code>、<code>strokeText()</code>。</li><li><strong>交互处理</strong>：需要手动计算鼠标位置与图形的关系，实现点击检测。</li><li><strong>动画实现</strong>：通过 <code>requestAnimationFrame</code> 循环重绘实现动画。</li></ul><figure class="highlight javascript"><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">// Canvas 交互检测示例</span></span><br><span class="line">canvas.<span class="title function_">addEventListener</span>(<span class="string">&#x27;click&#x27;</span>, <span class="function">(<span class="params">e</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> rect = canvas.<span class="title function_">getBoundingClientRect</span>();</span><br><span class="line">  <span class="keyword">const</span> x = e.<span class="property">clientX</span> - rect.<span class="property">left</span>;</span><br><span class="line">  <span class="keyword">const</span> y = e.<span class="property">clientY</span> - rect.<span class="property">top</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 手动检测点击区域</span></span><br><span class="line">  <span class="keyword">if</span> (x &gt;= <span class="number">10</span> &amp;&amp; x &lt;= <span class="number">110</span> &amp;&amp; y &gt;= <span class="number">10</span> &amp;&amp; y &lt;= <span class="number">60</span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;点击了矩形&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="SVG：声明式操作"><a href="#SVG：声明式操作" class="headerlink" title="SVG：声明式操作"></a>SVG：声明式操作</h3><ul><li><strong>绘制方式</strong>：通过 XML 标签声明图形结构。</li><li><strong>交互处理</strong>：天然支持事件绑定，每个图形元素都可以独立响应事件。</li><li><strong>动画实现</strong>：支持 CSS 动画、SMIL 动画，或通过 JavaScript 修改属性。</li></ul><figure class="highlight html"><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="comment">&lt;!-- SVG 交互示例 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">svg</span> <span class="attr">width</span>=<span class="string">&quot;300&quot;</span> <span class="attr">height</span>=<span class="string">&quot;100&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">rect</span> <span class="attr">x</span>=<span class="string">&quot;10&quot;</span> <span class="attr">y</span>=<span class="string">&quot;10&quot;</span> <span class="attr">width</span>=<span class="string">&quot;100&quot;</span> <span class="attr">height</span>=<span class="string">&quot;50&quot;</span> <span class="attr">fill</span>=<span class="string">&quot;#ff6b6b&quot;</span> </span></span><br><span class="line"><span class="tag">        <span class="attr">onclick</span>=<span class="string">&quot;handleClick()&quot;</span> </span></span><br><span class="line"><span class="tag">        <span class="attr">onmouseover</span>=<span class="string">&quot;this.style.fill=&#x27;#ff8e8e&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">onmouseout</span>=<span class="string">&quot;this.style.fill=&#x27;#ff6b6b&#x27;&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="三、内存消耗与性能表现"><a href="#三、内存消耗与性能表现" class="headerlink" title="三、内存消耗与性能表现"></a>三、内存消耗与性能表现</h2><h3 id="Canvas：内存友好"><a href="#Canvas：内存友好" class="headerlink" title="Canvas：内存友好"></a>Canvas：内存友好</h3><ul><li><strong>内存占用</strong>：固定内存占用，与图形复杂度无关，只与画布尺寸相关。</li><li><strong>大数据量</strong>：适合处理大量图形（数万到数十万个），内存占用稳定。</li><li><strong>垃圾回收</strong>：绘制完成后对象可被回收，内存压力小。</li></ul><figure class="highlight javascript"><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="comment">// Canvas 大数据量绘制</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">drawManyCircles</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="number">10000</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">const</span> x = <span class="title class_">Math</span>.<span class="title function_">random</span>() * canvas.<span class="property">width</span>;</span><br><span class="line">    <span class="keyword">const</span> y = <span class="title class_">Math</span>.<span class="title function_">random</span>() * canvas.<span class="property">height</span>;</span><br><span class="line">    <span class="keyword">const</span> r = <span class="title class_">Math</span>.<span class="title function_">random</span>() * <span class="number">5</span> + <span class="number">1</span>;</span><br><span class="line">    </span><br><span class="line">    ctx.<span class="title function_">beginPath</span>();</span><br><span class="line">    ctx.<span class="title function_">arc</span>(x, y, r, <span class="number">0</span>, <span class="number">2</span> * <span class="title class_">Math</span>.<span class="property">PI</span>);</span><br><span class="line">    ctx.<span class="property">fillStyle</span> = <span class="string">`hsl(<span class="subst">$&#123;<span class="built_in">Math</span>.random() * <span class="number">360</span>&#125;</span>, 70%, 50%)`</span>;</span><br><span class="line">    ctx.<span class="title function_">fill</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="SVG：DOM-开销"><a href="#SVG：DOM-开销" class="headerlink" title="SVG：DOM 开销"></a>SVG：DOM 开销</h3><ul><li><strong>内存占用</strong>：每个图形对应一个 DOM 节点，内存占用与图形数量成正比。</li><li><strong>大数据量</strong>：当图形数量超过数千个时，DOM 操作性能急剧下降。</li><li><strong>垃圾回收</strong>：DOM 节点需要手动清理，否则容易造成内存泄漏。</li></ul><figure class="highlight javascript"><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="comment">// SVG 大数据量问题示例</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">createManySVGCircles</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> svg = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;svg&#x27;</span>);</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++) &#123; <span class="comment">// 超过1000个性能开始下降</span></span><br><span class="line">    <span class="keyword">const</span> circle = <span class="variable language_">document</span>.<span class="title function_">createElementNS</span>(<span class="string">&#x27;http://www.w3.org/2000/svg&#x27;</span>, <span class="string">&#x27;circle&#x27;</span>);</span><br><span class="line">    circle.<span class="title function_">setAttribute</span>(<span class="string">&#x27;cx&#x27;</span>, <span class="title class_">Math</span>.<span class="title function_">random</span>() * <span class="number">300</span>);</span><br><span class="line">    circle.<span class="title function_">setAttribute</span>(<span class="string">&#x27;cy&#x27;</span>, <span class="title class_">Math</span>.<span class="title function_">random</span>() * <span class="number">100</span>);</span><br><span class="line">    circle.<span class="title function_">setAttribute</span>(<span class="string">&#x27;r&#x27;</span>, <span class="title class_">Math</span>.<span class="title function_">random</span>() * <span class="number">5</span> + <span class="number">1</span>);</span><br><span class="line">    circle.<span class="title function_">setAttribute</span>(<span class="string">&#x27;fill&#x27;</span>, <span class="string">`hsl(<span class="subst">$&#123;<span class="built_in">Math</span>.random() * <span class="number">360</span>&#125;</span>, 70%, 50%)`</span>);</span><br><span class="line">    svg.<span class="title function_">appendChild</span>(circle);</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>四、响应时间与渲染性能</h2><h3 id="Canvas：渲染速度快"><a href="#Canvas：渲染速度快" class="headerlink" title="Canvas：渲染速度快"></a>Canvas：渲染速度快</h3><ul><li><strong>初始渲染</strong>：一次性绘制，渲染速度快。</li><li><strong>更新性能</strong>：局部更新需要重绘整个画布或使用离屏 Canvas。</li><li><strong>缩放性能</strong>：缩放时性能稳定，但可能出现像素化。</li></ul><figure class="highlight javascript"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Canvas 高性能更新策略</span></span><br><span class="line"><span class="keyword">const</span> offscreenCanvas = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&#x27;canvas&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> offscreenCtx = offscreenCanvas.<span class="title function_">getContext</span>(<span class="string">&#x27;2d&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在离屏 Canvas 上绘制</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">drawToOffscreen</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <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="keyword">function</span> <span class="title function_">updateDisplay</span>(<span class="params"></span>) &#123;</span><br><span class="line">  ctx.<span class="title function_">clearRect</span>(<span class="number">0</span>, <span class="number">0</span>, canvas.<span class="property">width</span>, canvas.<span class="property">height</span>);</span><br><span class="line">  ctx.<span class="title function_">drawImage</span>(offscreenCanvas, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="SVG：DOM-操作慢"><a href="#SVG：DOM-操作慢" class="headerlink" title="SVG：DOM 操作慢"></a>SVG：DOM 操作慢</h3><ul><li><strong>初始渲染</strong>：DOM 解析和渲染相对较慢。</li><li><strong>更新性能</strong>：修改属性时触发重排重绘，性能较差。</li><li><strong>缩放性能</strong>：矢量缩放性能优秀，无像素化问题。</li></ul><h2 id="五、事件处理与交互体验"><a href="#五、事件处理与交互体验" class="headerlink" title="五、事件处理与交互体验"></a>五、事件处理与交互体验</h2><h3 id="Canvas：手动事件处理"><a href="#Canvas：手动事件处理" class="headerlink" title="Canvas：手动事件处理"></a>Canvas：手动事件处理</h3><ul><li><strong>事件检测</strong>：需要手动实现点击、悬停等事件检测。</li><li><strong>交互复杂度</strong>：复杂交互需要大量计算，但可以实现更精细的控制。</li><li><strong>事件委托</strong>：所有事件都绑定在 Canvas 元素上，通过坐标计算分发。</li></ul><figure class="highlight javascript"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Canvas 复杂交互示例</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CanvasInteraction</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">canvas</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">canvas</span> = canvas;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">shapes</span> = [];</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">selectedShape</span> = <span class="literal">null</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">canvas</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;mousedown&#x27;</span>, <span class="variable language_">this</span>.<span class="property">handleMouseDown</span>.<span class="title function_">bind</span>(<span class="variable language_">this</span>));</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">canvas</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;mousemove&#x27;</span>, <span class="variable language_">this</span>.<span class="property">handleMouseMove</span>.<span class="title function_">bind</span>(<span class="variable language_">this</span>));</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">canvas</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;mouseup&#x27;</span>, <span class="variable language_">this</span>.<span class="property">handleMouseUp</span>.<span class="title function_">bind</span>(<span class="variable language_">this</span>));</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="title function_">handleMouseDown</span>(<span class="params">e</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> pos = <span class="variable language_">this</span>.<span class="title function_">getMousePos</span>(e);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">selectedShape</span> = <span class="variable language_">this</span>.<span class="title function_">findShapeAt</span>(pos);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="title function_">findShapeAt</span>(<span class="params">pos</span>) &#123;</span><br><span class="line">    <span class="comment">// 手动检测图形位置</span></span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">shapes</span>.<span class="title function_">find</span>(<span class="function"><span class="params">shape</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> pos.<span class="property">x</span> &gt;= shape.<span class="property">x</span> &amp;&amp; pos.<span class="property">x</span> &lt;= shape.<span class="property">x</span> + shape.<span class="property">width</span> &amp;&amp;</span><br><span class="line">             pos.<span class="property">y</span> &gt;= shape.<span class="property">y</span> &amp;&amp; pos.<span class="property">y</span> &lt;= shape.<span class="property">y</span> + shape.<span class="property">height</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><h3 id="SVG：原生事件支持"><a href="#SVG：原生事件支持" class="headerlink" title="SVG：原生事件支持"></a>SVG：原生事件支持</h3><ul><li><strong>事件绑定</strong>：每个图形元素都可以直接绑定事件。</li><li><strong>事件冒泡</strong>：支持标准 DOM 事件冒泡机制。</li><li><strong>交互简单</strong>：简单交互实现容易，但复杂交互可能受 DOM 性能限制。</li></ul><figure class="highlight html"><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="comment">&lt;!-- SVG 原生事件支持 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">svg</span> <span class="attr">width</span>=<span class="string">&quot;300&quot;</span> <span class="attr">height</span>=<span class="string">&quot;200&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;chart&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">rect</span> <span class="attr">x</span>=<span class="string">&quot;10&quot;</span> <span class="attr">y</span>=<span class="string">&quot;10&quot;</span> <span class="attr">width</span>=<span class="string">&quot;50&quot;</span> <span class="attr">height</span>=<span class="string">&quot;30&quot;</span> <span class="attr">fill</span>=<span class="string">&quot;blue&quot;</span> </span></span><br><span class="line"><span class="tag">          <span class="attr">onclick</span>=<span class="string">&quot;selectBar(this)&quot;</span> </span></span><br><span class="line"><span class="tag">          <span class="attr">onmouseover</span>=<span class="string">&quot;highlight(this)&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">onmouseout</span>=<span class="string">&quot;unhighlight(this)&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">rect</span> <span class="attr">x</span>=<span class="string">&quot;70&quot;</span> <span class="attr">y</span>=<span class="string">&quot;20&quot;</span> <span class="attr">width</span>=<span class="string">&quot;50&quot;</span> <span class="attr">height</span>=<span class="string">&quot;20&quot;</span> <span class="attr">fill</span>=<span class="string">&quot;red&quot;</span> </span></span><br><span class="line"><span class="tag">          <span class="attr">onclick</span>=<span class="string">&quot;selectBar(this)&quot;</span> </span></span><br><span class="line"><span class="tag">          <span class="attr">onmouseover</span>=<span class="string">&quot;highlight(this)&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">onmouseout</span>=<span class="string">&quot;unhighlight(this)&quot;</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">g</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="六、缩放与分辨率适配"><a href="#六、缩放与分辨率适配" class="headerlink" title="六、缩放与分辨率适配"></a>六、缩放与分辨率适配</h2><h3 id="Canvas：像素化问题"><a href="#Canvas：像素化问题" class="headerlink" title="Canvas：像素化问题"></a>Canvas：像素化问题</h3><ul><li><strong>高 DPI 支持</strong>：需要手动处理设备像素比，否则在高分辨率屏幕上模糊。</li><li><strong>缩放质量</strong>：放大时会出现像素化，影响视觉效果。</li></ul><figure class="highlight javascript"><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="comment">// Canvas 高 DPI 支持</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">setupHighDPICanvas</span>(<span class="params">canvas</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> ctx = canvas.<span class="title function_">getContext</span>(<span class="string">&#x27;2d&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> dpr = <span class="variable language_">window</span>.<span class="property">devicePixelRatio</span> || <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">const</span> rect = canvas.<span class="title function_">getBoundingClientRect</span>();</span><br><span class="line">  </span><br><span class="line">  canvas.<span class="property">width</span> = rect.<span class="property">width</span> * dpr;</span><br><span class="line">  canvas.<span class="property">height</span> = rect.<span class="property">height</span> * dpr;</span><br><span class="line">  ctx.<span class="title function_">scale</span>(dpr, dpr);</span><br><span class="line">  </span><br><span class="line">  canvas.<span class="property">style</span>.<span class="property">width</span> = rect.<span class="property">width</span> + <span class="string">&#x27;px&#x27;</span>;</span><br><span class="line">  canvas.<span class="property">style</span>.<span class="property">height</span> = rect.<span class="property">height</span> + <span class="string">&#x27;px&#x27;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="SVG：完美缩放"><a href="#SVG：完美缩放" class="headerlink" title="SVG：完美缩放"></a>SVG：完美缩放</h3><ul><li><strong>矢量特性</strong>：任意缩放都保持清晰，无像素化问题。</li><li><strong>响应式</strong>：天然支持响应式设计，配合 CSS 媒体查询效果更佳。</li></ul><figure class="highlight html"><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">&lt;!-- SVG 响应式示例 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">svg</span> <span class="attr">viewBox</span>=<span class="string">&quot;0 0 300 200&quot;</span> <span class="attr">preserveAspectRatio</span>=<span class="string">&quot;xMidYMid meet&quot;</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- 图形内容 --&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="七、为什么主流可视化库选择-Canvas？"><a href="#七、为什么主流可视化库选择-Canvas？" class="headerlink" title="七、为什么主流可视化库选择 Canvas？"></a>七、为什么主流可视化库选择 Canvas？</h2><h3 id="1-性能优势"><a href="#1-性能优势" class="headerlink" title="1. 性能优势"></a>1. 性能优势</h3><ul><li><strong>大数据量处理</strong>：现代数据可视化经常需要处理数万甚至数十万个数据点，Canvas 的内存占用和渲染性能明显优于 SVG。</li><li><strong>实时更新</strong>：数据可视化需要频繁更新，Canvas 的重绘性能更适合实时场景。</li></ul><h3 id="2-技术生态"><a href="#2-技术生态" class="headerlink" title="2. 技术生态"></a>2. 技术生态</h3><ul><li><strong>WebGL 支持</strong>：Canvas 可以切到 WebGL 上下文，做 3D 或 GPU 加速渲染（比如 three.js 这类就是直接跑 WebGL）。</li><li><strong>工程可控</strong>：如果你把渲染和交互都做成“自己的对象模型”，Canvas 会更自然；SVG 走 DOM，则更依赖浏览器对 DOM 的性能表现。</li></ul><h3 id="3-跨平台一致性"><a href="#3-跨平台一致性" class="headerlink" title="3. 跨平台一致性"></a>3. 跨平台一致性</h3><ul><li><strong>渲染一致性</strong>：Canvas 在不同平台和浏览器上的渲染结果更一致。</li><li><strong>性能可预测</strong>：Canvas 的性能表现更可预测，便于优化。</li></ul><h3 id="4-开发效率"><a href="#4-开发效率" class="headerlink" title="4. 开发效率"></a>4. 开发效率</h3><ul><li><strong>批量操作</strong>：Canvas 支持批量绘制操作，减少 API 调用次数。</li><li><strong>状态管理</strong>：Canvas 的状态管理更简单，不需要维护复杂的 DOM 树。</li></ul><h2 id="八、技术选型建议"><a href="#八、技术选型建议" class="headerlink" title="八、技术选型建议"></a>八、技术选型建议</h2><h3 id="选择-Canvas-的场景"><a href="#选择-Canvas-的场景" class="headerlink" title="选择 Canvas 的场景"></a>选择 Canvas 的场景</h3><ul><li>大数据量可视化（&gt;1000 个图形元素）</li><li>需要频繁更新的实时图表</li><li>复杂的交互和动画效果</li><li>3D 或 WebGL 渲染需求</li><li>性能要求较高的场景</li></ul><h3 id="选择-SVG-的场景"><a href="#选择-SVG-的场景" class="headerlink" title="选择 SVG 的场景"></a>选择 SVG 的场景</h3><ul><li>小到中等数据量的静态图表</li><li>需要精确的矢量缩放</li><li>简单的交互需求</li><li>需要 SEO 友好的场景</li><li>需要 CSS 样式控制的场景</li></ul><h2 id="九、技术对比总结"><a href="#九、技术对比总结" class="headerlink" title="九、技术对比总结"></a>九、技术对比总结</h2><table><thead><tr><th>特性</th><th>Canvas</th><th>SVG</th></tr></thead><tbody><tr><td><strong>渲染方式</strong></td><td>位图渲染，像素级操作</td><td>矢量图形，DOM 元素</td></tr><tr><td><strong>内存占用</strong></td><td>固定，与图形数量无关</td><td>与图形数量成正比</td></tr><tr><td><strong>大数据量性能</strong></td><td>优秀，适合数万个元素</td><td>较差，超过千个性能下降</td></tr><tr><td><strong>交互处理</strong></td><td>需要手动实现事件检测</td><td>原生支持 DOM 事件</td></tr><tr><td><strong>动画性能</strong></td><td>优秀，适合复杂动画</td><td>一般，DOM 操作开销大</td></tr><tr><td><strong>缩放质量</strong></td><td>可能出现像素化</td><td>完美缩放，无像素化</td></tr><tr><td><strong>开发复杂度</strong></td><td>较高，需要手动处理细节</td><td>较低，声明式语法</td></tr><tr><td><strong>SEO 友好性</strong></td><td>差，无法被搜索引擎解析</td><td>好，XML 结构可被解析</td></tr><tr><td><strong>CSS 样式支持</strong></td><td>不支持</td><td>完全支持</td></tr><tr><td><strong>3D 渲染能力</strong></td><td>支持 WebGL</td><td>不支持</td></tr><tr><td><strong>实时更新性能</strong></td><td>优秀</td><td>一般</td></tr><tr><td><strong>文件大小</strong></td><td>小，纯 JavaScript</td><td>较大，XML 结构</td></tr></tbody></table><h2 id="十、结语"><a href="#十、结语" class="headerlink" title="十、结语"></a>十、结语</h2><p>最后给一个更“工程化”的总结：</p><ul><li>如果你需要画很多点&#x2F;很多图形，并且要频繁更新（实时图表、拖拽缩放、动画），优先 Canvas&#x2F;WebGL。</li><li>如果你的图形数量不大，但你想要“每个元素都是 DOM”，方便绑事件、改样式、做无损缩放，那 SVG 会更舒服。</li></ul><p>另外，混用也很常见：大背景&#x2F;大数据量用 Canvas，少量需要精确交互的元素用 SVG&#x2F;DOM 做覆盖层。</p><p><strong>Happy Visualizing!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2024/canvas-svg/</id>
    <link href="https://linkdiary.com/2024/canvas-svg/"/>
    <published>2024-08-08T09:41:35.000Z</published>
    <summary>Canvas 和 SVG 到底怎么选？这篇从“渲染模型 + 交互方式 + 性能边界”三个角度把取舍讲清楚，顺便给一套可直接用的选型规则。</summary>
    <title>前端可视化技术 Canvas vs SVG</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工程化" scheme="https://linkdiary.com/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-shield-alt"></i><p>权限设计最常见的翻车姿势有两种：</p><ol><li>前端只做“菜单隐藏”，接口没做鉴权（安全漏洞）  </li><li>后端鉴权做了，但前端没有体验层的控制（用户到处撞 403）</li></ol><p>这篇就按“模型 → 同步 → 前端落点 → 后端兜底”的顺序，把常见实现拆开说：路由守卫、按钮&#x2F;组件权限、动态路由、接口权限控制，各自适合解决什么问题。</p></div><h2 id="一、先立模型：资源、动作与主体"><a href="#一、先立模型：资源、动作与主体" class="headerlink" title="一、先立模型：资源、动作与主体"></a>一、先立模型：资源、动作与主体</h2><ul><li><strong>主体（Subject）</strong>：用户、角色、组织、租户等。</li><li><strong>资源（Resource）</strong>：页面、菜单、按钮、接口、文件等。</li><li><strong>动作（Action）</strong>：view&#x2F;create&#x2F;update&#x2F;delete&#x2F;export…</li><li><strong>常见模型</strong>：<ul><li><strong>RBAC</strong>：角色-权限映射，简单高效；粒度通常到菜单&#x2F;按钮&#x2F;接口。</li><li><strong>ABAC</strong>：基于属性的策略更灵活（时间段、地理、部门级别等），实现与维护更复杂。</li></ul></li></ul><p>建议以 RBAC 起步，保留扩展位点（在权限项中附带资源&#x2F;动作&#x2F;条件）。前端更多负责“展示与导航层过滤”，真正的安全边界一定在后端（别指望前端挡住恶意请求）。</p><h2 id="二、路由权限控制（Route-Guard）"><a href="#二、路由权限控制（Route-Guard）" class="headerlink" title="二、路由权限控制（Route Guard）"></a>二、路由权限控制（Route Guard）</h2><p>— 页面级拦截入口。</p><ul><li>优点：统一把关，能有效阻止未授权页面访问；配合路由元信息，规则可读性强。</li><li>局限：必须与后端权限同步，单靠前端不安全；需为每个受限路由显式声明。</li></ul><h3 id="1）Vue-Router-示例"><a href="#1）Vue-Router-示例" class="headerlink" title="1）Vue Router 示例"></a>1）Vue Router 示例</h3><figure class="highlight ts"><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 class="comment">// router/permission.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; router &#125; <span class="keyword">from</span> <span class="string">&#x27;./router&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useAuthStore &#125; <span class="keyword">from</span> <span class="string">&#x27;@/stores/auth&#x27;</span></span><br><span class="line"></span><br><span class="line">router.<span class="title function_">beforeEach</span>(<span class="function">(<span class="params">to, _from, next</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> auth = <span class="title function_">useAuthStore</span>()</span><br><span class="line">  <span class="keyword">const</span> requiresAuth = <span class="title class_">Boolean</span>(to.<span class="property">meta</span>?.<span class="property">requiresAuth</span>)</span><br><span class="line">  <span class="keyword">if</span> (!requiresAuth) <span class="keyword">return</span> <span class="title function_">next</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!auth.<span class="property">user</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">next</span>(&#123; <span class="attr">name</span>: <span class="string">&#x27;login&#x27;</span>, <span class="attr">query</span>: &#123; <span class="attr">redirect</span>: to.<span class="property">fullPath</span> &#125; &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> needRoles = (to.<span class="property">meta</span>?.<span class="property">roles</span> <span class="keyword">as</span> <span class="built_in">string</span>[] | <span class="literal">undefined</span>) ?? []</span><br><span class="line">  <span class="keyword">if</span> (needRoles.<span class="property">length</span> &amp;&amp; !needRoles.<span class="title function_">some</span>(<span class="function"><span class="params">r</span> =&gt;</span> auth.<span class="property">user</span>!.<span class="property">roles</span>.<span class="title function_">includes</span>(r))) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">next</span>(&#123; <span class="attr">name</span>: <span class="string">&#x27;403&#x27;</span> &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">next</span>()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>路由元信息示例：</p><figure class="highlight ts"><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">&#123;</span><br><span class="line">  <span class="attr">path</span>: <span class="string">&#x27;/admin&#x27;</span>,</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&#x27;admin&#x27;</span>,</span><br><span class="line">  <span class="attr">component</span>: <span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;@/pages/Admin.vue&#x27;</span>),</span><br><span class="line">  <span class="attr">meta</span>: &#123; <span class="attr">requiresAuth</span>: <span class="literal">true</span>, <span class="attr">roles</span>: [<span class="string">&#x27;admin&#x27;</span>] &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2）React-Router-示例"><a href="#2）React-Router-示例" class="headerlink" title="2）React Router 示例"></a>2）React Router 示例</h3><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">import</span> &#123; <span class="title class_">Navigate</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router-dom&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useAuth &#125; <span class="keyword">from</span> <span class="string">&#x27;./auth&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">Guard</span>(<span class="params">&#123; roles, children &#125;: &#123; roles?: <span class="built_in">string</span>[]; children: React.ReactNode &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; user &#125; = <span class="title function_">useAuth</span>()</span><br><span class="line">  <span class="keyword">if</span> (!user) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">&quot;/login&quot;</span> <span class="attr">replace</span> /&gt;</span></span></span><br><span class="line">  <span class="keyword">if</span> (roles &amp;&amp; !roles.<span class="title function_">some</span>(<span class="function"><span class="params">r</span> =&gt;</span> user.<span class="property">roles</span>.<span class="title function_">includes</span>(r))) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">&quot;/403&quot;</span> <span class="attr">replace</span> /&gt;</span></span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;&gt;</span>&#123;children&#125;<span class="tag">&lt;/&gt;</span></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="comment">// &lt;Route path=&quot;/admin&quot; element=&#123;&lt;Guard roles=&#123;[&#x27;admin&#x27;]&#125;&gt;&lt;Admin /&gt;&lt;/Guard&gt;&#125; /&gt;</span></span><br></pre></td></tr></table></figure><p>要点：路由层做“页面级”拦截，所有受限页面都需声明 <code>meta.roles</code> 或通过包装组件传入 <code>roles</code>。</p><h2 id="三、按钮-组件权限控制（Fine-grained-UI）"><a href="#三、按钮-组件权限控制（Fine-grained-UI）" class="headerlink" title="三、按钮&#x2F;组件权限控制（Fine-grained UI）"></a>三、按钮&#x2F;组件权限控制（Fine-grained UI）</h2><p>— 细粒度的页面内显隐&#x2F;禁用。</p><ul><li>优点：就地控制，用户体验最佳；权限变化可即时反映到交互元素。</li><li>局限：需要在多个位置加判断，开发与维护成本较高；仅影响可见性，不提供真正的安全保证。</li></ul><h3 id="1）Vue-自定义指令"><a href="#1）Vue-自定义指令" class="headerlink" title="1）Vue 自定义指令"></a>1）Vue 自定义指令</h3><figure class="highlight ts"><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="comment">// directives/permission.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; useAuthStore &#125; <span class="keyword">from</span> <span class="string">&#x27;@/stores/auth&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="title function_">mounted</span>(<span class="params"><span class="attr">el</span>: <span class="title class_">HTMLElement</span>, <span class="attr">binding</span>: &#123; value: <span class="built_in">string</span> | <span class="built_in">string</span>[] &#125;</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> auth = <span class="title function_">useAuthStore</span>()</span><br><span class="line">    <span class="keyword">const</span> need = ([] <span class="keyword">as</span> <span class="built_in">string</span>[]).<span class="title function_">concat</span>(binding.<span class="property">value</span> || [])</span><br><span class="line">    <span class="keyword">const</span> has = need.<span class="title function_">some</span>(<span class="function"><span class="params">code</span> =&gt;</span> auth.<span class="property">perms</span>.<span class="title function_">includes</span>(code))</span><br><span class="line">    <span class="keyword">if</span> (!has) el.<span class="property">parentNode</span>?.<span class="title function_">removeChild</span>(el)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// &lt;button v-permission=&quot;&#x27;user:delete&#x27;&quot;&gt;删除用户&lt;/button&gt;</span></span><br></pre></td></tr></table></figure><h3 id="2）React-组件封装"><a href="#2）React-组件封装" class="headerlink" title="2）React 组件封装"></a>2）React 组件封装</h3><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">import</span> &#123; useAuth &#125; <span class="keyword">from</span> <span class="string">&#x27;./auth&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">Can</span>(<span class="params">&#123; perm, children &#125;: &#123; perm: <span class="built_in">string</span>; children: React.ReactNode &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; perms &#125; = <span class="title function_">useAuth</span>()</span><br><span class="line">  <span class="keyword">if</span> (!perms?.<span class="title function_">includes</span>(perm)) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;&gt;</span>&#123;children&#125;<span class="tag">&lt;/&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// &lt;Can perm=&quot;user:delete&quot;&gt;&lt;Button danger&gt;删除&lt;/Button&gt;&lt;/Can&gt;</span></span><br></pre></td></tr></table></figure><p>要点：UI 层只负责“显隐&#x2F;禁用”，不要在前端做安全假设；真正的权限校验在后端接口。</p><h2 id="四、动态路由加载（Login-后按权限注入）"><a href="#四、动态路由加载（Login-后按权限注入）" class="headerlink" title="四、动态路由加载（Login 后按权限注入）"></a>四、动态路由加载（Login 后按权限注入）</h2><p>— 登录后按权限注入路由与菜单。</p><ul><li>优点：减少无权限页面的代码与加载时间；与菜单&#x2F;导航天然统一。</li><li>局限：实现与状态同步更复杂，需要 404&#x2F;403 兜底与缓存恢复策略。</li></ul><h3 id="1）Vue-动态注入"><a href="#1）Vue-动态注入" class="headerlink" title="1）Vue 动态注入"></a>1）Vue 动态注入</h3><figure class="highlight ts"><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="comment">// 登录成功后</span></span><br><span class="line"><span class="keyword">const</span> &#123; <span class="attr">data</span>: serverRoutes &#125; = <span class="keyword">await</span> api.<span class="title function_">get</span>(<span class="string">&#x27;/me/routes&#x27;</span>)</span><br><span class="line">serverRoutes.<span class="title function_">forEach</span>(<span class="function">(<span class="params"><span class="attr">r</span>: <span class="built_in">any</span></span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 可按父子关系选择 addRoute(parentName, route)</span></span><br><span class="line">  router.<span class="title function_">addRoute</span>(<span class="title function_">mapToVueRoute</span>(r))</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="2）React-Router-构建路由表"><a href="#2）React-Router-构建路由表" class="headerlink" title="2）React Router 构建路由表"></a>2）React Router 构建路由表</h3><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">import</span> &#123; useMemo &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useRoutes &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router-dom&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">AppRoutes</span>(<span class="params">&#123; rawRoutes &#125;: &#123; rawRoutes: <span class="built_in">any</span>[] &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> routes = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> <span class="title function_">buildRoutes</span>(rawRoutes), [rawRoutes])</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">useRoutes</span>(routes)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要点：动态路由可减少无权限页面的打包体积与加载时间，但需要配合菜单构建、缓存恢复与 404&#x2F;403 兜底处理。</p><h2 id="五、接口权限控制（后端兜底、安全边界）"><a href="#五、接口权限控制（后端兜底、安全边界）" class="headerlink" title="五、接口权限控制（后端兜底、安全边界）"></a>五、接口权限控制（后端兜底、安全边界）</h2><p>— 真正的安全边界在后端。</p><ul><li>优点：最安全、可审计、可细化到资源实例级；防止接口被恶意调用。</li><li>局限：前端无法替代，需后端网关&#x2F;服务配合与策略下发。</li></ul><p>前端在请求中携带凭证（Cookie&#x2F;JWT&#x2F;Bearer token），后端网关&#x2F;服务侧做鉴权与鉴定；前端只负责处理 401&#x2F;403 与跳转。</p><figure class="highlight ts"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Axios 拦截器示例</span></span><br><span class="line">axios.<span class="property">interceptors</span>.<span class="property">request</span>.<span class="title function_">use</span>(<span class="function">(<span class="params">cfg</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> token = <span class="title function_">getToken</span>()</span><br><span class="line">  <span class="keyword">if</span> (token) cfg.<span class="property">headers</span>.<span class="property">Authorization</span> = <span class="string">`Bearer <span class="subst">$&#123;token&#125;</span>`</span></span><br><span class="line">  <span class="keyword">return</span> cfg</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">axios.<span class="property">interceptors</span>.<span class="property">response</span>.<span class="title function_">use</span>(<span class="literal">undefined</span>, <span class="function">(<span class="params">err</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (err.<span class="property">response</span>?.<span class="property">status</span> === <span class="number">401</span>) &#123;</span><br><span class="line">    <span class="title function_">logout</span>()</span><br><span class="line">    location.<span class="title function_">assign</span>(<span class="string">`/login?redirect=<span class="subst">$&#123;<span class="built_in">encodeURIComponent</span>(location.pathname + location.search)&#125;</span>`</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(err)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>后端建议：</p><ul><li>在 JWT 的 claims 中放入 <code>roles</code>&#x2F;<code>perms</code>&#x2F;<code>tenant</code> 等只读信息；关键写操作仍以服务端 ACL&#x2F;Policy 为准。</li><li>对敏感接口做二次校验（如资源拥有者校验、操作幂等签名、防重放）。</li></ul><h2 id="六、权限数据同步与一致性"><a href="#六、权限数据同步与一致性" class="headerlink" title="六、权限数据同步与一致性"></a>六、权限数据同步与一致性</h2><ul><li>登录后获取 <code>userInfo + roles + perms + menus/routes</code>，并缓存到 Store；支持刷新恢复。</li><li>变更时（角色变更、强制下线）通过推送&#x2F;轮询刷新权限。</li><li>前端每次进入受限页面二次校验（例如检查 <code>token</code> 是否过期、角色是否仍然匹配）。</li></ul><h2 id="七、统一的权限元信息约定"><a href="#七、统一的权限元信息约定" class="headerlink" title="七、统一的权限元信息约定"></a>七、统一的权限元信息约定</h2><p>建议在路由&#x2F;菜单项中统一描述权限：</p><figure class="highlight ts"><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_">Meta</span> = &#123;</span><br><span class="line">  <span class="attr">requiresAuth</span>?: <span class="built_in">boolean</span></span><br><span class="line">  <span class="attr">roles</span>?: <span class="built_in">string</span>[]</span><br><span class="line">  <span class="attr">perms</span>?: <span class="built_in">string</span>[] <span class="comment">// 细粒度按钮/接口能力声明（用于 UI 展示）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="highlight ts"><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">&#123;</span><br><span class="line">  <span class="attr">path</span>: <span class="string">&#x27;/user/list&#x27;</span>,</span><br><span class="line">  <span class="attr">meta</span>: &#123; <span class="attr">requiresAuth</span>: <span class="literal">true</span>, <span class="attr">roles</span>: [<span class="string">&#x27;admin&#x27;</span>,<span class="string">&#x27;ops&#x27;</span>], <span class="attr">perms</span>: [<span class="string">&#x27;user:list&#x27;</span>,<span class="string">&#x27;user:export&#x27;</span>] &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="八、总结"><a href="#八、总结" class="headerlink" title="八、总结"></a>八、总结</h2><ul><li>页面进入前由“路由守卫”兜底；页面内用“按钮&#x2F;组件权限”做细粒度体验优化；配合“动态路由”减少无关代码；真正的安全由“接口权限控制”在后端完成。</li><li>前端权限只负责“看得见&#x2F;点得着”，不可替代后端鉴权；两端需共享统一的权限模型与数据。</li></ul><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2024/fe-auth/</id>
    <link href="https://linkdiary.com/2024/fe-auth/"/>
    <published>2024-08-08T09:41:35.000Z</published>
    <summary>权限这块最怕“前端只隐藏菜单”。这篇从 RBAC/ABAC 到路由/按钮/动态路由/接口鉴权，把前端能做的体验优化和后端必须做的安全边界讲清楚。</summary>
    <title>前端如何进行权限设计</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="工程化" scheme="https://linkdiary.com/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-check-circle"></i><p>你有没有遇到过这种场景：CI 上 ESLint 跑了 5 分钟，最后报了一个“no-unused-vars”，你一脸问号：就这？</p><p>ESLint 其实干的活比你想的多：找文件、读配置、解析代码、建 AST、跑规则、收集问题、还能顺手给你修一部分。它慢，很多时候不是“它摆烂”，而是“它真在干活”。</p></div><h2 id="一句话版本：ESLint-在做什么？"><a href="#一句话版本：ESLint-在做什么？" class="headerlink" title="一句话版本：ESLint 在做什么？"></a>一句话版本：ESLint 在做什么？</h2><p>把 ESLint 想象成“代码体检”就行：</p><ol><li><strong>把源代码变成 AST</strong>（抽象语法树）</li><li><strong>用一堆规则（rules）去遍历 AST</strong>，找到问题</li><li><strong>输出诊断结果</strong>（可选：自动修复一部分）</li></ol><p>你看到的每一条报错，本质上就是：“在某个 AST 节点上，这条规则觉得你写得不对。”</p><hr><h2 id="ESLint-的完整流水线（从命令到报错）"><a href="#ESLint-的完整流水线（从命令到报错）" class="headerlink" title="ESLint 的完整流水线（从命令到报错）"></a>ESLint 的完整流水线（从命令到报错）</h2><p>先上一个总览流程图，后面逐段拆开。</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart TD    A[eslint CLI &#x2F; IDE] --&gt; B[解析参数与工作目录]    B --&gt; C[加载配置&lt;br&#x2F;&gt;flat config 或 eslintrc]    C --&gt; D[构建规则集&#x2F;插件&#x2F;共享配置]    D --&gt; E[收集目标文件&lt;br&#x2F;&gt;glob&#x2F;ignore&#x2F;overrides]    E --&gt; F[读取文件内容]    F --&gt; G[选择 parser &amp; parserOptions]    G --&gt; H[解析为 AST]    H --&gt; I[构建 scope&#x2F;变量引用]    I --&gt; J[运行 rules 遍历 AST]    J --&gt; K[生成 messages + fixes]    K --&gt; L[--fix 应用修复并回写文件]    K --&gt; M[formatter 输出结果]    M --&gt; N[退出码&lt;br&#x2F;&gt;error&#x2F;warn&#x2F;max-warnings]  </pre></div><hr><h2 id="1-入口：CLI-IDE-触发"><a href="#1-入口：CLI-IDE-触发" class="headerlink" title="1) 入口：CLI &#x2F; IDE 触发"></a>1) 入口：CLI &#x2F; IDE 触发</h2><p>两种常见入口：</p><ul><li>你在终端跑：<code>npx eslint .</code></li><li>你在编辑器里保存文件，IDE 插件后台跑（你看到红线&#x2F;黄线）</li></ul><p>这俩本质都是“调用 ESLint 引擎”，只是入口不同，默认参数可能不一样（比如 IDE 往往只 lint 当前文件，CLI 会扫整个项目）。</p><hr><h2 id="2-配置加载：flat-config-vs-eslintrc"><a href="#2-配置加载：flat-config-vs-eslintrc" class="headerlink" title="2) 配置加载：flat config vs .eslintrc"></a>2) 配置加载：flat config vs .eslintrc</h2><p>ESLint 首先要搞清楚：“你想让它按什么规则检查？”</p><p>目前你可能会遇到两种体系：</p><h3 id="2-1-传统：-eslintrc-（层层合并）"><a href="#2-1-传统：-eslintrc-（层层合并）" class="headerlink" title="2.1 传统：.eslintrc.*（层层合并）"></a>2.1 传统：.eslintrc.*（层层合并）</h3><p>比如 <code>.eslintrc.js/.json</code>，特点是：</p><ul><li>支持 <code>extends</code> 一层层叠加</li><li>支持 <code>overrides</code> 按文件类型覆盖</li><li>支持“从当前目录往上找”，一路找到项目根</li></ul><h3 id="2-2-新的：flat-config（eslint-config-js）"><a href="#2-2-新的：flat-config（eslint-config-js）" class="headerlink" title="2.2 新的：flat config（eslint.config.js）"></a>2.2 新的：flat config（eslint.config.js）</h3><p>flat config 的心智更直：</p><ul><li>配置就是一个数组：从上到下匹配、合并</li><li>更偏“JS 代码配置”，可组合性更强</li><li>很多新生态（尤其新版本插件）优先支持它</li></ul><p>你不需要纠结“哪种更好”，你只要记住：<strong>ESLint 会先把配置解析成“最终规则集”，后面流程才能继续。</strong></p><hr><h2 id="3-文件收集：哪些文件要检查？"><a href="#3-文件收集：哪些文件要检查？" class="headerlink" title="3) 文件收集：哪些文件要检查？"></a>3) 文件收集：哪些文件要检查？</h2><p>这一步决定了“ESLint 要跑多大范围”，也决定了你 lint 为啥慢。</p><p>ESLint 会综合这些来源来决定文件列表：</p><ul><li>CLI 传入的路径 &#x2F; glob（比如 <code>eslint src</code> &#x2F; <code>eslint &quot;src/**/*.&#123;ts,tsx&#125;&quot;</code>）</li><li>ignore（<code>.eslintignore</code>、<code>ignores</code>、默认忽略 <code>node_modules</code>）</li><li><code>overrides</code> &#x2F; flat config 的 file patterns（某些规则只对某些文件生效）</li></ul><p>一个很现实的经验：<strong>你 lint 的文件越多，ESLint 越慢；你让它去扫大目录，它就会真的去扫。</strong></p><hr><h2 id="4-解析：从文本到-AST"><a href="#4-解析：从文本到-AST" class="headerlink" title="4) 解析：从文本到 AST"></a>4) 解析：从文本到 AST</h2><p>ESLint 不直接“看字符串”，它要先解析成 AST。解析这一步由 parser 决定：</p><ul><li>默认 parser：Espree（支持 JS）</li><li>TS 常用：<code>@typescript-eslint/parser</code></li><li>Vue&#x2F;Svelte 等：往往有各自的 parser 或 processor</li></ul><p>大概过程是：</p><ol><li>读取文件内容（字符串）</li><li>按 <code>parserOptions</code>（比如 <code>ecmaVersion</code>、<code>sourceType</code>）解析</li><li>得到 AST</li></ol><p>如果你看到那种报错：</p><blockquote><p>Parsing error: Unexpected token</p></blockquote><p>十有八九是 parser &#x2F; parserOptions 不对，或者你 lint 了不该 lint 的文件类型。</p><hr><h2 id="5-建立“变量与作用域”信息（scope）"><a href="#5-建立“变量与作用域”信息（scope）" class="headerlink" title="5) 建立“变量与作用域”信息（scope）"></a>5) 建立“变量与作用域”信息（scope）</h2><p>很多规则并不只是“看一眼节点就结束”，它需要知道：</p><ul><li>这个变量在哪声明？</li><li>在哪被引用？</li><li>有没有引用但没用？</li><li>是不是 shadow 了外层变量？</li></ul><p>所以 ESLint 会在 AST 基础上构建 scope 信息（变量&#x2F;引用关系）。</p><p>这也是为什么 <code>no-unused-vars</code> 这种规则看起来简单，实际上要做不少分析工作。</p><hr><h2 id="6-跑规则：rules-如何工作？"><a href="#6-跑规则：rules-如何工作？" class="headerlink" title="6) 跑规则：rules 如何工作？"></a>6) 跑规则：rules 如何工作？</h2><p>核心来了：ESLint 的 rule 本质是一个“AST 监听器”。</p><p>它一般长这样：</p><figure class="highlight js"><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="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">meta</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;problem&#x27;</span>, <span class="attr">fixable</span>: <span class="string">&#x27;code&#x27;</span> &#125;,</span><br><span class="line">  <span class="title function_">create</span>(<span class="params">context</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="title class_">Identifier</span>(node) &#123;</span><br><span class="line">        <span class="comment">// 看到某个节点就做检查</span></span><br><span class="line">        <span class="comment">// 满足条件就 context.report(...)</span></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>你可以把它理解成：</p><ul><li>ESLint 遍历 AST</li><li>走到某种节点（比如 <code>CallExpression</code>）时，把节点扔给对应 rule 的处理函数</li><li>rule 决定要不要报错、报什么、能不能修</li></ul><h3 id="rule-的输出是什么？"><a href="#rule-的输出是什么？" class="headerlink" title="rule 的输出是什么？"></a>rule 的输出是什么？</h3><p>每条问题通常包含：</p><ul><li>文件名、行列号</li><li>rule 名（比如 <code>no-undef</code>）</li><li>message（告诉你哪里不对）</li><li>severity（warn &#x2F; error）</li><li>可选：fix（怎么改）</li></ul><hr><h2 id="7-修复：–fix-到底做了什么？"><a href="#7-修复：–fix-到底做了什么？" class="headerlink" title="7) 修复：–fix 到底做了什么？"></a>7) 修复：–fix 到底做了什么？</h2><p>当你跑：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">eslint . --fix</span><br></pre></td></tr></table></figure><p>ESLint 会在拿到规则给出的 fix 后，按一定策略把修复应用到源文本上，然后回写文件。</p><p>注意：不是所有规则都能 fix。</p><ul><li>有些是“可安全自动修复”的（比如补分号、调整引号、简单替换）</li><li>有些是“需要你做决定”的（比如逻辑改写、可能影响行为）</li></ul><p>所以 <code>--fix</code> 更像“能修的我帮你修”，不是“一键全修复”。</p><hr><h2 id="8-输出：formatter-退出码"><a href="#8-输出：formatter-退出码" class="headerlink" title="8) 输出：formatter + 退出码"></a>8) 输出：formatter + 退出码</h2><p>最后 ESLint 会把结果格式化输出（终端表格、json、stylish 等），然后决定退出码：</p><ul><li>0：没有 error（注意：有 warn 也可能是 0，取决于配置）</li><li>非 0：有 error，或触发了 <code>--max-warnings</code></li></ul><p>这就是为什么 CI 里经常会配：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">eslint . --max-warnings 0</span><br></pre></td></tr></table></figure><p>意思是：别给我“警告也算过”，要么干净，要么挂。</p><hr><h2 id="ESLint-为什么会慢？（最常见-4-个原因）"><a href="#ESLint-为什么会慢？（最常见-4-个原因）" class="headerlink" title="ESLint 为什么会慢？（最常见 4 个原因）"></a>ESLint 为什么会慢？（最常见 4 个原因）</h2><ol><li><strong>扫的文件太多</strong>：路径太大、glob 太宽、ignore 没写好</li><li><strong>TS 语义规则开太多</strong>：尤其是带 type 信息的规则，会读 <code>tsconfig</code>，成本很高</li><li><strong>插件规则太重</strong>：有些 rule 本身就复杂</li><li><strong>没用缓存</strong>：同样的文件每次都全量 lint</li></ol><p>如果你是大项目，建议至少了解一下缓存：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">eslint . --cache</span><br></pre></td></tr></table></figure><p>它能让“没变的文件”少跑很多。</p><hr><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>ESLint 的检查过程其实就是一条流水线：<strong>配置 → 找文件 → 解析 AST → 跑规则 → 输出&#x2F;修复</strong>。</p><p>你把这条链路想明白了之后：</p><ul><li>遇到误报：你会去查 parser&#x2F;config&#x2F;rule，而不是盲目关规则</li><li>遇到慢：你会先收敛文件范围、再看 TS type-aware 规则、再上 cache</li><li>需要定制：你也知道“写 rule”本质就是“写 AST 监听器”</li></ul><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2024/eslint/</id>
    <link href="https://linkdiary.com/2024/eslint/"/>
    <published>2024-05-26T04:20:00.000Z</published>
    <summary>ESLint 不是“跑一遍正则”那么简单：它把代码解析成 AST，按配置挑规则遍历节点，产出问题，再决定要不要自动修复。搞清楚这条流水线，你就知道为什么它会慢、为什么有时会误报、以及怎么调得更顺手。</summary>
    <title>ESLint 代码检查的过程是啥？</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="工具" scheme="https://linkdiary.com/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<h2 id="一、Oxlint-简介"><a href="#一、Oxlint-简介" class="headerlink" title="一、Oxlint 简介"></a>一、Oxlint 简介</h2><div class="note info flat"><p><strong>官网：</strong><a href="https://oxc-project.github.io/">https://oxc-project.github.io/</a></p></div><p><strong>Oxc</strong> (The Oxidation Compiler) 是一套用 Rust 写的 JS&#x2F;TS 工具链：parser、linter、formatter、transpiler、minifier……目标就是“把前端工程里那些很耗时的工具”用更快的实现替掉一部分。</p><p><strong>Oxlint</strong> 可以把它理解成“更快的 ESLint（但不是完全替代）”：它会检查常见错误&#x2F;陷阱&#x2F;不规范写法，并输出诊断信息。最大的卖点不是“规则更严”，而是<strong>速度</strong>——特别适合放在 <code>lint-staged</code>&#x2F;CI 的前置检查里，先把最常见的问题快速挡掉。</p><h2 id="二、特性"><a href="#二、特性" class="headerlink" title="二、特性"></a>二、特性</h2><h3 id="1-性能"><a href="#1-性能" class="headerlink" title="1. 性能"></a>1. 性能</h3><p>官方宣称速度比 ESLint 快 <strong>50-100 倍</strong>，并且可以更好地利用多核。<br>下图为 Oxlint 与 ESLint 耗费时间对比：</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224171059894.png"></p><p>尤雨溪在试用之后都在感叹 <strong>Oxlint</strong> 的速度之快。</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224171111533.png"></p><h3 id="2-开箱即用零配置"><a href="#2-开箱即用零配置" class="headerlink" title="2. 开箱即用零配置"></a>2. 开箱即用零配置</h3><p><strong>Oxlint</strong> 默认规则集更偏“正确性”和“明显的坑”（语法错误、冗余代码、容易误解的写法），而不是那种“风格洁癖”或“团队约定俗成”的细枝末节。</p><p>它的一个优点是：<strong>上手门槛低</strong>。很多场景你装完就能跑，不需要先花半天写配置。</p><p>而我们熟知的 <strong>ESLint</strong> 则是提供了大量可选的规则，使用者可利用其插件化和可层叠配置的特性来进行项目的定制化配置。基于此特性诞生了很多产品和工具包，如专注代码风格的 <code>prettier</code>、专注 TypeScript  规则的 <code>@typescript-eslint/eslint-plugin</code> 等。如果将 <strong>ESLint</strong> 应用到工程中，则需要安装多个包以及进行相关的配置。</p><h3 id="3-诊断可读性"><a href="#3-诊断可读性" class="headerlink" title="3. 诊断可读性"></a>3. 诊断可读性</h3><p>ESLint 的报错信息有时会比较“规则导向”：告诉你违反了哪个 rule，但你还得再翻文档&#x2F;结合上下文理解一遍。</p><p>Oxlint 在提示上更像“直接给你指出哪行哪列 + 发生了什么 + 可以怎么改”，阅读成本更低一些。</p><p><img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224171411073.png"></p><h3 id="4-兼容性"><a href="#4-兼容性" class="headerlink" title="4. 兼容性"></a>4. 兼容性</h3><p>对 <code>.eslintignore</code>、部分 <code>.eslintrc.json</code> 以及 ESLint 的 <code>eslint-disable</code> 注释有兼容；但它不等于 ESLint 的插件生态（依赖插件规则&#x2F;自定义规则的场景，ESLint 还是绕不过去）。</p><h2 id="三、安装与使用"><a href="#三、安装与使用" class="headerlink" title="三、安装与使用"></a>三、安装与使用</h2><h3 id="1-安装"><a href="#1-安装" class="headerlink" title="1. 安装"></a>1. 安装</h3><p>常规 npm 安装：</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">npm install -D oxlint</span><br><span class="line"></span><br><span class="line">yarn add -D oxlint</span><br></pre></td></tr></table></figure><h3 id="2-参数"><a href="#2-参数" class="headerlink" title="2. 参数"></a>2. 参数</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><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></pre></td><td class="code"><pre><span class="line">用法: oxlint [-A=NAME | -D=NAME]... [--fix] [-f] [-c=PATH] [--tsconfig=PATH] [PATH]...</span><br><span class="line"></span><br><span class="line">关闭和开启规则：</span><br><span class="line">  举例： `-D correctness -A no-debugger` 或 `-A all -D no-debugger`.</span><br><span class="line">  默认开启的规则集是 correctness.</span><br><span class="line">  &quot;--rules&quot; 展示所有支持的规则.</span><br><span class="line">  &quot;--help --help&quot; 展示所有支持的规则集合.</span><br><span class="line">  -A, --allow=NAME          关闭哪些规则或规则集合</span><br><span class="line">  -D, --deny=NAME           开启哪些规则或规则集合</span><br><span class="line"></span><br><span class="line">开启插件：</span><br><span class="line">        --import-plugin       启用实验性的导入插件并检测ESM问题</span><br><span class="line">        --jest-plugin         启用 Jest 插件并检测测试问题</span><br><span class="line">        --jsx-a11y-plugin     启用 JSX-a11y 插件并检测可访问性问题</span><br><span class="line">        --nextjs-plugin       启用 Next.js 插件并检测 Next.js 问题</span><br><span class="line">        --react-perf-plugin   启用React性能插件并检测渲染性能问题</span><br><span class="line"></span><br><span class="line">问题修复：</span><br><span class="line">        --fix                 尽可能多地解决问题。输出中只报告未解决的问题</span><br><span class="line"></span><br><span class="line">忽略文件：</span><br><span class="line">        --ignore-path=PATH    指定要用作 .eslintignore 的文件</span><br><span class="line">        --ignore-pattern=PAT  指定要忽略的文件模式（除了 .eslintignore 中指定之外的）</span><br><span class="line">        --no-ignore           禁止从 .eslintignore 文件中排除文件, --ignore-path --ignore-pattern</span><br><span class="line"></span><br><span class="line">告警处理：</span><br><span class="line">        --quiet               禁用警告报告，只报告错误</span><br><span class="line">        --deny-warnings       确保警告产生非零退出代码</span><br><span class="line">        --max-warnings=INT    指定一个警告阈值，如果项目中存在太多违反警告级别规则的情况，该阈值可用于强制退出并显示错误状态</span><br><span class="line"></span><br><span class="line">输出：</span><br><span class="line">        -f, --format          使用特定的输出格式（默认为json）</span><br><span class="line"></span><br><span class="line">多线程：</span><br><span class="line">        --threads=INT         要使用的线程数。设置为1以仅使用1个CPU核心</span><br><span class="line"></span><br><span class="line">其他参数:</span><br><span class="line">        --rules               列出当前注册的所有规则</span><br><span class="line">    -c, --config=PATH         ESLint 配置文件 (experimental)</span><br><span class="line">        --tsconfig=PATH       TypeScript“tsconfig.json”路径，用于读取导入插件的路径别名和项目引用</span><br><span class="line">    -h, --help                帮助信息</span><br><span class="line">    -V, --version             版本信息</span><br></pre></td></tr></table></figure><h3 id="3-相关工具"><a href="#3-相关工具" class="headerlink" title="3. 相关工具"></a>3. 相关工具</h3><h4 id="3-1-eslint-plugin-oxlint"><a href="#3-1-eslint-plugin-oxlint" class="headerlink" title="3.1 eslint-plugin-oxlint"></a>3.1 eslint-plugin-oxlint</h4><p>借助 <a href="https://github.com/oxc-project/eslint-plugin-oxlint">eslint-plugin-oxlint</a>，可以关闭 ESLint 中 Oxlint 已经支持的规则，这样可以在 ESLint 项目中结合 Oxlint 来进行代码 Lint，提高速度。</p><h4 id="3-2-lint-staged"><a href="#3-2-lint-staged" class="headerlink" title="3.2 lint-staged"></a>3.2 lint-staged</h4><figure class="highlight json"><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="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;lint-staged&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte&#125;&quot;</span><span class="punctuation">:</span> <span class="string">&quot;oxlint&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h4 id="3-3-VSCode-插件"><a href="#3-3-VSCode-插件" class="headerlink" title="3.3 VSCode 插件"></a>3.3 VSCode 插件</h4><p><a href="https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode">Visual Studio Marketplace</a></p><h4 id="3-4-Vite-插件"><a href="#3-4-Vite-插件" class="headerlink" title="3.4 Vite 插件"></a>3.4 Vite 插件</h4><p><a href="https://github.com/52-entertainment/vite-plugin-oxlint">https://github.com/52-entertainment/vite-plugin-oxlint</a></p><h4 id="3-5-pre-commit"><a href="#3-5-pre-commit" class="headerlink" title="3.5 pre-commit"></a>3.5 pre-commit</h4><figure class="highlight yaml"><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="attr">repos:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">repo:</span> <span class="string">https://github.com/oxc-project/mirrors-oxlint</span></span><br><span class="line">    <span class="attr">rev:</span> <span class="string">v0.0.0</span> <span class="comment"># change to the latest version</span></span><br><span class="line">    <span class="attr">hooks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">oxlint</span></span><br><span class="line">        <span class="attr">verbose:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><h2 id="四、与-ESLint-的对比"><a href="#四、与-ESLint-的对比" class="headerlink" title="四、与 ESLint 的对比"></a>四、与 ESLint 的对比</h2><div class="note warning flat"><p>为了方便演示两个工具在实际应用中的表现，–下表– 中关于在项目中应用的介绍以某业务工程为例。</p></div><table><thead><tr><th></th><th align="center">ESLint</th><th align="center">Oxlint</th><th>Note</th></tr></thead><tbody><tr><td><strong>运行速度</strong></td><td align="center"><strong>10.6s</strong> <br/> <em>ESLint 的控制台输出没有时间，此时间为秒表计时，不够严谨。</em></td><td align="center"><mark class="hl-label red">34ms</mark></td><td>--</td></tr><tr><td><strong>包数量</strong></td><td align="center"><strong>4 个</strong> <br/> <em>除 ESLint 工具本身以外，至少还需要 React、TypeScript、Standard 三个 ESLint Plugin</em></td><td align="center"><mark class="hl-label red">1个</mark></td><td>事实上，除 ESLint Plugin 外，工程中还需要使用 ESLint config 包来使用 默认&#x2F;推荐 的规则配置</td></tr><tr><td><strong>配置项</strong></td><td align="center"><strong>较复杂</strong> <br/> <em>需配置插件、规则集及对应告警等级（error、warning）</em></td><td align="center"><mark class="hl-label red">较简单</mark> <br/><em>可以零配置使用，只有规则可配置</em></td><td>--</td></tr><tr><td><strong>报错信息</strong></td><td align="center"><strong>简单</strong><br/> <img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224175347581.png"></td><td align="center"><mark class="hl-label red">详细</mark> <br/> <img src="https://assets.kiteblog.cn/cdn-cgi/image/format=auto/images/20250224175312913.png"></td><td>Oxlint 的报错信息会明确告知错误在哪里，为什么报错和如何解决，更加详细和智能。</td></tr><tr><td><strong>规则数量</strong></td><td align="center"><mark class="hl-label red">多</mark> <br/> <em>目前 ESLint 的生态非常繁荣，相关的各技术栈的插件工具以及规则多到难以统计。</em></td><td align="center"><strong>少</strong> <br/> <em>目前 Oxlint 仅支持 318 个规则。</em></td><td>Oxlint 不支持extends中的rules规则，这导致只能使用Oxlint 的现有规则</td></tr><tr><td><strong>可定制化程度</strong></td><td align="center"><mark class="hl-label red">高</mark><br/><em>基于 ESLint 插件化和可层叠配置的特性，社区有非常多的插件。插件提供了各种定制化的规则，使用者可以按需引用插件定制规则。</em></td><td align="center"><strong>低</strong><br/><em>Oxlint  仅可以从目前已支持 318 个规则当中进行定制。</em></td><td>ESLint 有成熟的插件化系统，而 Oxlint 暂时不支持插件系统。</td></tr><tr><td><strong>参与开发成本</strong></td><td align="center"><mark class="hl-label red">低</mark><br/><em>ESLint 及其插件都是基于 JavaScript 开发，开发者参与规则开发成本较低。</em></td><td align="center"><strong>高</strong><br/><em>Oxlint 是基于 Rust 编写，自定义规则需使用 Rust，成本较高。</em></td><td>Oxlint 正在研究开发一套，专门用来编写规则，但何时问世和是否好用暂不得知。</td></tr></tbody></table><h2 id="五、思考"><a href="#五、思考" class="headerlink" title="五、思考"></a>五、思考</h2><h3 id="1-Oxlint-是否会取代-ESLint"><a href="#1-Oxlint-是否会取代-ESLint" class="headerlink" title="1. Oxlint 是否会取代 ESLint"></a>1. Oxlint 是否会取代 ESLint</h3><blockquote><p>At the current stage, oxlint is <strong>not intended to fully replace ESLint</strong>; it serves as an enhancement when ESLint’s slowness becomes a bottleneck in your workflow.</p><p>We recommend running oxlint before ESLint in your lint-staged or CI setup for a quicker feedback loop, considering it only takes a few seconds to run on large codebases.</p></blockquote><p>这段话我理解成一句：<strong>Oxlint 先负责把“最常见、最便宜”的问题快速拦住；ESLint 继续负责“生态丰富、可深度定制”的那一部分。</strong></p><p>官方也建议在 <code>lint-staged</code>&#x2F;CI 里先跑 Oxlint 再跑 ESLint：大部分常见问题在 Oxlint 这一步就能被挡掉，开发者得到反馈更快。</p><h3 id="2-Oxlint-的不足之处"><a href="#2-Oxlint-的不足之处" class="headerlink" title="2. Oxlint 的不足之处"></a>2. Oxlint 的不足之处</h3><p>Oxlint 刚出来不久，生态完整度肯定不如 ESLint。尤其是你依赖某些“社区插件规则”的时候，它很可能还覆盖不到。</p><p>如果你们有“自己写规则”的需求，Oxlint 目前得用 Rust 来做，门槛会比写 ESLint rule 高不少。这个问题不解决，就一定存在某些 ESLint 支持而 Oxlint 覆盖不到的角落，所以要完全取代 ESLint，短期内不现实。</p><h3 id="3-Oxlint-的前景如何"><a href="#3-Oxlint-的前景如何" class="headerlink" title="3. Oxlint 的前景如何"></a>3. Oxlint 的前景如何</h3><p>如果你团队对“lint 太慢”已经忍无可忍，那 Oxlint 很值得试：收益很直接，上手也不费劲。</p><p>我个人更倾向的预期是：短期内形成“Oxlint 负责快、ESLint 负责全”的组合；后面等规则覆盖度上来，ESLint 在工程里的比重可能会逐步下降。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2024/oxlint/</id>
    <link href="https://linkdiary.com/2024/oxlint/"/>
    <published>2024-04-18T02:43:05.000Z</published>
    <summary>ESLint 最大的槽点之一就是慢。Oxlint 用 Rust 写了一套更快的 linter，这篇记录它到底解决了什么、适合怎么接进工程里。</summary>
    <title>基于 Rust 的代码 Lint 方案 - Oxlint</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工程化" scheme="https://linkdiary.com/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-tachometer-alt"></i><p>很多时候“React 很慢”其实是“某几个组件在不停做没必要的事”：重复计算、反复渲染、列表一次性渲染太多、Context 一更新全家跟着跑……这篇文章不聊 Web 性能的全套（资源、网络、缓存那些），只聚焦在 React 使用层面：我平时会优先排查什么、哪些招数用起来确实顺手、以及常见的反作用。</p></div><h2 id="一、组件渲染优化"><a href="#一、组件渲染优化" class="headerlink" title="一、组件渲染优化"></a>一、组件渲染优化</h2><p>先说一个我自己的习惯：别一上来就“优化”，先确认“慢”到底慢在哪。多数情况下你会发现问题不是渲染次数多，而是某次渲染里做了太多工作（比如列表的排序&#x2F;过滤、复杂的图表计算、或者一堆组件因为 props 引用不稳定被带着刷新）。</p><h3 id="1-React-memo-避免不必要的重渲染"><a href="#1-React-memo-避免不必要的重渲染" class="headerlink" title="1. React.memo - 避免不必要的重渲染"></a>1. React.memo - 避免不必要的重渲染</h3><p><code>React.memo</code> 的核心作用不是“让组件变快”，而是“当 props 没变时，别再渲染一遍”。它适合那种渲染成本比较高、同时 props 又比较稳定的子组件（尤其是列表 item &#x2F; 图表 &#x2F; 大块 UI）。</p><p>但也别把它当银弹：如果你每次都传进来一个新对象&#x2F;新数组&#x2F;新函数（比如 <code>style=&#123;&#123;...&#125;&#125;</code>、<code>onClick=&#123;() =&gt; ...&#125;</code>），那 <code>memo</code> 基本等于没用；更糟的是，全站乱加 <code>memo</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#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> <span class="title class_">ExpensiveComponent</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(<span class="function">(<span class="params">&#123; data, onUpdate &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;ExpensiveComponent rendered&#x27;</span>);</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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data.map(item =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span>&gt;</span>&#123;item.name&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;onUpdate&#125;</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">div</span>&gt;</span></span></span><br><span class="line">  );</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">const</span> <span class="title class_">CustomMemoComponent</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(</span><br><span class="line">  <span class="function">(<span class="params">&#123; data, onUpdate &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;/* 组件内容 */&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function">(<span class="params">prevProps, nextProps</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 返回 true 表示不重新渲染，false 表示重新渲染</span></span><br><span class="line">    <span class="keyword">return</span> prevProps.<span class="property">data</span>.<span class="property">length</span> === nextProps.<span class="property">data</span>.<span class="property">length</span>;</span><br><span class="line">  &#125;</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="2-useMemo-缓存计算结果"><a href="#2-useMemo-缓存计算结果" class="headerlink" title="2. useMemo - 缓存计算结果"></a>2. useMemo - 缓存计算结果</h3><p><code>useMemo</code> 更像是“把一次昂贵计算的结果缓存起来”。典型场景是：过滤、排序、分组、派生数据这些计算本身就挺费，且依赖项变化频率不高。</p><p>我一般不会为了“看起来专业”到处加 <code>useMemo</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useMemo &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">DataTable</span>(<span class="params">&#123; data, filterText &#125;</span>) &#123;</span><br><span class="line">  <span class="comment">// 缓存过滤后的数据</span></span><br><span class="line">  <span class="keyword">const</span> filteredData = <span class="title function_">useMemo</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;重新计算过滤数据&#x27;</span>);</span><br><span class="line">    <span class="keyword">return</span> data.<span class="title function_">filter</span>(<span class="function"><span class="params">item</span> =&gt;</span> </span><br><span class="line">      item.<span class="property">name</span>.<span class="title function_">toLowerCase</span>().<span class="title function_">includes</span>(filterText.<span class="title function_">toLowerCase</span>())</span><br><span class="line">    );</span><br><span class="line">  &#125;, [data, filterText]); <span class="comment">// 依赖项</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> sortedData = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> [...filteredData].<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> a.<span class="property">name</span>.<span class="title function_">localeCompare</span>(b.<span class="property">name</span>));</span><br><span class="line">  &#125;, [filteredData]);</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">table</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;sortedData.map(item =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">tr</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">td</span>&gt;</span>&#123;item.name&#125;<span class="tag">&lt;/<span class="name">td</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">td</span>&gt;</span>&#123;item.value&#125;<span class="tag">&lt;/<span class="name">td</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">table</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-useCallback-缓存函数引用"><a href="#3-useCallback-缓存函数引用" class="headerlink" title="3. useCallback - 缓存函数引用"></a>3. useCallback - 缓存函数引用</h3><p><code>useCallback</code> 解决的是“函数引用不稳定”这个很常见的问题：父组件一 re-render，就会创建新的函数对象，传给子组件后会让 <code>React.memo</code> 失效，或者触发依赖它的 <code>useEffect</code> 重新执行。</p><p>它同样不建议滥用：如果函数不会被下游当作依赖、也不会传给做了 memo 的子组件，那你加 <code>useCallback</code> 多半只是增加复杂度。另外依赖数组也别为了“图省事”随手写空——空依赖不是不行，但要确认函数体里没有读到会变化的外部值（除了 <code>setState</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useCallback, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ParentComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> [data, setData] = <span class="title function_">useState</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> handleAddItem = <span class="title function_">useCallback</span>(<span class="function">(<span class="params">newItem</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">setData</span>(<span class="function"><span class="params">prev</span> =&gt;</span> [...prev, newItem]);</span><br><span class="line">  &#125;, []); <span class="comment">// 空依赖数组，函数引用永远不变</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> handleUpdateItem = <span class="title function_">useCallback</span>(<span class="function">(<span class="params">id, updates</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">setData</span>(<span class="function"><span class="params">prev</span> =&gt;</span> prev.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> </span><br><span class="line">      item.<span class="property">id</span> === id ? &#123; ...item, ...updates &#125; : item</span><br><span class="line">    ));</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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>Count: &#123;count&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(c =&gt; c + 1)&#125;&gt;增加计数<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">ChildComponent</span> <span class="attr">data</span>=<span class="string">&#123;data&#125;</span> <span class="attr">onAdd</span>=<span class="string">&#123;handleAddItem&#125;</span> <span class="attr">onUpdate</span>=<span class="string">&#123;handleUpdateItem&#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></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ChildComponent</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(<span class="function">(<span class="params">&#123; data, onAdd, onUpdate &#125;</span>) =&gt;</span> &#123;</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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data.map(item =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;item.name&#125;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> onUpdate(item.id, &#123; name: &#x27;Updated&#x27; &#125;)&#125;&gt;</span></span><br><span class="line"><span class="language-xml">            更新</span></span><br><span class="line"><span class="language-xml">          <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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="二、状态管理优化"><a href="#二、状态管理优化" class="headerlink" title="二、状态管理优化"></a>二、状态管理优化</h2><p>状态这块很多性能问题的根源其实是“谁在订阅变化”。你希望改 A 只影响 A 的消费者，而不是把整棵子树都带着刷新一遍。</p><h3 id="1-状态分割-避免不必要的重渲染"><a href="#1-状态分割-避免不必要的重渲染" class="headerlink" title="1. 状态分割 - 避免不必要的重渲染"></a>1. 状态分割 - 避免不必要的重渲染</h3><p>把所有东西塞进一个巨大的 state 对象里，看起来“统一管理”很爽，但更新任何一个字段都会让依赖这个对象的地方一起动。如果组件树又深一点，很容易牵一发而动全身。</p><p>更实用的做法是：按“变化频率”和“消费范围”去拆 state。更新频繁的、只影响局部 UI 的状态尽量就地放；跨组件共享的再往上提，或者交给专门的 store。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ❌ 不好的做法：一个大状态对象</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">BadExample</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, setState] = <span class="title function_">useState</span>(&#123;</span><br><span class="line">    <span class="attr">user</span>: &#123; <span class="attr">name</span>: <span class="string">&#x27;John&#x27;</span>, <span class="attr">email</span>: <span class="string">&#x27;john@example.com&#x27;</span> &#125;,</span><br><span class="line">    <span class="attr">settings</span>: &#123; <span class="attr">theme</span>: <span class="string">&#x27;dark&#x27;</span>, <span class="attr">language</span>: <span class="string">&#x27;en&#x27;</span> &#125;,</span><br><span class="line">    <span class="attr">notifications</span>: &#123; <span class="attr">count</span>: <span class="number">5</span>, <span class="attr">list</span>: [] &#125;</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_">updateUser</span> = (<span class="params">userData</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setState</span>(<span class="function"><span class="params">prev</span> =&gt;</span> (&#123; ...prev, <span class="attr">user</span>: &#123; ...prev.<span class="property">user</span>, ...userData &#125; &#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">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;/* 组件内容 */&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></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="keyword">function</span> <span class="title function_">GoodExample</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [user, setUser] = <span class="title function_">useState</span>(&#123; <span class="attr">name</span>: <span class="string">&#x27;John&#x27;</span>, <span class="attr">email</span>: <span class="string">&#x27;john@example.com&#x27;</span> &#125;);</span><br><span class="line">  <span class="keyword">const</span> [settings, setSettings] = <span class="title function_">useState</span>(&#123; <span class="attr">theme</span>: <span class="string">&#x27;dark&#x27;</span>, <span class="attr">language</span>: <span class="string">&#x27;en&#x27;</span> &#125;);</span><br><span class="line">  <span class="keyword">const</span> [notifications, setNotifications] = <span class="title function_">useState</span>(&#123; <span class="attr">count</span>: <span class="number">5</span>, <span class="attr">list</span>: [] &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">updateUser</span> = (<span class="params">userData</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setUser</span>(<span class="function"><span class="params">prev</span> =&gt;</span> (&#123; ...prev, ...userData &#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">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;/* 组件内容 */&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-使用-useReducer-管理复杂状态"><a href="#2-使用-useReducer-管理复杂状态" class="headerlink" title="2. 使用 useReducer 管理复杂状态"></a>2. 使用 useReducer 管理复杂状态</h3><p>当状态更新逻辑开始“带条件、带流程、带多分支”时（比如 loading&#x2F;error&#x2F;items&#x2F;filter 一起联动），我会更倾向用 <code>useReducer</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useReducer &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> initialState = &#123;</span><br><span class="line">  <span class="attr">items</span>: [],</span><br><span class="line">  <span class="attr">loading</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">error</span>: <span class="literal">null</span>,</span><br><span class="line">  <span class="attr">filter</span>: <span class="string">&#x27;all&#x27;</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">reducer</span>(<span class="params">state, action</span>) &#123;</span><br><span class="line">  <span class="keyword">switch</span> (action.<span class="property">type</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;SET_LOADING&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> &#123; ...state, <span class="attr">loading</span>: action.<span class="property">payload</span> &#125;;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;SET_ITEMS&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> &#123; ...state, <span class="attr">items</span>: action.<span class="property">payload</span>, <span class="attr">loading</span>: <span class="literal">false</span> &#125;;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;ADD_ITEM&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> &#123; ...state, <span class="attr">items</span>: [...state.<span class="property">items</span>, action.<span class="property">payload</span>] &#125;;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;REMOVE_ITEM&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> &#123; </span><br><span class="line">        ...state, </span><br><span class="line">        <span class="attr">items</span>: state.<span class="property">items</span>.<span class="title function_">filter</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="property">id</span> !== action.<span class="property">payload</span>) </span><br><span class="line">      &#125;;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;SET_FILTER&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> &#123; ...state, <span class="attr">filter</span>: action.<span class="property">payload</span> &#125;;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;SET_ERROR&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> &#123; ...state, <span class="attr">error</span>: action.<span class="property">payload</span>, <span class="attr">loading</span>: <span class="literal">false</span> &#125;;</span><br><span class="line">    <span class="attr">default</span>:</span><br><span class="line">      <span class="keyword">return</span> state;</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">function</span> <span class="title function_">ItemList</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, dispatch] = <span class="title function_">useReducer</span>(reducer, initialState);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">addItem</span> = (<span class="params">item</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">dispatch</span>(&#123; <span class="attr">type</span>: <span class="string">&#x27;ADD_ITEM&#x27;</span>, <span class="attr">payload</span>: item &#125;);</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_">removeItem</span> = (<span class="params">id</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">dispatch</span>(&#123; <span class="attr">type</span>: <span class="string">&#x27;REMOVE_ITEM&#x27;</span>, <span class="attr">payload</span>: id &#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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;state.loading &amp;&amp; <span class="tag">&lt;<span class="name">div</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;</span></span><br><span class="line"><span class="language-xml">      &#123;state.error &amp;&amp; <span class="tag">&lt;<span class="name">div</span>&gt;</span>错误: &#123;state.error&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;</span></span><br><span class="line"><span class="language-xml">      &#123;state.items.map(item =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;item.name&#125;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> removeItem(item.id)&#125;&gt;删除<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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="三、列表渲染优化"><a href="#三、列表渲染优化" class="headerlink" title="三、列表渲染优化"></a>三、列表渲染优化</h2><h3 id="1-虚拟列表-处理大量数据"><a href="#1-虚拟列表-处理大量数据" class="headerlink" title="1. 虚拟列表 - 处理大量数据"></a>1. 虚拟列表 - 处理大量数据</h3><p>列表卡顿是最常见的性能问题之一：一次性渲染几千个节点，浏览器和 React 都很难受。虚拟列表的思路很朴素——只渲染可视区域附近那一小段，其它的用占位高度“假装存在”。</p><p>实际项目里我通常会直接用成熟库（比如 <code>react-window</code> &#x2F; <code>react-virtualized</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useState, useMemo &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">VirtualList</span>(<span class="params">&#123; items, itemHeight = <span class="number">50</span>, containerHeight = <span class="number">400</span> &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [scrollTop, setScrollTop] = <span class="title function_">useState</span>(<span class="number">0</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> visibleRange = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> startIndex = <span class="title class_">Math</span>.<span class="title function_">floor</span>(scrollTop / itemHeight);</span><br><span class="line">    <span class="keyword">const</span> endIndex = <span class="title class_">Math</span>.<span class="title function_">min</span>(</span><br><span class="line">      startIndex + <span class="title class_">Math</span>.<span class="title function_">ceil</span>(containerHeight / itemHeight) + <span class="number">1</span>,</span><br><span class="line">      items.<span class="property">length</span></span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">return</span> &#123; startIndex, endIndex &#125;;</span><br><span class="line">  &#125;, [scrollTop, itemHeight, containerHeight, items.<span class="property">length</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> visibleItems = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> items.<span class="title function_">slice</span>(visibleRange.<span class="property">startIndex</span>, visibleRange.<span class="property">endIndex</span>);</span><br><span class="line">  &#125;, [items, visibleRange]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleScroll</span> = (<span class="params">e</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setScrollTop</span>(e.<span class="property">target</span>.<span class="property">scrollTop</span>);</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">div</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">height:</span> <span class="attr">containerHeight</span>, <span class="attr">overflow:</span> &#x27;<span class="attr">auto</span>&#x27; &#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">onScroll</span>=<span class="string">&#123;handleScroll&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    &gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">height:</span> <span class="attr">items.length</span> * <span class="attr">itemHeight</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">transform:</span> `<span class="attr">translateY</span>($&#123;<span class="attr">visibleRange.startIndex</span> * <span class="attr">itemHeight</span>&#125;<span class="attr">px</span>)` &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;visibleItems.map((item, index) =&gt; (</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">div</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">height:</span> <span class="attr">itemHeight</span>, <span class="attr">borderBottom:</span> &#x27;<span class="attr">1px</span> <span class="attr">solid</span> #<span class="attr">eee</span>&#x27; &#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            &gt;</span></span></span><br><span class="line"><span class="language-xml">              &#123;item.name&#125;</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          ))&#125;</span></span><br><span class="line"><span class="language-xml">        <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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-使用稳定的-key"><a href="#2-使用稳定的-key" class="headerlink" title="2. 使用稳定的 key"></a>2. 使用稳定的 key</h3><p><code>key</code> 的问题经常不是“性能”而是“错乱”：用索引当 key，一旦插入&#x2F;删除&#x2F;排序，React 可能复用错节点，导致输入框光标跳、展开状态串行、动画怪异。稳定且唯一的 key 基本是写列表的底线。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 不好的做法：使用索引作为 key</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">BadList</span>(<span class="params">&#123; items &#125;</span>) &#123;</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">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;items.map((item, index) =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;index&#125;</span>&gt;</span>&#123;item.name&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span> // 索引会变化</span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 好的做法：使用唯一 ID</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">GoodList</span>(<span class="params">&#123; items &#125;</span>) &#123;</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">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;items.map(item =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span>&gt;</span>&#123;item.name&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span> // 使用稳定的 ID</span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="四、事件处理优化"><a href="#四、事件处理优化" class="headerlink" title="四、事件处理优化"></a>四、事件处理优化</h2><h3 id="1-事件委托"><a href="#1-事件委托" class="headerlink" title="1. 事件委托"></a>1. 事件委托</h3><p>如果你渲染了很多相似元素（比如一长串按钮&#x2F;菜单项），每个都挂一个 handler 并不是“绝对不行”，但在某些场景下（尤其是你还在做复杂计算）确实会让内存和更新成本上来。</p><p>事件委托的做法是：把事件绑在父节点上，通过 <code>event.target</code>&#x2F;<code>closest</code> 判断点的是谁。React 的合成事件本身就做了一层封装，这个思路依旧可用，只是要注意目标元素可能是子节点（比如按钮里的 icon）。</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">import</span> <span class="title class_">React</span>, &#123; useCallback &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">EventDelegationExample</span>(<span class="params">&#123; items &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="title function_">useCallback</span>(<span class="function">(<span class="params">e</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> target = e.<span class="property">target</span>;</span><br><span class="line">    <span class="keyword">if</span> (target.<span class="title function_">matches</span>(<span class="string">&#x27;.item-button&#x27;</span>)) &#123;</span><br><span class="line">      <span class="keyword">const</span> itemId = target.<span class="property">dataset</span>.<span class="property">id</span>;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;点击了项目:&#x27;</span>, itemId);</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">div</span> <span class="attr">onClick</span>=<span class="string">&#123;handleClick&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;items.map(item =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span> <span class="attr">className</span>=<span class="string">&quot;item&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;item.name&#125;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">button</span> <span class="attr">className</span>=<span class="string">&quot;item-button&quot;</span> <span class="attr">data-id</span>=<span class="string">&#123;item.id&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            操作</span></span><br><span class="line"><span class="language-xml">          <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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-防抖和节流"><a href="#2-防抖和节流" class="headerlink" title="2. 防抖和节流"></a>2. 防抖和节流</h3><p>输入框联想、窗口 resize、滚动监听这类事件很容易“刷屏”。防抖&#x2F;节流本质是降低触发频率，别让主线程一直在跑回调。</p><p>另外一个小坑：在 Hook 里用 <code>useState</code> 存 timer id&#x2F;时间戳，会引入额外的 state 更新和重渲染；这里用 <code>useRef</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><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><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useState, useCallback, useEffect, useRef &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 防抖 Hook</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">useDebounce</span>(<span class="params">callback, delay</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> timeoutRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> latestCallbackRef = <span class="title function_">useRef</span>(callback);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    latestCallbackRef.<span class="property">current</span> = callback;</span><br><span class="line">  &#125;, [callback]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">useCallback</span>(<span class="function">(<span class="params">...args</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (timeoutRef.<span class="property">current</span>) <span class="built_in">clearTimeout</span>(timeoutRef.<span class="property">current</span>);</span><br><span class="line">    timeoutRef.<span class="property">current</span> = <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      latestCallbackRef.<span class="title function_">current</span>(...args);</span><br><span class="line">    &#125;, delay);</span><br><span class="line">  &#125;, [delay]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 节流 Hook</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">useThrottle</span>(<span class="params">callback, delay</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> lastCallRef = <span class="title function_">useRef</span>(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> latestCallbackRef = <span class="title function_">useRef</span>(callback);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    latestCallbackRef.<span class="property">current</span> = callback;</span><br><span class="line">  &#125;, [callback]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">useCallback</span>(<span class="function">(<span class="params">...args</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> now = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line">    <span class="keyword">if</span> (now - lastCallRef.<span class="property">current</span> &gt;= delay) &#123;</span><br><span class="line">      lastCallRef.<span class="property">current</span> = now;</span><br><span class="line">      latestCallbackRef.<span class="title function_">current</span>(...args);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [delay]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">SearchComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [searchTerm, setSearchTerm] = <span class="title function_">useState</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> debouncedSearch = <span class="title function_">useDebounce</span>(<span class="function">(<span class="params">term</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;搜索:&#x27;</span>, term);</span><br><span class="line">    <span class="comment">// 执行搜索逻辑</span></span><br><span class="line">  &#125;, <span class="number">300</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> throttledScroll = <span class="title function_">useThrottle</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;滚动事件&#x27;</span>);</span><br><span class="line">    <span class="comment">// 处理滚动逻辑</span></span><br><span class="line">  &#125;, <span class="number">100</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleInputChange</span> = (<span class="params">e</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> value = e.<span class="property">target</span>.<span class="property">value</span>;</span><br><span class="line">    <span class="title function_">setSearchTerm</span>(value);</span><br><span class="line">    <span class="title function_">debouncedSearch</span>(value);</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">div</span> <span class="attr">onScroll</span>=<span class="string">&#123;throttledScroll&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;searchTerm&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">onChange</span>=<span class="string">&#123;handleInputChange&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">placeholder</span>=<span class="string">&quot;搜索...&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="五、代码分割与懒加载"><a href="#五、代码分割与懒加载" class="headerlink" title="五、代码分割与懒加载"></a>五、代码分割与懒加载</h2><h3 id="1-React-lazy-和-Suspense"><a href="#1-React-lazy-和-Suspense" class="headerlink" title="1. React.lazy 和 Suspense"></a>1. React.lazy 和 Suspense</h3><p>懒加载的目标很简单：首屏先把“必须的”交付出去，后面的页面&#x2F;模块等用户真的要用再下载。对于后台系统、组件库页面特别多的项目，这个收益往往很直观。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; <span class="title class_">Suspense</span>, lazy, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#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> <span class="title class_">LazyComponent</span> = <span class="title function_">lazy</span>(<span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;./LazyComponent&#x27;</span>));</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">AnotherLazyComponent</span> = <span class="title function_">lazy</span>(<span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;./AnotherLazyComponent&#x27;</span>));</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [showLazy, setShowLazy] = <span class="title function_">useState</span>(<span class="literal">false</span>);</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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setShowLazy(!showLazy)&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        切换懒加载组件</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span></span><br><span class="line"><span class="language-xml">      &#123;showLazy &amp;&amp; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">LazyComponent</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      )&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-路由级别的代码分割"><a href="#2-路由级别的代码分割" class="headerlink" title="2. 路由级别的代码分割"></a>2. 路由级别的代码分割</h3><p>路由级别拆包是最常见的落点：不同页面通常天然是独立 chunk。注意点也很现实：拆得太碎会带来更多请求和瀑布流（尤其弱网）；拆得太粗首屏又大。一般先把“非首屏页面”拆出来，就能解决大部分问题。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; <span class="title class_">Suspense</span>, lazy &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">BrowserRouter</span>, <span class="title class_">Routes</span>, <span class="title class_">Route</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router-dom&#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> <span class="title class_">Home</span> = <span class="title function_">lazy</span>(<span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;./pages/Home&#x27;</span>));</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">About</span> = <span class="title function_">lazy</span>(<span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;./pages/About&#x27;</span>));</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Contact</span> = <span class="title function_">lazy</span>(<span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;./pages/Contact&#x27;</span>));</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</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">BrowserRouter</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>页面加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Routes</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&quot;/&quot;</span> <span class="attr">element</span>=<span class="string">&#123;</span>&lt;<span class="attr">Home</span> /&gt;</span>&#125; /&gt;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&quot;/about&quot;</span> <span class="attr">element</span>=<span class="string">&#123;</span>&lt;<span class="attr">About</span> /&gt;</span>&#125; /&gt;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&quot;/contact&quot;</span> <span class="attr">element</span>=<span class="string">&#123;</span>&lt;<span class="attr">Contact</span> /&gt;</span>&#125; /&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">Routes</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">BrowserRouter</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="六、Context-优化"><a href="#六、Context-优化" class="headerlink" title="六、Context 优化"></a>六、Context 优化</h2><h3 id="1-分割-Context"><a href="#1-分割-Context" class="headerlink" title="1. 分割 Context"></a>1. 分割 Context</h3><p>Context 的痛点是：Provider 的 value 一变，所有消费它的组件都会重新渲染。把“用户信息&#x2F;主题&#x2F;通知”这种完全不同的东西塞进同一个 Context，等于人为扩大了受影响范围。</p><p>拆分 Context 的收益很朴素：谁关心谁更新。它不是为了“优雅”，而是为了“别误伤”。</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><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; createContext, useContext, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 分割 Context</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">UserContext</span> = <span class="title function_">createContext</span>();</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ThemeContext</span> = <span class="title function_">createContext</span>();</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">NotificationContext</span> = <span class="title function_">createContext</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UserProvider</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [user, setUser] = <span class="title function_">useState</span>(<span class="literal">null</span>);</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">UserContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;&#123;</span> <span class="attr">user</span>, <span class="attr">setUser</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">UserContext.Provider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ThemeProvider</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [theme, setTheme] = <span class="title function_">useState</span>(<span class="string">&#x27;light&#x27;</span>);</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">ThemeContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;&#123;</span> <span class="attr">theme</span>, <span class="attr">setTheme</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ThemeContext.Provider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">NotificationProvider</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [notifications, setNotifications] = <span class="title function_">useState</span>([]);</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">NotificationContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;&#123;</span> <span class="attr">notifications</span>, <span class="attr">setNotifications</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">NotificationContext.Provider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用 Hook</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">useUser</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> context = <span class="title function_">useContext</span>(<span class="title class_">UserContext</span>);</span><br><span class="line">  <span class="keyword">if</span> (!context) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;useUser must be used within UserProvider&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> context;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">useTheme</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> context = <span class="title function_">useContext</span>(<span class="title class_">ThemeContext</span>);</span><br><span class="line">  <span class="keyword">if</span> (!context) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;useTheme must be used within ThemeProvider&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> context;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</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">UserProvider</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ThemeProvider</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">NotificationProvider</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">MainApp</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">NotificationProvider</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">ThemeProvider</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">UserProvider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-使用-useMemo-优化-Context-值"><a href="#2-使用-useMemo-优化-Context-值" class="headerlink" title="2. 使用 useMemo 优化 Context 值"></a>2. 使用 useMemo 优化 Context 值</h3><p>如果你在 Provider 里直接写 <code>value=&#123;&#123; user, setUser &#125;&#125;</code>，那每次渲染都会创建新对象，即使 <code>user</code> 没变也会让消费者跟着刷新。用 <code>useMemo</code> 把 value 稳定下来，通常能少掉很多“无意义更新”。</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">import</span> <span class="title class_">React</span>, &#123; createContext, useContext, useState, useMemo &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">AppContext</span> = <span class="title function_">createContext</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">AppProvider</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [user, setUser] = <span class="title function_">useState</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> [theme, setTheme] = <span class="title function_">useState</span>(<span class="string">&#x27;light&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 使用 useMemo 缓存 Context 值</span></span><br><span class="line">  <span class="keyword">const</span> contextValue = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> (&#123;</span><br><span class="line">    user,</span><br><span class="line">    setUser,</span><br><span class="line">    theme,</span><br><span class="line">    setTheme,</span><br><span class="line">    <span class="attr">isLoggedIn</span>: !!user,</span><br><span class="line">    <span class="attr">isDarkTheme</span>: theme === <span class="string">&#x27;dark&#x27;</span></span><br><span class="line">  &#125;), [user, theme]);</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">AppContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;contextValue&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">AppContext.Provider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="七、Ref-优化"><a href="#七、Ref-优化" class="headerlink" title="七、Ref 优化"></a>七、Ref 优化</h2><h3 id="1-使用-useRef-避免不必要的重渲染"><a href="#1-使用-useRef-避免不必要的重渲染" class="headerlink" title="1. 使用 useRef 避免不必要的重渲染"></a>1. 使用 useRef 避免不必要的重渲染</h3><p>有些数据只是“存一下，后面拿来用”，并不需要驱动 UI（比如 timer id、第三方实例、上一次的值）。这类东西放在 <code>useState</code> 里只会白白触发重渲染，用 <code>useRef</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useRef, useEffect &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">TimerComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> intervalRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> countRef = <span class="title function_">useRef</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    intervalRef.<span class="property">current</span> = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      countRef.<span class="property">current</span> += <span class="number">1</span>;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;计数:&#x27;</span>, countRef.<span class="property">current</span>);</span><br><span class="line">    &#125;, <span class="number">1000</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (intervalRef.<span class="property">current</span>) &#123;</span><br><span class="line">        <span class="built_in">clearInterval</span>(intervalRef.<span class="property">current</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 class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>计时器运行中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-使用-useImperativeHandle-暴露方法"><a href="#2-使用-useImperativeHandle-暴露方法" class="headerlink" title="2. 使用 useImperativeHandle 暴露方法"></a>2. 使用 useImperativeHandle 暴露方法</h3><p><code>useImperativeHandle</code> 属于“必要时再用”的工具：当你确实需要让父组件调用子组件内部方法（比如控制一个输入框的 focus、暴露某个 reset），它很好用；但如果只是为了传递数据&#x2F;状态，优先考虑受控组件或 props 回调，通常更直观。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; forwardRef, useImperativeHandle, useRef, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ChildComponent</span> = <span class="title function_">forwardRef</span>(<span class="function">(<span class="params">props, ref</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useImperativeHandle</span>(ref, <span class="function">() =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">increment</span>: <span class="function">() =&gt;</span> <span class="title function_">setCount</span>(<span class="function"><span class="params">c</span> =&gt;</span> c + <span class="number">1</span>),</span><br><span class="line">    <span class="attr">decrement</span>: <span class="function">() =&gt;</span> <span class="title function_">setCount</span>(<span class="function"><span class="params">c</span> =&gt;</span> c - <span class="number">1</span>),</span><br><span class="line">    <span class="attr">reset</span>: <span class="function">() =&gt;</span> <span class="title function_">setCount</span>(<span class="number">0</span>),</span><br><span class="line">    <span class="attr">getCount</span>: <span class="function">() =&gt;</span> count</span><br><span class="line">  &#125;));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>计数: &#123;count&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ParentComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> childRef = <span class="title function_">useRef</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleIncrement</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    childRef.<span class="property">current</span>?.<span class="title function_">increment</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_">handleDecrement</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    childRef.<span class="property">current</span>?.<span class="title function_">decrement</span>();</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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ChildComponent</span> <span class="attr">ref</span>=<span class="string">&#123;childRef&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleIncrement&#125;</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">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleDecrement&#125;</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">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="八、开发工具与调试"><a href="#八、开发工具与调试" class="headerlink" title="八、开发工具与调试"></a>八、开发工具与调试</h2><h3 id="1-React-DevTools-Profiler"><a href="#1-React-DevTools-Profiler" class="headerlink" title="1. React DevTools Profiler"></a>1. React DevTools Profiler</h3><p>Profiler 是我排查 React 性能问题的首选：先定位“到底是谁在渲染、渲染花了多久”，再决定要不要上 <code>memo/useMemo/useCallback</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><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="keyword">import</span> <span class="title class_">React</span>, &#123; <span class="title class_">Profiler</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">onRenderCallback</span>(<span class="params"></span></span><br><span class="line"><span class="params">  id, <span class="comment">// Profiler 树的 id</span></span></span><br><span class="line"><span class="params">  phase, <span class="comment">// &quot;mount&quot; (首次挂载) 或 &quot;update&quot; (重新渲染)</span></span></span><br><span class="line"><span class="params">  actualDuration, <span class="comment">// 渲染花费的时间</span></span></span><br><span class="line"><span class="params">  baseDuration, <span class="comment">// 估计不使用 memoization 的情况下渲染整棵子树需要的时间</span></span></span><br><span class="line"><span class="params">  startTime, <span class="comment">// 渲染开始的时间</span></span></span><br><span class="line"><span class="params">  commitTime, <span class="comment">// 渲染提交的时间</span></span></span><br><span class="line"><span class="params">  interactions <span class="comment">// 属于这次更新的 interactions 的集合</span></span></span><br><span class="line"><span class="params"></span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Profiler:&#x27;</span>, &#123;</span><br><span class="line">    id,</span><br><span class="line">    phase,</span><br><span class="line">    actualDuration,</span><br><span class="line">    baseDuration,</span><br><span class="line">    startTime,</span><br><span class="line">    commitTime</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">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</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">Profiler</span> <span class="attr">id</span>=<span class="string">&quot;App&quot;</span> <span class="attr">onRender</span>=<span class="string">&#123;onRenderCallback&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">MainComponent</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Profiler</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-使用-why-did-you-render"><a href="#2-使用-why-did-you-render" class="headerlink" title="2. 使用 why-did-you-render"></a>2. 使用 why-did-you-render</h3><p>当你怀疑“我明明没改这个组件，为啥它老在渲染”时，<code>why-did-you-render</code> 很适合用来抓“触发源”（props 引用变化、hook 依赖变化等等）。我一般只在本地&#x2F;开发环境开它，问题定位完就关掉。</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="comment">// 在开发环境中启用</span></span><br><span class="line"><span class="keyword">if</span> (process.<span class="property">env</span>.<span class="property">NODE_ENV</span> === <span class="string">&#x27;development&#x27;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> whyDidYouRender = <span class="built_in">require</span>(<span class="string">&#x27;@welldone-software/why-did-you-render&#x27;</span>);</span><br><span class="line">  <span class="title function_">whyDidYouRender</span>(<span class="title class_">React</span>, &#123;</span><br><span class="line">    <span class="attr">trackAllPureComponents</span>: <span class="literal">true</span>,</span><br><span class="line">  &#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="title class_">MyComponent</span>.<span class="property">whyDidYouRender</span> = <span class="literal">true</span>;</span><br></pre></td></tr></table></figure><h2 id="九、性能优化对比总结"><a href="#九、性能优化对比总结" class="headerlink" title="九、性能优化对比总结"></a>九、性能优化对比总结</h2><table><thead><tr><th>优化手段</th><th>适用场景</th><th>常见收益（看场景）</th><th>实现复杂度</th><th>更像“坑”的部分</th></tr></thead><tbody><tr><td><strong>React.memo</strong></td><td>子组件渲染重、props 稳定</td><td>有时很赚</td><td>低</td><td>props 引用不稳会直接失效；到处加会变难维护</td></tr><tr><td><strong>useMemo</strong></td><td>过滤&#x2F;排序&#x2F;派生数据很费</td><td>有时很赚</td><td>中</td><td>过度使用收益不明显；依赖写错会出 bug</td></tr><tr><td><strong>useCallback</strong></td><td>传给 memo 子组件&#x2F;作为依赖</td><td>小到中</td><td>中</td><td>空依赖导致闭包旧值；依赖项管理麻烦</td></tr><tr><td><strong>状态分割</strong></td><td>大对象 state 被到处消费</td><td>经常有效</td><td>中</td><td>拆太碎也会让逻辑分散；要按消费范围拆</td></tr><tr><td><strong>useReducer</strong></td><td>多分支状态更新</td><td>更多是“好维护”</td><td>中</td><td>reducer 写得太大也会变成另一种“巨石”</td></tr><tr><td><strong>虚拟列表</strong></td><td>成百上千条数据渲染</td><td>往往立竿见影</td><td>高</td><td>动态高度&#x2F;滚动同步&#x2F;吸顶等边角成本高</td></tr><tr><td><strong>事件委托</strong></td><td>大量相似交互元素</td><td>看情况</td><td>中</td><td><code>target</code>&#x2F;<code>closest</code> 判断要写严谨，别点错人</td></tr><tr><td><strong>防抖&#x2F;节流</strong></td><td>输入&#x2F;滚动&#x2F;resize 等高频事件</td><td>经常有效</td><td>中</td><td>延迟不合适会影响体验；注意清理 timer</td></tr><tr><td><strong>代码分割</strong></td><td>页面多、首包大</td><td>常见有效</td><td>中</td><td>拆得太碎会造成请求瀑布；注意加载态</td></tr><tr><td><strong>Context 分割</strong></td><td>Context 更新误伤一大片</td><td>经常有效</td><td>高</td><td>Provider 太多会让结构变深；别为拆而拆</td></tr><tr><td><strong>useRef</strong></td><td>存非 UI 状态&#x2F;缓存实例</td><td>主要是减少渲染</td><td>低</td><td>ref 不触发渲染，别拿它当 state 用</td></tr></tbody></table><h2 id="十、最佳实践总结"><a href="#十、最佳实践总结" class="headerlink" title="十、最佳实践总结"></a>十、最佳实践总结</h2><ol><li><strong>先定位，再动手</strong>：Profiler 看清楚“谁慢、慢在哪”，别凭感觉到处 <code>memo</code>。</li><li><strong>优先解决大头</strong>：首屏大就拆包；列表卡就上虚拟列表；计算重就缓存派生数据。</li><li><strong>把引用稳定下来</strong>：对象&#x2F;数组&#x2F;函数如果要跨组件传递，先想想怎么让它别每次都变。</li><li><strong>别用优化把代码写烂</strong>：可维护性本身也是性能（人维护得动才会持续优化）。</li><li><strong>改完要复测</strong>：有些“优化”只是把问题挪走，甚至会引入更隐蔽的 bug。</li></ol><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>性能优化其实没那么玄学：把浪费的渲染次数砍掉、把不必要的计算挪走、把一次性渲染太多的列表“缩小到视口”，再配合工具把问题定位清楚，绝大多数卡顿都能解决。</p><p>如果你愿意再往前一步，我建议给页面设一个“性能预算”（比如交互响应时间、首屏可用时间、列表滚动帧率），这样优化目标会更明确，也更容易持续。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2023/react-performance/</id>
    <link href="https://linkdiary.com/2023/react-performance/"/>
    <published>2023-09-27T03:24:37.000Z</published>
    <summary>结合项目踩坑经验，整理一份「React 里哪些优化真的有用」的清单：从减少重渲染、稳定引用，到列表虚拟化、代码分割和 Context 拆分。</summary>
    <title>从 React 层面能做的性能优化有哪些？</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="学习" scheme="https://linkdiary.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="JavaScript" scheme="https://linkdiary.com/tags/JavaScript/"/>
    <content>
      <![CDATA[<p>前端最容易踩的大数坑通常有两类：</p><ol><li>金额、计数、ID 这类“看起来是整数”的东西，一旦超过 <code>2^53 - 1</code>（<code>Number.MAX_SAFE_INTEGER</code>），就会开始丢精度；</li><li>真的特别大的浮点数，超过 <code>Number.MAX_VALUE</code> 直接变成 <code>Infinity</code>。</li></ol><p>原因也很简单：JavaScript 的 <code>Number</code> 用的是 IEEE 754 双精度浮点。它能表示很大的范围，但整数精度只有 53 位。</p><ul><li><p><strong>超出 Number.MAX_VALUE</strong>：超出此值的数值会变为 <code>Infinity</code>。</p></li><li><p><strong>超过安全整数范围</strong>：大于 <code>Number.MAX_SAFE_INTEGER</code> 的整数可能会失去精度。</p></li></ul><p>遇到这些问题时，别急着“找一个库来解决”。先想清楚：你要处理的是“大整数”、还是“高精度小数”（比如金融）？两者方案不一样。</p><h2 id="使用-BigInt"><a href="#使用-BigInt" class="headerlink" title="使用 BigInt"></a>使用 BigInt</h2><p>ES2020 引入了 <code>BigInt</code>，用来表示任意大小的整数。它的心智模型也很直接：只要是整数，就不会丢精度。</p><figure class="highlight javascript"><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">// 创建一个超过 Number.MAX_SAFE_INTEGER 的整数</span></span><br><span class="line"><span class="keyword">const</span> bigIntValue = <span class="number">9007199254740993n</span>;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(bigIntValue); <span class="comment">// 输出：9007199254740993n</span></span><br></pre></td></tr></table></figure><h3 id="BigInt-特点"><a href="#BigInt-特点" class="headerlink" title="BigInt 特点"></a>BigInt 特点</h3><ul><li><p><strong>任意精度整数</strong>：能够表示非常大的整数，不会因为超出 Number 范围而变成 Infinity。</p></li><li><p><strong>运算符支持</strong>：BigInt 支持常见的算术运算，但不能和 <code>Number</code> 直接混算（需要显式转换）。</p></li></ul><p>例如：</p><figure class="highlight javascript"><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> a = <span class="number">9007199254740993n</span>;</span><br><span class="line"><span class="keyword">const</span> b = <span class="number">2n</span>;</span><br><span class="line"><span class="keyword">const</span> sum = a + b; <span class="comment">// 9007199254740995n</span></span><br></pre></td></tr></table></figure><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><ul><li><strong>不支持小数</strong>：BigInt 只能表示整数。涉及金额&#x2F;利率这类小数精度需求，通常要用 decimal 类库或“分&#x2F;厘”为单位的整数。</li><li><strong>JSON 不友好</strong>：<code>JSON.stringify</code> 不能直接序列化 BigInt（会报错），接口传输更常见的做法仍然是用字符串。</li></ul><hr><h2 id="使用第三方库"><a href="#使用第三方库" class="headerlink" title="使用第三方库"></a>使用第三方库</h2><p>如果你需要的是“高精度小数”（金融计算、计费、科学计算），那 BigInt 往往不够用。这时更合适的是 <a href="https://github.com/MikeMcl/bignumber.js/">bignumber.js</a> 或 <a href="https://github.com/MikeMcl/decimal.js/">decimal.js</a> 这类库，它们会自己维护精度和舍入规则。</p><h3 id="示例：使用-bignumber-js"><a href="#示例：使用-bignumber-js" class="headerlink" title="示例：使用 bignumber.js"></a>示例：使用 bignumber.js</h3><ol><li>安装 bignumber.js：</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install bignumber.js --save</span><br></pre></td></tr></table></figure><ol start="2"><li>使用示例：</li></ol><figure class="highlight javascript"><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="keyword">const</span> <span class="title class_">BigNumber</span> = <span class="built_in">require</span>(<span class="string">&#x27;bignumber.js&#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> bigNum = <span class="keyword">new</span> <span class="title class_">BigNumber</span>(<span class="string">&#x27;1.7976931348623157e+308&#x27;</span>);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(bigNum.<span class="title function_">toString</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> result = bigNum.<span class="title function_">plus</span>(<span class="string">&#x27;1e+292&#x27;</span>);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(result.<span class="title function_">toString</span>());</span><br></pre></td></tr></table></figure><p>这些库不仅能处理非常大的整数，还能处理高精度的小数运算，适用于金融计算、科学计算等场景。</p><h3 id="处理-Infinity-和-NaN"><a href="#处理-Infinity-和-NaN" class="headerlink" title="处理 Infinity 和 NaN"></a>处理 Infinity 和 NaN</h3><p>当计算结果超出 <code>Number.MAX_VALUE</code>，JS 会给你一个 <code>Infinity</code>。业务上怎么处理取决于场景：提示用户、做截断、还是切换到更高精度的计算方式。</p><figure class="highlight javascript"><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> num = <span class="title class_">Number</span>.<span class="property">MAX_VALUE</span> * <span class="number">2</span>;</span><br><span class="line"><span class="keyword">if</span> (num === <span class="title class_">Infinity</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">warn</span>(<span class="string">&#x27;数值超出 Number.MAX_VALUE，变为 Infinity&#x27;</span>);</span><br><span class="line">  <span class="comment">// 可以选择使用 BigInt 或第三方库进行处理</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="大数计算三方库实现原理"><a href="#大数计算三方库实现原理" class="headerlink" title="大数计算三方库实现原理"></a>大数计算三方库实现原理</h2><h3 id="数字的表示与存储"><a href="#数字的表示与存储" class="headerlink" title="数字的表示与存储"></a>数字的表示与存储</h3><p>大数库一般会用类似科学计数法的结构来存一个数：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">number</span> = sign × coefficient × <span class="number">10</span>^exponent</span><br></pre></td></tr></table></figure><ul><li><p><strong>sign</strong>：正负符号（通常用 1 或 -1 表示）。</p></li><li><p><strong>coefficient</strong>（又称为 mantissa）：一个表示数字有效位的整数或数字字符串。</p></li><li><p><strong>exponent</strong>：整数，表示系数需要乘以 10 的幂次。</p></li></ul><p>例如，数字 12345.6789 可能会被转换成：</p><ul><li><p>sign: 1</p></li><li><p>coefficient: 123456789</p></li><li><p>exponent: -4</p></li></ul><p>这样做的好处是：数字可以无限长；精度&#x2F;舍入规则也可以由库自己控制。</p><p>很多大数库内部会将 coefficient 存储为一个字符串或者一个数组（每个元素代表若干位数字），以便于进行逐位运算。通过字符串或数组，可以突破 JavaScript 内部整数最大安全值的限制，实现大数的存储与计算。</p><h3 id="算法实现"><a href="#算法实现" class="headerlink" title="算法实现"></a>算法实现</h3><p><strong>加法和减法</strong></p><p>对于加法和减法，大数库通常遵循以下步骤：</p><ol><li><p><strong>对齐指数</strong>：如果两个数的 exponent 不同，需要将它们转换到相同的指数。例如，调整系数，使得指数相同，从而使得数字能够直接相加或相减。</p></li><li><p><strong>逐位运算</strong>：将对齐后的系数进行逐位加法或减法，处理进位和借位问题。</p></li><li><p><strong>标准化结果</strong>：最后，调整结果的系数和指数，确保系数在规范范围内，并应用必要的舍入规则。</p></li></ol><p><strong>乘法</strong></p><p>乘法的实现通常涉及到对两个系数进行多位乘法：</p><ol><li><p><strong>乘以整数</strong>：把两个系数视作整数进行乘法运算，可以采用传统的乘法算法或更高效的算法（如 Karatsuba 算法）。</p></li><li><p><strong>指数相加</strong>：两个数相乘后，指数部分相加。</p></li><li><p><strong>处理进位和标准化</strong>：乘积可能会产生额外的位数，最后需要标准化结果。</p></li></ol><p><strong>除法</strong></p><p>除法的计算相对复杂，大数库可能采用类似长除法的方法来计算系数的商，并调整指数。许多库还支持设置精度和舍入模式，以保证运算结果满足用户需求。</p><p><strong>舍入和精度控制</strong></p><p>大数库通常允许用户设置运算精度和不同的舍入模式（如向上舍入、向下舍入、四舍五入等），在每次运算后对结果进行格式化，确保输出符合预期精度。</p><h3 id="代码示例：简单大数加法"><a href="#代码示例：简单大数加法" class="headerlink" title="代码示例：简单大数加法"></a>代码示例：简单大数加法</h3><p>下面是一个非常简化的示例，用来帮助理解“系数 + 指数”的思路。真实的大数库要复杂得多：要处理精度、舍入、符号、异常、性能优化等等。</p><figure class="highlight javascript"><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">// 简单大数表示：采用对象 &#123; sign, coefficient, exponent &#125;</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">BigNumber</span>(<span class="params">sign, coefficient, exponent</span>) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">sign</span> = sign; <span class="comment">// 1 或 -1</span></span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">coefficient</span> = coefficient; <span class="comment">// 字符串形式的整数</span></span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">exponent</span> = exponent; <span class="comment">// 整数</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 简单加法：假设两个数的 exponent 相同</span></span><br><span class="line"><span class="title class_">BigNumber</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">add</span> = <span class="keyword">function</span> (<span class="params">other</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">exponent</span> !== other.<span class="property">exponent</span>) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;示例仅支持相同指数的加法&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 将系数转换为大整数形式（字符串相加）</span></span><br><span class="line">  <span class="comment">// 这里简单采用内置 BigInt 来演示运算，实际库通常自己实现大整数算法</span></span><br><span class="line">  <span class="keyword">const</span> a = <span class="title class_">BigInt</span>(<span class="variable language_">this</span>.<span class="property">coefficient</span>);</span><br><span class="line">  <span class="keyword">const</span> b = <span class="title class_">BigInt</span>(other.<span class="property">coefficient</span>);</span><br><span class="line">  <span class="keyword">const</span> sum = a + b;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">BigNumber</span>(<span class="variable language_">this</span>.<span class="property">sign</span>, sum.<span class="title function_">toString</span>(), <span class="variable language_">this</span>.<span class="property">exponent</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="keyword">const</span> num1 = <span class="keyword">new</span> <span class="title class_">BigNumber</span>(<span class="number">1</span>, <span class="string">&#x27;123456789123456789&#x27;</span>, -<span class="number">4</span>);</span><br><span class="line"><span class="keyword">const</span> num2 = <span class="keyword">new</span> <span class="title class_">BigNumber</span>(<span class="number">1</span>, <span class="string">&#x27;987654321987654321&#x27;</span>, -<span class="number">4</span>);</span><br><span class="line"><span class="keyword">const</span> result = num1.<span class="title function_">add</span>(num2);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Result: <span class="subst">$&#123;result.sign * <span class="built_in">Number</span>(result.coefficient)&#125;</span>e<span class="subst">$&#123;result.exponent&#125;</span>`</span>);</span><br><span class="line"><span class="comment">// 实际输出：Result: 1.1111111111111111e-4（示例仅用于说明原理）</span></span><br></pre></td></tr></table></figure><p>在上述示例中，我们将大数表示为一个对象，包含符号、系数（以字符串存储）和指数。加法操作通过内置 BigInt 模拟，但实际库往往采用自定义的算法来避免使用 BigInt，从而兼容更多环境和满足更高的性能需求。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li><strong>只是“大整数”</strong>：能用 BigInt 就用 BigInt；接口传输优先用字符串，别强行用 Number 扛。</li><li><strong>需要“高精度小数”</strong>：上 decimal&#x2F;bignumber 类库，明确精度与舍入规则。</li><li><strong>碰到 Infinity&#x2F;NaN</strong>：先做检测与兜底，再考虑是不是要切换计算策略。</li></ul><p>希望可以帮到你！</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2023/bignumber/</id>
    <link href="https://linkdiary.com/2023/bignumber/"/>
    <published>2023-06-25T06:48:52.000Z</published>
    <summary>JS 的 Number 有上限也有精度坑：超过 `2^53-1` 就不再可靠。这里整理几种实战里常用的处理方式。</summary>
    <title>JS超过Number最大值的数怎么处理?</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Link</name>
    </author>
    <category term="实践" scheme="https://linkdiary.com/categories/%E5%AE%9E%E8%B7%B5/"/>
    <category term="工程化" scheme="https://linkdiary.com/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <content>
      <![CDATA[<div class="note blue icon-padding flat"><i class="note-icon fas fa-tachometer-alt"></i><p>页面性能优化最容易走偏的一点是：一上来就“上手段”（压缩、上 CDN、拆包），但没有度量闭环。我的习惯是先把指标跑起来（本地 + 线上），再按问题类型逐个拆解：体积、网络、渲染路径、主线程、用户感知，最后再用工程化把基线守住。</p></div><h2 id="一、先谈“怎么量”——关键指标"><a href="#一、先谈“怎么量”——关键指标" class="headerlink" title="一、先谈“怎么量”——关键指标"></a>一、先谈“怎么量”——关键指标</h2><ul><li><strong>Core Web Vitals</strong>：<ul><li><strong>LCP</strong>（Largest Contentful Paint）：最大内容绘制时间，衡量首屏主要内容何时可见。</li><li><strong>INP</strong>（Interaction to Next Paint）：交互到下一帧的时延，替代 FID 评估交互响应性。</li><li><strong>CLS</strong>（Cumulative Layout Shift）：累积布局偏移，衡量页面稳定性。</li></ul></li><li><strong>其他常用指标</strong>：TTFB、FCP、TTI、TBT、FP&#x2F;FMP、JS long task 数量等。</li></ul><p>小建议：本地 Lighthouse 先摸个底，线上用 RUM（真实用户监控）看真实分布，最好再按设备&#x2F;网络分层，不然平均值很容易骗你。</p><h2 id="二、资源体积优化（越少越好）"><a href="#二、资源体积优化（越少越好）" class="headerlink" title="二、资源体积优化（越少越好）"></a>二、资源体积优化（越少越好）</h2><h3 id="1-JavaScript：减少、拆分、可缓存"><a href="#1-JavaScript：减少、拆分、可缓存" class="headerlink" title="1. JavaScript：减少、拆分、可缓存"></a>1. JavaScript：减少、拆分、可缓存</h3><ul><li><strong>Tree Shaking &#x2F; Dead Code Elimination</strong>：确保使用 ESM、移除无用导出。</li><li><strong>Code Splitting</strong>：按路由&#x2F;功能动态加载，降低首屏负担。</li></ul><figure class="highlight javascript"><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="comment">// 路由级按需加载</span></span><br><span class="line"><span class="keyword">import</span>(<span class="string">&#x27;~/pages/settings&#x27;</span>).<span class="title function_">then</span>(<span class="function">(<span class="params">&#123; render &#125;</span>) =&gt;</span> <span class="title function_">render</span>());</span><br></pre></td></tr></table></figure><ul><li><strong>第三方依赖瘦身</strong>：<ul><li>替换大库（如 moment → dayjs&#x2F;luxon）。</li><li>只引入子模块（lodash-es 按需）。</li><li>开启 bundler 的现代产物（module&#x2F;exports 字段、esm 优先）。</li></ul></li></ul><h3 id="2-压缩与传输编码"><a href="#2-压缩与传输编码" class="headerlink" title="2. 压缩与传输编码"></a>2. 压缩与传输编码</h3><ul><li>JS&#x2F;CSS 开启代码压缩（Terser&#x2F;CSSNano）。</li><li>服务端（或 CDN）开启 <strong>Brotli</strong> 优先，Gzip 兜底。</li></ul><figure class="highlight nginx"><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="attribute">gzip</span> <span class="literal">on</span>;</span><br><span class="line"><span class="attribute">gzip_types</span> application/javascript text/css application/json image/svg+xml;</span><br><span class="line"><span class="attribute">gzip_min_length</span> <span class="number">1024</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Brotli（示例需安装 brotli 模块/CDN 支持）</span></span><br><span class="line"><span class="comment"># brotli on; brotli_types application/javascript text/css application/json image/svg+xml;</span></span><br></pre></td></tr></table></figure><h3 id="3-图片与媒体"><a href="#3-图片与媒体" class="headerlink" title="3. 图片与媒体"></a>3. 图片与媒体</h3><ul><li><strong>格式选型</strong>：优先 WebP&#x2F;AVIF，按兼容性回退。</li><li><strong>响应式与懒加载</strong>：<code>srcset/sizes</code> + <code>loading=&quot;lazy&quot;</code> + 显式 <code>width/height</code> 防 CLS。</li></ul><figure class="highlight html"><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="tag">&lt;<span class="name">img</span></span></span><br><span class="line"><span class="tag">  <span class="attr">src</span>=<span class="string">&quot;/img/hero.avif&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">srcset</span>=<span class="string">&quot;/img/hero-640.avif 640w, /img/hero-1280.avif 1280w&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">sizes</span>=<span class="string">&quot;(max-width: 768px) 100vw, 768px&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">loading</span>=<span class="string">&quot;lazy&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">fetchpriority</span>=<span class="string">&quot;high&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">width</span>=<span class="string">&quot;1280&quot;</span> <span class="attr">height</span>=<span class="string">&quot;720&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">alt</span>=<span class="string">&quot;hero&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span></span><br></pre></td></tr></table></figure><ul><li><strong>占位&#x2F;渐进</strong>：LQIP&#x2F;BlurHash；视频首帧海报 poster。</li></ul><h3 id="4-CSS-与字体"><a href="#4-CSS-与字体" class="headerlink" title="4. CSS 与字体"></a>4. CSS 与字体</h3><ul><li><strong>Critical CSS</strong>：首屏关键样式内联&#x2F;预加载；非关键样式延后应用。</li></ul><figure class="highlight html"><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="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;preload&quot;</span> <span class="attr">as</span>=<span class="string">&quot;style&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/css/critical.css&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/css/index.css&quot;</span> <span class="attr">media</span>=<span class="string">&quot;print&quot;</span> <span class="attr">onload</span>=<span class="string">&quot;this.media=&#x27;all&#x27;&quot;</span> /&gt;</span></span><br></pre></td></tr></table></figure><ul><li><strong>Purge&#x2F;Unused CSS</strong>：PurgeCSS&#x2F;Tailwind JIT 等移除未使用选择器。</li><li><strong>字体</strong>：子集化 + <code>font-display: swap</code> + 预加载。</li></ul><figure class="highlight css"><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">@font-face</span> &#123;</span><br><span class="line">  <span class="attribute">font-family</span>: <span class="string">&#x27;InterSubset&#x27;</span>;</span><br><span class="line">  <span class="attribute">src</span>: <span class="built_in">url</span>(<span class="string">&#x27;/fonts/inter-subset.woff2&#x27;</span>) <span class="built_in">format</span>(<span class="string">&#x27;woff2&#x27;</span>);</span><br><span class="line">  <span class="attribute">font-display</span>: swap;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;preload&quot;</span> <span class="attr">as</span>=<span class="string">&quot;font&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/fonts/inter-subset.woff2&quot;</span> <span class="attr">type</span>=<span class="string">&quot;font/woff2&quot;</span> <span class="attr">crossorigin</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="三、网络请求优化（更快拿到、更少往返）"><a href="#三、网络请求优化（更快拿到、更少往返）" class="headerlink" title="三、网络请求优化（更快拿到、更少往返）"></a>三、网络请求优化（更快拿到、更少往返）</h2><h3 id="1-协议与拓扑"><a href="#1-协议与拓扑" class="headerlink" title="1. 协议与拓扑"></a>1. 协议与拓扑</h3><ul><li><strong>HTTP&#x2F;2&#x2F;3</strong> 多路复用、头部压缩；尽量走 <strong>CDN</strong>（就近、缓存、压缩、TLS 优化）。</li><li>合理的域名拆分，控制并发与连接复用。</li></ul><h3 id="2-缓存策略"><a href="#2-缓存策略" class="headerlink" title="2. 缓存策略"></a>2. 缓存策略</h3><ul><li><strong>静态资源指纹化</strong> + <code>Cache-Control: public, max-age=31536000, immutable</code>。</li><li>非指纹接口配合 ETag&#x2F;Last-Modified，短缓存 + 协商。</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">Cache-Control: public, max-age=31536000, immutable</span><br><span class="line">ETag: &quot;68f4-5f1a...&quot;</span><br></pre></td></tr></table></figure><h3 id="3-预连接与预加载"><a href="#3-预连接与预加载" class="headerlink" title="3. 预连接与预加载"></a>3. 预连接与预加载</h3><figure class="highlight html"><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="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;dns-prefetch&quot;</span> <span class="attr">href</span>=<span class="string">&quot;//cdn.example.com&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;preconnect&quot;</span> <span class="attr">href</span>=<span class="string">&quot;https://cdn.example.com&quot;</span> <span class="attr">crossorigin</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;preload&quot;</span> <span class="attr">as</span>=<span class="string">&quot;script&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/js/main.js&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;prefetch&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/js/route-settings.chunk.js&quot;</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="4-阻塞外链与脚本"><a href="#4-阻塞外链与脚本" class="headerlink" title="4. 阻塞外链与脚本"></a>4. 阻塞外链与脚本</h3><ul><li><code>defer</code> 优先、慎用 <code>async</code>（有依赖顺序时）。</li></ul><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;/js/main.js&quot;</span> <span class="attr">defer</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><ul><li>延迟&#x2F;条件加载三方脚本（埋点、广告、A&#x2F;B 等），必要时沙箱隔离。</li><li><strong>Service Worker</strong>：离线缓存、预缓存关键路径，回源兜底。</li></ul><h2 id="四、渲染与主线程优化（更快可见、更快可交互）"><a href="#四、渲染与主线程优化（更快可见、更快可交互）" class="headerlink" title="四、渲染与主线程优化（更快可见、更快可交互）"></a>四、渲染与主线程优化（更快可见、更快可交互）</h2><h3 id="1-关键渲染路径（CRP）"><a href="#1-关键渲染路径（CRP）" class="headerlink" title="1. 关键渲染路径（CRP）"></a>1. 关键渲染路径（CRP）</h3><ul><li>减少首屏 JS 执行量与 CSS 阻塞；SSR&#x2F;SSG 提升首屏可见性；Hydration 分片或延后。</li><li>精简首屏 DOM 复杂度，避免大列表一次性渲染（使用虚拟列表&#x2F;分页）。</li></ul><h3 id="2-主线程“长任务”治理"><a href="#2-主线程“长任务”治理" class="headerlink" title="2. 主线程“长任务”治理"></a>2. 主线程“长任务”治理</h3><ul><li>将 &gt;50ms 的长任务拆分到多个微任务&#x2F;宏任务，或丢给 <strong>Web Worker</strong>。</li></ul><figure class="highlight javascript"><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">// 计算密集任务放 Worker</span></span><br><span class="line"><span class="keyword">const</span> worker = <span class="keyword">new</span> <span class="title class_">Worker</span>(<span class="string">&#x27;/worker.js&#x27;</span>);</span><br><span class="line">worker.<span class="title function_">postMessage</span>(&#123; <span class="attr">type</span>: <span class="string">&#x27;calc&#x27;</span>, <span class="attr">payload</span>: bigData &#125;);</span><br></pre></td></tr></table></figure><ul><li>动画使用 <code>transform/opactiy</code>，避免触发布局；使用 <code>will-change</code> 谨慎。</li><li>事件节流&#x2F;防抖，减少同步布局抖动（Layout Thrashing）。</li></ul><h3 id="3-优先级与调度"><a href="#3-优先级与调度" class="headerlink" title="3. 优先级与调度"></a>3. 优先级与调度</h3><ul><li>使用 <code>requestIdleCallback</code> 在空闲时做非关键工作；采用现代框架的并发特性&#x2F;调度器。</li><li>网络&#x2F;渲染优先级：<code>fetchpriority=&quot;high&quot;</code> 用于首图，<code>priority hints</code> 配合资源管理。</li></ul><h2 id="五、用户感知优化（看起来更快）"><a href="#五、用户感知优化（看起来更快）" class="headerlink" title="五、用户感知优化（看起来更快）"></a>五、用户感知优化（看起来更快）</h2><ul><li><strong>Skeleton&#x2F;占位</strong>：首屏骨架、图片模糊占位，避免“白屏恐惧”。</li><li><strong>渐进展示</strong>：先上关键信息，再补充增强；列表先展示可见区域。</li><li><strong>交互回馈</strong>：按钮点击即刻反馈（禁用态&#x2F;Loading），避免用户重复操作。</li><li><strong>错误&#x2F;重试策略</strong>：弱网下的容错与降级，提供离线提示。</li></ul><figure class="highlight html"><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="comment">&lt;!-- 示例：图片首帧优先 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&quot;/img/hero.jpg&quot;</span> <span class="attr">fetchpriority</span>=<span class="string">&quot;high&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;hero&quot;</span> /&gt;</span></span><br></pre></td></tr></table></figure><h2 id="六、工程化与持续保障"><a href="#六、工程化与持续保障" class="headerlink" title="六、工程化与持续保障"></a>六、工程化与持续保障</h2><ul><li><strong>性能预算（Performance Budget）</strong>：为 JS&#x2F;CSS&#x2F;图片设定体积与指标预算，超标即失败。</li><li><strong>分析工具</strong>：webpack-bundle-analyzer、Source Map 体积审计；依赖许可证与重复库检测。</li><li><strong>CI 守门</strong>：Lighthouse CI&#x2F;Calibre&#x2F;SpeedCurve 接入流水线；PR 侧边栏展示变化。</li><li><strong>RUM 监控</strong>：上报 LCP&#x2F;CLS&#x2F;INP、资源耗时、长任务、错误栈；按端&#x2F;网络维度聚合。</li><li><strong>实验治理</strong>：灰度&#x2F;AB 测试，评估改动对指标的真实影响。</li></ul><h2 id="七、常见坑与对策"><a href="#七、常见坑与对策" class="headerlink" title="七、常见坑与对策"></a>七、常见坑与对策</h2><ul><li>过多三方脚本 → 延迟&#x2F;条件加载，或使用代理与沙箱；定期体检下线。</li><li>大而全 polyfill → 基于 UA&#x2F;feature 的差异化注入（polyfill.io&#x2F;self-host）。</li><li>单包过大 → 路由&#x2F;组件分包、公共依赖拆分、动态导入。</li><li>Reflow 频繁 → 批量读写 DOM（FastDom）、动画走合成层。</li><li>图片失衡 → 统一中台（裁剪&#x2F;压缩&#x2F;格式自动），规范宽高与密度。</li></ul><h2 id="八、落地清单（Checklist）"><a href="#八、落地清单（Checklist）" class="headerlink" title="八、落地清单（Checklist）"></a>八、落地清单（Checklist）</h2><ul><li>已建立 CWV 与性能预算基线，并接入线上 RUM。</li><li>首屏 JS &lt; 170KB（gz&#x2F;br 后）、首屏 CSS &lt; 30KB；路由级分包。</li><li>图片 WebP&#x2F;AVIF + 懒加载 + 宽高&#x2F;占位；字体子集化 + preload。</li><li>静态资源带指纹且长缓存；接口协商缓存；预连接&#x2F;预加载关键资源。</li><li>关键渲染路径可控：Critical CSS、defer 脚本、SSR&#x2F;SSG 可选。</li><li>长任务监控 &lt; 50ms；Worker&#x2F;调度器落地；虚拟列表替换大渲染。</li></ul><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>性能优化更像一套长期的“体检 + 治疗 + 复诊”：先看指标和分布，找到最拖后腿的那一项，再用最省事的手段把它拉回基线。把资源做小、请求更稳、渲染更轻、感知更顺，最后都能体现在用户体感和业务指标上。</p><p><strong>Happy Coding!</strong></p>]]>
    </content>
    <id>https://linkdiary.com/2023/page-performance/</id>
    <link href="https://linkdiary.com/2023/page-performance/"/>
    <published>2023-06-06T01:23:56.000Z</published>
    <summary>页面优化别只盯着“压缩资源”。我更习惯按度量→定位→治理来拆：体积、网络、渲染、感知和工程化兜底。</summary>
    <title>前端页面性能优化应该从哪些方向来思考?</title>
    <updated>2026-05-31T09:20:18.995Z</updated>
  </entry>
</feed>
