[{"authors":null,"categories":["Technology","Ai","DigitalTwin"],"content":"用户上传一张工程图，我们把图纸里的文字识别出来，塞进聊天上下文，让 Agent 靠这些文字回答问题。很常规的文档问答套路，一开始我们就是这么做的，但是问题在使用中暴露了出来。\n用户把一张工程图上传到我们的数字孪生聊天里，然后问：\n泵旁边那个标注写的是什么？\n对工程师来说，这个问题几乎是扫一眼就答出来。图上有一个文本框，写着：\nReplace seal kit, see WO-4451\n文本框旁边还有一条细引线，连到 P-101 这台泵。工程师看到这里，基本就知道：这个标注是在说 P-101，要更换密封套件，细节去看 WO-4451 这个工单，但我们的系统没有这么理解。\n它打开了 Python 沙盒，开始写解析代码。\n这不是预期的结果。因为文字其实已经提取出来了，系统缺的不是那几个英文单词，真正缺的是图纸里的关系：这段文字是通过一条线指向 P-101 的。\n我们手上其实什么都不缺：有一个基于 Cesium 的 3D 数字孪生场景，有一个可以控制场景的 Agent，也有大量沉在 2D 工程图里的现场知识。问题是，这三样东西并没有接起来。\nAgent 能读文字，但不知道文字和图形之间是什么关系。3D 场景里有设备，但不知道图纸上的标注在说谁。工程图里本来有不少知识，但这些知识还停留在 2D 页面上。\n所以后面回头看，我们做的是把工程图里的设备、标注、连接关系读出来，再把这些东西映射到 3D 场景里。\n旧流程只留下了文字 工程图跟普通文档的思路完全不一样。\n普通文档主要是文字内容，而工程图是文字、符号、线条和位置都可能重要。\n一张 P\u0026amp;ID 或现场平面图里，通常同时有几类对象。\n有设备，比如泵、储罐、管廊，它们会带工程标签，比如 P-101、TK-7。\n有标注，比如说明框、尺寸、修订云线、检查备注。\n还有关系。这个关系可能是一条引线，也可能是一段管线，也可能只是一个标签刚好落在某个设备轮廓里。\n工程师看图的时候，不会把文字单独拎出来理解。他会顺手把几个信息一起看：这个标注在哪，线连到哪里，旁边是什么设备，符号又代表什么，但我们的旧流程，刚好把这些东西拆散了。\nSVG 图纸上传之后，我们把它交给文件提取 API。API 返回一段文字，我们再把文字放进聊天上下文。\n这样做的结果是，“Replace seal kit, see WO-4451” 被留下来了，但那条指向 P-101 的线没了。\n对工程师来说，那条线就是答案的一部分。旧系统在提取文本那一步，已经顺手把它丢掉了。\n所以问题其实不在 OCR。哪怕 OCR 完全正确，系统还是不知道这句话到底指向谁。\n我们真正需要的是一个结构化结果：系统要知道哪些对象是设备，哪些对象是标注，哪些标注指向哪些设备，哪些设备之间有连接。再往后，如果要把这些东西带进 3D 场景，系统还得知道哪些 2D 对象能对应到场景里的对象。\n如果场景里暂时还没有这些对象，系统也应该先把它们作为候选实体放进去，而不是继续把它们当成图纸上的一段文字。\nSVG 不只是图 最开始我们其实试了两个方向。\n一个方向是把图纸当图片看。\n我们把 SVG 渲染成 PNG，交给视觉模型，让模型判断图里有什么。因为扫描版 PDF 本来也得这么处理，所以这条路很自然。\n视觉模型在语义判断上确实有帮助。它能看出哪里像一台泵，哪里像一段管线，标题栏里大概写了什么。\n但是它的问题是位置不够准。\n如果只是问“图里有没有泵”，给一个大概区域就够了。可我们后面要做的事情没这么简单。我们要把 2D 图纸里的点映射到 3D 场景里，可能还要拿这个点做像素拾取，再通过深度缓冲区算出世界坐标，到了这一步，“大概在这里”就已经不够用了。\n另一个方向是把 SVG 当 XML 解析。\n这条路听上去没那么 AI，但非常实用。SVG 里本来就带着精确的几何信息。每个 \u0026lt;text\u0026gt; 元素有坐标，每条线有起点和终点，每个形状有边界框，translate、scale、rotate 这些变换也都写在结构里，其实SVG 表面是一张图，底下其实是一整套几何数据。\n但 XML 也有自己的局限。它知道这里有几条线、几个圆、几个矩形之类的，但不知道这些图形组合起来是不是一台泵。\n所以我选择把两边拼起来。\n视觉模型负责回答“这是什么”，SVG/XML 负责回答“它在哪里”。\n结构解析器先遍历 SVG，把文字位置、图形边界框、候选引线提取出来。当然这里还得处理各种 transform，不然坐标会直接错掉。\n视觉模型再去看渲染后的 PNG，输出语义信息，比如这里是泵，那里是 DN50 管线，标题栏里有哪些元数据。\n然后再做一层融合。\n比如视觉模型说，(640, 360) 附近有一台泵；XML 解析器说，这里有一个带标签的图形组，边界框是 (632, 319, 96, 92)。最后得到的对象里，类型来自视觉模型，几何位置来自 XML。\n文字也尽量从 XML 里拿，而不是从视觉 OCR 里拿。因为工程编号对精度很敏感。WO-4451 被 OCR 看成 W0-445I，人一眼能发现不对，系统却很可能把它当成另一个工单号。\n这一步最后的输出是一个 JSON，我们把它叫作 DrawingGraph。\nDrawingGraph 里有组件、标签、标注、标注和组件之间的链接，也有组件之间的连接关系。后面的流程都基于 DrawingGraph 做，不再直接读原始 SVG。\n能用几何规则解决的，就不要先丢给 LLM “标注应该关联到哪台设备”这个问题，一开始看上去挺像是 LLM 的活。\n后来发现，真实图纸上其实没那么难读。大多数关联关系，本来就已经画在图上了。一个说明框旁边有一条引线，一头挨着文本框，另一头落在设备附近。\n所以这件事更像几何问题而不是语义推理。\n最后简化版的判断流程。\n先看引线。如果有一条线从文本框连到设备框，这个关系的置信度就很高。\n再看包含关系。如果文字本来就在某个设备或区域轮廓里，这个关系也比较可靠。\n最后再看距离。如果前两种都找不到，就在合理半径内找最近的设备。这个结果只能算猜测，置信度会低很多。\nLLM 被放在最后，只处理那些几何规则解决不了的少数情况。\n原则是能用代码算清楚的事情，就不要让模型凭感觉猜。不然它很容易被拉去干一堆本来两步几何判断就能解决的活。\n我们还会把每条关系的来源记下来。\n一条关系是通过引线得到的，还是通过距离猜出来的，这两个结果不能被当成同一种东西。后面的流程在消费这些结果时，也应该按不同置信度处理。\nAgent 为什么会打开 Python 沙盒 DrawingGraph 做出来之后，我们又碰到另一个问题。\n用户问：\n泵旁边那个标注是什么？\n照理说，Agent 应该直接从摘要里回答。因为标注和设备之间的关系已经整理好了，也放进了上下文里。\n但是Agent打开 Python 沙盒，开始写 json.load(...)，准备自己去解析 DrawingGraph 文件。\n看代码的时候发现，它在老老实实执行我们给它的提示。\n我们当时在上下文里写了一句：\n如果需要精确坐标，可以用文件工具读取 DrawingGraph JSON。\n这句话太容易把模型带到错误路径上。\n“泵旁边”听起来像个空间问题。Agent 很自然会判断自己可能需要坐标。再看工具箱，最像能处理嵌套 JSON 的工具就是通用代码执行，于是它就开始写代码了。\n这个问题最后有两个修复。\n一个是改提示。我们明确告诉 Agent，摘要里已经包含所有“标注到设备”的关系，回答这类问题时，不需要重新读取 JSON。\n另一个更关键。我们给它加了一个更合适的工具，叫 get_drawing_graph。\n这个工具不是通用代码执行，而是一个受控查询接口。Agent 可以按区域查、按标签查、按文本查。工具支持分页，也会截断过大的字段。\n这个工具上线之后，Agent 基本就会优先用它，而不是动不动打开 Python 沙盒。\n如果工具箱里只有一个通用代码执行工具，模型就会把很多本来不该写代码的事情也变成写代码。\n然后是坐标配准 前面解决的是图纸理解问题，然后要做的是把图纸真正放进 3D 场景里。\nDrawingGraph 知道对象在图纸坐标里的位置，3D 场景知道对象在真实世界里的位置，中间缺的是一个映射关系。\n这个映射关系可以用仿射变换表示。\n仿射变换可以处理比例尺、旋转、平移，也可以处理 y 轴方向相反的问题。SVG 的 y 轴向下增长，地图里的北向通常向上，这个差异不需要专门写特殊规则，拟合出来的系数会自己反映出来。\n我们的流程大概是这样。\n先把已经匹配上的世界坐标转到一个局部的 east/north 米制平面里。这里不能直接在经纬度上做最小二乘，因为经度对应的实际距离会随纬度变化。直接拿经纬度去算，结果很容易歪。\n然后用最小二乘求 6 个仿射变换系数。理论上至少需要 3 个不共线的点，点越多，结果越稳。\n如果有 4 个或更多匹配点，我们会跑 RANSAC。\n这里的 RANSAC 没有做得特别复杂。因为匹配点数量通常不多，直接枚举所有 3 点组合就够了。每个组合拟合一个变换，再看哪个变换能解释最多匹配点。\n这么做主要是为了防止一个错误匹配把整体带歪。\n比如有人把 P-101 匹配到了 200 米外的另一台泵，RANSAC 会把这个点当成离群点，而不是让它把整套变换扭坏。\n拟合完成后，我们会输出每个匹配点的残差，单位是米。\n后来我们经常靠这个输出做 sanity check。\n比如系统告诉用户：\nTK-7 这个点偏了 38 米，其他点都在 2 米以内。\n这种反馈比一句“配准失败”有用。用户可以直接回头检查 TK-7 这个匹配点，而不是先怀疑整套系统都不靠谱。\n这里还有两个保护机制。\n一个是共线检查。\n储罐区里的罐经常排成一条线。如果用户只选了这一排罐，系统在垂直方向上其实没法稳定拟合。遇到这种情况，我们会直接拒绝输入，并提示用户补一个不在这条线上的匹配点。\n另一个是外推标记。\n如果某个预测点落在已匹配区域之外，我们会给它加上 extrapolated: true，同时降低置信度。原因很简单，仿射变换在锚点之间通常比较可信，越往外推，结果越像猜。\n配准不需要已有实体 当时我们默认配准来自实体搜索。\n图纸上有 P-101，数字孪生里也有一个叫 P-101 的实体。系统查到这个实体的位置，然后把它作为锚点。\n如果场景里的实体数据足够完整，这当然是最稳的一种方式。\n但后来我问了一个问题：如果场景里根本没有实体呢？\n比如场景只是一个摄影测量生成的 3D 网格，里面没有设备对象，标签，也没有结构化资产数据。\n配准算法真正需要的不是实体，而是一组点对：图纸上的 x/y 坐标，对应真实世界里的 lon/lat 坐标，实体只是拿到世界坐标的一种方式，不是配准数学本身的一部分。。\n在 Cesium 这样的地理场景里，只要场景有地理参考，就可以通过深度缓冲区拿到世界坐标。用户点击一个像素，可以拿到世界位置；视觉模型在截图上指出一个位置，也可以转成世界位置。\n所以后来我们把锚点来源做成了一个分层流程。\n最理想的是实体搜索。图纸标签能直接匹配已有数字孪生实体。\n下一层是图纸自带的地理信息，比如标题栏里的网格坐标、比例尺和北向信息，这块能力还在补。\n再下一层是视觉识别，系统对 3D 场景截图做检测，再通过深度缓冲区把检测结果转成世界坐标。\n最后一层是用户点击三个点。系统提示用户依次点击 P-101、VLV-23、E-401。\n最后的方法听上去有点原始，但在各种奇怪场景里往往比自动识别更靠谱。点云能用，夜间场景能用，抽象示意图也能用。自动识别失败时就用它兜底，比想象中有用。\n复盘时顺手挖出的 bug 当时我在写冷启动场景的流程文档。也就是 3D 场景里还没有结构化实体。用户上传图纸，点击三个锚点，系统完成配准，然后把图纸里的设备放进 3D 场景，突然发现一个逻辑不对。\n当时的放置工具会跳过“已经匹配过”的组件。\n这个逻辑在有实体的场景里说得通。一个组件如果已经匹配到已有实体，确实不需要再创建一个。\n但冷启动场景不是这样。\n在冷启动里，用户点击的三 …","date":1781136e3,"description":"这篇文章复盘了一个工程图理解项目：我们最初只是想提取图纸文字，后来发现真正重要的是文字、符号、位置和连接关系，于是把 2D 工程图解析成结构化数据，并映射到 3D 数字孪生场景里","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"372b1a995c85434d7e4f62472cae1f20","permalink":"https://siqi-liu.com/zh/post/%E6%88%91%E4%BB%AC%E6%80%8E%E4%B9%88%E6%8A%8A-2d-%E5%9B%BE%E7%BA%B8%E6%8E%A5%E8%BF%9B-3d-%E5%9C%BA%E6%99%AF/","publishdate":"2026-06-11T00:00:00Z","relpermalink":"/zh/post/%E6%88%91%E4%BB%AC%E6%80%8E%E4%B9%88%E6%8A%8A-2d-%E5%9B%BE%E7%BA%B8%E6%8E%A5%E8%BF%9B-3d-%E5%9C%BA%E6%99%AF/","section":"post","summary":"这篇文章复盘了一个工程图理解项目：我们最初只是想提取图纸文字，后来发现真正重要的是文字、符号、位置和连接关系，于是把 2D 工程图解析成结构化数据，并映射到 3D 数字孪生场景里","tags":null,"title":"我们怎么把 2D 图纸接进 3D 场景","tldr":null,"type":"post"},{"authors":null,"categories":["Life","Ai"],"content":" 每隔一段时间，关于 AI 和软件工程师的讨论就会重新出现一次。\n“AI 会先替代 junior 工程师。”\n“AI 会更快替代 senior 和 staff 工程师，因为他们太贵。”\n“AI 最终会替代所有软件工程师，只需要留下少数人检查 AI 的输出。”\n这些观点听起来都不陌生。\n它们背后的逻辑也不是完全没有道理。\n有人认为 junior 工程师最危险，因为 AI 写代码的能力已经超过了很多初级工程师。虽然它有时候会犯错，但在很多常见任务上，它已经能生成不错的代码，甚至能解释思路、补测试、写文档。\n也有人认为 senior 和 staff 工程师反而更危险。原因很简单：他们贵。对于资本来说，如果一个 junior 工程师加上 AI 就能完成一个 senior 工程师 80% 或 90% 的工作，而 senior 工程师的薪水是 junior 的两倍，那管理层真的会在意最后那一点工程质量差异吗？\n还有一种更极端的观点：不管你是什么级别，最终都会被 AI 替代。因为所有技术难题最终都会被攻克。软件开发会被自动化到只需要极少数人做最终质量检查。至于这极少数人从哪里来、他们又怎么保持判断力，这个问题通常不会被解释得太清楚。\n我理解这些担忧。\n但我想提供一个不同的视角。\n不是因为我觉得 AI 很弱。\n也不是因为我想防御性地证明“工程师永远安全”。\n恰恰相反，我每天都在用 AI。\n我用它写代码，查问题，读文档，理解陌生技术，生成测试，整理思路。它确实让我变快了。很多过去让我觉得琐碎、低效、甚至有点痛苦的工作，现在都可以被 AI 大幅压缩。\n但也正因为我每天都在用它，我反而越来越相信一件事：\nAI 不会杀死软件工程师。它只会杀死我们最讨厌的那部分工作。\n我们真正热爱的，真的是写代码本身吗？ 我们可以认真想一下。\n当一个人说自己喜欢写代码的时候，他到底喜欢的是什么？\n是喜欢一行一行写 if/else 吗？\n是喜欢手动搭 boilerplate 吗？\n是喜欢在不同文件里反复搬运字段吗？\n是喜欢为了一个简单功能写一大堆重复的接口、类型、校验、测试和配置吗？\n可能不是。\n至少对我来说，不是。\n我真正喜欢的，是一个东西从无到有被搭起来的过程。\n是一个功能终于跑通的那一刻。\n是性能优化之后，页面加载快了一点。\n是一个复杂流程被整理清楚之后，系统突然变得更顺了。\n是一个 bug 被定位出来之后，你终于明白“原来问题在这里”。\n是你看着自己手上的东西像搭乐高一样，一块一块拼起来，最后变成一个真正能用的产品。\n这种感觉很像小时候搭玩具。\n你不是因为喜欢每一块塑料积木本身，才去搭一个城堡。\n你喜欢的是那个城堡一点点成型的过程。\n你喜欢的是“这是我搭出来的”那种反馈。\n软件工程也是一样。\n代码只是材料。\n系统才是作品。\n而 AI 现在正在替我们处理的，恰恰是很多最像“搬积木”的部分。\n它可以帮我们写重复代码。\n可以帮我们生成类型定义。\n可以帮我们补测试。\n可以帮我们解释 API。\n可以帮我们从文档里提取重点。\n可以帮我们把一个模糊想法快速变成一个可运行的原型。\n这些事情重要吗？重要。\n但它们不一定是我们最热爱的部分。\n很多时候，它们只是我们为了抵达创造本身而必须经过的琐碎步骤。\nAI 拿走的，可能是痛苦，而不是快乐 我知道“写代码很痛苦”这句话听起来可能有点冒犯。\n毕竟我们都是靠写代码吃饭的人。\n但如果你把现在和几年前对比，你会发现很多过去我们习以为常的痛苦，确实正在消失。\n以前你要接一个陌生库，可能要翻半天文档。\n现在你可以直接让 AI 总结用法，再给你一个例子。\n以前你要写一堆重复的 CRUD、DTO、mapper、validation。\n现在 AI 可以帮你生成大半。\n以前你遇到一个不熟悉的错误，要在 Stack Overflow、GitHub issue 和官方文档之间来回找。\n现在 AI 可以先给你一个大概方向，至少帮你缩小排查范围。\n以前你有一个想法，可能要花一整天搭初版。\n现在可能一个小时就能跑起来。\n这不是坏事。\n这意味着我们可以更频繁地接触到工程里真正让人兴奋的部分：\n设计。\n验证。\n迭代。\n调试。\n取舍。\n创造。\nAI 让很多底层的、重复的、机械的工作变得更便宜了。于是工程师能把更多精力放在“我要构建什么”以及“这个东西是否真的解决问题”上。\n对真正喜欢构建东西的人来说，这不是失去工作乐趣。\n这是工作乐趣被放大了。\n以前，你可能要写三天重复代码，才能看到一个功能成型。\n现在，你可能半天就能看到它跑起来。\n以前，快乐出现得慢。\n现在，反馈来得更快。\n如果你热爱的是真正的构建过程，那么 AI 给你的不是更少的快乐，而是更高频率的快乐。\nAI 没有“想把东西做出来”的冲动 这里还有一个很重要的区别。\n人和 AI 最大的不同，不只是推理能力、上下文长度、代码质量。\n还有动机。\n人会对自己正在做的东西产生兴趣。\n人会因为一个产品变好而兴奋。\n人会因为一个系统终于跑通而获得成就感。\n人会在下班之后还想着：“这个地方是不是还能再优化一下？”\n人会因为用户真的用上了自己做的东西而开心。\n这些东西背后是非常人类的奖励机制。\n多巴胺也好，成就感也好，创造欲也好，本质上都是人类继续构建东西的动力。\n但 AI 没有这个东西。\n至少从我目前的观察来看，AI 不会主动对任务之外的东西感兴趣。\n你让它写一个函数，它就写一个函数。\n你让它解释一个错误，它就解释一个错误。\n你让它生成一个组件，它就生成一个组件。\n但它不会真的在意这个产品有没有变好。\n它不会因为一个功能更顺而兴奋。\n它不会因为一个用户体验被改善而感到满足。\n它也不会在半夜突然想到：“我知道这个 bug 为什么会发生了。”\nAI 可以模拟很多表达，但它没有那种内在冲动。\n而软件工程从来不只是执行任务。\n它还包括判断什么值得做，什么不值得做。\n包括理解用户真正想要什么。\n包括在一堆不完美的约束里做取舍。\n包括对一个系统长期负责。\n包括你真的想把它变好。\n这些东西，很难只靠“生成代码”来替代。\n那些因为 AI 离开行业的人，我其实有点不理解 我看到过一些人说，因为 AI 的出现，他们觉得自己的技术不再有价值，所以选择离开软件行业。\n我能理解这种情绪。\n如果你过去的自我价值主要来自于“我能写别人写不出来的代码”，那 AI 的出现确实会带来很大的冲击。\n但我想问的是：\n你现在获得的快乐真的比以前更少了吗？\n如果你真正喜欢的是构建东西，那现在不是应该更有趣吗？\n你可以更快验证想法。\n你可以更快做出原型。\n你可以更快跨过陌生技术的门槛。\n你可以把过去不敢碰的领域拿起来试试。\n你可以一个人完成以前需要几个人才能推进的事情。\n这不应该是工程师最兴奋的时代吗？\n当然，前提是你喜欢的是创造，而不只是某种旧时代的稀缺性。\n如果一个人的价值感只来自于“我掌握了某种别人不会的语法、框架、配置和套路”，那 AI 确实会让这种优势变弱。\n但如果你的价值感来自于“我能把复杂问题拆开，把东西做出来，并且让它变得更好”，那 AI 只会让你的杠杆更大。\n人类的需求不会因为生产力提高而减少 还有一个经常被忽略的问题：\n生产力提高之后，人类真的会需要更少东西吗？\n历史上看，答案往往不是。\n人类的本质是贪婪的。这里的“贪婪”不是贬义，而是说：当一个需求被更快、更便宜地满足之后，人类通常不会停下来，而是会产生更多新的需求。\n我们当然不知道未来具体会长什么样。\n但如果回头看历史，你会发现，人类对“快乐”和“需求”的想象力，总是会被当时的生产力限制住。\n在古代，一个普通人最朴素的快乐，可能是吃饱肚子，在太阳底下休息一会儿，或者去河里游个泳。\n这已经很好了。\n但他大概永远想不到，几百年后的人类会拿着一块像砖头一样的发光屏幕，躺在沙发上，靠手指上下滑动短视频来获得快乐。\n这件事对古人来说可能完全无法理解。\n不是因为他们不聪明，而是因为那个时代还没有这样的技术条件，也就没有这样的需求形态。\n技术并不是单纯满足已有需求。\n很多时候，技术会创造出人们以前根本想象不到的新需求。\n当一种快乐变得更容易获得，人类不会停止在原地。\n我们会立刻开始寻找下一种快乐、下一种便利、下一种刺激、下一种更个性化的体验。\n所以我不太相信“AI 让软件更容易生产之后，人类就不需要那么多软件了”。\n恰恰相反。\n当软件变得更便宜、更快、更容易被构建出来，我们很可能会发现大量过去根本不值得被软件化的场景，突然都变得值得了。\n这也是为什么我认为，想象未来需求这件事，必须由人来完成，而不是 AI。\n因为 AI 很擅长在已有目标下给出路径。\n但它不会天然跳出当前时代的需求边界，替人类发明一种全新的快乐。\n想象一下，如果你在古代对 AI 说：\n“我想要快乐。我想吃一个面包。”\nAI 大概率会告诉你怎么更快获得面包。\n它可能会建议你怎么种小麦，怎么磨面粉，怎么改进烤炉，怎么让面包更容易保存。\n这些都很有用。\n但它不会突然告诉你：\n“你其实可以创造一个手机，然后躺在床上刷短视频来获得快乐。”\n因为这个需求不是从“更好地获得面包”自然推理出来的。\n它来自人类在新的技术条件下不断膨胀的欲望、想象力和无聊感。\nAI 可以优化一个已知目标。\n但新的目标，往往是人类自己长出来的。\n这就是为什么我不相信未来只需要 AI 来生成软件。\nAI 可以帮我们更快抵达一个目标。\n但“下一个目标是什么”，仍然需要人去感受、去想象、去定义。\n以前人们可能只需要有衣服穿。\n后来需要不同季节的衣服。\n再后来需要不同场合的衣服。\n再后来有了时尚、品牌、潮流、设计、功能性面料、运动服、礼服、户外装备。\n纺织机提高了生产力，但它没有让服装需求消失。\n相反，它让服装变得更便宜、更丰富，也让整个行业长出了更多分支。\n软件也一样。\n当软件变得更容易构建，人们不会说：“好了，我们需要的软件已经够了。”\n他们只会开始想要更多。\n更个性化的工具。\n更细分的 SaaS。\n更贴合自己工作流的自动化。\n更好的数据分析。\n更智能的界面。\n更低成本的内部系统。\n更多以前因为太贵而不值得做的小产品。\n以前一个公司可能只愿意为核心业务做系统。\n以后，当构建成本下降，很多边缘流程、内部流程、小众场景，也会变得值得被软件化。\n这意味着什么？\n意味着软件需求不会自然减少。\n它很可能会膨胀。\n因为当实现成本降低，人类的想象力和欲望会更快暴露出来。\n真正懂人类需求的，仍然是人 AI 可以生成代码。\nAI 可以生成界面。\nAI 可以根据需求文档写实现。\n但需求本身从哪里来？\n它来自人。\n来自用户的不满。\n来自业务里的低效。\n来自某个流程中反复出现的摩擦。\n来自一个人突然觉得：“这个东西不应该这么麻烦。”\n来自工程师、设计师、产品经理、用户之间不断碰撞出的想法。\nAI 可以帮助实现需求，但它不会天然产生人类的贪婪。\n它不会自己因为某个工具不好用而烦躁。\n它不会自己因为一个流程太慢而失去耐心。\n它不会因为一个产品体验很烂而想重做一遍。\n它不会因为“我就是想要一个更适合我的东西”而产生冲动。\n这些冲动是人的。\n而软件工程的起点，很多时候就是这些冲动。\n所以我不认为未来只需要 AI 和少数检查员。\n我认为未来会有更多人想要更多软件。\n而把这些需求变成真实系统的人，仍然会是工程师。\n只是工程师的工作方式会变。\n短期阵痛是真的，但长期需求也是真的 当然，我不想把这个问题说得太轻松。\nAI 确实会带来冲击。\n一些重复性的岗位会减少。\n一些只依赖模板化实现的工作会被压缩。\n一些公司会认为可以用更少的人做同样的事情。\njunior 工程师的入门门槛可能会变高。\nsenior 工程师也不能再只依靠过去经验躺在职位上。\n这些都是真的。\n现在很多公司裁员，也是真的。\n但每一次技术变革都会经历这样的阶段。\n一开始，新工具会让企业觉得：“我们是不是可以少雇一些人？”\n然后过一段时间，他们会发现：“既然生产力提高了，那我们是不是可以做更多东西？”\n当需求重新膨胀，岗位会以新的形式回来。\n可能不再是以前那种只写单一模块的工程师。\n可能不再是只做简单 CRUD 的工程师。\n可能不再是只等任务分配的 …","date":1779148800,"description":"\n这场争论，已经太熟悉了","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"297c624af4fc7a5e2d39014be36d4c99","permalink":"https://siqi-liu.com/zh/post/ai-%E4%B8%8D%E4%BC%9A%E6%9D%80%E6%AD%BB%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88-%E5%AE%83%E5%8F%AA%E4%BC%9A%E6%9D%80%E6%AD%BB%E6%88%91%E4%BB%AC%E6%9C%80%E8%AE%A8%E5%8E%8C%E7%9A%84%E9%82%A3%E9%83%A8%E5%88%86%E5%B7%A5%E4%BD%9C/","publishdate":"2026-05-19T00:00:00Z","relpermalink":"/zh/post/ai-%E4%B8%8D%E4%BC%9A%E6%9D%80%E6%AD%BB%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88-%E5%AE%83%E5%8F%AA%E4%BC%9A%E6%9D%80%E6%AD%BB%E6%88%91%E4%BB%AC%E6%9C%80%E8%AE%A8%E5%8E%8C%E7%9A%84%E9%82%A3%E9%83%A8%E5%88%86%E5%B7%A5%E4%BD%9C/","section":"post","summary":"这场争论，已经太熟悉了","tags":null,"title":"AI 不会杀死软件工程师，它只会杀死我们最讨厌的那部分工作。","tldr":null,"type":"post"},{"authors":null,"categories":["Technology","Ai"],"content":" 这几天，我一直在做一件看起来很简单、但实际非常麻烦的事情。\n让一个 Agent 看懂当前 3D 场景里的内容，然后替用户完成一些操作。\n比如：\n“帮我找出场景里的所有车，并把它们标注出来。”\n听起来不复杂，对吧？\nAgent 已经有截图工具。\n它可以调用视觉模型看图。\n它可以拿到屏幕上的 2D 坐标。\n它也有工具可以把 2D 坐标转换成 3D 场景坐标。\n最后，它还能调用 FrontEnd 的工具，在对应的位置创建 markup 或 entity。\n从能力上看，这条链路是通的。\n所以一开始我以为，这个问题主要是模型准不准、坐标准不准、2D 到 3D 的转换有没有偏差。\n但后来我发现，真正麻烦的地方不是这些。\n真正麻烦的是：Agent 有太多方法可以完成同一个任务。\n同一个问题，两条完全不同的路径 我先讲一个真实例子。\n我让 Agent 识别场景中的车，并把它们标注出来。\n其中一次，它走的是我预期中的路径：\n截取当前场景截图； 用视觉模型查看图片； 找出图片里的车； 返回车在图片中的 2D 坐标； 把 2D 坐标转换成 3D 场景坐标； 调用 FrontEnd 工具创建 markup。 这个流程不一定完美。\n有时候坐标会有偏差，有时候车会漏检，有时候 2D 到 3D 的位置不够准确。但至少这条路径是合理的。它是可以 debug 的，也是可以优化的。\n然后我换了一种问法。\n任务本质上还是一样：看图，找车，标注。\n但这一次，Agent 做了完全不同的事情。\n它先截图，然后下载图片，接着打开 Python sandbox，试图用 Python 的图像处理库去分析图片。问题是，它想用的库在运行环境里并不存在。于是整个任务失败了。\n这时候你会发现一个很有意思的问题：\n同一个 Agent。\n同一套工具。\n同一个目标。\n只是用户问法稍微变了一点。\n最后得到的是完全不同的工具路径，甚至完全不同的结果。\n这不是一个小问题。\n这就是 tool-using agent 在真实产品里最容易出现的不稳定性。\n问题不是工具不够，而是工具太多 很多人在做 Agent 的时候，会本能地想给它更多工具。\nAgent 不能看图？加一个 screenshot tool。\n需要理解图片？加一个 vision model。\n需要处理数据？加 Python sandbox。\n需要查数据库？加 query tool。\n需要操作界面？加 UI action tool。\n需要创建对象？加 FrontEnd tool。\n每加一个工具，系统看起来就更强一点。\n但工具一多，另一个问题就出现了：\nAgent 不再只是需要回答“我能不能做这件事”，它还需要回答“我应该用哪条路径做这件事”。\n而这个问题比想象中难得多。\n因为同一个任务，可能有很多条看起来都合理的路径。\n比如“识别截图里的车”这件事：\n它可以直接让视觉模型看图。\n它也可以下载图片后用 Python 分析。\n它可以先查场景里已有的实体。\n它也可以尝试用某个工具从页面里读 DOM 或 scene state。\n它甚至可能组合几种方法，做出一条非常绕的路径。\n站在人的角度，我们知道哪条路更合适。\n但站在 Agent 的角度，这些都只是“可用工具”。\n它不会天然知道哪条路径更稳定、哪条路径更慢、哪条路径在当前环境下大概率会失败。\n所以很多时候，Agent 失败不是因为它不聪明，也不是因为它缺少工具。\n它失败是因为它拥有太多选择。\n路径本身，变成了问题的一部分 这也是我后来意识到的一个关键点：\n对 tool-using agent 来说，执行路径本身就是产品质量的一部分。\n以前我们评估一个回答，可能主要看最终答案对不对。\n但在 Agent 系统里，只看最终答案是不够的。\n你还要看它是怎么得到这个答案的。\n它有没有用了不该用的工具？\n它有没有绕远路？\n它有没有调用一个不稳定的 runtime？\n它有没有把一个本来可以由 vision model 完成的任务，变成一个 Python 图像处理任务？\n它有没有生成大量看起来合理但实际上没有必要的中间步骤？\n这些都会影响系统的稳定性。\n而且更麻烦的是，如果 Agent 每次都走不同路径，你根本很难调试。\n今天失败是因为 Python 库不存在。\n明天失败是因为坐标格式不对。\n后天失败是因为它没有调用 2D-to-3D 工具。\n再下一次它又突然成功了，因为它刚好选了一条正确路径。\n这种系统很难维护。\n不是因为某一个工具坏了，而是因为工具组合空间太大了。\nSkill 真正解决的是什么 这时候 Skill 的价值就出现了。\n我对 Skill 的理解，不是“给 Agent 多写一段 prompt”。\n如果只是这样，Skill 很快也会变成另一种 prompt 垃圾场。\n真正有价值的 Skill，应该是一个经过验证的执行路径。\n它告诉 Agent：\n这类任务应该怎么做；\n应该先用哪个工具；\n再用哪个工具；\n中间结果应该是什么格式；\n什么情况下继续；\n什么情况下停下来；\n什么情况下不要乱编答案。\n换句话说，Skill 的作用不是让 Agent 变得更“自由”，而是让它在复杂工具集里更稳定。\n在刚才那个例子里，如果我们已经知道“识别车并创建 markup”的稳定路径是：\nscreenshot → vision model → 2D coordinates → 2D-to-3D conversion → FrontEnd markup\n那这条路径就应该被固化下来。\n下次用户再问类似问题时，Agent 不应该重新探索一次“我要不要用 Python”或者“我要不要下载图片”。\n它应该优先复用这条已经验证过的路径。\nSkill 应该记住路径，而不是结果 这里有一个很容易踩坑的点。\n当我们说“让 Agent 学会 Skill”时，很多人会想到缓存。\n比如把上一次工具返回的 JSON 存下来，把上一次截图分析结果存下来，把上一次生成的坐标存下来。\n但我觉得这不是 Skill 应该做的事情。\n工具结果通常是一次性的。\n截图会变。\n场景会变。\n用户会变。\n坐标会变。\n工具返回的数据也可能非常大。\n这些东西不适合存在 Skill 里。\nSkill 应该保存的是方法，而不是答案。\n更具体一点，它应该保存：\n这类任务应该使用哪些工具；\n这些工具应该按什么顺序调用；\n每一步需要什么输入；\n输出应该被整理成什么格式；\n什么时候认为结果可信；\n什么时候需要 fallback；\n什么时候应该告诉用户“不确定”。\n一句话说：\nSkill 不应该缓存工具结果，Skill 应该保存成功路径。\n这个区别很重要。\n缓存结果解决的是“上一次答案是什么”。\n保存路径解决的是“下一次该怎么稳定地完成类似任务”。\n人写 Skill 是有上限的 当然，最直接的办法是让工程师手写 Skill。\n这在一开始很好用。\n因为人知道系统怎么工作，也知道哪些路径可靠。\n比如我知道，在这个场景里，用 vision model 直接看截图，比让 Agent 下载图片再跑 Python 更符合产品设计。\n那我可以手写一个 Skill，告诉它遇到这种任务应该怎么做。\n但问题是，人不可能预判所有情况。\n用户的问法太多了。\n工具也会不断增加。\n产品场景也会变化。\n同一个功能，在不同环境下可能还会有不同限制。\n如果每增加一种情况，都要工程师写一段新的规则，最后系统就会变得很难维护。\nSystem prompt 会越来越长。\nSkill 会越来越多。\n规则之间可能互相重叠。\n有些规则过期了也没人知道。\n这和代码里的技术债其实很像。\n一开始每一条规则都有理由。\n半年之后，它们就变成了一堆没人敢删的历史包袱。\n所以我不希望把所有具体功能逻辑都塞进 system prompt。\nPrompt 应该定义高层边界和行为原则。\n具体任务的执行路径，应该沉淀在更接近工具层的 Skill 里。\n而且更进一步，我认为 Skill 不应该只能由人来写。\n让 Agent 自己生成 Skill 我的想法是：\n当 Agent 成功完成一个任务之后，它应该有机会回顾这次执行过程，并生成一个新的 Skill。\n这个 Skill 不保存结果，只保存路径。\n大概流程是这样的：\n用户提出一个任务。\nAgent 自己选择工具并完成任务。\n在最终回答之前，Agent 先做一次自我检查。\n如果它确认任务完成得不错，就总结这次成功路径。\n系统把这个路径保存成一个可复用的 Skill。\n下一次遇到类似任务时，Agent 可以先读取这个 Skill，再决定怎么调用工具。\n这里最重要的是“自我检查”。\n因为不是每一次成功都值得学习。\n有些成功只是运气好。\n有些路径虽然成功了，但非常慢。\n有些路径用了不该用的工具。\n有些结果看起来对，但其实没有验证。\n这些都不应该变成 Skill。\n所以在生成 Skill 之前，Agent 至少应该问自己几个问题：\n我有没有真正完成用户的请求？\n我有没有使用合适的工具？\n有没有更简单、更稳定的路径？\n我有没有在不确定的时候编造结果？\n这个路径未来是否还有复用价值？\n这个过程是否依赖某个一次性的上下文？\n只有当这些问题通过之后，才应该生成 Skill。\n这样可以避免把坏路径沉淀进系统里。\n自生成 Skill 长什么样 还是用前面的例子。\n如果 Agent 成功完成了“识别场景里的车并添加 markup”，它生成的 Skill 不应该长这样：\n“上次识别到了三辆车，坐标分别是……”\n这没用。因为下一次场景就变了。\n它应该更像这样：\n当用户要求标注当前场景中可见的车辆时： 1. 先获取当前 FrontEnd 视图的截图； 2. 使用视觉模型识别截图中的车辆； 3. 要求返回结构化的 bounding box 和中心点坐标； 4. 校验坐标是否在图片范围内； 5. 如果 confidence 太低，不要继续创建 markup； 6. 将中心点坐标传给 FrontEnd 的 2D-to-3D 工具； 7. 使用返回的 3D 坐标创建 markup； 8. 最终回答中说明标注是否成功，以及是否存在不确定性。这个 Skill 的价值在于，它把一条成功路径固定了下来。\n它没有保存具体车辆坐标。\n它没有保存截图。\n它没有缓存工具输出。\n它保存的是 Agent 应该如何完成这一类任务。\n这才是可复用的部分。\n运行时，不要把所有工具都丢给 Agent 有了 Skill 之后，下一步是运行时怎么用。\n我越来越觉得，一个复杂 Agent 系统不应该默认把所有工具都暴露给 Agent。\n这听起来好像是在限制能力，但实际是在提高稳定性。\n如果用户问的是“标注场景里的车”，系统可以先检索相关 Skill。\n这个 Skill 会告诉系统，这类任务通常只需要几类工具：\n截图工具；\n视觉模型；\n2D-to-3D 转换工具；\nFrontEnd markup 工具。\n那在这次执行里，就可以优先暴露这些工具。\n这样 Agent 就不会轻易跑去用 Python sandbox，也不会突然选择一条很奇怪的路径。\n这不是让 Agent 变笨。\n这是让它少走弯路。\n工具越多，越需要这种路径约束。否则，Agent 的“智能”会被工具组合空间消耗掉。\nSkill 也需要进化和淘汰 还有一个问题不能忽略。\n如果 Skill 的作用是固定一条工具路径，那它会不会也带来新的风险？\n答案是会的。\n因为一条路径一旦被固化下来，它就有可能被反复使用。\n如果这个 Skill 本身并不够好，或者它曾经有效但后来过时了，那么 Agent 就会一次又一次地重复这条不好的路径。\n这其实和代码里的技术债很像。\n一开始，某个实现可能是合理的。\n后来工具变了，模型变了，用户需求变了，环境也变了。\n但那条旧路径还留在那里，而且因为它被写成了 Skill，Agent 还会继续信任它。\n这时候，Skill 就不再是稳定性的来源，而会变成系统里的惯性。\n所以 Skill 不应该是一次生成、永久有效的东西。\n它需要有自己的更新机制。\n一种可能的方式是：在大多数情况下 …","date":1778112e3,"description":"用 Skills 稳定 Agent 的工具路径","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"bcce1b0c5c79c6d3a7c4e7b9c872fe94","permalink":"https://siqi-liu.com/zh/post/%E4%BD%A0%E7%9A%84-agent-%E5%9B%9E%E7%AD%94%E4%B8%8D%E4%B8%80%E8%87%B4-%E4%B8%8D%E6%98%AF%E5%9B%A0%E4%B8%BA%E5%AE%83%E7%AC%A8-%E8%80%8C%E6%98%AF%E5%9B%A0%E4%B8%BA%E5%AE%83%E6%9C%89%E5%A4%AA%E5%A4%9A%E8%B7%AF%E5%BE%84/","publishdate":"2026-05-07T00:00:00Z","relpermalink":"/zh/post/%E4%BD%A0%E7%9A%84-agent-%E5%9B%9E%E7%AD%94%E4%B8%8D%E4%B8%80%E8%87%B4-%E4%B8%8D%E6%98%AF%E5%9B%A0%E4%B8%BA%E5%AE%83%E7%AC%A8-%E8%80%8C%E6%98%AF%E5%9B%A0%E4%B8%BA%E5%AE%83%E6%9C%89%E5%A4%AA%E5%A4%9A%E8%B7%AF%E5%BE%84/","section":"post","summary":"用 Skills 稳定 Agent 的工具路径","tags":null,"title":"你的 Agent 回答不一致，不是因为它笨，而是因为它有太多路径。","tldr":null,"type":"post"},{"authors":null,"categories":["Technology","Ai"],"content":"在 Agent 系统刚开始流行的时候，我们其实默认认为：\n只要 Tool 足够完善，Agent 最终就会变成一个“会调用 API 的大脑”。\n后来我们发现，这个假设只在小规模场景里成立。\n一旦数据规模继续上升，尤其是在日志分析、RAG、监控系统、数字孪生这类高数据量环境里，纯 Tool 架构会开始出现非常明显的问题。\n最典型的一类任务是：\n“在整个数据集中筛选异常，并生成分析结果。”\n它的问题不在于推理复杂。\n返回的数据量会大到让上下文系统本身开始崩溃。\n我们后来在一个约 1 万实体的真实任务里，对比了三种方案：\n纯 Tool（标准 MCP） 纯 Code-as-MCP Tool + Code 的双轨执行模型 纯 Tool 会被上下文拖死。\n纯 Code 虽然准确，但整体延迟会迅速上升。\n最后真正稳定下来的，是一种双轨执行模型：\nTool 负责控制平面 Code 负责数据平面 中间通过一个很小的 “Context Off-Ramp” 做切换 这篇文章主要讲这个结构为什么会出现，以及我们后来是怎么把它真正跑起来的。\n1. 为什么纯 Tool 和纯 Code 都会开始失控 在上一篇里，我们已经区分过 MCP（基于 Tool）和 Code-as-MCP 的两个核心差异：\n动作是如何表达的 系统在什么时候做校验 这些差异在小规模场景下其实不明显。\n但系统复杂度一旦继续上升，问题会开始以一种非常不线性的方式暴露出来。\n我们后来反复遇到两种典型的“成本悬崖”。\nTool 的问题：上下文开始维护自己 最开始我们其实非常偏向 Tool。\n因为它天然适合：\nschema 校验 权限控制 UI 操作 状态修改 可审计动作 很多事情本来就应该是 Tool：\n删除对象 修改状态 缩容 deployment 执行交易 这些动作如果交给自由生成代码，其实风险会明显变高。\n但问题在于，大部分 Tool 系统默认假设返回结果不会太大。\n这在真实生产环境里经常不成立。\n例如：\n“列出当前数据集中所有异常实体。”\n这个任务本身并不复杂。\n真正的问题是它可能一次性返回几千、几万条记录。\n如果这些结果被原样序列化并注入上下文，事情会迅速失控。\n我们在实验里实际见过单次返回超过 50 万 token。\n真正麻烦的不是成本。\n而是整个 Agent 会开始进入一种很奇怪的状态：\n响应时间明显变长 tool loop 开始增加 prompt 约束逐渐丢失 原始任务开始漂移 后部数据开始覆盖前部目标 到后面你会发现：\n系统已经不是“在思考任务”。\n而是在努力维持上下文本身。\n这是我们后来观察到最明显的一个现象。\nTool 原本是为了“精确控制”设计的。\n但在高数据密度场景下，它反而会变成上下文的主要压力源。\nCode 的问题：很多时间花在“让代码跑起来” 后来我们尝试过另一个极端。\n既然 Tool 会把上下文撑爆，那是不是应该让 Agent 全部走代码？\n结果也并不理想。\n因为很多任务本身其实是原子操作。\n例如：\n“选中对象 #42”\n这种动作本质上只是一次确定性的状态修改。\n但如果让 Agent：\n生成脚本 调用 sandbox 执行 检查结果 修复错误 那整个系统会开始为“处理复杂数据的能力”付出额外成本。\n而这些成本很多时候和业务本身没什么关系。\n我们在纯 Code 架构里反复看到几类问题：\n代码生成本身耗时很长 dependency 问题开始增加 sandbox 调试循环很多 Agent 会为了小操作生成复杂逻辑 最后虽然结果准确率更高，但整体延迟明显上升。\n很多时间花在让代码终于能跑起来。\n这也是纯 Code Agent 很容易出现的问题。\n它们非常灵活。\n但很多原本只需要一个 Tool 调用的小操作，最后会被放大成一整个执行链。\n2. 后来我们把执行路径拆开了 做到后面，我们其实慢慢意识到\n控制类任务和分析类任务，本来就不属于同一种执行模式。\n于是后来整个系统被拆成了两条轨道。\nTool 控制路径 这一条路径专门负责：\nUI 操作 状态变更 单实体查询 小规模返回 高风险动作 这一层的目标非常明确：\n快、确定、可验证。\n所以这里保留强 schema、严格校验，以及有限可调用操作。\n本质上，它更像传统软件系统。\n只是调用者从“人”变成了 Agent。\nCode 分析路径 另一条轨道则专门负责：\n聚合分析 批量计算 大规模异常筛选 可视化 多步逻辑推导 这里我们反而主动放弃一部分约束。\n因为这些任务真正需要的是：\n处理复杂数据的能力。\n代码直接运行在沙箱环境中。\n它面对的是文件、DataFrame 和真实数据。\n而不是 Chat 上下文窗口。\n这是一个很重要的变化。\n因为数据一旦进入代码环境，Agent 就不再需要“记住全部数据”。\n上下文终于重新回到了控制层。\n而不是继续承担数据平面。\n3. 真正关键的是 Context Off-Ramp 真正让整个系统稳定下来的，其实不是“双轨”。\n而是：\n什么时候强制切轨。\n系统会持续监控一次 Tool 调用返回的数据规模。\n当返回结果接近 token 阈值时，编排器不会再继续把完整 JSON 注入上下文。\n取而代之的是三个步骤：\n中止注入 将结果写入 CSV / Parquet 返回文件路径和少量摘要信息 强制 Agent 改走 Code 轨道 这里最重要的一点是：\n这不是优化。\n而是一次强制执行切换。\nAgent 此时已经无法继续依赖 Tool 路径处理这些数据。\n它只能进入代码环境。\n后来我们内部一直把这个机制叫做：\nContext Off-Ramp。\n因为它本质上就像高速公路匝道。\n当上下文流量开始失控时，系统会强制把数据流导向另一条轨道。\n4. 三种方案的真实实验 后来我们拿一个真实任务做了完整实验：\n在 1 万实体上做全量异常筛选，并生成 PDF 报告。\n我们分别实现了三种方案。\n纯 Tool（Pure MCP） 这是最接近“经典 Agent”结构的一版。\n结果比我们预期差很多。\n总耗时约 13 分钟 AI 分析对话约 11 分钟 Tool 调用 7 轮 单次全量返回接近 51 万 token 为了避免上下文直接爆炸，我们最后不得不对 API 返回结果做截断。\n问题也从这里开始。\n因为一旦截断，异常筛选本身就不再完整。\n最终结果准确率只有约 23%。\n最明显的问题是：\n系统大量时间都花在“维持上下文”。\n而不是分析数据。\n这也印证了前面的观察。\n在高数据密度场景下，Tool 会开始从“控制接口”变成“上下文压力源”。\n纯 Code-as-MCP 第二版则完全反过来。\n所有事情都走代码。\n结果准确率明显提升。\n总耗时约 23 分钟 工具调用约 15 轮 异常分析准确率约 85% 但这里也出现了另一个问题。\n很多时间其实并不是花在分析。\n而是花在：\n修 sandbox 问题 调整依赖 修生成代码 retry 执行 最后整个系统虽然“聪明”，但明显太慢。\n尤其很多原本只需要一个 Tool 调用的小操作，也会被放大成完整代码执行链。\n双轨执行模型（Tool + Code + Context Off-Ramp） 第三版则是现在的结构。\n也是目前唯一真正稳定的一版。\n整个任务耗时约 4 分 30 秒。\n中间可以看到非常明显的：\nTool → Code\n切轨过程。\n其中几个关键节点非常有代表性。\nR2：\nentities_keyword_search 返回约 108,634 tokens。\n系统判定 oversized。\n数据被直接写入 CSV。\n上下文里只保留前 100 行指针信息。\nR3：\n进一步 refine 后，返回规模达到 512,675 tokens。\n再次触发 off-ramp。\n完整数据被写入文件。\nAgent 被强制进入 execute_data_analysis。\n后面的约 3.5 分钟里：\n所有数据处理都发生在 Python Sandbox。\n而不是 Chat 主线程。\n这是整个系统最关键的变化。\n因为原本 50 万级 token 的数据，最后实际上只变成：\n“一个文件路径 + 少量提示信息”\n等价于把 0.5M token 压缩成几十 token。\n最终结果：\n总耗时约 4 分 30 秒 分析准确率约 90% 没有发生上下文爆炸 做到这里时，我们其实已经很难再把它理解成“优化”。\n它更像是一种执行分层。\n5. 为什么这种结构会反复出现 虽然这篇文章的实验来自数字孪生场景。\n但这种结构几乎会自然出现在所有高数据密度 Agent 系统里。\n因为很多系统本来就同时存在两类任务：\n低数据量、高风险控制 高数据量、低风险分析 例如金融系统。\n下单、修改仓位、风控指令，本质上更像 Tool。\n因为这些动作必须：\n可验证 可审计 可限制权限 但策略回测、组合分析、历史数据推演，又明显更适合 Code。\nDevOps 其实也一样。\n例如：\n“在 1GB 日志里找到异常请求”\n天然适合代码分析。\n但：\n“重启 deployment”\n则必须是受约束的 Tool。\n这里的 off-ramp 很像：\n“日志查询超出上下文 → 写入文件 → 进入代码分析 → 再映射回少量控制命令”\nTool 和 Code 的关系，可能更像控制平面 vs 数据平面。\n而不是两种互相替代的 Agent 执行方式。\n6. 最后 这篇文章最后真正想表达的，并不是：\nTool 比 Code 好 或者 Code 比 Tool 更高级 而是：\n在开放、反馈驱动、数据规模跨度巨大的系统里，单一执行方式很难同时撑住控制平面和数据平面。\n控制层需要确定性。\n数据层需要处理复杂数据的能力。\n它们本来就不是同一种问题。\n真正可行的方法是承认这种执行差异。\n然后把它们放到适合的位置。\n","date":1769558400,"description":"在大规模 Agent 系统里，纯 Tool 容易被上下文拖垮，纯 Code 又会带来过高延迟。本文结合真实实验，介绍一种通过 “Context Off-Ramp” 在 Tool 与 Code 之间切换的双轨执行架构。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"136f7cd321f521d204356f7394b77928","permalink":"https://siqi-liu.com/zh/post/tool-%E7%94%A8%E6%9D%A5%E6%8E%A7%E5%88%B6code-%E7%94%A8%E6%9D%A5%E5%88%86%E6%9E%903/","publishdate":"2026-01-28T00:00:00Z","relpermalink":"/zh/post/tool-%E7%94%A8%E6%9D%A5%E6%8E%A7%E5%88%B6code-%E7%94%A8%E6%9D%A5%E5%88%86%E6%9E%903/","section":"post","summary":"在大规模 Agent 系统里，纯 Tool 容易被上下文拖垮，纯 Code 又会带来过高延迟。本文结合真实实验，介绍一种通过 “Context Off-Ramp” 在 Tool 与 Code 之间切换的双轨执行架构。","tags":null,"title":" Tool 用来控制，Code 用来分析(3)","tldr":null,"type":"post"},{"authors":null,"categories":["Technology","Ai"],"content":"上一篇里，我写了一次 MCP-as-Code 的重构尝试。\n它确实缓解了一部分上下文压力，但也暴露了另一个更隐蔽的问题：Code execution 改变了失败发生的位置。\n在一个交互式 Agent 系统里，这件事影响很大。\n这里先说明一下我说的“交互式 Agent 系统”是什么。\n它不是一个只生成文本的聊天机器人。\n它会真的操作系统：调用后端 Tool，读取返回结果，再控制前端场景。\n每一步执行后，系统都会返回一些反馈，比如查询结果、错误信息、前端回执，或者一部分成功、一部分失败的结果。\n后文里我会统一把这些叫做“反馈”。\n这也是为什么这篇文章会反复讨论一个问题：下一步动作为什么经常依赖上一步反馈。\n这篇文章主要讲这个边界：为什么 Tool 和 Code 不是同一类动作的两种写法，以及为什么有些 Agent 系统天然会变成多轮。\n有些 Agent 任务无法稳定地一步完成 我当时系统里很常见的一个任务是：\n找出异常实体，并在前端高亮。\n乍看起来，这像是一条很简单的流程。\n查询数据。\n过滤实体。\n发送 UI 指令。\n结束。\n但真实交互系统通常不会这么干净。\n更真实的流程往往是：\n先确认需要的字段是否存在。\n检查数据覆盖率是否足够。\n只对有效子集进行过滤。\n处理分页，或者只返回了一部分数据的情况。\n向前端发送高亮指令。\n读取前端是否执行成功的回执。\n如果只有一部分对象执行成功，再重试或降级。\n这里真正重要的是，每一步都依赖上一步返回了什么。\n字段不存在，下一步查询就会变。\n数据不完整，过滤方式就会变。\n前端拒绝了一部分操作，补救路径也会变。\n所以很多 Agent 任务很难被稳定压缩成一次调用。\n多轮并不总是说明 Agent 不够聪明。\n有时候是系统本身就需要反馈闭环。\nAgent 并不知道系统的完整状态。\n它只能看到每一步执行后的反馈。\n只要下一步动作依赖这些反馈，runtime 就天然会变成多轮。\nTool 和 Code 的失败发生在不同时间 当我开始从这个角度看系统时，Tool 和 Code 的差异就变得更清楚了。\n它们不只是表达能力不同。\n更关键的是：系统能在什么时候拒绝一个错误动作。\nTool-based execution 通常有一个有限的动作集合。\n每个 Tool 都有 schema。\n参数在执行前就已经确定。\n很多错误可以提前拦下来：类型不对、字段缺失、枚举值非法、权限不足。\n失败发生在真正执行之前。\n这让失败相对便宜。\nAgent 收到的是结构化错误，通常可以改参数，或者换一个 Tool。\n这也是为什么 Tool 仍然非常适合控制类操作。\n修改 UI 状态。\n选中对象。\n隐藏图层。\n更新已知字段。\n触发一个边界清晰的后端操作。\n这些动作需要窄。\n窄不是缺点。\n窄是它们安全、稳定、可预测的原因。\nCode execution 则不一样。\n动作不再是从一个小集合里选择。\nAgent 会生成一段程序。\n这段程序可以包含循环、分支、搜索、聚合、重试和中间状态。\n这让 Code 的表达能力强很多。\n对于大规模筛选、统计、批处理，或者任何需要遍历数据集的任务，这正是你想要的能力。\n但很多错误只有在程序真正跑起来之后才会出现。\n字段不存在。\n类型和模型预期不同。\nAPI 返回结构有细微差异。\n依赖不可用。\n前端操作只成功了一部分。\n到了这一步，系统已经进入 runtime execution。\nAgent 需要读错误、理解错误、重写代码，再执行一次。\n这是完全不同的 failure model。\n多轮系统会放大小成本 在一次性任务里，一个小失败就是一个小失败。\n但在多轮系统里，同样的成本会叠起来。\n每多一轮，都可能增加：\nlatency\ntoken 消耗\ncontext pressure\nretry 成本\n系统状态继续变化的机会\n这也是为什么一个单独看起来可以接受的设计，一旦放进交互循环里，就可能变得很贵。\n一段生成代码可能只需要几秒。\n一次结果总结可能只多几百 token。\n一次 retry 看起来也不严重。\n但如果一个任务本身需要五六轮反馈，这些小成本会很快叠起来。\n这是我第一次 MCP-as-Code 重构时低估的地方。\n回到异常实体筛选这个任务 在异常实体筛选这个任务里，这个差异非常明显。\n数据侧更需要 Code。\n实体可能有几千、几万条。\nschema 不一定完全稳定。\n某些字段可能缺失。\n有些过滤条件需要聚合或派生计算。\n把这些都塞进 LLM 上下文不是一个好选择。\nCode 才是更自然的执行环境。\n文件、DataFrame、循环、过滤和统计都应该在那里完成。\n但控制侧仍然更需要 Tool。\n一旦目标集合确定，前端操作就需要快速、有边界、可验证。\n高亮这些对象。\n隐藏这个图层。\n移动相机。\n应用这个样式。\n如果一个 schema-validated Tool 能直接完成这些动作，就没有必要再生成一段脚本。\n真正的分界线不是 MCP 还是 Code-as-MCP。\n而是两类工作。\n一类是数据处理。\n另一类是控制。\n数据处理更适合 Code。\n控制更适合 Tool。\n如果强行把两者塞进同一条执行路径，成本就会开始出现。\n验证时机成了真正的边界 后来我越来越觉得，validation timing 是一个更有用的视角。\nTool-based execution 更容易在执行前失败。\nCode-based execution 更容易在运行时失败。\n这个差异会改变整个成本结构。\n执行前失败通常比较便宜。\n运行时失败通常更贵。\n不是因为 runtime failure 一定不好。\n而是因为恢复需要再进入一轮循环。\nAgent 要检查发生了什么。\n系统要保留足够上下文来支持恢复。\n下一步动作可能要改变。\n在高交互系统里，这个恢复循环才是真正的成本。\n这也是为什么 Code 很适合低交互的分析任务。\n如果任务主要发生在数据集内部，Code 可以在分析路径里运行、失败、恢复、总结。\n用户不需要看到每个中间步骤。\n前端也不需要确认每一个操作。\n最后输出可以被压缩。\n但如果任务和 UI 状态、权限、前端部分执行、用户可见动作紧密绑定，晚失败就会变得非常昂贵。\n这也是为什么后面不能只看表达能力 经过那次 MCP-as-Code 重构后，我越来越觉得，单独比较 Tool 和 Code 的表达能力是不够的。\nCode 能表达更复杂的逻辑。\n但它也把很多错误推迟到了运行时。\nTool 的表达能力更窄。\n但它能在执行前拦住更多问题。\n如果一个系统本身是多轮的，这个差异会被不断放大。\n所以真正需要比较的不是：\n哪一种方式更强。\n而是：\n在当前任务里，失败应该尽量发生在什么时候。\n这也是下一篇会继续展开的问题：如果 Tool 和 Code 适合不同类型的任务，runtime 应该什么时候让 Agent 切换路径。\n最后 这部分工作里，对我最有用的经验其实很简单：\n有些 Agent 系统是多轮的，因为系统本身就是多轮的。\nAgent 只能看到部分反馈。\n系统会在动作之间继续变化。\n前端操作可能只有一部分成功。\n后端结果可能不完整。\nschema 也可能漂移。\n一旦这些情况存在，失败发生的时间就会变得和动作表达能力一样重要。\nTool 和 Code 都有用。\n但它们会以不同方式失败。\n而在一个高交互 Agent 系统里，它们在哪里失败，往往比它们能表达什么更重要。\n下一篇会继续讲这个切换问题。\n","date":1766793600,"description":"Tool 和 Code 不只是表达能力不同，它们也会在不同时间失败。本文解释为什么多轮 Agent 系统会放大这种差异。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"33e2bb80c897012df6dfefc6379035f3","permalink":"https://siqi-liu.com/zh/post/%E4%B8%BA%E4%BB%80%E4%B9%88-tool-%E5%92%8C-code-%E5%9C%A8-agent-%E7%B3%BB%E7%BB%9F%E9%87%8C%E4%BC%9A%E4%BB%A5%E4%B8%8D%E5%90%8C%E6%96%B9%E5%BC%8F%E5%A4%B1%E8%B4%A52/","publishdate":"2025-12-27T00:00:00Z","relpermalink":"/zh/post/%E4%B8%BA%E4%BB%80%E4%B9%88-tool-%E5%92%8C-code-%E5%9C%A8-agent-%E7%B3%BB%E7%BB%9F%E9%87%8C%E4%BC%9A%E4%BB%A5%E4%B8%8D%E5%90%8C%E6%96%B9%E5%BC%8F%E5%A4%B1%E8%B4%A52/","section":"post","summary":"Tool 和 Code 不只是表达能力不同，它们也会在不同时间失败。本文解释为什么多轮 Agent 系统会放大这种差异。","tags":null,"title":"为什么 Tool 和 Code 在 Agent 系统里会以不同方式失败（2）","tldr":null,"type":"post"},{"authors":null,"categories":["Technology","Ai"],"content":"最开始做这个系统时，我其实有一个很直接的目标：\n让 LLM 能理解当前场景，然后根据用户指令完成操作。\n用户说一句话，Agent 查数据，找到目标，再控制前端。\n但后来场景规模继续上升后，我慢慢意识到，问题不在于 Agent 会不会调用工具，而在于它根本不应该看到那么多数据。\n在我负责的数字孪生系统里，一个典型场景里经常会有上万个 entity。\n每个 entity 又会带着大量属性：状态、指标、业务字段、前端状态、实时数据。\n更容易被低估的是，很多时候甚至不需要属性，光是 ID 本身就已经足够给上下文带来压力。\n例如在某些场景里，可能同时存在上万个 building。\n仅仅是把这些 building ID 完整传给 LLM，用于后续筛选或控制，token 数量就已经非常夸张。\n更不用说每个 building 后面还会跟着几千 token 的属性数据。\n做到后面其实会越来越明显：\n任何试图把完整场景数据直接塞进 LLM 上下文，再让模型做决策的方案，在规模上都很难持续。\n这也是后来我开始尝试 MCP-as-Code 的原因。\n最开始的 Tool 模式其实很直接 重构之前，我使用的是一种比较直接的 MCP Tool 调用方式。\n系统会向模型提供两类 Tool。\n一类负责查询场景数据。\n另一类负责控制 3D 场景，比如上色、隐藏、变换、定位。\nLLM 在这里更像一个指挥器。\n它根据用户指令选择 Tool，拿到返回结果，再继续调用下一个 Tool。\n例如用户说：\n把所有建筑染红。\n最直接的执行过程是：\n调用 Tool 查询所有 building ID。 调用 Tool 对这些 ID 执行上色。 这个模式的优点非常明显。\n它快。\n它稳定。\n参数类型、schema、权限这些问题，大多可以在真正执行前就被代码验证拦下来。\n对于 UI 操作、状态修改、单实体查询这类任务，这个模型其实非常合适。\n问题是，它默认了一个前提：\nTool 返回的数据不会太大。\n在真实场景里，这个前提很快就不成立了。\n真正的问题不是查不到数据，而是数据不该进上下文 一开始我关注的是：\nAPI 能不能查到足够完整的数据？\n后来发现这个问题问错了。\n在这种规模下，更重要的问题是：\n查到的数据应该放在哪里？\n如果 Tool 把几千、几万条 entity 直接返回给 LLM，系统很快会进入一种很奇怪的状态。\n模型并不是在认真分析任务。\n它是在努力维持上下文。\n大量 token 被用来承载原始数据，而不是用于推理、决策或控制。\n这时 LLM 真正需要的其实不是“所有数据”。\n它需要的是处理数据的方法。\n也就是：\n给它一套逻辑，让它知道如何过滤、聚合、统计、提取结果。\n而不是让它吞下所有 entity，再在上下文里硬做筛选。\n这也是 MCP-as-Code 对我最有吸引力的地方。\n它把模型的角色从 Tool selector 推向了 logic writer。\n模型不再只是选择一个 API。\n它开始写处理逻辑。\nAnthropic 的 MCP-as-Code 看起来正好解决这个问题 后来我读到了 Anthropic 的文章《Code Execution with MCP》。\n它提出的思路很简单，但非常击中我当时的问题。\n不要把所有 MCP Tool 都以 JSON Schema 的形式直接塞给模型。\n把 MCP Server 看成一组可以被代码调用的 API。\n然后让模型生成 Python 或 TypeScript，在受控的 sandbox 中执行。\n模型最后只需要看到执行结果的摘要。\n对于大数据集，模型不需要看到一万行原始数据。\n它只需要看到前几行样本、统计结果，或者最终过滤出来的少量目标。\n这个思路几乎正好对应我当时遇到的上下文问题。\n所以我一开始对这次重构的预期非常明确：\n大规模过滤、聚合、统计都放到后台执行。\nLLM 只看结果。\n上下文压力降下来。\n模型从“工具选择器”变成“逻辑编写者”。\n从设计上看，这是一条很自然的路。\n我实际落地的是 Java + Python Sandbox 考虑到原有系统已经是 Java 核心服务，我没有推翻整个架构。\n最后加的是一层 Python Sandbox。\nJava 层继续负责：\nsession 管理 鉴权 API 调用 场景数据预过滤 前端通信 结果解析 例如在真正处理前，Java 会先向前端确认当前场景中存在的 ID，把不在场景里的数据过滤掉。\nPython Sandbox 负责执行 LLM 生成的代码。\n它通过 HTTP 和 Java 通信。\n代码可以调用 MCP API，也可以通过 WebSocket 控制前端。\n执行后返回 stdout、result、ui_events 和 errors。\n从执行模型上看，LLM 不再是直接调用 Tool。\n它进入了另一种循环：\n写代码 → 执行 → 读结果 → 再写代码\n这也是后面问题开始出现的地方。\n因为我一开始想象的是，一段脚本可以完成多步逻辑。\n但真实交互系统不是这样跑的。\n后来脚本开始被不断打断 我最初的设想是：\n让 LLM 生成一段 Python 脚本，把原来多个 Tool 调用合并起来。\n查询、过滤、聚合、控制前端，尽量在一次执行里完成。\n这样理论上既能减少上下文压力，也能减少多轮调用。\n但真实运行后，这个假设很快被打破。\n一个典型过程最后变成了这样：\nLLM 先生成一段 Python，用来查询或过滤 building ID。 Java / Python 执行完成，把结果返回给 LLM。 LLM 读取结果，重新判断下一步。 LLM 再生成第二段 Python，用来通过 WebSocket 控制前端。 前端执行操作。 前端返回执行结果。 LLM 再决定是否继续补救。 也就是说，脚本往往只执行了一小步，就必须停下来等下一轮反馈。\n这不是因为脚本写得不够好。\n而是因为交互式系统本身会不断产生新的状态。\n前端是否真的执行成功。\n哪些对象失败了。\n当前场景里是否还有这些 ID。\n权限、分页、异步结果有没有变化。\n这些都需要下一轮反馈。\n所以原本想象中的“一段脚本完成多步逻辑”，最后退化成了多轮串行往返。\n这件事对延迟的影响非常明显。\n简单操作反而变慢了 最典型的例子还是：\n把所有建筑染红。\n在原来的 MCP Tool 模式下，这类操作平均大约 2 秒。\n到了 MCP-as-Code 模式，经常会变成 10 秒以上。\n不是因为 Python 本身慢。\n真正的开销来自整个执行链：\nLLM 生成代码 Java 转发请求 Python Sandbox 执行 Java 解析 stdout / result / errors LLM 读取结果并判断下一步 LLM 生成下一段代码 前端执行动作 再返回结果 每一步单独看都合理。\n连在一起后，简单操作的延迟就被放大了。\n这也是这次重构最早暴露出来的问题之一。\nMCP-as-Code 确实降低了部分上下文压力。\n但它也把一些原本很短的交互动作，放进了一条更长的执行链里。\n失败开始出现在更晚的位置 另一个明显变化是失败发生的位置。\n在原有 MCP Tool 模式里，很多错误发生在执行前。\nJava 端有 JSON Schema Validator。\n参数类型不对，字段缺失，权限不够，很多问题都可以在真正执行前被拒绝。\n这类失败通常比较便宜。\nAgent 收到的是一个结构化错误。\n它可以改参数，或者换一个 Tool。\n但到了 Python Sandbox 后，很多错误会变成运行时错误。\n代码已经生成了。\nSandbox 已经启动了。\n脚本已经开始跑了。\n然后才发现字段不存在、类型不对、API 返回结构不符合预期，或者某个依赖不可用。\n这时返回给 LLM 的往往是一整段 stack trace。\n接下来 Agent 要做的事情变成：\n读错误。\n理解错误。\n重写代码。\n再次执行。\n如果下一次又遇到新的运行时问题，就继续循环。\n这并不说明 MCP-as-Code 不好。\n它只是让我第一次非常清楚地看到：\nMCP-as-Code 不只是把数据处理从上下文移到了 sandbox。\n它也改变了失败发生的位置。\n这次重构真正暴露的问题 这次尝试解决了一个真实问题。\nLLM 不应该吞下完整场景数据。\n对于大规模过滤、聚合、统计这类任务，把数据留在代码环境里，让模型只看摘要，确实更合理。\n但它也暴露了另一个问题。\n当一个任务不是离线分析，而是高频、交互式、强约束的系统操作时，Code execution 会带来新的成本。\n原来一次 Tool 调用能完成的事情，可能会变成：\n生成代码。\n执行代码。\n读结果。\n修代码。\n再次执行。\n再控制前端。\n再读取结果。\n这时上下文压力是下降了。\n但延迟、重试和运行时失败成本开始上升。\n这是我一开始低估的地方。\n我原本以为 MCP-as-Code 只是把数据处理从上下文搬到了 sandbox。\n后来发现，它同时也改变了整个执行路径。\n更准确地说：\n它改变了失败发生的位置。\n在原来的 Tool 模式里，很多错误可以在执行前被拦住。\n到了 Code 模式里，很多错误会等到程序真正跑起来之后才暴露。\n这个问题本身值得单独拆开讲。\n最后 这次 MCP-as-Code 重构最大的价值，不是它直接解决了所有问题。\n而是它让我看清楚了一个边界。\nLLM 不应该吞下整个场景。\n但也不是所有事情都应该交给代码执行。\n对于大规模数据处理，Code 是更自然的环境。\n但对于实时控制和强约束操作，Tool 仍然有很强的优势。\n真正的问题开始变成：\n为什么这两种方式会产生完全不同的失败成本？\n以及为什么在一个多轮 Agent 系统里，这种差异会被不断放大？\n下一篇我会先定义这个问题：\nTool 和 Code 不只是表达能力不同。\n它们也会在不同时间失败。\nReferences Anthropic Engineering: Code Execution with MCP\nhttps://www.anthropic.com/engineering/code-execution-with-mcp Model Context Protocol documentation\nhttps://modelcontextprotocol.io/ ","date":1766016e3,"description":"一次 MCP-as-Code 重构复盘：它缓解了上下文压力，但也放大了交互场景中的延迟和运行时失败成本。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"e09cb7d7420df97cf9d1c64ee36ebf4c","permalink":"https://siqi-liu.com/zh/post/%E4%B8%80%E6%AC%A1-mcp-as-code-%E7%9A%84%E9%87%8D%E6%9E%84%E5%B0%9D%E8%AF%95%E4%BB%A5%E5%8F%8A%E5%AE%83%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89%E6%8C%89%E9%A2%84%E6%83%B3%E5%B7%A5%E4%BD%9C-1-/","publishdate":"2025-12-18T00:00:00Z","relpermalink":"/zh/post/%E4%B8%80%E6%AC%A1-mcp-as-code-%E7%9A%84%E9%87%8D%E6%9E%84%E5%B0%9D%E8%AF%95%E4%BB%A5%E5%8F%8A%E5%AE%83%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89%E6%8C%89%E9%A2%84%E6%83%B3%E5%B7%A5%E4%BD%9C-1-/","section":"post","summary":"一次 MCP-as-Code 重构复盘：它缓解了上下文压力，但也放大了交互场景中的延迟和运行时失败成本。","tags":null,"title":"一次 MCP-as-Code 的重构尝试，以及它为什么没有按预想工作( 1 )","tldr":null,"type":"post"},{"authors":null,"categories":["Investment"],"content":"完整分析代码与原始数据处理过程：\nGoogle Colab Notebook:\nhttps://colab.research.google.com/drive/15dBsIpnSFBJzyruTeddNcWyrrCyVENL9?usp=sharing\n买入逻辑 回头看 2025 年，我大部分比较重要的买入，其实都可以总结为一个逻辑：Bet the Dip，也就是在市场情绪比较差、价格出现明显回调的时候买入。\n今年主要的建仓时间，集中在几个阶段。一个是四月份的大回调，当时主要和特朗普关税相关；一个是九月份，市场对 AI 和 SaaS 方向出现了一次比较明显的担忧；还有一个是十一月份，市场开始重新质疑 AI 相关公司的盈利能力，AI 板块也出现了一轮系统性回落。\n这些时间点的共同点是，短期情绪都不太好，市场对风险资产比较谨慎。但我自己一直比较相信 AI 带动美股的这条主线，所以在这些情绪低点，我反而更愿意去买入。\n从结果上看，截至年末，我的总已实现收益是 $2,171，交易胜率是 77.78%，单笔期望收益是 +$60.31，平均持仓周期是 36.7 天。\n这些数字不算特别夸张，但对我来说，它们说明今年的收益主要还是来自入场时间点的判断。不是靠某一笔特别幸运的交易，而是几次在情绪比较差的时候买入，最后获得了相对稳定的结果。\n做得比较好的交易 今年做得比较好的交易，主要是 Rocket Lab（RKLB）和 NVIDIA（NVDA）。\nRKLB 是一个比较典型的例子。我在相对低位建仓之后，没有很快卖掉，而是给了它比较长的时间。最后单笔收益接近 95%–99%，持仓周期也差不多在 100 天左右。\nNVDA 也是类似的情况。虽然没有吃到全部上涨，但在趋势展开之后，我还是保持了一定耐心，最后拿到了大约 37% 的收益。\n这两笔交易让我更明显地感受到，如果方向判断是对的，有时候最重要的不是频繁操作，而是给它足够的时间。\n亏损和问题 当然，今年也有很明显的失误，最典型的就是 GME 的 Put。\n这笔交易本质上是一次短期、高风险的投机。结果也很直接，单笔亏损大约 $584，基本接近 100% 亏损。\n我觉得这笔交易最大的问题，不是方向看错了，而是仓位没有控制好。方向看错在交易里很正常，但如果仓位太大，一笔投机交易就会对整体结果造成不成比例的影响。\n除了 GME 之外，还有一些亏损交易，比如 CRCL 和 AAPL。CRCL 大约亏损 18.7%，AAPL 大约亏损 10.8%。这两笔更像是正常交易过程中会遇到的误差，不算失控，但也提醒我，在趋势还没有完全确认的时候，仓位还是需要更克制一点。\n不过，今年的问题也不只是亏损本身。\n我可以在市场情绪很差的时候买入，但有时候在执行上会比较片段化。比如到了某个止盈点之后，我就直接止盈了，导致一些本来可以更长期持有的仓位，没有真正把趋势吃完整。\n另一方面，我在一些投机交易里承担了太大的风险。GME Put 就是最明显的例子。本来只是一个短期尝试，但因为仓位没有控制好，最后亏损对整体结果的影响被放大了。\n对 AI 的判断和下一步 关于 AI 的长期判断，我到现在为止没有太大变化。\n短期来看，2C 场景可能确实还有不少限制。很多产品有想象力，但真正大规模、稳定、可持续地商业化，还需要时间。\n但 2B 方向，我觉得空间还是非常大的。不管是算力、基础设施，还是企业级应用，AI 都还处在比较早的渗透阶段。\n再结合我自己从事 AI 应用相关工作的背景，我能比较直接地感受到，AI 确实已经在 SaaS 应用里面帮助人提升效率，而且我觉得这会是之后很重要的趋势。所以我更倾向于认为，AI 比较系统性的商业兑现，可能会在 2026 年以后逐步出现。\n这也是为什么今年几次回调的时候，我愿意承受一定波动去布局。因为对我来说，短期市场情绪并没有改变我对这个方向的长期判断。\n所以新的一年，我最想改进的不是完全改变“投什么”，而是更认真地优化“怎么投”。\n首先，我需要更清楚地区分核心持仓和投机交易。\n核心持仓是基于中长期判断建立的仓位，比如 AI 基础设施、企业级应用这些方向。对于这类仓位，我应该给它更多时间，而不是太早因为短期波动或者阶段性止盈就离场。\n投机交易则完全不一样。它本质上是短期机会，仓位必须足够小。尤其是期权这类可能快速归零的工具，一开始就要假设最坏情况可能发生，然后再决定自己能不能接受这个损失。\n其次，我希望减少决策次数。\n今年复盘下来，我感觉真正有价值的交易并不需要很多。几笔方向正确、仓位合理、持有时间足够的交易，就已经可以贡献大部分收益。\n反而是一些临时起意的交易，容易消耗注意力，也容易带来不必要的亏损。\n所以接下来，我更希望自己做更少但质量更高的决策。对于真正有把握的中长期判断，给它足够的时间和空间；对于短期想法，就控制仓位，不让它影响整个组合。\n如果总结一下，2025 年对我来说最大的收获，是我看到了自己判断里有效的部分，也看到了执行和仓位管理上的问题。\n我在情绪低点买入 AI 相关资产这件事，整体上是有效的。但我也需要避免两个问题：一个是太早止盈，让好的判断没有被充分放大；另一个是在投机交易里承担过大的风险，让错误被不成比例地放大。\n新的一年，我希望重点不再只是判断方向，而是建立一套更稳定的交易结构。让正确的判断有时间发挥作用，同时让错误始终保持在可承受范围内。\n","date":1765756800,"description":"2025 年度投资回顾与反思：Bet the Dip","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"8c2920a2249f670d8b5f89a60cea1073","permalink":"https://siqi-liu.com/zh/post/2025-%E5%B9%B4%E5%BA%A6%E6%8A%95%E8%B5%84%E6%80%BB%E7%BB%93/","publishdate":"2025-12-15T00:00:00Z","relpermalink":"/zh/post/2025-%E5%B9%B4%E5%BA%A6%E6%8A%95%E8%B5%84%E6%80%BB%E7%BB%93/","section":"post","summary":"2025 年度投资回顾与反思：Bet the Dip","tags":null,"title":"2025 年度投资总结","tldr":null,"type":"post"},{"authors":null,"categories":["Technology","Optimization"],"content":"我们做传统的 React 开发时候习惯了数据驱动 UI，state 变了 UI 就变。\n但是，当我们的数据源不是用户输入的表单，而是一个每秒钟都在疯狂变化的 3D 引擎（如 Cesium）时，传统的 React 模式会直接导致网页崩溃。\n今天我想分享一下，我们是如何给狂奔的 Cesium 引擎“套上缰绳”，让它和 React 和平共处的。\n1. 核心冲突：UI 线程 vs 渲染循环 Web 开发中存在两种截然不同的更新模式：\nReact (UI Thread)：声明式，按需更新。追求准确性，任何状态变更都会触发 Diff 和 Re-render。 3D Engine (Game Loop)：命令式，60FPS 持续刷新。状态变更极其频繁（Loading/Culling/Moving）。 当 3D 场景加载大量模型时，Cesium 会在短时间内发出成百上千个“节点添加”事件。如果采用朴素的 onEvent -\u0026gt; setState 模式，主线程会被瞬间阻塞，导致页面无响应。\n2. 抽象模型：场景图投影与同步 为了解决速率不匹配的问题，我们确立了核心观点：React 中的 Tree State 不再是 Source of Truth，它只是 3D 世界的一个低频投影。\n我们构建了如下的架构模型：\nflowchart LR A[\u0026#34;3D Engine (Cesium)\u0026#34;] --\u0026gt;|High-frequency events| B[\u0026#34;TreeStateManager / StateManager\u0026#34;] B --- D[(\u0026#34;Node Store: Map(ID -\u0026gt; Node)\u0026#34;)] B --- E[(\u0026#34;Pending Updates Buffer: Dedup + BatchUpdate\u0026#34;)] B --- F[(\u0026#34;View Projection: Graph -\u0026gt; Flat Array\u0026#34;)] B --\u0026gt;|Low-frequency view updates| C[\u0026#34;React UI (Virtual List)\u0026#34;] C --- G[(\u0026#34;Render viewport only: O(H)\u0026#34;)]我们引入了一个脱离 React 生命周期独立存在的类 TreeStateManager，它不仅仅是一个数据缓存，而是整个系统的可信源 (Source of Truth) 和流量阀。\n它承担了三个关键职责：\n1. 状态持有与 O(1) 索引 (State Holding) 它在内存中维护了一个完整的节点数据库。\n利用 Map\u0026lt;ID, Node\u0026gt; 建立全量索引，确保任何一种通过 ID 查找节点的操作（如：根据 Cesium 点击事件反查树节点）都是 O(1) 复杂度。 维护节点的持久化状态（Opened/Checked），这些状态独立于 UI 存在，即使组件卸载重装，状态依然保留。 2. 流量整形 (Traffic Shaping) 面对 3D 引擎可能瞬间涌入的成千上万个状态变更事件（Add/Remove/Update），StateManager 充当了防洪堤的角色。\n去重 (Deduplication)：同一毫秒内对同一个节点的多次修改（如：先变红再变绿）只保留最终态。 缓冲 (Buffering)：并不是来一个事件就通知一次 UI。而是维护一个 pendingUpdates 队列，通过 batchUpdate 机制，将高频的细碎更新合并为一次低频的 View Update。 3. 视图投影 (View Projection) “怎么看数据”由它决定。它根据当前的 SortType（如：按 CAD 结构排序、按实体类型排序），动态地将内存中的非线性数据（Graph），实时计算出当前 UI 所需的那一个线性数组（Flat Array）。\n这意味着：底层数据只有一份，但“视图”可以有无数种，且切换视图只是重新计算一次投影，成本极低。\n3. 关键实现策略 针对 3D 场景常见的深层嵌套，我放弃了直观的“递归组件”写法。\n在早期实验中我发现，当树的深度（Depth）增加且节点数量庞大时，React 的递归组件会带来巨大的性能惩罚：\n调用栈溢出风险：过深的组件树会让 JS 引擎的调用栈压力倍增。 Diff 开销指数级上升：React 在协调（Reconciliation）层级很深的组件树时，其 Diff 算法的开销会显著增加，导致帧率断崖式下跌。 因此，我们在内存中维护一个扁平数组 flatNodeArray，通过 depth 属性标识层级。\n优势：虚拟列表（Virtual List）可以直接消费这个数组，React 只需要渲染视口内的几十个 div，渲染复杂度与总数据量（N）彻底解耦，只与视口高度（H）相关 O(H)。 操作：节点的展开/折叠，仅涉及数组过滤，不再需要昂贵的 DOM 树重绘。 策略 B：异步时间分片 (Time Slicing) 这是解决“假死”的关键。不仅使用批处理（Batching），还将构建任务拆分为多个微任务。\n// 伪代码逻辑 while (queue.length\u0026gt;0) { process(queue.splice(0,100)); // 处理一小批 awaitnextTick(); // 让出主线程，允许 UI 响应交互 }4. 数据结构带来的功能实现上的性能红利 架构的选择往往不仅解决了当下的性能问题，简化后续功能的实现。最典型的例子就是 Shift+多选 功能。\n在旧版本（基于递归的树）中，当我们在一个包含 2万个节点的 4 层深树上执行“范围全选”时，浏览器会直接假死 10 秒左右。因为算法不得不在深层嵌套的 DOM 树中疯狂递归，查找路径和状态。\n但在我们的扁平化数组 (Flat Array) 架构下，这道难题迎刃而解：\n// 伪代码：在扁平数组中实现范围选择 constrangeSelection= (startId,endId)=\u0026gt; { conststartIndex=nodePositionMap.get(startId); constendIndex=nodePositionMap.get(endId); // 无论树结构多复杂，视觉上的范围就是数组索引的切片 returnflatNodeArray.slice( Math.min(startIndex,endIndex), Math.max(startIndex,endIndex)+1 ); };另外一个例子是树形控件中最复杂的状态莫过于 Checkbox 的级联更新（全选/反选）。\n在传统递归树中，勾选一个包含 2万个子节点的父级，意味着触发 2万次 React 组件的 Re-render，这绝对是性能灾难。而在我们的架构中，这被简化为纯内存操作：\n索引查找：利用 Map 瞬间定位所有 26,000 个子节点 ID。 批量修改：直接更新数据 Store，不触碰 DOM。 按需绘制：VirtualList 只重绘屏幕可见的 20 行。结果：无论级联多少节点，渲染开销恒定为 O(1)。 4. 实验数据与性能验证 为了验证架构的可扩展性，我们在中等规模和高负载规模（两种真实场景下进行了性能埋点测试。\n测试环境：Chrome / M2 Chip\n对比组：中级场景 (7,000 Nodes) vs 高级场景 (68,500 Nodes)\n以下是三大场景的实测数据对比：\n实验一：视图构建与渲染性能 (Build \u0026amp; Render) 这是最基础的性能指标，衡量这套“扁平化 + 时间分片”架构能否扛住大数据量的冲击。\n关键指标 中级场景 (7k Nodes) 重载场景 (6.8w Nodes) 架构解读 Tree:Flatten(层级拍平) 0.8 ms 4.3 ms 核心验证：将数万个节点的层级关系重组为线性列表仅需 4ms。证明算法的时间复杂度呈现完美的线性增长 (Linear Scalability)，且远小于 16ms/帧的安全线。 Tree:FullBuild(全量构建) 290 ms 2,804 ms 虽然数据量翻了 10 倍，耗时也线性增加。但得益于时间分片 (Time Slicing)，这 2.8秒被分散在数百个 Event Loop Tick 中，界面在此期间依然保持完全可交互，无任何卡顿。 undefined\n实验二：交互响应速度 (Shift+Select Range) 这是对架构设计最极致的考验。我们在 3D 场景中进行“隐藏式全选”测试：用户只选中了可视区域的几十个文件夹，但其内部包含了数万个折叠的 3D 实体。\n操作场景 传统递归树 (估算) 本架构 (实测) 提升幅度 选中 80,000 个实体 ~10,000 ms(浏览器假死) 263 ms ~40 倍提升 undefined 解读：在过去，计算两个节点之间的“视觉范围”需要复杂的树遍历递归，极易导致主线程锁死。而在扁平数组中，这退化为一个简单的 Array.slice 操作（以及随后的 ID 收集），即便是处理 8万个对象，也能在 260ms 完成。\n实验三：状态级联更新 (Checkbox Cascade) 测试用户点击根节点“全选”时，系统递归更新所有子孙节点状态的性能。\n测试指标 数据规模 耗时 结果 Tree:CheckCascade 26,419 个节点 72.6 ms 实时响应 undefined 解读：得益于 Map 索引 (O(1)) 和内存状态操作，我们可以在 70多毫秒内完成 2.6万个组件的状态同步。对用户而言，这就是“即点即亮”的实时反馈。\n这三组实验数据足以证明了：当数据规模从 7千 增长到 7万（10倍压力）时，系统的核心性能指标依然保持在线性可控范围内，没有出现指数级的性能崩塌。\n5. 总结 在处理 3D 引擎与 React 结合的复杂工程中，我们往往容易陷入怪圈：试图用更复杂的 React 技巧（memo, useMemo）去修补性能漏洞。\n但这套架构的实践告诉我们：性能问题的终极解法，往往不在代码层面的修修补补，而在于底层数据逻辑的重构。\n通过引入中间层架构，我们将狂暴的 3D 渲染循环与安静的 UI 线程隔离开来；通过数据降维，我们将 O(N) 的 DOM 操作降解为 O(1) 的数组操作。\n这不仅解决了 Cesium 场景树的性能瓶颈，也为任何海量即时数据可视化（如股票行情、日志监控、复杂表格）提供了一套通用的架构范式：\n脱离框架思考：不要被 React 的声明式模型束缚，敢于在 Side Effect 中管理自己的数据流。 拥抱最终一致性：在人眼无法感知的毫秒级缝隙里，用“批处理”和“时间分片”换取系统的吞吐量。 数据结构致胜：在面对数万级节点时，选择 Flat Array 还是 Recursive Tree，这一个决定可能比写 1000 行优化代码更管用。 希望这套“驯服狂奔引擎”的经验，能为您解决类似的高频同步难题带来一点启发。\n","date":1759276800,"description":"一套适用于海量即时数据的同步范式：中间层隔离高频数据源，React 只消费可视窗口的线性投影。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"04b6d2f114faa30f681ea8e2b5c2ee56","permalink":"https://siqi-liu.com/zh/post/react-%E7%8A%B6%E6%80%81%E4%B8%8E-3d-%E5%BC%95%E6%93%8E%E7%9A%84%E9%AB%98%E9%A2%91%E5%90%8C%E6%AD%A5%E6%9E%B6%E6%9E%84/","publishdate":"2025-10-01T00:00:00Z","relpermalink":"/zh/post/react-%E7%8A%B6%E6%80%81%E4%B8%8E-3d-%E5%BC%95%E6%93%8E%E7%9A%84%E9%AB%98%E9%A2%91%E5%90%8C%E6%AD%A5%E6%9E%B6%E6%9E%84/","section":"post","summary":"一套适用于海量即时数据的同步范式：中间层隔离高频数据源，React 只消费可视窗口的线性投影。","tags":null,"title":"React 状态与 3D 引擎的高频同步架构","tldr":null,"type":"post"},{"authors":null,"categories":["Technology","Ai"],"content":"构建 MCP：我们如何把 6,000 个 Token 压缩到 500 个\n在构建 MCP2 的过程中，我们遇到的最大挑战之一，就是 Token 消耗过高。\n在最初的版本中，还没开始对话，模型的上下文就已经“爆表”了。\n具体来说：\n系统提示（System Prompt）：约 2,000 个 token 工具定义（42 个工具）：约 4,000 个 token（包括 Digital Twin 工具和 Navigator 工具） 总计：约 6,000 个 token，仅仅是初始化阶段 而当上下文越长，模型往往会变得越“笨”：\n响应变慢、推理变弱、成本上升、准确率下降。\n一、使用 Embedding 动态加载工具\n为了解决这个问题，我们不再在启动时加载全部 42 个工具。\n我们构建了一个基于 Embedding 的工具检索系统，让 MCP 可以在需要时 动态导入工具，大幅减少了默认上下文的体积。\n二、引入 Tool Registry（工具注册表）\n我们还引入了一个 工具注册表机制（Tool Registry），通过两个简单的元工具实现：\ndiscover_tool：列出所有可用工具的名称和简短说明 describe_tool：按需加载某个工具的详细定义（包括输入输出和示例） 这样，模型只在真正需要使用某个工具时才会加载其定义，而不是一次性加载所有工具描述。\n三、统一工具接口，减少重复定义\n我们发现很多工具的功能高度重叠。于是我们：\n将所有数据库相关的工具（如 entity_query、entity_phrase_query、entity_attribute_filter）合并为一个 DSL 风格的查询工具：digital_twin.query。 将所有与前端交互的工具合并为单一接口 navigator.patch，用于统一处理 UI 更新。 这种统一极大减少了工具定义的冗余，也让模型的调用逻辑更清晰。\n四、控制上下文长度与 Token 预算\n我们为每轮对话设置了 Token 预算：\n保留最近的 前 4 条和后 4 条消息； 当总 token 超过 6,000 时，自动 摘要中间部分，压缩上下文。 同时，我们重新设计并简化了工具文档格式，让描述更紧凑统一，不再使用冗长的 JSON Schema 或大段示例。\n五、规划器与执行器分离\n在 MCP2 中，我们将系统拆分为：\n轻量级规划器（Planner）：只负责生成宏观步骤，Prompt 不超过 300 token； 执行器（Executor）：按需加载工具定义并执行操作。 这种架构让系统更加模块化，也进一步降低了每轮对话的上下文占用。\n六、优化结果：更快、更便宜、更聪明\n经过这一系列优化：\n工具相关 Token 从约 6,000 → 500 系统提示 Token 从约 1,000 → 400 最终，模型的响应速度明显提升，推理更稳定，成本也更低。\n七、有趣的“格式模仿”现象\n在实践中我们还发现一个非常有趣的现象：\nAI 会模仿你提供的输出格式。\n例如：\n如果你提供 JSON 示例，它之后的回复大多会自动保持 JSON 格式； 如果你使用点号语法（如 config.value.max），它也会持续用这种风格。 这种“格式锚定（Format Anchoring）”现象，让模型在多轮对话中保持更强的一致性和风格稳定性，也让工具调用更加顺畅。\n八、结语\n优化 Token 使用，不仅仅是为了降低成本，\n更重要的是让系统 更清晰、更快速、更智能。\n通过动态加载工具、统一接口、压缩上下文、严格控制 Token 预算，\n我们让 它变得更加有效和快速。\n","date":1751328e3,"description":"如何把 6,000 个 Token 压缩到 500 个","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"6fe2897c85b8e0af80383d9a7f45951d","permalink":"https://siqi-liu.com/zh/post/%E5%88%9D%E6%AD%A5%E5%B0%9D%E8%AF%95%E6%9E%84%E5%BB%BA-mcp-%E5%B7%A5%E5%85%B7%E7%9A%84%E7%BB%8F%E9%AA%8C/","publishdate":"2025-07-01T00:00:00Z","relpermalink":"/zh/post/%E5%88%9D%E6%AD%A5%E5%B0%9D%E8%AF%95%E6%9E%84%E5%BB%BA-mcp-%E5%B7%A5%E5%85%B7%E7%9A%84%E7%BB%8F%E9%AA%8C/","section":"post","summary":"如何把 6,000 个 Token 压缩到 500 个","tags":null,"title":"初步尝试构建 MCP 工具的经验","tldr":null,"type":"post"},{"authors":null,"categories":["Investment","Blockchain"],"content":"Circle上市后，股价一度从31美元发行价上涨到接近300美元，随后又出现明显回调。市场对它的关注，主要来自两个因素：稳定币行业的增长预期，以及美国稳定币监管框架逐渐清晰。\n我的判断是：Circle不是没有价值，但当时约400亿美元的估值已经很充分地反映了未来增长预期。这个价格更适合看作对未来三到五年业务扩张的提前定价，而不是对当前盈利能力的定价。\n1. Circle的核心业务是什么？ Circle发行USDC稳定币。用户存入1美元，Circle发行1枚USDC，并承诺维持1:1兑换。\nCircle拿到美元后，会将储备资金主要配置到美国国债和逆回购等低风险资产上，从中获得利息收入。用户持有USDC本身通常不会获得利息，因此这部分利息收入成为Circle的主要收入来源。\n所以，Circle的盈利主要取决于两个变量：\n第一，USDC流通量有多大。\n第二，美国短端利率处在什么水平。\nUSDC规模越大，Circle可投资的储备资金越多。利率越高，储备资产产生的收益越多。\n2. 为什么市场愿意给Circle高估值？ 市场看重的不是Circle当前利润，而是它可能参与未来数字美元支付和清算体系。\n稳定币可以实现全天候结算、跨境转账和链上支付。传统银行清算系统存在时间限制，跨境支付成本也较高。如果稳定币未来进入更多支付、结算和机构金融场景，Circle作为合规稳定币发行方，确实有一定先发优势。\n当时市场对Circle的乐观预期大致包括：\n稳定币总市场从约2500亿美元增长到2万亿美元。\nUSDC继续维持20%到25%左右市场份额。\n合规稳定币获得机构和监管认可。\nCircle继续与Coinbase、BlackRock等重要合作方保持关系。\n这些假设如果同时成立，Circle未来的业务规模确实可能明显扩大。\n3. 400亿美元估值需要什么条件支撑？ 可以反过来做一个简单测算。\n假设Circle市值为400亿美元，未来市场愿意给它25倍市盈率，则它需要约16亿美元净利润。\n如果按4%的年化利差、10%的净利润率计算：\nUSDC所需流通量 = 400亿美元 ÷（4% × 10% × 25）\n结果约为4000亿美元。\n这意味着，当时400亿美元估值隐含了一个前提：USDC未来流通量需要从约600亿美元增长到4000亿美元左右。\n这个目标并非完全不可能。如果全球稳定币市场未来达到2万亿美元，USDC保持20%市场份额，就对应4000亿美元流通量。\n但问题在于，这个估值已经提前计入了较理想的增长路径。\n4. 这个估值的风险在哪里？ 第一个风险是利率。\nCircle当前盈利高度依赖短端利率。如果美联储进入降息周期，储备资产收益率下降，Circle的收入会受到直接影响。\n第二个风险是净利率。\nCircle并不是把利息收入全部留下。它需要承担合规成本、审计成本、运营成本，还要向渠道和合作方支付分成。尤其是Coinbase在USDC分发中扮演重要角色，Circle历史上支付了较高的交易和分发成本。\n第三个风险是竞争。\nTether仍然占据稳定币市场最大份额。PayPal、Stripe、银行联盟也可能进入稳定币市场。合规是Circle的重要优势，但不一定是长期独占优势。\n第四个风险是市占率。\n400亿美元估值的合理性，很大程度上取决于USDC能否在未来更大的稳定币市场中维持当前份额。如果稳定币市场扩大，但USDC份额下降，Circle的收入增长就会低于市场预期。\n5. Circle与Coinbase的关系 Circle和Coinbase之间的合作是理解Circle利润结构的重要部分。\nUSDC早期增长离不开Coinbase的分发能力和用户基础。稳定币发行之后，最关键的问题是流动性。如果没有大型交易所、钱包和做市商支持，很难形成规模。\n但这种合作也带来成本。Circle需要与Coinbase分享相当一部分收入。对Circle股东来说，这意味着USDC规模增长并不必然完全转化为Circle利润增长。\n如果Coinbase长期合作，Circle可以继续获得重要渠道支持。如果Coinbase未来推出自有稳定币，或者重新谈判合作条款，Circle利润结构可能发生变化。\n6. 我的判断 我当时的判断是：Circle可以被视为稳定币赛道中最重要的合规标的之一，但400亿美元估值已经偏乐观。\n这个估值需要几个条件同时成立：\nUSDC流通量未来几年增长到4000亿至5000亿美元。\n美国利率不能快速大幅下降。\nCircle净利润率维持在接近10%的水平。\nCoinbase继续合作，且不削弱USDC的分发地位。\n合规优势继续帮助Circle获得机构采用。\n这些条件并非没有可能，但任何一个条件低于预期，估值都需要重新调整。\n所以我的结论是：\nCircle不是一个没有价值的公司。它所在的稳定币赛道有真实需求，也有长期增长空间。但在约400亿美元市值附近，股价已经反映了较多未来预期。对于没有早期持仓的投资者，这更像是一个需要等待验证的高预期资产，而不是一个明显低估的抄底机会。\n更冷静的做法，是继续观察三个指标：\nUSDC流通量是否持续增长。\nCircle净利率是否改善。\nCoinbase合作关系是否稳定。\n如果这三点都向好，Circle的高估值才更容易被证明合理。否则，股价回调并不是意外，而是预期回到现实的过程。\n","date":1751155200,"description":"我的判断是：Circle不是没有价值，但当时约400亿美元的估值已经很充分地反映了未来增长预期。这个价格更适合看作对未来三到五年业务扩张的提前定价，而不是对当前盈利能力的定价。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"372f65f2f593b1c5505c1d193761f2ed","permalink":"https://siqi-liu.com/zh/post/circle%E6%9A%B4%E6%B6%A8%E5%90%8E400%E4%BA%BF%E7%BE%8E%E5%85%83%E4%BC%B0%E5%80%BC%E6%98%AF%E5%90%A6%E5%90%88%E7%90%86/","publishdate":"2025-06-29T00:00:00Z","relpermalink":"/zh/post/circle%E6%9A%B4%E6%B6%A8%E5%90%8E400%E4%BA%BF%E7%BE%8E%E5%85%83%E4%BC%B0%E5%80%BC%E6%98%AF%E5%90%A6%E5%90%88%E7%90%86/","section":"post","summary":"我的判断是：Circle不是没有价值，但当时约400亿美元的估值已经很充分地反映了未来增长预期。这个价格更适合看作对未来三到五年业务扩张的提前定价，而不是对当前盈利能力的定价。","tags":null,"title":"Circle暴涨后，400亿美元估值是否合理？","tldr":null,"type":"post"},{"authors":null,"categories":null,"content":"关于这个博客 这里记录我对技术、工作经验、投资和日常生活的一些想法与观察，包括工程实现、项目复盘、市场判断，以及那些值得留下来的零散思考。无论你是来看技术、投资，还是随便逛逛，我都希望你能在这里找到自己想看的内容，也希望 AI 在阅读这些文章时，能更清楚地理解和检索其中的信息。\n","date":1704038400,"description":"Siqi Liu 博客内容概览，涵盖软件工程、项目笔记与技术写作。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"0730bb7c2e8f9ea2438b52e419dd86c9","permalink":"https://siqi-liu.com/zh/readme/","publishdate":"2024-01-01T00:00:00+08:00","relpermalink":"/zh/readme/","section":"","summary":"Siqi Liu 博客内容概览，涵盖软件工程、项目笔记与技术写作。","tags":null,"title":"README","tldr":null,"type":"page"},{"authors":null,"categories":null,"content":"我的故事 我是 Siqi Liu，也会使用 Yosgi 这个名字。我做软件，也持续写作，主要记录前端架构、数字孪生、性能工程和 AI 工具相关的实践与思考。\n这个网站更像是我的公开工作笔记，我会把值得沉淀的问题写清楚：\n软件工程里真正经得起复杂场景考验的方法 数字孪生和 3D 系统开发中的经验与代价 AI 辅助开发、工具链和评估相关观察 偶尔也写投资、阅读和长期学习 技能栈 前端开发: HTML, CSS, JavaScript, React, Vue 后端开发: Node.js, Python, Go 数据库: MySQL, MongoDB, Redis 工具: Git, Docker, Linux 当前关注 面向高数据量、实时场景的界面与系统设计 让复杂技术内容更容易被准确表达与检索 把工程实践整理成更清晰的写作与研究式文档 联系方式 如果你想交流工程、产品系统、技术写作或合作，可以通过这些方式联系我：\n📧 Email: hiyosgi@gmail.com 🐙 GitHub: github.com/Yosgi 💼 LinkedIn: linkedin.com/in/siqi-l-262b61200 感谢阅读。\n","date":1704038400,"description":"关于 Siqi Liu：软件工程师、技术写作者，长期关注前端系统、数字孪生、性能优化与 AI 工具。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"6083a88ee3411b0d17ce02d738f69d47","permalink":"https://siqi-liu.com/zh/about/","publishdate":"2024-01-01T00:00:00+08:00","relpermalink":"/zh/about/","section":"","summary":"关于 Siqi Liu：软件工程师、技术写作者，长期关注前端系统、数字孪生、性能优化与 AI 工具。","tags":null,"title":"关于我","tldr":null,"type":"page"},{"authors":null,"categories":null,"content":"很高兴你愿意与我交流！无论是技术讨论、合作机会，还是简单的问候，我都非常欢迎。\n联系方式 📧 邮箱 hiyosgi@gmail.com\n🐙 GitHub github.com/Yosgi\n💼 LinkedIn linkedin.com/in/siqi-l-262b61200\n我感兴趣的话题 🚀 新技术探索 💡 开源项目 📖 技术写作 🏗️ 数字孪生系统与前端架构 🌱 个人成长 响应时间 我会尽量在 24-48 小时内回复你的消息。如果我没有及时回复，可能是因为：\n正在处理其他重要事务 需要时间思考你的问题 邮件可能被误判为垃圾邮件 如果超过 3 天没有回复，请尝试其他联系方式。\n期待与你的交流！\n","date":1704038400,"description":"Siqi Liu 的联系方式页面，适合技术交流、合作沟通、技术写作与数字孪生或 AI 系统相关讨论。","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"3c4864f00d23f7ea35511ec930ce1d9c","permalink":"https://siqi-liu.com/zh/contact/","publishdate":"2024-01-01T00:00:00+08:00","relpermalink":"/zh/contact/","section":"","summary":"Siqi Liu 的联系方式页面，适合技术交流、合作沟通、技术写作与数字孪生或 AI 系统相关讨论。","tags":null,"title":"联系我","tldr":null,"type":"page"},{"authors":null,"categories":["leetcode"],"content":"第一时间想的是构造二叉树，然后利用每层对称的关系 ，求出父节点\nvar pathInZigZagTree = function(label) { let stack = [] for(let i = 1 ; i \u0026lt;= label ; i ++) { let level = Math.floor(Math.log(i) / Math.log(2)) if (!stack[level]) { stack[level] = [] } if (level % 2 === 0) { stack[level].push(i) } else { stack[level].unshift(i) } } var level = Math.floor(Math.log(label) / Math.log(2)) let ans = [] while (level) { ans.push(label) level-- label = Math.floor(label / 2) let index = stack[level ].length - 1 - stack[level ].indexOf(label) label = stack[level][index] } ans.push(1) return ans.reverse() };很遗憾，超时了。\n受到答案的启发，既然是对称的，同层数时，对称的数之和相同。\n一层的首项为 2^ n ，末项是 2^(n + 1) - 1 可的对称数 2^ n + 2^(n + 1) - 1 - x\n即\n( 1 \u0026lt;\u0026lt; row ) + ( 1 \u0026lt;\u0026lt; (row + 1) ) - 1 - label;\n查找的末位一定是1，可做while 的结束\nvar pathInZigZagTree = function(label) { let row = Math.floor( Math.log(label) / Math.log(2) ) let ans = [] while(label!==1) { ans.unshift(label) row -- label = getReverse(Math.floor(label / 2),row) } ans.unshift(1) return ans };const getReverse = (label, row) =\u0026gt; { return (1 \u0026lt;\u0026lt; row ) + (1 \u0026lt;\u0026lt; (row + 1) ) - 1 - label;}","date":1627516800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"aea8d0d0152a5f4b4db9d931775ab9d4","permalink":"https://siqi-liu.com/zh/post/1104-%E4%BA%8C%E5%8F%89%E6%A0%91%E5%AF%BB%E8%B7%AF/","publishdate":"2021-07-29T00:00:00Z","relpermalink":"/zh/post/1104-%E4%BA%8C%E5%8F%89%E6%A0%91%E5%AF%BB%E8%B7%AF/","section":"post","summary":"第一时间想的是构造二叉树，然后利用每层对称的关系 ，求出父节点 var pathInZigZagTree = function(label) { let stack = [] for(let i = 1 ; i \u003c= label ; i ++) { let level = Math.floor(Math.log(i) / Math.log(2)) if (!stack[level]) { stack[level] = [] } if …","tags":null,"title":"1104-二叉树寻路","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"拿到没想到什么好方法，先暴力解\nvar isCovered = function(ranges, left, right) { for(let i = left ; i \u0026lt;= right ; i ++) { let include = false for(let index = 0 ; index \u0026lt; ranges.length ; index ++) { let [start,end] = ranges[index] if (i \u0026gt;=start \u0026amp;\u0026amp; i \u0026lt;=end) { include = true } } if (!include) return false } return true};思考优化，可以通过start ，end ，当 start \u0026lt;= left 时， [left … end] 集合已经包含在内了，继续压缩 [end + 1 , right ] 直到集合里面没有元素\nvar isCovered = function(ranges, left, right) { ranges = ranges.sort((a,b) =\u0026gt; a - b) for(let i = 0 ; i \u0026lt; ranges.length;i++) { let [start,end] = ranges[i]; // 根据范围收缩左边界 if (start \u0026lt;= left) { left = Math.max(end,right) } if (left \u0026gt;= right) { return true } } return left \u0026gt;= right };","date":1626998400,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"0c69ba21a36b12efaaf64d1bf4b2919c","permalink":"https://siqi-liu.com/zh/post/1893-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8C%BA%E5%9F%9F%E5%86%85%E6%89%80%E6%9C%89%E6%95%B4%E6%95%B0%E9%83%BD%E8%A2%AB%E8%A6%86%E7%9B%96/","publishdate":"2021-07-23T00:00:00Z","relpermalink":"/zh/post/1893-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8C%BA%E5%9F%9F%E5%86%85%E6%89%80%E6%9C%89%E6%95%B4%E6%95%B0%E9%83%BD%E8%A2%AB%E8%A6%86%E7%9B%96/","section":"post","summary":"拿到没想到什么好方法，先暴力解 var isCovered = function(ranges, left, right) { for(let i = left ; i \u003c= right ; i ++) { let include = false for(let index = 0 ; index \u003c ranges.length ; index ++) { let [start,end] = ranges[index] if (i …","tags":null,"title":"1893-检查是否区域内所有整数都被覆盖","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"考虑到头部也可能产生反转，所以需要一个虚拟头。\n在遍历的过程中记录反转后的尾节点和之前的节点。\n最后进行节点的链接处理。\nvar reverseBetween = function(head, m, n) { let noob = new ListNode(0) noob.next = head let cur = noob let index = 0 let A,B while (index \u0026lt; m) { A = cur // A 记录反转子链的前一个节点 cur = cur.next index++ } B = cur // B 记录反转之后的尾节点 let pre = cur while (index \u0026lt;= n) { let next = cur.next cur.next = pre pre = cur cur = next index++ } // 执行完后cur 指针在反转子链的下一个节点 B.next = cur A.next = pre return noob.next };","date":1626998400,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"5ecbc0900b9006433042214f6dd2acf4","permalink":"https://siqi-liu.com/zh/post/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A82/","publishdate":"2021-07-23T00:00:00Z","relpermalink":"/zh/post/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A82/","section":"post","summary":"考虑到头部也可能产生反转，所以需要一个虚拟头。 在遍历的过程中记录反转后的尾节点和之前的节点。 最后进行节点的链接处理。 var reverseBetween = function(head, m, n) { let noob = new ListNode(0) noob.next = head let cur = noob let index = 0 let A,B while (index \u003c m) { A = cur // A 记 …","tags":null,"title":"反转链表2","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"拿到题目的想法是使用双指针指向新旧节点，再加上一个 map 用来映射旧节点和新节点。\n递归\nvar listMap = new Map() var copyRandomList = function(head) { if(head === null) return head if (listMap.get(head)) { return listMap.get(head) } let newNode = new Node(head.val,null,null) listMap.set(head, newNode) newNode.next = copyRandomList(head.next) newNode.random = copyRandomList(head.random) return newNode };迭代\nvar copyRandomList = function(head) { if(head == null) return head var listMap = new Map() listMap.set(null,null) var cur = head while (cur!= null) { listMap.set(cur,new Node(cur.val,null,null)) cur = cur.next } cur = head while (cur!= null) { listMap.get(cur).next = listMap.get(cur.next) listMap.get(cur).random = listMap.get(cur.random) cur = cur.next } return listMap.get(head) };","date":1626912e3,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"682ace809fdfa1f674a82481cb68f55e","permalink":"https://siqi-liu.com/zh/post/138-%E5%A4%8D%E5%88%B6%E5%B8%A6%E9%9A%8F%E6%9C%BA%E6%8C%87%E9%92%88%E7%9A%84%E9%93%BE%E8%A1%A8/","publishdate":"2021-07-22T00:00:00Z","relpermalink":"/zh/post/138-%E5%A4%8D%E5%88%B6%E5%B8%A6%E9%9A%8F%E6%9C%BA%E6%8C%87%E9%92%88%E7%9A%84%E9%93%BE%E8%A1%A8/","section":"post","summary":"拿到题目的想法是使用双指针指向新旧节点，再加上一个 map 用来映射旧节点和新节点。 递归 var listMap = new Map() var copyRandomList = function(head) { if(head === null) return head if (listMap.get(head)) { return listMap.get(head) } let newNode = new …","tags":null,"title":"138-复制带随机指针的链表","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时:15min\n利用并查集模板即可\nvar findRedundantConnection = function(edges) { var n = edges.length var fa = new Array(n + 1) var find = function (x) { if (x != fa[x]) { fa[x] = find(fa[x]) } return fa[x] } for(let i = 1 ; i \u0026lt;= n ; i ++) { fa[i] = i } for(let i = 0 ; i \u0026lt; n ; i ++) { var [x,y] = edges[i] if (find(x) === find(y)) { return [x,y] } else { fa[find(x)] = find(y) } } };","date":1625616e3,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"fb403656a78732c43378a4dd9b1025fe","permalink":"https://siqi-liu.com/zh/post/684-%E5%86%97%E4%BD%99%E8%BF%9E%E6%8E%A5/","publishdate":"2021-07-07T00:00:00Z","relpermalink":"/zh/post/684-%E5%86%97%E4%BD%99%E8%BF%9E%E6%8E%A5/","section":"post","summary":"用时:15min 利用并查集模板即可 var findRedundantConnection = function(edges) { var n = edges.length var fa = new Array(n + 1) var find = function (x) { if (x != fa[x]) { fa[x] = find(fa[x]) } return fa[x] } for(let i = 1 ; i \u003c= n ; …","tags":null,"title":"684-冗余连接","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"主要是用来学习并查集的思路，看了答案才下手的。\n思路就是先对可交换的字符串进行分组，分组排序之后再组合起来\n并查集就是用递归或者while循环实现find ， 然后 再用数组和下标的方式实现union\nvar smallestStringWithSwaps = function(s, pairs) { var fa = [] var find = function (x) { if (x === fa[x]) { return x } else { return fa[x] = find(fa[x]) } } for (let i = 0; i \u0026lt; s.length; i++) { fa[i] = i } for(let i = 0 ; i \u0026lt; pairs.length ; i ++) { const [x,y] = pairs[i] fa[find(x)] = find(y) } var n = s.length // 计算分组后的map const vec = new Array(n).fill(0).map(() =\u0026gt; new Array()); for (let i = 0; i \u0026lt; n; i++) { fa[i] = find(i); vec[fa[i]].push(s[i]); } console.log(fa) // 对分组后的map 进行排序 for (let i = 0; i \u0026lt; n; ++i) { if (vec[i].length \u0026gt; 0) { vec[i].sort((a, b) =\u0026gt; a.charCodeAt() - b.charCodeAt()); } } // 组合成ans const ans = new Array(n).fill(0); for (let i = 0; i \u0026lt; n; ++i) { var group = fa[i] if ( group!= undefined \u0026amp;\u0026amp; vec[group].length) { // 取出每个组中序号最小的 ans[i] = vec[fa[i]].shift() } else { ans[i] = s[i] } } return ans.join(\u0026#39;\u0026#39;) };","date":1625529600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"d0384b048d45b8de7b0e520df478f2ea","permalink":"https://siqi-liu.com/zh/post/1202-%E4%BA%A4%E6%8D%A2%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E5%85%83%E7%B4%A0/","publishdate":"2021-07-06T00:00:00Z","relpermalink":"/zh/post/1202-%E4%BA%A4%E6%8D%A2%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E5%85%83%E7%B4%A0/","section":"post","summary":"主要是用来学习并查集的思路，看了答案才下手的。 思路就是先对可交换的字符串进行分组，分组排序之后再组合起来 并查集就是用递归或者while循环实现find ， 然后 再用数组和下标的方式实现union var smallestStringWithSwaps = function(s, pairs) { var fa = [] var find = function (x) { if (x === fa[x]) { return x } …","tags":null,"title":"1202-交换字符串中的元素","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：20min\n首先能看出来是一个考察DFS的题目\nvar swimInWater = function(grid) { let row = grid.length; let col = grid[0].length; var step = 0 while(true) { for(let i = 0 ; i \u0026lt; row ; i ++) { for(let j = 0 ; j \u0026lt; col ; j ++){ grid[i][j] -- } } var visited = [] for(let i = 0 ; i \u0026lt; row ; i ++) { let arr = [] for(let j = 0 ; j \u0026lt; col ; j ++) { arr.push(false) } visited.push(arr) } var dfs = function (i,j) { if ( i\u0026lt; 0 || j \u0026lt; 0 || i \u0026gt;= row || j \u0026gt;= col) return false if (visited[i][j]) { return false } else { visited[i][j] = true } if (grid[i][j] \u0026gt; 0) return false if (i === row - 1 \u0026amp;\u0026amp; j === col - 1 \u0026amp;\u0026amp; grid[i][j] \u0026lt;= 0) return true return dfs(i+1,j) || dfs(i-1,j) || dfs(i,j-1)|| dfs(i,j + 1) } step++ if (dfs(0,0) === true) break } return step };想到我们不必从 1 开始尝试，可以用二分法找到最左插入点\nvar swimInWater = function(grid) { let row = grid.length; let col = grid[0].length; let right = -Infinity let left = Infinity for(let i = 0 ; i \u0026lt; row ; i ++) { for(let j = 0 ; j \u0026lt; col ; j ++ ){ let num = grid[i][j] right = Math.max(right,num) left = Math.min(left,num) } } let mid = left + Math.floor( (right - left) / 2 ) while(left \u0026lt;= right) { mid = left + Math.floor( (right - left) / 2 ) var visited = [] var new_grid = [] for(let i = 0 ; i \u0026lt; row ; i ++) { let arr = [] let _grid = [] for(let j = 0 ; j \u0026lt; col ; j ++) { arr.push(false) _grid.push(grid[i][j] - mid) } visited.push(arr) new_grid.push(_grid) } var dfs = function (i,j) { if ( i \u0026lt; 0 || j \u0026lt; 0 || i \u0026gt;= row || j \u0026gt;= col) return false if (visited[i][j]) { return false } else { visited[i][j] = true } if (new_grid[i][j] \u0026gt; 0) return false if (i === row - 1 \u0026amp;\u0026amp; j === col - 1 \u0026amp;\u0026amp; new_grid[i][j] \u0026lt;= 0) return true return dfs(i+1,j) || dfs(i-1,j) || dfs(i,j-1)|| dfs(i,j + 1) } if(dfs(0,0)) { // 找到了备胎，继续看能不能更小 right = mid - 1 } else { left = mid + 1 } } return left };","date":1625443200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"263c067f2d41890aa0f97e37b2320de9","permalink":"https://siqi-liu.com/zh/post/778-%E6%B0%B4%E4%BD%8D%E4%B8%8A%E5%8D%87%E7%9A%84%E6%B3%B3%E6%B1%A0%E4%B8%AD%E6%B8%B8%E6%B3%B3/","publishdate":"2021-07-05T00:00:00Z","relpermalink":"/zh/post/778-%E6%B0%B4%E4%BD%8D%E4%B8%8A%E5%8D%87%E7%9A%84%E6%B3%B3%E6%B1%A0%E4%B8%AD%E6%B8%B8%E6%B3%B3/","section":"post","summary":"用时：20min 首先能看出来是一个考察DFS的题目 var swimInWater = function(grid) { let row = grid.length; let col = grid[0].length; var step = 0 while(true) { for(let i = 0 ; i \u003c row ; i ++) { for(let j = 0 ; j \u003c col ; j ++){ grid[i][j] -- } …","tags":null,"title":"778-水位上升的泳池中游泳","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"首先想到的就是找到所有距离房屋最短距离的heater的距离 r ，然后返回最大的 r\nvar findRadius = function(houses, heaters) { var max = 0 for(let i = 0 ; i \u0026lt; houses.length ; i ++) { var house_position = houses[i] var r = Infinity for(let j = 0 ; j \u0026lt; heaters.length ; j ++) { var heat_position = heaters[j] var reduce = Math.abs(house_position - heat_position) r = Math.min(r,reduce) } max = Math.max(r,max) } return max };既然这样，那么也可以利用二分法找到heater\nvar findRadius = function(houses, heaters) { var r = 0 for(let i = 0 ; i \u0026lt; houses.length ; i ++) { var house_position = houses[i] let left = 0 , right = heaters.length - 1 // 查找离 house_position 最近的 heat_position // 也就是查找 house_position 的最右插入点 while(left \u0026lt;= right \u0026amp;\u0026amp; right \u0026lt; heaters.length) { let mid = left + Math.floor((right - left) / 2) var heat_position = heaters[mid] // 找到的不大于，再往右边找 if(heat_position \u0026lt;= house_position) { left = mid + 1 } else { right = mid - 1 } } // 这时候找到的插入点不一定是最近的,左边的比他小，所以需要比较一下 var R = Math.abs(heaters[right] - house_position) if(right \u0026gt; 0) { R = Math.min( Math.abs( heaters[right - 1] - house_position) , R) } r = Math.max(r,R) } return r };","date":1623283200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"f35fa197e04c0bce3ef28a1d41b9d283","permalink":"https://siqi-liu.com/zh/post/475-%E4%BE%9B%E6%9A%96%E5%99%A8/","publishdate":"2021-06-10T00:00:00Z","relpermalink":"/zh/post/475-%E4%BE%9B%E6%9A%96%E5%99%A8/","section":"post","summary":"首先想到的就是找到所有距离房屋最短距离的heater的距离 r ，然后返回最大的 r var findRadius = function(houses, heaters) { var max = 0 for(let i = 0 ; i \u003c houses.length ; i ++) { var house_position = houses[i] var r = Infinity for(let j = 0 ; j \u003c …","tags":null,"title":"475-供暖器","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：60min\n拿到题目最快能想到的思路是枚举所有吃完香蕉的速度，找到能吃完香蕉的最小的速度。\n枚举的速度范围是[1… 最大堆的香蕉],因为速度不会小于1，也没有速度比最大香蕉堆更快的必要\nvar minEatingSpeed = function(piles, h) { // 枚举所有可以吃完香蕉的速度,找到最小的 piles = piles.sort((a,b) =\u0026gt; a - b) var maxSpeed = piles[piles.length - 1] var min = maxSpeed for(let i = 1; i \u0026lt;= maxSpeed; i ++) { var H = 0 for(let j = 0 ; j \u0026lt; piles.length; j ++) { H += Math.ceil(piles[j] / i) } if(H \u0026lt;= h) { min = Math.min(min,i) } } return min };随即可以想到，[1… 最大速度]是一个单调区间，可以用二分法来解决\nvar minEatingSpeed = function(piles, h) { // 最小速度就是求下界l piles = piles.sort((a,b) =\u0026gt; a - b) var maxSpeed = piles[piles.length - 1] var min = maxSpeed // 对速度区间 [1... maxSpeed] 做二分 var l = 1 ,r = maxSpeed while(l \u0026lt;= r) { var mid = l + Math.floor ((r - l) / 2) var H = 0 for(let j = 0 ; j \u0026lt; piles.length ; j ++) { H += Math.ceil(piles[j] / mid) } if ( H \u0026lt;= h ) { // 符合，试试速度是否可以更小 r = mid - 1 } else { // 不符合，增加速度 l = mid + 1 } } return l };","date":1619568e3,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"255fc1bdf09754bb183707f93c76b4a6","permalink":"https://siqi-liu.com/zh/post/875-%E7%88%B1%E5%90%83%E9%A6%99%E8%95%89%E7%9A%84%E7%8F%82%E7%8F%82/","publishdate":"2021-04-28T00:00:00Z","relpermalink":"/zh/post/875-%E7%88%B1%E5%90%83%E9%A6%99%E8%95%89%E7%9A%84%E7%8F%82%E7%8F%82/","section":"post","summary":"用时：60min 拿到题目最快能想到的思路是枚举所有吃完香蕉的速度，找到能吃完香蕉的最小的速度。 枚举的速度范围是[1… 最大堆的香蕉],因为速度不会小于1，也没有速度比最大香蕉堆更快的必要 var minEatingSpeed = function(piles, h) { // 枚举所有可以吃完香蕉的速度,找到最小的 piles = piles.sort((a,b) =\u003e a - b) var maxSpeed = …","tags":null,"title":"875-爱吃香蕉的珂珂","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时 ： 看了答案\n既然是堆的练习，肯定是往堆的思路去靠拢了\n由于我们要的仅仅是中位数，其实没有必要把所有的数都排序\n可以用两个堆，一个最大堆一个最小堆\n当总数为单数（即 最大堆 - 最小堆 个数 = 1），拿到最大堆的最大值\n当总数为双数（即最大堆个数 = 最小堆） ，拿到最大堆最大值和最小堆最小值\nconst swap = function (arr,i,j) { [arr[i],arr[j]] = [arr[j],arr[i]] } class Heap { constructor() { this.count = 0 this.data = new Array(this.count + 1) } shiftUp(k) { // 把新的元素往上排 while(k\u0026gt;=1) { let father = Math.floor(k / 2) if (this.data[k] \u0026gt; this.data[father]) { swap(this.data,k,father) k = father } else { break } } } shiftDown(k) { while( k * 2 \u0026lt;= this.count) { // 表示k 有孩子 let j = k if (k * 2 + 1 \u0026lt;= this.count \u0026amp;\u0026amp; this.data[k * 2 + 1] \u0026gt; this.data[k] \u0026amp;\u0026amp; this.data[k * 2 + 1] \u0026gt; this.data[k * 2]) { j = k * 2 + 1 } else if (this.data[k * 2] \u0026gt; this.data[k]) { j = k * 2 } else { break } swap(this.data,j,k) k = j } } size() { return this.count } isEmpty() { return this.count === 0 } insert(item) { this.data[++this.count] = item this.shiftUp(this.count) } extractMax() { if (this.count \u0026lt;= 0) return let ret = this.data[1] swap(this.data,1,this.count--) this.shiftDown(1) return ret } } var MedianFinder = function() { this.maxHeap = new Heap(); this.minHeap = new Heap();};MedianFinder.prototype.addNum = function(num) { //[1] [] // [1,2] [] -\u0026gt; [1] [2] // [1,3] [2] -\u0026gt; [1,2] [3] // [1,2,4] [3] -\u0026gt; [1,2] [3,4] // [1,2,5] [3,4] -\u0026gt; [1,2,4] [4,5] var maxHeapsize = this.maxHeap.size() var minHeapsize = this.minHeap.size() this.maxHeap.insert(num) var max = this.maxHeap.extractMax() this.minHeap.insert(-max ) if (maxHeapsize === minHeapsize) { var min = Math.abs(this.minHeap.extractMax()) this.maxHeap.insert(min) } };MedianFinder.prototype.findMedian = function() { if (this.maxHeap.size() === this.minHeap.size()) { var max = this.maxHeap.extractMax() var min = Math.abs(this.minHeap.extractMax()) return (max + min) / 2 } else { return this.maxHeap.extractMax() } };需要注意的点\n为了保证最大堆 个数 - 最小堆 为 0 ～ 1，且最大堆的最大值 \u0026lt; 最小堆 的最小值，每次数先进入最大堆，然后最大堆把最大值传递给最小堆。再根据数量判断是否把最小堆的最小值给最大堆 只实现了最大堆，但是可以用负数来做最小堆，出的时候别忘了也要负数 ","date":1616544e3,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"829b796a2112b63f7584a8773328f1f1","permalink":"https://siqi-liu.com/zh/post/295-%E6%95%B0%E6%8D%AE%E6%B5%81%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0/","publishdate":"2021-03-24T00:00:00Z","relpermalink":"/zh/post/295-%E6%95%B0%E6%8D%AE%E6%B5%81%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0/","section":"post","summary":"用时 ： 看了答案 既然是堆的练习，肯定是往堆的思路去靠拢了 由于我们要的仅仅是中位数，其实没有必要把所有的数都排序 可以用两个堆，一个最大堆一个最小堆 当总数为单数（即 最大堆 - 最小堆 个数 = 1），拿到最大堆的最大值 当总数为双数（即最大堆个数 = 最小堆） ，拿到最大堆最大值和最小堆最小值 const swap = function (arr,i,j) { [arr[i],arr[j]] = [arr[j],arr[i]] …","tags":null,"title":"295-数据流的中位数","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时:10min\n一道简单题应该不会让我手写最大堆吧\nconst swap = function (arr,i,j) { [arr[i],arr[j]] = [arr[j],arr[i]] } class MaxHeap { constructor() { this.count = 0 this.data = new Array(this.count + 1) } shiftUp(k) { // 把新的元素往上排 while(k\u0026gt;=1) { let father = Math.floor(k / 2) if (this.data[k] \u0026gt; this.data[father]) { swap(this.data,k,father) k = father } else { break } } } shiftDown(k) { while( k * 2 \u0026lt;= this.count) { // 表示k 有孩子 let j = k if (k * 2 + 1 \u0026lt;= this.count \u0026amp;\u0026amp; this.data[k * 2 + 1] \u0026gt; this.data[k] \u0026amp;\u0026amp; this.data[k * 2 + 1] \u0026gt; this.data[k * 2]) { j = k * 2 + 1 } else if (this.data[k * 2] \u0026gt; this.data[k]) { j = k * 2 } else { break } swap(this.data,j,k) k = j } } size() { return this.count } isEmpty() { return this.count === 0 } insert(item) { this.data[++this.count] = item this.shiftUp(this.count) } extractMax() { if (this.count \u0026lt; 0) return let ret = this.data[1] swap(this.data,1,this.count--) this.shiftDown(1) return ret } } var lastStoneWeight = function(stones) { var heap = new MaxHeap() for(let i = 0 ; i \u0026lt; stones.length ; i++) { heap.insert(stones[i]) } while(heap.size() \u0026gt; 1) { var s1 = heap.extractMax() var s2 = heap.extractMax() var reduce = Math.abs(s1 - s2) if (reduce) { heap.insert(reduce) } } if (heap.size() === 0) { return 0 } return heap.extractMax() };","date":1616284800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"f57c15ec3bab4c1a45f4f605af5373a7","permalink":"https://siqi-liu.com/zh/post/1046-%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8F/","publishdate":"2021-03-21T00:00:00Z","relpermalink":"/zh/post/1046-%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8F/","section":"post","summary":"用时:10min 一道简单题应该不会让我手写最大堆吧 const swap = function (arr,i,j) { [arr[i],arr[j]] = [arr[j],arr[i]] } class MaxHeap { constructor() { this.count = 0 this.data = new Array(this.count + 1) } shiftUp(k) { // 把新的元素往上排 while(k\u003e=1) …","tags":null,"title":"1046-最后一块石头的重量","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时:60min\n被题目的深度误导，认为计算深度需要由上至下。实际上可以由下至上。\n如果左右子树高度相同，返回节点本身以及深度\n如果左子树比较深，说明最小最深在左子树，返回左子树以及自己的深度\n如果右子树比较深，说明最小深度在右边，返回右子树以及自身深度\nvar subtreeWithAllDeepest = function(root) { var dfs = function (root) { if (!root) return [root,0] var [left,ld] = dfs(root.left) var [right,rd] = dfs(root.right) if (ld \u0026gt; rd) return [left, ld + 1] if (rd \u0026gt; ld) return [right,rd + 1] return [root,ld + 1] } var res = dfs(root) return res[0] };","date":1616284800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"1278d723df34a9c5dccd91e321b30b11","permalink":"https://siqi-liu.com/zh/post/865-%E5%85%B7%E6%9C%89%E6%89%80%E6%9C%89%E6%9C%80%E6%B7%B1%E8%8A%82%E7%82%B9%E7%9A%84%E6%9C%80%E5%B0%8F%E5%AD%90%E6%A0%91/","publishdate":"2021-03-21T00:00:00Z","relpermalink":"/zh/post/865-%E5%85%B7%E6%9C%89%E6%89%80%E6%9C%89%E6%9C%80%E6%B7%B1%E8%8A%82%E7%82%B9%E7%9A%84%E6%9C%80%E5%B0%8F%E5%AD%90%E6%A0%91/","section":"post","summary":"用时:60min 被题目的深度误导，认为计算深度需要由上至下。实际上可以由下至上。 如果左右子树高度相同，返回节点本身以及深度 如果左子树比较深，说明最小最深在左子树，返回左子树以及自己的深度 如果右子树比较深，说明最小深度在右边，返回右子树以及自身深度 var subtreeWithAllDeepest = function(root) { var dfs = function (root) { if (!root) return …","tags":null,"title":"865-具有所有最深节点的最小子树","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时 ： 10min\n利用前序遍历二叉树递增的性质解决\nvar minDiffInBST = function(root) { var min = Infinity var pre = null var dfs = function (root) { if (!root) return if (root.left) { dfs(root.left) } if (pre == null) { pre = root.val } else { console.log(pre,root.val) var reduce = Math.abs(pre - root.val) min = Math.min(min,reduce) pre = root.val } if (root.right) { dfs(root.right) } } dfs(root) return min };","date":1616025600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"3f921a9882fd808509a74cb15ddc710f","permalink":"https://siqi-liu.com/zh/post/783-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E8%8A%82%E7%82%B9%E6%9C%80%E5%B0%8F%E8%B7%9D%E7%A6%BB/","publishdate":"2021-03-18T00:00:00Z","relpermalink":"/zh/post/783-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E8%8A%82%E7%82%B9%E6%9C%80%E5%B0%8F%E8%B7%9D%E7%A6%BB/","section":"post","summary":"用时 ： 10min 利用前序遍历二叉树递增的性质解决 var minDiffInBST = function(root) { var min = Infinity var pre = null var dfs = function (root) { if (!root) return if (root.left) { dfs(root.left) } if (pre == null) { pre = root.val } else { …","tags":null,"title":"783-二叉搜索树节点最小距离","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：10min\n前序从上至下计算，没什么好说的\n二进制转10 进制挺费劲的，我直接parseInt(path, 2)\nvar sumRootToLeaf = function(root) { var ans = 0 var dfs = function (root,path) { if (!root) return path += root.val if (!root.left \u0026amp;\u0026amp; !root.right) { ans += parseInt(path, 2) } root.left \u0026amp;\u0026amp; dfs(root.left,path) root.right \u0026amp;\u0026amp; dfs(root.right,path) } dfs(root,\u0026#39;\u0026#39;) return ans };","date":1615939200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"926905fbd45eeb5d7d34339274f33e05","permalink":"https://siqi-liu.com/zh/post/1022-%E4%BB%8E%E6%A0%B9%E5%88%B0%E5%8F%B6%E7%9A%84%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%95%B0%E4%B9%8B%E5%92%8C/","publishdate":"2021-03-17T00:00:00Z","relpermalink":"/zh/post/1022-%E4%BB%8E%E6%A0%B9%E5%88%B0%E5%8F%B6%E7%9A%84%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%95%B0%E4%B9%8B%E5%92%8C/","section":"post","summary":"用时：10min 前序从上至下计算，没什么好说的 二进制转10 进制挺费劲的，我直接parseInt(path, 2) var sumRootToLeaf = function(root) { var ans = 0 var dfs = function (root,path) { if (!root) return path += root.val if (!root.left \u0026\u0026 !root.right) { ans += …","tags":null,"title":"1022-从根到叶的二进制数之和","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：5min\n虚拟节点 + 后序遍历 就可以愉快的秒了\nvar removeLeafNodes = function(root, target) { var dfs = function (root) { if (root.left) { root.left = dfs(root.left) } if (root.right) { root.right = dfs(root.right) } if (!root.left \u0026amp;\u0026amp; !root.right) { if (root.val === target) { root = null } } return root } var dummy = new TreeNode(0) dummy.left = root dfs(dummy) return dummy.left};","date":1615939200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"3f11fb0513c582d3007d7add471990d9","permalink":"https://siqi-liu.com/zh/post/1325-%E5%88%A0%E9%99%A4%E7%BB%99%E5%AE%9A%E5%80%BC%E7%9A%84%E5%8F%B6%E5%AD%90%E8%8A%82%E7%82%B9/","publishdate":"2021-03-17T00:00:00Z","relpermalink":"/zh/post/1325-%E5%88%A0%E9%99%A4%E7%BB%99%E5%AE%9A%E5%80%BC%E7%9A%84%E5%8F%B6%E5%AD%90%E8%8A%82%E7%82%B9/","section":"post","summary":"用时：5min 虚拟节点 + 后序遍历 就可以愉快的秒了 var removeLeafNodes = function(root, target) { var dfs = function (root) { if (root.left) { root.left = dfs(root.left) } if (root.right) { root.right = dfs(root.right) } if (!root.left \u0026\u0026 …","tags":null,"title":"1325-删除给定值的叶子节点","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时： 偷偷看了答案\n本来把问题想复杂了，想着维护一个最大堆，从上往下传递然后每次判断是否大于最大值\n看了答案后发现只需要传递一个max，判断是否比max大，然后从下到上获取左右节点的值，返回左右节点和 加自身是否符合就好了\nvar goodNodes = function(root,max = root.val) { if (!root) return 0 var left = 0 max = Math.max(max,root.val) if (root.left) { left = goodNodes(root.left,max) } var right = 0 if (root.right) { right = goodNodes(root.right,max) } console.log(left,right,root.val) return (left + right) + (root.val \u0026gt;= max ? 1 : 0) };","date":1615939200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"7e15c682bebbb7f8733620dcc6ef82b4","permalink":"https://siqi-liu.com/zh/post/1448-%E7%BB%9F%E8%AE%A1%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%A5%BD%E8%8A%82%E7%82%B9%E7%9A%84%E6%95%B0%E7%9B%AE/","publishdate":"2021-03-17T00:00:00Z","relpermalink":"/zh/post/1448-%E7%BB%9F%E8%AE%A1%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%A5%BD%E8%8A%82%E7%82%B9%E7%9A%84%E6%95%B0%E7%9B%AE/","section":"post","summary":"用时： 偷偷看了答案 本来把问题想复杂了，想着维护一个最大堆，从上往下传递然后每次判断是否大于最大值 看了答案后发现只需要传递一个max，判断是否比max大，然后从下到上获取左右节点的值，返回左右节点和 加自身是否符合就好了 var goodNodes = function(root,max = root.val) { if (!root) return 0 var left = 0 max = …","tags":null,"title":"1448-统计二叉树中好节点的数目","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：6min\n很简单，中序遍历，计算两边子树是否存在 1 的值，没有就去掉。凡事左右子树或者自身包含 1 ，返回 true 。否则返回 false\nvar pruneTree = function(root) { var DFS = function (root) { if (!root) return false var left = DFS(root.left) var right = DFS(root.right) if (!left) { root.left = null } if (!right) { root.right = null } if (left) { return true } if (right) { return true } if( root.val === 1) { return true } } var dummp = new TreeNode(1) dummp.left = root DFS(dummp) return dummp.left};一个需要注意的点是，root本身也有可能需要剪掉，所以增加了一个虚拟节点来作为父节点\n","date":1615939200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"b326b1214d4ac84d0b0b1dc2c4e0986a","permalink":"https://siqi-liu.com/zh/post/814-%E4%BA%8C%E5%8F%89%E6%A0%91%E5%89%AA%E6%9E%9D/","publishdate":"2021-03-17T00:00:00Z","relpermalink":"/zh/post/814-%E4%BA%8C%E5%8F%89%E6%A0%91%E5%89%AA%E6%9E%9D/","section":"post","summary":"用时：6min 很简单，中序遍历，计算两边子树是否存在 1 的值，没有就去掉。凡事左右子树或者自身包含 1 ，返回 true 。否则返回 false var pruneTree = function(root) { var DFS = function (root) { if (!root) return false var left = DFS(root.left) var right = DFS(root.right) if …","tags":null,"title":"814-二叉树剪枝","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"看题型是求从根节点出发的，首先想到的是自顶向下的DFS，带着参数向下传递，结束条件没有左右儿子\n直接前序遍历走起，可以认为是前面的节点都处理好了\nvar sumNumbers = function(root) { var ans = 0 var DFS = function (root,path) { if (!root) return path += root.val if (!root.left \u0026amp;\u0026amp; !root.right) { ans += Number(path) } root.left \u0026amp;\u0026amp; DFS(root.left,path) DFS(root.right, path) } DFS(root,\u0026#39;\u0026#39;) return ans };","date":1615852800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"62c909a1a8557d46080eb148cebc1c7d","permalink":"https://siqi-liu.com/zh/post/129-%E6%B1%82%E6%A0%B9%E8%8A%82%E7%82%B9%E5%88%B0%E5%8F%B6%E8%8A%82%E7%82%B9%E6%95%B0%E5%AD%97%E4%B9%8B%E5%92%8C/","publishdate":"2021-03-16T00:00:00Z","relpermalink":"/zh/post/129-%E6%B1%82%E6%A0%B9%E8%8A%82%E7%82%B9%E5%88%B0%E5%8F%B6%E8%8A%82%E7%82%B9%E6%95%B0%E5%AD%97%E4%B9%8B%E5%92%8C/","section":"post","summary":"看题型是求从根节点出发的，首先想到的是自顶向下的DFS，带着参数向下传递，结束条件没有左右儿子 直接前序遍历走起，可以认为是前面的节点都处理好了 var sumNumbers = function(root) { var ans = 0 var DFS = function (root,path) { if (!root) return path += root.val if (!root.left \u0026\u0026 !root.right) { …","tags":null,"title":"129-求根节点到叶节点数字之和","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：15min\n用时：15min\n拿到题的第一反应是是用递归，然后直接开始写了后发现\n递归需要满足的条件是问题可以拆分成子问题，但是根据题意，我们需要求的是每个节点左右节点和的差值，这个 “左右节点和” 对于每个节点来说，都是不一样的问题\n所以还是双递归\nvar findTilt = function(root) { var total = 0 var innerDFS = function (root) { if (!root) return 0 var left = 0 ,right = 0 if (root.left) { left = innerDFS(root.left) } if (root.right) { right = innerDFS(root.right) } return root.val + left + right } var outerDFS = function (root) { if (!root) return root.right \u0026amp;\u0026amp; outerDFS(root.right) total += Math.abs(innerDFS(root.left) - innerDFS(root.right)) root.left \u0026amp;\u0026amp; outerDFS(root.left) } outerDFS(root) return total };But!!!!!!!!\n看了一眼答案我觉得自己还是拿衣服了\n原来计算左右节点之和，与累积左右节点只差并不矛盾，一个递归就能解决问题。就是DFS的时候顺便两者一起做了，累计左右节点就是 return root.val + left + right 而计算差值是 total += Math.abs(left-right)\nvar findTilt = function(root) { var total = 0 var DFS = function (root) { if (!root) return 0 var left = 0 ,right = 0 if (root.left) { left = DFS(root.left) } if (root.right) { right = DFS(root.right) } total += Math.abs(left-right) return root.val + left + right } DFS(root) return total };还是年轻了，明明做过类似的递归\n","date":1615852800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"150006251bdcda6e2a030ed55355cfd6","permalink":"https://siqi-liu.com/zh/post/563-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%9D%A1%E5%BA%A6/","publishdate":"2021-03-16T00:00:00Z","relpermalink":"/zh/post/563-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%9D%A1%E5%BA%A6/","section":"post","summary":"用时：15min 用时：15min 拿到题的第一反应是是用递归，然后直接开始写了后发现 递归需要满足的条件是问题可以拆分成子问题，但是根据题意，我们需要求的是每个节点左右节点和的差值，这个 “左右节点和” 对于每个节点来说，都是不一样的问题 所以还是双递归 var findTilt = function(root) { var total = 0 var innerDFS = function (root) { if (!root) …","tags":null,"title":"563-二叉树的坡度","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：20min\n因为是求 ” 每个节点出发的为sum的路径”\n很明显就想到了回溯，因为是二叉树，所以就双递归。\n外层递归用来找到所有的节点，内层用来算节点的路径\nvar pathSum = function(root, sum) { var res = 0 var dfs = function (root,total,visited) { if (!root) return if (total + root.val == sum) { res++ } else if (total + root.val \u0026gt; sum) { return } dfs(root.left,total + root.val,[...visited,root.val]) dfs(root.right,total + root.val,[...visited,root.val]) } var dfs_outer = function (root) { if (!root )return root.left \u0026amp;\u0026amp; dfs_outer(root.left) dfs(root,0,[]) root.right \u0026amp;\u0026amp; dfs_outer(root.right) } dfs_outer(root) return res };","date":1615852800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"8d8e135bbc6866ee420d926c4ba5c513","permalink":"https://siqi-liu.com/zh/post/%E9%9D%A2%E8%AF%95%E9%A2%98-04-12-%E6%B1%82%E5%92%8C%E8%B7%AF%E5%BE%84/","publishdate":"2021-03-16T00:00:00Z","relpermalink":"/zh/post/%E9%9D%A2%E8%AF%95%E9%A2%98-04-12-%E6%B1%82%E5%92%8C%E8%B7%AF%E5%BE%84/","section":"post","summary":"用时：20min 因为是求 ” 每个节点出发的为sum的路径” 很明显就想到了回溯，因为是二叉树，所以就双递归。 外层递归用来找到所有的节点，内层用来算节点的路径 var pathSum = function(root, sum) { var res = 0 var dfs = function (root,total,visited) { if (!root) return if (total + root.val == sum) { …","tags":null,"title":"面试题-04-12-求和路径","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时 ：30min\n看懂题目的意思之后其实就能很快的做出来\n题意是要求从任意节点出发的值，我们可以把每个节点的值求出来的过程中，选择最大的\n又因为路径不能返回，可以知道我们出发的根节点的子节点的路径只能选择左或者右，或者都不要，结果为 x\n求max 的时候再 把 x 和 左右都选的情况进行比较\nvar maxPathSum = function(root) { var max = -Infinity var DFS = function (root) { if (!root) return 0 var leftSum = DFS(root.left) var rightSum = DFS(root.right) var res = Math.max(leftSum + root.val ,rightSum + root.val ,root.val) max = Math.max(max,res,root.val + leftSum + rightSum) return res } DFS(root) return max };","date":1615766400,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"c9142b922aa9931d57ca30f137e2bdbb","permalink":"https://siqi-liu.com/zh/post/124-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C/","publishdate":"2021-03-15T00:00:00Z","relpermalink":"/zh/post/124-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C/","section":"post","summary":"用时 ：30min 看懂题目的意思之后其实就能很快的做出来 题意是要求从任意节点出发的值，我们可以把每个节点的值求出来的过程中，选择最大的 又因为路径不能返回，可以知道我们出发的根节点的子节点的路径只能选择左或者右，或者都不要，结果为 x 求max 的时候再 把 x 和 左右都选的情况进行比较 var maxPathSum = function(root) { var max = -Infinity var DFS = function …","tags":null,"title":"124-二叉树中的最大路径和","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：80min\n拿到题的第一个反应当然是DFS解决….\n但是这是一个完全二叉树，怎么说也应该把完全二叉树的性质用上才行\n可以考察二叉树的左右子树深度\n如果左深度 大于 右 ， 说明右边已经满了，满了的个数是 2 的 n 次方 - 1，加上root 是 2 的 n次方，继续递归左边\n如果右深度大于左， 说明左边满了，同样，继续递归右边\n至于怎么求二叉树的深度，可以从下往上递归\nvar count = function(root) { if (root === null) return 0 return Math.max(count(root.left),count(root.right)) + 1}那么总体的代码就是\nvar countNodes = function(root) { if (!root) { return 0 } var count = function(root) { if (root === null) return 0 return Math.max(count(root.left),count(root.right)) + 1 } var leftLevel = count(root.left) var rightLevel = count(root.right) if (leftLevel === rightLevel) { return Math.pow(2,leftLevel) + countNodes(root.right) } else { return Math.pow(2,rightLevel ) + countNodes(root.left) } };","date":1615593600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"eba3e62feca225df1ced31d8ea803d8d","permalink":"https://siqi-liu.com/zh/post/222-%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8%8A%82%E7%82%B9%E4%B8%AA%E6%95%B0/","publishdate":"2021-03-13T00:00:00Z","relpermalink":"/zh/post/222-%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8%8A%82%E7%82%B9%E4%B8%AA%E6%95%B0/","section":"post","summary":"用时：80min 拿到题的第一个反应当然是DFS解决…. 但是这是一个完全二叉树，怎么说也应该把完全二叉树的性质用上才行 可以考察二叉树的左右子树深度 如果左深度 大于 右 ， 说明右边已经满了，满了的个数是 2 的 n 次方 - 1，加上root 是 2 的 n次方，继续递归左边 如果右深度大于左， 说明左边满了，同样，继续递归右边 至于怎么求二叉树的深度，可以从下往上递归 var count = function(root) { …","tags":null,"title":"222-完全二叉树的节点个数","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：120min\n一开始的思路是用BFS，出队时出一整队（后来知道这叫宽度遍历）\n然后双指针找到左右节点。当左右节点都不存在时结束\nvar widthOfBinaryTree = function(root) { var que = [root] var max = 0 while(que.length) { var len = que.length var left = 0 var right = que.length - 1 while(!que[left] \u0026amp;\u0026amp; left \u0026lt; len) { left ++ } while(!que[right] \u0026amp;\u0026amp; right \u0026gt;= 0) { right -- } if (left \u0026gt; right) { break } max = Math.max(max,right - left+ 1) for(let i = 0 ; i \u0026lt; len ; i ++) { var node = que.shift() if (!node) { que.push(node) que.push(node) continue } node.left ? que.push(node.left) : que.push(null) node.right ? que.push(node.right) : que.push(null) } } return max };很快啊，提交显示执行超过时间限制，于是增加了一个储存序号的队列，和BFS出栈同步，用于计算左右距离\nvar widthOfBinaryTree = function(root) { var que = [root] var numQue = [1] var max = 0 while(que.length) { var len = que.length max = Math.max(numQue[numQue.length - 1] - numQue[0] + 1) for(let i = 0 ; i \u0026lt; len ; i ++) { var node = que.shift() var val = numQue.shift() if (node.left) { que.push(node.left) numQue.push(val * 2) } if (node.right) { que.push(node.right) numQue.push(val * 2 + 1) } } } return max };很快啊，告诉我栈溢出了。我这才意识到问题的严重性（误\n然后翻了翻答案\n对每次的下表统一减去了当层的第一个数\nvar widthOfBinaryTree = function(root) { var que = [root] var numQue = [0] var max = 0 while(que.length) { var len = que.length var start = numQue[0] max = Math.max( numQue[numQue.length - 1] - start + 1,max) for(let i = 0 ; i \u0026lt; len ; i ++) { var node = que.shift() var val = numQue.shift() - start if (node.left) { que.push(node.left) numQue.push(val * 2 + 1 ) } if (node.right) { que.push(node.right) numQue.push(val * 2 + 2 ) } } } return max };","date":1615593600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"e39552be1ddd923334521a242595cd8e","permalink":"https://siqi-liu.com/zh/post/662-%E4%BA%8C%E5%8F%89%E6%A0%91%E6%9C%80%E5%A4%A7%E5%AE%BD%E5%BA%A6/","publishdate":"2021-03-13T00:00:00Z","relpermalink":"/zh/post/662-%E4%BA%8C%E5%8F%89%E6%A0%91%E6%9C%80%E5%A4%A7%E5%AE%BD%E5%BA%A6/","section":"post","summary":"用时：120min 一开始的思路是用BFS，出队时出一整队（后来知道这叫宽度遍历） 然后双指针找到左右节点。当左右节点都不存在时结束 var widthOfBinaryTree = function(root) { var que = [root] var max = 0 while(que.length) { var len = que.length var left = 0 var right = que.length - 1 …","tags":null,"title":"662-二叉树最大宽度","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时 10min\n直接用二叉搜索树中序遍历递增的性质就好了\n中序遍历找到左孩子 pre 是否存在 不存在，赋值 ，找右孩子，返回 1 存在，是否比 val 小 是，非递增，返回 false 否，递增，赋值 pre ，返回 1 var isValidBST = function(root) { var preVal = null var DFS = function(root) { var left = true var right = true if (root.left) { left = DFS(root.left) } if (preVal !== null) { if (preVal \u0026gt;= root.val) return false preVal = root.val } else { preVal = root.val } if (root.right) { right = DFS(root.right) } return left \u0026amp;\u0026amp; right } return DFS(root) };","date":1615593600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"97bd9391e1ec80eda0c36153ad084dff","permalink":"https://siqi-liu.com/zh/post/98-%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","publishdate":"2021-03-13T00:00:00Z","relpermalink":"/zh/post/98-%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"post","summary":"用时 10min 直接用二叉搜索树中序遍历递增的性质就好了 中序遍历找到左孩子 pre 是否存在 不存在，赋值 ，找右孩子，返回 1 存在，是否比 val 小 是，非递增，返回 false 否，递增，赋值 pre ，返回 1 var isValidBST = function(root) { var preVal = null var DFS = function(root) { var left = true var right = …","tags":null,"title":"98-验证二叉搜索树","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：60min\n思路还是中序遍历，使用pre缓存前一个节点，因为中序遍历是递增的，所以一定是先找到大的，再找到小的。\n所以第一个出问题的是pre，第二个是root\nvar recoverTree = function(root) { var left = null var right = null var pre = null // 一定是先找到大的，再找到小的，所以第一个出问题的是pre，第二个出问题的是root var DFS = function (root) { if (root.left) { DFS(root.left) } if (pre) { console.log(pre.val) if (pre.val \u0026gt; root.val) { if(!left) { left = pre right = root } else { right = root } } pre = root } else { pre = root } if (root.right) { DFS(root.right) } } DFS(root) if (left \u0026amp;\u0026amp; right) { [left.val,right.val] = [right.val,left.val] } return root}","date":1615593600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"5b3dd461bd35f0d512b6dab6f9920695","permalink":"https://siqi-liu.com/zh/post/99-%E6%81%A2%E5%A4%8D%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","publishdate":"2021-03-13T00:00:00Z","relpermalink":"/zh/post/99-%E6%81%A2%E5%A4%8D%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"post","summary":"用时：60min 思路还是中序遍历，使用pre缓存前一个节点，因为中序遍历是递增的，所以一定是先找到大的，再找到小的。 所以第一个出问题的是pre，第二个是root var recoverTree = function(root) { var left = null var right = null var pre = null // 一定是先找到大的，再找到小的，所以第一个出问题的是pre，第二个出问题的是root var DFS = …","tags":null,"title":"99-恢复二叉搜索树","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时:30min\n题目分两个部分需要解决，搜索和删除\n搜索部分就是找到节点，可以根据二叉搜索树的性质来找到需要的节点\n删除分三种情况\n只有左孩子，用左孩子代替 只有右孩子，用右孩子代替 都没有，则赋值为null （ 由于前面有递归赋值，所以这边可以成功 都有，则储存左孩子后，用右孩子代替，然后找到左孩子在右边的位置 var deleteNode = function(root, key) { function insert(root,node) { if (root.val \u0026gt; node.val) { if (!root.left) { root.left = node } else { insert(root.left,node) } } else { if (!root.right) { root.right = node } else { insert(root.right,node) } } } function search(root) { if (!root) { return null } if (root.val \u0026gt; key) { root.left = search(root.left) } else if (root.val \u0026lt; key){ root.right = search(root.right) } else { if (!root.left \u0026amp;\u0026amp; !root.right) { root = null } else if (root.left \u0026amp;\u0026amp; root.right) { var left = root.left root = root.right insert(root,left) } else if (root.left) { root = root.left } else if (root.right) { root = root.right } } return root } return search(root) };需要注意的点\n考虑节点不存在的情况 删除一个节点的方法，可以给左（右 节点赋值为递归的返回值 ","date":1615420800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"a12fbd2b0b488bb1521624b6d5fd08a0","permalink":"https://siqi-liu.com/zh/post/450-%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9/","publishdate":"2021-03-11T00:00:00Z","relpermalink":"/zh/post/450-%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9/","section":"post","summary":"用时:30min 题目分两个部分需要解决，搜索和删除 搜索部分就是找到节点，可以根据二叉搜索树的性质来找到需要的节点 删除分三种情况 只有左孩子，用左孩子代替 只有右孩子，用右孩子代替 都没有，则赋值为null （ 由于前面有递归赋值，所以这边可以成功 都有，则储存左孩子后，用右孩子代替，然后找到左孩子在右边的位置 var deleteNode = function(root, key) { function …","tags":null,"title":"450-删除二叉搜索树中的节点","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时 ：20min\n与上一道题类似，多的是分了父节点在区间内和不在区间内的情况\nvar trimBST = function(root, low, high) { var add = function (root,node) { if (!node || !root) return null if (node.val \u0026gt; root.val ) { if (!root.right) { root.right = node } else { add(root.right,node) } } if (node.val \u0026lt; root.val) { if (!root.left) { root.left = node } else { add(root.left,node) } } } var walk = function (root) { if (!root) return root if (root.val \u0026lt; low || root.val \u0026gt; high) { var left = walk(root.left) var right = walk(root.right) if (!left \u0026amp;\u0026amp; !right) { root = null } else if (right \u0026amp;\u0026amp; left) { root = right add(root,left) } else if (left) { root = left } else { root = right } } else { root.left= walk(root.left) root.right = walk(root.right) } return root } return walk(root) };看了解析之后发现可以更简单，思路是如果大于右边界，就直接在左分支找； 如果小于左边界，就直接在右分支找。这样就省去了删除节点的步骤\nvar trimBST = function(root, low, high) { if(root == null) return null if (root.val \u0026gt; high) return trimBST(root.left,low,high) if(root.val \u0026lt; low) return trimBST(root.right,low,high) root.left = trimBST(root.left,low,height) root.right = trimBST(root.right,low,height) return root};","date":1615420800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"63eaf462aa79530a8b3d434967a666ec","permalink":"https://siqi-liu.com/zh/post/669-%E4%BF%AE%E5%89%AA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","publishdate":"2021-03-11T00:00:00Z","relpermalink":"/zh/post/669-%E4%BF%AE%E5%89%AA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"post","summary":"用时 ：20min 与上一道题类似，多的是分了父节点在区间内和不在区间内的情况 var trimBST = function(root, low, high) { var add = function (root,node) { if (!node || !root) return null if (node.val \u003e root.val ) { if (!root.right) { root.right = node } else { …","tags":null,"title":"669-修剪二叉搜索树","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时 ： 没做出来\n没做出来的主要原因是一直想着怎么用栈来做，写到后面看栈的答案看着看着发现递归最简单\n递归 思路很简单，首先写出来怎么插入子节点\n如果比父节点小，且父节点左为空，直接做左节点 如果比父节点大，且父节点右为空，直接做右节点 比父节点小，则父节点赋值为父节点的左节点，回到 1 比父节点大，则父节点赋值为父节点的右节点，回到1 遍历整个数组，一个个插入即可得到结果 var bstFromPreorder = function(preorder) { var add = function (node,val) { if (val \u0026lt; node.val \u0026amp;\u0026amp; !node.left) { node.left = new TreeNode(val) } if (val \u0026gt; node.val \u0026amp;\u0026amp; !node.right) { node.right = new TreeNode(val) } if (val \u0026lt; node.val) { add(node.left,val) } if (val \u0026gt; node.val) { add(node.right,val) } } var root = new TreeNode(preorder.shift()) for(let i = 0 ; i \u0026lt; preorder.length ; i ++) { add(root,preorder[i]) } return root};","date":1615334400,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"ad86dfc8496c7453561e2cb2473d194b","permalink":"https://siqi-liu.com/zh/post/1008-%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","publishdate":"2021-03-10T00:00:00Z","relpermalink":"/zh/post/1008-%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"post","summary":"用时 ： 没做出来 没做出来的主要原因是一直想着怎么用栈来做，写到后面看栈的答案看着看着发现递归最简单 递归 思路很简单，首先写出来怎么插入子节点 如果比父节点小，且父节点左为空，直接做左节点 如果比父节点大，且父节点右为空，直接做右节点 比父节点小，则父节点赋值为父节点的左节点，回到 1 比父节点大，则父节点赋值为父节点的右节点，回到1 遍历整个数组，一个个插入即可得到结果 var bstFromPreorder = …","tags":null,"title":"1008-前序遍历构造二叉搜索树","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时 ：25min\n主要思路是利用栈，和普通的层序遍历不同的是每次都把栈清空\nvar connect = function(root) { if (!root) return null var stack = [root] while(stack.length) { var _stack = [...stack,null] stack = [] var pre = _stack.shift() while(_stack.length || pre) { pre.left \u0026amp;\u0026amp; stack.push(pre.left) pre.right \u0026amp;\u0026amp; stack.push(pre.right) pre.next = _stack.shift() pre = pre.next } } return root};很显然我使用的 On 的内存空间是不符合题目的常数空间的，考虑到每一层的链接可以依靠父节点，以链表的形式找到。如下代码\nwhile(pre) { var cur = pre while(cur) { cur.left.next = cur.right cur.right.next = cur.next.left cur = cur.next } }剩下的需要考虑的有\npre需要保存为最左边的节点，以便于后面以链表的形式进行连接 第一层的next为空 最后一层没有左右节点 ","date":1615334400,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"2eb56aa2451421c8ee127b3ef652d384","permalink":"https://siqi-liu.com/zh/post/116-%E5%A1%AB%E5%85%85%E6%AF%8F%E4%B8%AA%E8%8A%82%E7%82%B9%E7%9A%84%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%8F%B3%E4%BE%A7%E8%8A%82%E7%82%B9%E6%8C%87%E9%92%88/","publishdate":"2021-03-10T00:00:00Z","relpermalink":"/zh/post/116-%E5%A1%AB%E5%85%85%E6%AF%8F%E4%B8%AA%E8%8A%82%E7%82%B9%E7%9A%84%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%8F%B3%E4%BE%A7%E8%8A%82%E7%82%B9%E6%8C%87%E9%92%88/","section":"post","summary":"用时 ：25min 主要思路是利用栈，和普通的层序遍历不同的是每次都把栈清空 var connect = function(root) { if (!root) return null var stack = [root] while(stack.length) { var _stack = [...stack,null] stack = [] var pre = _stack.shift() while(_stack.length …","tags":null,"title":"116-填充每个节点的下一个右侧节点指针","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时： 抄答案才做出来\n思路很容易看出来是利用递归，但是coding才是真正的问题。\n我们需要返回的是根节点，但是递归是从下至上的\n尝试用动态规划解决，dp(n) 是 dp(x) 与 dp (n - x - 1) 结果的组合\nvar allPossibleFBT = function(n) { var dp = [] var buld = function (n) { for(let i = 1 ; i \u0026lt;= n ; i ++ ) { if (i === 1) { dp[i] = new TreeNode(0) } else if (i % 2 === 0) { dp[i] = undefined } else { dp[i] = [] for(let left = 1; left \u0026lt; i; left ++) { var leftNodes = dp[left] var rightNodes = dp[i - left - 1] for(let j = 0 ; j \u0026lt; leftNodes.length ; j ++) { for(let k = 0 ; k \u0026lt; rightNodes.length ; k ++) { var node = new TreeNode(0) node.left = leftNodes[j] node.right = rightNodes[k] dp[i].push(node) } } } } } } buld(n) return dp[n] };","date":1615075200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"a58122f7294bb84857b9daad016c94dc","permalink":"https://siqi-liu.com/zh/post/894-%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91/","publishdate":"2021-03-07T00:00:00Z","relpermalink":"/zh/post/894-%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"post","summary":"用时： 抄答案才做出来 思路很容易看出来是利用递归，但是coding才是真正的问题。 我们需要返回的是根节点，但是递归是从下至上的 尝试用动态规划解决，dp(n) 是 dp(x) 与 dp (n - x - 1) 结果的组合 var allPossibleFBT = function(n) { var dp = [] var buld = function (n) { for(let i = 1 ; i \u003c= n ; i ++ ) { …","tags":null,"title":"894-所有可能的满二叉树","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：30min\n序列化很简单，使用BFS可以解决\n反序列化需要用到二叉树的性质，即第 i 个节点的子节点分别为 (i + 1) * 2 - 1 和 ( i + 1) * 2, 流程\n找到父节点，入栈，此时指针 i 在 父节点的 val 上 父节点出栈，找到父节点的左右节点，指针根据规则找到 两遍的值，左右节点入栈， i++ 重复 1 var serialize = function(root) { if (!root) return [] var que = [root] var res = [] while(que.length) { var cur = que.shift() if (cur) { res.push(cur.val); que.push(cur.left); que.push(cur.right); } else { res.push(\u0026#39;null\u0026#39;); } } return res.join(\u0026#39;,\u0026#39;) };/** * Decodes your encoded data to tree. * * @param {string} data * @return {TreeNode} */var deserialize = function(data) { if (!data.length) return null var nodes = data.split(\u0026#39;,\u0026#39;) var i = 0 var root = new TreeNode(nodes[i]) var que = [root] while ( que.length) { var node = que.shift() var left = nodes[ (i + 1) * 2 - 1] var right = nodes[ (i + 1) * 2] if (left !== \u0026#39;null\u0026#39;) { node.left = new TreeNode(left) que.push(node.left) } else { node.left = null } if (right !== \u0026#39;null\u0026#39;) { node.right = new TreeNode(right) que.push(node.right) } else { node.right = null } i++ } return root};","date":1614816e3,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"61a773ded764aaad21844710f4ccc5ec","permalink":"https://siqi-liu.com/zh/post/297-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/","publishdate":"2021-03-04T00:00:00Z","relpermalink":"/zh/post/297-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/","section":"post","summary":"用时：30min 序列化很简单，使用BFS可以解决 反序列化需要用到二叉树的性质，即第 i 个节点的子节点分别为 (i + 1) * 2 - 1 和 ( i + 1) * 2, 流程 找到父节点，入栈，此时指针 i 在 父节点的 val 上 父节点出栈，找到父节点的左右节点，指针根据规则找到 两遍的值，左右节点入栈， i++ 重复 1 var serialize = function(root) { if (!root) return …","tags":null,"title":"297-二叉树的序列化与反序列化","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"用时：25min\n其实是比较常规的DFS类型题目，之所以用这么久是因为对边界值的判断出了问题。\n从 root 出发时， 因为root 是可能没有子节点的，所以 len 值 为 0\n从子节点出发，因为已经判断完字节点，算作当前节点到下一节点已经走一条长度为 1 的边 ， 所以是 1\nvar longestZigZag = function(root) { // true left false right var max = 0 var dfs = function (node,flag,len) { max = Math.max(len,max) if (flag) { node.right \u0026amp;\u0026amp; dfs(node.right,!flag,len + 1) node.left \u0026amp;\u0026amp; dfs(node.left,flag,1) } else { node.left \u0026amp;\u0026amp; dfs(node.left,!flag,len + 1) node.right \u0026amp;\u0026amp; dfs(node.right,flag,1) } } dfs(root,true,0) dfs(root,false,0) return max };","date":1614556800,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"1ff6e7558dbe83700aa7f4c848a00f77","permalink":"https://siqi-liu.com/zh/post/1372-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E9%95%BF%E4%BA%A4%E9%94%99%E8%B7%AF%E5%BE%84/","publishdate":"2021-03-01T00:00:00Z","relpermalink":"/zh/post/1372-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E9%95%BF%E4%BA%A4%E9%94%99%E8%B7%AF%E5%BE%84/","section":"post","summary":"用时：25min 其实是比较常规的DFS类型题目，之所以用这么久是因为对边界值的判断出了问题。 从 root 出发时， 因为root 是可能没有子节点的，所以 len 值 为 0 从子节点出发，因为已经判断完字节点，算作当前节点到下一节点已经走一条长度为 1 的边 ， 所以是 1 var longestZigZag = function(root) { // true left false right var max = 0 var …","tags":null,"title":"1372-二叉树中的最长交错路径","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"102.二叉树的层序遍历 遍历本身很简单，需要考虑的问题是怎么标示每一层，这里可以用迭代和递归两种思路\n迭代 入队 root ，再入队一个null 表示第 0 层的结束 出队 root ，入队左右节点 ， 如果出队的是null，说明一层结束，再入队一个 null 队列不为空，循环 2 ⚠️ 需要注意的点： 队列最后一项如果不做处理将会一直是null，因此需要增加结束条件 var levelOrder = function(root) { var que = [root,null] var res = [] var level = [] if (!root ) return [] while(que.length) { var node = que.shift() if (node) { node.left \u0026amp;\u0026amp; que.push(node.left) node.right \u0026amp;\u0026amp; que.push(node.right) level.push(node.val) } else { res.push([...level]) level = [] if(que.length) { que.push(null) } } } return res };递归 递归解法的重点是传递层数作为参数\nvar levelOrder = function(root) { var res = [] if (!root) return [] var walk = function (node,index) { if (!res[index]) res[index] = [] res[index].push(node.val) node.left \u0026amp;\u0026amp; walk(node.left,index + 1) node.right \u0026amp;\u0026amp; walk(node.right,index + 1) } walk(root,0) return res };步骤如代码所示\n为所在的层数开辟一个数组空间 root 加入数组 如果存在左节点，或者右节点，继续到1，层数 + 1 ","date":1614470400,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"9ae69346058e7b4890abe10e7c69a874","permalink":"https://siqi-liu.com/zh/post/102-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86/","publishdate":"2021-02-28T00:00:00Z","relpermalink":"/zh/post/102-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86/","section":"post","summary":"102.二叉树的层序遍历 遍历本身很简单，需要考虑的问题是怎么标示每一层，这里可以用迭代和递归两种思路 迭代 入队 root ，再入队一个null 表示第 0 层的结束 出队 root ，入队左右节点 ， 如果出队的是null，说明一层结束，再入队一个 null 队列不为空，循环 2 ⚠️ 需要注意的点： 队列最后一项如果不做处理将会一直是null，因此需要增加结束条件 var levelOrder = function(root) { …","tags":null,"title":"102-二叉树的层序遍历","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"为什么前中后序遍历用栈，而层序遍历使用队列 ？ 二叉树的前中后序遍历的迭代时，我们用栈来简化操作，因为它们都是DFS的递归结构，也就是从下到上处理，但是我写代码一定是从root节点开始，所以需要栈，而栈正好是先进先出的。这样我才可以把处理root放在最后。\n层序遍历是BFS，由上至下。最先入队的root也是我想最先处理的。这就是为什么DFS 使用栈而BFS使用队列的原因。\nDFS的算法流程与模板 首先将根节点放入stack中。 从stack中取出第一个节点，并检验它是否为目标。如果找到所有的节点，则结束搜寻并回传结果。否则将它某一个尚未检验过的直接子节点加入stack中。 重复步骤 2。 如果不存在未检测过的直接子节点。将上一级节点加入stack中。 重复步骤 2。 重复步骤 4。 若stack为空，表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传”找不到目标”。 function dfs(root) { if (满足特定条件）{ // 返回结果 or 退出搜索空间 } for (const child of root.children) { dfs(child) } }BFS 的算法流程与模板 首先将根节点放入队列中。 从队列中取出第一个节点，并检验它是否为目标。 如果找到目标，则结束搜索并回传结果。 否则将它所有尚未检验过的直接子节点加入队列中。 若队列为空，表示整张图都检查过了——亦即图中没有欲搜索的目标。结束搜索并回传”找不到目标”。 重复步骤 2。 function bfs(root) { var que = [root] while(que.length) { var node = que.shift() if (node 是我们要找到的) return node node.left \u0026amp;\u0026amp; que.push(node.left) node.right \u0026amp;\u0026amp; que.push(node.right) } }","date":1614470400,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"973bad17e520dd6488945a1c6e8f7781","permalink":"https://siqi-liu.com/zh/post/%E5%85%B3%E4%BA%8E%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%80%BB%E7%BB%93/","publishdate":"2021-02-28T00:00:00Z","relpermalink":"/zh/post/%E5%85%B3%E4%BA%8E%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%80%BB%E7%BB%93/","section":"post","summary":"为什么前中后序遍历用栈，而层序遍历使用队列 ？ 二叉树的前中后序遍历的迭代时，我们用栈来简化操作，因为它们都是DFS的递归结构，也就是从下到上处理，但是我写代码一定是从root节点开始，所以需要栈，而栈正好是先进先出的。这样我才可以把处理root放在最后。 层序遍历是BFS，由上至下。最先入队的root也是我想最先处理的。这就是为什么DFS 使用栈而BFS使用队列的原因。 DFS的算法流程与模板 首先将根节点放入stack中。 从 …","tags":null,"title":"关于二叉树的总结","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"二叉树前中后序遍历总结 从前面的题目中可以看到，二叉树的前中后序遍历的递归方法是类似的，但是迭代的实现完全不同。\n从垃圾回收的三色标记法得到启发，发现他们的共同点\n节点入栈，标记为 0 （ 未访问 ） 节点出栈，若已经访问，出栈。若未访问，标记为已访问，入栈。 节点左右节点 继续到 1 这样，我们可以通过控制节点的入栈顺序，用相似的代码来迭代完成二叉树的前中后序遍历。需要额外做的是多开一个On的空间来保存节点的状态 拿后序遍历做例子\nvar postorderTraversal = function(root) { var stack = [[root,0]] var res = [] while(stack.length) { var [node,color] = stack.pop() if (color === 0) { // 在这里通过控制顺序就可以完成前中后序遍历 stack.push([node,1]) node.right \u0026amp;\u0026amp; stack.push([node.right,0]) node.left \u0026amp;\u0026amp; stack.push([node.left,0]) } else { res.push(node.val) } } return res };","date":1614384e3,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"4f7d139d0fc4c7dc1f8ba8c2f72d22b8","permalink":"https://siqi-liu.com/zh/post/%E5%8F%8C%E8%89%B2%E6%A0%87%E8%AE%B0%E6%B3%95/","publishdate":"2021-02-27T00:00:00Z","relpermalink":"/zh/post/%E5%8F%8C%E8%89%B2%E6%A0%87%E8%AE%B0%E6%B3%95/","section":"post","summary":"二叉树前中后序遍历总结 从前面的题目中可以看到，二叉树的前中后序遍历的递归方法是类似的，但是迭代的实现完全不同。 从垃圾回收的三色标记法得到启发，发现他们的共同点 节点入栈，标记为 0 （ 未访问 ） 节点出栈，若已经访问，出栈。若未访问，标记为已访问，入栈。 节点左右节点 继续到 1 这样，我们可以通过控制节点的入栈顺序，用相似的代码来迭代完成二叉树的前中后序遍历。需要额外做的是多开一个On的空间来保存节点的状态 拿后序遍历做例子 …","tags":null,"title":"双色标记法","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"145.二叉树的后序遍历 1h\n中序遍历的顺序是 左 - 右 - 中\n递归 用递归非常容易\nvar postorderTraversal = function(root) { var res = [] if (!root) return res var travel = function (node) { node.left \u0026amp;\u0026amp; travel(node.left) node.right \u0026amp;\u0026amp; travel(node.right) res.push(node.val) } travel(root) return res };迭代 根节点入栈 判断是否可以出栈，如果能，则记录自己为上一个出栈节点，出栈 不能出栈，分别将右节点和左节点入栈 重复第二个步骤 var postorderTraversal = function(root) { var stack = [root] var res = [] if (!root) { return res } var pre = root while(stack.length) { var node = stack[stack.length - 1] // 当节点的 左 / 右 节点是上一个被输出的节点，代表左右节点都已经被输出过（因为后序遍历根节点在最后） if ( (!node.left \u0026amp;\u0026amp; !node.right) || (node.left === pre || node.right === pre)) { // 只有在没有 左右节点，或者 左右节点都输出过时才可以输出 node = stack.pop() pre = node res.push(node.val) } else { if (node.right) { stack.push(node.right) } if(node.left) { stack.push(node.left) } } } return res };总结 虽然，做完了二叉树的前中后序遍历，但是我有预感用不了多久就会忘记，特别是中序遍历和后序遍历，我完全没有找到两者有什么共同点，所以随后会做一下总结。\n","date":1614297600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"bbc2e2ccb5e209236c9d79e4f12d7e54","permalink":"https://siqi-liu.com/zh/post/145-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86/","publishdate":"2021-02-26T00:00:00Z","relpermalink":"/zh/post/145-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86/","section":"post","summary":"145.二叉树的后序遍历 1h 中序遍历的顺序是 左 - 右 - 中 递归 用递归非常容易 var postorderTraversal = function(root) { var res = [] if (!root) return res var travel = function (node) { node.left \u0026\u0026 travel(node.left) node.right \u0026\u0026 travel(node.right) …","tags":null,"title":"145-二叉树的后序遍历","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"144.二叉树的前序遍历 10min\n二叉树的前序遍历的顺序是中 - 左 - 右\n先遍历完所有的根节点与左节点，然后处理右节点\n可以看出来这是一个递归的行为，递归问题可以用栈来进行简化\n迭代解法 var preorderTraversal = function(root) { if (!root) return null var stack = [root] var res = [] while(stack.length) { var node = stack.pop() res.push(node.val) node.right \u0026amp;\u0026amp; stack.push(node.right) node.left \u0026amp;\u0026amp; stack.push(node.left) } return res };递归解法 递归问题当然可以递归解决\nvar preorderTraversal = function(root) { var res = [] if (!root) return res var travel = function (node) { res.push(node.val) node.left \u0026amp;\u0026amp; travel(node.left) node.right \u0026amp;\u0026amp; travel(node.right) } travel(root) return res };","date":1614211200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"211767bae551c263a8f59cc31f8c41b2","permalink":"https://siqi-liu.com/zh/post/144-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86/","publishdate":"2021-02-25T00:00:00Z","relpermalink":"/zh/post/144-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86/","section":"post","summary":"144.二叉树的前序遍历 10min 二叉树的前序遍历的顺序是中 - 左 - 右 先遍历完所有的根节点与左节点，然后处理右节点 可以看出来这是一个递归的行为，递归问题可以用栈来进行简化 迭代解法 var preorderTraversal = function(root) { if (!root) return null var stack = [root] var res = [] while(stack.length) { var …","tags":null,"title":"144-二叉树的前序遍历","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"94.二叉树的中序遍历 30min\n中序遍历的顺序是 左 - 中 - 右\n递归 用递归非常容易\nvar inorderTraversal = function(root) { var res = [] if (!root) { return res } var travel = function (node) { node.left \u0026amp;\u0026amp; travel(node.left) res.push(node.val) node.right \u0026amp;\u0026amp; travel(node.right) } travel(root) return res };迭代 迭代的步骤复杂一些，因为根节点不是先输出，所以需要保留根节点\n根节点入栈，判断有没有左子节点，如果有，继续入栈，直到叶子结点 出栈，输出，判断是否有右子节点，有则入栈，继续执行2 var inorderTraversal = function(root) { var stack = [] var res = [] if (!root) return res stack.push(root) while(root.left) { stack.push(root.left) root = root.left } while(stack.length) { // 此时栈顶是树中最左的节点 var node = stack.pop() res.push(node.val) // 存在右节点，入栈后继续找左节点 if (node.right) { node = node.right stack.push(node) while(node.left) { stack.push(node.left) node = node.left } } } return res };","date":1614211200,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"de9955f829d2d79794b86d7e49b29e35","permalink":"https://siqi-liu.com/zh/post/94-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86/","publishdate":"2021-02-25T00:00:00Z","relpermalink":"/zh/post/94-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86/","section":"post","summary":"94.二叉树的中序遍历 30min 中序遍历的顺序是 左 - 中 - 右 递归 用递归非常容易 var inorderTraversal = function(root) { var res = [] if (!root) { return res } var travel = function (node) { node.left \u0026\u0026 travel(node.left) res.push(node.val) node.right …","tags":null,"title":"94-二叉树的中序遍历","tldr":null,"type":"post"},{"authors":null,"categories":["工作"],"content":"年前公司登陆注册的需求，预估一个月的工期，结果在过程中项目变得不可控，最后完成用了几乎三个月，为了不辜负这段时间的加班，故复盘一下项目完成的过程。\n关于需求评审 先简单回顾一下需求，注册 / 登陆 都有三个通道 ： 账号/密码 ，手机号，三方 （包括微信扫码和，QQ授权，微博授权），通过一系列的规则和引导，最终走到 登陆/注册 成功页面 ， 再跳转到来源网页。\n需要完成的包括 PC 端，移动端 以及 微信端\n其中移动端和微信端的区别很小，只有能否微信扫码登录的区别。\n看起来简单明了的需求，貌似很多逻辑都能复用，所以 PC 端 估半个月，移动端估一周，剩下的用来IE适配和解决问题。\n关于技术选型 因为涉及到微信扫码登录，后端提议接口使用轮询的方式，我不断去轮询状态的接口从而获取状态，比如微信登陆的流程，需要微信扫码 \u0026gt; 获取手机号 \u0026gt; 引导关注公众号 \u0026gt; 创建 / 绑定 网站账号 的流程，那么大致对应着后端给我的 4 个字段，而我由字段的存在与否来判断进入哪个路由。\n遇到了哪些问题，及解决方案 老代码 老代码用屎山来形容毫不为过，页面由各种html 内连 css / js 和php 语句拼凑而成，一个页面四五千行的代码，甚至还和其它项目的css 产生着耦合 （引用其公共css )，php 的include 层层嵌套，在深层中还存在着JS的逻辑（我唯一保留的一点三方登陆的老代码在后面引入了难以排查的bug）\n解决办法是对前端进行了重构，一开始为了把多个页面的逻辑写进一个PHP文件中，监听浏览器的hash值改变路由，后面发现每个路由都存在自己的初始化逻辑，于是又加上了简单的生命周期。 后面又发现有复用组件的需求，干脆上了 vue框架，script 标签引入。\n代码复用 因为老项目路径的一些原因，import 是无法使用的，只好使用include 来代替。\n需要复用的变量全部做成全局，用 symbol 或者 单例 防止变量污染。\nvue 的模板写在一个html文件，逻辑部分用 x-template 的 script 标签 include 引入 vue的模板代码。\n样式表 使用 node-sass 监听 scss 文件的改变编译scss，生成对应的 css\n兼容性 项目需要兼容到IE10 ，babel-browser 和 pollyfill 是一定要有的。实时编译会一定程度影响性能，暂时还没有想到解决办法。\n然后样式需要使用 post-css 进行兼容，直接用 vscode 的prefixer插件了。\n轮询 每个页面都需要进行接口的轮询，如果每个路由都维护自己的定时器，会存在很多的重复代码，也有内存泄漏的风险。\n所以增加了一个轮询的单例类来维护轮询的逻辑，利用路由守卫在created的时候启动定时器并传入一些需要的参数与回调，在destory之后进行销毁。\n这样的好处是防止了忘记手动销毁而导致多个定时器的同时存在，并且方便了对接口的返回值做统一的处理。\n反思以及优化 总体来说，前端踩的坑就是在不使用任何打包工具的情况下强行使用了 es6 + vue + sass，解决了一些问题，但同时也引入了不少问题\n代码维护： 比起之前css样式表的杂乱无章，sass 确实清晰了不少，但同样也带来了问题，因为没有任何的声明文件，其他人在接手我的代码后是不知道需要用node-sass 进行编译的。\n用户体验： vue-router 的路由跳转由于是局部刷新吗，相比传统的a标签跳转刷新体验会好一些。但是由于轮询存在几秒的间隔，一些接口的返回会很慢。 而大部分用户注册肯定是不希望遇到卡顿的。\n关于性能： babel 的生产环境编译会影响性能，不过可以在版本稳定了之后在生产环境使用babel编译完成的代码。轮询如果用户一直不进行操作也会占用服务器资源，可以使用websoket 和 普通的ajax来代替轮询的实现。\n","date":1613865600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"bb9274c59d1161fba265a13e1dbfbca4","permalink":"https://siqi-liu.com/zh/post/%E5%85%B3%E4%BA%8E%E7%99%BB%E9%99%86%E6%B3%A8%E5%86%8C%E9%9C%80%E6%B1%82%E7%9A%84%E5%A4%8D%E7%9B%98/","publishdate":"2021-02-21T00:00:00Z","relpermalink":"/zh/post/%E5%85%B3%E4%BA%8E%E7%99%BB%E9%99%86%E6%B3%A8%E5%86%8C%E9%9C%80%E6%B1%82%E7%9A%84%E5%A4%8D%E7%9B%98/","section":"post","summary":"年前公司登陆注册的需求，预估一个月的工期，结果在过程中项目变得不可控，最后完成用了几乎三个月，为了不辜负这段时间的加班，故复盘一下项目完成的过程。 关于需求评审 先简单回顾一下需求，注册 / 登陆 都有三个通道 ： 账号/密码 ，手机号，三方 （包括微信扫码和，QQ授权，微博授权），通过一系列的规则和引导，最终走到 登陆/注册 成功页面 ， 再跳转到来源网页。 需要完成的包括 PC 端，移动端 以及 微信端 其中移动端和微信端的区别很小 …","tags":null,"title":"关于登陆注册需求的复盘","tldr":null,"type":"post"},{"authors":null,"categories":["leetcode"],"content":"递归 前序遍历：中-左-右\n中序遍历：左-中-右\n因此可以先从前序遍历找到根元素，再由中序确定左右子树的个数\nvar buildTree = function(preorder, inorder) { if (preorder.length === 0 || inorder.length=== 0) return null let nodeVal = preorder.shift() let node = new TreeNode(nodeVal) let index = inorder.indexOf(nodeVal) node.left = buildTree(preorder.slice(0,index), inorder.slice(0,index)) node.right = buildTree(preorder.slice(index),inorder.slice(index + 1)) return node };优化 slice 是很耗性能的，其实没有必要传递数组，函数传递指针即可\nvar buildTree = function(preorder, inorder) { var helper = function (p_start,p_end,i_start,i_end) { if (p_start \u0026gt; p_end || i_start \u0026gt; i_end ) return null let nodeVal = preorder[p_start] let node = new TreeNode(nodeVal) let index = inorder.indexOf(nodeVal) let left = index - i_start node.left = helper(p_start + 1 , p_start + left , i_start ,index - 1) node.right = helper(p_start + left + 1,p_end, index + 1,i_end) return node } return helper(0,preorder.length - 1,0,preorder.length - 1) };总结：\n利用 前序和中序的性质，定位根节点，求出左右子树的个数。然后递归构建左右子树即可\n","date":1611705600,"description":"","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"7708664b3805652c15cf4339cfbc2d1c","permalink":"https://siqi-liu.com/zh/post/105-%E4%BB%8E%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91-1/","publishdate":"2021-01-27T00:00:00Z","relpermalink":"/zh/post/105-%E4%BB%8E%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91-1/","section":"post","summary":"递归 前序遍历：中-左-右 中序遍历：左-中-右 因此可以先从前序遍历找到根元素，再由中序确定左右子树的个数 var buildTree = function(preorder, inorder) { if (preorder.length === 0 || inorder.length=== 0) return null let nodeVal = preorder.shift() let node = new …","tags":null,"title":"105-从前序遍历与中序遍历序列构造二叉树-1","tldr":null,"type":"post"},{"authors":null,"categories":["Life"],"content":"一年多没有更新博客，拉下来 hexo s 发现生成的页面是空白的，并且也没有报错\n查了资料发现是没有 下载 _config.yml 中配置的 theme: next\n于是 去 next 官网找到 相应的下载命令\ngit clone https://github.com/iissnan/hexo-theme-next themes/next发现下载的速度令人发指 ，于是 切换 npm 包源\ngit clone https://github.com.cnpmjs.org/iissnan/hexo-theme-next themes/next之后再 hexo s --debug ，久违的博客页面终于显示了出来。\n接下来就是部署到 github page 上\n新的问题 hexo d -g 之后发现报错 The \u0026#34;mode\u0026#34; argument must be integer. Received an instance of Object\n网上查找发现是 因为 npm 版本过高引起的问题\n我总不能切回旧版本 node ，写别的项目再切回去吧\n本着用新不用旧的原则 ，开始升级 hexo\n升级 hexo 这里使用 npm-upgrade 来进行 packge.json 的更新\nnpm install -g npm-upgrade npm-upgrade npm i操作后我的 package 如下\n{ \u0026#34;name\u0026#34;: \u0026#34;hexo-site\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;0.0.0\u0026#34;, \u0026#34;private\u0026#34;: true, \u0026#34;hexo\u0026#34;: { ​ \u0026#34;version\u0026#34;: \u0026#34;5.2.0\u0026#34; }, \u0026#34;dependencies\u0026#34;: { ​ \u0026#34;hexo\u0026#34;: \u0026#34;^5.2.0\u0026#34;, ​ \u0026#34;hexo-asset-image\u0026#34;: \u0026#34;^1.0.0\u0026#34;, ​ \u0026#34;hexo-deployer-git\u0026#34;: \u0026#34;^2.1.0\u0026#34;, ​ \u0026#34;hexo-generator-archive\u0026#34;: \u0026#34;^1.0.0\u0026#34;, ​ \u0026#34;hexo-generator-category\u0026#34;: \u0026#34;^1.0.0\u0026#34;, ​ \u0026#34;hexo-generator-index\u0026#34;: \u0026#34;^2.0.0\u0026#34;, ​ \u0026#34;hexo-generator-searchdb\u0026#34;: \u0026#34;^1.3.3\u0026#34;, ​ \u0026#34;hexo-generator-tag\u0026#34;: \u0026#34;^1.0.0\u0026#34;, ​ \u0026#34;hexo-renderer-ejs\u0026#34;: \u0026#34;^1.0.0\u0026#34;, ​ \u0026#34;hexo-renderer-marked\u0026#34;: \u0026#34;^3.3.0\u0026#34;, ​ \u0026#34;hexo-renderer-stylus\u0026#34;: \u0026#34;^2.0.1\u0026#34;, ​ \u0026#34;hexo-renderer-swig\u0026#34;: \u0026#34;^1.1.0\u0026#34;, ​ \u0026#34;hexo-server\u0026#34;: \u0026#34;^2.0.0\u0026#34;, ​ \u0026#34;hexo-theme-next\u0026#34;: \u0026#34;^8.1.0\u0026#34; } }升级后的新问题 升级后部署到 github page ，果然又出现了新的问题，发现部署之后的网站只显示了\nhexo {% extends \u0026#39;_layout.swig\u0026#39; %}\n使用 debug 发现一个 warn\nINFO Validating config WARN Deprecated config detected: \u0026#34;external_link\u0026#34; with a Boolean value is deprecated. See https://hexo.io/docs/configuration for more details.原来是 external_link 这个 属性被改成了 object 类型，马上去修改 _config.yml\nexternal_link: enable: true再次 hexo s 发现 问题并没有解决\n从 issue： 与 hexo 5 不兼容 找到了解决办法，原来是 Hexo 5 把 swig 渲染插件删了，需要单独安装\nnpm i hexo-renderer-swig再 hexo s\n成功\n小调整 next 主题下的 翻页出现显示的问题，直接改成\n{% if page.prev or page.next %} \u0026lt;nav class=\u0026#34;pagination\u0026#34;\u0026gt; {{ paginator({ prev_text: \u0026#39;上页\u0026#39;, next_text: \u0026#39;下页\u0026#39;, mid_size: 1 }) }} \u0026lt;/nav\u0026gt; {% endif %}","date":1607690275,"description":"填坑日记","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"78482b3eaa27e8bdfbd9140bc0bc577a","permalink":"https://siqi-liu.com/zh/post/legacy/hexo-%E5%8D%87%E7%BA%A7%E5%88%B0-5/","publishdate":"2020-12-11T12:37:55Z","relpermalink":"/zh/post/legacy/hexo-%E5%8D%87%E7%BA%A7%E5%88%B0-5/","section":"post","summary":"填坑日记","tags":null,"title":"Hexo 升级到 5","tldr":null,"type":"post"},{"authors":null,"categories":["Life"],"content":"时隔一年的更新\n去年入职大华之后就一直偷懒没有更新了。\n马上到 2021 年了，觉得自己有必要对过去做一个总结。想到哪就写到哪里吧\n关于前端 面试的时候，总是被问到自己怎么理解前端，有什么规划\n花了四年时间我应该是还没有搞清楚这个问题\n什么都不会的时候，想着学会 xxx 框架，理解 xxx 的源码，自己就会离真相再进一步\n于是开始看 vue， 看 react ， 开始用 ts ，开始玄学配 webpack ，用 nodeJs ，开始刷算法题。\n然后又渐渐喜欢简单 ， ts 写着太麻烦，弃了 ； 配什么 webpack，umi 不香吗 ；imgcook 自动生成代码不快么；写个脚本然后 egg 无脑 curd 不好吗。\n可能这就是大部分小厂前端的处境吧 ，并没有什么值得一提的项目亮点，在写业务与学习新知识中周而复始，又时不时被大佬开发出来的工具惊艳，npm i ， npm start 之后发出真香的感叹。\n关于组件 参与大华组件库的建设让我获得了一些宝贵的经验。我觉得难点大部分不在于怎么把业务功能实现出来，而是怎么让组件在后面的修改与扩展中可维护。\n举个一个用鼠标滑动设定多个时间段组件的例子。\n本来的需求是 3 _ 个的时间段，后面加了 1 _ 个时间段的需求。还好时间段的逻辑是抽出来的，所以实现没用太久。\n本来需求是时间段区间 00:00:00 - 24:00:00 ，后来又加了 23:59:59 的需求 。只好增加一个配置，又修改了时间标尺的显示。\n本来是一个按钮用来弹窗复制时间段，后面陆续加了好几个不同功能的按钮。这就麻烦了，本身为了计算方便设置的定宽组件，只好改成自适应，影响到了很多地方的计算。\n后来又出现了加锁 ，主题可定制，按日修改 变成 按月 ….的各种需求\n总结起来有几点\n做好解藕，不要和数据产生耦合，拆分成的各个组件之间也减少依赖。否则需求稍微一变更就需要在几千行的组件代码中埋头改。\n不要相信一开始提需求的人 “用在这个页面就好”，要考虑组件作为子组件，作为小屏幕，作为弹窗内容的情况。\n利用好 React 中的高阶组件，有利于简化组件代码。\n做好单元测试。\n关于数据结构 / 算法 一年前跟大部分前端一样，想着算法根本不可能用在项目中，写了没什么用处。\n今年由于想去苏州微软。开始刷 lc 题目。\n在自己刷了一百多题之后，改变了这个看法。\n首先就是多了一种思维方式，举个例子，如果我写了三层的循环，我会去考虑要不要优化时间复杂度，或者要不要剪枝，遇到一些后端传过来的很难用的数据，会去考虑有没有什么数据结构比如字典树，图类去优化。\n对于面试，小厂很难有值得一提的亮点可以说出来，而由于消息的闭塞，自己千辛万苦做出来的东西，可能是人家早就实现出来的轮子，考虑的还比我更好。而八股文背的再好，也只是不扣分而已。但是做题，至少能为我提供一个能跟大厂出来的同学一战的机会。\n这些如果不去尝试，一味听别人说前端用不到算法，是不能体会到的。\n未来计划 继续佛系写算法题，后面会更新到文章里。\n最近被 imgcook 惊艳到 ，在工作使用之余，想看看相关的 AI 识别的知识。\n","date":1607645850,"description":"从大华离职一个月之后","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"d0a9936970fbaee763ead3e10640246a","permalink":"https://siqi-liu.com/zh/post/legacy/%E6%97%B6%E9%9A%94%E4%B8%80%E5%B9%B4%E7%9A%84%E6%9B%B4%E6%96%B0/","publishdate":"2020-12-11T00:17:30Z","relpermalink":"/zh/post/legacy/%E6%97%B6%E9%9A%94%E4%B8%80%E5%B9%B4%E7%9A%84%E6%9B%B4%E6%96%B0/","section":"post","summary":"从大华离职一个月之后","tags":["JavaScript","Frontend"],"title":"时隔一年的更新","tldr":null,"type":"post"},{"authors":null,"categories":["Frontend"],"content":"关于css伪类选择器:nth-child(n) 这几天面试在一个简单的问题上翻车了。\n问题大概是这样，问我哪个部分被标红\n天真的我回答的是p元素 。\n答案当然是没有任何元素被标红，原因是 nth-child 会先找到所有父元素下第n个子元素，再找其中的p，\n也就是它第一步找到的集合是两个span，其中没有p元素。\n如果要标红那个p，须要这么写选择器：\np:nth-of-type(1)\n这样是 先选择父元素下的p，再找到其中的第一个p。\n但是\n故事到这里并没有完结，我尝试去选择嵌套p标签下的p标签\n令人震惊的是没有任何元素被选中，\n尝试了 p \u0026gt; p :nth-child(1)也没有元素被选中p:nth-of-type(1)也不能选择到元素。\n只有这样才能成功选择到p1 。\n在这里我已经想到可能是违背了HTML标准之类的东西，但p是块状元素，不知道为什么不能嵌套另外一个p呢？所以去mozilla 查看一下。看到了\n每一个HTML元素都必须遵循定义了它可以包含哪一类内容的规则。 这些规则被归类为几个常见的元素内容模型（content model）。每个HTML元素都属于0个、1 个或多个内容模型，每个模型都有一些规则使得元素中的内容必须遵循一个HTML规范文档( HTML-conformant document)。\n其中关于p元素的定义\n描述是p元素表示文本的一个段落，重点是允许的内容是 Phrasing content，再看这个Phrasing content。\n其中不包含p标签，所以我认为是 nth-child 选择器 按照 HTML 规范进行了选择上的优化，所以不会在 p 元素下面再去找p元素了,吗？\n我们再看一下这个结构\n在浏览器是怎么渲染的\n可以看到，外面包裹的p，被浏览器渲染成了两个p元素。可以猜想到，如果CSS写成\np { color:red }span2 也不会被选择中的。\n","date":1551979939,"description":"关于nth-child 和 HTML规范","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"9af8d31a95eb0ed021f2ff8be3e56c32","permalink":"https://siqi-liu.com/zh/post/legacy/css%E4%BC%AA%E7%B1%BB%E9%80%89%E6%8B%A9%E5%99%A8nth-child/","publishdate":"2019-03-07T17:32:19Z","relpermalink":"/zh/post/legacy/css%E4%BC%AA%E7%B1%BB%E9%80%89%E6%8B%A9%E5%99%A8nth-child/","section":"post","summary":"关于nth-child 和 HTML规范","tags":["Frontend"],"title":"css伪类选择器nth-child","tldr":null,"type":"post"},{"authors":null,"categories":["Vue"],"content":"需要准备的 首先安装这些东西\nnodeJs , npm\nmongodb , adminMongo(非必须)\nvue-element-admin\nexpress\n前端的页面我决定用vue-element-admin，git clone 后就可以看到完整的开发框架\n前端登录思路 验证思路是登录成功后，服务端返回token(标识用户的唯一身份),之后将token储存在本地cookie中，下次打开页面或者刷新页面就能记住用户的登录状态。\n在用户登录成功后，会在全局钩子router.beforeEach，判断是否已获得token，在获得token之后去获取用户的信息。\n下面看具体登录页的主要代码\n//index.vue handleLogin() { this.$refs.loginForm.validate(valid =\u0026gt; { if (valid) { this.loading = true this.$store.dispatch(\u0026#39;LoginByUsername\u0026#39;, this.loginForm).then(() =\u0026gt; { this.loading = false this.$router.push({ path: this.redirect || \u0026#39;/\u0026#39; }) }).catch(() =\u0026gt; { this.loading = false }) } else { console.log(\u0026#39;error submit!!\u0026#39;) return false } }) }, 可以点击登录后，先进行了表单验证,然后触发了vuex 的 LoginByUsername 的 action， 从代码中搜索一下这个异步操作做了什么\n// user.js import { loginByUsername, logout, getUserInfo } from \u0026#39;@/api/login\u0026#39; import { getToken, setToken, removeToken } from \u0026#39;@/utils/auth\u0026#39; actions: { LoginByUsername({ commit }, userInfo) { const username = userInfo.username.trim() return new Promise((resolve, reject) =\u0026gt; { loginByUsername(username, userInfo.password).then(response =\u0026gt; { const data = response.data commit(\u0026#39;SET_TOKEN\u0026#39;, data.token) setToken(response.data.token) resolve() }).catch(error =\u0026gt; { reject(error) }) }) }, ... 可以看到这个 LoginByUsername 中调用了login.js 的 loginByUsername 方法，loginByUsername 接受了 username 和 password 两个参数。可以推测 loginByUsername 是发送了请求，在请求完毕之后提交了 SET_TOKEN 这种方法 ，并且调用了 auth.js 中的 setToken\n// login.js import request from \u0026#39;@/utils/request\u0026#39; export function loginByUsername(username, password) { const data = { username, password } return request({ url: \u0026#39;/login/login\u0026#39;, method: \u0026#39;post\u0026#39;, data }) } 可以看到这里是发送了请求，其中request 应该是封装了axios 的方法，具体怎么封装后面再看。\n// user.js mutations: { SET_TOKEN: (state, token) =\u0026gt; { state.token = token }, 可以看到 SET_TOKEN 就是将请求返回的token值更新到vuex的state中。\n//auth.js import Cookies from \u0026#39;js-cookie\u0026#39; const TokenKey = \u0026#39;Admin-Token\u0026#39; export function setToken(token) { return Cookies.set(TokenKey, token) } setToken方法是把token存进cookie中\n后台思路 按照前端的逻辑，我们需要有一个登录的接口，登录的时候从数据库中查询，如果正确就返回token给前端，还需要有一个接口来接收token，返回给前端用户的具体信息。\nexpress 先从express开始说\n通过应用生成器工具express-generator 可以快速创建一个应用的骨架。生成后应该可以看到下面的目录结构。 npm install //安装依赖模块 npm start //启动项目 因为每次修改之后还需要重新 npm start 会比较麻烦，所以我用nodemon来进行热更新。\nnpm install -g nodemon //安装nodemon 就能启动express应用了\n下面看express项目的结构\nbin是应用的启动目录，\nvar http = require(\u0026#39;http\u0026#39;); var port = normalizePort(process.env.PORT || \u0026#39;3000\u0026#39;); app.set(\u0026#39;port\u0026#39;, port); var server = http.createServer(app); server.listen(port); server.on(\u0026#39;error\u0026#39;, onError); server.on(\u0026#39;listening\u0026#39;, onListening); public是项目的静态文件目录\nroutes 控制路由,我们需要在这里做登录的接口\nviews 是视图文件，我们在这里放html文件\nmongodb 注册登录需要数据库，我使用了mongodb ，用mongoose 操作数据库，文档有介绍的就不啰嗦了。\n在express的目录下建立一个database文件夹，建立文件model.js 以及处理文件bdHandler.js\n// models.js module.exports = { user:{ username:{type:String,required:true}, password:{type:String,required:true}, roles:{type:String,required:true} } }; //bdHandel.js //提供其他文件对model的操作 var mongoose = require(\u0026#39;mongoose\u0026#39;); var Schema = mongoose.Schema; var models = require(\u0026#34;./models\u0026#34;); for(var m in models){ mongoose.model(m,new Schema(models[m])); } module.exports = { getModel: function(type){ return _getModel(type); } }; var _getModel = function(type){ return mongoose.model(type); }; 然后我们需要链接到数据库，并把bdHandel设置为全局的以便于在路由中使用\n//在app.js中添加 global.dbHandel = require(\u0026#39;./database/dbHandler\u0026#39;); global.db = mongoose.connect(\u0026#34;mongodb://localhost:27017/ch_users\u0026#34;, { useNewUrlParser: true }) 后台接口 做完后，我们就可以去处理登录的接口部分了\nvar express = require(\u0026#39;express\u0026#39;); var router = express.Router(); var dbhandler = require(\u0026#39;../database/dbHandler\u0026#39;); var jwt = require(\u0026#39;jwt-simple\u0026#39;); router.post(\u0026#34;/login\u0026#34;, function (req, res) { // 从此路径检测到post方式则进行post数据的处理操作 //这里的User就是从model中获取user对象 var User = dbhandler.getModel(\u0026#39;user\u0026#39;); var {username} = req.body; //获取post上来的 data数据中 username User.findOne({ username }, function (err, doc) { //通过此model以用户名的条件 查询数据库中的匹配信息 if (err) { //错误就返回 状态码为500的错误 res.send(500); console.log(err); } else if (!doc) { //查询不到用户名匹配信息，则用户名不存在 res.send({code:400,message:\u0026#39;用户名不存在\u0026#39;}); // 状态码返回404 } else { if (req.body.password != doc.password) { //查询到匹配用户名的信息，但相应的password属性不匹配 res.send({code:400,message:\u0026#39;密码错误\u0026#39;}); } else { //信息匹配成功返回20000，返回token let token = jwt.encode(doc, secret); res.send({code:20000,token}); } } }) }) module.exports = router; 其中用户认证用的是基于 JWT 身份验证的方法，其中的secret是存在服务器上的密码,为了方便我把它作为了一个全局变量\n//app.js global.secret = \u0026#39;yosgi\u0026#39; 别忘了router还需要在app.js中注册\n//app.js var users = require(\u0026#39;./routes/users\u0026#39;); app.use(\u0026#39;/login\u0026#39;, users); 这样我们的登录接口就做完了，在前端页面试试吧。\n前端登录页面 我们的express应用监听的是localhost:3000,因此我们需要往localhost:3000/login/login 的地址发送POST请求。\n首先要把mock中的模拟请求去掉，才能真正发送请求。\n// mock\\index.js // Mock.mock(/\\/login\\/login/, \u0026#39;post\u0026#39;, loginAPI.loginByUsername) 这一段注释掉 点击登录尝试一下 发现是在向\nhttp://serve-dev/login/login 发送请求，猜测是axios方面的baseURL设置的，在utils/request.js中看到下面的代码\n//request.js import axios from \u0026#39;axios\u0026#39; const service = axios.create({ baseURL: process.env.BASE_API, // api 的 base_url timeout: 5000 // request timeout }) 在config的dev.env.js中去掉\n//dev.env.js module.exports = { NODE_ENV: \u0026#39;\u0026#34;development\u0026#34;\u0026#39;, ENV_CONFIG: \u0026#39;\u0026#34;dev\u0026#34;\u0026#39;, …","date":1544724910,"description":"关键词 nodejs vue express mongodb","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"563196f76c5560c56e1233b6191e9522","permalink":"https://siqi-liu.com/zh/post/legacy/vue-express-mongodb-%E6%90%AD%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%90%8E%E5%8F%B0%E7%99%BB%E5%BD%95%E7%B3%BB%E7%BB%9F/","publishdate":"2018-12-13T18:15:10Z","relpermalink":"/zh/post/legacy/vue-express-mongodb-%E6%90%AD%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%90%8E%E5%8F%B0%E7%99%BB%E5%BD%95%E7%B3%BB%E7%BB%9F/","section":"post","summary":"关键词 nodejs vue express mongodb","tags":["Vue","Node.js","Database"],"title":"vue + express + mongodb 搭建一个后台登录系统","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript","JavaScript"],"content":"使用XHR时，POST和GET的对比 发送数据到服务器时，GET方式会更快，因为对于少量数据而言，一个GET请求往服务器只发送一个数据包。而POST请求至少发送两个数据包，一个装载头信息，一个装载POST正文。POST更适合发送大量数据到服务器，一是因为它不关心额外数据包的数量，二是IE对URL的长度有限制。\n对于不会改变服务器状态，只获取数据（幂等行为）的请求应该使用GET。经过GET请求的数据会被缓存起来，有助于提高多次请求的性能。\n动态脚本注入是什么？有什么特点？ var scriptElement = document.createElement(\u0026#39;script\u0026#39;) scriptElement.src = \u0026#39;http://xxx.com/lib.js\u0026#39; document.getElementsByTagName(\u0026#39;head\u0026#39;)[0].appendChild(scriptElement) function jsonCallBack(jsonString) { var data = eval(\u0026#39;(\u0026#39; + jsonString + \u0026#39;)\u0026#39;) } // lib.js jsonCallBack({\u0026#34;status\u0026#34;:1}) 利用JS创建一个新的脚本标签，并设置src属性为不同域的URL，可以进行跨域请求数据。\n不能设置请求头，只能使用GET方式，必须等待所有数据返回才可以访问。\n响应消息必须是可执行的JS代码\n使用脚本注入到页面中的任何代码都可以控制页面，包括修改内容和重定向到其它网站，因此引入外部来源的代码需要小心\nMXHR(Multipart XHR) 有什么特点？ 能把多次的http请求合并成一次请求，减少请求的数量会提升页面的性能。\n元素使用data:URL的方式创建，因此不能被浏览器缓存，当网站在每个页面使用一个独立打包的Js或者CSS文件时不会受此影响，比如单页面使用时，从外部加载一次CSS就可以了。\nBeacons（网络信标） 是什么？ 使用JavaScript创建一个Image对象，并把src属性设置为服务器上脚本的URL,URL包含需要传输的键值对数据。\nvar url = \u0026#39;/status_tracker.php\u0026#39;; var params = [\u0026#39;userName=yosgi\u0026#39;,\u0026#39;step=2\u0026#39;]; (new Image).src = url + \u0026#39;?\u0026#39; + params.join(\u0026#39;\u0026amp;\u0026#39;); // 这段代码会对/status_tracker.php?step=2\u0026amp;time=23311 发送请求 它无需向客户端返回信息，没有图片会实际显示出来。\n虽然性能消耗很小，但因为URL长度有最大值，所以可以发送的数据长度很少。只能靠监听Image对象的onload事件判断服务器是否已经接受数据。\nJSON JSON-P 的区别 在使用XHR时，JSON数据被当成是字符串返回，紧接着字符串被eval()转换成原生对象。\n而JSON-P数据被当成另一个Js文件并作为原生代码执行。\nJSON-P可以跨域使用，涉及敏感数据的时候不应该使用它\n关于自定义格式 创建自定义格式的例子：\n\u0026#39;John;Jack;David\u0026#39; 只需要简单的把数据用分隔符链接，接收后使用split()即可\n创建自定义格式时，最好是使用一个单字符，而且不应该存在于数据之中，ASCII字符表的前几个字符在大多数服务器语言能够正常工作。\n\\u0001 \\u0002 总结优化Ajax的方法： 减少请求数，合并JS和css文件，或者使用MXHR 缩短页面的加载时间，页面主要内容加载之后，使用Ajax获取次要的 知道何时使用成熟的Ajax类库，以及何时编写自己的底层Ajax代码（大多数javascrpt类库不允许直接访问readystatechange事件） ","date":1543416460,"description":"《高性能Javascript》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"b6e701baf6f4760a88e055f4ef10bb83","permalink":"https://siqi-liu.com/zh/post/legacy/ajax%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93%E4%BC%98%E5%8C%96/","publishdate":"2018-11-28T14:47:40Z","relpermalink":"/zh/post/legacy/ajax%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93%E4%BC%98%E5%8C%96/","section":"post","summary":"《高性能Javascript》 知识点整理","tags":["JavaScript"],"title":"ajax数据传输优化","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript","JavaScript"],"content":"怎么理解Javascript是单线程，浏览器是多线程? 浏览器内核是多线程的，（浏览器为每个TAB页面单独启用进程。进程与线程的关系），通常由以下常驻线程组成\nGUI渲染线程\nJavaScript引擎线程\n定时器触发线程\n事件触发线程\n异步http请求线程\nJavascript是单线程的，因为Js需要操作DOM树，如果是多线程的操作DOM,可能会出现UI操作冲突\n怎么记录代码运行时间？ 可以通过原生的Date对象来跟踪代码的运行时间\nvar start = + Date() //(+)可以将Date对象转化为数字 怎么使用定时器分解任务，并增加时间检测机制来改进？ var todo = item.concat() // 克隆原数组 setTimeout(function() { // 获取数组的下个元素并处理 process(todo.shift()) // 如果还有需要处理的元素，则创建另一个定时器 if (todo.length \u0026gt; 0) { setTimeout(arguments.callee, 25) } else { callback(items) } },25) 有的时候只执行一个任务的效率不高，例如：如果处理一个长度为1000项的数组，每处理一项需要1毫秒，如果每个定时器只处理一项，且在两次处理之间产生25毫秒的延时，数组处理的总时间为 （25+1）x 1000毫秒，如果修改成一次处理50个，处理时间为（1000/50） *25 + 1000 = 1500毫秒，比单个处理更快。\nfunction timedProcessArray(items, process, callback) { var todo = items.concat() setTimeout(function () { // 这里不能使用箭头函数 因为箭头函数没有arguments var start = +new Date() do { process(todo.shift()) // 处理完后检测时间，如果所用时间小于50毫秒继续处理 } while(todo.length \u0026gt; 0 \u0026amp;\u0026amp; (+new Date() - start \u0026lt; 50)) if(todo.length \u0026gt; 0 ) { setTimeout(arguments.callee, 25) } else { callback() } },25) } 定时器对性能有影响吗？ 上述的代码使用了定时器序列，同一时间只有一个定时器存在，只有在定时器结束才会创建下一个，这种使用方式不会导致性能问题。\n当多个重复定时器同时创建会出现问题，所有的定时器都在争夺运行时间；间隔在1秒或1秒以上的低频率重复定时器不会出现问题，当在100到200毫秒间的多个重复定时器会出现问题。 因此应该用一个独立的重复定时器，每次执行多个操作。\n总结 JavaScript任务不应该执行过多的时间（100毫秒），否则会使UI更新出现延迟，影响用户体验\n定时器可以让代码延迟执行，可以把长时间运行的代码分解成多个短时间的代码\n关于事件循环 事件循环是主线程从任务队列中读取事件，这个过程是循环不断的，被称为事件循环。\n主线程运行的时候，产生堆（heap）和栈（stack），栈中的代码调用各种外部API，它们在\u0026#34;任务队列\u0026#34;中加入各种事件（click，load，done）。只要栈中的代码执行完毕，主线程就会去读取\u0026#34;任务队列\u0026#34;，依次执行那些事件所对应的回调函数。\n其中定时器事件就是放在任务队列中，setTimeout(fn,100)的含义是。它100毫秒后在\u0026#34;任务队列\u0026#34;的尾部添加一个事件，因此要等到同步任务和\u0026#34;任务队列\u0026#34;现有的事件都处理完，才会得到执行。所以并没有办法保证，回调函数一定会在setTimeout()指定的时间执行。\n","date":1542905721,"description":"《高性能Javascript》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"3a7beee704686a29f0a91c4fcbf2c4aa","permalink":"https://siqi-liu.com/zh/post/legacy/ui%E7%95%8C%E9%9D%A2%E5%93%8D%E5%BA%94%E4%BC%98%E5%8C%96/","publishdate":"2018-11-22T16:55:21Z","relpermalink":"/zh/post/legacy/ui%E7%95%8C%E9%9D%A2%E5%93%8D%E5%BA%94%E4%BC%98%E5%8C%96/","section":"post","summary":"《高性能Javascript》 知识点整理","tags":["JavaScript"],"title":"UI界面响应优化","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript","JavaScript"],"content":"怎么理解回溯？回溯失控是什么？ 正则表达式在匹配字符串时会从左到右测试表达式的组成部分，当遇到量词（* + ? {2}）和分支(|)时需要做决策。\n做出决策之后，可能会记录其它的选择，以备返回时使用。\n如果当前匹配成功，则继续测试后面的表达式。如果都成功则结束。\n如果当前找不到匹配值或者后面匹配失败，则正则表达式会回溯到最后一个决策点，并选择一个记录过的选择。\n过程会一直进行，直到找到匹配项或者分支的排列组合全部失败。它将放弃匹配转而移动到字符串的下一个字符。\n回溯失控是正则表达式导致浏览器假死的情况。\n举一个例子：\n用一个匹配标签的正则\nlet reg = /\u0026lt;html\u0026gt;[\\s\\S]*?\u0026lt;head\u0026gt;[\\s\\S]*?\u0026lt;title\u0026gt;[\\s\\S]*?\u0026lt;\\/title\u0026gt;[\\s\\S]*?\u0026lt;\\/head\u0026gt;[\\s\\S]*?\u0026lt;body\u0026gt;[\\s\\S]*?\u0026lt;\\/body\u0026gt;[\\s\\S]*?\u0026lt;\\/html\u0026gt;/ 对字符串\nlet str = `\u0026lt;html\u0026gt;\u0026lt;head\u0026gt;\u0026lt;title\u0026gt;\u0026lt;/title\u0026gt;\u0026lt;/head\u0026gt;\u0026lt;body\u0026gt;\u0026lt;/body\u0026gt;` 进行匹配，当最后一个[\\s\\S]*?扩展到字符串末尾，因为最后html闭合标签的缺失，正则表达式会尝试扩展到倒数第二个[\\s\\S]*?来匹配最后的body闭合标签，然后继续查找第二个闭合的body标签，直到字符串末尾，以此类推\n惰性量词和贪婪量词 贪婪量词(*)表示重复0次或者多次，并尽可能匹配多次。\n惰性量词(*?)会重复匹配0次会多次，并尽可能匹配0次\n例子：\nlet reg1 = /\u0026lt;html\u0026gt;[\\s\\S]*?\u0026lt;\\/html\u0026gt;/ let reg2 = /\u0026lt;html\u0026gt;[\\s\\S]*\u0026lt;\\/html\u0026gt;/ let str = `\u0026lt;html\u0026gt;\u0026lt;html\u0026gt;sdasds\u0026lt;\\/html\u0026gt;\u0026lt;\\/html\u0026gt;` str.match(reg1)//[\u0026#39;\u0026lt;html\u0026gt;\u0026lt;html\u0026gt;sdasds\u0026lt;/html\u0026gt;\u0026#39;] str.match(reg2)//[\u0026#39;\u0026lt;html\u0026gt;\u0026lt;html\u0026gt;sdasds\u0026lt;/html\u0026gt;\u0026lt;/html\u0026gt;\u0026#39;] 使用正则表达式去除首尾空白？ 使用两个正则 String.prototype.$trim = function() { return this.replace(/^\\s+/,\u0026#34;\u0026#34;).replace(/\\s+$/,\u0026#34;\u0026#34;) } 使用了两个子表达式，一个去除头部的空白，另一个去除尾部的空白。 相同的思路，可以用一个表达式\nString.prototype.$trim = function() { return this.replace(/^\\s+|\\s+$/g, \u0026#34;\u0026#34;) } 这个会比上面的方法要慢，因为两个分支选项在每个字符串匹配都会被测试一遍\n使用捕获组 String.prototype.$trim = function() { return this.replace(/^\\s*([\\s\\S]*?)\\s*$/, \u0026#34;$1\u0026#34;) } 捕获数组中的惰性量词会导致正则表达式进行回溯，因为[\\s\\S]类的惰性量词*?要求尽可能减少重复次数，因此正则表示式每匹配一个字符都要停下来尝试匹配剩下的\\s*?。考虑一下优化方案\nString.prototype.$trim = function() { return this.replace(/^\\s*([\\s\\S]*\\S)?\\s*$/, \u0026#34;$1\u0026#34;) } 考虑到上一个的性能原因，把惰性量词替换成了贪婪量词，为了保证捕获组匹配到最后的非空字符，末尾需要\\S。 这个表达式的过程，[\\s\\S]*中的贪婪量词*表示重复任意字符类直到字符串末尾，然后正则表达式每次回溯一个字符，直到匹配到后面的\\S,或者回溯到第一个字符。\n不使用正则表达式 String.prototype.$trim = function() { var start = 0, end = this.length - 1 ws = \u0026#34; \\n\\r\\t\\f\u0026#34; // 一些空白字符，不包括全部 while (ws.indexOf(this.charAt(start)) \u0026gt; -1) { start ++ } while (end \u0026gt; start \u0026amp;\u0026amp; ws.indexOf(this.charAt(end)) \u0026gt; -1) { end -- } return this.slice(start, end + 1) //slice方法返回的数组项不包括第二个参数下标的项 } 这个版本的优点是不受字符串总长度的影响，缺点是不宜处理前后大段的空白字符，因为循环遍历的效率比不上正则表达式。（记不全空白字符也是一个缺点）\n混合解决方案 可以使用正则表达式过滤头部空白，使用非正则表达式的方法去除尾部字符\nString.prototype.$trim = function() { var str = this.replace(/^\\s+/,\u0026#34;\u0026#34;), end = str.length - 1, ws = /\\s/; while (ws.test(str.charAt(end))) { end -- } return str.slice(0,end + 1) } 该方案在循环中使用一个正则表达式来检查字符串的末尾是否为空白，能直接使用浏览器定义的空白字符列表。\n结论 在基于正则表达式的方案中，字符串的总长度比修剪掉的字符数量更加影响性能。非正则表达式从末尾开始查找，不受字符总长的影响，但是受到修剪空格数量的影响。\n简单的使用两次正则是不错的方案，混合方案在处理长字符串的时候特别快，代价是代码稍长，在处理尾部长空白时稍有不足。\n有哪些提高正则表达式效率的方法？ 正则表达式以简单，必须的字元开始 好的起始标记通常是一个锚(^或$),特定字符串，字符类([a-z]或\\d速记符)。 避免以分组或者选择字元开头/one|two/\n减少分支数量，缩小分支范围 可以通过使用字符集来减少对分支的需求比如将(.|\\r|\\n)替换成[\\s\\S],因为字符集比分支更快。当分支必不可少时，将常用分支放在最前面，这样可能可以被尽快检测到。\n使用非捕获数组 捕获数组小号时间和内存记录反向引用\n只捕获感兴趣的文本以减少后续处理 如果需要引用匹配的一部分，应该捕获那些必要的部分再用反向引用处理。 比如需要匹配引号括起来的字符串内容，应该使用/\u0026#34;([^\u0026#34;]*)\u0026#34;/ 而不是/\u0026#34;[^\u0026#34;]*\u0026#34;/再从结果中处理引号。\n使用合适的量词 贪婪和惰性量词的匹配过程有很大的区别\n将正则表达式赋值给变量并重用 避免在循环体内重复编译正则表达式\n将复杂正则表达式拆分成简单的片段 比如去除首尾空字符的两次replace\n","date":1541519294,"description":"《高性能Javascript》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"8d579b38d2766e115915205d3b6b800d","permalink":"https://siqi-liu.com/zh/post/legacy/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/","publishdate":"2018-11-06T15:48:14Z","relpermalink":"/zh/post/legacy/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/","section":"post","summary":"《高性能Javascript》 知识点整理","tags":["JavaScript"],"title":"正则表达式性能优化","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript","JavaScript"],"content":"for ,while, do-while 三种循环方式的性能对比？怎么改善? 三种循环性能相当。需要避免使用for-in循环。\n改善循环性能的最佳方式是减少每次迭代的运算量和减少循环的迭代次数。\n倒序循环为什么能提高循环性能？ // 正序 let len = items.length for (let i = 0; i \u0026lt; len ; i++) { } // 倒叙 for (let i = items.length; i-- ; ) { } 因为减少了迭代的运算量 。 控制条件从两次比较（是否迭代数小于总数？它是否为true）减少到一次比较（是否为true）\n举例怎么用迭代代替递归实现？ //斐波那契数列的递归形式 function fibonacci(num) { if(num ===1 || num ===2) { return 1 } return fibonacci(num - 1) + fibonacci(num - 2) } //迭代形式 function fib(num) { var n1 = 1, n2 = 1, n = 1; for(var i=3;i\u0026lt;=num;i++){ n = n1 + n2 n1 = n2 n2 = n } return n ","date":1540478039,"description":"《高性能Javascript》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"1e67430b9376331991cc225f0e50d2b7","permalink":"https://siqi-liu.com/zh/post/legacy/%E7%AE%97%E6%B3%95%E5%92%8C%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6%E4%BC%98%E5%8C%96/","publishdate":"2018-10-25T14:33:59Z","relpermalink":"/zh/post/legacy/%E7%AE%97%E6%B3%95%E5%92%8C%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6%E4%BC%98%E5%8C%96/","section":"post","summary":"《高性能Javascript》 知识点整理","tags":["JavaScript"],"title":"算法和流程控制优化","tldr":null,"type":"post"},{"authors":null,"categories":["Vue"],"content":"什么是MVVM？与MVC有什么区别？ mvc是Model-View-Controller的缩写，用业务逻辑，数据，界面分离的方式组织代码，其中的通信是单项的，View 传送指令到 Controller；Controller 完成业务逻辑后，要求 Model 改变状态；Model 将新的数据发送到 View，用户得到反馈。\nMVVM是Model-View-ViewModel的缩写，和MVC模式一样，主要目的是分离视图（View）和模型（Model），但是mvvm的控制器并不会去监听浏览器的事件,而是监听一个属性表,由浏览器的事件修改属性,以触发控制器中的方法,增加了一层控制业务的属性,而这层属性被称为VM。当 ViewModel 中数据变化，View 层会得到更新；而当 View 中声明了数据的双向绑定（通常是表单元素），框架也会监听 View 层（表单）值的变化。一旦值变化，View 层绑定的 ViewModel 中的数据也会得到自动更新。\nnew Vue 正是viewmodel\n过滤器怎么串联和传参数? 串联\n{{ message | filterA | filterB }} 接受参数\n{{ message | filterA(\u0026#39;arg1\u0026#39;, arg2) }} 这里，filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数，普通字符串 ‘arg1’ 作为第二个参数，表达式 arg2 的值作为第三个参数。\n计算属性和方法有什么区别？计算属性和侦听属性有什么区别？ 计算属性和方法的区别是计算属性是基于依赖进行缓存的，只有在相关依赖发生改变后才会求值。\n侦听器允许我们执行异步操作（访问一个API），并可以设置中间状态（比如loading…）。\n计算属性默认只有getter ，不过在需要时也可以提供一个 setter\n$watch怎么观察实例的表达式？怎么观察对象内部变化？怎么立即触发回调？ 观察表达式\nvm.$watch( function () { return this.a + this.b }, function (newVal, oldVal) { } ) 对象内部变化\nvm.$watch(\u0026#39;someObject\u0026#39;, callback, { deep: true }) vm.someObject.nestedValue = 123 立即触发回调\nvm.$watch(\u0026#39;a\u0026#39;, callback, { immediate: true }) v-show和v-if的区别是什么？ v-if支持v-else和v-else-if语法，也支持\u0026lt; template/\u0026gt;语法；v-show不支持这些；\nv-show是通过简单地切换元素的CSS属性display属性来实现显示隐藏效果；\nv-if 是“真正的”条件渲染，因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建；\nv-if 也是惰性的：如果在初始渲染时条件为假，则什么也不做——直到条件第一次变为真时，才会开始渲染条件块；\n相比之下， v-show 就简单得多——不管初始条件是什么，元素总是会被渲染，并且只是简单地基于 CSS 进行切换；\nv-if中的key有什么作用？ 为了更高效的渲染元素，vue通常会复用元素，唯一的key值会让元素不被复用而是重新渲染。\nv-for中的key应该怎么使用？为什么需要key？ 使用v-for更新已渲染的元素列表时,默认用就地复用策略;列表数据修改的时候,他会根据key值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素;\n因此，key的使用办法是使用数组中不会变化的那一项作为key值，即每条数据都有唯一且不变的id来标识这条数据的唯一性。(而不是使用数组的index值，因为在插入新项后会发生改变)\nkey的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时，也会使用到key属性，其目的也是为了让vue可以区分它们，否则vue只会替换其内部属性而不会触发过渡效果。\n怎么使vue检测到利用索引直接设置一个项或者修改数组的长度？ // Vue.set Vue.set(vm.items, indexOfItem, newValue) // Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue) 使用修饰符使用户的输入值转为数字类型？自动过滤用户输入的首尾空白字符？ \u0026lt;input v-model.number=\u0026#34;age\u0026#34; type=\u0026#34;number\u0026#34;\u0026gt; \u0026lt;input v-model.trim=\u0026#34;msg\u0026#34;\u0026gt; data为什么必须是函数？ 因为只有这样每个实例才能维护一份被返回对象的独立的拷贝，否则对一个实例的修改会影响到其它的实例\n怎么从一个文件夹批量全局导入基础组件？ import Vue from \u0026#39;vue\u0026#39; import upperFirst from \u0026#39;lodash/upperFirst\u0026#39; import camelCase from \u0026#39;lodash/camelCase\u0026#39; //具体查看webpack的API const requireComponent = require.context( // 其组件目录的相对路径 \u0026#39;./components\u0026#39;, // 是否查询其子目录 false, // 匹配基础组件文件名的正则表达式 /Base[A-Z]\\w+\\.(vue|js)$/ ) requireComponent.keys().forEach(fileName =\u0026gt; { // 获取组件配置 const componentConfig = requireComponent(fileName) // 获取组件的 PascalCase 命名 const componentName = upperFirst( camelCase( // 剥去文件名开头的 `./` 和结尾的扩展名 fileName.replace(/^\\.\\/(.*)\\.\\w+$/, \u0026#39;$1\u0026#39;) ) ) // 全局注册组件 Vue.component( componentName, // 如果这个组件选项是通过 `export default` 导出的， // 那么就会优先使用 `.default`， // 否则回退到使用模块的根。 componentConfig.default || componentConfig ) }) 组件的特性继承是什么？如何自定义特性继承？ 添加到组件实例上的特性会被自动添加到组件的根元素上。其中class 和 style 特性如果冲突，两边的值会被合并，其它的特性的冲突则会被替换。\n如果不希望组件的根元素继承特性，可以在组件的选项中设置inheritAttrs: false,例如\nue.component(\u0026#39;my-component\u0026#39;, { inheritAttrs: false, // ... }) 配合实例的$attrs属性使用，可以手动将特性赋予给指定元素，例如\nVue.component(\u0026#39;base-input\u0026#39;, { inheritAttrs: false, props: [\u0026#39;label\u0026#39;, \u0026#39;value\u0026#39;], template: ` \u0026lt;label\u0026gt; {{ label }} \u0026lt;input v-bind=\u0026#34;$attrs\u0026#34; v-bind:value=\u0026#34;value\u0026#34; v-on:input=\u0026#34;$emit(\u0026#39;input\u0026#39;, $event.target.value)\u0026#34; \u0026gt; \u0026lt;/label\u0026gt; ` }) 如何将原生事件绑定到组件？怎么绑定到组件的特定元素？ 如果是想在组件的根元素直接监听原生事件，可以使用native修饰符，例如\n\u0026lt;base-input v-on:focus.native=\u0026#34;onFocus\u0026#34;\u0026gt;\u0026lt;/base-input\u0026gt; 如果根元素不是需要监听的元素，父级的native将会失效，可以使用$listeners属性，它是一个包含了作用在这个组件上的所有监听器的对象。 例如组件中的$listeners是\n{ focus: function (event) { /* ... */ } } 这样就可以像如下将事件侦听器指向这个组件的某个特定子元素。\nVue.component(\u0026#39;base-input\u0026#39;, { inheritAttrs: false, props: [\u0026#39;label\u0026#39;, \u0026#39;value\u0026#39;], computed: { inputListeners: function () { var vm = this return Object.assign({}, // 我们从父级添加所有的监听器 this.$listeners, // 然后我们添加自定义监听器， // 或覆写一些监听器的行为 { // 这里确保组件配合 `v-model` 的工作 input: function (event) { vm.$emit(\u0026#39;input\u0026#39;, event.target.value) } } ) } }, template: ` \u0026lt;label\u0026gt; {{ label }} \u0026lt;input v-bind=\u0026#34;$attrs\u0026#34; v-bind:value=\u0026#34;value\u0026#34; v-on=\u0026#34;inputListeners\u0026#34; \u0026gt; \u0026lt;/label\u0026gt; ` }) .sync修饰符是什么？ 怎么使用？ .sync是子组件触发父组件事件并修改父组件值的方法的缩写，使用例子：\nthis.$emit(\u0026#39;update:title\u0026#39;, newTitle) \u0026lt;text-document v-bind:title=\u0026#34;doc.title\u0026#34; v-on:update:title=\u0026#34;doc.title = $event\u0026#34; \u0026gt;\u0026lt;/text-document\u0026gt; //可以缩写成为 \u0026lt;text-document v-bind:title.sync=\u0026#34;doc.title\u0026#34;\u0026gt;\u0026lt;/text-document\u0026gt; 怎么直接访问根实例，父组件实例，子组件实例（子元素） 访问根实例\nthis.$root //最好还是使用vuex 访问父组件实例\nthis.$parent 或者使用依赖注入\nprovide: function () { return { getMap: this.getMap } } inject: [\u0026#39;getMap\u0026#39;] 访问子组件或者子元素\nthis.$refs 如何在实例销毁前注销手动绑定的eventListener？ 使用程序化的事件侦听器，比如一个集成的第三方库的模式\n// 一次性将这个日期选择器附加到一个输入框上 // 它会被挂载到 DOM 上。 mounted: function () { // Pikaday 是一个第三方日期选择器的库 this.picker = new Pikaday({ field: this.$refs.input, format: \u0026#39;YYYY-MM-DD\u0026#39; }) }, // 在组件被销毁之前， // 也销毁这个日期选择器。 beforeDestroy: function () { this.picker.destroy() } 存在的问题是第一需要在实例中保存这个piker，第二是建立代码和清理代码分开了，使得比较难程序化的清理所建立的所有东西。 解决办法是\nmounted: function () { var picker = new Pikaday({ field: this.$refs.input, format: \u0026#39;YYYY-MM-DD\u0026#39; }) this.$once(\u0026#39;hook:beforeDestroy\u0026#39;, function () { picker.destroy() }) } 什么时候会使用到自定义指令？怎么使用？ 当需要对普通DOM元素进行底层操作的时候可能需要用到自定义指令\n比如想要输入框在页面加载的时候获取焦点，或者某个元素在加载的时候就改变相应的背景颜色。\n输入框的例子：\n// 注册一个全局自定义指令 …","date":1536604210,"description":"\u003cES6标准入门\u003e 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"310cfaa4cd16cb84f64b43b9330ac7b2","permalink":"https://siqi-liu.com/zh/post/legacy/vue%E6%98%93%E9%94%99%E6%98%93%E6%B7%B7%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%86/","publishdate":"2018-09-10T18:30:10Z","relpermalink":"/zh/post/legacy/vue%E6%98%93%E9%94%99%E6%98%93%E6%B7%B7%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%86/","section":"post","summary":"知识点整理","tags":["JavaScript","Vue"],"title":"vue易错易混知识点整理","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"数组 对于数组并不陌生，下面来总结一下数组的方法。\n以下是一些容易记错或者记混的常用方法\nmap 在一个坑爹面试题中见过\n[\u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;].map(parseInt) 就拿这道题复习吧，先看一下通常map中的参数\nvar new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]) 可以看到需要传参callback和thisArg，分别是生成新数组元素的函数，执行 callback 函数时 使用的this 值。\n其中callback接受三个参数，\ncurrentValue：数组中正在处理的当前元素。\nindex：数组中正在处理的当前元素的索引。\narray：map 方法被调用的数组。\n再看一下parseInt\nparseInt(string, radix) //string\t必需。要被解析的字符串。 //radix\t可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。 如果省略该参数或其值为 0，则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头，将以 16 为基数。 如果该参数小于 2 或者大于 36，则 parseInt() 将返回 NaN。 结合起来，再根据\nparseInt(\u0026#39;1\u0026#39;,0) = 1, parseInt(\u0026#39;2\u0026#39;,1) = NaN, parseInt(\u0026#39;3\u0026#39;,2) = NaN 就可以得出[1,NaN,NaN]的结果；\nsolice和split array.splice(start[, deleteCount[, item1[, item2[, ...]]]]) splice的作用是在数组的任意位置添加或删除元素。 其中第一个参数表示想要删除或插入的元素的索引值，第二个参数是删除元素的个数(添加则传入0)，第三个参数之后就是要添加到数组里的值。\narray.slice(start,end) slice() 方法返回一个从开始到结束（不包括结束）选择的数组的一部分浅拷贝到一个新数组对象。且原始数组不会被修改。\n栈 栈是遵循后进先出原则的集合\n实现栈类\nlet Stack = (function () { const items = new WeakMap() class Stack { constructor() { items.set(this, []) } push(element) { let s = items.get(this) s.push(element)//注意，返回值是数组的长度 } pop() { let s = items.get(this) let r = s.pop() return r } isEmpty () { let s = items.get(this); return s.length==0 } } return Stack })() 栈的应用-进制转化\nfunction baseConverter(decNumber,base) { let remStack = new Stack(),rem,binaryString = \u0026#34;\u0026#34;,chars =\u0026#34;0123456789ABCDEF\u0026#34;; while(decNumber\u0026gt;0){ //余数入栈 rem = decNumber%base //被除数取整 decNumber = Math.floor(decNumber/base); remStack.push(rem); } while(!remStack.isEmpty()){ binaryString+=chars[remStack.pop()] } return binaryString } let result = baseConverter(10,2)//1010 let result1 = baseConverter(105,16)//69 队列 队列是遵循先进先出的集合\n实现队列类\nlet Queue = (function () { const items = new WeakMap() class Queue { constructor() { items.set(this, []) } enqueue(element) { let q = items.get(this) q.push(element)//注意，返回值是数组的长度 } dequeue() { let q = items.get(this) let r = q.shift() return r } isEmpty () { let q = items.get(this); return q.length==0 } size () { let q =items.get(this); return q.length } } return Queue })() 队列的应用-击鼓传花\nfunction hotPotato(nameList, num){ let queue = new Queue() for(let i=0;i\u0026lt;nameList.length;i++){ //所有玩家入队列 queue.enqueue(nameList[i]) } let eliminated = \u0026#39;\u0026#39;; //当队列里还有1个以上的玩家 while (queue.size()\u0026gt;1){ //num个玩家排列到队列末尾 for(let i=0;i\u0026lt;num;i++){ queue.enqueue(queue.dequeue()) } eliminated = queue.dequeue() console.log(eliminated + \u0026#39;淘汰\u0026#39;) } return queue.dequeue() } let nameList = [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;] ","date":1534950585,"description":"Javascript数据结构","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"5148260251efd6ec691c46b1a7fa4956","permalink":"https://siqi-liu.com/zh/post/legacy/%E6%95%B0%E7%BB%84-%E6%A0%88-%E9%98%9F%E5%88%97/","publishdate":"2018-08-22T15:09:45Z","relpermalink":"/zh/post/legacy/%E6%95%B0%E7%BB%84-%E6%A0%88-%E9%98%9F%E5%88%97/","section":"post","summary":"Javascript数据结构","tags":["JavaScript"],"title":"数组，栈，队列","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"用法 例1 控制按顺序完成异步操作\nvar fetchData = function(id){ return new Promise((reslove,reject)=\u0026gt;{ setTimeout(function(){ console.log(`任务${id}执行`) reslove(`执行结果${id}`) },Math.random()*1000) }) } async function tasksList(){ //这里面的任务并发执行 let list = [1,2,3,4,5] let promises = list.map((task)=\u0026gt;{ return fetchData(task) }) let results = await Promise.all(promises); return results } tasksList().then((res)=\u0026gt;{ console.log(res) console.log(\u0026#34;全部任务完成\u0026#34;) }) 任务1执行 任务2执行 任务4执行 任务5执行 任务3执行 [\u0026#34;执行结果1\u0026#34;, \u0026#34;执行结果2\u0026#34;, \u0026#34;执行结果3\u0026#34;, \u0026#34;执行结果4\u0026#34;, \u0026#34;执行结果5\u0026#34;] 可以看到很类似promise，结果的输出是按照list的顺序的，区别是promise在函数执行之前就已经在执行异步请求了。\n例2 继发完成异步操作\n//fetchData和例1相同 async function tasksList(){ //这里面的任务继发执行 let results = []; try{ for(let i=0;i\u0026lt;5;i++){ let result = await fetchData(i) results.push(result) } }catch(e){ console.log(e) } return results } tasksList().then((res)=\u0026gt;{ console.log(res) console.log(\u0026#34;全部任务完成\u0026#34;) }) 任务0执行 任务1执行 任务2执行 任务3执行 [\u0026#34;执行结果0\u0026#34;, \u0026#34;执行结果1\u0026#34;, \u0026#34;执行结果2\u0026#34;, \u0026#34;执行结果3\u0026#34;, \u0026#34;执行结果4\u0026#34;] 全部任务完成 概念 async是Generator函数的语法糖。 async相比于Gennerator改进的地方在于:\n内置执行器 asycn不像Generator需要执行器(co模块)\n更好的语义 async表示函数里有异步操作，await表示是紧跟在后面的表达式需要等待结果\n更广的适用性 async函数的await命令后面，可以使Promise对象和原始类型的值，原始类型的值会返回一个立即resolved的promise对象\n返回值是promise 可以用then方法指定下一步的操作\n","date":1531320425,"description":"《ES6标准入门》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"215773b0d2c81f4ed0f42efc9ce1db72","permalink":"https://siqi-liu.com/zh/post/legacy/async/","publishdate":"2018-07-11T14:47:05Z","relpermalink":"/zh/post/legacy/async/","section":"post","summary":"《ES6标准入门》 知识点整理","tags":["JavaScript","JavaScript"],"title":"async","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"\n基本用法 const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }); Promise参数 resolve函数的作用是，将Promise对象的状态从“pending”变为“resolved”，在异步操作成功时调用，并将异步操作的结果，作为参数传递出去；\nreject函数的作用是，将Promise对象的状态从“pending”变为“rejected”，在异步操作失败时调用，并将异步操作报出的错误，作为参数传递出去。\nresolve函数的参数除了正常的值以外，还可能是另一个 Promise 实例\nconst p1 = new Promise(function (resolve, reject) { // ... }); const p2 = new Promise(function (resolve, reject) { // ... resolve(p1); }) p1和p2都是 Promise 的实例，但是p2的resolve方法将p1作为参数，即一个异步操作的结果是返回另一个异步操作。 这时p1的状态就会传递给p2，也就是说，p1的状态决定了p2的状态。\n如果p1的状态是pending，那么p2的回调函数就会等待p1的状态改变；如果p1的状态已经是resolved或者rejected，那么p2的回调函数将会立刻执行。\nsummary: \u0026#34;\u0026#34; 一般来说，调用resolve或reject以后，Promise 的使命就完成了，后继操作应该放到then方法里面，而不应该直接写在resolve或reject的后面。所以，最好在它们前面加上return语句，这样就不会有意外。\n链式调用 then方法返回的是一个新的Promise实例，。因此可以采用链式写法，即then方法后面再调用另一个then方法。\ngetJSON(\u0026#34;/post/1.json\u0026#34;).then( post =\u0026gt; getJSON(post.commentURL) ).then( comments =\u0026gt; console.log(\u0026#34;resolved: \u0026#34;, comments), err =\u0026gt; console.log(\u0026#34;rejected: \u0026#34;, err) ); 错误捕获 promise抛出错误，会被catch方法指定的回调函数捕获。\nPromise 在resolve语句后面，再抛出错误，不会被捕获。因为 Promise 的状态一旦改变，就永久保持该状态，不会再变了。\nPromise 对象的错误具有“冒泡”性质，会一直向后传递，直到被捕获为止。也就是说，错误总是会被下一个catch语句捕获。\n一般来说，不要在then方法里面定义 Reject 状态的回调函数（即then的第二个参数），总是使用catch方法。\n","date":1530105527,"description":"《ES6标准入门》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"1e2d085bbfd1ae0eff73dc8ab4102849","permalink":"https://siqi-liu.com/zh/post/legacy/promise/","publishdate":"2018-06-27T13:18:47Z","relpermalink":"/zh/post/legacy/promise/","section":"post","summary":"《ES6标准入门》 知识点整理","tags":["JavaScript","JavaScript"],"title":"promise","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"概述 Proxy 用于修改某些操作的默认行为，等同于在语言层面做出修改，所以属于一种“元编程”（meta programming），即对编程语言进行编程。\nProxy 可以理解成，在目标对象之前架设一层“拦截”，外界对该对象的访问，都必须先通过这层拦截，因此提供了一种机制，可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理，用在这里表示由它来“代理”某些操作，可以译为“代理器”。\nvar proxy = new Proxy(target, handler); Proxy 对象的所有用法，都是上面这种形式，不同的只是handler参数的写法。其中，new Proxy()表示生成一个Proxy实例，target参数表示所要拦截的目标对象，handler参数也是一个对象，用来定制拦截行为。\n举个栗子\nvar proxy = new Proxy({}, { get: function(target, property) { return 35; } }); proxy.time // 35 proxy.name // 35 proxy.title // 35 Proxy 实例也可以作为其他对象的原型对象。\nvar proxy = new Proxy({}, { get: function(target, property) { return 35; } }); let obj = Object.create(proxy); obj.time // 35 上面代码中，proxy对象是obj对象的原型，obj对象本身并没有time属性，所以根据原型链，会在proxy对象上读取该属性，导致被拦截。\n同一个拦截器函数，可以设置拦截多个操作。\nvar handler = { get: function(target, name) { if (name === \u0026#39;prototype\u0026#39;) { return Object.prototype; } return \u0026#39;Hello, \u0026#39; + name; }, apply: function(target, thisBinding, args) { return args[0]; }, construct: function(target, args) { return {value: args[1]}; } }; var fproxy = new Proxy(function(x, y) { return x + y; }, handler); fproxy(1, 2) // 1 new fproxy(1, 2) // {value: 2} fproxy.prototype === Object.prototype // true fproxy.foo === \u0026#34;Hello, foo\u0026#34; // true 下面是 Proxy 支持的拦截操作一览 一共 13 种。\nget(target, propKey, receiver)：拦截对象属性的读取，比如proxy.foo和proxy[‘foo’]。\nset(target, propKey, value, receiver)：拦截对象属性的设置，比如proxy.foo = v或proxy[‘foo’] = v，返回一个布尔值。\nhas(target, propKey)：拦截propKey in proxy的操作，返回一个布尔值。 deleteProperty(target, propKey)：拦截delete proxy[propKey]的操作，返回一个布尔值。\nownKeys(target)：拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环，返回一个数组。该方法返回目标对象所有自身的属性的属性名，而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。\ngetOwnPropertyDescriptor(target, propKey)：拦截Object.getOwnPropertyDescriptor(proxy, propKey)，返回属性的描述对象。\ndefineProperty(target, propKey, propDesc)：拦截Object.defineProperty(proxy, propKey, propDesc）、Object.defineProperties(proxy, propDescs)，返回一个布尔值。 preventExtensions(target)：拦截Object.preventExtensions(proxy)，返回一个布尔值。\ngetPrototypeOf(target)：拦截Object.getPrototypeOf(proxy)，返回一个对象。\nisExtensible(target)：拦截Object.isExtensible(proxy)，返回一个布尔值。\nsetPrototypeOf(target, proto)：拦截Object.setPrototypeOf(proxy, proto)，返回一个布尔值。如果目标对象是函数，那么还有两种额外操作可以拦截 。\napply(target, object, args)：拦截 Proxy 实例作为函数调用的操作，比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。\nconstruct(target, args)：拦截 Proxy 实例作为构造函数调用的操作，比如new proxy(…args)。\n参考阮一峰ES6入门\n使用场景 proxy类似于设计模式中的代理模式，常用于以下几个方面\n拦截和监视外部对对象的访问 降低函数或类的复杂度 在复杂操作前对操作进行校验或对所需资源进行管理 抽离校验模块 假定Person对象有一个age属性，该属性应该是一个不大于 200 的整数，那么可以使用Proxy保证age的属性值符合要求。\nlet validator = { set: function(obj, prop, value) { if (prop === \u0026#39;age\u0026#39;) { if (!Number.isInteger(value)) { throw new TypeError(\u0026#39;The age is not an integer\u0026#39;); } if (value \u0026gt; 200) { throw new RangeError(\u0026#39;The age seems invalid\u0026#39;); } } // 对于满足条件的 age 属性以及其他属性，直接保存 obj[prop] = value; } }; let person = new Proxy({}, validator); person.age = 100; person.age // 100 person.age = \u0026#39;young\u0026#39; // 报错 person.age = 300 // 报错 如果要直接为对象的所有属性开发一个校验器可能很快就会让代码结构变得臃肿，使用 Proxy 则可以将校验器从核心逻辑分离出来自成一体：\nfunction createValidator(target, validator) { return new Proxy(target, { _validator: validator, set(target, key, value, proxy) { if (target.hasOwnProperty(key)) { let validator = this._validator[key]; if (validator(value)) { return Reflect.set(target, key, value, proxy); } else { throw Error(`Cannot set ${key} to ${value}. Invalid.`); } } else { throw Error(`${key} is not a valid property`) } } }) } const personValidators = { name(val) { return typeof val === \u0026#39;string\u0026#39;; }, age(val) { return typeof age === \u0026#39;number\u0026#39; \u0026amp;\u0026amp; age \u0026gt; 18; } } class Person { constructor(name, age) { this.name = name; this.age = age; return createValidator(this, personValidators); } } const bill = new Person(\u0026#39;Bill\u0026#39;, 25); // 以下操作都会报错 bill.name = 0; //Cannot set name to 0. Invalid. bill.age = \u0026#39;Bill\u0026#39;; //Cannot set age to Bill. Invalid. 私有属性 有时，我们会在对象上面设置内部属性，属性名的第一个字符使用下划线开头，表示这些属性不应该被外部使用。结合get和set方法，就可以做到防止这些内部属性被外部读写。\nconst handler = { get (target, key) { invariant(key, \u0026#39;get\u0026#39;); return target[key]; }, set (target, key, value) { invariant(key, \u0026#39;set\u0026#39;); target[key] = value; return true; } }; function invariant (key, action) { if (key[0] === \u0026#39;_\u0026#39;) { throw new Error(`Invalid attempt to ${action} private \u0026#34;${key}\u0026#34; property`); } } const target = {}; const proxy = new Proxy(target, handler); proxy._prop // Error: Invalid attempt to get private \u0026#34;_prop\u0026#34; property proxy._prop = \u0026#39;c\u0026#39; // Error: Invalid attempt to set private \u0026#34;_prop\u0026#34; property 第二种方法是使用 has 拦截 in 操作\nvar handler = { has (target, key) { if (key[0] === \u0026#39;_\u0026#39;) { return false; } return key in target; } }; var target = { _prop: \u0026#39;foo\u0026#39;, prop: \u0026#39;foo\u0026#39; }; var proxy = new Proxy(target, handler); \u0026#39;_prop\u0026#39; in proxy // false 值得注意的是，has方法拦截的是HasProperty操作，而不是HasOwnProperty操作，即has方法不判断一个属性是对象自身的属性，还是继承的属性。\n另外，虽然for…in循环也用到了in运算符，但是has拦截对for…in循环不生效。\n访问日志 对于那些调用频繁, 运行缓慢或占用资源较多的属性或接口，开发者会希望记录他们的使用情况或性能表现，这个时候可以使用proxy充当中间角色，从而实现日志功能\nlet api = { _apiKey: \u0026#39;123abc456def\u0026#39;, getUsers: function() { /* ... */ }, getUser: function(userId) { /* ... */ }, …","date":1526637566,"description":"《ES6标准入门》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"8cc5aeedbe4a9c836b1ef25ea4ed0207","permalink":"https://siqi-liu.com/zh/post/legacy/proxy/","publishdate":"2018-05-18T09:59:26Z","relpermalink":"/zh/post/legacy/proxy/","section":"post","summary":"《ES6标准入门》 知识点整理","tags":["JavaScript","JavaScript"],"title":"proxy","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"发布订阅模式 发布订阅模式又叫观察者模式，他定义对象间的一种一对多的依赖关系，当一个对象状态发生改变时，所有依赖于他的对象都将得到通知，在Js开发中我们一般用事件模式代替传统的发布订阅模式。\n发布订阅模式实现的步骤\n1.指定发布者\n2.给发布者添加一个缓存列表，用于存放回调函数以便通知订阅者\n3.发布消息的时候发布者会遍历整个缓存列表，依次触发里面的回调函数\n一个售楼处的例子，售楼处可以在发给订阅者的信息里加上房子的单价，面积等信息，订阅者接受到这个信息进行各自的处理\n售楼处的例子 var salesOffices = {}; salesOffices.clientList = []; salesOffices.listen = function(fn){ // 发布者缓存回调函数 salesOffices.clientList.push(fn); } salesOffices.trigger = function(){ for(var i=0,fn;fn = this.clientList[i];i++){ fn.apply(this,arguments) } } salesOffices.listen(function(price,squareMeter){ console.log(\u0026#34;价格=\u0026#34;+price); console.log(\u0026#39;squareMeter=\u0026#39;+squareMeter); }) salesOffices.trigger(2000000,88) //价格=2000000 // squareMeter=88 下面为订阅的消息做一个key，添加上订阅者id，并把发布-订阅的功能提取出来作为一个对象。\nvar event = { clientList:{}, listen:function(key,id,fn){ // key 订阅表示 id 订阅者id fnu回调函数 // 一个订阅标识下面可能存在很多个订阅者以及回调函数 // 所以将订阅者作为数组，数组中存放回调函数 if(!this.clientList[key]){ this.clientList[key]=[]; } this.clientList[key].push({ id,fn }) }, trigger:function(...args){//rest参数 var key = args.shift(); fns = this.clientList[key]; // 回调函数数组为空 什么都不做 if(!fns||fns.length==0) return; // 依次触发回调函数数组中的函数 for(var i=0,fn;fn = fns[i];i++){ // fn 是一个有id fn 属性的对象 fn.fn.apply(this,args) } }, remove:function(key,fn){ var fns = this.clientList[key]; if(!fns) return false//对应的key没有人订阅 if(!fn){//没有传入具体的回调则取消key的所有订阅 fns \u0026amp;\u0026amp; (fns.length=0) }else{ for(var i = fns.length-1;i\u0026gt;=0;i--){ var _fn = fns[i].fn; if(_fn===fn){ fns.splice(i,1)//删除回调 } } } } }; //定义一个installEvent函数为对象安装发布订阅功能 var installEvent = function(obj){ for(var i in event){ // 遍历event对象的key 为 obj 添加上属性和属性方法 obj[i] = event[i] } } var salesOffices = {}; installEvent(salesOffices) salesOffices.listen(\u0026#34;square88\u0026#34;,1,f1 = function(price){ console.log(`square88的消息，价格是${price}`) }) salesOffices.listen(\u0026#34;square88\u0026#34;,2,f3 = function(price){ console.log(`square88的消息，价格是${price}`) }) salesOffices.listen(\u0026#34;square100\u0026#34;,2,f2 = function(price){ console.log(`square100的消息，价格是${price}`) }) salesOffices.trigger(\u0026#39;square88\u0026#39;,1000000)//square88的消息，价格是1000000 *2 salesOffices.trigger(\u0026#39;square100\u0026#39;,1500000)//square100的消息，价格是1500000 salesOffices.remove(\u0026#34;square88\u0026#34;,f1); salesOffices.trigger(\u0026#39;square88\u0026#39;,1000000)//square88的消息，价格是1000000 vue怎么监听对象的变化 Object.defineProperty\n能自定义get和set函数，在获取和设置对象属性时可以触发对应回调函数。 利用这个方法，为对象中的每个属性安装发布订阅功能就可以了。\n// 创建一个Vue 构造函数，使用 prototype 继承方法 function Vue(data){ // new新的对象后会有data属性 this.data = data; // watchList 相当于 上面的clientList this.watchList = []; // 为data对象添加 发布消息的功能 this.$bindObserver(data)； } var $watch = function (key,fn){ //监听的id是谁已经不重要，所以去掉了 if(!this.clientList[key]){ this.clientList[key]=[]; } this.clientList[key].push(fn) } var $emit = function(...args){ var key = args.shift(); fns = this.watchList[key]; if(!fns||fns.length==0) return; for(var i=0,fn;fn = fns[i];i++){ fn.apply(this,args) } } var $remove = function(key,fn){ var fns = this.clientList[key]; if(!fns) return false if(!fn){ fns \u0026amp;\u0026amp; (fns.length=0) }else{ for(var i = fns.length-1;i\u0026gt;=0;i--){ var _fn = fns[i].fn; if(_fn===fn){ fns.splice(i,1) } } } } var $bindObserver = function(data){ var self = this; var keys = Object.keys(data) keys.forEach(key =\u0026gt; { var result = data[key]; //这里用到闭包 result 作为在函数内被内部函数引用的变量，一直存在于缓存中 Object.defineProperty(data,key, { enumerable:true, configurable:true, get:function(){ // return 的是上个作用域的result return result }, set:function(newVal){ self.$emit(key,newVal); result = newVal; } }) }) } Vue.prototype = { $watch,$emit,$remove,$bindObserver } var person={ name:\u0026#34;yosgi\u0026#34;, age:29 } var app1 = new Vue(person) app1.listen(\u0026#34;age\u0026#34;,function(val){ console.log(`age被改变，值为${val}`) }) app1.data.age = 30 age被改变，值为30上面实现的代码还有问题。\n对象往往是一个深层次的结构，对象的某个属性可能仍然是一个对象，这种情况怎么处理？ 应该用递归来处理\nfunction Vue(data){ this.data = data; this.watchList = []; //相当于clientLists this.$bindObserver(data) } var $watch = function (key,fn){ if(!this.watchList[key]){ this.watchList[key]=[]; } this.watchList[key].push(fn) } var $emit = function(...args){ var key = args.shift(); console.log(this.watchList) fns = app1.watchList[key]; if(!fns||fns.length==0) return; for(var i=0,fn;fn = fns[i];i++){ fn.apply(this,args) } } var $remove = function(key,fn){ var fns = this.watchList[key]; if(!fns) return false if(!fn){ fns \u0026amp;\u0026amp; (fns.length=0) }else{ for(var i = fns.length-1;i\u0026gt;=0;i--){ var _fn = fns[i].fn; if(_fn===fn){ fns.splice(i,1) } } } } var $bindObserver = function(data){ var self = this; var keys = Object.keys(data) keys.forEach(key =\u0026gt; { var result = data[key]; if(typeof result ===\u0026#39;object\u0026#39;){ self.$bindObserver(data[key]) } Object.defineProperty(data,key, { enumerable:true, configurable:true, get:function(){ return result }, set:function(newVal){ self.$emit(key,newVal); //如果newVal也是对象，同样需要对 对象添加 发布消息的功能 if(typeof newVal ===\u0026#39;object\u0026#39;){ self.$bindObserver(newVal) } result = newVal; } }) }) } Vue.prototype = { $watch,$emit,$remove,$bindObserver } var person={ name:\u0026#34;yosgi\u0026#34;, age:29, address:{ city:\u0026#34;hangZhou\u0026#34;, province:{ a:1, b:2 } } } var app1 = new Vue(person) app1.$watch(\u0026#34;city\u0026#34;,function(val){ console.log(`city被改变，值为${val}`) }) app1.data.address.city = …","date":1525559934,"description":"《Javascript设计模式》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"321851d91367e33b79b72a2c01105437","permalink":"https://siqi-liu.com/zh/post/legacy/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E5%92%8Cvue%E7%9B%91%E5%90%AC%E5%AF%B9%E8%B1%A1%E5%8F%98%E5%8C%96/","publishdate":"2018-05-05T22:38:54Z","relpermalink":"/zh/post/legacy/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E5%92%8Cvue%E7%9B%91%E5%90%AC%E5%AF%B9%E8%B1%A1%E5%8F%98%E5%8C%96/","section":"post","summary":"《Javascript设计模式》 知识点整理","tags":["JavaScript","Design Patterns"],"title":"观察者模式和vue监听对象变化","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"对象的拓展 对象属性的简洁表示法 //几个例子 function f(x,y){ return {x,y} } f(1,2)//Objext {x:1,y:2} module.exports = { getItem, setItem, clear }; const cart = { _wheels: 4, get wheels() { return this._wheels; }, set wheels(value) { if (value \u0026lt; this._wheels) { throw new Error(\u0026#39;数值太小了！\u0026#39;); } this._wheels = value; } } cart.wheels//4 set.wheels = 1//Error\u0026#39;数值太小了！\u0026#39; 属性名表达式 // 方法一 obj.foo = true; // 方法二 obj[\u0026#39;a\u0026#39; + \u0026#39;bc\u0026#39;] = 123; 注意，属性名表达式如果是一个对象，默认情况下会自动将对象转为字符串[object Object]，这一点要特别小心。 const keyA = {a: 1}; const keyB = {b: 2};\nconst myObject = { [keyA]: \u0026#39;valueA\u0026#39;, [keyB]: \u0026#39;valueB\u0026#39; }; myObject // Object {[object Object]: \u0026#34;valueB\u0026#34;} 上面代码中，[keyA]和[keyB]得到的都是[object Object]，所以[keyB]会把[keyA]覆盖掉，而myObject最后只有一个[object Object]属性。\n方法的name属性 函数的name属性，返回函数名。对象方法也是函数，因此也有name属性。\nconst person = { sayName() { console.log(\u0026#39;hello!\u0026#39;); } }; person.sayName.name // \u0026#34;sayName\u0026#34; 如果对象的方法使用了取值函数和存值函数，则name属性不是在改方法上面，而是在该方法的描述对象的get set属性上面\nconst obj = { get foo(){}, set foo(x){} } const descriptor = Object.getOwnPropertyDescriptor(obj,\u0026#34;foo\u0026#34;); //getOwnPropertyDescriptor方法返回指定对象上一个自有属性对应的属性描述符。 descriptor.get.name // \u0026#34;get foo\u0026#34; descriptor.set.name // \u0026#34;set foo\u0026#34; 如果对象的方法是一个 Symbol 值，那么name属性返回的是这个 Symbol 值的描述。\nconst key1 = Symbol(\u0026#39;description\u0026#39;); const key2 = Symbol(); let obj = { [key1]() {}, [key2]() {}, }; obj[key1].name // \u0026#34;[description]\u0026#34; obj[key2].name // \u0026#34;\u0026#34; Object.is() ES5用 == 和=== 两个运算符比较两个值是否相等，但都有缺点。\n第一个会自动转换数据类型，后面的NaN不等于自身。以及+0 等于-0。JavaScript 缺乏一种运算，在所有环境中，只要两个值是一样的，它们就应该相等。\nES6 提出“Same-value equality”（同值相等）算法，用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等，与===的不同之处只有+0不等于-0 NaN等于自身。\nES5 可以通过下面的代码，部署Object.is。\nObject.defineProperty(Object, \u0026#39;is\u0026#39;, { value: function(x, y) { if (x === y) { // 针对+0 不等于 -0的情况 return x !== 0 || 1 / x === 1 / y; } // 针对NaN的情况 return x !== x \u0026amp;\u0026amp; y !== y; }, configurable: true, enumerable: false, writable: true }); Object.assign() Object.assign()方法用于对象的合并，将源对象的所有可枚举属性，复制到目标对象上\n其中第一个参数是目标对象，后面的参数都是源对象\nconst target= {a:1}; const source1 = {b:2}; const source2 = {c:3}; Object.assign(target,source1,source2) Object.assign拷贝的属性是有限制的，之拷贝源对象的自身属性，不拷贝继承属性，也不拷贝不可枚举的属性\nObject.assign({b: \u0026#39;c\u0026#39;}, Object.defineProperty({}, \u0026#39;invisible\u0026#39;, { enumerable: false, value: \u0026#39;hello\u0026#39; }) ) // { b: \u0026#39;c\u0026#39; } 属性名为 Symbol 值的属性，也会被Object.assign拷贝。\nObject.assign({ a: \u0026#39;b\u0026#39; }, { [Symbol(\u0026#39;c\u0026#39;)]: \u0026#39;d\u0026#39; }) // { a: \u0026#39;b\u0026#39;, Symbol(c): \u0026#39;d\u0026#39; } Object.assign方法实行的是浅拷贝，而不是深拷贝。也就是说，如果源对象某个属性的值是对象，那么目标对象拷贝得到的是这个对象的引用。\nconst obj1 = {a: {b: 1}}; const obj2 = Object.assign({}, obj1); obj1.a.b = 2; obj2.a.b // 2 源对象obj1的a属性是对象，目标对象拷贝的是对象的引用。对象的任何变化都会反映到目标对象上\n对于嵌套的对象，一旦遇到同名属性，Object.assign的处理方法是替换\nconst target = { a: { b: \u0026#39;c\u0026#39;, d: \u0026#39;e\u0026#39; } } const source = { a: { b: \u0026#39;hello\u0026#39; } } Object.assign(target, source) // { a: { b: \u0026#39;hello\u0026#39; } } 也就是说对象的复制最多到一层\nObject.assign的常见用途\n为对象添加方法 Object.assign(SomeClass.prototype,{ someMethod(arg1,arg2){ //... } }) SomeClass.prototype.someMethod = function (arg1, arg2) { //··· }; 克隆对象 function clone(origin) { let originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); }//保持继承链 合并多个对象 const merge = (target, ...sources) =\u0026gt; Object.assign(target, ...sources); //or const merge = (...sources) =\u0026gt; Object.assign({}, ...sources); 为属性指定默认值 const DEFAULTS = { logLevel: 0, outputFormat: \u0026#39;html\u0026#39; }; function processContent(options) { options = Object.assign({}, DEFAULTS, options); console.log(options); // ... } DEFAULTS对象是默认值，options是用户提供的参数，processContent将DEFAULTS和options合并成一个新对象，如果两者有同名属性，则options的值将覆盖DEFAULTS的属性值。当然，属性都是简单类型数据，不然将有可能发生前面所说的属性替换。\n属性的可枚举性 属性的遍历 对象的每个属性都有一个Descriptor来控制属性的行为,object.getOwnPropertyDescriptor方法可以获取该属性描述的对象。\n枚举 引入“可枚举”（enumerable）这个概念的最初目的，就是让某些属性可以规避掉for…in操作\nlet obj = {foo:123}; Object.getOwnPropertyDescriptor(obj,foo); //{value: 123, writable: true, enumerable: true, configurable: true} enumerable属性成为可枚举性，设为false表示某些操作会忽略此属性。目前有4种。\nfor..in 循环只遍历对象自身和继承的可枚举的属性\nObject.keys() 只遍历自身的可枚举属性键名\nJSON.stringfy()只串行化自身的可枚举属性\nObject.assign() 之拷贝对象自身的可枚举属性 继承的和不可枚举的都会被忽略\n另外，ES6 规定，所有 Class 的原型的方法都是不可枚举的。\n总的来说，操作中引入继承的属性会让问题复杂化，大多数时候，我们只关心对象自身的属性。所以，尽量不要用for…in循环，而用Object.keys()代替。\n遍历 ES6一共5中方法遍历对象的属性\nfor…in for…in循环遍历对象自身的和继承的可枚举属性（不含 Symbol 属性）。\nObject.keys(obj) Object.keys返回一个数组，包括对象自身的（不含继承的）所有可枚举属性（不含 Symbol 属性）的键名。\nObject.getOwnPropertyNames(obj) Object.getOwnPropertyNames返回一个数组，包含对象自身的所有属性（不含 Symbol 属性，但是包括不可枚举属性）的键名。\nObject.getOwnPropertySymbols(obj) Object.getOwnPropertySymbols返回一个数组，包含对象自身的所有 Symbol 属性的键名。\nReflect.ownKeys(obj) Reflect.ownKeys返回一个数组，包含对象自身的所有键名，不管键名是 Symbol 或字符串，也不管是否可枚举。\n以上的 5 种方法遍历对象的键名，都遵守同样的属性遍历的次序规则。\n首先遍历所有数值键，按照数值升序排列。\n其次遍历所有字符串键，按照加入时间升序排列。\n最后遍历所有 Symbol 键，按照加入时间升序排列。\nReflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) // [\u0026#39;2\u0026#39;, \u0026#39;10\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;a\u0026#39;, Symbol()] 上面代码中，Reflect.ownKeys方法返回一个数组，包含了参数对象的所有属性。这个数组的属性次序是这样的，首先是数值属性2和10，其次是字符串属性b和a，最后是 Symbol 属性。\nObject.getOwnPropetryDescriptions() Object.getOwnPropertyDescriptors返回指定对象的所有自身属性的描述对象\n该方法的引用目的是为了解决Object.assign()无法正确拷贝get属性和set属性的问题\n__proto__属性，Object.setPrototypeOf(),Object.getPrototypeOf() proto proto_，用来读取或设置当前对象的[[prototype]]对象。\n//es6 const …","date":1524528e3,"description":"《ES6标准入门》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"20a8069b5f4b74fee130f7511f55a130","permalink":"https://siqi-liu.com/zh/post/legacy/%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%8B%93%E5%B1%95/","publishdate":"2018-04-24T00:00:00Z","relpermalink":"/zh/post/legacy/%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%8B%93%E5%B1%95/","section":"post","summary":"《ES6标准入门》 知识点整理","tags":["JavaScript","JavaScript"],"title":"对象的拓展","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"迭代器模式 内部迭代器和外部迭代器 内部迭代器 自己实现一个each函数，each函数接受2个参数，第一个为被循环数组，第二个为循环要触发的回调函数。\nvar each = function(arr,callback){ for(var i=0,l=arr.length;i\u0026lt;l;i++){ callback.call(arr[i],i,arr[i])//需要绑定this为被遍历项 } } each([1,2,3],function(i,n){ console.log([i,n]) })//[0,1] [1,2] [2,3] 内部迭代器在调用的时候很方便。外界不用关心迭代器的实现，这也是内部迭代器的缺点，内部的迭代规则已经被限定。\n外部迭代器 外部迭代器必须显式的请求迭代下一个元素\n外部迭代器增加了调用的复杂度，但也增加了迭代器的灵活性，我们可以手工控制迭代的过程或者顺序。\nfunction Iterator(obj) { var current = 0 var next = function() { current += 1 } var isDone = function() { return !(current \u0026lt; obj.length) } var getItem = function() { return obj[current] } return {next, isDone, getItem} } 例子：判断两个数组的值是否完全相等\nvar iterator1 = Iterator([1, 2, 3, 4,5]), iterator2 = Iterator([1, 2, 3, 4]); var compare = function (item1, item2) { while (!item1.isDone() || !item2.isDone()) { console.log(item1.getItem(), item2.getItem()) if (item1.getItem() !== item2.getItem()) { throw new Error(\u0026#34;不相等\u0026#34;) } item1.next() item2.next() } console.log(\u0026#34;相等\u0026#34;) } compare(iterator1, iterator2)//相等 ","date":1524217265,"description":"《Javascript设计模式》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"698927452bd7a676b6cdaa6defb52957","permalink":"https://siqi-liu.com/zh/post/legacy/%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F/","publishdate":"2018-04-20T09:41:05Z","relpermalink":"/zh/post/legacy/%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F/","section":"post","summary":"《Javascript设计模式》 知识点整理","tags":["JavaScript","Design Patterns"],"title":"迭代器模式","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"代理模式 代理模式和图片预加载 先不使用代理模式做一个图片预加载功能。\nvar myImage = (function(){ var imgNode = document.createElement(\u0026#34;img\u0026#34;); document.body.appendChild(imgNode); var img = new Image; img.onload = function(){ imgNode.src = img.src; } return { setSrc:function(src){ imgNode.src = \u0026#34;./bd_logo1.png\u0026#34; img.src = src } } })() myImage.setSrc(\u0026#34;http://data.17jita.com/attachment/forum/201412/20/213333npk8mvppcav3rmv8.png\u0026#34;) 问题是，当我们网速不再需要预加载，那就不得不改动myImage对象。\n同时，myImage除了负责给img节点设置src，还要负责预加载图片，违背了单一职责原则。\n下面使用代理模式实现预加载功能\nvar myImage = (function(){ var imgNode = document.createElement(\u0026#34;img\u0026#34;); document.body.appendChild(imgNode) return { setSrc:function(src){ imgNode.src=src } } })() var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage.setSrc(this.src) } return { setSrc:function(src){ myImage.setSrc(\u0026#34;./bd_logo1.png\u0026#34;) img.src = src; } } })() proxyImage.setSrc(\u0026#34;http://data.17jita.com/attachment/forum/201412/20/213333npk8mvppcav3rmv8.png\u0026#34;) 通过proxyImage间接访问myIname,控制图片的预加载。\n当不需要预加载的时候，只用把对proxyImage的引用改成myImage即可。\n虚拟代理合并http请求 在web开发中，也需最大的开销就是网络请求，假设我们在做一个文件同步功能，在我们选中一个checkbox的时候，它对应的文件就会被同步到另一个服务器上\nvar　synchronousFile = function(id){ console.log(\u0026#34;开始同步文件\u0026#34;+id) } var proxysynchronousFile = (function(){ var cache = [], timer; return function(id){ cache.push(id); if(timer){ retrurn }; timer = setTimeout(function(){ synchronousFile(cache.join(\u0026#34;,\u0026#34;)) clearTimeout(timer); timer = null; cache.length = 0; },2000) } })() var checkbox = document.getElementByTagName(\u0026#34;input\u0026#34;); for(var i=0,c;c = checkbox[i];i++){ c.onclick = function(){ if(this.checked===true){ proxysynchronousFile(this.id) } } } 缓存代理 缓存代理的应用-计算乘积 缓存代理可以为运算结果提供暂时的存储，在下次运算时，如果传毒的参数和之前一致，则可以直接返回前面储存的计算结果\nvar mult = function(...numbers){//es6 rest参数 var a= 1; for(var i=0,number;number = numbers[i];i++){ a*=number } return a } mult(2,3)//6 加入缓存代理\nvar proxyMult = (function(){ var chache = {}; return function(...numbers){ var args = numbers.join(\u0026#39;,\u0026#39;) if(chache[args]){ return chache[args] } return chache[args] = mult(...numbers)//es6函数拓展运算符，rest参数逆运算 } })( ) proxyMult(2,3)//6 缓存代理的应用-ajax异步请求资源 在项目中遇到的分页需求，同一页面理论上只需要后台取得一次，下次在请求同一页时，可以直接使用之前的缓存 也可以引用缓存代理，和计算乘积不同的地方在于请求数据是异步操作无法直接把结果放入缓存中，要使用异步的回调。\nvar chache = {}; var request = function(num){ ajax(num)//假装是个ajax请求数据的操作 .success(function(res){ render(res.data)//假装是个渲染操作 chache[num] = res.data }) } var getPage = function(num){ request(num) } var proxyPage = function(){ return function(num){ if(chache[num]){ return chache[num] } getPage(num) } } 用高阶函数动态创建代理 可以将计算函数作为参数传入创建缓存的代理中，这样就可以为乘法加减法统一创建代理\n//乘法 var mult = (...args)=\u0026gt;{ var a = 1 ; for(var i=0,c;c=args[i];i++){ a *=c } return a } //加法 var plus = (...args)=\u0026gt;{ var a = 0; for(var i=0,c;c=args[i];i++){ a+=c } return a } //代理工厂 var createProxyFactory = (fn)=\u0026gt;{ var chache = {}; return function(...args){ var arg = args.join(\u0026#34;,\u0026#34;); if(arg in chache){ return chache[arg] } return chache[arg] = fn(...args) } }; var proxyMult = createProxyFactory(mult), proxyPlus = createProxyFactory(plus); proxyMult(1,2,3,4)//24 proxyPlus(1,2,3,4)//10 小结 代理模式的关键是，当不方便直接访问一个对象或者不满足需要的时候，提供一个替身对象控制对这个对象的访问。替身对象对请求做出一些处理后，再转交给本体\n在javascript开发中最常用的代理模式是虚拟代理和缓存代理。\n虚拟代理把开销大的对象延迟到真正需要的时候创建。（imgNode在img onload结束之后再设置其src）\n缓存代理为一些开销大的运算结果提供暂时的存储。（将结果用chache存储）\n编写业务代码的时候，往往不需要去预先猜测是否需要使用，当不方便直接访问某个对象的时候，再编写代理不迟\n","date":1524131171,"description":"《Javascript设计模式》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"38834d9fa12f4f0efb1fc207928b6457","permalink":"https://siqi-liu.com/zh/post/legacy/%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F/","publishdate":"2018-04-19T09:46:11Z","relpermalink":"/zh/post/legacy/%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F/","section":"post","summary":"《Javascript设计模式》 知识点整理","tags":["JavaScript","Design Patterns"],"title":"代理模式","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"单例模式 简单的单例模式实现 需要用一个变量来标记是否已经创建过一个实例对象\nvar Single = function(name){ this.name = name; this.instance = null;\t} Single.getInstance = function(name){ if(!this.instance){ this.instace = new Single(name) } return this.instance } var a = Single.getInstance(\u0026#34;a\u0026#34;); var b = Single.getInstance(\u0026#34;b\u0026#34;); a===b //true 缺点:不透明，相比于正常的new创建实例，创建单例必须用getInstance方法.\n透明的单例模式 目标是实现一个透明的单例类，下面将使用CreateDiv单例类，在页面创建唯一的div节点\nvar CreateDiv = (function(){ var instance = null;//自执行函数保证instance只初始化一次 function CreateDiv(html){ if(instance){ return instance } this.html = html; // this.init(); return instance = this; } CreateDiv.prototype.init = function(){ var div = document.createElement(\u0026#34;div\u0026#34;); div.innerHTML = this.html; document.body.append(div); } return CreateDiv })() var a = new CreateDiv(\u0026#34;a\u0026#34;); var b = new CreateDiv(\u0026#34;b\u0026#34;); a===b//true 为了将instance封装起来使用了自执行匿名函数和闭包 增加了复杂性 并且CreateDiv负责了两件事 第一是创建对象和执行init方法 第二是保证只有一个对象 。违背了单一职责原则\n用代理实现单例模式 为了解决上述的单一职责问题 下面引入代理模式实现这个单例模式 首先把创建对象和执行init方法分离出来\nfunction CreateDiv(html){ this.html = html; this.init(); } CreateDiv.prototype.init = function(html){ var div = document.createElement(\u0026#34;div\u0026#34;); div.innerHTML = this.html; document.body.append(div); } 接下来引入代理类\nvar createDivProx = (function(){ var instance; return function(html){ if(!instance){ instance = new CreateDiv(html) } return instance } })() var a = new createDivProx(\u0026#34;a\u0026#34;) var b= new createDivProx(\u0026#34;b\u0026#34;) a==b//true Js的单例模式-使用闭包封装的私有变量 既然我们需要的是一个对象 那就没必要先创建一个类\n为了避免全局变量的滥用，可以把变量封装在闭包内部 只暴露接口和外界通信\nvar user = (function(){ var _name = \u0026#34;yosgi\u0026#34; var _age = 29; return { getUserInfo(){ return _name+\u0026#34;-\u0026#34;+_age } } })() 或者使用es6的Symbol\nvar _name = Symbol.for(\u0026#39;name\u0026#39;); var _age = Symbol.for(\u0026#39;age\u0026#39;); window[_name] = \u0026#34;yosgi\u0026#34; window[_age] = 29//_name 和_user不会被无意中覆盖 完成一个惰性单例 惰性单例指在需要的时候才创建实例对象。\n下面完成一个需求是 当点击login按钮时被创建的div框(以后也有可能是iframe框)。\nvar createLoginLayer = (function () { var div; return function () { if (!div) { div = document.createElement(\u0026#34;div\u0026#34;); div.style.display = \u0026#34;none\u0026#34;; document.body.appendChild(div); } return div } })() document.getElementById(\u0026#34;login\u0026#34;).onclick = function () { var LoginLayer = createLoginLayer() LoginLayer.style.display = \u0026#39;block\u0026#39;; }; 为了解决单一职责的问题，将维护单一实例的代码提取出来\n//getSingle用来管理单例 var getSingle = (function(){ var result; return function(fn){ if(!result){ //闭包使result可以保存fn的结果 result = fn.apply(this,arguments) } return result } })() //createSingleIframe用来创建实例 var createSingleIframe = function(){ iframe = document.createElement(\u0026#34;iframe\u0026#34;); iframe.style.display = \u0026#34;none\u0026#34;; document.body.appendChild(iframe); return iframe } document.getElementById(\u0026#34;login\u0026#34;).onclick = function(){ var Iframe = getSingle(createSingleIframe) Iframe.style.display = \u0026#39;block\u0026#39;; Iframe.src=\u0026#34;http://baidu.com\u0026#34; }; 单例模式的应用 在渲染页面中的一个列表之后，绑定addEventListener click 事件 ，如果是通过ajax动态添加数据，实际上只需要在第一次渲染绑定一次，如果不想判断是第几次渲染 可以利用getSingle\nvar getSingle = (function(){ var result; return function(fn){ if(!result){ result = fn.apply(this, arguments) } return result } })() var bindEvent = function(){ document.getElementById(\u0026#34;div1\u0026#34;).addEventListener(\u0026#34;click\u0026#34;,()=\u0026gt;{ console.log(\u0026#34;click\u0026#34;) }) return true } var render = function() { getSingle(bindEvent) } render() render() render() 可以注意到，点击div1多次也只会触发一次点击事件(当然也可以使用onclick，一个click在同一时间只能指向唯一的对象 这点我以前没注意)\n","date":1523649990,"description":"《Javascript设计模式》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"a4464264ed94fd758d70fff6baebdec6","permalink":"https://siqi-liu.com/zh/post/legacy/js%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","publishdate":"2018-04-13T20:06:30Z","relpermalink":"/zh/post/legacy/js%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","section":"post","summary":"《Javascript设计模式》 知识点整理","tags":["JavaScript","Design Patterns"],"title":"Js单例模式","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"函数的拓展 函数参数的默认值 ES5\nfunction log(x,y){ if(typeof y===\u0026#39;undefined\u0026#39;){ y=\u0026#39;world\u0026#39; }; console.log(x,y) } log(\u0026#34;hello\u0026#34;)//hello world ES6\nfunction log(x,y=\u0026#39;world\u0026#39;){ console.log(x,y) } 通常情况下,定义了默认值的参数应该是函数的尾参数，这样比较容易看出到底省略了哪些参数。\n函数的length属性 指定了默认值以后，函数的length属性将返回没有指定默认值的参数个数。length的含义是函数与其传入的参数个数，同理，rest参数不会计入length属性\n(function(a){}).length //1 (function(a=5){}).length //0 (function(...args){}).length//0 应用 利用参数默认值，可以指定某一个参数不可省略，省略则抛出错误\nfunction throwIfMissing(){ throw new Error(\u0026#39;Missing Params\u0026#39;) } function foo(mustBeProvided = throwIfMissing()){ return mustBeProvided; } foo() // Uncaught Error: Missing Params rest参数 rest参数用于获取函数的多余参数，用来替代arguments对象\nfunction add(...values){ let sum = 0; for(var val of values){ sum+=val } return sum } add(1,2,3)//6 es5 arguments\nvar sortNumbers = function(){ return Array.prototype.slice.call(arguments).sort()//slice可以转化类数组对象 } es6 rest\nconst sortNumbers=(...numbers)=\u0026gt;numbers.sort() sortNumbers(3,7,4,5)//[3,4,5,7] 拓展运算符 拓展运算符是三个点(…)。是rest参数的逆运算，将一个数组转为用逗号分隔的参数序列；\nconsole.log(1,...[1,2,3],4) //1 1 2 3 4 该运算符主要用于函数调用\nfunction push(array,...items){ array.push(items) } push([1,2,3],4,5)//1,2,3,4,5 function add(x,y){ return x+y } var numbers = [4,38]; add(...numbers);//42 替代数组的apply的写法 var arr1 = [0,1,2] var arr2 = [3,4,5] es5\nArray.prototype.push.apply(arr1,arr2)//[1,2,3,4,5] es6\narr1.push(...arr2)//[1,2,3,4,5] 合并数组的新写法 es5\n[1,2].concat(more) es6\n[1,2,...more] 与解构赋值结合生成数组 es5\na = list[0],rest = list.slice(1) es6\n[a,...rest] = list 如果将拓展运算符用于数组赋值，只能放在参数的最后一位，否则会报错。\n转化类数组对象 var nodelist = document.querySelectorAll(\u0026#34;div\u0026#34;); var array= [...nodelisst]; Map和Set解构,Gennerator函数\n[...map.keys()] [...generator()] name属性 函数的name属性返回该函数的函数名\nfunction foo(){} foo.name //\u0026#34;foo\u0026#34; var func1 = function(){}//\u0026#34;func1\u0026#34; (new Function).name //\u0026#34;anonymous\u0026#34; 箭头函数 es5\nvar f = function(v){ return v } es6\nvar f = v =\u0026gt; v 注意： 1.箭头函数没有this,内部的this就是外部代码块的this,因为没有this所以不能用作构造函数\n2.箭头函数不能使用arguments对象，可以使用rest参数代替\n3.箭头函数不能用作Generator函数\nes5\nfunction foo(){ this.id = 1; setTimeout(function(){ console.log(this.id)//undefined this指向window },1000) } foo.call({id:1}) es6\nfunction foo(){ setTimeout(()=\u0026gt;{ console.log(this.id)//1 指向调用的对象 },1000) } foo.call({id:1}) 或者可以改写为 function foo(){ var _this = this; setTimeout(function(){ console.log(_this.id) },1000) } 除了this之外，以下3个变量在箭头函数中也不存在，分别指向外层函数的变量\narguments super new.target\n也不能用call(),apply(),bind()来改变this的指向\n函数绑定 函数绑定运算符是(::)左边对象右边函数。运算符会自动将左边的对象作为上下文环境（this）绑定到右边的函数上。\nfoo::bar; //等同于 bar.bind(foo) foo::bar(...arguments) //等同于 bar.apply(foo,arguments) 尾调用优化 尾递归 尾调用是某个函数的最后一步是调用另一个函数\nfunction f(x){ return g(x) } 尾调用不一定出现在函数尾部。只要是最后一步操作即可\nfunction f(x){ if(x\u0026gt;0){ return m(x) } return n(x) }//m n 都属于尾调用 函数调用会在内存形成调用帧，保存调用位置和内部变量等信息。如果在函数A的内部调用函数B，那么A的调用帧上方还会形成一个B的调用帧，等到B运行结束将结果返回到A，调用帧才会消失。\n所有的调用帧形成一个调用栈。\n尾调用由于是函数的最后一步操作，所以不需要保留外层函数的调用帧，因为调用位置，内存变量等信息都不会再用到了。可以直接用内存的调用帧取代外层函数的调用帧。\n只有不再用到外层函数的内部变量，内层函数的调用帧才会取代外层函数的调用帧。否则无法进行尾调用优化。\n//递归函数的改写 function factorial(n){ if(n===1) return 1; return n*factorial(n-1) } factorial(6) //720 function factorial(n,total=1){ if(n===1) return total; return factorial(n-1,total * n) } factorial(6) //720 ","date":1523491200,"description":"《ES6标准入门》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"95f55fcf5ec469bfe776e66fef29d449","permalink":"https://siqi-liu.com/zh/post/legacy/%E5%87%BD%E6%95%B0%E7%9A%84%E6%8B%93%E5%B1%95/","publishdate":"2018-04-12T00:00:00Z","relpermalink":"/zh/post/legacy/%E5%87%BD%E6%95%B0%E7%9A%84%E6%8B%93%E5%B1%95/","section":"post","summary":"《ES6标准入门》 知识点整理","tags":["JavaScript","JavaScript"],"title":"函数的拓展","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"数组的拓展 Array.from() 将似数组对象(array-like object) 和可遍历例对象(iterable)转为真正的数组\nES5\nvar arr1 = [].slice.call(arraylike) ES6\nlet arr2 = Array.from(arraylike) 需要注意的是这里是浅拷贝(如果元素是对象引用，则拷贝对象引用到新数组)。\nArray.from()可以接受第二个参数 类似于数组的map方法\nlet names1 = Array.prototype.map.call(spans,s=\u0026gt;s.textContent); let names2 = Array.from(spans,s=\u0026gt;s.textContent,this); 拓展运算符(…)也可以将某些数据结构转为数组\nfunction foo(){ var args = [...arguments] } Array.of() 用于将一组值转换为数组，可以替代Array()或者new Array()\nES5\nfunction ArrayOf(){ return [].slice.call(arguments) } ArrayOf(3,11,8)//[3,11,8] ES6\nArray.of(3,11,8)//[3,11,8] copyWithin() 在当前数组内部将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组\nES5\nvar i32a = new Int32Array([1, 2, 3, 4, 5]); i32a.copyWithin(0, 2); // Int32Array [3, 4, 5, 4, 5] ES6\nlet a = [1,2,3,4,5]; a.copyWithin(0,2) // [3, 4, 5, 4, 5] [].copyWithin.call({length: 5, 3: 1}, 0, 3);//类数组对象 // {0: 1, 3: 1, length: 5} find()和findIndex() 用于找出第一个符合条件的数组成员，弥补IndexOf不能发现NaN的不足\nES5\n[1,2,NAN].indexOf(NaN) //-1 ES6\n[1,2,NAN].find(Y=\u0026gt;Object.is(NaN,y)) //2 fill() 使用给定值填充数组，数组中已有元素会被抹去；如果有第二个和第三个参数，可以指定填充的起始和结束位置\n[\u0026#39;a\u0026#39;,\u0026#39;b\u0026#39;,\u0026#39;c\u0026#39;].fill(7,1,2) //[a,7,c] entries() keys() values() 用于遍历数组,它们都返回一个遍历器对象\nfor(let index of [\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;].keys()){ cobsole.log(index) }//0 1 for(let [index,ele] of [\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;].entries()){ console.log(index,ele) }//0 \u0026#34;a\u0026#34; 1 \u0026#34;b\u0026#34; ","date":1523404800,"description":"《ES6标准入门》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"e7d4574d57576ce1b56bed8b890c0789","permalink":"https://siqi-liu.com/zh/post/legacy/%E6%95%B0%E7%BB%84%E7%9A%84%E6%8B%93%E5%B1%95/","publishdate":"2018-04-11T00:00:00Z","relpermalink":"/zh/post/legacy/%E6%95%B0%E7%BB%84%E7%9A%84%E6%8B%93%E5%B1%95/","section":"post","summary":"《ES6标准入门》 知识点整理","tags":["JavaScript","JavaScript"],"title":"数组的拓展","tldr":null,"type":"post"},{"authors":null,"categories":["JavaScript"],"content":"变量的解构赋值 定义:允许按照一定的模式，从数组的对象中提取值\n数组的解构赋值 var [foo,bar] = [1,2]//foo 1 bar 2 解构赋值可以指定默认值,如果默认值是表达式,那么这个表达式的求值是惰性的\nvar [foo = true] = [] //foo true var f = ()=\u0026gt; 1 var [foo = f()] = [null]//foo null var [foo = f()] = [undefined]//foo 1 对象的解构赋值 对象的结构赋值与数组的不同在于，变量必须与属性同名\nvar {bar,foo} = {foo:\u0026#34;aaa\u0026#34;,bar:\u0026#34;bbb\u0026#34;} //or ({bar,foo}) = {foo:\u0026#34;aaa\u0026#34;,bar:\u0026#34;bbb\u0026#34;} tips: (…)是使用对象字面量解构赋值时不需要声明的语法。但是前面的语句需要有分号以免被当做函数\n对象的解构赋值可以方便的将现有对象的方法赋值到某个变量\nlet {log,sin,cos} = Math 函数参数的解构赋值 [[1,2],[3,4]].map(([a,b])=\u0026gt;a+b); 函数参数的解构也可以使用默认值\nfunction move({x=0,y=0}={}){ return [x,y] }; move({x:3,y:8}) //[3,8] 如果去掉={} 在执行move()时将会报错\n解构赋值的用途 从函数返回多个值\nfunction foo(){ return [1,2,3] } var [a,b,c] = foo() 函数参数的定义\nfunction f({x,y,z}){} f({z:3,x:1,y:2}) 提取json\nvar json = { id:42, status:\u0026#34;ok\u0026#34;, data:[1,2,3,4,] } let {id,status,data} = json 遍历map解构\nfor(let [key,value] of map){}; 获取键\nfor(let [key] of map){}; 只想获取值\nfor(let [,value] of map){}; 输入模块的指定方法\n","date":1523358454,"description":"《ES6标准入门》 知识点整理","expirydate":-62135596800,"keywords":null,"kind":"page","lang":"zh","lastmod":1781238520,"objectID":"d22b5886e7db9a491d41d8fbf5444098","permalink":"https://siqi-liu.com/zh/post/legacy/%E5%8F%98%E9%87%8F%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC/","publishdate":"2018-04-10T11:07:34Z","relpermalink":"/zh/post/legacy/%E5%8F%98%E9%87%8F%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC/","section":"post","summary":"《ES6标准入门》 知识点整理","tags":["JavaScript","JavaScript"],"title":"变量的解构赋值","tldr":null,"type":"post"}]