<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Victor 的自留地</title>
  
  <subtitle>Code Clarity &gt; Code Brevity</subtitle>
  <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/atom.xml" rel="self"/>
  
  <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/"/>
  <updated>2026-01-11T10:49:33.490Z</updated>
  <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/</id>
  
  <author>
    <name>Victor Ye</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>对接小红书海量数据丨Go 并发利器 Worker Pool 实践</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/12e2a8de.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/12e2a8de.html</id>
    <published>2026-01-11T08:45:18.000Z</published>
    <updated>2026-01-11T10:49:33.490Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近有个业务需求，需要去<a href="https://ad-market.xiaohongshu.com/docs-center?bizType=943&articleId=2734">小红书开放平台</a>对接一些报表数据接口，通过定时任务，在每天特定的时间点拉取效果数据并落库。<span id="more"></span></p><p>对接第三方接口需要遵守他们的规则。小红书接口单页允许的 <code>page_size</code> 最大为 100，这就意味着，如果有 100 万条数据，那么就需要做 1 万次 “拉取-落库” 的动作。单线程去做的话会非常耗时，因此需要利用 Go 并发编程的特性去多线程并发。</p><p><img src="https://r.kezaihui.com/client/2026-01-11/image/yeyuxuan/redbook_report_api_screenshot.png" alt="redbook_report_api_screenshot"></p><p>让 Claude 4.5 Opus 实现功能之后，发现它使用了一种叫 <code>Worker Pool</code> (工作池) 的并发模式，通过 Gemini 进行学习了解后，发现这是 Go 中一种非常经典且高效的并发设计模式，正好最近刚学习完 <code>Goroutine</code>、<code>Channel</code>、<code>sync.WaitGroup</code> 等概念，<code>Worker Pool</code> 是一个综合的实践，而且 <code>Worker Pool</code> 的示例代码虽然看上去一眼就懂，但其实有很多细节我发现自己并没有完全理解（记录在了最后的 FAQ 章节），因此打算做一个记录~</p><h1 id="Worker-Pool-的核心思想"><a href="#Worker-Pool-的核心思想" class="headerlink" title="Worker Pool 的核心思想"></a><code>Worker Pool</code> 的核心思想</h1><blockquote><p>Gemini 的一句话总结和举例，我觉得非常完美，做到了 “多则惑，少则缺”，因此直接照抄过来。 </p></blockquote><p><code>Worker Pool</code> 的核心思想是：提前创建固定数量的 <code>Goroutine</code>（工作协程），让它们竞争式地从一个任务队列中获取并处理任务。</p><p>就像是一个工厂的流水线，无论有多少订单（任务），流水线上只有固定数量的工人（Worker）在干活。</p><h1 id="为什么需要-Worker-Pool？"><a href="#为什么需要-Worker-Pool？" class="headerlink" title="为什么需要 Worker Pool？"></a>为什么需要 <code>Worker Pool</code>？</h1><p>虽然 <code>Goroutine</code> 非常轻量（仅需 2KB 内存，作为对比，操作系统线程需要 2MB），但在处理海量任务时，如果直接为每个任务创建一个新的 <code>Goroutine</code>（还是前文的例子，假设有 100 万条数据，但每次最多只能拉取 100 条，如果为每个拉取 100 条数据的任务创建一个 <code>Goroutine</code>，那么就需要同时创建 1 万个），会产生以下问题：</p><ul><li>资源耗尽： 瞬时创建上万个 <code>Goroutine</code> 可能导致内存溢出。</li><li>上下文切换开销： 过多 <code>Goroutine</code> 会增加 CPU 调度器的负担。</li><li>缺乏控制： 难以限制对下游资源（如数据库连接、API 调用）的并发访问。</li></ul><p><code>Worker Pool</code> 通过 <strong>“复用”</strong> 和 <strong>“限额”</strong> 解决了这些问题。</p><h1 id="核心结构"><a href="#核心结构" class="headerlink" title="核心结构"></a>核心结构</h1><p>一个典型的 <code>Worker Pool</code> 由三个部分组成：</p><ul><li><code>Job Queue</code> (任务队列)： 通常是一个 <code>channel</code>，用于存放待处理的任务。</li><li><code>Worker</code> (工人)： 一组在后台循环运行的 <code>Goroutine</code>。</li><li><code>Result Queue</code> (结果队列)： 可选，用于收集处理后的结果。</li></ul><pre class="mermaid">graph LR    Generator[Job Generator<br/>任务生成者] -->|Enqueue| JobQueue[Job Queue<br/>任务队列]        subgraph WorkerPool [Worker Pool 工作池]        W1[Worker 1]        W2[Worker 2]        W3[Worker ...]    end        JobQueue -->|Fetch| W1    JobQueue -->|Fetch| W2    JobQueue -->|Fetch| W3        W1 -->|Result| ResultQueue[Result Queue<br/>结果队列]    W2 -->|Result| ResultQueue    W3 -->|Result| ResultQueue        ResultQueue -->|Collect| Collector[Result Collector<br/>结果收集者]        style JobQueue fill:#ffeb3b,stroke:#333,stroke-width:2px;    style ResultQueue fill:#4caf50,stroke:#333,stroke-width:2px;    style WorkerPool fill:#e3f2fd,stroke:#333,stroke-dasharray: 5 5;</pre><h1 id="Talk-is-cheap-show-me-the-code"><a href="#Talk-is-cheap-show-me-the-code" class="headerlink" title="Talk is cheap, show me the code!"></a>Talk is cheap, show me the code!</h1><p>一个简单的流程示意图，对应后续代码中的执行步骤：</p><pre class="mermaid">graph TD    %% 主流程节点    Init[1. 初始化通道] --> Start[2. 启动 5 个 Worker]    Start --> Send[3. 发送 1000 个任务]    Send --> CloseJobs[关闭 Job 通道]        %% 后台逻辑    Start -.->|后台运行| Worker[Worker: 抢任务 -> 处理 -> 存结果]        %% 监控与收集    CloseJobs --> Monitor[Monitor: 等待 Worker 全部结束]    Monitor --> CloseRes[关闭 Result 通道]    CloseRes --> Collect[4. 收集结果]        %% 数据流向示意    Worker -.->|处理结果| Collect        style Worker fill:#e1f5fe,stroke:#01579b    style Monitor fill:#fff9c4,stroke:#fbc02d</pre><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;sync&quot;</span></span><br><span class="line">    <span class="string">&quot;time&quot;</span></span><br><span class="line">)</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="comment"> * worker 函数：对应「核心结构」章节中的 Worker，从 jobs 通道读任务，处理后发给 results 通道</span></span><br><span class="line"><span class="comment"> * @param id      工人编号</span></span><br><span class="line"><span class="comment"> * @param jobs    只读通道 (&lt;-chan)：工人只能从这里拿任务</span></span><br><span class="line"><span class="comment"> * @param results 只写通道 (chan&lt;-)：工人只能把结果往这里放</span></span><br><span class="line"><span class="comment"> * @param wg      计数器：用来告诉主程序自己什么时候干完活</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(id <span class="type">int</span>, jobs &lt;-<span class="keyword">chan</span> <span class="type">int</span>, results <span class="keyword">chan</span>&lt;- <span class="type">int</span>, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="comment">// defer 保证即使函数中途出错，也会在退出前执行 Done()，避免主线程死锁</span></span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// range 会不断从 jobs 中取数据，直到 jobs 通道被 close(jobs) 且数据取完</span></span><br><span class="line">    <span class="keyword">for</span> j := <span class="keyword">range</span> jobs &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;Worker %d 开始处理任务 %d\n&quot;</span>, id, j)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 模拟耗时的真实业务逻辑（如调用小红书 API、读写数据库）</span></span><br><span class="line">        time.Sleep(time.Second)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 处理完后将结果放入结果通道</span></span><br><span class="line">        results &lt;- j * <span class="number">2</span></span><br><span class="line">        fmt.Printf(<span class="string">&quot;工人 %d: 完成任务 %d\n&quot;</span>, id, j)</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">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">const</span> numJobs = <span class="number">1000</span> <span class="comment">// 假设一共有 1000 个任务</span></span><br><span class="line">    <span class="keyword">const</span> numWorkers = <span class="number">5</span> <span class="comment">// 限制并发数为 5（控制压力），最多只有 5 个 worker 同时处理，可以灵活配置</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 初始化【任务通道】和【结果通道】</span></span><br><span class="line">    jobs := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, numJobs) <span class="comment">// 对应「核心结构」章节中的 Job Queue (任务队列)：通常是一个 channel，用于存放待处理的任务</span></span><br><span class="line">    results := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, numJobs) <span class="comment">// 对应「核心结构」章节中的 Result Queue (结果队列)：可选，用于收集处理后的结果</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 启动指定数量的 worker，目前数量是 5 </span></span><br><span class="line">    <span class="comment">// 此时工人已经就绪，但在 jobs 通道没数据前，他们都会阻塞在 range 那行 “待命”</span></span><br><span class="line">    <span class="keyword">for</span> w := <span class="number">1</span>; w &lt;= numWorkers; w++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> worker(w, jobs, results, &amp;wg)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 发送任务到队列</span></span><br><span class="line">    <span class="keyword">for</span> j := <span class="number">1</span>; j &lt;= numJobs; j++ &#123;</span><br><span class="line">        jobs &lt;- j</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 【关键点】任务发完了，一定要关闭 jobs 通道</span></span><br><span class="line">    <span class="comment">// 关闭后，workers 里的 range 循环在取完剩余任务后会自动结束</span></span><br><span class="line">    <span class="built_in">close</span>(jobs)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 监控完成情况 (开启一个“收尾”协程)</span></span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        wg.Wait() <span class="comment">// 等待所有工人退出</span></span><br><span class="line">        <span class="built_in">close</span>(results) <span class="comment">// 所有人都干完了，结果通道也可以关了</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 收集结果</span></span><br><span class="line">    <span class="keyword">for</span> res := <span class="keyword">range</span> results &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;结果:&quot;</span>, res)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;所有任务处理完毕！&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="FAQ"><a href="#FAQ" class="headerlink" title="FAQ"></a>FAQ</h1><blockquote><p>那些我发现一开始并没有完全理解的点。</p></blockquote><h2 id="1-为什么-jobs-和-results-都要初始化成有缓冲区的通道-Buffered-Channel-？"><a href="#1-为什么-jobs-和-results-都要初始化成有缓冲区的通道-Buffered-Channel-？" class="headerlink" title="1. 为什么 jobs 和 results 都要初始化成有缓冲区的通道 (Buffered Channel)？"></a>1. 为什么 <code>jobs</code> 和 <code>results</code> 都要初始化成有缓冲区的通道 (<code>Buffered Channel</code>)？</h2><p>如果没有缓冲区，发一个任务就必须立刻有一个工人接走，否则发送者就会卡在那动弹不得。</p><p>有了缓冲区，主程序可以先把 1000 个任务一股脑丢进暂存区，然后去干别的事。工人根据自己的节奏慢慢拿，这样能极大提高吞吐量，解耦了“发任务”和“做任务”的速度差异。</p><h2 id="2-在发送任务结束后就立刻-close-jobs-，那如果-jobs-内的数据还没有被处理，不会出问题吗？"><a href="#2-在发送任务结束后就立刻-close-jobs-，那如果-jobs-内的数据还没有被处理，不会出问题吗？" class="headerlink" title="2. 在发送任务结束后就立刻 close(jobs)，那如果 jobs 内的数据还没有被处理，不会出问题吗？"></a>2. 在发送任务结束后就立刻 <code>close(jobs)</code>，那如果 <code>jobs</code> 内的数据还没有被处理，不会出问题吗？</h2><p><strong>不会。</strong>  这是 Go Channel 的重要特性：</p><ul><li>关闭通道仅仅意味着：<strong>以后不会再有新任务进来了</strong>。</li><li>已经在通道里的任务（缓冲区里的）依然可以被正常读取。</li><li>工人们读完缓冲区里的最后一个任务后，<code>for j := range jobs</code> 就会自动结束循环。</li></ul><h2 id="3-为什么-wg-Wait-和-close-results-要单独放在一个-Goroutine-里？"><a href="#3-为什么-wg-Wait-和-close-results-要单独放在一个-Goroutine-里？" class="headerlink" title="3. 为什么 wg.Wait() 和 close(results) 要单独放在一个 Goroutine 里？"></a>3. 为什么 <code>wg.Wait()</code> 和 <code>close(results)</code> 要单独放在一个 <code>Goroutine</code> 里？</h2><p><strong>这是为了避免“死锁”。</strong> </p><p>主程序运行到 <code>for res := range results</code> 时会一直等着拿数据。</p><p>工人们一直在往 <code>results</code> 塞数据。</p><p>如果你在主程序里写 <code>wg.Wait()</code>，主程序会卡在这里等工人干完。但如果工人因为 <code>results</code> 通道满了（且没人从里面拿走）而阻塞，他们就永远干不完。</p><p><strong>结果：</strong> 工人在等主程序拿数据，主程序在等工人干完活。谁也不让谁，程序就“卡死”了。单独开一个协程监控收尾，可以保证主程序能立刻开始处理结果。</p><h2 id="4-多个-worker-并发从-jobs-里取数据，不会出现取到同一个任务的情况吗？"><a href="#4-多个-worker-并发从-jobs-里取数据，不会出现取到同一个任务的情况吗？" class="headerlink" title="4. 多个 worker 并发从 jobs 里取数据，不会出现取到同一个任务的情况吗？"></a>4. 多个 worker 并发从 <code>jobs</code> 里取数据，不会出现取到同一个任务的情况吗？</h2><p><strong>绝对不会。</strong> </p><p><code>Go</code> 的 <code>Channel</code> 底层有锁机制（加锁的环形链表），它是并发安全的。</p><p>就像一个自动售货机，无论多少人同时扫码买最后一瓶水，最终只会有一个人拿到，售货机内部会保证数据的原子性。</p><h1 id="TODO"><a href="#TODO" class="headerlink" title="TODO"></a>TODO</h1><blockquote><p>这两块内容在需求中没有设计，目前只停留在理论阶段，等实践过了再更新到正文。</p></blockquote><h2 id="1-如果其中一个任务导致-Worker-Panic（崩溃）了怎么办？"><a href="#1-如果其中一个任务导致-Worker-Panic（崩溃）了怎么办？" class="headerlink" title="1. 如果其中一个任务导致 Worker Panic（崩溃）了怎么办？"></a>1. 如果其中一个任务导致 Worker Panic（崩溃）了怎么办？</h2><p><strong>问题：</strong> 默认情况下，一个 <code>Goroutine</code> 崩溃会导致整个进程退出。<br><strong>解决：</strong> 在 worker 函数内部使用 <code>recover()</code>。在 <code>Worker Pool</code> 中，通常需要捕获异常，打印日志，并调用 <code>wg.Done()</code>，否则 <code>Wait()</code> 永远等不到结束，程序会死锁。</p><h2 id="2-如何优雅地“提前关闭”-Worker-Pool？"><a href="#2-如何优雅地“提前关闭”-Worker-Pool？" class="headerlink" title="2. 如何优雅地“提前关闭” Worker Pool？"></a>2. 如何优雅地“提前关闭” <code>Worker Pool</code>？</h2><p><strong>场景：</strong> 比如用户点击了取消按钮，或者遇到了严重错误，不想跑剩下的 1 万个任务了。<br><strong>技巧：</strong> 仅仅 <code>close(jobs)</code> 是不够的，因为工人可能正阻塞在某个耗时操作上。这时候需要引入 <code>context.Context</code>。给工人传入一个 <code>ctx</code>，在函数内通过 <code>select &#123; case &lt;-ctx.Done(): return &#125;</code> 来实现秒级的停止。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近有个业务需求，需要去&lt;a href=&quot;https://ad-market.xiaohongshu.com/docs-center?bizType=943&amp;articleId=2734&quot;&gt;小红书开放平台&lt;/a&gt;对接一些报表数据接口，通过定时任务，在每天特定的时间点拉取效果数据并落库。</summary>
    
    
    
    <category term="后端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="后端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="Go" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/Go/"/>
    
  </entry>
  
  <entry>
    <title>究竟什么是 Event Loop丨总结 Philip Roberts 在 JSConf 的演讲</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/2a798ab5.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/2a798ab5.html</id>
    <published>2024-03-07T15:04:15.000Z</published>
    <updated>2026-01-05T06:17:24.482Z</updated>
    
    <content type="html"><![CDATA[<p>  <a href="https://www.youtube.com/watch?v=8aGhZQkoFbQ&list=PLI-Ik-cyh3JN2usTBFpZAVVPezC2I-8gM&index=5">26 分钟的视频</a>深入浅出地讲解了什么是 Event Loop 以及它与浏览器渲染的关系。<span id="more"></span></p><h2 id="对-JavaScript-Runtime-的正确认识"><a href="#对-JavaScript-Runtime-的正确认识" class="headerlink" title="对 JavaScript Runtime 的正确认识"></a>对 JavaScript Runtime 的正确认识</h2><p>  对于 JavaScript，我们常说 JS 有一个调用栈 (call stack)、有一个事件循环机制 (Event Loop)、有一个任务/回调队列 (task/callback queue) 以及一些 WebAPI，但这等价于 V8 也拥有以上这些东西吗？</p><p>  答案是：并非如此。 </p><p>  以 V8 为例，简单来看，它其实只包含一个堆 (heap) 和一个调用栈 (call stack)。</p><p>  如果 clone 一下 V8 的源码，会发现 setTimeout、DOM、HTTP Request 这些东西都是不在 V8 的源码中的。</p><p>  也就是说，V8 并不包括 Event Loop、callback queue 和其他的 WebAPIs。</p>  <div align=center>    <img src="https://s3.bmp.ovh/imgs/2024/05/19/7848a596d0d408bb.jpg" />  </div><p>  由上图可以看出，JS 的浏览器运行环境包含三部分: </p><ul><li>V8 (JavaScript Runtime)</li><li>浏览器提供的 WebAPIs (DOM、AJAX、setTimeout…) </li><li>callback queue </li></ul><p>  这三者互相配合，共同实现了 Event Loop。</p><h2 id="The-call-stack"><a href="#The-call-stack" class="headerlink" title="The call stack"></a>The call stack</h2><p>  <code>one thread === one call stack === one thing at a time</code></p><p>  JavaScript 是一门单线程语言，也就是只有一个调用栈，即一次只能做一件事。</p>  <div align=center>    <img src="https://s3.bmp.ovh/imgs/2024/05/19/9dffbb458ecfd50d.jpg" />  </div>  <center style="color:#C0C0C0;">抛错后的 stack trace</center>  <div align=center>    <img src="https://s3.bmp.ovh/imgs/2024/05/19/245adf7cc4cfe99e.jpg" />  </div>  <center style="color:#C0C0C0;">爆栈的报错</center><h2 id="blocking-阻塞"><a href="#blocking-阻塞" class="headerlink" title="blocking 阻塞"></a>blocking 阻塞</h2><p>  <code>What happens when things are slow?</code></p><p>  慢的东西会阻塞调用栈</p><p>  什么是慢的/快的东西？以下是一些直观的理解：</p><ul><li>console.log 不慢</li><li>从 0 到 100亿 做一个 while 循环是慢的</li><li>网络请求是慢的 </li></ul><p>  假设所有的 network request 都是同步（串行）的，那我们要等待每一个请求都结束之后，才能运行其他的代码。</p><h2 id="Why-is-this-a-problem-because-browsers"><a href="#Why-is-this-a-problem-because-browsers" class="headerlink" title="Why is this a problem? because, browsers."></a>Why is this a problem? because, browsers.</h2><p>  blocking 会成为一个问题的一大原因，就是因为我们是在浏览器里运行代码。</p><p>  而阻塞性的代码，会卡住 call stack，从而<strong>阻塞页面的渲染</strong>。</p><h2 id="the-solution-asynchronous-callbacks"><a href="#the-solution-asynchronous-callbacks" class="headerlink" title="the solution? asynchronous callbacks"></a>the solution? asynchronous callbacks</h2><p>  <code>Here&#39;s a function. Call me maybe?</code></p>  <div align=center>    <img src="https://s3.bmp.ovh/imgs/2024/05/19/78ace090e55544cc.jpg" />  </div><p>  以这段简单的代码为例，执行到 setTimeOut 时，其中的 callback 就好像从 call stack 消失了一样。然后 5s 之后，又神奇地回到了 call stack 中，并被执行，其中到底有什么奥秘呢？</p><h2 id="Concurrency-amp-the-Event-Loop"><a href="#Concurrency-amp-the-Event-Loop" class="headerlink" title="Concurrency &amp; the Event Loop"></a>Concurrency &amp; the Event Loop</h2><p>  <code>One thing at a time, except not really?</code></p><p>  JavaScript Runtime 确实一次只能做一件事。但浏览器，并不只有 JavaScript Runtime。</p>  <div align=center>    <img src="https://s3.bmp.ovh/imgs/2024/05/19/7848a596d0d408bb.jpg" />  </div>  <center style="color:#C0C0C0;">还记得这张图吗~</center><p>  还是以 setTimeout 为例，正如上文提到的，setTimeout 并不存在于 runtime 中，而是由浏览器的提供的 WebAPI。</p><p>  setTimeout 接收一个 callback 和一个 delay，当执行到 setTimeout 时，便由 WebAPI 接管了（上文提到过，setTimeout 并不存在于 V8 源码中），在 WebAPI 接管后，setTimeout 本身会 pop 出 call stack。</p>  <div align=center>    <img src="https://s3.bmp.ovh/imgs/2024/05/19/1c8358013ed2daa1.jpg" />  </div>  <p>  WebAPI 提供了计时器的功能，当 delay 的时间到了之后，WebAPI 并不会直接把 callback 塞到 call stack 中（如果真是如此的话，那 callback 很可能会突然地出现在正在执行的代码里，这肯定是不对的）。</p><p>  事实是，浏览器还提供了一个任务队列（回调队列），当倒计时结束后，这个 callback 将会进入这个队列中，等待进入 call stack。</p>  <div align=center>    <img src="https://s3.bmp.ovh/imgs/2024/05/19/8b6b9225900c089f.jpg" />  </div><p>  这个时候，也就是 Event Loop 正式出场的时候。</p><p>  Event Loop 做的事情非常简单：看一眼 call stack，如果空了，那就从任务队列中取出第一个任务，塞到 call stack 中去执行。</p><p>  这个时候就得提到经典的 setTimeout(callback, 0) 了，通过上面的介绍，我们可以了解到这样写的目的，就是为了在 call stack 被清空的时候，才去执行 callback。</p><p>  AJAX 也是同理，AJAX 并不存在于 V8 中，而是由浏览器提供。请求的过程由 WebAPI 接手，因此并不会阻塞 call stack。即使这个请求永远不结束，也不会阻塞其他代码的执行 &amp; 浏览器的渲染。</p><p>  Event Listener 也是同理。以 $.on(‘button’, ‘click’, callback) 为例。WebAPIs 将会接手 click 事件的注册，当 click 发生时，WebAPIs 将会向任务队列中推入一个 callback。</p><h2 id="Event-Loop-是如何配合浏览器渲染的？"><a href="#Event-Loop-是如何配合浏览器渲染的？" class="headerlink" title="Event Loop 是如何配合浏览器渲染的？"></a>Event Loop 是如何配合浏览器渲染的？</h2><p>  在 call stack 不为空时，浏览器是无法渲染的。</p><p>  可以把 render 也看作是一个 callback，有一个专门的 render queue，它的优先级比 task queue 更高。</p><p>  以 60 帧为例，每 16.6ms，浏览器就会把一个 render 加到 render queue （优先级更高的队列）中，但要等到 call stack 清空时，再进行 render。</p><p>  我们常说的“不要阻塞事件循环”，就是指不要在栈上执行又臭又长的同步代码，因为这样一来，浏览器就无法渲染了。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;  &lt;a href=&quot;https://www.youtube.com/watch?v=8aGhZQkoFbQ&amp;list=PLI-Ik-cyh3JN2usTBFpZAVVPezC2I-8gM&amp;index=5&quot;&gt;26 分钟的视频&lt;/a&gt;深入浅出地讲解了什么是 Event Loop 以及它与浏览器渲染的关系。</summary>
    
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="JavaScript" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>发送验证码后的节流倒计时丨刷新 &amp; 重新进入页面，还原倒计时状态</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/9e1be607.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/9e1be607.html</id>
    <published>2023-08-13T04:45:38.000Z</published>
    <updated>2023-09-12T12:29:51.154Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>　　最近在做一个 H5 工具，需要手机号 + 验证码登录，很自然地，点击发送验证码后需要等待一段时间才能重新发送，用于请求节流，避免用户疯狂点击：<span id="more"></span></p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/08/13/34a3ff33a7f0fe76.gif" /></div>&nbsp;<p>　　不过这里其实有个隐藏需求——如果<code>仍然在冷却时间内</code>，那么用户无论是刷新或是关闭页面，再次打开登录弹窗，<code>需要直接展示正确的倒计时状态</code>。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><blockquote><p>使用经典的 localStorage：<br>· 发送验证码时，将发送时间 (lastSendingTime) 存入 localStorage，并开启 60 秒倒计时。<br>· 倒计时结束后，清除 localStorage 中的 lastSendingTime。<br>· 重新进入页面时，若 localStorage 中存有 lastSendingTime，则说明仍处于冷却时间内，那么计算出剩余的倒计时 N，并开启 N 秒倒计时。</p></blockquote><h1 id="Talk-is-cheap-show-me-the-code"><a href="#Talk-is-cheap-show-me-the-code" class="headerlink" title="Talk is cheap, show me the code!"></a>Talk is cheap, show me the code!</h1><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><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="keyword">const</span> [countdown, setCountdown] = <span class="title function_">useState</span>(<span class="number">60</span>) <span class="comment">// 倒计时</span></span><br><span class="line">  <span class="keyword">const</span> [canSendCode, setCanSendCode] = <span class="title function_">useState</span>(<span class="literal">true</span>) <span class="comment">// 控制按钮文案的状态</span></span><br><span class="line">  <span class="keyword">const</span> [timer, setTimer] = <span class="title function_">useState</span>() <span class="comment">// 定时器 ID</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">sendVerificationCode</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// network request...</span></span><br><span class="line">      <span class="title class_">Toast</span>.<span class="title function_">show</span>(&#123; <span class="attr">content</span>: <span class="string">&#x27;验证码发送成功&#x27;</span> &#125;)</span><br><span class="line">      <span class="title function_">startCountdown</span>()</span><br><span class="line">      <span class="title function_">setCanSendCode</span>(<span class="literal">false</span>)</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="title function_">setCountdown</span>(<span class="number">0</span>)</span><br><span class="line">      <span class="title function_">setCanSendCode</span>(<span class="literal">true</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">startCountdown</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> nowTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>()</span><br><span class="line">    <span class="keyword">const</span> lastSendingTime = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">&#x27;lastSendingTime&#x27;</span>)</span><br><span class="line">    <span class="keyword">if</span> (lastSendingTime) &#123;</span><br><span class="line">      <span class="comment">// 若 localStorage 中存有 lastSendingTime，则说明仍处于冷却时间内，计算出剩余的 countdown</span></span><br><span class="line">      <span class="keyword">const</span> restCountdown = <span class="number">60</span> - <span class="built_in">parseInt</span>(((nowTime - lastSendingTime) / <span class="number">1000</span>), <span class="number">10</span>)</span><br><span class="line">      <span class="title function_">setCountdown</span>(restCountdown &lt;= <span class="number">0</span> ? <span class="number">0</span> : restCountdown)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 否则说明冷却时间已结束，则 countdown 为 60s，并将发送时间存入 localStorage</span></span><br><span class="line">      <span class="title function_">setCountdown</span>(<span class="number">60</span>)</span><br><span class="line">      <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">&#x27;lastSendingTime&#x27;</span>, nowTime)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="title function_">setTimer</span>(</span><br><span class="line">      <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="title function_">setCountdown</span>(<span class="function"><span class="params">old</span> =&gt;</span> old - <span class="number">1</span>)</span><br><span class="line">      &#125;, <span class="number">1000</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">// 重新进入页面时，若 localStorage 中存有上次的发送时间，则说明还处于冷却时间内，则调用函数计算剩余倒计时；否则什么也不做</span></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> lastSendingTime = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">&#x27;lastSendingTime&#x27;</span>) </span><br><span class="line">    <span class="keyword">if</span> (lastSendingTime) &#123;</span><br><span class="line">      <span class="title function_">setCanSendCode</span>(<span class="literal">false</span>)</span><br><span class="line">      <span class="title function_">startCountdown</span>()</span><br><span class="line">    &#125;</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">clearInterval</span>(timer)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [])</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="comment">   * - 清空 localStorage 中存储的上次发送时间</span></span><br><span class="line"><span class="comment">   * - 清除定时器</span></span><br><span class="line"><span class="comment">   * - 重置倒计时</span></span><br><span class="line"><span class="comment">  */</span></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (countdown &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="title function_">setCanSendCode</span>(<span class="literal">true</span>)</span><br><span class="line">      <span class="variable language_">localStorage</span>.<span class="title function_">removeItem</span>(<span class="string">&#x27;lastSendingTime&#x27;</span>)</span><br><span class="line">      <span class="built_in">clearInterval</span>(timer)</span><br><span class="line">      <span class="title function_">setCountdown</span>(<span class="number">60</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [countdown])</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> (</span><br><span class="line">  &#123;canSendCode ? (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">span</span> <span class="attr">onClick</span>=<span class="string">&#123;sendVerificationCode&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      获取验证码</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line">  ) : (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      获取验证码(&#123;`$&#123;countdown&#125;`&#125;)</span></span><br><span class="line"><span class="language-xml">    <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></pre></td></tr></table></figure><h1 id="最终效果"><a href="#最终效果" class="headerlink" title="最终效果"></a>最终效果</h1><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/08/13/8218b9d41c0ad738.gif" /></div><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>　　一开始感觉这是个很简单的小需求，可能 20min 就写完了，但实际花了两个多小时才把逻辑全部 cover 到，还是不能太自信啊~</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;　　最近在做一个 H5 工具，需要手机号 + 验证码登录，很自然地，点击发送验证码后需要等待一段时间才能重新发送，用于请求节流，避免用户疯狂点击：</summary>
    
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="JavaScript" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>一些超级棒的前端技术博客 &amp; 演讲</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/435e2644.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/435e2644.html</id>
    <published>2023-06-18T04:22:01.000Z</published>
    <updated>2026-01-11T10:23:29.028Z</updated>
    
    <content type="html"><![CDATA[<p>　　时常看到一些超级棒的文章或者演讲，在这里留个档，空下来了会去做一下整理和总结，它们真的每一篇都值得反复阅览。<span id="more"></span></p><h1 id="JavaScript"><a href="#JavaScript" class="headerlink" title="JavaScript"></a>JavaScript</h1><p><a href="https://www.youtube.com/watch?v=8aGhZQkoFbQ">Event Loop I</a></p><p><a href="https://www.youtube.com/watch?v=u1kqx6AenYw">Event Loop II</a></p><h1 id="React"><a href="#React" class="headerlink" title="React"></a>React</h1><p><a href="https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/">A (Mostly) Complete Guide to React Rendering Behavior</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　时常看到一些超级棒的文章或者演讲，在这里留个档，空下来了会去做一下整理和总结，它们真的每一篇都值得反复阅览。</summary>
    
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="JavaScript" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>拼音输入法的神奇 API丨onCompositionStart &amp; onCompositionEnd</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/11426bbe.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/11426bbe.html</id>
    <published>2022-12-08T06:17:45.000Z</published>
    <updated>2023-08-14T11:19:55.400Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>　　最近开发遇到了一个很 tricky 的问题，直接上 gif，看了就明白了：<span id="more"></span></p><p><img src="https://s3.bmp.ovh/imgs/2023/01/01/9815a1854c8bffa4.gif" alt="bug复现"></p><p>　　可以明显看到，在搜狗拼音输入法<strong>还没有完成输入的时候</strong>就已经触发搜索了，此时根据拼音内容(一些英文)去搜索，会搜索不到任何结果，导致 Table 内显示空信息。</p><p>　　这明显不是我们想要的效果，我们所希望的应该是在<strong>输入法完成输入（按下空格或回车）</strong>后，再去触发搜索框的搜索。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><blockquote><p>onCompositionStart &amp; onCompositionEnd</p></blockquote><p>　　<a href="https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent">这两个事件</a>确实第一次接触，以 <code>compositionstart event</code> 为例：</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/577af882fddac58b.jpg" /></div><center style="color:#C0C0C0;">MDN 对 compositionstart event 的解释</center><p>　　简单来说，搜狗拼音输入法就是图中所说的 <code>IME(Input Method Editor)</code> ，当开启一段 <code>composition session</code> 的时候就会触发这个事件，至于什么是 <code>composition session</code> 呢~</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/a4202af698e488f7.jpg" /></div>  &nbsp;<p>　　知道有这个 event 之后，一切也就豁然开朗了，我们可以引入一个 <code>lock</code> 变量，通过 <code>onCompositionStart</code> &amp; <code>onCompositionEnd</code> 来维护；当且仅当 <code>lock</code> 为 <code>false</code> 的时候才触发搜索~</p><h1 id="（伪）代码"><a href="#（伪）代码" class="headerlink" title="（伪）代码"></a>（伪）代码</h1><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Input</span></span><br><span class="line">  onChange=&#123;<span class="function"><span class="params">e</span> =&gt;</span> <span class="title function_">setInputVal</span>(e.<span class="property">target</span>.<span class="property">value</span>)&#125;</span><br><span class="line">  onCompositionStart=&#123;<span class="function">() =&gt;</span> <span class="title function_">setLock</span>(<span class="literal">true</span>)&#125;</span><br><span class="line">  onCompositionEnd=&#123;<span class="function">() =&gt;</span> <span class="title function_">setLock</span>(<span class="literal">false</span>)&#125;</span><br><span class="line">/&gt;</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></pre></td><td class="code"><pre><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 输入拼音的情况下，选择了文字才搜索，正在输入拼音时不触发搜索</span></span><br><span class="line">  !lock &amp;&amp; <span class="title function_">triggerSearch</span>(inputVal);</span><br><span class="line">&#125;, [lock, inputVal]);</span><br></pre></td></tr></table></figure><h1 id="最终效果"><a href="#最终效果" class="headerlink" title="最终效果"></a>最终效果</h1><p><img src="https://s3.bmp.ovh/imgs/2023/01/01/4e3fad241e7bdbe7.gif" alt="最终效果"></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>　　之前我一直以为这不是个前端范畴的问题，是系统层面的，事实证明还是认知局限呀🤣今后还是要多多学习，多多记录。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;　　最近开发遇到了一个很 tricky 的问题，直接上 gif，看了就明白了：</summary>
    
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="JavaScript" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>提前转正啦😝丨一些回忆与复盘</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/ba9d0ab6.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/ba9d0ab6.html</id>
    <published>2022-11-29T12:32:59.000Z</published>
    <updated>2026-01-05T06:17:24.482Z</updated>
    
    <content type="html"><![CDATA[<p>　　提前转正啦!!!<span id="more"></span></p><p>　　​这段时间有太多想记录分享的，但随着相册里的照片越积越多却又一直没整理，原本设想的月度复盘最终演变成了这样一篇转正综述😝。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/348a1c73b181211b.jpg" /></div><p>&nbsp;<br>　　​简单 self review 了一下，相比五个月前的自己，最大的进步来自于四个方面:</p><h2 id="对-Stack-Overflow、GitHub-等社区的使用"><a href="#对-Stack-Overflow、GitHub-等社区的使用" class="headerlink" title="对 Stack Overflow、GitHub 等社区的使用"></a>对 <code>Stack Overflow</code>、<code>GitHub</code> 等社区的使用</h2><p>　　​本科期间对这些社区的使用仅限于嫖开源项目，现在逐渐能根据业务导向去探索一些解决方案了，也慢慢能够将宽泛的业务需求总结成一个个 one-sentence question 去寻找一些最佳实践。相比从前只能在 CSDN 的屎山里遨游，似乎感觉自己慢慢有 web developer 的感觉了🤣。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/67567e71fe07a63f.jpg" /></div><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/81e92460b7d40c20.png" /></div><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/950f21c9e2c2ba7e.png" /></div><center style="color:#C0C0C0;">一次完整的提 issue -> 被响应 -> 解决业务问题的经历😁</center><h2 id="面对业务难题的心态"><a href="#面对业务难题的心态" class="headerlink" title="面对业务难题的心态"></a>面对业务难题的心态</h2><p>　　如果要选这五个月来最难的一个需求，我觉得是贡献度页面的 PDF 下载——用从未使用过的库(html2canvas &amp; jspdf)去完成一个定制化很高的需求。<br>　　拆开看其实就是两个大难点——<code>从未使用过</code> &amp; <code>定制化很高</code>。<br>　　前者还好，只需要花时间去读文档试 API；但对于后者，则意味着网上不会有完全可以参照的解决方案。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/d9569f63653b0f6b.png" /></div><center style="color:#C0C0C0;">需要实现的 PDF 下载逻辑</center><p>　　官方文档的目的是用最简示例让你明白核心 API 的用法，但如何去组合可用的几十上百个 API 来完成业务需求，这个是官方文档没法 cover 到的。记得刚开始做的时候确实毫无头绪，后来真的是某个瞬间突然有了想法：html2canvas 是基于 DOM node 的，那我结合业务需求去手动控制 DOM node 是不是就可以了呢？后来尝试了一个简单的 case 发现确实可以！<br>　　有了大方向，后面去磨逻辑就可以了，细节会非常多，但到了这个阶段，其实已经演变成了和第一个难点同一级别的问题了——花时间就可以解决的问题，俗称体力活🤣。<br>　　这个需求完成之后，有种完成进化的感觉。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/9ee08fc73213ee7b.png" /></div><center style="color:#C0C0C0;">完成后提的 PR </center><p>　　quote 一下关于这件事的微博，记录了我的思考： </p><blockquote><p>　　最近刚做完一个比较特殊的导出 PDF 需求，用的两个库都是第一次接触( <code>html2canvas</code> &amp; <code>jspdf</code> )，一路上遇到了不少坑，最后都摸索着解决了。作为一个喜欢复盘的人，本来想着好好复盘一下这两个库的使用流程，但是后来好像就突然醒悟了：为什么要去细抠这两个库呢，在未来十几年开发生涯里用到这两个库的次数大概率屈指可数，真正有价值的其实是那些坑，是那些开发过程中的瓶颈，或者说，是跨过坑和迈过瓶颈的思考过程和处理方法。<br>　　其实是一个很简单的道理，相比聚焦于某个具体问题。作为一名工程师，需要培养的其实是解决问题的能力。从而达到一种 mentality:</p><ul><li>任何问题都有解决的方法</li><li>我遇到的问题也是</li><li>我一定可以解决它</li></ul></blockquote><h2 id="​bug-的分析-amp-调试"><a href="#​bug-的分析-amp-调试" class="headerlink" title="​bug 的分析 &amp; 调试"></a>​bug 的分析 &amp; 调试</h2><p>　　这点或许就是所谓的<code>经验</code>，开发年限久了，见识的场景多了，改 bug 的能力就自然而然提升了。<br>　　就我的例子而言，反映在​对组件渲染、状态流动、异步有了更深的认识，所以打断点越来越准确，定位问题越来越快了。<br>　　此外，也离不开其他前端家人们的 code review 与 comments，我在解决完一些比较 tricky 的 bug 的时候都会尽可能把心路历程和来龙去脉写在 commit message 里，我觉得能把原因-分析-解决的链路描述清楚，才算真正地吃透了这个 bug；此外，一般的 code review 大家只会看代码的风格是否可以优化，没太多时间去细究内部的业务逻辑(大家手头都有活)，但有了比较详尽的 description 之后能让 reviewer 更好地理解场景，有时他们会提供一些更好的思路，让我发现其实是我搞复杂了。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/77a43966a42615c8.jpg" /></div><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/01/88a60611ae1fd1c0.jpg" /></div><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/09/52c0372248e27ab5.jpg" /></div><center style="color:#C0C0C0;">解决一些 tricky bug 的 PR </center><h2 id="技术文档的书写"><a href="#技术文档的书写" class="headerlink" title="技术文档的书写"></a>技术文档的书写</h2><p>　　像<code>大字版</code>和<code>埋点</code>这两个需求，我算是前端的 owner，从调研技术实现，到这两个模块的基建，再到书写文档和各位前端家人一起维护，有体验到一种 leadership 的感觉，是一种从未有过的体验😁。</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20221129-10.jpg" /></div><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20221129-11.png" /></div><center style="color:#C0C0C0;">埋点的基建 & 文档</center>&nbsp;<div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20221129-12.png" /></div><center style="color:#C0C0C0;">大字版文档</center><p>　　包括在做某个功能的时候需要复用一个很复杂的组件，在确定复用方案上我也接受了<a href="https://liriansu.com/">紫月老师</a>的建议，写了篇文档把两种方式做了对比，最终和大家讨论选取了一个更适合的方案。</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20221129-13.jpg" /></div><center style="color:#C0C0C0;">关于复用方案的评估文档</center><p>　　把零碎的思路形成易于接受的、有条理的文字是一件很浪漫的事情。(顺便感叹一句飞书文档真的好用，国庆去敦煌做了份<a href="https://hzt1zdrg2z.feishu.cn/docx/Kz4rd8NBQopGH4xggircbhuhnqc?from=from_copylink">攻略</a>，一开始用的是 Google Docs，不是很顺手，换飞书文档之后体验大大提升，强烈安利😋)</p><h2 id="End"><a href="#End" class="headerlink" title="End"></a>End</h2><p>　　下班之后​随手写了点，anyway，保持热爱，继续前进!</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　提前转正啦!!!</summary>
    
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>或许遭遇了人生最大的 setback ?</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/7709adb1.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/7709adb1.html</id>
    <published>2022-06-13T15:11:01.000Z</published>
    <updated>2026-01-05T06:17:24.481Z</updated>
    
    <content type="html"><![CDATA[<p>　　6.7，毕设答辩结束后两个小时，明略告知我要毁约，仿佛当头一棒。<span id="more"></span><br>　　其实前两周在脉脉已经听到了些风声，大致是说公司动用 1500 人做了一年半的产品 (EIP) 失败了，疫情和外部环境雪上加霜，公司现金流已极度不健康，要裁员 70%，3500 人的公司只留 800 人。</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-1.jpg" width=256 /></div><center style="color:#C0C0C0;">CEO 发的全体邮件</center><p>　　即使消息一直在发酵，我还是相对乐观，因为那些爆料人也说了，秒针(我们部门)作为独立的核心部门，是不会有大动作的。</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-2.jpg" width=256 /></div><center style="color:#C0C0C0;">看着明略的同事圈热度一路飙升</center><p>　　而且，我去提前实习了近四个月，如期完成了布置给我的所有开发任务，出色地融入了团队，就算真的要毁约一批校招生，相比那些没去提前实习的同学，我也有绝对的优势能够留下。<br>　　最后，也是一丝侥幸心理，6 月份了，即使公司再没有人性，也做不出在毕业前两个礼拜毁约的举动吧，这不仅是毁人前途，也是断人后路。<br>　　但还是发生了，击碎了我最后的幻想。</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-3.jpg" /></div><center  style="color:#C0C0C0;">甚至在 5.23 还发了入职提示邮件</center><p>　　好笑的是，在被毁约的前一天，我都还在为相同遭遇的应届生们抱不平。</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-4.jpg" width=300 /></div><center  style="color:#C0C0C0;">一语成谶</center><p>　　7 号下午我处在崩溃的边缘，我的理性压制着杂乱稀碎的情绪，我努力告诉自己，去消化这个消息，去做对当下最好的决定。<br>　　我在找一个可以发泄的点，尝试将这个结果归咎于我的某一个错误决定或举动，但我找不到。<br>　　从去年 3 月份开始，我刷了五十多套视频，背了无数的面经八股，从 9.7 第一场面试开始，20 天拿到了 3 份 offer，10.9 明略正式给我开了 30k 的意向书，我觉得秋招圆满收官了，也拒绝了此后的所有笔面试流程。</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-5.jpg" /></div><center  style="color:#C0C0C0;">结束我秋招的 offer</center><p>　　我开始帮朋友们辅导面试、和他们分享学习路线、解决群友们的开发问题。这份 offer 让我保持着学习的热情，让我愿意放弃最后的校园生活去公司提前适应。公司的一切也让我无比满意，顶级的办公环境、前沿的技术栈、扁平的团队氛围、还有知无不言、贴心带着我成长的 mentor。</p><div align=center>    <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-6.jpg" width=500 /></div><div align=center>    <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-7.jpg" width=500 /></div><center  style="color:#C0C0C0;">那时候确实很热爱这里</center><br/><div align=center>    <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-8.jpg" width=500 /></div><div align=center>    <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220613-9.jpg" width=500 /></div><center  style="color:#C0C0C0;">津铭哥对我真的很好</center> <p>　　我曾经跑在前面，但一个解约电话和一份违约金让我回到了起点。<br>　　这就是绝境了吧？秋招的另外两份 offer 询问后被告知已经没有 hc 了，形势本就惨淡的春招完全结束，补录也所剩无几，即使还有面试机会，实习之后的我专注于业务，用来应付笔面试的八股文和算法也已淡忘生疏。<br>　　更何况，离毕业只有 20 天了，要去公司办离职手续，走毁约流程，要去了解封校期间的就业办政策，处理毕业事宜，要投简历，要去捡起淡忘的八股文和算法，我问自己，这怎么可能办得到呢。<br>　　曾经的野心和理想开始反噬我，我从未陷入过这个程度的自我怀疑。<br>　　晚上，我收拾了一下情绪，开始检索各个招聘平台，牛客、毕业申、智联、boss，我投递了所有还能投递的公司。<br>　　8 号醒的时候天还黑着，看了眼手机 4 点多，但没有了睡意，起床开始看秋招时的面经和笔记。上午开始陆续有 hr 联系我。第一个面试在下午两点半，五位面试官，持续一个多小时，一面过。晚上七点，另一家公司，将近一个小时，一面过。我屏蔽了所有的社交媒体，不让自己的思绪有一丝空闲，我怕那种随之而来的窒息感。<br>　　随着 9 号两场面试的通过，我的状态好些了，但更开心的是遇到了心仪的公司：在淮海中路，位置热闹、技术栈前沿，leader 是保送交大的竞赛生，hr 给我分享了他的博客，映入眼帘的第一篇：《那个喊了十三声发物资的邻居》，翻到最下面，看到一篇 2013 年的文章：《妈的，交大》，我的第一感觉：他是个有趣的人。面试的时候他也让我感受到了一种和那些生硬地问一个个技术问题的面试官不同的感觉。腾讯会议里还有另一位女生，他告诉我，这位女生本科也是交大，硕士在密歇根大学，同样是今年的校招生，如果我加入，以后大家就是同一个 team 了，我说，我喜欢和优秀的人在一起。在我一面通过并表达强烈的加入愿景之后，他也承诺会为我加快推进流程。<br>　　9 号晚上做完了笔试，10号下午 cto 面 + hr 面，晚上 6 点半等来了 offer call。<br>　　放下手机，也许是这几天没怎么休息，有些恍惚。两天前我以为最终的结局或许就是要去国企或者某个小厂冒着野心和技术慢慢被消磨的风险去沉淀（如果消磨太多的话或许是沉沦？）两三年，幸好，现在又有了在互联网泥沼里摸爬滚打的机会。我联系了其他几家的 hr，表示已经接到了合适的 offer，也对他们表达了最诚挚的感谢，在这个时间点给了我面试的机会，让我不至于沉浸在惶恐却什么都做不了的绝望情绪里。<br>　　这几天也让我明确了一些事，好像也就两件，第一件就是去搭一个博客，我会把这篇放上去，作为第一篇文章。我突然联想到高中，当时想着要写写日记，但一直没开始，后来第一次下笔是在 2015.11.11，因为那天月考还是期中考成绩出来了，我数学没及格，很伤心，突然就有了下笔的冲动，没想到就坚持了下来，每天晚自修的最后十分钟写个几行，坚持了三年。或许这次也是个开始的契机吧。说到这个就想起来日记本还在上大寝室的架子上，不知道啥时候才能回去搬寝室。<br>　　第二件事情就是，应该再也没有过不去的坎了，至少我现在是这样觉得的。<br>　　还有很多事情要处理，先写到这吧。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　6.7，毕设答辩结束后两个小时，明略告知我要毁约，仿佛当头一棒。</summary>
    
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>修改 antd 组件默认样式的坑丨:global 关键字</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/30ad5bee.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/30ad5bee.html</id>
    <published>2022-04-25T06:43:55.000Z</published>
    <updated>2023-08-14T11:19:55.400Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>　　今天做一个 Modal 的时候，需要修改 antd Form 的一些默认样式。一开始感觉没啥问题，用 Chrome DevTools 在页面上找到 antd 组件内相应的元素，修改对应的 class 达到效果后复制到了 VSCode 里，但是，<strong>页面完全没有变化！！！！！！！</strong> <span id="more"></span></p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/3b7d499d006810f0.jpg" /></div><center style="color:#C0C0C0;"> less 代码</center><p>　　钻了 20 分钟没解决，赶紧去问 mentor, 津铭哥笑着和我说需要在外层加一个 <code>:global</code> 才能生效，试了下还真是！赶紧去补了下这个关键字的知识，在这里做一个记录。</p><h1 id="坑"><a href="#坑" class="headerlink" title="坑"></a>坑</h1><blockquote><p>　　在 React 项目中使用 scss/less，如果想让样式仅作用在某个组件，而不影响全局，一般都会把样式文件进行<strong>模块化</strong>，即打包后每个 class 名都会被自动<strong>加上一串唯一的序列号</strong>。</p></blockquote><p>举个例子：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.main</span> &#123;</span><br><span class="line">     <span class="attribute">width</span>: <span class="number">100px</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>&nbsp;<br>模块化后打包的结果会是：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.main__3D0Xe</span>&#123; <span class="attribute">width</span>: <span class="number">100px</span>; &#125;</span><br></pre></td></tr></table></figure><p>&nbsp;<br>　　而我们公司的框架自动做了样式的模块化，因此正是这串序列号导致无法正确选中 antd 组件内部元素的 class🤣。</p><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h1><p>　　如果不想要这串序列号，那么在最外层套上一层 <code>:global</code> 即可～</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/63710433b803b78f.jpg" /></div>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;　　今天做一个 Modal 的时候，需要修改 antd Form 的一些默认样式。一开始感觉没啥问题，用 Chrome DevTools 在页面上找到 antd 组件内相应的元素，修改对应的 class 达到效果后复制到了 VSCode 里，但是，&lt;strong&gt;页面完全没有变化！！！！！！！&lt;/strong&gt;</summary>
    
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="antd" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/antd/"/>
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="JavaScript" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>回调函数也能主动调用丨antd Table 组件外实现全选 &amp; 取消全选</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/d87f7e0c.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/d87f7e0c.html</id>
    <published>2022-04-23T10:54:37.000Z</published>
    <updated>2026-01-05T06:17:24.481Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>　　最近在做宝马的一个项目，需要实现 Table 的全选 &amp; 取消全选，原型图如下:<span id="more"></span></p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220423-1.jpg" /></div><center style="color:#C0C0C0;">原型图</center><p>　　然而，<a href="https://ant.design/components/table-cn?theme=dark#components-table-demo-row-selection-custom">antd 官网给的例子</a>里只有在组件中的实现方法（自定义选择项——通过 <code>rowSelection.selections</code> 配置），如下图:</p><div align=center>  <img src="https://r.kezaihui.com/client/image/yeyuxuan/blog-assets/20220423-2.jpg" /></div><center style="color:#C0C0C0;">antd 提供的功能</center><p>　　如果想要做到原型图这样在组件外实现全选 &amp; 取消全选，则需要更灵活地运用 <code>rowSelection</code> 这个配置项。</p><h1 id="Talk-is-cheap-show-me-the-code"><a href="#Talk-is-cheap-show-me-the-code" class="headerlink" title="Talk is cheap, show me the code!"></a>Talk is cheap, show me the code!</h1><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><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [selectedRowKeys, setSelectedRowKeys] = <span class="title function_">useState</span>([]);</span><br><span class="line"><span class="keyword">const</span> [selectedRows, setSelectedRows] = <span class="title function_">useState</span>([]);</span><br><span class="line"><span class="keyword">const</span> [tableData, setTableData] = <span class="title function_">useState</span>([]);</span><br><span class="line"></span><br><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">setTableData</span>(mock); <span class="comment">// 将 tableData 先设为表格内的全部数据</span></span><br><span class="line">&#125;, []); </span><br><span class="line"></span><br><span class="line"><span class="comment">// 关键点：这个函数既作为回调函数，也能手动调用～</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params">selectedRowKeys, selectedRows</span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">setSelectedRowKeys</span>([...selectedRowKeys]);</span><br><span class="line">  <span class="title function_">setSelectedRows</span>([...selectedRows]);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 点击 选择全部 的回调函数</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">selectAll</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  tableData.<span class="title function_">forEach</span>(<span class="function">(<span class="params">o, i</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 遍历，和全部的 tableData 作对比，找到没有勾选的 row, 将它的 key 值保存到 selectedRowKeys 数组</span></span><br><span class="line">    <span class="keyword">if</span> (!selectedRows.<span class="title function_">includes</span>(o)) selectedRowKeys.<span class="title function_">push</span>(tableData[i].<span class="property">account</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="comment">// 手动修改 selectedRowKeys，和 Table 组件内的 selectedRowKeys 联动</span></span><br><span class="line">  <span class="title function_">handleChange</span>(selectedRowKeys, [...tableData]); </span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 点击 取消全部 的回调函数</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">selectNone</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 手动修改 selectedRowKeys 和 selectedRows，全部置为空</span></span><br><span class="line">  <span class="title function_">handleChange</span>([], []);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> rowSelection = &#123;</span><br><span class="line">  selectedRowKeys, <span class="comment">// 受控</span></span><br><span class="line">  <span class="attr">type</span>: <span class="string">&quot;checkbox&quot;</span>,</span><br><span class="line">  <span class="attr">onChange</span>: handleChange <span class="comment">// 将 handleChange 定义在外面，从而可以手动调用</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Table 组件，略去了无关配置</span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Table</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">rowKey</span>=<span class="string">&quot;account&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">columns</span>=<span class="string">&#123;columns&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">dataSource</span>=<span class="string">&#123;mock&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">rowSelection</span>=<span class="string">&#123;rowSelection&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">/&gt;</span></span></span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>　　其实最关键的地方在于 <code>rowSelection</code> 里的 <code>onChange</code> ，通常的回调函数我们不会直接调用，而在这里，如果要实现在表格外全选，则需要手动调用这个 <code>onChange</code> 。</p><h1 id="2023-1-21-更新"><a href="#2023-1-21-更新" class="headerlink" title="2023.1.21 更新"></a>2023.1.21 更新</h1><p>　　最近做了个需求，需要在一个 form 中做三个字段间的联动:<br><img src="https://s3.bmp.ovh/imgs/2023/01/21/b2126d6f58801b38.gif" alt="搜索反馈 form 联动"></p><p>　　期间遇到了<code>form.setFieldsValue 不会触发控件 onChange</code>的问题，找到了一个<a href="https://www.cnblogs.com/xinyouhunran/p/15578373.html">解决方案</a>，和我这篇文章的思路挺像的，也是需要<code>手动触发一个回调函数</code>，在这里更新记录一下。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;　　最近在做宝马的一个项目，需要实现 Table 的全选 &amp;amp; 取消全选，原型图如下:</summary>
    
    
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="antd" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/antd/"/>
    
    <category term="前端开发" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="JavaScript" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>傻逼上海国拍丨拍到牌照后的操蛋付款经历</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/47e794c4.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/47e794c4.html</id>
    <published>2021-07-27T06:48:49.000Z</published>
    <updated>2023-08-14T11:19:55.399Z</updated>
    
    <content type="html"><![CDATA[<p>　　<br>　　24 号拍中了牌照，今天早上和我爸去工行兴冲冲地进行一个付款动作，但没想到上海国拍这个线上支付是真的傻逼，搞了三个多小时才弄好，期间更是遇到了无数个坑🤣记录一下踩坑经历，和我一样离<a href="https://www.alltobid.com/">线下付款点(黄浦区福州路 108 号)</a>比较远，懒得特地为付款跑一趟的朋友们可以参考。<span id="more"></span></p><h1 id="TL-DR"><a href="#TL-DR" class="headerlink" title="TL;DR"></a>TL;DR</h1><p>  线上支付的懒人包(工行版)：</p><ul><li>前往就近的工行网点。</li><li>手机是无法支付的，带上电脑，下载好<code>旧版的 IE 浏览器</code>。</li><li>打消直接使用银联卡支付的念头，花 70 元购买 <code>U 盾</code>。</li><li>使用 U 盾前需要下载<code>安全证书</code>，如果页面上下载安全证书的链接点击后闪退，请工作人员在工行 app 上下载<code>蓝牙安全证书</code>。</li><li>使用 U 盾前需要下载<code>安全控件</code>，如果页面上的下载弹窗无法下载，前往<a href="https://corporbank-simp.icbc.com.cn/icbc/normalbank/index.jsp">工行网银官网</a>，点击页面最下方<code>网银助手</code>的链接进行下载。</li><li>付款吧！</li></ul><h1 id="设备的坑"><a href="#设备的坑" class="headerlink" title="设备的坑"></a>设备的坑</h1><p>　　首先移动端的 app 是支付不了的，意味着手机无法操作，<code>只能通过 PC 端付款</code>；</p><p>　　然后，和上大的毕设管理网站一样，<code>只能用老 IE 登录</code>，Edge，Chrome 和火狐等现代浏览器都无法成功进入付款界面。</p><h1 id="付款界面的坑"><a href="#付款界面的坑" class="headerlink" title="付款界面的坑"></a>付款界面的坑</h1><p>　　进入付款页面后可以直接忽略页面上的付款流程， 应该是 2015 年以前的版本没更新。2015 年 2 月 15 日之后银联卡有了 <code>5 万元的单日/单笔支付限额</code>，但沪牌价格要 9w+，根本不能直接付款。(和使用哪家银行的银行卡没有关系，三个付款选项除了分期付款，其他两个都是银联，就会存在 5 万的额度上限)</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/fe8ccb306eca03a4.jpg" /></div><center style="color:#C0C0C0;">支付页面超额报错</center><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/4af49c4dc56478c3.jpg" /></div><center style="color:#C0C0C0;">论坛上关于这个报错的讨论</center><p>　　报错之后我去工行网点加了卡的额度，<code>但还是会显示超额</code>，心态第一次炸裂😑。</p><p>　　本来已经打算放弃线上支付了，工作人员让我试试 U 盾，没想到又是一条新的踩坑之路。</p><h1 id="使用-U-盾的坑"><a href="#使用-U-盾的坑" class="headerlink" title="使用 U 盾的坑"></a>使用 U 盾的坑</h1><p>　　花了 70 搞了个 U 盾，使用之前需要下载安全证书，但估计是 IE 的问题，<code>安全证书的下载链接一点击就会闪退</code>，刚开始尝试就遇到了阻塞性的 bug，心态第二次炸裂😑。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/c7dda2a901e653b2.jpg" /></div><center style="color:#C0C0C0;">U 盾需要下载安全证书</center><p>　　但又出现了转机，一个年轻小哥和我说工行 app 也能下载安全证书，经过他的操作，15 分钟后我拥有了一个<code>蓝牙安全证书</code>，从而能够继续尝试下去。</p><p>　　接下来跳出了一个弹窗，提示我安装一个安全控件，点了确定之后会出来一个另一个弹窗，此时<code>不管你点重试还是取消选项都会自动退出网银登录</code>，而每次重登都要输身份证手机号验证码，来回搞了七八次还是没有装成功，心态第三次炸裂😑。</p><p>　　我问工作人员官网能不能下这玩意，工作人员说好像可以，于是我上官网寻找，首先没有任何提示性的信息，其次在搜索框搜关键词也没有任何结果，找了大概有十几分钟<code>才发现在他妈的页面最下面</code>，这到底是哪个反人类的 UX 设计的，这么关键的东西是真的一点指引都没有啊！！</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/35bf0d7239ff4d69.jpg" /></div><center style="color:#C0C0C0;">页面没有任何提示，藏在最下面</center><p>　　这个下完之后总算成功支付了，我安慰自己，不断折腾也是人生的意义😑。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/1de2f2f521d4862b.jpg" /></div><center style="color:#C0C0C0;">终于付款成功</center><h1 id="End"><a href="#End" class="headerlink" title="End"></a>End</h1><p>　　拍牌已经够难的了，没想到付个款也能这么 disgusting🤣。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　&lt;br&gt;　　24 号拍中了牌照，今天早上和我爸去工行兴冲冲地进行一个付款动作，但没想到上海国拍这个线上支付是真的傻逼，搞了三个多小时才弄好，期间更是遇到了无数个坑🤣记录一下踩坑经历，和我一样离&lt;a href=&quot;https://www.alltobid.com/&quot;&gt;线下付款点(黄浦区福州路 108 号)&lt;/a&gt;比较远，懒得特地为付款跑一趟的朋友们可以参考。</summary>
    
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>东决 G6 和西部半决 G6丨东詹和西詹的不同命运</title>
    <link href="https://yyx-the-oracle-github-io-g1lc.vercel.app/post/6308cf93.html"/>
    <id>https://yyx-the-oracle-github-io-g1lc.vercel.app/post/6308cf93.html</id>
    <published>2019-05-11T08:16:35.000Z</published>
    <updated>2026-01-05T06:17:24.481Z</updated>
    
    <content type="html"><![CDATA[<p>　　比赛结束之后才刷到杨毅老师在 G6 赛前发的这篇微博，突然就很感慨。<span id="more"></span></p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/55e26e1943cdfb9c.jpg" /></div><p>　　我第一时间就联想到了 12 年东决 G6 ，因为这两场比赛的结果都不只是停留在那一场或是那一轮系列赛，而是会影响一个巨星二三十年后的定义。</p><p>　　况且，两场比赛的主角名字里都带詹姆斯，也算是个奇妙的联结。</p><p>　　毋庸置疑的是勒布朗詹姆斯当时的处境比现在的詹姆斯哈登还要艰难的多，在 2010 年休赛期与韦德、波什组成三巨头后，那支热火成为了真正意义上的全民公敌：除了迈阿密的球迷，其余 29 队的球迷都希望热火一败涂地，希望詹姆斯终身无冠。</p><p>　　也真的如大家所愿，11 年总决赛，那支热火被老司机带着一群暮年球员击溃。随着诺维斯基彻底摆脱了软蛋的骂名，詹姆斯成为了全联盟的笑柄。</p><p>　　无法想象那个休赛季詹姆斯是如何承受漫天的质疑和嘲讽的，很明显，下个赛季如果再不夺冠，三巨头必将解散，詹姆斯将万劫不复。</p><p>　　2011-2012 赛季，热火东部第二，东决将对阵曾经多次淘汰詹姆斯的凯尔特人队。尽管热火先下两城，但却被老辣的绿军连扳三场，濒临淘汰。</p><p>　　G6，在绿军的死亡主场，漫天嘘声。</p><p>　　结局我们都知道，詹姆斯砍下 45+15+5，还有那个著名的死亡之瞳。热火将比赛拖入抢七并击败绿军，又势如破竹地在总决赛轻取年轻的雷霆，詹姆斯拿到了生涯第一个冠军戒指，从那时开始了逐神之路。</p><div align=center>  <img src="https://s3.bmp.ovh/imgs/2023/01/02/ff936035a83250ad.jpg" /></div><center style="color:#C0C0C0;">死亡之瞳</center><p>　　今天的 G6，火箭主场，全场助威。印象深刻的是一个球迷举的牌子: “The best gift for Mother’s Day is G7.”</p><p>　　但结果我们也知道了，在考辛斯和杜兰特伤退，库里受手指影响上半场 0 分的情况下，火箭仍然被击败，可能丧失了哈登核心时代最后的夺冠希望。</p><p>　　并非要黑哈登，只是这场 G6 和哈登前几年的季后赛表现很可能会被反复拿出来衡量哈登的历史地位。而且如果，我说如果，在今天如此天时地利人和的情况下火箭击败了勇士，那大概率很多事情就不一样了。就像詹姆斯如果输掉了 12 年的那场东决，那么也不会有如今坐二望一的评价了。</p><p>　　这场比赛很可能让哈登在命运的十字路口迈向了一条不那么美好的路。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　比赛结束之后才刷到杨毅老师在 G6 赛前发的这篇微博，突然就很感慨。</summary>
    
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/%E9%9A%8F%E7%AC%94/"/>
    
    <category term="NBA" scheme="https://yyx-the-oracle-github-io-g1lc.vercel.app/tags/NBA/"/>
    
  </entry>
  
</feed>
