<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>乔木博客</title>
    <link>https://qiaomu-blog.yiliang.app</link>
    <description>记录思考，分享所学，留住当下。</description>
    <language>zh-CN</language>
    <atom:link href="https://qiaomu-blog.yiliang.app/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>我的 AI Coding 工具流：Claude Code + MCP 编排实战</title>
      <link>https://qiaomu-blog.yiliang.app/ai-codingclaude-code-mcp</link>
      <guid isPermaLink="true">https://qiaomu-blog.yiliang.app/ai-codingclaude-code-mcp</guid>
      <description>本文系统梳理了我基于 Claude Code 构建的 AI Coding 工具流：以第一性原理驱动工具路由，通过 MCP 服务器编排 context7、Codex、Gemini、Cloudflare、browsermcp 等十余个专项能力，形成&quot;直接执行 + 深度交互&quot;的协作模式。文中包含工具选择决策树、典型场景示例，以及对&quot;AI 辅助 vs AI 替代&quot;边界的思考。</description>
      <content:encoded><![CDATA[<blockquote>
<p>"工具不是目的，思维方式才是护城河。"</p>
</blockquote>
<p>过去几个月，我把 AI Coding 的使用方式从"偶尔问问 ChatGPT"彻底重构成了一套可复用的<strong>工具流体系</strong>。核心载体是 <a href="https://claude.ai/code">Claude Code</a>——Anthropic 的官方 CLI——以及围绕它编排的一组 MCP（Model Context Protocol）服务器和技能插件。</p>
<p>这篇文章不是工具评测，而是<strong>工程实践记录</strong>：我是怎么设计这套流程的，每个工具在什么场景触发，以及踩过哪些坑。</p>
<hr>
<h2>一、为什么需要"工具流"而不是"一个大模型"</h2>
<p>最初的使用模式很简单：遇到问题 → 粘贴代码 → 等回答。这在简单场景下够用，但很快暴露出几个问题：</p>
<ol>
<li><strong>上下文污染</strong>：一个 8k token 的对话塞满后，模型开始"失忆"</li>
<li><strong>能力错配</strong>：让同一个模型又查文档、又写代码、又截图验 UI，每件事都做得平庸</li>
<li><strong>无法复现</strong>：今天的对话明天消失，没有任何沉淀</li>
</ol>
<p>解法是<strong>专职化 + 编排</strong>：每个 MCP 工具只做一件事，由 Claude Code 作为"主脑"负责路由和综合。</p>
<hr>
<h2>二、工具矩阵全景</h2>
<p>按触发方式分两层：</p>
<h3>2a. Skill 插件（<code>/skill</code> 命令触发）</h3>
<p>这些是封装好的复合能力，一条命令启动一个完整工作流：</p>
<table>
<thead>
<tr>
<th>场景</th>
<th>Skill</th>
<th>典型用法</th>
</tr>
</thead>
<tbody>
<tr>
<td>需求模糊 / 前端原型 / UI 规划</td>
<td><code>/mcp-gemini</code></td>
<td>"帮我澄清这个功能的边界，然后出一个 React 原型"</td>
</tr>
<tr>
<td>代码原型 / Review / 后端二次诊断</td>
<td><code>/mcp-codex</code></td>
<td>"review 这段 Rust 的生命周期管理，给出 unified diff"</td>
</tr>
<tr>
<td>已认证页面自动化 / E2E 测试</td>
<td><code>/mcp-browser</code></td>
<td>"登录后台，截图验证部署结果"</td>
</tr>
<tr>
<td>图像生成 / 博客封面 / 图标</td>
<td><code>/nano-banana</code></td>
<td>"生成这篇文章的封面，科技感蓝紫色调"</td>
</tr>
<tr>
<td>Cloudflare Workers / Pages / KV</td>
<td><code>/cloudflare:cloudflare</code></td>
<td>"把这个 Worker 部署到 Cloudflare，配好 KV 绑定"</td>
</tr>
<tr>
<td>社交平台操作</td>
<td><code>/opencli</code></td>
<td>"把这篇博客摘要发到 Twitter"</td>
</tr>
</tbody>
</table>
<h3>2b. MCP Server（直接调用工具函数）</h3>
<p>无需 <code>/skill</code>，Claude Code 根据上下文自动路由：</p>
<table>
<thead>
<tr>
<th>场景</th>
<th>MCP Server</th>
<th>我的使用频率</th>
</tr>
</thead>
<tbody>
<tr>
<td>第三方库文档（React、Prisma、Hexo…）</td>
<td><code>context7</code></td>
<td>★★★★★ 最高频</td>
</tr>
<tr>
<td>跨文件语义搜索</td>
<td><code>augment-context-engine</code></td>
<td>★★★★</td>
</tr>
<tr>
<td>网页搜索 / 全文抓取</td>
<td><code>exa</code></td>
<td>★★★★</td>
</tr>
<tr>
<td>Cloudflare API 执行</td>
<td><code>cloudflare-api</code></td>
<td>★★★</td>
</tr>
<tr>
<td>Workers 构建 CI/CD 日志</td>
<td><code>cloudflare-builds</code></td>
<td>★★★</td>
</tr>
<tr>
<td>Workers 日志 / 可观测性</td>
<td><code>cloudflare-observability</code></td>
<td>★★</td>
</tr>
<tr>
<td>Gmail / Google Calendar</td>
<td><code>claude_ai_Gmail</code></td>
<td>★★</td>
</tr>
<tr>
<td>无登态页面截图 / DOM 操作</td>
<td><code>playwright</code></td>
<td>★★★</td>
</tr>
</tbody>
</table>
<hr>
<h2>三、核心决策树：代码探索怎么走</h2>
<p>这是我踩坑最多的地方——一开始总是无脑 <code>grep</code>，结果把 8k token 的上下文全填满了无关代码。</p>
<p>现在的决策逻辑：</p>
<pre><code>有代码问题？
  ├─ 概念/行为/架构（"这段逻辑影响哪里？"）
  │    └─→ augment-context-engine（语义搜索）
  ├─ 精确符号名但不知位置（"找 handleSubmit 函数"）
  │    └─→ augment-context-engine 先 → 无结果再 Grep
  ├─ 已知精确文件路径
  │    └─→ Read（必须带 offset+limit，禁止无参数读大文件）
  └─ 只需文件列表/存在性验证
       └─→ Glob
</code></pre>
<p><strong>硬性规则</strong>：未经语义搜索定位就多轮 Grep，等于让 AI 做全文扫描——这是 token 黑洞。</p>
<hr>
<h2>四、两个真实场景复盘</h2>
<h3>场景 A：修复博客 CI/CD 流水线</h3>
<p>这个 blog 本身就是一个案例。几周前 CI 一直挂，报错模糊。我的处理流程：</p>
<ol>
<li><strong><code>cloudflare-builds</code></strong> 拉取最新构建日志，定位到 <code>npm ci</code> 因 lockfile 不同步失败</li>
<li><strong><code>Read</code></strong> 读 <code>pages.yml</code>，发现四个问题：Action 版本、缓存 key、lockfile 缺失、构建顺序错误</li>
<li><strong><code>Edit</code></strong> 精确替换相关行（没有重写整个文件）</li>
<li><strong><code>Bash</code></strong> 执行 <code>npm install</code> 重新生成 lockfile，git commit 推送</li>
</ol>
<p>全程没有离开 Claude Code 终端，4 步搞定，历史记录沉淀在 claude-mem 的跨会话记忆里。</p>
<h3>场景 B：第三方库 API 用法确认</h3>
<p>写 Hexo 插件时不确定 <code>hexo.extend.generator</code> 的签名，以往我会去 Google 找文档，经常找到过时的版本。</p>
<p>现在的流程：</p>
<pre><code>context7.resolve-library-id("hexo")
→ context7.query-docs("generator API signature return type")
→ 得到当前版本的精确 API 文档
→ 直接写代码，零猜测
</code></pre>
<p>context7 的核心价值：<strong>把"凭记忆猜"变成"凭文档写"</strong>，这在依赖快速迭代的 JS 生态里价值极大。</p>
<hr>
<h2>五、我遵守的几条硬规则</h2>
<p>这些规则是反复踩坑后沉淀的，写进了我的全局 CLAUDE.md：</p>
<p><strong>1. 工具优先，禁止先解释后调用</strong>
有合适工具就立刻调，不要先说"我来帮你……"再调。解释是事后的，不是事前的。</p>
<p><strong>2. 并行调用所有无依赖工具</strong>
读文件和搜文档可以同时做。顺序调用是在浪费时间。</p>
<p><strong>3. 外部模型输出仅作逻辑参考</strong>
Codex、Gemini 给的代码不能直接粘贴——我要理解后自己重构，保证风格一致、无冗余。</p>
<p><strong>4. 改动范围最小化</strong>
Bug fix 不顺手重构周边代码。加功能不加"以备将来"的配置项。这条规则救了我很多次——AI 特别容易过度工程化。</p>
<p><strong>5. 第三方库调用必须 context7 驱动</strong>
记忆会骗人，文档不会。</p>
<hr>
<h2>六、claude-mem：跨会话的"工程记忆"</h2>
<p>Claude Code 的对话是无状态的——关掉窗口就消失。我用 <strong>claude-mem</strong> 插件解决这个问题：</p>
<ul>
<li>每次解决非显而易见的问题后，把"是什么 + 为什么 + 怎么用"写入记忆</li>
<li>下次开新会话，先 <code>/mem-search</code> 检索相关历史</li>
<li>避免重复踩同一个坑</li>
</ul>
<p>这篇文章对应的记忆条目里就有本博客 CI/CD 历史修复记录的完整上下文——包括当时的错误日志和每个 fix 的 rationale。</p>
<hr>
<h2>七、对"AI 替代编程"的边界判断</h2>
<p>我的结论是：<strong>AI 擅长执行，人负责判断</strong>。</p>
<p>具体来说：</p>
<ul>
<li>✅ AI 做：文档查找、代码生成初稿、diff 格式化、重复性重构、日志分析</li>
<li>✅ 人做：架构决策、需求边界划定、安全审查、"这个抽象是否必要"的判断</li>
<li>⚠️ 危险区：让 AI 做架构决策，然后自己不 review 就合并</li>
</ul>
<p>最容易出问题的是<strong>过度信任 AI 的"以防万一"逻辑</strong>——它会给你加 fallback、加 retry、加 feature flag，每一个单独看都"合理"，合在一起就是过度工程。规则是：三行代码能解决的，不要五个抽象。</p>
<hr>
<h2>结语</h2>
<p>这套工具流不是终态，它还在随着 MCP 生态的扩展持续演化。但核心原则是稳定的：</p>
<blockquote>
<p><strong>第一性原理 > 经验主义。工具是手段，清晰的思维是护城河。</strong></p>
</blockquote>
<p>如果你也在用 Claude Code，欢迎交流你的工具路由策略——尤其是那些"踩坑之后才沉淀的规则"，那才是真正有价值的东西。</p>
]]></content:encoded>
      <category>技术</category>
      <pubDate>Mon, 13 Apr 2026 15:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Rust设计思想引申到JavaScript程序设计</title>
      <link>https://qiaomu-blog.yiliang.app/rustjavascript</link>
      <guid isPermaLink="true">https://qiaomu-blog.yiliang.app/rustjavascript</guid>
      <description>本文探讨了如何将Rust语言的设计理念融入JavaScript开发，以提升代码的可靠性与性能。核心借鉴点包括：通过明确资源管理借鉴所有权思想；利用不可变数据结构（如Immutable.js）减少状态复杂性；引入TypeScript增强类型安全性；以及通过模式匹配优化控制流。此外，文章指出JavaScript可借助ESLint、Web Workers等工具实现类似Rust的安全性、高效内存管理与并发处理。通过采纳这些设计哲学，开发者能够编写出更健壮、易于维护的现代JavaScript应用。</description>
      <content:encoded><![CDATA[<p>Rust是一门以安全性和性能著称的系统编程语言，其设计思想对JavaScript程序设计有着深远的启示。本文将探讨如何将Rust的设计理念应用到JavaScript开发中，以提升代码的可靠性和效率。</p>
<h2>Rust的设计思想</h2>
<h3>1. 所有权与借用 (Ownership and Borrowing)</h3>
<p>Rust通过所有权系统管理内存，避免了常见的内存泄漏和数据竞争问题。</p>
<h4>在JavaScript中的应用</h4>
<p>虽然JavaScript是垃圾回收语言，但我们仍可以通过明确的资源管理和避免全局状态来借鉴Rust的所有权思想。</p>
<pre><code class="language-javascript">function createResource() {
  let resource = { data: 'important data' };
  return {
    useResource: () => console.log(resource.data),
    dispose: () => { resource = null; }
  };
}

const res = createResource();
res.useResource();
res.dispose();
</code></pre>
<h3>2. 不可变性 (Immutability)</h3>
<p>Rust鼓励使用不可变数据，减少了状态变化带来的复杂性。</p>
<blockquote>
<p>Shared mutable state is the root of all evil（共享的可变状态是万恶之源）
-- Pete Hunt</p>
</blockquote>
<h4>在JavaScript中的应用</h4>
<p>在JavaScript中，我们可以使用<code>const</code>声明和不可变数据结构来实现类似的效果。</p>
<pre><code class="language-javascript">const data = Object.freeze({ name: 'John', age: 30 });
data.age = 31; // TypeError: Cannot assign to read only property 'age'
</code></pre>
<p>此外，我们还可以使用库如Immutable.js来帮助管理不可变数据。(在React前端应用中尤为重要)</p>
<pre><code class="language-javascript">const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);

console.log(map1.get('b')); // 2
console.log(map2.get('b')); // 50
</code></pre>
<p>Immutable 实现的原理是 Persistent Data Structure（持久化数据结构）:</p>
<p>用一种数据结构来保存数据
当数据被修改时，会返回一个对象，但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费
也就是使用旧数据创建新数据时，要保证旧数据同时可用且不变，同时为了避免 deepCopy把所有节点都复制一遍带来的性能损耗，Immutable 使用了 Structural Sharing（结构共享）</p>
<p>如果对象树中一个节点发生变化，只修改这个节点和受它影响的父节点，其它节点则进行共享</p>
<p>如下图所示：</p>
<p><img src="/api/images/image/2026/04/dc41f87b18e9770d-14-2b4c801a7b40eefcd4ee6767fb984fdf-df0457.gif" alt=""></p>
<h3>3. 类型系统 (Type System)</h3>
<p>Rust的强类型系统在编译时捕获错误，提升了代码的安全性。</p>
<h4>在JavaScript中的应用</h4>
<p>虽然JavaScript是动态类型语言，但我们可以使用TypeScript来引入静态类型检查。</p>
<pre><code class="language-typescript">function add(a: number, b: number): number {
  return a + b;
}

console.log(add(2, 3)); // 5
console.log(add('2', '3')); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
</code></pre>
<p>使用TypeScript可以在开发阶段捕获许多潜在的错误，提升代码的可靠性。</p>
<h2>Rust特点</h2>
<h3>1. 安全性</h3>
<p>Rust通过所有权和借用检查在编译时捕获内存安全问题，JavaScript可以通过严格的编码规范和工具（如ESLint）来提升代码安全性。</p>
<pre><code class="language-javascript">// ESLint配置示例
module.exports = {
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": "eslint:recommended",
  "parserOptions": {
    "ecmaVersion": 12,
    "sourceType": "module"
  },
  "rules": {
    "no-unused-vars": "warn",
    "no-console": "off"
  }
};
</code></pre>
<h3>2. 性能</h3>
<p>Rust的零成本抽象和高效内存管理使其性能卓越。JavaScript可以通过避免不必要的对象创建和使用高效的数据结构来提升性能。</p>
<pre><code class="language-js">// 使用高效的数据结构
const arr = new Array(1000000).fill(0);
console.time('Array');
for (let i = 0; i &#x3C; arr.length; i++) {
  arr[i] = i;
}
console.timeEnd('Array'); // Array: 3.83203125 ms

const map = new Map();
console.time('Map');
for (let i = 0; i &#x3C; 1000000; i++) {
  map.set(i, i);
}
console.timeEnd('Map'); // Map: 65.27001953125 ms
</code></pre>
<h3>3. 并发性</h3>
<p>Rust的所有权系统天然支持安全的并发编程。JavaScript可以通过Web Workers和异步编程模型来实现高效并发。</p>
<pre><code class="language-js">// 使用Web Workers
const worker = new Worker('worker.js');
worker.postMessage('start');

worker.onmessage = function(event) {
  console.log('Worker said: ', event.data);
};

// worker.js
self.onmessage = function(event) {
  if (event.data === 'start') {
    self.postMessage('Hello from worker');
  }
};
</code></pre>
<p>通过这种方式，我们可以在不阻塞主线程的情况下执行耗时任务。</p>
<h3>模式匹配 (Pattern Matching)</h3>
<p>Rust中的模式匹配通过<code>match</code>表达式提供了一种强大且灵活的控制流机制。并且Rust 编译器清晰地知道 match 中有哪些分支没有被覆盖，这种行为能强制我们处理所有的可能性，有效避免传说中<a href="https://linux.cn/article-6503-1.html">价值十亿美金的 null 陷阱</a>。</p>
<h4>在JavaScript中的应用</h4>
<p>虽然JavaScript没有原生的模式匹配语法，但我们可以使用<code>switch</code>语句或第三方库（如<code>match</code>库）来实现类似的功能。</p>
<pre><code class="language-javascript">// 使用switch语句实现模式匹配
function match(value) {
  switch (value) {
    case 'a':
      return 'Matched A';
    case 'b':
      return 'Matched B';
    default:
      return 'No Match';
  }
}

console.log(match('a')); // Matched A
console.log(match('c')); // No Match
</code></pre>
<p>此外，我们还可以使用第三方库来实现更强大的模式匹配功能。例如，match库提供了类似Rust的模式匹配语法。</p>
<pre><code class="language-js">const { match, when, otherwise } = require('match');

const value = 'a';

const result = match(value)(
  when('a', () => 'Matched A'),
  when('b', () => 'Matched B'),
  otherwise(() => 'No Match')
);

console.log(result); // Matched A
</code></pre>
<p>Rust编译器的检查也可以通过引入工具函数实现开发时通过Chrome DevTool进行开发时debugger</p>
<pre><code class="language-js">const DCHECK_ALWAYS_ON = false;

const NOOP = () => {};

export const DCHECK =
  __DEV__ || DCHECK_ALWAYS_ON
    ? (condition, msg = 'DCHECK failed') => {
        if (!condition) {
          console.warn(new Error(msg));
          debugger;
        }
      }
    : NOOP;

export const UNREACHABLE = (msg = 'UNREACHABLE') => DCHECK(false, msg);

</code></pre>
<h2>结论</h2>
<p>Rust的设计思想为JavaScript开发提供了宝贵的借鉴。通过引入所有权管理、不可变数据和类型检查等理念，我们可以编写出更安全、高效和可维护的JavaScript代码。</p>
<h2>参考阅读</h2>
<ul>
<li><a href="https://www.rust-lang.org/learn">Rust官方文档</a></li>
<li><a href="https://course.rs/about-book.html">Rust语言圣经</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/20295971">Immutable 详解及 React 中实践</a></li>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API">Web Workers API</a></li>
</ul>
]]></content:encoded>
      <category>技术</category>
      <pubDate>Mon, 21 Oct 2024 22:30:00 GMT</pubDate>
    </item>
    <item>
      <title>excel公式引擎方案设计</title>
      <link>https://qiaomu-blog.yiliang.app/excel</link>
      <guid isPermaLink="true">https://qiaomu-blog.yiliang.app/excel</guid>
      <description>本文探讨了前端电子表格计算引擎的实现方案，旨在解决海量数据处理中的逻辑运算与统计需求。核心挑战在于构建单元格间的复杂依赖关系以实现自动更新，以及对公式字符串进行词法解析。文章详细介绍了公式的组成要素（函数、常量、引用及运算符），并重点分析了基于表达式模板与引用位置组合的存储机制。此外，文中还阐述了相对引用、绝对引用及混合引用的实现原理，为设计高效、智能的表格计算引擎提供了理论基础与架构思路。</description>
      <content:encoded><![CDATA[<h2>一、背景</h2>
<p>身处信息时代之中，我们最能明显感受到的一点就是密集数据大量爆发，人们积累的数据也越来越多。这些庞杂的数据出现在一起，传统使用的很多数据记录、查询、汇总工具并不能满足人们的需求。更有效的将这些大量数据处理，让计算机听懂人类需要的数据效果，从而形成更加自动化、智能的数据处理方式。</p>
<p>为了处理这些海量数据，出现了各种大数据引擎、搜索引擎、计算引擎、3D引擎等，用以更好解决数据庞杂带来人工无法处理的问题。而作为其中比较常用的就是Excel的计算公式引擎，本文主要是讲解计算引擎的前端实现方案。</p>
<h2>二、核心问题</h2>
<ul>
<li>核心需求点：满足用户对表格数据的逻辑比较、逻辑运算和统计需求。</li>
<li>核心问题：1.建立表格各类区域和公式之间的复杂依赖关系并得到合理的计算更新顺序。2.公式字符串的词法解析生成结构化数据</li>
</ul>
<h2>三、功能分析</h2>
<p>对于核心问题第一点来说，可以在以下两个资料中，学习到一些思想方法。</p>
<ul>
<li><a href="https://gcdn.grapecity.com.cn/lesson-161.html">前端电子表格的计算引擎解密 - 前端电子表格的计算引擎解密</a></li>
<li><a href="https://leetcode.cn/problems/design-excel-sum-formula/">[Leetcode] 631. 设计 Excel 求和公式</a></li>
</ul>
<p>核心问题第二点：
如图，A8单元格的字段中存在一个公式SUM(1,5.512,B1,A1)，formula记录了两个信息，其中的10是公式表达式的索引，22、23是公式表达式中包含的引用位置的索引：</p>
<p><img src="/api/images/image/2026/04/3b80958444a70454-15-64bdf443fa67d94393eca8cfcdc36235-0a93aa.webp" alt="">
在元数据部分可以看到对应的数据：</p>
<p><img src="/api/images/image/2026/04/fa517a702c146fec-15-fef5e455970bbc260f9eb6145f9cdac4-dccbd6.webp" alt="">
exprs中的是表达式的模板，如果存在两个公式表达式只有引用位置不同，那它们共用一个模板：
<img src="/api/images/image/2026/04/873f7c178fd3e94d-15-d3b2f09d69d552502d71f51da9c93f34-6735f7.webp" alt=""></p>
<p>refs中的即是引用位置，通过相对位置进行表示：（<a href="###%E5%85%AC%E5%BC%8F%E4%B8%AD%E4%BD%BF%E7%94%A8%E5%BC%95%E7%94%A8">位置引用和公式字符串的转化关系参考这里</a>）
<img src="/api/images/image/2026/04/74bc9b48a03ef4bd-15-452ea323cd4806808f5b5681ca820f9b-11d8cf.webp" alt=""></p>
<p>由此可见钉钉表格完整的公式表达式由表达式模板(expres)+引用位置(refs)组合得出。
通过单元格上formula记录的索引，找到对应的表达式模板和引用位置，就能够完整表达整个公式。
公式之间的依赖关系并没有通过快照存储，需要在表格初始化时进行构建。</p>
<h2>四、概要设计</h2>
<h3>公式的基本原理</h3>
<p>在工作表中可以使用常量和算术运算符创建简单的公式。</p>
<p>公式以输入“＝”开始。
复杂一些的公式可能包含函数和常量。</p>
<blockquote>
<p>函数：函数是预先编写的公式，可以对一个或多个值执行运算，并返回一个或多个值。函数可以简化和缩短工作表中的公式，尤其在用公式执行很长或复杂的计算时。</p>
</blockquote>
<blockquote>
<p>常量：不进行计算的值，因此也不会发生变化。</p>
</blockquote>
<p>基本功能：</p>
<ul>
<li>可以进行＋、－、、四则运算等计算</li>
<li>可以引用其他单元格中的数据。</li>
<li>可使用文本字符串，或与数据相结合。</li>
<li>可运用>、&#x3C;之类的比较运算符比较单元格内的数据。</li>
<li>不仅可以用于公式的计算，还可以运用于其他情况中。</li>
</ul>
<p>位置引用：</p>
<ul>
<li>A1 相对引用</li>
<li>$A1 绝对引用列</li>
<li>A$1 绝对引用行</li>
<li>$A$1 绝对引用行和列</li>
</ul>
<p>下面将介绍公式功能的基本实现原理。</p>
<h3>公式的组成部分</h3>
<p><img src="/api/images/image/2026/04/14fcaabab28e46fa-15-877e0a6a1ff4552e09f5a87d2e8b9a9a-15b8c1.webp" alt=""></p>
<ol>
<li>函数：PI() 函数返回 pi 值：3.142...</li>
<li>引用：A2 返回单元格 A2 中的值。</li>
<li>常量：直接输入到公式中的数字或文本值，例如 2。</li>
<li>运算符：^（脱字号）运算符表示数字的乘方，而 *（星号）运算符表示数字的乘积。</li>
</ol>
<h3>公式中使用常量</h3>
<p>常量是一个不是通过计算得出的值；它始终保持相同。
例如，日期 10/9/2008、数字 210 以及文本“季度收入”都是常量。
表达式或从表达式得到的值不是常量。
如果在公式中使用常量而不是对单元格的引用（例如 =30+70+110），则仅在修改公式时结果才会变化。
通常，最好在各单元格中放置常量（必要时可轻松更改），然后在公式中引用这些单元格。</p>
<h3>公式中使用引用</h3>
<p>引用的作用在于标识工作表上的单元格或单元格区域，并告知 程序 在何处查找要在公式中使用的值或数据。
你可以使用引用在一个公式中使用工作表不同部分中包含的数据，或者在多个公式中使用同一个单元格的值。
还可以引用同一个工作簿中其他工作表上的单元格和其他工作簿中的数据。
引用其他工作簿中的单元格被称为链接或外部引用。</p>
<ul>
<li>
<p>A1 引用样式
默认情况下，程序 使用 A1 引用样式，此样式引用字母标识列（从 A 到 XFD，共 16,384 列）以及数字标识行（从 1 到 1,048,576）。 这些字母和数字被称为行号和列标。</p>
<p>要引用某个单元格，请输入列标，后跟行号。
例如，B2 引用列 B 和行 2 交叉处的单元格。</p>
<table>
<thead>
<tr>
<th>若要引用</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>A 和行 10 交叉处的单元格</td>
<td>A10</td>
</tr>
<tr>
<td>在列 A 和行 10 到行 20 之间的单元格区域</td>
<td>A10:A20</td>
</tr>
<tr>
<td>在行 15 和列 B 到列 E 之间的单元格区域</td>
<td>B15:E15</td>
</tr>
<tr>
<td>行 5 中的全部单元格</td>
<td>5:5</td>
</tr>
<tr>
<td>行 5 到行 10 之间的全部单元格</td>
<td>5:10</td>
</tr>
<tr>
<td>列 H 中的全部单元格</td>
<td>H:H</td>
</tr>
<tr>
<td>列 H 到列 J 之间的全部单元格</td>
<td>H:J</td>
</tr>
<tr>
<td>列 A 到列 E 和行 10 到行 20 之间的单元格区域</td>
<td>A10:E20</td>
</tr>
</tbody>
</table>
</li>
<li>
<p>引用同一工作簿中另一个工作表上的单元格或单元格区域</p>
<p>下例中，AVERAGE 函数将计算同一个工作簿中名为 Marketing 的工作表的 B1:B10 区域内的平均值。</p>
<p><img src="/api/images/image/2026/04/a40704c98a87ccfb-15-f1875f81cb589c1cde34dafb19e11c0c-4f12be.webp" alt=""></p>
<p>1、对名为 Marketing 的工作表的引用
2、引用 B1 到 B10 的单元格区域
3、感叹号 (！) 将工作表引用与单元格区域引用分开</p>
</li>
<li>
<p>绝对引用、相对引用和混合引用之间的区别</p>
<p><strong>相对引用</strong> 公式中的相对单元格引用（如 A1）是基于包含公式和单元格引用的单元格的相对位置。 如果公式所在单元格的位置改变，引用也随之改变。 如果多行或多列地复制或填充公式，引用会自动调整。 默认情况下，新公式使用相对引用。 例如，如果将单元格 B2 中的相对引用复制或填充到单元格 B3，将自动从 =A1 调整到 =A2。</p>
<p>复制的公式具有相对引用：</p>
<p><img src="/api/images/image/2026/04/ede1619527b8a909-15-11decd03cf733e245e56a61be6fa6862-390f7e.webp" alt=""></p>
<p><strong>绝对引用</strong> 公式中的绝对单元格引用（如 $A$1）总是在特定位置引用单元格。 如果公式所在单元格的位置改变，绝对引用将保持不变。 如果多行或多列地复制或填充公式，绝对引用将不作调整。 默认情况下，新公式使用相对引用，因此您可能需要将它们转换为绝对引用。 例如，如果将单元格 B2 中的绝对引用复制或填充到单元格 B3，则该绝对引用在两个单元格中一样，都是 =$A$1。</p>
<p>复制的公式具有绝对引用：</p>
<p><img src="/api/images/image/2026/04/00eb9e513d1431e5-15-1daefc8d644a37bf90a9e05dbb86932b-a1ff34.webp" alt=""></p>
<p><strong>混合引用</strong> 混合引用具有绝对列和相对行或绝对行和相对列。 绝对引用列采用 $A1、$B1 等形式。 绝对引用行采用 A$1、B$1 等形式。 如果公式所在单元格的位置改变，则相对引用将改变，而绝对引用将不变。 如果多行或多列地复制或填充公式，相对引用将自动调整，而绝对引用将不作调整。 例如，如果将一个混合引用从单元格 A2 复制到 B3，它将从 =A$1 调整到 =B$1。</p>
<p>复制的公式具有混合引用：</p>
<p><img src="/api/images/image/2026/04/8704714c424cb689-15-7bf162d2ba059eda7eefea9f327bbe6d-c76b16.webp" alt=""></p>
</li>
</ul>
<h3>公式的词法分析</h3>
<p><img src="/api/images/image/2026/04/ebb58eb2841adb91-16-06e857276594aedc7fe2f723d8613d2a-e3c3f4.webp" alt="">
<img src="/api/images/image/2026/04/f96d66eeef25f535-16-a3be05ff2b129bba28e2151f03502986-085ca7.webp" alt="">
<img src="/api/images/image/2026/04/f1d4e75bece683a8-16-edbfab0248564573d6a4b0168bd17f83-e02d78.webp" alt=""></p>
<h3>语法分析</h3>
<p>公式的语法分析使用开源库<a href="https://gerhobbelt.github.io/jison/docs/">jison</a>库，我们需要编写定义相关的语法规则，使用命令生成解析器。Jison 解析器会根据定义的语法规则对输入进行解析，并构建一个语法树。</p>
<p>首先，你需要使用 Jison 定义函数表达式的语法规则。这可以通过编写一个称为"Jison 文法"的规则文件来完成。Jison 文法使用类似于 BNF（巴科斯范式）的语法来描述语法规则。例如，下面是一个简单的 Jison 文法示例，用于解析简单的数学函数表达式：</p>
<pre><code class="language-json">%lex
%%
"+"             return '+';
"-"             return '-';
"*"             return '*';
"/"             return '/';
"("             return '(';
")"             return ')';
[0-9]+          return 'NUMBER';
[ \t\n]+        /* skip whitespace */
.               return 'INVALID';
/lex

%start expression

%%

expression
    : expression "+" expression   { $$ = $1 + $3; }
    | expression "-" expression   { $$ = $1 - $3; }
    | expression "*" expression   { $$ = $1 * $3; }
    | expression "/" expression   { $$ = $1 / $3; }
    | "(" expression ")"          { $$ = $2; }
    | NUMBER                      { $$ = Number($1); }
    ;

</code></pre>
<h3>语法树</h3>
<p>语法树是一个表示函数表达式结构的树状数据结构。每个节点代表一个语法规则的实例，例如函数调用、运算符、变量等。</p>
<p>例如：一个简单的数学表达式：2 + 3 * (4 - 1)可以解析成</p>
<pre><code class="language-js">{
  type: 'BinaryExpression',
  operator: '+',
  left: {
    type: 'NumericLiteral',
    value: 2
  },
  right: {
    type: 'BinaryExpression',
    operator: '*',
    left: {
      type: 'NumericLiteral',
      value: 3
    },
    right: {
      type: 'BinaryExpression',
      operator: '-',
      left: {
        type: 'NumericLiteral',
        value: 4
      },
      right: {
        type: 'NumericLiteral',
        value: 1
      }
    }
  }
}

</code></pre>
<h3>计算表达式</h3>
<p>计算表达式使用了开源库<a href="https://github.com/handsontable/formula.js">formula.js</a>。
该开源库能够支持三百多种函数的运算，但不支持 excel 中数组公式的计算，例如"=A:C+1"，需要对其 IF 函数、加、减、乘、除等运算规则进行修改，使其支持参与运算的因子是数组的情况。</p>
<h3>公式的相互依赖</h3>
<p>公式可以存在单元格引用，而被引用的单元格值可能也是由公式计算得到的，例如：
<img src="/api/images/image/2026/04/8bb94801713e2863-16-a6bff834f810dbf5f8548003fa403ef4-3e7289.webp" alt="">
C3 上的公式是=SUM(C1:D1)，F3 上的公式是=SUM(F1:G1)，E6 上的公式是=SUM(C3,F3).</p>
<p>如果我们将 C1 的值更新，依赖 C1 的 C3、依赖 C3 的 E6 需要重新计算并更新公式结果，并且根据依赖关系，必须先更新 C3 再更新 E6。</p>
<h2>五、详细设计</h2>
<hr>
<p>假设在 A1 单元格位置键入以下公式</p>
<pre><code>=SUM(B1:C1, B2:C2)
</code></pre>
<p>首先我们在单元格数据里记录公式信息/公式 ID（此处举例为“A”）</p>
<pre><code class="language-json">// 单元格数据
{
  "type": 9,
  // ...
  "formula": {
    "id": "A"
  }
}
</code></pre>
<p>另外，我们还需要在文档数据里更新公式信息，以及对应的引用区域。格式如下</p>
<pre><code class="language-json">"formulas": {
  "A": {
    // 引用范围, 仅记录refs中引用对象的id
    "refs": {
      "R1": "x",
      "R2": "y"
    },
    "expr": "SUM([R1, R2])"
  }
},
</code></pre>
<pre><code class="language-json">"refs": [
  {
    "id": "x",
    // sheetId，如果跨表，这里就是个链接
    "from": "GsuvR1",
    // range类型，下文讨论
    "type": 16,
    // range依旧保持为[4]int类型
    "range": [
      1,
      0,
      2,
      1
    ]
  },
  {
    "id": "y",
    "from": "GsuvR1",
    "type": 0,
    "range": [
      1,
      1,
      2,
      1
    ]
  }
]
</code></pre>
<h3>公式的还原方法</h3>
<p>在实际的应用程序中，我们还需要还原公式用以展示和编辑。</p>
<p>由于存在 相对引用 和 绝对引用，以及 混合引用 的场景，这使得引用位置的还原变得复杂。
我们通过 type 字段记录相关的位置信息，在还原时配合 ref 结构中的 type 字段，可以准确还原位置引用。</p>
<table aria-label="" class="banded flipColors">
  <thead>
    <tr>
      <td>
        <p>
          <b class="ocpLegacyBold">对于正在复制的公式：</b>
        </p>
      </td>
      <td>
        <p>
          <b class="ocpLegacyBold">如果引用是：A1=</b>
        </p>
      </td>
      <td>
        <p>
          <b class="ocpLegacyBold">它会更改为：C3=</b>
        </p>
      </td>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <p>
            <img src="https://support.content.office.net/zh-cn/media/73d5a617-c594-4d28-a613-34df7d53dffc.gif" loading="lazy" alt="正从 A1 被复制到向下和向右移两个单元格的公式。">
        </p>
      </td>
      <td>
        <p>$A$1（绝对列和绝对行）</p>
      </td>
      <td>
        <p>$A$1（引用是绝对的）</p>
      </td>
    </tr>
    <tr>
      <td></td>
      <td>
        <p>A$1（相对列和绝对行）</p>
      </td>
      <td>
        <p>C$1（引用是混合型）</p>
      </td>
    </tr>
    <tr>
      <td></td>
      <td>
        <p>$A1（绝对列和相对行）</p>
      </td>
      <td>
        <p>$A3（引用是混合型）</p>
      </td>
    </tr>
    <tr>
      <td></td>
      <td>
        <p>A1（相对列和相对行）</p>
      </td>
      <td>
        <p>C3（引用是相对的）</p>
      </td>
    </tr>
  </tbody>
</table>
<blockquote>
<p>钉钉：使用 type 的二进制数字表示所有不同位置的$符号</p>
</blockquote>
<table dir="auto" data-sourcepos="63:1-93:23">
  <thead>
    <tr data-sourcepos="63:1-63:26">
      <th data-sourcepos="63:2-63:10">TYPE</th>
      <th data-sourcepos="63:12-63:25">含义</th>
    </tr>
  </thead>
  <tbody>
    <tr data-sourcepos="65:1-65:32">
      <td data-sourcepos="65:2-65:8">-1</td>
      <td data-sourcepos="65:10-65:31">被删除的引用</td>
    </tr>
    <tr data-sourcepos="66:1-66:20">
      <td data-sourcepos="66:2-66:8">0(00)</td>
      <td data-sourcepos="66:10-66:19"><code>A1</code></td>
    </tr>
    <tr data-sourcepos="67:1-67:20">
      <td data-sourcepos="67:2-67:8">1(01)</td>
      <td data-sourcepos="67:10-67:19"><code>A$1</code></td>
    </tr>
    <tr data-sourcepos="68:1-68:22">
      <td data-sourcepos="68:2-68:8">2(10)</td>
      <td data-sourcepos="68:10-68:21"><code>$A1</code></td>
    </tr>
    <tr data-sourcepos="69:1-69:22">
      <td data-sourcepos="69:2-69:8">3(11)</td>
      <td data-sourcepos="69:10-69:21"><code>$A$1</code></td>
    </tr>
    <tr data-sourcepos="70:1-70:22">
      <td data-sourcepos="70:2-70:8">4(100)</td>
      <td data-sourcepos="70:10-70:21"><code>1:1</code></td>
    </tr>
    <tr data-sourcepos="71:1-71:22">
      <td data-sourcepos="71:2-71:8">5(101)</td>
      <td data-sourcepos="71:10-71:21"><code>1:$1</code></td>
    </tr>
    <tr data-sourcepos="72:1-72:22">
      <td data-sourcepos="72:2-72:8">6(110)</td>
      <td data-sourcepos="72:10-72:21"><code>$1:1</code></td>
    </tr>
    <tr data-sourcepos="73:1-73:22">
      <td data-sourcepos="73:2-73:8">7(111)</td>
      <td data-sourcepos="73:10-73:21"><code>$1:$1</code></td>
    </tr>
    <tr data-sourcepos="74:1-74:20">
      <td data-sourcepos="74:2-74:8">8(1000)</td>
      <td data-sourcepos="74:10-74:19"><code>C:D</code></td>
    </tr>
    <tr data-sourcepos="75:1-75:20">
      <td data-sourcepos="75:2-75:8">9(1001)</td>
      <td data-sourcepos="75:10-75:19"><code>C:$D</code></td>
    </tr>
    <tr data-sourcepos="76:1-76:20">
      <td data-sourcepos="76:2-76:8">10(1010)</td>
      <td data-sourcepos="76:10-76:19"><code>$C:D</code></td>
    </tr>
    <tr data-sourcepos="77:1-77:20">
      <td data-sourcepos="77:2-77:8">11(1011)</td>
      <td data-sourcepos="77:10-77:19"><code>$C:$D</code></td>
    </tr>
    <tr data-sourcepos="78:1-78:20">
      <td data-sourcepos="78:2-78:8">16(10000)</td>
      <td data-sourcepos="78:10-78:19"><code>A1:B1</code></td>
    </tr>
    <tr data-sourcepos="79:1-79:20">
      <td data-sourcepos="79:2-79:8">17(10001)</td>
      <td data-sourcepos="79:10-79:19"><code>A1:B$1</code></td>
    </tr>
    <tr data-sourcepos="80:1-80:20">
      <td data-sourcepos="80:2-80:8">18(10010)</td>
      <td data-sourcepos="80:10-80:19"><code>A1:$B1</code></td>
    </tr>
    <tr data-sourcepos="81:1-81:21">
      <td data-sourcepos="81:2-81:8">19(10111)</td>
      <td data-sourcepos="81:10-81:20"><code>A1:$B$1</code></td>
    </tr>
    <tr data-sourcepos="82:1-82:20">
      <td data-sourcepos="82:2-82:8">20(10100)</td>
      <td data-sourcepos="82:10-82:19"><code>A$1:B1</code></td>
    </tr>
    <tr data-sourcepos="83:1-83:21">
      <td data-sourcepos="83:2-83:8">21(10101)</td>
      <td data-sourcepos="83:10-83:20"><code>A$1:B$1</code></td>
    </tr>
    <tr data-sourcepos="84:1-84:21">
      <td data-sourcepos="84:2-84:8">22(10110)</td>
      <td data-sourcepos="84:10-84:20"><code>A$1:$B1</code></td>
    </tr>
    <tr data-sourcepos="85:1-85:22">
      <td data-sourcepos="85:2-85:8">23(10111)</td>
      <td data-sourcepos="85:10-85:21"><code>A$1:$B$1</code></td>
    </tr>
    <tr data-sourcepos="86:1-86:20">
      <td data-sourcepos="86:2-86:8">24(11000)</td>
      <td data-sourcepos="86:10-86:19"><code>$A1:B1</code></td>
    </tr>
    <tr data-sourcepos="87:1-87:21">
      <td data-sourcepos="87:2-87:8">25(11001)</td>
      <td data-sourcepos="87:10-87:20"><code>$A1:B$1</code></td>
    </tr>
    <tr data-sourcepos="88:1-88:21">
      <td data-sourcepos="88:2-88:8">26(11010)</td>
      <td data-sourcepos="88:10-88:20"><code>$A1:$B1</code></td>
    </tr>
    <tr data-sourcepos="89:1-89:22">
      <td data-sourcepos="89:2-89:8">27(11011)</td>
      <td data-sourcepos="89:10-89:21"><code>$A1:$B$1</code></td>
    </tr>
    <tr data-sourcepos="90:1-90:21">
      <td data-sourcepos="90:2-90:8">28(11100)</td>
      <td data-sourcepos="90:10-90:20"><code>$A$1:B1</code></td>
    </tr>
    <tr data-sourcepos="91:1-91:22">
      <td data-sourcepos="91:2-91:8">29(11101)</td>
      <td data-sourcepos="91:10-91:21"><code>$A$1:B$1</code></td>
    </tr>
    <tr data-sourcepos="92:1-92:22">
      <td data-sourcepos="92:2-92:8">30(11110)</td>
      <td data-sourcepos="92:10-92:21"><code>$A$1:$B1</code></td>
    </tr>
    <tr data-sourcepos="93:1-93:23">
      <td data-sourcepos="93:2-93:8">31(11111)</td>
      <td data-sourcepos="93:10-93:22"><code>$A$1:$B$1</code></td>
    </tr>
  </tbody>
</table>
<p>我们也采用相同的做法。</p>
<p>可以看到 四种类型的区域 可以定四个不同的基数 0，4，8，16。</p>
<ul>
<li>
<p>区域格式为$A$1，词法分析出单元格类型，则基数为 0，第一二个数字前面有$，type=0+1+2=3，二进制后四位 0011</p>
</li>
<li>
<p>区域格式为$A1:$B$1，词法分析出矩形区域类型，则基数为 16，第 1 第 3 第 4 字符前有$，type = 16+8+2+1=27，二进制后四位 1011</p>
</li>
</ul>
<p>通过先判断 type 值的范围，确定出区域类型，得到基数。
其中二进制值去掉基数之后 1 的位数就是$符号出现的位置。
最后就能通过 [$符号位置、range、区域类型] 三个信息反推出原公式。</p>
<h3>公式的链路计算</h3>
<p>假设存在以下公式</p>
<p>A = B
C = A
D = A+B
E = A + B + C + D
<font color=Red>B = A</font></p>
<p>注：红色公式会导致依赖链路成环。</p>
<p>基于上述公式信息，我们可以构建如下依赖关系。</p>
<p>[
{ id: 'b', dep: [ 'a', 'd', 'e' ] },
{ id: 'a', dep: [ 'c', 'd', 'e', <font color=Red>'b'</font> ] },
{ id: 'c', dep: [ 'e' ] },
{ id: 'd', dep: [ 'e' ] }
]</p>
<img src="/api/images/image/2026/04/297c64b765f1531f-16-d1b63e4cb8c047a5414193c9af8d2149-63ef5c.webp" width="220" height="270" align="middle" />
<p>当 B 节点数据变更之后，我们需要递归更新上游链路的所有节点。</p>
<p>更新逻辑如下：</p>
<p>1、清空更新链路</p>
<p>2、查询变更节点的上游</p>
<pre><code class="language-json">path: [b]

next: [a,d,e]  // path里的节点所有上游
</code></pre>
<p>发现 path 中的数据 b 不在 next 里（没有入度），则添加 b 到更新链路中（此时更新链路为 [b]）。</p>
<blockquote>
<p>此处的检查是为了保证更新顺序（最小入度）。</p>
</blockquote>
<p>对 next 去重后重复查询上游节点。</p>
<p>3、递归查询变更节点的上游</p>
<pre><code class="language-json">path: [a,d,e]

next: [c,d,e,b, e,]
</code></pre>
<p>发现 path 中的数据 a 不在 next 里（没有入度），则添加 a 到更新链路中（此时更新链路为 [b, a]）。</p>
<blockquote>
<p>这里需要注意的是，如果 next 中的节点存在于更新链路中，则出现循环依赖，递归终止。</p>
</blockquote>
<blockquote>
<p>如果开启 N 次迭代计算，就使用当前的更新链路[b,a]进行 N 次重复计算。否则 b 和 a 所在的单元格就需要报引用错误。</p>
</blockquote>
<p>如果 B = A 不存在（依赖成环），则重复上述步骤。</p>
<p>对 next 去重后重复查询上游节点。</p>
<pre><code class="language-json">path: [c,d,e]

next: [e,e]
</code></pre>
<p>发现 path 中的数据 c,d 不在 next 里（没有入度），则添加 c,d 到更新链路中（此时更新链路为 [b, a, c, d]）。</p>
<p>对 next 去重后重复查询上游节点。</p>
<pre><code class="language-json">path: [e]

next: []
</code></pre>
<p>发现 path 中的数据 e 不在 next 里（没有入度），则添加 e 到更新链路中（此时更新链路为 [b, a, c, d, e]）。</p>
<p>至此，公式递归完毕，按照更新链路依次更新单元格数据即可。</p>
<h3>公式的冲突处理</h3>
<p>由于存在冲突的情况：假设两个客户端基于同一个版本进行了不同的编辑操作，两种操作发生了冲突，没有经过冲突处理之前，两个客户端得到的计算结果都不是正确的。</p>
<p>因此，在发生公式编辑操作时，前端不会将计算结果进行上发到服务端，而是等到后端处理完op冲突收到op后，再进行计算结果。</p>
<h3>公式的初始化</h3>
<p>如果服务端快照不存储计算结果，服务端也没有计算能力的话，就需要前端在表格初始化时，进行一次<font color=Red>全局公式计算</font>。</p>
<p>如何进行初始化全局计算？</p>
<p>由于上方描述的算法是基于某个节点出现变更，再生成该节点变更之后需要更新的节点链路。项目初始化时就需要先得到<font color=Red>入度最小的节点</font>，并由该节点作为函数调动的发起者，得到完整的更新链路。然后再对更新链路中含有公式的单元格进行依次计算更新。</p>
<p>如何得到入度最小的节点？
表格初始化先使用快照model的单元格数据，转化成前端表格数据表。然后对前端表格数据表进行遍历，依据快照中的ref formula构建依赖图，在创建依赖图的方法中添加回调使用Map记录id出现次数。等数据初始化完毕，出现次数最少的节点即为入度最小的节点此案例为B）</p>
<h2>进阶:</h2>
<ul>
<li>使用Web Worker，实现多线程计算？</li>
<li>采用C、C++、Rust实现编译代码， 使用WASM实现更好的计算速度?</li>
<li>服务端运算的支持?</li>
</ul>
<h2>参考阅读</h2>
<ul>
<li><a href="https://gcdn.grapecity.com.cn/lesson-161.html">前端电子表格的计算引擎解密 - 前端电子表格的计算引擎解密</a></li>
<li><a href="https://leetcode.cn/problems/design-excel-sum-formula/">[Leetcode] 631. 设计 Excel 求和公式</a></li>
<li><a href="https://univer.ai/zh-CN/guides/sheet/architecture/formula">Univer公式引擎架构设计</a></li>
</ul>
]]></content:encoded>
      <category>技术</category>
      <pubDate>Thu, 25 Jul 2024 15:39:32 GMT</pubDate>
    </item>
    <item>
      <title>前端编程范式与设计模式</title>
      <link>https://qiaomu-blog.yiliang.app/2026-04-23-WvVzpC</link>
      <guid isPermaLink="true">https://qiaomu-blog.yiliang.app/2026-04-23-WvVzpC</guid>
      <description>本文探讨了前端开发中提升代码质量的核心编程范式与设计模式。在编程范式方面，重点介绍了异步编程（Promise/async/await）、面向对象编程（封装与继承）、函数式编程（纯函数与高阶函数）以及反应式编程（RxJS事件流）。在设计模式部分，详细阐述了单例、工厂、观察者及装饰者模式的实际应用。掌握这些技术有助于开发者构建更具可维护性、扩展性和可读性的高质量前端项目。</description>
      <content:encoded><![CDATA[<p>在前端开发中，理解并运用各种编程范式和设计模式有助于提升代码的可维护性、扩展性和可读性。本文将介绍一些常见且有用的编程范式和设计模式，并提供详细的代码示例。</p>
<h2>编程范式</h2>
<h3>1. 异步编程 (Asynchronous Programming)</h3>
<p>异步编程允许程序在等待某些任务完成时继续执行其他任务，从而避免阻塞。前端常见的异步操作包括网络请求、文件读取等。</p>
<h4>使用回调函数</h4>
<pre><code class="language-javascript">function fetchData(callback) {
  setTimeout(() => {
    const data = { id: 1, name: 'John' };
    callback(data);
  }, 1000);
}

fetchData(data => {
  console.log('Data received:', data);
});
</code></pre>
<h4>使用Promise</h4>
<pre><code class="language-javascript">function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { id: 1, name: 'John' };
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log('Data received:', data);
  })
  .catch(error => {
    console.error('Error:', error);
  });
</code></pre>
<h4>使用async/await</h4>
<pre><code class="language-javascript">async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log('Data received:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}
</code></pre>
<h3>2. 面向对象编程 (Object-Oriented Programming, OOP)</h3>
<p>面向对象编程通过对象来组织代码，核心概念包括封装、继承和多态。</p>
<h4>基本类与继承</h4>
<pre><code class="language-javascript">class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog('Rex');
dog.speak(); // Rex barks.
</code></pre>
<h4>多态</h4>
<pre><code class="language-javascript">class Cat extends Animal {
  speak() {
    console.log(`${this.name} meows.`);
  }
}

const animals = [new Dog('Rex'), new Cat('Whiskers')];
animals.forEach(animal => animal.speak());
// Rex barks.
// Whiskers meows.
</code></pre>
<h3>3. 函数式编程 (Functional Programming, FP)</h3>
<p>函数式编程强调使用纯函数、不可变数据和函数组合，避免可变状态和副作用。</p>
<h4>纯函数与不可变性</h4>
<pre><code class="language-javascript">const add = (a, b) => a + b;

const numbers = [1, 2, 3];
const newNumbers = numbers.map(num => num * 2);
console.log(newNumbers); // [2, 4, 6]
console.log(numbers); // [1, 2, 3] - 原数组不变
</code></pre>
<h4>高阶函数</h4>
<pre><code class="language-javascript">const withLogging = fn => (...args) => {
  console.log(`Calling ${fn.name} with args:`, args);
  return fn(...args);
};

const multiply = (a, b) => a * b;
const loggedMultiply = withLogging(multiply);
console.log(loggedMultiply(3, 4)); // Logs: Calling multiply with args: [3, 4], 12
</code></pre>
<h3>4. 反应式编程 (Reactive Programming)</h3>
<p>反应式编程处理动态数据流和异步事件，常用于构建响应式UI和处理事件流。</p>
<h4>使用RxJS处理事件流</h4>
<pre><code class="language-javascript">import { fromEvent } from 'rxjs';
import { throttleTime, map } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const positions = clicks.pipe(
  throttleTime(1000),
  map(event => ({ x: event.clientX, y: event.clientY }))
);

positions.subscribe(position => console.log('Clicked at:', position));
</code></pre>
<h2>设计模式</h2>
<h3>1. 单例模式 (Singleton Pattern)</h3>
<p>单例模式确保一个类只有一个实例，并提供全局访问点。</p>
<pre><code class="language-javascript">class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
    Logger.instance = this;
    this.logs = [];
  }

  log(message) {
    this.logs.push(message);
    console.log(`LOG: ${message}`);
  }

  printLogCount() {
    console.log(`${this.logs.length} logs`);
  }
}

const logger1 = new Logger();
const logger2 = new Logger();

logger1.log('This is the first log');
logger2.log('This is the second log');
logger1.printLogCount(); // 2 logs
</code></pre>
<h3>2. 工厂模式 (Factory Pattern)</h3>
<p>工厂模式通过定义一个接口或抽象类来创建对象，而不指定具体类。</p>
<pre><code class="language-javascript">class Shape {
  constructor(type) {
    this.type = type;
  }

  draw() {
    console.log(`Drawing a ${this.type}`);
  }
}

class ShapeFactory {
  createShape(type) {
    return new Shape(type);
  }
}

const factory = new ShapeFactory();
const circle = factory.createShape('circle');
const square = factory.createShape('square');

circle.draw(); // Drawing a circle
square.draw(); // Drawing a square
</code></pre>
<h3>3. 观察者模式 (Observer Pattern)</h3>
<p>观察者模式定义对象之间的一对多依赖关系，当一个对象状态改变时，所有依赖对象会收到通知并更新。</p>
<pre><code class="language-javascript">class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log(`Observer received data: ${data}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notify('Some data');
</code></pre>
<h3>4. 装饰者模式 (Decorator Pattern)</h3>
<p>装饰者模式允许向现有对象添加新功能，而不改变其结构。</p>
<pre><code class="language-javascript">class Coffee {
  cost() {
    return 5;
  }
}

class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost() + 1;
  }
}

class SugarDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost() + 0.5;
  }
}

let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 6.5
</code></pre>
<h2>前端中的依赖注入</h2>
<p>参考阅读<a href="https://redi.wendell.fun/zh-CN/blogs/di">这篇文章</a></p>
<h2>总结</h2>
<p>这些编程范式和设计模式各有其独特的应用场景，理解并掌握它们能够帮助开发者编写出更高效、可维护和可扩展的代码。在实际开发中，根据具体需求选择合适的范式和模式是提升项目质量的重要一环。</p>
]]></content:encoded>
      <category>技术</category>
      <pubDate>Wed, 24 Jul 2024 19:55:59 GMT</pubDate>
    </item>
    <item>
      <title>C语言编译为WASM vs JavaScript：计算斐波那契数列的性能对比</title>
      <link>https://qiaomu-blog.yiliang.app/cwasm-vs-javascript</link>
      <guid isPermaLink="true">https://qiaomu-blog.yiliang.app/cwasm-vs-javascript</guid>
      <description>本文探讨了WebAssembly（WASM）技术在现代Web开发中的性能优势。通过对比C语言编译为WASM与原生JavaScript在计算斐波那契数列时的表现，文章展示了WASM如何利用高效的二进制格式提升计算密集型任务的执行效率。文中不仅介绍了WASM的基本概念，还提供了具体的代码示例及编译流程，旨在说明WASM在处理音视频、复杂计算等高性能需求场景中，相较于JavaScript具有显著的性能提升，是优化Web应用性能的重要手段。</description>
      <content:encoded><![CDATA[<blockquote>
<p>在现代Web开发中，性能优化是一个重要的议题。WebAssembly（WASM）作为一种新兴的技术，为Web应用带来了显著的性能提升。**在需要高性能计算的Web应用程序，例如音视频、协作冲突中起着非常重要的作用。**本文将探讨C语言编译为WASM在计算斐波那契数列时相较于JavaScript的性能优势。</p>
</blockquote>
<p><img src="/api/images/image/2026/04/3ef615a60c06f9e4--02c46aee9974e0cb789073d42c0abf9a-eca445.png" alt=""></p>
<h2>什么是WebAssembly？</h2>
<p>WebAssembly（WASM）是一种新的二进制格式，可以在现代Web浏览器中高效地运行。这种格式可以将C、C++、Rust等编译型语言编译为高效的字节码，然后在浏览器中执行，从而提升了计算性能。</p>
<h2>斐波那契数列简介</h2>
<p>斐波那契数列是一种经典的数列，其中每一个数都是前两个数的和。即：</p>
<ul>
<li>F(0) = 0</li>
<li>F(1) = 1</li>
<li>F(n) = F(n-1) + F(n-2)  (n ≥ 2)</li>
</ul>
<p>计算斐波那契数列常常用来测试编程语言和计算平台的性能，因为它具有简单而计算量大的特性。</p>
<h2>使用JavaScript计算斐波那契数列</h2>
<p>在JavaScript中，我们可以通过递归或者迭代的方法来计算斐波那契数列。以下是一个使用递归方法的JavaScript代码示例：</p>
<pre><code class="language-javascript">function _fibonacciJS(n) {
    if (n == 1 || n == 2) {
        return 1;
    }
    return _fibonacciJS(n - 1) + _fibonacciJS(n - 2);
}
</code></pre>
<h2>使用C语言编译为WASM计算斐波那契数列</h2>
<p>C语言是一种高效的编译型语言，编译为WASM后，其性能相较于JavaScript会有显著提升。以下是一个使用C语言计算斐波那契数列的示例代码：</p>
<pre><code class="language-c">int fibonacci(int n)
{
    if (n == 1 || n == 2)
    {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}
</code></pre>
<p>然后，使用 <a href="https://emscripten.org/docs/tools_reference/emcc.html">emcc</a> 编译器将其编译为Wasm：</p>
<pre><code class="language-shell">emcc -O3 -o fibonacci.js -s EXPORTED_FUNCTIONS='["_fibonacci"]' fibonacci.c
</code></pre>
<h2>在HTML网页中进行测试</h2>
<pre><code class="language-html">&#x3C;!DOCTYPE html>
&#x3C;html lang="en">

&#x3C;head>
    &#x3C;meta charset="UTF-8">
    &#x3C;meta http-equiv="X-UA-Compatible" content="IE=edge">
    &#x3C;meta name="viewport" content="width=device-width, initial-scale=1.0">
    &#x3C;title>fjb Wasm&#x3C;/title>
&#x3C;/head>

&#x3C;body>
    &#x3C;p>num: &#x3C;input type="number" id="num">&#x3C;/p>
    &#x3C;p>JS: &#x3C;span id="JSresultDom">&#x3C;/span>&#x3C;/p>
    &#x3C;p>Wasm: &#x3C;span id="WasmresultDom">&#x3C;/span>&#x3C;/p>
    &#x3C;script src="fibonacci.js">&#x3C;/script>
    &#x3C;script>
        function _fibonacciJS(n) {
            if (n == 1 || n == 2) {
                return 1;
            }
            return _fibonacciJS(n - 1) + _fibonacciJS(n - 2);
        }
        num.onchange = () => {

            const jsStart = performance.now();
            const jsResult = _fibonacciJS(num.value);

            const jsEnd = performance.now();
            JSresultDom.textContent = ` (JS: ${(jsEnd - jsStart).toFixed(2)}ms)   jsResult: ${jsResult}`;


            const wasmStart = performance.now();
            const wasmResult = Module._fibonacci(num.value);
            const wasmEnd = performance.now();
            WasmresultDom.textContent = ` (Wasm: ${(wasmEnd - wasmStart).toFixed(2)}ms)   wasmResult: ${wasmResult}`;
        }
    &#x3C;/script>
&#x3C;/body>

&#x3C;/html>
</code></pre>
<p>性能对比：</p>
<p><img src="/api/images/image/2026/04/7fed1c133b935f9d-20-0734f478e0ecdabf2d470a841208277d-853ba9.webp" alt=""></p>
<h2>源码参考</h2>
<ul>
<li><a href="https://github.com/AquaHydro/code-examples/tree/main/fib-wasm">fib-wasm</a></li>
</ul>
]]></content:encoded>
      <category>技术</category>
      <pubDate>Wed, 24 Jul 2024 08:30:59 GMT</pubDate>
    </item>
    <item>
      <title>api接口的一些设计原则和最佳实践</title>
      <link>https://qiaomu-blog.yiliang.app/api</link>
      <guid isPermaLink="true">https://qiaomu-blog.yiliang.app/api</guid>
      <description>本文探讨了在API设计与开发中遵循核心软件工程原则的重要性。通过单一职责、最小惊讶、安全性、KISS及封装等原则，强调了系统设计应保持简洁与逻辑清晰。实践中，应将ID与创建时间等字段交由数据库自动处理，而非依赖客户端输入，这不仅能提升数据一致性与安全性，还能有效降低代码冗余。文章主张通过中间件精简请求上下文，并建议在接口设计中明确职责边界，从而显著提高代码的可维护性与可靠性。</description>
      <content:encoded><![CDATA[<h3>1. <strong>单一职责原则（Single Responsibility Principle, SRP）</strong></h3>
<ul>
<li>每个模块或函数应该只负责一件事情。新增接口的职责就是接收新数据并插入数据库，而不是处理自动生成的字段。</li>
</ul>
<h3>2. <strong>最小惊讶原则（Principle of Least Astonishment, POLA）</strong></h3>
<ul>
<li>系统应该按照用户或开发者预期的方式工作。自动生成的字段不应该由客户端提供，因为这会违反常规做法，容易导致混淆和错误。</li>
</ul>
<h3>3. <strong>安全性原则</strong></h3>
<ul>
<li>不让客户端传递自动生成的字段可以避免潜在的安全问题，例如数据伪造和篡改。确保数据的真实性和完整性。</li>
</ul>
<h3>4. <strong>简化设计（KISS - Keep It Simple, Stupid）</strong></h3>
<ul>
<li>保持设计简单，避免不必要的复杂性。只处理需要的字段，减少代码中的冗余和潜在错误。</li>
</ul>
<h3>5. <strong>封装（Encapsulation）</strong></h3>
<ul>
<li>封装数据库操作细节，客户端不需要关心数据库是如何生成id和创建时间的，只需要得到结果。这也是一种信息隐藏的方式。</li>
</ul>
<h3>实践中的体现</h3>
<p>在实际开发中，遵循这些原则可以提高代码的可维护性、安全性和可读性。例如：</p>
<ul>
<li><strong>数据库自动生成字段</strong>：让数据库来生成id、创建时间等字段，可以保证数据的一致性和唯一性，避免手动处理带来的错误。</li>
<li><strong>API设计</strong>：在API设计中，明确哪些字段是客户端需要提供的，哪些是由服务器生成的，保证接口的清晰性和可靠性。</li>
</ul>
<blockquote>
<p>Talk is cheap. Show me the code.</p>
<p>​                                                                                                -Linux 创始人 Linus Torvalds</p>
</blockquote>
<h4>从请求体上下文<code>ctx.user</code>中只取该接口控制器<code>AuthController</code>需要的字段数据，其中创建上下文的中间件<code>verifyAuth</code>可以应用在不同的路由/业务接口中。</h4>
<p><img src="/api/images/image/2026/04/4f50a7e1683b1db2-image-20240721081744300.png" alt="image-20240721081744300"></p>
<p><img src="/api/images/image/2026/04/b1570ac85ddf6d60-image-20240721081613510.png" alt="image-20240721081613510"></p>
<p><img src="/api/images/image/2026/04/0fe5c8094b112eaa-image-20240721075318479.png" alt="image-20240721075318479"></p>
<h4>cos的上传需要返回https链接，符合高版本chrome浏览器的安全策略以及最佳做法</h4>
<p><img src="/api/images/image/2026/04/0cbbc974d6a2dd07-image-20240721082223721.png" alt="image-20240721082223721"></p>
<p><img src="/api/images/image/2026/04/b58ec9f45ada1f16-image-20240721082517590.png" alt="image-20240721082517590"></p>
<h3>扩展阅读</h3>
<p><a href="https://github.com/ShawnLeee/the-book/blob/master/clean%20code-%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93%20%E4%B8%AD%E6%96%87%E5%AE%8C%E6%95%B4%E7%89%88-%E5%B8%A6%E4%B9%A6%E7%AD%BE.pdf">代码整洁之道 中文完整版-带书签.pdf</a></p>
<p><a href="https://cloud.tencent.com/developer/news/366899">九年总结：优秀程序设计的18大原则</a></p>
]]></content:encoded>
      <category>技术</category>
      <pubDate>Sun, 21 Jul 2024 08:30:59 GMT</pubDate>
    </item>
    <item>
      <title>如何将 univer-sheet 的粘贴解析性能提升 85%</title>
      <link>https://qiaomu-blog.yiliang.app/univer-sheet85</link>
      <guid isPermaLink="true">https://qiaomu-blog.yiliang.app/univer-sheet85</guid>
      <description>本文深入解析了对 univer-sheet 复制粘贴逻辑的性能优化实践。针对原方案中因频繁调用 getComputedStyle 导致的强制重排、主线程阻塞及内存泄漏问题，团队进行了核心重构。优化方案摒弃了传统的 DOMParser 和挂载 DOM 树的方式，转而采用深度优先遍历算法模拟样式计算，并将样式存储于 Map 中。该改进不仅将粘贴解析耗时从 27.5 秒大幅缩短至 2.68 秒，还通过规范化资源回收机制彻底解决了内存泄漏，显著提升了表格组件的渲染性能与稳定性。</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>注:</strong> 本文基于 <a href="https://github.com/dream-num/univer">univer-sheet</a> 源码，对其复制粘贴解析逻辑进行核心优化解读。</p>
</blockquote>
<h2>效果图：</h2>
<ul>
<li>
<p><strong>变更前:</strong></p>
<p><img src="/api/images/image/2026/04/2adcb68001062690--bcfe20cc2b851df896bc62c5c98a63b4-236cb6.webp" alt=""></p>
</li>
<li>
<p><strong>变更后:</strong></p>
<p><img src="/api/images/image/2026/04/89ca82a9450d66d0--d1eb6c86878a076da8d0256fa2106115-4e8ed1.webp" alt=""></p>
</li>
</ul>
<p>由上图可以看到在提交变更之前，粘贴解析长任务耗时 27.5 秒并且内存没有得到回收，出现了内存泄漏问题。在变更之后，耗时仅需要 2.68 秒，对应的内存也得到释放。</p>
<blockquote>
<p><strong>注:</strong> 内存泄漏问题也可以通过堆快照定位。</p>
</blockquote>
<p>PR 请求可以 <a href="https://github.com/dream-num/univer/pull/2631">点此查看</a>。</p>
<h2>耗时原因分析:</h2>
<p><img src="/api/images/image/2026/04/305cccd8e1734e04--f3fb909ef8b68491b23d88017135f919-bc1c2a.webp" alt=""></p>
<p>通过开发者工具结合源码分析，我们能发现 <code>windows.getComputedStyle().getPropertyValue</code> 出现了大量耗时的情况。</p>
<h3>为什么 <code>getComputedStyle()</code> 和 <code>getPropertyValue()</code> 方法会大量耗时造成页面卡死？</h3>
<ul>
<li>
<p><strong>强制重排（reflow）</strong>: <code>getComputedStyle()</code> 方法会导致浏览器计算元素的所有样式，这可能需要重新计算整个文档的布局。这是因为浏览器需要确保样式是最新的，并且在一些情况下可能会重新布局页面。这种重排操作是非常耗时的，特别是当页面上有大量的元素时。</p>
</li>
<li>
<p><strong>同步操作</strong>: <code>getComputedStyle()</code> 方法是同步的，这意味着浏览器必须在返回结果之前完成所有的计算。这会阻塞主线程，导致页面的其他操作变慢或卡顿。</p>
</li>
<li>
<p><strong>布局树的生成</strong>: 浏览器需要生成和更新布局树（layout tree），以便计算每个元素的最终样式。这些操作通常非常复杂，涉及大量的计算和内存操作。</p>
</li>
<li>
<p><strong>复杂的 CSS 规则</strong>: 如果页面中有大量复杂的 CSS 规则，或者样式表层级嵌套较深，浏览器计算每个元素的最终样式时会更加复杂和耗时。</p>
</li>
</ul>
<h2>解决方案</h2>
<p>DOM 树是树结构，我们可以采用深度优先遍历的方式，将上层样式传递到下层节点模拟计算样式，来避免使用 <code>getComputedStyle</code>。节点样式是通过样式选择器的优先级，来确定最终的样式。了解这两个基础逻辑后，我们就可以开始编码了。</p>
<h3>解析 style 标签，将标签的样式存储在 Map 中</h3>
<pre><code class="language-typescript">const style = this._dom.querySelector('style');
if (style) {
    const shadowHost = document.createElement('div');
    const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
    document.body.appendChild(shadowHost);
    shadowRoot.appendChild(style);
    for (const rule of style.sheet!.cssRules) {
        const cssRule = rule as CSSStyleRule;
        const selectorText = cssRule.selectorText;
        const style = cssRule.style;
        this._styleMap.set(selectorText, style);
    }
    style.remove();
    shadowHost.remove();
}
</code></pre>
<blockquote>
<p><strong>注:</strong> style 标签只有挂载到 DOM 上，才会实现 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleSheet"><code>CSSStyleSheet</code></a> 接口。而使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM">shadow DOM</a> 的目的是为了样式隔离，避免造成全局样式污染。</p>
</blockquote>
<h3>获取样式函数</h3>
<pre><code class="language-typescript">private _getStyle(node: HTMLElement, styleStr: string) {
    const recordStyle: Record&#x3C;string, string> = turnToStyleObject(styleStr);
    const style = node.style;
    // retrieve multiple sources for a node and compile them into a cohesive new style string. eg.`background`、`background-color`
    ···
    ···
    // style represents inline styles with the highest priority, followed by selectorText which corresponds to stylesheet rules, and recordStyle pertains to inherited styles with the lowest priority.
        value =
            style.getPropertyValue(key) ||
            this._getStyleBySelectorText(`#${node.id}`, key) ||
            value ||
            this._getStyleBySelectorText(node.nodeName.toLowerCase(), key) ||
            recordStyle[key] ||
            '';
        value &#x26;&#x26; (newStyleStr += `${key}:${value};`);
    }
    return newStyleStr;
}
</code></pre>
<blockquote>
<p>详细函数实现请翻阅 <code>packages/sheets-ui/src/services/clipboard/html-to-usm/converter.ts</code>。函数实现时需要注意在各类 html 中，例如 <code>background</code>、<code>background-color</code> 在表格中均可代表背景颜色，应该补充边界处理。</p>
</blockquote>
<h2>内存泄漏问题</h2>
<p>参考 StackOverflow 上的 <a href="https://stackoverflow.com/questions/56451731/dom-parser-chrome-extension-memory-leak">这篇帖子</a>。</p>
<pre><code class="language-diff">export default function parseToDom(rawHtml: string) {
-	const parser = new DOMParser();
- 	const html = `&#x3C;x-univer id="univer-root">${rawHtml}&#x3C;/x-univer>`;
- 	const doc = parser.parseFromString(html, 'text/html');

- 	return doc.querySelector('#univer-root');
+ 	const template = document.createElement('body');
+ 	template.innerHTML = rawHtml;
+	return template;
}
</code></pre>
<p>在解决内存泄漏时，剔除了 DOMParser API 的使用，并去除了将 html 字符串挂载到 DOM 上的行为。在粘贴行为结束后的调用dispose函数回收解析过程中使用的 Map 和临时变量。经过这样处理，不仅解决了内存泄漏的问题，还节约了挂载构建 DOM 树的时间。</p>
<h2>参考阅读</h2>
<ul>
<li><a href="https://univer.ai/zh-CN/guides/sheet/architecture/univer">Univer 架构</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/574069391">Chromium 渲染流水线——字节码到像素的一生</a></li>
</ul>
]]></content:encoded>
      <category>技术</category>
      <pubDate>Thu, 11 Jul 2024 21:05:59 GMT</pubDate>
    </item>
  </channel>
</rss>