<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Shang Chun</title>
  
  
  <link href="https://springuper.github.io/atom.xml" rel="self"/>
  
  <link href="https://springuper.github.io/"/>
  <updated>2026-10-03T09:20:43.753Z</updated>
  <id>https://springuper.github.io/</id>
  
  <author>
    <name>Chun Shang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Playwright 赢的不是测试：它赢的是“被调用的能力”</title>
    <link href="https://springuper.github.io/playwright-click-anatomy/"/>
    <id>https://springuper.github.io/playwright-click-anatomy/</id>
    <published>2026-10-03T20:00:00.000Z</published>
    <updated>2026-10-03T09:20:43.753Z</updated>
    
    <content type="html"><![CDATA[<p>如果你这两年留意过前端测试圈，大概会注意到一件事：新项目的端到端测试，默认都写成 Playwright 了。可这件事有点说不通：Cypress 也会自动等待，它的检查项甚至比 Playwright 还多，代码读起来也更舒服。</p><p>所以我想搞清楚一个问题：<strong>这一轮换代，为什么是 Playwright？</strong></p><p>趋势本身很清楚：2025 年的行业调查里，它的使用率第一次超过 Cypress（50% 对 47%），而最新的周下载量是它的 17 倍。但数字只能说明趋势，说明不了原因。</p><p>功能表也回答不了，得往下一层看：那一行 <code>click()</code> 在真的落下之前，究竟检查了什么。我拿一颗 600 毫秒之后才可点的按钮当尺子，把它拆成四道关：怎么找到元素、怎么判断能点、怎么把动作送进浏览器、失败之后留下什么。四道关看完，再回头说这两年的事。</p><span id="more"></span><p>被测的页面很小，存成 HTML 双击就能打开：一个标题、一颗 600 毫秒之后才可点的按钮、一行结果。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!doctype <span class="meta-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;zh&quot;</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">h1</span>&gt;</span>下单<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">id</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">disabled</span>&gt;</span>提交订单<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;result&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">    <span class="comment">// 模拟一个 600 毫秒才回来的接口：这期间按钮是禁用的</span></span></span><br><span class="line"><span class="javascript">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123; <span class="built_in">document</span>.getElementById(<span class="string">&#x27;submit&#x27;</span>).disabled = <span class="literal">false</span>; &#125;, <span class="number">600</span>);</span></span><br><span class="line"><span class="javascript"></span></span><br><span class="line"><span class="javascript">    <span class="built_in">document</span>.getElementById(<span class="string">&#x27;submit&#x27;</span>).addEventListener(<span class="string">&#x27;click&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="javascript">      <span class="built_in">document</span>.getElementById(<span class="string">&#x27;result&#x27;</span>).textContent = <span class="string">&#x27;已提交&#x27;</span>;</span></span><br><span class="line"><span class="javascript">    &#125;);</span></span><br><span class="line"><span class="javascript">  </span><span class="tag">&lt;/<span class="name">script</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></pre></td></tr></table></figure><p>判断成没成功只看一件事：最后那行 <code>&lt;p id=&quot;result&quot;&gt;</code> 里有没有出现「已提交」。</p><p><img src="../images/order-fixture-states.png" alt="正文那段 fixture 的三个状态：刚打开时按钮禁用、600 毫秒后恢复可点、点下去结果栏才出现「已提交」"><em>被测页面就长这样（Playwright 1.57.0 + Chromium，2026-10-01 实拍，360 像素宽视口）：同一个脚本的三种状态，下面四道关对着的都是这一张页面</em></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> page.mouse.click(<span class="number">320</span>, <span class="number">210</span>);                             <span class="comment">// ① 按坐标点</span></span><br><span class="line"><span class="keyword">await</span> page.locator(<span class="string">&#x27;#submit&#x27;</span>).dispatchEvent(<span class="string">&#x27;click&#x27;</span>);         <span class="comment">// ② 直接派发事件</span></span><br><span class="line"><span class="keyword">await</span> page.getByRole(<span class="string">&#x27;button&#x27;</span>, &#123; <span class="attr">name</span>: <span class="string">&#x27;提交订单&#x27;</span> &#125;).click();   <span class="comment">// ③ 正常点</span></span><br></pre></td></tr></table></figure><p>只有 ③ 会等。① 什么也没发生：按钮还禁用着，浏览器把那次鼠标事件吞了。② 结果栏变绿了，可那其实是假的，按钮当时根本点不动。这篇文章里的四道关，拆的就是 ③ 的那一行。</p><h2 id="一、第一关：它怎么知道“找到了”"><a href="#一、第一关：它怎么知道“找到了”" class="headerlink" title="一、第一关：它怎么知道“找到了”"></a>一、第一关：它怎么知道“找到了”</h2><p>我们习惯说“拿到一个元素”，但在 Playwright 里，<code>page.getByRole(&#39;button&#39;, &#123; name: &#39;提交订单&#39; &#125;)</code> 拿到的不是一个元素，而是一句描述。官方管它叫 locator，定义是“一种在任何时刻都能找到元素的方式”（见 <a href="https://playwright.dev/docs/locators">Locators</a>）。</p><p>这句话听着像修辞，其实有个很实在的后果：每次要动手之前，它会拿这句描述重新解析一遍。所以我那个实验才有意义。把 class 从构建哈希 <code>btn-primary-9f2c1a</code> 改成 <code>btn-primary-4b8e77</code>，模拟一次重新构建，<code>page.click(&#39;.btn-primary-9f2c1a&#39;)</code> 当场超时，报 <code>Timeout 1200ms exceeded</code>；那句按角色和名字写的描述照样命中。</p><p>解析出来之后其实还有一道更严的检查：必须恰好命中一个。这就是“找到”在这套实现里的定义。一句描述如果同时匹配到 20 个元素，它不会随便挑一个，而是停下来告诉你匹配到了 20 个。（这个决定的好处，第四关会看到。）</p><p>那“角色是 button、名字是提交订单”这些词从哪儿来？来自浏览器的可访问性树：浏览器会把页面另外解析成一棵树，每个节点带三样东西，角色（role）、名字（name）和状态（state）。屏幕阅读器读的就是它，<code>getByRole</code> 读的也是它，第六节那个 ARIA 快照还是它。</p><p>原生标签自带隐式的 role（<code>&lt;button&gt;</code>、<code>&lt;a href&gt;</code>、<code>&lt;input type=&quot;checkbox&quot;&gt;</code> 都有），自定义组件就得自己补上：<code>aria-label</code>、<code>aria-labelledby</code>，或者 <code>&lt;label for&gt;</code>。我拿一个只用 <code>&lt;div onclick&gt;</code> 拼的按钮试过：<code>getByRole(&#39;button&#39;).count()</code> 是 0，它在快照里降级成了一行普通文本；补上 <code>role=&quot;button&quot;</code> 之后才是 1。</p><p>所以第一关交出来的东西，其实是一句“此刻唯一命中的描述”。反过来，那种“真的抓到一个元素”的用法（<code>ElementHandle</code>）被官方<a href="https://playwright.dev/docs/handles">标上了 Discouraged</a>。</p><h2 id="二、第二关：它怎么判断“能点了”"><a href="#二、第二关：它怎么判断“能点了”" class="headerlink" title="二、第二关：它怎么判断“能点了”"></a>二、第二关：它怎么判断“能点了”</h2><p>找到了不等于能点。这一关是 Playwright 最出名的地方，官方叫 <a href="https://playwright.dev/docs/actionability">actionability</a>，原话是：</p><blockquote><p>It auto-waits for all the relevant checks to pass and only then performs the requested action.</p></blockquote><p>翻成人话：动手之前，它会把下面四条逐个等到成立。</p><table><thead><tr><th>条件</th><th>官方定义（人话版）</th></tr></thead><tbody><tr><td>Visible</td><td>有非空 bounding box，且不是 <code>visibility: hidden</code>；<code>opacity: 0</code> 算可见</td></tr><tr><td>Stable</td><td>连续两帧 bounding box 不变（还在动的元素不点）</td></tr><tr><td>Receives Events</td><td>命中测试时这个点上真的是它，不是被别的元素盖住</td></tr><tr><td>Enabled</td><td>没 <code>disabled</code>，祖先也没有 <code>aria-disabled</code></td></tr></tbody></table><p>四条里我最喜欢第二条。“连续两帧不变”这种抠到帧的定义，等于把老工程师嘴里那句“等它别动了再点”直接写成了代码。这一关干的就是这件事：<strong>把人的经验变成可判定的条件。</strong></p><p>回到那颗按钮，这一关跑完是 858 毫秒。它倒不是睡得久，是一直在等条件成立。</p><p>这里得补一句公道话，不然容易被读成 Playwright 的独家本事：自动等待其实不是它发明的。Cypress 早就有，检查项比这四个还多（多出 detached、readonly、covering 三项），它也会盯着 DOM 不断重跑查询（<a href="https://docs.cypress.io/app/core-concepts/interacting-with-elements">官方原文</a>：*”Cypress will watch the DOM - re-running the queries that yielded the current subject…”*）。我实测过，一条完全不带等待的 Cypress 用例照样通过，耗时 846 毫秒，和 Playwright 的 858 毫秒几乎一样。</p><p>真正没有它的是上一代那两家。<a href="https://www.selenium.dev/documentation/webdriver/waits/">Selenium 的文档</a>把话说得很直白：显式等待是“loops added to the code that poll the application for a specific condition”，说白了就是一段写在代码里、反复轮询到条件成立的循环；它那一页自己还摆了个 <code>sleep()</code> 的例子，里面写的是 <code>Thread.sleep(1000)</code>。<a href="https://pptr.dev/faq">Puppeteer</a> 也一样，判据得自己写：</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="keyword">await</span> page.waitForFunction(<span class="function">() =&gt;</span> &#123;          <span class="comment">// 判据得自己写</span></span><br><span class="line">  <span class="keyword">const</span> btn = <span class="built_in">document</span>.querySelector(<span class="string">&#x27;#submit&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span> btn &amp;&amp; !btn.disabled;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="keyword">await</span> page.click(<span class="string">&#x27;#submit&#x27;</span>);</span><br></pre></td></tr></table></figure><p>把中间这三行删掉会怎样？我倒是真跑了一遍：脚本不报错，输出是空的。按钮还禁用着，浏览器把那次点击吞了。</p><p>所以第一关和第二关合起来说的是一件事：<strong>“什么时候能动”这件判断，从使用者的代码里搬进了工具里。</strong> 差别不在“等不等”，而在那一行 <code>click()</code> 里到底有没有检查。</p><h2 id="三、回头说开篇那个“假成功”"><a href="#三、回头说开篇那个“假成功”" class="headerlink" title="三、回头说开篇那个“假成功”"></a>三、回头说开篇那个“假成功”</h2><p>开篇那三种点法里，② 为什么能“成功”？因为 actionability 有四条检查，但也留了后门。官方文档写得倒是很直白：给 <code>click()</code> 传 <code>force: true</code>，就会跳过“非必要”的检查，比如“这个点上真的能收到点击事件吗”这一条。<code>dispatchEvent(&#39;click&#39;)</code> 更彻底，它连点击都不模拟，直接把事件塞给元素。</p><p>三种点法摆在一起是这样：</p><table><thead><tr><th>点法</th><th>页面结果</th><th>说明</th></tr></thead><tbody><tr><td>按坐标立刻点 <code>page.mouse.click(x, y)</code></td><td><code>（未提交）</code></td><td>按钮还禁用着，真实鼠标事件被吞掉</td></tr><tr><td><code>locator.dispatchEvent(&#39;click&#39;)</code></td><td><code>（已提交）</code></td><td>事件绕过了检查，看着成功，其实是假成功</td></tr><tr><td><code>getByRole(&#39;button&#39;).click()</code></td><td><code>（已提交）</code>，等了 858 毫秒</td><td>条件成立才动手</td></tr></tbody></table><p><img src="../images/three-clicks.gif" alt="同一颗按钮的三种点法：按坐标点没反应、dispatchEvent 假成功、getByRole 等到条件成立才点"><em>录制自真实运行（Playwright 1.57 + Chromium）：① 按坐标点，按钮还在禁用态，什么也没发生；② <code>dispatchEvent</code> 把事件直接发进去，结果栏变绿，其实是假成功；③ <code>click()</code> 等到按钮可用才动手</em></p><p>我把这三条路都跑了一遍。按钮还禁用时，按坐标点没反应，用 DOM 自己的 <code>element.click()</code> 也没反应，唯独 <code>dispatchEvent(&#39;click&#39;)</code> 让结果栏变成了「已提交」。</p><p>原因是浏览器的“禁用”只拦两样东西：真实的指针事件，和元素自己的 <code>click()</code>。它拦不住直接用事件接口投递的那一下。所以这个“假成功”不是“什么都没发生”：监听器执行了，DOM 也变了（这个页面里没有表单，“已提交”就是结果栏那行字被改了），只不过<strong>它走的是一条真实用户走不到的路</strong>。真实用户那一刻点不动这颗按钮，你的用例却认为点成功了。</p><p><strong>不做检查的自动化，最危险的地方不是失败，而是它能给你一个假成功。</strong> 一条“禁用状态也照样点进去”的用例会在 CI 里一直绿着，直到上线被真实用户教做人。</p><h2 id="四、第三关：动作怎么送进浏览器"><a href="#四、第三关：动作怎么送进浏览器" class="headerlink" title="四、第三关：动作怎么送进浏览器"></a>四、第三关：动作怎么送进浏览器</h2><p>判断完了，动作得真的送出去。这一段路平时看不见，但它决定了这个工具最后能长成什么样：动作经由什么送出去，最后又落在哪个浏览器上。</p><p>先说 Cypress，它走的是另一条路。装完 <code>node_modules/cypress</code> 只有 7.3 MB，可真正的执行体是它下载到缓存里的 641 MB 应用：测试是被这个应用带着跑的，而不是被一行 <code>require</code> 拉起来的库。官方文档把原因说得很清楚：*”Cypress is executed in the same run loop as your application.”*（见 <a href="https://docs.cypress.io/app/get-started/why-cypress">Why Cypress</a>）</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></pre></td><td class="code"><pre><span class="line">Cypress 应用（一个自带浏览器）</span><br><span class="line">├── 测试代码 + Cypress 驱动</span><br><span class="line">└── iframe：被测应用</span><br><span class="line">    （两者共用一个事件循环）</span><br></pre></td></tr></table></figure><p>一个浏览器里同时住着测试代码和被测应用，两者共用一个事件循环。这个设计的确换来了很好的调试体验（后面会替它说话），代价是它只能以“一个应用”的形式存在：模块 API 文档里一共只有三个函数，<code>cypress.run()</code>、<code>cypress.open()</code>、<code>cypress.cli.parseRunArguments()</code>（见 <a href="https://docs.cypress.io/app/references/module-api">Module API</a>）。你能用 Node 启动一次运行，但拿不到一个页面：没有 <code>page</code>，也没有任何能在自己进程里操作的对象。</p><p>Playwright 反过来。<code>@playwright/test</code> 是一个测试 runner，但它建在 <code>playwright-core</code> 这个库上，<code>chromium.launch()</code> 在任何 Node 进程里都能跑起来：你可以在自己的脚本里开一个浏览器，做完事再关掉，全程不进 runner。</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></pre></td><td class="code"><pre><span class="line">你的进程（测试，或任意脚本）</span><br><span class="line">   └── browser / page 对象</span><br><span class="line">        │  Playwright 自有协议</span><br><span class="line">        ▼</span><br><span class="line">   浏览器进程（三选一）</span><br><span class="line">   ├── Chromium</span><br><span class="line">   ├── Firefox</span><br><span class="line">   └── WebKit</span><br></pre></td></tr></table></figure><p>（Python、Java 这类绑定中间还会多一个 driver 进程。）</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></pre></td><td class="code"><pre><span class="line">~/Library/Caches/ms-playwright/</span><br><span class="line">├── chromium-1200</span><br><span class="line">├── firefox-1497</span><br><span class="line">└── webkit-2227     # 真 WebKit，不是换皮</span><br></pre></td></tr></table></figure><p>这里的“一视同仁”指三个内核都由同一套 API 支持，而不是“能不能跑起来”：Cypress 的 WebKit 至今标着 experimental，Puppeteer 干脆没有 WebKit。</p><p>三个内核共用一套 API 这件事，看着像开个 flag 就行，其实翻一遍是三层叠起来的。</p><p>第一层，语言绑定和浏览器之间隔了一层协议。JS、Python、Java 里调的是同一份实现，通过 Playwright 自有协议（仓库里的 <code>packages/protocol/spec/*.yml</code>）跟浏览器说话。</p><p>第二层，三个内核走三条通道，要的东西却不一样。这张表是整件事的关键：</p><table><thead><tr><th>内核</th><th>要不要打补丁</th><th>依据</th></tr></thead><tbody><tr><td>Chromium</td><td>不要</td><td>它原生就说 CDP。Playwright 用开源 Chromium 构建，甚至领先品牌版一个大版本</td></tr><tr><td>Firefox</td><td>要</td><td>走 <code>-juggler-pipe</code>；Juggler 就是当年 Firefox 为 Puppeteer 做的那套自动化协议，官方文档也明说用不了品牌版 Firefox</td></tr><tr><td>WebKit</td><td>要，而且不是 Safari</td><td>走 <code>--inspector-pipe</code>，补丁在仓库 <code>browser_patches/</code> 下。官方原话：*”Playwright’s WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari”*，它测的是引擎，不是 Safari 那个 App</td></tr></tbody></table><p>第三层，也是最关键的一层：它自己编译并维护内核。上面那两个 flag 不是“开关”，而是<strong>只有打过补丁的构建里才存在的通道</strong>。Playwright 每次发版同步更新三个内核的版本，<code>npx playwright install</code> 下载的就是这些自定义构建，我本机这份缓存已经 1.0 GB：chromium 324 MB、webkit 275 MB、firefox 253 MB，另有 headless shell 与 ffmpeg。</p><p>只跑一个内核其实不必这么重。<code>npx playwright install chromium</code> 的下载量约 250 MB，CI 上加个 <code>--only-shell</code> 能压到 90 MB；一个字节都不下也行：配上 <code>channel: &#39;chrome&#39;</code>，直接用机器上现成的 Chrome。</p><p>那为什么别人做不到？其实不是开不了 flag，是改不了内核。</p><ul><li>Cypress 把驱动注入浏览器、与测试同处一个事件循环，内核不许注入它就没办法，自带不了打过补丁的 WebKit（反过来，这也是它的 devtools 联动调试更顺手的原因。Gleb Bahmutov 的<a href="https://glebbahmutov.com/blog/cy-vs-pw-browser/">个人观点</a>，不是官方结论）。</li><li>Puppeteer 是 CDP 的参考实现，不需要改内核。不过这里还有段前情：Firefox 那边当年就有一条专门为 Puppeteer 做的自动化协议（<a href="https://github.com/puppeteer/juggler"><code>puppeteer/juggler</code></a>，2020 年 3 月之后就停了），Puppeteer 还据此发过一个<a href="https://github.com/puppeteer/puppeteer/blob/7f7887ed11930f96cb64bb086fad5c29086b8ef2/experimental/puppeteer-firefox/README.md">自带 Firefox 构建的原型包</a>，后来标注废弃；正经的 Firefox 支持等到 2024 年的 v23，用的还是 W3C 标准的 WebDriver BiDi。能不能做是一回事，要不要长年养两个内核的分支是另一回事。</li><li>Selenium 走的是相反的路线：它从一段跑在浏览器里的 JavaScript 起家（<a href="https://www.selenium.dev/history/">官方 History</a>），因为出不了同源策略那道墙，才改成把浏览器当远端、中间放一台代理 server（Selenium RC），最后把这条路走成了公共契约（WebDriver，<a href="https://www.w3.org/TR/webdriver1/">Level 1 在 2018 年成了 W3C Recommendation</a>）：覆盖面不由任何一家供给，代价是契约不替你管会话和等待。</li></ul><p>代价倒也说清楚：这套“连内核一起维护”的赌注，换来了三内核一致和额外的自动化能力，付出的是每次升级重下几百 MB 的自定义浏览器，外加内核补丁的长年维护。</p><p><strong>能当零件用，还是只能当整机用，这是 Cypress 和 Playwright 的分界线。</strong> 所谓“一层能力”，不是 API 多，而是它可以脱离原来的应用形态，被另一个程序直接拿起来、观察、调用、组合、复现。这一关最要紧的产出就是这个：动作经由一条自有通道送出去，而这条通道可以被任何进程调用。</p><blockquote><p>口径：架构部分来自官方文档与仓库源码。“Juggler”这个词官方文档从不使用，只出现在源码与补丁目录里，属于源码级证据，别当成官方术语引用。</p></blockquote><h2 id="五、第四关：失败留下了什么"><a href="#五、第四关：失败留下了什么" class="headerlink" title="五、第四关：失败留下了什么"></a>五、第四关：失败留下了什么</h2><p>前三关讲的是动作怎么成功。可在 CI 上，你更常面对的是失败。</p><p>测试失败时，Playwright 会给你一个 <code>trace.zip</code>，里面装着每一步的动作与耗时、动作前后的全量 DOM 快照、截图、源码、日志和网络记录（见 <a href="https://playwright.dev/docs/trace-viewer">Trace Viewer</a>）。它把“一次性现象”变成了一个文件：能塞进 CI 产物，能在别人机器上打开，从 1.59 起还能直接用命令读（<code>npx playwright trace</code>）。</p><p>这里不妨先说人的收益，因为它跟 AI 没关系：以前 CI 挂了，你得把那台机器复现出来；现在工件跟着失败一起被存下来。至于模型也能读它，那是顺带的。</p><p><img src="../images/trace-viewer.png" alt="Playwright Trace Viewer：左侧动作列表里那步失败的点击被标红选中，右侧是当时的页面快照，底部 Errors 面板里是报错正文与 Copy prompt 按钮"><em>Trace Viewer 实拍（Playwright 1.57.0，2026-09-19）：左边那步失败的 Click 被标红选中，右边是当时的页面快照，底部 Errors 面板给出报错正文与 Copy prompt 按钮</em></p><p>然后是我最喜欢的那段报错原文。我构造了一个 20 行订单列表，故意用模糊定位去点它：</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></pre></td><td class="code"><pre><span class="line">locator.click: Error: strict mode violation: getByRole(&#x27;button&#x27;, &#123; name: &#x27;查看&#x27; &#125;) resolved to 20 elements:</span><br><span class="line">    1) &lt;button aria-label=&quot;查看订单 SO-1001 详情&quot; ...&gt; aka getByRole(&#x27;button&#x27;, &#123; name: &#x27;查看订单 SO-1001 详情&#x27; &#125;)</span><br><span class="line">    2) &lt;button aria-label=&quot;查看订单 SO-1002 详情&quot; ...&gt; aka getByRole(&#x27;button&#x27;, &#123; name: &#x27;查看订单 SO-1002 详情&#x27; &#125;)</span><br><span class="line">    3) &lt;button aria-label=&quot;查看订单 SO-1003 详情&quot; ...&gt; aka getByRole(&#x27;button&#x27;, &#123; name: &#x27;查看订单 SO-1003 详情&#x27; &#125;)</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p>第一关那道“必须恰好命中一个”的检查，在这里的确显出价值了。它不只是说“你错了”：它说明了为什么错（匹配到 20 个），给出了每个候选的真实身份，最关键的是那句 <code>aka</code>，把改正后的 locator 写法一条条列给你了。这不是给人看的礼貌提示，这是一份写好的补丁。而且这大概不是巧合：从 1.51 起，Playwright 在 HTML 报告、Trace Viewer 和 UI Mode 的报错旁都放了一个按钮，叫 Copy prompt（见<a href="https://playwright.dev/docs/release-notes">发布说明</a>）。</p><p>修复的前提是复现：一个失败如果只活在那台机器上，换个人（或者换个 agent）就只能靠猜。好在它现在有了载体，现场在 <code>trace.zip</code> 里，该点哪儿写在 locator 里。于是同一个失败能在 CI 上产生，在本地打开，也能被另一个程序读。</p><p><strong>可序列化不是“能被自动修复”的保证，但它是前提。</strong></p><h2 id="六、为什么是这两年"><a href="#六、为什么是这两年" class="headerlink" title="六、为什么是这两年"></a>六、为什么是这两年</h2><p>四道关讲完了。这四件事其实 2020 年就做完了，只不过那时它们的读者只有人。为什么会用的机器多起来之后，它们突然都变得重要？</p><table><thead><tr><th>四道关</th><th>对人</th><th>对 Agent</th></tr></thead><tbody><tr><td>找到元素（locator 是描述）</td><td>不用记 class 哈希</td><td>一句能生成、能重放的描述</td></tr><tr><td>判断能点（actionability）</td><td>不用写等待</td><td>动作有确定性</td></tr><tr><td>送进浏览器（自有协议）</td><td>一套 API 跑三个内核</td><td>能被任何进程调用</td></tr><tr><td>留下现场（trace）</td><td>CI 挂了能复现</td><td>失败是可传递的工件</td></tr></tbody></table><p>换句话说，技术的价值常常要等另一个时代来证明。Playwright 不是为 agent 发明的，但它恰好把这四样东西都备齐了。</p><p>先把官方自己的动作排一排（来源：<a href="https://playwright.dev/docs/release-notes">发布说明</a>，日期取自 npm registry 的发布时间）：</p><table><thead><tr><th>版本</th><th>时间</th><th>与“给模型用”有关的动作</th></tr></thead><tbody><tr><td>1.49</td><td>2024-11-18</td><td><code>toMatchAriaSnapshot</code> 断言落地</td></tr><tr><td>1.51</td><td>2025-03-06</td><td>报错旁出现 Copy prompt 按钮</td></tr><tr><td>1.56</td><td>2025-10-06</td><td><a href="https://playwright.dev/docs/test-agents">Test Agents</a>（planner / generator / healer）</td></tr><tr><td>1.57</td><td>2025-11-25</td><td>移除 <code>page.accessibility</code>，把能力收拢到结构化快照上</td></tr><tr><td>1.59</td><td>2026-04-01</td><td><code>npx playwright trace</code>：命令行里分析 trace，给 agent 用</td></tr><tr><td>1.60</td><td>2026-05-11</td><td>给快照加 <code>boxes</code> 选项，官方措辞：*”useful for AI consumption”*</td></tr><tr><td>1.62</td><td>2026-07-24</td><td>把 MCP server 与 playwright-cli 打包进来（<code>npx playwright mcp</code>）</td></tr><tr><td>1.63</td><td>2026-09-04</td><td>trace 里记录 aria 快照，Trace Viewer 新增 Display Aria 模式</td></tr></tbody></table><p>2025 年 3 月，微软把 Playwright 包成了一个 MCP server（<a href="https://github.com/microsoft/playwright-mcp">playwright-mcp</a>，建仓到 2026-10-03 已 37,776 star）。它的 README 第一段就把立场说透了：</p><blockquote><p><em>“enables LLMs to interact with web pages through structured accessibility snapshots, bypassing the need for screenshots or visually-tuned models.”</em></p></blockquote><p>再看它给模型的工具描述：<code>browser_snapshot</code> 的说明是 *”this is better than screenshot”*，而 <code>browser_take_screenshot</code> 是 *”You can’t perform actions based on the screenshot”*。（视觉能力要显式开：<code>--caps=vision</code>。）</p><p>那“结构化”到底省了多少？官方其实没有给过量化声明，所以我自己量了一次：同一张组件库风格的页面（20 行表格、class 哈希、内联 style、<code>__NEXT_DATA__</code>），<code>page.content()</code> 出来是 12,805 个字符，<code>locator.ariaSnapshot()</code>（<a href="https://playwright.dev/docs/aria-snapshots">ARIA snapshots</a>）出来是 2,286 个，17.9%。</p><p>但体积其实只是副产品，形态才是重点。不妨看一眼 ARIA 快照长什么样：</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></pre></td><td class="code"><pre><span class="line">- heading &quot;订单工作台&quot; [level=1]</span><br><span class="line">- form &quot;筛选&quot;:</span><br><span class="line">  - textbox &quot;关键词&quot;:</span><br><span class="line">    - /placeholder: 输入订单号</span><br><span class="line">  - combobox &quot;状态&quot;:</span><br><span class="line">    - option &quot;全部&quot; [selected]</span><br><span class="line">- row &quot;SO-1001 张三 ¥1,280.00 待发货&quot;</span><br></pre></td></tr></table></figure><p>没有 <code>css-1x1q7</code>，没有 <code>ant-table-cell</code>，没有构建产物留下的哈希。模型拿到的是词汇表（role、name、state），而不是渲染残渣，这恰好就是 <code>getByRole</code> 需要的输入：同一个抽象层，人和模型都能读。人看的是渲染出来的界面，模型看的是同一棵树，换的是观察方式，不是那一层。</p><p><img src="../images/aria-vs-pixels.png" alt="左边是页面渲染出来的样子（像素），右边是同一个页面的 ARIA 快照（结构）"><em>同一个页面、同一时刻的两种表示：左边是人看到的像素，右边是模型读到的结构（Playwright 1.57.0，2026-10-01 复现并截图；左图是 620 像素宽视口的页面顶部 8 行）</em></p><p>不过这里有个反转得说。同样是官方 README，在 2026 年给出了一个降温的说法：playwright-mcp 现在推荐 coding agent 改用 CLI + Skills（<a href="https://github.com/microsoft/playwright-cli">microsoft/playwright-cli</a>），理由是 *”…avoid loading large tool schemas and verbose accessibility trees into the model context.”*。连 accessibility tree 都会被嫌啰嗦。</p><p>这句话其实反而让整篇文章的论点更稳，但得说清楚它稳在哪：前面我量出 ARIA 快照比 HTML 小 5.6 倍，官方现在又说它依然太占地方。两件事不矛盾，因为被时代选中的从来不是“小”，而是可裁剪：2,286 个字符照样塞不进上下文，但它可以被切片、按需取一部分，HTML 那 12,805 个字符不行。</p><p>所以这两年真正稀缺的东西，其实是好的工具接口：能描述、能观察、能行动、能验证、能留下现场，还能被另一个程序重新消费。</p><p><strong>当“给模型消费”这几个字开始出现在发布说明里，剩下的就不用我多说了。</strong></p><h2 id="七、什么时候我不换"><a href="#七、什么时候我不换" class="headerlink" title="七、什么时候我不换"></a>七、什么时候我不换</h2><p>第六节说的是 Playwright 拿到了什么。但“一层能力”这个说法有个副作用：容易被读成“应用的形态过时了”。所以这里得把边界补上。</p><p><strong>组件测试，我仍然会先看 Cypress。</strong> 它有官方的 mounting library 和 bundler 集成（React、Vue、Angular、Svelte 都有）。Playwright 的组件测试到 1.62 才换成 <code>mount()</code> fixture 加自建 dev server 的 stories 模型，1.63 又宣布三个 <code>experimental-ct-*</code> 包不再更新。对以组件为主的团队来说，迁移成本是实打实的。</p><p><strong>调试联动性，是 Playwright 的真实短板。</strong> Cypress 的测试和应用跑在同一个 JS 事件循环里，在 devtools 里下一个断点，两边会一起停住；Playwright 的 runner 和应用则是两个进程，联动调试要麻烦得多。（口径：这是 Gleb Bahmutov 的<a href="https://glebbahmutov.com/blog/cy-vs-pw-browser/">个人观点</a>，不是官方结论，但的确是内行话。）</p><p><strong>另外两类场景也别硬换</strong>：Selenium 的六种语言绑定加 Grid 编排，是多语言或企业遗留团队的结构性优势；纯抓取和轻量脚本，谁也不想先装一个测试运行器，Puppeteer 在这条路上仍然稳。</p><p>还有一件得坦白说：Playwright 不会自动消灭 flaky 测试。我手头那个用 <code>@playwright/test</code> 的项目里就留着反面教材：</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> firstHistoryItem = page.locator(<span class="string">&#x27;.home-history-item&#x27;</span>).first();</span><br><span class="line"><span class="keyword">await</span> page.waitForTimeout(<span class="number">1000</span>);</span><br></pre></td></tr></table></figure><p><code>waitForTimeout(1000)</code> 和开头那个 <code>sleep(800)</code> 是同一种东西，只是换了个更时髦的名字。<strong>换工具不会消灭 flake，它只是把 flake 从“语言层面”挪到了“用法层面”。</strong> 换一副鞍具也不会自动让马跑得更快（“鞍具”这个说法来自上一篇横评《<a href="https://springuper.github.io/agent-harness-comparison/">给大脑配一副好鞍具</a>》，再往前那篇《<a href="https://springuper.github.io/pi-harness-anatomy/">积木，而非成品</a>》拆的是同一个问题的另一个样本）。</p><p>所以这一节的结论是：<strong>Cypress 不是被打败的，是被“换了用法”绕过去的。</strong> 你如果只写给人看的组件测试，它仍然是好选择；你要的如果是一层能被别的程序消费的能力，那现在只有 Playwright 够用。这两件事不是同一个问题的两个答案，是两个问题。</p><h2 id="八、把数字摆齐"><a href="#八、把数字摆齐" class="headerlink" title="八、把数字摆齐"></a>八、把数字摆齐</h2><p>坦白说，机制讲完了还得看采用。以下数据都是 2026-10-03 抓的。它们证明不了 Playwright 在所有测试场景都更好，只说明一件事：浏览器自动化正在往一处集中。</p><p><a href="https://2025.stateofjs.com/en-US/libraries/testing/">State of JS 2025</a>（13,002 人，调查期 2025-09-24 到 11-11）：</p><table><thead><tr><th></th><th>使用率</th><th>留存率</th><th>备注</th></tr></thead><tbody><tr><td>Playwright</td><td>50%</td><td>94%</td><td>2024 年是 36%，+14pp，官方称与 Vitest 并列最大涨幅</td></tr><tr><td>Cypress</td><td>47%</td><td>57%</td><td>2022 年留存是 84%</td></tr><tr><td>Puppeteer</td><td>42%</td><td>70%</td><td></td></tr><tr><td>Selenium</td><td>37%</td><td>24%</td><td>2022 年留存是 42%</td></tr></tbody></table><p>比使用率更值得看的倒是留存：94% 对 57%。同一周（2026-09-26 到 10-02，取自 <a href="https://api.npmjs.org/downloads/point/2026-09-26:2026-10-02/playwright">npm registry API</a>），playwright 的下载量是 1.09 亿，Puppeteer 1,200 万，Cypress 621 万，Selenium 的 JS 绑定 187 万，也就是 9 倍、17 倍和 58 倍。按 star 看，playwright 97,029 颗，puppeteer 95,644 颗，几乎打平。<strong>star 数是“多少人觉得它值得收藏”，不是“多少人在用”</strong>，星标打平、下载量差 9 倍，这个剪刀差就是“工具定位不同”最直观的证据：Puppeteer 还是抓取和脚本的第一选择，只是那条赛道上没有测试。</p><p>说“Cypress 完了”，其实既不准确也不厚道。它在下滑，但没停：2024 年<a href="https://www.cypress.io/blog/update-on-cypresss-workforce">裁员 11 人</a>，2026-09-01 照常发布 <a href="https://www.cypress.io/blog/cypress-16-faster-tests-starting-with-http2-support">Cypress 16</a>；这两年它一直在 AI 上押注，甚至出了一份《<a href="https://docs.cypress.io/app/guides/migration/playwright-to-cypress">Playwright → Cypress 迁移指南</a>》。守势是真的，停更不是。</p><p>还有一条数字倒是反过来的，得摆在这儿：按 <a href="https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/playwright">ecosyste.ms</a> 统计，cypress 被 6,559 个包反向依赖，playwright 只有 1,947 个。下载量赢了 17 倍，但“嵌进别人项目里”这件事 Cypress 仍然更多，这是存量与增量的差别，不写出来就是选择性取证。</p><p>Selenium 也还在跑（4.50，2026-09-30 发布），只是 24% 的留存率把痛点写在了脸上：正是“等待和会话”这两件它当年留给用户的事。</p><p>有几条流传很广的说法，我顺手核了一遍：</p><table><thead><tr><th>常见说法</th><th>实际情况</th></tr></thead><tbody><tr><td>“Puppeteer 只支持 Chrome”</td><td>v23.0.0（2024-08）起同时支持 Chrome 与 Firefox（Firefox 走 BiDi）</td></tr><tr><td>“WebDriver 是 W3C 标准”</td><td>只有 Level 1 是 2018 Recommendation；<a href="https://www.w3.org/TR/webdriver2/">Level 2</a> 与 <a href="https://www.w3.org/TR/webdriver-bidi/">WebDriver BiDi</a> 至今仍是 Working Draft</td></tr><tr><td>“Browser Use / Stagehand 都是基于 Playwright 的”</td><td>均未见直接依赖：Browser Use 用自研 <code>cdp-use</code>，发布出来的 <code>@browserbasehq/stagehand</code> 依赖 <code>@browserbasehq/sdk</code>（同一个 monorepo 里的 integrations、evals 包倒是把 playwright 列成了 devDependency）；Skyvern 才是直接依赖 <code>playwright</code></td></tr><tr><td>“Claude Code / Codex / Cursor 官方默认推荐 Playwright”</td><td>未验证。能确认的只有 Playwright 侧提供了这几家的接入命令，反向的官方表态我没找到</td></tr></tbody></table><blockquote><p>口径与免责：npm 下载量含 CI 里的重复安装、镜像同步和间接依赖，它是量级指标而不是用户数，<code>playwright</code> 这个包还被大量用于抓取，不全是测试。正文里的耗时、报错、ARIA 比例都是我自己在本机量的；版本号与下载量都只是 2026-10-03 的快照，引用请以当时为准。</p></blockquote><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>把全文收进一张表，方便你直接贴到团队文档里：</p><table><thead><tr><th></th><th>形态</th><th>抽象画在哪</th><th>读者</th><th>2026 年的处境</th></tr></thead><tbody><tr><td>Selenium</td><td>协议 + 各家 driver</td><td>协议层，等待交给人</td><td>人</td><td>仍在维护，留存率下滑</td></tr><tr><td>Puppeteer</td><td>库</td><td>浏览器控制层</td><td>脚本作者</td><td>抓取场景稳固，测试场景不参与</td></tr><tr><td>Cypress</td><td>应用（测试在它里面跑）</td><td>链式 DSL + 浏览器内运行</td><td>人</td><td>转入守势，组件测试仍有优势</td></tr><tr><td>Playwright</td><td>库（runner 建在库上）</td><td>语义定位 + 可序列化 + 可回放</td><td>人，以及模型</td><td>使用率与留存双第一</td></tr></tbody></table><p>一句话总结这篇长文：<strong>浏览器自动化从“一个应用”变成了一层“可以被调用的能力”。Playwright 赢的不是测试，它赢的是这一层。</strong> 至于 AI，它不是这场换代的原因，它是把这件事的价值放大了一档的那阵风。</p><p>这篇里的数据我其实核了两遍，但开源世界变化快，难免有疏漏；我对 Playwright 的用法也还在摸索，谈不上什么最佳实践。如果你在迁移路上踩到了不一样的坑，或者发现文中哪里写错了，欢迎指出、欢迎交流。也不妨拿自己项目里最 flaky 的那条用例试一遍，再回来说说体会。权当这篇是一次公开的读书笔记，能对你有点用，就算是额外的收益了。</p><h3 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h3><p>第三关引过的那份 2020 年的 FAQ 里，有一节专门回答“Playwright 用哪些浏览器版本”，开头第一句是：</p><blockquote><p>Playwright <strong>does not patch the rendering engines</strong>. It either uses stock versions of the browsers (Chromium) or extends remote debugging protocols of the respective browsers (WebKit, Firefox) for better automation. Our browsers are as pure as they can be.</p></blockquote><p>同一节里，他们给补丁安排了一个期限：补丁会持续往上游提，等关键改动能落地，就切回“上游优先”的模式；在那之前，WebKit 和 Firefox 的补丁“for now”放在 <code>browser_patches/</code> 下。六年后的今天，官方文档写的是“Playwright 依赖补丁，用不了品牌版 Firefox”，第三关那张表里的 Firefox 走的还是打过补丁的 <code>-juggler-pipe</code>，而 <code>browser_patches/</code> 这个目录还在，两套都在。那个“for now”，用了六年。</p><p>还有个更小的。当年团队判断“能不能上生产”，靠的是挂在维护者 <code>aslushnikov</code> 个人域名下的一个页面：<a href="https://aslushnikov.github.io/isplaywrightready/">Is Playwright Ready?</a>（那份 FAQ 里点名推荐过它），逐内核列出还没通过的测试。我今天打开，整页倒只剩一句：<strong>Is Playwright ready? YES!</strong> 六年的测试矩阵，被收进这两个词里。</p><p><strong>承诺会过期，目录不会。</strong></p><hr><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>官方文档与发布说明：</p><ul><li><a href="https://playwright.dev/docs/actionability">Playwright - Actionability（可操作性判定）</a></li><li><a href="https://playwright.dev/docs/locators">Playwright - Locators</a> / <a href="https://playwright.dev/docs/handles">ElementHandle（已标注不推荐）</a></li><li><a href="https://playwright.dev/docs/aria-snapshots">Playwright - ARIA snapshots</a> / <a href="https://playwright.dev/docs/trace-viewer">Trace Viewer</a></li><li><a href="https://playwright.dev/docs/test-agents">Playwright - Test Agents（planner / generator / healer）</a> / <a href="https://playwright.dev/docs/release-notes">Release notes</a></li><li><a href="https://github.com/microsoft/playwright-mcp">microsoft/playwright-mcp（README）</a> / <a href="https://github.com/microsoft/playwright-cli">microsoft/playwright-cli（CLI + Skills）</a></li><li><a href="https://pptr.dev/faq">Puppeteer - FAQ（定位与浏览器支持）</a></li><li><a href="https://github.com/puppeteer/juggler">puppeteer/juggler（已归档：Firefox 为 Puppeteer 写的自动化协议）</a> / <a href="https://github.com/puppeteer/puppeteer/blob/7f7887ed11930f96cb64bb086fad5c29086b8ef2/experimental/puppeteer-firefox/README.md">puppeteer-firefox（已废弃的原型，自带 Firefox 构建）</a></li><li><a href="https://docs.cypress.io/app/references/module-api">Cypress - Module API（只有 run / open / parseRunArguments）</a> / <a href="https://docs.cypress.io/app/get-started/why-cypress">Why Cypress（同一事件循环）</a></li><li><a href="https://docs.cypress.io/app/core-concepts/interacting-with-elements">Cypress - Interacting with elements（七项检查）</a></li><li><a href="https://www.cypress.io/blog/cypress-16-faster-tests-starting-with-http2-support">Cypress 16 发布</a> / <a href="https://www.cypress.io/blog/update-on-cypresss-workforce">Update on Cypress’s Workforce</a></li><li><a href="https://www.selenium.dev/history/">Selenium - History（同源策略与 driven 模式）</a> / <a href="https://www.selenium.dev/documentation/webdriver/waits/">Waits（显式等待即轮询循环）</a></li><li><a href="https://www.w3.org/TR/webdriver1/">W3C WebDriver Level 1（2018 Recommendation）</a> / <a href="https://www.w3.org/TR/webdriver2/">Level 2（Working Draft）</a> / <a href="https://www.w3.org/TR/webdriver-bidi/">WebDriver BiDi（Working Draft）</a></li></ul><p>数据与观点来源：</p><ul><li><a href="https://2025.stateofjs.com/en-US/libraries/testing/">State of JavaScript 2025 - Testing</a></li><li><a href="https://api.npmjs.org/downloads/point/2026-09-26:2026-10-02/playwright">npm registry downloads API</a>（对比 <a href="https://api.npmjs.org/downloads/point/2026-09-26:2026-10-02/cypress">cypress</a>、<a href="https://api.npmjs.org/downloads/point/2026-09-26:2026-10-02/puppeteer">puppeteer</a>）</li><li><a href="https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/playwright">ecosyste.ms（反向依赖统计）</a></li><li><a href="https://glebbahmutov.com/blog/cy-vs-pw-browser/">Cypress vs Playwright; Browser Included — Gleb Bahmutov（个人观点）</a></li></ul><p>延伸阅读（我自己的前几篇）：</p><ul><li>《<a href="https://springuper.github.io/agent-harness-comparison/">给大脑配一副好鞍具：五款 Agent Harness 的解剖与实测</a>》：同一个“形态决定上限”的问题，看的是模型外面那层壳</li><li>《<a href="https://springuper.github.io/pi-harness-anatomy/">积木，而非成品：Pi Agent Harness 的克制与精妙</a>》：同一套拆解方法的另一个样本</li></ul><blockquote><p>版本与核实说明：数据（下载量、调查、star、反向依赖）与彩蛋里那个页面的状态都核于 2026-10-03；文中代码都在本机实跑过：Playwright 1.57.0 + Node 24.13.0、Puppeteer 25.12.0 + Chrome for Testing 154、Cypress 16.1.1，输出是当时的终端原文，不是手写示意。（Playwright 本机锁在 1.57，撰写时最新是 1.63，1.60 之后的能力都以官方发布说明为准。）Selenium 那段沿革来自官方 History 页，没有实跑（跑它要另装 JDK、Maven 和 ChromeDriver）。四张图都在本机实拍：三种点法的动图与 Trace Viewer 实拍摄于 2026-09-19，ARIA 对比图与按钮三态图摄于 2026-10-01（字符数与 9 月 19 日那次一致）。凡官方没有量化声明的地方，文中都写明了那是我自己量的。</p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;如果你这两年留意过前端测试圈，大概会注意到一件事：新项目的端到端测试，默认都写成 Playwright 了。可这件事有点说不通：Cypress 也会自动等待，它的检查项甚至比 Playwright 还多，代码读起来也更舒服。&lt;/p&gt;
&lt;p&gt;所以我想搞清楚一个问题：&lt;strong&gt;这一轮换代，为什么是 Playwright？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;趋势本身很清楚：2025 年的行业调查里，它的使用率第一次超过 Cypress（50% 对 47%），而最新的周下载量是它的 17 倍。但数字只能说明趋势，说明不了原因。&lt;/p&gt;
&lt;p&gt;功能表也回答不了，得往下一层看：那一行 &lt;code&gt;click()&lt;/code&gt; 在真的落下之前，究竟检查了什么。我拿一颗 600 毫秒之后才可点的按钮当尺子，把它拆成四道关：怎么找到元素、怎么判断能点、怎么把动作送进浏览器、失败之后留下什么。四道关看完，再回头说这两年的事。&lt;/p&gt;</summary>
    
    
    
    
    <category term="AI" scheme="https://springuper.github.io/tags/AI/"/>
    
    <category term="Agent" scheme="https://springuper.github.io/tags/Agent/"/>
    
    <category term="Frontend" scheme="https://springuper.github.io/tags/Frontend/"/>
    
    <category term="Playwright" scheme="https://springuper.github.io/tags/Playwright/"/>
    
    <category term="Testing" scheme="https://springuper.github.io/tags/Testing/"/>
    
    <category term="E2E" scheme="https://springuper.github.io/tags/E2E/"/>
    
  </entry>
  
  <entry>
    <title>积木，而非成品：Pi Agent Harness 的克制与精妙</title>
    <link href="https://springuper.github.io/pi-harness-anatomy/"/>
    <id>https://springuper.github.io/pi-harness-anatomy/</id>
    <published>2026-09-20T09:00:00.000Z</published>
    <updated>2026-10-03T09:20:43.753Z</updated>
    
    <content type="html"><![CDATA[<p>上一篇文章《给大脑配一副好鞍具》里，Pi 交出的成绩单是这样的：<strong>9 秒修完 3 个 bug，6 次工具调用，3375 个 token，成本 0.00007 美元</strong>。五款 agent harness 里最快，也最便宜。</p><p>这篇文章想把 Pi 拆开看看，把内部机制往深里挖一层。我想搞清楚两个问题：它是怎么一步步长成今天这样的，以及我们敲下一行命令之后，它内部究竟发生了什么。</p><p>第一个问题的答案不在文档里，在 git 历史里。所以先去翻它的提交记录。</p><span id="more"></span><h2 id="一、它一开始只有三个包"><a href="#一、它一开始只有三个包" class="headerlink" title="一、它一开始只有三个包"></a>一、它一开始只有三个包</h2><h3 id="1-1-首提交里的三件套"><a href="#1-1-首提交里的三件套" class="headerlink" title="1.1 首提交里的三件套"></a>1.1 首提交里的三件套</h3><p>翻到 2025-08-09 的第一个 commit，里面只有三个包：pi-tui（差分渲染的终端 UI 库）、pi-agent（带会话持久化的 agent 库），以及一个叫 pi 的 CLI。第三个 pi 跟 coding agent 毫无关系，它的 README 第一段是：</p><blockquote><p>Deploy and manage LLMs on GPU pods with automatic vLLM configuration for agentic workloads.</p></blockquote><p>用法是 <code>pi start Qwen/Qwen2.5-Coder-32B-Instruct</code>。“Pi” 这个名字最早属于一个在 GPU 机器上部署模型的工具。后来编码 agent 越长越大，反过来继承了这个名字。所以别问它是不是圆周率，它更像“某台跑模型的机器”的昵称（官方从没解释过，这是我的考古推测）。</p><p>一周之后，pi-ai 被抽了出来。2025-08-17 的 commit 写着：<code>feat(ai): Create unified AI package with OpenAI, Anthropic, and Gemini support</code>。这是 Pi 的第一次分层，把“模型怎么接”从 agent 里剥出去，单独成了一个包。</p><p>两个月后，2025-10-17，coding-agent 出现。产品层有了。</p><h3 id="1-2-一年之后，十一个包"><a href="#1-2-一年之后，十一个包" class="headerlink" title="1.2 一年之后，十一个包"></a>1.2 一年之后，十一个包</h3><p>把每个包第一次出现的日子排一排：</p><table><thead><tr><th>日期</th><th>包</th><th>起因</th></tr></thead><tbody><tr><td>2025-08-09</td><td>pi-tui / pi-agent / pi(pods)</td><td>首提交，三件套。pods 已在 2026-04 删除</td></tr><tr><td>2025-08-17</td><td>pi-ai</td><td>统一 OpenAI / Anthropic / Gemini</td></tr><tr><td>2025-10-17</td><td>coding-agent</td><td>把 agent 做成产品</td></tr><tr><td>2026-06-18</td><td>server</td><td>会话要能被远程接进来。当时叫 orchestrator，07-21 改名</td></tr><tr><td>2026-07-25</td><td>evals</td><td>要能跑评测</td></tr><tr><td>2026-07-30</td><td>protocol</td><td>远程会话要有个通信协议</td></tr><tr><td>2026-07-31</td><td>client</td><td>不挑传输方式（WebSocket、Unix socket 都行）</td></tr><tr><td>2026-08-05</td><td>telemetry</td><td>遥测抽包</td></tr><tr><td>2026-08-05</td><td>session-backends</td><td>存储后端抽出来单列。原名 storage，07-21 建包，08-05 改名</td></tr><tr><td>2026-08-28</td><td>chord</td><td>把插件组装成应用的运行时</td></tr></tbody></table><p>一年时间，三个包长到十一个。中间有进出，pods 被删了，storage 改名成了 session-backends，表里都标了。这条时间线基本是被需求推着走的，很少看到先画一套架构再落地。模型要统一，就抽 pi-ai。要有产品，就长出 coding-agent。会话要能被别人接进来，就有了 server 和 protocol。</p><p>也有例外：上下文压缩在动手前两天先写了研究文档，pi-tui 至今留着一份 <code>tui-plan.md</code>，开头就说它记的是设计讨论里定下来的东西。</p><p>那些机制也是这么长出来的：</p><table><thead><tr><th>日期</th><th>机制</th></tr></thead><tbody><tr><td>2025-12-04</td><td>上下文压缩</td></tr><tr><td>2026-01-02</td><td>会话树（原地分支，v0.31.0 发布）</td></tr><tr><td>2026-03-29</td><td>faux provider，用来在没有模型的情况下测整个循环</td></tr></tbody></table><p>连“Pi 是什么”也是长出来的。它对自己的称呼改过好几轮：2025 年 11 月中旬叫 “a radically simple and opinionated coding agent”，2026 年 1 月底换成 “minimal terminal coding harness”，2026 年 5 月第一次把 “Pi Agent Harness” 写进标题（当时标题里还拖着一个尾巴：“# Pi Agent Harness Mono Repo”），到 2026 年 6 月才精简成今天这句。</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></pre></td><td class="code"><pre><span class="line">pi-coding-agent（CLI：interactive / print · json / rpc / SDK）</span><br><span class="line">   │</span><br><span class="line">   ├─ pi-agent-core（agent 循环、会话树、压缩、状态机）</span><br><span class="line">   │    └─ pi-ai（统一多 provider LLM API）</span><br><span class="line">   │         └─ OpenAI / Anthropic / Google / DeepSeek 等</span><br><span class="line">   │</span><br><span class="line">   └─ pi-tui（自研终端 UI：差分渲染）</span><br></pre></td></tr></table></figure><p>pi-tui 是这张图里唯一的旁支：它不依赖任何其他内部包，只是被产品层拿去渲染界面。</p><p>每一层都切在自己的 package 边界上，多数层在 npm 上能单独装，只有 evals 没发布。有意思的是 commit message 里给的理由全是内部需要，统一多 provider、把遥测抽出来、给目录改名，没有一条写着“方便别人复用”。但结果摆在那里：想要 agent 循环就拿 pi-agent-core，想要终端渲染再带上 pi-tui，产品层自己写就行。这也是它敢管自己叫“库”的底气。</p><p>说完它是怎么长成的，回到第二个问题：敲下一行命令之后，里面究竟发生了什么。</p><h2 id="二、一次请求的旅程"><a href="#二、一次请求的旅程" class="headerlink" title="二、一次请求的旅程"></a>二、一次请求的旅程</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></pre></td><td class="code"><pre><span class="line">pi -p <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span> \</span><br><span class="line">   --provider deepseek --model deepseek/deepseek-v4-flash</span><br></pre></td></tr></table></figure><p><code>-p</code> 是非交互模式，跑完就退出。按下回车之后，我们跟着这行字走一趟，看它每一步变成了什么。</p><h3 id="2-1-它把-prompt-变成了什么"><a href="#2-1-它把-prompt-变成了什么" class="headerlink" title="2.1 它把 prompt 变成了什么"></a>2.1 它把 prompt 变成了什么</h3><p>按下去之后的第一件事，是把上面那行字变成一份发给模型的东西。下面这份是真抓的：把 provider 地址指到本机一个假服务，让 pi 照常发请求，落在磁盘上的请求体原样抄下来。</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><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></pre></td><td class="code"><pre><span class="line">You are an expert coding assistant operating inside pi, a coding agent</span><br><span class="line">  harness. You help users by reading files, executing commands, editing</span><br><span class="line">  code, and writing new files.</span><br><span class="line"></span><br><span class="line">Available tools:</span><br><span class="line">- read: Read file contents</span><br><span class="line">- bash: Execute bash commands (ls, grep, find, etc.)</span><br><span class="line">- edit: Make precise file edits with exact text replacement, including</span><br><span class="line">  multiple disjoint edits in one call</span><br><span class="line">- write: Create or overwrite files</span><br><span class="line"></span><br><span class="line">In addition to the tools above, you may have access to other custom tools</span><br><span class="line">  depending on the project.</span><br><span class="line"></span><br><span class="line">Guidelines:</span><br><span class="line">- Use bash for file operations like ls, rg, find</span><br><span class="line">- Use read to examine files instead of cat or sed.</span><br><span class="line">- You can inspect PI_* environment variables for current model and session</span><br><span class="line">  details.</span><br><span class="line">- Use edit for precise changes (edits[].oldText must match exactly)</span><br><span class="line">- When changing multiple separate locations in one file, use one edit call</span><br><span class="line">  with multiple entries in edits[] instead of multiple edit calls</span><br><span class="line">- Each edits[].oldText is matched against the original file, not after</span><br><span class="line">  earlier edits are applied. Do not emit overlapping or nested edits. Merge</span><br><span class="line">  nearby changes into one edit.</span><br><span class="line">- Keep edits[].oldText as small as possible while still being unique in the</span><br><span class="line">  file. Do not pad with large unchanged regions.</span><br><span class="line">- Use write only for new files or complete rewrites.</span><br><span class="line">- Be concise in your responses</span><br><span class="line">- Show file paths clearly when working with files</span><br><span class="line"></span><br><span class="line">Pi documentation (read only when the user asks about pi itself, its SDK,</span><br><span class="line">  extensions, themes, skills, or TUI):</span><br><span class="line">- Main documentation: &lt;安装目录&gt;/pi-coding-agent/README.md</span><br><span class="line">- Additional docs: &lt;安装目录&gt;/pi-coding-agent/docs</span><br><span class="line">- Examples: &lt;安装目录&gt;/pi-coding-agent/examples</span><br><span class="line">  (extensions, custom tools, SDK)</span><br><span class="line">- 另有 5 条说明，讲怎么顺着这些文档和交叉引用往下读（此处省略）</span><br><span class="line">Current working directory: /tmp/demo-repo</span><br></pre></td></tr></table></figure><p>就这么短。真实的提示词是 31 行、2600 个字符，其中工具清单占 4 行，行为准则占 10 行。</p><p>准则那一节，10 条里有 7 条是<strong>工具自己带上来的</strong>：read 要求“用我，别用 cat 和 sed”，edit 交代“多处改动用一次调用”，write 声明“只用于新文件或整篇重写”。剩下三条是框架补的——一条按当前装了哪些工具生成，一条要求回答简洁，一条要求写清文件路径。这一节不是 Pi 手写的。</p><p>剩下的绝大部分是路径，指向<strong>已安装包里的文档</strong>。这一点 4.2 还会再提，它是 Pi 最有趣的设计之一。</p><p>（为了页面上不横向滚动，上面这块长行按 76 列折过，安装路径换成了 <code>&lt;安装目录&gt;</code>；文档那一节原有 8 行，折成 4 行并标了省略。所以看着是 39 行，正文说的 31 行是原始行数。）</p><p>一份请求在 pi-ai 里叫 <code>Context</code>，只有三样东西：上面这段 system prompt、一份消息列表、一份工具 schema。此刻消息列表里只有一条：</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><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;content&quot;</span>: [</span><br><span class="line">      &#123; <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;text&quot;</span>, <span class="attr">&quot;text&quot;</span>: <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span> &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">&quot;timestamp&quot;</span>: <span class="number">1789795481937</span></span><br><span class="line">  &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>模型能看到的除了历史，还有一份工具清单。coding-agent 内置的工具一共 8 个：read、bash、edit、write、grep、find、ls，外加一个可选、文档标注为 Windows 用的 powershell。<strong>默认激活的只有 read、bash、edit、write 四个。</strong></p><p>工具 schema 也一样朴素。四个默认工具里，bash 真正的参数只有两个（<code>type</code> 和 <code>required</code> 是骨架，参数本体在 <code>properties</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><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">&#123;</span><br><span class="line">  <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;required&quot;</span>: [<span class="string">&quot;command&quot;</span>],</span><br><span class="line">  <span class="attr">&quot;properties&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;command&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Shell command to execute&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;timeout&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;number&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Timeout in seconds (optional, no default timeout)&quot;</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>任意命令，没有白名单。这个“吓人”的细节先记下，4.3 会解释它为什么是故意的。</p><p>还有两个选择不好一眼带过。一个是没有 system 消息类型——系统提示是 <code>Context</code> 的独立字段，不混进历史，所以历史能原样存下来、原样续跑；另一个是这段提示词写死在代码里，本地拼装，不靠远程下发。</p><h3 id="2-2-它怎么发出去"><a href="#2-2-它怎么发出去" class="headerlink" title="2.2 它怎么发出去"></a>2.2 它怎么发出去</h3><p>同一份东西，到了 DeepSeek 那边长这样。这是这一趟的第一次请求，也是最轻的一次：一个系统提示、一份工具清单、一条用户消息。除了 system 那句和工具描述用 <code>...</code> 缩掉，其余逐字来自真实请求（工具清单里只留了 bash，实际是四个）：</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><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></pre></td><td class="code"><pre><span class="line">POST https:<span class="comment">//api.deepseek.com/chat/completions</span></span><br><span class="line"></span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;deepseek-v4-flash&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;messages&quot;</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">&quot;role&quot;</span>: <span class="string">&quot;system&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;content&quot;</span>: <span class="string">&quot;You are an expert coding assistant operating inside pi, ...&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;content&quot;</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;text&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;text&quot;</span>: <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">      ]</span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">&quot;stream&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">&quot;stream_options&quot;</span>: &#123; <span class="attr">&quot;include_usage&quot;</span>: <span class="literal">true</span> &#125;,</span><br><span class="line">  <span class="attr">&quot;max_tokens&quot;</span>: <span class="number">384000</span>,</span><br><span class="line">  <span class="attr">&quot;tools&quot;</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;function&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;function&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;bash&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Execute a bash command in the current ...&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;parameters&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>, <span class="attr">&quot;required&quot;</span>: [<span class="string">&quot;command&quot;</span>],</span><br><span class="line">          <span class="attr">&quot;properties&quot;</span>: &#123; <span class="attr">&quot;command&quot;</span>: &#123; <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span> &#125; &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">&quot;strict&quot;</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="attr">&quot;thinking&quot;</span>: &#123; <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;enabled&quot;</span> &#125;,</span><br><span class="line">  <span class="attr">&quot;reasoning_effort&quot;</span>: <span class="string">&quot;high&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>形状先对一下。Pi 的 <code>Context</code> 还是三样东西：系统提示、消息列表、工具清单。到了这里，系统提示变成 <code>messages</code> 里的第一条 <code>role: &quot;system&quot;</code> 消息，工具清单每条都要套一层 <code>function</code>，用户那句话本来就是文本块数组（2.1 里那个形状），搬过来还是原样。名字和嵌套都换了，三样还是那三样。</p><p>这份报文里还缺一样东西：缓存标记，一个都没有。DeepSeek 的上下文缓存是自动的，前缀一样就命中，不用你标什么；换个协议就得手动打断点，第三章会讲。</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></pre></td><td class="code"><pre><span class="line">start</span><br><span class="line"> ├── text      → text_start / delta / end</span><br><span class="line"> ├── thinking  → thinking_start / delta / end</span><br><span class="line"> ├── toolcall  → toolcall_start / delta / end</span><br><span class="line"> ├── done</span><br><span class="line"> └── error</span><br></pre></td></tr></table></figure><p>text、thinking、toolcall 各自展开成 start / delta / end，加上 start、done、error，一共 12 个变体。中间那三条各自成流，思考、正文、每个工具调用都有自己的增量。这个流容器既是 AsyncIterable，又承诺一个 <code>result()</code>：同一段流既能逐字渲染，也能等它给出最终消息。</p><h3 id="2-3-模型开口要工具"><a href="#2-3-模型开口要工具" class="headerlink" title="2.3 模型开口要工具"></a>2.3 模型开口要工具</h3><p>流里出现了 <code>toolcall</code> 事件，意思是“我要执行 <code>npm test</code>”。控制权交回给 harness。</p><p>Pi 里一个回合（turn）的定义很干净：一次 assistant 响应，加上它引发的所有工具调用与结果。驱动它的 <code>runLoop</code> 是“双层 while”。它本身是模块私有函数，对外入口有四个：<code>agentLoop</code> / <code>agentLoopContinue</code> / <code>runAgentLoop</code> / <code>runAgentLoopContinue</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><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="comment">// 双层循环的形状（示意，省去错误处理与截断保护）</span></span><br><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) &#123;                                    <span class="comment">// 外层：follow-up</span></span><br><span class="line">  <span class="keyword">let</span> hasMoreToolCalls = <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">while</span> (hasMoreToolCalls) &#123;                      <span class="comment">// 内层</span></span><br><span class="line">    <span class="keyword">const</span> msg = <span class="keyword">await</span> streamAssistantResponse(ctx, config); <span class="comment">// think</span></span><br><span class="line">    <span class="keyword">const</span> calls = msg.content.filter(<span class="function">(<span class="params">c</span>) =&gt;</span> c.type === <span class="string">&quot;toolCall&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (calls.length === <span class="number">0</span>) <span class="keyword">break</span>;                <span class="comment">// 没有工具要调，内层结束</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> batch = <span class="keyword">await</span> executeToolCalls(ctx, msg, config);  <span class="comment">// act</span></span><br><span class="line">    ctx.messages.push(...batch.messages);         <span class="comment">// observe</span></span><br><span class="line"></span><br><span class="line">    hasMoreToolCalls = !batch.terminate;          <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> followUps = <span class="keyword">await</span> config.getFollowUpMessages?.() ?? [];</span><br><span class="line">  <span class="keyword">if</span> (followUps.length === <span class="number">0</span>) <span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>内层那一圈就是 think、act、observe：让模型说一段，取出它想调的工具，执行完把结果写回上下文，再回到开头。外层只管一件事：这一圈走完，如果还有排队中的 follow-up，就带着新上下文再来一圈。</p><p>真实的 <code>runLoop</code> 有 118 行（<code>packages/agent/src/agent-loop.ts:156-273</code>），比上面这个形状多出来的部分几乎全是边界处理。那些分支平时不显眼，每一处却都对应一个真会踩到的坑：</p><ol><li><strong>模型一直要工具，谁来踩刹车？</strong> 循环里没有“跑到第 N 轮就停”这种保险丝，刹车被分散到三个地方：工具自己可以返回 <code>terminate: true</code> 说“我干完了”，宿主可以随时用 <code>AbortSignal</code> 打断它，扩展也可以拦下某个工具调用，顺手带上同一个 <code>terminate</code> 标记。读下来像是在说：什么时候该停，交给正在干活的那一层判断。至少循环自己没有替它们做这个主。</li><li><strong>一批工具里只有一个说“停”，算不算停？</strong> 不算。要这一批全部返回 <code>terminate: true</code>，循环才跳过紧接着的那次模型调用。这个标记最典型的用法是收尾工具：官方示例里的 <code>structured_output</code>，模型最后调一次它把结果交出来，它标上 <code>terminate: true</code>，循环就此收工，<strong>省掉一次完整的模型往返</strong>。反过来，同一批里如果还混着一个普通工具（比如刚跑完 <code>npm test</code>），它的输出模型还没看过，这一批就不能停，得再问一轮。</li><li><strong>输出被截断，参数只剩半截，还执行吗？</strong> 不执行。模型这次响应如果撞上了输出上限（<code>stopReason</code> 是 <code>length</code>），它吐出来的工具参数很可能是被腰斩的，Pi 把这批工具调用全部判错，让模型把参数重发一遍。宁可这一轮什么都不干，也不能拿着半截参数去改文件。</li></ol><p>模型那句“我要跑 <code>npm test</code>”到了 Pi 这里，被记成一条 assistant 消息。harness 执行完命令，把输出记成一条工具结果消息。下一趟请求把这两条一起带上去：Pi 内部的消息列表从一条变成三条，到了报文里再加上 system，一共四条：</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><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="string">&quot;messages&quot;</span>: [</span><br><span class="line">  &#123; <span class="attr">&quot;role&quot;</span>: <span class="string">&quot;system&quot;</span>, <span class="attr">&quot;content&quot;</span>: <span class="string">&quot;You are an expert coding assistant ...&quot;</span> &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;content&quot;</span>: [</span><br><span class="line">      &#123; <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;text&quot;</span>, <span class="attr">&quot;text&quot;</span>: <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span> &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;role&quot;</span>: <span class="string">&quot;assistant&quot;</span>, <span class="attr">&quot;content&quot;</span>: <span class="literal">null</span>,</span><br><span class="line">    <span class="attr">&quot;tool_calls&quot;</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;call_1&quot;</span>, <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;function&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;function&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;bash&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;arguments&quot;</span>: <span class="string">&quot;&#123;\&quot;command\&quot;:\&quot;npm test\&quot;&#125;&quot;</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="attr">&quot;reasoning_content&quot;</span>: <span class="string">&quot;&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123; <span class="attr">&quot;role&quot;</span>: <span class="string">&quot;tool&quot;</span>, <span class="attr">&quot;content&quot;</span>: <span class="string">&quot;3 failing&quot;</span>, <span class="attr">&quot;tool_call_id&quot;</span>: <span class="string">&quot;call_1&quot;</span> &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>有两处对着看更有意思。</p><p>模型要调工具这件事，翻译过去是 <code>tool_calls</code> 数组，而且<strong>参数是序列化好的 JSON 字符串</strong>，不是对象。Pi 内部那个 <code>arguments</code> 是 <code>&#123; command: &quot;npm test&quot; &#125;</code>，到了这里变成 <code>&quot;&#123;\&quot;command\&quot;:\&quot;npm test\&quot;&#125;&quot;</code>。谁负责这一下转换，就是这一层的事，上一层的循环不用管。</p><p>工具结果到了这里自己占一个角色，<code>role: &quot;tool&quot;</code>，用 <code>tool_call_id</code> 指回原来那次调用。换个协议就不一样了：在 Anthropic 那边，工具结果不能单独占角色，只能挂在一条 <code>role: &quot;user&quot;</code> 消息里。这套差异全在这一层消化掉，上一层的循环对此一无所知。</p><h3 id="2-4-结果回到树上"><a href="#2-4-结果回到树上" class="headerlink" title="2.4 结果回到树上"></a>2.4 结果回到树上</h3><p>这一趟只调了一个工具，真实的修复任务不会这么简单。上一篇文章那次任务里，模型和工具来回走了 6 轮，每一轮都在会话文件里留下记录。6 轮在文件里怎么排布，看文件本身最直接。</p><p>我让 Pi 真的写了一个会话文件。真实文件是每行一条记录的 JSONL：每条记录一层外壳，消息本体挂在外壳里。这一节只关心树怎么长出来，所以下面把外壳里与树无关的字段都收起来，只留每条的身份和它的父节点——被收起来的 <code>usage</code> 和 <code>cost</code> 后面还要回来交代，先放一放。完整文件长得多，下面这四条只是其中一段：</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">// 第 1 行：header，整个文件只有这一条</span><br><span class="line">&#123; &quot;type&quot;: &quot;session&quot;, &quot;version&quot;: 3, &quot;cwd&quot;: &quot;/tmp/demo-repo&quot;,</span><br><span class="line">  &quot;id&quot;: &quot;01a0b81f-b94f-7559-bfee-67609da12709&quot; &#125;</span><br><span class="line"></span><br><span class="line">// 第 2 行 / m1：用户消息，parentId 为 null，所以它是根</span><br><span class="line">&#123; &quot;type&quot;: &quot;message&quot;, &quot;id&quot;: &quot;a5c1c471&quot;, &quot;parentId&quot;: null,</span><br><span class="line">  &quot;message&quot;: &#123; /* 消息本体，形状同 2.1 */ &#125; &#125;</span><br><span class="line"></span><br><span class="line">// 第 3 行 / m2：assistant，挂在 m1 下面</span><br><span class="line">&#123; &quot;type&quot;: &quot;message&quot;, &quot;id&quot;: &quot;678f354f&quot;, &quot;parentId&quot;: &quot;a5c1c471&quot;,</span><br><span class="line">  &quot;message&quot;: &#123; /* 消息本体：内容 + usage + cost */ &#125; &#125;</span><br><span class="line"></span><br><span class="line">// 第 4 行 / m3：工具结果，挂在 m2 下面</span><br><span class="line">&#123; &quot;type&quot;: &quot;message&quot;, &quot;id&quot;: &quot;423c01c8&quot;, &quot;parentId&quot;: &quot;678f354f&quot;,</span><br><span class="line">  &quot;message&quot;: &#123; /* 消息本体：工具输出 */ &#125; &#125;</span><br></pre></td></tr></table></figure><p>外壳里只剩 <code>type</code>、<code>id</code>、<code>parentId</code> 三样，但这棵树已经在里面了。</p><p>第一行是 header，之后每一行是一个 entry，每个 entry 都指向自己的 parentId，<code>null</code> 表示它是根。所以一个文件里天然长着一棵树，而不是一条线：</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></pre></td><td class="code"><pre><span class="line">m1 ─ m2 ─ m3 ─┬─ m4 ─ m5   ← 叶子 A</span><br><span class="line">              │</span><br><span class="line">              └─ m6 ─ m7   ← 叶子 B</span><br></pre></td></tr></table></figure><p>id 是 8 位 hex，比如 <code>a5c1c471</code>，只用来定位，不参与语义。被收起来的 <code>usage</code> 挂在每条 assistant 记录上，缓存命中量是其中独立的一个字段——这次实录里是 <code>cacheRead: 1080</code>。这份文件是我用假模型在本地写的，所以 <code>cost</code> 全是 0、<code>usage</code> 里那几个数字也只是示意值，真实会话里会填上真金白银和真实计量。这一趟花了多少缓存，会话文件里就记着多少。</p><p>图里的 m1 到 m3 就是上面那三条记录，再往后是这次实录之外的部分。当前对话位置永远是树的某片叶子，我们刚才那 6 次工具调用，是在这棵树上走出的一条路径。想回到历史里的某个岔路口，那就是一次“切分支”。对应的命令有三个：</p><ul><li><code>/tree</code>：查看整棵树，在文件内跳转、切换分支。</li><li><code>/fork</code>：选中某条 user 消息，把它之前的路径复制成一个新的会话文件，并把那条 prompt 放回输入框供我们改写。</li><li><code>/clone</code>：把当前分支复制到新文件。</li></ul><p>这些文件就躺在 <code>~/.pi/agent/sessions/</code> 下，按工作目录分文件夹，而且是只追加的：entry 一旦写下就不能改也不能删，只有迁移旧格式时会整体重写一次。所以“重写历史”在 Pi 里不是修改，而是开新枝，就像 git 一样，历史不可变，想走另一条路就 branch。对 agent 来说这太重要了：我们 fork 出去让模型试一个激进方案，不满意，切回原来的叶子继续，两边的记忆都还在。</p><h3 id="2-5-出错、插话、太长"><a href="#2-5-出错、插话、太长" class="headerlink" title="2.5 出错、插话、太长"></a>2.5 出错、插话、太长</h3><p>真实的请求不会一路顺风。模型调用会遇到超时、报错和上下文溢出。</p><p>出错时的恢复原语叫 <code>agent.continue()</code>，注释写得很直白：</p><blockquote><p>Continue an agent loop from the current context without adding a new message.Used for retries - context already has user message or tool results.</p></blockquote><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></pre></td><td class="code"><pre><span class="line">可重试错误（过载 / 限流 / 5xx）</span><br><span class="line">  → 指数退避重试：2s → 4s → 8s，至多 3 次</span><br><span class="line"></span><br><span class="line">上下文溢出，且这一步的响应是失败的</span><br><span class="line">  → 从 agent state 摘掉末尾那条失败的 assistant</span><br><span class="line">  → 自动压缩，把历史折成摘要</span><br><span class="line">  → continue() 接着跑，只给一次机会</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></pre></td></tr></table></figure><p>出错之外，还有两件事是往循环里追加输入，Pi 把它们做成了两个队列：</p><ul><li><strong>steering（转向）</strong>：循环正在跑的时候想插一句话，投进 steer 队列，它会在当前工具执行完、模型下一次思考之前注入。</li><li><strong>follow-up（续尾）</strong>：循环已经决定要停了，补一句“顺便把测试也跑了”，投进 follow-up 队列，它会再多跑一轮。</li></ul><p>一个插在“还在干活时”，一个插在“正要收工时”。这两种时机对应完全不同的体验，所以是两个队列，不是一个。</p><p>还有太长的时候。上下文快装不下时，一种常见做法是把最旧的几条消息丢掉。Pi 用摘要替换，而且原文一条不删地留在文件里：</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">会话文件（档案：一行都不删）</span><br><span class="line"> m1  m2  m3  m4  m5  m6  m7  m8</span><br><span class="line"> └───────┬───────┘  └────┬───┘</span><br><span class="line">         │                │</span><br><span class="line">    compaction          保留的原文</span><br><span class="line">     摘要 entry        （keepRecent）</span><br><span class="line">         │                │</span><br><span class="line">         ▼                ▼</span><br><span class="line">模型输入 [ 摘要 ][ m6 m7 ][ 新消息 ]</span><br></pre></td></tr></table></figure><p>触发条件是一行公式：上下文估算的 token 超过“窗口减去预留量”。这个预留量的默认值是 16384，留给提示词和模型回复。切点则用另一个数，20000，表示压缩后至少要保留的近期内容预算。两者分工不同：一个管什么时候压，一个管压到哪儿。切点有一个硬规矩，绝不允许切在工具结果上，否则模型会看到“调了工具却没结果”。</p><p>至于那批被摘要覆盖掉的旧消息，一条都没有删。它们只是不再进模型窗口，永远留在文件里，用 <code>/tree</code> 跳回去还能看到原文。</p><h3 id="2-6-这一趟的证据"><a href="#2-6-这一趟的证据" class="headerlink" title="2.6 这一趟的证据"></a>2.6 这一趟的证据</h3><p>讲到这里，你可能会问：这些机制，我怎么确认它们真的在跑？</p><p>Pi 自己给了一个办法。它内置了一个 faux provider（<code>packages/ai/src/providers/faux.ts</code>，共 708 行），一个零网络的假模型：把调用方排队的响应脚本，按真实的 delta 事件流吐出来。它能限速来模拟慢模型，也能模拟 abort 和 deferred 异步响应，甚至能按 sessionId 前缀命中来仿真 prompt cache 的读写。</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">npm i @earendil-works/pi-ai@0.85.1</span><br><span class="line">node faux-demo.mjs</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// faux-demo.mjs</span></span><br><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  fauxAssistantMessage, fauxText, fauxThinking, fauxToolCall,</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&quot;@earendil-works/pi-ai&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; registerFauxProvider, streamSimple &#125;</span><br><span class="line">  <span class="keyword">from</span> <span class="string">&quot;@earendil-works/pi-ai/compat&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> faux = registerFauxProvider(&#123;</span><br><span class="line">  <span class="attr">models</span>: [&#123; <span class="attr">id</span>: <span class="string">&quot;faux-1&quot;</span>, <span class="attr">contextWindow</span>: <span class="number">100000</span> &#125;],</span><br><span class="line">&#125;);</span><br><span class="line">faux.setResponses([fauxAssistantMessage([</span><br><span class="line">  fauxThinking(<span class="string">&quot;先看看工作目录里有什么，再决定改哪个文件。&quot;</span>),</span><br><span class="line">  fauxText(<span class="string">&quot;我先列一下目录。&quot;</span>),</span><br><span class="line">  fauxToolCall(<span class="string">&quot;bash&quot;</span>, &#123; <span class="attr">command</span>: <span class="string">&quot;ls -la&quot;</span> &#125;, &#123; <span class="attr">id</span>: <span class="string">&quot;call_1&quot;</span> &#125;),</span><br><span class="line">], &#123; <span class="attr">stopReason</span>: <span class="string">&quot;toolUse&quot;</span> &#125;)]);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> stream = streamSimple(faux.getModel(), &#123;</span><br><span class="line">  <span class="attr">systemPrompt</span>: <span class="string">&quot;You are an expert coding assistant operating inside pi.&quot;</span>,</span><br><span class="line">  <span class="attr">messages</span>: [</span><br><span class="line">    &#123; <span class="attr">role</span>: <span class="string">&quot;user&quot;</span>, <span class="attr">content</span>: <span class="string">&quot;帮我看看这个目录&quot;</span>, <span class="attr">timestamp</span>: <span class="built_in">Date</span>.now() &#125;,</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">tools</span>: [</span><br><span class="line">    &#123; <span class="attr">name</span>: <span class="string">&quot;bash&quot;</span>, <span class="attr">description</span>: <span class="string">&quot;Run a shell command&quot;</span>, <span class="attr">parameters</span>: &#123;&#125; &#125;,</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> seen = [];</span><br><span class="line"><span class="keyword">for</span> <span class="keyword">await</span> (<span class="keyword">const</span> ev <span class="keyword">of</span> stream) &#123;</span><br><span class="line">  seen.push(ev.type);</span><br><span class="line">  <span class="keyword">const</span> payload = ev.type.endsWith(<span class="string">&quot;_delta&quot;</span>) ? <span class="built_in">JSON</span>.stringify(ev.delta) : <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="built_in">console</span>.log(ev.type, payload);</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">`共 <span class="subst">$&#123;seen.length&#125;</span> 个事件，事件类型种类 <span class="subst">$&#123;<span class="keyword">new</span> <span class="built_in">Set</span>(seen).size&#125;</span>`</span>);</span><br><span class="line"><span class="keyword">const</span> final = <span class="keyword">await</span> stream.result();</span><br><span class="line"><span class="built_in">console</span>.log(final.stopReason, final.content.map(<span class="function">(<span class="params">c</span>) =&gt;</span> c.type).join(<span class="string">&quot;, &quot;</span>));</span><br></pre></td></tr></table></figure><p>跑起来是这样（GIF 是真实终端输出录的，不是手绘）：</p><p><img src="../images/pi-harness-faux-event-stream.gif" alt="faux provider 的真实事件流：思考、正文、工具调用三段增量依次到达"></p><p>控制台里的原始输出（Node 24，2026-09-19）：</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><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">event  start</span><br><span class="line">event  thinking_start</span><br><span class="line">event  thinking_delta   &quot;先看看工作目录里有什么，再决定改&quot;</span><br><span class="line">event  thinking_delta   &quot;哪个文件。&quot;</span><br><span class="line">event  thinking_end</span><br><span class="line">event  text_start</span><br><span class="line">event  text_delta       &quot;我先列一下目录。&quot;</span><br><span class="line">event  text_end</span><br><span class="line">event  toolcall_start</span><br><span class="line">event  toolcall_delta   &quot;&#123;\&quot;command\&quot;:\&quot;ls -&quot;</span><br><span class="line">event  toolcall_delta   &quot;la\&quot;&#125;&quot;</span><br><span class="line">event  toolcall_end</span><br><span class="line">event  done             stopReason=toolUse</span><br><span class="line"></span><br><span class="line">共 13 个事件，事件类型种类 11</span><br><span class="line">toolUse  thinking, text, toolCall</span><br></pre></td></tr></table></figure><p>delta 是按 token 块切的，所以一句话会被切成几段，逐字渲染靠的就是这个。事件类型一共 12 种，这一趟没出现 <code>error</code>，所以最后那行只数到 11 种。</p><p>（faux 的切块大小是随机取的，所以每次跑出来的条数和切分点都略有不同，上面这份是其中一次。）</p><p>这件事看着小，却是工程上的分水岭：<strong>当“模型”可以被一个确定性脚本替代，整个循环的测试就从碰运气变成了可复现。</strong> 上一篇文章里那个 9 秒的成绩是真实 API 跑出来的，但同样的路径可以拿 faux 反复走，不必再花钱。仓库的开发守则里写得很硬：<code>packages/coding-agent/test/suite/</code> 这套回归测试只准用 faux，不许用真实 API key 和付费 token。</p><h2 id="三、为什么它快"><a href="#三、为什么它快" class="headerlink" title="三、为什么它快"></a>三、为什么它快</h2><p>现在回到开头那张成绩单：9 秒，3375 个 token，0.00007 美元。便宜和快是两回事，背后的机制也不一样，分开看下。</p><h3 id="3-1-缓存这本账"><a href="#3-1-缓存这本账" class="headerlink" title="3.1 缓存这本账"></a>3.1 缓存这本账</h3><p>先说便宜。那 0.00007 美元。</p><p>很多模型 API 对上下文缓存计费。如果这次请求的前缀和之前某次完全相同，命中的那部分就按远低于正常输入的价格算。这个差价有多大，看 pi 自己模型清单里的数字最直接：flash 档的缓存读价和输入价分别是 0.0028 和 0.14 每百万 token，差了 50 倍。（清单是随包发布的快照，未必等于官方页当下的挂牌价，但量级的一致性是它成立的前提。）</p><p>所以 harness 的功课很像前端压榨 HTTP 缓存：让请求前缀尽量稳定。能命中的，永远是“从头开始、一字不差的那一段”：</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></pre></td><td class="code"><pre><span class="line">两次请求的前缀：</span><br><span class="line">[tools][sys][u1][call][result1]</span><br><span class="line">[tools][sys][u1][call][result1][u2]</span><br><span class="line">└─────── 前缀一字不差 ───────┘</span><br></pre></td></tr></table></figure><p>Pi 在这件事上做的事分两类：一类让命中真的发生，一类让命中看得见。</p><p>能不能命中，前提是两次请求的前缀逐字节相同，所以下面这三条都在维护那个不变的前缀。</p><ul><li>请求级的 <code>cacheRetention</code> 默认就是 <code>&quot;short&quot;</code>，也就是缓存默认开着。听着不起眼，但它是前提，关着的话后面两条都无从谈起。</li><li>system prompt 是请求级顶层字段，每轮原样重发，工具集和插件不变时逐字节一致。所以最贵的那一段（系统提示加工具清单）永远不变，对话在它后面怎么长都不影响它。</li><li>打标记这件事，各家要求不一样，交给适配层去管。DeepSeek 这类是自动的，什么都不用标，前缀一样就命中（2.2 那份报文里一个缓存字段都没有，就是这个原因）。Anthropic 那类要手动打断点，标记打在哪儿、活多久，都得自己算。</li></ul><p>手动打的那一套，看 Anthropic 最清楚。所谓<strong>断点</strong>，就是在请求体里标一个位置，等于告诉服务端：从开头到这里的内容请存下来，下次前缀一样就能直接复用。标记本身就是 <code>cache_control</code>，它出现在哪个块上，断点就在哪。它的值是 <code>&#123;&quot;type&quot;: &quot;ephemeral&quot;&#125;</code>，ephemeral 是“临时的”，这是目前唯一的缓存类型，默认寿命 5 分钟，想延长到一小时要额外加 <code>ttl: &quot;1h&quot;</code>，写入按输入两倍计费。</p><p>OpenAI Responses 侧是另一套，靠一个缓存键，<code>prompt_cache_key</code>，把 sessionId 截到 64 字符。Fireworks 这类靠副本路由命中的，得加一个 session-affinity 头，让请求尽量落到同一个缓存副本上。harness 要做的不是挑一种，而是各家都照顾到。</p><p>让命中看得见的那一半在 usage 里：<code>cacheRead</code> 和 <code>cacheWrite</code> 是两个独立字段，成本按每家 provider 的缓存价单独算，连上面那种一小时缓存的写入按输入两倍计费，模型里也照样分开算。省了多少，账上明明白白。</p><p>上一篇文章实测里，Pi 的缓存命中率是 91% 到 93%。算一下：九成多的输入按 1/50 计价，剩下不到一成按原价，两项加起来约为完全未命中时的十分之一。<strong>缓存把输入这一项压掉了大约九成。</strong></p><p>但 0.00007 美元不是缓存一家的功劳。那笔账里至少还有两件事：总额只有 3375 个 token，而且用的是最便宜的 Flash 档。缓存是其中最大的一项，不是全部。上一篇也交代过，成本是各工具自己上报的数值，只宜做量级参考。</p><p>缓存也不是纯赚的：读取便宜，写入要按输入价另算（Anthropic 是 1.25 倍）。所以一次性请求不值得缓存，Pi 会把它们的 <code>cacheRetention</code> 设成 <code>&quot;none&quot;</code>，写摘要那次调用就是。</p><h3 id="3-2-不添乱"><a href="#3-2-不添乱" class="headerlink" title="3.2 不添乱"></a>3.2 不添乱</h3><p>再说快。9 秒里有很大一部分是模型自己在想、加上那 6 次命令跑掉的时间，harness 替不了这个忙。它能做的是别往上添东西，而 Pi 在这点上克制得有点反常：</p><ul><li>整个 system prompt 只有 2600 个字符，默认只挂 4 个工具，也就是 4 段 schema。这些东西每一轮都要原样重发，短一点，请求就轻一点。</li><li>一批工具默认并行执行。同一次模型响应里要调的几个工具会一起发出去，而不是排队一个个来。基准那一趟看不出这条省了多少，因为那 6 次调用大多是串的（跑测试、改、再跑测试）。</li><li>工具本身没有确认弹窗，该跑就跑。这里说的是工具层——第一次在一个带 <code>.pi</code> 资源（这个项目自己的扩展、提示词、技能）的目录里启动时，仍会有一次项目信任询问。</li></ul><p>这几条都不是什么发明，说到底只是没有被加上去。而忍住不加，本身也是一个设计决定。</p><h3 id="3-3-只重画变化的行"><a href="#3-3-只重画变化的行" class="headerlink" title="3.3 只重画变化的行"></a>3.3 只重画变化的行</h3><p>界面这一层也是同一路数。Pi 没有用现成的终端 UI 框架，自己写了一个 pi-tui：</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></pre></td><td class="code"><pre><span class="line">重绘派：整块重写       Pi：只更新变化的行</span><br><span class="line">┌────────────┐        ┌────────────┐</span><br><span class="line">│  aaaaa     │        │  aaaaa     │</span><br><span class="line">│  bbbbb     │        │  bbbbb     │  ← 没变</span><br><span class="line">│  ccccc     │        │  CCCCC     │  ← 只发这行</span><br><span class="line">│  dddddd    │        │  dddddd    │</span><br><span class="line">└────────────┘        └────────────┘</span><br></pre></td></tr></table></figure><p>严格说它是区间语义：每帧先全量重算出整个行数组，再求出第一处和最后一处变化的行，中间没变的也跟着这一段一起写出去。只变一行时，才是图里画的最省情形。配合 CSI 2026 同步输出，终端先把一整帧攒齐再一次渲染，杜绝半帧闪烁。</p><p>这和 3.1 那笔账是一回事的两面：<strong>把会变的部分和不会变的部分分开处理</strong>——缓存盯的是请求前缀，尽量让它不变；渲染盯的是屏幕，只把变了的行发出去。</p><p>作者是游戏圈出身（libGDX 的作者），这套做法直接来自游戏渲染管线。仓库里还有个用它跑 DOOM 的示例，在 overlay 里以 35 FPS 实时渲染，算是这套思路的一个旁证。</p><h2 id="四、它到底做对了什么"><a href="#四、它到底做对了什么" class="headerlink" title="四、它到底做对了什么"></a>四、它到底做对了什么</h2><p>拆到这里，可以说结论了。三件事，一件比一件反直觉。</p><h3 id="4-1-循环是个函数"><a href="#4-1-循环是个函数" class="headerlink" title="4.1 循环是个函数"></a>4.1 循环是个函数</h3><p>前端的发展史告诉我们：<strong>“框架”替我们决定控制流，“库”把控制流还给我们。</strong> jQuery 是库，Angular 是框架，React 一度被争论到底算哪个。</p><p>Pi 把这条审美写进了产品 README（<code>packages/coding-agent/README.md</code>）：</p><blockquote><p>Pi is a minimal terminal coding harness. Adapt pi to your workflows, not the other way around, without having to fork and modify pi internals.</p></blockquote><p>翻译过来是：让 Pi 适应你的工作流，而不是让你去适应它，而且你不需要 fork 它的源码就能做到。</p><p>很多 coding agent 把循环藏在产品内部，你能调用的是它的命令行或者界面，循环本身不对外。Pi 是“库”，agent 循环是一个可以 <code>import</code> 进自己程序的 async 函数。CLI、批处理、RPC、SDK，都只是这个函数的不同宿主。</p><p>看完第二章那一趟旅程，你会发现这有多实在：那套 <code>runLoop</code> 不认终端，不认 stdin，也不认任何界面。它只认一个 <code>Context</code> 和一份配置。所以它今天能跑在 CLI 里，明天就能跑在别人的 IDE 里。</p><p>同一个内核对外有四种出口：交互式 TUI，一次性的 print 模式（可以选 JSON 事件流），跑在 stdin/stdout 上的 RPC 协议（给非 Node 的编辑器集成用），以及 SDK。同一个循环，四个出口，无数种驾驶舱。</p><h3 id="4-2-把一切做成数据"><a href="#4-2-把一切做成数据" class="headerlink" title="4.2 把一切做成数据"></a>4.2 把一切做成数据</h3><p>会话是数据，所以可以 fork。压缩记录是数据，所以旧消息一条不丢。token 和缓存是消息上的一等字段，所以成本全程透明。</p><p>前端工程师对这套东西不陌生：它就是不可变状态加时间旅行调试——把状态的变化存成数据，把回到过去变成一次指针操作。</p><p>Pi 把说明书也放进了模型够得着的地方：系统提示词里给了三个指向已安装包内 README、docs、examples 的路径，并写明只在用户问起 Pi 本身时才读。意思是你可以直接问它“你自己是怎么工作的”，它会翻开自己的说明书作答。</p><p>仓库 README 里有一句自我介绍：</p><blockquote><p>This is the home of the Pi agent harness project including our self extensible coding agent.</p></blockquote><p>“self extensible” 是它给自己贴的标签，而且贴得住。仓库根目录的 <code>.pi/</code> 里就是它自己用的扩展、提示词和技能，它拿自己开发自己。</p><h3 id="4-3-敢不做"><a href="#4-3-敢不做" class="headerlink" title="4.3 敢不做"></a>4.3 敢不做</h3><p>Pi 的产品 README 里有一节叫 Philosophy，通篇是一个接一个的“No”，而且一个比一个反直觉：不做 MCP（不接外部工具与数据源那套协议），不做子代理（不派生分身去干活），不做权限弹窗，不做计划模式（不先出方案再动手），不做内置待办清单，不做后台 bash（不把命令丢到后台慢慢跑）。</p><p>第 2.1 节那个没有白名单的 bash，就是这条清单最直接的样子。它不替你判断哪条命令危险，只负责把命令原样交给 shell。</p><p>官网首页给这套减法起了个名字，叫 “Primitives, not features”。直译是“原语，而非功能”，也就是这篇文章标题里“积木，而非成品”的出处。</p><p>最容易被误解的是“不做权限”。它的理由写得很清楚：</p><blockquote><p>Pi does not include a built-in permission system for restricting filesystem, process, network, or credential access. By default, it runs with the permissions of the user and process that launched it.</p></blockquote><p>安全文档里说得更绝：</p><blockquote><p>Real isolation needs to come from the operating system or a virtualization/container boundary.</p></blockquote><p>它的论证是：一个进程内的半吊子沙箱，容易被误当成真正的安全边界，因为它自己还得依赖宿主 shell、文件系统、包管理器和凭证。所以与其给一个让人误以为安全的假边界，不如明说没有边界，边界请到操作系统或容器层面去画。</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></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────┐</span><br><span class="line">│  pi-ai / pi-agent-core / pi-coding-agent │</span><br><span class="line">└────────────────────┬────────────────────┘</span><br><span class="line">                     │ Pi 核心以你的用户权限运行</span><br><span class="line">                     │ 想要更强的边界？套一层壳</span><br><span class="line">       ┌─────────────┼─────────────┐</span><br><span class="line">       ▼             ▼             ▼</span><br><span class="line">   Gondolin       纯 Docker    OpenShell</span><br></pre></td></tr></table></figure><p>这几种边界都不在 Pi 里面，来源各不相同，隔离范围也不一样（官方文档列了四种，除了下面三个还有 Docker Sandboxes）：</p><table><thead><tr><th>壳</th><th>隔离什么</th></tr></thead><tbody><tr><td>Gondolin 扩展</td><td>pi 与 provider 凭证留在宿主，内置工具与 <code>!command</code> 这种在输入框里直接跑 shell 的入口，都路由进本地 Linux 微 VM</td></tr><tr><td>纯 Docker</td><td>整个 pi 进程装进容器</td></tr><tr><td>NVIDIA OpenShell</td><td>整个 pi 进程进策略沙箱（文件、进程、网络、凭证、推理都可控），需要 gateway</td></tr></tbody></table><p>这条清单的说法一直在调。2025 年 11 月这一节叫 “Security (YOLO by default)”，12 月 9 日改成 “No Permission System (YOLO Mode)”，八天之后整节被删，只剩一句 “No permission popups. Security theater.”。那句“安全表演”自己也没留住——2026 年 1 月底被删，今天同一条换成了“要么进容器，要么自己拿扩展拼一套确认流程”。严肃表述是 2026 年 6 月才补上的。立场没变，措辞一直在变。</p><p>不做权限弹窗，那想要权限系统的人怎么办？自己拼一个。扩展系统里有现成的样板：<code>permission-gate.ts</code> 拦危险命令并弹确认，<code>protected-paths.ts</code> 保护 <code>.env</code> 和 <code>.git</code>。照 <code>permission-gate.ts</code> 抄一个自己的权限门，大概二十分钟的事。这就是“积木，而非成品”的注脚。</p><p>不做的东西由谁来补？官方给的第一层是样板：<code>examples/extensions/</code> 里就躺着子代理、计划模式、待办清单这些现成例子，只是不装进核心。钩子也给得足，<code>ExtensionAPI</code> 一共 36 个事件，从输入到回合起止、压缩之前都有挂载点，日常用得上的四个就够：拦个工具、加个命令、改改提示词、压缩前插一手。</p><p>再往外一层才是生态：npm 上每一层都能单独装，别人完全可以只取一层去拼自己的东西；官方不做的能力由第三方补上，连公开的会话数据集、第三方解读和教学仓库都已经有了。本篇参考里那几条，就是这份生态地图的入口。</p><p>3.3 里那个自研的 pi-tui 也是这条清单的产物。现成的终端框架不是没有，但要按自己的方式渲染，就只能自己写。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>先把拆开的东西收进一张表，方便和上一篇那套“五件套”对照：</p><table><thead><tr><th>五件套</th><th>Pi 的配法</th></tr></thead><tbody><tr><td>循环</td><td>可 import 的函数，无轮次硬上限，工具可喊停</td></tr><tr><td>工具</td><td>内置 8 个、默认开 4 个，命令没有白名单，批量默认并行</td></tr><tr><td>记忆</td><td>会话是一棵 entry 树，压缩写摘要而不删历史</td></tr><tr><td>权限</td><td>没有内置权限系统，边界交给容器</td></tr><tr><td>界面</td><td>自研差分渲染 TUI。四种出口（interactive / print·json / rpc / SDK）共享同一个循环</td></tr></tbody></table><p>一年时间，三个包长到十一个，每一层都是被需求催出来的，所以多数层能单独拿走；会话、分支、压缩、缓存全是数据，所以历史能被 fork、被摘要、被续跑；别人标配的功能它一个不做，把工作流那一层的决定权留给我们自己拼。</p><p>这三件事指向同一句话：<strong>工作流那一层，它不替你做决定。</strong> 官网首页那句 “Primitives, not features”，还有那个曾经叫 shittycodingagent.ai 的域名，说的都是这件事。</p><p>当然，默认值还是它替你选的。默认只开四个工具，压缩的两个阈值默认 16384 和 20000，项目信任默认要问一句。它让出来的不是“从不做选择”，而是“选择可以被改”。这两件事不一样，前面几章的例子也只在说后一件。</p><p>它当然不是给所有人准备的。如果你要的是开箱即用的安全与周全，Claude Code 们更合适。但如果你想亲手掌控自己那副鞍具的每一颗螺丝，Pi 是这个品类里把选择权还回来还得最彻底的一个。上一篇文章结尾我说“马是谁不重要了，重要的是鞍具合不合手”，这篇的结尾想补一句：<strong>最好的鞍具，是你随时能拆开、能续上、还能请马自己讲讲它怎么跑的那一副。</strong></p><p>还有一件正文没展开的事。第一章那张表里的 server、protocol、client 三个包，加上它们都依赖的 chord 运行时（chord 本身是个独立的插件组装框架，不依赖任何 Pi 包），是正在成型的另一套运行时：它把 agent 循环从进程内的函数，改造成可以落盘、可以跨进程接续的服务，目标是一个会话能被多个客户端同时接进来。这套东西目前只在 coding-agent 的 <code>experimental/</code> 路线里，官方文档把它标成 development-only，要 <code>PI_EXPERIMENTAL=1</code> 才启用，默认 CLI 走的仍是经典架构。写这篇文章时它的定位是演进方向，不是当前行为。</p><p>想亲自上手验证这篇里的论断，最快的一条路是：装上 Pi，把官方 <code>examples/extensions/</code> 目录翻一遍，照着抄一个自己的扩展，再跑一次 2.6 节那段 faux demo。这比读十篇解剖文章都管用。别忘了上篇的提醒：给工具用独立的目录副本，别让它们互相剧透。</p><h2 id="该警惕的地方"><a href="#该警惕的地方" class="headerlink" title="该警惕的地方"></a>该警惕的地方</h2><ol><li>没有内置权限就是安全自负。这是设计，也是风险：让 Pi 在我们不完全信任的目录上裸奔，等于让任意提示词驱动我们的 shell。它的安全文档自己也认这一点：仓库文件、注释、文档、上下文文件、构建产物里的提示词注入属于本地 agent 的预期风险，“cannot be reliably prevented by pi”，Pi 只把你本人和 Pi 进程当同一个信任边界。在共享机器、CI、处理敏感数据时，先套壳。（顺带一提，它确实修过一个本地提权问题，CVE-2026-54328，影响 0.74.0 至 0.78.0，0.78.1 修掉；这个和权限模型无关，成因是临时扩展目录可预测。）</li><li>版本与文档有代差。经典 API 和新 harness 并存，教学材料往往只讲其一。写代码、看文档前先确认自己在哪一代上。</li><li>“不做 MCP、不做子代理、不做计划模式”的取舍不是免费的。这些能力要么靠社区包补，要么得自己写。对开箱即用党来说，它比 Claude Code 这类工具糙不少。</li><li>仓库默认自动关闭新贡献者的 issue 和 PR，维护者每日人工复查。项目红火但门槛不低，别被拒了一次就以为是在针对自己。</li></ol><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>2026 年 1 月，Pi 的官网长这样：</p><p><img src="../images/pi-harness-old-site.png" alt="2026 年 1 月的 Pi 官网首页"><em>Wayback Machine 对 shittycodingagent.ai 的存档，抓取时间 2026-01-09</em></p><p>那句 “There are many shitty coding agents, but this one is mine” 是冲着美国海军陆战队的步枪手誓词（Rifleman’s Creed）来的。原话是 “This is my rifle. There are many like it, but this one is mine”，《全金属外壳》里新兵齐声念的就是它。翻译过来大概是：烂编码 agent 有很多，但这一个是我的。</p><p>安装命令里还留着当时的 scope，<code>@mariozechner/pi-coding-agent</code>，包名后来才挪到 <code>@earendil-works</code> 下。</p><p>三个月后，README 顶上那个指向 shittycodingagent.ai 的 logo 链接被换成了 pi.dev。动手的那次提交，message 写的是 “feat(branding): corporate said we’re professionals”，意思是公司说我们得专业一点，提交人是 Armin Ronacher。pi.dev 这个域名是 exe.dev 友情捐赠的，致谢至今还写在 README 页脚里。</p><p>那个老域名今天还活着，访问它会 301 跳到 pi.dev。存档页显示这段历史一共有 38 次抓取，跨度从 2025 年 12 月 17 日到 2026 年 9 月 14 日。</p><p>一个敢把自己官网叫“烂编码 agent”的项目，大概也配得上“克制”这两个字。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>仓库：<a href="https://github.com/earendil-works/pi">earendil-works/pi</a>（<a href="https://pi.dev/">pi.dev</a> 官网与文档）</li><li>作者博客：<a href="https://mariozechner.at/posts/2025-11-30-pi-coding-agent/">《What I learned building an opinionated and minimal coding agent》</a>、<a href="https://mariozechner.at/posts/2025-11-02-what-if-you-dont-need-mcp/">《What if you don’t need MCP at all?》</a></li><li>我上一篇的横评：《<a href="https://springuper.github.io/agent-harness-comparison/">给大脑配一副好鞍具：五款 Agent Harness 的解剖与实测</a>》（复现实测用同一提示词即可，命令见其附录）</li><li>中文教学仓库：<a href="https://github.com/cellinlab/how-pi-agent-works">cellinlab/how-pi-agent-works</a></li><li>第三方解读：<a href="https://walkinglabs.github.io/learn-harness-engineering/zh-TW/harness-designs/pi/">walkinglabs 的 harness 工程设计系列（Pi 篇）</a></li><li>真实会话数据集：<a href="https://huggingface.co/datasets/badlogicgames/pi-mono">badlogicgames/pi-mono on Hugging Face</a></li></ul><blockquote><p>版本与核实说明：本文基于仓库 HEAD <code>9767ba2</code>（各包版本 0.85.1，2026-09-06 抓取）撰写，演进时间线里的日期与 commit 均取自仓库 git 历史，可按 commit 复核。版本号与生态数据随时间变化，引用请以当时为准。文中实跑的证据（faux 输出、会话文件）产生于 2026-09-19，比基于 HEAD <code>9767ba2</code> 的源码核对晚两周，是为了让它们和这一版正文对齐才重新跑的。文中的 faux 事件流 demo 是实跑输出（Node 24 + <code>@earendil-works/pi-ai@0.85.1</code>），不是手写示意。掌故类内容（名字来历、YOLO Mode、域名变更）同样按 git 历史核验，其中域名那条重定向在浏览器里可以直接复现。</p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;上一篇文章《给大脑配一副好鞍具》里，Pi 交出的成绩单是这样的：&lt;strong&gt;9 秒修完 3 个 bug，6 次工具调用，3375 个 token，成本 0.00007 美元&lt;/strong&gt;。五款 agent harness 里最快，也最便宜。&lt;/p&gt;
&lt;p&gt;这篇文章想把 Pi 拆开看看，把内部机制往深里挖一层。我想搞清楚两个问题：它是怎么一步步长成今天这样的，以及我们敲下一行命令之后，它内部究竟发生了什么。&lt;/p&gt;
&lt;p&gt;第一个问题的答案不在文档里，在 git 历史里。所以先去翻它的提交记录。&lt;/p&gt;</summary>
    
    
    
    
    <category term="AI" scheme="https://springuper.github.io/tags/AI/"/>
    
    <category term="Agent" scheme="https://springuper.github.io/tags/Agent/"/>
    
    <category term="Harness" scheme="https://springuper.github.io/tags/Harness/"/>
    
    <category term="Pi" scheme="https://springuper.github.io/tags/Pi/"/>
    
    <category term="TypeScript" scheme="https://springuper.github.io/tags/TypeScript/"/>
    
  </entry>
  
  <entry>
    <title>向内归因的人，手里永远多出几个选项</title>
    <link href="https://springuper.github.io/inward-attribution/"/>
    <id>https://springuper.github.io/inward-attribution/</id>
    <published>2026-09-14T10:35:00.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>最近一年多，我在团队里看到一个有点反直觉的现象：AI 编程工具普及之后，大部分人的产出都上去了，但人和人之间的差距没有缩小，反而拉大了。</p><p>按理说不该这样。同一件工具、同一类任务，所有人都能用；工具对每个人是公平的。那差额是从哪来的？</p><p>我的答案是：来自我们怎么解释自己的处境。</p><p>说得再直接一点：<strong>向内归因能解决更多问题。</strong></p><p>这不是一句道德劝告，而是一个效率判断。同一个处境落到两个人手里，会向内归因的那个人，手里永远多出几个选项；而习惯向外归因的人，会先把选项一个个划掉，最后得出”我没什么可做的”。</p><p>这篇文章想说清三件事：为什么它在今天比过去更管用，怎么才算真正做好它，以及我自己就是它的受益者。</p><span id="more"></span><h2 id="我看到的几种状态"><a href="#我看到的几种状态" class="headerlink" title="我看到的几种状态"></a>我看到的几种状态</h2><p>先说让我决定写下这篇文章的那些事。</p><p>大部分人的产出都在往上走，但也有人原地踏步，个别甚至有所回落。我留意到的信号通常是这几样：不主动同步进展，一件事推一下动一下。坐下来聊原因，听到的说法大致是这几类：</p><ul><li>没有工单就没办法开始开发，而且工单里的需求必须非常明确</li><li>决策都在美国那边，自己不在场，拿到的信息也不完整</li><li>受时差和协作方式的限制，重要的事情本来就轮不到自己；既然不重要，那也就不着急</li></ul><p>这些说法单拎出来，每一条都不算离谱。但合在一起，就构成了一个完整的、自洽的、几乎无法反驳的世界模型：<strong>问题都在外面，我没什么可做的。</strong></p><p>还有一类不满，是关于自己的发展速度，常见于两种处境：</p><p>一类是组织一直在动，汇报线换了几轮，投入很久的项目最后没能掀起什么水花，人会慢慢觉得使不上劲；一类是长期做支撑性、独立性较强的工作，这类工作不太容易被看见，身边的人陆续上去了，落差感就出来了。</p><p>前者容易滑向”很不爽，但也没办法”，后者容易滑向”这不公平”。两种情绪方向不同，落点却一样：都倾向于把问题放到外面去。</p><p>最后是一个反向的例子：也有人换过好几任老板，每个重点项目却都干得相当出色，非常主动，沟通到位，而且很自信。</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><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><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">  │                        │╲</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></pre></td></tr></table></figure><h2 id="为什么这个时代它更管用"><a href="#为什么这个时代它更管用" class="headerlink" title="为什么这个时代它更管用"></a>为什么这个时代它更管用</h2><p>要理解这件事为什么现在格外重要，得先看清 AI 到底改变了什么。</p><p>过去，一个想法从”看到问题”到”做出来”之间，隔着几周甚至几个月的实现成本。所以大部分时候，你能做什么不取决于你看到了什么，而取决于有没有人批准、有没有排期、有没有资源。</p><p>AI 很大程度上拆掉了这道墙。实现成本被压下去之后，”我先试一版”第一次变成了一个人可以自己决定的事。</p><p>所以 AI 给个人带来的最大变化不是”更快”，是”更自由”：你第一次可以不太依赖授权，直接去解决你看到的真问题。</p><p>这也意味着，向外归因的理由在变弱。以前”没人给我工单，所以我做不了”是真的，因为成本摆在那儿；现在不是了。</p><p>同一件工具落到不同的人手里，走向完全不同：</p><table><thead><tr><th></th><th>只把 AI 当提效工具</th><th>把 AI 当放大器</th></tr></thead><tbody><tr><td>解决的问题</td><td>原来那些</td><td>原来做不到的</td></tr><tr><td>省下的产能</td><td>省下来了</td><td>又投回去了</td></tr><tr><td>职责的边界</td><td>守住</td><td>常常跨出去</td></tr><tr><td>信息差</td><td>自己扛</td><td>主动同步去缩小</td></tr><tr><td>结果</td><td>平稳</td><td>差距被拉开</td></tr></tbody></table><p>所以在当下这个阶段，有几件事的回报率被显著抬高了：</p><ul><li><strong>主观能动性</strong>：工具把动手成本压得很低，”我先做一版试试”变得空前便宜；而”等条件齐全再动手”的相对成本就变高了</li><li><strong>积极沟通</strong>：当每个人都能更快产出时，瓶颈就从”做得出来”移到了”知道该做什么”，信息差成了主要矛盾，主动同步的价值因此上升</li><li><strong>判断力</strong>：工具能帮你实现方案，但”这个方案该不该做””这段代码能不能进生产”仍然只能由人判断。被贬值的是打字，升值的是取舍</li><li><strong>面向业务价值的取向</strong>：既然”做出来”变容易了，那”做什么值得做”就成了更稀缺的能力</li></ul><p>反过来讲，这也意味着<strong>停滞的代价变高了</strong>。以前”按部就班、不出错”是一种安全策略；现在它变成了一个相对落后的策略：因为你身边的人正在被同一件工具加速。</p><h2 id="为什么外归因会把选项关掉"><a href="#为什么外归因会把选项关掉" class="headerlink" title="为什么外归因会把选项关掉"></a>为什么外归因会把选项关掉</h2><p>外归因和向内归因不是我的发明。Rotter 在 1966 年提出”控制点”（Locus of Control）：内控型的人倾向于认为结果取决于自己的行为，外控型倾向于认为取决于运气、环境和他人。</p><p>这个区分最有价值的地方，是它能预测下一步行动。如果你认定问题在外面，逻辑上的必然结论就是”我无需改变”。短期内也的确舒服，它保护了自尊，也省去了面对自身短板的痛苦。但代价是长久的：你把自己人生的遥控器交了出去。</p><p>有一句总结我很认同，讲得很扎心：</p><blockquote><p>外归因最省力：出事先找替罪羊，过错全推给外界，短期心理平衡，不用跟自己的短处较劲。但代价是永久丧失进化的可能——既然问题都在外面，自己自然无需改变。——知乎答主<a href="https://www.zhihu.com/question/525200257/answer/2072798775140160515">「起风了」</a></p></blockquote><p>外归因有不少精巧的变体，不妨逐一拆开看，因为它们全都披着”客观陈述”的外衣。在《The Oz Principle》里，作者用”水平线”区分两种状态：线以上的人对结果负责，线以下的人则把精力用在推责上，比如”这不是我的工作”、指责他人、坐等指令、坐等观望。这些面孔和我下面要列的，倒也几乎一一对应。</p><p>第一种，「这不归我管」。没有工单就没法开发：流程确实有缺失，但从”缺一个工单”跳到”这件事做不了”，中间那个跳跃没有依据。</p><p>第二种，「我怎么会知道」。潜台词是：信息应该被分发给我。但在分布式团队里，信息不会自动落到每个人手上，要么自己去争，要么推到能建通道的人那里。</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><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><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">     ▼</span><br><span class="line">③ 更确信「轮不到自己」</span><br><span class="line">     │</span><br><span class="line">     └────────────────► 回到 ①，闭环成立</span><br></pre></td></tr></table></figure><p>这个闭环一旦形成，别人再想给机会也接不住了。它和”时差”没有关系，这件事两边都有责任。后面我会专门说管理者的那一半。</p><p>第四种，「不公平」。这种要格外小心，因为它可能是真的。评价体系确实会青睐某些能力（比如沟通、跨部门影响力），而冷落另一些能力（比如把支撑性的服务做深做透），这对当事人来说确实构成结构性的不利。承认这一点，是讨论的前提。但关键在于，别卡在”不公平”这个结论上不动。这种委屈里其实藏着一道判断题，我在下一节说。</p><h2 id="向内归因不是自责"><a href="#向内归因不是自责" class="headerlink" title="向内归因不是自责"></a>向内归因不是自责</h2><p>写到这里得先泼一盆冷水，否则上面这些很容易被读成”凡事多反省自己”。</p><p><strong>向内归因 ≠ 自责，更 ≠ 认命。</strong> 有三条边界必须守住：</p><ol><li><strong>归因要尽量准确，而不是尽量向内。</strong> 内归因最大的陷阱，是把一个具体问题迅速升级为人格问题：不是”这次方案没写好”，而是”我天生就写不好方案”。久而久之，人就活成一台职场自责机器，能量全耗在自我否定上，反而没力气改进。这种”过度内归因”和外归因一样有害。在资深工程师身上它尤其隐蔽，经常伪装成”高标准”和”owner 意识”，所以特别难自查。</li><li><strong>归因 ≠ 归责。</strong> 向内归因的对象是”我能改变的那部分”，而不是”全部责任”。承认环境确实占了一部分、自己也占了一部分，然后专注在自己那一部分上。这不叫认领全部罪责，这叫找到着力点。</li><li><strong>向内归因不等于否认环境的约束。</strong> 有些外因是真实存在的，制度可能确实失衡。成熟的认知不做非黑即白的二元判断。先承认大环境的力量，再问一句：在这个约束之下，我还能做什么？</li></ol><p>回到「不公平」那一种。</p><p>在选路之前，得先分清一件事：你遇到的到底是哪一种限制。</p><ol><li><p><strong>假性瓶颈：它看起来像能力不足，其实界是自己划的。</strong></p><blockquote><p>“我天生内向，这种要去说服别人的事我做不来。”“我英文不行，跨时区的讨论我就不掺和了。”</p></blockquote><p>这类话听起来像在陈述自己的短板，但它们往往是预测，不是结论。你还没试过，就先替自己判了。”我性格就是这样”本身也是一种外归因：它把结果归给了一个固定、不可改变的特质，而不是归给选择。</p><p>这一种的解药不是”补短板”，是先去试一次。而且很多时候试完会发现：它没有想象中那么难，只是不太舒服罢了。</p></li><li><p><strong>真的通道瓶颈：路是通的，只是你缺一样东西才能走过去。</strong></p><p>比如技术很强但不会写方案、不会同步，贡献就没人看见；或者确实试过了，跟人打交道每次都很吃力、但能推进，只是还不熟练。这一类是可以补的，补了就有用。</p></li><li><p><strong>身份冲突：走通这条路的前提，是放弃自己最擅长、也最看重的那部分。</strong></p><p>试过了，也做成了，但每一次都像在扮演别人，做完了只剩消耗。那可能不是短板问题，而是这条路本身不适合你。</p></li></ol><p>这三种用两个问题就能分开：</p><ol><li>我试过吗？ 没试过就认定”我不行”，那是第一种，先去试</li><li>试过了：我是在增长，还是在磨损？ 在增长，哪怕不舒服，是第二种，值得补；只剩磨损，是第三种，该认真考虑换道</li></ol><p>这里有一句不太讨喜的话，但我认为必须说：”我不想成为那种人”可以是最诚实的判断，也可以是最舒服的借口。两者的区别，只在于你有没有真的试过。</p><p>所以按这个分法，第一步是先试一次，把第一种和第二种分开，别急着选”补”还是”换”。分开之后，三条路都成立：</p><ol><li><strong>补短板</strong>：认清体系看重什么，然后去补。这条路最省力，也确实有人正在这么走</li><li><strong>换赛道</strong>：清醒地判断”我不想成为那种人”，然后去一个真正看重自己优势的环境</li><li><strong>接受现状，但停止情绪消耗</strong>：承认代价，也接受代价</li></ol><p>唯独”持续苦闷且不行动”不可取。前面那类不满里的两种走向，差别就在这里：同样是委屈，一种停在”不爽但没办法”，一种已经开始动了。时间会把这个差别无限放大。</p><h2 id="你负责的任务，和你负责的问题"><a href="#你负责的任务，和你负责的问题" class="headerlink" title="你负责的任务，和你负责的问题"></a>你负责的任务，和你负责的问题</h2><p>前面几节讲的，都是”在我认领的范围内，我能做什么”。这里我想把范围本身往外推一点：它不关于你怎么做事，而关于你认领多大的问题。</p><p>在工程师团队里，有个非常普遍的默认分工：<strong>PM 负责想清楚要做什么，工程师负责把它做出来。</strong></p><p>于是大家习惯了让 PM 去定义方案、评估效果，自己专注在明确被要求的那一小部分。这样的团队里，一个人完全可以把开发任务做得很漂亮，但你要是问他：</p><ul><li>这个功能到底在解决用户的什么痛点？</li><li>现在这个方案是不是最好的？有没有更简单的做法？</li><li>上线之后怎么迭代？下一版该动哪里？</li><li>效果评估怎么保证是准的？我们看的到底是不是真正的信号？</li></ul><p>这几个问题问下来，他大概答不上来。不是能力不够，是这块地盘他从没认领过。</p><p>这就是”向外归因”最隐蔽的一种形式：<strong>把问题的定义权让出去，只保留执行权。</strong> 它不像”这不归我管”那么刺耳，因为它披着”专业分工”的外衣。</p><p>这正是向内归因在这个时代最该做的一个动作：把”我的问题”的边界往外推一点。</p><p>我见过真正优秀的人，做法几乎都一样：他们穿过自己的合作方，走到真实的使用场景里去。这里说的不是越过谁去替 PM 做调研。PRD 和项目讨论通常都是必要的起点，绝大多数人也都是从那里开始的。区别在拿到之后：停在文档上就只是执行，而他们会拿这份文档去对照真实场景，想想这个功能到底在解决谁的什么痛点、有没有更简单的做法，再带着自己的判断回来跟 PM 讨论。</p><p>问”这个为什么要做”本身没有错，那是该问的；区别只在于问完之后，去不去找答案。</p><p>差别不在于谁更聪明，而在于<strong>谁认领的是一件事，谁认领的只是一个动作。</strong></p><p>这件事在今天就更加要紧了。前面说过，AI 把”实现”的成本压得很低。那么”我只负责实现”这份分工的稀缺性，也就跟着变低了。工具能帮你把方案做出来，但”该做什么方案、做完怎么验证有效”仍然只能由人判断。判断的前提，是你真的到过现场。</p><p>说到这里得补一句：这道坎是真实存在的，我不想绕过去。这确实不是每个人都舒服做的事：工程师群体整体偏内敛，主动去找关键合作方聊，意味着要准备很多、要说服别人、有时还要正面辩论；如果是跨时区的团队，还得用一门不完全是母语的语言去完成这些事。这些都是实打实的成本。但也正因为它是成本、而不是不可能，才需要一个判断。</p><p>所以这道坎到底要不要迈，是个需要诚实面对自己的判断。它和前面「不公平」那种委屈，本质上是同一道题，用的也是同一套判据：试过了吗？是在增长，还是在磨损？</p><h2 id="怎么做好向内归因"><a href="#怎么做好向内归因" class="headerlink" title="怎么做好向内归因"></a>怎么做好向内归因</h2><p>这是我最想说清楚的一节，因为它最容易被误解。</p><p>向内归因常被理解成”凡事都往自己身上揽”。恰恰相反，<strong>真正会向内归因的人，是外部因素识别得最清楚的人。</strong> 因为他要精确回答”我能动的那部分在哪”，就必须先把外部因素看准。反倒是外归因的人看得最粗：一句”环境不行”就把全部原因打了包。</p><p>所以第一步不是自责，是分拣：</p><table><thead><tr><th>看起来是这样</th><th>其实是</th><th>该做什么</th></tr></thead><tbody><tr><td>没人给我工单</td><td>假的外部因素</td><td>自己先写一版</td></tr><tr><td>流程没人负责</td><td>真的外部因素</td><td>把它变成别人的工单</td></tr><tr><td>时差、资源、战略</td><td>真的，短期无解</td><td>承认，绕开，或者换道</td></tr></tbody></table><p>三类的处理方式完全不同。把第三类当成第一类，会白白自责；把第一类当成第三类，会白白停摆。分拣错了，两个方向都要付代价。</p><p>注意中间那一类：识别出真外部因素之后的动作，不是认命，是找到能解决它的人，把这件事变成他的工单。这是一项能力，不是一句让步。”流程没人管”这件事，你完全可以主动写一版提案，推着把它变成某个人的待办。</p><p>然后是那条我自己一直在用的原则：<strong>看这件事做完之后，能不能留下可见的东西。</strong></p><p>没工单的事其实有两类。一类会留下资产：一个新的方案、一套能复用的工具、一条被打通的流程、别人对你的信任。另一类什么也不留：救火、补位、替别人扛一段，做完之后没有任何沉淀，下次还得从头再来。这两类表面上都是”主动”，回报却完全相反。</p><p>所以我的界是：能留下可见的东西，就值得做；如果只是让你在没有权限的情况下反复替别人兜底，那就该把责任还回去，而不是自己扛。把”缺工单”无差别地当成个人执行力问题，是管理者的偷懒。</p><p>分拣完，我还会按可控性把这件事再摆一遍。这是从柯维的关注圈（我关心但控制不了的事）和影响圈（我能实际施加影响的事）那儿借来的，只是我又拆细了一档：</p><table><thead><tr><th></th><th>例子</th><th>该怎么做</th></tr></thead><tbody><tr><td>我能控制</td><td>我的投入、我的沟通频率、我的技能</td><td>全力以赴</td></tr><tr><td>我能影响</td><td>老板的预期、需求的优先级、跨团队协作方式</td><td>持续经营</td></tr><tr><td>我控制不了</td><td>时差、人事变动、公司战略</td><td>接受，不反复咀嚼</td></tr></tbody></table><p>这一步最常见的错误，是把前两种当成了第三种。</p><p>几个门槛最低、收益最实在的动作，我自己一直在用：</p><ol><li><strong>做一次语言转换。</strong> 外归因有非常明显的语言特征：”都是因为……”、”如果不是他……”、”这个公司就是……”。内归因的特征同样鲜明：”在这件事里，我的责任是什么？”、”我能从中学到什么？”、”在我能控制的范围里，我能先做哪一件事？”不用强迫自己改变想法，先改变造句方式就行。语言会倒逼思维。</li><li><strong>主动同步。</strong> 每周更新一次进展、卡点和下一步，写下遇到的困难，也写下你打算怎么处理。在分布式团队里，可见度本身就是产出的一部分。很多人把汇报当负担，其实它是最低成本的存在感投资。</li><li><strong>从约束里挑一件小事做完。</strong> 不要等条件齐全才动手。缺工单，就先写一版草稿需求；跨时区信息拿不到，就主动去找参与过前序讨论的人把背景补齐，或者先把整理好的已知信息发出去请人补充。完成一件小事带来的信任额度，比十次抱怨管用。</li><li><strong>每个季度做一次”工单审计”。</strong> 回头数一数：过去三个月我做的事里，有多少是别人派的，有多少是我自己找的？这个数字很粗糙，但它是唯一能衡量的东西：衡量你还在积累资产，还是只是在原地执行。</li><li><strong>重要任务要争取，不要等。</strong> 这类事很少是被分配的，绝大多数得靠自己争取。争取的方式是先做一件超出预期的小事，换来一点信任额度，再靠它去拿更大的机会。</li><li><strong>把锚点放在成果上，别放在某个人身上。</strong> 老板会走，你在流程文档、项目成果、可迁移能力上留下的痕迹不会走。</li></ol><h2 id="同一个问题，两种视角"><a href="#同一个问题，两种视角" class="headerlink" title="同一个问题，两种视角"></a>同一个问题，两种视角</h2><p>“向内归因”在 IC 和管理者身上，含义其实不一样。</p><p>作为 IC，它是问：在我能动的范围里，我能做什么？ 把手上的事做到超出预期、主动同步、从约束里挑一件小事先做完。</p><p>作为管理者，它是问：他的卡点里，有多少是我造成的？ 需求有没有人转成工单？跨时区的信息通道我建了没有？派活时我有没有偏好，把某些人一直留在很难被看见的位置上？</p><p>这中间没有互相免责，而是各自把自己那一半认下来。</p><p>而且这里还有一个常被忽略的区分：<strong>有一类”外归因”根本不是态度问题，而是习得性无助。</strong> 塞利格曼的经典实验说明，当一个个体反复经历”无论怎么努力都无法改变结果”之后，他会彻底放弃尝试。哪怕后来环境变了、机会来了，他也不动了。</p><p>放到职场里就很清楚了：如果他主动提的方案被驳回、多做的额外工作没被看见、出了问题还要独自担责，那么”不主动”就是他学到的、最理性的自我保护。这一类不是靠讲道理能谈通的，你需要先给他一次完整的正向闭环：主动 → 被看见 → 有回报。</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></pre></td><td class="code"><pre><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">                    归属感</span><br></pre></td></tr></table></figure><p>其中「意愿不足」这一类，自我决定理论说得最清楚：胜任感（我做得了吗）、自主性（这是我的选择吗）、归属感（我和团队有关系吗），三者缺一不可。</p><p>然后是坐下来开口。上面这些都还是原则，真正难的是那场谈话。我自己试过有用的一组问题，思路只有一条：把”你为什么不主动”换成具体的提问。</p><ul><li>最近哪件事让你觉得卡住了？当时你想做什么？</li><li>你试过什么？结果怎么样？</li><li>如果这件事由你定，你会怎么排优先级？</li><li>我能帮你清掉哪个障碍？</li></ul><p>这组问题的用意很实在：<strong>把一次关于态度的审判，变成一次关于障碍的共谋。</strong></p><p>至于”试多久算够”，我的做法是先把环境侧的三件事做完：流程（工单该谁负责）、信息（跨时区通道）、机会（派活有没有偏好）。这三件事都有明确的负责人，也就是我。然后给一个明确的观察周期，我的经验是一个季度。</p><p>这个顺序不能颠倒。先修环境再谈人，不是因为管理者要宽容，而是因为不这样做，你根本分不清问题在人还是在环境。</p><p>如果都做过了，一个人依然把所有结果归于外部，既不沟通也不改善，那这就是一个需要直面的绩效问题了。原因很简单：团队里其他人看得见。我在 <a href="https://springuper.github.io/fire-fast/">Fire Fast</a> 里写过一句话：”容忍低绩效者是对大多数人的惩罚”。同样地，如果一个团队慢慢默认了”归因于外是可以被接受的”，这种默认会像传染病一样稀释掉整个团队的进取心。这比某一个人的产出下滑要严重得多。</p><p>这一节和 <a href="https://springuper.github.io/just-culture/">Just Culture</a> 里那件事是同一个道理：出了状况，先问”哪里出了问题”，而不是”谁出了问题”。</p><h2 id="我见过的那些发展比较快的人"><a href="#我见过的那些发展比较快的人" class="headerlink" title="我见过的那些发展比较快的人"></a>我见过的那些发展比较快的人</h2><p>前面讲的多半是停滞的样子，我想再说说另一面。这一面我看到的样本更多，也更稳定。</p><p>这些年我见过不少发展得比较快的人，他们的背景、方向、路径各不相同，但有一些共同点几乎反复出现：他们不等指令，主动同步，敢把不确定的事接过来。</p><p>项目做到一半黄了，他们的第一反应是”还剩什么能做”，而不是”这不是我的问题”；老板换了，他们先去摸清新老板在意什么，而不是先抱怨又要重来；遇到没人认领的空白地带，他们倾向于先占住，而不是先问这归谁管。</p><p>最极端的一个例子，是那位换过好几任老板的同事。每一任的风格、偏好、考核重点都不一样，按前面那些逻辑，他完全有理由抱怨：新老板上来就推翻旧方案，努力反复被打回，站队风险极高。但他没有。每一任老板到岗，他主动同步现状、风险和待决策事项；不管怎么换，他手上的重点项目永远做到超出预期。</p><p>于是他的履历上写的不是”我服务过好几位老板”，而是”我做成过 N 件事”。<strong>老板会走，成果不会走。</strong></p><p>我把这些共同点归纳成三条，它们其实都是”向内”的外显：</p><ul><li><strong>主动</strong> 是”不等指令”，是自己给自己下工单</li><li><strong>沟通好</strong> 是主动缩小信息差，主动管理对方的预期，而不是等对方来问</li><li><strong>稳定</strong> 是一种”我能把事情推进下去”的预期。它看起来像天赋，但我更愿意把它看成前两条的副产品：主动同步带来更快的反馈，更快的反馈让判断更准，判断更准就更容易做成事。做成的事多了，预期自然就稳了</li></ul><p>德鲁克在《卓有成效的管理者》里反复强调一件事：知识工作者必须自己管理自己，必须不断问自己”我能贡献什么”。当一个人把目光从”老板是谁”移开，转向”我贡献了什么”，他就获得了某种任何人事变动都夺不走的东西。</p><p>我知道这套观察会被这样反驳：样本太小、因果可能是反的、有人就是靠亮点项目和顺风局上来的、评价体系本身也未必公平。这四条我都认。</p><p>但它们全都成立，也不影响我的结论。因为我不是在论证”向内归因必然带来晋升”，那个我证明不了。我说的是：<strong>面对同一个处境，向内归因的人手里会多出几个选项。</strong> 这一条不需要因果论证，你回头翻一翻自己的经历就能验证。</p><h2 id="我自己就是受益者"><a href="#我自己就是受益者" class="headerlink" title="我自己就是受益者"></a>我自己就是受益者</h2><p>最后说说我自己，因为我大概是这篇文章最直接的样本。</p><p>这一路走得还算顺。回头看我做成的那些事：把播放方案重写一遍、接上 DRM、搭起监控、写一个让原生容器更好用的 JS bridge、做一套调试工具。没有一件是别人派给我的工单。都是我先看到问题，然后自己去填那块空白。</p><p>更典型的是”让国内团队和美国的资深管理者真正协作起来”这件事。它更不会有人给我开工单，而且一点都不轻松。我自己在介绍里写过一句：这件事难得超出预期。</p><p>当然，我也赶上了顺风。我加入现在公司的时候，国内团队刚刚成立，不过业务发展得不错。等老板们发现国内工程师质量高、性价比好，就开始逐渐扩大规模。后来的很多机会，确实也是这种扩张本身创造出来的。在前东家，我也赶上了团购外卖行业蓬勃发展的几年，从一个刚刚毕业的萌新逐渐成为技术管理者，推动了不少事情。</p><p><strong>但顺风解释的是”机会为什么出现”，解释不了”机会出现时我接没接住”。</strong> 同一个位置上换个人，结果不会一样。</p><p>所以我是向内归因的受益者，而且我不打算假装这只是运气。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>回到开头那个反直觉的现象：AI 把每个人的产出杠杆都放大了，差距反而在拉大。</p><p>我现在很确定，差距的主要来源不是”会不会写代码”，而是<strong>你怎么解释自己的处境，以及你因此做了什么</strong>。工具可以让一个主动的人产出翻倍，也可以让一个被动的人继续原地不动。它只是放大了你原本的样子。</p><p>归因方式不是道德评价。向内归因的人并不比向外归因的人更高尚，这只是一个效率问题：它决定你把有限的精力投向哪里。</p><p>它最终给你的回报，也不只是升职加薪。向内归因真正让人获得的东西是：<strong>无论环境怎么变，你手里始终有得选。</strong></p><p>这些话我只能说对自己管用，未必对别人也一样。如果对你也还算是有所启发，那就是额外的大收益了。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><a href="https://en.wikipedia.org/wiki/Locus_of_control">Locus of Control - Wikipedia</a></li><li><a href="https://en.wikipedia.org/wiki/Learned_helplessness">Learned Helplessness - Wikipedia</a></li><li><a href="https://www.franklincovey.co.nz/habit-1/">Habit 1: Be Proactive - FranklinCovey</a></li><li><a href="https://franklincovey.vn/en/resources/article/the-circle-of-influence-and-the-circle-of-concern">Understanding the Circle of Influence and the Circle of Concern</a></li><li><a href="https://www.supersummary.com/the-oz-principle/summary/">The Oz Principle: Getting Results Through Individual and Organizational Accountability</a></li><li><a href="https://www.zhihu.com/question/525200257/answer/2072798775140160515">什么是顶级的思维？- 知乎答主「起风了」</a></li><li><a href="https://zhuanlan.zhihu.com/p/1983589119364985133">在单位感到委屈和不公平，是时候改变”归因模式”了</a></li><li><a href="https://zhuanlan.zhihu.com/p/1931407639830594999">职场人最该戒掉的认知：过度反思自己</a></li><li><a href="https://zhuanlan.zhihu.com/p/2064678319854039430">同事升职了你没升：把”凭什么是他”换成”我缺什么”</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近一年多，我在团队里看到一个有点反直觉的现象：AI 编程工具普及之后，大部分人的产出都上去了，但人和人之间的差距没有缩小，反而拉大了。&lt;/p&gt;
&lt;p&gt;按理说不该这样。同一件工具、同一类任务，所有人都能用；工具对每个人是公平的。那差额是从哪来的？&lt;/p&gt;
&lt;p&gt;我的答案是：来自我们怎么解释自己的处境。&lt;/p&gt;
&lt;p&gt;说得再直接一点：&lt;strong&gt;向内归因能解决更多问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这不是一句道德劝告，而是一个效率判断。同一个处境落到两个人手里，会向内归因的那个人，手里永远多出几个选项；而习惯向外归因的人，会先把选项一个个划掉，最后得出”我没什么可做的”。&lt;/p&gt;
&lt;p&gt;这篇文章想说清三件事：为什么它在今天比过去更管用，怎么才算真正做好它，以及我自己就是它的受益者。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Growth" scheme="https://springuper.github.io/tags/Growth/"/>
    
    <category term="Management" scheme="https://springuper.github.io/tags/Management/"/>
    
    <category term="Culture" scheme="https://springuper.github.io/tags/Culture/"/>
    
  </entry>
  
  <entry>
    <title>分金的人最后拿：制度设计的六个原则</title>
    <link href="https://springuper.github.io/institution-design-six-principles/"/>
    <id>https://springuper.github.io/institution-design-six-principles/</id>
    <published>2026-08-31T10:00:00.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>带团队这么多年，我越来越觉得：<strong>管理者的终极武器不是”盯人”，而是”设计规则”。</strong> 一个人盯得了三五个人，盯不了三五十个人；但一套好的制度，可以让几百上千人各就各位、自动运转。反过来，一套坏的制度，哪怕配上再努力的人，也会把人心和干劲一点点磨掉。</p><p>邓小平在《党和国家领导制度的改革》里说过一段很重的话：</p><blockquote><p>我们过去发生的各种错误，固然与某些领导人的思想、作风有关，但是组织制度、工作制度方面的问题更重要。这些方面的制度好可以使坏人无法任意横行，制度不好可以使好人无法充分做好事，甚至会走向反面。</p></blockquote><p>这段话虽然是针对国家治理讲的，放到组织管理里同样成立。<strong>好的制度设计，能够最大限度释放生产力、优化资源配置；坏的制度设计，则会让好人做不了好事。</strong> 这篇文章想把我这些年读到、听到、实践过的一些制度案例整理成六个原则。案例尽量真实可查，文末附有出处，供大家批判性参考。</p><span id="more"></span><h2 id="原则一：让规则的制定者承担后果-——-分金的人最后拿"><a href="#原则一：让规则的制定者承担后果-——-分金的人最后拿" class="headerlink" title="原则一：让规则的制定者承担后果 —— 分金的人最后拿"></a>原则一：让规则的制定者承担后果 —— 分金的人最后拿</h2><p>先说一个最经典的设计。</p><p>一伙强盗分金子，如果规定”负责分金的人最后一个拿”，会发生什么？很简单：为了自己不亏，分金的人只能尽可能把金子分得均匀。这里不需要任何监督、任何道德说教，规则本身就让分配者”不得不公平”。管理上有一个同款的老故事叫”七人分粥”：七个人分一锅粥，轮流分、轮流挑，怎么分都有人觉得不公平，最后采用的方案是”轮流分粥，但分粥的人要等别人先挑完、拿最后剩下的那份”——从此再没有人抱怨。</p><p>（注：这里说的”分金的人最后拿”，和博弈论里著名的”海盗分金”问题不是一回事。后者是五个海盗按”提案+投票”规则分 100 枚金币的逆向归纳题，标准解里提案者独得 98 枚——因为它只需各花 1 枚金币买通两名关键支持者，就能以多数票通过提案。同一个”分金”场景，只因为规则细节不同（谁提案、投票门槛多高、顺序怎么排），结果天差地别：<strong>制度细节即命运</strong>。）</p><p>这套思想在学术上叫<strong>机制设计（Mechanism Design）</strong>——经济学里研究”如何设计规则，让参与者在追求个人利益的同时，恰好达成设计者想要的结果”的一个分支，核心概念是<strong>激励相容（Incentive Compatibility）</strong>：把规则设计成”个人怎么做对自己最有利，恰好也是组织希望他做的”，就不需要外部监督。机制设计的奠基人之一、2007 年诺贝尔经济学奖得主马斯金（Eric Maskin），2019 年到访海尔时讲解这门理论，用的第一个例子就是”分蛋糕”——怎么保证两个孩子都觉得分到的蛋糕不比对方小，标准答案正是”切的人最后拿”（<a href="https://finance.sina.cn/2019-04-30/detail-ihvhiewr9153516.d.html">新浪财经</a>）。连诺奖得主讲机制设计都要从”分蛋糕”讲起，可见这个朴素原则的分量。</p><p>落到日常管理上，我自己的体会是：凡是涉及分配的场景，都值得问一句——**”定规则的人，自己最后拿吗？”** 排班的人自己先上夜班、分活的人自己先挑硬骨头、定 KPI 的人自己先被这套 KPI 考核……把这些”对称性”写进规则，比任何动员讲话都管用。</p><h2 id="原则二：让资源必须流动起来-——-用完才能再拿"><a href="#原则二：让资源必须流动起来-——-用完才能再拿" class="headerlink" title="原则二：让资源必须流动起来 —— 用完才能再拿"></a>原则二：让资源必须流动起来 —— 用完才能再拿</h2><p>另一种常见病是”囤积”。资源一旦”抢到就是我的”，大家就会拼命多占、占着不用，等真正需要的人想用的时候已经没有了。要治这个病，最有效的设计是**”用完才能再拿”（use it or lose it）**。</p><p>最直观的例子在消费领域：迪士尼度假俱乐部（Disney Vacation Club）的积分采用”用进废退”规则，年度积分到期清零（仅允许有限次数的”存点”，且有严格限制，见 <a href="https://plandisney.disney.go.com/question/banked-points-used-august-mean-book-something-august-stay-607721/">planDisney 官方解答</a>），防止会员囤积积分造成旺季挤兑。有趣的是 Netflix 走了相反的路：干脆不设年假制度，想休就休——没有”攒假”的概念，自然也没有”囤假”和”突击休假”的问题。<strong>两种设计殊途同归：消灭囤积。</strong></p><p>不过这类机制要小心设计：如果”用不完就收回”做得太粗暴——比如预算当年不用完就作废——反而会催生”年底突击花钱”式的浪费。防囤积的关键不是简单”过期作废”，而是”可以结转 + 按浪费问责”。</p><p>落到日常管理上：<strong>凡是”名额、预算、项目、人力”类资源，都值得设计一个”用不完就回收”的机制</strong>，否则一定会有人以囤积代替使用、用占有代替产出。</p><h2 id="原则三：让创造价值的人分享价值-——-分钱的制度"><a href="#原则三：让创造价值的人分享价值-——-分钱的制度" class="headerlink" title="原则三：让创造价值的人分享价值 —— 分钱的制度"></a>原则三：让创造价值的人分享价值 —— 分钱的制度</h2><p>第三类优秀实践，是”分钱”的制度。</p><p>华为被研究最多的不是技术，而是它的价值分配体系，概括起来是三根支柱（<a href="http://chnstone.com.cn/a/media/guanlizhihui/2026nian/2026/0702/6157.html">华夏基石</a>）：<strong>ESOP 虚拟受限股</strong>——员工按内部股价认购的虚拟股份，只有分红权和增值权、没有所有权，离职时由公司回购，让知识劳动者的长期贡献资本化；<strong>TUP 时间单位计划</strong>——按绩效免费授予、不花员工一分钱，让新人和外籍员工也能零门槛参与；<strong>饱和配股</strong>——按职级设定持股上限，防止老员工持股越积越多、躺平吃分红，给新人留出上升空间。创始人任正非个人持股长期只有约 1%，其余由员工持股平台持有；华为内部的原则是**”劳动回报优先于资本回报”**——分钱时先保证干活的人拿够，再谈资本收益。这套制度把成千上万的知识员工从”打工人”变成了”合伙人”。</p><p>三根支柱里最值得琢磨的是 TUP：公司根据你的绩效和贡献，白送你一批”分红收益权”，期限 5 年——第 1 年只确认额度，第 2 年解锁 1/3、第 3 年 2/3，第 4–5 年全额分红，第 5 年结算（当年分红 + 内部股价涨幅对应的增值收益）后整批清零。而且只要绩效持续达标，每年都会再授予新批次，多批叠加、错开到期：</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></pre></td><td class="code"><pre><span class="line">批次1:  [确认]  1/3    2/3    全额    结算清零</span><br><span class="line">批次2:          [确认]  1/3    2/3    全额    结算清零</span><br><span class="line">批次3:                  [确认]  1/3    2/3    全额    结算清零</span><br><span class="line">年份:   1       2       3       4       5       6       7</span><br></pre></td></tr></table></figure><p><strong>TUP 不是资产，而是限时收益窗口</strong>：它不像股票那样拿了就永远是你的，每一批到期清零；但它也不会没收已到手的收益——躺平只意味着新批次断供，已授予的旧批次仍会按原计划兑现完整个周期，收入随之渐进下滑，短则一两年、长则五年明显缩水甚至归零——制度上不存在”吃老本”的稳态。</p><p>而奋斗者和躺平者的差距是<strong>复利式</strong>的：一个年年叠加新批次，一个只出不进，几年下来收入差出好几倍。这不是偶然，而是华为的制度意图——任正非说得直白：**”让拉车人比坐车人拿得多”**（<a href="https://www.c114.com.cn/news/126/a868472.html">C114</a>）。TUP 把”奋斗溢价”做成了可预期的长期复利，而不是靠领导拍脑袋打分。</p><p>胖东来是另一个把”分钱”做得很彻底的企业。2026 年 3 月，创始人于东来公布了胖东来约 40 亿元资产利润的分配标准：管理团队约 50%、员工约 50%（每年利润的一半给团队发奖金、一半归股东收益），创始人于东来个人在股本中的占比仅约 5%（<a href="https://finance.ifeng.com/c/8rVbqqmxh97">凤凰财经/第一财经</a>）。于东来自己说，这是胖东来”已经持续二十多年的分配体制”，靠的是”制度和股份制的运营模式”。胖东来员工的工资长期显著高于当地零售业同行——不过于东来近年也坦言在薪资上”太溺爱员工”（<a href="https://m.jiemian.com/article/14572778.html">界面新闻</a>），可见这个模式同样有它的争议和成本。</p><p>这个思路在国际上也不新鲜。美国林肯电气（Lincoln Electric）一百多年前就建立了一套组合：<strong>计件工资 + 年终利润分享 + 承诺不裁员</strong>，被管理学界视为”激励性管理”的教科书案例（<a href="https://www.sohu.com/a/932384192_122014422">搜狐财经</a>）。Costco 则用”显著高于行业的工资 + 远低于同行的员工流失率”证明了善待员工就是最好的生意（<a href="https://www.cheers.com.tw/article/article.action?id=5105802">Cheers</a>）。</p><p>海底捞的”师徒制”是服务业的变体：店长培养出合格的徒弟、帮徒弟开出新店，就可以从徒弟店的利润中持续分成，形成”徒子徒孙”的利益链条。创始人张勇说得直白：”自己不想去开店，就想尽一切办法让别人去开”（<a href="https://i.ifeng.com/c/8EzRcFtYZw3">凤凰网</a>）。<strong>让培养人的人分享被培养者创造的价值</strong>，人才自然能源源不断地被培养出来。</p><p>这些案例背后的道理，古人已经总结过了——<strong>财聚人散，财散人聚</strong>。但要注意，分钱不是平均主义，而是”按贡献分、可核算地分”：华为的饱和配股、林肯电气的计件、海底捞的师徒分成，都建立在<strong>贡献可以被计量</strong>的前提上，否则分着分着就变成大锅饭。</p><h2 id="原则四：用高薪买高产出-——-少人高薪"><a href="#原则四：用高薪买高产出-——-少人高薪" class="headerlink" title="原则四：用高薪买高产出 —— 少人高薪"></a>原则四：用高薪买高产出 —— 少人高薪</h2><p>第四个原则，是”人少一点、钱多一点、活干多一点”。</p><p>任正非 2013 年在内部讲话里说得很直白（<a href="https://people.techweb.com.cn/2013-07-19/1311136.shtml">钛媒体/TechWeb</a>）：</p><blockquote><p>我不眼红年轻人拿高工资，贡献很大才能拿到这么高的工资……<strong>三个人拿四个人的钱，干五个人的活，就是我们未来的期望</strong>。</p></blockquote><p>这话翻译成大白话，就是”两个人干四个人的活、拿三个人的钱”（网上俗称”234 付薪”；任正非原话是另一个比例——3 个人拿 4 份钱、干 5 个人的活，逻辑完全一致）：员工人均收入高于同行，公司总人力成本反而低于同行，因为产出密度上去了。算一笔账（以 2:4:3 为例）：同样的活过去要付 4 份工资，现在 2 个人干完、只付 3 份钱——两个人各拿 1.5 份，人均收入涨了 50%；公司总支出从 4 份降到 3 份，省了 25%。员工多拿、公司少付，双赢。</p><p>把这件事做到极致的，是 Netflix。它的<a href="https://jobs.netflix.com/culture">文化手册</a>写得很直白：只招”顶级人才”，付<strong>行业最高薪</strong>，不搞绩效排名，靠”留任测试（keeper test）”——管理者要问自己”如果这个员工明天提离职，我会拼命挽留吗？”答案是否定的，就体面地请走。它的逻辑是：<strong>与其养 10 个平庸的人，不如花同样的钱养 3 个顶尖的人</strong>，后者的产出和创造力远高于前者（详见 Patty McCord 的 <a href="https://hbr.org/2014/01/how-netflix-reinvented-hr">《How Netflix Reinvented HR》</a>）。</p><p>当然，这条路有严格的前提，业内也不乏质疑——营销人”老苗”就专门写过文章吐槽”三个人拿四个人的钱干五个人的活”容易变成画饼（<a href="http://cmmo.cn/b/746871/817024.html">销售与市场</a>）。我认为这个模式成立至少需要三个条件：<strong>① 产出可以衡量</strong>（干多干少看得见）；<strong>② 招聘标准足够高</strong>（人少必须精）；<strong>③ 有体面的淘汰机制</strong>（不合适的人能体面离开，而不是大家一起磨洋工）。缺了这些，”高薪高效”就会退化成”高薪低效”。</p><h2 id="原则五：划小核算单元，把权力交给一线"><a href="#原则五：划小核算单元，把权力交给一线" class="headerlink" title="原则五：划小核算单元，把权力交给一线"></a>原则五：划小核算单元，把权力交给一线</h2><p>第五个原则，是把组织划小，让”听得见炮声的人”做决策。</p><p>稻盛和夫在京瓷推行的<strong>阿米巴经营</strong>，是把公司切成一个个几十人的小单元，每个单元独立核算、自负盈亏，全员参与经营，目标是”销售最大化、经费最小化”（<a href="https://cn.kyocera.com/inamori/archive/lectures/amoeba.html">京瓷官网</a>）。日本航空破产重组时，稻盛和夫也是靠这套”让每个小单元都算账”的制度，一年多就实现扭亏为盈。</p><p>海尔从 2005 年开始搞<strong>人单合一</strong>：”人”是员工（海尔称之为”创客”），”单”是用户的需求和价值，”合一”就是让每个员工直接对用户负责、从自己创造的用户价值中获取报酬（”用户付薪”），而不是听命于上级、从企业领固定工资（<a href="https://www.haier.com/press-events/news/20250922_271884.shtml">海尔官网</a>）。配套的组织变革是把科层制拆成”平台 + 小微”，张瑞敏的口号是：”企业里每个人都应该成为自己的 CEO。”</p><p>国内电商圈有一个更轻巧的版本：韩都衣舍的<strong>三人小组制</strong>。每个小组由设计、运营、货品三人组成，对单款产品的企划、定价、库存全权负责、自负盈亏——卖不掉的库存由小组自己消化，收益直接和利润挂钩。责任、权力、利益在最小的单元里统一了，公司层面只做平台和赋能（<a href="https://sd.dzwww.com/sdnews/201908/t20190814_19058968.htm">大众日报</a>；<a href="https://www.jiemian.com/article/1418917.html">界面新闻</a>）。</p><p>制造业里最动人的设计是丰田的<strong>安灯（Andon）</strong>：任何一线工人发现质量问题，都可以拉绳停掉整条生产线，直到问题解决（<a href="https://www.pinzhi.org/forum.php?mod=viewthread&tid=57656">《丰田模式》</a>）。把”叫停权”交给最基层的人，看起来是放弃了控制，实际上是把纠错成本降到了最低——问题在源头被消灭，而不是流到下游变成事故。亚马逊的”两个披萨团队”（团队小到两个披萨够吃，<a href="https://baike.baidu.com/item/%E5%8F%8C%E6%AF%94%E8%90%A8%E5%9B%A2%E9%98%9F/4293637">百度百科</a>）也是同一个思路：<strong>规模小、自治强、沟通成本低</strong>。</p><p>华为内部那句著名的”<strong>让听得见炮声的人呼唤炮火</strong>“，说的也是权力要跟着信息和责任走。我自己的经验印证了这一点：<strong>核算单元越小、责权利越统一的地方，人的主观能动性越强。</strong></p><h2 id="原则六：制度也要做减法-——-砍掉反激励"><a href="#原则六：制度也要做减法-——-砍掉反激励" class="headerlink" title="原则六：制度也要做减法 —— 砍掉反激励"></a>原则六：制度也要做减法 —— 砍掉反激励</h2><p>最后一条容易被忽略：<strong>好制度不只是加法，更是减法。</strong></p><p>微软在 2013 年取消了臭名昭著的员工强制排名（stack ranking）——每个团队强制按曲线排名、末位淘汰，结果是团队内耗、人人自保、优秀员工流失。放弃这套排名后，微软的绩效体系转向”协作 + 成长”导向，外界普遍认为这是微软后来重新崛起的重要人事变革之一（<a href="http://finance.people.com.cn/n/2013/1210/c348883-23795952.html">人民网</a>）。</p><p>更极端的案例是晨星公司（Morning Star），全球最大的番茄加工商之一：<strong>没有 CEO、没有管理层</strong>，每个员工通过”同事谅解书（CLOU）”自己定目标、自己申请采购预算、自己评估同事，靠自我管理运转了几十年（<a href="https://m.jiemian.com/article/1210056.html">界面新闻</a>）。</p><p>我之前写过一篇《Just Culture》的文章，讲”公正文化”：处理事故时问”哪里出了问题”而不是”谁出了问题”。制度做减法，本质上也是这个态度——<strong>把惩罚导向、控制导向的制度，换成信任导向、学习导向的制度</strong>。坏的制度（末位排名、无休止审批、问责文化）看起来是在”管理”，实际上每一条都在悄悄消耗人的主动性。</p><h2 id="小结：制度设计的六个检查点"><a href="#小结：制度设计的六个检查点" class="headerlink" title="小结：制度设计的六个检查点"></a>小结：制度设计的六个检查点</h2><p>把上面的原则压缩成一张给管理者的自查清单：</p><ol><li><strong>定规则的人承担后果吗？</strong>——谁定规则，谁最后拿（分金的人最后拿）；</li><li><strong>资源会流动吗？</strong>——有没有”用完才能再拿、用不完就回收”的机制，防止囤积；</li><li><strong>钱分对了吗？</strong>——分钱是否与可核算的贡献挂钩？劳动回报是否优先于资本回报？</li><li><strong>薪酬买到的是产出还是时间？</strong>——是在养”人”还是在养”岗位”？有没有配套的淘汰机制？</li><li><strong>核算单元够小吗？</strong>——一线有没有决策权、叫停权？责权利是否在最小单元统一？</li><li><strong>有没有该砍的制度？</strong>——哪些规则正在制造内耗、官僚和反激励？</li></ol><p>这六个检查点背后其实只有一个问题：<strong>制度是否让每个人的”个人利益”与”组织目标”激励相容，让资源在流动中增值，让创造价值的人分享价值。</strong> 如果答案是肯定的，组织会像有了自己的发动机；如果是否定的，再多的会议、再多的监督、再多的口号，都只是给一台失灵的机器打补丁。</p><p>我以前写过一篇《管理的杠杆》，讲注入使命、盘活资源、分享利益。现在回头看，<strong>制度设计才是那根最硬的杠杆</strong>——使命、资源、利益，最终都要落到规则上才能被放大和固化。与其抱怨人心不古，不如把规则设计好。这大概也是管理者最值得投入的方向。</p><p>（如果你也对制度设计感兴趣，或者见过什么好机制、坏机制，欢迎交流指正。）</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li>邓小平：《党和国家领导制度的改革》（1980 年 8 月 18 日）</li><li><a href="https://finance.sina.cn/2019-04-30/detail-ihvhiewr9153516.d.html">马斯金：机制设计是理论，人单合一是实践（新浪财经）</a></li><li><a href="http://chnstone.com.cn/a/media/guanlizhihui/2026nian/2026/0702/6157.html">华为 ESOP、TUP 与饱和配股如何打造知识型企业命运共同体（华夏基石）</a></li><li><a href="https://www.c114.com.cn/news/126/a868472.html">任正非谈华为内部激励：让拉车人比坐车人拿得多（C114，2014）</a></li><li><a href="https://people.techweb.com.cn/2013-07-19/1311136.shtml">任正非：三个人拿四个人的钱干五个人的活是公司的期望（钛媒体/TechWeb，2013）</a></li><li><a href="https://finance.ifeng.com/c/8rVbqqmxh97">“40 亿资产利润分配给员工”，于东来再次回应（第一财经/凤凰财经，2026-03）</a></li><li><a href="https://m.jiemian.com/article/14572778.html">于东来称薪资上太溺爱员工，其实不值这么多钱（界面新闻）</a></li><li><a href="https://www.sohu.com/a/932384192_122014422">衡量一个 CEO 是否伟大，就看他能否激发组织活力与个人潜能——林肯电气激励系统（搜狐财经）</a></li><li><a href="https://www.cheers.com.tw/article/article.action?id=5105802">曾在仓库开堆高机，他 40 年后成好市多 CEO：善待员工就是最好的生意（Cheers）</a></li><li><a href="https://i.ifeng.com/c/8EzRcFtYZw3">对话海底捞 CEO：从张勇到杨利娟（凤凰网）</a></li><li><a href="http://cmmo.cn/b/746871/817024.html">老苗撕营销｜”三个人拿四个人的钱干五个人的活”，咱能不扯吗？（销售与市场）</a></li><li><a href="https://jobs.netflix.com/culture">Netflix 文化手册（Jobs at Netflix）</a>；Patty McCord，《<a href="https://hbr.org/2014/01/how-netflix-reinvented-hr">How Netflix Reinvented HR</a>》（Harvard Business Review，2014-01）</li><li><a href="https://cn.kyocera.com/inamori/archive/lectures/amoeba.html">阿米巴经营带来企业持续发展（京瓷官网）</a></li><li><a href="https://www.haier.com/press-events/news/20250922_271884.shtml">人单合一：20 年的颠覆，20 年的坚守（海尔官网）</a></li><li><a href="https://sd.dzwww.com/sdnews/201908/t20190814_19058968.htm">从淘宝小店到互联网品牌生态运营集团：韩都衣舍（大众日报）</a></li><li><a href="https://www.jiemian.com/article/1418917.html">传统企业转型案例之一：韩都衣舍——走近柔性供应链（界面新闻）</a></li><li><a href="https://www.pinzhi.org/forum.php?mod=viewthread&tid=57656">暂停流程以内建质检（自动化）——安灯机制（《丰田模式》节选）</a></li><li><a href="https://baike.baidu.com/item/%E5%8F%8C%E6%AF%94%E8%90%A8%E5%9B%A2%E9%98%9F/4293637">双比萨团队（百度百科）</a></li><li><a href="http://finance.people.com.cn/n/2013/1210/c348883-23795952.html">微软自救绝招：放弃绩效评比制度（人民网，2013）</a></li><li><a href="https://m.jiemian.com/article/1210056.html">没有 CEO 更没有管理层：它是全球最大的西红柿加工厂（界面新闻）</a></li><li><a href="https://plandisney.disney.go.com/question/banked-points-used-august-mean-book-something-august-stay-607721/">迪士尼度假俱乐部积分规则官方问答（planDisney）</a></li><li><a href="https://pro.huxiu.com/article/476385.html">从”海盗分金”聊聊：利益博弈中的生存法则（虎嗅）</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;带团队这么多年，我越来越觉得：&lt;strong&gt;管理者的终极武器不是”盯人”，而是”设计规则”。&lt;/strong&gt; 一个人盯得了三五个人，盯不了三五十个人；但一套好的制度，可以让几百上千人各就各位、自动运转。反过来，一套坏的制度，哪怕配上再努力的人，也会把人心和干劲一点点磨掉。&lt;/p&gt;
&lt;p&gt;邓小平在《党和国家领导制度的改革》里说过一段很重的话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们过去发生的各种错误，固然与某些领导人的思想、作风有关，但是组织制度、工作制度方面的问题更重要。这些方面的制度好可以使坏人无法任意横行，制度不好可以使好人无法充分做好事，甚至会走向反面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这段话虽然是针对国家治理讲的，放到组织管理里同样成立。&lt;strong&gt;好的制度设计，能够最大限度释放生产力、优化资源配置；坏的制度设计，则会让好人做不了好事。&lt;/strong&gt; 这篇文章想把我这些年读到、听到、实践过的一些制度案例整理成六个原则。案例尽量真实可查，文末附有出处，供大家批判性参考。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Organization" scheme="https://springuper.github.io/tags/Organization/"/>
    
    <category term="Management" scheme="https://springuper.github.io/tags/Management/"/>
    
    <category term="Institution" scheme="https://springuper.github.io/tags/Institution/"/>
    
  </entry>
  
  <entry>
    <title>给大脑配一副好鞍具：五款 Agent Harness 的解剖与实测</title>
    <link href="https://springuper.github.io/agent-harness-comparison/"/>
    <id>https://springuper.github.io/agent-harness-comparison/</id>
    <published>2026-08-23T22:00:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>如果你用过两个以上的 AI 编程工具，大概率会有这种困惑：<strong>同一个模型，装在 Claude Code 里和装在 OpenCode 里，干活的样子完全不一样。</strong> 一个慢条斯理、先列计划再动手；一个风风火火、脚本一甩就开跑；一个问东问西，一个闷头就干。模型明明是同一个，差别从哪来的？</p><p>答案藏在 harness 里。</p><p>“Harness” 的英文原意是<strong>马具、鞍具</strong>——套在牲口身上，让它的力气能变成拉车、耕地这些实际用途的那套东西。AI 语境下，<a href="https://code.claude.com/docs/en/glossary">Anthropic 官方文档</a>给这类工具起过一个很准确的名字：<strong>agentic harness</strong>（Agent 鞍具）。大模型本身只会”说话”——从概率里吐出下一段 token；是 harness 给它装上了”手脚”（读写文件、跑命令）、”眼睛”（观察执行结果）、”红绿灯”（权限审批），它才真正能干起活来。</p><p>所以，<strong>你买的从来不是模型，而是”模型 + 一整套鞍具”。</strong> 这篇文章想做的事很朴素：拿一把解剖刀，把五款有代表性的 agent harness——Claude Code、Codex、OpenCode、Pi、DeepSeek Harness——按同一套框架拆开，看看它们各自把”身体”装成了什么样；再把我亲手实测的记录摆上来，让结论都落在事实上。</p><p>为了拆得干净，我给自己配了一套统一的解剖框架。先讲框架，再拆标本。</p><span id="more"></span><h2 id="一、解剖学：Agent-Harness-的五件套"><a href="#一、解剖学：Agent-Harness-的五件套" class="headerlink" title="一、解剖学：Agent Harness 的五件套"></a>一、解剖学：Agent Harness 的五件套</h2><p>把几款 agent harness 拆开来看，最终都会得到同一张地图：<strong>无论哪一款，拆到底都是这五件套的不同配法。</strong></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><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">+--------------------------------------------+</span><br><span class="line">| 模型 LLM（大脑）                           |</span><br><span class="line">| 只会&quot;说话&quot;，不会&quot;干活&quot;                     |</span><br><span class="line">+--------------------------------------------+</span><br><span class="line">                      | system prompt + 工具 schema</span><br><span class="line">                      v</span><br><span class="line">+--------------------------------------------+</span><br><span class="line">| agent harness（鞍具）                      |</span><br><span class="line">|                                            |</span><br><span class="line">| 1. 循环 Loop   思考 -&gt; 行动 -&gt; 观察        |</span><br><span class="line">| 2. 工具 Tools  bash / fs / edit            |</span><br><span class="line">| 3. 记忆 Memory 上下文 / 压缩 / 会话持久化  |</span><br><span class="line">| 4. 权限 Sandbox 能自动做 vs 要问人         |</span><br><span class="line">| 5. 界面 UI     TUI / Web / headless        |</span><br><span class="line">|                                            |</span><br><span class="line">| + 扩展点  MCP / 插件 / subagent / hooks    |</span><br><span class="line">+--------------------------------------------+</span><br></pre></td></tr></table></figure><p>这五件套，每一件的职责都很清楚：</p><p><strong>① 循环（Loop）</strong>——agent 的”反射弧”。大模型不会自己反复思考，是 harness 在它外面套了一层 <code>think → act → observe</code> 的循环：让模型生成一段思考（要不要调用工具？调哪个？），harness 执行工具，把结果喂回去，再让模型接着想。这一圈一圈转下来，才叫”自主干活”。ReAct 循环本身并不神秘，几十行代码就能写出来；**麻烦的是循环的”质量”**——什么时候该停、出错怎么恢复、多轮工具调用怎么组织，这些才是 harness 的功夫。</p><p><strong>② 工具（Tools）</strong>——agent 的”手脚”。读写文件、执行命令、搜索、编辑……每个工具都是一块肌肉。五件套里它最容易理解，但也是最容易”失血”的地方：工具怎么描述给模型（schema 写得好不好）、参数怎么校验、结果怎么截断，直接决定模型”手脚灵不灵活”。</p><p><strong>③ 记忆（Memory）</strong>——上下文窗口是模型唯一的”工作台面”，而它<strong>又小又贵</strong>（哪怕 1M 上下文也装不下一个大型仓库）。harness 的功课是：往窗口里放什么、不放什么、装不下时怎么压缩、会话结束后怎么持久化。”记忆”这一件套，是五款工具差异最大的地方之一。</p><p><strong>④ 权限/沙箱（Permissions &amp; Sandbox）</strong>——agent 的”红绿灯”。文件能不能随便改？命令能不能随便跑？网络能不能访问？有的 harness 默认全放行（信任用户），有的默认全拦截（信任沙箱），大部分在这两者之间划了一条”需要问人”的线。<strong>这一件套的配法，直接决定了工具给人的”安全感”和”爽快感”，也最能看出一个团队的工程哲学。</strong></p><p><strong>⑤ 界面/入口（UI）</strong>——驾驶舱长什么样。终端 TUI、浏览器 Web、无头 headless、API/SDK……同一个引擎可以配不同的驾驶舱，这也是后文 DSH 的 profile 玩法。</p><p>还有一个横切的<strong>扩展点（Extensibility）</strong>：MCP 服务器、插件、subagent、hooks。它不是独立的第六件套，而是”这五件套都允许你改装”的能力。</p><p>有了这张地图，再看五款 harness，就不需要记一堆功能列表了——<strong>只需要问：它的五件套，各自是怎么配的？哪些是焊死的，哪些是能换的？</strong> 下面五个角色登场，然后我们逐个解剖。</p><h2 id="二、五个角色登场"><a href="#二、五个角色登场" class="headerlink" title="二、五个角色登场"></a>二、五个角色登场</h2><p>先给五款工具各发一张”身份卡”，顺便配上拟人化的印象。注意，所有数字都标注了抓取日期，因为开源项目的 star 数每天都在变。</p><table><thead><tr><th></th><th>Claude Code</th><th>Codex</th><th>OpenCode</th><th>Pi</th><th>DeepSeek Harness</th></tr></thead><tbody><tr><td><strong>出身</strong></td><td>Anthropic（2025-02 随 Claude 3.7 Sonnet 发布研究预览）</td><td>OpenAI（CLI 于 2025-04 开源）</td><td>sst 团队发起（现归 anomalyco 维护）</td><td>Earendil Works（作者 badlogic）</td><td>DeepSeek AI</td></tr><tr><td><strong>开源</strong></td><td>闭源产品</td><td>CLI 开源（Rust，Apache-2.0）</td><td>全开源（TypeScript，MIT）</td><td>全开源（TypeScript，MIT）</td><td>全开源（MIT，developer preview）</td></tr><tr><td><strong>GitHub star</strong>（2026-08-23）</td><td>—（闭源）</td><td><a href="https://github.com/openai/codex">114k</a></td><td><a href="https://github.com/anomalyco/opencode">200k</a></td><td><a href="https://github.com/earendil-works/pi-mono">96k</a></td><td><a href="https://github.com/deepseek-ai/deepseek-harness">186k</a></td></tr><tr><td><strong>模型绑定</strong></td><td>默认 Claude 系（可用 Anthropic 兼容 API 换）</td><td>默认 GPT 系（config.toml 可换）</td><td>中立（几十家 provider + 本地模型）</td><td>中立（统一 API + 订阅 OAuth）</td><td>默认 DeepSeek 适配器（provider 可插拔）</td></tr><tr><td><strong>默认界面</strong></td><td>终端 TUI（React/Ink）</td><td>终端 TUI（Rust）</td><td>终端 TUI + 桌面 App + IDE 插件</td><td>终端 TUI（自研渲染引擎）</td><td>Web UI（<code>dsh web</code>）/ headless / TUI</td></tr><tr><td><strong>招牌机制</strong></td><td>CLAUDE.md、hooks、subagents</td><td>AGENTS.md、审批模式、多层沙箱</td><td>provider 中立、opencode.json、LSP</td><td>无内置权限系统、可自我扩展</td><td>一切皆插件、profile 与配置层叠</td></tr></tbody></table><p>拟人化的第一印象是这样的：</p><ul><li><strong>Claude Code = 五星酒店的全能管家。</strong> 服务周到，门禁森严，会自己安排小团队（subagents），还会在你进门时记住你的喜好（CLAUDE.md）。代价是：默认得住他家酒店（模型绑定偏软，Anthropic 兼容 API 也能换 DeepSeek/MiniMax，见 3.1 实测），菜单是后厨定的（闭源）。</li><li><strong>Codex = 学院派的实验室主任。</strong> 纪律严明——先沙箱再动手，审批分级一丝不苟；论文公开（架构文档、AGENTS.md 公约），连实验设备都开源了（CLI 开源、2026 年连整套 Harness 也开源了）。</li><li><strong>OpenCode = 开源美食广场。</strong> 谁家模型都能来摆摊，摊位多到挑花眼；不站队是它最大的卖点，也是它最大的压力（二十万 star 的社区治理）。</li><li><strong>Pi = 极客的改装车手。</strong> 车是自己焊的（自研 TUI），<strong>默认不装安全带</strong>（没有内置权限系统），随时能拆开看（可自我扩展）。开得快，但安全自负。</li><li><strong>DeepSeek Harness = 乐高工厂。</strong> 一切皆插件，配置靠 patch 一层层叠汉堡；web、headless、TUI 是同一个引擎换外壳。缺点是还在施工（developer preview），图纸会改。</li></ul><p>下面进入正题，逐个解剖。每节都用同一把刀：循环怎么跑、记忆怎么管、权限怎么设、怎么扩展，外加一节实测记录。实测的方法和局限，我会在最后一节统一交代，这里先看结果。</p><h2 id="三、逐个解剖"><a href="#三、逐个解剖" class="headerlink" title="三、逐个解剖"></a>三、逐个解剖</h2><h3 id="3-1-Claude-Code：管家的服务体系"><a href="#3-1-Claude-Code：管家的服务体系" class="headerlink" title="3.1 Claude Code：管家的服务体系"></a>3.1 Claude Code：管家的服务体系</h3><p>Claude Code 是 2025 年把”AI 编程”这个词从演示变成日常的工具，也是整个品类的参照系。它的五件套配法，可以用一个词概括：<strong>服务周全</strong>。</p><p><strong>循环：最典型的 agent loop + hooks。</strong> 它的主循环和其他家没有本质区别（think → act → observe），但它给循环装上了<strong>挂钩点（hooks）</strong>——这是它最精巧的设计之一。你可以把 hook 理解成”在循环的特定阶段插一脚”：模型调用工具之前、之后、用户提交输入时、会话结束时……都能挂一段你自己的脚本，用来拦截、改写，或在一旁记录。</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><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">用户输入</span><br><span class="line">  │</span><br><span class="line">  ▼</span><br><span class="line">[UserPromptSubmit hook]  ← 可在此改写输入</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">[PreToolUse hook] ──► 工具执行 ──► [PostToolUse hook]</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">继续循环 或 收尾 ──► [Stop hook] ──► [SessionEnd hook]</span><br><span class="line"></span><br><span class="line">其他挂点：SessionStart / PreCompact（压缩前）/ SubagentStop（子 agent 结束）</span><br><span class="line">/ Notification（后台任务完成提醒）</span><br></pre></td></tr></table></figure><p>举个最实用的例子：团队想让”提交代码前自动跑 lint 和测试”，不需要改任何产品代码，只需要在 <code>PreToolUse</code> 或 <code>Stop</code> 阶段挂一个 hook，让 Claude Code 在动手前/收尾时强制执行 <code>npm run lint &amp;&amp; npm test</code>。这相当于<strong>把公司流程写进了鞍具里</strong>——<a href="https://code.claude.com/docs/en/hooks">官方文档</a>把这套机制开放给了所有用户，这也是它生态粘性的来源之一。</p><p><strong>记忆：CLAUDE.md 是长期记忆，上下文裁剪是短期记忆。</strong> CLAUDE.md（项目级 + 用户级）是给 agent 的”入职手册”，每次会话自动读入；配合 <code>.claude/</code> 目录下的子 agent 定义（subagents），相当于给管家配了”部门手册”和”下属名单”。上下文管理则交给内置的压缩策略，用户一般不用操心。</p><p><strong>权限：默认要问，可以授权。</strong> 第一次跑危险命令，它会停下来问你要不要允许；你可以记下”允许这条规则”，也可以整段放开。<code>--dangerously-skip-permissions</code> 这种 flag 的存在，说明它把”放权”也做成了显式操作——<strong>管家不会自作主张，但你说放行它就放行。</strong></p><p><strong>扩展：MCP + 插件 + hooks + subagents，四件套齐全。</strong> 2025 年下半年开始支持插件生态，配合 MCP 服务器接入外部工具，可扩展性是五家里最完整的之一——当然，这一切都发生在一个闭源的壳里，你能改的是”挂什么”，不能改的是”引擎本身”。</p><p><strong>实测记录。</strong> 我这台机器上 Claude Code 2.1.3 的配置很有意思：<code>~/.claude/settings.json</code> 里把 <code>ANTHROPIC_BASE_URL</code> 指到了 DeepSeek 的 Anthropic 兼容端点（<code>https://api.deepseek.com/anthropic</code>），模型换成了 deepseek-v4-flash——<strong>Claude Code 跑着 DeepSeek 模型</strong>。这说明”模型绑定”这一格其实是软的：只要厂商提供 Anthropic Messages 兼容端点，鞍具就能换马（DeepSeek 的路径是 <code>/anthropic/v1/messages</code>，MiniMax 也提供同款）。本次实测里，五款工具因此<strong>统一用上了 deepseek-v4-flash</strong>，对比完全落在 harness 本身（详见第五节）。实测任务一（修复 3 个注入 bug 并让测试全过）：37 秒完成，11 轮对话，输入 2.2 万 + 缓存读取 14 万 + 输出 0.1 万 token（合计约 16.3 万），成本 0.216 美元——五家里最贵的一趟（它偏好”多轮多确认”的风格 + 大量缓存读取）。它的修复过程很”管家”：先读全文件，明确列出三个 bug，一次性精准编辑，再跑测试验证。</p><h3 id="3-2-Codex：学院派的纪律与沙箱"><a href="#3-2-Codex：学院派的纪律与沙箱" class="headerlink" title="3.2 Codex：学院派的纪律与沙箱"></a>3.2 Codex：学院派的纪律与沙箱</h3><p>Codex 的开源 CLI（Rust，Apache-2.0）是 2025-04 发布的。如果说 Claude Code 的五件套关键词是”服务”，Codex 的关键词是<strong>纪律</strong>——尤其是权限/沙箱这一件，它做得最重、最认真。</p><p><strong>权限：审批模式分级 + 沙箱层层设防。</strong> 这是 Codex 最值得讲的部分。它把”模型想执行命令”这件事拆成两级关卡：先是审批策略（什么时候需要问人），再是沙箱（命令在什么隔离环境里跑）。</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><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><br><span class="line">     │</span><br><span class="line">     ▼</span><br><span class="line">┌────────────────────────────────┐</span><br><span class="line">│ 1. 审批策略                    │ deny ──► 命令不执行，错误回给模型</span><br><span class="line">│ auto（默认，视命令而定）       │</span><br><span class="line">│ never（从不询问）              │</span><br><span class="line">│ on-request（由模型决定）       │</span><br><span class="line">└────────────────────────────────┘</span><br><span class="line">                │ allow</span><br><span class="line">                ▼</span><br><span class="line">┌────────────────────────────────┐</span><br><span class="line">│ 2. 沙箱选择                    │</span><br><span class="line">│ read-only：只读，连写都拦      │</span><br><span class="line">│ workspace-write：工作区可写    │</span><br><span class="line">│ full-access：完全放行          │</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">│ macOS: Seatbelt(sandbox-exec)  │</span><br><span class="line">│ Linux: Landlock + seccomp      │</span><br><span class="line">│ 可选: Docker / Nix+Bubblewrap  │</span><br><span class="line">└────────────────────────────────┘</span><br><span class="line">                │</span><br><span class="line">                ▼</span><br><span class="line">           执行命令</span><br></pre></td></tr></table></figure><p>值得注意的细节：Codex 的沙箱不是”建议”，而是<strong>默认开启</strong>。<code>--sandbox read-only</code> 模式下它连写文件都要经过沙箱；macOS 上每个 shell 命令都包一层 <code>sandbox-exec</code>（Seatbelt），Linux 上用 Landlock + seccomp。命令的”工作区权限”和”网络权限”是分开管的，需要联网的操作（比如装依赖）会明确触发更高权限的请求。这套设计在五家里最接近”企业级”。</p><p><strong>记忆：AGENTS.md 公约 + 项目记忆。</strong> 它推动的 <a href="https://developers.openai.com/codex/agreements">AGENTS.md 约定</a> 是一个跨工具的”项目入职手册”标准——写一次，多个 agent 工具都能读。这算是它给行业留下的一个”公共品”。</p><p><strong>扩展：MCP 支持 + config.toml 换 provider。</strong> 我的实测配置就是证据：<code>~/.codex/config.toml</code> 里把 <code>model_provider</code> 换成了 DeepSeek（<code>base_url: api.deepseek.com</code>），跑的是 deepseek-v4-flash。<strong>Codex CLI 换非 OpenAI 模型，改几行配置就行</strong>——它的 harness 层和模型层解耦得很干净。</p><p><strong>2026-08 的大动作：Codex Harness 开源。</strong> OpenAI 在 8 月把整套 Codex Harness 开源了（Apache-2.0，<a href="https://developers.openai.com/blog/codex-as-a-platform">官方博客</a>），包含三件东西：<code>codex exec</code> 命令行、TypeScript/Python SDK、以及 app-server（通过 JSON-RPC 连到本地 Codex 进程，提供持久会话、事件流、人在环审批）。最震撼的是一组数据：<strong>两个 harness 设计改动，把 GPT-5.6 Sol 在 ARC-AGI-3 上的成绩从 13.3% 提到 38.3%，输出 token 降了 6 倍</strong>——“是 harness 设计，而不是模型大小，驱动了 agent 的收益”（<a href="https://www.edgen.tech/news/post/openai-open-sources-codex-harness-cutting-agent-token-use-sixfold">报道</a>）。这句话，几乎是整篇文章的题眼：<strong>当模型越来越同质化、越来越便宜，鞍具本身成了决定”一匹马能拉多少货”的关键变量。</strong></p><p><strong>实测记录。</strong> Codex CLI 0.146.0 + deepseek-v4-flash 跑同一个修复任务：25 秒，<strong>只用 1762 个 token 就完成了</strong>——五家里最省，比我预期的少一个数量级。它读代码 → 定位 → 一次编辑 → 跑测试，节奏干净利落，没有任何多余的来回。这跟”纪律”的设定完全一致：不废话，干完就停。</p><p>我还在一个”外层已有沙箱”的环境里跑了它，收获了一个意外但绝佳的观察：当 macOS 的 <code>sandbox-exec</code> 无法嵌套启动时，<strong>Codex 宁可失败也不绕过</strong>——它试了不同 shell、不同目录，确认是环境问题后，0 改动地退出，并如实汇报”审批策略是 never，我不能擅自提权”。13,339 个 token 全花在诊断和诚实汇报上。这就是”纪律”两个字在工程上的样子：<strong>安全边界是硬约束，不是建议。</strong></p><h3 id="3-3-OpenCode：美食广场的兼容性"><a href="#3-3-OpenCode：美食广场的兼容性" class="headerlink" title="3.3 OpenCode：美食广场的兼容性"></a>3.3 OpenCode：美食广场的兼容性</h3><p>OpenCode 从 sst 团队手里长出来（现在归 anomalyco 维护），TypeScript 全开源，MIT，是五家里 star 最高的（20 万）。它的五件套关键词：<strong>兼容</strong>。</p><p><strong>循环：标准 loop，但”中场”特别多。</strong> OpenCode 的会话模型支持中途插入权限确认、在 TUI 里切换 agent（Tab 键在 build/plan 之间切换）、把 LSP（语言服务器）接进来给 agent 提供实时的类型信息——这是它区别于其他家的一个亮点：<strong>让 agent 的”眼睛”更专业</strong>，不止看文本，还看符号、类型、引用关系。</p><p><strong>模型：五件套里它把”模型层”做成了多面插座。</strong> 这是它安身立命的本事：</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></pre></td><td class="code"><pre><span class="line">┌───────────────────────────────────────────────────┐</span><br><span class="line">│ Agent Loop（一套循环）                            │</span><br><span class="line">│ tools / sessions / LSP / MCP                      │</span><br><span class="line">└───────────────────────────────────────────────────┘</span><br><span class="line">                          │ 同一套 tool-call 协议</span><br><span class="line">┌───────────────────────────────────────────────────┐</span><br><span class="line">│ provider 适配层（多面插座）                       │</span><br><span class="line">└───────────────────────────────────────────────────┘</span><br><span class="line"> │         │      │      │        │       │</span><br><span class="line"> Anthropic OpenAI Google DeepSeek MiniMax Ollama(本地)</span><br></pre></td></tr></table></figure><p>实测里我的 OpenCode 1.14.18 配的是 deepseek-v4-flash（同一个 <code>auth.json</code> 里同时存着 MiniMax 国际版、国内版和 DeepSeek 三把钥匙）。<strong>同一套代码，今天接云端、明天接本地 Ollama，不用改业务逻辑</strong>——这是”美食广场”的底气：你不站队，所以你永远有得选。</p><p><strong>记忆与配置：opencode.json 一站管理。</strong> 规则、agent、模型、权限、工具，全在一个配置文件里；还内置了”权限策略”（permissions/policies）体系，可以给不同工具、不同目录设不同规则。</p><p><strong>扩展：插件 + MCP + 桌面 App + 分享。</strong> <code>opencode</code> 还能 <code>--share</code> 把会话分享出来；桌面 App 和 IDE 扩展让它可以不只在终端里用。</p><p><strong>实测记录。</strong> 任务一（单文件三 bug）：11 秒完成，8 次工具调用，约 1.3 万 token——上来先用 bash 看了 <code>ls</code>/<code>git log</code>/<code>git status</code> 摸清仓库，再读文件、分三次 edit、跑测试，节奏稳当。任务二（多文件）：19 秒，13 次工具调用，约 1.5 万 token，这次它改动了<strong>全部三个文件</strong>（含把 <code>subtractStock()</code> 改成防御式），和 codex/pi 同属”下沉派”。值得注意的一个细节：它默认 agent 叫 <code>build</code>，另外内置了一个只读的 <code>plan</code> agent——<strong>把”先看后动”做成了产品级的两个模式</strong>，这在五家里是独一份的设计（其他家大多靠用户自己口头约束）。</p><h3 id="3-4-Pi：极客的手工改装车"><a href="#3-4-Pi：极客的手工改装车" class="headerlink" title="3.4 Pi：极客的手工改装车"></a>3.4 Pi：极客的手工改装车</h3><p>Pi 是 Earendil Works（作者是游戏圈出身的 badlogic，Mario Zechner）做的。TypeScript monorepo，MIT，96k star，用 Bun 打包成单文件可执行。它的五件套关键词：<strong>极简与信任</strong>。</p><p><strong>权限：五家里唯一”默认不设防”的。</strong> Pi 的 README 里写得很直白：它<strong>没有内置权限系统</strong>来限制文件系统、进程、网络或凭据访问，默认以启动它的用户的权限运行。这在五家里是独一份的激进选择——其他家至少有一层”要问人”的机制，Pi 直接说：<strong>我信任你，你自己负责自己的安全。</strong></p><p>但”不设防”不等于”没法防”，它给了三种容器化方案（<a href="https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/containerization.md">文档</a>）：</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><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">┌──────────────────────────────────────┐</span><br><span class="line">│ Pi 核心（Bun 单文件）                │</span><br><span class="line">│ pi-ai        统一多 provider API     │</span><br><span class="line">│ pi-agent-core  循环 + 状态管理       │</span><br><span class="line">│ pi-coding-agent  CLI                 │</span><br><span class="line">│ pi-tui       自研 TUI（差分渲染）    │</span><br><span class="line">└──────────────────────────────────────┘</span><br><span class="line">                   │ 默认：以你的用户权限&quot;裸奔&quot;</span><br><span class="line">                   ▼</span><br><span class="line">┌──────────────────────────────────────┐</span><br><span class="line">│ 可选安全壳（按需挑一个）             │</span><br><span class="line">│ 1. Gondolin：微 VM，宿主保留         │</span><br><span class="line">│    凭证，内置工具/命令路由进 VM      │</span><br><span class="line">│ 2. Docker：整进程装进容器            │</span><br><span class="line">│ 3. OpenShell：策略控制的沙箱         │</span><br><span class="line">└──────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p>这套哲学的价值在于：<strong>把”安全”从产品里拿出来，还给用户和环境。</strong> 你在自己的机器上跑，裸奔最顺手；你在 CI 里跑，套 Docker 最干净；你给别人的机器用，上 OpenShell。相比”产品替你决定安全边界”，Pi 选择”边界由你画”。</p><p><strong>记忆：会话即文件 + 主动分享。</strong> Pi 的会话是文件（可以 <code>--export</code> 成 HTML），还推了一个很有意思的机制：<strong>把开源工作里的真实 agent 会话发布到 Hugging Face</strong>（<a href="https://github.com/badlogic/pi-share-hf">pi-share-hf</a>），用真实世界的工具调用、失败、修复数据来改进 agent，而不是靠玩具 benchmark。作者本人就定期公开自己的会话——这大概是五家里对”真实数据”最激进也最坦诚的做法。</p><p><strong>扩展：可自我解释。</strong> Pi 的文档里有一句很酷的话：**”你甚至可以问 agent 它自己是怎么工作的”**——因为它是开源的，agent 可以直接读自己的源码来回答。它还支持订阅制 OAuth（ChatGPT、Claude Pro、GitHub Copilot、xAI、OpenRouter 都能登录），把”用别人的订阅跑自己的 harness”做成了特性。</p><p><strong>实测记录。</strong> Pi 0.84.2 + deepseek/deepseek-v4-flash 跑同一个修复任务：<strong>9 秒完成，6 次工具调用（3 次 bash、2 次 read、1 次 edit），3375 token，成本 0.00007 美元</strong>——五家里最快、最便宜，便宜到可以忽略不计。它的风格非常”动手派”：上来就是 bash 一通 ls/find，然后 read、edit、npm test，行云流水。没有确认、没有多余的礼貌，就像改装车手直接掀开发动机盖。当然，这趟之所以这么顺，也是因为任务本身简单、不需要任何权限判断——如果任务需要联网或动敏感目录，裸奔的代价就要另算了。</p><h3 id="3-5-DeepSeek-Harness：插件流水线"><a href="#3-5-DeepSeek-Harness：插件流水线" class="headerlink" title="3.5 DeepSeek Harness：插件流水线"></a>3.5 DeepSeek Harness：插件流水线</h3><p>最后拆的是 DeepSeek Harness（<code>dsh</code>）——需要声明一下，这篇文章就运行在它上面，我会尽量用解剖刀而不是滤镜说话。它的五件套关键词：<strong>一切皆插件</strong>（”Everything is a Plugin”）。</p><p><strong>架构：没有”特权核心”。</strong> dsh 建立在 Cordis 这个插件框架之上（<a href="https://github.com/cordiverse/paper">设计论文</a>）：模型适配器、工具注册表、会话日志、甚至 agent 循环本身，<strong>全都是插件</strong>。官方架构文档里有一句很硬的话：”没有需要 patch 的特权核心——你扩展 dsh 的方式，就是把它旁边的插件挂上去。”（<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md">架构文档</a>）这意味着它的五件套，<strong>每一件理论上都能换</strong>。</p><p><strong>装配方式：配置层叠（叠汉堡）。</strong> dsh 的”profile”（配置档案）是核心概念。一个 profile = 一组”组合包（bundle）” + 你自己的覆盖配置，按顺序叠起来：</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></pre></td><td class="code"><pre><span class="line">┌────────────────────────────────────────────────────┐</span><br><span class="line">│ 1. 空配置根                                        │</span><br><span class="line">├────────────────────────────────────────────────────┤</span><br><span class="line">│ 2. dsh.profile.bundles 里各组合包的 patch（按序）  │</span><br><span class="line">│    dsh-base（模型/工具/沙箱/权限/持久化/遥测）     │</span><br><span class="line">│    dsh-web-app（浏览器 UI）                        │</span><br><span class="line">├────────────────────────────────────────────────────┤</span><br><span class="line">│ 3. profile 自己的 cordis.patch.yml（你的覆盖）     │</span><br><span class="line">├────────────────────────────────────────────────────┤</span><br><span class="line">│ 4. $DSH_HOME/cordis.patch.yml（全局覆盖）          │</span><br><span class="line">├────────────────────────────────────────────────────┤</span><br><span class="line">│ 5. --patch 参数（一次性覆盖）                      │</span><br><span class="line">└────────────────────────────────────────────────────┘</span><br><span class="line">每一层按 id 替换/插入&quot;插件行&quot;，后层可覆盖前层任意一行</span><br></pre></td></tr></table></figure><p>这套”叠汉堡”机制解决的是 agent 领域的一个真问题：<strong>agent 系统太容易变成”配置地狱”<strong>——几十个开关互相打架。dsh 用”层”来建立秩序：基础能力（dsh-base）是一层，模式（web/headless）是一层，你的个性化是一层，谁在上谁说了算，清清楚楚。<code>dsh web</code>（浏览器 UI）和 <code>dsh --profile headless &quot;任务&quot;</code>（一次性无头执行）就是</strong>同一个引擎的两个 profile</strong>——换驾驶舱不用换引擎，这正是五件套里”界面”这一件被做成可插拔的样子。</p><p><strong>循环：官方文档给了完整的 turn flow。</strong> 它的循环设计里我最想提的一点是”<strong>模型可见即记录</strong>“（model-visible means logged）这个不变量：任何到达模型请求的内容，都必须能从会话日志里重建出来。这保证了 replay、fork、持久化、遥测全都从一条真实的事件流派生，而不是各存各的副本。</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></pre></td><td class="code"><pre><span class="line">turn/start</span><br><span class="line">  claim 下一条输入</span><br><span class="line">  组装 prompt 段 + 工具 schema</span><br><span class="line">  → agent/pre-step                    拒绝 | 进入</span><br><span class="line">     step/start</span><br><span class="line">     追加输入为 user/message</span><br><span class="line">     从日志推导模型历史</span><br><span class="line">     agent/request → llm/stream → assistant/chunk* → assistant/message</span><br><span class="line">     tool/call* → tools/pre-execute → tools/execute → tools/post-execute → tool/result*</span><br><span class="line">     step/end</span><br><span class="line">     还有未清账的工具 → 下一个 step</span><br><span class="line">  → agent/turn-stopping</span><br><span class="line">turn/end</span><br></pre></td></tr></table></figure><p>（此图摘自<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md">官方架构文档</a>，<code>turn</code> = 一次”欠债还清”的完整回合，<code>step</code> = 一次模型请求 + 它调用的工具。）</p><p><strong>权限：沙箱与 fs 分离。</strong> dsh-base 里同时挂了 bash 沙箱（macOS/Linux 用 sandbox，Windows 用 ACL 受限令牌）和文件策略层，但两者是<strong>分开的</strong>——这带来一个实测里非常有意思的差异，见下面的记录。</p><p><strong>实测记录。</strong> DeepSeek Harness 0.1.1-rc.2（headless profile）跑同一个任务：26 秒完成，修复正确（事后验证 4/4 通过）。但过程很有戏剧性：<strong>它的 bash 沙箱在我这个”外层已有沙箱”的环境里同样启动失败</strong>（<code>sandbox-exec: sandbox_apply: Operation not permitted</code>），按理说它和 codex 一样寸步难行——但它没有停。因为它的 fs 工具（读、写、编辑）走的是独立的文件策略层，不受 bash 沙箱故障影响，于是 agent 照常读代码、照常编辑、修完全部 bug，然后发现 <code>npm test</code> 跑不了，就<strong>手动逐用例做了数学验证</strong>，并在汇报里诚实说明：”沙箱后端不可用，提权通道被拒，拒绝是最终的，所以我改为仔细检查验证。”（原话大意）</p><p>这组对照非常能说明问题：</p><ul><li><strong>Codex</strong>：所有操作都经过沙箱体系 → 沙箱挂了 → 整个 agent 停摆（安全优先，失败即停）。</li><li><strong>DSH</strong>：fs 和 shell 分属两套机制 → 一个挂了另一个照常 → 任务完成（可用性优先，降级继续）。</li></ul><p>没有谁对谁错，这是两种工程哲学：<strong>把安全做成一堵墙（墙倒了，什么都过不去）vs 把安全做成一道道闸（闸坏了，别的路还能走）。</strong> 对”只读分析”这类任务，DSH 的降级能力是优点；对”必须确保命令被隔离”的场景，Codex 的宁可停摆是优点。顺便说一句，dsh 现在还明确标注是 developer preview——接口随时可能破坏性变更，这一点文章里必须如实交代。</p><h2 id="四、横向对比：同一张表，五种配法"><a href="#四、横向对比：同一张表，五种配法" class="headerlink" title="四、横向对比：同一张表，五种配法"></a>四、横向对比：同一张表，五种配法</h2><p>先交代一下测试任务（完整方法见第五节，任务源码都在<a href="https://github.com/springuper/agent-harness-bench">复现仓库</a>的 <code>tasks/</code> 下）：<strong>任务一</strong>是一个 4 个测试的小仓库，故意注入 3 处 bug（税率重复计算、缺失四舍五入、硬编码税率），起步状态 1 过 3 挂；<strong>任务二</strong>升级成多文件订单服务（<code>inventory.js</code>/<code>pricing.js</code>/<code>checkout.js</code> 三个文件），同样注入 3 处 bug（折扣码大小写、缺库存检查、空订单不报错），其中”库存不足要报错”这条横跨两个文件，起步 3 过 3 挂。两个任务提示词完全一致，五家各自跑在独立副本上，不允许修改测试文件。</p><p>先回放任务一的”名场面”：五家各自的工具轨迹。下面这些不是示意图，是从各家会话日志里摘出来的<strong>真实记录</strong>（DSH 的轨迹来自它的 session 日志，Pi 和 OpenCode 来自各自的 JSON 输出，Claude Code 与 Codex 的无头模式不导出工具明细，所以用它们自述的流程）：</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><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">Claude Code（11 轮对话，37s）：</span><br><span class="line">  读全文件 → 列出 3 个 bug → 一次 edit 全部修复 → npm test</span><br><span class="line">  风格：先看全，再动手，多轮确认</span><br><span class="line"></span><br><span class="line">Codex（25s，仅 1762 tokens）：</span><br><span class="line">  读代码 → 定位 → 一次编辑 → 跑测试</span><br><span class="line">  风格：一把梭，不废话（这是它自己汇报的流程）</span><br><span class="line"></span><br><span class="line">OpenCode（8 次工具调用，11s）：</span><br><span class="line">  bash（ls + git log + git status）→ bash（cat + ls -R）→ read×2 → edit×3 → npm test</span><br><span class="line">  风格：先摸清仓库（连 git 历史都看了），再动手</span><br><span class="line"></span><br><span class="line">Pi（6 次工具调用，9s）：</span><br><span class="line">  bash ls → bash find → read×2 → edit → npm test</span><br><span class="line">  风格：bash 开路，跑得飞快</span><br><span class="line"></span><br><span class="line">DSH（12 次工具调用，26s）：</span><br><span class="line">  bash ls → glob → bash → read×3 → edit×3 → read → bash×2</span><br><span class="line">  风格：读得多、改得稳；bash 沙箱不可用就改用 fs 工具 + 数学验证</span><br><span class="line">  （其中 bash 调用都尝试过但被沙箱拦下——见 3.5 节的对照实验）</span><br></pre></td></tr></table></figure><p>同一个任务（任务一）、同样的三处 bug，五家的”解题姿势”各不相同：有先看后动的、有先测后改的、有一把梭的、有 bash 开路的。<strong>这就是五件套配法差异最直观的样子。</strong></p><h3 id="进阶任务（任务二）：任务变难后，修法开始分派"><a href="#进阶任务（任务二）：任务变难后，修法开始分派" class="headerlink" title="进阶任务（任务二）：任务变难后，修法开始分派"></a>进阶任务（任务二）：任务变难后，修法开始分派</h3><p>我又加跑了一档更复杂的任务，看看差距会不会拉开。结果很有意思：<strong>五家全部修复通过（6/6），”能不能修好”依然拉不开差距，”怎么修”却明显分成了两派。</strong></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></pre></td><td class="code"><pre><span class="line">claude（29s，11 轮，$0.197）   ：checkout 接上 hasStock() 前置检查 → 只改 2 个文件</span><br><span class="line">dsh（17s，13 次工具调用）      ：同上，checkout + pricing（这次 bash 环境正常，跑通了 npm test）</span><br><span class="line">codex（23s，6,030 tokens）     ：除 checkout 检查外，还把 subtractStock() 改成防御式 → 改全部 3 个文件</span><br><span class="line">opencode（19s，13 次工具调用，1.5 万 tokens）：同 codex，改全部 3 个文件</span><br><span class="line">pi（16s，12 次工具调用，5,394 tokens，$0.0001）：同 codex，改全部 3 个文件</span><br></pre></td></tr></table></figure><p><strong>一派只在”入口”设卡</strong>（claude、dsh）：在 <code>checkout</code> 里调用 <code>hasStock()</code> 前置检查，库存不足就抛错，<code>inventory</code> 原样不动；**另一派把防线下沉到”数据层”**（codex、opencode、pi）：让 <code>subtractStock()</code> 自己拒绝扣成负数，从根上保证库存不会变负。两种修法都让测试通过，但防御的边界画在了不同的位置——这跟它们在权限/沙箱上的哲学（Codex/Pi 更”底层兜底”）隐隐呼应。</p><p>这里还有个有趣的细节：<strong>修法不只看 harness，也看模型</strong>。OpenCode 第一轮（用 MiniMax-M2.7）跑任务二时只改了 checkout + pricing，属于”入口派”；换成 deepseek-v4-flash 重跑后，它改动了全部三个文件，变成了”下沉派”。同一个 harness、不同的模型，解题姿势都会变——这也是为什么我坚持把四家统一到同一个模型再对比。</p><p>顺带一提：任务一里 DSH 因为 bash 沙箱起不来只能”数学验证”，这一轮环境正常后它顺利跑通了 <code>npm test</code>——印证了 3.5 节的结论：那是环境问题，不是能力问题。</p><p>把五件套和五款工具放进一张表：</p><table><thead><tr><th>五件套</th><th>Claude Code</th><th>Codex</th><th>OpenCode</th><th>Pi</th><th>DSH</th></tr></thead><tbody><tr><td><strong>① 循环</strong></td><td>标准 loop + hooks 挂点</td><td>标准 loop，干净利落</td><td>标准 loop + LSP 增强</td><td>标准 loop，快</td><td>标准 loop，事件驱动（turn/step）</td></tr><tr><td><strong>② 工具</strong></td><td>bash/fs/edit/…（闭源内置）</td><td>bash/fs/edit/…（Rust 原生）</td><td>bash/fs/edit/… + LSP</td><td>bash/fs/edit/…（可扩展）</td><td>bash/fs/edit/…（全部可插拔）</td></tr><tr><td><strong>③ 记忆</strong></td><td>CLAUDE.md + subagents + 自动压缩</td><td>AGENTS.md 公约 + 项目记忆</td><td>opencode.json + 规则体系</td><td>会话即文件 + HF 公开分享</td><td>会话日志即真相 + 压缩/投影插件</td></tr><tr><td><strong>④ 权限</strong></td><td>默认询问，可授权</td><td><strong>审批分级 + 沙箱全家桶（最重）</strong></td><td>权限策略可配置</td><td><strong>默认无权限系统（最轻）</strong></td><td>沙箱与 fs 分层，可降级</td></tr><tr><td><strong>⑤ 界面</strong></td><td>TUI + headless -p</td><td>TUI + exec + IDE + App + Web</td><td>TUI + 桌面 + IDE + share</td><td>TUI（自研）</td><td><strong>Web / headless / TUI 多 profile</strong></td></tr><tr><td><strong>扩展</strong></td><td>hooks + MCP + 插件 + subagents</td><td>MCP + SDK + app-server</td><td>MCP + 插件 + LSP</td><td>扩展 + OAuth 订阅</td><td><strong>一切皆插件 + patch 层叠</strong></td></tr></tbody></table><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></pre></td><td class="code"><pre><span class="line">模型锁定</span><br><span class="line">  │</span><br><span class="line">Claude Code（Anthropic 兼容 API 可换）</span><br><span class="line">  │</span><br><span class="line">  │                     Codex（config 可换模型）</span><br><span class="line">  │</span><br><span class="line">  │                          DSH（默认 DeepSeek，provider 可插拔）</span><br><span class="line">  │</span><br><span class="line">  │                               OpenCode</span><br><span class="line">  │</span><br><span class="line">  │                                    Pi</span><br><span class="line">  └──────────────────────────────────────────────&gt; 模型中立</span><br><span class="line">  开箱即用                                         自己组装</span><br><span class="line">  （batteries included）                           （BYO）</span><br></pre></td></tr></table></figure><h3 id="资源占用：安装体积与内存"><a href="#资源占用：安装体积与内存" class="headerlink" title="资源占用：安装体积与内存"></a>资源占用：安装体积与内存</h3><p>选型时还有一个实在的维度：<strong>磁盘和内存</strong>。我把五款工具的安装体积和运行时峰值内存都量了一遍（同样跑任务一，macOS <code>/usr/bin/time -l</code> 抓主进程峰值 RSS）：</p><table><thead><tr><th></th><th>Claude Code</th><th>Codex</th><th>OpenCode</th><th>Pi</th><th>DSH</th></tr></thead><tbody><tr><td><strong>安装形态</strong></td><td>Node 应用</td><td>Rust 单二进制</td><td>Bun 单二进制</td><td>Bun 打包 + npm 依赖</td><td>Node 应用（多包）</td></tr><tr><td><strong>安装体积</strong>（实测）</td><td>170 MB</td><td><strong>258.5 MB</strong></td><td>96.4 MB（目录 153 MB）</td><td>143 MB</td><td>283 MB</td></tr><tr><td><strong>峰值内存</strong>（任务一，主进程 RSS）</td><td>385 MB</td><td><strong>85 MB</strong></td><td>374 MB</td><td>194 MB</td><td>221 MB</td></tr></tbody></table><p>这里有一个很反直觉的结果：<strong>Codex（Rust）的安装体积其实是五家里第二大的（258.5 MB），但运行时内存却是五家里最低的（85 MB）</strong>——“Rust 省内存”的印象成立，”Rust 体积小”的印象在它身上不成立。拆开看原因：258.5 MB 基本全是二进制本体（安装目录里其余资源加起来不到 5 MB）。需要澄清的是，<strong>桌面 App 本身（.dmg）和 app-server 是独立的 release 资产，装 CLI 不会带上它们</strong>——这个二进制就是 CLI 自己，只是大得惊人：<code>__TEXT</code> 段 252 MB，其中机器码 <code>__text</code> 约 185 MB（整个 agent 运行时——沙箱实现、协议客户端、MCP、TUI、内嵌 webview/字体等 UI 资源——被静态链接进了一个文件），且没有 debug 符号可剥。所有安装渠道（brew cask、npm、官方脚本、GitHub release）装的都是这同一个二进制（官方 gzip 压缩包 84 MB，解压即此），<strong>不存在更小的”纯 CLI”版本</strong>。反过来，两个”内存大户”是 Claude Code（385 MB）和 OpenCode（374 MB）——Node/Bun 运行时本身就有不小的底座，再加上推理时的上下文缓冲。</p><p>几点说明：内存测的是<strong>主进程</strong>的峰值 RSS，工具派生的 bash 子进程不计入；安装体积口径不同（单二进制 vs npm 全家桶），Pi 和 DSH 如果只算真正跑起来的文件会更小；这些数字随版本变化，仅供参考——但对”这台机器装得下吗、跑得起吗”这类问题，量级是有用的。</p><p>换个角度说结论：</p><ul><li><strong>想省心、要生态</strong>，选 Claude Code——服务最周全，代价是闭源；模型绑定是软的（Anthropic 兼容端点即可换 DeepSeek/MiniMax 等，见 3.1 实测）。</li><li><strong>重视安全和可审计性</strong>，选 Codex——沙箱最硬、最省 token，而且连 harness 本体都开源了。</li><li><strong>要模型自由、要社区</strong>，选 OpenCode——中立插座 + 20 万 star，怎么接都行。</li><li><strong>追求速度和极简，且自己掌控环境</strong>，选 Pi——快得离谱、便宜得离谱，安全自负。</li><li><strong>想深度定制 harness 本身</strong>，选 DSH——一切皆插件，五件套全都能换；但要接受 developer preview 的不稳定。</li></ul><h2 id="五、实测方法论与免责声明"><a href="#五、实测方法论与免责声明" class="headerlink" title="五、实测方法论与免责声明"></a>五、实测方法论与免责声明</h2><p>为了让上面这些结论站得住，我交代一下怎么测的。<strong>完整的复现材料——两档任务源码、测试环境说明、五款工具的运行脚本、全部原始会话记录（工具轨迹/token/成本）与结果汇总——已开源在 <a href="https://github.com/springuper/agent-harness-bench">github.com/springuper/agent-harness-bench</a>，任何人可以自行查证或重跑。</strong></p><ol><li><strong>同一任务，两档难度</strong>：<strong>任务一</strong>是一个 4 个测试的小仓库，故意注入 3 个 bug（税率重复计算、缺失四舍五入、硬编码税率），起步状态 1 过 3 挂；<strong>任务二</strong>是多文件订单服务（<code>inventory.js</code>/<code>pricing.js</code>/<code>checkout.js</code>），注入 3 个 bug（折扣码大小写、缺库存检查、空订单不报错），其中”库存不足要报错”横跨两个文件，起步 3 过 3 挂。两档提示词完全一致：”修复 bug，让 npm test 全过，不改测试文件，跑测试确认。”</li><li><strong>隔离环境</strong>：五款工具各自跑在<strong>独立的仓库副本</strong>里，避免”先跑完的改好了代码，后跑的捡现成”这种污染（第一轮我就踩了这个坑，五家共享一个目录，结果 Pi 8 秒修完后，其他家全在”验证已通过的代码”）。</li><li><strong>版本与模型</strong>：Claude Code 2.1.3、Codex CLI 0.146.0、OpenCode 1.14.18、Pi 0.84.2、DSH 0.1.1-rc.2，<strong>五款全部使用同一个模型 deepseek-v4-flash</strong>——对比完全落在 harness 本身的差异上。Claude Code 是通过 DeepSeek 的 Anthropic 兼容端点（<code>https://api.deepseek.com/anthropic</code>）接入的，这本身就是”模型与 harness 解耦”的一个例证（详见 3.1 节）。token 数和成本虽然同模型，但各家计费口径与缓存策略不同，仍只宜做量级参考。</li><li><strong>日期</strong>：所有 star 数、版本号、实测数据均为 2026-08-23 抓取，开源世界变化快，引用时请以当时为准。</li><li><strong>坦白</strong>：作者深度使用 DSH（这篇文章就是在 DSH 里写的），文章尽量用解剖刀而不是滤镜；但如果你发现哪里偏了，欢迎在评论区指出来。</li></ol><p>把两档任务的实测放到一起对照。先看任务一：</p><table><thead><tr><th></th><th>Claude Code</th><th>Codex</th><th>OpenCode</th><th>Pi</th><th>DSH</th></tr></thead><tbody><tr><td>版本</td><td>2.1.3</td><td>0.146.0</td><td>1.14.18</td><td>0.84.2</td><td>0.1.1-rc.2</td></tr><tr><td>实测模型</td><td>deepseek-v4-flash</td><td>deepseek-v4-flash</td><td>deepseek-v4-flash</td><td>deepseek-v4-flash</td><td>deepseek-v4-flash</td></tr><tr><td>结果</td><td>✅ 4/4</td><td>✅ 4/4</td><td>✅ 4/4</td><td>✅ 4/4</td><td>✅ 4/4</td></tr><tr><td>耗时</td><td>37s</td><td>25s</td><td>11s</td><td><strong>9s</strong></td><td>26s</td></tr><tr><td>工具调用/轮次</td><td>11 轮对话</td><td>自述：读→编辑→测试</td><td>8 次工具调用</td><td>6 次工具调用</td><td>12 次工具调用</td></tr><tr><td>token（约）</td><td>16.3 万（含缓存读）</td><td><strong>1,762</strong></td><td>1.3 万</td><td>3,375</td><td>6.8 万（含缓存读）</td></tr><tr><td>缓存命中率</td><td>86%</td><td>未上报</td><td><strong>98%</strong></td><td>91%</td><td>81%</td></tr><tr><td>成本（约）</td><td>$0.216</td><td>极低</td><td>低</td><td><strong>$0.00007</strong></td><td>极低</td></tr></tbody></table><p><em>（说明：五家同模型，但各家 token 计费口径与缓存策略不同，”成本”是各工具自己上报的数值，仅供参考。）</em></p><p>任务二同样全绿，关键数字：</p><table><thead><tr><th></th><th>Claude Code</th><th>Codex</th><th>OpenCode</th><th>Pi</th><th>DSH</th></tr></thead><tbody><tr><td>结果</td><td>✅ 6/6</td><td>✅ 6/6</td><td>✅ 6/6</td><td>✅ 6/6</td><td>✅ 6/6</td></tr><tr><td>耗时</td><td>29s</td><td>23s</td><td>19s</td><td><strong>16s</strong></td><td>17s</td></tr><tr><td>工具调用/轮次</td><td>11 轮对话</td><td>自述（6,030 tokens）</td><td>13 次工具调用</td><td>12 次工具调用</td><td>13 次工具调用</td></tr><tr><td>token（约）</td><td>11.7 万（含缓存读）</td><td>6,030</td><td>1.5 万</td><td><strong>5,394</strong></td><td>7.1 万（含缓存读）</td></tr><tr><td>缓存命中率</td><td>78%</td><td>未上报</td><td><strong>98%</strong></td><td>93%</td><td>82%</td></tr><tr><td>成本（约）</td><td>$0.197</td><td>极低</td><td>低</td><td><strong>~$0.0001</strong></td><td>极低</td></tr><tr><td>改动文件</td><td>checkout+pricing</td><td>全部 3 个</td><td>全部 3 个</td><td>全部 3 个</td><td>checkout+pricing</td></tr></tbody></table><p><em>（任务二的 codex 与 dsh 是在无外层沙箱的正常环境下跑的——任务一里它们的 bash 沙箱启动失败是环境限制，不是能力问题，见 3.5 节。缓存命中率 = 缓存读取 token / 总 token，反映 harness 复用上下文的程度。它和任务形态强相关：长会话里系统提示词与历史前缀稳定，命中率可以到 99% 以上（本文写作时的长会话实测 99.8%）；基准里 7 步的短任务上下文变化大、冷启动占比高，命中率低属正常，横向对比看相对差异即可。Codex 无头输出不提供缓存明细。）</em></p><h2 id="六、结尾：鞍具的时代"><a href="#六、结尾：鞍具的时代" class="headerlink" title="六、结尾：鞍具的时代"></a>六、结尾：鞍具的时代</h2><p>写这篇文章的过程中，最触动我的数据是 Codex Harness 开源时的那个结果：<strong>两个 harness 设计改动，让同一个模型的成绩从 13.3% 涨到 38.3%，token 还省了 6 倍。</strong> 这意味着什么？</p><p>意味着<strong>模型正在变成商品，鞍具正在变成差异。</strong> 当大模型的能力越来越同质化、价格越来越便宜，”选哪个模型”的答案会越来越不重要；而”怎么把模型的力气变成实际产出”——循环怎么设计、上下文怎么管理、安全边界怎么划、怎么扩展——会越来越值钱。2026 年回头看，这五款工具几乎在同一件事上押注，只是姿势不同：Anthropic 押服务与生态，OpenAI 押纪律与开源，OpenCode 押中立与社区，Pi 押极简与信任，DeepSeek 押插件化与可组合性。</p><p>还有一件小事我想留作彩蛋：<strong>我这台机器上，五款工具现在跑着同一个引擎——deepseek-v4-flash。</strong> Claude Code 是通过 DeepSeek 的 Anthropic 兼容端点接入的，另外四家各有各的连法，但没有一行业务代码是为”某个模型”写的，全是 harness 在替我干活。这大概就是鞍具时代最好的注脚——<strong>马是谁不重要了，重要的是鞍具合不合手。</strong></p><p>最后，如果你也想亲自感受”同一副大脑、五副鞍具”的差别，可以这样复现这轮实测：在 <a href="https://github.com/springuper/agent-harness-bench">agent-harness-bench</a> 的 <code>tasks/</code> 下任选一个任务，复制到独立目录（比如 <code>cp -R tasks/task1 /tmp/my-run &amp;&amp; cd /tmp/my-run</code>），再用附录里任何一款工具的无头命令跑下面这段提示词——提示词里的”这个仓库”指的就是你复制出来的那个目录：</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">修复这个仓库里的 bug，让 npm test 全部通过。不要修改 test/ 目录下的测试文件。完成后运行测试确认。</span><br></pre></td></tr></table></figure><p>（提醒：给每款工具用<strong>独立的副本</strong>，别让它们共享一个目录——否则先跑完的会”剧透”给后跑的，这正是我在方法论里踩过的坑。）</p><h2 id="附：五款工具的无头模式一行命令"><a href="#附：五款工具的无头模式一行命令" class="headerlink" title="附：五款工具的无头模式一行命令"></a>附：五款工具的无头模式一行命令</h2><p>本文的实测全部走的是各家的”无头（headless）”模式——不打开交互界面，把任务当作参数丢进去，跑完退出。这也是把 agent 接进 CI、批处理脚本的标准姿势。<strong>五款工具全都提供无头模式，所以”能不能接 CI”这个问题的答案是：都可以</strong>——差异只在输出格式（有的给 JSON 事件流，有的给纯文本）和权限/沙箱参数（见下方小提示）。以下命令都是我这次实测真实用过的（2026-08-23），复制即用——先到 <a href="https://github.com/springuper/agent-harness-bench">agent-harness-bench</a> 的 <code>tasks/</code> 复制一个任务到独立目录再运行，提示词里的”这个仓库”指的就是你运行命令所在的目录：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># Claude Code：-p 无头模式；通过 DeepSeek 的 Anthropic 兼容端点接引擎（CI 常用）</span></span><br><span class="line">ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic ANTHROPIC_AUTH_TOKEN=sk-你的key \</span><br><span class="line">  ANTHROPIC_MODEL=deepseek-v4-flash \</span><br><span class="line">  claude -p <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span> --dangerously-skip-permissions</span><br><span class="line"></span><br><span class="line"><span class="comment"># Codex CLI：exec 非交互执行，--sandbox workspace-write 用工作区沙箱</span></span><br><span class="line">codex <span class="built_in">exec</span> --sandbox workspace-write --skip-git-repo-check <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># OpenCode：run 非交互执行，-m 指定 provider/模型（这里用 DeepSeek）</span></span><br><span class="line">opencode run -m deepseek/deepseek-v4-flash --dangerously-skip-permissions <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Pi：-p 非交互模式，--provider/--model 指定引擎（这里用 DeepSeek）</span></span><br><span class="line">pi -p <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span> --provider deepseek --model deepseek/deepseek-v4-flash</span><br><span class="line"></span><br><span class="line"><span class="comment"># DeepSeek Harness：headless profile，一次性任务，打印最终答案后退出</span></span><br><span class="line">dsh --profile headless <span class="string">&quot;修复这个仓库里的 bug，让 npm test 全部通过&quot;</span></span><br></pre></td></tr></table></figure><p>几个小提示：</p><ul><li>换引擎（模型）只改对应参数：Claude Code 看 <code>ANTHROPIC_BASE_URL</code>/<code>ANTHROPIC_MODEL</code>，Codex 看 <code>config.toml</code> 的 <code>model_provider</code>，OpenCode 看 <code>-m provider/model</code>，Pi 看 <code>--provider/--model</code>，DSH 看 settings 里的模型配置——<strong>五款工具的”鞍具”和”马”都是可以分开换的</strong>，这正是本文想让你带走的核心印象。唯一的前提是<strong>协议兼容</strong>：Claude Code 需要对方提供 Anthropic Messages 兼容端点（DeepSeek 的 <code>https://api.deepseek.com/anthropic</code>、MiniMax 的 <code>/anthropic</code> 都行），其余四家走 OpenAI/Responses 兼容协议，选择面更大。</li><li>各家都支持把输出格式化成 JSON（<code>--output-format json</code> / <code>--format json</code> / <code>--mode json</code> 等），方便脚本解析。</li><li>沙箱和权限开关的名字各不相同：<code>--dangerously-skip-permissions</code>（Claude Code/OpenCode）、<code>--sandbox</code>（Codex）、无权限系统（Pi）、profile 内权限策略（DSH）。接 CI 前，请一定先搞清楚你在哪一档权限上跑。</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;如果你用过两个以上的 AI 编程工具，大概率会有这种困惑：&lt;strong&gt;同一个模型，装在 Claude Code 里和装在 OpenCode 里，干活的样子完全不一样。&lt;/strong&gt; 一个慢条斯理、先列计划再动手；一个风风火火、脚本一甩就开跑；一个问东问西，一个闷头就干。模型明明是同一个，差别从哪来的？&lt;/p&gt;
&lt;p&gt;答案藏在 harness 里。&lt;/p&gt;
&lt;p&gt;“Harness” 的英文原意是&lt;strong&gt;马具、鞍具&lt;/strong&gt;——套在牲口身上，让它的力气能变成拉车、耕地这些实际用途的那套东西。AI 语境下，&lt;a href=&quot;https://code.claude.com/docs/en/glossary&quot;&gt;Anthropic 官方文档&lt;/a&gt;给这类工具起过一个很准确的名字：&lt;strong&gt;agentic harness&lt;/strong&gt;（Agent 鞍具）。大模型本身只会”说话”——从概率里吐出下一段 token；是 harness 给它装上了”手脚”（读写文件、跑命令）、”眼睛”（观察执行结果）、”红绿灯”（权限审批），它才真正能干起活来。&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;你买的从来不是模型，而是”模型 + 一整套鞍具”。&lt;/strong&gt; 这篇文章想做的事很朴素：拿一把解剖刀，把五款有代表性的 agent harness——Claude Code、Codex、OpenCode、Pi、DeepSeek Harness——按同一套框架拆开，看看它们各自把”身体”装成了什么样；再把我亲手实测的记录摆上来，让结论都落在事实上。&lt;/p&gt;
&lt;p&gt;为了拆得干净，我给自己配了一套统一的解剖框架。先讲框架，再拆标本。&lt;/p&gt;</summary>
    
    
    
    
    <category term="AI" scheme="https://springuper.github.io/tags/AI/"/>
    
    <category term="Agent" scheme="https://springuper.github.io/tags/Agent/"/>
    
    <category term="Harness" scheme="https://springuper.github.io/tags/Harness/"/>
    
    <category term="Claude Code" scheme="https://springuper.github.io/tags/Claude-Code/"/>
    
    <category term="Codex" scheme="https://springuper.github.io/tags/Codex/"/>
    
    <category term="OpenCode" scheme="https://springuper.github.io/tags/OpenCode/"/>
    
    <category term="Pi" scheme="https://springuper.github.io/tags/Pi/"/>
    
    <category term="DeepSeek Harness" scheme="https://springuper.github.io/tags/DeepSeek-Harness/"/>
    
  </entry>
  
  <entry>
    <title>Terminal UI：用 React 的方式重做命令行</title>
    <link href="https://springuper.github.io/terminal-ui-react-ink/"/>
    <id>https://springuper.github.io/terminal-ui-react-ink/</id>
    <published>2026-02-27T11:00:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>如果你用过 Claude Code、Gemini CLI 或 GitHub Copilot CLI，会立刻注意到一件事：这些工具在终端里的表现，和过去那种黑底白字、一行行 <code>console.log</code> 的 CLI 完全不同。它们有清晰的布局——左侧是对话区，右侧是状态或工具面板，底部是输入框；有焦点切换、有动效、有层次感。这不是偶然。这些 AI Coding 工具都选择了同一条技术路线：<strong>用 React 写终端界面</strong>。</p><p>本文想和你聊聊这个方向的代表方案 <a href="https://github.com/vadimdemedes/ink">Ink</a>。文章分两层：先是「怎么用」——组件、Flexbox、Hooks，几乎不用换脑子；再是「为什么」——我会把 Ink 拆开，读它的源码（reconciler、Yoga 布局、2D buffer 渲染、终端写入），再跑真实的例子把输出摆上来，让结论都落在事实上。当「前端之美」的心智迁移到终端这块看似受限的介质时，会发生什么？值得亲眼看一下。</p><span id="more"></span><h2 id="一、为什么-Terminal-UI-突然「变美」了？"><a href="#一、为什么-Terminal-UI-突然「变美」了？" class="headerlink" title="一、为什么 Terminal UI 突然「变美」了？"></a>一、为什么 Terminal UI 突然「变美」了？</h2><h3 id="从-print-到应用"><a href="#从-print-到应用" class="headerlink" title="从 print 到应用"></a>从 print 到应用</h3><p>很长一段时间里，命令行工具给人的印象是：黑窗口，白字或灰字，偶尔加点颜色，输出靠 <code>console.log</code> 或 <code>printf</code> 堆叠。交互靠一问一答的 prompt。复杂一点的用 ncurses 画表格，但开发和维护成本都很高，和现代前端的组件化、声明式思路相去甚远。</p><p>2025 年前后，情况变了。Claude Code、Gemini CLI、Codex CLI 等一系列 AI 编程助手把终端当成了主战场。它们需要展示：多轮对话、工具调用结果、文件变更、进度状态……信息量大、结构复杂，纯文本流完全不够用。于是，<strong>终端被当作一块真正的 UI 画布</strong>来对待。</p><h3 id="技术选型的共识：React-Ink"><a href="#技术选型的共识：React-Ink" class="headerlink" title="技术选型的共识：React + Ink"></a>技术选型的共识：React + Ink</h3><p>这些工具不约而同地选用了同一条技术栈：<strong>Ink</strong>——一个把 React 渲染到终端的 renderer。Ink 的 README 里有一份官方的「Who’s Using Ink?」名单：Claude Code、Gemini CLI、GitHub Copilot CLI、Canva CLI、Cloudflare Wrangler、Prisma、Terraform CDK、Gatsby、tap……几乎每一个主流 AI 编程助手的 TUI 层都长在 Ink 上。（顺带纠正一个流传很广的说法：Shopify CLI 并不在名单上，它新版本已经用 Rust 重写了。）</p><p>这背后的逻辑很直观：这些团队大多有丰富的 React 经验。与其用传统方式（ncurses、readline、手写 ANSI 转义）从零搭一套终端 UI，不如复用已有的前端心智——组件、状态、Hooks、声明式渲染。Ink 恰好提供了这一层抽象。</p><h2 id="二、Ink-的核心：React-Flexbox，心智零迁移"><a href="#二、Ink-的核心：React-Flexbox，心智零迁移" class="headerlink" title="二、Ink 的核心：React + Flexbox，心智零迁移"></a>二、Ink 的核心：React + Flexbox，心智零迁移</h2><p>Ink 的 slogan 很干脆：「React for CLIs」。你写的就是 React 组件，只是渲染目标从 DOM 变成了终端。</p><h3 id="Yoga-布局：终端里的-Flexbox"><a href="#Yoga-布局：终端里的-Flexbox" class="headerlink" title="Yoga 布局：终端里的 Flexbox"></a>Yoga 布局：终端里的 Flexbox</h3><p>Ink 使用 <a href="https://github.com/facebook/yoga">Yoga</a> 作为布局引擎——这也是 React Native 用的那套。所以：</p><ul><li>每一个 <code>Box</code> 默认就是 <code>display: flex</code> 的容器</li><li><code>flexDirection</code>、<code>justifyContent</code>、<code>alignItems</code>、<code>padding</code>、<code>margin</code>、<code>flexGrow</code> 等属性，和你在 CSS 里写的几乎一一对应</li><li>百分比宽高（如 <code>width=&quot;70%&quot;</code>）也支持，可以做出响应式布局</li></ul><p>如果你已经会用 Flexbox 做 Web 布局，那在 Ink 里做终端布局，几乎没有额外学习成本。</p><h3 id="基础组件：Box-与-Text"><a href="#基础组件：Box-与-Text" class="headerlink" title="基础组件：Box 与 Text"></a>基础组件：Box 与 Text</h3><p>Ink 提供两个最核心的组件：</p><ul><li>**<code>Text</code>**：承载所有文本，支持 <code>chalk</code> 风格的颜色、粗体、下划线等样式</li><li>**<code>Box</code>**：布局容器，相当于浏览器里的 <code>div</code>，支持上述 Flexbox 属性</li></ul><p>所有可见文本必须包在 <code>Text</code> 里，所有布局结构用 <code>Box</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; render, Box, Text &#125; <span class="keyword">from</span> <span class="string">&#x27;ink&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> App = <span class="function">() =&gt;</span> (</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">flexDirection</span>=<span class="string">&quot;column&quot;</span> <span class="attr">padding</span>=<span class="string">&#123;1&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;green&quot;</span> <span class="attr">bold</span>&gt;</span>✓ 任务完成<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;gray&quot;</span>&gt;</span>已处理 42 个文件<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span></span>);</span><br></pre></td></tr></table></figure><p>实际输出：</p><p><img src="../images/terminal-ui-basic.png" alt="基础 Box/Text 例子"></p><h3 id="一个「迷你-Claude-Code」布局示例"><a href="#一个「迷你-Claude-Code」布局示例" class="headerlink" title="一个「迷你 Claude Code」布局示例"></a>一个「迷你 Claude Code」布局示例</h3><p>我们可以用 <code>Box</code> 和 <code>flexDirection</code> 快速搭出一个类似 Claude 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><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> React, &#123; useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; render, Box, Text, useInput &#125; <span class="keyword">from</span> <span class="string">&#x27;ink&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> App = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [input, setInput] = useState(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  useInput(<span class="function">(<span class="params">char, key</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (key.return) &#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 class="keyword">if</span> (key.backspace) &#123;</span><br><span class="line">      setInput(<span class="function">(<span class="params">prev</span>) =&gt;</span> prev.slice(<span class="number">0</span>, -<span class="number">1</span>));</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (char) setInput(<span class="function">(<span class="params">prev</span>) =&gt;</span> prev + char);</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="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">flexDirection</span>=<span class="string">&quot;column&quot;</span> <span class="attr">width</span>=<span class="string">&quot;100%&quot;</span> <span class="attr">height</span>=<span class="string">&quot;100%&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      &#123;/* 主内容区：左 70% + 右 30% */&#125;</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Box</span> <span class="attr">flexDirection</span>=<span class="string">&quot;row&quot;</span> <span class="attr">flexGrow</span>=<span class="string">&#123;1&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Box</span> <span class="attr">flexDirection</span>=<span class="string">&quot;column&quot;</span> <span class="attr">width</span>=<span class="string">&quot;70%&quot;</span> <span class="attr">padding</span>=<span class="string">&#123;1&#125;</span> <span class="attr">borderStyle</span>=<span class="string">&quot;single&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">Text</span> <span class="attr">bold</span>&gt;</span>对话区<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;gray&quot;</span>&gt;</span>Assistant: 你好，有什么我可以帮忙的？<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Box</span> <span class="attr">flexDirection</span>=<span class="string">&quot;column&quot;</span> <span class="attr">width</span>=<span class="string">&quot;30%&quot;</span> <span class="attr">padding</span>=<span class="string">&#123;1&#125;</span> <span class="attr">borderStyle</span>=<span class="string">&quot;single&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">Text</span> <span class="attr">bold</span>&gt;</span>工具面板<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;cyan&quot;</span>&gt;</span>Read, Edit, Bash...<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line"><span class="xml">      &#123;/* 底部输入区 */&#125;</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Box</span> <span class="attr">padding</span>=<span class="string">&#123;1&#125;</span> <span class="attr">borderStyle</span>=<span class="string">&quot;single&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;yellow&quot;</span>&gt;</span><span class="symbol">&amp;gt;</span> <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Text</span>&gt;</span>&#123;input || &#x27; &#x27;&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">Box</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">render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span></span>);</span><br></pre></td></tr></table></figure><p>这里的结构完全是「前端式」的：外层 <code>flexDirection=&quot;column&quot;</code> 分上下两块，上面再用 <code>flexDirection=&quot;row&quot;</code> 分左右。<code>borderStyle=&quot;single&quot;</code> 画出边框，<code>padding</code>、<code>width</code> 控制间距和占比。熟悉 React 和 Flexbox 的人一眼能看懂。</p><p>真实运行效果（渲染帧序列合成的 GIF，固定 60 列宽便于展示；输入是脚本模拟的——真实的 <code>useInput</code> 在终端里就长这样）：</p><p><img src="../images/terminal-ui-layout.gif" alt="迷你 Claude Code 布局：输入与运行"></p><p>注意两个细节：中文按双倍宽度排版，70%/30% 在 60 列下被 Yoga 精确换算成 42/18 列；<code>gap</code>、<code>marginTop</code>、<code>borderStyle</code> 这些「前端属性」在终端里原样生效。这段动图是真实运行结果，不是示意图。</p><h3 id="Hooks：状态与输入"><a href="#Hooks：状态与输入" class="headerlink" title="Hooks：状态与输入"></a>Hooks：状态与输入</h3><p>Ink 支持完整的 React Hooks。除了 <code>useState</code>、<code>useEffect</code>，还提供了针对终端的专用 Hooks：</p><ul><li>**<code>useInput</code>**：监听键盘输入，处理方向键、回车、Ctrl+C 等</li><li><strong><code>useFocus</code></strong> / **<code>useFocusManager</code>**：管理焦点，实现 Tab 切换、不同区域的键盘导航</li><li>**<code>useWindowSize</code>**：获取终端宽高，做响应式布局</li></ul><p>这些 API 让「交互式」TUI 的开发体验和写 Web 应用非常接近：状态驱动视图，事件驱动状态更新。</p><h2 id="三、Ink-的架构：React-的另一种-renderer"><a href="#三、Ink-的架构：React-的另一种-renderer" class="headerlink" title="三、Ink 的架构：React 的另一种 renderer"></a>三、Ink 的架构：React 的另一种 renderer</h2><p>很多人好奇：Ink 到底是怎么把 React 画到终端里的？答案其实很直接：<strong>Ink 是 React 的一个自定义 renderer</strong>，和 <code>react-dom</code> 平级。React 负责调度和协调，renderer 负责把组件树「画」到宿主环境里。浏览器里是 DOM，终端里就是 stdout。</p><h3 id="终端渲染的两块基石：字符网格与-ANSI-转义"><a href="#终端渲染的两块基石：字符网格与-ANSI-转义" class="headerlink" title="终端渲染的两块基石：字符网格与 ANSI 转义"></a>终端渲染的两块基石：字符网格与 ANSI 转义</h3><p>在讲 Ink 怎么做之前，先花两分钟搞清楚「终端到底是怎么把字画出来的」。做 Web 的读者可能从来没想过这个问题——浏览器替你封装了一切，但终端里没有这样的封装。</p><p><strong>终端是一张字符网格。</strong> 终端模拟器（Terminal.app、iTerm2、VS Code 里的 xterm.js）维护一个「宽 × 高」的字符矩阵（比如 80 列 × 24 行），外加一个<strong>光标位置</strong>。程序往 stdout 输出的字节会被<strong>按顺序填进网格</strong>：字符写到光标处，光标右移一格；<code>\n</code> 让光标换行。没有「元素」、没有树、没有样式表——只有「当前位置 + 覆盖写」。所以终端程序想「更新界面」，手段只有三种：</p><ol><li><strong>覆盖写</strong>：把光标移回去，用新字符盖住旧字符。最简单的例子是进度条——<code>\r</code>（回车但不换行）回到行首，重写这一行；</li><li><strong>擦除重写</strong>：<code>\x1b[2K</code> 擦掉当前行、<code>\x1b[2J</code> 清屏，然后把内容重新写一遍；</li><li><strong>移动光标</strong>：<code>\x1b[nA</code> / <code>\x1b[nB</code> 上下移 n 行，<code>\x1b[nC</code> / <code>\x1b[nD</code> 左右移，<code>\x1b[H</code> 回到左上角。</li></ol><p>这些 <code>\x1b[</code> 开头的字节序列就是 <strong>ANSI 转义序列</strong>（escape sequence）。颜色也是它：<code>\x1b[31m</code> 之后输出的字符变红，<code>\x1b[0m</code> 恢复默认。<strong>这套字节协议就是终端的「API」</strong>——没有 <code>document.querySelector</code>，没有 <code>textContent</code>，只有「把光标挪到某处，写一些字符」。</p><p>对比一下你熟悉的世界：浏览器里改一个数字，<code>countEl.textContent = &#39;1&#39;</code>，浏览器只重绘那一处；终端里没有「那一处」的句柄，你只能<strong>把光标移过去覆盖</strong>，或者<strong>擦掉整块重新画</strong>。Ink 的核心工作，就是把你用 React 声明出来的画面，翻译成这一串 ANSI 指令。理解了这一点，后面看 Ink 为什么每次更新都「重新生成整帧」，就顺理成章了。</p><blockquote><p>提醒：这类带中文的 ASCII 线框图，在浏览器里是否对齐取决于代码块的字体是不是等宽 CJK 字体（本站主题已做处理）。如果在你那边看到线框错位，通常是字体回退导致的，不是图本身的问题——本文所有线框图都按「中文占 2 格、其他字符占 1 格」的规则逐行校验过。</p></blockquote><h3 id="react-reconciler：同一套调度，不同的宿主"><a href="#react-reconciler：同一套调度，不同的宿主" class="headerlink" title="react-reconciler：同一套调度，不同的宿主"></a>react-reconciler：同一套调度，不同的宿主</h3><p>React 在 16 之后把「协调逻辑」和「宿主渲染」解耦了。<code>react-dom</code> 用的是 <code>react-reconciler</code> 提供的 API，实现了一套「createInstance、appendChild、commitUpdate……」的 host config，把 React 元素变成 DOM 节点。Ink 做的是一样的事，只不过宿主不是 DOM，而是<strong>终端</strong>。</p><p>Ink 和 Yoga 的关系可以这么理解：<strong>Ink 是 React 的 Terminal renderer，Yoga 是 Ink 内部使用的布局引擎</strong>。Ink 在创建 <code>Box</code> 等节点时，会同步创建对应的 Yoga 节点；协调完成后，调用 Yoga 计算布局，再根据布局结果生成输出。Yoga 是 Ink 的依赖，不作为独立进程或服务存在。</p><p>Ink 的实现大致分几层：</p><ol><li><p><strong>Reconciler 层</strong>：用 <code>react-reconciler</code> 注册一套 host config，告诉 React「如何创建/更新/删除一个终端节点」。这里的「节点」不是 DOM 元素，而是 Ink 内部的节点结构，每个 <code>Box</code> 会绑定一个 Yoga 节点。</p></li><li><p><strong>布局层</strong>：commit 完成后，Ink 调用 Yoga 对整棵布局树做 Flexbox 计算。<code>flexDirection</code>、<code>padding</code> 等属性在创建时已映射到 Yoga 节点，Yoga 算出每个节点的 x、y、width、height。</p></li><li><p><strong>输出层</strong>：根据 Yoga 输出的布局结果，把每个 <code>Text</code> 节点的内容、样式转换成 ANSI 转义序列，按行列写入一个 2D 字符 buffer。最后通过 <code>process.stdout.write</code> 输出，必要时先清屏再重绘。</p></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><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></pre></td><td class="code"><pre><span class="line">┌──────────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                           React (调度 + 协调)                            │</span><br><span class="line">│                 你的 JSX: &lt;Box&gt;&lt;Text&gt;Hello&lt;/Text&gt;&lt;/Box&gt;                  │</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">│                             react-reconciler                             │</span><br><span class="line">│提供 createContainer、updateContainer、commitUpdate 等 API                │</span><br><span class="line">│不关心宿主是 DOM 还是 Terminal，只负责 diff 和调度                        │</span><br><span class="line">└──────────────────────────────────────┬───────────────────────────────────┘</span><br><span class="line">                                      │ host config 回调                  </span><br><span class="line">                                      ▼                                   </span><br><span class="line">┌──────────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                           Ink (Host Renderer)                            │</span><br><span class="line">│  │createInstance     │──▶│Ink 节点树         │───▶│Yoga 布局计算      │  │</span><br><span class="line">│  │appendChild        │──▶│Box↔YogaNode       │───▶│(C/WASM)           │  │</span><br><span class="line">│  │commitUpdate       │──▶│Text               │───▶│flexDirection,     │  │</span><br><span class="line">│  │                   │──▶│                   │───▶│padding, margin    │  │</span><br><span class="line">│  └───────────────────┘──▶└─────────┬─────────┘───▶└─────────┬─────────┘  │</span><br><span class="line">│                                    │                        │            │</span><br><span class="line">│                                    │   布局结果 (x,y,w,h)   │            │</span><br><span class="line">│                                    ▼                        │            │</span><br><span class="line">│                              ┌────────────────┐             │            │</span><br><span class="line">│                              │2D 字符 buffer  │◀────────────┘            │</span><br><span class="line">│                              │+ ANSI 样式     │                          │</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">                                      │ process.stdout.write              </span><br><span class="line">                                      ▼                                   </span><br><span class="line">┌──────────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                        Terminal (stdout / stdin)                         │</span><br><span class="line">└──────────────────────────────────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p>简要来说：<strong>React 负责「要渲染什么」，react-reconciler 负责「何时、如何调度更新」，Ink 负责「在终端里怎么画」——而「怎么画」又分两步：Yoga 算位置，Ink 把位置 + 文本转成 ANSI 输出</strong>。</p><h3 id="源码里的证据：host-config-长什么样"><a href="#源码里的证据：host-config-长什么样" class="headerlink" title="源码里的证据：host config 长什么样"></a>源码里的证据：host config 长什么样</h3><p>示意图毕竟只是示意，还是把 Ink 拆开看。以下均来自 Ink 7.1.1（npm 当前最新版，配 React 19）。<code>build/reconciler.js</code> 的开头就是答案——它用 <code>react-reconciler</code> 的 <code>createReconciler</code> 注册了一套 host config：</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="comment">// ink/build/reconciler.js（节选，Ink 7.1.1）</span></span><br><span class="line"><span class="keyword">import</span> createReconciler <span class="keyword">from</span> <span class="string">&#x27;react-reconciler&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> Yoga <span class="keyword">from</span> <span class="string">&#x27;yoga-layout&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; createNode, appendChildNode, ... &#125; <span class="keyword">from</span> <span class="string">&#x27;./dom.js&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> createReconciler(&#123;</span><br><span class="line">  <span class="attr">getRootHostContext</span>: <span class="function">() =&gt;</span> (&#123; <span class="attr">isInsideText</span>: <span class="literal">false</span> &#125;),</span><br><span class="line">  <span class="attr">shouldSetTextContent</span>: <span class="function">() =&gt;</span> <span class="literal">false</span>,          <span class="comment">// 文本不走 content 属性，用独立文本节点</span></span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="title">createInstance</span>(<span class="params">type, props, rootNode, hostContext</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 每个 &lt;Box&gt;/&lt;Text&gt; 创建一个 Ink 节点；dom.js 里同步创建 Yoga 节点</span></span><br><span class="line">    <span class="keyword">const</span> node = createNode(type);            <span class="comment">// dom.js: Yoga.Node.create()</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function"><span class="title">createTextInstance</span>(<span class="params">text, _root, hostContext</span>)</span> &#123; <span class="comment">/* ... */</span> &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="attr">appendChild</span>: appendChildNode,               <span class="comment">// 同时把 Yoga 子节点挂到父节点上</span></span><br><span class="line">  <span class="attr">insertBefore</span>: insertBeforeNode,</span><br><span class="line">  <span class="attr">removeChild</span>: removeChildNode,</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="title">commitUpdate</span>(<span class="params">node, _type, oldProps, newProps</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> props = diff(oldProps, newProps);</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    setStyle(node, value);                    <span class="comment">// 样式直接映射到 Yoga 节点</span></span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="title">resetAfterCommit</span>(<span class="params">rootNode</span>)</span> &#123;</span><br><span class="line">    rootNode.onComputeLayout();               <span class="comment">// → yogaNode.calculateLayout()</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    rootNode.onRender();                      <span class="comment">// → 整帧渲染</span></span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>逐条对应前面的分层：</p><ul><li><code>createInstance</code> / <code>appendChild</code> / <code>commitUpdate</code>……就是第 1 层说的 host config。注意 <code>dom.js</code> 里每个节点创建时都调 <code>Yoga.Node.create()</code>，<code>appendChildNode</code> 会同步 <code>insertChild</code> 到父节点的 Yoga 节点——<strong>Box ↔ YogaNode 一一对应</strong>，不是概念上的类比。</li><li><code>resetAfterCommit</code> 是 commit 阶段的收尾钩子：先 <code>onComputeLayout()</code>（<code>ink.js</code> 里就是一行 <code>this.rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR)</code>，对整棵布局树做 Flexbox 计算），再 <code>onRender()</code> 走渲染。</li><li>渲染在 <code>renderer.js</code>：<code>new Output(&#123; width, height &#125;)</code> 建一张 2D 字符画布，<code>renderNodeToOutput</code> 把每个节点的文本和 ANSI 样式画进去，最后产出一帧完整的字符串。</li></ul><p>这就是第 2、3 层说的「Yoga 算位置，Ink 把位置 + 文本转成 ANSI 输出」——不是比喻，是源码里真实存在的调用链。</p><h3 id="用-Counter-串起全过程：原地变化时会发生什么？"><a href="#用-Counter-串起全过程：原地变化时会发生什么？" class="headerlink" title="用 Counter 串起全过程：原地变化时会发生什么？"></a>用 Counter 串起全过程：原地变化时会发生什么？</h3><p>用一个最简单的 Counter 来串起整个流程，尤其看看「只有数字原地变化」时，背后发生了什么：</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="keyword">import</span> React, &#123; useState, useEffect &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; render, Text &#125; <span class="keyword">from</span> <span class="string">&#x27;ink&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> Counter = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> timer = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> setCount(<span class="function"><span class="params">c</span> =&gt;</span> c + <span class="number">1</span>), <span class="number">100</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> <span class="built_in">clearInterval</span>(timer);</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="xml"><span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;green&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      计数: &#123;count&#125;</span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">Text</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">render(<span class="xml"><span class="tag">&lt;<span class="name">Counter</span> /&gt;</span></span>);</span><br></pre></td></tr></table></figure><p>这段代码在真实终端里长这样（渲染帧序列合成的 GIF，每 100ms 数字 +1）：</p><p><img src="../images/terminal-ui-counter.gif" alt="Counter 原地更新"></p><p><strong>首次渲染</strong>（count = 0）：</p><ol><li>React 渲染 <code>&lt;Counter&gt;</code>，得到 <code>&#123;count: 0&#125;</code> 的虚拟树</li><li>react-reconciler 调用 Ink 的 host config：createInstance(Text), appendChild(text “计数: 0”)</li><li>Ink 构建节点树，为布局创建 Yoga 节点（这里只有一个 Text，布局很简单）</li><li>Yoga 计算：这一行占多少宽、多高</li><li>Ink 把 “计数: 0” + 绿色 ANSI 写入 buffer，<code>stdout.write</code> 输出</li><li>终端显示：<code>计数: 0</code></li></ol><p><strong>100ms 后</strong>，<code>setCount(1)</code> 触发更新：</p><ol><li>React 调度一次更新，reconciler 做 diff，发现只有 <strong>Text 的 children 变了</strong>（”0” → “1”）</li><li>reconciler 调用 <code>commitUpdate</code> / <code>commitTextUpdate</code>，告诉 Ink：这个 Text 的内容要改成 “计数: 1”</li><li><strong>关键点</strong>：Ink 不会只更新终端里「数字那一格」。React 的 diff 只标记了这一处，但 Ink 没有 DOM 那种「改一个 textNode」的能力——<code>resetAfterCommit</code> 会触发 Yoga 对整棵布局树再算一次布局（结果可能一样），然后 <code>renderer.js</code> 把整棵树重新渲染成一张完整的 2D buffer。<strong>全量重绘发生在「帧生成」这一层</strong>。</li><li>写入终端时又是另一回事：Ink 的 <code>log-update</code> 会把新帧和上一帧做比较。默认模式下，<strong>内容没变就一行都不写</strong>；变了就擦掉旧帧的行数、写入新帧。如果开启 <code>incrementalRendering: true</code>，它进一步<strong>按行比较、只重写变化的行</strong>——源码注释写得很直白：”We do not write lines if the contents are the same. This prevents flickering during renders.”</li><li>至于「清屏再重绘」，只发生在少数特殊场景：帧超出终端高度（overflow）、全屏/非全屏切换、Windows 控制台。<code>ink.js</code> 里有个 <code>shouldClearTerminalForFrame</code> 函数，专门判断「这一帧要不要清屏」。</li></ol><p>所以准确的图景是：<strong>组件树的 diff 是增量的</strong>（React 只标记了 Text 的 children 变了），<strong>帧的生成是全量的</strong>（整棵树重新渲染成一张新帧），<strong>终端的写入是 diff 的</strong>（新帧和旧帧逐行比较，没变就不写）。浏览器里 react-dom 可以只 <code>textNode.textContent = &#39;1&#39;</code> 改那一处；终端里 Ink 没有这个选项，但也不至于每次清屏——它用「全帧生成 + 行级擦写」在正确性和性能之间取平衡。</p><h3 id="与-react-dom-的对比"><a href="#与-react-dom-的对比" class="headerlink" title="与 react-dom 的对比"></a>与 react-dom 的对比</h3><table><thead><tr><th>维度</th><th>react-dom</th><th>Ink</th></tr></thead><tbody><tr><td>宿主环境</td><td>DOM</td><td>终端（stdout）</td></tr><tr><td>布局引擎</td><td>浏览器引擎</td><td>Yoga（Flexbox）</td></tr><tr><td>更新粒度</td><td>DOM 增量更新</td><td>整帧重生成 + 行级 diff 写入</td></tr><tr><td>输入方式</td><td>鼠标 + 键盘</td><td>主要键盘</td></tr></tbody></table><p>同一套 React 心智，换了一个宿主环境，就能从浏览器「平移」到终端。这种抽象能力，正是 Ink 的核心价值。</p><h2 id="四、Terminal-特有的问题，Ink-怎么应对？"><a href="#四、Terminal-特有的问题，Ink-怎么应对？" class="headerlink" title="四、Terminal 特有的问题，Ink 怎么应对？"></a>四、Terminal 特有的问题，Ink 怎么应对？</h2><p>终端和浏览器差异很大：没有鼠标、没有滚动条、内容可能无限增长、窗口可能被 resize。Ink 针对这些场景提供了一些机制。</p><h3 id="无鼠标：键盘优先的交互模型"><a href="#无鼠标：键盘优先的交互模型" class="headerlink" title="无鼠标：键盘优先的交互模型"></a>无鼠标：键盘优先的交互模型</h3><p>终端里没有可靠的「点击」语义，交互主要靠键盘。Ink 用 <code>useInput</code> 监听 stdin 的按键事件，把 <code>key.upArrow</code>、<code>key.downArrow</code>、<code>key.return</code>、<code>key.tab</code> 等暴露出来。焦点切换则依赖 <code>useFocus</code> 和 <code>useFocusManager</code>：你声明哪些区域是可聚焦的，用户用 Tab 在它们之间切换，当前焦点区域才能接收 <code>useInput</code> 的事件。</p><p>这种「键盘 + 焦点」的模型和 Web 的「鼠标 + 键盘」不同，需要你显式设计 Tab 顺序和快捷键。例如用 <code>useFocus</code> 声明可聚焦区域，Tab 在它们之间切换，当前焦点用边框高亮：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> Pane = <span class="function">(<span class="params">&#123; label &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; isFocused &#125; = useFocus();</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">borderStyle</span>=<span class="string">&quot;single&quot;</span> <span class="attr">padding</span>=<span class="string">&#123;1&#125;</span> <span class="attr">borderColor</span>=<span class="string">&#123;isFocused</span> ? &#x27;<span class="attr">green</span>&#x27; <span class="attr">:</span> &#x27;<span class="attr">gray</span>&#x27;&#125;&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Text</span>&gt;</span>&#123;isFocused ? &#x27;▶ &#x27; : &#x27;  &#x27;&#125;&#123;label&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// 多个 Pane 时，Tab 按渲染顺序切换焦点；聚焦中的 Pane 才能收到 useInput 事件</span></span><br></pre></td></tr></table></figure><p>实际效果（两个 Pane，左侧聚焦：边框高亮 + <code>▶</code> 标记；右侧未聚焦为灰色边框。按 Tab 焦点在两者间切换）：</p><p><img src="../images/terminal-ui-focus.png" alt="useFocus 焦点切换"></p><p>一些终端支持鼠标事件（如 xterm 的 SGR 协议），但跨终端兼容性差，Ink 的设计以键盘为中心，这也是大多数 CLI 工具的共识。</p><h3 id="不断增长的内容：Static-与-overflow"><a href="#不断增长的内容：Static-与-overflow" class="headerlink" title="不断增长的内容：Static 与 overflow"></a>不断增长的内容：Static 与 overflow</h3><p>AI 对话、日志输出这类场景的特点是：<strong>新内容不断追加</strong>，旧的不会消失。如果每次都全树重绘，历史消息会被重复渲染，而且可能超出终端高度。</p><p>Ink 提供了 <strong><code>&lt;Static&gt;</code></strong> 组件专门处理这种「只增不减」的内容。<code>Static</code> 接收一个 <code>items</code> 数组，用 render 函数渲染每一项。关键是：<strong>已经渲染过的 item 不会被重新渲染</strong>，只会把新 item 追加到输出里。这样历史消息「钉」在终端上方，新的往下长，底部的动态区（比如输入框、进度条）照常更新。Gatsby 的构建输出、Tap 的测试列表都是这么做的——这不是我编的，Ink 源码 <code>Static.js</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></pre></td><td class="code"><pre><span class="line">&lt;Static items=&#123;messages&#125;&gt;</span><br><span class="line">  &#123;<span class="function"><span class="params">msg</span> =&gt;</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">key</span>=<span class="string">&#123;msg.id&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;gray&quot;</span>&gt;</span>&#123;msg.role&#125;: <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Text</span>&gt;</span>&#123;msg.content&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line">  )&#125;</span><br><span class="line">&lt;/Static&gt;</span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">marginTop</span>=<span class="string">&#123;1&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;<span class="name">Text</span>&gt;</span>输入: &#123;input&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span>  &#123;/* 动态区，每次更新都重绘 */&#125;</span></span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>上面这段的渲染效果（<code>messages</code> 是模拟数据）：</p><p><img src="../images/terminal-ui-static.png" alt="Static 历史消息累积 + 动态输入区"></p><p>对于有固定高度的区域，<code>Box</code> 支持 <code>overflow=&quot;hidden&quot;</code> 或 <code>overflow=&quot;visible&quot;</code>，可以裁掉超出部分。但 Ink 目前**没有 <code>overflow=&quot;scroll&quot;</code>**，也就是没有内置的「滚动视窗」——你不能在一个固定高度的 Box 里用方向键滚动查看超长内容。社区有 <code>ink-scroll-view</code> 等库做类似事情，但需要自己实现 windowing 逻辑（只渲染可见区域 + 维护 scroll offset）。</p><h3 id="终端尺寸变化与光标"><a href="#终端尺寸变化与光标" class="headerlink" title="终端尺寸变化与光标"></a>终端尺寸变化与光标</h3><p><code>useWindowSize</code> 可以拿到当前终端的行数和列数，配合 <code>stdout.on(&#39;resize&#39;)</code> 能在用户拖拽窗口时重新布局。<code>useCursor</code> 则用于控制光标的显示和位置，比如在输入框里让光标出现在正确的位置。</p><p>另外，当输出行数超过终端高度时，Ink 会清屏再重绘（这正是前面说的 <code>shouldClearTerminalForFrame</code> 要处理的 overflow 场景），而不是像 <code>docker-compose logs -f</code> 那样让内容自然向上卷动。这是 Ink 的设计选择：它假设你是「应用式」的全屏 TUI，而不是流式日志查看器。</p><h2 id="五、为什么-React-心智适合-Terminal-UI？"><a href="#五、为什么-React-心智适合-Terminal-UI？" class="headerlink" title="五、为什么 React 心智适合 Terminal UI？"></a>五、为什么 React 心智适合 Terminal UI？</h2><h3 id="1-组件化天然契合面板式布局"><a href="#1-组件化天然契合面板式布局" class="headerlink" title="1. 组件化天然契合面板式布局"></a>1. 组件化天然契合面板式布局</h3><p>TUI 的典型结构就是多块「面板」：对话区、日志区、状态栏、输入框……每一块都可以抽成组件，用 props 控制展示内容，用 state 控制展开/折叠、选中项等。这和我们在 Web 里拆 <code>Sidebar</code>、<code>ChatArea</code>、<code>InputBar</code> 的思路完全一致。</p><h3 id="2-状态复杂时声明式更可控"><a href="#2-状态复杂时声明式更可控" class="headerlink" title="2. 状态复杂时声明式更可控"></a>2. 状态复杂时声明式更可控</h3><p>AI 工具要展示的内容非常动态：对话在增长、工具在运行、进度在更新。用命令式的 <code>process.stdout.write</code> + 光标控制，很容易写出难以维护的意大利面。而 React 的「state → view」单向数据流，让每次渲染都是当前状态的完整快照，逻辑清晰得多。</p><h3 id="3-生态与调试"><a href="#3-生态与调试" class="headerlink" title="3. 生态与调试"></a>3. 生态与调试</h3><p>Ink 是 React renderer，所以可以用 React DevTools 检查组件树——<code>devtools.js</code> 会探测 <code>ws://localhost:8097</code>，发现 DevTools 在跑就连上去。测试也可以用 React Testing Library 的思路，对组件做快照或行为测试。对已经深度使用 React 的团队来说，这是巨大的效率加成。</p><h2 id="六、在受限介质中坚持审美"><a href="#六、在受限介质中坚持审美" class="headerlink" title="六、在受限介质中坚持审美"></a>六、在受限介质中坚持审美</h2><p>终端没有像素级自由，没有阴影和圆角（除非用 Unicode 和块字符模拟），色彩也受限于 256 色或真彩。但「前端之美」的本质不是特效堆砌，而是<strong>信息层次、留白、对齐、一致性</strong>。这些在终端里同样可以做到。</p><h3 id="留白与间距"><a href="#留白与间距" class="headerlink" title="留白与间距"></a>留白与间距</h3><p>Ink 的 <code>padding</code>、<code>margin</code>、<code>gap</code> 和 Flexbox 的 <code>justifyContent</code>、<code>alignItems</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></pre></td><td class="code"><pre><span class="line">&lt;Box flexDirection=<span class="string">&quot;row&quot;</span> gap=&#123;<span class="number">2</span>&#125;&gt;</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">padding</span>=<span class="string">&#123;1&#125;</span>&gt;</span><span class="tag">&lt;<span class="name">Text</span>&gt;</span>左<span class="tag">&lt;/<span class="name">Text</span>&gt;</span><span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">padding</span>=<span class="string">&#123;1&#125;</span>&gt;</span><span class="tag">&lt;<span class="name">Text</span>&gt;</span>右<span class="tag">&lt;/<span class="name">Text</span>&gt;</span><span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line">&lt;/Box&gt;</span><br></pre></td></tr></table></figure><p>实际输出（<code>gap=&#123;2&#125;</code> 让两个内容块之间留出两格空白，配合 <code>padding</code> 让文字不贴边）：</p><p><img src="../images/terminal-ui-gap.png" alt="留白与间距"></p><h3 id="色彩与层次"><a href="#色彩与层次" class="headerlink" title="色彩与层次"></a>色彩与层次</h3><p><code>Text</code> 的 <code>color</code>、<code>backgroundColor</code>、<code>bold</code>、<code>dimColor</code> 可以建立清晰的视觉层次。一个简单约定：标题用亮色 + 粗体，次要信息用 <code>dimColor</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></pre></td><td class="code"><pre><span class="line">&lt;Box flexDirection=<span class="string">&quot;column&quot;</span> gap=&#123;<span class="number">1</span>&#125;&gt;</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Text</span> <span class="attr">bold</span> <span class="attr">color</span>=<span class="string">&quot;cyan&quot;</span>&gt;</span># 构建完成<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;green&quot;</span>&gt;</span>  ✓ 编译通过<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;red&quot;</span>&gt;</span>  ✗ 测试失败: spec/a.test.js<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Text</span> <span class="attr">dimColor</span>&gt;</span>  耗时 2.3s<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line">&lt;/Box&gt;</span><br></pre></td></tr></table></figure><p>实际输出：</p><p><img src="../images/terminal-ui-colors.png" alt="色彩与层次"></p><p>Ink 底层用 chalk，支持 <code>green</code>、<code>#005cc5</code>、<code>rgb(232, 131, 136)</code> 等写法。避免到处高亮，保持克制的配色，终端里的层次感会明显提升。</p><h3 id="动效与反馈"><a href="#动效与反馈" class="headerlink" title="动效与反馈"></a>动效与反馈</h3><p>终端里没有 CSS 动画，但可以借助状态驱动的重绘，做出「活着」的感觉。这里举几个具体例子。</p><p><strong>Spinner</strong>：用社区库 <code>ink-spinner</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; render, Text &#125; <span class="keyword">from</span> <span class="string">&#x27;ink&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> Spinner <span class="keyword">from</span> <span class="string">&#x27;ink-spinner&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> Loading = <span class="function">() =&gt;</span> (</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;green&quot;</span>&gt;</span><span class="tag">&lt;<span class="name">Spinner</span> <span class="attr">type</span>=<span class="string">&quot;dots&quot;</span> /&gt;</span><span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">    &#123;&#x27; 正在拉取...&#x27;&#125;</span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line">render(<span class="xml"><span class="tag">&lt;<span class="name">Loading</span> /&gt;</span></span>);</span><br></pre></td></tr></table></figure><p>实际效果：</p><p><img src="../images/terminal-ui-spinner.gif" alt="Spinner 加载动画"></p><p><code>Spinner</code> 内部用 <code>setInterval</code> 切换不同字符（如 <code>⠋</code> <code>⠙</code> <code>⠹</code> …），触发 React 重绘，视觉上就是旋转。<code>type</code> 可选 <code>dots</code>、<code>line</code>、<code>arrow</code> 等，来自 <a href="https://github.com/sindresorhus/cli-spinners">cli-spinners</a>。</p><p><strong>进度条</strong>：用 <code>Box</code> 的 <code>width</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> ProgressBar = <span class="function">(<span class="params">&#123; percent = <span class="number">50</span> &#125;</span>) =&gt;</span> (</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">Box</span> <span class="attr">width</span>=<span class="string">&#123;20&#125;</span> <span class="attr">flexDirection</span>=<span class="string">&quot;row&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">Box</span> <span class="attr">width</span>=<span class="string">&#123;</span>`$&#123;<span class="attr">percent</span>&#125;%`&#125; <span class="attr">backgroundColor</span>=<span class="string">&quot;green&quot;</span>&gt;</span><span class="tag">&lt;<span class="name">Text</span>&gt;</span> <span class="tag">&lt;/<span class="name">Text</span>&gt;</span><span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">Text</span> <span class="attr">color</span>=<span class="string">&quot;gray&quot;</span>&gt;</span> &#123;percent&#125;%<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">Box</span>&gt;</span></span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>效果：</p><p><img src="../images/terminal-ui-progress.gif" alt="进度条"></p><p>每次 <code>percent</code> 变化，Ink 会重绘，绿色区域「长」一截。这是很多 CLI（如 npm install）的常见模式。</p><p><strong>光标</strong>：<code>useCursor</code> 可以控制光标位置和显隐。输入框里让光标出现在正确位置、等待输入时让光标闪烁，都能增强交互反馈。Gatsby 构建时的进度条 + 已完成页列表，就是 Static + 动效组合的典型。</p><h2 id="七、动手试试：资源与示例"><a href="#七、动手试试：资源与示例" class="headerlink" title="七、动手试试：资源与示例"></a>七、动手试试：资源与示例</h2><p>如果你对 Ink 产生了兴趣，官方和社区有大量现成资源可以快速上手。</p><h3 id="Useful-Components"><a href="#Useful-Components" class="headerlink" title="Useful Components"></a>Useful Components</h3><p>Ink 官方文档整理了 <a href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#useful-components">Useful Components</a>，都是经过验证的社区库，可直接 <code>npm install</code> 使用：</p><table><thead><tr><th>组件</th><th>用途</th></tr></thead><tbody><tr><td><code>ink-text-input</code></td><td>文本输入框</td></tr><tr><td><code>ink-select-input</code></td><td>下拉选择</td></tr><tr><td><code>ink-spinner</code></td><td>加载动画</td></tr><tr><td><code>ink-progress-bar</code></td><td>进度条</td></tr><tr><td><code>ink-multi-select</code></td><td>多选列表</td></tr><tr><td><code>ink-table</code></td><td>表格</td></tr><tr><td><code>ink-confirm-input</code></td><td>确认（Yes/No）</td></tr><tr><td><code>ink-syntax-highlight</code></td><td>代码高亮</td></tr><tr><td><code>ink-markdown</code></td><td>渲染 Markdown</td></tr><tr><td><code>ink-scroll-view</code></td><td>滚动容器</td></tr></tbody></table><p>这些组件覆盖了 TUI 里最常见的交互模式。比如做一个「选择模板」的 CLI，用 <code>ink-select-input</code> 几分钟就能搭起来；展示代码片段用 <code>ink-syntax-highlight</code>，和 IDE 的终端预览体验类似。</p><h3 id="官方-Examples"><a href="#官方-Examples" class="headerlink" title="官方 Examples"></a>官方 Examples</h3><p>Ink 仓库的 <a href="https://github.com/vadimdemedes/ink/tree/master/examples">examples</a> 目录提供了可运行的示例，用 <code>npm run example examples/[name]</code> 即可体验：</p><ul><li><strong>Counter</strong>：每 100ms 自增的数字，就是前文用来解释渲染流程的那个</li><li><strong>Jest</strong>：模拟 Jest 的测试输出 UI，Static + 动态进度</li><li><strong>Table</strong>：多列表格，感受 Flexbox 在终端里的对齐</li><li><strong>Focus management</strong>：用 <code>useFocus</code> 做 Tab 切换，多个可聚焦区域</li><li><strong>User input</strong>：<code>useInput</code> 监听键盘，实现简单输入</li><li><strong>Static</strong>：Gatsby 风格的「已完成项在上方累积 + 底部动态更新」</li><li><strong>Suspense</strong>：异步数据加载 + Suspense 边界</li><li><strong>Router</strong>：用 React Router 的 <code>MemoryRouter</code> 做「页面」切换</li></ul><p>这些示例都是完整的、可运行的，建议克隆 <a href="https://github.com/vadimdemedes/ink">ink</a> 仓库后逐个跑一遍，比读文档直观得多。</p><h3 id="5-分钟上手路径"><a href="#5-分钟上手路径" class="headerlink" title="5 分钟上手路径"></a>5 分钟上手路径</h3><ol><li><code>npx create-ink-app my-cli</code> 或 <code>npx create-ink-app --typescript my-cli</code></li><li>在 <code>ui.tsx</code> 里把默认组件改成前文的 Counter 或迷你布局</li><li><code>npm run build &amp;&amp; npm start</code> 看效果</li><li>装上 <code>ink-spinner</code>，加一个 <code>Loading</code> 组件</li><li>打开 <a href="https://github.com/vadimdemedes/ink/tree/master/examples">examples</a> 挑一个感兴趣的，对照源码改一改</li></ol><h2 id="八、Ink-的局限（选型参考）"><a href="#八、Ink-的局限（选型参考）" class="headerlink" title="八、Ink 的局限（选型参考）"></a>八、Ink 的局限（选型参考）</h2><p>了解这些，有助于你在「是否用 Ink」和「如何设计」时做出更合适的决策。它们不影响 Ink 作为 TUI 方案的价值，但值得在选型时纳入考量。</p><h3 id="全量重绘与性能"><a href="#全量重绘与性能" class="headerlink" title="全量重绘与性能"></a>全量重绘与性能</h3><p>每次 state 变化，帧都要<strong>整帧重新生成</strong>——组件树越深、节点越多，这一趟越贵；好在写入终端是 diff 的，且 <code>maxFps</code>（默认 30）会节流渲染频率。组件树深、更新频繁时，仍可能出现卡顿。建议：用 <code>Static</code> 分离只增不减的内容、对昂贵计算用 <code>useMemo</code>、必要时调低渲染频率。Ink 提供 <code>incrementalRendering</code>（按行增量写入）和 <code>maxFps</code> 两个旋钮，可以按需开启。</p><h3 id="内存占用"><a href="#内存占用" class="headerlink" title="内存占用"></a>内存占用</h3><p>Ink 依赖 Node、React、Yoga（含 WASM），内存比纯原生 CLI 高一截。我在 Node 24 上实测：空跑 node 本身 RSS 约 40MB，加载 Ink 7.1.1 + React 19 跑一个最小 Counter 应用后，RSS 约 100MB。网上常说的「打包后 32MB」其实是指安装体积，不是内存。对比 Go 的 Bubble Tea：编译产物是一个几 MB 的静态二进制，跑起来内存通常也小一个数量级。对本地 CLI、AI 工具通常可接受；资源受限环境需权衡。</p><h3 id="功能边界"><a href="#功能边界" class="headerlink" title="功能边界"></a>功能边界</h3><ul><li><strong>无 overflow: scroll</strong>：长列表滚动需自己实现或借助 <code>ink-scroll-view</code></li><li><strong>CJK / IME</strong>：中文输入法下可能有延迟或光标错位</li><li><strong>Windows</strong>：部分终端 ANSI 支持不完整</li></ul><h2 id="九、结语：介质在变，美感永恒"><a href="#九、结语：介质在变，美感永恒" class="headerlink" title="九、结语：介质在变，美感永恒"></a>九、结语：介质在变，美感永恒</h2><p>前端之美的初心，是追求更好的人机交互——清晰的视觉层次、流畅的反馈、符合直觉的操作。这份追求并不局限于浏览器。当 AI 工具、DevTools、云开发环境越来越依赖终端时，Terminal UI 就成了新的战场。</p><p>Ink 的价值在于：<strong>它让 React 开发者无需切换心智，就能在终端里延续同样的设计语言和工程实践</strong>。Flexbox、组件、Hooks——这些你早已掌握的前端技艺，在终端里依然有效。</p><p>还有一件小事，我想留作彩蛋：<strong>这篇文章里每一处标注「实测」的地方——那张 60 列宽的真实渲染帧、reconciler.js 里的 host config、还有那串 102MB 的内存数字——都是我在终端里一条命令一条命令跑出来的。</strong> 而你现在读到的这篇文章，同样是在终端里写完、构建、发布的；连帮我写它的 AI 工具，界面本身也是 Ink 画的。一个关于终端的故事，正坐在终端里被讲述——这大概就是「介质在变，美感永恒」最好的注脚：<strong>终端从来不是限制，它只是另一块画布。</strong></p><p>如果你对 Terminal UI 感兴趣，不妨从 Ink 开始。用你熟悉的 React，在 80x24 的字符画布上，做一点既实用又好看的东西。这也是前端之美的一种延续。</p><hr><p><strong>参考文献与延伸阅读</strong></p><ul><li><a href="https://github.com/vadimdemedes/ink">Ink - React for CLIs</a></li><li><a href="https://github.com/vadimdemedes/ink#whos-using-ink">Ink - Who’s Using Ink?（README 官方用户名单）</a></li><li><a href="https://github.com/vadimdemedes/ink/tree/master/src">Ink 源码（src 目录：reconciler / dom / renderer / ink / log-update）</a></li><li><a href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#useful-components">Ink - Useful Components</a></li><li><a href="https://github.com/vadimdemedes/ink/tree/master/examples">Ink - Examples</a></li><li><a href="https://github.com/facebook/react/tree/main/packages/react-reconciler">react-reconciler - React 的协调器</a></li><li><a href="https://github.com/facebook/yoga">Yoga - Cross-platform layout engine</a></li><li><a href="https://github.com/anthropics/claude-code">Claude Code - Anthropic’s agentic coding tool</a></li><li><a href="https://vadimdemedes.com/posts/building-rich-command-line-interfaces-with-ink-and-react">Building rich command-line interfaces with Ink and React</a></li></ul><blockquote><p>本文源码引用与实测数据基于 Ink 7.1.1 + React 19.2.8（Node 24），版本不同时实现细节可能有出入。</p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;如果你用过 Claude Code、Gemini CLI 或 GitHub Copilot CLI，会立刻注意到一件事：这些工具在终端里的表现，和过去那种黑底白字、一行行 &lt;code&gt;console.log&lt;/code&gt; 的 CLI 完全不同。它们有清晰的布局——左侧是对话区，右侧是状态或工具面板，底部是输入框；有焦点切换、有动效、有层次感。这不是偶然。这些 AI Coding 工具都选择了同一条技术路线：&lt;strong&gt;用 React 写终端界面&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本文想和你聊聊这个方向的代表方案 &lt;a href=&quot;https://github.com/vadimdemedes/ink&quot;&gt;Ink&lt;/a&gt;。文章分两层：先是「怎么用」——组件、Flexbox、Hooks，几乎不用换脑子；再是「为什么」——我会把 Ink 拆开，读它的源码（reconciler、Yoga 布局、2D buffer 渲染、终端写入），再跑真实的例子把输出摆上来，让结论都落在事实上。当「前端之美」的心智迁移到终端这块看似受限的介质时，会发生什么？值得亲眼看一下。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Frontend" scheme="https://springuper.github.io/tags/Frontend/"/>
    
    <category term="React" scheme="https://springuper.github.io/tags/React/"/>
    
    <category term="Terminal" scheme="https://springuper.github.io/tags/Terminal/"/>
    
    <category term="TUI" scheme="https://springuper.github.io/tags/TUI/"/>
    
    <category term="Ink" scheme="https://springuper.github.io/tags/Ink/"/>
    
  </entry>
  
  <entry>
    <title>AI 时代会需要更多程序员</title>
    <link href="https://springuper.github.io/ai-needs-more-programmers/"/>
    <id>https://springuper.github.io/ai-needs-more-programmers/</id>
    <published>2026-01-18T11:00:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>“AI 会取代程序员吗？”</p><p>这个问题从 AI Coding 工具开始真正进入工程实践以来，就一直萦绕在很多程序员的心头。虽然 GitHub Copilot 早在 2021 年就已发布，Cursor 也在 2023 年面世，但 2024 年大家并没有很认真地对待 AI coding，主要还停留在代码补全和测试用例生成等简单场景。然而到了 2025 年，随着 Claude Code 等新工具的推出，AI 在编程辅助上的应用真的变得广泛起来，从最初只能做简单的代码补全，到现在可以连续数小时不间断地生成相对完善的实现，这种变化让程序员的焦虑也在不断加剧。</p><p>作为一个在技术行业摸爬滚打多年的程序员，我想分享一个可能和主流焦虑不太一样的观点：<strong>AI 时代不仅不会让程序员失业，反而会需要更多的程序员——但前提是，我们必须学会与新的生产力工具共舞，升级自己的技能和思维方式。</strong></p><p><img src="../images/ai-programmer.jpg" alt="AI 时代会需要更多程序员"></p><span id="more"></span><h2 id="背景：AI-Coding-的快速发展与程序员的焦虑"><a href="#背景：AI-Coding-的快速发展与程序员的焦虑" class="headerlink" title="背景：AI Coding 的快速发展与程序员的焦虑"></a>背景：AI Coding 的快速发展与程序员的焦虑</h2><p>正如前面提到的，AI Coding 工具在 2025 年迎来了快速发展，进步速度超出了很多人的预期。根据公开的数据，微软内部已经有约 20-30% 的代码由 AI 模型生成，而且这个比例还在快速上升。</p><p>与此同时，程序员的焦虑也在不断加剧。很多人在担心：</p><ul><li>初级程序员的工作会不会被完全替代？</li><li>只会写代码的程序员还有没有价值？</li><li>我应该学习什么技能才能不被淘汰？</li></ul><p>这种焦虑并非空穴来风。美国劳工统计局的数据显示，在过去两年里，”程序员”（programmers）这一被定义为专门执行编码任务的岗位数量下降了超过 25%。而那些更偏向”开发者”（developer）、”系统设计”、”架构”、”产品工程”的岗位减少得要少得多。</p><p>但我想说的是，这个数据背后隐藏的，可能不是”程序员被淘汰”的故事，而是”程序员角色正在重塑”的故事。</p><h2 id="我的观点：AI-会让程序员有更多问题可解决"><a href="#我的观点：AI-会让程序员有更多问题可解决" class="headerlink" title="我的观点：AI 会让程序员有更多问题可解决"></a>我的观点：AI 会让程序员有更多问题可解决</h2><p>一句话总结我的观点：<strong>AI 时代会让程序员有更多的问题可以去解决，技术普惠会激发出远比之前软件行业大得多的场景，不过编程技能需要匹配新的 AI 生产力。</strong></p><h3 id="马车到汽车时代的跃迁"><a href="#马车到汽车时代的跃迁" class="headerlink" title="马车到汽车时代的跃迁"></a>马车到汽车时代的跃迁</h3><p>让我用一个形象的比喻来说明这个观点。</p><p><img src="../images/driver-evolvement.jpg" alt="马车到汽车时代的跃迁"></p><p>在汽车出现之前，马车是主要的交通工具。马车车夫需要掌握驾驭马的技能：如何让马听话、如何喂马、如何护理马车等等。当汽车出现后，虽然马车并没有立刻消失，但整个交通行业发生了翻天覆地的变化：</p><ul><li><strong>出行需求指数级增长</strong>：因为汽车让出行变得便捷，越来越多的人有了出行的需求。旅游、物流、通勤、商业活动，这些在马车时代只有少数人能享受的服务，在汽车时代变成了大众需求。</li><li><strong>新职业和新机会涌现</strong>：汽车司机、汽车修理工、加油站员工、道路建设者、交通规划师……这些在马车时代不存在的职业，在汽车时代大量涌现。</li><li><strong>技能要求的转变</strong>：那些固守驾驭马技能、拒绝学习开车的人，最终被淘汰。而那些学会驾驶汽车、理解引擎原理、懂得交通规则的人，迎来了新的机会。</li></ul><p>AI Coding 工具的出现，就像汽车替代马车一样，正在重塑整个软件行业：</p><ul><li><strong>重复性、模板化的代码编写</strong>（就像驾驭马的技能）正在被 AI 工具大量替代</li><li><strong>但新的需求正在爆发</strong>：因为写代码的门槛降低，更多行业、更多场景、更多个人和组织开始需要软件解决方案</li><li><strong>程序员需要掌握的技能在转变</strong>：从”写代码”转向”定义问题、设计方案、确保质量、理解业务”</li></ul><h3 id="为什么会有更多问题需要解决？"><a href="#为什么会有更多问题需要解决？" class="headerlink" title="为什么会有更多问题需要解决？"></a>为什么会有更多问题需要解决？</h3><p>这个观点听起来可能有些反直觉，但让我用几个例子来说明：</p><p><strong>1. 技术普惠带来的需求爆发</strong></p><p>当写代码的门槛大幅降低后，许多之前因为成本太高而无法实现的需求，现在变得可行了。比如：</p><ul><li>企业内部的小工具、自动化脚本、数据报表，这些在以前需要专门立项、分配开发资源的工作，现在可能一个懂业务的员工用 AI 工具就能快速实现</li><li>传统行业的数字化转型：农业、制造业、服务业等传统行业，现在可以更容易地用软件来改造自己的业务流程</li><li>个人和小团队的创意项目：一个想法可以快速用 AI 工具做出原型，验证市场需求</li></ul><p>这些需求的爆发，意味着需要更多的程序员来：</p><ul><li>将这些原型工程化、产品化</li><li>设计系统架构，确保可扩展性和可维护性</li><li>处理性能、安全、合规等复杂问题</li><li>理解业务需求，将技术方案转化为商业价值</li></ul><p><strong>2. 质量、安全、可维护性的要求提升</strong></p><p>AI 工具可以快速生成代码，但这些代码往往存在一些问题：</p><ul><li>代码质量参差不齐，需要人工审查和重构</li><li>边界条件和异常处理可能不完善</li><li>安全漏洞和性能问题需要专业人员的识别和修复</li><li>代码的可维护性和可理解性需要架构师和资深工程师来保证</li></ul><p>实际的研究数据也印证了这一点：即使是当前最先进的 AI Coding 工具（如 Claude Code），虽然让开发者写代码的速度大幅提升，但经验丰富的开发者在代码审查、重构和维护上的负担也显著增加。这说明，AI 生成代码的”量”在增加，但”质”的把关仍然需要人类程序员（具体案例见后文的详细分析）。</p><p><strong>3. 新场景和新问题的涌现</strong></p><p>AI 工具本身也带来了新的问题和挑战：</p><ul><li>如何评估 AI 生成代码的正确性？</li><li>如何设计 prompt 让 AI 生成更好的代码？</li><li>如何将 AI 工具集成到现有的开发流程中？</li><li>如何处理 AI 工具带来的法律、伦理、安全等问题？</li></ul><p>这些问题都需要程序员来解决，而且这些问题的复杂度和重要性，可能比传统的”写代码”更高。</p><h2 id="深刻例子：知名程序员的思考"><a href="#深刻例子：知名程序员的思考" class="headerlink" title="深刻例子：知名程序员的思考"></a>深刻例子：知名程序员的思考</h2><p>让我分享一些知名程序员和技术领袖的观点，这些观点帮助我们更好地理解这个变化。</p><h3 id="Redis-之父-Antirez-的观察"><a href="#Redis-之父-Antirez-的观察" class="headerlink" title="Redis 之父 Antirez 的观察"></a>Redis 之父 Antirez 的观察</h3><p>Antirez 在最近的文章中分享了他的观察。他承认自己广泛使用 LLM 来提升编程速度、减少重复性劳动，但他也强调：</p><blockquote><p>“AI 在创造性、打破常规和设想非常规解法方面，还远远落后于人类程序员。”</p></blockquote><p>Antirez 提到，在面对一些需要跳出框架思考的问题时，AI 往往无法像人类程序员那样设想出那些”奇特但高效”的解决方案。这说明，虽然 AI 可以处理很多常规的编程任务，但在创新和解决复杂问题方面，人类程序员仍然有不可替代的价值。</p><p>同时，Antirez 也指出：”在绝大多数情况下，写代码本身已经不再是必要的工作了。”这个判断指向了一个重要的转变：程序员的工作重点正在从”写代码”转向”定义问题、设计方案、确保质量”。</p><h3 id="微软-CTO-的预测"><a href="#微软-CTO-的预测" class="headerlink" title="微软 CTO 的预测"></a>微软 CTO 的预测</h3><p>微软 CTO Kevin Scott 预测，在未来五年内，大约 95% 的代码将由 AI 来生成。但他强调，这并不意味着”程序员将被淘汰”。相反，程序员的任务将从”写每一行代码”转向：</p><ul><li>设定目标和需求</li><li>设计方案和架构</li><li>监督代码质量</li><li>负责系统整体和边界把控</li></ul><p>这个预测和 Antirez 的观察不谋而合：代码的”写”这个动作可能会被 AI 大量替代，但”设计”、”审查”、”优化”、”维护”这些更高层次的工作，仍然需要人类程序员。</p><h3 id="OpenAI-前研究负责人的观点"><a href="#OpenAI-前研究负责人的观点" class="headerlink" title="OpenAI 前研究负责人的观点"></a>OpenAI 前研究负责人的观点</h3><p>Bob McGrew（OpenAI 前研究负责人）提出了”vibe coding”的概念，指的是通过自然语言提示让 AI 生成代码原型的方式。他发现，虽然这种方式让产品经理等非技术人员能够快速做出原型，但这些原型往往需要被重写，因为：</p><ul><li>代码的可维护性不足</li><li>架构设计不够合理</li><li>边界条件和异常处理不完善</li><li>性能和安全性问题</li></ul><p>这说明，AI 可以快速生成”能跑”的代码，但要生成”能用”、”好用”、”可靠”的代码，仍然需要经验丰富的程序员来把关和重构。</p><h3 id="历史类比：从汇编语言到高级语言的转变"><a href="#历史类比：从汇编语言到高级语言的转变" class="headerlink" title="历史类比：从汇编语言到高级语言的转变"></a>历史类比：从汇编语言到高级语言的转变</h3><p>回顾编程语言的发展历史，我们也能看到类似的模式。早期程序员需要用汇编语言编写代码，直接操作寄存器、内存地址等底层细节。随着高级语言（如 C、Java、Python）的出现，很多底层的”写代码”工作被抽象化了。</p><p>但结果是什么呢？程序员并没有消失，反而数量大幅增加。因为：</p><ul><li><strong>需求爆发</strong>：高级语言降低了编程门槛，更多行业、更多场景开始需要软件</li><li><strong>新角色涌现</strong>：编译器工程师、运行时工程师、框架开发者、架构师等新角色大量出现</li><li><strong>技能要求提升</strong>：程序员需要理解更高层次的抽象，设计更复杂的系统</li></ul><p>从汇编到高级语言的转变，和从手写代码到 AI Coding 的转变，本质上是相似的：都是生产力的跃升，都带来了需求的爆发和角色的重塑。</p><h3 id="一个真实的案例：Claude-Code-和-Cursor-的当前实践"><a href="#一个真实的案例：Claude-Code-和-Cursor-的当前实践" class="headerlink" title="一个真实的案例：Claude Code 和 Cursor 的当前实践"></a>一个真实的案例：Claude Code 和 Cursor 的当前实践</h3><p>在 2026 年初，Claude Code 和 Cursor 代表了当前 AI Coding 工具的最前沿水平。这些工具的实际应用情况，更能说明 AI Coding 对程序员工作的真实影响。</p><p>以 <strong>Claude Sonnet 4.5</strong> 为例，这是 Anthropic 在 2025 年 9 月发布的最新版本。它在软件工程基准测试（如 SWE-Bench Verified）上表现显著提升，尤其在可靠性、重构和生产环境准备任务上有很大进步。更重要的是，它能够连续自主运作长达 <strong>30 小时</strong>，这意味着在处理大型、跨文件、跨模块的复杂任务时，Claude Code 比早期工具更稳定、更强大。</p><p>实际使用中，开发者发现：</p><ul><li><strong>速度和质量的双重提升</strong>：Claude Code 在原型开发速度上远超早期工具。描述完整功能（如认证系统、支付集成、Dashboard 界面）后，Claude Code 可以在几分钟内生成可工作的代码原型，而且代码结构清晰、带有注释和边界情况考虑</li><li><strong>跨文件重构能力</strong>：Claude Code 在上下文保持和跨多个文件重构方面表现突出，能在保持整个项目结构理解的情况下处理大型重构任务，这是早期工具因上下文窗口限制而难以做到的</li><li><strong>审查负担仍然存在</strong>：虽然 AI 生成的代码质量更高，但经验丰富的开发者仍然需要在代码审查、架构设计、性能优化等方面投入大量时间。Anthropic 内部的数据显示，虽然约 90% 的普通、重复性代码由 AI 撰写，但那些最复杂、有创新性、需要判断的部分仍然需要人类开发者</li></ul><p>这个案例揭示了一个重要的事实：即使是最先进的 AI Coding 工具，也提升了代码的”产量”和”质量”，但仍然增加了代码”质量把关”和”架构设计”的需求。那些能够审查、优化、重构 AI 生成代码，并设计整体系统的程序员，反而变得更加重要。</p><h3 id="我的实践：从数据看工作流程的转变"><a href="#我的实践：从数据看工作流程的转变" class="headerlink" title="我的实践：从数据看工作流程的转变"></a>我的实践：从数据看工作流程的转变</h3><p>作为一个在技术行业工作多年的程序员，我也想分享一些自己的实践数据，这些数据可能更能说明 AI Coding 工具对实际工作的影响。</p><p>在过去的 2025 年下半年，我写了十五万行代码，同比增长 30 多倍，其中 95% 以上是 Coding Agent 写的。这个数据背后反映的，不是”我变懒了”，而是工作流程的根本性转变：</p><ul><li><strong>更多时间花在定义问题和设计方案</strong>：我更多关注在定义问题，然后先做 tech design 文档，而在 coding 阶段主要交给 coding agent，这部分能够帮我省掉大量时间。</li><li><strong>Code review 的重要性提升</strong>：虽然大部分代码由 AI 生成，但 code review 我会和 AI 一起来做，尤其是核心模块。目前坦白来说，coding agent 还没有完全达到资深工程师的水准，产出不太稳定，需要持续反馈。</li></ul><p>这个实践印证了前面提到的观点：AI 工具确实大幅提升了代码的”产量”，但同时也让”定义问题”、”设计方案”、”质量把关”这些工作变得更加重要。那些能够与 AI 协作、持续反馈、确保代码质量的程序员，反而能够产出更多、更有价值的代码。</p><h2 id="对程序员的建议：如何适应-AI-Coding-时代"><a href="#对程序员的建议：如何适应-AI-Coding-时代" class="headerlink" title="对程序员的建议：如何适应 AI Coding 时代"></a>对程序员的建议：如何适应 AI Coding 时代</h2><p>结合我自己的观察和思考，我想给正在焦虑的程序员一些真诚的建议。这些建议不是空泛的鼓励，而是基于实际观察和思考得出的实用方向。</p><h3 id="1-从”写代码”转向”定义问题”"><a href="#1-从”写代码”转向”定义问题”" class="headerlink" title="1. 从”写代码”转向”定义问题”"></a>1. 从”写代码”转向”定义问题”</h3><p>AI 工具很擅长执行你明确说清楚的任务，但如果你定义不清，AI 的输出往往也不理想。因此，<strong>定义问题的能力变得前所未有的重要</strong>。</p><p>具体来说：</p><ul><li><strong>参与需求调研</strong>：不要只是等需求明确后再开始编码，而是要主动参与需求讨论，理解业务背景和用户痛点</li><li><strong>写清晰的验收标准</strong>：在开始编码前，明确功能边界、成功标准、错误处理方式等</li><li><strong>问正确的问题</strong>：这个功能为什么要做？用户真正需要什么？如果失败了会怎样？这些问题能帮助你更好地定义问题，也能让 AI 工具生成更符合需求的代码</li></ul><p>我观察到，在过去一年里，那些更容易适应 AI Coding 的程序员，往往都是那些更善于<strong>定义问题、理解需求</strong>的人。Tech Lead、架构师、hands-on manager、从程序员转产品的同事，他们之所以更容易适应，很大程度上是因为他们离真实需求更近，能够更好地判断 AI 生成代码是否符合需求。</p><h3 id="2-提升架构设计和系统思维能力"><a href="#2-提升架构设计和系统思维能力" class="headerlink" title="2. 提升架构设计和系统思维能力"></a>2. 提升架构设计和系统思维能力</h3><p>随着很多”写代码”的工作被 AI 自动化，<strong>系统设计和架构思维</strong>成为了程序员的核心竞争力。</p><p>具体来说：</p><ul><li><strong>学习经典架构模式</strong>：微服务、事件驱动、领域驱动设计等</li><li><strong>理解系统权衡</strong>：性能 vs 可维护性 vs 安全性 vs 扩展性，如何在不同的场景下做出合适的权衡</li><li><strong>阅读优秀项目源码</strong>：理解大型开源项目的组织结构和设计思路</li><li><strong>参与设计评审</strong>：在团队中承担或参与架构设计评审，学习如何从全局视角思考问题</li><li><strong>培养全栈能力</strong>：在 AI 的助力下，学习和掌握与当前方向技术关联的新领域变得更加容易。后端开发者可以借助工具做出不错的前端或移动应用，前端开发者也可以轻松地做一些后端和数据库功能。各工种之间的边界变得更加模糊，具备能够交付整体方案能力的程序员会更有竞争力，因为避免了大量的跨团队沟通和协调成本</li></ul><p><img src="../images/problem-resolver.jpg" alt="Think in System"></p><p>AI 工具可以帮你写单个模块的代码，但如何将这些模块组合成一个可靠、可扩展、可维护的系统，如何跨越前后端、移动端等不同技术栈的边界，交付完整的解决方案，这仍然需要人类的系统思维、架构能力和全栈视野。</p><h3 id="3-掌握与-AI-工具协作的能力"><a href="#3-掌握与-AI-工具协作的能力" class="headerlink" title="3. 掌握与 AI 工具协作的能力"></a>3. 掌握与 AI 工具协作的能力</h3><p>既然 AI 工具已经成为新的生产力工具，那么<strong>掌握如何与 AI 工具有效协作</strong>就成为了必备技能。</p><p>具体来说：</p><ul><li><strong>学习 prompt engineering</strong>：如何写出能让 AI 生成高质量代码的 prompt，如何根据反馈不断调整</li><li><strong>理解 AI 工具的局限</strong>：当前的 AI 工具依然基于大量现有代码实现来训练和模拟，对于特定领域的新问题、创新性需求或缺乏训练数据的场景，可能不具备很好的适配性，需要大量人工介入和调整。了解不同 AI 工具的能力边界，知道什么时候该用 AI，什么时候该自己写，这是有效协作的关键</li><li><strong>建立审查和验证机制</strong>：不要盲目信任 AI 生成的代码，要建立代码审查、测试、静态分析等质量保障机制</li><li><strong>实践 AI 工具链</strong>：将 AI 工具集成到你的工作流程中，但不要让流程完全依赖 AI，保持人类的主导权</li></ul><p>我建议每个程序员都应该主动尝试不同的 AI 编程工具，记录哪些 prompt 效果好，哪些场景下 AI 容易出错，这样你就能更好地利用这些工具，而不是被工具牵着走。</p><h3 id="4-加强质量意识和测试能力"><a href="#4-加强质量意识和测试能力" class="headerlink" title="4. 加强质量意识和测试能力"></a>4. 加强质量意识和测试能力</h3><p>AI 生成的代码虽然速度快，但质量往往参差不齐。因此，<strong>质量把关和测试能力</strong>变得尤为重要。</p><p>具体来说：</p><ul><li><strong>编写全面的测试用例</strong>：单元测试、集成测试、端到端测试，确保 AI 生成的代码在各种场景下都能正确工作</li><li><strong>建立代码审查流程</strong>：重点关注边界条件、异常处理、性能瓶颈、安全隐患等 AI 容易忽略的地方</li><li><strong>学习测试驱动开发</strong>：先写测试，再让 AI 生成代码，这样可以更好地验证代码的正确性</li><li><strong>关注非功能性需求</strong>：性能、安全、可维护性、可扩展性等，这些往往是 AI 生成代码的薄弱环节</li></ul><p>正如前面提到的 Claude Code 案例所示，AI 辅助编程虽然提升了代码的生成速度和质量，但也增加了代码审查和维护的负担。这说明，质量把关的工作不仅没有减少，反而变得更加重要。</p><h3 id="5-向高阶角色和跨职能方向发展"><a href="#5-向高阶角色和跨职能方向发展" class="headerlink" title="5. 向高阶角色和跨职能方向发展"></a>5. 向高阶角色和跨职能方向发展</h3><p>在过去一年里，我发现<strong>Tech Lead、架构师、hands-on manager、从程序员转产品的同事</strong>，他们更容易适应 AI Coding 时代。这是因为：</p><ul><li>他们有代码背景，能够判断 AI 生成代码的质量和正确性</li><li>他们离真实需求更近，能够更好地定义问题和验收标准</li><li>他们具备系统思维和架构能力，能够设计整体方案</li><li>他们理解业务，能够将技术方案转化为商业价值</li></ul><p>因此，我建议程序员可以：</p><ul><li><strong>争取参与或主持设计评审</strong>：学习如何从全局视角思考问题</li><li><strong>理解产品策略和业务逻辑</strong>：不只是实现功能，还要理解为什么要做这个功能</li><li><strong>学习业务知识</strong>：了解你所在行业的痛点和需求，这样你才能更好地定义问题</li><li><strong>积累跨职能协作经验</strong>：与产品、设计、运营等角色协作，理解不同角色的视角和需求</li></ul><h3 id="6-保持学习的心态"><a href="#6-保持学习的心态" class="headerlink" title="6. 保持学习的心态"></a>6. 保持学习的心态</h3><p>最后，也是最重要的一点：<strong>保持学习的心态</strong>。AI Coding 工具还在快速迭代，新的工具、新的模式、新的最佳实践不断涌现。只有保持学习，才能跟上这个变化的时代。</p><p>具体来说：</p><ul><li><strong>关注行业动态</strong>：了解最新的 AI 工具和技术趋势</li><li><strong>实践新技术</strong>：不要只是看，要动手实践，在实际项目中尝试新的工具和方法</li><li><strong>分享和交流</strong>：与同事分享使用 AI 工具的经验和教训，共同成长</li><li><strong>不要忽视基础</strong>：数据结构、算法、系统原理等基础知识仍然重要，当系统复杂、性能要求高、安全性敏感时，这些基础知识能帮助你做出正确的判断</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>回顾整篇文章，我的核心观点其实很简单：<strong>AI 时代不仅不会让程序员失业，反而会需要更多的程序员。技术普惠会激发出远比之前软件行业大得多的场景和需求，但前提是，我们必须学会与新的生产力工具共舞，升级自己的技能和思维方式。</strong></p><p>就像马车到汽车的跃迁一样，那些固守旧技能的人会被淘汰，但那些学会驾驭新工具、理解新规则的人，会迎来更大的机会。从汇编语言到高级语言的转变，也证明了这一点：生产力的跃升带来的是需求的爆发和角色的重塑，而不是职业的消失。</p><p>对于正在阅读这篇文章的程序员同行们，我想说的是：这个时代的变化确实很快，焦虑是正常的。但与其担心被 AI 取代，不如主动拥抱变化。从今天开始，尝试用 AI 工具解决实际问题，练习定义问题和设计方案，提升架构思维和全栈能力，向高阶角色发展。这些技能不会一夜之间掌握，但每一步的努力，都会让你在这个新时代中更有竞争力。</p><p>我相信，那些愿意学习、愿意适应、愿意成长的程序员，不仅不会被淘汰，反而会成为这个时代最稀缺和最重要的人才。让我们一起，成为新时代的”汽车司机”，而不是被淘汰的马车车夫。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><a href="https://www.infoq.cn/article/qCLoDkgquLIp57kAkoV3">Antirez on AI and Programming</a></li><li><a href="https://www.businessinsider.com/microsoft-cto-ai-generated-code-software-developer-job-change-2025-4">Microsoft CTO on AI-Generated Code</a></li><li><a href="https://www.businessinsider.com/vibe-coding-wont-replace-software-engineers-openai-research-bob-mcgrew-2025-6">Bob McGrew on Vibe Coding</a></li><li><a href="https://arxiv.org/abs/2502.20429">Will AI replace Software Engineers?</a></li><li><a href="https://www.businessinsider.com/anthropic-ai-model-claude-sonnet-extend-coding-lead-2025-9">Claude Sonnet 4.5 Extends Coding Lead</a></li><li><a href="https://www.businessinsider.com/most-anthropic-teams-coding-with-claude-ai-not-replacing-humans-2025-10">Anthropic Teams Coding with Claude</a></li><li><a href="https://www.hashbuilds.com/articles/claude-code-vs-github-copilot-2025-developer-comparison">Claude Code vs GitHub Copilot Comparison</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;“AI 会取代程序员吗？”&lt;/p&gt;
&lt;p&gt;这个问题从 AI Coding 工具开始真正进入工程实践以来，就一直萦绕在很多程序员的心头。虽然 GitHub Copilot 早在 2021 年就已发布，Cursor 也在 2023 年面世，但 2024 年大家并没有很认真地对待 AI coding，主要还停留在代码补全和测试用例生成等简单场景。然而到了 2025 年，随着 Claude Code 等新工具的推出，AI 在编程辅助上的应用真的变得广泛起来，从最初只能做简单的代码补全，到现在可以连续数小时不间断地生成相对完善的实现，这种变化让程序员的焦虑也在不断加剧。&lt;/p&gt;
&lt;p&gt;作为一个在技术行业摸爬滚打多年的程序员，我想分享一个可能和主流焦虑不太一样的观点：&lt;strong&gt;AI 时代不仅不会让程序员失业，反而会需要更多的程序员——但前提是，我们必须学会与新的生产力工具共舞，升级自己的技能和思维方式。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/ai-programmer.jpg&quot; alt=&quot;AI 时代会需要更多程序员&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="AI" scheme="https://springuper.github.io/tags/AI/"/>
    
    <category term="Career" scheme="https://springuper.github.io/tags/Career/"/>
    
    <category term="Growth" scheme="https://springuper.github.io/tags/Growth/"/>
    
    <category term="Technology" scheme="https://springuper.github.io/tags/Technology/"/>
    
  </entry>
  
  <entry>
    <title>父爱如山 - 读 Kite Runner 中 Baba 有感</title>
    <link href="https://springuper.github.io/kite-runner/"/>
    <id>https://springuper.github.io/kite-runner/</id>
    <published>2024-05-20T11:25:46.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>很久没有读过这样扣人心弦的小说了，记得上一次有这种爱不释手的感觉还是在高铁上读《三体》。一方面，作者讲述故事的方式非常精巧，细节和铺垫特别多，让人感觉既真实又意料之外。另一方面，因为作者的母语并不是英语，所以读英文原版就没有那么的吃力。好像和他一起在学习各种进阶词汇和表达一样，非常适合我。</p><p>读完后的感受颇多，很多人都写过主人公自己的背叛和救赎，这篇我想聚焦在另一个主要的角色上：Baba，主人公 Amir 的父亲。</p><h2 id="善于经营"><a href="#善于经营" class="headerlink" title="善于经营"></a>善于经营</h2><p>文章开篇就介绍了 Amir 家境的优越，而这一切都可以归功于 Baba：</p><blockquote><p>Everyone agreed that my father, my Baba, had built the most beautiful house in the Wazir Akbar Khan district, a new and affluent neighborhood in the northern part of Kabul. Some thought it was the prettiest house in all of Kabul.</p></blockquote><blockquote><p>Baba proved them all wrong by not only running his own business but becoming one of the richest merchants in Kabul. Baba and Rahim Khan built a wildly successful carpet-exporting business, two pharmacies, and a restaurant.</p></blockquote><p>Baba 不但是一位成功的商人，而且他还非常乐善好施。一个具体的例子就是他投入了巨大的精力和财力，花费了三年时间建设了一座孤儿院。在开业典礼上，Baba 做了一场精心准备的演讲，虽然还有些滑稽的场面，但最终赢得了大家经久不息的热烈掌声。</p><p>此外，Baba 还是一位非常活跃的社交家，这一点在 Amir 10 岁生日聚会上体现得淋漓尽致。</p><blockquote><p>Baba’s motto about throwing parties was this: Invite the whole world or it’s not a party. I remember scanning over the invitation list a week before my birthday party and not recognizing at least three-quarters of the four hundred-plus Kakas and Khalas who were going to bring me gifts and congratulate me for having lived to thirteen. Then I realized they weren’t really coming for me. It was my birthday, but I knew who the real star of the show was.</p></blockquote><blockquote><p>The night before the big party Baba’s friend Del-Muhammad, who owned a kabob house in Shar-e-Nau, came to the house with his bags of spices. Like the butcher, Del-Muhammad – or Dello, as Baba called him – refused payment for his services. He said Baba had done enough for his family already. It was Rahim Khan who whispered to me, as Dello marinated the meat, that Baba had lent Dello the money to open his restaurant. Baba had refused repayment until Dello had shown up one day in our driveway in a Benz and insisted he wouldn’t leave until Baba took his money.</p></blockquote><p>第一次读到这段内容的时候，我脑中浮现的是教父在女儿婚礼上的那一幕幕经典场景，众人环绕，德高望重。</p><p>毫无疑问，有如此一位成功的父亲，Amir 是十足幸运的。</p><span id="more"></span><h2 id="思想开明且深刻"><a href="#思想开明且深刻" class="headerlink" title="思想开明且深刻"></a>思想开明且深刻</h2><p>作为一个穆斯林国家，那时的阿富汗还认为喝酒是一种罪过，会在将来的审判日受到惩罚。不过 Baba 并不这么认为：</p><blockquote><p>“They do nothing but thumb their prayer beads and recite a book written in a tongue they don’t even understand.” He took a sip. “God help us all if Afghanistan ever falls into their hands.”</p></blockquote><p>他对于这些只会对着古兰经牵强附会的教条主义者嗤之以鼻。同时，他对于罪有一种更深的洞见：</p><blockquote><p>“Good,” Baba said, but his eyes wondered. “Now, no matter what the mullah teaches, there is only one sin, only one. And that is theft. <strong>Every other sin is a variation of theft</strong>. Do you understand that?”</p><p>“When you kill a man, you steal a life,” Baba said. “You steal his wife’s right to a husband, rob his children of a father. When you tell a lie, you steal someone’s right to the truth. When you cheat, you steal the right to fairness. Do you see?”</p><p>“There is no act more wretched than stealing, Amir,” Baba said. “A man who takes what’s not his to take, be it a life or a loaf of naan… I spit on such a man. And if I ever cross paths with him, God help him. Do you understand?”</p><p>“If there’s a God out there, then I would hope he has more important things to attend to than my drinking scotch or eating pork. Now, hop down. All this talk about sin has made me thirsty again.”</p></blockquote><p>我自己出生在一个天主教家庭，祖上因为过于贫穷接受了教会的救济后入了教。对于圣经上的很多内容，还有时不时一些人宣扬的类似月亮后面有个巨大的十字架之类的圣迹，我个人也是持类似的态度。不过我也认为如果真的有神明，而且人真的有死后的生活，那么应该会公正地奖惩分明些吧。</p><h2 id="对少时儿子的隐忧"><a href="#对少时儿子的隐忧" class="headerlink" title="对少时儿子的隐忧"></a>对少时儿子的隐忧</h2><p>作为父亲，Baba 非常疼爱 Amir，甚至有时候对他有些严格。并且，像所有父亲一样，Baba 也观察到儿子的一些不足之处：</p><blockquote><p>“Sometimes I look out this window and I see him playing on the street with the neighborhood boys. I see how they push him around, take his toys from him, give him a shove here, a whack there. And, you know, he never fights back. Never. He just… drops his head and…”</p><p>“So he’s not violent,” Rahim Khan said.</p><p>“That’s not what I mean, Rahim, and you know it,” Baba shot back. “There is something missing in that boy.”</p><p>“You just need to let him find his way,” Rahim Khan said.</p><p>“And where is he headed?” Baba said. “<strong>A boy who won’t stand up for himself becomes a man who can’t stand up to anything</strong>.”</p></blockquote><p>从一再出现的懦弱表现来看，Baba 开始对 Amir 的性格有些担忧，因为他觉得不能为自己挺身而出的人也不会为任何其他事情去奋争。这也预示了 Amir 未来必须面对的挑战。Baba 的这一评价对 Amir 产生了深远的影响，成为他成长过程中不断反思和努力改变的动力。</p><h2 id="疼爱-Hassan"><a href="#疼爱-Hassan" class="headerlink" title="疼爱 Hassan"></a>疼爱 Hassan</h2><p>Amir 年少时对于 Hassan 是有一些羡慕嫉妒恨的，这其中极大一部分原因来自于 Baba 对 Hassan 不合逻辑的疼爱，远超一般主仆关系的界限。</p><p>一个典型的例子就是：Baba 从来不会忘记 Hassan 的生日：</p><blockquote><p>Baba never missed Hassan’s birthday. For a while, he used to ask Hassan what he wanted, but he gave up doing that because Hassan was always too modest to actually suggest a present. So every winter Baba picked something out himself.</p></blockquote><p>另一个例子是在 Amir 跟父亲提及是不是用新仆人换掉 Ali 的时候，Baba 怒不可遏，说了很严厉的一些话：</p><blockquote><p>“Hassan’s not going anywhere,” Baba snapped. He dug a new hole with the trowel, striking the dirt harder than he had to. “He’s staying right here with us, where he belongs. This is his home and we’re his family. Don’t you ever ask me that question again!”</p></blockquote><p>这些行为让 Amir 感到了深深的不安和嫉妒，进一步导致了后面的诬陷 Hassan 的事件。</p><h2 id="逃亡路上的英勇之举"><a href="#逃亡路上的英勇之举" class="headerlink" title="逃亡路上的英勇之举"></a>逃亡路上的英勇之举</h2><p>在阿富汗战乱中，Baba 不得不带着 Amir 开始了逃亡之旅。在经过一个检查站的时候，面对一个俄国士兵的无理要求，Baba 奋不顾身地挺身而出：</p><blockquote><p>That was when Baba stood up. It was my turn to clamp a hand on his thigh, but Baba pried it loose, snatched his leg away. When he stood, he eclipsed the moonlight. “I want you to ask this man something,” Baba said. He said it to Karim, but looked directly at the Russian officer. “Ask him where his shame is.”</p></blockquote><p>在对方威胁要枪毙他的时候，Baba 仍然据理力争：</p><blockquote><p>“Tell him I’ll take a thousand of his bullets before I let this indecency take place,” Baba said. My mind flashed to that winter day six years ago. Me, peering around the corner in the alley. Kamal and Wali holding Hassan down. Assef’s buttock muscles clenching and unclenching, his hips thrusting back and forth. Some hero I had been, fretting about the kite. Sometimes, I too wondered if I was really Baba’s son.</p></blockquote><p>这样的举动让懦弱的 Amir 深深触动，他不禁想到六年前 Assef 他们欺辱 Hassan 的时候，如果他更勇敢一些会怎么样，他甚至怀疑自己是不是 Baba 的亲生骨肉。我相信这样惊心动魄的经历也是让 Amir 后面踏上救赎之路的重要因素，Baba 的精神激励和鼓舞了他。</p><h2 id="努力支撑在美国的移民生活"><a href="#努力支撑在美国的移民生活" class="headerlink" title="努力支撑在美国的移民生活"></a>努力支撑在美国的移民生活</h2><p>为了让 Amir 享受到更好的教育，父子俩移民到美国。一开始的生活并不容易，Baba 在大家的帮助下找到了加油站的工作，工作非常努力，即便是 996 也绝无抱怨。</p><blockquote><p>“It’s not so bad now,” he said, meaning since he had become the day manager at the gas station. But I’d seen the way he winced and rubbed his wrists on damp days. The way sweat erupted on his forehead as he reached for his bottle of antacids after meals. “Besides, I didn’t bring us here for me, did I?”</p></blockquote><p>对美国有更多熟悉后，Baba 善于经营的头脑再次发挥了重要作用。他找到了周末做二手货生意的好路子：</p><blockquote><p>On Saturdays, Baba woke me up at dawn. As he dressed, I scanned the classifieds in the local papers and circled the garage sale ads. We mapped our route – Fremont, Union City, Newark, and Hayward first, then San Jose, Milpitas, Sunnyvale, and Campbell if time permitted. Baba drove the bus, sipping hot tea from the thermos, and I navigated. We stopped at garage sales and bought knickknacks that people no longer wanted. We haggled over old sewing machines, one-eyed Barbie dolls, wooden tennis rackets, guitars with missing strings, and old Electrolux vacuum cleaners. By midafternoon, we’d filled the back of the VW bus with used goods. Then early Sunday mornings, we drove to the San Jose flea market off Berryessa, rented a spot, and sold the junk for a small profit: a Chicago record that we’d bought for a quarter the day before might go for $1, or $4 for a set of five; a ramshackle Singer sewing machine purchased for $10 might, after some bargaining, bring in $25.</p></blockquote><p>也就是在这样很多阿富汗移民的跳蚤市场上，Baba 开始慢慢找回在 Kabul 生活的感觉，结交了越来越多的朋友。</p><p>从他后面跟老相识介绍儿子的自豪中，我们可以感受到 Baba 对于儿子热烈的爱：</p><blockquote><p>“Amir is going to be a great writer,” Baba said. I did a double take at this. “He has finished his first year of college and earned A’s in all of his courses.”</p></blockquote><h2 id="对儿子最后的付出"><a href="#对儿子最后的付出" class="headerlink" title="对儿子最后的付出"></a>对儿子最后的付出</h2><p>在跳蚤市场上，Amir 结识了 Soraya。两个年轻人非常投缘。Baba 明显看出了端倪，他这样评价 Soraya：</p><blockquote><p>“I hear she is a decent girl, hardworking and kind. But no khastegars, no suitors, have knocked on the general’s door since.” Baba sighed. “It may be unfair, but what happens in a few days, sometimes even a single day, can change the course of a whole lifetime, Amir,” he said.</p></blockquote><p>不知道是不是刻意地安排，这样的叙述让 Amir 在追求 Soraya 上变得更加主动积极了一些。</p><p>随后 Baba 被诊断出了小细胞肺癌，他不愿让任何其他人知道，免得大家出于同情而怜悯他。</p><p>面对越来越虚弱的父亲，在 Soraya 一家探望之后，Amir 终于鼓足勇气，做出了一个无比正确的决定：</p><blockquote><p>I sat beside him. “Then I wonder if you’ll do something for me. If you’re not too exhausted.”</p><p>“What?”</p><p>“I want you to go khastegari. I want you to ask General Taheri for his daughter’s hand.”</p><p>Baba’s dry lips stretched into a smile. A spot of green on a wilted leaf. “Are you sure?”</p><p>“More sure than I’ve ever been about anything.”</p></blockquote><p>Baba 对待儿子的方式一直是尊重，然后让孩子自己做决定，轻易绝不插手。所以，虽然明显看出老人明显想撮合两个年轻人，还是难以想象老父亲听到儿子这一请求时的激动。Amir 开始有一些可以为之而战的事情了，他的儿子，在 22 岁的这一时刻，终于迈出了勇敢的一步。</p><p>尽管体力虚弱，Baba 还是精心打扮一番后去上门提亲，并成功地完成了这一作为父亲的重要使命。</p><blockquote><p>“Thank you, Baba. Are you all right? Do you feel up to this?”</p><p>“Up to this? It’s <strong>the happiest day of my life</strong>, Amir,” he said, smiling tiredly.</p></blockquote><p>在婚礼后一个月，Baba 在睡梦中离世。</p><h2 id="Hassan-的身世"><a href="#Hassan-的身世" class="headerlink" title="Hassan 的身世"></a>Hassan 的身世</h2><p>父亲离世十多年后的 2001 年，他的挚友 Rahim 打来电话，从此 Amir 踏上了营救 Hassan 儿子的救赎之旅。在一封写给 Amir 的信中，Rahim 清晰阐述了 Baba 为何小时候对待 Amir 如此严厉：</p><blockquote><p>Amir jan, I know how hard your father was on you when you were growing up. I saw how you suffered and yearned for his affections, and my heart bled for you. But your father was a man torn between two halves, Amir jan: you and Hassan. He loved you both, but he could not love Hassan the way he longed to, openly, and as a father. So he took it out on you instead – Amir, the socially legitimate half, the half that represented the riches he had inherited and the sin-with-impunity privileges that came with them. When he saw you, he saw himself. And his guilt. You are still angry and I realize it is far too early to expect you to accept this, but maybe someday you will see that when your father was hard on you, he was also being hard on himself. Your father, like you, was a tortured soul, Amir jan.</p></blockquote><p>想必彼时彼刻，Amir 对父亲的理解会更深一个层次吧。Baba 也是在努力救赎自己。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>为了建立一个对 Baba 更加具象的理解，我后面去看了 Kite Runner 的电影版。里面的 Baba 非常符合我对书中这一角色的印象，聪明、英勇、坚毅、关爱家人。他希望 Amir 和 Hassan 都能成长为有所作为的人，能够为自己而战的男人。他很幸运，在人生的终点之前看到了 Amir 的蜕变。可惜的是，更像他的 Hassan 在战乱中不幸罹难，在一个英勇的举动之后。但这就是人生，只要你去努力了，结果并无所谓。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;很久没有读过这样扣人心弦的小说了，记得上一次有这种爱不释手的感觉还是在高铁上读《三体》。一方面，作者讲述故事的方式非常精巧，细节和铺垫特别多，让人感觉既真实又意料之外。另一方面，因为作者的母语并不是英语，所以读英文原版就没有那么的吃力。好像和他一起在学习各种进阶词汇和表达一样，非常适合我。&lt;/p&gt;
&lt;p&gt;读完后的感受颇多，很多人都写过主人公自己的背叛和救赎，这篇我想聚焦在另一个主要的角色上：Baba，主人公 Amir 的父亲。&lt;/p&gt;
&lt;h2 id=&quot;善于经营&quot;&gt;&lt;a href=&quot;#善于经营&quot; class=&quot;headerlink&quot; title=&quot;善于经营&quot;&gt;&lt;/a&gt;善于经营&lt;/h2&gt;&lt;p&gt;文章开篇就介绍了 Amir 家境的优越，而这一切都可以归功于 Baba：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Everyone agreed that my father, my Baba, had built the most beautiful house in the Wazir Akbar Khan district, a new and affluent neighborhood in the northern part of Kabul. Some thought it was the prettiest house in all of Kabul.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Baba proved them all wrong by not only running his own business but becoming one of the richest merchants in Kabul. Baba and Rahim Khan built a wildly successful carpet-exporting business, two pharmacies, and a restaurant.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Baba 不但是一位成功的商人，而且他还非常乐善好施。一个具体的例子就是他投入了巨大的精力和财力，花费了三年时间建设了一座孤儿院。在开业典礼上，Baba 做了一场精心准备的演讲，虽然还有些滑稽的场面，但最终赢得了大家经久不息的热烈掌声。&lt;/p&gt;
&lt;p&gt;此外，Baba 还是一位非常活跃的社交家，这一点在 Amir 10 岁生日聚会上体现得淋漓尽致。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Baba’s motto about throwing parties was this: Invite the whole world or it’s not a party. I remember scanning over the invitation list a week before my birthday party and not recognizing at least three-quarters of the four hundred-plus Kakas and Khalas who were going to bring me gifts and congratulate me for having lived to thirteen. Then I realized they weren’t really coming for me. It was my birthday, but I knew who the real star of the show was.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The night before the big party Baba’s friend Del-Muhammad, who owned a kabob house in Shar-e-Nau, came to the house with his bags of spices. Like the butcher, Del-Muhammad – or Dello, as Baba called him – refused payment for his services. He said Baba had done enough for his family already. It was Rahim Khan who whispered to me, as Dello marinated the meat, that Baba had lent Dello the money to open his restaurant. Baba had refused repayment until Dello had shown up one day in our driveway in a Benz and insisted he wouldn’t leave until Baba took his money.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第一次读到这段内容的时候，我脑中浮现的是教父在女儿婚礼上的那一幕幕经典场景，众人环绕，德高望重。&lt;/p&gt;
&lt;p&gt;毫无疑问，有如此一位成功的父亲，Amir 是十足幸运的。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Book Review" scheme="https://springuper.github.io/tags/Book-Review/"/>
    
    <category term="Father" scheme="https://springuper.github.io/tags/Father/"/>
    
    <category term="Love" scheme="https://springuper.github.io/tags/Love/"/>
    
  </entry>
  
  <entry>
    <title>Just Culture</title>
    <link href="https://springuper.github.io/just-culture/"/>
    <id>https://springuper.github.io/just-culture/</id>
    <published>2024-02-16T10:35:46.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>“尚春，你去写一份 Case Study 吧”。</p><p>当师父对我说出这句话的时候，我的第一反应是吃惊和委屈。根据我的了解，Case Study 一般是在发生较大的线上故障后才由当事人总结的。由于我们技术部门刚刚推行这个制度不久，当时只有很少的人写过。因此，我下意识地认为这更多是一种羞辱和惩罚，觉得本来一个不太大的问题会被所有人都知道，自己的面子都挂不住。</p><p>师父对我们一直都非常好，幽默风趣，善于授人以渔。他对技术和界面都有着极致的追求。虽然觉得写 Case Study 对这次线上故障有些小题大做，但我还是硬着头皮打开文档平台，开始着手写总结。首先，我简单了解了 Case Study 的文档结构，一般包含：</p><ul><li>关键事件时间线。主要记录线上故障的发生、发现、处理和解决过程；</li><li>预估损失。不同的故障所造成的损失也不不尽相同。不过一般可以分为直接损失和间接损失。间接损失主要和 KPI 挂钩；</li><li>原因分析。要求采用 5 Why 准则来层层递进地分析问题的本源；</li><li>改进措施。根据之前的分析罗列出来具体的改进措施和负责人。</li></ul><p>写完总结后，师父很满意，同时也给了一些建议，并嘱咐我一定要抓紧落实改进措施。这时我开始有些新的认识：他是想让更多人避免类似的问题，而不仅仅是惩罚我。</p><span id="more"></span><h2 id="写-Case-Study-最多的人"><a href="#写-Case-Study-最多的人" class="headerlink" title="写 Case Study 最多的人"></a>写 Case Study 最多的人</h2><p>随着时间的推移，我们之前引入的一些代码检查等措施，提前发现了一些即将上线代码的问题。这让大家对之前的改进有了更多积极的评价。同时，其他业务线上的一些问题也引起老板的关注，他也提出了类似的要求。这些变化让我更深层次地理解了他的动机：<strong>让团队变得更强，系统更健壮</strong>。至于当事人的声誉，这是见仁见智的。但老板最终还是给我升职加薪了，说明至少在他看来，我的输出还可以，之前线上的问题并没有明显影响我的绩效。</p><p>有了这样的经验后，我对 Case Study 的看法逐渐有了转变，认为它不再是一种惩罚，而更多的是一种深入分析问题原因并加以改进的重要手段。这种自我批评、持续迭代的能力对于一个工程师或者一个技术团队来说至关重要。</p><p>后来我开始带人，再后来开始带团队，我除了自己也会主动去写 Case Study 以外，也会像我师父一样鼓励团队成员更多地去写。我本人甚至成为技术部门写 Case Study 最多的人之一。相应的，我们的代码质量、开发效率也在稳步提升，团队逐渐形成了一种学习进步的氛围。神奇的是，我们小组的声誉并没有因为写的 Case Study 多就备受诟病。</p><h2 id="这里的-Post-Mortem-居然连当事人名字都不写"><a href="#这里的-Post-Mortem-居然连当事人名字都不写" class="headerlink" title="这里的 Post Mortem 居然连当事人名字都不写"></a>这里的 Post Mortem 居然连当事人名字都不写</h2><p>在 Tubi 工作后，我发现这里大家也会去写类似 Cast Study 的文档，名字叫 Post Mortem，中文是“事后剖析”。总体形式和目的都很相像，只有两点不太一样：</p><ul><li>引入了 Time To Detect, Time To Aware, Time To Engage, Time To Fix 等故障生命周期指标，可以监控和逐步改进故障响应效率；</li><li>不再提及任何具体的当事人名字。</li></ul><p>第一点很容易理解。第二点一开始挺让我吃惊的，整个时间线都在描述具体的事实，没有任何一个公司同事的名字。即便有些地方非常有必要用一个人物作为主语或者宾语，也会用 A Engineer 或者其它方式来指代。后来通过和大家的接触我慢慢理解了这样的用意：避免指责具体的个人，把主要的焦点放在故障本身。联想到之前第一次写 Case Study 的情境，如果我只作为文档的书写者而不必在时间线和责任人里写上自己名字的话，大概也不会感到那么难堪吧。这的确是一个好办法！</p><h2 id="公正文化-Just-Culture"><a href="#公正文化-Just-Culture" class="headerlink" title="公正文化 Just Culture"></a>公正文化 Just Culture</h2><p>后来有美国同事跟我推荐了一篇 Just Culture 的文章，顿时有种相见恨晚的感觉，之前模糊的认知被这套已经在航空、医疗等高风险行业实施了很多年的经验一下子变得清晰和深刻起来。</p><blockquote><p>Just culture is a concept related to systems thinking which emphasizes that <strong>mistakes are generally a product of faulty organizational cultures</strong>, rather than solely brought about by the person or persons directly involved. In a just culture, after an incident, the question asked is, “What went wrong?” rather than “Who caused the problem?”.</p></blockquote><p>Just Culture 是一种组织文化的理念，其核心思想是鼓励组织在处理错误和事故时采取一种公正和合理的态度，而不是简单地追究责任。和 Blame Culture 相对立，这一理念认为事故通常是由多个因素的复杂互动引起的，而不仅仅是个别人员的责任。因此，对于无意的错误和系统性问题，应该以学习和改进为导向，而不是惩罚个别员工。</p><p>具体来说，这种文化包含如下要素：</p><ul><li>采用共享责任的模型：鼓励建立一种共享责任的模型，使整个组织都参与到错误和事故的处理中。这意味着不仅仅是个别人员负责，而是整个团队和组织共同分享责任；</li><li>从错误中学习而不是责怪个体：强调将错误视为学习的机会，而不是简单地追究责任。通过分析错误的根本原因，组织可以制定改进措施，以防止将来类似的错误发生；</li><li>管理行为选择：关注并区分人为错误、有风险的行为和鲁莽行为。鼓励组织采取不同的方法来处理这些不同类型的行为，以确保适当的反应和学习；</li><li>将安全设计纳入系统和流程：强调将安全性集成到组织的所有系统和流程中，而不是仅仅依赖于个体的注意力和警觉。通过设计安全性，可以减少系统性的错误和事故；</li><li>组织、领导层对共同目标的承诺：这种承诺在组织层面上奠定了文化基石，使得处理错误和事故成为一个合作和学习的过程，而不是惩罚的手段。</li></ul><p>总体而言，在 Just Culture 下，团队成员能够更好地理解和接受自己的责任。他们知道他们的行动和决策对整个团队和项目的结果产生影响。这种责任感激励着他们更加谨慎和专注地工作，以减少错误和失误的发生。团队成员之间建立了更强的信任和合作关系，因为他们知道即使犯错，他们也会得到支持和帮助。</p><h2 id="对于管理者的挑战"><a href="#对于管理者的挑战" class="headerlink" title="对于管理者的挑战"></a>对于管理者的挑战</h2><p>从 Blame Culture 转变到 Just Culture 绝非易事。一方面，人的本性是趋利避害的，大多数人不会轻易地去提出各种潜在的问题。另一方面，准确地区分系统问题、人为失误、鲁莽行为等不同类型的错误极具挑战，团队不同人的认识层次也较难统一。这些挑战都是现实存在的，需要管理者认真思考和应对。</p><p>结合我自身的经验，在这里分享几个给管理者的建议：</p><ul><li><strong>以身作则</strong>。作为管理者，是团队成员的标杆。如果能够更多地去认真剖析自己的过失，在系统和流程层面做出改进，就会向大家传达一个强烈的信号：领导也是人，也会犯错，而且愿意和大家分享他当时的经历，并从中汲取教训。领导如果只说不做，大家会有些踌躇、将信将疑。只有领导这样做了，大家才会更加信服，认为这是一种真真切切的鼓励；</li><li><strong>建立透明的沟通机制</strong>：建立一个开放、透明、安全的沟通环境对于 Just Culture 来说至关重要，只有让大家畅所欲言才能把事实越辩越明、挖到足够多足够深入的细节，也让大家参与到团队文化的构建中来。事实上，这对于一个团队的成长来说几乎是最为重要的，需要每个管理者足够重视；</li><li><strong>平衡责任与学习</strong>：在处理错误时，要确保适当的责任被追究，同时也要强调学习和改进。区分人为错误、有风险的行为和鲁莽行为非常有难度，管理者需要制定清晰的标准和流程，以便在面临不同类型的行为时能够做出一致的决策。培训团队成员以更好地理解和评估这些行为也是关键的一步。</li></ul><p>总之，Just Culture 在团队建设方面具有重要的益处。它为团队成员提供了一个安全的环境，鼓励他们积极报告错误和失误，并从中学习和改进。同时，实施 Just Culture 也面临不少挑战，需要确保公正和平衡，并培养团队成员的自我反思和学习能力。通过领导者的支持和示范，Just Culture 可以在技术团队中发挥其潜力，促进构建一个持续学习、表现卓越的优秀团队。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><a href="https://en.wikipedia.org/wiki/Just_culture">Just Culture - Wikipedia</a></li><li><a href="https://www.rsna.org/news/2019/february/just-culture-background">Just Culture: Balancing Accountability with Quality and Safety</a></li><li><a href="https://pilotswhoaskwhy.com/2023/05/15/the-power-of-just-culture-killing-the-blame-game/">The Power of Just Culture: Killing the Blame Game</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;“尚春，你去写一份 Case Study 吧”。&lt;/p&gt;
&lt;p&gt;当师父对我说出这句话的时候，我的第一反应是吃惊和委屈。根据我的了解，Case Study 一般是在发生较大的线上故障后才由当事人总结的。由于我们技术部门刚刚推行这个制度不久，当时只有很少的人写过。因此，我下意识地认为这更多是一种羞辱和惩罚，觉得本来一个不太大的问题会被所有人都知道，自己的面子都挂不住。&lt;/p&gt;
&lt;p&gt;师父对我们一直都非常好，幽默风趣，善于授人以渔。他对技术和界面都有着极致的追求。虽然觉得写 Case Study 对这次线上故障有些小题大做，但我还是硬着头皮打开文档平台，开始着手写总结。首先，我简单了解了 Case Study 的文档结构，一般包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关键事件时间线。主要记录线上故障的发生、发现、处理和解决过程；&lt;/li&gt;
&lt;li&gt;预估损失。不同的故障所造成的损失也不不尽相同。不过一般可以分为直接损失和间接损失。间接损失主要和 KPI 挂钩；&lt;/li&gt;
&lt;li&gt;原因分析。要求采用 5 Why 准则来层层递进地分析问题的本源；&lt;/li&gt;
&lt;li&gt;改进措施。根据之前的分析罗列出来具体的改进措施和负责人。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写完总结后，师父很满意，同时也给了一些建议，并嘱咐我一定要抓紧落实改进措施。这时我开始有些新的认识：他是想让更多人避免类似的问题，而不仅仅是惩罚我。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Growth" scheme="https://springuper.github.io/tags/Growth/"/>
    
    <category term="Organization" scheme="https://springuper.github.io/tags/Organization/"/>
    
    <category term="Culture" scheme="https://springuper.github.io/tags/Culture/"/>
    
  </entry>
  
  <entry>
    <title>编译型框架 SvelteJS</title>
    <link href="https://springuper.github.io/Svelte/"/>
    <id>https://springuper.github.io/Svelte/</id>
    <published>2023-12-31T13:57:00.000Z</published>
    <updated>2026-10-03T09:20:43.750Z</updated>
    
    <content type="html"><![CDATA[<p>2016 年，Rollup 等工具的作者 Rich Harris 推出了又一力作：Svelte。这一新兴框架独辟蹊径，旨在解决 React 等框架的一系列问题。这几年 Svelte 虽然没有大红大紫，但从 <a href="https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/">State of JS</a>  统计来看，它一直保持着持久的热度：<img src="../images/Svelte-01.jpg"><em>使用度紧跟三大框架</em><img src="../images/Svelte-02.jpg"><em>兴趣度蝉联榜首多年</em></p><p>本文尝试由浅入深的介绍 Svelte，希望更多人在适合的场景下可以考虑尝试它。</p><h2 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h2><p>Rich 非常喜欢 React 等前端库和框架带来的更加声明式、低心智负担的开发方式，这让前端工程师可以更好地控制复杂度、提升开发效率和质量。然而他也敏锐地洞察到了一些不那么尽如人意之处。</p><h3 id="Virtual-DOM-真的快吗？"><a href="#Virtual-DOM-真的快吗？" class="headerlink" title="Virtual DOM 真的快吗？"></a>Virtual DOM 真的快吗？</h3><p>React、Vue 都引入了 Virtual DOM 的概念，从而能够进行现有 DOM 以及期望 DOM 的对比，然后再执行相对较重的 DOM 操作使得两者一致。</p><p>在 <a href="https://svelte.dev/blog/virtual-dom-is-pure-overhead">Virtual DOM is pure overhead</a> 一文中， Rich 指出 Virtual DOM 其实更多的是一种实现声明式、状态驱动 UI 开发的一种手段，并非不可或缺。它主要的问题在于有很多没有必要的 Diff。</p><p>举个例子，假设我们有如下一个 React App：</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="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="function">() =&gt;</span> setCount(count + <span class="number">1</span>);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;app&quot;</span>&gt;</span></span></span><br><span class="line"><span class="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="xml">       <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleClick&#125;</span>&gt;</span>+1<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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>如果点击一次按钮，新旧 Virtual DOM 对比如下：<img src="../images/Svelte-03.jpg"></p><span id="more"></span><p>虽然只有 p 节点下的文本需要更新，然而，实际上 React 会遍历上述所有的节点，造成不必要的额外开销。</p><p>或许很多人觉得纯粹的 JS 运算不会有很显著的开销，不过在页面内容和逻辑足够复杂的情况下这个问题会愈加明显。因此，React 才提供了 <code>useMemo</code>, <code>useCallback</code> 等 Hooks 来避免一些耗时操作的重复执行或者不必要的重复渲染。但这些其实可以视作为一种<strong>错误设计的副作用：</strong>让开发者需要关注一些本是实现原理层面的技巧，增加了额外的心智负担。</p><p>那么是否有一种新的方式，可以在<code>count</code> 变化的时候仅仅更新那一个引用它的 DOM 文本节点呢？ 如果能够做到这一点，那岂不是更加响应式？</p><h3 id="Runtime-真的有必要吗？"><a href="#Runtime-真的有必要吗？" class="headerlink" title="Runtime 真的有必要吗？"></a>Runtime 真的有必要吗？</h3><p>React、Angular、Vue 等前端框架都有运行时，用以处理用户操作或者数据请求等情况下引发的组件状态变化，尽可能高效率地更新相应 DOM 节点。因此，运行时代码也会被打包到最终产物中。以典型的 <a href="https://github.com/tastejs/todomvc">TodoMVC</a> 应用举例，我们看一下几种方案下的 JS 包体积：</p><table><thead><tr><th>框架</th><th>JS 包体积</th><th>JS 包体积（Gzip）</th></tr></thead><tbody><tr><td>React v17.0.2</td><td>148K</td><td>48K</td></tr><tr><td>Angular v17.0.0</td><td>219K</td><td>66K</td></tr><tr><td>Vue v3.3.10</td><td>80K</td><td>32K</td></tr><tr><td>JavaScript ES6</td><td>8.2K</td><td>2.8K</td></tr></tbody></table><p>如果我们以 JavaScript ES6 为基准来衡量其它方案因为运行时而带来的额外包体积来看，那么 Vue 大概是 10 倍，React 大约为 18 倍，Angular 则达到了近 27 倍。</p><p>诚然，这些优秀的框架为我们提供了更为便捷和高效的开发方式，而且现在动辄上兆的中大型前端应用比比皆是，更快的网络条件下这区区几十 K 压缩后的 JS 包并不是那么关键。不过，退一步想，如果有一个新的方案可以提供比肩主流框架的特性但是最终包体积接近原生 JavaScript，鱼和熊掌兼得，那岂不是更好？</p><h2 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h2><p>基于以上两点洞见，Rich 开始从另一个全新的角度来重新理解和思考前端框架：</p><blockquote><p>Instead, the reason that ideas like React are so wildly and deservedly successful is that they make it easier to manage the complexity of your concepts. Frameworks are primarily <strong>a tool for structuring your thoughts, not your code</strong>.<br>Given that, what if the framework <em>didn’t actually run in the browser</em>? What if, instead, <strong>it converted your application into pure vanilla JavaScript</strong>, just like Babel converts ES2016+ to ES5? You’d pay no upfront cost of shipping a hefty runtime, and your app would get seriously fast, because there’d be no layers of abstraction between your app and the browser.<br><a href="https://www.svelte.cn/blog/frameworks-without-the-framework">Frameworks without the framework: why didn’t we think of this sooner?</a></p></blockquote><p>他认为前端框架应该是主要用来<strong>结构化想法而非代码</strong>，它们使得一个应用的种种复杂度能够受到合理的控制。如果我们有办法能够在提供结构化想法的种种功能的同时，在<strong>编译阶段</strong>将应用源代码转换为接近原生 JS 实现的最终代码，那么就不再需要浏览器和源代码之间的中间抽象层，我们的应用也会运行的更快。</p><p>知易行难，虽然有了一个崭新的思路，但是落实在实现层面还会有众多挑战。接下来我们简要介绍下 Svelte 的具体实现。</p><h3 id="基于模板的组件"><a href="#基于模板的组件" class="headerlink" title="基于模板的组件"></a>基于模板的组件</h3><p>为了能够最大限度地理解和推断开发者的意图，Svelte 采用了模板为先的组件封装方案。每个组件都存储在<code>.svelte</code> 文件中，语法是 HTML 的超集。</p><p>下面这个示例把前面的计数器 App 用 Svelte 来重新实现：</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">let</span> count = <span class="number">0</span>;</span></span><br><span class="line"><span class="javascript">  <span class="keyword">const</span> handleClick = <span class="function">() =&gt;</span> count += <span class="number">1</span>;</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;app&quot;</span>&gt;</span></span><br><span class="line">  <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><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">on:click</span>=<span class="string">&#123;handleClick&#125;</span>&gt;</span>+1<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="css"></span></span><br><span class="line"><span class="css">  <span class="selector-class">.app</span> &#123; <span class="attribute">text-align</span>: center &#125;</span></span><br><span class="line"><span class="css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure><p><em>完整示例见 <a href="https://svelte.dev/repl/e8f0d2ebd57242b0915d070464611272?version=4.2.8">Counter App • REPL • Svelte</a>。</em></p><p>看起来真的非常像 Vue，组件的 JS、CSS、HTML 都写在同一文件中集中管理。因此从开发者的角度来看， Svelte 编程方式十分熟悉、自然。</p><p>选择模板而非灵活的 JSX 的原因也可以在这个例子中有所体现。编译器很容易发现结构化的视图中 p 元素有一部分文本需要用到<code>count</code>，以及点击按钮时需要调用一个回调函数，其它的部分都是确定的。因此只需在<code>count</code>变更时刷新相应的 DOM 文本节点即可。</p><p>为了在有一定约束的情况下提供足够多的功能，Svelte 也提供了一系列的模板语法：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- if block --&gt;</span></span><br><span class="line">&#123;#if answer === 42&#125;</span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>what was the question?<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">&#123;/if&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- each block --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span><br><span class="line">  &#123;#each items as item&#125;</span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span>&gt;</span>&#123;item.name&#125; x &#123;item.qty&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">  &#123;/each&#125;</span><br><span class="line"><span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- bind events --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">form</span> <span class="attr">on:submit</span>|<span class="attr">preventDefault</span>=<span class="string">&#123;handleSubmit&#125;</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- the `submit` event&#x27;s default is prevented, so the page won&#x27;t reload --&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- bind properties --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">input</span> <span class="attr">bind:value</span>=<span class="string">&#123;name&#125;</span> /&gt;</span></span><br><span class="line"><span class="comment">&lt;!-- same as &lt;input on:input=&#123;e =&gt; name = e.target.value&#125; value=&#123;name&#125; /&gt; --&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- nested components --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">import</span> Widget <span class="keyword">from</span> <span class="string">&#x27;./Widget.svelte&#x27;</span>;</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">Widget</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span> </span><br></pre></td></tr></table></figure><h3 id="Compiler-as-Framework"><a href="#Compiler-as-Framework" class="headerlink" title="Compiler as Framework"></a>Compiler as Framework</h3><p>在介绍 Svelte 编译器如何工作之前，我们不妨设想：如果让我们自己使用原生 JS 实现上面的计数器 App 的话，会是什么样的呢？</p><p>一个可供参考的实现如下（暂时忽略 CSS）：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// Step 1: Declare variables and functions in script tag</span></span><br><span class="line">  <span class="keyword">let</span> count = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    count += <span class="number">1</span>;</span><br><span class="line">    lifecycle.update(<span class="string">&quot;count&quot;</span>); <span class="comment">// Update the DOM nodes related to `count`</span></span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Step 2: Declare HTML elements</span></span><br><span class="line">  <span class="keyword">let</span> div;</span><br><span class="line">  <span class="keyword">let</span> p;</span><br><span class="line">  <span class="keyword">let</span> text1;</span><br><span class="line">  <span class="keyword">let</span> text2;</span><br><span class="line">  <span class="keyword">let</span> button;</span><br><span class="line">  <span class="keyword">let</span> text3;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Step 3: Declare component lifecycle</span></span><br><span class="line">  <span class="keyword">const</span> lifecycle = &#123;</span><br><span class="line">    <span class="comment">// Create DOM nodes and mount them under the target node</span></span><br><span class="line">    <span class="attr">create</span>: <span class="function">(<span class="params">target</span>) =&gt;</span> &#123;</span><br><span class="line">      div = <span class="built_in">document</span>.createElement(<span class="string">&quot;div&quot;</span>);</span><br><span class="line">      p = <span class="built_in">document</span>.createElement(<span class="string">&quot;p&quot;</span>);</span><br><span class="line">      text1 = <span class="built_in">document</span>.createTextNode(<span class="string">&quot;Count: &quot;</span>);</span><br><span class="line">      text2 = <span class="built_in">document</span>.createTextNode(count);</span><br><span class="line">      button = <span class="built_in">document</span>.createElement(<span class="string">&quot;button&quot;</span>);</span><br><span class="line">      text3 = <span class="built_in">document</span>.createTextNode(<span class="string">&quot;+1&quot;</span>);</span><br><span class="line"></span><br><span class="line">      p.appendChild(text1);</span><br><span class="line">      p.appendChild(text2);</span><br><span class="line">      div.appendChild(p);</span><br><span class="line">      button.addEventListener(<span class="string">&quot;click&quot;</span>, handleClick);</span><br><span class="line">      button.appendChild(text3);</span><br><span class="line">      div.appendChild(button);</span><br><span class="line">      target.appendChild(div);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// Update DOM nodes referring to some variable which has changed</span></span><br><span class="line">    <span class="attr">update</span>: <span class="function">(<span class="params">changed</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (changed === <span class="string">&quot;count&quot;</span>) &#123; <span class="comment">// If the changed variable is `count`, only update `text2` then</span></span><br><span class="line">        text2.data = count;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// Clean DOM nodes created in this component</span></span><br><span class="line">    <span class="attr">destroy</span>: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      div.removeChild(p);</span><br><span class="line">      button.removeEventListener(<span class="string">&quot;click&quot;</span>, handleClick);</span><br><span class="line">      div.removeChild(button);</span><br><span class="line">      div.parentNode.removeChild(div);</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> lifecycle;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em>可运行的完整示例见 <a href="https://codesandbox.io/p/sandbox/naive-compiled-app-hp8nmj?file=/index.html:4,29">Naive Compiled App</a>。</em></p><p>上述实现有如下特点：</p><ul><li>组件模板 script 中的部分基本原封不动，放在 App 组件函数的头部，因为一般这里会声明一些 DOM 中会用到的变量、回调等</li><li>App 组件返回的是其生命周期对象，具体包括如下三个阶段：</li><li>为了能够在<code>count</code>变量发生变化时更新对应 DOM 节点，一个可以考虑的方案是<strong>通过静态分析找到所有<code>count</code>变量更新的地方</strong>，然后插入<code>lifecycle.update(&#39;count&#39;)</code>语句</li></ul><p>下面让我们看一下使用 Svelte 生成的 JS 结果：</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="comment">/* App.svelte generated by Svelte v4.2.8 */</span></span><br><span class="line"><span class="keyword">import</span> &#123; SvelteComponent, append, ... &#125; <span class="keyword">from</span> <span class="string">&quot;svelte/internal&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">create_fragment</span>(<span class="params">ctx</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">let</span> div, p, ...</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="function"><span class="title">c</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">      div = element(<span class="string">&quot;div&quot;</span>);</span><br><span class="line">      t1 = text(<span class="comment">/*count*/</span> ctx[<span class="number">0</span>]);</span><br><span class="line">      ...</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="function"><span class="title">m</span>(<span class="params">target, anchor</span>)</span> &#123;</span><br><span class="line">      insert(target, div, anchor);</span><br><span class="line">      append(div, p);</span><br><span class="line">      ...</span><br><span class="line">      <span class="keyword">if</span> (!mounted) &#123;</span><br><span class="line">        dispose = listen(button, <span class="string">&quot;click&quot;</span>, <span class="comment">/*handleClick*/</span> ctx[<span class="number">1</span>]);</span><br><span class="line">        mounted = <span class="literal">true</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="function"><span class="title">p</span>(<span class="params">ctx, [dirty]</span>)</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (dirty &amp; <span class="comment">/*count*/</span> <span class="number">1</span>) set_data(t1, <span class="comment">/*count*/</span> ctx[<span class="number">0</span>]);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="function"><span class="title">d</span>(<span class="params">detaching</span>)</span> &#123;</span><br><span class="line">      ...</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="function"><span class="keyword">function</span> <span class="title">instance</span>(<span class="params">$$self, $$props, $$invalidate</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">let</span> count = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="function">() =&gt;</span> $$invalidate(<span class="number">0</span>, (count += <span class="number">1</span>));</span><br><span class="line">  <span class="keyword">return</span> [count, handleClick];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">App</span> <span class="keyword">extends</span> <span class="title">SvelteComponent</span> </span>&#123;</span><br><span class="line">  <span class="function"><span class="title">constructor</span>(<span class="params">options</span>)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>();</span><br><span class="line">    init(<span class="built_in">this</span>, options, instance, create_fragment, safe_not_equal, &#123;&#125;);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em>完整代码见 <a href="https://svelte.dev/repl/534e69f9c93046958241337ccbc8f8dd?version=4.2.8">Simple Counter App • REPL • Svelte</a>。</em></p><p>可以看出，虽然细节上有些许不同，但是整体的代码结构和我们前面人肉编译的版本非常相像。令人惊叹的是，Svelte 也能够在 <code>count</code> 变更时精准地更新对应的文本节点，十分高效。这里回调函数中的<code>$$invalidate</code>可以理解成一种更具效率的数据变更处理机制，具体实现运用了位掩码（bitmask）来标记脏数据的变量索引，然后安排一个 <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth">microtask</a> 异步进行批量更新。</p><p>此外，除了一些简单的 <code>append</code>、<code>element</code>、<code>set_data</code>等工具方法和<code>SvelteComponent</code>基类以外，最终构建的应用几乎没有使用任何额外的框架代码，所有的框架工作都基本在编译阶段完成，真所谓是“编译器即框架”！</p><h3 id="编译过程"><a href="#编译过程" class="headerlink" title="编译过程"></a>编译过程</h3><p>Svelte 编译器将 .svelte 文件中的组件编译成一个 JS 文件和一个可选的 CSS 文件，这些产物可以在浏览器中直接运行：<img src="../images/Svelte-04.jpg"></p><p>在编译器内部，可以细分为解析模板、分析代码、构建产物三个步骤。</p><p><strong>Step 1. 解析模板</strong></p><p>读取输入的 .svelte 组件内容后，Svelte 开始进行解析：</p><ul><li>对于<code>&lt;script&gt;</code>标签：使用 <a href="https://www.npmjs.com/package/acorn">Acron</a> 将其内容解析为抽象语法树 AST</li><li>对于<code>&lt;style&gt;</code>标签：使用 <a href="https://www.npmjs.com/package/css-tree">CSSTree</a> 将其内容解析为抽象语法树 AST</li><li>其它标签：使用自带的解析器将其内容解析为抽象语法树 AST，支持包括 HTML 标签、组件标签、逻辑语句块等</li></ul><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></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="comment">// HTML AST</span></span><br><span class="line">  <span class="attr">html</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;Fragment&#x27;</span>, <span class="attr">children</span>: [...] &#125;,</span><br><span class="line">  <span class="comment">// CSS AST</span></span><br><span class="line">  <span class="attr">css</span>: &#123; ... &#125;,</span><br><span class="line">  <span class="comment">// Script AST</span></span><br><span class="line">  <span class="attr">instance</span>: &#123; <span class="attr">context</span>: <span class="string">&#x27;default&#x27;</span>, <span class="attr">content</span>: &#123;...&#125; &#125;,</span><br><span class="line">  <span class="comment">// Module script AST (&lt;script context=&quot;module&quot;&gt;)</span></span><br><span class="line">  <span class="attr">module</span>: &#123; <span class="attr">context</span>: <span class="string">&#x27;context&#x27;</span>, <span class="attr">content</span>: &#123;...&#125; &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以计数器 App 为例，处理结果如图所示：<img src="../images/Svelte-05.jpg"></p><p><strong>Step 2. 分析代码</strong></p><p>这是非常核心的一个步骤，主要的工作是通过遍历上述 AST 来追踪视图用到的变量及其依赖，为接下来的代码生成做好准备。</p><p>仍然以计数器 App 为例，Svelte 分析过程大致如下：</p><ol><li>创建一个新的 Component 实例，用以存储分析结果信息，譬如 DOM 树 fragment、追踪的变量 vars 等</li><li>解析 instance 和 module 的 AST，从中找到所有的变量声明、变量引用和变量更新的表达式。在该例中，变量声明为 <code>let count = 0</code>，变量更新为 <code>count += 1</code> ，因此 Svelte 确定需要追踪的变量为<code>count</code></li><li>解析 html 的 AST，从中找到所有视图中使用到的变量。在该例中，有一个文本节点引用了<code>count</code>，因此 Svelte 标记该变量为 referenced，确定它更新时需要更新视图</li><li>处理 CSS selector 等</li></ol><p>有很多的优化手段都可以在这一步进行，譬如：</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="function"><span class="keyword">function</span> <span class="title">square</span>(<span class="params">num</span>) </span>&#123;</span></span><br><span class="line"><span class="javascript">    <span class="keyword">return</span> num * num;</span></span><br><span class="line"><span class="javascript">  &#125;</span></span><br><span class="line"><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">let</span> count = <span class="number">0</span>;</span></span><br><span class="line"><span class="javascript">  <span class="keyword">const</span> handleClick = <span class="function">() =&gt;</span> count += <span class="number">1</span>;</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;app&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>Square count: &#123;square(count)&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">on:click</span>=<span class="string">&#123;handleClick&#125;</span>&gt;</span>+1<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>添加的新函数<code>square</code>是一个不依赖任何外部变量的纯函数，虽然也被视图使用，但不需要每次创建这个组件的时候都重新声明，因此 Svelte 使用了变量提升（Hoisting）将其放在输出 JS module 的顶层而非<code>instance</code>函数内部。</p><p><strong>Step 3. 构建产物</strong></p><p>一切准备妥当，接下来就是组装最终的 JS 和 CSS 文件内容了。在这一步，一个主要的挑战是如何让生成的代码尽可能少。如无必要，勿增实体。</p><p>举一个例子，假设我们希望让计数器 App 可以通过属性来设定<code>count</code>的初始值，代码改动如下：</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="comment">// If this component is called with prop `count`, use its value. Otherwise use 0 by default</span></span></span><br><span class="line"><span class="javascript">  <span class="keyword">export</span> <span class="keyword">let</span> count = <span class="number">0</span>;</span></span><br><span class="line"><span class="javascript">  <span class="keyword">const</span> handleClick = <span class="function">() =&gt;</span> count += <span class="number">1</span>;</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">  <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><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">on:click</span>=<span class="string">&#123;handleClick&#125;</span>&gt;</span>+1<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>这时我们发现编译后的 JS 中<code>instance</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">instance</span>(<span class="params">$$self, $$props, $$invalidate</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">let</span> &#123; count = <span class="number">0</span> &#125; = $$props;</span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="function">() =&gt;</span> $$invalidate(<span class="number">0</span>, count += <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">  $$self.$$set = $$props =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="string">&#x27;count&#x27;</span> <span class="keyword">in</span> $$props) $$invalidate(<span class="number">0</span>, count = $$props.count);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> [count, handleClick];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里新定义的 <code>$$self.$$set</code> 方法专门提供给 <a href="https://svelte.dev/docs/client-side-component-api#%24set">component.$set</a> 方法以便动态更新组件属性。如果我们没有这样的需求，则最终的产物中就不会包含这样的逻辑。</p><p>想进一步了解整个编译过程的朋友可以阅读下 Svelte 核心贡献者陈立豪的这篇文章 <a href="https://lihautan.com/the-svelte-compiler-handbook/">The Svelte Compiler Handbook</a> 或者他的这个油管视频 <a href="https://www.youtube.com/watch?v=mwvyKGw2CzU">Build your own Svelte</a>。</p><h3 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h3><p>介绍完基本原理，现在让我们看下 Svelte 的具体表现吧。</p><p>从生成最终代码的体积来看，同样以 TodoMVC 为例，Svelte 代码的最终体积是：JS 包 13K，Gzip 压缩后 5.3K。下面是和其它框架对照的图表：<img src="../images/Svelte-06.jpg"></p><p>Svelte 构建 JS 包的体积是 Vue 的 1/6，React 的 1/9，Angular 的 1/12，已经非常接近于原生 JS。可以说是非常精巧了。</p><p>从运行速度来看，<a href="https://krausest.github.io/js-framework-benchmark/2023/table_chrome_120.0.6099.62.html">JS Framework Benchmark</a> 的测试结果如下：<img src="../images/Svelte-07.jpg"></p><p>从最后一行的加权结果来看，Svelte 的性能非常接近原生 JS，与同类型编译型框架 Solid 以及以快著称的类 React 框架 Inferno 相当，优于 Vue、Angular，更是大幅领先 React。</p><h2 id="进阶特性"><a href="#进阶特性" class="headerlink" title="进阶特性"></a>进阶特性</h2><p>了解完基本原理，我们这一小节来介绍几个常用的进阶特性。</p><h3 id="显式声明响应性"><a href="#显式声明响应性" class="headerlink" title="显式声明响应性"></a>显式声明响应性</h3><p>在实际开发中，我们常常遇到一些类似这样的场景：一个变量依赖了另外一个或者多个变量，且这种依赖关系非常确定。譬如：</p><ul><li>更新正方形的边长，其体积需要进行更新</li><li>添加一个新商品到购物车，购物车的商品数量和总价格需要相应的更新</li><li>标记一个待办事项为已完成，待办事项数量需要进行更新</li></ul><p>得益于编译器即框架的优势，Svelte 使用<code>$</code>标签来更为显式地声明这类依赖关系，建立更具效率的响应性：依赖的变量有变化则需要重新执行赋值操作。下面是一个示例：</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">let</span> count = <span class="number">1</span>;</span></span><br><span class="line"><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="comment">// the `$:` means &#x27;re-run whenever count changes&#x27;</span></span></span><br><span class="line"><span class="javascript">  $: doubled = count * <span class="number">2</span>;</span></span><br><span class="line"><span class="javascript">  <span class="comment">// the `$:` means &#x27;re-run whenever doubled changes&#x27;</span></span></span><br><span class="line"><span class="javascript">  $: quadrupled = doubled * <span class="number">2</span>;</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">button</span> <span class="attr">on:click</span>=<span class="string">&#123;()</span> =&gt;</span> count += 1&#125;&gt;</span><br><span class="line">  Count: &#123;count&#125;</span><br><span class="line"><span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;count&#125; * 2 = &#123;doubled&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;doubled&#125; * 2 = &#123;quadrupled&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br></pre></td></tr></table></figure><p><em>完整示例见 <a href="https://svelte.dev/examples/reactive-declarations">Reactive declarations • Svelte Examples</a>。</em></p><p>实现这一特性的思路并不复杂：遇到有标签<code>$</code>的赋值语句时，则在等号右侧变量变化的时候执行<code>$$invalidate</code>将左侧变量标记为脏数据，进而触发后续的更新逻辑。</p><p>进一步地，Svelte 还支持使用<code>$</code>标签来声明任意<code>&lt;script&gt;</code>标签内顶层语句的响应性，下面是官网提供的一些例子：</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">export</span> <span class="keyword">let</span> title;</span></span><br><span class="line"><span class="javascript">  <span class="keyword">export</span> <span class="keyword">let</span> person;</span></span><br><span class="line"><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="comment">// this will update `document.title` whenever</span></span></span><br><span class="line"><span class="javascript">  <span class="comment">// the `title` prop changes</span></span></span><br><span class="line"><span class="javascript">  $: <span class="built_in">document</span>.title = title;</span></span><br><span class="line"><span class="javascript"></span></span><br><span class="line"><span class="javascript">  $: &#123;</span></span><br><span class="line"><span class="javascript">    <span class="built_in">console</span>.log(<span class="string">`multiple statements can be combined`</span>);</span></span><br><span class="line"><span class="javascript">    <span class="built_in">console</span>.log(<span class="string">`the current title is <span class="subst">$&#123;title&#125;</span>`</span>);</span></span><br><span class="line"><span class="javascript">  &#125;</span></span><br><span class="line"><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="comment">// this will update `name` when &#x27;person&#x27; changes</span></span></span><br><span class="line"><span class="javascript">  <span class="attr">$</span>: (&#123; name &#125; = person);</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>顺便提一句，这类显式声明响应性的方式可以 work，但是如果我们忘了添加，遗漏了一些地方（毕竟这种方式不是那么的常规），就会造成一些问题。Svelte 5 提出了一个更加函数式的解决方案：Runes，有兴趣的朋友可以去读下官方博客的这篇文章：<a href="https://svelte.dev/blog/runes">Introducing runes</a>。</p><h3 id="动画"><a href="#动画" class="headerlink" title="动画"></a>动画</h3><p>Svelte 走编译路线也带来动画处理上的优势：一方面可以利用直接生成 DOM 操作的便利，在声明周期对象中添加各种动画所需的处理逻辑；另一方面，它可以做到只添加模板中使用到的动画逻辑，而不像普通的框架一样需要全部打包到 Runtime 中。</p><p>给节点添加过渡动画非常声明式，下面是一些示例：</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></pre></td><td class="code"><pre><span class="line">&#123;#if visible&#125;</span><br><span class="line">  <span class="comment">&lt;!-- Fade in, and fade out --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">transition:fade</span>&gt;</span>Fades in and out<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">&#123;/if&#125;</span><br><span class="line"></span><br><span class="line">&#123;#if visible&#125;</span><br><span class="line">  <span class="comment">&lt;!-- Fly in, and fade out --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">in:fly</span>=<span class="string">&#123;&#123;</span> <span class="attr">y:</span> <span class="attr">200</span>, <span class="attr">duration:</span> <span class="attr">2000</span> &#125;&#125; <span class="attr">out:fade</span>&gt;</span>Flies in, fades out<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">&#123;/if&#125;</span><br><span class="line"></span><br><span class="line">&#123;#if visible&#125;</span><br><span class="line">  <span class="comment">&lt;!-- You can even define your own transition function --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">transition:typewriter</span>&gt;</span>The quick brown fox jumps over the lazy dog<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">&#123;/if&#125;</span><br></pre></td></tr></table></figure><p>下图是官方动画的<a href="https://svelte.dev/examples/animate">一个示例</a>，Svelte 能够原生支持这种跨父节点的 DOM 节点移动动画，真的令人赞叹不已：<img src="../images/Svelte-08.gif"></p><h3 id="服务端渲染-SSR"><a href="#服务端渲染-SSR" class="headerlink" title="服务端渲染 (SSR)"></a>服务端渲染 (SSR)</h3><p>既然对标 React 的功能，那么 SSR 是必不可少的，也是在 Svelte 1.0 版本之前就已经支持的特性。</p><p>接下来我们来看下计数器 App 在服务端渲染出来的内容：</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="comment">/* App.svelte generated by Svelte v4.2.8 */</span></span><br><span class="line"><span class="keyword">import</span> &#123; create_ssr_component, <span class="built_in">escape</span> &#125; <span class="keyword">from</span> <span class="string">&quot;svelte/internal&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> App = create_ssr_component(<span class="function">(<span class="params">$$result, $$props, $$bindings, slots</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">let</span> count = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`&lt;div&gt;&lt;p&gt;Count: <span class="subst">$&#123;<span class="built_in">escape</span>(count)&#125;</span>&lt;/p&gt; &lt;button data-svelte-h=&quot;svelte-1usigck&quot;&gt;+1&lt;/button&gt;&lt;/div&gt;`</span>;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> App;</span><br></pre></td></tr></table></figure><p>与客户端生成代码非常不同，上面的 App 组件直接返回了一段字符串，也即根据当前的数据和组件模板渲染出的视图内容。其它的更新、销毁等声明周期在服务端这个场景下并不需要。</p><p>生成客户端代码时，需要开启 <a href="https://svelte.dev/docs/svelte-compiler#types-compileoptions">hydratable</a> 设置来让 Svelte 在 DOM 节点挂载阶段（mount）加入相应的逻辑，从而能够在保持由服务端输出的原始 DOM 结构的基础上让页面“动“起来。</p><h2 id="周边生态"><a href="#周边生态" class="headerlink" title="周边生态"></a>周边生态</h2><p>作为一个新兴的前端框架，Svelte 也逐步获得了越来越多的周边工具支持。譬如本专栏之前介绍过的 React Query 在更名为 Tanstack Query 后就推出了 <a href="https://tanstack.com/query/v5/docs/svelte/overview">Svelte Query</a>，他们团队的 React Table 目前也推出了 <a href="https://tanstack.com/table/v8/docs/adapters/svelte-table">Svelte Table</a>。在这一节，让我们从状态管理、UI 库和集成开发方案几个方面了解下 Svelte 的周边生态。</p><h3 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h3><p>Svelte 从 1.42 版本开始就提供了一个内置的轻量级 store，内部实现采用 PubSub 模式，对外提供了<code>readable</code>、<code>writable</code>、<code>derived</code> 三个不同类型 store 数据的接口。下面是采用 Svelte store 重写的计数器组件：</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></pre></td><td class="code"><pre><span class="line">// stores.js</span><br><span class="line">import &#123; writable &#125; from &#x27;svelte/store&#x27;;</span><br><span class="line">export const count = writable(0);</span><br><span class="line"></span><br><span class="line">// App.svelte</span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">import</span> &#123; count &#125; <span class="keyword">from</span> <span class="string">&#x27;./stores.js&#x27;</span>;</span></span><br><span class="line"><span class="javascript"></span></span><br><span class="line"><span class="javascript">  <span class="function"><span class="keyword">function</span> <span class="title">increment</span>(<span class="params"></span>) </span>&#123;</span></span><br><span class="line"><span class="javascript">    <span class="comment">// writble store has a `update` method to write its new value</span></span></span><br><span class="line"><span class="javascript">    count.update(<span class="function">(<span class="params">n</span>) =&gt;</span> n + <span class="number">1</span>);</span></span><br><span class="line"><span class="javascript">  &#125;</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- we need to add a prefix `$` in the store to use its value in the template --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>The count is &#123;$count&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">on:click</span>=<span class="string">&#123;increment&#125;</span>&gt;</span>+1<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>一般情况下，store 建议写在单独的 JS/TS 文件中，这样方便让各个组件共享。Svelte store 并不是以 Svelte 组件为核心，而是以数据为核心。在这一点上，按照本专栏 <a href="https://zhuanlan.zhihu.com/p/607970423">尚春：基于 Context 做 React 状态管理</a> 最后关于状态管理工具分类的表格，考虑到它的原子性和通过<code>derived</code>数据类型建立的依赖关系管理，应该类似右下角的 observables。</p><p>另一点需要指出的是，为了让 Svelte 编译器识别组件中引用的 store 数据，在模板中需要使用<code>$</code>作为 store 数据的前缀，这样的话，Svelte 就会在组件创建的<code>instance</code>函数中添加相应的订阅 store 数据更新的逻辑。</p><p>Svelte 提供了一个 <a href="https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-%24-to-access-their-values-store-contract">store contract</a> 以便社区贡献更多的 store 实现，目前已经有一些诸如 <a href="https://github.com/joshnuss/svelte-persisted-store">svelte-persisted-store</a>、<a href="https://github.com/statelyai/xstate">XState</a> 等状态管理工具。</p><h3 id="UI-库"><a href="#UI-库" class="headerlink" title="UI 库"></a>UI 库</h3><p>如果一个前端框架没有丰富的 UI 库，那么它就很难被更多人使用。对于 Svelte 来说，目前已经有足够多的选择：<img src="../images/Svelte-09.jpg"></p><p><em>完整列表见 <a href="https://sveltesociety.dev/packages?tag=design-system">https://sveltesociety.dev/packages?tag=design-system</a>。</em></p><h3 id="SvelteKit"><a href="#SvelteKit" class="headerlink" title="SvelteKit"></a>SvelteKit</h3><p>类似 Next 之于 React、Nust 之于 Vue，<a href="https://kit.svelte.dev/">SvelteKit</a> 是 Svelte 应用开发的一套集成开发方案，提供了开箱即用的 SSR、路由、数据请求、热更新等一系列功能，方便开发者迅速上手。</p><p>借用一张 <a href="https://joyofcode.xyz/learn-how-sveltekit-works#sveltekit-uses-the-web-platform">Learn How SvelteKit Works</a> 文中的图片，SvelteKit 采用的主要工具如下：<img src="../images/Svelte-10.jpg"></p><p>推荐刚入门的朋友优先考虑使用 SvelteKit 进行应用开发，有了 Vite 等工具的支持，开发体验非常顺滑。构建出的应用也做了性能、预渲染、SEO 等方面的优化，加上本就体积小巧的 Svelte 产物，用户体验也有一定保障。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>在 React、Angular 和 Vue 等主流框架之后，前端似乎陷入了一个发展瓶颈，很难再有引人瞩目的新概念，直到出现了像 Svelte 和 Solid 这样的编译型框架。它们不再依赖 Virtual DOM，而是直击本质，力求为开发者提供声明式、简化认知复杂度的开发体验，同时在浏览器中实现高效执行。</p><p>通过本文的分析，相信大家对Svelte也有了初步的了解，能够认识到相较于传统框架，这些新技术在性能上有着明显的优势，使得前端开发能够更加高效地交付产品。正是这种技术的革新，推动着前端开发领域不断前行。与此同时，它们也引发了开发者对编译过程和底层实现的更深层次思考。这种探索精神正是推动技术进步的动力，我们期待看到更多创新技术的出现，为前端开发带来新的可能性。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://www.youtube.com/watch?v=AdNJ3fydeao">Rich Harris - Rethinking reactivity</a></li><li><a href="https://zhuanlan.zhihu.com/p/350507037">新兴前端框架 Svelte 从入门到原理</a></li><li><a href="https://zhuanlan.zhihu.com/p/448469958">Elab：Svelte 原理浅析与评测</a></li><li><a href="https://svelte.dev/blog/virtual-dom-is-pure-overhead">Virtual DOM is pure overhead</a></li><li><a href="https://svelte.dev/blog/frameworks-without-the-framework">Frameworks without the framework: why didn’t we think of this sooner?</a></li><li><a href="https://vuejs.org/guide/extras/rendering-mechanism">Rendering Mechanism - VueJS</a></li><li><a href="https://dev.to/methodcoder/svelte-crash-course-with-pics-27cc">Svelte Crash Course (in 10 pics!)</a></li><li><a href="https://lihautan.com/the-svelte-compiler-handbook/">The Svelte Compiler Handbook</a></li><li><a href="https://dev.to/this-is-learning/a-look-at-compilation-in-javascript-frameworks-3caj">A Look at Compilation in JavaScript Frameworks</a></li><li><a href="https://www.youtube.com/watch?v=FNmvcswdjV8">Compile Svelte in Your Head</a></li><li><a href="https://www.youtube.com/watch?v=mwvyKGw2CzU">Build your own Svelte</a></li><li><a href="https://joyofcode.xyz/svelte-state-management">Svelte State Management Guide</a></li><li><a href="https://joyofcode.xyz/learn-how-sveltekit-works">Learn How SvelteKit Works</a></li><li><a href="https://blog.rexskz.info/enlightenment-from-svelte-3-compiler.html">从 Svelte 3 的编译器中得到的启示</a></li></ol><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>Rich 在 2021 年底加入了 Vercel，让这个团队的阵容更加豪华。随后他参加了一个<a href="https://vercel.com/blog/the-future-of-svelte-an-interview-with-rich-harris">关于 Svelte 未来想法的访谈</a>，里面介绍了不少”背后的故事“以及将来的畅想。<img src="../images/Svelte-11.jpg"></p><p>作为前端轮子哥，Rich 谈及了一个 2013 年他做的第一个开源项目 <a href="https://github.com/ractivejs/ractive">Reactive</a>。那时 React 还没有宣布，等他在 Hacker News 上看到 React 开源的消息的时候，觉得非常惋惜。不过后来他还是将其开源并坚持了很长时间去维护这个项目。这些点点滴滴为之后 Rollup、Svelte 的发展积累了丰富经验。</p><p>谈及加入 Vercel 最大的变化，Rich 坦言：</p><blockquote><p>They’re worried a project without any real financial backing is just going to disappear. Some of those people are now starting to turn around and say, <em>Well okay, it has a full-time engineer. Vercel is clearly excited about Svelte and is investing real resources into Svelte.</em><br>有些人担心一个没有任何真正财务支持的项目会消失。现在，其中一些人开始转变态度，他们说<em>，好吧，它有一个全职工程师。Vercel 明显对 Svelte 感到兴奋，并正在对 Svelte 进行实质性的投资。</em></p></blockquote><p>我相信这是真心话，也是 Rich 加入 Vercel 的最大理由之一（另一个可能是 Vercel 真给钱啊）。</p><p>还有一个有意思的问题：让 Rich 向 CTO 或者技术高管兜售 Svelte 和 SvelteKit，他的回复是：</p><blockquote><p>It kind of doesn’t matter what you’re building. Your competitive advantage is essentially limited by how fast you can ship stuff, and SvelteKit will let you ship stuff faster. That is what it boils down to.<br>不管你在开发什么，你的竞争优势基本上取决于你能够多快地上线产品，而 SvelteKit 能让你更快地完成。这就是核心所在。<br>I believe moving from React to Svelte will allow you to write stuff faster. We have a whole thesis about how Svelte is a UI language that is designed explicitly for solving this problem and it allows you to write things in a more idiomatic way.<br>我相信从 React 转向 Svelte 会让你更快地编写代码。我们有一个完整的论点，认为 Svelte 是一种专门设计用来解决这个问题的<strong>用户界面语言</strong>，它可以让你以更符合惯例的方式编写代码。</p></blockquote><p>让我们祝福他和 Vercel 团队吧！</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;2016 年，Rollup 等工具的作者 Rich Harris 推出了又一力作：Svelte。这一新兴框架独辟蹊径，旨在解决 React 等框架的一系列问题。这几年 Svelte 虽然没有大红大紫，但从 &lt;a href=&quot;https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/&quot;&gt;State of JS&lt;/a&gt;  统计来看，它一直保持着持久的热度：
&lt;img src=&quot;../images/Svelte-01.jpg&quot;&gt;
&lt;em&gt;使用度紧跟三大框架&lt;/em&gt;
&lt;img src=&quot;../images/Svelte-02.jpg&quot;&gt;
&lt;em&gt;兴趣度蝉联榜首多年&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;本文尝试由浅入深的介绍 Svelte，希望更多人在适合的场景下可以考虑尝试它。&lt;/p&gt;
&lt;h2 id=&quot;缘起&quot;&gt;&lt;a href=&quot;#缘起&quot; class=&quot;headerlink&quot; title=&quot;缘起&quot;&gt;&lt;/a&gt;缘起&lt;/h2&gt;&lt;p&gt;Rich 非常喜欢 React 等前端库和框架带来的更加声明式、低心智负担的开发方式，这让前端工程师可以更好地控制复杂度、提升开发效率和质量。然而他也敏锐地洞察到了一些不那么尽如人意之处。&lt;/p&gt;
&lt;h3 id=&quot;Virtual-DOM-真的快吗？&quot;&gt;&lt;a href=&quot;#Virtual-DOM-真的快吗？&quot; class=&quot;headerlink&quot; title=&quot;Virtual DOM 真的快吗？&quot;&gt;&lt;/a&gt;Virtual DOM 真的快吗？&lt;/h3&gt;&lt;p&gt;React、Vue 都引入了 Virtual DOM 的概念，从而能够进行现有 DOM 以及期望 DOM 的对比，然后再执行相对较重的 DOM 操作使得两者一致。&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://svelte.dev/blog/virtual-dom-is-pure-overhead&quot;&gt;Virtual DOM is pure overhead&lt;/a&gt; 一文中， Rich 指出 Virtual DOM 其实更多的是一种实现声明式、状态驱动 UI 开发的一种手段，并非不可或缺。它主要的问题在于有很多没有必要的 Diff。&lt;/p&gt;
&lt;p&gt;举个例子，假设我们有如下一个 React App：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; [count, setCount] = useState(&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; handleClick = &lt;span class=&quot;function&quot;&gt;() =&amp;gt;&lt;/span&gt; setCount(count + &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; (&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;attr&quot;&gt;className&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;quot;app&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;       &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;p&lt;/span&gt;&amp;gt;&lt;/span&gt;Count: &amp;#123;count&amp;#125;&lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;p&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;       &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;#123;handleClick&amp;#125;&lt;/span&gt;&amp;gt;&lt;/span&gt;+1&lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;    &lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  );&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;如果点击一次按钮，新旧 Virtual DOM 对比如下：
&lt;img src=&quot;../images/Svelte-03.jpg&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="Svelte" scheme="https://springuper.github.io/tags/Svelte/"/>
    
    <category term="Framework" scheme="https://springuper.github.io/tags/Framework/"/>
    
    <category term="Compiler" scheme="https://springuper.github.io/tags/Compiler/"/>
    
  </entry>
  
  <entry>
    <title>带着“答案”去提问</title>
    <link href="https://springuper.github.io/ask-with-answers/"/>
    <id>https://springuper.github.io/ask-with-answers/</id>
    <published>2023-05-30T21:50:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>前几天，一位美国团队的负责人跟我谈到了他团队里的两位工程师。其中一个工程师每周和他一对一沟通的时候都提出了好几个技术性的问题，诸如：</p><ul><li>调试过程中发现本地环境遇到一个错误跑不起来</li><li>在测试环境下面各种参数如何调节</li><li>压力测试中发现的一些新的不及预期的现象</li></ul><p>如此等等。总结下来，他感觉这位高级工程师更像是一个 Problem Maker，在不断地需要他介入到特别细枝末节的地方，给出具体的意见后才能进一步开展工作。</p><p>相比之下，另一位工程师则会在一对一沟通中除了汇报主要进展外，也会提出一两个接下来需要重点关注的问题。不同的是，那位工程师除了抛出问题外，还会提出几个他考虑到的可行性方案，并虚心跟他讨教。这位负责人笑着跟我说这样的团队成员才是他心目中称职的“高级”工程师，是典型的 Problem Solver。</p><p>这个案例彰显出“带着答案去提问”的重要价值，尤其在远程协作或者大型组织等沟通渠道弥足珍贵的环境中，这种能力更值得重视和培养。接下来我们从几个方面阐释其中的内涵。</p><span id="more"></span><h2 id="答案让问题变得更完整"><a href="#答案让问题变得更完整" class="headerlink" title="答案让问题变得更完整"></a>答案让问题变得更完整</h2><p>在提问问题时，提供可能的答案可以帮助澄清问题的意图，并给出更为清晰的上下文。例如，假设你想问一个关于旅行目的地的问题：“我应该去哪里旅行？”如果你只问这个问题而没有提供任何上下文，对方可能会很难给出有用的建议。但如果你提供了一些可能的答案，如：“我正在考虑去巴黎、东京或者悉尼旅行”，那么回答者就可以更好地理解你的兴趣和偏好，并给出更具针对性的建议。</p><p>在技术性问题中也是类似的，如果只是说“我们应不应该在这个项目中使用 React Query？”那么对方并不知道你是建议使用这个工具，还是看到一些现有方案的问题所以在考虑一些业界流行的方案，还是有什么其他意图。但如果换成“我们应不应该在这个项目中使用 React Query？我发现这个内部系统里其实主要的客户端状态都和服务器数据交互相关，这种场景 Redux 显得臃肿了，看起来 React Query 是一个更好的选择，另外一个我了解到的方案是比较轻量级的 SWR”，估计对方就会很快领会你的问题是觉得 React Query 以及 SWR 这类新兴工具可以更好地处理服务器数据交互类场景。</p><h2 id="抛砖引玉提升沟通效率"><a href="#抛砖引玉提升沟通效率" class="headerlink" title="抛砖引玉提升沟通效率"></a>抛砖引玉提升沟通效率</h2><p>在提问时准备的“答案”并不需要是最优的，它们更像是大海中的一座座灯塔，指明大致的方向即可，可以为对方提供一些参考，避免回答重复或不相关的信息，并促进进一步的讨论。</p><p>举一个简单的例子：小张在开发一个新功能的时候，发现需要用到的一个工具函数已经有多个版本的实现了，它们有些略微差异但是本质上可以通过一个通用方法解决。然后他去组里跟大家说：“我们代码仓库 xxx、yyy、zzz 这几个方法重复了，我们能不能重构下？”</p><p>在另外一个平行时空，面对同样的问题，小李做了一些简单的分析后，是这样跟大家沟通的：“最近我在开发一个新项目的时候，发现已经有 xxx、yyy、zzz 都用来处理数据分组问题了。我简单看了下它们各自的实现，其实是可以通过传递一些参数来整合为一个方法，重构完能减少上百行代码。或者也可以考虑直接使用一个流行的开源工具库里的 groupBy 方法。我目前想到这两个办法，权当抛砖引玉，大家一起看看怎么解决比较好呢？”</p><p>对比两种方式，明显第二种信息量要多出很多。相信如果你是他们团队中的一员，看到小张的问题你会觉得这几个方法可能有问题，但是还得实际看一看。但看到小李的描述，你应该大概已经知道这几个方法大概率是不同人不同时期添加的，属于技术债务。而且看起来引入一个比较标准的工具库会是一个更好的方案。当然，进一步可以去分析为什么这些工具函数会出现如此多的重复，是不是因为缺乏文档、缺乏规范等等。</p><p>除了提供更为丰富的信息，与问题相互印证之外，这些解决方案也提供了借鉴意义，让大家的讨论不再是天马行空，而是有了基本的“锚”，话题会围绕这几个方案的优缺点逐步展开，甚至往往能够引申出更加合理的新方案。这里其实是有一点心理学的概念的，做选择往往比给答案更容易，而且更有启发性。在《程序员修炼之道》中有一个“石头汤”的故事，本质也是类似的。</p><p>抛砖引玉，有的放矢，最终达成的效果是大家可以更有效地沟通，进而更快地达成共识，促进产出。</p><h2 id="主动思考带来更多成长"><a href="#主动思考带来更多成长" class="headerlink" title="主动思考带来更多成长"></a>主动思考带来更多成长</h2><p>发现问题是第一步，能够主动思考并提出有效方案是更高能力的体现，是第二步。相比较而言，这种意识的培养比技术的提升更容易，但效果却出乎意料地好，可谓极具性价比。初期的“答案”可能有些初级，但你的沟通对象至少会认为你非常负责任，也对他们足够尊重，做了一些功课才来请教他们。随着这种思维模式的贯彻和深入，你会找到更多有效的办法去寻找更优的方案，例如先与团队里的领域专家沟通、使用 ChatGPT 咨询建议、进行 PoC 进行简单验证等等。日积月累，你可能就会进入第三步：给出各个方案的优缺点，并陈述你的推荐方案以及理由，这足以表明你已经具备了一定的判断力和决策能力。上级和同事们看到你积极参与和贡献解决问题的能力，会加强对你的信任和认可，从而获得更大的发展空间。</p><p>换个角度来看，实际上带着答案去提问本质上是站在对方的角度去考虑问题，甚至是站在团队负责人或上级的角度去综合评估最优解。这种格局带来的提升动力非常强劲，对个人能力的全方面提升相当重要。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>在遇到一个新问题时，直接将问题抛出去既不专业也不负责。其他人可能会感到一头雾水，因为缺乏一些关键信息云里雾里。如果其他人发现这个问题其实行业内或团队内之前曾有过经验，或者认为提问者完全有足够能力搞定，也会降低对提问者的信任度。相反，如果每次遇到一个新问题都能够主动思考，准备一些初步的思路甚至解决方案，往往会给对方如遇春风的感觉，有助于快速达成共识。更重要的是，这种思维方式能够提升一个人的格局，为后续的发展进步奠定更为坚实的基础。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://36kr.com/p/1953327724501128">在职场，有问题该怎么提问？</a></li><li><a href="https://www.woshipm.com/zhichang/4239783.html">这样提问，你不可能得到想要的答案！</a></li><li><a href="https://www.niaogebiji.com/article-33854-1.html">有效提问 - 鸟哥笔记</a></li><li><a href="http://cripac.ia.ac.cn/people/wwang/post/how-to-ask-questions-the-smart-way/">提问的智慧</a></li><li><a href="https://wenku.baidu.com/view/a8de92b352e79b89680203d8ce2f0066f5336483.html">带着答案问问题</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;前几天，一位美国团队的负责人跟我谈到了他团队里的两位工程师。其中一个工程师每周和他一对一沟通的时候都提出了好几个技术性的问题，诸如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调试过程中发现本地环境遇到一个错误跑不起来&lt;/li&gt;
&lt;li&gt;在测试环境下面各种参数如何调节&lt;/li&gt;
&lt;li&gt;压力测试中发现的一些新的不及预期的现象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如此等等。总结下来，他感觉这位高级工程师更像是一个 Problem Maker，在不断地需要他介入到特别细枝末节的地方，给出具体的意见后才能进一步开展工作。&lt;/p&gt;
&lt;p&gt;相比之下，另一位工程师则会在一对一沟通中除了汇报主要进展外，也会提出一两个接下来需要重点关注的问题。不同的是，那位工程师除了抛出问题外，还会提出几个他考虑到的可行性方案，并虚心跟他讨教。这位负责人笑着跟我说这样的团队成员才是他心目中称职的“高级”工程师，是典型的 Problem Solver。&lt;/p&gt;
&lt;p&gt;这个案例彰显出“带着答案去提问”的重要价值，尤其在远程协作或者大型组织等沟通渠道弥足珍贵的环境中，这种能力更值得重视和培养。接下来我们从几个方面阐释其中的内涵。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Communication" scheme="https://springuper.github.io/tags/Communication/"/>
    
    <category term="Asking" scheme="https://springuper.github.io/tags/Asking/"/>
    
  </entry>
  
  <entry>
    <title>基于 Context 做 React 状态管理</title>
    <link href="https://springuper.github.io/react-context/"/>
    <id>https://springuper.github.io/react-context/</id>
    <published>2023-03-09T09:12:00.000Z</published>
    <updated>2026-10-03T09:20:43.753Z</updated>
    
    <content type="html"><![CDATA[<p>React Context 在一些弱交互的 Web 应用中被很多人用来作为全局状态管理的利器，理由非常简单：开箱即用，无需额外引入第三方工具，而且毕竟是“亲儿子”，对将来的 concurrent mode 等新特性支持良好。然而在略微复杂些的场景继续使用 context 有时会面临一定的性能问题，目前官方和社区都在探索可能的解决方案。期望通过本文的介绍，能让大家在这方面有一定概要性的理解。</p><h2 id="痛点"><a href="#痛点" class="headerlink" title="痛点"></a>痛点</h2><p>官方文档中有这样一句话：</p><blockquote><p>Context lets you “broadcast” such data, and changes to it, to all components below.</p></blockquote><p>简单来说，一个 context 中的数据发生变化后，所有下层使用这一 context 的组件都会被更新。当 context 数据越来越多的时候，就会出现一些根本不必要的重复更新。</p><p>让我们用一个例子来简单阐释下。假设我们有一个这样的应用：</p><ul><li>有一个名字的输入框以及一个姓氏的输入框，使用两个组件实现</li><li>在另一个组件中展示欢迎信息，需要用到上面输入的名字和姓氏</li></ul><p>界面效果如下：<img src="../images/react-context-01.jpg"></p><p>因为涉及到跨组件数据调用，我们使用 context 进行名字和姓氏数据的管理。简要代码如下：</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="comment">// Context &amp; Components</span></span><br><span class="line"><span class="keyword">const</span> PersonContext = createContext();</span><br><span class="line"><span class="keyword">const</span> PersonFirstName = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, dispatch] = useContext(PersonContext);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      First Name:</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">        <span class="attr">value</span>=<span class="string">&#123;state.firstName&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">        <span class="attr">onChange</span>=<span class="string">&#123;event</span> =&gt;</span> dispatch(&#123; type: &quot;setFirstName&quot;, firstName: event.target.value &#125;)&#125;</span></span><br><span class="line"><span class="xml">      /&gt;</span></span><br><span class="line"><span class="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 class="keyword">const</span> PersonFamilyName = <span class="function">() =&gt;</span> &#123; <span class="comment">/* similar to PersonFirstName */</span> &#125;;</span><br><span class="line"><span class="keyword">const</span> Greeting = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [state] = useContext(PersonContext);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">h2</span>&gt;</span>Hi, &#123;state.firstName&#125; &#123;state.familyName&#125;!<span class="tag">&lt;/<span class="name">h2</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">// Main App</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">PersonContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;useReducer(reducer,</span> <span class="attr">initialState</span>)&#125;&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Greeting</span> /&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">PersonFirstName</span> /&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">PersonFamilyName</span> /&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">PersonContext.Provider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><span id="more"></span><p><em>完整代码见 <a href="https://codesandbox.io/s/naive-context-2gw55c?file=/src/App.js">Codesandbox</a>。</em></p><p>在该应用中，通过使用<code>useContext</code>， <code>Greeting、PersonFirstName</code>、<code>PersonFamilyName</code>三个组件都订阅了<code>PersonContext</code>的更新：<img src="../images/react-context-02.jpg"></p><p>一旦<code>PersonContext</code>的数据有变化，譬如在名字输入框中输入了新的字符，那么不止<code>Greeting</code>和 <code>PersonFirstName</code>组件会被重新渲染，<code>PersonFamilyName</code>组件也会被重新渲染，尽管它并没有任何内容需要变更。<img src="../images/react-context-03.gif"></p><p>从上图中的时间戳变化可以注意到，输入 first name 时 family name 所在组件也有更新。</p><p>这种<strong>广播机制</strong>导致 context 无法应用在复杂的状态管理场景。目前官方推荐将其用在主题、国际化等变更稀疏的少数场景：</p><blockquote><p>Common examples where using context might be simpler than the alternatives include managing the current locale, theme, or a data cache.</p></blockquote><h2 id="改进一：Split-Contexts"><a href="#改进一：Split-Contexts" class="headerlink" title="改进一：Split Contexts"></a>改进一：Split Contexts</h2><p>一个直观的改进是：既然一个不行，那能不能再加一个？在上面的例子中，我们将<code>PersonContext</code>拆分为<code>PersonFirstNameContext</code>和<code>PersonFamilyNameContext</code>两个 contexts，新的订阅关系如图：<img src="../images/react-context-04.jpg"></p><p>可以看出，<code>PersonFirstNameContext</code> 变更时仅需要更新<code>PersonFirstName</code>与<code>Greeting</code>组件，而不影响<code>PersonFamilyName</code>。</p><p>代码改动较多，但思路非常简单，把 reducer 等都拆分为两份，核心部分如下：</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">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123; </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;App&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">PersonFirstNameContextWrapper</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">PersonFamilyNameContextWrapper</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">Greeting</span> /&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">PersonFirstName</span> /&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">PersonFamilyName</span> /&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;/<span class="name">PersonFamilyNameContextWrapper</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">PersonFirstNameContextWrapper</span>&gt;</span></span></span><br><span class="line"><span class="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><em>完整代码见 <a href="https://codesandbox.io/s/naive-context-splitted-qfzio0?file=/src/App.js">Codesandbox</a>。</em></p><p>改进后的效果如下图所示：<img src="../images/react-context-05.gif"></p><p>容易观察到，在输入 first name 时，family name 所在组件并没有更新，反之亦然。因此我们可以确定将耦合性强的全局状态分拆到独立 context 的办法行之有效。</p><p>然而，维护多个 context 的成本显而易见，如何将状态进行合理分拆也注定会有一些争议。因此这种方案仅适用于全局状态有限的少数场景中。</p><h2 id="改进二：useMemo"><a href="#改进二：useMemo" class="headerlink" title="改进二：useMemo"></a>改进二：useMemo</h2><p>另外一个思路是利用 React 提供的<code>useMemo</code>hook 来避免无意义的重复渲染。以 <code>PersonFirstName</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> PersonFirstName = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [&#123; firstName &#125;, dispatch] = useContext(PersonContext);</span><br><span class="line">  <span class="keyword">return</span> useMemo(</span><br><span class="line">    <span class="function">() =&gt;</span> (</span><br><span class="line">      <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;container&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        First Name:</span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">          <span class="attr">value</span>=<span class="string">&#123;firstName&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">          <span class="attr">onChange</span>=<span class="string">&#123;(event)</span> =&gt;</span> dispatch(&#123; type: &quot;setFirstName&quot;, firstName: event.target.value &#125;)&#125;</span></span><br><span class="line"><span class="xml">        /&gt;</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">    ),</span><br><span class="line">    [firstName, dispatch]</span><br><span class="line">  );</span><br><span class="line">&#125;; </span><br></pre></td></tr></table></figure><p>完整代码见 *<a href="https://codesandbox.io/s/context-usememo-no4f8x?file=/src/App.js">Codesandbox</a>*。</p><p>实际效果如下：<img src="../images/react-context-06.gif"></p><p>可以看出改进效果与分拆 context 的效果基本一致，可以避免重复渲染。更具体的讲，在姓氏输入框中输入新的字符后，代码执行过程大致为：</p><ol><li>在<code>PersonFamilyName</code>事件回调中执行<code>dispatch</code></li><li><code>PersonContext</code>的<code>state</code>更新</li><li>React 根据之前的<code>useContext</code>调用定位到<code>PersonFirstName</code>需要重新渲染（暂时忽略<code>PersonFamilyName</code>与<code>Greeting</code>）</li><li>执行<code>PersonFirstName</code>函数组件，调用<code>useMemo</code>方法获得渲染内容</li><li><code>useMemo</code>发现<code>firstName</code>和<code>dispatch</code>均没有较前值发生变化，直接返回之前缓存的结果，从而避免了无意义的重新渲染</li></ol><p>如果对于<code>useMemo</code>等 hooks 不太了解，可以看下本栏之前的<a href="https://zhuanlan.zhihu.com/p/372790745">React Hooks 原理剖析</a>一文。</p><p>这种改进方式理论上是可行的，但在实践中，这一方案需要额外的心智负担和维护成本，一旦涉及到的地方比较多，就会变得异常脆弱。</p><h2 id="改进三：useContextSelector"><a href="#改进三：useContextSelector" class="headerlink" title="改进三：useContextSelector"></a>改进三：useContextSelector</h2><p>为了解决 context 这种广播机制带来的弊端，除了以上两种渐进改良方案以外，官方和社区也在积极探索更加彻底的方案，目的很明确：可以实现<strong>选择性更新</strong>。</p><h3 id="官方-RFC"><a href="#官方-RFC" class="headerlink" title="官方 RFC"></a><strong>官方 RFC</strong></h3><p>早在 18 年，React 团队还聚焦在 hooks 的时候就有这样一个 <a href="https://github.com/facebook/react/issues/14110">Issue: Provide more ways to bail out inside Hooks</a> 讨论 context 的可能改进，后来 React core team 的 Josh Story 写了一份 <a href="https://github.com/gnoff/rfcs/blob/context-selectors/text/0000-context-selectors.md">Context Selector RFC</a> 来具体阐释 context 选择更新的设计和实现方案，下面是 RFC 文档中列举的一个示例：</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="keyword">let</span> Context = React.createContext(‘’)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> App = <span class="function">(<span class="params">&#123; index, string &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">Context.Provider</span> <span class="attr">value</span>=<span class="string">&#123;string&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Foo</span> <span class="attr">index</span>=<span class="string">&#123;index&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">Context.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">let</span> Foo = React.memo(<span class="function">(<span class="params">&#123; index &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">let</span> selector = React.useCallback(<span class="function"><span class="params">s</span> =&gt;</span> s.substring(<span class="number">0</span>, index), [index])</span><br><span class="line">  <span class="keyword">let</span> selection = React.useContextSelector(Context, selector)</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">span</span>&gt;</span>&#123;selection&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Foo renders (mount) and selector is called during Foo’s render: “abcd”</span></span><br><span class="line">ReactRenderer.render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> <span class="attr">index</span>=<span class="string">&#123;4&#125;</span> <span class="attr">string</span>=<span class="string">”abcdefg”</span> /&gt;</span></span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Foo renders (props update) and selector is called during Foo’s render: “abcde”</span></span><br><span class="line">ReactRenderer.render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> <span class="attr">index</span>=<span class="string">&#123;5&#125;</span> <span class="attr">string</span>=<span class="string">”abcdefg”</span> /&gt;</span></span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Foo does not render (memo props same), selector is called before Foo bails out (selection same): “abcde”</span></span><br><span class="line">ReactRenderer.render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> <span class="attr">index</span>=<span class="string">&#123;5&#125;</span> <span class="attr">string</span>=<span class="string">”abcdef*”</span> /&gt;</span></span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Foo renders (selection update), selector is called before Foo’s render and the result is memoized and returned again during that render: “a*cde”</span></span><br><span class="line">ReactRenderer.render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> <span class="attr">index</span>=<span class="string">&#123;5&#125;</span> <span class="attr">string</span>=<span class="string">”a*cdef*”</span> /&gt;</span></span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Foo renders (props update), selector is only called during Foo’s render even though the context value also changed: “a**d”</span></span><br><span class="line">ReactRenderer.render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> <span class="attr">index</span>=<span class="string">&#123;4&#125;</span> <span class="attr">string</span>=<span class="string">”a**def*”</span> /&gt;</span></span>)</span><br></pre></td></tr></table></figure><p>RFC 的核心内容在于新提议的<code>useContextSelector</code>hook，类型为<code>(ReactContext&lt;T&gt;, T =&gt; S) =&gt; S</code>。它在<code>useContext</code>基础上支持第二个参数<code>selector</code>，从而支持仅依赖部分 context 数据的场景，并能做到只有这部分数据更新的时候才触发 hook 宿主组件的更新。 第二个参数省略时，其行为退化为与<code>useContext</code>一致。</p><p>目前这个 RFC 还处于 open 状态，但是目前 React 团队还集中在一些 React 18 相关的功能开发上，这个功能还不在他们的确切日程上：</p><blockquote><p>We ran an experiment of the lazy propagation mechanism that showed mildly positive performance results, but we haven’t run an experiment for context selectors yet.<br>We’re not really blocked by anything, it just hasn’t been a priority while we work on other 18-related projects.<br><a href="https://github.com/reactwg/react-18/discussions/73#discussioncomment-1201850">Context selector status and plans? · Discussion #73 · reactwg/react-18</a></p></blockquote><h3 id="社区版本"><a href="#社区版本" class="headerlink" title="社区版本"></a><strong>社区版本</strong></h3><p>虽然官方进度缓慢，但好在 React 社区非常活跃，有一些开发者通过各种黑科技实现了类似官方提议的<code>useContextSelector</code> hook，其中主流的版本为在 React 状态管理工具方面颇有建树的 <a href="https://github.com/dai-shi">Daishi Kato</a> 开发的 <a href="https://github.com/dai-shi/use-context-selector">use-context-selector</a>。</p><p>使用 use-context-selector 修改之前的基础示例，核心的变化如下：</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="keyword">const</span> PersonFirstName = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> firstName = useContextSelector(</span><br><span class="line">    PersonContext,</span><br><span class="line">    <span class="function"><span class="params">value</span> =&gt;</span> value[<span class="number">0</span>].firstName</span><br><span class="line">  );</span><br><span class="line">  <span class="keyword">const</span> dispatch = useContextSelector(PersonContext, <span class="function"><span class="params">value</span> =&gt;</span> value[<span class="number">1</span>]);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;container&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      First Name:</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">        <span class="attr">value</span>=<span class="string">&#123;firstName&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">        <span class="attr">onChange</span>=<span class="string">&#123;event</span> =&gt;</span> dispatch(&#123; type: &quot;setFirstName&quot;, firstName: event.target.value &#125;)&#125;</span></span><br><span class="line"><span class="xml">      /&gt;</span></span><br><span class="line"><span class="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><em>完整代码见 <a href="https://codesandbox.io/s/use-context-selector-lyp88j?file=/src/App.js">Codesandbox</a>。</em></p><p>在上面的<code>PersonFirstName</code>组件中，我们有两处调用<code>useContextSelector</code>，分别获取了<code>PersonContext</code>中保存的<code>firstName</code>和<code>dispatch</code>，用于实现名字的输入框。</p><p>具体效果如下图所示：<img src="../images/react-context-07.gif"></p><p>容易观察到，在输入 first name 时，family name 所在组件并没有更新，反之亦然。这说明了 use-context-selector 在避免重复渲染方面非常有效，且相较之前的改进版本更容易理解和掌握。</p><h3 id="useContextSelector-实现原理"><a href="#useContextSelector-实现原理" class="headerlink" title="useContextSelector 实现原理"></a><strong>useContextSelector 实现原理</strong></h3><p>有些同学可能对<code>useContextSelector</code>的具体实现感兴趣，而且这也是一个不错的 React Hooks 活学活用的机会，那我们接下来就简要介绍一个极简版本的实现方案。</p><p>本质上讲，这其实是一个简单的发布/订阅模式，大体思路如下：</p><ol><li>实现一个自定义的 Provider</li><li>自定义 Provider 内部维护一个监听函数队列，并在数据发生变化时执行所有的监听函数</li><li>每个<code>useContextSelector</code>会在 Provider 中添加监听函数，当其在数据发生变化被调用时，使用 selector 和最新的 context 数据进行计算，并在且仅在计算结果发生变更时更新组件<img src="../images/react-context-08.jpg"></li></ol><p><strong>自定义 Provider</strong></p><p>自定义 Provider 的主要功能是保存当前 context 的数据，并维护一个监听函数队列，在数据发生变化时执行这些监听函数。简要实现如下：</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="function"><span class="keyword">function</span> <span class="title">createProvider</span>(<span class="params">ProviderOriginal</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function">(<span class="params">&#123; value, children &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> valueRef = useRef(value);</span><br><span class="line">    <span class="keyword">const</span> listenersRef = useRef(<span class="keyword">new</span> <span class="built_in">Set</span>());</span><br><span class="line">    <span class="keyword">const</span> contextValue = useRef(&#123;</span><br><span class="line">      <span class="attr">value</span>: valueRef,</span><br><span class="line">      <span class="attr">registerListener</span>: <span class="function">(<span class="params">listener</span>) =&gt;</span> &#123;</span><br><span class="line">        listenersRef.current.add(listener);</span><br><span class="line">        <span class="comment">// Return a callback to remove the listener</span></span><br><span class="line">        <span class="keyword">return</span> <span class="function">() =&gt;</span> listenersRef.current.delete(listener);</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// Whenever the value changes, we need</span></span><br><span class="line">      <span class="comment">// - change the valueRef so it stores the latest value</span></span><br><span class="line">      <span class="comment">// - notify all listeners of the new value</span></span><br><span class="line">      valueRef.current = value;</span><br><span class="line">      listenersRef.current.forEach(<span class="function">(<span class="params">listener</span>) =&gt;</span> &#123;</span><br><span class="line">        listener(value);</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;, [value]);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">      <span class="xml"><span class="tag">&lt;<span class="name">ProviderOriginal</span> <span class="attr">value</span>=<span class="string">&#123;contextValue.current&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        &#123;children&#125;</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">ProviderOriginal</span>&gt;</span></span></span><br><span class="line">    );</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该实现有如下要点：</p><ul><li>基于原生 Provider，从而可以在后续的 <code>useContextSelector</code>实现中通过原生<code>useContext</code>获取 context 数据</li><li>将传递给原 Provider 的<code>value</code>进行包装，加入<code>registerListener</code>方法，方便后续在<code>useContextSelector</code>中添加监听函数</li><li>为了避免<code>value</code>发生变更时引起所有<code>useContext</code>所在宿主组件重新渲染，使用<code>useRef</code>来保存该值，完全依赖调用监听函数来通知相关组件</li></ul><p><strong>创建 Context</strong></p><p>基于自定义 Provider，我们下面实现自定义的 context 创建方法：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createContext <span class="keyword">as</span> createContextOriginal &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createContext</span>(<span class="params">defaultValue</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> context = createContextOriginal();</span><br><span class="line">  context.Provider = createProvider(context.Provider);</span><br><span class="line">  <span class="keyword">return</span> context;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>逻辑非常简单，使用原生<code>createContext</code>方法创建 context 后，将其<code>Provider</code>修改为前面的自定义版本。</p><p><strong>实现 useContextSelector Hook</strong></p><p>该 hook 的实现思路为：</p><ol><li>读取 context 中的<code>registerListener</code>和<code>value</code></li><li>调用<code>registerListener</code>，添加监听函数</li><li>监听函数主要的逻辑为：使用最新 context 数据执行 selector，如果计算结果有变化则触发组件更新</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useContext, useEffect, useState &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">useContextSelector</span>(<span class="params">context, selector</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; value, registerListener &#125; = useContext(context);</span><br><span class="line">  <span class="comment">// Pass a function to `useState` to make sure the `value.current` can be a function,</span></span><br><span class="line">  <span class="comment">// otherwise `useState` will run it immediately whenever it&#x27;s a function</span></span><br><span class="line">  <span class="keyword">const</span> [selectedValue, setSelectedValue] = useState(<span class="function">() =&gt;</span></span><br><span class="line">    selector(value.current);</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> registerListener(<span class="function">(<span class="params">newValue</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// React only re-renders if the new compuated state changes</span></span><br><span class="line">      setSelectedValue(<span class="function">() =&gt;</span> selector(newValue));</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;, [registerListener, value, selector]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> selectedValue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，这里我们应用<code>useState</code>来保存经过<code>selector</code>计算后的值，每次监听函数执行时，直接将新的 <code>selector</code>计算后的新值作为<code>setSelectedValue</code> 参数进行调用，接下来 React 会对比是否和之前的状态一致，如果有变化则触发更新。</p><p>至此，我们已经有一个可用版本的 useContextSelector hook 实现了。完整代码见 *<a href="https://codesandbox.io/s/use-context-selector-simple-version-7mkuud?file=/src/App.js">Codesandbox</a>*。</p><p>真实的实现方案复杂得多，use-context-selector 最新 1.4.1 版本的主文件有 300 余行，会考虑服务端渲染、版本控制、批量更新、concurrent renderring 等等。此外，尽管做了诸多努力，但目前仍然有一些诸如 stale props 的<a href="https://github.com/dai-shi/use-context-selector#limitations">兼容性问题</a>尚待解决。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Context 有着非常简单的概念和 API，是轻量应用管理全局状态的上佳选择。但囿于广播机制带来的性能问题尚未被大范围使用。本文的三个改进逐层深入，分拆多个 contexts、使用<code>useMemo</code>、新的<code>useContextSelector</code> hook 均能解决重复渲染问题。最后的<code>useContextSelector</code>无论是从概念还是用法方面都与<code>useContext</code>非常接近，毫无疑问是上上之选。我们非常期待官方的版本能够加速推出，这样不仅一般的小型应用可以变得更加轻量和简单，之前在 v6 版本中尝试使用 context 进行内部状态管理但遇到性能问题的 Redux 也可以重拾旧爱，更多的工具也可能会应运而生。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://reactjs.org/docs/context.html">Context - React documentation</a></li><li><a href="https://blog.axlight.com/posts/4-options-to-prevent-extra-rerenders-with-react-context/">4 options to prevent extra rerenders with React context</a></li><li><a href="https://www.youtube.com/watch?v=MpdFj8MEuJA">Mastering React Context: Do you NEED a state manager?</a></li><li><a href="https://dev.to/romaintrotard/use-context-selector-demystified-4f8e">use-context-selector demystified</a></li><li><a href="https://freecontent.manning.com/everything-you-need-to-know-about-react-context-in-2022-part1/">Everything You Need to Know about React Context in 2022</a></li><li><a href="https://www.youtube.com/watch?v=oPfSC5bQPR8">React 18 for External Store Libraries</a></li></ol><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p><a href="https://github.com/dai-shi">Daishi Kato</a> 除了开发出 use-context-selector 外，还有 Zustand、Jotai、Valtio、React Tracked 等状态管理工具，可谓是既十分专注又特别高产！<img src="../images/react-context-09.jpg"></p><p>等等，你可能好奇他为啥搞出这么多状态管理工具来？？？很多人都有这个问题，而且真的搞不清楚它们之间的差异，后来 Daishi 贴了这样一张表格回应了下：<img src="../images/react-context-10.jpg"></p><p>这些工具可以按照是否由 React 自身的 useContext/useState/useSelector 进行 store 数据管理分为 interal store 和 external store 两大类：</p><ul><li>use-context-selector、Jotai 都是基于 context 的方案，Jotai 的不同之处在于可以进行状态依赖关系的自动追踪。React Tracked 主要配合<code>useState</code>进行使用，通过 Proxy 实现了 state usage tracking 进而可以自动避免重复渲染</li><li>Zustand、Valtio 更加类似 Redux，在一个独立于 React 的地方进行集中式数据管理<img src="../images/react-context-11.jpg"></li></ul><p>从 NPM Trends 数据来看，目前这几个工具最流行的是 Zustand，其次是 Jotai，第三是 use-context-selector。希望 Daishi 再接再厉，为社区打造更多精品！</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;React Context 在一些弱交互的 Web 应用中被很多人用来作为全局状态管理的利器，理由非常简单：开箱即用，无需额外引入第三方工具，而且毕竟是“亲儿子”，对将来的 concurrent mode 等新特性支持良好。然而在略微复杂些的场景继续使用 context 有时会面临一定的性能问题，目前官方和社区都在探索可能的解决方案。期望通过本文的介绍，能让大家在这方面有一定概要性的理解。&lt;/p&gt;
&lt;h2 id=&quot;痛点&quot;&gt;&lt;a href=&quot;#痛点&quot; class=&quot;headerlink&quot; title=&quot;痛点&quot;&gt;&lt;/a&gt;痛点&lt;/h2&gt;&lt;p&gt;官方文档中有这样一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Context lets you “broadcast” such data, and changes to it, to all components below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;简单来说，一个 context 中的数据发生变化后，所有下层使用这一 context 的组件都会被更新。当 context 数据越来越多的时候，就会出现一些根本不必要的重复更新。&lt;/p&gt;
&lt;p&gt;让我们用一个例子来简单阐释下。假设我们有一个这样的应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有一个名字的输入框以及一个姓氏的输入框，使用两个组件实现&lt;/li&gt;
&lt;li&gt;在另一个组件中展示欢迎信息，需要用到上面输入的名字和姓氏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;界面效果如下：
&lt;img src=&quot;../images/react-context-01.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为涉及到跨组件数据调用，我们使用 context 进行名字和姓氏数据的管理。简要代码如下：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;19&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;20&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;21&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;22&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;23&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;24&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;25&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;26&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;27&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;28&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;29&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;30&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;31&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;32&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Context &amp;amp; Components&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; PersonContext = createContext();&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; PersonFirstName = &lt;span class=&quot;function&quot;&gt;() =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; [state, dispatch] = useContext(PersonContext);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; (&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      First Name:&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;input&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;tag&quot;&gt;&lt;span class=&quot;xml&quot;&gt;        &lt;span class=&quot;attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;#123;state.firstName&amp;#125;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;tag&quot;&gt;&lt;span class=&quot;xml&quot;&gt;        &lt;span class=&quot;attr&quot;&gt;onChange&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;#123;event&lt;/span&gt; =&amp;gt;&lt;/span&gt; dispatch(&amp;#123; type: &amp;quot;setFirstName&amp;quot;, firstName: event.target.value &amp;#125;)&amp;#125;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;    &lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  );&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; PersonFamilyName = &lt;span class=&quot;function&quot;&gt;() =&amp;gt;&lt;/span&gt; &amp;#123; &lt;span class=&quot;comment&quot;&gt;/* similar to PersonFirstName */&lt;/span&gt; &amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; Greeting = &lt;span class=&quot;function&quot;&gt;() =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; [state] = useContext(PersonContext);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; (&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;h2&lt;/span&gt;&amp;gt;&lt;/span&gt;Hi, &amp;#123;state.firstName&amp;#125; &amp;#123;state.familyName&amp;#125;!&lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;h2&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  );&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Main App&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; (&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;PersonContext.Provider&lt;/span&gt; &lt;span class=&quot;attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;#123;useReducer(reducer,&lt;/span&gt; &lt;span class=&quot;attr&quot;&gt;initialState&lt;/span&gt;)&amp;#125;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;Greeting&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;PersonFirstName&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;PersonFamilyName&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;    &lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;PersonContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  );&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="React" scheme="https://springuper.github.io/tags/React/"/>
    
    <category term="Context" scheme="https://springuper.github.io/tags/Context/"/>
    
    <category term="useContextSelector" scheme="https://springuper.github.io/tags/useContextSelector/"/>
    
  </entry>
  
  <entry>
    <title>管理的杠杆</title>
    <link href="https://springuper.github.io/management-lever/"/>
    <id>https://springuper.github.io/management-lever/</id>
    <published>2022-07-05T21:06:17.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>最近听到有朋友说我比较善于使用管理的杠杆，顿觉旁观者清。坦白来说，我自己虽然在日常实践中有意无意地应用类似的思想，却还未有明确的这方面的总结和提炼。当时就萌生了写一篇文章来整理的想法。如今终于起笔，文中所言不一定对各位管理者有实际裨益，不过也权当是兼听则明吧。</p><p>一位好的管理者，在我理解来看，应当是一个 Multiplier，能够将大家的优势有效地发挥出来，形成合力，进而完成整个组织不断进阶的目标。我一直相信群体的力量远胜于少数核心人物的智慧。为了更好地激发大家的主观能动性，用好管理的杠杆，下面介绍几点我自己的思考。</p><h2 id="注入使命"><a href="#注入使命" class="headerlink" title="注入使命"></a>注入使命</h2><blockquote><p>If you want to build a ship，don’t drum up people to collect wood and don’t assign them tasks and work, but rather long for the endless immensity of sea.如果你想造一艘船，先不要雇人收集木头，也不要给人分配任务，而是激发他们对海洋的渴望。</p></blockquote><p>当一个项目需要一群人来完成时，我们可以有不同的方式来进行项目的启动：</p><ol><li>只介绍这个项目要做的具体事项</li><li>介绍项目的优先目标以及实施方案</li><li>介绍项目的背景、优先目标以及和公司整体战略的联系，并鼓励大家献计献策，通过沟通协作逐步确定具体方案</li></ol><span id="more"></span><p>第一种方式在很多公司都非常常见，大家都是公司雇员，拿钱办事，既然上面老板们决定要做那就没啥可讨论的，干就完了。第二种方式相较而言，会跟大家分享项目想要解决的核心问题，这样一个好处是大家有了更明确的方向，这样就为调动大家积极性、激发大家审视和改良现有的实施方案提供了很大空间，对于有效完成项目目标有非常正面的意义。第三种方案在两个点上做的更进一步：一方面会讲清楚项目对于公司的关键作用，有助于大家理解这一项目在哪些层面对于公司的进一步发展至关重要。另一方面也提高了大家的参与感，具体方案的确定是通过沟通协作来完成的，这样的方案也往往更接地气，在效率和成本方面都能做到非常出色。</p><p>在平时的实践中，我非常有意识地在用第三种方式来为一个事情注入使命，并在前期沟通中不断和利益攸关方沟通以凝聚共识。</p><p>以确立新人培训流程这个项目为例：</p><ul><li>项目的核心价值是帮助新人快速融入团队并为后续发挥战斗力打好基础，具体包括了解各团队的人和事，以及公司发展过程、产品演变历史、团队文化等内容</li><li>跟相关老大们协调一致后，在和各个团队骨干讲师的沟通中，着重强调的是这件事对于新人和公司的重大价值：公司的发展最核心的动力在于人，而让源源不断的新人快速适应和融入团队，对于传承优秀的团队文化和打造一支精英团队至关重要</li><li>后续的流程推进中，能够明显感觉到各位讲师在培训上投入很多，包括内容准备、讲解技巧等等方面，他们也通过培训更多地了解和认识了很多新人</li></ul><p>类似的例子还有很多，大到公司的一些核心项目，小到一次外出团建，我们都会竭尽所能地将其背后的重要意义跟大家阐释出来。</p><p>我能明显感觉到具有使命感与否所产生的巨大差异，目标明确且有信念的人常常让你眼前一亮，他们不仅会主动找到更优的方案，而且在面对困难时也百折不挠，使命必达。这也让我深刻认识到另一个道理：<strong>注入使命才会催发主人翁精神</strong>，之后的事情就变得简单起来，不需要那么多的微观管理。</p><h2 id="盘活资源"><a href="#盘活资源" class="headerlink" title="盘活资源"></a>盘活资源</h2><p>南水北调、西气东输、精准扶贫等祖国大地上发生的重大项目都体现出了盘活资源的重要意义。和国家治理类似，管理杠杆的另一个重点是合理地利用好手里的资源，这里的资源包括人、项目、资金、活动等等，当然最重要的还是人。</p><p>盘活资源在我理解有以下几个点：</p><ul><li>首先要能够识别出能够在某方面能够有所作为的资源。就人而言，有人擅长社交，有人擅长分析，有人演讲特别有感染力，有人观察细致入微，这些都是他们的一些特质，将来可能能够为团队所用</li><li>推进一个项目时，尽量从“能不能不做 -&gt; 能不能以后做 -&gt; 能不能别人做 -&gt; 自己做”这一逻辑出发，优先选派有相对优势的同事而非自己亲力亲为，以此来秣马厉兵，培养和发展人才</li><li>鉴于项目初期可能同事之间还没那么熟悉，同时也可能有一些重要决策需要有人来背书，可以“扶上马，送一程”，然后只做一些咨询类的支持</li></ul><p>以我们一个内部分享会 Tubi Talent Time 的组织为例，最初团队比较小的时候，还主要是我来具体负责协调。后来团队成长到一定规模，原来的模式频频出现各种问题，包括轮值主持人找不到讲师、讲师内容准备不够充分、缺乏反馈渠道等等。为了更好地解决这些问题，我们就从各个团队选了一些热衷于分享的同事，组建了一个新的委员会专门负责分享会的运作。大家勠力同心，不仅帮忙收集了很多潜在分享内容，而且在很多方面将分享活动的组织进一步优化，目前已经取得了超出我预期的进展。</p><p>我有一个很深的体会是：<strong>你能调动的资源往往比你想象的要多</strong>。甚至不光所有下属，其实你的上级、平级和其他有合作的部门都是非常重要的潜在资源。在一个非常有价值的项目的串联下，这些资源都是可以争取到的。</p><h2 id="分享利益"><a href="#分享利益" class="headerlink" title="分享利益"></a>分享利益</h2><p>“无双赢，不合作”是我的一个信条，如果你让一个人做一件事，而他却从中毫无所得，那人家为啥有动力去把这个事情做好呢？！财聚人散，财散人聚，这是一个很简单的道理。</p><p>因此想要最终实现管理的杠杆，让更多人有意愿发挥自己的比较优势，就应该想方设法让他们也能分到应得的利益。这里的利益并不一定是钱，也可以是荣誉、机会、人际关系等等。</p><p>在推进导师制的过程中，我对新晋导师进行培训的时候就开门见山介绍了他们可能的一些获益：</p><ul><li>增强知识储备。能够做好一件事和能够传授一件事是有本质区别的，后者才能让你对这件事的理解再上一个层次，吃深吃透</li><li>建立人际关系。我们招到的都是非常优秀的工程师，相对而言新人可能在经验方法上欠缺一些，但是都足够有智慧，相互的切磋和沟通，可以深化“亦师亦友”的良好关系</li><li>培养管理能力。很多人走上管理岗的第一步都是从做导师开始的，通过指导新人帮助他们快速成长，能够实践和掌握诸如预期管理、沟通、激励反馈等基本的管理技能</li></ul><p>后来事实上因为团队发展很快，很大一部分早期的导师都慢慢开始带更大规模的团队，获得收入、成长、人脉上的多丰收。</p><p>关于利益我的一个体会是：<strong>更多着眼在能力成长这些本源上，它们并非物质利益但会带来物质利益</strong>。一旦我们把合作关系庸俗化，弄成赤裸裸的金钱利益共同体，就可能会产生短视、腐败等问题。能力提升才是最根本的。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>教员曾经说过：所谓政治，就是把我们的人搞得多多的，把敌人搞得少少的。虽然管理并非政治，但有些思路是相通的。只有解放群众，发动群众，不断通过改造生产关系来释放和发展生产力，才能激发出团队的最大潜能，实现组织的可持续的成功。这其中，通过注入使命让大家有明确方向和一定灵活度，通过盘活资源让各类型要素流通协作，通过分享利益让大家真的有所得从而更有积极性和认同感，环环相扣，紧密联系，对于杠杆的有效发挥至关重要。管理方面我虽然不是新兵，但也还在摸索前进。如果你有更多的想法，欢迎约饭交流。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近听到有朋友说我比较善于使用管理的杠杆，顿觉旁观者清。坦白来说，我自己虽然在日常实践中有意无意地应用类似的思想，却还未有明确的这方面的总结和提炼。
当时就萌生了写一篇文章来整理的想法。如今终于起笔，文中所言不一定对各位管理者有实际裨益，不过也权当是兼听则明吧。&lt;/p&gt;
&lt;p&gt;一位好的管理者，在我理解来看，应当是一个 Multiplier，能够将大家的优势有效地发挥出来，形成合力，进而完成整个组织不断进阶的目标。
我一直相信群体的力量远胜于少数核心人物的智慧。为了更好地激发大家的主观能动性，用好管理的杠杆，下面介绍几点我自己的思考。&lt;/p&gt;
&lt;h2 id=&quot;注入使命&quot;&gt;&lt;a href=&quot;#注入使命&quot; class=&quot;headerlink&quot; title=&quot;注入使命&quot;&gt;&lt;/a&gt;注入使命&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;If you want to build a ship，don’t drum up people to collect wood and don’t assign them tasks and work, but rather long for the endless immensity of sea.
如果你想造一艘船，先不要雇人收集木头，也不要给人分配任务，而是激发他们对海洋的渴望。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当一个项目需要一群人来完成时，我们可以有不同的方式来进行项目的启动：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只介绍这个项目要做的具体事项&lt;/li&gt;
&lt;li&gt;介绍项目的优先目标以及实施方案&lt;/li&gt;
&lt;li&gt;介绍项目的背景、优先目标以及和公司整体战略的联系，并鼓励大家献计献策，通过沟通协作逐步确定具体方案&lt;/li&gt;
&lt;/ol&gt;</summary>
    
    
    
    
    <category term="Management" scheme="https://springuper.github.io/tags/Management/"/>
    
    <category term="Lever" scheme="https://springuper.github.io/tags/Lever/"/>
    
  </entry>
  
  <entry>
    <title>数据请求利器 React Query</title>
    <link href="https://springuper.github.io/react-query/"/>
    <id>https://springuper.github.io/react-query/</id>
    <published>2022-06-05T16:25:19.000Z</published>
    <updated>2026-10-03T09:20:43.753Z</updated>
    
    <content type="html"><![CDATA[<p>在前端项目的数据状态管理中，与服务端的交互数据往往占较大比例，尤其在诸如个人博客网站、内部管理系统这样的重数据交互、轻 UI 交互的场景。这类数据和客户端本地数据有很多差异：</p><ul><li>数据持久化存储在远程服务端，不受客户端控制</li><li>需要通过异步请求来获取和更新</li><li>有可能在 Web 应用中变得”过时”</li></ul><p>传统状态管理方案在应对这类场景时有些显得笨拙臃肿。React Hooks 推出后，逐渐有一些专注这一领域的工具出现并大放光彩。本文将着重介绍 React Query 这一代表性工具给出的简洁高效的解决方案。</p><h2 id="一个例子"><a href="#一个例子" class="headerlink" title="一个例子"></a>一个例子</h2><p>假设我们需要开发一个个人博客，以其中首页博客列表页为例，使用传统的 Redux 状态管理方案的话，需要先定义 actions、reducers 以及 store：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Redux actions, reducers and stores</span></span><br><span class="line"><span class="keyword">const</span> fetchPosts = createAsyncThunk(<span class="string">&quot;posts/fetchPosts&quot;</span>, <span class="function">() =&gt;</span></span><br><span class="line">  fetch(<span class="string">&quot;/fakeApi/posts&quot;</span>).then(<span class="function">(<span class="params">response</span>) =&gt;</span> response.json())</span><br><span class="line">);</span><br><span class="line"><span class="keyword">const</span> postsSlice = createSlice(&#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&quot;posts&quot;</span>,</span><br><span class="line">  <span class="attr">initialState</span>: &#123;</span><br><span class="line">    <span class="attr">posts</span>: [],</span><br><span class="line">    <span class="attr">status</span>: <span class="string">&quot;idle&quot;</span>,</span><br><span class="line">    <span class="attr">error</span>: <span class="literal">null</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function"><span class="title">extraReducers</span>(<span class="params">builder</span>)</span> &#123;</span><br><span class="line">    builder</span><br><span class="line">      .addCase(fetchPosts.pending, <span class="function">(<span class="params">state, action</span>) =&gt;</span> &#123;</span><br><span class="line">        state.status = <span class="string">&quot;loading&quot;</span>;</span><br><span class="line">      &#125;)</span><br><span class="line">      .addCase(fetchPosts.fulfilled, <span class="function">(<span class="params">state, action</span>) =&gt;</span> &#123;</span><br><span class="line">        state.status = <span class="string">&quot;succeeded&quot;</span>;</span><br><span class="line">        state.posts = action.payload;</span><br><span class="line">      &#125;)</span><br><span class="line">      .addCase(fetchPosts.rejected, <span class="function">(<span class="params">state, action</span>) =&gt;</span> &#123;</span><br><span class="line">        state.status = <span class="string">&quot;failed&quot;</span>;</span><br><span class="line">        state.error = action.error.message;</span><br><span class="line">      &#125;);</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br><span class="line"><span class="keyword">const</span> store = configureStore(&#123;</span><br><span class="line">  <span class="attr">reducer</span>: &#123;</span><br><span class="line">    <span class="attr">posts</span>: postsSlice.reducer,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><span id="more"></span><p>然后，定义 UI 组件并在其中调用 Redux 相关方法获取数据：</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="comment">// Components</span></span><br><span class="line"><span class="keyword">const</span> PostList = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> dispatch = useDispatch();</span><br><span class="line">  <span class="comment">// Retrieve data from Redux store state</span></span><br><span class="line">  <span class="keyword">const</span> &#123; status, error, posts &#125; = useSelector(<span class="function">(<span class="params">state</span>) =&gt;</span> state.posts);</span><br><span class="line"></span><br><span class="line">  useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// Start fetching data</span></span><br><span class="line">    <span class="keyword">if</span> (status === <span class="string">&quot;idle&quot;</span>) &#123;</span><br><span class="line">      dispatch(fetchPosts());</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [status, dispatch]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> content = <span class="literal">null</span>;</span><br><span class="line">  <span class="keyword">if</span> (status === <span class="string">&quot;loading&quot;</span>) &#123;</span><br><span class="line">    content = <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;loading&quot;</span>&gt;</span>Loading...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (status === <span class="string">&quot;succeeded&quot;</span>) &#123;</span><br><span class="line">    content = posts.map(<span class="function">(<span class="params">post</span>) =&gt;</span> (</span><br><span class="line">      <span class="xml"><span class="tag">&lt;<span class="name">article</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">h4</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">h4</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">span</span>&gt;</span>Author: &#123;post.author&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span>&#123;&quot; &quot;&#125;</span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">span</span>&gt;</span>Date: &#123;new Date(post.date).toLocaleDateString()&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">article</span>&gt;</span></span></span><br><span class="line">    ));</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (status === <span class="string">&quot;failed&quot;</span>) &#123;</span><br><span class="line">    content = <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;error&quot;</span>&gt;</span>&#123;error&#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">return</span> <span class="xml"><span class="tag">&lt;<span class="name">section</span> <span class="attr">className</span>=<span class="string">&quot;posts-list&quot;</span>&gt;</span>&#123;content&#125;<span class="tag">&lt;/<span class="name">section</span>&gt;</span></span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>可以看出，我们有一半多的代码是在对服务端数据进行操作。</p><p>那么，如果用 React Query 来实现的话会是什么样呢？</p><p>首先我们完全不需要定义 actions、reducers、store 等，只要修改下 UI 组件的如下部分即可：</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">// Components</span></span><br><span class="line"><span class="keyword">const</span> PostList = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// Retrieve data via React Query</span></span><br><span class="line">  <span class="keyword">const</span> &#123; status, error， data: posts &#125; = useQuery(<span class="string">&quot;posts&quot;</span>, <span class="function">() =&gt;</span></span><br><span class="line">    fetch(<span class="string">&quot;/fakeApi/posts&quot;</span>).then(<span class="function">(<span class="params">response</span>) =&gt;</span> response.json())</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> content = <span class="literal">null</span>;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>React Query 只用了仅仅 3 行就完成了之前 40 行代码的所有功能，而且也像 React 本身一样非常的声明式，开发者要做的仅仅是描述好自己想要的数据即可，真是令人赞叹。</p><p>进一步地，围绕数据请求这一场景，React Query 还针对下列常见需求给出了自己的解决方案：</p><ul><li>缓存……（可能是编程中最难做到的事情）</li><li>将对同一数据的多个请求简化为一个请求</li><li>在后台更新”过期”数据</li><li>知道数据何时”过期”</li><li>尽可能快地反映数据的更新</li><li>性能优化，如分页和懒加载数据</li><li>管理内存</li><li>共享数据</li></ul><p>接下来就让我们更全面地了解下它吧！</p><h2 id="数据请求"><a href="#数据请求" class="headerlink" title="数据请求"></a>数据请求</h2><p>作为最为基础的功能，React Query 提供了<code>useQuery</code>用以获取服务端数据，它接受三个参数：</p><ol><li><code>queryKey</code> 作为该数据请求的唯一标识，可用来重新请求、获取以及清理缓存等</li><li><code>queryFn</code> 数据请求逻辑，需返回一个 Promise</li><li><code>options</code> (可选） 额外配置项，可用来设置缓存时间、重试次数等</li></ol><p>它的返回值是一个对象，常用的属性主要有：</p><ul><li><code>status</code> 当前数据请求的状态，具体有 <code>idle</code>、<code>loading</code>、<code>error</code>、<code>success</code> 。此外，也可用<code>isIdle</code>、<code>isLoading</code> 、<code>isError</code>、 <code>isSucess</code>几个 flag 属性快速判断当前状态</li><li><code>error</code> 数据请求 Promise 失败时的错误信息</li><li><code>data</code> 数据请求 Promise 成功时返回的数据</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Todos</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; isLoading, isError, data, error &#125; = useQuery(<span class="string">&#x27;todos&#x27;</span>, <span class="function">() =&gt;</span> </span><br><span class="line">    fetch(<span class="string">&#x27;/api/todos&#x27;</span>).then(<span class="function"><span class="params">response</span> =&gt;</span> response.json()));</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">if</span> (isLoading) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">span</span>&gt;</span>Loading...<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (isError) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">span</span>&gt;</span>Error: &#123;error.message&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line"> </span><br><span class="line">  <span class="comment">// We can assume by this point that `isSuccess === true`</span></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="xml">      &#123;data.map(todo =&gt; (</span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;todo.id&#125;</span>&gt;</span>&#123;todo.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="xml">      ))&#125;</span></span><br><span class="line"><span class="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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; isLoading, isError, data, error &#125; = useQuery(</span><br><span class="line">  [<span class="string">&#x27;todos&#x27;</span>, status, page], </span><br><span class="line">  <span class="function">() =&gt;</span> fetch(<span class="string">`/api/todos?status=<span class="subst">$&#123;status&#125;</span>&amp;page=<span class="subst">$&#123;page&#125;</span>`</span>).then(<span class="function"><span class="params">response</span> =&gt;</span> response.json()),</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>这里我们将<code>queryKey</code>替换成了一个可以数组<code>[&#39;todos&#39;, status, page]</code>，这样一旦<code>status</code>、<code>page</code>参数发生变更的时候，React Query 通过<code>queryKey</code>的变更获悉应该重新发起请求，以此达到更新数据的目的。</p><p>实际上，React Query 对于<code>queryKey</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">useQuery([<span class="string">&#x27;todos&#x27;</span>, &#123; status, page &#125;], queryFn);</span><br><span class="line">useQuery(<span class="string">`todos/<span class="subst">$&#123;status&#125;</span>/<span class="subst">$&#123;page&#125;</span>`</span>, queryFn);</span><br><span class="line">useQuery([<span class="string">&#x27;todos&#x27;</span>, todoId], <span class="function">() =&gt;</span> fetchTodoById(todoId));</span><br></pre></td></tr></table></figure><p>关于<code>queryFn</code>，我们常用的可以是浏览器内置的<code>fetch</code>API，也可以是比较流行的 API fetching library，譬如 axios，只要返回的是一个 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><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Refined `fetch` API</span></span><br><span class="line">useQuery([<span class="string">&#x27;todos&#x27;</span>, todoId], <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">await</span> fetch(<span class="string">&#x27;/todos/&#x27;</span> + todoId);</span><br><span class="line">  <span class="comment">// Throw error if status code is not 2xx</span></span><br><span class="line">  <span class="keyword">if</span> (!response.ok) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(response.statusText);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> response.json();</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line"><span class="comment">// `axios` library</span></span><br><span class="line">useQuery([<span class="string">&#x27;todos&#x27;</span>, todoId], <span class="function">() =&gt;</span> axios.get(<span class="string">&#x27;/todos/&#x27;</span> + todoId));</span><br></pre></td></tr></table></figure><h3 id="分页请求"><a href="#分页请求" class="headerlink" title="分页请求"></a>分页请求</h3><p>请求分页数据时，我们常常遇到这样一个问题：</p><ol><li>当前页面的数据被清空</li><li>显示一个 loading 提示信息</li><li>获取到新数据后刷新页面</li></ol><p>这种方式的体验并不是很理想，一种更好的方式可以是：</p><ol><li>当前页面的数据仍然保留</li><li>在一个合适的位置显示 loading 提示信息，友好地给予用户反馈</li><li>获取到新数据后，无缝替换掉旧数据</li></ol><p>按照这一思路，React Query 提供了一个额外的配置项<code>keepPreviousData</code> 以及返回值的<code>isPreviousData</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="keyword">const</span> &#123; isLoading, isError, error, data, isFetching, isPreviousData &#125; = useQuery(</span><br><span class="line">  [<span class="string">&#x27;projects&#x27;</span>, page],</span><br><span class="line">  <span class="function">() =&gt;</span> fetchProjects(page),</span><br><span class="line">  &#123; <span class="attr">keepPreviousData</span> : <span class="literal">true</span> &#125;,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>完整代码见：<a href="https://codesandbox.io/s/react-query-pagination-c5ekbg?file=/src/index.js">https://codesandbox.io/s/react-query-pagination-c5ekbg?file=/src/index.js</a></p><h3 id="无限列表"><a href="#无限列表" class="headerlink" title="无限列表"></a>无限列表</h3><p>无限列表（Infinite List）是一种常见的数据请求场景，尤其在 Feed 流大行其道的当下。React Query 提供了<code>useInfiniteQuery</code>来处理这一需求。</p><p>首先假设我们的 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></pre></td><td class="code"><pre><span class="line">fetch(<span class="string">&#x27;/api/projects?cursor=0&#x27;</span>)</span><br><span class="line"><span class="comment">// &#123; data: [...], nextCursor: 3&#125;</span></span><br><span class="line">fetch(<span class="string">&#x27;/api/projects?cursor=3&#x27;</span>)</span><br><span class="line"><span class="comment">// &#123; data: [...], nextCursor: 6&#125;</span></span><br><span class="line">fetch(<span class="string">&#x27;/api/projects?cursor=6&#x27;</span>)</span><br><span class="line"><span class="comment">// &#123; data: [...], nextCursor: 9&#125;</span></span><br><span class="line">fetch(<span class="string">&#x27;/api/projects?cursor=9&#x27;</span>)</span><br><span class="line"><span class="comment">// &#123; data: [...] &#125;</span></span><br></pre></td></tr></table></figure><p>接下来我们展示下应用<code>useInfiniteQuery</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Projects</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> fetchProjects = ;</span><br><span class="line">  <span class="keyword">const</span> &#123;</span><br><span class="line">    data,</span><br><span class="line">    error,</span><br><span class="line">    fetchNextPage,</span><br><span class="line">    hasNextPage,</span><br><span class="line">    isFetching,</span><br><span class="line">    isFetchingNextPage,</span><br><span class="line">    status,</span><br><span class="line">  &#125; = useInfiniteQuery(</span><br><span class="line">    <span class="string">&#x27;projects&#x27;</span>,</span><br><span class="line">    <span class="function">(<span class="params">&#123; pageParam = <span class="number">0</span> &#125;</span>) =&gt;</span> fetch(<span class="string">&#x27;/api/projects?cursor=&#x27;</span> + pageParam), </span><br><span class="line">    &#123; <span class="attr">getNextPageParam</span>: <span class="function"><span class="params">lastPage</span> =&gt;</span> lastPage.nextCursor &#125;,</span><br><span class="line">  );</span><br><span class="line"> </span><br><span class="line">   <span class="keyword">return</span> status === <span class="string">&#x27;loading&#x27;</span> ? (</span><br><span class="line">     <span class="xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>Loading...<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line">   ) : status === <span class="string">&#x27;error&#x27;</span> ? (</span><br><span class="line">     <span class="xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>Error: &#123;error.message&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line">   ) : (</span><br><span class="line">     <span class="xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="xml">       &#123;data.pages.map((group, i) =&gt; (</span></span><br><span class="line"><span class="xml">         <span class="tag">&lt;<span class="name">React.Fragment</span> <span class="attr">key</span>=<span class="string">&#123;i&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">           &#123;group.projects.map(project =&gt; (</span></span><br><span class="line"><span class="xml">             <span class="tag">&lt;<span class="name">p</span> <span class="attr">key</span>=<span class="string">&#123;project.id&#125;</span>&gt;</span>&#123;project.name&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="xml">           ))&#125;</span></span><br><span class="line"><span class="xml">         <span class="tag">&lt;/<span class="name">React.Fragment</span>&gt;</span></span></span><br><span class="line"><span class="xml">       ))&#125;</span></span><br><span class="line"><span class="xml">       <span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">         <span class="tag">&lt;<span class="name">button</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">           <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> fetchNextPage()&#125;</span></span><br><span class="line"><span class="xml">           disabled=&#123;!hasNextPage || isFetchingNextPage&#125;</span></span><br><span class="line"><span class="xml">         &gt;</span></span><br><span class="line"><span class="xml">           &#123;isFetchingNextPage</span></span><br><span class="line"><span class="xml">             ? &#x27;Loading more...&#x27;</span></span><br><span class="line"><span class="xml">             : hasNextPage</span></span><br><span class="line"><span class="xml">             ? &#x27;Load More&#x27;</span></span><br><span class="line"><span class="xml">             : &#x27;Nothing more to load&#x27;&#125;</span></span><br><span class="line"><span class="xml">         <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">       <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">       <span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;isFetching &amp;&amp; !isFetchingNextPage ? &#x27;Fetching...&#x27; : null&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">     <span class="tag">&lt;/&gt;</span></span></span><br><span class="line">   )</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br></pre></td></tr></table></figure><p>相比普通的请求，<code>useInfiniteQuery</code>主要变更有：</p><ul><li>增加了一个配置项<code>getNextPageParam</code>，用以获取下次请求的游标 nextCursor 以获取正确的累加数据</li><li>增加了一个返回值属性<code>fetchNextPage</code>，用以触发请求，可以在按钮点击或者滚动位置接近当前数据底部等时机触发</li><li>增加了一个返回值属性<code>hasNextPage</code>，用以展示数据全部加载完毕等信息</li><li>将多批次服务端数据存储在<code>data.pages</code>数组中</li></ul><p>有兴趣可进一步参考官方的完整示例：<a href="https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/load-more-infinite-scroll">https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/load-more-infinite-scroll</a></p><h3 id="失败重试"><a href="#失败重试" class="headerlink" title="失败重试"></a>失败重试</h3><p>既然是向服务端请求数据，就有可能因为网络问题、反向代理或者服务器出现内部错误等问题导致请求失败。所以失败重试就是一个对于 React Query 这类工具来说非常必要的功能。</p><p>默认情况下，如果<code>queryFn</code>失败了，React Query 会自动重试 3 次，如果最终结果还是失败，则<code>status</code>变为<code>error</code>。可以通过设定<code>retry</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></pre></td><td class="code"><pre><span class="line">useQuery(<span class="string">&#x27;todos&#x27;</span>, fetchTodoList, &#123;</span><br><span class="line">  <span class="attr">retry</span>: <span class="number">10</span>, <span class="comment">// Will retry failed requests 10 times before displaying an error</span></span><br><span class="line">&#125;);</span><br><span class="line">useQuery(<span class="string">&#x27;todos&#x27;</span>, fetchTodoList, &#123;</span><br><span class="line">  <span class="attr">retry</span>: <span class="literal">false</span>, <span class="comment">// No retry</span></span><br><span class="line">&#125;);</span><br><span class="line">useQuery(<span class="string">&#x27;todos&#x27;</span>, fetchTodoList, &#123;</span><br><span class="line">  <span class="comment">// Custom retry function</span></span><br><span class="line">  <span class="attr">retry</span>: <span class="function">(<span class="params">failureCount, error</span>) =&gt;</span> error.statusCode === <span class="string">&#x27;403&#x27;</span> ? <span class="literal">false</span> : (failureCount &lt; <span class="number">3</span>),</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>还有另一个配置项<code>retryDelay</code>可以自定义重试的间隔，以避免服务端压力过大。默认的<code>retryDelay</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">attemptIndex =&gt; <span class="built_in">Math</span>.min(<span class="number">1000</span> * <span class="number">2</span> ** attemptIndex, <span class="number">30000</span>)</span><br></pre></td></tr></table></figure><p>这个设定有些略长，可根据实际业务情况修改。</p><h2 id="数据修改"><a href="#数据修改" class="headerlink" title="数据修改"></a>数据修改</h2><p>数据请求的另外一个大的类别是数据提交，诸如 POST、PATCH、DELETE 这几种具有副作用的请求类型都属于此类。我们希望在处理数据修改时，也有如同前面介绍的种种数据获取工具一样的趁手方案。</p><p>针对这一诉求，React Query 提供了<code>useMutation</code>，它接受两个参数：</p><ol><li><code>mutationFn</code> 类似<code>useQuery</code>中的<code>queryFn</code>，<code>mutationFn</code> 是执行数据提交逻辑的方法，需返回一个 Promise</li><li><code>options</code> 可选配置项，例如<code>onSucess</code>、<code>retry</code>等</li></ol><p><code>useMutation</code>返回的对象中，常用的一些属性有：</p><ul><li><code>mutate</code> 触发<code>mutationFn</code>，开始提交数据到服务器端</li><li><code>status</code>、<code>error</code>、<code>data</code> 等 与<code>useQuery</code>返回值类似，可以用这些属性确定当前请求的状态、错误信息和返回值</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> addTodoMutation = useMutation(<span class="function"><span class="params">newTodo</span> =&gt;</span> axios.post(<span class="string">&quot;/todos&quot;</span>, newTodo));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      &#123;addTodoMutation.isError ? (</span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">div</span>&gt;</span>An error occurred: &#123;mutation.error.message&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      ) : null&#125;</span></span><br><span class="line"><span class="xml">      &#123;addTodoMutation.isSuccess ? <span class="tag">&lt;<span class="name">div</span>&gt;</span>Todo added!<span class="tag">&lt;/<span class="name">div</span>&gt;</span> : null&#125;</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">form</span></span></span></span><br><span class="line"><span class="tag"><span class="xml">        <span class="attr">onSubmit</span>=<span class="string">&#123;(e)</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="xml">          e.preventDefault();</span></span><br><span class="line"><span class="xml">          addPostMutation.mutate(new FormData(e.currentTarget));</span></span><br><span class="line"><span class="xml">        &#125;&#125;</span></span><br><span class="line"><span class="xml">      &gt;</span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;todo&quot;</span>&gt;</span>Todo: <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span></span><br><span class="line"><span class="xml">          <span class="tag">&lt;<span class="name">input</span> <span class="attr">id</span>=<span class="string">&quot;todo&quot;</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;todo&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">button</span>&gt;</span>&#123;addTodoMutation.isLoading ? &quot;Creating...&quot; : &quot;Create&quot;&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line"><span class="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>如上所示，一开始我们使用<code>useMutation</code>创建了一个 mutation，并在提交表单的时候调用其<code>mutate</code>方法提交用户填写的新待办事项信息，并通过<code>isError</code>、<code>isSuccess</code>、<code>isLoading</code>等标志变量展示相关进展。</p><h3 id="清除数据缓存"><a href="#清除数据缓存" class="headerlink" title="清除数据缓存"></a>清除数据缓存</h3><p>当我们提交的新数据会导致应用中某些数据“过期”时，需要显式地清除数据缓存，以确保数据准确性。</p><p>举一个例子：</p><ul><li>应用包含两部分：待办事项列表和创建待办事项的表单</li><li>用户提交了新待办事项后，应该重新获取待办事项列表</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> addTodoMutation = useMutation(</span><br><span class="line">  useMutation(<span class="function"><span class="params">newTodo</span> =&gt;</span> axios.post(<span class="string">&quot;/todos&quot;</span>, newTodo)),</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">onSuccess</span>: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      queryClient.invalidateQueries(<span class="string">&quot;todos&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>创建 mutation 时，我们添加了一个<code>onSuccess</code> 配置项，它会在修改成功时执行。其中的<code>queryClient.invalidateQueries(&quot;todos&quot;)</code>正如它的名字一样，可以清理掉<code>queryKey</code>为 “todos” 的数据，在这种情况下，React Query 会自动的再次请求该数据，从而达到了更新待办事项列表的目的。</p><p>类似的，如果我们在某一个待办事项详情页进行了编辑，那么可以直接在<code>onSuccess</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="keyword">const</span> mutation = useMutation(editTodo, &#123;</span><br><span class="line">  <span class="attr">onSuccess</span>: <span class="function"><span class="params">data</span> =&gt;</span> &#123;</span><br><span class="line">    queryClient.setQueryData([<span class="string">&#x27;todo&#x27;</span>, &#123; <span class="attr">id</span>: <span class="number">5</span> &#125;], data);</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>完整代码见<a href="https://codesandbox.io/s/react-query-blog-3w24ks?file=/src/index.js">https://codesandbox.io/s/react-query-blog-3w24ks?file=/src/index.js</a></p><h2 id="进阶功能"><a href="#进阶功能" class="headerlink" title="进阶功能"></a>进阶功能</h2><p>围绕数据获取和数据修改两大服务端数据交互场景，React Query 还提供了非常丰富的进阶功能，进一步提高开发效率。</p><h3 id="数据共享"><a href="#数据共享" class="headerlink" title="数据共享"></a>数据共享</h3><p>观察比较敏锐的同学可能已经注意到：请求文章列表后，如果点击某一条具体的文章查看详情的时候，实际上不需要额外发一个单独的文章数据请求，直接读取现有的文章数据即可。即便在列表数据非完整数据的情况下，也可以先展示既有的部分数据，然后获取单项完整数据后再刷新 UI，从而达到非常顺滑的用户体验。</p><p>针对这一场景，React Query 提供了一些基础性支持：</p><ul><li>通过<code>queryClient.getQueryData</code> 可以根据<code>queryKey</code>获取之前请求的数据，譬如文章列表数据</li><li>如果列表数据包含各项的完整数据，则可通过在<code>useQuery</code>增加<code>initialData</code>配置项来设置初始数据</li><li>如果列表数据只包含各项的部分数据，则可通过在<code>useQuery</code>增加<code>placeholderData</code>配置项来设置占位数据，这种情况下<code>useQuery</code>会继续请求完整数据</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Fetch full post data via `initialData`</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PostDetail</span>(<span class="params">&#123; id &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> result = useQuery([<span class="string">&#x27;post&#x27;</span>, id], <span class="function">() =&gt;</span> fetch(<span class="string">`/posts/<span class="subst">$&#123;id&#125;</span>`</span>), &#123;</span><br><span class="line">    <span class="attr">initialData</span>: <span class="function">() =&gt;</span> &#123; <span class="comment">// Will not request again if there is valid query data</span></span><br><span class="line">      <span class="keyword">return</span> queryClient.getQueryData(<span class="string">&#x27;posts&#x27;</span>)?.find(<span class="function"><span class="params">d</span> =&gt;</span> d.id === id);</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="comment">// Fetch placeholder post data via `placeholderData`</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">PostDetail</span>(<span class="params">&#123; id &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> result = useQuery([<span class="string">&#x27;post&#x27;</span>, id], <span class="function">() =&gt;</span> fetch(<span class="string">`/posts/<span class="subst">$&#123;id&#125;</span>`</span>), &#123;</span><br><span class="line">    <span class="attr">placeholderData</span>: <span class="function">() =&gt;</span> &#123; <span class="comment">// Will always request again</span></span><br><span class="line">      <span class="keyword">return</span> queryClient.getQueryData(<span class="string">&#x27;posts&#x27;</span>)?.find(<span class="function"><span class="params">d</span> =&gt;</span> d.id === id);</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>关于<code>initialData</code>和<code>placeholderData</code>两者的更详细分析，可以查看 Dominik 的这篇文章：<a href="https://tkdodo.eu/blog/placeholder-and-initial-data-in-react-query">Placeholder and Initial Data in React Query</a></p><h3 id="数据预获取"><a href="#数据预获取" class="headerlink" title="数据预获取"></a>数据预获取</h3><p>改善交互体验的另外一个常见优化是在用户表现出下一步动作的明确倾向时预获取相关数据（Prefetching），譬如：</p><ul><li>鼠标悬浮在某个内容项 100ms 以上</li><li>页面焦点停止在某个内容项 100ms 以上</li><li>首屏着重推荐，用户点击率较大的内容</li></ul><p>在处理这类问题的时候，我们可以直接应用 <code>useQuery</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">&lt;li</span><br><span class="line">  key=&#123;post.id&#125;</span><br><span class="line">  onMouseEnter=&#123;<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    queryClient.prefetchQuery(</span><br><span class="line">      [<span class="string">&#x27;post&#x27;</span>, post.id],</span><br><span class="line">      <span class="function">() =&gt;</span> getPost(post.id),</span><br><span class="line">      &#123; <span class="attr">staleTime</span>: <span class="number">10</span> * <span class="number">1000</span> &#125;, <span class="comment">// Only prefetch if there is no cache or the cache is older than 10s</span></span><br><span class="line">    );</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</span><br><span class="line">  &#123;post.title&#125;</span><br><span class="line">&lt;/li&gt;</span><br></pre></td></tr></table></figure><p>这种方式的一个主要好处是：如果用户并没有真的访问对应的页面，则一段时间后 React Query 会自动清理掉预获取的数据。</p><p>具体可以查看官方示例：<a href="https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/prefetching">https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/prefetching</a></p><h3 id="SSR"><a href="#SSR" class="headerlink" title="SSR"></a>SSR</h3><p>对于一些重数据交互、轻 UI 交互的面向用户侧的展示型应用来说，服务端渲染 SSR 也是一个比较重要的诉求，这样一方面可以让用户从输入 url 到看到内容的速度更快，另一方面也方便搜索引擎进行索引。</p><p>React Query 从原理层面讲并没有特别依赖运行环境的代码，只要<code>queryFn</code>可以在客户端或者服务端正常获取数据即可。此外，它还提供了<code>dehydrate</code>、<code>Hydrate</code>等 SSR 相关 API，方便各个框架或者开发者进行整合。</p><p>下面是一个 SSR 实现的核心逻辑示例：</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="comment">// Server side</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ssr</span>(<span class="params">req, res</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> queryClient = <span class="keyword">new</span> QueryClient();</span><br><span class="line">  <span class="comment">// Fetch data</span></span><br><span class="line">  <span class="keyword">await</span> queryClient.prefetchQuery(<span class="string">&#x27;key&#x27;</span>, fn);</span><br><span class="line">  <span class="comment">// Dump React Query state in order to pass to client</span></span><br><span class="line">  <span class="keyword">const</span> dehydratedState = dehydrate(queryClient);</span><br><span class="line">  <span class="keyword">const</span> html = ReactDOM.renderToString(</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">QueryClientProvider</span> <span class="attr">client</span>=<span class="string">&#123;queryClient&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Hydrate</span> <span class="attr">state</span>=<span class="string">&#123;dehydratedState&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">App</span> /&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">Hydrate</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">QueryClientProvider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  res.send(<span class="string">`</span></span><br><span class="line"><span class="string">    &lt;html&gt;</span></span><br><span class="line"><span class="string">      &lt;body&gt;</span></span><br><span class="line"><span class="string">        &lt;div id=&quot;root&quot;&gt;<span class="subst">$&#123;html&#125;</span>&lt;/div&gt;</span></span><br><span class="line"><span class="string">        &lt;script&gt;</span></span><br><span class="line"><span class="string">          window.__REACT_QUERY_STATE__ = <span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(dehydratedState)&#125;</span>;</span></span><br><span class="line"><span class="string">        &lt;/script&gt;</span></span><br><span class="line"><span class="string">      &lt;/body&gt;</span></span><br><span class="line"><span class="string">    &lt;/html&gt;</span></span><br><span class="line"><span class="string">  `</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Clear internal cache to save memory</span></span><br><span class="line">  queryClient.clear();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Client side</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">start</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// Read React Query state from SSR</span></span><br><span class="line">  <span class="keyword">const</span> dehydratedState = <span class="built_in">window</span>.__REACT_QUERY_STATE__;</span><br><span class="line">  <span class="keyword">const</span> queryClient = <span class="keyword">new</span> QueryClient();</span><br><span class="line"></span><br><span class="line">  ReactDOM.hydrate(</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">QueryClientProvider</span> <span class="attr">client</span>=<span class="string">&#123;queryClient&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Hydrate</span> <span class="attr">state</span>=<span class="string">&#123;dehydratedState&#125;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">App</span> /&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">Hydrate</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">QueryClientProvider</span>&gt;</span></span>,</span><br><span class="line">    <span class="built_in">document</span>.getElementById(<span class="string">&#x27;root&#x27;</span>),</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果你恰好在使用 Next.js 的话，React Query 提供了更为多样化的 SSR 解决方案，详情可以查看<a href="https://react-query.tanstack.com/guides/ssr#using-nextjs">官方文档</a>。</p><h2 id="内部实现"><a href="#内部实现" class="headerlink" title="内部实现"></a>内部实现</h2><p>React Query 的作者 Tanner 曾在 2021 年 React Summit 远程会议上分享了<a href="https://www.youtube.com/watch?v=9SrIirrnwk0">如何用 150 行代码实现一个 Lite 版本的 React Query</a>，这对于我们了解其实现思路非常有帮助，知其然且知其所以然，之前介绍的很多功能也会应用得更加趁手。</p><h3 id="全局唯一且可共享的-Query-Client"><a href="#全局唯一且可共享的-Query-Client" class="headerlink" title="全局唯一且可共享的 Query Client"></a>全局唯一且可共享的 Query Client</h3><p>借助 React Context，可以在 web 应用启动的时候创建一个 Query Client 实例，并通过<code>QueryClientProvider</code>工具组件使得所有后续<code>useQuery</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Create a context in React Query module</span></span><br><span class="line"><span class="keyword">const</span> context = React.createContext();</span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">QueryClientProvider</span>(<span class="params">&#123; children, client &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">context.Provider</span> <span class="attr">value</span>=<span class="string">&#123;client&#125;</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">context.Provider</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">useQuery</span>(<span class="params">&#123; queryKey, queryFn, staleTime, cacheTime &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// Get the shared client instance from context</span></span><br><span class="line">  <span class="keyword">const</span> client = React.useContext(context);</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>进一步地，可以在<code>QueryClientProvider</code>组件中添加对页面窗口 focus、visibilitychange 事件的监听，来自动重新获取数据等。</p><h3 id="Query-Client-保存着所有请求信息"><a href="#Query-Client-保存着所有请求信息" class="headerlink" title="Query Client 保存着所有请求信息"></a>Query Client 保存着所有请求信息</h3><p>Query Client 既然是唯一且被所有<code>useQuery</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">QueryClient</span> </span>&#123;</span><br><span class="line">  <span class="function"><span class="title">constructor</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.queries = [];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  getQuery = <span class="function">(<span class="params">options</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// Serialize query key to get the unique ID of the specific query</span></span><br><span class="line">    <span class="keyword">const</span> queryHash = <span class="built_in">JSON</span>.stringify(options.queryKey);</span><br><span class="line">    <span class="keyword">let</span> query = <span class="built_in">this</span>.queries.find(<span class="function">(<span class="params">d</span>) =&gt;</span> d.queryHash === queryHash);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!query) &#123;</span><br><span class="line">      query = createQuery(<span class="built_in">this</span>, options);</span><br><span class="line">      <span class="built_in">this</span>.queries.push(query);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> query;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>创建请求的过程略显复杂，不过简单来说就是：</p><ul><li>创建一个 query 对象</li><li>主要包括 query 的 status、error、data 等状态信息</li><li>以及获取数据的<code>fetch</code>操作及对应的 Promise 等</li></ul><h3 id="应用观察者模式和-React-Hooks-进行响应"><a href="#应用观察者模式和-React-Hooks-进行响应" class="headerlink" title="应用观察者模式和 React Hooks 进行响应"></a>应用观察者模式和 React Hooks 进行响应</h3><p>为了能够在请求状态变更时刷新 UI，React Query 借助了观察者模式和 React Hooks：</p><ul><li>每个 query 对象还有一个属性 <code>subscribers</code>，并在请求状态变更时触发这些订阅者</li><li>初次调用<code>useQuery</code>时，它会创建一个 query 对象，并添加一个强制 rerender 的回调到其<code>subscribers</code>属性中</li></ul><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="function"><span class="keyword">function</span> <span class="title">useQuery</span>(<span class="params">&#123; queryKey, queryFn, staleTime, cacheTime &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> client = React.useContext(context);</span><br><span class="line">  <span class="keyword">const</span> [, rerender] = React.useReducer(<span class="function">(<span class="params">i</span>) =&gt;</span> i + <span class="number">1</span>, <span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> observerRef = React.useRef();</span><br><span class="line">  <span class="keyword">if</span> (!observerRef.current) &#123;</span><br><span class="line">    <span class="comment">// Query observer is a simple wrapper of query</span></span><br><span class="line">    observerRef.current = createQueryObserver(client, &#123;</span><br><span class="line">      queryKey,</span><br><span class="line">      queryFn,</span><br><span class="line">      staleTime,</span><br><span class="line">      cacheTime,</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  React.useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// Trigger rerendering whenever query state changes</span></span><br><span class="line">    <span class="keyword">return</span> observerRef.current.subscribe(rerender);</span><br><span class="line">  &#125;, []);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> observerRef.current.getResult();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>针对服务端数据交互这一场景应运而生的 React Query 为轻交互、重数据的业务场景提供了极大的开发上的便利。很多应用甚至可以仅需组合 React Query、Context 以及 Component state 即可实现全部的状态管理。此外，React Query 丰富的功能更是让一些最佳实践变得轻而易举，为构建愉悦的开发体验和用户体验做出了显著贡献。</p><p>在这一细分领域还有类似 <a href="https://swr.vercel.app/zh-CN">SWR</a>、<a href="https://redux-toolkit.js.org/rtk-query/overview">RTK-Query</a> 等方案。SWR 出自知名公司 Vercel 开发，更加轻量。RTK-Query 对于 Redux 为主的项目更加友好。各位可以根据自身业务团队偏好择优选用。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><a href="https://www.youtube.com/watch?v=seU46c6Jz7E">React Query: It’s Time to Break up with your “Global State”! –Tanner Linsley</a></li><li><a href="https://www.youtube.com/watch?v=DocXo3gqGdI">All About React Query (with Tanner Linsley) — Learn With Jason</a></li><li><a href="https://www.youtube.com/watch?v=9SrIirrnwk0">Let’s Build React Query in 150 Lines of Code! – Tanner Linsley, React Summit Remote Edition 2021</a></li><li><a href="https://www.smashingmagazine.com/2022/01/building-real-app-react-query/">Building The Real App With React Query</a></li><li><a href="https://nosleepjavascript.com/interview-tanner-linsley/">Interview with React Query’s creator: Tanner Linsley</a></li><li><a href="https://www.robinwieruch.de/react-hooks-fetch-data/">How to fetch data with React Hooks</a></li></ul><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>太阳底下没有新鲜事，Tanner Linsley 在<a href="https://nosleepjavascript.com/interview-tanner-linsley/">一次访谈</a>中介绍了 React Query 的由来：</p><blockquote><p>The pains that inspired React Query were years in the making, even before hooks. I was a heavy Redux user for a long time, but when hooks came out, I knew they would serve as a great API to finally ditch Redux in favor of something less manual. I came up with the initial inspiration for React Query when I saw a library called <a href="https://github.com/vadimdemedes/draqula">Draqula</a> was released. It was almost like a lightweight Apollo and something clicked about its patterns and style. I decided to draw from its patterns a bit and React Query was born.</p></blockquote><p>Apollo 几乎是 GraphQL 的标配，非常简单易用。但它要支持的功能实在是太多了，后面连本地状态管理都有了方案，堪称巨无霸。Draqula 致力于打造一个轻量版 Apollo，只有数据获取和修改，但仍然聚焦在 GraphQL 而非 RESTful API。React Query 借鉴了 Draqula 的模式，并围绕普通数据请求做了众多优化。</p><p>在访谈中，还有很多关于 Tanner 的趣闻，包括这大哥并非计算机专业出身，但非常热衷于实践中学习，甚至他的日常起居节奏、当爹经验等等，非常有趣。</p><p>最后，Tanner 还是一家致力于 SEO 解决方案的创业公司 <a href="http://nozzle.io/">Nozzle.io</a> 的联合创始人之一：<img src="../images/react-query-01.jpg"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在前端项目的数据状态管理中，与服务端的交互数据往往占较大比例，尤其在诸如个人博客网站、内部管理系统这样的重数据交互、轻 UI 交互的场景。这类数据和客户端本地数据有很多差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据持久化存储在远程服务端，不受客户端控制&lt;/li&gt;
&lt;li&gt;需要通过异步请求来获取和更新&lt;/li&gt;
&lt;li&gt;有可能在 Web 应用中变得”过时”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传统状态管理方案在应对这类场景时有些显得笨拙臃肿。React Hooks 推出后，逐渐有一些专注这一领域的工具出现并大放光彩。本文将着重介绍 React Query 这一代表性工具给出的简洁高效的解决方案。&lt;/p&gt;
&lt;h2 id=&quot;一个例子&quot;&gt;&lt;a href=&quot;#一个例子&quot; class=&quot;headerlink&quot; title=&quot;一个例子&quot;&gt;&lt;/a&gt;一个例子&lt;/h2&gt;&lt;p&gt;假设我们需要开发一个个人博客，以其中首页博客列表页为例，使用传统的 Redux 状态管理方案的话，需要先定义 actions、reducers 以及 store：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;19&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;20&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;21&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;22&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;23&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;24&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;25&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;26&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;27&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;28&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;29&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;30&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;31&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Redux actions, reducers and stores&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; fetchPosts = createAsyncThunk(&lt;span class=&quot;string&quot;&gt;&amp;quot;posts/fetchPosts&amp;quot;&lt;/span&gt;, &lt;span class=&quot;function&quot;&gt;() =&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  fetch(&lt;span class=&quot;string&quot;&gt;&amp;quot;/fakeApi/posts&amp;quot;&lt;/span&gt;).then(&lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;response&lt;/span&gt;) =&amp;gt;&lt;/span&gt; response.json())&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; postsSlice = createSlice(&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;quot;posts&amp;quot;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;initialState&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;posts&lt;/span&gt;: [],&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;status&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;quot;idle&amp;quot;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;error&lt;/span&gt;: &lt;span class=&quot;literal&quot;&gt;null&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;title&quot;&gt;extraReducers&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;builder&lt;/span&gt;)&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    builder&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      .addCase(fetchPosts.pending, &lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;state, action&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        state.status = &lt;span class=&quot;string&quot;&gt;&amp;quot;loading&amp;quot;&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &amp;#125;)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      .addCase(fetchPosts.fulfilled, &lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;state, action&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        state.status = &lt;span class=&quot;string&quot;&gt;&amp;quot;succeeded&amp;quot;&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        state.posts = action.payload;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &amp;#125;)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      .addCase(fetchPosts.rejected, &lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;state, action&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        state.status = &lt;span class=&quot;string&quot;&gt;&amp;quot;failed&amp;quot;&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        state.error = action.error.message;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; store = configureStore(&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;reducer&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;posts&lt;/span&gt;: postsSlice.reducer,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="React" scheme="https://springuper.github.io/tags/React/"/>
    
    <category term="React Query" scheme="https://springuper.github.io/tags/React-Query/"/>
    
  </entry>
  
  <entry>
    <title>Fire Fast</title>
    <link href="https://springuper.github.io/fire-fast/"/>
    <id>https://springuper.github.io/fire-fast/</id>
    <published>2022-05-03T09:28:25.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://springuper.github.io/hire-slow/">上篇文章</a>，我们谈过了”Hire slow，fire fast”的前半句，今天我们聊下后半句，也即”fire fast”。</p><p>历经千辛万苦，新人终于到岗并开始适应新环境，大部分人都可以在两到三个月时间恢复战斗力，进而产出符合预期的价值。但也有少数例外情况，由于种种原因，新人并没有达成预期。应该怎么处理这样的情况呢？</p><h2 id="一定要-fire-吗？"><a href="#一定要-fire-吗？" class="headerlink" title="一定要 fire 吗？"></a>一定要 fire 吗？</h2><p>首先我们要界定这个人是不是达到了需要 fire 的标准，具体各团队可能都有些适合自身的判断。我个人比较偏向的标准是：</p><ol><li>不能达成预期的预期是长期性的，短期内很难改观<p style="margin: 0 0 0.2rem 0">如果新人只是因为对于当前技术栈不熟悉，或者因为外语、远程办公等工作环境的剧烈变化导致的不符合预期，那么我们是可以考虑多给些时间以进一步考察。但如果新人是因为自身能力问题，譬如一个高级工程师无法产出合格的系统方案设计，或者总是用两倍甚至三倍以上的时间才能交付质量下乘的项目，这类问题通常无法短时间内通过培训等助力方式快速提升</p></li><li>存在一些原则性的根源问题<p style="margin: 0 0 0.2rem 0">每个团队可能都有一些”红线“，譬如一个非常分布式的基于信任构建的团队里，非常依赖高度自驱来推进各项目，如果新人无法自律，划水严重，如果在管理者进行警告督促后仍无有效改正的话，可以认定存在原则性问题。类似的红线问题还有不诚信、泄露公司机密牟利等等</p></li></ol><p>其实第二点只是第一点的一种特例，本质上第一点这一标准就够了。</p><span id="more"></span><h2 id="Fire-其实阻力重重"><a href="#Fire-其实阻力重重" class="headerlink" title="Fire 其实阻力重重"></a>Fire 其实阻力重重</h2><p>即便我们内心的天平已经偏向于 fire，但实操层面，往往还有各种因素导致我们无法快速做出决断：</p><ul><li>这位工程师只是解决问题速度慢了点，其它例如沟通方面还是可以的</li><li>代码写的虽然糙了点，但是也能如期交付</li><li>自律比较差，但是多催一催也能出活</li></ul><p>从根本上来讲，本身这个世界就是复杂的，并不是非黑即白，我们往往要在灰色地带做取舍。大部分未达预期的新人既然能够通过层层筛选，肯定也有自己的相对优势。那么怎么权衡要不要 fire 就高度依赖团队管理者对于情势的判断，尤其是团队定位和个人定位的调和空间。如果他的某方面核心能力能够在未来一段时间提升团队的短板，那么即便管理者适当补位也可以接受的话，那可以先留任看看。相反，如果他的核心能力或者很弱或者并非团队将来所需的情况下，就没有必要继续。</p><p>另外一个 fire 的阻力点是：这某种程度上证明了招聘流程存在疏漏。让人正面和解决问题需要高度自信，有的管理者可能由于这方面的疑虑，往往都优先选择再观察一段时间，直到很久之后迫不得已才动手。其实大可不必，即便是类似 Netflix 之类的优秀团队，也有可能遇到一些针对性比较强的候选人或者遗漏了一些要点。就像我们维护一个产品或者系统，逐步迭代查漏补缺就好。</p><p>还有人担忧说如果新人愤愤不平，故意捣乱怎么办，譬如实名或匿名开始语言攻击或者诋毁团队，破坏团结等。其实我倒是觉得，这样的人越早暴露越说明我们做对了。</p><h2 id="容忍低绩效者是对大多数人的惩罚"><a href="#容忍低绩效者是对大多数人的惩罚" class="headerlink" title="容忍低绩效者是对大多数人的惩罚"></a>容忍低绩效者是对大多数人的惩罚</h2><p>在上一篇文章里我们分享过平庸的工程师带来的一些负面影响，其中我认为最重要的一条是：</p><ul><li>向团队表明你接受平庸，从而使问题更加严重</li></ul><p>正如那篇文章开头的例子一样，低绩效者一旦在团队中被其他人显著地识别了出来，那么大家就会开始质疑团队是否还在一个健康发展之中。一旦他们做出了肯定的判断，那么高绩效者们的离开只是时间问题。因此我一向认为，容忍低绩效者实际是对大多数人的惩罚，是在向大家表明这个团队越来越平庸。</p><p>对于管理者，我理解非常类似 Jon Snow 这样的守夜人，核心职能之一就是能够营造出一种让团队的大部分人充分发挥自己的聪明才智为公司贡献价值的氛围。当有一些噪音或者负面情况出现时，守夜人能够及时发现和干预，以保障团队主体免遭影响。</p><p>也因此，即便困难重重，我依然建议管理者从整体利益出发，在负面影响扩大之前，早做决断。</p><h2 id="所有的底气来自于有能力招更好的人"><a href="#所有的底气来自于有能力招更好的人" class="headerlink" title="所有的底气来自于有能力招更好的人"></a>所有的底气来自于有能力招更好的人</h2><p>不少管理者在是否 fire 之间犹豫不决还有一个重要因素：招人不易。试问：如果一个月内就可以招到符合要求的候补成员，还用得着犹豫吗？当然不用！所以，类似我在<a href="https://springuper.github.io/recruitment-driven-organization/">招聘驱动的组织建设</a>中提到的</p><blockquote><p>工程师组织建设的核心就是招人，招足够优秀的人</p></blockquote><p>如果我们整个组织是优秀高效稳定的，辅以适当的面向招聘的各项举措，自然能够吸引到足够的候选人，从而实现自然可持续增长。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>”Hire slow, fire fast” 看似简单，实则是大智慧，关乎高人才密度团队建设的一体两面。我自己在平时工作中受用良多，对于“做正确而不是容易的事情”也有了更深的理解。这两篇粗浅的思考也希望能给更多人提供参考。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://springuper.github.io/hire-slow/&quot;&gt;上篇文章&lt;/a&gt;，我们谈过了”Hire slow，fire fast”的前半句，今天我们聊下后半句，也即”fire fast”。&lt;/p&gt;
&lt;p&gt;历经千辛万苦，新人终于到岗并开始适应新环境，大部分人都可以在两到三个月时间恢复战斗力，进而产出符合预期的价值。
但也有少数例外情况，由于种种原因，新人并没有达成预期。应该怎么处理这样的情况呢？&lt;/p&gt;
&lt;h2 id=&quot;一定要-fire-吗？&quot;&gt;&lt;a href=&quot;#一定要-fire-吗？&quot; class=&quot;headerlink&quot; title=&quot;一定要 fire 吗？&quot;&gt;&lt;/a&gt;一定要 fire 吗？&lt;/h2&gt;&lt;p&gt;首先我们要界定这个人是不是达到了需要 fire 的标准，具体各团队可能都有些适合自身的判断。
我个人比较偏向的标准是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不能达成预期的预期是长期性的，短期内很难改观&lt;p style=&quot;margin: 0 0 0.2rem 0&quot;&gt;
如果新人只是因为对于当前技术栈不熟悉，或者因为外语、远程办公等工作环境的剧烈变化导致的不符合预期，那么我们是可以考虑多给些时间以进一步考察。
但如果新人是因为自身能力问题，譬如一个高级工程师无法产出合格的系统方案设计，或者总是用两倍甚至三倍以上的时间才能交付质量下乘的项目，
这类问题通常无法短时间内通过培训等助力方式快速提升
&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;存在一些原则性的根源问题&lt;p style=&quot;margin: 0 0 0.2rem 0&quot;&gt;
每个团队可能都有一些”红线“，譬如一个非常分布式的基于信任构建的团队里，非常依赖高度自驱来推进各项目，如果新人无法自律，划水严重，
如果在管理者进行警告督促后仍无有效改正的话，可以认定存在原则性问题。类似的红线问题还有不诚信、泄露公司机密牟利等等
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其实第二点只是第一点的一种特例，本质上第一点这一标准就够了。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Recruitment" scheme="https://springuper.github.io/tags/Recruitment/"/>
    
    <category term="Organization" scheme="https://springuper.github.io/tags/Organization/"/>
    
  </entry>
  
  <entry>
    <title>Hire Slow</title>
    <link href="https://springuper.github.io/hire-slow/"/>
    <id>https://springuper.github.io/hire-slow/</id>
    <published>2022-04-04T08:51:04.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>我曾亲眼见证过这样一个事情：</p><p>一个内部工具团队，因为业务增长太快压力山大，某个时间点 lead 在招聘方面做出了妥协，放水招进来一个看起来还凑合的工程师。过了一小段时间，团队里其他人慢慢开始离职。有几个是我非常熟悉的伙伴，平时跟他们沟通不少，大家普遍的一些感触是：我怎么和这样的人一起工作。再后来，团队雪崩式解散，部门领导不得不抽调其他资源维系业务运转。</p><p>有一次我也跟原来的 lead 闲聊，当谈到为什么当初招那个人的时候，他坦言：业务太多了，内部服务开发又不招人喜欢，没办法。</p><p>这些年来，随着团队管理经验的逐步积累，我越来越意识到师傅曾经教给我们的那句话的重要性：“Hire slow，fire fast”。今天我们就先聊一下前半句：Hire slow。</p><span id="more"></span><h2 id="业务压力大-gt-不得不降低标准招人？"><a href="#业务压力大-gt-不得不降低标准招人？" class="headerlink" title="业务压力大 =&gt; 不得不降低标准招人？"></a>业务压力大 =&gt; 不得不降低标准招人？</h2><p>这是一个很自然的想法，没有人就没有资源迭代旧服务和开发新服务，要不然怎么办？</p><p>问题就在于这儿，作为团队负责人，切忌<strong>把自己逼得只有一个选择</strong>。</p><p>认真思考下，真的就没有别的办法了吗？譬如，是否可以尝试：</p><ul><li>开源<ul><li>跟上级 lead 充分沟通，争取临时借调兄弟组资源等方式</li><li>请求倾斜招聘资源，并推动内推、猎头等多种形式举荐人才</li><li>尝试引入外包或者临时工等外部资源</li></ul></li><li>节流<ul><li>精简需求，把钢用到刀刃上</li><li>采购 SaaS 服务或者基于成熟开源方案快速开发</li></ul></li><li>提效<ul><li>培养团队成员的核心技能，重点是开发能力和沟通能力，让他们成长更为快速一些，早日担当大任</li><li>积累可复用的方案、框架、中间件等，提升工程效率</li></ul></li></ul><p>这里只是列出一些可能的办法，实际大家可能还能想出更多。</p><p>其实 lead 除了在开源节流方面做得不是很理想外，还有一个认知上的误区：</p><blockquote><p>内部服务开发又不招人喜欢</p></blockquote><p>也许他本心也是这么认为的，但笔者不敢苟同。内部系统确实有一定的局限性：用户少，需求杂，工期短，反馈多。基本都是围绕 CRUD，做得好没人说，有问题挨批评，经常吃力不讨好。这些都是现实问题。</p><p>但跳出这个层面来看，内部系统其实某种程度上是公司成败的“秘密武器”：</p><ul><li>内部系统对于降本增效有极大的促进作用<p style="margin: 0 0 0.2rem 1.4rem">一个好用的内部系统，实际上是围绕组织结构来深度定制的，取舍得当的话，往往可以对业务和组织的不断演进产生深远影响。美团早期在快速上单方面做的一系列结构化深度改进让整个运营团队的效率提升了一大截，成功在千团大战等激烈竞争中脱颖而出。</p></li><li>内部系统可以充分挖掘用户、业务的痛点，以及大家的正向反馈<p style="margin: 0 0 0.2rem 1.4rem">用户可以轻易触达，业务也比较方便深入调研，这都是难得的差异化优势。另外，lead 要去注意抓一些用户或者数据方面的正向反馈，以此来给团队带来更多荣誉感和正向激励，让大家明白自己做的事情的价值</p></li></ul><p>类似人的五官一样，没有哪个是可以或缺的，内部系统也很重要，让团队理解自己团队的价值是 lead 的最为重要的职责之一。</p><h2 id="降低标准招人的后果"><a href="#降低标准招人的后果" class="headerlink" title="降低标准招人的后果"></a>降低标准招人的后果</h2><p>那实在是没有办法的情况下，不得不降低标准招人的话，会有什么后果呢？</p><p>Netflix 创始人 Hastings 在“不拘一格”一书中提到了平庸的工程师带来的一些负面影响：</p><ul><li>消耗管理者的精力，使他们没有时间把精力放在优秀员工身上</li><li>团队讨论的质量得不到保证，拉低团队整体智商</li><li>强迫他人国绕着他们开展工作，致使工作效率低下</li><li>排挤其他追求卓越的员工</li><li>向团队表明你接受平庸，从而使问题更加严重</li></ul><p>最大的问题在于：劣币驱除良币，其他的优秀工程师其实并不缺乏好机会，一旦他们确信团队水平在逐渐变差，那么离开是迟早的事。长此以往，团队只剩下一群次等工程师，交付能力愈加堪忧，撤职跑路也就为时不远了。</p><p>因此，原则上，绝不能优先考虑在招聘标准方面做出让步，这应该是所有尝试都失败的情况下才不得已而为之，仅限于短期手段的无奈之举。</p><h2 id="招聘速度过快还有更多问题"><a href="#招聘速度过快还有更多问题" class="headerlink" title="招聘速度过快还有更多问题"></a>招聘速度过快还有更多问题</h2><p>Hire slow 除了说不要降低标准招人以外，还有一个内涵就是：不要招聘过快。</p><p>初听起来这是反直觉的，业务发展快，团队招人也很顺利，那么快速招兵买马应当是自然之举。但事实并不是这样，和机器可以轻易水平扩容不同，工程师毕竟是人，平时工作的相当比例的时间其实是在沟通协作，扩张过快容易产生一系列问题：</p><ul><li>没有足够资源指导新人适应团队，任其自生自灭导致成长不稳定</li><li>过分多样化的人员背景导致内耗加大，很多项目推进效率低下，争论不已</li><li>团队优秀文化迅速稀释，认同感凝聚感逐步淡化</li></ul><p>正如 Brooks 在“人月神话”中提到的，粗暴加人对于项目进度有可能是负面而非正面影响。</p><p>建设团队，需要“结硬寨，打呆仗”，稳扎稳打，才能在永葆精华的同时，有序提升组织能力。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;我曾亲眼见证过这样一个事情：&lt;/p&gt;
&lt;p&gt;一个内部工具团队，因为业务增长太快压力山大，某个时间点 lead 在招聘方面做出了妥协，放水招进来一个看起来还凑合的工程师。
过了一小段时间，团队里其他人慢慢开始离职。有几个是我非常熟悉的伙伴，平时跟他们沟通不少，大家普遍的一些感触是：我怎么和这样的人一起工作。
再后来，团队雪崩式解散，部门领导不得不抽调其他资源维系业务运转。&lt;/p&gt;
&lt;p&gt;有一次我也跟原来的 lead 闲聊，当谈到为什么当初招那个人的时候，他坦言：业务太多了，内部服务开发又不招人喜欢，没办法。&lt;/p&gt;
&lt;p&gt;这些年来，随着团队管理经验的逐步积累，我越来越意识到师傅曾经教给我们的那句话的重要性：“Hire slow，fire fast”。今天我们就先聊一下前半句：Hire slow。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Recruitment" scheme="https://springuper.github.io/tags/Recruitment/"/>
    
    <category term="Organization" scheme="https://springuper.github.io/tags/Organization/"/>
    
  </entry>
  
  <entry>
    <title>前端中的 Functional Reactive Programming（二）- 进阶版 AutoComplete</title>
    <link href="https://springuper.github.io/frontend-frp-2nd/"/>
    <id>https://springuper.github.io/frontend-frp-2nd/</id>
    <published>2022-03-21T19:34:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>在上一篇文章<a href="https://zhuanlan.zhihu.com/p/77687564">尚春：前端中的 Functional Reactive Programming</a>末尾我们挖了个坑，提出在基于 RxJS 的自动补全实现基础上，怎么样添加如下功能以进一步提升体验：</p><ul><li>失败信息提示</li><li>失败重试，最多重试 2 次，且在新数据到来时取消之前的重试</li><li>添加加载提示，且如果请求完成时间小于 200ms 时不显示</li><li>缓存后端返回结果，可考虑 LRU 等方案</li><li>其它你能想象到的改进</li></ul><p>可能有的小伙伴已经完成了部分挑战，这篇文章让我们一起填坑吧。</p><p><em>友情提示：本文依赖一些之前的背景知识，建议新朋友先看下上篇文章：</em><a href="https://zhuanlan.zhihu.com/p/77687564">尚春：前端中的 Functional Reactive Programming</a></p><h2 id="错误信息展示"><a href="#错误信息展示" class="headerlink" title="错误信息展示"></a>错误信息展示</h2><p>一个显而易见的改进是：在网络出现问题或者搜索字符包含非法字符的情况下展示错误信息，有效传达相关进展给用户。</p><p>前文中我们提到，一个 Observable 的结果状态有成功（complete）和失败（error）两种：<img src="../images/frontend-frp-2nd-01.jpg"></p><p>基于之前的 RxJS 版本 AutoComplete 实现，我们先来改进下请求数据的部分：</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">// Observable from API response</span></span><br><span class="line"><span class="keyword">const</span> searchByValue = <span class="function">(<span class="params">value</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (value.length === <span class="number">0</span>) <span class="keyword">return</span> <span class="keyword">of</span>(&#123;&#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">from</span>(fetch(<span class="string">`/people?keyword=<span class="subst">$&#123;value&#125;</span>`</span>)).pipe(</span><br><span class="line">    switchMap(<span class="function">(<span class="params">response</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (response.ok) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">from</span>(response.json()).pipe(map(<span class="function"><span class="params">data</span> =&gt;</span> (&#123; data &#125;)));</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> throwError(response.status);</span><br><span class="line">    &#125;),</span><br><span class="line">    catchError(<span class="function">(<span class="params">error</span>) =&gt;</span> <span class="keyword">of</span>(&#123; error &#125;))</span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>主要改动如下：</p><span id="more"></span><ul><li>根据<code>response.ok</code>判断后端请求是否成功，相比之前简单依赖<code>fetch</code>来判断请求成功<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch">更准确</a>些</li><li>在<code>response.ok</code>为<code>false</code>时调用 RxJS 的 <a href="https://rxjs.dev/api/index/function/throwError">throwError</a> 运算符抛出封装了错误信息的 Observable</li><li>调用 <a href="https://www.learnrxjs.io/learn-rxjs/operators/error_handling/catch">catchError</a> 运算符来捕获失败 Observable 的错误消息，并包装为<code>&#123; error &#125;</code>的形式方便后续 UI 逻辑统一处理</li></ul><p>在 UI 方面也需要根据新的 Observable 结果稍作调整：</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">// Subscribe and print</span></span><br><span class="line">searchAutoComplete$.subscribe(<span class="function">(<span class="params">result</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> autocomplete_results = <span class="built_in">document</span>.getElementById(<span class="string">&quot;autocomplete-results&quot;</span>);</span><br><span class="line">  autocomplete_results.innerHTML = <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="keyword">if</span> (result.data) &#123;</span><br><span class="line">    <span class="keyword">if</span> (result.data.length === <span class="number">0</span>) &#123;</span><br><span class="line">      autocomplete_results.innerHTML = <span class="string">&quot;&lt;li&gt;no results&lt;/li&gt;&quot;</span>;</span><br><span class="line">    &#125; <span class="keyword">else</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; result.data.length; i++) &#123;</span><br><span class="line">        autocomplete_results.innerHTML += <span class="string">&quot;&lt;li&gt;&quot;</span> + result.data[i] + <span class="string">&quot;&lt;/li&gt;&quot;</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (result.error) &#123;</span><br><span class="line">    autocomplete_results.innerHTML += <span class="string">&quot;&lt;li&gt;error: &quot;</span> + result.error + <span class="string">&quot;&lt;/li&gt;&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>如上，主要的改动是在<code>result.error</code>存在的情况下展示错误信息，效果如图：<img src="../images/frontend-frp-2nd-02.jpg"></p><p>源代码及试用参见 CodeSandbox：<a href="https://codesandbox.io/s/rxjs-autocomplete-error-handling-8spl4h?file=/src/index.js">https://codesandbox.io/s/rxjs-autocomplete-error-handling-8spl4h?file=/src/index.js</a></p><p>出于演示目的，目前我们在错误信息中仅展示了服务端状态码，可以根据实际情况调整。</p><p>通过这一改进，我们可以看出 Observable 对错误情况有足够的支持，包括使用<code>throwError</code>将错误信息封装为 Observable、使用<code>catchError</code>捕获 Observable 源中的错误等。</p><h2 id="失败重试"><a href="#失败重试" class="headerlink" title="失败重试"></a>失败重试</h2><p>在数据请求失败时仅仅展示错误信息仍然不够完美，有时候错误是因为网络不稳定，或者其它临时发生的各种状况导致，这些情况下我们更希望可以由程序自动进行重试，而不是让用户自己完成。</p><p>众所周知，基于 Promise 的原生<code>fetch</code>方法本身并不支持重试，需要借助类似 <a href="https://github.com/jonbern/fetch-retry">fetch-retry</a> 等第三方库解决。RxJS 则提供了多种失败重试机制，方便对出现错误的 Observable 进行适宜的处理。</p><p>为了在 AutoComplete 中支持失败重试，我们首先需要改造下如下部分的代码：</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"><span class="keyword">return</span> <span class="keyword">from</span>(fetch(<span class="string">`/people?keyword=<span class="subst">$&#123;value&#125;</span>`</span>)).pipe(...);</span><br></pre></td></tr></table></figure><p>这段代码的一个问题是：<code>fetch(</code>/people?keyword=${value}``作为一个函数调用，在作为实参传给<code>from</code> 时已经变成了一个处理中的 Promise，由于 Promise 开弓没有回头箭，它是<strong>无法进行重试</strong>的。</p><p>Observable 和 Promise 的核心区别之一是 Observable 默认是<strong>懒执行</strong>的，直到有下游 subscribe 才会触发。因此，改造的核心思路就是将<code>fetch</code>调用进一步封装，放到 Observable 创建函数体中：</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="keyword">const</span> request = <span class="keyword">new</span> Observable(<span class="function">(<span class="params">observer</span>) =&gt;</span> &#123;</span><br><span class="line">  fetch(<span class="string">`/people?keyword=<span class="subst">$&#123;value&#125;</span>`</span>).then(<span class="function">(<span class="params">response</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!response.ok) &#123;</span><br><span class="line">      <span class="keyword">return</span> observer.error(response.status);</span><br><span class="line">    &#125;</span><br><span class="line">    response.json().then(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      observer.next(&#123; data &#125;);</span><br><span class="line">      oberver.complete();</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>RxJS 提供的 <a href="https://www.learnrxjs.io/learn-rxjs/operators/error_handling/retry">retry</a> 操作符，能够在错误发生的时候，通过重新 subscribe 源 Observable 进行重试：<img src="../images/frontend-frp-2nd-03.jpg"></p><p>接下来，让我们把<code>retry</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="comment">// Observable from API response</span></span><br><span class="line"><span class="keyword">const</span> searchByValue = <span class="function">(<span class="params">value</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (value.length === <span class="number">0</span>) <span class="keyword">return</span> <span class="keyword">of</span>(&#123;&#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> request = <span class="keyword">new</span> Observable(<span class="function">(<span class="params">observer</span>) =&gt;</span> &#123;</span><br><span class="line">    fetch(<span class="string">`/people?keyword=<span class="subst">$&#123;value&#125;</span>`</span>).then(<span class="function">(<span class="params">response</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (!response.ok) &#123;</span><br><span class="line">        <span class="keyword">return</span> observer.error(response.status);</span><br><span class="line">      &#125;</span><br><span class="line">      response.json().then(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">        observer.next(&#123; data &#125;);</span><br><span class="line">        observer.complete();</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">return</span> request.pipe(</span><br><span class="line">    timeout(<span class="number">5000</span>), <span class="comment">// if the API request doesn&#x27;t finish in 5000ms, throw a timeout error</span></span><br><span class="line">    retry(<span class="number">2</span>),</span><br><span class="line">    catchError(<span class="function">(<span class="params">error</span>) =&gt;</span> <span class="keyword">of</span>(&#123; error &#125;))</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>retry</code>操作符，RxJS 还提供了更为强大的 <a href="https://rxjs.dev/api/operators/retryWhen">retryWhen</a> 操作符，用于处理更复杂的自定义条件重试。譬如，如果我们只希望在出现请求超时的情况下进行失败重试，则可将<code>retry</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Retry only when timeout error happens</span></span><br><span class="line">retryWhen(<span class="function">(<span class="params">errors</span>) =&gt;</span></span><br><span class="line">  errors.pipe(</span><br><span class="line">    switchMap(<span class="function">(<span class="params">error, index</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (index &lt; <span class="number">2</span> &amp;&amp; (error <span class="keyword">instanceof</span> TimeoutError)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">of</span>(error).pipe(delay(<span class="number">1000</span>)); <span class="comment">// wait 1000ms and then retry</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> throwError(error);</span><br><span class="line">    &#125;)</span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>源代码参见 CodeSandbox：<a href="https://codesandbox.io/s/rxjs-autocomplete-retry-rmcq6o?file=/src/index.js">https://codesandbox.io/s/rxjs-autocomplete-retry-rmcq6o?file=/src/index.js</a></p><h2 id="加载提示"><a href="#加载提示" class="headerlink" title="加载提示"></a>加载提示</h2><p>标准的加载提示非常简单，但不够精雕细琢。我们在这个环节希望给 AutoComplete 加一个这样的 loading 效果：</p><ul><li>如果请求完成时间小于 200ms 时不显示 loading</li><li>否则显示 loading 直到请求完成</li></ul><p>下面是用 RxJS 惯用的弹珠图分别展示请求比较慢和比较快两种情形：<img src="../images/frontend-frp-2nd-04.jpg"><em>请求较慢时，展示 loading 效果</em><img src="../images/frontend-frp-2nd-05.jpg"><em>请求较快时，直接展示结果</em></p><p>为了实现图示的效果，我们首先创建一个 Timer 的 Observable：</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="keyword">const</span> loadingTimer = timer(<span class="number">200</span>).pipe(</span><br><span class="line">  mapTo(&#123; <span class="attr">loading</span>: <span class="literal">true</span> &#125;),</span><br><span class="line">  startWith(&#123; <span class="attr">loading</span>: <span class="literal">false</span> &#125;),</span><br><span class="line">  takeUntil(request),</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>上述代码涉及几个运算符，这里详细介绍下：</p><ul><li><code>timer(200)</code>创建一个 Observable，其会在 200ms 后发送一个数字 0</li><li><code>mapTo</code> 运算符是<code>map</code>运算符的一个简化版本，将 Obervable 封装的数据转化为另一种数据，这里等同于 <code>map(_ =&gt; (&#123; loading: true &#125;))</code></li><li><code>startWith</code>运算符可以在下游 subscribe 的时候立即发送一个数据，用以进行初始化</li><li><code>takeUntil</code>是一个过滤型的运算符，在<code>request</code> 完成时，无论源 Observable 即<code>timer(200)</code>完成与否，都会结束这个 Observable</li></ul><p>通过这些操作符的合作，<code>loadingTimer</code>完成了这样一种效果：</p><ol><li>首先发送一个 <code>&#123; loading: false &#125;</code> 数据</li><li>如果 request 在 200ms 内完成，则直接结束</li><li>否则，会在 200ms 时发送一个<code>&#123; loading: true &#125;</code>数据并结束</li></ol><p>此外，为了分离处理数据的逻辑和处理 loading 效果的逻辑，我们希望在 request 结束时，再发送一个<code>&#123; loading: false &#125;</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">request.pipe(mapTo(&#123; <span class="attr">loading</span>: <span class="literal">false</span> &#125;));</span><br></pre></td></tr></table></figure><p>然后，为了整合 loading 效果逻辑，我们可以使用<code>merge</code>运算符将这两个 Observable 合并为一个：</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> showLoadingIndicator = merge(</span><br><span class="line">  timer(<span class="number">200</span>).pipe(</span><br><span class="line">    mapTo(&#123; <span class="attr">loading</span>: <span class="literal">true</span> &#125;),</span><br><span class="line">    startWith(&#123; <span class="attr">loading</span>: <span class="literal">false</span> &#125;),</span><br><span class="line">    takeUntil(request)</span><br><span class="line">  ),</span><br><span class="line">  request.pipe(</span><br><span class="line">    mapTo(&#123; <span class="attr">loading</span>: <span class="literal">false</span> &#125;)</span><br><span class="line">  ),</span><br><span class="line">).pipe(distinctUntilChanged()); <span class="comment">// if request finishes in 200ms, there will be two </span></span><br><span class="line">                                <span class="comment">// duplicated &#123; loading: false &#125; emitted, so here we use</span></span><br><span class="line">                                <span class="comment">// distinctUntilChanged operator to filter duplicated values</span></span><br></pre></td></tr></table></figure><p>这里有一个潜在的问题：request 作为一个 Observable，目前已经被至少两处地方使用。然而默认的 Observable 是懒执行的，每次 subscribe 都会重新执行内部逻辑。但显然我们在这几处都希望<strong>共享 request 的当前状态</strong>，而非不断进行新的独立的 API 请求。</p><p>为了达成这一效果，我们需要适当改造下 request Observable：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> request.pipe(</span><br><span class="line">  timeout(<span class="number">5000</span>),</span><br><span class="line">  retry(<span class="number">2</span>),</span><br><span class="line">  catchError(<span class="function">(<span class="params">error</span>) =&gt;</span> <span class="keyword">of</span>(&#123; error &#125;)),</span><br><span class="line">  share() <span class="comment">// return a multicast Observable which could be consumed by multiple subscribers,</span></span><br><span class="line">          <span class="comment">// it makes the stream hot</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>这里引入了 share 运算符将 request Observable 变为一个可以由多个 Subscribers 共享的 hot Observable：只要有一个 Subscriber 订阅，则后面的 Subscribers 订阅时也会继续获得第一个 Observable 的后续数据，而非重新开始。</p><p>最后一步是将 request 和 showLoadingIndicator 两个 Observables 合并：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> searchByValue = <span class="function">(<span class="params">value</span>) =&gt;</span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">return</span> merge(showLoadingIndicator, request);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>更新 UI 部分逻辑后，我们第一版加载信息改进就已经完成啦，效果如下：<img src="../images/frontend-frp-2nd-06.jpg"></p><h3 id="锦上添花"><a href="#锦上添花" class="headerlink" title="锦上添花"></a>锦上添花</h3><p>前面的改进都集中在处理单个请求上，对于 AutoComplete，还需要处理用户连续输入等场景，由此引发更多值得注意的问题：</p><ol><li>当现在请求还在继续，loading 信息已展示的情况下，用户输入了新的字符，是否需要继续展示 loading 信息？</li><li>当现在请求还在继续，loading 信息已展示的情况下，用户清除了最后的字符甚至所有字符，应当如何处理？</li><li>请求在 loading 信息刚刚出现时就很快完成，导致 loading 信息闪现，体验比较糟糕，应当如何处理？</li></ol><p>这里没有简单的答案，需要针对性的进行讨论和优化。为了展示下 RxJS 解决这类复杂问题的优秀能力，我们尝试解决一下第一个问题。</p><p>假设我们的方案是：在当前请求还在进行且 loading 信息已展示的情况下，用户又输入了新的字符，继续展示 loading 信息，直到新的请求完成。</p><p>下面我们推演下整个流程：</p><ol><li>第一个请求发出</li><li>Observable 发出<code>&#123; loading: false &#125;</code>数据</li><li>200ms 后请求未完成，Observable 发出<code>&#123; loading: true &#125;</code> 数据</li><li>用户输入新字符</li><li>第二个请求发出</li><li>Observable 发出<code>&#123; loading: false &#125;</code>数据，但在这种情况下，我们不希望隐藏 loading 信息</li></ol><p>为了达成这一效果，需要有能力对 Observable 连续产生多个值之间进行逻辑处理，即<code>&#123; loading: true &#125;</code> 后面如果紧跟着<code>&#123; loading: false &#125;</code>的话，则将该值舍弃或者变更为<code>&#123; loading: true &#125;</code>。</p><p>对应到 RxJS，我们可以结合使用 <code>startWith</code>、<code>pariwise</code>和<code>map</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Observable from keyup event</span></span><br><span class="line"><span class="keyword">const</span> obs = fromEvent(<span class="built_in">document</span>.querySelector(<span class="string">&quot;input&quot;</span>), <span class="string">&quot;keyup&quot;</span>).pipe(</span><br><span class="line">  map(<span class="function">(<span class="params">e</span>) =&gt;</span> e.target.value),</span><br><span class="line">  tap(<span class="function">(<span class="params">x</span>) =&gt;</span> <span class="built_in">console</span>.log(<span class="string">&quot;input:&quot;</span>, x, <span class="built_in">Date</span>.now())),</span><br><span class="line">  debounceTime(<span class="number">200</span>),</span><br><span class="line">  distinctUntilChanged(),</span><br><span class="line">  switchMap(searchByValue),</span><br><span class="line"></span><br><span class="line">  startWith(&#123;&#125;),</span><br><span class="line">  pairwise(),</span><br><span class="line">  map(<span class="function">(<span class="params">pair</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// if the latest two emitted values are &#123; loading: true &#125; and &#123; loading: false &#125;,</span></span><br><span class="line">    <span class="comment">// it means a new API request starts while the last request is still ongoing,</span></span><br><span class="line">    <span class="comment">// so here we return &#123; loading: true &#125; to keep showing the loading indicator</span></span><br><span class="line">    <span class="keyword">if</span> (pair[<span class="number">0</span>].loading === <span class="literal">true</span> &amp;&amp; pair[<span class="number">1</span>].loading === <span class="literal">false</span>) <span class="keyword">return</span> pair[<span class="number">0</span>];</span><br><span class="line">    <span class="keyword">return</span> pair[<span class="number">1</span>];</span><br><span class="line">  &#125;),</span><br><span class="line"></span><br><span class="line">  distinctUntilChanged()</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>源代码可以参阅 CodeSandbox：<a href="https://codesandbox.io/s/rxjs-autocomplete-loading-sxeb6g?file=/src/index.js">RxJS AutoComplete Loading - CodeSandbox</a></p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>通过上述三项改进，我们不能看出，一方面精益求精是永无止境的，只要场景足够复杂，对体验的要求足够高，我们总是可以找出更多的可以改善的地方；另一方面，也可以看出 RxJS 优异的抽象能力和丰富的工具集，能够帮我们将复杂的异步问题简单化，用为数不多的代码实现一个又一个有挑战的功能。诚然 RxJS 的门槛非常高，Observable、Subscriber、Subject 还有繁多的 operators 足够复杂，不过假以时日，经过岁月和经历的磨练，相信你也可以某种程度上掌握它的精髓。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ol><li><a href="https://stackoverflow.com/a/56361092/1131882">Loading indication with a delay and anti-flickering in RxJS</a></li><li><a href="https://stackoverflow.com/a/33072989/1131882">Rx fromPromise and retry</a></li><li><a href="https://www.educative.io/edpresso/what-is-the-rxjs-retry-operator-and-how-does-it-work">What is the RxJs Retry Operator and how does it work?</a></li><li><a href="https://medium.com/javascript-everyday/rxjs-retrywhen-operator-15e3c83b97eb">RxJS: retryWhen operator</a></li><li><a href="https://netbasal.com/understanding-rxjs-multicast-operators-77b3f60af0a2">Understanding RxJS Multicast Operators</a></li><li><a href="https://medium.com/@bencabanes/marble-testing-observable-introduction-1f5ad39231c">Marble testing Observable Introduction</a></li></ol><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>异步逻辑难以思考，针对异步逻辑的测试更是难上加难。为了便于开发者写出更为<strong>正确</strong>的代码，RxJS 提供了<a href="https://rxjs.dev/guide/testing/marble-testing">弹珠测试</a>的方案。</p><p>该方案基于文本化表示的弹珠图，并提供了一系列便捷的 API 支持各种场景的测试需求。弹珠图类似我们前面贴出来的很多图片，但毕竟是在代码中，因此是文本化的：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># emit value &#x27;a&#x27; at 30ms, &#x27;b&#x27; at 60ms, and complete at 90ms</span></span><br><span class="line">--a--b--|</span><br><span class="line"></span><br><span class="line"><span class="comment"># emit &#x27;a&#x27; at 40ms then an error at 70ms</span></span><br><span class="line">—--a--<span class="comment">#</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># emit &#x27;a&#x27; and &#x27;b&#x27; and &#x27;c&#x27; at 30ms, and complete at 50ms</span></span><br><span class="line">--(abc)-|</span><br></pre></td></tr></table></figure><p>API 包括使用上述文本弹珠图创建 Observable 的<code>cold</code>和<code>hot</code>方法，以及<code>TestScheduler</code>、<code>expectObservable</code>、<code>flush</code>等辅助方法。</p><p>下面我们使用 jasmine-marbles 展示一些弹珠测试的示例：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; cold, getTestScheduler, hot &#125; <span class="keyword">from</span> <span class="string">&#x27;jasmine-marbles&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; interval &#125; <span class="keyword">from</span> <span class="string">&#x27;rxjs&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; falter, map, switchMap, take &#125; <span class="keyword">from</span> <span class="string">&#x27;rxjs/operators&#x27;</span>;</span><br><span class="line"></span><br><span class="line">describe(<span class="string">&#x27;Marble testing operators&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  describe(<span class="string">&#x27;Map&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    it(<span class="string">&#x27;should add &quot;1&quot; to each value emitted&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> values = &#123; <span class="attr">a</span>: <span class="number">1</span>, <span class="attr">b</span>: <span class="number">2</span>, <span class="attr">c</span>: <span class="number">3</span>, <span class="attr">x</span>: <span class="number">2</span>, <span class="attr">y</span>: <span class="number">3</span>, <span class="attr">z</span>: <span class="number">4</span> &#125;;</span><br><span class="line">      <span class="keyword">const</span> source = cold(<span class="string">&#x27;-a-b-c-|&#x27;</span>, values);</span><br><span class="line">      <span class="keyword">const</span> expected = cold(<span class="string">&#x27;-x-y-z-|&#x27;</span>, values);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> result = source.pipe(map(<span class="function"><span class="params">x</span> =&gt;</span> x + <span class="number">1</span>));</span><br><span class="line">      expect(result).toBeObservable(expected);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  describe(<span class="string">&#x27;SwitchMap&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    it(<span class="string">&#x27;should maps each value to inner observable and flattens&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> values = &#123; <span class="attr">a</span>: <span class="number">10</span>, <span class="attr">b</span>: <span class="number">30</span>, <span class="attr">x</span>: <span class="number">20</span>, <span class="attr">y</span>: <span class="number">40</span> &#125;;</span><br><span class="line">      <span class="keyword">const</span> obs1 = cold(    <span class="string">&#x27;-a-----a--b-|&#x27;</span>, values);</span><br><span class="line">      <span class="keyword">const</span> obs2 = cold(    <span class="string">&#x27;a-a-a|&#x27;</span>, values);</span><br><span class="line">      <span class="keyword">const</span> expected = cold(<span class="string">&#x27;-x-x-x-x-xy-y-y|&#x27;</span>, values);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> result = obs1.pipe(switchMap(<span class="function"><span class="params">x</span> =&gt;</span> obs2.pipe(map(<span class="function"><span class="params">y</span> =&gt;</span> x + y))));</span><br><span class="line">      expect(result).toBeObservable(expected);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  describe(<span class="string">&#x27;Interval&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    it(<span class="string">&#x27;should keeps only even numbers&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> source = interval(<span class="number">10</span>, getTestScheduler()).pipe(</span><br><span class="line">        take(<span class="number">10</span>),</span><br><span class="line">        filter(<span class="function"><span class="params">x</span> =&gt;</span> x % <span class="number">2</span> === <span class="number">0</span>),</span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">const</span> expected = cold(<span class="string">&#x27;-a-b-c-d-e|&#x27;</span>, &#123; <span class="attr">a</span>: <span class="number">0</span>, <span class="attr">b</span>: <span class="number">2</span>, <span class="attr">c</span>: <span class="number">4</span>, <span class="attr">d</span>: <span class="number">6</span>, <span class="attr">e</span>: <span class="number">8</span> &#125;);</span><br><span class="line"></span><br><span class="line">      expect(source).toBeObservable(expected);</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><blockquote><p><em>When you need to test asynchrone code, Marble testing can be an elegant way to test it. It will be more visual, clearer and with less code.</em></p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;在上一篇文章&lt;a href=&quot;https://zhuanlan.zhihu.com/p/77687564&quot;&gt;尚春：前端中的 Functional Reactive Programming&lt;/a&gt;末尾我们挖了个坑，提出在基于 RxJS 的自动补全实现基础上，怎么样添加如下功能以进一步提升体验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;失败信息提示&lt;/li&gt;
&lt;li&gt;失败重试，最多重试 2 次，且在新数据到来时取消之前的重试&lt;/li&gt;
&lt;li&gt;添加加载提示，且如果请求完成时间小于 200ms 时不显示&lt;/li&gt;
&lt;li&gt;缓存后端返回结果，可考虑 LRU 等方案&lt;/li&gt;
&lt;li&gt;其它你能想象到的改进&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能有的小伙伴已经完成了部分挑战，这篇文章让我们一起填坑吧。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;友情提示：本文依赖一些之前的背景知识，建议新朋友先看下上篇文章：&lt;/em&gt;
&lt;a href=&quot;https://zhuanlan.zhihu.com/p/77687564&quot;&gt;尚春：前端中的 Functional Reactive Programming&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;错误信息展示&quot;&gt;&lt;a href=&quot;#错误信息展示&quot; class=&quot;headerlink&quot; title=&quot;错误信息展示&quot;&gt;&lt;/a&gt;错误信息展示&lt;/h2&gt;&lt;p&gt;一个显而易见的改进是：在网络出现问题或者搜索字符包含非法字符的情况下展示错误信息，有效传达相关进展给用户。&lt;/p&gt;
&lt;p&gt;前文中我们提到，一个 Observable 的结果状态有成功（complete）和失败（error）两种：
&lt;img src=&quot;../images/frontend-frp-2nd-01.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;基于之前的 RxJS 版本 AutoComplete 实现，我们先来改进下请求数据的部分：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Observable from API response&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; searchByValue = &lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;value&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (value.length === &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;) &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;of&lt;/span&gt;(&amp;#123;&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt;(fetch(&lt;span class=&quot;string&quot;&gt;`/people?keyword=&lt;span class=&quot;subst&quot;&gt;$&amp;#123;value&amp;#125;&lt;/span&gt;`&lt;/span&gt;)).pipe(&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    switchMap(&lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;response&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (response.ok) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt;(response.json()).pipe(map(&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;params&quot;&gt;data&lt;/span&gt; =&amp;gt;&lt;/span&gt; (&amp;#123; data &amp;#125;)));&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; throwError(response.status);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;),&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    catchError(&lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;error&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;of&lt;/span&gt;(&amp;#123; error &amp;#125;))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  );&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;主要改动如下：&lt;/p&gt;</summary>
    
    
    
    
    <category term="Functional Programming" scheme="https://springuper.github.io/tags/Functional-Programming/"/>
    
    <category term="Functional Reactive Programming" scheme="https://springuper.github.io/tags/Functional-Reactive-Programming/"/>
    
    <category term="RxJS" scheme="https://springuper.github.io/tags/RxJS/"/>
    
  </entry>
  
  <entry>
    <title>面试是双向的</title>
    <link href="https://springuper.github.io/interview-is-bidirectional/"/>
    <id>https://springuper.github.io/interview-is-bidirectional/</id>
    <published>2022-01-31T15:34:13.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>2018 年一段真实的对话：</p><ul><li>小 A：你好！非常抱歉，我考虑再三，决定还是加入另外一家公司</li><li>我：（内心有点小崩溃，但是还想再抢救一下）嗯嗯，替你感到高兴！不过方便再问一下，你最期望在下一份工作中得到什么呢？</li><li>小 A：主要还是想在技术方面有一些成长吧</li><li>我：（感觉我们还是有点竞争力）面试是双向的，可以的话，我建议你再比较下两边的面试体验，看下哪边的工程师技术更好些</li><li>小 A：ok，那我再考虑一下</li></ul><p>几天后：</p><ul><li>小 A：我觉得还是你们这边技术方面更好些，所以我改变了主意，准备加入你们</li></ul><p>后来小 A 入职后，成长速度的确非常非常好，业余爱好也是一个没落下，说是事业爱好双丰收也不为过。类似的“话术”后面我也用过几次，在候选人的几个选项比较接近时，成功率还是蛮高的。</p><span id="more"></span><h2 id="在面试候选人的同时，候选人也在考察面试官"><a href="#在面试候选人的同时，候选人也在考察面试官" class="headerlink" title="在面试候选人的同时，候选人也在考察面试官"></a>在面试候选人的同时，候选人也在考察面试官</h2><p>一般人理解面试官总是高人一等的，牢牢把握着面试中的主动权。究其根本，这种先入为主的偏见其实往往来源于层级分明的公司文化中：</p><ul><li>一切问题，老大说了算</li><li>高 P 总是掌握着更多的资源和决策权</li><li>新来的是龙得盘着点，是虎得卧着点，轮不到你 BB</li></ul><p>在这种文化中，人才是不太那么受重视的。面试的时候，既然你是过来讨口饭吃，那么居高临下去审视你当然就是理所当然的了。面试中也因此出现各种乱象：</p><ul><li>侧重寻找不足而非挖掘亮点</li><li>代码可以不写，但是一定要能说</li><li>偏好算法、脑筋急转弯，轻视工程实践、问题解决能力</li><li>迟到、早退、和候选人没有实质互动</li><li>简单粗暴不专业，被质疑的时候气急败坏</li></ul><p>面试作为候选人和公司进行深入交流的重要窗口，其实不只是面试官们在评估候选人是不是适合特定岗位，与此同时，候选人其实也在通过面试体验来审视这家公司是不是适合自己：</p><ul><li>面试官一般是将来的团队骨干，他们是不是足够资深，自己跟着他们混有没有前途？</li><li>面试官对自己从事的工作是不是眼中有光，激情投入？</li><li>当自己遇到了困惑，或者情绪上有些紧张的时候，面试官是不是在引导和调整自己的状态，以更好地体现出自己的真实水平？如果是的话，那么这会是一个很 supportive 的团队</li><li>面试体验一定程度上体现出了公司的文化，自己是不是喜欢接下来三年五年在这样的文化里工作？</li></ul><p>简而言之，候选人会去考虑自己是不是想和这样一批人共事。</p><h2 id="如何更好适应双向的面试？"><a href="#如何更好适应双向的面试？" class="headerlink" title="如何更好适应双向的面试？"></a>如何更好适应双向的面试？</h2><p>作为如此重要的双方交流窗口，如何确保面试能够筛选出我们需要的人才，同时也能够提升候选人的体验和评价呢？我有如下几点思考。</p><h3 id="营造舒适平等的面试氛围"><a href="#营造舒适平等的面试氛围" class="headerlink" title="营造舒适平等的面试氛围"></a>营造舒适平等的面试氛围</h3><p>先说舒适。</p><p>很多候选人在面试开始的时候，或者遇到了一些挑战，都会有一些紧张。实话实话，我自己为数不多的面试里，就好多次比较紧张，导致没有发挥出应有水平。因此，在面试刚刚开始的时候，最好有一些举措让整个氛围热络起来：</p><ul><li>给候选人倒杯水，然后聊下今天来的路上堵不堵</li><li>简单评价下简历上的一些亮点，譬如拿过很多次奖学金、ACM 排名等等，表达对候选人的认可</li><li>面试官自我介绍下，同时也介绍下该轮面试的主要流程</li></ul><p>基本就是套套近乎，为接下来的面试主题做些铺垫，帮助对方进入状态。</p><p>在面试中间如果发现候选人还是非常紧张，不太在状态，可以给一点小提示，或者其它的合理方式帮助对方降压。如果还是很难调整过来，可以再约面试。</p><p>另外一点也很重要：平等。</p><p>能够进入面试流程的候选人基本也是有货的，作为招聘方，既然我们希望能够招揽人才，那么就不要抱着高人一等的态度。反过来，对于一些非常出色的候选人，也大可不必太过谦卑，双方有利益共同点才是真正稳固的合作。</p><p>我们应该总是秉持一种平等的心态去沟通和交流。正如在后面的工作中一样，团队负责人只是一个协助大家发挥自身所长的角色而已。具体实践中有很多可以体现的点：</p><ul><li>沟通方式比较专业、和睦、不卑不亢</li><li>一些问题点候选人有疑问的时候，面试官应该与其交流，而非单纯让其自己考虑，冷眼相对</li><li>最后预留一些时间，一般五分钟左右，专门回答候选人的问题</li></ul><p>通过营造这样一个愉悦的氛围，搭建好一个候选人能够高水平发挥的舞台，为接下来面试提供良好的基础。</p><h3 id="努力挖掘相关方向的亮点"><a href="#努力挖掘相关方向的亮点" class="headerlink" title="努力挖掘相关方向的亮点"></a>努力挖掘相关方向的亮点</h3><p>好的面试题应该是怎样的？</p><p>不同的人有不同的解答。实践中，我们也发现很多人感慨“面试造火箭，入职扭螺丝”，面试内容和实际工作大相径庭。既然这么多人吐槽，那么显然这不太像好的面试题。还有一些候选人比较喜欢“应试”，刷题几百道后，面试 so easy，然而这样的技能在实际工作中真得有用吗？</p><p>目前我对好面试题的理解是这样的：</p><ul><li>来源于相关领域实践，但有一定拔高</li><li>有较好的层次性，很容易想到一些解决办法，且有多种层层递进的进阶方案</li><li>能够让有才华的候选人举一反三，融会贯通</li></ul><p>譬如说前端的一个架构能力考核面试中，可以出一个题目：如何做一个微博首页。这是一个非常开放的题目，需要候选人去考虑实际场景、核心要求、团队结构、现有技术栈等去权衡各种方案的利弊。每当候选人有了一定的见解后，面试官可以选择一些要点继续深挖，直到逼近候选人的极限为止。如果遇到难得的高水平人才，我们可以继续讨论有所关联的类似产品但不同场景、不同产品类似场景等多种方案，看对方是否融会贯通，高屋建瓴。</p><p>另外，相比于挖掘对方的缺点，一种更好的方式还是抓亮点。我们需要知道候选人的不足，但这些不是能够提升我们团队能力的关键。构建一个高绩效团队和木桶理论不太相关，团队在某个方面的天花板更大程度地体现在大家的专长上。因此，抓亮点是关键所在。准确地抓亮点是设计面试题目的目标，如果面试题目和实际问题相去甚远、没有任何层次只是闭合性问题、没有一通百通而只适用特定领域，那么这样的题目就不能达成抓亮点的目标。</p><h3 id="专业的全流程体验"><a href="#专业的全流程体验" class="headerlink" title="专业的全流程体验"></a>专业的全流程体验</h3><p>面试整个流程不只有面试官，还有人力部门、当地主管等等众多角色参与。某个环节掉链子的话，也会对面试体验有一定程度负面影响。譬如：</p><ul><li>某个新手面试官上来就引用编程语言问题晾了候选人一会儿。这可能需要更多事前培训和传帮带流程</li><li>约好的面试时间发现会议室被占用。这可能需要公司办公系统方面的支持</li><li>Offer 沟通流程过长。这可能需要决策人员和 HR 团队的紧密配合</li></ul><p>没有完美的流程，任何事情不可能一蹴而就。本着招贤纳士的根本目标，不断优化和完善全流程体验，才会赢得越来越多候选人的芳心。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>人才是公司最宝贵的资源，面试是招揽人才最为重要的渠道。打造高水平的面试体验难度很大，但是非常物有所值。期望我的一点思考能够对你有所帮助，也欢迎一起探讨面试的相关问题。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;2018 年一段真实的对话：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小 A：你好！非常抱歉，我考虑再三，决定还是加入另外一家公司&lt;/li&gt;
&lt;li&gt;我：（内心有点小崩溃，但是还想再抢救一下）嗯嗯，替你感到高兴！不过方便再问一下，你最期望在下一份工作中得到什么呢？&lt;/li&gt;
&lt;li&gt;小 A：主要还是想在技术方面有一些成长吧&lt;/li&gt;
&lt;li&gt;我：（感觉我们还是有点竞争力）面试是双向的，可以的话，我建议你再比较下两边的面试体验，看下哪边的工程师技术更好些&lt;/li&gt;
&lt;li&gt;小 A：ok，那我再考虑一下&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;几天后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小 A：我觉得还是你们这边技术方面更好些，所以我改变了主意，准备加入你们&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后来小 A 入职后，成长速度的确非常非常好，业余爱好也是一个没落下，说是事业爱好双丰收也不为过。
类似的“话术”后面我也用过几次，在候选人的几个选项比较接近时，成功率还是蛮高的。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Interview" scheme="https://springuper.github.io/tags/Interview/"/>
    
  </entry>
  
  <entry>
    <title>前端中的 Functional Reactive Programming</title>
    <link href="https://springuper.github.io/frontend-frp/"/>
    <id>https://springuper.github.io/frontend-frp/</id>
    <published>2022-01-18T10:10:23.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>由于思维方式上的显著差异，以 RxJS 为代表的函数式响应式编程（Functional Reactive Programming，简称 FRP）在前端工程师中有些“水土不服”，喜欢的推崇备至，然而对于更多开发者来说它却像“阳春白雪”一般，叫好不叫座。</p><p>本文尝试从一个实际案例开始，通过对比普通实现和基于 RxJS 的版本阐释 FRP 的核心要义，之后简要介绍 RxJS 的核心概念，以及一些实际示例。希望能够帮助一些还在观望的同学更进一步了解这种编程范式。</p><h2 id="AutoComplete"><a href="#AutoComplete" class="headerlink" title="AutoComplete"></a>AutoComplete</h2><p>自动补全是很多前端工程师都曾接触过的实用功能：当用户在文本框中输入字符时，将当前输入内容传递给后端服务拿到可能的自动补全结果，并在文本框下方列出供用户选择。<img src="../images/frontend-frp-01.jpg"></p><h3 id="普通实现"><a href="#普通实现" class="headerlink" title="普通实现"></a>普通实现</h3><p>比较传统的实现思路非常直接，直接监听文本框的 keyup 事件，然后取数据后更新 UI 即可。代码如下：</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">const</span> input = <span class="built_in">document</span>.querySelector(<span class="string">&quot;input&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1. Bind keyup event</span></span><br><span class="line">input.addEventListener(<span class="string">&quot;keyup&quot;</span>, <span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> keyword = event.target.value;</span><br><span class="line">  <span class="keyword">const</span> autocomplete_results = <span class="built_in">document</span>.getElementById(<span class="string">&quot;autocomplete-results&quot;</span>);</span><br><span class="line"></span><br><span class="line">  autocomplete_results.innerHTML = <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="keyword">if</span> (keyword.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 2. Fetch autocomplete list</span></span><br><span class="line">    fetch(<span class="string">`/people?keyword=<span class="subst">$&#123;keyword&#125;</span>`</span>)</span><br><span class="line">      .then(<span class="function">(<span class="params">response</span>) =&gt;</span> response.json())</span><br><span class="line">      .then(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// 3. Update UI</span></span><br><span class="line">        autocomplete_results.innerHTML = <span class="string">&quot;&quot;</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; data.length; i++) &#123;</span><br><span class="line">          autocomplete_results.innerHTML += <span class="string">&quot;&lt;li&gt;&quot;</span> + data[i] + <span class="string">&quot;&lt;/li&gt;&quot;</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><span id="more"></span><p>有经验的朋友可能会注意到上述实现的一些潜在问题：</p><ol><li>每一个 keyup 事件都会发送请求到后端，当用户连续输入时前面的请求并没有必要，只有最后一个请求是用户真正想要的结果</li><li>由于后端接口响应时间的不确定，有可能出现一些竞态危害（race condition）：譬如用户前后的两个输入内容分别为 <code>ma</code> 和 <code>mar</code>，有可能 <code>ma</code> 对应的后端请求完成时间晚于 <code>mar</code> ，先发后至，导致最终展现的结果是 <code>ma</code> 的补全数据</li></ol><p>第一个问题解决起来并不难，一般我们引入<code>debounce</code> 来削减不必要的 keyup 回调执行即可。第二个问题更有挑战一些，不过也难不倒大家。一般可以通过加入哨兵变量、判定条件等方式，验证下后端请求的 <code>keyword</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> input = <span class="built_in">document</span>.querySelector(<span class="string">&quot;input&quot;</span>);</span><br><span class="line"><span class="comment">// Debounce callback</span></span><br><span class="line"><span class="keyword">const</span> debouncedCallback = debounce(<span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> keyword = event.target.value;</span><br><span class="line">  <span class="keyword">const</span> autocomplete_results = <span class="built_in">document</span>.getElementById(<span class="string">&quot;autocomplete-results&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (keyword.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    fetch(<span class="string">`/people?keyword=<span class="subst">$&#123;keyword&#125;</span>`</span>)</span><br><span class="line">      .then(<span class="function">(<span class="params">response</span>) =&gt;</span> response.json())</span><br><span class="line">      .then(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// Avoid race condition</span></span><br><span class="line">        <span class="keyword">if</span> (keyword !== input.value) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">        autocomplete_results.innerHTML = <span class="string">&quot;&quot;</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; data.length; i++) &#123;</span><br><span class="line">          autocomplete_results.innerHTML += <span class="string">&quot;&lt;li&gt;&quot;</span> + data[i] + <span class="string">&quot;&lt;/li&gt;&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    autocomplete_results.innerHTML = <span class="string">&quot;&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;, <span class="number">200</span>);</span><br><span class="line">input.addEventListener(<span class="string">&#x27;keyup&#x27;</span>, debouncedCallback);</span><br></pre></td></tr></table></figure><p>这一版本的实现基本能够应对大部分实际场景。然而随着业务和体验要求的进一步提高，再添加诸如失败重试、加载图标、请求去重、数据缓存等功能时，就可能遇到由于添加越来越多类似 <code>debounce</code>、哨兵变量所带来的维护性灾难。</p><p>综合普通实现的迭代过程，我们不难发现如下几个问题：</p><ul><li>难以一次写对，由于异步所带来的一些隐含问题需要通过<code>debounce</code>、防御 race condition 等解决，心智负担高</li><li>代码偏指令式编程（imperative programming），比较脆弱难维护</li><li>添加进阶功能成本高，堪比浮沙筑高楼</li></ul><h3 id="基于-RxJS-的实现"><a href="#基于-RxJS-的实现" class="headerlink" title="基于 RxJS 的实现"></a>基于 RxJS 的实现</h3><p>在进一步介绍 RxJS 之前，我们先来看下应用它实现的自动补全代码：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Observable from API response</span></span><br><span class="line"><span class="keyword">const</span> searchByValue = <span class="function">(<span class="params">value</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (value.length === <span class="number">0</span>) <span class="keyword">return</span> <span class="keyword">of</span>([]);</span><br><span class="line">  <span class="keyword">const</span> request = fetch(<span class="string">`/people?keyword=<span class="subst">$&#123;value&#125;</span>`</span>).then(<span class="function">(<span class="params">response</span>) =&gt;</span> response.json());</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">from</span>(request);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Observable from keyup event</span></span><br><span class="line"><span class="keyword">const</span> searchAutoComplete$ = fromEvent(<span class="built_in">document</span>.querySelector(<span class="string">&quot;input&quot;</span>), <span class="string">&quot;keyup&quot;</span>)</span><br><span class="line">  .pipe(</span><br><span class="line">    map(<span class="function">(<span class="params">event</span>) =&gt;</span> event.target.value),</span><br><span class="line">    debounceTime(<span class="number">200</span>),</span><br><span class="line">    distinctUntilChanged(),</span><br><span class="line">    switchMap(searchByValue),</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line"><span class="comment">// Subscribe and print</span></span><br><span class="line">searchAutoComplete$.subscribe(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> autocomplete_results = <span class="built_in">document</span>.getElementById(<span class="string">&quot;autocomplete-results&quot;</span>);</span><br><span class="line">  autocomplete_results.innerHTML = <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; data.length; i++) &#123;</span><br><span class="line">    autocomplete_results.innerHTML += <span class="string">&quot;&lt;li&gt;&quot;</span> + data[i] + <span class="string">&quot;&lt;/li&gt;&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>虽然代码行数接近，且有一些 RxJS 引入的操作，但即便对于新手来说，仍然可以看出一些不同之处：</p><ul><li><strong>逻辑分割清晰</strong></li><li><strong>声明式 API</strong> 从<code>pipe</code> 开始的几行管线操作部分代码非常直观，分别进行了事件数据提取（<code>map</code>）、防抖（<code>debounceTime</code>）、过滤无变更输入（<code>distinctUntilChanged</code>）和切换映射（<code>switchMap</code>）操作，从而将原始的 <code>keyup</code> 事件变成了异步的自动补全数据流</li></ul><p>还有一点是函数式响应式编程特别提倡的：</p><blockquote><p>Programming in the style of functional reactive programming means to specify the dynamic behavior of a value completely at the time of declaration.<br><em>from: The Model-View-Controller Pattern and Functional Reactive Programming, by Heinrich Apfelmus</em></p></blockquote><p>相较于命令式的意大利面条般的代码，函数式响应式编程希望能够<strong>将一个异步数据流的所有动态行为在声明时即全部确定</strong>，正如上例的中间核心部分。这无疑大大提高了代码的健壮性和可维护性。</p><p>怎么样？看到这里，是不是感觉 RxJS 还是有一些用武之地，有了些学习的动力？下一个章节，让我们开始进一步了解下它吧。</p><h2 id="RxJS-核心概念"><a href="#RxJS-核心概念" class="headerlink" title="RxJS 核心概念"></a>RxJS 核心概念</h2><p>RxJS 是函数式响应式编程在前端领域的典型代表。</p><blockquote><p>Reactive programming is programming with asynchronous data streams.</p></blockquote><p>响应式编程是围绕<strong>异步数据流</strong>建立的一种编程范式，由于天生契合函数式思维，因此常见的主要流派又称作函数式响应式编程。在前端领域中，因为本身围绕人机交互场景，异步数据流可谓举目皆是：</p><ul><li>异步请求</li><li>键盘、鼠标事件</li><li>窗口滚动</li><li>定时器</li><li>动画</li></ul><p>更广义地讲，类似物理学中静止是运动的特殊情况，所有同步操作也是异步数据流的特殊情况，可以被其建模和处理。因此函数式响应式编程可以用以解决前端领域中大多数问题，当然它最适合有一定复杂度的异步数据流操作。</p><h3 id="Observable"><a href="#Observable" class="headerlink" title="Observable"></a>Observable</h3><p>作为 RxJS 最基础的概念，Observable 是对所有异步数据流的抽象，它一般包括：</p><ul><li>随时间变化的一系列离散数据</li><li><em>[可选]</em> 异常。这会导致 Observable 终止，即再也不会有后续数据</li><li>*[可选] *完成。类似的，这会导致 Observable 终止<img src="../images/frontend-frp-02.jpg"></li></ul><p><em>Observable</em></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="keyword">from</span>, fromEvent, interval, <span class="keyword">of</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;rxjs&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Emits document click events</span></span><br><span class="line"><span class="keyword">const</span> documentClick$ = fromEvent(<span class="built_in">document</span>, <span class="string">&#x27;click&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Emit result of promise</span></span><br><span class="line"><span class="keyword">const</span> promiseSource$ = <span class="keyword">from</span>(<span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="params">resolve</span> =&gt;</span> resolve(<span class="string">&#x27;Hello World!&#x27;</span>)));</span><br><span class="line"></span><br><span class="line"><span class="comment">// Emits data in an array one by one</span></span><br><span class="line"><span class="keyword">const</span> dataSource$ = <span class="keyword">of</span>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Emit value in sequence every 1 second</span></span><br><span class="line"><span class="keyword">const</span> intervalSource$ = interval(<span class="number">1000</span>);</span><br></pre></td></tr></table></figure><p>除了以上 RxJS 提供的内置构造工具方法外，还可以自定义 Observable：</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> finiteSource$ = <span class="keyword">new</span> Observable(<span class="function"><span class="params">subscriber</span> =&gt;</span> &#123;</span><br><span class="line">  subscriber.next(<span class="number">1</span>);</span><br><span class="line">  subscriber.next(<span class="number">2</span>);</span><br><span class="line">  <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    subscriber.next(<span class="number">3</span>);</span><br><span class="line">    subscriber.complete();</span><br><span class="line">  &#125;, <span class="number">1000</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> infiniteSource$ = <span class="keyword">new</span> Observable(<span class="function"><span class="params">subscriber</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">let</span> i = <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">    subscriber.next(i++);</span><br><span class="line">  &#125;, <span class="number">1000</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>可谓是<strong>世间万物皆 Observable</strong>！</p><p>我们之前有个非常熟悉的概念 Promise，Observable 和它有几点<a href="https://stackoverflow.com/a/37365955">不同</a>：</p><ul><li>Promise 处理的是单值，而 Observable 可以处理数据流，即随时间变化的多个值</li><li>Promise 只有成功和失败，而 Observable 还可以取消、重试</li><li>Promise 创建时传入的<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise#%E5%88%9B%E5%BB%BApromise">处理器函数</a>（executer method）会被立即执行，而 Observable 则是惰性的 譬如<code>fromEvent(document,&#39;click&#39;)</code> 并没有马上在文档上绑定<code>click</code>事件回调，而是会一直等到有数据消费者，具体在下一小节中解释</li></ul><p>现在有了数据流，相当于数据的生产者，接下来让我们看下如何消费数据。</p><h3 id="Subscription"><a href="#Subscription" class="headerlink" title="Subscription"></a>Subscription</h3><p>作为数据消费方，订阅器是让<strong>一切流动起来</strong>的关键。可以把它想象成一个水龙头，Observable 是水箱里的水，当希望接水的时候，你要做的就是——拧开水龙头。自然而然，也可以在需要的时候关闭水龙头。<img src="../images/frontend-frp-03.jpg"></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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Emits data in an array one by one</span></span><br><span class="line"><span class="keyword">const</span> dataSource$ = <span class="keyword">of</span>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>);</span><br><span class="line"><span class="keyword">const</span> dataSubscription = dataSource$.subscribe(<span class="function"><span class="params">x</span> =&gt;</span> <span class="built_in">console</span>.log(x));</span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// 1</span></span><br><span class="line"><span class="comment">// 2</span></span><br><span class="line"><span class="comment">// 3</span></span><br><span class="line"><span class="comment">// 4</span></span><br><span class="line"><span class="comment">// 5</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Emit result of promise</span></span><br><span class="line"><span class="keyword">const</span> promiseSource$ = <span class="keyword">from</span>(<span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="params">resolve</span> =&gt;</span> resolve(<span class="string">&#x27;Hello World!&#x27;</span>)));</span><br><span class="line"><span class="keyword">const</span> promiseSubscription = promiseSource$.subscribe(&#123;</span><br><span class="line">  <span class="function"><span class="title">next</span>(<span class="params">x</span>)</span> &#123; <span class="built_in">console</span>.log(x) &#125;</span><br><span class="line">  <span class="function"><span class="title">error</span>(<span class="params">reason</span>)</span> &#123; <span class="built_in">console</span>.error(reason) &#125;</span><br><span class="line">  <span class="function"><span class="title">complete</span>(<span class="params"></span>)</span> &#123; <span class="built_in">console</span>.log(<span class="string">&#x27;Done!&#x27;</span>) &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// &quot;Hello World!&quot;</span></span><br><span class="line"><span class="comment">// &quot;Done!&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Emit value in sequence every 1 second</span></span><br><span class="line"><span class="keyword">const</span> intervalSource$ = interval(<span class="number">1000</span>);</span><br><span class="line"><span class="keyword">const</span> intervalSubscription = intervalSource$.subscribe(<span class="function"><span class="params">x</span> =&gt;</span> <span class="built_in">console</span>.log(x));</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">// Unsubscribe after 3s</span></span><br><span class="line">  intervalSubscription.unsubscribe();</span><br><span class="line">&#125;, <span class="number">3000</span>);</span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// 0</span></span><br><span class="line"><span class="comment">// 1</span></span><br></pre></td></tr></table></figure><p>在上一小节中我们提到，Observable 是惰性的，需要等到有下游数据消费者订阅的时候才开始执行内部逻辑，下面的示例中我们观察下它的行为：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> inputSource$ = <span class="keyword">new</span> Observable(<span class="function"><span class="params">observer</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> input = <span class="built_in">document</span>.querySelector(<span class="string">&#x27;input[name=search]&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> handler = <span class="function">(<span class="params">event</span>) =&gt;</span> observer.next(event);</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">&#x27;bind event&#x27;</span>);</span><br><span class="line">  input.addEventListener(<span class="string">&#x27;keyup&#x27;</span>, handler);</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="built_in">console</span>.log(<span class="string">&#x27;unbind event&#x27;</span>);</span><br><span class="line">    input.removeEventListener(<span class="string">&#x27;keyup&#x27;</span>, handler);</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;before subscribe&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> inputSubscription = inputSource$.subscribe(<span class="function">(<span class="params">event</span>) =&gt;</span> <span class="built_in">console</span>.log(event.target.value));</span><br><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  inputSubscription.unsubscribe();</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">&#x27;after unsubscribe&#x27;</span>);</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// &quot;before subscribe&quot;</span></span><br><span class="line"><span class="comment">// &quot;bind event&quot;</span></span><br><span class="line"><span class="comment">// &quot;unbind event&quot;</span></span><br><span class="line"><span class="comment">// &quot;after unsubscribe&quot;</span></span><br></pre></td></tr></table></figure><h3 id="Operators"><a href="#Operators" class="headerlink" title="Operators"></a>Operators</h3><p>介绍完数据的生产者 Observable 和消费者 Subscription，这一小节我们重点介绍下将它们连接起来的管线（pipeline）：各种各样的操作符 Operators。非常类似前文“<a href="https://zhuanlan.zhihu.com/p/28561932">尚春：前端中的 Pipeline</a>”中提到的其它前端中的管线，RxJS 中丰富的操作符经过合理组合，就能够满足我们日常开发中大部分场景。这正是 RxJS 精华和魅力所在，以至于 RxJS 官方将自己称之为：</p><blockquote><p>Think of RxJS as <strong>Lodash for events</strong>.</p></blockquote><p>简单来说，操作符提供了一种方法来<strong>处理来自上游数据源的值，并返回转换后的值的 Observable</strong>。例如自动补全例子中应用到的<code>map</code>、<code>debounceTime</code>、<code>switchMap</code>都是比较常用的操作符。多个操作符就组成了类似下图中的管线：<img src="../images/frontend-frp-04.jpg"></p><p><strong>map 操作符</strong></p><p>为了便于大家进一步理解操作符原理，下面展示下常用的<code>map</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">const</span> myMap = <span class="function">(<span class="params">mapFn</span>) =&gt;</span> <span class="function">(<span class="params">source</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// myMap is a transform operator, it returns a new Obervable</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> Observable(<span class="function">(<span class="params">subscriber</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> source.subscribe(</span><br><span class="line">      <span class="comment">// Whenever there is next data in the source, apply `mapFn` on it</span></span><br><span class="line">      <span class="comment">// and then pass it to the downstream</span></span><br><span class="line">      <span class="function">(<span class="params">next</span>) =&gt;</span> subscriber.next(mapFn(next)),</span><br><span class="line">      <span class="function">(<span class="params">error</span>) =&gt;</span> subscriber.error(error),</span><br><span class="line">      <span class="function">() =&gt;</span> subscriber.complete(),</span><br><span class="line">    );</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>所有操作符都可以用下面的弹珠图（Marble Diagram）来表示，map 自然也不例外：<img src="../images/frontend-frp-05.jpg"></p><p>更详细的介绍可以移步下面这个精彩的系列教程：<a href="https://blog.strongbrew.io/build-the-operators-from-rxjs-from-scratch/?lectureId=map#app">Video course - Build the operators from RxJS from scratch</a></p><p><strong>switchMap 操作符</strong></p><p>了解完<code>map</code>操作符之后，可能很多同学会问：前面一直提到的<code>switchMap</code>操作符是做啥的？实不相瞒，最早笔者看到<code>switchMap</code>、<code>mergeMap</code>、<code>concatMap</code>也是晕圈的，不过一旦把它们代入到实际场景中，就能很快意识到精妙所在。</p><p><code>switchMap</code>操作符最为适合的场景就是类似自动补全中的异步请求处理：</p><ul><li>在数据流的新数据（最新的文本框内容）到来时，创建一个新的 observable 来处理异步请求，此为 map 步骤</li><li>在数据流的新数据到来时，如果之前的异步请求尚未完成，则取消订阅其对应的 observable，转而订阅最新的异步请求 observable，此为 switch 步骤</li></ul><p>如此，我们便实现了自动补全异步请求的时序正确性。</p><p><code>switchMap</code>也可以用如下的弹珠图表示：<img src="../images/frontend-frp-06.jpg"><em>obs1$ 的每个数据都会在 obs2$ 中 map 为三个，B 对应的 observable 进行到 B2 时接收到 obs1$ 的新数据 C，因此跳过了接下来的 B3，转而开始生产 C1, C2, C3</em></p><p>感兴趣的同学可以观看下<code>switchMap</code>的实现教程，甚至可以考虑和<code>mergeMap</code>、<code>concatMap</code>一起打包看：<a href="https://blog.strongbrew.io/build-the-operators-from-rxjs-from-scratch/?lectureId=switchMap#app">Video course - Build the operators from RxJS from scratch</a></p><p><strong>操作符分类</strong></p><p>很多同学望 RxJS 止步大抵是因为浩如烟海的操作符：<img src="../images/frontend-frp-07.jpg"></p><p>为了降低 RxJS 学习门槛，RxJS 官方和社区也做了很多工作。除了可以借助弹珠图来具象化各个操作符外，还有一个好的方式是归类，各个击破。常见的几种操作符类型如下：</p><ul><li><strong>创建</strong> 这些运算符几乎允许你基于任何东西来创建一个 observable。前面 Observable 小节中提到的<code>of</code>、<code>fromEvent</code>、<code>interval</code>等都属于此类</li><li><strong>过滤</strong> 这些操作符提供了从 observable 源中接受值和处理背压（backpressure）的技术。我们耳熟能详的<code>filter</code>、<code>take</code>、<code>skip</code>、<code>first</code>、<code>last</code>、<code>debounce</code>、<code>throttle</code>等都属于此类</li><li><strong>转换</strong> 这些操作符提供了转换技术几乎可以涵盖你所能遇到的任何场景。譬如<code>map</code>、<code>reduce</code>、<code>switchMap</code>、<code>groupBy</code>等</li><li>**组合 ** 组合操作符允许连接来自多个 observables 的信息，例如<code>merge</code>、<code>pairwise</code>、<code>zip</code>等</li><li>**条件 ** 根据一定条件确定创建的<code>observable</code>，例如<code>iff</code>可以根据判定条件确定订阅传入的第一个 observable 参数还是第二个</li><li><strong>错误处理</strong> 这些操作符提供了一些高效的方式来优雅地处理错误，例如<code>retry</code>、<code>retryWhen</code>、<code>catchError</code>等</li></ul><p>在实践中，如果有一个你需要解决的问题，很可能就会有一个对应的操作符。社区很多人建议刚开始上手时，可以从<strong>各个类别的核心操作符子集开始学习和使用</strong>，例如上文中点名的一些操作符。随着时间的推移，当晦涩难懂的场景不可避免地出现时，你会越来越意识到操作符库的灵活性和多样化是多么重要。</p><h3 id="Subject"><a href="#Subject" class="headerlink" title="Subject"></a>Subject</h3><p>在 RxJS 中，Observable 默认是单播的（unicast），即多个订阅者之间不能共享任何信息，完全相互独立。这也被形象地被称为“冷 ”的。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Emits data in an array one by one</span></span><br><span class="line"><span class="keyword">const</span> dataSource$ = <span class="keyword">of</span>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br><span class="line"><span class="keyword">const</span> dataSubscription = dataSource$.subscribe(<span class="function"><span class="params">x</span> =&gt;</span> <span class="built_in">console</span>.log(x));</span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// 1</span></span><br><span class="line"><span class="comment">// 2</span></span><br><span class="line"><span class="comment">// 3</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> anotherDataSubscription = dataSource$.subscribe(<span class="function"><span class="params">x</span> =&gt;</span> <span class="built_in">console</span>.log(x));</span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// 1</span></span><br><span class="line"><span class="comment">// 2</span></span><br><span class="line"><span class="comment">// 3</span></span><br></pre></td></tr></table></figure><p>可以看出，第二次订阅同一个 obervable 得到的结果和第一次完全一致。</p><p>在实际应用中，有时我们需要支持多播（multicast），即在同一 Observable 的多个订阅者之间共享信息，这便是 Subject 的由来：</p><blockquote><p>A Subject is like an Observable, but can multicast to many Observers. Subjects are like EventEmitters: they maintain a registry of many listeners.</p></blockquote><p>Subject 类似我们常见的 EventEmitters，它会内部维护多个订阅者从而实现多种模式的信息共享：</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">const</span> subject = <span class="keyword">new</span> Subject();</span><br><span class="line"></span><br><span class="line"><span class="comment">// Add one observer</span></span><br><span class="line">subject.subscribe(&#123;</span><br><span class="line">  <span class="attr">next</span>: <span class="function">(<span class="params">v</span>) =&gt;</span> <span class="built_in">console</span>.log(<span class="string">`observerA: <span class="subst">$&#123;v&#125;</span>`</span>)</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">subject.next(<span class="number">1</span>);</span><br><span class="line">subject.next(<span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Add another observer</span></span><br><span class="line">subject.subscribe(&#123;</span><br><span class="line">  <span class="attr">next</span>: <span class="function">(<span class="params">v</span>) =&gt;</span> <span class="built_in">console</span>.log(<span class="string">`observerB: <span class="subst">$&#123;v&#125;</span>`</span>)</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">subject.next(<span class="number">3</span>);</span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// &quot;observerA: 1&quot;</span></span><br><span class="line"><span class="comment">// &quot;observerA: 2&quot;</span></span><br><span class="line"><span class="comment">// &quot;observerA: 3&quot;</span></span><br><span class="line"><span class="comment">// &quot;observerB: 3&quot;</span></span><br></pre></td></tr></table></figure><p>在 Angular 项目中，就经常应用 Subject 来在多个组件中共享数据。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在前端领域中，为了追求体验和代码的双重完美，不断有新的建模方案喷薄涌现，如滚滚车轮般推动着行业的发展。异步逻辑本身就有悖于直觉，本质复杂度高。以 RxJS 为代表的函数式响应式编程采用了类似 NodeJS Stream 的建模思想为异步编程提供了简洁优雅的解决方案。正如下图所示，虽然上手有一定的门槛，但一旦领会其万物皆流、Observable =&gt; Pipeline =&gt; Subscription 的要义，随着异步逻辑复杂度的提升，例如表单交互、视频播放器、大型应用等等复杂场景，这种编程范式将会给你带来越来越高的性价比。<img src="../images/frontend-frp-08.jpg"></p><p>在结束之前，我们不妨挖个新坑：在基于 RxJS 的自动补全实现基础上，怎么样添加如下功能以进一步提升体验呢：</p><ul><li>失败信息提示</li><li>失败重试，最多重试 2 次，且在新数据到来时取消之前 observable 的重试</li><li>添加加载提示，且如果请求完成时间小于 100ms 时不显示</li><li>缓存后端返回结果，可考虑 LRU 等方案</li><li>其它你能想象到的改进</li></ul><p>推荐各位小伙伴积极尝试下，后续我们一起来填坑吧。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754">The introduction to Reactive Programming you’ve been missing</a></li><li><a href="https://www.youtube.com/watch?v=NkVfvxaKVVY">“Reactive Programming: A Better Way To Write Frontend Applications” by Hannah Howard</a></li><li><a href="https://www.youtube.com/watch?v=Agu6jipKfYw">“Controlling Time and Space: understanding the many formulations of FRP” by Evan Czaplicki</a></li><li><a href="https://github.com/kriskowal/gtor">A General Theory of Reactivity</a></li><li><a href="https://www.cnblogs.com/apolis/p/11437688.html">函数式响应式编程 - Functional Reactive Programming</a></li><li><a href="https://www.learnrxjs.io/learn-rxjs/concepts/rxjs-primer">RxJS Primer</a></li><li><a href="https://blog.thoughtram.io/angular/2016/06/16/cold-vs-hot-observables.html">Cold vs Hot Observables</a></li><li><a href="https://blog.strongbrew.io/build-the-operators-from-rxjs-from-scratch/?lectureId=map#app">Build the Operators from RxJS from Scratch - map</a></li><li><a href="https://limeii.github.io/2019/07/rxjs-subject/">RxJS：四种 Subject 的用法和区别</a></li></ol><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>函数式响应式编程最早出现于 1997 年，微软的 Conal Elliott 和 Paul Hudak 发表了一篇“<a href="http://conal.net/papers/icfp97/">Functional Reactive Animation</a>”的论文，文中引入了两个新概念：</p><ul><li><strong>Behavior</strong> 一个随时间变化的值。你可以把它看作是一个函数，将时间中的每个时刻映射到相应的值。譬如一个动画，每个时刻都会对应某个帧</li><li><strong>Event</strong> 一个事件发生的序列。可以把它看作是一个有限或无限的事件列表，它是成对的：第一个数据表示事件发生的时间，第二个数据是标记给这个事件的一个值<img src="../images/frontend-frp-09.jpg"><img src="../images/frontend-frp-10.jpg"></li></ul><p>换句话说，Behavior 就像一个持续变化的值，比如说一个篮球运动员正在投掷的球的位置，而 Event 则是一连串发生的事件，比如说，每当上述球落入篮筐时，都要进行跟踪。通过围绕这两者进行的建模和扩充，Conal 逐步完成了一个基于 Haskell 的 FRP 雏形来处理动画等复杂交互场景。他在后面的一篇论文“<a href="http://conal.net/papers/ppdp00/">Declarative Event-Oriented Programming</a>”里，更是给出了一个具体的贝塞尔曲线编辑器的案例：<img src="../images/frontend-frp-11.jpg"></p><p>大概读完这两篇文章，实在是佩服大神在 20 多年前就开始琢磨如何建构异步操作流并给出了比较优雅的方案，要知道，JavaScript 1995 年才刚刚诞生，那时候 web 前端还只是处理一些极简单的情况。所以不得不惊叹，这样水平的人称之为开山鼻祖也并不过誉。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;由于思维方式上的显著差异，以 RxJS 为代表的函数式响应式编程（Functional Reactive Programming，简称 FRP）在前端工程师中有些“水土不服”，喜欢的推崇备至，然而对于更多开发者来说它却像“阳春白雪”一般，叫好不叫座。&lt;/p&gt;
&lt;p&gt;本文尝试从一个实际案例开始，通过对比普通实现和基于 RxJS 的版本阐释 FRP 的核心要义，之后简要介绍 RxJS 的核心概念，以及一些实际示例。希望能够帮助一些还在观望的同学更进一步了解这种编程范式。&lt;/p&gt;
&lt;h2 id=&quot;AutoComplete&quot;&gt;&lt;a href=&quot;#AutoComplete&quot; class=&quot;headerlink&quot; title=&quot;AutoComplete&quot;&gt;&lt;/a&gt;AutoComplete&lt;/h2&gt;&lt;p&gt;自动补全是很多前端工程师都曾接触过的实用功能：当用户在文本框中输入字符时，将当前输入内容传递给后端服务拿到可能的自动补全结果，并在文本框下方列出供用户选择。
&lt;img src=&quot;../images/frontend-frp-01.jpg&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;普通实现&quot;&gt;&lt;a href=&quot;#普通实现&quot; class=&quot;headerlink&quot; title=&quot;普通实现&quot;&gt;&lt;/a&gt;普通实现&lt;/h3&gt;&lt;p&gt;比较传统的实现思路非常直接，直接监听文本框的 keyup 事件，然后取数据后更新 UI 即可。代码如下：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;19&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;20&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;21&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; input = &lt;span class=&quot;built_in&quot;&gt;document&lt;/span&gt;.querySelector(&lt;span class=&quot;string&quot;&gt;&amp;quot;input&amp;quot;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// 1. Bind keyup event&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;input.addEventListener(&lt;span class=&quot;string&quot;&gt;&amp;quot;keyup&amp;quot;&lt;/span&gt;, &lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;event&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; keyword = event.target.value;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; autocomplete_results = &lt;span class=&quot;built_in&quot;&gt;document&lt;/span&gt;.getElementById(&lt;span class=&quot;string&quot;&gt;&amp;quot;autocomplete-results&amp;quot;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  autocomplete_results.innerHTML = &lt;span class=&quot;string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (keyword.length &amp;gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// 2. Fetch autocomplete list&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    fetch(&lt;span class=&quot;string&quot;&gt;`/people?keyword=&lt;span class=&quot;subst&quot;&gt;$&amp;#123;keyword&amp;#125;&lt;/span&gt;`&lt;/span&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      .then(&lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;response&lt;/span&gt;) =&amp;gt;&lt;/span&gt; response.json())&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      .then(&lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;data&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// 3. Update UI&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        autocomplete_results.innerHTML = &lt;span class=&quot;string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;; i &amp;lt; data.length; i++) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;          autocomplete_results.innerHTML += &lt;span class=&quot;string&quot;&gt;&amp;quot;&amp;lt;li&amp;gt;&amp;quot;&lt;/span&gt; + data[i] + &lt;span class=&quot;string&quot;&gt;&amp;quot;&amp;lt;/li&amp;gt;&amp;quot;&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="Functional Programming" scheme="https://springuper.github.io/tags/Functional-Programming/"/>
    
    <category term="Functional Reactive Programming" scheme="https://springuper.github.io/tags/Functional-Reactive-Programming/"/>
    
    <category term="RxJS" scheme="https://springuper.github.io/tags/RxJS/"/>
    
  </entry>
  
  <entry>
    <title>招聘驱动的组织建设</title>
    <link href="https://springuper.github.io/recruitment-driven-organization/"/>
    <id>https://springuper.github.io/recruitment-driven-organization/</id>
    <published>2021-11-23T21:56:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>今天有一位候选人问我：“你作为团队负责人，最重要的工作是什么？”，坦白来说，我自己并没有很深刻地思考过这个问题。脱口而出的答案是：招人，招足够优秀的人。</p><p>这时，我看到候选人深以为然地点了点头，似乎是非常认可我这由衷的答复。考虑到他也十足优秀，既然得到了肯定，那我不妨继续沿着这个思路畅想下，谈谈招聘驱动的组织建设。</p><h2 id="组织建设的核心？"><a href="#组织建设的核心？" class="headerlink" title="组织建设的核心？"></a>组织建设的核心？</h2><p>在我们这个工程师行业，虽然很多人声称自己是码畜，只是外表光鲜地干着搬砖的体力活。不过纵观最近 20 多年互联网的飞速发展，诚如 Paul Graham 所言，真正意义上的工程师，或者黑客，实际上是最具有可放大性和可测量性的工种之一。当你的代码跑在成百上千台机器上为百万千万甚至以亿计的用户服务时，毫无疑问你的价值也得到了巨大程度的放大。</p><p>进一步，优秀的工程师和普通的工程师的差距就像太阳和灯泡一样大，虽然都能发光发热，但是数量级不一样。</p><p>基于此，我粗俗地以为，工程师组织建设的核心就是招人，招足够优秀的人：</p><ul><li>优秀的工程师会带来优秀的解决方案，进而提升组织效能</li><li>优秀的工程师会自我迭代，填满组织成长过程中的内生间隙，保障组织可持续发展</li><li>优秀的工程师会自我管理，结果驱动且不断对齐组织目标，管理者更关注中长期问题</li><li>最重要的是，优秀的工程师会吸引优秀的工程师</li></ul><span id="more"></span><h2 id="怎样招到足够优秀的人"><a href="#怎样招到足够优秀的人" class="headerlink" title="怎样招到足够优秀的人"></a>怎样招到足够优秀的人</h2><p>老实说，地球人都知道优秀工程师非常好，可关键问题是：如何招到呢？</p><p>其实秘诀也非常简单，那就是一开始就只招优秀工程师，一旦你的团队里有三个这样的人，剩下的就是给他们足够有挑战的事情，然后专注在业务发展上就好了。</p><p>但现实很骨感：</p><ul><li>业务压力太大，人少活多，实在干不过来</li><li>头部人才抢夺严重，成本高昂</li><li>并不是所有的优秀人才都适合当前团队，水土不服的情况非常普遍</li></ul><p>个中缘由林林总总，这大概也是很多团队慢慢变得平庸的原因吧。</p><p>既然今天这篇是畅想，不妨设想，如果我们的根本目标是招聘优秀人才，那这会倒逼我们做些什么决策呢？</p><ul><li>选择一件有意义的事情作为公司的核心业务，一方面要有梦想，另外一方面也兼具社会价值，以此来吸引初始团队</li><li>维系一个小而美的优秀核心团队。如果平庸的人太多，不妨逐步提高人才密度，哪怕付出高成本，以此来稳定扩充团队</li><li>适当利用外包、采购等外部杠杆，将团队的焦点维持在核心领域，以此来提高团队效能</li><li>培养和发展人才，建设起健康梯队的同时，提高团队留存度</li><li>通过技术交流活动、媒体传播、招聘运营活动等等方式，对外展示团队优势，获得更广泛知名度的同时，也赢得内部员工认同感</li></ul><h2 id="鸡生蛋，蛋生鸡？"><a href="#鸡生蛋，蛋生鸡？" class="headerlink" title="鸡生蛋，蛋生鸡？"></a>鸡生蛋，蛋生鸡？</h2><p>可是没有优秀的团队，怎么会有好的业务呢？</p><p>这似乎陷入了先有鸡还是先有蛋的死循环。然而众所周知，解决死循环的方式之一就是：开始的那第一个人就是个懂业务的优秀工程师！这在很多企业中比比皆是，马化腾、张小龙、任正非、雷军、李彦宏等等。还有一个方式就是搭班子，CEO 最先找的通常是 CTO。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>或许几年后，回过头来看，我会觉得这篇胡言乱语有些稚嫩，不过既然目前还算好使，就沿着这条路继续走下去。如果对你也还算是有所启发，那就是额外的大收益了。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;今天有一位候选人问我：“你作为团队负责人，最重要的工作是什么？”，坦白来说，我自己并没有很深刻地思考过这个问题。脱口而出的答案是：招人，招足够优秀的人。&lt;/p&gt;
&lt;p&gt;这时，我看到候选人深以为然地点了点头，似乎是非常认可我这由衷的答复。考虑到他也十足优秀，既然得到了肯定，那我不妨继续沿着这个思路畅想下，谈谈招聘驱动的组织建设。&lt;/p&gt;
&lt;h2 id=&quot;组织建设的核心？&quot;&gt;&lt;a href=&quot;#组织建设的核心？&quot; class=&quot;headerlink&quot; title=&quot;组织建设的核心？&quot;&gt;&lt;/a&gt;组织建设的核心？&lt;/h2&gt;&lt;p&gt;在我们这个工程师行业，虽然很多人声称自己是码畜，只是外表光鲜地干着搬砖的体力活。不过纵观最近 20 多年互联网的飞速发展，诚如 Paul Graham 所言，真正意义上的工程师，或者黑客，实际上是最具有可放大性和可测量性的工种之一。当你的代码跑在成百上千台机器上为百万千万甚至以亿计的用户服务时，毫无疑问你的价值也得到了巨大程度的放大。&lt;/p&gt;
&lt;p&gt;进一步，优秀的工程师和普通的工程师的差距就像太阳和灯泡一样大，虽然都能发光发热，但是数量级不一样。&lt;/p&gt;
&lt;p&gt;基于此，我粗俗地以为，工程师组织建设的核心就是招人，招足够优秀的人：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优秀的工程师会带来优秀的解决方案，进而提升组织效能&lt;/li&gt;
&lt;li&gt;优秀的工程师会自我迭代，填满组织成长过程中的内生间隙，保障组织可持续发展&lt;/li&gt;
&lt;li&gt;优秀的工程师会自我管理，结果驱动且不断对齐组织目标，管理者更关注中长期问题&lt;/li&gt;
&lt;li&gt;最重要的是，优秀的工程师会吸引优秀的工程师&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    
    <category term="Recruitment" scheme="https://springuper.github.io/tags/Recruitment/"/>
    
    <category term="Organization" scheme="https://springuper.github.io/tags/Organization/"/>
    
  </entry>
  
  <entry>
    <title>React Hooks 原理剖析</title>
    <link href="https://springuper.github.io/react-hooks/"/>
    <id>https://springuper.github.io/react-hooks/</id>
    <published>2021-08-31T21:54:00.000Z</published>
    <updated>2026-10-03T09:20:43.753Z</updated>
    
    <content type="html"><![CDATA[<p>自 19 年 <a href="https://reactjs.org/blog/2019/02/06/react-v16.8.0.html">React 16.8</a> 正式发布 Hooks 以来，已然在社区中形成了席卷之势，甚至也影响到了很多其它前端库以及框架。目前介绍如何使用 Hooks 的文章汗牛充栋，本文着重从原理层面，从零开始构建一个简化版 Hooks 实现，期望能够让你知其然也知其所以然，更好地把握和运用 Hooks。</p><h2 id="为什么要引入-Hooks？"><a href="#为什么要引入-Hooks？" class="headerlink" title="为什么要引入 Hooks？"></a>为什么要引入 Hooks？</h2><p>React 团队的解释是目前以 React.Component 类为主的组件实现方式下，存在如下<a href="https://reactjs.org/docs/hooks-intro.html#motivation">三方面问题</a>：</p><ol><li><strong>组件之间一些包含状态的逻辑很难拆分复用</strong> 因为函数式组件不能处理状态，且之前 Mixin 模式已被证伪，目前社区主流的 HoC 和 render props  模式也被诟病导致 wrapper hell</li><li><strong>复杂的大型组件越发难以理解和维护</strong> 关联紧密的逻辑被迫分散在各个 lifecycle 方法中，非常容易出现疏漏</li><li><strong>组件类的方式对开发者和机器都不友好</strong> 难以理解的 this，回调函数需要手动绑定，还有很多诸如此类的组件类模式的认知成本，导致新手入门难度高。同样地，组件类因为有内部状态、继承逻辑等特征，对编译器的各种优化也非常不友好</li></ol><p>Dan 在 React Conf 2018 大会上介绍完这几个问题后，也再进一步地说明其实这三个问题本质是一个问题的三种体现而已。而这个本质的问题就是：</p><blockquote><p>React does not provide a simpler, smaller, lightweight primitive to add state and lifecycle than a class component.</p></blockquote><p>换言之，React 本身非常函数式的设计哲学， <img src="../images/react-hooks-01.jpg" alt="fn(data) = UI"> ，并没有被当前组件类模式很好地表达。</p><p>为了彻底解决这一问题，在 React 团队的 Sebastian 等人的带领下，经过参考和探索诸如 <a href="https://github.com/reactjs/react-future/blob/master/07%20-%20Returning%20State/01%20-%20Stateful%20Functions.js">Stateful Functions</a>、<a href="http://displayscript.org/introduction.html">DisplayScript</a>、<a href="https://github.com/ocamllabs/ocaml-effects-tutorial#2-effectful-computations-in-a-pure-setting">Algebraic effects in Multicore OCaml</a> 等方案后，给出了 React Hooks 方案。</p><span id="more"></span><h2 id="Hooks-实现的准备工作"><a href="#Hooks-实现的准备工作" class="headerlink" title="Hooks 实现的准备工作"></a>Hooks 实现的准备工作</h2><p>在这个小节中，让我们一起从零开始实现一个极简版本的 React，一方面为接下来的 Hooks 实现做准备，另一方面，也更深入地理解 React 的基本原理。</p><p><strong>虚拟 DOM</strong></p><p>React 是一个声明式 UI library，读入 data 和组件，处理后输出 UI。为了实现差量更新以降低 DOM 操作成本，React 引入了虚拟 DOM，让 UI 以一种”虚拟“的表现形式被保存于内存中，并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> Greet = <span class="function">(<span class="params">&#123; name &#125;</span>) =&gt;</span> (</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">h2</span>&gt;</span>Hello, &#123;name&#125;<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>在 React 中，Greet 在虚拟 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><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="comment">// element 对应的 DOM node</span></span><br><span class="line">  <span class="attr">dom</span>: divNode,</span><br><span class="line">  <span class="comment">// element 数据</span></span><br><span class="line">  <span class="attr">element</span>: &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;div&#x27;</span>,</span><br><span class="line">    <span class="attr">props</span>: &#123;&#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// 子节点列表</span></span><br><span class="line">  <span class="attr">children</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">dom</span>: h2Node,</span><br><span class="line">      <span class="attr">element</span>: &#123;</span><br><span class="line">        <span class="attr">type</span>: <span class="string">&#x27;h2&#x27;</span>,</span><br><span class="line">        <span class="attr">props</span>: &#123;&#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">children</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">dom</span>: textNode,</span><br><span class="line">          <span class="attr">element</span>: &#123; </span><br><span class="line">            <span class="attr">type</span>: <span class="string">&#x27;text&#x27;</span>, </span><br><span class="line">            <span class="attr">props</span>: &#123; <span class="attr">nodeValue</span>: <span class="string">&#x27;Hello, xxx&#x27;</span> &#125; </span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      ]</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可见，虚拟 DOM 和真实 DOM 类似，也是树形结构，每个节点有真实 DOM 节点引用、组件元素数据以及子节点等信息。更多内容可以参考本专栏之前的一篇文章：<a href="https://zhuanlan.zhihu.com/p/28562066">尚春：React 设计中的闪光点</a></p><p><strong>TinyReact v1</strong></p><p>基于虚拟 DOM，我们接下来实现一个极简版 React，不妨称之为 TinyReact。</p><p>作为第一步，我们实现 <code>createElement</code> 和 <code>createTextElement</code> 两个工厂函数，用以表达组件渲染所需的 element 信息：</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"><span class="function"><span class="keyword">function</span> <span class="title">createElement</span>(<span class="params">type, props, ...children</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    type,</span><br><span class="line">    props,</span><br><span class="line">    <span class="attr">children</span>: children.map(<span class="function">(<span class="params">child</span>) =&gt;</span></span><br><span class="line">      <span class="keyword">typeof</span> child === <span class="string">&quot;object&quot;</span> ? child : createTextElement(child)</span><br><span class="line">    )</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createTextElement</span>(<span class="params">text</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&quot;TEXT_ELEMENT&quot;</span>,</span><br><span class="line">    <span class="attr">props</span>: &#123;</span><br><span class="line">      <span class="attr">nodeValue</span>: text</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>接下来，实现第一版 <code>render</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="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params">element, parentDom</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; type, props, children = [] &#125; = element;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Create DOM element</span></span><br><span class="line">  <span class="keyword">const</span> dom = type === <span class="string">&quot;TEXT_ELEMENT&quot;</span></span><br><span class="line">    ? <span class="built_in">document</span>.createTextNode(<span class="string">&quot;&quot;</span>)</span><br><span class="line">    : <span class="built_in">document</span>.createElement(type);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Set properties</span></span><br><span class="line">  <span class="built_in">Object</span>.keys(props).forEach(<span class="function">(<span class="params">name</span>) =&gt;</span> &#123;</span><br><span class="line">    dom[name] = props[name];</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Render children</span></span><br><span class="line">  children.forEach(<span class="function">(<span class="params">childElement</span>) =&gt;</span> render(childElement, dom));</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Append to parent</span></span><br><span class="line">  parentDom.appendChild(dom);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如上短短三十余行代码，我们就完成了 TinyReact 的第一版！下面试试下它的威力吧：</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">const</span> TinyReact = &#123;</span><br><span class="line">  createElement,</span><br><span class="line">  render</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> App = <span class="function">(<span class="params">&#123; name = <span class="string">&#x27;World&#x27;</span> &#125; = &#123;&#125;</span>) =&gt;</span> TinyReact.createElement(</span><br><span class="line">  <span class="string">&quot;div&quot;</span>,</span><br><span class="line">  &#123; <span class="attr">style</span>: <span class="string">&quot;background: salmon; padding: 5rem; text-align: center&quot;</span> &#125;,</span><br><span class="line">  TinyReact.createElement(<span class="string">&quot;h1&quot;</span>, &#123;&#125;, <span class="string">&quot;Hello &quot;</span> + name),</span><br><span class="line">  TinyReact.createElement(<span class="string">&quot;div&quot;</span>, &#123; <span class="attr">style</span>: <span class="string">&quot;color: white&quot;</span> &#125;, <span class="string">&quot;by TinyReact&quot;</span>)</span><br><span class="line">);</span><br><span class="line">TinyReact.render(App(), <span class="built_in">document</span>.getElementById(<span class="string">&quot;app&quot;</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// 上述代码太过啰嗦，借助 JSX 语法糖 ，可以被重写为</span></span><br><span class="line"><span class="comment">/** <span class="doctag">@jsx </span>TinyReact.createElement */</span></span><br><span class="line"><span class="keyword">const</span> App = <span class="function">(<span class="params">&#123; name = <span class="string">&#x27;World&#x27;</span> &#125; = &#123;&#125;</span>) =&gt;</span> (</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&quot;background: salmon; padding: 5rem; text-align: center&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello &#123;name&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&quot;color: white&quot;</span>&gt;</span>by TinyReact<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line">TinyReact.render(App(), <span class="built_in">document</span>.getElementById(<span class="string">&quot;app&quot;</span>));</span><br></pre></td></tr></table></figure><p>该示例中，App 组件通过调用 <code>createElement</code> 方法创建了一个包含一个父节点和两个子节点的 element 树，经过 <code>render</code> 方法递归处理后渲染到 DOM 中。效果如图：<img src="../images/react-hooks-02.jpg"></p><p>可以到 CodeSandbox 上手体验：<a href="https://codesandbox.io/s/tinyreact-simple-render-ez65i?file=/src/index.js">TinyReact-simple-render - CodeSandbox</a></p><p><strong>调谐过程（Reconciliation）</strong></p><p>TinyReact v1 已经可以很轻松地处理组件的首次渲染，但是当我们再次调用 <code>render</code> 方法时，出现了两个问题：</p><ol><li>TinyReact 依然重新创建了一遍所有 DOM 节点，没有检查是否已有 DOM 节点可以复用</li><li>创建的 DOM 节点又被加入到了页面父节点中，导致重复内容</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// First render</span></span><br><span class="line">TinyReact.render(App(), <span class="built_in">document</span>.getElementById(<span class="string">&quot;app&quot;</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// Second render</span></span><br><span class="line">TinyReact.render(App(&#123; <span class="attr">name</span>: <span class="string">&quot;JavaScript&quot;</span> &#125;), <span class="built_in">document</span>.getElementById(<span class="string">&quot;app&quot;</span>));</span><br></pre></td></tr></table></figure><p><img src="../images/react-hooks-03.jpg"></p><p>产生这些问题的原因是 TinyReact v1 并没有 React 中的调谐过程，即通过对比新旧虚拟 DOM 之间的不同来进行真实 UI 的差异化更新。</p><p>那就让我们撸起袖子解决这个看似不简单的问题吧。</p><p>首先改造下 <code>render</code> 方法，从而在下一次 render 的时候能够对比两次的差异：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> rootInstance = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params">element, container</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> prevInstance = rootInstance;</span><br><span class="line">  <span class="keyword">const</span> nextInstance = reconcile(container, prevInstance, element);</span><br><span class="line">  rootInstance = nextInstance;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到几个变化：</p><ol><li><code>render</code> 中的原渲染逻辑被封装到 <code>reconcile</code> 调谐函数中</li><li>加入一个全局的 <code>rootInstance</code> 变量，它保存着每次调谐过程的结果，即一个完整的虚拟 DOM 实例</li><li>调用 <code>reconcile</code> 时，会传入当前的 <code>rootInstance</code> 作为参照，完成调用后，将结果更新到 <code>rootInstance</code></li></ol><p>因此，<strong>每次 render 就是一个调谐过程，这一过程会对比当前虚拟 DOM 实例和新传入的渲染内容之间的差异从而实现差量更新</strong>。</p><p>有了对调谐过程的基本理解，下面看下 <code>reconcile</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reconcile</span>(<span class="params">parentDom, instance, element</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (instance === <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// Create instance</span></span><br><span class="line">    <span class="keyword">const</span> newInstance = instantiate(element);</span><br><span class="line">    parentDom.appendChild(newInstance.dom);</span><br><span class="line">    <span class="keyword">return</span> newInstance;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (element === <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// Remove instance</span></span><br><span class="line">    parentDom.removeChild(instance.dom);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (instance.element.type === element.type) &#123;</span><br><span class="line">    <span class="comment">// Update instance</span></span><br><span class="line">    updateDomProperties(instance.dom, instance.element.props, element.props);</span><br><span class="line">    instance.childInstances = reconcileChildren(instance, element);</span><br><span class="line">    instance.element = element;</span><br><span class="line">    <span class="keyword">return</span> instance;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// Replace instance</span></span><br><span class="line">    <span class="keyword">const</span> newInstance = instantiate(element);</span><br><span class="line">    parentDom.replaceChild(newInstance.dom, instance.dom);</span><br><span class="line">    <span class="keyword">return</span> newInstance;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可见 <code>reconcile</code> 处理了四种情况：</p><ol><li>如果没有对应的之前虚拟 DOM 节点实例，则创建一个实例（包括 DOM 节点、传入的 element 数据、子实例列表等），并添加到页面 DOM 中。这也是第一次执行 <code>render</code> 时的逻辑</li><li>如果已经有虚拟 DOM 节点实例，但是本次渲染传入的 element 数据为空，则意味着新 UI 中不再需要该节点，因此从 DOM 中删除之前的 DOM 节点，并返回空结果</li><li>如果当前虚拟 DOM 实例和新的 element 类型是一致的，说明节点类型没有变化，例如之前是一个 div，现在仍然是一个 div，则只检查是否有对应的节点属性需要变更，并递归处理所有子节点实例</li><li>最后一种情况是当前虚拟 DOM 实例和新的 element 类型不一致，则创建新实例并进行替换</li></ol><p>限于篇幅，文中不再详述 <code>instantiate</code>、<code>updateDomProperties</code>、<code>reconcileChildren</code> 等方法的实现。感兴趣的请移步到下面的 CodeSandbox 查阅体验：<a href="https://codesandbox.io/s/tinyreact-reconciliation-91jfw?file=/src/index.js">TinyReact-reconciliation - CodeSandbox</a></p><p>经过这一改造，TinyReact v2 已经能够处理多次渲染。</p><p><strong>函数组件</strong></p><p>准备工作的最后一步是支持函数组件，它是 Hooks 的主要使用场景。</p><p>需要改进的地方有两处，一处为首次渲染函数组件时，在 <code>instantiate</code> 方法内创建对应虚拟 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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">instantiate</span>(<span class="params">element</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; type, props, children = [] &#125; = element;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Handle function component</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> type === <span class="string">&quot;function&quot;</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> childElement = type(props);</span><br><span class="line">    <span class="keyword">const</span> childInstance = instantiate(childElement);</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">dom</span>: childInstance.dom,</span><br><span class="line">      element,</span><br><span class="line">      childInstance</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述代码中，与处理普通 DOM 节点（div/ul 等）主要的不同在于：</p><ul><li>需要执行函数组件，<code>type(props)</code>，进而获得最终的 element 表达</li><li>函数组件对应的虚拟 DOM 节点只会有一个子节点实例，而普通 DOM 节点可以有多个</li></ul><p>另一处为 <code>reconcile</code> 调谐过程中需要更新函数组件对应虚拟 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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reconcile</span>(<span class="params">parentDom, instance, element</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// Update function instance</span></span><br><span class="line">    <span class="keyword">const</span> childElement = element.type(element.props);</span><br><span class="line">    <span class="keyword">const</span> oldChildInstance = instance.childInstance;</span><br><span class="line">    <span class="keyword">const</span> childInstance = reconcile(parentDom, oldChildInstance, childElement);</span><br><span class="line">    instance.dom = childInstance.dom;</span><br><span class="line">    instance.element = element;</span><br><span class="line">    instance.childInstance = childInstance;</span><br><span class="line">    <span class="keyword">return</span> instance;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在，TinyReact v3 已经可以处理函数组件：</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></pre></td><td class="code"><pre><span class="line">TinyReact.render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span></span>, <span class="built_in">document</span>.getElementById(<span class="string">&quot;app&quot;</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// App 是一个函数组件，上述代码等同于：</span></span><br><span class="line">TinyReact.render(TinyReact.createElement(App, &#123;&#125;), <span class="built_in">document</span>.getElementById(<span class="string">&quot;app&quot;</span>));</span><br></pre></td></tr></table></figure><p>一如既往，可以到 CodeSandbox 查阅完整代码并体验：<a href="https://codesandbox.io/s/tinyreact-functional-component-x51ul?file=/src/index.js">TinyReact-functional-component - CodeSandbox</a></p><p>至此，我们完成了所有 Hooks 实现的准备工作。</p><h2 id="实现第一个-Hook：useState"><a href="#实现第一个-Hook：useState" class="headerlink" title="实现第一个 Hook：useState"></a>实现第一个 Hook：useState</h2><p><code>useState</code> 可谓是最为常用的 Hook，我们接下来动手实现它吧！</p><p>让我们稍稍回顾下 <code>useState</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>You clicked &#123;count&#125; times<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="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></span><br><span class="line"><span class="xml">        Click me</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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>可以总结出 <code>useState</code> 的几个特点：</p><ol><li>接受一个初始值</li><li>返回一个 tuple，第一个元素为 state 最新的值，第二个元素为更新函数</li><li>在函数组件多次执行时，仍然能够返回之前的 state 值</li></ol><p>结合我们在预备阶段介绍的 TinyReact 实现，能够满足这些条件，尤其是第三个条件的实现方式，看起来只要在和组件有深度绑定的虚拟 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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Function instance</span></span><br><span class="line">&#123;</span><br><span class="line">  dom,</span><br><span class="line">  element,</span><br><span class="line">  childInstance,</span><br><span class="line">  <span class="comment">// Add state to store state across multiple executions</span></span><br><span class="line">  state,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如上所示，我们拓展函数组件对应的虚拟 DOM 节点实例，加入一个 <code>state</code>  字段来保存当前状态数据。</p><p>经过分析 TinyReact 实现，我们需要在每次执行函数组件之前进行 state 数据的准备工作：</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="keyword">let</span> wipInstance = <span class="literal">null</span>;</span><br><span class="line"><span class="keyword">let</span> wipState = <span class="literal">undefined</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">instantiate</span>(<span class="params">element</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; type, props, children = [] &#125; = element;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Create function instance</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> type === <span class="string">&quot;function&quot;</span>) &#123;</span><br><span class="line">    wipInstance = &#123;&#125;;</span><br><span class="line">    wipState = <span class="literal">undefined</span>;</span><br><span class="line">    <span class="keyword">const</span> childElement = type(props);</span><br><span class="line">    ...</span><br><span class="line">    wipInstance.state = wipState;</span><br><span class="line">    <span class="keyword">return</span> wipInstance;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reconcile</span>(<span class="params">parentDom, instance, element</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// Update function instance</span></span><br><span class="line">    wipInstance = instance;</span><br><span class="line">    wipState = instance.state;</span><br><span class="line">    <span class="keyword">const</span> childElement = element.type(element.props);</span><br><span class="line">    ...</span><br><span class="line">    instance.state = wipState,</span><br><span class="line">    <span class="keyword">return</span> instance;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>共有三处变更：</p><ol><li>添加两个全局变量，<code>wipInstance</code> 保存当前正在处理的虚拟 DOM 实例，<code>wipState</code> 保存当前虚拟 DOM 实例的 state 数据，以便在 <code>useState</code> 中直接读取</li><li>在 <code>instantiate</code> 方法中首次渲染函数组件（mount）时，将 <code>wipInstance</code> 指向新创建的对象，并初始化 <code>wipState</code></li><li>在 reconcile 方法中更新渲染函数组件（update）时，将 <code>wipInstance</code> 指向已创建的虚拟 DOM 实例，<code>wipState</code> 保存该实例的 <code>state</code> 数据</li></ol><p>上述变更保证了 <code>wipInstance</code> 和 <code>wipState</code> 在执行函数组件之前已经指向了正确的数据，且由于是全局变量（模块级变量，仅在 TinyReact 内部共享，下同），它们是可以被其它 TinyReact 方法直接读取的。接下来我们可以尝试实现 <code>useState</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="function"><span class="keyword">function</span> <span class="title">useState</span>(<span class="params">initialValue</span>) </span>&#123;</span><br><span class="line">  wipState = <span class="keyword">typeof</span> wipState === <span class="string">&#x27;undefined&#x27;</span> ? initialValue : wipState;</span><br><span class="line">  <span class="keyword">const</span> instance = wipInstance;</span><br><span class="line">  setState = <span class="function">(<span class="params">newState</span>) =&gt;</span> &#123;</span><br><span class="line">    instance.state = newState;</span><br><span class="line">    reconcile(instance.dom.parentNode, instance, instance.element);</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="keyword">return</span> [wipState, setState];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>That’s IT! 逻辑可以说是非常简明扼要，主要有两方面：</p><ul><li>更新 <code>wipState</code>，在初始化时赋值为 <code>initialValue</code></li><li>创建 <code>setState</code> 方法，接受一个参数作为新指定的 state，先将其更新到虚拟 DOM 实例的 <code>state</code> 字段，然后执行调谐过程触发 UI 更新</li><li>将最新 state 和 <code>setState</code> 方法组合为二元 tuple 返回</li></ul><p>这段逻辑也似乎解释了 React Hooks 名字的寓意：<strong>Hook 就是 React 内部引擎扩展到函数组件的一个“钩子（hook）”，通过这个钩子，函数组件可以读写内部引擎保存的信息，且该信息在其多次执行时保持一致</strong>。<img src="../images/react-hooks-04.jpg"></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> App = <span class="function">(<span class="params">&#123; name = <span class="string">&quot;World&quot;</span> &#125; = &#123;&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&quot;background: salmon; padding: 5rem; text-align: center&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello &#123;name&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">&quot;color: white; margin-right: 1rem&quot;</span>&gt;</span>count: &#123;count&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="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;Add<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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">TinyReact.render(<span class="xml"><span class="tag">&lt;<span class="name">App</span> <span class="attr">name</span>=<span class="string">&quot;TinyReact&quot;</span> /&gt;</span></span>, <span class="built_in">document</span>.getElementById(<span class="string">&quot;app&quot;</span>));</span><br></pre></td></tr></table></figure><p><img src="../images/react-hooks-05.jpg"></p><p>完整代码见下面 CodeSandbox：<a href="https://codesandbox.io/s/tinyreact-usestate-single-xdu90?file=/src/index.js">TinyReact-useState-single - CodeSandbox</a></p><p><strong>多状态支持</strong></p><p>上述实现存在一个非常明显的问题：只能保存一个 state。</p><p>为了让 TinyReact 更加切合实际，需要进一步添加多状态的支持。实现方式多种多样，例如：</p><ol><li>将虚拟 DOM 节点实例的 state 替换为 hooks 数组</li><li>将虚拟 DOM 节点实例的 state 替换为 hookHash 对象，key 可以通过 <code>useState(key, initialState)</code> 来指定</li><li>将虚拟 DOM 节点实例的 hooks 或者 hookHash 对象作为函数组件的第二个参数传入，这样组件内部是可以随意读写任意状态</li></ol><p>React 团队经过<a href="https://github.com/reactjs/rfcs/pull/68#issuecomment-433170023">研究权衡</a>，最终采用了第一种方式，虽然有顺序相关等缺点，但是心智成本低，不需要指定 key，不需要额外参数。而且顺序相关的缺点可以通过 lint 解决。</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">let</span> wipInstance = <span class="literal">null</span>;</span><br><span class="line"><span class="keyword">let</span> wipHookIndex = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">instantiate</span>(<span class="params">element</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// Create function instance</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> type === <span class="string">&quot;function&quot;</span>) &#123;</span><br><span class="line">    wipInstance = &#123;&#125;;</span><br><span class="line">    wipHookIndex = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">const</span> childElement = type(props);</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reconcile</span>(<span class="params">parentDom, instance, element</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">    <span class="comment">// Update function instance</span></span><br><span class="line">    wipInstance = instance;</span><br><span class="line">    wipHookIndex = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">const</span> childElement = element.type(element.props);</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>修改的地方和上一小节如出一辙：</p><ol><li>再次新增一个全局变量 <code>wipHookIndex</code>，用以指向当前正在调用的 hook 索引，从而获得正确的 hook 状态数据</li><li>在每次执行函数组件（mount 或者 update）时，先要重置 <code>wipHookIndex</code></li></ol><p>接下来继续修改 <code>useState</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useState</span>(<span class="params">initialValue</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> instance = wipInstance;</span><br><span class="line">  <span class="keyword">const</span> hooks = wipInstance.hooks;</span><br><span class="line">  <span class="keyword">const</span> hookIndex = wipHookIndex;</span><br><span class="line">  hooks[hookIndex] = hooks[hookIndex] || initialValue;</span><br><span class="line">  <span class="keyword">const</span> setState = <span class="function">(<span class="params">newState</span>) =&gt;</span> &#123;</span><br><span class="line">    hooks[hookIndex] = newState;</span><br><span class="line">    reconcile(instance.dom.parentNode, instance, instance.element);</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="keyword">return</span> [wipInstance.hooks[wipHookIndex++], setState];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对比第一版 <code>useState</code> 实现，主要进行了数组化的改进。还有一个至关重要的改动：在 <code>return</code> 之前需要步进 <code>wipHookIndex</code> 从而保证下一个 <code>useState</code> 可以拿到正确状态。</p><p>又到了示例环节，这次我们在 count 基础上，添加一个 age 的 state：</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">const</span> App = <span class="function">(<span class="params">&#123; name = <span class="string">&quot;World&quot;</span> &#125; = &#123;&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> [age, setAge] = useState(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&quot;background: salmon; padding: 5rem; text-align: center&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello &#123;name&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">&quot;color: white; margin-right: 1rem&quot;</span>&gt;</span>count: &#123;count&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="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;Add<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">&quot;color: white; margin: 0 1rem 0 2rem&quot;</span>&gt;</span>age: &#123;age&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setAge(age + 1)&#125;&gt;Grow<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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><img src="../images/react-hooks-06.jpg"><a href="https://codesandbox.io/s/tinyreact-usestate-swsty?file=/src/index.js">TinyReact-useState - CodeSandbox</a></p><h2 id="更多-Hooks"><a href="#更多-Hooks" class="headerlink" title="更多 Hooks"></a>更多 Hooks</h2><p>回过头看，虚拟 DOM 实例的 <code>hooks</code> 属性既然是一个数组，那么它的每个元素存储的可以是状态数据，但也可以是其它类似 callback 等数据，只要保证顺序即可。<strong>它本质上提供了函数组件在多次执行时可以共享的数据</strong>。</p><p>在本节中，我们就基于目前的 TinyReact，继续实现其它的 Hooks。</p><p><strong>useEffect</strong></p><p><code>useEffect</code> 大概是除 <code>useState</code> 外最为常用的 Hook，它与组件生命周期息息相关。正如它的名字所喻示的，常被用来处理副作用相关的逻辑。</p><p>有了 <code>useState</code> 的实现思路作为借鉴，<code>useEffect</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useEffect</span>(<span class="params">callback, deps</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> oldDeps = wipInstance.hooks[wipHookIndex];</span><br><span class="line">  <span class="keyword">const</span> hasChangedDeps = oldDeps</span><br><span class="line">    ? deps.some(<span class="function">(<span class="params">el, i</span>) =&gt;</span> el !== oldDeps[i])</span><br><span class="line">    : <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">if</span> (!deps || hasChangedDeps) &#123;</span><br><span class="line">    callback();</span><br><span class="line">    wipInstance.hooks[wipHookIndex] = deps;</span><br><span class="line">  &#125;</span><br><span class="line">  wipHookIndex++;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>接受两个参数：<code>callback</code> 是回调函数，用以处理副作用；<code>deps</code> 为判断是否需要再次执行 <code>callback</code> 的依赖条件</li><li>在虚拟 DOM 实例 hooks 对应元素存储的内容是上次执行时的 <code>deps</code> 信息，用以进行比对</li><li>在函数组件初次渲染（mount）时，因为 hooks 对应元素为空，所以 <code>callback</code> 一定被执行</li><li>在函数组件更新渲染（update）时，会检查最新传入的 <code>deps</code> 是否相同，如果有变更则执行 <code>callback</code></li><li>一旦执行 <code>callback</code>，则会更新 <code>deps</code> 信息到 hooks 对应元素，以备下次对比使用</li><li>如果没有指定第二个参数 <code>deps</code>，则每次都会执行 <code>callback</code></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> EffectExample = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&quot;effect with deps&quot;</span>, count);</span><br><span class="line">  &#125;, [count]);</span><br><span class="line">  useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&quot;effect with empty deps&quot;</span>);</span><br><span class="line">  &#125;, []);</span><br><span class="line">  useEffect(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&quot;effect without deps&quot;</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="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>You clicked &#123;count&#125; times<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="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;Click me<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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><img src="../images/react-hooks-07.jpg"><a href="https://codesandbox.io/s/tinyreact-more-hooks-t5ylj?file=/src/index.js">TinyReact-more-hooks - CodeSandbox</a></p><p>相比官方的 <code>useState</code>，这里我们并没有支持在 <code>callback</code> 中返回析构函数，从而能够在 <code>deps</code> 发生变更或者删除函数组件时进行清理工作。感兴趣的同学可以在上面实现的基础上继续探索。</p><p><strong>useCallback</strong></p><p><code>useCallback</code> 的目的是可以在函数组件多次执行时仍然返回同一个回调函数，主要目的是为了避免子组件因为每次新创建的回调函数而重新渲染。它的逻辑其实和 <code>useEffect</code> 是非常相像的，甚至连参数都一样。</p><p>下面是 <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><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="function"><span class="keyword">function</span> <span class="title">useCallback</span>(<span class="params">callback, deps</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; hooks &#125; = wipInstance;</span><br><span class="line">  <span class="keyword">if</span> (hooks[wipHookIndex] &amp;&amp; deps) &#123;</span><br><span class="line">    <span class="keyword">const</span> [oldCallback, oldDeps] = hooks[wipHookIndex];</span><br><span class="line">    <span class="keyword">if</span> (!deps.some(<span class="function">(<span class="params">el, i</span>) =&gt;</span> el !== oldDeps[i])) &#123;</span><br><span class="line">      wipHookIndex++;</span><br><span class="line">      <span class="keyword">return</span> oldCallback;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  hooks[wipHookIndex++] = [callback, deps];</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>useEffect</code> 的基础，上述逻辑并不复杂：</p><ul><li>hooks 对应元素存储的信息是一个二元 tuple，与传入的两个参数一致</li><li>首次执行（mount）时，存储到 hooks 对应元素然后返回 <code>callback</code></li><li>更新执行（update）时，通过对比新旧 <code>deps</code> ，如一致则返回之前的 <code>callback</code></li><li>如未指定 <code>deps</code>，则每次都返回最新的 <code>callback</code></li></ul><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">const</span> BigList = <span class="function">(<span class="params">&#123; onClick &#125;</span>) =&gt;</span> (</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">li</span> <span class="attr">onClick</span>=<span class="string">&#123;onClick&#125;</span>&gt;</span>Foo<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">li</span> <span class="attr">onClick</span>=<span class="string">&#123;onClick&#125;</span>&gt;</span>Bar<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">li</span> <span class="attr">onClick</span>=<span class="string">&#123;onClick&#125;</span>&gt;</span>Baz<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">const</span> CallbackExample = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> onClick = useCallback(<span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&quot;click&quot;</span>, count, event.currentTarget.textContent);</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="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>You clicked &#123;count&#125; times<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="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;Click me<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">BigList</span> <span class="attr">onClick</span>=<span class="string">&#123;onClick&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="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>经过观察，可以发现在当前 TinyReact 实现中并没有降低无效渲染次数，在 <code>count</code> 变更是 <code>BigList</code> 依然进行了渲染。原因就在于 TinyReact 并没有在每次更新函数组件前进行 props  比较，而这一工作实际上是由 <code>React.memo</code> 负责，也因此日常开发中 <code>useCallback</code> 常常与 <code>React.memo</code> 结合使用。感兴趣的同学可以挑战下。</p><p><strong>useMemo</strong></p><p><code>useMemo</code> 就像是 <code>useCallback</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useMemo</span>(<span class="params">create, deps</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; hooks &#125; = wipInstance;</span><br><span class="line">  <span class="keyword">if</span> (hooks[wipHookIndex] &amp;&amp; deps) &#123;</span><br><span class="line">    <span class="keyword">const</span> [oldValue, oldDeps] = hooks[wipHookIndex];</span><br><span class="line">    <span class="keyword">if</span> (!deps.some(<span class="function">(<span class="params">el, i</span>) =&gt;</span> el !== oldDeps[i])) &#123;</span><br><span class="line">      wipHookIndex++;</span><br><span class="line">      <span class="keyword">return</span> oldValue;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> newValue = create();</span><br><span class="line">  hooks[wipHookIndex++] = [newValue, deps];</span><br><span class="line">  <span class="keyword">return</span> newValue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>显而易见，除了 hooks 对应元素保存的 tuple 中第一个数据从 <code>callback</code> 变成了 <code>create</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// An expensive operation</span></span><br><span class="line"><span class="keyword">const</span> fibonacci = <span class="function">(<span class="params">n</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">0</span> || n === <span class="number">1</span>) <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">return</span> fibonacci(n - <span class="number">1</span>) + fibonacci(n - <span class="number">2</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> MemoExample = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> [input, setInput] = useState(<span class="number">10</span>);</span><br><span class="line">  <span class="keyword">const</span> result = useMemo(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&quot;memo&quot;</span>, input);</span><br><span class="line">    <span class="keyword">return</span> fibonacci(input);</span><br><span class="line">  &#125;, [input]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>Fibonacci of input &#123;input&#125; is: &#123;result&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setInput(input + 1)&#125;&gt;Change input<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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;Change state<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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><strong>useRef</strong></p><p>让我们再来实现最后一个 Hook：<code>useRef</code>，它主要用来保存一个可以在组件的整个生命周期内持续存在的 ref 对象，其 <code>current</code> 属性被初始化为传入的 <code>initialValue</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useRef</span>(<span class="params">initialValue</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; hooks &#125; = wipInstance;</span><br><span class="line">  <span class="keyword">if</span> (!hooks[wipHookIndex]) &#123;</span><br><span class="line">    hooks[wipHookIndex] = &#123; <span class="attr">current</span>: initialValue &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> hooks[wipHookIndex++];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Aha！这是最为简单的一个 Hook 了，在虚拟 DOM 实例 hooks 对应位置存储的就是一个对象而已。</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="keyword">const</span> RefExample = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> ref = useRef(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> onClick = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    ref.current++;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&#x27;ref count&#x27;</span>, ref.current);</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="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;ref.current&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="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 me<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="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>每次点击按钮的时候，<code>ref.current</code> 都会自增然后打印信息到控制台，但不会像 <code>useState</code> 一样触发组件更新，p 标签里的内容一直是 0。</p><p><strong>其它 Hooks</strong></p><p>还有一些 Hooks 本文尚未提及，譬如 <code>useContext</code>、<code>useImperativeHandle</code> 、<code>useLayoutEffect</code> 等，它们与 React 的实现密切相关，限于篇幅暂且搁置。</p><p>以上所有示例代码可以到下列 CodeSandbox 查阅体验：<a href="https://codesandbox.io/s/tinyreact-more-hooks-t5ylj?file=/src/index.js">TinyReact-more-hooks - CodeSandbox</a></p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>非常感谢你看到了这里，从零实现一个 React 并且添加 Hooks 支持是一段挑战十足的旅程。</p><p>React Hooks 极大地改变了 React 的开发方式，它以一种非常符合直觉、简单易懂的方式为我们提供了一套可靠的原语，社区里也逐渐涌现出如 <a href="https://github.com/streamich/react-use">react-use</a>、<a href="https://ahooks.js.org/">ahooks</a> 等方便易用的 Hook 库帮助我们把工作化繁为简，更重要的是，代码量更少了！</p><p>React Hooks 仍然存在许多不足，尤其对于新手来说需要转变他们的 mindset，还有 RFC 中提到的<a href="https://github.com/reactjs/rfcs/blob/master/text/0068-react-hooks.md#drawbacks">种种问题</a>。不过瑕不掩瑜，近两年的快速发展一方面改善了许多缺陷，另外也证明了它就是 React 的未来。</p><p>通过层层递进的讲述和举例，希望这篇文章能够让你对 Hooks 背后的工作机制有一个深入理解，并能在实际开发中灵活运用，用更具效能的方式打造一流的用户体验！</p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p><img src="../images/react-hooks-08.jpg"></p><p>在 React Conf 2018，Dan 在介绍完首次公开亮相的 React Hooks 后，作为结束语，他深情款款地讲述了他对 React logo 的新思考：</p><blockquote><p>I found a different interpretation that made more sense to me.  </p><p>The way I think about it, we know that physical matter consists of atoms and we’ve learned that it’s the types of these atoms and their properties that determine how the physical matter looks and behaves. React has taught me something similar. You can take a user interface, and you can split it into these independent units called components. It’s the types and properties of these components that can describe how the user interface looks and behaves.   </p><p>What’s ironic though is that the word ‘atom’ literally means indivisible. When scientists just discovered atom for the first time, they thought this is the smallest thing we’re gonna find. But later they discovered an electron which is a smaller particle inside the atom. It turns out that actually electrons explain a lot about how atoms work.  </p><p>I kind of feel the same way of Hooks. I don’t think Hooks are a new feature, rather I think Hooks provide me with access to React features that I already know, such as state and context and lifecycle. I feel like Hooks are a more direct representation of React, they really explain how a component works inside. I feel like they’ve been hiding in plain sight for 4 years.  </p><p>In fact, if you look at the React logo, you can see those electron orbits there, so maybe Hooks have been there all along.</p></blockquote><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><a href="https://reactjs.org/docs/hooks-intro.html">Introducing Hooks - Reactjs org</a></li><li><a href="https://pomb.us/build-your-own-react/">Build your own react - Rodrigo Pombo</a></li><li><a href="https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/">Deep dive: How do React hooks really work? - Netlify</a></li><li><a href="https://www.ruanyifeng.com/blog/2020/09/react-hooks-useeffect-tutorial.html">轻松学会 React 钩子：以 useEffect() 为例 - 阮一峰</a></li><li><a href="https://dmitripavlutin.com/dont-overuse-react-usecallback/">Your Guide to React.useCallback()</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;自 19 年 &lt;a href=&quot;https://reactjs.org/blog/2019/02/06/react-v16.8.0.html&quot;&gt;React 16.8&lt;/a&gt; 正式发布 Hooks 以来，已然在社区中形成了席卷之势，甚至也影响到了很多其它前端库以及框架。目前介绍如何使用 Hooks 的文章汗牛充栋，本文着重从原理层面，从零开始构建一个简化版 Hooks 实现，期望能够让你知其然也知其所以然，更好地把握和运用 Hooks。&lt;/p&gt;
&lt;h2 id=&quot;为什么要引入-Hooks？&quot;&gt;&lt;a href=&quot;#为什么要引入-Hooks？&quot; class=&quot;headerlink&quot; title=&quot;为什么要引入 Hooks？&quot;&gt;&lt;/a&gt;为什么要引入 Hooks？&lt;/h2&gt;&lt;p&gt;React 团队的解释是目前以 React.Component 类为主的组件实现方式下，存在如下&lt;a href=&quot;https://reactjs.org/docs/hooks-intro.html#motivation&quot;&gt;三方面问题&lt;/a&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;组件之间一些包含状态的逻辑很难拆分复用&lt;/strong&gt; 因为函数式组件不能处理状态，且之前 Mixin 模式已被证伪，目前社区主流的 HoC 和 render props  模式也被诟病导致 wrapper hell&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂的大型组件越发难以理解和维护&lt;/strong&gt; 关联紧密的逻辑被迫分散在各个 lifecycle 方法中，非常容易出现疏漏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件类的方式对开发者和机器都不友好&lt;/strong&gt; 难以理解的 this，回调函数需要手动绑定，还有很多诸如此类的组件类模式的认知成本，导致新手入门难度高。同样地，组件类因为有内部状态、继承逻辑等特征，对编译器的各种优化也非常不友好&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Dan 在 React Conf 2018 大会上介绍完这几个问题后，也再进一步地说明其实这三个问题本质是一个问题的三种体现而已。而这个本质的问题就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;React does not provide a simpler, smaller, lightweight primitive to add state and lifecycle than a class component.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;换言之，React 本身非常函数式的设计哲学， &lt;img src=&quot;../images/react-hooks-01.jpg&quot; alt=&quot;fn(data) = UI&quot;&gt; ，并没有被当前组件类模式很好地表达。&lt;/p&gt;
&lt;p&gt;为了彻底解决这一问题，在 React 团队的 Sebastian 等人的带领下，经过参考和探索诸如 &lt;a href=&quot;https://github.com/reactjs/react-future/blob/master/07%20-%20Returning%20State/01%20-%20Stateful%20Functions.js&quot;&gt;Stateful Functions&lt;/a&gt;、&lt;a href=&quot;http://displayscript.org/introduction.html&quot;&gt;DisplayScript&lt;/a&gt;、&lt;a href=&quot;https://github.com/ocamllabs/ocaml-effects-tutorial#2-effectful-computations-in-a-pure-setting&quot;&gt;Algebraic effects in Multicore OCaml&lt;/a&gt; 等方案后，给出了 React Hooks 方案。&lt;/p&gt;</summary>
    
    
    
    
    <category term="React" scheme="https://springuper.github.io/tags/React/"/>
    
    <category term="Hooks" scheme="https://springuper.github.io/tags/Hooks/"/>
    
    <category term="Web" scheme="https://springuper.github.io/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>“觉醒年代”有感</title>
    <link href="https://springuper.github.io/the-awakeing-age/"/>
    <id>https://springuper.github.io/the-awakeing-age/</id>
    <published>2021-08-12T22:09:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>今天追完了“觉醒年代”，从来没有一部电视剧让我这样心潮澎湃，诸多思绪喷薄涌动，不吐不快。</p><h2 id="总体"><a href="#总体" class="headerlink" title="总体"></a>总体</h2><p>我对民国这段历史的了解除了历史书本之外，主要就是国史大纲最后的缪缪数语，以及各种民国大师们的点滴片段。觉醒年代自 1915 年袁世凯和日本签订二十一条开始，至 1921 年中国共产党正式成立剧终，期间重点以新青年杂志引领的新文化运动、五四运动、建立党组织为主线，生动演绎了陈独秀、李大钊、教员、蔡元培、北洋政府等各色人等的精彩故事和心路历程。</p><p>为了点题，剧中最为重要的一条线就是以陈独秀和李大钊为代表的，为探索救国救民道路奋斗不已的一群人的思想转变过程，最终找到了马克思主义这条真理。可以说是十分扣题的。这其中李大钊觉醒地最早也最为坚决，陈独秀紧随其后。教员显然道行更深，轻易不表态，坚持实践和检验，了解各种路线之后，最终一锤定音，并真的做到了“奋斗终生”。延年乔年也是多次尝试了无政府主义后，才在父亲的感召下幡然醒悟。这背后，还有被他们影响的一批青年志士。是他们这些觉醒者如星星之火，点燃了这个沉睡几百年的中国。</p><p>毫无疑问，这段历史是如此地鲜活、奋进、激动人心。一批人愿意放弃优渥的生活，放弃人上人的特权，放弃自己无限美好的未来，只是为了一个简单朴素的理想：救国救民。我想不到有什么更钦佩的话来向他们致敬，可能这一句能聊表心意：</p><blockquote><p><em>中国总是被他们最勇敢的人保护</em>的<em>很好。</em><br>——基辛格《论<em>中国</em>》</p></blockquote><h2 id="教员"><a href="#教员" class="headerlink" title="教员"></a>教员</h2><p>剧中粉墨登场的主要有两个人，教员是第一个。相信看了那段出场片段的朋友都会印象深刻：雨中各色人等粉墨登场，豪华轿车里吃着西洋面包的小公子，街边卖儿卖女的难民，生动刻画出那个年代阶级的巨大差异。一个青年匆匆走来，他把这一切都看在了眼里，手里倾心呵护着拿生活费买来的几本新青年，步伐坚定，斗志昂扬。他来了~</p><p>教员从湖南师范毕业，被引荐到北大担任图书馆管理员，从此和李大钊结识，因为思维深刻，很快成为良师益友。教员后来也曾说过早期李大钊对他影响巨大，侧面说明了这段时间他们交流的深入程度。</p><p>有两个事情让我对教员有了更深的理解。</p><p>其一是他为新青年投递的第一篇文章“体育之研究”：<img src="../images/the-awakeing-age-01.jpg"></p><p>据说这是他对于陈独秀创刊词中的六大标准的补遗：强壮的而非懦弱的。他认为“国力恭弱，武风不振，民族之体质，日趋轻细。此甚可忧之现象也”。都说湘人彪悍，看来都非常注重体育锻炼。另外他能独立思考，拾漏补遗，并写出来这篇体育研究的开篇之作，确实体现出了一代雄主的韬略。</p><span id="more"></span><p>另外一件事就是他非常喜欢了解各种思潮，并通过实际检验来做出决策。一开始李大钊跟他探讨救国之路时，教员还在研究无政府主义等几种方案，并没有什么明确的倾向。即便在马克思主义先驱守常的耳濡目染之下，都不动如山。后来他去上海又搞了一段时间的无政府主义实验，失败之后找到陈独秀，非常坚定的说明了自己接受马克思主义思想的决定，并坦言愿为之终身奋斗。轻易不下结论，谨慎再谨慎。而一旦有所定夺，则全力以赴，矢志不渝。</p><h2 id="鲁迅"><a href="#鲁迅" class="headerlink" title="鲁迅"></a>鲁迅</h2><p>另外一个浓墨重彩出厂的就是大文豪鲁迅先生。和教员类似，鲁迅先生也属于人狠话不多的类型，而且尤甚。</p><p>之前这是从书本上的几篇课文了解到鲁迅是对旧社会充满了彻底的披露，对救国救民的学生志士充满了支持和同情。然而，通过这部电视剧，我对这位现代文学第一人有了更全方位的认识。</p><p>他对蔡元培毕恭毕敬，有求必应，总是回复说：我听蔡公的。其中一大壮举就是北大校徽的设计，当真是形神俱佳的上乘作品。</p><p>他有感而发，闷头写作，产出中国第一部白话文小说“狂人日记”，极力讽刺旧社会“吃人”的现实，社会影响力巨大。这并不是偶然，而是基于他多年来对于小说的深刻理解，新文化运动中的思潮变化，以及对这个“无可救药”的国家的深刻观察，最终因为一个疯掉的朋友作为引子喷涌而发。<img src="../images/the-awakeing-age-02.jpg"></p><p>他也有些孤傲，堂堂教育部司长，居然竖起一个“不干了”的木牌，来表明自己不懈与张勋复辟之流沦为同类的态度。当时的镜头竟让我不禁大笑，这个人物的形象也一下子跃动起来，还真是一个有些怪脾气的人啊。<img src="../images/the-awakeing-age-03.jpg"></p><p>他爱做比喻。大概是研究小说多年的缘故，鲁迅非常喜欢通过比喻来表达他的观点。譬如新青年准备取消同人编辑，需要决定是陈独秀来主编还是胡适，鲁迅就做了个比喻：</p><blockquote><p>假如将韬略比作一间仓库罢，独秀先生的是外面竖一面大旗，大书道：“内皆武器，来者小心！”但那门却开着的，里面有几枝枪，几把刀，一目了然，用不着提防。适之先生的是紧紧的关着门，门上粘一条小纸条道：“内无武器，请勿疑虑。”这自然可以是真的，但有些人——至少是我这样的人——有时总不免要侧着头想一想</p></blockquote><p>剧中还有一段非常精彩的文学评论环节，就是新青年编辑们一起探讨“狂人日记”，每个人都念出一段自己最为印象深刻的段落，美不胜收。</p><h2 id="胡适"><a href="#胡适" class="headerlink" title="胡适"></a>胡适</h2><p>以前并没有觉得胡适怎么样，不过从电视剧中，以及后续我查阅的一些资料，总算有些轮廓。他大概就是典型的“精致利己主义者”吧，一直倡导专注学术，致力于温和社会改良，任何有风险的事情都退避三舍，有面子的事情趋之若鹜。</p><p>也难得，他居然同时被国共两党痛骂：</p><blockquote><p>此人实为一个最无品格之文化买办，无以名之，只可名曰‘狐仙’，乃为害国家、为害民族文化之蟊贼。<br>1960年10月13日 蒋介石日记  </p><p>胡适这个人也真顽固，我们托人带信给他，劝他回来，也不知他到底贪恋什么。批判嘛，总没有什么好话。说实话，新文化运动他是有功劳的，不能一笔抹杀，应当实事求是。到了21世纪，那时候替他恢复名誉吧。<br>毛泽东</p></blockquote><p>当真是一个十足的“公知”！</p><h2 id="李大钊"><a href="#李大钊" class="headerlink" title="李大钊"></a>李大钊</h2><p>守常先生是我最为钦佩的，每当他主动担起一个新的职责，快步前往一个新的战场，毅然登上简陋的台子进行激动地演讲时，我就在想，如果当时是我在那里，我会如他一样身先士卒，慷慨激昂吗？非常惭愧，我很可能做不到。</p><p>有两个细节让我印象颇深：</p><ol><li>极度乐善好施。刚从日本回来，碰到街上的贫苦百姓慷慨解囊，居然还是当了很多自己的日常用品。后来到北大任职，月薪丰厚，但是却常常因为接济学生和赞助各种运动，最后蔡公不得不先行扣下 30 大洋来保证家用</li><li>演讲精彩纷呈。我一度觉得中国人是讷于言的，很少有人能在公开场合即兴演讲。但是在剧中，守常先生的演讲不下几十次，每次都那么鼓舞人心，振聋发聩。尤其是他深入到工人群众中去，引领了马克思主义在最广大人民中的传播<img src="../images/the-awakeing-age-04.jpg"></li></ol><p>先生千古！</p><h2 id="陈独秀"><a href="#陈独秀" class="headerlink" title="陈独秀"></a>陈独秀</h2><p>坦白来说，这部剧我认识最深刻的就是男一号陈独秀同志。以前仲甫被黑的太凶，一般的认知就是一个错误的早期领导人。但是这部剧让我看到了他的真实情状，一个为民族大义奔波不已的人，一个激情洋溢毫无城府的人，一个坦坦荡荡的性情中人，一个用笔杆子唤醒了一代青年的人。</p><p>相比李大钊的坚毅，陈独秀更加真性情。他会因为看到这个国家受尽苦难的底层人民而哭泣，他会见到老相识后激烈地拥抱，他会不断地找鲁迅催稿，他会说出监狱也是研究室之类的言辞。</p><p>然而这样一个觉醒了的人，后面却过得异常凄惨。两个儿子 27 年牺牲，自己也因为共产国际等各方面原因辞去领导职务。后面还坐过牢，晚年居然落魄到隐居教书，最后因为误食中毒而亡。<img src="../images/the-awakeing-age-05.jpg"></p><blockquote><p>他是五四运动时期的总司令，整个运动实际上是他领导的。他与周围的一群人，如李大钊同志等，是起了大作用的。我们那个时候学习作白话文，听他说什文章要加标点符号，这是一大发明，又听他说世界上有马克思主义。我们是他们那一代人的学生。五四运动，替中国共产党准备了干部。那个时候有《新青年》杂志，是陈独秀主编的。被这个杂志和五四运动警醒起来的人，后头有一部分进了共产党。这些人受陈独秀和他周围一群人的影响很大，可以说是由他们集合起来，这才成立了党。我说陈独秀在某几点上，好象俄国的普列汉诺夫，做了启蒙运动的工作，创造了党。<br>毛泽东</p></blockquote><p>在国家走向复兴的今天，这部剧代表了一种回归，开始给引领一个时代的仲甫一个正名，没有新青年引领的新文化运动，就没有后面的五四运动，没有五四运动，就没有后面的建党，没有党，就没有今天我们幸福的美好生活。他值得赞美，值得我们去歌颂！</p><h2 id="尾声"><a href="#尾声" class="headerlink" title="尾声"></a>尾声</h2><p>絮絮叨叨，有感而发，不成文章。但是我依然觉得要记录些什么，要给这些给我们这个国家带来深刻变化的先辈们说几句话。这不一定能改变什么，不过，点点滴滴，如果很多人都开始做了，就会汇成涛涛江河，奔腾入海。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;今天追完了“觉醒年代”，从来没有一部电视剧让我这样心潮澎湃，诸多思绪喷薄涌动，不吐不快。&lt;/p&gt;
&lt;h2 id=&quot;总体&quot;&gt;&lt;a href=&quot;#总体&quot; class=&quot;headerlink&quot; title=&quot;总体&quot;&gt;&lt;/a&gt;总体&lt;/h2&gt;&lt;p&gt;我对民国这段历史的了解除了历史书本之外，主要就是国史大纲最后的缪缪数语，以及各种民国大师们的点滴片段。觉醒年代自 1915 年袁世凯和日本签订二十一条开始，至 1921 年中国共产党正式成立剧终，期间重点以新青年杂志引领的新文化运动、五四运动、建立党组织为主线，生动演绎了陈独秀、李大钊、教员、蔡元培、北洋政府等各色人等的精彩故事和心路历程。&lt;/p&gt;
&lt;p&gt;为了点题，剧中最为重要的一条线就是以陈独秀和李大钊为代表的，为探索救国救民道路奋斗不已的一群人的思想转变过程，最终找到了马克思主义这条真理。可以说是十分扣题的。这其中李大钊觉醒地最早也最为坚决，陈独秀紧随其后。教员显然道行更深，轻易不表态，坚持实践和检验，了解各种路线之后，最终一锤定音，并真的做到了“奋斗终生”。延年乔年也是多次尝试了无政府主义后，才在父亲的感召下幡然醒悟。这背后，还有被他们影响的一批青年志士。是他们这些觉醒者如星星之火，点燃了这个沉睡几百年的中国。&lt;/p&gt;
&lt;p&gt;毫无疑问，这段历史是如此地鲜活、奋进、激动人心。一批人愿意放弃优渥的生活，放弃人上人的特权，放弃自己无限美好的未来，只是为了一个简单朴素的理想：救国救民。我想不到有什么更钦佩的话来向他们致敬，可能这一句能聊表心意：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;中国总是被他们最勇敢的人保护&lt;/em&gt;的&lt;em&gt;很好。&lt;/em&gt;&lt;br&gt;——基辛格《论&lt;em&gt;中国&lt;/em&gt;》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;教员&quot;&gt;&lt;a href=&quot;#教员&quot; class=&quot;headerlink&quot; title=&quot;教员&quot;&gt;&lt;/a&gt;教员&lt;/h2&gt;&lt;p&gt;剧中粉墨登场的主要有两个人，教员是第一个。相信看了那段出场片段的朋友都会印象深刻：雨中各色人等粉墨登场，豪华轿车里吃着西洋面包的小公子，街边卖儿卖女的难民，生动刻画出那个年代阶级的巨大差异。一个青年匆匆走来，他把这一切都看在了眼里，手里倾心呵护着拿生活费买来的几本新青年，步伐坚定，斗志昂扬。他来了~&lt;/p&gt;
&lt;p&gt;教员从湖南师范毕业，被引荐到北大担任图书馆管理员，从此和李大钊结识，因为思维深刻，很快成为良师益友。教员后来也曾说过早期李大钊对他影响巨大，侧面说明了这段时间他们交流的深入程度。&lt;/p&gt;
&lt;p&gt;有两个事情让我对教员有了更深的理解。&lt;/p&gt;
&lt;p&gt;其一是他为新青年投递的第一篇文章“体育之研究”：
&lt;img src=&quot;../images/the-awakeing-age-01.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;据说这是他对于陈独秀创刊词中的六大标准的补遗：强壮的而非懦弱的。他认为“国力恭弱，武风不振，民族之体质，日趋轻细。此甚可忧之现象也”。都说湘人彪悍，看来都非常注重体育锻炼。另外他能独立思考，拾漏补遗，并写出来这篇体育研究的开篇之作，确实体现出了一代雄主的韬略。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Series" scheme="https://springuper.github.io/tags/Series/"/>
    
    <category term="Review" scheme="https://springuper.github.io/tags/Review/"/>
    
  </entry>
  
  <entry>
    <title>TypeScript 进阶</title>
    <link href="https://springuper.github.io/typescript-advanced/"/>
    <id>https://springuper.github.io/typescript-advanced/</id>
    <published>2021-05-15T13:33:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>在上一篇文章<a href="https://zhuanlan.zhihu.com/p/64446259">浅谈 TypeScript 类型系统</a>中，我们介绍了 TypeScript 的一些基础知识，也简要提到了泛型编程等进阶内容。在开发实践中，可能不少同学会发现虽然这些基础基本能够解决大部分问题，但是总时不时有一些解决不了的类型相关问题。</p><p>这篇文章在前文的基础上，进一步介绍一些 TypeScript 的进阶知识和应用，希望能够对你有所帮助。在内容编排上，我们首先引入一些典型问题，然后介绍相关的必备知识点，最后阐述这些问题的解决思路。</p><h2 id="典型问题"><a href="#典型问题" class="headerlink" title="典型问题"></a>典型问题</h2><p><strong>问题 A：</strong>现有一个工具方法 compose(f, g) 用以整合两个函数，如何添加合适的 type？</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">compose</span>(<span class="params">f, g</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function">(<span class="params">...args</span>) =&gt;</span> g(f(...args));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>问题 B：</strong>如何获取返回 Promise<T> 类型的函数中的类型 T，从而能够实现一些类似 React Query 中 <a href="https://github.com/tannerlinsley/react-query/blob/v3.15.2/src/react/useQuery.ts#L26-L35">useQuery</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><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  age?: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> fetchUser = (id: <span class="built_in">string</span>): <span class="built_in">Promise</span>&lt;User&gt; =&gt; &#123; ... &#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// status 为 &#x27;success&#x27; 时，data 类型应为 User</span></span><br><span class="line"><span class="keyword">const</span> &#123; status, data &#125; = useQuery(<span class="string">`getUser<span class="subst">$&#123;id&#125;</span>`</span>, <span class="function">() =&gt;</span> fetchUser(id));</span><br></pre></td></tr></table></figure><p><strong>问题 C：</strong>如何为工具方法 getProperty(obj, path) 添加合适的类型，从而使得 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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> person = &#123; </span><br><span class="line">  <span class="attr">name</span>: <span class="string">&#x27;Simon&#x27;</span>, </span><br><span class="line">  <span class="attr">school</span>: &#123; <span class="attr">name</span>: <span class="string">&#x27;MIT&#x27;</span>, <span class="attr">graduated</span>: <span class="number">2018</span> &#125;,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// 第二个参数 path 应对应第一个对象参数的属性路径</span></span><br><span class="line"><span class="keyword">const</span> graduatedYear = getProperty(person, <span class="string">&#x27;school.graduated&#x27;</span>);</span><br></pre></td></tr></table></figure><span id="more"></span><p>如果你对自己的 TypeScript 技能有信心，不妨在继续阅读之前挑战下这几个问题。</p><h2 id="进阶知识点"><a href="#进阶知识点" class="headerlink" title="进阶知识点"></a>进阶知识点</h2><p><strong>类型参数推断（Type argument inference）</strong></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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">identity</span>&lt;<span class="title">T</span>&gt;(<span class="params">arg: T</span>): <span class="title">T</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> arg;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> output = identity&lt;<span class="built_in">string</span>&gt;(<span class="string">&#x27;myString&#x27;</span>);</span><br></pre></td></tr></table></figure><p>上例在调用 identity 方法时传入的实参已经是一个字符串字面量，我们自然而然地期望 TypeScript 编译器足够聪明，能从这个参数推断出实例化类型是 string。官方团队很早就支持了类型参数推断，让开发者写代码更为舒爽：</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> output = identity(<span class="string">&#x27;myString&#x27;</span>); <span class="comment">// output 类型为 string</span></span><br><span class="line"><span class="keyword">const</span> output2 = identity(<span class="literal">true</span>); <span class="comment">// output2 类型为 boolean</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><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> output3 = identity([<span class="number">1</span>, <span class="string">&#x27;spring&#x27;</span>]); <span class="comment">// output3 类型是 (number | string)[]</span></span><br><span class="line"><span class="comment">// 如果你期望传入的类型是一个 [number, string] 的 Tuple，可以显式指定实例化类型</span></span><br><span class="line"><span class="keyword">const</span> output4 = identity&lt;[<span class="built_in">number</span>, <span class="built_in">string</span>]&gt;([<span class="number">1</span>, <span class="string">&#x27;spring&#x27;</span>]);</span><br></pre></td></tr></table></figure><p><strong>多重含义的 extends</strong></p><p>TypeScript 的类型操作符有一些和 JavaScript 操作符重合，譬如 typeof、in 等。然而最为关键的一个却是 extends，可谓集万千宠爱于一身。</p><p>Extends 的第一种用法是实现接口的继承：</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="comment">// 继承接口</span></span><br><span class="line"><span class="keyword">interface</span> Animal &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> Bird <span class="keyword">extends</span> Animal &#123;</span><br><span class="line">  <span class="attr">fly</span>: <span class="built_in">boolean</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> pigeon: Bird = &#123; <span class="attr">name</span>: <span class="string">&#x27;pigeon&#x27;</span>, <span class="attr">fly</span>: <span class="literal">true</span> &#125;;</span><br></pre></td></tr></table></figure><p>第二种用法也十分普遍，主要应用在<a href="https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints">泛型约束</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><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="function"><span class="keyword">function</span> <span class="title">getProperty</span>&lt;<span class="title">T</span>, <span class="title">K</span> <span class="title">extends</span> <span class="title">keyof</span> <span class="title">T</span>&gt;(<span class="params">obj: T, key: K</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> obj[key];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> x = &#123; <span class="attr">a</span>: <span class="number">1</span>, <span class="attr">b</span>: <span class="number">2</span> &#125;;</span><br><span class="line"></span><br><span class="line">getProperty(x, <span class="string">&#x27;a&#x27;</span>); <span class="comment">// Okay</span></span><br><span class="line">getProperty(x, <span class="string">&#x27;m&#x27;</span>); <span class="comment">// Error!</span></span><br></pre></td></tr></table></figure><p>根据类型参数推断 TypeScript 能够判断出 T 的实际类型是 { a: number, b: number }，附加的泛型约束是 K extends keyof T，即 K 是一个从 T 泛型衍生出来的类型 ‘a’ | ‘b’，因此第二个参数 key 只能是 a 或者 b。这一特性进一步提升了类型之间关系的表达力。</p><p>第三种用法是在 TypeScript 2.8 中引入的条件类型中，extends 担当条件语句关键的运算符角色：</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">SomeType <span class="keyword">extends</span> OtherType ? TrueType : FalseType;</span><br></pre></td></tr></table></figure><p>根据<a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html">定义</a>，如果 SomeType 可以<a href="https://www.typescriptlang.org/docs/handbook/type-compatibility.html">赋值</a>给 OtherType，则返回 TrueType，否则返回 FalseType。所以可以理解成这里的 SomeType extends OtherType 应该是返回了一个布尔值。</p><p>以上三种不同的 extends 用法具体实践上有细微的差异，但究其根本，其实是一以贯之的：A extends B 意味着 A 应该是 B 的子集。借助上一篇文章的集合论观点的话，可以表示为 <img src="../images/typescript-advanced-01.jpg" alt="A \subseteq B"> ：<img src="../images/typescript-advanced-02.jpg"><em>A extends B</em></p><p><strong>infer 模式匹配</strong></p><p>和条件类型一同发布的，还有一个至关重要的特性：使用 infer 关键字在条件语句中进行类型推断，从而实现了类型层面的模式匹配（pattern matching）。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Flatten&lt;T&gt; = T <span class="keyword">extends</span> <span class="built_in">any</span>[] ? T[<span class="built_in">number</span>] : T;</span><br><span class="line"><span class="keyword">type</span> F1 = Flatten&lt;<span class="built_in">string</span>[]&gt;; <span class="comment">// F1 为 string</span></span><br><span class="line"><span class="keyword">type</span> F2 = Flatten&lt;<span class="built_in">number</span>[]&gt;; <span class="comment">// F2 为 number</span></span><br></pre></td></tr></table></figure><p>相信很多人都会不太理解 T[number] 这种偏门的技巧，上例可以用 infer 重写为：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> FlattenInfer&lt;T&gt; = T <span class="keyword">extends</span> (infer Item)[] ? Item : T;</span><br><span class="line"><span class="keyword">type</span> F3 = FlattenInfer&lt;<span class="built_in">string</span>[]&gt;; <span class="comment">// F3 为 string</span></span><br><span class="line"><span class="keyword">type</span> F4 = FlattenInfer&lt;<span class="built_in">number</span>[]&gt;; <span class="comment">// F4 为 number</span></span><br></pre></td></tr></table></figure><p>在 F3 示例中，TypeScript 能够从传入的实例化类型 string[]，根据 (infer Item)[] 匹配出 string 类型并将其赋值给新声明的类型 Item，然后在条件为真的分支中使用。</p><p>让我们继续看一个更加实用的例子，使用 infer 提取函数返回类型：</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"><span class="keyword">type</span> ReturnType&lt;T&gt; = T <span class="keyword">extends</span> (...args: <span class="built_in">any</span>[]) =&gt; infer R ? R : <span class="built_in">any</span>;</span><br><span class="line"><span class="keyword">type</span> R1 = ReturnType&lt;<span class="function">() =&gt;</span> <span class="built_in">string</span>&gt;; <span class="comment">// R1 为 string</span></span><br><span class="line"><span class="keyword">type</span> R2 = ReturnType&lt;<span class="function">(<span class="params">a: <span class="built_in">number</span></span>) =&gt;</span> &#123; <span class="attr">name</span>: <span class="built_in">string</span> &#125;&gt;; <span class="comment">// R2 为 &#123; name: string &#125;</span></span><br><span class="line"><span class="keyword">type</span> R3 = ReturnType&lt;<span class="built_in">boolean</span>&gt;; <span class="comment">// R3 为 any</span></span><br></pre></td></tr></table></figure><p>模式匹配的威力再一次体现出来，infer R 准确匹配到了 () =&gt; string 与 (a: number) =&gt; { name: string } 两个方法的返回值，最后一个例子因为 boolean 并不是一个函数所以没有进入模式匹配。ReturnType 是如此常用，所以 TypeScript 直接将其作为内置工具类型。</p><p><strong>合集类型与分配律</strong></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><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">type</span> UnionCompositeType = (&#123; <span class="attr">a</span>: <span class="built_in">string</span> &#125; | &#123; <span class="attr">b</span>: <span class="built_in">string</span> &#125;) &amp; &#123; <span class="attr">c</span>: <span class="built_in">string</span> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 推导</span></span><br><span class="line">(&#123; <span class="attr">a</span>: <span class="built_in">string</span> &#125; | &#123; <span class="attr">b</span>: <span class="built_in">string</span> &#125;) &amp; &#123; <span class="attr">c</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">==&gt; (&#123; <span class="attr">a</span>: <span class="built_in">string</span> &#125; &amp; &#123; <span class="attr">c</span>: <span class="built_in">string</span> &#125;) | (&#123; <span class="attr">b</span>: <span class="built_in">string</span> &#125; &amp; &#123; <span class="attr">c</span>: <span class="built_in">string</span> &#125;) </span><br><span class="line">==&gt; &#123; <span class="attr">a</span>: <span class="built_in">string</span>; c: <span class="built_in">string</span> &#125; | &#123; <span class="attr">b</span>: <span class="built_in">string</span>; c: <span class="built_in">string</span> &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> objectA: UnionCompositeType = &#123;  <span class="comment">// Okay</span></span><br><span class="line">  <span class="attr">a</span>: <span class="string">&#x27;javascript&#x27;</span>,</span><br><span class="line">  <span class="attr">c</span>: <span class="string">&#x27;great&#x27;</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> objectB: UnionCompositeType = &#123;  <span class="comment">// Okay</span></span><br><span class="line">  <span class="attr">b</span>: <span class="string">&#x27;javascript&#x27;</span>,</span><br><span class="line">  <span class="attr">c</span>: <span class="string">&#x27;not bad&#x27;</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> objectC: UnionCompositeType = &#123;  <span class="comment">// Error!</span></span><br><span class="line">  <span class="attr">c</span>: <span class="string">&#x27;not good enough though&#x27;</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>TypeScript 在推出条件类型时，也第一时间支持了检查类型是合集时的分配律，并称之为<a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types">分布条件类型</a>。简单来说，就是 T extends U 中的 T 实例化类型为 A | B | C 时，那么条件类型表达式会被展开为：</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">(A <span class="keyword">extends</span> U ? X : Y) | (B <span class="keyword">extends</span> U ? X : Y) | (C <span class="keyword">extends</span> U ? X : Y)</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><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">type</span> TypeName&lt;T&gt; = T <span class="keyword">extends</span> <span class="built_in">string</span></span><br><span class="line">  ? <span class="string">&#x27;string&#x27;</span></span><br><span class="line">  : T <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">  ? <span class="string">&#x27;number&#x27;</span></span><br><span class="line">  : <span class="string">&#x27;other&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> T1 = TypeName&lt;<span class="built_in">string</span> | <span class="built_in">number</span>&gt;; <span class="comment">// &#x27;string&#x27; | &#x27;number&#x27;</span></span><br><span class="line"><span class="keyword">type</span> T2 = TypeName&lt;<span class="built_in">string</span> | <span class="function">() =&gt;</span> <span class="built_in">void</span>&gt;; <span class="comment">// &#x27;string&#x27; | &#x27;other&#x27;</span></span><br><span class="line"><span class="keyword">type</span> T3 = TypeName&lt;<span class="built_in">boolean</span> | <span class="built_in">void</span>&gt;; <span class="comment">// &#x27;other&#x27;</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><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Extract&lt;T, U&gt; = T <span class="keyword">extends</span> U ? T : <span class="built_in">never</span>;</span><br><span class="line"><span class="keyword">type</span> Extract1 = Extract&lt;<span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;b&#x27;</span> | <span class="string">&#x27;c&#x27;</span> | <span class="string">&#x27;d&#x27;</span>, <span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;c&#x27;</span> | <span class="string">&#x27;f&#x27;</span>&gt;; <span class="comment">// &#x27;a&#x27; | &#x27;c&#x27;</span></span><br><span class="line"><span class="keyword">type</span> Extract2 = Extract&lt;<span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;b&#x27;</span>, keyof &#123; <span class="attr">a</span>: <span class="built_in">string</span>, <span class="attr">c</span>: <span class="built_in">string</span> &#125;&gt;; <span class="comment">// &#x27;a&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Exclude&lt;T, U&gt; = T <span class="keyword">extends</span> U ? <span class="built_in">never</span> : T;</span><br><span class="line"><span class="keyword">type</span> Exclude1 = Exclude&lt;<span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;b&#x27;</span> | <span class="string">&#x27;c&#x27;</span> | <span class="string">&#x27;d&#x27;</span>, <span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;c&#x27;</span> | <span class="string">&#x27;f&#x27;</span>&gt;; <span class="comment">// &#x27;b&#x27; | &#x27;d&#x27;</span></span><br><span class="line"><span class="keyword">type</span> Exclude2 = Exclude&lt;<span class="built_in">string</span> | <span class="function">() =&gt;</span> <span class="built_in">void</span>, <span class="built_in">Function</span>&gt;; <span class="comment">// string</span></span><br></pre></td></tr></table></figure><p>在新近发布的 4.1 中，TypeScript 新增的<a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types">模板字面量类型</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Color = <span class="string">&#x27;red&#x27;</span> | <span class="string">&#x27;blue&#x27;</span>;</span><br><span class="line"><span class="keyword">type</span> Quantity = <span class="string">&#x27;one&#x27;</span> | <span class="string">&#x27;two&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> SeussFish = <span class="string">`<span class="subst">$&#123;Quantity | Color&#125;</span> fish`</span>; <span class="comment">// 类型为 &#x27;one fish&#x27; | &#x27;two fish&#x27; | &#x27;red fish&#x27; | &#x27;blue fish&#x27;</span></span><br></pre></td></tr></table></figure><p>但并非所有的工具类型都支持合集的分配律，譬如 Pick&lt;T, K&gt; 和 Omit&lt;T, K&gt;，具体 TypeScript 有一些性能、复杂度等方面的<a href="https://davidgomes.com/pick-omit-over-union-types-in-typescript/">考量</a>，且有一些 workarounds。</p><h2 id="问题解答"><a href="#问题解答" class="headerlink" title="问题解答"></a>问题解答</h2><p>在理解了上述进阶知识点后，我们接下来探讨前面三个问题的解法。</p><p><strong>问题 A：compose(f, g)</strong></p><p>首先我们需要厘清的问题是，什么样的类型算是比较合适的，具体到这个工具方法，应满足如下要求：</p><ol><li>f 的输入类型应该是 compose 方法的输入类型</li><li>f 的输出类型应该是 g 方法的输入类型</li><li>g 方法的输出类型应是 compose 方法的输出类型</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><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">// 第一步：分别列出 f、g、compose 的类型：</span></span><br><span class="line"><span class="comment">// f: (...args: A) =&gt; B</span></span><br><span class="line"><span class="comment">// g: (arg: B) =&gt; C</span></span><br><span class="line"><span class="comment">// compose: (...args: A) =&gt; C</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 第二步：尝试将上述类型引入 compose 方法中</span></span><br><span class="line"><span class="comment">// TypeScript 报错：(...args: A) A rest parameter must be of an array type</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">compose</span>&lt;<span class="title">A</span>, <span class="title">B</span>, <span class="title">C</span>&gt;(<span class="params">f: (...args: A) =&gt; B, g: (arg: B) =&gt; C</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function">(<span class="params">...args: A</span>) =&gt;</span> g(f(...args));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第三步：添加泛型约束 A extends any[]</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">compose</span>&lt;<span class="title">A</span> <span class="title">extends</span> <span class="title">any</span>[], <span class="title">B</span>, <span class="title">C</span>&gt;(<span class="params">f: (...args: A) =&gt; B, g: (arg: B) =&gt; C</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function">(<span class="params">...args: A</span>) =&gt;</span> g(f(...args));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在实际调用 compose 方法时，绝大部分情况下我们并不需要显式指定 A、B、C 泛型的实例化类型，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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getName</span>(<span class="params">obj: &#123; name: <span class="built_in">string</span> &#125;</span>) </span>&#123; <span class="keyword">return</span> obj.name; &#125;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getLength</span>(<span class="params">s: <span class="built_in">string</span></span>) </span>&#123; <span class="keyword">return</span> s.length; &#125;</span><br><span class="line"><span class="keyword">const</span> lengthOfName = compose(getName, getLength); <span class="comment">// lengthOfName 类型为 number</span></span><br></pre></td></tr></table></figure><p>这个问题应用到了前文的类型参数推断和泛型约束两个知识点。</p><p><strong>问题 B：提取 Promise<T> 中的 T</strong></p><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="keyword">type</span> UnPromisify&lt;T&gt; = T <span class="keyword">extends</span> <span class="built_in">Promise</span>&lt;infer U&gt; ? U : T;</span><br></pre></td></tr></table></figure><p>是不是太过简单了？的确如此。接下来让我们继续尝试给 useQuery 添加类型吧：</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><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">// 第一步：定义 useQuery 的返回值类型，这里我们应用到了可辨析联合</span></span><br><span class="line"><span class="keyword">type</span> QueryIdleResult&lt;TData, TError&gt; = &#123;</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;idle&#x27;</span>;</span><br><span class="line">  data: <span class="literal">undefined</span>;</span><br><span class="line">  error: <span class="literal">null</span>;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">type</span> QueryLoadingResult&lt;TData, TError&gt; = &#123;</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;loading&#x27;</span>;</span><br><span class="line">  data: <span class="literal">undefined</span>;</span><br><span class="line">  error: <span class="literal">null</span>;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">type</span> QueryErrorResult&lt;TData, TError&gt; = &#123;</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;error&#x27;</span>;</span><br><span class="line">  data: TData;</span><br><span class="line">  error: TError;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">type</span> QuerySuccessResult&lt;TData, TError&gt; = &#123;</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;success&#x27;</span>;</span><br><span class="line">  data: TData;</span><br><span class="line">  error: <span class="literal">null</span>;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">type</span> QueryResult&lt;TData, TError&gt; =</span><br><span class="line">  | QueryIdleResult&lt;TData, TError&gt;</span><br><span class="line">  | QueryLoadingResult&lt;TData, TError&gt;</span><br><span class="line">  | QueryErrorResult&lt;TData, TError&gt;</span><br><span class="line">  | QuerySuccessResult&lt;TData, TError&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第二步：尝试为 useQuery 添加类型签名，这里我们将 queryFn 定义为泛型 T，然后尝试析取它的返回类型并做 Promise 解包操作</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useQuery</span>&lt;<span class="title">T</span> <span class="title">extends</span> (<span class="params">...args: <span class="built_in">any</span>[]</span>) =&gt; <span class="title">any</span>, <span class="title">TError</span> = <span class="title">Error</span>&gt;</span></span><br><span class="line"><span class="function">  (<span class="params">queryKey: <span class="built_in">string</span>, queryFn: T</span>): <span class="title">QueryResult</span>&lt;<span class="title">UnPromisify</span>&lt;<span class="title">ReturnType</span>&lt;<span class="title">T</span>&gt;&gt;, <span class="title">TError</span>&gt; </span>&#123; ... &#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> id = <span class="string">&#x27;123&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> user = useQuery(<span class="string">`getUser<span class="subst">$&#123;id&#125;</span>`</span>, <span class="function">() =&gt;</span> fetchUser(id));</span><br><span class="line"><span class="keyword">if</span> (user.status === <span class="string">&#x27;loading&#x27;</span>) &#123;</span><br><span class="line">  <span class="built_in">console</span>.log(result.data); <span class="comment">// 类型为 undefined</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (user.status === <span class="string">&#x27;success&#x27;</span>) &#123;</span><br><span class="line">  <span class="built_in">console</span>.log(result.data);  <span class="comment">// 类型为 User</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (user.status === <span class="string">&#x27;error&#x27;</span>) &#123;</span><br><span class="line">  <span class="built_in">console</span>.log(result.error);  <span class="comment">// 类型为 Error</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个解法应用前文提到的类型参数推断、泛型约束和 infer 模式匹配等知识点。</p><p>然而，在 React Query 的官方实现中，采用了一种更为简洁的方式：</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="function"><span class="keyword">function</span> <span class="title">useQuery</span>&lt;<span class="title">TData</span> = <span class="title">unknown</span>, <span class="title">TError</span> = <span class="title">unknown</span>&gt;</span></span><br><span class="line"><span class="function">  (<span class="params">queryKey: <span class="built_in">string</span>, queryFn: () =&gt; <span class="built_in">Promise</span>&lt;TData&gt;</span>): <span class="title">QueryResult</span>&lt;<span class="title">TData</span>, <span class="title">TError</span>&gt;</span></span><br></pre></td></tr></table></figure><p>不得不感慨：一顿操作猛如虎，一看战绩零杠五。因此也不是什么情况下都需要用到非常复杂的类型特性，Keep it simple and stupid 反而是最优解。</p><p><strong>问题 C：getProperty(obj, path)</strong></p><p>在前文介绍泛型约束时，我们已经举例说明了 path 为简单属性情况下的解法，接下来，让我们继续探讨 path 为多层属性（’a.b.c’）时如何处理。</p><p>假定 obj 的类型为 T，path 的类型为 Path，显然 Path 应为 T 推导而来。最简单的情况为 path 仅包含简单属性：</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">type</span> PathKeys&lt;T&gt; = T <span class="keyword">extends</span> <span class="built_in">object</span> ? keyof T : <span class="built_in">never</span>;</span><br><span class="line">getProperty&lt;T, Path <span class="keyword">extends</span> PathKeys&lt;T&gt;&gt;(obj: T, <span class="attr">path</span>: Path);</span><br></pre></td></tr></table></figure><p>考虑到有些情况下对象的 key 可能是 number、symbol 等其它类型，但这里我们期望只支持 string 类型，可以将 PathKeys<T> 定义改进为：</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">type</span> PathKeys&lt;T&gt; = T <span class="keyword">extends</span> <span class="built_in">object</span> ? Extract&lt;keyof T, <span class="built_in">string</span>&gt; : <span class="built_in">never</span>;</span><br></pre></td></tr></table></figure><p>类似数学归纳法，现在我们明确了简单情况下的类型，下一步处理 path 为多层属性的情况。可能有些敏感的同学已经嗅到了递归的味道，是的，因为多层属性是通过递归的思路规约到最简单情况的。思路大致为：</p><ol><li>扩展 PathKeys<T> 的条件为真的分支，继续处理下一层属性</li><li>处理每个多层属性时，需加上当前层次的属性作为前缀，然后继续递归，从而最终得出所有可能属性路径</li><li>为了支持上诉两条，结合一般情况下递归的解决办法，可以定义一个新的 SubKeys&lt;T, K&gt; 类型作为中间桥梁</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> PathKeys&lt;T&gt; = T <span class="keyword">extends</span> <span class="built_in">object</span> ?</span><br><span class="line">  Extract&lt;keyof T, <span class="built_in">string</span>&gt; | SubKeys&lt;T, Extract&lt;keyof T, <span class="built_in">string</span>&gt;&gt; :</span><br><span class="line">  <span class="built_in">never</span>;</span><br><span class="line"><span class="keyword">type</span> SubKeys&lt;T, K <span class="keyword">extends</span> <span class="built_in">string</span>&gt; = K <span class="keyword">extends</span> keyof T ?</span><br><span class="line">  <span class="string">`<span class="subst">$&#123;K&#125;</span>.<span class="subst">$&#123;PathKeys&lt;T[K]&gt;&#125;</span>`</span> : </span><br><span class="line">  <span class="built_in">never</span>;</span><br><span class="line"><span class="keyword">declare</span> <span class="function"><span class="keyword">function</span> <span class="title">getProperty</span>&lt;<span class="title">T</span>, <span class="title">Path</span> <span class="title">extends</span> <span class="title">PathKeys</span>&lt;<span class="title">T</span>&gt;&gt;(<span class="params">obj: T, path: Path</span>)</span>;</span><br></pre></td></tr></table></figure><p>下图为在 VSCode 里面运行时的自动补全提示，可以感受下完善的类型系统给我们带来的便利和惊喜：<img src="../images/typescript-advanced-03.jpg"></p><p>然而这并不是结束，我们还有一个类型需要一并完成：返回值类型。理想的结果应该是，以上面的代码为例，TypeScript 应该可以根据我们传入的参数推断出 graduatedYear 的类型。</p><p>有了前面 PathKeys<T> 的思路作为借鉴，借助 infer 模式匹配，我们可以定义 PropertyType&lt;T, Path&gt; 如下：</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> PropertyType&lt;T, Path <span class="keyword">extends</span> <span class="built_in">string</span>&gt; =</span><br><span class="line">    Path <span class="keyword">extends</span> keyof T ? T[Path] :</span><br><span class="line">    Path <span class="keyword">extends</span> <span class="string">`<span class="subst">$&#123;infer K&#125;</span>.<span class="subst">$&#123;infer R&#125;</span>`</span> ? K <span class="keyword">extends</span> keyof T ? PropertyType&lt;T[K], R&gt; : unknown :</span><br><span class="line">    unknown;</span><br><span class="line"><span class="keyword">declare</span> <span class="function"><span class="keyword">function</span> <span class="title">getProperty</span>&lt;<span class="title">T</span>, <span class="title">Path</span> <span class="title">extends</span> <span class="title">PathKeys</span>&lt;<span class="title">T</span>&gt;&gt;(<span class="params">obj: T, path: Path</span>): <span class="title">PropertyType</span>&lt;<span class="title">T</span>, <span class="title">Path</span>&gt;</span>;</span><br></pre></td></tr></table></figure><p>VSCode 中运行结果如下：<img src="../images/typescript-advanced-04.png"></p><p>Aha！TypeScript 真是神奇！</p><p>有兴趣的朋友，可以进一步挑战下在 path 中支持数组索引等的复杂情况。</p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>近些年，国内程序员圈感觉上越来越卷，35 岁甚至 30 岁不升管理岗就好像是异类，前途渺茫。然而不为人所知的是，TypeScript 的设计者 Anders Hejlsberg 是 1960 年生人，已然花甲之年，按照国内的标准来看，他老人家已经应该是退休在家，天天带娃或者环游世界了。但是，近期的很多 TypeScript 的重要功能都是他的 Pull Request 引入的，每年 TSConf 的主题演讲也常常是他出场，从代码质量和思维活跃度来看显然仍处于全盛时期。<img src="../images/typescript-advanced-05.jpg"><em>Anders 在 TSConf 2019 做主题演讲</em><img src="../images/typescript-advanced-06.jpg"><em>Anders 在 TSConf 2020 展示他去年的 TS</em></p><p>对比两次大会，可以看出 Anders 白发明显变多，可能是过度思考导致。没有他和 TypeScript 团队以及整个社区的辛苦付出，近几年前端的高质量发展不会如此迅猛。</p><p>笔者提这些，只是期望更多地鼓励那些对技术改变美好生活仍充满信仰的同学继续坚持下去，也许我们和 Anders 能力差距甚远，但是只要方向正确且执行到位，靠接地气的技术是可以一直写到退休的。</p><p>共勉！</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.youtube.com/watch?v=jmPZztKIFf4&list=PLw5h0DiJ-9PBIgIyd2ZA1CVnJf0BLFJg2">TSConf 2019 Keynote - Anders Hejlsberg</a></li><li><a href="https://www.youtube.com/watch?v=IGw2MRI0YV8">TSConf 2020: Keynote - Anders Hejlsberg</a></li><li><a href="https://youtu.be/T-vQoI_AM9E">Magical TypeScript features - Alex Okrushko</a></li><li><a href="https://fettblog.eu/typescript-union-to-intersection/">TypeScript: Union to intersection type</a></li><li><a href="https://skovy.dev/typescript-explained-in-javascript-extends/">Typescript explained in Javascript: extends</a></li></ul><hr><p>插个小广告：因为笔者所在团队业务发展远快于开发资源，人手很缺，非常希望有志于写代码到退休的攻城狮、程序媛加入。这里拒绝 996，坚信技术是一门严肃且充满创造力的脑力活动，既要高效率工作又要高质量生活，追求从代码到界面的双重完美。</p><p>具体职位可以移步：<a href="http://chinateam.tubi.tv/">Tubi 中国团队</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在上一篇文章&lt;a href=&quot;https://zhuanlan.zhihu.com/p/64446259&quot;&gt;浅谈 TypeScript 类型系统&lt;/a&gt;中，我们介绍了 TypeScript 的一些基础知识，也简要提到了泛型编程等进阶内容。在开发实践中，可能不少同学会发现虽然这些基础基本能够解决大部分问题，但是总时不时有一些解决不了的类型相关问题。&lt;/p&gt;
&lt;p&gt;这篇文章在前文的基础上，进一步介绍一些 TypeScript 的进阶知识和应用，希望能够对你有所帮助。在内容编排上，我们首先引入一些典型问题，然后介绍相关的必备知识点，最后阐述这些问题的解决思路。&lt;/p&gt;
&lt;h2 id=&quot;典型问题&quot;&gt;&lt;a href=&quot;#典型问题&quot; class=&quot;headerlink&quot; title=&quot;典型问题&quot;&gt;&lt;/a&gt;典型问题&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;问题 A：&lt;/strong&gt;现有一个工具方法 compose(f, g) 用以整合两个函数，如何添加合适的 type？&lt;/p&gt;
&lt;figure class=&quot;highlight ts&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;compose&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;f, g&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;(&lt;span class=&quot;params&quot;&gt;...args&lt;/span&gt;) =&amp;gt;&lt;/span&gt; g(f(...args));&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;问题 B：&lt;/strong&gt;如何获取返回 Promise&lt;T&gt; 类型的函数中的类型 T，从而能够实现一些类似 React Query 中 &lt;a href=&quot;https://github.com/tannerlinsley/react-query/blob/v3.15.2/src/react/useQuery.ts#L26-L35&quot;&gt;useQuery&lt;/a&gt; 的方法？&lt;/p&gt;
&lt;figure class=&quot;highlight ts&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; User = &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  age?: &lt;span class=&quot;built_in&quot;&gt;number&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; fetchUser = (id: &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt;): &lt;span class=&quot;built_in&quot;&gt;Promise&lt;/span&gt;&amp;lt;User&amp;gt; =&amp;gt; &amp;#123; ... &amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// status 为 &amp;#x27;success&amp;#x27; 时，data 类型应为 User&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &amp;#123; status, data &amp;#125; = useQuery(&lt;span class=&quot;string&quot;&gt;`getUser&lt;span class=&quot;subst&quot;&gt;$&amp;#123;id&amp;#125;&lt;/span&gt;`&lt;/span&gt;, &lt;span class=&quot;function&quot;&gt;() =&amp;gt;&lt;/span&gt; fetchUser(id));&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;问题 C：&lt;/strong&gt;如何为工具方法 getProperty(obj, path) 添加合适的类型，从而使得 TypeScript 自行推断出最终的返回结果的类型？&lt;/p&gt;
&lt;figure class=&quot;highlight ts&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; person = &amp;#123; &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;Simon&amp;#x27;&lt;/span&gt;, &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;school&lt;/span&gt;: &amp;#123; &lt;span class=&quot;attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;MIT&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;attr&quot;&gt;graduated&lt;/span&gt;: &lt;span class=&quot;number&quot;&gt;2018&lt;/span&gt; &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// 第二个参数 path 应对应第一个对象参数的属性路径&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; graduatedYear = getProperty(person, &lt;span class=&quot;string&quot;&gt;&amp;#x27;school.graduated&amp;#x27;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="TypeScript" scheme="https://springuper.github.io/tags/TypeScript/"/>
    
    <category term="Type System" scheme="https://springuper.github.io/tags/Type-System/"/>
    
  </entry>
  
  <entry>
    <title>Continuation 在 JS 中的应用</title>
    <link href="https://springuper.github.io/continuation/"/>
    <id>https://springuper.github.io/continuation/</id>
    <published>2020-05-05T20:05:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>React 新近发布的 Hooks、Suspense、Concurrent Mode 等新功能让人眼前一亮，甚至惊叹 JS 居然有如此魔力。同时，这几个功能或多或少附带一些略显奇怪的规则，没有更深层次理解的话难以把握。其实这里面并没有什么“黑科技”，就大的趋势来讲，前端整体上还是在不断借鉴计算机其它领域的优秀实践，来帮助我们更方便地解决人机交互问题。本文着眼于支撑这些功能的一个底层编程概念 Continuation（译作“续延”），期望能够在了解它之后，大家对这几个功能有进一步的理解和掌握。当然，Continuation 在 React 之外也有很多的应用，可以一眼窥豹。</p><h2 id="Continuation-是什么？"><a href="#Continuation-是什么？" class="headerlink" title="Continuation 是什么？"></a>Continuation 是什么？</h2><p>有些人对 Continuation 并不陌生，因为有时候在谈到 Callback Hell（回调地狱）时会有提到这一概念。但其实它和回调函数大相径庭。</p><p>维基百科对它的定义是：</p><blockquote><p>A continuation is an abstract representation of the control state of a computer program.</p></blockquote><p>即，continuation 是计算机程序控制状态的抽象表示。一个坊间更通俗的说法是：它代表程序的剩余部分。像 continue、break 这类控制流操作符一样，continuation 能够暴露给用户程序从而可以在恰当时机恢复执行，这种基本能力大大扩展了编程语言使用者的发挥空间，也为 excpetion handling、generators、coroutines、algebraic effects 等提供了坚实基础。<img src="../images/continuation-01.jpg"></p><p>相信很多人和我一样，对这样不明就里的官方解释迷惑不解。没关系，我们首先举一个现实生活中的例子——Continuation 三明治：</p><blockquote><p>譬如说你现在站在厨房的冰箱前，正在想要不要做一个三明治吃。这时候你做了一个 continuation 小纸条，上面写了你的位置和想要做的事情，然后把 continuation 小纸条贴在口袋上。然后你从冰箱里拿了一些面包和火鸡，自己做了一个三明治，放在柜子上。之后，你拿起口袋上的 continuation 小纸条，然后你发现自己现在又站在了冰箱前面，正在想着要不要做个三明治吃。不过不幸的是，在柜子上已经摆着一个三明治了，而且所有做三明治的食材也都不见了。所以，你就拿起那个三明治开心地吃了起来 :-)</p></blockquote><span id="more"></span><h2 id="默认-Continuation"><a href="#默认-Continuation" class="headerlink" title="默认 Continuation"></a>默认 Continuation</h2><p>事实上，所有的程序都自带一个默认的 continuation，那就是调用栈（Call Stack）。调用栈中存放着当前程序的一系列剩余任务，每个任务在调用栈中表示为一个栈帧（Stack Frame），用以存放任务的数据、变量和调用信息。当调用栈为空时，意味着整个程序执行结束了。<img src="../images/continuation-02.jpg"></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="function"><span class="keyword">function</span> <span class="title">main</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  foo();</span><br><span class="line">  bar();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  bar();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">bar</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// do something</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看出，调用栈是严格按照后进先出的方式运行的，无法灵活调整执行顺序。此外，控制流的控制权也被运行环境牢牢掌握，程序员无能为力。</p><p>现在，让我们设想下，如果未来有一天我们能够将调用任务以链表的方式存储在堆中，是不是就可以突破调用栈的限制了呢？<img src="../images/continuation-03.jpg"></p><p>首先，因为任务以调用桢的形式存储在堆中，并通过指针相互关联，形成一个调用帧链表。当前任务完成时，运行时可以使用这些指针跳到下一个调用帧。得益于链表这一组织形式，执行程序有能力调整调用帧之间的结构顺序。</p><h2 id="Continuation-Passing-Style-CPS"><a href="#Continuation-Passing-Style-CPS" class="headerlink" title="Continuation-Passing Style (CPS)"></a>Continuation-Passing Style (CPS)</h2><p>为了获得更多控制权，广大程序员们进行了艰苦卓绝的努力。CPS 即是第一种有意义的尝试。简单来说，它是将程序控制流通过 continuation 的形式进行显式传递的一种编程方式，具体有三个典型特征：</p><ol><li>每个函数的最后一个参数都是它的 continuation</li><li>函数内部不能显式地使用 return</li><li>函数只能通过调用 continuation 以传递它完成的计算结果</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">double</span>(<span class="params">x, next</span>) </span>&#123;</span><br><span class="line">  next(x * <span class="number">2</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">add</span>(<span class="params">x, y, next</span>) </span>&#123;</span><br><span class="line">  next(x + y);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">minus</span>(<span class="params">x, y, next</span>) </span>&#123;</span><br><span class="line">  next(x - y);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ((1 + 2) - 5) * 2</span></span><br><span class="line">add(<span class="number">1</span>, <span class="number">2</span>, <span class="function"><span class="params">resultAdd</span> =&gt;</span> &#123;</span><br><span class="line">  minus(resultAdd, <span class="number">5</span>, <span class="function"><span class="params">resultMinus</span> =&gt;</span> &#123;</span><br><span class="line">    double(resultMinus, <span class="function"><span class="params">resultDouble</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="built_in">console</span>.log(<span class="string">`result: <span class="subst">$&#123;resultDouble&#125;</span>`</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>这不就是我们前端工程师耳熟能详的回调函数么，最后的调用也再次让我们想起了恐怖的回调地狱。表面上看的确如此，但是从控制流的角度来进一步考虑，这种模式的确赋予了程序员更多控制权，因为所有的计算步骤（函数）的 continuation 都是显式传递的。</p><p>例如，假设我们希望能够在计算的中间点进行检查，一旦计算结果小于 0 则直接返回该结果。基于 CPS 的三点特征，我们可以定义如下一个 evaluate 的计算过程：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">frames, operate, next</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">let</span> output;</span><br><span class="line">  <span class="keyword">const</span> run = <span class="function">(<span class="params">index</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// Finish all frames, go to run the top level continuation</span></span><br><span class="line">    <span class="keyword">if</span> (index === frames.length) <span class="keyword">return</span> next(output);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Pick up the next frame and run it with assembled arguments</span></span><br><span class="line">    <span class="keyword">const</span> &#123; fn, args = [] &#125; = frames[index];</span><br><span class="line">    <span class="keyword">const</span> fnArgs = index &gt; <span class="number">0</span> ? [output, ...args] : [...args];</span><br><span class="line">    fnArgs.push(<span class="function">(<span class="params">result</span>) =&gt;</span> &#123;</span><br><span class="line">      output = result;</span><br><span class="line">      operate(output, next, <span class="function">() =&gt;</span> run(++index));</span><br><span class="line">    &#125;);</span><br><span class="line">    fn(...fnArgs);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Kick off</span></span><br><span class="line">  run(<span class="number">0</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ((1 + 2) - 5) * 2</span></span><br><span class="line">evaluate(</span><br><span class="line">  [</span><br><span class="line">    &#123; <span class="attr">fn</span>: add, <span class="attr">args</span>: [<span class="number">1</span>, <span class="number">2</span>] &#125;,</span><br><span class="line">    &#123; <span class="attr">fn</span>: minus, <span class="attr">args</span>: [<span class="number">5</span>] &#125;,</span><br><span class="line">    &#123; <span class="attr">fn</span>: double &#125;,</span><br><span class="line">  ],</span><br><span class="line">  <span class="function">(<span class="params">output, abort, next</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (output &lt; <span class="number">0</span>) <span class="keyword">return</span> abort(<span class="string">`the intermedia result is less than zero: <span class="subst">$&#123;output&#125;</span>`</span>);</span><br><span class="line">    next(output);</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function">(<span class="params">output</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">`output: <span class="subst">$&#123;output&#125;</span>`</span>);</span><br><span class="line">  &#125;,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p><em><a href="https://jsbin.com/bidayeg/3/edit?js,console">CPS 示例</a></em></p><p>可以看出，一方面，通过合理组织计算步骤模型，evaluate 可以帮助避免回调地狱的问题，另一方面，evaluate 的第二个参数会在每个计算步骤完成时进行检查，并且有能力 abort 后续所有计算步骤，直接调用顶层 continuation 返回中间结果。</p><p>这个示例展示了 CPS 为我们拓展的控制流操作能力，除此之外，CPS 还有如下优点：</p><ul><li><strong>尾调用</strong>。每个函数都是在最后一个动作调用 continuation 返回计算结果，因此执行上下文不需要被保存到当前调用栈，编译器可以针对这种情况做尾调用消除（Tail Call Elimination）的优化，这种优化在函数式语言编译器中大量应用</li><li><strong>异步友好</strong>。众所众知，JavaScript 是单线程的，如果使用直接函数调用来处理远程请求等操作，那么我们将不得不暂停这唯一的线程直到异步操作结果返回，这意味着用户的其它交互得不到及时响应。CPS 或者换言之的回调模式提供了一种有效易用的方式来处理这类问题</li></ul><p>然而，程序终究是人来编写和维护的，CPS 虽然有众多好处，但让所有人都遵循这样严格的方式编程非常困难，目前这种技术更多地在编译器中作为中间表示层应用。</p><h2 id="CallCC"><a href="#CallCC" class="headerlink" title="CallCC"></a>CallCC</h2><p>目前 Continuation 的主流应用方式是通过形如 <a href="https://en.wikipedia.org/wiki/Call-with-current-continuation">callCC</a>（call with current continuation）的过程调用以捕获当前 continuation，并在之后适时执行它以恢复到 continuation 所在上下文继续执行后续计算从而实现各种控制流操作。</p><p>Scheme、Scala 等语言提供了 call/cc 或等效控制流操作符，JS 目前并没有原生支持，但是通过后续介绍的两种方式可以间接实现。</p><p>现在假设我们已经可以在 JS 中使用 callCC 操作符，让我们试试看它都能为我们带来什么样的头脑风暴吧。</p><p><strong>小试牛刀</strong></p><p>让我们从一个非常简单的例子开始，了解下 callCC 如何运作：</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="keyword">const</span> x = callCC(<span class="function"><span class="keyword">function</span> (<span class="params">cont</span>) </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">10</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (i === <span class="number">3</span>) &#123;</span><br><span class="line">      cont(<span class="string">&#x27;done&#x27;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">console</span>.log(i);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(x);</span><br><span class="line"></span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// 0</span></span><br><span class="line"><span class="comment">// 1</span></span><br><span class="line"><span class="comment">// 2</span></span><br><span class="line"><span class="comment">// done</span></span><br></pre></td></tr></table></figure><p>从输出结果可以看出，程序的 for 循环并没有全部完成，而是在 i 为 3 时执行 callCC 捕获的 continuation 过程时直接退出了整个 callCC 调用，并将 ‘done’ 返回给了变量 x。我们可以总结下 callCC 方法的逻辑：</p><ul><li>接受一个函数为唯一参数</li><li>该函数也有唯一一个参数 cont，代表 callCC 的后续计算，在这个例子中，即将 callCC 的计算结果赋值给 x，然后执行最后的 console.log(x) 打印结果</li><li>callCC 会立即调用其函数参数</li><li>在该函数参数执行过程中，cont 可以接受一个参数作为 callCC 的返回值，一旦调用，则忽略后续所有计算，程序控制流跳转会 callCC 的调用处继续执行</li></ul><p>得益于 <a href="https://github.com/jlongster">James Long</a> 开发的 <a href="https://github.com/jlongster/unwinder">Unwinder</a> 在线编译工具，非常推荐各位去 <em><a href="https://jlongster.github.io/unwinder/browser/?code=Y29uc3QgeCA9IGNhbGxDQyhmdW5jdGlvbiAoY29udCkgewogIGZvciAobGV0IGkgPSAwOyBpIDwgMTA7IGkrKykgewogICAgaWYgKGkgPT09IDMpIHsKICAgICAgY29udCgnZG9uZScpOwogICAgfQogICAgY29uc29sZS5sb2coaSk7CiAgfQp9KTsKCmNvbnNvbGUubG9nKHgpOw==">Simple 示例</a></em> 尝试在浏览器里执行下，你甚至可以打断点然后单步执行哦~</p><p><strong>重新实现列表 some 方法</strong></p><p>进一步地，让我们检验下刚刚介绍的对 callCC 的理解，重新实现下列表的 some 方法：</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="function"><span class="keyword">function</span> <span class="title">some</span>(<span class="params">predicate, arr</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> x = callCC(<span class="function"><span class="keyword">function</span> (<span class="params">cont</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> index = <span class="number">0</span>; index &lt; arr.length; index++) &#123;</span><br><span class="line">      <span class="built_in">console</span>.log(<span class="string">&#x27;testing&#x27;</span>, arr[index]);</span><br><span class="line">      <span class="keyword">if</span> (predicate(arr[index])) &#123;</span><br><span class="line">        cont(<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="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> x;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(some(<span class="function"><span class="params">x</span> =&gt;</span> x &gt;= <span class="number">2</span>, [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>]));</span><br><span class="line"><span class="built_in">console</span>.log(some(<span class="function"><span class="params">x</span> =&gt;</span> x &gt;= <span class="number">2</span>, [<span class="number">1</span>, -<span class="number">5</span>]));</span><br><span class="line"></span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// testing 1</span></span><br><span class="line"><span class="comment">// testing 2</span></span><br><span class="line"><span class="comment">// true</span></span><br><span class="line"><span class="comment">// testing 1</span></span><br><span class="line"><span class="comment">// testing -5</span></span><br><span class="line"><span class="comment">// false</span></span><br></pre></td></tr></table></figure><p><em><a href="https://jlongster.github.io/unwinder/browser/?code=ZnVuY3Rpb24gc29tZShwcmVkaWNhdGUsIGFycikgewogIGNvbnN0IHggPSBjYWxsQ0MoZnVuY3Rpb24gKGNvbnQpIHsKICAgIGZvciAobGV0IGluZGV4ID0gMDsgaW5kZXggPCBhcnIubGVuZ3RoOyBpbmRleCsrKSB7CiAgICAgIGNvbnNvbGUubG9nKCd0ZXN0aW5nJywgYXJyW2luZGV4XSk7CiAgICAgIGlmIChwcmVkaWNhdGUoYXJyW2luZGV4XSkpIHsKICAgICAgICBjb250KHRydWUpOwogICAgICB9CiAgICB9CgogICAgcmV0dXJuIGZhbHNlOwogIH0pOwoKICByZXR1cm4geDsKfQoKY29uc29sZS5sb2coc29tZSh4ID0+IHggPj0gMiwgWzEsIDIsIDMsIDRdKSk7CmNvbnNvbGUubG9nKHNvbWUoeCA9PiB4ID49IDIsIFsxLCAtNV0pKTs=">列表 some 示例</a></em></p><p>在第一个 some 函数调用中，当 predicate 返回为 true 时，cont(true) 执行后程序控制流跳转到 callCC 调用处，然后 some 函数返回 true 并被打印。然而在第二个 some 调用中，因为所有 predicate 都为 false，没有 cont 被调用，因此 callCC 返回了其函数参数的最后一个 return 语句的结果。</p><p>在这个例子中，我们进一步了解了 callCC 的运行原理，并能用它实现一些工具方法。</p><p><strong>重新实现 Try-Catch</strong></p><p>接下来，让我们挑战一个难度更大的 callCC 应用：重写 try-catch。</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">const</span> tryStack = [];</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Try</span>(<span class="params">body, handler</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> ret = callCC(<span class="function"><span class="keyword">function</span> (<span class="params">cont</span>) </span>&#123;</span><br><span class="line">    tryStack.push(cont);</span><br><span class="line">    <span class="keyword">return</span> body();</span><br><span class="line">  &#125;);</span><br><span class="line">  tryStack.pop();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (ret.__exc) &#123;</span><br><span class="line">    <span class="keyword">return</span> handler(ret.__exc);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Throw</span>(<span class="params">exc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (tryStack.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    tryStack[tryStack.length - <span class="number">1</span>](&#123; <span class="attr">__exc</span>: exc &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">&quot;unhandled exception&quot;</span>, exc);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Try 函数接受两个参数：body 是接下来准备执行的主体逻辑，handler 是异常处理逻辑。关键点在于 Try 内部在执行 body 前会先将捕获的 cont 压入到堆栈 tryStack 中，以便在 Throw 时获取 cont 从而继续从 callCC 调用处恢复，从而实现类似 try-catch 语句的功能。</p><p>下面是一个 Try-Catch 的应用示例：</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="function"><span class="keyword">function</span> <span class="title">bar</span>(<span class="params">x</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (x &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    Throw(<span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">&quot;error!&quot;</span>));</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> x * <span class="number">2</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">x</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> bar(x);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Try(</span><br><span class="line">  <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(foo(<span class="number">1</span>));</span><br><span class="line">    <span class="built_in">console</span>.log(foo(-<span class="number">1</span>));</span><br><span class="line">    <span class="built_in">console</span>.log(foo(<span class="number">2</span>));</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function"><span class="keyword">function</span> (<span class="params">ex</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&quot;caught&quot;</span>, ex);</span><br><span class="line">  &#125;</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// 2</span></span><br><span class="line"><span class="comment">// caught Error: error!</span></span><br></pre></td></tr></table></figure><p><em><a href="https://jlongster.github.io/unwinder/browser/?code=Y29uc3QgdHJ5U3RhY2sgPSBbXTsKCmZ1bmN0aW9uIFRyeShib2R5LCBoYW5kbGVyKSB7CiAgY29uc3QgcmV0ID0gY2FsbENDKGZ1bmN0aW9uIChjb250KSB7CiAgICB0cnlTdGFjay5wdXNoKGNvbnQpOwogICAgcmV0dXJuIGJvZHkoKTsKICB9KTsKICB0cnlTdGFjay5wb3AoKTsKCiAgaWYgKHJldC5fX2V4YykgewogICAgcmV0dXJuIGhhbmRsZXIocmV0Ll9fZXhjKTsKICB9CiAgcmV0dXJuIHJldDsKfQoKZnVuY3Rpb24gVGhyb3coZXhjKSB7CiAgaWYgKHRyeVN0YWNrLmxlbmd0aCA+IDApIHsKICAgIHRyeVN0YWNrW3RyeVN0YWNrLmxlbmd0aCAtIDFdKHsgX19leGM6IGV4YyB9KTsKICB9CiAgY29uc29sZS5sb2coInVuaGFuZGxlZCBleGNlcHRpb24iLCBleGMpOwp9CgpmdW5jdGlvbiBiYXIoeCkgewogIGlmKHggPCAwKSB7CiAgICBUaHJvdyhuZXcgRXJyb3IoImVycm9yISIpKTsKICB9CiAgcmV0dXJuIHggKiAyOwp9CgpmdW5jdGlvbiBmb28oeCkgewogIHJldHVybiBiYXIoeCk7Cn0KClRyeSgKICBmdW5jdGlvbiAoKSB7CiAgICBjb25zb2xlLmxvZyhmb28oMSkpOwogICAgY29uc29sZS5sb2coZm9vKC0xKSk7CiAgICBjb25zb2xlLmxvZyhmb28oMikpOwogIH0sCiAgZnVuY3Rpb24gKGV4KSB7CiAgICBjb25zb2xlLmxvZygiY2F1Z2h0IiwgZXgpOwogIH0KKTs=">Try-Catch 示例</a></em></p><p>和我们预期的效果一致，异常处理函数可以捕获 Throw 抛出的异常，同时主体逻辑 body 中的剩余部分也不再执行。另外，Throw 也像 JavaScript 原生的 throw 一样，能够击穿多层函数调用，直到被 Try 语句的异常处理逻辑处理。</p><p><strong>可恢复的 Try-Catch</strong></p><p>基于上一小节中 Try-Catch 实现，我们现在尝试一个真正的能体现 continuation 魔力的改造：让 Try-Catch 在捕获异常后，能够从抛出异常的地方恢复执行。</p><p>为了实现这一效果，我们只需要对 Throw 进行改造，使其也通过 callCC 过程捕获调用 Throw 时的 continuation，并将该 continuation 赋值给异常对象以供 Resume 过程调用从而实现异常恢复：</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="function"><span class="keyword">function</span> <span class="title">Throw</span>(<span class="params">exc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (tryStack.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> callCC(<span class="function"><span class="keyword">function</span> (<span class="params">cont</span>) </span>&#123;</span><br><span class="line">      exc.__cont = cont;</span><br><span class="line">      tryStack[tryStack.length - <span class="number">1</span>](&#123; <span class="attr">__exc</span>: exc &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">throw</span> exc;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Resume</span>(<span class="params">exc, value</span>) </span>&#123;</span><br><span class="line">  exc.__cont(value);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">double</span>(<span class="params">x</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">&#x27;x is&#x27;</span>, x);</span><br><span class="line">  <span class="keyword">if</span> (x &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    x = Throw(&#123; <span class="attr">BAD_NUMBER</span>: x &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> x * <span class="number">2</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">main</span>(<span class="params">x</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> double(x);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Try(</span><br><span class="line">  <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(main(<span class="number">1</span>));</span><br><span class="line">    <span class="built_in">console</span>.log(main(-<span class="number">2</span>));</span><br><span class="line">    <span class="built_in">console</span>.log(main(<span class="number">3</span>));</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function"><span class="keyword">function</span> (<span class="params">ex</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">typeof</span> ex.BAD_NUMBER !== <span class="string">&#x27;undefined&#x27;</span>) &#123;</span><br><span class="line">      Resume(ex, <span class="built_in">Math</span>.abs(ex.BAD_NUMBER));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&#x27;caught&#x27;</span>, ex);</span><br><span class="line">  &#125;</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// output:</span></span><br><span class="line"><span class="comment">// x is 1</span></span><br><span class="line"><span class="comment">// 2</span></span><br><span class="line"><span class="comment">// x is -2</span></span><br><span class="line"><span class="comment">// 4</span></span><br><span class="line"><span class="comment">// x is 3</span></span><br><span class="line"><span class="comment">// 6</span></span><br></pre></td></tr></table></figure><p><em><a href="https://jlongster.github.io/unwinder/browser/?code=Y29uc3QgdHJ5U3RhY2sgPSBbXTsKCmZ1bmN0aW9uIFRyeShib2R5LCBoYW5kbGVyKSB7CiAgY29uc3QgcmV0ID0gY2FsbENDKGZ1bmN0aW9uIChjb250KSB7CiAgICB0cnlTdGFjay5wdXNoKGNvbnQpOwogICAgcmV0dXJuIGJvZHkoKTsKICB9KTsKICB0cnlTdGFjay5wb3AoKTsKCiAgaWYgKHJldCAmJiByZXQuX19leGMpIHsKICAgIHJldHVybiBoYW5kbGVyKHJldC5fX2V4Yyk7CiAgfQogIHJldHVybiByZXQ7Cn0KCmZ1bmN0aW9uIFRocm93KGV4YykgewogIGlmICh0cnlTdGFjay5sZW5ndGggPiAwKSB7CiAgICByZXR1cm4gY2FsbENDKGZ1bmN0aW9uIChjb250KSB7CiAgICAgIGV4Yy5fX2NvbnQgPSBjb250OwogICAgICB0cnlTdGFja1t0cnlTdGFjay5sZW5ndGggLSAxXSh7IF9fZXhjOiBleGMgfSk7CiAgICB9KTsKICB9CiAgdGhyb3cgZXhjOwp9CgpmdW5jdGlvbiBSZXN1bWUoZXhjLCB2YWx1ZSkgewogIGV4Yy5fX2NvbnQodmFsdWUpOwp9CgovLyBSZXN1bWFibGUgZXhjZXB0aW9uCgpmdW5jdGlvbiBkb3VibGUoeCkgewogIGNvbnNvbGUubG9nKCd4IGlzJywgeCk7CiAgaWYgKHggPCAwKSB7CiAgICB4ID0gVGhyb3coeyBCQURfTlVNQkVSOiB4IH0pOwogIH0KICByZXR1cm4geCAqIDI7Cn0KCmZ1bmN0aW9uIG1haW4oeCkgewogIHJldHVybiBkb3VibGUoeCk7Cn0KCgpUcnkoCiAgZnVuY3Rpb24oKSB7CiAgICBjb25zb2xlLmxvZyhtYWluKDEpKTsKICAgIGNvbnNvbGUubG9nKG1haW4oLTIpKTsKICAgIGNvbnNvbGUubG9nKG1haW4oMykpOwogIH0sCiAgZnVuY3Rpb24oZXgpIHsKICAgIGlmKHR5cGVvZiBleC5CQURfTlVNQkVSICE9PSAndW5kZWZpbmVkJykgewogICAgICBSZXN1bWUoZXgsIE1hdGguYWJzKGV4LkJBRF9OVU1CRVIpKTsKICAgIH0KICAgIGNvbnNvbGUubG9nKCdjYXVnaHQnLCBleCk7CiAgfQopOw==">Resumable Exception 示例</a></em></p><p>从上例输出中，我们可以清晰地注意到，在执行 main(-2) 时抛出的错误被准确地识别并且恢复为正确的正整数，并最终执行完所有主体逻辑。</p><p><strong>Algebraic Effects</strong></p><p>这种异常恢复的机制，也被称作 Algebraic Effects。它有一个非常核心的优势：<strong>将主体逻辑与异常恢复逻辑分离</strong>。例如我们可以在 UI 组件中抛出一个数据读取的异常，然后在更上层的异常处理逻辑中尝试获取该数据后恢复执行，这样既简化了 UI 组件的复杂度，也将数据获取的逻辑交给了调用方，更加灵活高效。</p><p>实际上 Algebraic Effects 还有着诸多的应用，Eff、Ocaml 等编程语言对 Algebraic Effects 有着<a href="https://github.com/ocamllabs/ocaml-effects-tutorial">丰富的支持</a>。React 有不少团队成员是 Ocaml 的拥趸，新近推出的 Hooks、Suspense 都深受这种思想启发，能够让我们类似线性同步地调用各种状态读取、数据获取等异步过程。</p><p>下面我们来分析一个 Suspense 示例，体会下背后解决思路的相似之处：</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"><span class="function"><span class="keyword">function</span> <span class="title">ProfilePage</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">h1</span>&gt;</span>Loading profile...<span class="tag">&lt;/<span class="name">h1</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">ProfileDetails</span> /&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">Suspense</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="function"><span class="keyword">function</span> <span class="title">ProfileDetails</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// Try to read user info, although it might not have loaded yet</span></span><br><span class="line">  <span class="keyword">const</span> user = resource.user.read();</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;user.name&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> rootElement = <span class="built_in">document</span>.getElementById(<span class="string">&quot;root&quot;</span>);</span><br><span class="line">ReactDOM.createRoot(rootElement).render(</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">ProfilePage</span> /&gt;</span></span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p><em><a href="https://codesandbox.io/s/happy-voice-mf4ol?file=/src/index.js">Suspense 示例</a></em></p><p>在 ProfileDetails 组件中，执行 resource.user.read() 时，由于当前数据并不存在，所以需要 throw 一个 promise 实例。位于上层的 Suspense 在捕获这个 promise 后会先展示 fallback 指定的 UI，然后等待 promise resolve 后再次尝试渲染 ProfileDetails 组件。虽然对比基于 Continuation 实现的异常恢复仍然有一定差距，并不能精确地从主体逻辑中抛出异常的语句处恢复，而是将主体逻辑重新执行一遍。不过 React 内部做了大量优化，尽最大可能地避免不必要开销。</p><h2 id="CallCC-实现"><a href="#CallCC-实现" class="headerlink" title="CallCC 实现"></a><strong>CallCC 实现</strong></h2><p>相信很多读者在一览 callCC 的强大能力之后，已经忍不住想要尽快了解下它的实现方式，很难想象土鳖的 JS 是如何能做到这一切的。这一章节我们就为大家揭开它的神秘面纱。</p><p><strong>编译</strong></p><p>类似 Babel 帮助我们将各种 JS 新标准甚至是草案阶段的语言特性转化为主流浏览器都能运行的最终代码一样，我们可以借助增加一个编译阶段将含有 callCC 调用的代码转化为普通浏览器都能运行的代码。</p><p>Prettier 作者 James Long 早些年开发网页游戏编辑器时曾打算制作一款交互式代码调试工具，种种尝试之后，他在友人的指导下学习了 <a href="http://www.schemeworkshop.org/2007/procPaper4.pdf">Exceptional Continuations in JavaScript</a> 论文中介绍的高性能方法，并基于当时 Facebook 刚刚开源不久的编译 generator 利器 <a href="http://facebook.github.io/regenerator/">Regenerator</a>，开发了 Unwinder 来编译 callCC，同时还提供了一个运行时以及实时在线 debug 工具。</p><p>Unwinder 或者说 Regenerator 的核心是状态机，即将源代码中的所有计算步骤打散，相互之间的跳转通过状态变换来进行。例如下面这段简短的代码：</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="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> x = <span class="number">5</span>;</span><br><span class="line">  <span class="keyword">var</span> y = <span class="number">6</span>;</span><br><span class="line">  <span class="keyword">return</span> x + y;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">let</span> $__next = <span class="number">0</span>, x, y;</span><br><span class="line">  <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">switch</span>($__next) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">0</span>:</span><br><span class="line">        x = <span class="number">5</span>;</span><br><span class="line">        $__next = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">        y = <span class="number">6</span>;</span><br><span class="line">        $__next = <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line">        <span class="keyword">return</span> x + y;</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>基于这种核心能力，辅以 Exceptional Continuations 特有的 try-catch、restore 等逻辑支持，Unwinder 能够很好地实现 Continuation。不过后续作者并没有再对其进行维护，同时它在异步操作方面的支持有一定缺陷，导致目前并不是非常流行。</p><p><strong>Generator</strong></p><p>另外一派是直接采用 Generator 来实现，这非常符合直觉，毕竟 Generator 就是一种转移控制流的非常独特的方式。</p><p>Yassine Elouafi 在系列文章 <a href="https://dev.to/yelouafi/algebraic-effects-in-javascript-part-1---continuations-and-control-transfer-3g88">Algebraic Effects in JavaScript</a> 中系统性地介绍了 Continuation、CPS、使用 Generator 改造 CPS 并实现 callCC、进一步支持 Delimited Continuation 以及最终支持 Algebraic Effects 等内容，行文顺畅，内容示例夯实，是研究 JS Continuation 上乘的参考资料。</p><p>限于篇幅，本文不再对其原理进行深入介绍，感兴趣的同学可以读一下他的系列文章。下面是非常核心的 callcc 实现部分：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">callcc</span>(<span class="params">genFunc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params">capturedCont</span>) </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">jumpToCallccPos</span>(<span class="params">value</span>) </span>&#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="function"><span class="params">next</span> =&gt;</span> capturedCont(value);</span><br><span class="line">    &#125;</span><br><span class="line">    runGenerator(genFunc(jumpToCallccPos), <span class="literal">null</span>, capturedCont);</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了支持类似上文中提到的 Try-Catch，我们可以定义如下方法：</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="keyword">const</span> handlerStack = [];</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span>* <span class="title">trycc</span>(<span class="params">computation, handler</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">yield</span> callcc(<span class="function"><span class="keyword">function</span>*(<span class="params">k</span>) </span>&#123;</span><br><span class="line">    handlerStack.push([handler, k]);</span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">yield</span> computation;</span><br><span class="line">    handlerStack.pop();</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span>* <span class="title">throwcc</span>(<span class="params">exception</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [handler, k] = handlerStack.pop();</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">yield</span> handler(exception);</span><br><span class="line">  <span class="keyword">yield</span> k(result);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em><a href="https://jsbin.com/pekuxoq/edit?js,console">Try-Catch Generator 示例</a></em></p><p>从实现层面来看，Generator 方式比编译方式更加简单，<a href="https://github.com/yelouafi/algebraic-effects.js/blob/master/algebraic-effects.js">核心代码</a>不到百行。但是因为 Generator 本身的认知复杂度导致一定门槛，另外所有调用 callCC 的相关代码都必须使用 Generator 才能够顺利运行，这对于应用开发来说太过艰难，更不必说需要改造的海量的第三方模块。</p><h2 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h2><p>Continuation 并非银弹，究其本质，它是一个高级版本的能够处理函数表达式的 Goto 语句。众所众知，由于高度灵活导致的难以理解和调试，Goto 语句在各个语言中都属于半封禁甚至封禁状态。Continuation 面临类似的窘境，需要使用者思虑周全，慎之又慎，将其应用控制在一定合理范围，甚至像 React 这样完全封装在自身实现内部。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>Continuation 是个非常复杂的概念，为了能够由浅入深、结合 JS 实际地来系统性阐述这一概念，笔者花费了自专栏开设以来最长的时间做各种梳理准备。不期望大家读过这篇文章后就马上开始使用 Continuation 或者 Algebraic Effects。如前文所述，目前 Continuation 还存在各方面的问题，应该实事求是，因地制宜，取其精华去其糟粕。正如 React Hooks、Suspense 一样，它们并没有真的搞了内部的编译器或者引入 Generator，而是结合实际，神似而形不同，最大限度地满足了设计目标。此外，期望这篇长文能帮助大家理解一些设计背后的思路，拓展一点前端工程师的技术视野，了解到整个编程领域内的优秀实践。</p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>React Fiber 是 React 16 引入的最为重要的底层变化，主要解决阻塞渲染的问题。为了实现这一目标，Fiber 化整为零，将组件中的每一个子组件或者子元素都视为一个 Fiber，通过类似 DOM Tree 的组织方式形成一个 Fiber Tree：<img src="../images/continuation-04.jpg"></p><p>每个 Fiber 都有独立的 render 过程和状态存储，在渲染时，我们可以把整个 Fiber Tree 的渲染过程理解成遍历整个 Fiber Tree 的过程，每个 Fiber 的渲染工作可以理解为一个函数调用，为了不阻塞页面交互，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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">workLoop</span>(<span class="params">deadline</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">let</span> shouldYield = <span class="literal">false</span>;</span><br><span class="line">  <span class="keyword">while</span> (nextUnitOfWork &amp;&amp; !shouldYield) &#123;</span><br><span class="line">    nextUnitOfWork = performUnitOfWork(</span><br><span class="line">      nextUnitOfWork</span><br><span class="line">    );</span><br><span class="line">    shouldYield = deadline.timeRemaining() &lt; <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!nextUnitOfWork &amp;&amp; wipRoot) &#123;</span><br><span class="line">    commitRoot();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  requestIdleCallback(workLoop);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">requestIdleCallback(workLoop);</span><br></pre></td></tr></table></figure><p>在每个浏览器 idle 的时间片内，workLoop 会尽可能多地执行 Fiber 渲染任务，如果时间到期且仍然有未完成任务时，nextUnitOfWork 会更新到最后一个待执行任务，然后等待下一个 idle 时间片继续执行。</p><p>虽然这部分代码并没有明确地使用我们前文提到的种种 Continuation 方式，但是究其本质，React 是将 Fiber 引入之前的递归调用实现一次性完整渲染改变成以 Fiber  Tree 为基础的虚拟任务堆栈（或许不应该称为栈，因为它是一个树形结构），从而实现了对渲染任务的灵活调度。因此，nextUnitOfWork 在这里可以视作某种程度上的 Continuation，它代表着 React 渲染任务的“剩余部分”。</p><p>联想到前面提到的 React Hooks、Suspense 背后借鉴的 Algebraic Effects 思想，难怪 React 团队核心成员 Sebastian Markbåge 曾经放言：</p><blockquote><p><em>React is operating at the level of a language feature</em></p></blockquote><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ol><li><a href="https://en.wikipedia.org/wiki/Continuation">Continuation - WikiPedia</a></li><li><a href="https://jlongster.com/Whats-in-a-Continuation">What is a Continuation</a></li><li><a href="http://www.schemeworkshop.org/2007/procPaper4.pdf">Exceptional Continuation in JavaScript</a></li><li><a href="https://dev.to/yelouafi/algebraic-effects-in-javascript-part-1---continuations-and-control-transfer-3g88">Algebraic Effects in JavaScript part 1 - continuations and control transfer</a></li><li><a href="https://www.yld.io/blog/continuations-coroutines-fibers-effects/">Continuations, coroutines, fibers, effects</a></li><li><a href="https://engineering.hexacta.com/didact-fiber-incremental-reconciliation-b2fe028dcaec">Didact Fiber: Incremental reconciliation</a></li><li><a href="https://pomb.us/build-your-own-react/">Build your own React</a></li><li><a href="https://delapuente.github.io/presentations/cps-javascript/index.html#/">Continuation passing style in JavaScript</a></li><li><a href="https://segmentfault.com/a/1190000020110166">Algebraic Effects，以及它在React中的应用</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;React 新近发布的 Hooks、Suspense、Concurrent Mode 等新功能让人眼前一亮，甚至惊叹 JS 居然有如此魔力。同时，这几个功能或多或少附带一些略显奇怪的规则，没有更深层次理解的话难以把握。其实这里面并没有什么“黑科技”，就大的趋势来讲，前端整体上还是在不断借鉴计算机其它领域的优秀实践，来帮助我们更方便地解决人机交互问题。本文着眼于支撑这些功能的一个底层编程概念 Continuation（译作“续延”），期望能够在了解它之后，大家对这几个功能有进一步的理解和掌握。当然，Continuation 在 React 之外也有很多的应用，可以一眼窥豹。&lt;/p&gt;
&lt;h2 id=&quot;Continuation-是什么？&quot;&gt;&lt;a href=&quot;#Continuation-是什么？&quot; class=&quot;headerlink&quot; title=&quot;Continuation 是什么？&quot;&gt;&lt;/a&gt;Continuation 是什么？&lt;/h2&gt;&lt;p&gt;有些人对 Continuation 并不陌生，因为有时候在谈到 Callback Hell（回调地狱）时会有提到这一概念。但其实它和回调函数大相径庭。&lt;/p&gt;
&lt;p&gt;维基百科对它的定义是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A continuation is an abstract representation of the control state of a computer program.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;即，continuation 是计算机程序控制状态的抽象表示。一个坊间更通俗的说法是：它代表程序的剩余部分。像 continue、break 这类控制流操作符一样，continuation 能够暴露给用户程序从而可以在恰当时机恢复执行，这种基本能力大大扩展了编程语言使用者的发挥空间，也为 excpetion handling、generators、coroutines、algebraic effects 等提供了坚实基础。
&lt;img src=&quot;../images/continuation-01.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;相信很多人和我一样，对这样不明就里的官方解释迷惑不解。没关系，我们首先举一个现实生活中的例子——Continuation 三明治：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;譬如说你现在站在厨房的冰箱前，正在想要不要做一个三明治吃。这时候你做了一个 continuation 小纸条，上面写了你的位置和想要做的事情，然后把 continuation 小纸条贴在口袋上。然后你从冰箱里拿了一些面包和火鸡，自己做了一个三明治，放在柜子上。之后，你拿起口袋上的 continuation 小纸条，然后你发现自己现在又站在了冰箱前面，正在想着要不要做个三明治吃。不过不幸的是，在柜子上已经摆着一个三明治了，而且所有做三明治的食材也都不见了。所以，你就拿起那个三明治开心地吃了起来 :-)&lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    
    <category term="Functional Programming" scheme="https://springuper.github.io/tags/Functional-Programming/"/>
    
    <category term="Continuation" scheme="https://springuper.github.io/tags/Continuation/"/>
    
    <category term="Promise" scheme="https://springuper.github.io/tags/Promise/"/>
    
  </entry>
  
  <entry>
    <title>浅谈 TypeScript 类型系统</title>
    <link href="https://springuper.github.io/typescript-101/"/>
    <id>https://springuper.github.io/typescript-101/</id>
    <published>2019-06-22T13:53:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>时下，TypeScript 可谓当红炸子鸡，众多知名开源项目纷纷采用，大型项目几乎成为必备。其中缘由，除了微软这一开源项目新势力的强大背书以外，最为核心的即是 TypeScript 的类型系统。JavaScript 太需要一套合适的类型体系来支撑日益规模化的协作开发了。</p><p>那么为什么 TypeScript 的类型系统能够做到这一点，而同时期的很多其它方案纷纷落入下风呢？本文着重从原理上剖析 TypeScript 的类型系统。</p><h2 id="结构类型系统（Structural-Type-System）"><a href="#结构类型系统（Structural-Type-System）" class="headerlink" title="结构类型系统（Structural Type System）"></a>结构类型系统（Structural Type System）</h2><p>TypeScript 和 C# 有着颇深的渊源，他们都是在微软大神 Anders Hejlsberg 的领导之下产生的编程语言，两者在诸多设计细节方面十分相似。然而，一个非常重要的不同之处在于，C# 采用的是 <a href="https://en.wikipedia.org/wiki/Nominal_type_system">Nominal Type System</a>（标明类型系统），TypeScript 考虑到 JavaScript 本身的灵活特性，采用的是 <a href="https://en.wikipedia.org/wiki/Structural_type_system">Structural Type System</a>。</p><p>下面我们通过一个例子解释下两者的不同。首先来看一段 C# 代码：</p><figure class="highlight csharp"><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">public</span> <span class="keyword">class</span> <span class="title">Foo</span>  </span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> Name &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">int</span> Id &#123; <span class="keyword">get</span>; <span class="keyword">set</span>;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Bar</span>  </span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> Name &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">int</span> Id &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Foo foo = <span class="keyword">new</span> Foo(); <span class="comment">// Okay.</span></span><br><span class="line">Bar bar = <span class="keyword">new</span> Foo(); <span class="comment">// Error!!!</span></span><br></pre></td></tr></table></figure><p><code>Foo</code>和<code>Bar</code>两个类的内部定义完全一致，但是当将<code>Foo</code>实例赋值给<code>Bar</code>类型的变量时编译器报错，说明两者的类型并不一致。标明类型系统比较的是类型本身，具备非常强的一致性要求。</p><p>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><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span> </span>&#123;</span><br><span class="line">  method(input: <span class="built_in">string</span>): <span class="built_in">number</span> &#123; ... &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Bar</span> </span>&#123;</span><br><span class="line">  method(input: <span class="built_in">string</span>): <span class="built_in">number</span> &#123; ... &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> foo: Foo = <span class="keyword">new</span> Foo(); <span class="comment">// Okay.</span></span><br><span class="line"><span class="keyword">const</span> bar: Bar = <span class="keyword">new</span> Foo(); <span class="comment">// Okay.</span></span><br></pre></td></tr></table></figure><span id="more"></span><p>啊哈，没有任何错误发生。究其原因，TypeScript 比较的并不是类型定义本身，而是类型定义的形状（Shape），即各种约束条件：</p><blockquote><p>One of TypeScript’s core principles is that type checking focuses on the *shape *that values have. This is sometimes called “duck typing” or “structural subtyping”.<br><a href="https://www.typescriptlang.org/docs/handbook/interfaces.html">https://www.typescriptlang.org/docs/handbook/interfaces.html</a></p></blockquote><p>当我们实例化一个<code>Foo</code>对象然后将其赋值给一个<code>Bar</code>类型的变量时，TypeScript 检查发现该实例上具有<code>Bar</code>类型需要的所有约束条件，即一个名为<code>method</code>的接受一个<code>string</code>参数并返回一个<code>number</code>的方法（<code>method(input: string): number</code>），所以不会有任何报错。</p><p>这样做的好处是什么呢？一个好处是和 JavaScript 一脉相承。众所周知，JavaScript 是一门动态脚本语言，<a href="https://en.wikipedia.org/wiki/Duck_typing">Duck Typing</a>（鸭子类型）应用广泛。一个典型等实例是 Iterable，它并不要求像 C++ 一样要求实例必须继承于某个父类或者像 Java 一样要求实例实现某个 <code>Interface</code>，它只检查当前的对象是否实现了<code>@@iterator</code>方法。TypeScript 对症下药，接地气地采用了 Structure Type System 来为 JavaScript 量身定制一套灵活的类型系统。</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><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Point = &#123;</span><br><span class="line">  <span class="attr">x</span>: <span class="built_in">number</span>;</span><br><span class="line">  y: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">plot</span>(<span class="params">point: Point</span>) </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">plot(&#123; <span class="attr">x</span>: <span class="number">10</span>, <span class="attr">y</span>: <span class="number">25</span> &#125;); <span class="comment">// Okay.</span></span><br><span class="line">plot(&#123; <span class="attr">x</span>: <span class="number">8</span>, <span class="attr">y</span>: <span class="number">13</span>, <span class="attr">name</span>: <span class="string">&#x27;foo&#x27;</span> &#125;); <span class="comment">// Extra fields Okay. Need enable `suppressExcessPropertyError`</span></span><br></pre></td></tr></table></figure><p>细细品味，当真没有一点违和感。理想中的 JavaScript 类型系统就应该是这样。</p><p>另外一个好处稍稍隐晦些。总体上来说面向对象型的语言更多地采用 Nominal Type System，函数式语言偏向于采用 Structure Type System，JavaScript 是一种非常独特的语言，两种编程范式兼而有之，不过近些年来主流方向是函数式，arrow function、Promise 等语言特性遍地开花，React、RxJS、Ramda 等社区方案备受推崇。TypeScript 顺势而为，解决 JavaScript 弱类型问题的同时，为函数式编程的发展狠狠地添了一把火。</p><h2 id="集合与类型"><a href="#集合与类型" class="headerlink" title="集合与类型"></a>集合与类型</h2><p>TypeScript 只检查 Shape，即类型定义的约束条件，听起来和集合（Set）这一概念颇为相像。接下来，我们试着从集合的角度更深层次地理解 TypeScript 的类型。</p><p>上一个例子中定义的 Point 类型实际上可以理解为一个这样的集合：</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></pre></td><td class="code"><pre><span class="line">&#123; obj | <span class="keyword">typeof</span> obj === <span class="string">&#x27;object&#x27;</span> &amp;&amp;</span><br><span class="line">        <span class="keyword">typeof</span> obj.x === <span class="string">&#x27;number&#x27;</span> &amp;&amp;</span><br><span class="line">        <span class="keyword">typeof</span> obj.y === <span class="string">&#x27;number&#x27;</span> &#125;</span><br></pre></td></tr></table></figure><p><strong>交集</strong></p><p>假设我们再定义一个<code>Name</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Name = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>在所有的对象实例组成的集合中，有的对象实例符合 <code>Point</code> 类型，有的符合 <code>Name</code> 类型，有的符合它们两者，有的两者都不符合。问题来了，我们是否可以定义一个类型，要求符合它们两者呢？TypeScript 给出的答案是交集类型（Intersection Type）：</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">type</span> NamedPoint = Name &amp; Point;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">superPlot</span>(<span class="params">point: NamedPoint</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(point.name); <span class="comment">// Okay.</span></span><br><span class="line">  <span class="built_in">console</span>.log(point.x); <span class="comment">// Okay.</span></span><br><span class="line">  <span class="built_in">console</span>.log(point.sing); <span class="comment">// Error!!!</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过集合来理解交集类型，一目了然：<img src="../images/typescript-101-01.jpg"><em>交集类型 NamedPoint</em></p><p><strong>合集</strong></p><p>TypeScript 既然支持交集类型，那么合集类型（Union Type，也译作联合类型）呢？当然支持，而且更为强大。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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> NameOrPoint = Name | Point;</span><br><span class="line"><span class="keyword">type</span> MyBoolean = <span class="literal">true</span> | <span class="literal">false</span>;</span><br><span class="line"><span class="keyword">type</span> Result = &#123; <span class="attr">status</span>: <span class="string">&#x27;ok&#x27;</span> &#125; | &#123; <span class="attr">status</span>: <span class="string">&#x27;error&#x27;</span>, <span class="attr">reason</span>: <span class="built_in">string</span> &#125;;</span><br></pre></td></tr></table></figure><p>类似地，上述例子中定义的<code>NameOrPoint</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">&#123; item | item satisfies Name ||</span><br><span class="line">          item satisfies Point &#125;</span><br><span class="line"></span><br><span class="line">===&gt;</span><br><span class="line"></span><br><span class="line">&#123; item | (<span class="keyword">typeof</span> item === <span class="string">&#x27;object&#x27;</span> &amp;&amp; <span class="keyword">typeof</span> item.name === <span class="string">&#x27;string&#x27;</span>) ||</span><br><span class="line">         (<span class="keyword">typeof</span> item === <span class="string">&#x27;object&#x27;</span> &amp;&amp;</span><br><span class="line">          <span class="keyword">typeof</span> item.x === <span class="string">&#x27;number&#x27;</span> &amp;&amp;</span><br><span class="line">          <span class="keyword">typeof</span> item.y === <span class="string">&#x27;number&#x27;</span>) &#125;</span><br></pre></td></tr></table></figure><p>通过图形，我们可以更直观地理解合集类型：<img src="../images/typescript-101-02.jpg"><em>合集类型（Union Type）</em></p><p><strong>类型收缩（Type Narrowing）</strong></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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">triple</span>(<span class="params">input: <span class="built_in">number</span> | <span class="built_in">string</span></span>): <span class="title">number</span> | <span class="title">string</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> input === <span class="string">&#x27;number&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> input * <span class="number">3</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> (<span class="keyword">new</span> <span class="built_in">Array</span>(<span class="number">4</span>)).join(input);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>TypeScript 能否正确推断出各个逻辑分支中的<code>input</code>类型呢？借助基于控制流的类型分析（<a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#control-flow-based-type-analysis">Control Flow Based Type Analysis</a>）以及<code>typeof</code>等类型哨兵（<a href="https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types">Type Guard</a>），TypeScript 可以成功分析出上述示例中 if 分支中的<code>input</code>一定是 number 类型，else 分支<code>input</code>只能是其余的类型，即 string。这一贴心的功能显著提高了代码类型匹配的“智能”程度，有效降低了不必要的类型断言或者转换。微软大法真香~</p><p>然而，<code>typeof</code>、<code>instanceof</code> 等类型哨兵对于复合类型构成的合集类型并不奏效。在介绍解决方案之前，我们需要先理解字面量类型（literal Type）。</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">type</span> GreetName = <span class="string">&#x27;world&#x27;</span> | <span class="string">&#x27;moto&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">greet</span>(<span class="params">name: GreetName</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`hello, <span class="subst">$&#123;name&#125;</span>!`</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">greet(<span class="string">&#x27;world&#x27;</span>); <span class="comment">// Okay.</span></span><br><span class="line">greet(<span class="string">&#x27;moto&#x27;</span>); <span class="comment">// Okay.</span></span><br><span class="line">greet(<span class="string">&#x27;foo&#x27;</span>); <span class="comment">// Error!!!</span></span><br></pre></td></tr></table></figure><p><code>GreetName</code>只包含两个字面量 <code>world</code>和<code>moto</code>，任何其它值都不属于这一类型：</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">&#123; item | item === <span class="string">&#x27;world&#x27;</span> || item === <span class="string">&#x27;moto&#x27;</span> &#125; </span><br></pre></td></tr></table></figure><p>字面量限定的不再是一个类似<code>string</code>的一个范围，而是具体的单值，因此这种类型又称作单例类型（Singleton Type）。</p><p>Ok，现在我们可以介绍如何收缩复合类型的合集类型了，解决方案的核心思路是：每一个合集中的构成类型都有一个同名不同值的单例类型属性，保证它们之间没有任何交集，然后通过在该属性上应用类型哨兵便可以唯一区分。一起看下这个稍长的示例：</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><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">type</span> Square = &#123;</span><br><span class="line">  <span class="attr">kind</span>: <span class="string">&#x27;square&#x27;</span>;</span><br><span class="line">  size: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Rectangle = &#123;</span><br><span class="line">  <span class="attr">kind</span>: <span class="string">&#x27;rectangle&#x27;</span>;</span><br><span class="line">  width: <span class="built_in">number</span>;</span><br><span class="line">  height: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Circle = &#123;</span><br><span class="line">  <span class="attr">kind</span>: <span class="string">&#x27;circle&#x27;</span>;</span><br><span class="line">  radius: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Shape = Square | Rectangle | Circle;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">area</span>(<span class="params">shape: Shape</span>): <span class="title">number</span> </span>&#123;</span><br><span class="line">  <span class="keyword">switch</span> (shape.kind) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;square&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> shape.size * shape.size;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;rectangle&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> shape.width * shape.height;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;circle&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="built_in">Math</span>.PI * shape.radius * shape.radius;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Shape</code>合集类型中的各个构成类型都有一个<code>kind</code>属性，它的值是一个具体的字符串，在<code>area</code>方法中，switch 类型哨兵针对<code>kind</code>的不同取值可以分析出各个<code>case</code>分支中的具体类型。这种模式在很多情况下非常有用，所以给它起了一个别致的名字：可识别联合（Discriminated Union）。</p><h2 id="类型编程"><a href="#类型编程" class="headerlink" title="类型编程"></a>类型编程</h2><p>TypeScript 中不仅可以对各种类型进行类似集合一样的操作，进一步地，我们可以进行类型层面的编程。</p><p><strong>泛型（</strong><a href="https://www.typescriptlang.org/docs/handbook/generics.html">Generics</a><strong>）</strong></p><p>主流的编程语言通常都支持泛型以提供更加出色的抽象能力，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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">identity</span>&lt;<span class="title">T</span>&gt;(<span class="params">x: T</span>): <span class="title">T</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> x;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> outputString = identity&lt;<span class="built_in">string</span>&gt;(<span class="string">&#x27;foo&#x27;</span>); <span class="comment">// Okay. outputString is a string</span></span><br><span class="line"><span class="keyword">const</span> outputNumber = identity&lt;<span class="built_in">number</span>&gt;(<span class="number">666</span>); <span class="comment">// Okay. outputNumber is a number</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><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> identity = <span class="function"><span class="params">x</span> =&gt;</span> x; <span class="comment">// value level</span></span><br><span class="line"><span class="keyword">type</span> Identity&lt;T&gt; = T; <span class="comment">// type level</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> pair = <span class="function">(<span class="params">x, y</span>) =&gt;</span> [x, y]; <span class="comment">// value level</span></span><br><span class="line"><span class="keyword">type</span> Pair&lt;T, U&gt; = [T, U]; <span class="comment">// type level</span></span><br></pre></td></tr></table></figure><p>在定义泛型时，我们只是定义了一个逻辑处理过程，只有在用具体输入类型调用它时，才会得到真正的结果类型。所以我们在编写泛型时，实际上是在进行类型层面的编程，因为它是一个函数。</p><p><strong>片段（</strong><a href="https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types">Partial</a><strong>）</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User = &#123;</span><br><span class="line">  <span class="attr">id</span>: <span class="built_in">number</span>;</span><br><span class="line">  name: <span class="built_in">string</span>;</span><br><span class="line">  birthday: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">updateUser(user.id, &#123;</span><br><span class="line">  name,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>在<code>updateUser</code>的第二个参数中，我们希望放松限制，满足<code>User</code>类型的部分约束即可，例如只有<code>name</code>。也就是说，我们希望有一个类型函数，能够完成如下操作：<img src="../images/typescript-101-03.jpg"></p><p>前面我们提到过，泛型是类型的函数，TypeScript 提供了一些精简的类型操作符，例如<code>keyof</code>，<code>in</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Partial&lt;T&gt; = &#123;</span><br><span class="line">  [P <span class="keyword">in</span> keyof T]?: T[P];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">updateUser</span>(<span class="params">id: User[<span class="string">&#x27;id&#x27;</span>], data: Partial&lt;User&gt;</span>) </span>&#123;&#125;</span><br></pre></td></tr></table></figure><p>鉴于<code>Partial</code>需求非常普遍，TypeScript 在 2.1 版本中加入了包含<code>Partial</code>、<code>Readonly</code>、<code>Pick</code>等工具类型。</p><p><strong>条件类型（</strong><a href="https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types">Conditional Type</a><strong>）</strong></p><p>千呼万唤始出来，TypeScript 终于在 2.8 版本中引入了条件类型，用来表述非单一形式的类型。当时一位主要维护者甚至感慨道：</p><blockquote><p>Working through our (enormous) backlog of unsorted TypeScript “Suggestions” and it’s remarkable how many of them are solved by conditional types.<br>– Ryan Cavanaugh</p></blockquote><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="function"><span class="keyword">function</span> <span class="title">process</span>(<span class="params">text: <span class="built_in">string</span> | <span class="literal">null</span></span>): <span class="title">string</span> | <span class="title">null</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> text &amp;&amp; text.replace(<span class="regexp">/f/g</span>, <span class="string">&#x27;p&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">process(<span class="string">&#x27;foo&#x27;</span>).toUpperCase(); <span class="comment">// Error!!!</span></span><br></pre></td></tr></table></figure><p><code>process</code>方法可以接受一个字符串或者<code>null</code>的参数，如果这个参数是字符串，则返回一个字符串，否则返回<code>null</code>。上述实现由于欠缺输入类型和输出类型之间的关联关系，导致即便输入是字符串时 TypeScript 仍然不能推断出输出是字符串，最终编译报错。</p><p>条件类型一般形式是<code>T extends U ? X : Y</code> ，和 JavaScript 的三元表达式一致，其中条件部分<code>T extends U</code> 表示 T 是 U 的子集，即 T 类型的所有取值都包含在 U 类型中。借助这一能力，<code>process</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">process</span>&lt;<span class="title">T</span> <span class="title">extends</span> <span class="title">string</span> | <span class="title">null</span>&gt;(<span class="params">text: T</span>): <span class="title">T</span> <span class="title">extends</span> <span class="title">string</span> ? <span class="title">string</span> : <span class="title">null</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> text &amp;&amp; text.replace(<span class="regexp">/f/g</span>, <span class="string">&#x27;p&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">process(<span class="string">&#x27;foo&#x27;</span>).toUpperCase(); <span class="comment">// Okay.</span></span><br><span class="line">process(<span class="literal">null</span>).toUpperCase(); <span class="comment">// Error!!!</span></span><br></pre></td></tr></table></figure><p>上述示例通过定义一个泛型 T，然后对输入类型（T）和输出类型进行关联，并通过条件类型进行不同类型处理达到预期效果。注：目前 TypeScript 支持还有问题，详见 <a href="https://github.com/microsoft/TypeScript/issues/24929">#24929</a>。</p><p>条件类型支持嵌套，轻松支持类似 switch 的多条件分支效果。当 T 类型是合集类型时，条件类型可以进行展开：</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">(A | B) <span class="keyword">extends</span> U ? X : Y ==&gt; (A <span class="keyword">extends</span> U ? X : Y) | (B <span class="keyword">extends</span> U ? X : Y)</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">type</span> Diff&lt;T, U&gt; = T <span class="keyword">extends</span> U ? <span class="built_in">never</span> : T;</span><br><span class="line"><span class="keyword">type</span> DiffDemo = Diff&lt;<span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;b&#x27;</span> | <span class="string">&#x27;c&#x27;</span>, <span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;d&#x27;</span> | <span class="string">&#x27;e&#x27;</span>&gt;;  <span class="comment">// &#x27;b&#x27; | &#x27;c&#x27;</span></span><br></pre></td></tr></table></figure><p>考虑到常见需求，TypeScript 在发布条件类型时，一并发布了<code>Exclude&lt;T, U&gt;</code>、<code>NonNullable&lt;T&gt;</code>等内置工具类型。</p><p><strong>递归</strong></p><p>有些情况下我们希望 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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Stack&lt;T&gt; = &#123;</span><br><span class="line">  <span class="attr">top</span>: T;</span><br><span class="line">  rest: Stack&lt;T&gt;;</span><br><span class="line">&#125; | <span class="literal">null</span>;</span><br></pre></td></tr></table></figure><p>堆栈本身可能是<code>null</code>，如果存在的话，栈顶是一个类型为 T 的数据，其余部分又可以被描述为一个子堆栈。在前面我们提到，泛型本身可以认为是一个类型函数，因此这里的递归并不会导致无限循环发生，<code>rest</code>对应的类型是一个函数，只有当需要推断数据类型的时候才会调用。</p><p>对递归的支持这一特性往往被低估。事实上，递归是很多复杂操作的基础，例如条件类型可以通过递归实现。</p><p><strong>图灵完备（Turing Complete）</strong></p><p>有了类型层面的函数（泛型）、条件语句（条件类型）、递归等功能之后，我们不禁有一个疑问：TypeScript 能够描述所有的数据类型吗？是的，已经有人证明，<a href="https://github.com/Microsoft/TypeScript/issues/14833">TypeScript 是图灵完备的</a>，它完全有能力完成任何类型层面的可计算问题。更通俗一点说，TypeScript 包含了一套完整的类型层面编程能力，就像我们可以用 JavaScript、C++、Go 等编程语言解决各种实际问题一样，TypeScript 可以解决各种类型问题，因为本质上它们的内核都和图灵机等价。</p><p>目前已经有一些“不安分” 的开发者开发出了判定素数的类型<code>IsPrime&lt;T&gt;</code> 、将合集类型转换为元组的类型<code>UnionToTuple&lt;T&gt;</code>、根据条件获取子集类型的类型<code>ConditionalSubset&lt;T&gt;</code> 等，每一个实现都令人叹为观止。</p><p>诚然，现在很多复杂类型仍然需要大量的深度类型编程才能实现，这种门槛一定程度上限制了 TypeScript 在一些复杂场景下的应用。不过我们也欣喜地注意到，作为一个流行的开源项目，TypeScript 也在聆听社区的声音，不断改进底层特性并推出新的工具类型，方便开发者多快好省地写代码。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>在引入 TypeScript 的过程中，我们基本都可以感受到它的类型约束带来的种种益处。明确的接口契约，一方面有力加强了工程质量，弥补了 JavaScript 的最大软肋，另一方面，借助一些基于此构建的周边工具，开发效率也获得了明显提升。然而 TypeScript 的中高级进阶并非易事，我们时常困惑于某些具体特性的要义，纠结在一个复杂类型的实现。最终我们发现，欠缺的实际上是一个对 TypeScript 类型系统的深层次理解。</p><p>本文深受 Drew Colthorp 先生的 <a href="https://youtu.be/MbZoQlmQaWQ">Understanding TypeScript’s Structural Type System </a>演讲启发，并在此基础上进行了适当外延，从原理层面由浅入深层层递进地阐述了 TypeScript 类型系统的特性和思想。期望这篇文章能够帮助你理解 TypeScript 类型系统的基本原理和强大功能，知其然且知其所以然，在 TypeScript 的进阶之路上一路狂奔。</p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>TypeScript 是用什么编程语言写的呢？令人惊讶的是，它是 100% 用 TypeScript 完成的：<img src="../images/typescript-101-04.jpg"></p><p>其实这种情况并不鲜见，很多编程语言，例如 C、C++ 等，都有自举（self-hosting，用自身语言实现）编译器，这种技术一般称作 <a href="https://en.wikipedia.org/wiki/Bootstrapping_(compilers)">Compiler Bootstraping</a>，有兴趣的小伙伴可以去进一步了解。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md">TypeScript Language Specification</a></li><li><a href="https://youtu.be/MbZoQlmQaWQ">Understanding TypeScript’s Structural Type System - Drew Colthorp</a></li><li><a href="https://www.triplet.fi/blog/type-system-differences-in-typescript-structural-type-system-vs-c-java-nominal-type-system/">Type System Differences in TypeScript (Structural Type System) VS C# &amp; Java (Nominal Type System)</a></li><li><a href="https://mariusschulz.com/blog/typescript-2-0-control-flow-based-type-analysis">TypeScript 2.0: Control Flow Based Type Analysis</a></li><li><a href="https://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/">Conditional types in TypeScript</a></li><li><a href="https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c">TypeScript: Create a condition-based subset types</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;时下，TypeScript 可谓当红炸子鸡，众多知名开源项目纷纷采用，大型项目几乎成为必备。其中缘由，除了微软这一开源项目新势力的强大背书以外，最为核心的即是 TypeScript 的类型系统。JavaScript 太需要一套合适的类型体系来支撑日益规模化的协作开发了。&lt;/p&gt;
&lt;p&gt;那么为什么 TypeScript 的类型系统能够做到这一点，而同时期的很多其它方案纷纷落入下风呢？本文着重从原理上剖析 TypeScript 的类型系统。&lt;/p&gt;
&lt;h2 id=&quot;结构类型系统（Structural-Type-System）&quot;&gt;&lt;a href=&quot;#结构类型系统（Structural-Type-System）&quot; class=&quot;headerlink&quot; title=&quot;结构类型系统（Structural Type System）&quot;&gt;&lt;/a&gt;结构类型系统（Structural Type System）&lt;/h2&gt;&lt;p&gt;TypeScript 和 C# 有着颇深的渊源，他们都是在微软大神 Anders Hejlsberg 的领导之下产生的编程语言，两者在诸多设计细节方面十分相似。然而，一个非常重要的不同之处在于，C# 采用的是 &lt;a href=&quot;https://en.wikipedia.org/wiki/Nominal_type_system&quot;&gt;Nominal Type System&lt;/a&gt;（标明类型系统），TypeScript 考虑到 JavaScript 本身的灵活特性，采用的是 &lt;a href=&quot;https://en.wikipedia.org/wiki/Structural_type_system&quot;&gt;Structural Type System&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;下面我们通过一个例子解释下两者的不同。首先来看一段 C# 代码：&lt;/p&gt;
&lt;figure class=&quot;highlight csharp&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;Foo&lt;/span&gt;  &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt; Name &amp;#123; &lt;span class=&quot;keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;keyword&quot;&gt;set&lt;/span&gt;; &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;built_in&quot;&gt;int&lt;/span&gt; Id &amp;#123; &lt;span class=&quot;keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;keyword&quot;&gt;set&lt;/span&gt;;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;Bar&lt;/span&gt;  &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt; Name &amp;#123; &lt;span class=&quot;keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;keyword&quot;&gt;set&lt;/span&gt;; &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;built_in&quot;&gt;int&lt;/span&gt; Id &amp;#123; &lt;span class=&quot;keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;keyword&quot;&gt;set&lt;/span&gt;; &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Foo foo = &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; Foo(); &lt;span class=&quot;comment&quot;&gt;// Okay.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Bar bar = &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; Foo(); &lt;span class=&quot;comment&quot;&gt;// Error!!!&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code&gt;Foo&lt;/code&gt;和&lt;code&gt;Bar&lt;/code&gt;两个类的内部定义完全一致，但是当将&lt;code&gt;Foo&lt;/code&gt;实例赋值给&lt;code&gt;Bar&lt;/code&gt;类型的变量时编译器报错，说明两者的类型并不一致。标明类型系统比较的是类型本身，具备非常强的一致性要求。&lt;/p&gt;
&lt;p&gt;TypeScript 则不太一样：&lt;/p&gt;
&lt;figure class=&quot;highlight ts&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;class&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;Foo&lt;/span&gt; &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  method(input: &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt;): &lt;span class=&quot;built_in&quot;&gt;number&lt;/span&gt; &amp;#123; ... &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;class&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;Bar&lt;/span&gt; &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  method(input: &lt;span class=&quot;built_in&quot;&gt;string&lt;/span&gt;): &lt;span class=&quot;built_in&quot;&gt;number&lt;/span&gt; &amp;#123; ... &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; foo: Foo = &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; Foo(); &lt;span class=&quot;comment&quot;&gt;// Okay.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; bar: Bar = &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; Foo(); &lt;span class=&quot;comment&quot;&gt;// Okay.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="TypeScript" scheme="https://springuper.github.io/tags/TypeScript/"/>
    
    <category term="Type System" scheme="https://springuper.github.io/tags/Type-System/"/>
    
  </entry>
  
  <entry>
    <title>前端中的 Monad</title>
    <link href="https://springuper.github.io/monad/"/>
    <id>https://springuper.github.io/monad/</id>
    <published>2018-10-23T18:30:00.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>Monad 这一概念起源于上世纪五十年代末，由数学家 Roger Godement 提出并应用在范畴论中。直到上世纪九十年代，计算机科学家 Eugenio Moggi 首次将其引入到函数式编程中。由于 Monad 具备优秀的抽象和通用能力，从 Haskell 开始，众多编程语言都相继引入了这一概念，函数式编程迎来一次质的飞跃。</p><p>JavaScript 具备函数式编程的基本要素—— λ 算子，即一等公民的函数及其附属的一些性质。Monad 在 JavaScript 中也有不少应用，其中最知名的就是备受赞誉的 Promise。</p><p>然而，江湖上有句传言：</p><blockquote><p><em>“Once you understand monads, you immediately become incapable of explaining them to anyone else” by Gilad Bracha</em></p></blockquote><p>笔者自认为虽有些愚钝，还算是勤奋，但读过诸多介绍 Monad 的文章书籍后都不得要领，浑浑噩噩。直到最近看了 Programming in Haskell 作者 Graham Hutton 教授的 <a href="https://www.youtube.com/watch?v=t1e8gqXLbsU">What is a Monad? - Computerphile</a> 后，才算是焕然大悟。</p><p>本文尝试从一个实例出发，讲解遇到的问题以及 Monad 解决这些问题的思路，希望可以帮助大家领会其要义。为了清晰地标注类型，本文的代码均使用 TypeScript。</p><h2 id="举个栗子"><a href="#举个栗子" class="headerlink" title="举个栗子"></a>举个栗子</h2><p>假设我们要处理这样一个问题：</p><p>实现一个求值函数<code>evaluate</code>，它可以接收类似 <img src="../images/monad-01.jpg" alt="8 \div 2"> 、 <img src="../images/monad-02.jpg" alt="8 \div (12 \div 3)"> 的只有除法的表达式，并返回最终的结果。</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><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> Expr &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;value&#x27;</span> | <span class="string">&#x27;division&#x27;</span>;</span><br><span class="line">  value: <span class="built_in">number</span> | DivisionExpr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> DivisionExpr &#123;</span><br><span class="line">  <span class="attr">left</span>: Expr;</span><br><span class="line">  right: Expr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// AST representation for expression &#x27;8 / (12 / 3)&#x27;</span></span><br><span class="line"><span class="keyword">const</span> expr: Expr = &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;division&#x27;</span>,</span><br><span class="line">  <span class="attr">value</span>: &#123;</span><br><span class="line">    <span class="attr">left</span>: &#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;value&#x27;</span>,</span><br><span class="line">      <span class="attr">value</span>: <span class="number">8</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">right</span>: &#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;division&#x27;</span>,</span><br><span class="line">      <span class="attr">value</span>: &#123;</span><br><span class="line">        <span class="attr">left</span>: &#123;</span><br><span class="line">          <span class="attr">type</span>: <span class="string">&#x27;value&#x27;</span>,</span><br><span class="line">          <span class="attr">value</span>: <span class="number">12</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">right</span>: &#123;</span><br><span class="line">          <span class="attr">type</span>: <span class="string">&#x27;value&#x27;</span>,</span><br><span class="line">          <span class="attr">value</span>: <span class="number">3</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><span class="line">&#125;;</span><br></pre></td></tr></table></figure><span id="more"></span><h2 id="第一版evaluate"><a href="#第一版evaluate" class="headerlink" title="第一版evaluate"></a><strong>第一版<code>evaluate</code></strong></h2><p>只处理除法，而且抽象语法树非常清晰，<code>evaluate</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">e: Expr</span>): <span class="title">number</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (e.type === <span class="string">&#x27;value&#x27;</span>) <span class="keyword">return</span> &lt;<span class="built_in">number</span>&gt;e.value;</span><br><span class="line">  <span class="keyword">return</span> evaluate((&lt;DivisionExpr&gt;e.value).left) / evaluate((&lt;DivisionExpr&gt;e.value).right);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>逻辑非常清晰：</p><ul><li>如果当前处理的抽象语法树节点类型是<code>value</code>，直接返回具体数值</li><li>否则，当前处理的节点类型是<code>division</code>，递归计算出<code>left</code>和<code>right</code>的最终结果，然后返回做除法后的结果</li></ul><h2 id="第二版evaluate"><a href="#第二版evaluate" class="headerlink" title="第二版evaluate"></a><strong>第二版<code>evaluate</code></strong></h2><p>在数学中，除法的除数是不能为 0 的。JavaScript 中的<code>/</code><a href="https://www.ecma-international.org/ecma-262/5.1/#sec-11.5.2">运算符</a>遵循了 IEEE 754 规范，当除数为 0 时行为如下：</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="number">1</span> / <span class="number">0</span> <span class="comment">// =&gt; Infinity</span></span><br><span class="line"><span class="number">0</span> / <span class="number">0</span> <span class="comment">// =&gt; NaN</span></span><br></pre></td></tr></table></figure><p>在其它语言中，例如 Haskell、Python 等，如果除数为 0 会直接抛异常。既然 JavaScript 在上述两种情况下的值也都是没有意义的，也可以将其理解为一种异常。<code>evaluate</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="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">e: Expr</span>): <span class="title">number</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (e.type === <span class="string">&#x27;value&#x27;</span>) <span class="keyword">return</span> &lt;<span class="built_in">number</span>&gt;e.value;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> left = evaluate((&lt;DivisionExpr&gt;e.value).left);</span><br><span class="line">  <span class="keyword">const</span> right = evaluate((&lt;DivisionExpr&gt;e.value).right);</span><br><span class="line">  <span class="keyword">if</span> (right === <span class="number">0</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">&#x27;The divisor is zero.&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> left / right;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然而，这意味着对于调用方来说，要么需要提前检查各个除数是否为零，要么需要使用<code>try-catch</code>来确保自己的程序不会崩溃，这是一件让人崩溃的事情。</p><h2 id="第三版evaluate"><a href="#第三版evaluate" class="headerlink" title="第三版evaluate"></a><strong>第三版<code>evaluate</code></strong></h2><p>我们可以对第二版<code>evaluate</code>进行改进，发现除数为 0 时并不是抛异常，而是返回一个无意义的结果。</p><p>如何表示有意义和无意义的结果呢？这里我们引入<code>Maybe</code>:</p><ul><li><code>Maybe.nothing()</code>代表无意义的结果</li><li><code>Maybe.just(number)</code>代表有意义的结果</li></ul><p><code>Maybe</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><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">default</span> <span class="class"><span class="keyword">class</span> <span class="title">Maybe</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">static</span> nothing&lt;T&gt;() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Maybe&lt;T&gt;();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">static</span> just&lt;T&gt;(value: T) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Maybe(value);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> value?: T;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="title">constructor</span>(<span class="params">value?: T</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (value) &#123;</span><br><span class="line">      <span class="built_in">this</span>.value = value;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="title">isNothing</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">typeof</span> <span class="built_in">this</span>.value === <span class="string">&#x27;undefined&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Maybe</code>的本质是一种容器，在类似数字、字符串、布尔值等数据的表达能力欠缺的情况下可以对其进行增强。<img src="../images/monad-03.jpg"></p><p>借助<code>Maybe</code>，第三版<code>evaluate</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><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="function"><span class="keyword">function</span> <span class="title">safeDiv</span>(<span class="params">x: <span class="built_in">number</span>, y: <span class="built_in">number</span></span>): <span class="title">Maybe</span>&lt;<span class="title">number</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (y === <span class="number">0</span>) <span class="keyword">return</span> Maybe.nothing&lt;<span class="built_in">number</span>&gt;();</span><br><span class="line">  <span class="keyword">return</span> Maybe.just(x / y);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">e: Expr</span>): <span class="title">Maybe</span>&lt;<span class="title">number</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (e.type === <span class="string">&#x27;value&#x27;</span>) <span class="keyword">return</span> Maybe.just(&lt;<span class="built_in">number</span>&gt;e.value);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> left = evaluate((&lt;DivisionExpr&gt;e.value).left);</span><br><span class="line">  <span class="keyword">if</span> (left.isNothing()) &#123;</span><br><span class="line">    <span class="keyword">return</span> Maybe.nothing&lt;<span class="built_in">number</span>&gt;();</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> right = evaluate((&lt;DivisionExpr&gt;e.value).right);</span><br><span class="line">    <span class="keyword">if</span> (right.isNothing()) &#123;</span><br><span class="line">      <span class="keyword">return</span> Maybe.nothing&lt;<span class="built_in">number</span>&gt;();</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> safeDiv(&lt;<span class="built_in">number</span>&gt;left.value, &lt;<span class="built_in">number</span>&gt;right.value);</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>首先定义了<code>safeDiv</code>函数，专门处理除法，当发现除数为 0 时，返回<code>Maybe.nothing()</code>，否则做除法并将结果放在容器中返回。</p><p>在<code>evaluate</code>函数中，如果<code>left</code>是无意义的，则直接返回<code>Maybe.nothing()</code>，否则继续对<code>right</code>进行递归求值，如果<code>right</code>是无意义的，直接返回<code>Maybe.nothing()</code>。最后，在<code>left</code>和<code>right</code>都正常求得结果的情况下，调用<code>safeDiv</code>进行除法运算并返回。</p><h2 id="第四版evaluate"><a href="#第四版evaluate" class="headerlink" title="第四版evaluate"></a>第四版<code>evaluate</code></h2><p>有些敏锐的同学可能已经注意到，第三版<code>evaluate</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">const</span> left = evaluate((&lt;DivisionExpr&gt;e.value).left);</span><br><span class="line"><span class="keyword">if</span> (left.isNothing()) &#123;</span><br><span class="line">  <span class="keyword">return</span> Maybe.nothing&lt;<span class="built_in">number</span>&gt;();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> right = evaluate((&lt;DivisionExpr&gt;e.value).right);</span><br><span class="line"><span class="keyword">if</span> (right.isNothing()) &#123;</span><br><span class="line">  <span class="keyword">return</span> Maybe.nothing&lt;<span class="built_in">number</span>&gt;();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>DRY 深入程序员心，有没有办法将其简化呢？</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> maybe = someFunReturnsMaybeInstance(params);</span><br><span class="line"><span class="keyword">return</span> maybe.isNothing() ? Maybe.nothing&lt;T&gt;() : doSomething(&lt;T&gt;maybe.value);</span><br></pre></td></tr></table></figure><p>即，在求解多个级联 Maybe 实例时：</p><ul><li>只要其中一个无意义，便不再继续求解，直接返回<code>Maybe.nothing()</code></li><li>所有 Maybe 实例都成功求解时，进行最终的运算（本例中是<code>safeDiv</code>）</li></ul><p>得出这样的结论后，我们来改进下<code>Maybe</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">export</span> <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> <span class="title">Maybe</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  </span><br><span class="line">  bind&lt;U&gt;(transform: <span class="function">(<span class="params">value: T</span>) =&gt;</span> Maybe&lt;U&gt;) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.isNothing() ? Maybe.nothing&lt;U&gt;() : transform(&lt;T&gt;<span class="built_in">this</span>.value);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>增加<code>bind</code>方法（并非函数原型的<code>bind</code>方法）后，Maybe 进化为了一个 Monad，稍后我们详细论述。改进的第四版<code>evaluate</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="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">e: Expr</span>): <span class="title">Maybe</span>&lt;<span class="title">number</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (e.type === <span class="string">&#x27;value&#x27;</span>) <span class="keyword">return</span> Maybe.just(&lt;<span class="built_in">number</span>&gt;e.value);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> evaluate((&lt;DivisionExpr&gt;e.value).left)</span><br><span class="line">    .bind(<span class="function"><span class="params">left</span> =&gt;</span> evaluate((&lt;DivisionExpr&gt;e.value).right)</span><br><span class="line">      .bind(<span class="function"><span class="params">right</span> =&gt;</span> safeDiv(left, right)));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在和之前方法行为一致的前提下，代码变得更加简练易读，这正是 Maybe Monad 给我们带来的好处。</p><h2 id="Monad"><a href="#Monad" class="headerlink" title="Monad"></a>Monad</h2><p>从<code>evaluate</code>的案例中我们展示了 Monad 所能解决的问题，即通过包装数据并赋予其额外的链式运算能力来简化一系列多步骤的计算。下面我们比较正式的介绍一下什么是 Monad。</p><p>在函数式编程中，Monad 是一种设计模式，它的的概念定义包含三部分：</p><ol><li>类型构造器，用来指定如何从其它已知类型构造新的一元类型 例如<code>Maybe&lt;number&gt;</code>这一类型构造器指定了从底层<code>number</code>类型的构造方式</li><li>一个一元运算符，通常称作<code>return</code>或者<code>unit</code>，用以将一个底层数据封装为 Monad。形式化定义为： <img src="../images/monad-04.jpg" alt="unit (a) : T \rightarrow M\ T"> （a 为类型为 T 的任意对象） 在 Maybe 中，这个一元运算符是类型的<code>constructor</code>方法，我们也可以再定义一个类方法<code>unit</code>来返回<code>new Maybe&lt;number&gt;(number)</code></li><li>一个连接符，通常称作<code>bind</code>，用以从 Monad 中取出数据，并将其放入一个接受这一数据并返回 Monad 的函数中。形式化定义为 <img src="../images/monad-05.jpg" alt="bind (ma,f) : (M\ T,(T \rightarrow M\ U)) -&gt; M\ U"> （ma 为类型为 <img src="../images/monad-06.jpg" alt="M\ T"> 的 Monad 实例， <img src="../images/monad-07.jpg" alt="f"> 是转换函数） 在 Maybe 中，<code>bind</code>接受一个<code>transform</code>函数，该函数接受<code>T</code>类型的原始数据，并返回新的<code>Maybe&lt;T&gt;</code>Monad 。因为<code>bind</code>返回的仍然是一个新的 Monad，这个 Monad 也具有<code>bind</code>方法，因此可以链式调用下去，威力十足</li></ol><p>虽然枯燥，不过有了上面实例的铺垫，相信大家对于这三个部分也有了自己的见解。</p><p>定义之外，Monad 还需要遵循三个定律：</p><ol><li><img src="../images/monad-08.jpg" alt="bind (unit (a), f)     ==  f(a)"></li><li><img src="../images/monad-09.jpg" alt="bind (ma, unit)        ==  ma"></li><li><img src="../images/monad-10.jpg" alt="bind (bind (ma,f), g)  ==  bind (ma, λx.bind ( f(x), g))"></li></ol><p>前面两条显而易见，第三条要求 Monad 的<code>bind</code>运算需要满足结合律，类似 <img src="../images/monad-11.jpg" alt="(8 + 4) + 2"> 与 <img src="../images/monad-12.jpg" alt="8 + (4 + 2)"> 等价一样，<code>bind</code>的先后次序并不改变最终结果（把<code>bind</code>想象成 <img src="../images/monad-13.jpg" alt="(8 + 4) + 2"> 中的加号）。</p><p>下面我们验证下 Maybe Monad 是否满足这三条定律：</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="keyword">const</span> unit = <span class="function">(<span class="params">value: <span class="built_in">number</span></span>) =&gt;</span> Maybe.just&lt;<span class="built_in">number</span>&gt;(value);</span><br><span class="line"><span class="keyword">const</span> f = <span class="function">(<span class="params">value: <span class="built_in">number</span></span>) =&gt;</span> Maybe.just&lt;<span class="built_in">number</span>&gt;(value * <span class="number">2</span>);</span><br><span class="line"><span class="keyword">const</span> g = <span class="function">(<span class="params">value: <span class="built_in">number</span></span>) =&gt;</span> Maybe.just&lt;<span class="built_in">number</span>&gt;(value - <span class="number">5</span>);</span><br><span class="line"><span class="keyword">const</span> ma = Maybe.just&lt;<span class="built_in">number</span>&gt;(<span class="number">13</span>);</span><br><span class="line"><span class="keyword">const</span> assertEqual = <span class="function">(<span class="params">x: Maybe&lt;<span class="built_in">number</span>&gt;, y: Maybe&lt;<span class="built_in">number</span>&gt;</span>) =&gt;</span> x.value === y.value;</span><br><span class="line"></span><br><span class="line"><span class="comment">// first law</span></span><br><span class="line">assertEqual(unit(<span class="number">5</span>).bind(f), f(<span class="number">5</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// second law</span></span><br><span class="line">assertEqual(ma.bind(unit), ma);</span><br><span class="line"></span><br><span class="line"><span class="comment">// third law</span></span><br><span class="line">assertEqual(ma.bind(f).bind(g), ma.bind(<span class="function"><span class="params">value</span> =&gt;</span> f(value).bind(g)));</span><br></pre></td></tr></table></figure><p>事实证明 Maybe 确实是一个 Monad。</p><h2 id="Promise-Monad"><a href="#Promise-Monad" class="headerlink" title="Promise Monad"></a>Promise Monad</h2><p>JavaScript 中的 Promise 也是一个 Monad：</p><ul><li>类型构造器就是<code>Promise</code></li><li><code>unit</code>为<code>x =&gt; Promise.resolve(x)</code></li><li><code>bind</code>为<code>Promise.prototype.then</code></li></ul><p>Promise 同样满足 Monad 三定律，详见 StackOverflow 的这一<a href="https://stackoverflow.com/a/48553568/1131882">回答</a>。</p><p>与 Maybe 可以表达数据有无意义两种状态不同，Promise 作为一种延续型 Monad，可以用来封装异步操作：</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">fetch(<span class="string">&#x27;http://example.com/movies.json&#x27;</span>)</span><br><span class="line">  .then(<span class="function"><span class="params">response</span> =&gt;</span> response.json())</span><br><span class="line">  .then(<span class="function"><span class="params">movies</span> =&gt;</span> fetch(<span class="string">`http://example.com/author/<span class="subst">$&#123;movies[<span class="number">0</span>].author&#125;</span>`</span>))</span><br><span class="line">  .then(<span class="function"><span class="params">response</span> =&gt;</span> response.json())</span><br><span class="line">  .then(<span class="function"><span class="params">author</span> =&gt;</span> <span class="built_in">console</span>.log(author));</span><br></pre></td></tr></table></figure><p>Promise 以及在它基础上的 async/await 帮助我们避免了回调地狱以及复杂的错误处理，以一种非常优雅的方式处理各种异步操作。说到根上，这正是 Monad 的功劳。</p><p>延续型 Monad 还有另外一个<a href="https://gist.github.com/dypsilon/6b242998ba3474fc239255d42b28dd02">例子</a>，感兴趣的同学可以移步。</p><h2 id="IO-Monad"><a href="#IO-Monad" class="headerlink" title="IO Monad"></a>IO Monad</h2><p>我们在前端编程中有时候遇到这样一个挑战：一些有副作用的 DOM 操作需要能够组合起来，从而灵活解决各种实际问题。一个常见的解决办法就是编写各种工具函数，然后搭配调用。</p><p>下面介绍下另外一种思路，IO Monad。我们假设现在有下面几个工具类方法：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> $ = <span class="function">(<span class="params">id: <span class="built_in">string</span></span>) =&gt;</span> &lt;HTMLElement&gt;<span class="built_in">document</span>.querySelector(<span class="string">`#<span class="subst">$&#123;id&#125;</span>`</span>);</span><br><span class="line"><span class="keyword">const</span> read = <span class="function">(<span class="params">id: <span class="built_in">string</span></span>) =&gt;</span> $(id).value;</span><br><span class="line"><span class="keyword">const</span> write = <span class="function">(<span class="params">id: <span class="built_in">string</span></span>) =&gt;</span> <span class="function">(<span class="params">text: <span class="built_in">string</span></span>) =&gt;</span> $(id).textContent = text;</span><br></pre></td></tr></table></figure><p>上述<code>read、write</code>方法因为涉及到 DOM 操作，都有副作用。假设现在需要实现一个新的方法，将一个 Input 表单项的值转化为大写并显示到另外一个展示区域，传统的实现方法是：</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="function"><span class="keyword">function</span> <span class="title">syncInputToOutput</span>(<span class="params">idInput: <span class="built_in">string</span>, idOutput: <span class="built_in">string</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> inputValue = read(idInput);</span><br><span class="line">  <span class="keyword">const</span> outputValue = inputValue.toUpperCase();</span><br><span class="line">  write(idOutput, outputValue);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>基于 IO Monad 的实现则是另外一番景象：</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">const</span> read = <span class="function">(<span class="params">id: <span class="built_in">string</span></span>) =&gt;</span> <span class="keyword">new</span> IO&lt;<span class="built_in">string</span>&gt;(<span class="function">() =&gt;</span> $(id).value);</span><br><span class="line"><span class="keyword">const</span> write = <span class="function">(<span class="params">id: <span class="built_in">string</span></span>) =&gt;</span> <span class="function">(<span class="params">text: <span class="built_in">string</span></span>) =&gt;</span> <span class="keyword">new</span> IO&lt;<span class="built_in">string</span>&gt;(<span class="function">() =&gt;</span> $(id).textContent = text);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">syncInputToOutput</span>(<span class="params">idInput: <span class="built_in">string</span>, idOutput: <span class="built_in">string</span></span>) </span>&#123;</span><br><span class="line">  read(idInput)</span><br><span class="line">    .bind(<span class="function">(<span class="params">value: <span class="built_in">string</span></span>) =&gt;</span> <span class="keyword">new</span> IO&lt;<span class="built_in">string</span>&gt;(<span class="function">() =&gt;</span> value.toUpperCase()))</span><br><span class="line">    .bind(write(idOutput)))</span><br><span class="line">    .run();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码变得更加声明式。如果直接返回 IO 实例但是不执行<code>run</code>方法，所有的副作用并不会真正发生，而且还可以继续组合其它的转换函数，函数式编程的风格非常明显。</p><p>IO Monad 的实现如下：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> <span class="title">IO</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> effectFn: <span class="function">() =&gt;</span> T;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="title">constructor</span>(<span class="params">effectFn: () =&gt; T</span>)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.effectFn = effectFn;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  bind&lt;U&gt;(transform: <span class="function">(<span class="params">value: T</span>) =&gt;</span> IO&lt;U&gt;) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> IO&lt;U&gt;(<span class="function">() =&gt;</span> transform(<span class="built_in">this</span>.effectFn()).run());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  run(): T &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.effectFn();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="更多-Monad"><a href="#更多-Monad" class="headerlink" title="更多 Monad"></a>更多 Monad</h2><p>常见的 Monad 还有 Either、List 等，具体可以参考 <a href="https://github.com/monet/monet.js#contents">Monet.js</a> 文档。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>Monad 是一个非常抽象的概念，但其也是为了解决现实问题而发明，并非空中楼阁，仅供装 X 或者学术研究所用。本文从具体问题着手，深入浅出的介绍了 Monad 的现实意义、概念定义，并在最后给出了前端中的一些具体应用。希望可以帮助和从前的我一样有着同样困惑的朋友。</p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>Haskell 为了方便 Monad 运算，还提供了 do 符号语法糖，避免了使用<code>bind</code>+ lambda function 的复杂度：</p><figure class="highlight haskell"><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="title">eval</span> :: <span class="type">Expr</span> -&gt; <span class="type">Maybe</span> <span class="type">Int</span></span><br><span class="line"><span class="title">eval</span> (<span class="type">Val</span> n) = return n</span><br><span class="line"><span class="title">eval</span> (<span class="type">Div</span> x y) = <span class="keyword">do</span> m &lt;- eval x</span><br><span class="line">                    n &lt;- eval y</span><br><span class="line">                    safediv m n</span><br></pre></td></tr></table></figure><p>当真是易读了不少。那么 JavaScript 也可以做到吗？</p><p>答案是肯定的，借助 generator 我们可以实现一个如下的<code>doM</code>（寓意 do for Monad）函数：</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="function"><span class="keyword">function</span> <span class="title">doM</span>&lt;<span class="title">T</span>&gt;(<span class="params">generator: Iterator&lt;Maybe&lt;T&gt;&gt;</span>) </span>&#123;</span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">step</span>(<span class="params">value?: <span class="built_in">any</span></span>): <span class="title">Maybe</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> result = generator.next(value);</span><br><span class="line">    <span class="keyword">if</span> (result.done) &#123;</span><br><span class="line">      <span class="keyword">return</span> result.value;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result.value.bind(step);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> step();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终版的<code>evaluate</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="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">e: Expr</span>): <span class="title">Maybe</span>&lt;<span class="title">number</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (e.type === <span class="string">&#x27;value&#x27;</span>) <span class="keyword">return</span> Maybe.just(&lt;<span class="built_in">number</span>&gt;e.value);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> doM(<span class="function"><span class="keyword">function</span>* (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> left = <span class="keyword">yield</span> evaluate((&lt;DivisionExpr&gt;e.value).left);</span><br><span class="line">    <span class="keyword">const</span> right = <span class="keyword">yield</span> evaluate((&lt;DivisionExpr&gt;e.value).right);</span><br><span class="line">    <span class="keyword">return</span> safeDiv(left, right);</span><br><span class="line">  &#125;());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>本文中所有代码均可到 <a href="https://github.com/springuper/js-monad">GitHub</a> 查看。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://en.wikipedia.org/wiki/Monad_(functional_programming)#History">Monad (functional programming) - Wikipedia</a></li><li><a href="https://medium.com/@ahlechandre/lambda-calculus-with-javascript-897f7e81f259">Lambda Calculus with JavaScript</a></li><li><a href="https://curiosity-driven.org/monads-in-javascript">Monads in JavaScript</a></li><li><a href="https://medium.com/javascript-scene/javascript-monads-made-simple-7856be57bfe8">JavaScript Monads Made Simple</a></li><li><a href="https://www.youtube.com/watch?v=b0EF0VTs9Dc">Monads and Gonads</a></li><li><a href="http://www.haskellforall.com/2012/12/the-continuation-monad.html">The Continuation Monad</a></li><li><a href="https://github.com/monet/monet.js/blob/develop/docs/IO.md">monet.js - IO</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;Monad 这一概念起源于上世纪五十年代末，由数学家 Roger Godement 提出并应用在范畴论中。直到上世纪九十年代，计算机科学家 Eugenio Moggi 首次将其引入到函数式编程中。由于 Monad 具备优秀的抽象和通用能力，从 Haskell 开始，众多编程语言都相继引入了这一概念，函数式编程迎来一次质的飞跃。&lt;/p&gt;
&lt;p&gt;JavaScript 具备函数式编程的基本要素—— λ 算子，即一等公民的函数及其附属的一些性质。Monad 在 JavaScript 中也有不少应用，其中最知名的就是备受赞誉的 Promise。&lt;/p&gt;
&lt;p&gt;然而，江湖上有句传言：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Once you understand monads, you immediately become incapable of explaining them to anyone else” by Gilad Bracha&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;笔者自认为虽有些愚钝，还算是勤奋，但读过诸多介绍 Monad 的文章书籍后都不得要领，浑浑噩噩。直到最近看了 Programming in Haskell 作者 Graham Hutton 教授的 &lt;a href=&quot;https://www.youtube.com/watch?v=t1e8gqXLbsU&quot;&gt;What is a Monad? - Computerphile&lt;/a&gt; 后，才算是焕然大悟。&lt;/p&gt;
&lt;p&gt;本文尝试从一个实例出发，讲解遇到的问题以及 Monad 解决这些问题的思路，希望可以帮助大家领会其要义。为了清晰地标注类型，本文的代码均使用 TypeScript。&lt;/p&gt;
&lt;h2 id=&quot;举个栗子&quot;&gt;&lt;a href=&quot;#举个栗子&quot; class=&quot;headerlink&quot; title=&quot;举个栗子&quot;&gt;&lt;/a&gt;举个栗子&lt;/h2&gt;&lt;p&gt;假设我们要处理这样一个问题：&lt;/p&gt;
&lt;p&gt;实现一个求值函数&lt;code&gt;evaluate&lt;/code&gt;，它可以接收类似 &lt;img src=&quot;../images/monad-01.jpg&quot; alt=&quot;8 \div 2&quot;&gt; 、 &lt;img src=&quot;../images/monad-02.jpg&quot; alt=&quot;8 \div (12 \div 3)&quot;&gt; 的只有除法的表达式，并返回最终的结果。&lt;/p&gt;
&lt;p&gt;简便起见，我们假设已经将输入的除法表达式（字符串）进行解析，并得到类似下面这样的抽象语法树：&lt;/p&gt;
&lt;figure class=&quot;highlight ts&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;19&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;20&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;21&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;22&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;23&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;24&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;25&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;26&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;27&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;28&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;29&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;30&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;31&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;32&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;33&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;interface&lt;/span&gt; Expr &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;value&amp;#x27;&lt;/span&gt; | &lt;span class=&quot;string&quot;&gt;&amp;#x27;division&amp;#x27;&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  value: &lt;span class=&quot;built_in&quot;&gt;number&lt;/span&gt; | DivisionExpr;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;interface&lt;/span&gt; DivisionExpr &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;left&lt;/span&gt;: Expr;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  right: Expr;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// AST representation for expression &amp;#x27;8 / (12 / 3)&amp;#x27;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; expr: Expr = &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;division&amp;#x27;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;value&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;left&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &lt;span class=&quot;attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;value&amp;#x27;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &lt;span class=&quot;attr&quot;&gt;value&lt;/span&gt;: &lt;span class=&quot;number&quot;&gt;8&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;right&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &lt;span class=&quot;attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;division&amp;#x27;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &lt;span class=&quot;attr&quot;&gt;value&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;attr&quot;&gt;left&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;          &lt;span class=&quot;attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;value&amp;#x27;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;          &lt;span class=&quot;attr&quot;&gt;value&lt;/span&gt;: &lt;span class=&quot;number&quot;&gt;12&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;attr&quot;&gt;right&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;          &lt;span class=&quot;attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;value&amp;#x27;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;          &lt;span class=&quot;attr&quot;&gt;value&lt;/span&gt;: &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &amp;#125;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="Functional Programming" scheme="https://springuper.github.io/tags/Functional-Programming/"/>
    
    <category term="Monad" scheme="https://springuper.github.io/tags/Monad/"/>
    
  </entry>
  
  <entry>
    <title>JS Linter 进化史</title>
    <link href="https://springuper.github.io/js-linter-history/"/>
    <id>https://springuper.github.io/js-linter-history/</id>
    <published>2018-05-29T19:52:00.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<p>据说在 C 语言刚刚开始起步的时候，有若干常见的代码问题不能被编译器捕获，因此有人开发了一款名为 <a href="https://en.wikipedia.org/wiki/Lint_(software)">Lint</a> 的附加程序，用于扫描源代码以捕获这些问题。因此后续类似的工具都叫 Lint 或 Linter。</p><p>JavaScript 最初被仓促设计出来解决一些网页中的“小”问题，不过蒙幸运女神的眷顾，逐步发发展壮大，成为当前风头最劲的编程语言之一。在这一过程中，Linter 工具应运而生，主要解决如下几个问题：</p><ul><li>避免最初设计中存在的不少糟粕，提倡更多地使用 Good Parts</li><li>发现一些 JS 代码中存在的问题，例如忘记声明而导致的全局变量</li><li>保证代码风格统一，便于大型项目维护</li></ul><p>本文主要介绍下 JS Linter 进化史中的三个里程碑式的工具：JSLint、JSHint 和 ESLint。</p><h2 id="开山鼻祖-JSLint"><a href="#开山鼻祖-JSLint" class="headerlink" title="开山鼻祖 JSLint"></a>开山鼻祖 JSLint</h2><p>JSLint 是公认的第一个 JS Linter，由赫赫有名的老道－Douglas Crockford 在 2002 发布。彼时的前端还处于萌芽状态，大部分人还是用 JS 来写一些非常粗糙的页面效果、表单验证。JQuery 也要在四年之后才正式发布。</p><p>JSLint 的核心是 <a href="http://crockford.com/javascript/tdop/tdop.html">Top Down Operator Precedence</a>（自顶向下的运算符优先级）技术实现的 Pratt 解析器。由于涉及到比较底层的词法解析、语法解析等编译原理，本文中不再赘述，不过推荐感兴趣的同学去看下原文，或者看“代码之美”一书中的第九章中文版。简单来说，就是用 JS 实现了一个处理 JS 源码的解析器，可以识别出其中的各种表达式、语句、函数等组成要素。</p><p>Pratt 解析器的处理结果本质上类似于 AST（抽象语法树），但并没有做到那么极致，毕竟那时候还是 2002 年啊，用 JS 实现一个 JS 解析器本身已经是一件非常了不得的成就。有了这样强大的解析器支持，事实上基本可以做任何事，例如代码压缩、代码检查、代码高亮等。</p><p>JSLint 即是借助它的强大能力，在其内部的一些处理过程中恰当地进行改造，加入了一些规则的检查，从而实现了 Lint 的功能。</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="keyword">var</span> varstatement = <span class="function"><span class="keyword">function</span> <span class="title">varstatement</span>(<span class="params">prefix</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> id, name, value;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (funct[<span class="string">&#x27;(onevar)&#x27;</span>] &amp;&amp; option.onevar) &#123;</span><br><span class="line">        warning(<span class="string">&quot;Too many var statements.&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (!funct[<span class="string">&#x27;(global)&#x27;</span>]) &#123;</span><br><span class="line">        funct[<span class="string">&#x27;(onevar)&#x27;</span>] = <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><span id="more"></span><p>如果设定了 <code>onevar</code> 规则，即同一作用域下只能用一个 <code>var</code> 声明所有变量，那么在每个包含 <code>var</code> 的语句解析时都会进行该规则的检查，如果发现之前同一作用域已经有 <code>var</code> 语句，则报告一个 “Too many var statements.” 的 warnning。</p><p>毫无疑问，JSLint 的出现，以及接下来蝴蝶书的问世，让广大前端工程师在如果用好 JS 的 Good Parts 方面受益匪浅。</p><h2 id="继往开来-JSHint"><a href="#继往开来-JSHint" class="headerlink" title="继往开来 JSHint"></a>继往开来 JSHint</h2><p>金无足赤，人无完人。在随后的前端跨越式发展中，JSLint 的一些不足暴露了出来。2011 年 12 月 20 日，Anton Kovalyov 发表了一篇标志性的文章：<a href="https://web.archive.org/web/20110224022052/http://anton.kovalyov.net/2011/02/20/why-i-forked-jslint-to-jshint/">Why I forked JSLint to JSHint</a>，指出了 JSLint 存在的几个主要问题：</p><ol><li>令人不安地固执己见，没有提供一些规则的配置 Anton 措辞激烈：“It is quickly transforming from a tool that helps developers to prevent bugs to a tool that makes sure you write your code like Douglas Crockford.”</li><li>对社区反馈不关注 这一点从 JSLint 的 Issues、Pull Requests 中可以看出，另外值得注意的是 <a href="https://github.com/douglascrockford/JSLint/graphs/contributors">Contributors</a> 中只有两名开发者，而且第二名开发者只有三行代码的修改（注：可能跟 Douglas 的习惯有关，有些 PR 是通过的，但是并没有合并，而是由他自己提交了新的 commit）</li></ol><p>JSHint 就是在这样的背景下诞生了：</p><blockquote><p>And, as we were saying from the day zero, it will always be a <strong>community-driven</strong> tool. Simply because a community of programmers working together is better than a single person working alone. No matter who this person is.</p></blockquote><p>它的核心定位是<strong>社区驱动</strong>的代码质量工具，因为 Anton 坚信程序员组成的社区一起协作要好于单打独斗。</p><p>在核心实现仍然基于 Pratt 解析器的基础上，借助社区的共同努力，JSHint 进行了多方面的改进：</p><ol><li>更多可配置的规则 这是社区的核心诉求。除了开放规则的 Pull Request 外，JSHint 在规则文档、预定义环境方面也做了诸多努力</li><li>代码模块化 JSLint 源文件只有一个 5000 余行的大文件，对于一般开发者来说难以维护。JSHint 按照功能划分为 lex、scope、core、reporter 等多个模块，功能更聚焦</li><li>CLI 命令行工具的支持，为很多第三方工具提供了基础，JSHint 可以很好地和各种 IDE 集成</li><li>测试 JSLint 一行测试代码也没有，JSHint 的整体代码测试覆盖率达到了 99%</li></ol><p>下图是 Google Trends 中 JSHint 和 JSLint 关键字搜索次数的趋势图：</p><p><em>JSLint VS JSHint - Google Trends</em></p><p>可以看出，在 JSHint 快速增长的同时，JSLint 基本处于停滞或者下降状态。</p><p>回过头来看，JSHint 的出现几乎是必然的，前端开发者日益增长的代码质量诉求和落后偏执的工具之间出现了不可调和的矛盾。JSHint 选择了一条正确的道路，众志成城，其利断金。现在 JSHint 已经有多达 236 个 Contributors，上千个 Pull Requests。</p><h2 id="重新出发-ESLint"><a href="#重新出发-ESLint" class="headerlink" title="重新出发 ESLint"></a>重新出发 ESLint</h2><p>JSHint 在第三版的计划中有这样一条：</p><blockquote><p>Build a foundation for plugins by exposing AST and adding additional hooks.</p></blockquote><p>前端爆发式增长带来的巨大需求让 JSHint 变得愈加难以应对，通过暴露 AST 信息来支持第三方插件无疑是一剂良方。然而理想总是美好的，现实却非常残酷。这一计划直到现在仍然没有实现。究其原因，在 JSLint 时代就存在的规则检查和 Pratt 解析器深度耦合首当其冲，可谓是成也萧何败萧何。</p><p>2013 年，前端界另外一名标杆性人物 Zakas 在业务开发中遇到一个 <code>XMLHttpRequest</code> 在 IE7 中的问题，他希望使用 JSHint 来避免类似问题。Zakas 找到 Anton，了解到插件的提案被搁置并且短期内看起来没有希望去实施。Zakas 很失望，但牛人毕竟是牛人，他开始着手寻找其他解决方案。</p><p>Zakas 首先在公司使用的一个 PHP Linter 上得到了一些启示，因为那个 Linter 使用了 AST 作为中间表示层，所有的规则都基于 AST 做进一步分析。他开始调研 JS 是否有类似工具，Ariya 开发的 Esprima 进入了他的视线。Zakas 盛情邀请 Ariya 到他的公司 Box 做一次不限主题的分享，而 Ariya 选择的主题恰恰是 Esprima。分享过程中，Ariya 介绍到了 AST 表示层的意义以及目前基于它的很多工具，例如遍历 AST 的工具 Estraverse 以及作用域分析工具 Escope。这一切让 Zakas 受益匪浅，下一代 JS Linter 工具的方案在他脑海中渐渐清晰起来。</p><p>很快，在 2013 年六月份的一个周末，Zakas 完成了第一个<a href="https://github.com/eslint/eslint/commit/a658d7b0">可执行的版本</a>。借助于 Esprima 等工具，ESLint 的核心代码只有 100 行：</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">api.verify = <span class="function"><span class="keyword">function</span>(<span class="params">text, config</span>) </span>&#123;</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="built_in">Object</span>.keys(config.rules).forEach(<span class="function"><span class="keyword">function</span>(<span class="params">key</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> rule = rules.get(key);</span><br><span class="line">        <span class="keyword">if</span> (rule) &#123;</span><br><span class="line">            rule(<span class="keyword">new</span> RuleContext(key, api));</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">&quot;Definition for rule &#x27;&quot;</span> + key + <span class="string">&quot;&#x27; was not found.&quot;</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">// 解析代码，生成 AST</span></span><br><span class="line">    <span class="keyword">var</span> ast = esprima.parse(text, &#123; <span class="attr">loc</span>: <span class="literal">true</span>, <span class="attr">range</span>: <span class="literal">true</span> &#125;),</span><br><span class="line">        walk = astw(ast);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 核心逻辑：遍历 AST 各个节点</span></span><br><span class="line">    walk(<span class="function"><span class="keyword">function</span>(<span class="params">node</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 将当前节点通过事件进行分发，监听这些类型节点的规则会执行自己的检查逻辑</span></span><br><span class="line">        api.emit(node.type, node);</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> messages;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>规则的实现完全和 Linter 核心逻辑分离，以全等（eqeqeq）规则为例：</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">// eqeqeq rule</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="keyword">function</span>(<span class="params">context</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 监听遍历 AST 中触发的 BinaryExpression 事件</span></span><br><span class="line">    context.on(<span class="string">&quot;BinaryExpression&quot;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">node</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 对这一类型节点的操作符进行检查</span></span><br><span class="line">        <span class="keyword">var</span> operator = node.operator;</span><br><span class="line">        <span class="keyword">if</span> (operator === <span class="string">&quot;==&quot;</span>) &#123;</span><br><span class="line">            context.report(node, <span class="string">&quot;Unexpected use of ==, use === instead.&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (operator === <span class="string">&quot;!=&quot;</span>) &#123;</span><br><span class="line">            context.report(node, <span class="string">&quot;Unexpected use of !=, use !== instead.&quot;</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>这种架构带来了很多好处：</p><ol><li><strong>关注点分离</strong> 解析器专注于源码的词法解析、语法解析，并生成符合 <a href="https://github.com/estree/estree">ESTree</a> 规范的 AST。ESLint 核心部分专注于配置生成、规则管理、上下文维护、遍历 AST、报告产出等主流程。ESLint 的规则、报告部分则通过约定接口的形式独立出来，方便自定义扩展。这种关注点分离的架构使得 ESLint 具备了很好的可维护性和扩展性。举了例子，为了支持一些 ES6 的新特性和 JSX，ESLint 团队在 2014 年开发了 <a href="https://eslint.org/blog/2014/12/espree-esprima">Espree</a> 替代 Esprima，得益于这一良好架构，ESLint 只需要改一下依赖即可，开着飞机修飞机不再是啥难事儿</li><li><strong>支持自定义规则</strong> 这是 ESLint 的核心诉求。通过将代码解析与代码检查逻辑分开，自定义规则可以注册相关事件关注到自己需要检查的代码片段，并将检查结果添加到报告中。一个典型的例子是 eslint-plugin-react 中的 <a href="https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/no-find-dom-node.js#L34">no-find-dom-node</a> 规则，通过自己编写这个规则，可以禁止所有 React 代码中的 <code>findDomNode</code> 调用，Code is law，而且 Code 可以 Contribute，这是多么激动人心的一件事情啊！迄今为止，已经有超过 1000 个 ESLint 的规则插件，比较知名的有 eslint-plugin-react，eslint-plugin-jsx-a11y，eslint-plugin-vue 等</li><li><strong>保持内核的简单</strong> 众多功能通过插件的形式予以支持，内核不与特定的框架、库、JS 方言相关。目前 ESLint 支持自定义的解析器、规则插件、预编译插件、结果报告，它更像是一个平台，对核心的流程设定约束，并开放各方面的能力，从而适应复杂多变的实际场景</li></ol><p>下图是 NPM Trends 中 ESLint，JSHint 和 JSLint 关键字搜索次数的趋势图：<img src="../images/js-linter-history-02.jpg"><em>ESLint VS JSHint VS JSLint - NPM Trends</em></p><p>Zakas 后来专门写了<a href="https://www.nczonline.net/blog/2016/02/reflections-on-eslints-success/">一篇文章</a>来反思 ESLint 的成功，其中主要几条有：</p><ul><li>人们编写更多的 JS 代码 这是一个大环境，有需求才有供给，正是前端的蓬勃发展带动了前端框架、库、工具、规范的繁荣</li><li>ES6/Babel ES6 借助于 Babel 等工具，「旧时王谢堂前燕，飞入寻常百姓家」，然而 JSHint 囿于架构问题，难以灵活调整并适应这种变化，ESLint 则站在 Esprima 等工具的肩膀上，对 ES6 甚至更新的一些规范都有良好的支持，发展迅速</li><li>React React 横空出世时的 JSX 着实让大家吃惊了一把，JS 还可以这样写。随之而来的一个问题是，Linter 怎么对 JSX 进行解析和检查呢？得益于良好的架构，ESLint 基于 Esprima 开发了 Espree 支持了 JSX 的解析，再加上针对 JSX 开发的自定义规则，以一种优雅的方式实现了对 JSX 的支持。15 年 2 月份 ESLint 的下载量是 89,000，这个功能发布后，三月份的下载量暴涨了 80%</li><li>可扩展性是关键 做好内核，开放能力，这种深入问题本质而又极度开放包容的思路是 ESLint 成功的关键</li><li>倾听社区 JSLint 的弊端 Zakas 怎能不知。在 ESLint 项目中，很多功能其实并不是他一个人拍脑袋想出来的，而是从社区的交流中慢慢摸索而来。例如可共享配置、JSX、自定义解析器等等。社区代表了用户，他们是最熟悉实际需求的人</li><li>专注于实际使用价值而不是竞争对手 从 ESLint 项目创立之初起，Zakas 就从没有说过 ESLint 比 JSHint 更好。他一直在说的是 ESLint 可以让你写自己的检查规则，而不是贬低其它工具。他很尊重那些继续使用 JSLint、JSHint 或者 JSCS 的开发者。当然，ESLint 的整个团队仍然在继续让它变得更好用。这样一种开放包容的心态，也让 ESLint 和其它 Linter 工具保持了良好的互动，有时候也会「商业互吹」一把</li></ul><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>通过回顾这十几年 JS Linter 工具进化史，一方面情不自禁地为这些核心贡献者的付出和他们的聪明才智感到钦佩、敬仰，他们缔造了这段传奇。另一方面，于兴衰之间，我们可以窥见一些成功的信条：顺势而为，关注本质，开放能力，发动群众搞建设才是王道。</p><p>期望本文能让你在了解这段还在继续的历史的同时，投身于前端开发的洪流中，做出自己的一份贡献。</p><h2 id="不是彩蛋"><a href="#不是彩蛋" class="headerlink" title="不是彩蛋"></a>不是彩蛋</h2><p>Zakas 病了，而且很严重。2016 年 4 月 12 号离开最后一家公司 Box 后，就一直在家休养。他目前还在跟莱姆病（Lyme disease）做斗争。希望他可以早日好起来！有能力翻墙的小伙伴可以到他的 <a href="https://twitter.com/slicknet">Twitter</a> 或者 <a href="https://medium.com/@slicknet">Medium</a> 加油鼓励。</p><p>想要了解细节的话，可以读下他的这篇文章 <a href="https://medium.com/lyme-disease-warrior/when-your-health-costs-you-your-job-1f49180e7dd4">When your health costs you your job</a>。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ol><li><a href="https://en.wikipedia.org/wiki/JSLint">JSLint - WikiPedia</a></li><li><a href="https://www.sitepoint.com/comparison-javascript-linting-tools/">A Comparison of JavaScript Linting Tools</a></li><li><a href="https://www.nczonline.net/blog/2018/02/the-inception-of-eslint/">The inception of ESLint</a></li><li><a href="https://www.nczonline.net/blog/2016/02/reflections-on-eslints-success/">Reflections on ESLint’s success</a></li><li><a href="https://speakerdeck.com/ariya/javascript-code-analysis">JavaScript Code Analysis</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;据说在 C 语言刚刚开始起步的时候，有若干常见的代码问题不能被编译器捕获，因此有人开发了一款名为 &lt;a href=&quot;https://en.wikipedia.org/wiki/Lint_(software)&quot;&gt;Lint&lt;/a&gt; 的附加程序，用于扫描源代码以捕获这些问题。因此后续类似的工具都叫 Lint 或 Linter。&lt;/p&gt;
&lt;p&gt;JavaScript 最初被仓促设计出来解决一些网页中的“小”问题，不过蒙幸运女神的眷顾，逐步发发展壮大，成为当前风头最劲的编程语言之一。在这一过程中，Linter 工具应运而生，主要解决如下几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免最初设计中存在的不少糟粕，提倡更多地使用 Good Parts&lt;/li&gt;
&lt;li&gt;发现一些 JS 代码中存在的问题，例如忘记声明而导致的全局变量&lt;/li&gt;
&lt;li&gt;保证代码风格统一，便于大型项目维护&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文主要介绍下 JS Linter 进化史中的三个里程碑式的工具：JSLint、JSHint 和 ESLint。&lt;/p&gt;
&lt;h2 id=&quot;开山鼻祖-JSLint&quot;&gt;&lt;a href=&quot;#开山鼻祖-JSLint&quot; class=&quot;headerlink&quot; title=&quot;开山鼻祖 JSLint&quot;&gt;&lt;/a&gt;开山鼻祖 JSLint&lt;/h2&gt;&lt;p&gt;JSLint 是公认的第一个 JS Linter，由赫赫有名的老道－Douglas Crockford 在 2002 发布。彼时的前端还处于萌芽状态，大部分人还是用 JS 来写一些非常粗糙的页面效果、表单验证。JQuery 也要在四年之后才正式发布。&lt;/p&gt;
&lt;p&gt;JSLint 的核心是 &lt;a href=&quot;http://crockford.com/javascript/tdop/tdop.html&quot;&gt;Top Down Operator Precedence&lt;/a&gt;（自顶向下的运算符优先级）技术实现的 Pratt 解析器。由于涉及到比较底层的词法解析、语法解析等编译原理，本文中不再赘述，不过推荐感兴趣的同学去看下原文，或者看“代码之美”一书中的第九章中文版。简单来说，就是用 JS 实现了一个处理 JS 源码的解析器，可以识别出其中的各种表达式、语句、函数等组成要素。&lt;/p&gt;
&lt;p&gt;Pratt 解析器的处理结果本质上类似于 AST（抽象语法树），但并没有做到那么极致，毕竟那时候还是 2002 年啊，用 JS 实现一个 JS 解析器本身已经是一件非常了不得的成就。有了这样强大的解析器支持，事实上基本可以做任何事，例如代码压缩、代码检查、代码高亮等。&lt;/p&gt;
&lt;p&gt;JSLint 即是借助它的强大能力，在其内部的一些处理过程中恰当地进行改造，加入了一些规则的检查，从而实现了 Lint 的功能。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; varstatement = &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;varstatement&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;prefix&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; id, name, value;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (funct[&lt;span class=&quot;string&quot;&gt;&amp;#x27;(onevar)&amp;#x27;&lt;/span&gt;] &amp;amp;&amp;amp; option.onevar) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        warning(&lt;span class=&quot;string&quot;&gt;&amp;quot;Too many var statements.&amp;quot;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125; &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (!funct[&lt;span class=&quot;string&quot;&gt;&amp;#x27;(global)&amp;#x27;&lt;/span&gt;]) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        funct[&lt;span class=&quot;string&quot;&gt;&amp;#x27;(onevar)&amp;#x27;&lt;/span&gt;] = &lt;span class=&quot;literal&quot;&gt;true&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    ...&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="Linter" scheme="https://springuper.github.io/tags/Linter/"/>
    
    <category term="ESLint" scheme="https://springuper.github.io/tags/ESLint/"/>
    
    <category term="JSLint" scheme="https://springuper.github.io/tags/JSLint/"/>
    
  </entry>
  
  <entry>
    <title>React 设计中的闪光点</title>
    <link href="https://springuper.github.io/react-design-highlights/"/>
    <id>https://springuper.github.io/react-design-highlights/</id>
    <published>2018-01-31T21:04:00.000Z</published>
    <updated>2026-10-03T09:20:43.753Z</updated>
    
    <content type="html"><![CDATA[<p>React 自从 2013 年开源以来，凭借先进的理念、简洁的 API 和优秀的性能迅猛发展起来。如今已经构建起一个完整庞大的生态圈。从 NPM Trend 的统计结果来看，React 显著领先于其它前端框架：<img src="../images/react-design-highlights-01.jpg"><em>近两年 NPM 下载量对比情况</em></p><p>为什么 React 如此流行？它有哪些精妙之处值得我们深究？本文着重从 React 的核心设计理念方面进行探索。</p><h2 id="单向数据流"><a href="#单向数据流" class="headerlink" title="单向数据流"></a>单向数据流</h2><p>在 React 问世之前，已经有很多框架为日趋复杂的前端开发提供了解决方案。那么为什么还要创造 React 呢？从 React 早期的灵魂人物 Pete Hunt 的 <a href="https://reactjs.org/blog/2013/06/05/why-react.html">Why did we build React?</a> 一文和 <a href="https://www.youtube.com/watch?v=-DX3vJiqxm4">The Secrets of React‘s Virtual DOM</a> 演讲中，我们似乎可以得到一些启示。</p><p><strong>Data Binding</strong></p><p>为了解决 UI 和数据同步复杂性的问题，Data Binding 被提出并得到了广泛应用。其中两个主要的流派是：</p><ul><li>Key Value Observable (KVO)，以 Ember、Meteor 和 Knockout 为代表</li><li>Dirty Check，主要以 AngularJS 为代表</li></ul><p>让我们尝试用 TodoMVC 为例来看下各个方案的不同。</p><p>下面是 Ember TodoMVC 的核心代码：</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="comment">// Template</span></span><br><span class="line">&lt;script type=<span class="string">&quot;text/x-handlebars&quot;</span> data-template-name=<span class="string">&quot;todo-list&quot;</span>&gt;</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">ul</span> <span class="attr">id</span>=<span class="string">&quot;todo-list&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">    &#123;&#123;#each&#125;&#125;</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">li</span> &#123;&#123;<span class="attr">bind-attr</span> <span class="attr">class</span>=<span class="string">&quot;isCompleted:completed isEditing:editing&quot;</span>&#125;&#125;&gt;</span></span></span><br><span class="line"><span class="xml">        &#123;&#123;input type=&quot;checkbox&quot; class=&quot;toggle&quot; checked=isCompleted&#125;&#125;</span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">label</span> &#123;&#123;<span class="attr">action</span> &quot;<span class="attr">editTodo</span>&quot; <span class="attr">on</span>=<span class="string">&quot;doubleClick&quot;</span>&#125;&#125;&gt;</span>&#123;&#123;title&#125;&#125;<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">button</span> &#123;&#123;<span class="attr">action</span> &quot;<span class="attr">removeTodo</span>&quot;&#125;&#125; <span class="attr">class</span>=<span class="string">&quot;destroy&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="xml">    &#123;&#123;/each&#125;&#125;</span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Todos List Controller</span></span><br><span class="line">Todos.TodosListController = Ember.ArrayController.extend(&#123;</span><br><span class="line">  <span class="attr">needs</span>: [<span class="string">&#x27;todos&#x27;</span>],</span><br><span class="line">  <span class="attr">allTodos</span>: Ember.computed.alias(<span class="string">&#x27;controllers.todos&#x27;</span>),</span><br><span class="line">  <span class="attr">itemController</span>: <span class="string">&#x27;todo&#x27;</span>,</span><br><span class="line">  <span class="attr">canToggle</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> anyTodos = <span class="built_in">this</span>.get(<span class="string">&#x27;allTodos.length&#x27;</span>);</span><br><span class="line">    <span class="keyword">var</span> isEditing = <span class="built_in">this</span>.isAny(<span class="string">&#x27;isEditing&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> anyTodos &amp;&amp; !isEditing;</span><br><span class="line">  &#125;.property(<span class="string">&#x27;allTodos.length&#x27;</span>, <span class="string">&#x27;@each.isEditing&#x27;</span>)</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><span id="more"></span><p><em>注：完整可执行代码见 <a href="https://github.com/tastejs/todomvc/tree/1.3.0/architecture-examples/emberjs">TodoMVC 1.3.0 - EmberJS</a>。</em></p><p>这种实现存在以下几个问题：</p><ul><li>Computed properties，即那些依赖其它 property 的 property，不但需要手动指定依赖，而且往往需要了解 Ember 的内部机制，例如 <code>@each.isEditing</code></li><li>模板和 View 分离，这样的确能够让 HTML 和 JS 各司其职，但它们之间的紧密程度是如此之大，同时代的 <a href="https://github.com/tastejs/todomvc/blob/1.2.0/architecture-examples/polymer/elements/td-item.html">Polymer</a> 将它们统一封装为 Element 显然更加合理</li></ul><p>接下来让我们看一下 AngularJS TodoMVC 的核心代码：</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="comment">// template</span></span><br><span class="line">&lt;script type=<span class="string">&quot;text/ng-template&quot;</span> id=<span class="string">&quot;todomvc-index.html&quot;</span>&gt;</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">ul</span> <span class="attr">id</span>=<span class="string">&quot;todo-list&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">li</span> <span class="attr">ng-repeat</span>=<span class="string">&quot;todo in todos | filter:statusFilter track by $index&quot;</span> <span class="attr">ng-class</span>=<span class="string">&quot;&#123;completed: todo.completed, editing: todo == editedTodo&#125;&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;view&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">input</span> <span class="attr">class</span>=<span class="string">&quot;toggle&quot;</span> <span class="attr">type</span>=<span class="string">&quot;checkbox&quot;</span> <span class="attr">ng-model</span>=<span class="string">&quot;todo.completed&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">label</span> <span class="attr">ng-dblclick</span>=<span class="string">&quot;editTodo(todo)&quot;</span>&gt;</span>&#123;&#123;todo.title&#125;&#125;<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">button</span> <span class="attr">class</span>=<span class="string">&quot;destroy&quot;</span> <span class="attr">ng-click</span>=<span class="string">&quot;removeTodo(todo)&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// todoFocus directive</span></span><br><span class="line">angular.module(<span class="string">&#x27;todomvc&#x27;</span>)</span><br><span class="line">  .directive(<span class="string">&#x27;todoFocus&#x27;</span>, <span class="function"><span class="keyword">function</span> <span class="title">todoFocus</span>(<span class="params">$timeout</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span> (<span class="params">scope, elem, attrs</span>) </span>&#123;</span><br><span class="line">      scope.$watch(attrs.todoFocus, <span class="function"><span class="keyword">function</span> (<span class="params">newVal</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (newVal) &#123;</span><br><span class="line">          $timeout(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            elem[<span class="number">0</span>].focus();</span><br><span class="line">          &#125;, <span class="number">0</span>, <span class="literal">false</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><span class="line"></span><br><span class="line"><span class="comment">// Todo Controller</span></span><br><span class="line">angular.module(<span class="string">&#x27;todomvc&#x27;</span>)</span><br><span class="line">  .controller(<span class="string">&#x27;TodoCtrl&#x27;</span>, <span class="function"><span class="keyword">function</span> <span class="title">TodoCtrl</span>(<span class="params">$scope, $routeParams, $filter, todoStorage</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> todos = $scope.todos = todoStorage.get();</span><br><span class="line"></span><br><span class="line">    $scope.newTodo = <span class="string">&#x27;&#x27;</span>;</span><br><span class="line">    $scope.editedTodo = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    $scope.$watch(<span class="string">&#x27;todos&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">newValue, oldValue</span>) </span>&#123;</span><br><span class="line">      $scope.remainingCount = $filter(<span class="string">&#x27;filter&#x27;</span>)(todos, &#123; <span class="attr">completed</span>: <span class="literal">false</span> &#125;).length;</span><br><span class="line">      $scope.completedCount = todos.length - $scope.remainingCount;</span><br><span class="line">      $scope.allChecked = !$scope.remainingCount;</span><br><span class="line">      <span class="keyword">if</span> (newValue !== oldValue) &#123; <span class="comment">// This prevents unneeded calls to the local storage</span></span><br><span class="line">        todoStorage.put(todos);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;, <span class="literal">true</span>);</span><br><span class="line">    ...</span><br><span class="line">  &#125;);</span><br></pre></td></tr></table></figure><p>*注：完整可执行代码见 *<a href="https://github.com/tastejs/todomvc/tree/1.3.0/architecture-examples/angularjs">TodoMVC 1.3.0 - AngularJS</a><em>。</em></p><p>AngularJS 的实现存在一些类似的问题：</p><ul><li>Controller 围绕 <code>$scope</code> 来组织和更新数据、方法、事件，对于那些依赖其它数据的数据（类似 KVO 中的 computed properties），需要通过 <code>$scope.watch</code>解决</li><li>Dirty Check 需要开发者了解其内部原理，不然很可能就会出现有些更新没有识别到的问题，例如 $timeout</li></ul><p>另外，观察仔细或者有过亲身实践的同学或许能够注意到，Ember 和 AngularJS 在整体代码风格上与原本的 JS 相去甚远，它们引入了很多新的概念和 API。当按照这些范式写代码的时候，离 JS 本身却越来越远。他们更像是一门方言，一名出色的前端工程师需要重新学习它们的设计和文档，并不能很好地将之前的积累转化为生产力。</p><p>究其原因，套用 Frederick 著名的没有银弹中的概念，UI 和数据同步这一问题存在本质复杂度和偶然复杂度，Data Binding 这种解决途径带来了很多偶然的复杂度，需要开发者花费额外的精力去了解和克服，而非专注在本质问题上。</p><p><strong>数据决定 UI</strong></p><p>面对这些问题，React 的先驱们决定另辟蹊径，寻找一种更加可预测、可依赖的前端 UI 实现方案。一个简单的想法出现了：能不能用 JS 来构建 UI，在需要更新时，简单地全部更新就可以？</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">renderTodoItem</span>(<span class="params">todo</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> DOM.li(&#123; <span class="attr">className</span>: todo.completed ? <span class="string">&#x27;completed&#x27;</span> : <span class="string">&#x27;&#x27;</span> &#125;, [</span><br><span class="line">    DOM.input(&#123; <span class="attr">type</span>: <span class="string">&#x27;checkbox&#x27;</span>, <span class="attr">className</span>: <span class="string">&#x27;toggle&#x27;</span>, <span class="attr">checked</span>: todo.completed &#125;),</span><br><span class="line">    DOM.label(&#123; <span class="attr">onDoubleClick</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123; editTodo(todo); &#125; &#125;, todo.title),</span><br><span class="line">    DOM.button(&#123; <span class="attr">onClick</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123; destroy(todo); &#125; &#125;)</span><br><span class="line">  ]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">renderTodoList</span>(<span class="params">todoList, filterFlag</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> DOM.ul(&#123; <span class="attr">className</span>: <span class="string">&#x27;todo-list&#x27;</span> &#125;,</span><br><span class="line">    todoList.filter(<span class="function"><span class="keyword">function</span> (<span class="params">item</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (filterFlag === ACTIVE) <span class="keyword">return</span> !item.completed;</span><br><span class="line">        <span class="keyword">if</span> (filterFlag === COMPLETED) <span class="keyword">return</span> item.completed;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">      &#125;)</span><br><span class="line">      .map(renderTodoItem)</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">DOM.render(renderTodoList(todoList, filterFlag));</span><br></pre></td></tr></table></figure><p>传统的 Data Binding 实现在渲染 DOM 之后，需要不断从数据侧和 DOM 本身检测变化从而完成 UI 和数据的双向同步。React 则选择了另外一条完全不同的路：单向渲染，或者更加本质地来说：</p><p><img src="../images/react-design-highlights-02.jpg" alt="fn(data) =&gt; UI"></p><p>这种方式简单易懂：React 就像是一个没有副作用的函数，忠实地根据输入的数据构建符合预期的 UI。有很多人认为这是 One Way Data Binding，因为只有数据到 UI 的绑定，而不是像其它 Data Binding 方案还有 UI 到数据的绑定。同时，React 也更像 JS 本身，而不是一种面向某个领域的方言。前端工程师只需要学习其少量的 API，可以借助之前积累的 JS 经验来优化开发效率。</p><p>后来的事实也证明，正是这个简单的想法改变了一切，React 后续所有的功能都是以此为起点，很多周边的库如 Redux 更是蒙此恩泽。</p><h2 id="虚拟-DOM"><a href="#虚拟-DOM" class="headerlink" title="虚拟 DOM"></a>虚拟 DOM</h2><p>如果说单向渲染是 React 的内核，那么虚拟 DOM 就是能够让这个内核得以接地气的关键技术。因为每次都采用完全更新的方式是不可接受的，当前浏览器的性能并不足以在这种情况下提供可以容忍的使用体验。这也正是之前没有类似技术得以流行的根本原因。</p><p>这里不得不佩服 React 先驱们的智慧。他们大概从 Git Snapshot 或者 Docker 的 AUFS 中得到了一些灵感，通过在 JS 和 DOM 之间增加一个新的抽象表示层，从而在需要更新时，对比这一表示层的 diff，最终差量更新 DOM。</p><p>举个?，假设我们渲染如下 Component：</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="keyword">const</span> SimpleList = <span class="function">(<span class="params">&#123; list &#125;</span>) =&gt;</span> (</span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">ul</span> <span class="attr">className</span>=<span class="string">&quot;list&quot;</span>&gt;</span></span></span><br><span class="line"><span class="xml">  &#123;</span></span><br><span class="line"><span class="xml">    list.map((item) =&gt; (</span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">li</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>&#123;item.text&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="xml">    ))</span></span><br><span class="line"><span class="xml">  &#125;</span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> list = [</span><br><span class="line">  &#123; <span class="attr">id</span>: <span class="number">1</span>, <span class="attr">text</span>: <span class="string">&#x27;Item 1&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">id</span>: <span class="number">2</span>, <span class="attr">text</span>: <span class="string">&#x27;Item 2&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">id</span>: <span class="number">3</span>, <span class="attr">text</span>: <span class="string">&#x27;Item 3&#x27;</span> &#125;,</span><br><span class="line">];</span><br><span class="line">ReactDOM.render(<span class="xml"><span class="tag">&lt;<span class="name">SimpleList</span> <span class="attr">list</span>=<span class="string">&#123;list&#125;</span>&gt;</span>, document.body);</span></span><br></pre></td></tr></table></figure><p>这些节点在 Virtual 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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> element = &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;ul&#x27;</span>,</span><br><span class="line">  <span class="attr">props</span>: &#123; <span class="attr">className</span>: <span class="string">&#x27;list&#x27;</span> &#125;,</span><br><span class="line">  <span class="attr">children</span>: [</span><br><span class="line">    &#123; <span class="attr">type</span>: <span class="string">&#x27;li&#x27;</span>, <span class="attr">props</span>: &#123; <span class="attr">key</span>: <span class="number">1</span>, <span class="attr">className</span>: <span class="string">&#x27;item&#x27;</span>, <span class="attr">children</span>: [<span class="string">&quot;Item 1&quot;</span>] &#125; &#125;,</span><br><span class="line">    &#123; <span class="attr">type</span>: <span class="string">&#x27;li&#x27;</span>, <span class="attr">props</span>: &#123; <span class="attr">key</span>: <span class="number">2</span>, <span class="attr">className</span>: <span class="string">&#x27;item&#x27;</span>, <span class="attr">children</span>: [<span class="string">&quot;Item 2&quot;</span>] &#125; &#125;,</span><br><span class="line">    &#123; <span class="attr">type</span>: <span class="string">&#x27;li&#x27;</span>, <span class="attr">props</span>: &#123; <span class="attr">key</span>: <span class="number">3</span>, <span class="attr">className</span>: <span class="string">&#x27;item&#x27;</span>, <span class="attr">children</span>: [<span class="string">&quot;Item 3&quot;</span>] &#125; &#125;,</span><br><span class="line">  ],</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>当有数据变化时，例如 list 变为了 [ { id: 1, text: ‘Item 1’ } ]，React 会执行如下三个步骤：</p><ol><li>接收到变更数据，整个 UI 被重新渲染为新的 Virtual DOM 表示</li><li>对比新旧 Virtual DOM 表示的区别，得到差量更新内容。React 称这一过程为调和（Reconciliation）过程。在这个例子中，差量更新的内容为：删除后面两个 li</li><li>对 DOM 应用上一步中计算的差量更新<img src="../images/react-design-highlights-03.jpg"></li></ol><p><em>调和（Reconciliation）过程</em></p><p>进一步地，在增加了 DOM 结构特有的<a href="https://reactjs.org/docs/reconciliation.html">一些规则</a>后，React 可以将调和过程的时间复杂度控制在 O(n) 内。<img src="../images/react-design-highlights-04.jpg"><em>Web Frameworks Benchmark（slowdown 值越小越好）</em></p><p>可以这样评价虚拟 DOM：正是这一技术使得 React 在保持单向渲染理念的同时，在性能上和 Data Binding 方案保持在同一水准。</p><p>然而这并不是 Virtual DOM 的全部价值，在增加这一抽象层之后，React 其实脱离了 DOM 这样单一的应用场景，它的解决问题的思路可以普世地应用在所有 UI 场景中。前面三个步骤中的前两个属于 React 核心部分，第三个步骤可以由特定的 Renderer 来完成。目前单就官方目前支持的就有 DOM Renderer（浏览器）、Server Renderer（服务器）、Native Renderer（iOS 和 Android）、ART Renderer（Canvas，SVG 或者 VML），以及测试使用的 Test Renderer。比较知名的第三方 Renderer 有 Flipboard 的 <a href="https://github.com/Flipboard/react-canvas">react-canvas</a>，更多 Renderer 可以看这个 <a href="https://github.com/chentsulin/awesome-react-renderer">Awesome list</a>。</p><h2 id="Fiber"><a href="#Fiber" class="headerlink" title="Fiber"></a>Fiber</h2><p>React 在每次收到数据更新之后，会进行一次调和过程并一次性更新 DOM，这在一般情况下不存在显著的性能瓶颈。但在一些需要 UI 快速响应的场景中，例如动画、手势等，当 DOM 的更新量较多或者 JS 逻辑较为复杂时，就会引起卡顿等有损体验的情况。针对这一问题，从 2015 年起，React 团队就开始研究 Fiber（纤程），在最新的 v16 中，Fiber 已经成为了作为新一代调和过程的基础。</p><p>Fiber 的主要特性是支持增量渲染：能够将渲染工作分割为小块，并且将它们分散到多个帧中。</p><p>Lin Clark 用下面两幅图非常形象地展示了新旧调和过程的区别。<img src="../images/react-design-highlights-05.jpg"><em>原调和过程调用栈</em></p><p>原调和过程会沿组件树递归遍历，遇到需要更新的情况时直接更新 DOM。由于 JS 和样式计算、布局（Layout）以及许多情况下的绘制共享浏览器主线程，如果有的组件执行时间较长，就会导致一些需要快速响应的更新被阻塞，进而导致卡顿等有损用户体验的现象。<img src="../images/react-design-highlights-06.jpg"><em>基于 Fiber 的调和过程调用栈</em></p><p>新的基于 Fiber 的调和过程将原来的递归遍历打散，可以每次只计算部分节点的更新内容后回到主逻辑查看是否有需要立即更新的高优先级内容，从而能够实现快速响应。</p><p>那么怎么区分更新内容的优先级呢？既然总体更新操作是一定的，那么就需要根据响应速度的要求来划分优先级：</p><ol><li>对于动画或者用户手势这类场景来说，需要以最快速度进行响应，一般要在 16ms 左右才能保证 60 帧的平滑效果</li><li>对于点击、触摸这类用户主动的操作，控制在 80~150ms 左右即可保证无迟滞感</li><li>对于网络请求这类高延迟、被动触发的场景来说，UI 更新可以适当排在后面</li><li>对于一些在一段时间内用户没有明显察觉的内容来说，例如不在视口范围内的元素，可以等待用户即将看到他们时再进行更新</li></ol><p>为了能够将原调和过程的递归调用打散，React 团队基于 Fiber 开发了一套新的调度（Scheduling）算法，其核心思路是：</p><ul><li>构建和组件树对应的 Fiber Tree，每个 fiber 节点保存更新信息，并增加 sibling 和 return 分别指向下一同级节点和父节点，以方便暂停遍历、恢复遍历、提交待更新内容等需求</li><li>在检查到更新时，并不立即更新到 DOM，而是分为调和和提交两个阶段，调和阶段会收集待更新列表，可以被打断，后者会真正更新 DOM，不可以打断</li><li>加入时间片的概念，在时间片用光时检查待更新列表，并将其通过 <code>requestAnimationFrame</code>或者 <code>requestIdleCallback</code>让浏览器在合适的时间点进行更新</li><li>支持抢占，有高优先级更新内容时会优先对其进行处理</li></ul><p>据笔者所知，这似乎是 Scheduling 这一概念第一次应用在前端领域中，恰好迷思专栏最近刚刚更新了一篇详细介绍它的文章：“<a href="https://zhuanlan.zhihu.com/p/33389178">当我谈 scheduling 时我在谈什么？</a>”，有兴趣的话可以详细了解下这一技术在操作系统、编程语言中的应用。</p><p>fiber 本质上可以认为是一个虚拟的堆栈帧（<a href="http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch10s07.html">Stack Frame</a>），它保留着每个组件实例的更新信息以及对其它 fiber 的指向，新的 Scheduling 具备对它们的所有控制权，可以按照任意方式对它们进行调度，就像操作系统可以对应用程序随意进行调度一样。这为 React 的未来提供了更多可能性，例如在浏览器支持多线程渲染时，React 的 Scheduling 可以同时为每个线程安排一些工作量，借助并行处理能力大幅提高渲染性能。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>单向数据流让 React 具备简单优雅的内核，Virtual DOM 使 React 得以落地生根，新近的 Fiber 令 React 适应越来越复杂严苛的应用场景，这三个设计上的闪光点应该可以解释 React 如此流行的原因。透过表象看本质，我们可以注意到它们无不是从实际问题出发，去探究正确的解决之道，不断打磨并做到极致的。我们也很欣喜地看到 React 将很多其它工程领域中的优秀实践引进到前端当中来，例如 Functional Programming、Double Buffering、Scheduling 等等。诚然，React 仍然带给了我们一些新的问题和挑战，例如如何减少无效重渲染等，但只要具备这种探究问题本源、追寻至上之道的精神，相信 React 会走得更好、更远。</p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>React 最初是由 Facebook 工程师 Jordan Walke 受 <a href="http://hhvm.github.io/xhp-lib/">XHP</a> 启发创造的。简单来说，XHP 是 Facebook 开发的一个 PHP 扩展，使其能够高效率地创建可复用、可定制页面元素。</p><p>下面是一个 XHP 的例子，看起来 React 还真的是它的近亲呢~</p><figure class="highlight php"><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="class"><span class="keyword">class</span> :<span class="title">ui</span>:<span class="title">blog</span> <span class="keyword">extends</span> :<span class="title">x</span>:<span class="title">element</span> </span>&#123;</span><br><span class="line">  attribute</span><br><span class="line">    Blog blog @required,</span><br><span class="line">    <span class="class"><span class="keyword">enum</span> </span>&#123;<span class="string">&#x27;public&#x27;</span>, <span class="string">&#x27;private&#x27;</span>&#125; privacy = <span class="string">&#x27;public&#x27;</span>;</span><br><span class="line"></span><br><span class="line">  children <span class="keyword">empty</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">protected</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">$this</span>-&gt;getAttribute(<span class="string">&#x27;privacy&#x27;</span>) == <span class="string">&#x27;private&#x27;</span>) &#123;</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="variable">$blog</span> = <span class="keyword">$this</span>-&gt;getAttribute(<span class="string">&#x27;blog&#x27;</span>);</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">      &lt;div <span class="class"><span class="keyword">class</span>=&quot;<span class="title">blogPost</span>&quot;&gt;</span></span><br><span class="line"><span class="class">        &lt;<span class="title">h3</span>&gt;</span>&#123;<span class="variable">$blog</span>-&gt;getTitle()&#125;&lt;/h3&gt;</span><br><span class="line">        &lt;ui:blog-meta-data blog=&#123;<span class="variable">$blog</span>&#125; /&gt;</span><br><span class="line">        &lt;div <span class="class"><span class="keyword">class</span>=&quot;<span class="title">content</span>&quot;&gt;</span>&#123;<span class="variable">$blog</span>-&gt;getContent()&#125;&lt;/div&gt;</span><br><span class="line">      &lt;/div&gt;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有趣的是，XHP 又是受了 <a href="https://en.wikipedia.org/wiki/ECMAScript_for_XML">ECMAScript for XML</a> 的启示，冥冥中有种天道轮回的味道。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://en.wikipedia.org/wiki/React_(JavaScript_library)">React (JavaScript library) - Wikipedia</a></li><li><a href="https://reactjs.org/blog/2013/06/05/why-react.html">Why did we build React? - React Blog</a></li><li><a href="https://medium.freecodecamp.org/yes-react-is-taking-over-front-end-development-the-question-is-why-40837af8ab76">Yes, React is taking over front-end development. The question is why</a></li><li><a href="https://www.youtube.com/watch?v=ZCuYPiUIONs">Lin Clark - A Cartoon Intro to Fiber - React Conf 2017</a></li><li><a href="http://codebeforethehorse.tumblr.com/post/3096387855/an-introduction-to-xhp">An Introduction to XHP</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;React 自从 2013 年开源以来，凭借先进的理念、简洁的 API 和优秀的性能迅猛发展起来。如今已经构建起一个完整庞大的生态圈。从 NPM Trend 的统计结果来看，React 显著领先于其它前端框架：
&lt;img src=&quot;../images/react-design-highlights-01.jpg&quot;&gt;
&lt;em&gt;近两年 NPM 下载量对比情况&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;为什么 React 如此流行？它有哪些精妙之处值得我们深究？本文着重从 React 的核心设计理念方面进行探索。&lt;/p&gt;
&lt;h2 id=&quot;单向数据流&quot;&gt;&lt;a href=&quot;#单向数据流&quot; class=&quot;headerlink&quot; title=&quot;单向数据流&quot;&gt;&lt;/a&gt;单向数据流&lt;/h2&gt;&lt;p&gt;在 React 问世之前，已经有很多框架为日趋复杂的前端开发提供了解决方案。那么为什么还要创造 React 呢？从 React 早期的灵魂人物 Pete Hunt 的 &lt;a href=&quot;https://reactjs.org/blog/2013/06/05/why-react.html&quot;&gt;Why did we build React?&lt;/a&gt; 一文和 &lt;a href=&quot;https://www.youtube.com/watch?v=-DX3vJiqxm4&quot;&gt;The Secrets of React‘s Virtual DOM&lt;/a&gt; 演讲中，我们似乎可以得到一些启示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data Binding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了解决 UI 和数据同步复杂性的问题，Data Binding 被提出并得到了广泛应用。其中两个主要的流派是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Key Value Observable (KVO)，以 Ember、Meteor 和 Knockout 为代表&lt;/li&gt;
&lt;li&gt;Dirty Check，主要以 AngularJS 为代表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;让我们尝试用 TodoMVC 为例来看下各个方案的不同。&lt;/p&gt;
&lt;p&gt;下面是 Ember TodoMVC 的核心代码：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;19&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;20&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;21&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;22&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;23&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;24&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;25&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Template&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;lt;script type=&lt;span class=&quot;string&quot;&gt;&amp;quot;text/x-handlebars&amp;quot;&lt;/span&gt; data-template-name=&lt;span class=&quot;string&quot;&gt;&amp;quot;todo-list&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;ul&lt;/span&gt; &lt;span class=&quot;attr&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;quot;todo-list&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;    &amp;#123;&amp;#123;#each&amp;#125;&amp;#125;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;li&lt;/span&gt; &amp;#123;&amp;#123;&lt;span class=&quot;attr&quot;&gt;bind-attr&lt;/span&gt; &lt;span class=&quot;attr&quot;&gt;class&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;quot;isCompleted:completed isEditing:editing&amp;quot;&lt;/span&gt;&amp;#125;&amp;#125;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;        &amp;#123;&amp;#123;input type=&amp;quot;checkbox&amp;quot; class=&amp;quot;toggle&amp;quot; checked=isCompleted&amp;#125;&amp;#125;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;        &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;label&lt;/span&gt; &amp;#123;&amp;#123;&lt;span class=&quot;attr&quot;&gt;action&lt;/span&gt; &amp;quot;&lt;span class=&quot;attr&quot;&gt;editTodo&lt;/span&gt;&amp;quot; &lt;span class=&quot;attr&quot;&gt;on&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;quot;doubleClick&amp;quot;&lt;/span&gt;&amp;#125;&amp;#125;&amp;gt;&lt;/span&gt;&amp;#123;&amp;#123;title&amp;#125;&amp;#125;&lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;label&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;        &lt;span class=&quot;tag&quot;&gt;&amp;lt;&lt;span class=&quot;name&quot;&gt;button&lt;/span&gt; &amp;#123;&amp;#123;&lt;span class=&quot;attr&quot;&gt;action&lt;/span&gt; &amp;quot;&lt;span class=&quot;attr&quot;&gt;removeTodo&lt;/span&gt;&amp;quot;&amp;#125;&amp;#125; &lt;span class=&quot;attr&quot;&gt;class&lt;/span&gt;=&lt;span class=&quot;string&quot;&gt;&amp;quot;destroy&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;      &lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;li&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;    &amp;#123;&amp;#123;/each&amp;#125;&amp;#125;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;xml&quot;&gt;  &lt;span class=&quot;tag&quot;&gt;&amp;lt;/&lt;span class=&quot;name&quot;&gt;ul&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Todos List Controller&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Todos.TodosListController = Ember.ArrayController.extend(&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;needs&lt;/span&gt;: [&lt;span class=&quot;string&quot;&gt;&amp;#x27;todos&amp;#x27;&lt;/span&gt;],&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;allTodos&lt;/span&gt;: Ember.computed.alias(&lt;span class=&quot;string&quot;&gt;&amp;#x27;controllers.todos&amp;#x27;&lt;/span&gt;),&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;itemController&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;#x27;todo&amp;#x27;&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &lt;span class=&quot;attr&quot;&gt;canToggle&lt;/span&gt;: &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; anyTodos = &lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.get(&lt;span class=&quot;string&quot;&gt;&amp;#x27;allTodos.length&amp;#x27;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; isEditing = &lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.isAny(&lt;span class=&quot;string&quot;&gt;&amp;#x27;isEditing&amp;#x27;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; anyTodos &amp;amp;&amp;amp; !isEditing;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &amp;#125;.property(&lt;span class=&quot;string&quot;&gt;&amp;#x27;allTodos.length&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;string&quot;&gt;&amp;#x27;@each.isEditing&amp;#x27;&lt;/span&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="React" scheme="https://springuper.github.io/tags/React/"/>
    
    <category term="Virtual DOM" scheme="https://springuper.github.io/tags/Virtual-DOM/"/>
    
    <category term="Fiber" scheme="https://springuper.github.io/tags/Fiber/"/>
    
  </entry>
  
  <entry>
    <title>前端中的 Pipeline</title>
    <link href="https://springuper.github.io/fe-pipeline/"/>
    <id>https://springuper.github.io/fe-pipeline/</id>
    <published>2017-10-05T21:35:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>计算机领域的 Pipeline 通常认为起源于 Unix。最初 Douglas Mcllroy 发现很多时候人们会将 shell 命令的输出传递给另外一个 shell 命令，因此就提出了 Pipeline 这一概念。后来同在贝尔实验室的大牛 Ken Thompson 在 1973 年将其实现，并使用 | 作为 pipe 的语法符号：</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">$ ls -l | grep key | less</span><br></pre></td></tr></table></figure><p>如此优雅而又实用的 Pipeline 很快在各种操作系统中传播开来。</p><p>简单来说，Pipeline 一般具有如下特点：</p><ol><li>各个子过程高内聚，专注于解决特定问题，Simple &amp; Sharp</li><li>所有子过程具有一致的接口，例如从标准输入读取数据，正常结果输出到标准输出，异常结果输出到标准错误</li><li>能够通过一定形式将子过程组合起来解决复杂问题，例如 pipe</li></ol><p>事实上，Pipeline 作为化整为零、去繁就简的重要手段，在前端中也有诸多应用。</p><h2 id="Middleware-Pipeline"><a href="#Middleware-Pipeline" class="headerlink" title="Middleware Pipeline"></a>Middleware Pipeline</h2><p>NodeJS 框架 Express 在 1.0 版本中引入的 Middleware Pipeline 可以说为 Express 的流行居功至伟。透过下面简单几行代码，你就能感受到它散发的优雅气息：</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></pre></td><td class="code"><pre><span class="line">express()</span><br><span class="line">  .use(bodyParser.json())</span><br><span class="line">  .use(cookieParser())</span><br><span class="line">  .use(session(sessionOptions))</span><br><span class="line">  .use(<span class="string">&#x27;api&#x27;</span>, apiRoutes)</span><br><span class="line">  .use(errorHandler);</span><br></pre></td></tr></table></figure><p>或许对于很多后来人来说，并不觉得这有什么精巧独到之处。但在 NodeJS 刚刚开始流行的那个蛮荒年代，大多数人写的还是流水账一样的过程式代码，好一些的会去整理一些工具函数以供抽象和复用：</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">var</span> srv = http.createServer(<span class="function"><span class="keyword">function</span> (<span class="params">req, res</span>) </span>&#123;</span><br><span class="line">  req.parsedBody = bodyParser(req);</span><br><span class="line">  req.parsedCookie = cookieParser(req);</span><br><span class="line">  session(req, res, <span class="function"><span class="keyword">function</span> (<span class="params">err</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (err) &#123;</span><br><span class="line">      errorHandler(err);</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">// routes</span></span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><span id="more"></span><p>相比之下，我们可以明显看出 Middleware 的几个优势：</p><ol><li>代码简练、符合直觉。这是一个很重要的优势，因为代码的大部分生命周期内都是由程序员在维护，符合直觉的代码更容易被理解，在维护和定位问题时能够更有效率</li><li>合理的错误处理。任意 Middleware 出现问题，会越过后续所有普通 Middleware，直接由 Error Middleware 进行处理</li></ol><p>事实上，还有一个更为重要的优势：标准化，为解决高层次问题提供了良好基础。这一点在迷思专栏的 <a href="https://zhuanlan.zhihu.com/p/20691649">再谈 API 的撰写 - 架构</a> 这篇文章中得到了充分的诠释：通过将 API 执行路径上的各个环节抽象为中间件，然后再将中间件划分为通用逻辑（Pre-processing / Post-processing 等）和开发者需要关注的逻辑（Processing）等类别，并提供精细化的控制，最终得到一个流程清晰、功能完善、标准统一的 API 开发方案。</p><p>Middleware Pipeline 还有一个值得提及的独特之处：由于本质是是一种递归调用，因此整个调用过程更像是一个环环相扣的洋葱：<img src="../images/fe-pipeline-01.jpg"></p><p>有兴趣了解其实现的同学，可以查看早期 Express 所使用的 <a href="https://github.com/senchalabs/connect/blob/3.6.3/index.js">connect</a> 或者 Koa 的 <a href="https://github.com/koajs/compose">compose</a>。</p><h2 id="Stream-Pipeline"><a href="#Stream-Pipeline" class="headerlink" title="Stream Pipeline"></a>Stream Pipeline</h2><p>Stream 是 NodeJS 的一个核心功能，使得快速、高效处理数据成为了可能。例如读写大文件、处理高并发网络请求等。</p><p>建立在 Stream 之上的 Pipeline 非常自然而形象：数据像水流一样依次经过不同的处理流程，并最终得到期望的结果。下面这张 <a href="https://github.com/osscafe/gulp-cheatsheet">Gulp Cheet Sheet</a> 中的图片能够形象地说明这一比喻：<img src="../images/fe-pipeline-02.png"></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></pre></td><td class="code"><pre><span class="line">gulp.task(<span class="string">&#x27;js&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> gulp.src(<span class="string">&#x27;./js/src/*.coffee&#x27;</span>)</span><br><span class="line">    .pipe(coffee())</span><br><span class="line">    .pipe(uglify())</span><br><span class="line">    .pipe(gulp.dest(<span class="string">&#x27;./js/&#x27;</span>));</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>凭借对 Stream 惟妙惟肖地运用，Gulp 在与配置为主的 Grunt 的竞争中迅速取得了领先优势。</p><p>另一个必须提及的例子是 substack 的 <a href="http://browserify.org/">Browserify</a>。作为 <a href="https://github.com/substack/stream-handbook">Stream Handbook</a> 的作者，substack 对 Stream 的理解可谓深刻。于是在 Browserify 的实现中，我们可以看到下面这段核心逻辑：</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">var</span> pipeline = splicer.obj([</span><br><span class="line">  <span class="string">&#x27;record&#x27;</span>, [ <span class="built_in">this</span>._recorder() ],</span><br><span class="line">  <span class="string">&#x27;deps&#x27;</span>, [ <span class="built_in">this</span>._mdeps ],</span><br><span class="line">  <span class="string">&#x27;json&#x27;</span>, [ <span class="built_in">this</span>._json() ],</span><br><span class="line">  <span class="string">&#x27;unbom&#x27;</span>, [ <span class="built_in">this</span>._unbom() ],</span><br><span class="line">  <span class="string">&#x27;unshebang&#x27;</span>, [ <span class="built_in">this</span>._unshebang() ],</span><br><span class="line">  <span class="string">&#x27;syntax&#x27;</span>, [ <span class="built_in">this</span>._syntax() ],</span><br><span class="line">  <span class="string">&#x27;sort&#x27;</span>, [ depsSort(dopts) ],</span><br><span class="line">  <span class="string">&#x27;dedupe&#x27;</span>, [ <span class="built_in">this</span>._dedupe() ],</span><br><span class="line">  <span class="string">&#x27;label&#x27;</span>, [ <span class="built_in">this</span>._label(opts) ],</span><br><span class="line">  <span class="string">&#x27;emit-deps&#x27;</span>, [ <span class="built_in">this</span>._emitDeps() ],</span><br><span class="line">  <span class="string">&#x27;debug&#x27;</span>, [ <span class="built_in">this</span>._debug(opts) ],</span><br><span class="line">  <span class="string">&#x27;pack&#x27;</span>, [ <span class="built_in">this</span>._bpack ],</span><br><span class="line">  <span class="string">&#x27;wrap&#x27;</span>, []</span><br><span class="line">]);</span><br></pre></td></tr></table></figure><p>Browserify 的设计目标是将 CommonJS 模块组织的 JS 代码打包为可以在浏览器中运行的代码。实现这一目标所需要做的工作非常复杂，因此 Browserify 将其拆解为职责单一的多个子过程，例如分析依赖、拓扑排序、模块去重、打包合并等，并通过 Stream Pipeline 打通整个流程。这使得整个代码的架构异常清晰，对将来的维护和优化提供了良好基础。</p><p>点睛之笔在于，这个基于 <a href="https://github.com/browserify/labeled-stream-splicer">labeled-stream-splicer</a> 实现的 pipeline 还支持动态修改和扩展，而且不仅在内部实现中多处应用，还暴露为外部接口方便调用方进行定制。下面这个示例展示了将 deps 子过程输出结果的 source 属性改为大写的逻辑：</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">pipeline.get(<span class="string">&#x27;deps&#x27;</span>).push(through.obj(<span class="function"><span class="keyword">function</span> (<span class="params">row, enc, next</span>) </span>&#123;</span><br><span class="line">  row.source = row.source.toUpperCase();</span><br><span class="line">  <span class="built_in">this</span>.push(row);</span><br><span class="line">  next();</span><br><span class="line">&#125;));</span><br></pre></td></tr></table></figure><p>Browserify 众多的 Plugin 也大多利用了这一特性进行功能的增强。例如编译 TypeScript 的插件 <a href="https://github.com/TypeStrong/tsify/blob/v3.0.3/index.js#L46">Tsify</a> 就是在 record 这一子过程之后插入一个遍历所有输入文件并进行编译的过程：</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">b.pipeline.get(<span class="string">&#x27;record&#x27;</span>).push(gatherEntryPoints());</span><br></pre></td></tr></table></figure><p>毫不夸张的说，这是笔者从业以来所见到过最为优秀的设计，没有之一。在为一个使用 SeaJS 的团队设计组件化方案时，由于各种限制并不能直接应用 Browserify，因此就借（chao）鉴（xi）它的设计思路，完成了一个简单的组件打包工具 <a href="https://github.com/springuper/tiler/blob/master/index.js#L44">Tiler</a>，并受用至今。</p><h2 id="Promise-Pipeline"><a href="#Promise-Pipeline" class="headerlink" title="Promise Pipeline"></a>Promise Pipeline</h2><p>由 Promise 组成的 Pipeline 与 Middleware Pipeline 有一些相通之处，例如都支持异步，错误处理也有异曲同工之妙。但毫无疑问 Promise 天生就在异步处理上更加得心应手，而且在函数式编程中具有一席之地，有人专门证明了下，<a href="https://gist.github.com/briancavalier/3296186">Promise 属于 Monad</a>（感兴趣的可以看下蝴蝶书的作者 Douglas Crockford 这个专门介绍 Monad 的讲座：<a href="https://www.youtube.com/watch?v=dkZFtimgAcM">Monads and Gonads</a>）。有了理论上的保证，我们总是可以通过 Promise.resolve/Promise.reject 将非 promise 的值转换为 promise，而 promise.then/promise.catch 也总是返回一个新的 promise 从而方便链式调用。</p><p>此外，Promise 还有一个 Killer Feature：一旦有一个 promise 出现异常，那么会忽视后面所有的 then 直到第一个 catch。这样的错误处理机制和先前介绍的 Middleware Pipeline 非常类似，但却更为强大，例如 catch 后还可以在做必要的处理后再次返回一个正常的 promise，实现优雅降级等业务需求。</p><p>下面是笔者在实现 VPAID Player 时的核心逻辑：</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">client.prototype.playAd = <span class="function"><span class="keyword">function</span> (<span class="params">vastXMLString</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> constructResponseFromString(vastXMLString)</span><br><span class="line">    .then(<span class="built_in">this</span>.loadAdUnit)</span><br><span class="line">    .then(<span class="built_in">this</span>.handshake)</span><br><span class="line">    .then(<span class="built_in">this</span>.initAd)</span><br><span class="line">    .then(<span class="built_in">this</span>.bindEvents)</span><br><span class="line">    .then(<span class="built_in">this</span>.startAd)</span><br><span class="line">    .then(<span class="built_in">this</span>.finish)</span><br><span class="line">    .catch(<span class="built_in">this</span>.handleError);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>通过将播放广告的逻辑划分为构建返回值对象、加载第三方 JS、初始化广告等各个小而精的细分子过程，然后串联成 Promise Pipeline，并在最后做统一的错误处理，使得整体逻辑十分流畅清晰，提高了代码的可维护性。</p><h2 id="Ramda-Pipeline"><a href="#Ramda-Pipeline" class="headerlink" title="Ramda Pipeline"></a>Ramda Pipeline</h2><p>最后，让我们再看一个函数式编程领域中的 Pipeline：<a href="http://ramdajs.com/">Ramda</a> Pipeline。</p><p>假设我们需要解决这个问题：将如下对象转换为 query 字符串</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="keyword">const</span> obj = &#123;</span><br><span class="line">  <span class="attr">foo</span>: <span class="string">&#x27;bar&#x27;</span>,</span><br><span class="line">  <span class="attr">baz</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">qux</span>: <span class="number">3.1415</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>先来看下 Lodash 的解法：</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="keyword">const</span> objToQueryStr = <span class="function">(<span class="params">obj</span>) =&gt;</span></span><br><span class="line">  _.join(_.map(_.toPairs(obj), <span class="function">(<span class="params">kvs</span>) =&gt;</span> _.join(kvs, <span class="string">&#x27;=&#x27;</span>)), <span class="string">&#x27;=&#x27;</span>);</span><br></pre></td></tr></table></figure><p>再来看下 Ramda 的解法：</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="keyword">const</span> objToQueryStr = R.pipe(</span><br><span class="line">  R.toPairs,</span><br><span class="line">  R.map(R.join(<span class="string">&#x27;=&#x27;</span>)),</span><br><span class="line">  R.join(<span class="string">&#x27;&amp;&#x27;</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>可以看出，Ramda 在如下两个方面更加出色：</p><ol><li>借助 currying 和数据后置，Ramda 并不需要显式创建新函数，代码更简练</li><li>顺序执行，容易理解（虽然很多函数式编程的童鞋们更喜欢 R.compose）</li></ol><p>因此，在推崇函数式编程的团队中，Ramda 基本已成为必需品。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>前端中的 Pipeline 远不止本文介绍的这几种，比较知名的还有 RxJS 等等。从表面上看，它们每个都有着不同的目标问题域和因此而设计的特性，不过从本质上来讲，基本都遵循了 Unix Pipeline 的基本思路：化整为零 + 灵活组合。希望我们前端工程师们再接再厉，将这种精神发扬光大，更好地解决实际问题，不断推动前端的发展。</p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>macOS 中的 workflow 定制工具 Automator 应用的图标是一个机器人，它的手上拿着的正是一根管子（Pipe）：<img src="../images/fe-pipeline-03.jpg"></p><p>怎么样，非常的可爱吧~</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://en.wikipedia.org/wiki/Pipeline_(Unix)">Pipeline (Unix)</a></li><li><a href="https://dzone.com/articles/understanding-middleware-pattern-in-expressjs">Understanding the Middleware Pattern in Express.js - DZone Web Dev</a></li><li><a href="https://gist.github.com/domenic/3889970">You’re Missing the Point of Promises</a></li><li><a href="https://github.com/toastal/Curried-Functional-Pipelines-and-Ramda-Slides">Pipelines &amp; Ramda</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;计算机领域的 Pipeline 通常认为起源于 Unix。最初 Douglas Mcllroy 发现很多时候人们会将 shell 命令的输出传递给另外一个 shell 命令，因此就提出了 Pipeline 这一概念。后来同在贝尔实验室的大牛 Ken Thompson 在 1973 年将其实现，并使用 | 作为 pipe 的语法符号：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;$ ls -l | grep key | less&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;如此优雅而又实用的 Pipeline 很快在各种操作系统中传播开来。&lt;/p&gt;
&lt;p&gt;简单来说，Pipeline 一般具有如下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;各个子过程高内聚，专注于解决特定问题，Simple &amp;amp; Sharp&lt;/li&gt;
&lt;li&gt;所有子过程具有一致的接口，例如从标准输入读取数据，正常结果输出到标准输出，异常结果输出到标准错误&lt;/li&gt;
&lt;li&gt;能够通过一定形式将子过程组合起来解决复杂问题，例如 pipe&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;事实上，Pipeline 作为化整为零、去繁就简的重要手段，在前端中也有诸多应用。&lt;/p&gt;
&lt;h2 id=&quot;Middleware-Pipeline&quot;&gt;&lt;a href=&quot;#Middleware-Pipeline&quot; class=&quot;headerlink&quot; title=&quot;Middleware Pipeline&quot;&gt;&lt;/a&gt;Middleware Pipeline&lt;/h2&gt;&lt;p&gt;NodeJS 框架 Express 在 1.0 版本中引入的 Middleware Pipeline 可以说为 Express 的流行居功至伟。透过下面简单几行代码，你就能感受到它散发的优雅气息：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;express()&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  .use(bodyParser.json())&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  .use(cookieParser())&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  .use(session(sessionOptions))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  .use(&lt;span class=&quot;string&quot;&gt;&amp;#x27;api&amp;#x27;&lt;/span&gt;, apiRoutes)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  .use(errorHandler);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;或许对于很多后来人来说，并不觉得这有什么精巧独到之处。但在 NodeJS 刚刚开始流行的那个蛮荒年代，大多数人写的还是流水账一样的过程式代码，好一些的会去整理一些工具函数以供抽象和复用：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; srv = http.createServer(&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;req, res&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  req.parsedBody = bodyParser(req);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  req.parsedCookie = cookieParser(req);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  session(req, res, &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;err&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (err) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      errorHandler(err);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// routes&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;  &amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="Promise" scheme="https://springuper.github.io/tags/Promise/"/>
    
    <category term="Frontend" scheme="https://springuper.github.io/tags/Frontend/"/>
    
    <category term="Pipeline" scheme="https://springuper.github.io/tags/Pipeline/"/>
    
    <category term="Middleware" scheme="https://springuper.github.io/tags/Middleware/"/>
    
    <category term="Stream" scheme="https://springuper.github.io/tags/Stream/"/>
    
  </entry>
  
  <entry>
    <title>js 中的 !! 与 ~~</title>
    <link href="https://springuper.github.io/bitwise-not-and-logical-not/"/>
    <id>https://springuper.github.io/bitwise-not-and-logical-not/</id>
    <published>2015-10-17T21:01:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>在各大 js 的开源项目中，时常见到 !! 和 ~~，偶有猜对，却总不得要领。本文旨在深入剖析下这两个运算符的原理，以及使用时的利弊。</p><h2 id=""><a href="#" class="headerlink" title="!"></a>!</h2><p>为了简化问题，我们首先了解下常见的逻辑非运算符 !，EcmaScript 中的定义是：</p><blockquote><p>产生式 <code>UnaryExpression : ! UnaryExpression</code> 按照下面的过程执行：</p><ul><li>令 <code>expr</code> 为解析执行 <code>UnaryExpression</code> 的结果</li><li>令 <code>oldValue</code> 为 ToBoolean(GetValue(<code>expr</code>))</li><li>如果 <code>oldValue</code> 为 true, 返回 false</li><li>返回 true</li></ul></blockquote><p>GetValue 处理取值的细节，例如依附于对象的属性、执行 getter 等，不再深究。重点看下 ToBoolean，它能够将各种类型的值最终转化为布尔类型。具体的规则可参考 <a href="http://es5.github.io/#x9.2">ES5#9.2 ToBoolean</a> 一节。</p><p>接下来的处理很简单，如果 ToBoolen 得到的结果 <code>oldValue</code> 是 true，那就返回 false，否则返回 true。</p><span id="more"></span><h2 id="-1"><a href="#-1" class="headerlink" title="!!"></a>!!</h2><p>了解了 ! 之后，!! 就很好解释了，简单来说就是：</p><blockquote><p>产生式 <code>UnaryExpression : !! UnaryExpression</code> 按照下面的过程执行：</p><ul><li>令 <code>expr</code> 为解析执行 <code>UnaryExpression</code> 的结果</li><li>返回 ToBoolean(GetValue(<code>expr</code>))</li></ul></blockquote><p>是的，比 ! 的运算过程减少了两步，执行完 ToBoolean 后就直接返回了。这也是 !! 最主要的用途：<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="literal">null</span> <span class="comment">// false</span></span><br><span class="line">!! <span class="literal">undefined</span> <span class="comment">// false</span></span><br><span class="line"></span><br><span class="line">!! <span class="string">&#x27;&#x27;</span> <span class="comment">// false</span></span><br><span class="line">!! <span class="string">&#x27;hello&#x27;</span> <span class="comment">// true</span></span><br><span class="line"></span><br><span class="line">!! <span class="number">5</span> <span class="comment">// true</span></span><br><span class="line">!! <span class="number">0</span> <span class="comment">// false</span></span><br><span class="line"></span><br><span class="line">!! &#123;&#125; <span class="comment">// true</span></span><br></pre></td></tr></table></figure><p>值得提示的一点是，!! 实际上等效于 <code>Boolean</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">!!(value) === <span class="built_in">Boolean</span>(value)</span><br></pre></td></tr></table></figure><h2 id="-2"><a href="#-2" class="headerlink" title="~"></a>~</h2><p>按位非操作符 ~ 比逻辑非操作符 ! 复杂一些，作用是将数值比特位中的 1 变成 0，0 变成 1。EcmaScript 中的定义为：</p><blockquote><p>产生式 <code>UnaryExpression : ~ UnaryExpression</code> 按照下面的过程执行：</p><ul><li>令 <code>expr</code> 为解析执行 <code>UnaryExpression</code> 的结果</li><li>令 <code>oldValue</code> 为 ToInt32(GetValue(<code>expr</code>))</li><li>返回 <code>oldValue</code> 按位取反的结果</li></ul></blockquote><p>与逻辑非执行过程第二步不同，按位非调用的是 ToInt32 而不是 ToBoolean。ToInt32 的处理过程比较复杂，简化为以下四步：</p><ul><li>令 <code>number</code> 为调用 ToNumber 将输入参数转化为数值类型的结果</li><li>如果 <code>number</code> 是 NaN，+0，-0，+∞ 或者 -∞，返回 +0</li><li>令 <code>posInt</code> 为 sign(<code>number</code>) * floor(abs(<code>number</code>))</li><li>将 <code>posInt</code> 进行取模处理，转化为在 −2^31 到 2^31−1 之间的 32 位有符号整数并返回</li></ul><p>从效果上看，ToInt32 依次做了这样几件事：</p><ul><li>类型转换，非数值类型的需要转化为数值类型</li><li>特殊值处理，NaN 和 ∞ 都被转化为 0</li><li>取整，如果是浮点数，会损失小数点后面的精度</li><li>取模，将整数调整到 32 位有符号整数区间内，如果整数原本不在这个区间，会丧失精度</li></ul><p>执行完 ToInt32 之后，将得到的 32 位有符号整数进行按位取反，并将结果返回。</p><p>需要注意的是，所有的位操作都会先将操作数转化为 32 位有符号整数。</p><h2 id="-3"><a href="#-3" class="headerlink" title="~~"></a>~~</h2><p>和 !! 与 ! 的关系类似，~~ 实际上是 ~ 的简化版：</p><blockquote><p>产生式 <code>UnaryExpression : ~~ UnaryExpression</code> 按照下面的过程执行：</p><ul><li>令 <code>expr</code> 为解析执行 <code>UnaryExpression</code> 的结果</li><li>返回 ToInt32(GetValue(<code>expr</code>))</li></ul></blockquote><p>因为第一次执行 ~ 时已经将操作数转化为 32 位有符号整数，第二次执行 ~ 时实际只是将按位取反的结果再次按位取反，相当于取消掉 ~ 处理过程中的第三步。那么 ~~ 的用途也就很明确了：<strong>将操作数转化为 32 位有符号整数</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></pre></td><td class="code"><pre><span class="line">~~ <span class="literal">null</span> <span class="comment">// 0</span></span><br><span class="line">~~ <span class="literal">undefined</span> <span class="comment">// 0</span></span><br><span class="line">~~ <span class="literal">NaN</span> <span class="comment">// 0</span></span><br><span class="line">~~ &#123;&#125; <span class="comment">// 0</span></span><br><span class="line">~~ <span class="literal">true</span> <span class="comment">// 1</span></span><br><span class="line">~~ <span class="string">&#x27;&#x27;</span> <span class="comment">// 0</span></span><br><span class="line">~~ <span class="string">&#x27;string&#x27;</span> <span class="comment">// 0</span></span><br><span class="line">~~ <span class="string">&#x27;1&#x27;</span> <span class="comment">// 1</span></span><br><span class="line">~~ <span class="built_in">Number</span>.POSITIVE_INFINITY <span class="comment">// 0</span></span><br><span class="line"></span><br><span class="line">~~ <span class="number">1.2</span> <span class="comment">// 1</span></span><br><span class="line">~~ -<span class="number">1.2</span> <span class="comment">// -1</span></span><br><span class="line">~~ <span class="number">1.6</span> <span class="comment">// 1</span></span><br><span class="line">~~ -<span class="number">1.6</span> <span class="comment">// -1</span></span><br><span class="line">~~ (<span class="built_in">Math</span>.pow(<span class="number">2</span>, <span class="number">31</span>) - <span class="number">1</span>) <span class="comment">// 2147483647 = 2^31-1</span></span><br><span class="line">~~ (<span class="built_in">Math</span>.pow(<span class="number">2</span>, <span class="number">31</span>)) <span class="comment">// -2147483648 = -2^31</span></span><br><span class="line">~~ (-<span class="built_in">Math</span>.pow(<span class="number">2</span>, <span class="number">31</span>)) <span class="comment">// -2147483648 = -2^31</span></span><br><span class="line">~~ (-<span class="built_in">Math</span>.pow(<span class="number">2</span>, <span class="number">31</span>) - <span class="number">1</span>) <span class="comment">// 2147483647 = 2^31-1</span></span><br><span class="line">~~ (<span class="built_in">Math</span>.pow(<span class="number">2</span>, <span class="number">32</span>)) <span class="comment">// 0</span></span><br></pre></td></tr></table></figure><p>如果你需要将一个参数转化为 32 位有符号整数，那么 ~~ 将是最简便的方式。不过要切记，它会损失精度，包括小数和整数部分。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators">按位操作符-MDN</a></li><li><a href="http://es5.github.io/#x9.2">ToBoolean - ES5</a></li><li><a href="http://es5.github.io/#x9.5">ToInt32 - ES5</a></li><li><a href="http://es5.github.io/#x11.4.8">Bitwise NOT Operator - ES5</a></li><li><a href="http://es5.github.io/#x11.4.9">Logical NOT Operator - ES5</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;在各大 js 的开源项目中，时常见到 !! 和 ~~，偶有猜对，却总不得要领。本文旨在深入剖析下这两个运算符的原理，以及使用时的利弊。&lt;/p&gt;
&lt;h2 id=&quot;&quot;&gt;&lt;a href=&quot;#&quot; class=&quot;headerlink&quot; title=&quot;!&quot;&gt;&lt;/a&gt;!&lt;/h2&gt;&lt;p&gt;为了简化问题，我们首先了解下常见的逻辑非运算符 !，EcmaScript 中的定义是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;产生式 &lt;code&gt;UnaryExpression : ! UnaryExpression&lt;/code&gt; 按照下面的过程执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;令 &lt;code&gt;expr&lt;/code&gt; 为解析执行 &lt;code&gt;UnaryExpression&lt;/code&gt; 的结果&lt;/li&gt;
&lt;li&gt;令 &lt;code&gt;oldValue&lt;/code&gt; 为 ToBoolean(GetValue(&lt;code&gt;expr&lt;/code&gt;))&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;oldValue&lt;/code&gt; 为 true, 返回 false&lt;/li&gt;
&lt;li&gt;返回 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;GetValue 处理取值的细节，例如依附于对象的属性、执行 getter 等，不再深究。重点看下 ToBoolean，它能够将各种类型的值最终转化为布尔类型。具体的规则可参考 &lt;a href=&quot;http://es5.github.io/#x9.2&quot;&gt;ES5#9.2 ToBoolean&lt;/a&gt; 一节。&lt;/p&gt;
&lt;p&gt;接下来的处理很简单，如果 ToBoolen 得到的结果 &lt;code&gt;oldValue&lt;/code&gt; 是 true，那就返回 false，否则返回 true。&lt;/p&gt;</summary>
    
    
    
    
    <category term="BitwiseNOT" scheme="https://springuper.github.io/tags/BitwiseNOT/"/>
    
    <category term="LogicalNOT" scheme="https://springuper.github.io/tags/LogicalNOT/"/>
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>前端组件化开发实践</title>
    <link href="https://springuper.github.io/frontend-component-practice/"/>
    <id>https://springuper.github.io/frontend-component-practice/</id>
    <published>2015-06-29T11:07:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>一位计算机前辈曾说过：</p><pre><code>Controlling complexity is the essence of computer programming.</code></pre><p>随着前端开发复杂度的日益提升，组件化开发应运而生，并随着 FIS、React 等优秀框架的出现遍地开花。这一过程同样发生在美团，面临业务规模的快速发展和工程师团队的不断扩张，我们历经引入组件化解决资源整合问题、逐步增强组件功能促进开发效率、重新打造新一代组件化方案适应全栈开发和共享共建等阶段，努力“controlling complexity”。本文将介绍我们组件化开发的实践过程。</p><h3 id="组件化-1-0：资源重组"><a href="#组件化-1-0：资源重组" class="headerlink" title="组件化 1.0：资源重组"></a>组件化 1.0：资源重组</h3><p>在美团早期，前端资源是按照页面或者类似业务页面集合的形式进行组织的。例如 order.js 对应订单相关页面的交互，account.css 对应账户相关页面的样式。这种方式在过去的较长一段时间内，持续支撑了整个项目的正常推进，功勋卓著。</p><center><img src="/images/frontend-component-practice/legacy-flow.png" alt="legacy-flow" width="500"></center><p>随着业务规模的增加和开发团队的扩张，这套机制逐渐显示出它的一些不足：</p><ol><li><strong>资源冗余</strong>页面的逐渐增加，交互的逐渐复杂化，导致对应的 css 和 js 都有大幅度增长，进而出现为了依赖某个 js 中的一个函数，需要加载整个模块，或者为了使用某个 css 中的部分样式依赖整个 css，冗余资源较多</li><li><strong>对应关系不直观</strong>没有显而易见的对应规则，导致的一个问题是修改某个业务模块的 css 或者 js 时，几乎只能依靠 grep。靠人来维护页面模块 html、css 和 js 之间的依赖关系，容易犯错，常常出现内容已经删除但是 css 或 js 还存在的问题</li><li><strong>难于单元测试</strong>以页面为最小粒度进行资源整合，不同功能的业务模块相互影响，复杂度太高，自动化测试难以推进</li></ol><p>2013 年开始，在调研了 FIS、BEM 等方案之后，结合美团开发框架的实际，我们初步实现了一套轻量级的组件化开发方案。主要的改进是：</p><ul><li>以页面功能组件为单位聚合前端资源</li><li>自动加载符合约定的 css、js 资源</li><li>将业务数据到渲染数据的转换过程独立出来</li></ul><center><img src="/images/frontend-component-practice/component-flow.png" alt="component-flow" width="500"></center><p>举例来说，美团顶部的搜索框就被实现为一个组件。</p><center><img src="/images/frontend-component-practice/smart-box.png" alt="smart-box" width="500"></center><span id="more"></span><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><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">www/component/smart-box/</span><br><span class="line">├── smart-box.js    <span class="comment"># 交互</span></span><br><span class="line">├── smart-box.php   <span class="comment"># 渲染数据生产、组件配置</span></span><br><span class="line">├── smart-box.scss  <span class="comment"># 样式</span></span><br><span class="line">├── smart-box.tpl   <span class="comment"># 内容</span></span><br><span class="line">└── <span class="built_in">test</span></span><br><span class="line">    ├── default.js  <span class="comment"># 自动化测试</span></span><br><span class="line">    └── default.php <span class="comment"># 单测页面</span></span><br></pre></td></tr></table></figure><p>调用组件变得十足简单：</p><figure class="highlight php"><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">echo</span> View::useComponent(<span class="string">&#x27;smart-box&#x27;</span>, [</span><br><span class="line">    <span class="string">&#x27;keyword&#x27;</span> =&gt; <span class="variable">$keyword</span></span><br><span class="line">]);</span><br></pre></td></tr></table></figure><p>对比之前，可以看到组件化的一些特点：</p><ol><li><strong>按需加载</strong>只加载必要的前端资源</li><li><strong>对应关系非常清晰</strong>组件所需要的前端资源都在同一目录，职责明确且唯一，对应关系显著</li><li><strong>易于测试</strong>组件是具备独立展现和交互的最小单元，可利用 Phantom 等工具自动化测试</li></ol><p>此外，由于前端资源集中进行调度，组件化也为高阶性能优化提供了空间。例如实现组件级别的 BigRender、通过数据分析进行资源的合并加载等等。</p><h3 id="组件化-2-0：趋于成熟"><a href="#组件化-2-0：趋于成熟" class="headerlink" title="组件化 2.0：趋于成熟"></a>组件化 2.0：趋于成熟</h3><p>组件化 1.0 上线后，由于简单易用，很快得到工程师的认可，并开始在各项业务中应用起来。新的需求接踵而来，一直持续到 2014 年底，这个阶段我们称之为组件化 2.0。下面介绍下主要的几个改进。</p><h4 id="Lifecycle"><a href="#Lifecycle" class="headerlink" title="Lifecycle"></a>Lifecycle</h4><p>组件在高内聚的同时，往往需要暴露一些接口供外界调用，从而能够适应复杂的页面需求，例如提交订单页面需要在支付密码组件启动完成后绑定提交时的检查。Web Components、React 等都选择了生命周期事件/方法，我们也是一样。</p><p>组件的生命周期：</p><center><img src="/images/frontend-component-practice/component-lifecycle.png" alt="component-lifecycle" width="800"></center><p>一个组件的完整生命周期包括：</p><ul><li>init，初始化组件根节点和配置</li><li>fetch，加载 css 和 js 资源</li><li>render，内容渲染，默认的渲染内容方式是 BigRender</li><li>ready，进行数据绑定等操作</li><li>update，数据更新</li><li>destroy，解除所有事件监听，删除所有组件节点</li></ul><p>组件提供 pause、resume 方法以方便进行生命周期控制。各个阶段使用 Promise 串行进行，异步的管理更清晰。使用自定义语义事件，在修改默认行为、组件间通信上充分利用了 YUI 强大的自定义事件体系，有效降低了开发维护成本。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> afterLoadList = [];</span><br><span class="line">Y.all(<span class="string">&#x27;[data-component]&#x27;</span>).each(<span class="function"><span class="keyword">function</span> (<span class="params">node</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> component = <span class="keyword">new</span> Y.mt.Component(node);</span><br><span class="line">    <span class="comment">// 绑定 init 生命周期事件，在 init 默认行为完成后执行回调</span></span><br><span class="line">    component.after(<span class="string">&#x27;init&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 如果配置了延迟启动</span></span><br><span class="line">        <span class="keyword">if</span> (e.config.afterLoad) &#123;</span><br><span class="line">            <span class="comment">// 暂停组件生命周期</span></span><br><span class="line">            e.component.pause();</span><br><span class="line">            <span class="comment">// 压入延迟启动数组</span></span><br><span class="line">            afterLoadList.push(e.component);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">// 开始进入生命周期</span></span><br><span class="line">    component.start();</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">Y.on(<span class="string">&#x27;load&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 在页面 load 事件发生时恢复组件生命周期</span></span><br><span class="line">    afterLoadList.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">component</span>) </span>&#123;</span><br><span class="line">        component.resume();</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>回过头来看，引入生命周期除了带来扩展性外，更重要的是理顺了组件的各个阶段，有助于更好的理解和运用。</p><h4 id="Data-Binding"><a href="#Data-Binding" class="headerlink" title="Data Binding"></a>Data Binding</h4><p>数据绑定是我们期盼已久的功能，将 View 和 ViewModel 之间的交互自动化无疑会节省工程师的大量时间。在组件化减少关注点和降低复杂度后，实现数据绑定变得更加可能。</p><p>我们最终实现的数据绑定方案主要参考了 Angular，通过在 html 节点上添加特定的属性声明绑定逻辑，js 扫描这些内容并进行相应的渲染和事件绑定。当数据发生变化时，对应的内容全部重新渲染。</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">ul</span> <span class="attr">class</span>=<span class="string">&quot;addressList&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span></span></span><br><span class="line"><span class="tag">        <span class="attr">mt-bind-repeat</span>=<span class="string">&quot;addr in addrList&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">mt-bind-html</span>=<span class="string">&quot;addr.text&quot;</span></span></span><br><span class="line"><span class="tag">    &gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">Y.use([<span class="string">&#x27;mt-bind&#x27;</span>, <span class="string">&#x27;mt-scope&#x27;</span>], <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span></span><br><span class="line"><span class="javascript">    Y.mt.bind.init(<span class="built_in">document</span>.body);</span></span><br><span class="line"><span class="javascript">    <span class="keyword">var</span> scope = Y.one(<span class="string">&#x27;.addressList&#x27;</span>).getScope();</span></span><br><span class="line"><span class="javascript">    <span class="comment">// 将 scope.addrList 设置为一个数组，DOM 上将自动渲染其内容   </span></span></span><br><span class="line"><span class="javascript">    scope.$set(<span class="string">&#x27;addrList&#x27;</span>, [</span></span><br><span class="line"><span class="javascript">        &#123; <span class="attr">text</span>: <span class="string">&quot;first address&quot;</span> &#125;,</span></span><br><span class="line"><span class="javascript">        &#123; <span class="attr">text</span>: <span class="string">&quot;second address&quot;</span> &#125;</span></span><br><span class="line"><span class="javascript">    ]);</span></span><br><span class="line"><span class="javascript">&#125;);</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>使用属性声明绑定逻辑的好处是可以同时支持后端渲染，这对于美团团购这样的偏展现型业务是非常必要的，用户可以很快看到页面内容。</p><h4 id="Flux"><a href="#Flux" class="headerlink" title="Flux"></a>Flux</h4><p>实现数据绑定后，我们不得不面对另外一个问题：如何协同多个组件间的数据。因为某个组件的数据变化，很有可能引起其他组件的变化。例如当修改购买数量，总金额会变化，而总金额超过 500 后，还需要展示大额消费提醒。</p><p>为了解决这个问题，我们引入了 Flux，使用全局消息总线的思路进行跨组件交互。</p><p>例如因为交互复杂而一直让我们非常头疼的项目购买页，在应用组件 + Flux 重构后，各模块之间的互动更加清晰：</p><center><img src="/images/frontend-component-practice/component-flux.png" alt="component-flux" width="600"></center><p>其他方面的改进还有很多，包括引入模板引擎 LightnCandy 约束模板逻辑、支持组件任意嵌套、支持异步加载并自动初始化等。</p><p>随着组件化 2.0 的逐步完善，基本已经可以从容应对日常开发，在效率和质量方面都上了一个台阶。</p><h3 id="组件化-3-0：重启征程"><a href="#组件化-3-0：重启征程" class="headerlink" title="组件化 3.0：重启征程"></a>组件化 3.0：重启征程</h3><p>时间的车轮滚滚前行，2014 年底，我们遇到一些新的机遇和挑战：</p><ul><li>基于 Node 的全栈开发模式开始应用，前后端渲染有了更多的可能性</li><li>YUI 停止维护，需要一套新的资源管理方案</li><li>新业务不断增加，需要找到一种组件共享的方式，避免重复造轮子</li></ul><p>结合之前的实践，以及在这一过程中逐渐积累的对业内方案的认知，我们提出了新的组件化方案：</p><ul><li>基于 React 开发页面组件，使用 NPM 进行分发，方便共建共享</li><li>基于 Browserify 二次开发，建设资源打包工具 Reduce，方便浏览器加载</li><li>建设适应组件化开发模式的工程化开发方案 Turbo，方便工程师将组件应用于业务开发中</li></ul><h4 id="React"><a href="#React" class="headerlink" title="React"></a>React</h4><p>在组件化 2.0 的过程中，我们发现很多功能和 React 重合，例如 Data Binding、Lifecycle、前后端渲染，甚至直接借鉴的 Flux。除此之外，React 的函数式编程思想、增量更新、兼容性良好的事件体系也让我们非常向往。借着前端全栈开发的契机，我们开始考虑基于 React 进行组件化 3.0 的建设。</p><h4 id="NPM-Reduce"><a href="#NPM-Reduce" class="headerlink" title="NPM + Reduce"></a>NPM + Reduce</h4><p>NPM + Reduce 构成了我们新的资源管理方案，其中：</p><ul><li>NPM 负责组件的发布和安装。可以认为是“分”的过程，粒度越小，重用的可能性越大</li><li>Reduce 负责将页面资源进行打包。可以认为是“合”的过程，让浏览器更快地加载</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><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">smart-box/</span><br><span class="line">├── package.json    <span class="comment"># 组件包元信息</span></span><br><span class="line">├── smart-box.jsx   <span class="comment"># React Component</span></span><br><span class="line">├── smart-box.scss  <span class="comment"># 样式</span></span><br><span class="line">└── <span class="built_in">test</span></span><br><span class="line">    └── main.js     <span class="comment"># 测试</span></span><br></pre></td></tr></table></figure><p>NPM 默认只支持 js 文件的管理，我们对 NPM 中的 package.json 进行了扩展，增加了 style 字段，以使打包工具 Reduce 也能够对 css 和 css 中引用的 image、font 进行识别和处理：</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">&#123;</span><br><span class="line">    <span class="string">&quot;style&quot;</span>: <span class="string">&quot;./smart-box.scss&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只要在页面中 require 了 smart-box，经过 Reduce 打包后，js、css 甚至图片、字体，都会出现在浏览器中。</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">var</span> SmartBox = <span class="built_in">require</span>(<span class="string">&#x27;@mtfe/smart-box&#x27;</span>);</span><br><span class="line"><span class="comment">// 页面</span></span><br><span class="line"><span class="keyword">var</span> IndexPage = React.createClass(&#123;</span><br><span class="line">    <span class="attr">render</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> (</span><br><span class="line">            <span class="xml"><span class="tag">&lt;<span class="name">Header</span>&gt;</span></span></span><br><span class="line"><span class="xml">                <span class="tag">&lt;<span class="name">SmartBox</span> <span class="attr">keyword</span>=<span class="string">&#123;</span> <span class="attr">this.props.keyword</span> &#125; /&gt;</span></span></span><br><span class="line"><span class="xml">            <span class="tag">&lt;/<span class="name">Header</span>&gt;</span></span></span><br><span class="line">            ...</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="built_in">module</span>.exports = IndexPage;</span><br></pre></td></tr></table></figure><p>整体思路和组件化 1.0 如出一辙，却又那么不同。</p><h4 id="Turbo"><a href="#Turbo" class="headerlink" title="Turbo"></a>Turbo</h4><p>单单解决分发和打包的问题还不够，业务开发过程如果变得繁琐、难以 Debug、性能低下的话，恐怕不会受到工程师欢迎。</p><p>为了解决这些问题，我们在 Node 框架的基础上，提供了一系列中间件和开发工具，逐步构建对组件友好的前端工程化方案 Turbo。主要有：</p><ul><li>支持前后端同构渲染，让用户更早看到内容</li><li>简化 Flux 流程，数据流更加清晰易维护</li><li>引入 ImmutableJS，保证 Store 以外的数据不可变</li><li>采用 cursor 机制，保证数据修改/获取同步</li><li>支持 Hot Module Replacement，改进开发流自动化</li></ul><p>通过这些改进，一线工程师可以方便的使用各种组件，专注在业务本身上。开发框架层面的支持也反过来促进了组件化的发展，大家更乐于使用一系列组件来构建页面功能。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>发现痛点、分析调研、应用改进的解决问题思路在组件化开发实践中不断运用。历经三个大版本的演进，组件化开发模式有效缓解了业务发展带来的复杂度提升的压力，并培养工程师具备小而美的工程思想，形成共建共享的良好氛围。毫无疑问，组件化这种“分而治之”的思想将会长久地影响和促进前端开发模式。我们现在已经准备好，迎接新的机遇和挑战，用技术的不断革新提升工程师的幸福感。</p>]]></content>
    
    
    <summary type="html">&lt;h3 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h3&gt;&lt;p&gt;一位计算机前辈曾说过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Controlling complexity is the essence of computer programming.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随着前端开发复杂度的日益提升，组件化开发应运而生，并随着 FIS、React 等优秀框架的出现遍地开花。这一过程同样发生在美团，面临业务规模的快速发展和工程师团队的不断扩张，我们历经引入组件化解决资源整合问题、逐步增强组件功能促进开发效率、重新打造新一代组件化方案适应全栈开发和共享共建等阶段，努力“controlling complexity”。本文将介绍我们组件化开发的实践过程。&lt;/p&gt;
&lt;h3 id=&quot;组件化-1-0：资源重组&quot;&gt;&lt;a href=&quot;#组件化-1-0：资源重组&quot; class=&quot;headerlink&quot; title=&quot;组件化 1.0：资源重组&quot;&gt;&lt;/a&gt;组件化 1.0：资源重组&lt;/h3&gt;&lt;p&gt;在美团早期，前端资源是按照页面或者类似业务页面集合的形式进行组织的。例如 order.js 对应订单相关页面的交互，account.css 对应账户相关页面的样式。这种方式在过去的较长一段时间内，持续支撑了整个项目的正常推进，功勋卓著。&lt;/p&gt;
&lt;center&gt;&lt;img src=&quot;/images/frontend-component-practice/legacy-flow.png&quot; alt=&quot;legacy-flow&quot; width=&quot;500&quot;&gt;&lt;/center&gt;


&lt;p&gt;随着业务规模的增加和开发团队的扩张，这套机制逐渐显示出它的一些不足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;资源冗余&lt;/strong&gt;
页面的逐渐增加，交互的逐渐复杂化，导致对应的 css 和 js 都有大幅度增长，进而出现为了依赖某个 js 中的一个函数，需要加载整个模块，或者为了使用某个 css 中的部分样式依赖整个 css，冗余资源较多&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对应关系不直观&lt;/strong&gt;
没有显而易见的对应规则，导致的一个问题是修改某个业务模块的 css 或者 js 时，几乎只能依靠 grep。靠人来维护页面模块 html、css 和 js 之间的依赖关系，容易犯错，常常出现内容已经删除但是 css 或 js 还存在的问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;难于单元测试&lt;/strong&gt;
以页面为最小粒度进行资源整合，不同功能的业务模块相互影响，复杂度太高，自动化测试难以推进&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;2013 年开始，在调研了 FIS、BEM 等方案之后，结合美团开发框架的实际，我们初步实现了一套轻量级的组件化开发方案。主要的改进是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以页面功能组件为单位聚合前端资源&lt;/li&gt;
&lt;li&gt;自动加载符合约定的 css、js 资源&lt;/li&gt;
&lt;li&gt;将业务数据到渲染数据的转换过程独立出来&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;&lt;img src=&quot;/images/frontend-component-practice/component-flow.png&quot; alt=&quot;component-flow&quot; width=&quot;500&quot;&gt;&lt;/center&gt;

&lt;p&gt;举例来说，美团顶部的搜索框就被实现为一个组件。&lt;/p&gt;
&lt;center&gt;&lt;img src=&quot;/images/frontend-component-practice/smart-box.png&quot; alt=&quot;smart-box&quot; width=&quot;500&quot;&gt;&lt;/center&gt;</summary>
    
    
    
    
    <category term="Frontend" scheme="https://springuper.github.io/tags/Frontend/"/>
    
    <category term="component" scheme="https://springuper.github.io/tags/component/"/>
    
    <category term="reduce" scheme="https://springuper.github.io/tags/reduce/"/>
    
    <category term="turbo" scheme="https://springuper.github.io/tags/turbo/"/>
    
  </entry>
  
  <entry>
    <title>剖析 Promise 之基础篇</title>
    <link href="https://springuper.github.io/promise-insight/"/>
    <id>https://springuper.github.io/promise-insight/</id>
    <published>2014-05-08T22:16:00.000Z</published>
    <updated>2026-10-03T09:20:43.753Z</updated>
    
    <content type="html"><![CDATA[<p>随着浏览器端异步操作的复杂程度日益增加，以及以 Evented I/O 为核心思想的 NodeJS 的火爆，Promise、Async 等异步操作封装由于解决了异步编程上面临的诸多挑战，得到了飞速发展。本文旨在剖析 Promise 的内部机制，从实现原理层面深入探讨，从而达到“知其然且知其所以然”，在使用 Promise 上更加熟练自如。如果你还不太了解 Promise，推荐阅读下 <a href="https://www.promisejs.org/">promisejs.org</a> 的介绍。</p><h2 id="是什么"><a href="#是什么" class="headerlink" title="是什么"></a>是什么</h2><p>Promise 是一种对异步操作的封装，可以通过独立的接口添加在异步操作执行成功、失败时执行的方法。主流的规范是 <a href="http://promisesaplus.com/">Promises/A+</a>。</p><p>Promise 较通常的回调、事件/消息，在处理异步操作时具有显著的优势。其中最为重要的一点是：Promise 在语义上代表了异步操作的主体。这种准确、清晰的定位极大推动了它在编程中的普及，因为具有单一职责，而且将份内事做到极致的事物总是具有病毒式的传染力。分离输入输出参数、错误冒泡、串行/并行控制流等特性都成为 Promise 横扫异步操作编程领域的重要砝码，以至于 ES6 都将其收录，并已在 Chrome、Firefox 等现代浏览器中实现。</p><h2 id="内部机制"><a href="#内部机制" class="headerlink" title="内部机制"></a>内部机制</h2><p>自从看到 Promise 的 API，我对它的实现就充满了深深的好奇，一直有心窥其究竟。接下来，将首先从最简单的基础实现开始，由浅入深的逐步探索，剖析每一个 feature 后面的故事。</p><p>为了让语言上更加准确和简练，本文做如下约定：</p><ul><li>Promise：代表由 Promises/A+ 规范所定义的异步操作封装方式；</li><li>promise：代表一个 Promise 实例。</li></ul><h3 id="基础实现"><a href="#基础实现" class="headerlink" title="基础实现"></a>基础实现</h3><p>为了增加代入感，本文从最为基础的一个应用实例开始探索：通过异步请求获取用户id，然后做一些处理。在平时大家都是习惯用回调或者事件来处理，下面我们看下 Promise 的处理方式：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例1</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getUserId</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span> (<span class="params">resolve</span>) </span>&#123;</span><br><span class="line"><span class="comment">// 异步请求</span></span><br><span class="line">Y.io(<span class="string">&#x27;/userid&#x27;</span>, &#123;</span><br><span class="line"><span class="attr">on</span>: &#123;</span><br><span class="line"><span class="attr">success</span>: <span class="function"><span class="keyword">function</span> (<span class="params">id, res</span>) </span>&#123;</span><br><span class="line">resolve(<span class="built_in">JSON</span>.parse(res).id);</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><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getUserId().then(<span class="function"><span class="keyword">function</span> (<span class="params">id</span>) </span>&#123;</span><br><span class="line"><span class="comment">// do sth with id</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><a href="http://jsbin.com/kebigicu/1/embed?js,console">JS Bin</a></p><span id="more"></span><p><code>getUserId</code> 方法返回一个 promise，可以通过它的 <code>then</code> 方法注册在 promise 异步操作成功时执行的回调。自然、表意的 API，用起来十分顺手。</p><p>满足这样一种使用场景的 Promise 是如何构建的呢？其实并不复杂，下面给出最基础的实现：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Promise</span>(<span class="params">fn</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> value = <span class="literal">null</span>,</span><br><span class="line">    deferreds = [];</span><br><span class="line">        </span><br><span class="line">    <span class="built_in">this</span>.then = <span class="function"><span class="keyword">function</span> (<span class="params">onFulfilled</span>) </span>&#123;</span><br><span class="line">        deferreds.push(onFulfilled);</span><br><span class="line">    &#125;;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">resolve</span>(<span class="params">value</span>) </span>&#123;</span><br><span class="line">    deferreds.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">    deferred(value);</span><br><span class="line">    &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fn(resolve);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码很短，逻辑也非常清晰：</p><ul><li>调用<code>then</code>方法，将想要在 Promise 异步操作成功时执行的回调放入 <code>deferreds</code> 队列；</li><li>创建 Promise 实例时传入函数被赋予一个函数类型的参数，即 <code>resolve</code>，用以在合适的时机触发异步操作成功。真正执行的操作是将 <code>deferreds</code> 队列中的回调一一执行；</li><li><code>resolve</code> 接收一个参数，即异步操作返回的结果，方便回调使用。</li></ul><p>有时需要注册多个回调，如果能够支持 jQuery 那样的链式操作就好了！事实上，这很容易：</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></pre></td><td class="code"><pre><span class="line"><span class="built_in">this</span>.then = <span class="function"><span class="keyword">function</span> (<span class="params">onFulfilled</span>) </span>&#123;</span><br><span class="line">    deferreds.push(onFulfilled);</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例2</span></span><br><span class="line"></span><br><span class="line">getUserId().then(<span class="function"><span class="keyword">function</span> (<span class="params">id</span>) </span>&#123;</span><br><span class="line"><span class="comment">// do sth with id</span></span><br><span class="line">&#125;).then(<span class="function"><span class="keyword">function</span> (<span class="params">id</span>) </span>&#123;</span><br><span class="line"><span class="comment">// do sth else with id</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><a href="http://jsbin.com/fedukaso/2/edit?js,console">JS Bin</a></p><h3 id="延时"><a href="#延时" class="headerlink" title="延时"></a>延时</h3><p>如果 promise 是同步代码，<code>resolve</code> 会先于 <code>then</code> 执行，这时 <code>deferreds</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><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">// 例3</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getUserId</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span> (<span class="params">resolve</span>) </span>&#123;</span><br><span class="line">resolve(<span class="number">9876</span>);</span><br><span class="line">&#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getUserId().then(<span class="function"><span class="keyword">function</span> (<span class="params">id</span>) </span>&#123;</span><br><span class="line"><span class="comment">// do sth with id</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><a href="http://jsbin.com/fenopelo/3/edit?js,console">JS Bin</a></p><p>此外，Promises/A+ 规范明确要求回调需要通过异步方式执行，用以保证一致可靠的执行顺序。为解决这两个问题，可以通过 <code>setTimeout</code> 将 <code>resolve</code> 中执行回调的逻辑放置到 JS 任务队列末尾：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">resolve</span>(<span class="params">value</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        deferreds.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">        deferred(value);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="引入状态"><a href="#引入状态" class="headerlink" title="引入状态"></a>引入状态</h3><p>Hmm，好像存在一点问题：如果 Promise 异步操作已经成功，之后调用 <code>then</code> 注册的回调再也不会执行了，而这是不符合我们预期的。</p><p>解决这个问题，需要引入规范中所说的 States，即每个 Promise 存在三个互斥状态：pending、fulfilled、rejected，它们之间的关系是：</p><center><img title="states flow" src="/images/promise-states-flow.png" /></center><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="function"><span class="keyword">function</span> <span class="title">Promise</span>(<span class="params">fn</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> state = <span class="string">&#x27;pending&#x27;</span>,</span><br><span class="line">    value = <span class="literal">null</span>,</span><br><span class="line">    deferreds = [];</span><br><span class="line">        </span><br><span class="line">    <span class="built_in">this</span>.then = <span class="function"><span class="keyword">function</span> (<span class="params">onFulfilled</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (state === <span class="string">&#x27;pending&#x27;</span>) &#123;</span><br><span class="line">        deferreds.push(onFulfilled);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        onFulfilled(value);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">    &#125;;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">resolve</span>(<span class="params">newValue</span>) </span>&#123;</span><br><span class="line">        value = newValue;</span><br><span class="line">    state = <span class="string">&#x27;fulfilled&#x27;</span>;</span><br><span class="line">        <span class="built_in">setTimeout</span>(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            deferreds.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">                deferred(value);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fn(resolve);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="http://jsbin.com/lamewijo/2/edit?js,console">JS Bin</a></p><p><code>resolve</code> 执行时，会将状态设置为 fulfilled，在此之后调用 <code>then</code> 添加的新回调，都会立即执行。</p><p>似乎少了点什么，哦，是的，没有任何地方将 state 设为 rejected，这个问题稍后会聊，方便聚焦在核心代码上。</p><h3 id="串行-Promise"><a href="#串行-Promise" class="headerlink" title="串行 Promise"></a>串行 Promise</h3><p>在这一小节，将要探索的是 Promise 的 Killer Feature：<strong>串行 Promise</strong>，这是最为有趣也最为神秘的一个功能。</p><p>串行 Promise 是指在当前 promise 达到 fulfilled 状态后，即开始进行下一个 promise（后邻 promise）。例如获取用户 id 后，再根据用户 id 获取用户手机号等其他信息，这样的场景比比皆是：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例4</span></span><br><span class="line"></span><br><span class="line">getUserId()</span><br><span class="line">    .then(getUserMobileById)</span><br><span class="line">    .then(<span class="function"><span class="keyword">function</span> (<span class="params">mobile</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// do sth with mobile</span></span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getUserMobileById</span>(<span class="params">id</span>) </span>&#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span> (<span class="params">resolve</span>) </span>&#123;</span><br><span class="line">    Y.io(<span class="string">&#x27;/usermobile/&#x27;</span> + id, &#123;</span><br><span class="line">        <span class="attr">on</span>: &#123;</span><br><span class="line">            <span class="attr">success</span>: <span class="function"><span class="keyword">function</span> (<span class="params">i, o</span>) </span>&#123;</span><br><span class="line">                resolve(<span class="built_in">JSON</span>.parse(o).mobile);</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><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="http://jsbin.com/pibicefe/2/edit?js,console">JS Bin</a></p><p>这个 feature 实现的难点在于：如何衔接当前 promise 和后邻 promise。</p><p>首先对 <code>then</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><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="built_in">this</span>.then = <span class="function"><span class="keyword">function</span> (<span class="params">onFulfilled</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span> (<span class="params">resolve</span>) </span>&#123;</span><br><span class="line">        handle(&#123;</span><br><span class="line">            <span class="attr">onFulfilled</span>: onFulfilled || <span class="literal">null</span>,</span><br><span class="line">            <span class="attr">resolve</span>: resolve</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="function"><span class="keyword">function</span> <span class="title">handle</span>(<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (state === <span class="string">&#x27;pending&#x27;</span>) &#123;</span><br><span class="line">        deferreds.push(deferred);</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="keyword">var</span> ret = deferred.onFulfilled(value);</span><br><span class="line">    deferred.resolve(ret);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>then</code> 方法改变很多，这是一段暗藏玄机的代码：</p><ul><li><code>then</code> 方法中，创建了一个新的 Promise 实例，并作为返回值，这类 promise，权且称作 bridge promise。这是串行 Promise 的基础。另外，因为返回类型一致，之前的链式执行仍然被支持；</li><li><code>handle</code> 方法是当前 promise 的内部方法。这一点很重要，看不懂的童鞋可以去补充下闭包的知识。<code>then</code> 方法传入的形参 <code>onFullfilled</code>，以及创建新 Promise 实例时传入的 <code>resolve</code> 均被压入当前 promise 的 <code>deferreds</code> 队列中。所谓“巧妇难为无米之炊”，而这，正是衔接当前 promise 与后邻 promise 的“米”之所在。</li></ul><p>新增的 <code>handle</code> 方法，相比改造之前的 <code>then</code> 方法，仅增加了一行代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">deferred.resolve(ret);</span><br></pre></td></tr></table></figure><p>这意味着当前 promise 异步操作成功后执行 <code>handle</code> 方法时，先执行 <code>onFulfilled</code> 方法，然后将其返回值作为实参执行 <code>resolve</code> 方法，而这标志着后邻 promise 异步操作成功，<strong>接力</strong>工作就这样完成啦！</p><p>以例 2 代码为例，串行 Promise 执行流如下：</p><center><img title="promise series flow" src="/images/promise-series-simple-flow.png" /></center><p>这就是所谓的串行 Promise？当然不是，这些改造只是为了为最后的冲刺做铺垫，它们在重构底层实现的同时，兼容了本文之前讨论的所有功能。接下来，画龙点睛之笔–最后一个方法 <code>resolve</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><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="function"><span class="keyword">function</span> <span class="title">resolve</span>(<span class="params">newValue</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (newValue &amp;&amp; (<span class="keyword">typeof</span> newValue === <span class="string">&#x27;object&#x27;</span> || <span class="keyword">typeof</span> newValue === <span class="string">&#x27;function&#x27;</span>)) &#123;</span><br><span class="line">        <span class="keyword">var</span> then = newValue.then;</span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">typeof</span> then === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">            then.call(newValue, resolve);</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">    state = <span class="string">&#x27;fulfilled&#x27;</span>;</span><br><span class="line">    value = newValue;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        deferreds.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">            handle(deferred);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>啊哈，<code>resolve</code> 方法现在支持传入的参数是一个 Promise 实例了！以例 4 为例，执行步骤如下：</p><ol><li><code>getUserId</code> 生成的 promise （简称 <code>getUserId</code> promise）异步操作成功，执行其内部方法 <code>resolve</code>，传入的参数正是异步操作的结果 <code>userid</code>；</li><li>调用 <code>handle</code> 方法处理 <code>deferreds</code> 队列中的回调：<code>getUserMobileById</code> 方法，生成新的 promise（简称 <code>getUserMobileById</code> promise）；</li><li>执行之前由 <code>getUserId</code> promise 的 <code>then</code> 方法生成的 bridge promise 的 <code>resolve</code> 方法，传入参数为 <code>getUserMobileById</code> promise。这种情况下，会将该 <code>resolve</code> 方法传入 <code>getUserMobileById</code> promise 的 <code>then</code> 方法中，并直接返回；</li><li>在 <code>getUserMobileById</code> promise 异步操作成功时，执行其 <code>deferreds</code> 中的回调：<code>getUserId</code> bridge promise 的 <code>resolve</code> 方法；</li><li>最后，执行 <code>getUserId</code> bridge promise 的后邻 promise 的 <code>deferreds</code> 中的回调</li></ol><p>上述步骤实在有些复杂，主要原因是 bridge promise 的引入。不过正是得益于此，注册一个返回值也是 promise 的回调，从而实现异步操作串行的机制才得以实现。</p><p>一图胜千言，下图描述了例 4 的 Promise 执行流：</p><center><img title="promise series flow" src="/images/promise-series-flow.png" /></center><h3 id="失败处理"><a href="#失败处理" class="headerlink" title="失败处理"></a>失败处理</h3><p>本节处理之前遗留的 rejected 状态问题。在异步操作失败时，标记其状态为 rejected，并执行注册的失败回调：</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">// 例5</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getUserId</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span> (<span class="params">resolve, reject</span>) </span>&#123;</span><br><span class="line"><span class="comment">// 异步请求</span></span><br><span class="line">Y.io(<span class="string">&#x27;/userid/1&#x27;</span>, &#123;</span><br><span class="line"><span class="attr">on</span>: &#123;</span><br><span class="line"><span class="attr">success</span>: <span class="function"><span class="keyword">function</span> (<span class="params">id, res</span>) </span>&#123;</span><br><span class="line">                    <span class="keyword">var</span> o = <span class="built_in">JSON</span>.parse(res);</span><br><span class="line">                    <span class="keyword">if</span> (o.status === <span class="number">1</span>) &#123;</span><br><span class="line">                        resolve(o.id);</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">                        reject(o.errorMsg);</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><span class="line">&#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getUserId().then(<span class="function"><span class="keyword">function</span> (<span class="params">id</span>) </span>&#123;</span><br><span class="line"><span class="comment">// do sth with id</span></span><br><span class="line">&#125;, <span class="function"><span class="keyword">function</span> (<span class="params">error</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(error);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><a href="http://jsbin.com/padolebi/2/edit?js,console">JS Bin</a></p><p>有了之前处理 fulfilled 状态的经验，支持错误处理变得很容易。毫无疑问的是，这将加倍 code base，在注册回调、处理状态变更上都要加入新的逻辑：</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><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="function"><span class="keyword">function</span> <span class="title">Promise</span>(<span class="params">fn</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> state = <span class="string">&#x27;pending&#x27;</span>,</span><br><span class="line">    value = <span class="literal">null</span>,</span><br><span class="line">        deferreds = [];</span><br><span class="line">        </span><br><span class="line">    <span class="built_in">this</span>.then = <span class="function"><span class="keyword">function</span> (<span class="params">onFulfilled, onRejected</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span> (<span class="params">resolve, reject</span>) </span>&#123;</span><br><span class="line">            handle(&#123;</span><br><span class="line">                <span class="attr">onFulfilled</span>: onFulfilled || <span class="literal">null</span>,</span><br><span class="line">                <span class="attr">onRejected</span>: onRejected || <span class="literal">null</span>,</span><br><span class="line">                <span class="attr">resolve</span>: resolve,</span><br><span class="line">                <span class="attr">reject</span>: reject</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="function"><span class="keyword">function</span> <span class="title">handle</span>(<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (state === <span class="string">&#x27;pending&#x27;</span>) &#123;</span><br><span class="line">            deferreds.push(deferred);</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="keyword">var</span> cb = state === <span class="string">&#x27;fulfilled&#x27;</span> ? deferred.onFulfilled : deferred.onRejected,</span><br><span class="line">            ret;</span><br><span class="line">        <span class="keyword">if</span> (cb === <span class="literal">null</span>) &#123;</span><br><span class="line">            cb = state === <span class="string">&#x27;fulfilled&#x27;</span> ? deferred.resolve : deferred.reject;</span><br><span class="line">            cb(value);</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        ret = cb(value);</span><br><span class="line">        deferred.resolve(ret);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">resolve</span>(<span class="params">newValue</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (newValue &amp;&amp; (<span class="keyword">typeof</span> newValue === <span class="string">&#x27;object&#x27;</span> || <span class="keyword">typeof</span> newValue === <span class="string">&#x27;function&#x27;</span>)) &#123;</span><br><span class="line">            <span class="keyword">var</span> then = newValue.then;</span><br><span class="line">            <span class="keyword">if</span> (<span class="keyword">typeof</span> then === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">                then.call(newValue, resolve, reject);</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">        state = <span class="string">&#x27;fulfilled&#x27;</span>;</span><br><span class="line">        value = newValue;</span><br><span class="line">        finale();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">reject</span>(<span class="params">reason</span>) </span>&#123;</span><br><span class="line">        state = <span class="string">&#x27;rejected&#x27;</span>;</span><br><span class="line">        value = reason;</span><br><span class="line">        finale();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">finale</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">setTimeout</span>(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            deferreds.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">                handle(deferred);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fn(resolve, reject);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>增加了新的 <code>reject</code> 方法，供异步操作失败时调用，同时抽出了 <code>resolve</code> 和 <code>reject</code> 共用的部分，形成 <code>finale</code> 方法。</p><p><strong>错误冒泡</strong>是上述代码已经支持，且非常实用的一个特性。在 <code>handle</code> 中发现没有指定异步操作失败的回调时，会直接将 bridge promise 设为 rejected 状态，如此达成执行后续失败回调的效果。这有利于简化串行 Promise 的失败处理成本，因为一组异步操作往往会对应一个实际功能，失败处理方法通常是一致的：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例6</span></span><br><span class="line"></span><br><span class="line">getUserId()</span><br><span class="line">    .then(getUserMobileById)</span><br><span class="line">    .then(<span class="function"><span class="keyword">function</span> (<span class="params">mobile</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// do sth else with mobile</span></span><br><span class="line">    &#125;, <span class="function"><span class="keyword">function</span> (<span class="params">error</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// getUserId或者getUerMobileById时出现的错误</span></span><br><span class="line">        <span class="built_in">console</span>.log(error);</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure><p><a href="http://jsbin.com/joqamiko/3/edit?js,console">JS Bin</a></p><h3 id="异常处理"><a href="#异常处理" class="headerlink" title="异常处理"></a>异常处理</h3><p>如果在执行成功回调、失败回调时代码出错怎么办？对于这类异常，可以使用 <code>try-catch</code> 捕获错误，并将 bridge promise 设为 rejected 状态。<code>handle</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><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="function"><span class="keyword">function</span> <span class="title">handle</span>(<span class="params">deferred</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (state === <span class="string">&#x27;pending&#x27;</span>) &#123;</span><br><span class="line">        deferreds.push(deferred);</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="keyword">var</span> cb = state === <span class="string">&#x27;fulfilled&#x27;</span> ? deferred.onFulfilled : deferred.onRejected,</span><br><span class="line">        ret;</span><br><span class="line">    <span class="keyword">if</span> (cb === <span class="literal">null</span>) &#123;</span><br><span class="line">        cb = state === <span class="string">&#x27;fulfilled&#x27;</span> ? deferred.resolve : deferred.reject;</span><br><span class="line">        cb(value);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        ret = cb(value);</span><br><span class="line">        deferred.resolve(ret);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">        deferred.reject(e);</span><br><span class="line">    &#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果在异步操作中，多次执行 <code>resolve</code> 或者 <code>reject</code> 会重复处理后续回调，可以通过内置一个标志位解决。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Promise 作为异步操作的一种 Monad，魔幻一般的 API 让人难以驾驭。本文从简单的基础实现起步，逐步添加内置状态、串行、失败处理/失败冒泡、异常处理等关键特性，最终达到类似由 Forbes Lindesay 所完成的一个<a href="https://github.com/then/promise/blob/master/core.js">简单 Promise 实现</a>的效果。在让我本人更加深刻理解 Promise 魔力之源的同时，希望为各位更加熟练的使用这一实用工具带来一些帮助。</p><h2 id="预告"><a href="#预告" class="headerlink" title="预告"></a>预告</h2><p>下一篇关于 Promise 的文章中，将重点关注高阶应用的一些场景，例如并行 Promise、基于 Promise 的异步操作流封装、语法糖等。敬请期待。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.promisejs.org/">Introduction to Promises</a></li><li><a href="http://mattgreer.org/articles/promises-in-wicked-detail/">JavaScript Promises … In Wicked Detail</a></li><li><a href="http://sean.voisen.org/blog/2013/10/intro-monads-maybe/">A Gentle Introduction to Monads in JavaScript</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;随着浏览器端异步操作的复杂程度日益增加，以及以 Evented I/O 为核心思想的 NodeJS 的火爆，Promise、Async 等异步操作封装由于解决了异步编程上面临的诸多挑战，得到了飞速发展。本文旨在剖析 Promise 的内部机制，从实现原理层面深入探讨，从而达到“知其然且知其所以然”，在使用 Promise 上更加熟练自如。如果你还不太了解 Promise，推荐阅读下 &lt;a href=&quot;https://www.promisejs.org/&quot;&gt;promisejs.org&lt;/a&gt; 的介绍。&lt;/p&gt;
&lt;h2 id=&quot;是什么&quot;&gt;&lt;a href=&quot;#是什么&quot; class=&quot;headerlink&quot; title=&quot;是什么&quot;&gt;&lt;/a&gt;是什么&lt;/h2&gt;&lt;p&gt;Promise 是一种对异步操作的封装，可以通过独立的接口添加在异步操作执行成功、失败时执行的方法。主流的规范是 &lt;a href=&quot;http://promisesaplus.com/&quot;&gt;Promises/A+&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;Promise 较通常的回调、事件/消息，在处理异步操作时具有显著的优势。其中最为重要的一点是：Promise 在语义上代表了异步操作的主体。这种准确、清晰的定位极大推动了它在编程中的普及，因为具有单一职责，而且将份内事做到极致的事物总是具有病毒式的传染力。分离输入输出参数、错误冒泡、串行/并行控制流等特性都成为 Promise 横扫异步操作编程领域的重要砝码，以至于 ES6 都将其收录，并已在 Chrome、Firefox 等现代浏览器中实现。&lt;/p&gt;
&lt;h2 id=&quot;内部机制&quot;&gt;&lt;a href=&quot;#内部机制&quot; class=&quot;headerlink&quot; title=&quot;内部机制&quot;&gt;&lt;/a&gt;内部机制&lt;/h2&gt;&lt;p&gt;自从看到 Promise 的 API，我对它的实现就充满了深深的好奇，一直有心窥其究竟。接下来，将首先从最简单的基础实现开始，由浅入深的逐步探索，剖析每一个 feature 后面的故事。&lt;/p&gt;
&lt;p&gt;为了让语言上更加准确和简练，本文做如下约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Promise：代表由 Promises/A+ 规范所定义的异步操作封装方式；&lt;/li&gt;
&lt;li&gt;promise：代表一个 Promise 实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;基础实现&quot;&gt;&lt;a href=&quot;#基础实现&quot; class=&quot;headerlink&quot; title=&quot;基础实现&quot;&gt;&lt;/a&gt;基础实现&lt;/h3&gt;&lt;p&gt;为了增加代入感，本文从最为基础的一个应用实例开始探索：通过异步请求获取用户id，然后做一些处理。在平时大家都是习惯用回调或者事件来处理，下面我们看下 Promise 的处理方式：&lt;/p&gt;
&lt;figure class=&quot;highlight javascript&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// 例1&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;getUserId&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;built_in&quot;&gt;Promise&lt;/span&gt;(&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;resolve&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		&lt;span class=&quot;comment&quot;&gt;// 异步请求&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		Y.io(&lt;span class=&quot;string&quot;&gt;&amp;#x27;/userid&amp;#x27;&lt;/span&gt;, &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;			&lt;span class=&quot;attr&quot;&gt;on&lt;/span&gt;: &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;				&lt;span class=&quot;attr&quot;&gt;success&lt;/span&gt;: &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;id, res&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;					resolve(&lt;span class=&quot;built_in&quot;&gt;JSON&lt;/span&gt;.parse(res).id);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;				&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;			&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;getUserId().then(&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;id&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;comment&quot;&gt;// do sth with id&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;http://jsbin.com/kebigicu/1/embed?js,console&quot;&gt;JS Bin&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="Promise" scheme="https://springuper.github.io/tags/Promise/"/>
    
    <category term="Monad" scheme="https://springuper.github.io/tags/Monad/"/>
    
  </entry>
  
  <entry>
    <title>YUI经验谈-自定义事件默认行为</title>
    <link href="https://springuper.github.io/yui3-event-default-action/"/>
    <id>https://springuper.github.io/yui3-event-default-action/</id>
    <published>2014-02-14T10:00:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>纵观主流JS库和框架，YUI在自定义事件方面做的尤为出色。如果需要挑出一个代表性的feature，那么非<strong>事件默认行为</strong>莫属。</p><h2 id="是什么"><a href="#是什么" class="headerlink" title="是什么"></a>是什么</h2><p>YUI自定义事件在总体上模仿了DOM事件的设计思想。DOM中的一些事件是有默认行为的，详细见<a href="https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-flow-default-cancel">DOM3 Event - Default actions and cancelable events</a>一节。简单来说，所谓默认行为，是指该事件在通常情况下所表现出来的动作，例如：</p><ul><li>一个链接节点的<code>click</code>事件，默认行为是转向该链接href属性对应的地址</li><li>表单的submit事件，默认行为是将表单包含的数据提交给表单的action</li></ul><p>说通常情况下，是因为有时开发者会在事件的回调函数中调用</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">e.preventDefault();</span><br></pre></td></tr></table></figure><p>来阻止默认行为的发生。</p><span id="more"></span><center><img alt="Event Default Action" src="/images/event-default-action.png" /></center><p>YUI自定义事件遵循了同样的思路，甚至API也和DOM完全一致。</p><h2 id="有啥用"><a href="#有啥用" class="headerlink" title="有啥用"></a>有啥用</h2><p>事件默认行为，本质上，是一种<strong>管理事件和行为的对应关系</strong>的机制。这种机制既不像回调那样死板，也不像消息那样开放。通过将通用处理逻辑作为事件默认行为，满足常见需求的同时，为定制化需求提供了一定开放性，整体上更加灵活。</p><p>在DOM事件中，和默认行为相关的场景并不少见：</p><ul><li>监听到链接的<code>click</code>事件时，在链接地址中加入追踪参数，利用默认行为跳转到新地址</li><li>阻止表单<code>submit</code>事件默认行为，改为异步提交表单，提供更好的用户体验</li></ul><p>在自定义事件的应用中，也会遇到一些类似的例子。例如：</p><ul><li>注册时，有一些邮箱虽然是可用的，但对于EDM不给力，在这种情况下，阻止表单项验证成功的默认行为，展示建议用户使用其它邮箱的提示</li><li>表单验证组件在检查表单项失败后触发<code>failure</code>事件，对应的默认行为是在表单项下方显示错误信息。这样的处理在大部分情况下是完全OK的。不过有一天，交互设计师在一个特定场景下提出所有提示都应该放在整个表单顶部，得益于这种灵活的机制，实现这种定制化逻辑十分轻松</li><li>字符计数插件在输入变化时会默认更新字符数提示。在评价内容中，有更复杂的提示逻辑和展示效果，这时阻止默认行为，实现定制化内容即可</li></ul><h2 id="怎么用"><a href="#怎么用" class="headerlink" title="怎么用"></a>怎么用</h2><p>下面以表单项验证组件为例，展示如何使用事件默认行为。</p><p>首先创建<code>FieldValidator</code>组件，并使其具备<code>EventTarget</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="keyword">var</span> FieldValidator = <span class="function"><span class="keyword">function</span> (<span class="params">ndField, validateFn</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> instance = <span class="built_in">this</span>;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br><span class="line">Y.augment(FieldValidator, Y.EventTarget);</span><br></pre></td></tr></table></figure><p>使用<code>publish</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">var</span> FieldValidator = <span class="function"><span class="keyword">function</span> (<span class="params">ndField, validateFn</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 声明检查成功事件，设置默认行为</span></span><br><span class="line">    instance.publish(<span class="string">&#x27;success&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">emitFacade</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">defaultFn</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            e.field.next(<span class="string">&#x27;.tip&#x27;</span>).setHTML(<span class="string">&#x27;ok&#x27;</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">    instance.publish(<span class="string">&#x27;failure&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">emitFacade</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">defaultFn</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            e.field.next(<span class="string">&#x27;.tip&#x27;</span>).setHTML(<span class="string">&#x27;error&#x27;</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>接下来注册表单项的<code>focus</code>、<code>blur</code>事件，在<code>blur</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> FieldValidator = <span class="function"><span class="keyword">function</span> (<span class="params">ndField, validateFn</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    ndField.on(&#123;</span><br><span class="line">        <span class="attr">focus</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            ndField.next(<span class="string">&#x27;.tip&#x27;</span>).setHTML(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">blur</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            <span class="keyword">if</span> (validateFn(<span class="built_in">this</span>.get(<span class="string">&#x27;value&#x27;</span>))) &#123;</span><br><span class="line">                <span class="comment">// 检查通过，触发检查成功事件</span></span><br><span class="line">                instance.fire(<span class="string">&#x27;success&#x27;</span>, &#123; <span class="attr">field</span>: ndField &#125;);</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">                instance.fire(<span class="string">&#x27;failure&#x27;</span>, &#123; <span class="attr">field</span>: ndField &#125;);</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 检查邮箱</span></span><br><span class="line"><span class="keyword">new</span> FieldValidator(Y.one(<span class="string">&#x27;[name=&quot;email&quot;]&#x27;</span>), <span class="function"><span class="keyword">function</span> (<span class="params">value</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="regexp">/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/</span>.test(value);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>一切看起来都很美，直到有一天你接到一个需求：Yahoo邮箱在检查通过时需要展示EDM不给力的提示，这时候默认行为就可以来拯救你了：</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">var</span> validator = <span class="keyword">new</span> FieldValidator(Y.one(<span class="string">&#x27;[name=&quot;email&quot;]&#x27;</span>), <span class="function"><span class="keyword">function</span> (<span class="params">value</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="regexp">/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/</span>.test(value);</span><br><span class="line">&#125;);</span><br><span class="line">validator.on(<span class="string">&#x27;success&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (e.field.get(<span class="string">&#x27;value&#x27;</span>).indexOf(<span class="string">&#x27;@yahoo.com&#x27;</span>) !== -<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// 阻止默认行为</span></span><br><span class="line">        e.preventDefault();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 定制化行为</span></span><br><span class="line">e.field.next(<span class="string">&#x27;.tip&#x27;</span>).setHTML(<span class="string">&#x27;换个邮箱吧，yahoo.com邮箱收不到优惠通知哦&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>在<code>success</code>事件的回调中，通过阻止默认行为，不再执行提示内容为OK的默认逻辑，而是切换成判断雅虎邮箱，并给出特定提示的定制化逻辑。</p><p>完整代码展示，请移步<a href="http://jsfiddle.net/springuper/h4XAY/">JSFiddle</a>。</p><h2 id="要注意"><a href="#要注意" class="headerlink" title="要注意"></a>要注意</h2><p>一个好的idea，最容易被滥用。默认行为不是万能药，只适合一些这样的场景：</p><ul><li>需要通过事件进行消息广播。如果callback就可以解决问题，那么明智之举是使用callback</li><li>存在定制化需求的预期，即有些情况下需要中止默认行为的发生，换之以定制化行为</li></ul><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-flow-default-cancel">DOM3 Event - Default actions and cancelable events</a></li><li><a href="http://yuilibrary.com/yui/docs/event-custom/index.html">YUI EventTarget</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;纵观主流JS库和框架，YUI在自定义事件方面做的尤为出色。如果需要挑出一个代表性的feature，那么非&lt;strong&gt;事件默认行为&lt;/strong&gt;莫属。&lt;/p&gt;
&lt;h2 id=&quot;是什么&quot;&gt;&lt;a href=&quot;#是什么&quot; class=&quot;headerlink&quot; title=&quot;是什么&quot;&gt;&lt;/a&gt;是什么&lt;/h2&gt;&lt;p&gt;YUI自定义事件在总体上模仿了DOM事件的设计思想。DOM中的一些事件是有默认行为的，详细见&lt;a href=&quot;https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-flow-default-cancel&quot;&gt;DOM3 Event - Default actions and cancelable events&lt;/a&gt;一节。简单来说，所谓默认行为，是指该事件在通常情况下所表现出来的动作，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个链接节点的&lt;code&gt;click&lt;/code&gt;事件，默认行为是转向该链接href属性对应的地址&lt;/li&gt;
&lt;li&gt;表单的submit事件，默认行为是将表单包含的数据提交给表单的action&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说通常情况下，是因为有时开发者会在事件的回调函数中调用&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;e.preventDefault();&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;来阻止默认行为的发生。&lt;/p&gt;</summary>
    
    
    
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="Event" scheme="https://springuper.github.io/tags/Event/"/>
    
  </entry>
  
  <entry>
    <title>[译]优秀与伟大程序员的区别</title>
    <link href="https://springuper.github.io/diff-great-software-engineer-from-a-good/"/>
    <id>https://springuper.github.io/diff-great-software-engineer-from-a-good/</id>
    <published>2013-10-19T20:39:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<div class="preface">    <p>在程序员的职业生涯中，总有一些理念让你眼前一亮，犹如夜空中一颗颗明亮的星，指引你不断前行。Quora上Russel Simmons的这个答案就是曾让我深受触动的三个理念，翻译出来给E文一般的童鞋。E文好的童鞋请移步，原著更加准确生动些。</p>    <em>原文链接：<a href="http://www.quora.com/Software-Engineering/What-distinguishes-a-good-software-engineer-from-a-great-one/answer/Russel-Simmons?srid=mo0" target="_blank">Software Engineering: What distinguishes a good software engineer from a great one?</a></em></div><p>我并不打算给出一个全面的答案，不过我已注意到几个伟大程序员具备却不常被提及的特质：</p><ul><li><strong>能够平衡实效与完美</strong>。伟大工程师具备进行娴熟、快速而粗略的hack，和设计优雅、精炼、健壮的解决方案的两种能力，以及，根据给定问题选择恰当方式的智慧。一些稍普通的程序员们看起来缺乏对问题关键细节的极致关注，另一些人则太过坚持完美主义。</li><li><strong>不排斥调试代码和修复bug</strong>。平庸的程序员害怕、厌恶调试，甚至对自己的代码也是如此。伟大程序员会以丘吉尔般顽强的精神潜心钻研。如果问题被证明不在他们的代码中，伟大程序员会有些不愉快，他们会最终找出问题所在。</li><li><strong>合理质疑</strong>。优秀程序员会找到看似可行的解决办法并一直沿用。伟大程序员则倾向于质疑自己的代码，直到测试完备。而这一过程往往需要大量的数据分析和系统管理。一般程序员可能看到一个似乎没什么危害的细微异常，然后忽略掉了它。如果换作伟大程序员，他们会怀疑这可能是一个更大问题的线索，并投入精力研究透彻。伟大程序员乐于做更多的交叉校验和完备性检查，通过这种方式发现隐含的问题。</li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;div class=&quot;preface&quot;&gt;
    &lt;p&gt;在程序员的职业生涯中，总有一些理念让你眼前一亮，犹如夜空中一颗颗明亮的星，指引你不断前行。Quora上Russel</summary>
        
      
    
    
    
    
    <category term="Programming" scheme="https://springuper.github.io/tags/Programming/"/>
    
  </entry>
  
  <entry>
    <title>YUI3在美团的实践</title>
    <link href="https://springuper.github.io/yui3-practice/"/>
    <id>https://springuper.github.io/yui3-practice/</id>
    <published>2013-05-10T22:35:00.000Z</published>
    <updated>2026-10-03T09:20:43.755Z</updated>
    
    <content type="html"><![CDATA[<p>美团网在2010年引爆了团购行业，并在2012年销售额超过55亿，实现了全面盈利。在业务规模不断增长的背后，作为研发队伍中和用户最接近的前端团队承担着非常大的压力，比如用户量急剧上升带来的产品多样化，业务运营系统的界面交互日益复杂，代码膨胀造成维护成本增加等等。面对这些挑战，我们持续改进前端技术架构，在提升用户体验和工作效率的同时，成功支撑了美团业务的快速发展，这一切都得益于构建在YUI3框架之上稳定高效的前端代码。在应用YUI3的过程中，我们团队积累了一些经验，这里总结成篇，分享给大家。</p><h2 id="为什么选择YUI3"><a href="#为什么选择YUI3" class="headerlink" title="为什么选择YUI3"></a>为什么选择YUI3</h2><p>使用什么前端基础框架是建立前端团队最重要的技术决策之一。美团项目初期因为要加快开发进度，选择了当时团队最熟悉的YUI2（前框架时代杰出的类库），保证美团能够更快更早地上线，抢占市场先机。不久由于前端技术发展很快，YUI2的缺点逐渐凸显，例如开发方式落后、影响工作效率等等，于是我们开始考虑基础库的迁移。</p><p>经过一段时间对主流前端库、框架的反复考量，我们认为YUI3是最适合我们团队使用的基础框架。</p><p>首先，国内的开源框架及其社区刚开始起步，在代码质量、架构设计和理念创新上还难以跟YUI3比肩，所以基本排除在外。其次，国外像YUI3这样面向用户产品、文档丰富、扩展性良好的成熟框架屈指可数，例如ExtJS和Dojo则更适合业务复杂的传统企业级开发。最后，使用jQuery这种类库构建同YUI3一样强大的框架对创业团队来说并不可取，美团快速发展、竞争多变的业务特点决定了我们必须把主要精力放在更高一层的业务开发上，而不是去重复发明一个蹩脚的YUI。</p><p>YUI3成为最终选择有以下几个直接的原因：</p><ul><li>非常优秀，是真正的框架，真正的重型武器，具有强劲的持续开发能力，可以应对业务的快速发展。不管是规模不断增长的用户产品，还是交互日趋复杂的业务系统（美团有超过100个业务系统作全电子化的运营支撑），YUI3都游刃有余。</li><li>代码整齐规范，容易维护，适合有洁癖的工程师，同时能够显著提高团队协作时的开发效率。因为人手紧缺，后端工程师也需要参与前端开发，一致的代码风格使前后端配合轻松简单。</li><li>有出色的架构设计，是很好的框架范本，通过研究学习可以帮助工程师成长，培养良好的工程思维。人是美团最重要的产品。</li></ul><p>随着团队成长，我们最后引入了YUI3，在迁移过程中，遇到了很多技术上的和工程上的挑战，但是我们一直在前进，一直在行进中开火。从结果来看，YUI3为我们团队提供了先进生产力，为快速开发、快速部署、快速迭代提供了源源不断的力量。</p><p>YUI3的优秀主要表现在模块和组件框架的出色设计，下面我们着重介绍这两方面的一些实践经验。</p><span id="more"></span><h2 id="改变一切的模块"><a href="#改变一切的模块" class="headerlink" title="改变一切的模块"></a>改变一切的模块</h2><p>前端开发日益复杂化，代码组织成为一个显著的问题。受到后端代码普遍采用的模块机制启发，很多前端模块机制应运而生。目前比较著名的有CommonJS和AMD。但早在2008年8月13日，YUI3 Preview Release 1中就已经给出了YUI团队的解决方案，并在2009年9月29日YUI3正式版发布时定型。</p><p>以下是使用YUI3进行模块化开发的简单例子</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义模块</span></span><br><span class="line">YUI.add(<span class="string">&#x27;greeting&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">Y.sayHello = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;Hello, world!&#x27;</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">YUI().use(<span class="string">&#x27;greeting&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">Y.sayHello(); <span class="comment">// output &#x27;Hello, world!&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>模块的引入，使得更细粒度的按功能进行代码组织成为可能，也为方便的进行扩展和分层提供了基础，自底向上的彻底改变了YUI3。一套完整的模块机制，还包括解决关系依赖、自动加载的Loader和提高加载效率的Combo。</p><p>面对如此彻底的改变，我们需要解决很多挑战：</p><ul><li>如何将原来的功能划分为模块？</li><li>如何管理模块元信息？</li><li>如何高效的获取模块？</li></ul><h3 id="划分模块"><a href="#划分模块" class="headerlink" title="划分模块"></a>划分模块</h3><p>经过两年来不断的实践和总结，我们归纳了如下几条划分模块的原则：</p><ul><li><strong>抽象与应用脱离</strong>。更通用的功能放在更低的层级，应用层完全面向实际问题，在解决的过程中调用抽象出来的方法。</li><li><strong>职责单一</strong>。保持每个模块的足够简单和专一，方便维护和可持续开发。</li><li><strong>粒度得当</strong>。有了Combo，我们可以不必担心粒度太小，文件过多导致的速度问题。但是，从可维护的角度来考虑，粒度应该适当而不宜过小，避免海底捞针的情形出现。</li><li><strong>海纳百川</strong>。我们的模块体系应该是开放的，不符合YUI规范的第三方模块，可以借鉴整合进来，使我们的基础框架更加完善，更加性感。</li></ul><center>  <img alt="美团前端架构" src="/images/mt-fe-architecture.png" /></center><p>按照模块的层次划分，美团的JS框架可以分为四个层次：</p><ol><li><strong>最底层交给强悍的YUI3</strong>，为我们提供跨浏览器兼容的API和良好的框架设计。</li><li><strong>第二层是我们二次开发的核心方法、组件（Component）和控件（Widget）</strong>。现已独立为前端核心库，为美团所有系统提供前端支持。核心库的种子文件中定义了全局变量M，除了对YUI3进行封装的代码以外，还包含了对语言层面的扩展，以及一些基础工具类。核心库有一个非常重要的组成部分，就是我们功能丰富的控件集合，比如常用的自动完成、排序表格、气泡提示、对话框等基础控件。除了这些，核心库还包含了常用的基础组件、插件（Plugin）、扩展（Extension）以及单元测试代码。</li><li><strong>第三层包含各个系统的一些通用模块</strong>。例如www-base模块包含美团主站（www）的消息系统、用户行为追踪系统等通用功能。这一层更加接近应用。</li><li><strong>最上面一层，应用模块</strong>。这些模块的方法都是用来解决实际业务问题。例如www-deal用来处理美团主站所有deal相关功能的交互，finance-pay用来处理财务系统中付款相关的交互。一些零碎的应用方法我们放在对应系统的misc模块中，避免模块碎片化。</li></ol><p>这套框架仍在不断演变，以便更好的支撑业务需求。其中一个明显的方向是，在第二层和第三层之间，出现一个为了更好整合所有内部业务系统前端通用资源的中间层。</p><h3 id="管理模块元信息"><a href="#管理模块元信息" class="headerlink" title="管理模块元信息"></a>管理模块元信息</h3><p>模块元信息主要包括模块名称、路径、依赖关系等内容。其中最为重要的是依赖关系，这决定了有哪些模块需要加载。为了实现自动加载，需要将所有模块的元信息提供给YUI的Loader。</p><p>最初，为了更快的从YUI2迁移到YUI3，模块元信息放在PHP中进行维护。随着时间的推移，渐渐显示出很多弊端。首先，在定义模块的js文件中已经包含模块名称、依赖关系等信息，和PHP中内容重复。其次，这些元信息最终直接输出到html中，没有有效利用缓存。</p><p>随后，我们使用NodeJS开发了一系列脚本，收集所有模块元信息，保存为独立js文件，并实现了自动化。为了防止出错，在Git Hooks和上线脚本中都加入了校验过程。工程师需要做的，只是修改模块定义中的元信息。</p><p>最近一段时间，我们的精力主要放在两个方面：</p><ul><li><strong>自动生成依赖</strong>。随着模块粒度细化和模块数量的增长，依赖关系日益复杂，依靠人工配置经常出现过多依赖或过少依赖等问题。我们准备开发一套自动扫描模块引用API，并确定依赖关系的机制。</li><li><strong>自动打包依赖模块</strong>。如果在代码发布时，就已根据页面模块调用计算好所有依赖模块，并进行打包，可以避免引用全部模块元信息、Loader计算依赖等过程，提高网站性能。</li></ul><h3 id="Combo"><a href="#Combo" class="headerlink" title="Combo"></a>Combo</h3><p>Combo可以一次请求多个文件，能够有效解决多个模块加载带来的性能问题。Yahoo提供了Combo服务，但只能提供YUI3模块，而且速度在国内并不理想。为了提供更好的体验，让用户访问速度更快，我们最终考虑搭建自己的Combo服务，并把Combo发布到CDN上。</p><p>以下是一个Combo请求的例子：</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">http://c.meituan.net/combo/?f=mt-yui-core.v3.5.1.js;fecore/mt/js/base.js</span><br></pre></td></tr></table></figure><p>为了节约时间，我们最开始采用了开源的minify，经过一些修改和配置，就可以在生产和开发环境提供Combo服务。使用一段时间后，发现minify过于复杂，以至于添加一些定制功能相当困难。我们需要的只是简单的文件合并功能，在明确需求和开发量后，着手开发自己的Combo程序。从最初的仅支持文件合并，后来陆续添加了服务器／浏览器端缓存、文件集别名、调试模式、CSS图片相对路径转URI、错误日志等特性，全部代码仅有300多行。经过两年时间以及每天几千万PV的考验，服务一直非常稳定。</p><h2 id="灵活健壮的组件框架"><a href="#灵活健壮的组件框架" class="headerlink" title="灵活健壮的组件框架"></a>灵活健壮的组件框架</h2><p>YUI3之所以成为纯粹的框架，真正的原因在于提供了一套灵活、健壮的组件框架。借助这套框架，可以轻松的将业务场景进行解耦、分层，并持续的进行改进。通过不断的实践，我们越发认为这是YUI3的精髓所在。</p><p>从YUI3定义的开发范式和源代码中可以看出，YUI团队非常重视AOP（Aspect Oriented Programming）和OOP（Object Oriented Programming），这一点可以在接下来的介绍中有所体会。</p><h3 id="EventTarget、Attribute和Base"><a href="#EventTarget、Attribute和Base" class="headerlink" title="EventTarget、Attribute和Base"></a>EventTarget、Attribute和Base</h3><p>在介绍组件框架之前，有必要首先了解下EventTarget。YUI3创建了一套类似DOM事件的自定义事件体系，支持冒泡传播、默认行为等功能。EventTarget提供了操作自定义事件的接口，可以让任意一个对象拥有定义、监听、触发、注销自定义事件的功能。YUI组件框架中的所有类，以及在此框架之上开发的所有组件，都继承了EventTarget。</p><p>Attribute是组件框架中最底层的类，实现了数据和逻辑的完美解耦。为什么说是完美呢？存储在attribute（Attribute提供的数据存取接口）中的数据发生变化时，会触发相应的事件，为相关的逻辑处理提供了便捷的接口。从下面这个简单的例子可以感受到这一点：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 在name属性变化时，触发nameChange事件</span></span><br><span class="line"><span class="built_in">this</span>.on(<span class="string">&#x27;nameChange&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line"><span class="built_in">console</span>.log(e.newVal);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 修改name属性</span></span><br><span class="line"><span class="built_in">this</span>.set(<span class="string">&#x27;name&#x27;</span>, <span class="string">&#x27;meituan&#x27;</span>); <span class="comment">// output &#x27;meituan&#x27;</span></span><br></pre></td></tr></table></figure><p>实践中发现，妥善处理属性的分类非常重要。供实例进行操作的属性适合作为attribute，例如表单验证组件FormChecker的fields属性，方便应用层进行表单项的增删改。在类方法内部使用的一些属性可以作为私有属性，例如计时器、监听器句柄。供所有类的实例使用的一些常量适合作为类的静态属性，例如一些模板、样式类。</p><p>Base是组件框架的核心类。它模拟了C++、Java等语言的经典继承方式和生命周期管理，借助Attribute来实现数据与逻辑的分离，并提供扩展、插件支持，从而获得了良好的扩展性以及强大的可持续开发能力。YUI团队通过多年来对业务实践的抽象，最终演化而成一种开发范式，这，就是一切组件的基石——Base，实至名归。</p><p>依照这种范式，我们开发了一系列组件，例如之前提到的FormChecker，以及延迟加载器LazyLoader、地图的封装Map等。最显著的体会是，开发思路更为清晰，代码结构更有条理，维护变得简单轻松。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 构造方法</span></span><br><span class="line">FormChecker.prototype.initializer = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="keyword">var</span> form = <span class="built_in">this</span>.get(<span class="string">&#x27;form&#x27;</span>);</span><br><span class="line"><span class="built_in">this</span>._handle = form.on(<span class="string">&#x27;submit&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line"><span class="comment">// check fields</span></span><br><span class="line">&#125;);</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// 析构方法</span></span><br><span class="line">FormChecker.prototype.destructor = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="built_in">this</span>._handle.detach();</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">var</span> checker = <span class="keyword">new</span> FormChecker(&#123; <span class="attr">form</span>: Y.one(<span class="string">&#x27;#buy-form&#x27;</span>) &#125;);</span><br><span class="line"><span class="comment">// 销毁实例时，自动执行析构方法</span></span><br><span class="line">checker.destroy();</span><br></pre></td></tr></table></figure><h3 id="Extension和Plugin"><a href="#Extension和Plugin" class="headerlink" title="Extension和Plugin"></a>Extension和Plugin</h3><p>Extension（扩展）是为了解决多重继承，以一种类似组合的方式在类上添加功能的模式，它本身不能创建实例。这种设计非常像Ruby等语言中的Mixin。Plugin（插件）的作用是在对象上添加一些功能，这些功能也可以很方便的移除。</p><p>它们有什么区别呢？简单来说，Extension是在类上加一些功能，所有类的实例都拥有这些功能。Plugin只是在某些类的实例中添加功能。举两个典型的例子：一些节点需要使用动画效果，这个功能适合作为Plugin。气泡提示控件需要支持多种对齐方式，所有实例都需要此功能，因此使用YUI3的WidgetPositionAlign扩展。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 传统的函数方式实现动画</span></span><br><span class="line">Effect.fadeIn(nodeTip);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 插件方式实现动画</span></span><br><span class="line">nodeTip.plug(NodeEffect);</span><br><span class="line">nodeTip.effect.fadeIn();</span><br></pre></td></tr></table></figure><p>Extension和Plugin很好的解决了我们遇到的诸多功能重用问题。我们开发了提供全屏功能的WidgetFullScreen、自动对齐对话框的DialogAutoAlign等扩展，以及进行异步查询的AsyncSearch、提供动画效果的NodeEffect等插件。将这些偏重OOP的编程思想应用在前端开发中，比较深刻的体会是：有更多的概念清晰、定位明确的开发模式可以选择。</p><h3 id="Widget体系"><a href="#Widget体系" class="headerlink" title="Widget体系"></a>Widget体系</h3><p>Widget（控件）建立在Base之上，主要增加了UI层面的功能，例如<code>renderUI</code>、<code>bindUI</code>、<code>syncUI</code>等生命周期方法，<code>HTML_PARSER</code>等渐进增强功能，以及样式类、HTML结构和DOM事件的统一管理。Widget提供了控件开发的通用范式。</p><p>由于前端资源相对紧张，我们倾向于大量使用控件，尤其在业务系统这样更注重功能的场景。主要出于两点考虑：</p><ul><li><strong>减少不必要的重复劳动，提高产出</strong>。通过将交互、业务逻辑合理抽象，一次解决一类问题，One Shot One Kill。</li><li><strong>节约前端工程师资源</strong>。通过自动加载和初始化控件、封装简单易用的后端方法、制作Demo和使用手册等措施，降低使用门槛，后端工程师只需要知道参数的数据结构就可以轻松调用，提高了开发效率。</li></ul><p>以下是一个自动加载控件的例子</p><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">// 页面初始化时，会扫描所有带有data-widget属性的节点，自动加载对应控件，并根据data-params数据进行初始化</span><br><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;…&quot;</span> <span class="attr">data-widget</span>=<span class="string">&quot;bubbleTip&quot;</span> <span class="attr">data-params</span>=<span class="string">&#x27;&#123; &quot;tip&quot;: &quot;全新改版，支持随时退款&quot; &#125;&#x27;</span>&gt;</span>下载手机版<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></table></figure><p>目前，我们已经构建了一个包含近30个控件的Widget体系，为所有系统提供丰富、便捷、集成的解决方案。</p><h2 id="行进中开火"><a href="#行进中开火" class="headerlink" title="行进中开火"></a>行进中开火</h2><p>在整个YUI3的实践中，我们犯过很多错误，例如全局只有一个YUI实例、Combo的CSS图片依赖等等，但这些并没有成为放弃的理由。从今天回过头来看，YUI3带给我们团队的，不只是更高的开发效率、更好的可持续开发能力，还有它本身的设计思路、源码书写、辅助工具等诸多方面潜移默化的影响。这些回报的价值，比起较高的使用门槛、犯过的一些错误，要贵重百倍。</p><p>指导这一切的，是我们始终坚持的 <strong>“行进中开火”</strong>。在互联网这个高速发展的行业里，对于我们这种小规模的创业团队，一天不前进，就意味落后。做事不应该准备太多，一定要先做起来，然后发现不足并不断改进，宁可十年不将军，不可一日不拱卒。每天都做得更好一点，日积月累，我们才会在激烈的竞争中占据越来越大的优势。</p><p>YUI3并非完美，存在着学习成本高、对社区不够开放等问题。我们所做的更远非完美，但经过不断的尝试和经验的积累，已经渐渐摸索出一条明确的路线，并会坚持不懈的继续走下去。</p><p>关于YUI3和我们团队更多的信息请关注 <a href="http://fe.meituan.com/">http://fe.meituan.com</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;美团网在2010年引爆了团购行业，并在2012年销售额超过55亿，实现了全面盈利。在业务规模不断增长的背后，作为研发队伍中和用户最接近的前端团队承担着非常大的压力，比如用户量急剧上升带来的产品多样化，业务运营系统的界面交互日益复杂，代码膨胀造成维护成本增加等等。面对这些挑战，我们持续改进前端技术架构，在提升用户体验和工作效率的同时，成功支撑了美团业务的快速发展，这一切都得益于构建在YUI3框架之上稳定高效的前端代码。在应用YUI3的过程中，我们团队积累了一些经验，这里总结成篇，分享给大家。&lt;/p&gt;
&lt;h2 id=&quot;为什么选择YUI3&quot;&gt;&lt;a href=&quot;#为什么选择YUI3&quot; class=&quot;headerlink&quot; title=&quot;为什么选择YUI3&quot;&gt;&lt;/a&gt;为什么选择YUI3&lt;/h2&gt;&lt;p&gt;使用什么前端基础框架是建立前端团队最重要的技术决策之一。美团项目初期因为要加快开发进度，选择了当时团队最熟悉的YUI2（前框架时代杰出的类库），保证美团能够更快更早地上线，抢占市场先机。不久由于前端技术发展很快，YUI2的缺点逐渐凸显，例如开发方式落后、影响工作效率等等，于是我们开始考虑基础库的迁移。&lt;/p&gt;
&lt;p&gt;经过一段时间对主流前端库、框架的反复考量，我们认为YUI3是最适合我们团队使用的基础框架。&lt;/p&gt;
&lt;p&gt;首先，国内的开源框架及其社区刚开始起步，在代码质量、架构设计和理念创新上还难以跟YUI3比肩，所以基本排除在外。其次，国外像YUI3这样面向用户产品、文档丰富、扩展性良好的成熟框架屈指可数，例如ExtJS和Dojo则更适合业务复杂的传统企业级开发。最后，使用jQuery这种类库构建同YUI3一样强大的框架对创业团队来说并不可取，美团快速发展、竞争多变的业务特点决定了我们必须把主要精力放在更高一层的业务开发上，而不是去重复发明一个蹩脚的YUI。&lt;/p&gt;
&lt;p&gt;YUI3成为最终选择有以下几个直接的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非常优秀，是真正的框架，真正的重型武器，具有强劲的持续开发能力，可以应对业务的快速发展。不管是规模不断增长的用户产品，还是交互日趋复杂的业务系统（美团有超过100个业务系统作全电子化的运营支撑），YUI3都游刃有余。&lt;/li&gt;
&lt;li&gt;代码整齐规范，容易维护，适合有洁癖的工程师，同时能够显著提高团队协作时的开发效率。因为人手紧缺，后端工程师也需要参与前端开发，一致的代码风格使前后端配合轻松简单。&lt;/li&gt;
&lt;li&gt;有出色的架构设计，是很好的框架范本，通过研究学习可以帮助工程师成长，培养良好的工程思维。人是美团最重要的产品。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随着团队成长，我们最后引入了YUI3，在迁移过程中，遇到了很多技术上的和工程上的挑战，但是我们一直在前进，一直在行进中开火。从结果来看，YUI3为我们团队提供了先进生产力，为快速开发、快速部署、快速迭代提供了源源不断的力量。&lt;/p&gt;
&lt;p&gt;YUI3的优秀主要表现在模块和组件框架的出色设计，下面我们着重介绍这两方面的一些实践经验。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Framework" scheme="https://springuper.github.io/tags/Framework/"/>
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
  </entry>
  
  <entry>
    <title>YUI事件体系之Y.delegate</title>
    <link href="https://springuper.github.io/event-delegate/"/>
    <id>https://springuper.github.io/event-delegate/</id>
    <published>2013-04-05T23:23:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p><img src="../images/delegate.jpg" alt="relay baton"></p><p>在介绍了YUI自定义事件体系和对DOM事件的封装后，本篇文章重点阐述事件方面的一种常用技术——事件代理。事件代理（Event Delegation，又称事件委托）充分运用事件传播模型，用一种十分优雅的方式实现了批量节点事件监听。具体的原理和优点请移步zakas比较古老的一篇文章<a href="http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/">Event delegation in JavaScript</a>。事件代理在YUI中的实现为<code>Y.delegate</code>。</p><h2 id="基本用法"><a href="#基本用法" class="headerlink" title="基本用法"></a>基本用法</h2><p>为方便讨论，约定以下名称：</p><ul><li>代理节点：实际监听事件的节点。在事件传播到此节点时判断是否符合代理条件，符合则执行回调函数。</li><li>被代理节点：希望监听事件的节点。如果不采用事件代理，那么应该直接监听这些节点的事件。</li><li>目标节点：事件发生的目标节点，即<code>event.target</code>。</li></ul><p>三者的层次关系从内到外依次为：目标节点 &lt;= 被代理节点 &lt;= 代理节点。</p><span id="more"></span><p>假设html为：</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;http://google.com&quot;</span>&gt;</span>google<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">a</span> <span class="attr">name</span>=<span class="string">&quot;facebook&quot;</span> <span class="attr">href</span>=<span class="string">&quot;http://facebook.com&quot;</span>&gt;</span>facebook<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">a</span> <span class="attr">name</span>=<span class="string">&quot;twitter&quot;</span> <span class="attr">href</span>=<span class="string">&quot;http://twitter.com&quot;</span>&gt;</span>twitter<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span><br></pre></td></tr></table></figure><p>先来看下<code>Y.delegate</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="comment">// 例1</span></span><br><span class="line"><span class="comment">// API: Y.delegate(type, fn, el, filter)</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-delegate&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> handle = Y.delegate(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        e.halt();</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="built_in">this</span>.get(<span class="string">&#x27;tagName&#x27;</span>) + <span class="string">&#x27; is clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;ul&#x27;</span>, <span class="string">&#x27;a&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    Y.delegate(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="built_in">this</span>.get(<span class="string">&#x27;tagName&#x27;</span>) + <span class="string">&#x27; is clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;ul&#x27;</span>, <span class="string">&#x27;li&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click first anchor</span></span><br><span class="line">    <span class="comment">// output &#x27;A is clicked&#x27;</span></span><br><span class="line">    <span class="comment">// output &#x27;LI is clicked&#x27;</span></span><br><span class="line"></span><br><span class="line">    handle.detach();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click first anchor again</span></span><br><span class="line">    <span class="comment">// output &#x27;LI is clicked&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>可以看出，回调函数中的this指向的是使用<code>Y.Node</code>包装的被代理节点。</p><p>YUI3新加入的<code>Y.Node</code>对象也封装了delegate，例1更常见的实现如下：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例2</span></span><br><span class="line"><span class="comment">// API: Y.Node.prototype.delegate(type, fn, filter)</span></span><br><span class="line">YUI().use(<span class="string">&#x27;node-event-delegate&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> ndList = Y.one(<span class="string">&#x27;ul&#x27;</span>),</span><br><span class="line">        handle;</span><br><span class="line"></span><br><span class="line">    handle = ndList.delegate(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        e.halt();</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="built_in">this</span>.get(<span class="string">&#x27;tagName&#x27;</span>) + <span class="string">&#x27; is clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;a&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    ndList.delegate(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="built_in">this</span>.get(<span class="string">&#x27;tagName&#x27;</span>) + <span class="string">&#x27; is clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;li&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click first anchor</span></span><br><span class="line">    <span class="comment">// output &#x27;A is clicked&#x27;</span></span><br><span class="line">    <span class="comment">// output &#x27;LI is clicked&#x27;</span></span><br><span class="line"></span><br><span class="line">    handle.detach();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click first anchor again</span></span><br><span class="line">    <span class="comment">// output &#x27;LI is clicked&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>筛选条件除了例1中使用的selector外，还支持函数：</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"><span class="comment">// 例3</span></span><br><span class="line">YUI().use(<span class="string">&#x27;node-event-delegate&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> ndList = Y.one(<span class="string">&#x27;ul&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    ndList.delegate(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        e.halt();</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="built_in">this</span>.get(<span class="string">&#x27;tagName&#x27;</span>) + <span class="string">&#x27; is clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="function"><span class="keyword">function</span> (<span class="params">nd, e</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> nd.get(<span class="string">&#x27;name&#x27;</span>) &amp;&amp;</span><br><span class="line">               e.target.get(<span class="string">&#x27;tagName&#x27;</span>).toLowerCase() === <span class="string">&#x27;a&#x27;</span>;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click the first anchor</span></span><br><span class="line">    <span class="comment">// no output</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// click the second anchor</span></span><br><span class="line">    <span class="comment">// output &#x27;LI is clicked&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><p>接下来，让我们看看YUI的内部实现吧。</p><p>注：为了更容易的看懂代码的核心，我做了适当的简化，感兴趣的朋友可以去看未删节的<a href="https://github.com/yui/yui3/blob/v3.9.1/src/event/js/delegate.js">源码</a>。</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> toArray = Y.Array,</span><br><span class="line">    isString = Y.Lang.isString;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">delegate</span>(<span class="params">type, fn, el, filter</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> container = isString(el) ? Y.Selector.query(el, <span class="literal">null</span>, <span class="literal">true</span>) : el,</span><br><span class="line">        handle;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监听代理节点的type事件</span></span><br><span class="line">    handle = Y.Event._attach([type, fn, container], &#123; <span class="attr">facade</span>: <span class="literal">false</span> &#125;);</span><br><span class="line">    handle.sub.filter  = filter;</span><br><span class="line">    <span class="comment">// 改写事件触发回调函数</span></span><br><span class="line">    handle.sub._notify = delegate.notifySub;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> handle;</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">delegate.notifySub = <span class="function"><span class="keyword">function</span> (<span class="params">thisObj, args, ce</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 计算符合filter的被代理节点集合</span></span><br><span class="line">    <span class="keyword">var</span> currentTarget = delegate._applyFilter(<span class="built_in">this</span>.filter, args, ce),</span><br><span class="line">        e, i, len, ret;</span><br><span class="line">    <span class="keyword">if</span> (!currentTarget) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    currentTarget = toArray(currentTarget);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 生成事件对象</span></span><br><span class="line">    e = args[<span class="number">0</span>] = <span class="keyword">new</span> Y.DOMEventFacade(args[<span class="number">0</span>], ce.el, ce);</span><br><span class="line">    <span class="comment">// 将代理节点保存在事件对象container属性上，方便回调函数调用</span></span><br><span class="line">    e.container = Y.one(ce.el);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>, len = currentTarget.length; i &lt; len &amp;&amp; !e.stopped; ++i) &#123;</span><br><span class="line">        <span class="comment">// 将被代理节点保存在事件对象currentTarget属性上</span></span><br><span class="line">        e.currentTarget = Y.one(currentTarget[i]);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 回调函数中的this指向被代理节点</span></span><br><span class="line">        ret = <span class="built_in">this</span>.fn.apply(e.currentTarget, args);</span><br><span class="line">        <span class="keyword">if</span> (ret === <span class="literal">false</span>) <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 计算符合filter的被代理节点集合</span></span><br><span class="line">delegate._applyFilter = <span class="function"><span class="keyword">function</span> (<span class="params">filter, args, ce</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> e         = args[<span class="number">0</span>],</span><br><span class="line">        container = ce.el,</span><br><span class="line">        target    = e.target || e.srcElement,</span><br><span class="line">        match     = [],</span><br><span class="line">        isContainer = <span class="literal">false</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> (target.nodeType === <span class="number">3</span>) &#123;</span><br><span class="line">        target = target.parentNode;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// filter是selector</span></span><br><span class="line">    <span class="keyword">if</span> (isString(filter)) &#123;</span><br><span class="line">        <span class="keyword">while</span> (target) &#123;</span><br><span class="line">            isContainer = (target === container);</span><br><span class="line">            <span class="comment">// 测试target是否符合selector filter</span></span><br><span class="line">            <span class="keyword">if</span> (Y.Selector.test(target, filter, (isContainer ? <span class="literal">null</span> : container))) &#123;</span><br><span class="line">                match.push(target);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (isContainer) <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">            target = target.parentNode;</span><br><span class="line">        &#125;</span><br><span class="line">    <span class="comment">// filter是function</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 将target节点作为function filter的第一个参数，</span></span><br><span class="line">        <span class="comment">// 第二个参数为事件对象</span></span><br><span class="line">        args.unshift(Y.one(target));</span><br><span class="line">        args[<span class="number">1</span>] = <span class="keyword">new</span> Y.DOMEventFacade(e, container, ce);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (target) &#123;</span><br><span class="line">            <span class="comment">// function filter中this指向target</span></span><br><span class="line">            <span class="keyword">if</span> (filter.apply(args[<span class="number">0</span>], args)) &#123;</span><br><span class="line">                match.push(target);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (target === container) <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 更新target</span></span><br><span class="line">            target = target.parentNode;</span><br><span class="line">            args[<span class="number">0</span>] = Y.one(target);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 恢复args对象</span></span><br><span class="line">        args[<span class="number">1</span>] = e;</span><br><span class="line">        args.shift();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> match.length &lt;= <span class="number">1</span> ? match[<span class="number">0</span>] : match;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">Y.delegate = Y.Event.delegate = delegate;</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><p><code>Y.delegate</code>支持同时代理多种类型的事件，调用方式有如下两种：</p><ul><li>Y.delegate({ typeA: fnA, typeB: fnB }, el, filter)</li><li>Y.delegate([typeA, typeB], fn, el, filter)</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><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">// 例4</span></span><br><span class="line">YUI().use(<span class="string">&#x27;node-event-delegate&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> ndList = Y.one(<span class="string">&#x27;ul&#x27;</span>);</span><br><span class="line">    ndList.delegate(&#123;</span><br><span class="line">        <span class="attr">click</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            e.halt();</span><br><span class="line">            <span class="built_in">console</span>.log(e.type);</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">dblclick</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            e.halt();</span><br><span class="line">            <span class="built_in">console</span>.log(e.type);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, <span class="string">&#x27;li&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// double click the first anchor</span></span><br><span class="line">    <span class="comment">// output &#x27;click&#x27;</span></span><br><span class="line">    <span class="comment">// output &#x27;click&#x27;</span></span><br><span class="line">    <span class="comment">// output &#x27;dblclick&#x27;</span></span><br><span class="line"></span><br><span class="line">    ndList.delegate([<span class="string">&#x27;click&#x27;</span>, <span class="string">&#x27;dblclick&#x27;</span>], <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        e.halt();</span><br><span class="line">        <span class="built_in">console</span>.log(e.type);</span><br><span class="line">    &#125;, <span class="string">&#x27;li&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// double click the first anchor</span></span><br><span class="line">    <span class="comment">// output &#x27;click&#x27;</span></span><br><span class="line">    <span class="comment">// output &#x27;click&#x27;</span></span><br><span class="line">    <span class="comment">// output &#x27;dblclick&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="修改回调函数this，传递数据"><a href="#修改回调函数this，传递数据" class="headerlink" title="修改回调函数this，传递数据"></a>修改回调函数this，传递数据</h3><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">// 例5</span></span><br><span class="line">YUI().use(<span class="string">&#x27;node-event-delegate&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    Y.one(<span class="string">&#x27;ul&#x27;</span>).delegate(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e, args</span>) </span>&#123;</span><br><span class="line">        e.halt();</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="built_in">this</span> === <span class="built_in">document</span>.body);</span><br><span class="line">        <span class="built_in">console</span>.log(args.data);</span><br><span class="line">    &#125;, <span class="string">&#x27;li&#x27;</span>, <span class="built_in">document</span>.body, &#123; <span class="attr">data</span>: <span class="string">&#x27;data&#x27;</span> &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click the first anchor</span></span><br><span class="line">    <span class="comment">// output &#x27;true&#x27;</span></span><br><span class="line">    <span class="comment">// output &#x27;data&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="focus、blur事件的代理"><a href="#focus、blur事件的代理" class="headerlink" title="focus、blur事件的代理"></a>focus、blur事件的代理</h3><p>在DOM规范中，诸如focus、blur、load、unload、resize等事件是不冒泡的，因为这类事件只限定在某个节点上触发。但focus和blur事件有些特殊，在表单交互方面十分常用，如果能够支持冒泡，那么通过事件代理可以减少很多监听事件的操作。滑稽的是，focusin、focusout两个类似的事件却支持冒泡。</p><p>令人欣喜的是，YUI通过定义新的DOM事件实现了focus、blur事件的代理，awesome！</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="comment">// 例6</span></span><br><span class="line">YUI().use(<span class="string">&#x27;node-event-delegate&#x27;</span>, <span class="string">&#x27;event-focus&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    Y.one(<span class="string">&#x27;form&#x27;</span>).delegate(&#123;</span><br><span class="line">        <span class="attr">focus</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            <span class="comment">// 清除错误提示</span></span><br><span class="line">            clearErr(<span class="built_in">this</span>);</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">blur</span>: <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">            <span class="comment">// 如果内容为空，则提示错误信息</span></span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.get(<span class="string">&#x27;value&#x27;</span>) === <span class="string">&#x27;&#x27;</span>) showErr(<span class="built_in">this</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, <span class="string">&#x27;input&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">clearErr</span>(<span class="params">nd</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> ndErr = nd.next(<span class="string">&#x27;.error&#x27;</span>);</span><br><span class="line">        <span class="keyword">if</span> (ndErr) ndErr.hide().setHTML(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">showErr</span>(<span class="params">nd</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> ndErr = nd.next(<span class="string">&#x27;.error&#x27;</span>);</span><br><span class="line">        <span class="keyword">if</span> (!ndErr) &#123;</span><br><span class="line">            ndErr = Y.Node.create(<span class="string">&#x27;&lt;span class=&quot;error&quot;&gt;&lt;/span&gt;&#x27;</span>);</span><br><span class="line">            nd.insert(ndErr, <span class="string">&#x27;after&#x27;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ndErr.show().setHTML(<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><h2 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</h2><p>所有示例代码均在<a href="https://github.com/springuper/yuianalyser/tree/master/event">GitHub</a>。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://yuilibrary.com/yui/docs/event/#delegation">YUILibrary-UserGuides-Event Delegation</a></li><li><a href="http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/">Event delegation in JavaScript</a></li><li><a href="https://github.com/yui/yui3/blob/v3.9.1/src/event/js/delegate.js">YUILibrary-delegate</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;../images/delegate.jpg&quot; alt=&quot;relay baton&quot;&gt;&lt;/p&gt;
&lt;p&gt;在介绍了YUI自定义事件体系和对DOM事件的封装后，本篇文章重点阐述事件方面的一种常用技术——事件代理。事件代理（Event Delegation，又称事件委托）充分运用事件传播模型，用一种十分优雅的方式实现了批量节点事件监听。具体的原理和优点请移步zakas比较古老的一篇文章&lt;a href=&quot;http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/&quot;&gt;Event delegation in JavaScript&lt;/a&gt;。事件代理在YUI中的实现为&lt;code&gt;Y.delegate&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;基本用法&quot;&gt;&lt;a href=&quot;#基本用法&quot; class=&quot;headerlink&quot; title=&quot;基本用法&quot;&gt;&lt;/a&gt;基本用法&lt;/h2&gt;&lt;p&gt;为方便讨论，约定以下名称：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代理节点：实际监听事件的节点。在事件传播到此节点时判断是否符合代理条件，符合则执行回调函数。&lt;/li&gt;
&lt;li&gt;被代理节点：希望监听事件的节点。如果不采用事件代理，那么应该直接监听这些节点的事件。&lt;/li&gt;
&lt;li&gt;目标节点：事件发生的目标节点，即&lt;code&gt;event.target&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三者的层次关系从内到外依次为：目标节点 &amp;lt;= 被代理节点 &amp;lt;= 代理节点。&lt;/p&gt;</summary>
    
    
    
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="Y.delegate" scheme="https://springuper.github.io/tags/Y-delegate/"/>
    
  </entry>
  
  <entry>
    <title>YUI事件体系之Y.Event</title>
    <link href="https://springuper.github.io/event/"/>
    <id>https://springuper.github.io/event/</id>
    <published>2013-01-20T14:12:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p><img src="../images/mouse.png" alt="mouse event"></p><p>在介绍了由<code>Y.Do</code>、<code>Y.CustomEvent</code>、<code>Y.EventTarget</code>构建的自定义事件体系后，本篇文章将为大家介绍建立在这一体系之上，YUI对DOM事件的封装——<code>Y.Event</code>。</p><h2 id="Y-DOMEventFacade"><a href="#Y-DOMEventFacade" class="headerlink" title="Y.DOMEventFacade"></a>Y.DOMEventFacade</h2><p>众所周知，浏览器之间存在大量的不兼容问题，在事件方面尤其如此。<code>Y.DOMEventFacade</code>主要用来处理DOM事件对象的浏览器兼容问题，提供跨浏览器的简洁接口。事实上，我们常在<code>Y.one(&#39;.selector&#39;).on(&#39;click&#39;, function (e) &#123;&#125;)</code>中使用的<code>e</code>就是<code>Y.DOMEventFacade</code>的实例。</p><p>具体来说，兼容处理的属性主要有：</p><ul><li>target，专门处理了target为文本节点的情况，统一为元素节点，方便操作</li><li>relativeTarget，关联目标节点，在mouseover/mouseout等事件中设置</li><li>keyCode/charCode等输入信息</li><li>pageX/clientX等位置信息</li></ul><p>兼容处理的方法主要有：</p><ul><li>stopPropagation/stopImmediatePropagation，不支持停止立即传播时，仅能在YUI层面模拟，不会阻止通过原生方法添加的同层回调，即，在YUI监听过el的click事件后，又通过<code>el.addEventListener(&#39;click&#39;, nativeCallback)</code>监听，如果在YUI的回调中调用<code>e.stopImmediatePropagation</code>的话，<code>nativeCallback</code>仍然会执行</li><li>preventDefault</li></ul><p>另外，为了方便同时停止传播和阻止默认行为，YUI还提供了<code>halt</code>方法。</p><span id="more"></span><p>我们来简单分析下<code>Y.DOMEventFacade</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><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 为简化代码，省略了专门针对未实现DOM2 Events规范浏览器中的事件对象兼容性处理（代码在event-base-ie模块）</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> DOMEventFacade = <span class="function"><span class="keyword">function</span> (<span class="params">ev, currentTarget, wrapper</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>._event = ev;</span><br><span class="line">    <span class="built_in">this</span>._currentTarget = currentTarget;</span><br><span class="line">    <span class="built_in">this</span>._wrapper = wrapper || &#123;&#125;;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">this</span>.init();</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">DOMEventFacade.resolve = <span class="function"><span class="keyword">function</span> (<span class="params">n</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!n) <span class="keyword">return</span> n;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 如果是TEXT_NODE，则取其父节点</span></span><br><span class="line">        <span class="keyword">if</span> (n &amp;&amp; <span class="number">3</span> === n.nodeType) n = n.parentNode;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</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="keyword">return</span> Y.one(n);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">Y.extend(DOMEventFacade, <span class="built_in">Object</span>, &#123;</span><br><span class="line">    <span class="comment">// 初始化。主要处理事件对象的浏览器兼容问题</span></span><br><span class="line">    <span class="attr">init</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> e = <span class="built_in">this</span>._event,</span><br><span class="line">            resolve = DOMEventFacade.resolve;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 此处省略对key和dimension的兼容性处理</span></span><br><span class="line"></span><br><span class="line">        <span class="built_in">this</span>.type = e.type;</span><br><span class="line">        <span class="built_in">this</span>.target = resolve(e.target);</span><br><span class="line">        <span class="built_in">this</span>.currentTarget = resolve(<span class="built_in">this</span>._currentTarget);</span><br><span class="line">        <span class="built_in">this</span>.relatedTarget = resolve(e.relatedTarget);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 停止传播</span></span><br><span class="line">    <span class="attr">stopPropagation</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">this</span>._event.stopPropagation();</span><br><span class="line">        <span class="built_in">this</span>._wrapper.stopped = <span class="number">1</span>;</span><br><span class="line">        <span class="built_in">this</span>.stopped = <span class="number">1</span>;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 立即停止传播，不处理同一节点的后续回调</span></span><br><span class="line">    <span class="attr">stopImmediatePropagation</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> e = <span class="built_in">this</span>._event;</span><br><span class="line">        <span class="keyword">if</span> (e.stopImmediatePropagation) &#123;</span><br><span class="line">            <span class="comment">// 原生事件对象支持立即停止传播</span></span><br><span class="line">            e.stopImmediatePropagation();</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="built_in">this</span>.stopPropagation();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">this</span>._wrapper.stopped = <span class="number">2</span>;</span><br><span class="line">        <span class="built_in">this</span>.stopped = <span class="number">2</span>;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 阻止默认事件</span></span><br><span class="line">    <span class="attr">preventDefault</span>: <span class="function"><span class="keyword">function</span> (<span class="params">returnValue</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> e = <span class="built_in">this</span>._event;</span><br><span class="line">        e.preventDefault();</span><br><span class="line">        e.returnValue = returnValue || <span class="literal">false</span>;</span><br><span class="line">        <span class="built_in">this</span>._wrapper.prevented = <span class="number">1</span>;</span><br><span class="line">        <span class="built_in">this</span>.prevented = <span class="number">1</span>;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 中止事件，包括停止传播和阻止默认事件</span></span><br><span class="line">    <span class="attr">halt</span>: <span class="function"><span class="keyword">function</span> (<span class="params">immediate</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (immediate) &#123;</span><br><span class="line">            <span class="built_in">this</span>.stopImmediatePropagation();</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="built_in">this</span>.stopPropagation();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">this</span>.preventDefault();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">Y.DOMEventFacade = DOMEventFacade;</span><br></pre></td></tr></table></figure><h2 id="Y-Event"><a href="#Y-Event" class="headerlink" title="Y.Event"></a>Y.Event</h2><p><code>Y.Event</code>的主要作用是提供添加、注销DOM事件监听的接口。和我们通常的理解不一样的是，它并不是一个类，而是一个简单的对象。</p><p>YUI对DOM事件监听的处理思路大体是：根据节点、事件类型，创建一个<code>Y.CustomEvent</code>对象cewrapper。然后通过原生方法注册事件监听，回调执行<code>cewrapper.fire</code>方法。所有通过YUI添加的事件监听，都注册cewrapper上，从而实现对DOM事件的包装。</p><p>我们先来看下如何使用<code>Y.Event</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例1</span></span><br><span class="line">YUI().use(<span class="string">&#x27;selector&#x27;</span>, <span class="string">&#x27;event-base&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;#btn-one clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-one&#x27;</span>);</span><br><span class="line">    Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;#btn-one clicked again&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-one&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click #btn-one</span></span><br><span class="line">    <span class="comment">// output &#x27;#btn-one clicked&#x27;, &#x27;#btn-one clicked again&#x27;</span></span><br><span class="line"></span><br><span class="line">    Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;#btn-one clicked&#x27;</span>);</span><br><span class="line">        e.stopPropagation();</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-two&#x27;</span>);</span><br><span class="line">    Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;#btn-one clicked again&#x27;</span>);</span><br><span class="line">        e.stopImmediatePropagation();</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-two&#x27;</span>);</span><br><span class="line">    Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;#btn-one clicked the third time&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-two&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click #btn-two</span></span><br><span class="line">    <span class="comment">// output &#x27;#btn-two clicked&#x27;, &#x27;#btn-two clicked again&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>当然，也可以使用<code>Y.Event</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例2</span></span><br><span class="line">YUI().use(<span class="string">&#x27;selector&#x27;</span>, <span class="string">&#x27;event-base&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> countThree = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">var</span> handle = Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;#btn-three clicked&#x27;</span>, ++countThree);</span><br><span class="line">        handle.detach();</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-three&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click #btn-three many times</span></span><br><span class="line">    <span class="comment">// output &#x27;#btn-three clicked 1&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> countFour = <span class="number">0</span>;</span><br><span class="line">    Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;#btn-four clicked&#x27;</span>, ++countFour);</span><br><span class="line">        Y.Event.detach(<span class="string">&#x27;click&#x27;</span>, <span class="literal">null</span>, <span class="string">&#x27;#btn-four&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-four&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click #btn-four many times</span></span><br><span class="line">    <span class="comment">// output &#x27;#btn-four clicked 1&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="源代码分析"><a href="#源代码分析" class="headerlink" title="源代码分析"></a>源代码分析</h3><p>接下来，让我们看看YUI的内部实现吧。</p><p>注：为了更容易的看懂代码的核心，我做了适当的简化，感兴趣的朋友可以去看未删节的<a href="https://github.com/yui/yui3/blob/v3.8.0/build/event-base/event-base-debug.js#L397">源码</a>。</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> add = <span class="function"><span class="keyword">function</span> (<span class="params">el, type, fn, capture</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (el &amp;&amp; el.addEventListener) &#123;</span><br><span class="line">            el.addEventListener(type, fn, capture);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (el &amp;&amp; el.attachEvent) &#123;</span><br><span class="line">            el.attachEvent(<span class="string">&#x27;on&#x27;</span> + type, fn);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    remove = <span class="function"><span class="keyword">function</span> (<span class="params">el, type, fn, capture</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (el &amp;&amp; el.removeEventListener) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                el.removeEventListener(type, fn, capture);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (ex) &#123;&#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (el &amp;&amp; el.detachEvent) &#123;</span><br><span class="line">            el.detachEvent(<span class="string">&#x27;on&#x27;</span> + type, fn);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 判断o是否为HTMLCollection或HTMLElement数组</span></span><br><span class="line">    shouldIterate = <span class="function"><span class="keyword">function</span> (<span class="params">o</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> (o &amp;&amp; <span class="keyword">typeof</span> o !== <span class="string">&quot;string&quot;</span> &amp;&amp; Y.Lang.isNumber(o.length) &amp;&amp; !o.tagName &amp;&amp; !Y.DOM.isWindow(o));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (ex) &#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">Y.Env.evt.dom_wrappers = &#123;&#125;;</span><br><span class="line">Y.Env.evt.dom_map = &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> Event = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> _wrappers = Y.Env.evt.dom_wrappers,</span><br><span class="line">        _el_events = Y.Env.evt.dom_map;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="comment">// 添加事件监听</span></span><br><span class="line">        <span class="attr">attach</span>: <span class="function"><span class="keyword">function</span> (<span class="params">type, fn, el, context</span>) </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> Event._attach(Y.Array(<span class="built_in">arguments</span>, <span class="number">0</span>, <span class="literal">true</span>));</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 创建自定义事件对象，在原生事件触发时执行该对象的fire方法，</span></span><br><span class="line">        <span class="comment">// 从而处理它上面的所有回调</span></span><br><span class="line">        <span class="attr">_createWrapper</span>: <span class="function"><span class="keyword">function</span> (<span class="params">el, type</span>) </span>&#123;</span><br><span class="line">            <span class="keyword">var</span> cewrapper,</span><br><span class="line">                ek  = Y.stamp(el),</span><br><span class="line">                key = <span class="string">&#x27;event:&#x27;</span> + ek + type;</span><br><span class="line"></span><br><span class="line">            cewrapper = _wrappers[key];</span><br><span class="line">            <span class="keyword">if</span> (!cewrapper) &#123;</span><br><span class="line">                cewrapper = Y.publish(key, &#123;</span><br><span class="line">                    <span class="attr">silent</span>: <span class="literal">true</span>,</span><br><span class="line">                    <span class="attr">bubbles</span>: <span class="literal">false</span>,</span><br><span class="line">                    <span class="attr">contextFn</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">                        cewrapper.nodeRef = cewrapper.nodeRef || Y.one(cewrapper.el);</span><br><span class="line">                        <span class="keyword">return</span> cewrapper.nodeRef;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line"></span><br><span class="line">                cewrapper.overrides = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">                cewrapper.el = el;</span><br><span class="line">                cewrapper.key = key;</span><br><span class="line">                cewrapper.domkey = ek;</span><br><span class="line">                cewrapper.type = type;</span><br><span class="line">                <span class="comment">// 作为原生DOM事件回调</span></span><br><span class="line">                cewrapper.fn = <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </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">                    cewrapper.fire(Event.getEvent(e, el));</span><br><span class="line">                &#125;;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 重写_delete方法，执行_clean来注销原生DOM节点事件监听</span></span><br><span class="line">                cewrapper._delete = <span class="function"><span class="keyword">function</span> (<span class="params">s</span>) </span>&#123;</span><br><span class="line">                    <span class="keyword">var</span> ret = Y.CustomEvent.prototype._delete.apply(<span class="built_in">this</span>, <span class="built_in">arguments</span>);</span><br><span class="line">                    <span class="keyword">if</span> (!<span class="built_in">this</span>.hasSubs()) &#123;</span><br><span class="line">                        <span class="comment">// 全部回调都被注销，则注销DOM事件监听</span></span><br><span class="line">                        Event._clean(<span class="built_in">this</span>);</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">return</span> ret;</span><br><span class="line">                &#125;;</span><br><span class="line"></span><br><span class="line">                _wrappers[key] = cewrapper;</span><br><span class="line">                _el_events[ek] = _el_events[ek] || &#123;&#125;;</span><br><span class="line">                _el_events[ek][key] = cewrapper;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 通过原生方法注册事件监听，这是关键的入口</span></span><br><span class="line">                add(el, type, cewrapper.fn);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> cewrapper;</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="attr">_attach</span>: <span class="function"><span class="keyword">function</span> (<span class="params">args</span>) </span>&#123;</span><br><span class="line">            <span class="keyword">var</span> handles, oEl, cewrapper, context,</span><br><span class="line">                ret,</span><br><span class="line">                type = args[<span class="number">0</span>],</span><br><span class="line">                fn = args[<span class="number">1</span>],</span><br><span class="line">                el = args[<span class="number">2</span>];</span><br><span class="line">            <span class="keyword">if</span> (!fn || !fn.call) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">       l) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (Y.Node &amp;&amp; Y.instanceOf(el,      lis = Event.getListeners(oEl, type),</span><br><span class="line">                i, lYUI().use(<span class="string">&#x27;node-base&#x27;</span>, <span class="string">&#x27;selector&#x27;</span>, <span class="string">&#x27;event-base&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>)  <span class="title">recursively</span>&#x27;)</span>;</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-six&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// click #btn-six</span></span><br><span class="line">    <span class="comment">// and then, click any elements in #wrapper</span></span><br><span class="line">    <span class="comment">// output nothing</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="通过Y和Node监听事件"><a href="#通过Y和Node监听事件" class="headerlink" title="通过Y和Node监听事件"></a>通过Y和Node监听事件</h2><p>YUI3借鉴了jQuery对HTMLElement/HTMLCollection的封装方式，方便进行链式调用，这就是<code>Y.Node</code>。</p><p>事件监听是不是可以不用繁琐的使用<code>Y.Event</code>，在<code>Y.Node</code>对象上直接调用呢？当然可以。主要有四个方法：</p><ul><li>NodeInstance.on</li><li>NodeInstance.once</li><li>NodeInstance.after</li><li>NodeInstance.onceAfter</li></ul><p>实际上，以上四个方法是通过内部调用Y的对应方法，例如<code>Node.prototype.on</code>内部调用了<code>Y.on</code>，而Y上的对应方法是由于Y本身是一个<code>Y.EventTarget</code>对象才获得的。最终，在<code>Y.EventTarget.prototype.on</code>中调用了<code>Y.Event</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">// 例5</span></span><br><span class="line">YUI().use(<span class="string">&#x27;node&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    Y.Event.attach(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;wrapper clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-one&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等价的Y.on写法</span></span><br><span class="line">    Y.on(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;btn-one clicked&#x27;</span>);</span><br><span class="line">    &#125;, <span class="string">&#x27;#btn-one&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等价的Node.on写法</span></span><br><span class="line">    Y.one(<span class="string">&#x27;#btn-one&#x27;</span>).on(<span class="string">&#x27;click&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;btn-one clicked&#x27;</span>);</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><p>所有示例代码均在<a href="https://github.com/springuper/yuianalyser/tree/master/event">GitHub</a>。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://yuilibrary.com/yui/docs/event/">YUILibrary-UserGuides-Event</a></li><li><a href="https://github.com/yui/yui3/blob/v3.8.0/build/event-base/event-base-debug.js">YUILibrary-Event</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;../images/mouse.png&quot; alt=&quot;mouse event&quot;&gt;&lt;/p&gt;
&lt;p&gt;在介绍了由&lt;code&gt;Y.Do&lt;/code&gt;、&lt;code&gt;Y.CustomEvent&lt;/code&gt;、&lt;code&gt;Y.EventTarget&lt;/code&gt;构建的自定义事件体系后，本篇文章将为大家介绍建立在这一体系之上，YUI对DOM事件的封装——&lt;code&gt;Y.Event&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;Y-DOMEventFacade&quot;&gt;&lt;a href=&quot;#Y-DOMEventFacade&quot; class=&quot;headerlink&quot; title=&quot;Y.DOMEventFacade&quot;&gt;&lt;/a&gt;Y.DOMEventFacade&lt;/h2&gt;&lt;p&gt;众所周知，浏览器之间存在大量的不兼容问题，在事件方面尤其如此。&lt;code&gt;Y.DOMEventFacade&lt;/code&gt;主要用来处理DOM事件对象的浏览器兼容问题，提供跨浏览器的简洁接口。事实上，我们常在&lt;code&gt;Y.one(&amp;#39;.selector&amp;#39;).on(&amp;#39;click&amp;#39;, function (e) &amp;#123;&amp;#125;)&lt;/code&gt;中使用的&lt;code&gt;e&lt;/code&gt;就是&lt;code&gt;Y.DOMEventFacade&lt;/code&gt;的实例。&lt;/p&gt;
&lt;p&gt;具体来说，兼容处理的属性主要有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;target，专门处理了target为文本节点的情况，统一为元素节点，方便操作&lt;/li&gt;
&lt;li&gt;relativeTarget，关联目标节点，在mouseover/mouseout等事件中设置&lt;/li&gt;
&lt;li&gt;keyCode/charCode等输入信息&lt;/li&gt;
&lt;li&gt;pageX/clientX等位置信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;兼容处理的方法主要有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stopPropagation/stopImmediatePropagation，不支持停止立即传播时，仅能在YUI层面模拟，不会阻止通过原生方法添加的同层回调，即，在YUI监听过el的click事件后，又通过&lt;code&gt;el.addEventListener(&amp;#39;click&amp;#39;, nativeCallback)&lt;/code&gt;监听，如果在YUI的回调中调用&lt;code&gt;e.stopImmediatePropagation&lt;/code&gt;的话，&lt;code&gt;nativeCallback&lt;/code&gt;仍然会执行&lt;/li&gt;
&lt;li&gt;preventDefault&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，为了方便同时停止传播和阻止默认行为，YUI还提供了&lt;code&gt;halt&lt;/code&gt;方法。&lt;/p&gt;</summary>
    
    
    
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="Y.Event" scheme="https://springuper.github.io/tags/Y-Event/"/>
    
  </entry>
  
  <entry>
    <title>YUI事件体系之Y.EventTarget</title>
    <link href="https://springuper.github.io/event-target/"/>
    <id>https://springuper.github.io/event-target/</id>
    <published>2012-11-25T21:25:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p><img src="../images/bubble-girl.jpg" alt="bubble girl"></p><p>上两篇文章<a href="/event-do/">YUI事件体系之Y.Do</a>、<a href="/event-custom/">YUI事件体系之Y.CustomEvent</a>中，分别介绍了YUI实现AOP的<code>Y.Do</code>对象，以及建立自定义事件机制的<code>Y.CustomEvent</code>对象。</p><p>本篇文章，将要介绍YUI事件体系集大成者、最为精华的部分——<code>Y.EventTarget</code>。</p><h2 id="Y-EventTarget"><a href="#Y-EventTarget" class="headerlink" title="Y.EventTarget"></a>Y.EventTarget</h2><p>DOM事件中的目标元素为<code>event.target</code>，这类元素可以触发、监听一些事件，例如input元素的click、focus等事件。这也正是<code>Y.EventTarget</code>的命名渊源，它提供了一种让任意对象定义、监听、触发自定义事件的实现方式。</p><p>从设计上看，<code>Y.EventTarget</code>通过内部维护一系列<code>Y.EventCustom</code>对象，提供了可以通过事件名称进行事件定义、监听和触发的便捷接口。另外，推荐使用<code>Y.augment</code>将它以组合的方式加载在其它类上，而不要使用继承。关于<code>Y.augment</code>和<code>Y.extend</code>之间的异同，可以参考我之前的一篇文章：<a href="/extend-and-augment/">Y.extend与Y.augment</a>。</p><p>YUI很多基础类都扩展了<code>Y.EventTarget</code>，重要的有<code>Y</code>（YUI instance，sandbox）、<code>Y.Global</code>、<code>Y.Node</code>、<code>Y.NodeList</code>、<code>Y.Base</code>等。</p><p>YUILibrary有专门一个章节介绍EventTarget，非常详尽，如果你对EventTarget的设计思路和使用方法感兴趣，请移步<a href="http://yuilibrary.com/yui/docs/event-custom/">YUILibrary-EventTarget</a>。</p><span id="more"></span><h3 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h3><p>首先，让我们看看<code>Y.EventTarget</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><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">// 例1</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> et = <span class="keyword">new</span> Y.EventTarget();</span><br><span class="line"></span><br><span class="line">    et.on(<span class="string">&#x27;say&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">msg</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;say:&#x27;</span>, msg);</span><br><span class="line">    &#125;);</span><br><span class="line">    et.on(<span class="string">&#x27;listen&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">msg</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;listen:&#x27;</span>, msg);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: say: Hello, world</span></span><br><span class="line">    instance.fire(<span class="string">&#x27;say&#x27;</span>, <span class="string">&#x27;Hello, world&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><!-- more --><p>这种方式实际意义不大，YUI中只有<code>Y.Global</code>使用了这种方式。</p><p>下面，让我们看下最广泛的使用方式，即通过<code>Y.augment</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><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">// 例2</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">MyClass</span>(<span class="params"></span>) </span>&#123;&#125;</span><br><span class="line">    MyClass.prototype.add = <span class="function"><span class="keyword">function</span> (<span class="params">item</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// do sth</span></span><br><span class="line">        <span class="built_in">this</span>.fire(<span class="string">&#x27;addItem&#x27;</span>, &#123; <span class="attr">item</span>: item &#125;);</span><br><span class="line">    &#125;;</span><br><span class="line">    MyClass.prototype.remove = <span class="function"><span class="keyword">function</span> (<span class="params">item</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// do sth</span></span><br><span class="line">        <span class="built_in">this</span>.fire(<span class="string">&#x27;removeItem&#x27;</span>, &#123; <span class="attr">item</span>: item &#125;);</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="comment">// 用EventTarget扩展MyClass</span></span><br><span class="line">    Y.augment(MyClass, Y.EventTarget);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> instance = <span class="keyword">new</span> MyClass();</span><br><span class="line">    <span class="comment">// 监听addItem事件</span></span><br><span class="line">    instance.on(<span class="string">&#x27;addItem&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">data</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;add an item:&#x27;</span>, data.item);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">// 监听removeItem事件</span></span><br><span class="line">    instance.on(<span class="string">&#x27;removeItem&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">data</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;remove an item:&#x27;</span>, data.item);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: add an item: orange</span></span><br><span class="line">    instance.add(<span class="string">&#x27;orange&#x27;</span>);</span><br><span class="line">    <span class="comment">// output: remove an item: red</span></span><br><span class="line">    instance.remove(<span class="string">&#x27;red&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="源代码分析"><a href="#源代码分析" class="headerlink" title="源代码分析"></a>源代码分析</h3><p>接下来，让我们看看YUI的内部实现吧。</p><p>注：为了更容易的看懂代码的核心，我做了适当的简化，感兴趣的朋友可以去看未删节的<a href="http://yuilibrary.com/yui/docs/api/files/event-custom_js_event-custom.js.html#l38">源码</a>。</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> AFTER_PREFIX = <span class="string">&#x27;~AFTER~&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// EventTarget构造器</span></span><br><span class="line"><span class="keyword">var</span> ET = <span class="function"><span class="keyword">function</span> (<span class="params">opts</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> o = opts || &#123;&#125;;</span><br><span class="line">    <span class="comment">// 私有事件聚合器</span></span><br><span class="line">    <span class="built_in">this</span>._yuievt = <span class="built_in">this</span>._yuievt || &#123;</span><br><span class="line">        <span class="attr">id</span>: Y.guid(),</span><br><span class="line">        <span class="attr">events</span>: &#123;&#125;,</span><br><span class="line">        <span class="attr">config</span>: o,</span><br><span class="line">        <span class="comment">// 默认配置</span></span><br><span class="line">        <span class="attr">defaults</span>: &#123;</span><br><span class="line">            <span class="attr">context</span>: o.context || <span class="built_in">this</span>,</span><br><span class="line">            <span class="attr">host</span>: <span class="built_in">this</span>,</span><br><span class="line">            <span class="attr">emitFacade</span>: o.emitFacade,</span><br><span class="line">            <span class="attr">fireOnce</span>: o.fireOnce,</span><br><span class="line">            <span class="attr">queuable</span>: o.queuable,</span><br><span class="line">            <span class="attr">broadcast</span>: o.broadcast</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">ET.prototype = &#123;</span><br><span class="line">    <span class="attr">constructor</span>: ET,</span><br><span class="line">    <span class="comment">// 创建事件</span></span><br><span class="line">    <span class="attr">publish</span>: <span class="function"><span class="keyword">function</span> (<span class="params">type, opts</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> ce,</span><br><span class="line">            events = <span class="built_in">this</span>._yuievt.events,</span><br><span class="line">            defaults = <span class="built_in">this</span>._yuievt.defaults;</span><br><span class="line"></span><br><span class="line">        ce = events[type];</span><br><span class="line">        <span class="keyword">if</span> (ce) &#123; <span class="comment">// 已创建过该事件</span></span><br><span class="line">            <span class="keyword">if</span> (opts) &#123;</span><br><span class="line">                ce.applyConfig(opts, <span class="literal">true</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123; <span class="comment">// 基于CustomEvent，创建新事件</span></span><br><span class="line">            ce = <span class="keyword">new</span> Y.CustomEvent(type,</span><br><span class="line">                                    (opts) ? Y.merge(defaults, opts) : defaults);</span><br><span class="line">            events[type] = ce;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> ce;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 监听事件</span></span><br><span class="line">    <span class="attr">on</span>: <span class="function"><span class="keyword">function</span> (<span class="params">type, fn, context</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> ce,</span><br><span class="line">            after,</span><br><span class="line">            handle,</span><br><span class="line">            args = <span class="literal">null</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> (type.indexOf(AFTER_PREFIX) &amp;gt; -<span class="number">1</span>) &#123;</span><br><span class="line">            after = <span class="literal">true</span>;</span><br><span class="line">            type = type.substr(AFTER_PREFIX.length);</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">        ce = <span class="built_in">this</span>._yuievt.events[type] || <span class="built_in">this</span>.publish(type);</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">arguments</span>.length &amp;gt; <span class="number">3</span>) &#123;</span><br><span class="line">            args = Y.Array(<span class="built_in">arguments</span>, <span class="number">3</span>, <span class="literal">true</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 调用自定义事件对象的_on方法监听事件</span></span><br><span class="line">        handle = ce._on(fn, context, args, after ? <span class="string">&#x27;after&#x27;</span> : <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> handle;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 监听一次事件</span></span><br><span class="line">    <span class="attr">once</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> handle = <span class="built_in">this</span>.on.apply(<span class="built_in">this</span>, <span class="built_in">arguments</span>);</span><br><span class="line">        <span class="keyword">if</span> (handle.sub) &#123;</span><br><span class="line">            <span class="comment">// 监听器执行一次则失效</span></span><br><span class="line">            handle.sub.once = <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> handle;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 后置监听事件</span></span><br><span class="line">    <span class="attr">after</span>: <span class="function"><span class="keyword">function</span> (<span class="params">type, fn</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> a = Y.Array(<span class="built_in">arguments</span>, <span class="number">0</span>, <span class="literal">true</span>);</span><br><span class="line">        a[<span class="number">0</span>] = AFTER_PREFIX + type;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.on.apply(<span class="built_in">this</span>, a);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 后置监听一次事件</span></span><br><span class="line">    <span class="attr">onceAfter</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> handle = <span class="built_in">this</span>.after.apply(<span class="built_in">this</span>, <span class="built_in">arguments</span>);</span><br><span class="line">        <span class="keyword">if</span> (handle.sub) &#123;</span><br><span class="line">            handle.sub.once = <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> handle;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 触发事件</span></span><br><span class="line">    <span class="attr">fire</span>: <span class="function"><span class="keyword">function</span> (<span class="params">type</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> ce,</span><br><span class="line">            args;</span><br><span class="line">        args = Y.Array(<span class="built_in">arguments</span>, <span class="number">1</span>, <span class="literal">true</span>);</span><br><span class="line">        ce = <span class="built_in">this</span>._yuievt.events[type];</span><br><span class="line">        <span class="comment">// 尚未创建事件</span></span><br><span class="line">        <span class="keyword">if</span> (!ce) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> ce.fire.apply(ce, args);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 注销事件监听</span></span><br><span class="line">    <span class="attr">detach</span>: <span class="function"><span class="keyword">function</span> (<span class="params">type, fn, context</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> events = <span class="built_in">this</span>._yuievt.events,</span><br><span class="line">            ce,</span><br><span class="line">            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> (!type) &#123;</span><br><span class="line">            <span class="keyword">for</span> (i <span class="keyword">in</span> events) &#123;</span><br><span class="line">                <span class="keyword">if</span> (events.hasOwnProperty(i)) &#123;</span><br><span class="line">                    events[i].detach(fn, context);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ce = events[type];</span><br><span class="line">        <span class="keyword">if</span> (ce) &#123;</span><br><span class="line">            ce.detach(fn, context);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="进阶用法"><a href="#进阶用法" class="headerlink" title="进阶用法"></a>进阶用法</h3><p><code>Y.EventTarget</code>作为一个十分重要的类，提供了非常丰富、方便的使用方式，除了依赖内部<code>Y.CustomEvent</code>实现的事件接口、默认执行方法、事件广播等，其余主要有：</p><h4 id="a-事件冒泡"><a href="#a-事件冒泡" class="headerlink" title="a) 事件冒泡"></a>a) 事件冒泡</h4><p>多个EventTarget对象之间可以建立一定事件传播关系，类似DOM事件中的冒泡。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例3</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 父类</span></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">Parent</span>(<span class="params"></span>) </span>&#123; ... &#125;</span><br><span class="line">    Y.augment(Parent, Y.EventTarget, <span class="literal">true</span>, <span class="literal">null</span>, &#123; <span class="attr">emitFacade</span>: <span class="literal">true</span> &#125;);</span><br><span class="line">    <span class="comment">// 子类</span></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">Child</span>(<span class="params"></span>) </span>&#123; ... &#125;</span><br><span class="line">    Y.augment(Child, Y.EventTarget, <span class="literal">true</span>, <span class="literal">null</span>, &#123; <span class="attr">emitFacade</span>: <span class="literal">true</span> &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> parent = <span class="keyword">new</span> Parent(),</span><br><span class="line">        child = <span class="keyword">new</span> Child();</span><br><span class="line">    <span class="comment">// 子类对象添加冒泡目标对象，child -&amp;gt; parent</span></span><br><span class="line">    child.addTarget(parent);</span><br><span class="line"></span><br><span class="line">    parent.on(<span class="string">&#x27;hear&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;parent hear&#x27;</span>, e.msg);</span><br><span class="line">    &#125;);</span><br><span class="line">    child.on(<span class="string">&#x27;hear&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;child hear&#x27;</span>, e.msg);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: child hear Hi, parent hear Hi</span></span><br><span class="line">    child.fire(<span class="string">&#x27;hear&#x27;</span>, &#123; <span class="attr">msg</span>: <span class="string">&#x27;Hi&#x27;</span> &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h4 id="b-事件前缀"><a href="#b-事件前缀" class="headerlink" title="b) 事件前缀"></a>b) 事件前缀</h4><p>在事件冒泡的基础上，考虑到区分不同EventTarget对象触发相同事件，YUI引入了事件前缀（Event Prefix）。</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例4</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 父类</span></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">Parent</span>(<span class="params"></span>) </span>&#123; ... &#125;</span><br><span class="line">    Y.augment(Parent, Y.EventTarget, <span class="literal">true</span>, <span class="literal">null</span>, &#123;</span><br><span class="line">        <span class="attr">emitFacade</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">prefix</span>: <span class="string">&#x27;parent&#x27;</span> <span class="comment">// 配置事件前缀</span></span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">// 子类</span></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">Child</span>(<span class="params"></span>) </span>&#123; ... &#125;</span><br><span class="line">    Y.augment(Child, Y.EventTarget, <span class="literal">true</span>, <span class="literal">null</span>, &#123;</span><br><span class="line">        <span class="attr">emitFacade</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">prefix</span>: <span class="string">&#x27;child&#x27;</span> <span class="comment">// 配置事件前缀</span></span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> parent = <span class="keyword">new</span> Parent(),</span><br><span class="line">        child = <span class="keyword">new</span> Child();</span><br><span class="line">    child.addTarget(parent);</span><br><span class="line"></span><br><span class="line">    parent.on(<span class="string">&#x27;hear&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123; <span class="comment">// 不能捕捉到child的hear事件</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;parent hear&#x27;</span>, e.msg);</span><br><span class="line">    &#125;);</span><br><span class="line">    child.on(<span class="string">&#x27;hear&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;child hear&#x27;</span>, e.msg);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: child hear Hi</span></span><br><span class="line">    child.fire(<span class="string">&#x27;hear&#x27;</span>, &#123; <span class="attr">msg</span>: <span class="string">&#x27;Hi&#x27;</span> &#125;);</span><br><span class="line"></span><br><span class="line">    parent.on(<span class="string">&#x27;*:see&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123; <span class="comment">// 要想监听到其它EventTarget对象的see事件，需要设置prefix</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;parent see&#x27;</span>, e.thing);</span><br><span class="line">    &#125;);</span><br><span class="line">    child.on(<span class="string">&#x27;child:see&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123; <span class="comment">// 等同监听see事件</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;child see&#x27;</span>, e.thing);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: child hear MM, parent see MM</span></span><br><span class="line">    child.fire(<span class="string">&#x27;see&#x27;</span>, &#123; <span class="attr">thing</span>: <span class="string">&#x27;MM&#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><a href="http://yuilibrary.com/yui/docs/api/files/event-custom_js_event-custom.js.html">YUILibrary-CustomEvent</a></li><li><a href="http://yuilibrary.com/yui/docs/event-custom/index.html">YUILibrary-EventTarget</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;../images/bubble-girl.jpg&quot; alt=&quot;bubble girl&quot;&gt;&lt;/p&gt;
&lt;p&gt;上两篇文章&lt;a href=&quot;/event-do/&quot;&gt;YUI事件体系之Y.Do&lt;/a&gt;、&lt;a href=&quot;/event-custom/&quot;&gt;YUI事件体系之Y.CustomEvent&lt;/a&gt;中，分别介绍了YUI实现AOP的&lt;code&gt;Y.Do&lt;/code&gt;对象，以及建立自定义事件机制的&lt;code&gt;Y.CustomEvent&lt;/code&gt;对象。&lt;/p&gt;
&lt;p&gt;本篇文章，将要介绍YUI事件体系集大成者、最为精华的部分——&lt;code&gt;Y.EventTarget&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;Y-EventTarget&quot;&gt;&lt;a href=&quot;#Y-EventTarget&quot; class=&quot;headerlink&quot; title=&quot;Y.EventTarget&quot;&gt;&lt;/a&gt;Y.EventTarget&lt;/h2&gt;&lt;p&gt;DOM事件中的目标元素为&lt;code&gt;event.target&lt;/code&gt;，这类元素可以触发、监听一些事件，例如input元素的click、focus等事件。这也正是&lt;code&gt;Y.EventTarget&lt;/code&gt;的命名渊源，它提供了一种让任意对象定义、监听、触发自定义事件的实现方式。&lt;/p&gt;
&lt;p&gt;从设计上看，&lt;code&gt;Y.EventTarget&lt;/code&gt;通过内部维护一系列&lt;code&gt;Y.EventCustom&lt;/code&gt;对象，提供了可以通过事件名称进行事件定义、监听和触发的便捷接口。另外，推荐使用&lt;code&gt;Y.augment&lt;/code&gt;将它以组合的方式加载在其它类上，而不要使用继承。关于&lt;code&gt;Y.augment&lt;/code&gt;和&lt;code&gt;Y.extend&lt;/code&gt;之间的异同，可以参考我之前的一篇文章：&lt;a href=&quot;/extend-and-augment/&quot;&gt;Y.extend与Y.augment&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;YUI很多基础类都扩展了&lt;code&gt;Y.EventTarget&lt;/code&gt;，重要的有&lt;code&gt;Y&lt;/code&gt;（YUI instance，sandbox）、&lt;code&gt;Y.Global&lt;/code&gt;、&lt;code&gt;Y.Node&lt;/code&gt;、&lt;code&gt;Y.NodeList&lt;/code&gt;、&lt;code&gt;Y.Base&lt;/code&gt;等。&lt;/p&gt;
&lt;p&gt;YUILibrary有专门一个章节介绍EventTarget，非常详尽，如果你对EventTarget的设计思路和使用方法感兴趣，请移步&lt;a href=&quot;http://yuilibrary.com/yui/docs/event-custom/&quot;&gt;YUILibrary-EventTarget&lt;/a&gt;。&lt;/p&gt;</summary>
    
    
    
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="Event" scheme="https://springuper.github.io/tags/Event/"/>
    
  </entry>
  
  <entry>
    <title>YUI事件体系之Y.CustomEvent</title>
    <link href="https://springuper.github.io/event-custom/"/>
    <id>https://springuper.github.io/event-custom/</id>
    <published>2012-09-01T19:13:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p><img src="../images/DIY.jpg" alt="DIY"></p><p>上一篇<a href="/2013/01/20/event-do/">文章</a>中，简要介绍了YUI实现AOP的<code>Y.Do</code>对象。</p><p>接下来，我们继续对YUI事件体系进行探索。本次要介绍的是<code>Y.CustomEvent</code>对象，从命名上就可以看出，这个对象在整个YUI事件体系中十分重要。它建立起整个自定义事件的体系，而且，DOM事件也构建在这个体系之上。</p><h2 id="Y-Subscriber"><a href="#Y-Subscriber" class="headerlink" title="Y.Subscriber"></a>Y.Subscriber</h2><p>Y.Subscriber的作用比较简单：执行回调函数。</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></pre></td><td class="code"><pre><span class="line">Y.Subscriber = <span class="function"><span class="keyword">function</span> (<span class="params">fn, context</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.fn = fn; <span class="comment">// 回调函数</span></span><br><span class="line">    <span class="built_in">this</span>.context = context; <span class="comment">// 上下文</span></span><br><span class="line">    <span class="built_in">this</span>.id = Y.stamp(<span class="built_in">this</span>); <span class="comment">// 设置唯一id</span></span><br><span class="line">&#125;;</span><br><span class="line">Y.Subscriber.prototype = &#123;</span><br><span class="line">    <span class="attr">constructor</span>: Y.Subscriber,</span><br><span class="line">    <span class="comment">// 执行回调函数</span></span><br><span class="line">    <span class="attr">notify</span>: <span class="function"><span class="keyword">function</span> (<span class="params">args, ce</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.deleted) <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">var</span> ret;</span><br><span class="line"></span><br><span class="line">        ret = <span class="built_in">this</span>.fn.apply(<span class="built_in">this</span>.context, args || []);</span><br><span class="line">        <span class="comment">// 只监听一次</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.once) &#123;</span><br><span class="line">            ce._delete(<span class="built_in">this</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> ret;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="Y-CustomEvent"><a href="#Y-CustomEvent" class="headerlink" title="Y.CustomEvent"></a>Y.CustomEvent</h2><p><code>Y.CustomEvent</code>主要作用是：建立自定义事件机制，为方便的进行事件创建、监听、触发提供良好基础。自定义事件机制，实际上是<a href="http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">Observer Pattern</a>（<a href="http://en.wikipedia.org/wiki/Observer_pattern">Publish–subscribe Pattern</a>的演化）的一种实现，这种机制能够方便的实现模块间解耦，增强模块的扩展性。</p><p>YUI的自定义事件较其它一些js库来说要强大一些，有这样一些好的features：</p><ul><li>支持事件接口(Event Facade)，在回调函数中可以进行调用</li><li>支持设置默认执行方法</li><li>支持停止/立即停止传播，并可设定停止传播时执行的方法</li><li>支持阻止默认行为(默认执行方法)，并可设定阻止默认行为时执行的方法</li><li>支持冒泡。指定冒泡目标序列，就可以顺序的触发事件(需要<code>Y.EventTarget</code>)</li><li>支持广播。每个自定义事件，都可以设置在当前YUI实例范围内和全局YUI内进行广播</li></ul><p>可以看出，YUI的自定义事件和DOM事件极其类似，这种设计自然到我们在用自定义事件时，丝毫感觉不到和DOM事件的差异。</p><span id="more"></span><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例1 简单自定义事件</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> eatEvent = <span class="keyword">new</span> Y.CustomEvent(<span class="string">&#x27;eat&#x27;</span>);</span><br><span class="line">    <span class="keyword">var</span> onHandle = eatEvent.on(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;before eating&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">var</span> onHandle2 = eatEvent.on(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;before eating, too&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">var</span> afterHandle = eatEvent.after(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;after eating&#x27;</span>);</span><br><span class="line">    &#125;); </span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: &quot;before eating&quot;, &quot;before eating, too&quot;, &quot;after eating&quot;</span></span><br><span class="line">    eatEvent.fire();</span><br><span class="line"></span><br><span class="line">    onHandle2.detach();</span><br><span class="line">    <span class="comment">// output: &quot;before eating&quot;, &quot;after eating&quot;</span></span><br><span class="line">    eatEvent.fire();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例2 仅触发一次的自定义事件</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> birthEvent = <span class="keyword">new</span> Y.CustomEvent(<span class="string">&#x27;birth&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">fireOnce</span>: <span class="literal">true</span>  <span class="comment">// you can only birth once</span></span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">var</span> onBirthHandle = birthEvent.on(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;before birth&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: &quot;before birth&quot;</span></span><br><span class="line">    birthEvent.fire();</span><br><span class="line">    <span class="comment">// nothing happened</span></span><br><span class="line">    birthEvent.fire();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 只触发一次的事件在触发后，再次添加监听方法时，会被立即执行</span></span><br><span class="line">    <span class="comment">// output: before birth, too</span></span><br><span class="line">    <span class="keyword">var</span> onBirthHandle2 = birthEvent.on(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;before birth, too&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>也许你还在琢磨，事件广播是什么？因为YUI使用了sandbox设计，可以生成不同实例绑定不同api，所以才有了事件广播机制。来看这个例子：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例3 事件广播</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> cryEvent = <span class="keyword">new</span> Y.CustomEvent(<span class="string">&#x27;cry&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">broadcast</span>: <span class="number">2</span>  <span class="comment">// global broadcast</span></span><br><span class="line">    &#125;);</span><br><span class="line">    cryEvent.on(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;before cry&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">    Y.on(<span class="string">&#x27;cry&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;YUI instance broadcast&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">    Y.Global.on(<span class="string">&#x27;cry&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;YUI global broadcast&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: &quot;before cry&quot;, &quot;YUI instance broadcast&quot;, &quot;YUI global broadcast&quot;</span></span><br><span class="line">    cryEvent.fire();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>文章之前介绍过YUI自定义事件的种种NB之处，那么用起来如何呢，来看下面的例子：</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><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="comment">// 例4 复杂自定义事件</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> driveEvent = <span class="keyword">new</span> Y.CustomEvent(<span class="string">&#x27;drive&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">emitFacade</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">host</span>: &#123;  <span class="comment">// hacking. 复杂自定义事件需要指定host，该host必须augment Y.EventTarget</span></span><br><span class="line">            <span class="attr">_yuievt</span>: &#123;&#125;,</span><br><span class="line">            <span class="attr">_monitor</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;&#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">defaultFn</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            Y.log(<span class="string">&#x27;execute defaultFn&#x27;</span>);</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">preventedFn</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            Y.log(<span class="string">&#x27;execute preventedFn&#x27;</span>);</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">stoppedFn</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            Y.log(<span class="string">&#x27;execute stoppedFn&#x27;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    driveEvent.on(<span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        e.stopImmediatePropagation();</span><br><span class="line">    &#125;);</span><br><span class="line">    driveEvent.on(<span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        e.preventDefault();</span><br><span class="line">    &#125;);</span><br><span class="line">    driveEvent.after(<span class="function"><span class="keyword">function</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">        Y.log(<span class="string">&#x27;after driving&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// output: &quot;execute stoppedFn&quot;, &quot;execute defaultFn&quot;</span></span><br><span class="line">    driveEvent.fire();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>不要失望，现在还没有介绍到事件体系的精华部分<code>Y.EventTarget</code>，所以很多特性（例如冒泡）还不能体现出来，拭目以待吧。</p><h3 id="源代码分析"><a href="#源代码分析" class="headerlink" title="源代码分析"></a>源代码分析</h3><p>接下来，让我们看看YUI的内部实现吧。</p><p>注：为了更容易的看懂代码的核心，我做了适当的简化，感兴趣的朋友可以去看未删节的<a href="http://yuilibrary.com/yui/docs/api/files/event-custom_js_event-custom.js.html#l38">源码</a>。</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><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><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> AFTER = <span class="string">&#x27;after&#x27;</span>,</span><br><span class="line">    <span class="comment">// config白名单</span></span><br><span class="line">    CONFIGS = [<span class="string">&#x27;broadcast&#x27;</span>, <span class="string">&#x27;monitored&#x27;</span>, <span class="string">&#x27;bubbles&#x27;</span>, <span class="string">&#x27;context&#x27;</span>, <span class="string">&#x27;contextFn&#x27;</span>, <span class="string">&#x27;currentTarget&#x27;</span>, <span class="string">&#x27;defaultFn&#x27;</span>, <span class="string">&#x27;defaultTargetOnly&#x27;</span>, <span class="string">&#x27;details&#x27;</span>, <span class="string">&#x27;emitFacade&#x27;</span>, <span class="string">&#x27;fireOnce&#x27;</span>, <span class="string">&#x27;async&#x27;</span>, <span class="string">&#x27;host&#x27;</span>, <span class="string">&#x27;preventable&#x27;</span>, <span class="string">&#x27;preventedFn&#x27;</span>, <span class="string">&#x27;queuable&#x27;</span>, <span class="string">&#x27;silent&#x27;</span>, <span class="string">&#x27;stoppedFn&#x27;</span>, <span class="string">&#x27;target&#x27;</span>, <span class="string">&#x27;type&#x27;</span>];</span><br><span class="line"></span><br><span class="line">Y.CustomEvent = <span class="function"><span class="keyword">function</span> (<span class="params">type, o</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.id = Y.stamp(<span class="built_in">this</span>);</span><br><span class="line">    <span class="built_in">this</span>.type = type;</span><br><span class="line">    <span class="built_in">this</span>.context = Y;</span><br><span class="line">    <span class="built_in">this</span>.preventable = <span class="literal">true</span>;</span><br><span class="line">    <span class="built_in">this</span>.bubbles = <span class="literal">true</span>;</span><br><span class="line">    <span class="built_in">this</span>.subscribers = &#123;&#125;; <span class="comment">// (前置)监听对象容器 注：YUI3.7.0将此处进行了优化</span></span><br><span class="line">    <span class="built_in">this</span>.afters = &#123;&#125;; <span class="comment">// 后置监听对象容器 注：YUI3.7.0将此处进行了优化</span></span><br><span class="line">    <span class="built_in">this</span>.subCount = <span class="number">0</span>;</span><br><span class="line">    <span class="built_in">this</span>.afterCount = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    o = o || &#123;&#125;;</span><br><span class="line">    <span class="built_in">this</span>.applyConfig(o, <span class="literal">true</span>);</span><br><span class="line">&#125;;</span><br><span class="line">Y.CustomEvent.prototype = &#123;</span><br><span class="line">    <span class="attr">constructor</span>: Y.CustomEvent,</span><br><span class="line">    <span class="comment">// 设置参数</span></span><br><span class="line">    <span class="attr">applyConfig</span>: <span class="function"><span class="keyword">function</span> (<span class="params">o, force</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (o) &#123;</span><br><span class="line">            Y.mix(<span class="built_in">this</span>, o, force, CONFIGS);</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="attr">on</span>: <span class="function"><span class="keyword">function</span> (<span class="params">fn, context</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> a = (<span class="built_in">arguments</span>.length &amp;gt; <span class="number">2</span>) ? Y.Array(<span class="built_in">arguments</span>, <span class="number">2</span>, <span class="literal">true</span>) : <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>._on(fn, context, a, <span class="literal">true</span>);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 添加后置监听对象</span></span><br><span class="line">    <span class="attr">after</span>: <span class="function"><span class="keyword">function</span> (<span class="params">fn, context</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> a = (<span class="built_in">arguments</span>.length &amp;gt; <span class="number">2</span>) ? Y.Array(<span class="built_in">arguments</span>, <span class="number">2</span>, <span class="literal">true</span>) : <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>._on(fn, context, a, AFTER);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 内部添加监听对象</span></span><br><span class="line">    <span class="attr">_on</span>: <span class="function"><span class="keyword">function</span> (<span class="params">fn, context, args, when</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> s = <span class="keyword">new</span> Y.Subscriber(fn, context);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.fireOnce &amp;amp;&amp;amp; <span class="built_in">this</span>.fired) &#123;</span><br><span class="line">            <span class="comment">// 仅触发一次的事件在触发后，再次添加监听方法时，会被立即执行</span></span><br><span class="line">            <span class="built_in">this</span>._notify(s, <span class="built_in">this</span>.firedWith);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (when == AFTER) &#123;</span><br><span class="line">            <span class="built_in">this</span>.afters[s.id] = s;</span><br><span class="line">            <span class="built_in">this</span>.afterCount++;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="built_in">this</span>.subscribers[s.id] = s;</span><br><span class="line">            <span class="built_in">this</span>.subCount++;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> Y.EventHandle(<span class="built_in">this</span>, s);</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="attr">fire</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.fireOnce &amp;amp;&amp;amp; <span class="built_in">this</span>.fired) &#123;</span><br><span class="line">            <span class="comment">// 仅触发一次的事件，如果已经触发过，直接返回true</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</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="keyword">var</span> args = Y.Array(<span class="built_in">arguments</span>, <span class="number">0</span>, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">            <span class="built_in">this</span>.fired = <span class="literal">true</span>;</span><br><span class="line">            <span class="built_in">this</span>.firedWith = args;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.emitFacade) &#123;</span><br><span class="line">                <span class="comment">// 复杂事件</span></span><br><span class="line">                <span class="keyword">return</span> <span class="built_in">this</span>.fireComplex(args);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="built_in">this</span>.fireSimple(args);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 触发简单事件</span></span><br><span class="line">    <span class="attr">fireSimple</span>: <span class="function"><span class="keyword">function</span> (<span class="params">args</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">this</span>.stopped = <span class="number">0</span>;</span><br><span class="line">        <span class="built_in">this</span>.prevented = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.hasSubs()) &#123;</span><br><span class="line">            <span class="keyword">var</span> subs = <span class="built_in">this</span>.getSubs();</span><br><span class="line">            <span class="comment">// 处理前置监听对象</span></span><br><span class="line">            <span class="built_in">this</span>._procSubs(subs[<span class="number">0</span>], args);</span><br><span class="line">            <span class="comment">// 处理前置监听对象</span></span><br><span class="line">            <span class="built_in">this</span>._procSubs(subs[<span class="number">1</span>], args);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">this</span>._broadcast(args);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.stopped ? <span class="literal">false</span> : <span class="literal">true</span>;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 判断是否有监听对象</span></span><br><span class="line">    <span class="attr">hasSubs</span>: <span class="function"><span class="keyword">function</span> (<span class="params">when</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> s = <span class="built_in">this</span>.subCount,</span><br><span class="line">            a = <span class="built_in">this</span>.afterCount;</span><br><span class="line">        <span class="keyword">if</span> (when) &#123;</span><br><span class="line">            <span class="keyword">return</span> (when == <span class="string">&#x27;after&#x27;</span>) ? a : s;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> (s + a);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 获取所有前置／后置监听对象</span></span><br><span class="line">    <span class="attr">getSubs</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> s = Y.merge(<span class="built_in">this</span>.subscribers),</span><br><span class="line">            a = Y.merge(<span class="built_in">this</span>.afters);</span><br><span class="line">        <span class="keyword">return</span> [s, a];</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 获取监听对象</span></span><br><span class="line">    <span class="attr">_procSubs</span>: <span class="function"><span class="keyword">function</span> (<span class="params">subs, args, ef</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> s, i;</span><br><span class="line">        <span class="keyword">for</span> (i <span class="keyword">in</span> subs) &#123;</span><br><span class="line">            <span class="keyword">if</span> (subs.hasOwnProperty(i)) &#123;</span><br><span class="line">                s = subs[i];</span><br><span class="line">                <span class="keyword">if</span> (s &amp;&amp; s.fn) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (<span class="literal">false</span> === <span class="built_in">this</span>._notify(s, args, ef)) &#123;</span><br><span class="line">                        <span class="comment">// 回调返回false时，立即停止处理后续回调</span></span><br><span class="line">                        <span class="built_in">this</span>.stopped = <span class="number">2</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">if</span> (<span class="built_in">this</span>.stopped == <span class="number">2</span>) &#123;</span><br><span class="line">                        <span class="comment">// 立即停止处理后续回调，方便实现stopImmediatePropagation</span></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">            &#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><span class="line">    <span class="comment">// 通知监听对象，执行回调方法</span></span><br><span class="line">    <span class="attr">_notify</span>: <span class="function"><span class="keyword">function</span> (<span class="params">s, args, ef</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> ret = s.notify(args, <span class="built_in">this</span>);</span><br><span class="line">        <span class="keyword">if</span> (<span class="literal">false</span> === ret || <span class="built_in">this</span>.stopped &amp;gt; <span class="number">1</span>) &#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">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 广播事件</span></span><br><span class="line">    <span class="attr">_broadcast</span>: <span class="function"><span class="keyword">function</span> (<span class="params">args</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!<span class="built_in">this</span>.stopped &amp;amp;&amp;amp; <span class="built_in">this</span>.broadcast) &#123;</span><br><span class="line">            <span class="keyword">var</span> a = Y.Array(args);</span><br><span class="line">            a.unshift(<span class="built_in">this</span>.type);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 在当前YUI实例Y上广播</span></span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.host !== Y) &#123;</span><br><span class="line">                Y.fire.apply(Y, a);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 在全局对象YUI上广播，跨实例</span></span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.broadcast == <span class="number">2</span>) &#123;</span><br><span class="line">                Y.Global.fire.apply(Y.Global, a);</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="comment">// <span class="doctag">TODO:</span> 在下一篇介绍Y.EventTarget的文章中再做介绍</span></span><br><span class="line">    <span class="attr">fireComplex</span>: <span class="function"><span class="keyword">function</span> (<span class="params">args</span>) </span>&#123;&#125;,</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 移除监听器</span></span><br><span class="line">    <span class="attr">detach</span>: <span class="function"><span class="keyword">function</span> (<span class="params">fn, context</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// unsubscribe handle</span></span><br><span class="line">        <span class="keyword">if</span> (fn &amp;amp;&amp;amp; fn.detach) &#123;</span><br><span class="line">            <span class="keyword">return</span> fn.detach();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">var</span> i, s,</span><br><span class="line">            found = <span class="number">0</span>,</span><br><span class="line">            subs = Y.merge(<span class="built_in">this</span>.subscribers, <span class="built_in">this</span>.afters);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (i <span class="keyword">in</span> subs) &#123;</span><br><span class="line">            <span class="keyword">if</span> (subs.hasOwnProperty(i)) &#123;</span><br><span class="line">                s = subs[i];</span><br><span class="line">                <span class="keyword">if</span> (s &amp;amp;&amp;amp; (!fn || fn === s.fn)) &#123;</span><br><span class="line">                    <span class="built_in">this</span>._delete(s);</span><br><span class="line">                    found++;</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> found;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">_delete</span>: <span class="function"><span class="keyword">function</span> (<span class="params">s</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (s) &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.subscribers[s.id]) &#123;</span><br><span class="line">                <span class="keyword">delete</span> <span class="built_in">this</span>.subscribers[s.id];</span><br><span class="line">                <span class="built_in">this</span>.subCount--;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.afters[s.id]) &#123;</span><br><span class="line">                <span class="keyword">delete</span> <span class="built_in">this</span>.afters[s.id];</span><br><span class="line">                <span class="built_in">this</span>.afterCount--;</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">if</span> (s) &#123;</span><br><span class="line">            s.deleted = <span class="literal">true</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="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h3><p>自定义事件的适用场景与Publish–subscribe Pattern基本一致。具体来讲，我觉得以下一些场景是非常适合用自定义事件的：</p><h4 id="a-需要暴露接口-行为以满足扩展需要"><a href="#a-需要暴露接口-行为以满足扩展需要" class="headerlink" title="a) 需要暴露接口/行为以满足扩展需要"></a>a) 需要暴露接口/行为以满足扩展需要</h4><p>底层模块一般会设计的尽量简单，解决核心问题，并适当的开放一些接口，方便应用层进行扩展以满足实际需求。例如表单验证控件，有可能需要在某个表单项验证成功/失败后执行一些额外操作，举一个实际的例子：当用户输入的邮箱地址验证成功时，我们会检查是不是某些比较烂的邮件服务商，如果是则给出一些建议。</p><p>YUI作为一个底层基础库，在组件/控件层面加入了大量的自定义事件，以满足实际应用中的需要。例如<code>Y.Anim</code>的start、end事件，<code>Y.io</code>的success、failure、end事件，<code>Y.Attribute</code>中的属性变化事件等。</p><h4 id="b-行为可能会被其它模块-方法中止"><a href="#b-行为可能会被其它模块-方法中止" class="headerlink" title="b) 行为可能会被其它模块/方法中止"></a>b) 行为可能会被其它模块/方法中止</h4><p>这一点非常像DOM事件，我们经常会中止一些事件的默认行为，例如anchor的点击事件。</p><h3 id="自定义事件-VS-回调函数"><a href="#自定义事件-VS-回调函数" class="headerlink" title="自定义事件 VS 回调函数"></a>自定义事件 VS 回调函数</h3><p>这是一个比较难的问题，我自己的看法是：相对回调函数，自定义事件是一种更重但更灵活的方案。在实际应用中，如果对于关心某消息的受众不够清楚，那么就使用事件。否则，比较适合使用回调函数。</p><p><a href="http://msdn.microsoft.com/en-us/library/aa733743%28v=vs.60%29.aspx">MSDN</a>上的解释更好一些：“An event is like an anonymous broadcast, while a call-back is like a handshake. The corollary of this is that a component that raises events knows nothing about its clients, while a component that makes call-backs knows a great deal”。</p><p>另外，如果对于性能特别关心，在可能的情况下，尽量使用回调。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://yuilibrary.com/yui/docs/api/files/event-custom_js_event-custom.js.html">YUILibrary-CustomEvent</a></li><li><a href="http://yuilibrary.com/yui/docs/event-custom/index.html">YUILibrary-EventTarget</a></li><li><a href="http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">Wikipedia-Publish–subscribe Pattern</a></li><li><a href="http://www.nczonline.net/blog/2010/03/09/custom-events-in-javascript/">Zakas-Custom events in JavaScript</a></li><li><a href="http://msdn.microsoft.com/en-us/library/aa733743(v=vs.60).aspx">When to Use Events or Call-Backs for Notifications</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;../images/DIY.jpg&quot; alt=&quot;DIY&quot;&gt;&lt;/p&gt;
&lt;p&gt;上一篇&lt;a href=&quot;/2013/01/20/event-do/&quot;&gt;文章&lt;/a&gt;中，简要介绍了YUI实现AOP的&lt;code&gt;Y.Do&lt;/code&gt;对象。&lt;/p&gt;
&lt;p&gt;接下来，我们继续对YUI事件体系进行探索。本次要介绍的是&lt;code&gt;Y.CustomEvent&lt;/code&gt;对象，从命名上就可以看出，这个对象在整个YUI事件体系中十分重要。它建立起整个自定义事件的体系，而且，DOM事件也构建在这个体系之上。&lt;/p&gt;
&lt;h2 id=&quot;Y-Subscriber&quot;&gt;&lt;a href=&quot;#Y-Subscriber&quot; class=&quot;headerlink&quot; title=&quot;Y.Subscriber&quot;&gt;&lt;/a&gt;Y.Subscriber&lt;/h2&gt;&lt;p&gt;Y.Subscriber的作用比较简单：执行回调函数。&lt;/p&gt;
&lt;figure class=&quot;highlight javascript&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;19&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;20&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;21&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;Y.Subscriber = &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;fn, context&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.fn = fn; &lt;span class=&quot;comment&quot;&gt;// 回调函数&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.context = context; &lt;span class=&quot;comment&quot;&gt;// 上下文&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.id = Y.stamp(&lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;); &lt;span class=&quot;comment&quot;&gt;// 设置唯一id&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Y.Subscriber.prototype = &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;constructor&lt;/span&gt;: Y.Subscriber,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// 执行回调函数&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;attr&quot;&gt;notify&lt;/span&gt;: &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;args, ce&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.deleted) &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;literal&quot;&gt;null&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; ret;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        ret = &lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.fn.apply(&lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.context, args || []);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// 只监听一次&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.once) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;            ce._delete(&lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; ret;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;h2 id=&quot;Y-CustomEvent&quot;&gt;&lt;a href=&quot;#Y-CustomEvent&quot; class=&quot;headerlink&quot; title=&quot;Y.CustomEvent&quot;&gt;&lt;/a&gt;Y.CustomEvent&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Y.CustomEvent&lt;/code&gt;主要作用是：建立自定义事件机制，为方便的进行事件创建、监听、触发提供良好基础。自定义事件机制，实际上是&lt;a href=&quot;http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern&quot;&gt;Observer Pattern&lt;/a&gt;（&lt;a href=&quot;http://en.wikipedia.org/wiki/Observer_pattern&quot;&gt;Publish–subscribe Pattern&lt;/a&gt;的演化）的一种实现，这种机制能够方便的实现模块间解耦，增强模块的扩展性。&lt;/p&gt;
&lt;p&gt;YUI的自定义事件较其它一些js库来说要强大一些，有这样一些好的features：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持事件接口(Event Facade)，在回调函数中可以进行调用&lt;/li&gt;
&lt;li&gt;支持设置默认执行方法&lt;/li&gt;
&lt;li&gt;支持停止/立即停止传播，并可设定停止传播时执行的方法&lt;/li&gt;
&lt;li&gt;支持阻止默认行为(默认执行方法)，并可设定阻止默认行为时执行的方法&lt;/li&gt;
&lt;li&gt;支持冒泡。指定冒泡目标序列，就可以顺序的触发事件(需要&lt;code&gt;Y.EventTarget&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;支持广播。每个自定义事件，都可以设置在当前YUI实例范围内和全局YUI内进行广播&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看出，YUI的自定义事件和DOM事件极其类似，这种设计自然到我们在用自定义事件时，丝毫感觉不到和DOM事件的差异。&lt;/p&gt;</summary>
    
    
    
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="Event" scheme="https://springuper.github.io/tags/Event/"/>
    
  </entry>
  
  <entry>
    <title>YUI事件体系之Y.Do</title>
    <link href="https://springuper.github.io/event-do/"/>
    <id>https://springuper.github.io/event-do/</id>
    <published>2012-07-14T17:34:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>YUI团队在种种场合不断的夸耀自己的事件体系是多么强大：</p><ul><li>YUI 3′s Event module is one of the strengths of the library  –Eric Miraglia, <a href="http://www.yuiblog.com/blog/2009/10/30/smith-yuiconf2009-events/">YUI Theater — Luke Smith: “Events Evolved”</a></li><li>YUI 3 is not all about DOM manipulation — it also contains a robust set of class/object management tools, not to mention our powerful custom events –Tilo Mitra, <a href="http://net.tutsplus.com/articles/general/10-things-i-learned-while-interning-at-yui/">10 Things I Learned While Interning at YUI</a></li><li>One of the strengths of the YUI App Framework is that it’s integrated tightly with the rest of YUI and benefits from YUI’s fantastic event system and plugin/extension infrastructure. –Ryan Grove, <a href="http://www.quora.com/How-can-I-decided-whether-to-choose-YUI-3s-MVC-or-Backbone-for-a-new-project#ld_xJGMd1_3012">How can I decided whether to choose YUI 3’s MVC or Backbone for a new project?</a></li></ul><p>事实的确如此吗？就使用YUI的开发者反馈来看，应该是不错的：</p><ul><li>AFAIK YUI 3’s event system is the most sophisticated of any JavaScript framework. Am I wrong in thinking that? –<a href="https://twitter.com/wrumsby/status/113568040834174976">Walter Rumsby</a></li><li>I love the event system in YUI. Pure awesomeness. –<a href="https://twitter.com/kev_nz/statuses/180472697644515328">Kevin Isom</a></li><li>I am constantly impressed by the degree of excellence I find in working with the YUI3 framework –Andrew Wooldridge, <a href="http://andrewwooldridge.com/blog/2011/03/08/cross-yui-communication-and-custom-events/">Cross YUI Communication and Custom Events</a></li></ul><p>作为一名YUI用户，我对其事件体系的强大深有体会。从本篇文章起，我将对YUI事件机制做一个全面分析。</p><p>本次我们介绍的是比较基础的两个对象<code>Y.EventHandle</code>和<code>Y.Do</code>。千里之行积于跬步，YUI整套事件机制也是从这两个对象开始构筑的。</p><span id="more"></span><h2 id="Y-EventHandle"><a href="#Y-EventHandle" class="headerlink" title="Y.EventHandle"></a>Y.EventHandle</h2><p><code>Y.EventHandle</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><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">Y.EventHandle = <span class="function"><span class="keyword">function</span> (<span class="params">evt, sub</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.evt = evt; <span class="comment">// 事件对象</span></span><br><span class="line">    <span class="built_in">this</span>.sub = sub; <span class="comment">// 监听对象</span></span><br><span class="line">&#125;;</span><br><span class="line">Y.EventHandle.detach = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.evt._delete(<span class="built_in">this</span>.sub); <span class="comment">// 执行event对象的_delete方法，注销事件/消息监听</span></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><h2 id="Y-Do"><a href="#Y-Do" class="headerlink" title="Y.Do"></a>Y.Do</h2><p><code>Y.Do</code>的作用是：向对象方法前面或者后面插入其它方法(前置、后置方法)，以达到动态修改对象行为的目的。这种方式，也称作<a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming">AOP</a>。</p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例1</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> cat = &#123;</span><br><span class="line">        <span class="attr">eat</span>: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            <span class="built_in">console</span>.log(<span class="string">&#x27;eat a fish&#x27;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    cat.eat(); <span class="comment">// output: eat a fish</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> beforeHandle = Y.Do.before(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;catch a fish&#x27;</span>);</span><br><span class="line">    &#125;, cat, <span class="string">&#x27;eat&#x27;</span>);</span><br><span class="line">    <span class="keyword">var</span> afterHandle = Y.Do.after(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;done!&#x27;</span>);</span><br><span class="line">    &#125;, cat, <span class="string">&#x27;eat&#x27;</span>);</span><br><span class="line">    cat.eat(); <span class="comment">// output: catch a fish, eat, done!</span></span><br><span class="line"></span><br><span class="line">    afterHandle.detach();</span><br><span class="line">    cat.eat(); <span class="comment">// output: catch a fish, eat</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>在不修改原对象方法的基础上，可以方便的添加前置、后置方法，并且注销这些方法也很容易。<code>Y.Do</code>非常漂亮的解决了我们动态修改对象方法的需求！很难想象，如果不用<code>Y.Do</code>代码会复杂成怎样。</p><h2 id="源代码分析"><a href="#源代码分析" class="headerlink" title="源代码分析"></a>源代码分析</h2><p>接下来，让我们看看YUI的内部实现吧。这是多么有趣的事，就像小时候买把手枪，想不明白为什么可以射击，就砸开一看究竟。</p><p>为了更容易的看懂代码的核心，我做了适当的简化，感兴趣的朋友可以去看未删节的<a href="http://yuilibrary.com/yui/docs/api/files/event-custom_js_event-do.js.html#l16">源码</a>。</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><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="comment">// 代码版本为YUI3.4.1，YUI3.5.0对Y.Do的实现有所改进</span></span><br><span class="line"><span class="keyword">var</span> DO_BEFORE = <span class="number">0</span>,</span><br><span class="line">    DO_AFTER = <span class="number">1</span>;</span><br><span class="line">Y.Do = &#123;</span><br><span class="line">    <span class="comment">// 缓存处理对象</span></span><br><span class="line">    <span class="attr">objs</span>: &#123;&#125;,</span><br><span class="line">    <span class="attr">before</span>: <span class="function"><span class="keyword">function</span> (<span class="params">fn, obj, sFn</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>._inject(DO_BEFORE, fn, obj, sFn);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">after</span>: <span class="function"><span class="keyword">function</span> (<span class="params">fn, obj, sFn</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>._inject(DO_AFTER, fn, obj, sFn);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">_inject</span>: <span class="function"><span class="keyword">function</span> (<span class="params">when, fn, obj, sFn</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> id = Y.stamp(obj), o, sid;</span><br><span class="line">        <span class="keyword">if</span> (!<span class="built_in">this</span>.objs[id]) <span class="built_in">this</span>.objs[id] = &#123;&#125;;</span><br><span class="line">        o = <span class="built_in">this</span>.objs[id];</span><br><span class="line">        <span class="keyword">if</span> (!o[sFn]) &#123;</span><br><span class="line">            <span class="comment">// 创建保存对象、方法名的Method对象</span></span><br><span class="line">            o[sFn] = <span class="keyword">new</span> Y.Do.Method(obj, sFn);</span><br><span class="line">            <span class="comment">// 修改对象方法</span></span><br><span class="line">            obj[sFn] = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> o[sFn].exec.apply(o[sFn], <span class="built_in">arguments</span>);</span><br><span class="line">            &#125;;</span><br><span class="line">        &#125;</span><br><span class="line">        sid = id + Y.stamp(fn) + sFn;</span><br><span class="line">        <span class="comment">// 注册插入方法</span></span><br><span class="line">        o[sFn].register(sid, fn, when);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 返回EventHandle对象，方便注销</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> Y.EventHandle(o[sFn], sid);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Y.Do.Method = <span class="function"><span class="keyword">function</span> (<span class="params">obj, sFn</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.obj = obj;</span><br><span class="line">    <span class="built_in">this</span>.methodName = sFn;</span><br><span class="line">    <span class="built_in">this</span>.method = obj[sFn];</span><br><span class="line">    <span class="built_in">this</span>.before = &#123;&#125;;</span><br><span class="line">    <span class="built_in">this</span>.after = &#123;&#125;;</span><br><span class="line">&#125;;</span><br><span class="line">Y.Do.Method.prototype.register = <span class="function"><span class="keyword">function</span> (<span class="params">sid, fn, when</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (when) &#123;</span><br><span class="line">        <span class="built_in">this</span>.after[sid] = fn;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.before[sid] = fn;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// 注销插入方法</span></span><br><span class="line">Y.Do.Method.prototype._delete = <span class="function"><span class="keyword">function</span> (<span class="params">sid</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">delete</span> <span class="built_in">this</span>.before[sid];</span><br><span class="line">    <span class="keyword">delete</span> <span class="built_in">this</span>.after[sid];</span><br><span class="line">&#125;;</span><br><span class="line">Y.Do.Method.prototype.exec = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> before = <span class="built_in">this</span>.before,</span><br><span class="line">        after = <span class="built_in">this</span>.after,</span><br><span class="line">        i, ret;</span><br><span class="line">    <span class="comment">// 执行插入前面的方法</span></span><br><span class="line">    <span class="keyword">for</span> (i <span class="keyword">in</span> before) &#123;</span><br><span class="line">        <span class="keyword">if</span> (before.hasOwnProperty(i)) &#123;</span><br><span class="line">            ret = before[i].apply(<span class="built_in">this</span>.obj, <span class="built_in">arguments</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 执行原方法</span></span><br><span class="line">    ret = <span class="built_in">this</span>.method.apply(<span class="built_in">this</span>.obj, <span class="built_in">arguments</span>);</span><br><span class="line">    <span class="comment">// 执行插入后面的方法</span></span><br><span class="line">    <span class="keyword">for</span> (i <span class="keyword">in</span> after) &#123;</span><br><span class="line">        <span class="keyword">if</span> (after.hasOwnProperty(i)) &#123;</span><br><span class="line">            ret = after[i].apply(<span class="built_in">this</span>.obj, <span class="built_in">arguments</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h2><h3 id="a-动态修改对象方法"><a href="#a-动态修改对象方法" class="headerlink" title="a) 动态修改对象方法"></a>a) 动态修改对象方法</h3><p>请参照例1。</p><h3 id="b-动态修改原型方法"><a href="#b-动态修改原型方法" class="headerlink" title="b) 动态修改原型方法"></a>b) 动态修改原型方法</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 例2</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">Car</span>(<span class="params">brand</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">this</span>.brand = brand;</span><br><span class="line">    &#125;;</span><br><span class="line">    Car.prototype.start = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;start&#x27;</span>);</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> myCar = <span class="keyword">new</span> Car(<span class="string">&#x27;bmw&#x27;</span>);</span><br><span class="line">    Y.Do.before(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;open the door&#x27;</span>);</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;start&#x27;</span>);</span><br><span class="line">    Y.Do.after(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;the car is started!&#x27;</span>);</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;start&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    myCar.start(); <span class="comment">// output: open the door, start, the car is started!</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="c-动态修改宿主方法"><a href="#c-动态修改宿主方法" class="headerlink" title="c) 动态修改宿主方法"></a>c) 动态修改宿主方法</h3><p>为宿主对象添加插件时，插件往往需要在宿主一些方法前后执行某些操作。YUI提供了一个很好的<a href="http://yuilibrary.com/yui/docs/plugin/#methods">例子</a>。</p><h3 id="d-动态修改被扩展对象方法"><a href="#d-动态修改被扩展对象方法" class="headerlink" title="d) 动态修改被扩展对象方法"></a>d) 动态修改被扩展对象方法</h3><p>为对象添加扩展时，扩展对象往往需要在被扩展对象一些方法前后执行某些操作。YUI提供了一个很好的<a href="http://yuilibrary.com/yui/docs/assets/base/myextension.js.txt">例子</a>。</p><h2 id="进阶使用"><a href="#进阶使用" class="headerlink" title="进阶使用"></a>进阶使用</h2><p>由于简化代码，省略了一些细节。<code>Y.Do</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><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="comment">// 例3</span></span><br><span class="line">YUI().use(<span class="string">&#x27;event-custom&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">Car</span>(<span class="params">brand, degree</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">this</span>.brand = brand;</span><br><span class="line">        <span class="built_in">this</span>.degree = degree || <span class="number">0</span>;</span><br><span class="line">    &#125;;</span><br><span class="line">    Car.prototype.shift = <span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;change to &#x27;</span> + degree);</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> myCar = <span class="keyword">new</span> Car(<span class="string">&#x27;bmw&#x27;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 多个前置方法</span></span><br><span class="line">    Y.Do.before(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;prepare to change&#x27;</span>);</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    Y.Do.before(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;prepare to change again&#x27;</span>);</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    myCar.shift(<span class="number">1</span>); <span class="comment">// output: prepare to change, prepare to change again, change to 1</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 多个后置方法</span></span><br><span class="line">    Y.Do.after(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;already change&#x27;</span>);</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    Y.Do.after(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;already change again&#x27;</span>);</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    myCar.shift(<span class="number">2</span>); <span class="comment">// output: ..., change to 2, already change, already change again </span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 中止执行</span></span><br><span class="line">    Y.Do.before(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (degree &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="built_in">console</span>.log(<span class="string">&#x27;halt, too low!&#x27;</span>);</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> Y.Do.Halt();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    myCar.shift(-<span class="number">1</span>); <span class="comment">// output: ..., halt, too low! </span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 阻止默认方法</span></span><br><span class="line">    Y.Do.before(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (degree &gt; <span class="number">4</span>) &#123;</span><br><span class="line">            <span class="built_in">console</span>.log(<span class="string">&#x27;prevent changing, too high!&#x27;</span>);</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> Y.Do.Prevent();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    myCar.shift(<span class="number">5</span>); <span class="comment">// output: ..., prevent changing, too high!, already change, ... </span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 替换参数</span></span><br><span class="line">    Y.Do.before(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> d = <span class="built_in">Math</span>.floor(degree);</span><br><span class="line">        <span class="keyword">if</span> (degree !== d) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> Y.Do.AlterArgs(<span class="string">&#x27;degree should be a integer&#x27;</span>, [d]);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    myCar.shift(<span class="number">2.5</span>); <span class="comment">// output: ..., change to 2, ... </span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 替换返回值</span></span><br><span class="line">    Y.Do.after(<span class="function"><span class="keyword">function</span> (<span class="params">degree</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (degree === <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> Y.Do.AlterReturn(<span class="string">&#x27;&#x27;</span>, <span class="string">&#x27;wow, your car now has no power&#x27;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, Car.prototype, <span class="string">&#x27;shift&#x27;</span>);</span><br><span class="line">    <span class="keyword">var</span> ret = myCar.shift(<span class="number">0</span>); <span class="comment">// output: ..., change to 0, ... </span></span><br><span class="line">    <span class="built_in">console</span>.log(ret); <span class="comment">// wow, your car now has no power</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://yuilibrary.com/yui/docs/api/classes/Do.html">YUILibrary-Do</a></li><li><a href="http://yuilibrary.com/yui/docs/event-custom/index.html">YUILibrary-EventTarget</a></li><li><a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming">Wikipedia-AOP</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;YUI团队在种种场合不断的夸耀自己的事件体系是多么强大：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;YUI 3′s Event module is one of the strengths of the library  –Eric Miraglia, &lt;a href=&quot;http://www.yuiblog.com/blog/2009/10/30/smith-yuiconf2009-events/&quot;&gt;YUI Theater — Luke Smith: “Events Evolved”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;YUI 3 is not all about DOM manipulation — it also contains a robust set of class/object management tools, not to mention our powerful custom events –Tilo Mitra, &lt;a href=&quot;http://net.tutsplus.com/articles/general/10-things-i-learned-while-interning-at-yui/&quot;&gt;10 Things I Learned While Interning at YUI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;One of the strengths of the YUI App Framework is that it’s integrated tightly with the rest of YUI and benefits from YUI’s fantastic event system and plugin/extension infrastructure. –Ryan Grove, &lt;a href=&quot;http://www.quora.com/How-can-I-decided-whether-to-choose-YUI-3s-MVC-or-Backbone-for-a-new-project#ld_xJGMd1_3012&quot;&gt;How can I decided whether to choose YUI 3’s MVC or Backbone for a new project?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事实的确如此吗？就使用YUI的开发者反馈来看，应该是不错的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AFAIK YUI 3’s event system is the most sophisticated of any JavaScript framework. Am I wrong in thinking that? –&lt;a href=&quot;https://twitter.com/wrumsby/status/113568040834174976&quot;&gt;Walter Rumsby&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;I love the event system in YUI. Pure awesomeness. –&lt;a href=&quot;https://twitter.com/kev_nz/statuses/180472697644515328&quot;&gt;Kevin Isom&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;I am constantly impressed by the degree of excellence I find in working with the YUI3 framework –Andrew Wooldridge, &lt;a href=&quot;http://andrewwooldridge.com/blog/2011/03/08/cross-yui-communication-and-custom-events/&quot;&gt;Cross YUI Communication and Custom Events&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作为一名YUI用户，我对其事件体系的强大深有体会。从本篇文章起，我将对YUI事件机制做一个全面分析。&lt;/p&gt;
&lt;p&gt;本次我们介绍的是比较基础的两个对象&lt;code&gt;Y.EventHandle&lt;/code&gt;和&lt;code&gt;Y.Do&lt;/code&gt;。千里之行积于跬步，YUI整套事件机制也是从这两个对象开始构筑的。&lt;/p&gt;</summary>
    
    
    
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="Event" scheme="https://springuper.github.io/tags/Event/"/>
    
  </entry>
  
  <entry>
    <title>Y.extend与Y.augment</title>
    <link href="https://springuper.github.io/extend-and-augment/"/>
    <id>https://springuper.github.io/extend-and-augment/</id>
    <published>2012-02-11T21:24:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>很长一段时间内，我都没有搞懂YUI3 OOP模块中的<code>Y.extend</code>方法与<code>Y.augment</code>之间的区别，尽管它们的名称如此显著。现在有些时间，我相信分析源码是最好的解决方法。为了减少不必要的干扰，我简化了这两个方法，使它们仅处理类构造器。</p><h2 id="Y-extend"><a href="#Y-extend" class="headerlink" title="Y.extend"></a>Y.extend</h2><p><code>Y.extend</code>方法应用的场景很简单，就是继承。我们知道，JavaScript有多种继承方式，例如原型继承、构造器继承、组合继承、寄生继承等等。YUI采取的为寄生组合继承（Parasitic combination inheritance）。</p><p><code>Y.extend</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><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">Y.extend = <span class="function"><span class="keyword">function</span> (<span class="params">r, s</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> sp = s.prototype, rp = Y.Object(sp);</span><br><span class="line">    r.prototype = rp;</span><br><span class="line"></span><br><span class="line">    rp.constructor = r;</span><br><span class="line">    r.superclass = sp;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> r;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">Y.Object = <span class="function"><span class="keyword">function</span> (<span class="params">o</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> F = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;&#125;;</span><br><span class="line">    F.prototype = o;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> F();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先获取一个<code>__proto__</code>指向父类原型的空对象<code>rp</code>，然后将其作为子类原型，这样子类实例就可以沿原型链父类原型方法。然后设置子类的<code>superclass</code>属性为父类原型，使得子类构造器可以访问到父类。</p><span id="more"></span><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Programmer</span>(<span class="params">name</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.name = name;</span><br><span class="line">&#125;</span><br><span class="line">Programmer.prototype.getName = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.name;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">FrontEndProgrammer</span>(<span class="params">name, gender</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 调用父类构造器</span></span><br><span class="line">    FrontEndProgrammer.superclass.constructor.call(<span class="built_in">this</span>, name);</span><br><span class="line">        <span class="built_in">this</span>.gender = gender;</span><br><span class="line">&#125;</span><br><span class="line">FrontEndProgrammer.prototype.getGender = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.gender;</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">Y.extend(FrontEndProgrammer, Programmer);</span><br></pre></td></tr></table></figure><h2 id="Y-augment"><a href="#Y-augment" class="headerlink" title="Y.augment"></a>Y.augment</h2><p>OOP中，继承是一种主要方式，但还有另一种方式同样重要，即组合(Composition)。组合不同于继承的是，不会在类之间建立关系，只将提供类（类似父类）的属性、原型属性和方法添加在接受类（类似子类）中。<code>Y.augment</code>就是YUI中一种用来组合类的方法。</p><p>Y.augment的简化代码如下：</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><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">Y.augment = <span class="function"><span class="keyword">function</span> (<span class="params">receiver, supplier, args</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> rProto    = receiver.prototype,</span><br><span class="line">        sProto    = supplier.prototype,</span><br><span class="line">        copy,</span><br><span class="line">        property;</span><br><span class="line"></span><br><span class="line">    args = args ? Y.Array(args) : [];</span><br><span class="line"></span><br><span class="line">    copy = <span class="function"><span class="keyword">function</span> (<span class="params">value, key</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!(key <span class="keyword">in</span> rProto)) &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">Object</span>.prototype.toString.call(value) === <span class="string">&#x27;[object Function]&#x27;</span>) &#123;</span><br><span class="line">                rProto[key] = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">                    <span class="comment">// 将方法赋给实例</span></span><br><span class="line">                    <span class="built_in">this</span>[key] = value;</span><br><span class="line">                    <span class="comment">// 执行提供类构造器</span></span><br><span class="line">                    supplier.apply(<span class="built_in">this</span>, args);</span><br><span class="line">                    <span class="comment">// 执行提供类方法</span></span><br><span class="line">                    <span class="keyword">return</span> value.apply(<span class="built_in">this</span>, <span class="built_in">arguments</span>);                </span><br><span class="line">                &#125;;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                rProto[key] = value;</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">for</span> (property <span class="keyword">in</span> sProto) &#123;</span><br><span class="line">        copy.call(<span class="literal">null</span>, sProto[property], property);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> receiver;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>逻辑比较简单，将提供类原型属性处理后拷贝给接受类原型。如果拷贝的是一个方法，则使用一个代理方法，主要作用是执行一次提供类构造器，使接受类对象获得提供类构造器中添加的属性。</p><p>从代码中可以发现，<code>Y.augment</code>的第三个参数<code>args</code>是传递给提供类构造器的，问题在于<code>args</code>只能在执行<code>Y.augment</code>时指定，也就是说不能在创建接受类实例时指定。这是<code>Y.augment</code>与<code>Y.extend</code>非常重要的一个区别。</p><p>另外一个问题是，接受类实例在执行不同提供类原型方法时，提供类构造器会被多次执行，在提供类构造器中逻辑比较复杂时会引起显而易见的效率问题。这个问题是由于我简化代码的缘故，<a href="http://yuilibrary.com/yui/docs/api/files/oop_js_oop.js.html#l31">YUI源码</a>中采用了一种方法隔离技术，能够在第一次调用提供类方法时才将所有提供类方法赋给接收类实例，并保证只执行一次提供类构造器。</p><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Code</span>(<span class="params">language</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.language = language;</span><br><span class="line">&#125;</span><br><span class="line">Code.prototype.getLanguage = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.language;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">FrontEndProgrammer</span>(<span class="params">name, gender</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.name = name;</span><br><span class="line">    <span class="built_in">this</span>.gender = gender;</span><br><span class="line">&#125;</span><br><span class="line">FrontEndProgrammer.prototype.getName = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.name;</span><br><span class="line">&#125;;</span><br><span class="line">FrontEndProgrammer.prototype.getGender = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.gender;</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">Y.augment(FrontEndProgrammer, Code, <span class="string">&#x27;JavaScript&#x27;</span>);</span><br></pre></td></tr></table></figure><h2 id="Y-extend与Y-augment区别"><a href="#Y-extend与Y-augment区别" class="headerlink" title="Y.extend与Y.augment区别"></a>Y.extend与Y.augment区别</h2><p><code>Y.extend</code>和<code>Y.augment</code>很好的体现了OOP两种主要方式：继承和组合。设计模式中提倡使用组合方式：Favor ‘object composition’ over ‘class inheritance’ （<a href="http://en.wikipedia.org/wiki/Design_Patterns">Gang of Four</a> 1995:20）。</p><p>总结一下，<code>Y.extend和</code>Y.augment`有如下区别：</p><ul><li><code>Y.extend</code>改变原型链，可以通过<code>instanceof</code>操作符判定子类实例与父类关系</li><li><code>Y.extend</code>可以在创建子类实例时指定传递给父类的参数，<code>Y.augment</code>只能在组合类时设定</li><li><code>Y.augment</code>会将提供类原型方法赋给接受类实例</li></ul><p>根据各自特点，可以发现，<code>Y.extend</code>更适合从属关系非常强的两个类，例如男人和人，男人的主体属性是人，附加一些胡须、喉结、力气之类的特征；<code>Y.augment</code>更适合提供类是接受类一个扩展的情况，例如程序员和工具书，工具书只是程序员用来参考的工具，而不是主要属性。</p><p>以下是YUI3中的一些实际使用例子：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Y.Base作为YUI组件框架的核心，为继承它的子类提供了属性管理、事件、生命周期等方法。</span></span><br><span class="line">Y.extend(Y.Anim, Y.Base);</span><br><span class="line">Y.extend(ScrollView, Y.Widget);</span><br><span class="line">Y.extend(ACListPlugin, Y.AutoCompleteList);</span><br><span class="line">Y.extend(CacheOffline, Y.Cache);</span><br><span class="line">Y.extend(Calendar, Y.CalendarBase);</span><br><span class="line"></span><br><span class="line"><span class="comment">// EventTarget定义了一整套自定义事件、AOP的机制，通过Y.augment可以方便的赋给接受类这些方法。</span></span><br><span class="line">Y.augment(Y.Node, Y.EventTarget);</span><br><span class="line">Y.augment(Y.DataSource.Local, Pollable);</span><br><span class="line">Y.augment(Lines, Y.Attribute);</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://yuilibrary.com/yui/docs/api/files/oop_js_oop.js.html">YUI OOP API</a></li><li><a href="http://www.yuiblog.com/blog/2010/01/06/inheritance-patterns-in-yui-3/">Inheritance Patterns in YUI 3</a></li><li><a href="http://www.yuiblog.com/blog/2010/01/07/more-code-reuse-patterns-in-yui3/">More code reuse patterns in YUI3</a></li><li><a href="http://en.wikipedia.org/wiki/Design_Patterns">Design Patterns</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;很长一段时间内，我都没有搞懂YUI3 OOP模块中的&lt;code&gt;Y.extend&lt;/code&gt;方法与&lt;code&gt;Y.augment&lt;/code&gt;之间的区别，尽管它们的名称如此显著。现在有些时间，我相信分析源码是最好的解决方法。为了减少不必要的干扰，我简化了这两个方法，使它们仅处理类构造器。&lt;/p&gt;
&lt;h2 id=&quot;Y-extend&quot;&gt;&lt;a href=&quot;#Y-extend&quot; class=&quot;headerlink&quot; title=&quot;Y.extend&quot;&gt;&lt;/a&gt;Y.extend&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Y.extend&lt;/code&gt;方法应用的场景很简单，就是继承。我们知道，JavaScript有多种继承方式，例如原型继承、构造器继承、组合继承、寄生继承等等。YUI采取的为寄生组合继承（Parasitic combination inheritance）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Y.extend&lt;/code&gt;的简化代码如下：&lt;/p&gt;
&lt;figure class=&quot;highlight javascript&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;Y.extend = &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;r, s&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; sp = s.prototype, rp = Y.Object(sp);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    r.prototype = rp;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    rp.constructor = r;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    r.superclass = sp;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; r;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Y.Object = &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;o&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; F = &lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;params&quot;&gt;&lt;/span&gt;) &lt;/span&gt;&amp;#123;&amp;#125;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    F.prototype = o;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; F();&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;首先获取一个&lt;code&gt;__proto__&lt;/code&gt;指向父类原型的空对象&lt;code&gt;rp&lt;/code&gt;，然后将其作为子类原型，这样子类实例就可以沿原型链父类原型方法。然后设置子类的&lt;code&gt;superclass&lt;/code&gt;属性为父类原型，使得子类构造器可以访问到父类。&lt;/p&gt;</summary>
    
    
    
    
    <category term="JavaScript" scheme="https://springuper.github.io/tags/JavaScript/"/>
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="OOP" scheme="https://springuper.github.io/tags/OOP/"/>
    
  </entry>
  
  <entry>
    <title>Javascript核心概念</title>
    <link href="https://springuper.github.io/javascript-the-core/"/>
    <id>https://springuper.github.io/javascript-the-core/</id>
    <published>2011-12-28T23:49:00.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<div style="text-align: center;"><iframe src="http://www.slideshare.net/slideshow/embed_code/10708244" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" width="425" height="355"></iframe></div><div style="text-align: center;"><a href="/assets/the-core-of-javascript.key_.zip">keynote下载</a> | <a href="/assets/the-core-of-javascript.pdf.zip">pdf下载</a></div><span id="more"></span><h2 id="Notes"><a href="#Notes" class="headerlink" title="Notes"></a>Notes</h2><ol><li><p>这次分享一些Javascript或者说ECMAScript中的一些比较基础的知识点。这些东西在我们日常的开发和调试中发挥着非常重要而又容易被忽略的作用。希望这次分享能够让大家有一个更深入的认识。</p></li><li><p>对于我们所使用的核心技术，我们要做到知其然知其所以然。这是一个非常重要的态度，只有这样，我们才能更好的理解这些技术背后蕴涵的思想，运作机制，以及优点缺陷，才能免于陷入缤纷表现的漩涡，能够做到透过现象看本质。</p></li><li><p>每个人的精力都是有限的，尤其是我这种愚钝的人，所以，把你的精力放在最值得放的一些地方，而不是处处留芳。</p></li><li><p>这次我们主要分享这几个知识点：原型链. 构造器. 执行上下文. 变量对象. 作用域链. 闭包和This</p></li><li><p>对象是一切的基础，我们从这里起步，用一个个对象构建前端的摩天大楼！对象实际非常简单，它就是一个容器，可以以键值对的形式储藏信息，例如姓名. 性别。对象有一个非常重要的内部属性__proto__，指向它的原型对象，以实现方法. 属性的复用或着说继承。</p></li><li><p>我们写过这样的代码，那它是如何执行的呢？事实上，这里涉及到一个属性查找的问题。[]对象本身没有hasOwnProperty这个方法，于是继续查找__proto__属性指向Array prototype是否有该方法，依然查找失败，于是继续查找Array prototype对象的__proto__属性指向的对象…最终找到Object.prototype上有这个方法，执行的结果为true。每个对象通过__proto__属性与自己的原型对象建立联系，而且这个过程是可以延续的，这就是js里原型链的概念。通过这个例子，我们知道：原型链在JS中的作用是查找对象的属性/方法。</p></li><li><p>原型链除了方法的复用还有什么作用呢？一个重要的例子就是instanceof操作符。instanceof操作符就是通过判断第二个操作数的原型对象是否在第一个操作数原型链上来进行判别。</p></li><li><p>接下来的一个疑问是：这些对象是怎么得来的呢？答案是构造器。</p></li><li><p>构造器，在JS中也就是函数。这里展示下函数的创建过程。这里需要注意的几点是：</p><ul><li>[[Class]]：因为typeof不准确，现在各个js库都是根据这个属性来判别对象类型的（Object.prototype.toString.call(obj)）</li><li>[[Prototype]]：因为函数也是一个对象，所以也有指向自己原型的内部属性</li><li>[[Call]]：在执行函数时调用，会产生一个新的执行上下文</li><li>[[Construct]]：在函数作为构造器调用时调用（通过new操作符）</li><li>[[Scope]]：保存函数的作用域</li><li>length：函数期望参数数目</li><li>prototype：用来为构造器创建对象设置[[prototype]]</li></ul></li><li><p>制造对象通过函数对象的内部属性[[Construct]]进行。</p><ul><li>创建一个原生对象，这个对象是”纯洁的“</li><li>添加这个原生对象的内部属性[[Class]]，这个属性是用来判定对象类型的</li><li>然后添加这个原生对象的内部属性[[Prototype]]，也记做__proto__</li><li>执行函数对象代码，其中的this设置为刚刚创建的原生对象。记执行代码返回值为R</li><li>如果R为对象，则返回R，否则返回创建的原生对象O</li></ul></li><li><p>一个简单的例子。</p></li><li><p>只要记住一点：构造器的prototype属性是用来为创建对象设置__proto__属性的，构造器作为对象也有自身的__proto__属性，指向的是Function.prototype对象。</p></li><li><p>JS中的代码可以划分为三种类型：全局代码. 函数代码和eval代码。eval这个方法我们要尽量少的使用，因为不单有创建一个新解析器的开销，还有安全性方面的问题。</p></li><li><p>每种类型的代码在执行时都会在相应类型的上下文中，我们称之为执行上下文。</p></li></ol><p>15-31. 这个例子简单展示了执行上下文栈的运行过程。</p><ol start="32"><li><p>接下来我们详细了解一下执行上下文。执行上下文也可以理解为一个对象，主要的属性有三个：变量对象. 作用域链和thisValue。</p><ul><li>变量对象是一个保存当前代码中所有变量. 函数声明等的对象</li><li>作用域链是由执行上下文的变量对象和所有父级作用域构成的，可以理解为一个数组</li><li>thisValue用来指明当前代码中this所代表的值</li></ul></li><li><p>变量对象根据执行上下文的不同有一些差异:</p><ul><li>在全局执行上下文中，变量对象就是全局对象本身，而this也是指向全局对象的，所以它们三个相等。这个特性非常重要，因为我们可以通过访问全局对象来获得变量对象中储存的变量，这也是唯一可以访问变量对象的情形</li><li>在函数执行上下文中，变量对象增加了arguments对象和行参等，这被称为活动对象。我们是不能直接访问到活动对象的</li></ul></li><li><p>全局变量对象的一个例子。</p></li><li><p>活动对象的一个例子。注意arguments对象是一个array-like的对象而不是一个array。</p></li><li><p>代码的执行过程分为两步，首先进入执行上下文，主要是初始化上下文中的三个属性：变量对象. 作用域链和thisValue，然后开始执行代码。</p></li><li><p>在进入执行上下文阶段，初始化变量对象的过程是非常值得注意，因为在这个过程中，函数中所有形参. 变量声明. 函数声明都会被保存至变量对象中，进而影响作用域链，最终影响到变量的查找。初始化变量分为三步，这其中要注意的是：</p><ul><li>所有函数声明都会将函数名添加为变量对象的一个属性，函数对象为该属性的值，所以我们可以将函数定义在任何位置。为了主体逻辑更加清晰，一般我们都将函数声明放在靠后的位置</li><li>所有变量声明的变量名都会作为变量对象的一个属性，其值为undefined。在执行代码阶段，解析器其实不会理会var的。为了防止忘记声明变量，所以建议在函数开始时声明所有局部变量</li><li>变量对象属性之间的覆盖问题。函数声明的函数名可以覆盖之前所有VO属性，变量声明的变量只能覆盖之前的重名变量</li></ul></li><li><p>声明可以不被覆盖，但是语句仍然会执行。在本例中，形参x没有被变量x覆盖，但执行var x ＝ 10之后x的值仍然会变化。形参y会被函数声明y覆盖。</p></li><li><p>一个作用域链的例子。</p></li><li><p>作用域链的几个特性，静态作用域的概念非常重要，函数在声明的时候即已确定自身的作用域链内容([[Scope]])。</p></li><li><p>闭包实现的基础有两个方面：</p><ul><li>函数是一级对象，可以像普通对象一样赋值给变量. 作为函数返回值. 作为参数传递给函数</li><li>函数对象会保存声明时的作用域链，也就是函数具有静态作用域</li></ul><p>这两个特性使得JS中每个函数都是一个闭包，为这种语言增加了无限的变化和魔力。</p></li><li><p>函数作为返回值，或者称为自底向上的闭包。</p></li><li><p>函数作为参数，或者称为自上到下的闭包。</p></li><li><p>this在进入执行上下文阶段被赋值，在全局执行上下文中就是全局对象，在函数执行上下文中会随着函数的调用方式不同有所变化。</p></li><li><p>本文主要参考了Dmitry Soshnikov的ECMA-262-3 in detail系列文章，我本人获益匪浅。</p></li><li><p>谢谢大家。</p></li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://dmitrysoshnikov.com/ecmascript/javascript-the-core/">The Core</a></li><li><a href="http://dmitrysoshnikov.com/tag/ecma-262-3/">ECMA-262-3 in detail</a></li><li><a href="http://es5.github.com/">Annotated ECMAScript 5.1</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;div style=&quot;text-align: center;&quot;&gt;&lt;iframe src=&quot;http://www.slideshare.net/slideshow/embed_code/10708244&quot; frameborder=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; scrolling=&quot;no&quot; width=&quot;425&quot; height=&quot;355&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;/assets/the-core-of-javascript.key_.zip&quot;&gt;keynote下载&lt;/a&gt; | &lt;a href=&quot;/assets/the-core-of-javascript.pdf.zip&quot;&gt;pdf下载&lt;/a&gt;&lt;/div&gt;</summary>
    
    
    
    
    <category term="Closure" scheme="https://springuper.github.io/tags/Closure/"/>
    
    <category term="Execution Context" scheme="https://springuper.github.io/tags/Execution-Context/"/>
    
    <category term="Prototype Chain" scheme="https://springuper.github.io/tags/Prototype-Chain/"/>
    
  </entry>
  
  <entry>
    <title>Javascript错误监控机制</title>
    <link href="https://springuper.github.io/js-error-catch/"/>
    <id>https://springuper.github.io/js-error-catch/</id>
    <published>2011-11-06T20:30:00.000Z</published>
    <updated>2026-10-03T09:20:43.752Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>If an error is possible, someone will make it. The designer must assume that all possible errors will occur and design so as to minimize the chance of the error in the first place, or its effects once it gets made. Errors should be easy to detect, they should have minimal consequences, and, if possible, their effects should be reversible.<em>–Donald Norman, author, The Design of Everyday Things</em></p></blockquote><p>我觉得后端工程师较前端工程师最大的便利之一就是：<strong>良好的代码监控机制</strong>。</p><p>每次后端工程师上线后，如果PHP运行中出现错误，会立即记录在error log中，并由脚本根据不同的错误级别进行邮件、短信报警。前端工程师在这方面被动很多，大多数情况下是通过用户反映给客服，然后客服再通知技术部门解决，中间的链条甚至会更长一些，因此前端方面的bug造成的影响往往较为严重。</p><p>有什么办法可以缓解这一点，能够让我们在错误面前变被动为主动，第一时间解决问题呢？下面就介绍一种简单的Javascript错误监控机制。</p><h2 id="Javascript错误模型"><a href="#Javascript错误模型" class="headerlink" title="Javascript错误模型"></a>Javascript错误模型</h2><h3 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h3><center>  <img alt="error flow" src="/images/error-flow.png" /></center><p>如上图所示，一个错误发生后，首先会被<code>try-catch</code>处理，如果没有被停止，则继续传递给<code>window.onerror</code>处理，如果没有被停止，则最终传递给浏览器控制台处理。</p><span id="more"></span><h4 id="try-catch"><a href="#try-catch" class="headerlink" title="try-catch"></a>try-catch</h4><p><code>try-catch</code>属于ECMAScript中的语句，能够捕捉到顺序执行代码中的错误，以及<code>throw</code>抛出的错误，但遗憾的是，它对于延时执行（或称异步执行，例如通过事件、计时器等触发的代码）代码却无能为力。</p><p><code>try-catch</code>捕捉到的错误会作为catch的第一个参数，记作ex，兼容性较好的一个属性为<code>ex.message</code>，此属性在IE、FireFox、Chrome中均能够较为准确的描述错误信息。例如：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    M.unexistMethod();</span><br><span class="line">&#125; <span class="keyword">catch</span>(ex) &#123;</span><br><span class="line">    alert(ex.message);</span><br><span class="line">    <span class="comment">// IE: &quot;Object does&#x27;t support this property or method&quot;</span></span><br><span class="line">    <span class="comment">// FF: &quot;M.unexistMethod is not a function&quot;</span></span><br><span class="line">    <span class="comment">// Chrome: &quot;Object # has no method &#x27;unexistMethod&#x27;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>被<code>try-catch</code>捕捉到的错误不会继续传播。如果需要的话，可以在<code>catch</code>中通过<code>throw</code>语句向更高层次传播错误。</p><h4 id="window-onerror"><a href="#window-onerror" class="headerlink" title="window.onerror"></a>window.onerror</h4><p><code>window.onerror</code>属于DOM的范畴，其支持和实现情况自然有很多差异。<code>window.onerror</code>先由IE、FireFox支持，并被加入到HTML5标准中。现在Chrome、Safari新版本均已支持，Opera在12 alpha中会增加支持。<code>window.onerror</code>可以捕获到所有类型的js错误，包括顺序、延时代码，这是它非常明显的优点。</p><p>在错误的处理方面，各个浏览器也是有些差异的。例如，在js文件与页面不在同一个域时：</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></pre></td><td class="code"><pre><span class="line"><span class="built_in">window</span>.onerror = <span class="function"><span class="keyword">function</span>(<span class="params">msg, url, line</span>) </span>&#123;</span><br><span class="line">    alert(msg + <span class="string">&#x27;:&#x27;</span> + line);</span><br><span class="line">    <span class="comment">// IE:&quot;Object does&#x27;t support this property or method:7&quot;</span></span><br><span class="line">    <span class="comment">// FF:&quot;Script error.:0&quot;</span></span><br><span class="line">    <span class="comment">// Chrome:&quot;Script error.:0&quot;</span></span><br><span class="line">&#125;;</span><br><span class="line">M.unexistMethod();</span><br></pre></td></tr></table></figure><p>可以看出，FireFox和Chrome下都是统一的“Script error.”，且行号错误，这样的信息除了告诉我们有错误以外没有任何意义。有人解释说这是出于同源策略的考虑。如果你的js文件与页面在同一个域中，那么恭喜，在下面的错误日志收集机制中只需使用<code>window.onerror</code>就可以了。</p><p>此外，<code>window.onerror</code>在FireFox中可以捕捉到js文件加载失败的错误。</p><h3 id="错误日志收集"><a href="#错误日志收集" class="headerlink" title="错误日志收集"></a>错误日志收集</h3><p>综合<code>try-catch</code>和<code>window.onerror</code>两种错误捕捉方式的特点，设计一个较为简单的错误日志收集机制如下：</p><ul><li>考虑到<code>try-catch</code>能够获得更为准确的错误描述信息，兼容性良好，且在错误传播模型中处于最底层，所以将页面方法入口放在<code>try-catch</code>内，这样在这些方法顺序代码中出现的错误就可以被<code>try-catch</code>捕获到</li><li>虽然<code>window.onerror</code>非常强大，但由于<code>window.onerror</code>在兼容性方面的问题，只将延时代码中可能出现的错误交给<code>window.onerror</code>处理</li><li>捕获到错误后，使用简单强大的beacon将错误信息传递给日志服务器</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// base.js</span></span><br><span class="line"><span class="keyword">var</span> M ＝ &#123;</span><br><span class="line">    <span class="attr">toQueryString</span>: <span class="function"><span class="keyword">function</span>(<span class="params">o</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> res = [], p, encode = <span class="built_in">encodeURIComponent</span>;</span><br><span class="line">        <span class="keyword">for</span> (p <span class="keyword">in</span> o) &#123;</span><br><span class="line">             <span class="keyword">if</span> (o.hasOwnProperty(p)) res.push(encode(p) + <span class="string">&#x27;=&#x27;</span> + encode(o[p]));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> res.join(<span class="string">&#x27;&amp;&#x27;</span>);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">beacon</span>: <span class="function"><span class="keyword">function</span>(<span class="params">msg</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> img = <span class="keyword">new</span> Image();</span><br><span class="line">        img.src = <span class="string">&#x27;http://xxx.com/_.gif?&#x27;</span> + msg;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">log</span>: <span class="function"><span class="keyword">function</span>(<span class="params">info</span>) </span>&#123;</span><br><span class="line">        M.beacon(M.toQueryString(info));</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">runMethod</span>: <span class="function"><span class="keyword">function</span>(<span class="params">method</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            method();</span><br><span class="line">        &#125; <span class="keyword">catch</span>(ex) &#123;</span><br><span class="line">            M.log(&#123; <span class="attr">msg</span>: ex.message, <span class="attr">type</span>: <span class="string">&#x27;try-catch&#x27;</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 class="built_in">window</span>.onerror = <span class="function"><span class="keyword">function</span>(<span class="params">msg, url, line</span>) </span>&#123;</span><br><span class="line">    M.log(&#123; <span class="attr">msg</span>: msg, <span class="attr">url</span>: url, <span class="attr">line</span>: line, <span class="attr">type</span>: <span class="string">&#x27;onerror&#x27;</span> &#125;);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// app.js</span></span><br><span class="line">M.app = &#123;</span><br><span class="line">    editAddress = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> elForm = <span class="built_in">document</span>.getElementById(<span class="string">&#x27;address-form&#x27;</span>);</span><br><span class="line">        elForm.onsubmit = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">            <span class="keyword">if</span> (elForm.address.value === <span class="string">&#x27;&#x27;</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;;</span><br><span class="line">        elForm.address.focus();</span><br><span class="line">    &#125;</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><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="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&quot;address&quot;</span> <span class="attr">method</span>=<span class="string">&quot;post&quot;</span> <span class="attr">id</span>=<span class="string">&quot;address-form&quot;</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- the name is misspelled, so it may cause an error --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;adress&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">value</span>=<span class="string">&quot;Submit&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;text/javascript&quot;</span> <span class="attr">src</span>=<span class="string">&quot;base.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;text/javascript&quot;</span> <span class="attr">src</span>=<span class="string">&quot;app.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="javascript"></span></span><br><span class="line"><span class="javascript">M.runMethod(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span></span><br><span class="line"><span class="javascript">    M.app.editAddress();</span></span><br><span class="line"><span class="javascript">&#125;);</span></span><br><span class="line"><span class="javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>完整代码见<a href="https://github.com/springuper/log-error">Github</a>。</p><h2 id="错误日志处理"><a href="#错误日志处理" class="headerlink" title="错误日志处理"></a>错误日志处理</h2><p>由于javascript运行在种类、版本繁多的浏览器中，用户的网络情况也千差万别，直接造成的问题就是错误日志中包含很多垃圾信息，例如：</p><ul><li>Script error.</li><li>Object doesn’t support this property or method.</li></ul><p>这些错误信息信息量太少，不能准确描述错误的真正原因，需要进行过滤。</p><p>在监听到一些描述准确的错误后，需要及时发送错误报警，形式可以为邮件、短信，这样就可以在短时间内将线上的问题反馈给前端工程师，减少错误响应时间。</p><p>前端工程师终于可以放心大胆的上线了！</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://www.w3.org/TR/2010/WD-html5-20100624/webappapis.html#handler-window-onerror">HTML5标准-window.onerror</a></li><li><a href="http://msdn.microsoft.com/en-us/library/cc197053%28VS.85%29.aspx">MSDN-window.onerror</a></li><li><a href="https://developer.mozilla.org/en/DOM/window.onerror">MDN-window.onerror</a></li><li><a href="http://www.slideshare.net/nzakas/enterprise-javascript-error-handling-presentation">Enterprise JavaScript Error Handling</a></li><li><a href="http://stackoverflow.com/questions/5913978/cryptic-script-error-reported-in-javascript-in-chrome-and-firefox">Cryptic “Script Error.” reported in Javascript in Chrome and Firefox</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;If an error is possible, someone will make it. The designer must assume that all possible errors will occur and design so as to minimize the chance of the error in the first place, or its effects once it gets made. Errors should be easy to detect, they should have minimal consequences, and, if possible, their effects should be reversible.
&lt;em&gt;–Donald Norman, author, The Design of Everyday Things&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我觉得后端工程师较前端工程师最大的便利之一就是：&lt;strong&gt;良好的代码监控机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;每次后端工程师上线后，如果PHP运行中出现错误，会立即记录在error log中，并由脚本根据不同的错误级别进行邮件、短信报警。前端工程师在这方面被动很多，大多数情况下是通过用户反映给客服，然后客服再通知技术部门解决，中间的链条甚至会更长一些，因此前端方面的bug造成的影响往往较为严重。&lt;/p&gt;
&lt;p&gt;有什么办法可以缓解这一点，能够让我们在错误面前变被动为主动，第一时间解决问题呢？下面就介绍一种简单的Javascript错误监控机制。&lt;/p&gt;
&lt;h2 id=&quot;Javascript错误模型&quot;&gt;&lt;a href=&quot;#Javascript错误模型&quot; class=&quot;headerlink&quot; title=&quot;Javascript错误模型&quot;&gt;&lt;/a&gt;Javascript错误模型&lt;/h2&gt;&lt;h3 id=&quot;生命周期&quot;&gt;&lt;a href=&quot;#生命周期&quot; class=&quot;headerlink&quot; title=&quot;生命周期&quot;&gt;&lt;/a&gt;生命周期&lt;/h3&gt;&lt;center&gt;
  &lt;img alt=&quot;error flow&quot; src=&quot;/images/error-flow.png&quot; /&gt;
&lt;/center&gt;

&lt;p&gt;如上图所示，一个错误发生后，首先会被&lt;code&gt;try-catch&lt;/code&gt;处理，如果没有被停止，则继续传递给&lt;code&gt;window.onerror&lt;/code&gt;处理，如果没有被停止，则最终传递给浏览器控制台处理。&lt;/p&gt;</summary>
    
    
    
    
    <category term="try-cacth" scheme="https://springuper.github.io/tags/try-cacth/"/>
    
    <category term="onerror" scheme="https://springuper.github.io/tags/onerror/"/>
    
  </entry>
  
  <entry>
    <title>w3cTech 21期在美团举行</title>
    <link href="https://springuper.github.io/w3ctech-meituan/"/>
    <id>https://springuper.github.io/w3ctech-meituan/</id>
    <published>2011-08-04T20:05:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p><img src="../images/w3ctech-meituan.jpg" alt="w3ctech in meituan"></p><p>很高兴能够作为东道主参加w3cTech。</p><p>在我看来，前端行业更加团结、热情，有非常开放、分享的精神。非常喜欢这个行业的风气。</p><p>此次交流会，我做了关于我们团队在YUI3方面的一些实践（<a href="/yui3-in-meituan.html">YUI3在美团</a>），虽然还很初级，但我们在慢慢强大。在行进中开火，纵情向前！</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;img src=&quot;../images/w3ctech-meituan.jpg&quot; alt=&quot;w3ctech in</summary>
        
      
    
    
    
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="W3CTech" scheme="https://springuper.github.io/tags/W3CTech/"/>
    
    <category term="Meituan" scheme="https://springuper.github.io/tags/Meituan/"/>
    
  </entry>
  
  <entry>
    <title>YUI3在美团</title>
    <link href="https://springuper.github.io/yui3-in-meituan/"/>
    <id>https://springuper.github.io/yui3-in-meituan/</id>
    <published>2011-08-04T19:51:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<div style="text-align:center;"><div style="width:425px; margin:0 auto;" id="__ss_8759550"><object id="__sse8759550" width="425" height="355"><param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=yui3-110802225931-phpapp02&stripped_title=yui3-8759550&userName=springuper" /><param name="allowFullScreen" value="true"/><param name="allowScriptAccess" value="always"/><embed name="__sse8759550" src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=yui3-110802225931-phpapp02&stripped_title=yui3-8759550&userName=springuper" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"></embed></object></div></div><p style="text-align:center; text-indent:0;"><a href="/assets/YUI3-in-Meituan.key_.zip">keynote下载</a> | <a href="/assets/YUI3-in-Meituan.pdf.zip">pdf下载</a></p><span id="more"></span><h2 id="Notes："><a href="#Notes：" class="headerlink" title="Notes："></a>Notes：</h2><ol><li><p>常荣幸和大家一起分享我们团队在YUI3方面的一些实践。</p></li><li><p>我叫尚春，他们都叫我春哥。这里是我的博客和email。我在2010年12月11日毕业后进入美团工作，到现在已有7个多月的工作经验。这段时间，在师傅的指导下迅速成长，对前端的认识不断深入。</p></li><li><p>此次分享主要涉及两方面的内容。首先是关于选择YUI3作为基础库的原因，这里我们重点介绍下YUI3的模块机制；然后我们主要就我们实际迁移中遇到的一些挑战作详细表述，例如划分模块、加载策略等。</p></li><li><p>简要介绍下我们的前端团队。我们是一支小而精悍的队伍，为美团网视觉界面和用户交互提供充足动力。</p><ul><li>我们崇尚的是“快”，通过快速开发、快速部署、快速迭代来完成任务、优化功能</li><li>我们坚持“在行进中开火”</li><li>我们认为做事不应该准备太多，一定要先做起来，然后发现不足并不断改进，宁可十年不将军，不可一日不拱卒</li></ul><p> 这些信条可能更加适合我们这样的小团队。</p></li><li><p>我们首先要回答的问题是：为什么选择YUI3。第一个原因，我们原来采用的是YUI2。作为YUI团队的作品，YUI2非常正统、稳定。第二个原因，YUI团队已经把重心转移到YUI3，基本不再维护YUI2。但这都不是最有说服力的原因。</p></li><li><p>最为重要的原因是YUI3本身，它足够的优秀。YUI3具有很多新的特性，例如模块机制、选择器的全面应用、更强大的自定义事件、组件框架等等。如果说YUI2是库时代杰出典范之一的话，YUI3就是框架时代一颗璀璨的明星。</p></li><li><p>时间有限，我们重点介绍下YUI3的模块机制。模块是YUI3最大的变化，没有之一。这一底层设计机制改变了原有YUI2时代的规则。它为我们带来了自动加载、沙箱、combo等等特性，解放生产力的同时，提高了多人协作上的便利。使用YUI3添加模块、调用模块比较简单。</p></li><li><p>我们把源码进行适当简化，主要理解一下内部的机制。YUI构造器的add方法是这样定义的，即将模块的信息注册在YUI构造器的静态属性Env中。</p></li><li><p>YUI的use方法主要流程是：计算调用模块所依赖的所有模块，将这些模块进行排序并输出，通过attach方法将这些模块的api依次捆绑在YUI实例（沙箱）上,将载满api的YUI实例传递到回调函数中。</p></li><li><p>与直接嵌入script标签相比，模块的特点有：</p><ul><li>注册模块与调用模块分离，两者不是紧密耦合关系</li><li>所有模块都加载并绑定后才会执行回调函数，多个use间是异步关系。但，当所有模块都具备的情况下，多个use又是同步关系</li><li>通过实例绑定调用模块的api，实现了一种简便的沙箱机制。这一机制有利于协同开发</li><li>在模块定义时的一些元信息将所有模块有序组织起来，形成一个系统</li></ul></li><li><p>YUI3的所有模块分为四个层级，它们构成了YUI3的架构。</p><ul><li>最底层是种子文件YUI，它定义了YUI构造器以及yui模块，主要包括Lang，UA，封装的Array, Object, 以及Get等等</li><li>第二层为核心工具层，主要是一些工具模块，例如DOM，Node，Event</li><li>第三层为组件框架层，主要是为构建组件提供丰富灵活的机制</li><li>第四层为组件层，包括了IO，JSON，Animation等重要模块</li></ul></li><li><p>我们遇到很多挑战：</p><ul><li>如何将原来的功能划分为模块，如何将所有模块组织起来，这是比较大的一个挑战</li><li>Combo服务的实现。yahoo的combo只能提供YUI自身模块，而且速度在国内并不占优势。我们采用了minify，进行了一些修改和配置，能够在线上线下提供combo的支持</li><li>模块加载策略的研究</li><li>版本管理的问题。原来我们采用了query string的方式进行版本管理，现在则是用embedding的方式</li></ul></li><li><p>目前，我们已经将大多数后台系统迁移到了YUI3。本季度我们要完成主站的迁移工作。</p></li><li><p>在介绍迁移之前，先看一下原来我们组织js的方式。这里主要包含三类文件：二次开发的核心文件core.js和一些widget文件，应用层级的文件，例如app-deal.js，来自第三方的文件，例如jquery.js。这种方式非常简单，但在web app时代已经不再适合较为复杂的前端开发，维护成本比较高。</p></li><li><p>那如何划分模块呢，我们总结了如下几条原则：</p><ul><li>抽象与应用必须脱离。更通用的功能应该放在更低的层级，应用层完全面向实际问题，在解决的过程中调用抽象出来的方法</li><li>职责单一。保持每个模块的职责足够简单，方便维护和可持续开发</li><li>粒度得当。有了combo，我们可以不必担心粒度太小，文件过多导致的速度问题。但是，从可维护的角度来考虑，粒度应该适当而不宜过小，避免海底捞针的情形出现</li><li>我们的模块体系应该是开放的，不符合YUI规范的第三方模块，我们可以借鉴整合进来，使我们的基础框架更加完善，更加性感</li></ul></li><li><p>美团的JS架构，按照抽象与应用程度，分为四层：</p><ul><li>最底层交给强悍的YUI3，为我们提供很跨浏览器的api和很好的框架</li><li>第二层是我们二次开发的一些核心方法和控件库。mt-base中包含strtotime，getRadioValue等基础方法，对Cookie的简单操作，一些封装的页面动画等。w-base，w-autocomplete等包含一些封装的控件，提供更加高效的用户体验，且便于后端人员调用</li><li>第三层包含美团各个分站点的一些通用模块。例如mis-base包含后台mis系统的消息系统、checkFormChanged等通用方法。这一层更加接近应用</li><li>最上面一层，应用模块，这些模块的方法都是用来解决实际问题的。例如mis-deal用来处理mis系统中所有deal相关页面的交互，finance-pay用来处理财务系统中的付款相关页面的交互。一些零碎的应用方法我们放在对应站点的misc模块中</li></ul></li><li><p>为了保持向前兼容，我们定义了新的全局对象M。我们对YUI进行了简单的封装，可以使用M.add添加模块，使用M.use调用模块。请注意，每次使用M.use都会新生成一个config的实例，这个config信息主要包含一些combo服务器信息，我们自己开发模块的信息。这样loader就知道该如何计算依赖的模块了。在对use的封装中，我们还引入了debug的功能。［代码演示］</p></li><li><p>目前我们采用的加载方式是：预加载通用基础模块，例如yui，node，event，io等，然后是站点级别的一些操作，例如mis系统的widget初始化、消息系统初始化等，最后是页面级别的一个特定应用入口。</p></li><li><p>我们对基于PHP loader的服务器端loader进行了尝试，并准备近期进行部署。</p><p>服务器端loader优势在于速度，同时，将其整合在框架中降低了维护成本，简便易用。</p></li><li><p>通过使用服务器端loader，可以做到一次性加载。</p></li></ol>]]></content>
    
    
    <summary type="html">&lt;div style=&quot;text-align:center;&quot;&gt;&lt;div style=&quot;width:425px; margin:0 auto;&quot; id=&quot;__ss_8759550&quot;&gt;&lt;object id=&quot;__sse8759550&quot; width=&quot;425&quot; height=&quot;355&quot;&gt;&lt;param name=&quot;movie&quot; value=&quot;http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=yui3-110802225931-phpapp02&amp;stripped_title=yui3-8759550&amp;userName=springuper&quot; /&gt;&lt;param name=&quot;allowFullScreen&quot; value=&quot;true&quot;/&gt;&lt;param name=&quot;allowScriptAccess&quot; value=&quot;always&quot;/&gt;&lt;embed name=&quot;__sse8759550&quot; src=&quot;http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=yui3-110802225931-phpapp02&amp;stripped_title=yui3-8759550&amp;userName=springuper&quot; type=&quot;application/x-shockwave-flash&quot; allowscriptaccess=&quot;always&quot; allowfullscreen=&quot;true&quot; width=&quot;425&quot; height=&quot;355&quot;&gt;&lt;/embed&gt;&lt;/object&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p style=&quot;text-align:center; text-indent:0;&quot;&gt;&lt;a href=&quot;/assets/YUI3-in-Meituan.key_.zip&quot;&gt;keynote下载&lt;/a&gt; | &lt;a href=&quot;/assets/YUI3-in-Meituan.pdf.zip&quot;&gt;pdf下载&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
    <category term="Meituan" scheme="https://springuper.github.io/tags/Meituan/"/>
    
  </entry>
  
  <entry>
    <title>[译]理解Javascript关键字this</title>
    <link href="https://springuper.github.io/understanding-this-keyword/"/>
    <id>https://springuper.github.io/understanding-this-keyword/</id>
    <published>2011-06-21T21:52:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<div class="preface">    <p>在上一次原生javascript分享时，我发现自己对this的理解仍然不够准确。在翻看了很多现有的文章后，我很失望的发现基本全是在讲种种类型的场景下this是怎样怎样，我需要的不是这些，我想看到更深入的一些解释，例如this在函数中从何而来。后来，我准备自己查阅资料后总结一篇，就在准备资料的时候我欣喜的看到了下面这篇文章，当时的心情只能用相见恨晚来表达。我认为自己不会写出更好的文章，所以就勉强翻译过来给一些E文不太好的童鞋分享，E文好的童鞋请移步，原著更加准确生动些。</p>    <p>原文链接：<a href="http://javascriptweblog.wordpress.com/2010/08/30/understanding-javascripts-this/" target="_blank">Understanding JavaScript’s this keyword</a></p></div><p><code>this</code>在Javascript中应用广泛，但对它的误解却比比皆是。</p><h2 id="你需要知道"><a href="#你需要知道" class="headerlink" title="你需要知道"></a>你需要知道</h2><p>每个运行环境(execution context，简称环境)都含有一个与之关联的ThisBinding常量，它们具有相同的生命周期。运行环境分为三类：</p><h3 id="1-全局环境"><a href="#1-全局环境" class="headerlink" title="1. 全局环境"></a>1. 全局环境</h3><p><code>this</code>指向全局对象，在浏览器中为<code>window</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">alert(<span class="built_in">this</span>);  <span class="comment">// window</span></span><br></pre></td></tr></table></figure><span id="more"></span><h3 id="2-函数环境"><a href="#2-函数环境" class="headerlink" title="2. 函数环境"></a>2. 函数环境</h3><p>至少有5种调用函数的方式，<code>this</code>的值取决于具体的调用方式。</p><h4 id="a-作为属性调用"><a href="#a-作为属性调用" class="headerlink" title="a) 作为属性调用"></a>a) 作为属性调用</h4><p><code>this</code>的值为将函数作为属性调用的基本对象(<a href="http://javascriptweblog.wordpress.com/2010/08/09/variables-vs-properties-in-javascript/">baseValue</a>)</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">var</span> a = &#123;</span><br><span class="line">    <span class="attr">b</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">a.b();  <span class="comment">// a</span></span><br><span class="line">a[<span class="string">&#x27;b&#x27;</span>]();  <span class="comment">// a</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> c = &#123;&#125;;</span><br><span class="line">c.d = a.b;</span><br><span class="line">c.d();  <span class="comment">// c</span></span><br></pre></td></tr></table></figure><h4 id="b-作为变量调用"><a href="#b-作为变量调用" class="headerlink" title="b) 作为变量调用"></a>b) 作为变量调用</h4><p><code>this</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">var</span> a = &#123;</span><br><span class="line">    <span class="attr">b</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</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">var</span> foo = a.b;</span><br><span class="line">foo();  <span class="comment">// window</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> a = &#123;</span><br><span class="line">    <span class="attr">b</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> c = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">        &#125;;</span><br><span class="line">        <span class="keyword">return</span> c();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">a.b();  <span class="comment">// window</span></span><br></pre></td></tr></table></figure><p>自执行函数(self-invoking functions)也是如此：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a = &#123;</span><br><span class="line">    <span class="attr">b</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> (<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; <span class="keyword">return</span> <span class="built_in">this</span>; &#125;)();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">a.b();  <span class="comment">// window</span></span><br></pre></td></tr></table></figure><h4 id="c-通过Function-prototype-call调用"><a href="#c-通过Function-prototype-call调用" class="headerlink" title="c) 通过Function.prototype.call调用"></a>c) 通过Function.prototype.call调用</h4><p><code>this</code>的值由<code>call</code>的第一个参数决定。</p><h4 id="d-通过Function-prototype-apply调用"><a href="#d-通过Function-prototype-apply调用" class="headerlink" title="d) 通过Function.prototype.apply调用"></a>d) 通过Function.prototype.apply调用</h4><p><code>this</code>的值由<code>apply</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">var</span> a = &#123;</span><br><span class="line">    <span class="attr">b</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</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">var</span> d = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">a.b.apply(d);  <span class="comment">// d</span></span><br></pre></td></tr></table></figure><h4 id="e-通过new作为构造器调用"><a href="#e-通过new作为构造器调用" class="headerlink" title="e) 通过new作为构造器调用"></a>e) 通过new作为构造器调用</h4><p><code>this</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="keyword">var</span> A = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.toString = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; <span class="keyword">return</span> <span class="string">&quot;I&#x27;m an A&quot;</span>; &#125;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">new</span> A();  <span class="comment">// &quot;I&#x27;m an A&quot;</span></span><br></pre></td></tr></table></figure><h3 id="3-eval环境"><a href="#3-eval环境" class="headerlink" title="3. eval环境"></a>3. eval环境</h3><p><code>this</code>的值等于调用<code>eval</code>方法的执行环境中的<code>this</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></pre></td><td class="code"><pre><span class="line">alert(<span class="built_in">eval</span>(<span class="string">&#x27;this == window&#x27;</span>));  <span class="comment">// true - (except firebug, see above)</span></span><br><span class="line"><span class="keyword">var</span> a = &#123;</span><br><span class="line">    <span class="attr">b</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">eval</span>(<span class="string">&#x27;alert(this == a)&#x27;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">a.b();  <span class="comment">// true</span></span><br></pre></td></tr></table></figure><h2 id="你也许想知道"><a href="#你也许想知道" class="headerlink" title="你也许想知道"></a>你也许想知道</h2><p>本节以ECMA 5 262为参考，对在函数环境下<code>this</code>获取值的过程做深入探究。</p><p>我们从ECMA中的<code>this</code>定义开始：</p><blockquote><p>关键字this等于当前执行环境中ThisBinding的值。  <em>ECMA 5, 11.1.1</em></p></blockquote><h3 id="ThisBinding是如何设定的呢？"><a href="#ThisBinding是如何设定的呢？" class="headerlink" title="ThisBinding是如何设定的呢？"></a>ThisBinding是如何设定的呢？</h3><p>每个函数都定义了一个内部方法[[Call]](ECMA 5, 13.2.1 [[Call]]) ，用来将invocation values传给该函数的执行环境：</p><blockquote><p>当控制器进入函数对象F的函数代码(function code)的执行环境时，依据调用对象提供的参数thisValue和argumentsList，执行以下步骤：</p><ol><li>若函数代码 (function code) 为严格代码 (strict code)，令ThisBinding等于thisValue</li><li>否则，若thisValue为null或undefined，令ThisBinding等于全局对象</li><li>否则，若thisValue不是Object类型，令thisBinding等于ToObject(thisValue)</li><li>否则，令thisBinding等于thisValue</li><li>⋯⋯</li></ol><p><em>ECMA 5, 10.4.3 Entering Function Code</em></p></blockquote><p>也就是说，<code>ThisBinding</code>在<code>thisValue</code>为基本类型时设定为其强制转化对象，或者当<code>thisValue</code>为<code>undefined</code>、<code>null</code>时，设定为全局对象(运行于严格模式时除外，这种情况下<code>ThisBinding</code>等于<code>thisValue</code>)。</p><h3 id="那thisValue从何而来？"><a href="#那thisValue从何而来？" class="headerlink" title="那thisValue从何而来？"></a>那thisValue从何而来？</h3><p>这里我们需要回到之前提到的五种调用函数的方式：</p><h4 id="1-作为属性调用"><a href="#1-作为属性调用" class="headerlink" title="1. 作为属性调用"></a>1. 作为属性调用</h4><h4 id="2-作为变量调用"><a href="#2-作为变量调用" class="headerlink" title="2. 作为变量调用"></a>2. 作为变量调用</h4><p>用ECMAScript的说法，这两种方式称为Function Calls，包含两个要素：MemberExpression和Arguments list。</p><blockquote><ol><li>令ref为执行MemberExpression后得到的结果</li><li>令func为GetValue(ref)</li><li>若Type(ref)是引用，则 a) 若IsPropertyReference(ref)为true，令thisValue为GetBase(ref) b) 否则，ref的基本对象是一个Environment Record，令thisValue为执行GetBase(ref)的具体方法ImplicitThisValue得到的结果</li><li>否则，Type(ref)不是引用，则令thisValue为undefined</li><li>令this等于thisValue，argument values等于argList，调用func内部方法[[Call]]，并将结果返回</li></ol><p><em>ECMA 5, 11.2.3 Function Calls</em></p></blockquote><p>那么，从本质来讲，<code>thisValue</code>成为函数表达式的baseValue(见上面第6步。译者注：baseValue为<code>GetBase</code>方法得到的结果)。</p><p>当函数作为属性调用时，baseValue就是在点号(或中括号)前面的标识符。</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">var</span> foo = &#123;</span><br><span class="line">    <span class="attr">bar</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="comment">// (Comments apply to example invocation only)</span></span><br><span class="line">        <span class="comment">// MemberExpression = foo.bar</span></span><br><span class="line">        <span class="comment">// thisValue = foo</span></span><br><span class="line">        <span class="comment">// ThisBinding = foo</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">foo.bar();  <span class="comment">// foo</span></span><br><span class="line">foo[<span class="string">&#x27;bar&#x27;</span>]();  <span class="comment">// foo</span></span><br></pre></td></tr></table></figure><p>对于作为变量调用的情况，<code>baseValue</code>则是变量对象(VariableObject，即上面提到的Environment Record)，变量对象属于声明式Environment Record。ECMA 10.2.1.1讲解道，声明式Environment Record的ImplicitThisValue为<code>undefined</code>。</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="keyword">var</span> bar = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; ... &#125;;</span><br><span class="line">bar();  <span class="comment">// thisValue is undefined</span></span><br></pre></td></tr></table></figure><p>重温上面提到过的10.4.3 Entering Function Code后，我们可以看到，除非在严格模式下，<code>thisValue</code>为<code>undefined</code>会使<code>ThisBinding</code>的值为全局对象。因此<code>this</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">var</span> bar = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="comment">// (Comments apply to example invocation only)</span></span><br><span class="line">    <span class="comment">// MemberExpression = bar</span></span><br><span class="line">    <span class="comment">// thisValue = undefined</span></span><br><span class="line">    <span class="comment">// ThisBinding = global object (e.g. window)</span></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">bar();  <span class="comment">// window</span></span><br></pre></td></tr></table></figure><h4 id="3-通过Function-prototype-apply调用"><a href="#3-通过Function-prototype-apply调用" class="headerlink" title="3. 通过Function.prototype.apply调用"></a>3. 通过Function.prototype.apply调用</h4><h4 id="4-通过Function-prototype-use调用"><a href="#4-通过Function-prototype-use调用" class="headerlink" title="4. 通过Function.prototype.use调用"></a>4. 通过Function.prototype.use调用</h4><p>(规范参见15.3.4.3 Function.prototype.apply，15.3.4.4 Function.prototype.use)</p><p>这两个小节描述了在<code>call</code>和<code>apply</code>调用函数时，函数中的<code>this</code>参数(它的第一个参数)的实际值是如何作为<code>thisValue</code>传递给10.4.3 Entering Function Code的。(注意，这一点不同于ECMA 3，后者规定<code>thisArg</code>的值为基本类型时需要转换为对象类型，为<code>null</code>或<code>undefined</code>时需要转化为全局对象——但这些区别通常可以忽略，因为<code>thisArg</code>的值会在目标函数调用时进行相同的转换过程(参见已讲过的10.4.3 Entering Function Code))</p><h4 id="5-通过new作为构造器调用"><a href="#5-通过new作为构造器调用" class="headerlink" title="5. 通过new作为构造器调用"></a>5. 通过new作为构造器调用</h4><blockquote><p>当函数对象F的内部方法[[Construct]]被调用时，执行以下步骤：</p><ol><li>令obj为新生成的原生ECMAScript对象</li><li>令thisValue等于obj，args为传入[[Construct]]的参数列表，调用F内部方法[[Call]]，并将结果保存为result</li><li>返回obj</li></ol><p><em>ECMA 5, 13.2.2 [[Construct]]</em></p></blockquote><p>显而易见，作为构造器调用函数会生成一个新的对象，它被赋给<code>thisValue</code>。这种方式与其它<code>this</code>的使用方式截然不同。</p><h2 id="释疑"><a href="#释疑" class="headerlink" title="释疑"></a>释疑</h2><h3 id="严格模式"><a href="#严格模式" class="headerlink" title="严格模式"></a>严格模式</h3><p>在ECMAScript的严格模式下，<code>thisValue</code>不会强制转化为一个对象。<code>this</code>的值为<code>null</code>或<code>undefined</code>时不会转化为全局对象，并且基本类型的值不会转化为包装类型对象。</p><h3 id="bind函数"><a href="#bind函数" class="headerlink" title="bind函数"></a>bind函数</h3><p><code>Function.prototype.bind</code>是ECMAScript 5新添加的一个方法，使用主流框架的开发者对它已经非常熟悉。基于<code>call</code>/<code>apply</code>，<code>bind</code>可以通过简单的语法预设执行环境中<code>thisValue</code>的值。这在事件响应函数中非常有用，例如一个监听按钮点击事件的函数，它的<code>ThisBinding</code>默认为<code>onclick</code>属性的<code>baseValue</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Bad Example: fails because ThisBinding of handler will be button</span></span><br><span class="line"><span class="keyword">var</span> sorter = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="attr">sort</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        alert(<span class="string">&#x27;sorting&#x27;</span>);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">requestSorting</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">this</span>.sort();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">$(<span class="string">&#x27;sortButton&#x27;</span>).onclick = sorter.requestSorting;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Good Example: sorter baked into ThisBinding of handler</span></span><br><span class="line"><span class="keyword">var</span> sorter = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="attr">sort</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        alert(<span class="string">&#x27;sorting&#x27;</span>);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">requestSorting</span>: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">this</span>.sort();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">$(<span class="string">&#x27;sortButton&#x27;</span>).onclick = sorter.requestSorting.bind(sorter);</span><br></pre></td></tr></table></figure><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><p><a href="http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf">ECMA 262 5th Edition (PDF)</a></p><ul><li>11.1.1 Definition of this</li><li>10.4.3 Entering Function Code</li><li>11.2.3 Function Calls</li><li>13.2.1 [[Call]]</li><li>10.2.1.1 Declarative Environment Record (ImplicitThisValue)</li><li>13.2.2 [[Construct]]</li><li>15.3.4.3 Function.prototype.apply</li><li>15.3.4.4 Function.prototype.call</li><li>15.3.4.5 Function.prototype.bind</li><li>Annex C The Strict Mode of ECMAScript</li></ul>]]></content>
    
    
    <summary type="html">&lt;div class=&quot;preface&quot;&gt;
    &lt;p&gt;在上一次原生javascript分享时，我发现自己对this的理解仍然不够准确。在翻看了很多现有的文章后，我很失望的发现基本全是在讲种种类型的场景下this是怎样怎样，我需要的不是这些，我想看到更深入的一些解释，例如this在函数中从何而来。后来，我准备自己查阅资料后总结一篇，就在准备资料的时候我欣喜的看到了下面这篇文章，当时的心情只能用相见恨晚来表达。我认为自己不会写出更好的文章，所以就勉强翻译过来给一些E文不太好的童鞋分享，E文好的童鞋请移步，原著更加准确生动些。&lt;/p&gt;
    &lt;p&gt;原文链接：&lt;a href=&quot;http://javascriptweblog.wordpress.com/2010/08/30/understanding-javascripts-this/&quot; target=&quot;_blank&quot;&gt;Understanding JavaScript’s this keyword&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;&lt;code&gt;this&lt;/code&gt;在Javascript中应用广泛，但对它的误解却比比皆是。&lt;/p&gt;
&lt;h2 id=&quot;你需要知道&quot;&gt;&lt;a href=&quot;#你需要知道&quot; class=&quot;headerlink&quot; title=&quot;你需要知道&quot;&gt;&lt;/a&gt;你需要知道&lt;/h2&gt;&lt;p&gt;每个运行环境(execution context，简称环境)都含有一个与之关联的ThisBinding常量，它们具有相同的生命周期。运行环境分为三类：&lt;/p&gt;
&lt;h3 id=&quot;1-全局环境&quot;&gt;&lt;a href=&quot;#1-全局环境&quot; class=&quot;headerlink&quot; title=&quot;1. 全局环境&quot;&gt;&lt;/a&gt;1. 全局环境&lt;/h3&gt;&lt;p&gt;&lt;code&gt;this&lt;/code&gt;指向全局对象，在浏览器中为&lt;code&gt;window&lt;/code&gt;对象。&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;alert(&lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;);  &lt;span class=&quot;comment&quot;&gt;// window&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="This" scheme="https://springuper.github.io/tags/This/"/>
    
  </entry>
  
  <entry>
    <title>新外观，更优雅，更直接</title>
    <link href="https://springuper.github.io/change-theme/"/>
    <id>https://springuper.github.io/change-theme/</id>
    <published>2011-06-05T20:20:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p><img src="../images/metro.png" alt="Metro"></p><p>我叫尚春，来自美团网</p><p>如你所看</p><p>我的blog彻底重建</p><p>引入了很多<a href="http://ubelly.com/2011/01/metro-windows-phone-7-design-language/">Metro</a>的设计理念</p><p>因为我欣赏更加凸显内容的表达方式</p><p>而不再认为高光、阴影是现代风格所必须</p><p>这，是一种真正意义上的返璞归真</p><p>在这里</p><p>所有的图标都是用<a href="http://raphaeljs.com/">Raphaël</a>绘制</p><p>我写的一个小型通用库Spring也崭露头角</p><p>并将茁壮成长</p><p>一切都远未完善</p><p>我会有节奏的进行</p><p>这一切都值得期待</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;img src=&quot;../images/metro.png&quot; alt=&quot;Metro&quot;&gt;&lt;/p&gt;
&lt;p&gt;我叫尚春，来自美团网&lt;/p&gt;
&lt;p&gt;如你所看&lt;/p&gt;
&lt;p&gt;我的blog彻底重建&lt;/p&gt;
&lt;p&gt;引入了很多&lt;a</summary>
        
      
    
    
    
    
    <category term="Self" scheme="https://springuper.github.io/tags/Self/"/>
    
    <category term="Metro" scheme="https://springuper.github.io/tags/Metro/"/>
    
  </entry>
  
  <entry>
    <title>浅析YUI3</title>
    <link href="https://springuper.github.io/yui3-intro/"/>
    <id>https://springuper.github.io/yui3-intro/</id>
    <published>2011-05-21T21:16:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>最近因为忙于YUI2到YUI3的迁移，没时间更新博客。现在闲暇一些，觉得似乎写一点自己对于YUI3的理解再合适不过。</p><p>主要内容有以下几点：</p><ol><li>全局命名空间</li><li>改变一切的模块</li><li>Combo</li><li>链式调用</li><li>更广泛的自定义事件</li><li>一些问题</li></ol><h2 id="全局命名空间"><a href="#全局命名空间" class="headerlink" title="全局命名空间"></a>全局命名空间</h2><p>YUI3为了保持向前兼容，采用了新的全局命名空间<code>YUI</code>。新的命名空间与YUI2的全局命名空间<code>YAHOO</code>最大的不同就是：<code>YUI</code>是一个构造函数，而且是一个<a href="https://springuper.github.io/scope-safe-constructor/">无论如何也返回一个实例的构造函数</a>。</p><h2 id="改变一切的模块"><a href="#改变一切的模块" class="headerlink" title="改变一切的模块"></a>改变一切的模块</h2><p>YUI2时代，一般都是将某方面的全部方法写在统一的命名空间下，例如DOM相关的方法均在<code>YAHOO.util.Dom</code>，在需要使用这些方法时我们直接调用即可。YUI2真正体现了<strong>基础方法库</strong>(function library)这样一种定位。</p><span id="more"></span><p>YUI3最大的变化和进步在于，它采用了革新性的底层组织方式，其核心就是模块(module)。在YUI3中，每个方法不再属于某个文件、某个命名空间，而是属于某个模块。每个模块代表一个独立的功能，例如<code>DOM</code>、<code>Event</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></pre></td><td class="code"><pre><span class="line">YUI.add(<span class="string">&#x27;new-module&#x27;</span>, <span class="function"><span class="keyword">function</span> (<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    Y.sayHelloWorld = <span class="function"><span class="keyword">function</span> (<span class="params">id</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> el = Y.DOM.byId(id);</span><br><span class="line">        Y.DOM.set(el, <span class="string">&#x27;innerHTML&#x27;</span>, <span class="string">&#x27;Hello, world!&#x27;</span>);</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;, <span class="string">&#x27;1.0.0&#x27;</span>, &#123; <span class="attr">requires</span>: [<span class="string">&#x27;dom&#x27;</span>] &#125;);</span><br></pre></td></tr></table></figure><p>通过调用<code>YUI</code>构造器本身的<code>add</code>静态方法，我们声明了一个新的模块，模块的名称为new-module，模块为<code>YUI</code>的实例<code>Y</code>挂载了<code>sayHelloWorld</code>方法，因为这个方法使用了dom模块的方法<code>byId</code>，所以要在<code>add</code>的第四个参数中标明new-module模块依赖于dom模块。</p><p>添加模块的目的是为了使用它，下面给出调用new-module的示例：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// html</span></span><br><span class="line">&lt;div id=<span class="string">&quot;entry&quot;</span>&gt;&lt;/div&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">// js</span></span><br><span class="line">YUI().use(<span class="string">&#x27;new-module&#x27;</span>, <span class="function"><span class="keyword">function</span>(<span class="params">Y</span>) </span>&#123;</span><br><span class="line">    Y.sayHelloWorld(<span class="string">&#x27;entry&#x27;</span>);   <span class="comment">// &lt;div id=&quot;entry&quot;&gt;Hello, world!&lt;/div&gt;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>通过调用YUI实例的<code>use</code>方法，在列出模块名称之后，我们可以随意使用它们挂载的方法。需要提醒的是，YUI的<a href="http://developer.yahoo.com/yui/3/yui/#loader">loader</a>会发现new-module模块依赖于dom模块，然后它去check当前页面是否已经有dom模块，没有的话则动态加载。</p><p>总结一下，发现模块为我们带来了这些好处：</p><ul><li><strong>遗弃domready</strong>：在YUI2中，页面通常要在domready之后调用js方法。在YUI3中可以省掉这一步了，因为只有在所有需要的模块都加载完毕后才会调用js方法，而我们通常都是在&lt;body&gt;最后面引入js，此时dom基本已经就绪。当然，YUI3仍然提供domready事件监听方法。</li><li><strong>沙箱</strong>(Sandbox)：在使用模块时，我们只能调用这些模块以及它们依赖的所有模块给YUI实例挂载的方法。这种机制可以限定你的代码在一定范围内执行，而不是肆意妄为。</li><li><strong>自动加载</strong>：我们不必再手动添加&lt;script&gt;标签引入js，更不必为js文件间的顺序依赖问题揪心，YUI的loader会为你摆平，而它的依据正是各个模块中的<code>requires</code>参数。</li><li><strong>开发框架</strong>(Framework)：基于这种方法的声明/调用机制，YUI3更像一个js开发框架而不仅仅是一个方法库。我们可以将自己需要的功能作为一个个模块，可以细分为全局通用模块、分站通用模块、分站应用模块等层级。YUI3为此提供了非常简便的接口。</li></ul><p>我觉得用这样一句话形容模块对于YUI的意义最好不过：“<a href="http://movie.douban.com/subject/3541415/">Inception</a>”中Cobb所说的“a very simple idea that changed everything”。</p><p>至于模块如何设计、如何分级，将是一个非常有挑战的问题。我准备在适当的时间做下经验总结，现在暂不展开讲。</p><h2 id="Combo"><a href="#Combo" class="headerlink" title="Combo"></a>Combo</h2><p>在YUI3框架中，每个模块都对应一个独立的js文件(也有所有子模块组成一个模块的特殊情况，在此不细较)，日常开发中，往往需要动态加载大量的模块，造成http请求数较YUI2时代翻几番。众所周知的是，在YUI团队的<a href="http://developer.yahoo.com/performance/rules.html">Best Practices for Speeding Up Your Web Site</a>一文中提到的第一条准则就是降低http请求数。如果模块化带来的更小粒度更大规模的js文件使得页面加载速度更慢，那么它所有的优势将不再具有吸引力。好消息是，YUI团队很好的解决了这一问题，他们引入了combo的思想，即使用服务器端技术，收到包含多个js文件请求的url之后，合并这些文件为一个大文件返回。</p><p>关于Combo的具体文章，可以参考<a href="http://dancewithnet.com/2010/06/08/minify-js-and-css-files-in-server">在服务端合并和压缩JavaScript和CSS文件</a>。</p><h2 id="链式调用"><a href="#链式调用" class="headerlink" title="链式调用"></a>链式调用</h2><p>jQuery因为方便的链式调用而风靡全球，其write less do more的核心理念也随之深入人心。欣喜的是，YUI团队汲取了这一优点，在YUI3中引入了新的<a href="http://developer.yahoo.com/yui/3/node">Node/NodeList</a>对象，并取代DOM成为YUI3 Core。这一变化无疑将增加YUI3代码的书写效率，成为有一个显著的加分点。</p><h2 id="更广泛的自定义事件"><a href="#更广泛的自定义事件" class="headerlink" title="更广泛的自定义事件"></a>更广泛的自定义事件</h2><p>YUI3提供了简约而强大的custom event：更加dom-like的调用方法，可以冒泡，可以经历各个事件阶段，可以定义默认动作，可以设定传播范围是模块内部还是模块之间等等。目前对于这部分仍然不甚熟悉，有兴趣的朋友可以参考Luke Smith做的这个<a href="http://developer.yahoo.com/yui/theater/video.php?v=smith-yuiconf2009-events">讲座</a>。</p><h2 id="一些问题"><a href="#一些问题" class="headerlink" title="一些问题"></a>一些问题</h2><p>YUI3并不完美，目前为止仍然存在一些问题，列举如下：</p><ul><li>文档仍然不够详尽，很多时候只能去查源代码。例如YUI2中<code>getRegion</code>方法在YUI3中的对应方法为<code>node.get(&#39;region&#39;)</code>。</li><li>Node实例只能通过<code>get</code>方法取id，value等自身属性，可以参考jQuery采用更简洁的api，例如<code>val()</code>, <code>height()</code>, <code>text()</code>等。</li><li>得到一个定制的YUI实例时，需要将自己开发的模块的<code>path</code>、<code>requires</code>信息作为参数传进去，而在声明自定义模块时仍需要设定<code>requires</code>，这种冗余设计是可以做优化的。</li><li>使用Node操作Form时非常繁琐。例如要获取一个text表单项只能使用<code>ndForm.one(&#39;input[name=&quot;xxx&quot;]&#39;)</code>，而不能<code>ndForm.get(&#39;xxx&#39;)</code>。</li><li>在存在name=”id”项的表单中，存在因<code>Y.Selector.test</code>失效而产生的各种问题。例如<code>Y.one(&#39;#input-id&#39;).ancestor(&#39;form&#39;)</code>取不到等等。</li><li>没有公开的Form的<code>serialize</code>方法。</ul></li></ul><p>ok，这就是我对YUI3的一些浅薄见解。欢迎讨论和指正。随着经验的不断积累，后续会奉上更深入的一些分析。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近因为忙于YUI2到YUI3的迁移，没时间更新博客。现在闲暇一些，觉得似乎写一点自己对于YUI3的理解再合适不过。&lt;/p&gt;
&lt;p&gt;主要内容有以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;全局命名空间&lt;/li&gt;
&lt;li&gt;改变一切的模块&lt;/li&gt;
&lt;li&gt;Combo&lt;/li&gt;
&lt;li&gt;链式调用&lt;/li&gt;
&lt;li&gt;更广泛的自定义事件&lt;/li&gt;
&lt;li&gt;一些问题&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;全局命名空间&quot;&gt;&lt;a href=&quot;#全局命名空间&quot; class=&quot;headerlink&quot; title=&quot;全局命名空间&quot;&gt;&lt;/a&gt;全局命名空间&lt;/h2&gt;&lt;p&gt;YUI3为了保持向前兼容，采用了新的全局命名空间&lt;code&gt;YUI&lt;/code&gt;。新的命名空间与YUI2的全局命名空间&lt;code&gt;YAHOO&lt;/code&gt;最大的不同就是：&lt;code&gt;YUI&lt;/code&gt;是一个构造函数，而且是一个&lt;a href=&quot;https://springuper.github.io/scope-safe-constructor/&quot;&gt;无论如何也返回一个实例的构造函数&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;改变一切的模块&quot;&gt;&lt;a href=&quot;#改变一切的模块&quot; class=&quot;headerlink&quot; title=&quot;改变一切的模块&quot;&gt;&lt;/a&gt;改变一切的模块&lt;/h2&gt;&lt;p&gt;YUI2时代，一般都是将某方面的全部方法写在统一的命名空间下，例如DOM相关的方法均在&lt;code&gt;YAHOO.util.Dom&lt;/code&gt;，在需要使用这些方法时我们直接调用即可。YUI2真正体现了&lt;strong&gt;基础方法库&lt;/strong&gt;(function library)这样一种定位。&lt;/p&gt;</summary>
    
    
    
    
    <category term="YUI" scheme="https://springuper.github.io/tags/YUI/"/>
    
  </entry>
  
  <entry>
    <title>Event小记</title>
    <link href="https://springuper.github.io/event-note/"/>
    <id>https://springuper.github.io/event-note/</id>
    <published>2011-03-17T00:39:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>Event在javascript中的重要性不言而喻，正是它驱动着所有事情的进行。记下一些读书的心得，整理如下：</p><ul><li>Event按类别可分为input events和semantic events。semantic events的发生通常都建立在input events之上，例如点击“提交”按钮后产生onsubmit事件。input events依赖于输入设备。Events按模块可分为HTMLEvents、MouseEvents和UIEvents，对应的接口分别为Event、MouseEvent和UIEvent，其中MouseEvent接口继承了Event接口和UIEvent接口，常用的属性有altKey、ctrlKey、shiftKey和一些位置信息。</li><li>DOM level 0的事件响应方式得到最为广泛的支持。具体应用方式有两种，一种是在页面目标元素中添加对应属性，如&lt;input type=”button” onclick=”alert(this.nodeName)” /&gt;；对应的另一种方式为 elButton.onclick = function(){ alert(this.nodeName); }。显式调用响应方法也非常简单，elButton.onclick()即可。响应方法中的this指代的是触发事件的页面元素。这种事件处理方式有一些缺点，例如：每个对象的某种事件只能添加一个响应方法，当需要将多个元素绑定事件时实现较为复杂等。</li><li>DOM level 2的事件响应方式进行了诸多改进。事件被赋予一个传播过程，细分为capturing、target node、bubbling三个阶段。这一机制是有一定渊源的，不妨细讲。在当年Netscape与IE大战的年代，当遇到多个嵌套元素绑定同一事件时，如何确定响应方法执行顺序成为大家共同的问题，不幸的是他们做出了相反的选择：NetScape按照事件捕获的顺序执行，即父级节点响应方法优先执行，而IE按照事件冒泡的顺序执行，即子级节点响应方法优先执行。之后W3C过来和稀泥，也就制定了事件传播过程这一机制。需要注意的是，可以通过设定addEventListener的第三个参数设定响应方法在capturing阶段执行（true）还是在bubbling阶段执行（false）。</li><li>有些元素在一些操作后会有默认动作，如果恰好添加了这类事件监听器的话，执行的顺序是：响应方法执行先于默认动作。阻止默认动作发生，DOM level 0中可以用return false（window.status是一个例外，需return true）；DOM level 2中可以使用preventDefault方法。停止事件传播使用stopPropagation方法。</li><li>IE的事件模型并未遵循W3C标准。事件传播没有capturing阶段，bubbling阶段不能获得currentTarget，不会传递给事件响应方法event对象而是有一个全局event对象，注册/移除响应方法使用attachEvent/detachEvent，阻止事件传播使用cancelBubble等等。</li><li>为了将鼠标动作限定在特定页面元素，即便鼠标已不在该元素区域内，可将响应方法和事件注册在document上，在IE中可使用setCapture/releaseCapture方法。一个典型的案例是在拖拽模块时，鼠标移动常常快于模块。</li><li>keyCode代表键位编号，charCode代表键位上字符的编号。IE的键盘事件中只有keyCode属性，可以用 e.charCode || e.keyCode获得keypress事件发生时对应按键的字符编号。</li><li>通过Document.createEvent创建自定义事件，并用Event.initEvent进行初始化，dispatchEvent进行调度。IE中可使用Document.createEventObject创建自定义事件，并用fireEvent进行调度。这部分可以说成为高级前端必备知识，在模块化编程等方面有较为重要的应用。</li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Event在javascript中的重要性不言而喻，正是它驱动着所有事情的进行。记下一些读书的心得，整理如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Event按类别可分为input events和semantic events。semantic</summary>
        
      
    
    
    
    
    <category term="Event" scheme="https://springuper.github.io/tags/Event/"/>
    
  </entry>
  
  <entry>
    <title>YUI onDOMReady滞后window.onload原因浅析</title>
    <link href="https://springuper.github.io/yui-domready-after-load/"/>
    <id>https://springuper.github.io/yui-domready-after-load/</id>
    <published>2011-03-11T14:12:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<h2 id="开篇"><a href="#开篇" class="headerlink" title="开篇"></a>开篇</h2><p>我很好奇其他前端工程师在wp后台写新文章的时候是在用可视化还是html模式。反正我是一路打着标签过来的，呵呵。</p><h2 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h2><p>一直以来，我都先验的认为dom ready事件肯定发生在load之前。但是，最近的一个ticket上线后发现一个很奇怪的现象：在IE下，有时会发生页面load时仍没有捕捉到dom ready的时间。通过多次观察，发现发生这种现象一般是较为简单的页面。</p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>首先，在反复检查代码后排除了自身bug的问题。然后，去找到YUI（我们团队主要以YUI2作为基础库，并准备近期升级到YUI3）的<code>onDOMReady</code>实现，希望通过分析源码找到问题的起源，将<code>onDOMReady</code>在不含frame的IE中分支拣出来，代码量并不多：</p><span id="more"></span><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">// Process onAvailable/onContentReady items when the</span></span><br><span class="line"><span class="comment">// DOM is ready.</span></span><br><span class="line">YAHOO.util.Event.onDOMReady(</span><br><span class="line">    YAHOO.util.Event._tryPreloadAttach,</span><br><span class="line">    YAHOO.util.Event, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> n = <span class="built_in">document</span>.createElement(<span class="string">&#x27;p&#x27;</span>);</span><br><span class="line"></span><br><span class="line">EU._dri = <span class="built_in">setInterval</span>(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// throws an error if doc is not ready</span></span><br><span class="line">        n.doScroll(<span class="string">&#x27;left&#x27;</span>);</span><br><span class="line">        <span class="built_in">clearInterval</span>(EU._dri);</span><br><span class="line">        EU._dri = <span class="literal">null</span>;</span><br><span class="line">        EU._ready();</span><br><span class="line">        n = <span class="literal">null</span>;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (ex) &#123;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;, EU.POLL_INTERVAL);</span><br></pre></td></tr></table></figure><p>实现的思路非常清晰：创建一个段落元素，然后轮询这个元素是否可以运行<code>doScroll</code>方法，在IE中，所有元素都有<code>doScroll</code>方法，而且如果文档没有完全解析完成的话，运行这个方法会报错。YUI正是运用了IE的这个特点来判定dom ready。而问题恰恰出在轮询而不是原生事件上，因为轮询有一个时间差（40毫秒），如果在下次嗅探之前文档加载完成，那么YUI所得到的dom ready时间就会滞后，进一步，如果页面并不包括很多图片等额外元素时，那么在下一次嗅探前dom ready且迅速触发load的话，就会出现YUI得到的dom ready时间比load时间更晚的怪象。</p><h2 id="修正"><a href="#修正" class="headerlink" title="修正"></a>修正</h2><p>了解清楚问题的真正原因，问题的解决办法也就非常容易想到。最简单的办法是在load触发后滞后一段时间再执行需要dom ready时间的方法。进一步，因为在Firefox、Chrome、Safari等现代浏览器可以直接利用DOMContentLoaded事件即时捕捉到dom ready的时间，没有必要滞后执行，所以可以做下相应的判断。这种细节的处理才能显示出对用户体验的追求和优秀前端的价值。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;开篇&quot;&gt;&lt;a href=&quot;#开篇&quot; class=&quot;headerlink&quot; title=&quot;开篇&quot;&gt;&lt;/a&gt;开篇&lt;/h2&gt;&lt;p&gt;我很好奇其他前端工程师在wp后台写新文章的时候是在用可视化还是html模式。反正我是一路打着标签过来的，呵呵。&lt;/p&gt;
&lt;h2 id=&quot;缘起&quot;&gt;&lt;a href=&quot;#缘起&quot; class=&quot;headerlink&quot; title=&quot;缘起&quot;&gt;&lt;/a&gt;缘起&lt;/h2&gt;&lt;p&gt;一直以来，我都先验的认为dom ready事件肯定发生在load之前。但是，最近的一个ticket上线后发现一个很奇怪的现象：在IE下，有时会发生页面load时仍没有捕捉到dom ready的时间。通过多次观察，发现发生这种现象一般是较为简单的页面。&lt;/p&gt;
&lt;h2 id=&quot;分析&quot;&gt;&lt;a href=&quot;#分析&quot; class=&quot;headerlink&quot; title=&quot;分析&quot;&gt;&lt;/a&gt;分析&lt;/h2&gt;&lt;p&gt;首先，在反复检查代码后排除了自身bug的问题。然后，去找到YUI（我们团队主要以YUI2作为基础库，并准备近期升级到YUI3）的&lt;code&gt;onDOMReady&lt;/code&gt;实现，希望通过分析源码找到问题的起源，将&lt;code&gt;onDOMReady&lt;/code&gt;在不含frame的IE中分支拣出来，代码量并不多：&lt;/p&gt;</summary>
    
    
    
    
    <category term="YUI2" scheme="https://springuper.github.io/tags/YUI2/"/>
    
    <category term="domready" scheme="https://springuper.github.io/tags/domready/"/>
    
    <category term="load" scheme="https://springuper.github.io/tags/load/"/>
    
  </entry>
  
  <entry>
    <title>如何保证调用构造函数也会得到一个实例</title>
    <link href="https://springuper.github.io/scope-safe-constructor/"/>
    <id>https://springuper.github.io/scope-safe-constructor/</id>
    <published>2011-03-10T13:36:00.000Z</published>
    <updated>2026-10-03T09:20:43.754Z</updated>
    
    <content type="html"><![CDATA[<p>题目比较绕，其实意思很简单，先不解释，给出如下两个场景：</p><h2 id="情景1"><a href="#情景1" class="headerlink" title="情景1"></a>情景1</h2><p>在js中，面向对象编程方法越来越流行，构造函数作为基础概念，使用的频率较高。</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">spring</span>(<span class="params">name</span>)</span>&#123;</span><br><span class="line">    <span class="built_in">this</span>.name = name;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> firstIns = <span class="keyword">new</span> spring(<span class="string">&#x27;shang chun&#x27;</span>);   <span class="comment">//an instance of spring</span></span><br></pre></td></tr></table></figure><p>众所周知，构造函数本身也是一个函数，这就意味着它可以被随意调用。随着岁月的积累或协作人员之间信息不对称，很有可能不慎如下调用了这个构造函数：</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"><span class="keyword">var</span> secondeIns = spring(<span class="string">&#x27;wang qiang&#x27;</span>);   <span class="comment">// &#x27;this&#x27; refer to window now</span></span><br></pre></td></tr></table></figure><p>这样的代码并不会报错，但是却给全局对象<code>window</code>增加了<code>name</code>属性，这样的失误需要一种合适的方法避免。</p><span id="more"></span><h2 id="情景2"><a href="#情景2" class="headerlink" title="情景2"></a>情景2</h2><p>想想看，如果我们想要马上调用新生成的实例，是不是只能这样去写</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">(<span class="keyword">new</span> spring(<span class="string">&#x27;shang chun&#x27;</span>)).hasOwnProperty(<span class="string">&#x27;name&#x27;</span>);</span><br></pre></td></tr></table></figure><p>有没有更好的方法呢？</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>事情回到了起点，我们想要的实际上是一个只要调用就无论如何都返回一个实例的构造函数，不管用不用<code>new</code>操作符。最近在看YUI3的源码，发现他们给出一种很好的解决方案，示例：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">spring</span>(<span class="params">name</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> _this = <span class="built_in">this</span>;</span><br><span class="line">    <span class="keyword">var</span> insanceOf = <span class="function"><span class="keyword">function</span>(<span class="params">o, type</span>)</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> (o &amp;&amp; o.hasOwnProperty &amp;&amp; (o <span class="keyword">instanceof</span> type));</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">if</span> (!instanceOf(_this, spring)) &#123;</span><br><span class="line">         _this = <span class="keyword">new</span> spring(name);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">         _this.name = name;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> _this;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> ins = <span class="keyword">new</span> spring(<span class="string">&#x27;shang chun&#x27;</span>);  <span class="comment">// normal constructor</span></span><br><span class="line">ins.name;  <span class="comment">// outputs &#x27;shang chun&#x27;</span></span><br><span class="line"></span><br><span class="line">spring(<span class="string">&#x27;shang chun&#x27;</span>).name;  <span class="comment">// outputs &#x27;shang chun&#x27;</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;题目比较绕，其实意思很简单，先不解释，给出如下两个场景：&lt;/p&gt;
&lt;h2 id=&quot;情景1&quot;&gt;&lt;a href=&quot;#情景1&quot; class=&quot;headerlink&quot; title=&quot;情景1&quot;&gt;&lt;/a&gt;情景1&lt;/h2&gt;&lt;p&gt;在js中，面向对象编程方法越来越流行，构造函数作为基础概念，使用的频率较高。&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;spring&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;name&lt;/span&gt;)&lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;built_in&quot;&gt;this&lt;/span&gt;.name = name;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; firstIns = &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; spring(&lt;span class=&quot;string&quot;&gt;&amp;#x27;shang chun&amp;#x27;&lt;/span&gt;);   &lt;span class=&quot;comment&quot;&gt;//an instance of spring&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;众所周知，构造函数本身也是一个函数，这就意味着它可以被随意调用。随着岁月的积累或协作人员之间信息不对称，很有可能不慎如下调用了这个构造函数：&lt;/p&gt;
&lt;figure class=&quot;highlight js&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;var&lt;/span&gt; secondeIns = spring(&lt;span class=&quot;string&quot;&gt;&amp;#x27;wang qiang&amp;#x27;&lt;/span&gt;);   &lt;span class=&quot;comment&quot;&gt;// &amp;#x27;this&amp;#x27; refer to window now&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;这样的代码并不会报错，但是却给全局对象&lt;code&gt;window&lt;/code&gt;增加了&lt;code&gt;name&lt;/code&gt;属性，这样的失误需要一种合适的方法避免。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Instanceof" scheme="https://springuper.github.io/tags/Instanceof/"/>
    
    <category term="Scope-safe Constructor" scheme="https://springuper.github.io/tags/Scope-safe-Constructor/"/>
    
  </entry>
  
  <entry>
    <title>创世纪</title>
    <link href="https://springuper.github.io/birth/"/>
    <id>https://springuper.github.io/birth/</id>
    <published>2011-03-10T13:35:00.000Z</published>
    <updated>2026-10-03T09:20:43.751Z</updated>
    
    <content type="html"><![CDATA[<p>至此，自己的blog终于搭了起来。</p><p>过程并不算复杂，不过还是需要一定的热情。</p><p>建站的初衷源于个人积累的一些诉求，希望能够将一些日常的idea或problem经过思维的处理沉淀成一直留守的文字，无论何时何地何人，看到它们时都会有一些收获或感想。</p><p>不追求能有很多人来看，我把这里当成自己的自留地，肆意非常。</p><p>持续的写作需要极大热忱，希望我能保持。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;至此，自己的blog终于搭了起来。&lt;/p&gt;
&lt;p&gt;过程并不算复杂，不过还是需要一定的热情。&lt;/p&gt;
&lt;p&gt;建站的初衷源于个人积累的一些诉求，希望能够将一些日常的idea或problem经过思维的处理沉淀成一直留守的文字，无论何时何地何人，看到它们时都会有一些收获或感想。&lt;/</summary>
        
      
    
    
    
    
    <category term="Self" scheme="https://springuper.github.io/tags/Self/"/>
    
    <category term="Life" scheme="https://springuper.github.io/tags/Life/"/>
    
  </entry>
  
</feed>
